Skip to content

risk_analysis max_drawdown ignores a loss in the first period #2358

Description

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
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 feat(evaluate): add Sortino, Calmar, and max drawdown duration to risk_analysis #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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions