Files
MoFin/archive/evolution-cleanup-20260821/evolution/lesson_extractor.py
T
xxm 5b9d46efc6 refactor: 归档策略进化模块+新建评估页面+API
- 归档 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: 新评估页面(概览/详细评估/推荐记录/执行记录)
- 策略进化改为人驱动闭环(评估→用户决策→调整)
2026-08-21 02:47:38 +08:00

151 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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()