<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P085-stock-chart/ | updated: 2026-10-15 -->

# P085 Stock chart

Daily prices in whole cents drawn as a chart: the best single buy and sell, which is the best run of daily changes (Kadane); every rise taken; and the best profit from at most k trades by dynamic programming, with the trades listed.

EML-P project `projects/stock-chart` in the EML language repo: 2 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/stock-chart` runs it and `eml project verify projects/stock-chart` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: stock-buy-sell-profit (https://efficientnewlanguage.org/cases/105-stock-buy-sell-profit/), max-subarray-kadane (https://efficientnewlanguage.org/cases/091-max-subarray-kadane/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
0
x
3
0
6
abc

4
12
12 abc
12 -3
12 0
12.345 13
12. 13
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1

5
```

Screen:

```text
== Stock chart ==
Daily prices: the best single trade, the best run of changes, and the best k trades.

1) chart  2) best trade  3) best k trades  4) new prices  5) quit
choice> 0
Pick a number from 1 to 5.

1) chart  2) best trade  3) best k trades  4) new prices  5) quit
choice> x
Pick a number from 1 to 5.

1) chart  2) best trade  3) best k trades  4) new prices  5) quit
choice> 3
at most how many trades (1 to 5)> 0
Type a whole number from 1 to 5.
at most how many trades (1 to 5)> 6
Type a whole number from 1 to 5.
at most how many trades (1 to 5)> abc
Type a whole number from 1 to 5.
at most how many trades (1 to 5)> 
Cancelled.

1) chart  2) best trade  3) best k trades  4) new prices  5) quit
choice> 4
prices, oldest first (2 to 40, like 12.40 13 11.85)> 12
Type 2 to 40 prices above 0, up to 99999.99, separated by spaces.
prices, oldest first (2 to 40, like 12.40 13 11.85)> 12 abc
Type 2 to 40 prices above 0, up to 99999.99, separated by spaces.
prices, oldest first (2 to 40, like 12.40 13 11.85)> 12 -3
Type 2 to 40 prices above 0, up to 99999.99, separated by spaces.
prices, oldest first (2 to 40, like 12.40 13 11.85)> 12 0
Type 2 to 40 prices above 0, up to 99999.99, separated by spaces.
prices, oldest first (2 to 40, like 12.40 13 11.85)> 12.345 13
Type 2 to 40 prices above 0, up to 99999.99, separated by spaces.
prices, oldest first (2 to 40, like 12.40 13 11.85)> 12. 13
Type 2 to 40 prices above 0, up to 99999.99, separated by spaces.
prices, oldest first (2 to 40, like 12.40 13 11.85)> 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
Type 2 to 40 prices above 0, up to 99999.99, separated by spaces.
prices, oldest first (2 to 40, like 12.40 13 11.85)> 
Cancelled.

1) chart  2) best trade  3) best k trades  4) new prices  5) quit
choice> 5
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
2
3
1
3
2
3
5
4
10 9 8.5 7
2
3
2
4
5 5 5
2
1
5
```

Screen:

