Sunday, February 15, 2026

Building a VIX-Filtered Mean Reversion Strategy: Part 1 - Framework Setup and Data Infrastructure

 Introduction to Systematic Strategy Development

The development of robust quantitative trading strategies requires a systematic approach to backtesting that balances statistical rigor with practical implementation constraints. This three-part series demonstrates the construction of a VIX-filtered mean reversion strategy for equity index futures using the backtesting.py framework, a lightweight Python library that provides vectorized backtesting capabilities while maintaining compatibility with event-driven logic. The strategy presented here exploits short-term mean reversion characteristics in E-mini S&P 500 (ES) and E-mini NASDAQ-100 (NQ) futures contracts, filtered through volatility regime detection using the CBOE Volatility Index (VIX) as a market state classifier.

Mean reversion strategies operate on the fundamental assumption that asset prices exhibit temporary deviations from their equilibrium value, creating profitable opportunities when prices reach statistical extremes. By incorporating VIX-based regime filtering, we distinguish between market environments where mean reversion mechanics remain intact versus periods of sustained directional momentum or structural breaks. The framework implements Volume Weighted Average Price (VWAP) as the dynamic equilibrium anchor, with standard deviation bands serving as entry and exit thresholds. This first installment focuses on establishing the data infrastructure and backtesting environment necessary for systematic strategy evaluation.

Environment Configuration and Dependency Management

The backtesting infrastructure relies on several specialized Python libraries that handle data acquisition, numerical computation, and strategy evaluation. The core dependencies include yfinance for market data retrieval, pandas and numpy for data manipulation, and the backtesting.py framework itself, which provides the strategy evaluation engine. Additionally, we incorporate scikit-optimize and sambo for parameter optimization, along with bokeh for interactive visualization of results. The installation process can be automated through pip, ensuring consistent dependency versions across development environments:

python
!pip install -q backtesting yfinance pandas numpy
!pip install -q scikit-optimize
!pip install -q sambo
!pip install -q bokeh>=2.4.0
print("✓ All packages installed successfully!")

Once dependencies are installed, the environment requires proper configuration of logging and warning suppression to maintain clean output during backtesting operations. The logging module should be configured to capture informational messages that track data retrieval progress and validation checkpoints, while filtering out deprecation warnings from underlying libraries that may clutter the output stream. This configuration ensures that critical operational messages remain visible while suppressing noise from external dependencies:

python
import warnings
warnings.filterwarnings('ignore')

import yfinance as yf
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from backtesting import Backtest, Strategy
from backtesting.lib import crossover
import logging

logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
logger = logging.getLogger(__name__)

Market Data Acquisition and Temporal Alignment

