1. Bot Identity
| Layer | Risk Risk |
|---|
| Bot class | Guardrail |
|---|
| Authority | VetoReshape |
|---|
| Status | PLANNED |
|---|
| Readiness | Planned |
|---|
| Runs before | ExecutionPlan emit |
|---|
| Runs after | Strategy OrderIntent |
|---|
| Applies to | Every OrderIntent — 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 — Risk pod |
|---|
Operational profile
| Modes supported | quarantine |
|---|
2. 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.
3. Why This Bot Matters
Tail loss unquantified before sizing up
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.
Correlated shock undetected
A scripted scenario that resolves all markets in the same direction reveals hidden concentration risk that individual position limits would miss.
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 |
|---|
| max_tail_loss_usd | 500 | 400 | 500 | Maximum acceptable simulated tail loss (in pUSD) across all shock scenarios before the order is blocked or downsized. |
| shock_scenarios | ['all_yes_resolves', 'all_no_resolves', 'macro_adverse_shift'] | None | None | List of named shock scenarios from the scenario library to run against the proposed portfolio. |
| tail_percentile | 0.05 | None | None | The probability tail used when computing expected tail loss from a scenario distribution. 0.05 = CVaR at 5th percentile. |
7. Detailed Parameter Instructions
max_tail_loss_usd
What it means
Maximum acceptable simulated tail loss (in pUSD) across all shock scenarios before the order is blocked or downsized.
Default
{ "max_tail_loss_usd": 500 }
Why this default matters
500 pUSD is a conservative default that limits single-event drawdown to a manageable fraction of a typical portfolio.
Threshold logic
| Condition | Action |
|---|
| max_scenario_loss <= 400 | APPROVE |
| 400 < max_scenario_loss <= 500 | WARN — TAIL_LOSS_APPROACHING |
| max_scenario_loss > 500 | RESHAPE or HARD_REJECT — TAIL_LOSS_EXCEEDED |
Developer check
if (maxScenarioLoss > params.max_tail_loss_usd) return reshape_or_reject('TAIL_LOSS_EXCEEDED');
User-facing English
Your order would expose your portfolio to a potential tail loss above the configured limit.
shock_scenarios
What it means
List of named shock scenarios from the scenario library to run against the proposed portfolio.
Default
{ "shock_scenarios": ["all_yes_resolves", "all_no_resolves", "macro_adverse_shift"] }
Why this 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 | Action |
|---|
| all scenarios run | Use worst-case loss across all scenarios |
Developer check
const scenarioLosses = params.shock_scenarios.map(s => runScenario(portfolio, s));
User-facing English
— not yet authored —
tail_percentile
What it means
The probability tail used when computing expected tail loss from a scenario distribution. 0.05 = CVaR at 5th percentile.
Default
{ "tail_percentile": 0.05 }
Why this default matters
5th-percentile tail captures extreme but plausible adverse outcomes without being overly conservative for normal market conditions.
Threshold logic
| Condition | Action |
|---|
| always | Use configured percentile in scenario loss aggregation |
Developer check
const tailLoss = cvar(scenarioLossDistribution, params.tail_percentile);
User-facing English
— not yet authored —
8. Default Configuration
{
"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
}
}
}
9. 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 — APPROVE with worst_case_loss attached.
10. Reference Implementation
Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. IF/THEN/ELSE = decision. Translate directly to TypeScript, Python, Go, or Rust.
FUNCTION evaluateTailLoss(intent):
ks = FETCH internal.killswitch.status
IF ks.active: EMIT RiskVote(HARD_REJECT, KILL_SWITCH_ACTIVE); RETURN
positions = FETCH internal.open_positions(intent.user_id)
prices = FETCH clob_public.prices(ALL position.market_ids)
scenarios = FETCH internal.shock_scenario_library()
IF positions IS NULL OR prices IS NULL OR scenarios IS NULL:
EMIT RiskVote(HARD_REJECT, TAIL_LOSS_DATA_UNAVAILABLE); RETURN
proposedPortfolio = positions + [intent as position]
scenarioLosses = []
FOR scenario IN params.shock_scenarios:
shockedPrices = applyShock(prices, scenarios[scenario])
pnl = computePortfolioPnL(proposedPortfolio, shockedPrices)
scenarioLosses.append(-pnl if pnl < 0 else 0)
tailLoss = max(scenarioLosses)
IF tailLoss > params.max_tail_loss_usd:
safeSize = binarySearchSafeSize(positions, intent, scenarios, prices, params)
IF safeSize > 0:
EMIT RiskVote(RESHAPE_REQUIRED, TAIL_LOSS_EXCEEDED,
constraints={max_size_usd: safeSize}); RETURN
EMIT RiskVote(HARD_REJECT, TAIL_LOSS_EXCEEDED); RETURN
IF tailLoss > params.max_tail_loss_usd * 0.8:
annotations.append(WARN(TAIL_LOSS_APPROACHING, tail_loss=tailLoss))
EMIT RiskVote(APPROVE, worst_case_loss=tailLoss)
SDK calls used
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
11. Wire Examples
Input — what arrives on the wire
OrderIntent — tail loss would be exceeded — internal
{
"intent_id": "int_e5f6a7b8c9d00005",
"market_id": "0x3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d",
"size_usd": 500,
"side": "BUY",
"generated_at_ms": 1746800000000
}
Output — what the bot emits
RiskVote — RESHAPE_REQUIRED
{
"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"
}
12. 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.
WARNING_ONLY
— not yet authored —13. Standard Decision Output
This bot returns a RiskVote object. See RiskVote schema.
{
"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"
}
14. Reason Codes
| Code | Severity | Meaning | Action | User-facing message |
|---|
KILL_SWITCH_ACTIVE | HARD_REJECT | Global kill switch active. | Immediate HARD_REJECT. | Trading is paused. Please try again later. |
TAIL_LOSS_EXCEEDED | RESHAPE | Simulated tail loss exceeds max_tail_loss_usd; order resized if possible. | RESHAPE if safe_size > 0; else HARD_REJECT. | Your order was adjusted due to portfolio tail risk limits. |
TAIL_LOSS_APPROACHING | WARN | Tail loss is between warning and hard threshold. | Attach WARN annotation; APPROVE. | |
TAIL_LOSS_DATA_UNAVAILABLE | HARD_REJECT | Position data or scenario library unavailable. | HARD_REJECT (fail-closed). | We could not complete the portfolio stress test. Please try again. |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|
polytraders_risk_taillosssimulator_decisions_total | counter | count | decision, reason_code | Total decisions by type. |
polytraders_risk_taillosssimulator_worst_case_loss_usd | gauge | usd | | Worst-case tail loss across scenarios at last evaluation. |
polytraders_risk_taillosssimulator_eval_latency_ms | histogram | milliseconds | | Latency from intent to RiskVote emit. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|
TailLossSimulatorHighTailLoss | polytraders_risk_taillosssimulator_worst_case_loss_usd > 400 | P2 | #runbook-tailloss-high |
TailLossSimulatorDataUnavailable | rate(polytraders_risk_taillosssimulator_decisions_total{reason_code='TAIL_LOSS_DATA_UNAVAILABLE'}[5m]) > 0 | P1 | #runbook-tailloss-data |
16. Developer Reporting
{
"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"
}
17. Plain-English Reporting
| Situation | User-facing explanation |
|---|
| Order downsized — tail loss limit | 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. |
| Order blocked — tail loss cannot be reduced | Even at a minimal size, this order would push your portfolio tail risk above the allowed limit. Please close some existing positions first. |
18. Failure-Mode Block
| 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 |
|---|
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|
SCENARIO_LIBRARY_UNAVAILABLE | Delete scenario library from Redis | | Returns to normal after scenario library is restored. |
EXTREME_TAIL_LOSS | Set open positions to all-in YES with large notional, submit a BUY | | Returns to APPROVE after existing positions are reduced. |
PRICE_DATA_UNAVAILABLE | Return 503 from CLOB prices endpoint | | Returns to normal within one portfolio snapshot refresh. |
20. State & Persistence
Cold-start recovery
On cold start, portfolio snapshot fetched from CLOB before first evaluation. Scenario library loaded from Redis.
21. Concurrency & Idempotency
| Aspect | Specification |
|---|
| Execution model | single-threaded event loop |
| Max in-flight | 50 |
| Idempotency key | intent_id |
| Per-call timeout (ms) | 300 |
| Backpressure strategy | drop newest |
| Locking / mutual exclusion | per-user_id mutex during scenario computation |
22. Dependencies
Depends on (must run first)
| Bot | Why | Contract |
|---|
| risk.kill_switch | Global brake checked first. | HARD_REJECT(KILL_SWITCH_ACTIVE) short-circuits all scenario evaluation. |
Emits to (downstream consumers)
External services
| Service | Endpoint | SLA assumed | On failure |
|---|
| CLOB API (current prices) | https://clob.polymarket.com | 99.95% / 200ms p99 | HARD_REJECT(TAIL_LOSS_DATA_UNAVAILABLE) if prices unavailable. |
23. Security Surfaces
Abuse vectors considered
- 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
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 |
| Notes | All loss values denominated in pUSD. NegRisk market outcomes are included in shock scenarios using the NegRiskAdapter resolution model. |
25. Versioning & Migration
| Field | Value |
|---|
| spec | 2.0.0 |
| implementation | 0.1.0 |
| schema | 2 |
| released | None |
| planned_release | Q4-2026 |
Migration history
| Date | From | To | Reason | Action taken |
|---|
| 2026-04-28 | n/a | v2-spec | Spec drafted post-CLOB-V2 cutover; bot not yet implemented | Designed against V2 schema (pUSD, builder codes, V2 EIP-712 domain) |
26. Acceptance Tests
Unit Tests
| Test | Setup | Expected result |
|---|
| Approve when all scenario losses within warning threshold | max_scenario_loss=380, ceiling=500 | APPROVE |
| Reshape when tail loss exceeds ceiling but safe size exists | tail_loss=620, safe_size=320 | RESHAPE_REQUIRED(max_size_usd=320) |
| Reject when tail loss exceeds ceiling even at min size | tail_loss=800 even at min_size=10 | HARD_REJECT(TAIL_LOSS_EXCEEDED) |
| Warn when tail loss between warning and hard | tail_loss=450, warning=400, hard=500 | APPROVE with WARN annotation |
Integration Tests
| Test | Expected result |
|---|
| Shock scenario reshapes order flowing through to SmartRouter | SmartRouter receives constraints.max_size_usd from RESHAPE; executes at reduced size |
| KillSwitch bypasses scenario computation | HARD_REJECT(KILL_SWITCH_ACTIVE) without running any scenarios |
Property Tests
| Property | Required behaviour |
|---|
| Approved order never causes tail loss above max_tail_loss_usd | Always true after reshape |
| Reshape size is strictly <= original intent size_usd | Always true |
27. Operational Runbook
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.
On-call actions
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|
TailLossSimulatorHighTailLoss | Inspect worst_case_loss_usd gauge; identify which scenario is driving the worst case. Check if portfolio concentration has increased. | | | Risk pod lead if tail loss is genuinely at the ceiling. |
TailLossSimulatorDataUnavailable | Check CLOB prices endpoint and scenario library Redis key. | | | Infra on-call if CLOB or Redis unavailable > 2 minutes. |
Manual overrides
polytraders risk update-scenarios --scenario-file <path> — After a market structure change that requires new or revised shock scenarios.
Healthcheck
GET /internal/health/taillosssimulator → green: Scenario library loaded, CLOB prices reachable, portfolio snapshot age < 30s; red: Scenario library missing, CLOB unreachable, or snapshot age > 60s
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 |