Polytraders Dev Guide
internal
v3 spine Phase 1 · Shared contracts 9 demo-wired · 0 shadow-ready · 0 production-live · 100 pending · 109 total 15/33 infra tasks the plan status board
HomeBy LayerStrategy3.9 Sports Model

3.9 Sports Model

Strategy Alpha Strategy Trade BETA Limited live capital · Direct P8 · Additional strategies pending stub

Sports Model computes a quantitative fair-value probability for Polymarket sports markets using an internal power-rating model fed by league APIs (NBA, NFL, EPL, ATP/WTA, MLB), lineup and injury data via SportsFeed-Adapter, and weather data for outdoor events. When the CLOB mid-price diverges from the model price by more than min_edge_bps_vs_model, the bot sizes an IOC OrderIntent using a fractional-Kelly position sizing formula (kelly_fraction) bounded by max_per_bet_usd. The bot targets the Apr 2026 $5M pUSD maker rebate pool for sports+esports markets when the edge direction permits maker posting. This is a user-controlled execution tool that automates quantitative sports market trading decisions. No performance claims are made.

v3 readiness

Docs27/27
donehow scored
Impl0/15
pendinghow scored
Backtest0/4
pendinghow scored
Runtime0/8
pendinghow scored

A bot is done when all four scores are. What does done mean?

1. Bot Identity

LayerStrategy  Strategy
Bot classAlpha Strategy
AuthorityTrade
StatusBETA
ReadinessLimited live
Runs beforeRisk guardrail pipeline
Runs afterSportsFeed-Adapter model price feed + Market scanner
Applies toPolymarket sports markets (NBA, NFL, EPL, ATP/WTA, MLB, esports) where internal power-rating model price diverges from CLOB mid by >= min_edge_bps_vs_model, and lineup/injury/weather data is current
Default modelimited_live
User-visibleAdvanced details only
Developer ownerPolytraders core — Strategy pod

2. Purpose

Sports Model computes a quantitative fair-value probability for Polymarket sports markets using an internal power-rating model fed by league APIs (NBA, NFL, EPL, ATP/WTA, MLB), lineup and injury data via SportsFeed-Adapter, and weather data for outdoor events. When the CLOB mid-price diverges from the model price by more than min_edge_bps_vs_model, the bot sizes an IOC OrderIntent using a fractional-Kelly position sizing formula (kelly_fraction) bounded by max_per_bet_usd. The bot targets the Apr 2026 $5M pUSD maker rebate pool for sports+esports markets when the edge direction permits maker posting. This is a user-controlled execution tool that automates quantitative sports market trading decisions. No performance claims are made.

3. Why This Bot Matters

  • Model price computed with stale lineup or injury data

    A key player's injury changes the model price substantially. Trading on the stale model produces a position in the wrong direction relative to the true updated probability.

  • Kelly fraction set too high

    Fractional Kelly with a high fraction creates large positions that overwhelm the top-of-book depth, causing significant slippage and potentially exceeding the Risk guardrail's position limits.

  • In-play market state not accounted for

    For in-play markets, the CLOB price updates continuously with live game state. Trading on a model price calibrated for pre-game conditions during in-play produces systematically mispriced entries.

  • feeRateBps present on signed order (V1 pattern)

    CTFExchangeV2 rejects orders with feeRateBps. Fees are operator-set at match time. The signed order must not contain this field.

No worked examples on this bot yet. Worked examples are optional but strongly recommended — they turn an abstract failure mode into something a developer can verify in a fixture.

4. Required Polymarket Inputs

InputSourceRequired?Use
Polymarket sports market mid and depthws_market (CLOB WebSocket)YesCompute CLOB mid-price; measure divergence from model price in bps; size order against available depth.
In-play market state (where applicable)ws_sports (Polymarket sports WebSocket)NoDetect live game state changes (score, time remaining, possession) that affect model price validity.
Market open/closed/resolved statusclob_publicYesSkip markets that are closed, resolved, or halted (in-play pause).

5. Required Internal Inputs

