#!/usr/bin/env python3 """strategy_review.py — 三层策略复盘 (no_agent) 每层独立评估: 1. 信号层 — 买入/卖出/持有的timing对不对? 2. 执行层 — 止损/止盈设得合理吗? 3. 综合层 — 这波操作整体赚钱了吗? 用法: python3 scripts/strategy_review.py """ import json, sqlite3, sys, time, urllib.request from pathlib import Path from datetime import datetime from collections import Counter BASE = Path("/home/hmo/MoFin") DATA = BASE / "data" DB_PATH = DATA / "mofin.db" DECISIONS_PATH = DATA / "decisions.json" # 失败模式定义(执行层) EXEC_FAILURES = { "stop_too_tight": {"label": "止损过紧", "fix": "放宽止损到强支撑×0.95,给价格波动留空间"}, "tp_too_close": {"label": "止盈过近", "fix": "止盈放到更高阻力位,让利润奔跑"}, "stop_too_loose": {"label": "止损过宽", "fix": "收紧止损,少亏当赢"}, "tp_too_far": {"label": "止盈过远", "fix": "止盈靠近合理阻力位,提高兑现概率"}, } # 失败模式定义(信号层) SIGNAL_FAILURES = { "wrong_direction": {"label": "方向看反", "fix": "检查多周期趋势判断逻辑"}, "entry_too_early": {"label": "入场过早", "fix": "等缩量确认支撑再入,不追回调"}, "bad_signal": {"label": "信号误判", "fix": "修正timing_signal合成权重"}, "regime_mismatch": {"label": "情景错配", "fix": "加入市场情景过滤条件"}, } def fetch_price(code): try: prefix = "sh" if code.startswith(('60','68','51','56','50')) else "sz" if code.startswith(('00','30','15')) else "hk" url = f"http://qt.gtimg.cn/q={prefix}{code}" req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) resp = urllib.request.urlopen(req, timeout=5).read().decode('gbk') fld = resp.split('=')[1].strip().strip('"').strip(';').split('~') return float(fld[3]) if len(fld) > 3 else 0 except: return 0 def evaluate_strategy(s, price): """三层评估单条策略,返回 (signal_verdict, exec_verdict, overall_verdict, detail)""" code = s.get("code", "") name = s.get("name", "") sl = s.get("stop_loss", 0) or 0 tp = s.get("take_profit", 0) or 0 entry_low = s.get("entry_low", 0) or 0 entry_high = s.get("entry_high", 0) or 0 cost = s.get("cost", 0) or s.get("avg_price", 0) or 0 signal = (s.get("timing_signal", "") or s.get("current", "") or "").lower() created = s.get("created_at", "") or s.get("timestamp", "") s_type = s.get("type", "") # 持仓策略/自选策略 if not created or not price: return "skip", "skip", "skip", "数据不足" # 计算运行天数 try: days = (datetime.now() - datetime.fromisoformat(created)).days except: days = 0 # ─── 综合层:赚钱了吗? ─── if cost > 0 and s_type == "持仓策略": profit_pct = (price - cost) / cost * 100 if profit_pct > 5: overall = "盈利" elif profit_pct > -5: overall = "持平" else: overall = f"亏损{profit_pct:.0f}%" elif tp > 0 and price >= tp: overall = "触止盈" elif sl > 0 and price <= sl: overall = "触止损" else: overall = "持有中" # ─── 信号层:timing对不对? ─── is_buy_signal = any(kw in signal for kw in ["买入", "加仓", "追涨", "可买"]) is_sell_signal = any(kw in signal for kw in ["卖出", "减仓", "止损", "离场"]) is_hold_signal = any(kw in signal for kw in ["持有", "观望", "等待", "持股"]) signal_verdict = "待定" signal_fail = None if is_buy_signal or is_hold_signal: if sl > 0 and price <= sl: # 买入/持有信号下触发止损 → 信号方向可能错了 signal_verdict = "存疑" signal_fail = "wrong_direction" elif tp > 0 and price >= tp * 0.95: signal_verdict = "正确" elif entry_low > 0 and price < entry_low * 0.85: signal_verdict = "存疑" signal_fail = "entry_too_early" elif days > 30 and tp > 0 and price < entry_low: signal_verdict = "存疑" signal_fail = "wrong_direction" else: signal_verdict = "待定" elif is_sell_signal: if sl > 0 and price <= sl: signal_verdict = "正确" elif price > (cost or entry_low or 0) * 1.05: signal_verdict = "存疑" signal_fail = "bad_signal" else: signal_verdict = "待定" else: # 无明确信号 if price > (entry_high or 0): signal_verdict = "待定(价涨)" elif sl > 0 and price <= sl * 1.05: signal_verdict = "待定(近止损)" else: signal_verdict = "待定" # ─── 执行层:止损/止盈设得好不好? ─── exec_verdict = "待定" exec_fail = None # 取近期最高/最低价(判断卖飞/洗盘) recent_high = 0 recent_low = 0 sl_recovery = False if tp > 0 or sl > 0: try: prefix = "sh" if code.startswith(('60','68','51','56','50')) else "sz" if code.startswith(('00','30','15')) else "hk" url = f"http://ifzq.gtimg.cn/appstock/app/fqkline/get?param={prefix}{code},day,,,60,qfq" import subprocess as sp r = sp.run(["curl", "-s", "--max-time", "3", url], capture_output=True, text=True, timeout=5) if r.returncode == 0 and r.stdout: data = json.loads(r.stdout) day_key = 'qfqday' if prefix != 'hk' else 'day' bars = data.get('data', {}).get(f'{prefix}{code}', {}).get(day_key, []) if bars: prices = [(float(b[2]), float(b[3]), b[0]) for b in bars if len(b) > 3] # (high, low, date) recent_high = max(p[0] for p in prices) recent_low = min(p[1] for p in prices) # 检查止损触发后的走势:是否后来反弹了? if sl > 0: # 找出价格低于SL的K线 below_sl = [p for p in prices if p[1] <= sl] above_sl_later = [p for p in prices if p[1] > sl * 1.03] if below_sl and above_sl_later: # 曾跌破SL,但后来涨回去了 → 洗盘 first_below = min(below_sl, key=lambda x: x[2]) last_above = max(above_sl_later, key=lambda x: x[2]) if last_above[2] > first_below[2]: sl_recovery = True except: pass if sl > 0 and price <= sl: if sl_recovery: exec_verdict = "洗盘(触发后反弹)" exec_fail = "stop_too_tight" elif price >= sl * 0.95: exec_verdict = "临界(差一点触发)" exec_fail = "stop_too_tight" else: exec_verdict = "已触发" elif tp > 0 and (price >= tp or recent_high >= tp): # 止盈触发或曾触发过 max_price = max(price, recent_high) if max_price <= tp * 1.05: exec_verdict = "已触发" else: overshoot = (max_price - tp) / tp * 100 exec_verdict = f"卖飞({overshoot:.0f}%)" exec_fail = "tp_too_close" elif days > 45 and tp > 0 and price < entry_low: exec_verdict = "存疑(久未达标)" exec_fail = "tp_too_far" elif sl > 0 and price >= entry_low and price <= entry_high: exec_verdict = "持有中" else: exec_verdict = "待定" return signal_verdict, exec_verdict, overall, signal_fail, exec_fail def review(): start = time.time() decisions = json.loads(DECISIONS_PATH.read_text()) strategies = decisions.get("decisions", []) conn = sqlite3.connect(str(DB_PATH)) stats = {"correct": 0, "wrong": 0, "mixed": 0, "pending": 0, "total": 0} signal_fails = Counter() exec_fails = Counter() detail_lines = [] for s in strategies: if s.get("status") == "closed": continue stats["total"] += 1 code = s.get("code", "") name = s.get("name", "") price = fetch_price(code) if not price: detail_lines.append(f" ⏭️ {name}({code}): 无行情") stats["pending"] += 1 continue sv, ev, overall, sf, ef = evaluate_strategy(s, price) # 综合评级 if overall in ("盈利", "触止盈"): if sv == "正确" or "存疑" not in sv: stats["correct"] += 1 else: stats["mixed"] += 1 elif overall in ("触止损",) and "存疑" in sv: stats["wrong"] += 1 elif "存疑" in sv or "存疑" in ev: stats["wrong"] += 1 elif overall in ("持有中", "持平"): stats["mixed"] += 1 else: stats["pending"] += 1 # 记录失败模式 if sf: signal_fails[sf] += 1 if ef: exec_fails[ef] += 1 # 逐条摘要 tags = [] if overall in ("盈利", "触止盈"): tags.append("✅") elif overall == "触止损": tags.append("❌") else: tags.append("⏳") tags.append(f"信号:{sv}") tags.append(f"执行:{ev}") tags.append(f"整体:{overall}") detail_lines.append(f" {' | '.join(tags)} {name}({code})") # 写入accuracy_stats conn.execute( "INSERT OR REPLACE INTO accuracy_stats (id, total_advice, correct, wrong, partial, pending, " "accuracy_pct, updated_at) VALUES (1, ?, ?, ?, ?, ?, ?, ?)", (stats["total"], stats["correct"], stats["wrong"], stats["mixed"], stats["pending"], round(stats["correct"] / max(stats["total"] - stats["pending"], 1) * 100, 1), datetime.now().isoformat())) conn.commit() conn.close() # 输出 total_eval = stats["total"] - stats["pending"] accuracy = stats["correct"] / max(total_eval, 1) * 100 print(f"策略复盘 | {datetime.now().strftime('%Y-%m-%d')} | {stats['total']}条 | ({time.time()-start:.0f}s)") print(f" ✅正确 {stats['correct']} | ❌错误 {stats['wrong']} | ⚠️部分 {stats['mixed']} | ⏳待定 {stats['pending']}") print(f" 综合准确率: {accuracy:.1f}%") if signal_fails: print(f"\n📡 信号层失败模式:") for mode, cnt in signal_fails.most_common(): info = SIGNAL_FAILURES.get(mode, {}) print(f" {info.get('label', mode)}({cnt}次): {info.get('fix', '')}") if exec_fails: print(f"\n🎯 执行层失败模式:") for mode, cnt in exec_fails.most_common(): info = EXEC_FAILURES.get(mode, {}) print(f" {info.get('label', mode)}({cnt}次): {info.get('fix', '')}") if detail_lines: print(f"\n逐条复盘:") for line in detail_lines: print(line) if __name__ == "__main__": review()