Skip to content

Add ulcer index and Martin ratio to the returns accessor - #881

Open
anishgurjar wants to merge 7 commits into
polakowo:masterfrom
anishgurjar:ulcer-index-martin-ratio
Open

anishgurjar wants to merge 7 commits into
polakowo:masterfrom
anishgurjar:ulcer-index-martin-ratio

Conversation

@anishgurjar

Copy link
Copy Markdown
Contributor

Adds two drawdown-aware risk metrics to ReturnsAccessor:

  • ulcer_index() / rolling_ulcer_index(): root mean square of the drawdown series. Unlike max drawdown it reflects both depth and duration of drawdowns.
  • martin_ratio() / rolling_martin_ratio(): annualized return divided by the ulcer index (the ulcer performance index).

Both are built on the existing drawdown_1d_nb and annualized_return_1d_nb, follow the same layout as calmar_ratio (1d/2d/rolling numba functions, accessor methods) and return NaN for an empty or drawdown-free series where the ratio would divide by zero. They run on the numba path only; they do not take an engine argument since vectorbt-rust has no counterpart yet.

I did not add them to stats() so existing stats output stays unchanged. Happy to add them if you want.

Tests: values are checked against an independent pandas reference, plus naming, rolling shape and the flat-series edge cases. tests/test_returns.py passes locally.

@arhancanli

Copy link
Copy Markdown

One definitional point on martin_ratio: the original Martin and McCann ulcer performance index is (annualized return minus the risk-free rate) divided by the ulcer index, the same way Sharpe uses excess return. The PR divides the raw annualized return, so the result only matches the textbook number when rf is 0, and it will read higher than other libraries' UPI for any non-zero rf. calmar_ratio here also has no rf, so staying consistent with it is defensible, but the docstring should say so ("annualized return, no risk-free adjustment") or the method could take a risk_free argument defaulting to 0 like sharpe_ratio/omega_ratio do. A small test with a non-zero rf against the pandas reference would pin whichever choice is made.

@anishgurjar

Copy link
Copy Markdown
Contributor Author

Thanks, good catch. martin_ratio and rolling_martin_ratio now take a risk_free argument (annualized, default 0) and the numerator is annualized return minus risk_free. The docstring says it defaults to the plain annualized return like calmar_ratio. I added a test with a non-zero rf against the pandas reference.

@anishgurjar
anishgurjar marked this pull request as ready for review October 11, 2026 00:39
@anishgurjar

Copy link
Copy Markdown
Contributor Author

@polakowo this is ready for review. CI is green, and the earlier review point on risk_free is addressed. Small, self-contained addition next to calmar_ratio; happy to adjust naming or add the metrics to stats() if you prefer.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants