1. Bot Identity
| Layer | Governance Governance |
|---|
| Bot class | Governance Service |
|---|
| Authority | Explain |
|---|
| Status | BETA |
|---|
| Readiness | Limited live |
|---|
| Runs before | Nothing — Paper-Trade Runner is a simulation bot in paper mode; it runs concurrently with live bots but never precedes live execution |
|---|
| Runs after | Live signal stream and CLOB order book snapshot are available; strategy under test is configured |
|---|
| Applies to | All 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 mode | limited_live |
|---|
| User-visible | Advanced details only |
|---|
| Developer owner | Polytraders 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.
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|
| min_paper_days | 14 | 7 | 3 | Minimum number of continuous calendar days of paper trading required before a strategy may be considered for promotion to limited live. |
| require_positive_risk_adj | True | None | None | 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. |
| simulated_capital_usd | 10000 | 100000 | 500000 | The 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
| Condition | Action |
|---|
| min_paper_days >= 14 | Standard paper trading period |
| 7 <= min_paper_days < 14 | WARN — reduced paper period; promotion reviewer must sign off |
| min_paper_days < 3 | Reject 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
| Condition | Action |
|---|
| require_positive_risk_adj=true AND sharpe_ratio > 0 | Promotion gate passes on risk-adj P&L dimension |
| require_positive_risk_adj=true AND sharpe_ratio <= 0 | Promotion gate fails; emit PAPER_TRADE_RUNNER_RISK_ADJ_FAIL |
| require_positive_risk_adj=false | Not 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
| Condition | Action |
|---|
| simulated_capital_usd <= 100000 | Standard paper run |
| 100000 < simulated_capital_usd <= 500000 | WARN — large simulated capital; fill assumptions may not hold at live scale |
| simulated_capital_usd > 500000 | Reject — 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
- On startup, validate configuration: min_paper_days >= 3, simulated_capital_usd <= 500000, strategy in registry.
- Assign a paper_run_id (ULID) for this paper trading session; all emitted reports for this session carry paper_run_id and mode=paper.
- Subscribe to the live internal signal bus and CLOB WebSocket feed (ws_market) using the same subscription path as live strategies.
- On each signal event, run the strategy under test in paper mode: evaluate intent, simulate risk guardrail votes (paper mode), shape the intent.
- 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.
- Check KillSwitch; if active, suppress simulated fill emission and log KILL_SWITCH_ACTIVE.
- 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.
- 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.
- 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.
- 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.
- 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
| Code | Severity | Meaning | Action | User-facing message |
|---|
PAPER_TRADE_RUNNER_FILL_SIMULATED | INFO | A 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_PASSED | INFO | The 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_FAILED | WARN | The 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_FAIL | WARN | Sharpe 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_STALE | WARN | The 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_DATA | WARN | The 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_ACTIVE | WARN | KillSwitch 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_BLOCKED | INFO | A simulated intent was blocked by the paper-mode risk guardrail stack. | Log; no fill emitted; paper account unchanged. | |
PARAMETER_CHANGE_REQUIRES_APPROVAL | HARD_REJECT | min_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
| Metric | Type | Unit | Labels | Meaning |
|---|
polytraders_gov_papertraderunner_fills_simulated_total | counter | count | strategy, paper_run_id | Total simulated fills across all paper trading sessions. |
polytraders_gov_papertraderunner_simulated_volume_pusd_total | counter | usd | strategy | Cumulative simulated pUSD volume across all paper fills. |
polytraders_gov_papertraderunner_simulated_pnl_pusd | gauge | usd | strategy, paper_run_id | Cumulative simulated P&L in pUSD for the current paper session. |
polytraders_gov_papertraderunner_sharpe_ratio | gauge | ratio | strategy, paper_run_id | Current risk-adjusted Sharpe ratio for the paper session; used in promotion gate evaluation. |
polytraders_gov_papertraderunner_paper_days_elapsed | gauge | days | strategy, paper_run_id | Calendar days elapsed since paper session start; tracks progress toward min_paper_days gate. |
polytraders_gov_papertraderunner_gate_evaluations_total | counter | count | strategy, result | Total promotion gate evaluations, labelled by pass/fail result. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|
PaperTradeRunnerGateFailed | rate(polytraders_gov_papertraderunner_gate_evaluations_total{result='fail'}[1h]) > 0 | warn | #runbook-papertraderunner-gate-failed |
PaperTradeRunnerFeedStale | polytraders_gov_papertraderunner_fills_simulated_total rate over 30m == 0 during active trading hours | warn | #runbook-papertraderunner-feed-stale |
PaperTradeRunnerNegativeSharpe | polytraders_gov_papertraderunner_sharpe_ratio < 0 | warn | #runbook-papertraderunner-negative-sharpe |
PaperTradeRunnerNoSession | polytraders_gov_papertraderunner_paper_days_elapsed == 0 for > 1h | warn | #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
| Situation | User-facing explanation |
|---|
| Paper trading period active | The 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 passed | The strategy completed its required paper trading period and demonstrated positive risk-adjusted returns. It is eligible for promotion to limited live. |
| Promotion gate failed | The 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 switch | Simulated order activity is paused while the system kill switch is active. The paper trading timer continues. |
18. Failure-Mode Block
| main_failure_mode | The 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_risk | Paper 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_risk | Simulated 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_fallback | If 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_dependencies | Live 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
| Scenario | How to inject | Expected behaviour | Recovery |
|---|
WEBSOCKET_DISCONNECT | Drop the ws_market WebSocket connection for 5 minutes | STALE_DATA warn emitted; fill simulation suspended; paper clock continues; reconnect triggers simulation resume | Automatic on WebSocket reconnect. If > 30 minutes, PAPER_TRADE_RUNNER_FEED_STALE fired and promotion gate clock suspended until recovery. |
NEGATIVE_SHARPE_AT_GATE | Insert paper fills with negative pnl_pusd totalling a Sharpe of -0.5 at the min_paper_days boundary | PAPER_TRADE_RUNNER_GATE_FAILED emitted; promotion_gate_passed=false; PaperTradeRunnerGateFailed alert fired | Continue paper trading or revise strategy parameters. Gate re-evaluated after additional paper days. |
REQUIRE_POSITIVE_RISK_ADJ_BYPASS | Attempt to set require_positive_risk_adj=false in config | PARAMETER_CHANGE_REQUIRES_APPROVAL raised; config rejected; parameter remains locked true | No recovery needed — bypass is rejected. |
MIN_PAPER_DAYS_BELOW_HARD | Submit config with min_paper_days=2 | PARAMETER_CHANGE_REQUIRES_APPROVAL raised; config rejected | Set min_paper_days >= 3 (or ideally >= 14). |
KILL_SWITCH_ACTIVE | Activate KillSwitch during an active paper session | KILL_SWITCH_ACTIVE logged; fill simulation suspended; paper days timer continues; cadence reports continue | Deactivate KillSwitch; fill simulation resumes on next signal event. |
PAPER_FILL_IN_LIVE_REPORT | Set include_paper=true in PnLReporter and observe report content | PNL_REPORTER_PAPER_IN_REGULATORY warn emitted by PnLReporter; paper fills flagged but not included in regulatory SettlementReport | Set 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
| Aspect | Specification |
|---|
| Execution model | single-threaded event loop per paper session; multiple sessions (one per strategy under test) may run concurrently |
| Max in-flight | 10 |
| Idempotency key | paper_run_id + tick_ts_ms |
| Per-call timeout (ms) | 2000 |
| Backpressure strategy | shed signal events beyond queue depth; emit WARN; process backlog on recovery |
| Locking / mutual exclusion | Postgres unique constraint on (paper_run_id, tick_ts_ms); session state is per-goroutine |
22. Dependencies
Depends on (must run first)
| Bot | Why | Contract |
|---|
internal.signal_bus | Paper-Trade Runner subscribes to the live signal bus to receive the same signals as live strategies, ensuring paper results are directly comparable. | |
internal.risk_stack | Risk guardrail votes are simulated through the full stack in paper mode to ensure the paper path is identical to live. | |
Emits to (downstream consumers)
| Bot | Why | Contract |
|---|
internal.governance_audit | | |
Sibling bots (same OrderIntent)
| Bot | Why | Contract |
|---|
| gov.backtester | Backtester uses archived data in replay mode; Paper-Trade Runner uses live data in paper mode. Both emit OperationsReport to polytraders.reports.operations. | |
| gov.pnl-reporter | PnLReporter reads include_paper flag to segregate paper fills from live SettlementReports. Paper-Trade Runner's paper_tagged=true ensures no contamination. | |
External services
| Service | Endpoint | SLA assumed | On 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
| Aspect | Value |
|---|
| CLOB version | v2 |
| Collateral asset | pUSD |
| EIP-712 Exchange domain version | 2 |
| Aware of builderCode field | no |
| Aware of negative-risk markets | yes |
| Multi-chain ready | no |
| SDK used | py-clob-client-v2 |
| Settlement contract | CTFExchangeV2 on Polygon |
| Notes | Paper-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
| Field | Value |
|---|
| spec | 2.0.0 |
| implementation | 2.1.0 |
| schema | 2 |
| released | 2026-04-28 |
Migration history
| Date | From | To | Reason | Action taken |
|---|
| 2026-04-28 | v1 | v2 | CLOB V2 cutover | Updated 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
| Test | Setup | Expected result |
|---|
| min_paper_days below hard limit (3) is rejected | min_paper_days=2, hard=3 | PARAMETER_CHANGE_REQUIRES_APPROVAL ConfigError |
| require_positive_risk_adj=false is rejected | require_positive_risk_adj=false | PARAMETER_CHANGE_REQUIRES_APPROVAL ConfigError; parameter is locked to true |
| simulated_capital_usd above hard limit is rejected | simulated_capital_usd=600000, hard=500000 | PARAMETER_CHANGE_REQUIRES_APPROVAL ConfigError |
| All emitted reports carry paper_tagged=true and mode=paper | Run paper session for 1 minute; inspect all emitted OperationsReport payloads | All 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 signal | simulated_fill output contains size_pusd field; no USDC.e references |
| Promotion gate fails when Sharpe ratio <= 0 | Paper session completes min_paper_days; simulated_sharpe_ratio=-0.2 | PAPER_TRADE_RUNNER_GATE_FAILED emitted; promotion_gate_passed=false |
Integration Tests
| Test | Expected result |
|---|
| 14-day paper session completes and gate passes for a known-good strategy | PAPER_TRADE_RUNNER_GATE_PASSED emitted; promotion_gate_passed=true; all records retained for 1y |
| WebSocket disconnection pauses fill simulation and emits STALE_DATA | STALE_DATA warn emitted; fill simulation suspended; paper clock continues; simulation resumes on reconnect |
| KillSwitch active suppresses fill simulation | KILL_SWITCH_ACTIVE logged; no simulated fills emitted; paper timer continues |
Property Tests
| Property | Required behaviour |
|---|
| All emitted reports carry mode=paper and paper_run_id | Always true — paper mode is locked immutable in default_config |
| No live CLOB orders are submitted during paper trading | Always true — clob_auth and onchain surfaces are never accessed by Paper-Trade Runner |
| Paper fills are never included in live PnLReporter SettlementReports | Always 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
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|
PaperTradeRunnerGateFailed | Review 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 |
PaperTradeRunnerFeedStale | Check Polymarket WebSocket feed (ws_market) status. Verify CLOB public API is reachable. | | | Exec pod lead if WebSocket disconnection is confirmed |
PaperTradeRunnerNegativeSharpe | Review recent paper fills for unusual losses. Check if a strategy parameter change occurred recently. | | | Governance pod lead; strategy owner |
PaperTradeRunnerNoSession | Check 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.
29. Developer Checklist
Ready-to-ship score: 27/27 sections complete · 100%
| Requirement | Status |
|---|
| 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 |