3.5 Bregman-Projection Arb
Bregman-Projection Arb detects when the joint probability distribution implied by a neg-risk multi-outcome market's order book diverges from the closest valid (simplex-constrained) distribution by more than kl_divergence_threshold nats. It uses a Frank-Wolfe iterative projection to find the nearest valid distribution, measures the KL-divergence of the observed book from that projection, and emits a set of up to max_legs_per_trade OrderIntents to exploit the divergence. This is a user-controlled execution tool for multi-outcome neg-risk markets. It never trades standard 2-outcome binary markets (those are handled by Sum-to-One Arb). No performance claims are made; the tool automates the mechanical detection and sizing of Bregman-projection violations.
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 |
| Applies to | Negative-risk multi-outcome CLOB markets where the joint order-book distribution deviates from the nearest Bregman-projection-valid simplex distribution by more than kl_divergence_threshold |
| Default mode | limited_live |
| User-visible | Advanced details only |
| Developer owner | Polytraders core — Strategy pod |
2. Purpose
Bregman-Projection Arb detects when the joint probability distribution implied by a neg-risk multi-outcome market's order book diverges from the closest valid (simplex-constrained) distribution by more than kl_divergence_threshold nats. It uses a Frank-Wolfe iterative projection to find the nearest valid distribution, measures the KL-divergence of the observed book from that projection, and emits a set of up to max_legs_per_trade OrderIntents to exploit the divergence. This is a user-controlled execution tool for multi-outcome neg-risk markets. It never trades standard 2-outcome binary markets (those are handled by Sum-to-One Arb). No performance claims are made; the tool automates the mechanical detection and sizing of Bregman-projection violations.
3. Why This Bot Matters
KL-divergence computed on stale snapshots
The divergence measurement reflects an order book that no longer exists. Emitting OrderIntents based on stale data incurs slippage on all legs and may produce a net loss.
Frank-Wolfe projection does not converge within frank_wolfe_iters
The nearest valid distribution is not found; the measured divergence is an upper bound, not the true value. The bot may under-size or skip genuinely profitable trades.
Leg count exceeds max_legs_per_trade while liquidity is thin
In a 10-outcome market, all legs may be needed to fully capture the divergence. Capping legs leaves residual exposure that does not hedge cleanly.
NegRisk market settled via DVM while legs are open
If a disputed NegRisk outcome enters a 24-48h UMA DVM vote, open positions cannot be settled immediately. The bot must detect the dispute flag and halt new legs on affected markets.
feeRateBps present on signed order (V1 pattern)
CTFExchangeV2 rejects orders with feeRateBps. Fees are operator-set at match time. No leg intent may 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 |
|---|---|---|---|
| Full order book for all outcome tokens in the neg-risk market | ws_market (CLOB WebSocket) | Yes | Build observed joint distribution from best-ask prices across all outcome legs. |
| Market condition ID, outcome token IDs, negRisk flag, NegRiskAdapter address | clob_public / onchain | Yes | Confirm market uses NegRiskAdapter and identify all outcome token IDs for the projection. |
| Top-of-book depth for each outcome leg | clob_public | Yes | Size each leg to min(depth_available, liquidity_cap_usd / n_legs). |
| Market open/closed/resolved/dispute status | clob_public | Yes | Skip markets in UMA DVM dispute or resolution; halt new legs immediately. |
| Historical co-movement matrix (internal feed) | internal | No | Optional prior for initialising the Frank-Wolfe projection; speeds up convergence. |
5. Required Internal Inputs
| Input | Source | Required? | Use |
|---|---|---|---|
| KillSwitch active flag | KillSwitch | Yes | Abort all intent emission immediately if KillSwitch is active. |
| Builder code bytes32 | internal config | Yes | Injected into builder field on every signed V2 OrderIntent for attribution. |
6. Parameter Guide
| Parameter | Default | Warning | Hard | What it controls |
|---|---|---|---|---|
| kl_divergence_threshold | 0.015 | 0.008 | 0.003 | Minimum KL-divergence (in nats) from the nearest valid simplex distribution required before emitting OrderIntents. |
| frank_wolfe_iters | 200 | 80 | 30 | Maximum iterations for the Frank-Wolfe simplex projection algorithm. Higher values give a more precise projection but increase latency. |
| max_legs_per_trade | 6 | 9 | 12 | Maximum number of outcome-token legs emitted in a single trade cycle. Limits exposure on high-outcome-count markets. |
| liquidity_cap_usd | 400 | 600 | 800 | Total pUSD budget divided evenly across emitted legs. Each leg is capped at liquidity_cap_usd / n_legs, further bounded by available depth. |
7. Detailed Parameter Instructions
kl_divergence_threshold
What it means
Minimum KL-divergence (in nats) from the nearest valid simplex distribution required before emitting OrderIntents.
Default
{ "kl_divergence_threshold": 0.015 }
Why this default matters
0.015 nats provides meaningful edge after fee drag on typical neg-risk books. Below 0.008 the divergence is marginal; below 0.003 the bot will not fire regardless of config.
Threshold logic
| Condition | Action |
|---|---|
| kl_div >= 0.015 | EMIT multi-leg OrderIntents |
| 0.008 <= kl_div < 0.015 | WARN BREGMAN_ARB_DIVERGENCE_MARGINAL; emit at 50% leg size |
| kl_div < 0.003 (hard floor) | SKIP — BREGMAN_ARB_NO_EDGE; do not emit |
Developer check
if kl_div < params.hard: return skip('BREGMAN_ARB_NO_EDGE')
User-facing English
The multi-outcome pricing distribution was not far enough from the valid range to justify a trade after fees.
frank_wolfe_iters
What it means
Maximum iterations for the Frank-Wolfe simplex projection algorithm. Higher values give a more precise projection but increase latency.
Default
{ "frank_wolfe_iters": 200 }
Why this default matters
200 iterations converges to within 1e-6 nats on markets with up to 20 outcomes. Below 80 the projection may not converge on large markets.
Threshold logic
| Condition | Action |
|---|---|
| >= 200 | Full convergence expected |
| 80–200 | WARN BREGMAN_ARB_PROJECTION_MARGINAL; acceptable for small markets (≤ 5 outcomes) |
| < 30 | Hard floor; config rejected |
Developer check
assert params.frank_wolfe_iters >= params.hard
User-facing English
— not yet authored —
max_legs_per_trade
What it means
Maximum number of outcome-token legs emitted in a single trade cycle. Limits exposure on high-outcome-count markets.
Default
{ "max_legs_per_trade": 6 }
Why this default matters
6 legs covers most neg-risk markets cleanly. Beyond 9 legs the tail positions have tiny notional and the incremental edge is smaller than combined fee drag.
Threshold logic
| Condition | Action |
|---|---|
| <= 6 | Normal; all legs emitted |
| 7–12 | WARN; each additional leg has diminishing edge |
| > 12 | Reject config — PARAMETER_CHANGE_REQUIRES_APPROVAL |
Developer check
if params.max_legs_per_trade > params.hard: raise ConfigError('PARAMETER_CHANGE_REQUIRES_APPROVAL')
User-facing English
Trade was sized across the most mispriced outcome legs.
liquidity_cap_usd
What it means
Total pUSD budget divided evenly across emitted legs. Each leg is capped at liquidity_cap_usd / n_legs, further bounded by available depth.
Default
{ "liquidity_cap_usd": 400 }
Why this default matters
400 pUSD total keeps per-leg sizes modest (< 70 pUSD each for 6 legs) and within typical neg-risk top-of-book depth.
Threshold logic
| Condition | Action |
|---|---|
| <= 400 pUSD | Normal leg sizing |
| 400–800 pUSD | WARN; confirm depth covers each leg |
| > 800 pUSD | Reject config — PARAMETER_CHANGE_REQUIRES_APPROVAL |
Developer check
legSize = min(depthAvail, liquidity_cap_usd / n_legs)
User-facing English
Each leg of the multi-outcome trade was sized to fit within available market liquidity.
8. Default Configuration
{
"bot_id": "strat.bregman_projection_arb",
"version": "2.1.0",
"mode": "limited_live",
"defaults": {
"kl_divergence_threshold": 0.015,
"frank_wolfe_iters": 200,
"max_legs_per_trade": 6,
"liquidity_cap_usd": 400
},
"locked": {
"kl_divergence_threshold": {
"min": 0.003
},
"frank_wolfe_iters": {
"min": 30
},
"max_legs_per_trade": {
"max": 12
},
"liquidity_cap_usd": {
"max": 800
}
}
}9. Implementation Flow
- Check KillSwitch active flag; if active, emit no OrderIntents.
- Subscribe to ws_market book updates for all active neg-risk multi-outcome markets.
- On each book snapshot: validate freshness (last_seen < 3s); else emit STALE_MARKET_DATA and skip.
- Confirm market negRisk=true and not closed/resolved/in-dispute via clob_public.
- Build observed probability vector p_obs from best-ask prices across all outcome token IDs (normalise to sum-to-one).
- Run Frank-Wolfe projection: initialise q = uniform; iterate up to frank_wolfe_iters; converge to nearest simplex distribution q*.
- Compute KL(p_obs || q*). If < kl_divergence_threshold hard floor (0.003), emit DecisionReport intent_emitted=false BREGMAN_ARB_NO_EDGE (sampled 1/100); RETURN.
- If kl_div < warning threshold (0.015), WARN BREGMAN_ARB_DIVERGENCE_MARGINAL; reduce all leg sizes by 50%.
- Sort outcome legs by |p_obs_i - q*_i| descending; select top min(max_legs_per_trade, n_outcomes) legs.
- Fetch top-of-book depth for each selected leg from clob_public; set legSize = min(depth, liquidity_cap_usd / n_legs).
- Emit OrderIntent for each selected leg: market_id, outcome_token_id, side=buy (if p_obs_i > q*_i: NO/short; else YES/long), price=best_ask, size_pUSD=legSize, tif=FOK, builder={code, fee_bps:25}.
- Note: fees are operator-set at match time in V2 — feeRateBps is NOT on any signed order.
- Emit DecisionReport with intent_emitted=true, kl_divergence, n_legs, reason BREGMAN_ARB_EDGE_DETECTED.
10. Reference Implementation
Subscribes to CLOB WebSocket book snapshots for neg-risk markets, runs Frank-Wolfe simplex projection to find the nearest valid distribution, measures KL-divergence, and emits per-leg FOK OrderIntents when divergence exceeds threshold.
Pseudocode is language-agnostic. FETCH = read input. EMIT = produce output. Translate to TS/Python/Go/Rust.
FUNCTION evaluateNegRiskMarket(market_id, bookSnapshot):
// --- 0. KillSwitch gate ---
ks = FETCH internal.killswitch.status
IF ks.active: RETURN
// --- 1. Staleness check ---
IF isStale(bookSnapshot, maxAgeS=3):
EMIT DecisionReport(intent_emitted=false, reason='STALE_MARKET_DATA')
RETURN
// --- 2. NegRisk market validation ---
market = FETCH clob_public.GET('/markets/' + market_id)
IF NOT market.neg_risk OR market.closed OR market.resolved OR market.dispute_active:
EMIT DecisionReport(intent_emitted=false, reason='MARKET_CLOSED')
RETURN
// --- 3. Build observed probability vector ---
outcomes = bookSnapshot.outcomes // list of {token_id, best_ask, depth_pusd}
p_obs = [o.best_ask for o in outcomes]
p_obs = normalise(p_obs) // sum-to-one on simplex
// --- 4. Frank-Wolfe projection to nearest valid distribution ---
q = uniform(len(outcomes)) // init
FOR iter IN range(params.frank_wolfe_iters):
grad = KL_gradient(p_obs, q) // ∇KL(p||q) = -p/q
s = argmin_simplex(grad) // Frank-Wolfe linear minimisation step
gamma = 2.0 / (iter + 2) // step size
q_new = (1 - gamma) * q + gamma * s
IF norm(q_new - q) < 1e-8: BREAK
q = q_new
q_star = q
// --- 5. KL-divergence check ---
kl_div = KL(p_obs, q_star)
IF kl_div < params.kl_divergence_threshold_hard: // 0.003
IF random() < 0.01:
EMIT DecisionReport(intent_emitted=false, reason='BREGMAN_ARB_NO_EDGE', kl_div=kl_div)
RETURN
// --- 6. Warning threshold ---
sizeMultiplier = 1.0
IF kl_div < params.kl_divergence_threshold: // 0.015
WARN('BREGMAN_ARB_DIVERGENCE_MARGINAL')
sizeMultiplier = 0.5
// --- 7. Select legs by divergence magnitude ---
divergences = [(i, abs(p_obs[i] - q_star[i])) for i in range(len(outcomes))]
divergences.sort(by=magnitude, descending=true)
selected = divergences[:params.max_legs_per_trade]
// --- 8. Size and emit legs ---
legBudget = params.liquidity_cap_usd / len(selected)
FOR (i, _) IN selected:
legSize = toPusdUnits(min(outcomes[i].depth_pusd, legBudget) * sizeMultiplier)
side = 'buy' IF p_obs[i] < q_star[i] ELSE 'sell'
EMIT OrderIntent(
market_id = market_id,
outcome_token_id = outcomes[i].token_id,
side = side,
price = outcomes[i].best_ask,
size_pUSD = legSize,
tif = 'FOK',
post_only = false,
negrisk_aware = true,
builder = {code: internal.builder_code, fee_bps: 25}
// V2: no feeRateBps on signed order
)
EMIT DecisionReport(intent_emitted=true, kl_divergence=kl_div,
n_legs=len(selected), reason='BREGMAN_ARB_EDGE_DETECTED')
SDK calls used
fetchClobPublic('/markets/' + market_id)ws_market.subscribe('book', outcome_token_ids)toPusdUnits(rawFloat)buildOrderTypedData(orderParams, { name: 'CTFExchange', version: '2', chainId: 137 })internal.killswitch.status()internal.builder_code
Complexity: O(k * frank_wolfe_iters) per book snapshot, where k = number of outcomes
11. Wire Examples
Input — what arrives on the wire
NegRisk market book snapshot — 8 outcomes, divergence detected — ws_market
{
"market_id": "0xbregman0000000000000000000000000000000000000000000000000000000001",
"neg_risk": true,
"outcomes": [
{
"token_id": "0xbrg_tk_0",
"best_ask": "0.310",
"depth_pusd": "180.00"
},
{
"token_id": "0xbrg_tk_1",
"best_ask": "0.185",
"depth_pusd": "220.00"
},
{
"token_id": "0xbrg_tk_2",
"best_ask": "0.112",
"depth_pusd": "90.00"
},
{
"token_id": "0xbrg_tk_3",
"best_ask": "0.098",
"depth_pusd": "75.00"
},
{
"token_id": "0xbrg_tk_4",
"best_ask": "0.095",
"depth_pusd": "60.00"
},
{
"token_id": "0xbrg_tk_5",
"best_ask": "0.074",
"depth_pusd": "50.00"
},
{
"token_id": "0xbrg_tk_6",
"best_ask": "0.068",
"depth_pusd": "45.00"
},
{
"token_id": "0xbrg_tk_7",
"best_ask": "0.058",
"depth_pusd": "40.00"
}
],
"received_at_ms": 1746790000000
}
Output — what the bot emits
OrderIntent — leg 2 (buy outcome 2, FOK, builder-attributed)
{
"intent_id": "oi_01HXB9PARZ0001A",
"trace_id": "tr_01HXB9PARZ000TR",
"market_id": "0xbregman0000000000000000000000000000000000000000000000000000000001",
"outcome_token_id": "0xbrg_tk_2",
"side": "buy",
"price": "0.112",
"size_pUSD": "66.00",
"tif": "FOK",
"post_only": false,
"builder": {
"code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
"fee_bps": 25
},
"negrisk_aware": true,
"decision": {
"kl_divergence": 0.022,
"n_legs": 6,
"leg_index": 2,
"reasons": [
"BREGMAN_ARB_EDGE_DETECTED"
]
},
"comment": "fees are operator-set at match time in V2 — feeRateBps is NOT on the signed order"
}
DecisionReport — skipped (no edge), sampled 1/100
{
"report_id": "dr_01HXB9PARZ999Z",
"bot_id": "strat.bregman_projection_arb",
"market_id": "0xbregman0000000000000000000000000000000000000000000000000000000001",
"intent_emitted": false,
"kl_divergence": 0.001,
"reasons": [
"BREGMAN_ARB_NO_EDGE"
],
"sampled": true,
"evaluated_at_ms": 1746790001000
}12. Decision Logic
APPROVE
kl_div >= kl_divergence_threshold, market is neg-risk and open, all legs have depth, KillSwitch inactive. Emit per-leg OrderIntents (FOK).
RESHAPE_REQUIRED
Not applicable — strat bots emit OrderIntents; reshaping is handled by the downstream Risk guardrail pipeline.
REJECT
kl_div < 0.003 hard floor; market closed/resolved/disputed; KillSwitch active; stale snapshot. Emit DecisionReport intent_emitted=false.
WARNING_ONLY
kl_div between 0.003 and 0.015 triggers BREGMAN_ARB_DIVERGENCE_MARGINAL warn and 50% size reduction.
13. Standard Decision Output
This bot returns a OrderIntent object. See OrderIntent schema.
{
"intent_id": "oi_01HXB9PARZ0001A",
"trace_id": "tr_01HXB9PARZ000TR",
"market_id": "0xbregman0000000000000000000000000000000000000000000000000000000001",
"outcome_token_id": "0xbregman_tk_outcome_3",
"side": "buy",
"price": "0.112",
"size_pUSD": "66.00",
"tif": "FOK",
"post_only": false,
"builder": {
"code": "0x706f6c7974726164657273000000000000000000000000000000000000000000",
"fee_bps": 25
},
"negrisk_aware": true,
"decision": {
"kl_divergence": 0.022,
"n_legs": 6,
"leg_index": 2,
"reasons": [
"BREGMAN_ARB_EDGE_DETECTED"
]
},
"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 |
|---|---|---|---|---|
BREGMAN_ARB_EDGE_DETECTED | INFO | KL-divergence from the nearest valid simplex distribution exceeds kl_divergence_threshold. Multi-leg OrderIntents emitted. | Emit per-leg FOK OrderIntents. | A pricing inconsistency across multiple outcomes was detected and orders were placed to capture it. |
BREGMAN_ARB_NO_EDGE | INFO | KL-divergence is below the 0.003 nats absolute hard floor. No trade opportunity exists after fees. | Skip; emit DecisionReport intent_emitted=false (sampled 1/100). | The multi-outcome pricing was within normal bounds. No order was placed. |
BREGMAN_ARB_DIVERGENCE_MARGINAL | WARN | KL-divergence is between the hard floor (0.003) and the warning threshold (0.015). Trade is marginal. | Emit OrderIntents at 50% leg size; log warning. | A small pricing inconsistency was detected. Leg sizes were reduced. |
BREGMAN_ARB_PROJECTION_MARGINAL | WARN | Frank-Wolfe iteration count is below the warning threshold; projection may not have fully converged. | Emit with caution; log warning for ops review. | |
BREGMAN_ARB_DEPTH_INSUFFICIENT | WARN | Top-of-book depth on one or more selected legs is below minimum viable trade size (5 pUSD). | Skip affected leg; reduce n_legs; emit DecisionReport. | Not enough liquidity on some outcome legs to place the full trade. |
STALE_MARKET_DATA | HARD_REJECT | Book snapshot is older than 3 seconds or market is in UMA DVM dispute. | Skip; no OrderIntent emitted. | Market data was too old to act on safely. |
MARKET_CLOSED | HARD_REJECT | Market is closed, resolved, disputed, or is not a neg-risk market. | Skip immediately; no OrderIntent emitted. | This market is no longer open for trading. |
KILL_SWITCH_ACTIVE | HARD_REJECT | Global kill switch is active. | Skip all markets; no OrderIntents emitted. | Trading is currently paused. |
PARAMETER_CHANGE_REQUIRES_APPROVAL | HARD_REJECT | A config change would push a parameter past its locked hard limit. | Reject config change; do not apply. |
15. Metrics & Logs
Metrics emitted
| Metric | Type | Unit | Labels | Meaning |
|---|---|---|---|---|
polytraders_strat_bregmanarb_decisions_total | counter | count | verdict, reason_code | Total evaluation cycles by intent_emitted (true/false) and reason code. |
polytraders_strat_bregmanarb_kl_divergence | histogram | nats | Distribution of measured KL-divergence across all evaluated neg-risk markets. | |
polytraders_strat_bregmanarb_legs_emitted_total | counter | count | outcome_index | Total OrderIntents emitted per outcome leg index. |
polytraders_strat_bregmanarb_projection_iters | histogram | iterations | Frank-Wolfe iteration count at convergence per evaluation. | |
polytraders_strat_bregmanarb_eval_latency_ms | histogram | milliseconds | Wall-clock time from book snapshot receipt to last OrderIntent emit. | |
polytraders_strat_bregmanarb_stale_feed_total | counter | count | Evaluation cycles skipped due to stale ws_market feed. |
Alerts
| Alert | Condition | Severity | Runbook |
|---|---|---|---|
BregmanArbHighStaleFeed | rate(polytraders_strat_bregmanarb_stale_feed_total[5m]) > 0.1 | warn | #runbook-bregmanarb-stale-feed |
BregmanArbHighLatency | histogram_quantile(0.99, rate(polytraders_strat_bregmanarb_eval_latency_ms_bucket[5m])) > 300 | warn | #runbook-bregmanarb-latency |
BregmanArbNoEdgeStreak | rate(polytraders_strat_bregmanarb_decisions_total{verdict='false'}[15m]) / rate(polytraders_strat_bregmanarb_decisions_total[15m]) > 0.99 | warn | #runbook-bregmanarb-no-edge |
BregmanArbKillSwitchBlocking | rate(polytraders_strat_bregmanarb_decisions_total{reason_code='KILL_SWITCH_ACTIVE'}[1m]) > 0 | page | #runbook-killswitch |
Dashboards
- Grafana — Strategy / BregmanArb KL-divergence distribution
- Grafana — Strategy / BregmanArb leg throughput and projection convergence
16. Developer Reporting
{
"bot_id": "strat.bregman_projection_arb",
"market_id": "0xbregman0000000000000000000000000000000000000000000000000000000001",
"n_outcomes": 8,
"kl_divergence": 0.022,
"frank_wolfe_iters_used": 147,
"n_legs_selected": 6,
"liquidity_cap_usd": 400,
"per_leg_size_pusd": 66.0,
"intent_emitted": true,
"reason": "BREGMAN_ARB_EDGE_DETECTED",
"emitted_at_ms": 1746790000000
}17. Plain-English Reporting
| Situation | User-facing explanation |
|---|---|
| Multi-outcome arb trade initiated | The combined pricing across several outcomes of this multi-choice market deviated from a mathematically consistent distribution. Orders were placed across the mispriced outcomes to capture the discrepancy. |
| No divergence — no trade | The multi-outcome pricing was within normal bounds after fees. No order was placed. |
| Divergence marginal — reduced size | A small pricing inconsistency was detected. Leg sizes were halved to limit exposure given the thin margin. |
| Market in dispute | This market's outcome is under review. No new orders were placed until the dispute resolves. |
18. Failure-Mode Block
| main_failure_mode | Frank-Wolfe projection does not converge within frank_wolfe_iters: the KL-divergence measurement is imprecise, and the bot may misidentify which legs to trade or over-size positions. |
|---|---|
| false_positive_risk | Stale or illiquid book data produces a spurious divergence; the bot emits legs that no longer have edge by the time they reach the CLOB. |
| false_negative_risk | kl_divergence_threshold set too conservatively misses genuine divergences on smaller-outcome-count markets where even 0.010 nats represents meaningful pricing error. |
| safe_fallback | If book snapshot is stale (last_seen > 3s) or market is in UMA DVM dispute, emit STALE_MARKET_DATA DecisionReport and skip without emitting any OrderIntent. |
| required_dependencies | ws_market book stream (all outcome token IDs for neg-risk market), clob_public market endpoint (status + depth + negRisk flag), KillSwitch active flag, internal builder code, Historical co-movement matrix (optional; improves projection init) |
19. Failure-Injection Recipes
| Scenario | How to inject | Expected behaviour | Recovery |
|---|---|---|---|
STALE_WS_FEED | Pause ws_market WebSocket; let last_seen age beyond 3s | Automatic on WebSocket reconnect within one evaluation cycle. | |
PROJECTION_NON_CONVERGENCE | Set frank_wolfe_iters=1 on a 15-outcome market | Increase frank_wolfe_iters in config; ops review required. | |
DISPUTE_ACTIVE | Set market.dispute_active=true on the test market | Automatic when dispute resolves and market returns to open status. | |
KILL_SWITCH_ON | Set killswitch.active=true | Automatic on manual KillSwitch reset. | |
DEPTH_INSUFFICIENT_ON_LEG | Set mock depth on outcome leg 3 to 2 pUSD (below 5 pUSD floor) | Automatic when depth replenishes. |
20. State & Persistence
Cold-start recovery
On cold start, state is empty; first book snapshot triggers fresh Frank-Wolfe projection without stale-state comparison.
21. Concurrency & Idempotency
| Aspect | Specification |
|---|---|
| Execution model | single-threaded event loop |
| Max in-flight | 30 |
| Idempotency key | intent_id |
| Per-call timeout (ms) | 400 |
| Backpressure strategy | drop oldest pending snapshot per market_id when queue depth > 3 |
| Locking / mutual exclusion | per-market_id mutex for Redis state write during projection |
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)
| Bot | Why | Contract |
|---|---|---|
| risk.portfolio_guard | ||
| gov.builder_attribution |
External services
| Service | Endpoint | SLA assumed | On failure |
|---|---|---|---|
| Polymarket CLOB v2 (public) | 99.9% (Polymarket-published) | ||
| Polymarket CLOB WebSocket (ws_market) | best-effort |
23. Security Surfaces
On-chain contract calls
| Contract | Method | Network | Effect |
|---|---|---|---|
CTFExchangeV2 | | polygon |
Abuse vectors considered
- Adversary spams thin neg-risk legs to force spurious KL-divergence signals
- Replaying a signed OrderIntent after the projected divergence closes
- Injecting a modified builder.code to redirect attribution fees
Mitigations
- All legs use FOK; partial fills are rejected rather than leaving naked positions
- V2 order timestamp(ms) invalidates replays outside the exchange acceptance window
- Builder code is read from immutable internal config; not user-supplied
- Book snapshot staleness check (3s) prevents acting on stale divergence signals
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 | no |
| SDK used | py-clob-client-v2 |
| Settlement contract | CTFExchangeV2 |
| Notes | Operates exclusively on negative-risk multi-outcome markets via NegRiskAdapter. Builder code is injected on each leg intent. feeRateBps is not present on any signed order. |
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 all signed multi-leg order construction. Updated collateral denomination to pUSD. Injected builder field (bytes32) on every OrderIntent. EIP-712 Exchange domain version updated from '1' to '2'. NegRisk market handling now uses NegRiskAdapter for multi-outcome CLOB V2 settlement. |
26. Acceptance Tests
Unit Tests
| Test | Setup | Expected result |
|---|---|---|
| Emit 6 legs when kl_div=0.022 (above threshold 0.015) | 8-outcome market; p_obs deviates from projection by 0.022 nats; depth=200 pUSD each | Six OrderIntents emitted; DecisionReport intent_emitted=true, reason=BREGMAN_ARB_EDGE_DETECTED |
| Skip when kl_div=0.002 (below hard floor 0.003) | p_obs near-valid distribution; kl_div=0.002 | No OrderIntents; sampled DecisionReport reason=BREGMAN_ARB_NO_EDGE |
| Reduce leg sizes 50% when kl_div marginal (0.010) | kl_div=0.010, liquidity_cap_usd=400, n_legs=6 | Six OrderIntents at 33 pUSD each (50% of 66); WARN BREGMAN_ARB_DIVERGENCE_MARGINAL |
| Skip when market in UMA DVM dispute | market.dispute_active=true | No OrderIntents; reason=MARKET_CLOSED |
| Skip when KillSwitch active | killswitch.active=true | No OrderIntents emitted |
| Frank-Wolfe iteration cap enforced | frank_wolfe_iters=200; convergence at iter 147 | Projection completes; developer_log.frank_wolfe_iters_used <= 200 |
Integration Tests
| Test | Expected result |
|---|---|
| Full cycle: neg-risk ws_market snapshot → KL computed → 6 signed V2 FOK OrderIntents submitted | All intents have builder.code (bytes32), no feeRateBps, negrisk_aware=true, EIP-712 domain version '2' |
| Stale ws_market feed triggers STALE_MARKET_DATA skip | DecisionReport intent_emitted=false, reason=STALE_MARKET_DATA after 3s feed gap |
Property Tests
| Property | Required behaviour |
|---|---|
| All emitted leg OrderIntents are FOK; bot never posts resting maker orders | Always true |
| n_legs emitted never exceeds max_legs_per_trade | Always true |
| feeRateBps never present on any signed OrderIntent | Always true — V2 fees are operator-set at match time |
27. Operational Runbook
Bregman-Projection Arb incidents are usually stale feed, Frank-Wolfe non-convergence on high-outcome markets, or UMA DVM dispute halts. Non-convergence requires a config fix; dispute halts resolve automatically.
On-call actions
| Alert | First step | Diagnosis | Mitigation | Escalate to |
|---|---|---|---|---|
BregmanArbHighStaleFeed | ||||
BregmanArbHighLatency | ||||
BregmanArbNoEdgeStreak | ||||
BregmanArbKillSwitchBlocking |
Manual overrides
——
Healthcheck
GET /internal/health/bregman-projection-arb -> 200 if ws_market feed last_seen < 3s, Redis reachable, KillSwitch inactive, at least one market evaluated in last 60s.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 FOK-only invariant and projection convergence | CI test run | 100% pass |
| feeRateBps absence verified; negrisk_aware=true on all OrderIntents | Integration test asserting V2 order schema | Pass |
Promote to Limited live
| Gate | How measured | Threshold |
|---|---|---|
| p99 eval latency < 400ms over 24h (includes Frank-Wolfe projection time) | polytraders_strat_bregmanarb_eval_latency_ms histogram | p99 < 400ms |
| Zero partial-leg fills (all FOK either fully fill or reject) in 48h shadow run | Fill reconciliation report | 0 partial fills |
Promote to General live
| Gate | How measured | Threshold |
|---|---|---|
| E2E: neg-risk book snapshot → KL computed → N signed V2 FOK OrderIntents submitted on Polygon testnet | E2E test | Pass |
| Frank-Wolfe projection convergence verified across 10 synthetic market distributions | Unit test suite | All converge within frank_wolfe_iters |
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 |