Files
MoFin/archive/evolution-cleanup-20260821/evolution/merge_b_group.py
T
xxm 5b9d46efc6 refactor: 归档策略进化模块+新建评估页面+API
- 归档 evolution/ + meta_growth/meta_watchdog/ab_research_daily
- docs/evolution-archive-readme.md: 归档说明(旧模块功能+替代方案)
- server.py: 新增 /api/research/effectiveness + effectiveness/summary + recommendation_log + execution_log
- static/effectiveness.html: 新评估页面(概览/详细评估/推荐记录/执行记录)
- 策略进化改为人驱动闭环(评估→用户决策→调整)
2026-08-21 02:47:38 +08:00

218 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""evolution/merge_b_group.py — AB融合机制(2026-08-16 方向二闭环)
老莫:B组候选与A组对照后,融合/合并成为最终实施组(新的A组)。
流程:
1. 读 B 组 verified 候选(data/b_group_candidates.json, status='verified'
2. 老莫选择要融合的候选 → 注册为正式策略版本:
- A股:写入 strategy_researchresults_json 用回测验证的 trades
- 港股:注册进 hk_strategies.pyentry 条件)
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_researchB组候选,供研究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))