---
title: Adding A New Metric
product: tide
doc_type: guide
version: master
source: git2docs (code-derived, validation-filtered)
canonical: https://git2docs.com/wabam/docs/tide/tide-capital-pressure-observatory/adding-metrics
---

# Adding A New Metric

_Step-by-step guide to registering a new indicator_

## Overview

This page walks you through adding a new indicator to TIDE from scratch — writing the ingest function, implementing the compute function, registering the metric definition, and wiring everything into the dashboard. Every indicator in TIDE follows the same four-file pattern (one module per metric, one import in the tier's `__init__.py`, one `make init-db` call, and one `tide-ingest run` call), so once you have done it once you can add subsequent metrics in minutes. Understanding the pattern also lets you audit and extend any of the 15 live metrics already in the codebase.

## Prerequisites

Before you begin, make sure you have:

- **Python ≥ 3.10** with the TIDE virtual environment activated (the Makefile uses `VENV` to locate it).
- **A configured `.env` file** at the project root containing at least `FRED_API_KEY=<your_key>` if your new metric pulls from FRED. Get a free key at [fred.stlouisfed.org](https://fred.stlouisfed.org/docs/api/api_key.html).
- **DuckDB initialised** — run `make init-db` at least once before you begin so the `MetricDefinition` registry table exists.
- **`make`** available on your `PATH`.
- Familiarity with Python dataclasses and a basic understanding of what a z-score represents (see the [Key Concepts](../concepts) page).
- Decided which of the four tiers your metric belongs to:
  - **Tier 1** — Macro Liquidity
  - **Tier 2** — Capital Flows
  - **Tier 3** — Market Microstructure
  - **Tier 4** — Sentiment & Positioning

## Configuration

All configuration for a metric lives in its `MetricDefinition` dataclass. The fields below have the most impact on how the dashboard calculates and displays your indicator.

### `indicator_kind`

| Value | Behaviour |
|---|---|
| `"level"` | The raw stored value is the indicator. Use `level_series()` in `_compute()`. |
| `"yoy"` | The indicator is the year-over-year percentage change of the stored value. Use `yoy_series()` in `_compute()` and set `indicator_lag` to the appropriate lag in observations. |

This field is used by `composite_history` to recompute directional z-scores for every historical date without re-running `_compute()` for each one.

### `indicator_lag`

Only consulted when `indicator_kind == "yoy"`. Set it to the number of observations that represent one year:

- **Weekly series** → `52`
- **Monthly series** → `12`
- **Quarterly series** → `4`
- **Daily series** → `252`

### `indicator_window`

The rolling window (in observations) used to compute the z-score. The default `zscore_years=3` translates to:

- **Daily** → `756` observations
- **Weekly** → `156` observations
- **Monthly** → `36` observations
- **Quarterly** → `12` observations

You can override `zscore_years` per metric when historical data is limited. For example, `hy_spread` uses `zscore_years=1` and `indicator_window=252` because FRED truncated `BAMLH0A0HYM2` to a rolling 3-year window in April 2026.

### `direction_kind`

Controls how the raw z-score is converted to a `directional_z` (bull-positive) for the composite:

| Value | Effect | Example metrics |
|---|---|---|
| `"natural"` | `directional_z = z` — high reading = bullish | `walcl`, `m2`, `totll` |
| `"inverted"` | `directional_z = -z` — low reading = bullish | `nfci`, `hy_spread`, `amihud_spy` |
| `"contrarian_long"` | Extreme long (z > +1.5σ) is contrarian bear; extreme short is contrarian bull; mid-range gets a mild −0.3× bias | `aaii` |

The composite averages `directional_z` across all four tier averages, not the raw `z`. Choosing the wrong `direction_kind` will flip your metric's contribution to the composite score.

### `include_in_composite`

Defaults to `True`. Set to `False` for "loaded spring" or fragility gauges that convey magnitude of risk rather than a directional bull/bear signal. The metric will still appear as a tier card and count in the vote tally — it just will not move the composite z-score.

### `zscore_years`

Default `3`. Overriding this changes both the z-score window and, by extension, how far back `_compute()` requests data. Match this to the realistic depth of the upstream series.

### `cadence`

A display string (`"daily"`, `"weekly"`, `"monthly"`, `"quarterly"`) shown on the Sources page. It does not drive scheduling logic — the Scheduler always runs `tide-ingest` for all metrics regardless of cadence.

### Environment variables

Two environment variables affect all metrics globally (set in `.env` or your shell):

| Variable | Default | Effect |
|---|---|---|
| `FRED_API_KEY` | _(none)_ | Required for any metric with `source_kind="fred"`. Without it, `_ingest()` raises `FredKeyMissing`. |
| `TIDE_DB_PATH` | `<project_root>/data/tide.duckdb` | Path to the DuckDB file shared by the API and the ingest CLI. |

The Makefile also honours `VENV` (path to the virtual environment) and `PORT` (API server port).

## Usage

Once your metric module is wired in, you interact with it through two tools: the `tide-ingest` CLI and `make` targets.

### Pulling data for a single metric

During development, refresh only the metric you are working on to keep iteration fast:

```bash
tide-ingest run --metric <metric_id>
```

This calls your `_ingest()` function, writes the returned `(date, float)` pairs to DuckDB, and then calls `_compute()` to verify the reading resolves correctly. If `_compute()` returns `None`, the CLI prints a warning — you likely need more historical data or a wider `indicator_window`.

### Pulling data for all metrics

Once you are satisfied with the new metric, run a full ingest to keep all indicators in sync:

```bash
tide-ingest run
```

### Rebuilding the composite history

The 252-business-day composite history chart is pre-computed and stored in DuckDB. Run the backfill after any ingest that adds or changes a metric:

```bash
make backfill-composite
```

Skipping this step means your new metric will appear in the live composite reading but will not yet be reflected in the historical chart.

### Checking the result in the dashboard

Open the TIDE Dashboard in your browser (default `http://127.0.0.1:8000` — see `PORT` in your Makefile). Your new metric card should appear in the correct tier with its current value, z-score label, vote chip, and sparkline. The as-of date on the card reflects the most recent observation returned by `_ingest()`.

If the metric card is missing, check that the import was added to the tier's `__init__.py` and that `make init-db` was re-run after the change.

### Verifying source status

Navigate to the Sources page at `/sources` in the dashboard. It shows when each data source last ran, when it will next run, and any errors. A newly added metric appears here automatically once its first ingest completes.

### Automating daily refresh

The Scheduler runs `tide-ingest` automatically after market close each weekday. Start it with:

```bash
make scheduler
```

The Scheduler fires two APScheduler cron jobs in the `America/New_York` timezone: `daily_ingest` Monday–Friday at 17:00, and `release_ingest` on Fridays at 18:00 (for data that publishes after the close). Your new metric is included in both jobs without any additional configuration. Scheduler state persists to the `scheduler_status` DuckDB table and is visible on the Sources page.
