refine strategy variant lab
Deploy / lint (push) Successful in 7s
Deploy / test (push) Successful in 1m0s
Deploy / deploy (push) Successful in 33s

This commit is contained in:
2026-07-02 16:47:58 +02:00
parent 80b4113280
commit 849489a4b5
4 changed files with 54 additions and 93 deletions
+37 -79
View File
@@ -968,7 +968,6 @@ def _simulate_portfolio(
ranking_key: str = "momentum_percentile",
max_positions: int = SIM_MAX_POSITIONS,
risk_per_trade: float = SIM_RISK_PER_TRADE,
risk_scale_by_ord: dict[int, float] | None = None,
) -> dict | None:
"""Replay the qualified setups as ONE capital-constrained book and report
portfolio economics from the daily equity curve (return, CAGR, drawdown,
@@ -1077,12 +1076,8 @@ def _simulate_portfolio(
risk_ps = entry - stop
if risk_ps <= 0 or entry <= 0:
continue
risk_scale = (risk_scale_by_ord or {}).get(o, 1.0)
effective_risk = risk_per_trade * risk_scale
if effective_risk <= 0:
continue
shares = min(
(equity * effective_risk) / risk_ps,
(equity * risk_per_trade) / risk_ps,
(equity * SIM_NOTIONAL_CAP) / entry,
max(cash, 0.0) / (entry * (1.0 + COST_PER_SIDE)),
)
@@ -1211,6 +1206,15 @@ STRATEGY_VARIANTS: tuple[dict, ...] = (
"risk_per_trade": 0.01,
"risk_scale": None,
},
{
"variant": "raw_90_fixed15",
"label": "Raw 90 / max 15",
"percentile_key": "momentum_percentile",
"cutoff": 90.0,
"max_positions": 15,
"risk_per_trade": 0.01,
"risk_scale": None,
},
{
"variant": "residual_80_fixed10",
"label": "Residual 80 / max 10",
@@ -1221,11 +1225,20 @@ STRATEGY_VARIANTS: tuple[dict, ...] = (
"risk_scale": None,
},
{
"variant": "residual_90_fixed10",
"label": "Residual 90 / max 10",
"variant": "residual_80_fixed15",
"label": "Residual 80 / max 15",
"percentile_key": "residual_momentum_percentile",
"cutoff": 90.0,
"max_positions": 10,
"cutoff": 80.0,
"max_positions": 15,
"risk_per_trade": 0.01,
"risk_scale": None,
},
{
"variant": "residual_80_fixed20",
"label": "Residual 80 / max 20",
"percentile_key": "residual_momentum_percentile",
"cutoff": 80.0,
"max_positions": 20,
"risk_per_trade": 0.01,
"risk_scale": None,
},
@@ -1238,24 +1251,6 @@ STRATEGY_VARIANTS: tuple[dict, ...] = (
"risk_per_trade": 0.01,
"risk_scale": None,
},
{
"variant": "raw_80_regime_scaled",
"label": "Raw 80 / SPY-200 risk scale",
"percentile_key": "momentum_percentile",
"cutoff": 80.0,
"max_positions": 10,
"risk_per_trade": 0.01,
"risk_scale": "spy_200",
},
{
"variant": "residual_80_regime_scaled",
"label": "Residual 80 / SPY-200 risk scale",
"percentile_key": "residual_momentum_percentile",
"cutoff": 80.0,
"max_positions": 10,
"risk_per_trade": 0.01,
"risk_scale": "spy_200",
},
)
@@ -1272,33 +1267,14 @@ def _qualifies_by_percentile(cand: dict, percentile_key: str, threshold: float)
return pct is not None and pct >= threshold
def _spy_200_risk_scale(spy_closes: dict[date, float] | None) -> dict[int, float]:
"""Entry-date risk scale: 0.5 when SPY closes below its 200-day SMA, else 1.0.
Missing/short benchmark history returns an empty map, which the simulator
treats as unscaled 1.0 risk."""
if not spy_closes:
return {}
rows = sorted((d, c) for d, c in spy_closes.items() if c and c > 0)
out: dict[int, float] = {}
closes: list[float] = []
for d, close in rows:
closes.append(float(close))
if len(closes) < 200:
continue
sma = sum(closes[-200:]) / 200.0
out[d.toordinal()] = 0.5 if close < sma else 1.0
return out
def _strategy_variant_sims(
candidates: list[dict],
prices: dict[str, tuple],
spy_closes: dict[date, float] | None,
_spy_closes: dict[date, float] | None,
hold_days: int,
) -> list[dict]:
"""Research-only portfolio variants for comparing rank signals, cutoff, book
capacity, and simple SPY-200 risk scaling. Live qualification is untouched."""
risk_scales = {"spy_200": _spy_200_risk_scale(spy_closes)}
"""Research-only portfolio variants for comparing rank signals, cutoff and
book capacity. Live qualification is untouched."""
rows: list[dict] = []
for cfg in STRATEGY_VARIANTS:
percentile_key = str(cfg["percentile_key"])
@@ -1306,14 +1282,13 @@ def _strategy_variant_sims(
sim = _simulate_portfolio(
candidates,
prices,
spy_closes,
_spy_closes,
"hold",
hold_days,
qualified_fn=lambda c, pk=percentile_key, th=cutoff: _qualifies_by_percentile(c, pk, th),
ranking_key=percentile_key,
max_positions=int(cfg["max_positions"]),
risk_per_trade=float(cfg["risk_per_trade"]),
risk_scale_by_ord=risk_scales.get(cfg["risk_scale"]),
)
if sim is None:
continue
@@ -1358,7 +1333,7 @@ def _build_research_recommendation(report: dict) -> dict:
residuals = [
v for key, v in variants.items()
if key.startswith("residual_") and v.get("risk_scale") is None
if key.startswith("residual_80_") and v.get("risk_scale") is None
]
residual = max(residuals, key=lambda v: v.get("sharpe") or -999, default=None)
if (
@@ -1378,32 +1353,15 @@ def _build_research_recommendation(report: dict) -> dict:
),
})
raw_regime = variants.get("raw_80_regime_scaled")
if (
raw_regime and base_dd is not None and base_cagr is not None
and raw_regime.get("cagr_pct") is not None
and raw_regime.get("max_drawdown_pct") is not None
):
dd_reduction = (base_dd - raw_regime["max_drawdown_pct"]) / base_dd if base_dd > 0 else None
cagr_loss = _pct_loss(base_cagr, raw_regime.get("cagr_pct"))
candidate = (
dd_reduction is not None and cagr_loss is not None
and dd_reduction >= 0.20 and cagr_loss <= 0.15
)
items.append({
"topic": "regime_scaling",
"candidate": candidate,
"text": (
f"SPY-200 risk scaling {'is a promotion candidate' if candidate else 'stays research-only'}: "
f"drawdown {raw_regime['max_drawdown_pct']:.1f}% vs {base_dd:.1f}%, "
f"CAGR {raw_regime.get('cagr_pct'):+.1f}% vs {base_cagr:+.1f}%."
),
})
raw_90 = variants.get("raw_90_fixed10")
raw_90s = [
v for key, v in variants.items()
if key.startswith("raw_90_") and v.get("risk_scale") is None
]
raw_90 = max(raw_90s, key=lambda v: v.get("sharpe") or -999, default=None)
if (
raw_90 and base_sharpe is not None and base_dd is not None and base_cagr is not None
and raw_90.get("sharpe") is not None and raw_90.get("cagr_pct") is not None
and raw_90.get("max_drawdown_pct") is not None
):
cagr_loss = _pct_loss(base_cagr, raw_90.get("cagr_pct"))
raw_90_sharpe = raw_90.get("sharpe")
@@ -1418,7 +1376,7 @@ def _build_research_recommendation(report: dict) -> dict:
"candidate": candidate,
"text": (
f"Cutoff 90 {'is a promotion candidate' if candidate else 'stays research-only'}: "
f"Sharpe {raw_90_sharpe:.2f} vs {base_sharpe:.2f}, "
f"{raw_90['label']} Sharpe {raw_90_sharpe:.2f} vs {base_sharpe:.2f}, "
f"drawdown {raw_90['max_drawdown_pct']:.1f}% vs {base_dd:.1f}%, "
f"CAGR {raw_90.get('cagr_pct'):+.1f}% vs {base_cagr:+.1f}%."
),
@@ -1822,8 +1780,8 @@ async def run_backtest(
"variants": strategy_variant_rows,
"note": (
"Research-only hold-to-horizon portfolio variants. These compare "
"raw vs residual momentum ranking, cutoff 80 vs 90, max 10 vs 15 "
"positions, and SPY-200 risk scaling. They do not change live "
"raw vs residual momentum ranking, cutoff 80 vs 90, and max 10/15/20 "
"position capacity. They do not change live "
"qualification or paper-trade behavior."
),
},