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.
The algo VM
Section titled “The algo VM”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
Section titled “A program”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, ETHchannels closewarmup 21dparam 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.0The manifest
Section titled “The manifest”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
Section titled “Every: the evaluation stride”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 BTCchannels close, volumeevery 1h at 17mwarmup 2dparam n = 24hwindow hist n
step: push(hist, close[BTC]) let m = mean(hist) emit BTC, where(close[BTC] > m, 1.0, 0.0)Market names
Section titled “Market names”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 closewarmup 1dstep: let spread = close["xyz:SP500"] / close["xyz:GOLD"] emit "xyz:SP500", tanh(spread - 1.0) emit "xyz:GOLD", tanh(1.0 - spread)Channels
Section titled “Channels”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
Section titled “Order flow”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, SOLchannels volume, buy_volumewarmup 1hkeep 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 BTCchannels volume, buy_volumewarmup 1hkeep 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 BTCchannels close, open_interestwarmup 1dkeep lastOikeep lastPxkeep 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
Section titled “Values, and NaN”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, HYPEchannels closeevery 1hwarmup 3hwindow 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
Section titled “Expressions”Binary operators, loosest first, every level left-associative:
orand==,!=<,<=,>,>=+,-*,/,@
@ 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
Section titled “Statements”let name = exprbinds 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), withna constant.emit MARKET, exprsets the target exposure for a market. A market that is emitted to is one the strategy may hold.emit markets, vsets every market’s target from avec[markets].name = exprandname[i] = exprwrite kept memory (keep), andname[i] = expr/name[i, :] = vwrite elements or rows of aletarray.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
Section titled “Script controlled target filtering”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, ETHchannels open, close, timewarmup 2hparam 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
Section titled “Builtins”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.
Kept memory, and what looking back costs
Section titled “Kept memory, and what looking back costs”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
Section titled “Arrays”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, SOLchannels closewarmup 1dkeep 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
Section titled “Making arrays”[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 tolo + i * ((hi - lo) / (n - 1)), in exactly that order, and needs n >= 2.ch[:]— channelchat this bar across the declared markets, avec[markets]in canonical (sorted) market order.len(a)— the first dimension, a compile-time integer;len(markets)is the market count andlen(w)a window’s length.
markets BTC, ETH, SOLchannels closewarmup 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
Section titled “Indexing and slicing”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, SOLchannels closewarmup 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
Section titled “Broadcasting”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, SOLchannels closewarmup 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.0Elementwise operations
Section titled “Elementwise operations”+ - * /, 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, SOLchannels closewarmup 1dkeep 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
Section titled “Reductions”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, SOLchannels closewarmup 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
Section titled “Vectors across markets”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, SOLchannels closewarmup 1dkeep 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) * breadthLinear algebra
Section titled “Linear algebra”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): solvesL @ transpose(L) @ x = bfrom the factor, forward then back, forba vector or a matrix of right-hand sides.solve_triangular(L, b[, trans]): solvesL @ x = b(forward), or withtrans1transpose(L) @ x = b(back). scipy’s name and third argument; the factor is always the lower triangle, which is whatcholeskyreturns.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, DOGEchannels closewarmup 30dparam halflife = 7dparam 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
Section titled “Sorting and gathering”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, SOLchannels closewarmup 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.0Bit sets
Section titled “Bit sets”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, SOLchannels closewarmup 1d
step: let a = bitwise_xor(arange(3), 5.0) let high = bit_length(a) - 1.0 emit markets, (a - high) * 0.0Example: Vietoris–Rips persistence
Section titled “Example: Vietoris–Rips persistence”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 BTCchannels close, timewarmup 1dparam gain = 0.2 in (0.01, 1.0)keep prevkeep levelwindow hist 13keep 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, levelkeep: named memory
Section titled “keep: named memory”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, SOLchannels closewarmup 3dparam 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
Section titled “window: a rolling history”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, SOLchannels closewarmup 1dparam n = 1dwindow 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
Section titled “init: once per run”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 BTCchannels closewarmup 7dparam 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 lastkeep 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 BTCchannels closewarmup 7dparam 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 lastkeep 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
Section titled “What arrays cost”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, SOLchannels closewarmup 1dparam n = 1dwindow 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
Section titled “Where arrays live”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 BTCchannels closewarmup 14dparam 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 lastkeep mkeep 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
Section titled “Array arithmetic is ordered”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, SOLchannels closewarmup 1d
step: let px = close[:] emit markets, px / sum(px) - px * (1.0 / sum(px))Example: momentum, vectorised
Section titled “Example: momentum, vectorised”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, SOLchannels closewarmup 3dparam 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
Section titled “Example: mean reversion over a window”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, SOLchannels closewarmup 1dparam n = 1dwindow 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
Section titled “Example: cross-sectional minimum variance”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, DOGEchannels closewarmup 30dparam halflife = 7dparam 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
Section titled “Example: kernel model with fitted centres”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 BTCchannels closewarmup 7dparam 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 lastkeep 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
Section titled “Example: kernel model solved once in init”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 BTCchannels closewarmup 7dparam 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 lastkeep 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)
Section titled “Example: online kernel regression (RLS)”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 BTCchannels closewarmup 14dparam 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 lastkeep mkeep 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
Section titled “Mathematical functions”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,sqrtare 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 is0.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, SOLchannels closewarmup 1dkeep 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
Section titled “Deterministic random numbers”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, SOLchannels closewarmup 1dkeep 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
Section titled “Rank-1 Cholesky updates”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, DOGEchannels closewarmup 30dparam halflife = 7dparam 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, DOGEchannels closewarmup 30dparam halflife = 7dparam 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 BTCchannels closewarmup 7dparam 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 b2keep xPrev[3]keep lastkeep 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
Section titled “Templates”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
Section titled “Template: principal components”PCA of a rolling covariance with eigh; trades the residual after the leading factors.
markets BTC, ETH, SOL, BNB, XRP, DOGEchannels closewarmup 1dparam 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
Section titled “Template: slow feature analysis”The slowest linear feature: whiten with Cholesky, then the smallest eigenvector of the whitened derivative covariance.
markets BTC, ETH, SOL, BNB, XRP, DOGEchannels closewarmup 1dparam 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
Section titled “Template: canonical correlation”CCA between two blocks of markets by Cholesky whitening and eigh; the first canonical correlation.
markets BTC, ETH, SOL, BNB, XRP, DOGEchannels closewarmup 1dparam 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
Section titled “Template: kernel ridge regression”RBF kernel ridge on lagged returns: exact squared distances by rows, Cholesky solve, one prediction.
markets BTCchannels closewarmup 1dparam width = 0.0002 in (0.00001, 0.01)param ridge = 0.1 in (0.001, 10.0)keep fit[1]keep prevwindow 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
Section titled “Template: Gaussian process regression”GP posterior mean and variance and the log marginal likelihood, from one Cholesky factor.
markets BTCchannels closewarmup 1dparam 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 prevwindow 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
Section titled “Template: support vector machine”A linear SVM trained by dual coordinate descent for a fixed number of sweeps, predicting the sign of the next return.
markets BTCchannels closewarmup 1dparam cost = 1.0 in (0.01, 100.0)param sweeps = 10 in (2, 20)keep fit[5]keep prevwindow 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
Section titled “Template: k-means”Lloyd’s k-means on two features with one-hot assignment by broadcasting and fixed iterations.
markets BTCchannels closewarmup 1dparam rounds = 8 in (2, 20)keep fit[3, 2]keep prevwindow 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
Section titled “Template: k nearest neighbours”kNN regression: distances to stored patterns, argsort, the mean target of the k nearest.
markets BTCchannels closewarmup 1dparam k = 8 in (1, 32)keep fit[1]keep prevwindow 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
Section titled “Template: logistic regression”Logistic regression by Newton (IRLS) steps with a ridge, each a Cholesky solve.
markets BTCchannels closewarmup 1dparam ridge = 1.0 in (0.01, 100.0)param steps = 6 in (1, 12)keep fit[5]keep prevwindow 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.0Template: ridge and lasso
Section titled “Template: ridge and lasso”Ridge in closed form and lasso by cyclic coordinate descent with soft thresholding.
markets BTCchannels closewarmup 1dparam 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 prevwindow 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
Section titled “Template: softmax regression”Three-class softmax regression on the next return’s sign and size, by fixed gradient steps with logsumexp.
markets BTCchannels closewarmup 1dparam rate = 0.5 in (0.01, 2.0)param steps = 30 in (5, 100)keep fit[3]keep prevwindow 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
Section titled “Template: a small MLP, trained online”A one-hidden-layer network: forward pass, then one hand-written gradient step per bar on the previous bar’s input.
markets BTCchannels closewarmup 1dparam 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 prevwindow 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
Section titled “Template: Gaussian mixture by EM”A two-component 1-D Gaussian mixture of returns fitted by EM with logsumexp responsibilities.
markets BTCchannels closewarmup 1dparam rounds = 20 in (5, 50)keep fit[7]keep prevwindow 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
Section titled “Template: Kalman filter”A local linear trend model filtered online: predict, update, and the innovation log-likelihood.
markets BTCchannels closewarmup 1dparam 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
Section titled “Template: hidden Markov model”A two-state Gaussian HMM over returns: the normalised forward filter and Viterbi decoding of the last state.
markets BTCchannels closewarmup 1dparam 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 prevwindow 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
Section titled “Template: Bayesian linear regression”Conjugate Bayesian regression: posterior mean and covariance, the evidence, and the matching diagonal-Gaussian ELBO.
markets BTCchannels closewarmup 1dparam prior = 1.0 in (0.01, 100.0)param noise = 1.0 in (0.01, 100.0)keep fit[3]keep prevwindow 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
Section titled “Template: posterior predictive sampling”Monte Carlo with the deterministic generator: normal draws through a Cholesky factor, and a categorical draw by cumsum.
markets BTC, ETHchannels close, timewarmup 1dparam 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
Section titled “Unbounded iteration”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.
Why the arithmetic is ours
Section titled “Why the arithmetic is ours”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
Section titled “Versions”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
Section titled “Unterminated string”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
Section titled “Unknown character”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
Section titled “Malformed number”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
Section titled “Unexpected token”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
Section titled “Expected a token that is not there”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
Section titled “A construct this language does not have”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
Section titled “Market not declared”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
Section titled “Channel not declared”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
Section titled “Declared twice”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
Section titled “Manifest field missing”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
Section titled “Manifest value outside its limit”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
Section titled “Duration off the stride”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
Section titled “Unknown name”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
Section titled “Type mismatch”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
Section titled “Over the fuel budget”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
Section titled “Over the memory budget”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
Section titled “Shapes do not fit”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
Section titled “Index not proven in range”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
Section titled “Declared and never used”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.