maxkru92 commited on
Commit
107af29
·
verified ·
1 Parent(s): 0d31e7a

feat: vol-regime module

Browse files
Files changed (1) hide show
  1. cboe.py +614 -0
cboe.py ADDED
@@ -0,0 +1,614 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ CBOE (Chicago Board Options Exchange) data source.
3
+
4
+ This module provides web scrapers/crawlers to download data from CBOE:
5
+ - VIX Index historical data
6
+ - VVIX (VIX of VIX)
7
+ - VIX9D, VIX3M, VIX6M
8
+ - VIX Futures term structure
9
+
10
+ These are free, publicly available datasets that CBOE provides.
11
+ """
12
+
13
+ import io
14
+ import re
15
+ from datetime import datetime, timedelta
16
+ from pathlib import Path
17
+ from typing import List, Optional, Dict, Tuple
18
+ import pandas as pd
19
+ import requests
20
+ from bs4 import BeautifulSoup
21
+ import logging
22
+ from concurrent.futures import ThreadPoolExecutor, as_completed
23
+ from tqdm import tqdm
24
+
25
+ from src.data.base import BaseDataSource, DataFetchError, DataValidationError
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ # CBOE data URLs - Volatility Indices
31
+ CBOE_INDEX_URLS = {
32
+ 'VIX': 'https://cdn.cboe.com/api/global/us_indices/daily_prices/VIX_History.csv',
33
+ 'VVIX': 'https://cdn.cboe.com/api/global/us_indices/daily_prices/VVIX_History.csv',
34
+ 'VIX9D': 'https://cdn.cboe.com/api/global/us_indices/daily_prices/VIX9D_History.csv',
35
+ 'VIX3M': 'https://cdn.cboe.com/api/global/us_indices/daily_prices/VIX3M_History.csv',
36
+ 'VIX6M': 'https://cdn.cboe.com/api/global/us_indices/daily_prices/VIX6M_History.csv',
37
+ 'VIX1Y': 'https://cdn.cboe.com/api/global/us_indices/daily_prices/VIX1Y_History.csv',
38
+ 'SKEW': 'https://cdn.cboe.com/api/global/us_indices/daily_prices/SKEW_History.csv',
39
+ }
40
+
41
+ # Put/Call Ratio and Volume data URLs
42
+ CBOE_PUTCALL_URLS = {
43
+ 'TOTAL_PC': 'https://cdn.cboe.com/resources/options/volume_and_call_put_ratios/totalpc.csv',
44
+ 'INDEX_PC': 'https://cdn.cboe.com/resources/options/volume_and_call_put_ratios/indexpc.csv',
45
+ 'EQUITY_PC': 'https://cdn.cboe.com/resources/options/volume_and_call_put_ratios/equitypc.csv',
46
+ 'VIX_PC': 'https://cdn.cboe.com/resources/options/volume_and_call_put_ratios/vixpc.csv',
47
+ }
48
+
49
+ # VIX Futures base URL pattern
50
+ VIX_FUTURES_BASE_URL = 'https://cdn.cboe.com/data/us/futures/market_statistics/historical_data/VX/'
51
+
52
+
53
+ class CBOEDataSource(BaseDataSource):
54
+ """
55
+ Data source for CBOE public data.
56
+
57
+ Provides access to:
58
+ - Volatility indices (VIX, VVIX, VIX9D, etc.)
59
+ - VIX Futures historical data
60
+
61
+ Example:
62
+ source = CBOEDataSource()
63
+ vix = source.fetch_vix_index(
64
+ start_date=datetime(2006, 1, 1),
65
+ end_date=datetime.now()
66
+ )
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ cache_dir: Optional[Path] = None,
72
+ cache_enabled: bool = True,
73
+ cache_expiry_days: int = 1,
74
+ request_timeout: int = 30
75
+ ):
76
+ """
77
+ Initialize CBOE data source.
78
+
79
+ Args:
80
+ cache_dir: Directory for caching data.
81
+ cache_enabled: Whether to cache downloaded data.
82
+ cache_expiry_days: Days before cache expires.
83
+ request_timeout: HTTP request timeout in seconds.
84
+ """
85
+ super().__init__(
86
+ name="cboe",
87
+ cache_dir=cache_dir,
88
+ cache_enabled=cache_enabled,
89
+ cache_expiry_days=cache_expiry_days
90
+ )
91
+ self.timeout = request_timeout
92
+ self.session = requests.Session()
93
+ self.session.headers.update({
94
+ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) '
95
+ 'AppleWebKit/537.36 (KHTML, like Gecko) '
96
+ 'Chrome/120.0.0.0 Safari/537.36'
97
+ })
98
+
99
+ def get_available_series(self) -> List[str]:
100
+ """Get list of available CBOE series."""
101
+ return list(CBOE_INDEX_URLS.keys()) + list(CBOE_PUTCALL_URLS.keys()) + ['VX_FUTURES']
102
+
103
+ def fetch(
104
+ self,
105
+ start_date: datetime,
106
+ end_date: datetime,
107
+ series: Optional[List[str]] = None,
108
+ **kwargs
109
+ ) -> pd.DataFrame:
110
+ """
111
+ Fetch data from CBOE.
112
+
113
+ Args:
114
+ start_date: Start date for data retrieval.
115
+ end_date: End date for data retrieval.
116
+ series: List of series to fetch. Defaults to ['VIX'].
117
+
118
+ Returns:
119
+ DataFrame with series as columns and date index.
120
+ """
121
+ if series is None:
122
+ series = ['VIX']
123
+
124
+ data_frames = []
125
+
126
+ for series_id in series:
127
+ if series_id == 'VX_FUTURES':
128
+ # Handle futures separately
129
+ df = self._fetch_vix_futures(start_date, end_date)
130
+ elif series_id in CBOE_INDEX_URLS:
131
+ df = self._fetch_index(series_id, start_date, end_date)
132
+ elif series_id in CBOE_PUTCALL_URLS:
133
+ df = self._fetch_putcall(series_id, start_date, end_date)
134
+ else:
135
+ logger.warning(f"Unknown CBOE series: {series_id}")
136
+ continue
137
+
138
+ if df is not None and not df.empty:
139
+ data_frames.append(df)
140
+
141
+ if not data_frames:
142
+ raise DataFetchError("No data retrieved from CBOE")
143
+
144
+ # Combine all series
145
+ combined = pd.concat(data_frames, axis=1)
146
+ combined = combined.sort_index()
147
+
148
+ # Filter to requested date range
149
+ combined = combined.loc[start_date:end_date]
150
+
151
+ return combined
152
+
153
+ def _fetch_index(
154
+ self,
155
+ series_id: str,
156
+ start_date: datetime,
157
+ end_date: datetime
158
+ ) -> pd.DataFrame:
159
+ """
160
+ Fetch a single volatility index from CBOE.
161
+
162
+ Args:
163
+ series_id: Index identifier (VIX, VVIX, etc.)
164
+ start_date: Start date.
165
+ end_date: End date.
166
+
167
+ Returns:
168
+ DataFrame with the index data.
169
+ """
170
+ url = CBOE_INDEX_URLS.get(series_id)
171
+ if not url:
172
+ raise DataFetchError(f"Unknown series: {series_id}")
173
+
174
+ logger.info(f"Fetching {series_id} from CBOE")
175
+
176
+ try:
177
+ response = self.session.get(url, timeout=self.timeout)
178
+ response.raise_for_status()
179
+
180
+ # Parse CSV
181
+ df = pd.read_csv(
182
+ io.StringIO(response.text),
183
+ parse_dates=['DATE'],
184
+ index_col='DATE'
185
+ )
186
+
187
+ # Standardize column names
188
+ df.columns = [f"{series_id}_{col}" for col in df.columns]
189
+
190
+ # Filter to date range
191
+ df = df.loc[start_date:end_date]
192
+
193
+ logger.info(
194
+ f"Fetched {series_id}: {len(df)} observations "
195
+ f"({df.index.min()} to {df.index.max()})"
196
+ )
197
+
198
+ return df
199
+
200
+ except requests.RequestException as e:
201
+ logger.error(f"Failed to fetch {series_id}: {e}")
202
+ raise DataFetchError(f"Failed to fetch {series_id}: {e}")
203
+
204
+ def _fetch_putcall(
205
+ self,
206
+ series_id: str,
207
+ start_date: datetime,
208
+ end_date: datetime
209
+ ) -> pd.DataFrame:
210
+ """
211
+ Fetch put/call ratio and volume data from CBOE.
212
+
213
+ Args:
214
+ series_id: Put/call series identifier (TOTAL_PC, INDEX_PC, etc.)
215
+ start_date: Start date.
216
+ end_date: End date.
217
+
218
+ Returns:
219
+ DataFrame with put/call data.
220
+ """
221
+ url = CBOE_PUTCALL_URLS.get(series_id)
222
+ if not url:
223
+ raise DataFetchError(f"Unknown put/call series: {series_id}")
224
+
225
+ logger.info(f"Fetching {series_id} put/call data from CBOE")
226
+
227
+ try:
228
+ response = self.session.get(url, timeout=self.timeout)
229
+ response.raise_for_status()
230
+
231
+ # Skip header rows (varies by file)
232
+ lines = response.text.strip().split('\n')
233
+
234
+ # Find the header row (contains DATE or Date)
235
+ header_idx = 0
236
+ for i, line in enumerate(lines):
237
+ if 'DATE' in line.upper() and 'RATIO' in line.upper():
238
+ header_idx = i
239
+ break
240
+
241
+ # Parse CSV from header row
242
+ csv_text = '\n'.join(lines[header_idx:])
243
+ df = pd.read_csv(io.StringIO(csv_text))
244
+
245
+ # Standardize date column
246
+ date_col = [c for c in df.columns if 'date' in c.lower()][0]
247
+ df[date_col] = pd.to_datetime(df[date_col], format='mixed')
248
+ df = df.set_index(date_col)
249
+ df.index.name = 'DATE'
250
+
251
+ # Standardize column names based on series
252
+ prefix = series_id.replace('_PC', '')
253
+ new_cols = {}
254
+ for col in df.columns:
255
+ col_lower = col.lower()
256
+ if 'ratio' in col_lower or 'p/c' in col_lower:
257
+ new_cols[col] = f"{prefix}_PC_RATIO"
258
+ elif 'put' in col_lower and 'call' not in col_lower:
259
+ new_cols[col] = f"{prefix}_PUT_VOL"
260
+ elif 'call' in col_lower and 'put' not in col_lower:
261
+ new_cols[col] = f"{prefix}_CALL_VOL"
262
+ elif 'total' in col_lower:
263
+ new_cols[col] = f"{prefix}_TOTAL_VOL"
264
+
265
+ df = df.rename(columns=new_cols)
266
+
267
+ # Keep only renamed columns
268
+ df = df[[c for c in df.columns if prefix in c]]
269
+
270
+ # Filter to date range
271
+ df = df.loc[start_date:end_date]
272
+
273
+ logger.info(
274
+ f"Fetched {series_id}: {len(df)} observations "
275
+ f"({df.index.min()} to {df.index.max()})"
276
+ )
277
+
278
+ return df
279
+
280
+ except Exception as e:
281
+ logger.error(f"Failed to fetch {series_id}: {e}")
282
+ raise DataFetchError(f"Failed to fetch {series_id}: {e}")
283
+
284
+ def _fetch_vix_futures(
285
+ self,
286
+ start_date: datetime,
287
+ end_date: datetime,
288
+ max_workers: int = 5
289
+ ) -> pd.DataFrame:
290
+ """
291
+ Fetch VIX futures historical data.
292
+
293
+ Downloads individual contract files and constructs term structure.
294
+
295
+ Args:
296
+ start_date: Start date.
297
+ end_date: End date.
298
+ max_workers: Number of parallel download threads.
299
+
300
+ Returns:
301
+ DataFrame with futures term structure data.
302
+ """
303
+ logger.info("Fetching VIX futures term structure from CBOE")
304
+
305
+ # Get list of available contracts
306
+ contract_urls = self._get_futures_contract_urls(start_date, end_date)
307
+
308
+ if not contract_urls:
309
+ logger.warning("No VIX futures contracts found")
310
+ return pd.DataFrame()
311
+
312
+ logger.info(f"Found {len(contract_urls)} VIX futures contracts to download")
313
+
314
+ # Download contracts in parallel
315
+ all_data = []
316
+
317
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
318
+ futures = {
319
+ executor.submit(self._download_futures_contract, url): url
320
+ for url in contract_urls
321
+ }
322
+
323
+ for future in tqdm(as_completed(futures), total=len(futures), desc="Downloading VIX futures"):
324
+ url = futures[future]
325
+ try:
326
+ df = future.result()
327
+ if df is not None and not df.empty:
328
+ all_data.append(df)
329
+ except Exception as e:
330
+ logger.warning(f"Failed to download {url}: {e}")
331
+
332
+ if not all_data:
333
+ logger.warning("No VIX futures data downloaded")
334
+ return pd.DataFrame()
335
+
336
+ # Combine all contract data
337
+ combined = pd.concat(all_data, ignore_index=True)
338
+
339
+ # Process into term structure format
340
+ term_structure = self._process_futures_to_term_structure(combined, start_date, end_date)
341
+
342
+ return term_structure
343
+
344
+ def _get_futures_contract_urls(
345
+ self,
346
+ start_date: datetime,
347
+ end_date: datetime
348
+ ) -> List[str]:
349
+ """
350
+ Generate URLs for VIX futures contracts within date range.
351
+
352
+ CBOE uses expiration date in filename: VX_YYYY-MM-DD.csv
353
+ We need to fetch contracts that were active during our period.
354
+ """
355
+ urls = []
356
+
357
+ # Generate monthly expiration dates (3rd Wednesday of each month, approximately)
358
+ current = start_date.replace(day=1)
359
+
360
+ while current <= end_date + timedelta(days=365): # Include contracts expiring up to 1 year after end
361
+ # Find 3rd Wednesday
362
+ first_day = current.replace(day=1)
363
+ # Days until first Wednesday
364
+ days_to_wed = (2 - first_day.weekday()) % 7
365
+ first_wed = first_day + timedelta(days=days_to_wed)
366
+ # Third Wednesday
367
+ third_wed = first_wed + timedelta(days=14)
368
+
369
+ # Build URL
370
+ expiry_str = third_wed.strftime('%Y-%m-%d')
371
+ url = f"{VIX_FUTURES_BASE_URL}VX_{expiry_str}.csv"
372
+ urls.append(url)
373
+
374
+ # Move to next month
375
+ if current.month == 12:
376
+ current = current.replace(year=current.year + 1, month=1)
377
+ else:
378
+ current = current.replace(month=current.month + 1)
379
+
380
+ return urls
381
+
382
+ def _download_futures_contract(self, url: str) -> Optional[pd.DataFrame]:
383
+ """
384
+ Download a single VIX futures contract file.
385
+
386
+ Args:
387
+ url: URL to the contract CSV.
388
+
389
+ Returns:
390
+ DataFrame with contract data, or None if failed.
391
+ """
392
+ try:
393
+ response = self.session.get(url, timeout=self.timeout)
394
+
395
+ if response.status_code == 404:
396
+ return None # Contract doesn't exist
397
+
398
+ response.raise_for_status()
399
+
400
+ df = pd.read_csv(io.StringIO(response.text))
401
+
402
+ # Extract expiry date from URL
403
+ expiry_match = re.search(r'VX_(\d{4}-\d{2}-\d{2})\.csv', url)
404
+ if expiry_match:
405
+ df['Expiry'] = pd.to_datetime(expiry_match.group(1))
406
+
407
+ return df
408
+
409
+ except requests.RequestException:
410
+ return None
411
+
412
+ def _process_futures_to_term_structure(
413
+ self,
414
+ df: pd.DataFrame,
415
+ start_date: datetime,
416
+ end_date: datetime
417
+ ) -> pd.DataFrame:
418
+ """
419
+ Process raw futures data into term structure format.
420
+
421
+ Creates columns for front month (VX1), second month (VX2), etc.
422
+
423
+ Args:
424
+ df: Raw futures data with all contracts.
425
+ start_date: Start date.
426
+ end_date: End date.
427
+
428
+ Returns:
429
+ DataFrame with term structure columns.
430
+ """
431
+ if df.empty:
432
+ return pd.DataFrame()
433
+
434
+ # Standardize column names
435
+ df.columns = [col.strip().upper() for col in df.columns]
436
+
437
+ # Find date column
438
+ date_col = None
439
+ for col in ['TRADE DATE', 'DATE', 'TRADE_DATE']:
440
+ if col in df.columns:
441
+ date_col = col
442
+ break
443
+
444
+ if date_col is None:
445
+ logger.warning("Could not find date column in futures data")
446
+ return pd.DataFrame()
447
+
448
+ df['Date'] = pd.to_datetime(df[date_col])
449
+
450
+ # Find settle column
451
+ settle_col = None
452
+ for col in ['SETTLE', 'SETTLEMENT', 'CLOSE']:
453
+ if col in df.columns:
454
+ settle_col = col
455
+ break
456
+
457
+ if settle_col is None:
458
+ logger.warning("Could not find settle column in futures data")
459
+ return pd.DataFrame()
460
+
461
+ # Convert EXPIRY to datetime if not already
462
+ if 'EXPIRY' in df.columns:
463
+ df['Expiry'] = pd.to_datetime(df['EXPIRY'])
464
+
465
+ # For each date, rank contracts by expiry and create VX1, VX2, etc.
466
+ term_structure_data = []
467
+
468
+ for date, group in df.groupby('Date'):
469
+ if date < start_date or date > end_date:
470
+ continue
471
+
472
+ # Sort by expiry and filter to only future expiries
473
+ group = group[group['Expiry'] > date].sort_values('Expiry')
474
+
475
+ row = {'Date': date}
476
+ for i, (_, contract) in enumerate(group.iterrows()):
477
+ if i >= 9: # VX1 to VX9
478
+ break
479
+ row[f'VX{i+1}'] = contract[settle_col]
480
+ row[f'VX{i+1}_Expiry'] = contract['Expiry']
481
+
482
+ term_structure_data.append(row)
483
+
484
+ result = pd.DataFrame(term_structure_data)
485
+
486
+ if not result.empty:
487
+ result = result.set_index('Date').sort_index()
488
+
489
+ # Add derived term structure metrics
490
+ if 'VX1' in result.columns and 'VX2' in result.columns:
491
+ result['VX_Slope_1_2'] = result['VX2'] - result['VX1']
492
+ if 'VX1' in result.columns and 'VX4' in result.columns:
493
+ result['VX_Slope_1_4'] = result['VX4'] - result['VX1']
494
+
495
+ logger.info(f"Processed VIX futures term structure: {len(result)} trading days")
496
+
497
+ return result
498
+
499
+ def validate(self, df: pd.DataFrame) -> bool:
500
+ """
501
+ Validate CBOE data.
502
+
503
+ Args:
504
+ df: DataFrame to validate.
505
+
506
+ Returns:
507
+ True if valid.
508
+
509
+ Raises:
510
+ DataValidationError: If validation fails.
511
+ """
512
+ if df.empty:
513
+ raise DataValidationError("CBOE DataFrame is empty")
514
+
515
+ if not isinstance(df.index, pd.DatetimeIndex):
516
+ raise DataValidationError("CBOE DataFrame index is not DatetimeIndex")
517
+
518
+ # Check for reasonable values in VIX columns
519
+ vix_cols = [col for col in df.columns if 'VIX' in col.upper() or col.startswith('VX')]
520
+ for col in vix_cols:
521
+ # Skip expiry columns and slope columns (slopes can be negative in backwardation)
522
+ if col.endswith('_Expiry') or 'Slope' in col:
523
+ continue
524
+
525
+ values = df[col].dropna()
526
+ if len(values) > 0:
527
+ if values.min() < 0:
528
+ raise DataValidationError(f"Negative values in {col}")
529
+ if values.max() > 200: # VIX rarely exceeds 100
530
+ logger.warning(f"Very high values in {col}: max={values.max()}")
531
+
532
+ logger.info(f"CBOE data validation passed: {len(df)} rows")
533
+ return True
534
+
535
+ def fetch_vix_index(
536
+ self,
537
+ start_date: datetime,
538
+ end_date: datetime
539
+ ) -> pd.DataFrame:
540
+ """
541
+ Convenience method to fetch VIX index data.
542
+
543
+ Args:
544
+ start_date: Start date.
545
+ end_date: End date.
546
+
547
+ Returns:
548
+ DataFrame with VIX data.
549
+ """
550
+ return self.fetch_with_cache(
551
+ start_date=start_date,
552
+ end_date=end_date,
553
+ series=['VIX']
554
+ )
555
+
556
+ def fetch_all_vix_indices(
557
+ self,
558
+ start_date: datetime,
559
+ end_date: datetime
560
+ ) -> pd.DataFrame:
561
+ """
562
+ Fetch all available VIX-related indices.
563
+
564
+ Args:
565
+ start_date: Start date.
566
+ end_date: End date.
567
+
568
+ Returns:
569
+ DataFrame with VIX, VVIX, VIX9D, VIX3M, VIX6M.
570
+ """
571
+ return self.fetch_with_cache(
572
+ start_date=start_date,
573
+ end_date=end_date,
574
+ series=['VIX', 'VVIX', 'VIX9D', 'VIX3M', 'VIX6M']
575
+ )
576
+
577
+ def fetch_vix_futures(
578
+ self,
579
+ start_date: datetime,
580
+ end_date: datetime
581
+ ) -> pd.DataFrame:
582
+ """
583
+ Fetch VIX futures term structure.
584
+
585
+ Args:
586
+ start_date: Start date.
587
+ end_date: End date.
588
+
589
+ Returns:
590
+ DataFrame with VX1-VX9 and term structure metrics.
591
+ """
592
+ return self.fetch_with_cache(
593
+ start_date=start_date,
594
+ end_date=end_date,
595
+ series=['VX_FUTURES']
596
+ )
597
+
598
+
599
+ if __name__ == "__main__":
600
+ # Test the CBOE data source
601
+ logging.basicConfig(level=logging.INFO)
602
+
603
+ source = CBOEDataSource()
604
+
605
+ # Fetch VIX index
606
+ vix = source.fetch_vix_index(
607
+ start_date=datetime(2020, 1, 1),
608
+ end_date=datetime.now()
609
+ )
610
+
611
+ print(f"\nVIX Index Data:")
612
+ print(f"Shape: {vix.shape}")
613
+ print(f"Columns: {vix.columns.tolist()}")
614
+ print(f"Date Range: {vix.index.min()} to {vix.index.max()}")