flashalpha 0.3.3__tar.gz → 0.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. flashalpha-0.3.4/CLAUDE.md +67 -0
  2. {flashalpha-0.3.3 → flashalpha-0.3.4}/PKG-INFO +1 -1
  3. {flashalpha-0.3.3 → flashalpha-0.3.4}/pyproject.toml +1 -1
  4. flashalpha-0.3.4/src/flashalpha/__init__.py +54 -0
  5. {flashalpha-0.3.3 → flashalpha-0.3.4}/src/flashalpha/client.py +11 -3
  6. flashalpha-0.3.4/src/flashalpha/types.py +215 -0
  7. {flashalpha-0.3.3 → flashalpha-0.3.4}/tests/test_integration.py +254 -0
  8. flashalpha-0.3.3/src/flashalpha/__init__.py +0 -22
  9. {flashalpha-0.3.3 → flashalpha-0.3.4}/.claude/settings.json +0 -0
  10. {flashalpha-0.3.3 → flashalpha-0.3.4}/.github/workflows/ci.yml +0 -0
  11. {flashalpha-0.3.3 → flashalpha-0.3.4}/.gitignore +0 -0
  12. {flashalpha-0.3.3 → flashalpha-0.3.4}/CHANGELOG.md +0 -0
  13. {flashalpha-0.3.3 → flashalpha-0.3.4}/CONTRIBUTING.md +0 -0
  14. {flashalpha-0.3.3 → flashalpha-0.3.4}/LICENSE +0 -0
  15. {flashalpha-0.3.3 → flashalpha-0.3.4}/README.md +0 -0
  16. {flashalpha-0.3.3 → flashalpha-0.3.4}/article-flashalpha-python-sdk.md +0 -0
  17. {flashalpha-0.3.3 → flashalpha-0.3.4}/awesome-investing/CODE_OF_CONDUCT.md +0 -0
  18. {flashalpha-0.3.3 → flashalpha-0.3.4}/awesome-investing/LICENSE +0 -0
  19. {flashalpha-0.3.3 → flashalpha-0.3.4}/awesome-investing/PULL_REQUEST_TEMPLATE.md +0 -0
  20. {flashalpha-0.3.3 → flashalpha-0.3.4}/awesome-investing/README.md +0 -0
  21. {flashalpha-0.3.3 → flashalpha-0.3.4}/awesome-investing/contributing.md +0 -0
  22. {flashalpha-0.3.3 → flashalpha-0.3.4}/awesome_investing/README.md +0 -0
  23. {flashalpha-0.3.3 → flashalpha-0.3.4}/awesome_investing/src/static/img/.gitkeep +0 -0
  24. {flashalpha-0.3.3 → flashalpha-0.3.4}/awesome_investing/src/static/img/markus-spiske-5gGcn2PRrtc-unsplash.jpg +0 -0
  25. {flashalpha-0.3.3 → flashalpha-0.3.4}/docs/api.md +0 -0
  26. {flashalpha-0.3.3 → flashalpha-0.3.4}/examples/quickstart.py +0 -0
  27. {flashalpha-0.3.3 → flashalpha-0.3.4}/src/flashalpha/exceptions.py +0 -0
  28. {flashalpha-0.3.3 → flashalpha-0.3.4}/tests/__init__.py +0 -0
  29. {flashalpha-0.3.3 → flashalpha-0.3.4}/tests/test_client.py +0 -0