InputSourceRequired?Use
KillSwitch active flagKillSwitchYesAbort all intent emission immediately if KillSwitch is active.
Power-rating model price (fair-value probability per market)internal (model engine)YesCompare model_price against CLOB mid; compute edge_bps = |model_price - clob_mid| * 10000.
League APIs (NBA, NFL, EPL, ATP/WTA, MLB)internal (SportsFeed-Adapter)YesCurrent standings, team stats, and match schedules for model calibration.
Lineup / injury / weather data via SportsFeed-Adapterinternal (SportsFeed-Adapter)YesAdjust power-rating model for confirmed lineup changes, injuries, or weather conditions for outdoor events.
Builder code bytes32internal configYesInjected into builder field on every signed V2 OrderIntent. Sports maker rebate pool (Apr 2026: $5M pUSD) eligible.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
min_edge_bps_vs_model20010050Minimum basis-point divergence between CLOB mid-price and internal model price required before emitting an OrderIntent.
kelly_fraction0.10.20.3Fraction of the full Kelly criterion bet size applied to each trade. Kelly criterion determines theoretically optimal bet fraction; this parameter scales it down to control risk.
max_per_bet_usd5007501000Hard cap in pUSD on any single sports market bet, regardless of Kelly-derived size.
drawdown_guard_bps5008001200Maximum basis-point drawdown on the running sports book P&L (mark-to-model) before the bot pauses new entries on all sports markets for the session.

7. Detailed Parameter Instructions

min_edge_bps_vs_model

What it means

Minimum basis-point divergence between CLOB mid-price and internal model price required before emitting an OrderIntent.

Default

{ "min_edge_bps_vs_model": 200 }

Why this default matters

200 bps provides meaningful edge after fee drag (sports markets: 0.75% fee, ~37.5 bps at p=0.5) and expected slippage. Below 100 bps the trade is marginal; below 50 bps the bot will not fire.

Threshold logic

ConditionAction
>= 200 bpsEMIT IOC OrderIntent
100–200 bpsWARN SPORTS_MODEL_EDGE_MARGINAL; emit at 50% kelly size
< 50 bps (hard floor)SKIP — SPORTS_MODEL_NO_EDGE

Developer check

if edge_bps < params.hard: return skip('SPORTS_MODEL_NO_EDGE')

User-facing English

The difference between the model's estimate and the market price was too small to justify a trade after fees.

kelly_fraction

What it means

Fraction of the full Kelly criterion bet size applied to each trade. Kelly criterion determines theoretically optimal bet fraction; this parameter scales it down to control risk.

Default

{ "kelly_fraction": 0.1 }

Why this default matters

0.10 (10% Kelly) provides conservative position sizing that limits variance. Full Kelly (1.0) maximises long-run growth but produces extreme drawdowns; 0.30 hard cap prevents over-aggressive sizing.

Threshold logic

ConditionAction
<= 0.10Conservative Kelly sizing
0.10–0.30WARN SPORTS_MODEL_HIGH_KELLY; elevated per-trade size
> 0.30 (hard cap)Reject config — PARAMETER_CHANGE_REQUIRES_APPROVAL

Developer check

if params.kelly_fraction > params.hard: raise ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')

User-facing English

The trade was sized using a conservative fraction of the model's suggested bet size.

max_per_bet_usd

What it means

Hard cap in pUSD on any single sports market bet, regardless of Kelly-derived size.

Default

{ "max_per_bet_usd": 500 }

Why this default matters

500 pUSD per bet limits single-event exposure and fits within typical Polymarket sports market top-of-book depth without significant slippage.

Threshold logic

ConditionAction
<= 500 pUSDNormal bet sizing
500–1000 pUSDWARN; confirm depth supports order size
> 1000 pUSDReject config — PARAMETER_CHANGE_REQUIRES_APPROVAL

Developer check

if params.max_per_bet_usd > params.hard: raise ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')

User-facing English

The trade size was capped at the configured maximum to limit single-event exposure.

drawdown_guard_bps

What it means

Maximum basis-point drawdown on the running sports book P&L (mark-to-model) before the bot pauses new entries on all sports markets for the session.

Default

{ "drawdown_guard_bps": 500 }

Why this default matters

500 bps (5%) drawdown guard prevents runaway losses from systematic model miscalibration. Above 800 bps the guard may be too late; above 1200 bps the bot will not fire.

Threshold logic

ConditionAction
< 500 bps drawdownNormal trading
500–1200 bpsWARN SPORTS_MODEL_DRAWDOWN_WARNING; reduce kelly_fraction to 50%
> 1200 bps (hard cap)SKIP all new entries — SPORTS_MODEL_DRAWDOWN_GUARD_TRIGGERED

