""" Execution Quality Metrics — measure the quality of execution. Metrics: - Slippage vs arrival price - Implementation shortfall - Market impact - Fill rate vs expected - Maker fill ratio """ from __future__ import annotations import math from dataclasses import dataclass, field from typing import List, Optional @dataclass(frozen=True, slots=True) class ExecutionQualityReport: """Report on execution quality for a set of fills.""" total_fills: int avg_slippage_bps: float avg_implementation_shortfall_bps: float avg_market_impact_bps: float fill_rate: float # actual fills / expected fills maker_fill_ratio: float taker_fill_ratio: float adverse_fill_ratio: float avg_fill_time_ms: float total_fees_bps: float class ExecutionQualityTracker: """ Track execution quality metrics. Measures slippage, implementation shortfall, market impact, fill rates, and fee quality. """ def __init__(self) -> None: self._fills: list[dict] = [] def record_fill( self, fill_price: float, arrival_price: float, expected_price: float, is_maker: bool, fill_time_ms: float, fee_bps: float, toxicity: float = 0.0, ) -> None: """Record a fill for quality analysis.""" slippage = (fill_price - arrival_price) / max(arrival_price, 1e-12) * 10_000 shortfall = (fill_price - expected_price) / max(expected_price, 1e-12) * 10_000 impact = abs(fill_price - arrival_price) / max(arrival_price, 1e-12) * 10_000 self._fills.append({ "fill_price": fill_price, "arrival_price": arrival_price, "expected_price": expected_price, "slippage_bps": slippage, "shortfall_bps": shortfall, "impact_bps": impact, "is_maker": is_maker, "fill_time_ms": fill_time_ms, "fee_bps": fee_bps, "toxicity": toxicity, }) def report(self) -> ExecutionQualityReport: """Generate execution quality report.""" if not self._fills: return ExecutionQualityReport( total_fills=0, avg_slippage_bps=0.0, avg_implementation_shortfall_bps=0.0, avg_market_impact_bps=0.0, fill_rate=0.0, maker_fill_ratio=0.0, taker_fill_ratio=0.0, adverse_fill_ratio=0.0, avg_fill_time_ms=0.0, total_fees_bps=0.0, ) n = len(self._fills) avg_slip = sum(f["slippage_bps"] for f in self._fills) / n avg_shortfall = sum(f["shortfall_bps"] for f in self._fills) / n avg_impact = sum(f["impact_bps"] for f in self._fills) / n avg_time = sum(f["fill_time_ms"] for f in self._fills) / n avg_fee = sum(f["fee_bps"] for f in self._fills) / n maker_count = sum(1 for f in self._fills if f["is_maker"]) toxic_count = sum(1 for f in self._fills if f["toxicity"] > 0.5) return ExecutionQualityReport( total_fills=n, avg_slippage_bps=avg_slip, avg_implementation_shortfall_bps=avg_shortfall, avg_market_impact_bps=avg_impact, fill_rate=1.0, # placeholder maker_fill_ratio=maker_count / n, taker_fill_ratio=1.0 - maker_count / n, adverse_fill_ratio=toxic_count / n, avg_fill_time_ms=avg_time, total_fees_bps=avg_fee, ) @property def total_fills(self) -> int: return len(self._fills) class RiskAdjustedReturns: """ Compute risk-adjusted return metrics. Metrics: - Sharpe ratio - Sortino ratio - Calmar ratio - Profit factor - Max drawdown """ def __init__(self, risk_free_rate: float = 0.0) -> None: self._risk_free_rate = risk_free_rate self._returns: list[float] = [] def add_return(self, ret: float) -> None: self._returns.append(ret) @property def sharpe_ratio(self) -> float: if len(self._returns) < 2: return 0.0 mean = sum(self._returns) / len(self._returns) variance = sum((r - mean) ** 2 for r in self._returns) / (len(self._returns) - 1) std = math.sqrt(variance) if variance > 0 else 1e-12 return (mean - self._risk_free_rate) / std @property def sortino_ratio(self) -> float: if len(self._returns) < 2: return 0.0 mean = sum(self._returns) / len(self._returns) downside = [r for r in self._returns if r < 0] if not downside: return float('inf') if mean > 0 else 0.0 downside_var = sum(r ** 2 for r in downside) / len(downside) downside_std = math.sqrt(downside_var) if downside_var > 0 else 1e-12 return (mean - self._risk_free_rate) / downside_std @property def profit_factor(self) -> float: gains = sum(r for r in self._returns if r > 0) losses = abs(sum(r for r in self._returns if r < 0)) if losses <= 0: return float('inf') if gains > 0 else 0.0 return gains / losses @property def max_drawdown(self) -> float: if not self._returns: return 0.0 peak = self._returns[0] max_dd = 0.0 cumulative = 0.0 for r in self._returns: cumulative += r if cumulative > peak: peak = cumulative dd = peak - cumulative if dd > max_dd: max_dd = dd return max_dd @property def calmar_ratio(self) -> float: if not self._returns: return 0.0 mean = sum(self._returns) / len(self._returns) annual_return = mean * 252 # annualize dd = self.max_drawdown if dd <= 0: return float('inf') if annual_return > 0 else 0.0 return annual_return / dd def report(self) -> dict: return { "sharpe_ratio": self.sharpe_ratio, "sortino_ratio": self.sortino_ratio, "profit_factor": self.profit_factor, "max_drawdown": self.max_drawdown, "calmar_ratio": self.calmar_ratio, "total_returns": len(self._returns), }