P2.3 of the autotrade-foundation branch: the MarketData async trait and ResolvedMarket snapshot type, with validated outcome→CLOB-token-id lookup. Provides the engine’s market-resolution seam so sizing and order-building code never works with raw API responses.

What was built

New file: crates/polymarket-fetch/src/pmv2/autotrade/market.rs. Wired in via pub mod market; in autotrade/mod.rs. Added async-trait = "0.1" to crates/polymarket-fetch/Cargo.toml.

Types

MarketStatusView — copy struct (derives Clone, Debug):

fieldtypemeaning
activeboolmarket is active
accepting_ordersboolCLOB is open
resolvedboolmarket has settled
closedboolmarket is closed
secs_to_resolutionOption<i64>seconds until end date (None if unknown)
resolution_proposedboolUMA result proposed but market not yet closed (added 2026-06-17, commit 8b3c451; set fail-open by the adapter — 30s Gamma timeout, warn!, default false; consumed by liveness_gate to block entries — see autotrade-resolution-proposed-entry-guard)

ResolvedMarket — full market snapshot:

fieldtype
condition_idString
outcomesVec<String>
clob_token_idsVec<String>
tick_sizef64
neg_riskbool
statusMarketStatusView
best_bidOption<f64>
best_askOption<f64>

ResolvedMarket::token_for(outcome_index: i32, outcome_label: &str) -> Result<String> The load-bearing validation function:

  1. Checks that clob_token_ids.len() == outcomes.len() (array-length invariant guard)
  2. Checks that outcome_index is ≥ 0 and in range
  3. Checks that outcomes[outcome_index] matches outcome_label (case-sensitive)
  4. Returns clob_token_ids[outcome_index].clone()

MarketData async trait (via #[async_trait]):

#[async_trait]
pub trait MarketData: Send + Sync {
    async fn resolve(
        &self,
        condition_id: &str,
        outcome_index: i32,
    ) -> Result<ResolvedMarket>;
}

Single method returning a fully-hydrated ResolvedMarket. The concrete impl (backed by the vendored CLOB client) will live in the engine layer; this trait is the seam that lets tests inject a mock.

Tests (4 passing)

testwhat it verifies
token_for_validated_outcome_okhappy path — correct index + label returns the right token
token_for_label_mismatch_rejectedwrong label for a valid index → Err
token_for_out_of_range_rejectedindex ≥ vec length → Err
token_for_array_length_mismatch_rejectedclob_token_ids.len() != outcomes.len()Err

Gate gotchas

clippy::unwrap_used fires in test bodies

The gate runs cargo clippy --workspace --all-targets -- -D warnings, which includes test code. unwrap() in test bodies triggers clippy::unwrap_used. Fix: add #[allow(clippy::unwrap_used)] on each test function (or the test module).

cargo fmt --check reorders imports alphabetically

rustfmt sorts use-items alphabetically — {Result, bail} must be written {bail, Result} or fmt fails. It also expands long struct literals onto multiple lines.

async-trait must be in the crate's own Cargo.toml

async-trait = "0.1" was already in the workspace lock (version 0.1.89) but NOT in crates/polymarket-fetch/Cargo.toml. The crate must declare it explicitly or the #[async_trait] macro import fails to resolve.