Tutorial

Pine Script Functions: Writing and Reusing Your Own

Learn to write user-defined functions in Pine Script v6: syntax, typed parameters, tuple returns, and the every-bar rule that keeps results correct.

•9 min read

You wrote a smoothed momentum calculation for one indicator, and it worked. A week later you need the same calculation in a second script, so you copy the four lines across. Then a third script. Then you find a mistake in the original, fix it there, and realise two other scripts still carry the bug. Copy and paste is the quiet source of most maintenance pain in Pine Script, and user-defined functions are the tool built to stop it.

This guide covers how to write your own functions in Pine Script v6, how they return values, how they handle series data, and the rules that catch people out. By the end you will be able to take any repeated block of code in a script, turn it into a function with clear inputs and outputs, and know why it still behaves correctly on every bar.

What a function is in Pine Script

A function is a named piece of code that takes inputs, called parameters, and produces a result. Pine Script already ships hundreds of them. ta.sma, math.max, and str.tostring are all functions that someone else wrote. A user-defined function lets you add your own to that set, and then call it exactly the way you call the built-in ones.

The simplest form fits on one line. You write the name, the parameters in parentheses, the => operator, and the expression to return. The => operator is what separates the signature from the body.

pine
//@version=6
indicator("Function basics", overlay = true)

midpoint(float a, float b) => (a + b) / 2

plot(midpoint(high, low), "Midpoint")

The function midpoint takes two numbers and returns their average. Calling it with high and low plots the midpoint of every bar's range. The function body is one expression, and the value of that expression is what the call returns. There is no return keyword in Pine Script. That surprises people who come from other languages, and it is worth remembering because it explains how multi-line functions work too.

Multi-line functions and the last expression

When a function needs more than one step, you put the body on the following lines and indent it by four spaces or a tab. Every statement in the body belongs to the function, and the value of the last statement is the value the function returns.

pine
//@version=6
indicator("Centered RSI")

lengthInput = input.int(14, "RSI length", minval = 2)

// Rescales RSI from a 0 to 100 range to a range of -1 to 1
centeredRsi(float src, simple int len) =>
    value = ta.rsi(src, len)
    (value - 50) / 50

plot(centeredRsi(close, lengthInput), "Centered RSI")
hline(0)

Inside centeredRsi, the variable value exists only within the function. It cannot be seen from the rest of the script, and it does not collide with a variable called value that you declare elsewhere. The last line, (value - 50) / 50, is a bare expression with no assignment, and that is what gets handed back to the caller.

This structure is the reason functions make scripts easier to read. The rescaling logic now has a name that says what it does, and the main body of the script reads like a description of the indicator rather than a pile of arithmetic.

Parameters, types, and qualifiers

You may have noticed the words float, simple int, and src in the signature above. Pine Script lets you annotate each parameter with a type, and optionally with a qualifier that says how quickly the value can change. Annotations are optional, because the compiler can usually infer them from how the parameter is used, but they are worth writing for two reasons. They document what the function expects, and they turn a confusing error deep inside the body into a clear error at the call site.

The qualifier matters most for parameters that feed built-in functions with strict requirements. ta.rsi wants its length argument to be a simple int, which means a value that is known at the first bar and does not change. If you declare your own len parameter as simple int, you promise the compiler that callers will pass something that fits, such as a literal, an input.int value, or another simple value. If someone tries to pass bar_index % 10 + 2, which changes from bar to bar, the compiler rejects the call and tells them why. The Pine Script type system guide explains the const, input, simple, and series hierarchy in more detail, and it is the single most useful background reading for writing typed functions.

You can also call a function with named arguments, which helps when the parameter list is long. centeredRsi(src = close, len = 21) and centeredRsi(close, 21) do the same thing. Named arguments cost nothing and make calls self-explanatory, so use them whenever a call has more than two parameters.

Returning more than one value

A single return value is limiting. Many calculations naturally produce several related numbers, such as the middle, upper, and lower lines of a channel. Pine Script handles this with tuples. You return several values in square brackets, and the caller unpacks them the same way.

pine
//@version=6
indicator("Band function", overlay = true)

bands(float src, simple int len, float mult) =>
    basis = ta.sma(src, len)
    dev = mult * ta.stdev(src, len)
    [basis, basis + dev, basis - dev]

[mid, upper, lower] = bands(close, 20, 2.0)

plot(mid, "Basis")
upperPlot = plot(upper, "Upper")
lowerPlot = plot(lower, "Lower")
fill(upperPlot, lowerPlot, color = color.new(color.blue, 90))

The function computes the three lines once and returns them together. The call site declares three variables at the same time, in the same order. If the numbers do not match, the compiler reports a mismatch, and that is one of the clearer errors you will meet. Tuples are also how request.security can fetch several values in one request, and the same unpacking syntax applies there.

Why you should call functions on every bar

This is the rule that separates a function that works from one that quietly misbehaves, and it comes straight from how Pine Script executes. A script runs once for every bar, from the oldest to the newest. Functions that use history, such as ta.ema, ta.rsi, or the [1] operator, keep an internal record of their previous values. For that record to be correct, the function has to run on every bar.

Consider what happens when you call a function that contains ta.ema only inside a condition. On the bars where the condition is false, the function does not run, so its internal history has gaps. When the condition becomes true later, the EMA is computed from a patchy record and gives a different answer from the one you expected. The compiler warns about some of these situations, but not all of them.

The fix is simple. Call the function unconditionally on every bar, store the result in a variable, and apply the condition to the variable afterwards.

pine
//@version=6
indicator("Consistent calls", overlay = true)

