Running Drawdown
The deepest peak-to-trough decline since the first value, over an unbounded stream. Tracks the running peak, the current drawdown and the worst drawdown so far in O(1) memory, with no parameters and no warmup. Output is a non-negative fraction (
0.20= 20% drop from peak), the same convention asMaxDrawdown.
Quick reference
| Field | Value |
|---|---|
| Family | Risk / Performance |
| Input type | f64 — one equity-curve sample per update |
| Output type | f64 |
| Output range | [0, 1] (fraction of peak) for non-negative equity |
| Default parameters | none — RunningDrawdown::new() |
| Warmup period | 1 |
| Interpretation | Worst drawdown since inception; never forgets |
Formula
peak_t = max(input over [0..=t])
current_t = (peak_t - input_t) / peak_t (0 while peak_t <= 0)
max_t = max(current over [0..=t])
output_t = max_tEach update folds the new value into three numbers: the running peak (the high-water mark), the drawdown of the latest value from that peak, and the worst drawdown seen so far. Nothing is stored per bar, so memory is constant and each update is O(1) however long the stream runs.
Only a positive peak counts. While the running peak is zero or negative the fractional drawdown is undefined and the indicator reports 0, the same guard MaxDrawdown uses. See crates/wickra-core/src/indicators/running_drawdown.rs.
The result equals MaxDrawdown with a period at least as long as the stream. Two unit tests pin that: matches_max_drawdown_with_a_period_covering_the_stream runs both side by side over 200 bars, and matches_max_drawdown_at_every_bar_via_growing_periods checks every prefix against MaxDrawdown(n) over exactly that prefix.
Parameters
None. RunningDrawdown::new() takes no arguments, and RunningDrawdown::default() is the same (unit test default_matches_new). Python: wickra.RunningDrawdown(). Node: new RunningDrawdown().
Inputs / Outputs
From crates/wickra-core/src/indicators/running_drawdown.rs:
use wickra::{Indicator, RunningDrawdown};
// RunningDrawdown: Input = f64, Output = f64
const _: fn(&mut RunningDrawdown, f64) -> Option<f64> = <RunningDrawdown as Indicator>::update;update returns the maximum drawdown so far. Three accessors expose the full running state, each None before the first finite value:
| Rust | Python (property) | Meaning |
|---|---|---|
max_drawdown() | max_drawdown | Worst drawdown since the first value; what update returns. |
current_drawdown() | current_drawdown | Drawdown of the latest value from the running peak; 0 at a new peak. |
peak() | peak | Highest value seen since the first value. |
Python streams as float | None and batches as an array.array('d'). Node streams as number | null and batches as Array<number>; the Node class carries update, batch and the batch-fast variants, but not the three accessors. Since the first finite value already emits, a batch has no warmup NaN; it carries NaN only where the input itself is non-finite.
Warmup
warmup_period() == 1. The first finite input emits 0.0: a single value is its own peak, so there is no drawdown yet. Unit tests first_value_emits_immediately and accessors_and_metadata pin this.
Edge cases
- Old drawdowns are kept. A 50% drop followed by 1000 bars of new highs still reports
0.5as the maximum, while the current drawdown is back to0(testremembers_a_drawdown_older_than_any_window). This is the point of the indicator:MaxDrawdownwould drop it once it slides out of the window. - Monotonically rising input. Output is
0throughout (testpure_uptrend_yields_zero). - Non-positive peak. While the running peak is zero or negative, no drawdown is measured, so an all-non-positive series gives
0(testnon_positive_peak_yields_zero). A dip below zero after a positive peak gives a value above1. - Non-finite input.
NaNand±∞returnNoneand leave the peak, the current and the maximum drawdown untouched (testignores_non_finite_input). A non-finite first input leaves the indicator unseeded:is_ready()staysfalseandpeak()staysNone(testnon_finite_first_input_leaves_it_unseeded). - Reset.
reset()forgets the peak and both drawdowns; the next value seeds a fresh run and emits0.0(testreset_clears_state). - Batch equals streaming.
batchgives the same values as callingupdatein a loop (testbatch_equals_streaming).
Examples
The series below peaks at 120, falls to 90 (a 25% drawdown) and recovers to 110. These are the numbers of the reference_drawdown unit test.
Rust
use wickra::{BatchExt, RunningDrawdown};
fn main() {
let mut dd = RunningDrawdown::new();
let out = dd.batch(&[100.0, 120.0, 90.0, 110.0]);
println!("{:?}", out);
println!("max = {:?}", dd.max_drawdown());
println!("current = {:?}", dd.current_drawdown());
println!("peak = {:?}", dd.peak());
}Output:
[Some(0.0), Some(0.0), Some(0.25), Some(0.25)]
max = Some(0.25)
current = Some(0.08333333333333333)
peak = Some(120.0)The drop from 120 to 90 is 30 / 120 = 0.25. The recovery to 110 shrinks the current drawdown to 10 / 120 but leaves the maximum at 0.25.
Python
import wickra as ta
dd = ta.RunningDrawdown()
for x in [100.0, 120.0, 90.0, 110.0]:
print(x, '->', dd.update(x))
print('max_drawdown:', dd.max_drawdown)
print('current_drawdown:', dd.current_drawdown)
print('peak:', dd.peak)
print(ta.RunningDrawdown().batch([100.0, 120.0, 90.0, 110.0]))Output:
100.0 -> 0.0
120.0 -> 0.0
90.0 -> 0.25
110.0 -> 0.25
max_drawdown: 0.25
current_drawdown: 0.08333333333333333
peak: 120.0
array('d', [0.0, 0.0, 0.25, 0.25])Node
const ta = require('wickra');
const dd = new ta.RunningDrawdown();
for (const x of [100, 120, 90, 110]) {
console.log(x, '->', dd.update(x));
}
console.log(new ta.RunningDrawdown().batch([100, 120, 90, 110]));Output:
100 -> 0
120 -> 0
90 -> 0.25
110 -> 0.25
[ 0, 0, 0.25, 0.25 ]Streaming
use wickra::{Indicator, RunningDrawdown};
let mut dd = RunningDrawdown::new();
let equity_stream: Vec<f64> = Vec::new(); // your equity-curve feed
for equity in equity_stream {
// Emits from the first value on; no warmup to wait for.
if let Some(worst) = dd.update(equity) {
let now = dd.current_drawdown().unwrap();
if now > 0.10 { /* currently more than 10% under water */ }
if worst > 0.25 { /* the account has been down 25% at some point */ }
}
}Interpretation
RunningDrawdown answers "what is the worst this equity curve has ever been hit", for a stream that has no natural end. It is the metric a live account, a long backtest fed bar by bar, or a fund's risk report wants, where a trailing window would quietly forget the crash that matters most.
- Since-inception risk. The maximum is the figure that goes into a track record: the deepest loss an investor would have sat through, measured from the high-water mark.
- Live position in the drawdown.
current_drawdownsays how far below the high-water mark the curve is right now; at0the account is at a new peak. Many risk rules (cut size at 10% under water, stop trading at 20%) key off this value rather than the maximum. - Return per unit of pain. Divide the total return by
max_drawdownfor a since-inception RecoveryFactor, or use the rolling CalmarRatio when a window is wanted.
Use MaxDrawdown instead when the question is about recent behaviour ("the worst drawdown of the last year"); its window lets old drawdowns roll off. DrawdownDuration measures the time spent under water rather than the depth.
Common pitfalls
- Feeding returns instead of equity. The input is the equity curve (account value, cumulative wealth), not per-bar returns. A return series dips below zero all the time; fed directly, the "peak" is the best single return and the output means nothing. Accumulate returns into equity first.
- Expecting the maximum to recover. The maximum never decreases until
reset(). If you want a number that comes back down after a recovery, readcurrent_drawdown, or useMaxDrawdownwith a window. - Reading the output as signed. The value is a positive magnitude (
0.25= 25% below peak), not a negative return. - Negative or zero equity. The fraction is measured from a positive peak. A curve that starts at or below zero reports
0until it first goes positive.
References
- Magdon-Ismail, M. & Atiya, A. F. (2004). Maximum Drawdown. Risk Magazine, October 2004 — formal statistical treatment of the maximum drawdown.
- The high-water mark this indicator tracks is the same reference level hedge funds use to charge performance fees only on new highs.
See also
- MaxDrawdown — the same measure over a trailing window.
- DrawdownDuration — time under water since the all-time peak.
- RecoveryFactor — cumulative net return divided by the maximum drawdown.
- AverageDrawdown — mean drawdown depth over a window.
- UlcerIndex — RMS-weighted drawdown measure.
- Indicators-Overview — full taxonomy.