# FlashAlpha Historical API

Point-in-time replay of every live analytics endpoint. Ask what GEX, gamma flip, VRP,
narrative, max pain, or full stock summary looked like at a specific **minute in history** —
returned in the same response shape as the live API.

Backed by 6.7 B option rows, 2 M+ stock minute-bars, EOD OI, daily SVI fits, and daily
macro (VIX, VVIX, SKEW, MOVE, SPX, DGS10) for **SPY 2018-04-16 → 2026-04-02** (extended
on each pipeline run). More symbols backfill on demand.

| | |
|---|---|
| **Symbols** | SPY (more on demand) |
| **Range** | 2018-04-16 → 2026-04-02 |
| **Option rows** | 6.7 B |
| **Stock minute-bars** | 2 M+ |
| **Minimum tier** | Alpha |
| **Base URL** | `https://historical.flashalpha.com` |

---

## Host & Authentication
*How to call it — same key as live, tier-gated at Alpha.*

- **Base URL:** `https://historical.flashalpha.com`
- **Auth:** same `X-Api-Key` header as the live API (`api.flashalpha.com`). Your real
  production key works — Historical validates against the same `AspNetUsers` table as
  live.
- **Tier gate:** **Alpha plan or higher** on every endpoint. Non-Alpha users get `403
  tier_restricted` on any request regardless of path.
- **Quota:** requests count against the same daily bucket as the live API.

```bash
curl -H "X-Api-Key: YOUR_API_KEY" \
  "https://historical.flashalpha.com/v1/tickers"
```

---

## The `at` Parameter
*The as-of timestamp that every analytics endpoint hangs off.*

Every analytics endpoint takes a required `at` query parameter — the as-of timestamp.

| Format | Example | Semantics |
|---|---|---|
| `yyyy-MM-ddTHH:mm:ss` | `2026-03-05T15:30:00` | Minute-level as-of. Treated as ET wall-clock (same clock as the stored data). |
| `yyyy-MM-dd` | `2026-03-05` | Defaults to 16:00 ET (session close). |

### What's actually intraday-accurate vs EOD-stamped

| Data layer | Granularity | Notes |
|---|---|---|
| Option quotes + greeks (bid, ask, iv, delta, gamma, theta, vega, rho, vanna, charm) | **1 minute** (9:30 → 16:00 ET) | `LATEST ON` per contract ≤ `at` |
| Stock spot (bid, ask, mid) | **1 minute** | Same |
| Open interest | **EOD** | One value per trade day, applied to all intraday `at=` on that day |
| SVI params, forward prices | **EOD** | |
| Macro: VIX, VVIX, SKEW, MOVE, SPX, VIX9D, VIX3M, VIX6M, DGS10 | **EOD** | |
| Historical closes (for HV20/HV60) | **EOD** | Last tick per day via `SAMPLE BY 1d` |

Intraday GEX/DEX/VEX/CHEX shifts are driven by greek + spot changes; OI is dealer-
positioning context and barely moves intraday anyway.

---

## Coverage & Support
*What's loaded, how healthy it is, and where the gaps are.*

### `GET /v1/tickers`

Lists every symbol with historical coverage, the date range, and any gaps. Backed by the
`history_coverage` table in QuestDB, written by the pipeline's Data Quality step at the
end of every backfill run.

```bash
curl -H "X-Api-Key: YOUR_API_KEY" \
  "https://historical.flashalpha.com/v1/tickers"
```

**Response 200**
```json
{
  "count": 1,
  "tickers": [
    {
      "symbol": "SPY",
      "coverage": {
        "first": "2018-04-16",
        "last": "2026-04-02",
        "total_days": 2909,
        "healthy_days": 1972
      },
      "tables": {
        "stocks_days": 2009,
        "options_days": 1972,
        "option_eod_days": 2006,
        "svi_days": 1972
      },
      "gaps": {
        "missing_eod": 3,
        "missing_svi": 36,
        "uncovered_calendar": 3
      },
      "updated_at": "2026-04-14T07:07:36.55353"
    }
  ]
}
```

