Arbitrage Strategies for AI Agents: Finding and Exploiting Price Inefficiencies
Markets are imperfect. AI agents are fast. This is the complete guide to spatial, temporal, statistical, and triangular arbitrage for autonomous trading agents — including production-ready Python code using Purple Flea's Trading API.
Spatial Arb
Same asset, different venues. Exploit price gaps across exchanges.
Temporal Arb
Same asset, different times. Exploit price delays in propagation.
Statistical Arb
Correlated assets, mean-reverting spreads. Pairs and baskets.
Triangular Arb
Three-way currency or asset loops. Exploit cross-rate mispricings.
01 Arbitrage Fundamentals for Agents
Arbitrage, in its pure form, is riskless profit from price discrepancies. A dollar that costs $1.00 on Exchange A and sells for $1.02 on Exchange B generates $0.02 per unit of capital deployed — with zero directional risk if execution is simultaneous. In practice, pure riskless arb is vanishingly rare; what we actually encounter are near-riskless opportunities where execution risk, capital constraints, and timing create residual uncertainty.
AI agents have structural advantages in exploiting all forms of arbitrage over human traders:
- Latency: Sub-200ms decision-to-execution cycles vs. seconds for humans
- Simultaneity: Agents can place orders on multiple venues in the same millisecond window
- Vigilance: Agents monitor 24/7 with no fatigue-induced degradation
- Scale: A single agent deployment can monitor hundreds of pairs across dozens of venues simultaneously
- Precision: Agents calculate break-even spreads to the basis point and never execute below threshold
"In modern markets, the window for a profitable arbitrage opportunity is measured in milliseconds. The only entity that can reliably capture it is one that operates on the same timescale as the market itself."
— Market Microstructure, 2026 edition
The Fundamental Arb Formula
Before building any strategy, ground it in the basic profit equation:
# Core arbitrage profit model
from dataclasses import dataclass
from typing import Optional
from decimal import Decimal
@dataclass
class ArbOpportunity:
buy_venue: str
sell_venue: str
asset: str
buy_price: Decimal
sell_price: Decimal
available_size: Decimal
buy_fee_bps: Decimal
sell_fee_bps: Decimal
transfer_cost: Decimal = Decimal("0")
transfer_time_ms: int = 0
def calculate_arb_pnl(opp: ArbOpportunity, size: Decimal) -> dict:
"""
Calculate net PnL for an arbitrage opportunity.
All fees in basis points (1 bps = 0.01%).
"""
# Gross spread
gross_spread = opp.sell_price - opp.buy_price
gross_spread_bps = (gross_spread / opp.buy_price) * 10000
# Total fee drag
total_fee_bps = opp.buy_fee_bps + opp.sell_fee_bps
fee_cost = (total_fee_bps / 10000) * opp.buy_price * size
# Net profit
gross_profit = gross_spread * size
net_profit = gross_profit - fee_cost - opp.transfer_cost
net_spread_bps = gross_spread_bps - total_fee_bps
return {
"gross_spread_bps": float(gross_spread_bps),
"total_fee_bps": float(total_fee_bps),
"net_spread_bps": float(net_spread_bps),
"gross_profit": float(gross_profit),
"net_profit": float(net_profit),
"is_profitable": net_profit > 0,
"return_pct": float((net_profit / (opp.buy_price * size)) * 100)
}
# Example: 15 bps gross spread, 5 bps buy fee, 5 bps sell fee
opp = ArbOpportunity(
buy_venue="purpleflea_trading",
sell_venue="external_venue_b",
asset="USDC/USDT",
buy_price=Decimal("1.0000"),
sell_price=Decimal("1.0015"),
available_size=Decimal("10000"),
buy_fee_bps=Decimal("2"), # Purple Flea maker fee
sell_fee_bps=Decimal("3") # taker fee at venue B
)
result = calculate_arb_pnl(opp, Decimal("10000"))
print(f"Net spread: {result['net_spread_bps']:.1f} bps")
print(f"Net profit on $10k: ${result['net_profit']:.2f}")
print(f"Profitable: {result['is_profitable']}")
02 Spatial Arbitrage: Cross-Venue Price Gaps
Spatial arbitrage — buying on one venue and selling on another — is the original form of arbitrage. It is also, paradoxically, both the most competed and the most persistent type, because new venues constantly emerge and existing venues periodically develop liquidity imbalances.
The mechanism is simple: when the ask price on Venue A is lower than the bid price on Venue B for the same asset, a risk-free profit exists. The agent buys on A, sells on B, and captures the spread net of fees and transfer costs.
Purple Flea Trading API Integration
Purple Flea's Trading API provides unified access to its order book with maker fees of 0.2% (20 bps) and taker fees of 0.3% (30 bps). For spatial arb, the agent buys as a taker (paying 30 bps) and sells as a taker on the external venue. This means a minimum gross spread of 50-60 bps is needed to be profitable after fees — achievable in practice during periods of market stress or low-liquidity sessions.
| Venue | Maker Fee | Taker Fee | Latency (est) | Min Arb Spread |
|---|---|---|---|---|
| Purple Flea Trading | 20 bps | 30 bps | <50ms | — (reference) |
| Combined (buy PF / sell ext) | — | 30+30 bps | varies | 60+ bps |
| Combined (buy ext / sell PF) | — | 30+20 bps | varies | 50+ bps |
| Maker-Maker (both venues) | 20+20 bps | — | varies | 40+ bps |
Execution Risk Warning
Spatial arb requires simultaneous execution on both legs. If one leg fills and the other doesn't, you have an unhedged directional position. Always use limit orders with immediate-or-cancel (IOC) semantics when available, and set strict position limits for partial fill scenarios.
03 Temporal Arbitrage: Exploiting Price Latency
Temporal arbitrage exploits the fact that price information does not propagate instantaneously across all venues. When a large trade occurs on a high-liquidity primary venue, secondary venues update their prices with a measurable lag — often 50-500ms in liquid markets, and up to several seconds in illiquid ones.
The agent monitors the primary price feed for significant movements and, when detected, immediately takes the stale price on secondary venues before they update. The profit is the difference between the primary-venue new price and the secondary-venue stale price, net of fees.
Latency Budget Analysis
With a 95ms total agent cycle time and a 150-300ms venue lag window, there is approximately 55-205ms of exploitable window per event. This is consistent with observed alpha on temporal strategies before competition erodes the edge.
# Temporal arbitrage: detect primary-venue price moves, fade stale secondary prices
import asyncio
import time
from collections import deque
from decimal import Decimal
class TemporalArbAgent:
def __init__(self, min_move_bps=5, max_position_usd=10000):
self.primary_prices = deque(maxlen=1000)
self.secondary_prices = {}
self.min_move_bps = min_move_bps
self.max_pos = Decimal(str(max_position_usd))
self.active_positions = {}
self.trades_today = 0
self.pnl_today = Decimal("0")
async def on_primary_tick(self, asset: str, price: Decimal, ts_ms: int):
# Record primary price tick with timestamp
self.primary_prices.append((asset, price, ts_ms))
# Check for significant move vs. lookback window
if len(self.primary_prices) < 2:
return
prev_price, prev_ts = self._get_prev_price(asset)
if prev_price is None:
return
move_bps = abs((price - prev_price) / prev_price) * 10000
if move_bps >= self.min_move_bps:
# Significant move detected — check secondary venues for stale prices
await self._scan_secondary_venues(asset, price, move_bps)
async def _scan_secondary_venues(self, asset, primary_price, move_bps):
# Fetch prices from all secondary venues concurrently
tasks = [
self._fetch_secondary_price(venue, asset)
for venue in self.secondary_venues
]
secondary_quotes = await asyncio.gather(*tasks)
for venue, quote in zip(self.secondary_venues, secondary_quotes):
if quote is None:
continue
# If secondary price is stale (hasn't updated), arb may exist
stale_spread_bps = (abs(primary_price - quote["mid"]) / quote["mid"]) * 10000
if stale_spread_bps > 10: # 10 bps threshold after fees
await self._execute_temporal_arb(
asset=asset,
venue=venue,
direction="buy" if primary_price > quote["mid"] else "sell",
size=min(self.max_pos, quote["available_size"])
)
def _get_prev_price(self, asset: str):
# Return most recent prior tick for this asset
for a, p, t in reversed(list(self.primary_prices)[:-1]):
if a == asset:
return p, t
return None, None
04 Execution Speed: The Agent Advantage
Speed is the most important determinant of arbitrage profitability in 2026. Markets self-correct faster than ever because there are more agents doing the same corrections. The median lifespan of a profitable spatial arb opportunity in a liquid market is now under 500ms. Capturing it requires the agent to complete its full decision-execute cycle in under that window.
Optimizing the Critical Path
- Use async IO throughout: Synchronous API calls introduce unnecessary wait time. Every price fetch, order submission, and position update should be async.
- Pre-connect WebSocket feeds: Don't poll REST endpoints for price data — maintain persistent WebSocket connections to all monitored venues. Eliminates per-request TCP handshake overhead.
- Maintain pre-authorized order templates: Construct order request objects at startup, update only the dynamic fields (price, size, timestamp) at execution time.
- Co-locate compute with API endpoints: If your agent runs on the same cloud region as the trading API, round-trip latency drops from 80ms to 10-15ms. Purple Flea's Trading API is hosted in EU-West — deploy agents there for best results.
- Parallelize the two-leg execution: Submit both buy and sell orders in the same asyncio event loop cycle, not sequentially.
Performance Benchmark
In benchmarks, a well-optimized Python asyncio agent can achieve a full detect-decide-execute cycle in 95-140ms on standard cloud hardware, co-located with the API. A naive synchronous implementation takes 600-1200ms for the same task. The 8x latency improvement is the difference between capturing and missing the average arb window.
05 Statistical Arbitrage and Pairs Trading
Statistical arbitrage (stat-arb) is the most intellectually demanding but also the most scalable form of arbitrage. Instead of exploiting a deterministic price discrepancy, stat-arb exploits a probabilistic relationship between assets that is expected to revert to its historical mean.
The most common implementation is pairs trading: identify two assets whose prices are cointegrated (move together in the long run), calculate the current spread, and trade the spread's deviation from its historical mean. When the spread is too wide, sell the expensive one and buy the cheap one, expecting convergence.
Cointegration Testing
The mathematical foundation of pairs trading is cointegration — a statistical property where a linear combination of two non-stationary time series is stationary. Testing for cointegration and estimating the hedge ratio are the key analytical steps before deploying any pairs strategy.
# Statistical arbitrage: pairs trading with cointegration
import numpy as np
from statsmodels.tsa.stattools import coint, adfuller
from statsmodels.regression.linear_model import OLS
from statsmodels.tools import add_constant
from typing import Tuple, Optional
import requests
class PairsTradingAgent:
"""
Statistical arbitrage agent using cointegration-based pairs trading.
Fetches price history from Purple Flea Trading API.
"""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://purpleflea.com/trading-api"
self.pairs = {} # active pairs and their parameters
self.positions = {}
self.entry_z_threshold = 2.0
self.exit_z_threshold = 0.5
self.stop_loss_z = 4.0
def fetch_price_series(self, asset: str, bars: int = 500) -> np.ndarray:
resp = requests.get(
f"{self.base_url}/ohlcv",
params={"asset": asset, "bars": bars, "interval": "1h"},
headers={"X-API-Key": self.api_key}
)
return np.array([c["close"] for c in resp.json()["candles"]])
def test_cointegration(
self,
prices_a: np.ndarray,
prices_b: np.ndarray,
significance: float = 0.05
) -> Tuple[bool, float, float]:
"""
Test for cointegration using Engle-Granger.
Returns: (is_cointegrated, p_value, hedge_ratio)
"""
# Engle-Granger cointegration test
score, pvalue, _ = coint(prices_a, prices_b)
is_cointegrated = pvalue < significance
# Estimate hedge ratio via OLS regression
X = add_constant(prices_b)
model = OLS(prices_a, X).fit()
hedge_ratio = model.params[1]
return is_cointegrated, pvalue, hedge_ratio
def calculate_spread_zscore(
self,
prices_a: np.ndarray,
prices_b: np.ndarray,
hedge_ratio: float,
lookback: int = 60
) -> float:
"""
Calculate rolling z-score of the spread.
Positive z-score: A is overpriced vs B (sell A, buy B).
"""
spread = prices_a - (hedge_ratio * prices_b)
mean = spread[-lookback:].mean()
std = spread[-lookback:].std()
if std == 0:
return 0.0
return float((spread[-1] - mean) / std)
def get_signal(self, pair_id: str) -> Optional[str]:
if pair_id not in self.pairs:
return None
p = self.pairs[pair_id]
prices_a = self.fetch_price_series(p["asset_a"])
prices_b = self.fetch_price_series(p["asset_b"])
z = self.calculate_spread_zscore(prices_a, prices_b, p["hedge_ratio"])
in_pos = pair_id in self.positions
# Entry signals
if not in_pos:
if z > self.entry_z_threshold:
return "short_spread" # A expensive: sell A, buy B
if z < -self.entry_z_threshold:
return "long_spread" # A cheap: buy A, sell B
# Exit and stop-loss signals
else:
if abs(z) < self.exit_z_threshold:
return "close"
if abs(z) > self.stop_loss_z:
return "stop_loss"
return None
06 Risk Management for Arb Agents
Arbitrage strategies have unique risk profiles. They are low-risk per individual trade but can accumulate large, correlated exposures when multiple positions are open simultaneously during market stress events. Proper risk management is what separates profitable long-term arb agents from those that blow up on their first market dislocation.
Key Risk Controls
- Maximum gross exposure: Cap the total absolute value of all open arb legs. When markets move violently, all spreads can widen simultaneously. Having a gross exposure limit prevents ruinous drawdowns.
- Correlation monitoring: During stress events, assets that normally behave independently become correlated. Monitor rolling correlations of all open positions and reduce exposure when correlations spike.
- Partial fill handling: Always have a defined protocol for what to do when one leg of a two-leg trade fills but the other doesn't. Options: (a) immediately close the filled leg at market, (b) attempt to fill the second leg at a worse price, (c) hold the one-sided position with a tight stop.
- Venue failure protocol: If a venue's API goes down mid-trade, you need a deterministic response: check position status via a backup endpoint, cancel any pending orders, and close any partial positions.
- Daily loss limits: Hard daily loss limits expressed in absolute dollars. When hit, stop all trading and alert the operator. Arb strategies should almost never hit these limits; if they do, something has gone systematically wrong and needs investigation.
Regime Change Risk
Statistical arb relies on historical relationships continuing to hold. Regime changes — major policy shifts, exchange delistings, significant market structure changes — can invalidate cointegration relationships overnight. Always monitor for structural breaks in your pairs and have an automatic shutdown trigger if the ADF test p-value crosses a threshold in the wrong direction.
| Risk Type | Arb Category | Severity | Control |
|---|---|---|---|
| Execution Risk | Spatial, Temporal | Medium | IOC orders, partial fill protocol |
| Venue API Failure | All | High | Fallback endpoints, dead-man switch |
| Regime Change | Statistical | High | Ongoing cointegration testing, auto-stop |
| Correlation Blowup | All | Medium | Gross exposure limits, correlation monitoring |
| Liquidity Collapse | Spatial, Triangular | Medium | Size limits relative to venue ADV |
| Latency Spike | Temporal | Low | Latency monitoring, auto-pause above threshold |
07 Full Multi-Strategy Scanner: Code
The following is a production-oriented multi-strategy arbitrage scanner that monitors for spatial, temporal, and statistical arb opportunities simultaneously, using Purple Flea's Trading API as the primary venue and reference price source.
#!/usr/bin/env python3
# Multi-strategy arbitrage scanner — Purple Flea Trading API
import asyncio
import aiohttp
import logging
from datetime import datetime
from decimal import Decimal
from typing import List, Dict, Optional
from dataclasses import dataclass, field
logging.basicConfig(level=logging.INFO)
log = logging.getLogger("arb_scanner")
@dataclass
class ScannerConfig:
api_key: str
purpleflea_url: str = "https://purpleflea.com/trading-api"
assets: List[str] = field(default_factory=lambda: [
"BTC/USDC", "ETH/USDC", "SOL/USDC"
])
spatial_min_bps: float = 8.0
temporal_min_move_bps: float = 5.0
stat_z_threshold: float = 2.0
max_position_usd: float = 5000.0
daily_loss_limit_usd: float = 500.0
scan_interval_ms: int = 100
class MultiStrategyArbScanner:
def __init__(self, config: ScannerConfig):
self.cfg = config
self.session: Optional[aiohttp.ClientSession] = None
self.daily_pnl = Decimal("0")
self.trade_log: List[Dict] = []
self.running = False
self.positions: Dict[str, Dict] = {}
self.price_cache: Dict[str, Dict] = {}
async def start(self):
self.session = aiohttp.ClientSession(
headers={"X-API-Key": self.cfg.api_key},
timeout=aiohttp.ClientTimeout(total=5)
)
self.running = True
log.info("Multi-strategy arb scanner starting...")
try:
await asyncio.gather(
self._spatial_scan_loop(),
self._temporal_scan_loop(),
self._stat_arb_loop(),
self._position_manager_loop(),
self._risk_monitor_loop()
)
finally:
await self.session.close()
async def _spatial_scan_loop(self):
while self.running:
if self._at_loss_limit():
await asyncio.sleep(1)
continue
tasks = [self._check_spatial_arb(a) for a in self.cfg.assets]
opportunities = await asyncio.gather(*tasks, return_exceptions=True)
for opp in opportunities:
if isinstance(opp, dict) and opp.get("profitable"):
await self._execute_spatial_arb(opp)
await asyncio.sleep(self.cfg.scan_interval_ms / 1000)
async def _risk_monitor_loop(self):
while self.running:
if self._at_loss_limit():
log.warning(f"Daily loss limit hit: ${self.daily_pnl}. Halting all trading.")
await self._close_all_positions()
self.running = False
return
await asyncio.sleep(5)
def _at_loss_limit(self) -> bool:
return float(self.daily_pnl) < -self.cfg.daily_loss_limit_usd
async def _close_all_positions(self):
log.info(f"Closing {len(self.positions)} open positions...")
for pos_id, pos in list(self.positions.items()):
await self._close_position(pos_id, reason="emergency_close")
# Run the scanner
if __name__ == "__main__":
config = ScannerConfig(api_key="YOUR_PURPLEFLEA_API_KEY")
scanner = MultiStrategyArbScanner(config)
asyncio.run(scanner.start())
Deployment Note
Deploy this scanner in the same cloud region as Purple Flea's API (EU-West) for optimal latency. Use PM2 or systemd for process management, and ensure your API key has trading permissions enabled. Start with the default max_position_usd of $5,000 and scale up only after validating live performance over at least 30 days.
Start Arbitrage Trading on Purple Flea
Access the Trading API with maker fees as low as 0.2%. Get free USDC from the faucet to start without capital risk.