Polytraders Dev Guide
internal
v3 spine Phase 1 · Shared contracts 9 demo-wired · 0 shadow-ready · 0 production-live · 100 pending · 109 total 15/33 infra tasks the plan status board
HomeBy LayerStrategy3.15 VolatilityHarvest

3.15 VolatilityHarvest

Strategy Alpha Strategy Trade PLANNED Spec started capital · Direct P8 · Additional strategies pending stub

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.

v3 readiness

Docs27/27
donehow scored
Impl0/15
pendinghow scored
Backtest0/4
pendinghow scored
Runtime0/8
pendinghow scored

A bot is done when all four scores are. What does done mean?

1. Bot Identity

LayerStrategy  Strategy
Bot classAlpha Strategy
AuthorityTrade
StatusPLANNED
ReadinessSpec started
Runs beforeRisk guardrail pipeline
Runs afterVolatility monitor / Observation bus
Applies toPolymarket binary event markets with elevated realised volatility and thick order books, where posting inside the spread earns rebates from wide-spread trading activity
Default modeshadow_only
User-visibleAdvanced details only
Developer ownerPolytraders 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.

4. Required Polymarket Inputs

InputSourceRequired?Use
CLOB book (bid, ask, depth, trades)ws_marketYesCompute realised vol from tick history; measure current spread; detect depth thickness.
Market statusclob_publicYesSkip closed or resolved markets.

5. Required Internal Inputs

InputSourceRequired?Use
KillSwitch active flagKillSwitchYesAbort all intent emission if KillSwitch active.
Realised volatility per marketinternal (volatility model)YesGate entry on min_realised_vol threshold.
Builder code bytes32internal configYesInjected into builder field on every signed V2 OrderIntent.

6. Parameter Guide

ParameterDefaultWarningHardWhat it controls
min_realised_vol0.050.030.01Minimum 1h realised volatility (annualised) required before quoting inside the spread.
quote_inside_bps50205How many bps inside the current best bid/ask the bot posts its maker quote.
max_inventory_skew0.30.50.7Maximum fraction of total position that can be on one side (YES or NO) before the bot stops quoting that side.
cool_off_after_loss60300Seconds 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

ConditionAction
>= 0.05Allow spread quoting
0.03–0.05WARN VH_LOW_VOL; halve quote size
< 0.01HARD_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

ConditionAction
>= 50 bps insideStandard inside quote
20–50 bpsWARN VH_TIGHT_INSIDE_QUOTE
< 5 bpsHARD_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

ConditionAction
<= 0.30Quote both sides normally
0.50–0.70WARN VH_HIGH_SKEW; stop quoting skewed side
> 0.70HARD_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

