"""--- name: cn-scan description: "A股日内做T点位扫描 — 高股息候选池 + 实时 quote + 日线/5min K, 算 SL/TP1/TP2 (不交易, 仅参考)" ---""" """ A股日内做T点位扫描 (cron 模板) - 高股息候选池 (预设, 与 scan_cn.py 共用) - 拉实时 quote + 日线/5min K 线 (longport_http) - 算 SL/TP1/TP2 用 calc_exit_levels() - 三级输出: ✅ R:R≥1.5 / ⚠️ R:R 1.0 / ❌ 否决 - 推 QQ (origin delivery) 用法: python3 calc_cn_levels.py # 跑 (默认) python3 calc_cn_levels.py --top 3 # 只看 top 3 python3 calc_cn_levels.py --period day # 用日线 (默认) python3 calc_cn_levels.py --strategy turtle_breakout # 海龟 """ import argparse import json import os import sys from pathlib import Path sys.path.insert(0, '/home/openclaw/.hermes/scripts') sys.path.insert(0, str(Path(__file__).parent)) from longport_http import get_quote, get_candlesticks from exit_levels import calc_exit_levels from indicators import atr as calc_atr, vwap as calc_vwap # === A 股候选池 (与 scan_cn.py 共用) === A_SHARE_POOL = { "601088.SH": {"name": "中国神华", "yield": 6.7, "sector": "煤炭"}, "601328.SH": {"name": "交通银行", "yield": 6.2, "sector": "银行"}, "601398.SH": {"name": "工商银行", "yield": 5.9, "sector": "银行"}, "601288.SH": {"name": "农业银行", "yield": 5.8, "sector": "银行"}, "601939.SH": {"name": "建设银行", "yield": 6.0, "sector": "银行"}, "601988.SH": {"name": "中国银行", "yield": 5.7, "sector": "银行"}, "600900.SH": {"name": "长江电力", "yield": 3.8, "sector": "电力"}, "601857.SH": {"name": "中国石油", "yield": 5.5, "sector": "能源"}, "600028.SH": {"name": "中国石化", "yield": 5.2, "sector": "能源"}, "601728.SH": {"name": "中国电信", "yield": 4.8, "sector": "电信"}, "600036.SH": {"name": "招商银行", "yield": 4.5, "sector": "银行"}, "601166.SH": {"name": "兴业银行", "yield": 5.8, "sector": "银行"}, "601818.SH": {"name": "光大银行", "yield": 5.9, "sector": "银行"}, "600377.SH": {"name": "宁沪高速", "yield": 6.2, "sector": "高速"}, "601666.SH": {"name": "平煤股份", "yield": 6.2, "sector": "煤炭"}, "600023.SH": {"name": "浙能电力", "yield": 5.5, "sector": "电力"}, "000858.SZ": {"name": "五粮液", "yield": 10.5, "sector": "白酒"}, "000568.SZ": {"name": "泸州老窖", "yield": 7.0, "sector": "白酒"}, "000937.SZ": {"name": "冀中能源", "yield": 11.0, "sector": "煤炭"}, "002304.SZ": {"name": "洋河股份", "yield": 10.8, "sector": "白酒"}, "000596.SZ": {"name": "古井贡酒", "yield": 6.9, "sector": "白酒"}, "000001.SZ": {"name": "平安银行", "yield": 5.4, "sector": "银行"}, "600519.SH": {"name": "贵州茅台", "yield": 5.0, "sector": "白酒"}, } MIN_YIELD = 5.0 # 股息率下限 def calc_levels(symbol: str, klines: list, quote: dict, side: str, min_rr: float, strategy: str = 'rsi2_revert'): """ strategy: 'rsi2_revert' | 'vwap_revert' | 'early_bird' | 'turtle_breakout' A 股默认用 rsi2_revert (震荡回归, 适合高股息股) """ if not klines or not quote: return None, '数据缺失' closes = [k['close'] for k in klines] highs = [k['high'] for k in klines] lows = [k['low'] for k in klines] volumes = [k.get('volume', 0) for k in klines] atr_vals = calc_atr(highs, lows, closes, 14) current_atr = atr_vals[-1] if not current_atr: return None, 'ATR 失败' vwaps = calc_vwap(closes, volumes) current_vwap = vwaps[-1] current_price = quote.get('price') or quote.get('last_done', 0) if not current_price: return None, '价格缺失' # ── 策略参数映射 ── if strategy == 'turtle_breakout': vol_sl_multi, vol_tp1_multi, vol_tp2_multi = 2.0, 4.0, 8.0 eff_min_rr = min(min_rr, 1.0) use_vwap = False elif strategy == 'vwap_revert': vol_sl_multi, vol_tp1_multi, vol_tp2_multi = 1.0, 2.0, 3.0 eff_min_rr = max(min_rr, 1.5) use_vwap = True elif strategy == 'early_bird': vol_sl_multi, vol_tp1_multi, vol_tp2_multi = 1.5, 2.0, 3.0 eff_min_rr = min_rr use_vwap = False else: # rsi2_revert vol_sl_multi, vol_tp1_multi, vol_tp2_multi = 1.5, 2.0, 3.0 eff_min_rr = min_rr use_vwap = True eff_vwap = current_vwap if use_vwap else None return calc_exit_levels( entry=current_price, atr=current_atr, current_price=current_price, day_high=quote.get('high') or max(highs), day_low=quote.get('low') or min(lows), prev_high=max(highs), prev_low=min(lows), vwap=eff_vwap, side=side, min_rr=eff_min_rr, vol_sl_multi=vol_sl_multi, vol_tp1_multi=vol_tp1_multi, vol_tp2_multi=vol_tp2_multi, ), strategy def format_cn_output(levels, change_pct, current_price, side, symbol, info, mode): """A 股格式: ¥ 符号, 股息率""" name = info.get('name', symbol) div_yield = info.get('yield', 0) sector = info.get('sector', '') if mode == 'strict': return ( f"\n📈 **{symbol}** {name} (股息率 {div_yield}% | {sector})\n" f"现价 ¥{current_price:.2f} ({change_pct:+.2f}%) | {side.upper()}\n" f"SL ¥{levels.sl:.2f}\n" f"TP1 ¥{levels.tp1:.2f}\n" f"TP2 ¥{levels.tp2:.2f}\n" f"R:R 1:{levels.rr_ratio:.2f} ✅" ) elif mode == 'relaxed': return ( f"\n📈 **{symbol}** {name} (股息率 {div_yield}% | {sector})\n" f"现价 ¥{current_price:.2f} ({change_pct:+.2f}%) | {side.upper()} [R:R 1.0 宽松]\n" f"SL ¥{levels.sl:.2f}\n" f"TP1 ¥{levels.tp1:.2f}\n" f"TP2 ¥{levels.tp2:.2f}\n" f"R:R 1:{levels.rr_ratio:.2f} ⚠️" ) else: # atr_adj return ( f"\n📈 **{symbol}** {name} (股息率 {div_yield}% | {sector})\n" f"现价 ¥{current_price:.2f} ({change_pct:+.2f}%) | {side.upper()} [ATR 调整]\n" f"SL ¥{levels.sl:.2f}\n" f"TP1 ¥{levels.tp1:.2f}\n" f"TP2 ¥{levels.tp2:.2f}\n" f"R:R 1:{levels.rr_ratio:.2f} ⚠️" ) def main(): ap = argparse.ArgumentParser() ap.add_argument('--top', type=int, default=5, help='top N (default 5)') ap.add_argument('--period', default='day', choices=['day', '5m', '15m', '1h'], help='K线周期 (default day)') ap.add_argument('--strategy', default='rsi2_revert', choices=['rsi2_revert', 'vwap_revert', 'early_bird', 'turtle_breakout'], help='策略 (default rsi2_revert, A 股推荐)') args = ap.parse_args() # 过滤高股息 candidates = {k: v for k, v in A_SHARE_POOL.items() if v.get('yield', 0) >= MIN_YIELD} top = list(candidates.items())[:args.top] from datetime import date today = date.today().isoformat() print(f"📊 A 股日内做T点位扫描 ({today}, top {args.top}, 策略={args.strategy})") print(f"📋 共扫描 {len(top)} 支\n") output_lines = [] for symbol, info in top: print(f"--- {symbol} {info['name']} ---") quote = get_quote(symbol) if not quote: print(f" ❌ quote 拉取失败") continue klines = get_candlesticks(symbol, args.period, 30) if not klines: print(f" ❌ K线 拉取失败") continue current_price = quote.get('price') or quote.get('last_done', 0) change_pct = quote.get('change_pct', 0) print(f" 现价: ¥{current_price:.2f} ({change_pct:+.2f}%)") side = 'long' if change_pct > 0 else 'short' levels, _ = calc_levels(symbol, klines, quote, side, min_rr=1.5, strategy=args.strategy) if levels: print(f" ✅ R:R 1.5 [{args.strategy}] → SL=¥{levels.sl:.2f} TP1=¥{levels.tp1:.2f} TP2=¥{levels.tp2:.2f} R:R=1:{levels.rr_ratio:.2f}") output_lines.append(format_cn_output(levels, change_pct, current_price, side, symbol, info, 'strict')) else: levels_relaxed, _ = calc_levels(symbol, klines, quote, side, min_rr=1.0, strategy=args.strategy) if levels_relaxed: print(f" ⚠️ R:R 1.5 否决, 1.0 通过 → R:R=1:{levels_relaxed.rr_ratio:.2f}") output_lines.append(format_cn_output(levels_relaxed, change_pct, current_price, side, symbol, info, 'relaxed')) else: closes = [k['close'] for k in klines] highs = [k['high'] for k in klines] lows = [k['low'] for k in klines] volumes = [k.get('volume', 0) for k in klines] atr_v = calc_atr(highs, lows, closes, 14)[-1] vwaps_v = calc_vwap(closes, volumes) levels_alt = calc_exit_levels( entry=current_price, atr=atr_v, current_price=current_price, day_high=max(highs), day_low=min(lows), prev_high=max(highs), prev_low=min(lows), vwap=vwaps_v[-1], side=side, min_rr=1.5, vol_sl_multi=1.5, vol_tp1_multi=3.0, ) if levels_alt: print(f" ⚠️ ATR 调整 → R:R=1:{levels_alt.rr_ratio:.2f}") output_lines.append(format_cn_output(levels_alt, change_pct, current_price, side, symbol, info, 'atr_adj')) else: print(f" ❌ 全部场景否决") print() if output_lines: header = f"📊 A 股日内做T点位 ({today})\n⚠️ 仅参考, 不交易\n" print("\n=== QQ 推送内容 ===") print(header + "\n---\n".join(output_lines)) else: print("\n💤 全部场景否决, 无输出") if __name__ == '__main__': main()