1. Bot Identity
| Layer | Strategy Strategy |
|---|
| Bot class | Alpha Strategy |
|---|
| Authority | Trade |
|---|
| Status | PLANNED |
|---|
| Readiness | Spec started |
|---|
| Runs before | Risk guardrail pipeline |
|---|
| Runs after | Volatility monitor / Observation bus |
|---|
| Applies to | Polymarket binary event markets with elevated realised volatility and thick order books, where posting inside the spread earns rebates from wide-spread trading activity |
|---|
| Default mode | shadow_only |
|---|
| User-visible | Advanced details only |
|---|
| Developer owner | Polytraders core — Strategy pod |
|---|
2. Purpose
VolatilityHarvest quotes inside the spread on volatile Polymarket event markets with thick books, collecting maker rebates while managing inventory skew. It enters when realised volatility exceeds min_realised_vol, quotes inside the current spread by quote_inside_bps, and limits total position via max_inventory_skew. After a losing quote it cools off for cool_off_after_loss seconds.
3. Why This Bot Matters
Inventory accumulates in one direction
Sustained directional order flow leaves the bot long or short at a mispriced level; inventory risk grows until the market moves against the accumulated position.
Volatility drops after entry
When realised volatility falls, spread income shrinks but position remains; the strategy is no longer compensated for carrying risk.
Cool-off period ignored after loss
Immediately re-quoting after an adverse fill can compound losses if the adverse move is continuing.
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 |
|---|
| min_realised_vol | 0.05 | 0.03 | 0.01 | Minimum 1h realised volatility (annualised) required before quoting inside the spread. |
| quote_inside_bps | 50 | 20 | 5 | How many bps inside the current best bid/ask the bot posts its maker quote. |
| max_inventory_skew | 0.3 | 0.5 | 0.7 | Maximum fraction of total position that can be on one side (YES or NO) before the bot stops quoting that side. |
| cool_off_after_loss | 60 | 30 | 0 | Seconds to pause quoting on a market after an adverse fill that moves the mark against the position. |
7. Detailed Parameter Instructions
min_realised_vol
What it means
Minimum 1h realised volatility (annualised) required before quoting inside the spread.
Default
{ "min_realised_vol": 0.05 }
Why this default matters
0.05 ensures there is enough price movement to generate meaningful order flow and rebates.
Threshold logic
| Condition | Action |
|---|
| >= 0.05 | Allow spread quoting |
| 0.03–0.05 | WARN VH_LOW_VOL; halve quote size |
| < 0.01 | HARD_REJECT VH_VOL_BELOW_FLOOR |
Developer check
if realised_vol < params.hard: return skip('VH_VOL_BELOW_FLOOR')
User-facing English
Market volatility was too low for spread harvesting.
quote_inside_bps
What it means
How many bps inside the current best bid/ask the bot posts its maker quote.
Default
{ "quote_inside_bps": 50 }
Why this default matters
50 bps inside provides meaningful price improvement while still earning the maker rebate.
Threshold logic
| Condition | Action |
|---|
| >= 50 bps inside | Standard inside quote |
| 20–50 bps | WARN VH_TIGHT_INSIDE_QUOTE |
| < 5 bps | HARD_REJECT — too close to crossing |
Developer check
if params.quote_inside_bps < params.hard: return skip('VH_QUOTE_TOO_TIGHT')
User-facing English
The inside quote was too close to the crossing point.
max_inventory_skew
What it means
Maximum fraction of total position that can be on one side (YES or NO) before the bot stops quoting that side.
Default
{ "max_inventory_skew": 0.3 }
Why this default matters
0.30 limits directional exposure from accumulated inventory.
Threshold logic
| Condition | Action |
|---|
| <= 0.30 | Quote both sides normally |
| 0.50–0.70 | WARN VH_HIGH_SKEW; stop quoting skewed side |
| > 0.70 | HARD_REJECT VH_INVENTORY_LIMIT — stop all quoting |
Developer check
if abs(inventory_skew) > params.hard: return skip('VH_INVENTORY_LIMIT')
User-facing English
Position skew limit reached; quoting paused until inventory rebalances.
cool_off_after_loss
What it means
Seconds to pause quoting on a market after an adverse fill that moves the mark against the position.
Default
{ "cool_off_after_loss": 60 }
Why this default matters
60s cool-off prevents immediately compounding losses if directional flow continues.
Threshold logic
| Condition | Action |
|---|
| >= 60s | Standard cool-off |
| 30–60s | WARN VH_SHORT_COOLOFF |
| = 0s | No cool-off — not recommended |
Developer check
if in_cooloff(market_id): return skip('VH_COOLOFF_ACTIVE')
User-facing English
A brief pause is in effect after an adverse fill.
8. Default Configuration
{
"bot_id": "strat.volatilityharvest",
"version": "0.1.0",
"mode": "shadow_only",
"defaults": {
"min_realised_vol": 0.05,
"quote_inside_bps": 50,
"max_inventory_skew": 0.3,
"cool_off_after_loss": 60
},
"locked": {
"min_realised_vol": {
"min": 0.01
},
"quote_inside_bps": {
"min": 5
},
"max_inventory_skew": {
"max": 0.7
}
}
}
9. Implementation Flow
- Check KillSwitch; if active, emit no OrderIntents.
- FETCH realised vol from volatility model; if < hard (0.01), skip VH_VOL_BELOW_FLOOR.
- Check cool-off state; if in cool-off for market_id, skip VH_COOLOFF_ACTIVE.
- FETCH ws_market book; check inventory skew.
- IF abs(inventory_skew) > hard (0.70): skip VH_INVENTORY_LIMIT.
- Compute inside quote: bid = best_bid + quote_inside_bps/10000; ask = best_ask - quote_inside_bps/10000.
- Compute quote size = min(max_quote_size_usd, available_capital).
- Adjust size down 50% if realised_vol < warning (0.03).
- EMIT GTC post-only OrderIntent for YES (bid side) and/or NO (ask side) depending on skew.
- EMIT DecisionReport with intent_emitted=true, reason=VH_QUOTE_EMITTED.
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 onVolUpdate(market_id, volSignal):
ks = FETCH internal.killswitch.status
IF ks.active: RETURN
// Vol gate
realisedVol = volSignal.realised_vol
IF realisedVol < params.min_realised_vol_hard: // 0.01
EMIT DecisionReport(intent_emitted=false, reason='VH_VOL_BELOW_FLOOR')
RETURN
// Cool-off check
IF state.inCooloff(market_id):
EMIT DecisionReport(intent_emitted=false, reason='VH_COOLOFF_ACTIVE')
RETURN
// Inventory skew check
inventory = FETCH state.inventory(market_id)
skew = computeSkew(inventory.yes_usd, inventory.no_usd)
IF abs(skew) > params.max_inventory_skew_hard: // 0.70
EMIT DecisionReport(intent_emitted=false, reason='VH_INVENTORY_LIMIT')
RETURN
book = FETCH ws_market.book(market_id)
insideBps = params.quote_inside_bps / 10000
bidPrice = book.best_bid + insideBps
askPrice = book.best_ask - insideBps
sizeMultiplier = 0.5 IF realisedVol < params.min_realised_vol_warn ELSE 1.0
IF sizeMultiplier < 1.0: WARN('VH_LOW_VOL')
orderSize = toPusdUnits(params.max_quote_size_usd * sizeMultiplier)
// Quote both sides unless inventory skew exceeds warn threshold
IF skew < params.max_inventory_skew_warn OR skew >= 0:
EMIT OrderIntent(market=market_id, outcome='YES', side='buy', price=bidPrice,
size_pUSD=orderSize, tif='GTC', post_only=true, builder=code)
EMIT DecisionReport(intent_emitted=true, realised_vol=realisedVol, reason='VH_QUOTE_EMITTED')
SDK calls used
ws_market.subscribe('book', [market_id])fetchClobPublic('/markets/' + market_id)internal.volatilityModel.realisedVol(market_id)buildOrderTypedData(orderParams, {name:'CTFExchange', version:'2', chainId:137})
Complexity: O(1) per vol update per market; O(open_quotes) for inventory tracking
11. Wire Examples
Input — what arrives on the wire
Vol signal — market at 0.08 realised vol — internal (volatility model)
{
"market_id": "0xvh000000000000000000000000000000000000000000000000000000000000001",
"realised_vol": "0.08",
"best_bid": "0.490",
"best_ask": "0.510",
"inventory_skew": "0.10",
"received_at_ms": 1746790800000
}
Output — what the bot emits
OrderIntent — VH GTC post-only inside bid
{
"intent_id": "oi_01HVH0000001A",
"market_id": "0xvh000000000000000000000000000000000000000000000000000000000000001",
"outcome": "YES",
"side": "buy",
"price": "0.495",
"size_pUSD": "200.00",
"tif": "GTC",
"post_only": true,
"builder": {
"code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
"fee_bps": 10
},
"decision": {
"realised_vol": 0.08,
"quote_inside_bps": 50,
"reasons": [
"VH_QUOTE_EMITTED"
]
}
}
12. Decision Logic
APPROVE
realised_vol >= 0.05, not in cool-off, inventory skew within bounds, market open, KillSwitch inactive.
RESHAPE_REQUIRED
Not applicable — reshaping handled by downstream Risk guardrail.
REJECT
realised_vol < 0.01; inventory skew > 0.70; in cool-off; KillSwitch active.
WARNING_ONLY
realised_vol 0.03–0.05 or skew 0.50–0.70 triggers warning and size/side reduction.
13. Standard Decision Output
This bot returns a OrderIntent object. See OrderIntent schema.
{
"intent_id": "oi_01HVH0000001A",
"trace_id": "tr_01HVH000TR001",
"market_id": "0xvh000000000000000000000000000000000000000000000000000000000000001",
"outcome": "YES",
"side": "buy",
"price": "0.495",
"size_pUSD": "200.00",
"tif": "GTC",
"post_only": true,
"builder": {
"code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
"fee_bps": 10
},
"negrisk_aware": false,
"decision": {
"realised_vol": 0.08,
"quote_inside_bps": 50,
"inventory_skew": 0.1,
"reasons": [
"VH_QUOTE_EMITTED"
]
},
"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 |
|---|
VH_QUOTE_EMITTED | INFO | Vol >= min, no cool-off, skew within bounds. GTC post-only OrderIntent emitted. | Emit GTC maker quote. | A spread-harvesting maker quote was placed. |
VH_VOL_BELOW_FLOOR | INFO | Realised vol below 0.01 hard floor. | Skip; no quote. | Market volatility was too low for spread harvesting. |
VH_COOLOFF_ACTIVE | INFO | Post-loss cool-off period is active for this market. | Skip; no quote. | Brief cool-off after adverse fill is in effect. |
VH_INVENTORY_LIMIT | HARD_REJECT | Inventory skew exceeds 0.70 hard limit. | Skip all quoting on this market. | Position skew limit reached; quoting paused. |
KILL_SWITCH_ACTIVE | HARD_REJECT | Global kill switch is active. | Skip all markets; no OrderIntents emitted. | Trading is currently paused. |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|
polytraders_strat_volatilityharvest_decisions_total | counter | count | verdict, reason_code | Total evaluation cycles by verdict and reason. |
polytraders_strat_volatilityharvest_inventory_skew | gauge | fraction | market_id | Current inventory skew per tracked market. |
polytraders_strat_volatilityharvest_realised_vol | histogram | annualised | | Distribution of realised vol at quote emission. |
polytraders_strat_volatilityharvest_eval_latency_ms | histogram | milliseconds | | Latency from vol signal to OrderIntent emit. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|
VolatilityHarvestInventoryHigh | polytraders_strat_volatilityharvest_inventory_skew > 0.50 | warn | #runbook-vh-inventory |
VolatilityHarvestKillSwitch | rate(polytraders_strat_volatilityharvest_decisions_total{reason_code='KILL_SWITCH_ACTIVE'}[1m]) > 0 | page | #runbook-killswitch |
VolatilityHarvestInventoryLimit | polytraders_strat_volatilityharvest_inventory_skew > 0.70 | page | #runbook-vh-inventory-limit |
16. Developer Reporting
{
"bot_id": "strat.volatilityharvest",
"market_id": "0xvh000000000000000000000000000000000000000000000000000000000000001",
"realised_vol": 0.08,
"quote_inside_bps": 50,
"inventory_skew": 0.1,
"intent_emitted": true,
"reason": "VH_QUOTE_EMITTED",
"emitted_at_ms": 1746790800000
}
17. Plain-English Reporting
| Situation | User-facing explanation |
|---|
| Spread-harvesting quote placed | Market volatility is elevated. A maker quote was placed inside the spread to earn rebates from the resulting order flow. |
| Cool-off active | A brief pause is in effect after an adverse fill. Quoting will resume automatically. |
| Inventory skew limit reached | The position is too one-sided. Quoting is paused until inventory rebalances. |
18. Failure-Mode Block
| main_failure_mode | Inventory accumulation from persistent directional order flow: the bot continuously fills on one side without offsetting fills, building an exposed directional position. |
|---|
| false_positive_risk | Low realised vol that briefly spikes triggers quotes, then vol drops below min_realised_vol, leaving a quoted position with insufficient spread income. |
|---|
| false_negative_risk | min_realised_vol set too high misses genuine high-vol opportunities on markets with strong but sub-threshold volatility. |
|---|
| safe_fallback | If ws_market feed stale or volatility model unavailable, skip without emitting any OrderIntent. |
|---|
| required_dependencies | ws_market, clob_public, internal volatility model, KillSwitch, internal builder code |
|---|
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|
INVENTORY_SKEW_BREACH | Inject fills only on YES side until skew > 0.70 | | Automatic when offsetting fills reduce skew below 0.50. |
COOLOFF_ACTIVE | Trigger adverse fill; confirm cool-off fires | | Automatic after cool-off expires. |
KILL_SWITCH_ON | Set killswitch.active=true | | Automatic on manual KillSwitch reset. |
20. State & Persistence
Cold-start recovery
On cold start, inventory rebuilt from exec layer fills; cool-off resets to 0.
21. Concurrency & Idempotency
| Aspect | Specification |
|---|
| Execution model | actor-per-market |
| Max in-flight | 30 |
| Idempotency key | intent_id |
| Per-call timeout (ms) | 200 |
| Backpressure strategy | drop oldest vol update per market_id when queue > 3 |
| Locking / mutual exclusion | per-market_id mutex for inventory and cool-off state |
22. Dependencies
Depends on (must run first)
| Bot | Why | Contract |
|---|
| risk.kill_switch | Checked first; blocks all intent emission when active. | |
Emits to (downstream consumers)
External services
| Service | Endpoint | SLA assumed | On failure |
|---|
| Polymarket CLOB WebSocket (ws_market) | | best-effort | |
| Internal volatility model | | internal SLA | |
23. Security Surfaces
Abuse vectors considered
- Adversary floods one side to push inventory skew to limit, halting the bot
- Spoof high realised-vol signal to induce quoting on illiquid markets
Mitigations
- Inventory skew limit and hard cap prevent runaway one-sided exposure
- Realised vol sourced from authenticated internal model; not user-configurable in real-time
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 | Bot not yet implemented; designed against V2 schema (pUSD, builder codes, V2 EIP-712 domain). feeRateBps not present on any signed OrderIntent. |
API surfaces declared
clob_publicclob_authws_marketinternal
Networks supported
polygon
25. Versioning & Migration
| Field | Value |
|---|
| spec | 2.0.0 |
| implementation | 0.1.0 |
| schema | 2 |
| released | None |
| planned_release | Q3-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 |
|---|
| Emit GTC inside quote when vol=0.08, no cool-off, skew=0.1 | min_realised_vol=0.05, quote_inside_bps=50 | GTC post_only OrderIntent; reason=VH_QUOTE_EMITTED |
| Skip when in cool-off period | cool_off_state=active, remaining=45s | No OrderIntent; reason=VH_COOLOFF_ACTIVE |
| Skip when inventory skew > 0.70 hard limit | inventory_skew=0.75 | No OrderIntent; reason=VH_INVENTORY_LIMIT |
Integration Tests
| Test | Expected result |
|---|
| Full cycle: vol signal → inside quote → GTC post-only on Polygon testnet | Order has builder.code, post_only=true, no feeRateBps, EIP-712 domain v2 |
Property Tests
| Property | Required behaviour |
|---|
| Bot never quotes when inventory skew > max_inventory_skew hard limit | Always true |
| feeRateBps never present on any signed OrderIntent | Always true |
27. Operational Runbook
VolatilityHarvest incidents are typically inventory skew breaches (auto-halt) or kill-switch activations. Cool-off triggers are expected normal behavior.
On-call actions
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|
VolatilityHarvestInventoryHigh | | | | |
VolatilityHarvestKillSwitch | | | | |
VolatilityHarvestInventoryLimit | | | | |
Manual overrides
Healthcheck
GET /internal/health/volatilityharvest -> 200 if No market inventory skew > 0.50; vol model active; KillSwitch inactive.. Red: Any market at inventory hard limit or KillSwitch active..
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 |