smooth(float src, simple int len) => ta.ema(src, len)

// Both calls run on every bar
fast = smooth(close, 10)
slow = smooth(close, 30)

bullish = fast > slow
turnedUp = bullish and not bullish[1]

plotshape(turnedUp, "Cross up", shape.triangleup, location.belowbar, color.green)
bgcolor(bullish ? color.new(color.green, 90) : na)

Here fast and slow are always calculated, and the logic that decides what to display sits outside the function. This pattern also pays off when you test your work, because you can plot fast and slow directly to see whether the numbers look right. The Pine Script execution model guide walks through why history works this way, and it is worth a read if the behavior still feels arbitrary.

What a function can and cannot touch

Functions in Pine Script have a few firm boundaries, and knowing them in advance saves a lot of confusing error messages.

A function can read global variables that were declared above it. It cannot reassign them with :=. In practice this means a function should be a pure calculation: values go in through parameters, and a result comes out through the return. A function that quietly depends on a global is harder to reuse, because you cannot move it to another script without also moving the variable it relies on.

pine
//@version=6
indicator("Explicit inputs", overlay = true)

multiplier = input.float(2.0, "Multiplier", minval = 0.1)

// Passing the multiplier in makes the function portable
scaled(float x, float factor) => x * factor

plot(close + scaled(ta.atr(14), multiplier), "Close plus scaled ATR")

Functions must also be defined before the line where they are called, because the compiler reads the script from the top. If you see an undeclared identifier error on a function name you are sure you wrote, check the order first.

Drawing and output calls are the other boundary. Functions like plot, hline, and bgcolor that draw on the chart cannot be called from inside a function body. The usual workaround is to have the function return the value or color, and let the global scope do the drawing. The local scope plot error has the details. Recursion is not supported either, so a function cannot call itself. A loop usually does the same job.

Methods and where objects fit in

Pine Script v6 also has methods, which are functions declared with the method keyword whose first parameter is an object or a built-in type. Calling a method uses dot syntax, so myArray.sum() reads naturally. Methods are most useful once you start defining your own types, because they let you attach behavior to the data it works on. That is a larger topic, and the custom types and objects guide covers it in full. For most scripts, plain functions are all you need, and they are the right place to start.

Turning repeated code into a function

Here is a practical way to refactor an existing script. Find a block of code that appears twice or more, with only a few values changing between the copies. Those changing values are your parameters. Give the block a name that describes its result rather than its steps, so trendStrength is better than calcStuff. Move the block into a function above its first use, replace each copy with a call, and confirm the plot looks identical before and after. That last step matters. A refactor should not change behavior, so a visual comparison on a few symbols and timeframes is a cheap safety check.

Watch for two traps while you do it. If the original block used var to keep state between bars, the state belongs inside the function only when each call needs its own copy, and the var and varip guide explains how state behaves in that case. And if the block used a variable from the global scope, decide whether it should become a parameter. In nearly every case it should.

Once you have several functions you reuse across scripts, the next step is to move them into a library so that every script imports the same tested version. The Pine Script libraries guide shows how to publish and import them.

Common errors with user-defined functions

Most function problems fall into a short list. A mismatched number of arguments gives an error naming the function and the count it expected. A qualifier mismatch, such as passing a series value where a simple one is required, gives a message about the argument type. Putting a plot call inside the body gives a local scope error. Unpacking the wrong number of values from a tuple gives a mismatch on the declaration line. And a function that seems to return the wrong number is usually a function called conditionally, as covered above.

When one of these shows up in TradingView, the quickest route is to read the message, find the line, and compare the call with the signature. Our guide on why a script will not compile covers the general approach.

Where PineScripter fits

If you have a script full of repeated blocks and you would rather not untangle it by hand, describe the refactor to PineScripter in plain English, for example "move the smoothing and rescaling into a function and call it for both lines". The AI sees TradingView compile errors and fixes them itself, and it edits only the lines that change, so the rest of your script stays as you wrote it. The Code Changes view shows each edit line by line, which makes it easy to confirm that a refactor did not alter anything it should not have.

Whether you write functions by hand or have them drafted for you, the habit is the same. Give each calculation one name, one place, and explicit inputs. Your scripts get shorter, your bugs get fixed once instead of three times, and the next indicator you build starts from parts you already trust.

These tools help you write code faster, but they do not guarantee anything about how a strategy or indicator will perform. Code quality and trading results are separate matters. Always test thoroughly and understand the risks before using any script with real money.

Frequently asked questions

How do you write a function in Pine Script?

Write the name, the parameters in parentheses, then => and the body. A one-line function is `midpoint(float a, float b) => (a + b) / 2`. For several lines, indent the body; the last expression is the returned value.

Does Pine Script have a return keyword?

No. A function returns the value of its last expression. To return several values, end the body with a tuple such as [basis, upper, lower].

Why does my function give different results when called inside an if block?

Functions that use history, such as ta.ema, need to run on every bar. Call the function unconditionally, store the result, and apply the condition to the stored value.

Can a function call plot?

No. Drawing calls such as plot, hline, and bgcolor cannot be used inside a function body. Return the value from the function and plot it in the global scope.

Can a Pine Script function change a global variable?

A function can read globals declared above it but cannot reassign them with :=. Pass values in as parameters and return the result instead.

Describe your indicator in plain English and get Pine Script that compiles →


Disclaimer: PineScripter is a coding tool for Pine Script development. It does not provide financial advice and does not guarantee trading profits. Always backtest strategies thoroughly and understand the risks before live trading.