Functions, types and enums
Write your own functions with fn, return values early or several at once, add methods to a type, and know how state works inside a function.
A function packs a calculation under a name. Write it with fn, put it above the code that uses it, and call it like a built-in.
A function#
A function with a block returns its last expression, or the value of a return. A function written on one line uses =.
indicator "Two functions"
edition = 1
fn range_percent(length: int): float
let top = tech.highest(high, length)
let bottom = tech.lowest(low, length)
return (top - bottom) / bottom * 100
fn middle(a: float, b: float): float = (a + b) / 2
plot(range_percent(20), name = "20-bar range, %")
plot(middle(high, low), name = "Middle of the bar")The types after the names are optional documentation for the reader. In edition 1 the engine does not check them: (length: int): float tells a reader that the function takes a whole number and gives a number, but a text passed in is not refused.
A parameter can have a default, and an argument can be passed by name:
indicator "Defaults and names"
edition = 1
fn scaled(x: float, factor: float = 2.0): float = x * factor
plot(scaled(close), name = "Doubled")
plot(scaled(close, factor = 0.5), name = "Halved")Leaving early#
return ends the function at once, wherever it is. A function can use it as a guard:
indicator "A guard"
edition = 1
fn safe_ratio(a: float, b: float): float
if b == 0
return na
return a / b
plot(safe_ratio(close - open, high - low), name = "Body as a share of the range")indicator "Absolute value"
edition = 1
fn absolute(x: float): float
return if x >= 0 then x else -x
plot(absolute(close - open), name = "Body size")A function cannot call itself#
A function may call other functions, but not itself, and not through a chain of calls that comes back to it. The checker refuses it with E3310 and names the chain. The reason is that the depth of such a chain would then be decided by the machine that runs the script, so the same script could work on one machine and stop on another. Use a loop, or keep the running value in a state (see State inside a function, below).
indicator "A function that calls itself"
edition = 1
fn total(n: int): int = if n < 1 then 0 else n + total(n - 1)
plot(total(5), name = "Sum")Corrected:
indicator "A loop instead"
edition = 1
fn total(n: int): int
mut sum = 0
for i in 1 to n
sum += i
return sum
plot(total(5), name = "Sum")Functions that call one another without coming back to the first are fine. The engine also counts how many of your functions are in progress at once, one inside another, and stops a bar that goes past 64 with E-LIMIT-DEPTH; see Limits and budgets.
Several values at once#
A function can return a tuple, and the caller takes it apart. The number of names must match the number of values.
indicator "Channel"
edition = 1
display = price
fn channel(length: int): (float, float, float)
let middle = tech.sma(close, length)
let spread = tech.stdev(close, length) * 2
return (middle + spread, middle, middle - spread)
let (upper, middle, lower) = channel(20)
plot(upper, name = "Upper", color = color.red)
plot(middle, name = "Middle", color = color.gray)
plot(lower, name = "Lower", color = color.green)indicator "Too many names"
edition = 1
fn pair(): (float, float) = (1.0, 2.0)
let (a, b, c) = pair()
plot(a)Use _ for a value you do not need: let (upper, _, lower) = channel(20). It also works as a loop variable: for _ in 0 to 4.
State inside a function#
A function that uses tech.* or a state keeps a separate memory for every place it is called from. Two calls are two memories:
indicator "Two calls, two memories"
edition = 1
fn smoothed(x: float): float = tech.ema(x, 5)
plot(smoothed(close), name = "Smoothed close")
plot(smoothed(open), name = "Smoothed open")The two plots do not interfere: each call remembers its own series. A function may not change a mut or state declared outside it (E3104); give it the value as an argument and take the new value as its result. Variables shows the error.
Types with methods#
A type has fields. A function written as fn TypeName.method(self, ...) is a method of that type, and is called with a dot.
indicator "A type with a method"
edition = 1
type Level
price: float
width: float = 1.0
fn Level.top(self): float = self.price + self.width
fn Level.bottom(self): float = self.price - self.width
let level_ = Level(price = close, width = 2.0)
plot(level_.top(), name = "Top", color = color.red)
plot(level_.bottom(), name = "Bottom", color = color.green)You build a record by naming its fields. You can also give a built-in type a method of your own, such as fn array.size(self). Yours then wins over the built-in method of the same name, and the checker warns about it (W-METHOD-SHADOWS-BUILTIN) so you know it happened.
The same dot form calls a text function on a text: "10:30".split(":") is text.split("10:30", ":").
Enums#
An enum is a closed list of names, written one per line. A member may carry a label:
indicator "An enum input"
edition = 1
enum Speed
fast = "Fast"
normal = "Normal"
slow = "Slow"
input speed: Speed = Speed.normal, name = "Speed"
let length = match speed
Speed.fast => 8
Speed.normal => 21
Speed.slow => 55
plot(tech.ema(close, length), name = "Average")The settings panel shows an enum input as a dropdown with the labels. match on an enum is the way to act on it. Choices and loops covers match.
See also: Values and types, Modules and libraries, Inputs.