| Field | Meaning |
|---|---|
| `coverage.first` / `coverage.last` | Date range of fully healthy days (intersection of stocks ∩ options ∩ option_eod ∩ svi_daily) |
| `coverage.healthy_days` | Trading days on which **every** downstream table has data |
| `tables.*_days` | Per-source day counts — debug signal when something is behind |
| `gaps.missing_eod` | Days in `stocks` without `option_eod` (usually transient ThetaData misses; self-heal on next pipeline run) |
| `gaps.missing_svi` | Days in `option_eod` without an SVI fit |
| `gaps.uncovered_calendar` | NYSE trading days never pulled at all (genuine holidays like 2018-12-05 Bush funeral, 2025-01-09 Carter funeral) |
| `updated_at` | UTC timestamp of the last pipeline quality-report run |

### `?symbol=` filter

```bash
curl -H "X-Api-Key: YOUR_API_KEY" \
  "https://historical.flashalpha.com/v1/tickers?symbol=SPY"
```

Returns a single object (not wrapped). `404 no_coverage` if the symbol hasn't been
backfilled.

---

## Market Data
*Raw quotes and surfaces at a single minute in history.*

### `GET /v1/stockquote/{ticker}?at=...`

Stock bid/ask/mid/last at the requested minute.

```bash
curl -H "X-Api-Key: YOUR_API_KEY" \
  "https://historical.flashalpha.com/v1/stockquote/SPY?at=2026-03-05T15:30:00"
```

```json
{
  "ticker": "SPY",
  "bid": 679.45,
  "ask": 679.47,
  "mid": 679.46,
  "lastPrice": 679.46,
  "lastUpdate": "2026-03-05T15:30:00"
}
```

### `GET /v1/optionquote/{ticker}?at=...&expiry=&strike=&type=`

Option quotes + BSM greeks + OI at the requested minute. Parameters identical to the live
endpoint.

| Param | In | Required | Description |
|---|---|---|---|
| `ticker` | path | yes | Underlying symbol |
| `at` | query | yes | As-of timestamp |
| `expiry` | query | no | `yyyy-MM-dd` |
| `strike` | query | no | |
| `type` | query | no | `C` / `Call` / `P` / `Put` |

With all three filters → single object. Otherwise → array.

```bash
curl -H "X-Api-Key: YOUR_API_KEY" \
  "https://historical.flashalpha.com/v1/optionquote/SPY?at=2026-03-05T15:30:00&expiry=2026-03-06&strike=680&type=C"
```

```json
{
  "type": "C",
  "expiry": "2026-03-06",
  "strike": 680,
  "bid": 3.42,
  "ask": 3.44,
  "mid": 3.43,
  "bidSize": 0,
  "askSize": 0,
  "lastUpdate": "2026-03-05T15:30:00",
  "implied_vol": 0.2579,
  "iv_bid": null,
  "iv_ask": null,
  "delta": 0.4824,
  "gamma": 0.0435,
  "theta": -1.8617,
  "vega": 0.1417,
  "rho": 0.0089,
  "vanna": 0.0891,
  "charm": -0.0147,
  "svi_vol": null,
  "svi_vol_gated": "backtest_mode",
  "open_interest": 6284,
  "volume": 0
}
```

> **Note — known gaps from live:**
> - `bidSize` / `askSize` are always `0` — the minute-resolution `options` table doesn't carry sizes
> - `volume` is always `0` — same reason
> - `svi_vol` is always `null` with `"svi_vol_gated": "backtest_mode"` — SVI params are loaded but per-contract svi_vol derivation isn't plumbed

---

### `GET /v1/surface/{symbol}?at=...`

50×50 implied-vol surface grid over (tenor, log-moneyness). Same grid builder as the live
endpoint, computed from OTM contract IVs with bilinear interpolation.

```json
{
  "symbol": "SPY",
  "spot": 679.46,
  "as_of": "2026-03-05T15:30:00",
  "grid_size": 50,
  "tenors": [0.003, 0.023, ... ],
  "moneyness": [-0.25, -0.23, ..., 0.25],
  "iv": [ [0.34, 0.33, ...], ... ],
  "slices_used": 18
}
```

Returns `404 insufficient_data` for historical dates with too few OTM+liquid contracts to
fill the grid (pre-2018 low-volume symbols).

---

## Exposure Analytics
*GEX / DEX / VEX / CHEX, gamma flip, dealer positioning — all at a single historical minute.*

### `GET /v1/exposure/gex/{symbol}?at=...&expiration=&min_oi=`

