#2358·qlib

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:

  1. Install qlib from main at be72549
  2. Run the script below
  3. Compare the printed max drawdown with the returns
python
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.04999999999999993

Expected 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_drawdown gives -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_analysis is what PortAnaRecord writes to port_analysis.pkl so 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_mdd in qlib/contrib/report/analysis_position/report.py which draws the drawdown lines in the report and in get_max_drawdown_from_series in qlib/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
python
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