From 67ed1ec9bdad08b048b078894c8845b15c17e0fb Mon Sep 17 00:00:00 2001 From: xxm Date: Tue, 25 Aug 2026 13:26:41 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20evolution=E7=9B=AE=E5=BD=95=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E5=85=A5=E5=BA=93(=E6=98=A8=E6=97=A5=E5=BD=92?= =?UTF-8?q?=E6=A1=A3=E6=94=B6=E5=B0=BE)+analyst=E7=9F=A5=E8=AF=86=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/analyst-knowledge-log.md | 106 ++++++++ evolution/__init__.py | 70 ----- evolution/b_group_miner.py | 200 -------------- evolution/evolution_api.py | 256 ----------------- evolution/evolution_engine.py | 438 ------------------------------ evolution/health_monitor.py | 143 ---------- evolution/hypothesis_miner.py | 142 ---------- evolution/lesson_extractor.py | 150 ---------- evolution/merge_b_group.py | 217 --------------- evolution/precompute_evolution.py | 70 ----- 10 files changed, 106 insertions(+), 1686 deletions(-) delete mode 100644 evolution/__init__.py delete mode 100644 evolution/b_group_miner.py delete mode 100644 evolution/evolution_api.py delete mode 100644 evolution/evolution_engine.py delete mode 100644 evolution/health_monitor.py delete mode 100644 evolution/hypothesis_miner.py delete mode 100644 evolution/lesson_extractor.py delete mode 100644 evolution/merge_b_group.py delete mode 100644 evolution/precompute_evolution.py diff --git a/docs/analyst-knowledge-log.md b/docs/analyst-knowledge-log.md index c81a0762..c106e61f 100644 --- a/docs/analyst-knowledge-log.md +++ b/docs/analyst-knowledge-log.md @@ -79,3 +79,109 @@ system_audit 报 MEDIUM:6个陈旧.pyc(accumulation_scanner/batch_reassess/s ### [2026-08-18 11:52] 盘中自检宏观HIGH误报·美债收益率主题同日第5次重写(TODO#296) **闭环记录:** 11:31 执行器推送宏观HIGH「美债收益率飙升!华尔街拉响警报:美联储9月加息风险并未解除」——采集器 ID=1945 原始 HIGH,当日同主题第5次(1933/1938/1940/1942/1945)。判 MEDIUM(延续 ID=1935/1939 既定判定:30Y美债19年新高真实渠道 + 城堡证券警告属预期非政策兑现)。signal_news INSERT ID=1946 修正覆盖;raw_news 当日 unassessed 清至 0(美债主题 39066/39007 标 medium,高盛政策观点 39013/日本国债 39100 标 info);UPDATE todos#296 status='completed' 并 SELECT 验证 completed;state level=medium expired=false。根因同前(采集器利率关键词粗筛+LLM cron 流式失败),不重复展开。 + +### [2026-08-18 13:54] 盘中自检宏观HIGH误报·粮食ETF主题同日第2次重写(TODO#298 执行器重试变体) +**闭环记录:** 13:31 执行器推送宏观HIGH「粮食ETF景顺涨近4%!新一轮全球粮食危机或于2027年上半年爆发」——采集器 ID=1949 原始 HIGH,当日粮食主题第2次(10:31 ID=1942→1943、13:31 ID=1949→1950)。判 INFO:同主题 10:30 批次 raw ID=38973 已标 medium(8/17 慢变量定性),本条重复+预测类标题("或于2027爆发"=观点非事实)+系板块上涨信号(粮食ETF涨近4%)非系统性下跌链路;state level=none expired=true、divergence none bias=opportunity、市场温和回调(上证-0.38%/创业板-1.76%)无系统性崩盘。signal_news INSERT ID=1950 修正覆盖;raw_news 当日 unassessed 87 条清零(14条精确调级:美债长端39168/39228、地缘霍尔木兹39177/39163 标 medium 保留溯源,粮食重复及板块上涨类标 info,其余 bulk info);UPDATE todos#298 status='completed' 并 SELECT 验证 completed。 +**执行器重试变体二次复现(本 session):** 本 session 即执行器第二次推送(原 TODO note="调用知微失败: timed out,下次再试" pending)。我首次 UPDATE 后 SELECT 显示 completed,但数秒后执行器把 status 重置回 pending 并覆盖 note——证明执行器重试窗口会在处理期间并行重置 TODO。正确处理:UPDATE 后必须立即 SELECT 确认,若又变 pending 需再次 UPDATE,最终以稳定 completed 为准。与 8/13#269/8/14#273/8/18#294 同型,本次为第4次复现。 + +## [2026-08-20 17:32] 系统审计根因修复 — signal_news trend 记录 created_at 字面值 + +### 发现 +系统审计 HIGH 报警「风险信号: 999天未更新」,根因是 signal_news 表中 source='trend' 的最新记录(id=1941)的 created_at 字段存储了字面字符串 `datetime('now','localtime')` 而非实际时间戳。SQLite 在写入时未调用 datetime() 函数,导致后续比较时无法解析为有效日期,计算出"999天"。 + +### 修复 +```sql +UPDATE signal_news SET created_at='2026-08-20 17:32:38' WHERE id=1941; +``` +验证:所有 created_at 字段已无格式异常。 + +### 根因 +写入 trend 记录的代码(可能是 trend_analyzer.py 或 signal_collector.py)使用了 `datetime('now','localtime')` 作为 Python 字符串而非 SQL 函数。当通过 Python sqlite3 的 `INSERT ... VALUES (?)` 参数化写入时,Python 会将该字符串作为纯文本存储,不会触发 SQLite 的 datetime() 计算。 + +### 预防 +- 审查 trend 源的写入逻辑,确认所有 created_at 使用 Python `datetime.now().strftime('%Y-%m-%d %H:%M:%S')` 而非 SQLite 函数 +- 在写入脚本中增加 created_at 格式校验(正则 YYYY-MM-DD HH:MM:SS) + +--- + +## [2026-08-20 17:32] 每日组合健康快照 + +### 组合概况 (2026-08-20 收盘) +- 总资产: 928,238.23 CNY +- 可用现金: 149,518.90 CNY (16.1%) +- 总仓位: 83.89% (15只活跃持仓) +- 活跃策略: 112条 + +### 持仓盈亏分布 +| 类别 | 数量 | 标的 | +|------|------|------| +| 盈利 | 3只 | 华茂+9.19%, 爱博+4.46%, 腾讯+0.96% | +| 小亏<10% | 4只 | 影石-8.78%, 宁德-3.84%, 神华-4.03%, 广信-0.8% | +| 中亏10-30% | 4只 | 紫金-14.14%, 比亚-12.97%, 华恒-19.33%, 海博-25.09% | +| 深套>30% | 4只 | 科电-36.34%, 万科-48.19%, 丘钛-50.68%, 黄金ETF-24.29% | + +### ⚠️ 风险关注(RR<1.0 的持仓) +| 标的 | RR | 止损 | 现价 | 类型 | +|------|-----|------|------|------| +| 300035 中科电气 | 0.5 | 12.78 | 14.19 | 深套 | +| 02202 万科企业 | 0.6 | 2.31 | 2.44 HKD | 深套 | +| 688639 华恒生物 | 0.8 | 15.75 | 17.35 | 中短线 | +| 01088 中国神华 | 0.9 | 42.5 | 44.44 HKD | 中短线 | + +### ⚠️ 距止损不足5% +| 标的 | 现价 | 止损 | 距离 | +|------|------|------|------| +| 300750 宁德时代 | 385.0 | 377.0 | 2.1% | +| 603599 广信股份 | 10.14 | 9.9 | 2.4% | +| 000850 华茂股份 | 4.19 | 4.04 | 3.6% | +| 01088 中国神华 | 44.44 | 42.5 | 4.4% | + +### 在买入区内的持仓 +| 标的 | 现价 | 买入区 | RR | +|------|------|--------|-----| +| 000850 华茂股份 | 4.19 | 4.04-4.22 | 2.9 | +| 300750 宁德时代 | 385.0 | 383.02-387.56 | 1.2 | +| 300035 中科电气 | 14.19 | 12.66-14.77 | 0.5 | +| 02202 万科企业 | 2.44 | 2.2-2.56 | 0.6 | +| 688639 华恒生物 | 17.35 | 16.59-17.36 | 0.8 | + +### 数据管道状态 +- ✅ 价格数据: 1小时前更新(收盘后正常) +- ✅ 宏观上下文: 2小时前更新 +- ✅ 市场快照: 2小时前更新 +- ✅ 策略评估: 2小时前更新 +- ✅ 原始新闻: 1小时前更新 +- ✅ Dashboard/服务: 正常运行 +- 🔧 signal_news trend: 已修复 created_at 格式问题 +2026-08-21 11:40:10 Dad asset update: cash 58491 (already correct), added 603920 世运电路 2400 shares cost 37.93, total_mv fixed to 869472.60 (HKD converted), total_assets=927963.65, position_pct=93.70% + +## 2026-08-21 12:30 - LLM中断恢复 +- LLM中断约2天(2026-08-19 08:21 至 2026-08-21 ~11:00) +- Dad 2026-08-20 14:12 买入603920,现金58491,系统在中断期间自动处理 +- 恢复后确认:现金58491,总资产1,061,121,仓位94.5% + +## 2026-08-25 13:15 price_monitor.py 市值同步bug + +**问题**: price_monitor.py 第464行更新price后未重算market_value,导致holdings.market_value与实时价漂移。 + +**根因代码** (deploy/profile-scripts/price_monitor.py L456-466): +```python +if code in prices: + price_val, _, change_pct = prices[code] + if price_val > 0: + h['price'] = round(price_val, 2) # 更新了价格 + h['change_pct'] = ... # 更新了涨跌幅 + # 但没有重算 h['market_value']! +``` + +**修复方案**: 在第464行后加一行: +```python +h['market_value'] = round(h['shares'] * price_val, 2) +``` + +**影响**: 每次price_monitor tick都会导致market_value漂移,需手动修正。 + +**临时修正**: 已用live_prices全量重算holdings.market_value (2026-08-25 13:15) + +**待办**: 需要笑笑审核并提交代码修复 (kanban API不可用,已记录) + diff --git a/evolution/__init__.py b/evolution/__init__.py deleted file mode 100644 index 42d799b7..00000000 --- a/evolution/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -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/evolution/b_group_miner.py b/evolution/b_group_miner.py deleted file mode 100644 index f7a5759f..00000000 --- a/evolution/b_group_miner.py +++ /dev/null @@ -1,200 +0,0 @@ -# -*- 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/evolution/evolution_api.py b/evolution/evolution_api.py deleted file mode 100644 index a25527fe..00000000 --- a/evolution/evolution_api.py +++ /dev/null @@ -1,256 +0,0 @@ -""" -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/evolution/evolution_engine.py b/evolution/evolution_engine.py deleted file mode 100644 index bbff4152..00000000 --- a/evolution/evolution_engine.py +++ /dev/null @@ -1,438 +0,0 @@ -# -*- 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/evolution/health_monitor.py b/evolution/health_monitor.py deleted file mode 100644 index de5293e7..00000000 --- a/evolution/health_monitor.py +++ /dev/null @@ -1,143 +0,0 @@ -""" -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/evolution/hypothesis_miner.py b/evolution/hypothesis_miner.py deleted file mode 100644 index e6c9f13b..00000000 --- a/evolution/hypothesis_miner.py +++ /dev/null @@ -1,142 +0,0 @@ -# -*- 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/evolution/lesson_extractor.py b/evolution/lesson_extractor.py deleted file mode 100644 index 2f97c7e3..00000000 --- a/evolution/lesson_extractor.py +++ /dev/null @@ -1,150 +0,0 @@ -# -*- 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/evolution/merge_b_group.py b/evolution/merge_b_group.py deleted file mode 100644 index 8724d22b..00000000 --- a/evolution/merge_b_group.py +++ /dev/null @@ -1,217 +0,0 @@ -# -*- 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/evolution/precompute_evolution.py b/evolution/precompute_evolution.py deleted file mode 100644 index 9ba9cc83..00000000 --- a/evolution/precompute_evolution.py +++ /dev/null @@ -1,70 +0,0 @@ -# -*- 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()