Maximum drawdown¶
Drawdown is a nonnegative fractional loss from the running peak of an ordered positive portfolio equity/NAV path. It is not a signed return, an annualized statistic, or a loss percentile.
For each observation i, let Pᵢ = max(NAV₀, ..., NAVᵢ). The result is
maxᵢ (Pᵢ - NAVᵢ) / Pᵢ, with zero-based peak and trough indices.
| Contract | Behavior |
|---|---|
| Inputs | Ordered, finite, strictly positive NAV values |
| Empty input | Error |
| One observation or no decline | drawdown = 0, indices (0, 0) |
| Equal running peaks | Retain the earliest peak index |
| Equal computed maximum losses | Retain the first winning trough |
| Invalid value anywhere | Error, including after an earlier winning trough |
| Result | Owned snapshot, no session, handles or destruction |
[100, 120, 90, 130] loses (120 - 90) / 120 = 0.25, or 25%,
from peak index 1 to trough index 2. Later recovery does not erase that loss.
[100, 80, 100, 70] keeps peak index 0, even after recovery to the same peak.
[100, 50, 200, 100] retains the first 50% loss, indices (0, 1).
Preserve observation order
Unordered return samples cannot identify drawdown. Supply an ordered equity or NAV path, not the returns themselves. This utility does not reconstruct NAVs, adjust cash flows, assume an initial investment, or sort samples.
Runnable examples¶
"""Measure the maximum running-peak loss in an ordered positive NAV path."""
# itofin library
from itofin.statistics import maximum_drawdown
result = maximum_drawdown([100.0, 120.0, 90.0, 130.0])
assert (result.drawdown, result.peak_index, result.trough_index) == (0.25, 1, 2)
print(f"maximum drawdown {result.drawdown:.0%}, peak {result.peak_index}, trough {result.trough_index}")
package main
import (
"fmt"
itofin "github.com/benbenbang/libitofin/sdk/go"
)
func main() {
result, err := itofin.MaximumDrawdown([]float64{100, 120, 90, 130})
if err != nil {
panic(err)
}
if result.Drawdown != 0.25 || result.PeakIndex != 1 || result.TroughIndex != 2 {
panic("unexpected drawdown")
}
fmt.Printf("maximum drawdown %.0f%%, peak %d, trough %d\n", 100*result.Drawdown, result.PeakIndex, result.TroughIndex)
}
From a native-build checkout, run python example/python/drawdown.py or
cd sdk/go && go run ./examples/drawdown.
Shared APIs and numerical limits¶
- Rust:
libitofin::math::statistics::{maximum_drawdown, DrawdownResult}. - Python:
itofin.statistics.maximum_drawdownand immutableDrawdownResult. - Go:
itofin.MaximumDrawdownandDrawdownResult. - C:
itofin_maximum_drawdown, writing one caller-ownedItofinDrawdownResultwithdouble drawdownandsize_t peak_index, trough_index. Errors leave the entire result unchanged. Follow the generated header's pointer contract.
The one-pass computation uses constant extra space and (peak - NAV) / peak.
Strictly positive finite inputs make this subtraction safe from overflow, and
preserve tiny representable losses better than subtracting a ratio from one.
Floating-point comparisons define loss ties. Extremely small positive troughs
relative to peaks may round the fractional loss to 1; that is not proof
that a supplied positive NAV reached zero. No weights or annualization apply.
Independent fixtures¶
The hand-calculated fixture covers increase/decrease, recovery, repeated peaks/troughs, equal maximum losses and multiple drawdowns. Python independently enumerates every ordered peak/trough pair with exact rational arithmetic; Rust, C/C++ and Go validate the same contract, errors and snapshot behavior.