Module 3 · Lists & Loops · Lesson 3.3

Running max

Carry state through a loop with the running-maximum pattern, then use it to find a price series' maximum drawdown in one pass.

Hook

The worst ride along the way

Your fund returned 40% over five years. Investors are pleased, then they ask a harder question: what was the worst peak-to-trough loss along the way? A fund that fell by half before recovering is a very different ride from one that never fell more than a tenth. This lesson finds that number with a single loop.

Concept

Carrying state through a loop

A for loop looks at one item at a time. To answer a question about the whole list, you keep a variable outside the loop and update it inside it. That variable carries what you have learned so far from one step to the next.

The most useful example is the running maximum: the highest value seen so far.

prices = [100, 104, 101, 108, 103]
peak = prices[0]
for price in prices:
    if price > peak:
        peak = price
    print(price, peak)

peak starts at the first price. On each step it either stays the same or moves up to a new high, and it never moves down. When the loop ends, peak holds the highest price in the list.

Worked example

Recording the peak every day

A risk chart needs the running peak for every day, not only at the end. Add each day’s peak to a new list:

prices = [100, 104, 101, 108, 103]
peaks = []
peak = prices[0]
for price in prices:
    if price > peak:
        peak = price
    peaks.append(peak)
print(peaks)

It prints [100, 104, 104, 108, 108]. On day three the price dips to 101, but the peak stays at 104: the dip is a drawdown from that peak. The problem below measures exactly those dips.

Check

Predict the output

Problem

Maximum drawdown

max_drawdown(prices) -> float

A drawdown is how far a price has fallen from its highest point so far, as a fraction of that high. If LABX climbs to 120 and then falls to 90, the drawdown at 90 is (120 − 90) / 120 = 0.25, a 25% fall.

The maximum drawdown is the largest drawdown over the whole series: the worst peak-to-trough loss an investor would have lived through.

Fill in max_drawdown(prices). It receives a list of daily closing prices, oldest first, and returns the maximum drawdown as a fraction between 0 and 1.

prices returns
[100, 120, 90, 130] 0.25
[100, 90, 80] 0.2

A longer example, a month of LABX, with the running peak and the drawdown shaded:

Shaded: the drawdown, the fall from the running peak.

a month of LABX: 22 daily closes from 100 to 88.48. The worst fall runs from 106.19 on day 6 to 87.59 on day 17, a drawdown of 17.5%.
Show the data
DayClose
1100
2104.77
3103.37
4104.16
5104.45
6106.19
7103.23
8102.36
9100.81
1098.65
1196.98
1295.97
1395.4
1493.67
1594.44
1693.4
1787.59
1889.68
1988.97
2087.64
2188.09
2288.48

Constraints: prices holds between 1 and 100,000 positive numbers. Your function should look at each price once: the tests time it on long series.

Solve it in the editor

Hints

Stuck? Open one hint at a time

Hint 1 · Nudge

What do you need to remember as you walk through the prices, one at a time?

Hint 2 · Approach

Keep two numbers as you go: the highest price seen so far, and the worst drawdown seen so far. Each new price can raise the peak, or it can set a new worst drawdown against the current peak.

Hint 3 · Pseudo-code
peak = first price
worst = 0
for each price:
    if price is above peak: peak = price
    drawdown = (peak - price) / peak
    if drawdown is above worst: worst = drawdown
return worst

Solution

How to solve it

Run your code at least once to unlock the solution.

Recap

Three things to keep

  • A variable set before a loop and updated inside it carries state from one step to the next.
  • The running maximum is the highest value seen so far; update it before you use it.
  • Maximum drawdown needs one pass and two variables: O(n) time, O(1) extra space.