```text
== Stock chart ==
Daily prices: the best single trade, the best run of changes, and the best k trades.

1) chart  2) best trade  3) best k trades  4) new prices  5) quit
choice> 1
  day  1  12.50  ######
  day  2  13.10  ##########
  day  3  11.80  #
  day  4  12.25  ####
  day  5  14.05  #################
  day  6  13.90  ################
  day  7  15.30  ###########################
  day  8  14.80  #######################
  day  9  12.95  #########
  day 10  13.50  #############
  day 11  16.10  #################################
  day 12  15.75  ##############################
  day 13  14.40  ####################
  day 14  15.20  ##########################
  day 15  17.00  ########################################
  day 16  16.55  ####################################

1) chart  2) best trade  3) best k trades  4) new prices  5) quit
choice> 2
Best single trade: buy on day 3 at 11.80, sell on day 15 at 17.00 - 5.20 a share.
The best run of daily changes is days 3 to 15, adding up to 5.20.
The same thing: a trade's profit is the sum of the daily changes it holds through.
Trading every rise, as often as you like: 10.00.

1) chart  2) best trade  3) best k trades  4) new prices  5) quit
choice> 3
at most how many trades (1 to 5)> 1
  buy day 3 at 11.80, sell day 15 at 17.00: 5.20
At most 1 trade, one share at a time: 5.20 in all.

1) chart  2) best trade  3) best k trades  4) new prices  5) quit
choice> 3
at most how many trades (1 to 5)> 2
  buy day 3 at 11.80, sell day 7 at 15.30: 3.50
  buy day 9 at 12.95, sell day 15 at 17.00: 4.05
At most 2 trades, one share at a time: 7.55 in all.

1) chart  2) best trade  3) best k trades  4) new prices  5) quit
choice> 3
at most how many trades (1 to 5)> 5
  buy day 1 at 12.50, sell day 2 at 13.10: 0.60
  buy day 3 at 11.80, sell day 5 at 14.05: 2.25
  buy day 6 at 13.90, sell day 7 at 15.30: 1.40
  buy day 9 at 12.95, sell day 11 at 16.10: 3.15
  buy day 13 at 14.40, sell day 15 at 17.00: 2.60
At most 5 trades, one share at a time: 10.00 in all.

1) chart  2) best trade  3) best k trades  4) new prices  5) quit
choice> 4
prices, oldest first (2 to 40, like 12.40 13 11.85)> 10 9 8.5 7
4 days of prices.

1) chart  2) best trade  3) best k trades  4) new prices  5) quit
choice> 2
No single trade makes money: the price only falls (or stays). Not trading is the best.
The best run of daily changes is days 2 to 3, adding up to -0.50.
Every daily change is a fall, so the best run is the smallest fall - while the best trade is not to trade.
Trading every rise, as often as you like: 0.00.

1) chart  2) best trade  3) best k trades  4) new prices  5) quit
choice> 3
at most how many trades (1 to 5)> 2
No trade makes money.

1) chart  2) best trade  3) best k trades  4) new prices  5) quit
choice> 4
prices, oldest first (2 to 40, like 12.40 13 11.85)> 5 5 5
3 days of prices.

1) chart  2) best trade  3) best k trades  4) new prices  5) quit
choice> 2
No single trade makes money: the price only falls (or stays). Not trading is the best.
The best run of daily changes is days 1 to 2, adding up to 0.00.
No day rises, so the best run adds up to nothing - and the best trade is not to trade.
Trading every rise, as often as you like: 0.00.

1) chart  2) best trade  3) best k trades  4) new prices  5) quit
choice> 1
  day  1  5.00  #
  day  2  5.00  #
  day  3  5.00  #

1) chart  2) best trade  3) best k trades  4) new prices  5) quit
choice> 5
Bye.
```

## Modules

### main.eml

