Latency Arbitrage Deep Dive
import { Aside, Card, CardGrid, Steps, Tabs, TabItem } from ‘@astrojs/starlight/components’;
This guide provides a comprehensive deep dive into the latency arbitrage strategy, covering:
- How trades are evaluated and executed
- The mathematical models behind probability calculation
- Kelly criterion position sizing
- Complete worked examples with real numbers
Strategy Overview
Section titled “Strategy Overview”The latency arbitrage strategy exploits the speed difference between centralized exchanges (Binance) and prediction markets (Polymarket). When BTC moves on Binance, it takes time for Polymarket prices to adjust—this creates a window where we can calculate the “correct” probability and trade against stale prices.
sequenceDiagram
participant Binance
participant Bot
participant Polymarket
Binance->>Bot: BTC price update ($91,620)
Note over Bot: Calculate theoretical<br/>probability (42.8%)
Bot->>Polymarket: Request orderbook
Polymarket-->>Bot: YES ask: $0.42, NO ask: $0.60
Note over Bot: Market implies 42%<br/>Theo says 42.8%<br/>Edge: 0.8% - too small
Binance->>Bot: BTC price jumps ($91,850)
Note over Bot: Recalculate<br/>probability (51.2%)
Note over Bot: Edge now 9.2%!<br/>Trade triggered
Bot->>Polymarket: Buy YES @ $0.42
Polymarket-->>Bot: Filled
Trade Execution Flow
Section titled “Trade Execution Flow”Every tick of the strategy evaluates whether to trade. Here’s the complete decision tree:
flowchart TD
Start([Market Update]) --> Parse{Parse Market Slug}
Parse -->|Failed| Skip1[Skip: Cannot parse]
Parse -->|Success| Asset{Asset Mapping?}
Asset -->|Unknown asset| Skip2[Skip: No asset mapping]
Asset -->|BTC/ETH/SOL| Price{Reference Price<br/>Available?}
Price -->|No| Skip3[Skip: No Binance price]
Price -->|Yes| Vol{Volatility ≥ 10%?}
Vol -->|No| Skip4[Skip: Low volatility]
Vol -->|Yes| Cooldown{Cooldown<br/>Elapsed?}
Cooldown -->|No| Skip5[Skip: In cooldown]
Cooldown -->|Yes| Calc[Calculate Edge]
Calc --> Edge{Net Edge ><br/>Threshold?}
Edge -->|No| Skip6[Skip: Insufficient edge]
Edge -->|Yes| Bounds{Price in<br/>Bounds?}
Bounds -->|No| Skip7[Skip: Price out of bounds]
Bounds -->|Yes| Position{Position<br/>Under Limit?}
Position -->|No| Skip8[Skip: Max position reached]
Position -->|Yes| Size[Calculate Kelly Size]
Size --> Execute[Execute Trade]
style Execute fill:#10b981,stroke:#059669,color:#fff
style Skip1 fill:#6b7280,stroke:#4b5563,color:#fff
style Skip2 fill:#6b7280,stroke:#4b5563,color:#fff
style Skip3 fill:#6b7280,stroke:#4b5563,color:#fff
style Skip4 fill:#6b7280,stroke:#4b5563,color:#fff
style Skip5 fill:#6b7280,stroke:#4b5563,color:#fff
style Skip6 fill:#6b7280,stroke:#4b5563,color:#fff
style Skip7 fill:#6b7280,stroke:#4b5563,color:#fff
style Skip8 fill:#6b7280,stroke:#4b5563,color:#fff
Trade Conditions Checklist
Section titled “Trade Conditions Checklist”All seven conditions must be true for a trade to execute:
| # | Condition | Check | Config Parameter |
|---|---|---|---|
| 1 | Market Parseable | Slug contains recognized asset (bitcoin, btc, ethereum, eth, solana, sol) | assetMapping |
| 2 | Reference Price Available | Live Binance price exists for the asset | Binance WebSocket |
| 3 | Sufficient Volatility | Annualized volatility ≥ minimum threshold | minVolatility (default: 10%) |
| 4 | Cooldown Elapsed | Time since last trade on this market ≥ cooldown | cooldownMs (default: 3000ms) |
| 5 | Net Edge > Threshold | netEdge > edgeThreshold + modelUncertainty | edgeThreshold (default: 2.5%) |
| 6 | Price In Bounds | 0.01 < ask price < 0.99 | Hardcoded in strategy |
| 7 | Position Under Limit | Current position < maximum allowed | maxPositionSize (default: 50) |
Probability Model: Black-Scholes for Binary Options
Section titled “Probability Model: Black-Scholes for Binary Options”The strategy uses a simplified Black-Scholes model to calculate the theoretical probability that a binary option will expire in-the-money.
The Formula
Section titled “The Formula”For a binary option asking “Will BTC be above strike K at expiry T?”:
d = [ln(S/K) - σ²T/2] / (σ√T)P(above) = Φ(d)Where:
- S = Current spot price (from Binance)
- K = Strike price (parsed from market slug)
- σ = Annualized volatility (calculated from price history)
- T = Time to expiry (in years)
- Φ = Standard normal cumulative distribution function
const timeYears = timeToExpiryMs / (365.25 * 24 * 60 * 60 * 1000); const sqrtT = Math.sqrt(timeYears);
// d2 from Black-Scholes with zero drift (μ=0 for short-term crypto) const d = (Math.log(currentPrice / strikePrice) - 0.5 * volatility * volatility * timeYears) / (volatility * sqrtT);
const prob = normalCDF(d); return direction === “above” ? prob : 1 - prob; }
</TabItem><TabItem label="Math Explanation">**Why d₂ instead of d₁?**
In the standard Black-Scholes formula for options pricing:- d₁ is used for delta hedging- d₂ is the probability measure under the risk-neutral distribution
For binary (digital) options, we care about the *probability* of finishing in-the-money, which is Φ(d₂).
**Why zero drift?**
For short-term crypto markets (minutes to hours), expected drift is negligible compared to volatility. Setting μ=0 simplifies the formula and is empirically accurate for our timeframes.</TabItem></Tabs>
### Volatility Calculation
Historical volatility is calculated from a sliding window of Binance prices:
```typescript// From src/strategies/latency-arb/math.tsexport function computeVolatility( priceHistory: { ts: number; price: number }[], windowMs: number, nowMs: number,): number | null { const recent = priceHistory.filter((p) => p.ts >= nowMs - windowMs); if (recent.length < 2) return null;
// Calculate log returns const returns: number[] = []; for (let i = 1; i < recent.length; i++) { const prev = recent[i - 1]!; const curr = recent[i]!; if (prev.price > 0) { returns.push(Math.log(curr.price / prev.price)); } }
if (returns.length < 2) return null;
// Standard deviation of returns const mean = returns.reduce((a, b) => a + b, 0) / returns.length; const variance = returns.reduce((sum, r) => sum + (r - mean) ** 2, 0) / returns.length; const stdDev = Math.sqrt(variance);
// Annualize based on observation frequency const avgIntervalMs = windowMs / recent.length; const intervalsPerYear = (365.25 * 24 * 60 * 60 * 1000) / avgIntervalMs; const annualizedVol = stdDev * Math.sqrt(intervalsPerYear);
// Clamp to reasonable range return Math.max(0.1, Math.min(annualizedVol, 3.0));}Edge Calculation
Section titled “Edge Calculation”Edge is the difference between our theoretical probability and the market’s implied probability, minus fees.
flowchart LR
subgraph "Input"
Theo["Theoretical Prob<br/>(from Black-Scholes)"]
YesAsk["YES Ask Price<br/>(Polymarket)"]
NoAsk["NO Ask Price<br/>(Polymarket)"]
end
subgraph "Edge Calculation"
YesEdge["YES Edge =<br/>Theo - YesAsk"]
NoEdge["NO Edge =<br/>(1 - Theo) - NoAsk"]
Gross["Gross Edge =<br/>max(YesEdge, NoEdge)"]
Net["Net Edge =<br/>Gross - Fees"]
end
subgraph "Decision"
Thresh["Net Edge ><br/>Threshold?"]
Trade["Trade!"]
Skip["Skip"]
end
Theo --> YesEdge
YesAsk --> YesEdge
Theo --> NoEdge
NoAsk --> NoEdge
YesEdge --> Gross
NoEdge --> Gross
Gross --> Net
Net --> Thresh
Thresh -->|Yes| Trade
Thresh -->|No| Skip
style Trade fill:#10b981,stroke:#059669,color:#fff
style Skip fill:#6b7280,stroke:#4b5563,color:#fff
Fee Accounting
Section titled “Fee Accounting”// Round-trip fees = entry fee + exit feeconst takerFeeDecimal = params.takerFeeBps / 10000; // 100 bps = 1%const roundTripFees = takerFeeDecimal * 2; // 2% round tripconst netEdge = Math.abs(edge) - roundTripFees;Model Uncertainty Buffer
Section titled “Model Uncertainty Buffer”The Black-Scholes model has known limitations. The strategy dynamically increases the edge threshold in scenarios where the model is less reliable:
| Condition | Buffer | Rationale |
|---|---|---|
| Base uncertainty | +2% always | No model is perfect |
| Deep ITM/OTM strikes | +0-5% | Log-normal assumptions break down at extremes |
| Tail probabilities (<5% or >95%) | +0-5% | Model least accurate at extremes |
| Long-dated options (>7 days) | +1% | More time = more uncertainty |
Uncertainty Calculation
Section titled “Uncertainty Calculation”// From src/strategies/latency-arb/math.tsexport function computeModelUncertainty( theoreticalProb: number, currentPrice: number, strikePrice: number, volatility: number, timeToExpiryMs: number, baseEdgeThreshold: number,): ModelUncertainty { const BASE_UNCERTAINTY = 0.02; // Always 2%
// Moneyness: how far is spot from strike? const logMoneyness = Math.abs(Math.log(currentPrice / strikePrice)); const moneynessAdjustment = Math.min(logMoneyness * 0.1, 0.05);
// Tail probability adjustment let tailAdjustment = 0; if (theoreticalProb < 0.1 || theoreticalProb > 0.9) { const distanceFromExtreme = Math.min(theoreticalProb, 1 - theoreticalProb); tailAdjustment = 0.03 * (1 - distanceFromExtreme / 0.1); } if (theoreticalProb < 0.05 || theoreticalProb > 0.95) { tailAdjustment += 0.02; // Extra buffer for extreme tails }
// Time adjustment for long-dated options const timeYears = timeToExpiryMs / (365.25 * 24 * 60 * 60 * 1000); const timeAdjustment = timeYears > 7 / 365 ? 0.01 : 0;
const totalUncertainty = BASE_UNCERTAINTY + moneynessAdjustment + tailAdjustment + timeAdjustment;
return { adjustedEdgeThreshold: baseEdgeThreshold + totalUncertainty, // ... other fields };}Kelly Criterion Position Sizing
Section titled “Kelly Criterion Position Sizing”The Kelly criterion determines optimal bet sizing to maximize long-term growth while avoiding ruin.
The Formula
Section titled “The Formula”For a binary bet with:
- p = Probability of winning (our theoretical probability)
- q = Probability of losing (1 - p)
- b = Payout odds (win amount per dollar risked)
Kelly Fraction = (p × b - q) / bFor Polymarket binary options, the payout odds are derived from the price:
b = (1 / price) - 1If you buy YES at $0.40, you win $1 if correct and lose $0.40 if wrong:
- Win: $1.00 - $0.40 = $0.60 profit
- Lose: $0.40 loss
- Odds: b = 0.60 / 0.40 = 1.5 (same as (1/0.40) - 1)
Position Sizing Code
Section titled “Position Sizing Code”// From src/strategies/latency-arb/kelly.tsexport function computeKellySize( input: KellyInput, params: KellyParams,): number { const { theoreticalProb, marketPrice, edge } = input; const { bankroll, fraction, minSize, maxSize, uncertaintyDiscount = 0, correlatedPositions = 0 } = params;
if (edge <= 0 || bankroll <= 0) return 0;
const p = theoreticalProb; const q = 1 - p; const b = 1 / marketPrice - 1; // Payout odds
if (b <= 0) return 0;
// Standard Kelly formula const kellyFraction = (p * b - q) / b; if (kellyFraction <= 0) return 0;
// Apply fraction (e.g., 0.25 for quarter-Kelly) let adjustedKelly = kellyFraction * fraction;
// Uncertainty discount for model risk if (uncertaintyDiscount > 0) { adjustedKelly *= (1 - uncertaintyDiscount); }
// Reduce sizing when holding correlated positions if (correlatedPositions > 1) { adjustedKelly /= Math.sqrt(correlatedPositions); }
// Convert to dollar amount and then to contracts const dollarSize = bankroll * adjustedKelly; const contractSize = Math.floor(dollarSize / marketPrice);
return Math.max(minSize, Math.min(maxSize, contractSize));}Why Quarter-Kelly?
Section titled “Why Quarter-Kelly?”Full Kelly betting is mathematically optimal but practically volatile. The strategy uses quarter-Kelly (fraction: 0.25) for several reasons:
- Parameter uncertainty: Our probability estimate isn’t perfect
- Volatility reduction: Quarter-Kelly has ~1/4 the variance of full Kelly
- Psychological sustainability: Smaller drawdowns are easier to stomach
- Edge degradation: Our edge might shrink as we trade
xychart-beta
title "Kelly Fraction vs Expected Growth vs Volatility"
x-axis [0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5]
y-axis "Relative Performance" 0 --> 1.2
line "Expected Growth" [0, 0.75, 0.94, 0.99, 1.0, 0.94, 0.75]
line "Volatility" [0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5]
Worked Example: Bitcoin Market
Section titled “Worked Example: Bitcoin Market”Let’s walk through a complete example with real numbers.
Scenario Setup
Section titled “Scenario Setup”Step 1: Parse the Market
Section titled “Step 1: Parse the Market”const parsed = parseStrike("bitcoin-above-92000-jan-12");// Result: { asset: "BTC", strikePrice: 92000, direction: "above" }✅ Condition 1 passed: Market slug contains “bitcoin”, maps to “BTC”
Step 2: Get Reference Price
Section titled “Step 2: Get Reference Price”const refPrice = snapshot.referencePrices.find(r => r.symbol === "BTC");// Result: { symbol: "BTC", price: 91620, tsMs: 1736694000000 }✅ Condition 2 passed: Binance price available
Step 3: Calculate Volatility
Section titled “Step 3: Calculate Volatility”With 5 minutes of price history and ~300 price samples:
const volatility = computeVolatility(history, 300000, nowMs);// Result: 0.45 (45% annualized)✅ Condition 3 passed: 45% > 10% minimum
Step 4: Check Cooldown
Section titled “Step 4: Check Cooldown”const lastTrade = state.lastTradeTs.get(marketId) ?? 0;const elapsed = nowMs - lastTrade;// Result: 5000ms since last trade✅ Condition 4 passed: 5000ms > 3000ms cooldown
Step 5: Calculate Theoretical Probability
Section titled “Step 5: Calculate Theoretical Probability”const timeToExpiry = 4 * 60 * 60 * 1000; // 4 hours in msconst theo = computeTheoreticalProbability(91620, 92000, "above", 0.45, timeToExpiry);Manual calculation:
T = 4h / (365.25 × 24h) = 0.000456 years√T = 0.0214
d = [ln(91620/92000) - 0.45² × 0.000456 / 2] / (0.45 × 0.0214)d = [ln(0.99587) - 0.000046] / 0.00963d = [-0.00414 - 0.000046] / 0.00963d = -0.435
P(above) = Φ(-0.435) = 0.332 (33.2%)Result: Theoretical probability = 33.2%
Step 6: Calculate Edge
Section titled “Step 6: Calculate Edge”const yesEdge = 0.332 - 0.42; // -8.8% (YES overpriced)const noEdge = 0.668 - 0.60; // +6.8% (NO underpriced)
const grossEdge = 0.068; // Best edge is on NO sideconst roundTripFees = 0.02; // 2% (1% entry + 1% exit)const netEdge = 0.068 - 0.02 = 0.048; // 4.8%Step 7: Calculate Model Uncertainty
Section titled “Step 7: Calculate Model Uncertainty”const uncertainty = computeModelUncertainty(0.332, 91620, 92000, 0.45, timeToExpiry, 0.025);Components:
- Base uncertainty: +2%
- Moneyness: ln(91620/92000) = -0.004, adjustment = 0.04% (negligible)
- Tail: 33.2% is not extreme, no adjustment
- Time: 4 hours < 7 days, no adjustment
Effective threshold: 2.5% + 2% = 4.5%
Step 8: Compare Edge vs Threshold
Section titled “Step 8: Compare Edge vs Threshold”const shouldTrade = netEdge > effectiveThreshold;// 4.8% > 4.5% → TRUE✅ Condition 5 passed: 4.8% net edge > 4.5% threshold
Step 9: Check Price Bounds
Section titled “Step 9: Check Price Bounds”const noAsk = 0.60;const inBounds = noAsk > 0.01 && noAsk < 0.99;// TRUE✅ Condition 6 passed: $0.60 is within bounds
Step 10: Check Position Limit
Section titled “Step 10: Check Position Limit”const currentDownSize = position?.downSize ?? 0; // 0const underLimit = currentDownSize < 50;// TRUE✅ Condition 7 passed: No existing position
Step 11: Calculate Kelly Size
Section titled “Step 11: Calculate Kelly Size”const kellyInput = { theoreticalProb: 0.668, // NO probability marketPrice: 0.60, // NO ask edge: 0.048};const kellyParams = { bankroll: 10000, fraction: 0.25, // Quarter-Kelly minSize: 5, maxSize: 50};const size = computeKellySizeForNo(kellyInput, kellyParams);Manual calculation:
p = 0.668 (probability NO wins)q = 0.332b = 1/0.60 - 1 = 0.667 (payout odds)
Kelly = (p × b - q) / bKelly = (0.668 × 0.667 - 0.332) / 0.667Kelly = (0.445 - 0.332) / 0.667Kelly = 0.169 (16.9%)
Quarter-Kelly = 0.169 × 0.25 = 0.0423 (4.23%)
Dollar size = $10,000 × 0.0423 = $423Contract size = floor($423 / $0.60) = 705 contractsClamped to max = min(705, 50) = 50 contractsResult: Buy 50 NO contracts at $0.60
Final Trade
Section titled “Final Trade”intents.push({ type: "PlaceOrder", marketId: market.marketId, token: "DOWN", // NO side side: "BUY", price: 0.60, size: 50, reason: "latency_arb: theo=0.332 < mkt=0.400, edge=6.8%, netEdge=4.8%, vol=45%, size=50", orderType: "FAK" // Fill-and-kill});Outcome Analysis
Section titled “Outcome Analysis”With 66.8% theoretical probability of winning, expected value:
EV = 0.668 × $20 - 0.332 × $30 = $13.36 - $9.96 = +$3.40 per tradeExample: No Trade (Insufficient Edge)
Section titled “Example: No Trade (Insufficient Edge)”Using the same market, but now BTC is at $91,900 (closer to strike):
const theo = computeTheoreticalProbability(91900, 92000, "above", 0.45, timeToExpiry);// Result: 0.421 (42.1%)
const yesEdge = 0.421 - 0.42 = 0.001; // 0.1%const noEdge = 0.579 - 0.60 = -0.021; // -2.1% (NO overpriced!)
const grossEdge = 0.001;const netEdge = 0.001 - 0.02 = -0.019; // Negative after fees!❌ Condition 5 failed: -1.9% net edge < 4.5% threshold
Result: No trade. The market is fairly priced after accounting for fees.
Configuration Reference
Section titled “Configuration Reference”Here’s the complete latency arbitrage configuration from configs/paper-realistic.yaml:
strategies: enabled: [latency_arb]
latency_arb: # Minimum net edge to trigger a trade (after fees) edgeThreshold: 0.025 # 2.5%
# Maximum position per market maxPositionSize: 50
# Minimum time between trades on same market cooldownMs: 3000 # 3 seconds
# Lookback window for volatility calculation volatilityWindowMs: 300000 # 5 minutes
# Skip trading if volatility below this threshold minVolatility: 0.10 # 10% annualized
# Fee assumption (basis points) takerFeeBps: 100 # 1%
# Asset name patterns to symbol mapping assetMapping: bitcoin: BTC btc: BTC ethereum: ETH eth: ETH solana: SOL sol: SOL
# Kelly criterion position sizing kelly: enabled: true fraction: 0.25 # Quarter-Kelly minSize: 5 # Minimum contracts maxSize: 250 # Maximum contracts bankroll: 10000 # Starting capital for sizingKey Takeaways
Section titled “Key Takeaways”-
Model uncertainty is additive: The effective threshold is
edgeThreshold + modelUncertainty, typically 4.5-7% depending on market conditions. -
Kelly sizing prevents ruin: Quarter-Kelly means risking ~4% of bankroll per trade even on high-edge opportunities.
-
Volatility is critical: Without volatility, probability calculations are meaningless. The strategy won’t trade in low-vol environments.
-
Cooldowns prevent overtrading: 3-second cooldown per market prevents chasing the same opportunity multiple times.
Related Documentation
Section titled “Related Documentation”- Trading Strategies Overview - High-level strategy concepts
- Risk Management - How trades are validated and limited
- Configuration Reference - Full config schema
- Paper Trading Guide - Testing strategies safely