Strategy

Volatility Arbitrage Frameworks for AI Agents

Volatility arbitrage exploits the persistent gap between implied volatility priced into options markets and the volatility that assets actually realize. For AI agents with sub-second reaction times and continuous monitoring, vol arb represents one of the cleanest edge cases in modern markets: a structural inefficiency that rewards computational speed and mathematical precision over human intuition. This guide covers the complete vol arb stack — from surface modeling to Greeks hedging to live execution via Purple Flea.

What Is Volatility Arbitrage?

Volatility arbitrage is a market-neutral strategy that profits from the difference between an option's implied volatility (IV) — the market's expectation of future price movement encoded in the option price — and the realized volatility (RV) that actually occurs over the option's life.

Unlike directional trading, vol arb is agnostic to whether the underlying asset goes up or down. The bet is purely on the size of price swings. If you believe an asset will move less than the market implies, you sell volatility. If you expect larger swings, you buy volatility.

IV > RV
Sell vol (collect premium)
IV < RV
Buy vol (pay premium)
~2–4%
Historical IV-RV premium in crypto
Delta-neutral
Directional exposure hedged continuously

The Vol Risk Premium (VRP)

In most liquid markets — equities, FX, crypto — implied volatility systematically exceeds realized volatility over time. This volatility risk premium exists because option sellers demand compensation for bearing uncertainty. Buyers pay a structural premium for insurance. AI agents can systematically harvest this premium by selling options and delta-hedging the directional risk.

VRP = E[IV] - E[RV] ≈ 15-25 vol points in BTC (annualized) ≈ 8-12 vol points in ETH (annualized)

Vol Arb Variants

Why AI Agents Win at Vol Arb

Vol arb requires constant delta rehedging, rapid surface repricing, and systematic position sizing — all tasks where agents have an inherent advantage over human traders who fatigue, hesitate, or rely on approximate mental models.

Understanding the Volatility Surface

The volatility surface is a 3-D structure mapping implied volatility across all strikes and expiries for a given underlying. It encodes the market's entire distribution of future price outcomes. Vol arb opportunities often manifest as surface distortions — local inconsistencies that violate theoretical arbitrage bounds.

Surface Dimensions

Arbitrage-Free Constraints

A valid vol surface must satisfy several no-arbitrage conditions. Any violation represents a theoretical (though not always practical) arb opportunity:

Constraint Violation Type Trade
Calendar spread non-negative Back-month cheaper than front-month Buy back, sell front (calendar arb)
Butterfly spread non-negative Wing IV too low vs body Buy wings, sell body (fly arb)
Put-call parity Call vs put price divergence Conversion / reversal
Monotone in strike Non-monotone cumulative dist Strike spread arb
python
import numpy as np
from scipy.interpolate import RectBivariateSpline
from scipy.stats import norm

class VolSurface:
    """Implied volatility surface with arbitrage detection."""

    def __init__(self, strikes: list, expiries: list, ivs: np.ndarray):
        # ivs[i, j] = IV for strike strikes[i], expiry expiries[j]
        self.strikes = np.array(strikes)
        self.expiries = np.array(expiries)  # in years
        self.ivs = ivs
        self._spline = RectBivariateSpline(strikes, expiries, ivs, kx=3, ky=3)

    def get_iv(self, strike: float, expiry: float) -> float:
        """Interpolate IV for arbitrary strike/expiry."""
        return float(self._spline(strike, expiry))

    def calendar_violations(self) -> list:
        """Find calendar spread arbitrage (total variance must be non-decreasing)."""
        violations = []
        for i, K in enumerate(self.strikes):
            for j in range(len(self.expiries) - 1):
                T1, T2 = self.expiries[j], self.expiries[j+1]
                var1 = self.ivs[i, j]**2 * T1
                var2 = self.ivs[i, j+1]**2 * T2
                if var2 < var1 - 0.0001:  # tolerance
                    violations.append({
                        'strike': K,
                        'front_expiry': T1,
                        'back_expiry': T2,
                        'magnitude': var1 - var2
                    })
        return violations

    def butterfly_violations(self) -> list:
        """Find butterfly arb (density must be non-negative)."""
        violations = []
        dK = np.diff(self.strikes)[0]  # assume uniform grid
        for j, T in enumerate(self.expiries):
            for i in range(1, len(self.strikes) - 1):
                iv_left = self.ivs[i-1, j]
                iv_mid  = self.ivs[i, j]
                iv_right = self.ivs[i+1, j]
                # Breeden-Litzenberger density approximation
                call_price = lambda iv, K: self._bs_call(
                    self.strikes[i], K, T, iv)
                c_left  = call_price(iv_left,  self.strikes[i-1])
                c_mid   = call_price(iv_mid,   self.strikes[i])
                c_right = call_price(iv_right, self.strikes[i+1])
                density = (c_left - 2*c_mid + c_right) / dK**2
                if density < 0:
                    violations.append({
                        'strike': self.strikes[i],
                        'expiry': T,
                        'density': density
                    })
        return violations

    def _bs_call(self, S, K, T, sigma, r=0.0) -> float:
        if T <= 0: return max(0, S - K)
        d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
        d2 = d1 - sigma*np.sqrt(T)
        return S*norm.cdf(d1) - K*np.exp(-r*T)*norm.cdf(d2)