@@ -0,0 +1,67 @@
1
+ # CLAUDE.md
2
+
3
+ Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
4
+
5
+ **Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
6
+
7
+ ## 1. Think Before Coding
8
+
9
+ **Don't assume. Don't hide confusion. Surface tradeoffs.**
10
+
11
+ Before implementing:
12
+ - State your assumptions explicitly. If uncertain, ask.
13
+ - If multiple interpretations exist, present them - don't pick silently.
14
+ - If a simpler approach exists, say so. Push back when warranted.
15
+ - If something is unclear, stop. Name what's confusing. Ask.
16
+
17
+ ## 2. Simplicity First
18
+
19
+ **Minimum code that solves the problem. Nothing speculative.**
20
+
21
+ - No features beyond what was asked.
22
+ - No abstractions for single-use code.
23
+ - No "flexibility" or "configurability" that wasn't requested.
24
+ - No error handling for impossible scenarios.
25
+ - If you write 200 lines and it could be 50, rewrite it.
26
+
27
+ Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
28
+
29
+ ## 3. Surgical Changes
30
+
31
+ **Touch only what you must. Clean up only your own mess.**
32
+
33
+ When editing existing code:
34
+ - Don't "improve" adjacent code, comments, or formatting.
35
+ - Don't refactor things that aren't broken.
36
+ - Match existing style, even if you'd do it differently.
37
+ - If you notice unrelated dead code, mention it - don't delete it.
38
+
39
+ When your changes create orphans:
40
+ - Remove imports/variables/functions that YOUR changes made unused.
41
+ - Don't remove pre-existing dead code unless asked.
42
+
43
+ The test: Every changed line should trace directly to the user's request.
44
+
45
+ ## 4. Goal-Driven Execution
46
+
47
+ **Define success criteria. Loop until verified.**
48
+
49
+ Transform tasks into verifiable goals:
50
+ - "Add validation" → "Write tests for invalid inputs, then make them pass"
51
+ - "Fix the bug" → "Write a test that reproduces it, then make it pass"
52
+ - "Refactor X" → "Ensure tests pass before and after"
53
+
54
+ For multi-step tasks, state a brief plan:
55
+ ```
56
+ 1. [Step] → verify: [check]
57
+ 2. [Step] → verify: [check]
58
+ 3. [Step] → verify: [check]
59
+ ```
60
+
61
+ Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
62
+
63
+ ---
64
+
65
+ **These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
66
+
67
+ _Source: [forrestchang/andrej-karpathy-skills](https://github.com/forrestchang/andrej-karpathy-skills)_
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flashalpha
3
- Version: 0.3.3
3
+ Version: 0.3.4
4
4
  Summary: 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.
5
5
  Project-URL: Homepage, https://flashalpha.com
6
6
  Project-URL: Documentation, https://flashalpha.com/docs
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "flashalpha"
7
- version = "0.3.3"
7
+ version = "0.3.4"
8
8
  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."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,54 @@
1
+ """FlashAlpha Python SDK — options exposure analytics API."""
2
+
3
+ from .client import FlashAlpha
4
+ from .exceptions import (
5
+ AuthenticationError,
6
+ FlashAlphaError,
7
+ NotFoundError,
8
+ RateLimitError,
9
+ ServerError,
10
+ TierRestrictedError,
11
+ )
12
+ from .types import (
13
+ ZeroDteDecay,
14
+ ZeroDteExpectedMove,
15
+ ZeroDteExposures,
16
+ ZeroDteFlow,
17
+ ZeroDteHedging,
18
+ ZeroDteHedgingBucket,
19
+ ZeroDteLevels,
20
+ ZeroDteLiquidity,
21
+ ZeroDteMetadata,
22
+ ZeroDtePinComponents,
23
+ ZeroDtePinRisk,
24
+ ZeroDteRegime,
25
+ ZeroDteResponse,
26
+ ZeroDteStrike,
27
+ ZeroDteVolContext,
28
+ )
29
+
30
+ __version__ = "0.3.4"
31
+ __all__ = [
32
+ "FlashAlpha",
33
+ "FlashAlphaError",
34
+ "AuthenticationError",
35
+ "TierRestrictedError",
36
+ "NotFoundError",
37
+ "RateLimitError",
38
+ "ServerError",
39
+ "ZeroDteResponse",
40
+ "ZeroDteRegime",
41
+ "ZeroDteExposures",
42
+ "ZeroDteExpectedMove",
43
+ "ZeroDtePinRisk",
44
+ "ZeroDtePinComponents",
45
+ "ZeroDteHedging",
46
+ "ZeroDteHedgingBucket",
47
+ "ZeroDteDecay",
48
+ "ZeroDteVolContext",
49
+ "ZeroDteFlow",
50
+ "ZeroDteLevels",
51
+ "ZeroDteLiquidity",
52
+ "ZeroDteMetadata",
53
+ "ZeroDteStrike",
54
+ ]
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from typing import Any
5
+ from typing import TYPE_CHECKING, Any
6
6
 
7
7
  import requests
8
8
 
@@ -15,6 +15,9 @@ from .exceptions import (
15
15
  TierRestrictedError,
16
16
  )
17
17
 
18
+ if TYPE_CHECKING:
19
+ from .types import ZeroDteResponse
20
+
18
21
  BASE_URL = "https://lab.flashalpha.com"
19
22
 
20
23
 
@@ -192,8 +195,13 @@ class FlashAlpha:
192
195
  """Verbal narrative analysis of exposure. Requires Growth+."""
193
196
  return self._get(f"/v1/exposure/narrative/{symbol}")
194
197
 
195
- def zero_dte(self, symbol: str, *, strike_range: float | None = None) -> dict:
196
- """Real-time 0DTE analytics: regime, expected move, pin risk, hedging, decay. Requires Growth+."""
198
+ def zero_dte(self, symbol: str, *, strike_range: float | None = None) -> "ZeroDteResponse":
199
+ """Real-time 0DTE analytics: regime, expected move, pin risk, hedging, decay. Requires Growth+.
200
+
201
+ Returns a ``ZeroDteResponse`` (a ``TypedDict`` — runtime-equivalent to
202
+ ``dict``). Existing ``result["field"]`` access continues to work; new
203
+ callers get autocomplete and type-checking on the documented fields.
204
+ """
197
205
  params: dict[str, Any] = {}
198
206
  if strike_range is not None:
199
207
  params["strike_range"] = strike_range
@@ -0,0 +1,215 @@
1
+ """Typed response models for FlashAlpha SDK.
2
+
3
+ These are ``TypedDict`` aliases — at runtime each is a plain ``dict``. Existing
4
+ code that does ``result["field"]`` keeps working unchanged. Static type
5
+ checkers (mypy/pyright) and IDEs see the field shape and provide autocomplete.
6
+
7
+ Currently typed:
8
+ - ``ZeroDteResponse`` (full payload of GET /v1/exposure/zero-dte/{symbol})
9
+
10
+ All numeric fields are typed ``Optional[float]``/``Optional[int]`` because
11
+ the API returns ``null`` for any value it can't compute (insufficient data,
12
+ market closed, etc.). Treat the typed shape as a *hint*, not a guarantee —
13
+ unknown fields added by the API in future revisions will still pass through.
14
+ """
15
+
16
+ from typing import List, Literal, Optional, TypedDict
17
+
18
+
19
+ class ZeroDteRegime(TypedDict, total=False):
20
+ label: str
21
+ description: str
22
+ gamma_flip: Optional[float]
23
+ spot_vs_flip: Literal["above", "below"]
24
+ spot_to_flip_pct: Optional[float]
25
+ distance_to_flip_dollars: Optional[float]
26
+ distance_to_flip_sigmas: Optional[float]
27
+
28
+
29
+ class ZeroDteExposures(TypedDict, total=False):
30
+ net_gex: float
31
+ net_dex: float
32
+ net_vex: float
33
+ net_chex: float
34
+ pct_of_total_gex: Optional[float]
35
+ total_chain_net_gex: float
36
+
37
+
38
+ class ZeroDteExpectedMove(TypedDict, total=False):
39
+ implied_1sd_dollars: Optional[float]
40
+ implied_1sd_pct: Optional[float]
41
+ remaining_1sd_dollars: Optional[float]
42
+ remaining_1sd_pct: Optional[float]
43
+ upper_bound: Optional[float]
44
+ lower_bound: Optional[float]
45
+ straddle_price: Optional[float]
46
+ atm_iv: Optional[float]
47
+
48
+
49
+ class ZeroDtePinComponents(TypedDict, total=False):
50
+ oi_score: int
51
+ proximity_score: int
52
+ time_score: int
53
+ gamma_score: int
54
+
55
+
56
+ class ZeroDtePinRisk(TypedDict, total=False):
57
+ magnet_strike: Optional[float]
58
+ magnet_gex: Optional[float]
59
+ distance_to_magnet_pct: Optional[float]
60
+ pin_score: int
61
+ components: ZeroDtePinComponents
62
+ max_pain: Optional[float]
63
+ oi_concentration_top3_pct: Optional[float]
64
+ description: str
65
+
66
+
67
+ class ZeroDteHedgingBucket(TypedDict, total=False):
68
+ dealer_shares_to_trade: float
69
+ direction: Literal["buy", "sell"]
70
+ notional_usd: float
71
+
72
+
73
+ class ZeroDteHedging(TypedDict, total=False):
74
+ spot_up_10bp: ZeroDteHedgingBucket
75
+ spot_down_10bp: ZeroDteHedgingBucket
76
+ spot_up_25bp: ZeroDteHedgingBucket
77
+ spot_down_25bp: ZeroDteHedgingBucket
78
+ spot_up_half_pct: ZeroDteHedgingBucket
79
+ spot_down_half_pct: ZeroDteHedgingBucket
80
+ spot_up_1pct: ZeroDteHedgingBucket
81
+ spot_down_1pct: ZeroDteHedgingBucket
82
+ convexity_at_spot: Optional[float]
83
+
84
+
85
+ class ZeroDteDecay(TypedDict, total=False):
86
+ net_theta_dollars: Optional[float]
87
+ theta_per_hour_remaining: Optional[float]
88
+ charm_regime: str
89
+ charm_description: str
90
+ gamma_acceleration: Optional[float]
91
+ description: str
92
+
93
+
94
+ class ZeroDteVolContext(TypedDict, total=False):
95
+ zero_dte_atm_iv: Optional[float]
96
+ seven_dte_atm_iv: Optional[float]
97
+ iv_ratio_0dte_7dte: Optional[float]
98
+ vix: Optional[float]
99
+ vanna_exposure: Optional[float]
100
+ vanna_interpretation: str
101
+ description: str
102
+
103
+
104
+ class ZeroDteFlow(TypedDict, total=False):
105
+ total_volume: int
106
+ call_volume: int
107
+ put_volume: int
108
+ net_call_minus_put_volume: int
109
+ total_oi: int
110
+ call_oi: int
111
+ put_oi: int
112
+ pc_ratio_volume: Optional[float]
113
+ pc_ratio_oi: Optional[float]
114
+ volume_to_oi_ratio: Optional[float]
115
+ atm_volume_share_pct: Optional[float]
116
+ top3_strike_volume_pct: Optional[float]
117
+
118
+
119
+ class ZeroDteLevels(TypedDict, total=False):
120
+ call_wall: Optional[float]
121
+ call_wall_gex: Optional[float]
122
+ call_wall_strength: Optional[float]
123
+ distance_to_call_wall_pct: Optional[float]
124
+ put_wall: Optional[float]
125
+ put_wall_gex: Optional[float]
126
+ put_wall_strength: Optional[float]
127
+ distance_to_put_wall_pct: Optional[float]
128
+ distance_to_magnet_dollars: Optional[float]
129
+ highest_oi_strike: Optional[float]
130
+ highest_oi_total: Optional[int]
131
+ max_positive_gamma: Optional[float]
132
+ max_negative_gamma: Optional[float]
133
+ level_cluster_score: Optional[int]
134
+
135
+
136
+ class ZeroDteLiquidity(TypedDict, total=False):
137
+ atm_spread_pct: Optional[float]
138
+ weighted_spread_pct: Optional[float]
139
+ execution_score: Optional[int]
140
+
141
+
142
+ class ZeroDteMetadata(TypedDict, total=False):
143
+ snapshot_age_seconds: Optional[float]
144
+ chain_contract_count: int
145
+ data_quality_score: Optional[int]
146
+ greek_smoothness_score: Optional[int]
147
+
148
+
149
+ class ZeroDteStrike(TypedDict, total=False):
150
+ strike: float
151
+ distance_from_spot_pct: float
152
+ call_symbol: str
153
+ put_symbol: str
154
+ call_gex: Optional[float]
155
+ put_gex: Optional[float]
156
+ net_gex: Optional[float]
157
+ call_dex: Optional[float]
158
+ put_dex: Optional[float]
159
+ net_dex: Optional[float]
160
+ net_vex: Optional[float]
161
+ net_chex: Optional[float]
162
+ call_oi: Optional[int]
163
+ put_oi: Optional[int]
164
+ call_volume: Optional[int]
165
+ put_volume: Optional[int]
166
+ gex_share_pct: Optional[float]
167
+ oi_share_pct: Optional[float]
168
+ volume_share_pct: Optional[float]
169
+ call_iv: Optional[float]
170
+ put_iv: Optional[float]
171
+ call_delta: Optional[float]
172
+ put_delta: Optional[float]
173
+ call_gamma: Optional[float]
174
+ put_gamma: Optional[float]
175
+ call_theta: Optional[float]
176
+ put_theta: Optional[float]
177
+ call_mid: Optional[float]
178
+ put_mid: Optional[float]
179
+ call_spread_pct: Optional[float]
180
+ put_spread_pct: Optional[float]
181
+
182
+
183
+ class ZeroDteResponse(TypedDict, total=False):
184
+ """Full response for GET /v1/exposure/zero-dte/{symbol}.
185
+
186
+ On weekends/holidays or symbols without 0DTE today, ``no_zero_dte`` is
187
+ ``True`` and most fields are absent — only ``symbol``, ``as_of``,
188
+ ``message``, and ``next_zero_dte_expiry`` are populated.
189
+ """
190
+
191
+ symbol: str
192
+ underlying_price: float
193
+ expiration: Optional[str]
194
+ as_of: str
195
+ market_open: bool
196
+ time_to_close_hours: Optional[float]
197
+ time_to_close_pct: Optional[float]
198
+ regime: ZeroDteRegime
199
+ exposures: ZeroDteExposures
200
+ expected_move: ZeroDteExpectedMove
201
+ pin_risk: ZeroDtePinRisk
202
+ hedging: ZeroDteHedging
203
+ decay: ZeroDteDecay
204
+ vol_context: ZeroDteVolContext
205
+ flow: ZeroDteFlow
206
+ levels: ZeroDteLevels
207
+ liquidity: ZeroDteLiquidity
208
+ metadata: ZeroDteMetadata
209
+ strikes: List[ZeroDteStrike]
210
+ # Optional — only present near close (<5 min) when greeks may be unstable.
211
+ warnings: List[str]
212
+ # ── No-0DTE fallback ─────────────────────────────────────────────
213
+ no_zero_dte: bool
214
+ message: str
215
+ next_zero_dte_expiry: Optional[str]
@@ -136,6 +136,260 @@ def test_zero_dte(fa):
136
136
  assert "decay" in result
137
137
 
138
138
 
139
+ def test_zero_dte_new_fields(fa):
140
+ """Validate the full zero-DTE response shape including the v0.3.4 fields:
141
+ distance-to-flip in dollars/sigmas, sub-score breakdown for pin_score,
142
+ fine-grained hedging buckets (10bp/25bp/50bp/100bp + convexity_at_spot),
143
+ flow concentration (atm/top-3 + net_call_minus_put_volume), wall-strength
144
+ and level-cluster scores, the new liquidity and metadata sections, and
145
+ per-strike greeks/quotes/spreads.
146
+
147
+ Uses SPX which has daily 0DTE — falls back gracefully on weekends/holidays.
148
+ """
149
+ result = fa.zero_dte("SPX")
150
+ assert result["symbol"] == "SPX"
151
+
152
+ if result.get("no_zero_dte"):
153
+ # Weekend / holiday — response is just a stub, nothing else to verify
154
+ assert "next_zero_dte_expiry" in result
155
+ return
156
+
157
+ # ── top-level ──────────────────────────────────────────────────────
158
+ for k in ("underlying_price", "expiration", "as_of", "market_open",
159
+ "time_to_close_hours", "time_to_close_pct"):
160
+ assert k in result, f"top-level {k} missing"
161
+
162
+ # ── regime ─────────────────────────────────────────────────────────
163
+ regime = result["regime"]
164
+ for k in ("label", "description", "gamma_flip", "spot_vs_flip", "spot_to_flip_pct",
165
+ "distance_to_flip_dollars", "distance_to_flip_sigmas"):
166
+ assert k in regime, f"regime.{k} missing"
167
+
168
+ # ── exposures ──────────────────────────────────────────────────────
169
+ exposures = result["exposures"]
170
+ for k in ("net_gex", "net_dex", "net_vex", "net_chex",
171
+ "pct_of_total_gex", "total_chain_net_gex"):
172
+ assert k in exposures, f"exposures.{k} missing"
173
+
174
+ # ── expected_move ──────────────────────────────────────────────────
175
+ em = result["expected_move"]
176
+ for k in ("implied_1sd_dollars", "implied_1sd_pct", "remaining_1sd_dollars",
177
+ "remaining_1sd_pct", "upper_bound", "lower_bound",
178
+ "straddle_price", "atm_iv"):
179
+ assert k in em, f"expected_move.{k} missing"
180
+
181
+ # ── pin_risk ───────────────────────────────────────────────────────
182
+ pr = result["pin_risk"]
183
+ for k in ("magnet_strike", "magnet_gex", "distance_to_magnet_pct",
184
+ "pin_score", "components", "max_pain",
185
+ "oi_concentration_top3_pct", "description"):
186
+ assert k in pr, f"pin_risk.{k} missing"
187
+ components = pr["components"]
188
+ for k in ("oi_score", "proximity_score", "time_score", "gamma_score"):
189
+ assert k in components, f"pin_risk.components.{k} missing"
190
+
191
+ # ── hedging — fine-grained buckets + convexity ─────────────────────
192
+ hedging = result["hedging"]
193
+ for bucket in ("spot_up_10bp", "spot_down_10bp",
194
+ "spot_up_25bp", "spot_down_25bp",
195
+ "spot_up_half_pct", "spot_down_half_pct",
196
+ "spot_up_1pct", "spot_down_1pct"):
197
+ assert bucket in hedging, f"hedging.{bucket} missing"
198
+ b = hedging[bucket]
199
+ for k in ("dealer_shares_to_trade", "direction", "notional_usd"):
200
+ assert k in b, f"hedging.{bucket}.{k} missing"
201
+ assert "convexity_at_spot" in hedging
202
+
203
+ # ── decay ──────────────────────────────────────────────────────────
204
+ decay = result["decay"]
205
+ for k in ("net_theta_dollars", "theta_per_hour_remaining", "charm_regime",
206
+ "charm_description", "gamma_acceleration", "description"):
207
+ assert k in decay, f"decay.{k} missing"
208
+
209
+ # ── vol_context ────────────────────────────────────────────────────
210
+ vc = result["vol_context"]
211
+ for k in ("zero_dte_atm_iv", "seven_dte_atm_iv", "iv_ratio_0dte_7dte",
212
+ "vix", "vanna_exposure", "vanna_interpretation", "description"):
213
+ assert k in vc, f"vol_context.{k} missing"
214
+
215
+ # ── flow ───────────────────────────────────────────────────────────
216
+ flow = result["flow"]
217
+ for k in ("total_volume", "call_volume", "put_volume",
218
+ "net_call_minus_put_volume",
219
+ "total_oi", "call_oi", "put_oi",
220
+ "pc_ratio_volume", "pc_ratio_oi", "volume_to_oi_ratio",
221
+ "atm_volume_share_pct", "top3_strike_volume_pct"):
222
+ assert k in flow, f"flow.{k} missing"
223
+
224
+ # ── levels ─────────────────────────────────────────────────────────
225
+ levels = result["levels"]
226
+ for k in ("call_wall", "call_wall_gex", "call_wall_strength",
227
+ "distance_to_call_wall_pct",
228
+ "put_wall", "put_wall_gex", "put_wall_strength",
229
+ "distance_to_put_wall_pct",
230
+ "distance_to_magnet_dollars",
231
+ "highest_oi_strike", "highest_oi_total",
232
+ "max_positive_gamma", "max_negative_gamma",
233
+ "level_cluster_score"):
234
+ assert k in levels, f"levels.{k} missing"
235
+
236
+ # ── liquidity (new section) ────────────────────────────────────────
237
+ liquidity = result["liquidity"]
238
+ for k in ("atm_spread_pct", "weighted_spread_pct", "execution_score"):
239
+ assert k in liquidity, f"liquidity.{k} missing"
240
+
241
+ # ── metadata (new section) ─────────────────────────────────────────
242
+ metadata = result["metadata"]
243
+ for k in ("snapshot_age_seconds", "chain_contract_count",
244
+ "data_quality_score", "greek_smoothness_score"):
245
+ assert k in metadata, f"metadata.{k} missing"
246
+
247
+ # ── per-strike entries ─────────────────────────────────────────────
248
+ strikes = result["strikes"]
249
+ assert isinstance(strikes, list)
250
+ if strikes:
251
+ s = strikes[0]
252
+ for k in ("strike", "distance_from_spot_pct",
253
+ "call_symbol", "put_symbol",
254
+ "call_gex", "put_gex", "net_gex",
255
+ "call_dex", "put_dex", "net_dex",
256
+ "net_vex", "net_chex",
257
+ "call_oi", "put_oi", "call_volume", "put_volume",
258
+ "gex_share_pct", "oi_share_pct", "volume_share_pct",
259
+ "call_iv", "put_iv",
260
+ "call_delta", "put_delta",
261
+ "call_gamma", "put_gamma",
262
+ "call_theta", "put_theta",
263
+ "call_mid", "put_mid",
264
+ "call_spread_pct", "put_spread_pct"):
265
+ assert k in s, f"strikes[0].{k} missing"
266
+
267
+
268
+ def test_zero_dte_typed_response(fa):
269
+ """Comprehensive end-to-end test of the typed ``ZeroDteResponse``.
270
+
271
+ Mirrors ``test_zero_dte_new_fields`` field-for-field, but reads via the
272
+ typed paths (``r["regime"]["distance_to_flip_dollars"]`` is fine because
273
+ ``ZeroDteResponse`` is a ``TypedDict`` — at runtime it's identical to
274
+ ``dict``). The point is to lock in that every documented field name
275
+ in the Python type definitions matches what the API actually ships.
276
+ """
277
+ from flashalpha.types import ZeroDteResponse
278
+
279
+ r: ZeroDteResponse = fa.zero_dte("SPX")
280
+ assert r["symbol"] == "SPX"
281
+
282
+ if r.get("no_zero_dte"):
283
+ assert "next_zero_dte_expiry" in r
284
+ return
285
+
286
+ # Top-level
287
+ assert isinstance(r["underlying_price"], (int, float))
288
+ assert "expiration" in r
289
+ assert isinstance(r["as_of"], str)
290
+ assert isinstance(r["market_open"], bool)
291
+ assert "time_to_close_hours" in r
292
+ assert "time_to_close_pct" in r
293
+
294
+ # regime
295
+ regime = r["regime"]
296
+ for k in ("label", "description", "gamma_flip", "spot_vs_flip", "spot_to_flip_pct",
297
+ "distance_to_flip_dollars", "distance_to_flip_sigmas"):
298
+ assert k in regime, f"regime.{k} missing in typed access"
299
+
300
+ # exposures
301
+ exposures = r["exposures"]
302
+ for k in ("net_gex", "net_dex", "net_vex", "net_chex",
303
+ "pct_of_total_gex", "total_chain_net_gex"):
304
+ assert k in exposures, f"exposures.{k} missing in typed access"
305
+
306
+ # expected_move
307
+ em = r["expected_move"]
308
+ for k in ("implied_1sd_dollars", "implied_1sd_pct",
309
+ "remaining_1sd_dollars", "remaining_1sd_pct",
310
+ "upper_bound", "lower_bound",
311
+ "straddle_price", "atm_iv"):
312
+ assert k in em, f"expected_move.{k} missing in typed access"
313
+
314
+ # pin_risk
315
+ pr = r["pin_risk"]
316
+ for k in ("magnet_strike", "magnet_gex", "distance_to_magnet_pct",
317
+ "pin_score", "components", "max_pain",
318
+ "oi_concentration_top3_pct", "description"):
319
+ assert k in pr, f"pin_risk.{k} missing in typed access"
320
+ for k in ("oi_score", "proximity_score", "time_score", "gamma_score"):
321
+ assert k in pr["components"], f"pin_risk.components.{k} missing"
322
+
323
+ # hedging
324
+ hedging = r["hedging"]
325
+ for bucket in ("spot_up_10bp", "spot_down_10bp",
326
+ "spot_up_25bp", "spot_down_25bp",
327
+ "spot_up_half_pct", "spot_down_half_pct",
328
+ "spot_up_1pct", "spot_down_1pct"):
329
+ assert bucket in hedging, f"hedging.{bucket} missing in typed access"
330
+ for k in ("dealer_shares_to_trade", "direction", "notional_usd"):
331
+ assert k in hedging[bucket], f"hedging.{bucket}.{k} missing"
332
+ assert "convexity_at_spot" in hedging
333
+
334
+ # decay
335
+ for k in ("net_theta_dollars", "theta_per_hour_remaining", "charm_regime",
336
+ "charm_description", "gamma_acceleration", "description"):
337
+ assert k in r["decay"], f"decay.{k} missing in typed access"
338
+
339
+ # vol_context
340
+ for k in ("zero_dte_atm_iv", "seven_dte_atm_iv", "iv_ratio_0dte_7dte",
341
+ "vix", "vanna_exposure", "vanna_interpretation", "description"):
342
+ assert k in r["vol_context"], f"vol_context.{k} missing in typed access"
343
+
344
+ # flow
345
+ for k in ("total_volume", "call_volume", "put_volume",
346
+ "net_call_minus_put_volume",
347
+ "total_oi", "call_oi", "put_oi",
348
+ "pc_ratio_volume", "pc_ratio_oi", "volume_to_oi_ratio",
349
+ "atm_volume_share_pct", "top3_strike_volume_pct"):
350
+ assert k in r["flow"], f"flow.{k} missing in typed access"
351
+
352
+ # levels
353
+ for k in ("call_wall", "call_wall_gex", "call_wall_strength",
354
+ "distance_to_call_wall_pct",
355
+ "put_wall", "put_wall_gex", "put_wall_strength",
356
+ "distance_to_put_wall_pct",
357
+ "distance_to_magnet_dollars",
358
+ "highest_oi_strike", "highest_oi_total",
359
+ "max_positive_gamma", "max_negative_gamma",
360
+ "level_cluster_score"):
361
+ assert k in r["levels"], f"levels.{k} missing in typed access"
362
+
363
+ # liquidity
364
+ for k in ("atm_spread_pct", "weighted_spread_pct", "execution_score"):
365
+ assert k in r["liquidity"], f"liquidity.{k} missing in typed access"
366
+
367
+ # metadata
368
+ for k in ("snapshot_age_seconds", "chain_contract_count",
369
+ "data_quality_score", "greek_smoothness_score"):
370
+ assert k in r["metadata"], f"metadata.{k} missing in typed access"
371
+
372
+ # strikes[0] — every per-strike field
373
+ strikes = r["strikes"]
374
+ assert isinstance(strikes, list)
375
+ if strikes:
376
+ s = strikes[0]
377
+ for k in ("strike", "distance_from_spot_pct",
378
+ "call_symbol", "put_symbol",
379
+ "call_gex", "put_gex", "net_gex",
380
+ "call_dex", "put_dex", "net_dex",
381
+ "net_vex", "net_chex",
382
+ "call_oi", "put_oi", "call_volume", "put_volume",
383
+ "gex_share_pct", "oi_share_pct", "volume_share_pct",
384
+ "call_iv", "put_iv",
385
+ "call_delta", "put_delta",
386
+ "call_gamma", "put_gamma",
387
+ "call_theta", "put_theta",
388
+ "call_mid", "put_mid",
389
+ "call_spread_pct", "put_spread_pct"):
390
+ assert k in s, f"strikes[0].{k} missing in typed access"
391
+
392
+
139
393
  # ── Pricing ─────────────────────────────────────────────────────────
140
394
 
141
395
 
@@ -1,22 +0,0 @@
1
- """FlashAlpha Python SDK — options exposure analytics API."""
2
-
3
- from .client import FlashAlpha
4
- from .exceptions import (
5
- AuthenticationError,
6
- FlashAlphaError,
7
- NotFoundError,
8
- RateLimitError,
9
- ServerError,
10
- TierRestrictedError,
11
- )
12
-
13
- __version__ = "0.3.2"
14
- __all__ = [
15
- "FlashAlpha",
16
- "FlashAlphaError",
17
- "AuthenticationError",
18
- "TierRestrictedError",
19
- "NotFoundError",
20
- "RateLimitError",
21
- "ServerError",
22
- ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes