tide
Concept

Key Concepts

Domain model and core abstractions


Overview

This page defines the core domain model that TIDE uses to turn raw market data into a single Capital Pressure reading. Understanding these abstractions — how indicators become z-scores, how z-scores become votes, and how votes and scores roll up through tiers into a composite — will help you read the dashboard accurately, debug unexpected readings, and extend TIDE with new metrics.


Content

Composite Reading

The composite reading (also called the composite score or composite z) is the single number displayed in the hero panel of the TIDE Dashboard. It summarises the overall level of Capital Pressure across all four tiers of indicators. A high composite reading indicates tighter financial conditions — credit is expensive, market stress is elevated, and liquidity is constrained. A low reading indicates easier conditions.

The composite is computed as the equal-weight average of the four tier averages. Each tier averages its own metrics' directional_z values first, and then those four tier averages are averaged together. This two-level averaging ensures that every tier contributes equally to the composite regardless of how many metrics it contains — Tier II's two metrics carry the same weight as Tier III's four.

Tier 1 avg_z  ─┐
Tier 2 avg_z  ─┤
               ├─► equal-weight mean ─► composite z
Tier 3 avg_z  ─┤
Tier 4 avg_z  ─┘

Capital Pressure

Capital pressure describes how tight or loose financial conditions are across the market at a given moment. High capital pressure generally means credit is expensive, borrowing is constrained, and market stress is elevated. Low capital pressure means conditions are accommodative. TIDE aggregates 19 indicators across four domains to produce a single continuous measure of capital pressure rather than requiring you to interpret each source separately.

Tiers

TIDE organises its 19 indicators into four tiers, each covering a distinct aspect of market conditions:

TierNameWhat it covers
IMacro LiquidityBroad monetary and credit conditions: M2, Fed Balance Sheet, HY Credit Spread, NFCI, Bank Credit Total
IICapital FlowsMoney moving into and out of markets: Margin Debt, Foreign Equity Holdings
IIIMarket MicrostructureHow markets are functioning mechanically: MOVE, SPY illiquidity, HYG illiquidity, Stock-Bond 60D correlation
IVSentiment & PositioningHow participants are positioned and feeling: VIX term structure, AAII Bull/Bear, Dealer Gamma (DIX), COT S&P spec net

Each tier computes its own average directional_z across its metrics. That average appears on the dashboard alongside the individual metric cards and feeds into the composite.

Z-Score

A z-score (also: rolling z-score, directional z) expresses how far today's indicator value is from its historical average, measured in standard deviations. A z-score near zero means conditions are within their normal historical range. A strongly positive z-score means the indicator is at an unusually elevated level relative to history; a strongly negative z-score means it is unusually depressed.

Z-scores in TIDE are 3-year rolling by default — the mean and standard deviation are computed over the most recent 3 years of observations rather than over the full history. This keeps the baseline responsive to structural regime changes. The window length is configurable per metric via MetricDefinition.zscore_years; for example, the HY Credit Spread metric uses a 1-year window because FRED truncated the BAMLH0A0HYM2 series to a 3-year window in April 2026.

The window passed to the z-score helpers is expressed in observation periods, not calendar days, so it is calibrated to each metric's publication cadence:

  • Monthly metric → window ≈ 36 (3 years × 12 months)
  • Weekly metric → window ≈ 156 (3 years × 52 weeks)
  • Daily metric → window ≈ 756 (3 years × 252 business days)

TIDE provides two z-score shapes:

  • level_series — z-scores the raw observation values directly. Used for metrics like HY Credit Spread, NFCI, and VIX, where the level itself is the meaningful signal.
  • yoy_series — computes year-over-year percentage change first, then z-scores that change series. Used for metrics like M2 money supply, Fed Balance Sheet, and Bank Credit Total, where the growth rate matters more than the absolute level.

Vote

Every metric casts a vote of bull, neutral, or bear based on its current z-score. A bullish vote means the metric is signalling easier conditions; a bearish vote means it is signalling tighter or more stressed conditions. Each Vote also carries a human-readable reason string that appears on the metric card to explain the signal in plain language.

The tally displayed in the dashboard header is the count of bull, neutral, and bear votes across all active metrics. It provides a quick at-a-glance read on how many individual indicators agree with the composite direction versus diverge from it.

As-of Date

The as-of date (also: last updated, observation date) is the date on which an indicator's underlying data was last published by its source. Because TIDE pulls from a mix of daily, weekly, monthly, and quarterly sources, different metric cards will show different as-of dates. TIDE displays the as-of date on every metric card so you always know exactly how fresh each reading is — you are never left guessing whether a number reflects today's conditions or last month's.

Stale Data

Stale data is an indicator reading that has not been updated recently because its source publishes on a delay. For example, a monthly series may not publish its latest value until several weeks into the following month. TIDE shows stale readings rather than hiding them — the last known value remains visible on the metric card along with its as-of date, and the composite incorporates that last-known value via forward-fill. This means the composite continues to reflect the most recent information available for every metric, even when some series are running on a lag.

