entry-gate: extract shared _evaluate_entry_gates for both paths (plan 005 step 2)

One ordered gate sequence (session, confirmation-speed, population, VWAP,
quality, concentrate-by-rank, daily cap [fast-only], dedup/cooldown/reentry,
dilution veto, anti-chase) that BOTH request_approvals_for_ready_signals and
submit_breakout_now call. Gate bodies lifted verbatim from whichever path
owned them; slow-path risk events preserved source-guarded so the fast path
gains no new events. The two documented asymmetries now close:
  - fast path routes through the VWAP gate (was ungated),
  - slow path honours concentrate-by-rank via a rank_map threaded from the
    trigger book into tick()/request_approvals (no-op at concentrate_top_n=0,
    which is the production config — STOP #3 does not apply).

Price-sanity/entry_ref coercion stays path-native (different data sources);
slow-path dedup pre-filter kept first so per-tick re-scans don't re-emit.
16 characterization tests pin both paths; the only decision diffs vs
pre-extraction are the two closures above.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Fable 5 committed 2026-07-13 23:17:56 -04:00
1 parent 20e6efc248
commit d10b5cc921
3 files changed
+328 -201

No files matched your search

+45 -6
View File
@@ -142,15 +142,54 @@ def test_slow_late_confirmation_skips(store):
assert "late_confirmation" in _reasons(store)
def test_slow_ignores_rank_today(store):
# STEP 2 CLOSURE: today the slow path has NO rank gate, so a name that would
# rank outside the top-N still gets an approval. Step 2 threads a rank_map and
# flips this to a rank_concentration skip.
_emit(store, gap_pct=0.25)
created = _make(store, concentrate_top_n=3).request_approvals_for_ready_signals()
def test_slow_rank_gate_closure(store):
# STEP 2 CLOSURE (plan 005): the slow path now honours concentrate-by-rank when
# a rank_map is threaded in from the trigger book. A name ranked outside the
# top-N is rejected — before extraction the slow path had no rank gate and this
# same call created an approval (see git history of this test).
_emit(store, symbol="RANKED", gap_pct=0.25)
svc = _make(store, concentrate_top_n=3)
created = svc.request_approvals_for_ready_signals(rank_map={"RANKED": 5})
assert created == []
def test_slow_rank_within_top_n_still_creates(store):
# A ranked name INSIDE the top-N still gets an approval (rank-1 = full size).
_emit(store, symbol="RANKED", gap_pct=0.25)
svc = _make(store, concentrate_top_n=3)
created = svc.request_approvals_for_ready_signals(rank_map={"RANKED": 1})
assert len(created) == 1
def test_fast_vwap_gate_closure(store):
# STEP 2 CLOSURE (plan 005): the fast path now routes through the shared gate,
# which contains the VWAP check. Demonstrated directly on _evaluate_entry_gates
# with source="fast" (the fast public signature carries no above_vwap, so the
# closure is only reachable when a below-VWAP value is supplied). Before
# extraction the fast path had no VWAP gate at all.
from trading_execution import EntryContext
svc = _make(store, require_above_vwap=True)
ctx = EntryContext(symbol="X", source="fast", entry=14.0, stop=13.45,
trigger=14.0, entry_ref=14.0, gap_pct=0.25, above_vwap=False,
vwap=15.0, confirm_ts=T0)
decision = svc._evaluate_entry_gates(ctx)
assert decision.passed is False
assert decision.reason == "vwap_below"
def test_gate_parity_below_vwap_enforced_both_paths(store):
# With above_vwap supplied, BOTH sources now reach the same vwap_below verdict —
# the asymmetry is closed. (The auto source also emits the shadow/skip event;
# the fast source stays event-free, only the decision unifies.)
from trading_execution import EntryContext
svc = _make(store, require_above_vwap=True)
for src in ("auto", "fast"):
d = svc._evaluate_entry_gates(EntryContext(
symbol="X", source=src, entry=14.0, stop=13.45, trigger=14.0,
entry_ref=14.0, gap_pct=0.25, above_vwap=False, vwap=15.0, confirm_ts=T0))
assert d.reason == "vwap_below", src
# ---- FAST path (submit_breakout_now): decision = skipped string ----
def test_fast_in_population_fires(store):