# Arcana documentation # Arcana documentation > Learn the language, understand its host contract, and compile and replay programs over your own data. [Canonical documentation](https://arcn.ftw.trade/docs) Arcana is a language for programs that read time series, calculate signals and describe when to act. The same program and inputs produce the same evaluated values and decisions. The compiler checks types, reads and actions before evaluation; the runtime enforces work and memory limits. The language is an independent library. You can use its Rust compiler and evaluator or embed `@arcana/core`, the JavaScript and TypeScript package backed by WebAssembly. Neither requires an ftw.trade account. ## Start here - [Quickstart](https://arcn.ftw.trade/docs/quickstart): compile a complete signal program and replay it over a small, explicitly illustrated history. - [Run locally with your own sources](https://arcn.ftw.trade/docs/local): install the standalone CLI, supply typed observations from files or a producer pipe, and save and resume evaluation. - [Embedding Arcana](https://arcn.ftw.trade/docs/embedding): declare a host, feed its data, handle refusals and save or resume evaluation. - [Build libraries and tools](https://arcn.ftw.trade/docs/extending): compose reusable helpers, declare application actions and connect recorded model predictions. - [Performance and latency](https://arcn.ftw.trade/docs/performance): choose an execution path, measure retained steps and complete execution latency, and bound state, queues and model work. - [Outputs and consumers](https://arcn.ftw.trade/docs/outputs): turn evaluated metrics, text and action intents into charts, application logs and your own data. - [Language reference](https://arcn.ftw.trade/docs/reference): the types, syntax, series, built-in functions, triggers, diagnostics and host contract. ## What belongs to the language Arcana provides values and series, expressions, functions, explicit state through `fold`, indicators, metrics and trigger semantics. `na` represents an unknown observation or a value that cannot yet be calculated, including indicator warmup. There are no loops or mutable variables in a program. A program reaches only the reads and actions its application declares. It cannot open a file, fetch a URL, read a secret or obtain the system clock. The application supplies observations and evaluation timestamps. Compilation checks whether a program fits that declaration. It does not establish that an external data feed is complete, that a future input is valid, or that an order will settle. ## What the host supplies The **host** is the application's declaration of available sources, capability manifests, libraries and integrations. A source declaration gives a read its name, type, key shape and intervals. A capability manifest declares actions and their arguments. `bars.close` is a source name used in the examples. Your host must declare it and provide its samples. Names such as `paper`, `perp` and `aave` describe host capabilities; they are not connections to a broker or protocol built into the language. A program can publish metrics without declaring any actions. If actions are declared, evaluation can return decisions for an application to inspect. An execution adapter or an explicitly configured paper broker supplies the execution behavior. ## Using Arcana on ftw.trade ftw.trade is an application that embeds Arcana. It provides market data, its module catalog, strategy settings, backtests, accounts, wallets and execution services. Its injected names, saved-strategy workflow and trading permissions belong to that platform's host. Read [Strategies on ftw.trade](https://docs.ftw.trade/strategies/overview) for that integration. Use these language docs when learning Arcana itself or embedding it in your own application. --- # Your first Arcana program > Compile a complete program against an explicit host and replay an illustrated price sequence with @arcana/core. [Canonical documentation](https://arcn.ftw.trade/docs/quickstart) This example calculates two moving averages and a numeric trend signal. It declares one data source and no actions. The twelve input values are an illustrated price path for learning the API, not recorded market data or a backtest result. ## Get the library There is currently no public package release or public download of `@arcana/core`. It is not available from the npm registry. Running this example locally requires authorized access to the source repository and a library build. From `arcana/npm` in an authorized checkout or a copy of `arcana/`, build, test and pack the library: ```sh npm ci npm test npm pack ``` These commands write `arcana-core-0.1.0.tgz` in that directory at the current package version. Install that locally built tarball in your application. It includes compiled JavaScript, the local CLI and `arcana.wasm`; it does not publish a release. The [local guide](https://arcn.ftw.trade/docs/local) runs files or your own source producer without an application server. The [embedding guide](https://arcn.ftw.trade/docs/embedding) covers prerequisites, installation and the boundary your application owns. ## Compile and replay Save this complete example as `signal.mjs` in the project where you installed the package, then run `node signal.mjs`. ```js import { loadArcana } from '@arcana/core'; const arcana = await loadArcana(); const host = { sources: [ { name: 'bars.close', type: 'number', keyed: true, intervals: ['1h'], }, ], capabilities: [], }; const source = `arcana "0.1"; uses { bars: "^1" } const close = bars.close("DEMO-USD", "1h"); const fast = ema(close, 3); const slow = ema(close, 5); metric("close", close); metric("fast", fast); metric("slow", slow); metric("trend", fast > slow ? 1 : 0); `; const compiled = arcana.compile(source, host); if (!compiled.ok) { throw new Error(compiled.problems.map((p) => `${p.line}:${p.col} ${p.code} ${p.message}`).join('\n')); } try { const closes = new Float64Array([ 100, 100, 100, 100, 100, 102, 104, 106, 105, 103, 101, 99, ]); const firstClose = Date.UTC(2026, 0, 1, 1) / 1000; const history = { len: closes.length, series: { 'bars.close': closes }, time: Float64Array.from(closes, (_, i) => firstClose + i * 3600), }; const replay = arcana.replay(compiled.program, history); if (!replay.ok) throw new Error(replay.error); if (replay.unmodelled.length) { throw new Error(replay.unmodelled.join('; ')); } const last = history.len - 1; console.log({ close: replay.metrics.close[last], fast: replay.metrics.fast[last], slow: replay.metrics.slow[last], trend: replay.metrics.trend[last], decisions: replay.decisions.length, }); } finally { arcana.drop(compiled.program); } ``` ## Read the result - `uses` declares the `bars` module. The host explicitly permits `bars.close` with a key and the `1h` interval. - `"DEMO-USD"` and `"1h"` are written in the program. It needs no injected `market`, `interval` or account settings. - Each metric returns one value per input bar. The trend is `1` when the fast average is above the slow average, `0` otherwise, and unknown while its inputs are warming up. Numeric unknown values cross the JavaScript API as `NaN`. - `history.time` gives each completed bar's evaluation time in whole Unix seconds. The input columns are ordered oldest first and have exactly `len` values. - The source is read under one key, so the history can use its bare name, `bars.close`. A program reading several keys needs separate `source|key` columns. - There are zero decisions: this program publishes signals and declares no action capability. Replace the illustrated input with observations your application owns when evaluating real data. Preserve the source identity, units, ordering and timestamps; an interval declaration does not fetch or construct hourly bars for you. Next, read [Embedding Arcana](https://arcn.ftw.trade/docs/embedding) or the [language reference](https://arcn.ftw.trade/docs/reference). To use the hosted editor, market data and strategy workflow, see [Strategies on ftw.trade](https://docs.ftw.trade/strategies/overview). --- # Run Arcana locally > Install the standalone package, declare your own typed sources, evaluate JSONL files or producer pipes, and save and resume a local run. [Canonical documentation](https://arcn.ftw.trade/docs/local) Arcana runs independently of ftw.trade. Declare your own sources, supply observations and inspect the program's results on your machine. The local CLI, JavaScript package and native Rust library use the same compiler and evaluator; ftw.trade supplies one application host for them. | Use | Entry point | Your application supplies | |---|---|---| | Local files or a producer pipe | The packaged `arcana` CLI | A host JSON file and JSONL observations | | A JavaScript or TypeScript service | `@arcana/core` | A host object, retained run, observations and output consumers | | A native service | `arcana-core` | A `Host`, retained `live::Run` and application bindings | The examples below use an arbitrary `prices` namespace. There are no injected market names, FTW sources, accounts or credentials. ## Build and install the local package There is currently no public package download or npm registry release. With authorized access to the source, copy or check out `arcana/` alone. Building needs Node.js 22.18 or newer, npm, Rust and its `wasm32-unknown-unknown` target. No other repository directory, application server or root `Justfile` is needed. From `arcana/npm`: ```sh npm ci npm test npm pack ``` `npm test` builds the JavaScript and WebAssembly and tests the package. At the current version, `npm pack` creates `arcana-core-0.1.0.tgz`. The filename follows `package.json`; packing does not publish it or change the repository's license. In a separate project directory, install that tarball. Replace the source path with your own checkout or copy: ```sh npm init -y npm install /path/to/source/arcana/npm/arcana-core-0.1.0.tgz ./node_modules/.bin/arcana --help ``` The tarball contains compiled JavaScript, the CLI and `arcana.wasm`. Running the installed package needs Node.js; it does not need Rust, TypeScript, the source checkout or a connection to ftw.trade. ## Declare sources and a program Save these three files in your project. Their values illustrate the input contract; they are not a market dataset or strategy performance evidence. `host.json`: ```json { "sources": [ { "name": "prices.mid", "type": "number" }, { "name": "prices.status", "type": { "record": { "venue": "text", "asOf": "number" } } } ], "capabilities": [], "documentation": { "sources": { "prices.mid": "Observed midpoint in the producer's declared price units.", "prices.status": "Venue identity and original observation time in seconds." } } } ``` `strategy.arcn`: ```arcana arcana "0.1"; uses { prices: "^1" } const mid = prices.mid(); const status = prices.status(); const average = isNa(mid[2]) ? na : sma(mid, 3); metric("mid", mid); metric("average", average); metric("asOf", status.asOf); metric("venue", status.venue); ``` `observations.jsonl` (one complete JSON object per line): ```jsonl {"time":1700000000,"values":{"prices.mid":100,"prices.status":{"venue":"demo","asOf":1700000000}}} {"time":1700000001,"values":{"prices.mid":102,"prices.status":{"venue":"demo","asOf":1700000001}}} {"time":1700000002,"values":{"prices.mid":104,"prices.status":{"venue":"demo","asOf":1700000002}}} ``` Check and run: ```sh ./node_modules/.bin/arcana check strategy.arcn --host host.json ./node_modules/.bin/arcana run strategy.arcn --host host.json --input observations.jsonl ``` The first JSONL output record has `type: "program"`, the artifact hash, language version, declared reads, requirements, warnings and `actionMode: "unsent-intents"`. Each following `type: "step"` record includes the input line, time, bar, `metrics`, `texts` and `decisions`. The third step's `metrics.average` is `102`; the program explicitly leaves earlier averages unknown until three observations are present. Unknown numeric metrics encode as JSON `null`. Each step's `texts.venue` is `"demo"`. Errors go to stderr and exit with status 1; invalid command usage exits with status 2. Nothing substitutes fabricated observations or restarts implicitly. The host file is the same `Host` JSON accepted by `compile(source, host)`. Source declarations provide types, key shapes and intervals; they do not fetch data. There are no default sources or capability manifests. Declare each read and action the program uses. The [embedding guide](https://arcn.ftw.trade/docs/embedding) covers the API; [libraries and tools](https://arcn.ftw.trade/docs/extending) covers imports, typed external model outputs, custom actions and integrations. ## Connect your own producer Your producer can read a file, subscribe to a venue, collect sensor values or run an external model in its own process. It writes the same JSONL frames to stdout; application diagnostics belong on stderr. Pipe it into the evaluator: ```sh node producer.mjs | ./node_modules/.bin/arcana run strategy.arcn --host host.json --input - ``` `producer.mjs` is your adapter, not a built-in subscription service. The CLI does not connect to a data provider, poll a URL or infer symbol names. Choose the producer's authentication, ordering, reconnect policy and freshness checks explicitly. For an existing observation file, the equivalent pipe is: ```sh cat observations.jsonl | ./node_modules/.bin/arcana run strategy.arcn --host host.json --input - ``` For a live producer, supervise both processes or enable your shell's `pipefail` so a collection failure remains visible. EOF means the input stream ended; it does not certify provider health. A producer must report unavailable data explicitly or stop with an error, rather than quietly dropping failed reads. Each frame has exactly `time` and `values`. Time is a nonnegative safe integer in seconds and must not decrease; equal timestamps allow several observations within one second. Preserve finer source time and sequence in declared fields when you need them. `values` maps source names to their declared observations. An omitted source holds its previous observation; explicit `null` makes it unknown. A clock-only frame such as `{"time":1700000003,"values":{}}` still steps the evaluator, including any deadlines. Build bars or sampling policies in your adapter rather than assuming the CLI aggregates the feed. Blank lines and extra frame fields are refused. End each appended file row with a newline; appending inside an already consumed final row is an error on resume. The CLI bounds source text to 1,000,000 UTF-8 bytes, the host document to 4 MiB, an input line to 1 MiB and its checkpoint document to 32 MiB. ## Save and resume a file For a file input, save the retained evaluator and its consumed input cursor: ```sh ./node_modules/.bin/arcana run strategy.arcn --host host.json --input observations.jsonl --checkpoint state.json ``` Append further complete observations to that same file. Resume explicitly: ```sh ./node_modules/.bin/arcana run strategy.arcn --host host.json --input observations.jsonl --checkpoint state.json --resume ``` Resume verifies the program and host identity and the exact consumed file prefix before it evaluates the suffix. A changed prefix, incompatible program or host, corrupt or missing checkpoint, or conflicting checkpoint ownership is an error. Starting a new run over an existing checkpoint is refused. The CLI does not silently discard state and replay from the beginning. The runner holds a sibling `state.json.lock` while it owns the checkpoint and removes it on normal exit. A killed process can leave that lock behind. Confirm that no writer remains before removing a retained lock and resuming. Checkpoint replacement atomically saves the evaluator snapshot, line cursor and prefix hash together after each successful step and before its output. It is available only with file input, not stdin. Stdout and checkpoint storage do not form one transaction: a crash can leave emitted output and the saved cursor out of step. Consumers that require durable exactly-once effects need an application-owned transactional input/output journal and reconciliation; redirecting stdout is not an outbox. ## Add execution in your application A program can declare custom action capabilities and produce decisions. The CLI prints these as unsent intents; it does not place orders, sign, send transactions or report simulated fills. A manifest alone does not implement an executor. Use the JavaScript or Rust embedding API to authorize and consume decisions and report authoritative outcomes. Keep keys, limits, durable submission identity and settlement reconciliation in that host. The local runner refuses `async`/`await` actions and any behavior its runtime reports as unmodelled. Embed Arcana with a request and settlement coordinator when a venue completes an action later. The [outputs guide](https://arcn.ftw.trade/docs/outputs) demonstrates application consumers. The [performance guide](https://arcn.ftw.trade/docs/performance) explains retained execution, queue and allocation budgets and why evaluator timing alone is not order latency. --- # Moving to Arcana 0.1 > Update source files and developer tools while preserving historical programs, signatures and checkpoints. [Canonical documentation](https://arcn.ftw.trade/docs/migration) Arcana is the language formerly named Flowscript. Arcana 0.1 includes the complete arrays, models, checked integers, standard library and runtime behavior present before the rename. Package releases start at 0.1.0. Use `@arcana/core`, `loadArcana`, the `arcana` command, `.arcn` programs, `.t.arcn` tests and the Arcana editor extension. New source declares `arcana "0.1";`. Documentation lives at [arcn.ftw.trade/docs](https://arcn.ftw.trade/docs). The old learning domain is removed, without redirects. The package still ships as a private GitHub release tarball. No npm registry publication or license change is implied. Install the new Arcana VSIX and remove the old editor extension; move `flowscript.*` settings to `arcana.*` and update workspace language associations. The new extension's identity is `ftw-trade.arcana`. ## Migrate a source file explicitly For a program whose recorded language is Flowscript 0.3: ```sh arcana migrate strategy.flow --host host.json --output strategy.arcn arcana check strategy.arcn --host host.json ``` The migration checks the historical program and its Arcana replacement, requires identical executable and authority fields, writes a new file without overwriting any existing file, and reports both artifact hashes. For an unversioned source, the host file must record its original language version. Earlier editions require source review rather than an assumed semantic upgrade. Pinned libraries and host declarations remain application-owned. ## Preserve running programs Existing signed artifacts and checkpoints keep their original bytes and hashes. Arcana explicitly reads supported historical formats under their recorded semantics. A new format never retries as an old one after failure. Saved sources and deployments carry their language family because the two families' version numbers overlap. The local runner recognizes the exact historical `flowscript-local-v1` checkpoint envelope before compilation, verifies its original source, host, artifact and consumed input prefix, and resumes it with the historical compiler identity. It keeps that envelope's identity when saving progress. New local runs use `arcana-local-v1`. Never replace a checkpoint to make a failed restore start again. A newly migrated program has a new artifact identity. Deploy it as a new version with the ordinary authorization; it does not inherit an old signature or reuse the old program's state by changing its hash. --- # Values, series and na > Values and series, missing observations, expressions, functions, folds, enums and phases. [Canonical documentation](https://arcn.ftw.trade/docs/language) Arcana calculates over the inputs its [host declares](https://arcn.ftw.trade/docs/reference#the-host). A host supplies data, available actions and any names bound to a run. ftw.trade is one such host; these language rules also apply when you embed the compiler yourself. ## Values | Kind | Example | |---|---| | Number | `1`, `0.5`, `1e-3` | | Condition | `true`, `false` or `na` | | Text | `"ETH"` | | Record | `{ usd: 100, side: "long" }` | | List | `[1, 2, 3]` | ## Everything is a series Every `const` is a series: one value per bar. `close` is today's close; `close[1]` is the bar before; `close[5]` five bars back. Arithmetic works bar by bar, so `(high + low) / 2` is a series of midpoints. Durations are seconds: `seconds(30)`, `minutes(15)`, `hours(6)`. ## na: not known yet `na` means a value isn't known yet: an indicator still warming up, a division by zero, a bar with no data. - It spreads: `na + 1` is `na`, `na > 3` is `na`, and `!na` is `na`. - A trigger never fires on `na`. - Test for it with `isNa(x)`; replace it with `nz(x, 0)`. So write the condition you mean: `close > ema(close, 200)` is `na` (not false) for the first 200 bars. ## Expressions - Arithmetic `+ - * / %`, comparisons `< <= > >= == !=` (they don't chain: write `1 < x && x < 3`), logic `&& || !`. - `c ? a : b`, and `if (c) a else b`. - Math: `abs`, `min`, `max`, `clamp`, `ln`, `exp`, `sqrt`, `pow`, `floor`, `ceil`, `round`, `sign`. - Time: `hour`, `minute`, `dayofweek` read a timestamp in UTC, and `nyTime`, `londonTime`, `tokyoTime` and others give a city's clock with daylight saving. `inWeekdaySession(nyTime(t), 930, 1600)` identifies the US stock session. The host declares its timestamp source and bar intervals. ## Functions and folds ```text fn mid(a: Series, b: Series): Series { return (a + b) / 2; } const m = mid(high, low); const doubled = map(xs, (x) => x * 2); ``` There are no loops and no variables to change. State that carries from bar to bar is a `fold`: a seed and a step from the last state and the new value. ```text const streak = fold(close, 0, (st, c) => isNa(c) ? st : (c > c[1] ? st + 1 : 0)); ``` Guard every fold step with `isNa(x) ? st : …`: one missing bar would otherwise turn the state `na` forever. ## Enums and match ```arcana uses { bars: "^1" } enum Regime { hot, quiet, normal } const close = bars.close("BTC-USD", "1h"); const vol = stddev(change(ln(close)), 24); const regime = vol > 0.02 ? Regime.hot : (vol < 0.005 ? Regime.quiet : Regime.normal); metric("size", match (regime) { hot => 0, quiet => 1, _ => 0.5 }); ``` A `match` must cover every variant or end in `_`. A swap's `side` and a source's cases are enums too. ## Phases `phases Trade { phase Flat { … } phase Long { … } }` is a state machine: inside each phase, `when … => Phase` moves to another, at most once a bar. It's the clearest way to write "wait for a setup, then enter, then manage the position". ## Higher timeframes On a 15-minute run, `bars.close(market, "4h")` is the last closed four-hour bar's close, held until the next one closes. It never repaints. `higher("4h", close, c => ema(c, 20))` runs a calculation on the four-hour bars themselves. ## Libraries Keep functions you reuse in a library and `import "mylib";` after the `uses` line. Your host provides the library resolver. On ftw.trade, [the module and library guide](https://docs.ftw.trade/arcana/modules) explains how to save and version them. ## Exact integers Numbers are 64-bit floats. For token amounts that need every digit, `u256` functions carry exact 256-bit integers, with checked arithmetic and unit conversion. See [exact integers](https://arcn.ftw.trade/docs/reference#exact-integers) for the versioned conversion rules. An action with an invalid argument is refused by the runtime; the host owns execution and signing. --- # Indicators and models > Moving averages, oscillators, windows, sessions, market profile and models over the inputs your host supplies. [Canonical documentation](https://arcn.ftw.trade/docs/indicators) Indicators calculate over series your host supplies. The language does not connect to a venue or choose a data provider; the host declares and records those inputs. ## Moving averages `sma`, `ema`, `wma`, `rma`, `hma`, `dema`, `tema` take `(src, len)`; `vwma(src, vol, len)` and `vwap(price, vol, len)` weigh by volume. ## Momentum and oscillators | Function | Gives | |---|---| | `rsi(src, len)` | 0 to 100 | | `stoch(close, high, low, len)` | 0 to 100 | | `cci(src, len)`, `mfi(src, vol, len)`, `obv(src, vol)` | The classic readings | | `roc(src, len)`, `momentum(src, len)` | Rate of change (percent) and momentum | | `macd(src, fast, slow)`, `macdSignal(…, sig)`, `macdHist(…, sig)` | One function per line | ## Volatility and bands `bbUpper(src, len, mult)` and `bbLower(…)` (the middle is `sma(src, len)`), `stddev`, `zscore`, `wilderAtr(high, low, close, len)` (the ATR charts draw) and `trueRange`. ## Trend `adx(high, low, close, diLen, adxLen)`, `diPlus`, `diMinus`, `supertrend(high, low, close, factor, len)` with `supertrendDir`, and `sar(high, low, close, start, inc, max)`. ## Windows and levels `highest`, `lowest`, `rolling_sum`, `cum`, `linreg`, `corr(a, b, len)`, `median`, `percentile`, `barssince(cond)`, `valuewhen(cond, src)`, and `pivothigh` and `pivotlow` (known a few bars late, by design). ## Sessions and VWAP - `periodVwap(price, volume, t, 86400)` is the day's VWAP from 00:00 UTC; `anchoredVwap` starts from any bar; both have σ bands. - `inSession`, `inWeekdaySession`, `sessionOpen`, `sessionHigh` and `sessionLow` frame a trading session in any city's time. - `import "std/vwap";` adds what a VWAP trader reads: where price sits in σ, the week's and month's anchors, whether the day is trending, and a failed push into a band. ## Market profile `tpo.poc`, `tpo.vah`, `tpo.val`, `tpo.vpoc`, `tpo.poorHigh`, `tpo.singleAbove`, `tpo.nakedPocAbove` and more read the last whole day's (or week's, or the forming session's) time-price profile, the same in a backtest and live. ## Trained models Models train on the market as the strategy runs, with no look-ahead, and refit on a schedule. The same model is fitted in a backtest and live. | Model | Answers | |---|---| | `ml.linear`, `ml.ridge`, `ml.lasso`, `ml.elasticNet`, `ml.svr` | A forecast of `y` | | `ml.boost` | Gradient-boosted trees forecasting `y` | | `ml.logistic`, `ml.boostClassifier` | The chance `y` is above zero | | `ml.svm` | A signed margin | | `ml.knn` | The mean `y` of the nearest rows | Each is written `ml.(y, [x1, x2, …], window: n, horizon: h, retrain: r)`: it learns `y` `h` bars after the predictors over the last `n` rows, refits every `r` rows, and answers `na` until its first fit. ```arcana uses { bars: "^1" } const close = bars.close("BTC-USD", "1h"); const ret = close[0] / close[1] - 1; const fwd = close[0] / close[3] - 1; const f = ml.ridge(fwd, [ret, close / ema(close, 20) - 1, rsi(close, 14)], window: 300, horizon: 3, retrain: 20); metric("forecast", f); ``` This publishes a forecast without sending an action. Your host can expose actions that use the result. The [ftw.trade strategy starters](https://docs.ftw.trade/strategies/starters) include a trading example with five features. ## Liquidations Liquidation feeds are host-provided inputs. ftw.trade exposes `liq.*` for supported perp markets; its [source documentation](https://docs.ftw.trade/sources/arcana-sources) describes coverage. These feeds are part of that platform's binding rather than built into the language. --- # Language reference > The compiler-owned reference for syntax, types, hosts, actions, functions, diagnostics and runtime semantics. [Canonical documentation](https://arcn.ftw.trade/docs/reference) Arcana is a small language for trading strategies and indicators, designed for AIs to write. A program reads time series (prices, volumes, event tapes), computes values over them, and fires actions when its conditions turn true. It is deterministic: the same inputs always produce the same values and the same actions, which is why a backtest over history and a live run over new bars make the same decisions. What makes it suit a model writing strategies is that the language is small and bounded. A program has no loops and no mutation, so it always terminates. It reaches only what its host declares. It is checked before it runs, and compiler errors are located, carry stable codes, and suggest fixes. Because each run replays exactly, a model can backtest a change and trust the comparison. Compilation does not promise that external data or execution will always succeed. A run refuses invalid inputs and damaged checkpoints, and stops when it exhausts a resource budget. Hosts must surface that refusal and retain the last successful checkpoint; they must not treat it as a successful bar or replace it with fabricated values. Missing observations and indicator warmup use the language's explicit `na` value. This reference describes the language as `arcana-core` and `@arcana/core` implement it, without ftw. Every program in a `arcana` block below is compiled by the library's tests (`src/docs_reference.rs`) against the host in [The examples' host](#the-examples-host) and replayed over a synthetic history, so the examples cannot drift from the compiler. The semantics are specified in `ARCANA_SPEC.md` at the repository root; this is the user's view of them. - [A first program](#a-first-program) - [Language versions](#language-versions) - [The host](#the-host) - [Values and types](#values-and-types) - [Expressions](#expressions) - [Functions, lambdas and fold](#functions-lambdas-and-fold) - [Metrics](#metrics) - [Triggers](#triggers) - [Actions and handlers](#actions-and-handlers) - [Enums and match](#enums-and-match) - [Phase machines](#phase-machines) - [Tapes](#tapes) - [The prelude](#the-prelude) - [Built-in functions](#built-in-functions) - [Diagnostics](#diagnostics) - [What runs where](#what-runs-where) - [Coming from Pine Script](#coming-from-pine-script) ## A first program ```arcana uses { bars: "^1", paper: "^1" } const close = bars.close("BTC-USD", "1h"); const fast = ema(close, 5); const slow = ema(close, 20); metric("fast", fast); metric("slow", slow); when (crossover(fast, slow)) { paper.buy({ symbol: "BTC-USD", quote: 100 }); } when (crossunder(fast, slow)) { paper.sell({ symbol: "BTC-USD" }); } ``` - `uses { … }` names the modules the program calls, with a version range. A module is either a source namespace (`bars`) or a capability (`paper`). - `const` binds a name to a value. Every binding is a series: `close` is the close of every hourly bar, and `ema(close, 5)` is a new series computed from it, one value per bar. - `metric(name, value)` publishes a value: a replay returns it for every bar, and a live run returns it at each step. - `when (condition) { … }` fires its actions when the condition turns true. `crossover(fast, slow)` is true at the bar `fast` crosses above `slow`. Comments are `// …` to the end of the line, or `/* … */`. ## Language versions Arcana is versioned as `MAJOR.MINOR`, and starts at 0.1. It includes the complete language and runtime available before the rename, including arrays, models and exact integer math. Package versions start at 0.1.0. A program says which version it is written in on its first line, before `uses` (comments may come above it): ```arcana arcana "0.1"; uses { bars: "^1", paper: "^1" } const close = bars.close("BTC-USD", "1h"); when (crossover(ema(close, 5), ema(close, 20))) { paper.buy({ symbol: "BTC-USD", quote: 100 }); } ``` The version is a requirement string, written as a module's is in `uses`: `"0.1"` and `"^0.1"` mean the same, Cargo's caret. Before 1.0 each minor version may change what a program means, so a 0.x requirement names its minor and compiles only as exactly that version; `"0"` alone is refused. From 1.0 on, `"^1.2"` compiles as the newest 1.x the compiler speaks that is at least 1.2. - A version the compiler does not speak is refused where it is written (`language.version`), with the versions it does speak, never compiled under another version's rules. So is a declaration anywhere but the first line, or given twice. - A program that names none compiles as the compiler's newest version. Applications that keep programs record the version each was saved as and compile it as that version from then on (`Options::language`, or `language` in an `@arcana/core` host); the program's own line selects its version within the declared language family. - New Arcana artifacts use `language: "arcana/1"` and always pin the version they compiled as (`language_version: "0.1"`), including programs using the default. A runtime refuses an artifact of a family or version it does not speak. `Program.language` in `@arcana/core` gives the version a program compiled as. - Historical Flowscript artifacts retain `language: "flowscript/1"` and their original 0.1, 0.2 or 0.3 semantics. A trusted host selects `ArtifactIdentity::LegacyFlowscript` (`artifactIdentity: 'legacy-flowscript'` in JavaScript) before compiling their original source. Version numbers are interpreted within their family; historical Flowscript 0.1 and Arcana 0.1 are distinct identities. - A trusted host restoring a known historical unversioned artifact may request `ArtifactIdentity::LegacyUnversioned` (`artifactIdentity: 'legacy-unversioned'` in a JavaScript host) with explicitly selected language `0.1`. Without an own source header, this reproduces the omitted marker and original digest while reporting effective language `0.1`. An own source header wins with its normal pinned identity. Checkpoint hash checks remain strict; new programs leave this restoration option unset. - A library may name its version on its first line too, and is held to it. | Family | Supported versions | Behavior | |---|---|---| | Arcana | 0.1 | The full current language: [arrays](#arrays), models, [exact integer math](#exact-integers), checked arithmetic and safe numeric inputs. Built-in names such as `filter` are reserved. | | Historical Flowscript | 0.1, 0.2, 0.3 | Explicit historical readers preserve each saved edition's function names, numeric input policy, artifact bytes and checkpoint identity. | The [migration guide](migration.md) covers an explicit source migration and continued execution of historical checkpoints. ## The host The language has no built-in data and no built-in actions. The application compiling a program declares what it may read and do. That declaration is the **host**. - **Sources** are the reads. Each has a `namespace.name`, a type (number, bool, text, a tape of events with typed fields, or a record of typed fields read one bar at a time, such as a position's `size` and `pnl`), whether it takes a literal key as its first argument (a symbol, an address), and which bar intervals it is published at. `bars.close("BTC-USD", "1h")` is a keyed number source read at `1h`. A key can instead be a typed **identity**: a source declared with `identity: 'pool'` (`'token'`, `'market'`, `'venue'`, `'wallet'`) takes `pool("base:0x…")` (`wallet("main")`, keyed by the wallet's name) and refuses a bare string or another kind of identity (`market.identity`), so a pool's bars cannot be read with a token's address. Optional `args` follow the key and the interval, each typed (`duration`, `number`, `quantity`, `text`): a TWAP's window, `twap(pool("…"), "5m")`. Reading a source the host does not declare is the `stream.unknown` error, with the nearest name it does declare; an interval it does not publish is refused too. - **Capabilities** are the actions, declared as YAML manifests: the module's name and version, and each action with its typed arguments. A call that does not match a manifest (an unknown action, an argument it does not take, or one of the wrong kind, such as a number where it takes `Text`) is refused at compile time. An action's `required:` lists the arguments it cannot run without (`pair|token` for either of two), after its `defaults:`, and its `positive:` the amounts that must be above 0 where the source writes them: `paper.buy({ symbol: "BTC-USD" })` is `action.required` and `quote: 0` is `action.positive`, each at the call. Its `evaluated:` lists the arguments the host reads as values when the action fires (each decision's `values`), not as the source that computes them: an application's own action, such as a source's `emit`, gets what they were at that bar. A manifest's `streams` are the module's reads: the first argument's type is the key's identity (`Market`, `Token`, `Pool`, `Wallet`, or `Venue` for either a pool or a market), the rest are optional arguments (`Duration`, `Bps`, `Number`, `Text`), and `out` is `Series`, `Series`, `Series` or a record, `"Series<{ size: number }>"`. - **Catalog** rows resolve `series("tenant:me/name@1")` in Rust hosts through `Host::with_catalog(json)` or `extend_catalog(json)`. Each reference is unique and declares `type: "number"`, `"bool"` or `"string"`; an optional `keyed` is `"pool"`, `"token"` or `"market"`. String enum `variants` are distinct, nonempty names, using the same word rules as tape/record enums. References contain no whitespace/control characters and use a positive decimal major within `u32`. Tenant names occupy one path segment; platform names may use nested nonempty segments, such as `platform/metrics/volume-5m@1`. Invalid rows are refused when the host is built, even if no program reads them. A misspelled type never becomes a numeric series. - **Markets** are the names `market("…")` may take, when the application has any. - **Units** are the numeric reads' scales, `Host::with_stream_units(json)`, in the format of ftw's `platform/stream-units.json`. A row's `identity` (`pool`, `token`, `market`, `venue`, `wallet`) is what its read is keyed by, and makes the row a read the host declares: a number with the row's bar `intervals`, a tape when the row lists its `events` (`{ "px": "number" }`), otherwise a number taking the row's `args` (`[{ "name": "window", "type": "duration" }]`). A record row is not a call, and a read the host already declares keeps its declaration. A row naming any other identity is refused when the host is built. A bar's `forms_from` names the live mark its forming bar is decided on (`when (…, intrabar: true)`), and `unformed: true` marks a bar price the forming bar does not deliver, which an intrabar condition may not read. A tape's row may name a candle read, `"candles": { "read": "dex.candles", "price": "price", "volume": "usd" }`: bars of any period built from the tape (`Host::with_candles` declares one directly). In TypeScript the host is a plain object (see `npm/README.md`): ```ts const host = { sources: [{ name: 'bars.close', type: 'number', keyed: true, intervals: ['1h'] }], capabilities: [paperManifestYaml], }; ``` In Rust it is `arcana_core::Host`, built with `Host::empty()`, `.with_source(Source::number("bars.close").keyed().intervals(["1h"]))` and `.with_capability(yaml)`. ### The examples' host Every example in this reference compiles against this host: | Source | Type | Keyed | Intervals | |---|---|---|---| | `bars.open`, `bars.high`, `bars.low`, `bars.close`, `bars.volume`, `bars.time` (each bar's start, unix seconds) | number | yes | `1h`, `1d` | | `news.sentiment` | text | no | | | `trades.prints` | tape: `price` number, `size` number, `side` text | yes | | Two capabilities: `paper` with `buy({ symbol: Text, quote: Number })` and `sell({ symbol: Text })`, and `alerts` with `send(message: Text)`. ## Values and types | Type | Written | Notes | |---|---|---| | number | `1`, `0.5`, `1_000_000` | 64-bit float | | bool | `true`, `false` | | | text | `"BTC-USD"` | `+` concatenates text: `"filled @ " + fill.price`; a number joins with two decimals, more below 1 (`"p" + 1` is `p1.00`) | | `na` | `na` | "not available": the one option type | | record | `{ sum: 0, n: 0 }` | field access is typed: `r.sum`; an unknown field is an error | | list | `[a, b, c]`, `[[a, b], [c, d]]` | a fixed list of numbers or series, a vector; a list of rows is a matrix. `xs[0]` is an item; see [Lists, statistics and linear algebra](#lists-statistics-and-linear-algebra) | | array | `buffer(e, 100)`, `filter(xs, x => x.usd > 1000)` | items a run gathers, as many as its data makes (at most 10,000): numbers, text, conditions, records or arrays; see [Arrays](#arrays) | | series | any of the above, over time | what a source returns, and anything computed from one | | duration | `seconds(30)`, `minutes(5)`, `hours(4)` | | | basis points | `bps(25)` | 25 basis points, as an action's argument declared in them takes it (`max_slippage: bps(25)`); in arithmetic it is the number 25, so a rate is written `25 / 10000` | Quantity constructors such as `usdc(25)`, `eth(1)`, `bps(25)` and `pct(5)` take exactly one argument. Extra arguments are compile errors. **`na`** is how the language says a value is not known yet: an `ema(close, 20)` is `na` for its first 19 bars rather than a made-up number. It propagates through arithmetic and comparisons, and a condition that is `na` is not true. A condition is true, false or `na`: `close > open` is `na` where either side is, `!` of `na` is `na`, and `&&` and `||` answer `na` only when the known side cannot decide (`false && na` is false, `true || na` is true, `true && na` is `na`). `c ? a : b` and `if` over an `na` condition are `na`, and a `when` does not fire on one. A chart and a running strategy read conditions the same way. `isNa(x)` tests for it and `nz(x, 0)` replaces it. A condition or a text names what replaces it, `nz(cond, false)`, since 0 is neither. A condition is not a number: functions that take numbers (arithmetic, `crossover`, the windows such as `median`) refuse one, and `cond ? 1 : 0` is its number. A duration such as `"1h"` goes only where a clock takes one (`cooldown`, `resample`); `hours(1)` is that many seconds as a number (D1290). **Scalars lift over series.** `close * 2`, `close > open` and `abs(change(close))` apply bar by bar. ## Expressions - Arithmetic `+ - * / %` on numbers, comparison `< <= > >= == !=`, logic `&& || !` on conditions. A condition is not a number: `(a > b) * 2` is an error, and `a > b ? 1 : 0` is the number. - Comparisons do not chain. `1 < x < 3` is an error; write `1 < x && x < 3`. - A comparison with `na` is `na` at every bar, so `x == na` is an error; test with `isNa(x)`. - Conditional: `cond ? a : b`, or `if (cond) a else b` as an expression. Both arms must have the same type. - History: `close[1]` is the previous bar's close, `close[k]` the value `k` bars ago (`na` before there are `k` bars). - Blocks: `{ let x = …; return …; }` where a function or lambda body is expected. A `let` or a lambda's parameter cannot shadow a binding or a `const`, and a `fn` cannot take a built-in function's name. ```arcana uses { bars: "^1" } const close = bars.close("BTC-USD", "1h"); const open = bars.open("BTC-USD", "1h"); const body = abs(close - open); const bias = if (close > open) "up" else (close < open ? "down" : "flat"); metric("body", body); metric("bias", bias); metric("gap", (open - close[1]) / close[1]); ``` A `const` is bound once: a second `const` of the same name is an error, not a replacement. There are no loops, no recursion and no mutation. That is deliberate: every program terminates at every bar with bounded memory, which is what makes it replayable. ## Lexical structure and grammar **Text.** A program is UTF-8. Outside strings and comments it is ASCII: a non-ASCII character there is an error naming it, except Unicode spaces, which separate tokens as ASCII whitespace does. A leading byte-order mark is ignored. Comments are `// …` to the end of the line and `/* … */`, which does not nest. **Tokens.** | Token | Form | |---|---| | name | an ASCII letter or `_`, then letters, digits and `_` | | number | decimal digits with an optional fraction and exponent: `42`, `0.5`, `1_000`, `1.5e3`, `2E-1`. No sign (that is unary `-`), no leading `.`, no hex. A number too large for a 64-bit float, or so small it would read as `0`, is an error, as is a number run into a name (`2px`) | | string | `"…"`, with the escapes `\"`, `\\`, `\n` and `\t`; any other `\` is an error | | `true`, `false` | the two conditions | | operators | `+ - * / % < <= > >= == != && \|\| ! ? : => = . , ; ( ) [ ] { } @` | **Precedence**, loosest first. Binary operators on one level associate left, except comparisons, which do not associate at all. | Level | Operators | |---|---| | 1 | `c ? a : b`, `if (c) a else b` and `match (x) { … }` (right-associative: `a ? b : c ? d : e`) | | 2 | `\|\|` | | 3 | `&&` | | 4 | `< <= > >= == !=`, one per expression | | 5 | `+ -` | | 6 | `* / %` | | 7 | unary `-` and `!` | | 8 | history `x[k]`, field `r.f`, call `f(…)`, parentheses | So `-x[1]` is `-(x[1])`, `a || b && c` is `a || (b && c)`, and `x - 1 - 2` is `(x - 1) - 2`. **Limits.** An expression nests at most 128 levels as written: each parenthesis, operand, call and record is one, and so is each operator in a chain (`a + b + c` is two). Once its `const`s are substituted and its library calls inlined, one binding may lower to 160 levels and 1 MB. A library call's large argument that its body reads more than once is bound once rather than copied to every read, so stacked indicators (`sma(stoch(r, r, r, 14), 3)` over an `rsi`, `hma(hma(hma(close, 9), 9), 9)`) grow with their nesting, not exponentially. A large `const` of any type is bound once in the binding that reads it, and a small one is copied where it is read. The largest program in the repository uses about a sixth of the depth and a fortieth of the size. A program past either limit is refused with `syntax.depth` or `expr.size` instead of exhausting the memory of whatever compiles it. A program's source is at most 1 MB, and a metric's name at most 128 characters. The whole program lowers to at most 4 MB, summed over every const, metric, condition and argument, keeps at most 4,000,000 samples of state, and has at most 1,000 metrics, 2,000 triggers (each handler and each `if` in a body counts as one) and 2,000 actions. Past any of these the program is refused with `program.size`, as soon as the compiler passes it; the refusal names the const, metric or trigger that passed the size and the three that lowered to the most before it. A `const` that lowers to 2,000 bytes or more, whatever its type, is bound once in each binding that reads it, however many times that binding reads it, so a step that reads a scored list of pools through two or three consts holds the list once; a smaller `const` is copied at every read, as it always was. A `const` that reads another is still held once per binding that reads either, so a program over many pools keeps the bindings that read its scored list few. A run is bounded too, in evaluation steps (a node walked, or an element a window scans), which count the same on every replay of the same bars: a bar may take 50,000,000 and a replay 4,000,000,000, and a replay returns at most 1,000,000 decisions and orders. Past one the replay stops with an error that names the bar. What a program keeps is bounded where it is written: one expression keeps at most 1,000,000 samples of state (a window's length, a history depth `[n]`, a warm-up), and a `window` over a tape spans at most `"7d"`, since it keeps every event in its span. Both are refused with `expr.size`. A text value holds at most 4,096 bytes; joining past that is `na`, so a `fold` that grows its text cannot grow without end. A NaN or infinity a source feeds is `na`, and so is an answer with no finite reading, such as `log10(0)` or `asin(2)`. ## Functions, lambdas and fold `fn` defines a function. Parameters and the result are typed: `number`, `bool`, `text` or `count`, or a `Series<…>` of the first three. Annotations check the element kind: a scalar broadcasts as a constant series, so `ewma(3, 5)` is valid. `count` names a whole number from 1. A call whose argument, or a body whose result, is a different kind (a number, a condition, text) from what the annotation names is refused, and so is a name that is not one of these (`Series`). A parameter with no annotation takes anything. A function has one definition, and its parameters have distinct names. An annotation names exactly one type. After `:`, a type is required: `x:`, bare `Series`, `Series<>`, `number bool` and `number + text` are errors, rather than an untyped parameter or an alternative type. `Series` contains one of `number`, `bool` or `text`; `count` is scalar. The existing spellings `boolean` and `string` are aliases for `bool` and `text`. ```arcana uses { bars: "^1" } fn spread(a: Series, b: Series): Series { return (a - b) / b; } const close = bars.close("BTC-USD", "1h"); metric("stretch", spread(close, sma(close, 50))); ``` `fold(series, seed, (acc, x) => next)` is the one stateful loop: it steps once per bar, from `seed`, with the last result and the new sample. The seed may be a record, and the accumulator is typed by it. Every indicator in the prelude is written with it. ```arcana uses { bars: "^1" } const close = bars.close("BTC-USD", "1h"); // Bars since the close last made a new high. A bar with no close leaves // the state as it was: without the isNa test one missing sample would make // the state na for the rest of the run. const since = fold(close, { high: 0, n: 0 }, (st, x) => isNa(x) ? st : (x > st.high ? { high: x, n: 0 } : { high: st.high, n: st.n + 1 })); metric("bars_since_high", since.n); ``` A lambda is `(a, b) => expr` or `(a, b) => { let …; return …; }`. It captures the names around it by value. `@warmup(n)` after a `const`'s expression, or before a `fn`, states a lookback the compiler cannot derive. It can only ask for more history, never less. ### Libraries `import "name";`, after `uses`, gives a program the functions of a library its host offers (D1214). A library holds `fn` definitions, contract interfaces and imports of other libraries: no `uses`, metrics or triggers. Its caller owns the sources, actions and contract instances it may use. The compiler itself offers the `std/` libraries, such as `std/erc20` (see Contract calls) and `std/vwap` (below). Its functions are checked like the program's own, where the program calls them, and a problem inside one is named as an imported function's. ```text // The library "swing", as the host offers it. fn mid(a: Series, b: Series): Series { return (a + b) / 2; } ``` ```text uses { bars: "^1" } import "swing"; const high = bars.high("BTC-USD", "1h"); const low = bars.low("BTC-USD", "1h"); metric("mid", mid(high, low)); ``` `import "std/vwap";` is the VWAP trader's library, built on the prelude's `periodVwap`, `anchoredVwap` and their σ: `weekStarts(t)` and `monthStarts(t)` (anchors: the first bar of each UTC week from Monday 00:00, and of each month), `vwapZ(price, vw, dev)` (where price sits in σ), `vwapDrift(vw, dev, anchor)` (how many σ a bar the VWAP has walked since its anchor: near 0 on a balanced day), `ridingAbove(close, vw, dev, len)` and `ridingBelow(…)` (the share of the last `len` closes beyond the 1σ band: a trend, not a fade), and `failedPushUp(high, open, close, upper, within)` and `failedPushDown(low, open, close, lower, within)` (a push into a band that turned back within `within` bars). Its source and tests are `arcana/std/vwap.arcn` and `vwap.test.yaml`. ```arcana uses { bars: "^1" } import "std/vwap"; const close = bars.close("BTC-USD", "1h"); const high = bars.high("BTC-USD", "1h"); const vol = bars.volume("BTC-USD", "1h"); // Anchored at each new 20-bar high: the average price of everyone who bought since. const top = high >= highest(high, 20); const vw = anchoredVwap(close, vol, top); const dev = anchoredVwapDev(close, vol, top); metric("z", vwapZ(close, vw, dev)); metric("riding", ridingAbove(close, vw, dev, 12)); ``` A host names its libraries when it compiles (`Options.libraries` in Rust, `Host.libraries` in `@arcana/core`), so the same source compiles to the same program wherever it is given the same libraries. A library can compose another helper with `import "features/base";` before its functions; a strategy then imports only the entry library. Dependencies resolve by those exact host names, including `std/` helpers. A host-supplied standard-library source takes precedence over the compiler's installed copy in compilation and editor tools. The compiler performs no filesystem, package-registry or network discovery. The complete dependency closure may contain at most 15 unique libraries, each subject to the program source-size limit. A shared dependency reached through several libraries loads once; explicitly importing that same dependency beside an entry library also loads it once. Listing an import twice in the same file still refuses. Cycles, unavailable dependencies, malformed libraries and incompatible library language headers refuse before evaluation, at the strategy's importing line with the dependency path in the diagnostic. Library functions share the caller's function namespace: use distinct names when composing packages. Conflicts between two libraries, the prelude or the program's own functions refuse. Hosts can obtain the exact reached source map, including installed standard libraries, with `resolve_libraries(source, options)` in Rust or `arcana.resolveLibraries(source, host)` in `@arcana/core`. This validates source syntax and dependency admission without requiring the strategy's data host or typechecking its function bodies; compile and test it separately. Persist the returned sources together with the program, then supply that frozen map to compilation, testing and editors. Changes to a dependency's used code change the compiled artifact identity and cannot reuse another version's live checkpoint. On ftw.trade a library is the account's, saved with `save_library`, and a strategy keeps its complete saved dependency closure until it is saved again. ## Metrics `metric(name, value)` publishes one value per bar, and each metric has its own name. A numeric or boolean metric comes back as numbers (`true` is 1). A metric that is `na` (still warming up) reads `NaN`, and a text metric comes back as words (`""` while `na`). A constant is a metric too, a number or a word the same at every bar. ## Triggers A trigger is a condition and the actions to fire when it holds. - **`when (cond) { … }` fires on the rising edge**: the bar the condition turns from false to true, once per crossing. - **`while (cond) { … }` fires at every bar the condition holds** and that brought something new the condition reads. ```arcana uses { bars: "^1", alerts: "^1" } const close = bars.close("BTC-USD", "1h"); const volume = bars.volume("BTC-USD", "1h"); when (close > rolling_high(close, 24)[1]) { alerts.send("new 24h high"); } while (volume > 3 * sma(volume, 48)) { alerts.send("volume still heavy"); } ``` Combinators, all usable inside any condition: | | | |---|---| | `crossover(a, b)`, `crossunder(a, b)` | `a` crosses above, below `b` | | `rising(cond)`, `falling(cond)` | `cond` turned true, false this bar | | `changed(x)` | `x` differs from its last value | | `once(cond)` | true the first bar `cond` holds, never again | | `sustained(cond, n)` | `cond` has held for `n` bars; a count written in the source is a whole number from 1 | | `cooldown(d)` | false for `d` after the trigger last fired; `d` is a duration of 0 or more (`hours(6)`, `"6h"`, a `config`), and only in a `when` or `while` condition (FS0541) | | `hysteresis(x, on, off)` | true once `x` passes `on`, until it falls back past `off` | | `every("1d")`, `every("1w", at: "Mon 14:00")` | each boundary of chain time | ```arcana uses { bars: "^1", alerts: "^1" } const close = bars.close("BTC-USD", "1h"); when (sustained(close > sma(close, 20), 3) && cooldown(hours(6))) { alerts.send("three closes above the 20-bar average"); } when (every("1w", at: "Mon 14:00") && close < sma(close, 200)) { alerts.send("weekly buy window"); } ``` `every` counts chain time from the Unix epoch, from the time a run is given with each bar (or the bar number when there is none). It fires on the first bar at or after each boundary, once per boundary. `every`, `crossover`, `crossunder`, `rising`, `falling`, `changed`, `once` and `triggered` are impulses: true for exactly the bar they mean. A `when` whose condition contains one fires at every bar the condition is true, on consecutive bars too, so `when (every("1d"))` over daily bars fires every day and `when (changed(regime))` at every change. Everything else is a level, and a `when` over a level fires once per crossing. ## Actions and handlers An application with a correlated execution coordinator can choose how it waits: `async orders.marketBuy({ … })` leaves other strategy decisions running while this action's site remains pending. `await orders.marketBuy({ … }, timeout: "30s")` holds strategy decisions until settlement or that chain-time wait deadline. The trailing timeout is execution metadata, not a venue argument; write a positive duration literal. Existing unbounded `await` and action defaults retain their meaning. An explicitly bounded await may attach `.onTimeout { … }`. This reports **the wait ending**, not transaction cancellation or a failed trade. The coordinator keeps its request identity, site exclusivity and unresolved money reserved. A later `.onFill`/`.onRevert` still belongs to that same request. Do not fund a replacement order merely because the strategy resumed. Cancelling an upstream request is a separate supported action and must itself settle. Task `.onTimeout` retains the task invocation's separate timeout semantics. The language run folds reads/metrics while the coordinator pauses decisions; trigger edges and phase transitions remain held. Its snapshot retains the pause and in-flight observations, not the coordinator's authoritative request ledger. The application must persist request IDs, deadlines and event cursors, validate late/duplicate outcomes, then feed execution state and report outcomes. The legacy uncorrelated connector is not a bounded-wait execution coordinator. The JavaScript SDK's `requestRun` supplies a durable request journal for trigger strategies whose managed actions explicitly choose `await` or `async`. It refuses phase machines, maintainers and instantaneous paper brokers. The application supplies durable storage, submits requests and reconciles authoritative venue outcomes; the journal performs no IO. The SDK's legacy `connect`, Rust `Connectors` and the platform `Host` refuse bounded waits. Language support alone does not enable a venue or establish live execution. An action is a call to a capability: `paper.buy({ symbol: "BTC-USD", quote: 100 })`. Each action expression is an **action site**, named by where it is in the program (`when1#1:paper.buy`). A decision carries its site, and an outcome reported for it names the site back. Handlers attach to an action's reported lifecycle events: `.onFill`, `.onRevert`, `.onAbort` and `.onStuck`. A handler reads the outcome as `fill.price`, `fill.amountOut`, `outcome.reason` and the original evaluated arguments as `outcome.args.`, and may fire actions of its own, guarded by `if` if it likes. `outcome.args` contains this action's manifest-declared `evaluated` fields, captured when its decision fired. A child handler has its own outcome scope; an enclosing `fill`, task `result` or `progress` is not an implicit capture. Pass a needed value into the child's declared argument. Put settlement-dependent actions inside `.onFill`. Adjacent action statements are not success-dependent sequencing, including when both say `await`. Handle terminal failure and pre-submission abort on every leg, and handle failure of any recovery operation too. A timeout or uncertain submission is not permission to undo or replace a pending operation. Several operations are not atomic and compensation is never automatic rollback. The [dependent actions and recovery guide](https://arcn.ftw.trade/docs/action-recovery) shows the public primitives, captured argument contract, host persistence requirements and a runnable three-leg example. `requestRun` captures arguments from its saved decision; a raw reporting host must supply and validate the matching original arguments. This language support does not install a protocol settlement executor in an application. ```arcana uses { bars: "^1", paper: "^1", alerts: "^1" } const close = bars.close("BTC-USD", "1h"); when (crossover(close, ema(close, 20))) { paper.buy({ symbol: "BTC-USD", quote: 100 }) .onFill { if (fill.price > close * 1.01) { paper.sell({ symbol: "BTC-USD" }); } alerts.send("bought at " + fill.price); } .onRevert { alerts.send("buy failed: " + outcome.reason); }; } ``` A bridge transfer takes time to land, so it has one more handler: `.onSent` runs when the funds leave, reading what is expected to land as `fill.amountOut`, and `.onFill` runs when they arrive, reading what did. Between the two the transfer is `inFlight()`. `bridge.inTransit(route)` reads how much is on its way along a route, and `bridge.held(route)` what the route's destination account holds, so a strategy's funds are always somewhere it can read. This example needs a host with a bridge module, such as ftw's, so it isn't one of the examples run against this reference's host. ```text uses { bridge: "^1" } const out = market("relay:usdg-robinhood-to-usdc-hyperliquid"); when (bridge.feeBps(out) < 20 && !inFlight()) { bridge.transfer({ market: out, amount: 500 }) .onSent { notify("on its way: " + fill.amountOut); } .onFill { notify("arrived: " + fill.amountOut); }; } ``` An action that signs names its wallet with `from: wallet("main")`, or takes the strategy's `default_wallet`. `from: pick(group("Group A"))` sends it from one wallet of a group, chosen each time it fires; `pick` takes groups and wallets together (`pick(group("A"), wallet("desk"))`). `pick(group("A"), 3)` sends it from three of them at once, each its own order and nonce, as a competing fill needs. The host knows each group's wallets, and chooses by a hash of the action site and the bar, so a replay of the same bars picks the same wallets. A group the host does not know, or one smaller than the count, refuses the action when it fires. The choices are literal, so the wallets a strategy may sign with are read off the program (D1141). This example needs a host with wallet groups, such as ftw's. ```text uses { orders: "^1" } const P = pool("robinhood:0x…"); when (crossover(dex.close(P, "5m"), ema(dex.close(P, "5m"), 20))) { orders.marketBuy({ pair: P, quote: usdc(25), from: pick(group("snipers"), 3) }); } ``` An `if` in a `when` or `while` body guards the actions inside it. It is read at the bar the `when` fires, so `when (a) { if (b) { … } }` acts the bar `a` turns true, if `b` holds then, and not later when `b` turns true while `a` still holds (that is `when (a && b)`). The other actions in the body fire either way, and every action that fires comes out in the order it is written, guarded or not: an `if (inPosition()) { sell }` written above a buy is placed before it. ```arcana uses { bars: "^1", paper: "^1", alerts: "^1" } const close = bars.close("BTC-USD", "1h"); const volume = bars.volume("BTC-USD", "1h"); when (crossover(close, ema(close, 20))) { alerts.send("crossed up"); if (volume > sma(volume, 24)) { paper.buy({ symbol: "BTC-USD", quote: 100 }); } } ``` Who decides the outcome is the application's choice. With a paper broker, a replay or a run fills each order itself. An application with its own venue reports each outcome to the run (`run.report` in TypeScript, `Run::report` in Rust), and the handlers fire at the last bar stepped. ### Contract calls `uses { v: abi("Name", "0x…", ["sig", …], { Struct: "(…)" }) }` imports exactly the listed functions of the contract at that address (D550b, D1249, D1250). A program can call any function of any contract this way. A call is an action: `v.fn({ param: value, … })`, with each parameter passed by its name in the signature. ```text uses { bars: "^1", v4: abi("Router", "0x…", [ "swap(PoolKey key, bool zeroForOne, int256 amount, bytes hookData)", "swap(PoolKey key, bool zeroForOne, int256 amount) as swapSimple", ], { PoolKey: "(address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks)" }) } const key = { currency0: "0x…", currency1: "0x…", fee: 3000, tickSpacing: 60, hooks: "0x…" }; when (bars.close("ETH-USD", "1h") > 4000) { v4.swapSimple({ key: key, zeroForOne: true, amount: 5000, retries: 2, backoff: "3 blocks" }); } ``` | Solidity | Arcana value | | --- | --- | | `uintN`, `intN` | a whole number; sent as exact decimal text, range-checked by the encoder | | `address`, `bytesN`, `bytes`, `string` | text (`"0x…"` for addresses and bytes) | | `bool` | a condition | | a tuple `(T a, U b)` or a named struct | a record with exactly those fields | | `T[]`, `T[n]` | a list `[…]`, of exactly `n` items for `T[n]` | - **Structs.** A named struct is declared once in the fourth argument and used by name in any signature, nested up to eight deep. A tuple can also be written inline: `"f((address to, uint256 v)[] legs)"`. - **Overloads.** Two functions with one name are told apart by renaming one with `as`. The name in the program changes and the selector stays the contract's own. - **Call options.** `from`, `retries`, `backoff` and `value` sit beside the arguments, so a parameter cannot use those names. - **Views.** A signature ending `view returns (T)` is read like a stream, with literal arguments, or a `const` holding one: `when (vault.pending("0x…") > 1000)`. - **Checks.** A missing or extra field, a list of the wrong length, and a literal of the wrong type are refused when the program compiles. A value that does not fit its type, or that is `na` or fractional, refuses the call before anything is signed. - **Interfaces.** `interface ERC20 = abi(["sig", …], { structs });` lists a contract's functions without an address, and `uses { usdc: ERC20("0x…") }` binds it to one. Two contracts with the same functions are then two names, `usdc.balanceOf(…)` and `weth.balanceOf(…)`, with no clash. A program declares its own interfaces or imports them from a library. - **Standard libraries.** `import "std/erc20";` gives the `ERC20` interface (transfer, approve, transferFrom, and the balanceOf, allowance, totalSupply and decimals views) and `tokenUnits(amount, decimals)`. `import "std/erc4626";` gives `ERC4626`: deposit, mint, withdraw and redeem, and views for its assets, balances, conversions and previews. Their sources and tests are in `arcana/std/`. The standard's `from` parameter is named `owner`, because `from:` is a call option. - **Events.** The same parser reads event signatures for sources that decode logs: `abiEvent("Swap(bytes32 indexed id, int128 amount0, …)")` in `@arcana/core`, and `abiFunction(sig, structs)` for functions. ## Enums and match `enum` declares a closed set of words. An unknown variant is a compile error with the nearest one, and `match` must cover every variant or end in `_`. ```arcana uses { bars: "^1" } enum Regime { hot, quiet, normal } const close = bars.close("BTC-USD", "1h"); const vol = stddev(change(ln(close)), 24); const regime = vol > 0.02 ? Regime.hot : (vol < 0.005 ? Regime.quiet : Regime.normal); metric("regime", regime); metric("size", match (regime) { hot => 0, quiet => 1, _ => 0.5 }); ``` ## Phase machines `phases` declares a state machine. Each `phase` holds the triggers that apply in it, and a `when` inside one names the phase it moves to. A phase's `let` bindings start over each time the phase is entered, so `cummax(close)` inside `Long` is the high since this entry, the entry bar's close included; like any `let`, one cannot take a `const`'s name. A phase's `entry { … }` block fires when the machine enters it, right after the transition's own actions. ```arcana uses { bars: "^1", paper: "^1", alerts: "^1" } const close = bars.close("BTC-USD", "1h"); phases Trade { phase Flat { when (crossover(close, ema(close, 20))) => Long { paper.buy({ symbol: "BTC-USD", quote: 100 }); } } phase Long { entry { alerts.send("long"); } let stop = cummax(close) * 0.95; when (close < stop) => Flat { paper.sell({ symbol: "BTC-USD" }); } } } metric("phase", phase("Trade")); ``` A target that does not exist, a `when` in a phase with no target, and a phase nothing can reach are compile errors. Every trigger decides against the phases the bar began in, so a machine moves at most once a bar. An `if` inside a transition's body fires with the move, when its guard holds then; the machine moves either way. An `if` inside an `entry` block is a compile error (`phases.guard`): put the guard on the transition that enters the phase, or in a `when` inside it. ## Risk limits An `invariants` block bounds the risk a strategy takes. Each line is a comparison of something measured against a literal limit, and the block ends with what a breach does: ```arcana uses { bars: "^1", paper: "^1" } const close = bars.close("BTC-USD", "1h"); when (crossover(close, ema(close, 20))) { paper.buy({ symbol: "BTC-USD", quote: 100 }); } when (crossunder(close, ema(close, 20))) { paper.sell({ symbol: "BTC-USD" }); } invariants { drawdown() <= pct(5); dailyLoss() <= 50; trades.perDay() <= 4; on_breach: pause; } ``` - `drawdown()` is the percent fallen from the highest the money put to work has been worth: the largest cost basis held, plus everything made or lost. The book counted at cost is a peak too, so a gap on the bar after a fill is drawdown, and fees count. - `dailyLoss()` is how far profit has fallen since the UTC day began, in quote units. Bars with no time are one day. - `trades.perDay()` counts the orders sent today. A bound reads it as the count the next order would make, so `<= 4` admits four and refuses the fifth. - `position(P).size`, `.entryPrice` and `.pnl` read one market's position. While a bound is breached, or cannot be read, every entry is refused: a buy is not placed, and the run records why (`invariant (drawdown: 6.2/5, on_breach pause)`). A sell is never refused, because a cage that froze a losing position would be the opposite of one. `on_breach: flatten_and_pause` also sells every open position at the bar a `drawdown()` or `dailyLoss()` bound breaks, and at each bar after while it stays broken. `alert` refuses entries the same way and sells nothing. A replay, a live run and a test enforce the block on their paper book the way ftw's host does on its own. A replay lists what the cage refused in `refused`, and a test expects it with `{ paused: paper.buy, at: 15, by: drawdown }`. A program can read the same measures itself, in a `metric` or a condition. Without a paper broker there is no book to measure, and `unmodelled` says the block is not enforced. ## Tapes A tape is a source of events (trades, prints, messages) that arrive whenever they happen, not once a bar. `map` picks a field off each event, `resample` or `window` buckets the events, and a reducer (`sum`, `mean`, `count`, `first`, `last`, `distinct`) turns each bucket back into one number. ```arcana uses { trades: "^1" } const prints = trades.prints("BTC-USD"); const buys = map(prints, (p) => p.side == "buy" ? p.size : 0); metric("buy_volume_1h", sum(resample(buys, "1h"))); metric("prints_15m", count(window(prints, "15m"))); ``` Using a tape where a bar series is expected is the `cadence.events` error. A host may declare a tape field as an enum, `{ "case": { "enum": ["call", "exit", "note"] } }`: the tag of a sum type. Inside a `map` it reads as an enum, so `match (e.case) { call => e.usd, exit => -e.usd, note => 0 }` must cover every word or end in `_`, and `e.case == "cal"` is refused with the nearest word. An event whose tag is not one of the words is refused when it is fed to the program. ## The prelude The prelude is Arcana source (`prelude/standard.arcn`), and its doc comments are the definitions. In brief: | Function | | |---|---| | `sma(src, len)`, `ema(src, len)`, `ewma(src, len)`, `wma(src, len)`, `rma(src, len)` | moving averages (`ema` is SMA-seeded, as charts draw it; `ewma` seeds on the first close) | | `stddev(src, len)`, `zscore(src, len)` | dispersion over a window | | `rsi(src, len)` | Wilder's RSI | | `atr(src, len)`, `trueRange(h, l, c)`, `wilderAtr(h, l, c, len)` | true range and average true range | | `vwap(price, vol, len)` | volume-weighted average price over a window | | `anchoredVwap(price, vol, anchor)`, `anchoredVwapDev(price, vol, anchor)` | the VWAP since the last bar `anchor` held, that bar included (a swing low, a news bar, an entry), and its volume-weighted standard deviation: Pine's `ta.vwap(src, anchor)`. `na` until the anchor has held and traded. `import "std/vwap";` adds week and month anchors and the reads a VWAP trader makes (Libraries) | | `periodVwap(price, vol, t, secs)`, `periodVwapDev(price, vol, t, secs)` | the session VWAP a chart draws, reset on the first bar of each period (`86400`: from 00:00 UTC), and its volume-weighted standard deviation, so `periodVwap(…) ± 2 * periodVwapDev(…)` are its 2σ bands (Pine's `ta.vwap(src, anchor, stdev_mult)`). `na` until the period has traded. See the starter "VWAP rotation: fade failed pushes to the mean" | | `corr(a, b, len)`, `cov(a, b, len)` | correlation and covariance over a window; given a list, `cov([a, b, c], len)` is the matrix | | `beta(y, x, len)` | the hedge ratio: the slope of `y`'s least-squares line on `x` over the window, `ols(y, [x], len)[1]` | | `roc(src, len)`, `momentum(src, len)` | rate of change, and change over `len` bars | | `cummax(src)`, `cummin(src)` | high and low since the start (or since a phase was entered) | | `highest(src, len)`, `lowest(src, len)`, `rolling_sum(src, len)` | the high, low and sum of the last `len` bars (Pine's `ta.highest`, `ta.lowest`, `math.sum`) | | `cum(src)` | the running total since the start | | `barssince(cond)`, `valuewhen(cond, src)` | bars since `cond` last held, and `src` at that bar (`na` until it first has) | | `linreg(src, len)` | the least-squares line's value at this bar over the last `len` bars | | `stoch(c, h, l, len)` | where `c` sits in the last `len` bars' range, 0 to 100 | | `pivothigh(src, left, right)`, `pivotlow(src, left, right)` | the value `right` bars back when it is the extreme of the `left + right + 1` bars around it, else `na`; known `right` bars late, as Pine's are | | `macd(src, fast, slow)`, `macdSignal(src, fast, slow, sig)`, `macdHist(…)` | the MACD line, its signal and the histogram between them | | `bbUpper(src, len, mult)`, `bbLower(src, len, mult)`, `variance(src, len)` | Bollinger bands around `sma(src, len)`, and the window's variance | | `vwma(src, vol, len)`, `swma(src)` | volume-weighted and four-bar symmetric averages | | `wpr(h, l, c, len)`, `cmo(src, len)`, `mfi(src, vol, len)`, `obv(src, vol)`, `tsi(src, short, long)` | Williams %R, Chande momentum, money flow, on-balance volume, true strength | | `diPlus(h, l, c, len)`, `diMinus(h, l, c, len)`, `adx(h, l, c, len, adxLen)` | Wilder's directional movement | | `supertrend(h, l, c, factor, len)`, `supertrendDir(…)`, `sar(h, l, c, start, inc, max)` | Supertrend's line and direction (−1 up, 1 down), and the parabolic SAR | | `hour(t)`, `minute(t)`, `dayofweek(t)`, `dayofmonth(t)`, `month(t)`, `year(t)` | a unix-seconds time's calendar in UTC (`dayofweek` is 1 for Sunday to 7 for Saturday, as Pine's); another zone is a shifted time, `hour(t + 3600*9)` | | `nyTime(t)`, `chicagoTime(t)`, `londonTime(t)`, `frankfurtTime(t)`, `sydneyTime(t)`, `tokyoTime(t)`, `hongKongTime(t)` | the time on that city's wall clock, daylight saving included, for the calendar above: `hour(nyTime(t))` is New York's hour all year | | `inSession(lt, start, end)`, `inWeekdaySession(lt, start, end)` | whether a wall-clock time is in [start, end), each HHMM, Pine's session `"0930-1600"`; an end before the start runs past midnight; `inWeekdaySession` keeps to Monday to Friday, Pine's `":23456"` | | `sessionStarts(active)`, `sessionOpen(o, active)`, `sessionHigh(h, active)`, `sessionLow(l, active)` | a session's first bar, and its open, high and low so far (between sessions, the last one's) | | `nyseClose(t)` | the New York Stock Exchange's close on `t`'s New York date, HHMM: 1600, 1300 on an early close, 0 on a weekend or a holiday (its rules since 1993, Juneteenth from 2022, and the closures it named, such as 9/11 and Hurricane Sandy) | | `nyseSession(t)`, `nyseTradingDay(t)`, `nyseHoliday(t)`, `nyseEarlyClose(t)`, `nyseMinutesToClose(t)` | whether the exchange is open at `t` (9:30 to its close), opens that day, is shut on a weekday, closes at 13:00; the minutes to its close (`na` outside the session). The overnight hold: buy when `nyseMinutesToClose(t) <= 15`, sell when `sessionStarts(nyseSession(t))` | | `barIndex(src)` | the bar's index from 0 | | `hma(src, len)`, `dema(src, len)`, `tema(src, len)` | the Hull, double and triple exponential averages | | `cci(src, len)`, `cog(src, len)`, `range(src, len)` | the commodity channel index, the centre of gravity, and the window's high less its low | | `kcUpper(src, h, l, c, len, mult)`, `kcLower(…)`, `kcw(…)`, `bbw(src, len, mult)` | Keltner channels around `ema(src, len)`, and the Keltner and Bollinger widths as a ratio of the basis | | `cross(a, b)` | `a` crossed `b` either way this bar | | `pvt(src, vol)`, `accdist(h, l, c, vol)`, `wad(h, l, c)`, `nvi(src, vol)`, `pvi(src, vol)`, `iii(h, l, c, vol)`, `wvad(o, h, l, c, vol)` | price-volume trend, accumulation/distribution, Williams' A/D, the negative and positive volume indexes, intraday intensity, and Williams' variable A/D | ## Built-in functions | | | |---|---| | Math | `abs`, `min(a, b, …)`, `max(a, b, …)` (`na` if any is), `clamp(x, lo, hi)`, `ln`, `log10`, `exp`, `sqrt`, `pow`, `bps` | | Rounding | `floor`, `ceil`, `round` (halves away from zero, as Pine's), `sign` (1, −1 or 0) | | Trigonometry | `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, in radians | | `na` | `na`, `isNa(x)`, `nz(x, default)` | | Series | `change(x)`, `rolling_high(x, n)`, `rolling_low(x, n)`, `median(x, n)`, `percentile(x, n, q)` | | Higher timeframe | `sample(cond, inputs, x => body)`: `body` as if the chart had only the bars `cond` holds on, its answer held between them; `inputs` (a series or a record of them) is read on every bar and is all `body` reads. `newPeriod(t, secs)` and `periodEnds(t, secs)` pick a period's first and last bars, and `periodOpen`, `periodHigh`, `periodLow` and `periodVolume(…, t, secs)` gather its bar | | Window statistics | `skew(x, n)` and `kurtosis(x, n)` (the biased skewness and excess kurtosis, scipy's defaults), `dev(x, n)` (mean absolute deviation), `percentrank(x, n)` (the percent of the `n` bars before this one at or below it), `highestbars(x, n)` and `lowestbars(x, n)` (how many bars back the extreme is, as 0 or less), `alma(x, n, offset, sigma)` (the Arnaud Legoux average); `n`, `offset` and `sigma` are written in the source | | Time | `seconds`, `minutes`, `hours`, `every`, `daily`, `during` | | Triggers | `crossover`, `crossunder`, `rising`, `falling`, `changed`, `once`, `sustained`, `cooldown`, `hysteresis` | | Tapes | `map`, `resample`, `window`, `sum`, `mean`, `count`, `first`, `last`, `distinct` | | Lists | `median(xs)`, `mean`, `sum`, `min`, `max`, `count`, `stddev`, `variance`, `mad`, `skew`, `kurtosis`, `percentile(xs, q)`, `wmean(xs, ws)`, `dot(a, b)`, `solve(A, b)`, `inv(A)`, `det(A)`, `transpose(A)`, `ols(y, xs, n)`, `cov(xs, n)`, `corr(xs, n)`; see below | | Arrays | `buffer(x, n)`, `push(xs, x, n)`, `concat`, `size`, `xs[i]`, `slice(xs, start, end?)`, `reverse`, `sort`, `sortBy(xs, f)`, `unique`, `contains`, `indexOf`, `map(xs, f)`, `filter`, `reduce(xs, seed, f)`, `any`, `all`, `find`, `groupBy(xs, f)`, and the list reducers and `first`, `last`, `count`, `distinct`; see [Arrays](#arrays) | | Market profile | `tpo.poc`, `tpo.vah`, `tpo.val`, `tpo.vpoc`, `tpo.vvah`, `tpo.vval`, `tpo.vpocShift`, `tpo.high`, `tpo.low`, `tpo.ibHigh`, `tpo.ibLow`, `tpo.poorHigh`, `tpo.poorLow`, `tpo.singleAbove`, `tpo.singleBelow`, `tpo.nakedPocAbove`, `tpo.nakedPocBelow`, each `(high, low, close, volume, t, window: n, …)`; see [Market profile](#market-profile) | | Trained models | `ml.linear`, `ml.ridge`, `ml.lasso`, `ml.elasticNet`, `ml.logistic`, `ml.svm`, `ml.svr`, `ml.knn`, `ml.boost`, `ml.boostClassifier`, each `(y, [x1, x2, …], window: n, …)`; see [Trained models](#trained-models) | | Exact integers | `u256` and `i64` checked arithmetic, `u256MulDiv`/`u256MulDivUp`, `u256Units`, `u256Format` and safe number conversions; [details](#exact-integers) | ### Lists, statistics and linear algebra A list, `[a, b, c]`, is a fixed number of numbers or series: three price feeds, a portfolio's weights, the factors of a hedge. A list of equal-length lists is a matrix, row by row. A list has the length it was written with, so every shape is checked when the program compiles, and nothing about it costs anything at run time that its items do not (D1524). ```arcana uses { bars: "^1" } // The median of three readings, skipping one that has no price this bar. const feeds = [bars.open("BTC-USD", "1h"), bars.close("BTC-USD", "1h"), bars.high("BTC-USD", "1h")]; metric("mid", median(feeds)); metric("spread", max(feeds) - min(feeds)); // Arithmetic on a list applies to each item. const scaled = feeds / feeds[1] - 1; metric("open_vs_close", scaled[0]); ``` Across assets, the same tools hedge and size a book: ```text uses { bars: "^1" } const btc = bars.close("BTC-USD", "1h"); const eth = bars.close("ETH-USD", "1h"); const sol = bars.close("SOL-USD", "1h"); const returns = [btc, eth, sol] / [btc[1], eth[1], sol[1]] - 1; // SOL hedged with BTC and ETH: a rolling least-squares fit over 200 bars, // [intercept, b_btc, b_eth], and the residual the hedge leaves. const fit = ols(returns[2], [returns[0], returns[1]], 200); const residual = returns[2] - dot(fit, [1, returns[0], returns[1]]); metric("sol_beta_btc", fit[1]); metric("residual_z", (residual - sma(residual, 50)) / stddev(residual, 50)); // Minimum-variance weights: solve Σ·w = 1, then scale them to sum to 1. const w = solve(cov(returns, 200), [1, 1, 1]); const weights = w / sum(w); metric("w_btc", weights[0]); ``` - **Reducers** take one list and answer one number from its decided items: `median`, `mean`, `sum`, `min`, `max`, `count`, `stddev` and `variance` (dividing by `n`, as the windowed `stddev` does), `mad` (the median absolute deviation), `skew`, `kurtosis` (excess) and `percentile(xs, q)` with `q` from 0 to 1. An `na` item is skipped, so a feed that has no price this bar does not blank the median of the others; the answer is `na` only when no item is decided. The conventions are numpy's and scipy's defaults: an even `median` averages the middle two, and `percentile` interpolates between the two nearest ranks. (The windowed `median(x, n)` and `percentile(x, n, q)` pick the nearest rank, as they always have.) - **Arithmetic** `+ - * /` and unary `-` apply item by item: two lists of the same length pair up, and a number or series on one side applies to every item, as numpy broadcasts. A comparison does not take a list: reduce it first, `max(xs) > 100`. - **Items**: `xs[0]` is the first item and `xs[-1]` the last, an index written in the source; on a matrix, `A[0]` is the first row. On a list, `[k]` is an item, never a bar back: `feeds[1]` is the close, and `btc[1]` is BTC an hour ago. - **Linear algebra** at each bar: `dot(a, b)` is numpy's (a number for two vectors, a vector for a matrix and a vector, a matrix for two matrices), `wmean(xs, weights)` the weighted mean, `solve(A, b)` the vector `x` with `A·x = b`, `inv(A)`, `det(A)` and `transpose(A)`. A singular matrix makes `solve` and `inv` `na` rather than an answer made of rounding error. - **Rolling** over the last `n` bars at which every series is decided: `ols(y, xs, n)` is the least-squares fit of `y` on a constant and the list `xs`, `[intercept, b1, …]` (Householder QR, so a nearly collinear pair does not lose its digits as the normal equations would); `cov(xs, n)` and `corr(xs, n)` are the covariance and correlation matrices. Each is `na` until its window fills, and `ols` is `na` while a factor does not move. OLS normalizes each column and the target by an exact power of two before fitting, so very large or small units do not overflow the QR norms or turn a varying factor into a constant. Coefficients are returned in the original units. A rank-deficient fit, a nonfinite coefficient, or a fit whose normalization would lose an input's precision remains `na`. - A list is not a value to publish: `metric("w", weights)` is an error that asks for an item or a reduction. A `fn` takes and returns a list like any other value. Every evaluator answers these with one Rust implementation (`arcana/src/stats.rs`), in plain IEEE arithmetic, so a backtest, a live run and a chart study agree to the bit. Generated list and matrix IR is reserved against the compiler's existing size and lowering quotas before eager copying; oversized expansions are located compilation refusals. The live evaluator charges matrix elimination and rolling regression work before execution against its watermark evaluation budget, including computations whose result would be unavailable. ### Arrays An array is a value a run gathers as it goes, as long as its data makes it: the last hundred trades a source heard, the wallets among them that bought, the book grouped by pool (D1544). A [list](#lists-statistics-and-linear-algebra) has the length it was written with and costs nothing at run time; an array is held by the run, so it can be built from a stream, filtered and sorted. ```arcana uses { bars: "^1" } const close = bars.close("BTC-USD", "1h"); const volume = bars.volume("BTC-USD", "1h"); // The last 48 hourly bars, oldest first, each a record. const bars48 = buffer({ close: close, volume: volume }, 48); // The bars that traded more than usual, and the five busiest of them. const usual = mean(map(bars48, (b) => b.volume)); const busy = filter(bars48, (b) => b.volume > usual * 1.5); const busiest = slice(sortBy(bars48, (b) => -b.volume), 0, 5); metric("busy_bars", size(busy)); metric("busiest_close", busiest[0].close); metric("median_close", median(map(bars48, (b) => b.close))); // What a fold keeps can be an array too: every close above 100, the newest 20. const highs = fold(close, [], (xs, c) => c > 100 ? push(xs, c, 20) : xs); metric("highs", size(highs)); ``` - **Building one.** `buffer(x, n)` is the last `n` samples of `x`, oldest first: a series, or a record such as a source's `input.event()`. A sample that is `na` is not kept, and `buffer` steps when its input ticks, as a window does. Inside a `fold`, an array seed (`[]`) holds what the step puts in it: `push(xs, x, n)` is `xs` with `x` after its last item, keeping the newest `n` (an `na` `x` is not added). `concat(a, b)` is `a`'s items then `b`'s. A list written in the source, `[a, b, c]`, is an array wherever a function takes one. - **Reading one.** `size(xs)` is how many items it holds; `xs[i]` is an item, from 0, with −1 the last, and `na` past either end (the index may be any number the run computes). `first(xs)` and `last(xs)` are the ends. `slice(xs, start, end?)` is the items from `start` up to `end`, −1 counting from the last, so `slice(xs, -10)` is the last ten. `contains(xs, v)` and `indexOf(xs, v)` (`na` when it is not there) look for an item. - **Functions of its items.** `map(xs, x => …)`, `filter(xs, x => cond)`, `reduce(xs, seed, (acc, x) => next)`, `any`, `all`, `find` (the first item the condition holds for, or `na`), `sortBy(xs, x => key)` (lowest key first, stable, `na` last; `-x.usd` for the largest first) and `groupBy(xs, x => key)`, which is `{ key, items }` for each key in the order it first appears (an item whose key is `na` is in none). `sort(xs)` orders numbers or text, `reverse(xs)` turns it round, and `unique(xs)` keeps each different item once. - **Reducing one.** An array of numbers takes the list reducers, `sum`, `mean`, `median`, `min`, `max`, `stddev`, `variance`, `mad`, `skew`, `kurtosis` and `percentile(xs, q)`, skipping `na` items as a list's do; the sum of no numbers is 0. `count(xs)` is the items that are not `na`, and `distinct(xs)` how many different values it holds. Reduce an array of records by mapping it first: `sum(map(xs, x => x.usd))`. - **A list of markets reads each of them** (D1545). A read keyed by a list written in the source, `aerodrome.rewardApr(pools, interval)` with `const pools = ["base:a", "base:b"]`, is the list of the reads, one per market in the list's order: `apr[1]` is the second's, `max(apr)` the best, and the artifact names every series read. The keys are literals, as any read's key is; a list of anything else is refused (`identity.key`). - **`map` over a list runs its function at compile time**, once per item, with the item itself bound to the parameter, so, unlike an array's `map`, its body may window and read: `map(price, (p, i) => stddev(change(p) / p[1], 24))` is a list of one volatility per market, and the second parameter is the item's position from 0. The answer is a list, which is an array wherever a function takes one: `sortBy(map(pools, (p, i) => ({ pool: p, apr: apr[i] })), (s) => -s.apr)` ranks the markets. - **A function given to an array runs once for each item**, so it keeps no state and reads no feed of its own (`array.lambda`): `filter(xs, x => x > ema(close, 20))` is refused. What it reads from outside is computed once, before it, and the function reads that value, so bind it to a `const` and read the const: `const level = ema(close, 20); filter(xs, x => x > level)`. - An array is not a number: arithmetic and comparisons refuse one, and so does `metric`. Publish one of its items or a reduction of it. An action argument its capability declares `Json` takes an array, so a source can publish one as a field (below). - **Bounds.** An array holds at most 10,000 items: `buffer` and `push` keep at most that many, written in the source, and a `concat` that would hold more is `na`. Every item an operator touches, and a sort's `n log n`, is charged to the watermark's evaluation budget before the work, and an array a run keeps counts toward its retained state. A program that would walk or keep more than a run may is stopped with a refusal at that watermark, rather than slowing every run beside it. In a source, each event is one step, so `buffer(input.event(), 200)` is the last 200 events and every function above works on them. A field declared `json` publishes an array: ```text uses { input: "^1", source: "^1" } const e = input.event(); const recent = buffer(e, 200); const byPool = groupBy(recent, (s) => s.pool); const flows = map(byPool, (g) => ({ pool: g.key, usd: sum(map(g.items, (s) => s.usd)) })); const top = slice(sortBy(flows, (f) => -f.usd), 0, 5); while (size(recent) == 200) { source.emit({ pool: e.pool, top: top }); } ``` **A tape's array field.** A host may declare a tape's field an array, of numbers, text, words or records (`{ "array": { "record": { "pool": "text", "usd": "number" } } }`, as a ftw source declares `[{ pool: "pool", usd: "number" }]`), and a program reads it as one. `buffer(tape, n)` keeps a tape's last `n` events across watermarks as an array of records, newest last, so the newest event's array is `flows[-1].top`; inside a tape's `map`, an array function over the event's own field (`size(e.top)`) is one value per event, which its buckets reduce. An array field left out is empty. ```text const flows = buffer(sources.top_pools(), 10); const top = size(flows) > 0 ? flows[-1].top : []; metric("usd", sum(map(top, (f) => f.usd))); ``` **On a chart.** A study may keep an array too, as long as each line it draws is a number: `metric("up", size(filter(buffer(price, 20), (x) => x > open)))`. The compiled plan holds no arrays, so such a study is replayed bar by bar by the same evaluator a strategy runs on, and draws what that run computes. ### Trained models A strategy can train a regression or a classifier on its own series and use its prediction at every bar, in a backtest and live alike (D1561). A model is written `ml.(y, [x1, x2, …], window: n, …)`: the target `y`, a list of predictors, and named settings. ```arcana uses { bars: "^1", paper: "^1" } const close = bars.close("BTC-USD", "1h"); const volume = bars.volume("BTC-USD", "1h"); // The return over the last 6 hours: the target, known at each bar. const ret6 = close / close[6] - 1; // What the model sees at each bar. const features = [ close / close[1] - 1, close / ema(close, 24) - 1, volume / sma(volume, 24) - 1, rsi(close, 14) / 100 - 0.5, ]; // Learn the next 6 hours' return from the last 500 hours, refitted every // 24 hours and whenever volume spikes. const forecast = ml.ridge(ret6, features, window: 500, horizon: 6, retrain: 24, retrainOn: volume > sma(volume, 24) * 3, alpha: 0.5); // The chance the next 6 hours close higher. const up = ml.logistic(ret6, features, window: 500, horizon: 6, retrain: 24); metric("forecast", forecast); metric("p_up", up); when (forecast > 0.004 && up > 0.6) { paper.buy({ symbol: "BTC-USD", quote: 100 }); } when (forecast < 0) { paper.sell({ symbol: "BTC-USD" }); } ``` - **How it trains.** At every bar at which `y` and every predictor are decided, the model keeps a row of them, the last `window + horizon` rows. A training example pairs one row's predictors with `y` from `horizon` rows later, so the model learns what `y` will be `horizon` bars after what it sees, and never trains on a target that is in the future of the bar it answers at. `horizon` is 1 when omitted; `horizon: 0` fits `y` to the predictors of the same bar, as `ols` does. - **When it retrains.** The model is fitted when its rows first fill, again every `retrain` new rows (1 when omitted, so every bar), and again at any row where `retrainOn` is true. `retrain: 0` fits once and keeps that model, unless `retrainOn` asks for another. Between fits it keeps its parameters, which are part of the run's state: a resumed run continues with the same model. A fit that the rows do not determine (one class only for a classifier, a predictor that does not move for `ml.linear`) keeps the model it had. - **What it answers.** At each new row, the model's prediction from that row's predictors; at a bar with no new row it holds its last answer. A regression predicts `y`; `ml.logistic` and `ml.boostClassifier` answer the probability that `y` is above zero; `ml.svm` answers its signed margin, above zero for "`y` above zero". `na` until the first fit. - **Predictors are standardised** over each fit's examples (centred and divided by their standard deviation) and a prediction standardises its row with the same numbers, so a penalty treats every predictor alike whatever its units. A predictor constant over the window is left out of that fit. Bounded, power-of-two moment calculations preserve changing predictors even in very small or large units; only exactly constant predictors are omitted. A nonconstant spread that cannot be represented refuses the fit, and a prediction with a nonfinite standardized input is `na`. `window`, `horizon`, `retrain` and every setting are numbers written in the source (or `const`s), since they set the model's state and work. | Model | Settings (default) | What it is | |---|---|---| | `ml.linear` | | Ordinary least squares on a constant and the predictors | | `ml.ridge` | `alpha` (0.1) | Least squares with an L2 penalty, `(1/2n)·‖y − Xw‖² + (alpha/2)·‖w‖²`, on a standardised target | | `ml.lasso` | `alpha` (0.01) | An L1 penalty, `alpha·‖w‖₁`, which drops predictors: one whose correlation with the target is below about `alpha` gets no weight | | `ml.elasticNet` | `alpha` (0.01), `l1Ratio` (0.5) | The two penalties mixed, `alpha·(l1Ratio·‖w‖₁ + ((1 − l1Ratio)/2)·‖w‖²)` | | `ml.logistic` | `C` (1) | L2-regularised logistic regression on "`y` above zero", `C·Σ logloss + ½‖w‖²` (scikit-learn's) | | `ml.svm` | `C` (1), `kernel` (`"linear"`), `gamma` | A support-vector classifier on "`y` above zero"; `kernel: "rbf"` with `gamma` (1 / the number of predictors when omitted) for a curved boundary | | `ml.svr` | `C` (1), `epsilon` (0.1), `kernel`, `gamma` | Support-vector regression, `epsilon` in standard deviations of the target | | `ml.knn` | `k` (5) | The mean target of the `k` training examples nearest the row | | `ml.boost` | `trees` (100), `depth` (3), `learningRate` (0.1), `minLeaf` (20) | Gradient-boosted regression trees on the squared error of a standardised target: `trees` trees of `depth` levels, each fitted to what the trees before it missed and added at `learningRate`, every leaf holding at least `minLeaf` examples. Trees find thresholds and interactions (a feature that matters only in a trend, a reversal past an extreme) that a linear model cannot | | `ml.boostClassifier` | as `ml.boost` | Gradient-boosted trees on the log loss of "`y` above zero": the probability that `y` is above zero | Every evaluator fits with one Rust implementation (`arcana/src/ml.rs`), in plain IEEE arithmetic with no randomness: least squares by Householder QR, ridge by its normal equations, lasso and elastic net by coordinate descent, logistic regression by Newton's method, and support-vector models by dual coordinate descent (LIBLINEAR's), boosted trees by second-order gradient boosting over 32 quantile split points per predictor (XGBoost's leaves with λ = 1), each stopping at a fixed tolerance or iteration cap. So a backtest, the optimizer and a live run fit the same model to the bit. A fit's work is charged against the bar's evaluation budget before it starts, and a model whose fit could exceed it is refused at compile time: reduce `window`, the predictors, or a boosted model's `trees` or `depth`. An RBF model or `ml.knn` keeps its training rows, so its `window` is at most 1,000. Refitting every bar is the default and is cheap for `ml.linear` and `ml.ridge`; for the iterative models and boosted trees, a `retrain` cadence keeps backtests fast. `window` must hold at least two `minLeaf`s of examples. ### Market profile A market profile (TPO, time price opportunity) counts the time price spent at each level: every bar puts one block in each price row its range touched. `tpo.(high, low, close, volume, t, window: n, …)` reads a level of the profile of the last whole session (or, with `previous: false`, of the session still forming) from the last `n` bars, in a backtest and live alike. The blocks are the bars the program runs on, so the interval sets the resolution: 30-minute bars for a daily profile and 4-hour bars for a weekly one are the conventions. The chart's TPO study draws the same profiles. ```arcana uses { bars: "^1", paper: "^1" } const high = bars.high("BTC-USD", "1h"); const low = bars.low("BTC-USD", "1h"); const close = bars.close("BTC-USD", "1h"); const volume = bars.volume("BTC-USD", "1h"); const t = bars.time("BTC-USD", "1h"); // Yesterday's profile, from the last two days of hourly bars. const poc = tpo.poc(high, low, close, volume, t, window: 48); const poorLow = tpo.poorLow(high, low, close, volume, t, window: 48); // Volume sat below where the time was spent: sellers did the business. const shift = tpo.vpocShift(high, low, close, volume, t, window: 48); metric("poc", poc); metric("shift", shift); // Buy back into value over a poor low left yesterday, target its POC. when (poorLow == 1 && crossover(close, tpo.low(high, low, close, volume, t, window: 48))) { paper.buy({ symbol: "BTC-USD", quote: 100 }); } when (close >= poc) { paper.sell({ symbol: "BTC-USD" }); } ``` The series are the bars' high, low, close, volume and start time `t` (unix seconds). The levels: | Level | | |---|---| | `poc` | the time point of control: the middle of the row with the most blocks (a tie goes to the row nearest the middle of the range) | | `vah`, `val` | the value area's top and bottom: grown from the POC two rows at a time toward the heavier pair until it holds `valueArea` of the blocks | | `vpoc`, `vvah`, `vval` | the same over volume, each bar's spread evenly over the rows it touched | | `vpocShift` | `vpoc − poc`: positive when the volume sat above where the time was spent | | `high`, `low` | the session's range | | `ibHigh`, `ibLow` | the initial balance: the first two bars' range | | `poorHigh`, `poorLow` | 1 when the extreme row has two or more blocks (no single-print tail: an unfinished auction, which price tends to revisit), else 0 | | `singleAbove`, `singleBelow` | the nearest single print (a one-block row inside a profile, not its tail) above or below the close that no later bar has traded through, over every whole session in the window | | `nakedPocAbove`, `nakedPocBelow` | the nearest session POC above or below the close that no later bar has traded at | Settings, each a literal or a `const`: - `window: n` (required): the bars kept. A session is read only once it is whole in the window, so keep at least two sessions of bars (96 on 30m for a daily profile); more widens the search for single prints and naked POCs. - `session`: `"day"` (UTC, the default), `"week"` (from Monday 00:00 UTC), `"window"` (the whole window as one profile), or a length in seconds; `offset` shifts where a numbered session starts. - `previous`: `true` (the default) for the last whole session, `false` for the one still forming. - `row`: the row height in price; 0 (the default) picks a round number near a 36th of the median session's range. At most 2000 rows a profile. - `valueArea`: the value area's share, 0.7 by default. A level not there to read (no session whole yet, no single print above, a session with no volume) is `na`. ### Floating-point behavior Ordinary numeric expressions use IEEE 754 binary64. Finite observations retain their bits, including negative zero and subnormals, through native and WASM inputs, live evaluation, replay and checkpoints. A nonfinite observation or arithmetic result is `na`. `round` resolves halfway cases away from zero. Evaluation preserves the written grouping and separate rounding of operations; it does not reassociate sums or fuse multiplication with addition. ### Exact integers Ordinary numbers use IEEE 754 binary64. Consecutive integers are safe only through 9,007,199,254,740,991 (2^53 − 1). In an 18-decimal token that is about **0.009 tokens**, so balances and contract base units must use exact integers. Arcana 0.1 provides checked `u256` (0 through 2^256 − 1) and `i64` (−2^63 through 2^63 − 1, a signed `long`) functions. Their integer results are canonical **decimal text**, preserving every digit through action arguments, JSON, saved state and replay. They are a math library over text, not new operator-overloaded types: use `u256Add`/`i64Add` and `u256Cmp`/`i64Cmp`. Ordinary `+` on text concatenates; ordinary text ordering is lexical. ```arcana arcana "0.1"; import "std/math"; const budget = parseUnits("1.000000000000000001", 18); const minimum = basisPoints(budget, 9970); metric("minimum", formatUnits(minimum, 18)); metric("signed", i64Add(long("-9223372036854775808"), 1)); metric("aboveHalf", u256Cmp(minimum, parseUnits("0.5", 18)) > 0); ``` - **Inputs.** Exact integers accept decimal text; unsigned integers also accept `0x` hexadecimal text. Use quoted text or exact source fields for large values. In Arcana 0.1 integer arguments supplied as numbers must be finite safe integers. A literal beyond that range is refused at compile time; an unavailable or invalid dynamic operand produces `na`. All numeric literals and expressions are floats before this check; it validates the represented value, not the original decimal spelling. Even a fractional literal can round to an integer first. Text is required when every input decimal digit matters. - **Unsigned arithmetic.** `u256Add`, `u256Sub`, `u256Mul`, `u256Div`, `u256Mod`, `u256Min`, `u256Max` and `u256Cmp` are exact and checked. Division rounds down. `u256DivUp(a, b)` rounds up. - **Full precision products.** `u256MulDiv(a, b, denominator)` computes the complete 512-bit product before dividing, rounding down. `u256MulDivUp` rounds up. An intermediate product beyond uint256 is valid when the final answer fits. Division by zero or a final answer beyond uint256 is `na`. - **Signed long arithmetic.** `i64Add`, `i64Sub`, `i64Mul`, `i64Div`, `i64Mod`, `i64Neg`, `i64Abs`, `i64Min`, `i64Max` and `i64Cmp` are checked. Signed division truncates toward zero; the remainder has the dividend's sign. `i64Div("-9223372036854775808", -1)` overflows; its mathematical remainder is `"0"`. Negating or taking the absolute value of that minimum also overflows. No operation wraps or saturates. - **Units and formatting.** `u256Units(amount, decimals)` converts an unsigned decimal amount to base units without rounding. `u256Format(value, decimals)` produces exact whole-token decimal text, trimming trailing fractional zeros. Decimal places are whole numbers from 0 to 77. Excess nonzero fractional digits refuse; scientific notation in text refuses. A numeric amount uses its shortest decimal representation and cannot restore digits already lost in floating-point calculations. Use text for exact input. - **Display conversions.** `u256Number(value, decimals?)` and `i64Number(value)` explicitly round to a float for charts. Never route that result back into a monetary amount. `u256SafeNumber(value)` and `i64SafeNumber(value)` instead return `na` outside the safe integer range. - **Failure.** Invalid operands, overflow and division by zero produce `na`. An action with an `na` argument refuses before signing. No value is silently replaced with zero. `import "std/math";` adds `uint256`, `long`, `parseUnits`, `formatUnits`, `mulDiv`, `mulDivUp`, `basisPoints` and `basisPointsUp`. Basis-point functions divide the full product by 10,000; the `Up` form rounds up. `std/erc20`'s older `tokenUnits` helper uses ordinary floating-point math; use `parseUnits` for exact amounts. Historical Flowscript programs saved as 0.1 or 0.2 keep their earlier `u256` numeric operand semantics and function names. Arcana 0.1 includes the complete exact helpers introduced in historical Flowscript 0.3. Their artifacts record distinct exact operations; a historical artifact retains its original family, version and digest rather than being reinterpreted under a new input policy. The compiler publishes its own list: `arcana_core::vocabulary()` returns the built-ins, the prelude source and the diagnostic registry, which is what an editor or a code-writing model should read rather than a copy. ## Diagnostics Every compile error has a stable code (`FS0412`), a slug (`record.field`), a message, and a position. A code never changes meaning and is never reused, so tooling matches on it rather than on the message. The registry is `arcana_core::diagnostic_codes()`. ```arcana error uses { bars: "^1" } const close = bars.clsoe("BTC-USD", "1h"); ``` That is `stream.unknown`: "did you mean `bars.close`?". ### Warnings Some programs compile and then do something other than what they seem to say. `arcana_core::warnings_in(source, &host)` (`program.warnings` in `@arcana/core`) lists them, each located and coded like an error, and the program still compiles: the author may mean it. | Code | Slug | What it finds | |---|---|---| | `FS0901` | `lint.never` | a condition that is never true: `x > highest(x, n)` or `x < lowest(x, n)` (this bar is in the window), or one written of literals that is false | | `FS0902` | `lint.repeat` | a `while` that buys or opens a position with no position guard, so it sends the order at every bar its condition holds | | `FS0903` | `lint.unused` | a `const` nothing reads (name it `_x` to keep it) | | `FS0904` | `lint.na` | a divide or remainder by a literal 0, which is `na` at every bar | | `FS0905` | `lint.unit` | `bps(n)` under arithmetic or a comparison, where it is the number `n`, not the rate `n / 10000` | ```arcana warning uses { bars: "^1", paper: "^1" } const close = bars.close("BTC-USD", "1h"); when (close > highest(close, 20)) { paper.buy({ symbol: "BTC-USD", quote: 100 }); } ``` That is `lint.never`, at `close > highest(close, 20)`: compare with `highest(close, 20)[1]`. A replay says the rest: its `quiet` lists each `when` or `while` with actions that fired at no bar, with how its condition decided (`na` at every bar, never true, or true and held back by a cooldown, guard or phase). A strategy that never trades says why instead of returning no orders. ## Common pitfalls Each of these compiles, and each does something other than what it seems to say. They matter most in a strategy that trades. - **`while` repeats** (`FS0902`). `while (rsi(close, 14) < 30) { paper.buy(…) }` buys at every bar of a long dip. Entries belong in a `when`, which fires once per crossing; keep `while` for an action meant to repeat, with a guard that turns false once it has done its job. - **`when` fires again on every new crossing.** An entry that can turn true twice before its exit adds to the position each time. Gate it on `!inPosition()` where the host keeps a book, or cap entries in the broker (`pyramiding`). - **A window includes its own bar** (`FS0901`). `close > highest(close, 20)` is never true, since this bar's close is in the window. Compare with the window as of the bar before: `highest(close, 20)[1]`. - **`na` is not false.** `!(close > avg)` is `na`, not true, while `avg` warms up, so a `when` on it waits. Write the condition you mean (`close < avg`), and use `nz` or `isNa` where a missing value should count as something. - **A fold that does not test for `na` stays `na`.** `fold` carries its state from bar to bar, so one missing sample makes the state `na` for the rest of the run unless the step leaves the state as it was (`isNa(x) ? st : …`). The prelude's indicators all do this. - **Some windows are partial at first, some `na`.** `sma`, `stddev` and the other averaging windows average what they have until the window fills, while `highest`, `lowest` (`rolling_high`, `rolling_low`), `median` and `percentile` are `na` until it fills, as `ema`, `rsi` and `atr` are until they warm up. Gate on `barIndex(close) >= n` where the first bars must not trade, and `nz` or scale what a window has (`cummax(x) - cummin(x)` is the range since the start) where they must. - **Time needs the bar's time.** `every`, `cooldown`, `daily` and `during` count in the time each bar carries (`time` in a replay's history, the `time` of `step`). Without one a bar's time is its number, so `every("1d")` fires once and `cooldown(hours(1))` waits 3600 bars. Run times must be nondecreasing whole seconds from 0 through 9,007,199,254,740,991, the largest consecutively exact integer the numeric representation supports. Closed and forming bars refuse an invalid time before changing state. - **A watermark has one input budget.** All its sources together may supply at most 1,000,000 values and 64 MiB of logical allocation charges: 64 bytes per value and record entry, plus text/key bytes. Text and keys are limited to 4,096 UTF-8 bytes, nesting to 128 levels. Source keys are billed by their canonical stream/key identity, including when restored. Internally generated clock, position, phase and lifecycle views have a separate allowance of the same size, so they cannot make an admitted external observation impossible to restore. These admission budgets also apply alongside retained state and evaluation-work limits. - **A run has one retained-state allowance.** Folds, history, held reads, private caches, shared live state, the last input view and paper-book state share 256 MiB of logical charges. Each value and map/cell entry costs 64 bytes plus text and key bytes, even when allocations are shared. Shared handles and reservation entries also pay for their metadata, so total limit boundaries can differ by that overhead from a private materialization. Retained values may nest at most 32 levels. A resource fault after evaluation begins stops the run; recover explicitly from the last successful checkpoint. Optional shared memo/version/index caches have a separate 64 MiB allowance per shared runtime. Pressure can discard optional cache entries; it never discards a live fold or changes a decision. - **Checkpointing can refuse.** Encoded checkpoints are bounded to 128 MiB while writing. Rust `Run::snapshot()` returns `Result`; npm returns `{ ok, snapshot }` or `{ ok: false, error }`. Hosts must handle refusal before replacing a durable checkpoint. Restore checks encoded and expanded input limits and rebuilds live accounting from the actual values. - **An order is sized by a literal.** A paper broker sizes from a number (or a constant or `param` bound to one) and refuses a program with an order sized by anything computed before its first bar. Size by equity with the host's percent order where it has one, and sell a share of a position with its percent-of-position sell (a `{ position: "pct" }` size). ```arcana uses { bars: "^1", paper: "^1" } const close = bars.close("BTC-USD", "1h"); const top = highest(close, 20)[1]; const n = fold(close, 0, (st, x) => isNa(x) ? st : (x > top ? st + 1 : st)); metric("breakouts", n); when (barIndex(close) >= 20 && close > top && !inPosition()) { paper.buy({ symbol: "BTC-USD", quote: 100 }); } when (close < sma(close, 20)) { paper.sell({ symbol: "BTC-USD" }); } ``` ## Testing a program New tests can use native `.t.arcn` files with ordinary Arcana declarations, isolated named cases, source fixtures, and assertions written as expressions. The [testing reference](testing.md) specifies the syntax, CLI discovery, randomized inputs, invariants, failing-trial replay, source-bound suites, locations and limits. The YAML/JSON format below remains available through its explicit legacy parser. A test feeds a program sources you write and says what it should do with them. The same runner is in `arcana-core` (`testing::run`), `@arcana/core` (`arcana.test(source, suite, host)`) and ftw's CLI (`arcana test program.arcn program.test.yaml`). A check is compiled into the program as one more metric and replayed by the evaluator a backtest uses, so a passing test says what the real run does. `arcana.suite(text)` reads a suite's text into the object `test` takes, or says why it is not one, for an editor that keeps the cases rather than the text. A suite is JSON or YAML: an optional `program` (the source every case runs, when none is given beside it), an optional `paper` broker, and `tests`. Here is one for the strategy in [A first program](#a-first-program): ```yaml paper: cash: 1000 fee_bps: 10 orders: paper.buy: { side: buy, price: bars.close, size: { quote: quote } } paper.sell: { side: sell, price: bars.close, size: all } tests: - name: buys the rally, sells the fall bars: 60 ohlc: { steps: [[0, 100], [20, 120], [40, 90]] } paper: true expect: - { check: "fast > slow", at: 21 } - { fires: paper.buy, at: 20, args: { quote: 100 } } - { fills: paper.sell, at: 40 } - { check: "close > 0", at: always } - { warns: none } - name: a flat market never trades sources: { bars.close: { constant: 100 } } expect: - { fires: paper.buy, count: 0 } - { metric: fast, at: last, equals: 100 } ``` **A case** has a `name`, the `sources` it feeds and what it `expect`s. Bars are counted from 0. `bars` is how many (the longest written-out source, or 100). The replay limit of 1,000,000 bars is checked before generated sources allocate. `time` is a bar's length in seconds or a list of closes, for `every` and `cooldown`. `paper` is a broker's terms, or `true` for the suite's (or the host's). `program` gives a case its own source: a library's tests are cases whose programs import it. `runs: 50` runs a case 50 times, each `walk` with the next seed, so an expectation is held over many random markets rather than one, as a property test is (D1257). It passes only when every run does, and a failure names its run: `run 17 of 50: \`exposure <= 0.2\` is false at bar 31`. `runs` must be from 1 to 200. Bar times must fit whole unsigned 64-bit seconds; a generated time that would overflow is refused. For independent randomized inputs, set `fuzz: { trials: 100, seed: 42 }` instead of `runs`, and use bounded `random`/`integer` or explicit `choose` source generators. Every expectation must hold on each fresh trial. Failed cases report their seed and one-based `failedTrial`; adding `trial: 7` to the same `fuzz` record replays the original seventh trial directly. Seeds are unsigned safe integers. See the [randomized testing guide](testing.md#randomized-inputs-and-invariants) for distributions, unknown observations, complete examples and bounds. ```yaml - name: never more than a fifth of equity in, over any market bars: 200 runs: 50 ohlc: { walk: { from: 100, step: 2 } } paper: true expect: - { check: "exposure <= 0.2", at: always } ``` **A source** is named as the program reads it (`bars.close`, or `bars.close|BTC-USD` for one key of it) and is: | Written | Feeds | |---|---| | `[100, 101, null, 103]` | numbers; `null` is `na`; a short list holds its last value | | `[true, false]`, `["hot", "cold"]` | a bool or text series | | `[{ a: 1, b: "x" }, …]` | a record, one per bar | | `[[3, { price: 10, size: 2 }], …]` | a tape: each event at its bar | | `{ constant: 100 }` | the same number at every bar | | `{ ramp: [100, 120] }` | a straight line from the first bar to the last | | `{ sine: { mid: 100, amp: 5, period: 20, phase: 0 } }` | a wave | | `{ steps: [[0, 100], [20, 120]] }` | each value from its bar on | | `{ walk: { from: 100, step: 1, seed: 7 } }` | a random walk; a seed walks one path everywhere | Every record row must supply the same fields. Use an explicit `null` for a numeric field that is `na`; leaving a field out is refused, so a later observation cannot shift into an earlier bar. Any generator takes `na: [bar, …]` to blank those bars. `ohlc: ` feeds `bars.close` and derives `bars.open` (the close before), `bars.high`, `bars.low` and `bars.volume` (1000) from it; `ohlc: { source: pool, close: … }` does the same for another source's bars. A read the host adds for an action and the program never names (an order's live price, its pool) is fed `na` when a case leaves it out, and the report lists it under `unfed`. A read the program names is the case's to give. **An expectation** is one of: | Written | Holds when | |---|---| | `{ check: "", at: 12 }` | the condition is true at bar 12 | | `{ check: …, at: ever \| always \| never \| last }` | at some bar; at every bar it is decided, and at least one; at none; at the last (`ever` without `at`). `from` and `to` bound the bars | | `{ metric: fast, at: 12, equals: 101.5, within: 0.01 }` | a metric's value (within 1e-9 relative unless `within`); `at: last` | | `{ metric: fast, at: 3, na: true }` | a metric is `na` there | | `{ fires: paper.buy, at: 20, count: 1, args: { quote: 100 } }` | an action (`paper.buy`, `buy`, or its site) fired: at a bar, that many times (`count: 0` is never), with those arguments. A contract call's arguments are compared as they were at the bar it fired, `{ amount: 250000000 }` for `tokenUnits(250, 6)`; a record or list is compared field by field | | `{ fills: paper.buy, at: 20, count: 1 }`, `{ reverts: … }` | the paper broker filled, or reverted, that action's orders | | `{ paused: paper.buy, at: 15, by: drawdown }` | the invariant cage refused that action's entry, by that invariant when `by` is given; `count: 0` is never | | `{ refuses: FS0103, line: 3 }` | the program is refused with that code or slug, on that line | | `{ warns: FS0901 }`, `{ warns: none }` | the compiler warns with that code or slug; or not at all | A check reads the program's `const`s and sources, not its metric names. Every case but a `refuses` one must compile. Checks must produce numeric or boolean values. Their `from` and `to` range must lie within the replay, with `from` at or before `to`; an empty range cannot make a `never` check pass. A metric expectation supplies exactly one of `equals` or `na: true`. `within` is an absolute, finite tolerance at or above zero and requires `equals`. **The report** gives each case's `failures`, each saying what happened instead (`` `paper.buy` fired 0 times, not 1 ``, `` `fast > slow` is false at bar 21 ``), or why the case could not run (`expect: null`). The CLI exits 1 when one fails. A `fires … at` that fails also says what the action's `when` read at that bar: each comparison in its condition and the values it compared (D1255). ```text `paper.buy` did not fire at bar 3; it fired at 7. `fast > slow && close < 20` at bar 3: `fast > slow` = true, `fast` = 18.5, `slow` = 17.2, `close < 20` = false, `close` = 25 ``` ## What runs where `arcana-core` and `@arcana/core` compile any program in this reference, and replay or run all of it except the parts each says it does not model: - **Replay** (`replay`, `arcana.replay`) evaluates a whole history at once. With a paper broker it fills the orders and runs their handlers. Give it each bar's close as `time` (`replay_timed` in Rust) for `every` and `cooldown` to count in chain time. A metric or trigger that reads only number series is computed for every bar at once by the compiled study plan, and the rest is stepped bar by bar; the answers are the same to the bit, so a long backtest costs about what a chart study does (D1270). - **Live run** (`live::Run`, `arcana.run`) steps one bar at a time and makes the replay's decisions on the same bars. Outcomes come from a paper broker or from the application (`report`). - **Not modelled here yet**: `maintain` blocks are listed in the result's `unmodelled` with the reason, never silently dropped. So are invariants in a run with no paper broker. Some of the language is for a full execution host such as ftw's and needs data this library does not supply on its own: position context (`inPosition()`, `avgEntry()`, `unrealizedPnl()` …) reads the strategy's own execution stream, `param(…)` and `config(…)` read a configuration stream, and `pool(…)`, `token(…)` and `market(…)` name an application's venues. ## Parameters and search spaces `const lookback = param(7, { min: 3, max: 30, step: 1 });` declares a parameter: its value is the default until a run's settings change it. The bounds say what may change it and what an optimizer searches: - `min` and `max` bound an edit, and the default sits between them. - `step`, with both, makes min to max by step the search space (at most 1000 values). - `options: [...]` lists the values instead, up to 64 of the default's type, the default among them. It is not combined with min, max or step. The compiler refuses any other bound (`param.bounds`, `param.step`, `param.options`). A host that runs a strategy may bind each declared value in place as a constant before compiling, as ftw.trade does, so a parameter can size a window (`close[lookback]`); read as a configuration stream it can't. On ftw.trade the declarations are the strategy's parameters, and an optimization with no space named searches the declared steps and options. ## Coming from Pine Script Most Pine indicators port line for line, and an importer does the porting (D1194): `pineToArcana` in `@ftw-trade/charts/pine` translates the common v5/v6 subset of an indicator to a chart study and of a strategy to a ftw.trade strategy, and names, by Pine line, what it could not take (`while`, a `for` it cannot port, `request.security` of another symbol, `:=` on a name that is not a `var`). A `for i = 0 to len - 1` that adds, or takes the max or min of, one series' `x[i]` becomes `rolling_sum`, `highest` or `lowest`; a `for` over constant bounds (at most 64 steps) that only accumulates is unrolled. A `var` and its `:=`, `+=` … updates, under `if`s too, become one `fold` that applies them in order each bar: `var int n = 0` with `n := up ? n + 1 : 0` is `const n = fold(close, 0, (n, _) => nz(up, false) ? n + 1 : 0);`. The charts site's Arcana studio and ftw.trade's Strategies page take a pasted script, and an agent calls `get_pine_translation` over MCP. The differences are deliberate: - Data is read by name, so a program says which market and interval it reads. - Every run is replayable, so there are no loops and no mutation. - Orders are a host's capabilities, not a built-in simulator. ```arcana uses { bars: "^1", alerts: "^1" } const close = bars.close("BTC-USD", "1h"); const high = bars.high("BTC-USD", "1h"); const low = bars.low("BTC-USD", "1h"); const k = sma(stoch(close, high, low, 14), 3); const d = sma(k, 3); metric("k", k); metric("d", d); metric("bars_since_oversold", barssince(k < 20)); when (crossover(k, d) && k < 20) { alerts.send("stochastic turned up from oversold"); } ``` | Pine | Arcana | |---|---| | `close`, `high`, `close[1]` | `bars.close("BTC-USD", "1h")`, read once into a `const`; history is the same `x[1]` | | `ta.sma`, `ta.ema`, `ta.wma`, `ta.rma`, `ta.rsi`, `ta.stdev`, `ta.vwap`, `ta.roc`, `ta.mom`, `ta.correlation` | `sma`, `ema`, `wma`, `rma`, `rsi`, `stddev`, `vwap(price, vol, len)` (a rolling window; anchored, `anchoredVwap(src, vol, anchor)` or a session's `periodVwap`), `roc`, `momentum`, `corr` | | `ta.atr(14)`, `ta.tr` | `wilderAtr(high, low, close, 14)`, `trueRange(high, low, close)` | | `ta.highest`, `ta.lowest`, `math.sum`, `ta.cum` | `highest`, `lowest`, `rolling_sum`, `cum` | | `ta.stoch`, `ta.linreg(src, len, 0)` | `stoch`, `linreg(src, len)` | | `ta.barssince`, `ta.valuewhen(c, src, 0)` | `barssince`, `valuewhen(c, src)` | | `ta.pivothigh(src, l, r)`, `ta.pivotlow` | `pivothigh(src, l, r)`, `pivotlow` | | `[m, s, h] = ta.macd(src, 12, 26, 9)` | `macd(src, 12, 26)`, `macdSignal(src, 12, 26, 9)`, `macdHist(src, 12, 26, 9)`: one function per line | | `[mid, up, lo] = ta.bb(src, 20, 2)`, `ta.variance` | `sma(src, 20)`, `bbUpper(src, 20, 2)`, `bbLower(src, 20, 2)`, `variance` | | `[p, m, adx] = ta.dmi(14, 14)` | `diPlus(high, low, close, 14)`, `diMinus(…)`, `adx(high, low, close, 14, 14)` | | `[st, dir] = ta.supertrend(3, 10)`, `ta.sar(0.02, 0.02, 0.2)` | `supertrend(high, low, close, 3, 10)`, `supertrendDir(…)`, `sar(high, low, close, 0.02, 0.02, 0.2)` | | `ta.vwma`, `ta.swma`, `ta.wpr`, `ta.cmo`, `ta.mfi`, `ta.obv`, `ta.tsi` | the same names; a read Pine makes itself (the bars, the volume) is an argument | | `ta.hma`, `ta.alma`, `ta.cci`, `ta.dev`, `ta.percentrank`, `ta.highestbars`, `ta.lowestbars`, `ta.cog`, `ta.range`, `ta.cross` | the same names | | `[mid, up, lo] = ta.kc(src, 20, 2)`, `ta.kcw`, `ta.bbw` | `ema(src, 20)`, `kcUpper(src, high, low, close, 20, 2)`, `kcLower(…)`, `kcw(…)`, `bbw(src, 20, 2)` | | `ta.pvt`, `ta.accdist`, `ta.wad`, `ta.nvi`, `ta.pvi`, `ta.iii`, `ta.wvad` | the same names, with the bars and the volume as arguments | | `ta.crossover`, `ta.crossunder`, `ta.change` | `crossover`, `crossunder`, `change` | | `math.abs`, `round`, `floor`, `ceil`, `sign`, `log`, `log10`, `pow`, `sqrt`, `sin` … | the same names without `math.`; `log` is `ln` | | `na(x)`, `nz(x, y)` | `isNa(x)`, `nz(x, y)` | | `var float x = 0` then `x := x + 1` | `fold(series, seed, (acc, x) => next)` | | `import user/lib/1 as lib`, `export f(x) => …` | a library of `fn` definitions and `import "lib";`, its functions called by name | | `plot(x, "name")` | `metric("name", x)`; in a chart study, `// @plot name histogram #26a69a` styles it | | `plot(x, color = x > 0 ? green : red)` | `// @plot x up=green down=red`, or `by=other` to colour by another metric's sign | | `plotshape`, `barcolor`, `bgcolor`, `display = display.none` | the `@plot` styles `up` and `down`, `bars`, `bg` and `hidden` | | `plotshape(c, location = location.belowbar, text = "B")` | `// @plot c up below text="B"` (`above` for over the bar), with `metric("c", cond ? 1 : 0)` | | `if c` … `label.new(bar_index, high, "H " + str.tostring(x))` | `// @plot l down above text="H {{x}}"` with `metric("l", c ? 1 : 0)` and `x` published as a metric (D1213) | | `line.new` from pivot to pivot (a zigzag) | `// @plot p line connect`, with `metric("p", …)` set only at the pivots and `na` between | | `box.new`, `table.new` | a box is a `// @fill` between two lines; a table's values read in the legend | | `fill(p1, p2, color)`, `hline(70)` | `// @fill upper lower #2962ff`, `// @hline 30 70` | | `input.int(14)` | a top-level `const len = 14;` in a chart study, which its settings edit; `param(14, { min: 2, max: 50 })` in a strategy | | `input.int(14, "Length", minval = 2, group = "MA", tooltip = "…")` | `// @input len "Length" min=2 group="MA" tooltip="…"` over the `const` | | `input.bool(true, "Show")`, `input.string("EMA", options = ["SMA", "EMA"])` | `// @input show bool "Show"` over `const show = 1;`; `// @input kind options="SMA,EMA"` over `const kind = 1;` (the picked option's index) | | `hour`, `minute`, `dayofweek`, `dayofmonth`, `month`, `year`, `bar_index` | `hour(time)` … `year(time)` in a chart study (UTC), `barIndex(price)` | | `time(timeframe.period, "0930-1600:23456", "America/New_York")`, `timezone`, `session.isfirstbar` | `inWeekdaySession(nyTime(time), 930, 1600)`; `hour(nyTime(time))`; `sessionStarts(active)` | | `alertcondition(c)`, `alert(msg)` | `when (c) { alerts.send(msg); }`; ftw's host names it `notify(msg)` | | `alertcondition(c, message = "RSI {{plot("rsi")}} at {{close}}")` | in a chart study, `// @alert c "RSI {{rsi}} at {{close}}"`: `{{value}}`, the bar's `{{open}}` … `{{volume}}`, `{{time}}` and any line by name | | `strategy.entry`, `strategy.close` | the host's order actions in a `when` body, such as ftw's `orders.marketBuy` and `orders.sell` | | `strategy.entry(limit=, stop=)`, `strategy.exit(stop=, limit=)` at a price | a broker rule with `rest` (D1212), such as ftw.trade's `paper.buyLimit({ quote, at })` and `paper.sellStop({ at })`: it rests at `at`, a number or an expression the program also publishes with `metric()`, and fills at the first later close through it | | `request.security(syminfo.tickerid, "D", ta.sma(close, 20))` | `sample(periodEnds(time, 86400), close, c => sma(c, 20))`: the closed day's value, known on the bar that closes it and held, so it never repaints | | `request.security(syminfo.tickerid, "D", high - low)` | `sample(periodEnds(time, 86400), {h: periodHigh(high, time, 86400), l: periodLow(low, time, 86400)}, b => b.h - b.l)` | | `request.security(sym, "1D", close)` of another market | a read at that market and interval, `bars.close("BTC-USD", "1d")`, where the host publishes it | | `for`, arrays and maps | not in the language: a window function, `fold`, or `percentile` and `median` over a window | --- # Embed Arcana > Use the compiler and runtime in your own application: declare a host, replay data, step a live run and retain checkpoints. [Canonical documentation](https://arcn.ftw.trade/docs/embedding) An application embeds Arcana by declaring a host, compiling a program against it and supplying the observations that program reads. The Rust library is `arcana-core`; `@arcana/core` exposes the compiler and evaluator to JavaScript and TypeScript through WebAssembly. The JavaScript package has no runtime dependencies and does not depend on ftw.trade. ## Install and load There is currently no public package release or public download of `@arcana/core`, and it is not published to the npm registry. Embedding it locally requires authorized access to the source repository. These docs describe the library that exists in that checkout; they do not offer a public installation URL. With Node.js 22.18 or newer, npm and Rust with the `wasm32-unknown-unknown` target installed, run this from `arcana/npm`. A copy of `arcana/` alone is sufficient; the root application and `Justfile` are not required: ```sh npm ci npm test npm pack ``` These commands install development dependencies, build the JavaScript and WebAssembly, run the tests and pack the result. At the current package version, the output is `arcana-core-0.1.0.tgz` in that directory; the version in `package.json` determines the filename. Packing does not publish the artifact. Install the resulting tarball from your application directory. Replace the example checkout path with your actual source location: ```sh npm install /path/to/authorized-checkout/arcana/npm/arcana-core-0.1.0.tgz ``` In Node, `await loadArcana()` loads the WebAssembly shipped beside that installed package. In a browser it fetches that file. A bundled application must serve the file itself and pass its bytes or a compiled `WebAssembly.Module` to `loadArcana`. To use local files or pipe your own source adapter into a retained run without writing an embedding service, follow [Run Arcana locally](https://arcn.ftw.trade/docs/local). The packaged CLI accepts the same host declaration as the API and returns evaluated outputs and unsent action intents. Compiler and evaluator calls are synchronous. Run them in a Web Worker when embedding in a browser so evaluation does not block the interface. ## Declare your host The host determines what a program may read and call: - **Sources** declare names, value types, key shapes and available intervals. The application supplies the actual samples. - **Capabilities** are manifests declaring action names and arguments. Declaring an action does not implement or authorize an external transaction. - **Libraries** supply named source imports. **Integrations** describe which implementations serve particular interfaces and markets. An undeclared read or action is a compiler refusal. A host can declare sources without any capabilities, as the [quickstart](https://arcn.ftw.trade/docs/quickstart) does. Store the program source, its host and library configuration, and its compiled language version together. A program can pin its version with `arcana "0.1";`; a host can record a version for source that omits that line. Restoring a checkpoint requires the same compiled program identity. Use `arcana.resolveLibraries(source, host)` to obtain and persist the exact transitive dependency sources, including reached standard libraries. The [extension guide](https://arcn.ftw.trade/docs/extending) shows how to compose and test user libraries, implement custom tool consumers and record external model predictions. ## Replay and checkpoint `compile` returns a program or located problems. `replay` takes a program and a history with `len` rows, input columns by source name, and optional evaluation timestamps in seconds. It returns per-bar metrics and decisions, or an error. Inspect `unmodelled` too: a successful result can identify behavior the selected evaluation configuration did not model. For batches, `{ save: true }` returns a checkpoint in `snapshot`. Pass that text as `{ resume: snapshot }` with only the observations after it. This complete example uses illustrated hourly inputs and no actions: ```js import { loadArcana } from '@arcana/core'; const arcana = await loadArcana(); const host = { sources: [ { name: 'bars.close', type: 'number', keyed: true, intervals: ['1h'] }, ], capabilities: [], }; const source = `arcana "0.1"; uses { bars: "^1" } const close = bars.close("DEMO-USD", "1h"); metric("average", sma(close, 3)); `; const compiled = arcana.compile(source, host); if (!compiled.ok) throw new Error(JSON.stringify(compiled.problems)); try { const firstClose = Date.UTC(2026, 0, 1, 1) / 1000; const first = arcana.replay(compiled.program, { len: 3, series: { 'bars.close': new Float64Array([100, 102, 104]) }, time: new Float64Array([firstClose, firstClose + 3600, firstClose + 7200]), }, { save: true }); if (!first.ok) throw new Error(first.error); if (first.unmodelled.length) throw new Error(first.unmodelled.join('; ')); if (typeof first.snapshot !== 'string') throw new Error('No checkpoint returned'); // Your application persists this text with its input cursor and configuration. const saved = first.snapshot; const next = arcana.replay(compiled.program, { len: 2, series: { 'bars.close': new Float64Array([106, 108]) }, time: new Float64Array([firstClose + 10800, firstClose + 14400]), }, { resume: saved, save: true }); if (!next.ok) throw new Error(next.error); if (next.unmodelled.length) throw new Error(next.unmodelled.join('; ')); if (typeof next.snapshot !== 'string') throw new Error('No checkpoint returned'); console.log(Array.from(next.metrics.average)); // [104, 106] } finally { arcana.drop(compiled.program); } ``` Persist a new checkpoint and the cursor naming its consumed inputs as one application-level commit. Arcana produces checkpoint text; it does not provide a database, subscription cursor or transaction around your storage. For one bar at a time, `arcana.run(program)` creates a run. `run.step(values, time)` consumes one completed bar; `run.snapshot()` returns a result containing checkpoint text. Restore with `arcana.run(program, { snapshot })`, check each result and call `run.drop()` when finished. Supply the same paper-broker configuration when restoring a run that used one. Call `arcana.drop(program)` after its runs have been dropped. Each successful step returns structured metrics, text and decisions for your application to consume. [Outputs and consumers](https://arcn.ftw.trade/docs/outputs) shows a complete adapter that builds chart rows, unsent action records and application logs from those real results. ## Preserve the boundary The application owns data collection, bar construction, units, ordering, freshness and durable input cursors. An omitted observation in `run.step` holds its prior value; `null` explicitly supplies an unknown observation. Choose that behavior deliberately for your feed. An action decision records what the program requested. Sending it to a venue, protecting signing keys, enforcing trading permissions and reconciling the actual outcome are application responsibilities. The optional paper broker models explicitly configured orders; it is not a venue depth or partial-fill simulator. Runtime refusals and `unmodelled` behavior must reach the caller. Do not replace them with fabricated values or silently restart evaluation. A WebAssembly trap invalidates existing program and run handles; compile explicitly and restore a validated checkpoint before continuing. Checkpoint identity mismatches are refused. Read the [language reference](https://arcn.ftw.trade/docs/reference) for the complete host contract, runtime semantics and diagnostics. For ftw.trade's own host, modules, accounts and execution workflow, use [its platform guide](https://docs.ftw.trade/strategies/overview). --- # Build libraries and tools > Compose and test your own libraries, declare custom data and actions, and feed recorded external model predictions into an incremental strategy. [Canonical documentation](https://arcn.ftw.trade/docs/extending) The standalone compiler takes a host declaration; it has no dependency on ftw.trade. An application chooses the reads, libraries and actions its programs can use. These extension paths have different responsibilities: | Extension | Declare | Execute | |---|---|---| | Pure language helper | `Host.libraries[name]`, then `import "name";` | Arcana evaluates the imported `.arcn` functions | | Data or external model output | A typed entry in `Host.sources` | The application records observations and supplies them to `run.step` / `replay` | | Application tool | A capability manifest with arguments and host authority | The application authorizes and consumes the returned decisions | | Several providers of an interface | `Host.integrations` and their served members/markets | The application connects the declared runtime implementations | Imports provide source functions, not JavaScript callbacks or installed tools. A capability manifest describes an action; declaring it does not implement its consumer, grant signing authority, or authorize network access. Unknown imports, reads and actions are compiler refusals. Use host documentation for source and action descriptions; comments document imported functions for editor help. ## Author and compose libraries Supply your `.arcn` source under a name in `Host.libraries`. A library may import other libraries and declare functions or contract interfaces. It cannot declare `uses`, metrics, triggers or action side effects; the calling program owns its host capabilities. See [Libraries](https://arcn.ftw.trade/docs/reference#libraries) and [Embedding](https://arcn.ftw.trade/docs/embedding). Dependencies form a closure of at most 15 unique libraries, including reached standard libraries. Shared dependencies in a diamond are loaded once. Repeating an import in the same file, an import cycle, an unknown dependency or a duplicate function name is refused. Function names are global and unqualified: `import "user/risk";` exposes `userCappedEdge(...)`, not `risk.userCappedEdge(...)`. Choose distinct prefixes for reusable helpers. On ftw.trade, take observations as function arguments rather than hiding source reads in a helper, so the platform can discover the caller's data subscriptions. Test a library through a small importing program. Resolve and save the exact dependency sources with the program and its compiled language version: ```js import { loadArcana } from '@arcana/core'; const fs = await loadArcana(); const host = { libraries: { 'user/math': 'fn userPositiveEdge(x: number): number { return max(x, 0); }', 'user/risk': `import "user/math"; fn userCappedEdge(x: number, cap: number): number { return min(userPositiveEdge(x), cap); }`, } }; const source = 'import "user/risk"; metric("edge", userCappedEdge(3, 2));'; const resolved = fs.resolveLibraries(source, host); if (!resolved.ok) throw new Error(JSON.stringify(resolved.problems)); const pinnedHost = { ...host, libraries: resolved.libraries }; const compiled = fs.compile(source, pinnedHost); if (!compiled.ok) throw new Error(JSON.stringify(compiled.problems)); try { const tested = fs.test(source, { tests: [{ name: 'cap an imported positive edge', bars: 1, sources: {}, expect: [{ metric: 'edge', at: 'last', equals: 2 }], }] }, pinnedHost); if (!tested.ok) throw new Error(tested.error); if (tested.report.failed) throw new Error(JSON.stringify(tested.report)); const saved = { source, language: compiled.program.language, libraries: resolved.libraries }; // Persist saved with the rest of your host configuration and input cursor. } finally { fs.drop(compiled.program); } ``` `resolveLibraries` returns the selected source map without typechecking the strategy against its data host. Compilation and the test still have to pass. When restoring, use the saved map and language, not the current contents of a library registry. The closure includes the exact standard-library copies used by the program. Changed dependency implementations change program identity and cannot silently reuse an incompatible checkpoint. ## Built-in learning and external inference Built-in `ml.*` models fit on the program's recorded series in the evaluator. Their training rows, parameters and work participate in its state and budgets; replay and restored live runs use the same implementation. See [Trained models](https://arcn.ftw.trade/docs/reference#trained-models) for available methods, horizon and retraining semantics. They are bounded rolling models, not an import mechanism for arbitrary Python, ONNX, GPU or JavaScript runtimes. An external model runs in the application's chosen backend. Publish its output as a typed observation, just as another data source. The runnable [external-model.ts](https://github.com/johnny-emp/flywheel/blob/main/arcana/npm/examples/external-model.ts) example combines that path with an imported freshness helper and a custom `research.record` action: ```sh cd arcana/npm npm run build:wasm node examples/external-model.ts ``` The example has no additional dependencies. Its fixed logistic coefficients are illustrative and are not presented as a trained model, profitable strategy or production inference engine. The model document owns feature order, bounds, coefficients and intercept; its SHA-256 identity appears in the program's read key and every recorded output. Inference reads that same document. Replacing the document changes the compiled program identity; an old checkpoint cannot silently resume under the new model. The application computes a score, records `{time, asOf, model, score}`, then steps the program. `time` is when the prediction became available to the strategy; `asOf` is the input data's time. A response delivered at 110 cannot be inserted into bar 109 merely because its features came from 109. All times in this example are whole evaluation seconds. A future `asOf`, nonfinite or out-of-range probability, wrong model identity, unsupported feature dimension or feature outside the declared bounds is refused. Unavailable inference is recorded explicitly as `score: null`. The strategy checks the model identity and uses the `.arcn` helper to exclude unknown, stale, future and out-of-range observations before emitting a signal. Omission in `run.step` holds the previous observation, so an outage must send explicit unknown values when holding the old prediction is inappropriate. A freshness guard also limits how long an earlier valid prediction can remain usable. `research.record` has a real consumer: it validates the compiled action's host operation and evaluated arguments before appending candidate signals to an application-owned journal. It authorizes no trade. The example validates an entire batch before retaining effects; it does not parse rendered argument IR or report fabricated fills. A real application's storage, effect outbox, permissions, retries and action settlement belong at this boundary. ## Reproduce the observations, not the inference request Replay the recorded predictions at their original availability watermarks. Do not call a remote model again during replay, substitute a newer model, or backfill historical predictions using features or training data unavailable then. Record model artifact identity, backend/precision configuration, feature schema and input cursor with the inference outputs in a real integration. The example pins a small fixed model; its test history is explicitly synthetic. Persist the input cursor, observations, evaluator checkpoint and durable effect outbox consistently. An Arcana checkpoint contains evaluator state; it does not preserve an external model server, in-progress request or model-training state. Asynchronous inference needs the host's own correlated request/result coordinator, recorded delivery and timeout policy. The generic `Run.report` venue-outcome seam is not a task-result dispatcher. The [example tests](https://github.com/johnny-emp/flywheel/blob/main/arcana/npm/test/external-model.test.ts) exercise actual WebAssembly compilation, replay, live steps, checkpoint restoration, delayed availability, input bounds and the action consumer. There are no latency claims: measure the chosen inference backend, transport and evaluation workload together before selecting a deployment mode. --- # Dependent actions and recovery > Compose primitive actions on confirmed settlement, inspect captured inputs, handle each failed leg and retain uncertain submissions through recovery. [Canonical documentation](https://arcn.ftw.trade/docs/action-recovery) Put a dependent action inside its predecessor's `.onFill`, and give each forward and recovery action its own failure handlers. A group of actions is not an atomic transaction. A later failure does not undo an earlier fill. This guide describes an external application embedding Arcana with `requestRun`. The standalone CLI emits unsent intents and does not execute these requests. ftw.trade's protocol followers have their own execution and reconciliation paths; compiling this syntax does not install a correlated settlement executor in that product. ftw.trade's native protocol and spot paper books execute these continuations at the modeled fill or refusal. Its multi-book paper runner supports the native onchain protocol actions and market spot actions (`paper.buy`, `buyPct`, `sell`, `sellPct`). Other book and service actions with outcome handlers are refused at strategy load because that runner does not report their settlement. A standalone host or paper broker has its own declared consumers and support boundary. Protocol account/source reads in a same-bar paper continuation still reflect the bar's recorded observation. Use the current `fill` and `outcome.args` for immediate continuation inputs; do not assume an earlier action refreshed an account source. A spot outcome chain that fires during warm-up is refused: replay the actual entry with `startBar: 0` instead of replacing an earlier position with a synthetic fill. Live native protocol outcome chains are refused until the hosted follower can deliver correlated chain-settlement evidence. External hosts can use `requestRun` today. ## Choose the actual primitive actions On ftw.trade, `get_module_help { id: "aerodrome" }` lists every advertised native read/action and imported helper. Use `member` to inspect one method's argument names, units, required alternatives, scope and docstring. The same information drives editor hover and signature help. There is no runtime `aerodrome.help()` method. Read the protocol skill for custody, supported markets, pending states and which observation permits the next step. In another host, inspect that application's capability manifests and registered consumers. A manifest declares an interface; it does not submit transactions or authorize spending. Public Rust functions are not automatically Arcana methods. Importing a helper library does not register additional native actions. Keep user-composable primitives available when publishing a convenience wrapper: the individual swap, approval, supply, withdrawal, stake and claim operations, with their distinct units and settlement boundaries. A wrapper's internal transactions may partially succeed. Preserve each submitted transaction/request identity and confirmed result if a later internal step fails; a thrown exception alone does not establish that nothing happened. The hosted follower persists structured interruption receipts and checks known pending hashes. It stops for reconciliation after a partial or uncertain batch; it has no automatic rollback or resume command. This is not a durable pre-submission journal: a process crash between a send and checkpoint persistence still requires transaction/account reconciliation. Its interruption hold covers that run's books and market changes. It is not a shared account lease across independent runs using the same wallet. ## Sequence on settlement These statements request two operations; the second has no success dependency on the first: ```text await venue.first({ amount: 100 }); await venue.second({ amount: 100 }); ``` `await` controls strategy pausing at the execution coordinator. It is not an imperative promise binding between adjacent statements. Nest the dependent operation instead: ```text await venue.first({ amount: 100 }, timeout: "30s") .onFill { await venue.second({ amount: fill.amountOut }, timeout: "30s") .onFill { audit.note({ message: "completed" }); } .onRevert { await venue.release({ amount: outcome.args.amount }, timeout: "30s") .onFill { audit.note({ message: "recovered" }); } .onRevert { audit.note({ message: "recovery failed: " + outcome.reason }); } .onAbort { audit.note({ message: "recovery refused: " + outcome.reason }); } .onTimeout { audit.note({ message: "recovery still pending" }); }; } .onAbort { audit.note({ message: "second refused: " + outcome.reason }); } .onTimeout { audit.note({ message: "second still pending" }); }; } .onRevert { audit.note({ message: "first failed: " + outcome.reason }); } .onAbort { audit.note({ message: "first refused: " + outcome.reason }); } .onTimeout { audit.note({ message: "first still pending" }); }; ``` The names above are illustrative host capabilities. The complete [runnable example](https://github.com/johnny-emp/flywheel/tree/main/arcana/examples/action-recovery) declares its own host and covers three forward legs with recovery for both reverts and aborts. It uses invented outcomes in an isolated scenario, not recorded or live protocol evidence. The host must supply the authoritative output amount before advancing a branch that spends `fill.amountOut`. It must also check token identity, custody, fees, outstanding debt, liquidity and current authorization. A number alone does not establish that two protocols can share that asset. Size an unwind from confirmed positions and the operation's contract; do not withdraw an entire shared account merely because one strategy leg failed. For example, an Aave supply's moved underlying units need not equal the immediately withdrawable aToken balance because scaled balances round. Read the protocol's available balance and sizing contract; `all: true` is appropriate only when withdrawing that whole position is the intended policy. A reported amount is not a universal spendable claim across actions. ## What a handler can inspect | Read | Meaning | | --- | --- | | `fill.amountOut`, `fill.price` | Authoritative evidence supplied for this action's fill. Available in `.onFill`; these describe the operation being handled, not an enclosing action. | | `outcome.reason` | The host's reason for this action's reported event. | | `outcome.args.amount` | The evaluated original `amount` of this action, captured when its decision fired. It is not recalculated from current inputs and is not the output of the failed action. | | Ordinary declared reads | The observations at the watermark handling the event. They may differ from the observations that triggered the action. | `outcome.args` has this action's captured evaluated inputs. A capability's manifest selects these with `evaluated`; contract parameters and built-in evaluated fields also appear in `Decision.values`. Inspect the compiled action's `typed_args` for the exact names and `program.outcomeArgsSites` for the sites whose handlers consume them. Static unevaluated arguments are not available, and unknown fields are compile errors. A nested action's handler has its own outcome scope; an enclosing `fill`, task `result` or `progress` is not an implicit capture. Explicitly pass a value into the child's declared argument when it needs to remain available for the child's recovery. Exact argument types still apply; a `Number` does not become an exact token-unit integer. With `requestRun`, the coordinator takes these arguments from the original request's saved `Decision.values`. `record` refuses venue-supplied `args`: venue evidence cannot replace what the strategy originally requested. The journal retains those captured values across restart. A raw `Run.report` host must pass the matching original evaluated values in `Outcome.args` when a matching handler reads them, and must establish the action/invocation identity itself. Missing or incorrectly typed captured arguments are refused. The site-only raw reporting API is not a durable request coordinator. ## Distinguish failure from an unresolved operation | Observation | Permitted next step | | --- | --- | | Confirmed `fill` | Advance its `.onFill` branch using reconciled outputs and current guards. | | Confirmed terminal `revert` | Run `.onRevert`. Determine which earlier legs still hold assets before choosing compensation. | | Pre-submission `abort` | Run `.onAbort`; no transaction for that primitive was submitted. Earlier filled legs still exist. | | Wait timeout | Run `.onTimeout` for the wait ending. Keep the request and its resources reserved; do not compensate or replace an unresolved order. | | Lost acknowledgement, keeper silence or uncertain send | Reconcile the original saved submission identity. A missing answer is not a revert or abort. | | Partial effects inside a multi-transaction wrapper | Preserve every known transaction/result and reconcile the remaining state. Do not collapse the wrapper into an all-or-nothing failure. | `requestRun.record` accepts terminal `fill`, `revert` and `abort`. Its bounded wait emits `wait_timeout` internally. The lower-level language also has `.onStuck`, but `requestRun` does not accept a stuck observation as a terminal result. A stuck or timed-out request can still settle later. An upstream cancel is a separate operation whose acknowledgement is not proof of a refund; GMX's skill describes its keeper-settled example. Compensation is an explicitly requested operation that can fail, consume gas, return a different amount or wait on settlement. It is not rollback. Choose the policy yourself: unwind confirmed inventory, keep it and notify an operator, or stop until a reviewed recovery is authorized. Arcana does not automatically retry failed legs or choose replacement trades. ## Own submission and recovery in the host Use `requestRun` for the entire action set participating in a chain. Its default `concurrency: "serial"` admits one unresolved managed action in that run, including after a wait timeout. Sibling action statements still do not become success-dependent. Use separate success handlers for dependencies. For each `prepared` entry, `submitRequest` exposes three application hooks: build the exact envelope, atomically save its journal and submission identity, and send it through the authorized executor. It saves uncertainty before IO and never retries a failed save or send. On restart restore the whole coordinator snapshot, reconcile uncertain/submitted entries, and deliver terminal evidence by invocation ID. An acknowledgement is not a fill. Persist incoming evidence, its cursor and coordinator progress consistently. Use single-writer fencing, grants, account/position exclusion and an exact submission identity at the host boundary. A run's serial policy does not exclude a different run from touching the same position. Actions outside the managed action list are returned in `own`; the application must consume them, including audit/notification decisions and their delivery failures. Arcana libraries currently contain pure helpers and contract interfaces, not effectful functions containing actions and recovery handlers. Users can write the explicit strategy chain, import pure calculations, or package a custom JavaScript/TypeScript host coordinator that consumes the public primitive actions. The [extension guide](https://arcn.ftw.trade/docs/extending) describes that boundary. ## Test every exit from the chain The example's npm lifecycle tests exercise success, terminal revert and pre-submission abort at every forward leg, failed compensation, snapshot restoration with captured arguments, duplicate old outcomes, and timeout followed by late fill or revert. Add a failure after every internal transaction of a real wrapper, including a failed persistence step or lost acknowledgement. Verify that confirmed partial effects remain visible and uncertain submissions cannot be sent again. Use `.t.arcn` for input-driven strategy decisions, invariants and explicitly configured paper behavior. Its runner currently cannot inject external keeper settlement or restore `requestRun` journals. Test those through the embedding API with explicit isolated evidence, then qualify the actual protocol path with its contract/fork tests and authorized real-chain acceptance. Synthetic lifecycle tests establish orchestration behavior; they do not establish a protocol's live execution or economics. --- # Performance and latency > Choose native or WASM embedding, measure retained evaluation and execution latency, bound memory and queues, and account for ML training and host scheduling. [Canonical documentation](https://arcn.ftw.trade/docs/performance) Retain a compiled program and incremental run, feed observations at their recorded availability and schedule required clock watermarks. Measure the complete path that matters to your application. Evaluator speed, data freshness, time to submit an order and time to settle it are separate measurements. A fast backtest or arithmetic kernel does not establish a live execution latency bound. ## Choose the execution path | Path | What executes | What the application supplies | |---|---|---| | Native Rust | `arcana-core` compiles typed IR; `live::Run` retains its evaluator state | Typed samples, ordering, persistence and an authorized action consumer | | JavaScript or TypeScript embedding | `@arcana/core` runs the Rust evaluator in packaged WebAssembly | The host declaration and values passed to a retained `Run`; the public step boundary validates and serializes input and results | | Opt-in numeric folds | `numericFolds: 'wasm-v1'` specializes supported numeric/boolean fold bodies while the existing run owns state, decisions and budgets | An explicit backend choice; unsupported bodies refuse startup | | Hosted TypeScript extensions | ftw.trade transforms TypeScript and executes it in isolated QuickJS, itself built to WASM | Host calls for IO, persistence and actions, with CPU/memory/wall limits | Rust and JavaScript use the same language core. The native Rust route avoids the npm JSON/UTF-8 boundary; it is not a promise of allocation-free evaluation. Building the evaluator or QuickJS to WASM does not compile each user's whole program into a specialized machine-code loop. Numeric fold compilation has a deliberately narrower contract; it leaves other expressions and action handling under the existing runtime. It never changes backend after an execution failure. See [Embedding](https://arcn.ftw.trade/docs/embedding) for installation, host declaration and checkpoint ownership, and the repository's [numeric fold contract](https://github.com/johnny-emp/flywheel/blob/main/arcana/docs/numeric-folds.md) for supported bodies, admission and recovery. No public npm release is implied by these source-checkout workflows. ## Keep the live path incremental Compile and load model artifacts before processing observations. Warm the run once, retain it, then consume only inputs after its saved cursor. The host must also schedule clock/deadline watermarks the program requires: `run.step({}, time)` can advance evaluation time while holding prior source values when no new sample arrives. Retain the selected libraries, language version, host configuration and backend choice with the program. Compilation, module instantiation and warmup belong in startup measurements, not hidden inside a reported hot-step number. `run.snapshot()` serializes retained state; it does not persist an input cursor or create an atomic storage commit. Save checkpoint and consumed input position together through your host's storage seam. Measure snapshot size, encoding time, commit time and recovery separately. An invalid checkpoint or runtime refusal must remain visible; do not substitute values, replay effects or silently start a fresh run. Bound windows, tape buffers, record/array width, keys, imported libraries, live runs and output volume before admission. Profile retained bytes, temporary allocations, garbage collection, checkpoint bytes and queue high-water under realistic fan-out. A logical work quota is a deterministic resource bound, not a wall-clock deadline. WASM linear-memory capacity is a high-water allocation, not a measure of current live heap or a per-step allocation count. Native and WASM paths both retain the language's work and memory checks. ## Measure a retained public step After [building and installing the package](https://arcn.ftw.trade/docs/embedding#install-and-load), run this JavaScript with Node 22 or later. It uses illustrated minute-bar prices, declares no actions and sends nothing. Inputs and timing storage are prepared before measurement. Each duration covers the public `Run.step` call, including its validation, encoding, evaluation and result construction. ```js import { loadArcana } from '@arcana/core'; const fs = await loadArcana(); const host = { sources: [{ name: 'bars.close', type: 'number', keyed: true, intervals: ['1m'] }], capabilities: [], }; const source = `arcana "0.1"; uses { bars: "^1" } const close = bars.close("DEMO-USD", "1m"); metric("average", ema(close, 20));`; const compileAt = performance.now(); const compiled = fs.compile(source, host); const compileMs = performance.now() - compileAt; if (!compiled.ok) throw new Error(JSON.stringify(compiled.problems)); const startAt = performance.now(); const started = fs.run(compiled.program); const startMs = performance.now() - startAt; if (!started.ok) { fs.drop(compiled.program); throw new Error(started.error); } const run = started.run; try { const warmup = 128; const count = 4096; const samples = Array.from({ length: warmup + count }, (_, i) => ({ values: { 'bars.close': 100 + Math.sin(i / 17) }, time: 1700000000 + i * 60, })); const times = new Float64Array(count); for (let i = 0; i < warmup; i++) { const step = run.step(samples[i].values, samples[i].time); if (!step.ok) throw new Error(step.error); } let checksum = 0; for (let i = 0; i < count; i++) { const sample = samples[warmup + i]; const before = performance.now(); const step = run.step(sample.values, sample.time); times[i] = (performance.now() - before) * 1000; if (!step.ok) throw new Error(step.error); if (!Number.isFinite(step.metrics.average)) throw new Error('average is unknown'); checksum += step.metrics.average; } const snapshotAt = performance.now(); const saved = run.snapshot(); const snapshotUs = (performance.now() - snapshotAt) * 1000; if (!saved.ok) throw new Error(saved.error); times.sort(); const percentile = q => times[Math.floor((times.length - 1) * q)]; console.log({ samples: count, compileMs, startMs, p50Us: percentile(0.5), p95Us: percentile(0.95), p99Us: percentile(0.99), maxUs: times[times.length - 1], snapshotUs, checkpointBytes: new TextEncoder().encode(saved.snapshot).byteLength, checksum, }); } finally { run.drop(); fs.drop(compiled.program); } ``` This is a starting diagnostic, not a service benchmark or a trading strategy. Timer overhead and host scheduling affect its samples. It excludes collection, inference, database commits, signer calls, network submission and settlement. Do not copy its local p99 into a production promise. For comparisons, predeclare the workload and supported operations, use identical inputs, alternate lane order across repeated rounds, and check complete outputs and restored continuation before interpreting speed. Compare retained native core, native typed wrapper, raw ABI and public WASM calls separately. Raw ABI timing omits public validation and is not a recommended production API. Report code/module hashes, build profiles, machine/runtime versions, sample counts, warmup, snapshot frequency and distributions. Measure allocation counters in a separate pass because the counters change timing. From an authorized source checkout, existing repository measurements include `just arcana-wasm-evaluate` for public/raw ABI and numeric study batches, and `just arcana-numeric-fold-evaluate` for complete public live steps with the explicitly selected fold backend. Their [recorded scope and limitations](https://github.com/johnny-emp/flywheel/blob/main/arcana/docs/wasm-backend-evaluation.md) distinguish batch throughput from single-observation latency. These commands require installed development dependencies and the Rust WASM target; they start no venue connection or signer. ### Compare native core, typed native and public WASM The live harness builds release native latency and allocation executables plus the actual WASM module, then runs four shared finite fixtures: ```sh just arcana-live-evaluate --output live-baseline.json just arcana-live-evaluate --output live-candidate.json --compare live-baseline.json ``` The [harness](https://github.com/johnny-emp/flywheel/blob/main/scripts/arcana-live-evaluation.ts) and [versioned fixtures](https://github.com/johnny-emp/flywheel/blob/main/arcana/fixtures/live-latency.json) check expanded inputs, complete outputs, checkpoints, restored suffixes and logical work across the native core, typed native facade and public WASM lanes. `--compare` requires matching fixture/input/output/checkpoint/work identities and nonincreasing native allocation calls and requested bytes. Timing remains descriptive unless an explicit timing gate is requested. To enforce p99 limits, supply `--timing-budget machine-budget.json` **with** `--compare`. Set `ARCANA_BENCH_MACHINE` to the budget's declared machine class when generating both baseline and candidate. The budget names fixture and lane p99 maxima in nanoseconds; unknown fixtures/lanes, missing comparison or mismatched machine classes refuse. Choose limits for controlled hardware, build and load conditions rather than treating a shared-machine observation as a portable guarantee. Allocation counting runs separately and covers native System allocator requests; it does not count JavaScript/WASM allocations or establish an RSS limit. Native hot-step timing excludes destruction of the returned step; throughput and allocation passes include it, while public WASM garbage collection is runtime-controlled. Native lanes alternate within their rounds; WASM runs afterward. Cold compilation/initialization boundaries differ between lanes. These are useful retained-path diagnostics, not an isolated attribution of native-versus-WASM overhead, production arrival capacity or execution latency. ## Clocks, cadence and availability An application decides when to feed the evaluator. `Run.step` consumes a completed observation/bar; it does not subscribe, poll or schedule itself. `Run.forming` supports explicitly declared forming inputs and intrabar triggers. It has different semantics from a closed-bar replay, which reports that behavior as unmodelled. It is not an instruction to bypass bar construction or act on every raw exchange packet. `Run.step` and `Run.forming` evaluation timestamps use whole seconds; duration helpers express seconds. They are not a precision timer or a submillisecond scheduling contract. Preserve source timestamps and observed availability separately in typed fields when your host needs finer precision. Use a monotonic host clock for local elapsed durations; do not substitute it for replayable event time or compare unsynchronized machines' monotonic clocks. A delayed observation becomes usable when the consumer actually receives or selects it. Replay at that availability point, retaining the older source and collector receipt times for freshness checks. Backdating a queued observation or prediction to its source timestamp introduces lookahead. Declared freshness, ordering, gaps and explicit unknown observations are part of correctness, not optional latency overhead. ## ML on a latency budget Built-in `ml.*` models train and predict synchronously in the evaluator. Fitting is bounded and charged before it runs; it can still make a retraining step much slower than an ordinary prediction. Measure first fit, scheduled/conditional refits and steady predictions separately, with the actual model, feature count, window and history. Smaller windows or less frequent retraining change the model's behavior and need strategy validation as well as timing evidence. For an external Python, ONNX, GPU or remote model, load/train it in an application-owned backend. Feed recorded predictions as typed source inputs with model/feature identity, input as-of time and prediction availability time. Replay those outputs instead of rerunning inference. Declare bounds for pending requests, inference latency and stale results; an unavailable score stays explicitly unknown. The [extension guide](https://arcn.ftw.trade/docs/extending#built-in-learning-and-external-inference) provides a runnable illustrative adapter, not a production inference or model quality claim. There is no arbitrary model-runtime import inside Arcana. ## Measure submission and settlement separately For a live execution SLO, correlate one admitted observation with its decision and exact order/request identity. Record queue wait, evaluation, checkpoint and intent commit, risk checks, signer RPC, submission, acknowledgement and terminal outcome. Separate local monotonic durations from venue clocks, and separate acknowledgement from a fill or confirmed transaction. Measure p50/p95/p99/max under steady traffic, bursts, many readers, model refits and slow dependencies. Include refusal rates, maximum queue age and pending bytes; a fast accepted sample alone can conceal overload. Set capacity and tail-latency limits for the chosen deployment. Apply explicit backpressure, sampling or refusal when it cannot keep up; never silently drop events from a continuity-dependent model or trade an old signal as fresh. Single-writer fences, durable input/intent identity, risk invariants, grant checks, separate signing, in-flight exclusivity and outcome reconciliation remain required by the execution host. A timeout does not prove cancellation or release unresolved money. A compiler optimization creates no new authority. ## ftw.trade host path The language library and ftw.trade's hosted scheduling have different contracts: | Hosted stage | Current implementation and implication | |---|---| | Venue observation | `apps/web/lib/live-books.ts`: coherent book admission and freshness provenance | | Source delivery | `book-delivery.ts` and `source-sockets.ts`: bounded ordered consumers; configured delivery does not alter strategy cadence | | Arcana source | `flow-sources.ts` and `automations.ts`: retained WASM evaluation; emitted outputs pass publication authority and Postgres commit | | Hosted strategy | `strategies.ts`: closed-bar decisions, checkpoint/log/output writes and authorized order dispatch | | Placement | `runtime-worker.ts` and `project-runners.ts`: existing workers schedule another strategy round a second after completion; dedicated placement is configuration-dependent | | Execution | `perp-moves.ts`, `perps-live.ts` and the separate signer: intent/risk checks, preparation, signing, venue submission and authoritative outcomes | The repository also contains a native `platform/worker` → `platform/host` watermark path over the same language core. Its existence does not establish that ftw.trade's hosted book consumers use it, that a dedicated placement is active, or that current live strategies have an event-by-event execution SLO. Use the [platform's strategy guide](https://docs.ftw.trade/strategies/overview) for its actual supported cadence and execution workflow. Faster source delivery or numeric folds alone cannot remove closed-bar waits, persistence, signer round trips or venue settlement. --- # Use strategy outputs > Consume evaluated metrics, text and action intents in charts, application logs and your own data consumers, with delivery and storage owned by the host. [Canonical documentation](https://arcn.ftw.trade/docs/outputs) A program's outputs can become data in your own application: chart series, status labels, event rows, alerts or inputs to another consumer. The core returns structured values. Your host chooses where they go and which consumers may read them. ## What comes out | Output | Core API | Example consumer | |---|---|---| | Numeric or boolean metrics | `step.metrics` | A chart, table, monitor or derived dataset | | Text metrics | `step.texts` | A status label, annotation or application log message | | Action decisions | `step.decisions` | An event stream, notification adapter or execution adapter | | Application logs | Records your application constructs | A log viewer or observability service | | Stored rows | Records your application persists | A database, API, dashboard or another application | `metric("average", average)` publishes a number. `metric("state", "ready")` publishes text, returned through `texts`. In a replay those fields contain one column per metric; `run.step` returns the values for the bar just evaluated. An action decision means the program requested a declared action. It is not a receipt, fill or confirmation that an external effect happened. An application can inspect or store decisions without attaching an execution adapter. ## Turn evaluated values into application data The following complete example computes an average, publishes a text state and requests a host-defined `signals.publish` action when the condition changes. Its six hourly prices are an illustrated input sequence, not recorded market data. The example collects outputs in memory; it does not send notifications, place orders or write a database. Use an authorized source build of the package as described in [Embedding Arcana](https://arcn.ftw.trade/docs/embedding). Save this as `outputs.mjs` in an application with that built package installed and run `node outputs.mjs`. ```js import { loadArcana } from '@arcana/core'; const arcana = await loadArcana(); const host = { sources: [ { name: 'bars.close', type: 'number', keyed: true, intervals: ['1h'] }, ], capabilities: [JSON.stringify({ name: 'signals', actions: { publish: { args: { message: 'Text', close: 'Number' }, required: ['message', 'close'], evaluated: ['message', 'close'], authority: { kind: 'host', operation: 'signals.publish' }, }, }, })], }; const source = `arcana "0.1"; uses { bars: "^1", signals: "^1" } const close = bars.close("DEMO-USD", "1h"); const average = isNa(close[2]) ? na : sma(close, 3); const above = close > average; metric("close", close); metric("average", average); metric("state", isNa(average) ? "warming up" : above ? "above average" : "at or below average"); when (above) { signals.publish({ message: "above average", close: close }); } when (!above) { signals.publish({ message: "at or below average", close: close }); } `; const compiled = arcana.compile(source, host); if (!compiled.ok) throw new Error(JSON.stringify(compiled.problems)); try { if (compiled.program.warnings.length) { throw new Error(JSON.stringify(compiled.program.warnings)); } const started = arcana.run(compiled.program); if (!started.ok) throw new Error(started.error); try { const run = started.run; if (run.unmodelled.length) throw new Error(run.unmodelled.join('; ')); const closes = [100, 102, 104, 101, 99, 105]; const firstClose = Date.UTC(2026, 0, 1, 1) / 1000; const runId = 'illustrated-signal-run'; const chartRows = []; const actionIntents = []; const applicationLogs = []; // Preserve unknown values as explicit chart gaps when encoding JSON. const chartNumber = (value) => { if (Number.isNaN(value)) return null; if (!Number.isFinite(value)) throw new Error('Invalid output metric'); return value; }; for (let i = 0; i < closes.length; i++) { const time = firstClose + i * 3600; const step = run.step({ 'bars.close': closes[i] }, time); if (!step.ok) throw new Error(step.error); if (step.orders.length || step.refused.length) { throw new Error('Unexpected broker order or invariant refusal'); } chartRows.push({ time, close: chartNumber(step.metrics.close), average: chartNumber(step.metrics.average), state: step.texts.state, }); for (const decision of step.decisions) { actionIntents.push({ runId, bar: step.bar, time, decision }); } // This log record belongs to the application, not a core stdout stream. applicationLogs.push({ level: 'info', event: 'arcana.step', runId, bar: step.bar, time, message: step.texts.state, actionCount: step.decisions.length, }); } console.log(JSON.stringify({ chartRows, actionIntents, applicationLogs }, null, 2)); } finally { started.run.drop(); } } finally { arcana.drop(compiled.program); } ``` The program explicitly waits for three observations with `isNa(close[2])`, so its average is unknown for the first two bars, then `102`, `102.333…`, `101.333…` and `101.666…`. A chart can draw `close` and `average` against `time`, leave the initial unknown values as gaps, and use `state` for labels. `time` is the input bar's close in Unix seconds; convert it explicitly if your visualization expects milliseconds. The three actual decisions occur at bars `2`, `3` and `5`. Each retains the core's `site`, `capability`, `action`, symbolic `args` and evaluated `values`. Here, `values.message` and `values.close` contain the text and price at the moment the action fired. A consumer can place markers on those bars or store them as events. These records do not claim that anything was delivered or executed. ## Where logs come from The core returns metrics, text, decisions and result errors; this example does not call an Arcana `log` function or receive a core stdout stream. The JavaScript application creates `applicationLogs` from each successful step, then prints its collected data with its own `console.log`. A host can instead send those structured records to its own logging service, store text metrics as annotations, or declare a separate logging capability. In each case the host implements delivery and chooses retention. Compiler diagnostics and runtime errors should remain visible as failures, not successful evaluation log entries. ## Connect a durable consumer Choose the output contract your consumers need: program identity and version, run identity, source identity and units, input cursor, evaluation timestamp, metric names, and action site. Persist that identity with the output so a replay can be recognized rather than inserted as a new event. Commit output rows, the input cursor and the next [checkpoint](https://arcn.ftw.trade/docs/embedding#replay-and-checkpoint) together where your storage supports a transaction. For delivery to another system, an application-owned durable outbox can carry committed events; the consumer needs an idempotency key that identifies the same run, input and action site. An in-memory array like this example has no durability or delivery guarantee. An adapter that performs an external action must retain and reconcile its real outcome. `run.report` can feed supported outcomes back to the run for handlers; an emitted decision alone must never be marked as a receipt. The language does not provide your database, charting library, event bus, signing service or consumer access policy. These boundaries also let an application publish strategy outputs as its own data product or use them as inputs to another process. The host owns the schema, permissions, delivery and storage. For ftw.trade's particular strategy and visualization workflow, read [its platform documentation](https://docs.ftw.trade/strategies/overview); for the underlying API, read the [language reference](https://arcn.ftw.trade/docs/reference).