Pine Script v6 programming

Pine Script Functions: Returns, Tuples and Scope

Pine Script functions use => and return their final expression. Return multiple results as a tuple, then unpack them into new variables. To update global scalar state, return the next value and reassign it outside the function. The v6 examples below compare inline and extracted calculations, distinguish value types from references, and expose the separate state of each written function call.

Refactor Pine Script with AI

Parameter types and qualifiers solve different problems

A type describes the data; a qualifier constrains when it can vary. The hierarchy is const < input < simple < series. A simple int parameter accepts an input.int value but rejects a bar-dependent integer. Declaring only int does not guarantee a simple result. Explicit series cannot be weakened by casting to int. Reference types always carry series.

  • Define functions at global scope. Keep plot() calls outside their bodies.
  • Use an explicit parameter type when its default is na, when it has a qualifier, for exported function parameters, and for the first parameter of a method.
Parameter choiceUse in this pageConstraint
series float sourcePrice passed to the EMA helperAllows close or hl2 to change on each update
simple int lengthEMA lookback from input.intA length calculated from bar_index is too strong
float percent = 1.0Optional band distanceOmitting the argument selects 1.0 percent
const int parameterA compile-time-only argumentAvailable for non-exported functions; input values are not const
Untyped parameterGeneral helpers when inference is intendedEach written call must retain consistent argument and result types

Before: two copies of the same calculation

This indicator calculates a 10-bar EMA of close and a 20-bar EMA of hl2, then adds a 1% upper band to each. The repeated operation is basis * 1.01. Both lookbacks are positive inputs. Save this version before extracting a helper so the original plots remain available for comparison.

Complete v6 script: inline baseline for the refactorpinescript
//@version=6
indicator("Before: inline EMA bands", overlay=true)

fastLength = input.int(10, "Close EMA length", minval=1)
slowLength = input.int(20, "HL2 EMA length", minval=1)

fastBasis = ta.ema(close, fastLength)
fastUpper = fastBasis * 1.01
slowBasis = ta.ema(hl2, slowLength)
slowUpper = slowBasis * 1.01

plot(fastBasis, "Close basis", color=color.blue)
plot(fastUpper, "Close upper", color=color.aqua)
plot(slowBasis, "HL2 basis", color=color.orange)
plot(slowUpper, "HL2 upper", color=color.red)

After: a single return feeds a tuple return

smooth() returns one EMA. upperBand() uses it and returns basis and upper in that order. The two calls replace the duplicated calculation. A third call with a constant source is a numeric check: a basis of 100 and 1% distance should return 100 and 101. The error plots compare both extracted calls against inline baselines in the same script. They should be zero; these are expected outputs to check on a chart, not a claimed chart test.

Complete v6 refactor with tuple unpacking and comparison plotspinescript
//@version=6
indicator("After: function EMA bands", overlay=true)

smooth(series float source, simple int length) =>
    ta.ema(source, length)

upperBand(series float source, simple int length, float percent = 1.0) =>
    basis = smooth(source, length)
    upper = basis * (1.0 + percent / 100.0)
    [basis, upper]

fastLength = input.int(10, "Close EMA length", minval=1)
slowLength = input.int(20, "HL2 EMA length", minval=1)
[fastBasis, fastUpper] = upperBand(close, fastLength)
[slowBasis, slowUpper] = upperBand(hl2, slowLength)
[testBasis, testUpper] = upperBand(100.0, 3)

oldFast = ta.ema(close, fastLength)
oldSlow = ta.ema(hl2, slowLength)
fastError = math.max(math.abs(fastBasis - oldFast), math.abs(fastUpper - oldFast * 1.01))
slowError = math.max(math.abs(slowBasis - oldSlow), math.abs(slowUpper - oldSlow * 1.01))
if barstate.islastconfirmedhistory and (na(testUpper) or math.abs(testBasis - 100.0) > 0.000001 or math.abs(testUpper - 101.0) > 0.000001)
    runtime.error("Expected constant-source tuple [100, 101]")

plot(fastBasis, "Close basis", color=color.blue)
plot(fastUpper, "Close upper", color=color.aqua)
plot(slowBasis, "HL2 basis", color=color.orange)
plot(slowUpper, "HL2 upper", color=color.red)
plot(fastError, "Close refactor error: expected 0", display=display.data_window)
plot(slowError, "HL2 refactor error: expected 0", display=display.data_window)

Tuple rules when updating existing variables

Tuple declarations create new names with =; they cannot reassign an existing group with :=. Unpack into temporary names, then update persistent variables individually. Use _ for an unwanted result. A tuple can contain different types, but its results share a qualifier: returning a series price alongside a length also makes that length series. Keep the original input length separate when another call requires simple int.

