""" Simulation constraints: inventory limits, funding costs, fees, liquidation risk. Enforces realistic exchange and risk constraints on simulated trading. """ from __future__ import annotations from dataclasses import dataclass, field from typing import Optional @dataclass class InventoryConstraint: """Position and inventory limits.""" max_long: float = 0.005 # max long position (notional or units) max_short: float = 0.005 # max short position max_net_exposure: float = 0.005 max_gross_exposure: float = 0.01 def check(self, current_long: float, current_short: float) -> dict: """Check whether adding size violates limits.""" net = current_long - current_short gross = current_long + current_short return { "long_ok": current_long <= self.max_long, "short_ok": current_short <= self.max_short, "net_ok": abs(net) <= self.max_net_exposure, "gross_ok": gross <= self.max_gross_exposure, "long_headroom": max(0.0, self.max_long - current_long), "short_headroom": max(0.0, self.max_short - current_short), } @dataclass class FundingConstraint: """Funding rate cost estimation.""" funding_rate_hourly: float = 0.0001 # hourly funding rate (positive = shorts earn) predict_funding: bool = False def cost_per_hour(self, position: float, mark_price: float) -> float: """Cost (negative = earn) of holding position for one hour.""" return position * mark_price * self.funding_rate_hourly def annualized_cost_pct(self, funding_rate: float) -> float: """Annualize an hourly funding rate to percentage.""" return funding_rate * 365 * 24 * 100 @dataclass class FeeSchedule: """Exchange fee structure.""" maker_fee_pct: float = 0.0002 # 2 bps maker taker_fee_pct: float = 0.0005 # 5 bps taker def maker_fee(self, notional: float) -> float: return notional * self.maker_fee_pct def taker_fee(self, notional: float) -> float: return notional * self.taker_fee_pct @dataclass class LiquidationRisk: """Liquidation risk monitor.""" maintenance_margin_pct: float = 0.03 # 3% maintenance initial_margin_pct: float = 0.05 # 5% initial mark_price: float = 0.0 def liquidation_price( self, entry_price: float, size: float, position_side: str, # "long" or "short" wallet_balance: float, ) -> float: """Compute liquidation price for a position.""" if size <= 0: return 0.0 margin = entry_price * size * self.initial_margin_pct mtn = entry_price * size * self.maintenance_margin_pct if position_side == "long": return entry_price * (1 - (wallet_balance - margin) / (size * entry_price) + mtn / (size * entry_price)) else: return entry_price * (1 + (wallet_balance - margin) / (size * entry_price) - mtn / (size * entry_price)) def distance_to_liquidation_pct( self, mark_price: float, liquidation_price: float, position_side: str, ) -> float: """How far away is liquidation, as a percentage.""" if liquidation_price <= 0: return float("inf") if position_side == "long": return (mark_price - liquidation_price) / mark_price * 100 if mark_price > 0 else 0 else: return (liquidation_price - mark_price) / mark_price * 100 if mark_price > 0 else 0 def is_safe(self, distance_pct: float, threshold_pct: float = 2.0) -> bool: return distance_pct > threshold_pct @dataclass class CircuitBreaker: """Circuit breaker for stopping trading under adverse conditions.""" max_drawdown_pct: float = -5.0 # stop if PnL < -5% max_daily_trades: int = 500 max_slippage_bps: float = 50.0 # stop if avg slippage > 50bps max_toxic_rate: float = 0.4 # stop if >40% fills are toxic max_api_errors: int = 10 cooldown_seconds: float = 300.0 # 5 min cooldown after trip def evaluate(self, state: dict) -> dict: """Check all breakers. Returns reason if tripped, None if ok.""" if state.get("pnl_pct", 0) < self.max_drawdown_pct: return {"tripped": True, "reason": f"drawdown {state['pnl_pct']:.1f}% < {self.max_drawdown_pct}%"} if state.get("daily_trades", 0) > self.max_daily_trades: return {"tripped": True, "reason": f"trades {state['daily_trades']} > {self.max_daily_trades}"} if state.get("avg_slippage_bps", 0) > self.max_slippage_bps: return {"tripped": True, "reason": f"slippage {state['avg_slippage_bps']:.1f}bps > {self.max_slippage_bps}bps"} if state.get("toxic_rate", 0) > self.max_toxic_rate: return {"tripped": True, "reason": f"toxic rate {state['toxic_rate']:.1%} > {self.max_toxic_rate:.1%}"} if state.get("api_errors", 0) > self.max_api_errors: return {"tripped": True, "reason": f"api errors {state['api_errors']} > {self.max_api_errors}"} return {"tripped": False} class ConstraintManager: """Central constraint checker combining all limits.""" def __init__( self, inventory: InventoryConstraint | None = None, fees: FeeSchedule | None = None, funding: FundingConstraint | None = None, liquidation: LiquidationRisk | None = None, breaker: CircuitBreaker | None = None, ): self.inventory = inventory or InventoryConstraint() self.fees = fees or FeeSchedule() self.funding = funding or FundingConstraint() self.liquidation = liquidation or LiquidationRisk() self.breaker = breaker or CircuitBreaker() def can_place( self, side: str, size: float, current_position: float, mark_price: float, ) -> dict: """Check whether we can place an order of given side and size.""" new_pos = current_position + (size if side == "bid" else -size) limits = self.inventory.check( max(0.0, new_pos) if side == "bid" else max(0.0, current_position), max(0.0, -new_pos) if side == "ask" else max(0.0, -current_position), ) fee_est = self.fees.maker_fee(size * mark_price) return { "allowed": limits["long_ok"] and limits["short_ok"], "new_position": new_pos, "fee_estimate": round(fee_est, 6), "limits": limits, }