11"""
2- tests/test_integration.py
3-
42Integration tests against the live FlashAlpha API.
53
6- These tests require a valid FLASHALPHA_API_KEY environment variable.
7- If the variable is not set, all tests in this file are skipped automatically.
8-
94Run with:
105 export FLASHALPHA_API_KEY=your_key_here
116 pytest tests/test_integration.py -v -m integration
12-
13- API reference:
14- Base URL : https://lab.flashalpha.com
15- Auth : X-Api-Key: <your_key>
167"""
178
189import os
2314FLASHALPHA_BASE = "https://lab.flashalpha.com"
2415TICKER = "SPY"
2516
26- # ---------------------------------------------------------------------------
27- # Shared fixture: skip if no API key
28- # ---------------------------------------------------------------------------
2917
3018@pytest .fixture (scope = "module" )
3119def api_key () -> str :
@@ -40,136 +28,77 @@ def auth_headers(api_key: str) -> dict:
4028 return {"X-Api-Key" : api_key }
4129
4230
43- # ---------------------------------------------------------------------------
44- # Helper
45- # ---------------------------------------------------------------------------
46-
4731def get (path : str , headers : dict , timeout : int = 15 ) -> dict :
4832 url = f"{ FLASHALPHA_BASE } { path } "
4933 resp = requests .get (url , headers = headers , timeout = timeout )
5034 resp .raise_for_status ()
5135 return resp .json ()
5236
5337
54- # ---------------------------------------------------------------------------
55- # GEX endpoint: /gex/{ticker}
56- # ---------------------------------------------------------------------------
57-
5838@pytest .mark .integration
5939class TestGexEndpoint :
6040 def test_gex_returns_200 (self , auth_headers ):
61- url = f"{ FLASHALPHA_BASE } /gex/{ TICKER } "
41+ url = f"{ FLASHALPHA_BASE } /v1/exposure/ gex/{ TICKER } "
6242 resp = requests .get (url , headers = auth_headers , timeout = 15 )
63- assert resp .status_code == 200 , f"Expected 200, got { resp .status_code } : { resp .text } "
64-
65- def test_gex_response_is_json (self , auth_headers ):
66- data = get (f"/gex/{ TICKER } " , auth_headers )
67- assert isinstance (data , (dict , list )), f"Expected dict or list, got { type (data )} "
68-
69- def test_gex_contains_strike_data (self , auth_headers ):
70- data = get (f"/gex/{ TICKER } " , auth_headers )
71- # Accept either a dict (keyed by strike) or a list of strike records
72- assert len (data ) > 0 , "GEX response is empty"
73-
74- def test_gex_values_are_numeric (self , auth_headers ):
75- data = get (f"/gex/{ TICKER } " , auth_headers )
76- if isinstance (data , dict ):
77- for key , val in data .items ():
78- assert isinstance (val , (int , float )), (
79- f"GEX value at strike { key } is not numeric: { val !r} "
80- )
81- elif isinstance (data , list ):
82- for item in data :
83- assert "gex" in item or "net_gex" in item , (
84- f"GEX list item missing gex field: { item } "
85- )
86-
87-
88- # ---------------------------------------------------------------------------
89- # Levels endpoint: /gex/{ticker}/levels
90- # ---------------------------------------------------------------------------
43+ assert resp .status_code == 200
44+
45+ def test_gex_has_required_fields (self , auth_headers ):
46+ data = get (f"/v1/exposure/gex/{ TICKER } " , auth_headers )
47+ assert data ["symbol" ] == TICKER
48+ assert "net_gex" in data
49+ assert "gamma_flip" in data
50+ assert isinstance (data ["strikes" ], list )
51+ assert len (data ["strikes" ]) > 0
52+
53+ def test_gex_strike_fields (self , auth_headers ):
54+ data = get (f"/v1/exposure/gex/{ TICKER } " , auth_headers )
55+ strike = data ["strikes" ][0 ]
56+ for field in ("strike" , "call_gex" , "put_gex" , "net_gex" ):
57+ assert field in strike , f"Missing field '{ field } ' in strike data"
58+ assert isinstance (strike [field ], (int , float ))
59+
60+ def test_net_gex_is_numeric (self , auth_headers ):
61+ data = get (f"/v1/exposure/gex/{ TICKER } " , auth_headers )
62+ assert isinstance (data ["net_gex" ], (int , float ))
63+
9164
9265@pytest .mark .integration
9366class TestLevelsEndpoint :
9467 def test_levels_returns_200 (self , auth_headers ):
95- url = f"{ FLASHALPHA_BASE } /gex/ { TICKER } /levels "
68+ url = f"{ FLASHALPHA_BASE } /v1/exposure/levels/ { TICKER } "
9669 resp = requests .get (url , headers = auth_headers , timeout = 15 )
97- assert resp .status_code == 200 , f"Expected 200, got { resp .status_code } : { resp .text } "
98-
99- def test_levels_contains_call_wall (self , auth_headers ):
100- data = get (f"/gex/{ TICKER } /levels" , auth_headers )
101- assert "call_wall" in data , f"'call_wall' missing from levels response: { data } "
102-
103- def test_levels_contains_put_wall (self , auth_headers ):
104- data = get (f"/gex/{ TICKER } /levels" , auth_headers )
105- assert "put_wall" in data , f"'put_wall' missing from levels response: { data } "
106-
107- def test_levels_contains_gamma_flip (self , auth_headers ):
108- data = get (f"/gex/{ TICKER } /levels" , auth_headers )
109- assert "gamma_flip" in data , f"'gamma_flip' missing from levels response: { data } "
110-
111- def test_gamma_flip_is_between_put_wall_and_call_wall (self , auth_headers ):
112- """
113- The gamma flip should be a reasonable price level — between the put wall
114- and call wall (not necessarily strictly between, but within striking distance).
115- """
116- data = get (f"/gex/{ TICKER } /levels" , auth_headers )
117- flip = data .get ("gamma_flip" )
118- put_wall = data .get ("put_wall" )
119- call_wall = data .get ("call_wall" )
120-
121- if flip is None :
122- pytest .skip ("gamma_flip is None — no flip in current strike range" )
123-
124- assert isinstance (flip , (int , float )), f"gamma_flip is not numeric: { flip !r} "
125- assert isinstance (put_wall , (int , float )), f"put_wall is not numeric: { put_wall !r} "
126- assert isinstance (call_wall , (int , float )), f"call_wall is not numeric: { call_wall !r} "
127-
128- lower = min (put_wall , call_wall )
129- upper = max (put_wall , call_wall )
130-
131- # Allow 10% buffer outside the put/call wall range
132- buffer = (upper - lower ) * 0.10
133- assert lower - buffer <= flip <= upper + buffer , (
134- f"gamma_flip { flip } is not near the range [{ lower } , { upper } ]"
135- )
136-
137- def test_call_wall_is_positive_number (self , auth_headers ):
138- data = get (f"/gex/{ TICKER } /levels" , auth_headers )
139- call_wall = data ["call_wall" ]
140- assert isinstance (call_wall , (int , float ))
141- assert call_wall > 0
142-
143- def test_put_wall_is_positive_number (self , auth_headers ):
144- data = get (f"/gex/{ TICKER } /levels" , auth_headers )
145- put_wall = data ["put_wall" ]
146- assert isinstance (put_wall , (int , float ))
147- assert put_wall > 0
148-
149- def test_call_wall_is_above_put_wall (self , auth_headers ):
150- """Call wall should be at a higher strike than put wall in normal markets."""
151- data = get (f"/gex/{ TICKER } /levels" , auth_headers )
152- assert data ["call_wall" ] > data ["put_wall" ], (
153- f"call_wall ({ data ['call_wall' ]} ) should be above put_wall ({ data ['put_wall' ]} )"
154- )
155-
156-
157- # ---------------------------------------------------------------------------
158- # Auth: invalid key should return 401 or 403
159- # ---------------------------------------------------------------------------
70+ assert resp .status_code == 200
71+
72+ def test_levels_has_required_fields (self , auth_headers ):
73+ data = get (f"/v1/exposure/levels/{ TICKER } " , auth_headers )
74+ levels = data ["levels" ]
75+ for field in ("gamma_flip" , "call_wall" , "put_wall" ):
76+ assert field in levels , f"Missing '{ field } ' in levels"
77+
78+ def test_walls_are_positive (self , auth_headers ):
79+ levels = get (f"/v1/exposure/levels/{ TICKER } " , auth_headers )["levels" ]
80+ assert levels ["call_wall" ] > 0
81+ assert levels ["put_wall" ] > 0
82+
83+ def test_call_wall_above_put_wall (self , auth_headers ):
84+ levels = get (f"/v1/exposure/levels/{ TICKER } " , auth_headers )["levels" ]
85+ assert levels ["call_wall" ] >= levels ["put_wall" ]
86+
87+ def test_gamma_flip_is_reasonable (self , auth_headers ):
88+ levels = get (f"/v1/exposure/levels/{ TICKER } " , auth_headers )["levels" ]
89+ flip = levels ["gamma_flip" ]
90+ assert isinstance (flip , (int , float ))
91+ assert flip > 0
92+
16093
16194@pytest .mark .integration
16295class TestAuth :
163- def test_missing_key_returns_error (self ):
164- url = f"{ FLASHALPHA_BASE } /gex/ { TICKER } /levels "
96+ def test_missing_key_returns_401 (self ):
97+ url = f"{ FLASHALPHA_BASE } /v1/exposure/levels/ { TICKER } "
16598 resp = requests .get (url , headers = {}, timeout = 15 )
166- assert resp .status_code in (401 , 403 ), (
167- f"Expected 401 or 403 for missing key, got { resp .status_code } "
168- )
99+ assert resp .status_code == 401
169100
170- def test_invalid_key_returns_error (self ):
171- url = f"{ FLASHALPHA_BASE } /gex/ { TICKER } /levels "
101+ def test_invalid_key_returns_401 (self ):
102+ url = f"{ FLASHALPHA_BASE } /v1/exposure/levels/ { TICKER } "
172103 resp = requests .get (url , headers = {"X-Api-Key" : "invalid-key-xyz" }, timeout = 15 )
173- assert resp .status_code in (401 , 403 ), (
174- f"Expected 401 or 403 for invalid key, got { resp .status_code } "
175- )
104+ assert resp .status_code == 401
0 commit comments