Developer check

if session_drawdown_bps >= params.hard: return skip('SPORTS_MODEL_DRAWDOWN_GUARD_TRIGGERED')

User-facing English

Trading was paused because the session's mark-to-model losses reached the configured limit.

8. Default Configuration

{
  "bot_id": "strat.sports_model",
  "version": "2.1.0",
  "mode": "limited_live",
  "defaults": {
    "min_edge_bps_vs_model": 200,
    "kelly_fraction": 0.1,
    "max_per_bet_usd": 500,
    "drawdown_guard_bps": 500
  },
  "locked": {
    "min_edge_bps_vs_model": {
      "min": 50
    },
    "kelly_fraction": {
      "max": 0.3
    },
    "max_per_bet_usd": {
      "max": 1000
    },
    "drawdown_guard_bps": {
      "max": 1200
    }
  }
}

9. Implementation Flow

  1. Check KillSwitch active flag; if active, emit no OrderIntents.
  2. Subscribe to ws_market book updates and ws_sports live state for all active sports markets.
  3. On each model update or book tick: compute CLOB mid; measure edge_bps = |model_price - clob_mid| * 10000.
  4. Check session drawdown guard: if session_drawdown_bps >= drawdown_guard_bps hard (1200 bps), skip all new entries.
  5. Gate 1 — Edge check: if edge_bps < 50 bps hard, emit sampled DecisionReport SPORTS_MODEL_NO_EDGE; skip.
  6. Gate 2 — Data freshness: confirm SportsFeed-Adapter data is current (lineup_last_updated < 30 min); if stale, emit SPORTS_MODEL_STALE_DATA; skip.
  7. Gate 3 — In-play check: if market is in-play, confirm ws_sports game state is current (< 5s); if stale, skip.
  8. Gate 4 — Market status: confirm market is open and not approaching resolution (< 15 min to close for pre-game; skip in-play if halted).
  9. If edge_bps < warning (200 bps): WARN SPORTS_MODEL_EDGE_MARGINAL; reduce kelly_fraction to 50%.
  10. Compute Kelly size: kelly_size_usd = kelly_fraction * bankroll * edge_bps / (model_price * (1 - model_price) * 10000).
  11. Set orderSize = toPusdUnits(min(kelly_size_usd, max_per_bet_usd, available_depth)) using sizeMultiplier.
  12. Determine direction: buy YES if model_price > clob_mid (market underpricing YES); buy NO otherwise.
  13. Emit OrderIntent: outcome=YES/NO, side=buy, price=best_ask, size_pUSD=orderSize, tif=IOC, builder={code, fee_bps:25}.
  14. Note: fees are operator-set at match time in V2 — feeRateBps is NOT on the signed order. Sports maker rebate pool eligible.
  15. Update session_drawdown tracking in state.
  16. Emit DecisionReport with intent_emitted=true, edge_bps, model_price, clob_mid, kelly_size_usd, reason SPORTS_MODEL_EDGE_TRADE.

10. Reference Implementation

Consumes model price updates from the internal power-rating engine, computes edge against CLOB mid-price, and emits IOC OrderIntents when edge exceeds min_edge_bps_vs_model using fractional-Kelly sizing bounded by max_per_bet_usd.

Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. Translate to TS/Python/Go/Rust.

