Daily-entry cap: enforce on the auto-approval path (the one live entries use)

The per-day fresh-entry cap (TRADING_MAX_FRESH_ENTRIES_PER_DAY) was only
checked in the fast submit_breakout_now path. But live entries flow through
the SLOW auto-approval path (process_auto_approvals -> approve_order), which
had no cap — so the limit was silently dead. Observed 2026-06-23: 44 entries
taken against a configured cap of 5 (all approved_by="auto", zero via the
fast orb_trigger path).

Enforce the cap in approve_order for approved_by="auto", sharing the same
self._fresh_entries counter the fast path uses (so the cap is global across
both paths). Capped orders are REJECTED (reason=daily_entry_cap) so they
leave the pending queue instead of re-skipping every tick. Manual dashboard
approvals are a human override and are never capped.

Two regression tests: auto path binds at the cap + rejects the overflow;
manual approval ignores the cap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Opus 4.8 committed 2026-06-23 10:29:01 -04:00
1 parent 62540bab8c
commit ac1ac50c9e
2 files changed
+56

No files matched your search

+35
View File
@@ -1,5 +1,6 @@
"""Trading execution service tests: signal -> approval -> order -> exit."""
import json
from datetime import datetime, timedelta
import pytest
@@ -174,6 +175,40 @@ def test_auto_approve_tick_executes(store):
assert executor.executed[0].symbol == "GOOD"
def test_daily_entry_cap_enforced_on_auto_path(store):
# Regression: live entries flow through the AUTO approval path, not the fast
# submit_breakout_now path, so the per-day cap must bind HERE. cap=2 over 3
# distinct signals => exactly 2 execute; the 3rd is rejected (not left pending).
for sym, e, s in [("AAA", 14.0, 13.45), ("BBB", 8.0, 7.60), ("CCC", 5.0, 4.70)]:
emit_signal(store, symbol=sym, entry=e, stop=s)
executor = FakeExecutor()
service = make_service(store, executor, auto_approve=True,
max_fresh_entries_per_day=2,
max_concurrent_positions=10, max_orders_per_tick=10)
service.request_approvals_for_ready_signals()
ids = [q["order_id"] for q in query_approval_queue(store)]
[service.approve_order(oid, approved_by="auto") for oid in ids]
assert len(executor.executed) == 2 # cap binds at 2
assert query_approval_queue(store) == [] # capped order rejected, not left pending
rejects = store.query_events(event_type="order_rejected")
assert len(rejects) == 1 # exactly the 3rd
assert json.loads(rejects[0]["payload_json"])["rejection_reason"] == "daily_entry_cap"
def test_daily_cap_never_blocks_manual_approval(store):
# A human override (approved_by="dashboard") is never capped.
for sym, e, s in [("AAA", 14.0, 13.45), ("BBB", 8.0, 7.60)]:
emit_signal(store, symbol=sym, entry=e, stop=s)
executor = FakeExecutor()
service = make_service(store, executor, max_fresh_entries_per_day=1,
max_concurrent_positions=10, max_orders_per_tick=10)
service.request_approvals_for_ready_signals()
ids = [q["order_id"] for q in query_approval_queue(store)]
for oid in ids:
assert service.approve_order(oid, approved_by="dashboard")["ok"]
assert len(executor.executed) == 2 # cap ignored for manual override
def test_no_duplicate_requests_for_same_symbol(store):
emit_signal(store)
service = make_service(store)
+21
View File
@@ -710,6 +710,20 @@ class TradingExecutionService:
if entry is None:
return {"ok": False, "error": f"order {order_id} not pending"}
request = ExecutionRequest.from_payload(entry["execution_request"])
# Per-day fresh-entry cap. Live entries flow through THIS auto-approval
# path, so the cap must be enforced here (the fast submit_breakout_now
# path shares the same self._fresh_entries counter). Manual dashboard
# approvals are a human override and are never capped. Reject (not skip)
# so a capped order leaves the pending queue instead of re-skipping every
# tick.
if approved_by == "auto" and self.settings.max_fresh_entries_per_day > 0:
today = self._now().date().isoformat()
if self._fresh_entries.get("date") != today:
self._fresh_entries = {"date": today, "n": 0}
if self._fresh_entries["n"] >= self.settings.max_fresh_entries_per_day:
return self.reject_order(
order_id, rejected_by="auto", reason="daily_entry_cap"
)
self.store.emit(
OrderApprovedEvent(
timestamp=datetime.now(),
@@ -741,6 +755,13 @@ class TradingExecutionService:
"armed_at": self._now(),
"checks": 0,
}
if (
approved_by == "auto"
and result.ok
and self.settings.max_fresh_entries_per_day > 0
and isinstance(self._fresh_entries.get("n"), int)
):
self._fresh_entries["n"] += 1 # count toward the per-day cap
return {
"ok": result.ok,
"order_id": order_id,