Entry-path unification (step 2): liquidity sizing on the auto path

Adds the fast path's volume-based share cap to the live auto path: cap shares to
liquidity_max_volume_pct * cum_volume so a thin cheap name can't be oversized
into heavy slippage (the VTAK-style stop that filled far past its level on
2026-06-23). Threads cum_volume through the setup dict + ready-signal snapshot;
applies max_shares in request_approvals_for_ready_signals, gated by
TRADING_UNIFIED_ENTRY. Fail-open: no cum_volume -> no cap. Protective only
(can only shrink a position).

Tests: thin name capped to liq*cum_volume when on, risk-sized when off. 67 path
tests green. Remaining staged (supervised): the full single _place_entry
chokepoint + fill-model unification + halt-status plumbing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Opus 4.8 committed 2026-06-24 00:08:43 -04:00
1 parent 045692c3a5
commit bb7674a708
4 files changed
+36 -1

No files matched your search

+1
View File
@@ -230,6 +230,7 @@ def query_ready_signals_snapshot(store, session_id: str | None = None) -> list[d
"vwap": signal_data.get("vwap"),
"above_vwap": signal_data.get("above_vwap"),
"day_open": signal_data.get("day_open"),
"cum_volume": signal_data.get("cum_volume"),
"timestamp": event.get("timestamp"),
}
return sorted(
+1
View File
@@ -162,6 +162,7 @@ def evaluate_setup(
if levels.vwap is not None else None),
"above_vwap": bool(above_vwap),
"day_open": round(float(open_price), 4) if open_price > 0 else None,
"cum_volume": float(bars["volume"].sum()),
}
)
+24 -1
View File
@@ -67,7 +67,7 @@ class Clock:
def emit_signal(store, symbol="GOOD", entry=14.0, stop=13.45,
above_vwap=None, vwap=None, day_open=None):
above_vwap=None, vwap=None, day_open=None, cum_volume=None):
signal_data = {
"entry_price": entry,
"stop_loss_price": stop,
@@ -79,6 +79,8 @@ def emit_signal(store, symbol="GOOD", entry=14.0, stop=13.45,
round(entry - 1.0, 2) if above_vwap else round(entry + 1.0, 2))
if day_open is not None:
signal_data["day_open"] = day_open
if cum_volume is not None:
signal_data["cum_volume"] = cum_volume
store.emit(
SignalReadyEvent(
timestamp=T0,
@@ -289,6 +291,27 @@ def test_unified_entry_allows_normal_day(store):
assert len(make_service(store, unified_entry=True).request_approvals_for_ready_signals()) == 1
def test_liquidity_cap_sizes_down_thin_name_on_auto_path(store):
# thin name (cum_volume 10k) -> shares capped to liq*cum_volume, not risk-sized
emit_signal(store, symbol="THIN", entry=2.0, stop=1.9, above_vwap=True,
day_open=1.95, cum_volume=10_000)
svc = make_service(store, unified_entry=True, liquidity_max_volume_pct=0.01,
max_concurrent_positions=10)
svc.request_approvals_for_ready_signals()
q = query_approval_queue(store)[0]["execution_request"]["quantity"]
assert q <= int(0.01 * 10_000) # liquidity-capped at 100 shares
def test_liquidity_cap_off_when_unified_disabled(store):
emit_signal(store, symbol="THIN", entry=2.0, stop=1.9, above_vwap=True,
day_open=1.95, cum_volume=10_000)
svc = make_service(store, unified_entry=False, liquidity_max_volume_pct=0.01,
max_concurrent_positions=10)
svc.request_approvals_for_ready_signals()
q = query_approval_queue(store)[0]["execution_request"]["quantity"]
assert q > int(0.01 * 10_000) # not capped -> risk-based size is larger
def test_no_duplicate_requests_for_same_symbol(store):
emit_signal(store)
service = make_service(store)
+10
View File
@@ -719,6 +719,15 @@ class TradingExecutionService:
# remaining gross-notional budget (so the book can't pile into ~100%).
pos_cap = min(eq * self.settings.max_position_pct,
self._remaining_gross_budget(eq))
# Liquidity cap (unification): on the auto path too, cap shares to a
# fraction of the name's cumulative volume so a thin cheap name can't be
# oversized into heavy slippage (the VTAK-style stop that filled far past
# its level). Fail-open: no cum_volume in the signal -> no cap.
liq = self.settings.liquidity_max_volume_pct
cum_vol = signal.get("cum_volume") or 0.0
max_shares = (int(liq * float(cum_vol))
if (self.settings.unified_entry and liq > 0 and cum_vol > 0)
else None)
sizing = calculate_position_size(
float(entry),
float(stop),
@@ -728,6 +737,7 @@ class TradingExecutionService:
default_equity=self.settings.default_equity,
),
max_position_value=pos_cap,
max_shares=max_shares,
max_risk_dollars=self.settings.max_risk_dollars,
)
if sizing.position_size <= 0: