CoreWave Studio · Pine Script Tutorial

Building a Dual-Timeframe
OHLC Indicator

A step-by-step Pine Script v6 guide — from blank script to working indicator

Language Pine Script v6
Platform TradingView
Level Beginner — Intermediate
Author Chetan Keni
Introduction

What We Are Building

This tutorial walks through the construction of a dual-timeframe OHLC indicator in Pine Script v6. The finished indicator automatically fetches the previous completed bar for two user-selected timeframes and renders their OHLC structure as colour-coded zones directly on the chart — with no manual drawing required.

The indicator draws three zones per timeframe. The outer zone covers the full range from High to Low. The mid zone sits between Open and Close — colour-coded green when the previous bar was bullish, red when bearish. This gives you an instant read on trend bias and the range that price established in the prior period.

High → Open/Close
Outer upper zone
Open ↔ Close
Mid zone — bullish
Open ↔ Close
Mid zone — bearish
Open/Close → Low
Outer lower zone

Two timeframes are shown simultaneously — an outer higher timeframe and an inner lower timeframe. This is the structural foundation of Fibonacci Dimension, stripped back to its core concept so the programming logic is easy to follow and understand.

What you will learn — By the end of this tutorial you will understand: how Pine Script executes on every bar, how to fetch data from other timeframes using request.security(), how to anchor zones to calendar-accurate boundaries using bar_index, how to draw and manage boxes and lines, and the clear-and-redraw pattern that keeps your chart responsive.

Before We Start

Key Concepts

Pine Script has a specific execution model that is different from most programming languages. Understanding these three ideas before you write any code will make everything that follows much clearer.

Concept 01
Streaming Execution
Pine Script runs your entire script once for every bar on the chart — from the oldest bar to the most recent. On a daily chart with 500 bars, your script runs 500 times. This is called streaming execution. Every variable you declare is recalculated on each bar unless you use the var keyword to persist it.
Concept 02
barstate.islast
Because the script runs on every bar, you do not want to draw chart objects 500 times. The special variable barstate.islast is true only on the final bar — the most recent candle. All drawing logic is placed inside an if barstate.islast block so objects are created only once, on the current bar.
Concept 03
var Keyword
Normally a variable resets to its initial value on every bar. Adding var before a declaration tells Pine Script to initialise the variable only once and retain its value across all subsequent bars. This is essential for tracking state — like which bar the Weekly timeframe opened on.
Step 1 of 8

Script Declaration

Every Pine Script begins with two mandatory lines — the version declaration and the indicator declaration. These tell TradingView which version of the language you are using and how the indicator should behave on the chart.

// Version declaration — always the first line //@version=6 // Indicator declaration indicator( title = "Dual TF OHLC [CoreWave Studio]", shorttitle = "DualOHLC", overlay = true, max_boxes_count = 500, max_lines_count = 500, max_labels_count = 500 )

What each parameter does

ParameterValuePurpose
title"Dual TF OHLC [CoreWave Studio]"Full name shown in the Indicators menu
shorttitle"DualOHLC"Compact name shown on the chart
overlaytrueDraws on the price chart — not in a separate pane below
max_boxes_count500Maximum number of box objects allowed simultaneously
max_lines_count500Maximum number of line objects allowed simultaneously
max_labels_count500Maximum number of label objects allowed simultaneously

Why set max counts? Pine Script limits how many drawing objects can exist on a chart simultaneously. Setting these to 500 gives the indicator enough room to draw zones, level lines, and labels for both timeframes across multiple previous periods if needed. You will hit a compilation error if you draw more objects than the declared maximum.

Step 2 of 8

Defining Inputs

Inputs are the settings panel — the controls a trader sees when they click the indicator's settings icon on the chart. Pine Script provides several input types. We use four of them here, kept minimal so the focus stays on the programming concepts rather than configuration complexity.

// ── INPUT GROUPS ────────────────────────────────────────────── var string GRP_TF = "Timeframe Settings" var string GRP_ZONE = "Zone Display" // ── TIMEFRAME INPUTS ─────────────────────────────────────────── // input.timeframe() shows a dropdown of all TradingView timeframes tf1 = input.timeframe("W", title="#1 - Higher Timeframe", group=GRP_TF) tf2 = input.timeframe("D", title="#2 - Lower Timeframe", group=GRP_TF) // input.bool() shows a checkbox showTF2 = input.bool(true, title="Show #2 Dimension?", group=GRP_TF) // ── ZONE DISPLAY INPUTS ──────────────────────────────────────── // input.int() shows a number field with min/max validation zoneExtBars1 = input.int(45, title="#1 - Zone extends ahead (bars)", minval=1, maxval=200, group=GRP_ZONE) zoneExtBars2 = input.int(20, title="#2 - Zone extends ahead (bars)", minval=1, maxval=200, group=GRP_ZONE)

