Sources in Arcana
Build a source with no code to host. An Arcana program over ftw.trade's streams, a websocket, a polled API or a contract's events, keyed, barred, warmed up and backtested.
An Arcana source is a declared input and a program over its events, one step each. It publishes events of its own with source.emit. State carries from event to event, and across restarts.
A first source#
Tokens whose bonding curve jumped past 90%:
{
"name": "near-grad",
"fields": { "token": "address", "progress": "number", "jump?": "number" },
"from": { "stream": "curves", "where": { "progress": { "gte": 90 } } },
"key": "token",
"flow": "uses { input: \"^1\", source: \"^1\" }\nconst e = input.event();\nwhile (e.graduated == 0) { source.emit({ token: e.token, progress: e.progress, jump: e.progress - e.progress[1] }); }"
}fromis the input.wherefilters before the program runs.keynames the fields that make an event unique: the same key is published once a day (dedupeForchanges that).while (cond)publishes on every eventcondholds for;when (cond)only when it turns true.- A field the program leaves
nais left out, so mark it optional ("jump?").
The same source can be one .source.arcn file, with the definition in a // @source { … } header above the program.
Indicator sources, ready made#
Thirteen indicators are ready as sources: RSI zones, MACD cross, Bollinger Bands, EMA cross, SMA golden and death cross, stochastic, Supertrend, Donchian breakout, CCI, ADX/DMI cross, VWAP cross, volume spike and MFI. Pick one on the Sources page (or with get_source_guide), point it at a pool, a shared feed or a venue market, and it runs on closed bars.
Every indicator source publishes the same shape: the market, the bar's open, high, low and close, its own values (rsi, macd, upper…) and a signal: +1 buy, -1 sell, 0 nothing. So anything can trade it without knowing which indicator it is, and the recipe writes the strategy that does: buy on +1, sell on -1, with the values charted as metrics.
Inputs#
| Input | Reads |
|---|---|
{ stream: "…" } | ftw.trade's own streams: launches, graduations, curves, swaps, prices, markets, books, trades, protocols |
{ socket: "wss://…", read } | A websocket, each message's fields picked by path and type |
{ poll: "https://…", every, read } | A URL polled every second or more (GET or POST) |
{ book: { venue, market, maxAgeMs }, every? } | A coherent order-book observation satisfying bounded source and receipt freshness |
{ event: "Swap(…)", chain, address } | A contract's events, filtered at the RPC, backfilled from a date |
{ source: "…" } | Another source of yours, or one you subscribe to |
Up to four inputs can be merged into one series. Book snapshots and URL polls each run as a source of their own.
Choose how to consume an order book#
A source can request a book only when needed, reduce incoming updates, or process every admitted book revision. Choose the amount of detail the signal uses:
| Need | from configuration | What reaches the program |
|---|---|---|
| Confirm another signal against a fresh book | { book: { venue: "hyperliquid", market: "BTC", maxAgeMs: 1000, fresh: true } } | One coherent observation per requested run |
| Track the current quote | { stream: "books", where: { venue: "hyperliquid", market: "BTC" }, consumption: { mode: "sampled", everyMs: 1000, projection: "bbo" } } | The initial book, then the latest changed snapshot at most once per second |
| React to a meaningful feature change | { stream: "books", where: { venue: "hyperliquid", market: "BTC" }, consumption: { mode: "threshold", minIntervalMs: 250, projection: "full", changes: { spreadBps: 1, imbalance: 0.05 } } } | The initial baseline, then a snapshot when either feature changes enough from the last delivered one |
| Measure changes between admitted book revisions | { stream: "books", where: { venue: "hyperliquid", market: "BTC" }, consumption: { mode: "detailed" } } | Every coherent book revision admitted by the host, within capacity |
projection selects which values count as a change; it does not change the input schema. bbo compares best prices/sizes, mid, microprice and spread. full also compares depth, imbalance and observed levels. A newer timestamp alone does not qualify. Thresholds are positive absolute differences in the field's units: spreadBps: 1 is one basis point; imbalance: 0.05 is five hundredths on its -1 to +1 scale. A transition between unknown and known also qualifies. Pending sampled and threshold snapshots coalesce; brief crossings can disappear before delivery.
A TypeScript source uses the same policy under source({ subscribe: { stream: "books", where: { venue, market }, consumption }, … }); its onMessage receives the complete coherent payload. The SDK types supported streams, modes and feature names. Runtime admission checks intervals, thresholds and projection compatibility.
Sampling accepts 50–60,000 ms. A threshold's minIntervalMs accepts 0–60,000 ms. Delivery is bounded to 20 observations and 256 KiB including in-flight work across all configured books of an automation, with a 64 KiB frame bound. Per-book queues are also capped at 20/256 KiB. Ordinary reader, CPU, run, step and publication quotas apply. Detailed delivery can reach those limits quickly. It is not a promise to capture every raw exchange delta, trade or L3 order.
An invalid frame, gap, queue overflow or consumer failure stops detailed delivery for that deployment generation. A gateway restart or lease loss also interrupts continuity and halts it. Inspect the reason, save a strictly newer source version, then deploy with fresh indicator state. Starting or redeploying the same version, injecting an event or rolling back to an older detailed version cannot bridge the gap. Sampled and threshold readers can begin a new delivery comparison baseline after resynchronization; their downstream Arcana rolling/indicator state is not reset. Handle an epoch change explicitly, or choose detailed delivery when continuity is required.
The source reads scalar best prices and sizes, microprice, spread, depth/imbalance/bands and levels 2–5, plus epoch, revision, sourceTimeMs, receivedAtMs and requestStartedAtMs. A revision belongs to its epoch. Full level arrays are available to tools and TypeScript; Arcana's input.event() exposes the scalar fields. The default books stream without consumption, and the display SSE endpoint, remain coalesced at most once a second.
Request a fresh snapshot on demand#
{
"name": "btc-book-check",
"fields": { "mid": "number", "spreadBps": "number", "epoch": "text", "revision": "number", "sourceTimeMs": "number", "receivedAtMs": "number" },
"from": { "book": { "venue": "hyperliquid", "market": "BTC", "maxAgeMs": 1000, "timeoutMs": 5000, "fresh": true } },
"dedupeFor": 0,
"flow": "uses { input: \"^1\", source: \"^1\" }\nconst e = input.event();\nwhile (e.mid > 0) { source.emit({ mid: e.mid, spreadBps: e.spreadBps, epoch: e.epoch, revision: e.revision, sourceTimeMs: e.sourceTimeMs, receivedAtMs: e.receivedAtMs }); }"
}maxAgeMs is required (0–120,000 ms): both the venue's source time and the host's receipt time must be recent enough. An unknown source time cannot satisfy it. timeoutMs defaults to 5,000 ms (0–15,000); zero accepts only an already qualifying observation. fresh defaults to false; true requires a new observation admitted after the request, rather than rereading the cache. It can have the same prices. The host can share upstream reads between callers; a timeout fails the read without publishing stale data.
sourceTimeMs is the venue's observation/publication clock, not necessarily the last order mutation. For example, Lighter's websocket timestamp records frame generation; its timestamp reference distinguishes the sequencer's aggregate time. Lighter REST book responses lack an authoritative source timestamp, so REST alone cannot certify bounded freshness; a qualifying websocket observation is needed. A receipt clock is never substituted for a missing source clock.
Save with save_flow_source, then deploy with run_automation { id, action: "deploy" }. Without from.every, it runs only when polled. Add every: "5s" beside book to schedule it. poll.source({ name: "btc-book-check" }) in a strategy starts the read and continues immediately. Later statements in that bar still see the existing tape; use a later emitted observation and check its recorded clocks. A workflow's { kind: "poll", source: "btc-book-check" } waits for the source run or joins its in-flight book read. A busy or rate-limited book source can refuse; that does not certify an older event as fresh. Polls are limited to once per second per source.
The host owns the network request and records its selected observation before running the program. Arcana expressions perform no synchronous network IO. A read timeout is a failed observation request; it is separate from the asynchronous settlement of a trading action.
Delivery, computation and replay#
consumption reduces inputs before the program runs. bars.every groups admitted inputs and computes once each bar closes. emit.every computes on every input but coalesces actual output batches afterward. These settings do not change the closed-bar schedule of a strategy reading the source. Use demand reads for occasional confirmation, sampled/threshold inputs for current-state signals, and detailed delivery only when intermediate revisions matter.
Configured book stream events evaluate at actual host dispatch time. A demand observation evaluates at its selection time, asOfMs. These are availability times: preserve the original sourceTimeMs and receivedAtMs fields separately for freshness. For example, a book received at 12:00:00 and delivered at 12:00:02 is an input available at 12:00:02. Replaying it at the old receipt time would give a backtest information before the live consumer had it.
Book inputs are live only: since and warmup cannot reconstruct their original event stream. backtest_flow_source can replay explicit admitted observations with event.at set to their recorded availability time, without fetching the venue; preserve epoch, revision and original source/receipt clocks. Replay does not reapply a delivery policy or rebuild raw exchange deltas. Test delayed availability, deadline failures, stale/unknown source times, invalidation and fresh recovery separately. get_book_history supplies periodic summaries for strategy book.* backtests, not a detailed event journal. Published source outputs retain the usual 30 days of history.
Streaming like a chart#
by: "pool"keeps one run per value of a field: on the swaps stream,ema(e.price, 20)is each pool's own average.bars: { every: "1m", price: "price" }gathers events into bars and steps once each closes, even when nothing more arrives.warmup: "6h"steps stored history first, so indicators are warm on the first live event.since: "2026-09-01"fills the source's history from a date when it's deployed, so a strategy can backtest over it the day it's written.
Pools whose 1-minute RSI runs hot:
{
"name": "rsi-hot",
"fields": { "pool": "text", "rsi": "number" },
"from": { "stream": "swaps" },
"by": "pool",
"bars": { "every": "1m", "price": "price", "volume": "quote", "time": "ts" },
"warmup": "6h",
"flow": "uses { input: \"^1\", source: \"^1\" }\nconst e = input.event();\nconst r = rsi(e.close, 14);\nwhen (r > 70) { source.emit({ pool: e.pool, rsi: r }); }"
}Backtest a source#
backtest_flow_source (or Preview on the Sources page) steps a definition over events you give, or over stored history, and shows what it would have published, before you save or deploy anything.
A source as a market#
A strategy can run on a source's own bars with series: "source:<name>", so a websocket feed aggregated with bars becomes a market to backtest and trade. Sources keep 30 days of events.
Polls on demand#
A URL poll or book snapshot source with no every runs only when asked. A strategy's poll.source({ name }) is fire-and-forget; a workflow's poll step waits for the source run. A run can fail, be rate limited, or publish no event when its program condition is false; asking alone is not evidence of a fresh value.