API Reference
FastAPI endpoint documentation
The TIDE API is a read-only FastAPI service that exposes all data the SvelteKit frontend needs to render the Capital Pressure Observatory dashboard. Use these endpoints directly when you want to integrate TIDE data into your own tooling, debug ingestion health, or inspect raw readings without opening a browser. Every endpoint returns JSON; all endpoints except /health are mounted under the /api prefix.
Parameters vary by endpoint. The table below covers every endpoint in the API.
| Endpoint | Method | Path parameter | Query parameter | Type | Required | Description |
|---|---|---|---|---|---|---|
GET /health | GET | — | — | — | — | No parameters. |
GET /api/dashboard | GET | — | — | — | — | No parameters. Returns the full dashboard payload in one call. |
GET /api/metrics/{metric_id}/history | GET | metric_id | — | string | Yes | ID of the metric whose raw observation history you want. Must match a registered metric ID (e.g. hy_spread, m2_yoy). |
GET /api/metrics/{metric_id}/history | GET | — | since | date (YYYY-MM-DD) | No | If provided, only observations on or after this date are returned. Omit to receive the full history. |
GET /api/metrics/{metric_id}/detail | GET | metric_id | — | string | Yes | ID of the metric you want full detail for. Returns registry metadata, the latest reading, and the computed indicator + z-score series. |
GET /api/tier/{tier}/history | GET | tier | — | integer | Yes | Tier number: 1, 2, 3, or 4. |
GET /api/sources | GET | — | — | — | — | No parameters. Returns ingestion health for every registered metric. |
GET /api/scheduler | GET | — | — | — | — | No parameters. Returns status for every configured cron job. |
GET /api/watchlist | GET | — | — | — | — | No parameters. Returns current readings for all eight healthcare watchlist tickers. |
All endpoints return JSON. The shapes below match the Pydantic schemas that form the wire contract between the API and the SvelteKit frontend.
GET /health
Returns a simple liveness object. Use this to confirm the backend process is running before making other requests.
{ "status": "ok" }
GET /api/dashboard → DashboardOut
The largest response in the API. The SvelteKit page server calls this once per request and distributes the payload to every component on the page. Fields:
| Field | Type | Description |
|---|---|---|
composite | CompositeOut | null | The composite Capital Pressure reading. null if no metrics have data yet. |
composite.z | float | Equal-weight average of the four tier z-score averages, using each metric's directional_z. |
composite.z_label | string | Human-readable label for the current z-score level (e.g. "Elevated"). |
composite.sign_class | string | CSS class hint for coloring (e.g. "bear", "bull", "neutral"). |
composite.tally | TallyOut | Counts of Bullish / Neutral / Bearish votes across all composite-eligible metrics. |
composite.tally.bull | int | Number of indicators voting Bullish. |
composite.tally.neutral | int | Number of indicators voting Neutral. |
composite.tally.bear | int | Number of indicators voting Bearish. |
composite.metrics_total | int | Total number of indicators included in the composite tally. |
tiers | list[TierOut] | One entry for each of the four tiers, always present (may have an empty metrics list if no data has been ingested). |
tiers[].tier | int | Tier number: 1–4. |
tiers[].name | string | Tier display name (e.g. "Macro Liquidity"). |
tiers[].tag | string | Short descriptor (e.g. "The base · multi-year"). |
tiers[].cadence | string | Typical update frequency summary (e.g. "5 series · monthly–weekly cadence"). |
tiers[].avg_z | float | null | Simple mean of the z-scores of all metrics in this tier that returned a reading. null if the tier has no readings. |
tiers[].metrics | list[ReadingOut] | Per-metric cards for this tier. See ReadingOut below. |
composite_history | list[CompositeHistoryPoint] | Up to 252 business days of composite z-score history, used to draw the SVG history chart. |
composite_history[].ts | date | Observation date. |
composite_history[].z | float | Composite z-score on that date. |
composite_history[].spy_close | float | null | SPY closing price on that date, for overlay. null if not yet populated. |
composite_history[].tier_zs | dict[str, float] | Per-tier average z-scores keyed by string ("1"–"4"). Keys are absent if that tier had no data for that date. |
ReadingOut fields (appears inside tiers[].metrics):
| Field | Type | Description |
|---|---|---|
metric_id | string | Unique registry identifier. |
name | string | Display name. |
tier | int | Tier number (1–4). |
source | string | Data source label (e.g. "FRED", "CFTC"). |
unit | string | Unit of the value (e.g. "bps", "%"). |
value | string | Formatted current value. |
delta | string | Formatted change from the previous period. |
delta_class | string | CSS class hint for the delta direction. |
z | float | Rolling z-score (3-year window by default; some metrics use 1-year — see notes). |
z_label | string | Human-readable z-score label. |
z_class | string | CSS class hint for the z-score level. |
as_of | date | Observation date for this reading. |
sparkline | list[float] | Short recent price/value series for the inline sparkline chart. |
vote | VoteOut | The indicator's current vote. |
vote.direction | string | "bull", "neutral", or "bear". |
vote.reason | string | One-line explanation of why this vote was cast. |
include_in_composite | bool | true if this metric's directional z enters the composite average; false for loadedness/fragility gauges that appear in tier cards but are excluded from the tally. |
description | string | null | Longer text description from the metric registry. |
GET /api/metrics/{metric_id}/history → HistoryOut
Returns the raw observation series for a single metric — the values exactly as stored in the observations table, without z-score transformation.
| Field | Type | Description |
|---|---|---|
metric_id | string | Echoes back the requested metric ID. |
points | list[HistoryPoint] | Chronologically ordered list of observations. |
points[].ts | date | Observation date. |
points[].value | float | Raw observed value on that date. |
GET /api/metrics/{metric_id}/detail → MetricDetailOut
Returns everything the API knows about one metric: its registry definition, the latest rendered reading card, and the full computed indicator + rolling z-score series.
| Field | Type | Description |
|---|---|---|
metric_id | string | Unique registry ID. |
name | string | Display name. |
tier | int | Tier number (1–4). |
source | string | Data source label. |
source_kind | string | Source category (e.g. "fred", "yahoo", "cftc"). |
source_series | string | null | Upstream series identifier where applicable (e.g. a FRED series ID). null for sources that do not use a named series. |
unit | string | Unit of measurement. |
cadence | string | Typical publication frequency. |
description | string | null | Longer explanation from the metric registry. |
direction_kind | string | Whether a higher value is bullish or bearish (governs directional z sign). |
indicator_kind | string | "yoy" (year-over-year % change) or "level" (raw level). |
indicator_window | int | Rolling window in days used to compute the indicator. |
indicator_lag | int | Lag in days applied before computing the indicator (used for yoy). |
zscore_years | int | Rolling z-score lookback in years. Default is 3; some metrics use 1 (see notes). |
include_in_composite | bool | Whether this metric's z-score enters the composite average. |
reading | ReadingOut | null | The latest rendered reading card. null for stubbed metrics or metrics with no data. |
series | list[DetailPoint] | Full computed indicator + z-score series, matching what the reading card uses. |
series[].ts | date | Observation date. |
series[].indicator | float | Computed indicator value (yoy% or raw level). |
series[].z | float | null | Rolling z-score on that date. null before the lookback window is filled. |
obs_count | int | Total number of raw observations stored for this metric. |
GET /api/tier/{tier}/history → TierHistoryOut
Returns the per-tier average z-score series populated by tide-ingest backfill-composite. Use this to draw regime charts for a single tier over time.
| Field | Type | Description |
|---|---|---|
tier | int | Tier number (1–4). |
name | string | Tier display name. |
tag | string | Short tier descriptor. |
points | list[TierHistoryPoint] | Chronologically ordered tier z-score history. |
points[].ts | date | Observation date. |
points[].z | float | Tier average z-score on that date. |
latest_z | float | null | Most recent z-score in points. null if no history has been backfilled yet. |
GET /api/sources → SourcesOut
Returns ingestion health metadata for every metric registered in the system. Use this to check which metrics are live vs. stubbed, how recently each was ingested, and how many observations are stored.
| Field | Type | Description |
|---|---|---|
rows | list[SourceRow] | One entry per registered metric, including stubbed metrics. |
rows[].metric_id | string | Registry ID. |
rows[].name | string | Display name. |
rows[].tier | int | Tier number (1–4). |
rows[].source | string | Data source label. |
rows[].source_kind | string | Source category. |
rows[].cadence | string | Publication frequency. |
rows[].status | string | "live" if the metric has an ingest function; "stub" if it is registered but not yet fetching data. |
rows[].last_observation | date | null | Date of the most recent stored observation. null if no data has been ingested. |
rows[].last_ingested_at | datetime | null | Timestamp when the most recent observation was written to the database. null if no data has been ingested. |
rows[].obs_count | int | Total observation count stored for this metric. |
GET /api/scheduler → SchedulerOut
Returns the current state of every configured cron job. State is read from the scheduler_status DuckDB table, which the Scheduler process writes to after each run.
| Field | Type | Description |
|---|---|---|
jobs | list[SchedulerJob] | One entry per configured job. |
jobs[].id | string | Job identifier (matches the key used in scheduler_status). |
jobs[].label | string | Short display name. |
jobs[].description | string | What the job does. |
jobs[].schedule_repr | string | Human-readable cron schedule string. |
jobs[].last_run_at | datetime | null | When the job last started. null if the Scheduler has never run. |
jobs[].last_success_at | datetime | null | When the job last completed without error. null if it has never succeeded. |
jobs[].last_error | string | null | Error message from the most recent failed run. null if no errors have occurred. |
jobs[].last_error_at | datetime | null | When the most recent error occurred. null if no errors have occurred. |
jobs[].next_run_at | datetime | null | When the job is next scheduled to run. null if the Scheduler is not running. |
GET /api/watchlist → WatchlistOut
Returns current readings for all eight healthcare watchlist tickers, each benchmarked against XLV.
| Field | Type | Description |
|---|---|---|
rows | list[WatchlistRowOut] | One entry per watchlist ticker. |
rows[].ticker | string | Stock ticker symbol. |
rows[].name | string | Company display name. |
rows[].sub | string | Sub-sector or brief description. |
rows[].price | float | null | Most recent closing price. null if price data is unavailable. |
rows[].target | float | Analyst price target. |
rows[].fwd_pe | string | Forward P/E ratio, formatted as a string. |
rows[].status | string | Qualitative status label. |
rows[].return_30d | float | null | Absolute 30-day return for this ticker. null if insufficient history. |
rows[].return_30d_relative | float | null | 30-day return of this ticker minus the 30-day return of XLV — the relative return. null if insufficient history. |
rows[].sparkline | list[float] | Recent closing prices for the inline sparkline chart. |
rows[].as_of | date | null | Date of the most recent price observation. |
benchmark | string | Always "XLV" — the healthcare sector ETF used as the relative-return benchmark. |
All endpoints return standard HTTP error responses.
| Endpoint | Status code | When it occurs |
|---|---|---|
| Any | 500 Internal Server Error | Unhandled exception in the route handler, typically a DuckDB read error or a data shape mismatch. Check the backend process logs. |
GET /api/metrics/{metric_id}/history | 404 Not Found | metric_id is not registered in the metric registry. The response body contains { "detail": "Unknown metric: <metric_id>" }. |
GET /api/metrics/{metric_id}/detail | 404 Not Found | Same as above — metric_id is not in the registry. |
GET /api/tier/{tier}/history | 404 Not Found | tier is not one of 1, 2, 3, 4. The response body contains { "detail": "Unknown tier: <tier>" }. |
Note that all other endpoints (/health, /api/dashboard, /api/sources, /api/scheduler, /api/watchlist) have no path parameters and therefore cannot produce 404 errors for bad identifiers. If the database file is missing or unreadable, those endpoints will return 500.
All examples assume the TIDE backend is running on its default port. The Makefile exposes the PORT environment variable if you need to change it.
Check that the backend is alive
curl http://localhost:8000/health
{ "status": "ok" }
Fetch the full dashboard payload
This is the same call the SvelteKit page server makes on every page load.
curl http://localhost:8000/api/dashboard | python3 -m json.tool | head -60
Expect a large JSON object with composite, tiers (four entries), and composite_history (up to 252 points). If no data has been ingested yet, composite will be null and tiers[].metrics will be empty arrays.
Read ingestion health for every metric
Useful for debugging why a card shows a stale reading.
curl http://localhost:8000/api/sources | python3 -m json.tool
Abridged example response:
{
"rows": [
{
"metric_id": "hy_spread",
"name": "HY Credit Spread",
"tier": 1,
"source": "FRED",
"source_kind": "fred",
"cadence": "daily",
"status": "live",
"last_observation": "2025-07-14",
"last_ingested_at": "2025-07-14T17:03:12",
"obs_count": 1842
},
{
"metric_id": "put_call",
"name": "Put/Call Ratio",
"tier": 4,
"source": "CBOE",
"source_kind": "cboe",
"cadence": "daily",
"status": "stub",
"last_observation": null,
"last_ingested_at": null,
"obs_count": 0
}
]
}
A "status": "stub" row means the metric is registered but has no ingest function yet. The four currently stubbed metrics are ici_etf_flows, buyback_yield, put_call, and uvol_dvol.
Get raw observation history for a single metric
curl "http://localhost:8000/api/metrics/hy_spread/history"
Limit to the last month by passing a since date:
curl "http://localhost:8000/api/metrics/hy_spread/history?since=2025-06-01"
{
"metric_id": "hy_spread",
"points": [
{ "ts": "2025-06-02", "value": 312.5 },
{ "ts": "2025-06-03", "value": 308.1 }
]
}
Get full metric detail including indicator and z-score series
curl http://localhost:8000/api/metrics/hy_spread/detail | python3 -m json.tool
The series array in the response contains the computed indicator value (raw level or yoy%) alongside the rolling z-score for each date — this is the same transformation the reading card uses, so you can reproduce the card's z-score from this data.
Get historical z-scores for a single tier
curl http://localhost:8000/api/tier/1/history
{
"tier": 1,
"name": "Macro Liquidity",
"tag": "The base · multi-year",
"points": [
{ "ts": "2024-07-15", "z": 0.42 },
{ "ts": "2024-07-16", "z": 0.45 }
],
"latest_z": 0.45
}
If tide-ingest backfill-composite has not been run yet, points will be an empty array and latest_z will be null.
Check scheduler job status
curl http://localhost:8000/api/scheduler | python3 -m json.tool
Abridged example when the Scheduler is running:
{
"jobs": [
{
"id": "daily_ingest",
"label": "Daily Ingest",
"description": "Runs tide-ingest for all live metrics after market close.",
"schedule_repr": "Mon–Fri 17:00 America/New_York",
"last_run_at": "2025-07-14T17:00:04",
"last_success_at": "2025-07-14T17:00:51",
"last_error": null,
"last_error_at": null,
"next_run_at": "2025-07-15T17:00:00"
}
]
}
If the Scheduler has never been started with make scheduler, all timestamp fields will be null.
Get the watchlist
curl http://localhost:8000/api/watchlist | python3 -m json.tool
Abridged example:
{
"rows": [
{
"ticker": "UNH",
"name": "UnitedHealth Group",
"sub": "Managed Care",
"price": 487.30,
"target": 560.0,
"fwd_pe": "14.2x",
"status": "Hold",
"return_30d": -0.032,
"return_30d_relative": -0.018,
"sparkline": [492.1, 490.5, 488.0, 487.3],
"as_of": "2025-07-14"
}
],
"benchmark": "XLV"
}
Base URL and CORS
The FastAPI backend is a separate process from the SvelteKit frontend. In development, CORS is enabled for http://localhost:5173 and http://127.0.0.1:5173 so that client-side fetches (for example, timeframe tab switches and history endpoint calls) work without a proxy. In production, SSR fetches go server-to-server and do not cross origins. All API methods are GET-only; there are no mutation endpoints.
The API is read-only by design
There are no POST, PUT, or DELETE endpoints. Data enters TIDE only through tide-ingest (run manually or by the Scheduler). The API reflects whatever is currently in the DuckDB database file.
Database isolation
The backend and the ingest process share only the DuckDB file — they do not share memory or a network connection. A wedged or slow tide-ingest run cannot take the API down, and a running API cannot interfere with an in-progress ingest.
Stale data is surfaced, not hidden
Every ReadingOut includes an as_of date. Because different data sources publish on different schedules (FRED series may lag by days or weeks; CFTC data publishes weekly), the as_of date on a metric card may be older than today's date. This is expected behavior — TIDE shows the last known value with its observation date rather than suppressing the reading. Use /api/sources to see last_observation for every metric in one place.
Stubbed metrics appear in API responses
The four stubbed metrics (ici_etf_flows, buyback_yield, put_call, uvol_dvol) are registered in the metric registry and therefore appear in /api/sources responses with "status": "stub". They do not appear in /api/dashboard tiers[].metrics because their compute_fn is None and they are skipped during the dashboard read loop. Similarly, /api/metrics/{metric_id}/detail will return a MetricDetailOut for a stubbed metric, but reading will be null and series will be empty.
Composite history requires a backfill step
The composite_history array in GET /api/dashboard and the points returned by GET /api/tier/{tier}/history are populated by running tide-ingest backfill-composite. If you have never run this command, both arrays will be empty and the 252-day history chart on the dashboard will not render. Run the backfill after your first ingest.
Z-score lookback window varies by metric
Z-scores default to a 3-year rolling window. The FRED series BAMLH0A0HYM2 (HY spread) is an exception: its zscore_years is set to 1 because the series was truncated to a 3-year window in April 2026, making a longer lookback misleading. The zscore_years field in MetricDetailOut tells you exactly which window each metric uses.
Composite z-score uses directional z, not raw z
The composite score shown in composite.z is the equal-weight average of the four tier averages, where each metric contributes its directional_z. Metrics with inverted conventions — where a lower raw value signals stress (for example, tight HY spreads are bullish, low VIX is bullish) — are sign-flipped before entering the average. This ensures all metrics push the composite in the same direction when conditions are stressed.
Yahoo Finance data does not use yfinance
If you inspect source calls in the ingest layer, Yahoo Finance data is fetched by hitting query1.finance.yahoo.com/v8/finance/chart/ directly over HTTP. The yfinance package is intentionally not a dependency, to avoid recurrent authentication and crumb-handling breakage.
TLS verification exception for CFTC
TLS certificate verification is disabled only for requests to cftc.gov. All other upstream HTTP calls — including FRED, Yahoo Finance, and any other source — use standard TLS verification.