---
title: "A series with an address"
description: "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…"
order: 70
section: "cookbook"
---

<!-- source: docs/indicators/cookbook/series-address.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

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

```bash
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.

```json
{
  "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 }
      ]
    }
  ]
}
```

```typescript
// 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):

```bash
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](../core-concepts/data-sources.md) for the `missing` policies and how a secondary input lines up on the primary grid
- [Drawing primitives](../functions/drawing-primitives.md) for status cards, their row keys, and the series input note
- [Multi-source](../core-concepts/multi-source.md) for a second input on the chart's grid and arithmetic across inputs
- [Execution model](../core-concepts/execution-model.md) for `state()` abstaining, `finalize()`, and where pins and series are honored
- [Moving averages](../functions/moving-averages.md) for the `Sma` class and its `NaN` warm-up
- [Limitations](../faq/limitations.md) for what the chart host cannot supply
