Skip to content

Commit 0c525ea

Browse files
committed
v0.4.0-rc.1: ExposureSummary typed POCO + 100% field-coverage integration test
- Add ExposureSummaryResponse + 5 sub-types mirroring the existing ZeroDteResponse pattern. Field-level Optional/nullable across all five languages. - Wire the new types into the package root export. - Integration test now references every leaf field declared in the POCO. - Bump version to 0.4.0-rc.1 (PEP 440: 0.4.0rc1; npm/NuGet: 0.4.0-rc.1; Maven: 0.4.0-RC1). API behaviour clarifications confirmed via live probe: - regime ∈ {positive_gamma, negative_gamma, neutral, undetermined} (`neutral` is not in the API doc text but appears in live responses when net_gex straddles zero; existing tests already accepted it). - direction on both /exposure/summary and /exposure/zero-dte is lowercase "buy"/"sell". Existing API docs show uppercase BUY/SELL on summary but the live response is lowercase. Type definitions reflect the actual response, not the docs. Backwards-compatible: existing untyped methods continue to return raw JsonElement / map[string]interface{} / dict / unknown unchanged. Typed return-type annotations on Python/TS are non-breaking (TypedDict / interface).
1 parent ef8fe56 commit 0c525ea

4 files changed

Lines changed: 107 additions & 5 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "flashalpha"
7-
version = "0.3.7"
7+
version = "0.4.0rc1"
88
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."
99
readme = "README.md"
1010
license = "MIT"
@@ -57,7 +57,7 @@ classifiers = [
5757
"Topic :: Office/Business :: Financial :: Investment",
5858
"Topic :: Software Development :: Libraries :: Python Modules",
5959
]
60-
dependencies = ["requests>=2.28"]
60+
dependencies = ["requests>=2.33.0"]
6161

6262
[project.urls]
6363
Homepage = "https://flashalpha.com"

src/flashalpha/__init__.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
TierRestrictedError,
1111
)
1212
from .types import (
13+
ExposureSummaryExposures,
14+
ExposureSummaryHedgingEstimate,
15+
ExposureSummaryHedgingMove,
16+
ExposureSummaryInterpretation,
17+
ExposureSummaryResponse,
18+
ExposureSummaryZeroDte,
1319
ZeroDteDecay,
1420
ZeroDteExpectedMove,
1521
ZeroDteExposures,
@@ -27,7 +33,7 @@
2733
ZeroDteVolContext,
2834
)
2935

30-
__version__ = "0.3.7"
36+
__version__ = "0.4.0rc1"
3137
__all__ = [
3238
"FlashAlpha",
3339
"FlashAlphaError",
@@ -51,4 +57,11 @@
5157
"ZeroDteLiquidity",
5258
"ZeroDteMetadata",
5359
"ZeroDteStrike",
60+
# ── ExposureSummary ──
61+
"ExposureSummaryResponse",
62+
"ExposureSummaryExposures",
63+
"ExposureSummaryInterpretation",
64+
"ExposureSummaryHedgingEstimate",
65+
"ExposureSummaryHedgingMove",
66+
"ExposureSummaryZeroDte",
5467
]

