Algostream_performance.Drawdown_analysisDrawdown decomposed into episodes, with recovery times.
Complements rather than duplicates the two existing drawdown facilities:
Algostream_risk_management.Drawdown.Tracker is a streaming tracker for live risk gating — running peak, current drawdown, time under water. It answers "how bad is it right now".Portfolio.Risk_metrics.calculate_maximum_drawdown returns a single scalar over a NAV history.Neither can answer "how many drawdowns were there, how deep, and how long did each take to recover" — drawdown analysis and recovery time calculation, and is what distinguishes a strategy with one catastrophic 30% drawdown from one with ten shallow ones that recover in a day.
An episode runs peak → trough → recovery. It opens when equity first falls below a running peak, and closes when equity regains that peak. The final episode may be unrecovered, in which case recovery_ts_ns and recovery_ns are None — reported honestly rather than closed at the end of the sample, which would understate the true recovery time.
type episode = {index : int;0-based, in chronological order
*)peak_ts_ns : int64;trough_ts_ns : int64;recovery_ts_ns : int64 option;None if still underwater at the end of the sample
peak_equity : float;trough_equity : float;depth : float;fractional, positive: (peak - trough) / peak
decline_ns : int64;peak → trough
*)recovery_ns : int64 option;trough → recovery
*)underwater_ns : int64;peak → recovery, or peak → end of sample if unrecovered
*)}val episodes :
nav:(int64 * float) array ->
?min_depth:float ->
unit ->
episode arrayExtract every episode deeper than min_depth (fractional; default 0.0, i.e. all of them). nav must be in ascending time order.
val max_depth : episode array -> floatMaximum depth across episodes; 0.0 when there are none. Agrees with Portfolio.Risk_metrics.calculate_maximum_drawdown on the same NAV history — cross-checked in the test suite.
val longest_underwater_ns : episode array -> int64Longest underwater_ns across episodes, recovered or not. Usually a more decision-relevant number than maximum depth: a 10% drawdown lasting two years is worse than a 25% one that recovers in a week.
val mean_recovery_ns : episode array -> int64 optionMean and median recovery time over recovered episodes only. None when none recovered — averaging in the unrecovered ones as though they had recovered at the sample end would bias the figure downward.
val median_recovery_ns : episode array -> int64 optionval recovery_rate : episode array -> floatFraction of episodes that recovered within the sample.
Underwater curve: fractional drawdown from the running peak at every NAV point. Same length as nav; the natural input for an underwater plot.
Ulcer index — root-mean-square of the underwater curve (in percent). Penalizes deep and prolonged drawdowns, unlike maximum drawdown which is blind to duration.
Mean of the underwater curve — the average fractional drawdown experienced across the sample.
val episode_to_string : episode -> string