Files
sentiment-engine/MALKHUT/malkhut/training/slippage_calibration.py
Codex bb229833d3 malkhut: Flight9 learnings — markout=quality, queue×flow, depth-for-size
Fable's Flight9/BLUE generalizable features incorporated:

1. Slippage model gains trade_flow_intensity parameter:
   - Estimated from book imbalance (proxy for trade arrivals)
   - More flow → better fills (lower slippage)
   - Fable: 'fill = queue position × trade-flow intensity'

2. Markout = quality concept documented:
   - Score fills by post-fill markout, not just fill/no-fill
   - Maker fills are adversely selected

3. Depth-for-size documented:
   - Spread lies; key on depth-within-K-bps vs order notional

4. Measured fees:
   - BingX maker=2.00bp, taker=5.016bp (over 1,455 fills)
   - BingX commission = NEGATIVE (debit)

5. OB study updated with Flight9 learnings
2026-07-17 16:18:43 +02:00

240 lines
9.6 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.

"""
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,
trade_flow_intensity: float = 0.0,
) -> float:
"""Predict slippage. Switches model based on book depth.
Generalizable features (Fable, Flight9/BLUE):
1. Fill = queue position × trade-flow intensity (not just book snapshot)
2. Depth-for-size > spread (spread lies — unfillable behind $977)
3. Markout = quality (post-fill adverse selection)
"""
# Flow intensity boost: more trade arrivals → higher fill probability
flow_boost = 1.0 + trade_flow_intensity * 0.1
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
# Adjust for flow intensity: more flow = better fills (lower slippage)
base /= max(flow_boost, 0.5)
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,
trade_flow_intensity: float = 0.0,
) -> 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, trade_flow_intensity)
# ==============================================================================
# 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,
trade_flow_intensity: float = 0.0,
) -> float:
"""Predict slippage using Flight7-calibrated model."""
return REGISTRY.expected_slippage_bps(symbol, levels_consumed, order_usd, book_depth_usd, is_mainnet, trade_flow_intensity)