Tighten qualified signal gate
Deploy / lint (push) Successful in 8s
Deploy / test (push) Successful in 1m18s
Deploy / deploy (push) Successful in 40s

This commit is contained in:
2026-07-04 13:51:44 +02:00
parent 23d1db1f30
commit 65335cf1f3
4 changed files with 74 additions and 20 deletions
+25 -11
View File
@@ -5,9 +5,9 @@ performance stats (server) and mirrored on the frontend. The core selection is
residual cross-sectional momentum: a setup's ticker must rank in the top
``min_momentum_percentile`` of the universe by beta-adjusted 12-1 month momentum.
R:R and confidence remain as floors, and conviction/conflict survive as optional
tighteners (off by default). The activation percentile is computed across the
universe and attached to each setup upstream; when it's absent the gate falls
back to the floors.
tighteners (off by default). Qualified setups must also have a probability-backed
target; otherwise a mathematically high R:R can be driven by a fragile target
with no independent validation.
"""
from __future__ import annotations
@@ -34,6 +34,18 @@ def best_target_probability(setup: Any) -> float:
return max(probs, default=0.0)
def primary_target_probability(setup: Any) -> float | None:
"""Probability of the primary/headline target, falling back to best target."""
targets = getattr(setup, "targets", None) or []
for target in targets:
if not isinstance(target, dict) or not target.get("is_primary"):
continue
probability = target.get("probability")
return float(probability) if probability is not None else None
best = best_target_probability(setup)
return best if best > 0 else None
def live_risk_reward(setup: Any, current_price: float) -> float | None:
"""R:R recomputed from the CURRENT price, not the (possibly stale) entry.
@@ -58,10 +70,10 @@ def setup_qualifies(setup: Any, config: dict) -> bool:
``setup`` is duck-typed: any object exposing rr_ratio, confidence_score,
recommended_action, risk_level and a ``targets`` list of dicts.
Gate order: R:R floor freshness (live R:R) → confidence floor → momentum
percentile (the core selection) optional conviction / conflict tighteners.
``min_momentum_percentile`` defaults to 0 (off) for callers that pass a legacy
config without the key.
Gate order: R:R floor, freshness (live R:R), target probability, confidence
floor, momentum percentile (the core selection), then optional conviction /
conflict tighteners. ``min_momentum_percentile`` defaults to 0 (off) for
callers that pass a legacy config without the key.
"""
if setup.rr_ratio < config["min_rr"]:
return False
@@ -73,20 +85,22 @@ def setup_qualifies(setup: Any, config: dict) -> bool:
live_rr = live_risk_reward(setup, float(current_price))
if live_rr is not None and live_rr < config["min_rr"]:
return False
if primary_target_probability(setup) is None:
return False
if (setup.confidence_score or 0.0) < config["min_confidence"]:
return False
# Residual cross-sectional momentum: the core selection. A setup's ticker
# must rank in the top ``min_momentum_percentile`` of the universe by
# beta-adjusted 12-1 momentum. The validated edge is long-only, so while the
# gate is active shorts (which fight the trend) never qualify. The percentile
# floor is only enforced when a percentile is attached (live setups /
# backtest); callers that don't attach it defer to the floors above.
# gate is active shorts (which fight the trend) never qualify. Missing ranks
# do not qualify because the production edge depends on this cross-sectional
# selection.
min_pct = float(config.get("min_momentum_percentile", 0.0))
if min_pct > 0:
if (getattr(setup, "direction", "long") or "long") == "short":
return False
momentum_percentile = getattr(setup, "momentum_percentile", None)
if momentum_percentile is not None and momentum_percentile < min_pct:
if momentum_percentile is None or momentum_percentile < min_pct:
return False
# A setup is actionable only when the live ticker action points in the same
# direction. NEUTRAL means no clear signal; an opposite action means the