FlowscopeDocs

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:

Flowscope Script
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 :.

Flowscope Script
let body = (chart.close ?? 0.0) - (chart.open ?? 0.0)
let score = body / context.tickSize +
  (chart.volume ?? 0.0) / 1000.0

Inside 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

Text
script   data     input    state    setup    plot     fill
window   pane     alert    type     fn       on
let      if       else     match    for      in       return
true     false    null

self names the receiver inside methods of a type. strategy(...) at top level is the strategy declaration.

Number literals

FormTypeExamples
Digits without a decimal pointint (64-bit signed)0, 14, 1_000_000
Digits with a decimal pointfloat (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:

UnitMeaningExamples
sseconds30s
mminutes1m, 15m
hhours1h, 4h
Ddays1D
Wcalendar weeks1W
Mcalendar months1M
Ycalendar years1Y

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:

Flowscope Script
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

Text
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

Text
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.

Flowscope Script
setup names = ["Fast", "Slow"]
setup lines = for n in names { plot.line(title: n) }
setup onBinance = if context.exchange == "binancef" 1.0

state

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

Text
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

Text
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

Text
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

Text
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

TypeValuesNotes
int64-bit signed integersCounts, indices, keys
float64-bit floating pointPrices, volumes, ratios
booltrue, falseConditions need a non-null bool
stringUTF-8 textImmutable
colorRGBA colourLiterals, color.*
timeA point in time, UTCchart.time
durationA span of time2h, time.days(3); time + duration is a time
timeframeA bar period4h, tf("15m"), context.timeframe
sessionA daily time windowtime.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

TypeConstructorBehaviour
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[] and rolling<float> may hold null; other element types need non-null values.
  • Reads (xs[i], get, first, last, pop, shift) return T?. xs[i] = v requires i < xs.len.
  • map, filter and reduce take 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():

Flowscope Script
fn bands(mid: float, dev: float, mult: float) -> (float, float) {
  return mid + dev * mult, mid - dev * mult
}

Subscription records

SourceRecordFields
data.ohlcvOhlcvSubtime open high low close volume buyVolume sellVolume buyCount sellCount trades
data.oiOiSubtime open high low close
data.vdVdSubtime open high low close, bucket fields
data.cvdCvdSubtime open high low close, bucket fields
data.statStatSubtime sellLiq buyLiq markPrice fundingRate
data.bookBookSubSnapshot queries (data.book.* methods)
data.volumeVolumeSubSnapshot queries (data.profile.* methods)
data.tradesTradesSubDelivers 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:

LevelOperatorsMeaning
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.0 is (x ?? 0.0) > 5.0.
  • ?? mixed with && or || needs parentheses: (x ?? false) && y.
  • There is no %, no bitwise operators, no ** (use math.pow) and no ++.
  • &&, || and ! take non-null bool operands.
  • .. appears only in for ranges.
  • Assignment is a statement, not an expression.

Statements

let

Text
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

Text
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

Text
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

Flowscope Script
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

FormMeaning
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

HandlerRunsOwn fields read
on x.openOnce when a period of x startsThe forming period
on x.updateWhen the forming period changes; once per period in historyThe forming period
on x.closeOnce when a period of x is confirmedThe period just confirmed
on t.trade(tr)Once per trade, in time ordertr is the trade
  • In x’s own open and update handlers, x.field reads the forming period; in its close handler, the period just confirmed.
  • In handlers of other subscriptions, x.field reads x’s last confirmed period and x.forming.field its developing one.

Execution phases

  1. Setup. Inputs, setup values, state initialisers and surfaces, once.
  2. History. Loaded history is delivered bar by bar to the same handlers, in time order.
  3. 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

FieldTrue when
x.isHistoryDuring history replay
x.isRealtimeWhile the forming period receives live updates
x.isFirstOn the first loaded period
x.isNewIn an update in which x confirmed a period
x.isLastIn an update of the newest, live period; false in close handlers
x.indexPeriod number from the first loaded period, from 0

Setup and eval contexts

SetupEval
Whereinput, data, setup, state initialisers, surfaces, alert, strategy, parameter defaultsHandler, function and method bodies
WhenOnce, at startOn every event
Inputs, setup values, context.*, pure helpersyesyes
Array and map literalsyes (read-only)no; use constructors
Surface and plot constructorsyesno
Subscription reads, ta.*, user fn callsnoyes
Plotting, drawing, alerts, orders, log.*, state assignmentnoyes

Null semantics

  1. Every type T has a nullable form T?. Market fields and collection reads are nullable.
  2. Arithmetic with a null operand yields null.
  3. Ordering comparisons with a null operand yield false.
  4. a == b with one side null is false; a != b is then true.
  5. a ?? b replaces null with b.
  6. Conditions need a non-null bool: test for null or use ?? false.
  7. After if x != null, the local x is narrowed to T. Narrowing applies to locals and parameters, not to subscription fields, state or record fields; copy those to a local first.
  8. A bare null takes its type from context; c ? null : null is an error.
  9. Plotting null leaves a gap.
  10. There is no na(), nz(), ?. or postfix !.

Limits

LimitValue
Source size1 000 000 bytes
Inputs100
Plots200
Fills50
Alerts50, with at most 16 payload fields
Subscriptions30
User types128, at most 64 fields, nested at most 16 deep
Collection capacity50 000
Map key length64 bytes
Indicator length (ta.*)10 000
Nesting of expressions and blocks128 levels
Steps per update10 000 000
Steps over the history replay100 000 000 (25 000 000 in the browser)
Steps of the setup (all declarations)2 000 000
Function calls nested64
String length1 MB
Memory held in values256 MB
Entities per pool5 000
Table rows / columns5 000 / 64
Heatmap cells250 000
Log lines keptthe 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.