Skip to content

← Everyday Standard Library Tools step 21 of 55

Easy Primitives

Find the Largest Drop from a Running Peak

A price report needs two things from a list of prices. One is the largest fall from an earlier peak, and the other is the change from each day to the next. Use accumulate for the running peaks and pairwise for the neighbours.

max_drawdown([10.0, 100.0, 50.0, 200.0])  # 0.5, the fall from 100 to 50
deltas([10.0, 100.0, 50.0, 200.0])        # [90.0, -50.0, 150.0]

What to write

def max_drawdown(prices: Sequence[float]) -> float: ...
def deltas(prices: Sequence[float]) -> list[float]: ...
def solve(prices: list[float]) -> dict[str, object]: ...

The prices are finite floats in their original order. Do not change them.

max_drawdown(prices) returns the largest fractional fall from a running peak:

$$\text{drawdown} = \max_i \left(1 - \frac{p_i}{\text{peak}_i}\right), \qquad \text{peak}_i = \max(p_0 \ldots p_i)$$

  • Skip points whose running peak is 0 or less. If no points remain, return 0.0.
  • Update the running peak as you go, so the function takes linear time. Do not scan every prefix again.
  • max(values, default=0.0) is one way to handle the case where no points remain.
  • Prices can be negative. After a positive peak, a negative price gives a drawdown above 1.0.

deltas(prices) returns each price minus the one before it, in order.

solve(prices) returns a dict with three keys:

  • max_drawdown: the result of max_drawdown(prices).
  • deltas: the result of deltas(prices).
  • seeded_length: the number of results from accumulate(prices, max, initial=0.0), which is len(prices) + 1.

The seeded call only shows how initial changes the length. Its results start with the extra 0.0, so its peaks do not line up with the prices. Do not use them for the drawdown. solve may make a separate linear pass for each key.

Your code must pass mypy --strict.

Edge cases

  • Empty input, a single price, and prices that never fall all give a drawdown of 0.0.
  • Fewer than two prices give deltas of [].
  • Empty input has a seeded length of 1, and the four-price example has 5.