Coverage for adrs/performance/metric/ratio.py: 96%
27 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-29 05:07 +0800
« prev ^ index » next coverage.py v7.15.2, created at 2026-08-29 05:07 +0800
1import numpy as np
2import polars as pl
3from datetime import timedelta
4from typing import override, cast
6from .metric import Metrics
9class Ratio(Metrics[dict[str, np.float64]]):
10 def __init__(
11 self,
12 num_periods: int = 365,
13 period: timedelta = timedelta(days=1),
14 interval: timedelta | None = None,
15 ):
16 self.num_periods = num_periods # 365 trading days in a year (crypto)
17 self.period = period # 1 day as a base measurement
18 self.interval = interval # grid spacing; inferred from df when None
20 @override
21 def compute(self, df):
22 interval = (
23 self.interval
24 if self.interval is not None
25 else df["start_time"].diff().last()
26 )
27 if not isinstance(interval, timedelta):
28 raise Exception("performance_df does not have an interval in between data")
30 mean = cast(np.float64, df["pnl"].mean())
31 std = df["pnl"].std(ddof=1)
32 neg_std = df.select(
33 (pl.when(pl.col("pnl") < 0).then(pl.col("pnl")).otherwise(0) ** 2)
34 .mean()
35 .sqrt()
36 ).item()
38 multiplier = np.float64(self.period / interval)
39 sharpe_ratio = (
40 mean / std * cast(np.float64, np.sqrt(self.num_periods * multiplier))
41 if isinstance(std, float) and std != 0 and not np.isnan(std)
42 else np.float64(0.0)
43 )
44 sortino_ratio = (
45 mean / neg_std * cast(np.float64, np.sqrt(self.num_periods * multiplier))
46 if isinstance(neg_std, float) and neg_std != 0 and not np.isnan(neg_std)
47 else np.float64(0.0)
48 )
49 ar = mean * self.num_periods * multiplier
50 tr = df["equity"][-1]
52 total_duration = df["start_time"][-1] - df["start_time"][0]
53 years = total_duration / interval / (self.num_periods * multiplier)
54 cagr = np.prod(1 + df["pnl"].to_numpy()) ** (1 / years) - 1
56 return {
57 "sharpe_ratio": sharpe_ratio,
58 "sortino_ratio": sortino_ratio,
59 "min_cumu": cast(np.float64, df["equity"].min()),
60 "annualized_return": ar,
61 "total_return": tr,
62 "cagr": cagr,
63 "datapoints_per_year": self.num_periods * multiplier,
64 }