sqlx config repository layer for the auto-trade engine: load and mutate the pmv2_autotrade_config singleton row and the pmv2_autotrade_sources table via type-safe async functions.

File

crates/polymarket-fetch/src/pmv2/autotrade/config.rs — branch autotrade-foundation

sqlx Pattern in This Crate

This codebase uses runtime sqlx::query_as::<_, T>(...) — NOT compile-time query_as! macros. No live DB connection is needed at build time; the crate builds offline without a DATABASE_URL.

AutotradeConfig Struct

Derived sqlx::FromRow. Mirrors every column of the pmv2_autotrade_config row (id=1):

  • BigDecimal fields for all NUMERIC DB columns — plain column names in the SELECT (no ::float8 casts), since BigDecimal maps directly from NUMERIC.
  • i32 fields for INTEGER columns.
  • Option<String> for nullable text (category_filter).
  • bool for the breaker_tripped flag.

Helper Types

Mode (Off / Dry / Live)

as_str() maps to DB strings "off" / "dry_run" / "live". from_str_loose(s) accepts case-insensitive input including "dryrun" and "dry".

ExitMode (AutoSell / Notify)

as_str() maps to "auto_sell" / "notify".

NUMERIC Binding Pattern

All f64 → NUMERIC bindings go through:

fn numeric(value: f64) -> Result<BigDecimal> {
    BigDecimal::from_f64(value)
        .ok_or_else(|| anyhow::anyhow!("cannot represent {value} as NUMERIC"))
}

This is the same pattern established in repo.rs. The new update_numeric function converts f64 → BigDecimal before binding — never binds f64 directly to a NUMERIC column.

Public API

FunctionSignatureNotes
update_field_sql(column)fn(column: &str) -> StringGenerates UPDATE pmv2_autotrade_config SET {column} = $1, updated_at = now() WHERE id = 1; pure, #[must_use]
load(pool)async fn(&PgPool) -> Result<AutotradeConfig>fetch_one on id=1
update_numeric(pool, column, value)async fn(&PgPool, &'static str, f64) -> Result<()>Converts via numeric() then binds BigDecimal
set_text(pool, column, value)async fn(&PgPool, &'static str, Option<String>) -> Result<()>Handles nullable text fields
set_source_enabled(pool, source_id, enabled)async fn(&PgPool, &str, bool) -> Result<u64>Updates pmv2_autotrade_sources.enabled; returns rows_affected()
validate_set(param, value)fn(&str, &str) -> Result<f64>Parses a &str as f64, enforces domain bounds per param name; rejects unknown params

validate_set Domain Bounds

ParamsValid range
min_conviction, min_entry_price, max_entry_price[0.0, 1.0]
base, min, max, score_threshold, per_event_cap, total_exposure_cap, daily_spend_cap, min_fill_usd, slippage_bps, max_entry_premium_bps, min_round_trip_edge_bps, min_time_to_resolution, max_open, max_copies_per_event>= 0.0

Unknown param names → bail!("unknown param: {param}").

Tests

5 unit tests, all pass (gate exit 0, 243 total tests):

TestWhat it verifies
update_sql_only_uses_whitelisted_columnupdate_field_sql("base_usd") produces the expected SQL string
validate_accepts_good_valuesbase, min_conviction, slippage_bps, max_entry_price with in-range values
validate_rejects_out_of_rangemin_conviction > 1.0, negative values, max_entry_price > 1.0
validate_rejects_unknown_param_and_nonnumericunknown param name + non-numeric string both error
mode_round_tripsMode::from_str_loose("LIVE") and Mode::Dry.as_str()

For Agents

update_field_sql takes a raw column name string — the caller is responsible for only passing valid column names (there is no DB-level whitelist). The validate_set function provides the canonical validation layer for Telegram command inputs before any DB write.

Key Gotcha

BigDecimal binds correctly to Postgres NUMERIC without any explicit cast in SQL. Adding ::float8 in the SELECT would change the sqlx mapping type and break FromRow deserialization on BigDecimal fields. Keep the SELECT clean.