333 lines
15 KiB
Python
333 lines
15 KiB
Python
#!/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()
|