Files
MoFin/archive/evolution-cleanup-20260821/evolution/hypothesis_miner.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

143 lines
6.3 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/hypothesis_miner.py — 数据归纳假设引擎 v2
从策略最新交易数据 + 面板特征,归纳可描述的优化假设(方向一核心)
数据源:strategy_research tradescode+date)→ 关联 panel_12d 的入场日特征
"""
import json
import sqlite3
import pandas as pd
# 可归纳特征:panel 字段名 + 标签 + 高值是否坏
CANDIDATE_FEATURES = [
("mkt_adx", "大盘趋势强度ADX", True),
("mkt_rsi", "大盘RSI", True),
("mkt_ret20", "大盘近20日涨幅", False),
("bias60", "个股60日偏离", True),
("rsi", "个股RSI", True),
("vol_ratio", "量比", False),
("sec_ret20", "行业近20日涨幅", False),
]
_PANEL_CACHE = {} # 模块级缓存:market -> panel(避免每次重载600万行pkl
def _load_panel(market):
"""加载面板:A股 panel_12d.pkl,港股 panel_12d_hk.pkl(带缓存)"""
global _PANEL_CACHE
if market in _PANEL_CACHE:
return _PANEL_CACHE[market]
path = "/tmp/panel_12d_hk.pkl" if market == "hk" else "/tmp/panel_12d.pkl"
p = pd.read_pickle(path)
p = p.sort_values(["code", "date"]).reset_index(drop=True)
# 建 code+date → 特征映射
p["_key"] = p["code"].astype(str) + "_" + p["date"].astype(str)
p = p.drop_duplicates(subset=["_key"])
p = p.set_index("_key")
_PANEL_CACHE[market] = p
return p
def load_trades(version, market, period_tag="2y"):
"""从 strategy_research 读 trades,关联面板特征"""
conn = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=10)
conn.row_factory = sqlite3.Row
r = conn.execute(
"SELECT results_json FROM strategy_research WHERE version=? AND market=? AND period_tag=? "
"ORDER BY id DESC LIMIT 1", (version, market, period_tag)).fetchone()
conn.close()
if not r:
return []
res = json.loads(r["results_json"] or "{}")
trades = res.get("trades", [])
try:
panel = _load_panel(market)
except Exception as e:
print(f"panel 加载失败: {e}", flush=True)
return []
out = []
for t in trades:
key = str(t.get("code")) + "_" + str(t.get("entry_date"))
row = panel.loc[key] if key in panel.index else None
out.append({
"profit_pct": t.get("profit_pct", 0),
"win": t.get("profit_pct", 0) > 0,
"hold_days": t.get("hold_days", 0),
"exit_reason": t.get("exit_reason", ""),
"mkt_adx": row["mkt_adx"] if row is not None and pd.notna(row.get("mkt_adx")) else None,
"mkt_rsi": row["mkt_rsi"] if row is not None and pd.notna(row.get("mkt_rsi")) else None,
"mkt_ret20": row["mkt_ret20"] if row is not None and pd.notna(row.get("mkt_ret20")) else None,
"bias60": row["bias60"] if row is not None and pd.notna(row.get("bias60")) else None,
"rsi": row["rsi"] if row is not None and pd.notna(row.get("rsi")) else None,
"vol_ratio": row["vol_ratio"] if row is not None and pd.notna(row.get("vol_ratio")) else None,
"sec_ret20": row["sec_ret20"] if row is not None and pd.notna(row.get("sec_ret20")) else None,
})
return out
def _percentile(vals, p):
if not vals:
return None
s = sorted(vals)
return s[int((len(s) - 1) * p)]
def induce_hypotheses(version, market, period_tag="2y", min_trades=20, min_effect=15):
"""归纳优化假设:找盈利/亏损组的特征差异"""
trades = load_trades(version, market, period_tag)
if len(trades) < min_trades:
return [], trades
overall_wr = sum(1 for t in trades if t["win"]) / len(trades) * 100
hypotheses = []
for feat_key, feat_label, high_is_bad in CANDIDATE_FEATURES:
vals = [t[feat_key] for t in trades if t.get(feat_key) is not None]
if len(vals) < max(5, min_trades * 0.3):
continue
hi = _percentile(vals, 0.75)
lo = _percentile(vals, 0.25)
if hi is None or lo is None or hi == lo:
continue
hi_trades = [t for t in trades if t.get(feat_key) is not None and t[feat_key] >= hi]
lo_trades = [t for t in trades if t.get(feat_key) is not None and t[feat_key] <= lo]
if len(hi_trades) < 5 or len(lo_trades) < 5:
continue
hi_wr = sum(1 for t in hi_trades if t["win"]) / len(hi_trades) * 100
lo_wr = sum(1 for t in lo_trades if t["win"]) / len(lo_trades) * 100
if high_is_bad and hi_wr < overall_wr - min_effect:
hypotheses.append({
"hypothesis": f"当{feat_label}({feat_key})≥{hi:.1f}时胜率仅{hi_wr:.0f}%(整体{overall_wr:.0f}%),应规避",
"feature": feat_key, "direction": "max", "threshold": round(hi, 2),
"win_rate_affected": round(hi_wr, 1), "win_rate_clean": round(lo_wr, 1),
"overall_wr": round(overall_wr, 1),
"effect_pp": round(overall_wr - hi_wr, 1),
"evidence": f"高分组{len(hi_trades)}笔胜率{hi_wr:.0f}% vs 低分组{len(lo_trades)}笔胜率{lo_wr:.0f}%",
"trades_affected": len(hi_trades),
})
elif not high_is_bad and lo_wr < overall_wr - min_effect:
hypotheses.append({
"hypothesis": f"当{feat_label}({feat_key})≤{lo:.1f}时胜率仅{lo_wr:.0f}%(整体{overall_wr:.0f}%),应规避",
"feature": feat_key, "direction": "min", "threshold": round(lo, 2),
"win_rate_affected": round(lo_wr, 1), "win_rate_clean": round(hi_wr, 1),
"overall_wr": round(overall_wr, 1),
"effect_pp": round(overall_wr - lo_wr, 1),
"evidence": f"低分组{len(lo_trades)}笔胜率{lo_wr:.0f}% vs 高分组{len(hi_trades)}笔胜率{hi_wr:.0f}%",
"trades_affected": len(lo_trades),
})
hypotheses.sort(key=lambda h: -h["effect_pp"])
return hypotheses, trades
if __name__ == "__main__":
import sys
version = sys.argv[1] if len(sys.argv) > 1 else "v_oversold"
market = sys.argv[2] if len(sys.argv) > 2 else "a"
hs, trades = induce_hypotheses(version, market)
print(f"=== {version} [{market}] {len(trades)}笔 ===")
for h in hs:
print(f" [Δ{h['effect_pp']:.0f}pp] {h['hypothesis']}")
print(f" 证据: {h['evidence']}")
if not hs:
print(" 未归纳出显著假设")