The four input types used

FunctionReturnsUI Element
input.timeframe()stringTimeframe dropdown — "W", "D", "60", "15" etc.
input.bool()boolCheckbox — true or false
input.int()intInteger number field with optional min/max
input.color()colorColour picker — not used here but used in Fibonacci Dimension
Pine Script Concept
Input Groups

The group parameter organises inputs into collapsible sections in the settings panel. Using var string GRP_TF = "Timeframe Settings" defines the group name once and reuses it — so if you rename the group you only change it in one place. The var keyword ensures it is defined only once, not on every bar.

Step 3 of 8

Fetching Multi-Timeframe Data

This is the most important function in any multi-timeframe indicator. request.security() fetches data from a different timeframe or symbol than the one currently displayed on the chart. Without it, you can only read data from the current chart timeframe.

// ── FETCH PREVIOUS COMPLETED BAR OHLC ───────────────────────── // [1] means "one bar back" — the previous completed bar // We never use the current in-progress bar as it has not closed yet [o1, h1, l1, c1] = request.security( syminfo.tickerid, // same symbol as current chart tf1, // the timeframe to fetch from [open[1], high[1], low[1], close[1]], // what to fetch lookahead=barmerge.lookahead_on // see note below ) [o2, h2, l2, c2] = request.security( syminfo.tickerid, tf2, [open[1], high[1], low[1], close[1]], lookahead=barmerge.lookahead_on )
Pine Script Concept
The [1] History Operator

In Pine Script, square brackets after a series name access historical values. close[0] or just close is the current bar's close. close[1] is the previous bar's close. close[2] is two bars back. By passing open[1], high[1], low[1], close[1] to request.security() we always receive the previous completed bar — not the current in-progress one, which has not yet closed.

Pine Script Concept
lookahead — Why It Matters

barmerge.lookahead_on tells Pine Script that when the higher timeframe bar has just closed and a new one has not started yet, it is allowed to look ahead to use the freshly closed bar's data. Without this, the indicator would show the previous bar's data one bar late. For an indicator showing the previous completed bar this setting ensures accuracy at timeframe boundaries.

