Overall Statistics |
Total Orders 112488 Average Win 2.32% Average Loss -4.03% Compounding Annual Return 16.457% Drawdown 50.300% Expectancy 0.120 Start Equity 100000 End Equity 455389.02 Net Profit 355.389% Sharpe Ratio 0.52 Sortino Ratio 0.487 Probabilistic Sharpe Ratio 5.654% Loss Rate 29% Win Rate 71% Profit-Loss Ratio 0.58 Alpha -0.004 Beta 1.524 Annual Standard Deviation 0.229 Annual Variance 0.053 Information Ratio 0.415 Tracking Error 0.093 Treynor Ratio 0.078 Total Fees $765.68 Estimated Strategy Capacity $0 Lowest Capacity Asset SPY R735QTJ8XC9X Portfolio Turnover 0.25% |
# region imports from AlgorithmImports import * from datetime import timedelta from pandas.core.frame import DataFrame # endregion class OptionTradeFunction(Enum): BUY = 1 SELL = 2 class VixFilterType(Enum): VVIX = 1 VIX_RANK = 2 VIX_RATIO = 3 class Options(QCAlgorithm): _vix_filter_type: VixFilterType = VixFilterType.VIX_RANK _vix_filter_value_threshold: float = 0.5 _DTE: int = 35 _OTM: float = 0.01 _vix_rank_lookback: int = 150 _percentage_traded: float = 1.0 _options_alloc: int = 100 # options quantity = equity symbol quantity / _options_alloc _safe_asset_ticker: str = 'IEF' _traded_option_right: OptionRight = OptionRight.PUT _option_trade_function: OptionTradeFunction = OptionTradeFunction.SELL def initialize(self) -> None: self.init_cash: int = 100_000 self.SetStartDate(2015, 1, 1) self.SetCash(self.init_cash) self._equity: Equity = self.add_equity("SPY", Resolution.MINUTE) self._equity.set_data_normalization_mode(DataNormalizationMode.RAW) self._benchmark_values: List[float] = [] self._market: Symbol = self._equity.symbol self._trade_fn: Callable[Symbol, float] = self.buy if self._option_trade_function == OptionTradeFunction.BUY else self.sell self._move_to_safe_asset: bool = False self._contract: Symbol|str = str() self._contracts_added: Set = set() # safe asset self._safe_asset: Symbol = self.add_equity(self._safe_asset_ticker, Resolution.MINUTE).symbol # subscribe to VIX filter asset/s if self._vix_filter_type == VixFilterType.VVIX: iv: Symbol = self.add_data(CBOE, "VVIX", Resolution.DAILY).symbol self._signal_assets = [iv] elif self._vix_filter_type == VixFilterType.VIX_RANK: iv: Symbol = self.add_data(CBOE, "VIX", Resolution.DAILY).symbol self._signal_assets = [iv] self.set_warm_up(self._vix_rank_lookback, Resolution.DAILY) elif self._vix_filter_type == VixFilterType.VIX_RATIO: iv: Symbol = self.add_data(CBOE, "VIX", Resolution.DAILY).symbol iv_3m: Symbol = self.add_data(CBOE, "VIX3M", Resolution.DAILY).symbol self._signal_assets = [iv, iv_3m] self.settings.daily_precise_end_time = False self._recent_month: int = -1 self.schedule.on(self.date_rules.every_day(self._market), \ self.time_rules.after_market_open(self._market, 30), \ self._VIX_filter) def _VIX_filter(self) -> None: if self._vix_filter_type == VixFilterType.VVIX: vvix: float = self.securities[self._signal_assets[0]].price if vvix > self._vix_filter_value_threshold: self._move_to_safe_asset = True elif self._vix_filter_type == VixFilterType.VIX_RANK: vix_rank: float = self._get_VIX_rank(self._signal_assets[0], self._vix_rank_lookback) if vix_rank > self._vix_filter_value_threshold: move_to_safe_asset = True elif self._vix_filter_type == VixFilterType.VIX_RATIO: vix_ratio: float = self.securities[self._signal_assets[0]].price / self.securities[self._signal_assets[1]].price if vix_ratio > self._vix_filter_value_threshold: self._move_to_safe_asset = True def on_end_of_day(self, symbol: Symbol) -> None: # print benchmark in main equity plot mkt_price_df: DataFrame = self.history(self._equity.symbol, 2, Resolution.DAILY) if not mkt_price_df.empty: benchmark_price: float = mkt_price_df['close'].unstack(level= 0).iloc[-1] if len(self._benchmark_values) == 2: self._benchmark_values[-1] = benchmark_price benchmark_perf: float = self.init_cash * (self._benchmark_values[-1] / self._benchmark_values[0]) self.plot('Strategy Equity', self._equity.symbol, benchmark_perf) else: self._benchmark_values.append(benchmark_price) def on_data(self, slice: Slice) -> None: if self.is_warming_up: return if not self._move_to_safe_asset: # liquidate safe asset holdings if self.portfolio[self._safe_asset].invested: self.liquidate(self._safe_asset) # trade options with market self._trade_option(slice) if not self.portfolio[self._market].invested: self.set_holdings(self._market, self._percentage_traded) else: self._move_to_safe_asset = False # liquidate market holdings and options if self.portfolio[self._market].invested: self.liquidate(self._market) option_invested: List = [ x.key for x in self.portfolio if x.value.invested and x.value.type == SecurityType.OPTION ] for symbol in option_invested: self.liquidate(symbol) # trade safe asset if not self.portfolio[self._safe_asset].invested: self.set_holdings(self._safe_asset, self._percentage_traded) # monthly option trade if self._recent_month != self.time.month: self._recent_month = self.time.month if self._contract: self.remove_option_contract(self._contract) self._contract = str() def _trade_option(self, slice: Slice) -> None: if self._contract == str(): self._contract = self._filter_options(slice, self._traded_option_right) return elif not self.portfolio[self._contract].invested and slice.contains_key(self._contract): self._trade_fn(self._contract, round(self.portfolio[self._market].quantity / self._options_alloc)) def _filter_options(self, slice: Slice, option_right: OptionRight) -> Symbol|str: contracts: List[Symbol] = self.option_chain_provider.get_option_contract_list(self._market, slice.time) underlying_price: float = self.securities[self._market].price if option_right == OptionRight.CALL: options: List[Symbol] = [ i for i in contracts if i.id.option_right == OptionRight.CALL and i.id.strike_price >= underlying_price * (1 + self._OTM) and i.id.date >= slice.time + timedelta(days=self._DTE) ] else: options: List[Symbol] = [ i for i in contracts if i.id.option_right == OptionRight.PUT and i.id.strike_price <= underlying_price * (1 - self._OTM) and i.id.date >= slice.time + timedelta(days=self._DTE) ] if len(options) > 0: expiry: datetime.datetime = min(list(map(lambda x: x.id.date, options))) index: int = 0 if option_right == OptionRight.PUT else -1 contract: Symbol = sorted([o for o in options if o.id.date == expiry], key = lambda x: underlying_price - x.id.strike_price)[index] if contract not in self._contracts_added: self._contracts_added.add(contract) self.add_option_contract(contract, Resolution.MINUTE) return contract else: return str() def _get_VIX_rank(self, vix: Symbol, vix_rank_lookback: int = 150) -> float: history: DataFrame = self.history(TradeBar, vix, vix_rank_lookback, Resolution.DAILY) rank: float = ((self.securities[vix].price - min(history["low"])) / (max(history["high"]) - min(history["low"]))) return rank