Enforced the max_copies_per_event cap in the auto-trade engine. The field was wired through DB loading and validation but was never checked at order-placement time — every signal for a given event would pass through regardless of how many copies were already live.
Root Cause
max_copies_per_event was present in AutotradeConfig (the DB model) and mapped in engine_config(), but:
- It was never a field on
EngineConfigorReserveCaps, so the engine had no way to consult it. - Even if it had been, there was no query to count in-flight live orders for the same
event_slugbefore inserting a new reservation.
Changes — branch autotrade-foundation (2026-06-16)
engine.rs
EngineConfig: addedpub max_copies_per_event: usize.ReserveCaps: addedpub max_copies_per_event: usize.caps_from_cfg(): newconst fnthat constructsReserveCapsfromEngineConfig. Extracted to keepingest_entryunder the 100-line Clippy limit.pre_lock_cap_skip(): extracted the pre-lock fast-path cap checks (exposure / spend / count) into a private async helper, also for the 100-line limit.- Test helper
cfg(): updated withmax_copies_per_event: 0(zero = unlimited).
persistence.rs
engine_config(): mapsatc.max_copies_per_event: i32tousizeviamax(0, val) as usize.
Zero-means-unlimited (differs from max_open_positions)
Zero disables the cap (no check is performed). This is intentionally different from
max_open_positionswhere 0 blocks all entries. The divergence guards against a DB zero-default accidentally halting all event copies.
try_reserveLIVE branch: added per-event-slug count check under the existingFOR UPDATEconfig lock, before the INSERT:
SELECT COUNT(*) FROM pmv2_autotrade_orders
WHERE mode = 'live'
AND status NOT IN ('dry_run', 'closed', 'canceled', 'failed')
AND event_slug = $1Rejects (returns Ok(None)) when caps.max_copies_per_event > 0 && event_count >= caps.max_copies_per_event.
- New persistence tests:
engine_config_max_copies_per_event_zero_means_unlimited— asserts that 0 in DB producesmax_copies_per_event: 0(unlimited).engine_config_max_copies_per_event_negative_clamps_to_zero— asserts that a negative DB value clamps to 0 (unlimited, never panics).
No pre-lock fast-path
There is no pre-lock fast-path for the per-event count (unlike the exposure/spend/count checks). The in-tx query under the config FOR UPDATE lock is both sufficient and correct. There is no exposure_rows-style pre-lock source that would carry per-event counts.
Safety semantics
The check runs only in the LIVE reservation branch. Dry-run orders do not count toward the per-event cap (consistent with all other cap logic: only mode='live' rows with non-terminal statuses count).
Status exclusions match the dedup partial index: ('dry_run', 'closed', 'canceled', 'failed').
Gate result
327 tests passed, 0 failed. Clippy + fmt clean. Exit 0.
Deferred
is_resolution_onlyandmin_fill_size_usd: left deferred with existing#[allow(dead_code)]attributes untouched.