A series with an address

A package that reads a series by its address, plots it in the lower pane and keeps the latest reading in a status card. The series is one your own daemon…

A package that reads a series by its address, plots it in the lower pane and keeps the latest reading in a status card. The series is one your own daemon published: a watch that reads a number off a page each hour commits that number under a key, om series create turns the committed history into a series, om series publish gives it an address, and any package on any machine can name that address as an input. There is no kScript (legacy) precedent for this recipe; it is our own showcase of an input that is not a venue field.

A series input has no code-first declaration, so this recipe is written metadata-first: the sheet (wrun/metadata.json) declares the inputs, the outputs and the card, and the source only computes. The two blocks under The Indicator are that sheet and that source, and om wrun create scaffolds both as the series-address template. The surfaces it uses: a series input carried forward between readings, two lines in the lower pane, and a card with a value row, an average row and a clock row.

Publish the series first

A series is a watch's numeric history with an address. Any watch that commits a number under a key can become one: a page source in reading mode commits the number it read on every run, and a condition watch's fires carry the metric readings its rule read, by key. om series create snapshots that committed history under a local name, om series publish sends the snapshot to the registry as @<your username>/<name> and advances the patch version whenever the content changed (unchanged content refuses), and om install lands the newest snapshot on any machine, yours included. The package reads an installed snapshot, never the registry, so install the series where the package runs. @you stands for your username: the scope defaults to the signed-in account, and --scope names another one you publish under.

om series create aqi-shanghai --watch aqi-shanghai --key aqi --title "Shanghai AQI" --cadence 1h
om series publish aqi-shanghai --dry-run
om series publish aqi-shanghai --yes
om series show aqi-shanghai
om install @you/aqi-shanghai

om series show prints the address, the cadence and the last five rows, so you can see what the package will read before you build it. The second address form, watch/aqi-shanghai/aqi, skips all of this: it reads your own watch's committed history live, with no snapshot and no publish, and it works only on the machine that runs the watch.

The Indicator

The sheet first, then the source. Both carry the same id, and the series-address template scaffolds them together.

{
  "id": "series-address",
  "name": "A series with an address",
  "description": "Reads a published series by its address, plots it in the lower pane and shows the latest reading in a card.",
  "overlay": "offchart",
  "abi_version": "wrun-2",
  "warmup_bars": 7,
  "params": [],
  "inputSources": {
    "close": { "source": "ohlcv", "field": "close" },
    "aqi": { "source": "series", "ref": "@you/aqi-shanghai", "missing": "carry" }
  },
  "inputs": [
    { "index": 0, "name": "close", "description": "The chart's own candles: the grid every other input lines up on" },
    { "index": 1, "name": "aqi", "description": "The series at @you/aqi-shanghai, its last reading carried forward" }
  ],
  "outputs": [
    { "index": 0, "name": "aqi", "description": "The reading on each bar", "panel": "lower", "plot": "line" },
    { "index": 1, "name": "aqi_ma", "description": "Rolling 7-bar average of the reading", "panel": "lower", "plot": "line" }
  ],
  "drawings": [
    {
      "kind": "card",
      "name": "aqi_card",
      "title": "Shanghai AQI",
      "anchor": "top_right",
      "rows": [
        { "label": "AQI", "value": { "output": "aqi", "format": "int" } },
        { "label": "7-bar avg", "value": { "output": "aqi_ma", "format": "auto" } },
        { "label": "Updated", "clock": true }
      ]
    }
  ]
}
// A series read by its address. The sheet (wrun/metadata.json) declares the inputs, the outputs and the
// card; this file only computes. A series input is read through in_<name>() like any other input.
import { in_aqi, in_close } from "./gen/inputs"; // generated from the sheet: one in_ reader per input
import { emitRow, out_aqi, out_aqi_ma } from "./gen/outputs"; // generated: one out_ writer per output, plus emitRow
import { Sma } from "./sdk/ta"; // the rolling average over the readings

const average = new Sma(7); // seven bars of readings; the card's "7-bar avg" label is literal text in the sheet
let aqi: f64 = NaN; // the reading on this bar (missing: "carry" repeats the last one until a new one lands)
let avg: f64 = NaN; // its rolling average, NaN until seven readings are in

export function init(): void {}
export function state(): i32 { // once per bar, oldest first: read the candle and the reading, fold the average
  aqi = in_aqi();
  if (isNaN(in_close()) || isNaN(aqi)) return 0; // no candle, or no reading has arrived yet: this row abstains
  avg = average.update(aqi);
  return 1;
}
export function finalize(): void { // the two outputs the sheet's draw.card reads from the newest complete row
  out_aqi(aqi);
  out_aqi_ma(avg);
  emitRow();
}
export function reset(): void { average.reset(); aqi = NaN; avg = NaN; } // back to the start, the ring cleared

How it works