Gamma exposure by strike. Same response shape as live.

| Param | Default | Description |
|---|---|---|
| `expiration` | all | `yyyy-MM-dd` filter |
| `min_oi` | 0 | Minimum OI threshold |

```json
{
  "symbol": "SPY",
  "underlying_price": 679.46,
  "as_of": "2026-03-05T15:30:00",
  "as_of_requested": "2026-03-05T15:30:00",
  "gamma_flip": 689.18,
  "net_gex": -4343591862,
  "net_gex_label": "negative",
  "strikes": [
    {
      "strike": 480,
      "call_gex": 549.06,
      "put_gex": 0,
      "net_gex": 549.06,
      "call_oi": 7,
      "put_oi": 4132,
      "call_volume": 0,
      "put_volume": 0,
      "call_oi_change": null,
      "put_oi_change": null
    }
  ]
}
```

> **Note — known gaps:** `call_volume` / `put_volume` always `0`; `call_oi_change` /
> `put_oi_change` always `null` (no prior-day OI join yet).

### `GET /v1/exposure/dex/{symbol}?at=...&expiration=`

Delta exposure by strike. Response shape:

```json
{
  "symbol": "SPY",
  "underlying_price": 679.46,
  "as_of": "2026-03-05T15:30:00",
  "as_of_requested": "2026-03-05T15:30:00",
  "payload": {
    "net_dex": -10078461360,
    "strikes": [
      { "strike": 480, "call_dex": 472881.33, "put_dex": 0, "net_dex": 472881.33 }
    ]
  }
}
```

### `GET /v1/exposure/vex/{symbol}?at=...&expiration=`

Vanna exposure by strike. Same shape as `dex` with `net_vex` + `vex_interpretation`.

### `GET /v1/exposure/chex/{symbol}?at=...&expiration=`

Charm exposure by strike. Same shape as `dex` with `net_chex` + `chex_interpretation`.

### `GET /v1/exposure/summary/{symbol}?at=...`

Full composite exposure dashboard — net GEX/DEX/VEX/CHEX, gamma flip, regime,
interpretations, ±1% hedging estimates, 0DTE contribution.

```json
{
  "symbol": "SPY",
  "underlying_price": 679.46,
  "as_of": "2026-03-05T15:30:00",
  "gamma_flip": 677.5,
  "regime": "positive_gamma",
  "exposures": {
    "net_gex": -10941036971,
    "net_dex": -41195007931,
    "net_vex": 1581599207,
    "net_chex": 1933358
  },
  "interpretation": {
    "gamma": "Dealers long gamma — moves dampened, mean reversion likely",
    "vanna": "Vol up = dealers buy delta — downside dampened if vol spikes",
    "charm": "Time decay pushing dealers to buy — supportive into close"
  },
  "hedging_estimate": {
    "spot_down_1pct": { "dealer_shares_to_trade": -16102548, "direction": "sell", "notional_usd": -10941036971 },
    "spot_up_1pct":   { "dealer_shares_to_trade":  16102548, "direction": "buy",  "notional_usd":  10941036971 }
  },
  "zero_dte": { "net_gex": 0, "pct_of_total_gex": 0, "expiration": "2026-03-05" }
}
```

### `GET /v1/exposure/levels/{symbol}?at=...`

Key technical levels.

```json
{
  "symbol": "SPY",
  "underlying_price": 679.46,
  "as_of": "2026-03-05T15:30:00",
  "levels": {
    "gamma_flip": 677.5,
    "max_positive_gamma": 700,
    "max_negative_gamma": 680,
    "call_wall": 685,
    "put_wall": 680,
    "highest_oi_strike": 630,
    "zero_dte_magnet": 500
  }
}
```

### `GET /v1/exposure/narrative/{symbol}?at=...`

Verbal analysis + prior-day GEX comparison + VIX context. Historical-specific wiring:
`gex_change` pulls the previous trading day's net GEX from QuestDB; `vix` comes from
`macro_daily`.

