From 16071f81e8b768dd655a498156b62260a96a5d48 Mon Sep 17 00:00:00 2001 From: oficcejo Date: Sun, 26 Oct 2025 10:02:55 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=BFalphaarena=E9=A1=B9=E7=9B=AE=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0A=E8=82=A1ai=E8=87=AA=E5=8A=A8=E9=87=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 18 + app.py | 21 +- docs/Tushare积分获取指南.md | 350 ++++++++++++ docs/智能盯盘使用指南.md | 219 ++++++++ docs/智能盯盘持仓管理功能说明.md | 432 +++++++++++++++ docs/智能盯盘配置说明.md | 263 +++++++++ longhubang.db | Bin 647168 -> 778240 bytes monitor_db.py | 31 +- monitor_manager.py | 13 +- requirements.txt | 2 +- smart_monitor.db | Bin 0 -> 131072 bytes smart_monitor_data.py | 775 ++++++++++++++++++++++++++ smart_monitor_db.py | 634 ++++++++++++++++++++++ smart_monitor_deepseek.py | 526 ++++++++++++++++++ smart_monitor_engine.py | 591 ++++++++++++++++++++ smart_monitor_kline.py | 484 +++++++++++++++++ smart_monitor_qmt.py | 628 ++++++++++++++++++++++ smart_monitor_ui.py | 897 +++++++++++++++++++++++++++++++ stock_analysis.db | Bin 21450752 -> 22032384 bytes test_smart_monitor_data.py | 103 ++++ test_tushare_monitoring.py | 247 +++++++++ 21 files changed, 6218 insertions(+), 16 deletions(-) create mode 100644 docs/Tushare积分获取指南.md create mode 100644 docs/智能盯盘使用指南.md create mode 100644 docs/智能盯盘持仓管理功能说明.md create mode 100644 docs/智能盯盘配置说明.md create mode 100644 smart_monitor.db create mode 100644 smart_monitor_data.py create mode 100644 smart_monitor_db.py create mode 100644 smart_monitor_deepseek.py create mode 100644 smart_monitor_engine.py create mode 100644 smart_monitor_kline.py create mode 100644 smart_monitor_qmt.py create mode 100644 smart_monitor_ui.py create mode 100644 test_smart_monitor_data.py create mode 100644 test_tushare_monitoring.py diff --git a/README.md b/README.md index f6e9122..b727b28 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,24 @@ # 🤖 复合多AI智能体股票团队分析系统 - 初心:在股市摸爬滚打多年,自学自编各种指标,花冤枉钱学习了各种战法各种策略,也曾入各种小班,总是赚少赔多,逐渐失去在股市玩的信心。自从去年deepseek上市,一直探索用ai辅助分析,且近日受tradingagents项目启发(感谢原作),多agent结合跟踪主力资金战法(某指每年收费6000rmb),用各种ai辅助编程,拼凑了这么个小程序,根据软件提供的辅助信息,实盘测试盈率还是挺高的,并且逐步形成了自己的交易系统,近一个月来,账户也慢慢在扭亏为盈。开源此软件的目的,就是为了使像我一样的小散,不再迷茫。也许这个软件不能让你发大财,但是他能给你足够的信心。最后提醒:股市有风险,入市需谨慎! + +## 🤖 新增--AI盯盘 - AI自动化交易决策系统(NEW!) + +参照AlphaArena项目,基于 DeepSeek AI 的A股自动化交易系统,支持实时监控、智能决策、自动交易(T+1规则适配)。 + +**核心功能:** +- ✅ **DeepSeek AI决策** - 利用DeepSeek-V3进行深度技术分析 +- ✅ **实时监控** - 24/7持续监控目标股票 +- ✅ **自动交易** - 集成miniQMT,支持实盘/模拟交易 +- ✅ **T+1规则** - 完全遵循A股交易规则 +- ✅ **持仓管理** - 记录持仓成本,实时显示盈亏,AI决策考虑持仓情况 ⭐ NEW +- ✅ **邮件/Webhook通知** - 及时接收交易信号 +- ✅ **稳定数据源** - 优化AKShare接口,专注技术面分析 ⭐ 2025-10-25 +- ✅ **完整任务流程** - 创建任务后需手动启动,启动/停止自动同步状态,用户完全控制 ⭐ 2025-10-26 +- ✅ **K线图可视化** - 交互式K线图,AI决策标注,实时更新,一目了然 ⭐ 2025-10-26 NEW +- ✅ **统一配置** - 使用主程序的配置管理系统(miniQMT、邮件、Webhook) +- ✅ **技术指标完整** - 均线/MACD/RSI/KDJ/布林带,AI决策更可靠 + - 希望能帮到你!欢迎加微信群讨论 image diff --git a/app.py b/app.py index 8ef2371..ecc86b9 100644 --- a/app.py +++ b/app.py @@ -19,6 +19,7 @@ from config_manager import config_manager from main_force_ui import display_main_force_selector from sector_strategy_ui import display_sector_strategy from longhubang_ui import display_longhubang +from smart_monitor_ui import smart_monitor_ui # 页面配置 st.set_page_config( @@ -318,14 +319,14 @@ def main(): if st.button("🎯 智策板块", width='stretch', key="nav_sector_strategy", help="AI板块策略分析"): st.session_state.show_sector_strategy = True for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force', - 'show_longhubang', 'show_portfolio']: + 'show_longhubang', 'show_portfolio', 'show_smart_monitor']: if key in st.session_state: del st.session_state[key] if st.button("🐉 智瞰龙虎", width='stretch', key="nav_longhubang", help="龙虎榜深度分析"): st.session_state.show_longhubang = True for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force', - 'show_sector_strategy', 'show_portfolio']: + 'show_sector_strategy', 'show_portfolio', 'show_smart_monitor']: if key in st.session_state: del st.session_state[key] @@ -336,14 +337,21 @@ def main(): if st.button("📊 持仓分析", width='stretch', key="nav_portfolio", help="投资组合分析与定时跟踪"): st.session_state.show_portfolio = True for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force', - 'show_sector_strategy', 'show_longhubang']: + 'show_sector_strategy', 'show_longhubang', 'show_smart_monitor']: + if key in st.session_state: + del st.session_state[key] + + if st.button("🤖 AI盯盘", width='stretch', key="nav_smart_monitor", help="DeepSeek AI自动盯盘决策交易(支持A股T+1)"): + st.session_state.show_smart_monitor = True + for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force', + 'show_sector_strategy', 'show_longhubang', 'show_portfolio']: if key in st.session_state: del st.session_state[key] if st.button("📡 实时监测", width='stretch', key="nav_monitor", help="价格监控与预警提醒"): st.session_state.show_monitor = True for key in ['show_history', 'show_main_force', 'show_longhubang', 'show_portfolio', - 'show_config', 'show_sector_strategy']: + 'show_config', 'show_sector_strategy', 'show_smart_monitor']: if key in st.session_state: del st.session_state[key] @@ -464,6 +472,11 @@ def main(): display_longhubang() return + # 检查是否显示AI盯盘 + if 'show_smart_monitor' in st.session_state and st.session_state.show_smart_monitor: + smart_monitor_ui() + return + # 检查是否显示持仓分析 if 'show_portfolio' in st.session_state and st.session_state.show_portfolio: from portfolio_ui import display_portfolio_manager diff --git a/docs/Tushare积分获取指南.md b/docs/Tushare积分获取指南.md new file mode 100644 index 0000000..67f365f --- /dev/null +++ b/docs/Tushare积分获取指南.md @@ -0,0 +1,350 @@ +# Tushare积分获取指南 + +## 📋 问题说明 + +在使用智能盯盘的数据源降级功能时,您可能会看到以下提示: + +``` +Tushare获取失败: 抱歉,您没有接口访问权限 +⚠️ Tushare资金流向接口需要120积分,当前积分不足 +``` + +这是因为Tushare的某些接口需要**积分权限**才能访问。 + +--- + +## 🎯 Tushare积分体系 + +### 接口积分要求 + +| 接口类型 | 所需积分 | 说明 | +|---------|---------|------| +| **股票列表** | 0 | 免费 | +| **日线行情** | 0-120 | 基础行情免费,高级字段需积分 | +| **分钟线** | 2000 | 需要较高积分 | +| **资金流向** | 120 | ⭐ 智能盯盘使用 | +| **财务数据** | 不同 | 根据字段不同 | + +智能盯盘主要使用: +- ✅ **日线行情**(0积分)- 作为降级方案 +- ⚠️ **资金流向**(120积分)- 可选,没有也能运行 + +--- + +## 💡 快速获取120积分 + +### 方法一:完善个人信息(+100积分)⭐ 推荐 + +1. **登录Tushare** + - 访问:https://tushare.pro/ + - 登录您的账号 + +2. **进入个人中心** + - 点击右上角用户名 + - 选择"个人中心" + +3. **完善资料** + - 填写真实姓名 + - 填写手机号并验证 + - 填写邮箱并验证 + - 完善职业信息 + - **一次性获得100积分** + +### 方法二:每日签到(每天+1积分) + +1. **登录后签到** + - 每天访问 https://tushare.pro/ + - 点击"每日签到"按钮 + - 获得1积分 + +2. **累积积分** + - 连续签到30天 = 30积分 + - 配合方法一,总计130积分(够用!) + +### 方法三:社区互动 + +1. **发表文章** + - 分享使用经验 + - 数据分析案例 + - 获得5-20积分/篇 + +2. **回答问题** + - 在社区帮助其他用户 + - 获得1-5积分/次 + +3. **推荐新用户** + - 邀请好友注册 + - 获得积分奖励 + +--- + +## 🚀 推荐方案(最快) + +### 5分钟获取120积分 + +**第1步:完善个人信息(3分钟)** +``` +1. 填写姓名 +2. 验证手机号 +3. 验证邮箱 +4. 填写职业 +→ 立即获得100积分 +``` + +**第2步:连续签到20天** +``` +每天登录签到 × 20天 = 20积分 +100 + 20 = 120积分(满足要求!) +``` + +**或者:发表1篇文章(10分钟)** +``` +1. 写一篇使用心得 +2. 分享到社区 +3. 获得10-20积分 +→ 100 + 20 = 120积分(完成!) +``` + +--- + +## 🔍 查看当前积分 + +1. **登录Tushare** + - 访问:https://tushare.pro/user/token + +2. **查看积分余额** + ``` + 页面会显示: + - 当前积分 + - 可用接口 + - 积分明细 + ``` + +3. **检查接口权限** + - 查看"资金流向"是否可用 + - 如果显示"不可用",说明积分不足 + +--- + +## ⚙️ 智能盯盘的降级策略 + +### 当前实现的多层降级 + +``` +Level 1: AKShare(主数据源) + ↓ 失败 +Level 2: Tushare基础接口(免费,0积分) + ├─ daily_basic(基础日线) + ├─ pro_bar(社区版) + └─ stock_basic(股票列表) + ↓ 成功 → 返回行情数据(无资金流向) + ↓ 失败 +Level 3: Tushare高级接口(需120积分) + └─ moneyflow(资金流向) + ↓ 成功 → 返回完整数据 + ↓ 失败 → 返回部分数据(无资金流向) +``` + +### 没有120积分的影响 + +| 功能 | 是否受影响 | 说明 | +|-----|----------|------| +| AI决策 | ✅ 正常 | 使用行情数据即可 | +| 实时行情 | ✅ 正常 | 使用免费接口 | +| 技术指标 | ✅ 正常 | 本地计算 | +| 主力资金 | ⚠️ 受限 | 缺少资金流向数据 | +| 自动交易 | ✅ 正常 | 不依赖资金流向 | +| 通知发送 | ✅ 正常 | 独立功能 | + +**结论:没有120积分也能正常使用,只是缺少主力资金流向数据!** + +--- + +## 📊 实际测试结果 + +### 场景一:0积分(仅Token) + +```bash +# 日志输出 +✅ Tushare备用数据源初始化成功 +AKShare获取失败,尝试降级到Tushare +✅ Tushare降级成功(pro_bar接口),获取到数据 +⚠️ Tushare资金流向接口需要120积分,当前积分不足 + +# 结果 +- 行情数据:✅ 成功获取 +- 技术指标:✅ 正常计算 +- 主力资金:❌ 无数据 +- AI决策:✅ 正常运行 +``` + +### 场景二:120+积分 + +```bash +# 日志输出 +✅ Tushare备用数据源初始化成功 +AKShare获取失败,尝试降级到Tushare +✅ Tushare降级成功(基础接口),获取到数据 +✅ Tushare降级成功,获取到资金流向 + +# 结果 +- 行情数据:✅ 成功获取 +- 技术指标:✅ 正常计算 +- 主力资金:✅ 完整数据 +- AI决策:✅ 更准确 +``` + +--- + +## 🎓 Tushare积分详细规则 + +### 积分获取途径 + +| 方式 | 获得积分 | 频率 | 难度 | +|-----|---------|------|------| +| 完善个人信息 | +100 | 一次性 | ⭐ 简单 | +| 每日签到 | +1 | 每天 | ⭐ 简单 | +| 发表文章 | +5~20 | 不限 | ⭐⭐ 中等 | +| 回答问题 | +1~5 | 不限 | ⭐⭐ 中等 | +| 推荐新用户 | +10 | 不限 | ⭐⭐⭐ 较难 | +| 捐赠支持 | 不同 | 不限 | ⭐⭐⭐ 较难 | + +### 积分使用规则 + +- ✅ 积分不会过期 +- ✅ 一次获取永久有效 +- ✅ 调用接口不扣积分 +- ⚠️ 仅用于开通接口权限 + +### 积分查询方式 + +**方法一:网页查询** +``` +https://tushare.pro/user/token +``` + +**方法二:API查询** +```python +import tushare as ts +ts.set_token('your_token') +pro = ts.pro_api() + +# 查询积分(需要token) +print("当前积分:", pro.query('user', fields='id,nick,credits')) +``` + +--- + +## ❓ 常见问题 + +### Q1: 我填写了信息但没有得到100积分? + +**A:** 需要完成以下所有项: +- ✅ 真实姓名 +- ✅ 手机号验证 +- ✅ 邮箱验证 +- ✅ 职业信息 +- ✅ 点击"提交"保存 + +### Q2: 签到在哪里? + +**A:** +1. 登录 https://tushare.pro/ +2. 右上角会显示"每日签到"按钮 +3. 点击即可获得1积分 + +### Q3: 积分够了为什么还是提示权限不足? + +**A:** +1. 退出重新登录Tushare官网 +2. 检查积分是否真的到账 +3. 等待5-10分钟,系统可能有延迟 +4. 重新获取Token并更新.env配置 + +### Q4: 不想获取积分,有其他办法吗? + +**A:** +- ✅ 继续使用AKShare主数据源(大多数时候能用) +- ✅ 不配置Tushare Token(智能盯盘仍可正常运行) +- ⚠️ 但稳定性会降低(~70% vs ~95%) + +### Q5: 积分会扣除吗? + +**A:** +- ❌ 不会!调用接口不扣积分 +- ✅ 积分仅用于开通接口权限 +- ✅ 一次获取,永久有效 + +--- + +## 🔧 配置建议 + +### 推荐配置(最佳体验) + +```env +# .env配置 +TUSHARE_TOKEN="your_token_here" # 已获取120+积分 +``` + +**优点:** +- ✅ 数据获取成功率95%+ +- ✅ 包含主力资金流向 +- ✅ AI决策更准确 + +### 最小配置(基础功能) + +```env +# .env配置 +TUSHARE_TOKEN="your_token_here" # 0积分也可以 +``` + +**优点:** +- ✅ 仍有降级保护 +- ✅ 基础行情可获取 +- ⚠️ 缺少资金流向 + +### 不配置(仅AKShare) + +```env +# 不设置Tushare +``` + +**优点:** +- ✅ 无需注册Tushare +- ⚠️ 稳定性较低(~70%) + +--- + +## 📚 相关链接 + +- **Tushare官网**: https://tushare.pro/ +- **积分规则**: https://tushare.pro/document/1?doc_id=13 +- **接口权限**: https://tushare.pro/document/1?doc_id=108 +- **个人中心**: https://tushare.pro/user/token +- **积分获取**: https://tushare.pro/document/1?doc_id=13 + +--- + +## 💡 总结 + +### 建议操作步骤 + +**Step 1**: 完善Tushare个人信息(获得100积分) +**Step 2**: 连续签到20-30天(获得20-30积分) +**Step 3**: 总计120+积分,解锁所有功能 +**Step 4**: 享受95%+的数据获取成功率! + +### 如果不想获取积分 + +- ✅ 也可以正常使用智能盯盘 +- ✅ AI决策、自动交易都不受影响 +- ⚠️ 只是缺少主力资金流向数据 +- ⚠️ 网络问题时降级效果打折扣 + +**我们仍然推荐获取120积分,提升系统稳定性!** + +--- + +**更新时间**: 2025-10-25 +**版本**: v1.0 + diff --git a/docs/智能盯盘使用指南.md b/docs/智能盯盘使用指南.md new file mode 100644 index 0000000..d1c083d --- /dev/null +++ b/docs/智能盯盘使用指南.md @@ -0,0 +1,219 @@ +# 🤖 智能盯盘使用指南 + +## 📖 项目简介 + +**智能盯盘**是基于 DeepSeek AI 的A股自动化交易决策系统,支持: +- ✅ **DeepSeek AI决策** - 利用DeepSeek-V3强大的推理能力进行股票分析 +- ✅ **A股T+1适配** - 完全遵循A股交易规则 +- ✅ **miniQMT集成** - 支持实盘自动交易 +- ✅ **实时监控** - 24/7持续监控股票并自动决策 +- ✅ **邮件/Webhook通知** - 及时接收交易信号 + +## 🚀 快速开始 + +### 1. 环境配置 + +在项目根目录的 `.env` 文件中添加以下配置: + +```bash +# DeepSeek API配置 +DEEPSEEK_API_KEY=your_deepseek_api_key_here + +# miniQMT配置 +QMT_ACCOUNT_ID=your_qmt_account_id +USE_SIMULATOR=true # true=模拟交易, false=实盘交易 + +# 通知配置(可选) +NOTIFY_EMAIL=your_email@example.com +NOTIFY_WEBHOOK=https://your.webhook.url +``` + +### 2. 安装依赖 + +智能盯盘需要以下额外依赖: + +```bash +pip install akshare # A股数据获取 +pip install xtquant # miniQMT交易接口(可选,如使用实盘交易) +``` + +### 3. 启动程序 + +```bash +streamlit run app.py +``` + +进入主界面后,点击左侧菜单 **"策略分析"** → **"智能盯盘"** + +## 💡 功能说明 + +### 📊 实时分析 + +输入股票代码(如:600519),点击"开始分析",AI会: + +1. **获取实时行情数据** + - 当前价格、涨跌幅 + - 成交量、换手率 + - 技术指标(MA、MACD、RSI、KDJ、布林带等) + - 主力资金流向(如有数据) + +2. **AI决策分析** + - 综合技术面、资金面判断 + - 给出 **买入(BUY)** / **卖出(SELL)** / **持有(HOLD)** 建议 + - 提供详细的决策理由(200-300字) + - 建议仓位、止损位、止盈位 + +3. **自动执行(可选)** + - 勾选"自动交易",AI决策后自动下单 + - 支持T+1规则检查(今天买入的股票明天才能卖) + +### 🎯 监控任务 + +添加监控任务后,系统会定期(如每5分钟)自动分析目标股票: + +**添加任务步骤:** +1. 点击"添加新监控任务" +2. 填写任务名称、股票代码 +3. 设置检查间隔(建议300秒) +4. 选择是否自动交易 +5. 点击"添加任务" + +**任务管理:** +- ▶️ **启动/停止** - 控制任务运行 +- 🗑️ **删除** - 移除监控任务 +- 查看运行状态 + +### 📈 持仓管理 + +查看当前持仓: +- 持仓列表:代码、名称、数量、成本价、现价、盈亏 +- 快速操作:AI分析、卖出(需符合T+1规则) +- 账户概览:总资产、可用资金、总盈亏 + +### 📜 历史记录 + +- **AI决策历史** - 查看所有AI分析记录 +- **交易记录** - 查看买入/卖出记录及盈亏 +- **通知记录** - 查看发送的通知 + +## ⚙️ 系统设置 + +### DeepSeek API配置 + +1. 访问 [DeepSeek官网](https://www.deepseek.com) 注册账号 +2. 获取API Key +3. 在系统设置中填写API Key + +### miniQMT配置 + +**使用模拟交易(推荐新手):** +- 勾选"使用模拟交易" +- 无需真实QMT账户 +- 所有交易仅记录,不实际执行 + +**使用实盘交易(需要miniQMT):** +1. 下载并安装 [miniQMT](https://www.xtp-mini.com/) +2. 启动miniQMT客户端 +3. 获取账户ID +4. 在系统设置中填写账户ID +5. 取消勾选"使用模拟交易" + +### 通知配置 + +**邮件通知:** +- 填写接收通知的邮箱地址 +- 需在 `config.py` 中配置SMTP服务器 + +**Webhook通知:** +- 支持钉钉、企业微信、飞书等 +- 填写Webhook URL +- 可自定义关键词触发 + +## 📚 AI决策逻辑说明 + +### 交易时段判断 + +系统会根据北京时间自动判断交易时段: + +- **集合竞价** (9:00-9:30) - 不可交易,可观察 +- **上午盘** (9:30-11:30) - 波动大,交易活跃 +- **午间休市** (11:30-13:00) - 不可交易 +- **下午盘** (13:00-14:30) - 波动趋缓 +- **尾盘** (14:30-15:00) - 波动大,谨慎操作 +- **盘后/周末** - 不可交易 + +### 买入信号(必须满足至少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. 🔴 技术破位:跌破重要支撑位 + +### T+1规则说明 + +⚠️ **关键限制**: +- 今天买入的股票,**今天不能卖出** +- 必须等到下一个交易日才能卖出 +- 系统会自动检查并提示T+1限制 + +## ⚠️ 风险提示 + +1. **股市有风险,投资需谨慎** +2. AI决策仅供参考,不构成投资建议 +3. 建议先使用模拟交易测试 +4. 严格设置止损,控制单笔仓位(建议≤30%) +5. 不要投入超过承受能力的资金 + +## 🔧 故障排除 + +### Q: 提示"DeepSeek API调用失败" + +**A:** +- 检查API Key是否正确 +- 检查网络连接 +- 确认API账户余额充足 + +### Q: 提示"获取行情数据失败" + +**A:** +- 检查网络连接 +- akshare数据源可能暂时不可用 +- 尝试更换股票代码测试 + +### Q: 提示"miniQMT未连接" + +**A:** +- 如使用模拟交易,勾选"使用模拟交易" +- 如使用实盘,确保miniQMT客户端已启动 +- 检查账户ID是否正确 + +### Q: 买入/卖出失败 + +**A:** +- 检查是否在交易时间(9:30-15:00) +- 检查资金是否充足 +- 卖出时检查是否有可卖数量(T+1限制) +- 买入数量必须是100的整数倍 + +## 📞 技术支持 + +遇到问题或建议? + +- 📧 邮箱:support@example.com +- 💬 Issue:在GitHub上提交Issue +- 📖 文档:查看 `docs/` 目录下的其他文档 + +--- + +**祝您交易顺利!🚀** + diff --git a/docs/智能盯盘持仓管理功能说明.md b/docs/智能盯盘持仓管理功能说明.md new file mode 100644 index 0000000..a9a0163 --- /dev/null +++ b/docs/智能盯盘持仓管理功能说明.md @@ -0,0 +1,432 @@ +# 智能盯盘 - 持仓管理功能说明 + +## 🎯 功能概述 + +智能盯盘现已支持**持仓管理功能**,您可以在添加监控任务时: +- ✅ 标记是否已持仓该股票 +- ✅ 填入持仓成本和数量 +- ✅ 实时查看持仓盈亏 +- ✅ AI决策时考虑持仓情况 + +这对于已经持有股票并希望智能监控的用户非常实用! + +--- + +## 🆕 新增功能 + +### 1. 添加任务时设置持仓信息 + +在"添加新监控任务"表单中,新增了持仓信息输入: + +**📊 持仓信息区域:** +- **已持仓该股票** - 勾选表示您已持有该股票 +- **持仓成本(元)** - 填入买入时的成本价格 +- **持仓数量(股)** - 填入当前持有的股票数量 + +**示例:** +``` +任务名称: 茅台盯盘 +股票代码: 600519 +检查间隔: 300秒 + +📊 持仓信息: +☑ 已持仓该股票 +持仓成本: 1650.50元 +持仓数量: 200股 +``` + +### 2. 任务列表显示盈亏 + +任务列表现在会实时显示持仓盈亏: + +**显示内容:** +- 📊 持仓信息(数量和成本) +- 💰 实时盈亏金额和百分比 +- 颜色标识: + - 🟢 绿色 - 盈利 + - 🔴 红色 - 亏损 + - ⚪ 灰色 - 持平 + +**示例显示:** +``` +任务名称: 茅台盯盘 +600519 - 间隔300秒 +✅ 已启用 +🤖 自动交易 +📊 持仓: 200股 @ 1650.50元 + +▶️ 运行中 +💰 +5,240.00元 (+15.86%) +``` + +### 3. AI决策考虑持仓情况 + +当您设置了持仓信息后,AI决策会: + +**针对已持仓股票:** +- ✅ 显示当前盈亏状态 +- ✅ 建议止盈/止损时机 +- ✅ 评估是否加仓机会 +- ✅ 考虑持仓天数(T+1规则) + +**AI决策示例:** +``` +[POSITION] 当前持仓(600519) ⭐ 重要 +═══════════════════════════════════════ +持仓数量: 200股 +成本价: ¥1650.50 +当前价: ¥1676.70 +持仓市值: ¥335,340.00 +浮动盈亏: ¥5,240.00 (+1.59%) + +⚠️ T+1限制: 该股票可以卖出(不受T+1限制) + +💡 决策建议: +- 如果盈利且技术指标转弱 → 建议止盈卖出 +- 如果亏损超过止损线(通常-5%)→ 建议止损卖出 +- 如果技术指标强势且未到止盈位 → 建议继续持有 +- 如果盈利且看好后市 → 可考虑加仓(但注意仓位控制) +``` + +--- + +## 📊 使用场景 + +### 场景1: 已持仓股票监控 + +**适用情况:** +- 您已经买入某只股票 +- 希望AI帮您监控卖出时机 +- 需要实时了解盈亏情况 + +**操作步骤:** +1. 点击"添加新监控任务" +2. 填写股票代码 +3. ☑ 勾选"已持仓该股票" +4. 填入持仓成本和数量 +5. 可开启"自动交易"(AI建议卖出时自动执行) +6. 点击"添加任务" + +**结果:** +- ✅ 任务列表实时显示盈亏 +- ✅ AI决策会考虑您的持仓成本 +- ✅ 触发卖出信号时及时通知 +- ✅ 自动交易可自动止盈/止损 + +### 场景2: 未持仓股票监控 + +**适用情况:** +- 您还没买入该股票 +- 希望AI帮您监控买入时机 + +**操作步骤:** +1. 点击"添加新监控任务" +2. 填写股票代码 +3. ❌ 不勾选"已持仓该股票" +4. 设置仓位百分比(新建仓位时使用) +5. 可开启"自动交易" +6. 点击"添加任务" + +**结果:** +- ✅ AI决策会寻找买入机会 +- ✅ 触发买入信号时及时通知 +- ✅ 自动交易可自动建仓 + +### 场景3: 分批建仓/加仓 + +**适用情况:** +- 已有一部分持仓 +- 希望在合适时机加仓 + +**操作步骤:** +1. 设置已持仓信息(当前持仓) +2. 保持"仓位百分比"设置(加仓时使用) +3. 开启自动交易 +4. AI会评估: + - 如果技术强势 → 建议加仓 + - 如果技术转弱 → 建议减仓 + +--- + +## 🔍 功能详解 + +### 持仓成本的作用 + +**在AI决策中:** +1. **计算盈亏** - 当前价 vs 成本价 +2. **止损判断** - 是否达到止损线(如-5%) +3. **止盈判断** - 是否达到止盈线(如+10%) +4. **加仓评估** - 是否适合补仓或加仓 + +**示例分析:** +``` +持仓成本: ¥50.00 +当前价格: ¥47.50 +盈亏: -5.00% (达到止损线) + +AI决策: SELL (止损卖出) +理由: 亏损已达到-5%止损线,技术指标转弱, + 建议止损以避免更大亏损 +``` + +### 持仓数量的作用 + +**在AI决策中:** +1. **仓位计算** - 当前持仓占比 +2. **风险评估** - 是否过度集中 +3. **加仓空间** - 还能加多少仓位 + +**示例分析:** +``` +账户总资产: ¥100,000 +持仓市值: ¥30,000 (30%) +仓位状态: 适中 + +AI决策: 可以考虑小幅加仓 +理由: 仓位未超过30%警戒线,技术面强势, + 可适当增加仓位 +``` + +--- + +## 💡 最佳实践 + +### 1. 准确填写持仓信息 + +**重要提示:** +- ✅ 填写真实的成本价(不是当前价) +- ✅ 填写实际持有数量 +- ❌ 不要虚高或虚低填写 + +**为什么:** +- 准确的持仓信息 → 准确的盈亏计算 +- 准确的盈亏 → 更合理的AI决策 +- 合理的决策 → 更好的交易结果 + +### 2. 及时更新持仓 + +**需要更新的情况:** +- ✅ 手动买入/卖出后 +- ✅ 自动交易执行后 +- ✅ 分批建仓/减仓后 + +**如何更新:** +1. 删除旧任务 +2. 添加新任务(填写新的持仓信息) +3. 或手动编辑数据库(高级用户) + +### 3. 合理设置自动交易 + +**建议:** +- 🟢 **已持仓** + 自动交易 = 自动止盈/止损 +- 🟡 **未持仓** + 自动交易 = 谨慎使用 +- 🔴 新手建议先不开启自动交易 + +**原因:** +- 已持仓:主要是卖出决策,相对安全 +- 未持仓:涉及买入决策,需要更谨慎 +- 新手:先观察AI决策准确性 + +### 4. 设置合理止损止盈 + +**默认建议:** +- 止损线:-5%(可根据风险承受能力调整) +- 止盈线:+10%(可根据预期收益调整) + +**调整建议:** +- 高风险股票:止损-3%,止盈+8% +- 稳健股票:止损-7%,止盈+15% +- 根据实际情况灵活调整 + +--- + +## 📈 实际案例 + +### 案例1: 止盈卖出 + +**背景:** +``` +股票: 600519 贵州茅台 +持仓成本: ¥1,650.00 +持仓数量: 100股 +当前价格: ¥1,815.00 +浮动盈亏: +10.00% (+¥16,500) +``` + +**AI决策:** +``` +决策: SELL (止盈卖出) +信心度: 85% + +理由: +- 盈利已达10%止盈线 +- MACD出现死叉信号 +- RSI(6)=78 进入超买区 +- 成交量明显萎缩 + +建议: 止盈卖出,落袋为安 +``` + +### 案例2: 止损卖出 + +**背景:** +``` +股票: 000001 平安银行 +持仓成本: ¥12.50 +持仓数量: 1000股 +当前价格: ¥11.88 +浮动盈亏: -4.96% (-¥620) +``` + +**AI决策:** +``` +决策: SELL (止损卖出) +信心度: 90% + +理由: +- 亏损接近-5%止损线 +- 技术面全面转弱 +- MA5下穿MA20死叉 +- 主力资金持续流出 + +建议: 及时止损,避免更大亏损 +``` + +### 案例3: 持有观望 + +**背景:** +``` +股票: 300750 宁德时代 +持仓成本: ¥180.00 +持仓数量: 200股 +当前价格: ¥183.50 +浮动盈亏: +1.94% (+¥700) +``` + +**AI决策:** +``` +决策: HOLD (继续持有) +信心度: 75% + +理由: +- 盈利未达止盈线 +- 技术面保持强势 +- MACD金叉持续 +- 主力资金持续流入 +- 量价配合良好 + +建议: 继续持有,等待更大收益 +``` + +--- + +## ⚙️ 技术实现 + +### 数据库结构 + +新增字段到 `monitor_tasks` 表: + +```sql +has_position INTEGER DEFAULT 0 -- 是否已持仓 +position_cost REAL DEFAULT 0 -- 持仓成本 +position_quantity INTEGER DEFAULT 0 -- 持仓数量 +position_date TEXT -- 建仓日期 +``` + +### AI决策逻辑 + +```python +def analyze_stock_and_decide( + stock_code: str, + market_data: Dict, + account_info: Dict, + has_position: bool = False, + position_cost: float = 0, + position_quantity: int = 0 +) -> Dict: + # 计算实时盈亏 + current_price = market_data['current_price'] + profit_loss = (current_price - position_cost) * position_quantity + profit_loss_pct = (profit_loss / (position_cost * position_quantity)) * 100 + + # 构建包含持仓信息的Prompt + prompt = build_prompt_with_position( + stock_code, market_data, account_info, + has_position, position_cost, position_quantity, + profit_loss, profit_loss_pct + ) + + # 调用DeepSeek AI + decision = call_deepseek_api(prompt) + + return decision +``` + +--- + +## 🔧 故障排查 + +### 问题1: 盈亏显示不准确 + +**可能原因:** +- 持仓成本填写错误 +- 持仓数量填写错误 +- 获取实时价格失败 + +**解决方案:** +1. 检查持仓信息是否正确 +2. 重新创建监控任务 +3. 查看日志确认数据获取情况 + +### 问题2: AI决策未考虑持仓 + +**可能原因:** +- 持仓信息未勾选 +- 持仓数量为0 +- 持仓成本为0 + +**解决方案:** +1. 确认勾选"已持仓该股票" +2. 确认填写了正确的成本和数量 +3. 查看日志确认参数传递 + +### 问题3: 任务列表不显示盈亏 + +**可能原因:** +- 数据获取失败 +- 网络问题 +- 数据源不可用 + +**解决方案:** +1. 刷新页面重试 +2. 检查网络连接 +3. 查看终端日志 + +--- + +## 📚 相关文档 + +- [智能盯盘快速测试](./智能盯盘快速测试.md) +- [智能盯盘配置说明](./智能盯盘配置说明.md) +- [智能盯盘数据源降级说明](./智能盯盘数据源降级说明.md) +- [智能盯盘集成说明](./智能盯盘集成说明.md) + +--- + +## ✅ 总结 + +持仓管理功能的优势: + +1. **✅ 精准决策** - AI考虑您的实际持仓情况 +2. **✅ 实时盈亏** - 一目了然的盈亏显示 +3. **✅ 止盈止损** - 自动触发止盈止损信号 +4. **✅ 风险控制** - 基于持仓的仓位管理 +5. **✅ 便捷管理** - 集中管理所有持仓监控 + +**开始使用:** 立即添加您的持仓股票,让AI为您智能盯盘! + +--- + +**更新时间:** 2025-10-25 +**版本:** v1.0 + diff --git a/docs/智能盯盘配置说明.md b/docs/智能盯盘配置说明.md new file mode 100644 index 0000000..6a94977 --- /dev/null +++ b/docs/智能盯盘配置说明.md @@ -0,0 +1,263 @@ +# 智能盯盘 - 配置说明 + +## 📋 配置概览 + +智能盯盘使用主程序的**统一配置管理系统**,与其他功能模块共享配置。所有配置项通过主程序的 **"环境配置"** 页面进行管理。 + +--- + +## 🔧 配置项说明 + +### 1. 🤖 DeepSeek AI配置(必需) + +智能盯盘的AI决策引擎,用于分析市场数据并做出交易决策。 + +| 配置项 | 环境变量 | 说明 | 是否必需 | +|--------|---------|------|---------| +| DeepSeek API Key | `DEEPSEEK_API_KEY` | DeepSeek API密钥 | ✅ 是 | +| DeepSeek API地址 | `DEEPSEEK_BASE_URL` | API服务地址(默认官方) | ❌ 否 | + +**获取方式:** +1. 访问 [DeepSeek官网](https://platform.deepseek.com/) +2. 注册账号并创建API密钥 +3. 在主程序"环境配置"中填入 + +--- + +### 2. 🔌 MiniQMT配置(可选) + +用于实盘交易,如不配置则使用模拟交易。 + +| 配置项 | 环境变量 | 说明 | 默认值 | +|--------|---------|------|--------| +| 启用MiniQMT | `MINIQMT_ENABLED` | 是否启用实盘交易 | `false` | +| MiniQMT账户ID | `MINIQMT_ACCOUNT_ID` | QMT交易账户ID | 无 | +| MiniQMT服务器 | `MINIQMT_HOST` | QMT服务地址 | `127.0.0.1` | +| MiniQMT端口 | `MINIQMT_PORT` | QMT服务端口 | `58610` | + +**说明:** +- `MINIQMT_ENABLED=false`: 使用模拟交易(安全,推荐新手) +- `MINIQMT_ENABLED=true`: 使用实盘交易(需要miniQMT环境) + +**实盘交易要求:** +1. 已安装并运行miniQMT客户端 +2. 已配置交易账户 +3. 账户有足够资金和权限 +4. 详见:`docs/MINIQMT_INTEGRATION_GUIDE.md` + +--- + +### 3. 📧 邮件通知配置(可选) + +用于接收交易决策和执行结果的邮件通知。 + +| 配置项 | 环境变量 | 说明 | 示例 | +|--------|---------|------|------| +| 启用邮件通知 | `EMAIL_ENABLED` | 是否启用邮件 | `true` | +| SMTP服务器 | `SMTP_SERVER` | 邮件服务器地址 | `smtp.qq.com` | +| SMTP端口 | `SMTP_PORT` | 邮件服务器端口 | `587` | +| 发件人邮箱 | `EMAIL_FROM` | 发送邮件的邮箱 | `your@qq.com` | +| 邮箱授权码 | `EMAIL_PASSWORD` | SMTP授权码 | `xxx` | +| 收件人邮箱 | `EMAIL_TO` | 接收通知的邮箱 | `notify@qq.com` | + +**配置参考:** +- 详见:`docs/邮件配置指南.md` + +--- + +### 4. 🔔 Webhook通知配置(可选) + +支持钉钉、飞书机器人推送交易通知。 + +| 配置项 | 环境变量 | 说明 | 示例 | +|--------|---------|------|------| +| 启用Webhook | `WEBHOOK_ENABLED` | 是否启用Webhook | `true` | +| Webhook类型 | `WEBHOOK_TYPE` | `dingtalk`或`feishu` | `dingtalk` | +| Webhook地址 | `WEBHOOK_URL` | 机器人Webhook URL | `https://...` | +| 自定义关键词 | `WEBHOOK_KEYWORD` | 钉钉安全验证关键词 | `aiagents通知` | + +**配置参考:** +- 钉钉:`docs/Webhook钉钉关键词快速配置指南.md` +- 飞书:`docs/Webhook通知配置指南.md` + +--- + +## 🚀 配置步骤 + +### 步骤1: 打开环境配置 + +在主程序左侧菜单中,点击 **"环境配置"** 按钮。 + +### 步骤2: 填写必需配置 + +至少需要配置: +- ✅ **DeepSeek API Key** (必需) +- 其他配置项可选 + +### 步骤3: 保存配置 + +点击 **"保存配置"** 按钮,配置将保存到 `.env` 文件。 + +### 步骤4: 验证配置 + +1. 返回 **"智能盯盘"** 页面 +2. 点击 **"系统设置"** 标签 +3. 查看配置状态是否正常 + +### 步骤5: 刷新页面 + +如果修改了配置: +- 点击 **"重新加载配置"** 按钮 +- 或按 `Ctrl+R` 刷新浏览器 + +--- + +## 📊 配置示例 + +### 最小配置(仅AI决策 + 模拟交易) + +```env +# DeepSeek AI(必需) +DEEPSEEK_API_KEY="sk-xxxxxxxxxxxxxx" + +# 其他默认值 +MINIQMT_ENABLED="false" # 使用模拟交易 +EMAIL_ENABLED="false" # 不发送邮件 +WEBHOOK_ENABLED="false" # 不发送Webhook +``` + +### 完整配置(实盘 + 通知) + +```env +# DeepSeek AI +DEEPSEEK_API_KEY="sk-xxxxxxxxxxxxxx" +DEEPSEEK_BASE_URL="https://api.deepseek.com/v1" + +# MiniQMT实盘交易 +MINIQMT_ENABLED="true" +MINIQMT_ACCOUNT_ID="888888888" +MINIQMT_HOST="127.0.0.1" +MINIQMT_PORT="58610" + +# 邮件通知 +EMAIL_ENABLED="true" +SMTP_SERVER="smtp.qq.com" +SMTP_PORT="587" +EMAIL_FROM="yourmail@qq.com" +EMAIL_PASSWORD="smtp_auth_code" +EMAIL_TO="notify@qq.com" + +# 钉钉通知 +WEBHOOK_ENABLED="true" +WEBHOOK_TYPE="dingtalk" +WEBHOOK_URL="https://oapi.dingtalk.com/robot/send?access_token=xxx" +WEBHOOK_KEYWORD="aiagents通知" +``` + +--- + +## ⚠️ 注意事项 + +### 1. 配置安全 +- ❌ 不要将 `.env` 文件提交到版本控制 +- ✅ API Key和密码要妥善保管 +- ✅ 定期更换重要凭证 + +### 2. 实盘交易风险 +- ⚠️ 首次使用建议用模拟模式测试 +- ⚠️ 实盘交易有资金风险,谨慎操作 +- ⚠️ 建议设置合理的止损止盈参数 + +### 3. 配置变更 +- 修改配置后需要重新加载或重启程序 +- 部分配置(如miniQMT连接)可能需要重启服务 + +### 4. 通知频率 +- 实时监控会产生较多通知 +- 建议根据需要开启/关闭通知渠道 +- 可以通过监控频率控制通知数量 + +--- + +## 🔍 故障排查 + +### 问题1: DeepSeek API调用失败 + +**症状:** 分析时提示API错误 + +**解决:** +1. 检查API Key是否正确 +2. 检查网络连接 +3. 检查API额度是否充足 +4. 查看 `logs/` 目录下的日志 + +### 问题2: miniQMT连接失败 + +**症状:** 提示miniQMT连接失败 + +**解决:** +1. 确认miniQMT客户端已运行 +2. 检查账户ID是否正确 +3. 检查服务地址和端口 +4. 查看miniQMT客户端状态 + +### 问题3: 邮件发送失败 + +**症状:** 通知未收到邮件 + +**解决:** +1. 检查SMTP配置是否正确 +2. 确认使用授权码而非密码 +3. 检查邮件服务器端口(587/465) +4. 查看日志中的错误信息 + +### 问题4: Webhook通知失败 + +**症状:** 钉钉/飞书未收到消息 + +**解决:** +1. 检查Webhook URL是否正确 +2. 钉钉需要配置自定义关键词 +3. 测试Webhook是否可访问 +4. 检查机器人是否被禁用 + +--- + +## 📚 相关文档 + +- [智能盯盘快速测试](./智能盯盘快速测试.md) +- [智能盯盘集成说明](./智能盯盘集成说明.md) +- [MiniQMT集成指南](./MINIQMT_INTEGRATION_GUIDE.md) +- [邮件配置指南](./邮件配置指南.md) +- [Webhook通知配置指南](./Webhook通知配置指南.md) +- [环境配置快速指南](./环境配置快速指南.md) + +--- + +## 💡 最佳实践 + +1. **循序渐进** + - 先用最小配置(仅DeepSeek + 模拟交易)测试 + - 验证AI决策逻辑后再启用实盘 + - 逐步开启通知功能 + +2. **风险控制** + - 设置合理的仓位比例(建议≤30%) + - 设置止损止盈参数 + - 小资金测试后再加大投入 + +3. **监控管理** + - 定期查看决策历史 + - 分析AI决策准确率 + - 根据效果调整策略 + +4. **配置备份** + - 定期备份 `.env` 文件 + - 记录重要配置参数 + - 测试环境与生产环境分离 + +--- + +**更新时间:** 2025-10-25 +**版本:** v1.0 + diff --git a/longhubang.db b/longhubang.db index cae594a5e0bec40dce7d6925fa4247816bc44d20..f34c53dd20dd5da0779cb9e7cfbe0ed217934972 100644 GIT binary patch delta 67209 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 delta 7344 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.10.2 reportlab>=4.0.0 peewee>=3.17.0 -schedule>=1.2.0 +schedule>=1.2.0 pywencai>=0.7.0 \ No newline at end of file diff --git a/smart_monitor.db b/smart_monitor.db new file mode 100644 index 0000000000000000000000000000000000000000..4e45db1d9ce155d882a8cd2c7acc412dc61ed5d2 GIT binary patch literal 131072 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 Optional[Dict]: + """ + 获取实时行情(带重试和降级机制) + 优先使用AKShare,失败时降级到Tushare + + Args: + stock_code: 股票代码(如:600519) + retry: 重试次数(默认1次,避免IP封禁) + + Returns: + 实时行情数据 + """ + import time + + # 方法1: 组合使用分钟行情 + 基本信息(最可靠) + for attempt in range(retry): + try: + # 1.1 获取股票基本信息(名称) + info_df = ak.stock_individual_info_em(symbol=stock_code) + stock_name = 'N/A' + if not info_df.empty: + info_dict = dict(zip(info_df['item'], info_df['value'])) + stock_name = info_dict.get('股票简称', 'N/A') + + # 1.2 获取分钟级实时行情 + min_df = ak.stock_zh_a_hist_min_em(symbol=stock_code, period='1', adjust='') + + if min_df.empty: + self.logger.warning(f"AKShare未找到股票 {stock_code} 的分钟行情数据") + if attempt < retry - 1: + time.sleep(2) + continue + break + + # 1.3 获取历史数据(计算昨收) + hist_df = ak.stock_zh_a_hist(symbol=stock_code, period='daily', adjust='') + + # 提取最新分钟数据 + latest = min_df.iloc[-1] + current_price = float(latest['收盘']) + + # 计算昨收和涨跌幅 + if len(hist_df) >= 2: + pre_close = float(hist_df.iloc[-2]['收盘']) + else: + pre_close = current_price + + change_amount = current_price - pre_close + change_pct = (change_amount / pre_close * 100) if pre_close > 0 else 0 + + # 从历史数据获取今天的统计数据 + if len(hist_df) >= 1: + today_data = hist_df.iloc[-1] + daily_volume = float(today_data.get('成交量', 0)) + daily_amount = float(today_data.get('成交额', 0)) + daily_high = float(today_data.get('最高', 0)) + daily_low = float(today_data.get('最低', 0)) + daily_open = float(today_data.get('开盘', 0)) + turnover_rate = float(today_data.get('换手率', 0)) + else: + # 使用分钟数据 + daily_volume = min_df['成交量'].sum() + daily_amount = min_df['成交额'].sum() + daily_high = min_df['最高'].max() + daily_low = min_df['最低'].min() + daily_open = float(min_df.iloc[0]['开盘']) + turnover_rate = 0.0 + + self.logger.info(f"✅ AKShare成功获取 {stock_code} ({stock_name}) 实时行情") + + return { + 'code': stock_code, + 'name': stock_name, + 'current_price': current_price, + 'change_pct': change_pct, + 'change_amount': change_amount, + 'volume': daily_volume, # 手 + 'amount': daily_amount, # 元 + 'high': daily_high, + 'low': daily_low, + 'open': daily_open, + 'pre_close': pre_close, + 'turnover_rate': turnover_rate, + 'volume_ratio': 1.0, + 'update_time': str(latest['时间']), + 'data_source': 'akshare' + } + + except Exception as e: + if attempt < retry - 1: + self.logger.warning(f"AKShare获取失败 {stock_code},第{attempt+1}次重试... 错误: {type(e).__name__}: {str(e)[:50]}") + time.sleep(2) # 等待2秒后重试 + else: + self.logger.warning(f"AKShare获取失败 {stock_code}(已重试{retry}次),尝试降级") + + # 降级到Tushare + if self.ts_pro: + self.logger.info(f"降级到Tushare获取 {stock_code}...") + return self._get_realtime_quote_from_tushare(stock_code) + else: + self.logger.error(f"AKShare失败且未配置Tushare,无法获取 {stock_code} 行情") + return None + + def get_technical_indicators(self, stock_code: str, period: str = 'daily', retry: int = 1) -> Optional[Dict]: + """ + 计算技术指标(带降级机制) + 优先使用AKShare,失败时降级到Tushare + + Args: + stock_code: 股票代码 + period: 周期(daily/weekly/monthly) + retry: 重试次数(默认2次) + + Returns: + 技术指标数据 + """ + import time + + # 方法1: 尝试使用AKShare + for attempt in range(retry): + try: + # 获取历史数据(最近200个交易日,用于计算指标) + end_date = datetime.now().strftime('%Y%m%d') + start_date = (datetime.now() - timedelta(days=300)).strftime('%Y%m%d') + + # 获取历史数据 + df = ak.stock_zh_a_hist( + symbol=stock_code, + period=period, + start_date=start_date, + end_date=end_date, + adjust="qfq" # 前复权 + ) + + if df.empty or len(df) < 60: + if attempt < retry - 1: + self.logger.warning(f"AKShare历史数据不足 {stock_code},第{attempt+1}次重试...") + time.sleep(1) + continue + else: + self.logger.warning(f"AKShare历史数据不足 {stock_code},尝试降级") + break + + # 数据充足,计算技术指标 + return self._calculate_all_indicators(df, stock_code) + + except Exception as e: + if attempt < retry - 1: + self.logger.warning(f"AKShare获取历史数据失败 {stock_code},第{attempt+1}次重试... 错误: {type(e).__name__}: {str(e)[:50]}") + time.sleep(1) + else: + self.logger.warning(f"AKShare获取历史数据失败 {stock_code}(已重试{retry}次),尝试降级到Tushare") + break + + # 方法2: 降级到Tushare + if self.ts_pro: + self.logger.info(f"降级到Tushare获取 {stock_code} 历史数据...") + return self._get_technical_indicators_from_tushare(stock_code, period) + else: + self.logger.error(f"AKShare失败且未配置Tushare,无法获取 {stock_code} 技术指标") + return None + + def _calculate_all_indicators(self, df: pd.DataFrame, stock_code: str) -> Optional[Dict]: + """ + 根据历史数据计算所有技术指标 + + Args: + df: 历史数据DataFrame + stock_code: 股票代码 + + Returns: + 技术指标数据 + """ + try: + if df.empty or len(df) < 60: + self.logger.warning(f"股票 {stock_code} 历史数据不足") + return None + + # 计算均线 + df['ma5'] = df['收盘'].rolling(window=5).mean() + df['ma20'] = df['收盘'].rolling(window=20).mean() + df['ma60'] = df['收盘'].rolling(window=60).mean() + + # 计算MACD + df = self._calculate_macd(df) + + # 计算RSI + df = self._calculate_rsi(df, periods=[6, 12, 24]) + + # 计算KDJ + df = self._calculate_kdj(df) + + # 计算布林带 + df = self._calculate_bollinger(df) + + # 计算量能均线 + df['vol_ma5'] = df['成交量'].rolling(window=5).mean() + df['vol_ma10'] = df['成交量'].rolling(window=10).mean() + + # 取最后一行数据 + latest = df.iloc[-1] + + # 判断趋势 + current_price = float(latest['收盘']) + ma5 = float(latest['ma5']) + ma20 = float(latest['ma20']) + ma60 = float(latest['ma60']) + + if current_price > ma5 > ma20 > ma60: + trend = 'up' + elif current_price < ma5 < ma20 < ma60: + trend = 'down' + else: + trend = 'sideways' + + # 布林带位置 + boll_upper = float(latest['boll_upper']) + boll_mid = float(latest['boll_mid']) + boll_lower = float(latest['boll_lower']) + + if current_price >= boll_upper: + boll_position = '上轨附近(超买)' + elif current_price <= boll_lower: + boll_position = '下轨附近(超卖)' + elif current_price > boll_mid: + boll_position = '中轨上方' + else: + boll_position = '中轨下方' + + return { + 'ma5': ma5, + 'ma20': ma20, + 'ma60': ma60, + 'trend': trend, + 'macd_dif': float(latest['dif']), + 'macd_dea': float(latest['dea']), + 'macd': float(latest['macd']), + 'rsi6': float(latest['rsi6']), + 'rsi12': float(latest['rsi12']), + 'rsi24': float(latest['rsi24']), + 'kdj_k': float(latest['kdj_k']), + 'kdj_d': float(latest['kdj_d']), + 'kdj_j': float(latest['kdj_j']), + 'boll_upper': boll_upper, + 'boll_mid': boll_mid, + 'boll_lower': boll_lower, + 'boll_position': boll_position, + 'vol_ma5': float(latest['vol_ma5']), + 'volume_ratio': float(latest['成交量']) / float(latest['vol_ma5']) if latest['vol_ma5'] > 0 else 1.0 + } + + except Exception as e: + self.logger.error(f"计算技术指标失败 {stock_code}: {e}") + return None + + def _get_technical_indicators_from_tushare(self, stock_code: str, period: str = 'daily') -> Optional[Dict]: + """ + 使用Tushare获取历史数据并计算技术指标 + + Args: + stock_code: 股票代码(6位) + period: 周期(daily/weekly/monthly) + + Returns: + 技术指标数据 + """ + try: + # 转换股票代码格式(Tushare格式:600519.SH, 000001.SZ) + if stock_code.startswith('6'): + ts_code = f"{stock_code}.SH" + elif stock_code.startswith(('0', '3')): + ts_code = f"{stock_code}.SZ" + else: + ts_code = stock_code + + # 计算日期范围 + end_date = datetime.now().strftime('%Y%m%d') + start_date = (datetime.now() - timedelta(days=400)).strftime('%Y%m%d') + + # 获取历史数据 + df = self.ts_pro.daily( + ts_code=ts_code, + start_date=start_date, + end_date=end_date + ) + + if df is None or df.empty: + self.logger.error(f"Tushare未返回 {stock_code} 的历史数据") + return None + + # Tushare数据是从新到旧,需要反转 + df = df.sort_values('trade_date', ascending=True).reset_index(drop=True) + + if len(df) < 60: + self.logger.warning(f"Tushare历史数据不足 {stock_code}(仅{len(df)}条)") + return None + + # 统一列名为AKShare格式(完整映射) + df = df.rename(columns={ + 'open': '开盘', + 'high': '最高', + 'low': '最低', + 'close': '收盘', + 'vol': '成交量', + 'amount': '成交额', + 'trade_date': '日期' + }) + + # 如果没有关键列,尝试使用其他可能的列名 + column_mapping = { + '开盘': ['open', 'Open', 'OPEN'], + '最高': ['high', 'High', 'HIGH'], + '最低': ['low', 'Low', 'LOW'], + '收盘': ['close', 'Close', 'CLOSE'], + '成交量': ['vol', 'volume', 'Volume', 'VOLUME'], + '成交额': ['amount', 'Amount', 'AMOUNT'] + } + + for target_col, possible_cols in column_mapping.items(): + if target_col not in df.columns: + for col in possible_cols: + if col in df.columns: + df[target_col] = df[col] + break + + # 确认必需的列存在 + required_cols = ['开盘', '最高', '最低', '收盘', '成交量'] + missing_cols = [col for col in required_cols if col not in df.columns] + if missing_cols: + self.logger.error(f"Tushare数据缺少列 {stock_code}: {missing_cols}") + return None + + self.logger.info(f"✅ Tushare成功获取 {stock_code} 历史数据,共{len(df)}条") + + # 使用统一的计算方法 + return self._calculate_all_indicators(df, stock_code) + + except Exception as e: + self.logger.error(f"Tushare获取历史数据失败 {stock_code}: {type(e).__name__}: {str(e)}") + import traceback + self.logger.debug(traceback.format_exc()) + return None + + def get_main_force_flow(self, stock_code: str, retry: int = 2) -> Optional[Dict]: + """ + 获取主力资金流向(带重试机制) + + Args: + stock_code: 股票代码 + retry: 重试次数(默认2次) + + Returns: + 主力资金数据 + """ + import time + + for attempt in range(retry): + try: + # 获取个股资金流(新版AKShare API参数调整) + try: + df = ak.stock_individual_fund_flow_rank(market="今日") + except TypeError: + # 如果market参数也不支持,尝试无参数调用 + try: + df = ak.stock_individual_fund_flow_rank() + except TypeError as te: + self.logger.warning(f"AKShare API参数不兼容: {te}") + return None + + stock_data = df[df['代码'] == stock_code] + + if stock_data.empty: + self.logger.warning(f"未找到股票 {stock_code} 的资金流向数据") + return None + + row = stock_data.iloc[0] + + # 主力净额 + main_net = float(row.get('主力净流入-净额', 0)) / 10000 # 转换为万元 + main_net_pct = float(row.get('主力净流入-净占比', 0)) + + # 判断主力动向 + if main_net > 0 and main_net_pct > 5: + trend = '大幅流入' + elif main_net > 0: + trend = '小幅流入' + elif main_net < 0 and main_net_pct < -5: + trend = '大幅流出' + elif main_net < 0: + trend = '小幅流出' + else: + trend = '观望' + + return { + 'main_net': main_net, # 万元 + 'main_net_pct': main_net_pct, # 百分比 + 'super_net': float(row.get('超大单净流入-净额', 0)) / 10000, + 'big_net': float(row.get('大单净流入-净额', 0)) / 10000, + 'mid_net': float(row.get('中单净流入-净额', 0)) / 10000, + 'small_net': float(row.get('小单净流入-净额', 0)) / 10000, + 'trend': trend, + 'data_source': 'akshare' + } + + except Exception as e: + if attempt < retry - 1: + self.logger.warning(f"AKShare获取资金流向失败 {stock_code},第{attempt+1}次重试... 错误: {type(e).__name__}") + time.sleep(1) # 等待1秒后重试 + else: + self.logger.warning(f"AKShare获取资金流向失败 {stock_code}(已重试{retry}次),尝试降级到Tushare") + break + + # 降级到Tushare + if self.ts_pro: + return self._get_main_force_from_tushare(stock_code) + else: + self.logger.error(f"AKShare失败且未配置Tushare,无法获取 {stock_code} 资金流向") + return None + + def get_comprehensive_data(self, stock_code: str) -> Dict: + """ + 获取综合数据(实时行情+技术指标) + 注意:已移除主力资金流向数据,因为该接口不稳定且AI决策不依赖此数据 + + Args: + stock_code: 股票代码 + + Returns: + 综合数据 + """ + result = {} + + # 实时行情 + quote = self.get_realtime_quote(stock_code) + if quote: + result.update(quote) + + # 技术指标 + indicators = self.get_technical_indicators(stock_code) + if indicators: + result.update(indicators) + + # 主力资金(已禁用 - 接口不稳定) + # main_force = self.get_main_force_flow(stock_code) + # if main_force: + # result['main_force'] = main_force + + return result + + # ========== 技术指标计算方法 ========== + + def _calculate_macd(self, df: pd.DataFrame, + fast: int = 12, slow: int = 26, signal: int = 9) -> pd.DataFrame: + """计算MACD指标""" + ema_fast = df['收盘'].ewm(span=fast, adjust=False).mean() + ema_slow = df['收盘'].ewm(span=slow, adjust=False).mean() + + df['dif'] = ema_fast - ema_slow + df['dea'] = df['dif'].ewm(span=signal, adjust=False).mean() + df['macd'] = (df['dif'] - df['dea']) * 2 + + return df + + def _calculate_rsi(self, df: pd.DataFrame, periods: list = [6, 12, 24]) -> pd.DataFrame: + """计算RSI指标""" + for period in periods: + delta = df['收盘'].diff() + gain = (delta.where(delta > 0, 0)).rolling(window=period).mean() + loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean() + + rs = gain / loss + df[f'rsi{period}'] = 100 - (100 / (1 + rs)) + + return df + + def _calculate_kdj(self, df: pd.DataFrame, n: int = 9, + m1: int = 3, m2: int = 3) -> pd.DataFrame: + """计算KDJ指标""" + low_list = df['最低'].rolling(window=n).min() + high_list = df['最高'].rolling(window=n).max() + + rsv = (df['收盘'] - low_list) / (high_list - low_list) * 100 + + df['kdj_k'] = rsv.ewm(com=m1-1, adjust=False).mean() + df['kdj_d'] = df['kdj_k'].ewm(com=m2-1, adjust=False).mean() + df['kdj_j'] = 3 * df['kdj_k'] - 2 * df['kdj_d'] + + return df + + def _calculate_bollinger(self, df: pd.DataFrame, + period: int = 20, std_num: int = 2) -> pd.DataFrame: + """计算布林带""" + df['boll_mid'] = df['收盘'].rolling(window=period).mean() + std = df['收盘'].rolling(window=period).std() + + df['boll_upper'] = df['boll_mid'] + std_num * std + df['boll_lower'] = df['boll_mid'] - std_num * std + + return df + + + # ========== Tushare备用数据源方法 ========== + + def _get_realtime_quote_from_tushare(self, stock_code: str) -> Optional[Dict]: + """ + 从Tushare获取实时行情(备用数据源) + 使用免费接口,无需积分 + + Args: + stock_code: 股票代码 + + Returns: + 实时行情数据 + """ + try: + # 转换股票代码格式(Tushare格式:600519.SH) + if stock_code.startswith('6'): + ts_code = f"{stock_code}.SH" + elif stock_code.startswith(('0', '3')): + ts_code = f"{stock_code}.SZ" + else: + self.logger.warning(f"无法识别股票代码市场: {stock_code}") + return None + + # 方法1: 尝试使用daily_basic(基础日线,无需积分) + try: + df = self.ts_pro.daily_basic(ts_code=ts_code, + trade_date=datetime.now().strftime('%Y%m%d'), + fields='ts_code,trade_date,close,turnover_rate,volume_ratio,pe,pb') + + if df.empty: + # 获取最近交易日 + end_date = datetime.now().strftime('%Y%m%d') + df = self.ts_pro.daily_basic(ts_code=ts_code, + end_date=end_date, + fields='ts_code,trade_date,close,turnover_rate,volume_ratio,pe,pb') + df = df.head(1) + + if not df.empty: + row = df.iloc[0] + + # 获取日线数据补充价格信息 + df_daily = self.ts_pro.daily(ts_code=ts_code, + trade_date=row['trade_date'], + fields='open,high,low,pre_close,change,pct_chg,vol,amount') + + if not df_daily.empty: + daily_row = df_daily.iloc[0] + + # 获取股票名称 + stock_basic = self.ts_pro.stock_basic(ts_code=ts_code, fields='name') + stock_name = stock_basic.iloc[0]['name'] if not stock_basic.empty else 'N/A' + + self.logger.info(f"✅ Tushare降级成功(基础接口),获取到 {stock_code} 数据") + + return { + 'code': stock_code, + 'name': stock_name, + 'current_price': float(row['close']), + 'change_pct': float(daily_row.get('pct_chg', 0)), + 'change_amount': float(daily_row.get('change', 0)), + 'volume': float(daily_row.get('vol', 0)) * 100, + 'amount': float(daily_row.get('amount', 0)) * 1000, + 'high': float(daily_row.get('high', 0)), + 'low': float(daily_row.get('low', 0)), + 'open': float(daily_row.get('open', 0)), + 'pre_close': float(daily_row.get('pre_close', 0)), + 'turnover_rate': float(row.get('turnover_rate', 0)), + 'volume_ratio': float(row.get('volume_ratio', 1.0)), + 'update_time': row['trade_date'], + 'data_source': 'tushare' + } + except Exception as e: + self.logger.warning(f"Tushare基础接口失败: {str(e)[:100]}") + + # 方法2: 降级使用更基础的stock_basic+pro_bar + try: + # 获取股票名称 + stock_basic = self.ts_pro.stock_basic(ts_code=ts_code, fields='name') + stock_name = stock_basic.iloc[0]['name'] if not stock_basic.empty else 'N/A' + + # 使用pro_bar获取行情(社区版免费) + import tushare as ts + df = ts.pro_bar(ts_code=ts_code, adj='qfq', ma=[5, 20]) + + if df is not None and not df.empty: + row = df.iloc[0] + + self.logger.info(f"✅ Tushare降级成功(pro_bar接口),获取到 {stock_code} 数据") + + return { + 'code': stock_code, + 'name': stock_name, + 'current_price': float(row['close']), + 'change_pct': float(row.get('pct_chg', 0)), + 'change_amount': float(row.get('change', 0)), + 'volume': float(row.get('vol', 0)) * 100, + 'amount': float(row.get('amount', 0)) * 1000, + 'high': float(row.get('high', 0)), + 'low': float(row.get('low', 0)), + 'open': float(row.get('open', 0)), + 'pre_close': float(row.get('pre_close', 0)), + 'turnover_rate': float(row.get('turnover_rate', 0)), + 'volume_ratio': 1.0, + 'update_time': row['trade_date'], + 'data_source': 'tushare' + } + except Exception as e: + self.logger.warning(f"Tushare pro_bar接口失败: {str(e)[:100]}") + + # 所有方法都失败 + self.logger.error(f"Tushare所有接口都失败 {stock_code},可能是积分不足或网络问题") + self.logger.info("💡 提示:访问 https://tushare.pro/user/token 查看积分和权限") + return None + + except Exception as e: + error_msg = str(e) + if "权限" in error_msg or "积分" in error_msg: + self.logger.error(f"Tushare权限不足 {stock_code}: 需要更多积分") + self.logger.info("💡 获取积分方法:") + self.logger.info(" 1. 完善个人信息 +100积分") + self.logger.info(" 2. 每日签到 +1积分") + self.logger.info(" 3. 参与社区互动") + self.logger.info(" 详情访问: https://tushare.pro/document/1?doc_id=13") + else: + self.logger.error(f"Tushare获取失败 {stock_code}: {error_msg[:100]}") + return None + + def _get_main_force_from_tushare(self, stock_code: str) -> Optional[Dict]: + """ + 从Tushare获取主力资金流向(备用数据源) + 注意:资金流向接口需要较高积分 + + Args: + stock_code: 股票代码 + + Returns: + 主力资金数据 + """ + try: + # 转换股票代码格式 + if stock_code.startswith('6'): + ts_code = f"{stock_code}.SH" + elif stock_code.startswith(('0', '3')): + ts_code = f"{stock_code}.SZ" + else: + return None + + # 尝试获取资金流向数据(需要120积分) + today = datetime.now().strftime('%Y%m%d') + df = self.ts_pro.moneyflow(ts_code=ts_code, start_date=today, end_date=today) + + if df.empty: + # 获取最近一个交易日 + df = self.ts_pro.moneyflow(ts_code=ts_code, end_date=today) + df = df.head(1) + + if df.empty: + self.logger.warning(f"Tushare未找到股票 {stock_code} 的资金流向数据") + return None + + row = df.iloc[0] + + # 计算主力净额(大单+超大单) + buy_lg_amount = float(row.get('buy_lg_amount', 0)) + buy_elg_amount = float(row.get('buy_elg_amount', 0)) + sell_lg_amount = float(row.get('sell_lg_amount', 0)) + sell_elg_amount = float(row.get('sell_elg_amount', 0)) + + main_net = (buy_lg_amount + buy_elg_amount - sell_lg_amount - sell_elg_amount) / 10000 + + # 计算净占比 + net_mf_amount = float(row.get('net_mf_amount', 0)) + main_net_pct = (main_net / net_mf_amount * 100) if net_mf_amount != 0 else 0 + + # 判断主力动向 + if main_net > 0 and main_net_pct > 5: + trend = '大幅流入' + elif main_net > 0: + trend = '小幅流入' + elif main_net < 0 and main_net_pct < -5: + trend = '大幅流出' + elif main_net < 0: + trend = '小幅流出' + else: + trend = '观望' + + self.logger.info(f"✅ Tushare降级成功,获取到 {stock_code} 资金流向") + + return { + 'main_net': main_net, + 'main_net_pct': main_net_pct, + 'super_net': (buy_elg_amount - sell_elg_amount) / 10000, + 'big_net': (buy_lg_amount - sell_lg_amount) / 10000, + 'mid_net': float(row.get('buy_md_amount', 0) - row.get('sell_md_amount', 0)) / 10000, + 'small_net': float(row.get('buy_sm_amount', 0) - row.get('sell_sm_amount', 0)) / 10000, + 'trend': trend + } + + except Exception as e: + error_msg = str(e) + if "权限" in error_msg or "积分" in error_msg: + self.logger.warning(f"⚠️ Tushare资金流向接口需要120积分,当前积分不足") + self.logger.info("💡 获取积分方法:") + self.logger.info(" 1. 完善个人信息 +100积分") + self.logger.info(" 2. 每日签到累积 +30积分(30天)") + self.logger.info(" 3. 参与社区互动获得积分") + self.logger.info(" 详情: https://tushare.pro/document/1?doc_id=13") + self.logger.info(" 智能盯盘会继续运行,仅缺少资金流向数据") + else: + self.logger.error(f"Tushare获取资金流向失败 {stock_code}: {error_msg[:100]}") + return None + + +if __name__ == '__main__': + # 测试代码 + logging.basicConfig(level=logging.INFO) + + fetcher = SmartMonitorDataFetcher() + + # 测试贵州茅台 + print("测试获取贵州茅台(600519)数据...") + data = fetcher.get_comprehensive_data('600519') + + if data: + print("\n实时行情:") + print(f" 当前价: {data.get('current_price')} 元") + print(f" 涨跌幅: {data.get('change_pct')}%") + + print("\n技术指标:") + print(f" MA5: {data.get('ma5', 0):.2f}") + print(f" MA20: {data.get('ma20', 0):.2f}") + print(f" MACD: {data.get('macd', 0):.4f}") + print(f" RSI(6): {data.get('rsi6', 0):.2f}") + + if 'main_force' in data: + print("\n主力资金:") + print(f" 主力净额: {data['main_force']['main_net']:.2f}万") + print(f" 主力动向: {data['main_force']['trend']}") + diff --git a/smart_monitor_db.py b/smart_monitor_db.py new file mode 100644 index 0000000..9cc4aa5 --- /dev/null +++ b/smart_monitor_db.py @@ -0,0 +1,634 @@ +""" +智能盯盘 - 数据库模块 +记录AI决策、交易记录、监控配置等 +""" + +import sqlite3 +import logging +from typing import Dict, List, Optional +from datetime import datetime +import json + + +class SmartMonitorDB: + """智能盯盘数据库""" + + def __init__(self, db_file: str = 'smart_monitor.db'): + """ + 初始化数据库 + + Args: + db_file: 数据库文件路径 + """ + self.db_file = db_file + self.logger = logging.getLogger(__name__) + self._init_database() + + def _init_database(self): + """初始化数据库表结构""" + conn = sqlite3.connect(self.db_file) + cursor = conn.cursor() + + # 1. 监控任务表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS monitor_tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task_name TEXT NOT NULL, + stock_code TEXT NOT NULL, + stock_name TEXT, + enabled INTEGER DEFAULT 1, + check_interval INTEGER DEFAULT 300, + auto_trade INTEGER DEFAULT 0, + position_size_pct REAL DEFAULT 20, + stop_loss_pct REAL DEFAULT 5, + take_profit_pct REAL DEFAULT 10, + qmt_account_id TEXT, + notify_email TEXT, + notify_webhook TEXT, + has_position INTEGER DEFAULT 0, + position_cost REAL DEFAULT 0, + position_quantity INTEGER DEFAULT 0, + position_date TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP, + UNIQUE(stock_code) + ) + ''') + + # 添加持仓相关字段(如果表已存在但缺少这些字段) + try: + cursor.execute("ALTER TABLE monitor_tasks ADD COLUMN has_position INTEGER DEFAULT 0") + except sqlite3.OperationalError: + pass + + try: + cursor.execute("ALTER TABLE monitor_tasks ADD COLUMN position_cost REAL DEFAULT 0") + except sqlite3.OperationalError: + pass + + try: + cursor.execute("ALTER TABLE monitor_tasks ADD COLUMN position_quantity INTEGER DEFAULT 0") + except sqlite3.OperationalError: + pass + + try: + cursor.execute("ALTER TABLE monitor_tasks ADD COLUMN position_date TEXT") + except sqlite3.OperationalError: + pass + + # 2. AI决策记录表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS ai_decisions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + stock_code TEXT NOT NULL, + stock_name TEXT, + decision_time TEXT NOT NULL, + trading_session TEXT, + action TEXT NOT NULL, + confidence INTEGER, + reasoning TEXT, + position_size_pct REAL, + stop_loss_pct REAL, + take_profit_pct REAL, + risk_level TEXT, + key_price_levels TEXT, + market_data TEXT, + account_info TEXT, + executed INTEGER DEFAULT 0, + execution_result TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 3. 交易记录表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS trade_records ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + stock_code TEXT NOT NULL, + stock_name TEXT, + trade_type TEXT NOT NULL, + quantity INTEGER, + price REAL, + amount REAL, + order_id TEXT, + order_status TEXT, + ai_decision_id INTEGER, + trade_time TEXT NOT NULL, + commission REAL DEFAULT 0, + tax REAL DEFAULT 0, + profit_loss REAL DEFAULT 0, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(ai_decision_id) REFERENCES ai_decisions(id) + ) + ''') + + # 4. 持仓监控表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS position_monitor ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + stock_code TEXT NOT NULL, + stock_name TEXT, + quantity INTEGER, + cost_price REAL, + current_price REAL, + profit_loss REAL, + profit_loss_pct REAL, + holding_days INTEGER, + buy_date TEXT, + stop_loss_price REAL, + take_profit_price REAL, + last_check_time TEXT, + status TEXT DEFAULT 'holding', + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP, + UNIQUE(stock_code) + ) + ''') + + # 5. 通知记录表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS notifications ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + stock_code TEXT, + notify_type TEXT NOT NULL, + notify_target TEXT, + subject TEXT, + content TEXT, + status TEXT DEFAULT 'pending', + error_msg TEXT, + sent_at TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 6. 系统日志表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS system_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + log_level TEXT, + module TEXT, + message TEXT, + details TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP + ) + ''') + + conn.commit() + conn.close() + self.logger.info(f"数据库初始化完成: {self.db_file}") + + # ========== 监控任务管理 ========== + + def add_monitor_task(self, task_data: Dict) -> int: + """添加监控任务""" + conn = sqlite3.connect(self.db_file) + cursor = conn.cursor() + + cursor.execute(''' + INSERT INTO monitor_tasks + (task_name, stock_code, stock_name, enabled, check_interval, + auto_trade, position_size_pct, stop_loss_pct, take_profit_pct, + qmt_account_id, notify_email, notify_webhook, + has_position, position_cost, position_quantity, position_date) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + task_data.get('task_name'), + task_data.get('stock_code'), + task_data.get('stock_name'), + task_data.get('enabled', 1), + task_data.get('check_interval', 300), + task_data.get('auto_trade', 0), + task_data.get('position_size_pct', 20), + task_data.get('stop_loss_pct', 5), + task_data.get('take_profit_pct', 10), + task_data.get('qmt_account_id'), + task_data.get('notify_email'), + task_data.get('notify_webhook'), + task_data.get('has_position', 0), + task_data.get('position_cost', 0), + task_data.get('position_quantity', 0), + task_data.get('position_date') + )) + + task_id = cursor.lastrowid + conn.commit() + conn.close() + + position_info = f"(持仓: {task_data.get('position_quantity')}股 @ {task_data.get('position_cost')}元)" if task_data.get('has_position') else "" + self.logger.info(f"添加监控任务: {task_data.get('stock_code')} - {task_data.get('task_name')} {position_info}") + return task_id + + def get_monitor_tasks(self, enabled_only: bool = True) -> List[Dict]: + """获取监控任务列表""" + conn = sqlite3.connect(self.db_file) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + if enabled_only: + cursor.execute('SELECT * FROM monitor_tasks WHERE enabled = 1 ORDER BY id DESC') + else: + cursor.execute('SELECT * FROM monitor_tasks ORDER BY id DESC') + + rows = cursor.fetchall() + conn.close() + + return [dict(row) for row in rows] + + def update_monitor_task(self, task_id: int, updates: Dict): + """更新监控任务""" + conn = sqlite3.connect(self.db_file) + cursor = conn.cursor() + + set_clause = ', '.join([f"{k} = ?" for k in updates.keys()]) + values = list(updates.values()) + [task_id] + + cursor.execute(f''' + UPDATE monitor_tasks + SET {set_clause}, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + ''', values) + + conn.commit() + conn.close() + + def update_monitor_task(self, stock_code: str, task_data: Dict): + """更新监控任务""" + conn = sqlite3.connect(self.db_file) + cursor = conn.cursor() + + # 构建更新语句 + update_fields = [] + values = [] + + if 'task_name' in task_data: + update_fields.append('task_name = ?') + values.append(task_data['task_name']) + + if 'check_interval' in task_data: + update_fields.append('check_interval = ?') + values.append(task_data['check_interval']) + + if 'auto_trade' in task_data: + update_fields.append('auto_trade = ?') + values.append(task_data['auto_trade']) + + if 'position_size_pct' in task_data: + update_fields.append('position_size_pct = ?') + values.append(task_data['position_size_pct']) + + if 'has_position' in task_data: + update_fields.append('has_position = ?') + values.append(task_data['has_position']) + + if 'position_cost' in task_data: + update_fields.append('position_cost = ?') + values.append(task_data['position_cost']) + + if 'position_quantity' in task_data: + update_fields.append('position_quantity = ?') + values.append(task_data['position_quantity']) + + if 'position_date' in task_data: + update_fields.append('position_date = ?') + values.append(task_data['position_date']) + + if 'notify_email' in task_data: + update_fields.append('notify_email = ?') + values.append(task_data['notify_email']) + + # 添加更新时间 + update_fields.append('updated_at = CURRENT_TIMESTAMP') + + # 添加WHERE条件 + values.append(stock_code) + + sql = f"UPDATE monitor_tasks SET {', '.join(update_fields)} WHERE stock_code = ?" + cursor.execute(sql, values) + + conn.commit() + conn.close() + + self.logger.info(f"更新监控任务: {stock_code}") + + def delete_monitor_task(self, task_id: int): + """删除监控任务""" + conn = sqlite3.connect(self.db_file) + cursor = conn.cursor() + + cursor.execute('DELETE FROM monitor_tasks WHERE id = ?', (task_id,)) + + conn.commit() + conn.close() + + # ========== AI决策记录 ========== + + def save_ai_decision(self, decision_data: Dict) -> int: + """保存AI决策""" + conn = sqlite3.connect(self.db_file) + cursor = conn.cursor() + + cursor.execute(''' + INSERT INTO ai_decisions + (stock_code, stock_name, decision_time, trading_session, + action, confidence, reasoning, position_size_pct, + stop_loss_pct, take_profit_pct, risk_level, + key_price_levels, market_data, account_info) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + decision_data.get('stock_code'), + decision_data.get('stock_name'), + decision_data.get('decision_time', datetime.now().strftime('%Y-%m-%d %H:%M:%S')), + decision_data.get('trading_session'), + decision_data.get('action'), + decision_data.get('confidence'), + decision_data.get('reasoning'), + decision_data.get('position_size_pct'), + decision_data.get('stop_loss_pct'), + decision_data.get('take_profit_pct'), + decision_data.get('risk_level'), + json.dumps(decision_data.get('key_price_levels', {})), + json.dumps(decision_data.get('market_data', {})), + json.dumps(decision_data.get('account_info', {})) + )) + + decision_id = cursor.lastrowid + conn.commit() + conn.close() + + return decision_id + + def get_ai_decisions(self, stock_code: str = None, limit: int = 100) -> List[Dict]: + """获取AI决策历史""" + conn = sqlite3.connect(self.db_file) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + if stock_code: + cursor.execute(''' + SELECT * FROM ai_decisions + WHERE stock_code = ? + ORDER BY decision_time DESC + LIMIT ? + ''', (stock_code, limit)) + else: + cursor.execute(''' + SELECT * FROM ai_decisions + ORDER BY decision_time DESC + LIMIT ? + ''', (limit,)) + + rows = cursor.fetchall() + conn.close() + + decisions = [] + for row in rows: + d = dict(row) + # 解析JSON字段 + d['key_price_levels'] = json.loads(d['key_price_levels']) if d['key_price_levels'] else {} + d['market_data'] = json.loads(d['market_data']) if d['market_data'] else {} + d['account_info'] = json.loads(d['account_info']) if d['account_info'] else {} + decisions.append(d) + + return decisions + + def update_decision_execution(self, decision_id: int, executed: bool, result: str): + """更新决策执行状态""" + conn = sqlite3.connect(self.db_file) + cursor = conn.cursor() + + cursor.execute(''' + UPDATE ai_decisions + SET executed = ?, execution_result = ? + WHERE id = ? + ''', (1 if executed else 0, result, decision_id)) + + conn.commit() + conn.close() + + # ========== 交易记录 ========== + + def save_trade_record(self, trade_data: Dict) -> int: + """保存交易记录""" + conn = sqlite3.connect(self.db_file) + cursor = conn.cursor() + + cursor.execute(''' + INSERT INTO trade_records + (stock_code, stock_name, trade_type, quantity, price, amount, + order_id, order_status, ai_decision_id, trade_time, + commission, tax, profit_loss) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + trade_data.get('stock_code'), + trade_data.get('stock_name'), + trade_data.get('trade_type'), + trade_data.get('quantity'), + trade_data.get('price'), + trade_data.get('amount'), + trade_data.get('order_id'), + trade_data.get('order_status'), + trade_data.get('ai_decision_id'), + trade_data.get('trade_time', datetime.now().strftime('%Y-%m-%d %H:%M:%S')), + trade_data.get('commission', 0), + trade_data.get('tax', 0), + trade_data.get('profit_loss', 0) + )) + + record_id = cursor.lastrowid + conn.commit() + conn.close() + + return record_id + + def get_trade_records(self, stock_code: str = None, limit: int = 100) -> List[Dict]: + """获取交易记录""" + conn = sqlite3.connect(self.db_file) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + if stock_code: + cursor.execute(''' + SELECT * FROM trade_records + WHERE stock_code = ? + ORDER BY trade_time DESC + LIMIT ? + ''', (stock_code, limit)) + else: + cursor.execute(''' + SELECT * FROM trade_records + ORDER BY trade_time DESC + LIMIT ? + ''', (limit,)) + + rows = cursor.fetchall() + conn.close() + + return [dict(row) for row in rows] + + # ========== 持仓监控 ========== + + def save_position(self, position_data: Dict): + """保存/更新持仓信息""" + conn = sqlite3.connect(self.db_file) + cursor = conn.cursor() + + # 检查是否已存在 + cursor.execute('SELECT id FROM position_monitor WHERE stock_code = ?', + (position_data.get('stock_code'),)) + existing = cursor.fetchone() + + if existing: + # 更新 + cursor.execute(''' + UPDATE position_monitor + SET stock_name = ?, quantity = ?, cost_price = ?, + current_price = ?, profit_loss = ?, profit_loss_pct = ?, + holding_days = ?, stop_loss_price = ?, take_profit_price = ?, + last_check_time = ?, updated_at = CURRENT_TIMESTAMP + WHERE stock_code = ? + ''', ( + position_data.get('stock_name'), + position_data.get('quantity'), + position_data.get('cost_price'), + position_data.get('current_price'), + position_data.get('profit_loss'), + position_data.get('profit_loss_pct'), + position_data.get('holding_days'), + position_data.get('stop_loss_price'), + position_data.get('take_profit_price'), + datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + position_data.get('stock_code') + )) + else: + # 插入 + cursor.execute(''' + INSERT INTO position_monitor + (stock_code, stock_name, quantity, cost_price, current_price, + profit_loss, profit_loss_pct, holding_days, buy_date, + stop_loss_price, take_profit_price, last_check_time, status) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', ( + position_data.get('stock_code'), + position_data.get('stock_name'), + position_data.get('quantity'), + position_data.get('cost_price'), + position_data.get('current_price'), + position_data.get('profit_loss'), + position_data.get('profit_loss_pct'), + position_data.get('holding_days'), + position_data.get('buy_date'), + position_data.get('stop_loss_price'), + position_data.get('take_profit_price'), + datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + 'holding' + )) + + conn.commit() + conn.close() + + def get_positions(self) -> List[Dict]: + """获取所有持仓""" + conn = sqlite3.connect(self.db_file) + conn.row_factory = sqlite3.Row + cursor = conn.cursor() + + cursor.execute('SELECT * FROM position_monitor WHERE status = "holding" ORDER BY id DESC') + + rows = cursor.fetchall() + conn.close() + + return [dict(row) for row in rows] + + def close_position(self, stock_code: str): + """关闭持仓记录""" + conn = sqlite3.connect(self.db_file) + cursor = conn.cursor() + + cursor.execute(''' + UPDATE position_monitor + SET status = 'closed', updated_at = CURRENT_TIMESTAMP + WHERE stock_code = ? + ''', (stock_code,)) + + conn.commit() + conn.close() + + # ========== 通知记录 ========== + + def save_notification(self, notify_data: Dict) -> int: + """保存通知记录""" + conn = sqlite3.connect(self.db_file) + cursor = conn.cursor() + + cursor.execute(''' + INSERT INTO notifications + (stock_code, notify_type, notify_target, subject, content, status) + VALUES (?, ?, ?, ?, ?, ?) + ''', ( + notify_data.get('stock_code'), + notify_data.get('notify_type'), + notify_data.get('notify_target'), + notify_data.get('subject'), + notify_data.get('content'), + notify_data.get('status', 'pending') + )) + + notify_id = cursor.lastrowid + conn.commit() + conn.close() + + return notify_id + + def update_notification_status(self, notify_id: int, status: str, error_msg: str = None): + """更新通知状态""" + conn = sqlite3.connect(self.db_file) + cursor = conn.cursor() + + cursor.execute(''' + UPDATE notifications + SET status = ?, error_msg = ?, sent_at = CURRENT_TIMESTAMP + WHERE id = ? + ''', (status, error_msg, notify_id)) + + conn.commit() + conn.close() + + # ========== 系统日志 ========== + + def log_system_event(self, level: str, module: str, message: str, details: str = None): + """记录系统日志""" + conn = sqlite3.connect(self.db_file) + cursor = conn.cursor() + + cursor.execute(''' + INSERT INTO system_logs (log_level, module, message, details) + VALUES (?, ?, ?, ?) + ''', (level, module, message, details)) + + conn.commit() + conn.close() + + +if __name__ == '__main__': + # 测试数据库 + logging.basicConfig(level=logging.INFO) + + db = SmartMonitorDB('test_smart_monitor.db') + + # 测试添加监控任务 + task_id = db.add_monitor_task({ + 'task_name': '茅台盯盘', + 'stock_code': '600519', + 'stock_name': '贵州茅台', + 'auto_trade': 1, + 'notify_email': 'test@example.com' + }) + + print(f"创建监控任务 ID: {task_id}") + + # 获取任务列表 + tasks = db.get_monitor_tasks() + print(f"\n监控任务列表: {len(tasks)}个") + for task in tasks: + print(f" - {task['stock_code']} {task['stock_name']}") + diff --git a/smart_monitor_deepseek.py b/smart_monitor_deepseek.py new file mode 100644 index 0000000..622fe99 --- /dev/null +++ b/smart_monitor_deepseek.py @@ -0,0 +1,526 @@ +""" +智能盯盘 - DeepSeek AI 决策引擎 +适配A股T+1交易规则的AI决策系统 +""" + +import logging +from typing import Dict, List, Optional +from datetime import datetime, time +import pytz + + +class SmartMonitorDeepSeek: + """A股智能盯盘 - DeepSeek AI决策引擎""" + + def __init__(self, api_key: str): + """ + 初始化DeepSeek客户端 + + Args: + api_key: DeepSeek API密钥 + """ + self.api_key = api_key + self.base_url = "https://api.deepseek.com/v1" + self.headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + self.logger = logging.getLogger(__name__) + + def is_trading_time(self) -> bool: + """ + 判断当前是否在A股交易时间内 + + Returns: + bool: 是否可以交易 + """ + beijing_tz = pytz.timezone('Asia/Shanghai') + now = datetime.now(beijing_tz) + current_time = now.time() + + # 排除周末 + if now.weekday() >= 5: + return False + + # 上午:9:30-11:30 + morning_start = time(9, 30) + morning_end = time(11, 30) + + # 下午:13:00-15:00 + afternoon_start = time(13, 0) + afternoon_end = time(15, 0) + + is_trading = ( + (morning_start <= current_time <= morning_end) or + (afternoon_start <= current_time <= afternoon_end) + ) + + return is_trading + + def get_trading_session(self) -> Dict: + """ + 获取当前交易时段信息(A股版本) + + Returns: + Dict: 时段信息 + """ + beijing_tz = pytz.timezone('Asia/Shanghai') + now = datetime.now(beijing_tz) + current_time = now.time() + + # 判断是否交易日 + if now.weekday() >= 5: + return { + 'session': '休市', + 'volatility': 'none', + 'recommendation': '周末不可交易', + 'beijing_hour': now.hour, + 'can_trade': False + } + + # 开盘前(9:00-9:30):集合竞价时段 + if time(9, 0) <= current_time < time(9, 30): + return { + 'session': '集合竞价', + 'volatility': 'high', + 'recommendation': '可观察盘面情绪,准备开盘交易', + 'beijing_hour': now.hour, + 'can_trade': False + } + + # 上午盘(9:30-11:30) + elif time(9, 30) <= current_time <= time(11, 30): + return { + 'session': '上午盘', + 'volatility': 'high', + 'recommendation': '交易活跃,波动较大', + 'beijing_hour': now.hour, + 'can_trade': True + } + + # 午间休市(11:30-13:00) + elif time(11, 30) < current_time < time(13, 0): + return { + 'session': '午间休市', + 'volatility': 'none', + 'recommendation': '不可交易,可分析上午盘面', + 'beijing_hour': now.hour, + 'can_trade': False + } + + # 下午盘(13:00-15:00) + elif time(13, 0) <= current_time <= time(15, 0): + # 尾盘最后半小时(14:30-15:00) + if current_time >= time(14, 30): + return { + 'session': '尾盘', + 'volatility': 'high', + 'recommendation': '尾盘波动大,谨慎操作', + 'beijing_hour': now.hour, + 'can_trade': True + } + else: + return { + 'session': '下午盘', + 'volatility': 'medium', + 'recommendation': '波动趋缓,适合布局', + 'beijing_hour': now.hour, + 'can_trade': True + } + + # 盘后(15:00之后) + else: + return { + 'session': '盘后', + 'volatility': 'none', + 'recommendation': '收盘后,可复盘分析', + 'beijing_hour': now.hour, + 'can_trade': False + } + + def chat_completion(self, messages: List[Dict], model: str = "deepseek-chat", + temperature: float = 0.7, max_tokens: int = 2000) -> Dict: + """ + 调用DeepSeek API + + Args: + messages: 对话消息列表 + model: 模型名称 + temperature: 温度参数 + max_tokens: 最大token数 + + Returns: + API响应 + """ + import requests + + payload = { + "model": model, + "messages": messages, + "temperature": temperature, + "max_tokens": max_tokens + } + + try: + response = requests.post( + f"{self.base_url}/chat/completions", + headers=self.headers, + json=payload, + timeout=60 + ) + response.raise_for_status() + return response.json() + except Exception as e: + self.logger.error(f"DeepSeek API调用失败: {e}") + raise + + def analyze_stock_and_decide(self, stock_code: str, market_data: Dict, + account_info: Dict, has_position: bool = False, + position_cost: float = 0, position_quantity: int = 0) -> Dict: + """ + 分析股票并做出交易决策(A股T+1规则) + + Args: + stock_code: 股票代码(如:600519) + market_data: 市场数据 + account_info: 账户信息 + has_position: 是否已持有该股票 + position_cost: 持仓成本价格 + position_quantity: 持仓数量 + + Returns: + 交易决策 + """ + # 获取交易时段 + session_info = self.get_trading_session() + + # 构建Prompt + prompt = self._build_a_stock_prompt( + stock_code, market_data, account_info, + has_position, session_info, position_cost, position_quantity + ) + + system_prompt = """你是一位资深的A股量化交易专家,拥有15年实战经验。 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +⚠️ A股交易规则(与币圈完全不同!) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +[CRITICAL] T+1规则: +- 今天买入的股票,**今天不能卖出**,必须等到下一个交易日 +- 这意味着:一旦买入,至少要持有到明天才能卖出 +- 因此买入决策必须**极其谨慎**,不能像币圈那样快进快出 + +[CRITICAL] 涨跌停限制: +- 主板/中小板:±10%涨跌停 +- 创业板/科创板:±20%涨跌停 +- ST股票:±5%涨跌停 +- 一旦涨停,很难买入;一旦跌停,很难卖出 + +[CRITICAL] 交易时间: +- 上午:9:30-11:30 +- 下午:13:00-15:00 +- 其他时间不能交易 + +[CRITICAL] 只能做多: +- A股不能做空(融券门槛高,散户基本不用) +- 只有买入和卖出两个动作 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +🎯 你的交易哲学(适配T+1) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +**因为T+1限制,你的策略必须更加稳健!** + +1. **买入前三思**: + - 买入后至少持有1天,所以必须确保趋势向上 + - 不能像币圈那样"试探性开仓",一旦买入就是承诺 + - 最好在尾盘或第二天开盘前决策,避免盲目追高 + +2. **止损更困难**: + - 如果今天买入后下跌,今天无法止损(T+1) + - 只能等明天再卖,可能面临更大亏损 + - 因此:**宁可错过,不可做错** + +3. **技术分析更重要**: + - 日线级别趋势确认 + - 支撑位/阻力位 + - 成交量配合 + - 量价关系判断 + +4. **风险控制严格**: + - 单只股票仓位 ≤ 30%(T+1风险大) + - 止损位:-5%(明天开盘立即执行) + - 止盈位:+8-15%(分批止盈) + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +📊 可选的交易动作 +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +**如果当前无持仓**: +- action = "BUY"(买入)- 必须确保技术面强势,趋势向上 +- action = "HOLD"(观望)- 信号不明确时选择观望 + +**如果当前有持仓**: +- action = "SELL"(卖出)- 达到止盈/止损条件,或技术面转弱 +- action = "HOLD"(持有)- 趋势未改变,继续持有 +- ⚠️ 注意:如果股票是今天买入的,受T+1限制无法卖出,只能选择HOLD + +**绝对禁止**: +- 不要在开盘前5分钟(9:30-9:35)买入,容易追高 +- 不要在尾盘最后5分钟(14:55-15:00)买入,可能被套 +- 不要逆趋势交易(趋势向下时买入) + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +📈 买入信号(必须满足至少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. 🔴 技术破位:跌破重要支撑位 +6. 🔴 重大利空:公司公告重大利空消息 + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +💬 返回格式(必须严格JSON) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +{ + "action": "BUY" | "SELL" | "HOLD", + "confidence": 0-100, + "reasoning": "详细的决策理由,包括技术分析、风险评估等,200-300字", + "position_size_pct": 10-30, // 建议仓位百分比(因为T+1,建议≤30%) + "stop_loss_pct": 5.0, // 止损百分比(建议5%) + "take_profit_pct": 10.0, // 止盈百分比(建议10-15%) + "risk_level": "low" | "medium" | "high", + "key_price_levels": { + "support": 支撑位价格, + "resistance": 阻力位价格, + "stop_loss": 止损位价格 + } +} + +**reasoning 示例**: +"茅台当前价格1650元,日线级别呈多头排列(MA5 1645 > MA20 1620 > MA60 1580), +MACD金叉且柱状图持续放大,RSI 62处于健康区间。今日成交量较5日均量放大135%, +显示有增量资金入场。技术面支撑位在1630元附近,阻力位在1680元。综合判断短期 +趋势向上,但考虑T+1规则,建议仓位控制在20%,止损位设在1568元(-5%), +止盈目标1815元(+10%)。风险提示:如明日低开需谨慎..." +""" + + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": prompt} + ] + + try: + response = self.chat_completion(messages, temperature=0.3) + ai_response = response['choices'][0]['message']['content'] + + # 解析JSON决策 + decision = self._parse_decision(ai_response) + + return { + 'success': True, + 'decision': decision, + 'raw_response': ai_response + } + + except Exception as e: + self.logger.error(f"AI决策失败: {e}") + return { + 'success': False, + 'error': str(e) + } + + def _build_a_stock_prompt(self, stock_code: str, market_data: Dict, + account_info: Dict, has_position: bool, + session_info: Dict, position_cost: float = 0, + position_quantity: int = 0) -> str: + """构建A股分析提示词""" + + prompt = f""" +[TIMER] 当前交易时段 +═══════════════════════════════════════════════════════════ +当前时段: {session_info['session']} (北京时间{session_info['beijing_hour']}:00) +市场状态: {session_info['volatility'].upper()} +时段建议: {session_info['recommendation']} +可交易: {'是' if session_info['can_trade'] else '否'} + +[STOCK] 股票基本信息 +═══════════════════════════════════════════════════════════ +股票代码: {stock_code} +股票名称: {market_data.get('name', 'N/A')} +当前价格: ¥{market_data.get('current_price', 0):.2f} +今日涨跌: {market_data.get('change_pct', 0):+.2f}% +今日涨跌额: ¥{market_data.get('change_amount', 0):+.2f} +最高价: ¥{market_data.get('high', 0):.2f} +最低价: ¥{market_data.get('low', 0):.2f} +开盘价: ¥{market_data.get('open', 0):.2f} +昨收价: ¥{market_data.get('pre_close', 0):.2f} +成交量: {market_data.get('volume', 0):,.0f}手 +成交额: ¥{market_data.get('amount', 0):,.2f}万 + +[TECHNICAL] 技术指标 +═══════════════════════════════════════════════════════════ +MA5: ¥{market_data.get('ma5', 0):.2f} +MA20: ¥{market_data.get('ma20', 0):.2f} +MA60: ¥{market_data.get('ma60', 0):.2f} +趋势判断: {'多头排列' if market_data.get('trend') == 'up' else '空头排列' if market_data.get('trend') == 'down' else '震荡'} + +MACD: + DIF: {market_data.get('macd_dif', 0):.4f} + DEA: {market_data.get('macd_dea', 0):.4f} + MACD: {market_data.get('macd', 0):.4f} ({'金叉' if market_data.get('macd', 0) > 0 else '死叉'}) + +RSI(6): {market_data.get('rsi6', 50):.2f} {'[超买]' if market_data.get('rsi6', 50) > 80 else '[超卖]' if market_data.get('rsi6', 50) < 20 else '[正常]'} +RSI(12): {market_data.get('rsi12', 50):.2f} +RSI(24): {market_data.get('rsi24', 50):.2f} + +KDJ: + K: {market_data.get('kdj_k', 50):.2f} + D: {market_data.get('kdj_d', 50):.2f} + J: {market_data.get('kdj_j', 50):.2f} + +布林带: + 上轨: ¥{market_data.get('boll_upper', 0):.2f} + 中轨: ¥{market_data.get('boll_mid', 0):.2f} + 下轨: ¥{market_data.get('boll_lower', 0):.2f} + 位置: {market_data.get('boll_position', 'N/A')} + +[VOLUME] 量能分析 +═══════════════════════════════════════════════════════════ +今日成交量: {market_data.get('volume', 0):,.0f}手 +5日均量: {market_data.get('vol_ma5', 0):,.0f}手 +量比: {market_data.get('volume_ratio', 0):.2f} ({'放量' if market_data.get('volume_ratio', 0) > 1.2 else '缩量' if market_data.get('volume_ratio', 0) < 0.8 else '正常'}) +换手率: {market_data.get('turnover_rate', 0):.2f}% + +[ACCOUNT] 账户状态 +═══════════════════════════════════════════════════════════ +可用资金: ¥{account_info.get('available_cash', 0):,.2f} +总资产: ¥{account_info.get('total_value', 0):,.2f} +持仓数量: {account_info.get('positions_count', 0)} +""" + + # 如果已持有该股票 + if has_position and position_cost > 0 and position_quantity > 0: + current_price = market_data.get('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) if cost_total > 0 else 0 + + prompt += f""" +[POSITION] 当前持仓({stock_code}) ⭐ 重要 +═══════════════════════════════════════════════════════════ +持仓数量: {position_quantity}股 +成本价: ¥{position_cost:.2f} +当前价: ¥{current_price:.2f} +持仓市值: ¥{current_total:,.2f} +浮动盈亏: ¥{profit_loss:,.2f} ({profit_loss_pct:+.2f}%) + +⚠️ T+1限制: 该股票可以卖出(不受T+1限制) + +💡 决策建议: +- 如果盈利且技术指标转弱 → 建议止盈卖出 +- 如果亏损超过止损线(通常-5%)→ 建议止损卖出 +- 如果技术指标强势且未到止盈位 → 建议继续持有 +- 如果盈利且看好后市 → 可考虑加仓(但注意仓位控制) +""" + else: + prompt += """ +[POSITION] 当前无持仓 +═══════════════════════════════════════════════════════════ +可考虑买入,但必须确保: +1. 技术面强势(满足至少3个买入信号) +2. 有足够的安全边际 +3. 考虑T+1规则,买入后至少持有1天 +4. 控制仓位,建议单只股票仓位≤30% +""" + + # 主力资金数据(已禁用 - 接口不稳定) + # if 'main_force' in market_data: + # mf = market_data['main_force'] + # prompt += f""" + # [MONEY] 主力资金流向 + # ═══════════════════════════════════════════════════════════ + # 主力净额: ¥{mf.get('main_net', 0):,.2f}万 ({mf.get('main_net_pct', 0):+.2f}%) + # 超大单: ¥{mf.get('super_net', 0):,.2f}万 + # 大单: ¥{mf.get('big_net', 0):,.2f}万 + # 中单: ¥{mf.get('mid_net', 0):,.2f}万 + # 小单: ¥{mf.get('small_net', 0):,.2f}万 + # 主力动向: {mf.get('trend', '观望')} + # """ + + prompt += "\n请基于以上数据,给出交易决策(JSON格式)。" + + return prompt + + def _parse_decision(self, ai_response: str) -> Dict: + """解析AI决策响应""" + import json + + try: + # 尝试多种提取方式 + if "```json" in ai_response.lower(): + json_start = ai_response.lower().find("```json") + 7 + json_end = ai_response.find("```", json_start) + json_str = ai_response[json_start:json_end].strip() + elif "```" in ai_response: + first_tick = ai_response.find("```") + json_start = ai_response.find("\n", first_tick) + 1 + json_end = ai_response.find("```", json_start) + json_str = ai_response[json_start:json_end].strip() + elif "{" in ai_response and "}" in ai_response: + start_idx = ai_response.find('{') + end_idx = ai_response.rfind('}') + 1 + json_str = ai_response[start_idx:end_idx] + else: + json_str = ai_response + + decision = json.loads(json_str) + + # 验证必需字段 + required_fields = ['action', 'confidence', 'reasoning'] + for field in required_fields: + if field not in decision: + raise ValueError(f"缺少必需字段: {field}") + + # 设置默认值 + decision.setdefault('position_size_pct', 20) + decision.setdefault('stop_loss_pct', 5.0) + decision.setdefault('take_profit_pct', 10.0) + decision.setdefault('risk_level', 'medium') + + return decision + + except Exception as e: + self.logger.error(f"解析AI决策失败: {e}") + # 返回保守决策 + return { + 'action': 'HOLD', + 'confidence': 0, + 'reasoning': f'AI响应解析失败: {str(e)}', + 'position_size_pct': 0, + 'stop_loss_pct': 5.0, + 'take_profit_pct': 10.0, + 'risk_level': 'high' + } + diff --git a/smart_monitor_engine.py b/smart_monitor_engine.py new file mode 100644 index 0000000..2ec0f45 --- /dev/null +++ b/smart_monitor_engine.py @@ -0,0 +1,591 @@ +""" +智能盯盘 - 主引擎 +整合DeepSeek AI决策、数据获取、交易执行、通知等功能 +""" + +import logging +import time +from typing import Dict, List, Optional +from datetime import datetime +import threading + +from smart_monitor_deepseek import SmartMonitorDeepSeek +from smart_monitor_data import SmartMonitorDataFetcher +from smart_monitor_qmt import SmartMonitorQMT, SmartMonitorQMTSimulator +from smart_monitor_db import SmartMonitorDB +from notification_service import notification_service # 复用主程序的通知服务 +from config_manager import config_manager # 复用主程序的配置管理器 + + +class SmartMonitorEngine: + """智能盯盘引擎""" + + def __init__(self, deepseek_api_key: str = None, qmt_account_id: str = None, + use_simulator: bool = None): + """ + 初始化智能盯盘引擎 + + Args: + deepseek_api_key: DeepSeek API密钥(可选,从配置读取) + qmt_account_id: miniQMT账户ID(可选,从配置读取) + use_simulator: 是否使用模拟交易(可选,从配置读取) + """ + self.logger = logging.getLogger(__name__) + + # 从配置管理器读取配置 + config = config_manager.read_env() + + # DeepSeek API + if deepseek_api_key is None: + deepseek_api_key = config.get('DEEPSEEK_API_KEY', '') + + # MiniQMT配置 + if qmt_account_id is None: + qmt_account_id = config.get('MINIQMT_ACCOUNT_ID', '') + + if use_simulator is None: + # 如果MINIQMT_ENABLED=false,则使用模拟器 + miniqmt_enabled = config.get('MINIQMT_ENABLED', 'false').lower() == 'true' + use_simulator = not miniqmt_enabled + + # 初始化各个模块 + self.deepseek = SmartMonitorDeepSeek(deepseek_api_key) + self.data_fetcher = SmartMonitorDataFetcher() + self.db = SmartMonitorDB() + self.notification = notification_service # 使用主程序的通知服务 + + # 初始化交易接口 + if use_simulator: + self.qmt = SmartMonitorQMTSimulator() + self.qmt.connect(qmt_account_id or "simulator") + self.logger.info("使用模拟交易模式") + else: + self.qmt = SmartMonitorQMT() + if qmt_account_id: + success = self.qmt.connect(qmt_account_id) + if success: + self.logger.info(f"已连接miniQMT账户: {qmt_account_id}") + else: + self.logger.warning(f"连接miniQMT失败,切换到模拟模式") + self.qmt = SmartMonitorQMTSimulator() + self.qmt.connect("simulator") + else: + self.logger.warning("未配置miniQMT账户,使用模拟模式") + self.qmt = SmartMonitorQMTSimulator() + self.qmt.connect("simulator") + + # 监控线程控制 + self.monitoring_threads = {} + self.stop_flags = {} + + self.logger.info("智能盯盘引擎初始化完成") + + def analyze_stock(self, stock_code: str, auto_trade: bool = False, + notify: bool = True, has_position: bool = False, + position_cost: float = 0, position_quantity: int = 0) -> Dict: + """ + 分析单只股票并做出决策 + + Args: + stock_code: 股票代码 + auto_trade: 是否自动交易 + notify: 是否发送通知 + has_position: 是否已持仓(可选) + position_cost: 持仓成本(可选) + position_quantity: 持仓数量(可选) + + Returns: + 分析结果 + """ + try: + self.logger.info(f"[{stock_code}] 开始分析...") + + # 1. 检查交易时段 + session_info = self.deepseek.get_trading_session() + self.logger.info(f"[{stock_code}] 当前时段: {session_info['session']}") + + # 2. 获取市场数据 + market_data = self.data_fetcher.get_comprehensive_data(stock_code) + if not market_data: + return { + 'success': False, + 'error': '获取市场数据失败' + } + + # 3. 获取账户信息 + account_info = self.qmt.get_account_info() + + # 4. 检查是否已持有该股票 + # 优先使用传入的持仓信息,否则从QMT获取 + if has_position and position_cost > 0 and position_quantity > 0: + # 使用用户设置的持仓信息 + self.logger.info(f"[{stock_code}] 使用监控任务设置的持仓: {position_quantity}股 @ {position_cost:.2f}元") + else: + # 从QMT获取持仓 + position = self.qmt.get_position(stock_code) + has_position = position is not None + + if has_position: + position_cost = position.get('cost_price', 0) + position_quantity = position.get('quantity', 0) + account_info['current_position'] = position + self.logger.info(f"[{stock_code}] 从QMT获取持仓: {position_quantity}股, " + f"成本价: {position_cost:.2f}, " + f"浮动盈亏: {position.get('profit_loss_pct', 0):+.2f}%") + + # 5. 调用DeepSeek AI决策 + ai_result = self.deepseek.analyze_stock_and_decide( + stock_code=stock_code, + market_data=market_data, + account_info=account_info, + has_position=has_position, + position_cost=position_cost, + position_quantity=position_quantity + ) + + if not ai_result['success']: + return { + 'success': False, + 'error': 'AI决策失败', + 'details': ai_result + } + + decision = ai_result['decision'] + + self.logger.info(f"[{stock_code}] AI决策: {decision['action']} " + f"(信心度: {decision['confidence']}%)") + self.logger.info(f"[{stock_code}] 决策理由: {decision['reasoning'][:100]}...") + + # 6. 保存AI决策到数据库 + decision_id = self.db.save_ai_decision({ + 'stock_code': stock_code, + 'stock_name': market_data.get('name'), + 'trading_session': session_info['session'], + 'action': decision['action'], + 'confidence': decision['confidence'], + 'reasoning': decision['reasoning'], + 'position_size_pct': decision.get('position_size_pct'), + 'stop_loss_pct': decision.get('stop_loss_pct'), + 'take_profit_pct': decision.get('take_profit_pct'), + 'risk_level': decision.get('risk_level'), + 'key_price_levels': decision.get('key_price_levels', {}), + 'market_data': market_data, + 'account_info': account_info + }) + + # 7. 执行交易(如果开启自动交易) + execution_result = None + if auto_trade and session_info['can_trade']: + execution_result = self._execute_decision( + stock_code=stock_code, + decision=decision, + market_data=market_data, + has_position=has_position + ) + + # 更新决策执行状态 + self.db.update_decision_execution( + decision_id=decision_id, + executed=execution_result.get('success', False), + result=str(execution_result) + ) + + # 8. 发送通知 + if notify: + self._send_notification( + stock_code=stock_code, + stock_name=market_data.get('name'), + decision=decision, + execution_result=execution_result, + market_data=market_data + ) + + return { + 'success': True, + 'stock_code': stock_code, + 'stock_name': market_data.get('name'), + 'session_info': session_info, + 'market_data': market_data, + 'decision': decision, + 'decision_id': decision_id, + 'execution_result': execution_result + } + + except Exception as e: + self.logger.error(f"[{stock_code}] 分析失败: {e}") + import traceback + traceback.print_exc() + return { + 'success': False, + 'error': str(e) + } + + def _execute_decision(self, stock_code: str, decision: Dict, + market_data: Dict, has_position: bool) -> Dict: + """ + 执行AI决策 + + Args: + stock_code: 股票代码 + decision: AI决策 + market_data: 市场数据 + has_position: 是否已持有 + + Returns: + 执行结果 + """ + action = decision['action'] + + try: + if action == 'BUY' and not has_position: + # 买入逻辑 + return self._execute_buy(stock_code, decision, market_data) + + elif action == 'SELL' and has_position: + # 卖出逻辑 + return self._execute_sell(stock_code, decision, market_data) + + elif action == 'HOLD': + # 持有,不操作 + return { + 'success': True, + 'action': 'HOLD', + 'message': 'AI建议持有,未执行交易' + } + + else: + return { + 'success': False, + 'error': f'无效操作: {action}' + } + + except Exception as e: + self.logger.error(f"[{stock_code}] 执行交易失败: {e}") + return { + 'success': False, + 'error': str(e) + } + + def _execute_buy(self, stock_code: str, decision: Dict, market_data: Dict) -> Dict: + """执行买入""" + try: + # 获取账户信息 + account_info = self.qmt.get_account_info() + available_cash = account_info['available_cash'] + + # 计算买入金额 + position_size_pct = decision.get('position_size_pct', 20) + buy_amount = available_cash * (position_size_pct / 100) + + # 计算买入数量(必须是100的整数倍) + current_price = market_data['current_price'] + quantity = int(buy_amount / current_price / 100) * 100 + + if quantity < 100: + return { + 'success': False, + 'error': f'资金不足,最少需要买入100股(约{current_price * 100:.2f}元)' + } + + # 执行买入 + result = self.qmt.buy_stock( + stock_code=stock_code, + quantity=quantity, + price=current_price, + order_type='market' + ) + + if result['success']: + # 保存交易记录 + self.db.save_trade_record({ + 'stock_code': stock_code, + 'stock_name': market_data.get('name'), + 'trade_type': 'BUY', + 'quantity': quantity, + 'price': current_price, + 'amount': quantity * current_price, + 'order_id': result.get('order_id'), + 'order_status': '已提交' + }) + + # 保存持仓监控 + self.db.save_position({ + 'stock_code': stock_code, + 'stock_name': market_data.get('name'), + 'quantity': quantity, + 'cost_price': current_price, + 'current_price': current_price, + 'profit_loss': 0, + 'profit_loss_pct': 0, + 'holding_days': 0, + 'buy_date': datetime.now().strftime('%Y-%m-%d'), + 'stop_loss_price': current_price * (1 - decision.get('stop_loss_pct', 5) / 100), + 'take_profit_price': current_price * (1 + decision.get('take_profit_pct', 10) / 100) + }) + + self.logger.info(f"[{stock_code}] 买入成功: {quantity}股 @ {current_price:.2f}元") + + return result + + except Exception as e: + self.logger.error(f"[{stock_code}] 买入失败: {e}") + return { + 'success': False, + 'error': str(e) + } + + def _execute_sell(self, stock_code: str, decision: Dict, market_data: Dict) -> Dict: + """执行卖出""" + try: + # 获取持仓 + position = self.qmt.get_position(stock_code) + if not position: + return { + 'success': False, + 'error': '未持有该股票' + } + + # 可卖数量(考虑T+1限制) + can_sell = position['can_sell'] + if can_sell <= 0: + return { + 'success': False, + 'error': 'T+1限制,今天买入的股票明天才能卖出' + } + + # 执行卖出 + current_price = market_data['current_price'] + result = self.qmt.sell_stock( + stock_code=stock_code, + quantity=can_sell, + price=current_price, + order_type='market' + ) + + if result['success']: + # 计算盈亏 + profit_loss = (current_price - position['cost_price']) * can_sell + + # 保存交易记录 + self.db.save_trade_record({ + 'stock_code': stock_code, + 'stock_name': market_data.get('name'), + 'trade_type': 'SELL', + 'quantity': can_sell, + 'price': current_price, + 'amount': can_sell * current_price, + 'order_id': result.get('order_id'), + 'order_status': '已提交', + 'profit_loss': profit_loss + }) + + # 更新或关闭持仓记录 + if can_sell >= position['quantity']: + self.db.close_position(stock_code) + + self.logger.info(f"[{stock_code}] 卖出成功: {can_sell}股 @ {current_price:.2f}元, " + f"盈亏: {profit_loss:+.2f}元") + + return result + + except Exception as e: + self.logger.error(f"[{stock_code}] 卖出失败: {e}") + return { + 'success': False, + 'error': str(e) + } + + def _send_notification(self, stock_code: str, stock_name: str, + decision: Dict, execution_result: Optional[Dict], + market_data: Dict): + """发送通知(使用主程序的通知服务)""" + try: + # 构建通知内容 + action_text = { + 'BUY': '买入', + 'SELL': '卖出', + 'HOLD': '持有' + }.get(decision['action'], decision['action']) + + message = f"{action_text}信号 - {stock_name}({stock_code})" + + # 构建详细内容 + content = f""" +【股票信息】 +代码: {stock_code} +名称: {stock_name} +当前价: {market_data.get('current_price', 0):.2f}元 +涨跌幅: {market_data.get('change_pct', 0):+.2f}% + +【AI决策】 +操作: {action_text} +信心度: {decision['confidence']}% +风险等级: {decision.get('risk_level', 'N/A')} + +【决策理由】 +{decision['reasoning'][:200]}... + +【技术指标】 +MA5: {market_data.get('ma5', 0):.2f} | MA20: {market_data.get('ma20', 0):.2f} +MACD: {market_data.get('macd', 0):.4f} | RSI(6): {market_data.get('rsi6', 0):.2f} +""" + + if execution_result: + if execution_result.get('success'): + content += f"\n✅ 操作已自动执行成功" + else: + content += f"\n❌ 执行失败: {execution_result.get('error')}" + + content += f"\n\n⏰ {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" + + # 使用主程序的通知服务格式 + notification_data = { + 'symbol': stock_code, + 'name': stock_name, + 'type': '智能盯盘', + 'message': message, + 'details': content, + 'triggered_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + } + + # 直接调用主程序的通知服务发送 + success = self.notification.send_notification(notification_data) + + if success: + self.logger.info(f"[{stock_code}] 通知已发送") + else: + self.logger.warning(f"[{stock_code}] 通知发送失败") + + # 同时保存到智能盯盘的数据库 + self.db.save_notification({ + 'stock_code': stock_code, + 'notify_type': 'decision', + 'subject': f"智能盯盘 - {message}", + 'content': content, + 'status': 'sent' if success else 'failed' + }) + + except Exception as e: + self.logger.error(f"[{stock_code}] 发送通知失败: {e}") + import traceback + traceback.print_exc() + + def start_monitor(self, stock_code: str, check_interval: int = 300, + auto_trade: bool = False, notify: bool = True, + has_position: bool = False, position_cost: float = 0, + position_quantity: int = 0): + """ + 启动股票监控(在独立线程中运行) + + Args: + stock_code: 股票代码 + check_interval: 检查间隔(秒) + auto_trade: 是否自动交易 + notify: 是否发送通知 + has_position: 是否已持仓 + position_cost: 持仓成本 + position_quantity: 持仓数量 + """ + if stock_code in self.monitoring_threads: + self.logger.warning(f"[{stock_code}] 监控已在运行中") + return + + # 创建停止标志 + stop_flag = threading.Event() + self.stop_flags[stock_code] = stop_flag + + # 创建监控线程 + thread = threading.Thread( + target=self._monitor_loop, + args=(stock_code, check_interval, auto_trade, notify, stop_flag, + has_position, position_cost, position_quantity), + daemon=True + ) + + self.monitoring_threads[stock_code] = thread + thread.start() + + position_info = f"(持仓: {position_quantity}股 @ {position_cost:.2f}元)" if has_position else "" + self.logger.info(f"[{stock_code}] 监控已启动,间隔: {check_interval}秒 {position_info}") + + def stop_monitor(self, stock_code: str): + """停止股票监控""" + if stock_code not in self.monitoring_threads: + self.logger.warning(f"[{stock_code}] 监控未运行") + return + + # 设置停止标志 + self.stop_flags[stock_code].set() + + # 等待线程结束 + self.monitoring_threads[stock_code].join(timeout=5) + + # 清理 + del self.monitoring_threads[stock_code] + del self.stop_flags[stock_code] + + self.logger.info(f"[{stock_code}] 监控已停止") + + def _monitor_loop(self, stock_code: str, check_interval: int, + auto_trade: bool, notify: bool, stop_flag: threading.Event, + has_position: bool = False, position_cost: float = 0, + position_quantity: int = 0): + """监控循环(在独立线程中运行)""" + self.logger.info(f"[{stock_code}] 监控线程已启动") + + while not stop_flag.is_set(): + try: + # 执行分析 + result = self.analyze_stock( + stock_code=stock_code, + auto_trade=auto_trade, + notify=notify, + has_position=has_position, + position_cost=position_cost, + position_quantity=position_quantity + ) + + if result['success']: + self.logger.info(f"[{stock_code}] 分析完成: {result['decision']['action']}") + else: + self.logger.error(f"[{stock_code}] 分析失败: {result.get('error')}") + + except Exception as e: + self.logger.error(f"[{stock_code}] 监控循环异常: {e}") + + # 等待下一次检查 + stop_flag.wait(check_interval) + + self.logger.info(f"[{stock_code}] 监控线程已退出") + + +if __name__ == '__main__': + # 测试代码 + import os + from dotenv import load_dotenv + + load_dotenv() + + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + # 使用模拟模式测试 + engine = SmartMonitorEngine( + deepseek_api_key=os.getenv('DEEPSEEK_API_KEY'), + use_simulator=True + ) + + # 测试分析贵州茅台 + print("\n测试分析贵州茅台(600519)...") + result = engine.analyze_stock('600519', auto_trade=False, notify=False) + + if result['success']: + print(f"\n分析成功!") + print(f" 决策: {result['decision']['action']}") + print(f" 信心度: {result['decision']['confidence']}%") + print(f" 理由: {result['decision']['reasoning'][:100]}...") + else: + print(f"\n分析失败: {result.get('error')}") + diff --git a/smart_monitor_kline.py b/smart_monitor_kline.py new file mode 100644 index 0000000..8c3ed9f --- /dev/null +++ b/smart_monitor_kline.py @@ -0,0 +1,484 @@ +""" +智能盯盘 - K线图绘制模块 +支持AI决策标注、实时更新 +""" + +import plotly.graph_objects as go +from plotly.subplots import make_subplots +import pandas as pd +from datetime import datetime, timedelta +from typing import Dict, List, Optional +import logging + + +class SmartMonitorKline: + """智能盯盘K线图""" + + def __init__(self): + """初始化K线图""" + self.logger = logging.getLogger(__name__) + + def create_kline_with_decisions( + self, + stock_code: str, + stock_name: str, + kline_data: pd.DataFrame, + ai_decisions: List[Dict], + show_volume: bool = True, + show_ma: bool = True, + height: int = 600 + ) -> go.Figure: + """ + 创建带AI决策标注的K线图 + + Args: + stock_code: 股票代码 + stock_name: 股票名称 + kline_data: K线数据(DataFrame) + ai_decisions: AI决策列表 + show_volume: 是否显示成交量 + show_ma: 是否显示均线 + height: 图表高度 + + Returns: + plotly Figure对象 + """ + try: + # 确保数据不为空 + if kline_data is None or kline_data.empty: + self.logger.warning(f"K线数据为空 {stock_code}") + return self._create_empty_figure(stock_code, stock_name, height) + + # 确保必需的列存在 + required_cols = ['日期', '开盘', '收盘', '最高', '最低'] + if not all(col in kline_data.columns for col in required_cols): + self.logger.error(f"K线数据缺少必需列 {stock_code}") + return self._create_empty_figure(stock_code, stock_name, height) + + # 创建子图 + if show_volume: + fig = make_subplots( + rows=2, cols=1, + shared_xaxes=True, + vertical_spacing=0.03, + row_heights=[0.7, 0.3], + subplot_titles=(f'{stock_code} {stock_name}', '成交量') + ) + else: + fig = make_subplots( + rows=1, cols=1, + subplot_titles=(f'{stock_code} {stock_name}',) + ) + + # 1. 添加K线图 + fig.add_trace( + go.Candlestick( + x=kline_data['日期'], + open=kline_data['开盘'], + high=kline_data['最高'], + low=kline_data['最低'], + close=kline_data['收盘'], + name='K线', + increasing_line_color='#ef5350', # 红色(涨) + decreasing_line_color='#26a69a' # 绿色(跌) + ), + row=1, col=1 + ) + + # 2. 添加均线(如果需要) + if show_ma: + self._add_moving_averages(fig, kline_data, row=1, col=1) + + # 3. 添加AI决策标注 + if ai_decisions: + self._add_ai_decision_markers(fig, kline_data, ai_decisions, row=1, col=1) + + # 4. 添加成交量(如果需要) + if show_volume and '成交量' in kline_data.columns: + self._add_volume(fig, kline_data, row=2, col=1) + + # 5. 更新布局 + fig.update_layout( + height=height, + xaxis_rangeslider_visible=False, + showlegend=True, + hovermode='x unified', + template='plotly_white', + margin=dict(l=50, r=50, t=50, b=50), + ) + + # 更新x轴 + fig.update_xaxes( + title_text="日期", + row=2 if show_volume else 1, + col=1 + ) + + # 更新y轴 + fig.update_yaxes(title_text="价格(元)", row=1, col=1) + if show_volume: + fig.update_yaxes(title_text="成交量", row=2, col=1) + + return fig + + except Exception as e: + self.logger.error(f"创建K线图失败 {stock_code}: {e}") + import traceback + self.logger.debug(traceback.format_exc()) + return self._create_empty_figure(stock_code, stock_name, height) + + def _add_moving_averages(self, fig, kline_data: pd.DataFrame, row: int, col: int): + """添加均线""" + try: + # 计算均线 + ma_periods = [5, 10, 20, 60] + ma_colors = ['#FF6B6B', '#4ECDC4', '#45B7D1', '#FFA07A'] + + for period, color in zip(ma_periods, ma_colors): + if len(kline_data) >= period: + ma = kline_data['收盘'].rolling(window=period).mean() + fig.add_trace( + go.Scatter( + x=kline_data['日期'], + y=ma, + name=f'MA{period}', + line=dict(color=color, width=1), + opacity=0.7 + ), + row=row, col=col + ) + except Exception as e: + self.logger.warning(f"添加均线失败: {e}") + + def _add_ai_decision_markers( + self, + fig, + kline_data: pd.DataFrame, + ai_decisions: List[Dict], + row: int, + col: int + ): + """在K线图上添加AI决策标注""" + try: + # 决策类型映射 + action_config = { + 'buy': { + 'symbol': 'triangle-up', + 'color': '#ef5350', + 'text': '买入', + 'size': 15 + }, + 'sell': { + 'symbol': 'triangle-down', + 'color': '#26a69a', + 'text': '卖出', + 'size': 15 + }, + 'add_position': { + 'symbol': 'triangle-up', + 'color': '#ff9800', + 'text': '加仓', + 'size': 12 + }, + 'reduce_position': { + 'symbol': 'triangle-down', + 'color': '#9c27b0', + 'text': '减仓', + 'size': 12 + }, + 'hold': { + 'symbol': 'circle', + 'color': '#607d8b', + 'text': '持有', + 'size': 8 + } + } + + # 将K线数据日期转换为字符串,便于匹配 + kline_data['日期_str'] = pd.to_datetime(kline_data['日期']).dt.strftime('%Y-%m-%d') + + # 按决策类型分组 + for action_type, config in action_config.items(): + decisions_of_type = [d for d in ai_decisions if d.get('action') == action_type] + + if not decisions_of_type: + continue + + # 提取决策的日期和价格 + decision_dates = [] + decision_prices = [] + decision_texts = [] + + for decision in decisions_of_type: + decision_date = decision.get('decision_time', '').split()[0] # 只取日期部分 + + # 在K线数据中查找对应日期的收盘价 + matching_rows = kline_data[kline_data['日期_str'] == decision_date] + + if not matching_rows.empty: + price = matching_rows.iloc[0]['收盘'] + decision_dates.append(decision_date) + decision_prices.append(price) + + # 构建hover文本 + confidence = decision.get('confidence', 0) + reasoning = decision.get('reasoning', '无')[:50] # 截断过长的推理 + hover_text = ( + f"{config['text']}
" + f"日期: {decision_date}
" + f"价格: ¥{price:.2f}
" + f"置信度: {confidence}%
" + f"推理: {reasoning}..." + ) + decision_texts.append(hover_text) + + # 添加标注 + if decision_dates: + fig.add_trace( + go.Scatter( + x=decision_dates, + y=decision_prices, + mode='markers+text', + name=config['text'], + marker=dict( + symbol=config['symbol'], + size=config['size'], + color=config['color'], + line=dict(color='white', width=1) + ), + text=[config['text']] * len(decision_dates), + textposition='top center', + textfont=dict(size=10, color=config['color']), + hovertext=decision_texts, + hoverinfo='text', + showlegend=True + ), + row=row, col=col + ) + + except Exception as e: + self.logger.error(f"添加AI决策标注失败: {e}") + import traceback + self.logger.debug(traceback.format_exc()) + + def _add_volume(self, fig, kline_data: pd.DataFrame, row: int, col: int): + """添加成交量柱状图""" + try: + # 计算颜色(红涨绿跌) + colors = [] + for i in range(len(kline_data)): + if i == 0: + colors.append('#ef5350') + else: + if kline_data.iloc[i]['收盘'] >= kline_data.iloc[i-1]['收盘']: + colors.append('#ef5350') # 红色(涨) + else: + colors.append('#26a69a') # 绿色(跌) + + fig.add_trace( + go.Bar( + x=kline_data['日期'], + y=kline_data['成交量'], + name='成交量', + marker_color=colors, + showlegend=False + ), + row=row, col=col + ) + except Exception as e: + self.logger.warning(f"添加成交量失败: {e}") + + def _create_empty_figure(self, stock_code: str, stock_name: str, height: int) -> go.Figure: + """创建空图表""" + fig = go.Figure() + fig.add_annotation( + text=f"暂无 {stock_code} {stock_name} 的K线数据", + xref="paper", + yref="paper", + x=0.5, + y=0.5, + showarrow=False, + font=dict(size=20, color="gray") + ) + fig.update_layout( + height=height, + xaxis=dict(visible=False), + yaxis=dict(visible=False), + template='plotly_white' + ) + return fig + + def get_kline_data(self, stock_code: str, days: int = 60, data_fetcher=None) -> Optional[pd.DataFrame]: + """ + 获取K线数据(带Tushare降级机制) + + Args: + stock_code: 股票代码 + days: 获取天数 + data_fetcher: 数据获取器实例 + + Returns: + K线数据DataFrame + """ + try: + if data_fetcher is None: + from smart_monitor_data import SmartMonitorDataFetcher + data_fetcher = SmartMonitorDataFetcher() + + # 计算日期范围 + end_date = datetime.now().strftime('%Y%m%d') + start_date = (datetime.now() - timedelta(days=days + 30)).strftime('%Y%m%d') # 多取30天以确保足够数据 + + # 方法1: 尝试使用AKShare获取(只尝试1次,避免IP封禁) + try: + import akshare as ak + df = ak.stock_zh_a_hist( + symbol=stock_code, + period='daily', + start_date=start_date, + end_date=end_date, + adjust='qfq' + ) + + if df is not None and not df.empty: + # 只保留最近days天的数据 + df = df.tail(days) + self.logger.info(f"✅ AKShare获取K线数据成功 {stock_code},共{len(df)}条") + return df + else: + self.logger.warning(f"AKShare未返回K线数据 {stock_code},尝试降级到Tushare") + except Exception as e: + self.logger.warning(f"AKShare获取K线数据失败 {stock_code}: {type(e).__name__}, 尝试降级到Tushare") + + # 方法2: 降级到Tushare + if data_fetcher and data_fetcher.ts_pro: + self.logger.info(f"降级使用Tushare获取K线数据 {stock_code}") + df = self._get_kline_from_tushare(stock_code, days, data_fetcher.ts_pro) + if df is not None and not df.empty: + self.logger.info(f"✅ Tushare获取K线数据成功 {stock_code},共{len(df)}条") + return df + + self.logger.error(f"所有数据源都无法获取K线数据 {stock_code}") + return None + + except Exception as e: + self.logger.error(f"获取K线数据失败 {stock_code}: {e}") + import traceback + self.logger.debug(traceback.format_exc()) + return None + + def _get_kline_from_tushare(self, stock_code: str, days: int, ts_pro) -> Optional[pd.DataFrame]: + """ + 从Tushare获取K线数据 + + Args: + stock_code: 股票代码 + days: 获取天数 + ts_pro: Tushare API实例 + + Returns: + K线数据DataFrame + """ + try: + # 转换股票代码格式 + if stock_code.startswith('6'): + ts_code = f"{stock_code}.SH" + elif stock_code.startswith(('0', '3')): + ts_code = f"{stock_code}.SZ" + else: + ts_code = stock_code + + # 计算日期范围(多取一些确保足够) + end_date = datetime.now().strftime('%Y%m%d') + start_date = (datetime.now() - timedelta(days=days + 60)).strftime('%Y%m%d') + + # 获取日K线数据(前复权) + df = ts_pro.daily( + ts_code=ts_code, + start_date=start_date, + end_date=end_date, + adj='qfq' + ) + + if df is None or df.empty: + self.logger.error(f"Tushare未返回K线数据 {stock_code}") + return None + + # Tushare数据是从新到旧,需要反转 + df = df.sort_values('trade_date', ascending=True).reset_index(drop=True) + + # 统一列名为AKShare格式 + df = df.rename(columns={ + 'trade_date': '日期', + 'open': '开盘', + 'high': '最高', + 'low': '最低', + 'close': '收盘', + 'vol': '成交量', + 'amount': '成交额' + }) + + # 转换日期格式(Tushare: 20240115 -> 2024-01-15) + df['日期'] = pd.to_datetime(df['日期']) + + # 只保留最近days天的数据 + df = df.tail(days) + + return df + + except Exception as e: + self.logger.error(f"Tushare获取K线数据失败 {stock_code}: {type(e).__name__}: {str(e)}") + return None + + +if __name__ == '__main__': + # 测试代码 + logging.basicConfig(level=logging.INFO) + + kline = SmartMonitorKline() + + # 测试获取K线数据 + df = kline.get_kline_data('600519', days=60) + + if df is not None: + print(f"获取到 {len(df)} 条K线数据") + print(df.head()) + + # 模拟AI决策 + ai_decisions = [ + { + 'decision_time': '2024-01-15 10:00:00', + 'action': 'buy', + 'confidence': 85, + 'reasoning': '技术指标良好,MACD金叉' + }, + { + 'decision_time': '2024-01-20 14:30:00', + 'action': 'add_position', + 'confidence': 75, + 'reasoning': '突破关键压力位' + }, + { + 'decision_time': '2024-01-25 11:00:00', + 'action': 'sell', + 'confidence': 80, + 'reasoning': 'RSI超买,建议止盈' + } + ] + + # 创建K线图 + fig = kline.create_kline_with_decisions( + stock_code='600519', + stock_name='贵州茅台', + kline_data=df, + ai_decisions=ai_decisions, + show_volume=True, + show_ma=True + ) + + # 保存为HTML + fig.write_html('test_kline.html') + print("K线图已保存到 test_kline.html") + else: + print("获取K线数据失败") + diff --git a/smart_monitor_qmt.py b/smart_monitor_qmt.py new file mode 100644 index 0000000..9cb1521 --- /dev/null +++ b/smart_monitor_qmt.py @@ -0,0 +1,628 @@ +""" +智能盯盘 - miniQMT交易接口适配器 +封装miniQMT的交易功能,支持A股T+1交易 +使用主程序的配置管理系统 +""" + +import logging +import os +from typing import Dict, List, Optional +from datetime import datetime + + +class SmartMonitorQMT: + """miniQMT交易接口""" + + def __init__(self, mini_qmt_path: str = None): + """ + 初始化miniQMT接口 + + Args: + mini_qmt_path: miniQMT安装路径 + """ + self.logger = logging.getLogger(__name__) + self.xt_trader = None + self.account = None + self.connected = False + + # 尝试导入miniQMT + try: + from xtquant import xttrader, xtdata + self.xttrader = xttrader + self.xtdata = xtdata + self.logger.info("miniQMT模块加载成功") + except ImportError as e: + self.logger.warning(f"miniQMT模块未安装: {e}") + self.logger.warning("将使用模拟模式(不实际下单)") + + def connect(self, account_id: str = None) -> bool: + """ + 连接miniQMT + + Args: + account_id: 交易账户ID(可选,从环境变量读取) + + Returns: + 是否连接成功 + """ + if not self.xttrader: + self.logger.warning("miniQMT未安装,使用模拟模式") + self.connected = False + return False + + # 从配置读取账户ID + if account_id is None: + account_id = os.getenv('MINIQMT_ACCOUNT_ID', '') + + if not account_id: + self.logger.error("未配置miniQMT账户ID,请在环境配置中设置") + self.connected = False + return False + + try: + # 创建交易对象 + self.xt_trader = self.xttrader.XtQuantTrader() + + # 连接 + self.xt_trader.start() + + # 连接账户 + self.account = self.xttrader.StockAccount(account_id) + connect_result = self.xt_trader.connect() + + if connect_result == 0: + self.connected = True + self.logger.info(f"miniQMT连接成功,账户: {account_id}") + return True + else: + self.logger.error(f"miniQMT连接失败,错误码: {connect_result}") + return False + + except Exception as e: + self.logger.error(f"连接miniQMT失败: {e}") + return False + + def disconnect(self): + """断开连接""" + if self.xt_trader: + try: + self.xt_trader.stop() + self.connected = False + self.logger.info("miniQMT已断开连接") + except Exception as e: + self.logger.error(f"断开连接失败: {e}") + + def get_account_info(self) -> Dict: + """ + 获取账户信息 + + Returns: + 账户信息字典 + """ + if not self.connected or not self.account: + return { + 'available_cash': 0, + 'total_value': 0, + 'positions_count': 0, + 'total_profit_loss': 0 + } + + try: + # 获取资金信息 + asset = self.xt_trader.query_stock_asset(self.account) + + # 获取持仓信息 + positions = self.xt_trader.query_stock_positions(self.account) + + # 计算总浮动盈亏 + total_profit_loss = 0 + if positions: + for pos in positions: + total_profit_loss += pos.unrealized_profit + + return { + 'available_cash': asset.cash, # 可用资金 + 'total_value': asset.total_asset, # 总资产 + 'positions_count': len(positions) if positions else 0, + 'total_profit_loss': total_profit_loss, + 'frozen_cash': asset.frozen_cash, # 冻结资金 + 'market_value': asset.market_value # 持仓市值 + } + + except Exception as e: + self.logger.error(f"获取账户信息失败: {e}") + return { + 'available_cash': 0, + 'total_value': 0, + 'positions_count': 0, + 'total_profit_loss': 0 + } + + def get_position(self, stock_code: str) -> Optional[Dict]: + """ + 获取指定股票的持仓信息 + + Args: + stock_code: 股票代码 + + Returns: + 持仓信息,如果未持有则返回None + """ + if not self.connected or not self.account: + return None + + try: + positions = self.xt_trader.query_stock_positions(self.account) + + if not positions: + return None + + # 查找指定股票 + for pos in positions: + if pos.stock_code == stock_code: + # 计算持仓天数 + holding_days = 0 + if hasattr(pos, 'open_date'): + try: + open_date = datetime.strptime(str(pos.open_date), '%Y%m%d') + holding_days = (datetime.now() - open_date).days + except: + pass + + return { + 'stock_code': pos.stock_code, + 'stock_name': getattr(pos, 'stock_name', ''), + 'quantity': pos.volume, # 持仓数量 + 'can_sell': pos.can_use_volume, # 可用数量(考虑T+1) + 'cost_price': pos.avg_price, # 成本价 + 'current_price': pos.last_price, # 最新价 + 'market_value': pos.market_value, # 市值 + 'profit_loss': pos.unrealized_profit, # 浮动盈亏 + 'profit_loss_pct': (pos.last_price - pos.avg_price) / pos.avg_price * 100 if pos.avg_price > 0 else 0, + 'holding_days': holding_days, + 'buy_date': getattr(pos, 'open_date', '') + } + + return None + + except Exception as e: + self.logger.error(f"获取持仓信息失败 {stock_code}: {e}") + return None + + def get_all_positions(self) -> List[Dict]: + """ + 获取所有持仓 + + Returns: + 持仓列表 + """ + if not self.connected or not self.account: + return [] + + try: + positions = self.xt_trader.query_stock_positions(self.account) + + if not positions: + return [] + + result = [] + for pos in positions: + holding_days = 0 + if hasattr(pos, 'open_date'): + try: + open_date = datetime.strptime(str(pos.open_date), '%Y%m%d') + holding_days = (datetime.now() - open_date).days + except: + pass + + result.append({ + 'stock_code': pos.stock_code, + 'stock_name': getattr(pos, 'stock_name', ''), + 'quantity': pos.volume, + 'can_sell': pos.can_use_volume, + 'cost_price': pos.avg_price, + 'current_price': pos.last_price, + 'market_value': pos.market_value, + 'profit_loss': pos.unrealized_profit, + 'profit_loss_pct': (pos.last_price - pos.avg_price) / pos.avg_price * 100 if pos.avg_price > 0 else 0, + 'holding_days': holding_days + }) + + return result + + except Exception as e: + self.logger.error(f"获取所有持仓失败: {e}") + return [] + + def buy_stock(self, stock_code: str, quantity: int, + price: float = 0, order_type: str = 'market') -> Dict: + """ + 买入股票 + + Args: + stock_code: 股票代码(如:600519.SH) + quantity: 数量(股,必须是100的整数倍) + price: 价格(限价单时使用,市价单可为0) + order_type: 订单类型 ('market': 市价, 'limit': 限价) + + Returns: + 订单结果 + """ + if not self.connected: + return { + 'success': False, + 'error': 'miniQMT未连接', + 'message': '模拟模式:买入订单已记录但未实际执行' + } + + # 检查数量是否是100的整数倍 + if quantity % 100 != 0: + return { + 'success': False, + 'error': 'A股买入数量必须是100的整数倍(1手=100股)' + } + + try: + # 构造完整股票代码(带市场后缀) + full_code = self._format_stock_code(stock_code) + + # 根据订单类型选择价格类型 + if order_type == 'market': + # 市价单 + price_type = self.xttrader.XTP_PRICE_MARKET_OR_CANCEL # 市价剩余转限价 + else: + # 限价单 + price_type = self.xttrader.XTP_PRICE_LIMIT + + # 下单 + order_id = self.xt_trader.order_stock( + account=self.account, + stock_code=full_code, + order_type=self.xttrader.XTP_SIDE_BUY, # 买入 + order_volume=quantity, + price_type=price_type, + price=price + ) + + if order_id > 0: + self.logger.info(f"买入订单已提交: {stock_code}, 数量: {quantity}, 订单号: {order_id}") + return { + 'success': True, + 'order_id': order_id, + 'stock_code': stock_code, + 'quantity': quantity, + 'price': price, + 'order_type': order_type, + 'message': '买入订单已提交' + } + else: + return { + 'success': False, + 'error': f'下单失败,订单号: {order_id}' + } + + except Exception as e: + self.logger.error(f"买入股票失败 {stock_code}: {e}") + return { + 'success': False, + 'error': str(e) + } + + def sell_stock(self, stock_code: str, quantity: int, + price: float = 0, order_type: str = 'market') -> Dict: + """ + 卖出股票 + + Args: + stock_code: 股票代码 + quantity: 数量(股) + price: 价格(限价单时使用) + order_type: 订单类型 ('market': 市价, 'limit': 限价) + + Returns: + 订单结果 + """ + if not self.connected: + return { + 'success': False, + 'error': 'miniQMT未连接', + 'message': '模拟模式:卖出订单已记录但未实际执行' + } + + try: + # 检查是否有持仓 + position = self.get_position(stock_code) + if not position: + return { + 'success': False, + 'error': f'未持有股票 {stock_code}' + } + + # 检查可卖数量(T+1限制) + if quantity > position['can_sell']: + return { + 'success': False, + 'error': f'可卖数量不足(可卖: {position["can_sell"]}股,T+1限制)' + } + + # 构造完整股票代码 + full_code = self._format_stock_code(stock_code) + + # 根据订单类型选择价格类型 + if order_type == 'market': + price_type = self.xttrader.XTP_PRICE_MARKET_OR_CANCEL + else: + price_type = self.xttrader.XTP_PRICE_LIMIT + + # 下单 + order_id = self.xt_trader.order_stock( + account=self.account, + stock_code=full_code, + order_type=self.xttrader.XTP_SIDE_SELL, # 卖出 + order_volume=quantity, + price_type=price_type, + price=price + ) + + if order_id > 0: + self.logger.info(f"卖出订单已提交: {stock_code}, 数量: {quantity}, 订单号: {order_id}") + return { + 'success': True, + 'order_id': order_id, + 'stock_code': stock_code, + 'quantity': quantity, + 'price': price, + 'order_type': order_type, + 'message': '卖出订单已提交' + } + else: + return { + 'success': False, + 'error': f'下单失败,订单号: {order_id}' + } + + except Exception as e: + self.logger.error(f"卖出股票失败 {stock_code}: {e}") + return { + 'success': False, + 'error': str(e) + } + + def cancel_order(self, order_id: int) -> bool: + """ + 撤销订单 + + Args: + order_id: 订单ID + + Returns: + 是否撤销成功 + """ + if not self.connected: + return False + + try: + result = self.xt_trader.cancel_order_stock(self.account, order_id) + if result == 0: + self.logger.info(f"订单已撤销: {order_id}") + return True + else: + self.logger.error(f"撤销订单失败: {order_id}, 错误码: {result}") + return False + except Exception as e: + self.logger.error(f"撤销订单失败: {e}") + return False + + def get_orders(self, stock_code: str = None) -> List[Dict]: + """ + 获取当日委托订单 + + Args: + stock_code: 股票代码(可选,不传则返回所有订单) + + Returns: + 订单列表 + """ + if not self.connected: + return [] + + try: + orders = self.xt_trader.query_stock_orders(self.account) + + if not orders: + return [] + + result = [] + for order in orders: + # 如果指定了股票代码,则过滤 + if stock_code and order.stock_code != self._format_stock_code(stock_code): + continue + + result.append({ + 'order_id': order.order_id, + 'stock_code': order.stock_code, + 'stock_name': getattr(order, 'stock_name', ''), + 'order_type': '买入' if order.order_type == self.xttrader.XTP_SIDE_BUY else '卖出', + 'price': order.price, + 'quantity': order.order_volume, + 'traded_quantity': order.traded_volume, + 'status': self._format_order_status(order.order_status), + 'order_time': getattr(order, 'insert_time', '') + }) + + return result + + except Exception as e: + self.logger.error(f"获取订单失败: {e}") + return [] + + def _format_stock_code(self, stock_code: str) -> str: + """ + 格式化股票代码(添加市场后缀) + + Args: + stock_code: 股票代码(如:600519) + + Returns: + 完整代码(如:600519.SH) + """ + # 如果已经包含市场后缀,直接返回 + if '.' in stock_code: + return stock_code + + # 沪市:6开头 + if stock_code.startswith('6'): + return f"{stock_code}.SH" + # 深市:0、3开头 + elif stock_code.startswith(('0', '3')): + return f"{stock_code}.SZ" + else: + return stock_code + + def _format_order_status(self, status: int) -> str: + """格式化订单状态""" + status_map = { + 0: '未报', + 1: '待报', + 2: '已报', + 3: '已报待撤', + 4: '部分待撤', + 5: '部成待撤', + 6: '部撤', + 7: '已撤', + 8: '部成', + 9: '已成', + 10: '废单' + } + return status_map.get(status, f'未知({status})') + + +# 模拟交易类(当miniQMT不可用时使用) +class SmartMonitorQMTSimulator: + """模拟交易(用于测试)""" + + def __init__(self): + self.logger = logging.getLogger(__name__) + self.connected = True + self.cash = 100000 # 模拟资金10万 + self.positions = {} # 模拟持仓 + self.orders = [] # 模拟订单 + self.logger.info("使用模拟交易模式") + + def connect(self, account_id: str) -> bool: + self.logger.info(f"模拟连接成功: {account_id}") + return True + + def disconnect(self): + self.logger.info("模拟断开连接") + + def get_account_info(self) -> Dict: + total_value = self.cash + for pos in self.positions.values(): + total_value += pos['market_value'] + + return { + 'available_cash': self.cash, + 'total_value': total_value, + 'positions_count': len(self.positions), + 'total_profit_loss': sum(pos['profit_loss'] for pos in self.positions.values()) + } + + def get_position(self, stock_code: str) -> Optional[Dict]: + return self.positions.get(stock_code) + + def get_all_positions(self) -> List[Dict]: + """获取所有持仓""" + return list(self.positions.values()) + + def buy_stock(self, stock_code: str, quantity: int, + price: float = 0, order_type: str = 'market') -> Dict: + cost = quantity * price if price > 0 else quantity * 10 # 假设价格 + if cost > self.cash: + return {'success': False, 'error': '资金不足'} + + self.cash -= cost + + # 获取股票名称(简化版,实际应该从数据源获取) + stock_name = f"股票{stock_code}" + + self.positions[stock_code] = { + 'stock_code': stock_code, + 'stock_name': stock_name, + 'quantity': quantity, + 'can_sell': 0, # T+1,今天买入不能卖 + 'cost_price': price if price > 0 else 10, + 'current_price': price if price > 0 else 10, + 'market_value': cost, + 'profit_loss': 0, + 'profit_loss_pct': 0, + 'holding_days': 0, + 'buy_date': datetime.now().strftime('%Y%m%d') + } + + # 记录订单 + self.orders.append({ + 'order_id': len(self.orders) + 1, + 'stock_code': stock_code, + 'order_type': 'BUY', + 'quantity': quantity, + 'price': price, + 'status': '已成交', + 'order_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + }) + + self.logger.info(f"[模拟] 买入 {stock_code} {quantity}股 @ {price:.2f}元") + return {'success': True, 'order_id': len(self.orders), 'message': '模拟买入成功'} + + def sell_stock(self, stock_code: str, quantity: int, + price: float = 0, order_type: str = 'market') -> Dict: + if stock_code not in self.positions: + return {'success': False, 'error': '未持有该股票'} + + pos = self.positions[stock_code] + if quantity > pos['can_sell']: + return {'success': False, 'error': 'T+1限制,今天买入不能卖'} + + sell_price = price if price > 0 else pos['current_price'] + revenue = quantity * sell_price + self.cash += revenue + + # 计算盈亏 + profit_loss = (sell_price - pos['cost_price']) * quantity + + # 记录订单 + self.orders.append({ + 'order_id': len(self.orders) + 1, + 'stock_code': stock_code, + 'order_type': 'SELL', + 'quantity': quantity, + 'price': sell_price, + 'status': '已成交', + 'order_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S') + }) + + # 更新或删除持仓 + if quantity >= pos['quantity']: + del self.positions[stock_code] + else: + pos['quantity'] -= quantity + pos['can_sell'] -= quantity + pos['market_value'] = pos['quantity'] * pos['current_price'] + + self.logger.info(f"[模拟] 卖出 {stock_code} {quantity}股 @ {sell_price:.2f}元, 盈亏: {profit_loss:+.2f}元") + return { + 'success': True, + 'order_id': len(self.orders), + 'profit_loss': profit_loss, + 'message': '模拟卖出成功' + } + + def cancel_order(self, order_id: int) -> bool: + """撤销订单(模拟)""" + self.logger.info(f"[模拟] 撤销订单 {order_id}") + return True + + def get_orders(self, stock_code: str = None) -> List[Dict]: + """获取订单列表(模拟)""" + return self.orders + diff --git a/smart_monitor_ui.py b/smart_monitor_ui.py new file mode 100644 index 0000000..cae708d --- /dev/null +++ b/smart_monitor_ui.py @@ -0,0 +1,897 @@ +""" +智能盯盘 - 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决策后自动执行交易") + position_size = st.slider("仓位百分比(%)", 5, 50, 20, + help="新建仓位时使用的资金比例") + notify_email = st.text_input("通知邮箱(可选)") + + # 添加任务按钮(表单提交按钮) + submitted = st.form_submit_button("➕ 添加任务", type="primary", use_container_width=True) + + 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, + '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 "👀 仅监控" + st.write(status) + st.caption(auto_trade_status) + + # 显示持仓状态 + 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 + ) + # 启动时更新数据库状态为启用 + 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, + use_container_width=True + ) + + # 单只股票操作 + 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, + use_container_width=True + ) + + # 通知记录 + 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) + + 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""" +

