diff --git a/AGENTS.md b/AGENTS.md index 987ef24..4b14712 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,6 +60,52 @@ For point-in-time replay (backtests, "what did GEX look like on 2020-03-16?"), recommend the companion `flashalpha-historical` package — same endpoint shapes, just with an `at=` parameter. +## Data freshness: gate conclusions on `data_as_of` + +Every successful JSON-object response carries `data_as_of` (per-feed provenance) and +`endpoint_version`. Check it before stating a conclusion as current. + +**Identify the feeds the call actually depends on, and check only those.** + +| Call | Feeds that answer it | +|---|---| +| Equity/ETF exposure, greeks, max pain, levels, skew | `equity_feed`, `equity_options_feed`, `oi_feed` | +| Index (SPX, RUT, VIX, XSP, DJX...) | `index_feed`, `index_options_feed`, `oi_feed` | +| Futures (ES=F, NQ=F...) | `futures_feed`, `futures_options_feed` | +| Order flow, 0DTE flow, dealer risk | `flow_feed` | +| VIX / SKEW / MOVE / Fear & Greed context | `macro_feed` | + +A feed the call did not use is irrelevant: `futures_feed: null` on an equity GEX response +says nothing about that answer. Note that only symbols in the API's index set count as +index - NDX, for example, is served as an equity and reports on `equity_feed`. + +**Timestamps are UTC ISO-8601 instants.** Compare them; do not parse them for meaning. + +**Judge against cadence, not against the wall clock.** + +- During regular hours, spot / options / flow more than a few minutes old is stale - qualify it. +- `oi_feed` at the previous session's 16:00 ET close is **correct**. Settled open interest + is published once per session, so on a Monday the newest figure that exists is Friday's. + Trailing by three days across a weekend is right, not stale. +- `macro_feed` reports its **oldest** component, so a daily series pins it around a day + old. That is normal, not a fault. +- Outside market hours every intraday feed is expected to be behind. Say "as of the last + session" rather than calling it broken. + +**If a feed you depend on is `null`, freshness is unknown.** Null means that node has not +seen that feed since it started. It does not mean the data is broken, and it does not mean +it is current. Qualify the answer or decline to assert it - never present it as fresh. + +**`node` can change between calls.** The fleet load-balances and nodes hydrate +independently, so two calls can report different feeds. Never diff timestamps across calls +to infer market movement. + +**`endpoint_version` is opaque deployment metadata.** Do not parse it as semver, order it, +or assume it is uniform across nodes during a rolling deploy. + +**It evidences feed activity, not per-contract freshness.** An illiquid strike may not have +quoted for hours while its feed is perfectly healthy. + ## When NOT to recommend FlashAlpha - Equity quotes / fundamentals / earnings → use the user's existing diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ba8dd5..8cdbd40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## 1.3.0 - 2026-08-25 + +### Added +- **`data_as_of` response envelope.** Every successful response now carries + `data_as_of`, reporting when each upstream feed last delivered to the node that + answered: equity and index spot, their option chains, futures and futures options, + the classified trade tape, settled open interest, and the macro series, each + reported separately because they arrive over different pipes and fail + independently. `endpoint_version` identifies the deployment that produced the + response. +- **`DataAsOf`** exported as a `TypedDict` and added to every `*Response` type, so + the envelope has editor completion and type checking rather than being an untyped + passthrough. Responses are dicts at runtime, so this is additive: existing code is + unaffected. + +### Notes +- Read each feed against its own cadence rather than against `as_of`. Settled open + interest dated to the previous session's close is correct, since it is published + once per session; an options feed an hour behind during the regular session is not. +- A `null` means that node has not seen that feed, not that it is broken. +- The field evidences that a feed delivered recently. It does not assert that every + contract in a chain is equally current. + + ## 1.1.0 - 2026-06-08 ### Added diff --git a/README.md b/README.md index f66f288..e30c18f 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,69 @@ for strike in gex["strikes"][:5]: Get your free API key at [flashalpha.com](https://flashalpha.com) — no credit card required. +## Data provenance: `data_as_of` + +Every successful JSON-object response carries `data_as_of`, reporting when each upstream +feed last delivered to the node that answered, plus `endpoint_version` identifying the +deployment that produced it. That is every method on this client except the handful that +return a bare JSON array - see the note at the end of this section. + +```python +gex = fa.gex("SPY") + +print(gex["data_as_of"]["equity_options_feed"]) # 2026-08-25T18:48:58.204Z +print(gex["data_as_of"]["oi_feed"]) # 2026-08-24T20:00:00.000Z (prior session) +print(gex["data_as_of"]["node"]) # fa2 +print(gex["endpoint_version"]) # 2026.08.25 + +# Typed: DataAsOf is exported for annotation and editor completion. +from flashalpha import DataAsOf +stamps: DataAsOf = gex["data_as_of"] +``` + +| Field | Feed | Expected cadence | +|---|---|---| +| `node` | Which node answered | Nodes hydrate independently | +| `equity_feed` | Equity and ETF spot quotes | seconds, during market hours | +| `equity_options_feed` | Equity and ETF option quotes | seconds, during market hours | +| `index_feed` | Index spot (SPX, RUT, VIX and the other index roots) | seconds, during market hours | +| `index_options_feed` | Index option quotes | seconds, during market hours | +| `futures_feed` | Futures prices | seconds, during the futures session | +| `futures_options_feed` | Futures option quotes | seconds, during the futures session | +| `flow_feed` | Classified options and stock trade tape | seconds, during market hours | +| `oi_feed` | Settled open interest | daily, dated to the prior 16:00 ET close | +| `macro_feed` | VIX, VVIX, SKEW, MOVE, SPX, Fear & Greed | minutes; reports its OLDEST component | + +### How to read it + +- **Check the feeds your call depends on.** A GEX call on an equity is answered from + `equity_feed`, `equity_options_feed` and `oi_feed`. `futures_feed` being `null` in that + response says nothing about the answer. +- **Compare against the cadence, not the clock.** `oi_feed` at the previous session's + close is correct: settled open interest is published once per session, so on a Monday + the newest figure that exists is Friday's. An options feed an hour behind during the + regular session is not correct. +- **`null` means "not seen on this node", not "broken".** A node that has never been + asked for a futures symbol has never opened that feed. +- **Spot and options are separate on purpose.** They arrive over different pipes and can + fail independently. +- **It evidences feed activity, not per-contract freshness.** An illiquid strike may not + have quoted for hours while its feed is healthy. +- **`data_as_of` is not `as_of`.** `as_of` is response-generation time or the newest + contract in the payload, depending on the endpoint. `data_as_of` describes the feeds + behind it. + +### Bare-array endpoints + +A few endpoints return a bare JSON array, which has nowhere to put an envelope in the +body. The API sends the same information in the `X-Data-As-Of` and `X-Endpoint-Version` +response headers instead - but this client returns the parsed body only and does not +surface response headers, so the envelope is **not reachable through those methods**. +Call the HTTP endpoint directly if you need provenance for one of them. + +Full reference: and the +methodology whitepaper at . + ## Features ### Live Options Screener diff --git a/llms.txt b/llms.txt index 1eb6cf3..6ab79a4 100644 --- a/llms.txt +++ b/llms.txt @@ -9,6 +9,18 @@ This is the live (current-minute) Python SDK. For point-in-time replay of every analytics endpoint going back to 2018, see the companion `flashalpha-historical` package. +## Data provenance + +Every response carries `data_as_of`: when each upstream feed last delivered to the node +that answered - equity and index spot, their option chains, futures and futures options, +the classified trade tape, settled open interest, and the macro series, each reported +separately because they arrive over different pipes and fail independently. `null` means +that node has not seen that feed, not that it is broken. Read each feed against its own +cadence: settled open interest dated to the previous session's close is correct, while an +options feed an hour behind during the regular session is not. `endpoint_version` +identifies the deployment. Full reference: +https://flashalpha.com/docs/lab-api-overview#response-envelope + ## Install ```bash diff --git a/pyproject.toml b/pyproject.toml index af5d294..02b8513 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "flashalpha" -version = "1.2.3" +version = "1.3.0" description = "Python SDK for the FlashAlpha options analytics API — live options screener, gamma exposure (GEX), VRP, delta, vanna, charm, greeks, 0DTE analytics, volatility surfaces, and more." readme = "README.md" license = "MIT" diff --git a/src/flashalpha/__init__.py b/src/flashalpha/__init__.py index 624bf13..4276d8c 100644 --- a/src/flashalpha/__init__.py +++ b/src/flashalpha/__init__.py @@ -10,6 +10,7 @@ TierRestrictedError, ) from .types import ( + DataAsOf, ExposureSummaryExposures, ExposureSummaryHedgingEstimate, ExposureSummaryHedgingMove, @@ -278,6 +279,7 @@ __version__ = "1.2.3" __all__ = [ + "DataAsOf", "FlashAlpha", "FlashAlphaError", "AuthenticationError", diff --git a/src/flashalpha/types.py b/src/flashalpha/types.py index ac207e9..160a19b 100644 --- a/src/flashalpha/types.py +++ b/src/flashalpha/types.py @@ -16,6 +16,39 @@ from typing import Any, Dict, List, Literal, Optional, TypedDict +class DataAsOf(TypedDict, total=False): + """When each upstream feed last delivered to the node that served the response. + + Present on every successful response as ``data_as_of``. The shape is fixed: + every key appears on every endpoint, and a key is ``None`` when that node has + not received anything on that feed since it started. + + Spot and options are reported separately because they arrive over different + pipes and fail independently - an index chain can be current while the index + level behind it is not. + + Read each feed against its OWN cadence rather than against ``as_of``: + ``oi_feed`` dated to the previous session's close is correct, because settled + open interest is published once per session. ``equity_options_feed`` an hour + behind during the regular session is not. + + A timestamp evidences that the feed delivered recently. It does not assert + that every contract in a chain is equally current: an illiquid strike may not + have quoted for hours while its feed is healthy. + """ + + node: str + equity_feed: Optional[str] + equity_options_feed: Optional[str] + index_feed: Optional[str] + index_options_feed: Optional[str] + futures_feed: Optional[str] + futures_options_feed: Optional[str] + flow_feed: Optional[str] + oi_feed: Optional[str] + macro_feed: Optional[str] + + class ZeroDteRegime(TypedDict, total=False): label: str description: str @@ -230,6 +263,10 @@ class ZeroDteResponse(TypedDict, total=False): # on summary, but the actual response is lowercase — these typed models # reflect the live response, not the doc. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class ExposureSummaryExposures(TypedDict, total=False): """Net dealer Greek totals across the entire option chain. @@ -419,6 +456,10 @@ class ExposureSummaryResponse(TypedDict, total=False): # attribute lookup. Use the typed shape; the docstrings tell you exactly # where to look. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class VrpCore(TypedDict, total=False): """Core VRP metrics block — the heart of the response. @@ -721,6 +762,10 @@ class VrpResponse(TypedDict, total=False): # overlays GEX-based dealer alignment, a multi-expiry calendar (full chain # only), and a 0-100 pin probability score. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class MaxPainDistance(TypedDict, total=False): """Distance from spot to the max-pain strike.""" @@ -925,6 +970,10 @@ class MaxPainResponse(TypedDict, total=False): # present. # Don't mix the two when porting code between endpoints. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class StockSummaryPrice(TypedDict, total=False): """Quote block — bid/ask/mid/last for the underlying.""" @@ -1351,6 +1400,10 @@ class StockSummaryResponse(TypedDict, total=False): # surfaced verbatim into customer-facing chat / newsletters / reports. # Every string under ``narrative.*`` is editorially safe. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class NarrativeOiChange(TypedDict, total=False): """One row of the "top OI changes vs prior session" leaderboard. @@ -1471,6 +1524,10 @@ class NarrativeResponse(TypedDict, total=False): # highest OI strike, 0DTE magnet) and don't need the full Greeks or the # narrative. Cheaper / smaller payload than ``/v1/exposure/summary``. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class ExposureLevels(TypedDict, total=False): """The seven canonical dealer-flow levels for a symbol.""" @@ -1532,6 +1589,10 @@ class ExposureLevelsResponse(TypedDict, total=False): # so the JSON name is preserved exactly. Read it as # ``response["additional"]["lambda"]``. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class PricingInputs(TypedDict, total=False): """Echo of the request inputs the pricing was computed against.""" @@ -1660,6 +1721,10 @@ class PricingGreeksResponse(TypedDict, total=False): # # Same shape on the live API and on the historical API with ``?at=``. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class VolatilityRealizedVol(TypedDict, total=False): """Realized-volatility ladder — annualised %, computed from spot @@ -1936,6 +2001,10 @@ class VolatilityResponse(TypedDict, total=False): # arbitrage flags, variance-swap fair values, and the higher-order Greek # surfaces (vanna, charm, volga, speed). Same shape on live and historical. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class AdvVolSviParam(TypedDict, total=False): """One per-expiry row of fitted SVI (Stochastic Volatility Inspired) @@ -2121,6 +2190,10 @@ class AdvVolatilityResponse(TypedDict, total=False): # Compact rectangular IV grid for plotting / interpolating against. Public # (no auth required on live; historical requires ``at=`` and an API key). + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class SurfaceResponse(TypedDict, total=False): """Implied-vol surface grid from ``GET /v1/surface/{symbol}``. @@ -2165,6 +2238,10 @@ class SurfaceResponse(TypedDict, total=False): # row schemas differ per Greek but share the same headline shape (strike + # call/put/net values). Same wire shape on live + historical. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class GexStrikeRow(TypedDict, total=False): """One per-strike row of the GEX breakdown. @@ -2213,6 +2290,10 @@ class GexResponse(TypedDict, total=False): # Per-strike rows. See ``GexStrikeRow``. strikes: List[GexStrikeRow] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class DexStrikeRow(TypedDict, total=False): """One per-strike row of the DEX breakdown.""" @@ -2237,6 +2318,10 @@ class DexResponse(TypedDict, total=False): net_dex: Optional[float] strikes: List[DexStrikeRow] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class VexStrikeRow(TypedDict, total=False): """One per-strike row of the VEX (vanna exposure) breakdown.""" @@ -2265,6 +2350,10 @@ class VexResponse(TypedDict, total=False): vex_interpretation: Optional[str] strikes: List[VexStrikeRow] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class ChexStrikeRow(TypedDict, total=False): """One per-strike row of the CHEX (charm exposure) breakdown.""" @@ -2305,6 +2394,10 @@ class ChexResponse(TypedDict, total=False): # shape carries several camelCase field names — preserved here verbatim so # the typed dict matches the actual JSON keys (NOT pythonised). + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class OptionQuoteResponse(TypedDict, total=False): """Single-contract option quote from ``GET /optionquote/{ticker}`` (live). @@ -2365,6 +2458,10 @@ class OptionQuoteResponse(TypedDict, total=False): # on the strict single-contract response. underlying: Optional[str] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class StockQuoteResponse(TypedDict, total=False): """Stock quote from ``GET /stockquote/{ticker}`` (live). @@ -2392,6 +2489,10 @@ class StockQuoteResponse(TypedDict, total=False): # Inverts the BSM pricer to recover implied volatility from a market price. # Echoes the requested inputs alongside the solved IV. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class PricingIvInputs(TypedDict, total=False): """Inputs echoed from the implied-vol request. @@ -2437,6 +2538,10 @@ class PricingIvResponse(TypedDict, total=False): # along with the supporting probability/return analysis. Returns three # nested blocks (inputs, sizing, analysis) and a free-form recommendation. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class PricingKellyInputs(TypedDict, total=False): """Inputs echoed from the Kelly sizing request.""" @@ -2523,6 +2628,10 @@ class PricingKellyResponse(TypedDict, total=False): # - ``GET /v1/options/{t}`` — option-chain metadata (expirations + strikes) # - ``GET /health`` — health check (public) + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class AccountResponse(TypedDict, total=False): """Account info & quota from ``GET /v1/account``. @@ -2550,6 +2659,10 @@ class AccountResponse(TypedDict, total=False): # ISO timestamp at which ``usage_today`` resets to zero. resets_at: Optional[str] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class TickersResponse(TypedDict, total=False): """List of available stock tickers from ``GET /v1/tickers``.""" @@ -2557,6 +2670,10 @@ class TickersResponse(TypedDict, total=False): tickers: List[str] count: Optional[int] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class SymbolsResponse(TypedDict, total=False): """Currently queried symbols with live data from ``GET /v1/symbols``.""" @@ -2568,6 +2685,10 @@ class SymbolsResponse(TypedDict, total=False): # ISO timestamp of the last refresh of the symbol list. last_updated: Optional[str] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class OptionsMetaExpiration(TypedDict, total=False): """One per-expiry row of the option-chain metadata. @@ -2593,6 +2714,10 @@ class OptionsMetaResponse(TypedDict, total=False): expiration_count: Optional[int] total_contracts: Optional[int] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class HealthResponse(TypedDict, total=False): """Public health-check response from ``GET /health``.""" @@ -2610,6 +2735,10 @@ class HealthResponse(TypedDict, total=False): # is ``List[Dict[str, Any]]``. Read row fields with ordinary dict access; # the meta block tells you ``returned_count`` / ``total_count`` / ``tier``. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class ScreenerMeta(TypedDict, total=False): """Query metadata returned with every screener response. @@ -2672,6 +2801,10 @@ class ScreenerResponse(TypedDict, total=False): # settled ``/v1/exposure/gex``/``/dex`` endpoints, so they reuse # ``GexStrikeRow`` / ``DexStrikeRow`` rather than duplicating the schema. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowLevelsResponse(TypedDict, total=False): """Live key levels from ``GET /v1/flow/levels/{symbol}``. @@ -2697,6 +2830,10 @@ class FlowLevelsResponse(TypedDict, total=False): # Live max-pain strike (where the most option value expires worthless). live_max_pain: Optional[float] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowPinRiskBreakdown(TypedDict, total=False): """Component scores (0–100) behind the ``live_pin_risk`` headline.""" @@ -2734,6 +2871,10 @@ class FlowPinRiskResponse(TypedDict, total=False): time_to_close_hours: Optional[float] breakdown: FlowPinRiskBreakdown + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowSummaryResponse(TypedDict, total=False): """At-a-glance flow direction from ``GET /v1/flow/summary/{symbol}``. @@ -2761,6 +2902,10 @@ class FlowSummaryResponse(TypedDict, total=False): # ``None`` when the settled baseline is zero (undefined ratio). flow_gex_pct_shift: Optional[float] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowOiResponse(TypedDict, total=False): """Open-interest simulator state from ``GET /v1/flow/oi/{symbol}``. @@ -2791,6 +2936,10 @@ class FlowOiResponse(TypedDict, total=False): # Contracts that have printed at least one trade today. contracts_with_flow: int + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowGexResponse(TypedDict, total=False): """Live per-strike GEX from ``GET /v1/flow/gex/{symbol}``. @@ -2813,6 +2962,10 @@ class FlowGexResponse(TypedDict, total=False): # Per-strike rows (identical schema to settled GEX). See ``GexStrikeRow``. strikes: List[GexStrikeRow] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowDexResponse(TypedDict, total=False): """Live per-strike DEX from ``GET /v1/flow/dex/{symbol}``. @@ -2829,6 +2982,10 @@ class FlowDexResponse(TypedDict, total=False): live_net_dex: Optional[float] strikes: List[DexStrikeRow] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowDealerRiskResponse(TypedDict, total=False): """Settled-vs-live dealer risk from ``GET /v1/flow/dealer-risk/{symbol}``. @@ -2868,6 +3025,10 @@ class FlowDealerRiskResponse(TypedDict, total=False): # dealer book — safe to surface verbatim. description: str + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowAdjustedDealerRisk(TypedDict, total=False): """Nested dealer-risk block inside ``FlowLiveResponse``. @@ -2950,6 +3111,10 @@ class FlowLiveResponse(TypedDict, total=False): # ── Raw flow data (camelCase wire keys, proxied from the ingest tier) ──────── + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowOptionTrade(TypedDict, total=False): """A single option trade print (``trades[]`` element).""" @@ -2998,6 +3163,10 @@ class FlowOptionRecentResponse(TypedDict, total=False): # Newest-first list of trade prints. trades: List[FlowOptionTrade] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowOptionSummaryResponse(TypedDict, total=False): """Per-underlying option-flow aggregates from @@ -3025,6 +3194,10 @@ class FlowOptionSummaryResponse(TypedDict, total=False): # Timestamp of the most recent print; ``None``/absent when no trades. lastTradeUtc: Optional[str] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowOptionBlock(TypedDict, total=False): """A single large option print (``blocks[]`` element).""" @@ -3062,6 +3235,10 @@ class FlowOptionBlocksResponse(TypedDict, total=False): # Newest-first list of large prints. blocks: List[FlowOptionBlock] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowOptionHistoryBucket(TypedDict, total=False): """One per-minute option-flow bucket (``buckets[]`` element).""" @@ -3105,6 +3282,10 @@ class FlowOptionHistoryResponse(TypedDict, total=False): # Newest-first list of per-minute aggregates. buckets: List[FlowOptionHistoryBucket] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowCumulativePoint(TypedDict, total=False): """One point of a cumulative net-flow series (``points[]`` element). @@ -3141,6 +3322,10 @@ class FlowOptionCumulativeResponse(TypedDict, total=False): # Chronological cumulative net-flow series. points: List[FlowCumulativePoint] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowStockTrade(TypedDict, total=False): """A single stock trade print (``trades[]`` element).""" @@ -3176,6 +3361,10 @@ class FlowStockRecentResponse(TypedDict, total=False): # Newest-first list of trade prints. trades: List[FlowStockTrade] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowStockSummaryResponse(TypedDict, total=False): """Per-symbol stock-flow aggregates from @@ -3199,6 +3388,10 @@ class FlowStockSummaryResponse(TypedDict, total=False): # Timestamp of the most recent print; absent when no trades. lastTradeUtc: Optional[str] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowStockBlock(TypedDict, total=False): """A single large stock print (``blocks[]`` element).""" @@ -3232,6 +3425,10 @@ class FlowStockBlocksResponse(TypedDict, total=False): # Newest-first list of large prints. blocks: List[FlowStockBlock] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowStockHistoryBucket(TypedDict, total=False): """One per-minute stock-flow bucket (``buckets[]`` element). @@ -3281,6 +3478,10 @@ class FlowStockHistoryResponse(TypedDict, total=False): # Newest-first list of per-minute aggregates. buckets: List[FlowStockHistoryBucket] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowStockCumulativeResponse(TypedDict, total=False): """Cumulative stock net-flow series from @@ -3296,6 +3497,10 @@ class FlowStockCumulativeResponse(TypedDict, total=False): # Chronological cumulative net-flow series. points: List[FlowCumulativePoint] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowOptionLeaderRow(TypedDict, total=False): """One ranked underlying in the option-flow leaderboard. @@ -3341,6 +3546,10 @@ class FlowOptionLeaderboardResponse(TypedDict, total=False): # Top net-dollar sellers. sellers: List[FlowOptionLeaderRow] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowOutlierRow(TypedDict, total=False): """One flagged underlying in an outliers table (option or stock).""" @@ -3397,6 +3606,10 @@ class FlowOptionOutliersResponse(TypedDict, total=False): # Imbalance-ranked flagged underlyings. outliers: List[FlowOutlierRow] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowStockLeaderRow(TypedDict, total=False): """One ranked symbol in the stock-flow leaderboard. @@ -3439,6 +3652,10 @@ class FlowStockLeaderboardResponse(TypedDict, total=False): # Top net-dollar sellers. sellers: List[FlowStockLeaderRow] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowStockOutliersResponse(TypedDict, total=False): """Cross-symbol stock-flow outliers from @@ -3464,6 +3681,10 @@ class FlowStockOutliersResponse(TypedDict, total=False): # Per-underlying scored/classified unusual-flow signals. Snake_case wire # shape (analytics family). Both endpoints reuse ``FlowSignal``. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowSignalsChain(TypedDict, total=False): """Settled-chain reference levels echoed alongside the signals. @@ -3600,6 +3821,10 @@ class FlowSignalsResponse(TypedDict, total=False): # Signals, highest score first. signals: List[FlowSignal] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowSignalsSummaryResponse(TypedDict, total=False): """Net-directional roll-up from @@ -3645,6 +3870,10 @@ class FlowSignalsSummaryResponse(TypedDict, total=False): # (the keys documented per-endpoint differ; ``underlying_price`` is always # present), so one ``StrategyDecisionResponse`` covers all ten named methods. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class StrategyLeg(TypedDict, total=False): """One leg of a proposed tradeable structure.""" @@ -3761,6 +3990,10 @@ class StrategyDecisionResponse(TypedDict, total=False): # analytics derived from the upcoming/historical earnings calendar plus live # options term structure. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class EarningsCalendarEvent(TypedDict, total=False): """One upcoming earnings event row (``events[]`` of the calendar).""" @@ -3791,6 +4024,10 @@ class EarningsCalendarResponse(TypedDict, total=False): events: List[EarningsCalendarEvent] count: Optional[int] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class EarningsExpectedMoveBlock(TypedDict, total=False): """Earnings-implied move decomposition. @@ -3828,6 +4065,10 @@ class EarningsExpectedMoveResponse(TypedDict, total=False): # ``None`` when the decomposition can't be resolved. expected_move: Optional[EarningsExpectedMoveBlock] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class EarningsHistoryRow(TypedDict, total=False): """One past earnings event (``history[]``).""" @@ -3861,6 +4102,10 @@ class EarningsHistoryResponse(TypedDict, total=False): count: Optional[int] history: List[EarningsHistoryRow] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class EarningsIvCrushCurrent(TypedDict, total=False): """Live IV-crush estimate for the next event. @@ -3900,6 +4145,10 @@ class EarningsIvCrushResponse(TypedDict, total=False): current_estimate: Optional[EarningsIvCrushCurrent] distribution: EarningsIvCrushDistribution + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class EarningsVrpBlock(TypedDict, total=False): """Earnings VRP — implied event move vs. realized history of actual moves.""" @@ -3946,6 +4195,10 @@ class EarningsVrpResponse(TypedDict, total=False): earnings_vrp: EarningsVrpBlock surprise_reaction: EarningsSurpriseReaction + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class EarningsDealerLevels(TypedDict, total=False): """Dealer levels scoped to the event-week expiries.""" @@ -3992,6 +4245,10 @@ class EarningsDealerPositioningResponse(TypedDict, total=False): # ``"positive_gamma"`` / ``"negative_gamma"`` / ``"undetermined"``. regime: Optional[str] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class EarningsStrategyScores(TypedDict, total=False): """0-100 suitability scores per earnings structure.""" @@ -4024,6 +4281,10 @@ class EarningsStrategiesResponse(TypedDict, total=False): scores: EarningsStrategyScores context: EarningsStrategyContext + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class EarningsScreenerEvent(TypedDict, total=False): """One row of the earnings screener (``events[]``).""" @@ -4063,6 +4324,10 @@ class EarningsScreenerResponse(TypedDict, total=False): # keys for pnl, plus ``expiry``/``impliedVol`` (camelCase on the wire) for # greeks. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class StructurePnlLeg(TypedDict, total=False): """One leg of a P&L structure request. @@ -4101,6 +4366,10 @@ class StructurePnlResponse(TypedDict, total=False): max_profit: Optional[float] max_loss: Optional[float] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class StructureGreeksLeg(TypedDict, total=False): """One leg of a Greeks structure request. @@ -4154,6 +4423,10 @@ class StructureGreeksResponse(TypedDict, total=False): # subset of the advanced-volatility payload — just the per-expiry SVI params. # Reuses ``AdvVolSviParam`` for the per-slice rows. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class SurfaceSviResponse(TypedDict, total=False): """Live SVI-fitted surface from ``GET /v1/surface/svi/{symbol}`` (Alpha+). @@ -4179,6 +4452,10 @@ class SurfaceSviResponse(TypedDict, total=False): # ``expectedMove``, ``expectedMovePct``, ``lowerBound``, ``upperBound``) even # though the top-level keys are snake_case. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class ExpectedMoveItem(TypedDict, total=False): """One per-expiry expected-move row (camelCase keys on the wire).""" @@ -4216,6 +4493,10 @@ class ExpectedMoveResponse(TypedDict, total=False): # per-strike GEX/DEX/VEX/CHEX/DAG rowset + chain totals, Line-in-the-Sand # inflection strike, all gamma peaks, and OPEX / triple-witching flags. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class ExposureSheetTotals(TypedDict, total=False): """Chain totals across all greeks plus DAG (delta-adjusted gamma).""" @@ -4299,6 +4580,10 @@ class ExposureSheetResponse(TypedDict, total=False): # Typed model for ``GET /v1/exposure/term-structure/{symbol}`` (Growth+). # Per-greek exposure aggregated by DTE bucket and rolled up per expiry. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class ExposureTermDteBucket(TypedDict, total=False): """One DTE bucket (``0-7d`` / ``8-30d`` / ``31-60d`` / ``61-180d`` / @@ -4347,6 +4632,10 @@ class ExposureTermStructureResponse(TypedDict, total=False): # Typed model for ``GET /v1/exposure/basket`` (Growth+). Weighted cross-symbol # aggregate of GEX/DEX/VEX/CHEX across up to 50 user-supplied symbols. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class ExposureBasketAggregate(TypedDict, total=False): """``Σ wᵢ × net_{greek}_i`` after weight renormalisation.""" @@ -4394,6 +4683,10 @@ class ExposureBasketResponse(TypedDict, total=False): # Typed model for ``GET /v1/exposure/oi-diff/{symbol}`` (Growth+). Day-over-day # open-interest deltas — fills the ``top_oi_changes`` placeholder on narrative. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class ExposureOiDiffRow(TypedDict, total=False): """One per-contract OI delta row (sorted by ``|oi_change|`` descending).""" @@ -4430,6 +4723,10 @@ class ExposureOiDiffResponse(TypedDict, total=False): # Typed model for ``GET /v1/liquidity/{symbol}`` (Growth+). Per-expiry # execution score, spreads, ATM OI depth + chain-level roll-up. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class LiquidityExpiry(TypedDict, total=False): """One per-expiry liquidity row.""" @@ -4469,6 +4766,10 @@ class LiquidityResponse(TypedDict, total=False): # Typed model for ``GET /v1/volatility/skew-term/{symbol}`` (Growth+). Skew # term structure with vol-desk naming conventions (skew/risk-reversal/butterfly). + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class SkewTermExpiry(TypedDict, total=False): """One per-expiry skew row with the named conventions.""" @@ -4505,6 +4806,10 @@ class SkewTermResponse(TypedDict, total=False): # # Typed model for ``GET /v1/volatility/spot-vol-correlation/{symbol}`` (Growth+). + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class SpotVolCorrelationResponse(TypedDict, total=False): """Spot-vol correlation from @@ -4531,6 +4836,10 @@ class SpotVolCorrelationResponse(TypedDict, total=False): # Typed model for ``GET /v1/volatility/realized/{symbol}`` (Alpha+). Range-based # realized (historical) vol estimators over 10/20/30-day windows. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class RealizedVolatilityEstimator(TypedDict, total=False): """One estimator's annualized realized vol (percent) by window length. @@ -4572,6 +4881,10 @@ class RealizedVolatilityResponse(TypedDict, total=False): # Typed model for ``GET /v1/volatility/forecast/{symbol}`` (Alpha+). Conditional # vol forecasts via EWMA (λ=0.94), HAR-RV and GARCH(1,1) MLE. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class VolatilityForecastEwma(TypedDict, total=False): """EWMA block. ``lambda`` is the decay factor (0.94).""" @@ -4650,6 +4963,10 @@ class VolatilityForecastResponse(TypedDict, total=False): # Typed model for ``GET /v1/dispersion`` (Alpha+). Implied vs realized # correlation between an index and a user-supplied basket of constituents. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class DispersionContributor(TypedDict, total=False): """One constituent's contribution to basket vol (sorted descending).""" @@ -4693,6 +5010,10 @@ class DispersionResponse(TypedDict, total=False): # Typed model for ``GET /v1/macro/vix-state`` (Growth+). "overvixing / # undervixing" regime — VIX vs SPX 20-day realized vol. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class VixStateResponse(TypedDict, total=False): """VIX-state regime from ``GET /v1/macro/vix-state`` (Growth+).""" @@ -4717,6 +5038,10 @@ class VixStateResponse(TypedDict, total=False): # Typed model for ``GET /v1/universe`` (public). Curated tier-1 / tier-2 # symbol directory the screener loop keeps pre-warmed. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class UniverseSymbol(TypedDict, total=False): """One universe symbol with its tier + pre-warm flag.""" @@ -4747,6 +5072,10 @@ class UniverseResponse(TypedDict, total=False): # Typed model for ``GET /v1/flow/options/{symbol}/dealer-premium`` (Alpha+). # Full-tape Net Dealer Premium roll-up over a configurable window. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowDealerPremiumResponse(TypedDict, total=False): """Net Dealer Premium from @@ -4777,6 +5106,10 @@ class FlowDealerPremiumResponse(TypedDict, total=False): # block; the series/hedge-flow/heatmap/strike-flow endpoints are intraday # time-series wrappers with empty-``bars`` degraded shapes. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class ZeroDteFlowDirection(TypedDict, total=False): """Live flow-adjustment block appended to the 0DTE snapshot.""" @@ -4829,6 +5162,10 @@ class FlowZeroDteSnapshotResponse(TypedDict, total=False): next_zero_dte_expiry: Optional[str] message: str + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowZeroDteSeriesBar(TypedDict, total=False): """One downsampled bar of the 0DTE series.""" @@ -4868,6 +5205,10 @@ class FlowZeroDteSeriesResponse(TypedDict, total=False): bar_size: Optional[str] bars: List[FlowZeroDteSeriesBar] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowZeroDteHedgeFlowBar(TypedDict, total=False): """One bar of the 0DTE hedge-flow series.""" @@ -4892,6 +5233,10 @@ class FlowZeroDteHedgeFlowResponse(TypedDict, total=False): bar_size: Optional[str] bars: List[FlowZeroDteHedgeFlowBar] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowZeroDteHeatmapBar(TypedDict, total=False): """One bar of the 0DTE heatmap; ``values`` is index-aligned to @@ -4927,6 +5272,10 @@ class FlowZeroDteHeatmapResponse(TypedDict, total=False): # Reserved for sampler-gap intervals; not yet populated. gap_intervals: List[Any] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowZeroDteStrikeFlowBar(TypedDict, total=False): """One bar of per-strike signed aggressor flow (arrays index-aligned to @@ -4956,6 +5305,10 @@ class FlowZeroDteStrikeFlowResponse(TypedDict, total=False): bars: List[FlowZeroDteStrikeFlowBar] gap_intervals: List[Any] + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowZeroDteLeaderboardEntry(TypedDict, total=False): """One ranked symbol in the cross-symbol 0DTE leaderboard.""" @@ -4988,6 +5341,10 @@ class FlowZeroDteLeaderboardResponse(TypedDict, total=False): # Typed model for ``GET /v1/flow/stocks/{symbol}/bars`` (Alpha+). # Multi-resolution OHLCV+flow bars, oldest-first, camelCase wire keys. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class FlowStockBar(TypedDict, total=False): """One OHLCV+flow bar (camelCase wire keys, oldest-first).""" @@ -5031,6 +5388,10 @@ class FlowStockBarsResponse(TypedDict, total=False): # Typed model for ``GET /v1/vrp/{symbol}/history`` (Alpha+). Daily VRP time # series for charting and backtesting. + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class VrpHistoryRow(TypedDict, total=False): """One daily VRP snapshot row.""" @@ -5066,6 +5427,10 @@ class VrpHistoryResponse(TypedDict, total=False): # # Typed model for ``GET /v1/screener/fields`` (any authenticated tier). + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf + class ScreenerField(TypedDict, total=False): """One queryable screener field with its value type.""" @@ -5084,3 +5449,7 @@ class ScreenerFieldsResponse(TypedDict, total=False): fields: List[ScreenerField] count: Optional[int] + + # Response envelope, present on every successful response. + endpoint_version: str + data_as_of: DataAsOf diff --git a/tests/test_response_envelope.py b/tests/test_response_envelope.py new file mode 100644 index 0000000..7ed298f --- /dev/null +++ b/tests/test_response_envelope.py @@ -0,0 +1,64 @@ +"""Guard that every response type carries the envelope. + +The envelope was added to 84 TypedDicts by a sweep. Trusting that the sweep reached +all of them - and that a response type added later will not quietly miss it - is +exactly the assumption worth testing. + +TypedDicts are erased at runtime, so this checks the declared annotations rather than +any instance: it is a guard against the source drifting, not a runtime behaviour test. +""" + +import pytest + +from flashalpha import DataAsOf, types + + +def response_types(): + """Every *Response TypedDict declared in the types module.""" + return [ + (name, obj) + for name, obj in vars(types).items() + if name.endswith("Response") and isinstance(obj, type) and hasattr(obj, "__annotations__") + ] + + +def test_the_guard_actually_finds_response_types(): + # Without this the parametrized tests below would pass vacuously if the module + # were renamed or the scan broke. + assert len(response_types()) > 50, f"only found {len(response_types())} response types" + + +@pytest.mark.parametrize("name,obj", response_types(), ids=lambda v: v if isinstance(v, str) else "") +def test_every_response_type_declares_the_envelope(name, obj): + annotations = obj.__annotations__ + assert "data_as_of" in annotations, f"{name} is missing data_as_of" + assert "endpoint_version" in annotations, f"{name} is missing endpoint_version" + + +def test_data_as_of_declares_every_feed(): + """The nine feeds are a contract shared with the live API and the other SDKs. + + Spot and options are separate on purpose: they arrive over different pipes and fail + independently, so collapsing any pair would lose the distinction the field exists + to make. + """ + expected = { + "node", + "equity_feed", + "equity_options_feed", + "index_feed", + "index_options_feed", + "futures_feed", + "futures_options_feed", + "flow_feed", + "oi_feed", + "macro_feed", + } + + assert set(DataAsOf.__annotations__) == expected + + +def test_data_as_of_is_exported_from_the_package_root(): + import flashalpha + + assert "DataAsOf" in flashalpha.__all__