~/theproptrader $ ls indicators/

TradingView indicators

Free Pine Script indicators for your own chart. Each one draws information on the chart; none of them gives trade signals. Copy the source into the TradingView Pine Editor, or download the .pine file.

← Back to the home page

PINE v6

HTF Candles

Shows the last few candles of a higher timeframe to the right of your chart, so the wider candle is in view while you work on a lower timeframe.

  • Choose the timeframe and number of candles (default: five 1-hour candles)
  • Separate up and down colors for body and wick; borders match the wick
  • Dotted Open, High and Low lines for the latest candle, drawn from its real start
  • Open, High, Low and Close values beside the latest candle
  • Each candle’s open time inside its body (Central Time by default)
Platform
TradingView
Timeframe
Equal to or above the chart
  1. Copy the source or download the .pine file.
  2. In TradingView, open the Pine Editor and paste it in.
  3. Click Add to chart, then adjust the settings.
Download .pine

View source
//@version=6
indicator("HTF Candles", shorttitle="HTF Candles", overlay=true, max_boxes_count=500, max_lines_count=500, max_labels_count=500)

// ── Inputs ────────────────────────────────────────────────────────────────
const string G_HTF   = "HTF candles"
const string G_BULL  = "Bullish candle colors"
const string G_BEAR  = "Bearish candle colors"
const string G_OHLC  = "Latest candle OHLC"
const string G_TIME  = "Candle open time"

string htfTf         = input.timeframe("60", "Timeframe", group=G_HTF)
int    candleCount   = input.int(5, "Number of candles", minval=1, maxval=100, group=G_HTF)
int    offsetBars    = input.int(5, "Offset from last bar (bars)", minval=1, maxval=200, group=G_HTF)
int    candleWidth   = input.int(5, "Candle width (bars)", minval=1, maxval=50, group=G_HTF)
int    candleSpacing = input.int(2, "Spacing between candles (bars)", minval=0, maxval=50, group=G_HTF)
int    wickWidth     = input.int(1, "Wick width", minval=1, maxval=5, group=G_HTF)