```json
{
  "symbol": "SPY",
  "underlying_price": 679.46,
  "as_of": "2026-03-05T15:30:00",
  "as_of_requested": "2026-03-05T15:30:00",
  "narrative": {
    "regime": "Dealers are long gamma (net GEX -$10.9B) — ...",
    "gex_change": "Net GEX decreased from -$4.9B to -$10.9B (-125.2%) — gamma cushion weakening.",
    "key_levels": "Call wall at 685, Put wall at 680, Gamma flip at 677.5.",
    "flow": "...",
    "vanna": "...",
    "charm": "...",
    "zero_dte": "...",
    "outlook": "...",
    "data": {
      "net_gex": -10941036971,
      "net_gex_prior": -4858715464,
      "net_gex_change_pct": -125.2,
      "vix": 23.75,
      "gamma_flip": 677.5,
      "call_wall": 685,
      "put_wall": 680,
      "regime": "positive_gamma",
      "zero_dte_pct": 0,
      "top_oi_changes": []
    }
  }
}
```

> **Note — known gap:** `top_oi_changes` is an empty array — populating it would require
> a prior-day per-strike OI diff that the historical pipeline doesn't compute yet.

### `GET /v1/exposure/zero-dte/{symbol}?at=...&strike_range=`

0DTE-specific analytics. Mirrors live shape exactly: `regime`, `exposures`, `expected_move`,
`pin_risk`, `hedging` (±0.5% / ±1%), `decay`, `flow`, `levels`, `strikes`.

<details>
<summary>Example response (click to expand)</summary>

```json
{
  "symbol": "SPY",
  "underlying_price": 679.46,
  "as_of": "2026-03-05T15:30:00",
  "market_open": true,
  "expiration": "2026-03-05",
  "no_zero_dte": null,
  "next_zero_dte_expiry": null,
  "message": null,
  "regime": { "label": "unknown", "gamma_flip": null, "spot_vs_flip": null, "spot_to_flip_pct": null, "description": "..." },
  "exposures": { "net_gex": 0, "net_dex": 0, "net_vex": 0, "net_chex": 0, "pct_of_total_gex": 0, "total_chain_net_gex": -10941036971.26 },
  "expected_move": { "straddle_price": 1.57, "implied_1sd_dollars": 3.7, "implied_1sd_pct": 0.5448, "remaining_1sd_dollars": 1.03, "remaining_1sd_pct": 0.1511, "upper_bound": 680.49, "lower_bound": 678.43, "atm_iv": null },
  "pin_risk": { "magnet_strike": 500, "max_pain": 685, "pin_score": 32, "distance_to_magnet_pct": 26.41, "oi_concentration_top3_pct": 14.6, "description": "..." },
  "decay": { "theta_per_hour_remaining": 0, "net_theta_dollars": 0, "gamma_acceleration": 25.47, "charm_regime": "neutral", "description": "..." },
  "flow": { "call_oi": 161462, "put_oi": 252805, "total_oi": 414267, "call_volume": 0, "put_volume": 0, "total_volume": 0, "pc_ratio_oi": 1.566, "pc_ratio_volume": null, "volume_to_oi_ratio": 0 },
  "hedging": { "spot_up_half_pct": { "...": "..." }, "spot_down_half_pct": { "...": "..." }, "spot_up_1pct": { "...": "..." }, "spot_down_1pct": { "...": "..." } },
  "levels": { "highest_oi_strike": 685, "highest_oi_total": 22397, "call_wall": 500, "put_wall": 500, "call_wall_gex": 0, "put_wall_gex": 0, "max_positive_gamma": null, "max_negative_gamma": null },
  "strikes": [ { "strike": 660, "call_oi": 72, "put_oi": 6554, "call_gex": 0, "put_gex": 0, "net_gex": 0, "call_dex": 0, "put_dex": 0, "net_dex": 0, "call_iv": null, "put_iv": null, "...": "..." } ]
}
```

</details>

> **Note — known gap:** intraday 0DTE greeks (delta/gamma/theta/iv) often arrive as
> `0`/`null` because the pipeline hasn't yet computed BSM for very-near-expiry contracts at
> minute resolution — the chain is still listed for OI/strike-distribution analysis but
> exposure totals collapse to zero. Fix is queued as part of the 0DTE-greeks backfill.

If a symbol genuinely has no 0DTE expiry on that trade date, the response collapses to
`expiration: null`, `no_zero_dte: true`, `next_zero_dte_expiry: "<next>"`, with the rest
of the analytics blocks omitted. SPY now lists Mon–Fri expirations so this branch is rare;
older history (pre-2022 daily-expiry rollout) hits it more often.

