Pine Script v6 Custom Functions: Build Reusable Mathematical Utilities for Cleaner Code
As Pine Script indicators and strategies grow in complexity, repeating the same mathematical calculations across multiple lines creates maintenance overhead and increases the risk of inconsistency. Custom functions in Pine Script v6 allow you to encapsulate reusable logic into named, callable units — producing cleaner, more testable, and more maintainable code.
1. The Anatomy of a Pine Script Custom Function
A Pine Script custom function is defined using a specific syntax block. The function body is indented, and the last evaluated expression in the function body is automatically returned — there is no return keyword.
//@version=6
indicator("Custom Function Demo", overlay = false)
// --- Function Definition ---
// A function that computes the True Range manually
// Parameters: high_, low_, prevClose_ (all float)
// Returns: float — the True Range value
trueRange(high_, low_, prevClose_) =>
// Candidate 1: Current High minus Current Low
hl = high_ - low_
// Candidate 2: Absolute difference between current High and previous Close
hc = math.abs(high_ - prevClose_)
// Candidate 3: Absolute difference between current Low and previous Close
lc = math.abs(low_ - prevClose_)
// The last expression is automatically returned
math.max(hl, math.max(hc, lc))
// --- Calling the Function ---
float tr = trueRange(high, low, close[1])
plot(tr, title = "Manual True Range", color = color.orange)
2. Core Syntax Rules
| Concept | Rule | Example |
|---|---|---|
| Declaration | Use functionName(params) => |
myFunc(x, y) => |
| Return Value | Last evaluated expression is returned automatically | x + y (last line) |
No return keyword |
NEVER use return — it does not exist in Pine Script |
❌ return x + y |
| Multiple Return Values | Return a tuple using [val1, val2] as the last expression |
[minVal, maxVal] |
| Scope | Functions can access global variables but have their own local scope | Global close is accessible inside |
var inside functions |
var inside a function persists state across bars for that function's local scope |
var float sum = 0.0 |
3. Mathematical Utility Functions: Practical Examples
The following script demonstrates three reusable mathematical utility functions: a normalized value scaler, a rolling Z-Score calculator, and a min/max range finder that returns a tuple.
🔽 [Click to expand] View Full Pine Script Code
//@version=6
indicator("Mathematical Utility Functions", overlay = false)
// ============================================================
// UTILITY FUNCTION 1: Min-Max Normalization
// Scales a value into the [0, 1] range over a lookback window.
// Formula: (x - min) / (max - min)
// Parameters:
// src_ : float — the source series
// length_ : int — the lookback period
// Returns: float — normalized value in [0.0, 1.0]
// ============================================================
normalize(src_, length_) =>
// Lowest value over the lookback window
float lo = ta.lowest(src_, length_)
// Highest value over the lookback window
float hi = ta.highest(src_, length_)
// Avoid division by zero when hi == lo
float range_ = hi - lo
float result = range_ != 0.0 ? (src_ - lo) / range_ : 0.0
result
// ============================================================
// UTILITY FUNCTION 2: Rolling Z-Score
// Measures how many standard deviations a value is from its mean.
// Formula: Z = (x - μ) / σ
// Parameters:
// src_ : float — the source series
// length_ : int — the lookback period for mean and stdev
// Returns: float — the Z-Score
// ============================================================
zScore(src_, length_) =>
// Rolling mean (Simple Moving Average)
float mean_ = ta.sma(src_, length_)
// Rolling standard deviation
float stdev_ = ta.stdev(src_, length_)
// Avoid division by zero when stdev is 0
float z = stdev_ != 0.0 ? (src_ - mean_) / stdev_ : 0.0
z
// ============================================================
// UTILITY FUNCTION 3: Rolling Min and Max (Tuple Return)
// Returns both the lowest and highest values over a window.
// Parameters:
// src_ : float — the source series
// length_ : int — the lookback period
// Returns: [float, float] — [minValue, maxValue]
// ============================================================
rollingMinMax(src_, length_) =>
float minVal = ta.lowest(src_, length_)
float maxVal = ta.highest(src_, length_)
// Tuple: last expression with two values
[minVal, maxVal]
// ============================================================
// INPUT PARAMETERS
// ============================================================
int lookback = input.int(20, title = "Lookback Period", minval = 2)
// ============================================================
// CALLING THE FUNCTIONS
// ============================================================
// Call normalize() on the closing price
float normalizedClose = normalize(close, lookback)
// Call zScore() on the closing price
float zScoreClose = zScore(close, lookback)
// Call rollingMinMax() — destructure the returned tuple
[rollingMin, rollingMax] = rollingMinMax(close, lookback)
// ============================================================
// PLOTTING
// ============================================================
// Plot normalized close (oscillates between 0 and 1)
plot(normalizedClose, title = "Normalized Close [0,1]", color = color.blue, linewidth = 2)
// Plot Z-Score (centered around 0)
plot(zScoreClose, title = "Z-Score", color = color.red, linewidth = 2)
// Reference lines for Z-Score interpretation
hline(2.0, "Z +2σ", color = color.gray, linestyle = hline.style_dashed)
hline(-2.0, "Z -2σ", color = color.gray, linestyle = hline.style_dashed)
hline(0.0, "Zero", color = color.silver, linestyle = hline.style_solid)
// Display rolling min and max in a table for verification
var table infoTable = table.new(position.top_right, 2, 3,
bgcolor = color.white, border_width = 1)
if barstate.islast
table.cell(infoTable, 0, 0, "Metric", text_color = color.black, bgcolor = color.new(color.gray, 80))
table.cell(infoTable, 1, 0, "Value", text_color = color.black, bgcolor = color.new(color.gray, 80))
table.cell(infoTable, 0, 1, "Rolling Min", text_color = color.black)
table.cell(infoTable, 1, 1, str.tostring(rollingMin, "#.##"), text_color = color.blue)
table.cell(infoTable, 0, 2, "Rolling Max", text_color = color.black)
table.cell(infoTable, 1, 2, str.tostring(rollingMax, "#.##"), text_color = color.red)
4. Mathematical Foundations
Understanding the mathematics behind each utility function ensures correct interpretation of their outputs.
4.1 Min-Max Normalization
Given a source value $x$, a rolling minimum $x_{min}$, and a rolling maximum $x_{max}$ over a window of length $n$:
$$\hat{x} = \frac{x - x_{min}}{x_{max} - x_{min}}, \quad \hat{x} \in [0, 1]$$
When $x_{max} = x_{min}$ (a flat series), the denominator is zero. The guard clause range_ != 0.0 ? ... : 0.0 prevents a runtime division-by-zero error.
4.2 Rolling Z-Score
Given a rolling mean $\mu_n$ and rolling standard deviation $\sigma_n$ over $n$ bars:
$$Z = \frac{x - \mu_n}{\sigma_n}$$
A Z-Score of $|Z| > 2$ indicates the current value is more than 2 standard deviations from its recent mean — a statistically significant deviation under a normal distribution assumption.
4.3 Tuple Return
Pine Script functions can return multiple values as a tuple. The caller must destructure the result using the [var1, var2] = functionCall() syntax. This is the only mechanism for returning more than one value from a function.
5. State Persistence Inside Functions with var
When you declare a variable with var inside a function, it is initialized only on the first bar the function is called and persists its value across all subsequent bars. This enables stateful accumulators inside reusable functions.
🔽 [Click to expand] View Stateful Accumulator Function Example
//@version=6
indicator("Stateful Function with var", overlay = false)
// ============================================================
// STATEFUL FUNCTION: Cumulative Sum with Reset
// Accumulates a running sum, resetting when a condition is met.
// Parameters:
// src_ : float — value to accumulate
// resetCond : bool — when true, resets the accumulator to 0
// Returns: float — the current cumulative sum
// ============================================================
cumulativeSum(src_, resetCond) =>
// var persists this variable's value across bars within this function's scope
var float runningSum = 0.0
// Reset on condition, otherwise accumulate
if resetCond
runningSum := 0.0
else
runningSum := runningSum + src_
// Last expression is returned
runningSum
// Reset the sum every time a new session begins
bool sessionStart = ta.change(time("D")) != 0
// Accumulate the bar's price change (close - open) within each session
float sessionPriceAccum = cumulativeSum(close - open, sessionStart)
plot(sessionPriceAccum, title = "Session Cumulative Price Change", color = color.purple)
hline(0, "Zero Line", color = color.gray)
6. Function Design Best Practices
| Practice | Rationale |
|---|---|
Use trailing underscores on parameter names (e.g., src_) |
Prevents shadowing built-in series names like close, high, length |
| Always guard against division by zero | Flat price series or zero standard deviation will cause na propagation without a guard |
| Declare parameter types explicitly when possible | Improves readability and helps Pine Script's type inference engine |
| Keep functions single-purpose | A function that does one thing is easier to test, debug, and reuse |
Use var inside functions only when statefulness is intentional |
Unintentional state persistence is a common source of subtle bugs |
7. Conclusion
- Automatic return: Pine Script custom functions return the value of their last evaluated expression. There is no
returnkeyword — attempting to use one will cause a compile error. - Tuple returns enable multi-value output: Use
[val1, val2]as the final expression and destructure with[a, b] = myFunc()at the call site — this is the only way to return multiple values from a single function. - Guard clauses are non-negotiable for mathematical functions: Any function performing division must explicitly handle the zero-denominator case to prevent
napropagation corrupting downstream calculations.
Ideas for Script Advancement
- Function Library Script: Convert your utility functions into a Pine Script library using the
library()declaration. This allows you to import and reuse the same validated functions across multiple indicators and strategies without copy-pasting code. - Parameterized Statistical Suite: Extend the Z-Score function to support exponential weighting (using
ta.emafor the mean and a custom exponentially weighted standard deviation), producing a more responsive anomaly detector for fast-moving markets.
Comments
Post a Comment