增加选股策略

This commit is contained in:
oficcejo
2025-12-12 19:46:38 +08:00
parent 595278100f
commit ad5865d3b4
25 changed files with 4818 additions and 20 deletions
+29
View File
@@ -93,3 +93,32 @@ WEBHOOK_KEYWORD=
# ========== 时区配置 ==========
# 系统时区设置(可选)
TZ=Asia/Shanghai
# ========== TDX数据源API配置(策略监控必须)==========
# TDX API服务地址
# 说明:
# - 用于获取股票K线数据和技术指标
# - 默认本地服务地址:http://127.0.0.1:5000
# - 如果使用远程服务,请修改为实际地址
# API接口:
# - /api/kline?code=000001&type=day # 获取日K线数据
# - /api/quote?code=000001 # 获取实时行情
# - /api/health # 健康检查
TDX_BASE_URL=http://127.0.0.1:5000
# ========== 低价擒牛策略监控配置 ==========
# 扫描间隔(秒)
# 说明:
# - 监控服务扫描股票的时间间隔
# - 建议范围:30-300秒
# - 默认值:60秒(每分钟扫描1次)
LOW_PRICE_BULL_SCAN_INTERVAL=60
# 持股天数限制
# 说明:
# - 持股达到该天数后自动提醒卖出
# - 按自然日计算,不区分交易日
# - 默认值:5天
LOW_PRICE_BULL_HOLDING_DAYS=5
+22 -9
View File
@@ -294,7 +294,7 @@ def main():
if st.button("🏠 股票分析", width='stretch', key="nav_home", help="返回首页,进行单只股票的深度分析"):
# 清除所有功能页面标志
for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
'show_sector_strategy', 'show_longhubang', 'show_portfolio']:
'show_sector_strategy', 'show_longhubang', 'show_portfolio', 'show_low_price_bull']:
if key in st.session_state:
del st.session_state[key]
@@ -307,7 +307,14 @@ def main():
if st.button("💰 主力选股", width='stretch', key="nav_main_force", help="基于主力资金流向的选股策略"):
st.session_state.show_main_force = True
for key in ['show_history', 'show_monitor', 'show_config', 'show_sector_strategy',
'show_longhubang', 'show_portfolio']:
'show_longhubang', 'show_portfolio', 'show_low_price_bull']:
if key in st.session_state:
del st.session_state[key]
if st.button("🐂 低价擒牛", width='stretch', key="nav_low_price_bull", help="低价高成长股票筛选策略"):
st.session_state.show_low_price_bull = True
for key in ['show_history', 'show_monitor', 'show_config', 'show_sector_strategy',
'show_longhubang', 'show_portfolio', 'show_main_force']:
if key in st.session_state:
del st.session_state[key]
@@ -318,14 +325,14 @@ def main():
if st.button("🎯 智策板块", width='stretch', key="nav_sector_strategy", help="AI板块策略分析"):
st.session_state.show_sector_strategy = True
for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
'show_longhubang', 'show_portfolio', 'show_smart_monitor']:
'show_longhubang', 'show_portfolio', 'show_smart_monitor', 'show_low_price_bull']:
if key in st.session_state:
del st.session_state[key]
if st.button("🐉 智瞰龙虎", width='stretch', key="nav_longhubang", help="龙虎榜深度分析"):
st.session_state.show_longhubang = True
for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
'show_sector_strategy', 'show_portfolio', 'show_smart_monitor']:
'show_sector_strategy', 'show_portfolio', 'show_smart_monitor', 'show_low_price_bull']:
if key in st.session_state:
del st.session_state[key]
@@ -336,21 +343,21 @@ def main():
if st.button("📊 持仓分析", width='stretch', key="nav_portfolio", help="投资组合分析与定时跟踪"):
st.session_state.show_portfolio = True
for key in ['show_history', 'show_monitor', 'show_config', 'show_main_force',
'show_sector_strategy', 'show_longhubang', 'show_smart_monitor']:
'show_sector_strategy', 'show_longhubang', 'show_smart_monitor', 'show_low_price_bull']:
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']:
'show_sector_strategy', 'show_longhubang', 'show_portfolio', 'show_low_price_bull']:
if key in st.session_state:
del st.session_state[key]
if st.button("📡 实时监测", width='stretch', key="nav_monitor", help="价格监控与预警提醒"):
st.session_state.show_monitor = True
for key in ['show_history', 'show_main_force', 'show_longhubang', 'show_portfolio',
'show_config', 'show_sector_strategy', 'show_smart_monitor']:
'show_config', 'show_sector_strategy', 'show_smart_monitor', 'show_low_price_bull']:
if key in st.session_state:
del st.session_state[key]
@@ -360,7 +367,7 @@ def main():
if st.button("📖 历史记录", width='stretch', key="nav_history", help="查看历史分析记录"):
st.session_state.show_history = True
for key in ['show_monitor', 'show_longhubang', 'show_portfolio', 'show_config',
'show_main_force', 'show_sector_strategy']:
'show_main_force', 'show_sector_strategy', 'show_low_price_bull']:
if key in st.session_state:
del st.session_state[key]
@@ -368,7 +375,7 @@ def main():
if st.button("⚙️ 环境配置", width='stretch', key="nav_config", help="系统设置与API配置"):
st.session_state.show_config = True
for key in ['show_history', 'show_monitor', 'show_main_force', 'show_sector_strategy',
'show_longhubang', 'show_portfolio']:
'show_longhubang', 'show_portfolio', 'show_low_price_bull']:
if key in st.session_state:
del st.session_state[key]
@@ -460,6 +467,12 @@ def main():
if 'show_main_force' in st.session_state and st.session_state.show_main_force:
display_main_force_selector()
return
# 检查是否显示低价擒牛
if 'show_low_price_bull' in st.session_state and st.session_state.show_low_price_bull:
from low_price_bull_ui import display_low_price_bull
display_low_price_bull()
return
# 检查是否显示智策板块
if 'show_sector_strategy' in st.session_state and st.session_state.show_sector_strategy:
+246
View File
@@ -0,0 +1,246 @@
# TDX API配置快速指南
## 问题说明
如果您看到以下警告:
```
WARNING:low_price_bull_service:TDX数据源未配置,无法获取股票数据
```
说明系统无法连接到TDX API服务,需要进行配置。
## 解决方案
### 步骤1:配置TDX API地址
`.env` 文件中添加以下配置:
```bash
# TDX数据源API配置
TDX_BASE_URL=http://127.0.0.1:5000
```
### 步骤2:确保TDX API服务已启动
TDX API服务需要单独启动,请检查:
1. **检查服务是否运行**
```bash
# 在浏览器中访问
http://127.0.0.1:5000/api/health
# 或使用curl命令
curl http://127.0.0.1:5000/api/health
```
2. **如果服务未启动**
- 请启动您的TDX API服务
- 默认端口:5000
- 如使用其他端口,请修改.env中的URL
### 步骤3:测试配置
运行测试脚本验证配置:
```bash
python test_tdx_api.py
```
**期望输出**
```
============================================================
TDX API配置测试
============================================================
1. TDX API地址: http://127.0.0.1:5000
2. 测试健康检查接口...
✅ 健康检查成功
响应: OK
3. 测试K线数据接口...
测试股票: 000001
✅ K线数据获取成功
数据条数: 250
最新K线数据:
- 日期: 2024-12-12
- 开盘: 10.50
- 收盘: 10.60
- 最高: 10.80
- 最低: 10.30
- 成交量: 1000000
✅ 数据量充足,可以计算MA20(需要至少20条)
4. 测试均线计算...
✅ 均线计算成功
- 收盘价: 10.60
- MA5: 10.55
- MA20: 10.45
- 趋势: 🟢 MA5 > MA20 (多头)
============================================================
✅ 所有测试通过!TDX API配置正常
============================================================
```
## TDX API接口说明
### 核心接口
| 接口 | 说明 | 示例 |
|------|------|------|
| /api/health | 健康检查 | - |
| /api/kline | K线数据 | ?code=000001&type=day |
| /api/quote | 五档行情 | ?code=000001 |
| /api/stock-info | 综合信息 | ?code=000001 |
### K线数据接口详情
**URL**: `/api/kline`
**参数**:
- `code`: 股票代码(必填)
- 支持格式:
- 纯数字:`000001``600000`
- 市场前缀:`SZ000001``SH600000`
- 示例:000001(平安银行)
- 示例:600000(浦发银行)
- `type`: K线类型(必填)
- `day`: 日K线
- `week`: 周K线
- `month`: 月K线
**代码格式说明**
- 系统会自动处理不同格式的股票代码
- 如果使用`SZ`/`SH`前缀失败,会自动重试纯数字代码
- 支持带后缀的代码(如`002259.SZ`),系统会自动去除后缀
**返回格式**:
```json
[
{
"date": "2024-12-12",
"open": 10.50,
"high": 10.80,
"low": 10.30,
"close": 10.60,
"volume": 1000000
},
...
]
```
**使用示例**:
```bash
# 获取平安银行日K线
curl "http://127.0.0.1:5000/api/kline?code=000001&type=day"
```
## 常见问题
### Q1: 连接失败,显示连接超时?
**原因**
- TDX API服务未启动
- 防火墙阻止了连接
- 端口被占用
**解决方案**
1. 检查TDX API服务是否启动
2. 检查端口5000是否被占用
3. 尝试使用其他端口并修改.env配置
### Q2: 健康检查失败,HTTP 404
**原因**
- TDX API服务版本不支持`/api/health`接口
- URL配置错误
**解决方案**
1. 尝试访问其他接口,如`/api/kline`
2. 检查.env中的TDX_BASE_URL是否正确
3. 确认服务地址和端口
### Q3: K线数据获取失败?
**原因**
- 股票代码不存在
- 接口参数错误
- 数据源未就绪
**解决方案**
1. 使用已知存在的股票代码测试(如000001)
2. 检查参数格式是否正确
3. 查看TDX API服务日志
### Q4: 数据量不足,无法计算MA20?
**原因**
- TDX API返回的K线数据少于20条
- 新上市股票数据不足
**解决方案**
1. 这是正常情况,系统会自动跳过
2. 等待股票积累更多交易日数据
3. 监控服务会在下次扫描时重试
### Q5: 如何使用远程TDX API服务?
如果TDX API服务部署在其他服务器上:
```bash
# .env配置
TDX_BASE_URL=http://192.168.1.100:5000
# 或使用域名
TDX_BASE_URL=http://tdx-api.example.com:5000
```
### Q6: 如何修改扫描间隔?
在.env文件中修改:
```bash
# 扫描间隔(秒)
LOW_PRICE_BULL_SCAN_INTERVAL=60 # 默认60秒,可改为30-300秒
```
或在监控面板的"⚙️ 监控配置"中动态修改。
## 配置检查清单
- [ ] ✅ .env文件中已配置TDX_BASE_URL
- [ ] ✅ TDX API服务已启动
- [ ] ✅ 可以访问http://127.0.0.1:5000/api/health
- [ ] ✅ 运行test_tdx_api.py测试通过
- [ ] ✅ 在监控面板启动监控服务
- [ ] ✅ 配置了Webhook通知(可选)
## 下一步
配置完成后:
1. 进入"低价擒牛"选股板块
2. 执行选股,查看结果
3. 点击" 加入策略监控"
4. 进入"📊 策略监控"面板
5. 点击"▶️ 启动监控服务"
6. 监控服务将每60秒扫描一次
## 需要帮助?
如果仍然遇到问题,请:
1. 查看控制台错误日志
2. 运行`python test_tdx_api.py`获取详细错误信息
3. 检查TDX API服务日志
4. 确认网络连接正常
---
**文档版本**: v1.0
**更新日期**: 2024-12-12
+416
View File
@@ -0,0 +1,416 @@
'''
三重滤网交易系统 V2.0
by Alexander Elder (优化版)
核心改进
1. 第一重滤网月线MACD金叉/多头更稳定的趋势判断
2. 第二重滤网日线RSI/KDJ超卖回调放宽条件
3. 第三重滤网价格企稳或突破更灵活的入场
'''
import jqdata
import numpy as np
## 初始化函数
def initialize(context):
set_benchmark('000300.XSHG')
set_option('use_real_price', True)
set_option('order_volume_ratio', 1)
set_order_cost(OrderCost(open_tax=0, close_tax=0.001,
open_commission=0.0003, close_commission=0.0003,
close_today_commission=0, min_commission=5), type='stock')
# ========== 策略参数 ==========
g.stocknum = 10 # 增加持仓数
g.max_position_ratio = 0.98
# 第一重滤网参数(月线MACD
g.macd_fast = 12
g.macd_slow = 26
g.macd_signal = 9
# 第二重滤网参数(日线震荡 - 放宽)
g.rsi_period = 14
g.rsi_oversold = 50 # 放宽到50
g.rsi_low = 35 # 极度超卖
g.kdj_oversold = 40 # 放宽到40
# 第三重滤网参数(入场确认 - 放宽)
g.ma_short = 5 # 5日均线
g.ma_mid = 20 # 20日均线
# 止盈止损参数
g.stop_loss = 0.06
g.take_profit = 0.20
g.trailing_stop = 0.08
# 市值筛选(亿)
g.min_market_cap = 20
g.max_market_cap = 1500
# 记录
g.highest_profit = {}
g.hold_days = {}
# 定时任务
run_daily(morning_screen, '09:35')
run_daily(check_positions, '14:00')
run_daily(afternoon_trade, '14:50')
## 早盘筛选
def morning_screen(context):
g.buy_list = triple_screen_filter(context)
log.info("【三重滤网】筛选出 %d 只股票" % len(g.buy_list))
## 三重滤网筛选
def triple_screen_filter(context):
# 基础选股
q = query(
valuation.code,
valuation.market_cap
).filter(
valuation.market_cap.between(g.min_market_cap, g.max_market_cap)
).order_by(
valuation.market_cap.asc()
).limit(500)
df = get_fundamentals(q)
if df.empty:
return []
stock_list = list(df['code'])
stock_list = filter_basic(stock_list)
# 三重滤网筛选
candidates = []
for stock in stock_list[:150]:
result = check_triple_screen(stock)
if result['pass']:
candidates.append((stock, result['score']))
# 按评分排序
candidates.sort(key=lambda x: x[1], reverse=True)
return [c[0] for c in candidates[:g.stocknum * 3]]
## 检查三重滤网信号
def check_triple_screen(stock):
"""
三重滤网检查 V2.0
1. 第一重月线MACD金叉/多头
2. 第二重日线RSI/KDJ回调
3. 第三重价格企稳或突破
"""
result = {'pass': False, 'score': 0}
try:
# 获取更多日线数据(计算月线MACD需要约130天)
df = attribute_history(stock, 150, '1d', ['close', 'high', 'low'], skip_paused=True)
if len(df) < 130:
return result
close = df['close'].values
high = df['high'].values
low = df['low'].values
# ========== 第一重滤网:月线MACD ==========
# 模拟月线:每20天取收盘价(或用月末价格)
monthly_close = []
for i in range(19, len(close), 20):
monthly_close.append(close[i])
if len(monthly_close) < 6:
# 数据不足,改用日线MACD判断大趋势
dif, dea, macd = calculate_macd(close)
if dif[-1] <= dea[-1]: # MACD死叉
return result
if dif[-1] <= 0: # DIF在零轴下方
return result
else:
# 计算月线MACD
m_dif, m_dea, m_macd = calculate_macd(np.array(monthly_close))
# 月线MACD条件(放宽):
# 1. MACD金叉(DIF上穿DEA)或
# 2. MACD柱由绿变红 或
# 3. DIF > 0 且 DIF > DEA(多头)
macd_golden = m_dif[-1] > m_dea[-1] and m_dif[-2] <= m_dea[-2]
macd_turn_red = m_macd[-1] > 0 and m_macd[-2] <= 0
macd_bullish = m_dif[-1] > 0 and m_dif[-1] > m_dea[-1]
if not (macd_golden or macd_turn_red or macd_bullish):
return result
result['score'] += 30 # 趋势分
# ========== 第二重滤网:日线震荡指标 ==========
rsi = calculate_rsi(close, g.rsi_period)
k, d, j = calculate_kdj(high, low, close)
# RSI条件(放宽):
# 1. RSI < 50(相对低位)或
# 2. RSI从低位回升
rsi_current = rsi[-1] if len(rsi) > 0 else 50
rsi_prev = rsi[-2] if len(rsi) > 1 else 50
rsi_signal = (rsi_current < g.rsi_oversold) or \
(rsi_prev < g.rsi_low and rsi_current > rsi_prev) or \
(rsi_current < 60 and rsi_current > rsi_prev)
# KDJ条件(放宽):
# 1. K < D 后金叉 或
# 2. J < 40 或
# 3. K/D都在低位回升
kdj_golden = k[-1] > d[-1] and k[-2] <= d[-2]
kdj_oversold = j[-1] < g.kdj_oversold or k[-1] < g.kdj_oversold
kdj_rising = k[-1] > k[-2] and d[-1] > d[-2] and k[-1] < 60
kdj_signal = kdj_golden or kdj_oversold or kdj_rising
if not (rsi_signal or kdj_signal):
return result
result['score'] += 30
# ========== 第三重滤网:入场确认(放宽)==========
current_price = close[-1]
ma5 = np.mean(close[-g.ma_short:])
ma20 = np.mean(close[-g.ma_mid:])
# 入场条件(满足任一):
# 1. 价格站上5日均线
# 2. 价格接近20日均线(±3%
# 3. 价格突破近5日高点
recent_high = np.max(high[-6:-1])
price_above_ma5 = current_price > ma5
price_near_ma20 = abs(current_price - ma20) / ma20 < 0.03
price_breakout = current_price > recent_high * 0.99
if not (price_above_ma5 or price_near_ma20 or price_breakout):
return result
result['score'] += 40
# 额外加分
if rsi_current < 35:
result['score'] += 15
if kdj_golden:
result['score'] += 10
if current_price > ma5 > ma20:
result['score'] += 10
result['pass'] = True
return result
except:
return result
## 计算MACD
def calculate_macd(close, fast=12, slow=26, signal=9):
ema_fast = calculate_ema(close, fast)
ema_slow = calculate_ema(close, slow)
dif = ema_fast - ema_slow
dea = calculate_ema(dif, signal)
macd = (dif - dea) * 2
return dif, dea, macd
## 计算EMA
def calculate_ema(data, period):
ema = np.zeros(len(data))
ema[0] = data[0]
multiplier = 2 / (period + 1)
for i in range(1, len(data)):
ema[i] = (data[i] - ema[i-1]) * multiplier + ema[i-1]
return ema
## 计算RSI
def calculate_rsi(close, period=14):
delta = np.diff(close)
gain = np.where(delta > 0, delta, 0)
loss = np.where(delta < 0, -delta, 0)
avg_gain = np.zeros(len(delta))
avg_loss = np.zeros(len(delta))
avg_gain[period-1] = np.mean(gain[:period])
avg_loss[period-1] = np.mean(loss[:period])
for i in range(period, len(delta)):
avg_gain[i] = (avg_gain[i-1] * (period-1) + gain[i]) / period
avg_loss[i] = (avg_loss[i-1] * (period-1) + loss[i]) / period
rs = avg_gain / (avg_loss + 1e-10)
rsi = 100 - 100 / (1 + rs)
return rsi
## 计算KDJ
def calculate_kdj(high, low, close, n=9, m1=3, m2=3):
length = len(close)
rsv = np.zeros(length)
k = np.zeros(length)
d = np.zeros(length)
j = np.zeros(length)
for i in range(n-1, length):
hn = np.max(high[i-n+1:i+1])
ln = np.min(low[i-n+1:i+1])
rsv[i] = (close[i] - ln) / (hn - ln + 1e-10) * 100
k[n-1] = 50
d[n-1] = 50
for i in range(n, length):
k[i] = (m1-1)/m1 * k[i-1] + 1/m1 * rsv[i]
d[i] = (m2-1)/m2 * d[i-1] + 1/m2 * k[i]
j[i] = 3 * k[i] - 2 * d[i]
return k, d, j
## 检查持仓
def check_positions(context):
if len(context.portfolio.positions) == 0:
return
for stock in list(context.portfolio.positions.keys()):
position = context.portfolio.positions[stock]
if position.closeable_amount <= 0:
continue
cost = position.avg_cost
current_price = position.price
if cost <= 0:
continue
profit_ratio = (current_price - cost) / cost
g.hold_days[stock] = g.hold_days.get(stock, 0) + 1
if stock not in g.highest_profit:
g.highest_profit[stock] = profit_ratio
else:
g.highest_profit[stock] = max(g.highest_profit[stock], profit_ratio)
highest = g.highest_profit[stock]
# === 止损 ===
if profit_ratio < -g.stop_loss:
log.info("【止损】%s 亏损 %.2f%%" % (stock, profit_ratio * 100))
order_target(stock, 0)
clean_stock_data(stock)
continue
# === 趋势反转卖出 ===
if check_trend_reversal(stock):
log.info("【趋势反转】%s 周线趋势转弱" % stock)
order_target(stock, 0)
clean_stock_data(stock)
continue
# === 移动止盈 ===
if highest >= g.take_profit:
drawdown = highest - profit_ratio
allowed_drawdown = min(g.trailing_stop + highest * 0.4, 0.18)
if drawdown >= allowed_drawdown:
log.info("【移动止盈】%s 最高%.1f%% 回撤%.1f%%" %
(stock, highest*100, drawdown*100))
order_target(stock, 0)
clean_stock_data(stock)
continue
# === 分批止盈 ===
if profit_ratio >= g.take_profit * 2:
sell_amount = int(position.closeable_amount * 0.3 / 100) * 100
if sell_amount >= 100:
log.info("【止盈】%s +%.1f%%" % (stock, profit_ratio*100))
order(stock, -sell_amount)
## 检查趋势反转(使用MACD
def check_trend_reversal(stock):
try:
df = attribute_history(stock, 60, '1d', ['close'], skip_paused=True)
if len(df) < 50:
return False
close = df['close'].values
dif, dea, macd = calculate_macd(close)
# MACD死叉且DIF < 0
if dif[-1] < dea[-1] and dif[-1] < 0:
return True
# MACD连续3天下降且为负
if macd[-1] < 0 and macd[-2] < 0 and macd[-3] < 0:
if macd[-1] < macd[-2] < macd[-3]:
return True
return False
except:
return False
## 清理数据
def clean_stock_data(stock):
for d in [g.highest_profit, g.hold_days]:
if stock in d:
del d[stock]
## 尾盘交易
def afternoon_trade(context):
buy_stocks(context)
## 买入函数
def buy_stocks(context):
if not hasattr(g, 'buy_list') or not g.buy_list:
return
position_count = len(context.portfolio.positions)
if position_count >= g.stocknum:
return
available_cash = context.portfolio.available_cash * g.max_position_ratio
buy_count = min(g.stocknum - position_count, len(g.buy_list))
if buy_count <= 0 or available_cash < 10000:
return
cash_per_stock = available_cash / buy_count
bought = 0
for stock in g.buy_list:
if bought >= buy_count:
break
if stock in context.portfolio.positions:
continue
# 再次确认三重滤网信号
result = check_triple_screen(stock)
if result['pass']:
order_value(stock, cash_per_stock)
log.info("【买入】%s 评分:%.0f" % (stock, result['score']))
g.highest_profit[stock] = 0
g.hold_days[stock] = 0
bought += 1
## 基础过滤
def filter_basic(stock_list):
if not stock_list:
return []
current_data = get_current_data()
filtered = []
for stock in stock_list:
if current_data[stock].paused:
continue
if current_data[stock].is_st:
continue
if 'ST' in current_data[stock].name or '*' in current_data[stock].name:
continue
if stock.startswith('688') or stock.startswith('8') or stock.startswith('4'):
continue
if current_data[stock].last_price >= current_data[stock].high_limit:
continue
if current_data[stock].last_price <= current_data[stock].low_limit:
continue
filtered.append(stock)
return filtered
+240
View File
@@ -0,0 +1,240 @@
# 低价擒牛功能说明
## 功能概述
**低价擒牛**是一个专注于筛选低价高成长股票的选股策略板块,结合量化交易策略,帮助投资者发现潜在的价值洼地。
## 选股策略
### 筛选条件
| 条件 | 说明 |
|------|------|
| 股价 | < 10元 |
| 净利润增长率 | ≥ 100%(净利润同比增长率) |
| 市场板块 | 深圳A股 |
| 排除项 | ST股票、科创板、创业板 |
| 排序方式 | 按成交额由小至大排名 |
### 策略理念
1. **低价优势**:股价低于10元,易于散户参与,具有更大的上涨空间
2. **高成长性**:净利润增长率≥100%,说明公司业绩高速增长
3. **流动性考虑**:优先选择成交额较小的股票,避免过度热门
4. **风险控制**:排除ST、科创板、创业板,降低风险
## 量化交易策略
### 资金与仓位管理
| 参数 | 配置 | 说明 |
|------|------|------|
| 初始资金 | 100万元 | 策略启动资金 |
| 持股周期 | 5天 | 单只股票最长持有时间 |
| 交易仓位 | 满仓 | 使用所有可用资金 |
| 个股最大持仓 | 40% | 单只股票最多占用40%资金 |
| 账户最大持股数 | 4只 | 同时最多持有4只股票 |
| 单日最大买入数 | 2只 | 每天最多买入2只新股票 |
### 买卖择时规则
#### 买入时机
- **时间点**:开盘买入
- **优先级**:按成交额由小到大排序
- **执行条件**
- 未达到最大持股数(4只)
- 当日买入数量未达上限(2只)
- 有足够的可用资金
#### 卖出时机
满足以下**任一**条件即卖出:
1. **技术信号**MA5日线下穿MA20日线
2. **时间到期**:持股满5天
### 策略优势
1. ✅ **严格的仓位控制**:避免单一股票风险过大
2. ✅ **轮动机制**:卖出后释放资金,继续买入新标的
3. ✅ **纪律性强**:明确的买卖规则,避免情绪化交易
4. ✅ **风险分散**:最多持有4只股票,分散风险
## 使用步骤
### 1. 进入功能
在左侧菜单栏点击:**🎯 选股板块** → **🐂 低价擒牛**
### 2. 设置参数
- **筛选数量**:选择展示的股票数量(默认5只)
### 3. 开始选股
点击 **🚀 开始低价擒牛选股** 按钮
系统将:
- 调用问财接口获取符合条件的股票
- 按成交额排序
- 展示前N只股票
### 4. 查看结果
#### 股票列表
- 展示每只股票的基本信息
- 包含股价、净利润增长率、成交额等关键指标
- 可下载完整数据CSV文件
#### 钉钉通知
如果配置了Webhook,系统会自动发送钉钉消息,包含:
- 筛选策略说明
- 精选股票列表
- 关键财务指标
### 5. 量化交易模拟
#### 策略模拟
点击 **🎮 开始策略模拟** 可以查看:
- 模拟买入信号
- 当前持仓情况
- 账户资金状态
#### 实盘交易(可选)
如需使用MiniQMT进行实盘交易:
1. 配置环境变量(详见环境配置)
2. 点击 **🔗 连接MiniQMT实盘**
3. 系统将自动执行买卖操作
## 配置说明
### Webhook通知配置
`.env` 文件中配置:
```bash
# Webhook通知配置
WEBHOOK_ENABLED=true
WEBHOOK_URL=your_dingtalk_webhook_url
WEBHOOK_TYPE=dingtalk
WEBHOOK_KEYWORD=aiagents通知
```
### MiniQMT交易配置
`.env` 文件中配置:
```bash
# MiniQMT配置
MINIQMT_ENABLED=true
MINIQMT_ACCOUNT_ID=your_account_id
```
## 风险提示
⚠️ **重要提醒**
1. **历史业绩不代表未来收益**
- 净利润高增长可能不可持续
- 低价股波动性较大
2. **交易成本**
- 实际交易存在滑点
- 需要考虑手续费和印花税
3. **市场风险**
- 深圳A股市场波动较大
- 小盘股流动性风险
4. **技术风险**
- MA均线信号可能滞后
- 5天持股周期可能错过更好的卖点
5. **策略局限**
- 本策略为机械化交易策略
- 无法应对突发事件(如重大利空)
## 策略改进建议
### 可优化方向
1. **动态仓位管理**
- 根据市场情绪调整仓位
- 引入风险评分机制
2. **止损机制**
- 添加固定比例止损(如-5%
- 避免单只股票亏损过大
3. **择时优化**
- 结合更多技术指标(MACD、RSI等)
- 考虑大盘走势
4. **基本面过滤**
- 添加财务指标筛选
- 排除财务造假风险股
## 常见问题
### Q1: 为什么只选深圳A股?
A: 深圳A股集中了大量中小盘股票,符合"低价高成长"的特征。上海主板以大盘蓝筹为主,不太符合本策略定位。
### Q2: 成交额排序的意义是什么?
A: 成交额小的股票往往还未被市场充分发现,具有更大的潜在空间。但也要注意流动性风险。
### Q3: 持股5天的依据是什么?
A: 5天是一个交易周(一周)的时间,可以让股票有足够的时间展现其趋势,同时避免长期持有的风险。
### Q4: 能否调整策略参数?
A: 当前版本参数固定,未来版本将支持自定义参数配置。
### Q5: 模拟交易和实盘有什么区别?
A: 模拟交易不考虑滑点、手续费等实际成本,仅供策略验证。实盘交易需要接入MiniQMT,会有实际的资金变动。
## 技术实现
### 核心模块
1. **low_price_bull_selector.py**
- 使用pywencai获取数据
- 数据清洗和格式化
2. **low_price_bull_strategy.py**
- 量化交易策略实现
- 仓位管理和买卖信号
3. **low_price_bull_ui.py**
- Streamlit界面展示
- 用户交互逻辑
### 数据流程
```
用户输入参数
调用pywencai接口
获取符合条件的股票
按成交额排序
展示股票列表
发送钉钉通知(可选)
执行量化策略(可选)
```
## 更新日志
### v1.0 (2024-12-12)
- ✅ 初始版本发布
- ✅ 实现基础选股功能
- ✅ 集成钉钉通知
- ✅ 量化策略模拟
- ✅ MiniQMT接口支持
## 联系与反馈
如有问题或建议,请通过以下方式反馈:
- GitHub Issues
- 系统内置反馈功能
---
**免责声明**:本功能仅供学习和研究使用,不构成任何投资建议。股市有风险,投资需谨慎。
+260
View File
@@ -0,0 +1,260 @@
# 低价擒牛选股板块 - 实现总结
## 实现日期
2024-12-12
## 功能概述
在左侧菜单栏"选股板块"下新增"低价擒牛"选股策略,支持低价高成长股票筛选和量化交易模拟。
## 核心功能
### 1. 选股策略
- ✅ 股价 < 10元
- ✅ 净利润增长率 ≥ 100%
- ✅ 非ST、非科创板、非创业板
- ✅ 深圳A股
- ✅ 按成交额由小至大排名
- ✅ 筛选前5名(可配置3-10只)
### 2. 钉钉通知
- ✅ 读取.env中的Webhook配置
- ✅ 自动发送选股结果到钉钉群
- ✅ 包含股票代码、价格、净利增长率等信息
### 3. 量化交易策略
- ✅ 初始资金:100万元
- ✅ 持股周期:5天
- ✅ 仓位控制:满仓
- ✅ 个股最大持仓:40%
- ✅ 最大持股数:4只
- ✅ 单日最大买入:2只
- ✅ 买入时机:开盘买入
- ✅ 卖出信号:MA5下穿MA20 或 持股满5天
### 4. 交易模拟
- ✅ 支持策略模拟(无需配置)
- ✅ 支持MiniQMT实盘对接(需配置)
- ✅ 显示买入信号、持仓、账户状态
## 新增文件
### Python模块
1. **low_price_bull_selector.py** (158行)
- 使用pywencai获取符合条件的股票
- 数据清洗和格式化
- 股票代码列表生成
2. **low_price_bull_strategy.py** (278行)
- 量化交易策略实现
- 买卖信号判断
- 仓位管理
- 交易历史记录
3. **low_price_bull_ui.py** (382行)
- Streamlit界面展示
- 用户交互逻辑
- 钉钉通知集成
- 策略模拟展示
### 文档
1. **docs/低价擒牛功能说明.md** (241行)
- 完整功能文档
- 策略详解
- 配置说明
- 风险提示
2. **docs/低价擒牛快速开始.md** (197行)
- 快速上手指南
- 典型使用场景
- 常见问题解答
## 修改文件
### app.py
修改位置:第293-380行(菜单导航区域)
**修改内容**
1. 在"选股板块"下添加"🐂 低价擒牛"按钮
2. 更新所有按钮的状态清除列表,增加`show_low_price_bull`
3. 添加路由检查,显示低价擒牛界面
**代码变更**
```python
# 添加导航按钮
if st.button("🐂 低价擒牛", width='stretch', key="nav_low_price_bull", help="低价高成长股票筛选策略"):
st.session_state.show_low_price_bull = True
# 清除其他页面标志
# 添加路由
if 'show_low_price_bull' in st.session_state and st.session_state.show_low_price_bull:
from low_price_bull_ui import display_low_price_bull
display_low_price_bull()
return
```
## 技术实现
### 数据获取
- 使用pywencai模块调用问财接口
- 查询语句:`"股价<10元,净利润增长率(净利润同比增长率)≥100%,非st,非科创板,非创业板,深圳A股,成交额由小至大排名"`
- 智能解析返回结果,兼容DataFrame/dict/list格式
### 通知集成
- 复用notification_service模块
- 支持钉钉/飞书Webhook
- 自动包含关键词"aiagents通知"
- Markdown格式消息展示
### 策略实现
- 基于面向对象设计
- 严格的仓位管理
- 清晰的买卖信号
- 完整的交易记录
### UI展示
- Streamlit expander组件展示详情
- DataFrame展示完整数据
- Metric组件展示关键指标
- CSV下载功能
## 环境变量配置
### Webhook通知(可选)
```bash
WEBHOOK_ENABLED=true
WEBHOOK_URL=your_dingtalk_webhook_url
WEBHOOK_TYPE=dingtalk
WEBHOOK_KEYWORD=aiagents通知
```
### MiniQMT实盘(可选)
```bash
MINIQMT_ENABLED=true
MINIQMT_ACCOUNT_ID=your_account_id
```
## 使用流程
1. **启动系统**
```bash
streamlit run app.py
```
2. **进入功能**
- 左侧菜单 → 🎯 选股板块 → 🐂 低价擒牛
3. **开始选股**
- 调整筛选数量
- 点击"🚀 开始低价擒牛选股"
4. **查看结果**
- 股票列表
- 统计信息
- 钉钉通知(如已配置)
5. **策略模拟**(可选)
- 点击"🎮 开始策略模拟"
- 查看买入信号和持仓
6. **实盘交易**(可选)
- 配置MiniQMT
- 点击"🔗 连接MiniQMT实盘"
## 特色亮点
### 1. 智能筛选
- 多维度条件过滤
- 成交额排序优先低热度股票
- 自动排除风险股(ST、科创板)
### 2. 完整策略
- 清晰的买卖规则
- 严格的风险控制
- 可复现的交易逻辑
### 3. 即时通知
- 自动发送钉钉消息
- 无需人工查看
- 移动端即时接收
### 4. 灵活配置
- 筛选数量可调
- 策略参数可改
- 支持模拟和实盘
## 注意事项
### 使用限制
1. ⚠️ 问财接口有频率限制,建议每次间隔30秒以上
2. ⚠️ 净利润增长率≥100%的股票可能较少
3. ⚠️ 策略模拟不包含手续费和滑点
### 风险提示
1. ⚠️ 本功能仅供学习研究使用
2. ⚠️ 不构成任何投资建议
3. ⚠️ 股市有风险,投资需谨慎
4. ⚠️ 历史业绩不代表未来收益
### 技术限制
1. ⚠️ 需要稳定的网络连接
2. ⚠️ pywencai模块需要正确安装
3. ⚠️ MiniQMT需要额外配置
## 后续优化方向
### 短期优化
- [ ] 添加历史选股记录
- [ ] 支持自定义筛选条件
- [ ] 增加更多技术指标
### 中期优化
- [ ] 回测功能
- [ ] 策略参数优化
- [ ] 多策略对比
### 长期优化
- [ ] AI选股建议
- [ ] 实时监控
- [ ] 自动化交易
## 测试建议
### 功能测试
1. ✅ 测试选股功能是否正常
2. ✅ 验证数据格式是否正确
3. ✅ 检查钉钉通知是否发送
4. ✅ 确认策略模拟逻辑
### 边界测试
1. ✅ 筛选不到股票时的处理
2. ✅ 网络异常时的错误处理
3. ✅ Webhook未配置时的提示
### 性能测试
1. ✅ 数据获取速度
2. ✅ UI渲染性能
3. ✅ 并发访问支持
## 相关文档
- [低价擒牛功能说明](./docs/低价擒牛功能说明.md)
- [低价擒牛快速开始](./docs/低价擒牛快速开始.md)
- [Webhook通知配置指南](./docs/Webhook通知配置指南.md)
- [MiniQMT集成指南](./docs/MINIQMT_INTEGRATION_GUIDE.md)
## 总结
本次实现完整地添加了"低价擒牛"选股板块,包含:
- ✅ 完整的选股策略
- ✅ 钉钉通知集成
- ✅ 量化交易策略
- ✅ 策略模拟功能
- ✅ 详细的文档说明
所有功能均已集成到现有系统中,与主力选股等其他板块保持一致的交互体验。
---
**实现完成时间**2024-12-12
**版本**v1.0
**状态**:✅ 已完成并可使用
+196
View File
@@ -0,0 +1,196 @@
# 低价擒牛 - 快速开始指南
## 🚀 5分钟上手
### 第一步:进入功能
1. 启动系统:`streamlit run app.py`
2. 在左侧菜单栏找到 **🎯 选股板块**
3. 点击 **🐂 低价擒牛**
### 第二步:开始选股
1. 调整筛选数量(默认5只,可调整3-10只)
2. 点击 **🚀 开始低价擒牛选股**
3. 等待30-60秒,系统自动获取数据
### 第三步:查看结果
系统会展示:
- ✅ 筛选数量和统计信息
- ✅ 精选股票详细列表
- ✅ 完整数据表格(可下载CSV
### 第四步:接收通知(可选)
如果配置了钉钉Webhook,会自动收到消息通知
## 📊 选股策略一览
| 条件 | 值 |
|------|-----|
| 股价 | < 10元 |
| 净利润增长率 | ≥ 100% |
| 市场 | 深圳A股 |
| 排除 | ST、科创板、创业板 |
| 排序 | 成交额由小到大 |
## 🎯 量化策略参数
| 参数 | 值 |
|------|-----|
| 初始资金 | 100万元 |
| 持股周期 | 5天 |
| 最大持股数 | 4只 |
| 个股最大仓位 | 40% |
| 单日最大买入 | 2只 |
| 买入时机 | 开盘 |
| 卖出信号 | MA5下穿MA20 或 持股5天 |
## 🔧 配置Webhook通知(可选)
### 1. 获取钉钉机器人Webhook URL
1. 打开钉钉群 → 群设置 → 智能群助手
2. 添加机器人 → 自定义
3. 添加安全设置 → 自定义关键词:`aiagents通知`
4. 复制Webhook URL
### 2. 配置环境变量
编辑项目根目录的 `.env` 文件:
```bash
# Webhook通知配置
WEBHOOK_ENABLED=true
WEBHOOK_URL=https://oapi.dingtalk.com/robot/send?access_token=xxxxx
WEBHOOK_TYPE=dingtalk
WEBHOOK_KEYWORD=aiagents通知
```
### 3. 重启系统
配置完成后重启Streamlit即可
## 🎮 使用量化策略模拟
### 策略模拟(无需配置)
1. 完成选股后,在页面底部找到 **量化交易策略模拟**
2. 点击 **🎮 开始策略模拟**
3. 系统会展示:
- 模拟买入信号
- 当前持仓情况
- 账户资金状态
### MiniQMT实盘(需要配置)
**前提条件**
- 已安装MiniQMT客户端
- 已开通量化交易账户
**配置步骤**
1. 编辑 `.env` 文件:
```bash
MINIQMT_ENABLED=true
MINIQMT_ACCOUNT_ID=your_account_id
```
2. 重启系统
3. 点击 **🔗 连接MiniQMT实盘**
## 📋 典型使用场景
### 场景1:每日早盘选股
**时间**:每天早上9:15-9:25
**操作**
1. 打开低价擒牛功能
2. 开始选股,获取最新数据
3. 查看筛选结果
4. 开盘后根据买入信号下单
### 场景2:周末策略回测
**时间**:周末
**操作**
1. 选股获取数据
2. 导出CSV文件
3. 使用Excel或Python进行历史数据分析
4. 优化策略参数
### 场景3:自动通知监控
**时间**:工作日定时
**操作**
1. 配置Webhook
2. 设置定时任务(cron或Task Scheduler
3. 每日自动选股并推送钉钉消息
## 💡 使用技巧
### 技巧1:成交额排序的意义
- 成交额小 = 市场关注度低 = 可能被低估
- 优先买入排名靠前的股票(成交额最小)
### 技巧2:持股周期管理
- 5天是一个交易周期
- 可在第3-4天关注卖出信号
- 避免持股到期临近时盲目持有
### 技巧3:仓位控制
- 建议新手先用小资金测试
- 逐步增加至策略设定的资金量
- 严格执行单股40%的仓位限制
### 技巧4:结合大盘走势
- 大盘上涨时:可适当激进
- 大盘下跌时:减少买入或观望
- 策略不能替代市场判断
## ⚠️ 常见问题
### Q: 为什么有时候筛选不到股票?
A: 可能原因:
- 市场环境不符合条件(如整体估值偏高)
- 净利润增长率≥100%的股票较少
- 建议适当放宽筛选条件
### Q: 钉钉通知没收到?
A: 检查清单:
- ✅ WEBHOOK_ENABLED 设置为 true
- ✅ WEBHOOK_URL 正确
- ✅ 钉钉机器人关键词包含"aiagents通知"
- ✅ 系统已重启
### Q: 策略模拟和实盘有什么区别?
A: 主要区别:
- 模拟:不实际下单,无手续费
- 实盘:真实下单,有滑点和手续费
- 建议先用模拟验证策略
### Q: 如何调整策略参数?
A: 当前版本参数固定在代码中,如需调整:
1. 编辑 `low_price_bull_strategy.py`
2. 修改 `__init__` 方法中的参数
3. 重启系统
## 📚 进阶阅读
- [低价擒牛功能说明](./低价擒牛功能说明.md) - 完整功能文档
- [Webhook配置指南](./Webhook通知配置指南.md) - 详细配置步骤
- [MiniQMT集成指南](./MINIQMT_INTEGRATION_GUIDE.md) - 量化交易配置
## 🎯 下一步
1. ✅ 熟悉基础选股功能
2. ✅ 配置钉钉通知
3. ✅ 使用策略模拟
4. ✅ 尝试MiniQMT实盘(谨慎)
5. ✅ 根据实际情况优化策略
---
**祝您投资顺利!** 🚀📈💰
@@ -0,0 +1,365 @@
# 低价擒牛策略监控功能实现总结
## 实现日期
2024-12-12
## 功能概述
为"低价擒牛"选股板块添加策略监控功能,实现自动监控持仓股票并在满足卖出条件时发送提醒。
## 核心功能
### 1. 策略监控
- ✅ 加入监控:在选出的股票列表中点击"加入策略监控"按钮
- ✅ 自动扫描:每分钟扫描1次(可配置)
- ✅ 持股天数监控:持股满5天第6天开盘提醒卖出
- ✅ 均线监控:MA5下穿MA20提醒卖出
- ✅ 自动移除:提醒卖出后自动移出监控列表
### 2. 数据源集成
- ✅ TDX数据源:读取股票K线数据
- ✅ 均线计算:自动计算MA5和MA20
- ✅ 实时价格:获取最新股票价格
### 3. 通知系统
- ✅ 钉钉通知:自动发送卖出提醒到钉钉群
- ✅ 面板提醒:在监控面板显示待处理提醒
- ✅ 历史记录:保存所有提醒记录
## 新增文件
### 核心模块(3个Python文件)
1. **low_price_bull_monitor.py** (363行)
- 监控数据库管理
- 股票添加/移除
- 提醒生成和管理
- 持有天数更新
2. **low_price_bull_service.py** (310行)
- 后台监控服务
- 定时扫描循环
- TDX数据获取
- MA均线检测
- 钉钉通知发送
3. **low_price_bull_monitor_ui.py** (273行)
- 监控面板UI
- 服务控制(启动/停止)
- 监控列表展示
- 卖出提醒展示
- 历史记录查询
### 文档(2个Markdown文件)
1. **低价擒牛策略监控配置说明.md**
- 环境变量配置
- TDX数据源配置
- 使用流程说明
- 常见问题解答
2. **低价擒牛策略监控功能实现总结.md**
- 功能概述
- 技术实现
- 使用说明
## 修改文件
### low_price_bull_ui.py
- ✅ 添加导入语句
- ✅ 添加"策略监控"按钮(右上角)
- ✅ 在股票详情中添加"加入策略监控"按钮
- ✅ 集成监控面板切换
- ✅ 更新策略说明
## 数据库设计
### 表1: monitored_stocks(监控列表)
```sql
CREATE TABLE monitored_stocks (
id INTEGER PRIMARY KEY,
stock_code TEXT NOT NULL, -- 股票代码
stock_name TEXT NOT NULL, -- 股票名称
buy_price REAL NOT NULL, -- 买入价格
buy_date TEXT NOT NULL, -- 买入日期
holding_days INTEGER DEFAULT 0, -- 持有天数
status TEXT DEFAULT 'holding', -- 状态(holding/removed
add_time TEXT NOT NULL, -- 加入时间
remove_time TEXT, -- 移除时间
remove_reason TEXT, -- 移除原因
UNIQUE(stock_code, status)
)
```
### 表2: sell_alerts(卖出提醒)
```sql
CREATE TABLE sell_alerts (
id INTEGER PRIMARY KEY,
stock_code TEXT NOT NULL, -- 股票代码
stock_name TEXT NOT NULL, -- 股票名称
alert_type TEXT NOT NULL, -- 提醒类型(holding_days/ma_cross
alert_reason TEXT NOT NULL, -- 提醒原因
current_price REAL, -- 当前价格
ma5 REAL, -- MA5值
ma20 REAL, -- MA20值
holding_days INTEGER, -- 持有天数
alert_time TEXT NOT NULL, -- 提醒时间
is_sent INTEGER DEFAULT 0 -- 是否已发送
)
```
## 技术实现
### 1. 监控服务架构
```
┌─────────────────────────────────────┐
│ LowPriceBullService │
│ (后台监控服务) │
└──────────────┬──────────────────────┘
┌──────▼──────┐
│ 监控循环 │
│ (每60秒) │
└──────┬──────┘
┌──────────▼─────────────┐
│ 扫描监控列表 │
│ - 更新持有天数 │
│ - 检查卖出条件 │
└──────────┬──────────────┘
┌──────────▼────────────────┐
│ 检测卖出信号 │
│ 1. 持股天数≥5 │
│ 2. MA5 < MA20 │
└──────────┬─────────────────┘
┌──────────▼──────────────┐
│ 生成卖出提醒 │
│ - 保存到数据库 │
│ - 发送钉钉通知 │
│ - 自动移除股票 │
└───────────────────────────┘
```
### 2. TDX数据获取流程
```python
def _get_stock_data(stock_code):
# 1. 获取K线数据(30天)
df = get_stock_kline_data(stock_code, period='daily', count=30)
# 2. 计算均线
df['MA5'] = df['close'].rolling(window=5).mean()
df['MA20'] = df['close'].rolling(window=20).mean()
# 3. 返回最新数据
latest = df.iloc[-1]
return latest['close'], latest['MA5'], latest['MA20']
```
### 3. 卖出信号检测
```python
def _check_stock(stock):
# 检查1: 持股天数
if holding_days >= 5:
add_sell_alert(type='holding_days')
return
# 检查2: MA5下穿MA20
current_price, ma5, ma20 = _get_stock_data(stock_code)
if ma5 < ma20:
add_sell_alert(type='ma_cross')
```
## 环境变量配置
### 新增配置项
```bash
# 低价擒牛策略监控
LOW_PRICE_BULL_SCAN_INTERVAL=60 # 扫描间隔(秒)
LOW_PRICE_BULL_HOLDING_DAYS=5 # 持股天数限制
```
### 依赖的配置
```bash
# Webhook通知(建议配置)
WEBHOOK_ENABLED=true
WEBHOOK_URL=https://oapi.dingtalk.com/robot/send?access_token=xxxxx
WEBHOOK_TYPE=dingtalk
WEBHOOK_KEYWORD=aiagents通知
```
## 使用流程
### 1. 启动流程
```
1. 低价擒牛选股
2. 查看股票列表
3. 点击" 加入策略监控"
4. 点击右上角"📊 策略监控"
5. 点击"▶️ 启动监控服务"
6. 监控服务开始运行
```
### 2. 监控流程
```
监控服务 → 每60秒扫描 → 检测条件 → 生成提醒 → 发送通知 → 自动移除
```
### 3. 提醒流程
```
满足卖出条件
生成卖出提醒
┌─────────┴─────────┐
│ │
发送钉钉通知 显示在面板
│ │
└─────────┬─────────┘
自动移出监控列表
记录到历史
```
## 功能特点
### 1. 自动化监控
- ✅ 无需人工盯盘
- ✅ 定时自动扫描
- ✅ 智能卖出提醒
### 2. 双重条件
- ✅ 时间条件:持股5天
- ✅ 技术条件:MA5下穿MA20
- ✅ 满足任一即提醒
### 3. 灵活配置
- ✅ 可调整扫描间隔
- ✅ 可调整持股天数
- ✅ 可启动/停止服务
### 4. 完整记录
- ✅ 监控历史
- ✅ 提醒记录
- ✅ 操作日志
## 监控面板功能
### 📋 监控列表
- 显示所有监控中的股票
- 实时更新持有天数
- 支持批量移除
- 显示买入价格和日期
### 🔔 卖出提醒
- 显示待处理提醒
- 查看MA5/MA20数据
- 操作选项:
- ✅ 已处理(移除股票)
- ❌ 忽略(保留股票)
### 📜 历史记录
- 查看历史提醒
- 筛选已发送/未发送
- 清理旧记录
### ⚙️ 服务控制
- ▶️ 启动服务
- ⏸️ 停止服务
- 配置扫描间隔
- 查看服务状态
## 钉钉通知格式
```markdown
### aiagents通知 - 低价擒牛卖出提醒
**股票代码**: 002259
**股票名称**: 升达林业
**提醒类型**: MA均线死叉
**提醒原因**: MA5下穿MA20,技术信号卖出
**当前价格**: 8.52元
**MA5**: 8.45
**MA20**: 8.67
**持有天数**: 3天
**提醒时间**: 2024-12-12 14:30:00
---
**建议**: 开盘时卖出该股票
_此消息由AI股票分析系统自动发送_
```
## 注意事项
### 1. 服务运行
- ⚠️ 监控服务需要手动启动
- ⚠️ 关闭浏览器后服务停止
- ⚠️ 建议在服务器上运行
### 2. 数据源
- ⚠️ 需要配置TDX数据源
- ⚠️ 建议使用pytdx(无需配置)
- ⚠️ K线数据可能有延迟
### 3. 扫描频率
- ⚠️ 默认60秒扫描1次
- ⚠️ 不建议低于30秒
- ⚠️ 避免API限流
### 4. 持股天数
- ⚠️ 按自然日计算
- ⚠️ 不区分交易日
- ⚠️ 建议预留余地
## 后续优化方向
### 短期
- [ ] 支持后台常驻服务
- [ ] 增加更多技术指标
- [ ] 支持自定义卖出条件
### 中期
- [ ] 集成MiniQMT自动交易
- [ ] 支持止损止盈设置
- [ ] 增加回测功能
### 长期
- [ ] AI智能卖出建议
- [ ] 多策略组合监控
- [ ] 移动端APP推送
## 总结
本次实现为"低价擒牛"选股板块添加了完整的策略监控功能,包括:
- ✅ **3个核心模块**:监控管理、后台服务、UI界面
- ✅ **完整的数据库设计**:监控列表、提醒记录
- ✅ **自动化监控**:定时扫描、智能提醒、自动移除
- ✅ **双重卖出条件**:持股天数 + MA均线
- ✅ **钉钉通知集成**:实时接收卖出提醒
- ✅ **灵活的配置**:可调整扫描间隔和持股天数
- ✅ **完善的文档**:配置说明、使用指南
整个功能已集成到系统中,用户可以立即使用!
---
**实现完成时间**2024-12-12
**版本**v1.0
**状态**:✅ 已完成并可使用
@@ -0,0 +1,239 @@
# 低价擒牛策略监控配置说明
## 环境变量配置
`.env` 文件中添加以下配置:
```bash
# 低价擒牛策略监控配置
LOW_PRICE_BULL_SCAN_INTERVAL=60 # 扫描间隔(秒),默认60秒
LOW_PRICE_BULL_HOLDING_DAYS=5 # 持股天数限制,默认5天
# TDX数据源API配置(必须)
TDX_BASE_URL=http://127.0.0.1:5000 # TDX API服务地址
```
## TDX数据源配置
策略监控需要使用TDX数据API获取股票K线数据和均线指标。
### 配置方法
`.env` 文件中添加TDX API URL
```bash
# TDX数据源API配置
TDX_BASE_URL=http://127.0.0.1:5000 # TDX API服务地址
```
### API接口说明
#### 核心接口
| 接口 | 说明 | 示例 |
|------|------|------|
| /api/quote | 五档行情 | ?code=000001 |
| /api/kline | K线数据 | ?code=000001&type=day |
| /api/minute | 分时数据 | ?code=000001 |
| /api/trade | 分时成交 | ?code=000001 |
| /api/search | 搜索股票 | ?keyword=平安 |
| /api/stock-info | 综合信息 | ?code=000001 |
#### K线数据接口
策略监控使用的接口:**`/api/kline`**
**请求参数**
- `code`: 股票代码(例如:000001
- `type`: K线类型(day=日K线)
**返回格式**
```json
[
{
"date": "2024-12-12",
"open": 10.50,
"high": 10.80,
"low": 10.30,
"close": 10.60,
"volume": 1000000
},
...
]
```
**使用示例**
```bash
curl "http://127.0.0.1:5000/api/kline?code=000001&type=day"
```
### 启动TDX API服务
如果您还没有TDX API服务,需要先启动服务:
1. 确保已安装相关依赖
2. 启动TDX API服务器(默认端口5000)
3. 在.env中配置服务地址
### 数据说明
- **更新频率**K线数据实时更新
- **数据量**:策略需要至少20天的K线数据
- **均线计算**:系统自动计算MA5和MA20
## Webhook通知配置
建议配置钉钉Webhook以接收卖出提醒:
```bash
# Webhook通知配置
WEBHOOK_ENABLED=true
WEBHOOK_URL=https://oapi.dingtalk.com/robot/send?access_token=xxxxx
WEBHOOK_TYPE=dingtalk
WEBHOOK_KEYWORD=aiagents通知
```
## 功能说明
### 1. 加入策略监控
在"低价擒牛"选股结果中,点击任意股票详情中的"➕ 加入策略监控"按钮。
### 2. 监控条件
#### 条件1:持股天数到期
- 持股满5天(可配置)
- 第6天开盘时发送卖出提醒
- 自动从监控列表移除
#### 条件2MA5下穿MA20
- 每分钟扫描1次(可配置)
- 检测MA5是否小于MA20
- 满足条件时发送卖出提醒
- 自动从监控列表移除
### 3. 卖出提醒
提醒会通过以下方式发送:
- 钉钉Webhook通知(需配置)
- 监控面板中的"卖出提醒"标签页
### 4. 自动移除
当满足卖出条件时:
1. 生成卖出提醒
2. 发送钉钉通知(如已配置)
3. 自动从监控列表移除
4. 记录到历史提醒
## 使用流程
```
1. 低价擒牛选股
2. 查看选股结果
3. 点击" 加入策略监控"
4. 进入"📊 策略监控"面板
5. 启动监控服务
6. 等待卖出提醒
7. 收到提醒后卖出
```
## 监控面板功能
### 服务控制
- ▶️ 启动监控服务
- ⏸️ 停止监控服务
- ⚙️ 配置扫描间隔
### 监控列表
- 查看所有监控中的股票
- 显示持有天数
- 手动移除股票
### 卖出提醒
- 查看待处理的提醒
- 查看MA5/MA20数据
- 标记已处理/忽略
### 历史记录
- 查看历史提醒
- 清理旧记录
## 注意事项
1. ⚠️ **监控服务需要手动启动**
- 首次使用需在监控面板启动服务
- 服务启动后会持续运行
- 关闭浏览器后服务会停止
2. ⚠️ **TDX数据源**
- 必须配置TDX_BASE_URL
- 确俜TDX API服务已启动
- 使用`/api/kline`接口获取日K线数据
- 数据需要至少20天才能计算MA20
3. ⚠️ **扫描间隔**
- 默认60秒扫描1次
- 可在监控面板调整
- 建议不要低于30秒
4. ⚠️ **持股天数计算**
- 按自然日计算
- 不区分交易日/非交易日
- 建议留有余地
## 常见问题
### Q1: 监控服务无法启动?
A: 检查:
- Python环境是否正常
- 依赖包是否安装完整
- 查看控制台错误日志
### Q2: 没有收到卖出提醒?
A: 检查:
- Webhook是否配置正确
- 监控服务是否在运行
- TDX API服务是否启动
- 访问`http://127.0.0.1:5000/api/health`检查API状态
- 查看控制台日志是否有错误信息
### Q3: 如何修改扫描间隔?
A: 两种方式:
- 在监控面板的"⚙️ 监控配置"中修改
- 在.env文件中修改`LOW_PRICE_BULL_SCAN_INTERVAL`
### Q4: 持股天数如何调整?
A: 在.env文件中修改:
```bash
LOW_PRICE_BULL_HOLDING_DAYS=7 # 改为7天
```
## 技术实现
### 数据库
- SQLite本地数据库
- 文件:`low_price_bull_monitor.db`
- 包含监控列表和提醒记录
### 后台服务
- Python线程实现
- 定时扫描循环
- 异常自动恢复
### 数据获取
- 使用TDX API
- 调用`/api/kline`接口
- 获取日K线数据
- 计算MA5/MA20均线
---
**版本**: v1.0
**更新日期**: 2024-12-12
+460
View File
@@ -0,0 +1,460 @@
'''
均线回归策略 V3.0 进攻版
优化重点提高Beta增强进攻性减少过度保守
核心改进
1. 放宽市场择时只在极端熊市降仓
2. 增加持仓数量提高资金利用率
3. 放宽买入条件增加交易机会
4. 优化止盈让利润奔跑
5. 增加突破买入模式
'''
import jqdata
import numpy as np
## 初始化函数
def initialize(context):
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式
set_option('use_real_price', True)
# 设定成交量比例
set_option('order_volume_ratio', 1)
# 设置交易手续费
set_order_cost(OrderCost(open_tax=0, close_tax=0.001,
open_commission=0.0003, close_commission=0.0003,
close_today_commission=0, min_commission=5), type='stock')
# ========== 策略参数(进攻版)==========
g.stocknum = 8 # 持仓数量增加到8只
g.max_position_ratio = 0.98 # 最大仓位98%
# 均线参数
g.ma_short = 20 # 短期均线
g.ma_mid = 60 # 中期均线
g.ma_long = 120 # 长期均线
# 回调买入参数(放宽条件)
g.pullback_ratio = 0.035 # 放宽到3.5%
g.break_tolerance = 0.015 # 允许跌破1.5%
# 止盈止损参数(让利润奔跑)
g.base_take_profit = 0.20 # 止盈提高到20%
g.base_stop_loss = 0.06 # 止损放宽到6%
g.trailing_stop = 0.08 # 回撤容忍8%
# 市值筛选(亿)- 扩大范围
g.min_market_cap = 20
g.max_market_cap = 1000
# 行业分散参数
g.max_industry_stocks = 3 # 同行业最多3只
# 市场状态
g.market_state = 'NORMAL'
g.position_ratio = 1.0
# 记录
g.highest_profit = {}
g.stock_volatility = {}
g.hold_days = {} # 持仓天数
# 定时任务
run_daily(market_analysis, '09:31')
run_daily(morning_check, '09:35')
run_daily(check_positions, '14:00') # 只检查一次
run_daily(afternoon_trade, '14:50')
## 市场环境分析(放宽版)
def market_analysis(context):
"""
只在极端情况下降低仓位大部分时间保持高仓位
"""
index_code = '000300.XSHG'
try:
df = attribute_history(index_code, 130, '1d', ['close'], skip_paused=True)
if len(df) < 120:
g.market_state = 'NORMAL'
g.position_ratio = 1.0
return
close = df['close'].values
current = close[-1]
ma20 = np.mean(close[-20:])
ma60 = np.mean(close[-60:])
ma120 = np.mean(close[-120:])
change_5d = (close[-1] - close[-6]) / close[-6]
change_10d = (close[-1] - close[-11]) / close[-11]
# 只在极端熊市才降仓(条件更严格)
if current < ma120 and ma20 < ma60 < ma120 and change_10d < -0.08:
# 极端熊市:指数跌破120日均线,均线空头,10日跌幅超8%
g.market_state = 'BEAR'
g.position_ratio = 0.5
log.info("【市场】极端熊市,仓位50%%")
elif change_5d < -0.08:
# 短期暴跌超8%:暂时减仓
g.market_state = 'BEAR'
g.position_ratio = 0.6
log.info("【市场】短期暴跌,仓位60%%")
elif current > ma20 and ma20 > ma60:
# 趋势向上:满仓
g.market_state = 'BULL'
g.position_ratio = 1.0
log.info("【市场】趋势向上,满仓")
else:
# 默认高仓位运行
g.market_state = 'NORMAL'
g.position_ratio = 0.9
log.info("【市场】正常状态,仓位90%%")
except Exception as e:
g.market_state = 'NORMAL'
g.position_ratio = 0.9
## 早盘检查
def morning_check(context):
g.buy_list = check_stocks(context)
log.info("【选股】%d 只,市场: %s,仓位: %.0f%%" %
(len(g.buy_list), g.market_state, g.position_ratio * 100))
## 检查持仓 - 优化版(让利润奔跑)
def check_positions(context):
if len(context.portfolio.positions) == 0:
return
for stock in list(context.portfolio.positions.keys()):
position = context.portfolio.positions[stock]
if position.closeable_amount <= 0:
continue
cost = position.avg_cost
current_price = position.price
if cost <= 0:
continue
profit_ratio = (current_price - cost) / cost
# 更新持仓天数
g.hold_days[stock] = g.hold_days.get(stock, 0) + 1
# 更新最高盈利
if stock not in g.highest_profit:
g.highest_profit[stock] = profit_ratio
else:
g.highest_profit[stock] = max(g.highest_profit[stock], profit_ratio)
highest = g.highest_profit[stock]
hold_days = g.hold_days.get(stock, 0)
# 动态止损(根据波动率和持仓时间)
volatility = g.stock_volatility.get(stock, 0.025)
# 持仓时间越长,止损越宽松
time_factor = min(1 + hold_days * 0.01, 1.5)
dynamic_stop_loss = min(g.base_stop_loss * time_factor, 0.12)
# === 止损逻辑 ===
if profit_ratio < -dynamic_stop_loss:
log.info("【止损】%s 亏损 %.2f%%" % (stock, profit_ratio * 100))
order_target(stock, 0)
clean_stock_data(stock)
continue
# === 均线破位(只在亏损时卖)===
if profit_ratio < 0 and check_sell_signal(stock):
log.info("【均线破位】%s 破位且亏损,卖出" % stock)
order_target(stock, 0)
clean_stock_data(stock)
continue
# === 移动止盈(让利润奔跑)===
if highest >= g.base_take_profit:
drawdown = highest - profit_ratio
# 盈利越多,允许回撤越大(最多允许回撤15%)
allowed_drawdown = min(g.trailing_stop + highest * 0.4, 0.18)
if drawdown >= allowed_drawdown:
log.info("【移动止盈】%s 最高 %.2f%%,回撤 %.2f%%" %
(stock, highest * 100, drawdown * 100))
order_target(stock, 0)
clean_stock_data(stock)
continue
# === 分批止盈(保守止盈,只卖小部分)===
if profit_ratio >= g.base_take_profit * 2:
# 盈利超40%,卖出30%锁定利润
sell_amount = int(position.closeable_amount * 0.3 / 100) * 100
if sell_amount >= 100:
log.info("【大幅止盈】%s 盈利 %.2f%%,卖出30%%" % (stock, profit_ratio * 100))
order(stock, -sell_amount)
## 清理股票数据
def clean_stock_data(stock):
for d in [g.highest_profit, g.stock_volatility, g.hold_days]:
if stock in d:
del d[stock]
## 尾盘交易
def afternoon_trade(context):
buy_stocks(context)
## 买入函数(积极版)
def buy_stocks(context):
if not hasattr(g, 'buy_list') or not g.buy_list:
return
position_count = len(context.portfolio.positions)
adjusted_stocknum = int(g.stocknum * g.position_ratio)
if adjusted_stocknum <= 0 or position_count >= adjusted_stocknum:
return
# 计算资金分配
available_cash = context.portfolio.available_cash * g.max_position_ratio
buy_count = min(adjusted_stocknum - position_count, len(g.buy_list))
if buy_count <= 0 or available_cash < 10000:
return
cash_per_stock = available_cash / buy_count
held_industries = get_held_industries(context)
bought = 0
for stock in g.buy_list:
if bought >= buy_count:
break
if stock in context.portfolio.positions:
continue
# 行业分散
stock_industry = get_stock_industry(stock)
if stock_industry and held_industries.get(stock_industry, 0) >= g.max_industry_stocks:
continue
# 简化买入条件检查
if check_buy_signal(stock):
order_value(stock, cash_per_stock)
log.info("【买入】%s" % stock)
g.highest_profit[stock] = 0
g.stock_volatility[stock] = 0.025
g.hold_days[stock] = 0
if stock_industry:
held_industries[stock_industry] = held_industries.get(stock_industry, 0) + 1
bought += 1
## 获取已持仓的行业分布
def get_held_industries(context):
industries = {}
for stock in context.portfolio.positions.keys():
ind = get_stock_industry(stock)
if ind:
industries[ind] = industries.get(ind, 0) + 1
return industries
## 获取股票所属行业
def get_stock_industry(stock):
try:
ind_dict = get_industry(stock)
if ind_dict and stock in ind_dict:
# 获取申万一级行业
for ind_code, ind_info in ind_dict[stock].items():
if ind_code.startswith('sw_l1'):
return ind_info.get('industry_name', None)
return None
except:
return None
## 选股函数(进攻版)
def check_stocks(context):
# 第一步:基础筛选(放宽条件)
q = query(
valuation.code,
valuation.market_cap
).filter(
valuation.market_cap.between(g.min_market_cap, g.max_market_cap)
).order_by(
valuation.market_cap.asc()
).limit(500) # 扩大候选池
df = get_fundamentals(q)
if df.empty:
return []
stock_list = list(df['code'])
stock_list = filter_basic(stock_list)
# 均线信号筛选
candidates = []
for stock in stock_list[:200]: # 检查前200只
if check_buy_signal(stock):
score = calculate_trend_score(stock)
candidates.append((stock, score))
# 按评分排序
candidates.sort(key=lambda x: x[1], reverse=True)
buy_list = [c[0] for c in candidates[:g.stocknum * 3]]
return buy_list
## 计算趋势强度评分
def calculate_trend_score(stock):
"""
评分因素
1. 均线发散程度
2. 价格距离MA20的位置
3. 成交量配合
"""
try:
df = attribute_history(stock, 130, '1d', ['close', 'volume'], skip_paused=True)
if len(df) < 120:
return 0
close = df['close'].values
volume = df['volume'].values
ma20 = np.mean(close[-20:])
ma60 = np.mean(close[-60:])
ma120 = np.mean(close[-120:])
current = close[-1]
score = 0
# 均线发散程度(20分)
spread = (ma20 - ma120) / ma120
score += min(spread * 100, 20)
# 价格位置(30分)- 越接近MA20越好
distance = abs(current - ma20) / ma20
score += max(0, 30 - distance * 500)
# 成交量(20分)- 回调缩量为佳
vol_ma5 = np.mean(volume[-5:])
vol_ma20 = np.mean(volume[-20:])
if vol_ma5 < vol_ma20 * 0.8:
score += 20 # 缩量回调
elif vol_ma5 < vol_ma20:
score += 10
# 趋势持续性(30分)
ma20_slope = (ma20 - np.mean(close[-25:-5])) / np.mean(close[-25:-5])
if ma20_slope > 0:
score += min(ma20_slope * 300, 30)
return score
except:
return 0
## 基础过滤
def filter_basic(stock_list):
if not stock_list:
return []
current_data = get_current_data()
filtered = []
for stock in stock_list:
# 过滤停牌
if current_data[stock].paused:
continue
# 过滤ST
if current_data[stock].is_st:
continue
if 'ST' in current_data[stock].name or '*' in current_data[stock].name:
continue
# 过滤科创板、北交所
if stock.startswith('688') or stock.startswith('8') or stock.startswith('4'):
continue
# 过滤涨跌停
if current_data[stock].last_price >= current_data[stock].high_limit:
continue
if current_data[stock].last_price <= current_data[stock].low_limit:
continue
filtered.append(stock)
return filtered
## 检查买入信号(放宽版)
def check_buy_signal(stock):
"""
放宽买入条件增加交易机会
"""
try:
df = attribute_history(stock, g.ma_long + 10, '1d', ['close'], skip_paused=True)
if len(df) < g.ma_long:
return False
close = df['close'].values
current_price = close[-1]
# 计算均线
ma20 = np.mean(close[-g.ma_short:])
ma60 = np.mean(close[-g.ma_mid:])
ma120 = np.mean(close[-g.ma_long:])
# 条件1:均线多头排列(核心条件)
if not (ma20 > ma60 > ma120):
return False
# 条件2MA20向上
ma20_5d_ago = np.mean(close[-g.ma_short-5:-5])
if ma20 <= ma20_5d_ago:
return False
# 条件3:价格在MA20附近(放宽范围)
distance = (current_price - ma20) / ma20
# 允许在MA20上方5%以内,或跌破2%以内
if distance > 0.05 or distance < -0.02:
return False
# 条件4:价格不能离MA60太远
if current_price > ma60 * 1.30:
return False
return True
except:
return False
## 检查卖出信号(均线破位)
def check_sell_signal(stock):
"""
卖出条件
1. 价格跌破MA60
2. MA20下穿MA60死叉
3. 价格跌破MA20且持续3天
"""
try:
df = attribute_history(stock, g.ma_mid + 5, '1d', ['close'], skip_paused=True)
if len(df) < g.ma_mid:
return False
close = df['close'].values
current_price = close[-1]
ma20 = np.mean(close[-g.ma_short:])
ma60 = np.mean(close[-g.ma_mid:])
# 价格跌破MA60超过2%
if current_price < ma60 * 0.98:
return True
# MA20下穿MA60(死叉)
ma20_yesterday = np.mean(close[-g.ma_short-1:-1])
ma60_yesterday = np.mean(close[-g.ma_mid-1:-1])
if ma20_yesterday > ma60_yesterday and ma20 < ma60:
return True
# 连续3天收盘在MA20下方
ma20_3d = [np.mean(close[-g.ma_short-i:-i]) if i > 0 else ma20 for i in range(3)]
below_ma20_count = sum(1 for i in range(3) if close[-1-i] < ma20_3d[i] * 0.99)
if below_ma20_count >= 3:
return True
return False
except:
return False
BIN
View File
Binary file not shown.
+4 -1
View File
@@ -23,7 +23,9 @@ class LonghubangDataFetcher:
api_key: StockAPI的API密钥可选普通请求每日免费1000次
"""
print("[智瞰龙虎] 龙虎榜数据获取器初始化...")
self.base_url = "https://www.stockapi.com.cn/v1"
# self.base_url = "https://api-lhb.zhongdu.net"
self.base_url = "http://lhb-api.ws4.cn/v1"
# self.base_url = "https://www.stockapi.com.cn/v1"
self.api_key = api_key
self.max_retries = 3 # 最大重试次数
self.retry_delay = 2 # 重试延迟(秒)
@@ -79,6 +81,7 @@ class LonghubangDataFetcher:
"""
print(f"[智瞰龙虎] 获取 {date} 的龙虎榜数据...")
# url = f"{self.base_url}"
url = f"{self.base_url}/youzi/all"
params = {'date': date}
Binary file not shown.
+375
View File
@@ -0,0 +1,375 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
低价擒牛策略监控模块
监控持仓股票的卖出信号
"""
import sqlite3
import pandas as pd
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
import logging
import os
class LowPriceBullMonitor:
"""低价擒牛策略监控器"""
def __init__(self, db_path: str = "low_price_bull_monitor.db"):
"""
初始化监控器
Args:
db_path: 数据库文件路径
"""
self.logger = logging.getLogger(__name__)
self.db_path = db_path
self._init_database()
def _init_database(self):
"""初始化数据库"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 创建监控列表表
cursor.execute("""
CREATE TABLE IF NOT EXISTS monitored_stocks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
stock_code TEXT NOT NULL,
stock_name TEXT NOT NULL,
buy_price REAL NOT NULL,
buy_date TEXT NOT NULL,
holding_days INTEGER DEFAULT 0,
status TEXT DEFAULT 'holding',
add_time TEXT NOT NULL,
remove_time TEXT,
remove_reason TEXT,
UNIQUE(stock_code, status)
)
""")
# 创建卖出提醒表
cursor.execute("""
CREATE TABLE IF NOT EXISTS sell_alerts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
stock_code TEXT NOT NULL,
stock_name TEXT NOT NULL,
alert_type TEXT NOT NULL,
alert_reason TEXT NOT NULL,
current_price REAL,
ma5 REAL,
ma20 REAL,
holding_days INTEGER,
alert_time TEXT NOT NULL,
is_sent INTEGER DEFAULT 0
)
""")
conn.commit()
conn.close()
self.logger.info("低价擒牛监控数据库初始化完成")
def add_stock(self, stock_code: str, stock_name: str, buy_price: float,
buy_date: str = None) -> Tuple[bool, str]:
"""
添加股票到监控列表
Args:
stock_code: 股票代码不含后缀
stock_name: 股票名称
buy_price: 买入价格
buy_date: 买入日期格式YYYY-MM-DD
Returns:
(是否成功, 消息)
"""
try:
if buy_date is None:
buy_date = datetime.now().strftime("%Y-%m-%d")
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 检查是否已存在
cursor.execute("""
SELECT id FROM monitored_stocks
WHERE stock_code = ? AND status = 'holding'
""", (stock_code,))
if cursor.fetchone():
conn.close()
return False, f"股票 {stock_code} 已在监控列表中"
# 添加到监控列表
cursor.execute("""
INSERT INTO monitored_stocks
(stock_code, stock_name, buy_price, buy_date, add_time)
VALUES (?, ?, ?, ?, ?)
""", (stock_code, stock_name, buy_price, buy_date,
datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
conn.commit()
conn.close()
self.logger.info(f"添加股票到监控: {stock_code} {stock_name}")
return True, f"成功添加 {stock_code} {stock_name} 到监控列表"
except Exception as e:
self.logger.error(f"添加股票失败: {e}")
return False, f"添加失败: {str(e)}"
def remove_stock(self, stock_code: str, reason: str = "手动移除") -> Tuple[bool, str]:
"""
从监控列表移除股票
Args:
stock_code: 股票代码
reason: 移除原因
Returns:
(是否成功, 消息)
"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 先检查是否存在持仓中的股票
cursor.execute("""
SELECT id FROM monitored_stocks
WHERE stock_code = ? AND status = 'holding'
""", (stock_code,))
if not cursor.fetchone():
conn.close()
return False, f"股票 {stock_code} 不在监控列表中"
# 先删除该股票的所有'removed'记录(避免UNIQUE约束冲突)
cursor.execute("""
DELETE FROM monitored_stocks
WHERE stock_code = ? AND status = 'removed'
""", (stock_code,))
# 然后更新持仓中的记录为'removed'
cursor.execute("""
UPDATE monitored_stocks
SET status = 'removed',
remove_time = ?,
remove_reason = ?
WHERE stock_code = ? AND status = 'holding'
""", (datetime.now().strftime("%Y-%m-%d %H:%M:%S"), reason, stock_code))
conn.commit()
conn.close()
self.logger.info(f"移除股票: {stock_code}, 原因: {reason}")
return True, f"成功移除 {stock_code}"
except Exception as e:
self.logger.error(f"移除股票失败: {e}")
return False, f"移除失败: {str(e)}"
def get_monitored_stocks(self) -> List[Dict]:
"""
获取所有监控中的股票
Returns:
股票列表
"""
try:
conn = sqlite3.connect(self.db_path)
df = pd.read_sql_query("""
SELECT * FROM monitored_stocks
WHERE status = 'holding'
ORDER BY add_time DESC
""", conn)
conn.close()
return df.to_dict('records') if not df.empty else []
except Exception as e:
self.logger.error(f"获取监控列表失败: {e}")
return []
def update_holding_days(self):
"""更新所有股票的持有天数"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 获取所有持仓股票
cursor.execute("""
SELECT stock_code, buy_date FROM monitored_stocks
WHERE status = 'holding'
""")
stocks = cursor.fetchall()
today = datetime.now().date()
for stock_code, buy_date in stocks:
buy_date_obj = datetime.strptime(buy_date, "%Y-%m-%d").date()
holding_days = (today - buy_date_obj).days
cursor.execute("""
UPDATE monitored_stocks
SET holding_days = ?
WHERE stock_code = ? AND status = 'holding'
""", (holding_days, stock_code))
conn.commit()
conn.close()
self.logger.info("持有天数更新完成")
except Exception as e:
self.logger.error(f"更新持有天数失败: {e}")
def add_sell_alert(self, stock_code: str, stock_name: str, alert_type: str,
alert_reason: str, current_price: float = None,
ma5: float = None, ma20: float = None,
holding_days: int = None) -> bool:
"""
添加卖出提醒
Args:
stock_code: 股票代码
stock_name: 股票名称
alert_type: 提醒类型holding_days/ma_cross
alert_reason: 提醒原因
current_price: 当前价格
ma5: MA5值
ma20: MA20值
holding_days: 持有天数
Returns:
是否成功
"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
# 检查是否已存在相同的提醒
cursor.execute("""
SELECT id FROM sell_alerts
WHERE stock_code = ? AND alert_type = ? AND is_sent = 0
""", (stock_code, alert_type))
if cursor.fetchone():
conn.close()
return False
cursor.execute("""
INSERT INTO sell_alerts
(stock_code, stock_name, alert_type, alert_reason,
current_price, ma5, ma20, holding_days, alert_time)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
""", (stock_code, stock_name, alert_type, alert_reason,
current_price, ma5, ma20, holding_days,
datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
conn.commit()
conn.close()
self.logger.info(f"添加卖出提醒: {stock_code} - {alert_reason}")
return True
except Exception as e:
self.logger.error(f"添加卖出提醒失败: {e}")
return False
def get_pending_alerts(self) -> List[Dict]:
"""
获取待发送的提醒
Returns:
提醒列表
"""
try:
conn = sqlite3.connect(self.db_path)
df = pd.read_sql_query("""
SELECT * FROM sell_alerts
WHERE is_sent = 0
ORDER BY alert_time DESC
""", conn)
conn.close()
return df.to_dict('records') if not df.empty else []
except Exception as e:
self.logger.error(f"获取提醒失败: {e}")
return []
def mark_alert_sent(self, alert_id: int):
"""标记提醒已发送"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("""
UPDATE sell_alerts
SET is_sent = 1
WHERE id = ?
""", (alert_id,))
conn.commit()
conn.close()
except Exception as e:
self.logger.error(f"标记提醒失败: {e}")
def get_history_alerts(self, limit: int = 50) -> List[Dict]:
"""
获取历史提醒记录
Args:
limit: 返回记录数
Returns:
提醒列表
"""
try:
conn = sqlite3.connect(self.db_path)
df = pd.read_sql_query(f"""
SELECT * FROM sell_alerts
ORDER BY alert_time DESC
LIMIT {limit}
""", conn)
conn.close()
return df.to_dict('records') if not df.empty else []
except Exception as e:
self.logger.error(f"获取历史提醒失败: {e}")
return []
def clear_old_alerts(self, days: int = 30):
"""
清理旧的提醒记录
Args:
days: 保留天数
"""
try:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cutoff_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
cursor.execute("""
DELETE FROM sell_alerts
WHERE alert_time < ? AND is_sent = 1
""", (cutoff_date,))
deleted = cursor.rowcount
conn.commit()
conn.close()
self.logger.info(f"清理了 {deleted} 条旧提醒记录")
except Exception as e:
self.logger.error(f"清理旧提醒失败: {e}")
# 全局监控器实例
low_price_bull_monitor = LowPriceBullMonitor()
+293
View File
@@ -0,0 +1,293 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
低价擒牛策略监控UI模块
"""
import streamlit as st
import pandas as pd
from datetime import datetime
from low_price_bull_monitor import low_price_bull_monitor
from low_price_bull_service import low_price_bull_service
def display_monitor_panel():
"""显示策略监控面板"""
st.markdown("## 📊 策略监控中心")
st.markdown("---")
# 服务状态
status = low_price_bull_service.get_status()
col1, col2, col3, col4 = st.columns(4)
with col1:
service_status = "🟢 运行中" if status['running'] else "🔴 已停止"
st.metric("服务状态", service_status)
with col2:
st.metric("监控股票", f"{status['monitored_count']}")
with col3:
st.metric("待处理提醒", f"{status['pending_alerts']}")
with col4:
st.metric("扫描间隔", f"{status['scan_interval']}")
st.markdown("---")
# 服务控制
col_start, col_stop, col_config = st.columns(3)
with col_start:
if st.button("▶️ 启动监控服务", type="primary", disabled=status['running']):
if low_price_bull_service.start():
st.success("✅ 监控服务已启动")
st.rerun()
else:
st.error("❌ 启动失败")
with col_stop:
if st.button("⏸️ 停止监控服务", type="secondary", disabled=not status['running']):
if low_price_bull_service.stop():
st.success("✅ 监控服务已停止")
st.rerun()
else:
st.error("❌ 停止失败")
with col_config:
with st.popover("⚙️ 监控配置"):
st.markdown("**扫描间隔**")
new_interval = st.number_input(
"扫描间隔(秒)",
min_value=10,
max_value=600,
value=status['scan_interval'],
step=10,
label_visibility="collapsed"
)
if st.button("保存配置"):
low_price_bull_service.scan_interval = new_interval
st.success("✅ 配置已保存")
st.markdown("---")
# 标签页
tab1, tab2, tab3 = st.tabs(["📋 监控列表", "🔔 卖出提醒", "📜 历史记录"])
with tab1:
display_monitored_stocks()
with tab2:
display_sell_alerts()
with tab3:
display_alert_history()
def display_monitored_stocks():
"""显示监控中的股票列表"""
stocks = low_price_bull_monitor.get_monitored_stocks()
if not stocks:
st.info("暂无监控中的股票")
return
st.markdown(f"### 📋 监控列表(共{len(stocks)}只)")
# 转换为DataFrame
df = pd.DataFrame(stocks)
# 显示表格
display_df = df[['stock_code', 'stock_name', 'buy_price', 'buy_date', 'holding_days', 'add_time']].copy()
display_df.columns = ['股票代码', '股票名称', '买入价格', '买入日期', '持有天数', '加入时间']
st.dataframe(display_df, width='content', height=400)
# 批量移除
st.markdown("---")
st.markdown("### 🗑️ 批量管理")
selected_codes = st.multiselect(
"选择要移除的股票",
options=[f"{s['stock_code']} {s['stock_name']}" for s in stocks],
format_func=lambda x: x
)
if selected_codes and st.button("🗑️ 移除选中股票", type="secondary"):
for item in selected_codes:
code = item.split()[0]
success, msg = low_price_bull_monitor.remove_stock(code, "手动移除")
if success:
st.success(msg)
else:
st.error(msg)
st.rerun()
def display_sell_alerts():
"""显示待处理的卖出提醒"""
alerts = low_price_bull_monitor.get_pending_alerts()
if not alerts:
st.info("暂无待处理的卖出提醒")
return
st.markdown(f"### 🔔 卖出提醒(共{len(alerts)}条)")
for alert in alerts:
with st.expander(
f"🔔 {alert['stock_code']} {alert['stock_name']} - {alert['alert_reason']}",
expanded=True
):
col1, col2 = st.columns(2)
with col1:
st.markdown("#### 📊 提醒信息")
st.markdown(f"**股票代码**: {alert['stock_code']}")
st.markdown(f"**股票名称**: {alert['stock_name']}")
st.markdown(f"**提醒类型**: {_get_alert_type_name(alert['alert_type'])}")
st.markdown(f"**提醒原因**: {alert['alert_reason']}")
st.markdown(f"**提醒时间**: {alert['alert_time']}")
with col2:
st.markdown("#### 💰 市场数据")
# 处理当前价格(避免bytes类型错误)
current_price = alert.get('current_price')
if current_price is not None:
try:
price_val = float(current_price)
st.markdown(f"**当前价格**: {price_val:.2f}")
except (ValueError, TypeError):
st.markdown(f"**当前价格**: {current_price}")
# 处理MA5和MA20(避免bytes类型错误)
ma5 = alert.get('ma5')
ma20 = alert.get('ma20')
if ma5 is not None and ma20 is not None:
try:
ma5_val = float(ma5)
ma20_val = float(ma20)
st.markdown(f"**MA5**: {ma5_val:.2f}")
st.markdown(f"**MA20**: {ma20_val:.2f}")
st.markdown(f"**均线差**: {(ma5_val - ma20_val):.2f}")
except (ValueError, TypeError):
st.markdown(f"**MA5**: {ma5}")
st.markdown(f"**MA20**: {ma20}")
# 处理持有天数
holding_days = alert.get('holding_days')
if holding_days is not None:
try:
days_val = int(holding_days)
st.markdown(f"**持有天数**: {days_val}")
except (ValueError, TypeError):
st.markdown(f"**持有天数**: {holding_days}")
# 操作按钮
st.markdown("---")
col_action1, col_action2 = st.columns(2)
with col_action1:
if st.button(f"✅ 已处理 - {alert['stock_code']}", key=f"done_{alert['id']}"):
low_price_bull_monitor.mark_alert_sent(alert['id'])
low_price_bull_monitor.remove_stock(alert['stock_code'], "已处理提醒")
st.success(f"✅ 已标记为已处理并移除 {alert['stock_code']}")
st.rerun()
with col_action2:
if st.button(f"❌ 忽略 - {alert['stock_code']}", key=f"ignore_{alert['id']}"):
low_price_bull_monitor.mark_alert_sent(alert['id'])
st.success(f"✅ 已忽略提醒(股票保留在监控列表)")
st.rerun()
def display_alert_history():
"""显示历史提醒记录"""
alerts = low_price_bull_monitor.get_history_alerts(limit=50)
if not alerts:
st.info("暂无历史提醒记录")
return
st.markdown("### 📜 历史提醒记录")
# 转换为DataFrame
df = pd.DataFrame(alerts)
# 选择显示列
display_cols = ['stock_code', 'stock_name', 'alert_type', 'alert_reason', 'current_price', 'holding_days', 'alert_time', 'is_sent']
display_df = df[display_cols].copy()
# 重命名列
display_df.columns = ['股票代码', '股票名称', '提醒类型', '提醒原因', '当前价格', '持有天数', '提醒时间', '已发送']
# 格式化
display_df['提醒类型'] = display_df['提醒类型'].apply(_get_alert_type_name)
display_df['已发送'] = display_df['已发送'].apply(lambda x: '' if x == 1 else '')
st.dataframe(display_df, width='content', height=400)
# 清理按钮
st.markdown("---")
if st.button("🗑️ 清理30天前的记录"):
low_price_bull_monitor.clear_old_alerts(days=30)
st.success("✅ 已清理旧记录")
st.rerun()
def _get_alert_type_name(alert_type: str) -> str:
"""获取提醒类型名称"""
type_map = {
'holding_days': '持股到期',
'ma_cross': 'MA均线死叉'
}
return type_map.get(alert_type, alert_type)
def add_stock_to_monitor_button(stock_code: str, stock_name: str, price: float = None):
"""
添加股票到监控按钮在股票详情中使用
Args:
stock_code: 股票代码
stock_name: 股票名称
price: 买入价格
"""
# 检查是否已在监控
stocks = low_price_bull_monitor.get_monitored_stocks()
is_monitored = any(s['stock_code'] == stock_code for s in stocks)
if is_monitored:
st.info(f"{stock_code} 已在监控列表中")
if st.button(f"🗑️ 移出监控 - {stock_code}", key=f"remove_{stock_code}"):
success, msg = low_price_bull_monitor.remove_stock(stock_code, "手动移除")
if success:
st.success(msg)
st.rerun()
else:
st.error(msg)
else:
if st.button(f" 加入策略监控 - {stock_code}", type="primary", key=f"add_{stock_code}"):
if price is None:
price = 0.0 # 如果没有价格,使用0
success, msg = low_price_bull_monitor.add_stock(
stock_code=stock_code,
stock_name=stock_name,
buy_price=price,
buy_date=datetime.now().strftime("%Y-%m-%d")
)
if success:
st.success(msg)
st.info("💡 提示:请在左侧菜单进入'策略监控'查看监控状态")
st.rerun()
else:
st.error(msg)
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
低价擒牛选股模块
使用pywencai获取低价高成长股票
"""
import pandas as pd
import pywencai
from datetime import datetime
from typing import Tuple, Optional
import time
class LowPriceBullSelector:
"""低价擒牛选股类"""
def __init__(self):
self.raw_data = None
self.selected_stocks = None
def get_low_price_stocks(self, top_n: int = 5) -> Tuple[bool, Optional[pd.DataFrame], str]:
"""
获取低价高成长股票
选股策略
- 股价<10
- 净利润增长率100%
- 非ST
- 非科创板
- 非创业板
- 深圳A股
- 成交额由小至大排名
Args:
top_n: 返回前N只股票
Returns:
(success, dataframe, message)
"""
try:
print(f"\n{'='*60}")
print(f"🐂 低价擒牛选股 - 数据获取中")
print(f"{'='*60}")
print(f"策略: 股价<10元 + 净利润增长率≥100% + 深圳A股")
print(f"目标: 筛选前{top_n}只股票")
# 构建查询语句(按成交额由小至大排名)
query = (
"股价<10元,"
"净利润增长率(净利润同比增长率)≥100%"
"非st"
"非科创板,"
"非创业板,"
"深圳A股,"
"成交额由小至大排名"
)
print(f"\n查询语句: {query}")
print(f"正在调用问财接口...")
# 调用pywencai
result = pywencai.get(query=query, loop=True)
if result is None:
return False, None, "问财接口返回None,请检查网络或稍后重试"
# 转换为DataFrame
df_result = self._convert_to_dataframe(result)
if df_result is None or df_result.empty:
return False, None, "未获取到符合条件的股票数据"
print(f"✅ 成功获取 {len(df_result)} 只股票")
# 显示获取到的列名
print(f"\n获取到的数据字段:")
for col in df_result.columns[:15]:
print(f" - {col}")
if len(df_result.columns) > 15:
print(f" ... 还有 {len(df_result.columns) - 15} 个字段")
# 保存原始数据
self.raw_data = df_result
# 取前N只
if len(df_result) > top_n:
selected = df_result.head(top_n)
print(f"\n{len(df_result)} 只股票中选出前 {top_n}")
else:
selected = df_result
print(f"\n{len(df_result)} 只符合条件的股票")
self.selected_stocks = selected
# 显示选中的股票
print(f"\n✅ 选中的股票:")
for idx, row in selected.iterrows():
code = row.get('股票代码', 'N/A')
name = row.get('股票简称', 'N/A')
price = row.get('股价', row.get('最新价', 'N/A'))
growth = row.get('净利润增长率', row.get('净利润同比增长率', 'N/A'))
turnover = row.get('成交额', 'N/A')
print(f" {idx+1}. {code} {name} - 股价:{price} 净利增长:{growth}% 成交额:{turnover}")
print(f"{'='*60}\n")
return True, selected, f"成功筛选出{len(selected)}只低价高成长股票"
except Exception as e:
error_msg = f"获取数据失败: {str(e)}"
print(f"{error_msg}")
import traceback
traceback.print_exc()
return False, None, error_msg
def _convert_to_dataframe(self, result) -> Optional[pd.DataFrame]:
"""将pywencai返回结果转换为DataFrame"""
try:
if isinstance(result, pd.DataFrame):
return result
elif isinstance(result, dict):
if 'data' in result:
return pd.DataFrame(result['data'])
elif 'result' in result:
return pd.DataFrame(result['result'])
else:
return pd.DataFrame(result)
elif isinstance(result, list):
return pd.DataFrame(result)
else:
print(f"⚠️ 未知的数据格式: {type(result)}")
return None
except Exception as e:
print(f"转换DataFrame失败: {e}")
return None
def get_stock_codes(self) -> list:
"""
获取选中股票的代码列表去掉市场后缀
Returns:
股票代码列表
"""
if self.selected_stocks is None or self.selected_stocks.empty:
return []
codes = []
for code in self.selected_stocks['股票代码'].tolist():
if isinstance(code, str):
# 去掉 .SZ 等后缀
clean_code = code.split('.')[0] if '.' in code else code
codes.append(clean_code)
else:
codes.append(str(code))
return codes
+430
View File
@@ -0,0 +1,430 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
低价擒牛策略监控服务
定时扫描股票检测卖出信号
"""
import time
import threading
import logging
from datetime import datetime
from typing import Optional
import os
from low_price_bull_monitor import low_price_bull_monitor
from notification_service import notification_service
class LowPriceBullService:
"""低价擒牛策略监控服务"""
def __init__(self):
self.logger = logging.getLogger(__name__)
self.running = False
self.thread: Optional[threading.Thread] = None
self.scan_interval = 60 # 默认扫描间隔(秒)
self.holding_days_limit = 5 # 持股天数限制
# 从环境变量读取配置
self._load_config()
def _load_config(self):
"""从环境变量加载配置"""
try:
from dotenv import load_dotenv
load_dotenv()
# 扫描间隔
interval = os.getenv('LOW_PRICE_BULL_SCAN_INTERVAL', '60')
self.scan_interval = int(interval)
# 持股天数限制
days = os.getenv('LOW_PRICE_BULL_HOLDING_DAYS', '5')
self.holding_days_limit = int(days)
# TDX API配置
self.tdx_api_url = os.getenv('TDX_BASE_URL', 'http://127.0.0.1:5000')
self.logger.info(f"监控配置: 扫描间隔={self.scan_interval}秒, 持股天数限制={self.holding_days_limit}")
self.logger.info(f"TDX API: {self.tdx_api_url}")
except Exception as e:
self.logger.warning(f"加载配置失败,使用默认值: {e}")
def start(self):
"""启动监控服务"""
if self.running:
self.logger.warning("监控服务已在运行")
return False
self.running = True
self.thread = threading.Thread(target=self._monitor_loop, daemon=True)
self.thread.start()
self.logger.info("低价擒牛监控服务已启动")
return True
def stop(self):
"""停止监控服务"""
if not self.running:
return False
self.running = False
if self.thread:
self.thread.join(timeout=5)
self.logger.info("低价擒牛监控服务已停止")
return True
def _monitor_loop(self):
"""监控循环"""
while self.running:
try:
self._scan_stocks()
time.sleep(self.scan_interval)
except Exception as e:
self.logger.error(f"监控循环错误: {e}")
time.sleep(self.scan_interval)
def _scan_stocks(self):
"""扫描所有监控的股票"""
try:
# 更新持有天数
low_price_bull_monitor.update_holding_days()
# 获取监控列表
stocks = low_price_bull_monitor.get_monitored_stocks()
if not stocks:
return
self.logger.info(f"开始扫描 {len(stocks)} 只股票")
for stock in stocks:
try:
self._check_stock(stock)
except Exception as e:
self.logger.error(f"检查股票 {stock['stock_code']} 失败: {e}")
# 处理提醒
self._process_alerts()
except Exception as e:
self.logger.error(f"扫描股票失败: {e}")
def _check_stock(self, stock: dict):
"""
检查单只股票的卖出信号
Args:
stock: 股票信息字典
"""
stock_code = stock['stock_code']
stock_name = stock['stock_name']
holding_days = stock['holding_days']
# 检查1: 持股天数
if holding_days >= self.holding_days_limit:
# 添加提醒
low_price_bull_monitor.add_sell_alert(
stock_code=stock_code,
stock_name=stock_name,
alert_type='holding_days',
alert_reason=f'持股满{self.holding_days_limit}天,建议卖出',
holding_days=holding_days
)
self.logger.info(f"{stock_code} 持股满{self.holding_days_limit}天,生成卖出提醒")
return
# 检查2: MA5下穿MA20
current_price, ma5, ma20 = self._get_stock_data(stock_code)
if current_price and ma5 and ma20:
if ma5 < ma20:
# MA5下穿MA20,添加提醒
low_price_bull_monitor.add_sell_alert(
stock_code=stock_code,
stock_name=stock_name,
alert_type='ma_cross',
alert_reason='MA5下穿MA20,技术信号卖出',
current_price=current_price,
ma5=ma5,
ma20=ma20,
holding_days=holding_days
)
self.logger.info(f"{stock_code} MA5下穿MA20,生成卖出提醒")
def _get_stock_data(self, stock_code: str) -> tuple:
"""
获取股票数据价格和均线
Args:
stock_code: 股票代码可能带后缀如002259.SZ
Returns:
(当前价格, MA5, MA20)
"""
try:
import requests
import pandas as pd
# 处理股票代码格式:去掉后缀,保留纯数字代码
# 例如:002259.SZ -> 002259
clean_code = stock_code.split('.')[0] if '.' in stock_code else stock_code
# 判断市场并添加前缀(TDX API可能需要)
# 深圳:0开头、3开头 -> SZ前缀
# 上海:6开头 -> SH前缀
if clean_code.startswith(('0', '3')):
api_code = f"SZ{clean_code}"
elif clean_code.startswith('6'):
api_code = f"SH{clean_code}"
else:
api_code = clean_code
# 调用TDX API获取K线数据
url = f"{self.tdx_api_url}/api/kline"
params = {
'code': api_code,
'type': 'day' # 日K线
}
self.logger.debug(f"请求TDX API: code={stock_code} -> api_code={api_code}")
response = requests.get(url, params=params, timeout=10)
if response.status_code != 200:
self.logger.warning(f"获取 {stock_code} K线数据失败: HTTP {response.status_code}")
self.logger.warning(f"请求URL: {url}?code={api_code}&type=day")
# 尝试使用纯数字代码重试
if api_code != clean_code:
self.logger.info(f"尝试使用纯数字代码重试: {clean_code}")
params['code'] = clean_code
response = requests.get(url, params=params, timeout=10)
if response.status_code != 200:
self.logger.warning(f"重试失败: HTTP {response.status_code}")
return None, None, None
else:
return None, None, None
data = response.json()
# 检查数据格式
# 支持两种格式:
# 1. 直接返回数组: [{date, open, high, low, close, volume}, ...]
# 2. 嵌套格式: {code: 0, message: "success", data: {List: [...]}}
if isinstance(data, dict) and 'data' in data:
# 嵌套格式
if data.get('code') != 0:
self.logger.warning(f"{stock_code} API返回错误: {data.get('message')}")
return None, None, None
data_obj = data.get('data', {})
kline_list = data_obj.get('List', [])
if not kline_list or len(kline_list) < 20:
self.logger.warning(f"{stock_code} K线数据不足,需要至少20天,当前{len(kline_list)}")
return None, None, None
# 转换为DataFrame,字段名需要映射
# API返回:{Time, Open, High, Low, Close, Volume, Amount}
# 需要:{date, open, high, low, close, volume}
df = pd.DataFrame(kline_list)
# 重命名字段(大小写转换)
if 'Time' in df.columns:
df['date'] = df['Time']
if 'Open' in df.columns:
df['open'] = df['Open']
if 'High' in df.columns:
df['high'] = df['High']
if 'Low' in df.columns:
df['low'] = df['Low']
if 'Close' in df.columns:
df['close'] = df['Close']
if 'Volume' in df.columns:
df['volume'] = df['Volume']
elif isinstance(data, list):
# 直接数组格式
if len(data) < 20:
self.logger.warning(f"{stock_code} K线数据不足,需要至少20天")
return None, None, None
df = pd.DataFrame(data)
else:
self.logger.warning(f"{stock_code} K线数据格式错误")
return None, None, None
# 确保有close列
if 'close' not in df.columns:
self.logger.warning(f"{stock_code} K线数据缺少close字段")
return None, None, None
# 转换为浮点数
df['close'] = pd.to_numeric(df['close'], errors='coerce')
# 计算MA5和MA20
df['MA5'] = df['close'].rolling(window=5).mean()
df['MA20'] = df['close'].rolling(window=20).mean()
# 获取最新数据
latest = df.iloc[-1]
current_price = latest['close']
ma5 = latest['MA5']
ma20 = latest['MA20']
# 检查是否有效
if pd.isna(current_price) or pd.isna(ma5) or pd.isna(ma20):
self.logger.warning(f"{stock_code} 数据包含NaN值")
return None, None, None
self.logger.info(f"{stock_code} 数据: 价格={current_price:.2f}, MA5={ma5:.2f}, MA20={ma20:.2f}")
return current_price, ma5, ma20
except requests.exceptions.RequestException as e:
self.logger.error(f"请求TDX API失败 {stock_code}: {e}")
self.logger.error(f"请检查.env中的TDX_BASE_URL配置: {self.tdx_api_url}")
return None, None, None
except Exception as e:
self.logger.error(f"获取股票数据失败 {stock_code}: {e}")
import traceback
traceback.print_exc()
return None, None, None
def _process_alerts(self):
"""处理待发送的提醒"""
try:
alerts = low_price_bull_monitor.get_pending_alerts()
if not alerts:
return
self.logger.info(f"处理 {len(alerts)} 条卖出提醒")
for alert in alerts:
try:
# 发送通知
self._send_alert_notification(alert)
# 标记已发送
low_price_bull_monitor.mark_alert_sent(alert['id'])
# 自动移除股票
low_price_bull_monitor.remove_stock(
alert['stock_code'],
reason=alert['alert_reason']
)
self.logger.info(f"已处理提醒并移除股票: {alert['stock_code']}")
except Exception as e:
self.logger.error(f"处理提醒失败: {e}")
except Exception as e:
self.logger.error(f"处理提醒失败: {e}")
def _send_alert_notification(self, alert: dict):
"""
发送卖出提醒通知
Args:
alert: 提醒信息字典
"""
try:
# 构建消息
keyword = notification_service.config.get('webhook_keyword', 'aiagents通知')
message_text = f"### {keyword} - 低价擒牛卖出提醒\n\n"
message_text += f"**股票代码**: {alert['stock_code']}\n\n"
message_text += f"**股票名称**: {alert['stock_name']}\n\n"
message_text += f"**提醒类型**: {self._get_alert_type_name(alert['alert_type'])}\n\n"
message_text += f"**提醒原因**: {alert['alert_reason']}\n\n"
# 添加详细信息(确保数据类型正确)
current_price = alert.get('current_price')
if current_price is not None:
try:
price_val = float(current_price)
message_text += f"**当前价格**: {price_val:.2f}\n\n"
except (ValueError, TypeError):
pass
ma5 = alert.get('ma5')
ma20 = alert.get('ma20')
if ma5 is not None and ma20 is not None:
try:
ma5_val = float(ma5)
ma20_val = float(ma20)
message_text += f"**MA5**: {ma5_val:.2f}\n\n"
message_text += f"**MA20**: {ma20_val:.2f}\n\n"
except (ValueError, TypeError):
pass
holding_days = alert.get('holding_days')
if holding_days is not None:
try:
days_val = int(holding_days)
message_text += f"**持有天数**: {days_val}\n\n"
except (ValueError, TypeError):
pass
message_text += f"**提醒时间**: {alert['alert_time']}\n\n"
message_text += "---\n\n"
message_text += "**建议**: 开盘时卖出该股票\n\n"
message_text += "_此消息由AI股票分析系统自动发送_"
# 发送钉钉通知
if notification_service.config['webhook_enabled']:
import requests
data = {
"msgtype": "markdown",
"markdown": {
"title": f"{keyword} - 卖出提醒",
"text": message_text
}
}
response = requests.post(
notification_service.config['webhook_url'],
json=data,
headers={'Content-Type': 'application/json'},
timeout=10
)
if response.status_code == 200:
self.logger.info(f"卖出提醒已发送: {alert['stock_code']}")
else:
self.logger.error(f"发送提醒失败: HTTP {response.status_code}")
except Exception as e:
self.logger.error(f"发送通知失败: {e}")
def _get_alert_type_name(self, alert_type: str) -> str:
"""获取提醒类型名称"""
type_map = {
'holding_days': '持股到期',
'ma_cross': 'MA均线死叉'
}
return type_map.get(alert_type, alert_type)
def get_status(self) -> dict:
"""获取服务状态"""
stocks = low_price_bull_monitor.get_monitored_stocks()
alerts = low_price_bull_monitor.get_pending_alerts()
return {
'running': self.running,
'scan_interval': self.scan_interval,
'holding_days_limit': self.holding_days_limit,
'monitored_count': len(stocks),
'pending_alerts': len(alerts)
}
# 全局服务实例
low_price_bull_service = LowPriceBullService()
+277
View File
@@ -0,0 +1,277 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
低价擒牛量化交易策略
实现基于MA均线的买卖择时策略
"""
import pandas as pd
from datetime import datetime, timedelta
from typing import Dict, List, Optional
import logging
class LowPriceBullStrategy:
"""低价擒牛量化交易策略"""
def __init__(self, initial_capital: float = 1000000.0):
"""
初始化策略
Args:
initial_capital: 初始资金默认100万
"""
self.logger = logging.getLogger(__name__)
# 策略参数
self.initial_capital = initial_capital
self.available_cash = initial_capital
self.max_stocks = 4 # 账户最大持股数
self.max_position_per_stock = 0.4 # 个股最大持仓比例(4成)
self.max_daily_buy = 2 # 单日最大买入数
self.holding_period = 5 # 持股周期(天)
# 持仓信息
self.positions: Dict[str, Dict] = {} # {股票代码: {买入价, 数量, 买入日期, 持有天数}}
self.trade_history: List[Dict] = [] # 交易历史
# 当日交易计数
self.daily_buy_count = 0
self.current_date = None
def reset_daily_counter(self, date):
"""重置当日计数器"""
if self.current_date != date:
self.current_date = date
self.daily_buy_count = 0
def can_buy(self, stock_code: str) -> tuple[bool, str]:
"""
检查是否可以买入
Returns:
(是否可买, 原因)
"""
# 检查是否已持有
if stock_code in self.positions:
return False, "已持有该股票"
# 检查持股数量
if len(self.positions) >= self.max_stocks:
return False, f"已达最大持股数限制({self.max_stocks}只)"
# 检查当日买入数量
if self.daily_buy_count >= self.max_daily_buy:
return False, f"今日已达最大买入数限制({self.max_daily_buy}只)"
# 检查资金
if self.available_cash <= 0:
return False, "可用资金不足"
return True, "可以买入"
def calculate_buy_amount(self, stock_price: float) -> tuple[int, float]:
"""
计算买入数量满仓策略
Args:
stock_price: 股票价格
Returns:
(买入股数, 买入金额)
"""
# 满仓策略:使用所有可用资金
max_amount = self.available_cash
# 但不能超过个股最大持仓
max_per_stock = self.initial_capital * self.max_position_per_stock
target_amount = min(max_amount, max_per_stock)
# 计算股数(A股100股为1手)
shares = int(target_amount / stock_price / 100) * 100
if shares < 100:
return 0, 0
actual_amount = shares * stock_price
return shares, actual_amount
def buy(self, stock_code: str, stock_name: str, price: float, date: str) -> tuple[bool, str, Optional[Dict]]:
"""
执行买入操作
Returns:
(是否成功, 消息, 交易详情)
"""
# 重置当日计数
self.reset_daily_counter(date)
# 检查是否可买入
can_buy, reason = self.can_buy(stock_code)
if not can_buy:
return False, reason, None
# 计算买入数量
shares, amount = self.calculate_buy_amount(price)
if shares == 0:
return False, "资金不足,无法买入100股", None
# 执行买入
self.positions[stock_code] = {
'name': stock_name,
'shares': shares,
'buy_price': price,
'buy_date': date,
'holding_days': 0
}
self.available_cash -= amount
self.daily_buy_count += 1
# 记录交易
trade = {
'type': 'BUY',
'code': stock_code,
'name': stock_name,
'price': price,
'shares': shares,
'amount': amount,
'date': date,
'cash_after': self.available_cash
}
self.trade_history.append(trade)
message = f"✅ 买入成功: {stock_code} {stock_name} | 价格:{price:.2f} | 数量:{shares}股 | 金额:{amount:.2f}"
self.logger.info(message)
return True, message, trade
def should_sell(self, stock_code: str, ma5: float, ma20: float, current_date: str) -> tuple[bool, str]:
"""
判断是否应该卖出
策略
1. MA5下穿MA20时卖出
2. 持股满5天强制卖出
Returns:
(是否卖出, 原因)
"""
if stock_code not in self.positions:
return False, "未持有该股票"
position = self.positions[stock_code]
# 更新持有天数
# 简化处理,按交易日计算
position['holding_days'] += 1
# 检查持股周期
if position['holding_days'] >= self.holding_period:
return True, f"持股满{self.holding_period}天,到期卖出"
# 检查MA5下穿MA20
if ma5 is not None and ma20 is not None:
if ma5 < ma20:
return True, "MA5下穿MA20,技术信号卖出"
return False, "持有"
def sell(self, stock_code: str, price: float, date: str, reason: str = "") -> tuple[bool, str, Optional[Dict]]:
"""
执行卖出操作
Returns:
(是否成功, 消息, 交易详情)
"""
if stock_code not in self.positions:
return False, "未持有该股票", None
position = self.positions[stock_code]
shares = position['shares']
buy_price = position['buy_price']
# 计算盈亏
amount = shares * price
cost = shares * buy_price
profit = amount - cost
profit_pct = (profit / cost) * 100 if cost > 0 else 0
# 归还资金
self.available_cash += amount
# 移除持仓
del self.positions[stock_code]
# 记录交易
trade = {
'type': 'SELL',
'code': stock_code,
'name': position['name'],
'price': price,
'shares': shares,
'amount': amount,
'date': date,
'reason': reason,
'buy_price': buy_price,
'profit': profit,
'profit_pct': profit_pct,
'cash_after': self.available_cash
}
self.trade_history.append(trade)
profit_str = f"+{profit:.2f}" if profit >= 0 else f"{profit:.2f}"
message = f"✅ 卖出成功: {stock_code} {position['name']} | 价格:{price:.2f} | 数量:{shares}股 | 盈亏:{profit_str}元({profit_pct:+.2f}%) | 原因:{reason}"
self.logger.info(message)
return True, message, trade
def get_portfolio_summary(self) -> Dict:
"""
获取投资组合摘要
Returns:
组合摘要信息
"""
# 计算持仓市值(需要当前价格,这里用买入价估算)
position_value = sum(
pos['shares'] * pos['buy_price']
for pos in self.positions.values()
)
total_value = self.available_cash + position_value
# 计算收益
total_profit = total_value - self.initial_capital
total_profit_pct = (total_profit / self.initial_capital) * 100
return {
'initial_capital': self.initial_capital,
'available_cash': self.available_cash,
'position_value': position_value,
'total_value': total_value,
'total_profit': total_profit,
'total_profit_pct': total_profit_pct,
'positions_count': len(self.positions),
'max_stocks': self.max_stocks,
'trade_count': len(self.trade_history)
}
def get_positions(self) -> List[Dict]:
"""获取当前持仓列表"""
return [
{
'code': code,
'name': pos['name'],
'shares': pos['shares'],
'buy_price': pos['buy_price'],
'buy_date': pos['buy_date'],
'holding_days': pos['holding_days']
}
for code, pos in self.positions.items()
]
def get_trade_history(self) -> List[Dict]:
"""获取交易历史"""
return self.trade_history.copy()
+589
View File
@@ -0,0 +1,589 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
低价擒牛UI模块
"""
import streamlit as st
import pandas as pd
from datetime import datetime
from low_price_bull_selector import LowPriceBullSelector
from low_price_bull_strategy import LowPriceBullStrategy
from notification_service import notification_service
from low_price_bull_monitor import low_price_bull_monitor
from low_price_bull_service import low_price_bull_service
def display_low_price_bull():
"""显示低价擒牛选股界面"""
# 检查是否显示监控面板
if st.session_state.get('show_low_price_monitor'):
from low_price_bull_monitor_ui import display_monitor_panel
display_monitor_panel()
# 返回按钮
if st.button("🔙 返回选股", type="secondary"):
del st.session_state.show_low_price_monitor
st.rerun()
return
st.markdown("顶部按钮区")
col_select, col_monitor = st.columns([3, 1])
with col_select:
st.markdown("## 🐂 低价擒牛 - 低价高成长股票筛选")
with col_monitor:
st.write("") # 占位
if st.button("📊 策略监控", type="primary", width='content'):
st.session_state.show_low_price_monitor = True
st.rerun()
st.markdown("---")
st.markdown("""
### 📋 选股策略说明
**筛选条件**
- 股价 < 10
- 净利润增长率 100%净利润同比增长率
- 非ST股票
- 非科创板
- 非创业板
- 深圳A股
- 按成交额由小至大排名
**量化交易策略**
- 💰 资金量100万元
- 📅 持股周期5
- 💼 仓位控制满仓
- 📊 个股最大持仓440%
- 🎯 账户最大持股数4
- 🛒 单日最大买入数2
- 📈 买入时机开盘买入
- 📉 卖出时机MA5下穿MA20或持股满5天
""")
st.markdown("---")
# 参数设置
col1, col2 = st.columns([2, 1])
with col1:
top_n = st.slider(
"筛选数量",
min_value=3,
max_value=10,
value=5,
step=1,
help="选择展示的股票数量"
)
with col2:
st.info(f"💡 将筛选成交额最小的前{top_n}只股票")
st.markdown("---")
# 开始选股按钮
if st.button("🚀 开始低价擒牛选股", type="primary", width='content'):
with st.spinner("正在获取数据,请稍候..."):
# 创建选股器
selector = LowPriceBullSelector()
# 获取股票
success, stocks_df, message = selector.get_low_price_stocks(top_n=top_n)
if success and stocks_df is not None:
# 保存结果
st.session_state.low_price_bull_stocks = stocks_df
st.session_state.low_price_bull_selector = selector
st.success(f"{message}")
# 发送钉钉通知
send_dingtalk_notification(stocks_df, top_n)
st.rerun()
else:
st.error(f"{message}")
# 显示选股结果
if 'low_price_bull_stocks' in st.session_state:
display_stock_results(
st.session_state.low_price_bull_stocks,
st.session_state.get('low_price_bull_selector')
)
def display_stock_results(stocks_df: pd.DataFrame, selector):
"""显示选股结果"""
st.markdown("---")
st.markdown("## 📊 选股结果")
# 统计信息
col1, col2, col3 = st.columns(3)
with col1:
st.metric("筛选数量", f"{len(stocks_df)}")
with col2:
# 智能计算平均净利增长率(过滤无效值)
growth_col = stocks_df.get('净利润增长率', stocks_df.get('净利润同比增长率', pd.Series([])))
valid_growth = growth_col[growth_col.notna() & (growth_col != '') & (growth_col != 'N/A')]
if len(valid_growth) > 0:
avg_growth = pd.to_numeric(valid_growth, errors='coerce').mean()
if not pd.isna(avg_growth):
st.metric("平均净利增长率", f"{avg_growth:.1f}%")
else:
st.metric("平均净利增长率", "-")
else:
st.metric("平均净利增长率", "-")
with col3:
# 智能计算平均股价(过滤无效值)
price_col = stocks_df.get('股价', stocks_df.get('最新价', pd.Series([])))
valid_price = price_col[price_col.notna() & (price_col != '') & (price_col != 'N/A')]
if len(valid_price) > 0:
avg_price = pd.to_numeric(valid_price, errors='coerce').mean()
if not pd.isna(avg_price):
st.metric("平均股价", f"{avg_price:.2f}")
else:
st.metric("平均股价", "-")
else:
st.metric("平均股价", "-")
st.markdown("---")
# 显示股票列表
st.markdown("### 📋 精选股票列表")
for idx, row in stocks_df.iterrows():
# 获取股票代码和简称
code = row.get('股票代码', 'N/A')
name = row.get('股票简称', 'N/A')
# 获取价格信息作为标题补充
price = row.get('股价', row.get('最新价', None))
price_str = ''
if price is not None and not pd.isna(price):
try:
price_float = float(price)
price_str = f" | 价格: {price_float:.2f}"
except:
pass
with st.expander(
f"【第{idx+1}名】{code} - {name}{price_str}",
expanded=(idx < 3)
):
display_stock_detail(row)
# 完整数据表格
st.markdown("---")
st.markdown("### 📊 完整数据表格")
# 选择关键列显示
display_cols = ['股票代码', '股票简称']
# 智能匹配列名
for pattern in ['股价', '最新价']:
matching = [col for col in stocks_df.columns if pattern in col]
if matching:
display_cols.append(matching[0])
break
for pattern in ['净利润增长率', '净利润同比增长率']:
matching = [col for col in stocks_df.columns if pattern in col]
if matching:
display_cols.append(matching[0])
break
for pattern in ['成交额']:
matching = [col for col in stocks_df.columns if pattern in col]
if matching:
display_cols.append(matching[0])
break
for col_name in ['总市值', '市盈率', '市净率', '所属行业']:
matching = [col for col in stocks_df.columns if col_name in col]
if matching:
display_cols.append(matching[0])
# 选择存在的列
final_cols = [col for col in display_cols if col in stocks_df.columns]
if final_cols:
st.dataframe(stocks_df[final_cols], width='content', height=400)
# 下载按钮
csv = stocks_df[final_cols].to_csv(index=False, encoding='utf-8-sig')
st.download_button(
label="📥 下载股票列表CSV",
data=csv,
file_name=f"low_price_bull_{datetime.now().strftime('%Y%m%d')}.csv",
mime="text/csv"
)
# 量化交易模拟
st.markdown("---")
display_strategy_simulation(stocks_df, selector)
def display_stock_detail(row: pd.Series):
"""显示单个股票详情"""
def is_valid_value(value):
"""判断值是否有效(非None、非NaN、非空字符串、非'N/A'"""
if value is None:
return False
if pd.isna(value):
return False
if str(value).strip() in ['', 'N/A', 'nan', 'None']:
return False
return True
def format_value(value, suffix=''):
"""格式化显示值"""
if isinstance(value, float):
if abs(value) >= 100000000: # 亿
return f"{value/100000000:.2f}亿{suffix}"
elif abs(value) >= 10000: # 万
return f"{value/10000:.2f}{suffix}"
else:
return f"{value:.2f}{suffix}"
return f"{value}{suffix}"
# 先检查是否有任何财务数据
has_any_data = False
financial_fields = [
('所属行业', row.get('所属行业', row.get('所属同花顺行业', None))),
('总市值', row.get('总市值', row.get('总市值[20241211]', None))),
('市盈率', row.get('市盈率', row.get('市盈率pe', None))),
('市净率', row.get('市净率', row.get('市净率pb', None))),
('流通市值', row.get('流通市值', row.get('流通市值[20241211]', None))),
('换手率', row.get('换手率', row.get('换手率[%]', None)))
]
for _, value in financial_fields:
if is_valid_value(value):
has_any_data = True
break
# 只有当存在有效数据时才显示两列布局
if has_any_data:
col1, col2 = st.columns(2)
else:
col1 = st.container()
col2 = None
with col1:
st.markdown("#### 📊 基本信息")
# 股票代码(必显示)
code = row.get('股票代码', '')
if is_valid_value(code):
st.markdown(f"**股票代码**: {code}")
# 股票简称(必显示)
name = row.get('股票简称', '')
if is_valid_value(name):
st.markdown(f"**股票简称**: {name}")
# 当前价格
price = row.get('股价', row.get('最新价', None))
if is_valid_value(price):
st.markdown(f"**当前价格**: {format_value(price, '')}")
# 净利润增长率
growth = row.get('净利润增长率', row.get('净利润同比增长率', None))
if is_valid_value(growth):
st.markdown(f"**净利润增长率**: {format_value(growth, '%')}")
# 成交额
turnover = row.get('成交额', None)
if is_valid_value(turnover):
st.markdown(f"**成交额**: {format_value(turnover, '')}")
# 涨跌幅
change_pct = row.get('涨跌幅', row.get('涨跌幅:前复权[%]', None))
if is_valid_value(change_pct):
st.markdown(f"**涨跌幅**: {format_value(change_pct, '%')}")
# 只有当有财务数据时才显示财务指标栏目
if col2 is not None:
with col2:
st.markdown("#### 💼 财务指标")
# 所属行业
industry = row.get('所属行业', row.get('所属同花顺行业', None))
if is_valid_value(industry):
st.markdown(f"**所属行业**: {industry}")
# 总市值
market_cap = row.get('总市值', row.get('总市值[20241211]', None))
if is_valid_value(market_cap):
st.markdown(f"**总市值**: {format_value(market_cap, '')}")
# 市盈率
pe = row.get('市盈率', row.get('市盈率pe', None))
if is_valid_value(pe):
st.markdown(f"**市盈率**: {format_value(pe, '')}")
# 市净率
pb = row.get('市净率', row.get('市净率pb', None))
if is_valid_value(pb):
st.markdown(f"**市净率**: {format_value(pb, '')}")
# 流通市值
float_cap = row.get('流通市值', row.get('流通市值[20241211]', None))
if is_valid_value(float_cap):
st.markdown(f"**流通市值**: {format_value(float_cap, '')}")
# 换手率
turnover_rate = row.get('换手率', row.get('换手率[%]', None))
if is_valid_value(turnover_rate):
st.markdown(f"**换手率**: {format_value(turnover_rate, '%')}")
# 添加监控按钮
st.markdown("---")
st.markdown("#### 📊 策略监控")
from low_price_bull_monitor_ui import add_stock_to_monitor_button
stock_code = row.get('股票代码', '')
stock_name = row.get('股票简称', '')
price = row.get('股价', row.get('最新价', None))
# 去掉代码后缀
if isinstance(stock_code, str) and '.' in stock_code:
stock_code = stock_code.split('.')[0]
# 转换价格
try:
price_float = float(price) if price and not pd.isna(price) else None
except:
price_float = None
if stock_code and stock_name:
add_stock_to_monitor_button(stock_code, stock_name, price_float)
def display_strategy_simulation(stocks_df: pd.DataFrame, selector):
"""显示量化交易策略模拟"""
st.markdown("## 🎯 策略监控与模拟")
st.info("""
**监控说明**
- 在上方股票列表中点击" 加入策略监控"按钮即可加入
- 监控条件 持股满5天第6天开盘提醒卖出 MA5下穿MA20提醒卖出
- 扫描频率每分钟扫描1次可在监控面板配置
- 提醒卖出后自动移出监控列表
- 点击右上角"📊 策略监控"按钮查看监控面板
""")
col1, col2 = st.columns(2)
with col1:
if st.button("🎮 开始策略模拟", type="primary", width='content'):
st.session_state.show_strategy_simulation = True
with col2:
if st.button("🔗 连接MiniQMT实盘", type="secondary", width='content'):
st.warning("⚠️ MiniQMT实盘交易功能需要先配置环境变量,详见系统配置")
# 显示模拟结果
if st.session_state.get('show_strategy_simulation'):
run_strategy_simulation(stocks_df)
def run_strategy_simulation(stocks_df: pd.DataFrame):
"""运行策略模拟"""
st.markdown("---")
st.markdown("### 📈 策略模拟执行")
# 创建策略实例
strategy = LowPriceBullStrategy(initial_capital=1000000.0)
# 模拟买入(按成交额排序,优先买入成交额小的)
st.markdown("#### 1️⃣ 模拟买入信号")
buy_results = []
current_date = datetime.now().strftime("%Y-%m-%d")
for idx, row in stocks_df.head(strategy.max_daily_buy).iterrows():
code = str(row.get('股票代码', '')).split('.')[0]
name = row.get('股票简称', 'N/A')
price = float(row.get('股价', row.get('最新价', 0)))
if price > 0:
success, message, trade = strategy.buy(code, name, price, current_date)
buy_results.append({
'success': success,
'message': message,
'trade': trade
})
# 显示买入结果
for result in buy_results:
if result['success']:
st.success(result['message'])
else:
st.warning(f"⚠️ {result['message']}")
# 显示持仓
st.markdown("---")
st.markdown("#### 2️⃣ 当前持仓")
positions = strategy.get_positions()
if positions:
positions_df = pd.DataFrame(positions)
st.dataframe(positions_df, width='content')
else:
st.info("暂无持仓")
# 显示账户摘要
st.markdown("---")
st.markdown("#### 3️⃣ 账户摘要")
summary = strategy.get_portfolio_summary()
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("初始资金", f"{summary['initial_capital']:,.0f}")
with col2:
st.metric("可用资金", f"{summary['available_cash']:,.0f}")
with col3:
st.metric("持仓市值", f"{summary['position_value']:,.0f}")
with col4:
st.metric("总资产", f"{summary['total_value']:,.0f}")
st.markdown("---")
# 策略说明
st.markdown("#### 📝 策略执行说明")
st.markdown("""
**后续操作**
1. **持有期管理**系统会自动跟踪每只股票的持有天数
2. **卖出信号监测**
- 每日收盘后计算MA5和MA20
- 如果MA5下穿MA20触发卖出信号
- 如果持股满5天强制卖出
3. **轮动买入**卖出后释放资金继续买入新的符合条件的股票
**风险提示**
- 本策略为模拟演示实际交易存在滑点手续费等成本
- 历史业绩不代表未来收益
- 请谨慎评估风险理性投资
""")
def send_dingtalk_notification(stocks_df: pd.DataFrame, top_n: int):
"""发送钉钉通知"""
try:
# 检查webhook配置
webhook_config = notification_service.get_webhook_config_status()
if not webhook_config['enabled'] or not webhook_config['configured']:
st.info("💡 未配置Webhook通知,如需接收钉钉消息请在环境配置中设置")
return
# 构建消息内容
keyword = notification_service.config.get('webhook_keyword', 'aiagents通知')
message_text = f"### {keyword} - 低价擒牛选股完成\n\n"
message_text += f"**筛选策略**: 股价<10元 + 净利润增长率≥100% + 深圳A股\n\n"
message_text += f"**筛选数量**: {len(stocks_df)}\n\n"
message_text += f"**精选股票**:\n\n"
for idx, row in stocks_df.head(top_n).iterrows():
code = row.get('股票代码', '')
name = row.get('股票简称', '')
# 只显示有效的信息
message_text += f"{idx+1}. **{code} {name}**\n"
# 股价
price = row.get('股价', row.get('最新价', None))
if price is not None and not pd.isna(price) and str(price).strip() not in ['', 'N/A']:
try:
price_float = float(price)
message_text += f" - 股价: {price_float:.2f}\n"
except:
pass
# 净利润增长率
growth = row.get('净利润增长率', row.get('净利润同比增长率', None))
if growth is not None and not pd.isna(growth) and str(growth).strip() not in ['', 'N/A']:
try:
growth_float = float(growth)
message_text += f" - 净利增长: {growth_float:.2f}%\n"
except:
pass
# 成交额
turnover = row.get('成交额', None)
if turnover is not None and not pd.isna(turnover) and str(turnover).strip() not in ['', 'N/A']:
try:
turnover_float = float(turnover)
if turnover_float >= 100000000: # 亿
message_text += f" - 成交额: {turnover_float/100000000:.2f}亿元\n"
elif turnover_float >= 10000: # 万
message_text += f" - 成交额: {turnover_float/10000:.2f}万元\n"
else:
message_text += f" - 成交额: {turnover_float:.2f}\n"
except:
pass
# 所属行业
industry = row.get('所属行业', row.get('所属同花顺行业', None))
if industry is not None and not pd.isna(industry) and str(industry).strip() not in ['', 'N/A']:
message_text += f" - 所属行业: {industry}\n"
message_text += "\n"
message_text += f"**生成时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
message_text += "_此消息由AI股票分析系统自动发送_"
# 直接发送钉钉Webhook(不使用notification_service的默认格式)
if notification_service.config['webhook_type'] == 'dingtalk':
import requests
data = {
"msgtype": "markdown",
"markdown": {
"title": f"{keyword}",
"text": message_text
}
}
try:
response = requests.post(
notification_service.config['webhook_url'],
json=data,
headers={'Content-Type': 'application/json'},
timeout=10
)
if response.status_code == 200:
result = response.json()
if result.get('errcode') == 0:
st.success("✅ 已发送钉钉通知")
else:
st.warning(f"⚠️ 钉钉通知发送失败: {result.get('errmsg')}")
else:
st.warning(f"⚠️ 钉钉通知请求失败: HTTP {response.status_code}")
except Exception as e:
st.warning(f"⚠️ 发送钉钉通知失败: {str(e)}")
except Exception as e:
st.warning(f"⚠️ 发送通知时出错: {str(e)}")
+33 -9
View File
@@ -318,25 +318,42 @@ class NotificationService:
title_prefix = f"{keyword} - " if keyword else ""
content_prefix = f"### {keyword} - " if keyword else "### "
data = {
"msgtype": "markdown",
"markdown": {
"title": f"{title_prefix}{notification['symbol']} {notification['name']}",
"text": f"""{content_prefix}股票监测提醒
# 构建增强的消息内容
message_text = f"""{content_prefix}股票监测提醒
**股票代码**: {notification['symbol']}
**股票名称**: {notification['name']}
**提醒类型**: {notification['type']}
**📊 实时行情**:
- 当前价格: {notification.get('current_price', 'N/A')}
- 涨跌幅: {notification.get('change_pct', 'N/A')}%
- 涨跌额: {notification.get('change_amount', 'N/A')}
- 成交量: {notification.get('volume', 'N/A')}
- 换手率: {notification.get('turnover_rate', 'N/A')}%
**提醒内容**: {notification['message']}
**🎯 AI决策**: {notification['type']}
**触发时间**: {notification['triggered_at']}
**📝 分析内容**: {notification['message']}
**💰 持仓信息**:
- 持仓状态: {notification.get('position_status', '未知')}
- 持仓成本: {notification.get('position_cost', 'N/A')}
- 浮动盈亏: {notification.get('profit_loss_pct', 'N/A')}%
** 触发时间**: {notification['triggered_at']}
**🕐 交易时段**: {notification.get('trading_session', '未知')}
---
_此消息由AI股票分析系统自动发送_"""
data = {
"msgtype": "markdown",
"markdown": {
"title": f"{title_prefix}{notification['symbol']} {notification['name']}",
"text": message_text
}
}
@@ -756,4 +773,11 @@ _此消息由AI股票分析系统自动发送_"""
return False
# 全局通知服务实例
notification_service = NotificationService()
notification_service = NotificationService()
BIN
View File
Binary file not shown.
+13 -1
View File
@@ -482,7 +482,19 @@ class SmartMonitorEngine:
'type': '智能盯盘',
'message': message,
'details': content,
'triggered_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S')
'triggered_at': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
# 新增实时市场数据
'current_price': market_data.get('current_price', 'N/A'),
'change_pct': f"{market_data.get('change_pct', 0):+.2f}" if market_data.get('change_pct') else 'N/A',
'change_amount': f"{market_data.get('change_amount', 0):+.2f}" if market_data.get('change_amount') else 'N/A',
'volume': market_data.get('volume', 'N/A'),
'turnover_rate': f"{market_data.get('turnover_rate', 0):.2f}" if market_data.get('turnover_rate') else 'N/A',
# 持仓信息
'position_status': '已持仓' if has_position else '未持仓',
'position_cost': f"{position_cost:.2f}" if has_position and position_cost else 'N/A',
'profit_loss_pct': f"{((market_data.get('current_price', 0) - position_cost) / position_cost * 100):+.2f}" if has_position and position_cost else 'N/A',
# 交易时段信息
'trading_session': session_info.get('session', '未知')
}
# 直接调用主程序的通知服务发送
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+174
View File
@@ -0,0 +1,174 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
TDX API配置测试脚本
用于测试TDX API连接和数据获取是否正常
"""
import os
import sys
import requests
from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
# 获取TDX API URL
TDX_API_URL = os.getenv('TDX_BASE_URL', 'http://127.0.0.1:5000')
print("=" * 60)
print("TDX API配置测试")
print("=" * 60)
print(f"\n1. TDX API地址: {TDX_API_URL}")
# 测试1: 健康检查
print("\n2. 测试健康检查接口...")
try:
response = requests.get(f"{TDX_API_URL}/api/health", timeout=5)
if response.status_code == 200:
print(" ✅ 健康检查成功")
print(f" 响应: {response.text}")
else:
print(f" ❌ 健康检查失败: HTTP {response.status_code}")
sys.exit(1)
except Exception as e:
print(f" ❌ 连接失败: {e}")
print("\n提示:")
print(" - 请检查TDX API服务是否已启动")
print(" - 请检查.env中的TDX_API_URL配置是否正确")
print(" - 默认地址: http://192.168.1.222:8181")
sys.exit(1)
# 测试2: 获取K线数据
print("\n3. 测试K线数据接口...")
# 尝试不同的代码格式
test_codes = [
("SZ000001", "平安银行"),
("000001", "平安银行(纯数字)"),
("SH600000", "浦发银行"),
("600000", "浦发银行(纯数字)"),
]
data = None
test_code = None
for code, name in test_codes:
print(f"\n 尝试股票: {code} ({name})")
try:
url = f"{TDX_API_URL}/api/kline"
params = {
'code': code,
'type': 'day'
}
response = requests.get(url, params=params, timeout=10)
if response.status_code == 200:
data = response.json()
# 支持两种数据格式
kline_list = None
if isinstance(data, dict) and 'data' in data:
# 嵌套格式: {code: 0, message: "success", data: {List: [...]}}
if data.get('code') == 0:
data_obj = data.get('data', {})
kline_list = data_obj.get('List', [])
elif isinstance(data, list):
# 直接数组格式
kline_list = data
if kline_list and len(kline_list) > 0:
test_code = code
data = kline_list # 保存为全局变量
print(f" ✅ K线数据获取成功!")
print(f" 数据条数: {len(kline_list)}")
break
else:
print(f" ⚠️ 数据为空")
else:
print(f" ❌ HTTP {response.status_code}")
except Exception as e:
print(f" ❌ 错误: {e}")
if data is None or test_code is None:
print(f"\n ❌ 所有代码格式都失败,无法继续测试")
print("\n提示:")
print(" - 请检查TDX API服务是否正确启动")
print(" - 请确认API支持的股票代码格式")
print(" - 可能的格式:SZ000001, 000001, SH600000, 600000")
sys.exit(1)
print(f"\n 成功的代码格式: {test_code}")
# 显示最新一条数据
if len(data) > 0:
latest = data[-1]
print(f"\n 最新K线数据:")
# 支持两种字段名格式:小写和大写
print(f" - 日期: {latest.get('date') or latest.get('Time', 'N/A')}")
print(f" - 开盘: {latest.get('open') or latest.get('Open', 'N/A')}")
print(f" - 收盘: {latest.get('close') or latest.get('Close', 'N/A')}")
print(f" - 最高: {latest.get('high') or latest.get('High', 'N/A')}")
print(f" - 最低: {latest.get('low') or latest.get('Low', 'N/A')}")
print(f" - 成交量: {latest.get('volume') or latest.get('Volume', 'N/A')}")
# 检查数据量是否足够计算MA20
if len(data) >= 20:
print(f" ✅ 数据量充足,可以计算MA20(需要至少20条)")
else:
print(f" ⚠️ 数据量不足,仅{len(data)}条,需要至少20条才能计算MA20")
print(f" 请尝试其他股票或等待数据积累")
# 测试3: 计算均线
print("\n4. 测试均线计算...")
try:
import pandas as pd
df = pd.DataFrame(data)
# 支持两种字段名:小写close和大写Close
if 'Close' in df.columns and 'close' not in df.columns:
df['close'] = df['Close']
df['close'] = pd.to_numeric(df['close'], errors='coerce')
# 计算MA5和MA20
df['MA5'] = df['close'].rolling(window=5).mean()
df['MA20'] = df['close'].rolling(window=20).mean()
latest = df.iloc[-1]
if pd.notna(latest['MA5']) and pd.notna(latest['MA20']):
print(f" ✅ 均线计算成功")
print(f" - 收盘价: {latest['close']:.2f}")
print(f" - MA5: {latest['MA5']:.2f}")
print(f" - MA20: {latest['MA20']:.2f}")
# 判断MA5和MA20的关系
if latest['MA5'] > latest['MA20']:
print(f" - 趋势: 🟢 MA5 > MA20 (多头)")
elif latest['MA5'] < latest['MA20']:
print(f" - 趋势: 🔴 MA5 < MA20 (空头)")
else:
print(f" - 趋势: 🟡 MA5 = MA20 (震荡)")
else:
print(f" ❌ 均线计算失败,数据包含NaN")
sys.exit(1)
except Exception as e:
print(f" ❌ 均线计算失败: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
# 所有测试通过
print("\n" + "=" * 60)
print("✅ 所有测试通过!TDX API配置正常")
print("=" * 60)
print("\n提示:")
print(" - 现在可以启动低价擒牛策略监控服务")
print(" - 在监控面板中点击'▶️ 启动监控服务'")
print(" - 服务将每60秒扫描一次监控列表中的股票")
print("")