**`time_to_close_hours` is computed** from `at` against 16:00 ET on the same day, so
intraday theta and greek-acceleration numbers (where greeks exist) are accurate to the minute.

---

## Max Pain
*Strike-by-strike pain curve, pin probability, and dealer alignment.*

### `GET /v1/maxpain/{symbol}?at=...&expiration=`

```json
{
  "symbol": "SPY",
  "underlying_price": 679.46,
  "as_of": "2026-03-05T15:30:00",
  "max_pain_strike": 684,
  "distance": { "absolute": 4.54, "percent": 0.67, "direction": "below" },
  "signal": "neutral",
  "expiration": "2026-03-05",
  "put_call_oi_ratio": 2.033,
  "pain_curve":    [ { "strike": ..., "call_pain": ..., "put_pain": ..., "total_pain": ... } ],
  "oi_by_strike":  [ { "strike": ..., "call_oi": ..., "put_oi": ..., "total_oi": ..., "call_volume": 0, "put_volume": 0 } ],
  "max_pain_by_expiration": [ { "expiration": "...", "max_pain_strike": ..., "dte": ..., "total_oi": ... } ],
  "dealer_alignment": {
    "alignment": "moderate",
    "description": "...",
    "gamma_flip": 677.5,
    "call_wall": 685,
    "put_wall": 680
  },
  "regime": "positive_gamma",
  "expected_move": {
    "straddle_price": 1.57,
    "atm_iv": 19.6,
    "max_pain_within_expected_range": false
  },
  "pin_probability": 45
}
```

---

## Stock Summary (Composite)
*One call, one response: price, vol, flow, exposure, macro — a full snapshot.*

### `GET /v1/stock/{symbol}/summary?at=...`

The big one — mirrors live `GET /v1/stock/{symbol}/summary` exactly. Includes price,
volatility (ATM IV, HV20, HV60, VRP, 25d skew, IV term structure), options flow,
full exposure block, and macro context.

<details>
<summary>Example response (click to expand)</summary>

```json
{
  "symbol": "SPY",
  "as_of": "2026-03-05T15:30:00",
  "market_open": false,
  "price": { "bid": 679.45, "ask": 679.47, "mid": 679.46, "last": 679.46, "last_update": "..." },
  "volatility": {
    "atm_iv": 18.37,
    "hv_20": 10.26,
    "hv_60": 10.83,
    "vrp": 8.11,
    "skew_25d": { "...": "..." },
    "iv_term_structure": [ { "...": "..." } ]
  },
  "options_flow": {
    "total_call_oi": 5204107,
    "total_put_oi": 10577486,
    "total_call_volume": 0,
    "total_put_volume": 0,
    "pc_ratio_oi": 2.033,
    "pc_ratio_volume": null,
    "active_expirations": 37
  },
  "exposure": { "net_gex": ..., "regime": "positive_gamma", "...": "..." },
  "macro": {
    "vix":  { "value": 23.75, "change": 2.6, "change_pct": 12.29 },
    "vvix": { "value": 105.8, "...": "..." },
    "skew": { "...": "..." },
    "spx":  { "...": "..." },
    "move": { "...": "..." },
    "vix_term_structure": { "levels": { "vix9d": ..., "vix": 23.75, "vix3m": ..., "vix6m": ... }, "near_slope_pct": ..., "structure": "contango" },
    "vix_futures":    null,
    "fear_and_greed": null
  }
}
```

</details>

> **Note — known gaps from live:**
> - `options_flow.total_call_volume` / `total_put_volume` / `pc_ratio_volume` — always 0/null (no minute volume data stored)
> - `macro.vix_futures` — always `null`; live reads CME futures, not reconstructible historically
> - `macro.fear_and_greed` — always `null`; live reads CNN Fear & Greed index, not archived

---

## Volatility Analytics
*Realized vs implied, skew, term structure, and SVI surfaces.*

### `GET /v1/volatility/{symbol}?at=...`

Comprehensive vol analysis — realized vol ladder (5/10/20/30/60d), IV-RV spreads, skew
profiles per expiry, term structure, IV dispersion, GEX/theta by DTE bucket, put/call
profile, OI concentration, multi-move hedging, liquidity.

