Primary target: probability floor stops lottery headlines
A setup's primary target could carry a ~3% probability: the picker chose the most likely target among those with R:R >= 1.5, and after a run-up that pool can contain only far "lottery" levels (the near, likely levels fail the R:R floor). The lottery target's inflated R:R then became the setup's headline and passed the activation gate's min_rr floor - the gate's probability check only requires a value to exist. Fix, no new tuning knobs: the primary must clear BOTH floors (R:R >= 1.5 AND probability >= 20%). When nothing does, fall back to the most likely target overall, so the headline carries an honest low R:R and the gate rejects the setup on real numbers instead of being gamed by an unreachable target. Deliberately NOT pure EV-maximization (p*RR): the probability model adds strength/alignment bonuses as flat percentage points, so EV arithmetic would scale those bonuses by (RR+1) and systematically favor far targets on aligned setups - the same lottery bias through the back door. Shared by production (enhance_trade_setup) and the backtest simulator, so backtest comparisons stay apples-to-apples. Unit tests pin the degenerate case; full unit suite green (497 passed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -128,6 +128,32 @@ def test_primary_target_none_when_empty():
|
||||
assert _select_primary_target([]) is None
|
||||
|
||||
|
||||
def test_primary_target_never_headlines_a_lottery():
|
||||
# After a run-up: the only target clearing the R:R floor is a near-impossible
|
||||
# far level. The primary must NOT be that lottery — fall back to the most
|
||||
# likely target overall, so the headline R:R is honest (and the activation
|
||||
# gate rejects the setup on min_rr instead of being gamed).
|
||||
targets = [
|
||||
{"price": 101.0, "rr_ratio": 0.9, "probability": 55.0}, # likely, no asymmetry
|
||||
{"price": 140.0, "rr_ratio": 5.0, "probability": 3.0}, # asymmetric lottery
|
||||
]
|
||||
primary = _select_primary_target(targets)
|
||||
assert primary is not None
|
||||
assert primary["price"] == 101.0
|
||||
|
||||
|
||||
def test_primary_target_requires_probability_floor():
|
||||
# A worthwhile-R:R target below the probability floor loses to one that
|
||||
# clears both floors, even at lower R:R.
|
||||
targets = [
|
||||
{"price": 130.0, "rr_ratio": 4.0, "probability": 12.0}, # asymmetric but unlikely
|
||||
{"price": 112.0, "rr_ratio": 1.8, "probability": 38.0}, # clears both floors ← primary
|
||||
]
|
||||
primary = _select_primary_target(targets)
|
||||
assert primary is not None
|
||||
assert primary["price"] == 112.0
|
||||
|
||||
|
||||
def test_detects_sentiment_technical_conflict():
|
||||
conflicts = signal_conflict_detector.detect_conflicts(
|
||||
dimension_scores={"technical": 72.0, "momentum": 55.0, "fundamental": 50.0},
|
||||
|
||||
Reference in New Issue
Block a user