ConditionAction
>= 60sStandard cool-off
30–60sWARN VH_SHORT_COOLOFF
= 0sNo 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

  1. Check KillSwitch; if active, emit no OrderIntents.
  2. FETCH realised vol from volatility model; if < hard (0.01), skip VH_VOL_BELOW_FLOOR.
  3. Check cool-off state; if in cool-off for market_id, skip VH_COOLOFF_ACTIVE.
  4. FETCH ws_market book; check inventory skew.
  5. IF abs(inventory_skew) > hard (0.70): skip VH_INVENTORY_LIMIT.
  6. Compute inside quote: bid = best_bid + quote_inside_bps/10000; ask = best_ask - quote_inside_bps/10000.
  7. Compute quote size = min(max_quote_size_usd, available_capital).
  8. Adjust size down 50% if realised_vol < warning (0.03).
  9. EMIT GTC post-only OrderIntent for YES (bid side) and/or NO (ask side) depending on skew.
  10. 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 volinternal (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

CodeSeverityMeaningActionUser-facing message
VH_QUOTE_EMITTEDINFOVol >= 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_FLOORINFORealised vol below 0.01 hard floor.Skip; no quote.Market volatility was too low for spread harvesting.
VH_COOLOFF_ACTIVEINFOPost-loss cool-off period is active for this market.Skip; no quote.Brief cool-off after adverse fill is in effect.
VH_INVENTORY_LIMITHARD_REJECTInventory skew exceeds 0.70 hard limit.Skip all quoting on this market.Position skew limit reached; quoting paused.
KILL_SWITCH_ACTIVEHARD_REJECTGlobal kill switch is active.Skip all markets; no OrderIntents emitted.Trading is currently paused.

15. Metrics & Logs

Metrics emitted

MetricTypeUnitLabelsMeaning
polytraders_strat_volatilityharvest_decisions_totalcountercountverdict, reason_codeTotal evaluation cycles by verdict and reason.
polytraders_strat_volatilityharvest_inventory_skewgaugefractionmarket_idCurrent inventory skew per tracked market.
polytraders_strat_volatilityharvest_realised_volhistogramannualisedDistribution of realised vol at quote emission.
polytraders_strat_volatilityharvest_eval_latency_mshistogrammillisecondsLatency from vol signal to OrderIntent emit.

Alerts

AlertConditionSeverityRunbook
VolatilityHarvestInventoryHighpolytraders_strat_volatilityharvest_inventory_skew > 0.50warn#runbook-vh-inventory
VolatilityHarvestKillSwitchrate(polytraders_strat_volatilityharvest_decisions_total{reason_code='KILL_SWITCH_ACTIVE'}[1m]) > 0page#runbook-killswitch
VolatilityHarvestInventoryLimitpolytraders_strat_volatilityharvest_inventory_skew > 0.70page#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

SituationUser-facing explanation
Spread-harvesting quote placedMarket volatility is elevated. A maker quote was placed inside the spread to earn rebates from the resulting order flow.
Cool-off activeA brief pause is in effect after an adverse fill. Quoting will resume automatically.
Inventory skew limit reachedThe position is too one-sided. Quoting is paused until inventory rebalances.

18. Failure-Mode Block

main_failure_modeInventory accumulation from persistent directional order flow: the bot continuously fills on one side without offsetting fills, building an exposed directional position.
false_positive_riskLow realised vol that briefly spikes triggers quotes, then vol drops below min_realised_vol, leaving a quoted position with insufficient spread income.
false_negative_riskmin_realised_vol set too high misses genuine high-vol opportunities on markets with strong but sub-threshold volatility.
safe_fallbackIf ws_market feed stale or volatility model unavailable, skip without emitting any OrderIntent.
required_dependenciesws_market, clob_public, internal volatility model, KillSwitch, internal builder code

19. Failure-Injection Recipes

ScenarioHow to injectExpected behaviourRecovery
INVENTORY_SKEW_BREACHInject fills only on YES side until skew > 0.70Automatic when offsetting fills reduce skew below 0.50.
COOLOFF_ACTIVETrigger adverse fill; confirm cool-off firesAutomatic after cool-off expires.
KILL_SWITCH_ONSet killswitch.active=trueAutomatic 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

AspectSpecification
Execution modelactor-per-market
Max in-flight30
Idempotency keyintent_id
Per-call timeout (ms)200
Backpressure strategydrop oldest vol update per market_id when queue > 3
Locking / mutual exclusionper-market_id mutex for inventory and cool-off state

22. Dependencies

Depends on (must run first)

BotWhyContract
risk.kill_switchChecked first; blocks all intent emission when active.

Emits to (downstream consumers)

External services

ServiceEndpointSLA assumedOn failure
Polymarket CLOB WebSocket (ws_market)best-effort
Internal volatility modelinternal 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

AspectValue
CLOB versionv2
Collateral assetpUSD
EIP-712 Exchange domain version2
Aware of builderCode fieldyes
Aware of negative-risk marketsno
Multi-chain readyno
SDK usedpy-clob-client-v2
Settlement contractCTFExchangeV2
NotesBot 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

FieldValue
spec2.0.0
implementation0.1.0
schema2
releasedNone
planned_releaseQ3-2026

Migration history

DateFromToReasonAction taken
2026-04-28n/av2-specSpec drafted post-CLOB-V2 cutover; bot not yet implementedDesigned against V2 schema (pUSD, builder codes, V2 EIP-712 domain)

26. Acceptance Tests

Unit Tests

TestSetupExpected result
Emit GTC inside quote when vol=0.08, no cool-off, skew=0.1min_realised_vol=0.05, quote_inside_bps=50GTC post_only OrderIntent; reason=VH_QUOTE_EMITTED
Skip when in cool-off periodcool_off_state=active, remaining=45sNo OrderIntent; reason=VH_COOLOFF_ACTIVE
Skip when inventory skew > 0.70 hard limitinventory_skew=0.75No OrderIntent; reason=VH_INVENTORY_LIMIT

Integration Tests

TestExpected result
Full cycle: vol signal → inside quote → GTC post-only on Polygon testnetOrder has builder.code, post_only=true, no feeRateBps, EIP-712 domain v2

Property Tests

PropertyRequired behaviour
Bot never quotes when inventory skew > max_inventory_skew hard limitAlways true
feeRateBps never present on any signed OrderIntentAlways 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

AlertFirst stepDiagnosisMitigationEscalate 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..

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

GateHow measuredThreshold
Unit tests pass including inventory-limit halt and cool-off blockCI test run100% pass

Promote to Limited live

GateHow measuredThreshold
p99 eval latency < 200ms over 24h shadow runpolytraders_strat_volatilityharvest_eval_latency_ms histogramp99 < 200ms

Promote to General live

GateHow measuredThreshold
E2E: vol signal → inside quote → GTC maker on Polygon testnet with post_only=true and builder.codeE2E testPass

29. Developer Checklist

Ready-to-ship score: 27/27 sections complete · 100%

RequirementStatus
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