Files
sentiment-engine/MALKHUT/malkhut/training/slippage_calibration.py

225 lines
8.9 KiB
Python
Raw Normal View History

"""
Slippage Calibration — Flight7-anchored, per-asset, per-run overridable.
Fable's Flight7 calibration (2026-07-16):
TESTNET (PRODGREEN, measured 3481 MARKET/taker fills on BingX-VST):
Majors: BTC ~0.1 bps, ETH ~0.4 bps
Liquid alts: TRX 2.9, LINK 4.3, ATOM 5.1, LTC 5.7, XLM 6.5 bps
Illiquid alts: DASH 14.3, FET 15.0, NEO 18.8, ETC 24.6 bps
Size impact: $2-10K ~2.7 bps; >$10K ~14-19 bps
Taker fee: 5.02 bps. Maker fee: 2.0 bps.
VST matches near mid → UNDERSTATES true book impact (optimistic floor).
MAINNET (prospective, live book-walk):
BTC/ETH: ~0-2 bps (≈ testnet)
Liquid alts: ~10-30 bps (BingX 3-10x thinner than Binance)
Thin alts: ~140 bps round-trip
Notional-weighted: ~45 bps @ $30K, ~34 bps @ $4K per side
KEY INSIGHT: For alts on thin books, the fill walks the ENTIRE book in 1-2 levels.
The model should use intercept-dominant (adverse selection) not alpha*levels.
Per-asset configurable: each asset has its own SlippageCalibration.
Per-run overridable: ScenarioFactory can override per-asset models.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Dict, List, Optional, Tuple
import math
@dataclass(frozen=True, slots=True)
class SlippageCalibration:
"""Per-asset calibrated slippage model.
Two-mode model:
1. DEEP BOOK (majors): slippage = alpha * levels + beta * depth_ratio
Fill walks levels → slippage proportional to levels consumed.
2. THIN BOOK (alts): slippage = intercept + adverse_selection_bps
Fill walks entire book in 1-2 levels → intercept-dominant.
Switch: if book_depth_usd < thin_book_threshold, use thin-book mode.
"""
# Deep-book parameters (majors with deep books)
alpha: float = 0.05 # bps per level consumed
beta: float = 0.3 # bps per unit of order/book depth ratio
# Thin-book parameters (alts with shallow books)
intercept: float = 0.0 # base slippage (bps) — dominant for thin books
adverse_selection_bps: float = 0.0 # additional adverse selection cost
# Model switching
thin_book_threshold_usd: float = 50000.0 # below this book depth → thin mode
# Metadata
n_samples: int = 0
r_squared: float = 0.0
testnet_to_mainnet: float = 1.0 # multiplier for mainnet
def expected_slippage_bps(
self,
levels_consumed: int,
order_usd: float = 0.0,
book_depth_usd: float = 1.0,
is_mainnet: bool = False,
) -> float:
"""Predict slippage. Switches model based on book depth."""
if book_depth_usd < self.thin_book_threshold_usd:
# THIN BOOK: intercept-dominant (alts, meme coins)
# The fill walks the entire book in 1-2 levels.
# Real cost = base intercept + adverse selection.
depth_ratio = order_usd / max(book_depth_usd, 1.0)
base = self.intercept + self.adverse_selection_bps * min(depth_ratio, 5.0)
else:
# DEEP BOOK: alpha*levels model (majors, large-cap alts)
depth_ratio = order_usd / max(book_depth_usd, 1.0)
base = self.alpha * levels_consumed + self.beta * depth_ratio
if is_mainnet:
base *= self.testnet_to_mainnet
return base
def expected_slippage_per_level(
self,
order_usd: float,
book_depth_usd: float,
) -> float:
"""Expected slippage per level consumed (for CWM)."""
if book_depth_usd < self.thin_book_threshold_usd:
# Thin book: per-level is dominated by intercept
return self.intercept / max(1, int(book_depth_usd / max(order_usd, 1.0)))
return self.alpha
class SlippageRegistry:
"""Per-asset slippage registry with per-run override support."""
def __init__(self) -> None:
self._models: Dict[str, SlippageCalibration] = _FLIGHT7_ANCHORS.copy()
self._overrides: Dict[str, SlippageCalibration] = {}
def get(self, symbol: str) -> SlippageCalibration:
"""Get slippage model, with per-run override taking priority."""
return self._overrides.get(symbol, self._models.get(symbol, SlippageCalibration(intercept=5.0, adverse_selection_bps=3.0)))
def override(self, symbol: str, model: SlippageCalibration) -> None:
"""Set per-run override for a symbol."""
self._overrides[symbol] = model
def override_all(self, models: Dict[str, SlippageCalibration]) -> None:
"""Set per-run overrides for all symbols."""
self._overrides.update(models)
def reset_overrides(self) -> None:
"""Clear all per-run overrides."""
self._overrides.clear()
def expected_slippage_bps(
self,
symbol: str,
levels_consumed: int,
order_usd: float = 0.0,
book_depth_usd: float = 1.0,
is_mainnet: bool = False,
) -> float:
"""Predict slippage using the appropriate model."""
model = self.get(symbol)
return model.expected_slippage_bps(levels_consumed, order_usd, book_depth_usd, is_mainnet)
# ==============================================================================
# Flight7 Calibration Anchors
# ==============================================================================
#
# Testnet (PRODGREEN, measured 3481 MARKET/taker fills on BingX-VST):
# Majors: BTC ~0.1 bps, ETH ~0.4 bps
# Liquid alts: TRX 2.9, LINK 4.3, ATOM 5.1, LTC 5.7, XLM 6.5 bps
# Illiquid alts: DASH 14.3, FET 15.0, NEO 18.8, ETC 24.6 bps
# Size impact: $2-10K ~2.7 bps; >$10K ~14-19 bps
# Taker fee: 5.02 bps. Maker fee: 2.0 bps.
#
# Mainnet (prospective, live book-walk):
# BTC/ETH: ~0-2 bps (≈ testnet)
# Liquid alts: ~10-30 bps (BingX 3-10x thinner than Binance)
# Thin alts: ~140 bps round-trip
# Notional-weighted: ~45 bps @ $30K, ~34 bps @ $4K per side
#
# KEY: For thin-book assets, fill walks entire book in 1-2 levels.
# Model uses intercept-dominant (adverse selection), not alpha*levels.
_FLIGHT7_ANCHORS: Dict[str, SlippageCalibration] = {
# Majors (deep book, alpha*levels model works)
"BTCUSDT": SlippageCalibration(
alpha=0.02, beta=0.15, intercept=0.05, adverse_selection_bps=0.02,
thin_book_threshold_usd=100_000, n_samples=3481, testnet_to_mainnet=1.2,
),
"ETHUSDT": SlippageCalibration(
alpha=0.04, beta=0.20, intercept=0.10, adverse_selection_bps=0.05,
thin_book_threshold_usd=80_000, n_samples=3481, testnet_to_mainnet=1.5,
),
"BNBUSDT": SlippageCalibration(
alpha=0.05, beta=0.25, intercept=0.15, adverse_selection_bps=0.08,
thin_book_threshold_usd=60_000, testnet_to_mainnet=1.5,
),
# Liquid alts (medium book, hybrid model)
"SOLUSDT": SlippageCalibration(
alpha=0.08, beta=0.30, intercept=2.0, adverse_selection_bps=1.0,
thin_book_threshold_usd=30_000, testnet_to_mainnet=3.0,
),
"LINKUSDT": SlippageCalibration(
alpha=0.10, beta=0.35, intercept=3.0, adverse_selection_bps=1.5,
thin_book_threshold_usd=25_000, testnet_to_mainnet=3.5,
),
"DOTUSDT": SlippageCalibration(
alpha=0.09, beta=0.32, intercept=2.5, adverse_selection_bps=1.2,
thin_book_threshold_usd=28_000, testnet_to_mainnet=3.0,
),
"AVAXUSDT": SlippageCalibration(
alpha=0.08, beta=0.28, intercept=1.8, adverse_selection_bps=0.8,
thin_book_threshold_usd=30_000, testnet_to_mainnet=2.5,
),
# Meme/mid (retail-dominated, higher adverse selection)
"DOGEUSDT": SlippageCalibration(
alpha=0.12, beta=0.45, intercept=4.0, adverse_selection_bps=2.5,
thin_book_threshold_usd=20_000, testnet_to_mainnet=4.0,
),
"ADAUSDT": SlippageCalibration(
alpha=0.10, beta=0.40, intercept=3.5, adverse_selection_bps=2.0,
thin_book_threshold_usd=22_000, testnet_to_mainnet=5.0,
),
"MATICUSDT": SlippageCalibration(
alpha=0.11, beta=0.42, intercept=3.0, adverse_selection_bps=1.8,
thin_book_threshold_usd=25_000, testnet_to_mainnet=4.0,
),
# Thin alts (intercept-dominant, highest adverse selection)
"AAVEUSDT": SlippageCalibration(
alpha=0.15, beta=0.55, intercept=6.0, adverse_selection_bps=4.0,
thin_book_threshold_usd=15_000, testnet_to_mainnet=5.0,
),
"UNIUSDT": SlippageCalibration(
alpha=0.18, beta=0.60, intercept=7.0, adverse_selection_bps=5.0,
thin_book_threshold_usd=12_000, testnet_to_mainnet=5.0,
),
"ATOMUSDT": SlippageCalibration(
alpha=0.12, beta=0.50, intercept=5.0, adverse_selection_bps=3.0,
thin_book_threshold_usd=18_000, testnet_to_mainnet=4.0,
),
}
# Global registry (per-asset, per-run overridable)
REGISTRY = SlippageRegistry()
def expected_slippage_bps(
symbol: str,
levels_consumed: int,
order_usd: float = 0.0,
book_depth_usd: float = 1.0,
is_mainnet: bool = False,
) -> float:
"""Predict slippage using Flight7-calibrated model."""
return REGISTRY.expected_slippage_bps(symbol, levels_consumed, order_usd, book_depth_usd, is_mainnet)