color bullBody = input.color(#ffffff, "Body", group=G_BULL, inline="bull")
color bullWick = input.color(#585b70, "Wick / border", group=G_BULL, inline="bull")
color bearBody = input.color(#585b70, "Body", group=G_BEAR, inline="bear")
color bearWick = input.color(#585b70, "Wick / border", group=G_BEAR, inline="bear")

bool   showOhlcLines  = input.bool(true, "Show OHLC lines", group=G_OHLC)
bool   showOhlcValues = input.bool(true, "Show OHLC values", group=G_OHLC)
color  ohlcLineColor  = input.color(#000000, "Line color", group=G_OHLC, inline="ohlc")
string ohlcStyleIn    = input.string("Dotted", "Style", options=["Dotted", "Dashed", "Solid"], group=G_OHLC, inline="ohlc")
color  ohlcTextColor  = input.color(#000000, "Value text color", group=G_OHLC)
string ohlcSizeIn     = input.string("Small", "Value text size", options=["Tiny", "Small", "Normal"], group=G_OHLC)

bool   showOpenTime  = input.bool(true, "Show open time inside candles", group=G_TIME)
string timeTz        = input.string("America/Chicago", "Timezone", group=G_TIME)
color  timeTextColor = input.color(#000000, "Text color", group=G_TIME)
string timeSizeIn    = input.string("Auto", "Text size", options=["Auto", "Tiny", "Small", "Normal"], group=G_TIME)

if timeframe.in_seconds(htfTf) < timeframe.in_seconds(timeframe.period)
    runtime.error("HTF Candles: the selected timeframe must be equal to or higher than the chart timeframe.")

// TradingView rejects bar_index drawings more than 500 bars into the future.
int projectedSpan = offsetBars + candleCount * candleWidth + (candleCount - 1) * candleSpacing
if projectedSpan > 500
    runtime.error("HTF Candles: offset + candle widths + spacing exceed 500 bars. Reduce the count, width, spacing or offset.")

string ohlcStyle = switch ohlcStyleIn
    "Dashed" => line.style_dashed
    "Solid"  => line.style_solid
    => line.style_dotted

string ohlcSize = switch ohlcSizeIn
    "Tiny"   => size.tiny
    "Normal" => size.normal
    => size.small

string timeSize = switch timeSizeIn
    "Tiny"   => size.tiny
    "Small"  => size.small
    "Normal" => size.normal
    => size.auto

string timeFormat = timeframe.in_seconds(htfTf) < 86400 ? "HH:mm" : "MM-dd"

// ── HTF data ──────────────────────────────────────────────────────────────
// Runs in the HTF context: keeps a rolling window of the last n HTF candles.
// Realtime ticks roll var state back to the bar start, so the last element is
// always the developing candle.
htfHistory(int n) =>
    var array<float> os = array.new<float>()
    var array<float> hs = array.new<float>()
    var array<float> ls = array.new<float>()
    var array<float> cs = array.new<float>()
    var array<int>   ts = array.new<int>()
    os.push(open)
    hs.push(high)
    ls.push(low)
    cs.push(close)
    ts.push(time)
    while os.size() > n
        os.shift()
        hs.shift()
        ls.shift()
        cs.shift()
        ts.shift()
    [os, hs, ls, cs, ts]

// Lookahead keeps the latest HTF candle aligned with the chart bars it contains.
// Values are only drawn on the last bar, so this does not leak into history.
[hO, hH, hL, hC, hT] = request.security(syminfo.tickerid, htfTf, htfHistory(candleCount), lookahead=barmerge.lookahead_on)

// Chart bar index where the latest HTF candle starts.
int latestHtfTime = hT.size() > 0 ? hT.last() : na
var int htfStartIdx = na
if not na(latestHtfTime) and (na(htfStartIdx) or latestHtfTime != latestHtfTime[1])
    htfStartIdx := bar_index

// ── Drawing ───────────────────────────────────────────────────────────────
var array<box>   bodies     = array.new<box>()
var array<line>  wicks      = array.new<line>()
var array<line>  ohlcLines  = array.new<line>()
var array<label> ohlcLabels = array.new<label>()

if barstate.islast
    for b in bodies
        b.delete()
    for w in wicks
        w.delete()
    for l in ohlcLines
        l.delete()
    for lb in ohlcLabels
        lb.delete()
    bodies.clear()
    wicks.clear()
    ohlcLines.clear()
    ohlcLabels.clear()

    int n = hO.size()
    if n > 0
        int step      = candleWidth + candleSpacing
        int firstLeft = bar_index + offsetBars
        int lastRight = na
        for i = 0 to n - 1
            float o = hO.get(i)
            float h = hH.get(i)
            float l = hL.get(i)
            float c = hC.get(i)
            bool  bull    = c >= o
            float bodyTop = math.max(o, c)
            float bodyBot = math.min(o, c)
            int left  = firstLeft + i * step
            int right = left + candleWidth - 1
            int mid   = left + math.floor((candleWidth - 1) / 2)
            color wc  = bull ? bullWick : bearWick
            // Split wicks so they do not show through transparent or hollow bodies.
            if h > bodyTop
                wicks.push(line.new(mid, h, mid, bodyTop, xloc=xloc.bar_index, color=wc, width=wickWidth))
            if l < bodyBot
                wicks.push(line.new(mid, bodyBot, mid, l, xloc=xloc.bar_index, color=wc, width=wickWidth))
            bodies.push(box.new(left, bodyTop, right, bodyBot, xloc=xloc.bar_index,
                 border_color=wc, bgcolor=bull ? bullBody : bearBody,
                 text=showOpenTime ? str.format_time(hT.get(i), timeFormat, timeTz) : "",
                 text_color=timeTextColor, text_size=timeSize, text_halign=text.align_center, text_valign=text.align_center))
            lastRight := right

        if showOhlcLines or showOhlcValues
            // Drawings cannot reach more than ~10,000 bars into the past.
            int x1 = na(htfStartIdx) ? bar_index : math.max(htfStartIdx, bar_index - 9999)
            int x2 = lastRight + 1
            array<float>  prices = array.from(hO.last(), hH.last(), hL.last(), hC.last())
            array<string> names  = array.from("O", "H", "L", "C")
            for j = 0 to 3
                float p = prices.get(j)
                if showOhlcLines and names.get(j) != "C"
                    ohlcLines.push(line.new(x1, p, x2, p, xloc=xloc.bar_index, color=ohlcLineColor, style=ohlcStyle, width=1))
                if showOhlcValues
                    ohlcLabels.push(label.new(x2, p, names.get(j) + " " + str.tostring(p, format.mintick), xloc=xloc.bar_index,
                         style=label.style_label_left, color=color.new(color.black, 100), textcolor=ohlcTextColor, size=ohlcSize))
PINE v6

BSE / SSE Expansions

Highlights every buy-side and sell-side expansion on your chart: two or more same-direction candles, each closing at or beyond the previous candle’s extreme.

  • BSE: two or more up candles, each later Close at or above the previous High (white)
  • SSE: two or more down candles, each later Close at or below the previous Low (#636363)
  • A box around the whole expansion that grows while it continues, or colored candles instead
  • Only complete candles count; a doji or failed close ends the expansion
  • Alerts for each new BSE and SSE
Platform
TradingView
Timeframe
The chart’s own
  1. Copy the source or download the .pine file.
  2. In TradingView, open the Pine Editor and paste it in.
  3. Click Add to chart, then adjust the settings.
Download .pine

View source
//@version=6
indicator("BSE / SSE Expansions", shorttitle="BSE/SSE", overlay=true, max_boxes_count=500)

// ── Inputs ────────────────────────────────────────────────────────────────
const string G_EXP  = "Expansions"
const string G_BSE  = "BSE colors"
const string G_SSE  = "SSE colors"

string styleIn   = input.string("Box", "Highlight style", options=["Box", "Candle color"], group=G_EXP)
bool   showBse   = input.bool(true, "Show BSE", group=G_EXP, inline="show")
bool   showSse   = input.bool(true, "Show SSE", group=G_EXP, inline="show")
int    maxShown  = input.int(200, "Maximum boxes kept", minval=1, maxval=500, group=G_EXP)
int    fillTransp = input.int(60, "Box fill transparency", minval=0, maxval=100, group=G_EXP)

color bseFill = input.color(#ffffff, "Fill", group=G_BSE)
color sseFill = input.color(#636363, "Fill", group=G_SSE)

bool boxMode = styleIn == "Box"

// ── Expansion tracking ────────────────────────────────────────────────────
// BSE: two or more successive BSD candles (Close > Open); from the second
// candle on, Close >= previous High. SSE mirrors with Close < Open and
// Close <= previous Low. Dojis end a run. Only complete candles count.
var int   runDir   = 0
var int   runLen   = 0
var int   runStart = na
var float runHigh  = na
var float runLow   = na
var box   runBox   = na
var array<box> boxes = array.new<box>()

bool newExpansion = false
bool inExpansion  = false

if barstate.isconfirmed
    bool bsd = close > open
    bool ssd = close < open
    bool cont = (runDir == 1 and bsd and close >= high[1]) or (runDir == -1 and ssd and close <= low[1])
    if cont
        runLen  += 1
        runHigh := math.max(runHigh, high)
        runLow  := math.min(runLow, low)
        inExpansion := true
        if runLen == 2
            newExpansion := true
            bool shown = runDir == 1 ? showBse : showSse
            if boxMode and shown
                color fc = runDir == 1 ? bseFill : sseFill
                // Visual only: the box ends at the last candle's open, one candle short of its close.
                runBox := box.new(runStart, runHigh, time, runLow, xloc=xloc.bar_time,
                     border_color=na, bgcolor=color.new(fc, fillTransp))
                boxes.push(runBox)
                while boxes.size() > maxShown
                    boxes.shift().delete()
        else if not na(runBox)
            runBox.set_right(time)
            runBox.set_top(runHigh)
            runBox.set_bottom(runLow)
    else
        runDir   := bsd ? 1 : ssd ? -1 : 0
        runLen   := runDir != 0 ? 1 : 0
        runStart := time
        runHigh  := high
        runLow   := low
        runBox   := na

// ── Candle color mode ─────────────────────────────────────────────────────
bool  dirShown = runDir == 1 ? showBse : showSse
color runColor = runDir == 1 ? bseFill : sseFill
color barCol   = not boxMode and inExpansion and dirShown ? runColor : na
barcolor(barCol, title="Expansion candle")
// The first candle only becomes part of an expansion when the second closes.
barcolor(not boxMode and newExpansion and dirShown ? runColor : na, offset=-1, title="Expansion first candle")

alertcondition(newExpansion and runDir == 1, "New BSE", "BSE confirmed on {{ticker}} {{interval}}")
alertcondition(newExpansion and runDir == -1, "New SSE", "SSE confirmed on {{ticker}} {{interval}}")
PINE v6

Previous HTF Open / High / Low

Draws dotted Open, High and Low levels from the previous 1H, 4H, daily and weekly candles, plus each live candle’s open, with values at the right of your chart.

  • Previous 1H, 4H, daily and weekly High and Low, each timeframe switched on or off separately
  • Previous candle opens (1H O, 4H O, PDO, PWO) and live candle opens (1H LIVE O … W LIVE O), each optional
  • Only timeframes above the chart’s are shown; the chart’s own timeframe is skipped
  • Levels at the same price share one value label
  • Dotted, dashed or solid lines, with color, width, length and text size settings
Platform
TradingView
Timeframe
Below the levels shown
  1. Copy the source or download the .pine file.
  2. In TradingView, open the Pine Editor and paste it in.
  3. Click Add to chart, then adjust the settings.
Download .pine

View source
//@version=6
indicator("Previous HTF Open / High / Low", shorttitle="Prev O/H/L", overlay=true, max_lines_count=50, max_labels_count=50)

// ── Inputs ────────────────────────────────────────────────────────────────
const string G_LVL  = "Levels"
const string G_LINE = "Lines and values"

bool  show1h = input.bool(true, "Previous 1H high / low", group=G_LVL, inline="1h")
color col1h  = input.color(#000000, "", group=G_LVL, inline="1h")
bool  show4h = input.bool(true, "Previous 4H high / low", group=G_LVL, inline="4h")
color col4h  = input.color(#000000, "", group=G_LVL, inline="4h")
bool  showD  = input.bool(true, "Previous daily high / low", group=G_LVL, inline="d")
color colD   = input.color(#000000, "", group=G_LVL, inline="d")
bool  showW  = input.bool(true, "Previous weekly high / low", group=G_LVL, inline="w")
color colW   = input.color(#000000, "", group=G_LVL, inline="w")

bool   showPrevOpen = input.bool(true, "Show previous candle opens", group=G_LVL)
bool   showCurOpen  = input.bool(true, "Show live candle opens", group=G_LVL)

string startIn    = input.string("Previous candle open", "Lines start at", options=["Previous candle open", "Current candle open"], group=G_LINE)
string styleIn    = input.string("Dotted", "Line style", options=["Dotted", "Dashed", "Solid"], group=G_LINE)
int    lineWidth  = input.int(1, "Line width", minval=1, maxval=4, group=G_LINE)
int    offsetBars = input.int(10, "Extend right (bars)", minval=0, maxval=200, group=G_LINE)
bool   showValues = input.bool(true, "Show values", group=G_LINE)
color  textColor  = input.color(#000000, "Value text color", group=G_LINE)
string sizeIn     = input.string("Small", "Value text size", options=["Tiny", "Small", "Normal"], group=G_LINE)

string lineStyle = switch styleIn
    "Dashed" => line.style_dashed
    "Solid"  => line.style_solid
    => line.style_dotted

string textSize = switch sizeIn
    "Tiny"   => size.tiny
    "Normal" => size.normal
    => size.small

// ── HTF data ──────────────────────────────────────────────────────────────
// [1] with lookahead gives the completed previous candle without repainting.
// The current candle's open is known when it starts, so it does not repaint either.
[h1h, l1h, op1h, oc1h, tp1h, tc1h] = request.security(syminfo.tickerid, "60",  [high[1], low[1], open[1], open, time[1], time], lookahead=barmerge.lookahead_on)
[h4h, l4h, op4h, oc4h, tp4h, tc4h] = request.security(syminfo.tickerid, "240", [high[1], low[1], open[1], open, time[1], time], lookahead=barmerge.lookahead_on)
[hD,  lD,  opD,  ocD,  tpD,  tcD]  = request.security(syminfo.tickerid, "D",   [high[1], low[1], open[1], open, time[1], time], lookahead=barmerge.lookahead_on)
[hW,  lW,  opW,  ocW,  tpW,  tcW]  = request.security(syminfo.tickerid, "W",   [high[1], low[1], open[1], open, time[1], time], lookahead=barmerge.lookahead_on)

// Only timeframes strictly above the chart's; the chart's own timeframe is skipped.
tfAllowed(string tf) => timeframe.in_seconds(tf) > timeframe.in_seconds()

addLevel(array<float> ps, array<string> ns, array<color> cs, array<int> ts, float p, string n, color c, int t) =>
    if not na(p) and not na(t)
        ps.push(p)
        ns.push(n)
        cs.push(c)
        ts.push(t)

// ── Drawing ───────────────────────────────────────────────────────────────
var array<line>  lines  = array.new<line>()
var array<label> labels = array.new<label>()

if barstate.islast
    for l in lines
        l.delete()
    for lb in labels
        lb.delete()
    lines.clear()
    labels.clear()

    bool fromPrev = startIn == "Previous candle open"
    array<float>  prices = array.new<float>()
    array<string> names  = array.new<string>()
    array<color>  colors = array.new<color>()
    array<int>    starts = array.new<int>()

    if show1h and tfAllowed("60")
        addLevel(prices, names, colors, starts, h1h, "1H H", col1h, fromPrev ? tp1h : tc1h)
        addLevel(prices, names, colors, starts, l1h, "1H L", col1h, fromPrev ? tp1h : tc1h)
        if showPrevOpen
            addLevel(prices, names, colors, starts, op1h, "1H O", col1h, tp1h)
        if showCurOpen
            addLevel(prices, names, colors, starts, oc1h, "1H LIVE O", col1h, tc1h)
    if show4h and tfAllowed("240")
        addLevel(prices, names, colors, starts, h4h, "4H H", col4h, fromPrev ? tp4h : tc4h)
        addLevel(prices, names, colors, starts, l4h, "4H L", col4h, fromPrev ? tp4h : tc4h)
        if showPrevOpen
            addLevel(prices, names, colors, starts, op4h, "4H O", col4h, tp4h)
        if showCurOpen
            addLevel(prices, names, colors, starts, oc4h, "4H LIVE O", col4h, tc4h)
    if showD and tfAllowed("D")
        addLevel(prices, names, colors, starts, hD, "PDH", colD, fromPrev ? tpD : tcD)
        addLevel(prices, names, colors, starts, lD, "PDL", colD, fromPrev ? tpD : tcD)
        if showPrevOpen
            addLevel(prices, names, colors, starts, opD, "PDO", colD, tpD)
        if showCurOpen
            addLevel(prices, names, colors, starts, ocD, "D LIVE O", colD, tcD)
    if showW and tfAllowed("W")
        addLevel(prices, names, colors, starts, hW, "PWH", colW, fromPrev ? tpW : tcW)
        addLevel(prices, names, colors, starts, lW, "PWL", colW, fromPrev ? tpW : tcW)
        if showPrevOpen
            addLevel(prices, names, colors, starts, opW, "PWO", colW, tpW)
        if showCurOpen
            addLevel(prices, names, colors, starts, ocW, "W LIVE O", colW, tcW)

    int n = prices.size()
    if n > 0
        int xEnd = time + offsetBars * timeframe.in_seconds() * 1000
        // Levels at the same price share one value label.
        array<float>  labelPrices = array.new<float>()
        array<string> labelNames  = array.new<string>()
        for i = 0 to n - 1
            float p = prices.get(i)
            lines.push(line.new(starts.get(i), p, xEnd, p, xloc=xloc.bar_time, color=colors.get(i), style=lineStyle, width=lineWidth))
            int idx = labelPrices.indexof(p)
            if idx < 0
                labelPrices.push(p)
                labelNames.push(names.get(i))
            else
                labelNames.set(idx, labelNames.get(idx) + " / " + names.get(i))
        if showValues
            for j = 0 to labelPrices.size() - 1
                float p = labelPrices.get(j)
                labels.push(label.new(xEnd, p, labelNames.get(j) + " " + str.tostring(p, format.mintick), xloc=xloc.bar_time,
                     style=label.style_label_left, color=color.new(color.black, 100), textcolor=textColor, size=textSize))
PINE v6

AMR Zones

Marks the AMR zone left after a buy-side or sell-side expansion: the gap between the expansion’s extreme and the first later candle that trades fully beyond it.

  • After a BSE ends, the first candle with its Low above the BSE High frames the zone up to that Low
  • SSE mirrors: the first candle with its High below the SSE Low frames the zone down to that High
  • Trading back beyond the expansion’s opposite extreme first cancels the zone
  • Every completed BSE / SSE on the chart’s own candles can create one zone; only complete candles count
  • Separate BSE and SSE switches, with zone color, fill and border transparency and how many to keep
Platform
TradingView
Timeframe
The chart’s own
  1. Copy the source or download the .pine file.
  2. In TradingView, open the Pine Editor and paste it in.
  3. Click Add to chart, then adjust the settings.
Download .pine

View source
//@version=6
indicator("AMR Zones", shorttitle="AMR Zones", overlay=true, max_boxes_count=500)

// ── Inputs ────────────────────────────────────────────────────────────────
const string G_AMR = "AMR zones"

bool  showBull    = input.bool(true, "Show BSE AMR", group=G_AMR, inline="show")
bool  showBear    = input.bool(true, "Show SSE AMR", group=G_AMR, inline="show")
int   keepBoxes   = input.int(80, "AMR zones kept", minval=1, maxval=499, group=G_AMR)
color amrColor    = input.color(#1848cc, "Color", group=G_AMR)
int   fillTransp  = input.int(90, "Fill transparency", minval=0, maxval=100, group=G_AMR)
int   borderTransp = input.int(30, "Border transparency", minval=0, maxval=100, group=G_AMR)

// ── Expansions ────────────────────────────────────────────────────────────
// BSE: two or more successive BSD candles (Close > Open); from the second
// candle on, Close >= previous High. SSE mirrors. Dojis end a run.
// Every completed expansion may create an AMR.
type AmrPending
    int   dir
    float level
    float invalid
    int   left

type ExpansionRun
    int   dir = 0
    int   count = 0
    int   finalStart = na
    float top = na
    float bottom = na
    float prevHigh = na
    float prevLow = na
    array<AmrPending> amr

method step(ExpansionRun r, int startT, float o, float h, float l, float c) =>
    int d = c > o ? 1 : c < o ? -1 : 0
    bool continues = r.count > 0 and d != 0 and d == r.dir and (d == 1 ? c >= r.prevHigh : c <= r.prevLow)
    if not continues and r.count >= 2
        bool wanted = r.dir == 1 ? showBull : showBear
        if wanted
            r.amr.push(AmrPending.new(r.dir, r.dir == 1 ? r.top : r.bottom, r.dir == 1 ? r.bottom : r.top, r.finalStart))
            if r.amr.size() > 50
                r.amr.shift()
    if continues
        r.count += 1
        r.top := math.max(r.top, h)
        r.bottom := math.min(r.bottom, l)
    else
        r.dir := d
        r.count := d != 0 ? 1 : 0
        r.top := h
        r.bottom := l
    r.finalStart := startT
    r.prevHigh := h
    r.prevLow := l

// ── AMR ───────────────────────────────────────────────────────────────────
// After a BSE finishes, the first later candle whose Low is above the BSE High
// frames the gap between them. A candle trading below the BSE Low cancels the
// wait. SSE mirrors. The candle that ends the expansion can itself form the AMR.
var array<box> amrBoxes = array.new<box>()

method checkAmr(ExpansionRun r, int candleStart, float h, float l) =>
    int i = r.amr.size() - 1
    while i >= 0
        AmrPending p = r.amr.get(i)
        if p.dir == 1 ? l > p.level : h < p.level
            float zoneTop    = p.dir == 1 ? l : p.level
            float zoneBottom = p.dir == 1 ? p.level : h
            amrBoxes.push(box.new(p.left, zoneTop, candleStart, zoneBottom, xloc=xloc.bar_time,
                 bgcolor=color.new(amrColor, fillTransp), border_width=1, border_color=color.new(amrColor, borderTransp)))
            while amrBoxes.size() > keepBoxes
                amrBoxes.shift().delete()
            r.amr.remove(i)
        else if p.dir == 1 ? l < p.invalid : h > p.invalid
            r.amr.remove(i)
        i -= 1

var ExpansionRun run = ExpansionRun.new(amr=array.new<AmrPending>())

if barstate.isconfirmed
    run.step(time, open, high, low, close)
    run.checkAmr(time, high, low)
PINE v6

Timed Ranges

Boxes the High–Low range of each higher-timeframe period on your chart, with its Open, midpoint and range size. The range used depends on the chart’s timeframe:

Timed range by chart timeframe
ChartRange shown
M1 to M14H1 range
M15 to M59H4 range
H1 to H23Daily range
Daily to below WeeklyWeekly range
Weekly and aboveQuarterly range
  • One box per period, drawn from its first candle to its last, updating live as the current period develops
  • Open line (dashed) and midpoint line (dotted), each optional with its own color and style
  • Label with the period and range size, e.g. H4 05:00 · 87.25, Fri 06 Nov · 310.50, W45 2026, Q4 2026
  • Period times in Central Time by default; daily labels use the trading day
  • Box border and fill colors, and how many periods to keep
Platform
TradingView
Timeframe
Any below quarterly; see the table above
  1. Copy the source or download the .pine file.
  2. In TradingView, open the Pine Editor and paste it in.
  3. Click Add to chart, then adjust the settings.
Download .pine

View source
//@version=6
indicator("Timed Ranges", shorttitle="Timed Ranges", overlay=true, max_boxes_count=500, max_lines_count=500, max_labels_count=500)

// ── Inputs ────────────────────────────────────────────────────────────────
const string G_BOX  = "Range box"
const string G_LINE = "Open and midpoint"
const string G_TEXT = "Range size"

int   keepPeriods = input.int(30, "Periods kept", minval=1, maxval=160, group=G_BOX)
color boxBorder   = input.color(#636363, "Border", group=G_BOX, inline="box")
color boxFill     = input.color(color.new(#636363, 90), "Fill", group=G_BOX, inline="box")

bool   showOpen  = input.bool(true, "Open line", group=G_LINE, inline="open")
color  openColor = input.color(#000000, "", group=G_LINE, inline="open")
string openStyleIn = input.string("Dashed", "", options=["Dotted", "Dashed", "Solid"], group=G_LINE, inline="open")
bool   showMid   = input.bool(true, "Midpoint line", group=G_LINE, inline="mid")
color  midColor  = input.color(#000000, "", group=G_LINE, inline="mid")
string midStyleIn = input.string("Dotted", "", options=["Dotted", "Dashed", "Solid"], group=G_LINE, inline="mid")

bool   showSize  = input.bool(true, "Show range size", group=G_TEXT)
color  sizeColor = input.color(#000000, "Text color", group=G_TEXT)
string sizeIn    = input.string("Small", "Text size", options=["Tiny", "Small", "Normal"], group=G_TEXT)
bool   showWhen  = input.bool(true, "Show period time / day", group=G_TEXT)
string labelTz   = input.string("America/Chicago", "Timezone", group=G_TEXT)

styleOf(string s) =>
    switch s
        "Dashed" => line.style_dashed
        "Solid"  => line.style_solid
        => line.style_dotted

string openStyle = styleOf(openStyleIn)
string midStyle  = styleOf(midStyleIn)
string textSize = switch sizeIn
    "Tiny"   => size.tiny
    "Normal" => size.normal
    => size.small

// ── Box timeframe for this chart ──────────────────────────────────────────
// M1–M14 → H1, M15–M59 → H4, H1–H23 → Daily, Daily to below Weekly → Weekly,
// Weekly and above → Quarterly.
int chartSec = timeframe.in_seconds()
string boxTf = chartSec < 900 ? "60" : chartSec < 3600 ? "240" : chartSec < 86400 ? "D" : chartSec < 604800 ? "W" : "3M"
string boxName = switch boxTf
    "60"  => "H1"
    "240" => "H4"
    "D"   => "D"
    "W"   => "W"
    => "Q"
// A box no longer than one chart candle carries no information.
bool active = timeframe.in_seconds(boxTf) > chartSec

// ── Ranges ────────────────────────────────────────────────────────────────
var array<box>   boxes     = array.new<box>()
var array<line>  openLines = array.new<line>()
var array<line>  midLines  = array.new<line>()
var array<label> sizeLabels = array.new<label>()

var box   curBox   = na
var line  curOpen  = na
var line  curMid   = na
var label curLabel = na
var float pHigh    = na
var float pLow     = na
var string pName   = ""

int periodStart = time(boxTf)
int periodClose = time_close(boxTf)

// H1/H4: start time. Daily: trading day, taken from the close because futures
// sessions open on the previous calendar day. Weekly: week of year. Quarterly: Qn year.
periodName() =>
    int last = na(periodClose) ? time : periodClose - 1
    string when = switch boxTf
        "60"  => str.format_time(periodStart, "HH:mm", labelTz)
        "240" => str.format_time(periodStart, "HH:mm", labelTz)
        "D"   => str.format_time(last, "EEE dd MMM", labelTz)
        "W"   => "W" + str.tostring(weekofyear(last, labelTz)) + " " + str.format_time(last, "yyyy", labelTz)
        => "Q" + str.tostring(math.ceil(month(last, labelTz) / 3)) + " " + str.format_time(last, "yyyy", labelTz)
    // Daily, weekly and quarterly text already identifies the period.
    bool selfNamed = boxTf == "D" or boxTf == "W" or boxTf == "3M"
    showWhen ? (selfNamed ? when : boxName + " " + when) : boxName

rangeText(float h, float l) => pName + " · " + str.tostring(h - l, format.mintick)

if active and not na(periodStart)
    // The live period is redrawn on every update until the next one starts.
    if na(curBox) or periodStart != periodStart[1]
        pHigh := high
        pLow  := low
        pName := periodName()
        float mid = (pHigh + pLow) / 2
        curBox := box.new(time, pHigh, time_close, pLow, xloc=xloc.bar_time, border_color=boxBorder, bgcolor=boxFill)
        boxes.push(curBox)
        curOpen := showOpen ? line.new(time, open, time_close, open, xloc=xloc.bar_time, color=openColor, style=openStyle) : na
        openLines.push(curOpen)
        curMid := showMid ? line.new(time, mid, time_close, mid, xloc=xloc.bar_time, color=midColor, style=midStyle) : na
        midLines.push(curMid)
        curLabel := showSize ? label.new(time, pHigh, rangeText(pHigh, pLow), xloc=xloc.bar_time, style=label.style_label_lower_left,
             color=color.new(color.black, 100), textcolor=sizeColor, size=textSize) : na
        sizeLabels.push(curLabel)
        while boxes.size() > keepPeriods
            box.delete(boxes.shift())
            line.delete(openLines.shift())
            line.delete(midLines.shift())
            label.delete(sizeLabels.shift())
    else
        pHigh := math.max(pHigh, high)
        pLow  := math.min(pLow, low)
        float mid = (pHigh + pLow) / 2
        curBox.set_top(pHigh)
        curBox.set_bottom(pLow)
        curBox.set_right(time_close)
        if not na(curOpen)
            curOpen.set_x2(time_close)
        if not na(curMid)
            curMid.set_xy1(curMid.get_x1(), mid)
            curMid.set_xy2(time_close, mid)
        if not na(curLabel)
            curLabel.set_y(pHigh)
            curLabel.set_text(rangeText(pHigh, pLow))
PINE v6

ETR (Efficiently Traded Range)

An Efficiently Traded Range (ETR) is a delivery range that both buy-side and sell-side candle bodies have traded through. This draws an ETR as soon as one forms in a delivery cycle you set by anchor time and length (e.g. 0000 and 60m), using the chart’s own candles.

  • Anchor time (HHMM, Central Time by default) and cycle length in minutes; cycles restart at the anchor each day
  • BSD and SSD candle bodies must both cover the directional range, allowing missing bands up to 1 point
  • Needs a three-bar swing High above the delivery Open and a swing Low below it, each at the delivery’s extreme
  • An ETR must form within its own cycle; the box is drawn on the confirming candle’s close and its High / Low stay fixed
  • Dashed midpoint line and a label such as ETR 07:00 · 27113.25, plus an alert on each new ETR
Platform
TradingView
Timeframe
The chart’s own, below the cycle length
  1. Copy the source or download the .pine file.
  2. In TradingView, open the Pine Editor and paste it in.
  3. Click Add to chart, then adjust the settings.
Download .pine

View source
//@version=6
indicator("ETR", shorttitle="ETR", overlay=true, max_boxes_count=500, max_lines_count=500, max_labels_count=500)

// ── Inputs ────────────────────────────────────────────────────────────────
const string G_CYCLE = "Delivery cycle"
const string G_DRAW  = "Drawing"

string anchorIn  = input.string("0000", "Anchor time (HHMM)", group=G_CYCLE, tooltip="24-hour clock in the timezone below. Cycles restart at this time every day; an unfinished last cycle is cut short.")
int    cycleMin  = input.int(60, "Cycle length (minutes)", minval=1, maxval=1440, group=G_CYCLE, tooltip="Each delivery is built from the current chart timeframe's candles. Use a cycle at least three chart candles long.")
string tz        = input.string("America/Chicago", "Timezone", group=G_CYCLE)

int    keepEtrs  = input.int(50, "ETRs kept", minval=1, maxval=250, group=G_DRAW)
color  boxBorder = input.color(#000000, "Border", group=G_DRAW, inline="box")
color  boxFill   = input.color(color.new(#000000, 90), "Fill", group=G_DRAW, inline="box")
bool   showMid   = input.bool(true, "Midpoint line", group=G_DRAW, inline="mid")
color  midColor  = input.color(#000000, "", group=G_DRAW, inline="mid")
bool   showLabel = input.bool(true, "Label", group=G_DRAW, inline="lbl")
color  lblColor  = input.color(#000000, "", group=G_DRAW, inline="lbl")

int anchorNum = int(str.tonumber(anchorIn))
if str.length(anchorIn) != 4 or na(anchorNum) or anchorNum % 100 > 59 or anchorNum > 2359
    runtime.error("Anchor time must be HHMM, e.g. 0000 or 1648.")
int anchorMin = int(math.floor(anchorNum / 100)) * 60 + anchorNum % 100

// ── Coverage helpers (same logic as the M80 ETR in tbr_imb.pine) ──────────
// Sorted directional body unions. Inclusive endpoints merge touching bands.
etrAddBand(array<float> lows, array<float> highs, float bottom, float top) =>
    float lo = bottom
    float hi = top
    int i = 0
    while i < lows.size() and highs.get(i) < lo
        i += 1
    while i < lows.size() and lows.get(i) <= hi
        lo := math.min(lo, lows.remove(i))
        hi := math.max(hi, highs.remove(i))
    lows.insert(i, lo)
    highs.insert(i, hi)

// A nonzero wick reaching an earlier same-direction body makes that entire
// wick efficient. Equality touches count; only actual prior bodies are references.
etrWickTouchesBody(array<float> lows, array<float> highs, float lo, float hi) =>
    bool touches = false
    if hi > lo and lows.size() > 0
        for i = 0 to lows.size() - 1
            if lows.get(i) > hi
                break
            if highs.get(i) >= lo
                touches := true
                break
    touches

// Two distinct same-direction candles credit only their wick intersection.
// Prior wick unions contain raw wicks, never efficient/body coverage.
etrCreditWickOverlap(array<float> priorLo, array<float> priorHi, float lo, float hi, array<float> upLo, array<float> upHi, array<float> downLo, array<float> downHi) =>
    if hi > lo and priorLo.size() > 0
        for i = 0 to priorLo.size() - 1
            if priorLo.get(i) > hi
                break
            float overlapLo = math.max(lo, priorLo.get(i))
            float overlapHi = math.min(hi, priorHi.get(i))
            if overlapLo <= overlapHi
                etrAddBand(upLo, upHi, overlapLo, overlapHi)
                etrAddBand(downLo, downHi, overlapLo, overlapHi)

// Intersect the two unions, then measure contiguous gaps in BOTH-side coverage.
// A gap remains one band even when the missing direction changes within it.
etrBodiesCovered(array<float> upLo, array<float> upHi, array<float> downLo, array<float> downHi) =>
    bool covered = false
    if upLo.size() > 0 and downLo.size() > 0
        float envelopeLo = math.min(upLo.first(), downLo.first())
        float envelopeHi = math.max(upHi.last(), downHi.last())
        float through = envelopeLo
        float maxGap = 0.0
        int i = 0
        int j = 0
        while i < upLo.size() and j < downLo.size()
            float lo = math.max(upLo.get(i), downLo.get(j))
            float hi = math.min(upHi.get(i), downHi.get(j))
            if lo <= hi
                maxGap := math.max(maxGap, lo - through)
                through := math.max(through, hi)
            if upHi.get(i) < downHi.get(j)
                i += 1
            else
                j += 1
        maxGap := math.max(maxGap, envelopeHi - through)
        covered := envelopeHi > envelopeLo and maxGap <= 1.0 + 1e-9
    covered

// ── Delivery identity ─────────────────────────────────────────────────────
// Key = anchor day (yyyymmdd) and cycle index, using local clock minutes so
// DST changes do not shift the grid. A bar belongs to the cycle holding its open.
int m   = hour(time, tz) * 60 + minute(time, tz)
int rel = (m - anchorMin + 1440) % 1440
int idx = int(math.floor(rel / cycleMin))
// Step back past the anchor (plus a DST-safe margin) to read the anchor day.
int dayT = m >= anchorMin ? time : time - (m + 360) * 60000
int key  = (year(dayT, tz) * 10000 + month(dayT, tz) * 100 + dayofmonth(dayT, tz)) * 10000 + idx

// ── State ─────────────────────────────────────────────────────────────────
type Etr
    int key = na
    int openBar = na
    int openTime = na
    int count = 0
    bool eligible = false
    bool confirmed = false
    int firstHighSwingEnd = na
    int firstLowSwingEnd = na
    bool disjointSwings = false
    float deliveryOpen = na
    float rangeHigh = na
    float rangeLow = na
    float prevHigh = na
    float prevLow = na
    float olderHigh = na
    float olderLow = na
    array<float> upLo
    array<float> upHi
    array<float> downLo
    array<float> downHi
    array<float> bsdBodyLo
    array<float> bsdBodyHi
    array<float> ssdBodyLo
    array<float> ssdBodyHi
    array<float> bsdWickLo
    array<float> bsdWickHi
    array<float> ssdWickLo
    array<float> ssdWickHi

var Etr e = Etr.new(upLo=array.new<float>(), upHi=array.new<float>(), downLo=array.new<float>(), downHi=array.new<float>(), bsdBodyLo=array.new<float>(), bsdBodyHi=array.new<float>(), ssdBodyLo=array.new<float>(), ssdBodyHi=array.new<float>(), bsdWickLo=array.new<float>(), bsdWickHi=array.new<float>(), ssdWickLo=array.new<float>(), ssdWickHi=array.new<float>())

var array<box>   boxes  = array.new<box>()
var array<line>  mids   = array.new<line>()
var array<label> labels = array.new<label>()

bool etrNow = false

if barstate.isconfirmed
    if na(e.key) or key != e.key
        // Never seed a partial delivery from the first chart bar.
        e.eligible := not na(e.key) or rel % cycleMin == 0
        e.key := key
        e.openBar := bar_index
        e.openTime := time
        e.count := 0
        e.confirmed := false
        e.firstHighSwingEnd := na
        e.firstLowSwingEnd := na
        e.disjointSwings := false
        e.deliveryOpen := open
        e.rangeHigh := na
        e.rangeLow := na
        e.prevHigh := na
        e.prevLow := na
        e.olderHigh := na
        e.olderLow := na
        e.upLo.clear()
        e.upHi.clear()
        e.downLo.clear()
        e.downHi.clear()
        e.bsdBodyLo.clear()
        e.bsdBodyHi.clear()
        e.ssdBodyLo.clear()
        e.ssdBodyHi.clear()
        e.bsdWickLo.clear()
        e.bsdWickHi.clear()
        e.ssdWickLo.clear()
        e.ssdWickHi.clear()

    if e.eligible and not e.confirmed
        float o = open
        float h = high
        float l = low
        float c = close
        // The middle candle must be a delivery extreme when its third closes.
        // Equal neighbours/prior extremes qualify; equality with Open does not.
        if e.count >= 2
            bool highSwing = e.prevHigh > e.deliveryOpen and e.prevHigh >= e.olderHigh and e.prevHigh >= h and e.prevHigh >= e.rangeHigh
            bool lowSwing = e.prevLow < e.deliveryOpen and e.prevLow <= e.olderLow and e.prevLow <= l and e.prevLow <= e.rangeLow
            // All three candles belong to their swing. The later pattern must
            // start strictly after the earlier pattern's third candle.
            int patternStart = bar_index - 2
            if highSwing
                if not na(e.firstLowSwingEnd) and patternStart > e.firstLowSwingEnd
                    e.disjointSwings := true
                if na(e.firstHighSwingEnd)
                    e.firstHighSwingEnd := bar_index
            if lowSwing
                if not na(e.firstHighSwingEnd) and patternStart > e.firstHighSwingEnd
                    e.disjointSwings := true
                if na(e.firstLowSwingEnd)
                    e.firstLowSwingEnd := bar_index
        e.rangeHigh := na(e.rangeHigh) ? h : math.max(e.rangeHigh, h)
        e.rangeLow := na(e.rangeLow) ? l : math.min(e.rangeLow, l)
        if c != o
            float bodyLo = math.min(o, c)
            float bodyHi = math.max(o, c)
            array<float> priorLo = c > o ? e.bsdBodyLo : e.ssdBodyLo
            array<float> priorHi = c > o ? e.bsdBodyHi : e.ssdBodyHi
            array<float> priorWickLo = c > o ? e.bsdWickLo : e.ssdWickLo
            array<float> priorWickHi = c > o ? e.bsdWickHi : e.ssdWickHi
            // Credit intersections before storing either current wick, so a
            // candle cannot pair with itself. Dojis never enter this block.
            etrCreditWickOverlap(priorWickLo, priorWickHi, l, bodyLo, e.upLo, e.upHi, e.downLo, e.downHi)
            etrCreditWickOverlap(priorWickLo, priorWickHi, bodyHi, h, e.upLo, e.upHi, e.downLo, e.downHi)
            if l < bodyLo
                etrAddBand(priorWickLo, priorWickHi, l, bodyLo)
            if h > bodyHi
                etrAddBand(priorWickLo, priorWickHi, bodyHi, h)
            // Evaluate both wicks before adding this candle's own body.
            if etrWickTouchesBody(priorLo, priorHi, l, bodyLo)
                etrAddBand(e.upLo, e.upHi, l, bodyLo)
                etrAddBand(e.downLo, e.downHi, l, bodyLo)
            if etrWickTouchesBody(priorLo, priorHi, bodyHi, h)
                etrAddBand(e.upLo, e.upHi, bodyHi, h)
                etrAddBand(e.downLo, e.downHi, bodyHi, h)
            if c > o
                etrAddBand(e.upLo, e.upHi, o, c)
            else
                etrAddBand(e.downLo, e.downHi, c, o)
            etrAddBand(priorLo, priorHi, bodyLo, bodyHi)
        e.count += 1
        if e.disjointSwings and e.bsdBodyLo.size() > 0 and e.ssdBodyLo.size() > 0 and etrBodiesCovered(e.upLo, e.upHi, e.downLo, e.downHi)
            e.confirmed := true
            etrNow := true
            // Drawn on the confirming candle's close; the range is frozen there.
            boxes.push(box.new(e.openBar, e.rangeHigh, bar_index, e.rangeLow, border_color=boxBorder, bgcolor=boxFill))
            float midpoint = math.round_to_mintick((e.rangeHigh + e.rangeLow) / 2)
            mids.push(showMid ? line.new(e.openBar, midpoint, bar_index, midpoint, color=midColor, style=line.style_dashed, width=2) : na)
            string txt = "ETR " + str.format_time(e.openTime, "HH:mm", tz) + " · " + str.tostring(midpoint, format.mintick)
            labels.push(showLabel ? label.new(e.openBar, e.rangeHigh, txt, style=label.style_label_lower_left, color=color(na), textcolor=lblColor, size=size.small) : na)
            if boxes.size() > keepEtrs
                boxes.shift().delete()
                line oldMid = mids.shift()
                if not na(oldMid)
                    oldMid.delete()
                label oldLbl = labels.shift()
                if not na(oldLbl)
                    oldLbl.delete()
        e.olderHigh := e.prevHigh
        e.olderLow := e.prevLow
        e.prevHigh := h
        e.prevLow := l

alertcondition(etrNow, "ETR confirmed", "ETR confirmed in the current delivery cycle.")
SOON

More on the way

Further indicators from the study material will be added to this page as they are published.