User types, enums and methods
Define your own record types with fields and defaults, fixed sets of choices with enums, and methods you call with a dot.
When a script tracks things that have several properties at once (a price level with its price, the bar it was found on and how often it was touched), keeping each property in its own array soon gets messy. A user type bundles them into one object. An enum gives a fixed list of named choices, and a method is a function you call with a dot on a value, like level.touched(high, low).
#User types
Declare a type at the top level of the script with type, its name and the fields in braces. Each field is a name, a colon, its type and, optionally, = and a default. Put one field per line, or separate them with ; or , on one line: type Range { hi: float; lo: float }.
type Level {
price: float
bornAt: int
touches: int = 0
tint: color = color.orange
}
const lv = Level.new(high, bar_index)
plot(lv.price)- Create an object with
TypeName.new(...). Pass field values in the order the fields are declared, by name in an options object (Level.new(high, bar_index, { touches: 1 }), or all of them:Level.new({ price: high, bornAt: bar_index })), or both. Any field may be left out: it takes its default, ornawithout one.new Level(high, bar_index)is accepted as another spelling ofLevel.new(high, bar_index). - Read a field with a dot:
lv.price. Change it with=or a compound operator:lv.touches += 1. This works on any object expression, such aslevels.get(0).touches += 1. Level.copy(lv)makes a new object with the same field values (a shallow copy: arrays inside are shared, not copied).- A field may hold any type, including arrays, maps, drawings, other user types and even its own type (
next: Node).
A field default must be simple to evaluate: a constant, or a plain value such as close or time without a function call (CS2604). Types are declared at the top level only (CS3007).
//@chartscript 1
indicator("Computed default")
type Band {
mid: float = ta.sma(close, 20) // a call is not allowed as a default
}
const b = Band.new()
plot(b.mid)Set it when you create the object instead: const b = Band.new(ta.sma(close, 20)).
#Objects across bars
An object created on a bar is gone on the next bar, like any plain variable, unless you keep it with state. A state object, and an array of objects declared with state, persists from bar to bar. Changes made to it during a live update are rolled back before the next update, just like state numbers (see Execution model).
Reading a field of an object that is na stops the script with CS5102. Check with na(obj) first when an object may not exist yet.
#Methods
A method is a function that belongs to a type and works on one object of it. Declare it with method, the type, a dot and the method's name: method Level.touched(hi, lo). Inside the body, this is the object the method was called on. Like other functions, a method can be a one-line arrow (=> expression) or a body in braces that ends with return:
type Level {
price: float
touches: int = 0
}
method Level.touched(hi: float, lo: float) => hi >= this.price && lo <= this.price
method Level.distancePct(px: float) {
return (px - this.price) / this.price * 100
}
const lv = Level.new(ta.highest(high, 50)[1])
plot(lv.distancePct(close))Call a method with a dot (lv.touched(high, low)) or as a normal function with the object first (touched(lv, high, low)). Both mean the same.
this exists only inside a method. In an ordinary function there is no object to refer to, and using this is CS1056:
//@chartscript 1
indicator("this in a function")
function doubled() {
return this * 2
}
plot(doubled())Make it a method of the type it works on, method float.doubled() => this * 2, and call it as close.doubled(); or keep a function and pass the value as a parameter: const doubled = (x) => x * 2.
Methods also work on built-in types. Write the type before the dot as you would in an annotation (float[] or array<float>). A method on float[] adds your own operation to every float array:
method float[].spread() => this.max() - this.min()
const closes = array.from(close, close[1], close[2], close[3])
plot(closes.spread())You can declare several methods with the same name for different types (spread for float[] and for your own Range type); the receiver decides which one runs. If your method has the same name as a built-in method of that type, yours wins.
The built-in collection and drawing functions are methods too: arr.push(x) is the same as array.push(arr, x), and tag.set_text("hi") the same as label.set_text(tag, "hi").
#A complete example
This script remembers the last few pivot highs as Level objects, counts how often price comes back to each one, and plots the nearest level above the close.
//@chartscript 1
indicator("Pivot levels", { overlay: true })
const leftBars = input.int(5, "Left bars", { minval: 1 })
const rightBars = input.int(5, "Right bars", { minval: 1 })
const keep = input.int(5, "Levels to keep", { minval: 1, maxval: 50 })
type Level {
price: float
bornAt: int
touches: int = 0
}
method Level.touched(hi: float, lo: float) => hi >= this.price && lo <= this.price
state levels = array.new<Level>()
const ph = ta.pivothigh(high, leftBars, rightBars)
if (!na(ph)) {
levels.push(Level.new(ph, bar_index - rightBars))
if (levels.size() > keep) {
levels.shift()
}
}
let nearest = float(na)
for (const lv of levels) {
if (lv.touched(high, low)) {
lv.touches += 1
}
if (lv.price > close && (na(nearest) || lv.price < nearest)) {
nearest = lv.price
}
}
plot(nearest, "Nearest level above", color.orange, 2, plot.style_linebr)levels is a state array, so it keeps its objects from bar to bar, and lv.touches += 1 changes the object stored in it (objects are references). To sort such an array by a field, pass the field name: levels.sort(order.descending, "price").
#Enums
An enum is a closed list of named values. Declare it at the top level with enum, its name and the items in braces, separated by commas (or line breaks); each item may have a title in quotes after =, which is what users see in the settings and what str.tostring returns.
//@chartscript 1
indicator("Trend mode", { overlay: true })
enum Speed {
fast = "Fast (9 / 21)",
medium = "Medium (20 / 50)",
slow = "Slow (50 / 200)"
}
const speed = input.enum(Speed.medium, "Speed")
const fastLen = switch (speed) {
case Speed.fast: 9
case Speed.medium: 20
default: 50
}
const slowLen = switch (speed) {
case Speed.fast: 21
case Speed.medium: 50
default: 200
}
const fastMa = ta.ema(close, fastLen)
const slowMa = ta.ema(close, slowLen)
plot(fastMa, "Fast", color.accent)
plot(slowMa, "Slow", color.orange, 2)input.enum turns the enum into a drop-down of its titles in the settings dialog. Unlike a string input, a typo in an item name is caught when you compile, and a switch over the enum reads clearly.
What enums can do:
- Compare with
==and!=:if (speed == Speed.fast) { … }. - Serve as the subject of a
switch(case Speed.fast:), and as keys of a map (map.new<Speed, float>()). - Print as their title with
str.tostring(speed); an item without a title prints its name.
Values of two different enums never compare, even if the item names match (CS2008):
//@chartscript 1
indicator("Two enums")
enum Side {
long,
short
}
enum Bias {
long,
short
}
plot(Side.long == Bias.long ? 1 : 0)#Common mistakes
- Declaring a type inside an
ifor a function. Types and enums live at the top level only (CS3007). - Forgetting
state.const levels = array.new<Level>()starts an empty array on every bar, so nothing accumulates. Writestate levels = array.new<Level>(). - Reading a field of
na.lastLevel.pricewhenlastLevelwas never set stops the script (CS5102). Testna(lastLevel)first. - Expecting a copy.
const b = awith an object gives a second name for the same object. UseLevel.copy(a)for an independent one.