Examples
Higher Timeframes
Examples using higher-timeframe subscriptions — HTF candles, overlays, and timeframe.change detection.
Legacy Scripting v2
These docs remain available while the v2 engine is supported. For new scripts, use Scripting v3.
HTF Rollup Candles
Draws rolling higher-timeframe candles on the price chart using Box and Line entities. Each HTF bar gets a wick line and body box, colored by direction. The display name updates live with OHLC and percentage change.

//@version=2
indicator("Rolling HTF Candles", true);
// Timeframes tab
input.tab("Timeframes", { key: "tab_timeframes" });
input.group("HTF Windows", {
key: "grp_timeframes",
collapsible: true,
collapsed: false
});
const tf1 = input.timeframe("Timeframe 1", "1h", { key: "tf1" });
const tf2 = input.timeframe("Timeframe 2", "4h", { key: "tf2" });
const tf3 = input.timeframe("Timeframe 3", "1D", { key: "tf3" });
// Layout tab
input.tab("Layout", { key: "tab_layout" });
input.group("Projection Layout", {
key: "grp_layout",
collapsible: true,
collapsed: false
});
const startBarsAhead = input.int("Start Bars Ahead", 12, {
key: "startBarsAhead",
min: 1,
max: 300
});
const candleWidthBars = input.int("Candle Width Bars", 6, {
key: "candleWidthBars",
min: 1,
max: 50
});
const gapBars = input.int("Gap Between Candles", 3, {
key: "gapBars",
min: 1,
max: 100
});
const wickWidth = input.int("Wick Width", 2, {
key: "wickWidth",
min: 1,
max: 10
});
const bodyRounding = input.int("Body Rounding", 2, {
key: "bodyRounding",
min: 0,
max: 12
});
const labelSize = input.int("Timeframe Label Size", 24, {
key: "labelSize",
min: 8,
max: 48
});
const hlLabelSize = input.int("H/L Label Size", 16, {
key: "hlLabelSize",
min: 8,
max: 32
});
const labelGapPct = input.float("Main Label Gap %", 0.5, {
key: "labelGapPct",
min: 0.01,
max: 20
});
const hlExtraGapPct = input.float("H/L Extra Gap %", 0.35, {
key: "hlExtraGapPct",
min: 0.01,
max: 20
});
// Style tab
input.tab("Style", { key: "tab_style" });
input.group("Colors", {
key: "grp_colors",
collapsible: true,
collapsed: false
});
const upColor = input.color("Up", "#18BD39FF", {
key: "upColor"
});
const downColor = input.color("Down", "#FB1E3CFF", {
key: "downColor",
sameLine: true
});
const textColor = input.color("Text", color.white, {
key: "textColor"
});
// Checks tab
input.tab("Checks", { key: "tab_checks" });
input.group("Checks", {
key: "grp_checks",
collapsible: true,
collapsed: false
});
const showHlCheckLabels = input.bool("Show H/L Check Labels", false, {
key: "showHlCheckLabels"
});
const chart = subscribe(data.OHLCV);
const wick1 = Line("roll_wick_1");
const wick2 = Line("roll_wick_2");
const wick3 = Line("roll_wick_3");
const body1 = Box("roll_body_1");
const body2 = Box("roll_body_2");
const body3 = Box("roll_body_3");
const topLabel1 = Label("roll_top_label_1", { text: "" });
const topLabel2 = Label("roll_top_label_2", { text: "" });
const topLabel3 = Label("roll_top_label_3", { text: "" });
const highCheckLabel1 = Label("roll_high_check_label_1", { text: "" });
const highCheckLabel2 = Label("roll_high_check_label_2", { text: "" });
const highCheckLabel3 = Label("roll_high_check_label_3", { text: "" });
const lowCheckLabel1 = Label("roll_low_check_label_1", { text: "" });
const lowCheckLabel2 = Label("roll_low_check_label_2", { text: "" });
const lowCheckLabel3 = Label("roll_low_check_label_3", { text: "" });
function priceText(value) {
return str.toString(value, "mintick");
}
function signedPercentText(value) {
if (!math.isFinite(value)) return "na";
const text = str.toString(value, "percent");
return value > 0 ? str.format("+{0}", text) : text;
}
function candleChange(candle) {
if (!candle) return NaN;
if (!math.isFinite(candle.open) || !math.isFinite(candle.close)) return NaN;
if (candle.open === 0) return NaN;
return candle.close / candle.open - 1;
}
function candleSummary(tfSeconds, candle) {
const tfText = timeframe.fromSeconds(tfSeconds);
const pctText = signedPercentText(candleChange(candle));
return str.format("[{0}: {1}]", tfText, pctText);
}
function rollingCandle(src, windowSeconds) {
if (!math.isFinite(windowSeconds) || windowSeconds <= 0) return null;
if (windowSeconds < context.timeframe) return null;
const latestUnix = src.unix();
if (!math.isFinite(latestUnix)) return null;
const cutoffUnix = latestUnix - windowSeconds + context.timeframe;
let firstOffset = -1;
let high = -Infinity;
let low = Infinity;
for (let i = 0; i < barCount(); i++) {
const ts = src.unix(i);
if (!math.isFinite(ts)) break;
if (ts < cutoffUnix) break;
const hi = src.high(i);
const lo = src.low(i);
if (!(math.isFinite(hi) && math.isFinite(lo))) return null;
if (hi > high) high = hi;
if (lo < low) low = lo;
firstOffset = i;
}
if (firstOffset < 0) return null;
const open = src.open(firstOffset);
const close = src.close();
if (!(math.isFinite(open) && math.isFinite(close))) return null;
return {
open: open,
high: high,
low: low,
close: close
};
}
function drawProjectedCandle(
candle,
tfSeconds,
slot,
anchorUnix,
wick,
body,
topLabel,
highCheckLabel,
lowCheckLabel
) {
if (!candle) return;
const o = candle.open;
const h = candle.high;
const l = candle.low;
const c = candle.close;
const strideBars = candleWidthBars + gapBars;
const leftX =
anchorUnix + (startBarsAhead + slot * strideBars) * context.timeframe;
const rightX = leftX + candleWidthBars * context.timeframe;
const midX = leftX + (rightX - leftX) / 2;
let top = math.max(o, c);
let bottom = math.min(o, c);
if (top === bottom) {
const dojiPad = math.max(math.abs(c) * 0.0001, 0.00000001);
top += dojiPad;
bottom -= dojiPad;
}
const candleColor = c >= o ? upColor : downColor;
const baseTopGap = math.max(math.abs(h) * (labelGapPct / 100), 0.00000001);
const extraTopGap = math.max(math.abs(h) * (hlExtraGapPct / 100), 0.00000001);
const lowGap = math.max(math.abs(l) * (labelGapPct / 100), 0.00000001);
wick.set({
x1: midX,
y1: h,
x2: midX,
y2: l,
width: wickWidth,
color: candleColor
});
body.set({
x1: leftX,
y1: top,
x2: rightX,
y2: bottom,
color: candleColor,
rounding: bodyRounding
});
topLabel.set({
x: midX,
y: h + baseTopGap,
text: timeframe.fromSeconds(tfSeconds),
color: textColor,
size: labelSize,
textAlign: text.align.center
});
highCheckLabel.set({
x: midX,
y: showHlCheckLabels ? h + baseTopGap + extraTopGap : h,
text: showHlCheckLabels ? str.format("H {0}", priceText(h)) : "",
color: textColor,
size: hlLabelSize,
textAlign: text.align.center
});
lowCheckLabel.set({
x: midX,
y: showHlCheckLabels ? l - lowGap : l,
text: showHlCheckLabels ? str.format("L {0}", priceText(l)) : "",
color: textColor,
size: hlLabelSize,
textAlign: text.align.center
});
}
function onBar(index) {
if (!context.isLast) return;
const anchorUnix = chart.unix();
const c1 = rollingCandle(chart, tf1);
const c2 = rollingCandle(chart, tf2);
const c3 = rollingCandle(chart, tf3);
setDisplayName(
str.format(
"Rolling HTF Candles {0} {1} {2}",
candleSummary(tf1, c1),
candleSummary(tf2, c2),
candleSummary(tf3, c3)
)
);
drawProjectedCandle(
c1,
tf1,
0,
anchorUnix,
wick1,
body1,
topLabel1,
highCheckLabel1,
lowCheckLabel1
);
drawProjectedCandle(
c2,
tf2,
1,
anchorUnix,
wick2,
body2,
topLabel2,
highCheckLabel2,
lowCheckLabel2
);
drawProjectedCandle(
c3,
tf3,
2,
anchorUnix,
wick3,
body3,
topLabel3,
highCheckLabel3,
lowCheckLabel3
);
}HTF Candles using Boxes and Lines
Draws higher-timeframe candles on the price chart using Box and Line entities. Each HTF bar gets a wick line and body box, colored by direction. The display name updates live with OHLC and percentage change.

