仿alphaarena项目增加A股ai自动量化
This commit is contained in:
@@ -1,6 +1,24 @@
|
|||||||
# 🤖 复合多AI智能体股票团队分析系统
|
# 🤖 复合多AI智能体股票团队分析系统
|
||||||
|
|
||||||
- 初心:在股市摸爬滚打多年,自学自编各种指标,花冤枉钱学习了各种战法各种策略,也曾入各种小班,总是赚少赔多,逐渐失去在股市玩的信心。自从去年deepseek上市,一直探索用ai辅助分析,且近日受tradingagents项目启发(感谢原作),多agent结合跟踪主力资金战法(某指每年收费6000rmb),用各种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决策更可靠
|
||||||
|
|
||||||
- 希望能帮到你!欢迎加微信群讨论
|
- 希望能帮到你!欢迎加微信群讨论
|
||||||
<img width="300" height="324" alt="image" src="https://www.sd-hn.cn/img/2wm1031.png" />
|
<img width="300" height="324" alt="image" src="https://www.sd-hn.cn/img/2wm1031.png" />
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ from config_manager import config_manager
|
|||||||
from main_force_ui import display_main_force_selector
|
from main_force_ui import display_main_force_selector
|
||||||
from sector_strategy_ui import display_sector_strategy
|
from sector_strategy_ui import display_sector_strategy
|
||||||
from longhubang_ui import display_longhubang
|
from longhubang_ui import display_longhubang
|
||||||
|
from smart_monitor_ui import smart_monitor_ui
|
||||||
|
|
||||||
# 页面配置
|
# 页面配置
|
||||||
st.set_page_config(
|
st.set_page_config(
|
||||||
@@ -318,14 +319,14 @@ def main():
|
|||||||
if st.button("🎯 智策板块", width='stretch', key="nav_sector_strategy", help="AI板块策略分析"):
|
if st.button("🎯 智策板块", width='stretch', key="nav_sector_strategy", help="AI板块策略分析"):
|
||||||
st.session_state.show_sector_strategy = True
|
st.session_state.show_sector_strategy = True
|
||||||
for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
|
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:
|
if key in st.session_state:
|
||||||
del st.session_state[key]
|
del st.session_state[key]
|
||||||
|
|
||||||
if st.button("🐉 智瞰龙虎", width='stretch', key="nav_longhubang", help="龙虎榜深度分析"):
|
if st.button("🐉 智瞰龙虎", width='stretch', key="nav_longhubang", help="龙虎榜深度分析"):
|
||||||
st.session_state.show_longhubang = True
|
st.session_state.show_longhubang = True
|
||||||
for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
|
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:
|
if key in st.session_state:
|
||||||
del st.session_state[key]
|
del st.session_state[key]
|
||||||
|
|
||||||
@@ -336,14 +337,21 @@ def main():
|
|||||||
if st.button("📊 持仓分析", width='stretch', key="nav_portfolio", help="投资组合分析与定时跟踪"):
|
if st.button("📊 持仓分析", width='stretch', key="nav_portfolio", help="投资组合分析与定时跟踪"):
|
||||||
st.session_state.show_portfolio = True
|
st.session_state.show_portfolio = True
|
||||||
for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
|
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:
|
if key in st.session_state:
|
||||||
del st.session_state[key]
|
del st.session_state[key]
|
||||||
|
|
||||||
if st.button("📡 实时监测", width='stretch', key="nav_monitor", help="价格监控与预警提醒"):
|
if st.button("📡 实时监测", width='stretch', key="nav_monitor", help="价格监控与预警提醒"):
|
||||||
st.session_state.show_monitor = True
|
st.session_state.show_monitor = True
|
||||||
for key in ['show_history', 'show_main_force', 'show_longhubang', 'show_portfolio',
|
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:
|
if key in st.session_state:
|
||||||
del st.session_state[key]
|
del st.session_state[key]
|
||||||
|
|
||||||
@@ -464,6 +472,11 @@ def main():
|
|||||||
display_longhubang()
|
display_longhubang()
|
||||||
return
|
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:
|
if 'show_portfolio' in st.session_state and st.session_state.show_portfolio:
|
||||||
from portfolio_ui import display_portfolio_manager
|
from portfolio_ui import display_portfolio_manager
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -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/` 目录下的其他文档
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**祝您交易顺利!🚀**
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
Binary file not shown.
+25
-6
@@ -109,13 +109,20 @@ class StockMonitorDatabase:
|
|||||||
|
|
||||||
stocks = []
|
stocks = []
|
||||||
for row in cursor.fetchall():
|
for row in cursor.fetchall():
|
||||||
quant_config = json.loads(row[12]) if row[12] else None
|
try:
|
||||||
|
quant_config = json.loads(row[12]) if row[12] else None
|
||||||
|
entry_range = json.loads(row[4]) if row[4] else None
|
||||||
|
except (json.JSONDecodeError, TypeError) as e:
|
||||||
|
print(f"警告: 股票 {row[1]} 的JSON解析失败: {e}")
|
||||||
|
entry_range = None
|
||||||
|
quant_config = None
|
||||||
|
|
||||||
stocks.append({
|
stocks.append({
|
||||||
'id': row[0],
|
'id': row[0],
|
||||||
'symbol': row[1],
|
'symbol': row[1],
|
||||||
'name': row[2],
|
'name': row[2],
|
||||||
'rating': row[3],
|
'rating': row[3],
|
||||||
'entry_range': json.loads(row[4]),
|
'entry_range': entry_range,
|
||||||
'take_profit': row[5],
|
'take_profit': row[5],
|
||||||
'stop_loss': row[6],
|
'stop_loss': row[6],
|
||||||
'current_price': row[7],
|
'current_price': row[7],
|
||||||
@@ -373,13 +380,20 @@ class StockMonitorDatabase:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
if row:
|
if row:
|
||||||
quant_config = json.loads(row[12]) if row[12] else None
|
try:
|
||||||
|
quant_config = json.loads(row[12]) if row[12] else None
|
||||||
|
entry_range = json.loads(row[4]) if row[4] else None
|
||||||
|
except (json.JSONDecodeError, TypeError) as e:
|
||||||
|
print(f"警告: 股票 {row[1]} 的JSON解析失败: {e}")
|
||||||
|
entry_range = None
|
||||||
|
quant_config = None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': row[0],
|
'id': row[0],
|
||||||
'symbol': row[1],
|
'symbol': row[1],
|
||||||
'name': row[2],
|
'name': row[2],
|
||||||
'rating': row[3],
|
'rating': row[3],
|
||||||
'entry_range': json.loads(row[4]),
|
'entry_range': entry_range,
|
||||||
'take_profit': row[5],
|
'take_profit': row[5],
|
||||||
'stop_loss': row[6],
|
'stop_loss': row[6],
|
||||||
'current_price': row[7],
|
'current_price': row[7],
|
||||||
@@ -412,8 +426,13 @@ class StockMonitorDatabase:
|
|||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
if row:
|
if row:
|
||||||
entry_range = json.loads(row[4])
|
try:
|
||||||
quant_config = json.loads(row[12]) if row[12] else None
|
entry_range = json.loads(row[4]) if row[4] else None
|
||||||
|
quant_config = json.loads(row[12]) if row[12] else None
|
||||||
|
except (json.JSONDecodeError, TypeError) as e:
|
||||||
|
print(f"警告: 股票 {row[1]} 的JSON解析失败: {e}")
|
||||||
|
entry_range = None
|
||||||
|
quant_config = None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': row[0],
|
'id': row[0],
|
||||||
|
|||||||
+8
-5
@@ -282,11 +282,14 @@ def display_stock_card(stock: Dict):
|
|||||||
# 关键位置信息
|
# 关键位置信息
|
||||||
st.markdown("**🎯 关键位置**")
|
st.markdown("**🎯 关键位置**")
|
||||||
|
|
||||||
entry_range = stock['entry_range']
|
entry_range = stock.get('entry_range')
|
||||||
col1, col2, col3 = st.columns(3)
|
col1, col2, col3 = st.columns(3)
|
||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
st.info(f"**进场区间**\n¥{entry_range['min']} - ¥{entry_range['max']}")
|
if entry_range and isinstance(entry_range, dict):
|
||||||
|
st.info(f"**进场区间**\n¥{entry_range.get('min', 0)} - ¥{entry_range.get('max', 0)}")
|
||||||
|
else:
|
||||||
|
st.warning("**进场区间**\n未设置")
|
||||||
|
|
||||||
with col2:
|
with col2:
|
||||||
if stock['take_profit']:
|
if stock['take_profit']:
|
||||||
@@ -378,9 +381,9 @@ def display_edit_dialog(stock_id: int):
|
|||||||
|
|
||||||
with col1:
|
with col1:
|
||||||
st.subheader("🎯 关键位置")
|
st.subheader("🎯 关键位置")
|
||||||
entry_range = stock['entry_range']
|
entry_range = stock.get('entry_range', {})
|
||||||
entry_min = st.number_input("进场区间最低价", value=float(entry_range['min']), step=0.01, format="%.2f")
|
entry_min = st.number_input("进场区间最低价", value=float(entry_range.get('min', 0)), step=0.01, format="%.2f")
|
||||||
entry_max = st.number_input("进场区间最高价", value=float(entry_range['max']), step=0.01, format="%.2f")
|
entry_max = st.number_input("进场区间最高价", value=float(entry_range.get('max', 0)), step=0.01, format="%.2f")
|
||||||
take_profit = st.number_input("止盈价位", value=float(stock['take_profit']) if stock['take_profit'] else 0.0, step=0.01, format="%.2f")
|
take_profit = st.number_input("止盈价位", value=float(stock['take_profit']) if stock['take_profit'] else 0.0, step=0.01, format="%.2f")
|
||||||
stop_loss = st.number_input("止损价位", value=float(stock['stop_loss']) if stock['stop_loss'] else 0.0, step=0.01, format="%.2f")
|
stop_loss = st.number_input("止损价位", value=float(stock['stop_loss']) if stock['stop_loss'] else 0.0, step=0.01, format="%.2f")
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -12,5 +12,5 @@ pytz
|
|||||||
ta>=0.10.2
|
ta>=0.10.2
|
||||||
reportlab>=4.0.0
|
reportlab>=4.0.0
|
||||||
peewee>=3.17.0
|
peewee>=3.17.0
|
||||||
schedule>=1.2.0
|
schedule>=1.2.0
|
||||||
pywencai>=0.7.0
|
pywencai>=0.7.0
|
||||||
Binary file not shown.
@@ -0,0 +1,775 @@
|
|||||||
|
"""
|
||||||
|
智能盯盘 - A股数据获取模块
|
||||||
|
使用akshare获取实时行情和技术指标
|
||||||
|
支持降级到tushare作为备用数据源
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import akshare as ak
|
||||||
|
import pandas as pd
|
||||||
|
from typing import Dict, Optional
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
|
class SmartMonitorDataFetcher:
|
||||||
|
"""A股数据获取器(支持多数据源降级)"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# 初始化Tushare(备用数据源)
|
||||||
|
self.ts_pro = None
|
||||||
|
tushare_token = os.getenv('TUSHARE_TOKEN', '')
|
||||||
|
|
||||||
|
if tushare_token:
|
||||||
|
try:
|
||||||
|
import tushare as ts
|
||||||
|
ts.set_token(tushare_token)
|
||||||
|
self.ts_pro = ts.pro_api()
|
||||||
|
self.logger.info("Tushare备用数据源初始化成功")
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.warning(f"Tushare初始化失败: {e}")
|
||||||
|
else:
|
||||||
|
self.logger.info("未配置Tushare Token,仅使用AKShare数据源")
|
||||||
|
|
||||||
|
def get_realtime_quote(self, stock_code: str, retry: int = 1) -> 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']}")
|
||||||
|
|
||||||
@@ -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']}")
|
||||||
|
|
||||||
@@ -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'
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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')}")
|
||||||
|
|
||||||
@@ -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"<b>{config['text']}</b><br>"
|
||||||
|
f"日期: {decision_date}<br>"
|
||||||
|
f"价格: ¥{price:.2f}<br>"
|
||||||
|
f"置信度: {confidence}%<br>"
|
||||||
|
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线数据失败")
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -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"""
|
||||||
|
<div style="border-left: 4px solid {color}; padding-left: 10px; margin-bottom: 10px;">
|
||||||
|
<p style="margin: 0;">
|
||||||
|
<strong>{icon} {action_name}</strong>
|
||||||
|
{'✅' if executed else '⏳'}
|
||||||
|
</p>
|
||||||
|
<p style="margin: 5px 0; font-size: 0.85em; color: gray;">
|
||||||
|
{decision_time}
|
||||||
|
</p>
|
||||||
|
<p style="margin: 5px 0; font-size: 0.9em;">
|
||||||
|
<strong>置信度:</strong> {confidence}%
|
||||||
|
</p>
|
||||||
|
<p style="margin: 5px 0; font-size: 0.9em;">
|
||||||
|
<strong>推理:</strong> {reasoning[:100]}{'...' if len(reasoning) > 100 else ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
st.markdown("---")
|
||||||
|
else:
|
||||||
|
st.info("📭 暂无AI决策记录")
|
||||||
|
st.caption("启动监控后,AI会定期分析并记录决策")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"❌ 加载决策历史失败: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
smart_monitor_ui()
|
||||||
|
|
||||||
Binary file not shown.
@@ -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)
|
||||||
|
|
||||||
@@ -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()
|
||||||
|
|
||||||
Reference in New Issue
Block a user