tide
API reference

API Reference

FastAPI endpoint documentation


Description

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

Parameters vary by endpoint. The table below covers every endpoint in the API.

EndpointMethodPath parameterQuery parameterTypeRequiredDescription
GET /healthGETNo parameters.
GET /api/dashboardGETNo parameters. Returns the full dashboard payload in one call.
GET /api/metrics/{metric_id}/historyGETmetric_idstringYesID 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}/historyGETsincedate (YYYY-MM-DD)NoIf provided, only observations on or after this date are returned. Omit to receive the full history.
GET /api/metrics/{metric_id}/detailGETmetric_idstringYesID 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}/historyGETtierintegerYesTier number: 1, 2, 3, or 4.
GET /api/sourcesGETNo parameters. Returns ingestion health for every registered metric.
GET /api/schedulerGETNo parameters. Returns status for every configured cron job.
GET /api/watchlistGETNo parameters. Returns current readings for all eight healthcare watchlist tickers.

Returns

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/dashboardDashboardOut

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:

FieldTypeDescription
compositeCompositeOut | nullThe composite Capital Pressure reading. null if no metrics have data yet.
composite.zfloatEqual-weight average of the four tier z-score averages, using each metric's directional_z.
composite.z_labelstringHuman-readable label for the current z-score level (e.g. "Elevated").
composite.sign_classstringCSS class hint for coloring (e.g. "bear", "bull", "neutral").
composite.tallyTallyOutCounts of Bullish / Neutral / Bearish votes across all composite-eligible metrics.
composite.tally.bullintNumber of indicators voting Bullish.
composite.tally.neutralintNumber of indicators voting Neutral.
composite.tally.bearintNumber of indicators voting Bearish.
composite.metrics_totalintTotal number of indicators included in the composite tally.
tierslist[TierOut]One entry for each of the four tiers, always present (may have an empty metrics list if no data has been ingested).
tiers[].tierintTier number: 1–4.
tiers[].namestringTier display name (e.g. "Macro Liquidity").
tiers[].tagstringShort descriptor (e.g. "The base · multi-year").
tiers[].cadencestringTypical update frequency summary (e.g. "5 series · monthly–weekly cadence").
tiers[].avg_zfloat | nullSimple mean of the z-scores of all metrics in this tier that returned a reading. null if the tier has no readings.
tiers[].metricslist[ReadingOut]Per-metric cards for this tier. See ReadingOut below.
composite_historylist[CompositeHistoryPoint]Up to 252 business days of composite z-score history, used to draw the SVG history chart.
composite_history[].tsdateObservation date.
composite_history[].zfloatComposite z-score on that date.
composite_history[].spy_closefloat | nullSPY closing price on that date, for overlay. null if not yet populated.
composite_history[].tier_zsdict[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):

FieldTypeDescription
metric_idstringUnique registry identifier.
namestringDisplay name.
tierintTier number (1–4).
sourcestringData source label (e.g. "FRED", "CFTC").
unitstringUnit of the value (e.g. "bps", "%").
valuestringFormatted current value.
deltastringFormatted change from the previous period.
delta_classstringCSS class hint for the delta direction.
zfloatRolling z-score (3-year window by default; some metrics use 1-year — see notes).
z_labelstringHuman-readable z-score label.
z_classstringCSS class hint for the z-score level.
as_ofdateObservation date for this reading.
sparklinelist[float]Short recent price/value series for the inline sparkline chart.
voteVoteOutThe indicator's current vote.
vote.directionstring"bull", "neutral", or "bear".
vote.reasonstringOne-line explanation of why this vote was cast.
include_in_compositebooltrue 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.
descriptionstring | nullLonger text description from the metric registry.

GET /api/metrics/{metric_id}/historyHistoryOut

Returns the raw observation series for a single metric — the values exactly as stored in the observations table, without z-score transformation.

FieldTypeDescription
metric_idstringEchoes back the requested metric ID.
pointslist[HistoryPoint]Chronologically ordered list of observations.
points[].tsdateObservation date.
points[].valuefloatRaw observed value on that date.

GET /api/metrics/{metric_id}/detailMetricDetailOut

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.

