3.12 Mean-Reversion Sniper
Mean-Reversion Sniper fades price momentum in markets that have overreacted to recent information. It monitors markets where the best-ask for YES tokens exceeds price_threshold (default 0.80), computes a rolling z-score of the price change over a lookback window, and checks for order-book signals of a momentum reversal (aggressor side flips and large cancels on the heavy side). When z_score >= z_score_min and no active news cycle is detected by the NewsIngest event-density feed, the bot emits an IOC OrderIntent to sell YES tokens at or above price_threshold. An automatic time exit (time_exit_s) and stop-loss (stop_bps) control downside risk. This is a user-controlled execution tool; it does not predict outcomes or make performance claims.
v3 readiness
A bot is done when all four scores are. What does done mean?
1. Bot Identity
| Layer | Strategy Strategy |
|---|---|
| Bot class | Alpha Strategy |
| Authority | Trade |
| Status | BETA |
| Readiness | Limited live |
| Runs before | Risk guardrail pipeline |
| Runs after | Market scanner / opportunity feed + NewsIngest event-density feed |
| Applies to | Standard binary markets where price has moved above price_threshold (default 0.80) and z-score of last-N price change exceeds z_score_min, with no active news cycle on the market |
| Default mode | limited_live |
| User-visible | Advanced details only |
| Developer owner | Polytraders core — Strategy pod |
2. Purpose
Mean-Reversion Sniper fades price momentum in markets that have overreacted to recent information. It monitors markets where the best-ask for YES tokens exceeds price_threshold (default 0.80), computes a rolling z-score of the price change over a lookback window, and checks for order-book signals of a momentum reversal (aggressor side flips and large cancels on the heavy side). When z_score >= z_score_min and no active news cycle is detected by the NewsIngest event-density feed, the bot emits an IOC OrderIntent to sell YES tokens at or above price_threshold. An automatic time exit (time_exit_s) and stop-loss (stop_bps) control downside risk. This is a user-controlled execution tool; it does not predict outcomes or make performance claims.
3. Why This Bot Matters
Fading during an active news cycle
If a material news event is causing the price to move permanently higher, fading it produces a directional loss rather than a mean-reversion gain. NewsIngest event-density gate must be respected.
z-score computed on too short a lookback
Short lookbacks produce noisy z-scores that trigger on normal intraday price variation, leading to repeated small losses from false fades.
Stop-loss not configured or too wide
Without a stop-loss, an overreaction that continues (e.g., genuine resolution signal) produces an uncapped loss on the short YES position.
feeRateBps present on signed order (V1 pattern)
CTFExchangeV2 rejects orders with feeRateBps. Fees are operator-set at match time. The signed order must not contain this field.
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.
4. Required Polymarket Inputs
| Input | Source | Required? | Use |
|---|---|---|---|
| Price and z-score of last-N price change on target market | ws_market (CLOB WebSocket trade tape) | Yes | Confirm price >= price_threshold and z_score >= z_score_min before emitting fade intent. |
| Aggressor side flips and large cancel events | ws_market (order book updates) | Yes | Detect reversal signals: momentum fading when aggressor flips from buy-dominant to sell-dominant. |
| Top-of-book depth on YES side | clob_public | Yes | Size the IOC order to min(available_depth, max_position_usd). |
| Market open/closed/resolved status | clob_public | Yes | Skip markets that are closed, resolved, or approaching resolution (< 2h to close). |
5. Required Internal Inputs
| Input | Source | Required? | Use |
|---|---|---|---|
| KillSwitch active flag | KillSwitch | Yes | Abort all intent emission immediately if KillSwitch is active. |
| NewsIngest event-density window (don't fade during active news cycles) | internal (NewsIngest feed) | Yes | Block fade intent if NewsIngest signals an active material news cycle on the target market's entity. |
| Builder code bytes32 | internal config | Yes | Injected into builder field on every signed V2 OrderIntent. |
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|---|---|---|---|
| price_threshold | 0.8 | 0.9 | 0.95 | Minimum YES token price (0–1 pUSD) above which the bot considers a fade. Below this level the market is not overextended enough to fade. |
| z_score_min | 2.5 | 1.5 | 1.0 | Minimum z-score of the recent price move (normalised against rolling price volatility) required before emitting a fade intent. |
| stop_bps | 150 | 250 | 400 | Basis points above the fade entry price at which the bot automatically closes the position (stop-loss). If price continues up by stop_bps, the bot buys to cover. |
| time_exit_s | 120 | 200 | 300 | Maximum seconds the bot will hold a fade position before exiting regardless of price. Acts as a time-based stop to prevent open positions persisting into market resolution. |
7. Detailed Parameter Instructions
price_threshold
What it means
Minimum YES token price (0–1 pUSD) above which the bot considers a fade. Below this level the market is not overextended enough to fade.
Default
{ "price_threshold": 0.8 }
Why this default matters
0.80 captures markets where the crowd has pushed probability high enough that a reversion is plausible. Above 0.90 prices reflect near-certain resolution; fading near 0.95 hard cap is almost never correct.
Threshold logic
| Condition | Action |
|---|---|
| >= 0.80 | Eligible for fade evaluation; check z_score |
| 0.90–0.95 | WARN MEAN_REVERSION_HIGH_PRICE_THRESHOLD; market near resolution; fade only with high z_score |
| >= 0.95 (hard floor) | SKIP — MEAN_REVERSION_PRICE_TOO_HIGH; do not fade near resolution |
Developer check
if price >= params.hard: return skip('MEAN_REVERSION_PRICE_TOO_HIGH')
User-facing English
The market price was too close to certain resolution to fade safely.
z_score_min
What it means
Minimum z-score of the recent price move (normalised against rolling price volatility) required before emitting a fade intent.
Default
{ "z_score_min": 2.5 }
Why this default matters
2.5 sigma corresponds to a statistically unusual price move that is likely to partially revert. Below 1.5 sigma the move may be within normal daily variation; below 1.0 the bot will not fire.
Threshold logic
| Condition | Action |
|---|---|
| >= 2.5 | EMIT fade OrderIntent |
| 1.5–2.5 | WARN MEAN_REVERSION_Z_MARGINAL; emit at 50% size |
| < 1.0 (hard floor) | SKIP — MEAN_REVERSION_Z_TOO_LOW |
Developer check
if z_score < params.hard: return skip('MEAN_REVERSION_Z_TOO_LOW')
User-facing English
The recent price move was not statistically extreme enough to fade.
stop_bps
What it means
Basis points above the fade entry price at which the bot automatically closes the position (stop-loss). If price continues up by stop_bps, the bot buys to cover.
Default
{ "stop_bps": 150 }
Why this default matters
150 bps provides meaningful downside protection while allowing normal post-fade price noise. Above 250 bps the stop is too wide for a reversion trade; above 400 bps it is rejected.
Threshold logic
| Condition | Action |
|---|---|
| <= 150 bps | Normal stop-loss |
| 150–400 bps | WARN MEAN_REVERSION_WIDE_STOP; elevated risk per trade |
| > 400 bps | Reject config — PARAMETER_CHANGE_REQUIRES_APPROVAL |
Developer check
if params.stop_bps > params.hard: raise ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')
User-facing English
An automatic stop-loss is in place to limit losses if the price continues rising.
time_exit_s
What it means
Maximum seconds the bot will hold a fade position before exiting regardless of price. Acts as a time-based stop to prevent open positions persisting into market resolution.
Default
{ "time_exit_s": 120 }
Why this default matters
120 seconds limits exposure to a 2-minute window. Reversion trades either work quickly or not at all; holding longer on a market near resolution adds settlement risk.
Threshold logic
| Condition | Action |
|---|---|
| <= 120s | Normal time exit |
| 120–300s | WARN MEAN_REVERSION_LONG_TIME_EXIT; risk of open position at resolution |
| > 300s (hard ceiling) | Reject config — PARAMETER_CHANGE_REQUIRES_APPROVAL |
Developer check
if params.time_exit_s > params.hard: raise ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')
User-facing English
The position was automatically closed after the maximum holding time to avoid any unresolved exposure.
8. Default Configuration
{
"bot_id": "strat.mean_reversion_sniper",
"version": "2.1.0",
"mode": "limited_live",
"defaults": {
"price_threshold": 0.8,
"z_score_min": 2.5,
"stop_bps": 150,
"time_exit_s": 120
},
"locked": {
"price_threshold": {
"max": 0.95
},
"z_score_min": {
"min": 1.0
},
"stop_bps": {
"max": 400
},
"time_exit_s": {
"max": 300
}
}
}9. Implementation Flow
- Check KillSwitch active flag; if active, emit no OrderIntents; close any open fade positions.
- Subscribe to ws_market book and trade-tape streams for markets with best_ask_YES >= price_threshold.
- On each price tick: compute rolling z-score of price change over lookback window (default 20 ticks).
- Gate 1 — Price threshold: if best_ask_YES >= 0.95 (hard): skip MEAN_REVERSION_PRICE_TOO_HIGH.
- Gate 2 — NewsIngest: if active news cycle on market entity: skip MEAN_REVERSION_NEWS_ACTIVE.
- Gate 3 — Z-score: if z_score < 1.0 (hard): skip MEAN_REVERSION_Z_TOO_LOW (sampled 1/100).
- Gate 4 — Reversal signal: check aggressor side flip (taker-sell ratio rising) and large cancels on bid side.
- If z_score < 2.5 (warning): WARN MEAN_REVERSION_Z_MARGINAL; reduce size to 50% of max_position_usd.
- Fetch top-of-book YES ask depth; set orderSize = min(depth, max_position_usd) * sizeMultiplier.
- Emit OrderIntent: outcome=YES, side=sell, price=best_ask_YES, size_pUSD=orderSize, tif=IOC, builder={code, fee_bps:25}.
- Note: fees are operator-set at match time in V2 — feeRateBps is NOT on the signed order.
- Register position in state: entry_price, entry_time_ms, stop_price=entry_price+stop_bps/10000, exit_deadline_ms=now_ms()+time_exit_s*1000.
- Monitor position: if price rises above stop_price → emit buy IOC to close; if time > exit_deadline → emit buy IOC to close.
- Emit DecisionReport with intent_emitted=true, z_score, price_at_entry, reason MEAN_REVERSION_FADE_INITIATED.
10. Reference Implementation
Monitors markets for statistically unusual price spikes above price_threshold, checks NewsIngest event-density gate, and emits IOC sell OrderIntents when z_score >= z_score_min with reversal signals present. Manages open positions with stop-loss and time exit.
Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. Translate to TS/Python/Go/Rust.
FUNCTION onPriceTick(market_id, priceTick):
// --- 0. KillSwitch gate ---
ks = FETCH internal.killswitch.status
IF ks.active: CLOSE_OPEN_POSITIONS(); RETURN
// --- 1. Manage open positions (stop-loss + time exit) ---
pos = FETCH state.openPosition(market_id)
IF pos:
IF priceTick.best_ask >= pos.stop_price:
EMIT OrderIntent(side='buy', tif='IOC', size=pos.size, reason='MEAN_REVERSION_STOP_LOSS')
CLEAR state.openPosition(market_id)
RETURN
IF now_ms() >= pos.exit_deadline_ms:
EMIT OrderIntent(side='buy', tif='IOC', size=pos.size, reason='MEAN_REVERSION_TIME_EXIT')
CLEAR state.openPosition(market_id)
RETURN
// --- 2. Price threshold gate ---
price = priceTick.best_ask
IF price >= params.price_threshold_hard: // 0.95
EMIT DecisionReport(intent_emitted=false, reason='MEAN_REVERSION_PRICE_TOO_HIGH')
RETURN
IF price < params.price_threshold: // 0.80
RETURN // not in fade zone
// --- 3. NewsIngest gate ---
newsState = FETCH internal.newsingest.eventDensity(market_id)
IF newsState.active OR newsState.unavailable: // fail-closed
EMIT DecisionReport(intent_emitted=false, reason='MEAN_REVERSION_NEWS_ACTIVE')
RETURN
// --- 4. Z-score computation ---
tape = FETCH ws_market.tradeTape(market_id, lookback_ticks=20)
zScore = zScoreOfPriceChange(tape)
IF zScore < params.z_score_min_hard: // 1.0
IF random() < 0.01:
EMIT DecisionReport(intent_emitted=false, reason='MEAN_REVERSION_Z_TOO_LOW', z_score=zScore)
RETURN
// --- 5. Reversal signal check ---
IF NOT reversalSignalPresent(tape): // aggressor flip + large cancel on bid
RETURN
// --- 6. Size determination ---
sizeMultiplier = 0.5 IF zScore < params.z_score_min ELSE 1.0 // 2.5 warning
IF sizeMultiplier < 1.0: WARN('MEAN_REVERSION_Z_MARGINAL')
depth = FETCH clob_public.depth(market_id, side='YES')
orderSize = toPusdUnits(min(depth, params.max_position_usd) * sizeMultiplier)
// --- 7. Emit fade OrderIntent (sell YES, IOC, V2: no feeRateBps) ---
EMIT OrderIntent(
market_id = market_id, outcome = 'YES', side = 'sell',
price = price, size_pUSD = orderSize, tif = 'IOC', post_only = false,
builder = {code: internal.builder_code, fee_bps: 25}
)
// --- 8. Register position for stop/time-exit monitoring ---
SET state.openPosition(market_id, {
entry_price: price, size: orderSize,
stop_price: price + params.stop_bps / 10000,
exit_deadline_ms: now_ms() + params.time_exit_s * 1000
})
EMIT DecisionReport(intent_emitted=true, z_score=zScore,
price_at_entry=price, reason='MEAN_REVERSION_FADE_INITIATED')
SDK calls used
ws_market.subscribe('book', [market_id])ws_market.subscribe('trade_tape', [market_id])fetchClobPublic('/markets/' + market_id + '/orderbook')internal.newsingest.eventDensity(market_id)internal.killswitch.status()toPusdUnits(rawFloat)buildOrderTypedData(orderParams, { name: 'CTFExchange', version: '2', chainId: 137 })internal.builder_code
Complexity: O(1) per price tick; O(lookback_ticks) for z-score computation
11. Wire Examples
Input — what arrives on the wire
Price tick — YES spike to 0.847, z_score=3.1, no news cycle — ws_market
{
"market_id": "0xmrsniper00000000000000000000000000000000000000000000000000000001",
"best_ask_YES": "0.847",
"z_score_20tick": "3.1",
"taker_sell_ratio_5s": "0.65",
"news_active": false,
"depth_YES_pusd": "410.00",
"received_at_ms": 1746790200000
}
Output — what the bot emits
OrderIntent — fade sell YES (IOC, builder-attributed)
{
"intent_id": "oi_01HXMRS0000001A",
"trace_id": "tr_01HXMRS000TR001",
"market_id": "0xmrsniper00000000000000000000000000000000000000000000000000000001",
"outcome": "YES",
"side": "sell",
"price": "0.847",
"size_pUSD": "300.00",
"tif": "IOC",
"post_only": false,
"builder": {
"code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
"fee_bps": 25
},
"negrisk_aware": false,
"decision": {
"z_score": 3.1,
"price_at_entry": 0.847,
"stop_price": 0.862,
"exit_deadline_ms": 1746790320000,
"reasons": [
"MEAN_REVERSION_FADE_INITIATED"
]
},
"comment": "fees are operator-set at match time in V2 — feeRateBps is NOT on the signed order"
}
DecisionReport — skipped (news cycle active)
{
"report_id": "dr_01HXMRS999ZZZZ",
"bot_id": "strat.mean_reversion_sniper",
"market_id": "0xmrsniper00000000000000000000000000000000000000000000000000000001",
"intent_emitted": false,
"z_score": 2.8,
"reasons": [
"MEAN_REVERSION_NEWS_ACTIVE"
],
"sampled": false,
"evaluated_at_ms": 1746790201000
}12. Decision Logic
APPROVE
price >= price_threshold, z_score >= z_score_min, no active news cycle, reversal signal present, KillSwitch inactive. Emit sell IOC OrderIntent.
RESHAPE_REQUIRED
Not applicable — strat bots emit OrderIntents; reshaping is handled by the downstream Risk guardrail pipeline.
REJECT
price >= 0.95 (hard floor); z_score < 1.0; active news cycle; KillSwitch active; stale feed. Emit DecisionReport intent_emitted=false.
WARNING_ONLY
z_score between 1.0 and 2.5 triggers MEAN_REVERSION_Z_MARGINAL warn and 50% size reduction.
13. Standard Decision Output
This bot returns a OrderIntent object. See OrderIntent schema.
{
"intent_id": "oi_01HXMRS0000001A",
"trace_id": "tr_01HXMRS000TR001",
"market_id": "0xmrsniper00000000000000000000000000000000000000000000000000000001",
"outcome": "YES",
"side": "sell",
"price": "0.847",
"size_pUSD": "300.00",
"tif": "IOC",
"post_only": false,
"builder": {
"code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
"fee_bps": 25
},
"negrisk_aware": false,
"decision": {
"z_score": 3.1,
"price_at_entry": 0.847,
"stop_price": 0.862,
"exit_deadline_ms": 1746790320000,
"reasons": [
"MEAN_REVERSION_FADE_INITIATED"
]
},
"comment": "fees are operator-set at match time in V2 \u2014 feeRateBps is NOT on the signed order"
}14. Reason Codes
| Code | Severity | Meaning | Action | User-facing message |
|---|---|---|---|---|
MEAN_REVERSION_FADE_INITIATED | INFO | z_score >= z_score_min, price >= price_threshold, news gate clear, reversal signal present. Fade sell IOC emitted. | Emit sell IOC OrderIntent; register position for stop/time-exit monitoring. | A short sell was placed to capture an expected price reversal. |
MEAN_REVERSION_Z_TOO_LOW | INFO | z_score is below the 1.0 hard floor. Price move is within normal variation; no fade warranted. | Skip; emit sampled DecisionReport. | The recent price move was not extreme enough to fade. |
MEAN_REVERSION_Z_MARGINAL | WARN | z_score is between 1.0 and 2.5. Fade is marginal; size reduced to 50%. | Emit IOC at 50% size; log warning. | A moderate price spike was detected. A smaller fade order was placed. |
MEAN_REVERSION_PRICE_TOO_HIGH | HARD_REJECT | Price >= 0.95 hard floor. Market is near certain resolution; fading is prohibited. | Skip; no OrderIntent; emit DecisionReport. | The market price is too close to certain resolution to fade safely. |
MEAN_REVERSION_NEWS_ACTIVE | HARD_REJECT | NewsIngest signals an active material news cycle on this market's entity. Fading is blocked. | Skip; no OrderIntent; emit DecisionReport. | An active news event is affecting this market. No fade order was placed. |
MEAN_REVERSION_STOP_LOSS | WARN | Price rose above stop_price (entry + stop_bps/10000). Stop-loss triggered; position closed. | Emit buy IOC to close; emit DecisionReport. | The price continued to rise. The stop-loss automatically closed the position. |
MEAN_REVERSION_TIME_EXIT | INFO | Holding period exceeded time_exit_s. Time exit triggered; position closed. | Emit buy IOC to close; emit DecisionReport. | The maximum holding time elapsed. The position was automatically closed. |
MEAN_REVERSION_HIGH_PRICE_THRESHOLD | WARN | price_threshold config is above 0.90 (warning). Fading near-resolution markets carries elevated risk. | Allow but log warning; require higher z_score. | |
STALE_MARKET_DATA | HARD_REJECT | ws_market feed or NewsIngest feed is stale or unavailable. | Skip; no OrderIntent emitted. | Market data was too old to act on safely. |
KILL_SWITCH_ACTIVE | HARD_REJECT | Global kill switch is active. | Close open positions; no new OrderIntents. | Trading is currently paused. |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|---|---|---|---|
polytraders_strat_mrsniper_decisions_total | counter | count | verdict, reason_code | Total evaluation cycles by intent_emitted and reason code. |
polytraders_strat_mrsniper_z_score | histogram | sigma | Distribution of z-scores at evaluation time across all checked markets. | |
polytraders_strat_mrsniper_position_hold_s | histogram | seconds | exit_reason | Duration of open fade positions by exit reason (stop_loss, time_exit, profit_take). |
polytraders_strat_mrsniper_news_gate_blocks_total | counter | count | market_id | Number of fade attempts blocked by NewsIngest gate. |
polytraders_strat_mrsniper_intents_emitted_total | counter | count | side | Total OrderIntents emitted by side (sell=fade open, buy=close). |
polytraders_strat_mrsniper_eval_latency_ms | histogram | milliseconds | Wall-clock time from price tick to OrderIntent emit. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|---|---|---|
MrSniperHighNewsGateBlockRate | rate(polytraders_strat_mrsniper_news_gate_blocks_total[10m]) > 10 | warn | #runbook-mrsniper-news-gate |
MrSniperHighStopLossRate | rate(polytraders_strat_mrsniper_position_hold_s_bucket{exit_reason='stop_loss'}[15m]) / rate(polytraders_strat_mrsniper_position_hold_s_count[15m]) > 0.5 | warn | #runbook-mrsniper-stop-loss |
MrSniperStaleFeed | rate(polytraders_strat_mrsniper_decisions_total{reason_code='STALE_MARKET_DATA'}[5m]) > 0.1 | warn | #runbook-mrsniper-stale-feed |
MrSniperKillSwitchBlocking | rate(polytraders_strat_mrsniper_decisions_total{reason_code='KILL_SWITCH_ACTIVE'}[1m]) > 0 | page | #runbook-killswitch |
Dashboards
- Grafana — Strategy / MeanReversionSniper z-score distribution and fade frequency
- Grafana — Strategy / MeanReversionSniper position hold times and exit reasons
16. Developer Reporting
{
"bot_id": "strat.mean_reversion_sniper",
"market_id": "0xmrsniper00000000000000000000000000000000000000000000000000000001",
"price_at_entry": 0.847,
"z_score": 3.1,
"price_threshold": 0.8,
"news_gate_passed": true,
"reversal_signal": true,
"order_size_pusd": 300.0,
"stop_price": 0.862,
"exit_deadline_ms": 1746790320000,
"intent_emitted": true,
"reason": "MEAN_REVERSION_FADE_INITIATED",
"emitted_at_ms": 1746790200000
}17. Plain-English Reporting
| Situation | User-facing explanation |
|---|---|
| Mean-reversion fade initiated | The market price spiked unusually high with no active news to justify it. A short sell was placed to capture the expected return toward a more typical price level. |
| No fade — news cycle active | A material news event is currently affecting this market. Fading the price movement during active news would be risky. No order was placed. |
| No fade — z-score too low | The recent price move was not statistically unusual enough to justify a fade trade. No order was placed. |
| Position closed — stop-loss triggered | The market price continued to rise after the fade was placed. The stop-loss automatically closed the position to limit losses. |
| Position closed — time exit | The maximum holding time elapsed. The position was closed automatically to avoid any unresolved exposure. |
18. Failure-Mode Block
| main_failure_mode | Fading a genuine resolution signal: the market moves to 0.90+ because the underlying event has materially resolved (not overreaction), and the short YES position incurs a growing loss until the stop-loss triggers. |
|---|---|
| false_positive_risk | NewsIngest feed is delayed; the bot fades a genuine news-driven price move because the event-density signal has not yet registered, resulting in a loss. |
| false_negative_risk | z_score_min set too conservatively misses genuine overreaction episodes, especially on illiquid markets where z-scores are noisier. |
| safe_fallback | If ws_market feed is stale (last_seen > 5s) or NewsIngest feed is unavailable, emit STALE_MARKET_DATA and skip without emitting any OrderIntent. If NewsIngest is unavailable, treat as news-active (conservative gate). |
| required_dependencies | ws_market book and trade-tape stream, clob_public market endpoint (status + depth), NewsIngest event-density feed (internal), KillSwitch active flag, internal builder code |
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|---|---|---|
STALE_WS_FEED | Pause ws_market WebSocket; let last_seen age beyond 5s | Automatic on WebSocket reconnect. | |
NEWS_GATE_UNAVAILABLE | Disable NewsIngest internal feed | Automatic when NewsIngest feed is restored. | |
STOP_LOSS_TRIGGER | Set mock price to entry_price + stop_bps/10000 + 0.001 after fade is placed | Automatic; position closed. | |
TIME_EXIT_TRIGGER | Advance system clock past exit_deadline_ms with open position | Automatic; position closed. | |
KILL_SWITCH_ON | Set killswitch.active=true with open fade position | Automatic on manual KillSwitch reset. |
20. State & Persistence
Cold-start recovery
On cold start, open positions are re-read from clob_auth. z-score cache is rebuilt from first ws_market tape ticks.
21. Concurrency & Idempotency
| Aspect | Specification |
|---|---|
| Execution model | single-threaded event loop |
| Max in-flight | 20 |
| Idempotency key | intent_id |
| Per-call timeout (ms) | 150 |
| Backpressure strategy | drop oldest pending price tick per market_id when queue depth > 5 |
| Locking / mutual exclusion | per-market_id mutex for open position state read/write |
22. Dependencies
Depends on (must run first)
| Bot | Why | Contract |
|---|---|---|
| risk.kill_switch | Checked first; closes open positions and blocks new intent emission when active. |
Emits to (downstream consumers)
| Bot | Why | Contract |
|---|---|---|
| risk.portfolio_guard | ||
| gov.builder_attribution |
External services
| Service | Endpoint | SLA assumed | On failure |
|---|---|---|---|
| Polymarket CLOB WebSocket (ws_market) | best-effort | ||
| NewsIngest (internal feed) | internal SLA | ||
| Polymarket CLOB public API (depth) | 99.9% |
23. Security Surfaces
On-chain contract calls
| Contract | Method | Network | Effect |
|---|---|---|---|
CTFExchangeV2 | | polygon |
Abuse vectors considered
- Adversary manufactures a price spike to trigger the fade, then reverses to profit from the short position
- Delayed NewsIngest feed allows fading during a genuine news event
- Open fade position exploited if stop-loss level is known
Mitigations
- z_score threshold filters out manufactured spikes lacking sustained momentum
- NewsIngest unavailability treated as active-news (fail-closed); no fade without confirmed clear news gate
- Stop-loss level computed from entry price + stop_bps; not exposed externally
- V2 order timestamp(ms) invalidates replayed signed orders
- IOC orders do not rest on the book; not visible to adversaries post-emission
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 | no |
| Multi-chain ready | no |
| SDK used | py-clob-client-v2 |
| Settlement contract | CTFExchangeV2 |
| Notes | Executes taker IOC orders to fade price momentum on standard binary markets. feeRateBps is not present on any signed order. Builder code injected on every intent. |
API surfaces declared
Networks supported
25. Versioning & Migration
| Field | Value |
|---|---|
| spec | 2.0.0 |
| implementation | 2.1.0 |
| schema | 2 |
| released | 2026-04-28 |
Migration history
| Date | From | To | Reason | Action taken |
|---|---|---|---|---|
| 2026-04-28 | v1 (USDC.e, feeRateBps on signed order) | v2 (pUSD, fees operator-set at match time) | CLOB V2 cutover | Switched to py-clob-client-v2. Removed feeRateBps from signed order construction. Updated collateral denomination to pUSD. Injected builder field (bytes32). EIP-712 Exchange domain version updated '1' to '2'. NewsIngest event-density feed integration updated to V2 internal bus schema. |
26. Acceptance Tests
Unit Tests
| Test | Setup | Expected result |
|---|---|---|
| Emit fade when z_score=3.1, price=0.847, no news cycle, reversal signal present | price_threshold=0.80, z_score_min=2.5, stop_bps=150, time_exit_s=120 | Sell IOC OrderIntent emitted; DecisionReport intent_emitted=true, reason=MEAN_REVERSION_FADE_INITIATED |
| Skip when price >= hard floor (0.96) | price=0.96 | No OrderIntent; reason=MEAN_REVERSION_PRICE_TOO_HIGH |
| Skip when z_score < 1.0 hard floor | z_score=0.8 | No OrderIntent; sampled DecisionReport reason=MEAN_REVERSION_Z_TOO_LOW |
| Skip when NewsIngest signals active news cycle | newsingest.active=true for market entity | No OrderIntent; reason=MEAN_REVERSION_NEWS_ACTIVE |
| Reduce size 50% when z_score marginal (1.8) | z_score=1.8, z_score_min=2.5, max_position_usd=300 | OrderIntent emitted with size=150; WARN MEAN_REVERSION_Z_MARGINAL |
| Stop-loss triggers when price rises stop_bps above entry | entry_price=0.847, stop_bps=150; mock price moves to 0.862 | Buy IOC OrderIntent emitted to close position; DecisionReport reason=MEAN_REVERSION_STOP_LOSS |
Integration Tests
| Test | Expected result |
|---|---|
| Full cycle: price spike detected → z_score computed → news gate passed → sell IOC OrderIntent submitted | Order has builder.code (bytes32), no feeRateBps, side=sell, tif=IOC, EIP-712 domain version '2' |
| Time exit: position auto-closed after time_exit_s elapses with no stop-loss trigger | Buy IOC OrderIntent emitted at time_exit_s; DecisionReport reason=MEAN_REVERSION_TIME_EXIT |
Property Tests
| Property | Required behaviour |
|---|---|
| Bot never holds a fade position beyond time_exit_s seconds | Always true |
| feeRateBps never present on any signed OrderIntent | Always true — V2 fees are operator-set at match time |
| Bot never fades when NewsIngest feed is unavailable (fail-closed on news gate) | Always true |
27. Operational Runbook
Mean-Reversion Sniper incidents are typically high stop-loss rates (genuine resolution signals being faded), NewsIngest feed outages (causing no-fade lockout), or stale ws_market feeds. High stop-loss rates require immediate review of market conditions.
On-call actions
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|---|---|---|---|
MrSniperHighNewsGateBlockRate | ||||
MrSniperHighStopLossRate | ||||
MrSniperStaleFeed | ||||
MrSniperKillSwitchBlocking |
Manual overrides
——
Healthcheck
GET /internal/health/mean-reversion-sniper -> 200 if ws_market feed last_seen < 5s, NewsIngest feed live, KillSwitch inactive, Redis reachable.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 |
|---|---|---|
| All unit tests pass including news gate fail-closed and stop-loss trigger | CI test run | 100% pass |
| feeRateBps absence verified; tif=IOC verified in integration test | Integration test asserting V2 order schema | Pass |
Promote to Limited live
| Gate | How measured | Threshold |
|---|---|---|
| p99 eval latency < 150ms over 24h | polytraders_strat_mrsniper_eval_latency_ms histogram | p99 < 150ms |
| Stop-loss rate < 30% of all closed positions over 48h shadow run | polytraders_strat_mrsniper_position_hold_s_bucket exit_reason histogram | < 30% |
Promote to General live
| Gate | How measured | Threshold |
|---|---|---|
| E2E: price spike detected → z_score computed → news gate clear → sell IOC OrderIntent submitted on Polygon testnet | E2E test | Pass |
| Time exit and stop-loss scenarios verified in integration test | Integration test | Pass |
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 |