FUNCTION onModelUpdate(market_id, modelUpdate):
  // --- 0. KillSwitch gate ---
  ks = FETCH internal.killswitch.status
  IF ks.active: RETURN

  // --- 1. Drawdown guard ---
  sessionDrawdown = FETCH state.sessionDrawdownBps()
  IF sessionDrawdown >= params.drawdown_guard_bps_hard:  // 1200 bps
    EMIT DecisionReport(intent_emitted=false, reason='SPORTS_MODEL_DRAWDOWN_GUARD_TRIGGERED')
    RETURN

  // --- 2. Data freshness check ---
  sportsFeed = FETCH internal.sportsfeed.marketData(market_id)
  lineupAge = (now_ms() - sportsFeed.lineup_last_updated_ms) / 60000
  IF lineupAge > 30:
    EMIT DecisionReport(intent_emitted=false, reason='SPORTS_MODEL_STALE_DATA')
    RETURN

  // --- 3. In-play state check ---
  IF market_id.is_inplay:
    gameState = FETCH ws_sports.liveState(market_id)
    IF isStale(gameState, maxAgeS=5): RETURN
    IF gameState.halted: RETURN

  // --- 4. Market status check ---
  mkt = FETCH clob_public.GET('/markets/' + market_id)
  IF mkt.closed OR mkt.resolved OR mkt.minutes_to_close < 15: RETURN

  // --- 5. Edge computation ---
  book = FETCH ws_market.book(market_id)
  clobMid = (book.best_bid + book.best_ask) / 2
  modelPrice = modelUpdate.model_price
  edgeBps = abs(modelPrice - clobMid) * 10000

  // --- 6. Hard floor check ---
  IF edgeBps < params.min_edge_bps_vs_model_hard:  // 50 bps
    IF random() < 0.01:
      EMIT DecisionReport(intent_emitted=false, reason='SPORTS_MODEL_NO_EDGE', edge_bps=edgeBps)
    RETURN

  // --- 7. Kelly sizing ---
  kellyMultiplier = 0.5 IF edgeBps < params.min_edge_bps_vs_model ELSE 1.0
  IF kellyMultiplier < 1.0: WARN('SPORTS_MODEL_EDGE_MARGINAL')
  IF sessionDrawdown > params.drawdown_guard_bps: kellyMultiplier *= 0.5  // progressive reduction
  bankroll = FETCH internal.accountState.bankroll_usd
  kellySizeUSD = params.kelly_fraction * bankroll * edgeBps / (modelPrice * (1 - modelPrice) * 10000)
  depth = FETCH clob_public.depth(market_id)
  orderSize = toPusdUnits(min(kellySizeUSD, params.max_per_bet_usd, depth) * kellyMultiplier)

  // --- 8. Direction ---
  outcome = 'YES' IF modelPrice > clobMid ELSE 'NO'
  price = book.best_ask IF outcome == 'YES' ELSE book.best_ask_NO

  // --- 9. Emit IOC OrderIntent (V2: no feeRateBps) ---
  EMIT OrderIntent(
    market_id = market_id, outcome = outcome, side = 'buy',
    price = price, size_pUSD = orderSize, tif = 'IOC', post_only = false,
    builder = {code: internal.builder_code, fee_bps: 25}
    // sports maker rebate pool eligible when fee_bps applied to resting side
  )

  // --- 10. Update session drawdown tracking ---
  UPDATE state.sessionDrawdownBps(market_id, orderSize, modelPrice, price)

  EMIT DecisionReport(intent_emitted=true, edge_bps=edgeBps,
                      model_price=modelPrice, clob_mid=clobMid,
                      kelly_size_usd=kellySizeUSD, reason='SPORTS_MODEL_EDGE_TRADE')

SDK calls used

  • ws_market.subscribe('book', [market_id])
  • ws_sports.subscribe('live_state', [market_id])
  • fetchClobPublic('/markets/' + market_id)
  • internal.sportsfeed.marketData(market_id)
  • internal.killswitch.status()
  • internal.accountState.bankroll_usd()
  • toPusdUnits(rawFloat)
  • buildOrderTypedData(orderParams, { name: 'CTFExchange', version: '2', chainId: 137 })
  • internal.builder_code

Complexity: O(1) per model update or book tick per market

11. Wire Examples

Input — what arrives on the wire

Model update — NBA market, edge=245 bpsinternal (model engine)

{
  "market_id": "0xsportsmd00000000000000000000000000000000000000000000000000000001",
  "model_price": "0.537",
  "clob_mid": "0.512",
  "edge_bps": "245.0",
  "sport": "NBA",
  "lineup_last_updated_min": "8.2",
  "is_inplay": false,
  "received_at_ms": 1746790800000
}

Output — what the bot emits

OrderIntent — sports model IOC buy YES (builder-attributed)

{
  "intent_id": "oi_01HXSPM0000001A",
  "trace_id": "tr_01HXSPM000TR001",
  "market_id": "0xsportsmd00000000000000000000000000000000000000000000000000000001",
  "outcome": "YES",
  "side": "buy",
  "price": "0.512",
  "size_pUSD": "220.00",
  "tif": "IOC",
  "post_only": false,
  "builder": {
    "code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
    "fee_bps": 25
  },
  "negrisk_aware": false,
  "decision": {
    "edge_bps": 245.0,
    "model_price": 0.537,
    "clob_mid": 0.512,
    "kelly_size_usd": 220.0,
    "sport": "NBA",
    "reasons": [
      "SPORTS_MODEL_EDGE_TRADE"
    ]
  },
  "comment": "fees are operator-set at match time in V2 — feeRateBps is NOT on the signed order"
}

