Language reference
The precise rules of Flowscope Script, from lexical structure and declarations to types, operators, evaluation order, null semantics and limits.
This page defines the language. It is written to be exact rather than gentle; for explanations and worked examples, start with the guides. Every built-in function, constant and record is in the built-in catalog, every compiler message in diagnostics.
Syntax is shown in a compact notation: name stands for an identifier, expr for
an expression, T for a type and ... for repetition.
Source files
A script is a single UTF-8 text of at most 1 000 000 bytes. Its first line is the header:
script "Funding watch"The string is the display name; script.setTitle(text) replaces it at run time.
Everything after the header is a sequence of top-level declarations. There are
no statements at top level, no imports and no other entry point.
Lexical structure
Comments
A comment starts with // and runs to the end of the line. There are no block
comments.
Lines and statements
Statements and declarations end at the end of the line. There are no semicolons and exactly one statement per line.
A long expression continues on the next line when the line ends with a binary
operator, ?, :, ,, (, [ or {. A line must not begin with an operator, ?
or :.
let body = (chart.close ?? 0.0) - (chart.open ?? 0.0)
let score = body / context.tickSize +
(chart.volume ?? 0.0) / 1000.0Inside parentheses, brackets and grouped declaration blocks, newlines between elements are allowed.
Identifiers
An identifier starts with an ASCII letter or _ and continues with letters, digits
or _. Identifiers are case-sensitive. User type names start with an upper-case
letter. Built-in namespace names (ta, math, str, color, time, input,
plot, fill, data, context, entities, log, strategy, …) are predefined.
Keywords
script data input state setup plot fill
window pane alert type fn on
let if else match for in return
true false nullself names the receiver inside methods of a type. strategy(...) at top level is
the strategy declaration.
Number literals
| Form | Type | Examples |
|---|---|---|
| Digits without a decimal point | int (64-bit signed) | 0, 14, 1_000_000 |
| Digits with a decimal point | float (64-bit IEEE 754) | 1.5, 0.25, 1_000.0 |
_ may separate digit groups. A float literal needs digits on both sides of the
point. A leading - is the unary minus operator.
String literals
Strings are enclosed in double quotes and may not span lines. Escapes: \n, \t,
\" and \\; any other is an error. There is no interpolation; use
str.format("{0} / {1:.2}", a, b).
Boolean and null literals
true and false have type bool. null is the absent value; its type comes from
context (see null semantics).
Color literals
#RRGGBB is an opaque colour, #RRGGBBAA a colour with alpha (00 transparent to
FF opaque).
Timeframe and duration literals
A positive integer followed by a unit is a timeframe where a timeframe is expected
and a duration where a duration is expected:
| Unit | Meaning | Examples |
|---|---|---|
s | seconds | 30s |
m | minutes | 1m, 15m |
h | hours | 1h, 4h |
D | days | 1D |
W | calendar weeks | 1W |
M | calendar months | 1M |
Y | calendar years | 1Y |
Units are case-sensitive: 1m is one minute, 1M one month. 60m normalises to
1h. tf("15m") converts a string. Calendar units (W, M, Y) are valid for
subscriptions but not as indicator lengths. 0s is a valid duration: history: 0s
means live data only.
Sessions
A session is a daily UTC window, built with time.session("09:30", "16:00") or
time.sessionMinutes(570, 960). An end before the start wraps past midnight.
Declarations
All declarations appear at top level. input, data, state, setup, plot,
fill, pane and alert also have a grouped form with one declaration per line:
input (
fastLength = input.int(12, title: "Fast", min: 1, max: 200)
slowLength = input.int(26, title: "Slow", min: 2, max: 400)
)script
script "Name", exactly once, first.
input
input name = input.kind(default, title: ..., ...) declares a setting, fixed for a
run. Layout rows input.tab(title:), input.group(title:) and
input.section(title:) are written without a binding. Each effective key (key: or
the binding name) must be unique. See
inputs and visuals.
data
data name = subscribe(data.source, exchange: ..., symbol: ..., timeframe: ..., history: ..., bucket: ..., minSize: ...)Declares a subscription. Omitted exchange, symbol and timeframe follow the
chart. Arguments must be setup values. minSize: applies to data.trades, bucket:
to data.vd and data.cvd. See data and update events.
setup
setup name = expr
setup name = if cond expr
setup name = for k in collection { expr }Computes a value once at start. The if form has no else and yields T?. The
for form builds a family: a read-only map from each k to the body’s value,
such as a set of plots.
setup names = ["Fast", "Slow"]
setup lines = for n in names { plot.line(title: n) }
setup onBinance = if context.exchange == "binancef" 1.0state
state name = expr or state name: T = expr declares a variable initialised once and
kept across handler calls. The initialiser must be a setup expression. Handlers may
assign only to state.
plot
plot name = plot.kind(title: ..., color: ..., on: ..., ...) declares a plot. Kinds:
line, candle, bar, histogram, point, marker, arrow, label, bg.
Feed it from handlers with name.plot(value) (name.plot(o, h, l, c) for candles
and bars). Without on: a plot draws on the price pane.
fill
fill name = fill.between(a, b, color: ...) shades between two line plots.
pane
pane name = pane(title: ..., height: ...) declares a pane under the price chart
sharing its time axis; height is its share of the chart (default 0.3).
window
window name = window(title: ...) {
panelName: chart.panel(...),
...
}Declares a separate window with named panels: chart.panel, table.new or
heatmap.new. Panels are addressed as name.panelName. See
custom axes and charts.
alert
alert name = alert(title: ..., onClose: ..., payload: TypeName) declares an alert;
the binding name is its identity. Fire it from a handler with name.trigger() or
name.trigger(TypeName { ... }). Alerts fire on live data only, at most once per bar.
type
type Name {
field: T
...
fn method(params) -> T { ... }
}Declares a record type, optionally with methods. Values are created with a struct literal and copied on assignment. See user-defined types.
fn
fn name(param: T, param: T = literal) -> R { statements }
fn name(param: T) -> (R1, R2) { statements }
fn name(param: T) { statements }Parameter types are required; defaults must be literals and follow required
parameters. Each call site of a function that calls ta.* keeps its own indicator
state. User functions cannot be called from setup expressions. See
functions.
on
on sub.open { statements }
on sub.update { statements }
on sub.close { statements }
on sub.trade(name) { statements }trade is for data.trades subscriptions. data.book and data.volume have no
handlers. At most one handler per subscription and event.
Types
Primitive types
| Type | Values | Notes |
|---|---|---|
int | 64-bit signed integers | Counts, indices, keys |
float | 64-bit floating point | Prices, volumes, ratios |
bool | true, false | Conditions need a non-null bool |
string | UTF-8 text | Immutable |
color | RGBA colour | Literals, color.* |
time | A point in time, UTC | chart.time |
duration | A span of time | 2h, time.days(3); time + duration is a time |
timeframe | A bar period | 4h, tf("15m"), context.timeframe |
session | A daily time window | time.session("08:00", "16:00") |
Arithmetic operands must have the same numeric type. Convert explicitly with
float(x), int(x) (truncates; null for non-finite or out-of-range values) and
string(x).
Nullable types
T? is T or null. Market fields are nullable. A non-null value is accepted where
T? is expected; the reverse needs narrowing or ??.
Collections
| Type | Constructor | Behaviour |
|---|---|---|
T[] | float[](n) | Array with capacity n, length 0. Adding beyond capacity stops the script. |
rolling<T> | rolling<float>(n) | Fixed window; pushing when full drops the oldest element. |
map<K, V> | map<string, float>(n) | Key-value store with capacity n. |
- Capacities must be setup values and at most 50 000.
- Setup literals
[1.0, 2.0]and{"a": 1}create read-only collections. float[]androlling<float>may holdnull; other element types need non-null values.- Reads (
xs[i],get,first,last,pop,shift) returnT?.xs[i] = vrequiresi < xs.len. map,filterandreducetake lambdas; see the collections family in the catalog.
User types
Field types may be any value type, including nullable and other user types, but not collections. Struct literals give every field. Records are values: assignment and argument passing copy them.
Several return values
A function may return a tuple (T1, T2, ...): return a, b returns two values, and
the caller destructures them at once with let a, b = f():
fn bands(mid: float, dev: float, mult: float) -> (float, float) {
return mid + dev * mult, mid - dev * mult
}Subscription records
| Source | Record | Fields |
|---|---|---|
data.ohlcv | OhlcvSub | time open high low close volume buyVolume sellVolume buyCount sellCount trades |
data.oi | OiSub | time open high low close |
data.vd | VdSub | time open high low close, bucket fields |
data.cvd | CvdSub | time open high low close, bucket fields |
data.stat | StatSub | time sellLiq buyLiq markPrice fundingRate |
data.book | BookSub | Snapshot queries (data.book.* methods) |
data.volume | VolumeSub | Snapshot queries (data.profile.* methods) |
data.trades | TradesSub | Delivers Trade { time, price, size, isBuy } to on t.trade(tr) |
Period records have [n] history on their value fields, a forming view of the
developing period and status fields (isNew, isRealtime, isHistory, isFirst,
isLast, index). Subscription methods include atr, tr, vwap, vwma, mfi,
accDist, change and delta. Full lists are in the
data family.
Constant namespaces
Enumerated arguments are namespaced constants: linestyle.*, shape.*, anchor.*,
extend.*, align.*, axis.*, font.*, easing.*, colorspace.*, volbasis.*,
bookunit.*, and the strategy constants strategy.long, strategy.short,
strategy.fixed, strategy.cash, strategy.percentOfEquity,
strategy.commission.* and strategy.oca.*.
Operators
From lowest to highest precedence:
| Level | Operators | Meaning |
|---|---|---|
| 1 | ? : | Conditional (right-associative) |
| 2 | || | Logical or, short-circuit |
| 3 | && | Logical and, short-circuit |
| 4 | == != | Equality |
| 5 | < <= > >= | Ordering |
| 6 | ?? | Null coalescing (right-associative) |
| 7 | + - | Add, subtract |
| 8 | * / | Multiply, divide |
| 9 | ! - (unary) | Not, negate |
| 10 | . f( ) [ ] | Member access, call, index and history |
Rules:
??binds tighter than comparisons:x ?? 0.0 > 5.0is(x ?? 0.0) > 5.0.??mixed with&&or||needs parentheses:(x ?? false) && y.- There is no
%, no bitwise operators, no**(usemath.pow) and no++. &&,||and!take non-nullbooloperands...appears only inforranges.- Assignment is a statement, not an expression.
Statements
let
let name = expr
let name: T = expr
let a, b = f()Binds an immutable local for the rest of the block. An annotation is needed when the
initialiser does not fix the type, for example let x: float? = null.
Assignment
target = expr, +=, -=, *=, /=. The target must be a state binding, a
field path rooted in one, an element of a state collection or, inside a method,
self or one of its fields.
if and else
if cond { ... } else if cond { ... } else { ... }. cond must be a non-null
bool; braces are required. Null checks narrow locals in the branch.
for
for x in collection { ... }
for i in a..b { ... }
for k, v in map { ... }The range form counts from a up to but not including b. There is no while,
break or continue. Do not call ta.* inside a loop: each call site must run
exactly once per bar.
match
match subject {
pattern => statement
pattern => { statements }
}Arms are tried top to bottom; one arm per line, no commas. Patterns: literals and
constants (0, "bybitf", bookunit.base), comparisons (> 0.0, <= -2.0),
null, alternatives joined with | (0 | 1 | 2), and _ for anything. Patterns
do not bind names or take guards. The statement form need not be exhaustive; the
expression form must be.
return
return, return expr or return a, b leaves the function, method or handler.
Expression statements
A call whose result is unused may stand alone: p.plot(v), log.info("ready"),
xs.push(v), strategy.entry("L", strategy.long).
Expressions
Calls and named arguments
Positional arguments first, then named arguments in any order:
ta.ema(chart.close, 20), input.int(14, title: "Length", min: 1).
Conditional and coalescing
cond ? a : b evaluates only the chosen branch. a ?? b yields a unless it is
null; b is evaluated only when needed.
match expressions
fn regime(rate: float?) -> string {
return match rate {
null => "unknown"
> 0.0005 => "crowded long"
< -0.0003 => "crowded short"
_ => "neutral"
}
}Struct literals
TypeName { field: expr, field: expr }; every field must be given.
Lambdas
|x| expr and |acc, x| expr, as arguments to collection methods such as map,
filter and reduce. The body is one expression and must be pure: no ta.*, no
plotting or drawing, no state assignment.
Indexing and history
| Form | Meaning |
|---|---|
xs[i] | Element i of a collection, T? |
sub.field[n] | A subscription field n periods back, T? |
History applies to subscription value fields, not to locals or state; keep your own
history in a rolling<T>.
Handlers and evaluation order
| Handler | Runs | Own fields read |
|---|---|---|
on x.open | Once when a period of x starts | The forming period |
on x.update | When the forming period changes; once per period in history | The forming period |
on x.close | Once when a period of x is confirmed | The period just confirmed |
on t.trade(tr) | Once per trade, in time order | tr is the trade |
- In
x’s ownopenandupdatehandlers,x.fieldreads the forming period; in itsclosehandler, the period just confirmed. - In handlers of other subscriptions,
x.fieldreadsx’s last confirmed period andx.forming.fieldits developing one.
Execution phases
- Setup. Inputs, setup values, state initialisers and surfaces, once.
- History. Loaded history is delivered bar by bar to the same handlers, in time order.
- New data. On a chart, the script runs again over the history when new data arrives, at most every 0.75 s, so the newest bars reach the same handlers.
Status fields
| Field | True when |
|---|---|
x.isHistory | During history replay |
x.isRealtime | While the forming period receives live updates |
x.isFirst | On the first loaded period |
x.isNew | In an update in which x confirmed a period |
x.isLast | In an update of the newest, live period; false in close handlers |
x.index | Period number from the first loaded period, from 0 |
Setup and eval contexts
| Setup | Eval | |
|---|---|---|
| Where | input, data, setup, state initialisers, surfaces, alert, strategy, parameter defaults | Handler, function and method bodies |
| When | Once, at start | On every event |
Inputs, setup values, context.*, pure helpers | yes | yes |
| Array and map literals | yes (read-only) | no; use constructors |
| Surface and plot constructors | yes | no |
Subscription reads, ta.*, user fn calls | no | yes |
Plotting, drawing, alerts, orders, log.*, state assignment | no | yes |
Null semantics
- Every type
Thas a nullable formT?. Market fields and collection reads are nullable. - Arithmetic with a
nulloperand yieldsnull. - Ordering comparisons with a
nulloperand yieldfalse. a == bwith one sidenullisfalse;a != bis thentrue.a ?? breplacesnullwithb.- Conditions need a non-null
bool: test for null or use?? false. - After
if x != null, the localxis narrowed toT. Narrowing applies to locals and parameters, not to subscription fields, state or record fields; copy those to a local first. - A bare
nulltakes its type from context;c ? null : nullis an error. - Plotting
nullleaves a gap. - There is no
na(),nz(),?.or postfix!.
Limits
| Limit | Value |
|---|---|
| Source size | 1 000 000 bytes |
| Inputs | 100 |
| Plots | 200 |
| Fills | 50 |
| Alerts | 50, with at most 16 payload fields |
| Subscriptions | 30 |
| User types | 128, at most 64 fields, nested at most 16 deep |
| Collection capacity | 50 000 |
| Map key length | 64 bytes |
Indicator length (ta.*) | 10 000 |
| Nesting of expressions and blocks | 128 levels |
| Steps per update | 10 000 000 |
| Steps over the history replay | 100 000 000 (25 000 000 in the browser) |
| Steps of the setup (all declarations) | 2 000 000 |
| Function calls nested | 64 |
| String length | 1 MB |
| Memory held in values | 256 MB |
| Entities per pool | 5 000 |
| Table rows / columns | 5 000 / 64 |
| Heatmap cells | 250 000 |
| Log lines kept | the newest 1 000 |
Exceeding a static limit is a compile error. Exceeding a capacity, a budget or a
size at run time stops the script with an error that names it (budget_exceeded,
call_depth, string_too_long, memory_exceeded). Scripts are shared between
traders, so these limits keep any script, however written, from stalling or
crashing the terminal. See performance.
See also
- Built-in catalog: every function, method, constant and record.
- Diagnostics: every compiler message, with a failing and a fixed script.
- Guides: explanations and complete examples.