TaskChoiceCheck
Read both band outputsUse the two new names from the refactorFirst item is basis, second item is upper
Use only the upper bandDiscard the first item with _The discarded value cannot be referenced later
Update two persistent totalsUnpack next values; then assign each outside the functionDo not put existing variables in a tuple declaration
Store an indexed collectionUse an arrayA tuple is not an array handle with array.get() access

Fix "cannot modify global variable in function"

The nextStats() helper below calculates replacements for count and totalRange without assigning either global variable. The caller receives new names and commits both with :=. In the same script, remember() changes element zero of an existing array: the array reference remains unchanged. The commented assignment shows the forbidden operation. Mutating an object is an observable side effect, so use it deliberately and document which object changes.

  • The example allocates exactly one array element before remember() uses index zero. Passing an empty array would cause an out-of-bounds runtime error.
  • For two included ranges of 2 and 4, the next states are count 1 / total 2, then count 2 / total 6; the mean is 3.
  • The confirmed-bar guard avoids including the unfinished range. Ordinary var state still follows realtime rollback; it is not an intrabar tick counter.
Complete v6 script: returned scalar state versus an array mutationpinescript
//@version=6
indicator("Function state boundaries")

var int count = 0
var float totalRange = 0.0
var array<float> latestRange = array.new<float>(1, na)

nextStats(int previousCount, float previousTotal, float barRange, bool includeBar) =>
    // Forbidden here: count := previousCount + 1
    // Parameters also cannot be reassigned.
    newCount = previousCount + (includeBar ? 1 : 0)
    newTotal = previousTotal + (includeBar ? barRange : 0.0)
    [newCount, newTotal]

remember(array<float> target, float value) =>
    array.set(target, 0, value)
    array.get(target, 0)

[nextCount, nextTotal] = nextStats(count, totalRange, high - low, barstate.isconfirmed)
count := nextCount
totalRange := nextTotal
if barstate.isconfirmed
    remember(latestRange, high - low)

meanRange = count > 0 ? totalRange / count : na
plot(meanRange, "Mean confirmed-bar range", color=color.blue)
plot(array.get(latestRange, 0), "Latest confirmed-bar range", color=color.orange)
plot(count, "Confirmed bars counted", display=display.data_window)

Local var state belongs to each written call

Each written call has independent local history. Repeating one call inside a loop reuses that call-site state. In this example A adds 1 once per bar, B adds 2, and C adds 1 three times through one loop call. The first three historical bars should match the table. Moving A into a condition would change which bars update it; adding an argument does not create a separate state bucket for each argument value.

Historical bar_indexABC after three loop iterations
0123
1246
2369
Complete v6 script: independent call sites and shared loop-call statepinescript
//@version=6
indicator("Local var call-site check")

accumulate(int amount) =>
    var int total = 0
    total += amount
    total

a = accumulate(1)
b = accumulate(2)
int c = na
for i = 1 to 3
    c := accumulate(1)

expectedA = bar_index + 1
if barstate.ishistory and (a != expectedA or b != expectedA * 2 or c != expectedA * 3)
    runtime.error("Call-site counter mismatch")

plot(a, "A: one per bar", color=color.blue)
plot(b, "B: two per bar", color=color.orange)
plot(c, "C: three per bar", color=color.purple)

Validate the refactor before replacing the original

Compile each complete example as its own indicator. For the EMA pair, use the same chart, input lengths and loaded history, then inspect both error series in the Data Window. A successful compile does not prove equivalence under every input. Live prices can still change the bands while a bar is open.

  • Compare missing-value behavior on early bars as well as visible plot values. Do not use nz() merely to make an error plot disappear.
  • Keep history-dependent calculations on the same execution schedule as before. Skipping a helper on selected bars can change its history.
  • Recheck state ownership if you combine two calls, move a call into a loop, or replace returned values with object mutation.
  • Give the AI Coding Agent the original script, Pine version, input cases and expected outputs. Ask for extracted helpers plus a comparison, then review the returned source in TradingView.
Where Pineify fits

Pine Script AI Coding Agent

Request a function refactor of your existing Pine Script with named inputs and returned values. Keep the baseline and review the generated source and chart outputs before replacing it.

Refactor Pine Script with AI

Frequently asked questions

Programming examples, not trading signals or investment advice. Syntax checks do not establish runtime correctness, non-repainting behavior or trading profitability. Expected numerical checks must be run on a chart.

Sources and verification

Published by Pineify. Pineify is an independent product and is not affiliated with TradingView. Sources support the technical explanations; examples do not establish trading performance.