<!-- language: 1.0.0  surface: 0x2e1293f95439a19561cf4bef525f73d72b248b36b4bc8c90ee87ec74bcc2d26c  engine: 0x327d21acd72ab73c5146fc2a3bcb5f096f0a95ceea964f577e8eda9951414354
     current: https://app.tradepnl.xyz/api/v1/engine -->

# pnl script language

Language 1.0.0. Generated from the compiler's own tables — if it is not in here, it is not in the language.

## Contents

- [The algo VM](#overview) — The algo VM: a small stack machine stepped once per bar, whose cost is known before it runs.
- [A program](#a-program) — A manifest, an optional init block, then a step block that runs once per bar.
- [The manifest](#manifest) — The declarations every program makes: markets, channels and warmup, plus params and kept memory.
- [Every: the evaluation stride](#every) — every 1h at 17m: run once per stride on aggregated bars, at a whole-minute UTC offset.
- [Market names](#market-names) — Markets are named by ticker; a name that is not a plain word is written in double quotes.
- [Channels](#channels) — The per-market input series a program may declare and read.
- [Order flow](#order-flow) — buy_volume and open_interest: taker-buy volume and open interest, and how NaN behaves.
- [Values, and NaN](#values) — Every number is a float64, NaN is data rather than an error, and arrays have static shapes.
- [Expressions](#expressions) — Operators and their precedence, including @ and **, and duration literals.
- [Statements](#statements) — let, assignment to kept memory and let arrays, push, if/elif/else, for over a constant range, and emit.
- [Script controlled target filtering](#target-filtering) — Optional deadbands, smoothing and stepped sizing belong in script parameters and target logic.
- [Builtins](#builtins) — The scalar builtins: every one is the engine's own implementation, not the host's.
- [One bar at a time, and what that rules out](#one-bar-at-a-time) — The machine sees one bar; looking back in time means keeping what you want to look back at.
- [Kept memory, and what looking back costs](#state) — keep and window: the memory that survives to the next bar, and what it costs.
- [Arrays](#arrays) — Vectors and matrices of float64 whose shapes are fixed at compile time, so their cost is too.
- [Making arrays](#construction) — Literals, zeros/ones/full, eye, arange, linspace, and close[:] for a channel across markets.
- [Indexing and slicing](#indexing) — a[i], A[i, j], rows, columns and constant slices, every index proven in range; take(a, k) for data.
- [Broadcasting](#broadcasting) — numpy's rules for rank 1 and 2: a vector is a row, and each dimension matches or is 1; reshape makes a column.
- [Elementwise operations](#elementwise) — Arithmetic, comparisons, and/or/not, every scalar function, where and clip, element by element.
- [Reductions](#array-reductions) — sum, mean, var, std, min, max, argmin, argmax, median, logsumexp and their nan forms; count, any, all, norm, dot.
- [Vectors across markets](#market-vectors) — ch[:] reads a channel across markets, emit markets sets every target, and the nan reductions skip missing members.
- [Linear algebra](#linear-algebra) — @, transpose, outer, diag; cholesky, cho_solve, solve_triangular, eigh, eigvalsh (n <= 128); inverse and log-determinant as identities.
- [Sorting and gathering](#sorting) — argsort(a) orders a vector stably; take(a, idx) gathers by an index array.
- [Bit sets](#bitsets) — bitwise_xor(a, b) and bit_length(a): sets of up to 32 members per element, for F2 linear algebra.
- [Example: Vietoris–Rips persistence](#persistence) — H0 and H1 persistence of a 12-point delay embedding, from argsort, take, bitwise_xor and bit_length.
- [keep: named memory](#keep) — keep name, keep v[n], keep A[r, c]: persistent, zero-initialised, assigned with =.
- [window: a rolling history](#window) — window h[n] len holds the last len rows; push appends, h[-k] reads, reductions run over time.
- [init: once per run](#init) — An init: block runs once before the first bar, may read params and write keep, and may not read the market.
- [Params: the model's searchable hyperparameters](#params) — param x = v in (lo, hi) declares a hyperparameter the backtest search may move; scripts infer, the search fits.
- [What arrays cost](#array-fuel) — Every array op is priced from its static shape by a formula measured against its kernel.
- [Where arrays live](#array-memory) — One arena per machine: keep and windows, then never-zeroed scratch; every limit is a compile error.
- [Array arithmetic is ordered](#determinism-arrays) — Sequential, ascending reductions from 0, a fixed matmul and Cholesky order, and engine math only.
- [Example: momentum, vectorised](#example-momentum-vector) — Fast and slow EMAs per market in two keep vectors; bit-identical to the scalar template.
- [Example: mean reversion over a window](#example-zscore-window) — A true rolling z-score from a one-day window; NaN, and holding, until the window is full.
- [Example: cross-sectional minimum variance](#example-min-variance) — An EWMA covariance in keep, regularised and solved with cho_solve every bar.
- [Example: kernel model with fitted centres](#example-kernel-centers) — Gaussian features around param centres C (k × d), weighted by param w.
- [Example: kernel model solved once in init](#example-kernel-pseudo) — Pseudo-observations are params, so the kernel ridge solve runs once, in init.
- [Example: online kernel regression (RLS)](#example-online-rls) — Weights learned walk-forward by recursive least squares; only three hyperparameters are tuned.
- [Mathematical functions](#activations) — exp, log, sin, cos, arctan, erf, round and the activations: the engine's own algorithms, the same bits on every host.
- [Deterministic random numbers](#random) — uniform/normal(seed, i): a stateless counter-based generator; the same (seed, i) is the same number everywhere.
- [Rank-1 Cholesky updates](#cholupdate) — cholupdate/choldowndate(L, x) refresh a factor in O(n²), and cho_solve(L, b) solves from it.
- [Example: minimum variance by factor updates](#example-min-variance-update) — The minimum-variance example with the Cholesky factor kept and updated, O(n²) per bar.
- [Example: a small neural net trained online](#example-mlp) — A 2-layer MLP in keep, initialised by normal draws in init, trained by hand-written SGD each bar, causally.
- [Templates](#templates) — Reference programs, one per model family, each a script on these primitives with its hyperparameters as params.
- [Template: principal components](#template-pca) — PCA of a rolling covariance with eigh; trades the residual after the leading factors.
- [Template: slow feature analysis](#template-sfa) — The slowest linear feature: whiten with Cholesky, then the smallest eigenvector of the whitened derivative covariance.
- [Template: canonical correlation](#template-cca) — CCA between two blocks of markets by Cholesky whitening and eigh; the first canonical correlation.
- [Template: kernel ridge regression](#template-kernel-ridge) — RBF kernel ridge on lagged returns: exact squared distances by rows, Cholesky solve, one prediction.
- [Template: Gaussian process regression](#template-gp) — GP posterior mean and variance and the log marginal likelihood, from one Cholesky factor.
- [Template: support vector machine](#template-svm) — A linear SVM trained by dual coordinate descent for a fixed number of sweeps, predicting the sign of the next return.
- [Template: k-means](#template-kmeans) — Lloyd's k-means on two features with one-hot assignment by broadcasting and fixed iterations.
- [Template: k nearest neighbours](#template-knn) — kNN regression: distances to stored patterns, argsort, the mean target of the k nearest.
- [Template: logistic regression](#template-logistic) — Logistic regression by Newton (IRLS) steps with a ridge, each a Cholesky solve.
- [Template: ridge and lasso](#template-ridge-lasso) — Ridge in closed form and lasso by cyclic coordinate descent with soft thresholding.
- [Template: softmax regression](#template-softmax) — Three-class softmax regression on the next return's sign and size, by fixed gradient steps with logsumexp.
- [Template: a small MLP, trained online](#template-mlp) — A one-hidden-layer network: forward pass, then one hand-written gradient step per bar on the previous bar's input.
- [Template: Gaussian mixture by EM](#template-gmm) — A two-component 1-D Gaussian mixture of returns fitted by EM with logsumexp responsibilities.
- [Template: Kalman filter](#template-kalman) — A local linear trend model filtered online: predict, update, and the innovation log-likelihood.
- [Template: hidden Markov model](#template-hmm) — A two-state Gaussian HMM over returns: the normalised forward filter and Viterbi decoding of the last state.
- [Template: Bayesian linear regression](#template-bayes-linear) — Conjugate Bayesian regression: posterior mean and covariance, the evidence, and the matching diagonal-Gaussian ELBO.
- [Template: posterior predictive sampling](#template-sampling) — Monte Carlo with the deterministic generator: normal draws through a Cholesky factor, and a categorical draw by cumsum.
- [Unbounded iteration](#unbounded-loop) — There is no while loop, and a for loop's trip count is a constant.
- [Fuel](#fuel) — Every instruction costs, and a program is refused if its worst case per bar exceeds the cap.
- [Why the arithmetic is ours](#determinism) — Why the arithmetic, summation order and math library are the engine's own.
- [Versions](#versioning) — The language version, the surface digest, and which one an agent should compare.
- [Unterminated string](#unterminated-string) — A quote opened and the line ended.
- [Unknown character](#unknown-character) — The character is not part of the language.
- [Malformed number](#bad-number) — A numeric literal the lexer could not finish reading — a trailing `.`, an exponent with no digits, or a duration with an unknown unit.
- [Unexpected token](#unexpected-token) — The parser had no rule for this token in this position.
- [Expected a token that is not there](#expected-token) — A construct is missing its closing piece — a `)`, or the `:` that opens a block.
- [A construct this language does not have](#unsupported-construct) — Refused BY NAME, with the reason, rather than reported as a syntax error.
- [Market not declared](#undeclared-market) — Every market a program reads is named in the `markets` line.
- [Channel not declared](#undeclared-channel) — Same rule as markets, for the input series.
- [Declared twice](#duplicate-declaration) — The declared sets are canonicalised — sorted and deduplicated — because that canonical form is what is hashed.
- [Manifest field missing](#missing-manifest-field) — `markets`, `channels` and `warmup` are required and have no default.
- [Manifest value outside its limit](#manifest-out-of-range) — The ceilings exist so a mistake is refused rather than accepted and priced.
- [Duration off the stride](#stride-mismatch) — Under `every`, a program runs once per stride and its windows count bars of the stride, so every duration it writes must be a whole number of strides:
- [Unknown name](#unknown-name) — Not a declared market or channel, not a `let` binding in scope, and not a builtin.
- [Type mismatch](#type-mismatch) — Conditions are boolean and arithmetic is numeric.
- [Over the fuel budget](#fuel-exceeded) — The program's proven worst case per bar is above the ceiling of 2,000,000, or `init` is above 50,000,000.
- [Over the memory budget](#memory-exceeded) — Kept memory and windows together exceed their slot ceiling.
- [Shapes do not fit](#shape-mismatch) — Array shapes are static, so a mismatch is found before the program runs.
- [Index not proven in range](#index-unproven) — Every index is proven in range at compile time, so a read can never land outside its array and there is no out-of-bounds behaviour to specify.
- [Declared and never used](#unused-declaration) — A warning, not a refusal.

## The algo VM

<a id="overview"></a>

Every algo on this platform runs inside a **trading algo virtual machine** — a small stack machine that executes one bar at a time and has no wall clock, no allocation, no host calls and no way to observe anything except the bar it was handed.

You write a program for it in this language.

The machine is small on purpose. Because it cannot loop unboundedly, cannot allocate and cannot call out, the exact cost of running your program for one bar is known BEFORE it runs — which is what lets an algo be listed, priced and settled without anyone having to trust that it behaves. A sandbox restricts what a general language may do; this is a machine that cannot do those things at all.

## A program

<a id="a-program"></a>

A manifest, then a `step` block that runs once per bar. Indentation is significant and tabs are an error. There is no `main`, no imports and no top-level code outside the manifest — a program is a step function over declared inputs, params and memory. An optional `init:` block may come before `step:`; it runs once, before the first bar (see `init`).

Inside `(` and `[` a line may continue onto the next, so a matrix literal can be written one row per line.

```
markets BTC, ETH
channels close
warmup 21d
param rate = 0.05 in (0.005, 0.5)
keep level

step:
    let spread = close[BTC] - close[ETH]
    level = level + rate * (spread - level)
    if level > 0.0:
        emit ETH, tanh(level)
    else:
        emit ETH, 0.0
```

## The manifest

<a id="manifest"></a>

Three required fields, no defaults.

- `markets` — every market the program reads. Sorted and deduplicated into a canonical form, which is what the strategy hash covers.
- `channels` — every input series it reads.
- `warmup` — how much history the program needs before its output means anything. A duration, not a bar count.

Optional, once: `every` (how often the program runs, see `every`; absent, it runs every minute, the platform's own clock).

Optional, repeatable: `param` (a searchable hyperparameter, see `params`), `keep` (memory carried across bars, see `keep`) and `window` (a ring of past rows, see `window`).

There is no fuel declaration: the checker computes the program's worst-case cost per bar and refuses it above the cap (see `fuel`).

What the program may HOLD is derived from what it emits, not declared separately. Two lists would drift; one list plus the emit targets cannot.

Nothing has a default because a default is a number we chose that silently becomes part of someone's strategy.

## Every: the evaluation stride

<a id="every"></a>

`every` sets how often the program runs: `1m` (the default), `5m`, `15m`, `1h`, `4h` or `1d`. `at` shifts the buckets by whole minutes from the UTC boundary, below the stride: `every 1h at 17m` runs on the buckets that start at :17 past each hour, `every 1d at 810m` on days that start at 13:30 UTC. Both are part of the program and its hash; neither is a param, so a search never tunes them.

The program runs once per bucket, as its last minute closes, and every channel is that bucket's bar: the first `open`, the highest `high`, the lowest `low`, the last `close`, the summed `volume`, `buy_volume` and `funding`, and the last `open_interest`. `time` is the bucket's START. A bucket with a minute that has no price is unavailable: its price, volume and order-flow channels are NaN, as a missing minute is at `1m`. Minutes where nothing traded are priced (flat at the last close, zero volume) and count as present.

Bars count strides: `window hist 24` under `every 1h` is a day, and so is `window hist 24h`. A duration must be a whole number of strides (`30m` under `every 1h` is refused), so `warmup`, windows and duration params mean the same span of time they say.

Targets hold from one run to the next. Evaluation, paper and live trading fill and mark minute by minute between runs; the builder's backtest simulates on the bucket bars themselves and judges liquidation against each bucket's full range.

Each run may spend more fuel than a minute's: the stride in minutes, up to 15, times 2,000,000 (see `fuel`).

```
markets BTC
channels close, volume
every 1h at 17m
warmup 2d
param n = 24h
window hist n

step:
    push(hist, close[BTC])
    let m = mean(hist)
    emit BTC, where(close[BTC] > m, 1.0, 0.0)
```

## Market names

<a id="market-names"></a>

A market is named by its ticker: `BTC`, `US500`, `EURUSD`. A market whose name is not a plain word — a Hyperliquid HIP-3 market such as `xyz:SP500` — is written in double quotes, wherever a market goes: in `markets`, after `emit`, and inside `close[...]`. `"BTC"` and `BTC` are the same market. Quotes name a market only; there are no string values.

```
markets "xyz:SP500", "xyz:GOLD"
channels close
warmup 1d
step:
    let spread = close["xyz:SP500"] / close["xyz:GOLD"]
    emit "xyz:SP500", tanh(spread - 1.0)
    emit "xyz:GOLD", tanh(1.0 - spread)
```

## Channels

<a id="channels"></a>

One bar of the world is a row of channels per market: `close`, `high`, `low`, `open`, `volume`, `funding`, `buy_volume`, `open_interest`, `time`. Every channel is per market; there are no venue-wide channels. A channel may be NaN on any bar, and NaN always means unavailable. `time` is the verified bar-start Unix timestamp in seconds (the same on each aligned market), not the wall clock; an input without timestamps supplies NaN.

`funding` is the funding rate settled over the bar, as a plain fraction of notional for that bar (not annualised, not a percentage). A position held through the bar is charged `position * close * funding` at the bar's close, with `position` signed in base units: positive funding means longs pay and shorts receive, negative the reverse. `funding` is NaN when unknown. Unknown funding is billed as nothing, and the exposure held through it is reported separately as `fundingUnknownExposure`.

History may be composed from several sources for continuity, so levels across long spans may not match a single venue. Prefer returns, ratios and normalised measures over absolute levels.

`buy_volume` and `open_interest` are order flow; see `order-flow`. There is no sell channel: sell volume is `volume - buy_volume`.

`volume` is NaN where it was not measured, and that is different from zero: zero means no trade was recorded in the bar. A period in which the price was not observed may be represented as unchanged: flat, with zero volume. A sensor that conflated them would report maximum illiquidity for exactly the venues we know least about.

`time` is the VM's clock: arithmetic on it gives any calendar phase (the UTC hour is `floor(time[BTC] / 3600.0) - 24.0 * floor(time[BTC] / 86400.0)`), and a computation keyed to it gives the same value at the same instant however far back a replay starts.

## Order flow

<a id="order-flow"></a>

`buy_volume` is the bar's taker-buy volume and `open_interest` the market's open interest at bar close, both in base units. Sell volume is derived, never a channel: `volume - buy_volume`. Whenever both are finite, `0 <= buy_volume <= volume`. The data layer enforces this, so the VM does not check it and a program need not either. The buy-pressure imbalance `(2 * buy_volume - volume) / volume` lies in [-1, 1]. It is NaN when volume is zero or either input is missing.

Either channel may be NaN wherever it is unavailable; some markets never report order flow. History may be composed from several sources for continuity. Values are not interpolated. Prefer relative measures (imbalance, ratios, changes) over absolute levels.

The usual NaN rules apply. Arithmetic propagates NaN, every comparison with NaN is false, and a market-population reduction skips members whose bound channel is NaN. So guard with `isnan` or `where` before you let order flow move a position.

```
markets BTC, ETH, SOL
channels volume, buy_volume
warmup 1h
keep flow[markets]

step:
    let v = volume[:]
    let b = buy_volume[:]
    let imbalance = (2.0 * b - v) / v
    flow = where(isnan(imbalance), flow, flow + 0.05 * (imbalance - flow))
    emit markets, tanh(4.0 * flow)
```

```
markets BTC
channels volume, buy_volume
warmup 1h
keep ratio

step:
    let buy = buy_volume[BTC]
    let sell = volume[BTC] - buy
    if not isnan(sell) and buy + sell > 0.0:
        ratio = ratio + 0.1 * ((buy - sell) / (buy + sell) - ratio)
    emit BTC, clip(2.0 * ratio, -1.0, 1.0)
```

```
markets BTC
channels close, open_interest
warmup 1d
keep lastOi
keep lastPx
keep signal

step:
    let oi = open_interest[BTC]
    let px = close[BTC]
    if not isnan(oi) and not isnan(px) and lastOi > 0.0 and lastPx > 0.0:
        let dOi = oi / lastOi - 1.0
        let r = px / lastPx - 1.0
        # Rising OI confirms the move; falling OI is positions closing.
        signal = signal + 0.02 * (sign(r) * dOi * 100.0 - signal)
    if not isnan(oi):
        lastOi = oi
    if not isnan(px):
        lastPx = px
    emit BTC, tanh(signal)
```

## Values, and NaN

<a id="values"></a>

Every value is a float64. There are no integers, no strings in expressions and no objects. A value may also be an array of float64 with a shape fixed at compile time (see `arrays`).

NaN is a value the data actually contains — it is how a missing bar arrives — and the language does not hide it. Every comparison with NaN is false, including `nan == nan`, so a naive guard sends a missing bar down the branch meant for a normal one. Use `isnan(x)` when the difference between 'no reading' and 'a reading of zero' matters, which in market data is almost always.

Backtests and evaluations can start at the earliest available held market in the basket. A later-listed market has NaN inputs before its history starts; it does not shorten the other markets' history. The requested end cannot exceed any held market's published end. Each held symbol must exist in the dataset, even if its history starts after the requested window.

Handle readiness per market: the script's global `warmup` does not guarantee that a later-listed market has enough observations. Emit `nan` until that market's required inputs and indicators are ready. A NaN target means no new instruction: it preserves the previous target, initially zero. It does not close or erase an existing position; emit `0.0` to request an exit. Missing signals never erase P&L.

Ordinary reductions propagate NaN; NaN-skipping reductions such as `nanmean` are an explicit choice to use fewer observations. In this example, each market needs three complete hourly closes in its own window. BTC can trade while HYPE is unavailable or warming up; a missing close makes only that market hold.

```
markets BTC, HYPE
channels close
every 1h
warmup 3h
window history[markets] 3h

step:
    let px = close[:]
    push(history, px)
    let average = mean(history, 0)
    emit markets, where(isnan(average), nan, where(px > average, 0.1, 0.0))
```

## Expressions

<a id="expressions"></a>

Binary operators, loosest first, every level left-associative:

1. `or`
2. `and`
3. `==`, `!=`
4. `<`, `<=`, `>`, `>=`
5. `+`, `-`
6. `*`, `/`, `@`

`@` is matrix multiplication, at the level of `*`. `x ** k` takes an integer literal `k` from 0 to 8 and is repeated multiplication, left to right; it binds tighter than unary minus, so `-x ** 2` is `-(x ** 2)`. There is no general power: the host's `pow` is approximated.

Unary `-` and `not`. Parentheses group. There is no ternary operator — `where(cond, a, b)` selects, evaluating both — no assignment inside an expression and no operator overloading.

Durations are literals: a number followed by `m`, `h`, `d`, `w`. They are converted to bars of the program's stride at compile time (minutes, unless `every` says otherwise), so `1d` means a day at every stride rather than a number that changes meaning with the clock.

## Statements

<a id="statements"></a>

- `let name = expr` binds a local. Locals are zeroed at the start of every bar.
- `if` / `elif` / `else`, with a `:` and an indented block.
- `for i in range(n)`, with `n` a constant.
- `emit MARKET, expr` sets the target exposure for a market. A market that is emitted to is one the strategy may hold.
- `emit markets, v` sets every market's target from a `vec[markets]`.
- `name = expr` and `name[i] = expr` write kept memory (`keep`), and `name[i] = expr` / `name[i, :] = v` write elements or rows of a `let` array.
- `push(w, row)` appends a row to a window.

The reserved words are `and`, `at`, `channels`, `elif`, `else`, `emit`, `every`, `false`, `for`, `if`, `in`, `init`, `keep`, `let`, `markets`, `nan`, `not`, `or`, `param`, `range`, `step`, `true`, `warmup`, `window`.

## Script controlled target filtering

<a id="target-filtering"></a>

Script targets receive no additional platform deadband in backtests, paper or live signals. NaN holds the prior target; zero requests an exit. Venue minimum orders and account drift checks still affect execution. The example applies its band per market and lets zero bypass it. It resets memory on each UTC hour, with two hours of warmup, so a rolling live window can reconstruct it. That hourly reset is part of this example's strategy. Arbitrary keep state is not durable across independently evaluated live windows; test reconstruction before using a different reset or state rule.

```
markets BTC, ETH
channels open, close, time
warmup 2h
param band = 0.05 in (0.01, 0.2)
keep previous[markets]

step:
    let desired = clip((close[:] / open[:] - 1.0) * 10.0, -1.0, 1.0)
    if time[BTC] == floor(time[BTC] / 3600.0) * 3600.0:
        previous = 0.0
    let accept = (abs(desired - previous) >= band) or (desired == 0.0)
    previous = where(isnan(desired), previous, where(accept, desired, previous))
    emit markets, where(isnan(desired), nan, previous)

```

## Builtins

<a id="builtins"></a>

There is no standard library and no way to add one. These are all of it:

- `abs(x)`
- `arctan(x)`
- `arctan2(a, b)`
- `bitwise_xor(a, b)`
- `ceil(x)`
- `cos(x)`
- `elu(x)`
- `erf(x)`
- `erfc(x)`
- `exp(x)`
- `expm1(x)`
- `floor(x)`
- `gelu(x)`
- `hard_sigmoid(x)`
- `hard_tanh(x)`
- `isnan(x)`
- `leaky_relu(a, b)`
- `log(x)`
- `log1p(x)`
- `log_sigmoid(x)`
- `maximum(a, b)`
- `minimum(a, b)`
- `normal(a, b)`
- `relu(x)`
- `round(x)`
- `sigmoid(x)`
- `sign(x)`
- `silu(x)`
- `sin(x)`
- `soft_sign(x)`
- `softplus(x)`
- `sqrt(x)`
- `tanh(x)`
- `trunc(x)`
- `uniform(a, b)`

Each also applies elementwise to an array. The array builtins are `all(a[, axis])`, `any(a[, axis])`, `arange(n)`, `argmax(a[, axis])`, `argmin(a[, axis])`, `argsort(a)`, `bit_length(a[, axis])`, `cho_solve(L, b)`, `choldowndate(L, x)`, `cholesky(A)`, `cholupdate(L, x)`, `clip(a, lo, hi)`, `count(a[, axis])`, `cumsum(a[, axis])`, `diag(A)`, `dot(a, b)`, `eigh(A)`, `eigvalsh(A)`, `eye(n)`, `filled(window)`, `full(n[, m], x)`, `len(a)`, `linspace(lo, hi, n)`, `logsumexp(a[, axis])`, `max(a[, axis])`, `mean(a[, axis])`, `median(a[, axis])`, `min(a[, axis])`, `nanmax(a[, axis])`, `nanmean(a[, axis])`, `nanmedian(a[, axis])`, `nanmin(a[, axis])`, `nanstd(a[, axis])`, `nansum(a[, axis])`, `nanvar(a[, axis])`, `norm(a)`, `ones(n[, m])`, `outer(a, b)`, `push(window, row)`, `reshape(a, n[, m])`, `solve_triangular(L, b[, trans])`, `std(a[, axis])`, `sum(a[, axis])`, `take(a, k)`, `transpose(A)`, `var(a[, axis])`, `where(cond, a, b)`, `zeros(n[, m])`.

Every one of them is the engine's own implementation, not the host's. ECMAScript leaves `exp`, `log`, `pow` and `tanh` implementation-approximated, so two machines can return values one ulp apart — and the platform settles fees against replayed curves. A language with no way to reach the host's math library cannot disagree with itself across machines, which is the whole argument for the language being ours rather than a subset of someone else's.

## One bar at a time, and what that rules out

<a id="one-bar-at-a-time"></a>

The machine steps once per bar. There is no way to index a series by bar and no expression over a whole series: `close[-20:]` does not exist.

That is not a missing feature. A language that can operate on a whole series at once is a language whose cost depends on how much history exists, and the cost of a program has to be known before it runs. Looking back is done by KEEPING what you want to look back at: a `window` is a ring buffer whose length is declared up front and therefore paid for up front, and `keep` holds anything else.

The distinction worth holding onto is between looking back in TIME and looking across the market at one instant. The first is bounded by your memory budget. The second is a value: `close[:]` is this bar's closes across the declared markets, a `vec[markets]` whose width is known at compile time, so the cost stays computable.

## Kept memory, and what looking back costs

<a id="state"></a>

A vectorised evaluator holds the whole series, so looking back costs it nothing. A machine that sees one bar must KEEP what it wants to look back at: a rolling window is a ring buffer, and its size is its window.

That is the real price of the design, and it is memory rather than time. The existing reversion ensemble needs 92,160 slots — 720 KB — purely in ring buffers. `keep` and `window` declare it by name, laid out in declaration order and capped at 262,144 slots.

Kept memory is zeroed once, deterministically, before the first bar (window rows start NaN). There is no uninitialised memory and no source of entropy anywhere in the machine.

## Arrays

<a id="arrays"></a>

A value may be a vector `vec[n]` or a matrix `mat[r, c]` of float64. Every dimension is a compile-time integer: a literal, an integer `param`, `len(a)`, or `markets` (the number of declared markets). Rank is 1 or 2. Because every shape is static, every array operation's fuel and memory are static too, and the worst case is still proven before the program runs.

A comparison on arrays makes a MASK (elements 1 or 0). A mask is a number array where numbers are wanted; a number array is never a mask, for the same reason a number is never a condition. `if` needs a single condition, so an array is refused there: reduce it with `any(m)` or `all(m)`, or select elementwise with `where`.

Arrays have value semantics: `let b = a` copies, and nothing aliases. There are no views, no dynamic shapes, no boolean-mask indexing and no rank above 2.

```
markets BTC, ETH, SOL
channels close
warmup 1d
keep last[markets]

step:
    let px = close[:]
    let r = where(isnan(px) or last == 0.0, 0.0, px / last - 1.0)
    last = where(isnan(px), last, px)
    emit markets, tanh((r - mean(r)) * 100.0)
```

## Making arrays

<a id="construction"></a>

- `[1.0, x, -2.0]` and `[[a, b], [c, d]]` — literals of scalar expressions. An all-constant literal is one constant range.
- `zeros(n)`, `zeros(r, c)`, `ones(...)`, `full(n, x)`, `full(r, c, x)`.
- `eye(n)`; `arange(n)` is 0..n-1; `linspace(lo, hi, n)` has element i equal to `lo + i * ((hi - lo) / (n - 1))`, in exactly that order, and needs n >= 2.
- `ch[:]` — channel `ch` at this bar across the declared markets, a `vec[markets]` in canonical (sorted) market order.
- `len(a)` — the first dimension, a compile-time integer; `len(markets)` is the market count and `len(w)` a window's length.

```
markets BTC, ETH, SOL
channels close
warmup 1d

step:
    let grid = linspace(-1.0, 1.0, len(markets))
    let I = eye(3) * 2.0 + full(3, 3, 0.5)
    emit markets, tanh(I @ grid + arange(3) * 0.1 - close[:] * 0.0)
```

## Indexing and slicing

<a id="indexing"></a>

Forms: `a[i]`, `A[i, j]`, `a[lo:hi]`, `A[i, :]`, `A[:, j]`, `A[r0:r1, :]`, `A[i, c0:c1]`, `A[r0:r1, j]`, `A[r0:r1, c0:c1]` (read-only) and `A[i]` for a row. A negative constant counts from the end. `v[BTC]` or `v["xyz:SP500"]` indexes an array whose first dimension is the market count, in canonical order.

Every index is PROVEN in range at compile time: constants, params, `len`, loop variables (`range(n)` gives 0..n-1), and `+` or `-` of those. Anything else is `index-unproven`. Slice bounds are constants. The one data-dependent read is `take(a, k)`, with `k` a number or an index array, which returns NaN when an index is NaN, not an integer, or out of range. Every write index is proven; there are no dynamic writes.

```
markets BTC, ETH, SOL
channels close
warmup 1d

step:
    let px = close[:]
    let M = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]
    let acc = zeros(3)
    for i in range(3):
        acc[i] = px[i] * M[1, i] + M[0, :][-1]
    let pick = take(px, floor(px[BTC]) - floor(px[BTC]))
    emit markets, tanh(acc * 0.001 + pick * 0.0)
```

## Broadcasting

<a id="broadcasting"></a>

An elementwise operator combines a scalar with an array, or two arrays by numpy's rule: a vector `vec[n]` is a row `[1, n]`, and each dimension must be equal or 1, the 1 stretching. So `mat[r, c]` combines with `vec[c]` or `mat[1, c]` (each row), `mat[r, 1]` (each column) and `mat[1, 1]`, and `mat[r, 1]` with `vec[c]` gives `mat[r, c]`. Anything else is `shape-mismatch`. `reshape(a, n[, m])` gives the same elements, row-major, a new shape (`reshape(v, n, 1)` is a column). `a @ b` multiplies `mat @ mat`, `mat @ vec` and `vec @ mat`; for two vectors use `dot(a, b)`.

```
markets BTC, ETH, SOL
channels close
warmup 1d

step:
    let r = reshape(close[:], 3, 1)
    let gap = r - transpose(r)
    let rank = sum(r > transpose(r), 1)
    emit markets, tanh(rank - 1.0) + sum(gap, 1) * 0.0
```

## Elementwise operations

<a id="elementwise"></a>

`+ - * /`, the comparisons, and `and`/`or`/`not` (on masks) apply element by element. Every scalar function lifts: `exp`, `log`, `tanh`, `soft_sign`, `sqrt`, `abs`, `floor`, `ceil`, `round`, `trunc`, `sign`, `isnan`, `expm1`, `log1p`, `sin`, `cos`, `arctan`, `erf`, `erfc`, `relu`, `elu`, `sigmoid`, `log_sigmoid`, `softplus`, `silu`, `gelu`, `hard_sigmoid`, `hard_tanh`, and the two-argument `minimum`, `maximum`, `arctan2`, `leaky_relu`, `uniform`, `normal`, `bitwise_xor` broadcast. `isnan(a)` makes a mask. `where(cond, a, b)` picks per element (a scalar condition picks between two numbers; both sides are always evaluated), with `a` or `b` a scalar or an array of the mask's shape; `clip(a, lo, hi)` is exactly `minimum(maximum(a, lo), hi)`. Each element is computed by the same operation as its scalar form, so a vectorised program and its scalar spelling agree to the last bit.

```
markets BTC, ETH, SOL
channels close
warmup 1d
keep peak[markets]

step:
    let px = close[:]
    peak = where(isnan(px), peak, maximum(peak, px))
    let dd = where(peak > 0.0, px / peak - 1.0, 0.0)
    emit markets, clip(-dd * 10.0, 0.0, 1.0)
```

## Reductions

<a id="array-reductions"></a>

`sum mean var std median min max argmin argmax count any all bit_length logsumexp` and the `nan` forms reduce a whole array, or along a constant axis of a matrix: `mean(H, 0)` is the column means, `vec[c]`; axis 1 gives row results. As in numpy, `max` and `min` reduce and `maximum(a, b)`/`minimum(a, b)` are elementwise. `dot(a, b)` and `norm(a)` (no scaling: overflow is Inf).

The plain reductions PROPAGATE NaN, as numpy does: one missing element makes the result NaN, which holds the position. `nansum nanmean nanvar nanstd nanmin nanmax nanmedian` skip NaN instead (an empty `nansum` is 0, any other empty one NaN), and `count(a)` counts the non-NaN elements, so `where(count(r) >= 3.0, nanmean(r), nan)` is a mean over at least three present members. `any` and `all` take masks. `var` and `std` are population, two-pass. `argmin`/`argmax` return the first extremal index. `logsumexp(a)` is `max + log(sum(exp(a - max)))`, which never overflows; `exp(a - logsumexp(a))` is a softmax.

```
markets BTC, ETH, SOL
channels close
warmup 1d

step:
    let px = close[:]
    let H = [[1.0, 2.0, 3.0], [3.0, 5.0, 7.0]]
    let spread = max(px) - min(px)
    let z = (px - mean(px)) / maximum(std(px), 0.000000001)
    emit markets, tanh(z + mean(H, 0) * 0.0 + argmax(px) * 0.0 + spread * 0.0)
```

## Vectors across markets

<a id="market-vectors"></a>

`close[:]` is the channel at this bar across the declared markets, in canonical (sorted) order, a `vec[markets]`. `emit markets, v` emits one target per market from a `vec[markets]`; a NaN element holds that market, exactly like `emit` with NaN. `v[BTC]` reads one market's element. A market without data this bar is NaN in every vector computed from it, so the `nan` reductions are the cross-section over the members that are present, and `count` says how many there were.

```
markets BTC, ETH, SOL
channels close
warmup 1d
keep prev[markets]

step:
    let px = close[:]
    let r = where(prev == 0.0, nan, px / prev - 1.0)
    prev = where(isnan(px), prev, px)
    let enough = count(r) >= 2.0
    let breadth = where(enough, nanmean(where(isnan(r), nan, r > 0.0)), nan)
    let centre = where(enough, nanmean(r), nan)
    emit markets, tanh((r - centre) * 100.0) * breadth
```

## Linear algebra

<a id="linear-algebra"></a>

`transpose(A)`, `outer(a, b)`, `diag(A)` (a matrix's diagonal, or a vector's diagonal matrix) and `a @ b`. The factorisations take a square matrix of at most 128 rows and read only its lower triangle:

- `cholesky(A)`: the lower factor L of a positive definite A, Cholesky–Banachiewicz in a fixed order. A matrix that is not positive definite gives an all-NaN result, which holds, never an exception; regularise explicitly (`A + lam * eye(n)`).
- `cho_solve(L, b)`: solves `L @ transpose(L) @ x = b` from the factor, forward then back, for `b` a vector or a matrix of right-hand sides.
- `solve_triangular(L, b[, trans])`: solves `L @ x = b` (forward), or with `trans` 1 `transpose(L) @ x = b` (back). scipy's name and third argument; the factor is always the lower triangle, which is what `cholesky` returns.
- `eigvalsh(A)`: the eigenvalues of a symmetric A, ascending; `eigh(A)`: the matching unit eigenvectors as columns, each signed so its largest-magnitude entry is positive. Cyclic Jacobi in a fixed order with a fixed convergence test (an element at or below 2^-53 sqrt|A[p,p]| sqrt|A[q,q]| is zero; it stops after a sweep with none left), at most 20 sweeps, so small eigenvalues of graded matrices keep their relative accuracy.

The rest are identities on a factor `L = cholesky(A)`, written in the language: the inverse is `cho_solve(L, eye(n))`; the log-determinant `2.0 * sum(log(diag(L)))`; the quadratic form `transpose(x) A^-1 x` is `dot(z, z)` with `z = solve_triangular(L, x)`, which is also the squared Mahalanobis distance; the trace is `sum(diag(A))`; and a diagonal is set with `A + diag(v)` or `where(eye(n) > 0.0, diag(v), A)`. A determinant is `exp` of the log-determinant, which overflows long before the factor does, so work with the logarithm.

```
markets BTC, ETH, SOL, BNB, XRP, DOGE
channels close
warmup 30d
param halflife = 7d
param ridge = 0.0001 in (0.00001, 0.01)
keep prev[markets]
keep cov[markets, markets]

step:
    let px = close[:]
    let r = where(isnan(px) or prev == 0.0, 0.0, px / prev - 1.0)
    prev = where(isnan(px), prev, px)
    let a = 1.0 - exp(-0.6931471805599453 / halflife)
    cov = (1.0 - a) * cov + a * outer(r, r)
    let w = cho_solve(cholesky(cov + ridge * eye(len(markets))), ones(markets))
    emit markets, w / sum(abs(w))
```

## Sorting and gathering

<a id="sorting"></a>

`argsort(a)` returns the indices that put `vec[n]` in ascending order, as numbers: equal elements (and -0, +0) keep their index order, and every NaN sorts after every number. It is a bottom-up merge sort whose work depends only on n, at most 4,096. `take(a, k)` reads `a` flat, row-major (numpy's `take`): with a number `k` it is one element, with an index array it is an array of `k`'s shape, each element NaN unless its index is an integer in range. Together they sort (`take(a, argsort(a))`), rank (`argsort(argsort(a))` is each element's position) and read rows chosen by data (`take(A, i * c + arange(c))`); there are no dynamic writes. The k smallest are `take(a, argsort(a)[0:k])`. `cumsum(a[, axis])` is the running sum, sequential from 0 (a matrix needs the axis).

```
markets BTC, ETH, SOL
channels close
warmup 1d

step:
    let px = close[:]
    let order = argsort(px)
    let rank = argsort(order)
    let mid = take(px, take(order, 1.0))
    emit markets, (rank - 1.0) * 0.5 + mid * 0.0
```

## Bit sets

<a id="bitsets"></a>

A word is an integer from 0 to 2^32 - 1; a vector of words is a bit set, element i holding members 32i to 32i + 31. `bitwise_xor(a, b)` is exclusive or, on numbers or elementwise; `bit_length(a)` is Python's `int.bit_length` of the set read as one integer: one more than its highest member, 0 if empty, with an optional axis like the other reductions. Anything that is not a word gives NaN. Exclusive or is addition over F2, so a column reduction of a boundary matrix costs a few words per step instead of one element per row.

```
markets BTC, ETH, SOL
channels close
warmup 1d

step:
    let a = bitwise_xor(arange(3), 5.0)
    let high = bit_length(a) - 1.0
    emit markets, (a - high) * 0.0
```

## Example: Vietoris–Rips persistence

<a id="persistence"></a>

Persistent homology is a program here, not a builtin. `init` builds the combinatorial tables once; each refresh computes squared distances, the H0 deaths as Prim's minimum spanning tree, and the H1 bars by reducing each triangle's boundary column over F2 (Vietoris–Rips filtration, edges and triangles ordered by squared length, ties in lexicographic vertex order). Bars within rounding of zero length are dropped. The result is in the units of the coordinates. `time` is the verified bar-start UTC time in seconds, so refreshing on the UTC hour gives the same value at the same instant however far back a replay starts. The reduction is priced at its worst case (every elimination round of every triangle); the measured worst case at 12 points is in the engine's tests.

```
markets BTC
channels close, time
warmup 1d
param gain = 0.2 in (0.01, 1.0)
keep prev
keep level
window hist 13
keep FAB[220]
keep FAC[220]
keep FBC[220]
keep PIX[66]
keep BITS[66, 3]

init:
    level = nan
    # vertex triples a < b < c in lexicographic order: argsort sends every other triple last
    let k3 = arange(1728)
    let ta = floor(k3 / 144.0)
    let tb = floor(k3 / 12.0) - ta * 12.0
    let tc = k3 - floor(k3 / 12.0) * 12.0
    let tsel = argsort(where(ta < tb and tb < tc, k3, 1728.0 + k3))
    let A = take(ta, tsel[0:220])
    let B = take(tb, tsel[0:220])
    let C = take(tc, tsel[0:220])
    # the edge (a, b), a < b, is number a * (2n - 1 - a) / 2 + b - a - 1 in lexicographic order
    FAB = A * (23.0 - A) / 2.0 + B - A - 1.0
    FAC = A * (23.0 - A) / 2.0 + C - A - 1.0
    FBC = B * (23.0 - B) / 2.0 + C - B - 1.0
    let k2 = arange(144)
    let ea = floor(k2 / 12.0)
    let eb = k2 - ea * 12.0
    PIX = take(k2, argsort(where(ea < eb, k2, 144.0 + k2))[0:66])
    # edge rank r is bit r % 32 of word r / 32
    let p2 = zeros(32)
    p2[0] = 1.0
    for i in range(31):
        p2[i + 1] = p2[i] * 2.0
    for r in range(66):
        let word = floor(r / 32.0)
        BITS[r, :] = where(arange(3) == word, take(p2, r - 32.0 * word), 0.0)

step:
    let px = close[BTC]
    let now = time[BTC]
    let hour = not isnan(now) and now == floor(now / 3600.0) * 3600.0
    if hour:
        if not isnan(px) and prev > 0.0:
            push(hist, log(px / prev))
        else:
            push(hist, nan)
        prev = px
    if hour and filled(hist) == 13:
        let P = zeros(12, 2)
        for i in range(12):
            for c in range(2):
                P[i, c] = hist[i + c - 13]
        # squared distances, each summed over coordinates in ascending order
        let D2 = zeros(12, 12)
        for i in range(12):
            D2[i, :] = sum((P - P[i, :]) ** 2, 1)
        let ok = not (any(isnan(P * 0.0)) or any(isnan(D2 * 0.0)))
        # H0: Prim's minimum spanning tree; its edge lengths are the finite H0 deaths
        let NI = arange(12)
        let intree = zeros(12)
        intree[0] = 1.0
        let best = zeros(12)
        best[0:12] = D2[0, :]
        let mst = zeros(11)
        let far = 2.0 * max(D2) + 1.0
        for k in range(11):
            let cand = where(intree > 0.5, far, best)
            let j = argmin(cand)
            mst[k] = sqrt(take(cand, j))
            intree[0:12] = where(NI == j, 1.0, intree)
            best[0:12] = minimum(best, take(D2, j * 12.0 + NI))
        let deaths = take(mst, argsort(mst))
        # H1: reduce each triangle's boundary over F2, triangles and edges in filtration order
        let EL = take(D2, PIX)
        let eorder = argsort(EL)
        let erank = argsort(eorder)
        let ELs = take(EL, eorder)
        let TV = maximum(maximum(take(EL, FAB), take(EL, FAC)), take(EL, FBC))
        let torder = argsort(TV)
        let KW = arange(3)
        let EI = arange(66)
        let R = zeros(220, 3)
        let owner = full(66, -1.0)
        let life = zeros(220)
        let col = zeros(3)
        let live = zeros(1)
        let maxc = max(abs(P))
        for i in range(220):
            let tid = take(torder, i)
            let ra = take(erank, take(FAB, tid)) * 3.0
            let rb = take(erank, take(FAC, tid)) * 3.0
            let rc = take(erank, take(FBC, tid)) * 3.0
            col[0:3] = bitwise_xor(bitwise_xor(take(BITS, ra + KW), take(BITS, rb + KW)), take(BITS, rc + KW))
            live[0] = 1.0
            # each elimination lowers the pivot, so E + 1 rounds always finish
            for s in range(67):
                if live[0] > 0.5:
                    let o = take(owner, bit_length(col) - 1.0)
                    if o >= 0.0:
                        col[0:3] = bitwise_xor(col, take(R, o * 3.0 + KW))
                    else:
                        live[0] = 0.0
            let q = bit_length(col) - 1.0
            if q >= 0.0:
                owner[0:66] = where(EI == q, i, owner)
                R[i, :] = col
                let d2 = take(TV, tid)
                let b2 = take(ELs, q)
                let death = sqrt(d2)
                let lifetime = death - sqrt(b2)
                # a bar within rounding of zero length, from coordinate magnitude and radius, is none
                if lifetime > 0.0 and d2 - b2 > 64.0 * 2.220446049250313e-16 * maxc * death + 16.0 * 2.220446049250313e-16 * d2:
                    life[i] = lifetime
        let h0max = max(deaths)
        let h0sum = sum(deaths)
        let h1max = max(life)
        let h1sum = sum(life)
        if ok:
            level = tanh(gain * h1max / maximum(sqrt(max(D2)), 1e-12))
        else:
            level = nan
    emit BTC, level
```

## keep: named memory

<a id="keep"></a>

`keep last`, `keep prev[markets]`, `keep cov[markets, markets]` declare named memory that survives to the next bar, zero-initialised before the first. Assign with `name = expr` (a scalar broadcasts to every element), or `name[i] = x` / `name[i, :] = v` for part of it. Kept memory and windows are laid out in declaration order, and together count toward a 262,144-slot ceiling.

```
markets BTC, ETH, SOL
channels close
warmup 3d
param fa = 0.01 in (0.001, 0.1)
param sa = 0.001 in (0.0001, 0.01)
keep fast[markets]
keep slow[markets]

step:
    let px = close[:]
    let seen = not isnan(px)
    let first = seen and slow == 0.0
    fast = where(first, px, fast)
    slow = where(first, px, slow)
    fast = where(seen, fast + fa * (px - fast), fast)
    slow = where(seen, slow + sa * (px - slow), slow)
    emit markets, tanh((fast - slow) / maximum(slow, 0.000000000001) * 50.0)
```

## window: a rolling history

<a id="window"></a>

`window hist[markets] 1d` is a ring of the last 1440 rows of `vec[markets]` (the length is an integer, a param or a duration; `window h 30` holds scalars). `push(hist, v)` appends a row. `hist[-1]` is the newest row and `hist[-k]` the row pushed k pushes ago, with k proven between 1 and the length. `hist[:]` is the whole window, oldest row first. `mean(hist, 0)` and the other reductions with axis 0 run over time without copying, oldest to newest whatever the ring's position. `filled(hist)` is how many rows have been pushed, up to the length.

Rows start as NaN, not zero: a half-filled window read as zeros would look like data. So statistics over a window are NaN, and hold, until it is full — the same rule as the `sma` indicator. A window longer than `warmup` is a warning.

```
markets BTC, ETH, SOL
channels close
warmup 1d
param n = 1d
window hist[markets] n

step:
    let px = close[:]
    push(hist, px)
    let mu = mean(hist, 0)
    let sd = std(hist, 0)
    emit markets, -tanh((px - mu) / maximum(sd, 0.000000000001))
```

## init: once per run

<a id="init"></a>

An optional `init:` block, before `step:`, runs once when the machine is built. It may read params and constants and read and write kept memory. It may NOT read a channel (`unsupported-construct`), `emit` or `push`: it runs before any bar, which is what guarantees that a solve over the whole sample, or a normalisation from future data, cannot happen there. It is ordinary bytecode run once, so its result is bit-identical to computing the same thing in `step`. It has its own static budget of 50,000,000 fuel, charged once, and needs no declaration.

```
markets BTC
channels close
warmup 7d
param h = 1.0 in (0.1, 5.0)
param lam = 0.1 in (0.001, 10.0)
param speeds = [0.1, 0.01, 0.001] in (0.0001, 0.5)
param Z = [[-1.0, -1.0, 0.0], [-1.0, 1.0, 0.0], [1.0, -1.0, 0.0],
           [1.0, 1.0, 0.0], [0.0, 0.0, -1.0], [0.0, 0.0, 1.0]] in (-3.0, 3.0)
param y = [0.2, -0.1, 0.1, -0.2, 0.3, -0.3] in (-1.0, 1.0)
keep alpha[6]
keep last
keep m[3]
keep v[3]

init:
    let G = zeros(len(y), len(y))
    for i in range(len(y)):
        G[i, :] = exp(-sum((Z - Z[i, :]) ** 2, 1) / (h * h))
    alpha = cho_solve(cholesky(G + lam * eye(len(y))), y)

step:
    let p = close[BTC]
    if not isnan(p):
        if last > 0.0:
            let r = log(p / last)
            m = m + speeds * (r - m)
            v = v + speeds * ((r - m) ** 2 - v)
        last = p
    let x = m / sqrt(maximum(v, 0.000000000000000001))
    emit BTC, tanh(dot(alpha, exp(-sum((Z - x) ** 2, 1) / (h * h))))
```

## Params: the model's searchable hyperparameters

<a id="params"></a>

A script is a model; fitting it is the platform's job. `param` declares a constant the backtest search (Bayesian optimisation) may move, and `in (lo, hi)` is the range it may move it in: a ridge `lambda`, a kernel bandwidth, a learning rate, a window length, a number of clusters. A param that sizes an array or a loop (`param k = 8 in (1, 32)` used as `argsort(d)[0:k]`) must stay a whole number, and its range is the static bound the fuel check is made against. Every trial the search runs is counted by the tradeability gate, which is why the language does not also fit in-sample: online updates (Kalman, RLS, a gradient step on the previous bar) are part of the model's inference, walk-forward, and cost no trials.

A param may be a number, a duration, a vector or a matrix literal, with a leading `-` allowed: `param lean = -0.5`, `param w = [0.2, -0.1] in (-1.0, 1.0)`. Bounds are REQUIRED for an array param and every element must lie inside them. The plateau check perturbs a bounded scalar within its bounds, and each array element additively by a fraction of `hi - lo`, clamped. Every param element counts as a tunable coordinate (`tunableScalars`), and so does every distinct literal constant of an array literal written in `init` or `step`: weights fitted elsewhere and pasted into the body are still fitted weights. Structure does not count: -1, 0 and 1 (signs, masks, one-hot rows), a literal that is exactly an index permutation 0..n-1, and elements that are not literals (market inputs such as `[close[BTC], close[ETH]]`, param references, expressions). At most 4,096 param elements in all.

```
markets BTC
channels close
warmup 7d
param h = 1.0 in (0.1, 5.0)
param speeds = [0.1, 0.01, 0.001] in (0.0001, 0.5)
param C = [[-1.0, -1.0, 0.0], [-1.0, 1.0, 0.0], [1.0, -1.0, 0.0],
           [1.0, 1.0, 0.0], [0.0, 0.0, -1.0], [0.0, 0.0, 1.0]] in (-3.0, 3.0)
param w = [0.2, -0.1, 0.1, -0.2, 0.3, -0.3] in (-1.0, 1.0)
keep last
keep m[3]
keep v[3]

step:
    let p = close[BTC]
    if not isnan(p):
        if last > 0.0:
            let r = log(p / last)
            m = m + speeds * (r - m)
            v = v + speeds * ((r - m) ** 2 - v)
        last = p
    let x = m / sqrt(maximum(v, 0.000000000000000001))     # trend at 3 speeds
    let phi = exp(-sum((C - x) ** 2, 1) / (h * h))     # (k x d - d) per row -> vec[k]
    emit BTC, tanh(dot(w, phi))
```

## What arrays cost

<a id="array-fuel"></a>

An array instruction costs a base of 4 (its dispatch) plus a formula of its shape, measured against the kernel so that one unit is about one scalar instruction's time:

| operation | work |
|---|---|
| elementwise unary or binary, broadcast | n × (the scalar function's fuel) |
| `where` | 2n |
| constant, fill, copy, gather, `ch[:]`, `reshape` | n |
| `sum mean min max argmin argmax norm count bit_length nanmin nanmax`, `dot`, `take(a, idx)` | 2n |
| `nansum nanmean any all` | n |
| `var std` and their nan forms | 3n |
| `median nanmedian` | 2n + n·⌈log2 n⌉ |
| `argsort` | 2n + 2n·⌈log2 n⌉ |
| `logsumexp` | n × (exp + 3) + log |
| `cumsum` | 2n |
| `outer`, `transpose` | r·c |
| `@` (r×k by k×c) | ⌈r·k·c / 2⌉ |
| `cholesky` n | ⌈n³ / 8⌉ + 2n² |
| `cho_solve` n, c right-hand sides | c·(n² + 8n) |
| `solve_triangular` n, c right-hand sides | c·(⌈n² / 2⌉ + 8n) |
| `cholupdate`, `choldowndate` | n² + 8n |
| `eigh`, `eigvalsh` | 4n² + 20 sweeps × n(n-1)/2 rotations × (⌈n / 4⌉ + 24) |
| `push` n, `h[-k]` n, `h[:]` len·n | |

Axis and window reductions cost the 1D formula per output element. The exact figures are pinned in the language surface. The worst case stays static: the dearest branch and n × body rules apply, and `eigh` is priced at every sweep it may run.

```
markets BTC, ETH, SOL
channels close
warmup 1d
param n = 1d
window hist[markets] n

step:
    let px = close[:]
    push(hist, px)
    let mu = mean(hist, 0)
    let sd = std(hist, 0)
    emit markets, -tanh((px - mu) / maximum(sd, 0.000000000001))
```

## Where arrays live

<a id="array-memory"></a>

A machine owns one arena: the persistent part (`keep`, `window`) and scratch for array temporaries and `let` arrays. The compiler assigns every scratch region, reusing it after each statement and block, so memory is static. Scratch is never cleared between bars: every operation writes its whole result before anything reads it.

| limit | value |
|---|---|
| keep + windows | 262,144 slots |
| fuel per bar | 2,000,000 |
| argsort and median n | 4,096 |
| scratch | 262,144 slots |
| linear-algebra n | 128 |
| param elements | 4,096 |
| init fuel | 50,000,000 |

Every limit is a compile error (`memory-exceeded`, `fuel-exceeded`, `shape-mismatch`, `index-unproven`). At run time a numeric failure is NaN, never an exception.

```
markets BTC
channels close
warmup 14d
param h = 0.5 in (0.05, 2.0)
param forget = 0.999 in (0.99, 0.99999)
param speed = 0.01 in (0.001, 0.1)
keep c[16]
keep w[16]
keep P[16, 16]
keep phiPrev[16]
keep last
keep m
keep v

init:
    c = linspace(-3.0, 3.0, 16)
    P = 100.0 * eye(16)

step:
    let p = close[BTC]
    if not isnan(p) and last > 0.0:
        let r = log(p / last)
        let Pp = P @ phiPrev                  # learn: last bar's features -> this bar's return
        let g = Pp / (forget + dot(phiPrev, Pp))
        w = w + g * (r - dot(w, phiPrev))
        P = (P - outer(g, Pp)) / forget
        m = m + speed * (r - m)
        v = v + speed * ((r - m) ** 2 - v)
    if not isnan(p):
        last = p
    let x = m / sqrt(maximum(v, 0.000000000000000001))
    let phi = exp(-((c - x) / h) ** 2)
    phiPrev = phi
    emit BTC, tanh(dot(w, phi))
```

## Array arithmetic is ordered

<a id="determinism-arrays"></a>

Operands are evaluated left, then right. Every reduction is sequential in ascending index from an accumulator of 0 — row-major for a whole matrix, along the axis for an axis reduction, oldest to newest for a window. Matrix multiplication is `s = 0; for k ascending: s = s + a[i, k] * b[k, j]`. Median sorts with -0 before +0. Transcendentals are the engine's own. So an array program computes the same bits on every machine, and the same bits as its scalar spelling. Sequential, not pairwise: pairwise summation could be added later under a new name without changing what `sum` means.

```
markets BTC, ETH, SOL
channels close
warmup 1d

step:
    let px = close[:]
    emit markets, px / sum(px) - px * (1.0 / sum(px))
```

## Example: momentum, vectorised

<a id="example-momentum-vector"></a>

Per element this is the same arithmetic as the scalar momentum template, so it computes the same bits, in one pass over the markets instead of one block per market.

```
markets BTC, ETH, SOL
channels close
warmup 3d
param fa = 0.01 in (0.001, 0.1)
param sa = 0.001 in (0.0001, 0.01)
keep fast[markets]
keep slow[markets]

step:
    let px = close[:]
    let seen = not isnan(px)
    let first = seen and slow == 0.0
    fast = where(first, px, fast)
    slow = where(first, px, slow)
    fast = where(seen, fast + fa * (px - fast), fast)
    slow = where(seen, slow + sa * (px - slow), slow)
    emit markets, tanh((fast - slow) / maximum(slow, 0.000000000001) * 50.0)
```

## Example: mean reversion over a window

<a id="example-zscore-window"></a>

O(window × markets) per bar. An O(1) variant keeps running sums in `keep` and subtracts `hist[-n]` before each `push`; it inherits the usual cancellation caveat.

```
markets BTC, ETH, SOL
channels close
warmup 1d
param n = 1d
window hist[markets] n

step:
    let px = close[:]
    push(hist, px)
    let mu = mean(hist, 0)
    let sd = std(hist, 0)
    emit markets, -tanh((px - mu) / maximum(sd, 0.000000000001))
```

## Example: cross-sectional minimum variance

<a id="example-min-variance"></a>

About 3·n² for the covariance update plus n³ + 8n² for the solve per bar. At 64 markets, solve every k bars instead (keep the weights).

```
markets BTC, ETH, SOL, BNB, XRP, DOGE
channels close
warmup 30d
param halflife = 7d
param ridge = 0.0001 in (0.00001, 0.01)
keep prev[markets]
keep cov[markets, markets]

step:
    let px = close[:]
    let r = where(isnan(px) or prev == 0.0, 0.0, px / prev - 1.0)
    prev = where(isnan(px), prev, px)
    let a = 1.0 - exp(-0.6931471805599453 / halflife)
    cov = (1.0 - a) * cov + a * outer(r, r)
    let w = cho_solve(cholesky(cov + ridge * eye(len(markets))), ones(markets))
    emit markets, w / sum(abs(w))
```

## Example: kernel model with fitted centres

<a id="example-kernel-centers"></a>

Every element of `C` and `w` is a tunable coordinate for the gate's trial accounting; prefer the online form below when the weights can be learned.

```
markets BTC
channels close
warmup 7d
param h = 1.0 in (0.1, 5.0)
param speeds = [0.1, 0.01, 0.001] in (0.0001, 0.5)
param C = [[-1.0, -1.0, 0.0], [-1.0, 1.0, 0.0], [1.0, -1.0, 0.0],
           [1.0, 1.0, 0.0], [0.0, 0.0, -1.0], [0.0, 0.0, 1.0]] in (-3.0, 3.0)
param w = [0.2, -0.1, 0.1, -0.2, 0.3, -0.3] in (-1.0, 1.0)
keep last
keep m[3]
keep v[3]

step:
    let p = close[BTC]
    if not isnan(p):
        if last > 0.0:
            let r = log(p / last)
            m = m + speeds * (r - m)
            v = v + speeds * ((r - m) ** 2 - v)
        last = p
    let x = m / sqrt(maximum(v, 0.000000000000000001))     # trend at 3 speeds
    let phi = exp(-sum((C - x) ** 2, 1) / (h * h))     # (k x d - d) per row -> vec[k]
    emit BTC, tanh(dot(w, phi))
```

## Example: kernel model solved once in init

<a id="example-kernel-pseudo"></a>

The Gram matrix and its solve cost about k·(k·d·3 + k·24) + k³ + 8k² fuel, once per run, inside the init budget; the per-bar cost equals the centres model.

```
markets BTC
channels close
warmup 7d
param h = 1.0 in (0.1, 5.0)
param lam = 0.1 in (0.001, 10.0)
param speeds = [0.1, 0.01, 0.001] in (0.0001, 0.5)
param Z = [[-1.0, -1.0, 0.0], [-1.0, 1.0, 0.0], [1.0, -1.0, 0.0],
           [1.0, 1.0, 0.0], [0.0, 0.0, -1.0], [0.0, 0.0, 1.0]] in (-3.0, 3.0)
param y = [0.2, -0.1, 0.1, -0.2, 0.3, -0.3] in (-1.0, 1.0)
keep alpha[6]
keep last
keep m[3]
keep v[3]

init:
    let G = zeros(len(y), len(y))
    for i in range(len(y)):
        G[i, :] = exp(-sum((Z - Z[i, :]) ** 2, 1) / (h * h))
    alpha = cho_solve(cholesky(G + lam * eye(len(y))), y)

step:
    let p = close[BTC]
    if not isnan(p):
        if last > 0.0:
            let r = log(p / last)
            m = m + speeds * (r - m)
            v = v + speeds * ((r - m) ** 2 - v)
        last = p
    let x = m / sqrt(maximum(v, 0.000000000000000001))
    emit BTC, tanh(dot(alpha, exp(-sum((Z - x) ** 2, 1) / (h * h))))
```

## Example: online kernel regression (RLS)

<a id="example-online-rls"></a>

Causal by construction: the weights at bar t come only from bars before t. The 16 weights are state, not params, so they cost no trials.

```
markets BTC
channels close
warmup 14d
param h = 0.5 in (0.05, 2.0)
param forget = 0.999 in (0.99, 0.99999)
param speed = 0.01 in (0.001, 0.1)
keep c[16]
keep w[16]
keep P[16, 16]
keep phiPrev[16]
keep last
keep m
keep v

init:
    c = linspace(-3.0, 3.0, 16)
    P = 100.0 * eye(16)

step:
    let p = close[BTC]
    if not isnan(p) and last > 0.0:
        let r = log(p / last)
        let Pp = P @ phiPrev                  # learn: last bar's features -> this bar's return
        let g = Pp / (forget + dot(phiPrev, Pp))
        w = w + g * (r - dot(w, phiPrev))
        P = (P - outer(g, Pp)) / forget
        m = m + speed * (r - m)
        v = v + speed * ((r - m) ** 2 - v)
    if not isnan(p):
        last = p
    let x = m / sqrt(maximum(v, 0.000000000000000001))
    let phi = exp(-((c - x) / h) ** 2)
    phiPrev = phi
    emit BTC, tanh(dot(w, phi))
```

## Mathematical functions

<a id="activations"></a>

Every function is the engine's own implementation (fdlibm's algorithms: range reduction and fixed polynomials, no host math library), so the result is the same bits on every machine. Measured against 320-bit references, `exp expm1 log1p sin cos arctan erf elu` are within 1 ulp, `log softplus log_sigmoid arctan2` within 1.5 (log 2.03 at worst), `erfc` 2, `silu` 2.5, `tanh` 3.8 and `gelu` 4 ulp; `sin` and `cos` reduce exactly at every magnitude.

- `exp(x)`, `expm1(x)` (exp(x) - 1, accurate near 0), `log(x)`, `log1p(x)`.
- `round(x)` rounds half to even, as numpy does (`round(2.5)` is 2, `round(-0.5)` is -0); `floor`, `ceil`, `trunc`, `sign`, `abs`, `sqrt` are exact.
- `sin(x)`, `cos(x)` (time-of-day and Fourier features), `tanh(x)`, `arctan(x)`, `arctan2(y, x)` (the full IEEE table of signed zeros and infinities).
- `erf(x)`, `erfc(x)` (accurate in the tail); the normal CDF is `0.5 * erfc(-x / sqrt(2.0))`.
- `relu(x)` = maximum(x, 0); `leaky_relu(x, slope)` = x if x >= 0 else slope·x; `elu(x)` = x if x > 0 else expm1(x); `sigmoid(x)`, `log_sigmoid(x)` = -softplus(-x); `softplus(x)` = maximum(x, 0) + log1p(exp(-|x|)); `silu(x)` = x·sigmoid(x); `gelu(x)` = x·Φ(x), exact (erf, not the tanh approximation); `hard_sigmoid(x)` = clip(x / 6 + 0.5, 0, 1); `hard_tanh(x)` = clip(x, -1, 1); `soft_sign(x)` = x / (1 + |x|).

NaN in, NaN out. Each also applies elementwise. Fuel is measured per function at its slowest input range (see `array-fuel`); `sin` and `cos` are priced for the exact reduction of huge arguments.

```
markets BTC, ETH, SOL
channels close
warmup 1d
keep prev[markets]

step:
    let px = close[:]
    let r = where(isnan(px) or prev == 0.0, 0.0, px / prev - 1.0)
    prev = where(isnan(px), prev, px)
    let gate = sigmoid(r * 1000.0)                  # 0..1, never overflows
    let size = softplus(relu(abs(r) * 1000.0) - 1.0) # smooth, positive
    emit markets, (2.0 * gate - 1.0) * minimum(size, 1.0)
```

## Deterministic random numbers

<a id="random"></a>

`uniform(seed, i)` is the i-th output of splitmix64 seeded with `seed`, as a uniform in [0, 1): `z = seed + (i + 1) * 0x9E3779B97F4A7C15`, then `z ^= z >> 30; z *= 0xBF58476D1CE4E5B9; z ^= z >> 27; z *= 0x94D049BB133111EB; z ^= z >> 31`, all modulo 2^64, and the result is `(z >> 11) * 2^-53`, exactly. Both arguments must be integers in [0, 2^53), otherwise the result is NaN. `normal(seed, i)` is a standard normal by Box–Muller from counters 2i and 2i + 1: `sqrt(-2 log(1 - uniform(seed, 2i))) * cos(2π uniform(seed, 2i + 1))`, with the engine's own `ln` and a cosine reduced exactly in turns; i must be below 2^52.

There is no hidden state and no clock: a draw is a pure function of its two arguments, so replay, the JIT and every host agree, and a draw at bar t cannot depend on anything after t. Use a counter you control (a loop variable, a kept bar count) for fresh draws. With an array of counters they draw elementwise: `uniform(seed, arange(n))` is a vector of n draws and `reshape(normal(seed, arange(r * c)), r, c)` a matrix — for initialising weights in `init` and for Monte Carlo. The names are JAX's `uniform` and `normal`, the stateless, key-based generator this is: numpy's are stateful.

```
markets BTC, ETH, SOL
channels close
warmup 1d
keep w[markets]
keep t

init:
    w = normal(42, arange(markets))          # the same draws on every host, in every run

step:
    t = t + 1.0
    let u = uniform(7, t)               # counter = bar number: a fresh, reproducible draw each bar
    emit markets, tanh(w) * (0.5 + 0.5 * u)
```

## Rank-1 Cholesky updates

<a id="cholupdate"></a>

`cholupdate(L, x)` returns the lower factor of `L @ transpose(L) + outer(x, x)`, and `choldowndate(L, x)` that of `L @ transpose(L) - outer(x, x)`, for a square L of at most 128 rows. The sweep is fixed: for k ascending, `r = sqrt(L[k,k]² ± x[k]²)`, `c = r / L[k,k]`, `s = x[k] / L[k,k]`, `L[k,k] = r`; then for i > k ascending, `L[i,k] = (L[i,k] ± s·x[i]) / c` and `x[i] = c·x[i] - s·L[i,k]`. Only the lower triangle is read; the upper is zero. A pivot that is not positive — a downdate that loses definiteness, or NaN — gives an all-NaN result, which holds, exactly like `cholesky`; it never throws. Keep the factor in `keep` and test one element for NaN before adopting a result. `cho_solve(L, b)` then solves from the kept factor without the n³ refactor.

```
markets BTC, ETH, SOL, BNB, XRP, DOGE
channels close
warmup 30d
param halflife = 7d
param ridge = 0.0001 in (0.00001, 0.01)
keep prev[markets]
keep L[markets, markets]
keep k

init:
    L = sqrt(ridge) * eye(markets)

step:
    let px = close[:]
    let r = where(isnan(px) or prev == 0.0, 0.0, px / prev - 1.0)
    prev = where(isnan(px), prev, px)
    let a = 1.0 - exp(-0.6931471805599453 / halflife)
    # cov <- (1 - a) cov + a r r', plus a ridge cycled one axis per bar: two O(n^2) updates
    let e = where(arange(markets) == k, sqrt(a * ridge * markets), 0.0)
    let U = cholupdate(cholupdate(sqrt(1.0 - a) * L, sqrt(a) * r), e)
    if not isnan(U[0, 0]):
        L = U
    k = k + 1.0
    if k >= markets:
        k = 0.0
    let w = cho_solve(L, ones(markets))  # L L' w = 1 from the factor: O(n^2)
    emit markets, w / sum(abs(w))
```

## Example: minimum variance by factor updates

<a id="example-min-variance-update"></a>

Scaling L by sqrt(1 - a) and adding sqrt(a)·r is the EWMA covariance update on the factor; a second update adds the ridge along one axis per bar, which averages to `ridge * eye(n)`. The weights come from `cho_solve` on the kept factor, so no step is O(n³). It is not bit-identical to the refactoring example: the ridge enters differently.

```
markets BTC, ETH, SOL, BNB, XRP, DOGE
channels close
warmup 30d
param halflife = 7d
param ridge = 0.0001 in (0.00001, 0.01)
keep prev[markets]
keep L[markets, markets]
keep k

init:
    L = sqrt(ridge) * eye(markets)

step:
    let px = close[:]
    let r = where(isnan(px) or prev == 0.0, 0.0, px / prev - 1.0)
    prev = where(isnan(px), prev, px)
    let a = 1.0 - exp(-0.6931471805599453 / halflife)
    # cov <- (1 - a) cov + a r r', plus a ridge cycled one axis per bar: two O(n^2) updates
    let e = where(arange(markets) == k, sqrt(a * ridge * markets), 0.0)
    let U = cholupdate(cholupdate(sqrt(1.0 - a) * L, sqrt(a) * r), e)
    if not isnan(U[0, 0]):
        L = U
    k = k + 1.0
    if k >= markets:
        k = 0.0
    let w = cho_solve(L, ones(markets))  # L L' w = 1 from the factor: O(n^2)
    emit markets, w / sum(abs(w))
```

## Example: a small neural net trained online

<a id="example-mlp"></a>

Weights live in `keep`, so they are state, not params: they cost no trials. `init` draws them from fixed seeds, so every host starts from the same net. Each bar first learns from the pair it has just completed — last bar's features and this bar's return — with one gradient step written out by hand (relu's derivative is the mask `z > 0.0`), then predicts from this bar's features. The weights used at bar t therefore depend only on bars up to t, which the causality check verifies for this sample like every other: it replaces the future and requires every earlier target and the whole state to stay bit-identical.

```
markets BTC
channels close
warmup 7d
param lr = 0.01 in (0.0001, 0.1)
param speeds = [0.1, 0.01, 0.001] in (0.0001, 0.5)
keep W1[8, 3]
keep b1[8]
keep w2[8]
keep b2
keep xPrev[3]
keep last
keep m[3]
keep v[3]

init:
    W1 = 0.5 * reshape(normal(1, arange(24)), 8, 3)       # fixed seeds: the same initial net on every host
    w2 = 0.1 * normal(2, arange(8))

step:
    let p = close[BTC]
    if not isnan(p) and last > 0.0:
        let r = log(p / last)
        # learn: LAST bar's features -> THIS bar's return; one SGD step on squared error
        let z = W1 @ xPrev + b1
        let h = relu(z)
        let err = dot(w2, h) + b2 - 100.0 * r
        let dz = err * w2 * (z > 0.0)
        w2 = w2 - lr * err * h
        b2 = b2 - lr * err
        W1 = W1 - lr * outer(dz, xPrev)
        b1 = b1 - lr * dz
        m = m + speeds * (r - m)
        v = v + speeds * ((r - m) ** 2 - v)
    if not isnan(p):
        last = p
    let x = m / sqrt(maximum(v, 0.000000000000000001))
    xPrev = x
    emit BTC, tanh(dot(w2, relu(W1 @ x + b1)) + b2)
```

## Templates

<a id="templates"></a>

Each template below is a whole program: the model written from the primitives, its hyperparameters declared as bounded params for the search, and its fitted quantities kept in `fit` at the start of the kept memory. Each is checked by the engine's tests against an independent implementation, on the interpreter and the JIT. They are starting points: copy one, change the inputs and the signal, and keep the search ranges honest.

- `template-pca` — dimension reduction: PCA of a rolling covariance with eigh; trades the residual after the leading factors.
- `template-sfa` — dimension reduction: The slowest linear feature: whiten with Cholesky, then the smallest eigenvector of the whitened derivative covariance.
- `template-cca` — dimension reduction: CCA between two blocks of markets by Cholesky whitening and eigh; the first canonical correlation.
- `template-kernel-ridge` — kernel methods: RBF kernel ridge on lagged returns: exact squared distances by rows, Cholesky solve, one prediction.
- `template-gp` — kernel methods: GP posterior mean and variance and the log marginal likelihood, from one Cholesky factor.
- `template-svm` — kernel methods: A linear SVM trained by dual coordinate descent for a fixed number of sweeps, predicting the sign of the next return.
- `template-kmeans` — clustering and neighbours: Lloyd's k-means on two features with one-hot assignment by broadcasting and fixed iterations.
- `template-knn` — clustering and neighbours: kNN regression: distances to stored patterns, argsort, the mean target of the k nearest.
- `template-logistic` — classifiers and regression: Logistic regression by Newton (IRLS) steps with a ridge, each a Cholesky solve.
- `template-ridge-lasso` — classifiers and regression: Ridge in closed form and lasso by cyclic coordinate descent with soft thresholding.
- `template-softmax` — classifiers and regression: Three-class softmax regression on the next return's sign and size, by fixed gradient steps with logsumexp.
- `template-mlp` — neural: A one-hidden-layer network: forward pass, then one hand-written gradient step per bar on the previous bar's input.
- `template-gmm` — probabilistic: A two-component 1-D Gaussian mixture of returns fitted by EM with logsumexp responsibilities.
- `template-kalman` — probabilistic: A local linear trend model filtered online: predict, update, and the innovation log-likelihood.
- `template-hmm` — probabilistic: A two-state Gaussian HMM over returns: the normalised forward filter and Viterbi decoding of the last state.
- `template-bayes-linear` — probabilistic: Conjugate Bayesian regression: posterior mean and covariance, the evidence, and the matching diagonal-Gaussian ELBO.
- `template-sampling` — probabilistic: Monte Carlo with the deterministic generator: normal draws through a Cholesky factor, and a categorical draw by cumsum.

## Template: principal components

<a id="template-pca"></a>

PCA of a rolling covariance with eigh; trades the residual after the leading factors.

```
markets BTC, ETH, SOL, BNB, XRP, DOGE
channels close
warmup 1d
param factors = 2 in (1, 4)
param gain = 50.0 in (1.0, 500.0)
keep fit[6]
keep prev[markets]
window R[markets] 240

step:
    let px = close[:]
    push(R, where(prev > 0.0, log(px / prev), nan))
    prev = where(isnan(px), prev, px)
    let X = R[:]
    let Xc = X - mean(X, 0)
    let C = transpose(Xc) @ Xc / 240.0
    fit = eigvalsh(C)
    let V = eigh(C)
    # the leading components are the last columns
    let keepcols = arange(6) >= 6.0 - factors
    let P = V * keepcols
    let r = Xc[-1, :]
    let resid = r - P @ (transpose(P) @ r)
    emit markets, -tanh(gain * resid)
```

## Template: slow feature analysis

<a id="template-sfa"></a>

The slowest linear feature: whiten with Cholesky, then the smallest eigenvector of the whitened derivative covariance.

```
markets BTC, ETH, SOL, BNB, XRP, DOGE
channels close
warmup 1d
param gain = 20.0 in (1.0, 200.0)
keep fit[7]
keep prev[markets]
window R[markets] 240

step:
    let px = close[:]
    push(R, where(prev > 0.0, log(px / prev), nan))
    prev = where(isnan(px), prev, px)
    let X = R[:]
    let Xc = X - mean(X, 0)
    let L = cholesky(transpose(Xc) @ Xc / 240.0 + 0.0000000001 * eye(6))
    let D = X[1:240, :] - X[0:239, :]
    let Cd = transpose(D) @ D / 239.0
    # whitened derivative covariance M = L^-1 Cd L^-T
    let M = solve_triangular(L, transpose(solve_triangular(L, Cd)))
    let lam = eigvalsh(M)
    let w = solve_triangular(L, eigh(M)[:, 0], 1)
    fit[0] = lam[0]
    for i in range(6):
        fit[i + 1] = w[i]
    emit markets, tanh(gain * dot(w, Xc[-1, :])) * w / maximum(norm(w), 0.000000000001)
```

## Template: canonical correlation

<a id="template-cca"></a>

CCA between two blocks of markets by Cholesky whitening and eigh; the first canonical correlation.

```
markets BTC, ETH, SOL, BNB, XRP, DOGE
channels close
warmup 1d
param gain = 2.0 in (0.1, 20.0)
keep fit[1]
keep prev[markets]
window R[markets] 240

step:
    let px = close[:]
    push(R, where(prev > 0.0, log(px / prev), nan))
    prev = where(isnan(px), prev, px)
    let X = R[:]
    let Xc = X - mean(X, 0)
    let C = transpose(Xc) @ Xc / 240.0 + 0.0000000001 * eye(6)
    let A = C[0:3, 0:3]
    let B = C[3:6, 3:6]
    let AB = C[0:3, 3:6]
    let La = cholesky(A)
    let Lb = cholesky(B)
    let K = solve_triangular(La, transpose(solve_triangular(Lb, transpose(AB))))
    let rho = sqrt(maximum(max(eigvalsh(K @ transpose(K))), 0.0))
    fit[0] = rho
    emit markets, full(6, tanh(gain * (rho - 0.5)))
```

## Template: kernel ridge regression

<a id="template-kernel-ridge"></a>

RBF kernel ridge on lagged returns: exact squared distances by rows, Cholesky solve, one prediction.

```
markets BTC
channels close
warmup 1d
param width = 0.0002 in (0.00001, 0.01)
param ridge = 0.1 in (0.001, 10.0)
keep fit[1]
keep prev
window h 68

step:
    let px = close[BTC]
    push(h, where(prev > 0.0, log(px / prev), nan))
    prev = where(isnan(px), prev, px)
    let X = zeros(64, 4)
    let y = zeros(64)
    for i in range(64):
        for k in range(4):
            X[i, k] = h[i + k - 68]
        y[i] = h[i - 64]
    let G = zeros(64, 64)
    for i in range(64):
        G[i, :] = exp(-sum((X - X[i, :]) ** 2, 1) / width)
    let alpha = cho_solve(cholesky(G + ridge * eye(64)), y)
    let xs = zeros(4)
    for k in range(4):
        xs[k] = h[k - 4]
    fit[0] = dot(exp(-sum((X - xs) ** 2, 1) / width), alpha)
    emit BTC, tanh(fit[0] * 100.0)
```

## Template: Gaussian process regression

<a id="template-gp"></a>

GP posterior mean and variance and the log marginal likelihood, from one Cholesky factor.

```
markets BTC
channels close
warmup 1d
param width = 0.0002 in (0.00001, 0.01)
param amp = 0.0001 in (0.000001, 0.01)
param noise = 0.0001 in (0.000001, 0.01)
keep fit[3]
keep prev
window h 36

step:
    let px = close[BTC]
    push(h, where(prev > 0.0, log(px / prev), nan))
    prev = where(isnan(px), prev, px)
    let X = zeros(32, 4)
    let y = zeros(32)
    for i in range(32):
        for k in range(4):
            X[i, k] = h[i + k - 36]
        y[i] = h[i - 32]
    let K = zeros(32, 32)
    for i in range(32):
        K[i, :] = amp * exp(-sum((X - X[i, :]) ** 2, 1) / width)
    let L = cholesky(K + noise * eye(32))
    let alpha = cho_solve(L, y)
    let xs = zeros(4)
    for k in range(4):
        xs[k] = h[k - 4]
    let ks = amp * exp(-sum((X - xs) ** 2, 1) / width)
    let v = solve_triangular(L, ks)
    fit[0] = dot(ks, alpha)
    fit[1] = amp - dot(v, v)
    # log p(y) = -y'K^-1 y / 2 - log det(K) / 2 - n log(2 pi) / 2, log det from the factor
    fit[2] = -0.5 * dot(y, alpha) - sum(log(diag(L))) - 16.0 * log(6.283185307179586)
    emit BTC, tanh(fit[0] / sqrt(maximum(fit[1], 0.000000000001)))
```

## Template: support vector machine

<a id="template-svm"></a>

A linear SVM trained by dual coordinate descent for a fixed number of sweeps, predicting the sign of the next return.

```
markets BTC
channels close
warmup 1d
param cost = 1.0 in (0.01, 100.0)
param sweeps = 10 in (2, 20)
keep fit[5]
keep prev
window h 68

step:
    let px = close[BTC]
    push(h, where(prev > 0.0, log(px / prev), nan))
    prev = where(isnan(px), prev, px)
    let X = zeros(64, 4)
    let y = zeros(64)
    for i in range(64):
        for k in range(4):
            X[i, k] = h[i + k - 68] * 100.0
        y[i] = where(h[i - 64] > 0.0, 1.0, -1.0)
    let Q = sum(X * X, 1)
    let a = zeros(64)
    let w = zeros(4)
    for s in range(sweeps):
        for i in range(64):
            let g = y[i] * dot(w, X[i, :]) - 1.0
            let ai = minimum(maximum(a[i] - g / maximum(Q[i], 0.000000000001), 0.0), cost)
            w[0:4] = w + (ai - a[i]) * y[i] * X[i, :]
            a[i] = ai
    for k in range(4):
        fit[k] = w[k]
    let xs = zeros(4)
    for k in range(4):
        xs[k] = h[k - 4] * 100.0
    fit[4] = dot(w, xs)
    emit BTC, tanh(fit[4])
```

## Template: k-means

<a id="template-kmeans"></a>

Lloyd's k-means on two features with one-hot assignment by broadcasting and fixed iterations.

```
markets BTC
channels close
warmup 1d
param rounds = 8 in (2, 20)
keep fit[3, 2]
keep prev
window h 129

step:
    let px = close[BTC]
    push(h, where(prev > 0.0, log(px / prev), nan))
    prev = where(isnan(px), prev, px)
    let X = zeros(128, 2)
    for i in range(128):
        X[i, 0] = h[i - 129] * 100.0
        X[i, 1] = h[i - 128] * 100.0
    # three centres from the first, middle and last points
    let M = zeros(3, 2)
    M[0, :] = X[0, :]
    M[1, :] = X[64, :]
    M[2, :] = X[127, :]
    for it in range(rounds):
        let D = zeros(128, 3)
        for j in range(3):
            D[:, j] = sum((X - M[j, :]) ** 2, 1)
        let nearest = argmin(D, 1)
        let onehot = reshape(nearest, 128, 1) == arange(3)
        let counts = sum(onehot, 0)
        let sums = transpose(onehot) @ X
        M[0:3, :] = where(reshape(counts, 3, 1) > 0.0 + zeros(3, 2), sums / reshape(maximum(counts, 1.0), 3, 1), M)
    fit = M
    emit BTC, tanh(0.5 * mean(M[:, 1]))
```

## Template: k nearest neighbours

<a id="template-knn"></a>

kNN regression: distances to stored patterns, argsort, the mean target of the k nearest.

```
markets BTC
channels close
warmup 1d
param k = 8 in (1, 32)
keep fit[1]
keep prev
window h 260

step:
    let px = close[BTC]
    push(h, where(prev > 0.0, log(px / prev), nan))
    prev = where(isnan(px), prev, px)
    let X = zeros(256, 4)
    let y = zeros(256)
    for i in range(256):
        for j in range(4):
            X[i, j] = h[i + j - 260]
        y[i] = h[i - 256]
    let q = zeros(4)
    for j in range(4):
        q[j] = h[j - 4]
    let order = argsort(sum((X - q) ** 2, 1))
    fit[0] = mean(take(y, order[0:k]))
    emit BTC, tanh(fit[0] * 1000.0)
```

## Template: logistic regression

<a id="template-logistic"></a>

Logistic regression by Newton (IRLS) steps with a ridge, each a Cholesky solve.

```
markets BTC
channels close
warmup 1d
param ridge = 1.0 in (0.01, 100.0)
param steps = 6 in (1, 12)
keep fit[5]
keep prev
window h 68

step:
    let px = close[BTC]
    push(h, where(prev > 0.0, log(px / prev), nan))
    prev = where(isnan(px), prev, px)
    let X = ones(64, 5)
    let y = zeros(64)
    for i in range(64):
        for k in range(4):
            X[i, k + 1] = h[i + k - 68] * 100.0
        y[i] = h[i - 64] > 0.0
    let w = zeros(5)
    for s in range(steps):
        let p = sigmoid(X @ w)
        let H = transpose(X * reshape(p * (1.0 - p), 64, 1)) @ X + ridge * eye(5)
        let g = transpose(X) @ (p - y) + ridge * w
        w[0:5] = w - cho_solve(cholesky(H), g)
    fit = w
    let xs = ones(5)
    for k in range(4):
        xs[k + 1] = h[k - 4] * 100.0
    emit BTC, 2.0 * sigmoid(dot(w, xs)) - 1.0
```

## Template: ridge and lasso

<a id="template-ridge-lasso"></a>

Ridge in closed form and lasso by cyclic coordinate descent with soft thresholding.

```
markets BTC
channels close
warmup 1d
param ridge = 1.0 in (0.001, 100.0)
param l1 = 0.01 in (0.0001, 1.0)
param sweeps = 20 in (2, 50)
keep fit[8]
keep prev
window h 132

step:
    let px = close[BTC]
    push(h, where(prev > 0.0, log(px / prev), nan))
    prev = where(isnan(px), prev, px)
    let X = zeros(128, 4)
    let y = zeros(128)
    for i in range(128):
        for k in range(4):
            X[i, k] = h[i + k - 132] * 100.0
        y[i] = h[i - 128] * 100.0
    let G = transpose(X) @ X
    let c = transpose(X) @ y
    let wr = cho_solve(cholesky(G + ridge * eye(4)), c)
    let wl = zeros(4)
    for s in range(sweeps):
        for j in range(4):
            let rho = c[j] - dot(G[j, :], wl) + G[j, j] * wl[j]
            wl[j] = sign(rho) * maximum(abs(rho) - l1 * 128.0, 0.0) / G[j, j]
    for j in range(4):
        fit[j] = wr[j]
        fit[j + 4] = wl[j]
    let xs = zeros(4)
    for k in range(4):
        xs[k] = h[k - 4] * 100.0
    emit BTC, tanh(dot(wl, xs))
```

## Template: softmax regression

<a id="template-softmax"></a>

Three-class softmax regression on the next return's sign and size, by fixed gradient steps with logsumexp.

```
markets BTC
channels close
warmup 1d
param rate = 0.5 in (0.01, 2.0)
param steps = 30 in (5, 100)
keep fit[3]
keep prev
window h 132

step:
    let px = close[BTC]
    push(h, where(prev > 0.0, log(px / prev), nan))
    prev = where(isnan(px), prev, px)
    let X = ones(128, 3)
    let Y = zeros(128, 3)
    for i in range(128):
        X[i, 1] = h[i - 132] * 100.0
        X[i, 2] = h[i - 131] * 100.0
        let r = h[i - 128] * 100.0
        Y[i, 0] = r < -0.05
        Y[i, 1] = r >= -0.05 and r <= 0.05
        Y[i, 2] = r > 0.05
    let W = zeros(3, 3)
    for s in range(steps):
        let Z = X @ W
        let P = exp(Z - reshape(logsumexp(Z, 1), 128, 1))
        W[0:3, :] = W - rate / 128.0 * (transpose(X) @ (P - Y))
    let xs = ones(3)
    xs[1] = h[-2] * 100.0
    xs[2] = h[-1] * 100.0
    let z = xs @ W
    fit = exp(z - logsumexp(z))
    emit BTC, fit[2] - fit[0]
```

## Template: a small MLP, trained online

<a id="template-mlp"></a>

A one-hidden-layer network: forward pass, then one hand-written gradient step per bar on the previous bar's input.

```
markets BTC
channels close
warmup 1d
param rate = 0.01 in (0.0001, 0.1)
keep fit[1]
keep W1[16, 8]
keep b1[16]
keep w2[16]
keep xprev[8]
keep prev
window h 8

init:
    W1 = (reshape(uniform(1.0, arange(128)), 16, 8) - 0.5) * 0.5
    w2 = (uniform(2.0, arange(16)) - 0.5) * 0.5

step:
    let px = close[BTC]
    let r = where(prev > 0.0, log(px / prev), nan) * 100.0
    prev = where(isnan(px), prev, px)
    if not isnan(r) and filled(h) == 8:
        # learn: the previous input should have predicted this return
        let a = tanh(W1 @ xprev + b1)
        let e = dot(w2, a) - r
        let da = e * w2 * (1.0 - a * a)
        w2 = w2 - rate * e * a
        W1 = W1 - rate * outer(da, xprev)
        b1 = b1 - rate * da
    push(h, r)
    let x = h[:]
    xprev = where(isnan(x), 0.0, x)
    fit[0] = dot(w2, tanh(W1 @ xprev + b1))
    emit BTC, tanh(fit[0])
```

## Template: Gaussian mixture by EM

<a id="template-gmm"></a>

A two-component 1-D Gaussian mixture of returns fitted by EM with logsumexp responsibilities.

```
markets BTC
channels close
warmup 1d
param rounds = 20 in (5, 50)
keep fit[7]
keep prev
window h 256

step:
    let px = close[BTC]
    push(h, where(prev > 0.0, log(px / prev), nan) * 100.0)
    prev = where(isnan(px), prev, px)
    let x = reshape(h[:], 256, 1)
    let s0 = std(x)
    let mu = (2.0 * arange(2) - 1.0) * s0
    let va = ones(2) * s0 * s0
    let pi = full(2, 0.5)
    let ll = zeros(1)
    for it in range(rounds):
        let J = log(pi) - 0.5 * log(6.283185307179586 * va) - 0.5 * (x - mu) ** 2 / va
        let lse = reshape(logsumexp(J, 1), 256, 1)
        let G = exp(J - lse)
        let nk = sum(G, 0)
        mu[0:2] = sum(G * x, 0) / nk
        va[0:2] = maximum(sum(G * (x - mu) ** 2, 0) / nk, 0.000001)
        pi[0:2] = nk / 256.0
        ll[0] = sum(lse)
    fit[0] = mu[0]
    fit[1] = mu[1]
    fit[2] = va[0]
    fit[3] = va[1]
    fit[4] = pi[0]
    fit[5] = pi[1]
    fit[6] = ll[0]
    let hi = argmax(va)
    emit BTC, where(hi > 0.5, 1.0 - 2.0 * pi[1], 1.0 - 2.0 * pi[0]) * 0.0 + tanh(mu[0] * pi[0] + mu[1] * pi[1])
```

## Template: Kalman filter

<a id="template-kalman"></a>

A local linear trend model filtered online: predict, update, and the innovation log-likelihood.

```
markets BTC
channels close
warmup 1d
param q = 0.000001 in (0.00000001, 0.0001)
param obs = 0.0001 in (0.000001, 0.01)
keep fit[3]
keep x[2]
keep P[2, 2]
keep started

init:
    P = 1.0 * eye(2)

step:
    let z = log(close[BTC])
    if not isnan(z):
        if started == 0.0:
            x[0] = z
            started = 1.0
        let F = eye(2)
        F[0, 1] = 1.0
        let xp = F @ x
        let Pp = F @ P @ transpose(F) + q * eye(2)
        let s = Pp[0, 0] + obs
        let k = Pp[:, 0] / s
        let innov = z - xp[0]
        x = xp + k * innov
        P = Pp - outer(k, Pp[0, :])
        fit[0] = x[0]
        fit[1] = x[1]
        fit[2] = fit[2] - 0.5 * (log(6.283185307179586 * s) + innov * innov / s)
    emit BTC, tanh(x[1] / sqrt(P[1, 1]))
```

## Template: hidden Markov model

<a id="template-hmm"></a>

A two-state Gaussian HMM over returns: the normalised forward filter and Viterbi decoding of the last state.

```
markets BTC
channels close
warmup 1d
param stay = 0.95 in (0.5, 0.999)
param calm = 0.5 in (0.1, 2.0)
param wild = 2.0 in (0.5, 10.0)
keep fit[4]
keep prev
window h 128

step:
    let px = close[BTC]
    push(h, where(prev > 0.0, log(px / prev), nan) * 100.0)
    prev = where(isnan(px), prev, px)
    let x = h[:]
    let logA = log(stay * eye(2) + (1.0 - stay) * (1.0 - eye(2)))
    let sd = where(arange(2) > 0.5, wild, calm)
    let alpha = full(2, 0.5)
    let delta = log(full(2, 0.5))
    let loglik = zeros(1)
    for t in range(128):
        let e = -0.5 * (x[t] / sd) ** 2 - log(sd) - 0.9189385332046727
        let a = (alpha @ exp(logA)) * exp(e)
        loglik[0] = loglik[0] + log(sum(a))
        alpha[0:2] = a / sum(a)
        let m = reshape(delta, 2, 1) + logA
        delta[0:2] = max(m, 0) + e
    fit[0] = alpha[0]
    fit[1] = alpha[1]
    fit[2] = argmax(delta)
    fit[3] = loglik[0]
    emit BTC, alpha[0] - alpha[1]
```

## Template: Bayesian linear regression

<a id="template-bayes-linear"></a>

Conjugate Bayesian regression: posterior mean and covariance, the evidence, and the matching diagonal-Gaussian ELBO.

```
markets BTC
channels close
warmup 1d
param prior = 1.0 in (0.01, 100.0)
param noise = 1.0 in (0.01, 100.0)
keep fit[3]
keep prev
window h 68

step:
    let px = close[BTC]
    push(h, where(prev > 0.0, log(px / prev), nan) * 100.0)
    prev = where(isnan(px), prev, px)
    let X = zeros(64, 4)
    let y = zeros(64)
    for i in range(64):
        for k in range(4):
            X[i, k] = h[i + k - 68]
        y[i] = h[i - 64]
    # posterior precision A = XtX / noise + I / prior, mean m = A^-1 Xt y / noise
    let A = transpose(X) @ X / noise + eye(4) / prior
    let L = cholesky(A)
    let m = cho_solve(L, transpose(X) @ y / noise)
    let logdetA = 2.0 * sum(log(diag(L)))
    let resid = y - X @ m
    # log evidence: the marginal likelihood of y, in closed form
    let evidence = -0.5 * (dot(resid, resid) / noise + dot(m, m) / prior + logdetA + 4.0 * log(prior) + 64.0 * log(6.283185307179586 * noise))
    # mean-field q(w) = N(m, diag(s)), s = 1 / diag(A): the ELBO in closed form
    let s = 1.0 / diag(A)
    let fitq = dot(resid, resid) + dot(sum(X * X, 0), s)
    let elbo = -0.5 * fitq / noise - 32.0 * log(6.283185307179586 * noise) - 0.5 * (sum(s + m * m) / prior - 4.0 + 4.0 * log(prior) - sum(log(s)))
    fit[0] = evidence
    fit[1] = elbo
    fit[2] = dot(m, m)
    let xs = zeros(4)
    for k in range(4):
        xs[k] = h[k - 4]
    let v = solve_triangular(L, xs)
    emit BTC, tanh(dot(m, xs) / sqrt(noise + dot(v, v)))
```

## Template: posterior predictive sampling

<a id="template-sampling"></a>

Monte Carlo with the deterministic generator: normal draws through a Cholesky factor, and a categorical draw by cumsum.

```
markets BTC, ETH
channels close, time
warmup 1d
param draws = 256 in (16, 1024)
keep fit[3]
keep prev[markets]
window R[markets] 64

step:
    let px = close[:]
    push(R, where(prev > 0.0, log(px / prev), nan) * 100.0)
    prev = where(isnan(px), prev, px)
    let X = R[:]
    let mu = mean(X, 0)
    let Xc = X - mu
    let L = cholesky(transpose(Xc) @ Xc / 64.0 + 0.000001 * eye(2))
    # seed from the bar's time: the same draws at the same instant on every replay
    let seed = floor(time[BTC] / 60.0)
    let Z = reshape(normal(seed, arange(2 * draws)), draws, 2)
    let S = Z @ transpose(L) + mu
    let spread = S[:, 0] - S[:, 1]
    fit[0] = mean(spread > 0.0)
    let w = exp(-abs(spread))
    let c = cumsum(w / sum(w))
    # a categorical draw: the first index whose cumulative weight reaches a uniform
    fit[1] = argmax(c >= uniform(seed + 1.0, 0.0))
    fit[2] = mean(spread)
    emit BTC, 2.0 * fit[0] - 1.0
    emit ETH, 1.0 - 2.0 * fit[0]
```

## Unbounded iteration

<a id="unbounded-loop"></a>

`while` does not exist, and `for` takes `range(n)` with `n` a constant. This is the single rule the rest of the design rests on: the worst-case instruction count has to be computable BEFORE the program runs, because that number is what decides whether the program is admissible at all. A loop whose trip count depends on data has no such number. The cost of the rule is that some algorithms have to be written with a fixed bound; the benefit is that a program can never become an economic object without its cost being known.

```text
unbounded-loop: `while` does not exist in this language.
  hint: use `for i in range(n)` with a constant bound. The worst-case
        cost has to be known before the program runs.
```

## Fuel

<a id="fuel"></a>

Every instruction costs, the costs are fixed, and the compiler proves an upper bound on the total for one bar. A program whose proven worst case is above 2,000,000 is refused, or under `every` that times the stride in minutes, up to 15 times; `init` has its own once-per-run cap of 50,000,000. One unit is about one dispatched scalar instruction; an array op is priced from its shape by a formula measured against its kernel (see `array-fuel`).

Two things follow that are easy to get backwards. The cost is WORST-CASE, so a branch that rarely runs still costs whatever its expensive side costs — admissibility cannot depend on data. And the fuel cap decides ADMISSIBILITY, not output: a program within budget computes exactly what it would compute with no cap at all. Fuel is a pricing lever, not a parameter of the result.

Some operations are more expensive than they look. A rolling sum over a window is O(window) per bar, not O(1), because summing a window freshly and maintaining a running total give different answers in the last bits — float addition is not associative — and the platform needs the answer that matches the published one.

## Why the arithmetic is ours

<a id="determinism"></a>

Fees are settled against a replayed curve. If a replay could differ from the original in the last bit, the settlement would be arguing with itself.

So: no host math library, no wall clock, no allocation during a step, no iteration order that depends on a hash table, and a summation order that is part of what a program computes rather than an implementation detail either side may choose. Accuracy is a nicety; reproducibility is the product.

## Versions

<a id="versioning"></a>

This reference describes language 1.0.0, surface `0x2e1293f95439a19561cf4bef525f73d72b248b36b4bc8c90ee87ec74bcc2d26c`.

The language version is a compatibility claim: a later version only adds — a new primitive, a new channel — and never changes what an existing program computes or costs. The surface digest covers everything a program can observe — keywords, operators, builtins, channels, diagnostic codes, limits, and every opcode with its cost — and changes exactly when this document's content changes.

If you are an agent caching this reference, compare the surface digest, not the engine hash. The engine hash changes when any engine file changes, including ones this document does not describe, so branching on it would report staleness on almost every deploy — and a staleness signal that is usually wrong trains its reader to ignore the one time it is right.

## Unterminated string

<a id="unterminated-string"></a>

A quote opened and the line ended. The language has no strings in expressions at all — they appear only where the grammar names one — so this usually means a stray quote rather than a missing one.

## Unknown character

<a id="unknown-character"></a>

The character is not part of the language. A tab is reported here specifically: indentation is significant, and a file mixing tabs and spaces has a block structure that depends on the reader's editor settings. Refusing tabs is cheaper than making indentation width a configuration question.

## Malformed number

<a id="bad-number"></a>

A numeric literal the lexer could not finish reading — a trailing `.`, an exponent with no digits, or a duration with an unknown unit. The duration units are `m`, `h`, `d`, `w`.

## Unexpected token

<a id="unexpected-token"></a>

The parser had no rule for this token in this position. One diagnostic is reported per position and the parser then recovers at the next statement, so a single mistake does not produce a page of errors describing its aftermath.

## Expected a token that is not there

<a id="expected-token"></a>

A construct is missing its closing piece — a `)`, or the `:` that opens a block. Reported as a point rather than a span, because there is nothing to underline and underlining the previous token would blame the wrong thing.

## A construct this language does not have

<a id="unsupported-construct"></a>

Refused BY NAME, with the reason, rather than reported as a syntax error. `while` is the common one — see the bounded-iteration rule. The distinction matters: a syntax error says you typed it wrong, and this says you typed it right and we do not have it.

## Market not declared

<a id="undeclared-market"></a>

Every market a program reads is named in the `markets` line. The declaration is what the strategy hash covers, so a market that appeared only in the body would be a data dependency nothing recorded.

## Channel not declared

<a id="undeclared-channel"></a>

Same rule as markets, for the input series. The channels available are `close`, `high`, `low`, `open`, `volume`, `funding`, `buy_volume`, `open_interest`, `time`.

## Declared twice

<a id="duplicate-declaration"></a>

The declared sets are canonicalised — sorted and deduplicated — because that canonical form is what is hashed. Rather than silently dropping the repeat, the program is refused: a list someone wrote twice is a list they are not reading carefully, and hashing a quiet repair would make two different sources hash the same.

## Manifest field missing

<a id="missing-manifest-field"></a>

`markets`, `channels` and `warmup` are required and have no default. A default here would be a number chosen by us that silently becomes part of someone's strategy.

## Manifest value outside its limit

<a id="manifest-out-of-range"></a>

The ceilings exist so a mistake is refused rather than accepted and priced. They are admissibility, not behaviour: a program within them computes exactly what it would compute without them.

## Duration off the stride

<a id="stride-mismatch"></a>

Under `every`, a program runs once per stride and its windows count bars of the stride, so every duration it writes must be a whole number of strides: `every 1h` takes `24h` or `3d`, not `30m`. A plain number already counts bars of the stride.

## Unknown name

<a id="unknown-name"></a>

Not a declared market or channel, not a `let` binding in scope, and not a builtin. There is no global namespace to fall back to and no import to add one.

## Type mismatch

<a id="type-mismatch"></a>

Conditions are boolean and arithmetic is numeric. `if x:` on a number is refused rather than read as `x != 0`, because the two readings differ for NaN and NaN is a value this data actually contains. Write the comparison you mean — `if x > 0.0:` — and use `isnan(x)` to say what a missing reading should do.

The conversion runs one way only. A condition IS a number where one is wanted, 1 or 0, which is exact rather than a guess: `emit BTC, close[BTC] > open[BTC]` is a full-or-flat position, and the same predicate over many markets is a breadth reading.

## Over the fuel budget

<a id="fuel-exceeded"></a>

The program's proven worst case per bar is above the ceiling of 2,000,000, or `init` is above 50,000,000. This is the check that makes an expensive program a refusal at admission rather than a slow executor in production. Note that the cost is worst-case, not typical: a branch that rarely runs still costs whatever its expensive side costs, because admissibility cannot depend on data.

## Over the memory budget

<a id="memory-exceeded"></a>

Kept memory and windows together exceed their slot ceiling. They are what a machine seeing one bar has to carry in order to look back at all; a rolling window is a ring buffer, and its size is its window. The ceiling is 262,144 slots, and array temporaries have their own ceiling of 262,144.

## Shapes do not fit

<a id="shape-mismatch"></a>

Array shapes are static, so a mismatch is found before the program runs. Broadcasting is numpy's for rank 1 and 2: a vector is a row, and each dimension must match or be 1, so `mat[r, c]` combines with `vec[c]`, `mat[1, c]`, `mat[r, 1]` or `mat[1, 1]`; `reshape(v, n, 1)` makes a column. `@` needs inner dimensions that agree, and the factorisations a square matrix of at most 128 rows.

## Index not proven in range

<a id="index-unproven"></a>

Every index is proven in range at compile time, so a read can never land outside its array and there is no out-of-bounds behaviour to specify. An index may be a constant, a param, `len(a)`, a loop variable, and `+` or `-` of those; a negative constant counts from the end. A data-dependent read is `take(a, k)`, with `k` a number or an index array, which returns NaN where an index is not an integer in range, the same NaN that means missing everywhere else.

## Declared and never used

<a id="unused-declaration"></a>

A warning, not a refusal. A market declared and never read is most often a leftover from an edit, and it is not harmless: the declaration is part of what is hashed, so it makes two identical strategies hash differently.
