Task 6 of autotrade-foundation adds the first concrete signal source: sources.rs in pmv2/autotrade/, plus the Tier::from_label parser and the tier field on CohortTrader that feeds it.
Files Changed
| File | Change |
|---|---|
crates/polymarket-fetch/src/pmv2/model.rs | Added Tier::from_label(s: &str) -> Option<Tier> |
crates/polymarket-fetch/src/pmv2/positions.rs | Added tier: Option<crate::pmv2::model::Tier> to CohortTrader |
crates/polymarket-fetch/src/pmv2/repo.rs | latest_scored_cohort_with_scores now SELECTs t.tier, maps via from_label |
crates/polymarket-fetch/src/pmv2/autotrade/sources.rs | New file — the first_mover_core signal source |
crates/polymarket-fetch/src/pmv2/autotrade/mod.rs | Added pub mod sources; |
Tier::from_label
Added to crates/polymarket-fetch/src/pmv2/model.rs. Case-insensitive string parser over the Tier enum variants:
"core" -> Some(Tier::Core)
"proven" -> Some(Tier::Proven)
"emerging" -> Some(Tier::Emerging)
_ -> None
This is a pure parsing utility — it does NOT change how tiers are assigned or how classification logic works. Calling sites can treat None as “unclassified” without altering the scoring or ranking behavior.
tier on CohortTrader (positions.rs)
pub tier: Option<crate::pmv2::model::Tier>Added as a new field on the CohortTrader struct. Sources and the engine can inspect this to determine whether a trader is in the copy-eligible cohort (Tier::Core).
latest_scored_cohort_with_scores (repo.rs)
The existing cohort-load query gained a t.tier projection:
- The SQL now SELECTs
t.tier(aTEXTcolumn frompmv2_traders, may beNULL). - The
CohortTraderRowtuple type gainedOption<String>for tier, inserted betweenavg_lost_bet_sizeand rank. - The mapping code calls
Tier::from_label(s)on the raw string and stores the result inCohortTrader::tier. NULLin the DB →Nonein the struct. No NULL handling is needed beyondOption.
For Agents
The column position of
tierin theCohortTraderRowtuple matters for sqlx row mapping. It sits betweenavg_lost_bet_sizeandrank. If the query ever gains another column beforerank, the tuple index must shift accordingly.
sources.rs — the first_mover_core emitter
Path: crates/polymarket-fetch/src/pmv2/autotrade/sources.rs
Constants
pub const SOURCE_ID: &str = "first_mover_core";Stable identifier threaded into every TradeSignal::source_id this emitter produces. Consumed by the engine’s dedup key, order rows, and Telegram status output.
autotrade_active(mode, enabled) -> bool
pub const fn autotrade_active(mode: AutotradeMode, enabled: bool) -> boolA const fn guard:
- Returns
falseifmode == AutotradeMode::Off. - Returns
falseif!enabled. - Returns
trueotherwise.
The engine calls this before bothering to scan the cohort. Nothing executes in Off mode.
enter_signal(trader, position, conviction) -> TradeSignal
Constructs a TradeSignal with action = SignalAction::Enter { reference_price }.
reference_price MUST be p.avg_price, NOT p.cur_price
reference_priceis frozen at the moment of first detection and is used bychase_gateto reject entries where the current ask has drifted too far above what the signal source observed. Usingcur_price(the live snapshot price) defeats this guard: by the time the engine evaluates the signalcur_pricemight already reflect the drift, so the gate can never fire. Usingavg_price(the trader’s entry price) correctly captures the price at which the informed trader first entered the position.
The signal populates all TradeSignal fields from the provided trader and position:
source_id:SOURCE_ID("first_mover_core")conviction: passed in from the caller (computed frombet_confidence)source_score:Some(trader.score)tier:trader.tierinstance: derived from a stable combination of discriminators
format_dry_run_order(signal, outcome) -> String
Pure formatter for DRY_RUN mode. Produces a human-readable summary of what the engine would execute, without touching the CLOB or the DB. Used in Telegram status replies.
Tests (3 passing)
All three tests live in sources.rs and pass through scripts/gate.sh:
| Test | What it verifies |
|---|---|
active_false_when_off | autotrade_active(Off, true) → false |
active_false_when_disabled | autotrade_active(DryRun, false) → false |
enter_signal_freezes_avg_price | The constructed TradeSignal’s reference_price equals position.avg_price, NOT cur_price |
The third test is the critical correctness guard: it builds a position with avg_price ≠ cur_price, calls enter_signal, and asserts the enter variant’s reference_price matches avg_price. A regression here (accidentally using cur_price) would silently break the chase gate.
Gate Result
253 + 87 + 3 + 2 + 2 + 3 + 2 = 352 tests across the workspace, 0 failures. Clippy clean after fixing a missing semicolon in a test match arm.
Design Notes
For Agents
sources.rsis the first concrete signal emitter in the autotrade system. The pattern for future sources: define aSOURCE_ID, implementautotrade_active(or reuse this one), write anenter_signalfactory, and hand theTradeSignaltoengine::ingest_entry. The engine is fully source-agnostic — all gate/sizing/execution/reconcile logic lives there.
Tier gating is done in the ENGINE, not in sources.rs
enter_signaldoes not filter by tier. The engine consumesTradeSignal::tierand applies theTier::Coregate there.sources.rsis responsible only for constructing an accurate signal; the engine decides whether to act.
Task Sequence on autotrade-foundation
| Task | Description |
|---|---|
| Task 1 | Vendor + pin rs-clob-client-v2 into crates/polymarket-clob/ |
| Task 2 | authz.rs — owner-only command authorization |
| P2.1 | signal.rs — TradeSignal + SignalAction types |
| P2.3 | market.rs — MarketData trait + ResolvedMarket |
| P2.4 | gates.rs — 5 entry gate functions |
| Plan 2 capstone | engine.rs — ingest_entry orchestration |
| Plan 3a T3 | category.rs — on-demand category resolver |
| T4 | market_data.rs — ClobMarketData impl |
| T5 | persistence.rs — PgPersistence + engine_config |
| Task 6 | sources.rs — first_mover_core emitter (this note) |
Related
- autotrade-signal-type —
TradeSignal/SignalActiontypes; the structenter_signalconstructs - autotrade-gates —
chase_gateconsumesreference_price; explains whyavg_priceis correct - autotrade-engine-impl —
ingest_entryis the engine-side consumer ofTradeSignal - autotrade-market-data-trait —
ResolvedMarketprovides thecur_price/ bid / ask at evaluation time - autotrade-persistence-engine-config — T5; how
dry_runorders are stored - pmv2-autotrade-engine-design — full engine design;
first_mover_coreis the first launch source - pmv2-two-tier-trader-scoring —
Tier,bet_confidence,avg_priceall originate here - polymarket-fetch — project overview