diff --git a/archive/evolution-cleanup-20260821/ab_research_daily.py b/archive/evolution-cleanup-20260821/ab_research_daily.py new file mode 100644 index 00000000..109c2504 --- /dev/null +++ b/archive/evolution-cleanup-20260821/ab_research_daily.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""AB路线每日LLM主导研究 v2(老莫2026-08-18) +在原规则化分析基础上,集成 LLM 生成深度研究结论(真正"LLM主导") +1. 读温区覆盖 + 进化中心 + B组候选 +2. LLM 分析薄弱环节 → 建议尝试 +3. 写 strategy_research_log 表 +""" +import sys, os, json, sqlite3 +from datetime import datetime + +sys.path.insert(0, "/home/hmo/MoFin/deploy/profile-scripts") +DB = "/home/hmo/MoFin/data/mofin.db" +CENTER = "/home/hmo/MoFin/data/evolution_center.json" + +def ensure_table(conn): + conn.execute("""CREATE TABLE IF NOT EXISTS strategy_research_log ( + id INTEGER PRIMARY KEY AUTOINCREMENT, log_date TEXT NOT NULL, market TEXT, + weak_regime TEXT, finding TEXT, experiment TEXT, result TEXT, + produced_strategy TEXT, produced_verified INTEGER DEFAULT 0, llm_model TEXT, created_at TEXT)""") + conn.commit() + +def load_center(): + if not os.path.exists(CENTER): return {} + try: return json.load(open(CENTER)) + except: return {} + +def build_prompt(coverage, center): + """构造 LLM 研究 prompt""" + line = [] + line.append("你是MoFin策略研究员。分析当前策略覆盖,找出薄弱环节并给出研究建议。") + line.append("温区覆盖(trades>=30,2y):") + for c in coverage: + line.append(f"- {c['market']}/{c['regime']}: {c['count']}个策略") + bg = center.get("b_group") or [] + if bg: + line.append(f"B组候选: {len(bg)}条") + for b in bg[:3]: + line.append(f" - {str(b)[:80]}") + line.append("\n请输出:") + line.append("1. 最薄弱的温区/环节(策略匮乏或合格策略少)") + line.append("2. 具体研究建议(做什么尝试)") + line.append("3. 预期成果类型") + line.append("格式:发现|建议|预期") + return "\n".join(line) + +def analyze_llm(coverage, center): + """LLM 生成研究结论""" + try: + from llm_client import call_llm + prompt = build_prompt(coverage, center) + res = call_llm(prompt) + return str(res)[:400] if res else None + except Exception as e: + return f"[LLM调用失败: {e}]" + +def main(): + conn = sqlite3.connect(DB, timeout=30) + conn.execute("PRAGMA busy_timeout=30000") + ensure_table(conn) + today = datetime.now().strftime("%Y-%m-%d") + if conn.execute("SELECT COUNT(*) FROM strategy_research_log WHERE log_date=?", (today,)).fetchone()[0]: + print(f"[AB研究] {today} 已有记录"); conn.close(); return + # 读取覆盖 + rows = conn.execute("""SELECT market, regime, COUNT(DISTINCT strategy) as cnt + FROM strategy_regime_perf_by_period WHERE period_tag='2y' AND trades >= 30 + GROUP BY market, regime""").fetchall() + coverage = [{"market": r[0], "regime": r[1], "count": r[2]} for r in rows] + center = load_center() + # 基础规则发现 + findings = [] + weak = [] + if coverage: + c_sorted = sorted(coverage, key=lambda x: x["count"]) + weak = c_sorted[:2] + findings.append("覆盖最少的温区: " + "; ".join(f"{c['market']}/{c['regime']}({c['count']})" for c in weak)) + # LLM 深度分析 + if coverage: + llm_res = analyze_llm(coverage, center) + if llm_res: + findings.append("LLM分析: " + llm_res) + finding_text = "; ".join(findings) or "无明显薄弱点" + weak_rg = weak[0]["regime"] if weak else "" + weak_mkt = weak[0]["market"] if weak else "a" + conn.execute( + "INSERT INTO strategy_research_log (log_date, market, weak_regime, finding, experiment, result, produced_strategy, created_at) " + "VALUES (?,?,?,?,?,?,?,?)", + (today, weak_mkt, weak_rg, finding_text, "LLM主导温区覆盖+B组分析", "记录待验证", "", + datetime.now().isoformat())) + conn.commit() + print(f"[AB研究] {today} 记录完成 (LLM主导)") + conn.close() + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/archive/evolution-cleanup-20260821/evolution/__init__.py b/archive/evolution-cleanup-20260821/evolution/__init__.py new file mode 100644 index 00000000..42d799b7 --- /dev/null +++ b/archive/evolution-cleanup-20260821/evolution/__init__.py @@ -0,0 +1,70 @@ +""" +evolution/__init__.py — 自我进化模块 +Loop Engineering: 策略健康度监控 + 教训提取 + 自动迭代 +""" +import sqlite3, os + +DB = os.environ.get('MOFIN_DB', '/home/hmo/MoFin/data/mofin.db') + +def init_evolution_tables(conn=None): + """初始化进化模块数据表""" + close_conn = False + if conn is None: + conn = sqlite3.connect(DB) + close_conn = True + + # 策略健康度每日快照 + conn.execute(""" + CREATE TABLE IF NOT EXISTS strategy_health ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + strategy_version TEXT NOT NULL, + date TEXT NOT NULL, + live_trades INTEGER DEFAULT 0, + live_wins INTEGER DEFAULT 0, + live_return_pct REAL DEFAULT 0, + backtest_wr REAL DEFAULT 0, + backtest_avg_ret REAL DEFAULT 0, + deviation REAL DEFAULT 0, + health_score REAL DEFAULT 0, + created_at TEXT DEFAULT (datetime('now','localtime')), + UNIQUE(strategy_version, date) + ) + """) + conn.execute("CREATE INDEX IF NOT EXISTS idx_health_version_date ON strategy_health(strategy_version, date)") + + # 教训库 + conn.execute(""" + CREATE TABLE IF NOT EXISTS strategy_lessons ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + strategy_version TEXT NOT NULL, + trade_id INTEGER, + lesson_type TEXT NOT NULL, + lesson_text TEXT NOT NULL, + confidence REAL DEFAULT 0.5, + applied INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now','localtime')) + ) + """) + conn.execute("CREATE INDEX IF NOT EXISTS idx_lessons_version ON strategy_lessons(strategy_version, applied)") + + # 策略迭代历史 + conn.execute(""" + CREATE TABLE IF NOT EXISTS strategy_evolution ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + parent_version TEXT NOT NULL, + child_version TEXT NOT NULL, + change_description TEXT, + backtest_result TEXT, + promoted INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now','localtime')) + ) + """) + conn.execute("CREATE INDEX IF NOT EXISTS idx_evolution_parent ON strategy_evolution(parent_version, promoted)") + + conn.commit() + if close_conn: + conn.close() + +if __name__ == '__main__': + init_evolution_tables() + print("进化模块数据表初始化完成") diff --git a/archive/evolution-cleanup-20260821/evolution/b_group_miner.py b/archive/evolution-cleanup-20260821/evolution/b_group_miner.py new file mode 100644 index 00000000..f7a5759f --- /dev/null +++ b/archive/evolution-cleanup-20260821/evolution/b_group_miner.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +"""evolution/b_group_miner.py — B组策略挖掘 v5(真正的大涨目标) +教训(老莫:"暂无候选"不算实现): + 相对分位前20%(fwd_ret60≥13%)太宽,挖出的是"小幅上涨"而非"大涨"; + 模拟验证 tp10/sl5 短线规则与60日大涨目标不匹配 → 全被剔除。 +修正: + 果 = fwd_ret60 >= 30%(绝对大涨,趋势市基线7.8%) + 因子组合扫描找大涨率显著提升 + 模拟验证用匹配大涨的规则(tp20%/sl10%/maxh40)+ 扫描最优参数 +""" +import json +import sqlite3 +import numpy as np +import pandas as pd +from datetime import datetime +from itertools import combinations + +DATA_DIR = "/home/hmo/MoFin/data" +OUT_JSON = f"{DATA_DIR}/b_group_candidates.json" +BIG_TH = 30 # 大涨目标 + + +def load_regime_map(market="a"): + conn = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=10) + rows = conn.execute("SELECT date, regime FROM market_regime WHERE market=?", (market,)).fetchall() + conn.close() + return {d: r for d, r in rows} + + +def load_panel(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) + p["fwd_ret60"] = p.groupby("code")["close"].transform(lambda x: x.shift(-60) / x - 1) * 100 + return p + + +def scan_big(market, regime, panel, min_n=500): + """扫描因子组合:找绝对大涨率显著提升的组合""" + rm = load_regime_map(market) + p = panel.copy() + p["_regime"] = p["date"].map(rm) + sub = p[p["_regime"] == regime].dropna(subset=["fwd_ret60"]) + if len(sub) < min_n: + return [] + sub["is_big"] = (sub["fwd_ret60"] >= BIG_TH).astype(int) + br = sub["is_big"].mean() * 100 + print(f"[{market}/{regime}] 样本{len(sub)} 基线大涨率(60d>={BIG_TH}%){br:.1f}%") + + # 因子池(方向:大盘弱 + 个股超跌 + 小盘低估值 + 基本面催化) + factor_defs = { + "mkt_ret20": ("<", 0), "mkt_rsi": ("<", 50), "mkt_adx": (">", 20), + "bias60": ("<", -10), "rsi": ("<", 40), "dist_lo20": (">", 5), + "mcap_q": ("<", 0.3), "pe_q": ("<", 0.3), "pb_q": ("<", 0.3), + "sec_ret20": ("<", 0), "news3": (">=", 1), "vol_ratio": (">", 1.2), + "ret20": ("<", 0), "flow5": (">", 0), + } + # 单条件测试 + single = [] + for feat, (op, val) in factor_defs.items(): + if feat not in sub.columns: + continue + cond = sub[feat] < val if op == "<" else sub[feat] > val + m = sub[cond] + if len(m) < 200: + continue + rate = m["is_big"].mean() * 100 + if rate > br + 0.5: # 单条件提升>0.5pp 进组合池(多因子叠加才有大提升) + single.append((feat, round(rate, 1), len(m), round(rate - br, 1))) + single.sort(key=lambda x: -x[3]) + print(" 单条件:", single[:6]) + + # 4-6 因子组合(从单条件提升>0.5pp 里取 8 个,测 4/5/6 组合) + pool = [s[0] for s in single if s[3] > 0.5][:8] + results = [] + for k in [4, 5, 6]: + for combo in combinations(pool, k): + cond = pd.Series(True, index=sub.index) + for feat in combo: + op, val = factor_defs[feat] + cond &= (sub[feat] < val) if op == "<" else (sub[feat] > val) + m = sub[cond] + if len(m) < 200: + continue + rate = m["is_big"].mean() * 100 + avg = m["fwd_ret60"].mean() + results.append(({f: factor_defs[f] for f in combo}, len(m), round(rate, 1), + round(avg, 1), round(rate - br, 1), len(combo))) + results.sort(key=lambda x: -x[4]) + return results[:5] + + +def _simulate_verify(market, regime, panel, cond, tp=20, sl=10, maxh=40): + """模拟验证:候选在温区的模拟交易(大涨匹配规则)""" + rm = load_regime_map(market) + sub = panel.copy() + sub["_regime"] = sub["date"].map(rm) + sub = sub[(sub["_regime"] == regime) & cond].copy() + if len(sub) < 200: + return None + sub = sub.sort_values(["code", "date"]) + trades = [] + trade_details = [] + for code, g in sub.groupby("code"): + g = g.sort_values("date") + idxs = list(g.index) + for k, i in enumerate(idxs): + fut = g.iloc[k+1:k+maxh+1] + if len(fut) < 2: + continue + ep = g.loc[i, "close"] + if ep <= 0: + continue + res = None + hold_days = maxh + for j, (_, fb) in enumerate(fut.iterrows()): + if fb["close"] <= ep * (1 - sl / 100): + res = -sl + hold_days = j + 1 + break + if fb["close"] >= ep * (1 + tp / 100): + res = tp + hold_days = j + 1 + break + if res is None: + res = (fut.iloc[-1]["close"] / ep - 1) * 100 + hold_days = len(fut) + trades.append(res) + trade_details.append({"entry_date": str(g.loc[i, "date"]), "pnl_pct": round(res, 2), + "profit_pct": round(res, 2), "hold_days": hold_days, + "code": str(code)}) + if not trades: + return None + wins = [x for x in trades if x > 0] + if not trades: + return None + return {"n": len(trades), "win_rate": len(wins) / len(trades) * 100, + "avg_pnl": sum(trades) / len(trades), "trades": trade_details} + + +def to_entry(cond_dict): + entry = {} + for feat, (op, val) in cond_dict.items(): + key = feat + ("_min" if (op == ">" or op == ">=") else "_max") + entry[key] = float(val) + return entry + + +def mine(market="a", regimes=None): + regimes = regimes or ["trend_up", "choppy", "trend_down"] + panel = load_panel(market) + out = {"market": market, "mined_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "candidates": []} + for rg in regimes: + combos = scan_big(market, rg, panel) + for cond, n, rate, avg, extra, nf in combos[:3]: + c = pd.Series(True, index=panel.index) + for feat, (op, val) in cond.items(): + if feat not in panel.columns: + c = None + break + c &= (panel[feat] < val) if op == "<" else (panel[feat] > val) + verified = None + if c is not None: + # 多参数模拟验证,取最优 + # 先筛胜率≥50%的参数,再取其中收益最高(2026-08-16 修正:原取收益最高可能选中胜率<50%参数) + passed_params = [] + for tp, sl, mh in [(20, 10, 40), (25, 10, 45), (30, 12, 50), (15, 8, 35)]: + r = _simulate_verify(market, rg, panel, c, tp, sl, mh) + if r and r["win_rate"] >= 50 and r["avg_pnl"] > 0: + passed_params.append((tp, sl, mh, r)) + if passed_params: + best = max(passed_params, key=lambda x: x[3]["avg_pnl"]) + tp, sl, mh, rd = best + tn, twr, tavg = rd["n"], rd["win_rate"], rd["avg_pnl"] + if twr >= 50 and tavg > 0: + cand = { + "regime": rg, "market": market, "group": "B", "status": "verified", + "entry": to_entry(cond), "trades_est": n, "big_rate": rate, + "avg60": avg, "excess_pp": extra, + "sim_trades": tn, "sim_win_rate": round(twr, 1), "sim_avg_pnl": round(tavg, 2), + "sim_tp": tp, "sim_sl": sl, "sim_maxh": mh, + "trades": rd.get("trades", []), + "hypothesis": f"[{rg}] 由果及因{nf}因子: {list(cond.keys())} → 大涨率{rate}%(基线+{extra}pp)", + } + out["candidates"].append(cand) + print(f" [{rg}] {list(cond.keys())} ✅大涨率{rate}% 模拟胜率{twr:.0f}%/均{tavg:.2f}%", flush=True) + else: + print(f" [{rg}] {list(cond.keys())} 模拟未达标(胜率{twr:.0f}%/均{tavg:.2f}%) 剔除", flush=True) + else: + print(f" [{rg}] {list(cond.keys())} 模拟无结果 剔除", flush=True) + return out + + +if __name__ == "__main__": + import sys + market = sys.argv[1] if len(sys.argv) > 1 else "a" + res = mine(market) + with open(OUT_JSON, "w", encoding="utf-8") as f: + json.dump(res, f, ensure_ascii=False, indent=1) + print(f"写入 {OUT_JSON}: {len(res['candidates'])} 个候选") diff --git a/archive/evolution-cleanup-20260821/evolution/evolution_api.py b/archive/evolution-cleanup-20260821/evolution/evolution_api.py new file mode 100644 index 00000000..a25527fe --- /dev/null +++ b/archive/evolution-cleanup-20260821/evolution/evolution_api.py @@ -0,0 +1,256 @@ +""" +evolution/evolution_api.py — 进化模块 API 接口 +供 dashboard 查询健康度、教训、迭代历史 +""" +import sys, os, json, sqlite3 + +sys.path.insert(0, '/home/hmo/MoFin') +sys.path.insert(0, '/home/hmo/MoFin/deploy/profile-scripts') + +DB = os.environ.get('MOFIN_DB', '/home/hmo/MoFin/data/mofin.db') + + +def get_evolution_dashboard(): + """进化模块 Dashboard 数据""" + conn = sqlite3.connect(DB) + conn.row_factory = sqlite3.Row + + # 最近健康度(近30天) + health = [] + for r in conn.execute(""" + SELECT strategy_version, date, live_trades, live_wins, live_return_pct, + backtest_wr, backtest_avg_ret, deviation, health_score + FROM strategy_health ORDER BY date DESC LIMIT 30 + """).fetchall(): + health.append(dict(r)) + + # 最近教训(近20条) + lessons = [] + for r in conn.execute(""" + SELECT strategy_version, lesson_type, lesson_text, confidence, applied, created_at + FROM strategy_lessons ORDER BY id DESC LIMIT 20 + """).fetchall(): + lessons.append(dict(r)) + + # 迭代历史 + evolution = [] + for r in conn.execute(""" + SELECT parent_version, child_version, change_description, promoted, created_at + FROM strategy_evolution ORDER BY id DESC LIMIT 20 + """).fetchall(): + evolution.append(dict(r)) + + # 当前策略基线(2026-08-15: 数据驱动——跟随 strategy_weights.json 激活集合, + # 原硬编码 ['v_weak','v_oversold'] 与温区路由脱节,激活策略换了一批但基线还显示旧的) + def _active_versions(): + try: + d = json.loads(open('/home/hmo/MoFin/data/strategy_weights.json', encoding='utf-8').read()) + vs = list(d.get('active') or []) + vs += list(((d.get('markets') or {}).get('hk') or {}).get('active') or []) + seen, out = set(), [] + for v in vs: + if v and v not in seen: + seen.add(v) + out.append(v) + return out or ['v_weak', 'v_oversold'] + except Exception: + return ['v_weak', 'v_oversold'] + baseline = {} + for v in _active_versions(): + r = conn.execute(""" + SELECT results_json FROM strategy_research + WHERE version=? AND period_tag='5y' ORDER BY id DESC LIMIT 1 + """, (v,)).fetchone() + if r: + res = json.loads(r[0]) + s = res.get('summary', {}) + pf = s.get('portfolio_full', {}) + baseline[v] = { + 'win_rate': s.get('win_rate', 0), + 'total_return': pf.get('total_return_pct', 0), + 'cagr': pf.get('cagr_pct', 0), + 'max_dd': pf.get('portfolio_max_dd_pct', 0), + } + + # ── 2026-08-16 进化机制数据:读预计算快照(precompute_evolution.py 定期生成,避免实时重算)── + hypotheses = [] + b_group = [] + qual_overview = [] + try: + _ec = json.loads(open('/home/hmo/MoFin/data/evolution_center.json', encoding='utf-8').read()) + hypotheses = _ec.get('hypotheses', []) + b_group = _ec.get('b_group', []) + qual_overview = _ec.get('qual_overview', []) + except Exception: + pass + + conn.close() + + return { + 'health': health, + 'lessons': lessons, + 'evolution': evolution, + 'baseline': baseline, + 'hypotheses': hypotheses, + 'b_group': b_group, + 'qual_overview': qual_overview, + } + + +def get_combo_dashboard(): + """组合方案 Dashboard 数据 (2026-08-02 新增) + 返回: 当前组合方案(v_next4+v_mr按regime分工) + 组合回测版本(v_combo) + 市场阶段 + """ + conn = sqlite3.connect(DB) + conn.row_factory = sqlite3.Row + + # 1. 当前市场阶段 (market_regime) + regime = None + r = conn.execute("SELECT * FROM market_regime ORDER BY date DESC LIMIT 1").fetchone() + if r: + regime = dict(r) + + # 2. 组合回测版本 (v_combo 家族) + combos = [] + rows = conn.execute( + "SELECT id, version, market, period_tag, created_at, results_json" + " FROM strategy_research WHERE version LIKE '%combo%' OR version LIKE 'v_combo%'" + " ORDER BY id DESC" + ).fetchall() + for r in rows: + d = dict(r) + res = json.loads(d.pop("results_json") or "{}") + s = res.get("summary", {}) + pf = s.get("portfolio_full", {}) + p5 = s.get("portfolio", {}) + d["summary_stats"] = { + "total_trades": s.get("total_trades"), + "win_rate": s.get("win_rate"), + "avg_profit_pct": s.get("avg_profit_pct"), + "avg_hold_days": s.get("avg_hold_days"), + "sharpe_ratio": s.get("sharpe_ratio"), + "profit_factor": s.get("profit_factor"), + "universality": s.get("universality", {}), + "portfolio": p5, + "portfolio_full": pf, + } + combos.append(d) + + # 3. 组合成员策略的独立指标 + # 2026-08-11 更新:组合成员 = v_weak(实盘)+ p_oversold(新策略),替代旧的 v_next4+v_mr + # v_next4 移除(池内卫星仓,全市场失效;现有池子票不是它选的) + members = {} + for v in ["v_weak", "p_oversold"]: + sel_v = "v_weak" if v == "v_mr" else None + # 2026-08-12: p_oversold 实盘名 → 回测数据存 v_oversold(研究名),两个都查 + candidates = ["v_oversold", "p_oversold"] if v == "p_oversold" else (["v_weak", "v_mr_sel", v] if sel_v else [v]) + r = None + used_sel = False + for cv in candidates: + r = conn.execute( + "SELECT results_json FROM strategy_research" + " WHERE version=? AND period_tag='10y' ORDER BY id DESC LIMIT 1", + (cv,), + ).fetchone() + if r: + used_sel = (cv == "v_weak") + break + if r: + res = json.loads(r[0]) + s = res.get("summary", {}) + pf = s.get("portfolio_full", {}) + p5 = s.get("portfolio", {}) + members[v] = { + "role": "弱市超跌确认(实盘)" if v == "v_weak" else "预测超跌反弹(新策略)", + "version": ("v_weak" if used_sel else "v_mr") if v == "v_mr" else v, + "is_sel": used_sel, + "trades": s.get("total_trades"), + "win_rate": s.get("win_rate"), + "avg_profit_pct": s.get("avg_profit_pct"), + "avg_hold_days": s.get("avg_hold_days"), + "cagr_pct": p5.get("cagr_pct"), + "return_pct": p5.get("total_return_pct"), + "max_dd_pct": p5.get("portfolio_max_dd_pct"), + "slots": p5.get("slots") or 6, + "universality": s.get("universality", {}), + # 组合模拟实际执行笔数(扣费后) + 年均(手工可行性参考) + "positions_taken_5slot": p5.get("positions_taken"), + "positions_taken_full": pf.get("positions_taken"), + } + # 2026-08-11:p_oversold 无回测数据时给兜底卡片(新策略待回测) + if "p_oversold" not in members: + members["p_oversold"] = { + "role": "预测超跌反弹(新策略)", + "version": "p_oversold", + "is_sel": False, + "trades": None, "win_rate": None, "avg_profit_pct": None, + "avg_hold_days": None, "cagr_pct": None, "return_pct": None, + "max_dd_pct": None, "slots": 10, + "universality": {}, + "positions_taken_5slot": None, "positions_taken_full": None, + "note": "新策略,待回测/实盘验证", + } + + conn.close() + + # 2026-08-13 温区自适应:并入 strategy_weights.json(当前温区+温度+各策略权重/激活) + # + strategy_alerts.json(三振出局状态) + import json as _json + from pathlib import Path as _Path + _d = _Path("/home/hmo/MoFin/data") + weights_data = None + alerts_data = None + try: + _w = _d / "strategy_weights.json" + if _w.exists(): + weights_data = _json.loads(_w.read_text(encoding="utf-8")) + except Exception: + pass + try: + _a = _d / "strategy_alerts.json" + if _a.exists(): + alerts_data = _json.loads(_a.read_text(encoding="utf-8")) + except Exception: + pass + + return { + "regime": regime, + "regime_weights": weights_data, # 当前温区/温度/各策略权重/激活 + "strategy_alerts": alerts_data, # 三振出局状态 + "combos": combos, + "members": members, + "routing": [ + {"regime": "trend_up", "active": "p_oversold", "action": "预测超跌反弹", "desc": "趋势市/反弹期, p_oversold 预测超跌反弹"}, + {"regime": "choppy", "active": "v_weak", "action": "弱市超跌确认", "desc": "震荡/下跌市, v_weak 均值回复主战场"}, + {"regime": "trend_down", "active": "v_weak", "action": "深超跌管理", "desc": "下跌市, v_weak 管理超跌持仓"}, + ], + } + + +def get_health_trend(version='v_weak', days=30): + """健康度趋势""" + conn = sqlite3.connect(DB) + conn.row_factory = sqlite3.Row + rows = conn.execute(""" + SELECT date, health_score, deviation, live_trades + FROM strategy_health WHERE strategy_version=? ORDER BY date DESC LIMIT ? + """, (version, days)).fetchall() + conn.close() + return [dict(r) for r in rows] + + +def record_evolution(parent, child, description, backtest_result=None, promoted=0): + """记录一次策略迭代""" + conn = sqlite3.connect(DB) + conn.execute(""" + INSERT INTO strategy_evolution (parent_version, child_version, change_description, backtest_result, promoted) + VALUES (?, ?, ?, ?, ?) + """, (parent, child, description, json.dumps(backtest_result) if backtest_result else None, promoted)) + conn.commit() + conn.close() + + +if __name__ == '__main__': + d = get_evolution_dashboard() + print(f"健康度: {len(d['health'])}条, 教训: {len(d['lessons'])}条, 迭代: {len(d['evolution'])}条") + print(f"基线: {list(d['baseline'].keys())}") diff --git a/archive/evolution-cleanup-20260821/evolution/evolution_engine.py b/archive/evolution-cleanup-20260821/evolution/evolution_engine.py new file mode 100644 index 00000000..bbff4152 --- /dev/null +++ b/archive/evolution-cleanup-20260821/evolution/evolution_engine.py @@ -0,0 +1,438 @@ +# -*- coding: utf-8 -*- +""" +evolution/evolution_engine.py — 策略自我进化引擎(每周六 22:00,hermes cron) + +设计依据:docs/decisions/2026-08-15-策略自我进化闭环重构.md(老莫已批准) +闭环:统计数据每日自动更新 → 本引擎每周检测退化 → 生成参数变体 → 回测验证 → 有价值才推送 + +流程: + 1. 读激活策略集合(data/strategy_weights.json:A股 active + 港股 markets.hk.active) + 2. 退化信号检测(宁缺毋滥,任一命中即触发研究): + S1 健康度连续低:strategy_health 连续 5 天 health_score < 40(排除 50 中性=无实盘数据) + S2 温区表现衰减:激活策略在其适应温区(strategy_regime_perf)温区级组合年化 cagr_pct < 0 + 3. 有退化 → 生成参数变体: + - 只对 lab.STRATEGIES 里可回测的策略(v_oversold/v_weak 等标准回测体系) + - 参数空间从策略 config 实际数值字段出发(递归遍历,单变量 ±20%,一次只动一个) + - 港股走 hk_backtest(entry/exit 字段 ±20%) + 4. 回测验证(统一资金约束): + - A股:lab.run_backtest(save=False),取 portfolio_full + - 港股:hk_backtest.gen_trades_defensive + lab.portfolio_sim(max_positions=8) + - 验收:温区级组合年化 cagr_pct ≥ 原策略 + 3pp 且 max_dd 不劣化超过 2pp + 5. 达标变体 → 写 strategy_evolution(promoted=0)+ XMPP 推送老莫(附对比证据) + (永不自动 promote,老莫说"上线"才进路由) + 6. 无退化或变体全灭 → 当周静默(不制造噪音) + +单例守卫:fcntl.flock 防并发(deploy_guard / 手动重跑均安全) +""" +import sys, os, json, sqlite3, copy, io, traceback +from datetime import datetime, timedelta + +sys.path.insert(0, "/home/hmo/MoFin") +sys.path.insert(0, "/home/hmo/MoFin/deploy/profile-scripts") +sys.path.insert(0, "/home/hmo/MoFin/evolution") + +DB = os.environ.get("MOFIN_DB", "/home/hmo/MoFin/data/mofin.db") +WEIGHTS_JSON = "/home/hmo/MoFin/data/strategy_weights.json" + +# 退化信号参数 +HEALTH_LOW = 40 # 健康度低于此值视为低 +HEALTH_STREAK_DAYS = 5 # 连续天数 +REGIME_CAGR_BAD = 0.0 # 温区级组合年化低于此值视为退化 + +# 变体生成参数 +VAR_PCT = 0.20 # ±20% 网格 +MAX_VARIANTS = 6 # 每策略最多生成变体数 +MAX_VARIANTS_TEST = 1 # 最多回测验证的变体数(资源约束:单变体2y全市场回测6-8分钟/6-8GB,详见下方BT注释) + +# 验证回测周期(2026-08-15:原5y全市场回测单变体8+分钟/5GB内存,改为2y控制资源; +# 验收对比用同周期原策略数据,相对改善仍有效) +BT_START = "2024-07-01" +BT_END = "2026-07-24" +BT_PERIOD_TAG = "2y" + +# 验收门槛(2026-08-15 口径说明:变体与 parent 用同周期 strategy_research 2y 整体组合年化对比, +# 相对改善有效;原设计"温区级组合年化"需按温区分段重跑变体,资源过重,整体同口径更务实) +ACCEPT_CAGR_PP = 3.0 # 组合年化 ≥ 原 + 3pp +ACCEPT_DD_PP = 2.0 # max_dd 不劣化超过 2pp + + +def log(msg): + line = f"[{datetime.now().isoformat(timespec='seconds')}] {msg}" + print(line, flush=True) + + +# ── 单例守卫(fcntl,Windows 不可用则跳过)── +try: + import fcntl + _LOCK_FD = open("/tmp/evolution_engine.lock", "w") + try: + fcntl.flock(_LOCK_FD, fcntl.LOCK_EX | fcntl.LOCK_NB) + except OSError: + log("已有 evolution_engine 实例在运行,退出") + sys.exit(0) +except ImportError: + pass + + +def get_conn(): + conn = sqlite3.connect(DB) + conn.row_factory = sqlite3.Row + return conn + + +# ── 1. 激活策略集合 ── +def load_active_strategies(): + """返回 [(version, market, regime)]:A股 active + 港股 markets.hk.active""" + try: + d = json.load(io.open(WEIGHTS_JSON, encoding="utf-8")) + except Exception as e: + log(f"读 strategy_weights.json 失败: {e}") + return [] + out = [] + for v in (d.get("active") or []): + info = (d.get("weights") or {}).get(v, {}) + out.append({"version": v, "market": "a", + "regime": info.get("best_regime") or info.get("regime") or d.get("state")}) + hk = (d.get("markets") or {}).get("hk") or {} + for v in (hk.get("active") or []): + out.append({"version": v, "market": "hk", "regime": hk.get("state")}) + return out + + +# ── 2. 退化信号检测 ── +def detect_degradation(conn, version, market): + """返回退化原因列表(空=健康)。S1 健康度连续低;S2 温区组合年化<0""" + reasons = [] + + # S1:健康度连续 5 天 < 40(排除 50 中性=无实盘) + rows = conn.execute( + "SELECT date, health_score FROM strategy_health WHERE strategy_version=? ORDER BY date DESC LIMIT ?", + (version, HEALTH_STREAK_DAYS)).fetchall() + if len(rows) >= HEALTH_STREAK_DAYS: + scores = [r["health_score"] for r in rows] + # 排除"无实盘=50中性"污染:只要连续5天都 < 40 且不是 50 占位 + if all(s is not None and s < HEALTH_LOW for s in scores) and any(s != 50 for s in scores): + reasons.append(f"S1 健康度连续{HEALTH_STREAK_DAYS}天<{HEALTH_LOW}({scores})") + + # S2:适应温区温区级组合年化 < 0(strategy_regime_perf.cagr_pct) + r = conn.execute( + "SELECT regime, cagr_pct, trades FROM strategy_regime_perf WHERE strategy=? AND market=? ORDER BY trades DESC LIMIT 1", + (version, market)).fetchone() + if r and r["cagr_pct"] is not None and r["cagr_pct"] < REGIME_CAGR_BAD: + reasons.append(f"S2 适应温区[{r['regime']}]组合年化{r['cagr_pct']}%<0({r['trades']}笔)") + + return reasons + + +# ── 3. 变体生成(数据驱动,从实际 config 出发)── +_NUM_KEYS = ("tp_pct", "sl_pct", "sl_atr", "max_hold_days", "min_score", "min_momentum", + "adx_min", "atr_pct_min", "atr_pct_max", "roc_min", "roc_max", + "macd_hist_min", "macd_hist_max", "dist_ma20_min", "vol_ratio_min", + "vol_ratio_max", "ma20_slope_max", "mkt_slope_max", "mkt_adx_min", + "sector_slope_max", "bias_max", "rsi_max", "ret_max", "mom20_max", + "amount_max", "rsi_delta_min", "mkt_rsi_max", "mkt_dd60_max", + "mcap_q_max", "pe_q_max", "news3_min", "sec_ret20_max", + "pe_q_max", "mcap_q_max", "sec_ret20_min", "bias60_max", + "vol_ratio_min", "rsi_delta_min", "bias60_min") +_SKIP_KEYS = ("mode", "family", "launch", "version", "name", "summary", "hypothesis", + "mkt_mode", "mkt_above_ma20", "hh_only", "hl_only", "sector_above_ma20") + + +def iter_numeric_fields(node, path=()): + """递归遍历 config,产出 (path_list, field_name, value) 数值字段""" + if isinstance(node, dict): + for k, v in node.items(): + if k in _SKIP_KEYS: + continue + if isinstance(v, (int, float)) and not isinstance(v, bool) and k in _NUM_KEYS: + yield list(path) + [k], k, v + elif isinstance(v, dict): + yield from iter_numeric_fields(v, list(path) + [k]) + + +def get_parent_cagr(conn, version, market, period_tag=BT_PERIOD_TAG): + """原策略基准:优先同周期 strategy_research(period_tag=2y),无则温区级组合年化(全量)""" + if market == "a": + r = conn.execute( + "SELECT results_json FROM strategy_research WHERE version=? AND period_tag=? " + "AND market='a' ORDER BY id DESC LIMIT 1", (version, period_tag)).fetchone() + if r: + res = json.loads(r["results_json"] or "{}") + s = res.get("summary", {}) + pf = s.get("portfolio_full", {}) + cagr = pf.get("cagr_pct") + dd = pf.get("portfolio_max_dd_pct") + if cagr is not None: + return cagr, dd + # 回退:温区级组合年化(strategy_regime_perf,全量)——仅当同周期数据缺失时 + r = conn.execute( + "SELECT cagr_pct, portfolio_max_dd_pct FROM strategy_regime_perf WHERE strategy=? AND market=? ORDER BY trades DESC LIMIT 1", + (version, market)).fetchone() + if r: + return r["cagr_pct"], r["portfolio_max_dd_pct"] + return None, None + + +def generate_hypothesis_variants(version, market, base_config, period_tag=BT_PERIOD_TAG): + """2026-08-16 数据归纳假设变体:从交易数据归纳可描述条件 → 生成加条件的策略版本 + 假设格式:{feature, direction(max/min), threshold} → 对应入场条件 + 返回 [{version, name, config, change_desc, evidence, hypothesis}] + """ + try: + from hypothesis_miner import induce_hypotheses + hs, _ = induce_hypotheses(version, market, period_tag=period_tag) + except Exception: + hs = [] + variants = [] + for h in hs[:MAX_VARIANTS_TEST]: + feat = h["feature"] + direction = h["direction"] + threshold = h["threshold"] + # 映射到策略 config 的字段(A股 entry.filters/mr,港股 entry 顶层) + cfg = copy.deepcopy(base_config) + if market == "hk": + entry = cfg.get("entry", {}) + else: + entry = cfg.get("entry", {}) + # 字段名映射:面板字段 → 策略字段(多数同名,A股 mr 下) + key = feat + target = entry + # A股 config 是 {entry:{filters,mr}} 结构,找可放的位置 + if market != "hk": + if "mr" in entry: + target = entry["mr"] + elif "filters" in entry: + target = entry["filters"] + if direction == "max": + target[key + "_max"] = threshold + else: + target[key + "_min"] = threshold + vname = f"evo_{version}_{key}_{direction}{threshold}" + variants.append({ + "version": vname, + "name": f"自进化-{version}-规避{key}{direction}{threshold}", + "config": cfg, + "change_desc": f"[数据归纳] {h['hypothesis']}", + "evidence": h.get("evidence", ""), + "hypothesis": h.get("hypothesis", ""), + "field": key, + "delta": 0, + }) + return variants + + +def generate_variants(version, market, config): + """生成变体参数建议:单变量 ±20%,最多 MAX_VARIANTS 个 + 返回 [{version, name, config, change_desc, field, delta}]""" + fields = list(iter_numeric_fields(config)) + if not fields: + return [] + variants = [] + for path, fname, val in fields: + if val <= 0: + continue + for factor, tag in [(1 - VAR_PCT, "减20%"), (1 + VAR_PCT, "加20%")]: + new_val = round(val * factor, 4) + if new_val <= 0: + continue + # 克隆 config 并修改目标字段 + new_cfg = copy.deepcopy(config) + node = new_cfg + for p in path[:-1]: + node = node[p] + node[path[-1]] = new_val + variants.append({ + "version": f"evo_{version}_{fname}_{tag.replace('20%','')}{round(new_val, 2)}", + "name": f"自进化-{version}-{fname}{tag}", + "config": new_cfg, + "change_desc": f"{fname}: {val} → {new_val}({tag})", + "field": fname, + "delta": round(new_val - val, 4), + }) + if len(variants) >= MAX_VARIANTS: + return variants + return variants + + +# ── 4. 回测验证 ── +def verify_variant_a(variant, parent_version): + """A股变体验证:注册进 lab 跑回测(save=False),返回 summary 关键指标""" + import strategy_lab as lab + name = variant["version"] + base = lab.get_strategy(parent_version) + cfg = copy.deepcopy(base) + cfg["version"] = name + cfg["name"] = variant["name"] + # 用变体 config 覆盖(变体 config 从原 config 克隆并改了一个字段) + merged = copy.deepcopy(base["config"]) + _deep_update(merged, variant["config"]) + cfg["config"] = merged + lab.STRATEGIES[name] = cfg + try: + r = lab.run_backtest(name, BT_START, BT_END, 913000, save=False, + universe="a", period_tag=BT_PERIOD_TAG) + s = r.get("summary", {}) + pf = s.get("portfolio_full", {}) + return { + "trades": s.get("total_trades"), + "win_rate": s.get("win_rate"), + "cagr": pf.get("cagr_pct"), + "total_return": pf.get("total_return_pct"), + "max_dd": pf.get("portfolio_max_dd_pct"), + } + finally: + lab.STRATEGIES.pop(name, None) + + +def _deep_update(dst, src): + for k, v in src.items(): + if isinstance(v, dict) and isinstance(dst.get(k), dict): + _deep_update(dst[k], v) + else: + dst[k] = v + + +def verify_variant_hk(variant, parent_version): + """港股变体验证:hk_backtest 生成交易 + portfolio_sim 8槽""" + import pandas as pd + sys.path.insert(0, "/home/hmo/MoFin/deploy/profile-scripts") + from hk_strategies import HK_STRATEGIES, get_hk_strategy + import hk_backtest + import strategy_lab as lab + + base = get_hk_strategy(parent_version) + if not base: + return None + new_cfg = copy.deepcopy(base) + new_cfg["version"] = variant["version"] + new_cfg["name"] = variant["name"] + _deep_update(new_cfg, variant["config"]) + panel = hk_backtest.load_panel() + # 2y 窗口过滤(与 A股验证周期一致,控制资源) + panel = panel[(panel["date"] >= BT_START) & (panel["date"] <= BT_END)].copy() + trades = hk_backtest.gen_trades_defensive(panel, new_cfg, strike=3, cooldown_days=15) + if not trades: + return {"trades": 0, "win_rate": None, "cagr": None, "total_return": None, "max_dd": None} + sim = lab.portfolio_sim(trades, 1000000, max_positions=8) + return { + "trades": len(trades), + "win_rate": round(100 * sum(1 for t in trades if t["profit_pct"] > 0) / len(trades), 1), + "cagr": sim.get("cagr_pct"), + "total_return": sim.get("total_return_pct"), + "max_dd": sim.get("portfolio_max_dd_pct"), + } + + +# ── 5. 记录 + 推送 ── +def record_and_notify(conn, parent_version, market, variant, result, parent_cagr, parent_dd): + """写 strategy_evolution + XMPP 推送""" + conn.execute(""" + INSERT INTO strategy_evolution (parent_version, child_version, change_description, backtest_result, promoted) + VALUES (?, ?, ?, ?, 0) + """, (parent_version, variant["version"], variant["change_desc"], + json.dumps(result, ensure_ascii=False))) + conn.commit() + + msg = (f"🧬 策略进化建议 [{parent_version}]\n" + f"改动: {variant['change_desc']}\n" + f"回测: 年化 {parent_cagr}% → {result.get('cagr')}%" + f" (Δ{round((result.get('cagr') or 0) - (parent_cagr or 0), 1)}pp)" + f" | 回撤 {parent_dd}% → {result.get('max_dd')}%\n" + f"胜率 {result.get('win_rate')}% / {result.get('trades')}笔\n" + f"【验证达标,待你决定是否上线】") + try: + sys.path.insert(0, "/home/hmo/MoFin/deploy/profile-scripts") + from alert_helper import notify, ACTION + notify("策略进化", msg, level=ACTION) + log(f"XMPP 推送: {parent_version} → {variant['version']}") + except Exception as e: + log(f"XMPP 推送失败: {e}") + return msg + + +# ── 主流程 ── +def run_evolution(): + conn = get_conn() + actives = load_active_strategies() + log(f"激活策略: {[a['version'] for a in actives]}") + if not actives: + log("无激活策略,退出") + conn.close() + return + + findings = [] # 退化发现 + passed = [] # 达标变体 + + for act in actives: + v, mkt = act["version"], act["market"] + reasons = detect_degradation(conn, v, mkt) + if not reasons: + continue + log(f"退化信号: {v} [{mkt}] → {'; '.join(reasons)}") + findings.append((v, mkt, reasons)) + + # 生成变体(A股从 lab 读 config;港股从 HK_STRATEGIES) + if mkt == "hk": + sys.path.insert(0, "/home/hmo/MoFin/deploy/profile-scripts") + from hk_strategies import get_hk_strategy + base = get_hk_strategy(v) + if not base: + log(f" {v} 无港股策略定义,跳过") + continue + # 2026-08-16 优先数据归纳假设,无则参数变体 + variants = generate_hypothesis_variants(v, mkt, base) + if not variants: + variants = generate_variants(v, mkt, base) + verify_fn = verify_variant_hk + else: + try: + import strategy_lab as lab + base = lab.get_strategy(v) + except ValueError: + log(f" {v} 不在标准回测体系(scanner 类策略),跳过变体研究") + continue + # 2026-08-16 优先数据归纳假设,无则参数变体 + variants = generate_hypothesis_variants(v, mkt, base["config"]) + if not variants: + variants = generate_variants(v, mkt, base["config"]) + verify_fn = verify_variant_a + if not variants: + log(f" {v} 无可用变体字段,跳过") + continue + + parent_cagr, parent_dd = get_parent_cagr(conn, v, mkt) + log(f" {v} 原温区年化 {parent_cagr}% / 回撤 {parent_dd}% | 生成 {len(variants)} 个变体,验证前 {MAX_VARIANTS_TEST} 个") + for var in variants[:MAX_VARIANTS_TEST]: + try: + res = verify_fn(var, v) + except Exception as e: + log(f" {var['version']} 回测失败: {str(e)[:100]}") + continue + if not res or res.get("cagr") is None: + log(f" {var['version']} 无结果(0笔或空),跳过") + continue + ok_cagr = parent_cagr is None or res["cagr"] >= (parent_cagr or 0) + ACCEPT_CAGR_PP + ok_dd = parent_dd is None or res["max_dd"] <= (parent_dd or 0) + ACCEPT_DD_PP + status = "✅达标" if (ok_cagr and ok_dd) else "❌不达标" + log(f" {var['version']}: 年化 {parent_cagr}→{res['cagr']}% 回撤 {parent_dd}→{res['max_dd']}% [{status}]") + if ok_cagr and ok_dd: + record_and_notify(conn, v, mkt, var, res, parent_cagr, parent_dd) + passed.append((v, var, res)) + + conn.close() + + # 汇总 + if not findings: + log("── 无退化信号,当周静默 ──") + else: + log(f"── 检测 {len(findings)} 个退化策略,{len(passed)} 个达标变体已推送 ──") + return findings, passed + + +if __name__ == "__main__": + try: + run_evolution() + except Exception as e: + log(f"evolution_engine 异常: {e}") + traceback.print_exc() + sys.exit(1) diff --git a/archive/evolution-cleanup-20260821/evolution/health_monitor.py b/archive/evolution-cleanup-20260821/evolution/health_monitor.py new file mode 100644 index 00000000..de5293e7 --- /dev/null +++ b/archive/evolution-cleanup-20260821/evolution/health_monitor.py @@ -0,0 +1,143 @@ +""" +evolution/health_monitor.py — 策略健康度监控 +对比实盘交易 vs 回测预期,计算健康分,偏差过大时报警 +""" +import sys, os, json, sqlite3 +from datetime import datetime, timedelta + +sys.path.insert(0, '/home/hmo/MoFin') +sys.path.insert(0, '/home/hmo/MoFin/deploy/profile-scripts') + +DB = os.environ.get('MOFIN_DB', '/home/hmo/MoFin/data/mofin.db') +CURRENT_STRATEGY = 'v_next4' + + +def get_backtest_baseline(conn, version): + """从 strategy_research 取回测基线""" + r = conn.execute(""" + SELECT results_json FROM strategy_research + WHERE version=? AND period_tag='5y' ORDER BY id DESC LIMIT 1 + """, (version,)).fetchone() + if not r: + return None + res = json.loads(r[0]) + s = res.get('summary', {}) + return { + 'win_rate': s.get('win_rate', 0), + 'avg_profit_pct': s.get('avg_profit_pct', 0), + 'total_trades': s.get('total_trades', 0), + } + + +def get_live_trades(conn, days=7): + """取近N天实盘交易(holding_strategies 全部,计算盈亏)""" + since = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d') + try: + rows = conn.execute(""" + SELECT code, name, price as current_price, avg_price as entry_price, + timing_signal as signal, updated_at, + CASE WHEN avg_price > 0 THEN round((price - avg_price) / avg_price * 100, 2) ELSE 0 END as profit_pct + FROM holding_strategies + WHERE updated_at >= ? AND avg_price > 0 + ORDER BY updated_at DESC + """, (since,)).fetchall() + return rows + except sqlite3.OperationalError as e: + print(f"查询失败: {e}", flush=True) + return [] + + +def calc_health_score(live_wr, live_ret, backtest_wr, backtest_ret): + """计算健康分 (0-100) + 健康分 = 100 - 偏差惩罚 + 偏差 = |实盘胜率-回测胜率| + |实盘收益-回测收益|/2 + """ + if backtest_wr == 0: + return 50 # 无基线,中性分 + + wr_dev = abs(live_wr - backtest_wr) + ret_dev = abs(live_ret - backtest_ret) / 2 + deviation = wr_dev + ret_dev + + # 偏差越大,健康分越低 + health = max(0, 100 - deviation * 2) + return round(health, 1) + + +def run_health_check(strategy_version=None): + """执行健康度检查""" + version = strategy_version or CURRENT_STRATEGY + conn = sqlite3.connect(DB) + conn.row_factory = sqlite3.Row + + # 回测基线 + baseline = get_backtest_baseline(conn, version) + if not baseline: + print(f"无 {version} 回测基线", flush=True) + conn.close() + return None + + # 实盘交易(近7天) + live = get_live_trades(conn, days=7) + today = datetime.now().strftime('%Y-%m-%d') + + if not live: + # 无实盘数据,记录中性健康分 + health = 50 + deviation = 0 + live_wr = live_ret = 0 + print(f"{version}: 近7天无实盘交易,健康分=50(中性)", flush=True) + else: + wins = sum(1 for t in live if (t.get('profit_pct') or 0) > 0) + total = len(live) + live_wr = round(100 * wins / total, 1) if total else 0 + live_ret = round(sum(t.get('profit_pct') or 0 for t in live) / total, 2) if total else 0 + + health = calc_health_score(live_wr, live_ret, baseline['win_rate'], baseline['avg_profit_pct']) + deviation = abs(live_wr - baseline['win_rate']) + + # 写入 strategy_health 表 + conn.execute(""" + INSERT OR REPLACE INTO strategy_health + (strategy_version, date, live_trades, live_wins, live_return_pct, + backtest_wr, backtest_avg_ret, deviation, health_score) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + """, (version, today, len(live), sum(1 for t in live if (t.get('profit_pct') or 0) > 0), + live_ret, baseline['win_rate'], baseline['avg_profit_pct'], deviation, health)) + conn.commit() + + # 报警判断(2026-08-12 修:无实盘交易时不告警——health=50 是中性"无数据",非"偏低") + alert = None + if live: + if health < 40: + alert = f"🔴 策略健康度严重下降: {health}分 (偏差{deviation}pp)" + elif health < 60: + alert = f"🟡 策略健康度偏低: {health}分 (偏差{deviation}pp)" + + result = { + 'version': version, + 'date': today, + 'live_trades': len(live), + 'live_wr': live_wr, + 'live_ret': live_ret, + 'backtest_wr': baseline['win_rate'], + 'backtest_ret': baseline['avg_profit_pct'], + 'deviation': deviation, + 'health_score': health, + 'alert': alert, + } + + print(f"{version} 健康度: {health}分 (实盘{live_wr}%/{live_ret}% vs 回测{baseline['win_rate']}%/{baseline['avg_profit_pct']}%)", flush=True) + if alert: + print(f" {alert}", flush=True) + + conn.close() + return result + + +if __name__ == '__main__': + import sys + sys.path.insert(0, '/home/hmo/MoFin/evolution') + from __init__ import init_evolution_tables + init_evolution_tables() + run_health_check() diff --git a/archive/evolution-cleanup-20260821/evolution/hypothesis_miner.py b/archive/evolution-cleanup-20260821/evolution/hypothesis_miner.py new file mode 100644 index 00000000..e6c9f13b --- /dev/null +++ b/archive/evolution-cleanup-20260821/evolution/hypothesis_miner.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +"""evolution/hypothesis_miner.py — 数据归纳假设引擎 v2 +从策略最新交易数据 + 面板特征,归纳可描述的优化假设(方向一核心) +数据源:strategy_research trades(code+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(" 未归纳出显著假设") diff --git a/archive/evolution-cleanup-20260821/evolution/lesson_extractor.py b/archive/evolution-cleanup-20260821/evolution/lesson_extractor.py new file mode 100644 index 00000000..2f97c7e3 --- /dev/null +++ b/archive/evolution-cleanup-20260821/evolution/lesson_extractor.py @@ -0,0 +1,150 @@ +# -*- 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() diff --git a/archive/evolution-cleanup-20260821/evolution/merge_b_group.py b/archive/evolution-cleanup-20260821/evolution/merge_b_group.py new file mode 100644 index 00000000..8724d22b --- /dev/null +++ b/archive/evolution-cleanup-20260821/evolution/merge_b_group.py @@ -0,0 +1,217 @@ +# -*- coding: utf-8 -*- +"""evolution/merge_b_group.py — AB融合机制(2026-08-16 方向二闭环) + +老莫:B组候选与A组对照后,融合/合并成为最终实施组(新的A组)。 + +流程: + 1. 读 B 组 verified 候选(data/b_group_candidates.json, status='verified') + 2. 老莫选择要融合的候选 → 注册为正式策略版本: + - A股:写入 strategy_research(results_json 用回测验证的 trades) + - 港股:注册进 hk_strategies.py(entry 条件) + 3. 加入候选池(strategy_weights 路由可识别) + 4. 手动可用性把关(老莫决定是否启用)——融合≠自动上线 + +安全:不自动 promote,不自动启用;融合只是把候选变成"可用的新策略版本"。 +""" +import json +import subprocess +import sys +import sqlite3 +from datetime import datetime + +DATA_DIR = "/home/hmo/MoFin/data" +CAND_JSON = f"{DATA_DIR}/b_group_candidates.json" +DB = "/home/hmo/MoFin/data/mofin.db" + + +def load_candidates(): + try: + d = json.load(open(CAND_JSON, encoding="utf-8")) + return d.get("candidates", []) + except Exception: + return [] + + +def get_verified(): + return [c for c in load_candidates() if c.get("status") == "verified"] + + +def strategy_name(cand): + """生成策略版本名:b{regime缩写}{序号}""" + rg_map = {"trend_up": "tu", "choppy": "ch", "trend_down": "td"} + rg = rg_map.get(cand.get("regime"), "x") + idx = cand.get("_idx", 1) + return f"b_{rg}{idx}" + + +def register_a_share(cand): + """A股候选注册:写入 strategy_research(B组候选,供研究Tab/回测) + 实际回测验证由进化引擎跑,这里先注册占位 + 候选条件记录 + """ + conn = sqlite3.connect(DB, timeout=10) + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + name = strategy_name(cand) + # 检查是否已注册 + exist = conn.execute("SELECT 1 FROM strategy_research WHERE version=? LIMIT 1", (name,)).fetchone() + if exist: + conn.close() + return {"status": "exists", "version": name} + conn.execute(""" + INSERT INTO strategy_research (version, name, summary, hypothesis, parent, config_json, + results_json, analysis_json, period, created_at, market, period_tag, deprecated) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?) + """, (name, f"B组-{cand.get('regime','')}", cand.get("hypothesis", ""), + "B组融合候选(由果及因挖掘)", "B组", json.dumps(cand.get("entry", {})), + json.dumps({"summary": {"total_trades": cand.get("trades_est"), + "win_rate": cand.get("sim_win_rate"), + "avg_profit_pct": cand.get("sim_avg_pnl")}}), + None, None, now, "a", "2y", None)) + conn.commit() + conn.close() + return {"status": "registered", "version": name} + + +def register_hk(cand): + """港股候选注册:追加到 hk_strategies.py""" + name = strategy_name(cand) + entry = cand.get("entry", {}) + # 追加到 hk_strategies.py(先读再写) + path = "/home/hmo/MoFin/deploy/profile-scripts/hk_strategies.py" + src = open(path, encoding="utf-8").read() + if f'"{name}"' in src: + return {"status": "exists", "version": name} + new_block = f''' + "{name}": {{ + "version": "{name}", + "name": "B组-{cand.get('regime','')}(由果及因融合)", + "regime": "{cand.get('regime','all')}", + "summary": "{cand.get('hypothesis','B组候选')[:80]}", + "entry": {json.dumps(entry, ensure_ascii=False)}, + "exit": {{"tp_pct": 0.10, "sl_pct": 0.05, "max_hold_days": 20}}, + }}, +}}''' + # 在 HK_STRATEGIES 的收尾 "}" 前插入(精确:找最后一个顶层 dict 的收尾) + # HK_STRATEGIES 结构:{ "k1": {...}, ..., "kn": {...}, } 然后空行 + get_hk_strategy + marker = "\n\n\ndef get_hk_strategy" + idx = src.rfind(marker) + if idx == -1: + return {"status": "error", "version": name, "error": "hk_strategies 结构异常"} + insert_at = src.rfind("}", 0, idx) + # 去掉 new_block 末尾多余的 }} + clean_block = new_block.rstrip() + if clean_block.endswith("}}"): + clean_block = clean_block[:-1] + src = src[:insert_at] + clean_block + src[insert_at:] + open(path, "w", encoding="utf-8").write(src) + return {"status": "registered", "version": name} + + +def merge(version=None): + """融合:把 verified 候选注册为策略版本。version 指定要融合的候选,None=全部""" + verified = get_verified() + if not verified: + return {"error": "无 verified B组候选(需先通过模拟验证门槛)", "verified": 0} + out = [] + for i, cand in enumerate(verified): + if version and cand.get("version_name") != version: + continue + cand["_idx"] = i + 1 + if cand.get("market") == "hk": + r = register_hk(cand) + else: + r = register_a_share(cand) + r["candidate"] = cand.get("hypothesis", "") + # 融合链路:多周期trades + 温区预计算 + 资格评估 + 可用性初始化 + if r.get("status") in ("registered", "exists") and r.get("version"): + try: + link = _post_merge_chain(r["version"], cand) + r["chain"] = link + except Exception as e: + r["chain"] = {"error": str(e)} + out.append(r) + return {"merged": out} + + +def _post_merge_chain(version, cand): + """融合后链路:按period_tag生成窗口trades → 温区预计算 → 资格评估 → 可用性 + 返回 {period_trades: {...}, regime_records: n, qualification: {...}, availability: {...}}""" + import subprocess, json as _json + out = {} + # 1) 生成各周期窗口trades 写入 strategy_research(每个 period_tag 记录独立 results_json) + # (B组候选的 trades 来自模拟验证,按 entry_date 过滤窗口) + try: + import sys as _sys + _sys.path.insert(0, "/home/hmo/MoFin") + _sys.path.insert(0, "/home/hmo/MoFin/evolution") + import sqlite3 as _sq + import pandas as _pd + from datetime import datetime as _dt, timedelta as _td + from b_group_miner import _simulate_verify + market = cand.get("market", "a") + regime = cand.get("regime", "trend_down") + entry = cand.get("entry", {}) + panel_path = "/tmp/panel_12d_hk.pkl" if market == "hk" else "/tmp/panel_12d.pkl" + panel = _pd.read_pickle(panel_path) + panel = panel.sort_values(["code", "date"]).reset_index(drop=True) + panel["fwd_ret60"] = panel.groupby("code")["close"].transform(lambda x: x.shift(-60) / x - 1) * 100 + cond = _pd.Series(True, index=panel.index) + for feat, val in entry.items(): + if "_min" in feat: + cond &= panel[feat.replace("_min", "")] >= val + elif "_max" in feat: + cond &= panel[feat.replace("_max", "")] < val + elif feat in panel.columns: + cond &= panel[feat] == val + tp = int(cand.get("sim_tp", 15)); sl = int(cand.get("sim_sl", 8)); mh = int(cand.get("sim_maxh", 35)) + r = _simulate_verify(market, regime, panel, cond, tp=tp, sl=sl, maxh=mh) + if not r: + out["period_trades"] = {"error": "模拟验证无结果"} + else: + all_trades = r["trades"] + latest_dt = _dt.strptime(max(t["entry_date"] for t in all_trades), "%Y-%m-%d") + conn = _sq.connect("/home/hmo/MoFin/data/mofin.db", timeout=30) + for pt, yrs in [("1y", 1), ("2y", 2), ("5y", 5), ("10y", 10)]: + cutoff = (latest_dt - _td(days=365 * yrs)).strftime("%Y-%m-%d") + wt = [t for t in all_trades if t["entry_date"] >= cutoff] + n = len(wt) + wins = [t for t in wt if t.get("profit_pct", 0) > 0] + wr = round(len(wins) / n * 100, 1) if n else 0 + avg = round(sum(t.get("profit_pct", 0) for t in wt) / n, 2) if n else 0 + results = {"summary": {"total_trades": n, "win_rate": wr, "avg_profit_pct": avg}, + "trades": wt[:5000], + "sim_params": {"tp": tp, "sl": sl, "maxh": mh}, + "window": {"cutoff": cutoff, "latest": max(t["entry_date"] for t in all_trades)}} + conn.execute("UPDATE strategy_research SET results_json=? WHERE version=? AND period_tag=?", + (_json.dumps(results, ensure_ascii=False), version, pt)) + out.setdefault("period_trades", {})[pt] = {"n": n, "win_rate": wr} + conn.commit(); conn.close() + except Exception as e: + out["period_trades"] = {"error": str(e)} + # 2) 温区预计算 + try: + mkt_flag = "--market=hk" if market == "hk" else "--market=a" + p = subprocess.run(["/home/hmo/MoFin/venv/bin/python", + "/home/hmo/MoFin/deploy/profile-scripts/regime_perf_by_period.py", + mkt_flag, "--periods=1y 2y 5y 10y"], + capture_output=True, text=True, timeout=900) + out["regime_run"] = {"rc": p.returncode, "tail": (p.stdout or "").strip().splitlines()[-1:]} + except Exception as e: + out["regime_run"] = {"error": str(e)} + # 3) 资格评估 + 可用性 + try: + _sys.path.insert(0, "/home/hmo/MoFin/deploy/profile-scripts") + import strategy_qualify as sq + out["qualification"] = sq.evaluate_all_regimes(version, market=market) + sq.auto_init_availability([version]) + av = sq.load_availability().get(version) + out["availability"] = av + except Exception as e: + out["qualification"] = {"error": str(e)} + return out + + +if __name__ == "__main__": + import sys + v = sys.argv[1] if len(sys.argv) > 1 else None + res = merge(v) + print(json.dumps(res, ensure_ascii=False, indent=1)) diff --git a/archive/evolution-cleanup-20260821/evolution/precompute_evolution.py b/archive/evolution-cleanup-20260821/evolution/precompute_evolution.py new file mode 100644 index 00000000..9ba9cc83 --- /dev/null +++ b/archive/evolution-cleanup-20260821/evolution/precompute_evolution.py @@ -0,0 +1,70 @@ +# -*- coding: utf-8 -*- +"""evolution/precompute_evolution.py — 进化机制预计算(2026-08-16) +定期(每周/每日)预计算进化机制数据,供研究Tab展示(API 只读快照,不实时重算): +1. 假设归纳(方向一):每个激活策略的归纳优化假设 +2. B组候选(方向二):由果及因挖掘的候选 +3. 策略资格概览 +输出:data/evolution_center.json +""" +import sys, os, json +from datetime import datetime + +sys.path.insert(0, "/home/hmo/MoFin") +sys.path.insert(0, "/home/hmo/MoFin/evolution") +sys.path.insert(0, "/home/hmo/MoFin/deploy/profile-scripts") + +OUT = "/home/hmo/MoFin/data/evolution_center.json" + + +def active_versions(): + try: + d = json.load(open("/home/hmo/MoFin/data/strategy_weights.json", encoding="utf-8")) + vs = list(d.get("active") or []) + vs += list(((d.get("markets") or {}).get("hk") or {}).get("active") or []) + return list(dict.fromkeys(vs)) + except Exception: + return [] + + +def main(): + print("=== 进化机制预计算开始 ===", flush=True) + from hypothesis_miner import induce_hypotheses + from strategy_qualify import evaluate_all_regimes, get_benchmarks + + out = {"generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "hypotheses": [], "b_group": [], "qual_overview": []} + + # 1. 假设归纳(方向一) + for v in active_versions(): + mkt = "hk" if v.startswith("hk") else "a" + try: + hs, _ = induce_hypotheses(v, mkt, period_tag="2y") + for h in hs[:3]: + out["hypotheses"].append({"strategy": v, "market": mkt, **h}) + print(f" 假设 [{v}]: {h['hypothesis'][:60]}", flush=True) + except Exception as e: + print(f" 假设 [{v}] 失败: {e}", flush=True) + + # 2. B组候选(方向二) + try: + p = json.load(open("/home/hmo/MoFin/data/b_group_candidates.json", encoding="utf-8")) + out["b_group"] = p.get("candidates", []) + print(f" B组候选: {len(out['b_group'])}", flush=True) + except Exception as e: + print(f" B组读取失败: {e}", flush=True) + + # 3. 资格概览 + for v in active_versions(): + mkt = "hk" if v.startswith("hk") else "a" + try: + q = evaluate_all_regimes(v, mkt, bench=get_benchmarks(mkt)) + out["qual_overview"].append({"strategy": v, "market": mkt, "qualification": q}) + except Exception: + pass + + with open(OUT, "w", encoding="utf-8") as f: + json.dump(out, f, ensure_ascii=False, indent=1) + print(f"写入 {OUT}: hypotheses={len(out['hypotheses'])} b_group={len(out['b_group'])} qual={len(out['qual_overview'])}") + + +if __name__ == "__main__": + main() diff --git a/archive/evolution-cleanup-20260821/meta_growth.py b/archive/evolution-cleanup-20260821/meta_growth.py new file mode 100644 index 00000000..173f4349 --- /dev/null +++ b/archive/evolution-cleanup-20260821/meta_growth.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +""" +meta_growth.py — 自成长机制的元层 + +功能:读取近期 git log,识别修复模式,注入新扫描规则到 hardcode_scanner 的扩展点。 +让自成长机制本身也会成长——能自动发现新的问题类型并添加对应的扫描规则。 + +调度:交易日 12:45 和 00:45(no_agent 模式) +- 12:45: 上午盘发现的问题→下午17:25审计就能扫到 +- 00:45: 全天修复汇总→次日审计带新规则 + +输出:/home/hmo/web-dashboard/data/growth_registry.json +""" + +import subprocess +import re +import json +import os +import sys +import datetime + +SCANNER_PATH = "/home/hmo/MoFin/deploy/profile-scripts/hardcode_scanner.py" +PROFILE_SCANNER = "/home/hmo/.hermes/profiles/position-analyst/scripts/hardcode_scanner.py" +REGISTRY_PATH = "/home/hmo/web-dashboard/data/growth_registry.json" +EXTENSION_MARKER = "# 扩展点 — meta_growth 在此追加新规则" + +# 已知问题类别 → 扫描规则模板 +# meta_growth 分析 git log 后,把新模式匹配到这里生成规则元组 +PATTERN_TEMPLATES = [ + { + "name": "cash_hardcode", + "desc": "现金/金额硬编码", + "regex": r"return\s+\d{4,}\b", + "reason": "可能的硬编码现金/金额", + "git_keywords": ["cash", "现金", "硬编码", "金额", "fallback.*\\d+"], + }, + { + "name": "exchange_rate", + "desc": "汇率硬编码", + "regex": r"0\.8[5-9]\d{1,3}", + "reason": "可能的硬编码汇率值", + "git_keywords": ["汇率", "rate", "HKD", "CNY", "0.8[5-9]"], + }, + { + "name": "lot_size_hardcode", + "desc": "港股每手股数硬编码", + "regex": r"1手\s*[:=]\s*\d{3,}", + "reason": "可能的每手股数硬编码", + "git_keywords": ["lot_size", "每手", "手数", "lot", "board lot", "f\\[60\\]"], + }, + { + "name": "percent_threshold", + "desc": "百分比阈值硬编码", + "regex": r"[><=]\s*0\.[0-9]+", + "reason": "可能的百分比阈值硬编码", + "git_keywords": ["threshold", "阈值", "止损", "stop_loss", "止盈", "百分比"], + }, + { + "name": "position_limit", + "desc": "仓位金额硬编码", + "regex": r"仓位\s*[:=]\s*\d{3,}", + "reason": "可能的仓位金额硬编码", + "git_keywords": ["仓位", "position", "持仓金额"], + }, + { + "name": "hardcoded_path", + "desc": "路径硬编码", + "regex": r"['\"](?!http|~|\./|\.\./)/home/[^'\"]+['\"]", + "reason": "可能的文件路径硬编码(应使用环境变量或配置)", + "git_keywords": ["路径", "path", "hardcoded path"], + }, +] + + +def get_recent_git_log(hours=8): + """获取最近 N 小时的 git log""" + try: + result = subprocess.run( + ["git", "log", f"--since={hours} hours ago", "--oneline"], + capture_output=True, text=True, cwd="/home/hmo/MoFin", timeout=10 + ) + return result.stdout + except Exception as e: + print(f"[meta_growth] git log 失败: {e}", file=sys.stderr) + return "" + + +def analyze_log(log_text): + """分析 git log,识别修复模式""" + found_patterns = [] + lines = log_text.strip().split("\n") + + for tmpl in PATTERN_TEMPLATES: + hit_count = 0 + for line in lines: + for kw in tmpl["git_keywords"]: + if re.search(kw, line, re.IGNORECASE): + hit_count += 1 + break + if hit_count > 0: + found_patterns.append({ + "name": tmpl["name"], + "desc": tmpl["desc"], + "regex": tmpl["regex"], + "reason": tmpl["reason"], + "hits": hit_count, + }) + + return found_patterns + + +def load_registry(): + """加载问题类别注册表""" + try: + if os.path.exists(REGISTRY_PATH): + with open(REGISTRY_PATH) as f: + return json.load(f) + except Exception: + pass + return { + "known_categories": [], + "injected_rules": [], + "last_run": None, + "last_findings": [], + } + + +def save_registry(registry): + """保存注册表""" + os.makedirs(os.path.dirname(REGISTRY_PATH), exist_ok=True) + with open(REGISTRY_PATH, "w") as f: + json.dump(registry, f, indent=2, ensure_ascii=False) + + +def rule_already_exists(registry, regex): + """检查规则是否已注入""" + for r in registry.get("injected_rules", []): + if r.get("regex") == regex: + return True + return False + + +def inject_rule(scanner_path, regex, reason, marker=EXTENSION_MARKER): + """在 hardcode_scanner.py 的扩展点后插入新规则""" + if not os.path.exists(scanner_path): + return False + + try: + with open(scanner_path, "r") as f: + content = f.read() + except Exception: + return False + + if regex in content: + return False # 已存在 + + new_rule = f" (r'{regex}', '{reason}'),\n {marker}" + if marker not in content: + return False # 没有扩展点 + + content = content.replace(marker, new_rule) + + try: + with open(scanner_path, "w") as f: + f.write(content) + return True + except Exception: + return False + + +def self_check(): + """自检:检查自成长系统本身的健康度""" + issues = [] + if not os.path.exists(SCANNER_PATH): + issues.append("hardcode_scanner.py 不存在") + if not os.path.exists(REGISTRY_PATH): + issues.append("growth_registry.json 不存在(首次运行正常)") + return issues + + +def main(): + now = datetime.datetime.now().isoformat() + period = "afternoon" if datetime.datetime.now().hour < 15 else "overnight" + + # 自检 + issues = self_check() + if issues: + for issue in issues: + print(f"[meta_growth] ⚠ {issue}", file=sys.stderr) + + # 读取 git log + hours = 8 # 过去8小时(覆盖一整个交易时段) + log = get_recent_git_log(hours=hours) + if not log: + print(f"[meta_growth] 无近期提交,跳过") + return + + print(f"[meta_growth] 分析 {period} 时段日志 ({len(log.strip().split(chr(10)))} 条提交)") + + # 分析修复模式 + patterns = analyze_log(log) + + # 加载注册表 + registry = load_registry() + registry["last_run"] = now + + if not patterns: + print(f"[meta_growth] 未发现新修复模式") + registry["last_findings"] = [] + save_registry(registry) + return + + # 去重注入 + injected_count = 0 + for p in patterns: + if rule_already_exists(registry, p["regex"]): + print(f"[meta_growth] 规则已存在: {p['name']} ({p['regex']})") + continue + + # 注入到 MoFin and profile 两个副本 + ok1 = inject_rule(SCANNER_PATH, p["regex"], p["reason"]) + ok2 = inject_rule(PROFILE_SCANNER, p["regex"], p["reason"]) + + if ok1 or ok2: + registry["injected_rules"].append({ + "name": p["name"], + "desc": p["desc"], + "regex": p["regex"], + "reason": p["reason"], + "injected_at": now, + "period": period, + "hits_in_log": p["hits"], + }) + injected_count += 1 + print(f"[meta_growth] 注入新规则: {p['name']} ({p['desc']})") + + # 记录到已知类别 + if p["name"] not in registry["known_categories"]: + registry["known_categories"].append(p["name"]) + + registry["last_findings"] = patterns + save_registry(registry) + + print(f"[meta_growth] 本次注入 {injected_count} 条新规则") + if injected_count > 0: + print(f"[meta_growth] 下次 hardcode_scanner 运行时将自动使用新规则") + + +if __name__ == "__main__": + main() diff --git a/archive/evolution-cleanup-20260821/meta_watchdog.py b/archive/evolution-cleanup-20260821/meta_watchdog.py new file mode 100644 index 00000000..9aee6ae9 --- /dev/null +++ b/archive/evolution-cleanup-20260821/meta_watchdog.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +"""meta_watchdog.py — L4 自检系统的自检(看门狗的看门狗) + +检查 L1-L3 各自检组件本身是否在正常运转: +- L1 functional_health_check: functional_health.json 是否 <20min(交易时段) +- L2 system_hygiene_audit: hygiene_report.json 是否 <26h(每日) +- L3 self_repair: repair_state.json 存在性 + cron 是否注册 +- mofin_health 采集: mofin_health.json 是否 <20min(交易时段) +- XMPP 桥: :5805 是否可发(self_repair 的报备通道) + +(2026-08-13 删除 L0 agents_health_check 检查项:MoFin 无该组件,且 L1 functional_health_check 已覆盖健康检查功能,检查项是死代码) + +任何一层死了 → 推 XMPP 点名(这是最后的兜底,必须直达用户)。 +频率:每小时(cron)。输出 gateway/logs/meta_watchdog.json。 +""" +import os, sys, json, subprocess +from datetime import datetime + +OUT = '/home/hmo/MoFin/gateway/logs/meta_watchdog.json' + +LAYERS = [ + {"layer": "L1 functional_health", "file": "/home/hmo/MoFin/gateway/logs/functional_health.json", + "max_age_min": 25, "when": "trading", + "repair": "L1 cron 停摆,检查 hermes cron 引擎"}, + {"layer": "L2 hygiene_audit", "file": "/home/hmo/MoFin/gateway/logs/hygiene_report.json", + "max_age_min": 26 * 60, "when": "always", + "repair": "L2 每日审计未跑,检查 hermes cron"}, + {"layer": "L1.5 mofin_health采集", "file": "/home/hmo/web-dashboard/static/mofin_health.json", + "max_age_min": 25, "when": "trading", + "repair": "mofin_health.py 采集停摆"}, + {"layer": "L3 self_repair", "file": "/home/hmo/MoFin/gateway/logs/repair_log.jsonl", + "max_age_min": None, "when": "meta", + "repair": "self_repair cron 未注册"}, +] + + +def is_trading(now): + return now.weekday() < 5 and 9 <= now.hour <= 16 + + +def main(): + now = datetime.now() + trading = is_trading(now) + results = [] + + for L in LAYERS: + if L["when"] == "trading" and not trading: + results.append({"layer": L["layer"], "status": "skip", "reason": "非交易时段"}) + continue + if L["when"] == "meta": + # 检查 self_repair 是否注册在 cron + try: + d = json.load(open('/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json')) + jobs = d if isinstance(d, list) else d.get('jobs', []) + registered = any(j.get('script') == 'self_repair.py' and j.get('enabled', True) for j in jobs) + results.append({"layer": L["layer"], + "status": "ok" if registered else "fail", + "reason": "已注册" if registered else "未在 cron 注册"}) + except Exception as e: + results.append({"layer": L["layer"], "status": "fail", "reason": str(e)[:60]}) + continue + + f = L["file"] + if not os.path.exists(f): + results.append({"layer": L["layer"], "status": "fail", + "reason": f"输出物不存在", "repair": L["repair"]}) + continue + age_min = (now.timestamp() - os.path.getmtime(f)) / 60 + if L["max_age_min"] and age_min > L["max_age_min"]: + results.append({"layer": L["layer"], "status": "fail", + "reason": f"输出物 {age_min/60:.1f}h 未更新(阈值 {L['max_age_min']}min)", + "repair": L["repair"]}) + else: + results.append({"layer": L["layer"], "status": "ok", + "reason": f"{age_min:.0f}min 前"}) + + # XMPP 桥(报备通道):只收 POST,GET 会 501,但任何 HTTP 响应都说明进程活着 + try: + import urllib.request + urllib.request.urlopen('http://127.0.0.1:5805/', timeout=3) + results.append({"layer": "XMPP桥 :5805", "status": "ok", "reason": "可达"}) + except urllib.error.HTTPError as e: + results.append({"layer": "XMPP桥 :5805", "status": "ok", "reason": f"可达(HTTP {e.code})"}) + except Exception: + results.append({"layer": "XMPP桥 :5805", "status": "fail", + "reason": "不可达", "repair": "重启 xmpp-zhiwei"}) + + fails = [r for r in results if r["status"] == "fail"] + report = { + "generated_at": now.strftime("%Y-%m-%d %H:%M:%S"), + "status": "fail" if fails else "ok", + "layers": results, + } + os.makedirs(os.path.dirname(OUT), exist_ok=True) + with open(OUT, 'w', encoding='utf-8') as f: + json.dump(report, f, ensure_ascii=False, indent=2) + + print(f"meta_watchdog: {report['status']}") + for r in results: + icon = {"ok": "✅", "fail": "❌", "skip": "⏭"}[r["status"]] + print(f" {icon} {r['layer']}: {r['reason']}") + + if fails: + try: + import urllib.request + lines = [f"🚨 自检系统自检(L4兜底)发现 {len(fails)} 层异常:"] + for r in fails: + lines.append(f"❌ {r['layer']}: {r['reason']}") + if r.get('repair'): + lines.append(f" → 处置建议: {r['repair']}") + payload = json.dumps({'to': 'hmo@yoin.fun', 'body': '\n'.join(lines), 'type': 'chat'}).encode() + req = urllib.request.Request('http://127.0.0.1:5805/', data=payload, + headers={'Content-Type': 'application/json'}) + urllib.request.urlopen(req, timeout=5) + print(' 📨 已推 XMPP(兜底直达)') + except Exception as e: + print(f' XMPP 失败: {e}') + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/docs/evolution-archive-readme.md b/docs/evolution-archive-readme.md new file mode 100644 index 00000000..3881f349 --- /dev/null +++ b/docs/evolution-archive-readme.md @@ -0,0 +1,52 @@ +# 策略进化模块归档说明(2026-08-21) + +## 归档内容 + +### evolution/ 目录 +| 文件 | 功能 | +|---|---| +| evolution_engine.py | 策略进化引擎:自动调整策略参数(止损/止盈/买入区)| +| evolution_api.py | 进化API:暴露进化结果给前端 | +| hypothesis_miner.py | 假设挖掘:从历史数据中发现潜在的策略改进假设 | +| lesson_extractor.py | 教训提取:从策略失败中提取可复用的教训 | +| b_group_miner.py | B组候选挖掘:发现新的候选股票 | +| merge_b_group.py | B组合并:将新发现的候选合并到策略池 | +| precompute_evolution.py | 预计算:提前计算进化所需的数据 | +| health_monitor.py | 健康监控:监控进化系统的运行状态 | + +### 独立脚本 +| 文件 | 功能 | +|---|---| +| meta_growth.py | 元自成长:自动调整策略的元参数(如仓位系数、风险权重)| +| meta_watchdog.py | 元监控:监控元自成长系统的运行状态 | +| ab_research_daily.py | AB研究:每日对比不同策略版本的表现 | + +## 归档原因 + +1. **策略进化模块**(evolution/)的设计目标是"自动调整策略参数",但它与重评流程(per_stock_reassess)的功能高度重叠。重评本身就是"根据最新数据调整策略参数",进化引擎做的是类似的事。 + +2. **元自成长**(meta_growth.py)试图自动调整策略的元参数(仓位系数等),但这些参数应该由用户(老莫)根据经验判断,不应该由系统自动调整。 + +3. **AB研究**(ab_research_daily.py)对比不同策略版本的表现,但现在有了 `strategy_effectiveness` 表(策略到期评估),功能已被替代。 + +4. **策略自我进化**的目标是"让系统自动改进策略",但根据老莫的方法论,策略改进应该是**人驱动的**(基于评估结果 + 经验判断),不是系统自动的。 + +## 替代方案 + +| 旧模块 | 新方案 | +|---|---| +| evolution_engine(自动调参)| 重评流程(per_stock_reassess)+ 用户决策 | +| meta_growth(元参数自动调整)| 用户根据经验手动调整 | +| ab_research(版本对比)| strategy_effectiveness(到期评估)| +| hypothesis_miner(假设挖掘)| 策略自我进化(待实现,人驱动)| +| lesson_extractor(教训提取)| strategy_effectiveness 的 improvement_suggestion | + +## 后续方向 + +策略自我进化应该是**人驱动的闭环**: +1. strategy_effectiveness 自动评估策略表现 +2. 评估结果显示在 MoFin 研究页面 +3. **用户(老莫)根据评估结果决定是否调整策略** +4. 用户调整后,系统记录新的策略版本 + +而不是系统自动调参——那会导致"黑箱"问题。 diff --git a/server.py b/server.py index f0b75d75..a645d7cb 100644 --- a/server.py +++ b/server.py @@ -2409,4 +2409,102 @@ def api_docs_read(): if __name__ == "__main__": port = int(os.environ.get("PORT", 8899)) print(f"🚀 MoFin Dashboard → http://0.0.0.0:{port}") - app.run(host="0.0.0.0", port=port, debug=False) \ No newline at end of file + app.run(host="0.0.0.0", port=port, debug=False) + +# ── 策略评估 API ──────────────────────────────────────── +@app.route("/api/research/effectiveness") +def api_research_effectiveness(): + """策略到期评估结果查询 + + GET /api/research/effectiveness → 全部评估记录 + GET /api/research/effectiveness?code=600262 → 指定股票 + GET /api/research/effectiveness?source=v_next → 指定策略来源 + """ + import sqlite3 + code = request.args.get("code", "") + source = request.args.get("source", "") + + db = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=30) + db.row_factory = sqlite3.Row + + query = "SELECT * FROM strategy_effectiveness WHERE 1=1" + params = [] + if code: + query += " AND code=?" + params.append(code) + if source: + query += " AND strategy_source=?" + params.append(source) + query += " ORDER BY created_at DESC LIMIT 100" + + rows = db.execute(query, params).fetchall() + db.close() + + return jsonify([dict(r) for r in rows]) + + +@app.route("/api/research/effectiveness/summary") +def api_research_effectiveness_summary(): + """策略评估汇总:按策略来源分组统计""" + import sqlite3 + db = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=30) + db.row_factory = sqlite3.Row + + summary = db.execute(""" + SELECT strategy_source, + COUNT(*) as total, + SUM(CASE WHEN buy_zone_accuracy='effective' THEN 1 ELSE 0 END) as buy_effective, + SUM(CASE WHEN stop_loss_accuracy='effective' THEN 1 ELSE 0 END) as sl_effective, + SUM(CASE WHEN take_profit_accuracy='effective' THEN 1 ELSE 0 END) as tp_effective, + AVG(CASE WHEN time_accuracy='on_time' THEN 1.0 WHEN time_accuracy='slightly_late' THEN 0.7 ELSE 0.3 END) as time_score + FROM strategy_effectiveness + GROUP BY strategy_source + ORDER BY total DESC + """).fetchall() + db.close() + + return jsonify([dict(r) for r in summary]) + + +@app.route("/api/research/recommendation_log") +def api_research_recommendation_log(): + """推荐历史查询""" + import sqlite3 + code = request.args.get("code", "") + limit = int(request.args.get("limit", "50")) + + db = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=30) + db.row_factory = sqlite3.Row + + query = "SELECT * FROM recommendation_log WHERE 1=1" + params = [] + if code: + query += " AND code=?" + params.append(code) + query += f" ORDER BY recommend_time DESC LIMIT {limit}" + + rows = db.execute(query, params).fetchall() + db.close() + return jsonify([dict(r) for r in rows]) + + +@app.route("/api/research/execution_log") +def api_research_execution_log(): + """执行历史查询""" + import sqlite3 + code = request.args.get("code", "") + limit = int(request.args.get("limit", "50")) + + db = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=30) + db.row_factory = sqlite3.Row + + query = "SELECT * FROM execution_log WHERE 1=1" + params = [] + if code: + query += " AND code=?" + params.append(code) + query += f" ORDER BY execute_time DESC LIMIT {limit}" + + rows = db.execute(query, params).fetchall() + db.close() + return jsonify([dict(r) for r in rows]) diff --git a/static/effectiveness.html b/static/effectiveness.html new file mode 100644 index 00000000..32595335 --- /dev/null +++ b/static/effectiveness.html @@ -0,0 +1,192 @@ + + + + + +策略评估 - MoFin + + + +

📊 策略评估

+ +
+
概览
+
详细评估
+
推荐记录
+
执行记录
+
+ +
+ + +
+ +
加载中...
+
+ + + +