Files
ftdt-quant-lab/strategies/as_quoter.py
T

118 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Proper Avellaneda-Stoikov market making for the live node.
Key formulas (Avellaneda & Stoikov, 2008):
Reservation price: r = s - q * gamma * sigma^2 * tau
Optimal spread: spread = gamma * sigma^2 * tau + (2/gamma) * ln(1 + gamma/k)
Bid = r - spread/2 Ask = r + spread/2
Where:
s = mid price, q = inventory, gamma = risk aversion
sigma = volatility, tau = remaining session time, k = order intensity
Production adaptations:
- Rolling volatility estimation (5-min window)
- Circuit breaker: pause quoting when price jump exceeds 3σ
- Inventory bounds: stop quoting on over-exposed side
- Virtual session clock: 1-hour windows since crypto is 24/7
"""
import math
from collections import deque
class ASQuoter:
"""Stateless per-tick quote generator using A-S optimal control."""
def __init__(
self,
gamma: float = 0.1, # Risk aversion — higher = more aggressive inventory redux
k: float = 1.5, # Order flow sensitivity — higher = tighter market
tau: float = 1.0, # Virtual session length (hours, for 24/7 crypto)
min_spread: float = 0.0001, # 1 bp minimum spread
max_inventory: float = 0.001, # Max position before stopping one side
vol_window: int = 300, # Number of price ticks for rolling vol (5 min @ 1s)
cb_mult: float = 3.0, # Circuit breaker multiplier (3σ jump threshold)
):
self.gamma = gamma
self.k = k
self.tau = tau
self.min_spread = min_spread
self.max_inventory = max_inventory
self.vol_window = vol_window
self.cb_mult = cb_mult
self._mid_prices: deque[float] = deque(maxlen=vol_window)
self._current_sigma: float = 0.02 # fallback: ~32% annualized for crypto
self._session_start: float = 0.0
def observe(self, mid: float) -> None:
"""Feed a new mid-price observation. Updates rolling volatility."""
self._mid_prices.append(mid)
if len(self._mid_prices) >= 2:
prices = list(self._mid_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.02
self._current_sigma = sigma
@property
def sigma(self) -> float:
return self._current_sigma
def circuit_breaker(self) -> bool:
"""Check if recent price jump exceeds threshold. If true, pause quoting."""
if len(self._mid_prices) < 5:
return False
recent = list(self._mid_prices)[-5:]
move_pct = abs(recent[-1] - recent[0]) / recent[0]
threshold = self.cb_mult * self._current_sigma * math.sqrt(5)
return move_pct > threshold
def quotes(self, mid: float, inventory: float, t: float) -> dict | None:
"""
Generate bid/ask quotes given current state.
Args:
mid: current mid-price
inventory: current net position (positive = long)
t: elapsed session time in hours (0 to tau)
Returns:
{"bid": ..., "ask": ..., "reservation": ..., "spread": ...} or None if paused
"""
self.observe(mid)
if self.circuit_breaker():
return None # Pause quoting — price jump in progress
# Reservation price: skew center by inventory risk
tau_remaining = max(self.tau - t, 0.01)
reservation = mid - inventory * self.gamma * (self._current_sigma ** 2) * tau_remaining
# Optimal spread: balance risk compensation vs flow capture
try:
log_term = math.log(1.0 + self.gamma / self.k)
except ValueError:
log_term = 0.0
spread = (
self.gamma * (self._current_sigma ** 2) * tau_remaining
+ (2.0 / max(self.gamma, 0.001)) * log_term
)
spread = max(spread, self.min_spread)
half = spread / 2.0
bid = reservation - half
ask = reservation + half
return {
"bid": max(bid, 1.0), # Never negative/zero
"ask": max(ask, 1.0),
"reservation": reservation,
"spread": spread,
}