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.

~70%
of strategy alpha lost to poor execution
VPIN
volume-synchronized order flow imbalance
λ
Kyle's lambda — price impact per unit flow
3-5bp
typical spread in liquid crypto markets

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.

Spread = Pask − Pbid
Absolute spread. Relative spread = Spread / Midprice
Midprice = (Pask + Pbid) / 2
Best estimate of true fair value between bids and asks

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.

Agent Insight

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:

DI = (Vbid − Vask) / (Vbid + Vask)
Depth Imbalance: +1 = all bids, −1 = all asks, 0 = balanced

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.

OFI = Σ et where et = ΔVbid,t − ΔVask,t
Cont, Kukanov & Stoikov (2014) — top-of-book event contribution

Each event at time t contributes to OFI based on whether it increases bid depth (buy pressure) or ask depth (sell pressure). Specifically:

OFI RangeInterpretationAgent Action
> +2σStrong buy pressureBias quotes to ask side, reduce sell inventory
+1σ to +2σMild buy pressureMonitor, slight ask bias
−1σ to +1σBalanced flowStandard execution
−2σ to −1σMild sell pressureSlight bid bias
< −2σStrong sell pressureBias 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:

  1. Divide the trading session into equal-volume buckets (e.g., 1/50th of daily ADV each)
  2. Within each bucket, classify each trade as buy-initiated or sell-initiated (using tick rule or bulk volume classification)
  3. Compute VB (buy volume) and VS (sell volume) per bucket
  4. VPIN = rolling average of |VB − VS| / Vbucket over the last N buckets
VPIN = (1/n) Σ |ViB − ViS| / Vbucket
Typically n=50 buckets; V_bucket = ADV/50

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:

VB = V · Z((Pclose − Popen) / σΔP)
Z = standard normal CDF; σ_ΔP estimated from recent history

This lets agents compute VPIN from OHLCV bars without tick-level data — making it practical for most exchange APIs.

Trading Warning

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:

ΔP = λ · (VB − VS)
Kyle (1985). λ estimated via OLS regression of price changes on signed volume

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.

λ = Cov(ΔP, Q) / Var(Q) where Q = VB − VS
OLS estimator; use 50-200 observations for stability

Applications for agents:

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:

E[Cost] = α · X + (1/2) · γ · X2 + η · Σ xk2
α = permanent impact coeff., γ = temporary impact coeff., η = risk aversion

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 TypeDurationDriverAgent Response
TemporarySeconds to minutesLiquidity consumptionSpread execution over time
PermanentPersistentInformation revelationTrade stealth/fragmentation
RealizedTime-weightedBoth combinedMinimize 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:

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:

rb = s − d/2 − γσ²(T−t)q
Optimal bid reservation price. s = midprice, d = spread, q = inventory, γ = risk aversion
ra = s + d/2 − γσ²(T−t)q
Optimal ask reservation price. Inventory q shifts both quotes symmetrically.

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 Integration

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:

  1. Do not cancel on noise: Wait for meaningful OFI shift (>2σ) before pulling quotes
  2. Cancel before large news: High VPIN alerts justify pulling all limit orders
  3. Reprice, don't cancel: Some venues allow in-place modification while preserving queue position
  4. 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:

  1. If trade price > midprice: buyer-initiated
  2. If trade price < midprice: seller-initiated
  3. 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

SignalConstructionHorizonEdge Type
OFI momentumRolling OFI sum / volatility1-30sShort-term price direction
Trade imbalance(Buy trades − Sell trades) / Total trades1-5minSentiment proxy
Book slope ratioBid slope / Ask slopeImmediateShort-term push direction
Aggressive ratioMarket orders / All orders1-10minUrgency of participants
LOB resilienceTime to replenish after sweep5-60sMarket maker confidence
Midprice reversion speedHalf-life of midprice deviation1-30minMean 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:

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:

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
Getting Started

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:

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