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.
Files changed (25) hide show
  1. {flashalpha-0.3.1 → flashalpha-0.3.2}/.claude/settings.json +4 -1
  2. {flashalpha-0.3.1 → flashalpha-0.3.2}/PKG-INFO +1 -1
  3. {flashalpha-0.3.1 → flashalpha-0.3.2}/pyproject.toml +1 -1
  4. {flashalpha-0.3.1 → flashalpha-0.3.2}/src/flashalpha/__init__.py +1 -1
  5. {flashalpha-0.3.1 → flashalpha-0.3.2}/src/flashalpha/client.py +19 -0
  6. {flashalpha-0.3.1 → flashalpha-0.3.2}/tests/test_client.py +69 -0
  7. {flashalpha-0.3.1 → flashalpha-0.3.2}/tests/test_integration.py +49 -0
  8. {flashalpha-0.3.1 → flashalpha-0.3.2}/.github/workflows/ci.yml +0 -0
  9. {flashalpha-0.3.1 → flashalpha-0.3.2}/.gitignore +0 -0
  10. {flashalpha-0.3.1 → flashalpha-0.3.2}/CHANGELOG.md +0 -0
  11. {flashalpha-0.3.1 → flashalpha-0.3.2}/CONTRIBUTING.md +0 -0
  12. {flashalpha-0.3.1 → flashalpha-0.3.2}/LICENSE +0 -0
  13. {flashalpha-0.3.1 → flashalpha-0.3.2}/README.md +0 -0
  14. {flashalpha-0.3.1 → flashalpha-0.3.2}/article-flashalpha-python-sdk.md +0 -0
  15. {flashalpha-0.3.1 → flashalpha-0.3.2}/awesome-investing/CODE_OF_CONDUCT.md +0 -0
  16. {flashalpha-0.3.1 → flashalpha-0.3.2}/awesome-investing/LICENSE +0 -0
  17. {flashalpha-0.3.1 → flashalpha-0.3.2}/awesome-investing/PULL_REQUEST_TEMPLATE.md +0 -0
  18. {flashalpha-0.3.1 → flashalpha-0.3.2}/awesome-investing/README.md +0 -0
  19. {flashalpha-0.3.1 → flashalpha-0.3.2}/awesome-investing/contributing.md +0 -0
  20. {flashalpha-0.3.1 → flashalpha-0.3.2}/awesome_investing/README.md +0 -0
  21. {flashalpha-0.3.1 → flashalpha-0.3.2}/awesome_investing/src/static/img/.gitkeep +0 -0
  22. {flashalpha-0.3.1 → flashalpha-0.3.2}/awesome_investing/src/static/img/markus-spiske-5gGcn2PRrtc-unsplash.jpg +0 -0
  23. {flashalpha-0.3.1 → flashalpha-0.3.2}/examples/quickstart.py +0 -0
  24. {flashalpha-0.3.1 → flashalpha-0.3.2}/src/flashalpha/exceptions.py +0 -0
  25. {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.1
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.1"
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"
@@ -10,7 +10,7 @@ from .exceptions import (
10
10
  TierRestrictedError,
11
11
  )
12
12
 
13
- __version__ = "0.3.1"
13
+ __version__ = "0.3.2"
14
14
  __all__ = [
15
15
  "FlashAlpha",
16
16
  "FlashAlphaError",
@@ -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