feat: router择优激活——按当前温区评估资格+质量分(综合×普适)排序+家族去重取前3,温区切换自动轮换
This commit is contained in:
@@ -25,6 +25,19 @@ sys.path.insert(0, "/home/hmo/MoFin")
|
|||||||
|
|
||||||
OUT = Path("/home/hmo/MoFin/data/strategy_weights.json")
|
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"
|
DEFAULT_FAMILY = "mr"
|
||||||
|
|
||||||
FAMILY_MAP = {
|
FAMILY_MAP = {
|
||||||
@@ -87,19 +100,28 @@ def load_temp(market='a'):
|
|||||||
|
|
||||||
|
|
||||||
def load_regime_perf(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
|
conn = None
|
||||||
try:
|
try:
|
||||||
import sqlite3
|
import sqlite3
|
||||||
conn = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=30)
|
conn = sqlite3.connect("/home/hmo/MoFin/data/mofin.db", timeout=30)
|
||||||
conn.execute("PRAGMA busy_timeout=30000")
|
conn.execute("PRAGMA busy_timeout=30000")
|
||||||
|
# 新表 by_period(2y) 优先,旧表补齐
|
||||||
rows = conn.execute(
|
rows = conn.execute(
|
||||||
"SELECT strategy, regime, trades, win_rate, avg_pnl FROM strategy_regime_perf "
|
"SELECT strategy, regime, trades, win_rate, avg_pnl FROM strategy_regime_perf_by_period "
|
||||||
"WHERE COALESCE(market,'a')=?", (market,)
|
"WHERE COALESCE(market,'a')=? AND period_tag='2y'", (market,)
|
||||||
).fetchall()
|
).fetchall()
|
||||||
result = {}
|
result = {}
|
||||||
for strat, reg, n, wr, pnl in rows:
|
for strat, reg, n, wr, pnl in rows:
|
||||||
result.setdefault(strat, {})[reg] = {"trades": n, "win_rate": wr, "avg_pnl": pnl}
|
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
|
return result
|
||||||
except Exception:
|
except Exception:
|
||||||
return {}
|
return {}
|
||||||
@@ -134,10 +156,13 @@ def route(regime, temp, perf):
|
|||||||
# 2026-08-16 资格标准A(老莫):长期10y/近期2y/当下1y 适应温区年化>大盘 三项达标 + 手动可用
|
# 2026-08-16 资格标准A(老莫):长期10y/近期2y/当下1y 适应温区年化>大盘 三项达标 + 手动可用
|
||||||
# (替代只看胜率≥50%的旧判定——旧标准会让年化-41%但胜率51%的策略激活)
|
# (替代只看胜率≥50%的旧判定——旧标准会让年化-41%但胜率51%的策略激活)
|
||||||
try:
|
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')
|
_qbench = get_benchmarks('a')
|
||||||
_qev = evaluate_strategy(name, 'a', best_regime=best[0], bench=_qbench)
|
# 2026-08-17 择优激活:按当前温区评估资格(同 server.py 修复——best_regime 会让
|
||||||
_qual_ok = bool(_qev and _qev.get("long_ok") and _qev.get("mid_ok") and _qev.get("short_ok"))
|
# 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)
|
_manual_ok = is_available(name)
|
||||||
matched = _qual_ok and _manual_ok
|
matched = _qual_ok and _manual_ok
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -164,6 +189,58 @@ def route(regime, temp, perf):
|
|||||||
return dict(sorted(weights.items(), key=lambda x: -x[1]["weight"]))
|
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():
|
def main():
|
||||||
# A股路由(完全不变)
|
# A股路由(完全不变)
|
||||||
regime = load_regime() # A股温区
|
regime = load_regime() # A股温区
|
||||||
@@ -176,9 +253,9 @@ def main():
|
|||||||
"temp_band": temp.get("band", "unknown"),
|
"temp_band": temp.get("band", "unknown"),
|
||||||
"temp_rsi": temp.get("rsi"),
|
"temp_rsi": temp.get("rsi"),
|
||||||
"weights": weights,
|
"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"),
|
"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 定义)
|
# 港股路由(2026-08-14 新增,不影响A股)——港股组合按温区调度(hk_strategies 定义)
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user