Source code for alpheast.shared.metrics


from decimal import Decimal
import logging
from typing import Any, Dict, List, Optional

import numpy as np
import pandas as pd


TRADING_DAYS_PER_YEAR = 252

[docs] def calculate_performance_metrics( daily_values: List[Dict[str, Any]], trade_log: List[Dict[str, Any]], risk_free_rate: float = 0.0, benchmark_daily_values: Optional[List[Dict[str, Any]]] = None ) -> Dict[str, Any]: """ Calculates a set of common backtesting performance metrics for the strategy and optionally for a benchmark. Args: daily_values: List of dictionaries from PortfolioManager.get_daily_values(). Each dict should have "date" and "value". trade_log: List of dictionaries from PortfolioManager.get_trade_log(). risk_free_rate: Annual risk-free rate for Sharpe Ratio calculation. benchmark_daily_values: Optional list of dictionaries for benchmark equity. Each dict should have "date" and "value". Returns: A dictionary containing various performance metrics, potentially nested for strategy and benchmark. """ results = {} # --- Process Strategy Performance --- if not daily_values: logging.error("No daily values provided for strategy performance calculation.") results["strategy"] = {"error": "No daily values to calculate metrics."} else: df_strategy = pd.DataFrame(daily_values) df_strategy["date"] = pd.to_datetime(df_strategy["date"]) df_strategy = df_strategy.set_index("date") df_strategy = df_strategy.sort_index() # Ensure chronological order if df_strategy.empty: logging.warning("Strategy DataFrame is empty after processing. Cannot calculate metrics.") results["strategy"] = {"error": "Not enough data to calculate daily returns or metrics for strategy."} else: strategy_metrics = _calculate_single_equity_metrics(df_strategy, trade_log, risk_free_rate) strategy_metrics["total_trades"] = len(trade_log) results["strategy"] = strategy_metrics # --- Process Benchmark Performance (if provided) --- if benchmark_daily_values: if not benchmark_daily_values: logging.warning("Benchmark daily values list is empty, skipping benchmark metrics.") else: df_benchmark = pd.DataFrame(benchmark_daily_values) df_benchmark["date"] = pd.to_datetime(df_benchmark["date"]) df_benchmark = df_benchmark.set_index("date") df_benchmark = df_benchmark.sort_index() if df_benchmark.empty: logging.warning("Benchmark DataFrame is empty after processing. Cannot calculate metrics.") else: benchmark_metrics = _calculate_single_equity_metrics(df_benchmark, [], risk_free_rate) benchmark_metrics["total_trades"] = "N/A" results["benchmark"] = benchmark_metrics return results
def _calculate_single_equity_metrics( df: pd.DataFrame, trade_log: List[Dict[str, Any]], risk_free_rate: float ) -> Dict[str, Any]: """ Helper function to calculate metrics for a single equity curve (strategy or benchmark). """ if df.empty: return { "initial_portfolio_value": 0.0, "final_portfolio_value": 0.0, "total_return": 0.0, "annualized_return": 0.0, "annualized_volatility": 0.0, "sharpe_ratio": "N/A", "max_drawdown": 0.0, "total_trades": len(trade_log) } df["value"] = df["value"].apply(lambda x: float(x) if isinstance(x, Decimal) else float(x)) df["daily_return"] = df["value"].pct_change() df = df.dropna().copy() if df.empty: return { "initial_portfolio_value": 0.0, "final_portfolio_value": 0.0, "total_return": 0.0, "annualized_return": 0.0, "annualized_volatility": 0.0, "sharpe_ratio": "N/A", "max_drawdown": 0.0, "total_trades": len(trade_log) } initial_value = df["value"].iloc[0] final_value = df["value"].iloc[-1] # Total Return total_return = (final_value / initial_value) - 1.0 if initial_value != 0 else 0.0 # Annualized Return num_trading_days = len(df) if num_trading_days > 0 and (1 + total_return) >= 0: # Ensure base for power is non-negative annualization_factor_return = TRADING_DAYS_PER_YEAR / num_trading_days annualized_return = (1 + total_return) ** annualization_factor_return - 1 else: annualized_return = 0.0 # Annualized Volatility daily_volatility = df["daily_return"].std() annualized_volatility = daily_volatility * np.sqrt(TRADING_DAYS_PER_YEAR) if not pd.isna(daily_volatility) else 0.0 # Sharpe Ratio if annualized_volatility != 0: sharpe_ratio = (annualized_return - risk_free_rate) / annualized_volatility else: sharpe_ratio = np.nan # Max Drawdown df["peak"] = df["value"].cummax() df["drawdown"] = (df["value"] - df["peak"]) / df["peak"] max_drawdown = df["drawdown"].min() if not df["drawdown"].empty else 0.0 metrics = { "initial_portfolio_value": round(float(initial_value), 2), "final_portfolio_value": round(float(final_value), 2), "total_return": round(total_return * 100, 2), "annualized_return": round(annualized_return * 100, 2), "annualized_volatility": round(annualized_volatility * 100, 2), "sharpe_ratio": round(sharpe_ratio, 2) if not pd.isna(sharpe_ratio) else "N/A", "max_drawdown": round(max_drawdown * 100, 2), } return metrics