backtest: add min target-probability sweep
Re-applies the activation gate at several min_target_probability thresholds (60→30, other conditions fixed) over the already-replayed candidates, so the trade-off between how many setups qualify and their expectancy is visible in one table — the cheap "optimize" half of Phase 2. Candidates now carry meets_core + best_prob so the sweep needs no re-replay. New sweep table in BacktestPanel with the current threshold starred. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -160,6 +160,12 @@ def _window_setups(
|
||||
stop_loss=stop,
|
||||
entry_price=entry,
|
||||
)
|
||||
# meets_core = clears every gate EXCEPT target probability, so the report
|
||||
# can sweep the min_target_probability threshold without re-replaying.
|
||||
core_config = {**activation, "min_target_probability": 0.0}
|
||||
meets_core = setup_qualifies(setup_ns, core_config)
|
||||
best_prob = best_target_probability(setup_ns)
|
||||
min_tp = float(activation.get("min_target_probability", 0.0))
|
||||
out.append({
|
||||
"direction": direction,
|
||||
"entry": entry,
|
||||
@@ -168,10 +174,11 @@ def _window_setups(
|
||||
"rr": rr,
|
||||
"confidence": confidences[direction],
|
||||
"primary_prob": float(primary["probability"]),
|
||||
"best_prob": best_target_probability(setup_ns),
|
||||
"best_prob": best_prob,
|
||||
"meets_core": meets_core,
|
||||
"action": action,
|
||||
"risk_level": risk_level,
|
||||
"qualified": setup_qualifies(setup_ns, activation),
|
||||
"qualified": meets_core and best_prob >= min_tp,
|
||||
})
|
||||
return out
|
||||
|
||||
@@ -208,6 +215,8 @@ def _replay_ticker(symbol: str, records: list, config: dict, activation: dict) -
|
||||
"rr": s["rr"],
|
||||
"confidence": s["confidence"],
|
||||
"primary_prob": s["primary_prob"],
|
||||
"best_prob": s["best_prob"],
|
||||
"meets_core": s["meets_core"],
|
||||
"qualified": s["qualified"],
|
||||
"outcome": outcome,
|
||||
"target_hit": target_hit,
|
||||
@@ -279,6 +288,15 @@ async def run_backtest(
|
||||
longs = [c for c in qualified if c["direction"] == "long"]
|
||||
shorts = [c for c in qualified if c["direction"] == "short"]
|
||||
|
||||
# Threshold sweep: re-apply the gate at several min_target_probability values
|
||||
# (holding the other conditions fixed) so the trade-off between how many
|
||||
# setups qualify and their expectancy is visible without re-replaying.
|
||||
current_min_tp = float(activation.get("min_target_probability", 60.0))
|
||||
sweep = []
|
||||
for threshold in (60, 55, 50, 45, 40, 35, 30):
|
||||
cands = [c for c in candidates if c["meets_core"] and c["best_prob"] >= threshold]
|
||||
sweep.append({"min_target_probability": threshold, **_bucket_stats(cands)})
|
||||
|
||||
return {
|
||||
"generated_at": datetime.now(timezone.utc).isoformat(),
|
||||
"tickers": total,
|
||||
@@ -292,6 +310,8 @@ async def run_backtest(
|
||||
"long": _bucket_stats(longs),
|
||||
"short": _bucket_stats(shorts),
|
||||
},
|
||||
"min_target_probability": current_min_tp,
|
||||
"sweep": sweep,
|
||||
"calibration": _calibration(candidates),
|
||||
"note": (
|
||||
"Sentiment & fundamentals held neutral (no point-in-time history). "
|
||||
|
||||
Reference in New Issue
Block a user