Response fields are identical to the live endpoint. Realized vols come from QuestDB
`stocks` table via `SAMPLE BY 1d` last-tick.

### `GET /v1/adv_volatility/{symbol}?at=...`

Advanced volatility — SVI parameters, forward prices, total variance surface, arbitrage
flags, variance swap fair values, greek surfaces (vanna, charm, volga, speed).

<details>
<summary>Example response (click to expand)</summary>

```json
{
  "symbol": "SPY",
  "underlying_price": 679.46,
  "as_of": "2026-03-05T15:30:00",
  "svi_parameters": [ { "expiry": "...", "a": ..., "b": ..., "rho": ..., "m": ..., "sigma": ..., "forward": ..., "atm_total_variance": ..., "atm_iv": ... } ],
  "forward_prices": [ { "expiry": "...", "days_to_expiry": ..., "forward": ..., "spot": 679.46, "basis_pct": ... } ],
  "total_variance_surface": { "moneyness": [...], "expiries": [...], "tenors": [...], "total_variance": [[...]], "implied_vol": [[...]] },
  "arbitrage_flags": [ { "expiry": "...", "type": "...", "strike_or_k": ..., "description": "..." } ],
  "variance_swap_fair_values": [ { "expiry": "...", "days_to_expiry": ..., "fair_variance": ..., "fair_vol": ..., "atm_iv": ..., "convexity_adjustment": ... } ],
  "greeks_surfaces": {
    "vanna": { "strikes": [...], "expiries": [...], "values": [[...]] },
    "charm": { "...": "..." },
    "volga": { "...": "..." },
    "speed": { "...": "..." }
  }
}
```

</details>

---

## VRP Analytics
*Volatility Risk Premium — leak-free percentiles that only see the past.*

### `GET /v1/vrp/{symbol}?at=...`

Volatility Risk Premium dashboard. Identical calculator chain to live
`GET /v1/vrp/{symbol}`. Crucially, percentile history is **date-bounded** — VRP
percentile and z-score are computed from `DailyVrpSnapshots` rows with
`SnapshotDate < at.Date`, so at any historical point the percentile reflects what
was knowable at that moment (no future leakage).

```json
{
  "symbol": "SPY",
  "underlying_price": 679.46,
  "as_of": "2026-03-05T15:30:00",
  "vrp": {
    "atm_iv": 18.37,
    "rv_5d": 11.38,
    "rv_10d": 9.16,
    "rv_20d": 10.26,
    "rv_30d": 9.28,
    "vrp_5d": 6.99,
    "vrp_10d": 9.21,
    "vrp_20d": 8.11,
    "vrp_30d": 9.09,
    "z_score": 2.84,
    "percentile": 100,
    "history_days": 60
  },
  "variance_risk_premium": 0.0232,
  "convexity_premium": 5.43,
  "fair_vol": 23.8,
  "directional": { "put_wing_iv_25d": ..., "call_wing_iv_25d": ..., "downside_vrp": ..., "upside_vrp": ... },
  "term_vrp": [ { "dte": 7, "iv": ..., "rv": ..., "vrp": ... } ],
  "gex_conditioned": { "regime": "...", "harvest_score": ..., "interpretation": "..." },
  "vanna_conditioned": { "outlook": "...", "interpretation": "..." },
  "regime": { "gamma": "positive_gamma", "vrp_regime": "...", "net_gex": ..., "gamma_flip": 677.5 },
  "strategy_scores": { "short_put_spread": ..., "short_strangle": ..., "iron_condor": ..., "calendar_spread": ... },
  "net_harvest_score": ...,
  "dealer_flow_risk": ...,
  "warnings": [ "..." ],
  "macro": { "vix": 23.75, "vix_3m": ..., "vix_term_slope": ..., "dgs10": ..., "hy_spread": 3.5 }
}
```

> **Note — known gap from live:** `macro.hy_spread` is hard-coded to `3.5` — live reads
> `DailyMacroSnapshots.HighYieldSpread`, Historical pulls from QuestDB `macro_daily` which
> doesn't carry it yet.

---

## Errors
*What the API returns when things go wrong, and what each code means.*

