maxkru92 commited on
Commit
b59acbe
·
verified ·
1 Parent(s): ed9c561

Upload cboe_adapter.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. cboe_adapter.py +396 -16
cboe_adapter.py CHANGED
@@ -1,27 +1,407 @@
1
  """
2
- Lightweight CBOE adapter: attempts to fetch options chain JSON from CBOE CDN and normalize into DataFrames.
3
- Falls back to None in demo mode.
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  """
 
5
  import requests
6
  import pandas as pd
 
7
  import os
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
 
9
- CBOE_CDN = 'https://cdn.cboe.com/api/global/delayed_quotes/options' # placeholder
 
 
 
10
 
11
 
12
- def fetch_options_chain(symbol):
13
- """Return (calls_df, puts_df, expiries) or None on failure."""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  try:
15
- # This is a placeholder fetch - real endpoint and parsing required
16
- if os.environ.get('DEMO_MODE','1')=='1':
 
 
 
 
17
  return None
18
- url = f"{CBOE_CDN}/{symbol}.json"
19
- r = requests.get(url, timeout=8)
20
- j = r.json()
21
- # Parse into calls, puts - simplified
22
- calls = pd.DataFrame(j.get('calls', []))
23
- puts = pd.DataFrame(j.get('puts', []))
24
- expiries = sorted(j.get('expirations', []))
25
- return calls, puts, expiries
26
- except Exception:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
+ CBOE Adapter Module
3
+ ===================
4
+ Fetches and normalizes options chain data from CBOE delayed quotes API.
5
+
6
+ Supports:
7
+ - SPX (S&P 500 Index options)
8
+ - VIX (Volatility Index options)
9
+ - Other CBOE-listed index options
10
+
11
+ Functions:
12
+ - fetch_cboe_chain: Fetch raw CBOE options chain
13
+ - parse_cboe_chain: Parse raw JSON into normalized DataFrames
14
+ - fetch_spx_chain: Convenience function for SPX
15
+ - fetch_vix_chain: Convenience function for VIX
16
+ - get_spot_price: Get current spot price from CBOE
17
  """
18
+
19
  import requests
20
  import pandas as pd
21
+ import numpy as np
22
  import os
23
+ import logging
24
+ from typing import Optional, Dict, Any, List, Tuple
25
+ from datetime import datetime, timedelta
26
+
27
+ logger = logging.getLogger("mk_quant.cboe")
28
+
29
+ CBOE_BASE_URL = "https://cdn.cboe.com/api/global/delayed_quotes/options"
30
+
31
+ # Map of supported symbols to their CBOE API identifiers
32
+ CBOE_SYMBOLS = {
33
+ "SPX": "SPX",
34
+ "VIX": "VIX",
35
+ "NDX": "NDX",
36
+ "RUT": "RUT",
37
+ "DJX": "DJX",
38
+ "SPY": "SPY",
39
+ "QQQ": "QQQ",
40
+ }
41
 
42
+ HEADERS = {
43
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36",
44
+ "Accept": "application/json",
45
+ }
46
 
47
 
48
+ def fetch_cboe_chain(symbol: str) -> Optional[Dict[str, Any]]:
49
+ """
50
+ Fetch raw options chain from CBOE delayed quotes API.
51
+
52
+ Parameters
53
+ ----------
54
+ symbol : CBOE symbol (e.g., 'SPX', 'VIX')
55
+
56
+ Returns
57
+ -------
58
+ Raw JSON response dict or None on failure
59
+ """
60
+ cboe_symbol = CBOE_SYMBOLS.get(symbol.upper(), symbol.upper())
61
+ url = f"{CBOE_BASE_URL}/{cboe_symbol}.json"
62
+
63
+ demo_mode = os.environ.get("DEMO_MODE", "0") == "1"
64
+ if demo_mode:
65
+ logger.info(f"DEMO_MODE: Skipping CBOE fetch for {symbol}")
66
+ return None
67
+
68
  try:
69
+ r = requests.get(url, headers=HEADERS, timeout=15)
70
+ r.raise_for_status()
71
+ data = r.json()
72
+
73
+ if "data" not in data:
74
+ logger.warning(f"CBOE response missing 'data' key for {symbol}")
75
  return None
76
+
77
+ return data
78
+
79
+ except requests.exceptions.Timeout:
80
+ logger.warning(f"CBOE timeout for {symbol}")
81
+ except requests.exceptions.HTTPError as e:
82
+ logger.warning(f"CBOE HTTP error for {symbol}: {e}")
83
+ except Exception as e:
84
+ logger.warning(f"CBOE fetch error for {symbol}: {e}")
85
+
86
+ return None
87
+
88
+
89
+ def parse_cboe_chain(raw_data: Dict[str, Any]) -> Dict[str, Any]:
90
+ """
91
+ Parse raw CBOE JSON into normalized format.
92
+
93
+ Returns dict with:
94
+ - spot: Current spot price
95
+ - records: List of strike records with keys:
96
+ strike, expiry, option_type, iv, delta, gamma, theta, vega,
97
+ open_interest, bid, ask, last
98
+ - calls_df: DataFrame of call options
99
+ - puts_df: DataFrame of put options
100
+ - expirations: Sorted list of expiration dates
101
+ - timestamp: Fetch timestamp
102
+ """
103
+ if not raw_data or "data" not in raw_data:
104
+ return {"spot": 0, "records": [], "calls_df": pd.DataFrame(),
105
+ "puts_df": pd.DataFrame(), "expirations": [], "timestamp": ""}
106
+
107
+ data = raw_data["data"]
108
+ spot = float(data.get("current_price", 0))
109
+ options = data.get("options", [])
110
+ timestamp = datetime.utcnow().isoformat()
111
+
112
+ if not options:
113
+ return {"spot": spot, "records": [], "calls_df": pd.DataFrame(),
114
+ "puts_df": pd.DataFrame(), "expirations": [], "timestamp": timestamp}
115
+
116
+ # Parse all options into records
117
+ records = []
118
+ calls_records = []
119
+ puts_records = []
120
+
121
+ for opt in options:
122
+ try:
123
+ strike = float(opt.get("strike", 0))
124
+ if strike <= 0:
125
+ continue
126
+
127
+ expiry = str(opt.get("expiration", ""))
128
+ otype = str(opt.get("option_type", "")).upper()
129
+
130
+ record = {
131
+ "strike": strike,
132
+ "expiry": expiry,
133
+ "option_type": otype,
134
+ "iv": float(opt.get("iv", 0) or 0),
135
+ "delta": float(opt.get("delta", 0) or 0),
136
+ "gamma": float(opt.get("gamma", 0) or 0),
137
+ "theta": float(opt.get("theta", 0) or 0),
138
+ "vega": float(opt.get("vega", 0) or 0),
139
+ "open_interest": int(opt.get("open_interest", 0) or 0),
140
+ "bid": float(opt.get("bid", 0) or 0),
141
+ "ask": float(opt.get("ask", 0) or 0),
142
+ "last": float(opt.get("last_price", 0) or 0),
143
+ "volume": int(opt.get("volume", 0) or 0),
144
+ }
145
+
146
+ records.append(record)
147
+
148
+ if otype == "C":
149
+ calls_records.append(record)
150
+ elif otype == "P":
151
+ puts_records.append(record)
152
+
153
+ except (ValueError, TypeError) as e:
154
+ logger.debug(f"Skipping malformed option record: {e}")
155
+ continue
156
+
157
+ calls_df = pd.DataFrame(calls_records) if calls_records else pd.DataFrame()
158
+ puts_df = pd.DataFrame(puts_records) if puts_records else pd.DataFrame()
159
+
160
+ # Get unique expirations sorted
161
+ expirations = sorted(set(r["expiry"] for r in records if r["expiry"]))
162
+
163
+ return {
164
+ "spot": spot,
165
+ "records": records,
166
+ "calls_df": calls_df,
167
+ "puts_df": puts_df,
168
+ "expirations": expirations,
169
+ "timestamp": timestamp,
170
+ "source": "cboe_live",
171
+ }
172
+
173
+
174
+ def fetch_spx_chain() -> Optional[Dict[str, Any]]:
175
+ """
176
+ Fetch SPX options chain from CBOE.
177
+
178
+ Returns parsed chain dict or None on failure.
179
+ """
180
+ raw = fetch_cboe_chain("SPX")
181
+ if raw is None:
182
  return None
183
+ return parse_cboe_chain(raw)
184
+
185
+
186
+ def fetch_vix_chain() -> Optional[Dict[str, Any]]:
187
+ """
188
+ Fetch VIX options chain from CBOE.
189
+
190
+ Returns parsed chain dict or None on failure.
191
+ """
192
+ raw = fetch_cboe_chain("VIX")
193
+ if raw is None:
194
+ return None
195
+ return parse_cboe_chain(raw)
196
+
197
+
198
+ def get_spot_price(symbol: str) -> Optional[float]:
199
+ """
200
+ Get current spot price for a CBOE-listed symbol.
201
+
202
+ Returns float price or None on failure.
203
+ """
204
+ raw = fetch_cboe_chain(symbol)
205
+ if raw and "data" in raw:
206
+ return float(raw["data"].get("current_price", 0))
207
+ return None
208
+
209
+
210
+ def fetch_cboe_multi_expiry(symbol: str, max_expiries: int = 6) -> Dict[str, Any]:
211
+ """
212
+ Fetch options chain and organize by expiry.
213
+
214
+ Returns dict with:
215
+ - spot: Current spot price
216
+ - by_expiry: Dict mapping expiry -> {calls_df, puts_df, records}
217
+ - all_records: All records combined
218
+ - expirations: List of expirations
219
+ """
220
+ chain = fetch_cboe_chain(symbol)
221
+ if chain is None:
222
+ return {"spot": 0, "by_expiry": {}, "all_records": [], "expirations": []}
223
+
224
+ parsed = parse_cboe_chain(chain)
225
+
226
+ by_expiry = {}
227
+ for exp in parsed["expirations"][:max_expiries]:
228
+ exp_records = [r for r in parsed["records"] if r["expiry"] == exp]
229
+ exp_calls = [r for r in exp_records if r["option_type"] == "C"]
230
+ exp_puts = [r for r in exp_records if r["option_type"] == "P"]
231
+
232
+ by_expiry[exp] = {
233
+ "records": exp_records,
234
+ "calls_df": pd.DataFrame(exp_calls) if exp_calls else pd.DataFrame(),
235
+ "puts_df": pd.DataFrame(exp_puts) if exp_puts else pd.DataFrame(),
236
+ }
237
+
238
+ return {
239
+ "spot": parsed["spot"],
240
+ "by_expiry": by_expiry,
241
+ "all_records": parsed["records"],
242
+ "expirations": parsed["expirations"][:max_expiries],
243
+ "timestamp": parsed["timestamp"],
244
+ }
245
+
246
+
247
+ def generate_synthetic_chain(
248
+ spot: float = 6632.0,
249
+ n_strikes: int = 48,
250
+ strike_step: int = 25,
251
+ expiries: List[str] = None,
252
+ ) -> Dict[str, Any]:
253
+ """
254
+ Generate synthetic options chain for demo/testing.
255
+
256
+ Creates realistic-looking options data with proper skew and gamma profile.
257
+ """
258
+ rng = np.random.default_rng(42)
259
+
260
+ if expiries is None:
261
+ # Generate 4 weekly + 2 monthly expiries
262
+ today = datetime.utcnow().date()
263
+ expiries = []
264
+ for i in range(1, 5):
265
+ d = today + timedelta(weeks=i)
266
+ expiries.append(d.strftime("%Y-%m-%d"))
267
+ for i in range(1, 3):
268
+ d = today + timedelta(days=30*i)
269
+ expiries.append(d.strftime("%Y-%m-%d"))
270
+
271
+ center = spot
272
+ strikes = np.arange(center - (n_strikes//2)*strike_step,
273
+ center + (n_strikes//2)*strike_step + 1,
274
+ strike_step)
275
+
276
+ all_records = []
277
+
278
+ for exp in expiries:
279
+ try:
280
+ exp_date = datetime.strptime(exp, "%Y-%m-%d").date()
281
+ dte = max(1, (exp_date - datetime.utcnow().date()).days)
282
+ except:
283
+ dte = 30
284
+
285
+ T = dte / 365.0
286
+
287
+ for strike in strikes:
288
+ atm_dist = (strike - spot) / spot
289
+
290
+ # Realistic IV skew
291
+ base_iv = 0.18 + abs(atm_dist) * 0.3 - atm_dist * 0.05
292
+ iv_call = max(0.05, base_iv + rng.normal(0, 0.005))
293
+ iv_put = max(0.05, iv_call + 0.01 + max(0.0, -atm_dist * 0.08) + rng.normal(0, 0.005))
294
+
295
+ # Gamma profile (peaks ATM, decays with distance)
296
+ gamma_base = 0.003 * np.exp(-50 * atm_dist**2)
297
+ gamma_call = max(0.00001, gamma_base * (1 + rng.normal(0, 0.1)))
298
+ gamma_put = max(0.00001, gamma_base * (1 + rng.normal(0, 0.1)))
299
+
300
+ # Delta
301
+ if T > 0 and iv_call > 0:
302
+ d1 = (np.log(spot/strike) + (0.5 * iv_call**2) * T) / (iv_call * np.sqrt(T))
303
+ delta_call = float(max(0.01, min(0.99, 0.5 + d1 * 0.4)))
304
+ else:
305
+ delta_call = 0.5
306
+
307
+ if T > 0 and iv_put > 0:
308
+ d1 = (np.log(spot/strike) + (0.5 * iv_put**2) * T) / (iv_put * np.sqrt(T))
309
+ delta_put = float(max(-0.99, min(-0.01, -0.5 + d1 * 0.4)))
310
+ else:
311
+ delta_put = -0.5
312
+
313
+ # OI (higher ATM, lower wings)
314
+ oi_base = abs(rng.normal(8000, 3000))
315
+ oi_factor = np.exp(-30 * atm_dist**2) + 0.1
316
+ oi_call = int(oi_base * oi_factor)
317
+ oi_put = int(oi_base * oi_factor * 1.2) # Slightly more put OI
318
+
319
+ # Theta
320
+ theta_call = -gamma_call * spot * spot * iv_call / (2 * np.sqrt(T)) / 365 if T > 0 else 0
321
+ theta_put = -gamma_put * spot * spot * iv_put / (2 * np.sqrt(T)) / 365 if T > 0 else 0
322
+
323
+ # Vega
324
+ vega_val = spot * np.sqrt(T) * np.exp(-0.5 * ((np.log(spot/strike)/iv_call)**2 if iv_call > 0 else 0)) * 0.01 if T > 0 and iv_call > 0 else 0
325
+
326
+ # Vanna
327
+ vanna_val = -gamma_call * (1 - delta_call) / iv_call if iv_call > 0 else 0
328
+
329
+ # Bid/ask (synthetic spread)
330
+ intrinsic_c = max(0, spot - strike)
331
+ time_value_c = max(0.01, iv_call * spot * np.sqrt(T) * 0.4)
332
+ mid_c = intrinsic_c + time_value_c
333
+ spread_c = max(0.1, mid_c * 0.02)
334
+
335
+ intrinsic_p = max(0, strike - spot)
336
+ time_value_p = max(0.01, iv_put * spot * np.sqrt(T) * 0.4)
337
+ mid_p = intrinsic_p + time_value_p
338
+ spread_p = max(0.1, mid_p * 0.02)
339
+
340
+ # Call record
341
+ all_records.append({
342
+ "strike": float(strike),
343
+ "expiry": exp,
344
+ "option_type": "C",
345
+ "iv": round(iv_call, 4),
346
+ "delta": round(delta_call, 4),
347
+ "gamma": round(gamma_call, 6),
348
+ "theta": round(theta_call, 4),
349
+ "vega": round(vega_val, 4),
350
+ "vanna": round(vanna_val, 4),
351
+ "open_interest": oi_call,
352
+ "bid": round(max(0.01, mid_c - spread_c/2), 2),
353
+ "ask": round(mid_c + spread_c/2, 2),
354
+ "last": round(mid_c, 2),
355
+ "volume": int(abs(rng.normal(1000, 500))),
356
+ })
357
+
358
+ # Put record
359
+ all_records.append({
360
+ "strike": float(strike),
361
+ "expiry": exp,
362
+ "option_type": "P",
363
+ "iv": round(iv_put, 4),
364
+ "delta": round(delta_put, 4),
365
+ "gamma": round(gamma_put, 6),
366
+ "theta": round(theta_put, 4),
367
+ "vega": round(vega_val, 4),
368
+ "vanna": round(vanna_val, 4),
369
+ "open_interest": oi_put,
370
+ "bid": round(max(0.01, mid_p - spread_p/2), 2),
371
+ "ask": round(mid_p + spread_p/2, 2),
372
+ "last": round(mid_p, 2),
373
+ "volume": int(abs(rng.normal(1000, 500))),
374
+ })
375
+
376
+ calls_df = pd.DataFrame([r for r in all_records if r["option_type"] == "C"])
377
+ puts_df = pd.DataFrame([r for r in all_records if r["option_type"] == "P"])
378
+
379
+ return {
380
+ "spot": spot,
381
+ "records": all_records,
382
+ "calls_df": calls_df,
383
+ "puts_df": puts_df,
384
+ "expirations": expiries,
385
+ "timestamp": datetime.utcnow().isoformat(),
386
+ "source": "synthetic",
387
+ }
388
+
389
+
390
+ if __name__ == "__main__":
391
+ # Test live fetch
392
+ print("Testing CBOE SPX chain fetch...")
393
+ chain = fetch_spx_chain()
394
+
395
+ if chain:
396
+ print(f"Spot: {chain['spot']}")
397
+ print(f"Records: {len(chain['records'])}")
398
+ print(f"Expirations: {chain['expirations'][:5]}")
399
+ if not chain['calls_df'].empty:
400
+ print(f"Calls columns: {list(chain['calls_df'].columns)}")
401
+ print(chain['calls_df'].head(3).to_string())
402
+ else:
403
+ print("Live fetch failed, generating synthetic chain...")
404
+ synth = generate_synthetic_chain()
405
+ print(f"Synthetic spot: {synth['spot']}")
406
+ print(f"Synthetic records: {len(synth['records'])}")
407
+ print(f"Synthetic expirations: {synth['expirations']}")