{
  "schema_version": "1.0.0",
  "bot_id": "1.13",
  "bot_name": "TailLossSimulator",
  "slug": "taillosssimulator",
  "layer": "Risk",
  "layer_key": "risk",
  "bot_class": "Guardrail",
  "authority": [
    "Veto",
    "Reshape"
  ],
  "status": "planned",
  "readiness": "Planned",
  "flagship": false,
  "is_reference": false,
  "public_export": false,
  "identity": {
    "layer": "Risk",
    "bot_class": "Guardrail",
    "authority": "Veto, Reshape",
    "runs_before": "ExecutionPlan emit",
    "runs_after": "Strategy OrderIntent",
    "applies_to": "Every OrderIntent \u2014 stress-tests the combined portfolio (existing + proposed) against scripted shock scenarios before approving size-up orders",
    "default_mode": "planned",
    "user_visible": "summary-only",
    "developer_owner": "Polytraders core \u2014 Risk pod"
  },
  "purpose": "TailLossSimulator applies scripted adverse shock scenarios to the combined portfolio (open positions plus the proposed order) and rejects or downsizes the order if the simulated tail loss exceeds the configured maximum. It acts as a pre-trade stress test, replacing post-hoc margin calls with a forward-looking gate.",
  "why_it_matters": [
    {
      "failure": "Tail loss unquantified before sizing up",
      "consequence": "Adding a new position without a stress test can push the portfolio into a tail regime where a single adverse resolution event causes losses far beyond the user's tolerance."
    },
    {
      "failure": "Correlated shock undetected",
      "consequence": "A scripted scenario that resolves all markets in the same direction reveals hidden concentration risk that individual position limits would miss."
    }
  ],
  "polymarket_inputs": [
    {
      "input": "Current market prices for all open positions",
      "source": "clob_public",
      "required": true,
      "use": "Seed the shock scenario with current mark-to-market prices."
    },
    {
      "input": "Outcome probabilities for open markets",
      "source": "data_api",
      "required": true,
      "use": "Apply shock probability shifts to compute stressed portfolio value."
    }
  ],
  "internal_inputs": [
    {
      "input": "Open position list and notional values",
      "source": "internal",
      "required": true,
      "use": "Define the current portfolio state before adding the proposed order."
    },
    {
      "input": "Shock scenario library",
      "source": "internal",
      "required": true,
      "use": "Load scripted adverse scenarios (probability shifts per market category)."
    },
    {
      "input": "KillSwitch active flag",
      "source": "KillSwitch",
      "required": true,
      "use": "If active, reject immediately."
    }
  ],
  "raw_params": [
    "max_tail_loss_usd \u00b7 int",
    "shock_scenarios \u00b7 list",
    "tail_percentile \u00b7 float"
  ],
  "parameters": [
    {
      "name": "max_tail_loss_usd",
      "default": 500,
      "warning": 400,
      "hard": 500,
      "controls": "Maximum acceptable simulated tail loss (in pUSD) across all shock scenarios before the order is blocked or downsized.",
      "why_default_matters": "500 pUSD is a conservative default that limits single-event drawdown to a manageable fraction of a typical portfolio.",
      "threshold_logic": [
        {
          "condition": "max_scenario_loss <= 400",
          "action": "APPROVE"
        },
        {
          "condition": "400 < max_scenario_loss <= 500",
          "action": "WARN \u2014 TAIL_LOSS_APPROACHING"
        },
        {
          "condition": "max_scenario_loss > 500",
          "action": "RESHAPE or HARD_REJECT \u2014 TAIL_LOSS_EXCEEDED"
        }
      ],
      "dev_check": "if (maxScenarioLoss > params.max_tail_loss_usd) return reshape_or_reject('TAIL_LOSS_EXCEEDED');",
      "user_facing": "Your order would expose your portfolio to a potential tail loss above the configured limit."
    },
    {
      "name": "shock_scenarios",
      "default": [
        "all_yes_resolves",
        "all_no_resolves",
        "macro_adverse_shift"
      ],
      "warning": null,
      "hard": null,
      "controls": "List of named shock scenarios from the scenario library to run against the proposed portfolio.",
      "why_default_matters": "The three default scenarios cover full YES resolution, full NO resolution, and a macro probability shift, which together bound the binary tail exposure.",
      "threshold_logic": [
        {
          "condition": "all scenarios run",
          "action": "Use worst-case loss across all scenarios"
        }
      ],
      "dev_check": "const scenarioLosses = params.shock_scenarios.map(s => runScenario(portfolio, s));",
      "user_facing": ""
    },
    {
      "name": "tail_percentile",
      "default": 0.05,
      "warning": null,
      "hard": null,
      "controls": "The probability tail used when computing expected tail loss from a scenario distribution. 0.05 = CVaR at 5th percentile.",
      "why_default_matters": "5th-percentile tail captures extreme but plausible adverse outcomes without being overly conservative for normal market conditions.",
      "threshold_logic": [
        {
          "condition": "always",
          "action": "Use configured percentile in scenario loss aggregation"
        }
      ],
      "dev_check": "const tailLoss = cvar(scenarioLossDistribution, params.tail_percentile);",
      "user_facing": ""
    }
  ],
  "default_config": {
    "bot_id": "risk.tail_loss_simulator",
    "version": "0.1.0",
    "mode": "hard_guard",
    "defaults": {
      "max_tail_loss_usd": 500,
      "shock_scenarios": [
        "all_yes_resolves",
        "all_no_resolves",
        "macro_adverse_shift"
      ],
      "tail_percentile": 0.05
    },
    "locked": {
      "max_tail_loss_usd": {
        "min": 50
      }
    }
  },
  "implementation_flow": [
    "Receive OrderIntent with market_id, side, size_usd.",
    "Check KillSwitch; if active, HARD_REJECT(KILL_SWITCH_ACTIVE).",
    "Load open positions and current prices to construct current portfolio state.",
    "Append proposed order to the portfolio (as if filled).",
    "Load shock scenario library and run each configured scenario against the proposed portfolio.",
    "For each scenario, compute the portfolio value change (loss if negative).",
    "Take the maximum loss across all scenarios as the tail loss estimate.",
    "If tail_loss > max_tail_loss_usd.hard: compute safe_size_usd that would keep tail_loss within limit.",
    "If safe_size_usd > 0: RESHAPE with constraints.max_size_usd = safe_size_usd. Else HARD_REJECT(TAIL_LOSS_EXCEEDED).",
    "If tail_loss > max_tail_loss_usd.warning: attach WARN annotation; APPROVE.",
    "All checks passed \u2014 APPROVE with worst_case_loss attached."
  ],
  "decision_logic": {
    "approve": "Maximum simulated tail loss across all shock scenarios is at or below the warning threshold.",
    "reshape_required": "Tail loss exceeds the hard ceiling but can be reduced by downsizing the order.",
    "reject": "Tail loss exceeds the hard ceiling even at minimum order size, or data is unavailable."
  },
  "decision_output_schema": "RiskVote",
  "decision_output_example": {
    "guard_id": "risk.tail_loss_simulator",
    "decision": "RESHAPE_REQUIRED",
    "severity": "WARN",
    "reason_code": "TAIL_LOSS_EXCEEDED",
    "message": "Proposed order produces tail loss 620 pUSD under scenario 'all_yes_resolves'. Resized to keep tail loss <= 500 pUSD.",
    "constraints": {
      "max_size_usd": 320
    },
    "inputs_used": [
      "internal.positions",
      "internal.shock_scenarios",
      "clob_public.prices"
    ],
    "checked_at": "2026-05-10T12:00:00Z"
  },
  "developer_log": {
    "bot_id": "risk.tail_loss_simulator",
    "decision": "RESHAPE_REQUIRED",
    "reason_code": "TAIL_LOSS_EXCEEDED",
    "inputs_used": [
      "internal.positions",
      "internal.shock_scenarios",
      "clob_public.prices"
    ],
    "metrics": {
      "worst_scenario": "all_yes_resolves",
      "tail_loss_usd": 620,
      "safe_size_usd": 320,
      "max_tail_loss_usd": 500
    },
    "checked_at": "2026-05-10T12:00:00Z"
  },
  "user_explanations": [
    {
      "situation": "Order downsized \u2014 tail loss limit",
      "message": "Your order was reduced because the full size would expose your portfolio to a potential loss above the configured stress-test limit under adverse scenarios."
    },
    {
      "situation": "Order blocked \u2014 tail loss cannot be reduced",
      "message": "Even at a minimal size, this order would push your portfolio tail risk above the allowed limit. Please close some existing positions first."
    }
  ],
  "failure_modes": {
    "main_failure_mode": "Approving an order because the shock scenarios are insufficiently adverse, underestimating the true tail risk.",
    "false_positive_risk": "Rejecting a legitimate order because an extreme scenario is too pessimistic relative to actual market conditions.",
    "false_negative_risk": "Approving an order if the scenario library has not been updated to reflect a new market structure or correlated risk factor.",
    "safe_fallback": "If position data or scenario library is unavailable, HARD_REJECT with TAIL_LOSS_DATA_UNAVAILABLE. Never approve on missing data.",
    "required_dependencies": [
      "Open position ledger",
      "Shock scenario library",
      "CLOB current prices",
      "KillSwitch active flag"
    ]
  },
  "acceptance_tests": {
    "unit": [
      {
        "test": "Approve when all scenario losses within warning threshold",
        "setup": "max_scenario_loss=380, ceiling=500",
        "expected": "APPROVE"
      },
      {
        "test": "Reshape when tail loss exceeds ceiling but safe size exists",
        "setup": "tail_loss=620, safe_size=320",
        "expected": "RESHAPE_REQUIRED(max_size_usd=320)"
      },
      {
        "test": "Reject when tail loss exceeds ceiling even at min size",
        "setup": "tail_loss=800 even at min_size=10",
        "expected": "HARD_REJECT(TAIL_LOSS_EXCEEDED)"
      },
      {
        "test": "Warn when tail loss between warning and hard",
        "setup": "tail_loss=450, warning=400, hard=500",
        "expected": "APPROVE with WARN annotation"
      }
    ],
    "integration": [
      {
        "test": "Shock scenario reshapes order flowing through to SmartRouter",
        "expected": "SmartRouter receives constraints.max_size_usd from RESHAPE; executes at reduced size"
      },
      {
        "test": "KillSwitch bypasses scenario computation",
        "expected": "HARD_REJECT(KILL_SWITCH_ACTIVE) without running any scenarios"
      }
    ],
    "property": [
      {
        "property": "Approved order never causes tail loss above max_tail_loss_usd",
        "required": "Always true after reshape"
      },
      {
        "property": "Reshape size is strictly <= original intent size_usd",
        "required": "Always true"
      }
    ]
  },
  "checklist_overrides": {},
  "legacy_goal": "Stress-test open book against scripted shock scenarios before sizing up.",
  "legacy_pm_signals": [
    "Open positions, neg-risk topology, oracle exposure",
    "Scenario library: gap-to-1, gap-to-0, oracle-flip, simultaneous-resolution",
    "Resulting margin / equity hit per scenario"
  ],
  "legacy_external_feeds": [],
  "reporting_groups": [
    "risk_compliance"
  ],
  "reason_codes": [
    {
      "code": "KILL_SWITCH_ACTIVE",
      "severity": "HARD_REJECT",
      "meaning": "Global kill switch active.",
      "action": "Immediate HARD_REJECT.",
      "user_message": "Trading is paused. Please try again later."
    },
    {
      "code": "TAIL_LOSS_EXCEEDED",
      "severity": "RESHAPE",
      "meaning": "Simulated tail loss exceeds max_tail_loss_usd; order resized if possible.",
      "action": "RESHAPE if safe_size > 0; else HARD_REJECT.",
      "user_message": "Your order was adjusted due to portfolio tail risk limits."
    },
    {
      "code": "TAIL_LOSS_APPROACHING",
      "severity": "WARN",
      "meaning": "Tail loss is between warning and hard threshold.",
      "action": "Attach WARN annotation; APPROVE.",
      "user_message": ""
    },
    {
      "code": "TAIL_LOSS_DATA_UNAVAILABLE",
      "severity": "HARD_REJECT",
      "meaning": "Position data or scenario library unavailable.",
      "action": "HARD_REJECT (fail-closed).",
      "user_message": "We could not complete the portfolio stress test. Please try again."
    }
  ],
  "metrics": {
    "emitted": [
      {
        "name": "polytraders_risk_taillosssimulator_decisions_total",
        "type": "counter",
        "unit": "count",
        "labels": [
          "decision",
          "reason_code"
        ],
        "meaning": "Total decisions by type."
      },
      {
        "name": "polytraders_risk_taillosssimulator_worst_case_loss_usd",
        "type": "gauge",
        "unit": "usd",
        "labels": [],
        "meaning": "Worst-case tail loss across scenarios at last evaluation."
      },
      {
        "name": "polytraders_risk_taillosssimulator_eval_latency_ms",
        "type": "histogram",
        "unit": "milliseconds",
        "labels": [],
        "meaning": "Latency from intent to RiskVote emit."
      }
    ],
    "alerts": [
      {
        "name": "TailLossSimulatorHighTailLoss",
        "condition": "polytraders_risk_taillosssimulator_worst_case_loss_usd > 400",
        "severity": "P2",
        "runbook": "#runbook-tailloss-high"
      },
      {
        "name": "TailLossSimulatorDataUnavailable",
        "condition": "rate(polytraders_risk_taillosssimulator_decisions_total{reason_code='TAIL_LOSS_DATA_UNAVAILABLE'}[5m]) > 0",
        "severity": "P1",
        "runbook": "#runbook-tailloss-data"
      }
    ]
  },
  "state": {
    "store": "in-memory",
    "shape": "Current portfolio snapshot cached in-memory; scenario library loaded from Redis at startup.",
    "ttl": "Portfolio snapshot refreshed every 10s from CLOB.",
    "recovery": "On cold start, portfolio snapshot fetched from CLOB before first evaluation. Scenario library loaded from Redis.",
    "size_estimate": "~2 KB for portfolio snapshot; ~10 KB for scenario library"
  },
  "concurrency": {
    "execution_model": "single-threaded event loop",
    "max_in_flight": 50,
    "idempotency_key": "intent_id",
    "timeout_ms": 300,
    "backpressure": "drop newest",
    "locking": "per-user_id mutex during scenario computation"
  },
  "dependencies": {
    "depends_on": [
      {
        "bot_id": "risk.kill_switch",
        "why": "Global brake checked first.",
        "contract": "HARD_REJECT(KILL_SWITCH_ACTIVE) short-circuits all scenario evaluation."
      }
    ],
    "emits_to": [
      {
        "bot_id": "exec.smart_router",
        "why": "APPROVE or RESHAPE passes to SmartRouter.",
        "contract": "RESHAPE constraints respected by SmartRouter."
      }
    ],
    "sibling": [],
    "external": [
      {
        "service": "CLOB API (current prices)",
        "endpoint": "https://clob.polymarket.com",
        "sla": "99.95% / 200ms p99",
        "failure_mode": "HARD_REJECT(TAIL_LOSS_DATA_UNAVAILABLE) if prices unavailable."
      }
    ]
  },
  "security_surfaces": {
    "signs_orders": false,
    "private_key_access": "none",
    "abuse_vectors": [
      "Submitting a low-notional order to establish a position then sizing up in a second order after the first passes the stress test",
      "Manipulating the position cache to show a smaller portfolio, reducing apparent tail risk"
    ],
    "mitigations": [
      "Portfolio snapshot is refreshed every 10s from CLOB, limiting the staleness window",
      "Idempotency deduplication prevents rapid sequential submissions from bypassing the per-intent evaluation"
    ]
  },
  "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",
    "notes": "All loss values denominated in pUSD. NegRisk market outcomes are included in shock scenarios using the NegRiskAdapter resolution model."
  },
  "version": {
    "spec": "2.0.0",
    "implementation": "0.1.0",
    "schema": "2",
    "released": null,
    "planned_release": "Q4-2026"
  },
  "migration_history": [
    {
      "date": "2026-04-28",
      "from": "n/a",
      "to": "v2-spec",
      "reason": "Spec drafted post-CLOB-V2 cutover; bot not yet implemented",
      "action_taken": "Designed against V2 schema (pUSD, builder codes, V2 EIP-712 domain)"
    }
  ],
  "reference_implementation": {
    "pseudocode": "FUNCTION evaluateTailLoss(intent):\n  ks = FETCH internal.killswitch.status\n  IF ks.active: EMIT RiskVote(HARD_REJECT, KILL_SWITCH_ACTIVE); RETURN\n\n  positions = FETCH internal.open_positions(intent.user_id)\n  prices = FETCH clob_public.prices(ALL position.market_ids)\n  scenarios = FETCH internal.shock_scenario_library()\n  IF positions IS NULL OR prices IS NULL OR scenarios IS NULL:\n    EMIT RiskVote(HARD_REJECT, TAIL_LOSS_DATA_UNAVAILABLE); RETURN\n\n  proposedPortfolio = positions + [intent as position]\n  scenarioLosses = []\n  FOR scenario IN params.shock_scenarios:\n    shockedPrices = applyShock(prices, scenarios[scenario])\n    pnl = computePortfolioPnL(proposedPortfolio, shockedPrices)\n    scenarioLosses.append(-pnl if pnl < 0 else 0)\n\n  tailLoss = max(scenarioLosses)\n\n  IF tailLoss > params.max_tail_loss_usd:\n    safeSize = binarySearchSafeSize(positions, intent, scenarios, prices, params)\n    IF safeSize > 0:\n      EMIT RiskVote(RESHAPE_REQUIRED, TAIL_LOSS_EXCEEDED,\n                    constraints={max_size_usd: safeSize}); RETURN\n    EMIT RiskVote(HARD_REJECT, TAIL_LOSS_EXCEEDED); RETURN\n\n  IF tailLoss > params.max_tail_loss_usd * 0.8:\n    annotations.append(WARN(TAIL_LOSS_APPROACHING, tail_loss=tailLoss))\n\n  EMIT RiskVote(APPROVE, worst_case_loss=tailLoss)",
    "sdk_calls": [
      "clob_public.prices(market_ids)",
      "internal.open_positions(user_id)",
      "internal.shock_scenario_library()",
      "internal.killswitch.status()"
    ],
    "complexity": "O(S * N) where S = scenarios (default 3), N = open positions"
  },
  "wire_examples": {
    "input": [
      {
        "label": "OrderIntent \u2014 tail loss would be exceeded",
        "source": "internal",
        "payload": {
          "intent_id": "int_e5f6a7b8c9d00005",
          "market_id": "0x3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d",
          "size_usd": 500,
          "side": "BUY",
          "generated_at_ms": 1746800000000
        }
      }
    ],
    "output": [
      {
        "label": "RiskVote \u2014 RESHAPE_REQUIRED",
        "payload": {
          "guard_id": "risk.tail_loss_simulator",
          "decision": "RESHAPE_REQUIRED",
          "severity": "WARN",
          "reason_code": "TAIL_LOSS_EXCEEDED",
          "message": "Tail loss 620 pUSD under 'all_yes_resolves' exceeds 500. Resized to 320.",
          "constraints": {
            "max_size_usd": 320
          },
          "checked_at": "2026-05-10T12:00:00Z"
        }
      }
    ]
  },
  "failure_injection": [
    {
      "scenario": "SCENARIO_LIBRARY_UNAVAILABLE",
      "how_to_inject": "Delete scenario library from Redis",
      "expected_behaviour": "HARD_REJECT(TAIL_LOSS_DATA_UNAVAILABLE)",
      "recovery": "Returns to normal after scenario library is restored."
    },
    {
      "scenario": "EXTREME_TAIL_LOSS",
      "how_to_inject": "Set open positions to all-in YES with large notional, submit a BUY",
      "expected_behaviour": "HARD_REJECT(TAIL_LOSS_EXCEEDED) under 'all_yes_resolves' scenario",
      "recovery": "Returns to APPROVE after existing positions are reduced."
    },
    {
      "scenario": "PRICE_DATA_UNAVAILABLE",
      "how_to_inject": "Return 503 from CLOB prices endpoint",
      "expected_behaviour": "HARD_REJECT(TAIL_LOSS_DATA_UNAVAILABLE)",
      "recovery": "Returns to normal within one portfolio snapshot refresh."
    }
  ],
  "runbook": {
    "summary": "TailLossSimulator incidents are typically caused by a concentrated portfolio approaching the tail loss ceiling, or by stale scenario data. Verify the scenario library is current before adjusting parameters.",
    "oncall_actions": [
      {
        "alert": "TailLossSimulatorHighTailLoss",
        "first_step": "Inspect worst_case_loss_usd gauge; identify which scenario is driving the worst case. Check if portfolio concentration has increased.",
        "escalation": "Risk pod lead if tail loss is genuinely at the ceiling.",
        "diagnosis": "",
        "mitigation": ""
      },
      {
        "alert": "TailLossSimulatorDataUnavailable",
        "first_step": "Check CLOB prices endpoint and scenario library Redis key.",
        "escalation": "Infra on-call if CLOB or Redis unavailable > 2 minutes.",
        "diagnosis": "",
        "mitigation": ""
      }
    ],
    "manual_overrides": [
      {
        "command": "polytraders risk update-scenarios --scenario-file <path>",
        "effect": "After a market structure change that requires new or revised shock scenarios."
      }
    ],
    "healthcheck": "GET /internal/health/taillosssimulator \u2192 green: Scenario library loaded, CLOB prices reachable, portfolio snapshot age < 30s; red: Scenario library missing, CLOB unreachable, or snapshot age > 60s"
  },
  "promotion_gates": {
    "to_shadow": [
      {
        "gate": "Unit tests pass for reshape and reject scenarios",
        "how_measured": "CI test run",
        "threshold": "100% pass"
      }
    ],
    "to_limited_live": [
      {
        "gate": "Shadow reshape rate aligns with expected over 48h",
        "how_measured": "Grafana shadow vs live",
        "threshold": "No spurious HARD_REJECTs in shadow run"
      }
    ],
    "to_general_live": [
      {
        "gate": "Zero DATA_UNAVAILABLE rejections during normal hours over 7 days",
        "how_measured": "TailLossSimulatorDataUnavailable alert history",
        "threshold": "0 firings"
      }
    ]
  },
  "reporting": {
    "emits_kinds": [
      "RiskVote"
    ],
    "topics": [
      "polytraders.reports.risk"
    ],
    "partition_by": "trace_id",
    "cadence": "every-event",
    "retention_class": "2y",
    "sampling_rule": "emit-every",
    "bus_failure_action": "fail-closed",
    "user_visible": "summary-only",
    "consumes_kinds": [
      "ObservationReport"
    ]
  },
  "capital_impact": "Direct",
  "mode_support": [
    "quarantine"
  ],
  "v3_status": {
    "phase": 4,
    "phase_name": "Core risk",
    "docs": {
      "done": 27,
      "total": 27,
      "state": "done"
    },
    "impl": {
      "done": 0,
      "total": 15,
      "state": "pending"
    },
    "runtime": {
      "done": 0,
      "total": 8,
      "state": "pending"
    },
    "overall": "pending"
  }
}