Watchlist

The watchlist is a set of eight healthcare stock tickers tracked on the dashboard. Each ticker is compared to the XLV healthcare sector ETF to show how it is performing relative to the broader sector over the past 30 days. The watchlist is separate from the four-tier composite system and does not contribute to the Capital Pressure reading; it exists as a focused equity lens alongside the macro dashboard.

Sparkline

A sparkline is a small inline chart rendered next to each watchlist ticker (and each metric card) showing its recent price or value trend at a glance. Sparklines are intentionally minimal — they convey direction and shape without axes or labels — so you can scan many tickers or metrics quickly.

FRED API Key

The FRED API key (FRED_API_KEY) is a free personal key from the Federal Reserve Economic Data website at fred.stlouisfed.org. TIDE requires this key to download economic indicators like M2 money supply, the Fed Balance Sheet, HY Credit Spread, NFCI, and Bank Credit Total from the FRED API. You set this key in your .env file before running any ingest commands. Without it, all FRED-sourced metrics will fail to ingest.

Relative Return

Relative return is how much a watchlist stock has gained or lost compared to the XLV sector ETF over the past 30 days. A positive relative return means the stock has outperformed the healthcare sector benchmark over that window; a negative relative return means it has underperformed. Relative return makes it easy to identify which healthcare names are leading or lagging the sector without being misled by broad market moves that affect all healthcare stocks equally.

Composite History Chart

The composite history chart (also: 252-day history) is an SVG line chart in the main dashboard panel showing how the composite Capital Pressure reading has evolved over the past 252 business days — approximately one full trading year. The chart is computed by make backfill-composite, which replays the composite calculation for each historical date using the per-metric observation data in DuckDB and forward-fills any gaps from slow-publishing series.

Stubbed Metric

A stubbed metric is an indicator that appears in the dashboard's MetricDefinition registry with its name, description, tier assignment, and metadata, but does not yet fetch live data because its free data source is unavailable or requires disproportionate engineering to access. The four currently stubbed metrics are ici_etf_flows, buyback_yield, put_call, and uvol_dvol. They appear in the dashboard UI with their metadata intact but without a live reading. The unblock path for each is documented in the per-tier __init__.py docstring for its tier module.

Scheduler

The scheduler (also: tide-scheduler) is an optional background service that automatically runs data refreshes after market close each weekday, so you do not have to run update commands manually every day. It runs two APScheduler cron jobs in the America/New_York timezone:

  • daily_ingest — Monday through Friday at 17:00. Pulls every metric with cadence="daily", refreshes the watchlist, and recomputes the composite history.
  • release_ingest — Friday at 18:00. Pulls every non-daily metric (weekly, monthly, and quarterly cadences), then recomputes the composite history. Running this weekly on Fridays is cheap and reliably catches new FRED, FINRA, Treasury, and CFTC releases within a week of publication.

Scheduler state — next run time, last success, last error — persists to the scheduler_status table in DuckDB and is surfaced on the Sources page.

Sources Page

The Sources page (at /sources in the TIDE web app) shows the operational status of every data source. For each metric it displays when data was last ingested, when the next scheduled ingest will run, and any errors that occurred during the last run. Use the Sources page to diagnose stale readings — if a metric's as-of date is unexpectedly old, the Sources page will show whether the ingest failed and why.


Domain Types

Four Python types form the core of TIDE's domain model. Understanding them helps you read the source code and write new metrics.

MetricDefinition

MetricDefinition is the registry entry for a single indicator. It holds all the static metadata TIDE needs to ingest, compute, display, and historicise a metric:

FieldPurpose
idUnique string identifier (e.g. "hy_spread")
nameHuman-readable display name
tierInteger 1–4 — which tier this metric belongs to
source / source_kindDisplay string and machine-readable source type
unitDisplay unit (e.g. "%", "bps")
cadencePublication frequency: "daily", "weekly", "monthly"
zscore_yearsRolling window length in years (default 3)
ingest_fnCallable that pulls raw data from upstream; None for stubbed metrics
compute_fnCallable that reads from DuckDB and returns a Reading
indicator_kind"level" or "yoy" — which z-score shape to use
direction_kind"natural", "inverted", or "contrarian_long" — how to sign directional_z
include_in_compositeWhether this metric's directional_z feeds into the composite (default True)

Reading

Reading is the computed output for a single metric at a point in time — exactly what the dashboard renders on each metric card. Key fields:

  • value — the formatted display value (e.g. "+4.2%")
  • z — the raw z-score, signed by the metric's natural direction
  • directional_z — the bull-positive z-score used by the composite (see below)
  • as_of — the observation date
  • sparkline — a list of recent values for the inline chart
  • vote — the Vote object encoding the bull/neutral/bear signal

