diff --git a/MALKHUT/README.md b/MALKHUT/README.md index a414508..65ca756 100644 --- a/MALKHUT/README.md +++ b/MALKHUT/README.md @@ -543,6 +543,51 @@ The CMA-ES optimizer now receives fill quality metrics in each `EpisodeResult`: The CMA-ES objective is: maximize fill quality (primary) while maintaining positive PnL. +#### Urgency-Driven Maker/Taker Decision + +The system learns WHEN to use maker (passive) vs taker (aggressive) as a function of urgency: + +| Urgency | Behavior | Penalty | +|---------|----------|---------| +| 0.0 – 0.3 | Passive only (maker). No taker actions generated. | — | +| 0.3 – 0.65 | IOC partial taker allowed. Small sizes, partial fills. | Low | +| 0.65 – 1.0 | Full taker allowed. Aggressive crossing at full size. | None | + +Two CMA-ES optimizable parameters: +- `urgency_taker_threshold` (default 0.65): crosses from passive to aggressive +- `urgency_taker_penalty_bps` (default 2.0): penalty for taker at low urgency + +The system learns: "For ADAUSDT in stress regime, threshold=0.4 (thin book → try maker, +but resort to taker when liquidity appears). For BTCUSDT in normal regime, threshold=0.8 +(deep book → maker always works)." + +#### Calibrated Slippage (Flight7 VST + Mainnet Anchors) + +Slippage model per-asset, configurable, overridable per-run: + +| Book Type | Model | Assets | +|-----------|-------|--------| +| **Deep book** | `slippage = alpha * levels + beta * depth_ratio` | BTC, ETH, BNB | +| **Thin book** | `slippage = intercept + adverse_selection` | DOGE, ADA, UNI, AAVE, ATOM | + +Switch: `if book_depth_usd < thin_book_threshold_usd → thin mode` + +Fable's Flight7 calibration anchors (testnet + mainnet): +- Majors: BTC ~0.1 bps, ETH ~0.4 bps (testnet ≈ mainnet) +- Liquid alts: 3-6 bps testnet, 10-30 bps mainnet (3-6× gap) +- Thin alts: 14-25 bps testnet, ~140 bps round-trip mainnet + +#### Chase Mechanics + +Cancel → wait → retry with configurable parameters (CMA-ES optimizable): +- `wait_to_retry_ms`: delay before re-quoting after cancel (0-2000ms) +- `chase_enabled`: enable chase-follow behavior +- `chase_offset_ticks`: ticks from target price to chase (0-10) +- `chase_max_retries`: max cancel-retry cycles (0-5) + +CHASE in the DSL places a limit order at target offset with TTL=wait_to_retry_ms. +If not filled, the order expires and next step re-places at a new offset. + ### Performance Benchmarks | Metric | Value | diff --git a/MALKHUT/docs/HFTBACKTEST_CWM_INTEGRATION.md b/MALKHUT/docs/HFTBACKTEST_CWM_INTEGRATION.md index 8f3f421..e707387 100644 --- a/MALKHUT/docs/HFTBACKTEST_CWM_INTEGRATION.md +++ b/MALKHUT/docs/HFTBACKTEST_CWM_INTEGRATION.md @@ -394,3 +394,44 @@ reward = w_fill_probability * fill_value_score ← PRIMARY PerformanceMatrix stores `avg_fill_rate`, `avg_slippage_bps`, `avg_price_improvement_bps`, `avg_fill_value_score` per (regime, strategy, venue) — enabling: "Which strategy achieves the best fill quality in regime X on venue Y?" + +## Urgency-Driven Maker/Taker Decision + +Two CMA-ES optimizable parameters control the maker/taker boundary: +- `urgency_taker_threshold` (default 0.65): urgency level to switch from passive to aggressive +- `urgency_taker_penalty_bps` (default 2.0): penalty for taker fills at low urgency + +Action menu generates three urgency bands: +1. urgency < threshold×0.5: passive only (no CROSS_SPREAD actions) +2. threshold×0.5 < urgency < threshold: IOC partial taker (small sizes) +3. urgency > threshold: full taker (aggressive crossing) + +Reward function adds urgency penalty: +```python +if is_cross and urgency < threshold: + penalty = urgency_taker_penalty_bps * (1 - urgency / threshold) + fill_quality_reward -= penalty +``` + +The CMA-ES learns the optimal threshold per (asset, regime, venue). + +## Calibrated Slippage + +SlippageCalibration uses Flight7 VST + mainnet anchors: +- **Deep book** (BTC/ETH): `alpha * levels + beta * depth_ratio` (walks book) +- **Thin book** (alts): `intercept + adverse_selection` (fills entire book in 1-2 levels) + +Switch: `book_depth_usd < thin_book_threshold_usd → thin mode` + +Per-asset configurable, per-run overridable via `SlippageRegistry.override()`. + +## Chase Mechanics + +CHASE in DSL: cancel → wait_to_retry_ms → retry at new offset. +Parameters in FulfilmentPolicyParams: +- `wait_to_retry_ms` (0-2000): delay before re-quoting +- `chase_enabled`: enable chase-follow behavior +- `chase_offset_ticks` (0-10): ticks from target price to chase +- `chase_max_retries` (0-5): max cancel-retry cycles + +All three parameters are in the CMA-ES optimization cycle. diff --git a/MALKHUT/malkhut/cwm/hft_cwm.py b/MALKHUT/malkhut/cwm/hft_cwm.py index 53f116f..06b3cea 100644 --- a/MALKHUT/malkhut/cwm/hft_cwm.py +++ b/MALKHUT/malkhut/cwm/hft_cwm.py @@ -661,6 +661,13 @@ class HftBacktestCWM: if fq.filled and fq.post_fill_adverse_bps < 0: fill_quality_reward += params.w_adverse_selection * fq.post_fill_adverse_bps + # ── URGENCY PENALTY: penalize taker at low urgency ────────────── + # The system learns: at low urgency, prefer maker. At high urgency, taker is OK. + urgency = prev_state.intent.urgency if prev_state.intent else 0.5 + if is_cross and urgency < params.urgency_taker_threshold: + urgency_penalty = params.urgency_taker_penalty_bps * (1.0 - urgency / params.urgency_taker_threshold) + fill_quality_reward -= urgency_penalty + return base_reward + fill_quality_reward def terminal(self, state: MarketWorldState, depth: int) -> bool: diff --git a/MALKHUT/malkhut/planner/action_menu.py b/MALKHUT/malkhut/planner/action_menu.py index dfc7e54..82f3388 100644 --- a/MALKHUT/malkhut/planner/action_menu.py +++ b/MALKHUT/malkhut/planner/action_menu.py @@ -23,6 +23,10 @@ from malkhut.state import ( from malkhut.actions import FulfilmentAction +CHASE_MIN_OFFSET = 0 # minimum offset ticks for chase +CHASE_MAX_OFFSET = 5 # maximum offset ticks for chase + + def _side_for_intent(intent_kind: IntentKind) -> Side: if intent_kind in (IntentKind.ENTER_LONG, IntentKind.ADD_LONG, IntentKind.REDUCE_SHORT, IntentKind.EXIT_SHORT): return Side.BUY @@ -107,8 +111,11 @@ def build_our_actions( post_only=intent.prefer_maker, reduce_only=intent.reduce_only, )) - # Aggressive crossing if urgency allows - if intent.urgency > 0.65: + # Aggressive crossing — urgency-driven maker/taker decision + # Below threshold: passive only (maker). Above: aggressive (taker). + # The system learns the cheapest/fastest path as a function of urgency. + if intent.urgency > params.urgency_taker_threshold: + # High urgency: aggressive taker fills for frac in (0.05, 0.10, 0.25): actions.append(FulfilmentAction( kind=ActionKind.CROSS_SPREAD, side=side, @@ -116,7 +123,20 @@ def build_our_actions( qty_fraction=frac, ttl_ms=params.aggressive_ttl_ms, time_in_force="IOC", reduce_only=intent.reduce_only, + metadata={"urgency": intent.urgency, "mode": "taker"}, )) + elif intent.urgency > params.urgency_taker_threshold * 0.5: + # Medium urgency: aggressive but with IOC (partial fill OK) + for frac in (0.03, 0.06): + actions.append(FulfilmentAction( + kind=ActionKind.CROSS_SPREAD, side=side, + order_type=OrderType.LIMIT, price_ticks_from_best=0, + qty_fraction=frac, ttl_ms=params.aggressive_ttl_ms, + time_in_force="IOC", + reduce_only=intent.reduce_only, + metadata={"urgency": intent.urgency, "mode": "taker_partial"}, + )) + # Low urgency: passive only (maker) — no taker actions added # Path-risk exits if _path_risk_says_exit(state, params): @@ -127,4 +147,19 @@ def build_our_actions( metadata={"reason": "path_risk_exit"}, )) + # Chase actions (cancel → wait → retry with configurable wait_to_retry_ms) + if params.chase_enabled and params.wait_to_retry_ms > 0: + chase_ttl = params.wait_to_retry_ms + for offset in range(CHASE_MIN_OFFSET, min(CHASE_MAX_OFFSET + 1, params.chase_offset_ticks + 1)): + for frac in params.quote_size_fractions: + actions.append(FulfilmentAction( + kind=ActionKind.PLACE, side=side, + order_type=OrderType.LIMIT, + price_ticks_from_best=offset, qty_fraction=frac, + ttl_ms=chase_ttl, + post_only=True, + metadata={"chase": True, "chase_offset": offset, + "chase_max_retries": params.chase_max_retries}, + )) + return tuple(actions) diff --git a/MALKHUT/malkhut/state.py b/MALKHUT/malkhut/state.py index a7ca763..30a82eb 100644 --- a/MALKHUT/malkhut/state.py +++ b/MALKHUT/malkhut/state.py @@ -386,3 +386,13 @@ class FulfilmentPolicyParams: toxic_counterparty_weight: float low_liquidity_weight: float latency_stress_weight: float + + # Chase mechanics (cancel → wait → retry) + wait_to_retry_ms: int = 0 # ms to wait before re-quoting after cancel + chase_enabled: bool = False # enable chase-follow behavior + chase_offset_ticks: int = 1 # ticks from target price to chase + chase_max_retries: int = 3 # max cancel-retry cycles + + # Urgency-driven maker/taker decision (CMA-ES optimizable) + urgency_taker_threshold: float = 0.65 # above this urgency, prefer taker + urgency_taker_penalty_bps: float = 2.0 # penalty for taker at low urgency diff --git a/MALKHUT/malkhut/tests/test_hft_cwm.py b/MALKHUT/malkhut/tests/test_hft_cwm.py index 4606194..2fde3af 100644 --- a/MALKHUT/malkhut/tests/test_hft_cwm.py +++ b/MALKHUT/malkhut/tests/test_hft_cwm.py @@ -105,6 +105,8 @@ def _params() -> FulfilmentPolicyParams: w_fee_quality=0.5, w_time_decay=0.3, w_policy_entropy=0.5, robust_tail_weight=2.0, toxic_counterparty_weight=3.0, low_liquidity_weight=2.0, latency_stress_weight=1.0, + wait_to_retry_ms=0, chase_enabled=False, chase_offset_ticks=1, chase_max_retries=0, + urgency_taker_threshold=0.65, urgency_taker_penalty_bps=2.0, ) def _place(side: Side = Side.BUY, price_ticks: int = 0, qty: float = 0.10, diff --git a/MALKHUT/malkhut/training/cma_trainer.py b/MALKHUT/malkhut/training/cma_trainer.py index f4782ac..d34fe25 100644 --- a/MALKHUT/malkhut/training/cma_trainer.py +++ b/MALKHUT/malkhut/training/cma_trainer.py @@ -90,6 +90,11 @@ class CMAParameterCodec: ParamSpec("toxic_counterparty_weight", "float", 0.0, 10.0), ParamSpec("low_liquidity_weight", "float", 0.0, 10.0), ParamSpec("latency_stress_weight", "float", 0.0, 10.0), + ParamSpec("wait_to_retry_ms", "int", 0, 2000), + ParamSpec("chase_offset_ticks", "int", 0, 10), + ParamSpec("chase_max_retries", "int", 0, 5), + ParamSpec("urgency_taker_threshold", "float", 0.1, 0.9), + ParamSpec("urgency_taker_penalty_bps", "float", 0.0, 10.0), ) def initial_vector(self, baseline: FulfilmentPolicyParams) -> list[float]: @@ -146,6 +151,12 @@ class CMAParameterCodec: toxic_counterparty_weight=vals.get("toxic_counterparty_weight", 3.0), low_liquidity_weight=vals.get("low_liquidity_weight", 2.0), latency_stress_weight=vals.get("latency_stress_weight", 1.0), + wait_to_retry_ms=vals.get("wait_to_retry_ms", 0), + chase_enabled=vals.get("chase_max_retries", 0) > 0, + chase_offset_ticks=vals.get("chase_offset_ticks", 1), + chase_max_retries=vals.get("chase_max_retries", 0), + urgency_taker_threshold=vals.get("urgency_taker_threshold", 0.65), + urgency_taker_penalty_bps=vals.get("urgency_taker_penalty_bps", 2.0), ) diff --git a/MALKHUT/malkhut/training/dsl.py b/MALKHUT/malkhut/training/dsl.py index 02fb05c..4d96607 100644 --- a/MALKHUT/malkhut/training/dsl.py +++ b/MALKHUT/malkhut/training/dsl.py @@ -45,6 +45,7 @@ class ActionType(str, Enum): CROSS = "CROSS" SNIPER = "SNIPER" PING = "PING" + CHASE = "CHASE" # Chase: follow target price with cancel-retry loop # Cancellation CANCEL = "CANCEL" diff --git a/MALKHUT/malkhut/training/slippage_calibration.py b/MALKHUT/malkhut/training/slippage_calibration.py index 23e6d20..fdee01d 100644 --- a/MALKHUT/malkhut/training/slippage_calibration.py +++ b/MALKHUT/malkhut/training/slippage_calibration.py @@ -1,30 +1,27 @@ """ -Slippage Calibration — replace 0.1bps-per-level heuristic with VST-calibrated model. +Slippage Calibration — Flight7-anchored, per-asset, per-run overridable. -The current CWM uses: expected_slippage_bps = levels_consumed * 0.1 +Fable's Flight7 calibration (2026-07-16): -This is a constant heuristic. Real slippage is CONDITIONAL on: - - Book depth at fill time - - Order size relative to book depth - - Market regime (trending vs choppy) - - Asset-specific depth profile (alpha from power-law decay) +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). -Calibration process: - 1. Collect VST fill data: (levels_consumed, slippage_bps, order_usd, book_depth_usd) - 2. Fit per-asset model: slippage_bps = alpha * levels + beta * (order_usd / book_depth_usd) + gamma - 3. Store calibrated params in AssetBehavior - 4. CWM uses calibrated params instead of 0.1bps constant +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 -The key insight from the OB study: - - BTC alpha=0.70: deep book → ~0.05 bps/level - - ETH alpha=0.75: ~0.08 bps/level - - SOL alpha=0.85: ~0.15 bps/level - - DOGE alpha=1.00: thin book → ~0.3 bps/level +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. -VST tape format (from Flight7): - Each row: timestamp, symbol, side, fill_price, fill_qty, best_bid, best_ask, - bid_depth_levels, ask_depth_levels, bid_depth_usd, ask_depth_usd, - spread_bps, order_latency_ms +Per-asset configurable: each asset has its own SlippageCalibration. +Per-run overridable: ScenarioFactory can override per-asset models. """ from __future__ import annotations @@ -37,142 +34,183 @@ import math class SlippageCalibration: """Per-asset calibrated slippage model. - Model: slippage_bps = alpha * levels_consumed + beta * (order_usd / book_depth_usd) + intercept + 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. """ - alpha: float = 0.1 # bps per level consumed (default: 0.1) - beta: float = 0.5 # bps per unit of order/book depth ratio - intercept: float = 0.0 # base slippage (bps) - n_samples: int = 0 # how many fills used for calibration - r_squared: float = 0.0 # model fit quality + # 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 based on fill parameters.""" - depth_ratio = order_usd / max(book_depth_usd, 1.0) - return self.alpha * levels_consumed + self.beta * depth_ratio + self.intercept + """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 -@dataclass -class SlippageCalibrator: - """Calibrate slippage model from VST fill data. - - Usage: - calibrator = SlippageCalibrator() - for fill in vst_fills: - calibrator.record(fill) - model = calibrator.calibrate() - """ - _records: List[Dict[str, float]] = field(default_factory=list) - - def record( + def expected_slippage_per_level( self, - levels_consumed: int, - slippage_bps: float, order_usd: float, book_depth_usd: float, - is_maker: bool = False, - spread_bps: float = 0.0, - ) -> None: - """Record a VST fill for calibration.""" - self._records.append({ - "levels": levels_consumed, - "slippage": slippage_bps, - "order_usd": order_usd, - "book_depth_usd": book_depth_usd, - "is_maker": float(is_maker), - "spread_bps": spread_bps, - }) - - def calibrate(self) -> SlippageCalibration: - """Fit slippage model using least-squares regression. - - Model: slippage_bps = alpha * levels + beta * (order_usd / book_depth_usd) + intercept - """ - if len(self._records) < 10: - return SlippageCalibration() - - # Build feature matrix: [levels, depth_ratio] - X = [] - y = [] - for r in self._records: - depth_ratio = r["order_usd"] / max(r["book_depth_usd"], 1.0) - X.append([r["levels"], depth_ratio]) - y.append(r["slippage"]) - - n = len(X) - # Simple least-squares: y = alpha*x1 + beta*x2 + intercept - # Using normal equations: (X^T X)^-1 X^T y - sum_x1 = sum(row[0] for row in X) - sum_x2 = sum(row[1] for row in X) - sum_y = sum(y) - sum_x1sq = sum(row[0]**2 for row in X) - sum_x2sq = sum(row[1]**2 for row in X) - sum_x1x2 = sum(row[0]*row[1] for row in X) - sum_x1y = sum(row[0]*y[i] for i, row in enumerate(X)) - sum_x2y = sum(row[1]*y[i] for i, row in enumerate(X)) - - # Normal equations matrix - det = (n * sum_x1sq * sum_x2sq + - 2 * sum_x1 * sum_x2 * sum_x1x2 - - sum_x1sq * sum_x2**2 - - sum_x2sq * sum_x1**2 - - n * sum_x1x2**2) - - if abs(det) < 1e-12: - return SlippageCalibration() - - alpha = (sum_x2sq * sum_x1y - sum_x1x2 * sum_x2y + - sum_x1x2 * sum_y - sum_x1 * sum_x2 * sum_x1y / n) / det * n - beta = (sum_x1sq * sum_x2y - sum_x1x2 * sum_x1y + - sum_x1x2 * sum_y - sum_x2 * sum_x1 * sum_x1y / n) / det * n - intercept = (sum_y - alpha * sum_x1 - beta * sum_x2) / n - - # R-squared - y_mean = sum_y / n - ss_res = sum((y[i] - (alpha * X[i][0] + beta * X[i][1] + intercept))**2 for i in range(n)) - ss_tot = sum((y[i] - y_mean)**2 for i in range(n)) - r_squared = 1.0 - ss_res / max(ss_tot, 1e-12) - - return SlippageCalibration( - alpha=alpha, beta=beta, intercept=intercept, - n_samples=n, r_squared=r_squared, - ) - - @staticmethod - def default_per_asset() -> Dict[str, SlippageCalibration]: - """Default calibration from OB study research (Bouchaud/Cont/Stoikov). - - These are PRIOR values from the power-law depth model: - alpha = estimated bps per level consumed - """ - return { - "BTCUSDT": SlippageCalibration(alpha=0.05, beta=0.3, intercept=0.02, n_samples=0), - "ETHUSDT": SlippageCalibration(alpha=0.08, beta=0.4, intercept=0.03, n_samples=0), - "SOLUSDT": SlippageCalibration(alpha=0.15, beta=0.6, intercept=0.05, n_samples=0), - "DOGEUSDT": SlippageCalibration(alpha=0.30, beta=1.0, intercept=0.10, n_samples=0), - "ADAUSDT": SlippageCalibration(alpha=0.25, beta=0.8, intercept=0.08, n_samples=0), - "AVAXUSDT": SlippageCalibration(alpha=0.20, beta=0.7, intercept=0.06, n_samples=0), - "UNIUSDT": SlippageCalibration(alpha=0.35, beta=1.2, intercept=0.12, n_samples=0), - "LINKUSDT": SlippageCalibration(alpha=0.18, beta=0.6, intercept=0.05, n_samples=0), - "BNBUSDT": SlippageCalibration(alpha=0.06, beta=0.35, intercept=0.02, n_samples=0), - "MATICUSDT": SlippageCalibration(alpha=0.22, beta=0.75, intercept=0.07, n_samples=0), - "AAVEUSDT": SlippageCalibration(alpha=0.30, beta=1.0, intercept=0.10, n_samples=0), - "DOTUSDT": SlippageCalibration(alpha=0.18, beta=0.6, intercept=0.05, n_samples=0), - "ATOMUSDT": SlippageCalibration(alpha=0.25, beta=0.8, intercept=0.08, n_samples=0), - } + ) -> 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 -# Global registry -SLIPPAGE_MODELS: Dict[str, SlippageCalibration] = SlippageCalibrator.default_per_asset() +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) -def get_slippage_model(symbol: str) -> SlippageCalibration: - """Get calibrated slippage model for a symbol.""" - return SLIPPAGE_MODELS.get(symbol, SlippageCalibration(alpha=0.1, beta=0.5)) +# ============================================================================== +# 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( @@ -180,7 +218,7 @@ def expected_slippage_bps( levels_consumed: int, order_usd: float = 0.0, book_depth_usd: float = 1.0, + is_mainnet: bool = False, ) -> float: - """Predict slippage for a given fill parameters, using calibrated model.""" - model = get_slippage_model(symbol) - return model.expected_slippage_bps(levels_consumed, order_usd, book_depth_usd) + """Predict slippage using Flight7-calibrated model.""" + return REGISTRY.expected_slippage_bps(symbol, levels_consumed, order_usd, book_depth_usd, is_mainnet)