```eml
# P085 stock chart: daily prices drawn as a chart; the best single trade,
# which is the best run of daily changes; and the best profit from up to k
# trades, with the trades listed.
import market

def trim(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def cents_of(s):
    # "12.40", "12.4" or "12" as whole cents (up to 99999.99), or -1.
    0 => k
    while k < len(s) and s[k] != ".":
        k + 1 => k
    s[0:k] => whole
    "" => part
    if k < len(s):
        s[k + 1:len(s)] => part
    if len(whole) == 0 or len(whole) > 5 or len(part) > 2 or (k < len(s) and len(part) == 0):
        return -1
    0 => n
    for c in whole + part:
        if not (c >= "0" and c <= "9"):
            return -1
        n * 10 + int(c) => n
    if len(part) == 0:
        return n * 100
    if len(part) == 1:
        return n * 10
    return n

def money(c):
    if c < 0:
        return "-" + money(0 - c)
    0 => w
    c => r
    while r >= 100:
        r - 100 => r
        w + 1 => w
    if r < 10:
        return str(w) + ".0" + str(r)
    return str(w) + "." + str(r)

def prices_of(s):
    [] => out
    "" => w
    for c in s + " ":
        if c == " " or c == ",":
            if w != "":
                cents_of(w) => v
                if v <= 0:
                    return []
                out + [v] => out
            "" => w
        else:
            w + c => w
    return out

def chart(ps):
    # One row per day, a bar of up to 40 # from the lowest price to the
    # highest.
    ps[0] => lo
    ps[0] => hi
    for p in ps:
        if p < lo:
            p => lo
        if p > hi:
            p => hi
    for d in [0:len(ps) - 1]:
        1 => n
        if hi > lo:
            # 1 + floor((price - lowest) x 39 / (highest - lowest)), in
            # whole numbers
            (ps[d] - lo) * 39 => r
            while r >= hi - lo:
                r - (hi - lo) => r
                n + 1 => n
        "" => bar
        for i in [1:n]:
            bar + "#" => bar
        "" => day
        if d + 1 < 10:
            " " => day
        ("  day " + day + str(d + 1) + "  " + money(ps[d]) + "  " + bar) ^0

def best(ps):
    market.best_trade(ps) => t
    if t[0] == 0:
        "No single trade makes money: the price only falls (or stays). Not trading is the best." ^0
    else:
        ("Best single trade: buy on day " + str(t[1] + 1) + " at " + money(ps[t[1]]) + ", sell on day " + str(t[2] + 1) + " at " + money(ps[t[2]]) + " - " + money(t[0]) + " a share.") ^0
    market.changes(ps) => cs
    market.best_run(cs) => r
    ("The best run of daily changes is days " + str(r[1] + 1) + " to " + str(r[2] + 2) + ", adding up to " + money(r[0]) + ".") ^0
    if r[0] > 0:
        "The same thing: a trade's profit is the sum of the daily changes it holds through." ^0
    elif r[0] == 0:
        "No day rises, so the best run adds up to nothing - and the best trade is not to trade." ^0
    else:
        "Every daily change is a fall, so the best run is the smallest fall - while the best trade is not to trade." ^0
    ("Trading every rise, as often as you like: " + money(market.every_rise(ps)) + ".") ^0

def trades(ps):
    while True:
        trim(input("at most how many trades (1 to 5)> ")) => s
        if s == "":
            "Cancelled." ^0
            return
        if s == "1" or s == "2" or s == "3" or s == "4" or s == "5":
            int(s) => k
            market.best_k(ps, k) => r
            if len(r[1]) == 0:
                "No trade makes money." ^0
                return
            for t in r[1]:
                ("  buy day " + str(t[0] + 1) + " at " + money(ps[t[0]]) + ", sell day " + str(t[1] + 1) + " at " + money(ps[t[1]]) + ": " + money(ps[t[1]] - ps[t[0]])) ^0
            "trades" => word
            if k == 1:
                "trade" => word
            ("At most " + str(k) + " " + word + ", one share at a time: " + money(r[0]) + " in all.") ^0
            return
        "Type a whole number from 1 to 5." ^0

def new_prices(ps):
    while True:
        trim(input("prices, oldest first (2 to 40, like 12.40 13 11.85)> ")) => s
        if s == "":
            "Cancelled." ^0
            return ps
        prices_of(s) => out
        if len(out) >= 2 and len(out) <= 40:
            (str(len(out)) + " days of prices.") ^0
            return out
        "Type 2 to 40 prices above 0, up to 99999.99, separated by spaces." ^0

[1250, 1310, 1180, 1225, 1405, 1390, 1530, 1480, 1295, 1350, 1610, 1575, 1440, 1520, 1700, 1655] => prices

"== Stock chart ==" ^0
"Daily prices: the best single trade, the best run of changes, and the best k trades." ^0
True => running
while running:
    "" ^0
    "1) chart  2) best trade  3) best k trades  4) new prices  5) quit" ^0
    trim(input("choice> ")) => choice
    if choice == "1":
        chart(prices)
    elif choice == "2":
        best(prices)
    elif choice == "3":
        trades(prices)
    elif choice == "4":
        new_prices(prices) => prices
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import market

def trim(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def cents_of(s):
    k = 0
    while k < len(s) and s[k] != ".":
        k = k + 1
    whole = s[0:k]
    part = ""
    if k < len(s):
        part = s[k + 1:len(s)]
    if len(whole) == 0 or len(whole) > 5 or len(part) > 2 or k < len(s) and len(part) == 0:
        return -1
    n = 0
    for c in whole + part:
        if not (c >= "0" and c <= "9"):
            return -1
        n = n * 10 + int(c)
    if len(part) == 0:
        return n * 100
    if len(part) == 1:
        return n * 10
    return n

def money(c):
    if c < 0:
        return "-" + money(0 - c)
    w = 0
    r = c
    while r >= 100:
        r = r - 100
        w = w + 1
    if r < 10:
        return str(w) + ".0" + str(r)
    return str(w) + "." + str(r)

def prices_of(s):
    out = []
    w = ""
    for c in s + " ":
        if c == " " or c == ",":
            if w != "":
                v = cents_of(w)
                if v <= 0:
                    return []
                out = out + [v]
            w = ""
        else:
            w = w + c
    return out

def chart(ps):
    lo = ps[0]
    hi = ps[0]
    for p in ps:
        if p < lo:
            lo = p
        if p > hi:
            hi = p
    for d in range(0, len(ps)):
        n = 1
        if hi > lo:
            r = (ps[d] - lo) * 39
            while r >= hi - lo:
                r = r - (hi - lo)
                n = n + 1
        bar = ""
        for i in range(1, n+1):
            bar = bar + "#"
        day = ""
        if d + 1 < 10:
            day = " "
        print("  day " + day + str(d + 1) + "  " + money(ps[d]) + "  " + bar)

def best(ps):
    t = market.best_trade(ps)
    if t[0] == 0:
        print("No single trade makes money: the price only falls (or stays). Not trading is the best.")
    else:
        print("Best single trade: buy on day " + str(t[1] + 1) + " at " + money(ps[t[1]]) + ", sell on day " + str(t[2] + 1) + " at " + money(ps[t[2]]) + " - " + money(t[0]) + " a share.")
    cs = market.changes(ps)
    r = market.best_run(cs)
    print("The best run of daily changes is days " + str(r[1] + 1) + " to " + str(r[2] + 2) + ", adding up to " + money(r[0]) + ".")
    if r[0] > 0:
        print("The same thing: a trade's profit is the sum of the daily changes it holds through.")
    elif r[0] == 0:
        print("No day rises, so the best run adds up to nothing - and the best trade is not to trade.")
    else:
        print("Every daily change is a fall, so the best run is the smallest fall - while the best trade is not to trade.")
    print("Trading every rise, as often as you like: " + money(market.every_rise(ps)) + ".")

def trades(ps):
    while True:
        s = trim(input("at most how many trades (1 to 5)> "))
        if s == "":
            print("Cancelled.")
            return
        if s == "1" or s == "2" or s == "3" or s == "4" or s == "5":
            k = int(s)
            r = market.best_k(ps, k)
            if len(r[1]) == 0:
                print("No trade makes money.")
                return
            for t in r[1]:
                print("  buy day " + str(t[0] + 1) + " at " + money(ps[t[0]]) + ", sell day " + str(t[1] + 1) + " at " + money(ps[t[1]]) + ": " + money(ps[t[1]] - ps[t[0]]))
            word = "trades"
            if k == 1:
                word = "trade"
            print("At most " + str(k) + " " + word + ", one share at a time: " + money(r[0]) + " in all.")
            return
        print("Type a whole number from 1 to 5.")

def new_prices(ps):
    while True:
        s = trim(input("prices, oldest first (2 to 40, like 12.40 13 11.85)> "))
        if s == "":
            print("Cancelled.")
            return ps
        out = prices_of(s)
        if len(out) >= 2 and len(out) <= 40:
            print(str(len(out)) + " days of prices.")
            return out
        print("Type 2 to 40 prices above 0, up to 99999.99, separated by spaces.")

prices = [1250, 1310, 1180, 1225, 1405, 1390, 1530, 1480, 1295, 1350, 1610, 1575, 1440, 1520, 1700, 1655]
print("== Stock chart ==")
print("Daily prices: the best single trade, the best run of changes, and the best k trades.")
running = True
while running:
    print("")
    print("1) chart  2) best trade  3) best k trades  4) new prices  5) quit")
    choice = trim(input("choice> "))
    if choice == "1":
        chart(prices)
    elif choice == "2":
        best(prices)
    elif choice == "3":
        trades(prices)
    elif choice == "4":
        prices = new_prices(prices)
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
print("Bye.")
```

