Skip to content

Commit 97b5a84

Browse files
committed
Implement geometric accumulation mode for risk_analysis function (#964)
1 parent 85cc748 commit 97b5a84

File tree

1 file changed

+27
-11
lines changed

1 file changed

+27
-11
lines changed

qlib/contrib/evaluate.py

+27-11
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import numpy as np
88
import pandas as pd
99
import warnings
10-
from typing import Union
10+
from typing import Union, Literal
1111

1212
from ..log import get_module_logger
1313
from ..utils import get_date_range
@@ -24,16 +24,14 @@
2424
logger = 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

Comments
 (0)