Runner-propensity selection lever: cheap-price + premarket-gap, shadow-measured

The float audit and the labeler both point at the SAME free, available-today selection
signal the bot underuses: gap magnitude + cheap absolute price separate runners from
faders, while the bot's gap%×RVOL rank leans on RVOL (labeler-proven FLAT) and CAPS gap.

- strategy/evaluation/runner_propensity.py: pure, transparent, FROZEN-threshold primitive
  -> a coarse runner-propensity tier = gap_tier(0-3) + price_tier(0-2). Gap is weighted
  heavier BY CONSTRUCTION because the data says it dominates; premarket gap preferred over
  RTH gap; cheap price is a secondary tie-breaker (the two are correlated). 6 unit tests.
- research/labeler.py runner_rank_report + `runner-rank` CLI: measures the lever on the
  v1 ORB labeled set. SHADOW/measurement only, not wired into live selection.

MEASURED (v1 gappers, gap>=10%, n=187, 19 autocorrelated days -- small, caveated):
the combined tier ladders monotonically: tier1 2% -> tier3 35% -> tier5 (cheap+big-gap)
48% runner (1.63x base). Gap dominates (>=50% -> 48%, 1.64x); cheap<$2 a real secondary
(1.29x). OPERATIONAL KICKER: the live ~35% TRIGGER_GAP_MAX would EXCLUDE 73 gappers whose
runner rate is 44% vs 20% for the kept <=35% -- the cap discards the best runners (this is
how NXTS at +81% got excluded). Caveat: +1R/+2R separate far less than the +20% runner
label, and gap/price are correlated; a hypothesis to shadow-confirm + adversarially
validate before touching the live cap, not a validated rank.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Opus 4.8 committed 2026-06-29 18:08:56 -04:00
1 parent ae0651b769
commit 97e3bf91a6
3 files changed
+189

No files matched your search

+43
View File
@@ -0,0 +1,43 @@
"""Runner-propensity selection primitive: transparent, frozen, monotone tiers."""
from strategy.evaluation.runner_propensity import runner_propensity
def test_big_gap_cheap_scores_highest():
r = runner_propensity(1.50, premarket_gap_pct=0.80) # < $2 and +80% gap
assert r.tier == 5 and r.gap_tier == 3 and r.price_tier == 2
assert r.gap_source == "premarket"
def test_non_gapper_scores_low():
# a $40 name with a 2% gap is not a runner candidate
r = runner_propensity(40.0, gap_pct=0.02)
assert r.tier == 0 and r.gap_tier == 0 and r.price_tier == 0
def test_gap_dominates_price():
# a big-gap rich name outscores a tiny-gap cheap name -> gap is the heavier axis
big_gap_rich = runner_propensity(12.0, premarket_gap_pct=0.60) # gap3 + price0 = 3
small_gap_cheap = runner_propensity(1.0, premarket_gap_pct=0.12) # gap1 + price2 = 3
assert big_gap_rich.gap_tier > small_gap_cheap.gap_tier
# equal total here, but the gap axis spans more -> a >=50% gap alone (3) beats any
# price-only contribution (max 2)
assert runner_propensity(50.0, premarket_gap_pct=0.55).tier >= 3
def test_monotonic_in_gap_and_cheapness():
base = runner_propensity(3.0, premarket_gap_pct=0.20)
assert runner_propensity(3.0, premarket_gap_pct=0.60).tier > base.tier # bigger gap
assert runner_propensity(1.0, premarket_gap_pct=0.20).tier > base.tier # cheaper
def test_premarket_gap_preferred_over_rth():
# when both are present the premarket gap is the one used
r = runner_propensity(3.0, gap_pct=0.05, premarket_gap_pct=0.60)
assert r.gap_source == "premarket" and r.gap_used == 0.60 and r.gap_tier == 3
def test_rth_gap_fallback_and_missing():
assert runner_propensity(3.0, gap_pct=0.30).gap_source == "rth"
assert runner_propensity(3.0).gap_source == "none" # no gap data -> tier from price only
assert runner_propensity(3.0).gap_tier == 0