Skip to content

Commit fc9d5a0

Browse files
committed
Fix integration tests and build config
- Use correct API paths (/v1/exposure/gex/, /v1/exposure/levels/) - Switch from setuptools to hatchling build backend - Lower Python requirement to >=3.10
1 parent 3669846 commit fc9d5a0

2 files changed

Lines changed: 57 additions & 129 deletions

File tree

pyproject.toml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
[build-system]
2-
requires = ["setuptools>=68", "wheel"]
3-
build-backend = "setuptools.backends.legacy:build"
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
44

55
[project]
66
name = "gex-explained"
77
version = "0.1.0"
88
description = "Educational implementation of Gamma Exposure (GEX) from scratch"
99
readme = "README.md"
10-
requires-python = ">=3.11"
10+
requires-python = ">=3.10"
1111
license = { text = "MIT" }
1212
dependencies = [
1313
"numpy>=1.26",
@@ -28,6 +28,5 @@ markers = [
2828
"integration: marks tests as requiring a live API key (deselect with -m 'not integration')",
2929
]
3030

31-
[tool.setuptools.packages.find]
32-
where = ["."]
33-
include = ["code*"]
31+
[tool.hatch.build.targets.wheel]
32+
packages = ["code"]

tests/test_integration.py

Lines changed: 52 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,9 @@
11
"""
2-
tests/test_integration.py
3-
42
Integration 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-
94
Run 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

189
import os
@@ -23,9 +14,6 @@
2314
FLASHALPHA_BASE = "https://lab.flashalpha.com"
2415
TICKER = "SPY"
2516

26-
# ---------------------------------------------------------------------------
27-
# Shared fixture: skip if no API key
28-
# ---------------------------------------------------------------------------
2917

3018
@pytest.fixture(scope="module")
3119
def 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-
4731
def 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
5939
class 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
9366
class 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
16295
class 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

Comments
 (0)