DecisionReport — skipped (no edge), sampled 1/100

{
  "report_id": "dr_01HXSPM999ZZZZ",
  "bot_id": "strat.sports_model",
  "market_id": "0xsportsmd00000000000000000000000000000000000000000000000000000001",
  "intent_emitted": false,
  "edge_bps": 22.0,
  "reasons": [
    "SPORTS_MODEL_NO_EDGE"
  ],
  "sampled": true,
  "evaluated_at_ms": 1746790801000
}

12. Decision Logic

APPROVE

edge_bps >= min_edge_bps_vs_model, data fresh, market open, session drawdown within guard, KillSwitch inactive. Emit IOC OrderIntent.

RESHAPE_REQUIRED

Not applicable — strat bots emit OrderIntents; reshaping is handled by the downstream Risk guardrail pipeline.

REJECT

edge_bps < 50 bps hard floor; stale SportsFeed data; market closed; drawdown guard triggered; KillSwitch active. Emit DecisionReport intent_emitted=false.

WARNING_ONLY

edge_bps between 50 and 200 bps, or session drawdown between 500 and 1200 bps, triggers warning and 50% Kelly size reduction.

13. Standard Decision Output

This bot returns a OrderIntent object. See OrderIntent schema.

{
  "intent_id": "oi_01HXSPM0000001A",
  "trace_id": "tr_01HXSPM000TR001",
  "market_id": "0xsportsmd00000000000000000000000000000000000000000000000000000001",
  "outcome": "YES",
  "side": "buy",
  "price": "0.512",
  "size_pUSD": "220.00",
  "tif": "IOC",
  "post_only": false,
  "builder": {
    "code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
    "fee_bps": 25
  },
  "negrisk_aware": false,
  "decision": {
    "edge_bps": 245.0,
    "model_price": 0.537,
    "clob_mid": 0.512,
    "kelly_size_usd": 220.0,
    "sport": "NBA",
    "reasons": [
      "SPORTS_MODEL_EDGE_TRADE"
    ]
  },
  "comment": "fees are operator-set at match time in V2 \u2014 feeRateBps is NOT on the signed order"
}

14. Reason Codes

CodeSeverityMeaningActionUser-facing message
SPORTS_MODEL_EDGE_TRADEINFOedge_bps >= min_edge_bps_vs_model, data fresh, drawdown guard clear. IOC OrderIntent emitted.Emit IOC OrderIntent; update session drawdown tracking.The model found a pricing difference and placed a trade to capture it.
SPORTS_MODEL_NO_EDGEINFOedge_bps is below the 50 bps hard floor. Model and CLOB prices are sufficiently aligned.Skip; emit sampled DecisionReport.The model's estimate was too close to the market price to justify a trade.
SPORTS_MODEL_EDGE_MARGINALWARNedge_bps between 50 and 200 bps. Trade is marginal; Kelly size reduced by 50%.Emit IOC at 50% Kelly size; log warning.A small pricing difference was detected. A reduced-size trade was placed.
SPORTS_MODEL_STALE_DATAHARD_REJECTSportsFeed-Adapter lineup/injury data is more than 30 minutes old. Model may be miscalibrated.Skip; no OrderIntent emitted.The sports data powering the model was not current. No trade was placed.
SPORTS_MODEL_DRAWDOWN_GUARD_TRIGGEREDHARD_REJECTSession mark-to-model drawdown has exceeded drawdown_guard_bps hard limit (1200 bps). All new entries paused.Skip all markets; emit DecisionReport.Trading was paused because session losses reached the configured limit.
SPORTS_MODEL_DRAWDOWN_WARNINGWARNSession drawdown between 500 and 1200 bps. Kelly fraction reduced to 50%.Continue trading at 50% Kelly size; log warning.Session losses are elevated. Trade sizes were reduced.
SPORTS_MODEL_HIGH_KELLYWARNkelly_fraction config is above the 0.20 warning threshold. Per-trade position size is elevated.Allow but log warning.
STALE_MARKET_DATAHARD_REJECTws_market or ws_sports feed is stale (> 5s).Skip; no OrderIntent emitted.Market data was too old to act on safely.
KILL_SWITCH_ACTIVEHARD_REJECTGlobal kill switch is active.Skip all markets; no OrderIntents emitted.Trading is currently paused.
PARAMETER_CHANGE_REQUIRES_APPROVALHARD_REJECTA config change would push a parameter past its locked hard limit.Reject config change; do not apply.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_strat_sportsmodel_decisions_totalcountercountverdict, reason_code, sportTotal evaluation cycles by intent_emitted, reason code, and sport.
polytraders_strat_sportsmodel_edge_bpshistogrambasis_pointssportDistribution of model-vs-CLOB edge in bps per sport.
polytraders_strat_sportsmodel_kelly_size_usdhistogrampusdsportDistribution of Kelly-derived order sizes per sport.
polytraders_strat_sportsmodel_session_drawdown_bpsgaugebasis_pointsCurrent session mark-to-model drawdown in bps.
polytraders_strat_sportsmodel_intents_emitted_totalcountercountsport, outcomeTotal OrderIntents emitted by sport and outcome (YES/NO).
polytraders_strat_sportsmodel_eval_latency_mshistogrammillisecondsWall-clock time from model update to OrderIntent emit.