### market.eml

```eml
# P085 stock chart - a run of daily prices in whole cents: the best single
# buy and sell, the best run of daily changes, and the best profit from up
# to k trades, each holding at most one share at a time.

def best_trade(prices):
    # The corpus case stock-buy-sell-profit: one pass keeping the cheapest
    # price so far. Returns [profit, buy day, sell day] (days from 0), or
    # [0, -1, -1] when no trade makes money - not trading is allowed.
    0 => best
    -1 => buy
    -1 => sell
    0 => low
    for d in [1:len(prices) - 1]:
        if prices[d] - prices[low] > best:
            prices[d] - prices[low] => best
            low => buy
            d => sell
        if prices[d] < prices[low]:
            d => low
    return [best, buy, sell]

def best_run(xs):
    # The corpus case max-subarray-kadane, keeping where the run starts:
    # the largest sum of consecutive numbers (at least one), and its first
    # and last index. With every number negative it is the least negative.
    xs[0] => best
    0 => bs
    0 => be
    xs[0] => cur
    0 => cs
    for i in [1:len(xs) - 1]:
        if cur + xs[i] < xs[i]:
            xs[i] => cur
            i => cs
        else:
            cur + xs[i] => cur
        if cur > best:
            cur => best
            cs => bs
            i => be
    return [best, bs, be]

def changes(prices):
    [0] * (len(prices) - 1) => out
    for d in [1:len(prices) - 1]:
        prices[d] - prices[d - 1] => out[d - 1]
    return out

def every_rise(prices):
    # As many trades as you like: take every rise from one day to the next.
    0 => s
    for c in changes(prices):
        if c > 0:
            s + c => s
    return s

def best_k(prices, k):
    # At most k trades, by dynamic programming over the days. best[j][d] is
    # the most that can be made by day d with at most j trades: either day
    # d does nothing, or it sells a share bought on an earlier day b, with
    # best[j - 1][b] made before buying. `hold` keeps the best of
    # best[j - 1][b] - price[b] so far. Returns [profit, trades as [buy day,
    # sell day]], the trades read back from the choices made.
    len(prices) => n
    # best[j][d]: the most made by day d with at most j trades closed
    [0] * (k + 1) => best
    for j in [0:k]:
        [0] * n => best[j]
    # came[j][d]: the buy day of the trade closing on day d, or -1 if day d
    # closed none
    [0] * (k + 1) => came
    for j in [0:k]:
        [-1] * n => came[j]
    for j in [1:k]:
        0 - prices[0] => hold
        0 => hold_day
        for d in [1:n - 1]:
            best[j][d - 1] => keep
            prices[d] + hold => sell
            if sell > keep:
                sell => best[j][d]
                hold_day => came[j][d]
            else:
                keep => best[j][d]
            if best[j - 1][d] - prices[d] > hold:
                best[j - 1][d] - prices[d] => hold
                d => hold_day
    [] => trades
    k => j
    n - 1 => d
    while j > 0 and d > 0:
        if came[j][d] == -1:
            d - 1 => d
        else:
            [[came[j][d], d]] + trades => trades
            came[j][d] => d
            j - 1 => j
    return [best[k][n - 1], trades]
```