//@version=2
indicator("HTF Candles", false);
// Inputs
const tf = input.timeframe("Timeframe", "30m", { key: "tf" });
const upColor = input.color("Up", "#16a34a");
const downColor = input.color("Down", "#dc2626", { sameLine: true });
const htf = subscribe(data.OHLCV, { timeframe: tf });
const wicks = {};
const bodies = {};
function ensureLine(id) {
let h = wicks[id];
if (!h) {
h = Line("wick-" + id);
wicks[id] = h;
}
return h;
}
function ensureBox(id) {
let h = bodies[id];
if (!h) {
h = Box("body-" + id);
bodies[id] = h;
}
return h;
}
function candleColors(open, close) {
const up = close >= open;
return {
wick: up ? upColor : downColor,
body: up ? upColor : downColor
};
}
function signedPercentText(value) {
if (!math.isFinite(value)) return "na";
const text = str.toString(value, "percent");
return value > 0 ? str.format("+{0}", text) : text;
}
function candleChange(open, close) {
if (!math.isFinite(open) || !math.isFinite(close) || open === 0) return NaN;
return close / open - 1;
}
function drawCandle(id, start, end, open, high, low, close) {
if (na(start) || na(end) || na(open) || na(high) || na(low) || na(close))
return;
const span = end - start;
if (span <= 0) return;
const mid = start + span * 0.5;
const pad = span * 0.18;
const top = math.max(open, close);
const bottom = math.min(open, close);
const c = candleColors(open, close);
ensureLine(id).set({
x1: mid,
y1: low,
x2: mid,
y2: high,
color: c.wick,
width: 2
});
ensureBox(id).set({
x1: start + pad,
y1: top,
x2: end - pad,
y2: bottom,
color: c.body
});
}
function onBar() {
const start = htf.unix();
const open = htf.open();
const high = htf.high();
const low = htf.low();
const close = htf.close();
if (na(start) || na(open) || na(high) || na(low) || na(close)) return;
const tfText = timeframe.fromSeconds(tf);
const change = candleChange(open, close);
const dirText = close >= open ? "UP" : "DOWN";
setDisplayName(
str.format(
"HTF Candles [{0}] [{1}: {2}] [O {3} H {4} L {5} C {6}]",
tfText,
dirText,
signedPercentText(change),
str.toString(open, "mintick"),
str.toString(high, "mintick"),
str.toString(low, "mintick"),
str.toString(close, "mintick")
)
);
// Draw/update current provisional HTF candle
drawCandle("" + start, start, start + tf, open, high, low, close);
// When a new HTF period begins, finalize the previous one using exact boundaries
if (timeframe.change(tf)) {
const prevStart = htf.unix(1);
if (!na(prevStart) && start > prevStart) {
drawCandle(
"" + prevStart,
prevStart,
start,
htf.open(1),
htf.high(1),
htf.low(1),
htf.close(1)
);
}
}
}