Skip to content

Commit 3669846

Browse files
committed
Initial commit: GEX educational repo
Theory docs, from-scratch Python implementation of BSM gamma and GEX computation, matplotlib visualisation, FlashAlpha API comparison script, 25-row SPY sample chain, and a full pytest unit test suite (24 tests, all passing).
0 parents  commit 3669846

14 files changed

Lines changed: 1592 additions & 0 deletions

.gitignore

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
*.egg
7+
*.egg-info/
8+
dist/
9+
build/
10+
eggs/
11+
parts/
12+
var/
13+
sdist/
14+
develop-eggs/
15+
.installed.cfg
16+
lib/
17+
lib64/
18+
MANIFEST
19+
20+
# Virtual environments
21+
.env
22+
.venv
23+
env/
24+
venv/
25+
ENV/
26+
env.bak/
27+
venv.bak/
28+
29+
# Distribution / packaging
30+
.Python
31+
pip-wheel-metadata/
32+
share/python-wheels/
33+
*.egg-info/
34+
.eggs/
35+
36+
# Unit test / coverage reports
37+
htmlcov/
38+
.tox/
39+
.nox/
40+
.coverage
41+
.coverage.*
42+
.cache
43+
nosetests.xml
44+
coverage.xml
45+
*.cover
46+
*.py,cover
47+
.hypothesis/
48+
.pytest_cache/
49+
cover/
50+
51+
# Jupyter Notebook
52+
.ipynb_checkpoints
53+
54+
# IPython
55+
profile_default/
56+
ipython_config.py
57+
58+
# pyenv
59+
.python-version
60+
61+
# Environments — NEVER commit API keys
62+
.env
63+
.env.*
64+
*.env
65+
secrets.json
66+
credentials.json
67+
68+
# IDEs
69+
.idea/
70+
.vscode/
71+
*.swp
72+
*.swo
73+
*~
74+
75+
# OS
76+
.DS_Store
77+
.DS_Store?
78+
._*
79+
.Spotlight-V100
80+
.Trashes
81+
ehthumbs.db
82+
Thumbs.db
83+
84+
# Project-specific
85+
*.png
86+
*.svg
87+
plots/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 FlashAlpha Lab
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# What is Gamma Exposure (GEX) and Why It Moves Markets
2+
3+
This repository explains Gamma Exposure (GEX) from first principles — the math behind it, how it shapes market microstructure, and how to compute it yourself from a raw options chain. All code is runnable with publicly available data.
4+
5+
---
6+
7+
## Prerequisites
8+
9+
You should know what options are, what the Greeks mean, and roughly how market makers work. You do not need to be a quant.
10+
11+
---
12+
13+
## What is GEX?
14+
15+
Gamma Exposure is a measure of how much directional hedging pressure options dealers face as the underlying price moves.
16+
17+
Every time a retail trader or institution buys an option, a market maker (dealer) sells it and immediately hedges the resulting delta. That delta hedge is not static — it changes continuously as spot moves. The rate of change of delta with respect to spot is **gamma**. Dealers who are net short options are short gamma; their delta hedges move against price, forcing them to buy into rallies and sell into declines — or more precisely, to buy more as price falls and sell more as price rises. This is the source of GEX's market impact.
18+
19+
**GEX aggregates gamma across all strikes and expirations**, scaled to the notional dollar value each contract controls, giving you a single number (or a profile by strike) that represents how much dollar-hedging activity a one-percent move in the underlying would trigger.
20+
21+
---
22+
23+
## The Formula
24+
25+
The standard SpotGamma convention for dollar GEX per strike is:
26+
27+
```
28+
GEX = gamma * OI * 100 * spot^2 * 0.01
29+
```
30+
31+
Where:
32+
- `gamma` — the Black-Scholes gamma of the contract (per share, per dollar of underlying)
33+
- `OI` — open interest in contracts
34+
- `100` — shares per contract
35+
- `spot^2` — converts from per-share gamma to dollar-delta sensitivity
36+
- `0.01` — represents a 1% move in spot (scaling convention)
37+
38+
For **calls**, GEX is positive (dealers are long calls when customers buy them, so dealers are long gamma and act as stabilizers).
39+
40+
For **puts**, GEX is negated: dealers who sold puts to customers are short those puts, meaning they are short gamma on the put side.
41+
42+
```
43+
call_gex = gamma * OI * 100 * spot^2 * 0.01
44+
put_gex = -1 * gamma * OI * 100 * spot^2 * 0.01
45+
```
46+
47+
Net GEX at a given strike is the sum of call and put GEX at that strike. Total market GEX is the sum across all strikes and expirations.
48+
49+
See [theory/gamma-exposure.md](theory/gamma-exposure.md) for the full derivation.
50+
51+
---
52+
53+
## Why GEX Matters: Dealer Hedging Regimes
54+
55+
When aggregate GEX is **positive**, dealers in aggregate are long gamma. As spot rises, their delta increases, so they sell to rehedge. As spot falls, their delta decreases, so they buy. This is counter-cyclical — it dampens volatility and creates mean-reversion behavior around high-GEX strikes.
56+
57+
When aggregate GEX is **negative**, dealers are short gamma. As spot rises, their short-gamma position means their delta is getting shorter, so they buy to rehedge. As spot falls, they must sell. This is pro-cyclical — it amplifies moves and creates momentum-like behavior.
58+
59+
The practical implication: positive-GEX regimes tend to see range-bound, low-volatility trading. Negative-GEX regimes tend to see sharp, trending moves with elevated realized volatility.
60+
61+
See [theory/dealer-hedging.md](theory/dealer-hedging.md) and [theory/gex-regimes.md](theory/gex-regimes.md) for detail.
62+
63+
---
64+
65+
## The Gamma Flip
66+
67+
The **gamma flip** is the spot price at which aggregate dealer GEX crosses from positive to negative (or vice versa). It is arguably the most actionable level produced by GEX analysis.
68+
69+
- Above the gamma flip: positive gamma, dealers stabilize price
70+
- Below the gamma flip: negative gamma, dealers amplify price moves
71+
72+
Traders watch the gamma flip as a regime boundary. Sustained price action above it suggests a low-vol, mean-reverting environment. A break below it (especially with conviction) can trigger a volatility expansion as dealer hedging becomes pro-cyclical.
73+
74+
---
75+
76+
## Key Levels
77+
78+
Beyond the gamma flip, GEX analysis identifies:
79+
80+
- **Call Wall** — the strike with the highest positive GEX from calls. Dealers have maximum long-gamma exposure here; it often acts as a ceiling because dealer selling pressure intensifies as spot approaches it.
81+
- **Put Wall** — the strike with the highest negative GEX from puts. Dealers have maximum short-gamma exposure here; it often acts as a floor (or a trap door if breached).
82+
83+
---
84+
85+
## Code in This Repo
86+
87+
| File | What it does |
88+
|------|-------------|
89+
| [code/compute_gex.py](code/compute_gex.py) | Computes GEX from a raw CSV options chain |
90+
| [code/plot_gex.py](code/plot_gex.py) | Bar chart of GEX by strike with key levels marked |
91+
| [code/compare_with_api.py](code/compare_with_api.py) | Compares manual calculation against the FlashAlpha API |
92+
| [data/sample_chain.csv](data/sample_chain.csv) | Sample SPY options chain (~25 rows, realistic prices) |
93+
| [tests/test_compute_gex.py](tests/test_compute_gex.py) | Unit tests for the GEX computation logic |
94+
| [tests/test_integration.py](tests/test_integration.py) | Integration tests against the live FlashAlpha API |
95+
96+
Run the compute script:
97+
98+
```bash
99+
pip install numpy scipy matplotlib requests
100+
python code/compute_gex.py
101+
```
102+
103+
Run tests:
104+
105+
```bash
106+
pytest tests/test_compute_gex.py
107+
```
108+
109+
---
110+
111+
## Skip the Math
112+
113+
If you want production GEX data without implementing any of this yourself:
114+
115+
```bash
116+
pip install flashalpha
117+
```
118+
119+
```python
120+
import flashalpha as fa
121+
122+
gex = fa.gex("SPY") # full GEX profile by strike
123+
levels = fa.gex_levels("SPY") # gamma_flip, call_wall, put_wall
124+
125+
print(levels)
126+
```
127+
128+
The API is at `https://lab.flashalpha.com`. Auth via `X-Api-Key` header. See [code/compare_with_api.py](code/compare_with_api.py) for a raw-requests example.

