{
  "schema_version": "1.0.0",
  "bot_id": "6.5",
  "bot_name": "Paper-Trade Runner",
  "slug": "paper-trade-runner",
  "layer": "Governance",
  "layer_key": "gov",
  "bot_class": "Governance Service",
  "authority": [
    "Explain"
  ],
  "status": "beta",
  "readiness": "Limited live",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Governance",
    "bot_class": "Governance Service",
    "authority": "Explain",
    "runs_before": "Nothing \u2014 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 \u2014 Governance pod"
  },
  "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.",
  "why_it_matters": [
    {
      "failure": "Strategy promoted to live without paper trading",
      "consequence": "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."
    },
    {
      "failure": "Paper-Trade Runner uses different signal path than live",
      "consequence": "Paper P&L is not predictive of live performance. Governance evidence is invalid. The promotion gate is passed on false grounds."
    },
    {
      "failure": "Simulated fills not marked paper-tagged",
      "consequence": "Paper fills contaminate live P&L reports. PnLReporter may include hypothetical P&L in regulatory SettlementReports, which is a compliance violation."
    },
    {
      "failure": "min_paper_days requirement not enforced",
      "consequence": "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."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Live CLOB order book snapshots (real-time mid-price, depth, spread)",
      "source": "clob_public",
      "required": true,
      "use": "Primary signal for simulated fill computation. Paper fills are computed against the current live order book snapshot."
    },
    {
      "input": "Live market metadata (negRisk flag, condition_id, market_type)",
      "source": "clob_public",
      "required": true,
      "use": "Identify negative-risk markets for correct simulated payoff valuation."
    },
    {
      "input": "Live WebSocket market feed (order book updates, trade events)",
      "source": "ws_market",
      "required": true,
      "use": "Real-time signal stream fed into the paper strategy; same feed as live execution."
    }
  ],
  "internal_inputs": [
    {
      "input": "Live signal stream from intelligence and discovery bots",
      "source": "internal",
      "required": true,
      "use": "Paper-Trade Runner subscribes to the same internal signal bus as live strategies, ensuring paper results reflect live signal quality."
    },
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": false,
      "use": "When KillSwitch is active, suppress all paper-trading simulated order emission. Paper-Trade Runner continues to record signal observations but stops simulating fills."
    },
    {
      "input": "Risk guardrail stack (paper simulation)",
      "source": "internal",
      "required": true,
      "use": "All risk guardrail votes are simulated in paper mode. Paper-Trade Runner must pass through the same guardrail logic as live strategies."
    }
  ],
  "raw_params": [
    "min_paper_days \u00b7 int (default 14)",
    "require_positive_risk_adj \u00b7 bool",
    "simulated_capital_usd \u00b7 int"
  ],
  "parameters": [
    {
      "name": "min_paper_days",
      "default": 14,
      "warning": 7,
      "hard": 3,
      "controls": "Minimum number of continuous calendar days of paper trading required before a strategy may be considered for promotion to limited live.",
      "why_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": "min_paper_days >= 14",
          "action": "Standard paper trading period"
        },
        {
          "condition": "7 <= min_paper_days < 14",
          "action": "WARN \u2014 reduced paper period; promotion reviewer must sign off"
        },
        {
          "condition": "min_paper_days < 3",
          "action": "Reject config change \u2014 PARAMETER_CHANGE_REQUIRES_APPROVAL"
        }
      ],
      "dev_check": "if (p.min_paper_days < p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')",
      "user_facing": "The strategy must complete a minimum paper trading period before going live."
    },
    {
      "name": "require_positive_risk_adj",
      "default": true,
      "warning": null,
      "hard": null,
      "controls": "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.",
      "why_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": "require_positive_risk_adj=true AND sharpe_ratio > 0",
          "action": "Promotion gate passes on risk-adj P&L dimension"
        },
        {
          "condition": "require_positive_risk_adj=true AND sharpe_ratio <= 0",
          "action": "Promotion gate fails; emit PAPER_TRADE_RUNNER_RISK_ADJ_FAIL"
        },
        {
          "condition": "require_positive_risk_adj=false",
          "action": "Not permitted \u2014 parameter is locked to true"
        }
      ],
      "dev_check": "if (!p.require_positive_risk_adj) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')",
      "user_facing": "The strategy must show positive risk-adjusted returns during the test period before going live."
    },
    {
      "name": "simulated_capital_usd",
      "default": 10000,
      "warning": 100000,
      "hard": 500000,
      "controls": "The hypothetical pUSD capital allocated to the paper trading account. Governs position sizing in the simulated execution path.",
      "why_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": "simulated_capital_usd <= 100000",
          "action": "Standard paper run"
        },
        {
          "condition": "100000 < simulated_capital_usd <= 500000",
          "action": "WARN \u2014 large simulated capital; fill assumptions may not hold at live scale"
        },
        {
          "condition": "simulated_capital_usd > 500000",
          "action": "Reject \u2014 PARAMETER_CHANGE_REQUIRES_APPROVAL"
        }
      ],
      "dev_check": "if (p.simulated_capital_usd > p.hard) throw ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')",
      "user_facing": "The paper trading account uses a fixed simulated balance to test the strategy."
    }
  ],
  "default_config": {
    "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
      }
    }
  },
  "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."
  ],
  "decision_logic": {
    "approve": "Not applicable \u2014 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."
  },
  "decision_output_schema": "OperationsReport",
  "decision_output_example": {
    "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"
  },
  "developer_log": {
    "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
  },
  "user_explanations": [
    {
      "situation": "Paper trading period active",
      "message": "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."
    },
    {
      "situation": "Promotion gate passed",
      "message": "The strategy completed its required paper trading period and demonstrated positive risk-adjusted returns. It is eligible for promotion to limited live."
    },
    {
      "situation": "Promotion gate failed",
      "message": "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."
    },
    {
      "situation": "Fill simulation suppressed by kill switch",
      "message": "Simulated order activity is paused while the system kill switch is active. The paper trading timer continues."
    }
  ],
  "failure_modes": {
    "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"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "min_paper_days below hard limit (3) is rejected",
        "setup": "min_paper_days=2, hard=3",
        "expected": "PARAMETER_CHANGE_REQUIRES_APPROVAL ConfigError"
      },
      {
        "test": "require_positive_risk_adj=false is rejected",
        "setup": "require_positive_risk_adj=false",
        "expected": "PARAMETER_CHANGE_REQUIRES_APPROVAL ConfigError; parameter is locked to true"
      },
      {
        "test": "simulated_capital_usd above hard limit is rejected",
        "setup": "simulated_capital_usd=600000, hard=500000",
        "expected": "PARAMETER_CHANGE_REQUIRES_APPROVAL ConfigError"
      },
      {
        "test": "All emitted reports carry paper_tagged=true and mode=paper",
        "setup": "Run paper session for 1 minute; inspect all emitted OperationsReport payloads",
        "expected": "All records carry mode='paper', paper_run_id, paper_tagged=true"
      },
      {
        "test": "Simulated fill denominated in pUSD (not USDC.e)",
        "setup": "Simulate a fill from a live market signal",
        "expected": "simulated_fill output contains size_pusd field; no USDC.e references"
      },
      {
        "test": "Promotion gate fails when Sharpe ratio <= 0",
        "setup": "Paper session completes min_paper_days; simulated_sharpe_ratio=-0.2",
        "expected": "PAPER_TRADE_RUNNER_GATE_FAILED emitted; promotion_gate_passed=false"
      }
    ],
    "integration": [
      {
        "test": "14-day paper session completes and gate passes for a known-good strategy",
        "expected": "PAPER_TRADE_RUNNER_GATE_PASSED emitted; promotion_gate_passed=true; all records retained for 1y"
      },
      {
        "test": "WebSocket disconnection pauses fill simulation and emits STALE_DATA",
        "expected": "STALE_DATA warn emitted; fill simulation suspended; paper clock continues; simulation resumes on reconnect"
      },
      {
        "test": "KillSwitch active suppresses fill simulation",
        "expected": "KILL_SWITCH_ACTIVE logged; no simulated fills emitted; paper timer continues"
      }
    ],
    "property": [
      {
        "property": "All emitted reports carry mode=paper and paper_run_id",
        "required": "Always true \u2014 paper mode is locked immutable in default_config"
      },
      {
        "property": "No live CLOB orders are submitted during paper trading",
        "required": "Always true \u2014 clob_auth and onchain surfaces are never accessed by Paper-Trade Runner"
      },
      {
        "property": "Paper fills are never included in live PnLReporter SettlementReports",
        "required": "Always true \u2014 paper_tagged=true segregates records; PnLReporter checks include_paper flag"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Mirror live signals against paper accounts \u2014 required step before any new strategy goes live.",
  "legacy_pm_signals": [
    "Live signal stream + simulated fills"
  ],
  "legacy_external_feeds": [],
  "reporting_groups": [
    "governance_audit"
  ],
  "network": [
    "polygon"
  ],
  "api_surface": [
    "clob_public",
    "ws_market",
    "internal"
  ],
  "version": {
    "spec": "2.0.0",
    "implementation": "2.1.0",
    "schema": "2",
    "released": "2026-04-28"
  },
  "migration_history": [
    {
      "date": "2026-04-28",
      "from": "v1",
      "to": "v2",
      "reason": "CLOB V2 cutover",
      "action_taken": "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."
    }
  ],
  "polymarket_v2_compat": {
    "clob_version": "v2",
    "collateral": "pUSD",
    "eip712_domain_version": "2",
    "builder_code_aware": false,
    "negrisk_aware": true,
    "multichain_ready": false,
    "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."
  },
  "reference_implementation": {
    "summary": "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.",
    "language_note": "Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. Translate to TS/Python/Go/Rust.",
    "pseudocode": "// ---- STARTUP ----\nFUNCTION init(config):\n  IF config.min_paper_days < 3:\n    RAISE ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')\n  IF NOT config.require_positive_risk_adj:\n    RAISE ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')\n  IF config.simulated_capital_usd > 500000:\n    RAISE ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')\n  paper_run_id = generateULID()  // e.g. 'paper_01HX9KZQ7E8VR5'\n  strategy = strategyRegistry.load(config.strategy, params=config.defaults)\n  paper_account = { balance_pusd: toPusdUnits(config.simulated_capital_usd),\n                    positions: {}, fills: [], start_ts: now() }\n\n  // Subscribe to live feeds (same path as live strategies)\n  ws_market.subscribe(onBookUpdate)\n  internal_bus.subscribe(topics=['signals.*'], handler=onSignal)\n\n// ---- SIGNAL HANDLER ----\nFUNCTION onSignal(signal):\n  ks = FETCH killswitch.status()\n  IF ks.active:\n    log.warn('KILL_SWITCH_ACTIVE', { paper_run_id: paper_run_id })\n    RETURN\n\n  // Evaluate strategy intent in paper mode (same logic as live)\n  intent = strategy.evaluate(signal, paper_account, mode='paper')\n  IF intent IS NULL:\n    RETURN\n\n  // Simulate risk guardrail stack in paper mode\n  votes = riskStack.simulate(intent, context={\n    paper_account: paper_account, mode: 'paper'\n  })\n  shaped_intent = applyVotes(intent, votes)\n  IF shaped_intent IS NULL:\n    EMIT OperationsReport(event_type='PAPER_TRADE_RUNNER_INTENT_BLOCKED',\n                          paper_run_id=paper_run_id, mode='paper', ...)\n    RETURN\n\n  // Fetch current live order book snapshot for fill simulation\n  book = FETCH clob_public.GET('/book?market_id=' + shaped_intent.market_id)\n  IF book.last_updated_ms < now() - 30000:  // > 30s stale\n    alerting.emit('STALE_DATA', { market_id: shaped_intent.market_id })\n    RETURN  // Never simulate against stale book\n\n  // Simulate fill against live book\n  sim_fill = simulateFill(shaped_intent, book)\n  sim_fill.size_pusd         = toPusdUnits(sim_fill.size_usd)\n  sim_fill.fee_pusd          = sim_fill.size_pusd * sim_fill.fee_bps / 10_000\n  sim_fill.pnl_pusd          = computeRealisedPnL(paper_account, sim_fill)\n  sim_fill.paper_tagged      = true\n  sim_fill.mode              = 'paper'\n  sim_fill.paper_run_id      = paper_run_id\n\n  // Check negRisk\n  market = FETCH clob_public.getMarketByConditionId(shaped_intent.condition_id)\n  IF market.negRisk:\n    sim_fill.pnl_pusd = negRiskPayoff(paper_account, sim_fill, market)\n\n  // Persist to paper ledger\n  postgres.INSERT('paper_fills', sim_fill)\n  paper_account.fills.append(sim_fill)\n  paper_account.balance_pusd -= sim_fill.fee_pusd\n\n  // Emit per-fill paper OperationsReport\n  EMIT OperationsReport({\n    report_id:              'ops_paper-trade-runner_' + paper_run_id + '_' + now(),\n    bot_id:                 'gov.paper_trade_runner',\n    event_type:             'PAPER_TRADE_RUNNER_FILL_SIMULATED',\n    paper_run_id:           paper_run_id,\n    mode:                   'paper',\n    market_id:              shaped_intent.market_id,\n    simulated_fill:         sim_fill,\n    topic:                  'polytraders.reports.operations',\n    partition:              'paper-trade-runner+' + epochBucket(now())\n  })\n\n// ---- CADENCE REPORT (batched-1/min) ----\nFUNCTION onCadence():\n  days_elapsed = (now() - paper_account.start_ts) / 86400\n  sharpe = computeSharpe(paper_account.fills)\n\n  EMIT OperationsReport({\n    event_type:              'PAPER_TRADE_RUNNER_CADENCE_REPORT',\n    paper_run_id:            paper_run_id,\n    mode:                    'paper',\n    paper_days_elapsed:      days_elapsed,\n    simulated_fills:         len(paper_account.fills),\n    simulated_pnl_pusd:      SUM(f.pnl_pusd FOR f IN paper_account.fills),\n    simulated_sharpe_ratio:  sharpe,\n    promotion_gate_eligible: days_elapsed >= config.min_paper_days\n  })\n\n// ---- PROMOTION GATE (at min_paper_days boundary) ----\nFUNCTION evaluatePromotionGate():\n  sharpe = computeSharpe(paper_account.fills)\n  gate_passed = (sharpe > 0)\n  IF gate_passed:\n    EMIT OperationsReport(event_type='PAPER_TRADE_RUNNER_GATE_PASSED',\n                          promotion_gate_passed=true, ...)\n  ELSE:\n    EMIT OperationsReport(event_type='PAPER_TRADE_RUNNER_GATE_FAILED',\n                          promotion_gate_passed=false,\n                          reason_code='PAPER_TRADE_RUNNER_RISK_ADJ_FAIL', ...)",
    "sdk_calls": [
      "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"
  },
  "wire_examples": {
    "input": {
      "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": {
      "label": "OperationsReport \u2014 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"
      }
    }
  },
  "reason_codes": [
    {
      "code": "PAPER_TRADE_RUNNER_FILL_SIMULATED",
      "severity": "INFO",
      "meaning": "A simulated fill was computed against the live order book snapshot and logged to the paper account ledger.",
      "action": "No action \u2014 routine paper trading event.",
      "user_message": "A simulated trade was recorded in your paper trading account."
    },
    {
      "code": "PAPER_TRADE_RUNNER_GATE_PASSED",
      "severity": "INFO",
      "meaning": "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.",
      "action": "No action \u2014 governance pod to review promotion evidence.",
      "user_message": "The strategy completed its paper trading period and is eligible for live promotion."
    },
    {
      "code": "PAPER_TRADE_RUNNER_GATE_FAILED",
      "severity": "WARN",
      "meaning": "The strategy completed min_paper_days but did not achieve positive risk-adjusted P&L (Sharpe ratio <= 0).",
      "action": "Emit alert; do not promote strategy; governance pod to review.",
      "user_message": "The strategy did not meet the required performance threshold during paper trading."
    },
    {
      "code": "PAPER_TRADE_RUNNER_RISK_ADJ_FAIL",
      "severity": "WARN",
      "meaning": "Sharpe ratio at promotion gate evaluation is <= 0; require_positive_risk_adj gate failed.",
      "action": "Block promotion gate; emit WARN; continue paper trading or revise strategy.",
      "user_message": "Risk-adjusted returns were insufficient during the paper trading period."
    },
    {
      "code": "PAPER_TRADE_RUNNER_FEED_STALE",
      "severity": "WARN",
      "meaning": "The live CLOB WebSocket feed has been disconnected for more than 30 minutes; fill simulation paused and promotion gate clock suspended.",
      "action": "Pause fill simulation; suspend promotion gate clock; emit alert; resume on feed recovery.",
      "user_message": ""
    },
    {
      "code": "STALE_DATA",
      "severity": "WARN",
      "meaning": "The order book snapshot used for fill simulation is stale (last updated > 30s ago). Fill simulation skipped for this signal.",
      "action": "Skip fill simulation for this tick; emit WARN; retry on next fresh snapshot.",
      "user_message": ""
    },
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "WARN",
      "meaning": "KillSwitch is active; simulated fill emission is suppressed. Paper trading timer continues.",
      "action": "Suppress simulated fills; emit WARN; paper timer continues.",
      "user_message": "Simulated trading activity is paused while the system kill switch is active."
    },
    {
      "code": "PAPER_TRADE_RUNNER_INTENT_BLOCKED",
      "severity": "INFO",
      "meaning": "A simulated intent was blocked by the paper-mode risk guardrail stack.",
      "action": "Log; no fill emitted; paper account unchanged.",
      "user_message": ""
    },
    {
      "code": "PARAMETER_CHANGE_REQUIRES_APPROVAL",
      "severity": "HARD_REJECT",
      "meaning": "min_paper_days below hard limit (3), require_positive_risk_adj set to false, or simulated_capital_usd above hard limit (500000) attempted.",
      "action": "Reject config change; emit alert.",
      "user_message": ""
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_gov_papertraderunner_fills_simulated_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "strategy",
          "paper_run_id"
        ],
        "meaning": "Total simulated fills across all paper trading sessions."
      },
      {
        "name": "polytraders_gov_papertraderunner_simulated_volume_pusd_total",
        "type": "counter",
        "unit": "usd",
        "labels": [
          "strategy"
        ],
        "meaning": "Cumulative simulated pUSD volume across all paper fills."
      },
      {
        "name": "polytraders_gov_papertraderunner_simulated_pnl_pusd",
        "type": "gauge",
        "unit": "usd",
        "labels": [
          "strategy",
          "paper_run_id"
        ],
        "meaning": "Cumulative simulated P&L in pUSD for the current paper session."
      },
      {
        "name": "polytraders_gov_papertraderunner_sharpe_ratio",
        "type": "gauge",
        "unit": "ratio",
        "labels": [
          "strategy",
          "paper_run_id"
        ],
        "meaning": "Current risk-adjusted Sharpe ratio for the paper session; used in promotion gate evaluation."
      },
      {
        "name": "polytraders_gov_papertraderunner_paper_days_elapsed",
        "type": "gauge",
        "unit": "days",
        "labels": [
          "strategy",
          "paper_run_id"
        ],
        "meaning": "Calendar days elapsed since paper session start; tracks progress toward min_paper_days gate."
      },
      {
        "name": "polytraders_gov_papertraderunner_gate_evaluations_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "strategy",
          "result"
        ],
        "meaning": "Total promotion gate evaluations, labelled by pass/fail result."
      }
    ],
    "alerts": [
      {
        "name": "PaperTradeRunnerGateFailed",
        "condition": "rate(polytraders_gov_papertraderunner_gate_evaluations_total{result='fail'}[1h]) > 0",
        "severity": "warn",
        "runbook": "#runbook-papertraderunner-gate-failed"
      },
      {
        "name": "PaperTradeRunnerFeedStale",
        "condition": "polytraders_gov_papertraderunner_fills_simulated_total rate over 30m == 0 during active trading hours",
        "severity": "warn",
        "runbook": "#runbook-papertraderunner-feed-stale"
      },
      {
        "name": "PaperTradeRunnerNegativeSharpe",
        "condition": "polytraders_gov_papertraderunner_sharpe_ratio < 0",
        "severity": "warn",
        "runbook": "#runbook-papertraderunner-negative-sharpe"
      },
      {
        "name": "PaperTradeRunnerNoSession",
        "condition": "polytraders_gov_papertraderunner_paper_days_elapsed == 0 for > 1h",
        "severity": "warn",
        "runbook": "#runbook-papertraderunner-no-session"
      }
    ],
    "dashboards": [
      "Grafana \u2014 Governance / Paper-Trade Runner session dashboard (P&L, Sharpe, days elapsed)",
      "Grafana \u2014 Governance / Promotion gate history and pass rate by strategy"
    ],
    "log_level": "info"
  },
  "state": {
    "store": "postgres",
    "shape": "paper_fills table: { paper_run_id, market_id, simulated_fill_price, size_pusd, fee_pusd, pnl_pusd, slippage_bps, paper_tagged, mode, tick_ts_ms }. paper_sessions table: { paper_run_id, strategy, start_ts, status, simulated_fills_count, cumulative_pnl_pusd, sharpe_ratio, gate_result }.",
    "ttl": "1 year (governance_audit retention)",
    "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.",
    "size_estimate": "~500 B per simulated fill; ~200 KB per 14-day session at 30 fills/day; < 1 MB total per strategy per session"
  },
  "concurrency": {
    "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",
    "timeout_ms": 2000,
    "backpressure": "shed signal events beyond queue depth; emit WARN; process backlog on recovery",
    "locking": "Postgres unique constraint on (paper_run_id, tick_ts_ms); session state is per-goroutine"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "internal.signal_bus",
        "why": "Paper-Trade Runner subscribes to the live signal bus to receive the same signals as live strategies, ensuring paper results are directly comparable."
      },
      {
        "bot_id": "internal.risk_stack",
        "why": "Risk guardrail votes are simulated through the full stack in paper mode to ensure the paper path is identical to live."
      }
    ],
    "emits_to": [
      {
        "bot_id": "internal.governance_audit",
        "what": "Paper-tagged OperationsReport records (per-fill and cadence) emitted to polytraders.reports.operations"
      }
    ],
    "sibling": [
      {
        "bot_id": "gov.backtester",
        "why": "Backtester uses archived data in replay mode; Paper-Trade Runner uses live data in paper mode. Both emit OperationsReport to polytraders.reports.operations."
      },
      {
        "bot_id": "gov.pnl-reporter",
        "why": "PnLReporter reads include_paper flag to segregate paper fills from live SettlementReports. Paper-Trade Runner's paper_tagged=true ensures no contamination."
      }
    ],
    "external": [
      {
        "service": "Polymarket CLOB v2 (order book, market metadata)",
        "sla": "99.95% / 200ms p99 (Polymarket-published)",
        "fallback": "Pause fill simulation on stale data; emit STALE_DATA; resume on feed recovery."
      },
      {
        "service": "Polymarket WebSocket feed (ws_market)",
        "sla": "99.9% (Polymarket-published)",
        "fallback": "Suspend fill simulation and promotion gate clock on disconnect; emit PAPER_TRADE_RUNNER_FEED_STALE after 30 minutes; resume on reconnect."
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "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 \u2014 it can never submit real orders"
    ],
    "contract_calls": []
  },
  "failure_injection": [
    {
      "scenario": "WEBSOCKET_DISCONNECT",
      "how_to_inject": "Drop the ws_market WebSocket connection for 5 minutes",
      "expected_behavior": "STALE_DATA warn emitted; fill simulation suspended; paper clock continues; reconnect triggers simulation resume",
      "recovery": "Automatic on WebSocket reconnect. If > 30 minutes, PAPER_TRADE_RUNNER_FEED_STALE fired and promotion gate clock suspended until recovery."
    },
    {
      "scenario": "NEGATIVE_SHARPE_AT_GATE",
      "how_to_inject": "Insert paper fills with negative pnl_pusd totalling a Sharpe of -0.5 at the min_paper_days boundary",
      "expected_behavior": "PAPER_TRADE_RUNNER_GATE_FAILED emitted; promotion_gate_passed=false; PaperTradeRunnerGateFailed alert fired",
      "recovery": "Continue paper trading or revise strategy parameters. Gate re-evaluated after additional paper days."
    },
    {
      "scenario": "REQUIRE_POSITIVE_RISK_ADJ_BYPASS",
      "how_to_inject": "Attempt to set require_positive_risk_adj=false in config",
      "expected_behavior": "PARAMETER_CHANGE_REQUIRES_APPROVAL raised; config rejected; parameter remains locked true",
      "recovery": "No recovery needed \u2014 bypass is rejected."
    },
    {
      "scenario": "MIN_PAPER_DAYS_BELOW_HARD",
      "how_to_inject": "Submit config with min_paper_days=2",
      "expected_behavior": "PARAMETER_CHANGE_REQUIRES_APPROVAL raised; config rejected",
      "recovery": "Set min_paper_days >= 3 (or ideally >= 14)."
    },
    {
      "scenario": "KILL_SWITCH_ACTIVE",
      "how_to_inject": "Activate KillSwitch during an active paper session",
      "expected_behavior": "KILL_SWITCH_ACTIVE logged; fill simulation suspended; paper days timer continues; cadence reports continue",
      "recovery": "Deactivate KillSwitch; fill simulation resumes on next signal event."
    },
    {
      "scenario": "PAPER_FILL_IN_LIVE_REPORT",
      "how_to_inject": "Set include_paper=true in PnLReporter and observe report content",
      "expected_behavior": "PNL_REPORTER_PAPER_IN_REGULATORY warn emitted by PnLReporter; paper fills flagged but not included in regulatory SettlementReport",
      "recovery": "Set include_paper=false; paper fills remain segregated."
    }
  ],
  "runbook": {
    "summary": "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.",
    "oncall_actions": [
      {
        "alert": "PaperTradeRunnerGateFailed",
        "first_step": "Review the paper session Sharpe ratio and fill history. Check if market conditions during the paper period were anomalously adverse.",
        "escalation": "Governance pod lead; strategy owner for parameter review"
      },
      {
        "alert": "PaperTradeRunnerFeedStale",
        "first_step": "Check Polymarket WebSocket feed (ws_market) status. Verify CLOB public API is reachable.",
        "escalation": "Exec pod lead if WebSocket disconnection is confirmed"
      },
      {
        "alert": "PaperTradeRunnerNegativeSharpe",
        "first_step": "Review recent paper fills for unusual losses. Check if a strategy parameter change occurred recently.",
        "escalation": "Governance pod lead; strategy owner"
      },
      {
        "alert": "PaperTradeRunnerNoSession",
        "first_step": "Check if any strategies are configured for paper trading. Verify Paper-Trade Runner process is running.",
        "escalation": "Governance pod lead"
      }
    ],
    "manual_overrides": [
      {
        "command": "polytraders gov paper reset-gate --paper-run-id <id> --reviewed-by <operator>",
        "effect": "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."
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass for fill simulation, promotion gate evaluation, and parameter lock enforcement",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      },
      {
        "gate": "Postgres paper_fills schema migration verified",
        "how_measured": "Integration test",
        "threshold": "Pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "14-day paper session completes for at least one strategy with gate passed",
        "how_measured": "PAPER_TRADE_RUNNER_GATE_PASSED event in governance audit log",
        "threshold": "Pass"
      },
      {
        "gate": "Paper fill contamination test: no paper_tagged=true fills appear in live PnLReporter SettlementReports",
        "how_measured": "Audit of SettlementReport records in Postgres; paper_tagged field check",
        "threshold": "0 contaminated records"
      }
    ],
    "to_general_live": [
      {
        "gate": "3 separate strategy promotion gates evaluated; results consistent with subsequent limited live performance",
        "how_measured": "Governance pod retrospective review of paper vs limited-live Sharpe ratios",
        "threshold": "Correlation > 0.7 between paper and limited-live Sharpe"
      },
      {
        "gate": "WebSocket reconnect handling verified: simulation resumes correctly after 30-minute disconnect",
        "how_measured": "Failure injection test",
        "threshold": "Pass"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "OperationsReport"
    ],
    "topics": [
      "polytraders.reports.operations"
    ],
    "cadence": "every-event",
    "retention_class": "1y",
    "sampling_rule": "batched-1/min",
    "bus_failure_action": "drop-after-buffer",
    "user_visible": "no",
    "consumes_kinds": [
      "ObservationReport",
      "DecisionReport",
      "RiskVote",
      "ExecutionReport",
      "SettlementReport",
      "OperationsReport"
    ]
  },
  "capital_impact": "Indirect",
  "v3_status": {
    "phase": 7,
    "phase_name": "Governance & replay",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}