Alerts

AlertConditionSeverityRunbook
SportsModelDrawdownWarningpolytraders_strat_sportsmodel_session_drawdown_bps > 500warn#runbook-sportsmodel-drawdown
SportsModelDrawdownGuardTriggeredpolytraders_strat_sportsmodel_session_drawdown_bps > 1200page#runbook-sportsmodel-drawdown-guard
SportsModelStaleFeedrate(polytraders_strat_sportsmodel_decisions_total{reason_code='STALE_MARKET_DATA'}[5m]) > 0.1warn#runbook-sportsmodel-stale-feed
SportsModelKillSwitchBlockingrate(polytraders_strat_sportsmodel_decisions_total{reason_code='KILL_SWITCH_ACTIVE'}[1m]) > 0page#runbook-killswitch

Dashboards

  • Grafana — Strategy / SportsModel edge distribution per sport
  • Grafana — Strategy / SportsModel session drawdown and Kelly sizing

16. Developer Reporting

{
  "bot_id": "strat.sports_model",
  "market_id": "0xsportsmd00000000000000000000000000000000000000000000000000000001",
  "model_price": 0.537,
  "clob_mid": 0.512,
  "edge_bps": 245.0,
  "kelly_fraction": 0.1,
  "kelly_size_usd": 220.0,
  "max_per_bet_usd": 500,
  "sport": "NBA",
  "lineup_last_updated_min": 8.2,
  "session_drawdown_bps": 120.0,
  "intent_emitted": true,
  "reason": "SPORTS_MODEL_EDGE_TRADE",
  "emitted_at_ms": 1746790800000
}

17. Plain-English Reporting

SituationUser-facing explanation
Sports model trade placedThe quantitative model estimated a different probability than the current market price. An order was placed to trade the difference using conservative Kelly sizing.
No edge — no tradeThe model's estimate and the market price were too close after fees to justify a trade. No order was placed.
Stale model data — no tradeThe lineup, injury, or league data powering the model was not current enough. No order was placed until the data is refreshed.
Drawdown guard active — pausedThe session's mark-to-model losses reached the configured limit. New trades are paused for the remainder of the session.

18. Failure-Mode Block

main_failure_modeSystematic model miscalibration: the power-rating model consistently misprices a sport or event type, producing repeated losses across multiple markets until the drawdown guard triggers.
false_positive_riskStale lineup or injury data produces incorrect model prices, triggering trades in the wrong direction shortly before the correct data arrives and the CLOB reprices.
false_negative_riskmin_edge_bps_vs_model set too high misses genuine model edges on liquid sports markets, particularly when the fee rate is low (sports: 0.75%) and the edge is real.
safe_fallbackIf ws_market feed is stale (> 5s) or SportsFeed-Adapter data is unavailable, emit STALE_MARKET_DATA and skip without emitting any OrderIntent. Drawdown guard provides session-level risk control.
required_dependenciesws_market book stream, ws_sports live game state (for in-play markets), clob_public market endpoint (status + depth), Internal power-rating model engine, SportsFeed-Adapter (league APIs, lineup/injury/weather), KillSwitch active flag, internal builder code

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
STALE_SPORTSFEED_DATAFreeze SportsFeed-Adapter updates; let lineup_last_updated age beyond 30 minAutomatic when SportsFeed-Adapter data refreshes.
DRAWDOWN_GUARD_TRIGGERInject sequence of losing trades until session_drawdown_bps reaches 1200Manual session reset required after ops review.
INPLAY_FEED_STALEPause ws_sports for in-play market; let game state age beyond 5sAutomatic when ws_sports reconnects.
NO_EDGE_MARKETSet mock model_price=0.505, clob_mid=0.503 (edge=20 bps < 50 hard)Automatic when model or CLOB price diverges sufficiently.
KILL_SWITCH_ONSet killswitch.active=trueAutomatic on manual KillSwitch reset.