Python projection of market.eml:

```python
def best_trade(prices):
    best = 0
    buy = -1
    sell = -1
    low = 0
    for d in range(1, len(prices)):
        if prices[d] - prices[low] > best:
            best = prices[d] - prices[low]
            buy = low
            sell = d
        if prices[d] < prices[low]:
            low = d
    return [best, buy, sell]

def best_run(xs):
    best = xs[0]
    bs = 0
    be = 0
    cur = xs[0]
    cs = 0
    for i in range(1, len(xs)):
        if cur + xs[i] < xs[i]:
            cur = xs[i]
            cs = i
        else:
            cur = cur + xs[i]
        if cur > best:
            best = cur
            bs = cs
            be = i
    return [best, bs, be]

def changes(prices):
    out = [0] * (len(prices) - 1)
    for d in range(1, len(prices)):
        out[d - 1] = prices[d] - prices[d - 1]
    return out

def every_rise(prices):
    s = 0
    for c in changes(prices):
        if c > 0:
            s = s + c
    return s

def best_k(prices, k):
    n = len(prices)
    best = [0] * (k + 1)
    for j in range(0, k+1):
        best[j] = [0] * n
    came = [0] * (k + 1)
    for j in range(0, k+1):
        came[j] = [-1] * n
    for j in range(1, k+1):
        hold = 0 - prices[0]
        hold_day = 0
        for d in range(1, n):
            keep = best[j][d - 1]
            sell = prices[d] + hold
            if sell > keep:
                best[j][d] = sell
                came[j][d] = hold_day
            else:
                best[j][d] = keep
            if best[j - 1][d] - prices[d] > hold:
                hold = best[j - 1][d] - prices[d]
                hold_day = d
    trades = []
    j = k
    d = n - 1
    while j > 0 and d > 0:
        if came[j][d] == -1:
            d = d - 1
        else:
            trades = [[came[j][d], d]] + trades
            d = came[j][d]
            j = j - 1
    return [best[k][n - 1], trades]
```