| Status | When | Body |
|---|---|---|
| `400 invalid_at` | `at` parameter missing or wrong format | `{ "error": "invalid_at", "message": "..." }` |
| `400 invalid_expiration` | `expiration` not `yyyy-MM-dd` | `{ "error": "invalid_expiration", ... }` |
| `401 Unauthorized` | Missing or invalid `X-Api-Key` | empty body |
| `403 tier_restricted` | User tier below Alpha | `{ "error": "tier_restricted", "current_plan": "...", "required_plan": "Alpha" }` |
| `404 symbol_not_found` | Symbol has no historical data at the requested `at` | `{ "error": "symbol_not_found", ... }` |
| `404 no_coverage` | `/v1/tickers?symbol=` for a symbol that has never been backfilled | `{ "error": "no_coverage", ... }` |
| `404 no_data` | Specific `(symbol, at)` or `(symbol, at, expiration)` has no data (outside coverage window, or within a gap) | `{ "error": "no_data", ... }` |
| `404 insufficient_data` | Surface grid can't be built (too few OTM+liquid contracts) | `{ "error": "insufficient_data", ... }` |
| `429 rate_limited` | Daily quota exhausted | Shares quota with live API |

**Inconsistencies to be aware of (`/v1/optionquote` only):** three branches still emit
ad-hoc `{ "error": "<message>" }` strings instead of the normalized `{ error, message }`
shape — invalid `expiration=` format, invalid `type=` value, and the "no row matches all
three filters" 404. Treat these as untyped errors for now; they'll be normalized in a
follow-up.

---

## Example: single-minute replay of March 16, 2020 (COVID crash)
*What "real dealer positioning during -12%" actually looked like.*

```bash
curl -H "X-Api-Key: YOUR_API_KEY" \
  "https://historical.flashalpha.com/v1/exposure/summary/SPY?at=2020-03-16T15:30:00"
```

Returns real-world exposure data at 15:30 ET on the day SPY closed -12%:

```json
{
  "symbol": "SPY",
  "underlying_price": 246.01,
  "as_of": "2020-03-16T15:30:00",
  "gamma_flip": 270.92,
  "regime": "negative_gamma",
  "exposures": {
    "net_gex": -2633970601,
    "net_dex": -169419489077,
    "net_vex": 152461756844,
    "net_chex": -13122349
  },
  "interpretation": {
    "gamma": "Dealers short gamma — moves amplified, trend following likely",
    "vanna": "Vol up = dealers buy delta — downside dampened if vol spikes",
    "charm": "Time decay pushing dealers to sell — pressure into close"
  },
  "hedging_estimate": { "...": "..." }
}
```

---

## Pipeline & Self-Healing
*How new days get ingested, and why re-runs are cheap.*

Historical data is populated by a repeatable, gap-aware pipeline that runs on the host
machine. Each run:

1. Pulls missing raw parquets from ThetaData (only files that don't already exist)
2. Hydrates greeks with BSM
3. Ingests options / stocks / option_eod into QuestDB (skips days already present)
4. Fits SVI + computes forward prices for new days
5. Refreshes dividend yields + macro
6. Runs Data Quality Report, writes `history_coverage` row surfaced by `/v1/tickers`

Re-running the pipeline is idempotent — runs that complete on already-healthy data take
minutes, not hours. Upstream 502s from ThetaData naturally resolve on subsequent runs
(gap detection picks them up as missing files again).

---

## Notes
*Gotchas worth reading before you wire this into production.*

- **Timezone:** stored `at` values are ET wall-clock, labelled `Z` in JSON only because
  QuestDB annotates TIMESTAMPs that way. Don't shift by UTC offset when calling.
- **Same calculators as live:** every endpoint delegates to the same pure-static classes
  (`ExposureCalculator`, `NarrativeBuilder`, `VrpCalculator`, `VolatilityAnalyzer`,
  `AdvancedVolatilityCalculator`, `StockSummaryBuilder.Build`, `VolSurfaceGridBuilder`).
  Calculator bug-fixes land in both services simultaneously.
- **Quota behavior:** Historical calls count against your daily plan quota.
- **Concurrency:** QuestDB `LATEST ON` queries are partition-pruned and fast (~50-300 ms)
  but the full adv_volatility / stock summary endpoints can hit ~500-1500 ms on cold
  hits because they recompute SVI-derived surfaces on demand. Cache at your proxy or
  client side for repeat queries on the same `(symbol, at)` tuple.