FieldTypeDescription
metric_idstringUnique registry ID.
namestringDisplay name.
tierintTier number (1–4).
sourcestringData source label.
source_kindstringSource category (e.g. "fred", "yahoo", "cftc").
source_seriesstring | nullUpstream series identifier where applicable (e.g. a FRED series ID). null for sources that do not use a named series.
unitstringUnit of measurement.
cadencestringTypical publication frequency.
descriptionstring | nullLonger explanation from the metric registry.
direction_kindstringWhether a higher value is bullish or bearish (governs directional z sign).
indicator_kindstring"yoy" (year-over-year % change) or "level" (raw level).
indicator_windowintRolling window in days used to compute the indicator.
indicator_lagintLag in days applied before computing the indicator (used for yoy).
zscore_yearsintRolling z-score lookback in years. Default is 3; some metrics use 1 (see notes).
include_in_compositeboolWhether this metric's z-score enters the composite average.
readingReadingOut | nullThe latest rendered reading card. null for stubbed metrics or metrics with no data.
serieslist[DetailPoint]Full computed indicator + z-score series, matching what the reading card uses.
series[].tsdateObservation date.
series[].indicatorfloatComputed indicator value (yoy% or raw level).
series[].zfloat | nullRolling z-score on that date. null before the lookback window is filled.
obs_countintTotal number of raw observations stored for this metric.

GET /api/tier/{tier}/historyTierHistoryOut

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.

FieldTypeDescription
tierintTier number (1–4).
namestringTier display name.
tagstringShort tier descriptor.
pointslist[TierHistoryPoint]Chronologically ordered tier z-score history.
points[].tsdateObservation date.
points[].zfloatTier average z-score on that date.
latest_zfloat | nullMost recent z-score in points. null if no history has been backfilled yet.

GET /api/sourcesSourcesOut

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.

FieldTypeDescription
rowslist[SourceRow]One entry per registered metric, including stubbed metrics.
rows[].metric_idstringRegistry ID.
rows[].namestringDisplay name.
rows[].tierintTier number (1–4).
rows[].sourcestringData source label.
rows[].source_kindstringSource category.
rows[].cadencestringPublication frequency.
rows[].statusstring"live" if the metric has an ingest function; "stub" if it is registered but not yet fetching data.
rows[].last_observationdate | nullDate of the most recent stored observation. null if no data has been ingested.
rows[].last_ingested_atdatetime | nullTimestamp when the most recent observation was written to the database. null if no data has been ingested.
rows[].obs_countintTotal observation count stored for this metric.

GET /api/schedulerSchedulerOut

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.

FieldTypeDescription
jobslist[SchedulerJob]One entry per configured job.
jobs[].idstringJob identifier (matches the key used in scheduler_status).
jobs[].labelstringShort display name.
jobs[].descriptionstringWhat the job does.
jobs[].schedule_reprstringHuman-readable cron schedule string.
jobs[].last_run_atdatetime | nullWhen the job last started. null if the Scheduler has never run.
jobs[].last_success_atdatetime | nullWhen the job last completed without error. null if it has never succeeded.
jobs[].last_errorstring | nullError message from the most recent failed run. null if no errors have occurred.
jobs[].last_error_atdatetime | nullWhen the most recent error occurred. null if no errors have occurred.
jobs[].next_run_atdatetime | nullWhen the job is next scheduled to run. null if the Scheduler is not running.

GET /api/watchlistWatchlistOut

Returns current readings for all eight healthcare watchlist tickers, each benchmarked against XLV.

FieldTypeDescription
rowslist[WatchlistRowOut]One entry per watchlist ticker.
rows[].tickerstringStock ticker symbol.
rows[].namestringCompany display name.
rows[].substringSub-sector or brief description.
rows[].pricefloat | nullMost recent closing price. null if price data is unavailable.
rows[].targetfloatAnalyst price target.
rows[].fwd_pestringForward P/E ratio, formatted as a string.
rows[].statusstringQualitative status label.
rows[].return_30dfloat | nullAbsolute 30-day return for this ticker. null if insufficient history.
rows[].return_30d_relativefloat | null30-day return of this ticker minus the 30-day return of XLV — the relative return. null if insufficient history.
rows[].sparklinelist[float]Recent closing prices for the inline sparkline chart.
rows[].as_ofdate | nullDate of the most recent price observation.
benchmarkstringAlways "XLV" — the healthcare sector ETF used as the relative-return benchmark.

Errors

All endpoints return standard HTTP error responses.

EndpointStatus codeWhen it occurs
Any500 Internal Server ErrorUnhandled 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}/history404 Not Foundmetric_id is not registered in the metric registry. The response body contains { "detail": "Unknown metric: <metric_id>" }.
GET /api/metrics/{metric_id}/detail404 Not FoundSame as above — metric_id is not in the registry.
GET /api/tier/{tier}/history404 Not Foundtier 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.


Examples

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"
}

Notes

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.