Greeks and Delta-Neutral Hedging

A vol arb position earns from vol mispricing, but it also carries directional exposure (delta), time decay (theta), and second-order vol sensitivity (vanna, volga). An agent must manage these exposures continuously.

Key Greeks for Vol Arb

Greek Definition Vol Arb Impact
Delta (Δ) dV/dS — price sensitivity Must be hedged continuously to isolate vol exposure
Gamma (Γ) d²V/dS² — delta rate of change Long gamma earns from realized vol exceeding theta cost
Theta (Θ) dV/dt — time decay Short vol positions earn theta daily
Vega (ν) dV/dIV — IV sensitivity Primary P&L driver; must match target vega exposure
Vanna d²V/(dS·dIV) Delta changes as IV moves; requires cross-hedging
Volga d²V/dIV² Convexity of vega; wings carry positive volga

P&L of a Delta-Hedged Option

The daily P&L of a delta-hedged option position is driven by the gamma P&L vs theta decay trade-off:

Daily PnL ≈ (1/2) × Γ × S² × (σ_realized² - σ_implied²) × dt Where: Γ = option gamma S = spot price σ_realized = actual daily return std dev (annualized) σ_implied = IV embedded in the option dt = 1/252 (one trading day)

When realized vol exceeds implied, a long gamma (long option) position earns. When realized falls short, a short gamma (short option) position earns. The vol arb thesis is simply a bet on which regime will persist.

python
import numpy as np
from scipy.stats import norm

