Guest User

Untitled

a guest
Feb 15th, 2026
35
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 39.64 KB | Cryptocurrency | 0 0
  1. # -*- coding: utf-8 -*-
  2. """Version 2 VIX-Filtered Mean Reversion Futures Strategy.ipynb
  3.  
  4. Automatically generated by Colab.
  5.  
  6. Original file is located at
  7. https://colab.research.google.com/drive/1hVHcFDDox4gpxfu6OR6V8N1ZcMCA0Io3
  8. """
  9.  
  10. # @title
  11. # Install required packages
  12. !pip install -q backtesting yfinance pandas numpy
  13. !pip install -q scikit-optimize
  14. !pip install -q sambo
  15. !pip install -q bokeh>=2.4.0
  16. !pip install scikit-optimize
  17. print("✓ All packages installed successfully!")
  18.  
  19. # @title
  20. import warnings
  21. warnings.filterwarnings('ignore')
  22.  
  23. import yfinance as yf
  24. import pandas as pd
  25. import numpy as np
  26. from datetime import datetime, timedelta
  27. from backtesting import Backtest, Strategy
  28. from backtesting.lib import crossover
  29. import logging
  30.  
  31. logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')
  32. logger = logging.getLogger(__name__)
  33.  
  34. print("✓ All libraries imported successfully!")
  35.  
  36. # @title
  37. def fetch_futures_data(ticker='ES', max_days=729):
  38. """
  39. Fetch futures data from Yahoo Finance (1h bars)
  40. Auto-adjusts to last 729 days (Yahoo limit)
  41. """
  42. FUTURES_TICKERS = {
  43. 'ES': 'ES=F',
  44. 'NQ': 'NQ=F',
  45. 'MES': 'MES=F',
  46. 'MNQ': 'MNQ=F'
  47. }
  48.  
  49. yahoo_ticker = FUTURES_TICKERS.get(ticker, 'ES=F')
  50.  
  51. # Calculate date range
  52. end_date = datetime.now()
  53. start_date = end_date - timedelta(days=max_days)
  54.  
  55. logger.info(f"Fetching {ticker} data from {start_date.date()} to {end_date.date()}")
  56.  
  57. try:
  58. ticker_obj = yf.Ticker(yahoo_ticker)
  59. data = ticker_obj.history(
  60. start=start_date.strftime('%Y-%m-%d'),
  61. end=end_date.strftime('%Y-%m-%d'),
  62. interval='1h',
  63. auto_adjust=False
  64. )
  65.  
  66. if data.empty:
  67. logger.error(f"No data retrieved for {ticker}")
  68. return pd.DataFrame()
  69.  
  70. # Prepare columns
  71. data.columns = [col.title() for col in data.columns]
  72. data = data[['Open', 'High', 'Low', 'Close', 'Volume']]
  73. data = data.dropna()
  74.  
  75. # Timezone handling
  76. if data.index.tz is None:
  77. data.index = data.index.tz_localize('America/New_York', ambiguous='infer', nonexistent='shift_forward')
  78. else:
  79. data.index = data.index.tz_convert('America/New_York')
  80.  
  81. logger.info(f"✓ Fetched {len(data)} bars for {ticker}")
  82. return data
  83.  
  84. except Exception as e:
  85. logger.error(f"Error fetching {ticker}: {str(e)}")
  86. return pd.DataFrame()
  87.  
  88.  
  89. def fetch_vix_data(max_days=729):
  90. """Fetch VIX data for regime filtering"""
  91.  
  92. end_date = datetime.now()
  93. start_date = end_date - timedelta(days=max_days)
  94.  
  95. logger.info(f"Fetching VIX data")
  96.  
  97. try:
  98. vix = yf.Ticker('^VIX')
  99. data = vix.history(
  100. start=start_date.strftime('%Y-%m-%d'),
  101. end=end_date.strftime('%Y-%m-%d'),
  102. interval='1d',
  103. auto_adjust=False
  104. )
  105.  
  106. if data.empty:
  107. logger.error("No VIX data retrieved")
  108. return pd.DataFrame()
  109.  
  110. data = data[['Close']].rename(columns={'Close': 'VIX'})
  111. logger.info(f"✓ Fetched {len(data)} VIX data points")
  112. return data
  113.  
  114. except Exception as e:
  115. logger.error(f"Error fetching VIX: {str(e)}")
  116. return pd.DataFrame()
  117.  
  118.  
  119. def merge_data(futures_data, vix_data):
  120. """Merge VIX with futures data"""
  121.  
  122. if futures_data.empty or vix_data.empty:
  123. logger.error("Cannot merge: Empty data")
  124. return pd.DataFrame()
  125.  
  126. # Forward fill VIX to match hourly futures data
  127. vix_reindexed = vix_data.reindex(futures_data.index, method='ffill')
  128.  
  129. merged = futures_data.copy()
  130. merged['VIX'] = vix_reindexed['VIX']
  131. merged = merged.dropna(subset=['VIX'])
  132.  
  133. logger.info(f"✓ Merged {len(merged)} bars")
  134. return merged
  135.  
  136. print("✓ Data fetcher functions defined!")
  137.  
  138. # @title
  139. # Indicators
  140.  
  141. import pandas as pd
  142. import numpy as np
  143.  
  144.  
  145. def calculate_vwap(close, volume, high, low, index):
  146. """
  147. Calculate VWAP (Volume Weighted Average Price) - resets daily
  148.  
  149. Formula: VWAP = Σ(Typical Price × Volume) / Σ(Volume)
  150. Where Typical Price = (High + Low + Close) / 3
  151.  
  152. The calculation resets at the start of each trading day.
  153. """
  154. typical_price = (high + low + close) / 3
  155.  
  156. df = pd.DataFrame({
  157. 'tp': typical_price,
  158. 'vol': volume,
  159. 'date': index.date
  160. })
  161.  
  162. # Calculate cumulative sums grouped by date
  163. df['cum_tp_vol'] = df.groupby('date')['tp'].transform(
  164. lambda x: (x * df.loc[x.index, 'vol']).cumsum()
  165. )
  166. df['cum_vol'] = df.groupby('date')['vol'].transform('cumsum')
  167.  
  168. # VWAP = Cumulative(TP × Vol) / Cumulative(Vol)
  169. vwap = df['cum_tp_vol'] / df['cum_vol']
  170.  
  171. return vwap.values
  172.  
  173.  
  174. def calculate_vwap_bands(close, volume, high, low, index, std_mult=1.0):
  175. """
  176. Calculate VWAP Standard Deviation Bands using UNWEIGHTED standard deviation
  177.  
  178. Formula:
  179. 1. Calculate VWAP first
  180. 2. For each bar: deviation = (Typical Price - VWAP)
  181. 3. Variance = Σ(deviation²) / N (where N = number of bars)
  182. 4. Std Dev = sqrt(Variance)
  183. 5. Upper Band = VWAP + (std_mult × Std Dev)
  184. 6. Lower Band = VWAP - (std_mult × Std Dev)
  185.  
  186. This matches ThinkorSwim and standard statistical definition.
  187. Each bar contributes equally to the standard deviation regardless of volume.
  188. """
  189. # First calculate VWAP
  190. vwap = calculate_vwap(close, volume, high, low, index)
  191. typical_price = (high + low + close) / 3
  192.  
  193. df = pd.DataFrame({
  194. 'tp': typical_price,
  195. 'vwap': vwap,
  196. 'date': index.date
  197. })
  198.  
  199. # Calculate squared deviations from VWAP
  200. df['sq_diff'] = (df['tp'] - df['vwap']) ** 2
  201.  
  202. # Calculate unweighted variance (sum of squared differences / count)
  203. df['cum_sq_diff'] = df.groupby('date')['sq_diff'].transform('cumsum')
  204. df['count'] = df.groupby('date').cumcount() + 1 # Running count of bars
  205.  
  206. # Variance = Cumulative Sum of Squared Differences / Count of Bars
  207. variance = df['cum_sq_diff'] / df['count']
  208. std_dev = np.sqrt(variance)
  209.  
  210. # Calculate bands
  211. upper = vwap + (std_mult * std_dev)
  212. lower = vwap - (std_mult * std_dev)
  213.  
  214. return upper, lower
  215.  
  216.  
  217. def calculate_vwap_slope(close, volume, high, low, index, lookback=6):
  218. """
  219. Calculate VWAP slope as percentage change
  220.  
  221. Formula: Slope = ((VWAP_current - VWAP_lookback) / VWAP_lookback) × 100
  222.  
  223. Args:
  224. lookback: Number of periods to look back for slope calculation
  225.  
  226. Returns:
  227. Slope as percentage
  228. """
  229. vwap = calculate_vwap(close, volume, high, low, index)
  230. slope = pd.Series(vwap).pct_change(periods=lookback) * 100
  231.  
  232. return slope.values
  233.  
  234.  
  235. def calculate_atr(high, low, close, period=14):
  236. """
  237. Calculate ATR (Average True Range)
  238.  
  239. True Range is the maximum of:
  240. 1. Current High - Current Low
  241. 2. |Current High - Previous Close|
  242. 3. |Current Low - Previous Close|
  243.  
  244. ATR is the Exponential Moving Average of True Range over the period.
  245. """
  246. h = pd.Series(high)
  247. l = pd.Series(low)
  248. c = pd.Series(close)
  249.  
  250. # Calculate the three components of True Range
  251. tr1 = h - l
  252. tr2 = abs(h - c.shift())
  253. tr3 = abs(l - c.shift())
  254.  
  255. # True Range is the maximum of the three
  256. tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
  257.  
  258. # Apply exponential moving average
  259. atr = tr.ewm(span=period, adjust=False).mean()
  260.  
  261. return atr.values
  262.  
  263.  
  264. def calculate_volume_ma(volume, period=9):
  265. """
  266. Calculate Volume Moving Average
  267.  
  268. Simple moving average of volume over the specified period.
  269. """
  270. return pd.Series(volume).rolling(window=period).mean().values
  271.  
  272.  
  273. def calculate_volume_profile(high, low, close, volume, index, num_bins=24):
  274. """
  275. Calculate Volume Profile metrics: VAH (Value Area High), VAL (Value Area Low), POC (Point of Control)
  276.  
  277. Process:
  278. 1. Divide the day's price range into bins (default 24)
  279. 2. Distribute volume across bins based on where price traded
  280. 3. POC = price level with highest volume
  281. 4. Value Area = 70% of total volume centered around POC
  282. 5. VAH = upper boundary of value area
  283. 6. VAL = lower boundary of value area
  284.  
  285. The calculation resets daily.
  286. """
  287. df = pd.DataFrame({
  288. 'high': high,
  289. 'low': low,
  290. 'close': close,
  291. 'volume': volume,
  292. 'date': index.date
  293. })
  294.  
  295. vah_list, val_list, poc_list = [], [], []
  296.  
  297. for date in df['date'].unique():
  298. day_data = df[df['date'] == date]
  299.  
  300. # Handle insufficient data
  301. if len(day_data) < 3:
  302. vah_list.extend([np.nan] * len(day_data))
  303. val_list.extend([np.nan] * len(day_data))
  304. poc_list.extend([np.nan] * len(day_data))
  305. continue
  306.  
  307. price_min = day_data['low'].min()
  308. price_max = day_data['high'].max()
  309.  
  310. # Handle no price movement
  311. if price_max == price_min:
  312. vah_list.extend([day_data['close'].iloc[0]] * len(day_data))
  313. val_list.extend([day_data['close'].iloc[0]] * len(day_data))
  314. poc_list.extend([day_data['close'].iloc[0]] * len(day_data))
  315. continue
  316.  
  317. # Create price bins
  318. bins = np.linspace(price_min, price_max, num_bins)
  319. vol_profile = np.zeros(len(bins) - 1)
  320.  
  321. # Distribute volume across bins based on bar overlap
  322. for _, row in day_data.iterrows():
  323. for i in range(len(bins) - 1):
  324. if row['low'] <= bins[i+1] and row['high'] >= bins[i]:
  325. # Calculate overlap between bar range and bin range
  326. overlap = min(row['high'], bins[i+1]) - max(row['low'], bins[i])
  327. bar_range = row['high'] - row['low']
  328.  
  329. if bar_range > 0:
  330. # Distribute volume proportionally to overlap
  331. vol_profile[i] += row['volume'] * (overlap / bar_range)
  332. else:
  333. # Equal distribution if no range
  334. vol_profile[i] += row['volume'] / (len(bins) - 1)
  335.  
  336. # Find POC (Point of Control) - bin with highest volume
  337. poc_idx = np.argmax(vol_profile)
  338. poc_price = (bins[poc_idx] + bins[poc_idx + 1]) / 2
  339.  
  340. total_vol = vol_profile.sum()
  341. if total_vol == 0:
  342. vah_list.extend([day_data['close'].mean()] * len(day_data))
  343. val_list.extend([day_data['close'].mean()] * len(day_data))
  344. poc_list.extend([day_data['close'].mean()] * len(day_data))
  345. continue
  346.  
  347. # Calculate Value Area (70% of volume around POC)
  348. va_vol = total_vol * 0.70
  349. va_indices = [poc_idx]
  350. current_vol = vol_profile[poc_idx]
  351.  
  352. lower_idx = poc_idx - 1
  353. upper_idx = poc_idx + 1
  354.  
  355. # Expand from POC until 70% volume is captured
  356. while current_vol < va_vol:
  357. lower_vol = vol_profile[lower_idx] if lower_idx >= 0 else 0
  358. upper_vol = vol_profile[upper_idx] if upper_idx < len(vol_profile) else 0
  359.  
  360. if lower_vol >= upper_vol and lower_idx >= 0:
  361. va_indices.append(lower_idx)
  362. current_vol += lower_vol
  363. lower_idx -= 1
  364. elif upper_idx < len(vol_profile):
  365. va_indices.append(upper_idx)
  366. current_vol += upper_vol
  367. upper_idx += 1
  368. else:
  369. break
  370.  
  371. # Calculate VAH and VAL
  372. vah = bins[max(va_indices) + 1] if max(va_indices) + 1 < len(bins) else bins[-1]
  373. val = bins[min(va_indices)]
  374.  
  375. vah_list.extend([vah] * len(day_data))
  376. val_list.extend([val] * len(day_data))
  377. poc_list.extend([poc_price] * len(day_data))
  378.  
  379. return np.array(vah_list), np.array(val_list), np.array(poc_list)
  380.  
  381.  
  382. print("✓ All indicators defined correctly!")
  383.  
  384. # @title
  385. # Mean Reversion Futures Strategy - REVISED
  386. # Professional Implementation with backtesting.py
  387.  
  388. import pandas as pd
  389. import numpy as np
  390.  
  391.  
  392. class MeanReversionStrategy(Strategy):
  393. """
  394. Mean Reversion Futures Strategy - 1 Hour Bars
  395.  
  396. Entry Logic:
  397. - VIX regime filtering (low/mid volatility)
  398. - Price touches VWAP bands (1σ or 2σ)
  399.  
  400. Exit Logic:
  401. - Stop Loss: 2.0x ATR
  402. - Take Profit: 100% at VWAP touch
  403. - Profit Target: $100 USD per contract
  404.  
  405. Position Management:
  406. - 1 contract per trade
  407. - Maximum 3 concurrent positions
  408. """
  409.  
  410. # ========================
  411. # STRATEGY PARAMETERS
  412. # ========================
  413.  
  414. # VIX Regime Thresholds
  415. vix_low_threshold = 12 # Below this: use 1σ and 2σ bands
  416. vix_mid_threshold = 27 # Above this: no entries
  417.  
  418. # VWAP Bands
  419. bb_std_1 = 1.0 # First standard deviation multiplier
  420. bb_std_2 = 2.0 # Second standard deviation multiplier
  421.  
  422. # ATR Settings
  423. atr_period = 14 # ATR calculation period
  424. stop_multiplier = 3.0 # Stop loss distance (ATR multiplier)
  425.  
  426. # Profit Target (DISABLED)
  427. profit_target_usd = 100.0 # Take profit at $100 per contract
  428.  
  429. # Volume Indicators (DISABLED)
  430. volume_ma_period = 9 # Volume MA period (not used)
  431.  
  432. # VWAP Slope (DISABLED)
  433. vwap_slope_lookback = 9 # VWAP slope lookback (not used)
  434. vwap_slope_threshold = 0.15 # VWAP slope threshold (not used)
  435.  
  436. # Position Management
  437. max_positions = 3 # Maximum concurrent positions
  438. # max_trades_per_day = 3 # DISABLED: No daily trade limit
  439.  
  440. # Session Management (DISABLED)
  441. # session_start_hour = 10 # DISABLED: Trade all hours
  442.  
  443. # Risk Management (DISABLED)
  444. # daily_loss_limit = 0.03 # DISABLED: No daily loss limit
  445.  
  446. # Contract Specifications
  447. price_per_point = 2.0 # ES=50, MES=5, MNQ=2
  448.  
  449.  
  450. def init(self):
  451. """Initialize indicators and tracking variables"""
  452.  
  453. # ========================
  454. # REQUIRED DATA VALIDATION
  455. # ========================
  456. if 'VIX' not in self.data.df.columns:
  457. raise ValueError("VIX data not found in DataFrame")
  458.  
  459. # ========================
  460. # CORE INDICATORS
  461. # ========================
  462.  
  463. # VWAP (Volume Weighted Average Price)
  464. self.vwap = self.I(
  465. calculate_vwap,
  466. self.data.Close, self.data.Volume,
  467. self.data.High, self.data.Low,
  468. self.data.index,
  469. name='VWAP'
  470. )
  471.  
  472. # VWAP Bands - 1 Standard Deviation
  473. bb_result_1 = self.I(
  474. calculate_vwap_bands,
  475. self.data.Close, self.data.Volume,
  476. self.data.High, self.data.Low,
  477. self.data.index,
  478. self.bb_std_1,
  479. name='BB1'
  480. )
  481. self.bb_upper_1 = bb_result_1[0] # ✓ +1σ band (VWAP + 1×StdDev)
  482. self.bb_lower_1 = bb_result_1[1] # ✓ -1σ band (VWAP - 1×StdDev)
  483.  
  484. # VWAP Bands - 2 Standard Deviations
  485. bb_result_2 = self.I(
  486. calculate_vwap_bands,
  487. self.data.Close, self.data.Volume,
  488. self.data.High, self.data.Low,
  489. self.data.index,
  490. self.bb_std_2,
  491. name='BB2'
  492. )
  493. self.bb_upper_2 = bb_result_2[0] # ✓ +2σ band (VWAP + 2×StdDev)
  494. self.bb_lower_2 = bb_result_2[1] # ✓ -2σ band (VWAP - 2×StdDev)
  495.  
  496. # ATR (Average True Range) for stop loss
  497. self.atr = self.I(
  498. calculate_atr,
  499. self.data.High, self.data.Low, self.data.Close,
  500. self.atr_period,
  501. name='ATR'
  502. )
  503.  
  504. # ========================
  505. # VIX DATA
  506. # ========================
  507. self.vix = self.data.df['VIX'].values
  508.  
  509. # ========================
  510. # TRACKING VARIABLES
  511. # ========================
  512. self.current_bar = 0
  513.  
  514.  
  515. def next(self):
  516. """Main strategy logic executed on each bar"""
  517.  
  518. self.current_bar += 1
  519.  
  520. # ========================
  521. # DATA VALIDATION
  522. # ========================
  523. if np.isnan(self.vwap[-1]) or np.isnan(self.atr[-1]):
  524. return
  525.  
  526. # ========================
  527. # MANAGE EXISTING POSITIONS
  528. # ========================
  529. open_trades = getattr(self, 'trades_open', [])
  530. if open_trades:
  531. self._manage_exits()
  532.  
  533. # ========================
  534. # POSITION LIMIT CHECK
  535. # ========================
  536. if len(open_trades) >= self.max_positions:
  537. return # Maximum 3 concurrent positions
  538.  
  539. # ========================
  540. # CURRENT MARKET DATA
  541. # ========================
  542. vix_current = self.vix[-1]
  543. close_current = self.data.Close[-1]
  544.  
  545. # ========================
  546. # ENTRY LOGIC
  547. # ========================
  548.  
  549. # Filter 1: VIX Regime
  550. if vix_current > self.vix_mid_threshold:
  551. return # No entries in high volatility
  552.  
  553. # Determine which bands are valid for entry based on VIX
  554. if vix_current < self.vix_low_threshold:
  555. # Low VIX: can enter at 1σ or 2σ
  556. entry_band_1 = self.bb_lower_1[-1]
  557. entry_band_2 = self.bb_lower_2[-1]
  558. else:
  559. # Mid VIX (12-27): only enter at 2σ
  560. entry_band_1 = None
  561. entry_band_2 = self.bb_lower_2[-1]
  562.  
  563. # Check if price is at qualifying band
  564. price_at_band_1 = entry_band_1 is not None and close_current <= entry_band_1
  565. price_at_band_2 = close_current <= entry_band_2
  566.  
  567. if not (price_at_band_1 or price_at_band_2):
  568. return # Price not at any qualifying band
  569.  
  570. # ========================
  571. # EXECUTE ENTRY
  572. # ========================
  573.  
  574. # Calculate stop loss at entry
  575. stop_loss_entry = close_current - (self.atr[-1] * self.stop_multiplier)
  576.  
  577. # Enter with 1 contract
  578. trade = self.buy(size=1, stop=stop_loss_entry)
  579.  
  580. if trade:
  581. # Store entry details on trade object
  582. trade.entry_price = close_current
  583. trade.entry_bar = self.current_bar
  584.  
  585. # Calculate profit target in price points
  586. profit_target_points = self.profit_target_usd / self.price_per_point
  587. trade.profit_target_price = close_current + profit_target_points
  588.  
  589.  
  590. def _manage_exits(self):
  591. """
  592. Exit management for all open positions
  593.  
  594. Exit Rules:
  595. 1. Stop Loss: 2.0x ATR below entry
  596. 2. VWAP Touch: Close 100% at VWAP
  597. 3. Profit Target: Close 100% at $100 profit
  598. """
  599.  
  600. close_current = self.data.Close[-1]
  601. vwap_current = self.vwap[-1]
  602.  
  603. # Process each open trade
  604. open_trades = list(getattr(self, 'trades_open', []))
  605.  
  606. for trade in open_trades:
  607. # Ensure trade attributes exist
  608. if not hasattr(trade, 'entry_price'):
  609. trade.entry_price = trade.entry_price
  610. if not hasattr(trade, 'profit_target_price'):
  611. # Calculate if missing
  612. profit_target_points = self.profit_target_usd / self.price_per_point
  613. trade.profit_target_price = trade.entry_price + profit_target_points
  614.  
  615. # ========================
  616. # EXIT 1: ATR
  617. # ========================
  618. stop_loss = trade.entry_price - (self.atr[-1] * self.stop_multiplier)
  619. if close_current <= stop_loss:
  620. trade.close()
  621. continue
  622.  
  623. # ========================
  624. # EXIT 2: UPPER BAND TARGET (VIX-Based)
  625. # ========================
  626.  
  627. # Determine which upper bands are valid for exit based on VIX
  628. # if vix_current < self.vix_low_threshold:
  629. # # Low VIX: can exit at +1σ or +2σ
  630. # exit_band_1 = self.bb_upper_1[-1]
  631. # exit_band_2 = self.bb_upper_2[-1]
  632.  
  633. # Exit if price touches either band
  634. # if close_current >= exit_band_1 or close_current >= exit_band_2:
  635. # trade.close() # Close 100% of position
  636. # continue
  637.  
  638. # else:
  639. # Mid/High VIX: only exit at +2σ (more conservative, let winners run)
  640. # exit_band_2 = self.bb_upper_2[-1]
  641.  
  642. # if close_current >= exit_band_2:
  643. # trade.close() # Close 100% of position
  644. # continue
  645.  
  646. # ========================
  647. # EXIT 2.2: VWAP TOUCH (100% Exit)
  648. # ========================
  649. #if close_current >= vwap_current:
  650. #trade.close() # Close 100% of position
  651. #continue
  652.  
  653. # ========================
  654. # EXIT 3: PROFIT TARGET ($100)
  655. # ========================
  656. if close_current >= trade.profit_target_price:
  657. trade.close() # Close 100% of position
  658. continue
  659.  
  660.  
  661. print("✓ MeanReversionStrategy class defined successfully!")
  662.  
  663. # @title
  664. # Download data
  665. print("="*60)
  666. print("DOWNLOADING DATA")
  667. print("="*60)
  668.  
  669. # Fetch ES futures
  670. es_data = fetch_futures_data('ES', max_days=729)
  671.  
  672. if es_data.empty:
  673. raise ValueError("Failed to fetch ES data")
  674.  
  675. # Fetch VIX
  676. vix_data = fetch_vix_data(max_days=729)
  677.  
  678. if vix_data.empty:
  679. raise ValueError("Failed to fetch VIX data")
  680.  
  681. # Merge
  682. data = merge_data(es_data, vix_data)
  683.  
  684. if data.empty:
  685. raise ValueError("Failed to merge data")
  686.  
  687. print("\n" + "="*60)
  688. print("DATA READY")
  689. print("="*60)
  690. print(f"Total bars: {len(data):,}")
  691. print(f"Date range: {data.index[0]} to {data.index[-1]}")
  692. print(f"Columns: {list(data.columns)}")
  693. print("\nFirst 3 rows:")
  694. print(data.head(3))
  695.  
  696. # @title
  697. # Backtest
  698. import pandas as pd # Ensure pandas is imported for set_option
  699.  
  700. # Define custom commission function for a flat fee per order
  701. def custom_commission(size, price):
  702. # $0.25 flat fee per order (buy or sell)
  703. return 0.25
  704.  
  705. bt = Backtest(
  706. data,
  707. MeanReversionStrategy,
  708. cash=30000,
  709. commission=custom_commission, # Use the custom callable for commission
  710. margin=0.05,
  711. trade_on_close=False,
  712. hedging=False,
  713. exclusive_orders=True
  714. )
  715.  
  716. # Run backtest
  717. stats = bt.run()
  718.  
  719. #Results table
  720. print("\n" + "="*60)
  721. print("BACKTEST RESULTS")
  722. print("="*60)
  723. print(stats)
  724.  
  725. # Trades
  726. # Export trade log
  727. trades = stats['_trades']
  728.  
  729. # Columns to drop
  730. columns_to_drop = [
  731. 'Entry_Hammer', 'Exit_Hammer',
  732. 'Entry_Engulfing', 'Exit_Engulfing',
  733. 'Entry_Doji', 'Exit_Doji'
  734. ]
  735.  
  736. # Drop columns if they exist
  737. trades = trades.drop(columns=[col for col in columns_to_drop if col in trades.columns])
  738.  
  739. print("\n" + "="*60)
  740. print(f"Total trades executed: {len(trades)}")
  741. print("="*60)
  742. print("Trade log (all trades):")
  743.  
  744. # Set display options to show all columns and prevent wrapping
  745. pd.set_option('display.max_columns', None)
  746. pd.set_option('display.width', 1000) # Adjust as needed for your screen
  747. pd.set_option('display.max_rows', None) # Show all rows
  748.  
  749. #print(trades) Disabled for now
  750.  
  751. # Reset display options to default after printing (optional, but good practice)
  752. pd.reset_option('display.max_columns')
  753. pd.reset_option('display.width')
  754. pd.reset_option('display.max_rows')
  755.  
  756. # Save to CSV
  757. trades.to_csv('trades.csv')
  758. print("\n Trades exported to trades.csv")
  759.  
  760. # @title
  761. stats.tail()
  762.  
  763. # @title
  764. stats['_equity_curve']
  765.  
  766. # @title
  767. stats['_trades']
  768.  
  769. # @title
  770. # Native backtesting.py plot
  771. bt.plot()
  772.  
  773. # @title
  774. # Optimizer
  775. stats_optimized, heatmap, optimize_result = bt.optimize(
  776. vix_low_threshold=[10, 15],
  777. vix_mid_threshold=[20, 25, 30],
  778. atr_period=[10, 14, 18],
  779. risk_per_trade=[0.005, 0.01, 0.015],
  780. stop_multiplier=[1.0, 1.5, 2.0],
  781. first_target_portion=[0.5, 0.7, 0.8],
  782. constraint=lambda p: p.vix_low_threshold < p.vix_mid_threshold,
  783. maximize='Equity Final [$]', #Change to % if needed or sharpe ratio
  784. method='sambo',
  785. max_tries=100,
  786. random_state=42,
  787. return_heatmap=True,
  788. return_optimization=True
  789. )
  790. # Results
  791. print("\n" + "="*60)
  792. print("OPTIMIZATION VERIFICATION")
  793. print("="*60)
  794.  
  795. # Check the stats object
  796. if stats_optimized is not None:
  797. print("Optimization completed successfully")
  798. print(f" Optimal strategy: {stats_optimized._strategy}")
  799. print(f" Total trades in optimized backtest: {stats_optimized['# Trades']}")
  800. print("\n--- Full Optimized Strategy Statistics ---")
  801. print(stats_optimized) # Print the entire stats_optimized object
  802. print("----------------------------------------")
  803. else:
  804. print("Optimization failed")
  805.  
  806. # @title
  807. print("\n" + "="*60)
  808. print("COMPARISON: DEFAULT vs OPTIMIZED")
  809. print("="*60)
  810.  
  811. if stats_optimized is not None:
  812. comparison = pd.DataFrame({
  813. 'Metric': ['Return [%]', 'Sharpe Ratio', 'Max Drawdown [%]', 'Win Rate [%]', '# Trades'],
  814. 'Default': [
  815. stats['Return [%]'],
  816. stats['Sharpe Ratio'],
  817. stats['Max. Drawdown [%]'],
  818. stats['Win Rate [%]'],
  819. stats['# Trades']
  820. ],
  821. 'Optimized': [
  822. stats_optimized['Return [%]'],
  823. stats_optimized['Sharpe Ratio'],
  824. stats_optimized['Max. Drawdown [%]'],
  825. stats_optimized['Win Rate [%]'],
  826. stats_optimized['# Trades']
  827. ]
  828. })
  829.  
  830. comparison['Improvement %'] = ((comparison['Optimized'] - comparison['Default']) / comparison['Default'] * 100).round(2)
  831.  
  832. print(comparison.to_string(index=False))
  833.  
  834. print("\n✓ Analysis complete!")
  835. else:
  836. print("Optimization did not complete successfully, cannot perform comparison.")
  837.  
  838. # @title
  839. # Best combination
  840. print("\n" + "="*60)
  841. print("TOP 10 PARAMETER COMBINATIONS WITH TRADES")
  842. print("="*60)
  843.  
  844. if 'heatmap' in globals() and heatmap is not None and len(heatmap) > 0:
  845. # Filter heatmap to include only results where trades actually occurred
  846. initial_cash = 30000 # Corrected: Use the explicit initial cash amount
  847. trading_results = heatmap[heatmap != initial_cash].sort_values(ascending=False)
  848.  
  849. if not trading_results.empty:
  850. top_10 = trading_results.head(10)
  851.  
  852. print("\nBest 10 combinations by Equity Final [$] (that made trades):\n")
  853.  
  854. for i, (idx, value) in enumerate(top_10.items(), 1):
  855. print(f"#{i} | Equity Final: {value:.2f}")
  856. print(f" VIX Low: {idx[0]}, VIX Mid: {idx[1]}, ATR: {idx[2]}")
  857. print(f" Risk: {idx[3]*100:.1f}%, Stop: {idx[4]}x, Target: {idx[5]*100:.0f}%")
  858. print()
  859.  
  860. # Also show worst 5 trading results (changed from 3 to 5)
  861. print("="*60)
  862. print("WORST 5 TRADING COMBINATIONS (for comparison)")
  863. print("="*60 + "\n")
  864.  
  865. worst_5 = trading_results.sort_values(ascending=True).head(5) # Changed to head(5)
  866.  
  867. for i, (idx, value) in enumerate(worst_5.items(), 1):
  868. print(f"#{i} | Equity Final: {value:.2f}")
  869. print(f" VIX Low: {idx[0]}, VIX Mid: {idx[1]}, ATR: {idx[2]}")
  870. print(f" Risk: {idx[3]*100:.1f}%, Stop: {idx[4]}x, Target: {idx[5]*100:.0f}%")
  871. print()
  872. else:
  873. print("No parameter combinations resulted in any trades within the explored range.")
  874.  
  875. else:
  876. print("\nNo heatmap data available, likely due to optimization failure or `return_heatmap` being False.")
  877.  
  878. # @title
  879. # Heatmaps
  880. from backtesting.lib import plot_heatmaps
  881. plot_heatmaps(heatmap, agg='mean')
  882.  
  883. # @title
  884. # Plot
  885. from sambo.plot import plot_evaluations
  886.  
  887. # Define the names of the parameters for plotting
  888. names = [
  889. "VIX Low Threshold",
  890. "VIX Mid Threshold",
  891. "ATR Period",
  892. "Risk Per Trade",
  893. "Stop Multiplier",
  894. "First Target Portion"
  895. ]
  896. _ = plot_evaluations(optimize_result, names=names)
  897.  
  898. # @title
  899. from sambo.plot import plot_objective
  900. names = ["VIX Low Threshold",
  901. "VIX Mid Threshold",
  902. "ATR Period",
  903. "Risk Per Trade",
  904. "Stop Multiplier",
  905. "First Target Portion"]
  906. _ = plot_objective(optimize_result, names=names, estimator='et')
  907.  
  908. # @title
  909. # Mean Reversion Futures Strategy - CORRECTED VERSION
  910. # Fixed profit target exits and position management
  911. # Compatible with backtesting.py framework
  912.  
  913. import pandas as pd
  914. import numpy as np
  915. from backtesting import Strategy
  916. from backtesting.lib import crossover
  917.  
  918.  
  919. # ========================
  920. # INDICATOR FUNCTIONS
  921. # ========================
  922.  
  923. def calculate_vwap(close, volume, high, low, index):
  924. """Calculate VWAP anchored to session start"""
  925. df = pd.DataFrame({
  926. 'close': close,
  927. 'volume': volume,
  928. 'high': high,
  929. 'low': low
  930. }, index=index)
  931.  
  932. df['typical_price'] = (df['high'] + df['low'] + df['close']) / 3
  933. df['tp_volume'] = df['typical_price'] * df['volume']
  934.  
  935. # Reset at session start (assumes daily sessions)
  936. df['date'] = df.index.date
  937. df['cum_tp_volume'] = df.groupby('date')['tp_volume'].cumsum()
  938. df['cum_volume'] = df.groupby('date')['volume'].cumsum()
  939.  
  940. vwap = df['cum_tp_volume'] / df['cum_volume']
  941. return vwap.values
  942.  
  943.  
  944. def calculate_vwap_bands(close, volume, high, low, index, std_mult):
  945. """Calculate VWAP standard deviation bands"""
  946. df = pd.DataFrame({
  947. 'close': close,
  948. 'volume': volume,
  949. 'high': high,
  950. 'low': low
  951. }, index=index)
  952.  
  953. df['typical_price'] = (df['high'] + df['low'] + df['close']) / 3
  954. df['tp_volume'] = df['typical_price'] * df['volume']
  955.  
  956. df['date'] = df.index.date
  957. df['cum_tp_volume'] = df.groupby('date')['tp_volume'].cumsum()
  958. df['cum_volume'] = df.groupby('date')['volume'].cumsum()
  959.  
  960. vwap = df['cum_tp_volume'] / df['cum_volume']
  961.  
  962. # Calculate standard deviation
  963. df['vwap'] = vwap
  964. df['squared_diff'] = (df['typical_price'] - df['vwap']) ** 2
  965. df['weighted_sq_diff'] = df['squared_diff'] * df['volume']
  966. df['cum_weighted_sq_diff'] = df.groupby('date')['weighted_sq_diff'].cumsum()
  967.  
  968. variance = df['cum_weighted_sq_diff'] / df['cum_volume']
  969. std = np.sqrt(variance)
  970.  
  971. upper_band = vwap + (std * std_mult)
  972. lower_band = vwap - (std * std_mult)
  973.  
  974. return upper_band.values, lower_band.values
  975.  
  976.  
  977. def calculate_atr(high, low, close, period):
  978. """Calculate Average True Range"""
  979. df = pd.DataFrame({'high': high, 'low': low, 'close': close})
  980.  
  981. df['h_l'] = df['high'] - df['low']
  982. df['h_pc'] = abs(df['high'] - df['close'].shift(1))
  983. df['l_pc'] = abs(df['low'] - df['close'].shift(1))
  984.  
  985. df['tr'] = df[['h_l', 'h_pc', 'l_pc']].max(axis=1)
  986. atr = df['tr'].rolling(window=period).mean()
  987.  
  988. return atr.values
  989.  
  990.  
  991. # ========================
  992. # STRATEGY CLASS
  993. # ========================
  994.  
  995. class MeanReversionStrategy(Strategy):
  996. """
  997. Mean Reversion Futures Strategy - CORRECTED
  998.  
  999. Entry Logic:
  1000. - VIX regime filtering (low/mid volatility)
  1001. - Price touches VWAP bands (1σ or 2σ)
  1002.  
  1003. Exit Logic:
  1004. - Profit Target: $100 USD per contract
  1005. - Optional: Stop Loss (3x ATR)
  1006.  
  1007. Position Management:
  1008. - 1 position at a time (backtesting.py limitation)
  1009. """
  1010.  
  1011. # ========================
  1012. # STRATEGY PARAMETERS
  1013. # ========================
  1014.  
  1015. # VIX Regime Thresholds
  1016. vix_low_threshold = 12 # Below this: use 1σ and 2σ bands
  1017. vix_mid_threshold = 27 # Above this: no entries
  1018.  
  1019. # VWAP Bands
  1020. bb_std_1 = 1.0 # First standard deviation
  1021. bb_std_2 = 2.0 # Second standard deviation
  1022.  
  1023. # ATR Settings
  1024. atr_period = 14 # ATR calculation period
  1025. stop_multiplier = 2.0 # Stop loss distance (ATR multiplier)
  1026.  
  1027. # Profit Target
  1028. profit_target_usd = 100.0 # Take profit at $100 per contract
  1029.  
  1030. # Contract Specifications
  1031. price_per_point = 2.0 # MNQ=2, MES=5, ES=50
  1032.  
  1033. # Exit Rules
  1034. use_stop_loss = True # Enable/disable stop loss
  1035. use_time_exit = False # Exit after max holding period
  1036. max_holding_bars = 48 # Maximum bars to hold (48h on 1H chart)
  1037.  
  1038.  
  1039. def init(self):
  1040. """Initialize indicators and tracking variables"""
  1041.  
  1042. # Validate VIX data
  1043. if 'VIX' not in self.data.df.columns:
  1044. raise ValueError("ERROR: VIX data not found in DataFrame. "
  1045. "Ensure your data includes a 'VIX' column.")
  1046.  
  1047. print("=" * 60)
  1048. print("INITIALIZING MEAN REVERSION STRATEGY")
  1049. print("=" * 60)
  1050.  
  1051. # VWAP
  1052. self.vwap = self.I(
  1053. calculate_vwap,
  1054. self.data.Close, self.data.Volume,
  1055. self.data.High, self.data.Low,
  1056. self.data.index,
  1057. name='VWAP'
  1058. )
  1059.  
  1060. # VWAP Bands - 1σ
  1061. bb_result_1 = self.I(
  1062. calculate_vwap_bands,
  1063. self.data.Close, self.data.Volume,
  1064. self.data.High, self.data.Low,
  1065. self.data.index,
  1066. self.bb_std_1,
  1067. name='BB1'
  1068. )
  1069. self.bb_upper_1 = bb_result_1[0]
  1070. self.bb_lower_1 = bb_result_1[1]
  1071.  
  1072. # VWAP Bands - 2σ
  1073. bb_result_2 = self.I(
  1074. calculate_vwap_bands,
  1075. self.data.Close, self.data.Volume,
  1076. self.data.High, self.data.Low,
  1077. self.data.index,
  1078. self.bb_std_2,
  1079. name='BB2'
  1080. )
  1081. self.bb_upper_2 = bb_result_2[0]
  1082. self.bb_lower_2 = bb_result_2[1]
  1083.  
  1084. # ATR
  1085. self.atr = self.I(
  1086. calculate_atr,
  1087. self.data.High, self.data.Low, self.data.Close,
  1088. self.atr_period,
  1089. name='ATR'
  1090. )
  1091.  
  1092. # VIX data
  1093. self.vix = self.data.df['VIX'].values
  1094.  
  1095. # Tracking
  1096. self.current_bar = 0
  1097. self.entry_bar = None
  1098. self.entry_price = None
  1099. self.profit_target_price = None
  1100. self.stop_loss_price = None
  1101.  
  1102. print(f"✓ Contract: {self.price_per_point} USD per point")
  1103. print(f"✓ Profit Target: ${self.profit_target_usd}")
  1104. print(f"✓ Stop Loss: {'Enabled' if self.use_stop_loss else 'DISABLED'}")
  1105. print(f"✓ Time Exit: {self.max_holding_bars} bars" if self.use_time_exit else "✓ Time Exit: Disabled")
  1106. print("=" * 60)
  1107.  
  1108.  
  1109. def next(self):
  1110. """Main strategy logic executed on each bar"""
  1111.  
  1112. self.current_bar += 1
  1113.  
  1114. # Skip if indicators not ready
  1115. if np.isnan(self.vwap[-1]) or np.isnan(self.atr[-1]):
  1116. return
  1117.  
  1118. current_price = self.data.Close[-1]
  1119.  
  1120. # ===========================
  1121. # EXIT MANAGEMENT (IF IN POSITION)
  1122. # ===========================
  1123.  
  1124. if self.position:
  1125. self._check_exits(current_price)
  1126. return # Don't enter new position if already in one
  1127.  
  1128. # ===========================
  1129. # ENTRY LOGIC (IF NO POSITION)
  1130. # ===========================
  1131.  
  1132. vix_current = self.vix[-1]
  1133. atr_current = self.atr[-1]
  1134.  
  1135. # VIX filter: No entries if VIX too high
  1136. if vix_current > self.vix_mid_threshold:
  1137. return
  1138.  
  1139. # Determine which bands to use based on VIX
  1140. if vix_current < self.vix_low_threshold:
  1141. # Low VIX: Use both 1σ and 2σ bands
  1142. entry_band_1 = self.bb_lower_1[-1]
  1143. entry_band_2 = self.bb_lower_2[-1]
  1144. else:
  1145. # Mid VIX: Only use 2σ band
  1146. entry_band_1 = None
  1147. entry_band_2 = self.bb_lower_2[-1]
  1148.  
  1149. # Check if price touched entry bands
  1150. price_at_band_1 = (entry_band_1 is not None and
  1151. current_price <= entry_band_1)
  1152. price_at_band_2 = current_price <= entry_band_2
  1153.  
  1154. if not (price_at_band_1 or price_at_band_2):
  1155. return
  1156.  
  1157. # ===========================
  1158. # EXECUTE ENTRY
  1159. # ===========================
  1160.  
  1161. self.buy(size=1)
  1162.  
  1163. # Store entry data for exit management
  1164. self.entry_bar = self.current_bar
  1165. self.entry_price = current_price
  1166.  
  1167. # Calculate profit target price
  1168. profit_target_points = self.profit_target_usd / self.price_per_point
  1169. self.profit_target_price = current_price + profit_target_points
  1170.  
  1171. # Calculate stop loss price (if enabled)
  1172. if self.use_stop_loss:
  1173. stop_distance_points = atr_current * self.stop_multiplier
  1174. self.stop_loss_price = current_price - stop_distance_points
  1175. else:
  1176. self.stop_loss_price = None
  1177.  
  1178.  
  1179. def _check_exits(self, current_price):
  1180. """
  1181. Check all exit conditions for open position
  1182.  
  1183. Exit Priority:
  1184. 1. Stop Loss (if enabled)
  1185. 2. Profit Target
  1186. 3. Time Exit (if enabled)
  1187. """
  1188.  
  1189.  
  1190. # ===========================
  1191. # EXIT 1: STOP LOSS
  1192. # ===========================
  1193. if self.use_stop_loss and self.stop_loss_price is not None:
  1194. if current_price <= self.stop_loss_price:
  1195. self.position.close()
  1196. return
  1197.  
  1198. # ===========================
  1199. # EXIT 2: PROFIT TARGET
  1200. # ===========================
  1201. if self.profit_target_price is not None:
  1202. if current_price >= self.profit_target_price:
  1203. self.position.close()
  1204. return
  1205.  
  1206. # ===========================
  1207. # EXIT 3: TIME-BASED EXIT
  1208. # ===========================
  1209. if self.use_time_exit and self.entry_bar is not None:
  1210. bars_in_trade = self.current_bar - self.entry_bar
  1211.  
  1212. if bars_in_trade >= self.max_holding_bars:
  1213. # Exit if held too long and not profitable enough
  1214. unrealized_pnl_usd = (current_price - self.entry_price) * self.price_per_point
  1215.  
  1216. # Exit if underwater or small profit after long hold
  1217. if unrealized_pnl_usd < self.profit_target_usd * 0.5: # Less than 50% of target
  1218. self.position.close()
  1219. return
  1220.  
  1221. # @title
  1222. # Backtest
  1223. import pandas as pd # Ensure pandas is imported for set_option
  1224.  
  1225. # Define custom commission function for a flat fee per order
  1226. def custom_commission(size, price):
  1227. # $0.25 flat fee per order (buy or sell)
  1228. return 0.25
  1229.  
  1230. bt = Backtest(
  1231. data,
  1232. MeanReversionStrategy,
  1233. cash=30000,
  1234. commission=custom_commission, # Use the custom callable for commission
  1235. margin=0.05,
  1236. trade_on_close=False,
  1237. hedging=False,
  1238. exclusive_orders=True
  1239. )
  1240.  
  1241. # Run backtest
  1242. stats = bt.run()
  1243.  
  1244. #Results table
  1245. print("\n" + "="*60)
  1246. print("BACKTEST RESULTS")
  1247. print("="*60)
  1248. print(stats)
  1249.  
  1250. # Trades
  1251. # Export trade log
  1252. trades = stats['_trades']
  1253.  
  1254. # Columns to drop
  1255. columns_to_drop = [
  1256. 'Entry_Hammer', 'Exit_Hammer',
  1257. 'Entry_Engulfing', 'Exit_Engulfing',
  1258. 'Entry_Doji', 'Exit_Doji'
  1259. ]
  1260.  
  1261. # Drop columns if they exist
  1262. trades = trades.drop(columns=[col for col in columns_to_drop if col in trades.columns])
  1263.  
  1264. # Calculate PnL in USD
  1265. # Get the price_per_point directly from the strategy class
  1266. price_per_point = MeanReversionStrategy.price_per_point
  1267. trades['PnL_USD'] = trades['PnL'] * price_per_point
  1268.  
  1269. print("\n" + "="*60)
  1270. print(f"Total trades executed: {len(trades)}")
  1271. print("="*60)
  1272. print("Trade log (all trades):")
  1273.  
  1274. # Set display options to show all columns and prevent wrapping
  1275. pd.set_option('display.max_columns', None)
  1276. pd.set_option('display.width', 1000) # Adjust as needed for your screen
  1277. pd.set_option('display.max_rows', None) # Show all rows
  1278.  
  1279. print(trades)
  1280.  
  1281. # Reset display options to default after printing (optional, but good practice)
  1282. pd.reset_option('display.max_columns')
  1283. pd.reset_option('display.width')
  1284. pd.reset_option('display.max_rows')
  1285.  
  1286. # Save to CSV
  1287. trades.to_csv('trades.csv')
  1288. print("\n Trades exported to trades.csv")
Advertisement
Add Comment
Please, Sign In to add comment