Files
aiagents-stock/monitor_manager.py
T
2025-10-04 11:52:11 +08:00

694 lines
26 KiB
Python
Raw 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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
股票监测管理模块
支持添加、删除、编辑监测股票
卡片式布局,支持关键位置监测
"""
import streamlit as st
import time
import uuid
from datetime import datetime, timedelta
from typing import Dict, List
import json
from monitor_db import monitor_db
from monitor_service import monitor_service
from notification_service import notification_service
from stock_data import StockDataFetcher
from miniqmt_interface import miniqmt, get_miniqmt_status, QuantStrategyConfig
def display_monitor_manager():
"""显示监测管理主页面"""
st.markdown("## 📊 股票监测管理")
st.markdown("---")
# 监测服务状态
display_monitor_status()
# 添加新股票监测
display_add_stock_section()
# 监测股票列表
display_monitored_stocks()
# 通知管理
display_notification_management()
def display_monitor_status():
"""显示监测服务状态"""
col1, col2, col3, col4, col5, col6 = st.columns(6)
with col1:
if monitor_service.running:
st.success("🟢 运行中")
else:
st.error("🔴 已停止")
with col2:
stocks = monitor_db.get_monitored_stocks()
st.metric("监测股票", len(stocks))
with col3:
notifications = monitor_db.get_pending_notifications()
st.metric("待处理通知", len(notifications))
with col4:
# 显示MiniQMT状态
qmt_status = get_miniqmt_status()
if qmt_status['ready']:
st.success("🤖 QMT在线")
else:
st.info("🤖 QMT离线")
with col5:
if monitor_service.running:
if st.button("⏹️ 停止监测", type="secondary"):
monitor_service.stop_monitoring()
st.success("✅ 监测服务已停止")
st.rerun()
else:
if st.button("▶️ 启动监测", type="primary"):
monitor_service.start_monitoring()
st.success("✅ 监测服务已启动")
st.rerun()
with col6:
if st.button("🔄 刷新状态"):
st.rerun()
def display_add_stock_section():
"""显示添加股票监测区域"""
st.markdown("### 添加股票监测")
with st.expander("点击展开添加股票监测", expanded=False):
col1, col2 = st.columns([1, 1])
with col1:
# 股票信息输入
st.subheader("📈 股票信息")
symbol = st.text_input("股票代码", placeholder="例如: AAPL, 000001", help="支持美股和A股代码")
name = st.text_input("股票名称", placeholder="例如: 苹果公司", help="可选,用于显示")
# 获取股票基本信息
if symbol:
if st.button("🔍 获取股票信息"):
with st.spinner("正在获取股票信息..."):
fetcher = StockDataFetcher()
stock_info = fetcher.get_stock_info(symbol)
if "error" not in stock_info:
st.success("✅ 股票信息获取成功")
st.session_state.temp_stock_info = stock_info
else:
st.error(f"❌ {stock_info['error']}")
with col2:
# 监测设置
st.subheader("⚙️ 监测设置")
# 关键位置设置
st.markdown("**🎯 关键位置设置**")
entry_min = st.number_input("进场区间最低价", value=0.0, step=0.01, format="%.2f")
entry_max = st.number_input("进场区间最高价", value=0.0, step=0.01, format="%.2f")
take_profit = st.number_input("止盈价位", value=0.0, step=0.01, format="%.2f", help="可选")
stop_loss = st.number_input("止损价位", value=0.0, step=0.01, format="%.2f", help="可选")
# 监测参数
st.markdown("**⏰ 监测参数**")
check_interval = st.slider("监测间隔(分钟)", 5, 120, 30)
notification_enabled = st.checkbox("启用通知", value=True)
# 投资评级
rating = st.selectbox("投资评级", ["买入", "持有", "卖出"], index=0)
# 量化交易设置
st.markdown("**🤖 量化交易(MiniQMT**")
quant_enabled = st.checkbox("启用量化自动交易", value=False, help="需要先配置MiniQMT连接")
if quant_enabled:
max_position_pct = st.slider("最大仓位比例", 0.05, 0.5, 0.2, 0.05, help="单只股票最大占总资金的比例")
auto_stop_loss = st.checkbox("自动止损", value=True)
auto_take_profit = st.checkbox("自动止盈", value=True)
# 添加按钮
if st.button("✅ 添加监测", type="primary", use_container_width=True):
if symbol and entry_min > 0 and entry_max > 0 and entry_max > entry_min:
try:
# 准备数据
entry_range = {"min": entry_min, "max": entry_max}
# 准备量化配置
quant_config = None
if quant_enabled:
quant_config = {
'max_position_pct': max_position_pct,
'auto_stop_loss': auto_stop_loss,
'auto_take_profit': auto_take_profit,
'min_trade_amount': 5000
}
# 添加到数据库
stock_id = monitor_db.add_monitored_stock(
symbol=symbol,
name=name or symbol,
rating=rating,
entry_range=entry_range,
take_profit=take_profit if take_profit > 0 else None,
stop_loss=stop_loss if stop_loss > 0 else None,
check_interval=check_interval,
notification_enabled=notification_enabled,
quant_enabled=quant_enabled,
quant_config=quant_config
)
st.success(f"✅ 已成功添加 {symbol} 到监测列表")
st.balloons()
# 立即更新一次价格
monitor_service.manual_update_stock(stock_id)
# 清空表单
st.rerun()
except Exception as e:
st.error(f"❌ 添加失败: {str(e)}")
else:
st.error("❌ 请填写完整的股票信息和有效的进场区间")
def display_monitored_stocks():
"""显示监测股票列表 - 卡片式布局"""
st.markdown("### 📋 监测股票列表")
stocks = monitor_db.get_monitored_stocks()
if not stocks:
st.info("📭 暂无监测股票,请添加股票开始监测")
return
# 筛选和搜索
col1, col2, col3 = st.columns([2, 1, 1])
with col1:
search_term = st.text_input("🔍 搜索股票", placeholder="输入股票代码或名称")
with col2:
rating_filter = st.selectbox("评级筛选", ["全部", "买入", "持有", "卖出"])
with col3:
if st.button("🔄 刷新列表"):
st.rerun()
# 筛选股票
filtered_stocks = stocks
if search_term:
filtered_stocks = [s for s in stocks if search_term.lower() in s['symbol'].lower() or search_term.lower() in s['name'].lower()]
if rating_filter != "全部":
filtered_stocks = [s for s in filtered_stocks if s['rating'] == rating_filter]
if not filtered_stocks:
st.warning("🔍 未找到匹配的股票")
return
# 卡片式布局 - 每行显示2个卡片
for i in range(0, len(filtered_stocks), 2):
cols = st.columns(2)
for j, col in enumerate(cols):
if i + j < len(filtered_stocks):
stock = filtered_stocks[i + j]
with col:
display_stock_card(stock)
# 显示编辑对话框
if 'editing_stock_id' in st.session_state:
display_edit_dialog(st.session_state.editing_stock_id)
# 显示删除确认对话框
if 'deleting_stock_id' in st.session_state:
display_delete_confirm_dialog(st.session_state.deleting_stock_id)
def display_stock_card(stock: Dict):
"""显示单个股票监测卡片"""
with st.container():
# 卡片头部
st.markdown(f"""
<div style="
border: 1px solid #ddd;
border-radius: 10px;
padding: 15px;
margin: 10px 0;
background-color: #f9f9f9;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
">
""", unsafe_allow_html=True)
# 股票基本信息
col1, col2 = st.columns([2, 1])
with col1:
st.markdown(f"**{stock['symbol']}** - {stock['name']}")
# 评级显示
rating_color = {
'买入': '🟢',
'持有': '🟡',
'卖出': '🔴'
}
st.markdown(f"评级: {rating_color.get(stock['rating'], '⚪')} {stock['rating']}")
with col2:
if stock['current_price'] and stock['current_price'] != 'N/A':
st.metric("当前价格", f{stock['current_price']}")
else:
st.metric("当前价格", "等待更新")
# 关键位置信息
st.markdown("**🎯 关键位置**")
entry_range = stock['entry_range']
col1, col2, col3 = st.columns(3)
with col1:
st.info(f"**进场区间**\n¥{entry_range['min']} - ¥{entry_range['max']}")
with col2:
if stock['take_profit']:
st.success(f"**止盈位**\n¥{stock['take_profit']}")
else:
st.info("**止盈位**\n未设置")
with col3:
if stock['stop_loss']:
st.error(f"**止损位**\n¥{stock['stop_loss']}")
else:
st.info("**止损位**\n未设置")
# 监测状态
st.markdown("**📊 监测状态**")
col1, col2, col3 = st.columns(3)
with col1:
st.caption(f"监测间隔: {stock['check_interval']}分钟")
with col2:
if stock['last_checked']:
last_checked = datetime.fromisoformat(stock['last_checked'])
st.caption(f"最后检查: {last_checked.strftime('%m-%d %H:%M')}")
else:
st.caption("最后检查: 从未检查")
with col3:
status = "🟢 启用" if stock['notification_enabled'] else "🔴 禁用"
st.caption(f"通知: {status}")
# 显示量化状态
if stock.get('quant_enabled', False):
st.caption("🤖 量化: 🟢 启用")
else:
st.caption("🤖 量化: 🔴 禁用")
# 操作按钮
st.markdown("**🔧 操作**")
col1, col2, col3, col4 = st.columns(4)
with col1:
if st.button("🔄 更新", key=f"update_{stock['id']}"):
if monitor_service.manual_update_stock(stock['id']):
st.success("✅ 更新成功")
else:
st.error("❌ 更新失败")
with col2:
if st.button("✏️ 编辑", key=f"edit_{stock['id']}"):
st.session_state.editing_stock_id = stock['id']
st.rerun()
with col3:
# 切换通知状态
current_status = stock['notification_enabled']
if current_status:
if st.button("🔕 禁用", key=f"notify_{stock['id']}"):
monitor_db.toggle_notification(stock['id'], False)
st.success("✅ 已禁用通知")
st.rerun()
else:
if st.button("🔔 启用", key=f"notify_{stock['id']}"):
monitor_db.toggle_notification(stock['id'], True)
st.success("✅ 已启用通知")
st.rerun()
with col4:
if st.button("🗑️ 删除", key=f"delete_{stock['id']}"):
st.session_state.deleting_stock_id = stock['id']
st.rerun()
st.markdown("</div>", unsafe_allow_html=True)
def display_edit_dialog(stock_id: int):
"""显示编辑股票对话框"""
stock = monitor_db.get_stock_by_id(stock_id)
if not stock:
st.error("❌ 股票不存在")
del st.session_state.editing_stock_id
return
st.markdown("---")
st.markdown(f"### ✏️ 编辑监测 - {stock['symbol']} {stock['name']}")
with st.form(key=f"edit_form_{stock_id}"):
col1, col2 = st.columns([1, 1])
with col1:
st.subheader("🎯 关键位置")
entry_range = stock['entry_range']
entry_min = st.number_input("进场区间最低价", value=float(entry_range['min']), step=0.01, format="%.2f")
entry_max = st.number_input("进场区间最高价", value=float(entry_range['max']), step=0.01, format="%.2f")
take_profit = st.number_input("止盈价位", value=float(stock['take_profit']) if stock['take_profit'] else 0.0, step=0.01, format="%.2f")
stop_loss = st.number_input("止损价位", value=float(stock['stop_loss']) if stock['stop_loss'] else 0.0, step=0.01, format="%.2f")
with col2:
st.subheader("⚙️ 监测设置")
check_interval = st.slider("监测间隔(分钟)", 5, 120, stock['check_interval'])
rating = st.selectbox("投资评级", ["买入", "持有", "卖出"],
index=["买入", "持有", "卖出"].index(stock['rating']) if stock['rating'] in ["买入", "持有", "卖出"] else 0)
notification_enabled = st.checkbox("启用通知", value=stock['notification_enabled'])
# 量化交易设置
st.markdown("**🤖 量化交易**")
quant_enabled = st.checkbox("启用量化自动交易", value=stock.get('quant_enabled', False))
if quant_enabled:
quant_config = stock.get('quant_config', {})
max_position_pct = st.slider("最大仓位比例", 0.05, 0.5,
quant_config.get('max_position_pct', 0.2), 0.05)
auto_stop_loss = st.checkbox("自动止损", value=quant_config.get('auto_stop_loss', True))
auto_take_profit = st.checkbox("自动止盈", value=quant_config.get('auto_take_profit', True))
col1, col2, col3 = st.columns(3)
with col1:
submit = st.form_submit_button("✅ 保存修改", type="primary", use_container_width=True)
with col2:
cancel = st.form_submit_button("❌ 取消", use_container_width=True)
if submit:
if entry_min > 0 and entry_max > 0 and entry_max > entry_min:
try:
# 更新数据库
new_entry_range = {"min": entry_min, "max": entry_max}
# 准备量化配置
new_quant_config = None
if quant_enabled:
new_quant_config = {
'max_position_pct': max_position_pct,
'auto_stop_loss': auto_stop_loss,
'auto_take_profit': auto_take_profit,
'min_trade_amount': 5000
}
monitor_db.update_monitored_stock(
stock_id=stock_id,
rating=rating,
entry_range=new_entry_range,
take_profit=take_profit if take_profit > 0 else None,
stop_loss=stop_loss if stop_loss > 0 else None,
check_interval=check_interval,
notification_enabled=notification_enabled,
quant_enabled=quant_enabled,
quant_config=new_quant_config
)
st.success("✅ 修改已保存")
del st.session_state.editing_stock_id
st.rerun()
except Exception as e:
st.error(f"❌ 保存失败: {str(e)}")
else:
st.error("❌ 请输入有效的进场区间")
if cancel:
del st.session_state.editing_stock_id
st.rerun()
def display_delete_confirm_dialog(stock_id: int):
"""显示删除确认对话框"""
stock = monitor_db.get_stock_by_id(stock_id)
if not stock:
st.error("❌ 股票不存在或已被删除")
if 'deleting_stock_id' in st.session_state:
del st.session_state.deleting_stock_id
st.rerun()
return
st.markdown("---")
st.markdown(f"### ⚠️ 确认删除")
st.warning(f"""
您确定要删除以下监测吗?
**股票代码**: {stock['symbol']}
**股票名称**: {stock['name']}
**投资评级**: {stock['rating']}
此操作不可撤销!
""")
col1, col2, col3 = st.columns([1, 1, 1])
with col1:
if st.button("🗑️ 确认删除", type="primary", use_container_width=True, key=f"confirm_delete_{stock_id}"):
try:
result = monitor_db.remove_monitored_stock(stock_id)
if result:
# 清理session state
if 'deleting_stock_id' in st.session_state:
del st.session_state.deleting_stock_id
st.success("✅ 已成功删除监测")
st.balloons()
time.sleep(0.8) # 短暂延迟,让用户看到成功消息
st.rerun()
else:
st.error("❌ 删除失败:股票不存在或已被删除")
time.sleep(1)
if 'deleting_stock_id' in st.session_state:
del st.session_state.deleting_stock_id
st.rerun()
except Exception as e:
st.error(f"❌ 删除失败:{str(e)}")
time.sleep(1)
if 'deleting_stock_id' in st.session_state:
del st.session_state.deleting_stock_id
st.rerun()
with col2:
if st.button("❌ 取消", use_container_width=True, key=f"cancel_delete_{stock_id}"):
del st.session_state.deleting_stock_id
st.rerun()
def display_notification_management():
"""显示通知管理"""
st.markdown("### 🔔 通知管理")
# 显示MiniQMT量化交易状态
display_miniqmt_status()
st.markdown("---")
# 通知设置
col1, col2 = st.columns([1, 1])
with col1:
st.subheader("📧 邮件通知设置")
# 获取当前邮件配置状态
email_config = notification_service.get_email_config_status()
# 显示配置状态
if email_config['configured']:
st.success("✅ 邮件配置已完成")
else:
st.warning("⚠️ 邮件未配置或配置不完整")
# 显示配置信息
st.info(f"""
**当前配置:**
- SMTP服务器: {email_config['smtp_server']}
- SMTP端口: {email_config['smtp_port']}
- 发送邮箱: {email_config['email_from']}
- 接收邮箱: {email_config['email_to']}
- 启用状态: {'是' if email_config['enabled'] else '否'}
""")
st.markdown("---")
st.markdown("**⚙️ 配置说明**")
st.caption("""
在 `.env` 文件中配置以下参数:
```
EMAIL_ENABLED=true
SMTP_SERVER=smtp.qq.com
SMTP_PORT=587
EMAIL_FROM=your_email@qq.com
EMAIL_PASSWORD=your_authorization_code
EMAIL_TO=receiver@example.com
```
💡 提示:
- 端口:587 (TLS) 或 465 (SSL)
- 密码:使用邮箱授权码,不是登录密码
- QQ邮箱授权码获取:设置 → 账户 → POP3/IMAP/SMTP → 生成授权码
""")
# 测试邮件按钮
if email_config['configured']:
if st.button("📧 发送测试邮件", type="primary", use_container_width=True):
with st.spinner("正在发送测试邮件..."):
success, message = notification_service.send_test_email()
if success:
st.success(f"✅ {message}")
st.balloons()
else:
st.error(f"❌ {message}")
else:
st.button("📧 发送测试邮件", type="primary", use_container_width=True, disabled=True)
st.caption("请先在.env文件中配置邮件参数")
with col2:
st.subheader("📱 通知历史")
notifications = monitor_db.get_pending_notifications()
if notifications:
# 显示通知列表
for notification in notifications[-10:]: # 显示最近10条
notification_type = notification['type']
color_map = {
'entry': '🟢',
'take_profit': '🟡',
'stop_loss': '🔴'
}
icon = color_map.get(notification_type, '🔵')
# 显示通知信息
st.info(f"{icon} **{notification['symbol']}** - {notification['message']}\n\n_{notification['triggered_at']}_")
# 清空通知按钮
col_a, col_b = st.columns(2)
with col_a:
if st.button("✅ 标记已读"):
monitor_db.mark_all_notifications_sent()
st.success("✅ 所有通知已标记为已读")
st.rerun()
with col_b:
if st.button("🗑️ 清空通知"):
monitor_db.clear_all_notifications()
st.success("✅ 通知已清空")
st.rerun()
else:
st.info("📭 暂无通知")
def display_miniqmt_status():
"""显示MiniQMT量化交易状态"""
st.markdown("### 🤖 MiniQMT量化交易")
qmt_status = get_miniqmt_status()
col1, col2 = st.columns([1, 1])
with col1:
st.subheader("📊 连接状态")
if qmt_status['enabled']:
st.success("✅ MiniQMT已启用")
else:
st.warning("⚠️ MiniQMT未启用")
if qmt_status['connected']:
st.success("✅ 已连接到MiniQMT")
else:
st.info("⏸️ 未连接到MiniQMT")
if qmt_status['account_id']:
st.info(f"**账户ID**: {qmt_status['account_id']}")
else:
st.caption("未配置账户ID")
st.markdown("---")
st.markdown("**⚙️ 配置说明**")
st.caption("""
在 `config.py` 中配置以下参数:
```python
MINIQMT_CONFIG = {
'enabled': True,
'account_id': 'your_account_id'
}
```
💡 提示:
- 需要安装并启动MiniQMT客户端
- 确保账户已登录
- 预留接口已实现,可对接真实交易
""")
with col2:
st.subheader("📈 量化统计")
# 统计启用量化的股票
stocks = monitor_db.get_monitored_stocks()
quant_stocks = [s for s in stocks if s.get('quant_enabled', False)]
st.metric("启用量化的股票", f"{len(quant_stocks)}/{len(stocks)}")
if quant_stocks:
st.markdown("**量化监测列表:**")
for stock in quant_stocks:
st.caption(f"🤖 {stock['symbol']} - {stock['name']}")
else:
st.info("暂无启用量化交易的股票")
st.markdown("---")
# 连接按钮
if qmt_status['enabled'] and not qmt_status['connected']:
if st.button("🔗 连接MiniQMT", type="primary", use_container_width=True):
success, msg = miniqmt.connect()
if success:
st.success(f"✅ {msg}")
else:
st.error(f"❌ {msg}")
st.rerun()
elif qmt_status['connected']:
if st.button("🔌 断开连接", use_container_width=True):
if miniqmt.disconnect():
st.info("⏸️ 已断开MiniQMT连接")
st.rerun()
def get_monitor_summary():
"""获取监测摘要信息"""
stocks = monitor_db.get_monitored_stocks()
summary = {
'total_stocks': len(stocks),
'stocks_needing_update': len(monitor_service.get_stocks_needing_update()),
'pending_notifications': len(monitor_db.get_pending_notifications()),
'active_monitoring': monitor_service.running
}
return summary