Lesson 6: Your own functions
Pack a calculation under a name with fn. Parameters and defaults, a guard with return, several results at once, and functions that remember.
Lesson 6 of 18. When you write the same calculation twice, or a calculation deserves a name, put it in a function. You write one with fn, above the code that uses it, and call it like a built-in function.
A function on one line#
indicator "Percent change"
edition = 1
display = pane
fn percent_change(now: float, before: float): float = (now - before) / before * 100
input back: int = 10, name = "Bars back", min = 1, max = 200
plot(percent_change(close, close[back]), name = "Change %", style = histogram)
level(0, name = "Zero", color = color.gray)fn percent_change(now: float, before: float): floatnames the function and its two parameters. Inside the function,nowandbeforeare the values the call gave.- After
=comes the result. A function written on one line is one expression. - The types (
float) help the reader. In edition 1 the engine does not check them.
A function with a block#
A longer function has an indented block. return gives the result and ends the function at once, which makes it a good guard:
indicator "Small bodies"
edition = 1
display = price
fn body_share(o: float, c: float, h: float, l: float): float
let span = h - l
if span == 0
return na
return math.abs(c - o) / span
input limit: float = 0.1, name = "Largest share", min = 0.01, max = 1.0, step = 0.01
let share = body_share(open, close, high, low)
mark.char(share < limit, name = "Small body", char = "s", place = above, color = color.gray)
plot(close, name = "Close", color = color.gray)body_share gives the body of the bar as a share of its whole range: 0 for a bar that opened and closed at the same price, 1 for a bar with no wicks. A bar whose high equals its low would divide by zero, so the guard returns na first.
A default and a name#
A parameter can have a default value, and the call can name the argument it gives:
indicator "Envelope"
edition = 1
display = price
fn envelope(source: float, length: int, percent: float = 1.0): (float, float, float)
let middle = tech.sma(source, length)
let offset = middle * percent / 100
return (middle + offset, middle, middle - offset)
let (upper, middle, lower) = envelope(close, 20, percent = 0.5)
plot(upper, name = "Upper", color = color.red)
plot(middle, name = "Middle", color = color.gray)
plot(lower, name = "Lower", color = color.green)This function returns three values at once, a tuple: (float, float, float) says so. The caller takes them apart with let (upper, middle, lower) = ...; the number of names must match the number of values. Use _ for a value you do not need: let (upper, _, lower) = envelope(close, 20).
A function that remembers#
A function may use state or a function such as tech.ema. Then every place the function is called from has its own memory:
indicator "Rising in a row"
edition = 1
display = pane
fn rising_count(x: float): int
state count = 0
if x > x[1]
count += 1
else
count = 0
return count
plot(rising_count(close), name = "Closes rising in a row", color = color.blue)
plot(rising_count(volume), name = "Volume rising in a row", color = color.orange)The two calls do not share count: one counts rising closes, the other rising volume. A function may not change a mut or state declared outside it; give it the value as a parameter and take the new value as its result.
Where a function that remembers may stand#
Because such a function remembers the bars it has seen, it must run on every bar. Written on the right side of and, it would run only when the left side is true, and the checker refuses it:
indicator "Called only sometimes"
edition = 1
display = pane
fn rising_count(x: float): int
state count = 0
if x > x[1]
count += 1
else
count = 0
return count
let strong = close > open and rising_count(close) > 2
plot(if strong then 1 else 0, name = "Strong")Call it on a line of its own first, then use the result:
indicator "Called on every bar"
edition = 1
display = pane
fn rising_count(x: float): int
state count = 0
if x > x[1]
count += 1
else
count = 0
return count
let rising = rising_count(close)
let strong = close > open and rising > 2
plot(if strong then 1 else 0, name = "Strong")A function cannot call itself (E3310); use a loop instead. Functions, types and enums has the rest of the rules.
Try changing#
- In "Percent change", call
percent_change(close, open): the histogram becomes the change from the open to the close of each bar. - In "Small bodies", set Largest share to 0.05: fewer bars are marked.
- In "Envelope", leave out
percent = 0.5: the default of 1.0 is used and the bands move further out. - In "Rising in a row", add a third line
plot(rising_count(high), name = "Highs rising in a row"): it gets its own memory as well.
Checkpoint#
- You can write a function on one line and with a block, and use
returnas a guard. - You can give a parameter a default and pass an argument by name.
- You can return several values and take them apart with
let (a, b, c) = .... - You know that each call of a function that remembers has its own memory, and that it must be called on every bar.
Next: Lesson 7: Lists and maps