The address is the input. "aqi": { "source": "series", "ref": "@you/aqi-shanghai", "missing": "carry" } is an input the way close is one. The daemon resolves the address to the newest installed snapshot (or, in the watch/<slug>/<key> form, to the watch's own committed history), buckets its readings onto the chart's candles, and hands the source one number per bar through in_aqi(); the last reading in a bucket wins. A series can never be the primary input: close at index 0 defines the grid, and the series lines up on it. The address is the whole description, so a series input takes no field, no symbol, no exchange and no interval. Its missing policy is carry (the last reading repeats until a new one lands, so an hourly series reads as a step line on a 5-minute chart) or nan (a bar without a reading reads NaN); zero is refused, because a reading that did not happen is not zero.

Metadata-first, on purpose. The declaring words in ./sdk/declare cover venue fields, celled sources and the time source; there is no word for a series, so the sheet is hand-written and the source reads the generated accessors in_aqi(), out_aqi(...) and out_aqi_ma(...). om wrun build regenerates src/gen from the sheet before every compile, so renaming an input in the sheet renames its accessor. abi_version is wrun-2, the first version with drawings, and warmup_bars: 7 asks the daemon to feed seven bars ahead of the window you read, so a seven-bar average can be ready on the first row you see.

Readiness is per bar. state() runs once per bar, oldest first. It reads the candle and the reading; while no reading has arrived yet (the bars before the series' first row) it returns 0, so the row abstains and nothing is plotted. From the first reading on, every bar is ready: aqi is the reading carried onto that bar, aqi_ma its seven-bar Sma, NaN for the first six. A NaN output is a gap in the line, not a refusal.

The card reads outputs, not the source. The card is a drawing in the sheet, and the source never touches it: the host takes the newest ready row where every referenced output is finite and prints aqi as an integer and aqi_ma with up to six significant digits. Until the average exists no row is complete, so the newest ready row supplies the card and the average prints empty. The clock: true row asks the chart to show its clock on that row, the one line on the card the package cannot compute: readings are per bar, the clock is now. anchor: "top_right" pins the card to the pane corner; offset and z move it and stack it.

Where it runs. A series lives in daemon state, so this package is for your machine: om wrun install, then om metric series, or a watch on wrun/@you/series-address/aqi. The chart host cannot supply a series input. To see the raw series on a chart, om chart series push --address @you/aqi-shanghai plots the installed snapshot as a line. The market you read the package on names the grid: on BTCUSDT at 1h, the AQI readings line up on hourly BTC candles, and the reading has nothing to do with the price.

What kScript (legacy) could not do

  • An input that is not a venue field. A kScript source(...) named a data type on a symbol at an exchange; an air-quality reading, a survey number or a count your watch produced had no way in. Here the input is an address, and the address is the whole description: no field, no symbol, no exchange, no interval.
  • A series published by a daemon. A kScript's numbers stayed on the chart that computed them. A watch's committed history becomes a series with one command, gets an address with another, and every package that names the address reads it.
  • A card with a clock. plotTable under isLastBar printed values the script computed; a row that shows the chart's clock (clock: true) or counts down to an output (countdown_to) is a card row here, drawn by the host from the sheet.
  • An address another user can install. om install @you/aqi-shanghai on any machine lands the snapshot, and a package that names it works there too. A kScript could not read anything another user published.

Customize it

  • Read your own watch live. Change ref to watch/aqi-shanghai/aqi: the daemon reads the watch's committed history directly, no snapshot, no publish, no install. It works only on the machine that runs the watch.
  • Change the window. new Sma(7) in the source and the 7-bar avg label in the sheet are both literal; change them together, and keep warmup_bars at or above the window so the average is ready on the first row you read.
  • Show gaps. "missing": "nan" makes a bar without a reading read NaN: the line breaks between readings instead of stepping, and state() abstains on those bars as written.
  • Add a spark row. { "label": "Last 24", "spark": { "output": "aqi", "window": 24 } } carries the last 24 ready readings on the card, oldest first; the window is 2 to 64.
  • Move the card. anchor takes the nine pane positions, offset: [-12, 12] nudges it by pixels (each from -200 to 200), and z stacks it over other drawings.

Scaffold, build, install, and read the reading on your machine (the series must be installed first, see above):

om wrun create @you/series-address ./series-address --template series-address
om wrun build ./series-address
om wrun install ./series-address --replace
om metric series --metric wrun/@you/series-address/aqi --symbol BTCUSDT --exchange BINANCE_FUTURES --bars 20

Concepts used

  • Data sources for the missing policies and how a secondary input lines up on the primary grid
  • Drawing primitives for status cards, their row keys, and the series input note
  • Multi-source for a second input on the chart's grid and arithmetic across inputs
  • Execution model for state() abstaining, finalize(), and where pins and series are honored
  • Moving averages for the Sma class and its NaN warm-up
  • Limitations for what the chart host cannot supply