millionchart
فارسی

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#

Cuprel
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): float names the function and its two parameters. Inside the function, now and before are 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:

Cuprel
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:

Cuprel
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:

Cuprel
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:

Cuprel · error E3302
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:

Cuprel
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 return as 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