The script
A band function that returns a tuple, a one-line function with a default argument, and a Swing type with two methods. Paste it into a new indicator in the Pine Editor, then save and add it to the chart.
//@version=6
indicator("Functions and methods", overlay = true)
len = input.int(20, "Band length", minval = 2)
mult = input.float(2.0, "Band width (x stdev)", minval = 0.1, step = 0.1)
swingLen = input.int(3, "Swing strength", minval = 1)
// A function that returns three values (a tuple)
bands(float src, simple int length, float k) =>
float basis = ta.sma(src, length)
float dev = k * ta.stdev(src, length)
[basis, basis + dev, basis - dev]
// One-line functions; decimals has a default value
pctFrom(float price, float ref) => ref != 0 ? (price - ref) / ref * 100 : na
roundTo(float x, int decimals = 2) => math.round(x, decimals)
// A type, and two methods that work on it
type Swing
float price
int index
method pctTo(Swing s, float price) =>
pctFrom(price, s.price)
method age(Swing s) =>
na(s.index) ? na : bar_index - s.index
[basis, upper, lower] = bands(close, len, mult)
pl = ta.pivotlow(low, swingLen, swingLen)
var Swing lastLow = Swing.new(na, na)
if not na(pl)
// Fields of an object can be changed anywhere with :=
lastLow.price := pl
lastLow.index := bar_index - swingLen
plot(basis, "Basis", color.gray)
plot(upper, "Upper band", color.teal)
plot(lower, "Lower band", color.teal)
plot(lastLow.price, "Last swing low", color.orange, style = plot.style_stepline)
plot(roundTo(pctFrom(close, basis)), "% from the basis", display = display.data_window)
plot(roundTo(lastLow.pctTo(close), 1), "% above the last swing low", display = display.data_window)
plot(lastLow.age(), "Bars since the swing low", display = display.data_window)How it works
bands(float src, simple int length, float k) =>. Parameter types are optional in scripts (they're required in libraries), but they make mistakes show up at the call.simple intsays the length must be fixed once the script starts, which is whatta.sma()andta.stdev()are given here.- The tuple. The last line
[basis, basis + dev, basis - dev]is the return value;[basis, upper, lower] = bands(...)unpacks it into three variables. - One-line functions put the body after
=>.roundTo(x, decimals = 2)can be called with one argument, as inroundTo(pctFrom(close, basis)), or two. type Swingdeclares an object with two fields;Swing.new(na, na)creates one. Declared withvar, the same object lives across bars, and theifblock updates its fields with:=.- Methods.
method pctTo(Swing s, float price)takes the object as its first parameter, so it's called aslastLow.pctTo(close). It reusespctFrom()rather than repeating the formula. - History per call. Each place a function with
ta.*calls inside is called keeps its own history, so callingbands()twice with different sources gives two independent sets of bands.
Pitfalls
- Changing a global variable.
count := count + 1inside a function gives Cannot modify global variable in function. Return the new value and assign it at the top level, or keep the state in an object's fields or an array, which a function may change. - ta. calls in a function called conditionally.* If
bands()is only called inside anif, itsta.sma()sees only the bars where the block ran (should be called on each calculation). Call the function on every bar and use the result in theif. - A series where a fixed value is needed. Passing a length that changes from bar to bar to a function that gives it to
ta.ema()fails with series int used but simple int expected. Declaring the parameter assimple intmakes the error point at the call that passes it. - Plotting from a function.
plot()and the other output calls can't be inside a function (Cannot use plot in local scope). Return the value and plot it at the top level. - Branches of different types. A function whose
ifor ternary returns a string in one branch and a number in the other doesn't compile (return types not compatible). Every branch must return the same type, withnaallowed in place of any of them.
Variations
A method on a built-in type. Methods aren't limited to your own types: here pushCapped() adds a value to any array<float> and trims it to n elements, so a rolling window is one call.
//@version=6
indicator("Method on an array", overlay = false)
// Push a value and keep only the newest n
method pushCapped(array<float> a, float value, int n) =>
a.push(value)
if a.size() > n
a.shift()
a
var array<float> closes = array.new<float>()
closes.pushCapped(close, 50)
plot(closes.avg(), "Average of the last 50 closes", color.teal)
Share functions between scripts. Functions you reuse everywhere can live in a library that other scripts import; see the library example.
Questions
Can a Pine Script function return more than one value?
Yes: make the last line a tuple, such as [basis, upper, lower], and unpack it at the call with [b, u, l] = myFunction(). The number of variables must match the number of values returned.
What is the difference between a function and a method in Pine Script?
Only how they're declared and called. A method is declared with the method keyword, and its first parameter is the object it works on, so you call it with a dot: swing.age() instead of age(swing).
Can a Pine Script function keep a value from one bar to the next?
Yes: a var variable declared inside a function is initialized once and keeps its value, and each place the function is called has its own copy. That's how a function can count events or remember a level.
Related pages
Every script on this page was checked with Edgoro Pine Check (Pine Script v6 rules). Pine Check is a static checker, not TradingView's compiler: TradingView compiles the script when you save it in the Pine Editor. The scripts are examples to learn from, not trading systems. Trading involves substantial risk of loss and is not suitable for every investor. Edgoro is a software tool, not a broker or financial adviser: nothing it produces is investment advice, and past or backtested performance does not guarantee future results.