## README

# P085 - Stock chart

Daily prices in whole cents, drawn as a chart. The program finds the best
single buy and sell, which is the same as the best run of daily changes;
the profit from taking every rise; and the best profit from at most k
trades, with the trades listed. It starts with 16 days of prices.

- `main.eml` - the menu, reading prices, the chart and the reports
- `market.eml` - the best trade, the best run, daily changes, every rise,
  and the best k trades

How each part works:

- Best single trade, as in the corpus case `stock-buy-sell-profit`: one
  pass, keeping the cheapest price so far. Not trading is allowed, so when
  nothing rises the best is 0.
- Best run, as in the corpus case `max-subarray-kadane`: the largest sum
  of consecutive daily changes, keeping where the run starts. A trade's
  profit is the sum of the changes it holds through, so with any rise the
  two are the same. When every change is a fall, Kadane gives the
  smallest fall, while the best trade is still 0.
- Every rise: as many trades as you like means taking each day's rise.
- At most k trades, one share at a time, by dynamic programming over the
  days: the most made by day d with at most j trades is either the most by
  the day before, or a sale on day d of a share bought on an earlier day
  b, after the most made with j - 1 trades by day b. The trades are read
  back from the choices made.
- The chart scales each price to a bar of 1 to 40 # from the lowest to the
  highest, in whole numbers.

What is checked: menu choices 1 to 5; 1 to 5 trades; 2 to 40 prices, each
above 0 and up to 99999.99, with at most two decimals, separated by
spaces. An empty answer cancels.

Sessions: `sessions/basic.in` gives:
- the chart, and the best trade: buy on day 3, sell on day 15, 5.20 a
  share, the same as the best run of changes;
- at most 1, 2 and 5 trades: 5.20, 7.55 and 10.00, the last being every
  rise;
- prices that only fall (no trade, the smallest fall as the best run) and
  prices that stay flat.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- 0, 6 and abc trades;
- one price, a word, a negative price, 0, three decimals, "12." and 41
  prices - each refused, then cancelled.

Built on the verified corpus cases `stock-buy-sell-profit` (the cheapest
price so far) and `max-subarray-kadane` (the best run, and why it differs
when nothing rises).
