Tighten qualified signal gate
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user