1.17 MarketHaltDetector
Watches for market-level halt conditions across Polymarket — wide-spread blowouts, missing best bid/ask, locked or crossed books, and sudden order-rate collapse. When any halt condition fires, MarketHaltDetector quarantines the affected market_id so no new OrderIntent for that market can pass the Risk pipeline. It does not pause the whole system — only the affected market. Cleared automatically once conditions normalise for a configurable cool-off window, or manually via the Admin UI.
v3 readiness
A bot is done when all four scores are. What does done mean?
risk.markethaltguard
Maps to spec page market_halt_detector. SEARCH_SPACE declared. Fixture pack pending.
Source: @polytraders/bots · src/risk/markethaltguard.js · Impl 11/15 · Backtest 3/4
1. Bot Identity
| Layer | Risk Risk |
|---|---|
| Bot class | Guardrail |
| Authority | PauseReject |
| Status | PLANNED |
| Readiness | Spec ready |
| Runs before | risk.killswitch, exec.smart_router |
| Runs after | intel.orderflowanalyzer |
| Applies to | Continuous |
| Default mode | shadow |
| User-visible | Yes |
| Developer owner | Risk pod |
Operational profile
| Ownership | Risk pod · on-call risk-oncall · #polytraders-risk · escalates to Head of Risk · P1 |
|---|---|
| Latency budget | p50: 5ms · p99: 20ms |
| Modes supported | offshadowadvisoryenforcedquarantine |
| Data freshness | max_market_data_age_ms=1500 · max_orderbook_age_ms=1500 · on stale → Treat as halted (fail closed). |
| Human override | yes · by Risk on-call · logs RISK_MARKET_HALT_OVERRIDE · time-bound: 60 minutes max · scope: Single market_id · single approver |
2. Purpose
Watches for market-level halt conditions across Polymarket — wide-spread blowouts, missing best bid/ask, locked or crossed books, and sudden order-rate collapse. When any halt condition fires, MarketHaltDetector quarantines the affected market_id so no new OrderIntent for that market can pass the Risk pipeline. It does not pause the whole system — only the affected market. Cleared automatically once conditions normalise for a configurable cool-off window, or manually via the Admin UI.
3. Why This Bot Matters
Submitting orders into a halted book
Orders sent into a market with no two-sided liquidity end up either rejected by the CLOB or filled at runaway prices once the book reopens.
Treating wide-spread events as normal
A market with a 30%+ inside spread is structurally untradeable; trading it anyway invites massive slippage and unfair fills.
Manual halt management
Without automated detection, ops staff must monitor every market by hand — which does not scale past a handful of markets.
4. Required Polymarket Inputs
| Input | Source | Required? | Use |
|---|---|---|---|
| Best bid/ask per market_id | WebSocket | Yes | Detect missing or one-sided book; compute live spread. |
| Last-trade timestamp per market_id | CLOB | Yes | Detect sudden trade-rate collapse (no trades for trades_silent_ms with non-empty book). |
5. Required Internal Inputs
| Input | Source | Required? | Use |
|---|---|---|---|
| Recent OrderBookSnapshot history | intel.orderflowanalyzer | Yes | Smoothed spread and depth context for halt-vs-noise classification. |
| Active strategy → market_id map | OrderLifecycleManager | No | Decide which strategies to notify when a halt fires. |
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|---|---|---|---|
| halt_spread_pct | 30 | 15 | 30 | Spread percentage at which the market is considered halted. |
| trades_silent_ms | 60000 | 30000 | 60000 | Maximum allowed silent window with no recorded trades while the book is non-empty. |
| cooloff_ms | 120000 | — | — | Cool-off window the market must remain healthy before the halt is auto-cleared. |
| min_depth_usd | 250 | 100 | 250 | Minimum aggregate top-of-book depth (USD) below which the book is considered too thin. |
7. Detailed Parameter Instructions
halt_spread_pct
What it means
Spread percentage at which the market is considered halted.
Default
{ "halt_spread_pct": 30 }
Why this default matters
30% inside spread is well outside any tradeable regime on Polymarket binary markets.
Threshold logic
| Condition | Action |
|---|---|
| ≤ 15% | No action |
| 15–30% | WARN — log only |
| > 30% | QUARANTINE market_id |
Developer check
if (spreadPct(book) > p.halt_spread_pct) halt.activate(marketId, 'WIDE_SPREAD');
User-facing English
Trading was paused on this market because the price gap got too wide for safe orders.
trades_silent_ms
What it means
Maximum allowed silent window with no recorded trades while the book is non-empty.
Default
{ "trades_silent_ms": 60000 }
Why this default matters
60 seconds with a populated book and no prints is a strong signal of stale or stuck data.
Threshold logic
| Condition | Action |
|---|---|
| ≤ 30s | No action |
| 30–60s | WARN |
| > 60s | QUARANTINE market_id |
Developer check
if (now - lastTradeMs > p.trades_silent_ms && bookHasLiquidity()) halt.activate(marketId, 'TRADE_SILENCE');
User-facing English
Trading was paused on this market because no trades have happened for a long time.
cooloff_ms
What it means
Cool-off window the market must remain healthy before the halt is auto-cleared.
Default
{ "cooloff_ms": 120000 }
Why this default matters
Two minutes of healthy book + recent trades is a safe minimum before resuming.
Threshold logic
| Condition | Action |
|---|---|
| Healthy < 120s | Stay quarantined |
| Healthy ≥ 120s | Auto-clear halt |
Developer check
if (healthyFor(marketId) > p.cooloff_ms) halt.clear(marketId);
User-facing English
Trading on this market resumed automatically after conditions stabilised.
min_depth_usd
What it means
Minimum aggregate top-of-book depth (USD) below which the book is considered too thin.
Default
{ "min_depth_usd": 250 }
Why this default matters
Below $250 of top-of-book depth on a binary market, even small orders move the price meaningfully.
Threshold logic
| Condition | Action |
|---|---|
| > $250 | OK |
| $100–$250 | WARN |
| < $100 | QUARANTINE market_id |
Developer check
if (topDepthUsd(book) < p.min_depth_usd) halt.activate(marketId, 'THIN_BOOK');
User-facing English
Trading was paused on this market because there was not enough money on the book to fill orders safely.
8. Default Configuration
{
"halt_spread_pct": 30,
"trades_silent_ms": 60000,
"cooloff_ms": 120000,
"min_depth_usd": 250
}9. Implementation Flow
— not yet authored —
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.
for each book_tick:
if spread_pct(book) > p.halt_spread_pct: halt(market_id, 'WIDE_SPREAD'); continue
if now - last_trade[market_id] > p.trades_silent_ms and depth_usd(book) > 0: halt(market_id, 'TRADE_SILENCE'); continue
if depth_usd_top(book) < p.min_depth_usd: halt(market_id, 'THIN_BOOK'); continue
if all_rules_clean_for(market_id, p.cooloff_ms): clear(market_id)
for each OrderIntent o:
if is_halted(o.market_id): emit RiskVote(REJECT, RISK_MARKET_HALT); else PASS11. Wire Examples
Input — what arrives on the wire
{
"intent_id": "intent_001",
"market_id": "0xabc",
"side": "BUY",
"size_usd": 100
}
Output — what the bot emits
{
"vote": "REJECT",
"reason_code": "RISK_MARKET_HALT",
"explain": "Halted: inside spread 41% > 30% threshold."
}12. Decision Logic
APPROVE
On each OrderBookSnapshot tick → re-evaluate halt rules per market.
RESHAPE_REQUIRED
This bot does not reshape orders.
REJECT
On each OrderIntent for a quarantined market_id → REJECT with RISK_MARKET_HALT.
WARNING_ONLY
Emit OperationsReport on halt activation, halt clearance, and threshold WARNs.
13. Standard Decision Output
This bot returns a RiskVote object. See RiskVote schema.
{
"vote": "REJECT",
"market_id": "0xabc",
"reason_code": "RISK_MARKET_HALT",
"explain": "Halted: inside spread 41% > 30% threshold for 18s.",
"ts_ms": 1715260000000
}14. Reason Codes
| Code | Severity | Meaning | Action | User-facing message |
|---|---|---|---|---|
RISK_MARKET_HALT | P1 | Risk Market Halt | See decision output and developer log for context. | Trading was paused on this market because conditions made it unsafe to place orders. |
RISK_MARKET_HALT_WARN | P1 | Risk Market Halt Warn | See decision output and developer log for context. | Trading was paused on this market because conditions made it unsafe to place orders. |
RISK_MARKET_HALT_CLEARED | P1 | Risk Market Halt Cleared | See decision output and developer log for context. | Trading was paused on this market because conditions made it unsafe to place orders. |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|---|---|---|---|
halts_active_total | counter | event | market_id, reason_code | Halts active total. |
halt_activation_count | counter | event | market_id, reason_code | Halt activation count. |
halt_clear_count | counter | event | market_id, reason_code | Halt clear count. |
rejects_with_market_halt | counter | event | market_id, reason_code | Rejects with market halt. |
Dashboards
- 1.17 overview dashboard
16. Developer Reporting
"Per halt activation: market_id, rule (WIDE_SPREAD/TRADE_SILENCE/THIN_BOOK), measured value, threshold, ts_ms, expected cooloff_until_ms."17. Plain-English Reporting
| Situation | User-facing explanation |
|---|---|
| When this bot acts | Trading was paused on this market because conditions made it unsafe to place orders. |
| When this bot acts | Trading on this market will resume automatically once the market behaves normally again. |
18. Failure-Mode Block
| main_failure_mode | False quarantine on transient feed glitches that recover within seconds. |
|---|---|
| false_positive_risk | Brief WebSocket reconnects can mimic a wide spread; mitigation: require condition to hold for a minimum sustained window before halting. |
| false_negative_risk | A market that quietly stops printing but keeps a tight book may not trip any rule; mitigation: cross-check with Polymarket public last-trade endpoint on a slow timer. |
| safe_fallback | On internal failure, halt every market this bot is responsible for — fail closed, never fail open. |
| required_dependencies | — |
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|---|---|---|
Inject a sustained 35% inside spread for 30s and assert halt activates | Inject a sustained 35% inside spread for 30s and assert halt activates. | Bot detects within its latency budget and emits the corresponding reason code. | Remove the injected fault; bot returns to healthy state within one debounce window. |
Inject a 5-minute trade silence and assert halt activates | Inject a 5-minute trade silence and assert halt activates. | Bot detects within its latency budget and emits the corresponding reason code. | Remove the injected fault; bot returns to healthy state within one debounce window. |
Inject feed reconnect noise and assert no spurious halt within debounce window | Inject feed reconnect noise and assert no spurious halt within debounce window. | Bot detects within its latency budget and emits the corresponding reason code. | Remove the injected fault; bot returns to healthy state within one debounce window. |
20. State & Persistence
Per market_id: { halted: bool, halted_since_ms, last_rule, healthy_since_ms }. Persisted in fast key-value store; survives restart.
State stores
| Name | Kind | Key | Value shape | TTL | Durability |
|---|---|---|---|---|---|
market_halt_detector_state | in-memory + fast KV mirror | market_id | Per market_id: { halted: bool, halted_since_ms, last_rule, healthy_since_ms }. Persisted in fast key-value store; surviv | 24h | crash-safe via KV mirror |
Cold-start recovery
Cold-start hydrates from fast KV; missing keys default to safe fallback.
On restart
All in-flight decisions are re-evaluated; no bot decision is trusted across restart without re-emit.
21. Concurrency & Idempotency
| Aspect | Specification |
|---|---|
| Execution model | Single-writer per market_id. Idempotent: same OrderIntent re-evaluated returns same RiskVote while halt is active. |
| Max in-flight | 32 |
| Idempotency key | order_intent_id |
| Replay-safe | True |
| Deduplication | By idempotency_key within a 60s window. |
| Ordering guarantees | Per-market_id FIFO; cross-market unordered. |
| Per-call timeout (ms) | 250 |
| Backpressure strategy | Bounded queue; oldest-dropped with metric increment when full. |
| Locking / mutual exclusion | Per-market_id mutex; no global locks. |
22. Dependencies
Depends on (must run first)
| Bot | Why | Contract |
|---|---|---|
| intel.orderflowanalyzer |
Emits to (downstream consumers)
| Bot | Why | Contract |
|---|---|---|
| risk.killswitch | ||
| exec.smart_router |
Requires (graph.requires)
Required before (graph.required_before)
risk.killswitch exec.smart_router
| Consumes | OrderBookSnapshot TradeTick |
|---|---|
| Emits | RiskVote OperationsReport |
| Blocks orders | yes |
23. Security Surfaces
Reads CLOB WebSocket; emits RiskVote internally only.
Signing surface
None — bot does not sign or submit.
Abuse vectors considered
- Admin UI 'force clear' endpoint requires Risk role + audit log entry.
Mitigations
- Rate-limit per source
- Audit-log every override
- Require role-based authz on admin paths
24. Polymarket V2 Compatibility
| Aspect | Value |
|---|---|
| CLOB version | V2 |
| Collateral asset | pUSD |
| EIP-712 Exchange domain version | 2 |
| Aware of builderCode field | yes |
| Aware of negative-risk markets | yes |
| Multi-chain ready | yes |
| SDK used | Polymarket CLOB V2 SDK |
| Settlement contract | CTFExchangeV2 |
| Notes | Reads CLOB V2 normalised OrderBookSnapshot; no on-chain calls. |
25. Versioning & Migration
| Field | Value |
|---|---|
| current | 0.1.0 |
| contract_version | 1.0.0 |
| last_breaking_change | none |
| deprecation_window_days | 30 |
26. Acceptance Tests
Unit Tests
| Test | Setup | Expected result |
|---|---|---|
| Spread computation matches expected for synthetic books. | Synthetic fixture per template. | Behaviour matches the rule described in the test name. |
| Cool-off counter resets on any rule re-trip. | Synthetic fixture per template. | Behaviour matches the rule described in the test name. |
Integration Tests
| Test | Expected result |
|---|---|
| Halt activates and clears against a recorded CLOB feed with an injected 90-second silent window. | End-to-end behaviour matches the spec without manual intervention. |
Property Tests
| Property | Required behaviour |
|---|---|
| For any sequence of book ticks, halt-active state is monotonic per market until cool-off completes. | Always true across all generated inputs. |
27. Operational Runbook
Symptoms: many simultaneous halts → check upstream feed health. Single stuck market → manually clear via Admin UI then investigate the persistent rule violation.
On-call actions
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|---|---|---|---|
1.17_anomaly | Open the bot's reporting page and confirm the alert is real (not a metric hiccup). | Inspect developer log entries for the affected market_id over the last 30 minutes. | Force-clear via Admin UI if the rule is clearly stale; otherwise leave engaged and notify owner. | Risk pod |
Manual overrides
polytraders bot pause 1.17— Disables the bot's enforcement layer; downstream consumers fall back to safe defaults.
Healthcheck
GET /healthz/market_halt_detector → 200 if last successful evaluation < 60s ago.28. Promotion Gates
A bot does not advance to the next readiness state until every gate below is green. Gates are observable from production data — no subjective sign-off.
Promote to Shadow
| Gate | How measured | Threshold |
|---|---|---|
| Stub | deterministic against fixture book ticks. | Documented threshold met for the full window. |
Promote to Limited live
| Gate | How measured | Threshold |
|---|---|---|
| Shadow | 7 days against live CLOB; halt activations logged but not enforced. | Documented threshold met for the full window. |
| Advisory | 7 days; halts visible to ops dashboard. | Documented threshold met for the full window. |
Promote to General live
| Gate | How measured | Threshold |
|---|---|---|
| Enforced | requires Risk Lead sign-off. | Documented threshold met for the full window. |
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 |