20. State & Persistence

Cold-start recovery

On cold start, session drawdown resets to 0. Market state rebuilt from first ws_market tick and model update.

21. Concurrency & Idempotency

AspectSpecification
Execution modelactor-per-market
Max in-flight40
Idempotency keyintent_id
Per-call timeout (ms)250
Backpressure strategydrop oldest pending model update per market_id when queue depth > 3
Locking / mutual exclusionper-market_id mutex for position state; global mutex for session drawdown update

22. Dependencies

Depends on (must run first)

BotWhyContract
risk.kill_switchChecked first; blocks all intent emission when active.

Emits to (downstream consumers)

External services

ServiceEndpointSLA assumedOn failure
Polymarket CLOB WebSocket (ws_market)best-effort
Polymarket Sports WebSocket (ws_sports)best-effort
SportsFeed-Adapter (league APIs + lineup/injury/weather)internal SLA
Polymarket CLOB public API (depth)99.9%

23. Security Surfaces

On-chain contract calls

ContractMethodNetworkEffect
CTFExchangeV2polygon

Abuse vectors considered

  • Adversary manipulates SportsFeed-Adapter data to produce false model prices, triggering trades in the wrong direction
  • In-play market state spoofing via ws_sports injection to trigger model recalculation
  • Front-running: adversary infers model price by monitoring bot's IOC orders on specific markets

Mitigations

  • SportsFeed-Adapter data is authenticated from official league API sources; injection requires compromising the internal feed
  • ws_sports data validated against clob_public market metadata before use in model
  • IOC orders do not rest on the book; adversary cannot exploit known bid/ask placement
  • Drawdown guard limits total session exposure from systematic model attacks
  • V2 order timestamp(ms) invalidates replayed signed orders

24. Polymarket V2 Compatibility

AspectValue
CLOB versionv2
Collateral assetpUSD
EIP-712 Exchange domain version2
Aware of builderCode fieldyes
Aware of negative-risk marketsno
Multi-chain readyno
SDK usedpy-clob-client-v2
Settlement contractCTFExchangeV2
NotesExecutes IOC taker orders on Polymarket sports markets where model price diverges from CLOB mid by more than min_edge_bps_vs_model. Maker rebate pool (Apr 2026: $5M pUSD sports+esports) incentivises maker orders when feasible. feeRateBps is not present on any signed order.

API surfaces declared

clob_publicclob_authws_marketws_sportsinternal

Networks supported

polygon

25. Versioning & Migration

FieldValue
spec2.0.0
implementation2.1.0
schema2
released2026-04-28

Migration history

DateFromToReasonAction taken
2026-04-28v1 (USDC.e, feeRateBps on signed order, auto_disable_neg_sharpe_weeks)v2 (pUSD, fees operator-set at match time, no performance-claim parameters)CLOB V2 cutover + policy updateSwitched to py-clob-client-v2. Removed feeRateBps from all signed order construction. Updated collateral denomination to pUSD. Injected builder field (bytes32). EIP-712 Exchange domain version updated from '1' to '2'. Removed auto_disable_neg_sharpe_weeks parameter (performance claim; replaced with drawdown_guard_bps). SportsFeed-Adapter updated to V2 internal bus schema. ws_sports WebSocket added for live in-play market state.

26. Acceptance Tests

Unit Tests

