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.
Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.
About
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 reportsmarket.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).
Recorded sessions
What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.
bad-input
interpreter: byte-equal== 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.
What was typed (17 lines)
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
basic
interpreter: byte-equal== 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.
What was typed (18 lines)
1
2
3
1
3
2
3
5
4
10 9 8.5 7
2
3
2
4
5 5 5
2
1
5
Modules
The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.
main.eml(entry)
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 (main.py)
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 (market.py)
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]