risk_analysis max_drawdown ignores a loss in the first period
Author: AstralchemistCreated Sep 26, 2026Updated Sep 26, 2026
Bug Description
risk_analysis in qlib/contrib/evaluate.py builds the running peak only from the observed cumulative curve so the starting value is never treated as a peak and a loss in the first period is never part of max_drawdown
A series that opens with a 10% loss and then recovers reports a max drawdown of 0 in both sum and product mode and a series that opens with two 5% losses reports half of its real drawdown in sum mode
This is not about the absolute vs relative question in #1615 since the sum mode convention stays as it is and the only change is that the curve starts from 0 in sum mode and from 1 in product mode before the first period
To Reproduce
Steps to reproduce the behavior:
- Install qlib from main at be72549
- Run the script below
- Compare the printed max drawdown with the returns
import pandas as pd
from qlib.contrib.evaluate import risk_analysis
# -10% on the first day, then a recovery
r = pd.Series([-0.10, 0.05, 0.06, 0.02, 0.01], index=pd.date_range("2024-01-02", periods=5, freq="B"))
print(risk_analysis(r, N=252, mode="sum").loc["max_drawdown", "risk"])
print(risk_analysis(r, N=252, mode="product").loc["max_drawdown", "risk"])
# two -5% days, then a recovery
r = pd.Series([-0.05, -0.05, 0.03, 0.04, 0.02], index=pd.date_range("2024-01-02", periods=5, freq="B"))
print(risk_analysis(r, N=252, mode="sum").loc["max_drawdown", "risk"])
print(risk_analysis(r, N=252, mode="product").loc["max_drawdown", "risk"])Output on main
0.0
0.0
-0.05
-0.04999999999999993Expected Behavior
- the first series should report a max drawdown of -0.10 in both modes
- the second should report -0.10 in sum mode and -0.0975 in product mode
- empyrical
max_drawdowngives -0.10 and -0.0975 for the same two series
Screenshot
Not needed since the output above shows it
Environment
- Qlib version: 0.9.8.dev34 installed from main
- Python version: 3.12.14
- OS (
Windows,Linux,MacOS): MacOS - Commit number (optional, please provide it if you are using the dev version): be72549
Additional Notes
risk_analysisis whatPortAnaRecordwrites toport_analysis.pklso this is the max drawdown shown for excess return with and without cost in the standard workflow and the first day with cost starts with the opening trades- the same pattern is in
_calculate_mddinqlib/contrib/report/analysis_position/report.pywhich draws the drawdown lines in the report and inget_max_drawdown_from_seriesinqlib/contrib/evaluate_portfolio.py - open PR #2189 builds Calmar on this max drawdown and returns inf when it is 0 so a run that only loses on its first day would get an infinite Calmar
- the fix is to clip the running peak at the starting value
max_drawdown = (r.cumsum() - r.cumsum().cummax().clip(lower=0)).min() # sum
max_drawdown = (cumulative_curve / cumulative_curve.cummax().clip(lower=1) - 1).min() # product- I have a branch with this fix in all three places and tests that fail on main and pass with it and I am happy to sign the CLA and open a PR
Source: microsoft/qlib