TestSetupExpected result
Emit IOC when edge_bps=245, model_price=0.537, clob_mid=0.512, data freshmin_edge_bps_vs_model=200, kelly_fraction=0.10, max_per_bet_usd=500IOC OrderIntent emitted; DecisionReport intent_emitted=true, reason=SPORTS_MODEL_EDGE_TRADE
Skip when edge_bps=30 (< hard floor 50)model_price=0.503, clob_mid=0.500No OrderIntent; sampled DecisionReport reason=SPORTS_MODEL_NO_EDGE
Skip when drawdown guard triggeredsession_drawdown_bps=1300 (> hard 1200)No OrderIntent; reason=SPORTS_MODEL_DRAWDOWN_GUARD_TRIGGERED
Reduce size 50% when edge marginal (120 bps)edge_bps=120, min_edge_bps_vs_model=200OrderIntent emitted at 50% kelly size; WARN SPORTS_MODEL_EDGE_MARGINAL
Skip when SportsFeed data stale (lineup_last_updated > 30 min)lineup_last_updated_min=45No OrderIntent; reason=SPORTS_MODEL_STALE_DATA
Skip when KillSwitch activekillswitch.active=trueNo OrderIntents emitted

Integration Tests

TestExpected result
Full cycle: model update → edge computed → IOC OrderIntent submitted on Polygon testnetOrder has builder.code (bytes32), no feeRateBps, tif=IOC, EIP-712 domain version '2'
Drawdown guard accumulates across multiple trades and triggers at hard limitSPORTS_MODEL_DRAWDOWN_GUARD_TRIGGERED after session_drawdown_bps reaches 1200

Property Tests

PropertyRequired behaviour
Kelly-derived size is always bounded by max_per_bet_usdAlways true
feeRateBps never present on any signed OrderIntentAlways true — V2 fees are operator-set at match time
Bot never trades when SportsFeed-Adapter data is unavailable or stale > 30 minAlways true

27. Operational Runbook

Sports Model incidents are typically drawdown guard triggers (requiring manual session review), stale SportsFeed data (blocking all trades until feed refreshes), or stale in-play feeds. Drawdown guard triggers require ops review before session reset.

On-call actions

AlertFirst stepDiagnosisMitigationEscalate to
SportsModelDrawdownWarning
SportsModelDrawdownGuardTriggered
SportsModelStaleFeed
SportsModelKillSwitchBlocking

Manual overrides

Healthcheck

GET /internal/health/sports-model -> 200 if ws_market feed last_seen < 5s, SportsFeed-Adapter data age < 30 min, session drawdown < 500 bps, KillSwitch inactive.

28. Promotion Gates

A bot does not advance to the next readiness state until every gate below is green. Gates are observable from production data — no subjective sign-off.

Promote to Shadow

GateHow measuredThreshold
All unit tests pass including drawdown guard trigger and stale-data blockCI test run100% pass
feeRateBps absence verified; tif=IOC and kelly sizing verified in integration testIntegration test asserting V2 order schemaPass

Promote to Limited live

GateHow measuredThreshold
p99 eval latency < 250ms over 24hpolytraders_strat_sportsmodel_eval_latency_ms histogramp99 < 250ms
Session drawdown stays below 500 bps over 48h shadow runpolytraders_strat_sportsmodel_session_drawdown_bps gauge< 500 bps

Promote to General live

GateHow measuredThreshold
E2E: model update → edge computed → IOC OrderIntent submitted on Polygon testnetE2E testPass
Drawdown guard verified: session halted at 1200 bps in integration testIntegration testPass

29. Developer Checklist

Ready-to-ship score: 27/27 sections complete · 100%

RequirementStatus
Purpose defined✓ done
Required inputs listed✓ done
Parameters defined✓ done
Defaults defined✓ done
Warning thresholds defined✓ done
Hard thresholds defined✓ done
Safe fallback defined✓ done
Structured output defined✓ done
Developer log defined✓ done
Plain-English explanation✓ done
Unit tests defined✓ done
Integration tests defined✓ done
Property tests defined✓ done
Failure-mode block complete✓ done
Reference implementation pseudocode✓ done
Wire examples (input + output)✓ done
Reason codes listed✓ done
Metrics & logs defined✓ done
State & persistence defined✓ done
Concurrency & idempotency defined✓ done
Dependencies declared✓ done
Security surfaces declared✓ done
Polymarket V2 compatibility declared✓ done
Version & migration history declared✓ done
Operational runbook defined✓ done
Promotion gates defined✓ done
Failure-injection recipes defined✓ done