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 LayerGovernance6.5 Paper-Trade Runner

6.5 Paper-Trade Runner

Governance Governance Service Explain BETA Limited live capital · Indirect P7 · Governance & replay pending stub

Paper-Trade Runner mirrors every live signal against a simulated paper account in real time (mode=paper). It subscribes to the live CLOB order book and signal streams, runs the strategy under test through the full execution path, and simulates fills against the current order book snapshot without submitting any real orders. It is the mandatory pre-promotion environment for all new strategies. Paper-Trade Runner emits OperationsReport records (and paper-tagged copies of any report kind it simulates) to polytraders.reports.operations, partitioned by bot_slug+epoch, retained for 1 year. A strategy may only be promoted to limited live when Paper-Trade Runner has accumulated at least min_paper_days of continuous paper trading with require_positive_risk_adj=true passing. Paper-Trade Runner never signs orders, never submits to the CLOB, and never touches on-chain state.

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

LayerGovernance  Governance
Bot classGovernance Service
AuthorityExplain
StatusBETA
ReadinessLimited live
Runs beforeNothing — Paper-Trade Runner is a simulation bot in paper mode; it runs concurrently with live bots but never precedes live execution
Runs afterLive signal stream and CLOB order book snapshot are available; strategy under test is configured
Applies toAll strategies configured for paper mode; any strategy awaiting promotion to live must complete min_paper_days of paper trading with positive risk-adjusted P&L
Default modelimited_live
User-visibleAdvanced details only
Developer ownerPolytraders core — Governance pod

2. Purpose

Paper-Trade Runner mirrors every live signal against a simulated paper account in real time (mode=paper). It subscribes to the live CLOB order book and signal streams, runs the strategy under test through the full execution path, and simulates fills against the current order book snapshot without submitting any real orders. It is the mandatory pre-promotion environment for all new strategies. Paper-Trade Runner emits OperationsReport records (and paper-tagged copies of any report kind it simulates) to polytraders.reports.operations, partitioned by bot_slug+epoch, retained for 1 year. A strategy may only be promoted to limited live when Paper-Trade Runner has accumulated at least min_paper_days of continuous paper trading with require_positive_risk_adj=true passing. Paper-Trade Runner never signs orders, never submits to the CLOB, and never touches on-chain state.

3. Why This Bot Matters

  • Strategy promoted to live without paper trading

    Untested strategies can produce runaway losses on live capital. Paper trading is the mandatory governance gate. Skipping it removes the last safety check before real money is at risk.

  • Paper-Trade Runner uses different signal path than live

    Paper P&L is not predictive of live performance. Governance evidence is invalid. The promotion gate is passed on false grounds.

  • Simulated fills not marked paper-tagged

    Paper fills contaminate live P&L reports. PnLReporter may include hypothetical P&L in regulatory SettlementReports, which is a compliance violation.

  • min_paper_days requirement not enforced

    A strategy may be promoted after insufficient observation. Edge cases and drawdown events that occur only after extended periods are never observed before live promotion.

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
Live CLOB order book snapshots (real-time mid-price, depth, spread)clob_publicYesPrimary signal for simulated fill computation. Paper fills are computed against the current live order book snapshot.
Live market metadata (negRisk flag, condition_id, market_type)clob_publicYesIdentify negative-risk markets for correct simulated payoff valuation.
Live WebSocket market feed (order book updates, trade events)ws_marketYesReal-time signal stream fed into the paper strategy; same feed as live execution.

5. Required Internal Inputs

InputSourceRequired?Use
Live signal stream from intelligence and discovery botsinternalYesPaper-Trade Runner subscribes to the same internal signal bus as live strategies, ensuring paper results reflect live signal quality.
KillSwitch active flagKillSwitchNoWhen KillSwitch is active, suppress all paper-trading simulated order emission. Paper-Trade Runner continues to record signal observations but stops simulating fills.
Risk guardrail stack (paper simulation)internalYesAll risk guardrail votes are simulated in paper mode. Paper-Trade Runner must pass through the same guardrail logic as live strategies.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
min_paper_days1473Minimum number of continuous calendar days of paper trading required before a strategy may be considered for promotion to limited live.
require_positive_risk_adjTrueNoneNoneWhen true (locked), the strategy must demonstrate positive risk-adjusted P&L (Sharpe ratio > 0) over the paper trading window before the promotion gate passes.
simulated_capital_usd10000100000500000The hypothetical pUSD capital allocated to the paper trading account. Governs position sizing in the simulated execution path.

