diff --git a/.env.example b/.env.example index 19d9873..5529c41 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/app.py b/app.py index d3eed2e..aeaac58 100644 --- a/app.py +++ b/app.py @@ -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: diff --git a/docs/TDX_API配置快速指南.md b/docs/TDX_API配置快速指南.md new file mode 100644 index 0000000..46c1c8e --- /dev/null +++ b/docs/TDX_API配置快速指南.md @@ -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 diff --git a/docs/三重滤网策略保存.py b/docs/三重滤网策略保存.py new file mode 100644 index 0000000..a64c8d1 --- /dev/null +++ b/docs/三重滤网策略保存.py @@ -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 + diff --git a/docs/低价擒牛功能说明.md b/docs/低价擒牛功能说明.md new file mode 100644 index 0000000..0e57502 --- /dev/null +++ b/docs/低价擒牛功能说明.md @@ -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 +- 系统内置反馈功能 + +--- + +**免责声明**:本功能仅供学习和研究使用,不构成任何投资建议。股市有风险,投资需谨慎。 diff --git a/docs/低价擒牛实现总结.md b/docs/低价擒牛实现总结.md new file mode 100644 index 0000000..13606ae --- /dev/null +++ b/docs/低价擒牛实现总结.md @@ -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 +**状态**:✅ 已完成并可使用 diff --git a/docs/低价擒牛快速开始.md b/docs/低价擒牛快速开始.md new file mode 100644 index 0000000..fa28e48 --- /dev/null +++ b/docs/低价擒牛快速开始.md @@ -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. ✅ 根据实际情况优化策略 + +--- + +**祝您投资顺利!** 🚀📈💰 diff --git a/docs/低价擒牛策略监控功能实现总结.md b/docs/低价擒牛策略监控功能实现总结.md new file mode 100644 index 0000000..c4eb726 --- /dev/null +++ b/docs/低价擒牛策略监控功能实现总结.md @@ -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 +**状态**:✅ 已完成并可使用 diff --git a/docs/低价擒牛策略监控配置说明.md b/docs/低价擒牛策略监控配置说明.md new file mode 100644 index 0000000..59e522b --- /dev/null +++ b/docs/低价擒牛策略监控配置说明.md @@ -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天开盘时发送卖出提醒 +- 自动从监控列表移除 + +#### 条件2:MA5下穿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 diff --git a/docs/均线回归策略保存.py b/docs/均线回归策略保存.py new file mode 100644 index 0000000..d21007d --- /dev/null +++ b/docs/均线回归策略保存.py @@ -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 + + # 条件2:MA20向上 + 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 + diff --git a/longhubang.db b/longhubang.db index 8703486..f1c273b 100644 Binary files a/longhubang.db and b/longhubang.db differ diff --git a/longhubang_data.py b/longhubang_data.py index 2ebcc7e..501d5c3 100644 --- a/longhubang_data.py +++ b/longhubang_data.py @@ -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} diff --git a/low_price_bull_monitor.db b/low_price_bull_monitor.db new file mode 100644 index 0000000..1f5cbc1 Binary files /dev/null and b/low_price_bull_monitor.db differ diff --git a/low_price_bull_monitor.py b/low_price_bull_monitor.py new file mode 100644 index 0000000..4963eb9 --- /dev/null +++ b/low_price_bull_monitor.py @@ -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() diff --git a/low_price_bull_monitor_ui.py b/low_price_bull_monitor_ui.py new file mode 100644 index 0000000..6eb34ef --- /dev/null +++ b/low_price_bull_monitor_ui.py @@ -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) diff --git a/low_price_bull_selector.py b/low_price_bull_selector.py new file mode 100644 index 0000000..798b060 --- /dev/null +++ b/low_price_bull_selector.py @@ -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 diff --git a/low_price_bull_service.py b/low_price_bull_service.py new file mode 100644 index 0000000..e8586a0 --- /dev/null +++ b/low_price_bull_service.py @@ -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() diff --git a/low_price_bull_strategy.py b/low_price_bull_strategy.py new file mode 100644 index 0000000..4426344 --- /dev/null +++ b/low_price_bull_strategy.py @@ -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() diff --git a/low_price_bull_ui.py b/low_price_bull_ui.py new file mode 100644 index 0000000..8decb10 --- /dev/null +++ b/low_price_bull_ui.py @@ -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天 + - 💼 仓位控制:满仓 + - 📊 个股最大持仓:4成(40%) + - 🎯 账户最大持股数: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)}") diff --git a/notification_service.py b/notification_service.py index bdc06cc..dbc62d2 100644 --- a/notification_service.py +++ b/notification_service.py @@ -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() \ No newline at end of file +notification_service = NotificationService() + + + + + + + diff --git a/smart_monitor.db b/smart_monitor.db index 2b95286..81035a3 100644 Binary files a/smart_monitor.db and b/smart_monitor.db differ diff --git a/smart_monitor_engine.py b/smart_monitor_engine.py index 3e53caa..0c35798 100644 --- a/smart_monitor_engine.py +++ b/smart_monitor_engine.py @@ -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', '未知') } # 直接调用主程序的通知服务发送 diff --git a/stock_analysis.db b/stock_analysis.db index b38972d..6dfc294 100644 Binary files a/stock_analysis.db and b/stock_analysis.db differ diff --git a/stock_monitor.db b/stock_monitor.db index 27bc553..7d84a01 100644 Binary files a/stock_monitor.db and b/stock_monitor.db differ diff --git a/test_tdx_api.py b/test_tdx_api.py new file mode 100644 index 0000000..c777723 --- /dev/null +++ b/test_tdx_api.py @@ -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("")