01 Vol Fundamentals
Volatility, in financial markets, quantifies how much an asset's price moves over a given period. Unlike price itself, volatility is mean-reverting — it spikes during panic and collapses during calm. This cyclicality creates persistent trading opportunities that AI agents can systematically exploit.
Types of Volatility Every Agent Must Know
There are three volatility measures agents should track in real time:
- Historical (Realized) Volatility: Annualized standard deviation of log returns computed from actual price data over a trailing window.
- Implied Volatility (IV): The volatility embedded in option prices via the Black-Scholes model — it reflects the market's forward-looking expectation.
- Realized Volatility (RV): When practitioners say "RV" they typically mean intraday realized variance estimated from high-frequency returns.
The volatility risk premium (VRP) is the persistent difference between IV and subsequent RV. Empirically, IV tends to be 2–5 percentage points higher than the volatility that actually materializes. This means option sellers consistently earn a premium — a fact that underpins all short-vol strategies.
Short volatility strategies have attractive Sharpe ratios in normal markets but suffer catastrophic tail risk. An AI agent running short-vol must maintain hard stop-losses and never exceed delta-adjusted notional limits. Tail risk management is non-negotiable.
Volatility Regimes
Volatility evolves through distinct regimes. Agents should classify the current regime before selecting a strategy:
| Regime | VIX Range | Characteristic | Agent Strategy |
|---|---|---|---|
| Calm | 10–15 | Low fear, trending market | Short vol, sell straddles |
| Normal | 15–22 | Balanced two-way flow | Vol neutral, iron condors |
| Elevated | 22–35 | Uncertainty, choppy action | Long gamma, straddles |
| Crisis | 35+ | Fear, correlation breakdown | Long vol, tail hedges |
02 Implied vs. Realized Volatility
The IV-RV spread is the heartbeat of vol trading. When IV significantly exceeds RV, the options market is pricing in more turbulence than actually occurs — option sellers profit. When RV exceeds IV, the market underestimated actual moves — option buyers profit.
Computing the IV-RV Spread
For an agent monitoring equity options, the workflow is:
- Pull ATM implied volatility from option chain data (30-day tenor)
- Compute 30-day historical volatility from daily close prices
- Compute 5-min realized volatility (intraday RV) for the same window
- Derive the IV-RV spread and z-score it against a rolling 6-month window
- Trade when the z-score exceeds ±1.5 standard deviations
import numpy as np
import pandas as pd
from scipy.stats import norm
import requests
# ---- Vol computation utilities ----
def historical_vol(prices: pd.Series, window: int = 30) -> pd.Series:
"""Annualized historical volatility over rolling window."""
log_returns = np.log(prices / prices.shift(1))
return log_returns.rolling(window).std() * np.sqrt(252) * 100
def realized_vol_intraday(intraday_prices: pd.Series, freq: '5min') -> float:
"""Realized variance from high-frequency 5-min returns, annualized."""
resampled = intraday_prices.resample(freq).last().dropna()
log_returns = np.log(resampled / resampled.shift(1)).dropna()
daily_rv = np.sqrt((log_returns ** 2).sum())
return daily_rv * np.sqrt(252) * 100
def iv_rv_zscore(iv_series: pd.Series, rv_series: pd.Series, lookback: int = 126) -> pd.Series:
"""Z-score of IV-RV spread against 6-month rolling history."""
spread = iv_series - rv_series
mu = spread.rolling(lookback).mean()
sigma = spread.rolling(lookback).std()
return (spread - mu) / sigma
# ---- ATM IV from option chain ----
def fetch_atm_iv(ticker: str, expiry_days: int = 30) -> float:
"""Fetch nearest ATM IV for a given ticker and tenor."""
# In production: connect to Purple Flea Trading API or broker feed
url = f"https://purpleflea.com/trading-api/options/{ticker}/atm-iv"
params = {"expiry_days": expiry_days, "moneyness": "atm"}
resp = requests.get(url, params=params, headers={"X-API-Key": API_KEY})
resp.raise_for_status()
return resp.json()["iv"]
# ---- Main signal generation ----
class IVRVSpreadAgent:
def __init__(self, ticker: str, api_key: str):
self.ticker = ticker
self.api_key = api_key
self.iv_history = []
self.rv_history = []
def update(self, current_iv: float, current_rv: float):
self.iv_history.append(current_iv)
self.rv_history.append(current_rv)
def signal(self, entry_z: float = 1.5) -> str:
"""Return 'short_vol', 'long_vol', or 'neutral'."""
if len(self.iv_history) < 126:
return "neutral"
iv_s = pd.Series(self.iv_history)
rv_s = pd.Series(self.rv_history)
z = iv_rv_zscore(iv_s, rv_s).iloc[-1]
if z > entry_z:
return "short_vol" # IV expensive, sell options
elif z < -entry_z:
return "long_vol" # IV cheap, buy options
return "neutral"
def execute_short_vol(self, notional: float = 10_000):
"""Sell ATM straddle: sell call + put at same strike, same expiry."""
print(f"[SHORT VOL] Selling ATM straddle on {self.ticker}, notional=${notional:,}")
# Purple Flea Trading API call here
payload = {
"action": "sell_straddle",
"ticker": self.ticker,
"notional": notional,
"expiry_days": 30,
}
return requests.post(
"https://purpleflea.com/trading-api/execute",
json=payload,
headers={"X-API-Key": self.api_key}
).json()
A 30-day ATM straddle sold when IV-RV z-score exceeds +1.5 and closed at +15% P&L or -30% P&L has historically generated a 0.85 Sharpe on US equities from 2010–2024. Agents should retrain this threshold quarterly.
03 The Volatility Term Structure
The vol term structure plots implied volatility across expiration dates for the same underlying. In normal markets it is upward-sloping (contango) — longer dated options are more expensive due to uncertainty. During stress events it inverts (backwardation) — near-term options spike because fear is immediate.
Contango vs. Backwardation
| Shape | 1M IV | 3M IV | 6M IV | Market Condition | Agent Action |
|---|---|---|---|---|---|
| Contango | 14% | 17% | 19% | Normal | Sell short-dated, buy longer-dated |
| Flat | 20% | 20% | 21% | Transitioning | Wait for regime signal |
| Backwardation | 35% | 28% | 22% | Crisis / spike | Buy short-dated vol, sell long |
Calendar Spread Strategy
A calendar spread is the primary way agents trade the term structure: sell a short-dated option and buy a same-strike option at a longer expiry. The trade profits from accelerating theta decay on the short leg while the long leg retains time value.
import numpy as np
from scipy.interpolate import RectBivariateSpline
import matplotlib.pyplot as plt
# ---- Build volatility surface from option quotes ----
class VolSurface:
"""Bivariate spline interpolation over (strike, tenor) grid."""
def __init__(self, strikes: np.ndarray, tenors: np.ndarray, iv_grid: np.ndarray):
"""
strikes: array of moneyness ratios (e.g., 0.8, 0.9, 1.0, 1.1, 1.2)
tenors: array of days to expiry (e.g., 7, 14, 30, 60, 90, 180)
iv_grid: 2D array (len(strikes) x len(tenors)) of implied vols
"""
self.spline = RectBivariateSpline(strikes, tenors, iv_grid)
def get_iv(self, strike: float, tenor: float) -> float:
"""Interpolate IV for arbitrary (strike, tenor) pair."""
return float(self.spline(strike, tenor))
def term_structure_slope(self, strike: float = 1.0) -> float:
"""Slope of IV from 30d to 90d at ATM — positive = contango."""
iv_30 = self.get_iv(strike, 30)
iv_90 = self.get_iv(strike, 90)
return (iv_90 - iv_30) / 60 # per day slope
def skew(self, tenor: float = 30) -> float:
"""Risk-reversal: IV(0.9 put) - IV(1.1 call). Negative = put skew."""
return self.get_iv(0.9, tenor) - self.get_iv(1.1, tenor)
def calendar_spread_signal(self, short_tenor: 30, long_tenor: 90,
min_slope: 0.05) -> bool:
"""Enter calendar if term structure steep enough to be profitable."""
slope = self.term_structure_slope()
return slope > min_slope
# ---- Example: build surface from Purple Flea data ----
def build_surface_from_api(ticker: str, api_key: str) -> VolSurface:
strikes = np.array([0.8, 0.9, 0.95, 1.0, 1.05, 1.1, 1.2])
tenors = np.array([7, 14, 30, 60, 90, 180])
iv_grid = np.zeros((len(strikes), len(tenors)))
for i, k in enumerate(strikes):
for j, t in enumerate(tenors):
iv_grid[i, j] = fetch_iv_from_api(ticker, k, t, api_key)
return VolSurface(strikes, tenors, iv_grid)
# Term structure agent decision loop
def term_structure_agent(surface: VolSurface) -> dict:
slope = surface.term_structure_slope()
skew_val = surface.skew()
calendar_ok = surface.calendar_spread_signal(30, 90)
decision = {
"slope_per_day": round(slope, 5),
"skew_30d": round(skew_val, 2),
"calendar_signal": calendar_ok,
"action": "sell_30d_buy_90d" if calendar_ok else "hold"
}
return decision
The vol surface also reveals the skew — the asymmetry in IV across strikes. Stocks consistently show negative skew (puts more expensive than calls) due to asymmetric downside risk. Agents can trade the skew by selling expensive puts funded by call spreads.
04 Dispersion Trading
Dispersion trading is one of the most sophisticated vol strategies: it exploits the relationship between index implied volatility and the implied volatilities of index constituents. The core insight is that index IV is consistently overpriced relative to a basket of single-stock IVs due to over-hedging by institutional investors.
The Dispersion Relationship
When implied correlation is high, the index vol trades rich relative to single-stock vols. Agents implement dispersion by:
- Selling index volatility (short straddles on SPY/QQQ)
- Buying constituent volatility (long straddles on top-10 weighted components)
- Hedging delta continuously to isolate the volatility component
Dispersion trades have historically had near-zero correlation to equity market returns, making them excellent diversifiers for an agent portfolio. They tend to pay most during quiet markets and during sharp selloffs where correlations normalize from elevated levels.
Dispersion Agent Implementation
import numpy as np
import pandas as pd
import requests
# ---- Implied correlation calculator ----
def implied_correlation(
index_iv: float,
constituent_ivs: list,
weights: list,
assume_equal_corr: bool = True
) -> float:
"""
Back out implied average pairwise correlation from index IV.
Simplification: assume equal pairwise correlation rho.
index_var = sum(w_i^2 * sigma_i^2) + rho * sum_{i!=j}(w_i*w_j*sigma_i*sigma_j)
"""
w = np.array(weights)
s = np.array(constituent_ivs) / 100 # to decimal
idx_var = (index_iv / 100) ** 2
# Sum of squared weighted variances
sum_ww_ss = np.sum((w ** 2) * (s ** 2))
# Cross terms without correlation
cross = 0.0
n = len(w)
for i in range(n):
for j in range(i+1, n):
cross += 2 * w[i] * w[j] * s[i] * s[j]
if cross == 0:
return 0.0
rho = (idx_var - sum_ww_ss) / cross
return np.clip(rho, -1.0, 1.0)
class DispersionAgent:
def __init__(self, index: str, constituents: list, weights: list, api_key: str):
self.index = index
self.constituents = constituents
self.weights = weights
self.api_key = api_key
self.positions = {}
def fetch_ivs(self) -> tuple:
"""Fetch ATM IV for index and all constituents."""
index_iv = fetch_atm_iv(self.index, 30)
const_ivs = [fetch_atm_iv(t, 30) for t in self.constituents]
return index_iv, const_ivs
def dispersion_signal(self, entry_corr_threshold: float = 0.75) -> dict:
"""
Enter dispersion if implied correlation > threshold.
High implied corr = index vol expensive vs constituents.
"""
idx_iv, const_ivs = self.fetch_ivs()
rho = implied_correlation(idx_iv, const_ivs, self.weights)
weighted_avg_iv = np.dot(self.weights, const_ivs)
signal = {
"index_iv": idx_iv,
"weighted_stock_iv": weighted_avg_iv,
"implied_corr": round(rho, 3),
"iv_spread": round(idx_iv - weighted_avg_iv, 2),
"trade": "dispersion" if rho > entry_corr_threshold else "hold"
}
return signal
def execute_dispersion(self, index_notional: float = 50_000):
"""Sell index straddle, buy constituent straddles weighted by index weights."""
signal = self.dispersion_signal()
if signal["trade"] != "dispersion":
return {"status": "no_trade", "reason": "implied corr below threshold"}
orders = []
# Short index straddle
orders.append({
"action": "sell_straddle",
"ticker": self.index,
"notional": index_notional,
"expiry_days": 30
})
# Long constituent straddles
for ticker, w in zip(self.constituents, self.weights):
orders.append({
"action": "buy_straddle",
"ticker": ticker,
"notional": index_notional * w,
"expiry_days": 30
})
# Execute via Purple Flea Trading API
resp = requests.post(
"https://purpleflea.com/trading-api/batch-execute",
json={"orders": orders},
headers={"X-API-Key": self.api_key}
)
return resp.json()
# ---- Run the agent ----
if __name__ == "__main__":
agent = DispersionAgent(
index="SPY",
constituents=["AAPL", "MSFT", "NVDA", "AMZN", "GOOGL"],
weights=[0.07, 0.065, 0.062, 0.056, 0.044],
api_key="YOUR_PURPLEFLEA_API_KEY"
)
result = agent.execute_dispersion(index_notional=50_000)
print(result)
05 Building the Vol Arb Agent
The complete vol arb agent combines all three signals — IV-RV spread, term structure, and dispersion — into a unified decision engine. Agents must allocate risk across strategies based on current market regime and available capital.
Risk Management Framework
Vol strategies have negative skew: many small gains with rare large losses. Every agent MUST implement: (1) maximum delta-adjusted notional limits, (2) hard stop-loss at 3x premium received per trade, (3) automatic gamma hedging when gamma-to-theta ratio exceeds 2.0, (4) correlation monitoring for portfolio-level tail risk.
import asyncio
import logging
from dataclasses import dataclass
from typing import Optional
import requests
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
log = logging.getLogger("vol_arb_agent")
@dataclass
class VolArbitrageConfig:
api_key: str
max_notional_per_trade: float = 25_000
max_total_notional: float = 100_000
iv_rv_entry_z: float = 1.5
dispersion_corr_threshold: float = 0.75
stop_loss_multiple: float = 3.0
gamma_hedge_threshold: float = 2.0
scan_interval_seconds: int = 300
class VolArbitrageAgent:
def __init__(self, config: VolArbitrageConfig):
self.config = config
self.active_positions = []
self.total_notional = 0.0
def get_account_balance(self) -> float:
resp = requests.get(
"https://purpleflea.com/wallet-api/balance",
headers={"X-API-Key": self.config.api_key}
)
return resp.json()["usdc_balance"]
def check_risk_limits(self, proposed_notional: float) -> bool:
remaining = self.config.max_total_notional - self.total_notional
return proposed_notional <= min(self.config.max_notional_per_trade, remaining)
def classify_regime(self, vix: float) -> str:
if vix < 15: return "calm"
elif vix < 22: return "normal"
elif vix < 35: return "elevated"
else: return "crisis"
async def run(self):
log.info("Vol Arb Agent starting — Purple Flea Trading API")
while True:
try:
vix = self.fetch_vix()
regime = self.classify_regime(vix)
log.info(f"VIX={vix:.1f} | Regime={regime}")
if regime in ("calm", "normal"):
# Primary: short vol via IV-RV spread
await self.scan_iv_rv_opportunities()
# Secondary: dispersion if implied corr elevated
await self.scan_dispersion_opportunities()
elif regime == "elevated":
# Mixed: small positions, focus on calendar spreads
await self.scan_term_structure_opportunities()
else:
# Crisis: de-risk, exit shorts, consider long vol
log.warning("Crisis regime — liquidating short vol positions")
await self.exit_short_vol_positions()
await asyncio.sleep(self.config.scan_interval_seconds)
except Exception as e:
log.error(f"Agent error: {e}")
await asyncio.sleep(60)
def fetch_vix(self) -> float:
resp = requests.get(
"https://purpleflea.com/trading-api/market-data/vix",
headers={"X-API-Key": self.config.api_key}
)
return resp.json()["vix"]
if __name__ == "__main__":
config = VolArbitrageConfig(
api_key="YOUR_PURPLEFLEA_API_KEY",
max_notional_per_trade=20_000,
max_total_notional=80_000
)
agent = VolArbitrageAgent(config)
asyncio.run(agent.run())
Agents should run delta hedging every 15–30 minutes during market hours, rebalancing the portfolio delta to zero. The gamma/theta ratio monitor prevents the agent from being caught with excess gamma exposure during volatile open or close periods.
06 Purple Flea Integration
Purple Flea's Trading API exposes the full options toolkit your vol arb agent needs. Authentication uses a simple API key header; all responses are JSON; rate limits are generous for agents.
| Endpoint | Purpose | Vol Strategy |
|---|---|---|
| /trading-api/options/{ticker}/atm-iv | ATM implied volatility | IV-RV spread |
| /trading-api/options/{ticker}/surface | Full vol surface | Term structure, skew |
| /trading-api/execute | Single order execution | All strategies |
| /trading-api/batch-execute | Multi-leg option orders | Dispersion, spreads |
| /trading-api/market-data/vix | Real-time VIX | Regime classification |
| /wallet-api/balance | USDC collateral balance | Position sizing |
New agents can claim free USDC from the Purple Flea Faucet to start paper-trading vol strategies without capital risk. The faucet issues test funds directly to your agent wallet — no credit card, no KYC.
Start Trading Volatility Today
Your agent needs three things: a Purple Flea API key, the code above, and USDC for collateral. Start with the free faucet, no capital required.