Vote

Vote encodes a single indicator's signal at the current reading. Its direction field is one of "bull", "neutral", or "bear". Its reason field is a plain-English explanation displayed on the metric card's info hover. The tally of votes across all metrics is shown in the dashboard header.

directional_z

directional_z is the sign-adjusted form of a metric's raw z-score, normalised so that bull-positive always means easier conditions regardless of the metric's own sign convention. This matters because some metrics are naturally inverted:

  • HY Credit Spread: a high spread is bearish (tight credit), so its directional_z is −z
  • VIX: a high VIX is bearish, so its directional_z is −z
  • COT S&P spec net: extreme positioning is contrarian, so its directional_z uses the contrarian_long rule

The mapping is defined by each metric's direction_kind field and applied by directional_from_z(z, kind):

  • "natural"directional_z = z (high value = bullish)
  • "inverted"directional_z = −z (high value = bearish)
  • "contrarian_long" → when |z| > 1.5, directional_z = −z; otherwise directional_z = −0.3 × z

The composite averages directional_z values — never raw z values — so that every metric's contribution is expressed on the same bull-positive scale.


Examples

Reading the composite z-score

The dashboard header always shows the current composite z and the vote tally. A reading like +0.74σ with a tally of 7 bull / 5 neutral / 3 bear means capital pressure is moderately elevated (conditions are somewhat tight) but not at an extreme.

Composite: +0.74σ   [7 bull | 5 neutral | 3 bear]

A composite near zero (−0.10σ) with a balanced tally signals that conditions are roughly normal across all four tiers.


Understanding the directional_z calculation

Consider the HY Credit Spread metric (direction_kind = "inverted"). Suppose the raw z-score today is +1.8 (spreads are 1.8 standard deviations above their 3-year average — spreads are wide, which is bearish for credit conditions).

# From backend/tide/metrics/base.py
def directional_from_z(z: float, kind: DirectionKind) -> float:
    if kind == "inverted":
        return -z

# Applied:
raw_z = +1.8        # spreads are wide → metric's natural direction is stressed
directional_z = -1.8  # bull-positive: this is a bearish signal

On the metric card you see z = +1.8σ (the raw reading, showing how far spreads are from normal). The composite uses directional_z = −1.8, correctly pulling the composite in the bearish direction.


Tracing how a tier average feeds the composite

Suppose Tier I (Macro Liquidity) has five metrics with these directional_z values after the latest ingest:

m2            directional_z = +0.4   (YoY growth above average → easier)
fed_balance   directional_z = -0.3   (balance sheet shrinking → tighter)
hy_spread     directional_z = -1.8   (wide spreads → tighter)
nfci          directional_z = -0.6   (tighter-than-normal conditions)
bank_credit   directional_z = +0.2   (credit growth slightly above average)

Tier I avg_z = mean([+0.4, -0.3, -1.8, -0.6, +0.2]) = -0.42

The same calculation runs for Tiers II, III, and IV. Then:

composite z = mean([tier1_avg, tier2_avg, tier3_avg, tier4_avg])
            = mean([-0.42, +0.20, +0.55, -0.10])
            = +0.058  → rounds to approximately +0.06σ (neutral)

Checking the as-of date for a stale metric

On the Sources page (/sources), each row shows when a metric's data was last successfully ingested. If you see a monthly metric like Margin Debt showing an as-of date from several weeks ago, that is expected — the Z.1 Flow of Funds release publishes on a quarterly lag. The metric card will show the last known value with its observation date, and the composite will forward-fill that value until the next release.

Sources page row (example):
  margin_debt   Tier II   FRED · Z.1   monthly   last observed: 2024-09-30   status: live

This tells you the reading is not stale due to an error — it is simply waiting for the next scheduled data release.


Identifying a stubbed metric

The four stubbed metrics appear on the dashboard with their metadata but no live reading. On the Sources page they are flagged with status: stub:

Sources page row (example):
  put_call   Tier IV   Cboe   daily   last observed: —   status: stub

The unblock path for each stubbed metric is documented in the per-tier __init__.py docstring (e.g. backend/tide/metrics/tier4/__init__.py for put_call and uvol_dvol).


Related concepts
  • Reading the dashboard — How to interpret the composite score, tier panels, vote tally, and metric cards in the context of a live market session.
  • Refreshing data manually — How to run tide-ingest and the associated make targets to pull the latest readings from all data sources.
  • Setting up the Scheduler — How to configure the background scheduler service so that daily_ingest and release_ingest run automatically after market close.
  • Checking data source status — How to use the Sources page (/sources) to diagnose stale readings and ingest errors.
  • Adding a new metric — How to create a MetricDefinition, implement ingest_fn and compute_fn, and register the metric in a tier module.
  • Architecture overview — How the FastAPI backend, SvelteKit frontend, DuckDB database, and tide-ingest CLI fit together and share data.