7. Detailed Parameter Instructions

min_paper_days

What it means

Minimum number of continuous calendar days of paper trading required before a strategy may be considered for promotion to limited live.

Default

{ "min_paper_days": 14 }

Why this default matters

14 days covers at least two full calendar weeks of market activity including weekends. Reducing below 7 days risks missing weekly market structure shifts; below 3 days is not permitted.

Threshold logic

ConditionAction
min_paper_days >= 14Standard paper trading period
7 <= min_paper_days < 14WARN — reduced paper period; promotion reviewer must sign off
min_paper_days < 3Reject config change — PARAMETER_CHANGE_REQUIRES_APPROVAL

Developer check

if (p.min_paper_days < p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')

User-facing English

The strategy must complete a minimum paper trading period before going live.

require_positive_risk_adj

What it means

When true (locked), the strategy must demonstrate positive risk-adjusted P&L (Sharpe ratio > 0) over the paper trading window before the promotion gate passes.

Default

{ "require_positive_risk_adj": true }

Why this default matters

A strategy with negative or zero risk-adjusted paper P&L should not be promoted. This parameter is locked to true to prevent governance bypass.

Threshold logic

ConditionAction
require_positive_risk_adj=true AND sharpe_ratio > 0Promotion gate passes on risk-adj P&L dimension
require_positive_risk_adj=true AND sharpe_ratio <= 0Promotion gate fails; emit PAPER_TRADE_RUNNER_RISK_ADJ_FAIL
require_positive_risk_adj=falseNot permitted — parameter is locked to true

Developer check

if (!p.require_positive_risk_adj) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')

User-facing English

The strategy must show positive risk-adjusted returns during the test period before going live.

simulated_capital_usd

What it means

The hypothetical pUSD capital allocated to the paper trading account. Governs position sizing in the simulated execution path.

Default

{ "simulated_capital_usd": 10000 }

Why this default matters

10,000 pUSD simulates a realistic mid-size live account. Sweeping to very large capitals (> 100k) can produce unrealistic fill assumptions due to order book depth limits.

Threshold logic

ConditionAction
simulated_capital_usd <= 100000Standard paper run
100000 < simulated_capital_usd <= 500000WARN — large simulated capital; fill assumptions may not hold at live scale
simulated_capital_usd > 500000Reject — PARAMETER_CHANGE_REQUIRES_APPROVAL

Developer check

if (p.simulated_capital_usd > p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')

User-facing English

The paper trading account uses a fixed simulated balance to test the strategy.

8. Default Configuration

{
  "bot_id": "gov.paper_trade_runner",
  "version": "2.0.0",
  "mode": "paper",
  "defaults": {
    "min_paper_days": 14,
    "require_positive_risk_adj": true,
    "simulated_capital_usd": 10000
  },
  "locked": {
    "mode": {
      "immutable": true,
      "value": "paper"
    },
    "require_positive_risk_adj": {
      "immutable": true
    }
  }
}

9. Implementation Flow

  1. On startup, validate configuration: min_paper_days >= 3, simulated_capital_usd <= 500000, strategy in registry.
  2. Assign a paper_run_id (ULID) for this paper trading session; all emitted reports for this session carry paper_run_id and mode=paper.
  3. Subscribe to the live internal signal bus and CLOB WebSocket feed (ws_market) using the same subscription path as live strategies.
  4. On each signal event, run the strategy under test in paper mode: evaluate intent, simulate risk guardrail votes (paper mode), shape the intent.
  5. If a shaped intent would result in a simulated order: compute simulated fill against the current live order book snapshot (mid-price, depth). Denominate all amounts in pUSD.
  6. Check KillSwitch; if active, suppress simulated fill emission and log KILL_SWITCH_ACTIVE.
  7. Record simulated fill in the paper account ledger (Postgres): size_pusd, simulated_fill_price, simulated_slippage_bps, fee_pusd, pnl_pusd. Mark all records as paper_tagged=true.
  8. Emit paper-tagged OperationsReport per simulated fill to polytraders.reports.operations, partitioned by bot_slug+epoch. Include paper_run_id and mode=paper on every record.
  9. At each reporting cadence (batched-1/min), emit an aggregate OperationsReport summarising: total simulated fills, cumulative pUSD P&L, risk-adjusted P&L (Sharpe), days elapsed, and promotion gate status.
  10. At min_paper_days boundary, evaluate the promotion gate: require_positive_risk_adj check. Emit PAPER_TRADE_RUNNER_GATE_PASSED or PAPER_TRADE_RUNNER_GATE_FAILED.
  11. Retain all paper-tagged OperationsReport records for 1 year in polytraders.reports.operations.

10. Reference Implementation

Subscribes to the live signal bus and CLOB WebSocket feed, runs the strategy under test in paper mode through the full execution and risk guardrail stack, simulates fills against the current order book snapshot, emits paper-tagged OperationsReport per fill and an aggregate report at cadence. Evaluates promotion gate at min_paper_days boundary.

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

// ---- STARTUP ----
FUNCTION init(config):
  IF config.min_paper_days < 3:
    RAISE ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')
  IF NOT config.require_positive_risk_adj:
    RAISE ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')
  IF config.simulated_capital_usd > 500000:
    RAISE ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')
  paper_run_id = generateULID()  // e.g. 'paper_01HX9KZQ7E8VR5'
  strategy = strategyRegistry.load(config.strategy, params=config.defaults)
  paper_account = { balance_pusd: toPusdUnits(config.simulated_capital_usd),
                    positions: {}, fills: [], start_ts: now() }

  // Subscribe to live feeds (same path as live strategies)
  ws_market.subscribe(onBookUpdate)
  internal_bus.subscribe(topics=['signals.*'], handler=onSignal)

// ---- SIGNAL HANDLER ----
FUNCTION onSignal(signal):
  ks = FETCH killswitch.status()
  IF ks.active:
    log.warn('KILL_SWITCH_ACTIVE', { paper_run_id: paper_run_id })
    RETURN

  // Evaluate strategy intent in paper mode (same logic as live)
  intent = strategy.evaluate(signal, paper_account, mode='paper')
  IF intent IS NULL:
    RETURN

  // Simulate risk guardrail stack in paper mode
  votes = riskStack.simulate(intent, context={
    paper_account: paper_account, mode: 'paper'
  })
  shaped_intent = applyVotes(intent, votes)
  IF shaped_intent IS NULL:
    EMIT OperationsReport(event_type='PAPER_TRADE_RUNNER_INTENT_BLOCKED',
                          paper_run_id=paper_run_id, mode='paper', ...)
    RETURN

  // Fetch current live order book snapshot for fill simulation
  book = FETCH clob_public.GET('/book?market_id=' + shaped_intent.market_id)
  IF book.last_updated_ms < now() - 30000:  // > 30s stale
    alerting.emit('STALE_DATA', { market_id: shaped_intent.market_id })
    RETURN  // Never simulate against stale book

  // Simulate fill against live book
  sim_fill = simulateFill(shaped_intent, book)
  sim_fill.size_pusd         = toPusdUnits(sim_fill.size_usd)
  sim_fill.fee_pusd          = sim_fill.size_pusd * sim_fill.fee_bps / 10_000
  sim_fill.pnl_pusd          = computeRealisedPnL(paper_account, sim_fill)
  sim_fill.paper_tagged      = true
  sim_fill.mode              = 'paper'
  sim_fill.paper_run_id      = paper_run_id

  // Check negRisk
  market = FETCH clob_public.getMarketByConditionId(shaped_intent.condition_id)
  IF market.negRisk:
    sim_fill.pnl_pusd = negRiskPayoff(paper_account, sim_fill, market)

  // Persist to paper ledger
  postgres.INSERT('paper_fills', sim_fill)
  paper_account.fills.append(sim_fill)
  paper_account.balance_pusd -= sim_fill.fee_pusd

  // Emit per-fill paper OperationsReport
  EMIT OperationsReport({
    report_id:              'ops_paper-trade-runner_' + paper_run_id + '_' + now(),
    bot_id:                 'gov.paper_trade_runner',
    event_type:             'PAPER_TRADE_RUNNER_FILL_SIMULATED',
    paper_run_id:           paper_run_id,
    mode:                   'paper',
    market_id:              shaped_intent.market_id,
    simulated_fill:         sim_fill,
    topic:                  'polytraders.reports.operations',
    partition:              'paper-trade-runner+' + epochBucket(now())
  })

// ---- CADENCE REPORT (batched-1/min) ----
FUNCTION onCadence():
  days_elapsed = (now() - paper_account.start_ts) / 86400
  sharpe = computeSharpe(paper_account.fills)

  EMIT OperationsReport({
    event_type:              'PAPER_TRADE_RUNNER_CADENCE_REPORT',
    paper_run_id:            paper_run_id,
    mode:                    'paper',
    paper_days_elapsed:      days_elapsed,
    simulated_fills:         len(paper_account.fills),
    simulated_pnl_pusd:      SUM(f.pnl_pusd FOR f IN paper_account.fills),
    simulated_sharpe_ratio:  sharpe,
    promotion_gate_eligible: days_elapsed >= config.min_paper_days
  })

// ---- PROMOTION GATE (at min_paper_days boundary) ----
FUNCTION evaluatePromotionGate():
  sharpe = computeSharpe(paper_account.fills)
  gate_passed = (sharpe > 0)
  IF gate_passed:
    EMIT OperationsReport(event_type='PAPER_TRADE_RUNNER_GATE_PASSED',
                          promotion_gate_passed=true, ...)
  ELSE:
    EMIT OperationsReport(event_type='PAPER_TRADE_RUNNER_GATE_FAILED',
                          promotion_gate_passed=false,
                          reason_code='PAPER_TRADE_RUNNER_RISK_ADJ_FAIL', ...)

SDK calls used

  • clob_public.GET('/book?market_id=...')
  • clob_public.getMarketByConditionId(condition_id)
  • ws_market.subscribe(onBookUpdate)
  • toPusdUnits(raw_usd)
  • postgres.INSERT('paper_fills', sim_fill)
  • alerting.emit('STALE_DATA', metadata)
  • alerting.emit('PAPER_TRADE_RUNNER_GATE_FAILED', metadata)

Complexity: O(S) per signal event where S = signals/s from the live bus; O(F) per cadence report where F = cumulative simulated fill count

11. Wire Examples

Input — what arrives on the wire

{
  "label": "Live CLOB order book snapshot (ws_market update)",
  "source": "ws_market",
  "payload": {
    "market_id": "0x9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c",
    "condition_id": "0x3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f",
    "best_bid": 0.615,
    "best_ask": 0.625,
    "depth_pusd_bid_5pct": 38000,
    "depth_pusd_ask_5pct": 42000,
    "negRisk": false,
    "last_updated_ms": 1746792300000
  }
}

Output — what the bot emits

{
  "label": "OperationsReport — PAPER_TRADE_RUNNER_GATE_PASSED (14-day completion)",
  "payload": {
    "report_id": "ops_paper-trade-runner_paper_01HX9KZQ7E8VR5",
    "bot_id": "gov.paper_trade_runner",
    "event_type": "PAPER_TRADE_RUNNER_GATE_PASSED",
    "paper_run_id": "paper_01HX9KZQ7E8VR5",
    "strategy": "sum_to_one_arb",
    "mode": "paper",
    "paper_days_elapsed": 14,
    "simulated_fills": 312,
    "simulated_volume_pusd": 134200.0,
    "simulated_pnl_pusd": 1890.5,
    "simulated_sharpe_ratio": 1.42,
    "simulated_net_fees_pusd": 335.5,
    "promotion_gate_passed": true,
    "paper_tagged": true,
    "report_kind": "OperationsReport",
    "topic": "polytraders.reports.operations",
    "partition": "paper-trade-runner+2026-05-09T00:00Z",
    "retained_until": "2027-05-09"
  }
}

12. Decision Logic

APPROVE

Not applicable — Paper-Trade Runner is a simulation bot in paper mode. It never approves live orders.

RESHAPE_REQUIRED

Not applicable as a live trading decision. Risk guardrail votes are simulated; any reshape is applied to the simulated intent only.

REJECT

Not applicable as a live trading decision. Paper-Trade Runner will emit PAPER_TRADE_RUNNER_GATE_FAILED if promotion gate criteria are not met.

WARNING_ONLY

min_paper_days below 14 but above 3 emits WARN. simulated_capital_usd above 100k emits WARN. KillSwitch active suppresses fill simulation with WARN.

13. Standard Decision Output

This bot returns a OperationsReport object. See OperationsReport schema.

{
  "report_id": "ops_paper-trade-runner_paper_01HX9KZQ7E8VR5",
  "bot_id": "gov.paper_trade_runner",
  "event_type": "PAPER_TRADE_RUNNER_GATE_PASSED",
  "paper_run_id": "paper_01HX9KZQ7E8VR5",
  "strategy": "sum_to_one_arb",
  "mode": "paper",
  "paper_days_elapsed": 14,
  "simulated_fills": 312,
  "simulated_volume_pusd": 134200.0,
  "simulated_pnl_pusd": 1890.5,
  "simulated_sharpe_ratio": 1.42,
  "simulated_net_fees_pusd": 335.5,
  "promotion_gate_passed": true,
  "report_kind": "OperationsReport",
  "topic": "polytraders.reports.operations",
  "partition": "paper-trade-runner+2026-05-09T00:00Z",
  "retained_until": "2027-05-09"
}

14. Reason Codes

CodeSeverityMeaningActionUser-facing message
PAPER_TRADE_RUNNER_FILL_SIMULATEDINFOA simulated fill was computed against the live order book snapshot and logged to the paper account ledger.No action — routine paper trading event.A simulated trade was recorded in your paper trading account.
PAPER_TRADE_RUNNER_GATE_PASSEDINFOThe strategy has completed min_paper_days of continuous paper trading with positive risk-adjusted P&L. It is eligible for promotion to limited live.No action — governance pod to review promotion evidence.The strategy completed its paper trading period and is eligible for live promotion.
PAPER_TRADE_RUNNER_GATE_FAILEDWARNThe strategy completed min_paper_days but did not achieve positive risk-adjusted P&L (Sharpe ratio <= 0).Emit alert; do not promote strategy; governance pod to review.The strategy did not meet the required performance threshold during paper trading.
PAPER_TRADE_RUNNER_RISK_ADJ_FAILWARNSharpe ratio at promotion gate evaluation is <= 0; require_positive_risk_adj gate failed.Block promotion gate; emit WARN; continue paper trading or revise strategy.Risk-adjusted returns were insufficient during the paper trading period.
PAPER_TRADE_RUNNER_FEED_STALEWARNThe live CLOB WebSocket feed has been disconnected for more than 30 minutes; fill simulation paused and promotion gate clock suspended.Pause fill simulation; suspend promotion gate clock; emit alert; resume on feed recovery.
STALE_DATAWARNThe order book snapshot used for fill simulation is stale (last updated > 30s ago). Fill simulation skipped for this signal.Skip fill simulation for this tick; emit WARN; retry on next fresh snapshot.
KILL_SWITCH_ACTIVEWARNKillSwitch is active; simulated fill emission is suppressed. Paper trading timer continues.Suppress simulated fills; emit WARN; paper timer continues.Simulated trading activity is paused while the system kill switch is active.
PAPER_TRADE_RUNNER_INTENT_BLOCKEDINFOA simulated intent was blocked by the paper-mode risk guardrail stack.Log; no fill emitted; paper account unchanged.
PARAMETER_CHANGE_REQUIRES_APPROVALHARD_REJECTmin_paper_days below hard limit (3), require_positive_risk_adj set to false, or simulated_capital_usd above hard limit (500000) attempted.Reject config change; emit alert.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_gov_papertraderunner_fills_simulated_totalcountercountstrategy, paper_run_idTotal simulated fills across all paper trading sessions.
polytraders_gov_papertraderunner_simulated_volume_pusd_totalcounterusdstrategyCumulative simulated pUSD volume across all paper fills.
polytraders_gov_papertraderunner_simulated_pnl_pusdgaugeusdstrategy, paper_run_idCumulative simulated P&L in pUSD for the current paper session.
polytraders_gov_papertraderunner_sharpe_ratiogaugeratiostrategy, paper_run_idCurrent risk-adjusted Sharpe ratio for the paper session; used in promotion gate evaluation.
polytraders_gov_papertraderunner_paper_days_elapsedgaugedaysstrategy, paper_run_idCalendar days elapsed since paper session start; tracks progress toward min_paper_days gate.
polytraders_gov_papertraderunner_gate_evaluations_totalcountercountstrategy, resultTotal promotion gate evaluations, labelled by pass/fail result.

Alerts

AlertConditionSeverityRunbook
PaperTradeRunnerGateFailedrate(polytraders_gov_papertraderunner_gate_evaluations_total{result='fail'}[1h]) > 0warn#runbook-papertraderunner-gate-failed
PaperTradeRunnerFeedStalepolytraders_gov_papertraderunner_fills_simulated_total rate over 30m == 0 during active trading hourswarn#runbook-papertraderunner-feed-stale
PaperTradeRunnerNegativeSharpepolytraders_gov_papertraderunner_sharpe_ratio < 0warn#runbook-papertraderunner-negative-sharpe
PaperTradeRunnerNoSessionpolytraders_gov_papertraderunner_paper_days_elapsed == 0 for > 1hwarn#runbook-papertraderunner-no-session

Dashboards

  • Grafana — Governance / Paper-Trade Runner session dashboard (P&L, Sharpe, days elapsed)
  • Grafana — Governance / Promotion gate history and pass rate by strategy

16. Developer Reporting

{
  "bot_id": "gov.paper_trade_runner",
  "event_type": "PAPER_TRADE_RUNNER_FILL_SIMULATED",
  "paper_run_id": "paper_01HX9KZQ7E8VR5",
  "market_id": "0x9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c",
  "simulated_fill_price": 0.618,
  "simulated_fill_size_pusd": 640.0,
  "simulated_slippage_bps": 1.2,
  "fee_pusd": 16.0,
  "pnl_pusd": 0.0,
  "paper_tagged": true,
  "mode": "paper",
  "tick_ts_ms": 1746792300000
}

17. Plain-English Reporting

SituationUser-facing explanation
Paper trading period activeThe strategy is running in a simulated environment using live market data. No real trades are being placed. Results will be used to evaluate readiness for live deployment.
Promotion gate passedThe strategy completed its required paper trading period and demonstrated positive risk-adjusted returns. It is eligible for promotion to limited live.
Promotion gate failedThe strategy did not meet the required performance criteria during its paper trading period. It must continue paper trading or be revised before live promotion.
Fill simulation suppressed by kill switchSimulated order activity is paused while the system kill switch is active. The paper trading timer continues.

18. Failure-Mode Block

main_failure_modeThe live CLOB WebSocket feed (ws_market) disconnects, causing the paper strategy to run without fresh order book data. Simulated fills may be computed against stale snapshots, inflating apparent P&L.
false_positive_riskPaper P&L appears positive due to a period of anomalously favorable market microstructure that is unlikely to persist in live trading, leading to a premature promotion gate pass.
false_negative_riskSimulated fills use the current mid-price without modelling market impact. A large strategy on thin markets may show strong paper P&L but poor live performance due to slippage not captured in paper mode.
safe_fallbackIf the WebSocket feed is disconnected, suspend fill simulation, emit STALE_DATA warn, and continue the paper trading timer. Do not extrapolate or estimate fills from stale data. If disconnection exceeds 30 minutes, emit PAPER_TRADE_RUNNER_FEED_STALE and pause the promotion gate clock until the feed recovers.
required_dependenciesLive CLOB WebSocket feed (ws_market), CLOB public API (mid-prices, market metadata), Internal signal bus (strategy signals), Risk guardrail stack (paper simulation), Postgres paper account ledger

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
WEBSOCKET_DISCONNECTDrop the ws_market WebSocket connection for 5 minutesSTALE_DATA warn emitted; fill simulation suspended; paper clock continues; reconnect triggers simulation resumeAutomatic on WebSocket reconnect. If > 30 minutes, PAPER_TRADE_RUNNER_FEED_STALE fired and promotion gate clock suspended until recovery.
NEGATIVE_SHARPE_AT_GATEInsert paper fills with negative pnl_pusd totalling a Sharpe of -0.5 at the min_paper_days boundaryPAPER_TRADE_RUNNER_GATE_FAILED emitted; promotion_gate_passed=false; PaperTradeRunnerGateFailed alert firedContinue paper trading or revise strategy parameters. Gate re-evaluated after additional paper days.
REQUIRE_POSITIVE_RISK_ADJ_BYPASSAttempt to set require_positive_risk_adj=false in configPARAMETER_CHANGE_REQUIRES_APPROVAL raised; config rejected; parameter remains locked trueNo recovery needed — bypass is rejected.
MIN_PAPER_DAYS_BELOW_HARDSubmit config with min_paper_days=2PARAMETER_CHANGE_REQUIRES_APPROVAL raised; config rejectedSet min_paper_days >= 3 (or ideally >= 14).
KILL_SWITCH_ACTIVEActivate KillSwitch during an active paper sessionKILL_SWITCH_ACTIVE logged; fill simulation suspended; paper days timer continues; cadence reports continueDeactivate KillSwitch; fill simulation resumes on next signal event.
PAPER_FILL_IN_LIVE_REPORTSet include_paper=true in PnLReporter and observe report contentPNL_REPORTER_PAPER_IN_REGULATORY warn emitted by PnLReporter; paper fills flagged but not included in regulatory SettlementReportSet include_paper=false; paper fills remain segregated.

20. State & Persistence

Cold-start recovery

On restart, the paper_session record is reloaded from Postgres and the paper trading session resumes. In-flight signal processing is restarted from the next WebSocket event; no missed signals are retroactively replayed.

21. Concurrency & Idempotency

AspectSpecification
Execution modelsingle-threaded event loop per paper session; multiple sessions (one per strategy under test) may run concurrently
Max in-flight10
Idempotency keypaper_run_id + tick_ts_ms
Per-call timeout (ms)2000
Backpressure strategyshed signal events beyond queue depth; emit WARN; process backlog on recovery
Locking / mutual exclusionPostgres unique constraint on (paper_run_id, tick_ts_ms); session state is per-goroutine

22. Dependencies

Depends on (must run first)

BotWhyContract
internal.signal_busPaper-Trade Runner subscribes to the live signal bus to receive the same signals as live strategies, ensuring paper results are directly comparable.
internal.risk_stackRisk guardrail votes are simulated through the full stack in paper mode to ensure the paper path is identical to live.

Emits to (downstream consumers)

BotWhyContract
internal.governance_audit

Sibling bots (same OrderIntent)

BotWhyContract
gov.backtesterBacktester uses archived data in replay mode; Paper-Trade Runner uses live data in paper mode. Both emit OperationsReport to polytraders.reports.operations.
gov.pnl-reporterPnLReporter reads include_paper flag to segregate paper fills from live SettlementReports. Paper-Trade Runner's paper_tagged=true ensures no contamination.

External services

ServiceEndpointSLA assumedOn failure
Polymarket CLOB v2 (order book, market metadata)99.95% / 200ms p99 (Polymarket-published)
Polymarket WebSocket feed (ws_market)99.9% (Polymarket-published)

23. Security Surfaces

Abuse vectors considered

  • Setting require_positive_risk_adj=false to bypass the promotion gate risk-adjustment check
  • Reducing min_paper_days below the hard limit to fast-track a strategy to live promotion
  • Mixing paper_tagged=true fills with live fills to inflate live P&L reports

Mitigations

  • require_positive_risk_adj is locked immutable in default_config; any attempt to set false raises PARAMETER_CHANGE_REQUIRES_APPROVAL
  • min_paper_days hard limit of 3 enforced at config load; cannot be overridden without approval
  • All paper fills carry paper_tagged=true; PnLReporter checks this flag before including in live SettlementReports
  • Paper-Trade Runner has no clob_auth or onchain access — it can never submit real orders

24. Polymarket V2 Compatibility

AspectValue
CLOB versionv2
Collateral assetpUSD
EIP-712 Exchange domain version2
Aware of builderCode fieldno
Aware of negative-risk marketsyes
Multi-chain readyno
SDK usedpy-clob-client-v2
Settlement contractCTFExchangeV2 on Polygon
NotesPaper-Trade Runner subscribes to V2 CLOB WebSocket feeds and computes simulated fills against the V2 order book. All simulated amounts are in pUSD. Fee computation uses the V2 model (operator-set at match time). NegRiskAdapter payoff is applied for negative-risk market positions in paper mode.

API surfaces declared

clob_publicws_marketinternal

Networks supported

polygon

25. Versioning & Migration

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

Migration history

DateFromToReasonAction taken
2026-04-28v1v2CLOB V2 cutoverUpdated paper simulation pipeline to denominate all simulated fills in pUSD (removed USDC.e). Switched CLOB public API calls to V2 endpoints (py-clob-client-v2). Paper OperationsReport now includes paper_run_id and mode=paper fields per V2 ReportEnvelope schema. Removed legacy feeRateBps from simulated fill computation; fee now computed as notional * bps / 10000 matching CTFExchangeV2 fee model. NegRiskAdapter payoff path added for negative-risk market paper positions.

26. Acceptance Tests

Unit Tests

TestSetupExpected result
min_paper_days below hard limit (3) is rejectedmin_paper_days=2, hard=3PARAMETER_CHANGE_REQUIRES_APPROVAL ConfigError
require_positive_risk_adj=false is rejectedrequire_positive_risk_adj=falsePARAMETER_CHANGE_REQUIRES_APPROVAL ConfigError; parameter is locked to true
simulated_capital_usd above hard limit is rejectedsimulated_capital_usd=600000, hard=500000PARAMETER_CHANGE_REQUIRES_APPROVAL ConfigError
All emitted reports carry paper_tagged=true and mode=paperRun paper session for 1 minute; inspect all emitted OperationsReport payloadsAll records carry mode='paper', paper_run_id, paper_tagged=true
Simulated fill denominated in pUSD (not USDC.e)Simulate a fill from a live market signalsimulated_fill output contains size_pusd field; no USDC.e references
Promotion gate fails when Sharpe ratio <= 0Paper session completes min_paper_days; simulated_sharpe_ratio=-0.2PAPER_TRADE_RUNNER_GATE_FAILED emitted; promotion_gate_passed=false

Integration Tests

TestExpected result
14-day paper session completes and gate passes for a known-good strategyPAPER_TRADE_RUNNER_GATE_PASSED emitted; promotion_gate_passed=true; all records retained for 1y
WebSocket disconnection pauses fill simulation and emits STALE_DATASTALE_DATA warn emitted; fill simulation suspended; paper clock continues; simulation resumes on reconnect
KillSwitch active suppresses fill simulationKILL_SWITCH_ACTIVE logged; no simulated fills emitted; paper timer continues

Property Tests

PropertyRequired behaviour
All emitted reports carry mode=paper and paper_run_idAlways true — paper mode is locked immutable in default_config
No live CLOB orders are submitted during paper tradingAlways true — clob_auth and onchain surfaces are never accessed by Paper-Trade Runner
Paper fills are never included in live PnLReporter SettlementReportsAlways true — paper_tagged=true segregates records; PnLReporter checks include_paper flag

27. Operational Runbook

Paper-Trade Runner incidents involve feed staleness (fill simulation paused), failed promotion gates (strategy underperformed), or configuration lock violations (bypass attempts). Feed staleness is the most common operational issue during WebSocket instability.

On-call actions

AlertFirst stepDiagnosisMitigationEscalate to
PaperTradeRunnerGateFailedReview the paper session Sharpe ratio and fill history. Check if market conditions during the paper period were anomalously adverse.Governance pod lead; strategy owner for parameter review
PaperTradeRunnerFeedStaleCheck Polymarket WebSocket feed (ws_market) status. Verify CLOB public API is reachable.Exec pod lead if WebSocket disconnection is confirmed
PaperTradeRunnerNegativeSharpeReview recent paper fills for unusual losses. Check if a strategy parameter change occurred recently.Governance pod lead; strategy owner
PaperTradeRunnerNoSessionCheck if any strategies are configured for paper trading. Verify Paper-Trade Runner process is running.Governance pod lead

Manual overrides

  • polytraders gov paper reset-gate --paper-run-id <id> --reviewed-by <operator> — After a gate fails due to a known data quality issue (e.g. stale feed during gate evaluation), reset the gate clock with governance pod sign-off.

Healthcheck

Endpoint: /internal/health/paper-trade-runner | Green: Active paper session running; WebSocket feed connected; last simulated fill < 300s ago during active trading; Postgres paper_fills writable. | Red: No active paper session; WebSocket disconnected for > 30 minutes; Postgres write failure; fill simulation suspended.

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
Unit tests pass for fill simulation, promotion gate evaluation, and parameter lock enforcementCI test run100% pass
Postgres paper_fills schema migration verifiedIntegration testPass

Promote to Limited live

GateHow measuredThreshold
14-day paper session completes for at least one strategy with gate passedPAPER_TRADE_RUNNER_GATE_PASSED event in governance audit logPass
Paper fill contamination test: no paper_tagged=true fills appear in live PnLReporter SettlementReportsAudit of SettlementReport records in Postgres; paper_tagged field check0 contaminated records

Promote to General live

GateHow measuredThreshold
3 separate strategy promotion gates evaluated; results consistent with subsequent limited live performanceGovernance pod retrospective review of paper vs limited-live Sharpe ratiosCorrelation > 0.7 between paper and limited-live Sharpe
WebSocket reconnect handling verified: simulation resumes correctly after 30-minute disconnectFailure injection 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