193 lines
6.1 KiB
Python
193 lines
6.1 KiB
Python
|
|
"""
|
||
|
|
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),
|
||
|
|
}
|