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.

Simple moving average
// 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
// 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.

Trend background
// 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.

Inside bars
// 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
// 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.

ATR channel
// 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.

RSI written out
// 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.

Stochastic oscillator
// 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
// 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.

Bars since a high
// 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
// 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.

User functions
// 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.

Session highs
// 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.

Weekday mornings
// 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 pivots
// 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.