Files
MoFin/deploy/profile-scripts/strategy_router.py
T

333 lines
15 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.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""strategy_router.py v5 — MoFin 策略动态路由(动态策略库,2026-08-13)
核心(老莫原则):
- 策略全温区发信号(不留温区门控在策略内)
- 策略-温区表现常态化记录(regime_perf.py / strategy_regime_perf),适用温区动态化
- 组合按当前温区自动激活/休眠策略:当前温区实测胜率高的策略优先
- 不漏历史策略(v_mr_sel trend_down 93%、v7.3 trend_up 94% 等被埋没的强者)
数据流:
market_regime(三态) + regime_tracker(平滑K=5) → 当前温区
temp_band(rsi) → 温度/仓位乘数
regime_perf(实测) → 各策略温区表现
→ strategy_weights.json(各策略权重/激活状态)
"""
import json
import sys
from pathlib import Path
from datetime import datetime
_SCRIPT_DIR = Path(__file__).resolve().parent
sys.path.insert(0, str(_SCRIPT_DIR))
sys.path.insert(0, "/home/hmo/MoFin")
OUT = Path("/home/hmo/MoFin/data/strategy_weights.json")
# 2026-08-17 老莫:择优激活——每温区最多激活的策略数
MAX_ACTIVE = 3
# 家族映射(同家族只保留质量最优的一个,避免重复策略占位)
FAMILY_MAP_ACT = {
"s2_panic": "s2", "s2_panic_v2": "s2",
"v_lurk_v1": "vlurk", "v_lurk_v2": "vlurk", "v_lurk_v3": "vlurk",
"v_mr": "vmr", "v_mr2": "vmr", "v_mr3": "vmr", "v_mr4": "vmr",
"v_oversold": "vover", "v_weak": "vweak",
"b_td1": "b_td", "b_td1_v2": "b_td", "b_td1_v3": "b_td",
"v1.0": "v1", "v2.0": "v2", "v3.0": "v3", "v_next": "vnext",
}
DEFAULT_FAMILY = "mr"
FAMILY_MAP = {
"v_weak": "mr", "v_oversold": "mr", "v_mr": "mr", "v_mr2": "mr", "v_mr3": "mr",
"v_mr4": "mr", "v_mr_sel": "mr", "v_lurk_v1": "mr", "v_lurk_v2": "mr", "v_lurk_v3": "mr",
"s2_panic": "mr", "v_osc": "mr",
"v_next": "trend", "v_next3": "trend", "v_next4": "trend", "v_next5": "trend",
"v8.0": "trend", "v8.1": "trend", "v8.2": "trend", "v8.3": "trend",
"v7.0": "trend", "v7.1": "trend", "v7.1b": "trend", "v7.2": "trend", "v7.3": "trend",
"v6.0": "trend", "v6.1": "trend", "v6.2": "trend",
"v9.0": "trend", "v9.1": "trend", "v9.2": "trend",
"v_combo": "trend", "v1.0": "trend", "v2.0": "trend", "v3.0": "trend",
"v4.0": "trend", "v4.0a": "trend", "v4.0b": "trend", "v4.0c": "trend",
"v4.0d": "trend", "v4.0e": "trend", "v4.0f": "trend", "v4.0g": "trend",
"v4.0h": "trend", "v4.1": "trend", "v5.0": "trend",
"v11.0": "trend", "v11.1": "trend",
"hk_mr1": "mr", # 港股深度超卖反弹(2026-08-14 港股归因研发,trend_down 主战场)
}
def load_regime(market='a'):
"""读取当前温区。market='a'(默认,A股 smoothed 顶层,行为不变) / 'hk'(港股 markets.hk)"""
if market == 'hk':
try:
p = Path("/home/hmo/MoFin/data/market_regime_smoothed.json")
if p.exists():
d = json.loads(p.read_text(encoding="utf-8"))
mk = (d.get("markets") or {}).get("hk") or {}
if mk.get("current_regime"):
return {"regime": mk["current_regime"], "date": mk.get("current_date", "")}
except Exception:
pass
try:
from market_regime import load_market_regime
return load_market_regime(market='hk')
except Exception:
return {"regime": "unknown"}
try:
p = Path("/home/hmo/MoFin/data/market_regime_smoothed.json")
if p.exists():
d = json.loads(p.read_text(encoding="utf-8"))
return {"regime": d.get("current_regime", "unknown"),
"date": d.get("current_date", "")}
except Exception:
pass
try:
from market_regime import load_market_regime
return load_market_regime()
except Exception:
return {"regime": "unknown"}
def load_temp(market='a'):
"""读取当前温度。market='a'(默认,A股) / 'hk'(港股)"""
try:
from temp_band import get_market_temp
return get_market_temp(market=market)
except Exception:
return {"band": "unknown", "rsi": None}
def load_regime_perf(market='a'):
"""读取策略-温区表现(2026-08-17 改用 by_period 2y——含 b_td1_v3/s2_panic_v2 等新策略)。
旧表 strategy_regime_perf 不含新策略,导致择优激活漏选;by_period 全量覆盖。"""
conn = None
try:
import sqlite3
conn = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=30)
conn.execute("PRAGMA busy_timeout=30000")
# 新表 by_period(2y) 优先,旧表补齐
rows = conn.execute(
"SELECT strategy, regime, trades, win_rate, avg_pnl FROM strategy_regime_perf_by_period "
"WHERE COALESCE(market,'a')=? AND period_tag='2y'", (market,)
).fetchall()
result = {}
for strat, reg, n, wr, pnl in rows:
result.setdefault(strat, {})[reg] = {"trades": n, "win_rate": wr, "avg_pnl": pnl}
# 旧表补缺(by_period 没覆盖的策略)
rows2 = conn.execute(
"SELECT strategy, regime, trades, win_rate, avg_pnl FROM strategy_regime_perf "
"WHERE COALESCE(market,'a')=?", (market,)
).fetchall()
for strat, reg, n, wr, pnl in rows2:
result.setdefault(strat, {}).setdefault(reg, {"trades": n, "win_rate": wr, "avg_pnl": pnl})
return result
except Exception:
return {}
finally:
if conn:
conn.close()
def route(regime, temp, perf):
current_regime = regime.get("regime", "unknown")
band = temp.get("band", "unknown")
weights = {}
# 已证伪策略(从 strategy_research.deprecated 读,数据驱动——证伪绝不激活)
dep = set()
try:
import sqlite3 as _sq
_c = _sq.connect("/home/hmo/MoFin/data/mofin.db", timeout=5)
dep = {r[0] for r in _c.execute(
"SELECT DISTINCT version FROM strategy_research WHERE deprecated IS NOT NULL AND deprecated != ''")}
_c.close()
except Exception:
pass
for name, regs in perf.items():
if not regs:
continue
if name in dep: # 已证伪策略绝不激活(数据驱动,2026-08-15)
continue
cur = regs.get(current_regime)
best = max(regs.items(), key=lambda x: x[1].get("win_rate", 0))
best_wr = best[1].get("win_rate", 0)
cur_wr = cur.get("win_rate") if cur else None
# 2026-08-16 资格标准A(老莫):长期10y/近期2y/当下1y 适应温区年化>大盘 三项达标 + 手动可用
# 2026-08-18 重构(老莫):matched 改为【任一温区三项达标】即可(不只当前温区)。
# 核心:资格够就必须能激活;当前温区是实盘闸门,但choppy/trend_up等温区达标策略也要备好激活。
# 不合格(如 v_lurk_v3 @ choppy ✅❌❌)绝不激活该温区;合格(v_lurk_v2 @ choppy)就激活。
try:
from strategy_qualify import evaluate_all_regimes, is_available, get_benchmarks
_qbench = get_benchmarks('a')
_qev_all = evaluate_all_regimes(name, 'a', bench=_qbench) or {}
# 达标温区列表(任一温区三项全达)
_qual_regimes = []
for _rg in ("trend_down", "choppy", "trend_up"):
_q = (_qev_all or {}).get(_rg) or {}
if _q.get("long_ok") and _q.get("mid_ok") and _q.get("short_ok"):
_qual_regimes.append(_rg)
_manual_ok = is_available(name)
matched = bool(_qual_regimes) and _manual_ok
_qual_cur_ok = False
if _qual_regimes:
_qev_cur = (_qev_all or {}).get(current_regime) or {}
_qual_cur_ok = bool(_qev_cur.get("long_ok") and _qev_cur.get("mid_ok") and _qev_cur.get("short_ok"))
except Exception:
_qual_regimes = [current_regime] if ((best_wr >= 50) and (cur_wr is not None and cur_wr >= 50)) else []
_qual_cur_ok = current_regime in _qual_regimes
matched = bool(_qual_regimes)
# ✓ 记录达标温区(供按温区激活矩阵)
qualifies = _qual_regimes if 'qualifies' in dir() else []
family = FAMILY_MAP.get(name, DEFAULT_FAMILY)
base = 1.0 if matched else 0.3
try:
from temp_band import temp_multiplier
mult = temp_multiplier(band, family)
except Exception:
mult = 0.8
weights[name] = {
"weight": round(base * mult, 2),
"regime": current_regime,
"temp_band": band,
"family": family,
"matched": matched,
"qualifies": qualifies, # 达标温区列表(2026-08-18
"qual_cur_ok": _qual_cur_ok, # 当前温区是否达标(2026-08-18)
"best_regime": best[0],
"best_win_rate": best[1].get("win_rate", 0),
"cur_win_rate": cur.get("win_rate") if cur else None,
"cur_avg_pnl": cur.get("avg_pnl") if cur else None,
"trades": cur.get("trades") if cur else 0,
"active_regimes": qualifies, # 别名(前端用)
}
return dict(sorted(weights.items(), key=lambda x: -x[1]["weight"]))
def _quality_score(name, market, regime):
"""质量分 = 综合分(含效率惩罚) × 普适有效年占比(与 strategy_activation_selector 一致)"""
try:
import sqlite3 as _sq
_c = _sq.connect("/home/hmo/MoFin/data/mofin.db", timeout=5)
r = _c.execute(
"SELECT trades, positions_taken, win_rate, sharpe_ratio, profit_factor, total_return_pct, "
"portfolio_max_dd_pct, universality_score, universality_years, universality_valid_years "
"FROM strategy_regime_perf_by_period WHERE strategy=? AND market=? AND regime=? AND period_tag='2y'",
(name, market, regime)).fetchone()
_c.close()
if not r:
return 0
sig, pos, wr, sh, pf, ret, dd, univ, uy, uv = r
pos = pos or 0
ret_c = min(ret or 0, 100) / 100 * 30
wr_c = (wr or 0) / 100 * 20
sh_c = min(max(sh or 0, 0), 20) / 20 * 20
pf_c = min(pf or 0, 5) / 5 * 15
dd_c = (1 - min(dd or 0, 50) / 50) * 15
conf = min(1, (sig or 0) / 40)
# 效率惩罚(信号/成交比)
ratio = sig / pos if pos else 99
eff = 1.0 if ratio <= 2 else 0.9 if ratio <= 5 else 0.75 if ratio <= 10 else 0.5
comp = (ret_c + wr_c + sh_c + pf_c + dd_c) * conf * eff
univ_ratio = (uv / uy) if uy else 0
return comp * (0.5 + 0.5 * univ_ratio)
except Exception:
return 0
def _select_active(weights, regime, market):
"""择优激活:matched 策略按质量分排序,家族去重,取前 MAX_ACTIVE"""
matched = [(k, v) for k, v in weights.items() if v.get("matched")]
scored = []
for name, v in matched:
q = _quality_score(name, market, regime)
fam = FAMILY_MAP_ACT.get(name, name)
scored.append({"name": name, "quality": q, "family": fam})
scored.sort(key=lambda x: -x["quality"])
used_fam = set()
sel = []
for s in scored:
if s["family"] in used_fam:
continue
if len(sel) >= MAX_ACTIVE:
break
used_fam.add(s["family"])
sel.append(s["name"])
return sel
def main():
# A股路由(完全不变)
regime = load_regime() # A股温区
temp = load_temp() # A股温度
perf = load_regime_perf('a') # A股策略表现
weights = route(regime, temp, perf)
# 2026-08-18:按温区生成激活矩阵(老莫:资格够就激活,不合格绝不激活)
# - 每个温区,从"在该温区三项达标(qualifies含该温区)"里按质量分择优取前 MAX_ACTIVE
# - 当前温区 active = regime_active[当前温区](实盘闸门)
_all_regimes = ("trend_down", "choppy", "trend_up")
_cur_rg = regime.get("regime", "unknown")
regime_active = {}
for _rg in _all_regimes:
# 该温区达标策略(qualifies 含 rg
_rg_matched = [(k, v) for k, v in weights.items()
if v.get("matched") and _rg in v.get("qualifies", [])]
# 按质量分择优
_rg_scored = []
for name, v in _rg_matched:
q = _quality_score(name, 'a', _rg)
fam = FAMILY_MAP_ACT.get(name, name)
_rg_scored.append({"name": name, "quality": q, "family": fam})
_rg_scored.sort(key=lambda x: -x["quality"])
_used = set(); _sel = []
for s in _rg_scored:
if s["family"] in _used: continue
if len(_sel) >= MAX_ACTIVE: break
_used.add(s["family"]); _sel.append(s["name"])
regime_active[_rg] = _sel
out = {
"state": _cur_rg,
"state_date": regime.get("date", ""),
"temp_band": temp.get("band", "unknown"),
"temp_rsi": temp.get("rsi"),
"weights": weights,
"active": regime_active.get(_cur_rg, []),
"regime_active": regime_active,
"updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"note": "择优激活(2026-08-18): 按温区——任一温区三项达标即可激活该温区;当前温区实盘闸门;不合格绝不激活;合格就激活(择优排序)",
}
# 港股路由(2026-08-14 新增,不影响A股)——港股组合按温区调度(hk_strategies 定义)
try:
hk_regime = load_regime('hk')
hk_temp = load_temp('hk')
from hk_strategies import strategies_for_regime
hk_cur_rg = hk_regime.get("regime", "unknown")
hk_active = strategies_for_regime(hk_cur_rg)
# 2026-08-18 港股按温区激活矩阵(老莫:非当前温区资格够也激活/备好)
hk_regime_active = {}
for _r in ("trend_down", "choppy", "trend_up"):
hk_regime_active[_r] = strategies_for_regime(_r)
out["markets"] = {"hk": {
"state": hk_cur_rg,
"state_date": hk_regime.get("date", ""),
"temp_band": hk_temp.get("band", "unknown"),
"active": hk_active,
"regime_active": hk_regime_active,
}}
except Exception as e:
print(f"港股路由失败(不影响A股): {e}", flush=True)
OUT.write_text(json.dumps(out, ensure_ascii=False, indent=1), encoding="utf-8")
print(f"strategy_weights.json: 状态={out['state']} 温度={out['temp_band']}(rsi={out['temp_rsi']})")
print(f"激活策略(A股): {out['active']}")
if "markets" in out:
hk = out["markets"]["hk"]
print(f"港股温区={hk['state']} 激活策略(港股): {hk['active']}")
print("权重排名(前15:")
for name, w in list(weights.items())[:15]:
mark = "✓" if w["matched"] else "观察"
print(f" {name:<12} w={w['weight']:<5} {w['best_regime']:<11} 最佳{w['best_win_rate']:>3.0f}% {mark}")
if __name__ == "__main__":
main()