Advanced Pine Script Techniques 🔧
This chapter covers powerful programming patterns that go beyond the basics. You will learn how to build custom data types, implement efficient data structures, model strategies as state machines, combine signals across multiple timeframes, detect advanced price patterns, and build weighted scoring systems. Every code example here uses correct Pine Script v5 syntax and is designed to compile in TradingView without modification.
Custom User-Defined Types (UDTs)​
Pine Script v5 lets you define your own types using the type keyword. A UDT groups related fields into a single object, making your code cleaner and more organized than juggling separate variables.
The type Keyword and Constructors​
You declare a type with type TypeName, list its fields with their types, and create instances with TypeName.new().
//@version=5
indicator("UDT Basics", overlay=true)
// Define a custom type
type TradeInfo
float entryPrice = 0.0
int direction = 0 // 1 = long, -1 = short, 0 = flat
float stopLoss = 0.0
float takeProfit = 0.0
float pnl = 0.0
// Create an instance using .new()
var TradeInfo currentTrade = TradeInfo.new()
// Access and modify fields
if ta.crossover(ta.sma(close, 10), ta.sma(close, 50))
currentTrade.entryPrice := close
currentTrade.direction := 1
currentTrade.stopLoss := close - ta.atr(14) * 2
currentTrade.takeProfit := close + ta.atr(14) * 3
// Calculate running P&L when in a trade
if currentTrade.direction != 0
currentTrade.pnl := (close - currentTrade.entryPrice) * currentTrade.direction
plot(currentTrade.pnl, "Trade P&L", color=currentTrade.pnl >= 0 ? color.green : color.red)
Key points:
- Fields are listed below the
typedeclaration, each on its own indented line. - Default values are optional but recommended.
- Use
varwhen declaring the instance so it persists across bars instead of being re-created every bar. - Access fields with the dot operator:
currentTrade.entryPrice.
Methods on Custom Types​
You can define methods that operate on your custom type by specifying the type as the first parameter.
//@version=5
indicator("UDT Methods", overlay=true)
type Level
float price = 0.0
int strength = 0
int touches = 0
// Method: check if the current price is near this level
method isNear(Level this, float currentPrice, float tolerance) =>
math.abs(currentPrice - this.price) <= tolerance
// Method: record a touch and increase strength
method recordTouch(Level this) =>
this.touches += 1
this.strength += 1
var Level support = Level.new(0.0, 0, 0)
// Initialize on first bar
if barstate.isfirst
support.price := low
// Check proximity and record touch
float atrVal = ta.atr(14)
if not na(atrVal) and support.isNear(close, atrVal * 0.5)
support.recordTouch()
plot(support.price, "Support", color=color.blue, linewidth=2)
plotchar(support.touches > 3, "Strong Level", char="S", location=location.belowbar, color=color.green)
Arrays of Custom Types​
You can store multiple UDT instances in an array, which is useful for tracking collections of objects like multiple trade records or price levels.
//@version=5
indicator("UDT Array", overlay=true)
type PriceLevel
float price = 0.0
int barIndex = 0
bool isBull = true
var array<PriceLevel> levels = array.new<PriceLevel>(0)
// Detect swing highs and store them
bool isSwingHigh = high[2] < high[1] and high[1] > high[0]
if isSwingHigh
PriceLevel newLevel = PriceLevel.new(high[1], bar_index - 1, false)
array.push(levels, newLevel)
// Keep only the last 10 levels
if array.size(levels) > 10
array.shift(levels)
// Draw the most recent stored level
if array.size(levels) > 0
PriceLevel latest = array.get(levels, array.size(levels) - 1)
plot(latest.price, "Last Swing High", color=color.red, linewidth=2, style=plot.style_stepline)
Advanced Data Structures​
Ring Buffer (Circular Buffer)​
A ring buffer is a fixed-size array where new data overwrites the oldest entry. It provides constant-time O(1) insertions and uses a fixed amount of memory regardless of how many bars have processed.
//@version=5
indicator("Ring Buffer", overlay=false)
int BUFFER_SIZE = 50
// 'var' means these persist across bars -- they are initialized once
// and retain their values on subsequent bars.
var float[] ringBuffer = array.new_float(BUFFER_SIZE, 0.0)
var int writeIndex = 0
// Write the current close into the buffer, overwriting the oldest value
array.set(ringBuffer, writeIndex, close)
writeIndex := (writeIndex + 1) % BUFFER_SIZE
// Calculate the average of everything in the buffer
float bufferSum = 0.0
for i = 0 to BUFFER_SIZE - 1
bufferSum += array.get(ringBuffer, i)
float bufferAvg = bufferSum / BUFFER_SIZE
plot(bufferAvg, "Ring Buffer Avg", color=color.orange, linewidth=2)
plot(ta.sma(close, BUFFER_SIZE), "SMA Comparison", color=color.blue)
Why var matters here: Without var, ringBuffer and writeIndex would be re-initialized on every bar, destroying any accumulated data. The var keyword ensures the array and counter persist from bar to bar and are only initialized once (on the very first bar). Be cautious when using var inside functions -- the variable will persist across all calls, which can cause unexpected behavior if the function is called from different contexts.
Sorted Insert​
Maintaining a sorted array is useful for computing medians and percentiles without re-sorting every bar. This binary search approach finds the correct insertion point in O(log n) time.
//@version=5
indicator("Sorted Insert", overlay=false)
int MAX_SIZE = 50
var float[] sortedData = array.new_float(0)
// Binary search to find the correct insertion index
binarySearchInsert(float[] arr, float val) =>
int lo = 0
int hi = array.size(arr)
int mid = 0
while lo < hi
mid := math.floor((lo + hi) / 2)
if array.get(arr, mid) < val
lo := mid + 1
else
hi := mid
lo
// Insert the current close in sorted order
int insertIdx = binarySearchInsert(sortedData, close)
array.insert(sortedData, insertIdx, close)
// Trim to MAX_SIZE by removing the oldest conceptually
// Here we simply keep the array at max size by removing first element
if array.size(sortedData) > MAX_SIZE
array.shift(sortedData)
// After removing, the array may not be perfectly sorted anymore,
// so re-sort it (this is cheap for small arrays)
array.sort(sortedData, order.ascending)
// Calculate median from the sorted array
int sz = array.size(sortedData)
float medianVal = 0.0
if sz > 0
int midPoint = math.floor(sz / 2)
if sz % 2 == 0
medianVal := (array.get(sortedData, midPoint - 1) + array.get(sortedData, midPoint)) / 2
else
medianVal := array.get(sortedData, midPoint)
plot(medianVal, "Running Median", color=color.purple, linewidth=2)
plot(close, "Close", color=color.gray)
State Machines​
Many trading strategies are naturally expressed as state machines: the system is always in one well-defined state, and it transitions between states based on market conditions. This makes your logic easier to reason about, debug, and extend.
The states we will use:
- IDLE -- Waiting for a setup to appear.
- SEEKING_ENTRY -- Setup detected, waiting for confirmation to enter.
- IN_POSITION -- Trade is active, managing with a trailing stop.
- SEEKING_EXIT -- Exit signal detected, closing position.
//@version=5
strategy("State Machine Strategy", overlay=true)
// --- Inputs ---
int fastLen = input.int(10, "Fast MA Length")
int slowLen = input.int(30, "Slow MA Length")
float atrMult = input.float(2.0, "ATR Multiplier for Stop")
int confirmBars = input.int(2, "Confirmation Bars")
// --- Indicators ---
float fastMA = ta.sma(close, fastLen)
float slowMA = ta.sma(close, slowLen)
float atrVal = ta.atr(14)
// --- State Variables (persist across bars) ---
var string state = "IDLE"
var int setupBarCount = 0
var float trailingStop = 0.0
var float entryPrice = 0.0
// --- State Transitions ---
switch state
"IDLE" =>
// Look for bullish setup: fast MA crosses above slow MA
if ta.crossover(fastMA, slowMA)
state := "SEEKING_ENTRY"
setupBarCount := 0
"SEEKING_ENTRY" =>
// Wait for confirmation: price stays above fast MA for N bars
if close > fastMA
setupBarCount += 1
else
// Setup failed, go back to idle
state := "IDLE"
setupBarCount := 0
if setupBarCount >= confirmBars
// Enter the trade
state := "IN_POSITION"
entryPrice := close
trailingStop := close - atrVal * atrMult
strategy.entry("Long", strategy.long)
"IN_POSITION" =>
// Update trailing stop: only move it up, never down
float newStop = close - atrVal * atrMult
if newStop > trailingStop
trailingStop := newStop
// Check for exit conditions
if close < trailingStop or ta.crossunder(fastMA, slowMA)
state := "SEEKING_EXIT"
"SEEKING_EXIT" =>
// Close the position
strategy.close("Long")
state := "IDLE"
setupBarCount := 0
trailingStop := 0.0
entryPrice := 0.0
// --- Plotting ---
plot(fastMA, "Fast MA", color=color.blue)
plot(slowMA, "Slow MA", color=color.red)
plot(state == "IN_POSITION" ? trailingStop : na, "Trailing Stop", color=color.orange, style=plot.style_stepline, linewidth=2)
// State label on the last bar
if barstate.islast
string stateText = "State: " + state
label.new(bar_index, high, stateText, style=label.style_label_down, color=color.blue, textcolor=color.white)
How it works:
- The
switchstatement cleanly separates logic for each state. - The
varkeyword ensuresstate,setupBarCount,trailingStop, andentryPricepersist bar to bar. - The trailing stop only moves upward, locking in profits as price advances.
- If the confirmation fails mid-setup, the strategy resets to IDLE instead of entering a bad trade.
Multi-Timeframe Confluence​
Combining signals from multiple timeframes increases the reliability of your entries. The key rule: always use barmerge.lookahead_off in request.security() to prevent future data from leaking into historical bars, which causes unrealistic backtest results (repainting).
//@version=5
strategy("MTF Confluence", overlay=true)
// --- Inputs ---
int maLen = input.int(20, "MA Length")
// --- Current Timeframe ---
float currentMA = ta.sma(close, maLen)
float currentRSI = ta.rsi(close, 14)
bool currentBullish = close > currentMA and currentRSI > 50
// --- Higher Timeframes ---
// 4-Hour trend
float htf4hClose = request.security(syminfo.tickerid, "240", close, barmerge.gaps_off, barmerge.lookahead_off)
float htf4hMA = request.security(syminfo.tickerid, "240", ta.sma(close, maLen), barmerge.gaps_off, barmerge.lookahead_off)
bool htf4hBull = htf4hClose > htf4hMA
// Daily trend
float htfDClose = request.security(syminfo.tickerid, "D", close, barmerge.gaps_off, barmerge.lookahead_off)
float htfDMA = request.security(syminfo.tickerid, "D", ta.sma(close, maLen), barmerge.gaps_off, barmerge.lookahead_off)
bool htfDBull = htfDClose > htfDMA
// Weekly trend
float htfWClose = request.security(syminfo.tickerid, "W", close, barmerge.gaps_off, barmerge.lookahead_off)
float htfWMA = request.security(syminfo.tickerid, "W", ta.sma(close, maLen), barmerge.gaps_off, barmerge.lookahead_off)
bool htfWBull = htfWClose > htfWMA
// --- Confluence Score (0 to 4) ---
int confluenceScore = 0
if currentBullish
confluenceScore += 1
if htf4hBull
confluenceScore += 1
if htfDBull
confluenceScore += 1
if htfWBull
confluenceScore += 1
// --- Entry/Exit Logic ---
bool strongBullish = confluenceScore >= 3
bool strongBearish = confluenceScore <= 1
if strongBullish
strategy.entry("Long", strategy.long)
if strongBearish
strategy.close("Long")
// --- Display confluence score ---
plot(currentMA, "Current MA", color=color.blue)
var table scoreTable = table.new(position.top_right, 2, 5, bgcolor=color.new(color.black, 80))
if barstate.islast
table.cell(scoreTable, 0, 0, "Timeframe", text_color=color.white, text_size=size.small)
table.cell(scoreTable, 1, 0, "Trend", text_color=color.white, text_size=size.small)
table.cell(scoreTable, 0, 1, "Current", text_color=color.white, text_size=size.small)
table.cell(scoreTable, 1, 1, currentBullish ? "BULL" : "BEAR", text_color=currentBullish ? color.green : color.red, text_size=size.small)
table.cell(scoreTable, 0, 2, "4H", text_color=color.white, text_size=size.small)
table.cell(scoreTable, 1, 2, htf4hBull ? "BULL" : "BEAR", text_color=htf4hBull ? color.green : color.red, text_size=size.small)
table.cell(scoreTable, 0, 3, "Daily", text_color=color.white, text_size=size.small)
table.cell(scoreTable, 1, 3, htfDBull ? "BULL" : "BEAR", text_color=htfDBull ? color.green : color.red, text_size=size.small)
table.cell(scoreTable, 0, 4, "Weekly", text_color=color.white, text_size=size.small)
table.cell(scoreTable, 1, 4, htfWBull ? "BULL" : "BEAR", text_color=htfWBull ? color.green : color.red, text_size=size.small)
Avoiding repainting: The critical parameter is barmerge.lookahead_off. Without it, request.security() can peek at future data on historical bars, making your strategy appear more profitable than it would be in live trading. Always use barmerge.lookahead_off for reliable results.
Advanced Pattern Recognition​
Engulfing Patterns​
An engulfing pattern occurs when the current candle's body completely contains the previous candle's body, with the direction reversed.
//@version=5
indicator("Engulfing Patterns", overlay=true)
// Body boundaries
float prevBodyHigh = math.max(open[1], close[1])
float prevBodyLow = math.min(open[1], close[1])
float currBodyHigh = math.max(open, close)
float currBodyLow = math.min(open, close)
// Bullish engulfing: previous candle is bearish, current is bullish,
// and current body engulfs previous body
bool bullEngulf = close[1] < open[1] and
close > open and
currBodyLow <= prevBodyLow and
currBodyHigh >= prevBodyHigh
// Bearish engulfing: previous candle is bullish, current is bearish,
// and current body engulfs previous body
bool bearEngulf = close[1] > open[1] and
close < open and
currBodyLow <= prevBodyLow and
currBodyHigh >= prevBodyHigh
plotshape(bullEngulf, "Bullish Engulfing", shape.triangleup, location.belowbar, color.green, size=size.small)
plotshape(bearEngulf, "Bearish Engulfing", shape.triangledown, location.abovebar, color.red, size=size.small)
Order Block Detection​
An order block is the last opposing candle before a strong impulsive move. Bullish order blocks are the last bearish candle before a strong rally. Bearish order blocks are the last bullish candle before a sharp drop. Institutional traders watch these zones for potential reversals.
//@version=5
indicator("Order Blocks", overlay=true, max_boxes_count=100)
// --- Inputs ---
float impulseMult = input.float(1.5, "Impulse Strength (ATR multiple)")
int obLookback = input.int(5, "Order Block Lookback")
float atrVal = ta.atr(14)
// Detect a strong bullish impulse: current bar closes significantly above its open
bool strongBullBar = (close - open) > atrVal * impulseMult and close > open
// Detect a strong bearish impulse
bool strongBearBar = (open - close) > atrVal * impulseMult and close < open
// Find the last bearish candle before a bullish impulse (bullish order block)
var box bullOBBox = na
if strongBullBar
// Look back for the most recent bearish candle
for i = 1 to obLookback
if close[i] < open[i]
bullOBBox := box.new(bar_index - i, open[i], bar_index + 10, close[i],
border_color=color.new(color.green, 50),
bgcolor=color.new(color.green, 85))
break
// Find the last bullish candle before a bearish impulse (bearish order block)
var box bearOBBox = na
if strongBearBar
for i = 1 to obLookback
if close[i] > open[i]
bearOBBox := box.new(bar_index - i, open[i], bar_index + 10, close[i],
border_color=color.new(color.red, 50),
bgcolor=color.new(color.red, 85))
break
How it works:
- A "strong" bar is one whose body size exceeds a multiple of the ATR.
- When a strong bullish bar appears, the code looks backward up to
obLookbackbars for the last bearish candle and draws a box at that candle's body (open to close). - The box extends 10 bars to the right so you can see the zone on the chart.
Fair Value Gap (FVG) Detection​
A Fair Value Gap is a three-candle pattern where a gap exists between the high of candle 1 and the low of candle 3. This gap represents an imbalance that price may revisit.
//@version=5
indicator("Fair Value Gaps", overlay=true, max_boxes_count=200)
// --- Inputs ---
float minGapATR = input.float(0.5, "Min Gap Size (ATR multiple)")
float atrVal = ta.atr(14)
// Candle references:
// [2] = candle 1 (oldest), [1] = candle 2 (middle), [0] = candle 3 (newest)
// Bullish FVG: gap between candle 1 high and candle 3 low
// (candle 3 low is above candle 1 high, meaning price jumped up)
bool bullFVG = low > high[2] and (low - high[2]) > atrVal * minGapATR
float bullFVGTop = low
float bullFVGBottom = high[2]
// Bearish FVG: gap between candle 3 high and candle 1 low
// (candle 3 high is below candle 1 low, meaning price dropped)
bool bearFVG = high < low[2] and (low[2] - high) > atrVal * minGapATR
float bearFVGTop = low[2]
float bearFVGBottom = high
// Draw bullish FVG box
if bullFVG
box.new(bar_index - 2, bullFVGTop, bar_index + 5, bullFVGBottom,
border_color=color.new(color.green, 40),
bgcolor=color.new(color.green, 85))
// Draw bearish FVG box
if bearFVG
box.new(bar_index - 2, bearFVGTop, bar_index + 5, bearFVGBottom,
border_color=color.new(color.red, 40),
bgcolor=color.new(color.red, 85))
Key details:
- Bullish FVG:
low[0] > high[2]-- the newest candle's low is entirely above the oldest candle's high. - Bearish FVG:
high[0] < low[2]-- the newest candle's high is entirely below the oldest candle's low. - A minimum gap size filter (based on ATR) prevents noise from tiny gaps.
Signal Scoring and Weighting System​
Rather than relying on a single indicator, you can assign numeric scores to multiple indicators and combine them into a composite signal. Each indicator gets a configurable weight, and you enter or exit based on threshold values.
//@version=5
strategy("Signal Scoring System", overlay=true)
// --- Indicator Inputs ---
int rsiLen = input.int(14, "RSI Length")
int macdFast = input.int(12, "MACD Fast")
int macdSlow = input.int(26, "MACD Slow")
int macdSig = input.int(9, "MACD Signal")
int volLen = input.int(20, "Volume MA Length")
int trendLen = input.int(50, "Trend MA Length")
// --- Weight Inputs ---
float wRSI = input.float(1.0, "RSI Weight")
float wMACD = input.float(1.0, "MACD Weight")
float wVol = input.float(0.5, "Volume Weight")
float wTrend = input.float(1.5, "Trend Weight")
// --- Threshold Inputs ---
float entryThreshold = input.float(2.0, "Entry Threshold")
float exitThreshold = input.float(-1.0, "Exit Threshold")
// --- Calculate Indicators ---
float rsiVal = ta.rsi(close, rsiLen)
[macdLine, signalLine, histLine] = ta.macd(close, macdFast, macdSlow, macdSig)
float volMA = ta.sma(volume, volLen)
float trendMA = ta.sma(close, trendLen)
// --- Score Each Indicator ---
// RSI: bullish if recovering from oversold, bearish if falling from overbought
float rsiScore = 0.0
if rsiVal < 30
rsiScore := 1.0 // Oversold = bullish signal
else if rsiVal > 70
rsiScore := -1.0 // Overbought = bearish signal
else if rsiVal > 50
rsiScore := 0.5
else
rsiScore := -0.5
// MACD: bullish if histogram is positive and rising
float macdScore = 0.0
if histLine > 0 and histLine > histLine[1]
macdScore := 1.0
else if histLine > 0
macdScore := 0.5
else if histLine < 0 and histLine < histLine[1]
macdScore := -1.0
else
macdScore := -0.5
// Volume: above-average volume confirms the move
float volScore = 0.0
if not na(volMA) and volMA > 0
volScore := volume > volMA * 1.5 ? 1.0 : volume > volMA ? 0.5 : -0.5
// Trend: price above MA is bullish
float trendScore = 0.0
if close > trendMA
trendScore := 1.0
else
trendScore := -1.0
// --- Composite Score ---
float totalWeight = wRSI + wMACD + wVol + wTrend
float compositeScore = 0.0
if totalWeight > 0
compositeScore := (rsiScore * wRSI + macdScore * wMACD + volScore * wVol + trendScore * wTrend) / totalWeight
// --- Entry/Exit Decisions ---
if compositeScore >= entryThreshold / totalWeight * 2
strategy.entry("Long", strategy.long)
if compositeScore <= exitThreshold / totalWeight * 2
strategy.close("Long")
// --- Plot composite score in a separate pane ---
plot(compositeScore, "Composite Score", color=compositeScore > 0 ? color.green : color.red, linewidth=2)
hline(0, "Zero Line", color=color.gray)
// --- Dashboard Table ---
var table dashboard = table.new(position.bottom_right, 3, 6, bgcolor=color.new(color.black, 80))
if barstate.islast
table.cell(dashboard, 0, 0, "Indicator", text_color=color.white, text_size=size.small)
table.cell(dashboard, 1, 0, "Score", text_color=color.white, text_size=size.small)
table.cell(dashboard, 2, 0, "Weight", text_color=color.white, text_size=size.small)
table.cell(dashboard, 0, 1, "RSI", text_color=color.white, text_size=size.small)
table.cell(dashboard, 1, 1, str.tostring(rsiScore, "#.##"), text_color=rsiScore > 0 ? color.green : color.red, text_size=size.small)
table.cell(dashboard, 2, 1, str.tostring(wRSI, "#.#"), text_color=color.white, text_size=size.small)
table.cell(dashboard, 0, 2, "MACD", text_color=color.white, text_size=size.small)
table.cell(dashboard, 1, 2, str.tostring(macdScore, "#.##"), text_color=macdScore > 0 ? color.green : color.red, text_size=size.small)
table.cell(dashboard, 2, 2, str.tostring(wMACD, "#.#"), text_color=color.white, text_size=size.small)
table.cell(dashboard, 0, 3, "Volume", text_color=color.white, text_size=size.small)
table.cell(dashboard, 1, 3, str.tostring(volScore, "#.##"), text_color=volScore > 0 ? color.green : color.red, text_size=size.small)
table.cell(dashboard, 2, 3, str.tostring(wVol, "#.#"), text_color=color.white, text_size=size.small)
table.cell(dashboard, 0, 4, "Trend", text_color=color.white, text_size=size.small)
table.cell(dashboard, 1, 4, str.tostring(trendScore, "#.##"), text_color=trendScore > 0 ? color.green : color.red, text_size=size.small)
table.cell(dashboard, 2, 4, str.tostring(wTrend, "#.#"), text_color=color.white, text_size=size.small)
table.cell(dashboard, 0, 5, "TOTAL", text_color=color.yellow, text_size=size.small)
table.cell(dashboard, 1, 5, str.tostring(compositeScore, "#.###"), text_color=compositeScore > 0 ? color.green : color.red, text_size=size.small)
table.cell(dashboard, 2, 5, str.tostring(totalWeight, "#.#"), text_color=color.white, text_size=size.small)
This approach lets you:
- Adjust the influence of each indicator by changing its weight.
- Add new indicators by adding a score calculation and a weight input.
- Visually inspect which indicators are contributing to the signal via the dashboard.
Advanced Error Handling and Input Validation​
Pine Script does not have try/catch or runtime.error(). Instead, you validate inputs at the start of your script and handle na values gracefully throughout. Logging issues to a debug table helps you identify problems during development.
//@version=5
indicator("Input Validation", overlay=true)
// --- Raw Inputs ---
int rawRsiLen = input.int(14, "RSI Length", minval=1, maxval=500)
int rawMaLen = input.int(50, "MA Length", minval=1, maxval=1000)
float rawAtrMul = input.float(2.0, "ATR Multiplier", minval=0.1, maxval=10.0)
// --- Debug Table ---
var table debugTable = table.new(position.bottom_left, 2, 5, bgcolor=color.new(color.black, 85))
var int issueCount = 0
// Validation function: clamp a value to a range and log if it was out of bounds
clampAndLog(float val, float minV, float maxV, string name) =>
float result = math.max(minV, math.min(maxV, val))
// We cannot log dynamically per call easily, but the clamping protects us
result
// --- Validate and Clamp ---
int rsiLen = math.max(2, math.min(rawRsiLen, 200))
int maLen = math.max(2, math.min(rawMaLen, 500))
float atrMul = math.max(0.1, math.min(rawAtrMul, 10.0))
// --- Safely calculate indicators, handling na ---
float rsiVal = ta.rsi(close, rsiLen)
float maVal = ta.sma(close, maLen)
float atrVal = ta.atr(14)
// Replace na values with safe defaults for downstream logic
float safeRSI = na(rsiVal) ? 50.0 : rsiVal
float safeMA = na(maVal) ? close : maVal
float safeATR = na(atrVal) ? 0.0 : atrVal
// --- Use safe values for calculations ---
float upperBand = safeMA + safeATR * atrMul
float lowerBand = safeMA - safeATR * atrMul
plot(safeMA, "MA", color=color.blue)
plot(upperBand, "Upper Band", color=color.green)
plot(lowerBand, "Lower Band", color=color.red)
// --- Log status on last bar ---
if barstate.islast
bool rsiWasClamped = rawRsiLen != rsiLen
bool maWasClamped = rawMaLen != maLen
int naBarCount = ta.cum(na(rsiVal) ? 1 : 0) > 0 ? 1 : 0
table.cell(debugTable, 0, 0, "Check", text_color=color.white, text_size=size.small)
table.cell(debugTable, 1, 0, "Status", text_color=color.white, text_size=size.small)
table.cell(debugTable, 0, 1, "RSI Length", text_color=color.white, text_size=size.small)
table.cell(debugTable, 1, 1, rsiWasClamped ? "CLAMPED" : "OK",
text_color=rsiWasClamped ? color.yellow : color.green, text_size=size.small)
table.cell(debugTable, 0, 2, "MA Length", text_color=color.white, text_size=size.small)
table.cell(debugTable, 1, 2, maWasClamped ? "CLAMPED" : "OK",
text_color=maWasClamped ? color.yellow : color.green, text_size=size.small)
table.cell(debugTable, 0, 3, "Data Quality", text_color=color.white, text_size=size.small)
table.cell(debugTable, 1, 3, na(rsiVal) ? "NA present" : "Clean",
text_color=na(rsiVal) ? color.red : color.green, text_size=size.small)
table.cell(debugTable, 0, 4, "ATR Valid", text_color=color.white, text_size=size.small)
table.cell(debugTable, 1, 4, safeATR > 0 ? "OK" : "Zero/NA",
text_color=safeATR > 0 ? color.green : color.red, text_size=size.small)
Best practices for handling errors in Pine Script:
- Use
input()constraints (minval,maxval) as the first line of defense. - Always check for
nabefore using a calculated value in division or comparisons. - Provide fallback values so the script degrades gracefully instead of breaking.
- Use a debug table during development to track issues visually.
Memory-Efficient Programming​
var vs Recalculating Each Bar​
The var keyword initializes a variable once and persists it across bars. Without var, variables are recalculated from scratch on every bar.
| Pattern | When to Use |
|---|---|
var float x = 0.0 | When you need to accumulate or persist state (counters, running totals, buffers) |
float x = ta.sma(close, 20) | When the value depends on the current bar's data and should be fresh each bar |
Key rule: Use var for state. Omit var for calculations.
Managing Array Sizes​
Arrays that grow without bounds will eventually hit Pine Script's memory limits. Always enforce a maximum size.
//@version=5
indicator("Array Size Management", overlay=false)
var float[] recentHighs = array.new_float(0)
int MAX_ELEMENTS = 200
// Add the current high
array.push(recentHighs, high)
// Enforce maximum size by removing the oldest element
while array.size(recentHighs) > MAX_ELEMENTS
array.shift(recentHighs)
plot(array.size(recentHighs), "Array Size")
plot(array.max(recentHighs), "Highest in Window")
The max_bars_back Annotation​
When Pine Script cannot determine how many past bars a variable needs, it may throw a "reference too many bars back" error. You can solve this with max_bars_back.
//@version=5
indicator("Max Bars Back", overlay=true)
// Tell Pine Script this variable may reference up to 500 bars back
var float trackedPrice = close
max_bars_back(trackedPrice, 500)
// Some conditional logic that references the variable far back
if bar_index > 500 and close > trackedPrice[500]
trackedPrice := close
plot(trackedPrice, "Tracked Price")
Best Practices for Long-Running Scripts​
- Cap array sizes -- Always remove old elements when you push new ones.
- Limit drawing objects -- Use
max_lines_count,max_labels_count, andmax_boxes_countin yourindicator()declaration. - Avoid nested loops on large datasets -- An O(n^2) loop inside an array of 1,000 elements runs 1,000,000 iterations per bar.
- Use
varfor objects you update -- Creating new labels, lines, or boxes every bar without deleting old ones wastes resources. - Use
barstate.islastfor expensive display logic -- Tables and labels that only matter on the latest bar should be wrapped inif barstate.islast.
Practice Exercises​
-
Support/Resistance Levels with Custom Types: Create a custom
Leveltype with fields forprice(float),strength(int), andtouchCount(int). Write a script that detects swing highs and swing lows, createsLevelinstances, and incrementstouchCountwhen price revisits a level within an ATR-based tolerance. Display the strongest levels on the chart using horizontal lines. -
Multi-Timeframe Dashboard: Build a dashboard that displays the trend direction (bullish or bearish) for 4 timeframes (15-minute, 1-hour, 4-hour, and daily) in a table. Use
request.security()withbarmerge.lookahead_offfor each timeframe. Color each cell green for bullish and red for bearish. Add a row showing the overall confluence score. -
Custom Signal Scoring System: Implement a signal scoring system that combines at least 3 indicators (for example, RSI, Bollinger Band position, and volume trend) with configurable weights via
input.float(). Display the composite score as a histogram and add buy/sell markers when the score crosses your configurable thresholds.
- Start with state machines when your strategy has distinct phases. It prevents spaghetti logic and makes debugging straightforward.
- Use custom types early. Even simple strategies benefit from grouping related data into a
typeinstead of using parallel arrays. - Score-based systems are more robust than single-indicator signals. They let you tune sensitivity without rewriting logic.
- Always test with
barmerge.lookahead_offinrequest.security(). The default behavior can produce misleading backtest results. - Use
barstate.islastfor table updates and label creation to avoid unnecessary object creation on historical bars.
varinside functions: Avarvariable inside a function is initialized once and persists across all bars and all calls. This means if you call the function for different purposes, they share the same persisted variable. Usevarinside functions only when this shared persistence is intentional.- Unbounded arrays: Forgetting to cap array sizes will eventually crash your script with a memory error. Always enforce a maximum.
- Repainting with
request.security(): Usingbarmerge.lookahead_on(the default for some overloads) lets the script see future higher-timeframe data on historical bars. Always explicitly passbarmerge.lookahead_off. - Creating drawing objects every bar: If you create a
label.new()orbox.new()on every bar without limiting the count or deleting old objects, you will hit Pine Script's drawing limits quickly. - Ignoring
navalues: Many built-in functions returnnaon early bars where there is not enough data. Always guard againstnabefore using values in division, comparisons, or array operations.
Next Steps​
Ready to learn about creating custom libraries? Move on to the next chapter!