code/compare_with_api.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
"""
2+
compare_with_api.py
3+
4+
Compares the manually computed GEX (from the sample CSV) against live GEX
5+
data returned by the FlashAlpha API.
6+
7+
This script uses the `requests` library directly — no SDK dependency —
8+
so it works as a standalone integration test and as a reference for how to
9+
call the FlashAlpha API yourself.
10+
11+
Usage:
12+
export FLASHALPHA_API_KEY=your_key_here
13+
python code/compare_with_api.py
14+
15+
The API key is read from the environment variable FLASHALPHA_API_KEY.
16+
Never hard-code an API key in source code.
17+
18+
API reference:
19+
Base URL : https://lab.flashalpha.com
20+
Auth : X-Api-Key: <your_key>
21+
Endpoints used:
22+
GET /gex/{ticker} -> full GEX profile by strike
23+
GET /gex/{ticker}/levels -> gamma_flip, call_wall, put_wall
24+
"""
25+
26+
import os
27+
import sys
28+
29+
import requests
30+
31+
# Allow importing from the same directory
32+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
33+
34+
from compute_gex import load_chain, compute_gex_profile
35+
36+
FLASHALPHA_BASE = "https://lab.flashalpha.com"
37+
TICKER = "SPY"
38+
39+
40+
def get_api_key() -> str:
41+
key = os.environ.get("FLASHALPHA_API_KEY", "")
42+
if not key:
43+
print("ERROR: FLASHALPHA_API_KEY environment variable is not set.")
44+
print(" Set it before running: export FLASHALPHA_API_KEY=your_key_here")
45+
sys.exit(1)
46+
return key
47+
48+
49+
def fetch_api_gex(ticker: str, api_key: str) -> dict:
50+
"""Fetch GEX profile by strike from the FlashAlpha API."""
51+
url = f"{FLASHALPHA_BASE}/gex/{ticker}"
52+
headers = {"X-Api-Key": api_key}
53+
resp = requests.get(url, headers=headers, timeout=15)
54+
resp.raise_for_status()
55+
return resp.json()
56+
57+
58+
def fetch_api_levels(ticker: str, api_key: str) -> dict:
59+
"""Fetch key GEX levels (gamma_flip, call_wall, put_wall) from the API."""
60+
url = f"{FLASHALPHA_BASE}/gex/{ticker}/levels"
61+
headers = {"X-Api-Key": api_key}
62+
resp = requests.get(url, headers=headers, timeout=15)
63+
resp.raise_for_status()
64+
return resp.json()
65+
66+
67+
def compare_levels(manual: dict, api_levels: dict) -> None:
68+
"""Print a side-by-side comparison of key levels."""
69+
print("\n=== Level Comparison: Manual vs FlashAlpha API ===")
70+
print(f"{'Level':<16} {'Manual':>12} {'API':>12} {'Delta':>12}")
71+
print("-" * 54)
72+
73+
pairs = [
74+
("gamma_flip", manual.get("gamma_flip"), api_levels.get("gamma_flip")),
75+
("call_wall", manual.get("call_wall"), api_levels.get("call_wall")),
76+
("put_wall", manual.get("put_wall"), api_levels.get("put_wall")),
77+
]
78+
79+
for name, m_val, a_val in pairs:
80+
m_str = f"{m_val:.2f}" if m_val is not None else "N/A"
81+
a_str = f"{a_val:.2f}" if a_val is not None else "N/A"
82+
if m_val is not None and a_val is not None:
83+
delta_str = f"{a_val - m_val:+.2f}"
84+
else:
85+
delta_str = "N/A"
86+
print(f"{name:<16} {m_str:>12} {a_str:>12} {delta_str:>12}")
87+
88+
print()
89+
print("Note: Differences are expected. The manual calculation uses the")
90+
print("sample CSV (one expiry, approximate IV) while the API uses the full")
91+
print("live options chain with accurate IV from real-time quotes.")
92+
93+
94+
def main() -> None:
95+
api_key = get_api_key()
96+
97+
# --- Manual computation from sample CSV ---
98+
script_dir = os.path.dirname(os.path.abspath(__file__))
99+
csv_path = os.path.join(script_dir, "..", "data", "sample_chain.csv")
100+
101+
print(f"Loading sample chain from: {csv_path}")
102+
chain = load_chain(csv_path)
103+
manual_result = compute_gex_profile(chain)
104+
105+
manual_levels = {
106+
"gamma_flip": manual_result["gamma_flip"],
107+
"call_wall": manual_result["call_wall"],
108+
"put_wall": manual_result["put_wall"],
109+
}
110+
print(f"Manual total GEX : ${manual_result['total_gex']/1e9:.3f}B")
111+
112+
# --- FlashAlpha API ---
113+
print(f"\nFetching live GEX levels for {TICKER} from FlashAlpha API...")
114+
try:
115+
api_levels = fetch_api_levels(TICKER, api_key)
116+
except requests.HTTPError as exc:
117+
print(f"API request failed: {exc}")
118+
sys.exit(1)
119+
120+
print(f"API response: {api_levels}")
121+
122+
# --- Comparison ---
123+
compare_levels(manual_levels, api_levels)
124+
125+
126+
if __name__ == "__main__":
127+
main()

0 commit comments

Comments
 (0)