Canonical signal type that all autotrade sources emit to trigger trades. Implemented in crates/polymarket-fetch/src/pmv2/autotrade/signal.rs on branch autotrade-foundation.

Superseded shape (2026-06-17)

The SignalAction::{Enter{reference_price},Exit} discriminator documented below was replaced by an Execution enum (Directive{max_price} for weather vs Context{reference_price,conviction,source_score} for cohort) when TradeSignal was generalized for multi-source NATS ingestion. dedup_key() now returns signal_id directly. The cohort signal_id is set equal to the legacy dedup_key(..,"enter",..) so dedup stays byte-identical mid-migration. See pmv2-position-manager-nats-consumer for the current shape.

Location

  • File: crates/polymarket-fetch/src/pmv2/autotrade/signal.rs
  • Module declared in: crates/polymarket-fetch/src/pmv2/autotrade/mod.rs as pub mod signal;

Public Surface

SignalAction enum

VariantFieldsPurpose
Enterreference_price: f64Request to open a position; reference_price is frozen at first detection and feeds the chase guard in the engine
ExitRequest to close an existing position

SignalAction::label() is a const fn returning "enter" or "exit". Clippy use_self lint: match arms use Self::Enter/Self::Exit (not SignalAction::).

TradeSignal struct

FieldTypePurpose
source_id&'static strIdentifies which signal source emitted this (e.g. "first_mover")
actionSignalActionEnter or Exit
condition_idStringCTF condition ID for the market
outcome_indexu8Outcome side (YES=0, NO=1 typically)
outcome_labelStringHuman-readable outcome label
event_slugStringGamma event slug
slugStringMarket slug
origin_refStringOpaque ref to the event/row that triggered this signal (for tracing)
convictionf64bet_confidence value from the scoring layer; feeds position sizing
source_scoreOption<f64>Emitter’s trader score, if available
tierOption<Tier>Tier classification from pmv2_traders; Tier::Core is the first launch cohort
instanceStringUnique instance ID for dedup/idempotency

Tier re-export

pub use crate::pmv2::model::Tier;Tier was already pub in the model with variants Core, Proven, Emerging. No model changes were needed; the re-export makes it convenient to import from signal.rs.

TradeSignal::dedup_key()

Delegates entirely to crate::pmv2::autotrade::orders::dedup_key() using all 6 discriminators. This keeps the single dedup definition in orders.rs as the canonical reference; signal.rs calls it rather than re-implementing it.

Tests

2 unit tests, both passing:

  1. dedup_key_uses_all_discriminators — builds a TradeSignal, mutates each of the 6 discriminators in turn, asserts each mutation changes the dedup key
  2. exit_action_key_differs_from_enter — same signal with Enter vs Exit produces a different key (action is one of the 6 discriminators)

Clippy Compliance

Passes scripts/gate.sh clean (clippy pedantic+nursery with -D warnings). Key fix applied:

Clippy fix applied

fn label() promoted to const fn label() to satisfy the missing_const_for_fn lint. Match arms use Self::Enter/Self::Exit to satisfy the use_self lint — the function was originally written with SignalAction::, which clippy rejects.

Design Notes

For Agents

TradeSignal is the single pluggable seam between any signal source and the engine. The engine’s ingest(&TradeSignal) method is source-agnostic — the same sizing/risk/execution/reconcile machinery handles every source. Adding a new source = implement an emitter that populates and hands over a TradeSignal. The instance field carries the idempotency key all the way to order placement.

reference_price semantics

The Enter variant carries reference_price, which is frozen at the moment of first detection by the signal source. The engine uses this to gate the marketable-limit FAK chase — if the current ask drifts too far above reference_price, the order is skipped (chase guard). This is important: sources must capture the price they observed, not defer to the engine.

dedup_key ownership

The canonical dedup function lives in orders.rs, not signal.rs. If the discriminator set ever changes, change it in orders.rsTradeSignal::dedup_key() will automatically reflect it since it just delegates.

Relationship to Autotrade Plan

This is P2.1 — the canonical signal type. The task sequence on autotrade-foundation:

TaskDescription
Task 1Vendor + pin rs-clob-client-v2 into crates/polymarket-clob/
Task 2authz.rs — owner-only command authorization (see autotrade-authz-module)
P2.1signal.rsTradeSignal + SignalAction types (this note)
Nextengine::ingest(&TradeSignal) implementation; first-mover source emitter