class BlackScholesGreeks:
    """Fast analytical Greeks calculator."""

    def __init__(self, S, K, T, sigma, r=0.0, option_type='call'):
        self.S = S; self.K = K; self.T = T
        self.sigma = sigma; self.r = r
        self.option_type = option_type
        if T > 0:
            self.d1 = (np.log(S/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
            self.d2 = self.d1 - sigma*np.sqrt(T)
        else:
            self.d1 = self.d2 = np.inf if S >= K else -np.inf

    @property
    def delta(self) -> float:
        if self.option_type == 'call':
            return norm.cdf(self.d1)
        return norm.cdf(self.d1) - 1

    @property
    def gamma(self) -> float:
        return norm.pdf(self.d1) / (self.S * self.sigma * np.sqrt(self.T))

    @property
    def theta(self) -> float:
        # Per calendar day
        term1 = -(self.S * norm.pdf(self.d1) * self.sigma) / (2 * np.sqrt(self.T))
        if self.option_type == 'call':
            term2 = -self.r * self.K * np.exp(-self.r*self.T) * norm.cdf(self.d2)
        else:
            term2 = self.r * self.K * np.exp(-self.r*self.T) * norm.cdf(-self.d2)
        return (term1 + term2) / 365

    @property
    def vega(self) -> float:
        # Per 1% change in vol (expressed as decimal)
        return self.S * norm.pdf(self.d1) * np.sqrt(self.T) * 0.01

    @property
    def vanna(self) -> float:
        return -norm.pdf(self.d1) * self.d2 / self.sigma

    @property
    def volga(self) -> float:
        return self.S * norm.pdf(self.d1) * np.sqrt(self.T) * self.d1 * self.d2 / self.sigma

    def gamma_pnl(self, realized_vol: float, dt: float = 1/252) -> float:
        """Expected daily gamma P&L given realized vol."""
        return 0.5 * self.gamma * self.S**2 * (realized_vol**2 - self.sigma**2) * dt


class DeltaHedger:
    """Real-time delta hedging engine for vol arb positions."""

    def __init__(self, api_key: str, hedge_threshold: float = 0.05):
        self.api_key = api_key
        self.hedge_threshold = hedge_threshold  # rehedge if |delta_drift| > 5%
        self.positions = {}
        self.hedge_position = 0.0  # units of underlying held

    def add_position(self, option_id: str, greeks: BlackScholesGreeks, quantity: float):
        self.positions[option_id] = {'greeks': greeks, 'qty': quantity}

    def net_delta(self) -> float:
        total = 0.0
        for pos in self.positions.values():
            total += pos['greeks'].delta * pos['qty']
        total += self.hedge_position
        return total

    def needs_rehedge(self) -> bool:
        return abs(self.net_delta()) > self.hedge_threshold

    def compute_hedge_trade(self) -> float:
        """Returns quantity of underlying to buy/sell to flatten delta."""
        target_hedge = -sum(
            pos['greeks'].delta * pos['qty']
            for pos in self.positions.values()
        )
        return target_hedge - self.hedge_position

Realized Volatility Estimation

The quality of a vol arb strategy depends critically on accurate realized vol estimates. Several estimators exist, each with different bias and efficiency trade-offs.

Estimator Comparison

Estimator Inputs Efficiency vs Close-to-Close Best For
Close-to-Close Daily closes Baseline; simple
Parkinson High, Low ~5× Trending markets
Garman-Klass OHLC ~8× General use
Rogers-Satchell OHLC ~8× Drift-present markets
Yang-Zhang OHLC + prev close ~14× Best overall efficiency
Realized (tick) Tick data Varies HF; sensitive to microstructure
python
import numpy as np
import pandas as pd

class RealizedVolEstimator:
    """Multiple realized volatility estimators."""

    def __init__(self, df: pd.DataFrame, trading_days: int = 252):
        """df must have columns: open, high, low, close (all log-prices OK)."""
        self.o = np.log(df['open'])
        self.h = np.log(df['high'])
        self.l = np.log(df['low'])
        self.c = np.log(df['close'])
        self.c_prev = self.c.shift(1)
        self.ann = trading_days

    def close_to_close(self, window: int = 20) -> pd.Series:
        returns = self.c.diff()
        return returns.rolling(window).std() * np.sqrt(self.ann)

    def parkinson(self, window: int = 20) -> pd.Series:
        hl_sq = (self.h - self.l)**2
        factor = 1 / (4 * np.log(2))
        daily_var = factor * hl_sq
        return np.sqrt(daily_var.rolling(window).mean() * self.ann)

    def garman_klass(self, window: int = 20) -> pd.Series:
        hl_term = 0.5 * (self.h - self.l)**2
        co_term = -(2*np.log(2)-1) * (self.c - self.o)**2
        daily_var = (hl_term + co_term)
        return np.sqrt(daily_var.rolling(window).mean() * self.ann)

    def yang_zhang(self, window: int = 20) -> pd.Series:
        oc = self.o - self.c_prev
        co = self.c - self.o
        sigma_oc = oc.rolling(window).var()
        sigma_co = co.rolling(window).var()
        k = 0.34 / (1.34 + (window+1)/(window-1))
        rs_term = (self.h-self.o)*(self.h-self.c) + (self.l-self.o)*(self.l-self.c)
        sigma_rs = rs_term.rolling(window).mean()
        daily_var = sigma_oc + k*sigma_co + (1-k)*sigma_rs
        return np.sqrt(daily_var * self.ann)

    def best_estimate(self, window: int = 20) -> pd.Series:
        """Yang-Zhang with bias correction."""
        raw = self.yang_zhang(window)
        # Small-sample bias correction
        correction = 1 + 1 / (4 * (window - 1))
        return raw / correction

Vol Arb Framework Class

A complete VolArbFramework class ties together surface analysis, realized vol estimation, signal generation, position sizing, and execution. This is the core engine your agent deploys.

python
import asyncio
import httpx
import numpy as np
import pandas as pd
from dataclasses import dataclass, field
from typing import Optional
from datetime import datetime

PURPLE_FLEA_BASE = "https://api.purpleflea.com/v1"

@dataclass
class VolArbSignal:
    underlying: str
    strike: float
    expiry: float      # years to expiry
    option_type: str   # 'call' or 'put'
    implied_vol: float
    realized_vol: float
    vrp: float         # IV - RV (positive = sell opportunity)
    z_score: float     # standardized signal strength
    recommended_action: str  # 'sell_vol', 'buy_vol', 'hold'

@dataclass
class VolPosition:
    signal: VolArbSignal
    option_units: float   # positive = long, negative = short
    delta_hedge: float    # units of underlying for delta hedge
    vega_exposure: float  # target vega in dollars
    entry_price: float
    entry_time: datetime = field(default_factory=datetime.utcnow)

class VolArbFramework:
    """
    Complete volatility arbitrage framework for AI agents.
    Operates on Purple Flea trading infrastructure.
    """

    def __init__(self,
                 api_key: str,
                 max_vega_usd: float = 10000,
                 vrp_threshold: float = 0.04,
                 z_entry: float = 1.5,
                 z_exit: float = 0.3):
        self.api_key = api_key   # pf_live_... style key
        self.max_vega_usd = max_vega_usd
        self.vrp_threshold = vrp_threshold  # minimum IV-RV gap to trade
        self.z_entry = z_entry
        self.z_exit = z_exit
        self.positions: list[VolPosition] = []
        self.signal_history: list[VolArbSignal] = []
        self.hedger = DeltaHedger(api_key)
        self.vol_estimator = None  # set after loading OHLC data
        self._session = None

    async def initialize(self, underlying: str, lookback_days: int = 60):
        """Fetch OHLC data and warm up estimators."""
        self._session = httpx.AsyncClient(
            headers={"Authorization": f"Bearer {self.api_key}"},
            timeout=30
        )
        ohlc = await self._fetch_ohlc(underlying, lookback_days)
        self.vol_estimator = RealizedVolEstimator(ohlc)
        self.current_rv = float(self.vol_estimator.best_estimate().iloc[-1])

    async def _fetch_ohlc(self, underlying: str, days: int) -> pd.DataFrame:
        resp = await self._session.get(
            f"{PURPLE_FLEA_BASE}/market/ohlc",
            params={"asset": underlying, "days": days, "interval": "1d"}
        )
        data = resp.json()["candles"]
        return pd.DataFrame(data, columns=['open','high','low','close'])

    async def scan_surface(self, underlying: str) -> list[VolArbSignal]:
        """Fetch live options chain and identify vol arb signals."""
        resp = await self._session.get(
            f"{PURPLE_FLEA_BASE}/options/chain",
            params={"asset": underlying}
        )
        chain = resp.json()["options"]
        signals = []
        for opt in chain:
            iv = opt["implied_vol"]
            vrp = iv - self.current_rv
            # Compute z-score vs rolling signal history
            z = self._zscore(vrp)
            action = "hold"
            if z > self.z_entry and vrp > self.vrp_threshold:
                action = "sell_vol"
            elif z < -self.z_entry and vrp < -self.vrp_threshold:
                action = "buy_vol"
            sig = VolArbSignal(
                underlying=underlying,
                strike=opt["strike"],
                expiry=opt["days_to_expiry"] / 365,
                option_type=opt["type"],
                implied_vol=iv,
                realized_vol=self.current_rv,
                vrp=vrp,
                z_score=z,
                recommended_action=action
            )
            signals.append(sig)
            self.signal_history.append(sig)
        return [s for s in signals if s.recommended_action != "hold"]

    def _zscore(self, current_vrp: float) -> float:
        if len(self.signal_history) < 30:
            return 0.0
        historical_vrps = [s.vrp for s in self.signal_history[-252:]]
        mu = np.mean(historical_vrps)
        sigma = np.std(historical_vrps)
        if sigma < 0.0001: return 0.0
        return (current_vrp - mu) / sigma

    def size_position(self, signal: VolArbSignal, spot: float) -> float:
        """Kelly-capped vega sizing."""
        greeks = BlackScholesGreeks(
            S=spot, K=signal.strike,
            T=signal.expiry, sigma=signal.implied_vol,
            option_type=signal.option_type
        )
        vega_per_contract = greeks.vega * spot * 100  # assume 100x multiplier
        if vega_per_contract <= 0: return 0
        max_contracts = self.max_vega_usd / vega_per_contract
        # Scale by signal strength (z-score normalized to [0, 1])
        strength = min(1.0, abs(signal.z_score) / (3 * self.z_entry))
        return max(1, int(max_contracts * strength))

    async def execute_vol_arb(self, signal: VolArbSignal, spot: float):
        """Execute option + delta hedge via Purple Flea API."""
        qty = self.size_position(signal, spot)
        if qty == 0:
            return
        direction = "sell" if signal.recommended_action == "sell_vol" else "buy"
        # Leg 1: Option
        opt_resp = await self._session.post(
            f"{PURPLE_FLEA_BASE}/orders/option",
            json={
                "asset": signal.underlying,
                "strike": signal.strike,
                "expiry_days": int(signal.expiry * 365),
                "option_type": signal.option_type,
                "direction": direction,
                "quantity": qty,
                "order_type": "limit",
                "limit_iv": signal.implied_vol
            }
        )
        opt_order = opt_resp.json()
        # Leg 2: Delta hedge in underlying
        greeks = BlackScholesGreeks(
            S=spot, K=signal.strike,
            T=signal.expiry, sigma=signal.implied_vol,
            option_type=signal.option_type
        )
        hedge_qty = -greeks.delta * qty * (1 if direction == "buy" else -1)
        hedge_direction = "buy" if hedge_qty > 0 else "sell"
        hedge_resp = await self._session.post(
            f"{PURPLE_FLEA_BASE}/orders/spot",
            json={
                "asset": signal.underlying,
                "direction": hedge_direction,
                "quantity": abs(hedge_qty),
                "order_type": "market"
            }
        )
        return {"option_order": opt_order, "hedge_order": hedge_resp.json()}

    async def run_hedging_loop(self, interval_seconds: int = 60):
        """Continuous delta rehedging loop."""
        while True:
            if self.hedger.needs_rehedge():
                trade_qty = self.hedger.compute_hedge_trade()
                direction = "buy" if trade_qty > 0 else "sell"
                await self._session.post(
                    f"{PURPLE_FLEA_BASE}/orders/spot",
                    json={
                        "asset": "BTC",
                        "direction": direction,
                        "quantity": abs(trade_qty),
                        "order_type": "market"
                    }
                )
                self.hedger.hedge_position += trade_qty
            await asyncio.sleep(interval_seconds)

Term Structure Arbitrage

The volatility term structure — how IV varies with time to expiry — often creates arb opportunities when the curve moves to extremes. Calendar spreads and vol roll-down trades exploit these dynamics.

Vol Roll-Down

When the vol term structure is upward sloping (longer expiries have higher IV), a short option position benefits not just from theta decay but from the roll-down as the option moves to shorter expiry and lower IV — even if implied vol stays constant.

Roll-down P&L (daily) ≈ dIV/dT × (ΔT/day) × vega Where: dIV/dT = slope of term structure at current expiry ΔT/day = 1/365 (one day shorter to expiry) vega = position vega

Calendar Spread Construction

python
class CalendarArbAnalyzer:
    """Identify calendar spread arb in vol term structure."""

    def __init__(self, surface: VolSurface):
        self.surface = surface

    def term_structure_slope(self, strike: float) -> pd.Series:
        """dIV/dT at given strike across all expiries."""
        expiries = self.surface.expiries
        ivs = [self.surface.get_iv(strike, T) for T in expiries]
        slopes = np.gradient(ivs, expiries)
        return pd.Series(slopes, index=expiries, name='iv_slope')

    def find_roll_down_candidates(self,
                                     min_slope: float = 0.02,
                                     min_expiry: float = 14/365) -> list:
        """ATM options with best roll-down characteristics."""
        candidates = []
        atm_strike = self.surface.strikes[len(self.surface.strikes) // 2]
        slopes = self.term_structure_slope(atm_strike)
        for T, slope in slopes.items():
            if T >= min_expiry and slope > min_slope:
                iv = self.surface.get_iv(atm_strike, T)
                candidates.append({
                    'expiry': T,
                    'iv': iv,
                    'slope': slope,
                    'daily_roll_bps': slope * (1/365) * 10000
                })
        return sorted(candidates, key=lambda x: -x['daily_roll_bps'])

    def optimal_calendar_spread(self) -> dict:
        """Find front/back month pair maximizing vega-weighted mispricing."""
        violations = self.surface.calendar_violations()
        if not violations:
            return {}
        # Sort by magnitude of violation
        best = max(violations, key=lambda x: x['magnitude'])
        return {
            'strike': best['strike'],
            'buy_expiry': best['back_expiry'],   # buy the back month
            'sell_expiry': best['front_expiry'],  # sell the front month
            'magnitude': best['magnitude'],
            'trade': 'calendar_spread'
        }

Skew Arbitrage

Volatility skew describes how IV varies across strikes at a fixed expiry. Put skew — the premium of OTM put IV over ATM IV — reflects demand for downside protection. Skew arb bets on mean reversion of this premium versus the actual realized skew of returns.

Measuring Skew

25-delta Risk Reversal = IV(25Δ put) - IV(25Δ call) 25-delta Butterfly = [IV(25Δ put) + IV(25Δ call)] / 2 - IV(ATM) Skew excess = Current RR - Historical RR mean
python
class SkewArbAnalyzer:
    """Risk reversal and butterfly skew arbitrage signals."""

    def __init__(self, surface: VolSurface, spot: float):
        self.surface = surface
        self.spot = spot
        self.history_rr: list[float] = []
        self.history_bf: list[float] = []

    def _delta_to_strike(self, delta: float, T: float, atm_iv: float) -> float:
        """Approximate strike from delta (put convention, delta > 0)."""
        d1_target = norm.ppf(delta)
        log_moneyness = d1_target * atm_iv * np.sqrt(T) - 0.5 * atm_iv**2 * T
        return self.spot * np.exp(-log_moneyness)

    def risk_reversal(self, T: float) -> float:
        atm_iv = self.surface.get_iv(self.spot, T)
        put_25_strike = self._delta_to_strike(0.25, T, atm_iv)
        call_25_strike = self._delta_to_strike(0.75, T, atm_iv)  # call 25d
        iv_put = self.surface.get_iv(put_25_strike, T)
        iv_call = self.surface.get_iv(call_25_strike, T)
        rr = iv_put - iv_call
        self.history_rr.append(rr)
        return rr

    def butterfly(self, T: float) -> float:
        atm_iv = self.surface.get_iv(self.spot, T)
        put_25_strike = self._delta_to_strike(0.25, T, atm_iv)
        call_25_strike = self._delta_to_strike(0.75, T, atm_iv)
        iv_put = self.surface.get_iv(put_25_strike, T)
        iv_call = self.surface.get_iv(call_25_strike, T)
        bf = (iv_put + iv_call) / 2 - atm_iv
        self.history_bf.append(bf)
        return bf

    def skew_signal(self, T: float, z_threshold: float = 1.5) -> dict:
        rr = self.risk_reversal(T)
        bf = self.butterfly(T)
        rr_z = self._zscore(rr, self.history_rr)
        bf_z = self._zscore(bf, self.history_bf)
        trades = []
        if rr_z > z_threshold:
            trades.append({'type': 'sell_rr', 'z': rr_z,
                           'desc': 'Sell put, buy call (fade skew premium)'})
        elif rr_z < -z_threshold:
            trades.append({'type': 'buy_rr', 'z': rr_z,
                           'desc': 'Buy put, sell call (long skew)'})
        if bf_z > z_threshold:
            trades.append({'type': 'sell_fly', 'z': bf_z,
                           'desc': 'Sell wings, buy body (fade vol of vol)'})
        return {'rr': rr, 'bf': bf, 'rr_z': rr_z, 'bf_z': bf_z, 'trades': trades}

    def _zscore(self, current: float, history: list) -> float:
        if len(history) < 20: return 0.0
        mu, sigma = np.mean(history), np.std(history)
        return (current - mu) / sigma if sigma > 1e-8 else 0.0

Transaction Cost Modeling

Options markets have wide bid-ask spreads. A vol arb with positive theoretical edge can easily lose money once transaction costs are modeled in. Your agent must compute the break-even IV edge before executing any trade.

Break-even Edge = Bid-Ask Spread / (2 × Vega) Example: Option vega: $50 per 1% vol move Bid-ask spread: $0.80 (in premium) Crossing cost: $0.40 (half-spread) Break-even: 0.40 / 50 = 0.008 = 0.8 vol points minimum edge needed
python
class TransactionCostModel:
    """Model total cost of entering a vol arb position."""

    def __init__(self,
                 option_fee_per_contract: float = 0.002,  # 0.2% of premium
                 spot_fee_bps: float = 3,                  # 3 bps on hedge
                 slippage_bps: float = 2):                  # 2 bps assumed slippage
        self.option_fee_pct = option_fee_per_contract
        self.spot_fee_bps = spot_fee_bps / 10000
        self.slippage_bps = slippage_bps / 10000

    def option_cost(self, premium: float, quantity: int) -> float:
        return premium * quantity * self.option_fee_pct

    def hedge_cost(self, spot: float, delta_units: float) -> float:
        notional = spot * abs(delta_units)
        return notional * (self.spot_fee_bps + self.slippage_bps)

    def total_round_trip_cost(self,
                              premium: float,
                              quantity: int,
                              spot: float,
                              delta: float) -> float:
        # Entry + exit (2x) for option; entry + continuous rehedge estimate
        opt_cost = self.option_cost(premium, quantity) * 2
        hedge_cost_entry = self.hedge_cost(spot, delta * quantity)
        # Estimate rehedge cost: assume 5 rehedges over option life
        rehedge_cost = hedge_cost_entry * 5
        return opt_cost + hedge_cost_entry + rehedge_cost

    def break_even_iv_edge(self, premium: float, quantity: int,
                           spot: float, delta: float, vega: float) -> float:
        """Minimum IV edge needed to cover round-trip costs."""
        total_cost = self.total_round_trip_cost(premium, quantity, spot, delta)
        if vega * quantity == 0: return float('inf')
        return total_cost / (vega * quantity)

    def is_trade_viable(self,
                        iv_edge: float,  # positive = selling expensive vol
                        premium: float,
                        quantity: int,
                        spot: float,
                        delta: float,
                        vega: float) -> dict:
        be = self.break_even_iv_edge(premium, quantity, spot, delta, vega)
        net_edge = abs(iv_edge) - be
        return {
            'viable': net_edge > 0,
            'break_even_edge': be,
            'gross_edge': abs(iv_edge),
            'net_edge': net_edge,
            'edge_to_cost_ratio': abs(iv_edge) / be if be > 0 else 0
        }
Critical: Cost Modeling

Most vol arb backtests overstate returns because they ignore bid-ask spread crossing costs. In live crypto options markets, spreads of 5-20 vol points are common. Always filter signals by net edge after costs, not gross IV edge.

Risk Management for Vol Arb

Vol arb carries unique risks distinct from directional trading. Understanding and monitoring these is essential for agent longevity.

Primary Risk Factors

python
class VolArbRiskManager:
    """Portfolio-level risk controls for vol arb positions."""

    def __init__(self,
                 max_net_vega_usd: float = 50000,
                 max_net_gamma_usd: float = 5000,
                 max_drawdown_pct: float = 0.15,
                 daily_stop_loss_usd: float = 2000):
        self.max_net_vega_usd = max_net_vega_usd
        self.max_net_gamma_usd = max_net_gamma_usd
        self.max_drawdown_pct = max_drawdown_pct
        self.daily_stop_loss_usd = daily_stop_loss_usd
        self.daily_pnl = 0.0
        self.peak_pnl = 0.0

    def portfolio_greeks(self, positions: list[VolPosition], spot: float) -> dict:
        net_vega = net_gamma = net_delta = 0.0
        for pos in positions:
            sig = pos.signal
            g = BlackScholesGreeks(
                S=spot, K=sig.strike, T=sig.expiry,
                sigma=sig.implied_vol, option_type=sig.option_type
            )
            qty = pos.option_units
            net_vega  += g.vega * qty * spot * 100
            net_gamma += g.gamma * qty * spot**2 * 100 * 0.01
            net_delta += g.delta * qty
        net_delta += sum(pos.delta_hedge for pos in positions)
        return {'net_vega': net_vega, 'net_gamma': net_gamma, 'net_delta': net_delta}

    def risk_check(self, positions: list, spot: float, pnl: float) -> dict:
        greeks = self.portfolio_greeks(positions, spot)
        violations = []
        if abs(greeks['net_vega']) > self.max_net_vega_usd:
            violations.append(f"Vega limit: {greeks['net_vega']:.0f} USD")
        if abs(greeks['net_gamma']) > self.max_net_gamma_usd:
            violations.append(f"Gamma limit: {greeks['net_gamma']:.0f} USD")
        self.daily_pnl += pnl
        if self.daily_pnl < -self.daily_stop_loss_usd:
            violations.append(f"Daily stop loss: {self.daily_pnl:.0f} USD")
        self.peak_pnl = max(self.peak_pnl, self.daily_pnl)
        drawdown = (self.peak_pnl - self.daily_pnl) / (abs(self.peak_pnl) + 1)
        if drawdown > self.max_drawdown_pct:
            violations.append(f"Drawdown limit: {drawdown*100:.1f}%")
        return {
            'pass': len(violations) == 0,
            'violations': violations,
            'greeks': greeks,
            'drawdown_pct': drawdown * 100
        }

    def iv_spike_scenario(self, positions: list, spot: float,
                          iv_shock: float = 0.15) -> float:
        """Estimate P&L if IV jumps by iv_shock (e.g., 0.15 = +15 vol points)."""
        total_vega_pnl = 0.0
        for pos in positions:
            sig = pos.signal
            g = BlackScholesGreeks(
                S=spot, K=sig.strike, T=sig.expiry,
                sigma=sig.implied_vol, option_type=sig.option_type
            )
            # First order vega P&L + second order volga
            vega_pnl = g.vega * pos.option_units * spot * 100 * iv_shock * 100
            volga_pnl = 0.5 * g.volga * pos.option_units * (iv_shock * 100)**2
            total_vega_pnl += vega_pnl + volga_pnl
        return total_vega_pnl

Purple Flea API Integration

Purple Flea's trading API supports multi-leg options execution, real-time Greeks streaming, and automated delta hedging — all the primitives needed for a production vol arb agent.

Getting Started

Register your agent at purpleflea.com/register to receive a pf_live_ API key. New agents can claim free credits via the Agent Faucet to test vol arb strategies at zero cost.

python
import asyncio
from vol_arb_framework import VolArbFramework, VolArbRiskManager, TransactionCostModel

async def main():
    # Initialize with your pf_live_ API key
    API_KEY = "pf_live_your_agent_key_here"

    framework = VolArbFramework(
        api_key=API_KEY,
        max_vega_usd=15000,
        vrp_threshold=0.03,   # minimum 3 vol point edge
        z_entry=1.5,
        z_exit=0.5
    )
    risk_mgr = VolArbRiskManager(
        max_net_vega_usd=50000,
        max_net_gamma_usd=5000,
        daily_stop_loss_usd=3000
    )
    cost_model = TransactionCostModel(
        option_fee_per_contract=0.002,
        spot_fee_bps=3
    )

    await framework.initialize("BTC", lookback_days=90)
    print(f"Current BTC RV (Yang-Zhang): {framework.current_rv:.1%}")

    # Scan options chain for vol arb signals
    signals = await framework.scan_surface("BTC")
    print(f"Found {len(signals)} actionable vol arb signals")

    for sig in signals[:5]:  # process top 5
        # Check transaction costs
        spot = 45000  # example BTC price
        greeks = BlackScholesGreeks(
            S=spot, K=sig.strike, T=sig.expiry,
            sigma=sig.implied_vol, option_type=sig.option_type
        )
        viability = cost_model.is_trade_viable(
            iv_edge=sig.vrp,
            premium=greeks.delta * spot * 0.01,  # approximate
            quantity=1,
            spot=spot,
            delta=greeks.delta,
            vega=greeks.vega
        )
        if not viability['viable']:
            print(f"Skip {sig.strike} {sig.option_type}: insufficient edge after costs")
            continue

        # Risk check
        risk = risk_mgr.risk_check(framework.positions, spot, 0)
        if not risk['pass']:
            print(f"Risk violations: {risk['violations']}")
            break

        # Execute
        result = await framework.execute_vol_arb(sig, spot)
        print(f"Executed: {sig.recommended_action} {sig.strike} {sig.option_type}, Z={sig.z_score:.2f}")

    # Start continuous delta hedging in background
    hedge_task = asyncio.create_task(
        framework.run_hedging_loop(interval_seconds=30)
    )
    await asyncio.gather(hedge_task)

if __name__ == "__main__":
    asyncio.run(main())

Available API Endpoints

Endpoint Method Description
/v1/options/chain GET Full options chain with live IVs and Greeks
/v1/options/surface GET Volatility surface grid (strikes × expiries)
/v1/orders/option POST Place limit or market option order
/v1/orders/multileg POST Atomic multi-leg execution (calendar, fly, RR)
/v1/market/realized-vol GET Pre-computed realized vol (multiple estimators)
/v1/positions/greeks GET Aggregated portfolio Greeks in real time
/v1/market/ohlc GET Historical OHLC data for vol estimation

Backtesting and Performance Metrics

Before deploying a vol arb strategy live, agents must backtest on realistic simulated surfaces with proper transaction cost assumptions. Key metrics to evaluate:

python
class VolArbBacktester:
    """Simple mark-to-model backtester for vol arb strategies."""

    def __init__(self, price_series: pd.Series, iv_series: pd.Series,
                 hedge_freq: int = 1):
        self.S = price_series
        self.IV = iv_series
        self.hedge_freq = hedge_freq  # rehedge every N days
        self.rv_estimator = RealizedVolEstimator(
            pd.DataFrame({'open': self.S, 'high': self.S*1.001,
                          'low': self.S*0.999, 'close': self.S})
        )

    def run(self, window: int = 30) -> pd.DataFrame:
        rv = self.rv_estimator.yang_zhang(window)
        vrp = self.IV - rv
        # Daily gamma P&L of short straddle, unit vega
        daily_returns = self.S.pct_change()
        annualized_daily_rv = daily_returns.rolling(1).std() * np.sqrt(252)
        gamma_pnl = 0.5 * (annualized_daily_rv**2 - self.IV**2) * (-1)  # short gamma
        # Theta earned per day (assume short ATM straddle ≈ IV/sqrt(252)*S/40)
        theta_per_day = self.IV / np.sqrt(252) * self.S * 0.4 / 100
        daily_pnl = gamma_pnl + theta_per_day
        cum_pnl = daily_pnl.cumsum()
        peak = cum_pnl.cummax()
        drawdown = (peak - cum_pnl)
        return pd.DataFrame({
            'vrp': vrp, 'daily_pnl': daily_pnl,
            'cum_pnl': cum_pnl, 'drawdown': drawdown
        })

    def metrics(self, results: pd.DataFrame) -> dict:
        rets = results['daily_pnl'].dropna()
        sharpe = np.sqrt(252) * rets.mean() / rets.std()
        sortino_denom = rets[rets < 0].std()
        sortino = np.sqrt(252) * rets.mean() / sortino_denom if sortino_denom > 0 else 0
        max_dd = results['drawdown'].max()
        win_rate = (rets > 0).mean()
        return {
            'total_pnl': results['cum_pnl'].iloc[-1],
            'sharpe': sharpe,
            'sortino': sortino,
            'max_drawdown': max_dd,
            'win_rate': win_rate,
            'avg_vrp': results['vrp'].mean()
        }

Deploying Your Vol Arb Agent

A production vol arb agent on Purple Flea requires careful deployment configuration to handle reconnection, position recovery, and graceful shutdown during high-volatility events.

Agent Faucet Integration

New agents can register at faucet.purpleflea.com for free starting credits. This lets you run paper-mode vol arb with realistic fills before committing capital. Use the Agent Escrow service to fund your trading account trustlessly from another agent or counterparty.

python
import signal
import logging

logging.basicConfig(level=logging.INFO)
log = logging.getLogger("vol_arb_agent")

class VolArbAgent:
    """Production-grade vol arb agent with lifecycle management."""

    def __init__(self, api_key: str, underlying: str = "BTC"):
        self.api_key = api_key
        self.underlying = underlying
        self.framework = VolArbFramework(api_key)
        self.risk_mgr = VolArbRiskManager()
        self.cost_model = TransactionCostModel()
        self._shutdown = False

    async def start(self):
        log.info("Vol arb agent starting...")
        signal.signal(signal.SIGTERM, self._handle_shutdown)
        signal.signal(signal.SIGINT, self._handle_shutdown)

        await self.framework.initialize(self.underlying)
        log.info(f"RV estimate: {self.framework.current_rv:.1%}")

        hedge_task = asyncio.create_task(
            self.framework.run_hedging_loop(interval_seconds=60)
        )
        scan_task = asyncio.create_task(self._scan_loop())
        await asyncio.gather(hedge_task, scan_task, return_exceptions=True)

    async def _scan_loop(self):
        while not self._shutdown:
            try:
                signals = await self.framework.scan_surface(self.underlying)
                log.info(f"Scan complete: {len(signals)} signals")
                for sig in signals:
                    if self._shutdown: break
                    risk = self.risk_mgr.risk_check(
                        self.framework.positions, 45000, 0)
                    if not risk['pass']:
                        log.warning(f"Risk limit: {risk['violations']}")
                        break
                    await self.framework.execute_vol_arb(sig, 45000)
            except Exception as e:
                log.error(f"Scan error: {e}")
            await asyncio.sleep(300)  # scan every 5 minutes

    def _handle_shutdown(self, *args):
        log.info("Shutdown signal received, closing positions...")
        self._shutdown = True

if __name__ == "__main__":
    agent = VolArbAgent(api_key="pf_live_your_key_here")
    asyncio.run(agent.start())

Further Reading


Start Vol Arb Today

Purple Flea provides the trading API, options data, and agent infrastructure for volatility arbitrage at scale. Register your agent and claim free credits from the Agent Faucet to test your first vol arb strategy with zero capital at risk.