Important — avoid repainting. Always use [1] when fetching OHLC data for display. Using open[0] (the current bar's open) with lookahead_on would cause repainting — the indicator would show different values on historical bars than it showed when those bars were live. Using [1] prevents this entirely.

Step 4 of 8

Core Calculations

With OHLC data fetched for both timeframes, we now derive the values we need for drawing. Two calculations are required — the trend bias (was the previous bar bullish or bearish?) and the mid zone boundaries (where does the open-to-close body sit?).

// ── TREND BIAS ──────────────────────────────────────────────── // A bar is bullish if close >= open, bearish if close < open // This determines the mid zone colour bull1 = c1 >= o1 // true = bullish, false = bearish bull2 = c2 >= o2 // ── MID ZONE BOUNDARIES ─────────────────────────────────────── // The mid zone sits between open and close // math.max/min handles both bullish and bearish bars correctly // Bullish bar: open < close → math.max gives close, math.min gives open // Bearish bar: open > close → math.max gives open, math.min gives close midTop1 = math.max(o1, c1) // higher of open and close midBot1 = math.min(o1, c1) // lower of open and close midTop2 = math.max(o2, c2) midBot2 = math.min(o2, c2)
Pine Script Concept
math.max() and math.min()

math.max(a, b) returns the larger of two values. math.min(a, b) returns the smaller. Using these for the mid zone means we do not need to write separate logic for bullish and bearish bars — the same two lines work in both cases. For a bullish bar where close > open, math.max(open, close) correctly returns close as the top of the body. For a bearish bar it correctly returns open.

Zone structure summary

ZoneTopBottomColour
Outer upperh1 (High)midTop1Neutral fill
Mid zonemidTop1midBot1Green bullish / Red bearish
Outer lowermidBot1l1 (Low)Neutral fill
Step 5 of 8

Calendar-Accurate Left Edge

This is the most technically interesting part of the build. We need each zone to start at the true beginning of its timeframe bar — Monday for Weekly, the first session of the month for Monthly. If we simply use the current bar_index, both zones would start at the same point regardless of timeframe. We need to track exactly when each timeframe bar opened.

// ── DETECT WHEN EACH TIMEFRAME BAR CHANGES ──────────────────── // Fetch the open time of the PREVIOUS completed TF bar // When this value changes, a new TF bar has just opened tf1PrevBarTime = request.security(syminfo.tickerid, tf1, time[1], lookahead=barmerge.lookahead_on) tf2PrevBarTime = request.security(syminfo.tickerid, tf2, time[1], lookahead=barmerge.lookahead_on) // ta.change() returns the difference between current and previous value // If the result is not zero, the value changed — a new TF bar opened tf1Changed = ta.change(tf1PrevBarTime) != 0 tf2Changed = ta.change(tf2PrevBarTime) != 0 // ── RECORD BAR_INDEX WHEN EACH TF BAR OPENS ─────────────────── // var persists the value across bars — it does not reset each bar // When tf1Changed fires, we capture the current bar_index // This becomes the left edge of the zone var int tf1StartBarIdx = 0 var int tf2StartBarIdx = 0 if tf1Changed tf1StartBarIdx := bar_index // := is the reassignment operator if tf2Changed tf2StartBarIdx := bar_index
Pine Script Concept
ta.change() — Detecting Value Changes

ta.change(source) returns the difference between the current value of source and its value on the previous bar. If the Weekly bar has not changed, ta.change(tf1PrevBarTime) returns zero. The moment Monday arrives and a new Weekly bar opens, the timestamp changes and ta.change() returns a non-zero value. We compare with != 0 to get a boolean true/false signal.

Pine Script Concept
bar_index — Position on the Chart

bar_index is an integer that represents the position of the current bar on the chart. The first (oldest) bar has index 0. Each subsequent bar increments by 1. The most recent bar has the highest index. By recording bar_index at the moment a new timeframe bar opens, we have the exact chart position to use as the left edge of our zone — regardless of what timeframe the chart is displaying.

Pine Script Concept
:= vs = — Assignment in Pine Script

In Pine Script, = is used for the initial declaration of a variable. := is used to reassign a variable that has already been declared. Since tf1StartBarIdx was declared with var int tf1StartBarIdx = 0, all subsequent assignments inside if blocks use :=. Using = inside an if block would create a new local variable that disappears after the block — not what we want.

Step 6 of 8

Drawing Zones

Zones are drawn using box.new() — Pine Script's rectangle drawing function. All drawing takes place inside if barstate.islast so objects are created only on the final bar, not on every bar the script processes.

// ── PERSISTENT STORAGE ──────────────────────────────────────── // Arrays hold references to drawn objects so we can delete them // when the chart updates and we need to redraw // var ensures these arrays persist across all bars var box[] tf1Boxes = array.new_box() var box[] tf2Boxes = array.new_box() // ── ZONE DRAWING HELPER ─────────────────────────────────────── // A function that draws one box zone and stores it in the array // top > bot is validated — no box is drawn if levels are equal drawZone(float top, float bot, color col, int leftT, int rightT, box[] arr) => if top > bot b = box.new( left = leftT, top = top, right = rightT, bottom = bot, border_color = color.new(col, 100), // fully transparent border bgcolor = color.new(col, 85), // 85% transparent — subtle fill xloc = xloc.bar_index // positions by bar number ) array.push(arr, b) // store reference for later deletion // ── DRAW ALL ZONES INSIDE barstate.islast ───────────────────── if barstate.islast // Calculate zone boundaries maxLookback = bar_index - 490 t1L = math.max(tf1StartBarIdx, maxLookback) // clamped left edge t1R = bar_index + zoneExtBars1 // right edge — N bars ahead t2L = math.max(tf2StartBarIdx, maxLookback) t2R = bar_index + zoneExtBars2 // Timeframe 1 zones // Zone colours — 85% transparent fill matching Fibonacci Dimension defaults midCol1 = bull1 ? color.new(#262C1F, 0) : color.new(#36312C, 0) // muted green / red outerCol1 = color.new(#32323A, 0) drawZone(h1, midTop1, outerCol1, t1L, t1R, tf1Boxes) // upper outer drawZone(midTop1, midBot1, midCol1, t1L, t1R, tf1Boxes) // mid zone drawZone(midBot1, l1, outerCol1, t1L, t1R, tf1Boxes) // lower outer // Timeframe 2 zones — only if enabled if showTF2 midCol2 = bull2 ? color.new(#444B36, 0) : color.new(#4E463F, 0) // muted teal / brown outerCol2 = color.new(#323C3D, 0) drawZone(h2, midTop2, outerCol2, t2L, t2R, tf2Boxes) drawZone(midTop2, midBot2, midCol2, t2L, t2R, tf2Boxes) drawZone(midBot2, l2, outerCol2, t2L, t2R, tf2Boxes)

The 490-bar clamp. Pine Script objects drawn with xloc.bar_index cannot reference bar indices more than 500 bars back. On a low chart timeframe with a high TF selected (e.g. H1 chart with Monthly timeframe), the Monthly bar may have opened more than 490 bars ago. We clamp the left edge to prevent an error: t1L = math.max(tf1StartBarIdx, bar_index - 490)

Pine Script Concept
xloc.bar_index vs xloc.bar_time

Pine Script offers two coordinate systems for drawing objects. xloc.bar_time positions objects using timestamps. xloc.bar_index positions objects using bar numbers. The critical difference: time-based zones change visual width when you zoom in or out because the candle density changes. Bar-index zones always extend exactly N candles wide regardless of zoom level. For an indicator that should look correct at any zoom, xloc.bar_index is always the right choice.

Pine Script Concept
User-Defined Functions

drawZone() above is a user-defined function — a reusable block of code you write once and call multiple times. Functions in Pine Script are defined using the syntax functionName(param1, param2) => followed by an indented block. The function eliminates the need to repeat the same box.new() code six times (three zones × two timeframes). If you need to change how zones are drawn, you change it in one place.

Step 7 of 8

Drawing Level Lines

In addition to the filled zones, we draw horizontal lines at the four OHLC levels and optionally attach labels. Lines use line.new() and labels use label.new() — both follow the same pattern as boxes.

// ── PERSISTENT STORAGE FOR LINES AND LABELS ─────────────────── var line[] tf1Lines = array.new_line() var label[] tf1Labels = array.new_label() var line[] tf2Lines = array.new_line() var label[] tf2Labels = array.new_label() // ── LINE DRAWING HELPER ─────────────────────────────────────── drawLevel(float price, string labelTxt, color col, int leftT, int rightT, line[] linesArr, label[] labelsArr) => ln = line.new( x1 = leftT, y1 = price, x2 = rightT, y2 = price, color = col, width = 1, style = line.style_solid, extend = extend.none, xloc = xloc.bar_index ) array.push(linesArr, ln) // Label sits at the right edge of the line lbl = label.new( x = rightT, y = price, text = labelTxt, color = color.new(color.black, 100), // transparent background textcolor = color.new(#A4A4A4, 0), style = label.style_label_right, size = size.small, xloc = xloc.bar_index ) array.push(labelsArr, lbl) // ── DRAW LINES INSIDE barstate.islast ───────────────────────── // Called after the zone drawing code shown in Step 6 // Uses the same t1L, t1R, t2L, t2R variables lineCol1 = color.new(#1E90FF, 0) // Dodger Blue for TF1 lineCol2 = color.new(#26C6DA, 0) // Cyan for TF2 drawLevel(h1, tf1 + " Hi", lineCol1, t1L, t1R, tf1Lines, tf1Labels) drawLevel(midTop1, tf1 + " Cl", lineCol1, t1L, t1R, tf1Lines, tf1Labels) drawLevel(midBot1, tf1 + " Op", lineCol1, t1L, t1R, tf1Lines, tf1Labels) drawLevel(l1, tf1 + " Lo", lineCol1, t1L, t1R, tf1Lines, tf1Labels) if showTF2 drawLevel(h2, tf2 + " Hi", lineCol2, t2L, t2R, tf2Lines, tf2Labels) drawLevel(midTop2, tf2 + " Cl", lineCol2, t2L, t2R, tf2Lines, tf2Labels) drawLevel(midBot2, tf2 + " Op", lineCol2, t2L, t2R, tf2Lines, tf2Labels) drawLevel(l2, tf2 + " Lo", lineCol2, t2L, t2R, tf2Lines, tf2Labels)
Pine Script Concept
label.style_label_right

The style parameter of label.new() controls where the label anchor point sits relative to the text. label.style_label_right places the anchor on the left side of the text, so the label appears to the right of the anchor point — effectively floating the text to the right of the line's right edge. This keeps labels readable without overlapping the zones.

Step 8 of 8

The Clear and Redraw Pattern

Every time the chart updates — on a new bar, on a tick, or when the user changes a setting — the if barstate.islast block runs again. Without clearing old objects first, the indicator would accumulate hundreds of duplicate boxes and lines on the chart. The clear-and-redraw pattern deletes all existing objects before creating new ones.

// ── CLEAR HELPERS ───────────────────────────────────────────── // Loops through each array, deletes every object, then empties the array clearBoxes(box[] arr) => if array.size(arr) > 0 for i = 0 to array.size(arr) - 1 box.delete(array.get(arr, i)) array.clear(arr) clearLines(line[] arr) => if array.size(arr) > 0 for i = 0 to array.size(arr) - 1 line.delete(array.get(arr, i)) array.clear(arr) clearLabels(label[] arr) => if array.size(arr) > 0 for i = 0 to array.size(arr) - 1 label.delete(array.get(arr, i)) array.clear(arr) // ── CALL CLEAR FUNCTIONS FIRST INSIDE barstate.islast ───────── // These must be called BEFORE any new drawing takes place if barstate.islast // Clear all existing objects clearBoxes(tf1Boxes) clearBoxes(tf2Boxes) clearLines(tf1Lines) clearLines(tf2Lines) clearLabels(tf1Labels) clearLabels(tf2Labels) // ... then proceed with all drawing code from Steps 6 and 7
Pine Script Concept
for Loops and Arrays

Pine Script arrays use zero-based indexing — the first element is at index 0, the last is at array.size(arr) - 1. The for i = 0 to array.size(arr) - 1 loop iterates through every element. array.get(arr, i) retrieves the element at position i. box.delete(), line.delete(), and label.delete() remove the object from the chart. array.clear() empties the array itself so it is ready to accept new references.

Why this pattern is correct. The sequence is always: clear first, then draw. Clearing removes stale objects from the previous update. Drawing creates fresh objects based on the current data. The arrays then hold references to the new objects, ready to be cleared on the next update. This cycle keeps the chart clean with exactly the right objects at all times.

Complete Script

Full Annotated Code

Below is the complete indicator with all steps assembled in the correct order. Every section is annotated. Copy this into the Pine Script editor on TradingView, click Add to chart, and the indicator will be live on your chart immediately.

Complete Script

New to the Pine Script editor? The official TradingView guide covers how to open it and add a script to your chart: tradingview.com/pine-script-docs/primer/first-indicator

// ============================================================================= // Dual-Timeframe OHLC Indicator // © CoreWave Studio — corewavestudio.com // Tutorial: Building a Dual-Timeframe OHLC Indicator in Pine Script // Version: 1.0 // ============================================================================= //@version=6 indicator( title = "Dual TF OHLC [CoreWave Studio]", shorttitle = "DualOHLC", overlay = true, max_boxes_count = 500, max_lines_count = 500, max_labels_count = 500 ) // ============================================================================= // INPUTS // ============================================================================= var string GRP_TF = "Timeframe Settings" var string GRP_ZONE = "Zone Display" tf1 = input.timeframe("W", title="#1 - Higher Timeframe", group=GRP_TF) tf2 = input.timeframe("D", title="#2 - Lower Timeframe", group=GRP_TF) showTF2 = input.bool(true, title="Show #2 Dimension?", group=GRP_TF) zoneExtBars1 = input.int(45, minval=1, maxval=200, title="#1 - Zone extends ahead (bars)", group=GRP_ZONE) zoneExtBars2 = input.int(20, minval=1, maxval=200, title="#2 - Zone extends ahead (bars)", group=GRP_ZONE) // ============================================================================= // DATA FETCHING — previous completed bar OHLC for each timeframe // ============================================================================= [o1, h1, l1, c1] = request.security(syminfo.tickerid, tf1, [open[1], high[1], low[1], close[1]], lookahead=barmerge.lookahead_on) [o2, h2, l2, c2] = request.security(syminfo.tickerid, tf2, [open[1], high[1], low[1], close[1]], lookahead=barmerge.lookahead_on) // ============================================================================= // CALENDAR-ACCURATE LEFT EDGE // Track when each timeframe bar opens and record bar_index at that moment // ============================================================================= tf1PrevBarTime = request.security(syminfo.tickerid, tf1, time[1], lookahead=barmerge.lookahead_on) tf2PrevBarTime = request.security(syminfo.tickerid, tf2, time[1], lookahead=barmerge.lookahead_on) tf1Changed = ta.change(tf1PrevBarTime) != 0 tf2Changed = ta.change(tf2PrevBarTime) != 0 var int tf1StartBarIdx = 0 var int tf2StartBarIdx = 0 if tf1Changed tf1StartBarIdx := bar_index if tf2Changed tf2StartBarIdx := bar_index // ============================================================================= // CORE CALCULATIONS // ============================================================================= bull1 = c1 >= o1 bull2 = c2 >= o2 midTop1 = math.max(o1, c1) midBot1 = math.min(o1, c1) midTop2 = math.max(o2, c2) midBot2 = math.min(o2, c2) // ============================================================================= // PERSISTENT OBJECT STORAGE // ============================================================================= var box[] tf1Boxes = array.new_box() var box[] tf2Boxes = array.new_box() var line[] tf1Lines = array.new_line() var line[] tf2Lines = array.new_line() var label[] tf1Labels = array.new_label() var label[] tf2Labels = array.new_label() // ============================================================================= // CLEAR HELPERS // ============================================================================= clearBoxes(box[] arr) => if array.size(arr) > 0 for i = 0 to array.size(arr) - 1 box.delete(array.get(arr, i)) array.clear(arr) clearLines(line[] arr) => if array.size(arr) > 0 for i = 0 to array.size(arr) - 1 line.delete(array.get(arr, i)) array.clear(arr) clearLabels(label[] arr) => if array.size(arr) > 0 for i = 0 to array.size(arr) - 1 label.delete(array.get(arr, i)) array.clear(arr) // ============================================================================= // DRAWING HELPERS // ============================================================================= drawZone(float top, float bot, color col, int leftT, int rightT, box[] arr) => if top > bot b = box.new(left=leftT, top=top, right=rightT, bottom=bot, border_color=color.new(col, 100), bgcolor=color.new(col, 85), xloc=xloc.bar_index) array.push(arr, b) drawLevel(float price, string lbl, color col, int leftT, int rightT, line[] linesArr, label[] labelsArr) => ln = line.new(x1=leftT, y1=price, x2=rightT, y2=price, color=col, width=1, style=line.style_solid, extend=extend.none, xloc=xloc.bar_index) array.push(linesArr, ln) lb = label.new(x=rightT, y=price, text=lbl, color=color.new(color.black, 100), textcolor=color.new(#A4A4A4, 0), style=label.style_label_right, size=size.small, xloc=xloc.bar_index) array.push(labelsArr, lb) // ============================================================================= // MAIN DRAWING BLOCK — runs only on the final bar // ============================================================================= if barstate.islast // Step 1: Clear all existing objects clearBoxes(tf1Boxes) clearBoxes(tf2Boxes) clearLines(tf1Lines) clearLines(tf2Lines) clearLabels(tf1Labels) clearLabels(tf2Labels) // Step 2: Calculate zone boundaries maxLookback = bar_index - 490 t1L = math.max(tf1StartBarIdx, maxLookback) t1R = bar_index + zoneExtBars1 t2L = math.max(tf2StartBarIdx, maxLookback) t2R = bar_index + zoneExtBars2 // Step 3: Draw Timeframe 1 midCol1 = bull1 ? color.new(#262C1F, 0) : color.new(#36312C, 0) outerCol1 = color.new(#32323A, 0) lineCol1 = color.new(#1E90FF, 0) drawZone(h1, midTop1, outerCol1, t1L, t1R, tf1Boxes) drawZone(midTop1, midBot1, midCol1, t1L, t1R, tf1Boxes) drawZone(midBot1, l1, outerCol1, t1L, t1R, tf1Boxes) drawLevel(h1, tf1 + " Hi", lineCol1, t1L, t1R, tf1Lines, tf1Labels) drawLevel(midTop1, tf1 + " Cl", lineCol1, t1L, t1R, tf1Lines, tf1Labels) drawLevel(midBot1, tf1 + " Op", lineCol1, t1L, t1R, tf1Lines, tf1Labels) drawLevel(l1, tf1 + " Lo", lineCol1, t1L, t1R, tf1Lines, tf1Labels) // Step 4: Draw Timeframe 2 — only if enabled if showTF2 midCol2 = bull2 ? color.new(#444B36, 0) : color.new(#4E463F, 0) outerCol2 = color.new(#323C3D, 0) lineCol2 = color.new(#26C6DA, 0) drawZone(h2, midTop2, outerCol2, t2L, t2R, tf2Boxes) drawZone(midTop2, midBot2, midCol2, t2L, t2R, tf2Boxes) drawZone(midBot2, l2, outerCol2, t2L, t2R, tf2Boxes) drawLevel(h2, tf2 + " Hi", lineCol2, t2L, t2R, tf2Lines, tf2Labels) drawLevel(midTop2, tf2 + " Cl", lineCol2, t2L, t2R, tf2Lines, tf2Labels) drawLevel(midBot2, tf2 + " Op", lineCol2, t2L, t2R, tf2Lines, tf2Labels) drawLevel(l2, tf2 + " Lo", lineCol2, t2L, t2R, tf2Lines, tf2Labels)
Reference

Pine Script Concepts Summary

A quick reference of every Pine Script concept introduced in this tutorial.

ConceptSyntaxWhat It Does
Version//@version=6Declares Pine Script version — always required as the first line
Indicatorindicator()Declares the script as an indicator with title, overlay and object limits
Inputinput.timeframe() etc.Creates a user-configurable setting in the indicator's settings panel
request.securityrequest.security(ticker, tf, expr)Fetches data from a different timeframe or symbol
History operatorclose[1]Accesses the value of a series N bars back
var keywordvar int x = 0Declares a variable that persists its value across all bars
Reassignmentx := 5Reassigns a previously declared variable
ta.changeta.change(source)Returns the difference between current and previous bar value
bar_indexbar_indexInteger position of the current bar — 0 for oldest, increments to newest
barstate.islastif barstate.islastTrue only on the final (most recent) bar — used to trigger drawing
box.newbox.new(left, top, right, bottom)Draws a filled rectangle on the chart
line.newline.new(x1, y1, x2, y2)Draws a horizontal or diagonal line on the chart
label.newlabel.new(x, y, text)Draws a text label on the chart
xloc.bar_indexxloc=xloc.bar_indexPositions objects by bar number — zoom-aware, consistent width
array.pusharray.push(arr, item)Appends an item to an array
array.cleararray.clear(arr)Removes all items from an array
User functionmyFunc(param) =>Defines a reusable block of code callable by name
What Next

Extending the Indicator

This tutorial covered the structural foundation. The same pattern — fetch OHLC, track timeframe boundaries, draw zones and lines — is the exact architecture used in Fibonacci Dimension. The differences are in what is calculated from the OHLC range, not in how it is drawn.

Extension 01
Add Fibonacci Levels
Replace the four OHLC lines with calculated Fibonacci retracement levels using calcFibPrice(high, low, pct, isBull). The drawing pattern is identical — only the price values change.
Extension 02
Add Alert Conditions
Use alertcondition() to fire alerts when price crosses the mid zone boundaries. Compare close to midTop1 and midBot1 on each bar outside the barstate.islast block.
Extension 03
Add Colour Inputs
Replace the hardcoded colours with input.color() inputs so traders can customise the zone colours to match their chart theme. Add them to a new "Styling" input group.
Fibonacci Dimension

Fibonacci Dimension is the full evolution of this OHLC indicator — extending retracement, extension, and expansion levels across both timeframes with the same calendar-accurate anchoring and zoom-aware zone rendering built in this tutorial.

It is available free on MetaTrader 4 and 5, and as an open-source Pine Script indicator on TradingView. The Pine Script source code is a direct extension of the patterns covered here.

The published TradingView version is available at tradingview.com/script/QBUgDW6g-Dual-TF-OHLC-CoreWave-Studio/ — add it to your chart directly or use this tutorial as the foundation to build your own variation.

Pine Script Documentation — The official Pine Script v6 reference covers every built-in function, variable, and keyword in detail. An essential bookmark for any Pine Script developer: tradingview.com/pine-script-docs/