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):
BigDecimalfields for all NUMERIC DB columns — plain column names in the SELECT (no::float8casts), sinceBigDecimalmaps directly from NUMERIC.i32fields for INTEGER columns.Option<String>for nullable text (category_filter).boolfor thebreaker_trippedflag.
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
| Function | Signature | Notes |
|---|---|---|
update_field_sql(column) | fn(column: &str) -> String | Generates 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
| Params | Valid 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):
| Test | What it verifies |
|---|---|
update_sql_only_uses_whitelisted_column | update_field_sql("base_usd") produces the expected SQL string |
validate_accepts_good_values | base, min_conviction, slippage_bps, max_entry_price with in-range values |
validate_rejects_out_of_range | min_conviction > 1.0, negative values, max_entry_price > 1.0 |
validate_rejects_unknown_param_and_nonnumeric | unknown param name + non-numeric string both error |
mode_round_trips | Mode::from_str_loose("LIVE") and Mode::Dry.as_str() |
For Agents
update_field_sqltakes a raw column name string — the caller is responsible for only passing valid column names (there is no DB-level whitelist). Thevalidate_setfunction provides the canonical validation layer for Telegram command inputs before any DB write.
Key Gotcha
BigDecimalbinds correctly to Postgres NUMERIC without any explicit cast in SQL. Adding::float8in the SELECT would change the sqlx mapping type and breakFromRowdeserialization onBigDecimalfields. Keep the SELECT clean.