Learn
Sample programs
Complete indicators from easy to useful. Copy one, paste it into a new script in the chart, press Save and then Add to chart.
Every program in the Learn section is a complete script from the engine's own example set. Before it is published here it is compiled with no problem and run with output on synthetic seeded market data by the engine at the revision named on the page, and its text is copied byte for byte. Revision: production@9a82bf9. The programs are original examples written for this project.
More samples, grouped by kind, are in the MillionChart wiki: Sample programs, with moving averages and trend, channels and levels, oscillators and sessions, patterns and volume.
Simple moving average Start
What it does. Draws the simple moving average of the close over the price, with its length as a setting.
// A simple moving average: the mean of the last `length` closes.
// This is a complete indicator; the header says what kind of script it is.
indicator "Simple moving average"
edition = 1
display = price
input length: int = 20, name = "Length", min = 1
let average = tech.sma(close, length)
plot(average, name = "SMA", color = color.orange, width = 2)
How it works
tech.sma(close, length) is the mean of the last length closes. The input line makes the length a setting; plot draws one value per bar.
Open the chart to try it.
Donchian channel Start
What it does. Draws the highest high, the lowest low and their middle over a window of bars.
// Donchian Channel: the highest high and the lowest low of the last `length` bars, and their middle.
indicator "Donchian channel"
edition = 1
display = price
input length: int = 20, name = "Length", min = 1
let upper = tech.highest(high, length)
let lower = tech.lowest(low, length)
let middle = (upper + lower) / 2
plot(upper, name = "Upper", color = color.green)
plot(lower, name = "Lower", color = color.red)
plot(middle, name = "Middle", color = color.gray)
How it works
tech.highest(high, length) and tech.lowest(low, length) look back over the window. The middle line is plain arithmetic on the values of the same bar.
Open the chart to try it.
Trend background Start
What it does. Tints the background green above a moving average and red below it.
// A background that follows the trend: green above the moving average, red below it, nothing while it is not known yet.
indicator "Trend background"
edition = 1
display = price
input length: int = 50, name = "Length", min = 2
let trend = tech.sma(close, length)
let tint = if close > trend then color.fade(color.green, 88) else color.fade(color.red, 88)
chart.background(if is_na(trend) then na else tint, name = "Trend")
plot(trend, name = "Trend line", color = color.gray)
How it works
The colour is chosen with if close > trend then ... else .... While the average has no value yet, is_na(trend) is true and the background stays empty.
Open the chart to try it.
Inside bars Basics
What it does. Marks inside bars, and the bar that breaks out of the bar before the inside bar.
// A candlestick pattern from its definition: an inside bar has a high below and a low above the bar before it.
// A close above the mother bar's high after an inside bar is marked as a break.
indicator "Inside bars"
edition = 1
display = price
let inside = high < high[1] and low > low[1]
let broke_up = inside[1] and close > high[2]
let broke_down = inside[1] and close < low[2]
mark.char(inside, name = "Inside bar", char = "i", place = above, color = color.gray)
mark.shape(broke_up, name = "Break up", shape = triangle_up, place = below, color = color.green)
mark.shape(broke_down, name = "Break down", shape = triangle_down, place = above, color = color.red)
How it works
The pattern is written from its definition with comparisons on high[1] and low[1]. inside[1] asks whether the previous bar was an inside bar, and high[2] is the high of the bar before it.
Open the chart to try it.
Bollinger bands Basics
What it does. Draws a moving average with bands a number of standard deviations above and below it, shaded between them.
// Bollinger Bands: a simple moving average and bands a number of standard deviations above and below it.
indicator "Bollinger bands"
edition = 1
display = price
input length: int = 20, name = "Length", min = 2
input mult: float = 2.0, name = "Deviations", min = 0.1, step = 0.1
let basis = tech.sma(close, length)
let deviation = mult * tech.stdev(close, length)
let upper = basis + deviation
let lower = basis - deviation
let top = plot(upper, name = "Upper", color = color.blue)
let bottom = plot(lower, name = "Lower", color = color.blue)
plot(basis, name = "Basis", color = color.orange)
shade(top, bottom, color = color.fade(color.blue, 92), name = "Band")
How it works
tech.stdev gives the spread of the closes over the window. plot returns a handle, and shade(top, bottom, ...) fills between the upper and lower plots.
Open the chart to try it.
ATR channel Basics
What it does. Draws an exponential moving average widened by a multiple of the Average True Range.
// Average True Range: the Wilder mean of max(high - low, |high - previous close|, |low - previous close|).
// The channel is a moving average of the close, widened by a multiple of the ATR.
indicator "ATR channel"
edition = 1
display = price
input length: int = 20, name = "Length", min = 2
input mult: float = 2.0, name = "Multiple", min = 0.1, step = 0.1
let basis = tech.ema(close, length)
let range = tech.atr(length)
let upper = basis + mult * range
let lower = basis - mult * range
plot(upper, name = "Upper", color = color.red)
plot(lower, name = "Lower", color = color.green)
plot(basis, name = "Basis", color = color.gray)
How it works
tech.atr(length) measures the usual size of a bar. The multiple is a decimal input with a step, so it can be tuned from the settings.
Open the chart to try it.
RSI written out Useful
What it does. Builds Wilder’s Relative Strength Index from gains and losses, in its own pane, with overbought and oversold levels.
// Wilder's Relative Strength Index written out: RS = average gain / average loss and RSI = 100 - 100 / (1 + RS).
// The averages are Wilder's running means (tech.rma), the same smoothing the built-in tech.rsi uses.
indicator "RSI by hand"
edition = 1
display = pane
input length: int = 14, name = "Length", min = 2
let change = close - close[1]
let gain = math.max(change, 0)
let loss = math.max(-change, 0)
let average_gain = tech.rma(gain, length)
let average_loss = tech.rma(loss, length)
let strength = average_gain / average_loss
let value = 100 - 100 / (1 + strength)
plot(value, name = "RSI", color = color.purple)
level(70, name = "Overbought", color = color.red, style = dashed)
level(30, name = "Oversold", color = color.green, style = dashed)
How it works
close - close[1] is the change of each bar. math.max splits it into gains and losses, and tech.rma averages them with Wilder’s running mean. level draws the fixed lines.
Open the chart to try it.
Stochastic oscillator Useful
What it does. Shows where the close sits inside the range of the last bars, smoothed into %K and %D.
// The stochastic oscillator: where the close sits inside the last `length` bars' range, smoothed into %K and %D.
// raw %K = 100 * (close - lowest low) / (highest high - lowest low)
indicator "Stochastic"
edition = 1
display = pane
input length: int = 14, name = "Length", min = 1
input smooth: int = 3, name = "Smoothing", min = 1
let lowest_low = tech.lowest(low, length)
let highest_high = tech.highest(high, length)
let raw = 100 * (close - lowest_low) / (highest_high - lowest_low)
let k = tech.sma(raw, smooth)
let d = tech.sma(k, smooth)
plot(k, name = "%K", color = color.blue)
plot(d, name = "%D", color = color.orange)
level(80, name = "Upper", color = color.gray, style = dashed)
level(20, name = "Lower", color = color.gray, style = dashed)
How it works
The raw value is the position of the close between tech.lowest(low, length) and tech.highest(high, length). tech.sma smooths it once into k and again into d.
Open the chart to try it.
On-balance volume Useful
What it does. Adds the volume on an up close and subtracts it on a down close, and draws the running total with a signal line.
// On-Balance Volume: add the volume on an up close, subtract it on a down close, and smooth the running total.
indicator "On-balance volume"
edition = 1
display = pane
input length: int = 20, name = "Signal length", min = 1
let direction = math.sign(close - close[1])
let obv = tech.cum_sum(direction * volume)
plot(obv, name = "OBV", color = color.blue)
plot(tech.ema(obv, length), name = "Signal", color = color.orange)
How it works
math.sign(close - close[1]) gives the direction of each bar. tech.cum_sum keeps the running total over all bars, and tech.ema smooths it into the signal.
Open the chart to try it.
Bars since a high Useful
What it does. Counts the bars since the close last made a new high of the window, in a histogram.
// A value the script keeps between bars: `state` starts once, `mut` changes it.
// This counts the bars since the close last made a 20-bar high, and resets it on every new high.
indicator "Bars since a high"
edition = 1
display = pane
input length: int = 20, name = "Length", min = 2
state count = 0
let new_high = close >= tech.highest(close, length)
if new_high
count = 0
else
count += 1
plot(count, name = "Bars since", style = histogram, color = color.teal)
How it works
state count = 0 is set on the first bar and kept from bar to bar. The block if resets it on a new high and adds one on every other bar.
Open the chart to try it.
Reading earlier bars Useful
What it does. Compares the close with the close of earlier bars and with the highest close of a longer window.
// Reading earlier bars: `close[n]` is the close n bars ago, and the engine keeps a bounded history for every series.
// This compares the close with the close 50 bars ago, 100 bars ago, and the highest close of the last 200.
indicator "Look back"
edition = 1
display = pane
let change_50 = close - close[50]
let change_100 = close - close[100]
let from_peak = close - tech.highest(close, 200)
plot(change_50, name = "Change over 50 bars", color = color.blue)
plot(change_100, name = "Change over 100 bars", color = color.purple)
plot(from_peak, name = "From the 200-bar high", color = color.red)
How it works
close[50] and close[100] read earlier bars. A read reaches back only as far as the history of the series; the limits are in the notes.
Open the chart to try it.
User functions Useful
What it does. Shows the body of each candle as a share of its range, through functions written in the script.
// Your own functions: a block function with a guard, a one-line function, and an argument given by name.
indicator "User functions"
edition = 1
display = pane
fn body_share(open_price: float, close_price: float, high_price: float, low_price: float): float
let range = high_price - low_price
if range == 0
return na
return math.abs(close_price - open_price) / range
fn scaled(x: float, factor: float = 100.0): float = x * factor
plot(scaled(body_share(open, close, high, low)), name = "Body as a percent of the range")
plot(scaled(0.5), name = "Half", color = color.gray)
How it works
fn body_share(...) is a block function with a guard that returns na when the range is zero. fn scaled(x: float, factor: float = 100.0): float = x * factor is a one-line function with a default value.
Open the chart to try it.
Session highs Useful
What it does. Draws the high so far of the London session and of the New York session, each in its own time zone.
// A trading session is two times and a zone. `state session` keeps a value that restarts at the start of each session,
// so this draws the high of the London session so far, and the high of the New York session so far.
indicator "Session highs"
edition = 1
display = price
session london = 08:00 .. 16:30 @ "Europe/London"
session newyork = 09:30 .. 16:00 @ "America/New_York"
state session london london_high = high
state session newyork newyork_high = high
if london.active
london_high = math.max(london_high, high)
if newyork.active
newyork_high = math.max(newyork_high, high)
plot(london_high, name = "London high so far", color = color.green)
plot(newyork_high, name = "New York high so far", color = color.orange)
How it works
Each session line names a span of the day and a zone. state session london london_high = high starts again with each London session, and london.active keeps the update inside the session.
Open the chart to try it.
Weekday mornings Useful
What it does. Marks working-day mornings in New York time and counts them from a start date you choose.
// Calendar parts of a time value, read in a zone. clock.weekday_of and clock.hour_of take the bar's time, clock.timestamp builds the start date of the count,
// and clock.monday and clock.friday name the days. The count grows by one for each working-day morning that starts after the start date.
indicator "Weekday mornings"
edition = 1
display = pane
input start: time = clock.timestamp(2026, 1, 12, zone = "America/New_York"), name = "Count from"
let zone = "America/New_York"
let weekday = clock.weekday_of(bar.time, zone = zone)
let hour = clock.hour_of(bar.time, zone = zone)
let working_day = weekday >= clock.monday and weekday <= clock.friday
let morning = working_day and hour >= 8 and hour < 12
state mornings = 0
if morning and not morning[1] and bar.time >= start
mornings += 1
plot(if morning then 1 else 0, name = "Morning", style = histogram, color = color.blue)
plot(mornings, name = "Mornings counted", color = color.orange)
How it works
clock.weekday_of and clock.hour_of read the bar’s time in the zone. input start: time = ... is a date setting, and morning and not morning[1] is true on the first bar of each morning.
Open the chart to try it.
Swing pivots Useful
What it does. Finds swing highs and lows and labels each one with its price on the bar where it happened.
// Swing structure: a swing high is a bar whose high is the highest of `left` bars before it and `right` bars after it.
// The pivot is only known `right` bars later, so the label is placed back on the bar where the swing happened.
indicator "Swing pivots"
edition = 1
display = price
max_labels = 200
input left: int = 5, name = "Bars on the left", min = 1
input right: int = 5, name = "Bars on the right", min = 1
let swing_high = tech.pivot_high(high, left, right)
let swing_low = tech.pivot_low(low, left, right)
if not is_na(swing_high)
label.new(
bar.index - right,
swing_high,
f"{swing_high:price}",
style = label_down,
color = color.red,
)
if not is_na(swing_low)
label.new(
bar.index - right,
swing_low,
f"{swing_low:price}",
style = label_up,
color = color.green,
)
plot(close, name = "Close", color = color.gray)
How it works
tech.pivot_high(high, left, right) needs right bars after the swing, so a swing is known only that many bars later; the label is placed back with bar.index - right. max_labels in the header sets how many labels the chart keeps.
Open the chart to try it.
These samples are indicators. The wiki has more, strategies among them: moving averages and trend, channels and levels, oscillators, candle patterns, sessions, patterns and volume, statistics and tools, strategies.
Next: the fourth lesson of the course in the MillionChart wiki, Signals, arrows and zones. The whole course: Write your own scripts.