The foundation of any empirical backtesting framework rests upon the quality and consistency of historical market data. For futures contracts, we utilize Yahoo Finance as the primary data source, recognizing its limitations in terms of continuous contract construction and the absence of professional-grade adjustments for roll dates. The data retrieval function implements a sliding window approach that fetches the maximum available history (729 days for Yahoo Finance's hourly data endpoint) while handling timezone localization to ensure temporal consistency across multiple data streams:

python
def fetch_futures_data(ticker='ES', max_days=729):
"""
Fetch futures data from Yahoo Finance (1h bars)
Auto-adjusts to last 729 days (Yahoo limit)
"""
FUTURES_TICKERS = {
'ES': 'ES=F',
'NQ': 'NQ=F',
'MES': 'MES=F',
'MNQ': 'MNQ=F'
}

yahoo_ticker = FUTURES_TICKERS.get(ticker, 'ES=F')
end_date = datetime.now()
start_date = end_date - timedelta(days=max_days)

logger.info(f"Fetching {ticker} data from {start_date.date()} to {end_date.date()}")

try:
ticker_obj = yf.Ticker(yahoo_ticker)
data = ticker_obj.history(
start=start_date.strftime('%Y-%m-%d'),
end=end_date.strftime('%Y-%m-%d'),
interval='1h',
auto_adjust=False
)

if data.empty:
logger.error(f"No data retrieved for {ticker}")
return pd.DataFrame()

data.columns = [col.title() for col in data.columns]
data = data[['Open', 'High', 'Low', 'Close', 'Volume']]
data = data.dropna()

if data.index.tz is None:
data.index = data.index.tz_localize('America/New_York', ambiguous='infer', nonexistent='shift_forward')
else:
data.index = data.index.tz_convert('America/New_York')

logger.info(f"✓ Fetched {len(data)} bars for {ticker}")
return data

except Exception as e:
logger.error(f"Error fetching {ticker}: {str(e)}")
return pd.DataFrame()

The function incorporates several critical data validation steps that prevent downstream errors in the backtesting pipeline. Column standardization ensures consistent naming conventions regardless of API changes, while timezone handling addresses the common issue of ambiguous timestamps during daylight saving transitions. The use of America/New_York timezone alignment is deliberate, as US equity index futures reference New York market hours as their primary trading session, despite maintaining nearly 24-hour liquidity through global exchanges.

Volatility Index Integration and Data Synchronization

The VIX serves as the primary regime classifier in this framework, determining which VWAP bands qualify as valid entry signals based on current volatility conditions. Unlike futures price data which requires hourly granularity for intraday mean reversion detection, VIX data is retrieved at daily frequency since volatility regimes persist across multiple sessions and do not require intrabar precision. The VIX retrieval function mirrors the structure of the futures data fetcher but operates on daily intervals and returns only the closing VIX level:

python
def fetch_vix_data(max_days=729):
"""Fetch VIX data for regime filtering"""
end_date = datetime.now()
start_date = end_date - timedelta(days=max_days)

logger.info(f"Fetching VIX data")

try:
vix = yf.Ticker('^VIX')
data = vix.history(
start=start_date.strftime('%Y-%m-%d'),
end=end_date.strftime('%Y-%m-%d'),
interval='1d',
auto_adjust=False
)

if data.empty:
logger.error("No VIX data retrieved")
return pd.DataFrame()

data = data[['Close']].rename(columns={'Close': 'VIX'})
logger.info(f"✓ Fetched {len(data)} VIX data points")
return data

except Exception as e:
logger.error(f"Error fetching VIX: {str(e)}")
return pd.DataFrame()

The temporal alignment of daily VIX data with hourly futures bars presents a common challenge in multi-frequency data integration. The merge operation must propagate daily VIX values forward to fill all hourly bars within each trading session, effectively treating the VIX regime as constant throughout the day until the next daily close updates the regime classification. This forward-fill methodology ensures that strategy logic never references future VIX values, maintaining the integrity of the backtest by preventing look-ahead bias:

python
def merge_data(futures_data, vix_data):
"""Merge VIX with futures data"""
if futures_data.empty or vix_data.empty:
logger.error("Cannot merge: Empty data")
return pd.DataFrame()

vix_reindexed = vix_data.reindex(futures_data.index, method='ffill')

merged = futures_data.copy()
merged['VIX'] = vix_reindexed['VIX']
merged = merged.dropna(subset=['VIX'])

logger.info(f"✓ Merged {len(merged)} bars")
return merged

The reindexing operation creates a new DataFrame with the hourly frequency of the futures data while preserving the daily VIX values through forward-filling. Any bars that lack corresponding VIX data are removed through the dropna operation, ensuring that the strategy never executes on incomplete information. This conservative approach to data handling reduces the tradeable dataset slightly but eliminates edge cases where missing VIX data could trigger unintended strategy behavior.

Data Pipeline Execution and Validation

With the data acquisition functions defined, the execution pipeline orchestrates the retrieval and merging of both market data streams. The pipeline begins by fetching ES futures data, followed by VIX data retrieval, and concludes with the temporal alignment merge operation. Each stage includes error handling and validation to ensure that subsequent operations receive properly formatted data:

python
print("="*60)
print("DOWNLOADING DATA")
print("="*60)

es_data = fetch_futures_data('ES', max_days=729)

if es_data.empty:
raise ValueError("Failed to fetch ES data")

vix_data = fetch_vix_data(max_days=729)

if vix_data.empty:
raise ValueError("Failed to fetch VIX data")

data = merge_data(es_data, vix_data)

if data.empty:
raise ValueError("Failed to merge data")

print("\n" + "="*60)
print("DATA READY")
print("="*60)
print(f"Total bars: {len(data):,}")
print(f"Date range: {data.index[0]} to {data.index[-1]}")
print(f"Columns: {list(data.columns)}")
print("\nFirst 3 rows:")
print(data.head(3))

The validation output provides critical information about the dataset's temporal coverage and completeness. The bar count indicates the effective trading history available for backtesting, while the date range confirms that the data spans the intended lookback period. Displaying the first few rows allows for quick visual inspection of data formatting and the presence of all required columns including the merged VIX field. This defensive programming approach catches data quality issues early in the pipeline before they propagate into the strategy evaluation phase, where diagnosing the root cause of errors becomes significantly more complex.

The resulting DataFrame now contains synchronized hourly futures bars with forward-filled daily VIX values, creating a unified dataset ready for indicator calculation and strategy logic implementation. This completes the foundational data infrastructure required for systematic backtesting. Part 2 of this series will develop the technical indicator library, including VWAP calculation, standard deviation band construction, and ATR-based risk metrics that drive the strategy's entry and exit logic.

No comments:

Post a Comment

Popular Posts: