Skip to content

The pnl script language

Language 1.0.0, surface 0x2e1293f95439a19561cf4bef525f73d72b248b36b4bc8c90ee87ec74bcc2d26c. Generated from the compiler’s own tables — if it is not in here, it is not in the language.

The same reference as plain markdown, for agents and for anything that would rather not parse a page.

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

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 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)

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)

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.

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)

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))

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.

  • 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 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)

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

Section titled “One bar at a time, and what that rules out”

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.

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.

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)

  • [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)

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)

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

+ - * /, 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)

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)

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

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))

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

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

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 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 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))

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

Section titled “Params: the model’s searchable hyperparameters”

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))

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:

operationwork
elementwise unary or binary, broadcastn × (the scalar function’s fuel)
where2n
constant, fill, copy, gather, ch[:], reshapen
sum mean min max argmin argmax norm count bit_length nanmin nanmax, dot, take(a, idx)2n
nansum nanmean any alln
var std and their nan forms3n
median nanmedian2n + n·⌈log2 n⌉
argsort2n + 2n·⌈log2 n⌉
logsumexpn × (exp + 3) + log
cumsum2n
outer, transposer·c
@ (r×k by k×c)⌈r·k·c / 2⌉
cholesky n⌈n³ / 8⌉ + 2n²
cho_solve n, c right-hand sidesc·(n² + 8n)
solve_triangular n, c right-hand sidesc·(⌈n² / 2⌉ + 8n)
cholupdate, choldowndaten² + 8n
eigh, eigvalsh4n² + 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))

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.

limitvalue
keep + windows262,144 slots
fuel per bar2,000,000
argsort and median n4,096
scratch262,144 slots
linear-algebra n128
param elements4,096
init fuel50,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))

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))

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)

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))

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))

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))

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))))

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))

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)

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)

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

Section titled “Example: minimum variance by factor updates”

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

Section titled “Example: a small neural net trained online”

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)

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.

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)

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)

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)))

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)

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)))

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])

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]))

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)

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

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))

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]

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])

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])

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]))

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]

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)))

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]

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.