"""Fee, slippage and funding models for institutional-grade crypto backtests. Conventions (Binance USDT-M VIP0, verified 2026-10): - taker fee 5 bps, maker fee 2 bps - funding: charged at 8h ticks, cost = funding_rate * notional, sign follows the position side (longs pay when rate > 0) - slippage: square-root impact model k * sqrt(notional / ADV), floor 2 bps Everything here is a COST function: positive output = money lost. """ from __future__ import annotations import numpy as np import pandas as pd TAKER_FEE = 0.0005 MAKER_FEE = 0.0002 SLIPPAGE_FLOOR_BPS = 2.0 IMPACT_K = 0.5 # square-root impact constant DAILY_VOL_BPS_FALLBACK = 200.0 # used only if the caller cannot supply realized vol def fee(notional: float, side: str = "taker") -> float: return abs(notional) * (TAKER_FEE if side == "taker" else MAKER_FEE) def slippage_cost(notional: float, adv_notional: float, daily_vol_bps: float = DAILY_VOL_BPS_FALLBACK) -> float: """Square-root impact cost in quote currency. impact_bps = k * sigma_daily_bps * sqrt(Q / ADV) sigma_daily_bps is REALIZED daily volatility (the engine passes the value computed from the data); the floor is 2 bps. """ if adv_notional <= 0: raise ValueError("adv_notional must be positive") bps = max( SLIPPAGE_FLOOR_BPS, IMPACT_K * daily_vol_bps * np.sqrt(abs(notional) / adv_notional), ) return abs(notional) * bps / 1e4 def funding_cost( positions: pd.Series, funding: pd.Series, prices: pd.Series, ) -> pd.Series: """Per-bar funding cost series, accrued only at funding timestamps. positions: position in units of base asset, indexed by bar start time. funding: funding_rate at each 8h tick. prices: mark/close price at each bar, for notional valuation. A bar starting at t covers [t, t+1h); a funding tick at t charges that tick to the position held entering the tick, i.e. the position at t. """ tick_times = funding.index pos_at_tick = positions.reindex(tick_times, method="ffill") price_at_tick = prices.reindex(tick_times, method="ffill") cost = pos_at_tick * price_at_tick * funding # longs pay positive funding return cost.reindex(positions.index, fill_value=0.0).fillna(0.0)