T5 of the autotrade build adds the engine_config() converter and the PgPersistence<'a> struct — the two bridge points between the Telegram-controlled AutotradeConfig and the internal engine trait layer.

Files Changed

  • crates/polymarket-fetch/src/pmv2/autotrade/orders.rsOrderMode::as_str, OrderMode::from_db, OrderStatus::from_db + 2 new tests
  • crates/polymarket-fetch/src/pmv2/autotrade/persistence.rs — NEW: engine_config() function + PgPersistence<'a> implementing Persistence
  • crates/polymarket-fetch/src/pmv2/autotrade/mod.rspub mod persistence; added

engine_config: BigDecimal/i32 → EngineConfig

engine_config(cfg: &AutotradeConfig) -> EngineConfig converts the database-typed AutotradeConfig (which uses BigDecimal for NUMERIC columns and i32 for INTEGER columns) into the internal EngineConfig (which uses f64/i64/usize).

Key details:

  • Builds CopyConfig from autotrade’s OWN sizing fields (base_size_usd, min_size_usd, max_size_usd, score_threshold, per_event_cap) — NOT from any paper-copy config.
  • category_filter is built by calling parse_topbets_filter on the filter text column, splitting on whitespace.
  • BigDecimal::from_f64(x) requires use bigdecimal::FromPrimitive; in scope. bd.to_f64() requires use bigdecimal::ToPrimitive;. Both traits must be imported together in persistence.rs.

PgPersistence: mode-aware reservation

PgPersistence<'a> holds &'a PgPool and implements the Persistence trait. All methods are async and operate within a caller-supplied transaction.

Dry-run vs live reservation

try_reserve is mode-aware:

  • Dry-run: inserts with terminal status dry_run — excluded from the dedup partial index, excluded from exposure_rows queries, never counted toward exposure caps or daily spend. These rows are permanently inert.
  • Live: inserts with status pending — normal in-flight reservation that the fill/cancel/fail path transitions.

Dedup mechanism

ON CONFLICT (dedup_key)
WHERE status NOT IN ('dry_run','closed','canceled','failed')
DO NOTHING
RETURNING id

Returns None when no row was inserted (conflict → dedup skip). The partial index predicate must match exactly: dry_run is excluded from the partial index, so dry-run rows never block future signals on the same dedup_key.

Partial index alignment

The WHERE status NOT IN (...) clause in the ON CONFLICT must match the partial index definition exactly. If the index excludes a status, that status must appear in the NOT IN list or conflicts will be silently missed/double-inserted.

FOR UPDATE lock on config row

try_reserve acquires a row lock on pmv2_autotrade_config WHERE id = 1 before the INSERT, preventing double-reservation races when the scheduler and bot commands overlap.

Lock order is config-before-order — this is the global lock order documented in pmv2-autotrade-engine-design to prevent ABBA deadlocks.

spend_today_usd

Counts only mode='live' rows from today UTC:

date_trunc('day', now() AT TIME ZONE 'UTC')

Dry-run rows never count toward the daily cap.

OrderMode and OrderStatus mappers

Two small but load-bearing pairs added to orders.rs:

MethodDirectionNote
OrderMode::as_str()Rust → DB columnUsed when inserting/updating
OrderMode::from_db(s: &str)DB column → RustUsed when reading rows
OrderStatus::from_db(s: &str)DB column → RustUsed when reading rows

from_db methods return Result<Self> and error on unknown strings, keeping deserialization explicit and failing loudly rather than silently defaulting.

Transaction executor pattern

&mut *tx dereference syntax works for sqlx 0.7+ — the same pattern used in crates/polymarket-fetch/src/repo.rs (line 392). Methods accept &mut PgTransaction<'_> and pass &mut *tx to sqlx query executors.

Test counts after T5

  • polymarket-fetch main binary tests: 249 passed
  • polymarket-data tests: 87 passed
  • Gate exit: 0 (PASS)

BigDecimal import pattern (reference)

Both import traits must appear together wherever conversions cross the f64 ↔ BigDecimal boundary:

use bigdecimal::FromPrimitive;
use bigdecimal::ToPrimitive;

FromPrimitive provides BigDecimal::from_f64(x). ToPrimitive provides bd.to_f64(). Missing either silently removes the method from scope and produces a confusing “method not found” error.

TDD sequence

Tests were written first (verified failing), implementation added, tests verified passing, then cargo fmt + gate run. This is the standard T5 sequence matching prior autotrade tasks.