fix: marketable entries price from live last + time-of-day rvol arms fast path

Day-22 forensic (BIYA session): 6 entry submissions, 0 fills — IREX missed
+1.7R, IRE +1.65R by one cent. Two root causes fixed:

1. Slow-path limit rested AT the trigger while price ran 2% past it, and
   retries reused the same stale price. Marketable model now prices from
   max(trigger, live last) via price_provider; the anti-chase extension
   gate gets the live ref too (was an entry_ref==trigger no-op on this
   path). TRADING_ENTRY_FILL_MODEL flipped to marketable.

2. Fast trigger path armed nothing all day: legacy rvol (cum-so-far /
   full-day MEAN) is ~0.1-0.3 minutes after the open for a normal heavy
   name and its mean is poisoned by reverse-split/prior-squeeze days
   (BIYA legacy 1.27 vs real activity 481x median). New rvol_tod =
   cum / (MEDIAN(20d) * elapsed-session fraction) drives arm eligibility
   and the scanner keep-filter; legacy rvol untouched for validated
   boundaries (gap-squeeze rvol>=5).

Also: stagnation time-stop (TRADING_EXIT_STAGNATION_MIN/_R, default OFF)
in simulate_exit + manage_live with sweep_exits.py variants for
validation before any live enable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Fable 5 committed 2026-07-20 15:40:15 -05:00
1 parent f10a8033a1
commit 5597250ff1
11 files changed
+894 -8

No files matched your search

+11 -1
View File
@@ -56,6 +56,12 @@ class ArmedTrigger:
gap_pct: float = 0.0 # DYNAMIC gap (latest vs prev close) — display/rank only
gap_open_pct: float | None = None # STATIC overnight gap (session open vs prev close)
rvol: float = 0.0
# time-of-day rvol (cum vs median(20d) prorated by elapsed session) — the arm
# eligibility signal. Legacy rvol is full-day-mean based: ~0.1-0.3 minutes
# after the open for a NORMAL heavy name, so rvol_min=2 vs legacy armed almost
# nothing early (07-20: IREX 0.76/IRE 0.58/BIYA 1.27 all stuck "weak" while
# breaking out; tod = 16.8/11.9/481). None -> fall back to legacy rvol.
rvol_tod: float | None = None
trigger: float | None = None # opening-range high (entry breakout level)
stop: float | None = None # opening-range low, cushioned (protective stop)
range_pct: float = 0.0 # (high-low)/high — how wide the opening range is
@@ -86,6 +92,7 @@ class ArmedTrigger:
"rank": self.rank,
"gap": round(self.gap_pct, 2),
"rvol": round(self.rvol, 2),
"rvol_tod": round(self.rvol_tod, 2) if self.rvol_tod is not None else None,
"trigger": round(self.trigger, 4) if self.trigger else None,
"stop": round(self.stop, 4) if self.stop else None,
"price": round(self.price, 4) if self.price else None,
@@ -140,7 +147,8 @@ class ArmedTriggerBook:
and t.stop is not None
and t.stop < t.trigger
and self._gap_ok(t)
and t.rvol >= self.rvol_min
# tod rvol when the scanner provides it; legacy full-day-mean otherwise
and (t.rvol_tod if t.rvol_tod is not None else t.rvol) >= self.rvol_min
and t.range_pct >= self.min_range_pct
and t.dollar_vol >= self.min_dollar_vol
)
@@ -172,6 +180,8 @@ class ArmedTriggerBook:
_go = c.get("gap_open_pct")
t.gap_open_pct = float(_go) if _go is not None else None
t.rvol = float(c.get("rvol") or 0.0)
_rt = c.get("rvol_tod")
t.rvol_tod = float(_rt) if _rt is not None else None
t.trigger = c.get("trigger")
t.stop = c.get("stop")
t.range_pct = float(c.get("range_pct") or 0.0)