906 lines
34 KiB
Python
906 lines
34 KiB
Python
"""
|
||
智能盯盘 - UI界面
|
||
集成到主程序的智能盯盘功能界面
|
||
"""
|
||
|
||
import streamlit as st
|
||
import pandas as pd
|
||
from datetime import datetime
|
||
import logging
|
||
import os
|
||
from typing import Dict
|
||
from dotenv import load_dotenv
|
||
|
||
from smart_monitor_engine import SmartMonitorEngine
|
||
from smart_monitor_db import SmartMonitorDB
|
||
from config_manager import config_manager # 使用主程序的配置管理器
|
||
|
||
|
||
# 加载环境变量
|
||
load_dotenv()
|
||
|
||
|
||
def smart_monitor_ui():
|
||
"""AI盯盘主界面"""
|
||
|
||
st.title("🤖 AI盯盘 - AI决策交易系统")
|
||
st.caption("参照AlphaArena项目,基于DeepSeek AI的A股自动化交易系统")
|
||
|
||
# 使用说明
|
||
with st.expander("📖 快速使用指南", expanded=False):
|
||
st.markdown("""
|
||
### 🚀 快速开始
|
||
|
||
**第一步:环境配置**
|
||
1. 点击左侧菜单"⚙️ 环境配置"
|
||
2. 填写 DeepSeek API Key(必需)
|
||
3. 配置 miniQMT 账户(可选,用于实盘交易)
|
||
4. 配置通知方式(可选,邮件/Webhook)
|
||
|
||
**第二步:开始使用**
|
||
- **实时分析**:输入股票代码,AI即时分析并给出交易建议
|
||
- **监控任务**:添加股票到监控列表,定时自动分析
|
||
- **持仓管理**:查看和管理当前持仓(已持仓股票可直接监控)
|
||
|
||
---
|
||
|
||
### 💡 核心功能
|
||
|
||
| 功能 | 说明 |
|
||
|------|------|
|
||
| 📊 **实时分析** | 输入股票代码,AI分析市场数据并给出买入/卖出/持有建议 |
|
||
| 🎯 **监控任务** | 定时自动分析目标股票,可设置自动交易 |
|
||
| 📈 **持仓管理** | 记录持仓成本,实时显示盈亏,AI决策考虑持仓情况 |
|
||
| 📜 **历史记录** | 查看所有AI决策历史、交易记录和通知记录 |
|
||
| ⚙️ **系统设置** | 配置API、交易方式(实盘/模拟)、通知等 |
|
||
|
||
---
|
||
|
||
### 🎯 AI决策逻辑
|
||
|
||
**买入信号**(至少满足3个):
|
||
1. ✅ 趋势向上:价格 > MA5 > MA20 > MA60(多头排列)
|
||
2. ✅ 量价配合:成交量 > 5日均量的120%(放量上涨)
|
||
3. ✅ MACD金叉:MACD > 0 且DIF上穿DEA
|
||
4. ✅ RSI健康:RSI在50-70区间(不超买不超卖)
|
||
5. ✅ 突破关键位:突破前期高点或重要阻力位
|
||
6. ✅ 布林带位置:价格接近布林中轨上方,有上行空间
|
||
|
||
**卖出信号**(满足任一立即卖出):
|
||
1. 🔴 止损触发:亏损 ≥ -5%(明天开盘立即卖出)
|
||
2. 🟢 止盈触发:盈利 ≥ +10%(锁定收益)
|
||
3. 🔴 趋势转弱:跌破MA20/MA60,MACD死叉
|
||
4. 🔴 放量下跌:成交量放大但价格下跌
|
||
5. 🔴 技术破位:跌破重要支撑位
|
||
|
||
---
|
||
|
||
### ⚠️ A股T+1规则
|
||
|
||
**关键限制**:
|
||
- 今天买入的股票,**今天不能卖出**
|
||
- 必须等到下一个交易日才能卖出
|
||
- 系统会自动检查并遵守T+1规则
|
||
|
||
**建议**:
|
||
- **宁可错过,不可做错** - 买入前务必确认趋势
|
||
- 单只股票仓位 ≤ 30%(T+1风险较大)
|
||
- 止损位:-5%(明天开盘立即执行)
|
||
- 止盈位:+8-15%(分批止盈)
|
||
|
||
---
|
||
|
||
### 🔧 使用技巧
|
||
|
||
**新手建议**:
|
||
1. 先使用"模拟交易"模式测试
|
||
2. 小仓位试水(建议5-10%)
|
||
3. 严格执行止损,不要心存侥幸
|
||
4. 关注交易时段(9:30-11:30, 13:00-15:00)
|
||
|
||
**高级功能**:
|
||
- 在"监控任务"中勾选"已持仓",填入成本价
|
||
- AI会考虑当前盈亏情况给出更准确的建议
|
||
- 可设置多个监控任务,同时盯盘多只股票
|
||
|
||
---
|
||
|
||
### 📞 常见问题
|
||
|
||
**Q: 提示"DeepSeek API调用失败"?**
|
||
- 检查API Key是否正确
|
||
- 确认API账户余额充足
|
||
- 检查网络连接
|
||
|
||
**Q: 数据显示为0或获取失败?**
|
||
- 可能是非交易时间
|
||
- AKShare接口可能暂时不可用
|
||
- 尝试更换股票代码测试
|
||
|
||
**Q: 想实盘交易如何操作?**
|
||
1. 下载并安装 [miniQMT](https://www.xtp-mini.com/)
|
||
2. 启动miniQMT客户端并登录
|
||
3. 在"系统设置"中填写账户ID
|
||
4. 取消勾选"使用模拟交易"
|
||
|
||
---
|
||
|
||
### ⚠️ 风险提示
|
||
|
||
1. **股市有风险,投资需谨慎**
|
||
2. AI决策仅供参考,不构成投资建议
|
||
3. 建议先使用模拟交易充分测试
|
||
4. 严格控制仓位,不要满仓操作
|
||
5. 不要投入超过承受能力的资金
|
||
|
||
---
|
||
|
||
**🎉 祝您交易顺利!如有问题,请查看详细文档或联系技术支持。**
|
||
""")
|
||
|
||
st.markdown("---")
|
||
|
||
# 初始化组件(自动从配置读取)
|
||
if 'engine' not in st.session_state:
|
||
try:
|
||
# SmartMonitorEngine会自动从config_manager读取配置
|
||
st.session_state.engine = SmartMonitorEngine()
|
||
st.session_state.db = SmartMonitorDB()
|
||
except Exception as e:
|
||
st.error(f"初始化失败: {e}")
|
||
st.error("请先在'环境配置'中完成基础配置")
|
||
return
|
||
|
||
# 创建标签页
|
||
tabs = st.tabs([
|
||
"📊 实时分析",
|
||
"🎯 监控任务",
|
||
"📈 持仓管理",
|
||
"📜 历史记录",
|
||
"⚙️ 系统设置"
|
||
])
|
||
|
||
# 标签页1: 实时分析
|
||
with tabs[0]:
|
||
render_realtime_analysis()
|
||
|
||
# 标签页2: 监控任务
|
||
with tabs[1]:
|
||
render_monitor_tasks()
|
||
|
||
# 标签页3: 持仓管理
|
||
with tabs[2]:
|
||
render_position_management()
|
||
|
||
# 标签页4: 历史记录
|
||
with tabs[3]:
|
||
render_history()
|
||
|
||
# 标签页5: 系统设置
|
||
with tabs[4]:
|
||
render_settings()
|
||
|
||
|
||
def render_realtime_analysis():
|
||
"""实时分析界面"""
|
||
|
||
st.header("📊 实时分析")
|
||
|
||
col1, col2 = st.columns([2, 1])
|
||
|
||
with col1:
|
||
stock_code = st.text_input(
|
||
"输入股票代码",
|
||
placeholder="例如: 600519",
|
||
help="输入6位股票代码"
|
||
)
|
||
|
||
with col2:
|
||
auto_trade = st.checkbox("自动交易", value=False,
|
||
help="开启后AI会自动执行交易决策")
|
||
|
||
if st.button("🔍 开始分析", type="primary"):
|
||
if not stock_code:
|
||
st.error("请输入股票代码")
|
||
return
|
||
|
||
if len(stock_code) != 6 or not stock_code.isdigit():
|
||
st.error("股票代码格式错误,请输入6位数字")
|
||
return
|
||
|
||
# 显示进度
|
||
with st.spinner('正在分析...'):
|
||
engine = st.session_state.engine
|
||
result = engine.analyze_stock(
|
||
stock_code=stock_code,
|
||
auto_trade=auto_trade,
|
||
notify=True
|
||
)
|
||
|
||
if result['success']:
|
||
# 显示分析结果
|
||
display_analysis_result(result)
|
||
else:
|
||
st.error(f"分析失败: {result.get('error')}")
|
||
|
||
|
||
def display_analysis_result(result: dict):
|
||
"""显示分析结果"""
|
||
|
||
stock_code = result['stock_code']
|
||
stock_name = result['stock_name']
|
||
decision = result['decision']
|
||
market_data = result['market_data']
|
||
session_info = result['session_info']
|
||
|
||
st.success(f"✅ 分析完成: {stock_code} {stock_name}")
|
||
|
||
# 交易时段信息
|
||
st.info(f"⏰ 当前时段: {session_info['session']} - {session_info['recommendation']}")
|
||
|
||
# AI决策
|
||
st.markdown("### 🤖 AI决策")
|
||
|
||
col1, col2, col3, col4 = st.columns(4)
|
||
|
||
# 决策动作
|
||
action = decision['action']
|
||
action_emoji = {"BUY": "📈", "SELL": "📉", "HOLD": "⏸️"}
|
||
action_color = {"BUY": "green", "SELL": "red", "HOLD": "gray"}
|
||
|
||
col1.metric("决策", action, delta=None)
|
||
col2.metric("信心度", f"{decision['confidence']}%")
|
||
col3.metric("风险等级", decision.get('risk_level', 'N/A'))
|
||
col4.metric("建议仓位", f"{decision.get('position_size_pct', 0)}%")
|
||
|
||
# 决策理由
|
||
st.markdown("**决策理由:**")
|
||
st.text_area("决策理由", decision['reasoning'], height=150, disabled=True, label_visibility="hidden")
|
||
|
||
# 市场数据
|
||
st.markdown("### 📊 市场数据")
|
||
|
||
col1, col2, col3, col4 = st.columns(4)
|
||
col1.metric("当前价", f"¥{market_data.get('current_price', 0):.2f}")
|
||
col2.metric("涨跌幅", f"{market_data.get('change_pct', 0):+.2f}%")
|
||
col3.metric("成交量", f"{market_data.get('volume', 0):,.0f}手")
|
||
col4.metric("换手率", f"{market_data.get('turnover_rate', 0):.2f}%")
|
||
|
||
# 技术指标
|
||
st.markdown("### 📈 技术指标")
|
||
|
||
tech_col1, tech_col2, tech_col3 = st.columns(3)
|
||
|
||
with tech_col1:
|
||
st.markdown("**均线系统**")
|
||
st.write(f"MA5: ¥{market_data.get('ma5', 0):.2f}")
|
||
st.write(f"MA20: ¥{market_data.get('ma20', 0):.2f}")
|
||
st.write(f"MA60: ¥{market_data.get('ma60', 0):.2f}")
|
||
st.write(f"趋势: {market_data.get('trend', 'N/A')}")
|
||
|
||
with tech_col2:
|
||
st.markdown("**动量指标**")
|
||
st.write(f"MACD: {market_data.get('macd', 0):.4f}")
|
||
st.write(f"DIF: {market_data.get('macd_dif', 0):.4f}")
|
||
st.write(f"DEA: {market_data.get('macd_dea', 0):.4f}")
|
||
|
||
with tech_col3:
|
||
st.markdown("**摆动指标**")
|
||
st.write(f"RSI(6): {market_data.get('rsi6', 0):.2f}")
|
||
st.write(f"RSI(12): {market_data.get('rsi12', 0):.2f}")
|
||
st.write(f"RSI(24): {market_data.get('rsi24', 0):.2f}")
|
||
|
||
# 主力资金(已禁用 - 接口不稳定)
|
||
# if 'main_force' in market_data:
|
||
# st.markdown("### 💰 主力资金")
|
||
# mf = market_data['main_force']
|
||
#
|
||
# mf_col1, mf_col2, mf_col3 = st.columns(3)
|
||
# mf_col1.metric("主力净额", f"{mf['main_net']:,.2f}万",
|
||
# delta=f"{mf['main_net_pct']:+.2f}%")
|
||
# mf_col2.metric("超大单", f"{mf['super_net']:,.2f}万")
|
||
# mf_col3.metric("大单", f"{mf['big_net']:,.2f}万")
|
||
#
|
||
# st.info(f"主力动向: {mf['trend']}")
|
||
|
||
# 执行结果(如果有)
|
||
if result.get('execution_result'):
|
||
exec_result = result['execution_result']
|
||
st.markdown("### ⚡ 执行结果")
|
||
|
||
if exec_result.get('success'):
|
||
st.success(f"✅ {exec_result.get('message', '执行成功')}")
|
||
else:
|
||
st.error(f"❌ {exec_result.get('error', '执行失败')}")
|
||
|
||
|
||
def render_monitor_tasks():
|
||
"""监控任务界面"""
|
||
|
||
st.header("🎯 监控任务管理")
|
||
|
||
db = st.session_state.db
|
||
engine = st.session_state.engine
|
||
|
||
# 添加新任务
|
||
with st.expander("➕ 添加新监控任务", expanded=True):
|
||
# 改回使用form,确保值正确提交
|
||
with st.form("add_monitor_task_form", clear_on_submit=False):
|
||
col1, col2 = st.columns(2)
|
||
|
||
with col1:
|
||
task_name = st.text_input("任务名称", placeholder="例如: 茅台盯盘")
|
||
stock_code = st.text_input("股票代码", placeholder="例如: 600519")
|
||
check_interval = st.slider("检查间隔(秒)", 60, 3600, 300)
|
||
|
||
# 持仓信息
|
||
st.markdown("---")
|
||
st.markdown("**📊 持仓信息**")
|
||
has_position = st.checkbox("已持仓该股票", value=False,
|
||
help="勾选后可填写持仓成本和数量,AI会考虑持仓情况")
|
||
|
||
# 注意:在form内部,复选框的变化要到提交后才能看到
|
||
# 所以持仓输入框始终显示,用户可以选择填写或不填写
|
||
position_cost = st.number_input("持仓成本(元)", min_value=0.01, value=10.0, step=0.01,
|
||
help="如果已持仓,填写买入时的成本价格(未持仓可忽略)")
|
||
position_quantity = st.number_input("持仓数量(股)", min_value=100, value=100, step=100,
|
||
help="如果已持仓,填写持有的股票数量(未持仓可忽略)")
|
||
|
||
with col2:
|
||
auto_trade = st.checkbox("自动交易", value=False,
|
||
help="AI决策后自动执行交易")
|
||
trading_hours_only = st.checkbox(
|
||
"仅交易时段监控",
|
||
value=True,
|
||
help="开启后,只在交易日的交易时段(9:30-11:30, 13:00-15:00)进行AI分析"
|
||
)
|
||
position_size = st.slider("仓位百分比(%)", 5, 50, 20,
|
||
help="新建仓位时使用的资金比例")
|
||
notify_email = st.text_input("通知邮箱(可选)")
|
||
|
||
# 添加任务按钮(表单提交按钮)
|
||
submitted = st.form_submit_button("➕ 添加任务", type="primary", width='stretch')
|
||
|
||
if submitted:
|
||
# 验证必填项(form中直接使用局部变量)
|
||
if not task_name or not stock_code:
|
||
st.error("❌ 请填写必填项:任务名称和股票代码")
|
||
else:
|
||
|
||
try:
|
||
# 检查是否已存在该股票的监控任务
|
||
existing_tasks = db.get_monitor_tasks(enabled_only=False)
|
||
existing_task = next((t for t in existing_tasks if t['stock_code'] == stock_code), None)
|
||
|
||
if existing_task:
|
||
st.error(f"❌ 股票代码 {stock_code} 已存在监控任务!")
|
||
st.warning(f"任务名称: {existing_task['task_name']}")
|
||
st.info("💡 请在下方任务列表中找到该任务,点击启动或删除后重新添加")
|
||
else:
|
||
# 创建任务(初始状态为禁用,需要用户手动启动)
|
||
task_data = {
|
||
'task_name': task_name,
|
||
'stock_code': stock_code,
|
||
'enabled': 0, # 关键修改:初始状态为禁用,不自动启动
|
||
'check_interval': check_interval,
|
||
'auto_trade': 1 if auto_trade else 0,
|
||
'trading_hours_only': 1 if trading_hours_only else 0,
|
||
'position_size_pct': position_size,
|
||
'notify_email': notify_email,
|
||
'has_position': 1 if has_position else 0,
|
||
'position_cost': position_cost if has_position else 0,
|
||
'position_quantity': position_quantity if has_position else 0,
|
||
'position_date': datetime.now().strftime('%Y-%m-%d') if has_position else None
|
||
}
|
||
|
||
task_id = db.add_monitor_task(task_data)
|
||
|
||
st.success(f"✅ 任务创建成功! ID: {task_id}")
|
||
if has_position:
|
||
st.info(f"📊 已记录持仓: {position_quantity}股 @ {position_cost:.2f}元")
|
||
st.info("💡 任务已创建但未启动,请在下方任务列表中点击'▶️ 启动'按钮开始监控")
|
||
|
||
st.rerun()
|
||
except Exception as e:
|
||
error_msg = str(e)
|
||
if "UNIQUE constraint failed" in error_msg:
|
||
st.error(f"❌ 股票代码 {stock_code} 已存在监控任务!")
|
||
st.info("💡 请在下方任务列表中找到该任务")
|
||
else:
|
||
st.error(f"创建失败: {error_msg}")
|
||
|
||
# 显示任务列表
|
||
st.markdown("### 📋 监控任务列表")
|
||
|
||
tasks = db.get_monitor_tasks(enabled_only=False)
|
||
|
||
if not tasks:
|
||
st.info("暂无监控任务,点击上方'添加新监控任务'创建")
|
||
return
|
||
|
||
for task in tasks:
|
||
with st.container():
|
||
# 获取实时价格计算盈亏
|
||
has_position = task.get('has_position', 0)
|
||
position_cost = task.get('position_cost', 0)
|
||
position_quantity = task.get('position_quantity', 0)
|
||
|
||
# 尝试获取当前价格
|
||
current_price = 0
|
||
profit_loss = 0
|
||
profit_loss_pct = 0
|
||
|
||
if has_position and position_cost > 0 and position_quantity > 0:
|
||
try:
|
||
# 获取实时行情
|
||
from smart_monitor_data import SmartMonitorDataFetcher
|
||
data_fetcher = SmartMonitorDataFetcher()
|
||
quote = data_fetcher.get_realtime_quote(task['stock_code'], retry=1)
|
||
if quote:
|
||
current_price = quote.get('current_price', 0)
|
||
if current_price > 0:
|
||
# 计算盈亏
|
||
cost_total = position_cost * position_quantity
|
||
current_total = current_price * position_quantity
|
||
profit_loss = current_total - cost_total
|
||
profit_loss_pct = (profit_loss / cost_total) * 100
|
||
except Exception as e:
|
||
pass
|
||
|
||
col1, col2, col3, col4, col5 = st.columns([2, 2, 1.5, 1, 1])
|
||
|
||
with col1:
|
||
st.write(f"**{task['task_name']}**")
|
||
st.caption(f"{task['stock_code']} - 间隔{task['check_interval']}秒")
|
||
|
||
with col2:
|
||
status = "✅ 已启用" if task['enabled'] else "⏸️ 已禁用"
|
||
auto_trade_status = "🤖 自动交易" if task['auto_trade'] else "👀 仅监控"
|
||
trading_mode = "🕒 仅交易时段" if task.get('trading_hours_only', 1) else "🌐 全时段"
|
||
st.write(status)
|
||
st.caption(f"{auto_trade_status} | {trading_mode}")
|
||
|
||
# 显示持仓状态
|
||
if has_position:
|
||
st.caption(f"📊 持仓: {position_quantity}股 @ {position_cost:.2f}元")
|
||
|
||
with col3:
|
||
is_running = task['stock_code'] in engine.monitoring_threads
|
||
if is_running:
|
||
st.success("▶️ 运行中")
|
||
else:
|
||
st.info("⏸️ 未运行")
|
||
|
||
# 显示盈亏
|
||
if has_position and current_price > 0:
|
||
if profit_loss > 0:
|
||
st.success(f"💰 +{profit_loss:.2f}元 ({profit_loss_pct:+.2f}%)")
|
||
elif profit_loss < 0:
|
||
st.error(f"📉 {profit_loss:.2f}元 ({profit_loss_pct:+.2f}%)")
|
||
else:
|
||
st.info("持平")
|
||
|
||
with col4:
|
||
if is_running:
|
||
if st.button("⏹️ 停止", key=f"stop_{task['id']}"):
|
||
engine.stop_monitor(task['stock_code'])
|
||
# 停止时更新数据库状态为禁用
|
||
db.update_monitor_task(task['stock_code'], {'enabled': 0})
|
||
st.success("已停止")
|
||
st.rerun()
|
||
else:
|
||
# 启动按钮始终可点击(只要任务未运行)
|
||
if st.button("▶️ 启动", key=f"start_{task['id']}"):
|
||
# 启动监控
|
||
engine.start_monitor(
|
||
stock_code=task['stock_code'],
|
||
check_interval=task['check_interval'],
|
||
auto_trade=task['auto_trade'] == 1,
|
||
notify=True,
|
||
has_position=has_position == 1,
|
||
position_cost=position_cost,
|
||
position_quantity=position_quantity,
|
||
trading_hours_only=task.get('trading_hours_only', 1) == 1
|
||
)
|
||
# 启动时更新数据库状态为启用
|
||
db.update_monitor_task(task['stock_code'], {'enabled': 1})
|
||
st.success("已启动")
|
||
st.rerun()
|
||
|
||
with col5:
|
||
if st.button("🗑️ 删除", key=f"del_{task['id']}"):
|
||
# 如果正在运行,先停止
|
||
if task['stock_code'] in engine.monitoring_threads:
|
||
engine.stop_monitor(task['stock_code'])
|
||
|
||
db.delete_monitor_task(task['id'])
|
||
st.success("已删除")
|
||
st.rerun()
|
||
|
||
# K线图和AI决策详情(可展开)
|
||
with st.expander(f"📊 K线图 & AI决策 - {task['task_name']}", expanded=False):
|
||
_render_task_kline_and_decisions(task, db, engine)
|
||
|
||
st.markdown("---")
|
||
|
||
|
||
def render_position_management():
|
||
"""持仓管理界面"""
|
||
|
||
st.header("📈 持仓管理")
|
||
|
||
engine = st.session_state.engine
|
||
qmt = engine.qmt
|
||
|
||
# 获取账户信息
|
||
account_info = qmt.get_account_info()
|
||
|
||
st.markdown("### 💰 账户概览")
|
||
|
||
col1, col2, col3, col4 = st.columns(4)
|
||
col1.metric("总资产", f"¥{account_info['total_value']:,.2f}")
|
||
col2.metric("可用资金", f"¥{account_info['available_cash']:,.2f}")
|
||
col3.metric("持仓数量", f"{account_info['positions_count']}个")
|
||
col4.metric("总盈亏", f"¥{account_info['total_profit_loss']:,.2f}")
|
||
|
||
# 获取持仓列表
|
||
positions = qmt.get_all_positions()
|
||
|
||
if not positions:
|
||
st.info("当前无持仓")
|
||
return
|
||
|
||
st.markdown("### 📊 持仓列表")
|
||
|
||
# 转换为DataFrame
|
||
df = pd.DataFrame(positions)
|
||
|
||
# 显示表格
|
||
st.dataframe(
|
||
df[[
|
||
'stock_code', 'stock_name', 'quantity', 'can_sell',
|
||
'cost_price', 'current_price', 'profit_loss', 'profit_loss_pct'
|
||
]],
|
||
column_config={
|
||
"stock_code": "代码",
|
||
"stock_name": "名称",
|
||
"quantity": "持仓",
|
||
"can_sell": "可卖",
|
||
"cost_price": "成本价",
|
||
"current_price": "现价",
|
||
"profit_loss": "盈亏",
|
||
"profit_loss_pct": "盈亏%"
|
||
},
|
||
hide_index=True,
|
||
width='stretch'
|
||
)
|
||
|
||
# 单只股票操作
|
||
st.markdown("### ⚡ 快速操作")
|
||
|
||
selected_stock = st.selectbox(
|
||
"选择股票",
|
||
options=[f"{p['stock_code']} {p['stock_name']}" for p in positions]
|
||
)
|
||
|
||
col1, col2 = st.columns(2)
|
||
|
||
with col1:
|
||
if st.button("🔍 AI分析", type="secondary"):
|
||
stock_code = selected_stock.split()[0]
|
||
with st.spinner("分析中..."):
|
||
result = engine.analyze_stock(stock_code, auto_trade=False)
|
||
if result['success']:
|
||
st.success("分析完成,查看'实时分析'标签页")
|
||
|
||
with col2:
|
||
if st.button("📤 卖出", type="primary"):
|
||
stock_code = selected_stock.split()[0]
|
||
# 这里可以添加卖出确认对话框
|
||
st.warning("请在'实时分析'中使用AI决策后卖出")
|
||
|
||
|
||
def render_history():
|
||
"""历史记录界面"""
|
||
|
||
st.header("📜 历史记录")
|
||
|
||
db = st.session_state.db
|
||
|
||
tab1, tab2, tab3 = st.tabs(["AI决策历史", "交易记录", "通知记录"])
|
||
|
||
# AI决策历史
|
||
with tab1:
|
||
st.subheader("🤖 AI决策历史")
|
||
|
||
decisions = db.get_ai_decisions(limit=50)
|
||
|
||
if not decisions:
|
||
st.info("暂无决策记录")
|
||
else:
|
||
for dec in decisions:
|
||
with st.expander(
|
||
f"{dec['decision_time']} - {dec['stock_code']} {dec['stock_name']} "
|
||
f"- {dec['action']} (信心度{dec['confidence']}%)"
|
||
):
|
||
col1, col2 = st.columns([1, 3])
|
||
|
||
with col1:
|
||
st.write(f"**时段:** {dec['trading_session']}")
|
||
st.write(f"**风险:** {dec['risk_level']}")
|
||
st.write(f"**仓位:** {dec['position_size_pct']}%")
|
||
|
||
with col2:
|
||
st.write("**决策理由:**")
|
||
st.text(dec['reasoning'])
|
||
|
||
# 交易记录
|
||
with tab2:
|
||
st.subheader("💱 交易记录")
|
||
|
||
trades = db.get_trade_records(limit=50)
|
||
|
||
if not trades:
|
||
st.info("暂无交易记录")
|
||
else:
|
||
df = pd.DataFrame(trades)
|
||
st.dataframe(
|
||
df[[
|
||
'trade_time', 'stock_code', 'stock_name', 'trade_type',
|
||
'quantity', 'price', 'amount', 'profit_loss'
|
||
]],
|
||
column_config={
|
||
"trade_time": "时间",
|
||
"stock_code": "代码",
|
||
"stock_name": "名称",
|
||
"trade_type": "类型",
|
||
"quantity": "数量",
|
||
"price": "价格",
|
||
"amount": "金额",
|
||
"profit_loss": "盈亏"
|
||
},
|
||
hide_index=True,
|
||
width='stretch'
|
||
)
|
||
|
||
# 通知记录
|
||
with tab3:
|
||
st.subheader("📬 通知记录")
|
||
st.info("通知记录功能开发中...")
|
||
|
||
|
||
def render_settings():
|
||
"""系统设置界面(跳转到主程序的环境配置)"""
|
||
|
||
st.header("⚙️ 系统设置")
|
||
|
||
st.info("""
|
||
### 📌 配置说明
|
||
|
||
智能盯盘使用主程序的统一配置系统,包括:
|
||
- 🤖 **DeepSeek API** - AI决策引擎
|
||
- 🔌 **MiniQMT** - 量化交易接口
|
||
- 📧 **邮件通知** - SMTP配置
|
||
- 🔔 **Webhook** - 钉钉/飞书通知
|
||
|
||
请前往主程序的 **"环境配置"** 页面进行统一配置。
|
||
""")
|
||
|
||
# 显示当前配置状态
|
||
st.markdown("### 📊 当前配置状态")
|
||
|
||
config = config_manager.read_env()
|
||
|
||
col1, col2 = st.columns(2)
|
||
|
||
with col1:
|
||
st.markdown("**🤖 DeepSeek API**")
|
||
api_key = config.get('DEEPSEEK_API_KEY', '')
|
||
if api_key:
|
||
st.success(f"✅ 已配置({api_key[:8]}...)")
|
||
else:
|
||
st.error("❌ 未配置")
|
||
|
||
st.markdown("**🔌 MiniQMT**")
|
||
miniqmt_enabled = config.get('MINIQMT_ENABLED', 'false').lower() == 'true'
|
||
if miniqmt_enabled:
|
||
account_id = config.get('MINIQMT_ACCOUNT_ID', '')
|
||
st.success(f"✅ 已启用(账户:{account_id or '未设置'})")
|
||
else:
|
||
st.warning("⚠️ 未启用(使用模拟交易)")
|
||
|
||
with col2:
|
||
st.markdown("**📧 邮件通知**")
|
||
email_enabled = config.get('EMAIL_ENABLED', 'false').lower() == 'true'
|
||
if email_enabled:
|
||
email_to = config.get('EMAIL_TO', '')
|
||
st.success(f"✅ 已启用({email_to})")
|
||
else:
|
||
st.warning("⚠️ 未启用")
|
||
|
||
st.markdown("**🔔 Webhook通知**")
|
||
webhook_enabled = config.get('WEBHOOK_ENABLED', 'false').lower() == 'true'
|
||
if webhook_enabled:
|
||
webhook_type = config.get('WEBHOOK_TYPE', 'dingtalk')
|
||
st.success(f"✅ 已启用({webhook_type})")
|
||
else:
|
||
st.warning("⚠️ 未启用")
|
||
|
||
st.markdown("---")
|
||
|
||
# 快速跳转按钮
|
||
st.markdown("### 🔧 配置管理")
|
||
|
||
st.info("""
|
||
**配置步骤:**
|
||
1. 点击左侧菜单 → **"环境配置"**
|
||
2. 填写所需的配置项
|
||
3. 点击 **"保存配置"**
|
||
4. 返回智能盯盘页面
|
||
5. 刷新页面使配置生效
|
||
""")
|
||
|
||
if st.button("🔄 重新加载配置", type="primary"):
|
||
config_manager.reload_config()
|
||
st.success("✅ 配置已重新加载")
|
||
st.info("💡 如果修改了配置,请刷新页面(Ctrl+R)")
|
||
st.rerun()
|
||
|
||
|
||
def _render_task_kline_and_decisions(task: Dict, db: SmartMonitorDB, engine):
|
||
"""
|
||
渲染单个任务的K线图和AI决策
|
||
|
||
Args:
|
||
task: 任务信息
|
||
db: 数据库实例
|
||
engine: 监控引擎实例
|
||
"""
|
||
from smart_monitor_kline import SmartMonitorKline
|
||
from smart_monitor_data import SmartMonitorDataFetcher
|
||
|
||
stock_code = task['stock_code']
|
||
stock_name = task.get('stock_name', stock_code)
|
||
|
||
# 创建两列:左侧K线图,右侧AI决策列表
|
||
col_chart, col_decisions = st.columns([2, 1])
|
||
|
||
with col_chart:
|
||
st.markdown("#### 📈 K线图")
|
||
|
||
# 添加刷新按钮
|
||
if st.button("🔄 刷新K线", key=f"refresh_kline_{task['id']}"):
|
||
st.rerun()
|
||
|
||
# 获取K线数据
|
||
try:
|
||
kline = SmartMonitorKline()
|
||
data_fetcher = SmartMonitorDataFetcher()
|
||
|
||
# 获取K线数据(60天)
|
||
with st.spinner(f"正在获取 {stock_code} 的K线数据..."):
|
||
kline_data = kline.get_kline_data(stock_code, days=60, data_fetcher=data_fetcher)
|
||
|
||
if kline_data is not None and not kline_data.empty:
|
||
# 获取AI决策历史(最近100条,用于K线图标注)
|
||
ai_decisions = db.get_ai_decisions(
|
||
stock_code=stock_code,
|
||
limit=100
|
||
)
|
||
|
||
# 过滤最近30天的决策(用于K线图标注)
|
||
from datetime import timedelta
|
||
if ai_decisions:
|
||
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y-%m-%d')
|
||
ai_decisions = [
|
||
d for d in ai_decisions
|
||
if d.get('decision_time', '').split()[0] >= start_date
|
||
]
|
||
|
||
# 创建K线图
|
||
fig = kline.create_kline_with_decisions(
|
||
stock_code=stock_code,
|
||
stock_name=stock_name,
|
||
kline_data=kline_data,
|
||
ai_decisions=ai_decisions,
|
||
show_volume=True,
|
||
show_ma=True,
|
||
height=500
|
||
)
|
||
|
||
# 显示图表
|
||
st.plotly_chart(fig, use_container_width=True, config={'responsive': True})
|
||
|
||
st.caption(f"📅 数据时间范围:{kline_data['日期'].min()} ~ {kline_data['日期'].max()}")
|
||
else:
|
||
st.error(f"❌ 无法获取 {stock_code} 的K线数据")
|
||
|
||
except Exception as e:
|
||
st.error(f"❌ K线图加载失败: {str(e)}")
|
||
import traceback
|
||
st.text(traceback.format_exc())
|
||
|
||
with col_decisions:
|
||
st.markdown("#### 🤖 AI决策历史")
|
||
|
||
# 添加刷新按钮
|
||
if st.button("🔄 刷新决策", key=f"refresh_decisions_{task['id']}"):
|
||
st.rerun()
|
||
|
||
# 获取最近的AI决策(最近5条)
|
||
try:
|
||
recent_decisions = db.get_ai_decisions(
|
||
stock_code=stock_code,
|
||
limit=5
|
||
)
|
||
|
||
if recent_decisions:
|
||
for idx, decision in enumerate(recent_decisions):
|
||
action = decision.get('action', 'unknown')
|
||
decision_time = decision.get('decision_time', '')
|
||
confidence = decision.get('confidence', 0)
|
||
reasoning = decision.get('reasoning', '无')
|
||
executed = decision.get('executed', 0)
|
||
|
||
# 决策类型图标和颜色
|
||
action_icons = {
|
||
'buy': '🔺',
|
||
'sell': '🔻',
|
||
'add_position': '⬆️',
|
||
'reduce_position': '⬇️',
|
||
'hold': '⏸️'
|
||
}
|
||
|
||
action_colors = {
|
||
'buy': '#ef5350',
|
||
'sell': '#26a69a',
|
||
'add_position': '#ff9800',
|
||
'reduce_position': '#9c27b0',
|
||
'hold': '#607d8b'
|
||
}
|
||
|
||
action_names = {
|
||
'buy': '买入',
|
||
'sell': '卖出',
|
||
'add_position': '加仓',
|
||
'reduce_position': '减仓',
|
||
'hold': '持有'
|
||
}
|
||
|
||
icon = action_icons.get(action, '❓')
|
||
color = action_colors.get(action, '#000000')
|
||
action_name = action_names.get(action, action)
|
||
|
||
# 显示决策卡片
|
||
with st.container():
|
||
st.markdown(f"""
|
||
<div style="border-left: 4px solid {color}; padding-left: 10px; margin-bottom: 10px;">
|
||
<p style="margin: 0;">
|
||
<strong>{icon} {action_name}</strong>
|
||
{'✅' if executed else '⏳'}
|
||
</p>
|
||
<p style="margin: 5px 0; font-size: 0.85em; color: gray;">
|
||
{decision_time}
|
||
</p>
|
||
<p style="margin: 5px 0; font-size: 0.9em;">
|
||
<strong>置信度:</strong> {confidence}%
|
||
</p>
|
||
<p style="margin: 5px 0; font-size: 0.9em;">
|
||
<strong>推理:</strong> {reasoning[:100]}{'...' if len(reasoning) > 100 else ''}
|
||
</p>
|
||
</div>
|
||
""", unsafe_allow_html=True)
|
||
|
||
st.markdown("---")
|
||
else:
|
||
st.info("📭 暂无AI决策记录")
|
||
st.caption("启动监控后,AI会定期分析并记录决策")
|
||
|
||
except Exception as e:
|
||
st.error(f"❌ 加载决策历史失败: {str(e)}")
|
||
|
||
|
||
if __name__ == '__main__':
|
||
smart_monitor_ui()
|
||
|