2429394cd8
1. A-S reservation price now uses gamma*500000 scaling. Before: bash.003 skew on 4K BTC (invisible, same as naive dual-quote) After: ~0 skew at max inventory (0.05% of mid — enough to suppress one side) 2. Mean Reversion: 20-tick → 60-tick window, threshold 1.0σ → 0.5σ. 20 seconds of 1s ticks is noise, not mean-reverting. 60 seconds captures real short-term reversion dynamics. Fill attribution verified: BTC sizes differ by 50 μBTC, ETH by 0.0025 — all above matching tolerance. Orderbook null guards present — no crash on failed fetch.
108 lines
4.2 KiB
Python
108 lines
4.2 KiB
Python
"""
|
|
Production Avellaneda-Stoikov market making for crypto.
|
|
|
|
Key insight (missed by most naive implementations):
|
|
The AS formula does NOT tell you what price to quote.
|
|
The market spread is determined by competition (best bid/ask).
|
|
AS tells you WHEN to quote each side based on your inventory risk.
|
|
|
|
When you're long → reservation price drops below mid → stop quoting bid
|
|
When you're short → reservation price rises above mid → stop quoting ask
|
|
When flat → quote both sides symmetrically at market best bid/ask
|
|
|
|
Current adaptation for $100/strategy scale:
|
|
- gamma_eff = gamma * 500,000 (~$30 skew at max inventory)
|
|
- sigma floor = 0.001 (0.1% minimal vol)
|
|
- Sigma squared floor = 0.000001
|
|
- Skew: r = mid - q_notional * gamma_eff * sigma^2 * tau
|
|
- At max position (0.000950 BTC, $60): skew ≈ $30 = 0.05% of mid
|
|
- Enough to visibly suppress one quoting side
|
|
"""
|
|
|
|
import math
|
|
from collections import deque
|
|
|
|
|
|
class ASMarketMaker:
|
|
"""Avellaneda-Stoikov: pick quoting sides based on inventory-adjusted fair value."""
|
|
|
|
def __init__(
|
|
self,
|
|
gamma: float = 0.1, # Risk aversion (scaled internally by 500K)
|
|
tau: float = 1.0, # Session length (hours)
|
|
max_inventory: float = 0.003, # Max position (3x trade size for BTC)
|
|
vol_window: int = 300,
|
|
cb_mult: float = 3.0,
|
|
):
|
|
self.gamma = gamma
|
|
self.tau = tau
|
|
self.max_inventory = max_inventory
|
|
self.cb_mult = cb_mult
|
|
self._gamma_scale = 500000 # Aggressive for $100 allocation visibility
|
|
|
|
self._prices: deque[float] = deque(maxlen=vol_window)
|
|
self._sigma: float = 0.01 # fallback: 1% return vol
|
|
|
|
# ── Vol estimation ──
|
|
|
|
def observe(self, mid: float) -> None:
|
|
self._prices.append(mid)
|
|
if len(self._prices) >= 10:
|
|
prices = list(self._prices)
|
|
returns = [(prices[i] - prices[i-1]) / prices[i-1] for i in range(1, len(prices))]
|
|
mu = sum(returns) / len(returns)
|
|
var = sum((r - mu) ** 2 for r in returns) / len(returns)
|
|
sigma = math.sqrt(var) if var > 0 else 0.01
|
|
self._sigma = max(sigma, 0.001)
|
|
|
|
@property
|
|
def sigma(self) -> float:
|
|
return self._sigma
|
|
|
|
def circuit_breaker(self) -> bool:
|
|
if len(self._prices) < 5:
|
|
return False
|
|
recent = list(self._prices)[-5:]
|
|
move_pct = abs(recent[-1] - recent[0]) / recent[0]
|
|
return move_pct > self.cb_mult * self._sigma * math.sqrt(5)
|
|
|
|
# ── Side selection ──
|
|
|
|
def should_quote(self, mid: float, best_bid: float, best_ask: float, inventory: float, t: float) -> dict:
|
|
"""
|
|
Determine which sides to quote.
|
|
|
|
Primary: hard inventory bounds stop quoting over-exposed side.
|
|
Secondary: reservation price skew (with 500K gamma scaling for visibility at our size).
|
|
"""
|
|
self.observe(mid)
|
|
|
|
# Hard inventory bounds — stop quoting the over-exposed side
|
|
if abs(inventory) >= self.max_inventory:
|
|
if inventory > 0:
|
|
return {"quote_bid": False, "quote_ask": True, "reservation": mid, "sigma": self._sigma}
|
|
else:
|
|
return {"quote_bid": True, "quote_ask": False, "reservation": mid, "sigma": self._sigma}
|
|
|
|
# Circuit breaker
|
|
if self.circuit_breaker():
|
|
return {"quote_bid": False, "quote_ask": False, "reservation": mid, "sigma": self._sigma}
|
|
|
|
# Reservation price with aggressive gamma scaling
|
|
q_notional = inventory * mid
|
|
gamma_eff = self.gamma * self._gamma_scale
|
|
tau_rem = max(self.tau - t, 0.01)
|
|
sigma_sq = max(self._sigma ** 2, 0.000001) # floor: 0.1% vol squared
|
|
reservation = mid - q_notional * gamma_eff * sigma_sq * tau_rem
|
|
|
|
# At $60 notional: skew ≈ $30 → 0.05% of mid — small but directional
|
|
quote_bid = reservation >= best_bid or abs(inventory) < self.max_inventory * 0.1
|
|
quote_ask = reservation <= best_ask or abs(inventory) < self.max_inventory * 0.1
|
|
|
|
return {
|
|
"quote_bid": quote_bid,
|
|
"quote_ask": quote_ask,
|
|
"reservation": reservation,
|
|
"sigma": self._sigma,
|
|
}
|