millionchart
فارسی

Lesson 7: Lists and maps

Keep many values in an array or a map. A rolling window of recent bars, loops over the items, median and percentile, and counts by key.

Lesson 7 of 18. History with [n] gives one value at a time. When you need many values together, for example the last twenty bar ranges to find their middle, keep them in an array (an ordered list) or a map (values stored under keys).

A window of recent values#

Cuprel
indicator "Last closes"
    edition = 1
    display = price

input size: int = 20, name = "Window size", min = 2, max = 500
state window: array<float> = array<float>()
window.push(close)
if window.size() > size
    window.shift()
plot(window.avg(), name = "Average of the window", color = color.orange)
plot(window.max(), name = "Highest close", color = color.green)
plot(window.min(), name = "Lowest close", color = color.red)
  • state window: array<float> = array<float>() makes an empty array of numbers once and keeps it from bar to bar.
  • window.push(close) adds this bar's close at the end. When the window holds more than size items, window.shift() drops the oldest one.
  • .size(), .avg(), .max() and .min() read the whole array. .at(0) is the oldest item and .last() the newest; indexes count from 0.

Going through the items#

for x in window visits every item in order. This counts how many closes of the window lie above their average:

Cuprel
indicator "Share above the average"
    edition = 1
    display = pane

input size: int = 20, name = "Window size", min = 2, max = 500
state window: array<float> = array<float>()
window.push(close)
if window.size() > size
    window.shift()
let average = window.avg()
mut above = 0
for x in window
    if x > average
        above += 1
plot(above / window.size() * 100, name = "Closes above the average, %", color = color.purple)
level(50, name = "Half", color = color.gray, style = dashed)

mut above = 0 starts again on every bar, so the count is made fresh each time. for (i, x) in window gives the position i as well.

The middle of a window#

An array can sort its items to find a median or a percentile. Here the window holds bar ranges, and the script shows the typical range and a high one:

Cuprel
indicator "Typical range"
    edition = 1
    display = pane

input size: int = 50, name = "Window size", min = 5, max = 500
state ranges: array<float> = array<float>()
ranges.push(high - low)
if ranges.size() > size
    ranges.shift()
plot(high - low, name = "Range", style = columns, color = color.gray)
plot(ranges.median(), name = "Median range", color = color.blue)
plot(ranges.percentile(80), name = "80th percentile", color = color.orange)

ranges.median() is the middle value of the window and ranges.percentile(80) the value below which 80 percent of the items lie. A column above the orange line is a bar that is wider than four bars out of five in the window.

Values under keys#

A map stores a value under a key. This one keeps, for every hour of the day, the total range of the bars of that hour and their number, and draws the average range of the current hour:

Cuprel
indicator "Range by hour"
    edition = 1
    display = pane

state totals: map<int, float> = map<int, float>()
state counts: map<int, int> = map<int, int>()
let hour = clock.hour_of(bar.time, zone = "UTC")
totals.set(hour, (totals.at(hour) ?? 0) + (high - low))
counts.set(hour, (counts.at(hour) ?? 0) + 1)
mut widest_hour = 0
mut widest = 0.0
for key in totals.keys()
    let average = totals.at(key) / counts.at(key)
    if average > widest
        widest = average
        widest_hour = key
plot(totals.at(hour) / counts.at(hour), name = "Average range of this hour", color = color.blue)
plot(widest_hour, name = "Hour with the widest bars (UTC)", display = none)
  • totals.set(key, value) stores a value; totals.at(key) reads it, and gives na while the key is missing, so ?? 0 starts a new hour at 0.
  • totals.keys() is an array of the keys, so a for loop can go through them.
  • display = none keeps the last plot off the chart: the script still works the value out, but does not draw it.

Use it on an intraday chart: on a daily chart every bar has the same hour.

A mistake to avoid#

Brackets after a name always mean history, never "the item at": window[1] is the whole array as it was one bar ago. The checker refuses it and says to write window.at(1):

Cuprel · error E3173
indicator "Brackets on an array"
    edition = 1
    display = price

state window: array<float> = array<float>()
window.push(close)
if window.size() > 10
    window.shift()
plot(window[1], name = "Second item")

An array or a map holds at most 100,000 items; past that the script stops with E-LIMIT-ITEMS. A window that drops its oldest item with shift() never comes near it. Reading .at(i) outside the array also stops the script, so check .size() first.

Try changing#

  • In "Last closes", replace close with high - low in window.push(...): the lines now describe bar ranges instead of prices (move the script to its own pane with display = pane).
  • In "Share above the average", change x > average to x > close: the line shows the share of the window above the current close.
  • In "Typical range", try ranges.percentile(95): the orange line rises and fewer columns cross it.
  • In "Range by hour", use zone = "Europe/London": the hours are counted in London time.

Checkpoint#

  • You can keep a rolling window of values in a state array with push and shift.
  • You can go through an array with for x in ... and read single items with .at(i).
  • You can find a median and a percentile of a window.
  • You can count and sum values by key in a map, and you know that .at(key) gives na for a missing key.

Next: Lesson 8: Labels, lines and boxes