diff --git a/deploy/profile-scripts/strategy_router.py b/deploy/profile-scripts/strategy_router.py index 2abb7e24..645c22d4 100644 --- a/deploy/profile-scripts/strategy_router.py +++ b/deploy/profile-scripts/strategy_router.py @@ -25,6 +25,19 @@ 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 = { @@ -87,19 +100,28 @@ def load_temp(market='a'): def load_regime_perf(market='a'): - """读取策略-温区表现。market='a'(默认,A股,行为不变) / 'hk'(港股策略)""" + """读取策略-温区表现(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 " - "WHERE COALESCE(market,'a')=?", (market,) + "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 {} @@ -134,10 +156,13 @@ def route(regime, temp, perf): # 2026-08-16 资格标准A(老莫):长期10y/近期2y/当下1y 适应温区年化>大盘 三项达标 + 手动可用 # (替代只看胜率≥50%的旧判定——旧标准会让年化-41%但胜率51%的策略激活) try: - from strategy_qualify import evaluate_strategy, is_available, get_benchmarks + from strategy_qualify import evaluate_all_regimes, is_available, get_benchmarks _qbench = get_benchmarks('a') - _qev = evaluate_strategy(name, 'a', best_regime=best[0], bench=_qbench) - _qual_ok = bool(_qev and _qev.get("long_ok") and _qev.get("mid_ok") and _qev.get("short_ok")) + # 2026-08-17 择优激活:按当前温区评估资格(同 server.py 修复——best_regime 会让 + # v_lurk_v3 在 trend_down 达标却被 choppy 评估误判不合格) + _qev_all = evaluate_all_regimes(name, 'a', bench=_qbench) + _qev_cur = (_qev_all or {}).get(current_regime) or {} + _qual_ok = bool(_qev_cur.get("long_ok") and _qev_cur.get("mid_ok") and _qev_cur.get("short_ok")) _manual_ok = is_available(name) matched = _qual_ok and _manual_ok except Exception: @@ -164,6 +189,58 @@ def route(regime, temp, perf): 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股温区 @@ -176,9 +253,9 @@ def main(): "temp_band": temp.get("band", "unknown"), "temp_rsi": temp.get("rsi"), "weights": weights, - "active": [k for k, v in weights.items() if v["matched"]], + "active": _select_active(weights, regime.get("regime", "unknown"), 'a'), "updated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "note": "温区(平滑K=5)选策略类型 + 温度(rsi)乘数;策略全温区发信号,适用温区由实测动态决定", + "note": "择优激活(2026-08-17): 每温区按质量分(综合分×普适有效年占比)排序+家族去重取前%d个; 温区(平滑K=5)选类型+温度乘数" % MAX_ACTIVE, } # 港股路由(2026-08-14 新增,不影响A股)——港股组合按温区调度(hk_strategies 定义) try: