Overall Statistics |
Total Orders 254 Average Win 0.12% Average Loss -8.17% Compounding Annual Return 11.409% Drawdown 45.500% Expectancy -0.003 Start Equity 1000000 End Equity 2932431.99 Net Profit 193.243% Sharpe Ratio 0.392 Sortino Ratio 0.293 Probabilistic Sharpe Ratio 2.713% Loss Rate 2% Win Rate 98% Profit-Loss Ratio 0.01 Alpha -0.012 Beta 1.101 Annual Standard Deviation 0.195 Annual Variance 0.038 Information Ratio -0.034 Tracking Error 0.111 Treynor Ratio 0.07 Total Fees $4796.91 Estimated Strategy Capacity $1000.00 Lowest Capacity Asset SPY YNW79OWW6MME|SPY R735QTJ8XC9X Portfolio Turnover 0.11% |
# region imports from AlgorithmImports import * # endregion class WheelStrategyAlgorithm(QCAlgorithm): def initialize(self): self.set_start_date(2015, 1, 1) self.set_cash(1_000_000) self.set_security_initializer(BrokerageModelSecurityInitializer(self.brokerage_model, FuncSecuritySeeder(self.get_last_known_prices))) self._equity = self.add_equity("SPY", data_normalization_mode=DataNormalizationMode.Raw) self._otm_threshold = self._default_otm_threshold = 0.15 self._expiry = 28 self._last_options_missing_log = None def _get_target_contract(self, right, target_price): # Start with default threshold self._otm_threshold = self._default_otm_threshold contract_symbols = self.option_chain_provider.get_option_contract_list(self._equity.symbol, self.time) if not contract_symbols: # Log missing options data (but not more frequently than once per day) if (self._last_options_missing_log is None or self._last_options_missing_log.date() != self.time.date()): self.debug(f"No options data available for {self._equity.symbol} on {self.time.date()}") self._last_options_missing_log = self.time return None expiry = min([s.id.date for s in contract_symbols if s.id.date.date() >= self.time.date() + timedelta(self._expiry)]) while True: filtered_symbols = [ s for s in contract_symbols if (s.id.date == expiry and s.id.option_right == right and (s.id.strike_price <= target_price if right == OptionRight.PUT else s.id.strike_price >= target_price)) ] if filtered_symbols: symbol = sorted(filtered_symbols, key=lambda s: s.id.strike_price, reverse=right == OptionRight.PUT)[0] self.add_option_contract(symbol) return symbol # If no symbols found, reduce OTM threshold by 1% and try again self._otm_threshold -= 0.01 # Set a minimum threshold to prevent infinite loop if self._otm_threshold <= 0.05: raise Exception(f"No suitable options found even with reduced threshold for {right}") # Recalculate target price with new threshold target_price = self._equity.price * (1-self._otm_threshold if right == OptionRight.PUT else 1+self._otm_threshold) def on_data(self, data): if not self.portfolio.invested and self.is_market_open(self._equity.symbol): symbol = self._get_target_contract(OptionRight.PUT, self._equity.price * (1-self._otm_threshold)) if symbol: self.debug(f"Placed PUT order with OTM threshold: {self._otm_threshold}") self.set_holdings(symbol, -0.2) elif [self._equity.symbol] == [symbol for symbol, holding in self.portfolio.items() if holding.invested]: symbol = self._get_target_contract(OptionRight.CALL, self._equity.price * (1+self._otm_threshold)) if symbol: self.debug(f"Placed CALL order with OTM threshold: {self._otm_threshold}") self.market_order(symbol, -self._equity.holdings.quantity / 100)