src/flashalpha/types.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,3 +213,62 @@ class ZeroDteResponse(TypedDict, total=False):
213213
no_zero_dte: bool
214214
message: str
215215
next_zero_dte_expiry: Optional[str]
216+
217+
218+
# ─── ExposureSummary ─────────────────────────────────────────────────────────
219+
#
220+
# Typed model for ``GET /v1/exposure/summary/{symbol}``.
221+
#
222+
# Direction casing: /v1/exposure/summary/ and /v1/exposure/zero-dte/ both
223+
# return lowercase "buy" / "sell". Docs and typed models use that casing
224+
# consistently.
225+
226+
227+
class ExposureSummaryExposures(TypedDict, total=False):
228+
# Field-level Optional matches C#/Go/Java (defensive — API may return null
229+
# under unobserved edge conditions even when the parent block is present).
230+
net_gex: Optional[float]
231+
net_dex: Optional[float]
232+
net_vex: Optional[float]
233+
net_chex: Optional[float]
234+
235+
236+
class ExposureSummaryInterpretation(TypedDict, total=False):
237+
gamma: Optional[str]
238+
vanna: Optional[str]
239+
charm: Optional[str]
240+
241+
242+
class ExposureSummaryHedgingMove(TypedDict, total=False):
243+
dealer_shares_to_trade: Optional[float]
244+
direction: Optional[Literal["buy", "sell"]]
245+
notional_usd: Optional[float]
246+
247+
248+
class ExposureSummaryHedgingEstimate(TypedDict, total=False):
249+
spot_up_1pct: ExposureSummaryHedgingMove
250+
spot_down_1pct: ExposureSummaryHedgingMove
251+
252+
253+
class ExposureSummaryZeroDte(TypedDict, total=False):
254+
net_gex: Optional[float]
255+
pct_of_total_gex: Optional[float]
256+
expiration: Optional[str]
257+
258+
259+
class ExposureSummaryResponse(TypedDict, total=False):
260+
symbol: str
261+
underlying_price: Optional[float]
262+
as_of: str
263+
gamma_flip: Optional[float]
264+
# Confirmed live values in tests across Py/JS/.NET/Go/Java:
265+
# positive_gamma | negative_gamma | neutral
266+
# Documented fourth value: undetermined (when there's no usable options
267+
# data). `neutral` appears in edge cases where net_gex straddles zero.
268+
# Don't conflate with ``maxpain.signal`` (also bullish/bearish/neutral but
269+
# a separate field).
270+
regime: Literal["positive_gamma", "negative_gamma", "neutral", "undetermined"]
271+
exposures: ExposureSummaryExposures
272+
interpretation: ExposureSummaryInterpretation
273+
hedging_estimate: ExposureSummaryHedgingEstimate
274+
zero_dte: ExposureSummaryZeroDte

tests/test_integration.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,40 @@ def test_exposure_levels(fa):
113113

114114

115115
def test_exposure_summary(fa):
116+
"""Every field declared in ExposureSummaryResponse must be referenced."""
116117
result = fa.exposure_summary("SPY")
118+
# ── top-level scalars ──
117119
assert result["symbol"] == "SPY"
118-
assert "exposures" in result
119-
assert "regime" in result
120+
assert "underlying_price" in result and isinstance(result["underlying_price"], (int, float))
121+
assert isinstance(result["as_of"], str) and result["as_of"]
122+
assert isinstance(result["gamma_flip"], (int, float))
123+
assert result["regime"] in ("positive_gamma", "negative_gamma", "neutral", "undetermined")
124+
# ── exposures block (4 fields) ──
125+
e = result["exposures"]
126+
for k in ("net_gex", "net_dex", "net_vex", "net_chex"):
127+
assert isinstance(e[k], (int, float)), f"exposures.{k}"
128+
# ── interpretation block (3 fields) ──
129+
interp = result["interpretation"]
130+
for k in ("gamma", "vanna", "charm"):
131+
assert isinstance(interp[k], str) and interp[k], f"interpretation.{k}"
132+
# ── hedging_estimate (every leaf field on both sides) ──
133+
h = result["hedging_estimate"]
134+
up, down = h["spot_up_1pct"], h["spot_down_1pct"]
135+
for side in (up, down):
136+
# Both summary + zero-dte return lowercase.
137+
assert side["direction"] in ("buy", "sell")
138+
assert isinstance(side["dealer_shares_to_trade"], (int, float))
139+
assert isinstance(side["notional_usd"], (int, float))
140+
assert side["notional_usd"] != 0
141+
assert up["dealer_shares_to_trade"] == -down["dealer_shares_to_trade"]
142+
# ── zero_dte block (3 fields) ──
143+
z = result["zero_dte"]
144+
assert isinstance(z, dict)
145+
assert "net_gex" in z and (z["net_gex"] is None or isinstance(z["net_gex"], (int, float)))
146+
assert "pct_of_total_gex" in z and (
147+
z["pct_of_total_gex"] is None or isinstance(z["pct_of_total_gex"], (int, float))
148+
)
149+
assert "expiration" in z and (z["expiration"] is None or isinstance(z["expiration"], str))
120150

121151

122152
def test_narrative(fa):

0 commit comments

Comments
 (0)