Building a Dual-Timeframe
OHLC Indicator
A step-by-step Pine Script v6 guide — from blank script to working indicator
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.
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.
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.
var keyword to persist it.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.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.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.
What each parameter does
| Parameter | Value | Purpose |
|---|---|---|
title | "Dual TF OHLC [CoreWave Studio]" | Full name shown in the Indicators menu |
shorttitle | "DualOHLC" | Compact name shown on the chart |
overlay | true | Draws on the price chart — not in a separate pane below |
max_boxes_count | 500 | Maximum number of box objects allowed simultaneously |
max_lines_count | 500 | Maximum number of line objects allowed simultaneously |
max_labels_count | 500 | Maximum 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.
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.
The four input types used
| Function | Returns | UI Element |
|---|---|---|
input.timeframe() | string | Timeframe dropdown — "W", "D", "60", "15" etc. |
input.bool() | bool | Checkbox — true or false |
input.int() | int | Integer number field with optional min/max |
input.color() | color | Colour picker — not used here but used in Fibonacci Dimension |
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.
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.
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.
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.
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?).
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
| Zone | Top | Bottom | Colour |
|---|---|---|---|
| Outer upper | h1 (High) | midTop1 | Neutral fill |
| Mid zone | midTop1 | midBot1 | Green bullish / Red bearish |
| Outer lower | midBot1 | l1 (Low) | Neutral fill |
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.
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.
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.
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.
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.
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 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.
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.
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.
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.
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.
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.
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.
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
Pine Script Concepts Summary
A quick reference of every Pine Script concept introduced in this tutorial.
| Concept | Syntax | What It Does |
|---|---|---|
| Version | //@version=6 | Declares Pine Script version — always required as the first line |
| Indicator | indicator() | Declares the script as an indicator with title, overlay and object limits |
| Input | input.timeframe() etc. | Creates a user-configurable setting in the indicator's settings panel |
| request.security | request.security(ticker, tf, expr) | Fetches data from a different timeframe or symbol |
| History operator | close[1] | Accesses the value of a series N bars back |
| var keyword | var int x = 0 | Declares a variable that persists its value across all bars |
| Reassignment | x := 5 | Reassigns a previously declared variable |
| ta.change | ta.change(source) | Returns the difference between current and previous bar value |
| bar_index | bar_index | Integer position of the current bar — 0 for oldest, increments to newest |
| barstate.islast | if barstate.islast | True only on the final (most recent) bar — used to trigger drawing |
| box.new | box.new(left, top, right, bottom) | Draws a filled rectangle on the chart |
| line.new | line.new(x1, y1, x2, y2) | Draws a horizontal or diagonal line on the chart |
| label.new | label.new(x, y, text) | Draws a text label on the chart |
| xloc.bar_index | xloc=xloc.bar_index | Positions objects by bar number — zoom-aware, consistent width |
| array.push | array.push(arr, item) | Appends an item to an array |
| array.clear | array.clear(arr) | Removes all items from an array |
| User function | myFunc(param) => | Defines a reusable block of code callable by name |
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.
calcFibPrice(high, low, pct, isBull). The drawing pattern is identical — only the price values change.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.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 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/