From 89bd904ff870b8e3496b63f46d46c695a3f373a8 Mon Sep 17 00:00:00 2001 From: Tomasz Dobrowolski Date: Tue, 25 Aug 2026 20:35:59 +0300 Subject: [PATCH 1/4] feat: expose the data_as_of response envelope The API now returns provenance on every successful response: data_as_of, reporting when each upstream feed last delivered to the node that answered, and endpoint_version identifying the deployment. Nine feeds are reported separately - equity and index spot, their option chains, futures and futures options, the classified trade tape, settled open interest, and the macro series - because they arrive over different pipes and fail independently. An index chain can be current while the index level behind it is not, and one timestamp cannot express that. Responses are dicts at runtime, so the fields were already reachable; what was missing was the typing and the documentation. DataAsOf is now a TypedDict, exported from the package root, and added to all 84 *Response types, so the envelope has editor completion and type checking. Purely additive - existing code is unaffected. README and llms.txt gain a provenance section covering how to read it: each feed against its OWN cadence rather than against as_of. Settled open interest dated to the previous session's close is correct, because it is published once per session - on a Monday the newest figure that exists is Friday's. An options feed an hour behind during the regular session is not correct. A null means that node has not seen that feed, not that it is broken. The limit is stated alongside the claim: the field evidences that a feed delivered recently, not that every contract in a chain is equally current. An illiquid strike may not have quoted for hours while its feed is healthy. Version 1.3.0. --- CHANGELOG.md | 24 +++ README.md | 57 ++++++ llms.txt | 12 ++ pyproject.toml | 2 +- src/flashalpha/__init__.py | 2 + src/flashalpha/types.py | 369 +++++++++++++++++++++++++++++++++++++ 6 files changed, 465 insertions(+), 1 deletion(-) 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..54449ab 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,63 @@ 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 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. + +```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-22T20: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, NDX, RUT, VIX) | 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. + +Endpoints returning a bare JSON array carry the same information in the +`X-Data-As-Of` and `X-Endpoint-Version` response headers. + +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 From f787db5966cb64abddd5ada4b1b1460bbeb91fd1 Mon Sep 17 00:00:00 2001 From: Tomasz Dobrowolski Date: Tue, 25 Aug 2026 21:27:12 +0300 Subject: [PATCH 2/4] test: guard that every response type carries the envelope The four statically-typed SDKs each got a sweep guard; Python and JS did not, purely because TypedDicts and interfaces are erased at runtime and there was no object to reflect over. That left the largest sweep in the fleet - 84 types in the live Python SDK - resting on a regex having touched every one, with nothing to catch a type added later. Reading the declarations from source removes the excuse. The guard walks the declared annotations rather than any instance, so it checks the source has not drifted rather than any runtime behaviour. It asserts the scan found response types at all, so it cannot pass vacuously if a module is renamed or the scan breaks, and pins the nine feed names: spot and options being reported separately is a contract shared with the live API and the other SDKs, and collapsing any pair would lose the distinction the field exists to make. Mutation-checked by removing the envelope from VexResponse. --- tests/test_response_envelope.py | 64 +++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 tests/test_response_envelope.py 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__ From 83e743a7fff68de640cc4d9bb7c28853dddfd53a Mon Sep 17 00:00:00 2001 From: Tomasz Dobrowolski Date: Tue, 25 Aug 2026 21:42:44 +0300 Subject: [PATCH 3/4] docs: correct three factual errors in the provenance documentation Second-pass review against the API source found three claims that were wrong rather than merely loose. NDX was listed as an example of index_feed. It is not in IndexSymbols, so SymbolClassifier falls it through to equity - an NDX request ticks equity_feed, not index_feed. The other services in the API do treat NDX as an index, which is what made the claim look safe. Now names only roots that are actually classified as indexes. data_as_of on the replay service was described as "all null". Every feed is null, but node is always populated - it identifies which node answered. "All null" would have a caller testing the wrong thing. The historical SDKs repeated the live line about bare JSON arrays carrying the envelope in X-Data-As-Of and X-Endpoint-Version headers. That does not hold there: the header path never calls Additional(), so archive_as_of - the only provenance that means anything on a replay host - is not emitted at all, and the historical service has no bare-array endpoints in the first place. The claim is dropped rather than reworded, since the case does not arise. The nine feed names and their order were verified against FeedClock and ReplayClock in all ten SDKs: every one matches, order included. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 54449ab..e62a532 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ stamps: DataAsOf = gex["data_as_of"] | `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, NDX, RUT, VIX) | 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 | From eea56f460dca28c9e1c2fc03c936959508df7ace Mon Sep 17 00:00:00 2001 From: Tomasz Dobrowolski Date: Wed, 26 Aug 2026 13:24:06 +0300 Subject: [PATCH 4/4] docs: fix impossible OI dates, wrong imports, and an overclaim Review found three defects that were teaching the wrong thing. The sample OI timestamp was 2026-08-22T20:00:00Z presented as the prior session close for a 2026-08-25 response. 2026-08-22 is a Saturday, so no session closed then; the prior close for Tuesday the 25th is Monday the 24th. An impossible timestamp in a provenance example is worse than no example, because the whole point of the field is teaching people - and models - what a correct market calendar looks like. Corrected in 15 files, docs and test fixtures alike. The TypeScript import named a package that does not exist. The live package is `flashalpha` and the replay package is `flashalpha-historical`, not `@flashalpha/sdk` and `@flashalpha/historical`. Both were wrong; only one had been reported. "Every successful response carries data_as_of" was true of the HTTP API but not of this client. A few endpoints return a bare JSON array, where the API puts the envelope in the X-Data-As-Of and X-Endpoint-Version headers instead - and these clients return the parsed body only, so the envelope is not reachable through those methods at all. The claim is now scoped to JSON-object responses and the array case is documented as the limitation it is rather than implied to work. On the replay service every endpoint returns an object, so the unscoped claim holds there and says so. AGENTS.md gains a freshness section, which is the file an agent actually reads as operating rules and mentioned none of this. It maps each kind of call to the feeds that answer it, so a null on an unrelated feed is not treated as a problem; states that timestamps are UTC instants to compare rather than parse; judges staleness against each feed's cadence rather than the wall clock; and - the part that was missing entirely - says that a null on a feed you depend on means freshness is UNKNOWN, so the answer must be qualified or declined rather than presented as current. It also warns that node can change between calls, so timestamps must not be diffed across calls, and that endpoint_version is opaque deployment metadata rather than a semver to parse. Four .csproj.lscache files were committed by an over-broad git add in the first commit. They declare themselves disposable; now untracked and ignored. --- AGENTS.md | 46 ++++++++++++++++++++++++++++++++++++++++++++++ README.md | 18 ++++++++++++------ 2 files changed, 58 insertions(+), 6 deletions(-) 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/README.md b/README.md index e62a532..e30c18f 100644 --- a/README.md +++ b/README.md @@ -33,15 +33,16 @@ Get your free API key at [flashalpha.com](https://flashalpha.com) — no credit ## Data provenance: `data_as_of` -Every successful 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. +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-22T20:00:00.000Z (prior session) +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 @@ -82,8 +83,13 @@ stamps: DataAsOf = gex["data_as_of"] contract in the payload, depending on the endpoint. `data_as_of` describes the feeds behind it. -Endpoints returning a bare JSON array carry the same information in the -`X-Data-As-Of` and `X-Endpoint-Version` response headers. +### 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 .