- 归档 evolution/ + meta_growth/meta_watchdog/ab_research_daily - docs/evolution-archive-readme.md: 归档说明(旧模块功能+替代方案) - server.py: 新增 /api/research/effectiveness + effectiveness/summary + recommendation_log + execution_log - static/effectiveness.html: 新评估页面(概览/详细评估/推荐记录/执行记录) - 策略进化改为人驱动闭环(评估→用户决策→调整)
151 lines
6.0 KiB
Python
151 lines
6.0 KiB
Python
# -*- 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()
|