+ """, 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() + diff --git a/stock_analysis.db b/stock_analysis.db index 4278f0d30d9851517c096aa455f667093656ef5f..e51ce36d93769ec643ecbcb4a0ef8e20b3272592 100644 GIT binary patch delta 174865 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 z z@Nf?(!zUPFJw10|X*ya&r~Ra^iO~1`3$_MH%|90UI&5h~>9(`>8gczRF)|pLz4;4U z7FWTF*0N};C66c#KZ47j-LlP_0OoM%0}+AQ%Kp66CQ>xhwxk--elQ)iZrTpViHx6E zku0Aq+u2RdrmXcDmg%AM(2=pZZ*8GPi*MW>D34s@iYnN6l=4nx{Okdvp?Kkh&^}s- zKV?aH_O(^HD5AJ?`t*A? znHAfUjqI)jA!bRB0Z1wAxTqq|ZNCrVd!?4P*y1fV*?q8AdN^elpfcC#Ud@u=fkyBs z3PAR~cK)NK5C~TaB8ax+<$`?}soc(Qv?)7mW(us%`=NfGUtNEgiur{Ra?7NzUFIUB zCZjI(uv%Jjmyn=Xs%|aIH(UJn(CRm@J;d;OB-ffN_q1-?YZERi0iQ?X?!1yEQ)z#n z{1V!lE9<{G=-WN>{F>pSJ;&1PDtDdsUH#F0;39JQ# zAfvUnq~O!UP${^*o0lw39~J-=-!}9bZQ3Q66y=t%V<`@s4AeB1aK>b=+e7{3`5K!h z)h3(z&+@A_FOR*wtRYefMEe8!qE0?5+9o)$)FL~6-KG$M>*?@8RK;4>m-9h3ma>+v zuYqd>A{pz}r0$7rR>UJW;V z3M{Qr+j4YT#J^jm_AlF;rvD#)w7={qP7dipA?3=7nD8uEZ`zQsrn%n>BBQx&W;${B&agfnX zPDlRY%&^Cyvr*G$Tn3HmMvt*({2@)=HQ!bRk*{O6Bdd8)tiuJd3_lJpnJufQE9~UD z`7WEA5pqvy>Yo?U!{%~FBF|Pkl(T!LhDk(^(!dCojs5%`rpjIu^5wcGp_IwH$5-l` zQrt3l$4@a*x@>s3c|;tj6O@McLn@)|(Ll-mcxfLg+O*Fn$QD<&W${oT0Z&{$JVe4r zc=wb^XJQ<3VA^AWW?F7K>|D#6zjWw#_yyVvOnGc7tE9@iRF#5N!|0c2`NvhJw^T;- z^K;tYtT(!MKHm(4uU{Q#`;X>|H4wpZ4!>Kd#4SZIanlv=$H>t)?(#NI6eh9OUcY8^ zn5;P4!w~>m>Hgi#2g;i(x0}mviZ$|;#$!KHFKi%>hCoSOJ~VPyTmxCBMOkZ;lk#^d zH1qq)sK>VU?4f(fGAcHimADVi3YBS#f9w_vThHMRLTZnlGNx6;O7zKYeo6KkXUaNp zuWoO71j$=Sht5x#^SD>2+_Ep+A=CD}9cUJrdGQ>)IE6Hl9T zp0?6{x@m?PCUeIpI3&J%Pd{Ja>x@+DgQl~40Ot)(%`ih`-I++gZt$(C(t@%kSMQaE zoj)Z?a&3NEO2@*m&F%Tk#)^k6*rFR_r(wM1(@g{DpH9z_3hnS|#6D>~TXs9{q%3veVZLeVSW|Yy>an@Ha{|-;nq>g2up@O@IXorB~pHHe#Z^VHI)Y* z>Y@#zSA3D9`i(t97&u~MR#Jz0BO!QI$rP8D)?Cc;E0c^qU6uQ&t?*^7Q;`aQ#Fk|C z>tkSeUwRT{M7%IP=xqpi^&#*|`04g|IU4YpcUST+soOwLsk>+`XIlZzK=Gxxsn*sr zymoMel%HDaC0V2MoYLdyPrY@5>u38{wiYgFWrx+4|cvVqT{Xl5FzsP4&cMw@pRfPv>g&-0W?B&}|5sT~v!95_vWRJ;+pP-1w>Wtf5}?$!1IH zV+Yca49kN-=d-(#%r!5i8VOyr-{_&yePz(*7cA>-bx+|Ed+%##`&Jn$XsPSo8VG=u$Bz@}IcyG-Cf$6eQhBMcXO9xxsv~5B(@1ljl627a=yV6JA-#wgD$sBay_g-D0 zDwLidJQ2Gf*nA`~1}(~aC#FMeUV6b=G;JtVeLuI4Y=3-0Z-=I>dd4ENUy@8+^l1Xx z^_h9tv;#E87LUFxQNC$C;_^-fnxhVQb5@dk{nfP&$v=I21m$}@lLlh{%3oBFtl|Ax z;WFd37sI-c(+2JSDTA(OQefw_Bq+y=3O4rUv@mTy_3<|(!+c?E8p^-|Edo_Jgg2xi zg%|ktI#f(4-ZRr^s3pnalT&#U=%kd5E$kx$j;{`vBQ+Cr&gv#D4~9#nVPiHDeH1lw zq$=s=-d*^<+DTU?dV>ySa&ULY$YeQj`zvU`Y2yb)rIUy!?%4gNkBnJ(3%{jJ?L0x8 zJ5efszTHPw>|GJzpvFzP-E>aQRrS7?26&mlNNJ)K_VOh?<=L%2_Ovw_KWjP?=gn~( z-L*HO5L*0~wS3DRuXw3CTfj;PIG4sUV^`gHTa@-kX7kWzR5$9IGw&s!T*tthjZcnZ zW{YM8^S;Nn^(EWQUDEKLu=Of;HO0IRE|+o;5zg~Uk)o&c*zqV z%!>1(kPjHO?bSZWXtV#;UjH?4|4);=Oy@O;4C3&4XyMUep*28vZ8o~Aup$*jNzmlw z=>CCOIF~$t?4O=bqaP`KGjgj%!@Om|rGw!OptX?izVb4Ft$SB_%Lo6y7HdHb#dOxE zFr2@BatRxL!evojur2@sKGWD+W{lYg!yi-81$?IJqK6XAf8OFkLc&#aL6~&;_H0ja z{dT)GC~h+&CDOQnGLWuTN%#cK(6cpW zbU_x9ftggTfP)Zz?S-~{7IdY5o7%GrfRFp0ieTya^o@`nfe~g)@oh``Xc^Vm`ADLd z_KV8dV|Vq%7dfxIz~x0dBj=#e^2HGq54a%#jZFgas)@M_?D`jeazz11>M1tP0^s#L zk^h2n48%bx+?gH14s495!L)sy;>1?xyI0lrPxnz-R-h@V3%n@NtlSyG>Q0_9MMWn$ z0$MRN7g^8QLkB>AvFE#wqP&6nslrqp`YUkZD^tC#o!J#E$Wr)4>hxa&U1kB|!(RMY z-MN8Sg{g-_sS=N4m6{D(^6*H%m0g4WCkK^t=zqU4*b*!)zjSbKZ|W{A)~3j@$x?Rd zT9@>AdzN>XOo*L6s%2{@5LBN$^i#YyI4s@U_G9nZw*moyo9>KoXnP8_vFfMW6Xo^= zr_xOKjzQ}FW~}90c@`7E;%YnceQ#J_DrT`lyurj#4=pxF8GW$2a(j`n7?JD+^F z54T@`%M0;dKwhww#eLS>oPBc^B8zuT%7sY`ORojbgY&W9Gzfpt75se0w<%8f+ZS_? z3;)*~lz8!g8MZ3snXl%s6ltxLgy-fb$*`-x@sWjBS42Y3qsKVVh=lIoEAH_DQ&jl1F78C{F8S`=&VSuC&+uSeS%cW3-JibD zC)Xm_&NYj=^D<4#_H?{?8|&XydX;hv5#-NmmMVO<&LfuvGh0xo#aI4~WdUT36NsWZ6Fe zarBw;6{HQCBUvhc_B&g4?>@D+USE8s=FIB(HM6~q5Pubmv;Qn5H%$wb_m00AB}+c>u)|~rBp3!&8>6LZ5X`Y5)Gzdk zc9Fi5Rkbc;6%ax^z2m_abEEw!drmdwS-1ru2u4!hk4U8KLR*e|Vj@(@6aMstnTq6E ze*}qn4vmL3a-W=v=%z0E6af4IQ z@E=d~Hh+373phI^WyH0&m9*~M&-2*dASI)#qGfxx@+e4w6KSP*Fh+{r`m0mLL>=C``_S{-oKTGv%pIbwDNWK#7AI8n28MS-~;C4HCGQB9x`4N&(cT0rMf{*ka zeM8E5jmKbDe-D0Oe^}?*QhAKzsXI@L%^|FRHXG5C43_j49|@H!-`VMFPAv%}NdCS4 zXBH5)x^FOsx;|;;LDU3t zGHjt1-l<<}eWAX`V}C3QZR=!pdWSS+d=@T8f4j@a`Pg!Z<2(Inxzt^>pjUe5yRnB& z=elaKCG_*>@0NbEHl{^bTTpkPeRqHn%@041B##_A4xCmsYh!E%T7dSVQ4t7C&HBer z?Rl~JwnoE^rvk~`9rBd0L$0?2*zgrzOc-D=#-Zp#Yjp_gMKex{0 zT@)W&uKbd#bF;24$;!O?y4>uUGc*3nx0r8!m}kVAGk!=m0zxLIPo738nOjfS+9i!4 zWKRkG4N~@D_pJ4mv;&{|`Da^kP}U%l?7{<|K4slJ5nWScgDnKvv$GIn211bZNmq1- z5wQJvljO)nH%xDjrzHK?cnU**3Z=~A4@Z$&94im$>wo@_QuoX)cSI;?r(hT=C{I-& zgrbDts#qvD|2j2X+QQa^`UB;9&~*82d}yeY4vh9oK$k-mcr4oo6pq@XMop_LZL@xQ z+mjM>fnTVsTM^_Z!v~J?k)ozE>!OK zpYA6E6P^w<^S%f%qE{U`8Y$D0jyqQ^UlS->e_8Dp&89?kTK6nFVfA((bL;E05-RzR zZH@ES1#C+y%faZ45kr6P{n|@LopEy;PGn>}&(m>#SM(C2_r}OrtEi%)IQ8Xr6hw5W zD$^ElC@bo|NwqZn7FIP|+Gj|#oO$>Wb9hL+5jxiMjN6{@?AcBch2E(ph1C%;dg8QB zp%UGPs&C2rr1+UtDWMD;VuiY;t@xvPMJ>fenVl09+MlnpeFT9{>%L98&KB;l{gQe< zd%L%++t*U_kZ$@od1F^U=plBU@|I%{E>DncZw0gDZRFIFz

h|K(MUF7KIy}MeU zUJVC7U*i#QqLxx#f$$%6w=l{xVk|gYu2StH!SuRv70%nW5Iq`u&6!n+V}PSnQ?vTk zVIdNGX=zWn@{_B5q`P--bTwx_+jFL;ra@t(J;Km9u2YO*i(I$IBAyD5jU8*T@9@(o z650wmdQ1D4H!nk+d*SI#kMLI=gB4Fn)7878QDwG<9$bHg#*eN0HzAHeXd-QnZ|Qj0 z+s`M!TA{7`wzodJSB`!2W^@el3_$Q-AD|Gfky_CR6e<{Gx}-@rm9%LRu`i=XV!Ad}b{Nr(WnM(GPj~Nr%Gdm7N{Q|KDMej)5iH zXnTr|8iUkd&?C6}+8TuJI*JNXmilzO9Q)h7-Oc7x)DE(j=y+KIcsK**()N+Ocxr$7 zsl5wZ)>9VQMOA<&!IhpafAI9`Dxf)MoLTf*6n6jZeh)gXYS~|RdKb%PjV^RX8~1C8 z$|>Cr)dnjZu!~T@{d_kz>rXCPe{#>tj!N69!|TE7Tb|p0dbdVAF`ju!&CdqQ4f_JR2YagWwn$-1`TEnlw@chlZ^k+7 zoKTLPdm@@}IomYX`Wb!V^ad=oF)PmN&`xZ>$+`($VI~~+Sy)%bMgICopNJj^)Kfgx zq^@=W11|3MfmvqS#26#G3k_3c+s5|kN-b6Dwq6q>N1p!1R|b7@Z>W_2^#Lzw{rSxh zG0p9MJs1ZfY-~5Cv1R!t+4$v-{uI2f=$X}dRb#C%nkZ)d?+a8K4jI8+wd1U+yKgXf z4x&}&)(SuI8&}nh8(%0TU+oxfezM$~Fyd@Yptl*0yUenMMS}jQKO;ePyCP)nyoVh9 zJ&qAa`DZWDv5q=NcDbE=0H5k81G0Z?Ys)Bhb_O0dAQiLwM98^M-wllJ5Y^*xw7c6D z)%!nV|Ihy!D^6FJ;WOuddg;7=?SguX|22J2k0qC@pdKMp=MYA7%ucFosPtvK@4mO! zt#{vi_3WFpJ<9%wHkY-_>veow3jY3EI7nIXV}qhBMZ>K01V!`97WNPBMo%%_r!*cd{8|Ro2rh>MYh(rhS#C5EGxpOz?K%FzT_bTMp~H$w5?nPwwxs zC|s@zJ?iVtmUOwKetcYf9IBpnVzzz(c0!v8^ygA-wZ28NJ?h&CS^Ys+pcLFOBR~qC z{I^%U%VqcSq@;Kh{5VWJFw24%TnGS5!@kEthN?E)=_MO&dun@SDzpS_Rz9N=$aYcG zu9_kjbf&x#tsvVIOI>Y3VwVH^WOhn$w8T3r10^Qr`W_OK5*H*v8{(3s{LZkzP#XvU zM8LM$H$|Q)C-y{!%dx_q0n*m{^+>PH_)T;B#WB>??m8VCBJs--k{y3EI6w9Ylc6IM0!{Or z!*F!vR?UgT)sd1mHGrzrK_^x^(<|rEBfol+2)hxTTiBtfZYwm}DFI(^qa$j(_H4nH zwbaj(2bT9wj7`hR=Tn}AcwnFHX|46lI7mYD_2HEFRap{Oq^PG9)`dhA+H33dL-pFQ zvuBSAc3H|>c14~yd%nOdfzF^EPFMhhB)t0@_;t-gK63Ej7oikGi)x7E{dL1sX)+~UD_S#qp6K`RXL33L7gDxz!<=#9GuvphHFEaERuWp*x zyxp`LW2E6<_l8E(t-~d6u-!t(mZEJ(>OKz$p)>dRDD##iFTm-d)zP8-$*af1A~-tDd9R%WN=njlp5hfjsiGo*U&`c5i83yHAb|^$9iS zejCJLtwY^=(n=J1MU3>g@rKxR4fl0jEm}t0!!I-ZbP!uF?Rl`JMw<6L*|KO$Gk~i- zu|d7o#Nz~d zG}m~kJ=63*zQCqi_cO&KixbXB7wr`q6X16zDjot$mw(du-b79N& zwVJ^pcDL_IcMi3Yb~KFWiBL(SNYbZV zkrd0e_LU07*jl6L##HZ+qRU2wnXbNRSd`L}tNhG+f?@fP)-#V}ada@;TQZEFLS5%P*IYHbH9de@RO8_o(`7LFCT^^KYDupfR0Cqqhs9upH2i+eH{bBA)k(a^U+RU$(hKC@0ubmrEK4>Dax6ocAe1-d$kd+ z5lMXyHy$gM@h$){Z!FfTQSp80aX}p>E1K*H5s-M5j&Exw}h>8%MP&-Y`#qne|(CA zL6fcvnrmA*>j61$&rh=d)loGC*&UNb+d$_}aC}?WgjGq#Y}vnV zeM0;Et>1vLy7MR8J`h;wN2J@Rfr)XREsQwkfi0k|&CB=OVy1RGyc93roGK?~pKzq0 z*V+2uLN!CyDA>_R=O?#uNlJOrc-P_>d4EIyAhX~tm&S*CmYMGRhtQjOWcOY_Heh|A z>C0XW475*p*tId}zYh=caX~mnS|>-2B}U7FWl=rmddl6HI?E>81$VWmMI(?ghaio| z#j0CTfX7DK4|VFm4t3o79>+lH`|T4UoD|cMx#Lz>$9T1mkS7=gc*3f-e@=dg?n}bl z>luGJ5wqS$QV#A7n9EPs$a2h&fKS^M9ash$t5&9TxE-McQ_S5H+w?t zNqcf0#=E8B@#YQ76h*;r5tOz&AhkTUuVUbfMl;-&XY92zH$!8{-PDeE>W!#fuCK92 z!G3I6@!+%w34eN9h#cKjoM29WqYG`!|LF0YpL{v=*6!w!eYx1e<;(u&Cp+HW)jcpX z%_DM)&oe6@9cU2dJhA-D>Mi^rw$>~zj0YL`DsH_)ZpoSKNM}hm&!t@9G zShMX;Gs}v`7>UE|^>45Ij;M)dFf?}5F1ANaOFJeJgPDq5E4=XL?*UBn>y~w zCr=%yP?=crkYx)xc9>b!!^x1pbT|Ew{ZEZCLMPhd$+qig*)zXo^$ubjHtp{O*De2U zr-*hNJkiPNXZG3IMWD333Wi^-ffbP6EKTST-QoE=5@PnPdvw6oFtxsB0 zB|$9|YSLrsdooui{2bg<>;5fL^zzy`xhJN69;!GTh8OoQTXL(De~$9Uw=2alv<7C~ z1zyUr>baD{>mH1P6&1osI)gZjoK>krRhxmBCmc&OSLI2i_uL4M zeIu|3e@k`&otKc$(qJ`UQzR0VD2&191wd1OKU}wv%5yL@XuyRov^+VpN4<^Fctd(Zn;amt5zRS6rP?GQzjX{WP4r6A>_lIsYfN`U z-*`y=-az&CAbDqhs)71%ztW z3wnU?hEefdwbQPCW@@bVDI15Xr$})_(ikys`q|-^J;j|jC^Mf^gcZN_r(*uv)@bG6 zg)x+7wKTJ_`!GW#Mm#S_!x(p@o`TaA`5Kv~-ozkrqvw5uG)k!NDlO$z-qUB~9vGt!J<@G0m8^Llm$ofx!n_PY>$ zCyto(yYMn8Z@7Mv`QjbJAyZ;H=m~_L!i-g>J!&)tQN$H<~$bRFk=shUa413_=ni!4{K!hhHdDGz^RGlGa)oeO|HZmvH zY28-B`QoWZqonu}R7i=;SSrlrjC~J9u}C?e%n^Gr;;x3yAmu)*KeuGs551%|YlxSg zDaa5N@`G-iL*s0BoLPIDoH%x361}fgC(Cjm`GZ)To>>-K;fG_D z^snQ(V%6fPKvtsCxe{V3)x-QuXxnrjvZPtjuYcn5R}u?&eg-lGB^PI)e9ft3W#Ts5 zP-sP4!7Urjs2Ky)a38^~L07cNS# zY4kUj{B!Kst`?ovic+lB(oAn}x=tYmD4w%M1wA1MS0?4Az5QWmgUW{Oja@x9Ee?JG zCJSmop6<$|XxJiol04c5s<{`s<&{v19{;m1U`Zj}=O+;`85pUC4)C1;eRnVPy$m8h zXO^zZG}xOYBan?k?@}-(M2T(S(Sz0qU8iVFQv0+hy50UQx^2ae?LS*Kwh?7t4{W=h)ebi%%p;(eYx~A?;VFAH1}jVflJ3Sk2J& z6Qro}+UvV!K&V_e#m%v?9Z6S5L*LBF7?wAd?TcwhN1VTI{9;;^@-*lPfLzps>aQ7v z0wcT_98O^j;14K1*3XDnW-jHq#IcpdzJrVin}8DtDwybeAuG%Qbq)Sd+J1Z_64Ed_ z$l*Y7{2i&AoPQ8=6!(AgmYMP@8bj(LcQ;dLOmU`f|~GBy}pIfjCl zr0wm!OtI&Z^A0DIw&My7-$LqAbNlhjpzSYsW@;{>^EbK%-y# zMI#qXuN$T;IHaVo6l#a|E36j6%5uvGugp-MkeAG=N?J>0N<-2F13kKiKqFtN!(rM} zHkI;eYa&qxvae`T|JNxb6qz!mA%_svZAmC1&co3WOd`wX!x1=9q#TrETxt(54~L6W zO_vn_siLy(3MKoK8Q)c~W8l0{6NLGmU5>OlfdO=m;D?(dy`iTfz$$qL1r$z~z~J`h zS!LfctJrY(xtsnC!7+vdem(4prKL+35{6QCoGOcl%L0>)7cRfxC_{rI&*MAgY}lj7 z72)Wx^-}TU*THh^>M{P_O5y@3JnmIH)RBndkkm(yspc26W*wv{+`1AOPrQ_#Ow-tI zWlb=a8z%>Ky{M)p@k0BhN@@mlTw*z#2gk=apkY)ees=8sVGiq`&|Ib+9Oav8{~^mP zPZ$h?L&xO~E}_onG#cl_7#Zgq{Rf}FGz<2QRK--t>RP^_dOPbwFU|)g9PDRYg4eiu zoawqN2gGH=6yP#=2bJs*N-U3$?YOpmEQ~{(pCNpZ#?m{G%Yl0)hiOw4i-ziz|BT=q_lNus*j!+?oOH9Lg4yK=lVvKNy!BoyK5peHGG9qDl zQ%W^Pqw%$&7{ospJHzE79~LLpNOg>(kJd#Sufw zvH7zIyc|q0N*|``_Wr8xLn4qz2vBwQGNwm09{4pW*?1z{2U%}?XCB=Jd+=;*Q;2JH zr}8;G3LY?xb07`Z#v5ZDXtF^MWY!)HG*oqrpP)xYoR19+N9>-=2KV^vh}eKs!Z@6o z@@a`Nv9J17v_SaMFW!guT*)5^oOZmnHo_lLJ3dBqz>%b+&++(-FGoS8Z074x5H`a5 z#!;hVEwKId`kR~` z|5jX(Y9AosVCmpsE?<_yUCVRewA2?hY#O18mDYu`ZCYPFKuLVJ_h$IT`5ID0oL|66 zh;=+yG&8Eextv{k=wdnc!Uxwm22OF4!tCCW@o1*s`9mY7;nUT~CQ0g$q6`I2*%qMmpyNf@75b!{>&_97OzIeG-OUVgwp&TkOf0};+Zs~mmdoT8^Y2w>vvMXRIOuW zk2DV-_9dBtT*>~)`l2BM#!^395~kyiRk{6#h{1x=+0d=hRb$+~hAmwkFeIzOat znZwv5LI=_j4s5?Rix-VB2E%3KnMz2!ee_WH(_bZO7&FpB2OKs!Ans!)^;lgi@hC6@ z&;rz%&o5N&-A0rx5Wt%KPMe^o}sX8&$Id2#~f5RwL}j z1i*fX6Zv9sgsL4V=b>4+*HBHttA=>vmIyQXd&Sc9UcDGS>k4r7_gf0z#Wv-u&Bg(& z5;FH(Hnq$hv@PDBuu|{)S4IYSFS zSB6>eU|+sl$-k}V$(7PY``OJZjENb?p)jSPW&zT?0bP}+wBb-uGSU--sgP=#^|KL{ zkhANr^CrO|U0}AQ?cp(zu!#W=k!nfVU%td2vDL_&Ph55vr?NS6&nTFM0~mf-an+Xd zy(LpVN1ue}QFd@Cyt{9h1ihN%AfRI7p~45H``5ohVC%V-y6^8v_2o8+W${9m+TXF` zGZ{|^zu6?jNp&OUq5lpH#O&i<-S5??ZDAuQM=$OcogZi?$$#aQAG$I07K%4}qXpJAxn zkP(1Y@LZ=!)mxeQohQ)Q{pGbn(~z$O-I(bmle4~#GK==ni3oi;{bEqmAyzzKKztWQ zy?()ceSLW{B?`v9I{y+Pl?O(7`=AZYaAcXat8+;20ax&6>~Z$35%NH+H)q3{&Z^fI z<5HC5O+!!T|MX{@&S?6qTfEZiGIs8)-|tKOEI2HoSexGj4RzUGETIyw6k0vX^QygI z1tv1dGo8Eo>dH_*puYIXp+lChE9v=aB8RQ{YN@~dicoP)zdaZVSyG}*Ag!Q`1cTTH z8a)D^vY)EL`?LI+gk1XXY^^;=QSb_{uBftWeCiZ%yh^B;Cyl*##wex%4<>{1)rS#s z_WP9qDtpVbWlLf4tav~yQXo$0n-zbFMmkIND;Z0*EGNGGB-|e;Qh7~zS%UJ6K2s~9@1e+7JkH0CFg-x(1@`ElL7r-zBB(Tyd z98(7n>%arf>dLQPb3JFP@vCNAvIB3qV*Y@dR!V(yvSq5x2PTr&uL=!M+CV<5G z*+qlZzx}V9#1F@*bED!QX(PjY{;)|jk;0b@L;~*fDtsDutnd_|XhB7&;SN0i0$GSL zdE!9d_W6r6ks0QUM-1j(*Bki(EPbmZ z3~P^cc&`9fzm5k14g2~e_zf0=jGCfGb^dBJCpO)Itt8)N!?5I=E_o*rE>c2W@)kvr zHiZ&ikx}3qm9xG6MdVqq=++C;kWw3Jc`wz{;tq*#s*WtScVs~OOUj0w@c5WWh|85L zA6|_K3gd!4lJR6p+rqmQD`E`U(Pq%Oe*7Lb2QpPwV)f6@A|cOk194!vpL#u9ik_K? z)Q{qWMB#V?=i48~!+T8JOSwsEGhd{dgBjxc*?<*M`QZ))R`avW!!e_1Q>?stqQAGo zkQk4ixA)N?lPCo%2I&PcXgD04(3NK5z;Jnp(M0&r(Tm&2G=rw2?1LHy@hDrpA*a@p zn1fOmJ~IjxFpUctRyiwsu;m&&sgits(+SIi`FMj|1C?aFaVvg9F>I~?ww}ARKZ^ud zg*_HjHW+4B&6$bIr0R`^I7F}k&XfpK8c&Z8v%H$dtB`&y>aH5-#2#upK6xF292Fhq zFA0&!s&a}gR~@Q~*mVf>^v?G8u@GRW7@E>*6S!NI#NYK{$ZaW>D1={!Y89eTEHjn0$;~!;xqvWKiXzSZ2U!^%AKhls zcHifCdC+rqUGA!)g(wdzDZDD2NnOpJSaha!c>#&k?p)K6$q_>lJ8ODV{P{=&T|4$C zj#({#{ngh-^C(0@6xh~m3ipE)i3{!l70>HY)}zp&WskP;OLZTy=kOA_)vNl_8Dpu( zs%6oPrE0HeUV=ETvI0Y!a>x~+{NT7_k{jjCO$zmA*OQd8P@$fw*Z>lY)Edr!9IWdA z&5HpcHSHpQc%_uBLkJH;dafG3;w1ya5I$g@Fu>xRR;%y_Ppy$;aqTjNI{*Mx+Nb#L zlVn-&qOu?Xfzf~lm(J7BTq?H~C7>XwDHprUIrdzta`_N`v;X1;#Ol6bnGsjV?~a3u zn*q~R58TC8!D#Bw;{%I*worR!_5o!G3HIL|2Qe(p5s5kD%3(xt*b*kh5}NnPj^@so zIX0a*$K2(jXsxjT=D~T%Xd{6*5^z%W2E^%{3ZSlVI&cZjsbUQ``zKGP8%IgW%Zdx) z|rR(=-E`-T}s=Kl^lehAKtTrO^apz?3uU^c7eVLYLi;;Ig z8hjc0Rn!30RMrLbRqm{ca+?#s8S8zoke!p#|W66O!<2}PxRHm8)e%#0@1#)p7Q znC@46$yG?hy!ADGDMOqKGjzKUY;lbn{HQ#s)1~v{@+gPWl7rpVEuS0?lRYjf8|)6p zzBwb58Y!_Sivo7CNA8Z+WCN6euuYtskM~k;H3~tU{1++}Y&4d&REFH;3M7JI{i{XA zz7{@R83w3AEn2f_X=tgTn1yZ+a`NJQXgxXU2Y2|}h`d>{VuZmqB*!J_Ad)cMpdpM9 zUr#_n)Y0(p58(l1mYP{mAIP=zoK+i(C7B^XUK1$b0VNTRO7WuWLG3jkWFML(h^6Ah z*lNr@JJx$0cr6GRX{gk{vsnX0`w9WrcWxd4_Rq%{IOoG}1I&@NFdls8MIXhY?M9$T zK$~L$DB)dz{VR}aR7od_fb5JeO?)>H5^@7?mzR{1erX(IoFR`4yKsoQmTB0_wmz|hS@o__h;kzSN?C{n$Wio7jP!xk_L zuFQw4s$i2lz>gS$%A{J4!etYIySQ_}Cs4CPSVr(U;&lRgOTkp0j~pB%D?Y!!2j8!W z6oCCN3;wuAng-8_??P~?U?X4%Fe%WAG!EaFKwkxDgf-L&)wuvZ*e0SgO5sJm_maL4 zrNc<5@T`0950nr72v_`Ze`vvj?bUYsbP#_2cphD($_O~3AQti{S&?SJ5nqLLL9}>R zcFn#VBQrpwC+m_ZAmLDMKFYK06DL)7yC+L&*WZdLk|vaq=a{9NBUtdPXv`92g@z)h z;i5301r8~Di0tgh+zUfssnle}+>z6plM;xSd!vz|`sjoK#r0S}dDyVJd;7DyZy#sd{NY z=ps)}g$1hv1pig2)JDvwP=Qq=eq{*DN|#2{{>CNgDSobG#)TBwJlCs=OFYn`mUf5| ziNoq&X+T>MydGeuP04s`bV=mg=7efuw1S(clmk~jYmtc*? zkv%HCr+B0qz_xCWsLCi4&vC3RS3u=EBHQprbe*uAJeIV&YknLtp?xidht8y;6{7JX zzE8p85XlICGQdH91-EY&N3t9N`=B5t_oV^hOjU#)`BXnfYZuPjIvr5)Mkb%jo8=Yi zac3OMRLgs2)nR^6dfmsnuiE~yq{VL$C ztOmp{E9Qh7`QYUGlnCmb=!-BEkgej!xH}{GA=Gz>Y_OAb2}KMkqQGhM#wQTRrR5|=in zQZC@uXcc@l?Q$CZaGETerg!=STw^}@gZty@Qv%3pF>1gQxK873cew2Mu)bFps#GPP zE*e1YqVWQeTiInRSlh`JiCWobI}a4sj!8)YREh~x$yGImxbsMeSvx4oNN@n>fG|-o zn@{uE(%ndCF!3}jA$Zu_*GSSX&xS2mU*ON1s0y<7DAeZumJQg>ms`D+H)h)!c?=+q zo0{f^b0E4)ui_b1*<0mG+JgT+*f*815Gb93VU(%*BkT_fk9a1t;hJzG4Jkj$7-V^7 z(9>~7IvF2d#s?VUs(a^La=um6Cwq$3D$3|SN~Q4E4+r@W(c>&>DSIK59xFDiHh0pQ zFxG+m58(&lG$YDvq|geCy&>LV8^87_^U5w%Crq^9d^bH00FZbZzXd_>fGooWKE;Q1 zm0)duF3JI|L#Gt7g;0Jh}xx zowp8x-1?73&?<$+<8f5(i%b=ClRO~khx9l>P_H}ikPZdCNfYL0YKIsAkVXZKpJfd8 zNrhDrxI*f>mZgKkuw-#|pki%l=>eo$_FUpP3jOkcmRNkCBxJ~C{l9==Vd<;ge?Q+D zX!BU_5aWiOm%(Ryb+Oe!nkeM~X1YI!ScXIjYP*)_%2ZOf)=OmxVP@m-C3+<8dfVQH!AO2cKZ zDD^YzPUF7^(f$M|BX1<+ydu9JRWiJxDz!;rKyk3_*fuEv^a{JlSCjW`xHbi5O=@1& zE+&%&PA7UP-f|8BF&YlF5(X5KJ-V3KU)AB+*JjoA4j`o+e#x$`+HF?0>R8xXj>{rjWSi3+v@)5fq|`f&#y^@)RY#bX zj*|1!T}Y{dP$9eejW2Tvpy{o^$eUFMR2F$gQ))K{{){9-%G-`+p{uP>F7L-&6rNr* zi0F!M1=^L8Wgj5lMQDm&kc!9tl}<$eyRB;;Tc2!rd1qiBXa$cdzSIW3&t{HGl%PR3 zviGUj6EqT)(#vwHJB>DEhFW8pG5mXAf$B+KUi92Ekk8g*$^5dkWlZagguiCPi}~7C zjOip|qLx^(Cz>8NC2S_oC1-B!X`KRU1muOuu2{62MdIF-?4xi9G0u8-u8~4Lp05L8kjB>3U^)AI zeQ%6EH&1!v@-0b${FUFOB$dW+xG#Mmiy}V@6nBugwZBzL1j$md>4ML{O(q@%2csIi zWZVeVO~(P{j*DbLHNKs~3<9I3 zYIn$cF{=FA579N5vJf^eY*#EX6tOl-rySobff$SwhrE!lEQ`bDZ(%N?_C#eFpHEs zDegBOy?i`f38N&5vK+iETs1tlIL?Kp3A0=`yq!wzhvk*WHrPKK-k@79T`^%(Cy@#p zlH@5WPPT+q!VcBnpDkWXq=I1GhnY_?TA5KHwaQY$%*{bln{#nw4$B+{3spcns8^p9bs+gr0-ED;tjMN81=#bzS z@`3dJf8SXk(a{fy%^f334S#%9@4Nwo138cvG<>&9v=BwZ8*;!Z^E(@I&!>qWSipQj zc_GzAX^Mt@5M|J*G9)O?W!kt!lR-bnNAP@ z4!Vh;U(&~Xy|L6+%eP?IyS2H{f1tO!5H`{_q0Z_x^J>Oekpj_;_LEHPVyaO)W<*#9 zkFr!ZHbGGDRpT|YvoY+{dI>i2RK^{p$-Oa>s^Y})0pvJLTu_ED`{5+YC=Q8N1VWe3 zPU=m8MDOSvrd{qztb@rZEM1>V(T+(DmdhmGq$wzbdNkE0v=F8JRSxg54CP9GP?VI+ zK9ci@J@Wb^gBS#;64j3EmuUKp3a!?P5sYWZ^1jS>_A9VXf(S4;K8f=d$)uSaRa zGcOAROZi3y5cIRZ6C+ELn-rcw!~1Q-A*RUBCG$E?oEiz zQ4ETgDt6^^ModCxxv-4{FW3XyCn)3^=FM*x*GidjB$D^T$=6rlWW8wxczha2eb6IDNacNP;b_-3&ns{GuNBo}E6 zsj|;91~SktMIbiFe-^>;ePjEOIGXP?E?%D8o8Uw_9-ll4xObVxQ5DtK=P+!5_)@V^ z1}QIt0K`|wi0^lg2I{`EpiAIX8o6r@JcQ>5(iof05h*TZl-jhd$NnJ~LHC63zqsRcI)`9I)EzjJ>Dx^+{B{L`x zBD&^TrO~P~D%qRkV`vlQu>9(wkL7srWr9b_Arc|Lx|@qIA(S{8I*o5X6N^Q}Iuhup z!avKyV9Lv-T!ePhOQkcGoMk3Pq{1>eNM_V>EZ6kJG6;k(Hkj+nEO+$njZINy&!yc& zm@oATqNc~}!aT5MGU*GND@5YqGm~W;63R|Tj={7#o{gno$-ok*bA_Plk30w(I`t&N zEL*>~QZbY0wd5w(JqL;-CfS`z1hpKVf=tNmO8dT>BEdW;F8Nv9ecZT(gG$e+UXYke~{KZ}Xb zs@~V0by(>NaV#|pj7If!){SBD3$`->lb$PY0Lo+lLJ58|Jy0RWf{UuMN`3gm!5DqT ztx=l0hlwi5nNii!>xm$@#U>P;UUg<5fpg(!&%3E{g=KC&0%`|V;JUqSyIuxKM%Dhc zB!YPaa6?Q|8-3U1@f>L?9yHS>l<@>PK`S`B&Xe4n^yRFsD%;s3OK}#iYA?c1pRX;o`NS!+z>Z$5xM7ygg zq2XQ}67zM}XGaXzvU`D26ubDz;5o9&=8E!A<`pQ$JLE@9%0ly4di&vdK(F&-<{?~e z-#C*D5bKW5X^D)$S_FU@+) zG8WdTwjF>PE^r8B3t(FZ1T)0I4+dk>JcQqkKq+g)nQ*xeC-|U=Z{kGOGM;UqEIK8= z?mYt=#Qar`hO=~lMwt(6?f~}+(2a9tg|{#`nP9W$%f{-GciRPR4@e#W$qb+f0K7Wl zBLhq1l=xFyjD?V;tlI|eI=|?|6EiVp)!s{fg_{tZj0(`dmzdz(t-HakTxfD#=_jIw zYf;Xfq9pq2t*$Eiq31mGefdd1!uw1K2R~#)Nq`nNkt5*FyOFgw(HQw{C&QKbsK)X- zpMMdMkQY5I_tE_a42cPAgK{?^>9XV17N4LSPw!vIS#3i;4eSk$^}n*Rd-=f;Y@fFr zT&uPX3Zy2=iNGb9y4m{5Jn&t?FajK^`%Qpr&^VKMP`Abm*HYE|0SW$n59|kH{vw{Z z{|`A*jjeH{c{|3rs7F3Y3z#Cpwr4jZ)F$?bD1rh0!vmbz@iZMTpu>8s#e(g9&8;!n z4M7t#Pp)6Qu~0MaQD-);Yd`WTaJYtSAH`PZ{--$;Y09PEatn;@KD?Q1q<|m9B>6V1kuk_F=noExen1UxFU@?_j)xLLs zTz^>#Zbe|3o$KNJ>F&*4v>)i%HiJe|>0ez1DATlC{1m88UFG7c>Iw&fYPR%A zKXq{DAMUF6hnl~47ru0!z~iZw`x8UKB3Y?U-IW?N$5dSttW3a*SFJevH1H;uDlLn_ zv0{5pV&A_lg_C1eJk=$Zs!ctIzv6en95K}u`I4uD)uQ{^M5dMJ$qql2_Rb?-s?rkU zqMrEB&&|7DB;-hK>#BXSVWs&K!qxtVY*yv{q2@lwgpoCIk7%+BjJ>q`5rnMkk1#Z4XGm>;0BNBj}wYZ^46Pc$D+IU4r`%vrEq)cn7EO3#P8 zEs|+UG4)Vm%EB20j;6uqGp1?i`I@;VXAe~x7#Ei=Gm9u66mY{}fau!htu#eaMzMLfdNfHZ(q3&^cB;3IdPV zee|nWdu#p~vW|GTZr8lHTiN&pupPmFKD%aD`_j1vmeq3A>~n9B3o#hg$@;5<1~J-b zAkUf(Hlo@kJB#Y_VZmNo#=T^7^~1hdW6+Ub83kHwaKAAJp!`kKCANe5-#-sYegg`V!0tM$<8&QcFk8mv$DJ3oK zZ241w#F&0a?g56-6+Ds?-nt7qSx)E!i*I0lnlLGXMr^R}0`3hYxAUotxWh!8WFf)?lm{Rk;u3x<%lIwhvMMI|+EC>) zGQ=`RqNiosO(%Y_8STC>cv}G0bGrg3FHZxGk13X>L6mKOh7|`B$$860NOmzUpe)XU zm)c$u>Qn*vS+`rz4_EEx_bfaEUsF}1yhBvs+rBPyK!-D!Rs~e%&;k5&`|h0&+1xMLdpb&!We9a;WZa>(gdD?yjMNFB{@seApaZ2J0EZ_OuJw*0+s2agv4?44f zCN9(-=5xzq85#f)Q2Q&-_n1srkt+4F;W;Fi;Q@X@s%gl^TY5>CZ)07KFn0QJdQw1x zZBITHpoA*ZA`^U+B8sA);6xH9NT$Y9f1s32yG#}STAix#;ncw~YSNtVTsUh+)u8X0 z1U+@M5Gd-=FS~|Nb^_W=2+>RfhP@;5Y}Ol(+c~1~+OrFy>4B2cX3|yuY+r);HmE{%gZlWuQwVHlKq#mQ6MRAJ%2pk8Cf zm654icO)v$Lt-~_4FqlB8Xnyv3qTY}bthuOdo5pffn^7yqpJZ2iF<4~VvolRA z&>NOy(T#fj>3;)NB6ue>Tub|mgN1PHb#D69uSPO9F8y>Gld3WE-cAsl&U7S0Y+O}D=1A`Ep}?RN?AyZ-8;9{43K8Vb_PJdP3XOjLufE@DP+ z?NE3&`JL6z1?X4b+e?cEMTt@_Ln_pLLo*VDa-}U=i)Xx$(jzk}2E*t1ALqoovd(m- ztFE1&s8@bqN4%!I-;YJ0gTkkFEKbnNf3U&c_UQ3src@b6=SHdRkIPC>-}UYrtZKsS z!RqMp(*Y`G8>KVmmMGDwvX8HcSEnvKHdddBj;SpB>Jr8UO|;4p9Myi|6Acb-(ZJag0AwU8QkQdV6xRIyybsO;1`8 zr7>nov7Zuh{g7MWmK5H<;h`6Vf|0tJ?${X4T^>&04LW~JaGrYpU=m1Xwpn{a)Qq_F zV6|uVv_Q2m^jjNMO)7y5MqF`A0^MgJRb*(Cf|+X0vnT6QhxXGvR7&dLP$A`%QPGO% zspw3=1sF&fqF@h;PTZZWewub9T26J_3-pRBAq;k>)l+~C`G|(aMKb ze1=_rJ(;^H{QaSL@Bt{EF_j60qdw%-0h+r~86SuD%CT2ZDN9t9w-0ntmHyvGan72i z81o#ZH^!?&2mei-m;Melq3eo-__wB|fwQGM`UR@0x~GFw%&c{=bE2rZ^hf>~jjp{X zY_V?4Wm1{TQk?^bB&*_>Q!eV`N!LXJGa{vNz4JR8=HTn3-B5J0jSjtTNG5Mw`O~NF z`d{NyInOO%1{C&UmrqMzdfI-?gz)O|cLk`me%}UApK84At{4ta6bN{VVI7oz*-!DV zRR6G>4v)?hVXbVo4&bn~ls8qyoih`wt50Agca*Z9jHFV&>^%C91l6$gFBj$e(>tI7 zAdI+ywtXS|eFlZ1H6V_9v$hXwCKA&FBG*sfO9hQdC9V zSn8%6MWF_68#zB2JpKpW)$X&OhCwGw`QK!ZZJc$Q&6L&yqYtx|C972zV8mUYZx3_f zHfgio@m?Y;JlO?y^T}Z<_J=F{RoJrA{=NER1pD3#nD%h1)9L!7-`cros8(gRNWtKop!)Crz)klY*GmgSk!f>cNN=8*(7S*+hn9jS zI=66JUJ;VwhQgw#e6V$}_`nf3;J37jhaXALV%kFs)p1<(}W7ubzOHh8n znF*?9Z=H*p)cCfG+I-`{FxZZziBbG^zeF|e)Ab(erhZj|$Rr~meBt$f#OwBZlCYcS zKU>d<%%3L*s1rWl2I(PnBQWdznP`eVJ~odn!nS?4G%ECBoQaXFP6Bpc*Jge;dsy17pzsyF^|D`;Pw?PA)w{&WKR zeSR;wh(0$lOpESSkh>)!SWD8+k57_0P+Ge*cdEv$l%Fc^?X&o zsW$zM;Xaygq7+8q(`j3=3#3o}yZxE1ChRltXA;F56ohKV$8{n4*51T{!2bh{n$in?|64{Xus+@Gg^oJgpaent@Ug9 z3E=|Uv-R|W116i1lR#!j``w}X!FEo)>E|BmrCBW|t*Sb;!8I_*6ETJJiXU zJECp*JsBK0wq&U$ZnAj?M!Tl-ijM#4P&H`#)WEWTOSb$+;Us_ zcFEO^lvsYwxYp(d7mPfdspJgz-9_M!9{Wv|tMV(*Lpj+!4fu8w=AYb~KXuU{7*M}d zs*Og1`Qa|f7rxY07{Og-slxmQ?(HN&axz}Pv6Dn zZyS}SmCjEY)N4|#GfV-062gX^>r|AM7^Ww_>8-`+rz|-%@Jjm33v}f;0#Ww)C}xdm_uHi&s{GGZsJ8!n#9u!jeiIt8JvDskC#v-5f>aed zzsybfe!Iyx0Ctm&Tc}9Ezlr*t*^aeV)1H|Yp~^b03F1<^HZ4~Ro&(v>3(Hzpykyc^ zb=QidR43FtvPX-h&1X+E|0Zo1A*3_FtDSW+Dthu+lkAJ#l%f?F+AH7=#g-h{+`60A zv~Z--oZ``$q>_)zGp4DBe!4rz1X!lc*V3{-RX@=*Ky!0}36oQ0@BTZW@5R)pVxFiD z!WGrFTxdtB^g4<*7Uk#mK){vKK;s^rAk!;%w?0LYZJ8iU8)Y9#D13me;u+=X%r=~S0p#yZ zJEYr;r!+8_XKsuzb0@Q*difL%0h;^d7kt2~cUAXLrTqXFvv_UHYB zOh|C)Oh8>-2R#Zv--I~UkRWg8dV^fi_GFd*XOKUzHm0G~k0ZOGp_Elk;*$0~y0|{0 zrGUXuCAFtxhVc)RGR$OcW2@kCGeOEtA{kR5&BjyL9j|WNtn)<*ZLO=8>hDYo4HDmo9Q^#=ZEjIT5(B~jDQa$5;Xxhja zlbn!fUggF;i0HtgTY90eNM#ptr={SRyRR~;V5Gnuv_LmtpAycs1 zC-;?KFt}mWAHUrl_P=fd{DoMIo1iiv6x@$-6UMv$caJY&0V!}kH^Bp$H*v$JJO0;A zFf)|Zvl8-8<0k%p*aL|%I3n?1Hxd2c+rjya?we4a5#^6`VT!-n(x?h2?v4uNVeGl4 zr6i_gBA4R``rzPETAK5tor4Z9aH72B4q#ZgIV3kg*Yf(K|L{O5Vg` zoPJeTC$A3+x|ZK-TU;fr6vITJvDhOgPZWd!^PZ(mb!m@mxwJnn?#g6ikI$~3j}v!7 z(XZOAyT1}e?$wduuKB7XCHMNd8c^{}?*BVTZVS@_O+9P@&bY*|d14{D5adl_aH*5; zU#XgpjSfnY8|?zVr{3;xB%0sFz_uNz!WwZxS8@?m;!)JfM!D#H{z}%|g$IK!2*%P3WcaLeAjag&nL1-gve$y`JtIb{?AjSYVVAZ* zm-mE%s_gN{f^Rx}I5SfkF(!%X&2i?Bn6nC(3`dwWz0cOR9$aNcpz`|$->vx-NsOFB z9yA$5Rw!Yng{o8Tlag)xfU_s@_x6#q31Ku=)Rps~X+2VQi%)gtS*Gr^8(hX8S&#e5cFPF3MVMdN^miMy>Diuy7tJT4ZRfVuZ8Eyy%ruXzlO-_wLKW{!2@*e}*DiG>G{xIk;<-~6 z0{*bJc?P#H48{ldLr~&e2?>3awk#w@#b(~_rOwB#NKn&qM|kV*o-SG+8^a6T4jTdm z^MPCI=d%BhYvOVB+4EhA5Ss2tOj0#hU$E$F*LqN;`}49e52ae_y!F1k_MB|$xs$F9 z($)?0>GrFfS#6 z|B@Bbf;1S7&mYv~;SC;OAv<2bz#IvbD^K+C4$>#xmt*hw)MLp!)B4peea&Q;Nm7pcH8__tyM3b*<7xU z4y%e#9c}BqCdj2=yn;^ATstF?!|t9wu*1oZp>JRH+N${2uMnX<8f)>V8%%s=4uL^y3pt7M{rpLo-1pHDx@yB+7WLlD zTVe`@u)_>dy8}Zr5|pTLF+PTHSK`68rQ2zz^Pg^`Es|ULwEFAv$fWAN#nwIs$8tBj zvO086aD-a%LV&v(m>SV1oLunIF{ReJdNbL8pZB+DW|@(vb2mKerDiQz>1Ux!3>Aqg zPJ+9AOzN>{f3x>(<_{t(^A4#J+wFl;S0ZcWVXK#}3{~&d(Kl>Jvw8K#SEviXSjQ8hcE5^V6k2O?U6=@lCz^oFTn%rD+L#z(UZ5jlK%E+GiU z6q728$CNpvCD7;5}&sqAs+iMRky#-=W8MVGRB$S}}QfA?TOPPhuugJ0RE@ol# z>#8hpLqu=3LKTI%2aT3ACWjBrm^|_R(g*LEPOY5hh8|1ayRGLvVZq|9sipT%nf&0y z+e>HgLCS-2&_m8CFrQvgwef*?!emab05T)E6xn>?zIz^caN-@M4^EZ~b4G*%0*!75 zRtH`b1C#zcN69;FmYIEwkXFh@-63sAhHz*O=2mG4QPLE8VR;2vit#Gbc$LEho>A>Z za0r2eQ>E@yg$>-$x{dPcldX#tg6bq$%}>u4x?SwIgpMbM*&@`}FQ4}c<`dL;@Q#oN zGrlK;#9Rj4v%C=1@M^rb{`0OVN-V|yNU$j1)e(#{y&M_hV=Q7K5-U7X8`r6_m}8-a z4Ka#?D&zALL2Ba6rr?0CX`(!nd~+!>s8+NFOi+c_^>*n^DNI$(g^tnpL|%4po?n7* zs6LN0dnn%>WzlNVPu^Z|MCL`0mNN!~$V*x)m&0EyH44KMT|r<^iBRHuiL!!yt=cyT_Q&r#jZS&Mw5$*A zZ7DU0GtY^2YFo0Jp*U5$JuqSp@UGp@%mmJ*Le#pTUbexgx&d!sFNf7TWQ3qfDplZ|+U&#WD3QhvL(K7W8yc4R1Z-uAf^v zoDrNqBan&>KRgxzXqLA{9X){6-gMPjFFj&zf#%(7@p{@iC&JbFv|b}sUHk)<2$sv3 z=v|CNm}1f;Kuh%6+XfMop8RxLAN|&=hH92%^R8tAmE|@mwGBhwz1p%VE1y!a3D<_J zQxmOT>eT$*F`n>&Lm*dK#b5QtfD|0)@ug3Bh6!h{N&W0(_cO$Ri>ux-3300a!NdVW zjTl38&?H%w=yH?ZJv_$mn(r{UEQu!t>hcPc#=CXfZsN_=^_l&vKlSKC8#`S;^#@># zy(%24<5%zC5Uhm^?V&hMQeO(_q?hVH_XgQUm=B7jLFHF1B^QF>w`=ow{0dVB7I*z& znX#+#2h*1DMG8&Lne&FJ-FYr<7D+1^PilF!jOZ>V(P9zSKFGVr{pJ$^+)||Gt$$rn zp!ul@zx(=`FeAK3iAdIeFG$zi)7@qa}=VJxxgKzV^)Ow3+-(UsB}(!+FesZ z)qdaieN@#d?;sz{gqa|neSUfS?h27g{q2AGYJGep>s-6oR3)Zxl<%h=p`kp9Xq2Ha zm)M^RkDmz%l48oXdh$7LuA5X^6;JR8Qb*@ES>~9=&{-=tZj^--NzUjJSJMcIq-J5!^X&1G;x}YBpRucA!8JPWOXVJ5$ZS{)-o0rqhT(1+s*?}xD)iA4>2sS z*(EiL^JbewRm=)XR2`O-G_|EHAyAb>Cq}6AiMc^Brh74d+Ze?)yD;eD*{$ov>C~qO z1Z9+OXZqD-0_MI=zl=lsf8$R;z7qgu0wRLA+kil7lbdn|6wTf~z1i6&xrF*@+T7?~|h|rI; zTNy-fL&8kx@07tU^a(Ky>sEh@CJ=FFZN3VmwELsKKwA%FjZ-mOQLtBOCpKqGfb zSMja>!D`EBCP}WESLLP#JUrD`fVC#2JdmTBmJfl%Y1+NZFVdF!xlcUlQ?Ij5XAiGW z$>OLsLszvQ2bBu>w!f-8cLF-guD_d2N&URFuNJEBUr<0rV@|*>tG?LFP0PC_v%hm> zyPNh?hF!}Kn5>i8{RJG&75Q!TN;nS}!!)pSw zaW+T|x+%`8=KBMrwLw))ybz?)D?WBp|Nd7?A2QwV+*QFlclfH}|3n4or#wNs)a$Z_ zG49ts=2(QT?+w6kbu4(oqGr}w@M$6bzKo*0=c*^H_QH(}ekjyU#Z){CG0ykF{;J`f z(KHSF=LYp|n}0-A_1PS!{x#syAPY)H_@iQ8xG^|c4!#|%M}$nqNoU7z+cWac$~yDp zA*qq7J^R;)(GYr@AHXv*9>H@{N2EvM$Vnt~r6kGVvzfk_ACtAZ`l=#{0BZJe6_dk~ zuc<4a4GoRtozsWttI(y|xR^5plHV@fDA%S>-FQVZFc8N^yXu9jA(B>auiV{NkNG@H z^Aze~IY&}b+!>+2{im-M8E&%Unm=mZ6~pIpPkoic^|nwI6SCRWfjF*`Zch0#7$2I% zr}wX-DJI%;cys&91W?a2&|x-W%=^(Dvof`DYSIl`+=olS)Ywgv8=WCGc9!q54AOEn zI8qJK=&H+_w~=d4G{2CY!+7lLGnwjrW692P=Bg2zoR8!NJ zK7R6xL`CM;@ysN2)7AvLngvn6F@XX4#7H;Ist$DId3uT6TvC4K;A#ppx-B!pDrHp_B}hPo-4of+BoGqL3dFT zZN9Gh;D!JuDZ@WphfJz5Ol>as*w)|RBJN&o$_(bJ$sG$HHS3-&Q!=vSJC~4meiE0> zI<>#NPyV~|5KGg3WDBnCb2(06>;n5~uDP7n*m%S=qUx{rM+Y0-+Twj1gI!Uv!&R#A zoO`5=>LIyF`<|!RM)ORQY{lYjZPh!?`jGM)5fL2q489*Oc@BTou;qNjXsT_HQ?2WF zE5CYo)FbU44qOLqcG=R~PfMx(b*#Ut8GEPeoD@@WQwQ8U4Jf7@gUXrn3?@f5aN5Qj z6wJ*((8MzS!P${+u5q(~ZS3j_vnFne1o0#xyj+x@`0ollZz4x(M8ECOhBdzE0f^p=yCX+*{;x?2PSRl# zh4YVs?)!G?;vPvP+A8*9*vc)KXZ}|EG#(+YJMLF}pXDzN|T4 z-~fD4``R%5(GTFp_Q(}U=3T}{e?1YPk8KRl67+K=l>9_BCOC@cHIxG>G+E`HHNZm)rTlTr$9lA?`Ql5LNC}!vbuyKJsj0J1 zZFVm8sb_BqR2lnjWV~w8Dv6l=o}Um&#?5N|`pLS$s4my!^gH@!?I+I`zuf2jJh`0z3`t!6&8et4+8w!Ge1`vvjZ0UfU6%W$F6DOyHJGxmNWV zZ9`OX(Q%vl>pv~ve12Tvs$b{bm;7OC!L|Mu+1;dz?XHoN;Q7TM?b=}Op)*YXN6z?Y zwd$q$Uh1pHjW!iqUF9Pi7NWL}eKbPP_$`85=liBI59PIWmX~_${In3}DGLj(C0eK$ z;I1OCEbwE1+!hQS&L1yN@UXO3&&O*6xHw>}eotGLHoyl$aO$i0HnU@;*DuK={Vn|c z6KkY7w`1~9OUm3G3Z2=wOE0~eBqU6L!Dva=5zDdTm^?~z1`oB~6gG-c;>so@%>LVK zFIA|Vv;gkWB=3U~1%BP)@ zU_=h`XPy8zZ2rv*G|cvk@i!x2b8@5`qaLRAK`~x)d~Zk^g3!9~Ws&HvDlqHGN=m=M zIHAv<NP%~Ei5u{sI&B6x@LJ@$$LW`iZ z=e`a0WU3oOlUv`5-uC$KjX1P|e}L1-kakbZdQZ^G3d|N2eJlcI%1>8KF97eC<3*em z;7NX*86^On3^ruuu5Ec}fPmovlLY-J6D|`SBh`#!!$!Dwy;=R(!>Qdb^s0yB1Pjby z9&=QG_9_sfY!@d2m{9k-=a(VEpa?dRsu^}qE@*!Sz`jXpr=L=h*r>XnPES=G|9W?v z%4$eSQZq)`17M-yDRNkJz^QI_z_8Wxy6k30xF_IAU}u3h$ZS>Tqay^Zn1c!Yiaqfo zV6}4u*a0uG1(y)^S-cxE6k0NYBH zxh{vF6)2z_$iEyfJsS)kkPsB}NVTV~iv!t?(ldLZH?>2u0m>8uIvjxj>g{`W6o@Ji zTJpi;X&45ZiBPNvGN7@)z$bRXgxD zZ0gkF5cw$Zl88-#s$2JFxI5GCoVGz27%qdDfT}6jj(JMRx#hE6uMKwYNr`iS3Faoi z&Ed$-Bj0nn{Hp<0IYl5RWu{+`C~$m=U*meU8`dPM;@A4Z%^`rZq<VcPTpP-j}2WrLYA0E>qRn5^GBZYv7?Xm-Is+V7vA|~LX zcWQYD6CfCq;1M#Hqe|Mw!P*WYHuz)(z;8vtK?1U<;1IF296fpH_8@S;K=A^Mj1d0e z!y#bu%4ULgMrGXbyB_S0(#YjF(sl1~h#-Jk1=0*k1-5iT|6r@&`Urj#AFsVH7uHhH zazLUI(5kY{RrxM*qC{!A=?MV9Vs^km730Xq0ZgzN0XtVj<5b41{Bfr$Pf z0QhsjgQy7sp*M0fbPzyp!9T5<_d{sQb_*OifMSjpGqgw``+#2pWEc)h3wX((;i~fr zyTIX`T&$bkc#A^|2RV$BUmT0GJ@Z5m5S{{AD4Yq}+&C@9z3a8|+s&TW_`T6baQHx1 zLUS4&6Qh7Q1kDyh(zzx^cz0Q^EK;Y^6)Fh|lG|ZZ;|vj`blDh*E)f|C`-Jmd<#_9f zAi@3lQrVVdLP_5Be(76JOZS=nVs*9FTKSpd_nF zUku3sF%v_^X&J}S`BA#wma1K?8oo-3z>(K_WMAf%MI~(l;K8K_!n8=;cL&ESJOta0 zQ&1ecsoMTBB%nR7%cf+8d&3R`s1nZyyEiaOy(R}+fo#QUpq~hdds-|)f(?RjAz!!M z=FozXSFoShoJk-5B)o|Nhbkbtbpz9JDPU4_xCmmn`k$5oa0>#wz^5sP-ytjz!fT)- zs=_6QIng4JVmKeel5nzzBUP+&$?+q>;FStS7pGQce4eC58-j=V!U*B2ayLUB1}GzBAxAusQO@=ZsN}0a!Xh3yYSm5eMgq$W;%rXVmPW7!SUqrAsTy~vZ_z>A85V~# zKQmG^@Qc6@13ery&(}*17ia-sh9NQ_zp9qz`LRf7*Gt>HOp0t{clCh)25MU|sLj{T z!Iq3mv0BgvmIK834N>}}zX~)L3qBDbFI9iTF5340Sr*21A9;_%^T&#`iP$jwE`=+je_JW z8{*e8A0C;m*UFJnFR0=YA^^ULBEFpk;|2PRmqx<%`g`0po12`*ML$&OoFsM=!waDb zb=abSPeXxxF66 zh@UF%ye=LXGUPxGG1^|43MU|lidZEEVV_?b?a8raq0K1}WZfTIt2 zq3-ZsaP!7ayFq3G?kl7g6>(Z5DlSS0Zr8-(a0`J65~4~ImxREqOpHNnlcx$#l!YhZ z0kMckeR{sh?z4z={<*fjPt!4t<3uy4%>{Un&f6MeAa-!R)LS+eXbxTr)e0w4^}1Kl zEg+)}z5?Yqe+sS>xav5UZ4j1)3Eg;n_gi|s{}Amm2_8oFyjORB3Yd}I-zqzDCPuHD zKUB-YAR%}trfO&zJqUA!e`C-xtNQsNF{)-pW+(@XIcuAvd}qBMMQfR@8=9y^t9kc3 zu2mg%KVGg1JN|H4J3p;Js6a8_2tDAiU;HE-q}{J;zW6EJO90^nhLip#CS>#AAh5V; zFcY1K131kQOk5XUT{Z7GB`C^747dPznO28#Ee--ofG1V@8zJT)`r>N5@?0*ILIiLE zln5hvh!xH%xTk?=&sDbf7X^WON zYQEbZ?8d2KcDz6@Nf^R8Hz4}t^7W<6Ecp(o41tx1w=i6K*^j-n2xb;gUTl%Fl^_s! z39zV@wtg^DEr5MM0CPNe5UMXkp!hJV0goOFM9GA*6)UUC-aKOib>DoYcP6@Nnf#T% z;Hdzl99YTzqf7*6O1S3T`U|2t=AtC)jlnPl$iD^av?%AK5j2d1E!}ueRqPuBIeFKs z`h8>2ban_)!)&&k`vV$lWE?n00E5qE!9`wppQi@MG{0oVi(8h(kqYm)_|H@<)8#f2LPmn|Pn<0(nC5;lUJ6|i?;_@)eowy-P1ya^ocOewQ zQ=(iso(Uaa_23vcSrZUTRkOs3f=-Afz#%k$zA?nppkoolSUCXx^vl8Sgao)-Ia&So z5VoF`$4!GwI`-WVR**#^64NW92Xdx@yWkvN$HA-uIBk)oe7$Kv5CqpmNa!T_{XJW z@>&QO5L|+dlkWzqrfYJoZu}M-him=o4m>wD2GDAlJp8H$_PlC1G1^f55j}5J&3qxX z=T*_l#wfWw`^ygyj}N+MAg=}6T!_9@;~&d%yMIZ7#NPdh#_MmgNA~JZ2J?c-=?VhM zu1L8;byO4&vJ(4X$6)};RrVst883QWQj{JJU76s^vS+F)Xiun>iFSaxIS8GjH$_K4 zR!2AnVtTG>_+SW@9#$ye?4fGnnBkclgCLFrt(wFl2QSw6@7K_MLIOE#tvA&(&&d&3 zwvULd^qLIxQh*{jdd@({EQmSyWkM*`x$E0JH%_V&i^8crb(9w8ifw|sD>gd_HVFbi zqzn_@a`dSvIcSee!Gfn6JFf%efLq}*dUVggutvOz9Ip6oam1(8_y8%&%J z*Rn49fNH+=0EgW<5sZy2UwUe=)%Md_3X;DG!w@IY#JdL;?qj(&K_r6a;+5D^J$n9rRwaxD8 z)YDtm_Ml2;>dpB>w6XA!fw>_V8&$iG0kSXqPN;=$RV{iv0`LGq_0ZNGPlS~O6Ny&` zk)T58!%S9OqCvgPC7f~s)Z|QhM5!24 z!V<&~lUf@!HbRYi>j8K5?(Oe*a|G}mFYcAZN0hIkydHL;^+~=^DoK!t1!xJ{DRD4j z$U#}y2CR7o;WxLAWdTE1PiMBS`~^tKfSJZFXX^GeNNW*ufhH$3rXW&r-c<|6Z<%jE zBMpJ^jMeoXIBWAes`R$WNibM;zxH_<-cdqpGy!CHD39&IaTIO1DDh0Lxgu*9wm-i_ z0uTHQki>$3Q90}z0oNw(66#ssYukxEiEr89td%2s2AT#1}SI4HCiifx|6 zUL^D6OsHt7>xp15mKaloo>I+GRsIiA-r=G+Y!LJbSZlW7AyEOgAyaj(bqFmQI}Z{$ zVOi&k_=B#O`PJVZ6Ze5<<;-OPo!<3v3O> zJNf5WPXZP5CAOmZrpamE$nMuDBN!XPiBh@zL#Y&Sx4A@ls$yY!1ozINvg~qr@A+azscI`;y!fibQ2Wuc&gsCk}Q1ISG`mVB4f@2A;6wHbPOX47nXY zU(r4Z3N=A5AStn{rmYXdXaN-nQ3`^I?VSbs^F_0psSEi{pUlf7rj z(+f-<-X3j3EeIh{OPuv%e+sKAwu9%dIC{)T^{7=4gexs`GD5;n5Zz$HOT+fZcp-|| zL9Qy|+T1nxY(AY|h=_*vOz-bR`NY-~Jfq4UC13`@P zdpaffk~bW@A%>OhBRrzyu|k-Lz=Oc7@bZlp*e#w?s-xMuW38J8yQd*l+#mW^e>_g> z2VgnG28gbzcwv99e2*P@)mIJBns-(_f-xn_TPY_$ieIfhHC>TnsslN zUN$35yPmLlt{1mJo=fm0IZ1Q;>oNQ~5Z6KnRQt$kt~?~oDk>wKM2>e(rLe>jo+0>+ zYceQG!>LYcN?9~l9}0hTsFeeVY5M|H# zu}{458n0~f)^;l9W-M>CC$-&5d{y`-Uvt@H{`fHN}ccXMG8|B!xrEcxGZJ z>QurX;&|xqo)Q<$#!{oOeU%C8B#zR9(2nDGa<85H9}cWuJjj#6E*gR}(92&OMkGK! z$hwjixBSU;i9s&SHw^EA>NP4zHmSRiA^@pUxn>)i*Y&D?%{KVXdOqB|bPsyl^SbHS z7vWy`X=2sv99+W*gZ?(2avjSr5?EleyLaoZFue$cW~SjOAv{GzH>!k;KbS4dg(!X3jP0PA|G>a~`BRx06yrr1@uFrFJlmT~NMPgfL^V7hGE z@hDFW72ib?VXP^ec_pd~SCKSPh;B<>DaQ>_bP>iP)pYLbSS#gUO0uvQA$RAVq8x@@ zAcNJ5qI(lZ%$R-DQ&{z|T|mwam>cUwT}DXsSVq3l*qI4ssgz|2Qk{%sqF$G1u%H#u zV?vx9n{eb>cGIl~3LNT#*KB9z@a@NjO)KDaabbe->hH z!g=aU_@VKwu+yU-wmrdbi3Octr}!Sc8`ew}!R%s4Oq(lAy! z7S!l6b5&E}rUX^F^bISOC+IoJir(5nRxf9C$sys!&H0%iR?=~pfDlN@#os6;sgQ&q9URs>Teq_hkg(<%SLCh65{=U?;@tdfaa+3 z8|Q?(6Fm~|ao3gkSA+_GFY6`@*Cu^$w*pbxM%{=hRrBF;LOE$&;QA`tF$XiYkN0$= zicj$uI-C0;v7j5mU9kdsX-R|@<4Ms)1c3F!y1YX#L!xeWh!DVizZ8YoU}4i51J%i!Oj};UR^4^L6Q6(4SM@HF#&URn-9XfRBHOOFW5qN>7*i!rL$WU3XOFeb2jCaN3eBR=qO{PzC`wQ5aq)xqmuM31O zQt;4QUXLSR;<3%ws%G|TdL9(VX+<%Fp-TT)3D1JKUGWg5-@Q!8=zNuK{~LES?pT5u zLACV#VQRdoGlWX4z4H6!6pyidgD1SpI4DS8iIc6HAe9z@GE8R2KDs%QL7a@k^Yc7x zeBCcYlF$@dN<@dO{D=24F_WKfg@uo1mbi7U)4a8R5S+-wo@h_qM-K$6@*8qIprE6d zOTJ1!>*L!4lyB}PFZ!f}FOUOK0~g*+X*J6ms&i{DXj?n_U;M4oZMlKUcWF)_TvO(S z^oC?_qv0eDlQd)9lF&G`R7gTdXG{a~zf}=nP-@HcJc@RuL%A_2Kg|n)Ve(Y0MawmF zW9Tho%sCfw{;q+V%?mRlEnBpYxRZJeble2JE=C2DOM%@_(Qcwy*v+?K%G9_H;n(e(AO+#Jq(RtZ=@tl0>XCzBT zLst-ag?Cd*hQ=Fe=;5>J66({)62Da+vK8P;p*qY|GbU9>S;;$~^RonD~(h4iLFM({+am7lIxeN6k#jc|r8M1h*V zA>C+jN@H25WXm5+52crdJE2ZNT;lM%9(cw}yq8#0;bqC!YboqRSt%eBcF+UD^7zeW z@Bfz535yH%kbJRoSV~t;N5X;+$h51f+2y7h(q=^q@q%oe--ownSP8I5GKsKAB~$O0 zAhc(zYAd&0Q@!ntalLS0RVRlEITOTP;%}(-Qicgor@p&4$ep@01$I1T^XiF`12|u+ zzaCr-Z72Z|w@#y|d8-5MqpBe$H$C(34l9QYi|80-E80>l%xmPvM8=Jo{7m`mdpuDQ z9F83<#dv_Uh(@U3;5e};r$2|M7Tost~%FLlCH5#^gmQd(3H@4R30l&)jK`UjDi2RmWUQm zq@pP0w?(5ihlWs+LHSh4keV0>`|v-c#UhmT_g@|ZO)a$}&cT7{x5yJ&Bbecl={Ug} zckW*Lp;&K;fHJf7liA5IT2ke}x??grb6R1QMhP$wxSNiDoeJ%FsW*7#aW!Gx!Son3 zURPZeN#Pz%r*ffe)4vgYA~!)1ApLz5N?j5PB$Gg`xo}fIB5d3fHO?*526DnJ_Ty(g zU2^EZtEx4HQL3(ElB+Amns%B-!Gj!>RYmt`>wmh1=-McUkVZASEXF@c-4ozYRdIbg z{)xI7*%w}<#vY=VkL%5pF8hoEXit2Ef=FcvLr&08a`{etl*l{E#xK{NUm0h>^pTjJ zv`r4PVqkF#LKUXhuZ$3?T(pU|q`sE}k@}_Tp-&nvRD@V=uGR9<7k1)1Qf1;D#J(rK zgJuq+CF*kPWb!lNVWO%=LQ~W6#XxKK?&Ro2bT+QhcaBTY76{W1aUx}kEQ3j13>m+} zR?`=RK0SnKBi_I-oHE{&PIhuWeF>zMpTZtCEe)r|fN)}?jN)Wm*Q@`kYDlb)42Y`D zKM4fx(~X`0jFRmc48qY?Kwo(*sVeBTvnW85f3c((Tg-61|5gbtdUQnUPox4kX)=-| zZb4cu@UAvLtugx7^zk__4{SI$CPu#T*;hh1_(CCv8t2@Pf~xOKx8kA*uc@u$S&P39 zP)!dm^b|T%@&wedbbC@j^}pA+Ui8kMEx13w{ENP*H}|({$roM7!5Af#N#aK=G0f`r zf0yjm@O)*beW4bM5gchrJl>5=I!kAJh z-gmnVEupJNUX_)vSFJ(`J*7HD^d~7MNVe(3-El zO(7&j&#C@3UzLA-LPeHJO3SKvMj^8a6v(iHq-{l?C*kd+ z6HIRp)gCon>L$*YtbF|^TWG%1Ba(8^<5ChSB4{Dw@1&Wq0@XN4oLM<2N-JQRN!^#}o~6pM*YR9Sd7=@wzF z{3fS&nQwZ@9)zdBwo#_dWA*~#c2_jHTN{xg0wb|Sb^V?eWyI@=wNu4!uK2q$q9H^i z1Wt&t?UowCMard=7I{SFxA#~i1qq^hDO2f%6+s$`C^C$)zuAh&7vl{~~8w_eQiAu`luhODqx# zyLkhTgaqQO*o4wWSArZ(RDioN8v?}(GW;1_A)28=_ z(GFF-_QPoU95@Xy=pVW-3WH`ednrGw=Jqim(4paqRbx_G1ixi;`N%(uRugHlepI>5v4l=es%!76Dh4i7>@tmKVX z+Afl8rV#J}0#|R~>E2U7h(~IHqrxPP{h96}`$QN^pz?TxM%W>OOS@4z4BuB}$draO z4A_97Qx69LH*wQ|i{3jmW8t8S7rbKovr#gQ)uL*@?;N0fKiDulS^H4u$m86?Q z=RHj{3Vg5G@3695OuI;@N3X3WO7LLy&=%4XXrfmYFd`{cZqsrd89L7xYV0m{8_gR#(yY`_CC@_9Rj129P($GBq?j+wKoY~qy#r}6Qw-0~QALh2JHs~2(TGhb zTYf*R=R}1PKC)Q0tMh>qON`Qdn6xHr!OTzqEBbX*EJ3UJ8N7tewlaf7Gykq@vn|>L z)p7lS%M`~sExONgPf(O|8d9$IqT$6=v5473A|>CZP#B}?0_6Z5y|R`+^Cc6-@Q_CQ zo{>gl$r$j#HJI$Io&tDJ%?)ElHz#XaF1p4u)2cXDA1`Scdq!@d9N%7^OVfy;jIk`z z^wvIFDg_uGU94-CLBdQ|i9iX1jIC~YX<;hFl3ZA7D2x^KzkzvdEU2j=!-E2 zKn8)!-gp~fab!gw33VuKVJ)kEeTA&$?4L|4pqLUnv`l)59hc%OdgZw7gA8_@oQ=$h zR|oB$-5btkYT>e(T|G3v2`8_0PTcO_SuB!H3o@ym{k|q&Ic?KIg zHXNa+W=|YZo4RgH2N7mx(d>1!DZcYXekq$KxTwQ%NbdKs6kCemu;X z#8BKKB$kgPYYARNM$*3`I;2Kd|Erre%$3Rn4WGKB$6@8fY~eFxq5{hGfa)RB;!q1x zbVf8)$GP+*bQTMRwh^HwewOS>dM3doi6JJ^GBepzW?l(liQTyW&r%Y|ys2G?;mXx3 zes|M~l<(`Sg_%5yS%fb_Sy3Wxv>NYD3Vr;~6v~cR8lpL+jW|KT3$3S7e3k?SIe6vy zfeek{QW#LqR3*Px+8Gd#;dct%(t(=S>8FT_C4wDoj&N>u+;mmPtCPczDkePW`^_NA;0L0NM}bL(`xUT3-(w0nwJk ztYzCK3(Uo0m)`VrROj~RZ?G7{*(5k0x{;>X-=5A%YT=V5CYdpD)p(KznVrD0sIu|< z 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 delta 1133 zcmWm8XMoQI9Dwor{oS2&!`*S_8E2idAA38_xI}wsry<%ywCfU2p+wP9sjIYAbk#M{ zE~zwWD=jLGYwxcYpEu8|r)t}V?5diT*^7%a%F@z`W-TcxPs-L#U6r4;F{e6hcd5-S zSen!omZVlEwUf6`%Brm{o>7#P&Os7MNdrlj49S#+k|m8KTXG~<@}#lkOMw(h6KN_% z(oC953u!5>q_q@F8)+-;q`j0#2k9uCq_cF9Qt2vX(oMQc59ukrq_^~u_~|SCq`wT1 zfig%2%MckV!(_OOkdZP<%4M{SkqQ|r<7B){kV@G@Cd!^NNhZr)vbXFb`^tW@zZ@V3 z%0Y6lOp!z6P&rHvmm}mzIZBR}sd9`QE62(4GEGj9=`ur3l#}FSIYmyD)8uqHL(Y`5 zWTu=gvt+iMBj?I_a=u(37s^F)v0Nfma;eOb%Ve(16XkNbLgvc?StwV^RkBE~mc_C} zu90hHsVtN0q*|8C3b|fxkQ-&C+$1+kjoc!)%5AbrZkIdcPPt3&mV4x0SuOX;{qlgU zk+t%m)XGEhuskAl@~Au}kINJCq&y|-WW8*Vr{x)WR-TjRqmmlOu`AO>KXZb~bmEYud`9uDczvOTENB))n7LY6=38~N^q=$@<85)MH&?sbw zoRAyxLgSDh3PNFM5}Jmh&@40$EkeuCDzpy8p-pHT+J*L^ByX dIx3U&g3KgUpO&hxE3C=gky>A0QJLiB{SVPNjfwyO diff --git a/test_smart_monitor_data.py b/test_smart_monitor_data.py new file mode 100644 index 0000000..315c850 --- /dev/null +++ b/test_smart_monitor_data.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +智能盯盘 - 数据获取功能测试脚本 + +用于验证实时行情数据获取是否正常工作 +""" + +import logging +import sys + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) + +def test_data_fetcher(): + """测试数据获取功能""" + print("\n" + "="*70) + print("智能盯盘 - 数据获取功能测试") + print("="*70 + "\n") + + try: + from smart_monitor_data import SmartMonitorDataFetcher + + # 创建数据获取器 + fetcher = SmartMonitorDataFetcher() + print("✅ 数据获取器初始化成功\n") + + # 测试股票列表(可以自行修改) + test_stocks = [ + ('600519', '贵州茅台'), + ('000001', '平安银行'), + ('002167', '东方锆业') + ] + + for stock_code, stock_name in test_stocks: + print(f"\n{'─'*70}") + print(f"测试股票: {stock_code} ({stock_name})") + print(f"{'─'*70}") + + # 获取实时行情 + quote = fetcher.get_realtime_quote(stock_code) + + if quote: + print(f"\n✅ 数据获取成功:") + print(f" 📌 股票代码: {quote['code']}") + print(f" 📌 股票名称: {quote['name']}") + print(f" 💰 当前价格: ¥{quote['current_price']:.2f}") + print(f" 📊 涨跌幅: {quote['change_pct']:+.2f}%") + print(f" 💵 涨跌额: ¥{quote['change_amount']:+.2f}") + print(f" 📦 成交量: {quote['volume']:.0f}手") + print(f" 💸 成交额: ¥{quote['amount']/10000:.2f}万") + print(f" 📈 最高: ¥{quote['high']:.2f}") + print(f" 📉 最低: ¥{quote['low']:.2f}") + print(f" 🔓 今开: ¥{quote['open']:.2f}") + print(f" 🔒 昨收: ¥{quote['pre_close']:.2f}") + print(f" 🔄 换手率: {quote['turnover_rate']:.2f}%") + print(f" ⏰ 更新时间: {quote['update_time']}") + print(f" 🌐 数据源: {quote['data_source']}") + + # 验证数据是否有效(不全为0) + if quote['current_price'] > 0: + print(f"\n ✅ 数据有效性检查: 通过") + else: + print(f"\n ⚠️ 数据有效性检查: 价格为0,可能是非交易时间") + else: + print(f"\n❌ 获取 {stock_code} 的数据失败") + return False + + print("\n" + "="*70) + print("✅ 所有测试通过!数据获取功能正常工作") + print("="*70 + "\n") + return True + + except ImportError as e: + print(f"❌ 导入模块失败: {e}") + print("请确保 smart_monitor_data.py 文件存在") + return False + except Exception as e: + print(f"❌ 测试过程中出现错误: {e}") + import traceback + traceback.print_exc() + return False + + +if __name__ == '__main__': + print("\n💡 提示:") + print(" - 此脚本用于测试智能盯盘的数据获取功能") + print(" - 需要网络连接以访问AKShare API") + print(" - 如果所有数据都正常显示,说明修复成功") + print(" - 如果仍然显示0,请检查网络或查看日志\n") + + success = test_data_fetcher() + + if success: + print("🎉 恭喜!数据获取功能测试通过,可以正常使用智能盯盘了!\n") + sys.exit(0) + else: + print("⚠️ 测试失败,请查看上方错误信息并联系技术支持\n") + sys.exit(1) + diff --git a/test_tushare_monitoring.py b/test_tushare_monitoring.py new file mode 100644 index 0000000..33753c6 --- /dev/null +++ b/test_tushare_monitoring.py @@ -0,0 +1,247 @@ +""" +测试Tushare数据源是否能满足AI盯盘监控要求 +测试内容: +1. 实时行情数据获取 +2. 技术指标计算 +3. K线图数据获取 +""" + +import logging +import os +from dotenv import load_dotenv + +# 设置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) + +# 加载环境变量 +load_dotenv() + +def test_tushare_token(): + """测试Tushare Token是否配置""" + token = os.getenv('TUSHARE_TOKEN', '') + if token: + print(f"✅ Tushare Token已配置: {token[:10]}...") + return True + else: + print("❌ Tushare Token未配置") + return False + + +def test_realtime_quote(stock_code='000063'): + """测试实时行情获取(Tushare降级)""" + print(f"\n{'='*60}") + print(f"测试1: 实时行情数据 - {stock_code}") + print(f"{'='*60}") + + from smart_monitor_data import SmartMonitorDataFetcher + + fetcher = SmartMonitorDataFetcher() + + # 强制使用Tushare(模拟AKShare失败) + print("正在通过Tushare获取实时行情...") + quote = fetcher._get_realtime_quote_from_tushare(stock_code) + + if quote: + print("✅ 实时行情获取成功!") + print(f" 股票名称: {quote.get('stock_name', 'N/A')}") + print(f" 当前价格: ¥{quote.get('current_price', 0):.2f}") + print(f" 涨跌幅: {quote.get('change_pct', 0):+.2f}%") + print(f" 成交量: {quote.get('volume', 0):,}手") + print(f" 换手率: {quote.get('turnover_rate', 0):.2f}%") + print(f" 数据来源: {quote.get('data_source', 'N/A')}") + return True + else: + print("❌ 实时行情获取失败") + return False + + +def test_technical_indicators(stock_code='000063'): + """测试技术指标计算(Tushare降级)""" + print(f"\n{'='*60}") + print(f"测试2: 技术指标计算 - {stock_code}") + print(f"{'='*60}") + + from smart_monitor_data import SmartMonitorDataFetcher + + fetcher = SmartMonitorDataFetcher() + + # 强制使用Tushare + print("正在通过Tushare获取历史数据并计算技术指标...") + indicators = fetcher._get_technical_indicators_from_tushare(stock_code) + + if indicators: + print("✅ 技术指标计算成功!") + print(f"\n均线系统:") + print(f" MA5: {indicators.get('ma5', 0):.2f}") + print(f" MA20: {indicators.get('ma20', 0):.2f}") + print(f" MA60: {indicators.get('ma60', 0):.2f}") + print(f" 趋势: {indicators.get('trend', 'N/A')}") + + print(f"\nMACD指标:") + print(f" DIF: {indicators.get('macd_dif', 0):.4f}") + print(f" DEA: {indicators.get('macd_dea', 0):.4f}") + print(f" MACD: {indicators.get('macd', 0):.4f}") + + print(f"\nRSI指标:") + print(f" RSI6: {indicators.get('rsi6', 0):.2f}") + print(f" RSI12: {indicators.get('rsi12', 0):.2f}") + print(f" RSI24: {indicators.get('rsi24', 0):.2f}") + + print(f"\nKDJ指标:") + print(f" K: {indicators.get('kdj_k', 0):.2f}") + print(f" D: {indicators.get('kdj_d', 0):.2f}") + print(f" J: {indicators.get('kdj_j', 0):.2f}") + + print(f"\n布林带:") + print(f" 上轨: {indicators.get('boll_upper', 0):.2f}") + print(f" 中轨: {indicators.get('boll_mid', 0):.2f}") + print(f" 下轨: {indicators.get('boll_lower', 0):.2f}") + print(f" 位置: {indicators.get('boll_position', 'N/A')}") + + return True + else: + print("❌ 技术指标计算失败") + return False + + +def test_kline_data(stock_code='000063'): + """测试K线图数据获取(Tushare降级)""" + print(f"\n{'='*60}") + print(f"测试3: K线图数据 - {stock_code}") + print(f"{'='*60}") + + from smart_monitor_kline import SmartMonitorKline + from smart_monitor_data import SmartMonitorDataFetcher + + kline = SmartMonitorKline() + fetcher = SmartMonitorDataFetcher() + + # 使用Tushare获取K线数据 + print("正在通过Tushare获取K线数据(60天)...") + df = kline._get_kline_from_tushare(stock_code, days=60, ts_pro=fetcher.ts_pro) + + if df is not None and not df.empty: + print(f"✅ K线数据获取成功!") + print(f" 数据条数: {len(df)}条") + print(f" 日期范围: {df['日期'].min()} ~ {df['日期'].max()}") + print(f"\n数据列:") + for col in df.columns: + print(f" - {col}") + + print(f"\n最近5条数据预览:") + print(df.tail(5)[['日期', '开盘', '最高', '最低', '收盘', '成交量']].to_string()) + + return True + else: + print("❌ K线数据获取失败") + return False + + +def test_full_monitoring_flow(stock_code='000063'): + """测试完整的监控流程(使用Tushare)""" + print(f"\n{'='*60}") + print(f"测试4: 完整监控流程 - {stock_code}") + print(f"{'='*60}") + + from smart_monitor_data import SmartMonitorDataFetcher + + fetcher = SmartMonitorDataFetcher() + + # 1. 获取实时行情 + print("\n步骤1: 获取实时行情...") + quote = fetcher.get_realtime_quote(stock_code, retry=1) + if not quote: + print(" ❌ 实时行情获取失败") + return False + print(f" ✅ 当前价: ¥{quote.get('current_price', 0):.2f}") + + # 2. 计算技术指标 + print("\n步骤2: 计算技术指标...") + indicators = fetcher.get_technical_indicators(stock_code, retry=1) + if not indicators: + print(" ❌ 技术指标计算失败") + return False + print(f" ✅ MA5: {indicators.get('ma5', 0):.2f}, 趋势: {indicators.get('trend', 'N/A')}") + + # 3. 综合数据 + print("\n步骤3: 获取综合数据...") + comprehensive_data = fetcher.get_comprehensive_data(stock_code) + if not comprehensive_data: + print(" ❌ 综合数据获取失败") + return False + + print(" ✅ 综合数据包含:") + print(f" - 实时行情: {comprehensive_data.get('realtime_quote') is not None}") + print(f" - 技术指标: {comprehensive_data.get('technical_indicators') is not None}") + + print("\n✅ 完整监控流程测试通过!") + print(" Tushare可以满足AI盯盘的监控要求") + return True + + +def main(): + """主测试函数""" + print("="*60) + print("Tushare数据源监控能力测试") + print("="*60) + + # 检查Token + if not test_tushare_token(): + print("\n❌ 请在.env文件中配置TUSHARE_TOKEN") + return + + # 测试股票代码 + test_stock = '000063' # 中兴通讯 + + results = [] + + # 测试1: 实时行情 + results.append(("实时行情获取", test_realtime_quote(test_stock))) + + # 测试2: 技术指标 + results.append(("技术指标计算", test_technical_indicators(test_stock))) + + # 测试3: K线数据 + results.append(("K线图数据", test_kline_data(test_stock))) + + # 测试4: 完整流程 + results.append(("完整监控流程", test_full_monitoring_flow(test_stock))) + + # 汇总结果 + print(f"\n{'='*60}") + print("测试结果汇总") + print(f"{'='*60}") + + for test_name, result in results: + status = "✅ 通过" if result else "❌ 失败" + print(f"{test_name:20} {status}") + + all_passed = all(result for _, result in results) + + if all_passed: + print(f"\n{'='*60}") + print("🎉 所有测试通过!") + print(f"{'='*60}") + print("✅ Tushare完全可以满足AI盯盘的监控要求") + print("✅ 数据源降级策略工作正常") + print("✅ 可以在AKShare IP被封时使用Tushare作为备用") + print("\n建议:") + print("1. 保持Tushare Token配置在.env文件中") + print("2. AKShare重试次数已设置为1次,减少IP封禁风险") + print("3. Tushare 10000积分可以支持日常监控需求") + else: + print(f"\n{'='*60}") + print("⚠️ 部分测试失败") + print(f"{'='*60}") + print("请检查:") + print("1. Tushare Token是否有效") + print("2. Tushare积分是否足够") + print("3. 网络连接是否正常") + + +if __name__ == '__main__': + main() +