Entry-gate chain for the pmv2 auto-trade engine (Phase P2.4, branch autotrade-foundation). Implemented in crates/polymarket-fetch/src/pmv2/autotrade/gates.rs, registered via pub mod gates; in autotrade/mod.rs.

For Agents

All five gates return GateOutcome. The engine runs them in sequence before calling engine::ingest. Gates are pure functions — zero I/O. See pmv2-autotrade-engine-design for where each gate sits in the overall safety architecture (§ “Economic gates”).

GateOutcome

pub enum GateOutcome {
    Proceed,
    Skip(String),
}

Every gate returns this. Skip carries a human-readable reason for logging.

Gates

1. liveness_gate

pub fn liveness_gate(view: &MarketStatusView, min_secs_to_resolution: i64) -> GateOutcome

Rejects on any of:

  • Market is resolved, closed, or inactive
  • Market is not accepting orders
  • Resolution time is unknown or the epoch placeholder (end_time < 2000)
  • Seconds to resolution is below min_secs_to_resolution
  • Market is resolution-proposed (resolution_proposed set) — reason "market in resolution" (added 2026-06-17, commit 8b3c451; catches a UMA-proposed market that is still closed=false/acceptingOrders=true but whose result is already in — see autotrade-resolution-proposed-entry-guard)

Fail-closed on unknown end time — a market with no resolvable end date is always skipped. Entry-only: the resolution-proposed rejection does NOT make a market “terminal” for exits — a held position must stay sellable.

2. category_gate

pub fn category_gate(filter: Option<&CategoryFilter>, category: Option<&str>) -> GateOutcome

Delegates directly to crate::pmv2::category::passes_filter(&CategoryFilter, Option<&str>) -> bool. When filter is None, returns Proceed unconditionally (no filter configured = allow all). Reuses the same filter logic as /topbets eq:/neq: bucket filtering — no new filter code.

CategoryFilter and passes_filter are at crate::pmv2::category, confirmed by import resolution during implementation.

3. price_bounds_gate

pub fn price_bounds_gate(price: Decimal, min_entry: Decimal, max_entry: Decimal) -> GateOutcome

Skips if price < min_entry or price > max_entry. Enforces the [min_entry_price, max_entry_price] config bounds described in the design (no edge near 0 or 1). Also reused on the live best_ask by the ask-band gate in plan_context (commit 824a22d) so a market that collapsed after the copied trader entered is rejected — the cohort gate otherwise validates only the trader’s frozen reference price. See autotrade-resolution-proposed-entry-guard.

4. ev_floor_gate

#[allow(clippy::cast_precision_loss)]
pub fn ev_floor_gate(size_usd: f64, est_round_trip_fee_usd: f64, min_edge_bps: i64) -> GateOutcome

Computes edge = size_usd × (min_edge_bps as f64) / 10_000.0. Proceeds only if edge > est_round_trip_fee_usd. The #[allow(clippy::cast_precision_loss)] suppresses the i64 as f64 cast lint — acceptable because min_edge_bps will always be a small config value (single digits to low hundreds).

The caller supplies est_round_trip_fee_usd via the est_round_trip_cost_usd helper (see below) so the gate itself stays pure.

5. chase_gate

pub fn chase_gate(reference_price: Decimal, best_ask: Decimal, max_premium_bps: i64) -> GateOutcome

Cap = reference_price × (1 + max_premium_bps / 10_000). Skips if best_ask > cap. reference_price is the price frozen at first detection (carried on SignalAction::Enter { reference_price } in autotrade-signal-type) — this prevents chasing a price that has moved against you since the signal fired.

Helper

pub const ROUND_TRIP_COST_RATE: f64 = 0.02;
 
pub fn est_round_trip_cost_usd(size_usd: f64) -> f64 {
    size_usd * ROUND_TRIP_COST_RATE
}

Round-trip cost approximation (buy fee + sell fee + 2× expected slippage ≈ 2%). Callers pass this into ev_floor_gate. The constant is pub so it can be overridden or inspected in tests.

Test coverage

9/9 unit tests pass. Clippy clean (pedantic + nursery, the cast_precision_loss allow is intentional).

Gate script note

scripts/gate.sh passed clean. The intermediate exit=1 observed during development was rtk proxy intercepting -- -D warnings as a filename (the known rtk mangling issue — see ci). Confirmed by running rtk proxy cargo clippy --workspace --all-targets -- -D warnings directly, which finished clean.