Ulcer Index Scale, Definition, and Implementation Differences
Summary
The document explains why an Ulcer Index result expressed as a decimal may look smaller than values shown elsewhere. Under the stated definition, the index is the root mean square of the drawdown from a prior peak, where drawdown is measured as one minus current value divided by the peak. If the input series is normalized between zero and one, the index is also expressed as a fraction; multiplying by 100 converts it to percentage points without changing its meaning.
The response notes that the referenced library implementation is not identical to that definition: it takes returns as input, sums them without compounding, and divides by the sample size minus one rather than the sample size. Examples using an illustrative data file and historical US equity market prices show decimal results, while a yearly summary demonstrates that typical values depend on the series and period. Thus there is no universal expected range such as zero to four; users should check the input convention and calculation details before comparing results.
Key ideas
- The Ulcer Index is the root mean square of declines from prior peaks.
- A decimal result can be converted to percentage points by multiplying by 100.
- The library implementation described uses summed returns, ignores compounding, and divides by sample size minus one.
- Expected index values depend on the asset series and measurement period.
- Compare implementations only after checking their inputs, drawdown convention, and denominator.
Tags
Full text
# Discrepancy in Ulcer Index calculation with the Riskfolio-Lib library
# Discrepancy in Ulcer Index calculation with the Riskfolio-Lib library
I am using the Riskfolio-Lib Python library to calculate the Ulcer Index. When I run the function, the values I get are on the order of hundredths (for example, 0.0X).
My issue is that, based on other resources and examples I've seen online, the Ulcer Index is typically presented as a value between 0 and 4. This discrepancy makes me wonder if the results I'm getting are correct.
I've reviewed the library's code and identified the specific line where the function is calculated: https://github.com/dcajasn/Riskfolio-Lib/blob/2ba0ccd217b5de63c2c20e2bcfcfd40ee0ad72d2/riskfolio/src/RiskFunctions.py#L1005
My question is: Do I need to multiply the result from the function by 100 to get the value in the more common range (0-4)? Or is there a conceptual difference in how this library calculates the index compared to other sources?
I appreciate any help you can provide.
## Answer by Enrico Schumann (score 1)
https://quant.stackexchange.com/a/84043
From https://en.wikipedia.org/wiki/Ulcer_index and https://www.tangotools.com/ui/ui.htm , the Ulcer index is the square root of the mean squared amount "under water" of a series, with "under water" defined as `1 - current_value/prior_peak`.
Assuming a portfolio can be between 0 and 100% under water, any value for the Ulcer index must be between 0 and 100%, which numerically is between 0 and 1. You can, of course, multiply this value by 100, if you prefer percentage points (0.01 is the same as 1%).
The implementation you referenced differs somewhat from that definition, but that is mentioned in the code: it ignores compounding (it takes returns as input and then sums them), and it divides by N - 1 instead of N. It does not multiply by 100.
For reference, in base R (without any packages):
```
Ulcer <- function(s) {
D <- 1 - s/cummax(s)
sqrt(mean(D * D))
}
```
One can check with the example file provided at tangotools.com:
```
download.file("https://www.tangotools.com/ui/UlcerIndex.xls",
"~/Downloads/UlcerIndex.xls")
f <- xlsx::read.xlsx("~/Desktop/UlcerIndex.xls", 1,
startRow = 15, header = FALSE)
Ulcer(f[, 2])
## [1] 0.165385
```
Which is the result provided in the file. Or, using the (daily) returns over the last 10 years of the US market portfolio, from Kenneth French's website:
```
library("NMOF")
library("zoo")
market <- French(dataset = "market", dest.dir = tempdir(),
frequency = "daily", return.class = "zoo",
price.series = TRUE)
Ulcer(window(market, start = as.Date("2015-01-01")))
## [1] 0.07806045
```
In both examples the results were outside the 0-4 range. But what "normal" values are will much depend on the type of series you look at. For the calendar years 1980 to 2024, the US equity market had the following values.
```
summary(
sapply(
as.list(1980:2024),
function(y) {
Ulcer(window(market,
start = as.Date(paste0(y, "-01-01")),
end = as.Date(paste0(y, "-12-31"))))
}))
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.00759 0.02587 0.03701 0.05546 0.06869 0.19363
```Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.