Files
sentiment-engine/MALKHUT/malkhut/training/execution_quality.py

193 lines
6.1 KiB
Python
Raw Normal View History

"""
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),
}