Files
aiagents-stock/smart_monitor_ui.py

906 lines
34 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
智能盯盘 - 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/MA60MACD死叉
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()