Key Concepts
Domain model and core abstractions
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.
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:
| Tier | Name | What it covers |
|---|---|---|
| I | Macro Liquidity | Broad monetary and credit conditions: M2, Fed Balance Sheet, HY Credit Spread, NFCI, Bank Credit Total |
| II | Capital Flows | Money moving into and out of markets: Margin Debt, Foreign Equity Holdings |
| III | Market Microstructure | How markets are functioning mechanically: MOVE, SPY illiquidity, HYG illiquidity, Stock-Bond 60D correlation |
| IV | Sentiment & Positioning | How 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 withcadence="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:
| Field | Purpose |
|---|---|
id | Unique string identifier (e.g. "hy_spread") |
name | Human-readable display name |
tier | Integer 1–4 — which tier this metric belongs to |
source / source_kind | Display string and machine-readable source type |
unit | Display unit (e.g. "%", "bps") |
cadence | Publication frequency: "daily", "weekly", "monthly" |
zscore_years | Rolling window length in years (default 3) |
ingest_fn | Callable that pulls raw data from upstream; None for stubbed metrics |
compute_fn | Callable 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_composite | Whether 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 directiondirectional_z— the bull-positive z-score used by the composite (see below)as_of— the observation datesparkline— a list of recent values for the inline chartvote— theVoteobject 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_zis−z - VIX: a high VIX is bearish, so its
directional_zis−z - COT S&P spec net: extreme positioning is contrarian, so its
directional_zuses thecontrarian_longrule
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; otherwisedirectional_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.
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).
- 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-ingestand the associatedmaketargets to pull the latest readings from all data sources. - Setting up the Scheduler — How to configure the background scheduler service so that
daily_ingestandrelease_ingestrun 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, implementingest_fnandcompute_fn, and register the metric in a tier module. - Architecture overview — How the FastAPI backend, SvelteKit frontend, DuckDB database, and
tide-ingestCLI fit together and share data.