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

1import numpy as np 

2import polars as pl 

3from datetime import timedelta 

4from typing import override, cast 

5 

6from .metric import Metrics 

7 

8 

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 

19 

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

29 

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() 

37 

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] 

51 

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 

55 

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 }