Overall Statistics |
Total Orders 1850 Average Win 2.56% Average Loss -2.26% Compounding Annual Return 4.361% Drawdown 67.300% Expectancy 0.075 Start Equity 10000000 End Equity 17304322.84 Net Profit 73.043% Sharpe Ratio 0.216 Sortino Ratio 0.206 Probabilistic Sharpe Ratio 0.024% Loss Rate 50% Win Rate 50% Profit-Loss Ratio 1.13 Alpha 0.128 Beta -0.49 Annual Standard Deviation 0.388 Annual Variance 0.151 Information Ratio -0.015 Tracking Error 0.433 Treynor Ratio -0.171 Total Fees $3609727.16 Estimated Strategy Capacity $28000000.00 Lowest Capacity Asset VX YNNC8UBM7FMX Portfolio Turnover 31.44% |
#region imports from AlgorithmImports import * from collections import deque import statsmodels.api as sm #endregion # Use Minute resolution data. # To get closing VIX quotes, get first quote during last 15 minutes of trading day where the bid-ask spread is no greater than 0.1 VIX future point or $100, as one VIX futures point is worth $1K. In the absense of such a quote, use the final bid/ask quotes of the day. # Use front rollover adjusted mini-S&P 500 futures prices. # The spot VIX values and the mini-S&P 500 futures prices are the averages of the open and closing price during the minute in which the above conditions for VIX futures contract hold # To calculate daily roll # the spread b/w the price of the front VIX futures contract that has 10+ business days until settlement and the VIX, scaled by the number of business days until settlement. # ** Use number of business days to settlement, not number of days. # Enter positions in the last 15 minutes of the trading day at the first instance that the bid-ask spread is no greater than 0.1, and in the absense of such opportunities, at the final bid-ask spread recorded at the close. # - compare mid-point of VIX futures to average of the open and close price of the VIX during the same minute. # ** Do regression to get the Beta coef => Then use coeff to calculate hedge ratio # ** Hedge should average around 1 contract for every 1 VIX contract. class TermStructureOfVixAlgorithm(QCAlgorithm): def initialize(self): self.set_start_date(2012, 1, 1) self.set_end_date(2024, 11, 1) self.set_cash(10_000_000) # Add a security initializer so we can trade ES contracts immediately. self.set_security_initializer( BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices)) ) self._vix_index = self.add_index("VIX") self._vx_future = self.add_future(Futures.Indices.VIX, leverage=1) self._vx_future.set_filter(14, 60) self._vx_future.prices = pd.Series() self._vx_future.trading_days_until_expiry = pd.Series() self._vx_future.invested_contract = None self._es_future = self.add_future( Futures.Indices.SP_500_E_MINI, data_normalization_mode=DataNormalizationMode.BACKWARDS_RATIO, data_mapping_mode=DataMappingMode.OPEN_INTEREST, contract_depth_offset=0 ) self._es_future.prices = pd.Series() self._get_liquid_price = False self.schedule.on( self.date_rules.every_day(self._vx_future.symbol), self.time_rules.before_market_close(self._vx_future.symbol, 15), lambda: setattr(self, '_get_liquid_price', True) ) self._daily_roll_entry_threshold = 0.05 # The paper uses +/-0.1 self._daily_roll_exit_threshold = 0.025 # The paper uses +/-0.05 self._lookback = 252 # Not defined in the paper. self._enable_hedge = False # True self.set_warm_up(timedelta(self._lookback*1.5)) def on_data(self, data): vx_price = None selected_contract = None trading_days_until_expiry = None if self._get_liquid_price: chain = data.future_chains.get(self._vx_future.symbol) # Get the VIX Future price. if chain: for contract in chain: # Is the chain always sorted by expiry? # Select the contract with at least 10 trading days until expiry. trading_days_until_expiry = self._trading_days_until_expiry(contract) if trading_days_until_expiry < 10: continue # Wait for the selected contract to have a narrow spread. if (contract.ask_price - contract.bid_price > 0.1 and # If the spread isn't narrow before market close, use the prices right before market close. self._vx_future.exchange.hours.is_open(self.time + timedelta(minutes=1), extended_market_hours=False)): break vx_price = (contract.ask_price + contract.bid_price) / 2 selected_contract = contract if not vx_price: return self._get_liquid_price = False # "The spot VIX values and the mini-S&P 500 futures prices used in the study are averages of the open and closing quotes during the minute in which the above conditions for VIX futures contracts hold." (p.6) vix_price = (self._vix_index.open + self._vix_index.close) / 2 es_price = (self._es_future.open + self._es_future.close) / 2 # "the front rollover adjusted mini-S&P 500 futures prices" (p.5) self.plot('ES', 'Price', es_price) self.plot('VIX', 'Future Price', vx_price) self.plot('VIX', 'Spot Price', vix_price) basis = (vx_price - vix_price) in_contango = basis > 0 self.plot('In Contango?', 'Value', int(in_contango)) daily_roll = basis / trading_days_until_expiry self.plot('Daily Roll', 'Value', daily_roll) # "the size of the mini-S&P futures hedge is based on out of sample hedge ratio # estimates. The hedge ratios are constructed from regressions of VIX futures price changes on a # constant and on contemporaneous percentage changes of the front mini-S&P 500 futures contract # both alone and multiplied by the number of business days that the VIX futures contract is from # settlement" (pp. 11-12) t = self.time self._es_future.prices.loc[t] = es_price self._vx_future.prices.loc[t] = vx_price self._vx_future.trading_days_until_expiry.loc[t] = trading_days_until_expiry self._es_future.prices = self._es_future.prices.iloc[-self._lookback:] self._vx_future.prices = self._vx_future.prices.iloc[-self._lookback:] self._vx_future.trading_days_until_expiry = self._vx_future.trading_days_until_expiry.iloc[-self._lookback:] # Wait until there are sufficient samples to fit the regression model. if len(self._vx_future.prices) < self._lookback or self.is_warming_up: return vx_changes = self._vx_future.prices.pct_change()[1:] es_returns = self._es_future.prices.pct_change()[1:] X = pd.DataFrame( { 'es_returns': es_returns, 'es_returns*tts': es_returns * self._vx_future.trading_days_until_expiry[1:] }, index=es_returns.index ) X = sm.add_constant(X) y = vx_changes model = sm.OLS(y, X).fit() beta_1 = model.params[1] beta_2 = model.params[2] # "The β1 coefficient should be significantly negative in light of the tendency of VIX futures prices to move inversely to equity returns." (p. 12) self.plot('Regression B1', 'Value', beta_1) # "The β2 coefficient should be significantly positive if the reaction of VIX futures prices to equity returns is more subdued the further contracts are from settlement." (p. 12) self.plot('Regression B2', 'Value', beta_2) # Calculate the hedge ratio. Equation 4 on page 13. hedge_ratio = ( beta_1 * 1_000 + beta_2 * trading_days_until_expiry * 1_000 ) / (0.01 * es_price * 50) # "The average hedge ratio is close to one mini-S&P futures contract per VIX futures contract" (p. 13) with a range from about 0.5 to 2. self.plot('Hedge Ratio', 'Value', hedge_ratio) # Look for trade entries. # "Short VIX futures positions are entered when the VIX futures basis is in # contango and the daily roll exceeds .10 VIX futures points ($100 per day) and long VIX futures # positions are entered when the VIX futures basis is in backwardation and the daily roll is less # than -.10 VIX futures points" (p. 14) if not self.portfolio.invested: if in_contango and daily_roll > self._daily_roll_entry_threshold: weight = -0.5 # Enter short VIX Future trade. elif not in_contango and daily_roll < -self._daily_roll_entry_threshold: weight = 0.5 # Enter long VIX Future trade. else: return self.set_holdings(selected_contract.symbol, weight, tag=f"Entry: Expires on {selected_contract.expiry}") self._vx_future.invested_contract = selected_contract # Add ES hedge. if self._enable_hedge: es_contract_symbols = self.future_chain_provider.get_future_contract_list(self._es_future.symbol, self.time) es_contract_symbol = sorted( [s for s in es_contract_symbols if s.id.date >= self._vx_future.invested_contract.expiry], key=lambda symbol: symbol.id.date )[0] self.add_future_contract(es_contract_symbol) self.market_order(es_contract_symbol, (1 if weight > 0 else -1) * -np.sign(hedge_ratio)) return # Look for trade exits. # Exit long VIX Future trade when daily_roll < self._daily_roll_exit_threshold or when the contract expires next day. # Exit short VIX Future trade when daily_roll > -self._daily_roll_exit_threshold or when the contract expires next day. trading_days_until_expiry = self._trading_days_until_expiry(self._vx_future.invested_contract) holding = self.portfolio[self._vx_future.invested_contract.symbol] tag = "" if trading_days_until_expiry <= 1: tag = f"Exit: Expires in {trading_days_until_expiry} trading day(s)" elif holding.is_long and daily_roll < self._daily_roll_exit_threshold: tag = f"Exit: daily roll ({daily_roll}) < threshold" elif holding.is_short and daily_roll > -self._daily_roll_exit_threshold: tag = f"Exit: daily roll ({daily_roll}) > -threshold" if tag: self.liquidate(tag=tag) self._vx_future.invested_contract = None def _trading_days_until_expiry(self, contract): return len(list(self.trading_calendar.get_trading_days(self.time, contract.expiry - timedelta(1))))