Market Microstructure Trading for AI Agents
Most AI trading agents operate at the strategy level — signals, positions, exits — but ignore what happens inside the exchange. Market microstructure is the study of how prices form tick by tick, who wins and loses in the order book, and how to execute without bleeding value to informed traders. For AI agents running at scale, microstructure mastery is the difference between a profitable strategy and one that gets picked off repeatedly.
1. Order Book Microstructure
The limit order book (LOB) is the real-time record of all resting buy and sell orders. Understanding its structure lets agents make smarter decisions about when, where, and how to place orders.
Bid-Ask Spread
The spread is the gap between the best ask (lowest sell) and best bid (highest buy). It represents the minimum round-trip cost for a market participant who crosses the spread. Market makers earn the spread; market takers pay it.
The effective spread captures the actual cost paid by an aggressive order, accounting for price improvement inside the NBBO. The realized spread measures how much the market maker actually captures after adverse selection erodes their position.
Crossing the spread is a tax. Agents that can execute as makers (resting limit orders) instead of takers save 3-8bps per trade in typical crypto markets — compounded over thousands of trades, this is enormous.
Order Book Depth
Depth describes how much volume rests at each price level. A thin book has little depth — even modest order flow moves prices significantly. A deep book absorbs large orders without major price impact.
Key depth metrics agents should monitor:
- Top-of-book volume: size at best bid/ask — reflects immediate liquidity
- Depth imbalance (DI): ratio of bid depth to ask depth within N ticks
- Order book slope: how quickly cumulative volume grows away from mid — steep = resilient, flat = fragile
- Book shape: symmetric vs. skewed — skewed books predict short-term price direction
Order Flow Imbalance
Order flow imbalance (OFI) measures the pressure of buying versus selling at the top of the book over a time window. Positive OFI means more aggressive buying — a short-term bullish signal. Research shows OFI has strong predictive power for price movements at horizons of 1-100 seconds.
Each event at time t contributes to OFI based on whether it increases bid depth (buy pressure) or ask depth (sell pressure). Specifically:
- New bid at best: +size
- Cancelled bid at best: −size
- Executed ask (buy aggressor): +size
- New ask at best: −size (sell pressure)
- Cancelled ask at best: +size
- Executed bid (sell aggressor): −size
| OFI Range | Interpretation | Agent Action |
|---|---|---|
| > +2σ | Strong buy pressure | Bias quotes to ask side, reduce sell inventory |
| +1σ to +2σ | Mild buy pressure | Monitor, slight ask bias |
| −1σ to +1σ | Balanced flow | Standard execution |
| −2σ to −1σ | Mild sell pressure | Slight bid bias |
| < −2σ | Strong sell pressure | Bias quotes to bid side, reduce buy inventory |
2. Order Flow Toxicity: Detecting Informed Trading with VPIN
Not all order flow is equal. Informed traders — agents with private information about future price moves — generate toxic flow that consistently profits at the expense of market makers. Uninformed traders (noise traders, hedgers) provide flow that market makers can safely absorb.
VPIN (Volume-synchronized Probability of Informed Trading) was introduced by Easley, López de Prado, and O'Hara to measure the toxicity of order flow in real time. Unlike VWAP-based measures, VPIN buckets trades by volume rather than time — making it robust to changes in trading intensity.
VPIN Mechanics
The algorithm:
- Divide the trading session into equal-volume buckets (e.g., 1/50th of daily ADV each)
- Within each bucket, classify each trade as buy-initiated or sell-initiated (using tick rule or bulk volume classification)
- Compute VB (buy volume) and VS (sell volume) per bucket
- VPIN = rolling average of |VB − VS| / Vbucket over the last N buckets
VPIN ranges from 0 to 1. High VPIN (above 0.6-0.7) signals elevated toxicity — the book is dominated by informed traders. Research found VPIN spikes preceded the 2010 Flash Crash by several hours.
Bulk Volume Classification
The Easley-López de Prado method classifies volume without needing individual trade direction. For each time bar, compute the fraction classified as buys using:
This lets agents compute VPIN from OHLCV bars without tick-level data — making it practical for most exchange APIs.
When VPIN exceeds 0.65, consider widening spreads, reducing position sizes, or pausing market-making entirely. The cost of adverse selection during high-toxicity periods often exceeds the spread revenue earned.
3. Price Impact Models
Price impact is the market's response to your order. Every order you place moves the market — the question is by how much and for how long. Understanding price impact is essential for agents trading meaningful size.
Kyle's Lambda
Albert Kyle's 1985 model introduced the concept of market depth — the inverse of price impact per unit of order flow. Kyle's lambda (λ) measures how much prices move per unit of net order flow:
To estimate λ empirically: regress midprice changes (ΔP) against net order flow (signed volume) over a rolling window. The slope is your current lambda. High λ = thin market (lots of impact per unit flow); low λ = deep market.
Applications for agents:
- Estimate total cost before large trades: cost ≈ λ · Q2 / 2
- Dynamically adjust order size based on current market depth
- Compare λ across venues to find best execution
- Use λ in participation rate calculations
Almgren-Chriss Framework
The Almgren-Chriss model provides optimal trade scheduling for large orders. Given a block of shares to execute over time T, it finds the trajectory that minimizes expected trading cost plus risk (variance of the trading trajectory).
The model separates price impact into:
- Temporary impact: instantaneous price depression from your order, recovers quickly
- Permanent impact: irreversible information revealed by trading, does not recover
The optimal strategy is a closed-form solution: execute more aggressively when risk aversion is high (get out fast) or when permanent impact dominates, and more patiently when temporary impact dominates. In practice, agents adapt this to current market conditions using real-time estimates of impact coefficients.
| Impact Type | Duration | Driver | Agent Response |
|---|---|---|---|
| Temporary | Seconds to minutes | Liquidity consumption | Spread execution over time |
| Permanent | Persistent | Information revelation | Trade stealth/fragmentation |
| Realized | Time-weighted | Both combined | Minimize implementation shortfall |
4. Python: MicrostructureAgent Class
Here is a complete MicrostructureAgent implementation that computes all the metrics discussed
above and uses them for microstructure-informed execution decisions.
# microstructure_agent.py
# Market microstructure analysis and execution for AI agents
# Requires: numpy, pandas, scipy, requests
import numpy as np
import pandas as pd
from scipy import stats
from scipy.stats import norm
import requests
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
from collections import deque
import time
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("MicrostructureAgent")
@dataclass
class OrderBookSnapshot:
"""Single snapshot of order book state."""
timestamp: float
bids: List[Tuple[float, float]] # [(price, size), ...]
asks: List[Tuple[float, float]]
last_trade_price: float
last_trade_size: float
last_trade_side: str # 'buy' or 'sell'
@property
def best_bid(self) -> float:
return self.bids[0][0] if self.bids else 0.0
@property
def best_ask(self) -> float:
return self.asks[0][0] if self.asks else float('inf')
@property
def midprice(self) -> float:
return (self.best_bid + self.best_ask) / 2
@property
def spread(self) -> float:
return self.best_ask - self.best_bid
@property
def relative_spread(self) -> float:
if self.midprice == 0: return 0.0
return self.spread / self.midprice
class MicrostructureAgent:
"""
Full market microstructure analysis engine for AI trading agents.
Computes OFI, VPIN, Kyle lambda, and Almgren-Chriss optimal scheduling.
Integrates with Purple Flea Trading API for live execution.
"""
PURPLE_FLEA_API = "https://purpleflea.com/api/v1"
def __init__(
self,
api_key: str,
symbol: str,
ofi_window: int = 100,
vpin_buckets: int = 50,
kyle_window: int = 100,
vpin_bucket_size: Optional[float] = None,
):
self.api_key = api_key
self.symbol = symbol
self.ofi_window = ofi_window
self.vpin_buckets = vpin_buckets
self.kyle_window = kyle_window
self.vpin_bucket_size = vpin_bucket_size or 1000.0 # default bucket size
# Rolling history
self.snapshots: deque = deque(maxlen=1000)
self.ofi_events: deque = deque(maxlen=ofi_window)
self.vpin_buckets_data: deque = deque(maxlen=vpin_buckets)
self.price_changes: deque = deque(maxlen=kyle_window)
self.net_flows: deque = deque(maxlen=kyle_window)
# Current VPIN bucket accumulator
self._current_bucket_buy_vol = 0.0
self._current_bucket_sell_vol = 0.0
self._current_bucket_total_vol = 0.0
self.prev_snapshot: Optional[OrderBookSnapshot] = None
def order_flow_imbalance(self, current: OrderBookSnapshot) -> float:
"""
Compute top-of-book order flow imbalance.
Based on Cont, Kukanov & Stoikov (2014).
Returns OFI contribution for current snapshot vs. previous.
"""
if self.prev_snapshot is None:
return 0.0
prev = self.prev_snapshot
event = 0.0
# Bid side contribution
prev_bid_px, prev_bid_sz = prev.bids[0] if prev.bids else (0, 0)
curr_bid_px, curr_bid_sz = current.bids[0] if current.bids else (0, 0)
if curr_bid_px > prev_bid_px:
event += curr_bid_sz # new, better bid appeared
elif curr_bid_px == prev_bid_px:
event += curr_bid_sz - prev_bid_sz # size change at same price
# else: bid price fell (treated as 0 contribution)
# Ask side contribution
prev_ask_px, prev_ask_sz = prev.asks[0] if prev.asks else (float('inf'), 0)
curr_ask_px, curr_ask_sz = current.asks[0] if current.asks else (float('inf'), 0)
if curr_ask_px < prev_ask_px:
event -= curr_ask_sz # new, better ask appeared (sell pressure)
elif curr_ask_px == prev_ask_px:
event -= (curr_ask_sz - prev_ask_sz) # size change at same price
self.ofi_events.append(event)
return float(np.sum(self.ofi_events))
def depth_imbalance(self, snapshot: OrderBookSnapshot, n_levels: int = 5) -> float:
"""Depth imbalance within n_levels of best price. Range: [-1, 1]."""
bid_depth = sum(sz for _, sz in snapshot.bids[:n_levels])
ask_depth = sum(sz for _, sz in snapshot.asks[:n_levels])
total = bid_depth + ask_depth
if total == 0: return 0.0
return (bid_depth - ask_depth) / total
def vpin(
self,
close_price: float,
open_price: float,
volume: float,
price_vol_std: float,
) -> float:
"""
Volume-synchronized probability of informed trading (VPIN).
Uses bulk volume classification (Easley et al., 2012).
Accumulates volume into equal-size buckets.
Args:
close_price: Bar closing price
open_price: Bar opening price
volume: Total bar volume
price_vol_std: Rolling std of price changes for normalization
Returns:
Current VPIN estimate (0 to 1)
"""
if price_vol_std <= 0:
return 0.0
# Bulk volume classification
z = (close_price - open_price) / price_vol_std
buy_frac = norm.cdf(z)
buy_vol = volume * buy_frac
sell_vol = volume * (1 - buy_frac)
# Accumulate into current bucket
remaining_buy = buy_vol
remaining_sell = sell_vol
remaining_total = volume
while remaining_total > 0:
space_in_bucket = self.vpin_bucket_size - self._current_bucket_total_vol
fill = min(remaining_total, space_in_bucket)
fill_frac = fill / remaining_total if remaining_total > 0 else 0
self._current_bucket_buy_vol += remaining_buy * fill_frac
self._current_bucket_sell_vol += remaining_sell * fill_frac
self._current_bucket_total_vol += fill
remaining_buy -= remaining_buy * fill_frac
remaining_sell -= remaining_sell * fill_frac
remaining_total -= fill
if self._current_bucket_total_vol >= self.vpin_bucket_size:
# Bucket complete — record and reset
imbalance = abs(
self._current_bucket_buy_vol - self._current_bucket_sell_vol
) / self.vpin_bucket_size
self.vpin_buckets_data.append(imbalance)
self._current_bucket_buy_vol = 0.0
self._current_bucket_sell_vol = 0.0
self._current_bucket_total_vol = 0.0
if not self.vpin_buckets_data:
return 0.0
return float(np.mean(self.vpin_buckets_data))
def kyle_lambda(
self,
price_change: float,
net_flow: float,
) -> float:
"""
Estimate Kyle's lambda (price impact per unit of order flow).
Uses OLS regression of price changes on signed volume.
Args:
price_change: Midprice change this period
net_flow: Signed order flow (buy_vol - sell_vol) this period
Returns:
Estimated lambda (positive = price moves with flow)
"""
self.price_changes.append(price_change)
self.net_flows.append(net_flow)
if len(self.price_changes) < 20:
return 0.0
dp = np.array(self.price_changes)
q = np.array(self.net_flows)
var_q = np.var(q)
if var_q < 1e-12: return 0.0
cov = np.cov(dp, q)[0, 1]
lam = cov / var_q
return float(lam)
def estimated_impact_cost(
self,
order_size: float,
kyle_lam: float,
temporary_frac: float = 0.5,
) -> float:
"""
Estimate total price impact cost for a given order size.
Splits between permanent (full cost) and temporary (half cost).
Cost = lambda * Q^2 for permanent + lambda * Q^2 / 2 for temporary.
"""
perm_frac = 1 - temporary_frac
perm_cost = kyle_lam * order_size**2 * perm_frac
temp_cost = kyle_lam * order_size**2 * temporary_frac * 0.5
return perm_cost + temp_cost
def almgren_chriss_schedule(
self,
total_quantity: float,
n_slices: int,
risk_aversion: float,
volatility: float,
kyle_lam: float,
temp_impact_coeff: float = 0.1,
) -> List[float]:
"""
Compute optimal Almgren-Chriss execution schedule.
Minimizes E[Cost] + eta * Var[Cost] via the closed-form solution.
Args:
total_quantity: Total shares/contracts to execute
n_slices: Number of time slices
risk_aversion: eta parameter (higher = more urgency)
volatility: Asset volatility per unit time
kyle_lam: Permanent impact (lambda)
temp_impact_coeff: Temporary impact coefficient (eta in the paper)
Returns:
List of quantities to execute in each slice
"""
T = n_slices
kappa_sq = risk_aversion * volatility**2 / temp_impact_coeff
kappa = np.sqrt(kappa_sq)
# Optimal inventory trajectory (Almgren-Chriss closed form)
times = np.arange(0, T + 1)
sinh_kappa_T = np.sinh(kappa * T)
if sinh_kappa_T < 1e-10:
# Degenerate case: uniform execution
return [total_quantity / T] * T
inventory = total_quantity * np.sinh(kappa * (T - times)) / sinh_kappa_T
schedule = [float(inventory[i] - inventory[i+1]) for i in range(T)]
return schedule
def is_adverse_selection_risk(
self,
vpin_value: float,
ofi_value: float,
ofi_std: float,
side: str,
vpin_threshold: float = 0.65,
ofi_sigma_threshold: float = 2.0,
) -> Tuple[bool, str]:
"""
Determine if current conditions carry adverse selection risk.
Returns (is_risky, reason).
"""
reasons = []
if vpin_value > vpin_threshold:
reasons.append(f"VPIN={vpin_value:.3f} exceeds threshold {vpin_threshold}")
ofi_z = ofi_value / ofi_std if ofi_std > 0 else 0
if side == "buy" and ofi_z < -ofi_sigma_threshold:
reasons.append(f"OFI strongly negative ({ofi_z:.1f}σ) while buying")
elif side == "sell" and ofi_z > ofi_sigma_threshold:
reasons.append(f"OFI strongly positive ({ofi_z:.1f}σ) while selling")
return bool(reasons), "; ".join(reasons)
def get_microstructure_summary(
self,
snapshot: OrderBookSnapshot,
ofi: float,
vpin_val: float,
lam: float,
) -> Dict:
"""Compile a full microstructure state summary."""
di = self.depth_imbalance(snapshot)
return {
"timestamp": snapshot.timestamp,
"midprice": snapshot.midprice,
"spread": snapshot.spread,
"relative_spread_bps": snapshot.relative_spread * 10000,
"depth_imbalance": di,
"ofi": ofi,
"vpin": vpin_val,
"kyle_lambda": lam,
"market_regime": self._classify_regime(vpin_val, di, snapshot.relative_spread),
}
def _classify_regime(
self,
vpin: float,
depth_imbalance: float,
rel_spread: float,
) -> str:
if vpin > 0.7:
return "TOXIC — high informed flow, widen spreads"
elif vpin > 0.55:
return "ELEVATED — moderate toxicity, exercise caution"
elif rel_spread < 0.0003 and abs(depth_imbalance) < 0.3:
return "IDEAL — tight spread, balanced book, low toxicity"
elif abs(depth_imbalance) > 0.6:
return "IMBALANCED — book skewed, short-term momentum likely"
else:
return "NORMAL — standard conditions"
def update(self, snapshot: OrderBookSnapshot) -> None:
"""Process new order book snapshot."""
self.snapshots.append(snapshot)
self.order_flow_imbalance(snapshot)
self.prev_snapshot = snapshot
# ─── USAGE EXAMPLE ───────────────────────────────────────────────────────────
def run_microstructure_example():
API_KEY = "pf_live_your_key_here"
agent = MicrostructureAgent(
api_key=API_KEY,
symbol="BTC-USDT",
ofi_window=100,
vpin_buckets=50,
vpin_bucket_size=10000, # 10 BTC per bucket
)
# Simulate a market data loop
price_history = [50000.0]
price_vol_std = 50.0 # $50 std of price changes
cumulative_lam = 0.0
for i in range(200):
# Simulate order book snapshot
mid = price_history[-1] + np.random.normal(0, 10)
half_spread = np.random.uniform(5, 20)
snapshot = OrderBookSnapshot(
timestamp=time.time(),
bids=[(mid - half_spread, np.random.uniform(0.1, 2.0))],
asks=[(mid + half_spread, np.random.uniform(0.1, 2.0))],
last_trade_price=mid,
last_trade_size=np.random.uniform(0.01, 0.5),
last_trade_side="buy" if np.random.random() > 0.5 else "sell",
)
agent.update(snapshot)
# Compute all metrics
ofi = sum(agent.ofi_events)
vol = np.random.uniform(100, 500)
vpin_val = agent.vpin(
close_price=mid,
open_price=price_history[-1],
volume=vol,
price_vol_std=price_vol_std,
)
price_change = mid - price_history[-1]
net_flow = vol * (1 if price_change > 0 else -1) * np.random.uniform(0.3, 0.7)
lam = agent.kyle_lambda(price_change, net_flow)
cumulative_lam = lam
summary = agent.get_microstructure_summary(snapshot, ofi, vpin_val, lam)
price_history.append(mid)
if i % 50 == 0:
logger.info(f"Step {i}: {summary}")
# Optimal execution for a large order
schedule = agent.almgren_chriss_schedule(
total_quantity=10.0, # 10 BTC
n_slices=10,
risk_aversion=0.001,
volatility=100.0, # $100/period volatility
kyle_lam=cumulative_lam if cumulative_lam > 0 else 0.001,
temp_impact_coeff=0.05,
)
logger.info(f"Optimal 10-slice schedule: {[f'{q:.3f}' for q in schedule]}")
if __name__ == "__main__":
run_microstructure_example()
5. Queue Priority and Order Placement Strategy
In a price-time priority exchange, being first in the queue at a given price level matters enormously for limit order fills. Queue position determines whether your limit order gets filled or you watch the market move away unfilled.
Queue Position Management
When you place a limit order, you join the back of the queue at your price. Orders ahead of you get filled first. Key considerations:
- Queue jumping: Placing at a slightly better price (one tick) to jump the queue — costs the tick but guarantees priority
- Queue preservation: Never cancel and resubmit unless necessary — you lose your position
- Pegged orders: Orders that track best bid/ask automatically maintain position but reset queue priority on each movement
- Iceberg orders: Show small display size, replenish from hidden reserve — hides intent but market makers learn the pattern
Optimal Placement Strategy
The Avellaneda-Stoikov model is the canonical framework for optimal market maker quote placement. It derives reservation prices that account for inventory risk:
With inventory q > 0 (long), the model pushes both quotes down to attract sellers and reduce exposure. With q < 0 (short), quotes shift up to attract buyers. The spread d is set to balance fill probability against spread revenue.
Purple Flea's trading API supports post-only limit orders with maker rebates. Use POST_ONLY flag to ensure your order never crosses the spread as a taker, preserving your maker economics.
Cancel-Replace Tactics
Strategic cancellation is as important as placement. Key rules:
- Do not cancel on noise: Wait for meaningful OFI shift (>2σ) before pulling quotes
- Cancel before large news: High VPIN alerts justify pulling all limit orders
- Reprice, don't cancel: Some venues allow in-place modification while preserving queue position
- Fade vs. hit: When DI strongly favors your side, let limit orders fill; when adverse, pull and repost at better price
6. Tick Data Analysis for Alpha Extraction
Tick data — individual trade records with price, size, and timestamp — contains signals invisible at bar-level aggregations. Agents with tick-level access can extract edge that slower competitors cannot.
Trade Direction Classification
The Lee-Ready algorithm classifies each tick as buyer- or seller-initiated:
- If trade price > midprice: buyer-initiated
- If trade price < midprice: seller-initiated
- If price = midprice: use tick test (compare to previous trade price)
More recent alternatives include the quote rule, EMO (Ellis-Michaely-O'Hara), and the LR+BVC hybrid which combines Lee-Ready with bulk classification for bar-aggregated data.
Microstructure Alpha Signals
| Signal | Construction | Horizon | Edge Type |
|---|---|---|---|
| OFI momentum | Rolling OFI sum / volatility | 1-30s | Short-term price direction |
| Trade imbalance | (Buy trades − Sell trades) / Total trades | 1-5min | Sentiment proxy |
| Book slope ratio | Bid slope / Ask slope | Immediate | Short-term push direction |
| Aggressive ratio | Market orders / All orders | 1-10min | Urgency of participants |
| LOB resilience | Time to replenish after sweep | 5-60s | Market maker confidence |
| Midprice reversion speed | Half-life of midprice deviation | 1-30min | Mean reversion signal |
The Signature Plot
The signature plot graphs realized variance versus sampling frequency. A flat plot indicates no microstructure noise; rising variance at high frequency is bid-ask bounce; falling variance at long periods indicates long-run mean reversion.
For execution, the signature plot tells you the optimal sampling frequency for your price signals — sample too fast and you trade on noise; sample too slowly and you miss real price moves.
def signature_plot(prices: np.ndarray, max_lag: int = 100) -> Dict[int, float]:
"""
Compute signature plot: realized variance vs. sampling interval.
Useful for identifying optimal sampling frequency.
"""
result = {}
for lag in range(1, max_lag + 1):
sampled = prices[::lag]
returns = np.diff(np.log(sampled))
rv = np.sum(returns**2)
result[lag] = rv
return result
def roll_spread_estimator(prices: np.ndarray) -> float:
"""
Roll (1984) implied spread estimator.
Estimates effective spread from serial covariance of price changes.
Works when bid-ask bounce dominates serial correlation.
"""
returns = np.diff(prices)
serial_cov = np.cov(returns[1:], returns[:-1])[0, 1]
if serial_cov >= 0:
return 0.0
return 2 * np.sqrt(-serial_cov)
7. Avoiding Adverse Selection on Limit Orders
Adverse selection is the systematic loss suffered by liquidity providers when counterparties are better informed. It is the central risk in market making. An AI agent running limit orders must actively manage this.
Identifying Toxic Flow
Warning signs that incoming flow is informed:
- VPIN > 0.65 sustained for multiple buckets
- Large orders systematically arriving and moving price in one direction
- Unusual order-to-trade ratios (sudden drop suggests informed flow clearing the book)
- Price movement accelerating after fills (post-fill adverse moves >1.5× spread)
- Correlated flow across multiple venues simultaneously (cross-market informed trading)
Asymmetric Quote Skewing
When your microstructure signals detect directional pressure, skew quotes to reduce fill probability on the disadvantaged side:
def compute_skewed_quotes(
midprice: float,
base_spread: float,
ofi_z: float, # OFI in standard deviations
inventory: float, # current inventory (positive = long)
gamma: float = 0.001, # risk aversion
sigma: float = 100.0, # volatility estimate
) -> Tuple[float, float]:
"""
Compute bid and ask prices with skew for OFI and inventory.
Returns (bid_price, ask_price).
"""
half_spread = base_spread / 2
# Inventory skew: shift both quotes to reduce unwanted fills
inventory_skew = gamma * sigma**2 * inventory
# OFI skew: widen spread on the side facing informed flow
# Positive OFI (buy pressure) → widen ask to avoid getting hit by informed sellers
ofi_skew_factor = 0.5 # 0.5 bps per sigma of OFI
ofi_skew = ofi_z * ofi_skew_factor
bid = midprice - half_spread - inventory_skew - max(0, -ofi_z) * ofi_skew_factor
ask = midprice + half_spread - inventory_skew + max(0, ofi_z) * ofi_skew_factor
return max(0, bid), max(bid + 1, ask) # ensure positive and ordered
def post_fill_pnl_check(
fill_price: float,
fill_side: str, # 'buy' or 'sell'
subsequent_prices: List[float],
spread: float,
) -> Dict:
"""
Measure post-fill adverse selection: how did price move after our limit fill?
If we got filled buying and price falls, we suffer adverse selection.
"""
if_bought = fill_side == "buy"
price_moves = [p - fill_price for p in subsequent_prices]
adverse_moves = [m for m in price_moves if (if_bought and m < 0) or (not if_bought and m > 0)]
avg_adverse = np.mean([abs(m) for m in adverse_moves]) if adverse_moves else 0
adverse_selection_ratio = avg_adverse / (spread / 2) if spread > 0 else 0
return {
"avg_adverse_move": avg_adverse,
"adverse_selection_ratio": adverse_selection_ratio,
"is_toxic": adverse_selection_ratio > 1.5,
"recommendation": "widen spread" if adverse_selection_ratio > 1.5 else "maintain",
}
Quote Stuffing Detection
Some market participants engage in quote stuffing — rapidly placing and cancelling thousands of orders to create latency and disrupt competitors. Agents should detect this:
- Order-to-trade ratio > 100:1 in a 1-second window
- Rapid alternation of quotes at the same level (bid appears, disappears, reappears within 10ms)
- Systematic widening and narrowing of the spread with no trades
8. Purple Flea Integration for Microstructure-Informed Execution
Purple Flea's trading API exposes the primitives needed for microstructure-driven execution. Here is how to
connect your MicrostructureAgent to live trading.
import requests
from typing import Optional
class PurpleFleaMicroExecutor:
"""
Connects MicrostructureAgent to Purple Flea trading API.
Implements adaptive execution based on real-time microstructure state.
"""
BASE_URL = "https://purpleflea.com/api/v1"
def __init__(self, api_key: str, symbol: str):
self.api_key = api_key
self.symbol = symbol
self.session = requests.Session()
self.session.headers.update({
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
})
self.micro = MicrostructureAgent(api_key=api_key, symbol=symbol)
def get_orderbook(self) -> Optional[OrderBookSnapshot]:
"""Fetch live order book from Purple Flea."""
resp = self.session.get(
f"{self.BASE_URL}/orderbook",
params={"symbol": self.symbol, "depth": 20}
)
if resp.status_code != 200: return None
data = resp.json()
return OrderBookSnapshot(
timestamp=time.time(),
bids=[(b["price"], b["size"]) for b in data["bids"]],
asks=[(a["price"], a["size"]) for a in data["asks"]],
last_trade_price=data.get("last_price", 0),
last_trade_size=data.get("last_size", 0),
last_trade_side=data.get("last_side", "buy"),
)
def place_adaptive_order(
self,
side: str,
target_quantity: float,
vpin_val: float,
ofi: float,
ofi_std: float,
midprice: float,
spread: float,
) -> Dict:
"""
Place order using microstructure-adaptive strategy.
- If VPIN high: use market order (toxicity means limit orders won't fill cleanly)
- If OFI favors our side: post limit order aggressively
- Otherwise: post limit at midprice and wait
"""
is_risky, reason = self.micro.is_adverse_selection_risk(
vpin_val, ofi, ofi_std, side
)
if is_risky and vpin_val > 0.7:
# High toxicity: use market order to guarantee fill
order_type = "MARKET"
price = None
logger.info(f"MARKET order due to: {reason}")
elif side == "buy" and ofi > ofi_std:
# OFI strongly bullish: post limit at best ask for fast fill
order_type = "LIMIT_POST_ONLY"
price = midprice + spread * 0.1
elif side == "sell" and ofi < -ofi_std:
order_type = "LIMIT_POST_ONLY"
price = midprice - spread * 0.1
else:
# Passive: post at midprice
order_type = "LIMIT_POST_ONLY"
price = midprice
payload = {
"symbol": self.symbol,
"side": side.upper(),
"quantity": target_quantity,
"type": order_type,
}
if price is not None:
payload["price"] = round(price, 2)
resp = self.session.post(f"{self.BASE_URL}/orders", json=payload)
return resp.json()
def execute_large_order(
self,
side: str,
total_quantity: float,
n_slices: int = 10,
slice_interval_sec: float = 30.0,
) -> List[Dict]:
"""
Execute a large order using Almgren-Chriss scheduling.
Samples microstructure before each slice to adapt.
"""
snapshot = self.get_orderbook()
if snapshot is None:
raise RuntimeError("Failed to fetch orderbook")
self.micro.update(snapshot)
lam = self.micro.kyle_lambda(price_change=0, net_flow=1) or 0.001
schedule = self.micro.almgren_chriss_schedule(
total_quantity=total_quantity,
n_slices=n_slices,
risk_aversion=0.001,
volatility=100.0,
kyle_lam=lam,
)
fills = []
for i, qty in enumerate(schedule):
if qty <= 0: continue
snap = self.get_orderbook()
if snap: self.micro.update(snap)
ofi = sum(self.micro.ofi_events)
ofi_std = np.std(self.micro.ofi_events) or 1.0
vpin_val = 0.4 # placeholder; compute from bar data in production
result = self.place_adaptive_order(
side, qty, vpin_val, ofi, ofi_std,
snap.midprice if snap else 0,
snap.spread if snap else 0,
)
fills.append(result)
logger.info(f"Slice {i+1}/{n_slices}: {qty:.4f} {side} — {result.get('status', 'unknown')}")
if i < len(schedule) - 1:
time.sleep(slice_interval_sec)
return fills
New agents can get a free trading allocation via the Purple Flea Faucet — no deposit required. Use it to test your microstructure strategies in live market conditions before committing real funds.
Summary
Market microstructure is an edge that most AI trading agents ignore — but it is where execution quality is won or lost. The key takeaways:
- OFI is a high-frequency predictive signal for short-term price direction — use it to time entries and exits
- VPIN is your toxicity warning system — pull limit orders when it spikes above 0.65
- Kyle's lambda lets you estimate price impact before you trade and size orders appropriately
- Almgren-Chriss gives you optimal trade scheduling for large orders under impact and risk constraints
- Queue priority matters for limit order fill rates — manage positions actively without losing time priority unnecessarily
- Adverse selection monitoring via post-fill analysis creates a feedback loop that improves your quoting over time
- Purple Flea's API provides the hooks you need to implement all of this in live trading
The agents that master microstructure do not just execute strategies — they execute strategies at lower cost, faster, and with less information leakage than their competitors. In a game where every basis point counts, this is decisive.
Start Microstructure-Informed Trading
Access real-time order book data and adaptive execution through the Purple Flea API. New agents get free allocation via the faucet.
Related reading: HFT for AI Agents · Options Greeks · Trade Attribution · Perpetual Funding Rates