# -*- coding: utf-8 -*- """ evolution/lesson_extractor.py — 实盘平仓教训提取(2026-08-15 重写) 旧版病状(见 docs/decisions/2026-08-15-策略自我进化闭环重构.md): - 硬编码 version='v_next4'(已证伪策略) - 名为"已平仓交易教训",实际读的是回测 trades 而非实盘平仓 - 用 LLM 逐笔分析回测 trades(既贵又假——回测交易没有"教训"可挖) 重写方向(设计文档批准): 1. 数据源改实盘:strategy_tracking 已平仓记录(status=hit_tp/hit_sl/expired/manual_close) 2. 结合当日温区(market_regime)归因 3. 规则化提取(非 LLM):命中止盈=盈利规律,止损/超时=亏损教训 4. 每周一次,跟随 evolution_engine 同跑(周六 22:00) 幂等:按 trade_id 去重(同笔不重复写);已写过的 lesson_text 跳过。 """ import sys, os, sqlite3 from datetime import datetime, timedelta sys.path.insert(0, "/home/hmo/MoFin") DB = os.environ.get("MOFIN_DB", "/home/hmo/MoFin/data/mofin.db") LOOKBACK_DAYS = 30 # 提取近30天已平仓 # 状态 → 教训类型映射 STATUS_LESSON = { "hit_tp": ("win_pattern", "止盈有效"), "hit_sl": ("loss_pattern", "止损生效"), "expired": ("loss_pattern", "持有到期未达目标"), "manual_close": ("loss_pattern", "人工平仓"), } # 平仓原因 → 细化教训 REASON_TEXT = { "止盈触发": "触达止盈位落袋", "止损触发": "跌破止损位离场", "反弹减仓触发": "反弹遇阻减仓", "超时退出": "持有超时退出", } def get_conn(): conn = sqlite3.connect(DB) conn.row_factory = sqlite3.Row return conn def get_regime_for(conn, date_str, market="a"): """取指定日期最近的市场温区""" r = conn.execute( "SELECT regime FROM market_regime WHERE market=? AND date<=? ORDER BY date DESC LIMIT 1", (market, date_str)).fetchone() return r["regime"] if r else None def extract_lessons(days=LOOKBACK_DAYS, verbose=True): """提取近 N 天实盘已平仓交易的教训""" conn = get_conn() since = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d") rows = conn.execute(""" SELECT id, code, name, status, closed_at, close_reason, theoretical_pnl, actual_pnl, actual_exit_reason FROM strategy_tracking WHERE status != 'active' AND closed_at >= ? ORDER BY closed_at DESC """, (since,)).fetchall() if not rows: if verbose: print(f"近{days}天无已平仓记录,跳过", flush=True) conn.close() return [] # 统计 + 提取 stats = {"hit_tp": 0, "hit_sl": 0, "expired": 0, "manual_close": 0} lessons = [] written = 0 for r in rows: status = r["status"] stats[status] = stats.get(status, 0) + 1 # 只对止盈/止损提取(expired/manual_close 噪音大,跳过教训提取但统计) if status not in ("hit_tp", "hit_sl"): continue pnl = r["actual_pnl"] if r["actual_pnl"] is not None else r["theoretical_pnl"] if pnl is None: continue # 幂等:同 trade_id 已写过则跳过 exist = conn.execute( "SELECT 1 FROM strategy_lessons WHERE trade_id=? AND lesson_type=?", (r["id"], "win_pattern" if status == "hit_tp" else "loss_pattern")).fetchone() if exist: continue regime = get_regime_for(conn, (r["closed_at"] or "")[:10]) reason_txt = REASON_TEXT.get(r["close_reason"], r["close_reason"] or "平仓") if status == "hit_tp": ltype = "win_pattern" conf = 0.6 if pnl >= 5 else 0.4 text = (f"实盘止盈:{r['name']}({r['code']}) {reason_txt}," f"收益{pnl:+.1f}%" + (f"({regime}温区)" if regime else "")) else: ltype = "loss_pattern" conf = 0.6 if pnl <= -5 else 0.4 text = (f"实盘止损:{r['name']}({r['code']}) {reason_txt}," f"亏损{pnl:+.1f}%" + (f"({regime}温区)" if regime else "")) lessons.append({ "trade_id": r["id"], "lesson_type": ltype, "lesson_text": text, "confidence": conf, "profit_pct": pnl, }) # 写库 for l in lessons: conn.execute(""" INSERT INTO strategy_lessons (strategy_version, trade_id, lesson_type, lesson_text, confidence, applied) VALUES ('live_trades', ?, ?, ?, ?, 0) """, (l["trade_id"], l["lesson_type"], l["lesson_text"], l["confidence"])) written += 1 conn.commit() # 温区级汇总教训(全部已平仓按温区归因) if stats["hit_tp"] + stats["hit_sl"] > 0: tp_pnl = sum((r["actual_pnl"] if r["actual_pnl"] is not None else r["theoretical_pnl"] or 0) for r in rows if r["status"] == "hit_tp") sl_pnl = sum((r["actual_pnl"] if r["actual_pnl"] is not None else r["theoretical_pnl"] or 0) for r in rows if r["status"] == "hit_sl") summary = (f"近{days}天实盘复盘:止盈{stats['hit_tp']}笔(均{round(tp_pnl/max(stats['hit_tp'],1),1)}%)" f" / 止损{stats['hit_sl']}笔(均{round(sl_pnl/max(stats['hit_sl'],1),1)}%)") # 汇总教训写一条(幂等:按文本) exist_sum = conn.execute( "SELECT 1 FROM strategy_lessons WHERE lesson_text=? AND lesson_type='summary'", (summary,)).fetchone() if not exist_sum: conn.execute(""" INSERT INTO strategy_lessons (strategy_version, trade_id, lesson_type, lesson_text, confidence, applied) VALUES ('live_trades', NULL, 'summary', ?, 0.8, 0) """, (summary,)) written += 1 conn.commit() conn.close() if verbose: print(f"近{days}天已平仓: {stats},新增教训 {written} 条", flush=True) for l in lessons[:5]: print(f" [{l['lesson_type']}] {l['lesson_text']} ({l['confidence']})", flush=True) return lessons if __name__ == "__main__": extract_lessons()