77import numpy as np
88import pandas as pd
99import warnings
10- from typing import Union
10+ from typing import Union , Literal
1111
1212from ..log import get_module_logger
1313from ..utils import get_date_range
2424logger = get_module_logger ("Evaluate" )
2525
2626
27- def risk_analysis (r , N : int = None , freq : str = "day" ):
27+ def risk_analysis (r , N : int = None , freq : str = "day" , mode : Literal [ "sum" , "product" ] = "sum" ):
2828 """Risk Analysis
2929 NOTE:
30- The calculation of annulaized return is different from the definition of annualized return.
30+ The calculation of annualized return is different from the definition of annualized return.
3131 It is implemented by design.
32- Qlib tries to cumulated returns by summation instead of production to avoid the cumulated curve being skewed exponentially.
32+ Qlib tries to cumulate returns by summation instead of production to avoid the cumulated curve being skewed exponentially.
3333 All the calculation of annualized returns follows this principle in Qlib.
3434
35- TODO: add a parameter to enable calculating metrics with production accumulation of return.
36-
3735 Parameters
3836 ----------
3937 r : pandas.Series
@@ -42,11 +40,14 @@ def risk_analysis(r, N: int = None, freq: str = "day"):
4240 scaler for annualizing information_ratio (day: 252, week: 50, month: 12), at least one of `N` and `freq` should exist
4341 freq: str
4442 analysis frequency used for calculating the scaler, at least one of `N` and `freq` should exist
43+ mode: Literal["sum", "product"]
44+ the method by which returns are accumulated:
45+ - "sum": Arithmetic accumulation (linear returns).
46+ - "product": Geometric accumulation (compounded returns).
4547 """
4648
4749 def cal_risk_analysis_scaler (freq ):
4850 _count , _freq = Freq .parse (freq )
49- # len(D.calendar(start_time='2010-01-01', end_time='2019-12-31', freq='day')) = 2384
5051 _freq_scaler = {
5152 Freq .NORM_FREQ_MINUTE : 240 * 238 ,
5253 Freq .NORM_FREQ_DAY : 238 ,
@@ -62,11 +63,26 @@ def cal_risk_analysis_scaler(freq):
6263 if N is None :
6364 N = cal_risk_analysis_scaler (freq )
6465
65- mean = r .mean ()
66- std = r .std (ddof = 1 )
67- annualized_return = mean * N
66+ if mode == "sum" :
67+ mean = r .mean ()
68+ std = r .std (ddof = 1 )
69+ annualized_return = mean * N
70+ max_drawdown = (r .cumsum () - r .cumsum ().cummax ()).min ()
71+ elif mode == "product" :
72+ cumulative_curve = (1 + r ).cumprod ()
73+ # geometric mean (compound annual growth rate)
74+ mean = cumulative_curve .iloc [- 1 ] ** (1 / len (r )) - 1
75+ # volatility of log returns
76+ std = np .log (1 + r ).std (ddof = 1 )
77+
78+ cumulative_return = cumulative_curve .iloc [- 1 ] - 1
79+ annualized_return = (1 + cumulative_return ) ** (N / len (r )) - 1
80+ # max percentage drawdown from peak cumulative product
81+ max_drawdown = (cumulative_curve / cumulative_curve .cummax () - 1 ).min ()
82+ else :
83+ raise ValueError (f"risk_analysis accumulation mode { mode } is not supported. Expected `sum` or `product`." )
84+
6885 information_ratio = mean / std * np .sqrt (N )
69- max_drawdown = (r .cumsum () - r .cumsum ().cummax ()).min ()
7086 data = {
7187 "mean" : mean ,
7288 "std" : std ,
0 commit comments