<!-- Generated by `npm run skills` in apps/web from flowscript/docs/reference.md. Edit that, not this file. -->

# Flowscript language reference

Flowscript is a small language for trading strategies and indicators, designed
for AIs to write. A program reads time series (prices, volumes, event tapes),
computes values over them, and fires actions when its conditions turn true. It
is deterministic: the same inputs always produce the same values and the same
actions, which is why a backtest over history and a live run over new bars make
the same decisions.

What makes it suit a model writing strategies is that the language is small and
bounded. A program has no loops and no mutation, so it always terminates. It
reaches only what its host declares. It is checked before it runs, and compiler
errors are located, carry stable codes, and suggest fixes. Because each run
replays exactly, a model can backtest a change and trust the comparison.

Compilation does not promise that external data or execution will always
succeed. A run refuses invalid inputs and damaged checkpoints, and stops when
it exhausts a resource budget. Hosts must surface that refusal and retain the
last successful checkpoint; they must not treat it as a successful bar or
replace it with fabricated values. Missing observations and indicator warmup
use the language's explicit `na` value.

This reference describes the language as `flowscript-core` and `@flowscript/core`
implement it, without ftw. Every program in a `flowscript` block below is
compiled by the library's tests (`src/docs_reference.rs`) against the host in
[The examples' host](#the-examples-host) and replayed over a synthetic history,
so the examples cannot drift from the compiler. The semantics are specified in `FLOWSCRIPT_V2_SPEC.md` at the
repository root; this is the user's view of them.

- [A first program](#a-first-program)
- [Language versions](#language-versions)
- [The host](#the-host)
- [Values and types](#values-and-types)
- [Expressions](#expressions)
- [Functions, lambdas and fold](#functions-lambdas-and-fold)
- [Metrics](#metrics)
- [Triggers](#triggers)
- [Actions and handlers](#actions-and-handlers)
- [Enums and match](#enums-and-match)
- [Phase machines](#phase-machines)
- [Tapes](#tapes)
- [The prelude](#the-prelude)
- [Built-in functions](#built-in-functions)
- [Diagnostics](#diagnostics)
- [What runs where](#what-runs-where)
- [Coming from Pine Script](#coming-from-pine-script)

## A first program

```flowscript
uses { bars: "^1", paper: "^1" }

const close = bars.close("BTC-USD", "1h");
const fast = ema(close, 5);
const slow = ema(close, 20);

metric("fast", fast);
metric("slow", slow);

when (crossover(fast, slow)) { paper.buy({ symbol: "BTC-USD", quote: 100 }); }
when (crossunder(fast, slow)) { paper.sell({ symbol: "BTC-USD" }); }
```

- `uses { … }` names the modules the program calls, with a version range. A
  module is either a source namespace (`bars`) or a capability (`paper`).
- `const` binds a name to a value. Every binding is a series: `close` is the
  close of every hourly bar, and `ema(close, 5)` is a new series computed from
  it, one value per bar.
- `metric(name, value)` publishes a value: a replay returns it for every bar,
  and a live run returns it at each step.
- `when (condition) { … }` fires its actions when the condition turns true.
  `crossover(fast, slow)` is true at the bar `fast` crosses above `slow`.

Comments are `// …` to the end of the line, or `/* … */`.

## Language versions

Flowscript is versioned as `MAJOR.MINOR`, and is at 0.3 before its beta. A
program says which version it is written in on its first line, before `uses`
(comments may come above it):

```flowscript
flowscript "0.3";
uses { bars: "^1", paper: "^1" }

const close = bars.close("BTC-USD", "1h");
when (crossover(ema(close, 5), ema(close, 20))) { paper.buy({ symbol: "BTC-USD", quote: 100 }); }
```

The version is a requirement string, written as a module's is in `uses`:
`"0.1"` and `"^0.1"` mean the same, Cargo's caret. Before 1.0 each minor
version may change what a program means, so a 0.x requirement names its minor
and compiles only as exactly that version; `"0"` alone is refused. From 1.0 on,
`"^1.2"` compiles as the newest 1.x the compiler speaks that is at least 1.2.

- A version the compiler does not speak is refused where it is written
  (`language.version`), with the versions it does speak, never compiled under
  another version's rules. So is a declaration anywhere but the first line, or
  given twice.
- A program that names none compiles as the compiler's newest version.
  Applications that keep programs record the version each was saved as and
  compile it as that version from then on (`Options::language`, or `language`
  in an `@flowscript/core` host); the program's own line wins over the record.
- New 0.3 artifacts always pin the version they compiled as
  (`language_version`), including programs using the default. Historical
  artifacts with no marker retain their historical unsigned policy. A runtime
  refuses an artifact of a version it does not speak. `Program.language` in
  `@flowscript/core` gives the version a program compiled as.
- A trusted host restoring a known historical unversioned artifact may request
  `ArtifactIdentity::LegacyUnversioned` (`artifactIdentity: 'legacy-unversioned'`
  in a JavaScript host) with explicitly selected language `0.1`. Without an own
  source header, this reproduces the omitted marker and original digest while
  reporting effective language `0.1`. An own source header wins with its normal
  pinned identity. Checkpoint hash checks remain strict; new programs leave
  this restoration option unset.
- A library may name its version on its first line too, and is held to it.

| Version | What changed |
|---|---|
| 0.1 | Versions began (D1543). |
| 0.2 | [Arrays](#arrays) (D1544). Their functions (`buffer`, `filter`, `groupBy`, …) are built-in names, so a program's own `fn filter` is refused. A 0.1 program keeps its own function of such a name, and has arrays besides. |
| 0.3 | [Exact integer math](#exact-integers): checked signed long and uint256 arithmetic, full-precision multiply-divide, exact units, and safe numeric input rules. Earlier saved versions keep their own names and unsigned-input policy. |

## The host

The language has no built-in data and no built-in actions. The application
compiling a program declares what it may read and do. That declaration is the
**host**.

- **Sources** are the reads. Each has a `namespace.name`, a type (number, bool,
  text, a tape of events with typed fields, or a record of typed fields read
  one bar at a time, such as a position's `size` and `pnl`), whether it takes a literal key
  as its first argument (a symbol, an address), and which bar intervals it is
  published at. `bars.close("BTC-USD", "1h")` is a keyed number source read at
  `1h`. A key can instead be a typed **identity**: a source declared with
  `identity: 'pool'` (`'token'`, `'market'`, `'venue'`, `'wallet'`) takes
  `pool("base:0x…")` (`wallet("main")`, keyed by the wallet's name) and
  refuses a bare string or another kind of identity (`market.identity`), so a
  pool's bars cannot be read with a token's address. Optional `args` follow
  the key and the interval, each typed (`duration`, `number`, `quantity`,
  `text`): a TWAP's window, `twap(pool("…"), "5m")`. Reading a source the host does not declare is the `stream.unknown`
  error, with the nearest name it does declare; an interval it does not publish
  is refused too.
- **Capabilities** are the actions, declared as YAML manifests: the module's
  name and version, and each action with its typed arguments. A call that does
  not match a manifest (an unknown action, an argument it does not take, or
  one of the wrong kind, such as a number where it takes `Text`) is refused at
  compile time. An action's `required:` lists the arguments it cannot run
  without (`pair|token` for either of two), after its `defaults:`, and its
  `positive:` the amounts that must be above 0 where the source writes them:
  `paper.buy({ symbol: "BTC-USD" })` is `action.required` and `quote: 0` is
  `action.positive`, each at the call. Its `evaluated:` lists the arguments the host reads as values when the action fires (each decision's `values`), not as the source that computes them: an application's own action, such as a source's `emit`, gets what they were at that bar. A manifest's `streams` are the module's reads: the
  first argument's type is the key's identity (`Market`, `Token`, `Pool`,
  `Wallet`, or `Venue` for either a pool or a market), the rest are optional arguments
  (`Duration`, `Bps`, `Number`, `Text`), and `out` is `Series<number>`,
  `Series<bool>`, `Series<text>` or a record, `"Series<{ size: number }>"`.
- **Catalog** rows resolve `series("tenant:me/name@1")` in Rust hosts through
  `Host::with_catalog(json)` or `extend_catalog(json)`. Each reference is
  unique and declares `type: "number"`, `"bool"` or `"string"`; an optional
  `keyed` is `"pool"`, `"token"` or `"market"`. String enum `variants` are
  distinct, nonempty names, using the same word rules as tape/record enums.
  References contain no whitespace/control characters and use a positive
  decimal major within `u32`. Tenant names occupy one path segment;
  platform names may use nested nonempty segments, such as
  `platform/metrics/volume-5m@1`.
  Invalid rows are refused when the host is built, even if no program reads
  them. A misspelled type never becomes a numeric series.
- **Markets** are the names `market("…")` may take, when the application has any.
- **Units** are the numeric reads' scales, `Host::with_stream_units(json)`,
  in the format of ftw's `platform/stream-units.json`. A row's `identity`
  (`pool`, `token`, `market`, `venue`, `wallet`) is what its read is keyed by,
  and makes the row a read the host declares: a number with the row's bar
  `intervals`, a tape when the row lists its `events` (`{ "px": "number" }`),
  otherwise a number taking the row's `args` (`[{ "name": "window", "type":
  "duration" }]`). A record row is not a call, and a read the host already
  declares keeps its declaration. A row naming any other identity is refused
  when the host is built. A bar's
  `forms_from` names the live mark its forming bar is decided on
  (`when (…, intrabar: true)`), and `unformed: true` marks a bar price the
  forming bar does not deliver, which an intrabar condition may not read.
  A tape's row may name a candle read, `"candles": { "read": "dex.candles",
  "price": "price", "volume": "usd" }`: bars of any period built from the
  tape (`Host::with_candles` declares one directly).

In TypeScript the host is a plain object (see `npm/README.md`):

```ts
const host = {
  sources: [{ name: 'bars.close', type: 'number', keyed: true, intervals: ['1h'] }],
  capabilities: [paperManifestYaml],
};
```

In Rust it is `flowscript_core::Host`, built with `Host::empty()`,
`.with_source(Source::number("bars.close").keyed().intervals(["1h"]))` and
`.with_capability(yaml)`.

### The examples' host

Every example in this reference compiles against this host:

| Source | Type | Keyed | Intervals |
|---|---|---|---|
| `bars.open`, `bars.high`, `bars.low`, `bars.close`, `bars.volume`, `bars.time` (each bar's start, unix seconds) | number | yes | `1h`, `1d` |
| `news.sentiment` | text | no | |
| `trades.prints` | tape: `price` number, `size` number, `side` text | yes | |

Two capabilities: `paper` with `buy({ symbol: Text, quote: Number })` and
`sell({ symbol: Text })`, and `alerts` with `send(message: Text)`.

## Values and types

| Type | Written | Notes |
|---|---|---|
| number | `1`, `0.5`, `1_000_000` | 64-bit float |
| bool | `true`, `false` | |
| text | `"BTC-USD"` | `+` concatenates text: `"filled @ " + fill.price`; a number joins with two decimals, more below 1 (`"p" + 1` is `p1.00`) |
| `na` | `na` | "not available": the one option type |
| record | `{ sum: 0, n: 0 }` | field access is typed: `r.sum`; an unknown field is an error |
| list | `[a, b, c]`, `[[a, b], [c, d]]` | a fixed list of numbers or series, a vector; a list of rows is a matrix. `xs[0]` is an item; see [Lists, statistics and linear algebra](#lists-statistics-and-linear-algebra) |
| array | `buffer(e, 100)`, `filter(xs, x => x.usd > 1000)` | items a run gathers, as many as its data makes (at most 10,000): numbers, text, conditions, records or arrays; see [Arrays](#arrays) |
| series | any of the above, over time | what a source returns, and anything computed from one |
| duration | `seconds(30)`, `minutes(5)`, `hours(4)` | |
| basis points | `bps(25)` | 25 basis points, as an action's argument declared in them takes it (`max_slippage: bps(25)`); in arithmetic it is the number 25, so a rate is written `25 / 10000` |

**`na`** is how the language says a value is not known yet: an `ema(close, 20)`
is `na` for its first 19 bars rather than a made-up number. It propagates
through arithmetic and comparisons, and a condition that is `na` is not true.
A condition is true, false or `na`: `close > open` is `na` where either side
is, `!` of `na` is `na`, and `&&` and `||` answer `na` only when the known side
cannot decide (`false && na` is false, `true || na` is true, `true && na` is
`na`). `c ? a : b` and `if` over an `na` condition are `na`, and a `when` does
not fire on one. A chart and a running strategy read conditions the same way.
`isNa(x)` tests for it and `nz(x, 0)` replaces it. A condition or a text names
what replaces it, `nz(cond, false)`, since 0 is neither.
A condition is not a number: functions that take numbers (arithmetic,
`crossover`, the windows such as `median`) refuse one, and `cond ? 1 : 0` is
its number. A duration such as `"1h"` goes only where a clock takes one
(`cooldown`, `resample`); `hours(1)` is that many seconds as a number (D1290).

**Scalars lift over series.** `close * 2`, `close > open` and `abs(change(close))`
apply bar by bar.

## Expressions

- Arithmetic `+ - * / %` on numbers, comparison `< <= > >= == !=`, logic
  `&& || !` on conditions. A condition is not a number: `(a > b) * 2` is an
  error, and `a > b ? 1 : 0` is the number.
- Comparisons do not chain. `1 < x < 3` is an error; write `1 < x && x < 3`.
- A comparison with `na` is `na` at every bar, so `x == na` is an error; test
  with `isNa(x)`.
- Conditional: `cond ? a : b`, or `if (cond) a else b` as an expression. Both
  arms must have the same type.
- History: `close[1]` is the previous bar's close, `close[k]` the value `k` bars
  ago (`na` before there are `k` bars).
- Blocks: `{ let x = …; return …; }` where a function or lambda body is
  expected. A `let` or a lambda's parameter cannot shadow a binding or a
  `const`, and a `fn` cannot take a built-in function's name.

```flowscript
uses { bars: "^1" }
const close = bars.close("BTC-USD", "1h");
const open = bars.open("BTC-USD", "1h");
const body = abs(close - open);
const bias = if (close > open) "up" else (close < open ? "down" : "flat");
metric("body", body);
metric("bias", bias);
metric("gap", (open - close[1]) / close[1]);
```

A `const` is bound once: a second `const` of the same name is an error, not a
replacement.

There are no loops, no recursion and no mutation. That is deliberate: every
program terminates at every bar with bounded memory, which is what makes it
replayable.

## Lexical structure and grammar

**Text.** A program is UTF-8. Outside strings and comments it is ASCII: a
non-ASCII character there is an error naming it, except Unicode spaces, which
separate tokens as ASCII whitespace does. A leading byte-order mark is ignored.
Comments are `// …` to the end of the line and `/* … */`, which does not nest.

**Tokens.**

| Token | Form |
|---|---|
| name | an ASCII letter or `_`, then letters, digits and `_` |
| number | decimal digits with an optional fraction and exponent: `42`, `0.5`, `1_000`, `1.5e3`, `2E-1`. No sign (that is unary `-`), no leading `.`, no hex. A number too large for a 64-bit float, or so small it would read as `0`, is an error, as is a number run into a name (`2px`) |
| string | `"…"`, with the escapes `\"`, `\\`, `\n` and `\t`; any other `\` is an error |
| `true`, `false` | the two conditions |
| operators | `+ - * / % < <= > >= == != && \|\| ! ? : => = . , ; ( ) [ ] { } @` |

**Precedence**, loosest first. Binary operators on one level associate left,
except comparisons, which do not associate at all.

| Level | Operators |
|---|---|
| 1 | `c ? a : b`, `if (c) a else b` and `match (x) { … }` (right-associative: `a ? b : c ? d : e`) |
| 2 | `\|\|` |
| 3 | `&&` |
| 4 | `< <= > >= == !=`, one per expression |
| 5 | `+ -` |
| 6 | `* / %` |
| 7 | unary `-` and `!` |
| 8 | history `x[k]`, field `r.f`, call `f(…)`, parentheses |

So `-x[1]` is `-(x[1])`, `a || b && c` is `a || (b && c)`, and
`x - 1 - 2` is `(x - 1) - 2`.

**Limits.** An expression nests at most 128 levels as written: each
parenthesis, operand, call and record is one, and so is each operator in a
chain (`a + b + c` is two). Once its `const`s are substituted and its library
calls inlined, one binding may lower to 160 levels and 1 MB. A library
call's large argument that its body reads more than once is bound once
rather than copied to every read, so stacked indicators (`sma(stoch(r, r, r,
14), 3)` over an `rsi`, `hma(hma(hma(close, 9), 9), 9)`) grow with their
nesting, not exponentially. A large `const` of any type is bound once in
the binding that reads it, and a small one is copied where it is read. The
largest program in the repository uses about a sixth of the depth and a
fortieth of the size. A program past either limit is refused with
`syntax.depth` or `expr.size` instead of exhausting the memory of whatever
compiles it.

A program's source is at most 1 MB, and a metric's name at most 128
characters. The whole program lowers to at most 4 MB, summed over every
const, metric, condition and argument, keeps at most 4,000,000 samples of
state, and has at most 1,000 metrics, 2,000 triggers (each handler and each
`if` in a body counts as one) and 2,000 actions. Past any of these the
program is refused with `program.size`, as soon as the compiler passes it;
the refusal names the const, metric or trigger that passed the size and the
three that lowered to the most before it. A `const` that lowers to 2,000
bytes or more, whatever its type, is bound once in each binding that reads
it, however many times that binding reads it, so a step that reads a scored
list of pools through two or three consts holds the list once; a smaller
`const` is copied at every read, as it always was. A `const` that reads
another is still held once per binding that reads either, so a program over
many pools keeps the bindings that read its scored list few.

A run is bounded too, in evaluation steps (a node walked, or an element a
window scans), which count the same on every replay of the same bars: a bar
may take 50,000,000 and a replay 4,000,000,000, and a replay returns at most
1,000,000 decisions and orders. Past one the replay stops with an error that
names the bar.

What a program keeps is bounded where it is written: one expression keeps at
most 1,000,000 samples of state (a window's length, a history depth `[n]`, a
warm-up), and a `window` over a tape spans at most `"7d"`, since it keeps every
event in its span. Both are refused with `expr.size`. A text value holds at most
4,096 bytes; joining past that is `na`, so a `fold` that grows its text cannot
grow without end. A NaN or infinity a source feeds is `na`, and so is an answer with no finite
reading, such as `log10(0)` or `asin(2)`.

## Functions, lambdas and fold

`fn` defines a function. Parameters and the result are typed: `number`,
`bool`, `text` or `count`, or a `Series<…>` of the first three. Annotations
check the element kind: a scalar broadcasts as a constant series, so
`ewma(3, 5)` is valid. `count` names a whole number from 1. A call whose argument,
or a body whose result, is a different kind (a number, a condition, text) from
what the annotation names is refused, and so is a name that is not one of these
(`Series<Number>`). A parameter with no annotation takes anything. A function
has one definition, and its parameters have distinct names.

An annotation names exactly one type. After `:`, a type is required:
`x:`, bare `Series`, `Series<>`, `number bool` and `number + text` are
errors, rather than an untyped parameter or an alternative type. `Series`
contains one of `number`, `bool` or `text`; `count` is scalar. The existing
spellings `boolean` and `string` are aliases for `bool` and `text`.

```flowscript
uses { bars: "^1" }
fn spread(a: Series<number>, b: Series<number>): Series<number> {
  return (a - b) / b;
}
const close = bars.close("BTC-USD", "1h");
metric("stretch", spread(close, sma(close, 50)));
```

`fold(series, seed, (acc, x) => next)` is the one stateful loop: it steps once
per bar, from `seed`, with the last result and the new sample. The seed may be
a record, and the accumulator is typed by it. Every indicator in the prelude is
written with it.

```flowscript
uses { bars: "^1" }
const close = bars.close("BTC-USD", "1h");
// Bars since the close last made a new high. A bar with no close leaves
// the state as it was: without the isNa test one missing sample would make
// the state na for the rest of the run.
const since = fold(close, { high: 0, n: 0 },
  (st, x) => isNa(x) ? st
    : (x > st.high ? { high: x, n: 0 } : { high: st.high, n: st.n + 1 }));
metric("bars_since_high", since.n);
```

A lambda is `(a, b) => expr` or `(a, b) => { let …; return …; }`. It captures
the names around it by value.

`@warmup(n)` after a `const`'s expression, or before a `fn`, states a lookback
the compiler cannot derive.
It can only ask for more history, never less.

### Libraries

`import "name";`, after `uses`, gives a program the functions of a library its
host offers (D1214). A library is `fn` definitions and contract interfaces
only: no `uses`, no `import`, no metrics or triggers. The compiler itself
offers the `std/` libraries, such as `std/erc20` (see Contract calls) and `std/vwap` (below). Its functions are checked like the
program's own, where the program calls them, and a problem inside one is named
as an imported function's.

```text
// The library "swing", as the host offers it.
fn mid(a: Series<number>, b: Series<number>): Series<number> {
  return (a + b) / 2;
}
```

```text
uses { bars: "^1" }
import "swing";
const high = bars.high("BTC-USD", "1h");
const low = bars.low("BTC-USD", "1h");
metric("mid", mid(high, low));
```

`import "std/vwap";` is the VWAP trader's library, built on the prelude's
`periodVwap`, `anchoredVwap` and their σ: `weekStarts(t)` and `monthStarts(t)`
(anchors: the first bar of each UTC week from Monday 00:00, and of each
month), `vwapZ(price, vw, dev)` (where price sits in σ), `vwapDrift(vw, dev,
anchor)` (how many σ a bar the VWAP has walked since its anchor: near 0 on a
balanced day), `ridingAbove(close, vw, dev, len)` and `ridingBelow(…)` (the
share of the last `len` closes beyond the 1σ band: a trend, not a fade), and
`failedPushUp(high, open, close, upper, within)` and `failedPushDown(low, open,
close, lower, within)` (a push into a band that turned back within `within`
bars). Its source and tests are `flowscript/std/vwap.flow` and
`vwap.test.yaml`.

```flowscript
uses { bars: "^1" }
import "std/vwap";
const close = bars.close("BTC-USD", "1h");
const high = bars.high("BTC-USD", "1h");
const vol = bars.volume("BTC-USD", "1h");
// Anchored at each new 20-bar high: the average price of everyone who bought since.
const top = high >= highest(high, 20);
const vw = anchoredVwap(close, vol, top);
const dev = anchoredVwapDev(close, vol, top);
metric("z", vwapZ(close, vw, dev));
metric("riding", ridingAbove(close, vw, dev, 12));
```

A host names its libraries when it compiles (`Options.libraries` in Rust,
`Host.libraries` in `@flowscript/core`), so the same source compiles to the
same program wherever it is given the same libraries. A program imports up to
15. An import the host does not offer (`import.unknown`), one listed twice, a
library function that a second library or the prelude also defines, and a
program `fn` with a library function's name are refused. On ftw.trade a
library is the account's, saved with `save_library`, and a strategy keeps the
version it was saved with until it is saved again.

## Metrics

`metric(name, value)` publishes one value per bar, and each metric has its own
name. A numeric or boolean metric
comes back as numbers (`true` is 1). A metric that is `na` (still warming up)
reads `NaN`, and a text metric comes back as words (`""` while `na`). A
constant is a metric too, a number or a word the same at every bar.

## Triggers

A trigger is a condition and the actions to fire when it holds.

- **`when (cond) { … }` fires on the rising edge**: the bar the condition turns
  from false to true, once per crossing.
- **`while (cond) { … }` fires at every bar the condition holds** and that
  brought something new the condition reads.

```flowscript
uses { bars: "^1", alerts: "^1" }
const close = bars.close("BTC-USD", "1h");
const volume = bars.volume("BTC-USD", "1h");
when (close > rolling_high(close, 24)[1]) { alerts.send("new 24h high"); }
while (volume > 3 * sma(volume, 48)) { alerts.send("volume still heavy"); }
```

Combinators, all usable inside any condition:

| | |
|---|---|
| `crossover(a, b)`, `crossunder(a, b)` | `a` crosses above, below `b` |
| `rising(cond)`, `falling(cond)` | `cond` turned true, false this bar |
| `changed(x)` | `x` differs from its last value |
| `once(cond)` | true the first bar `cond` holds, never again |
| `sustained(cond, n)` | `cond` has held for `n` bars; a count written in the source is a whole number from 1 |
| `cooldown(d)` | false for `d` after the trigger last fired; `d` is a duration of 0 or more (`hours(6)`, `"6h"`, a `config`), and only in a `when` or `while` condition (FS0541) |
| `hysteresis(x, on, off)` | true once `x` passes `on`, until it falls back past `off` |
| `every("1d")`, `every("1w", at: "Mon 14:00")` | each boundary of chain time |

```flowscript
uses { bars: "^1", alerts: "^1" }
const close = bars.close("BTC-USD", "1h");
when (sustained(close > sma(close, 20), 3) && cooldown(hours(6))) {
  alerts.send("three closes above the 20-bar average");
}
when (every("1w", at: "Mon 14:00") && close < sma(close, 200)) {
  alerts.send("weekly buy window");
}
```

`every` counts chain time from the Unix epoch, from the time a run is given
with each bar (or the bar number when there is none). It fires on the first
bar at or after each boundary, once per boundary.

`every`, `crossover`, `crossunder`, `rising`, `falling`, `changed`, `once`
and `triggered` are impulses: true for exactly the bar they mean. A `when`
whose condition contains one fires at every bar the condition is true, on
consecutive bars too, so `when (every("1d"))` over daily bars fires every
day and `when (changed(regime))` at every change. Everything else is a
level, and a `when` over a level fires once per crossing.

## Actions and handlers

An action is a call to a capability: `paper.buy({ symbol: "BTC-USD", quote: 100 })`.
Each action expression is an **action site**, named by where it is in the
program (`when1#1:paper.buy`). A decision carries its site, and an outcome
reported for it names the site back.

Handlers attach to an action and run when it ends: `.onFill`, `.onRevert`,
`.onAbort` and `.onStuck`. A handler reads the outcome as `fill.price`,
`fill.amountOut` and `outcome.reason`, and may fire actions of its own, guarded
by `if` if it likes.

```flowscript
uses { bars: "^1", paper: "^1", alerts: "^1" }
const close = bars.close("BTC-USD", "1h");
when (crossover(close, ema(close, 20))) {
  paper.buy({ symbol: "BTC-USD", quote: 100 })
    .onFill {
      if (fill.price > close * 1.01) { paper.sell({ symbol: "BTC-USD" }); }
      alerts.send("bought at " + fill.price);
    }
    .onRevert { alerts.send("buy failed: " + outcome.reason); };
}
```

A bridge transfer takes time to land, so it has one more handler: `.onSent`
runs when the funds leave, reading what is expected to land as
`fill.amountOut`, and `.onFill` runs when they arrive, reading what did.
Between the two the transfer is `inFlight()`. `bridge.inTransit(route)` reads
how much is on its way along a route, and `bridge.held(route)` what the
route's destination account holds, so a strategy's funds are always somewhere
it can read. This example needs a host with a bridge module, such as ftw's, so
it isn't one of the examples run against this reference's host.

```text
uses { bridge: "^1" }
const out = market("relay:usdg-robinhood-to-usdc-hyperliquid");
when (bridge.feeBps(out) < 20 && !inFlight()) {
  bridge.transfer({ market: out, amount: 500 })
    .onSent { notify("on its way: " + fill.amountOut); }
    .onFill { notify("arrived: " + fill.amountOut); };
}
```

An action that signs names its wallet with `from: wallet("main")`, or takes
the strategy's `default_wallet`. `from: pick(group("Group A"))` sends it from
one wallet of a group, chosen each time it fires; `pick` takes groups and
wallets together (`pick(group("A"), wallet("desk"))`). `pick(group("A"), 3)`
sends it from three of them at once, each its own order and nonce, as a
competing fill needs. The host knows each group's wallets, and chooses by a
hash of the action site and the bar, so a replay of the same bars picks the
same wallets. A group the host does not know, or one smaller than the count,
refuses the action when it fires. The choices are literal, so the wallets a
strategy may sign with are read off the program (D1141). This example needs a
host with wallet groups, such as ftw's.

```text
uses { orders: "^1" }
const P = pool("robinhood:0x…");
when (crossover(dex.close(P, "5m"), ema(dex.close(P, "5m"), 20))) {
  orders.marketBuy({ pair: P, quote: usdc(25), from: pick(group("snipers"), 3) });
}
```

An `if` in a `when` or `while` body guards the actions inside it. It is read
at the bar the `when` fires, so `when (a) { if (b) { … } }` acts the bar `a`
turns true, if `b` holds then, and not later when `b` turns true while `a`
still holds (that is `when (a && b)`). The other actions in the body fire
either way, and every action that fires comes out in the order it is
written, guarded or not: an `if (inPosition()) { sell }` written above a buy
is placed before it.

```flowscript
uses { bars: "^1", paper: "^1", alerts: "^1" }
const close = bars.close("BTC-USD", "1h");
const volume = bars.volume("BTC-USD", "1h");
when (crossover(close, ema(close, 20))) {
  alerts.send("crossed up");
  if (volume > sma(volume, 24)) { paper.buy({ symbol: "BTC-USD", quote: 100 }); }
}
```

Who decides the outcome is the application's choice. With a paper broker, a
replay or a run fills each order itself. An application with its own venue
reports each outcome to the run (`run.report` in TypeScript, `Run::report` in
Rust), and the handlers fire at the last bar stepped.

### Contract calls

`uses { v: abi("Name", "0x…", ["sig", …], { Struct: "(…)" }) }` imports exactly
the listed functions of the contract at that address (D550b, D1249, D1250). A program
can call any function of any contract this way. A call is an action:
`v.fn({ param: value, … })`, with each parameter passed by its name in the
signature.

```text
uses {
  bars: "^1",
  v4: abi("Router", "0x…", [
    "swap(PoolKey key, bool zeroForOne, int256 amount, bytes hookData)",
    "swap(PoolKey key, bool zeroForOne, int256 amount) as swapSimple",
  ], { PoolKey: "(address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks)" })
}
const key = { currency0: "0x…", currency1: "0x…", fee: 3000, tickSpacing: 60, hooks: "0x…" };
when (bars.close("ETH-USD", "1h") > 4000) {
  v4.swapSimple({ key: key, zeroForOne: true, amount: 5000, retries: 2, backoff: "3 blocks" });
}
```

| Solidity | Flowscript value |
| --- | --- |
| `uintN`, `intN` | a whole number; sent as exact decimal text, range-checked by the encoder |
| `address`, `bytesN`, `bytes`, `string` | text (`"0x…"` for addresses and bytes) |
| `bool` | a condition |
| a tuple `(T a, U b)` or a named struct | a record with exactly those fields |
| `T[]`, `T[n]` | a list `[…]`, of exactly `n` items for `T[n]` |

- **Structs.** A named struct is declared once in the fourth argument and used
  by name in any signature, nested up to eight deep. A tuple can also be
  written inline: `"f((address to, uint256 v)[] legs)"`.
- **Overloads.** Two functions with one name are told apart by renaming one
  with `as`. The name in the program changes and the selector stays the
  contract's own.
- **Call options.** `from`, `retries`, `backoff` and `value` sit beside the
  arguments, so a parameter cannot use those names.
- **Views.** A signature ending `view returns (T)` is read like a stream, with
  literal arguments, or a `const` holding one:
  `when (vault.pending("0x…") > 1000)`.
- **Checks.** A missing or extra field, a list of the wrong length, and a
  literal of the wrong type are refused when the program compiles. A value
  that does not fit its type, or that is `na` or fractional, refuses the call
  before anything is signed.
- **Interfaces.** `interface ERC20 = abi(["sig", …], { structs });` lists a
  contract's functions without an address, and `uses { usdc: ERC20("0x…") }`
  binds it to one. Two contracts with the same functions are then two
  names, `usdc.balanceOf(…)` and `weth.balanceOf(…)`, with no clash. A
  program declares its own interfaces or imports them from a library.
- **Standard libraries.** `import "std/erc20";` gives the `ERC20` interface
  (transfer, approve, transferFrom, and the balanceOf, allowance,
  totalSupply and decimals views) and `tokenUnits(amount, decimals)`.
  `import "std/erc4626";` gives `ERC4626`: deposit, mint, withdraw and
  redeem, and views for its assets, balances, conversions and previews.
  Their sources and tests are in `flowscript/std/`. The standard's `from`
  parameter is named `owner`, because `from:` is a call option.
- **Events.** The same parser reads event signatures for sources that decode
  logs: `abiEvent("Swap(bytes32 indexed id, int128 amount0, …)")` in
  `@flowscript/core`, and `abiFunction(sig, structs)` for functions.

## Enums and match

`enum` declares a closed set of words. An unknown variant is a compile error
with the nearest one, and `match` must cover every variant or end in `_`.

```flowscript
uses { bars: "^1" }
enum Regime { hot, quiet, normal }
const close = bars.close("BTC-USD", "1h");
const vol = stddev(change(ln(close)), 24);
const regime = vol > 0.02 ? Regime.hot : (vol < 0.005 ? Regime.quiet : Regime.normal);
metric("regime", regime);
metric("size", match (regime) { hot => 0, quiet => 1, _ => 0.5 });
```

## Phase machines

`phases` declares a state machine. Each `phase` holds the triggers that apply
in it, and a `when` inside one names the phase it moves to. A phase's `let`
bindings start over each time the phase is entered, so `cummax(close)` inside
`Long` is the high since this entry, the entry bar's close included; like any
`let`, one cannot take a `const`'s name. A
phase's `entry { … }` block fires when the machine enters it, right after the
transition's own actions.

```flowscript
uses { bars: "^1", paper: "^1", alerts: "^1" }
const close = bars.close("BTC-USD", "1h");
phases Trade {
  phase Flat {
    when (crossover(close, ema(close, 20))) => Long {
      paper.buy({ symbol: "BTC-USD", quote: 100 });
    }
  }
  phase Long {
    entry { alerts.send("long"); }
    let stop = cummax(close) * 0.95;
    when (close < stop) => Flat { paper.sell({ symbol: "BTC-USD" }); }
  }
}
metric("phase", phase("Trade"));
```

A target that does not exist, a `when` in a phase with no target, and a phase
nothing can reach are compile errors. Every trigger decides against the phases
the bar began in, so a machine moves at most once a bar. An `if` inside a
transition's body fires with the move, when its guard holds then; the machine
moves either way. An `if` inside an `entry` block is a compile error
(`phases.guard`): put the guard on the transition that enters the phase, or in
a `when` inside it.

## Risk limits

An `invariants` block bounds the risk a strategy takes. Each line is a
comparison of something measured against a literal limit, and the block ends
with what a breach does:

```flowscript
uses { bars: "^1", paper: "^1" }
const close = bars.close("BTC-USD", "1h");
when (crossover(close, ema(close, 20))) { paper.buy({ symbol: "BTC-USD", quote: 100 }); }
when (crossunder(close, ema(close, 20))) { paper.sell({ symbol: "BTC-USD" }); }
invariants {
  drawdown() <= pct(5);
  dailyLoss() <= 50;
  trades.perDay() <= 4;
  on_breach: pause;
}
```

- `drawdown()` is the percent fallen from the highest the money put to work
  has been worth: the largest cost basis held, plus everything made or lost.
  The book counted at cost is a peak too, so a gap on the bar after a fill is
  drawdown, and fees count.
- `dailyLoss()` is how far profit has fallen since the UTC day began, in
  quote units. Bars with no time are one day.
- `trades.perDay()` counts the orders sent today. A bound reads it as the
  count the next order would make, so `<= 4` admits four and refuses the
  fifth.
- `position(P).size`, `.entryPrice` and `.pnl` read one market's position.

While a bound is breached, or cannot be read, every entry is refused: a buy
is not placed, and the run records why (`invariant (drawdown: 6.2/5,
on_breach pause)`). A sell is never refused, because a cage that froze a
losing position would be the opposite of one. `on_breach: flatten_and_pause`
also sells every open position at the bar a `drawdown()` or `dailyLoss()`
bound breaks, and at each bar after while it stays broken. `alert` refuses
entries the same way and sells nothing.

A replay, a live run and a test enforce the block on their paper book the
way ftw's host does on its own. A replay lists what the cage refused in
`refused`, and a test expects it with `{ paused: paper.buy, at: 15, by:
drawdown }`. A program can read the same measures itself, in a `metric` or a
condition. Without a paper broker there is no book to measure, and
`unmodelled` says the block is not enforced.

## Tapes

A tape is a source of events (trades, prints, messages) that arrive whenever
they happen, not once a bar. `map` picks a field off each event, `resample` or
`window` buckets the events, and a reducer (`sum`, `mean`, `count`, `first`,
`last`, `distinct`) turns each bucket back into one number.

```flowscript
uses { trades: "^1" }
const prints = trades.prints("BTC-USD");
const buys = map(prints, (p) => p.side == "buy" ? p.size : 0);
metric("buy_volume_1h", sum(resample(buys, "1h")));
metric("prints_15m", count(window(prints, "15m")));
```

Using a tape where a bar series is expected is the `cadence.events` error.

A host may declare a tape field as an enum, `{ "case": { "enum": ["call",
"exit", "note"] } }`: the tag of a sum type. Inside a `map` it reads as an
enum, so `match (e.case) { call => e.usd, exit => -e.usd, note => 0 }` must
cover every word or end in `_`, and `e.case == "cal"` is refused with the
nearest word. An event whose tag is not one of the words is refused when it is
fed to the program.

## The prelude

The prelude is Flowscript source (`prelude/standard.flow`), and its doc
comments are the definitions. In brief:

| Function | |
|---|---|
| `sma(src, len)`, `ema(src, len)`, `ewma(src, len)`, `wma(src, len)`, `rma(src, len)` | moving averages (`ema` is SMA-seeded, as charts draw it; `ewma` seeds on the first close) |
| `stddev(src, len)`, `zscore(src, len)` | dispersion over a window |
| `rsi(src, len)` | Wilder's RSI |
| `atr(src, len)`, `trueRange(h, l, c)`, `wilderAtr(h, l, c, len)` | true range and average true range |
| `vwap(price, vol, len)` | volume-weighted average price over a window |
| `anchoredVwap(price, vol, anchor)`, `anchoredVwapDev(price, vol, anchor)` | the VWAP since the last bar `anchor` held, that bar included (a swing low, a news bar, an entry), and its volume-weighted standard deviation: Pine's `ta.vwap(src, anchor)`. `na` until the anchor has held and traded. `import "std/vwap";` adds week and month anchors and the reads a VWAP trader makes (Libraries) |
| `periodVwap(price, vol, t, secs)`, `periodVwapDev(price, vol, t, secs)` | the session VWAP a chart draws, reset on the first bar of each period (`86400`: from 00:00 UTC), and its volume-weighted standard deviation, so `periodVwap(…) ± 2 * periodVwapDev(…)` are its 2σ bands (Pine's `ta.vwap(src, anchor, stdev_mult)`). `na` until the period has traded. See the starter "VWAP rotation: fade failed pushes to the mean" |
| `corr(a, b, len)`, `cov(a, b, len)` | correlation and covariance over a window; given a list, `cov([a, b, c], len)` is the matrix |
| `beta(y, x, len)` | the hedge ratio: the slope of `y`'s least-squares line on `x` over the window, `ols(y, [x], len)[1]` |
| `roc(src, len)`, `momentum(src, len)` | rate of change, and change over `len` bars |
| `cummax(src)`, `cummin(src)` | high and low since the start (or since a phase was entered) |
| `highest(src, len)`, `lowest(src, len)`, `rolling_sum(src, len)` | the high, low and sum of the last `len` bars (Pine's `ta.highest`, `ta.lowest`, `math.sum`) |
| `cum(src)` | the running total since the start |
| `barssince(cond)`, `valuewhen(cond, src)` | bars since `cond` last held, and `src` at that bar (`na` until it first has) |
| `linreg(src, len)` | the least-squares line's value at this bar over the last `len` bars |
| `stoch(c, h, l, len)` | where `c` sits in the last `len` bars' range, 0 to 100 |
| `pivothigh(src, left, right)`, `pivotlow(src, left, right)` | the value `right` bars back when it is the extreme of the `left + right + 1` bars around it, else `na`; known `right` bars late, as Pine's are |
| `macd(src, fast, slow)`, `macdSignal(src, fast, slow, sig)`, `macdHist(…)` | the MACD line, its signal and the histogram between them |
| `bbUpper(src, len, mult)`, `bbLower(src, len, mult)`, `variance(src, len)` | Bollinger bands around `sma(src, len)`, and the window's variance |
| `vwma(src, vol, len)`, `swma(src)` | volume-weighted and four-bar symmetric averages |
| `wpr(h, l, c, len)`, `cmo(src, len)`, `mfi(src, vol, len)`, `obv(src, vol)`, `tsi(src, short, long)` | Williams %R, Chande momentum, money flow, on-balance volume, true strength |
| `diPlus(h, l, c, len)`, `diMinus(h, l, c, len)`, `adx(h, l, c, len, adxLen)` | Wilder's directional movement |
| `supertrend(h, l, c, factor, len)`, `supertrendDir(…)`, `sar(h, l, c, start, inc, max)` | Supertrend's line and direction (−1 up, 1 down), and the parabolic SAR |
| `hour(t)`, `minute(t)`, `dayofweek(t)`, `dayofmonth(t)`, `month(t)`, `year(t)` | a unix-seconds time's calendar in UTC (`dayofweek` is 1 for Sunday to 7 for Saturday, as Pine's); another zone is a shifted time, `hour(t + 3600*9)` |
| `nyTime(t)`, `chicagoTime(t)`, `londonTime(t)`, `frankfurtTime(t)`, `sydneyTime(t)`, `tokyoTime(t)`, `hongKongTime(t)` | the time on that city's wall clock, daylight saving included, for the calendar above: `hour(nyTime(t))` is New York's hour all year |
| `inSession(lt, start, end)`, `inWeekdaySession(lt, start, end)` | whether a wall-clock time is in [start, end), each HHMM, Pine's session `"0930-1600"`; an end before the start runs past midnight; `inWeekdaySession` keeps to Monday to Friday, Pine's `":23456"` |
| `sessionStarts(active)`, `sessionOpen(o, active)`, `sessionHigh(h, active)`, `sessionLow(l, active)` | a session's first bar, and its open, high and low so far (between sessions, the last one's) |
| `nyseClose(t)` | the New York Stock Exchange's close on `t`'s New York date, HHMM: 1600, 1300 on an early close, 0 on a weekend or a holiday (its rules since 1993, Juneteenth from 2022, and the closures it named, such as 9/11 and Hurricane Sandy) |
| `nyseSession(t)`, `nyseTradingDay(t)`, `nyseHoliday(t)`, `nyseEarlyClose(t)`, `nyseMinutesToClose(t)` | whether the exchange is open at `t` (9:30 to its close), opens that day, is shut on a weekday, closes at 13:00; the minutes to its close (`na` outside the session). The overnight hold: buy when `nyseMinutesToClose(t) <= 15`, sell when `sessionStarts(nyseSession(t))` |
| `barIndex(src)` | the bar's index from 0 |
| `hma(src, len)`, `dema(src, len)`, `tema(src, len)` | the Hull, double and triple exponential averages |
| `cci(src, len)`, `cog(src, len)`, `range(src, len)` | the commodity channel index, the centre of gravity, and the window's high less its low |
| `kcUpper(src, h, l, c, len, mult)`, `kcLower(…)`, `kcw(…)`, `bbw(src, len, mult)` | Keltner channels around `ema(src, len)`, and the Keltner and Bollinger widths as a ratio of the basis |
| `cross(a, b)` | `a` crossed `b` either way this bar |
| `pvt(src, vol)`, `accdist(h, l, c, vol)`, `wad(h, l, c)`, `nvi(src, vol)`, `pvi(src, vol)`, `iii(h, l, c, vol)`, `wvad(o, h, l, c, vol)` | price-volume trend, accumulation/distribution, Williams' A/D, the negative and positive volume indexes, intraday intensity, and Williams' variable A/D |

## Built-in functions

| | |
|---|---|
| Math | `abs`, `min(a, b, …)`, `max(a, b, …)` (`na` if any is), `clamp(x, lo, hi)`, `ln`, `log10`, `exp`, `sqrt`, `pow`, `bps` |
| Rounding | `floor`, `ceil`, `round` (halves away from zero, as Pine's), `sign` (1, −1 or 0) |
| Trigonometry | `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, in radians |
| `na` | `na`, `isNa(x)`, `nz(x, default)` |
| Series | `change(x)`, `rolling_high(x, n)`, `rolling_low(x, n)`, `median(x, n)`, `percentile(x, n, q)` |
| Higher timeframe | `sample(cond, inputs, x => body)`: `body` as if the chart had only the bars `cond` holds on, its answer held between them; `inputs` (a series or a record of them) is read on every bar and is all `body` reads. `newPeriod(t, secs)` and `periodEnds(t, secs)` pick a period's first and last bars, and `periodOpen`, `periodHigh`, `periodLow` and `periodVolume(…, t, secs)` gather its bar |
| Window statistics | `skew(x, n)` and `kurtosis(x, n)` (the biased skewness and excess kurtosis, scipy's defaults), `dev(x, n)` (mean absolute deviation), `percentrank(x, n)` (the percent of the `n` bars before this one at or below it), `highestbars(x, n)` and `lowestbars(x, n)` (how many bars back the extreme is, as 0 or less), `alma(x, n, offset, sigma)` (the Arnaud Legoux average); `n`, `offset` and `sigma` are written in the source |
| Time | `seconds`, `minutes`, `hours`, `every`, `daily`, `during` |
| Triggers | `crossover`, `crossunder`, `rising`, `falling`, `changed`, `once`, `sustained`, `cooldown`, `hysteresis` |
| Tapes | `map`, `resample`, `window`, `sum`, `mean`, `count`, `first`, `last`, `distinct` |
| Lists | `median(xs)`, `mean`, `sum`, `min`, `max`, `count`, `stddev`, `variance`, `mad`, `skew`, `kurtosis`, `percentile(xs, q)`, `wmean(xs, ws)`, `dot(a, b)`, `solve(A, b)`, `inv(A)`, `det(A)`, `transpose(A)`, `ols(y, xs, n)`, `cov(xs, n)`, `corr(xs, n)`; see below |
| Arrays | `buffer(x, n)`, `push(xs, x, n)`, `concat`, `size`, `xs[i]`, `slice(xs, start, end?)`, `reverse`, `sort`, `sortBy(xs, f)`, `unique`, `contains`, `indexOf`, `map(xs, f)`, `filter`, `reduce(xs, seed, f)`, `any`, `all`, `find`, `groupBy(xs, f)`, and the list reducers and `first`, `last`, `count`, `distinct`; see [Arrays](#arrays) |
| Market profile | `tpo.poc`, `tpo.vah`, `tpo.val`, `tpo.vpoc`, `tpo.vvah`, `tpo.vval`, `tpo.vpocShift`, `tpo.high`, `tpo.low`, `tpo.ibHigh`, `tpo.ibLow`, `tpo.poorHigh`, `tpo.poorLow`, `tpo.singleAbove`, `tpo.singleBelow`, `tpo.nakedPocAbove`, `tpo.nakedPocBelow`, each `(high, low, close, volume, t, window: n, …)`; see [Market profile](#market-profile) |
| Trained models | `ml.linear`, `ml.ridge`, `ml.lasso`, `ml.elasticNet`, `ml.logistic`, `ml.svm`, `ml.svr`, `ml.knn`, `ml.boost`, `ml.boostClassifier`, each `(y, [x1, x2, …], window: n, …)`; see [Trained models](#trained-models) |
| Exact integers | `u256` and `i64` checked arithmetic, `u256MulDiv`/`u256MulDivUp`, `u256Units`, `u256Format` and safe number conversions; [details](#exact-integers) |

### Lists, statistics and linear algebra

A list, `[a, b, c]`, is a fixed number of numbers or series: three price
feeds, a portfolio's weights, the factors of a hedge. A list of equal-length
lists is a matrix, row by row. A list has the length it was written with, so
every shape is checked when the program compiles, and nothing about it costs
anything at run time that its items do not (D1524).

```flowscript
uses { bars: "^1" }
// The median of three readings, skipping one that has no price this bar.
const feeds = [bars.open("BTC-USD", "1h"), bars.close("BTC-USD", "1h"), bars.high("BTC-USD", "1h")];
metric("mid", median(feeds));
metric("spread", max(feeds) - min(feeds));
// Arithmetic on a list applies to each item.
const scaled = feeds / feeds[1] - 1;
metric("open_vs_close", scaled[0]);
```

Across assets, the same tools hedge and size a book:

```text
uses { bars: "^1" }
const btc = bars.close("BTC-USD", "1h");
const eth = bars.close("ETH-USD", "1h");
const sol = bars.close("SOL-USD", "1h");
const returns = [btc, eth, sol] / [btc[1], eth[1], sol[1]] - 1;

// SOL hedged with BTC and ETH: a rolling least-squares fit over 200 bars,
// [intercept, b_btc, b_eth], and the residual the hedge leaves.
const fit = ols(returns[2], [returns[0], returns[1]], 200);
const residual = returns[2] - dot(fit, [1, returns[0], returns[1]]);
metric("sol_beta_btc", fit[1]);
metric("residual_z", (residual - sma(residual, 50)) / stddev(residual, 50));

// Minimum-variance weights: solve Σ·w = 1, then scale them to sum to 1.
const w = solve(cov(returns, 200), [1, 1, 1]);
const weights = w / sum(w);
metric("w_btc", weights[0]);
```

- **Reducers** take one list and answer one number from its decided items:
  `median`, `mean`, `sum`, `min`, `max`, `count`, `stddev` and `variance`
  (dividing by `n`, as the windowed `stddev` does), `mad` (the median absolute
  deviation), `skew`, `kurtosis` (excess) and `percentile(xs, q)` with `q`
  from 0 to 1. An `na` item is skipped, so a feed that has no price this bar
  does not blank the median of the others; the answer is `na` only when no
  item is decided. The conventions are numpy's and scipy's defaults: an even
  `median` averages the middle two, and `percentile` interpolates between the
  two nearest ranks. (The windowed `median(x, n)` and `percentile(x, n, q)`
  pick the nearest rank, as they always have.)
- **Arithmetic** `+ - * /` and unary `-` apply item by item: two lists of the
  same length pair up, and a number or series on one side applies to every
  item, as numpy broadcasts. A comparison does not take a list: reduce it
  first, `max(xs) > 100`.
- **Items**: `xs[0]` is the first item and `xs[-1]` the last, an index
  written in the source; on a matrix, `A[0]` is the first row. On a list,
  `[k]` is an item, never a bar back: `feeds[1]` is the close, and
  `btc[1]` is BTC an hour ago.
- **Linear algebra** at each bar: `dot(a, b)` is numpy's (a number for two
  vectors, a vector for a matrix and a vector, a matrix for two matrices),
  `wmean(xs, weights)` the weighted mean, `solve(A, b)` the vector `x` with
  `A·x = b`, `inv(A)`, `det(A)` and `transpose(A)`. A singular matrix makes
  `solve` and `inv` `na` rather than an answer made of rounding error.
- **Rolling** over the last `n` bars at which every series is decided:
  `ols(y, xs, n)` is the least-squares fit of `y` on a constant and the list
  `xs`, `[intercept, b1, …]` (Householder QR, so a nearly collinear pair does
  not lose its digits as the normal equations would); `cov(xs, n)` and
  `corr(xs, n)` are the covariance and correlation matrices. Each is `na`
  until its window fills, and `ols` is `na` while a factor does not move.
- A list is not a value to publish: `metric("w", weights)` is an error that
  asks for an item or a reduction. A `fn` takes and returns a list like any
  other value.

Every evaluator answers these with one Rust implementation
(`flowscript/src/stats.rs`), in plain IEEE arithmetic, so a backtest, a live
run and a chart study agree to the bit. Generated list and matrix IR is reserved
against the compiler's existing size and lowering quotas before eager copying;
oversized expansions are located compilation refusals. The live evaluator charges matrix
elimination and rolling regression work before execution against its watermark
evaluation budget, including computations whose result would be unavailable.

### Arrays

An array is a value a run gathers as it goes, as long as its data makes it:
the last hundred trades a source heard, the wallets among them that bought,
the book grouped by pool (D1544). A [list](#lists-statistics-and-linear-algebra)
has the length it was written with and costs nothing at run time; an array
is held by the run, so it can be built from a stream, filtered and sorted.

```flowscript
uses { bars: "^1" }
const close = bars.close("BTC-USD", "1h");
const volume = bars.volume("BTC-USD", "1h");
// The last 48 hourly bars, oldest first, each a record.
const bars48 = buffer({ close: close, volume: volume }, 48);
// The bars that traded more than usual, and the five busiest of them.
const usual = mean(map(bars48, (b) => b.volume));
const busy = filter(bars48, (b) => b.volume > usual * 1.5);
const busiest = slice(sortBy(bars48, (b) => -b.volume), 0, 5);
metric("busy_bars", size(busy));
metric("busiest_close", busiest[0].close);
metric("median_close", median(map(bars48, (b) => b.close)));
// What a fold keeps can be an array too: every close above 100, the newest 20.
const highs = fold(close, [], (xs, c) => c > 100 ? push(xs, c, 20) : xs);
metric("highs", size(highs));
```

- **Building one.** `buffer(x, n)` is the last `n` samples of `x`, oldest
  first: a series, or a record such as a source's `input.event()`. A sample
  that is `na` is not kept, and `buffer` steps when its input ticks, as a
  window does. Inside a `fold`, an array seed (`[]`) holds what the step puts
  in it: `push(xs, x, n)` is `xs` with `x` after its last item, keeping the
  newest `n` (an `na` `x` is not added). `concat(a, b)` is `a`'s items then
  `b`'s. A list written in the source, `[a, b, c]`, is an array wherever a
  function takes one.
- **Reading one.** `size(xs)` is how many items it holds; `xs[i]` is an item,
  from 0, with −1 the last, and `na` past either end (the index may be any
  number the run computes). `first(xs)` and `last(xs)` are the ends.
  `slice(xs, start, end?)` is the items from `start` up to `end`, −1 counting
  from the last, so `slice(xs, -10)` is the last ten. `contains(xs, v)` and
  `indexOf(xs, v)` (`na` when it is not there) look for an item.
- **Functions of its items.** `map(xs, x => …)`, `filter(xs, x => cond)`,
  `reduce(xs, seed, (acc, x) => next)`, `any`, `all`, `find` (the first item
  the condition holds for, or `na`), `sortBy(xs, x => key)` (lowest key first,
  stable, `na` last; `-x.usd` for the largest first) and `groupBy(xs, x =>
  key)`, which is `{ key, items }` for each key in the order it first appears
  (an item whose key is `na` is in none). `sort(xs)` orders numbers or text,
  `reverse(xs)` turns it round, and `unique(xs)` keeps each different item
  once.
- **Reducing one.** An array of numbers takes the list reducers, `sum`,
  `mean`, `median`, `min`, `max`, `stddev`, `variance`, `mad`, `skew`,
  `kurtosis` and `percentile(xs, q)`, skipping `na` items as a list's do;
  the sum of no numbers is 0. `count(xs)` is the items that are not `na`, and
  `distinct(xs)` how many different values it holds. Reduce an array of
  records by mapping it first: `sum(map(xs, x => x.usd))`.
- **A list of markets reads each of them** (D1545). A read keyed by a list
  written in the source, `aerodrome.rewardApr(pools, interval)` with `const
  pools = ["base:a", "base:b"]`, is the list of the reads, one per market in
  the list's order: `apr[1]` is the second's, `max(apr)` the best, and the
  artifact names every series read. The keys are literals, as any read's key
  is; a list of anything else is refused (`identity.key`).
- **`map` over a list runs its function at compile time**, once per item,
  with the item itself bound to the parameter, so, unlike an array's `map`,
  its body may window and read: `map(price, (p, i) => stddev(change(p) /
  p[1], 24))` is a list of one volatility per market, and the second
  parameter is the item's position from 0. The answer is a list, which is an
  array wherever a function takes one: `sortBy(map(pools, (p, i) => ({ pool:
  p, apr: apr[i] })), (s) => -s.apr)` ranks the markets.
- **A function given to an array runs once for each item**, so it keeps no
  state and reads no feed of its own (`array.lambda`): `filter(xs, x => x >
  ema(close, 20))` is refused. What it reads from outside is computed once,
  before it, and the function reads that value, so bind it to a `const` and
  read the const: `const level = ema(close, 20); filter(xs, x => x > level)`.
- An array is not a number: arithmetic and comparisons refuse one, and so
  does `metric`. Publish one of its items or a reduction of it. An action
  argument its capability declares `Json` takes an array, so a source can
  publish one as a field (below).
- **Bounds.** An array holds at most 10,000 items: `buffer` and `push` keep
  at most that many, written in the source, and a `concat` that would hold
  more is `na`. Every item an operator touches, and a sort's `n log n`, is
  charged to the watermark's evaluation budget before the work, and an array
  a run keeps counts toward its retained state. A program that would walk
  or keep more than a run may is stopped with a refusal at that watermark,
  rather than slowing every run beside it.

In a source, each event is one step, so `buffer(input.event(), 200)` is the
last 200 events and every function above works on them. A field declared
`json` publishes an array:

```text
uses { input: "^1", source: "^1" }
const e = input.event();
const recent = buffer(e, 200);
const byPool = groupBy(recent, (s) => s.pool);
const flows = map(byPool, (g) => ({ pool: g.key, usd: sum(map(g.items, (s) => s.usd)) }));
const top = slice(sortBy(flows, (f) => -f.usd), 0, 5);
while (size(recent) == 200) { source.emit({ pool: e.pool, top: top }); }
```

**A tape's array field.** A host may declare a tape's field an array, of
numbers, text, words or records (`{ "array": { "record": { "pool": "text",
"usd": "number" } } }`, as a ftw source declares `[{ pool: "pool", usd:
"number" }]`), and a program reads it as one. `buffer(tape, n)` keeps a
tape's last `n` events across watermarks as an array of records, newest
last, so the newest event's array is `flows[-1].top`; inside a tape's `map`,
an array function over the event's own field (`size(e.top)`) is one value
per event, which its buckets reduce. An array field left out is empty.

```text
const flows = buffer(sources.top_pools(), 10);
const top = size(flows) > 0 ? flows[-1].top : [];
metric("usd", sum(map(top, (f) => f.usd)));
```

**On a chart.** A study may keep an array too, as long as each line it draws
is a number: `metric("up", size(filter(buffer(price, 20), (x) => x > open)))`.
The compiled plan holds no arrays, so such a study is replayed bar by bar by
the same evaluator a strategy runs on, and draws what that run computes.

### Trained models

A strategy can train a regression or a classifier on its own series and use
its prediction at every bar, in a backtest and live alike (D1561). A model
is written `ml.<kind>(y, [x1, x2, …], window: n, …)`: the target `y`, a list
of predictors, and named settings.

```flowscript
uses { bars: "^1", paper: "^1" }
const close = bars.close("BTC-USD", "1h");
const volume = bars.volume("BTC-USD", "1h");
// The return over the last 6 hours: the target, known at each bar.
const ret6 = close / close[6] - 1;
// What the model sees at each bar.
const features = [
  close / close[1] - 1,
  close / ema(close, 24) - 1,
  volume / sma(volume, 24) - 1,
  rsi(close, 14) / 100 - 0.5,
];
// Learn the next 6 hours' return from the last 500 hours, refitted every
// 24 hours and whenever volume spikes.
const forecast = ml.ridge(ret6, features, window: 500, horizon: 6, retrain: 24,
                          retrainOn: volume > sma(volume, 24) * 3, alpha: 0.5);
// The chance the next 6 hours close higher.
const up = ml.logistic(ret6, features, window: 500, horizon: 6, retrain: 24);
metric("forecast", forecast);
metric("p_up", up);
when (forecast > 0.004 && up > 0.6) { paper.buy({ symbol: "BTC-USD", quote: 100 }); }
when (forecast < 0) { paper.sell({ symbol: "BTC-USD" }); }
```

- **How it trains.** At every bar at which `y` and every predictor are
  decided, the model keeps a row of them, the last `window + horizon` rows.
  A training example pairs one row's predictors with `y` from `horizon` rows
  later, so the model learns what `y` will be `horizon` bars after what it
  sees, and never trains on a target that is in the future of the bar it
  answers at. `horizon` is 1 when omitted; `horizon: 0` fits `y` to the
  predictors of the same bar, as `ols` does.
- **When it retrains.** The model is fitted when its rows first fill, again
  every `retrain` new rows (1 when omitted, so every bar), and again at any
  row where `retrainOn` is true. `retrain: 0` fits once and keeps that model,
  unless `retrainOn` asks for another. Between fits it keeps its parameters,
  which are part of the run's state: a resumed run continues with the same
  model. A fit that the rows do not determine (one class only for a
  classifier, a predictor that does not move for `ml.linear`) keeps the model
  it had.
- **What it answers.** At each new row, the model's prediction from that
  row's predictors; at a bar with no new row it holds its last answer. A
  regression predicts `y`; `ml.logistic` and `ml.boostClassifier` answer the
  probability that `y` is above zero; `ml.svm` answers its signed margin, above zero for "`y` above
  zero". `na` until the first fit.
- **Predictors are standardised** over each fit's examples (centred and
  divided by their standard deviation) and a prediction standardises its row
  with the same numbers, so a penalty treats every predictor alike whatever
  its units. A predictor constant over the window is left out of that fit.
  `window`, `horizon`, `retrain` and every setting are numbers written in the
  source (or `const`s), since they set the model's state and work.

| Model | Settings (default) | What it is |
|---|---|---|
| `ml.linear` | | Ordinary least squares on a constant and the predictors |
| `ml.ridge` | `alpha` (0.1) | Least squares with an L2 penalty, `(1/2n)·‖y − Xw‖² + (alpha/2)·‖w‖²`, on a standardised target |
| `ml.lasso` | `alpha` (0.01) | An L1 penalty, `alpha·‖w‖₁`, which drops predictors: one whose correlation with the target is below about `alpha` gets no weight |
| `ml.elasticNet` | `alpha` (0.01), `l1Ratio` (0.5) | The two penalties mixed, `alpha·(l1Ratio·‖w‖₁ + ((1 − l1Ratio)/2)·‖w‖²)` |
| `ml.logistic` | `C` (1) | L2-regularised logistic regression on "`y` above zero", `C·Σ logloss + ½‖w‖²` (scikit-learn's) |
| `ml.svm` | `C` (1), `kernel` (`"linear"`), `gamma` | A support-vector classifier on "`y` above zero"; `kernel: "rbf"` with `gamma` (1 / the number of predictors when omitted) for a curved boundary |
| `ml.svr` | `C` (1), `epsilon` (0.1), `kernel`, `gamma` | Support-vector regression, `epsilon` in standard deviations of the target |
| `ml.knn` | `k` (5) | The mean target of the `k` training examples nearest the row |
| `ml.boost` | `trees` (100), `depth` (3), `learningRate` (0.1), `minLeaf` (20) | Gradient-boosted regression trees on the squared error of a standardised target: `trees` trees of `depth` levels, each fitted to what the trees before it missed and added at `learningRate`, every leaf holding at least `minLeaf` examples. Trees find thresholds and interactions (a feature that matters only in a trend, a reversal past an extreme) that a linear model cannot |
| `ml.boostClassifier` | as `ml.boost` | Gradient-boosted trees on the log loss of "`y` above zero": the probability that `y` is above zero |

Every evaluator fits with one Rust implementation (`flowscript/src/ml.rs`),
in plain IEEE arithmetic with no randomness: least squares by Householder
QR, ridge by its normal equations, lasso and elastic net by coordinate
descent, logistic regression by Newton's method, and support-vector models by
dual coordinate descent (LIBLINEAR's), boosted trees by second-order
gradient boosting over 32 quantile split points per predictor (XGBoost's
leaves with λ = 1), each stopping at a fixed tolerance or
iteration cap. So a backtest, the optimizer and a live run fit the same
model to the bit. A fit's work is charged against the bar's evaluation
budget before it starts, and a model whose fit could exceed it is refused at
compile time: reduce `window`, the predictors, or a boosted model's `trees`
or `depth`. An RBF model or `ml.knn`
keeps its training rows, so its `window` is at most 1,000. Refitting every
bar is the default and is cheap for `ml.linear` and `ml.ridge`; for the
iterative models and boosted trees, a `retrain` cadence keeps backtests
fast. `window` must hold at least two `minLeaf`s of examples.

### Market profile

A market profile (TPO, time price opportunity) counts the time price spent
at each level: every bar puts one block in each price row its range touched.
`tpo.<level>(high, low, close, volume, t, window: n, …)` reads a level of the
profile of the last whole session (or, with `previous: false`, of the
session still forming) from the last `n` bars, in a backtest and live alike.
The blocks are the bars the program runs on, so the interval sets the
resolution: 30-minute bars for a daily profile and 4-hour bars for a weekly
one are the conventions. The chart's TPO study draws the same profiles.

```flowscript
uses { bars: "^1", paper: "^1" }
const high = bars.high("BTC-USD", "1h");
const low = bars.low("BTC-USD", "1h");
const close = bars.close("BTC-USD", "1h");
const volume = bars.volume("BTC-USD", "1h");
const t = bars.time("BTC-USD", "1h");
// Yesterday's profile, from the last two days of hourly bars.
const poc = tpo.poc(high, low, close, volume, t, window: 48);
const poorLow = tpo.poorLow(high, low, close, volume, t, window: 48);
// Volume sat below where the time was spent: sellers did the business.
const shift = tpo.vpocShift(high, low, close, volume, t, window: 48);
metric("poc", poc);
metric("shift", shift);
// Buy back into value over a poor low left yesterday, target its POC.
when (poorLow == 1 && crossover(close, tpo.low(high, low, close, volume, t, window: 48))) {
  paper.buy({ symbol: "BTC-USD", quote: 100 });
}
when (close >= poc) { paper.sell({ symbol: "BTC-USD" }); }
```

The series are the bars' high, low, close, volume and start time `t` (unix
seconds). The levels:

| Level | |
|---|---|
| `poc` | the time point of control: the middle of the row with the most blocks (a tie goes to the row nearest the middle of the range) |
| `vah`, `val` | the value area's top and bottom: grown from the POC two rows at a time toward the heavier pair until it holds `valueArea` of the blocks |
| `vpoc`, `vvah`, `vval` | the same over volume, each bar's spread evenly over the rows it touched |
| `vpocShift` | `vpoc − poc`: positive when the volume sat above where the time was spent |
| `high`, `low` | the session's range |
| `ibHigh`, `ibLow` | the initial balance: the first two bars' range |
| `poorHigh`, `poorLow` | 1 when the extreme row has two or more blocks (no single-print tail: an unfinished auction, which price tends to revisit), else 0 |
| `singleAbove`, `singleBelow` | the nearest single print (a one-block row inside a profile, not its tail) above or below the close that no later bar has traded through, over every whole session in the window |
| `nakedPocAbove`, `nakedPocBelow` | the nearest session POC above or below the close that no later bar has traded at |

Settings, each a literal or a `const`:

- `window: n` (required): the bars kept. A session is read only once it is
  whole in the window, so keep at least two sessions of bars (96 on 30m for
  a daily profile); more widens the search for single prints and naked POCs.
- `session`: `"day"` (UTC, the default), `"week"` (from Monday 00:00 UTC),
  `"window"` (the whole window as one profile), or a length in seconds;
  `offset` shifts where a numbered session starts.
- `previous`: `true` (the default) for the last whole session, `false` for
  the one still forming.
- `row`: the row height in price; 0 (the default) picks a round number near
  a 36th of the median session's range. At most 2000 rows a profile.
- `valueArea`: the value area's share, 0.7 by default.

A level not there to read (no session whole yet, no single print above,
a session with no volume) is `na`.

### Exact integers

Ordinary numbers use IEEE 754 binary64. Consecutive integers are safe only
through 9,007,199,254,740,991 (2^53 − 1). In an 18-decimal token that is about
**0.009 tokens**, so balances and contract base units must use exact integers.

Flowscript 0.3 provides checked `u256` (0 through 2^256 − 1) and `i64`
(−2^63 through 2^63 − 1, a signed `long`) functions. Their integer results
are canonical **decimal text**, preserving every digit through action arguments,
JSON, saved state and replay. They are a math library over text, not new
operator-overloaded types: use `u256Add`/`i64Add` and `u256Cmp`/`i64Cmp`.
Ordinary `+` on text concatenates; ordinary text ordering is lexical.

```flowscript
flowscript "0.3";
import "std/math";
const budget = parseUnits("1.000000000000000001", 18);
const minimum = basisPoints(budget, 9970);
metric("minimum", formatUnits(minimum, 18));
metric("signed", i64Add(long("-9223372036854775808"), 1));
metric("aboveHalf", u256Cmp(minimum, parseUnits("0.5", 18)) > 0);
```

- **Inputs.** Exact integers accept decimal text; unsigned integers also accept
  `0x` hexadecimal text. Use quoted text or exact source fields for large
  values. In 0.3 integer arguments supplied as numbers must be finite safe
  integers. A literal beyond that range is refused at compile time; an
  unavailable or invalid dynamic operand produces `na`.
  All numeric literals and expressions are floats before this check; it
  validates the represented value, not the original decimal spelling. Even
  a fractional literal can round to an integer first. Text is required when
  every input decimal digit matters.
- **Unsigned arithmetic.** `u256Add`, `u256Sub`, `u256Mul`, `u256Div`,
  `u256Mod`, `u256Min`, `u256Max` and `u256Cmp` are exact and checked.
  Division rounds down. `u256DivUp(a, b)` rounds up.
- **Full precision products.** `u256MulDiv(a, b, denominator)` computes the
  complete 512-bit product before dividing, rounding down. `u256MulDivUp`
  rounds up. An intermediate product beyond uint256 is valid when the final
  answer fits. Division by zero or a final answer beyond uint256 is `na`.
- **Signed long arithmetic.** `i64Add`, `i64Sub`, `i64Mul`, `i64Div`,
  `i64Mod`, `i64Neg`, `i64Abs`, `i64Min`, `i64Max` and `i64Cmp` are checked.
  Signed division truncates toward zero; the remainder has the dividend's
  sign. `i64Div("-9223372036854775808", -1)` overflows; its mathematical
  remainder is `"0"`. Negating or taking the absolute value of that minimum
  also overflows. No operation wraps or saturates.
- **Units and formatting.** `u256Units(amount, decimals)` converts an unsigned
  decimal amount to base units without rounding. `u256Format(value, decimals)`
  produces exact whole-token decimal text, trimming trailing fractional zeros.
  Decimal places are whole numbers from 0 to 77. Excess nonzero fractional
  digits refuse; scientific notation in text refuses. A numeric amount uses
  its shortest decimal representation and cannot restore digits already lost
  in floating-point calculations. Use text for exact input.
- **Display conversions.** `u256Number(value, decimals?)` and `i64Number(value)`
  explicitly round to a float for charts. Never route that result back into
  a monetary amount. `u256SafeNumber(value)` and `i64SafeNumber(value)` instead
  return `na` outside the safe integer range.
- **Failure.** Invalid operands, overflow and division by zero produce `na`.
  An action with an `na` argument refuses before signing. No value is silently
  replaced with zero.

`import "std/math";` adds `uint256`, `long`, `parseUnits`, `formatUnits`,
`mulDiv`, `mulDivUp`, `basisPoints` and `basisPointsUp`. Basis-point functions
divide the full product by 10,000; the `Up` form rounds up. `std/erc20`'s
older `tokenUnits` helper uses ordinary floating-point math; use `parseUnits`
for exact amounts.

Programs explicitly saved as 0.1 or 0.2 keep their earlier `u256` numeric
operand semantics and function names. New exact helpers require 0.3, and
0.3 artifacts record distinct exact operations, so a saved historical artifact
is not silently reinterpreted with the stricter input policy.

The compiler publishes its own list: `flowscript_core::vocabulary()` returns
the built-ins, the prelude source and the diagnostic registry, which is what an
editor or a code-writing model should read rather than a copy.

## Diagnostics

Every compile error has a stable code (`FS0412`), a slug (`record.field`), a
message, and a position. A code never changes meaning and is never reused, so
tooling matches on it rather than on the message. The registry is
`flowscript_core::diagnostic_codes()`.

```flowscript error
uses { bars: "^1" }
const close = bars.clsoe("BTC-USD", "1h");
```

That is `stream.unknown`: "did you mean `bars.close`?".

### Warnings

Some programs compile and then do something other than what they seem to
say. `flowscript_core::warnings_in(source, &host)` (`program.warnings` in
`@flowscript/core`) lists them, each located and coded like an error, and the
program still compiles: the author may mean it.

| Code | Slug | What it finds |
|---|---|---|
| `FS0901` | `lint.never` | a condition that is never true: `x > highest(x, n)` or `x < lowest(x, n)` (this bar is in the window), or one written of literals that is false |
| `FS0902` | `lint.repeat` | a `while` that buys or opens a position with no position guard, so it sends the order at every bar its condition holds |
| `FS0903` | `lint.unused` | a `const` nothing reads (name it `_x` to keep it) |
| `FS0904` | `lint.na` | a divide or remainder by a literal 0, which is `na` at every bar |
| `FS0905` | `lint.unit` | `bps(n)` under arithmetic or a comparison, where it is the number `n`, not the rate `n / 10000` |

```flowscript warning
uses { bars: "^1", paper: "^1" }
const close = bars.close("BTC-USD", "1h");
when (close > highest(close, 20)) { paper.buy({ symbol: "BTC-USD", quote: 100 }); }
```

That is `lint.never`, at `close > highest(close, 20)`: compare with
`highest(close, 20)[1]`.

A replay says the rest: its `quiet` lists each `when` or `while` with actions
that fired at no bar, with how its condition decided (`na` at every bar,
never true, or true and held back by a cooldown, guard or phase). A
strategy that never trades says why instead of returning no orders.

## Common pitfalls

Each of these compiles, and each does something other than what it seems to
say. They matter most in a strategy that trades.

- **`while` repeats** (`FS0902`). `while (rsi(close, 14) < 30) { paper.buy(…) }` buys at
  every bar of a long dip. Entries belong in a `when`, which fires once per
  crossing; keep `while` for an action meant to repeat, with a guard that
  turns false once it has done its job.
- **`when` fires again on every new crossing.** An entry that can turn true
  twice before its exit adds to the position each time. Gate it on
  `!inPosition()` where the host keeps a book, or cap entries in the broker
  (`pyramiding`).
- **A window includes its own bar** (`FS0901`). `close > highest(close, 20)` is never
  true, since this bar's close is in the window. Compare with the window as
  of the bar before: `highest(close, 20)[1]`.
- **`na` is not false.** `!(close > avg)` is `na`, not true, while `avg` warms
  up, so a `when` on it waits. Write the condition you mean (`close < avg`),
  and use `nz` or `isNa` where a missing value should count as something.
- **A fold that does not test for `na` stays `na`.** `fold` carries its state
  from bar to bar, so one missing sample makes the state `na` for the rest of
  the run unless the step leaves the state as it was (`isNa(x) ? st : …`). The
  prelude's indicators all do this.
- **Some windows are partial at first, some `na`.** `sma`, `stddev` and the
  other averaging windows average what they have until the window fills,
  while `highest`, `lowest` (`rolling_high`, `rolling_low`), `median` and
  `percentile` are `na` until it fills, as `ema`, `rsi` and `atr` are until
  they warm up. Gate on `barIndex(close) >= n` where the first bars must not
  trade, and `nz` or scale what a window has (`cummax(x) - cummin(x)` is the
  range since the start) where they must.
- **Time needs the bar's time.** `every`, `cooldown`, `daily` and `during`
  count in the time each bar carries (`time` in a replay's history, the
  `time` of `step`). Without one a bar's time is its number, so
  `every("1d")` fires once and `cooldown(hours(1))` waits 3600 bars.
  Run times must be nondecreasing whole seconds from 0 through
  9,007,199,254,740,991, the largest consecutively exact integer the numeric
  representation supports. Closed and forming bars refuse an invalid time
  before changing state.
- **A watermark has one input budget.** All its sources together may supply
  at most 1,000,000 values and 64 MiB of logical allocation charges: 64 bytes
  per value and record entry, plus text/key bytes. Text and keys are limited
  to 4,096 UTF-8 bytes, nesting to 128 levels. Source keys are billed by their
  canonical stream/key identity, including when restored. Internally generated
  clock, position, phase and lifecycle views have a separate allowance of the
  same size, so they cannot make an admitted external observation impossible
  to restore. These admission budgets also apply alongside retained state and
  evaluation-work limits.
- **A run has one retained-state allowance.** Folds, history, held reads,
  private caches, shared live state, the last input view and paper-book state
  share 256 MiB of logical charges. Each value and map/cell entry costs 64
  bytes plus text and key bytes, even when allocations are shared. Shared
  handles and reservation entries also pay for their metadata, so total limit
  boundaries can differ by that overhead from a private materialization.
  Retained values may nest at most 32 levels. A resource fault after evaluation begins
  stops the run; recover explicitly from the last successful checkpoint.
  Optional shared memo/version/index caches have a separate 64 MiB allowance
  per shared runtime. Pressure can discard optional cache entries; it never
  discards a live fold or changes a decision.
- **Checkpointing can refuse.** Encoded checkpoints are bounded to 128 MiB
  while writing. Rust `Run::snapshot()` returns `Result`; npm returns
  `{ ok, snapshot }` or `{ ok: false, error }`. Hosts must handle refusal
  before replacing a durable checkpoint. Restore checks encoded and expanded
  input limits and rebuilds live accounting from the actual values.
- **An order is sized by a literal.** A paper broker sizes from a number (or
  a constant or `param` bound to one) and refuses a program with an order
  sized by anything computed before its first bar. Size by equity with the
  host's percent order where it has one, and sell a share of a position
  with its percent-of-position sell (a `{ position: "pct" }` size).

```flowscript
uses { bars: "^1", paper: "^1" }
const close = bars.close("BTC-USD", "1h");
const top = highest(close, 20)[1];
const n = fold(close, 0, (st, x) => isNa(x) ? st : (x > top ? st + 1 : st));
metric("breakouts", n);
when (barIndex(close) >= 20 && close > top && !inPosition()) {
  paper.buy({ symbol: "BTC-USD", quote: 100 });
}
when (close < sma(close, 20)) { paper.sell({ symbol: "BTC-USD" }); }
```

## Testing a program

A test feeds a program sources you write and says what it should do with
them. The same runner is in `flowscript-core` (`testing::run`),
`@flowscript/core` (`flowscript.test(source, suite, host)`) and ftw's CLI
(`flowscript test program.flow program.test.yaml`). A check is compiled into
the program as one more metric and replayed by the evaluator a backtest uses,
so a passing test says what the real run does. `flowscript.suite(text)` reads
a suite's text into the object `test` takes, or says why it is not one, for an
editor that keeps the cases rather than the text.

A suite is JSON or YAML: an optional `program` (the source every case runs,
when none is given beside it), an optional `paper` broker, and `tests`. Here
is one for the strategy in [A first program](#a-first-program):

```yaml
paper:
  cash: 1000
  fee_bps: 10
  orders:
    paper.buy: { side: buy, price: bars.close, size: { quote: quote } }
    paper.sell: { side: sell, price: bars.close, size: all }
tests:
  - name: buys the rally, sells the fall
    bars: 60
    ohlc: { steps: [[0, 100], [20, 120], [40, 90]] }
    paper: true
    expect:
      - { check: "fast > slow", at: 21 }
      - { fires: paper.buy, at: 20, args: { quote: 100 } }
      - { fills: paper.sell, at: 40 }
      - { check: "close > 0", at: always }
      - { warns: none }
  - name: a flat market never trades
    sources: { bars.close: { constant: 100 } }
    expect:
      - { fires: paper.buy, count: 0 }
      - { metric: fast, at: last, equals: 100 }
```

**A case** has a `name`, the `sources` it feeds and what it `expect`s. Bars are
counted from 0. `bars` is how many (the longest written-out source, or 100).
`time` is a bar's length in seconds or a list of closes, for `every` and
`cooldown`. `paper` is a broker's terms, or `true` for the suite's (or the
host's). `program` gives a case its own source: a library's tests are cases
whose programs import it.

`runs: 50` runs a case 50 times, each `walk` with the next seed, so an
expectation is held over many random markets rather than one, as a property
test is (D1257). It passes only when every run does, and a failure names its
run: `run 17 of 50: \`exposure <= 0.2\` is false at bar 31`.

```yaml
  - name: never more than a fifth of equity in, over any market
    bars: 200
    runs: 50
    ohlc: { walk: { from: 100, step: 2 } }
    paper: true
    expect:
      - { check: "exposure <= 0.2", at: always }
```

**A source** is named as the program reads it (`bars.close`, or
`bars.close|BTC-USD` for one key of it) and is:

| Written | Feeds |
|---|---|
| `[100, 101, null, 103]` | numbers; `null` is `na`; a short list holds its last value |
| `[true, false]`, `["hot", "cold"]` | a bool or text series |
| `[{ a: 1, b: "x" }, …]` | a record, one per bar |
| `[[3, { price: 10, size: 2 }], …]` | a tape: each event at its bar |
| `{ constant: 100 }` | the same number at every bar |
| `{ ramp: [100, 120] }` | a straight line from the first bar to the last |
| `{ sine: { mid: 100, amp: 5, period: 20, phase: 0 } }` | a wave |
| `{ steps: [[0, 100], [20, 120]] }` | each value from its bar on |
| `{ walk: { from: 100, step: 1, seed: 7 } }` | a random walk; a seed walks one path everywhere |

Any generator takes `na: [bar, …]` to blank those bars. `ohlc: <a close
series>` feeds `bars.close` and derives `bars.open` (the close before),
`bars.high`, `bars.low` and `bars.volume` (1000) from it; `ohlc: { source:
pool, close: … }` does the same for another source's bars. A read the host
adds for an action and the program never names (an order's live price, its
pool) is fed `na` when a case leaves it out, and the report lists it under
`unfed`. A read the program names is the case's to give.

**An expectation** is one of:

| Written | Holds when |
|---|---|
| `{ check: "<condition>", at: 12 }` | the condition is true at bar 12 |
| `{ check: …, at: ever \| always \| never \| last }` | at some bar; at every bar it is decided, and at least one; at none; at the last (`ever` without `at`). `from` and `to` bound the bars |
| `{ metric: fast, at: 12, equals: 101.5, within: 0.01 }` | a metric's value (within 1e-9 relative unless `within`); `at: last` |
| `{ metric: fast, at: 3, na: true }` | a metric is `na` there |
| `{ fires: paper.buy, at: 20, count: 1, args: { quote: 100 } }` | an action (`paper.buy`, `buy`, or its site) fired: at a bar, that many times (`count: 0` is never), with those arguments. A contract call's arguments are compared as they were at the bar it fired, `{ amount: 250000000 }` for `tokenUnits(250, 6)`; a record or list is compared field by field |
| `{ fills: paper.buy, at: 20, count: 1 }`, `{ reverts: … }` | the paper broker filled, or reverted, that action's orders |
| `{ paused: paper.buy, at: 15, by: drawdown }` | the invariant cage refused that action's entry, by that invariant when `by` is given; `count: 0` is never |
| `{ refuses: FS0103, line: 3 }` | the program is refused with that code or slug, on that line |
| `{ warns: FS0901 }`, `{ warns: none }` | the compiler warns with that code or slug; or not at all |

A check reads the program's `const`s and sources, not its metric names. Every
case but a `refuses` one must compile.

**The report** gives each case's `failures`, each saying what happened
instead (`` `paper.buy` fired 0 times, not 1 ``, `` `fast > slow` is false
at bar 21 ``), or why the case could not run (`expect: null`). The CLI exits
1 when one fails.

A `fires … at` that fails also says what the action's `when` read at that
bar: each comparison in its condition and the values it compared (D1255).

```text
`paper.buy` did not fire at bar 3; it fired at 7. `fast > slow && close < 20` at bar 3:
`fast > slow` = true, `fast` = 18.5, `slow` = 17.2, `close < 20` = false, `close` = 25
```

## What runs where

`flowscript-core` and `@flowscript/core` compile any program in this reference,
and replay or run all of it except the parts each says it does not model:

- **Replay** (`replay`, `flowscript.replay`) evaluates a whole history at once.
  With a paper broker it fills the orders and runs their handlers. Give it
  each bar's close as `time` (`replay_timed` in Rust) for `every` and
  `cooldown` to count in chain time. A metric or trigger that reads only
  number series is computed for every bar at once by the compiled study plan,
  and the rest is stepped bar by bar; the answers are the same to the bit, so
  a long backtest costs about what a chart study does (D1270).
- **Live run** (`live::Run`, `flowscript.run`) steps one bar at a time and makes
  the replay's decisions on the same bars. Outcomes come from a paper broker or
  from the application (`report`).
- **Not modelled here yet**: `maintain` blocks are listed in the result's
  `unmodelled` with the reason, never silently dropped. So are invariants
  in a run with no paper broker.

Some of the language is for a full execution host such as ftw's and needs data
this library does not supply on its own: position context (`inPosition()`,
`avgEntry()`, `unrealizedPnl()` …) reads the strategy's own execution stream,
`param(…)` and `config(…)` read a configuration stream, and `pool(…)`,
`token(…)` and `market(…)` name an application's venues.

## Parameters and search spaces

`const lookback = param(7, { min: 3, max: 30, step: 1 });` declares a
parameter: its value is the default until a run's settings change it. The
bounds say what may change it and what an optimizer searches:

- `min` and `max` bound an edit, and the default sits between them.
- `step`, with both, makes min to max by step the search space (at most 1000
  values).
- `options: [...]` lists the values instead, up to 64 of the default's type,
  the default among them. It is not combined with min, max or step.

The compiler refuses any other bound (`param.bounds`, `param.step`,
`param.options`). A host that runs a strategy may bind each declared value
in place as a constant before compiling, as ftw.trade does, so a parameter can
size a window (`close[lookback]`); read as a configuration stream it can't.
On ftw.trade the declarations are the strategy's parameters, and an
optimization with no space named searches the declared steps and options.

## Coming from Pine Script

Most Pine indicators port line for line, and an importer does the porting
(D1194): `pineToFlowscript` in `@ftw-trade/charts/pine` translates the common
v5/v6 subset of an indicator to a chart study and of a strategy to a
ftw.trade strategy, and names, by Pine line, what it could not take
(`while`, a `for` it cannot port, `request.security` of another symbol,
`:=` on a name that is not a `var`). A `for i = 0 to len - 1` that adds, or
takes the max or min of, one series' `x[i]` becomes `rolling_sum`, `highest`
or `lowest`; a `for` over constant bounds (at most 64 steps) that only
accumulates is unrolled. A `var` and its `:=`, `+=` … updates, under `if`s too, become one
`fold` that applies them in order each bar: `var int n = 0` with
`n := up ? n + 1 : 0` is `const n = fold(close, 0, (n, _) => nz(up, false) ? n + 1 : 0);`. The charts site's Flowscript studio
and ftw.trade's Strategies page take a pasted script, and an agent
calls `get_pine_translation` over MCP. The differences are deliberate:

- Data is read by name, so a program says which market and interval it reads.
- Every run is replayable, so there are no loops and no mutation.
- Orders are a host's capabilities, not a built-in simulator.

```flowscript
uses { bars: "^1", alerts: "^1" }
const close = bars.close("BTC-USD", "1h");
const high = bars.high("BTC-USD", "1h");
const low = bars.low("BTC-USD", "1h");
const k = sma(stoch(close, high, low, 14), 3);
const d = sma(k, 3);
metric("k", k);
metric("d", d);
metric("bars_since_oversold", barssince(k < 20));
when (crossover(k, d) && k < 20) { alerts.send("stochastic turned up from oversold"); }
```

| Pine | Flowscript |
|---|---|
| `close`, `high`, `close[1]` | `bars.close("BTC-USD", "1h")`, read once into a `const`; history is the same `x[1]` |
| `ta.sma`, `ta.ema`, `ta.wma`, `ta.rma`, `ta.rsi`, `ta.stdev`, `ta.vwap`, `ta.roc`, `ta.mom`, `ta.correlation` | `sma`, `ema`, `wma`, `rma`, `rsi`, `stddev`, `vwap(price, vol, len)` (a rolling window; anchored, `anchoredVwap(src, vol, anchor)` or a session's `periodVwap`), `roc`, `momentum`, `corr` |
| `ta.atr(14)`, `ta.tr` | `wilderAtr(high, low, close, 14)`, `trueRange(high, low, close)` |
| `ta.highest`, `ta.lowest`, `math.sum`, `ta.cum` | `highest`, `lowest`, `rolling_sum`, `cum` |
| `ta.stoch`, `ta.linreg(src, len, 0)` | `stoch`, `linreg(src, len)` |
| `ta.barssince`, `ta.valuewhen(c, src, 0)` | `barssince`, `valuewhen(c, src)` |
| `ta.pivothigh(src, l, r)`, `ta.pivotlow` | `pivothigh(src, l, r)`, `pivotlow` |
| `[m, s, h] = ta.macd(src, 12, 26, 9)` | `macd(src, 12, 26)`, `macdSignal(src, 12, 26, 9)`, `macdHist(src, 12, 26, 9)`: one function per line |
| `[mid, up, lo] = ta.bb(src, 20, 2)`, `ta.variance` | `sma(src, 20)`, `bbUpper(src, 20, 2)`, `bbLower(src, 20, 2)`, `variance` |
| `[p, m, adx] = ta.dmi(14, 14)` | `diPlus(high, low, close, 14)`, `diMinus(…)`, `adx(high, low, close, 14, 14)` |
| `[st, dir] = ta.supertrend(3, 10)`, `ta.sar(0.02, 0.02, 0.2)` | `supertrend(high, low, close, 3, 10)`, `supertrendDir(…)`, `sar(high, low, close, 0.02, 0.02, 0.2)` |
| `ta.vwma`, `ta.swma`, `ta.wpr`, `ta.cmo`, `ta.mfi`, `ta.obv`, `ta.tsi` | the same names; a read Pine makes itself (the bars, the volume) is an argument |
| `ta.hma`, `ta.alma`, `ta.cci`, `ta.dev`, `ta.percentrank`, `ta.highestbars`, `ta.lowestbars`, `ta.cog`, `ta.range`, `ta.cross` | the same names |
| `[mid, up, lo] = ta.kc(src, 20, 2)`, `ta.kcw`, `ta.bbw` | `ema(src, 20)`, `kcUpper(src, high, low, close, 20, 2)`, `kcLower(…)`, `kcw(…)`, `bbw(src, 20, 2)` |
| `ta.pvt`, `ta.accdist`, `ta.wad`, `ta.nvi`, `ta.pvi`, `ta.iii`, `ta.wvad` | the same names, with the bars and the volume as arguments |
| `ta.crossover`, `ta.crossunder`, `ta.change` | `crossover`, `crossunder`, `change` |
| `math.abs`, `round`, `floor`, `ceil`, `sign`, `log`, `log10`, `pow`, `sqrt`, `sin` … | the same names without `math.`; `log` is `ln` |
| `na(x)`, `nz(x, y)` | `isNa(x)`, `nz(x, y)` |
| `var float x = 0` then `x := x + 1` | `fold(series, seed, (acc, x) => next)` |
| `import user/lib/1 as lib`, `export f(x) => …` | a library of `fn` definitions and `import "lib";`, its functions called by name |
| `plot(x, "name")` | `metric("name", x)`; in a chart study, `// @plot name histogram #26a69a` styles it |
| `plot(x, color = x > 0 ? green : red)` | `// @plot x up=green down=red`, or `by=other` to colour by another metric's sign |
| `plotshape`, `barcolor`, `bgcolor`, `display = display.none` | the `@plot` styles `up` and `down`, `bars`, `bg` and `hidden` |
| `plotshape(c, location = location.belowbar, text = "B")` | `// @plot c up below text="B"` (`above` for over the bar), with `metric("c", cond ? 1 : 0)` |
| `if c` … `label.new(bar_index, high, "H " + str.tostring(x))` | `// @plot l down above text="H {{x}}"` with `metric("l", c ? 1 : 0)` and `x` published as a metric (D1213) |
| `line.new` from pivot to pivot (a zigzag) | `// @plot p line connect`, with `metric("p", …)` set only at the pivots and `na` between |
| `box.new`, `table.new` | a box is a `// @fill` between two lines; a table's values read in the legend |
| `fill(p1, p2, color)`, `hline(70)` | `// @fill upper lower #2962ff`, `// @hline 30 70` |
| `input.int(14)` | a top-level `const len = 14;` in a chart study, which its settings edit; `param(14, { min: 2, max: 50 })` in a strategy |
| `input.int(14, "Length", minval = 2, group = "MA", tooltip = "…")` | `// @input len "Length" min=2 group="MA" tooltip="…"` over the `const` |
| `input.bool(true, "Show")`, `input.string("EMA", options = ["SMA", "EMA"])` | `// @input show bool "Show"` over `const show = 1;`; `// @input kind options="SMA,EMA"` over `const kind = 1;` (the picked option's index) |
| `hour`, `minute`, `dayofweek`, `dayofmonth`, `month`, `year`, `bar_index` | `hour(time)` … `year(time)` in a chart study (UTC), `barIndex(price)` |
| `time(timeframe.period, "0930-1600:23456", "America/New_York")`, `timezone`, `session.isfirstbar` | `inWeekdaySession(nyTime(time), 930, 1600)`; `hour(nyTime(time))`; `sessionStarts(active)` |
| `alertcondition(c)`, `alert(msg)` | `when (c) { alerts.send(msg); }`; ftw's host names it `notify(msg)` |
| `alertcondition(c, message = "RSI {{plot("rsi")}} at {{close}}")` | in a chart study, `// @alert c "RSI {{rsi}} at {{close}}"`: `{{value}}`, the bar's `{{open}}` … `{{volume}}`, `{{time}}` and any line by name |
| `strategy.entry`, `strategy.close` | the host's order actions in a `when` body, such as ftw's `orders.marketBuy` and `orders.sell` |
| `strategy.entry(limit=, stop=)`, `strategy.exit(stop=, limit=)` at a price | a broker rule with `rest` (D1212), such as ftw.trade's `paper.buyLimit({ quote, at })` and `paper.sellStop({ at })`: it rests at `at`, a number or an expression the program also publishes with `metric()`, and fills at the first later close through it |
| `request.security(syminfo.tickerid, "D", ta.sma(close, 20))` | `sample(periodEnds(time, 86400), close, c => sma(c, 20))`: the closed day's value, known on the bar that closes it and held, so it never repaints |
| `request.security(syminfo.tickerid, "D", high - low)` | `sample(periodEnds(time, 86400), {h: periodHigh(high, time, 86400), l: periodLow(low, time, 86400)}, b => b.h - b.l)` |
| `request.security(sym, "1D", close)` of another market | a read at that market and interval, `bars.close("BTC-USD", "1d")`, where the host publishes it |
| `for`, arrays and maps | not in the language: a window function, `fold`, or `percentile` and `median` over a window |
