flashalpha 0.3.1__tar.gz → 0.3.2__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.
- {flashalpha-0.3.1 → flashalpha-0.3.2}/.claude/settings.json +4 -1
- {flashalpha-0.3.1 → flashalpha-0.3.2}/PKG-INFO +1 -1
- {flashalpha-0.3.1 → flashalpha-0.3.2}/pyproject.toml +1 -1
- {flashalpha-0.3.1 → flashalpha-0.3.2}/src/flashalpha/__init__.py +1 -1
- {flashalpha-0.3.1 → flashalpha-0.3.2}/src/flashalpha/client.py +19 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/tests/test_client.py +69 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/tests/test_integration.py +49 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/.github/workflows/ci.yml +0 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/.gitignore +0 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/CHANGELOG.md +0 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/CONTRIBUTING.md +0 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/LICENSE +0 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/README.md +0 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/article-flashalpha-python-sdk.md +0 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/awesome-investing/CODE_OF_CONDUCT.md +0 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/awesome-investing/LICENSE +0 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/awesome-investing/PULL_REQUEST_TEMPLATE.md +0 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/awesome-investing/README.md +0 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/awesome-investing/contributing.md +0 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/awesome_investing/README.md +0 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/awesome_investing/src/static/img/.gitkeep +0 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/awesome_investing/src/static/img/markus-spiske-5gGcn2PRrtc-unsplash.jpg +0 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/examples/quickstart.py +0 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/src/flashalpha/exceptions.py +0 -0
- {flashalpha-0.3.1 → flashalpha-0.3.2}/tests/__init__.py +0 -0
|
@@ -42,7 +42,10 @@
|
|
|
42
42
|
"Bash(grep -rhE \"oy2[a-z0-9]{40,46}|NUGET_API_KEY|NUGET_TOKEN\" --include=\"*.env\" --include=\"*.env.local\" --include=\"*.env.example\" --include=\".env*\")",
|
|
43
43
|
"Bash(npm profile:*)",
|
|
44
44
|
"Bash(NPM_CONFIG_TOKEN=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 npm publish --//registry.npmjs.org/:_authToken=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)",
|
|
45
|
-
"Bash(npm config:*)"
|
|
45
|
+
"Bash(npm config:*)",
|
|
46
|
+
"Bash(export PATH=\"$PATH:/c/Program Files/Go/bin\")",
|
|
47
|
+
"Bash(go test:*)",
|
|
48
|
+
"Bash(FLASHALPHA_API_KEY=\"ejbe0oW5f4n7C7drEg7Ggo6bPtFuFpaLPmvmkDrE\" \"C:/Users/disea/tools/apache-maven-3.9.14/bin/mvn.cmd\" test -Dtest=\"IntegrationTest#testMaxPain*\")"
|
|
46
49
|
],
|
|
47
50
|
"additionalDirectories": [
|
|
48
51
|
"C:\\Users\\disea"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flashalpha
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
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.
|
|
7
|
+
version = "0.3.2"
|
|
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"
|
|
@@ -299,6 +299,25 @@ class FlashAlpha:
|
|
|
299
299
|
"""Currently queried symbols with live data."""
|
|
300
300
|
return self._get("/v1/symbols")
|
|
301
301
|
|
|
302
|
+
# ── Max Pain ────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
def max_pain(self, symbol: str, *, expiration: str | None = None) -> dict:
|
|
305
|
+
"""Max pain analysis with dealer alignment overlay, pain curve, OI
|
|
306
|
+
breakdown, expected move context, pin probability, and multi-expiry
|
|
307
|
+
calendar. Requires Growth+.
|
|
308
|
+
|
|
309
|
+
Parameters
|
|
310
|
+
----------
|
|
311
|
+
symbol : str
|
|
312
|
+
Underlying symbol.
|
|
313
|
+
expiration : str, optional
|
|
314
|
+
Filter to single expiry (YYYY-MM-DD). Omit for full-chain analysis.
|
|
315
|
+
"""
|
|
316
|
+
params: dict[str, Any] = {}
|
|
317
|
+
if expiration:
|
|
318
|
+
params["expiration"] = expiration
|
|
319
|
+
return self._get(f"/v1/maxpain/{symbol}", params or None)
|
|
320
|
+
|
|
302
321
|
# ── Screener ────────────────────────────────────────────────────
|
|
303
322
|
|
|
304
323
|
def screener(
|
|
@@ -321,6 +321,75 @@ def test_health(fa):
|
|
|
321
321
|
assert result["status"] == "Healthy"
|
|
322
322
|
|
|
323
323
|
|
|
324
|
+
# ── Max Pain ────────────────────────────────────────────────────────
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@responses.activate
|
|
328
|
+
def test_max_pain(fa):
|
|
329
|
+
payload = {"symbol": "SPY", "max_pain_strike": 545, "pin_probability": 68}
|
|
330
|
+
responses.get(f"{BASE}/v1/maxpain/SPY", json=payload)
|
|
331
|
+
result = fa.max_pain("SPY")
|
|
332
|
+
assert result["max_pain_strike"] == 545
|
|
333
|
+
assert result["pin_probability"] == 68
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@responses.activate
|
|
337
|
+
def test_max_pain_calls_correct_path(fa):
|
|
338
|
+
responses.get(f"{BASE}/v1/maxpain/AAPL", json={})
|
|
339
|
+
fa.max_pain("AAPL")
|
|
340
|
+
assert "/v1/maxpain/AAPL" in responses.calls[0].request.url
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@responses.activate
|
|
344
|
+
def test_max_pain_with_expiration(fa):
|
|
345
|
+
responses.get(f"{BASE}/v1/maxpain/SPY", json={})
|
|
346
|
+
fa.max_pain("SPY", expiration="2026-04-17")
|
|
347
|
+
assert "expiration=2026-04-17" in responses.calls[0].request.url
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@responses.activate
|
|
351
|
+
def test_max_pain_without_expiration(fa):
|
|
352
|
+
responses.get(f"{BASE}/v1/maxpain/SPY", json={"max_pain_by_expiration": [{"expiration": "2026-04-17"}]})
|
|
353
|
+
result = fa.max_pain("SPY")
|
|
354
|
+
assert "max_pain_by_expiration" in result
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@responses.activate
|
|
358
|
+
def test_max_pain_full_response(fa):
|
|
359
|
+
payload = {
|
|
360
|
+
"symbol": "SPY",
|
|
361
|
+
"underlying_price": 548.32,
|
|
362
|
+
"max_pain_strike": 545,
|
|
363
|
+
"distance": {"absolute": 3.32, "percent": 0.61, "direction": "above"},
|
|
364
|
+
"signal": "neutral",
|
|
365
|
+
"put_call_oi_ratio": 1.284,
|
|
366
|
+
"pain_curve": [{"strike": 545, "total_pain": 3700000}],
|
|
367
|
+
"oi_by_strike": [{"strike": 545, "call_oi": 35000, "put_oi": 42000}],
|
|
368
|
+
"dealer_alignment": {"alignment": "converging", "gamma_flip": 546},
|
|
369
|
+
"regime": "positive_gamma",
|
|
370
|
+
"expected_move": {"max_pain_within_expected_range": True},
|
|
371
|
+
"pin_probability": 68,
|
|
372
|
+
}
|
|
373
|
+
responses.get(f"{BASE}/v1/maxpain/SPY", json=payload)
|
|
374
|
+
result = fa.max_pain("SPY")
|
|
375
|
+
assert result["distance"]["direction"] == "above"
|
|
376
|
+
assert result["dealer_alignment"]["alignment"] == "converging"
|
|
377
|
+
assert result["signal"] == "neutral"
|
|
378
|
+
assert result["regime"] == "positive_gamma"
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
@responses.activate
|
|
382
|
+
def test_max_pain_403_tier_restricted(fa):
|
|
383
|
+
responses.get(
|
|
384
|
+
f"{BASE}/v1/maxpain/SPY",
|
|
385
|
+
json={"status": "ERROR", "error": "tier_restricted", "message": "Requires Growth plan.", "current_plan": "Free", "required_plan": "Growth"},
|
|
386
|
+
status=403,
|
|
387
|
+
)
|
|
388
|
+
with pytest.raises(TierRestrictedError) as exc:
|
|
389
|
+
fa.max_pain("SPY")
|
|
390
|
+
assert exc.value.current_plan == "Free"
|
|
391
|
+
|
|
392
|
+
|
|
324
393
|
# ── Screener ───────────────────────────────────────────────────────
|
|
325
394
|
|
|
326
395
|
import json as _json
|
|
@@ -221,6 +221,55 @@ def test_symbols(fa):
|
|
|
221
221
|
assert isinstance(result["symbols"], list)
|
|
222
222
|
|
|
223
223
|
|
|
224
|
+
# ── Max Pain ─────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_max_pain(fa):
|
|
228
|
+
result = fa.max_pain("SPY")
|
|
229
|
+
assert "max_pain_strike" in result
|
|
230
|
+
assert "pain_curve" in result
|
|
231
|
+
assert "dealer_alignment" in result
|
|
232
|
+
assert "pin_probability" in result
|
|
233
|
+
assert isinstance(result["pain_curve"], list)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_max_pain_response_fields(fa):
|
|
237
|
+
result = fa.max_pain("SPY")
|
|
238
|
+
assert "distance" in result
|
|
239
|
+
assert result["distance"]["direction"] in ("above", "below", "at")
|
|
240
|
+
assert result["signal"] in ("bullish", "bearish", "neutral")
|
|
241
|
+
assert result["regime"] in ("positive_gamma", "negative_gamma")
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_max_pain_with_expiration(fa):
|
|
245
|
+
# Get first available expiration
|
|
246
|
+
opts = fa.options("SPY")
|
|
247
|
+
if opts.get("expirations") and len(opts["expirations"]) > 0:
|
|
248
|
+
exp = opts["expirations"][0]["expiration"]
|
|
249
|
+
result = fa.max_pain("SPY", expiration=exp)
|
|
250
|
+
assert result["expiration"] == exp
|
|
251
|
+
assert "max_pain_strike" in result
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_max_pain_oi_by_strike(fa):
|
|
255
|
+
result = fa.max_pain("SPY")
|
|
256
|
+
if result.get("oi_by_strike"):
|
|
257
|
+
row = result["oi_by_strike"][0]
|
|
258
|
+
assert "strike" in row
|
|
259
|
+
assert "call_oi" in row
|
|
260
|
+
assert "put_oi" in row
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_max_pain_multi_expiry_calendar(fa):
|
|
264
|
+
"""Without expiration filter, max_pain_by_expiration should be populated."""
|
|
265
|
+
result = fa.max_pain("SPY")
|
|
266
|
+
if result.get("max_pain_by_expiration"):
|
|
267
|
+
entry = result["max_pain_by_expiration"][0]
|
|
268
|
+
assert "expiration" in entry
|
|
269
|
+
assert "max_pain_strike" in entry
|
|
270
|
+
assert "dte" in entry
|
|
271
|
+
|
|
272
|
+
|
|
224
273
|
# ── Screener ────────────────────────────────────────────────────────
|
|
225
274
|
|
|
226
275
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|