870df57051
Fixes three bugs in dashboard: 1. vbt.html: bare abs(dd) → Math.abs(dd) — fixes ReferenceError 2. server.py: Add /cv/ routing for Next.js quant dashboard - Mount _next static assets at /cv/_next (not /cv/ which eats API routes) - Add /cv/api/* routes for backtests/historical, detail, recalc, risk - Add /cv/ws WebSocket endpoints for live/paper metrics - Add /cv/ catchall for Next.js HTML pages 3. dashboard-next/src/lib/api.ts: add res.ok guard to fetchHistorical() — prevents SyntaxError when API returns HTML error pages 4. sim/maker.py: guard observe() against zero mid_price
185 lines
6.0 KiB
Python
185 lines
6.0 KiB
Python
"""
|
|
Market maker quoting logic.
|
|
|
|
Generates bid/ask quotes based on microprice, inventory, volatility,
|
|
and spread constraints. Uses Avellaneda-Stoikov optimal control framework.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
from dataclasses import dataclass, field
|
|
from typing import Optional
|
|
|
|
|
|
@dataclass
|
|
class Quote:
|
|
"""A pair of maker quotes."""
|
|
|
|
bid: float
|
|
ask: float
|
|
bid_size: float
|
|
ask_size: float
|
|
reservation: float # optimal price given inventory
|
|
spread_bps: float
|
|
timestamp: float = 0.0
|
|
|
|
|
|
@dataclass
|
|
class MakerConfig:
|
|
"""Configuration for a market-making strategy."""
|
|
|
|
gamma: float = 0.1 # risk aversion
|
|
k: float = 1.5 # orderbook liquidity parameter
|
|
tau: float = 1.0 # time horizon (hours)
|
|
min_spread_bps: float = 1.0 # minimum spread in bps
|
|
max_spread_bps: float = 20.0
|
|
base_size: float = 0.001 # base quote size
|
|
max_inventory: float = 0.005
|
|
skew_factor: float = 0.5 # how aggressively to skew with inventory
|
|
volatility_window: int = 100
|
|
|
|
|
|
class AvellanedaStoikovMaker:
|
|
"""Market maker using Avellaneda-Stoikov stochastic control.
|
|
|
|
Generates bid/ask quotes that balance spread capture against
|
|
inventory risk via a reservation price.
|
|
|
|
Usage:
|
|
maker = AvellanedaStoikovMaker(MakerConfig())
|
|
maker.observe(100000.0) # feed mid prices
|
|
quote = maker.quote(100000.0, inventory=0.001, elapsed=0.5)
|
|
"""
|
|
|
|
def __init__(self, config: MakerConfig | None = None):
|
|
self._cfg = config or MakerConfig()
|
|
self._prices: list[float] = []
|
|
self._sigma: float = 0.02 # annualized volatility estimate
|
|
|
|
def observe(self, mid_price: float):
|
|
"""Feed a new mid price observation for volatility estimation."""
|
|
if mid_price <= 0:
|
|
return
|
|
self._prices.append(mid_price)
|
|
if len(self._prices) > self._cfg.volatility_window:
|
|
self._prices = self._prices[-self._cfg.volatility_window:]
|
|
|
|
if len(self._prices) >= 2:
|
|
returns = [
|
|
math.log(self._prices[i] / self._prices[i - 1])
|
|
for i in range(1, len(self._prices))
|
|
if self._prices[i - 1] > 0
|
|
]
|
|
if returns:
|
|
mean = sum(returns) / len(returns)
|
|
var = sum((r - mean) ** 2 for r in returns) / max(len(returns) - 1, 1)
|
|
self._sigma = max(math.sqrt(var * 365 * 24), 0.001) # annualize
|
|
|
|
def quote(
|
|
self,
|
|
mid_price: float,
|
|
inventory: float,
|
|
elapsed_hours: float,
|
|
) -> Quote:
|
|
"""Generate bid/ask quotes given current state.
|
|
|
|
Args:
|
|
mid_price: current mid price
|
|
inventory: current signed inventory (+ = long, - = short)
|
|
elapsed_hours: elapsed time in this session (for T-t decay)
|
|
"""
|
|
s = self._sigma
|
|
gamma = self._cfg.gamma
|
|
tau_remaining = self._cfg.tau - elapsed_hours
|
|
tau_remaining = max(tau_remaining, 0.01)
|
|
|
|
sigma_sq = s * s
|
|
r = mid_price - inventory * gamma * sigma_sq * tau_remaining
|
|
|
|
optimal_spread = gamma * sigma_sq * tau_remaining + (2.0 / gamma) * math.log(
|
|
1.0 + gamma / self._cfg.k
|
|
)
|
|
optimal_spread = max(optimal_spread, mid_price * self._cfg.min_spread_bps / 10000)
|
|
optimal_spread = min(optimal_spread, mid_price * self._cfg.max_spread_bps / 10000)
|
|
|
|
half = optimal_spread / 2.0
|
|
bid = r - half
|
|
ask = r + half
|
|
|
|
bid = max(bid, 1.0)
|
|
ask = max(ask, bid + mid_price * self._cfg.min_spread_bps / 10000)
|
|
|
|
spread_bps = (ask - bid) / mid_price * 10000 if mid_price > 0 else 0
|
|
|
|
return Quote(
|
|
bid=round(bid, 2),
|
|
ask=round(ask, 2),
|
|
bid_size=self._cfg.base_size,
|
|
ask_size=self._cfg.base_size,
|
|
reservation=round(r, 2),
|
|
spread_bps=round(spread_bps, 2),
|
|
)
|
|
|
|
def quote_with_skew(
|
|
self,
|
|
mid_price: float,
|
|
inventory: float,
|
|
elapsed_hours: float,
|
|
target_inventory: float = 0.0,
|
|
) -> Quote:
|
|
"""Quote with additional inventory skew toward target."""
|
|
base = self.quote(mid_price, inventory, elapsed_hours)
|
|
inv_deviation = (inventory - target_inventory) / max(self._cfg.max_inventory, 0.0001)
|
|
skew = inv_deviation * self._cfg.skew_factor * base.spread_bps / 10000 * mid_price
|
|
|
|
if inventory > target_inventory:
|
|
return Quote(
|
|
bid=round(base.bid - skew, 2),
|
|
ask=round(base.ask - skew, 2),
|
|
bid_size=base.bid_size * 0.5,
|
|
ask_size=base.ask_size * 1.5,
|
|
reservation=base.reservation,
|
|
spread_bps=base.spread_bps,
|
|
)
|
|
else:
|
|
return Quote(
|
|
bid=round(base.bid - skew, 2),
|
|
ask=round(base.ask - skew, 2),
|
|
bid_size=base.bid_size * 1.5,
|
|
ask_size=base.ask_size * 0.5,
|
|
reservation=base.reservation,
|
|
spread_bps=base.spread_bps,
|
|
)
|
|
|
|
@property
|
|
def sigma(self) -> float:
|
|
return self._sigma
|
|
|
|
@property
|
|
def config(self) -> MakerConfig:
|
|
return self._cfg
|
|
|
|
|
|
class GridMaker:
|
|
"""Simple grid market maker — places orders at evenly-spaced levels."""
|
|
|
|
def __init__(
|
|
self,
|
|
grid_levels: int = 5,
|
|
spacing_bps: float = 5.0,
|
|
size_per_level: float = 0.001,
|
|
):
|
|
self._levels = grid_levels
|
|
self._spacing = spacing_bps
|
|
self._size = size_per_level
|
|
|
|
def quotes(self, mid_price: float) -> list[dict]:
|
|
"""Generate grid quotes around mid."""
|
|
quotes = []
|
|
for i in range(1, self._levels + 1):
|
|
offset = mid_price * self._spacing * i / 10000
|
|
quotes.append({"side": "bid", "price": round(mid_price - offset, 2), "size": self._size})
|
|
quotes.append({"side": "ask", "price": round(mid_price + offset, 2), "size": self._size})
|
|
return quotes
|