Overall Statistics |
Total Orders 223 Average Win 0.33% Average Loss -0.39% Compounding Annual Return 53.144% Drawdown 11.600% Expectancy 0.376 Start Equity 400000 End Equity 594953 Net Profit 48.738% Sharpe Ratio 1.769 Sortino Ratio 1.75 Probabilistic Sharpe Ratio 80.647% Loss Rate 25% Win Rate 75% Profit-Loss Ratio 0.84 Alpha 0.121 Beta 1.184 Annual Standard Deviation 0.175 Annual Variance 0.031 Information Ratio 1.185 Tracking Error 0.127 Treynor Ratio 0.261 Total Fees $169.00 Estimated Strategy Capacity $0 Lowest Capacity Asset MARA VSI9G9W3OAXX Portfolio Turnover 1.50% |
from AlgorithmImports import * import datetime import math class HighIVCoveredCallComposite(QCAlgorithm): def Initialize(self): self.SetStartDate(2024, 1, 1) self.SetEndDate(2024, 12, 31) self.SetCash(400000) self.high_iv_stock_count = 10 self.selected_symbols = [] self.selected_contracts = {} self.contract_indicators = {} self.spy = self.AddEquity("SPY", Resolution.Daily, dataNormalizationMode=DataNormalizationMode.Raw).Symbol self.delta_flags = {} self.new_contract_flags = {} self.processed_symbols = [] self.earnings_dict = {} self.collect_earnings_info_window = False self.earnings_data = self.AddData(EODHDUpcomingEarnings, "earnings").Symbol # Schedule earnings collection window self.Schedule.On(self.DateRules.WeekEnd(self.spy, -1), self.TimeRules.At(23, 59), self.ActivateEarningsCollection) self.Schedule.On(self.DateRules.WeekEnd(self.spy), self.TimeRules.At(0, 1), self.DeactivateEarningsCollection) # Universe selection settings self.UniverseSettings.Resolution = Resolution.Daily self.UniverseSettings.DataNormalizationMode = DataNormalizationMode.Raw # Seeder and fill forward setting seeder = FuncSecuritySeeder(self.get_last_known_prices) self.SetSecurityInitializer(BrokerageModelSecurityInitializer(self.brokerage_model, seeder)) self.fill_forward = True # Schedule quarterly equity selection on the first trading day of each quarter self.Schedule.On(self.DateRules.MonthStart(self.spy, 1), self.TimeRules.AfterMarketOpen(self.spy, 1), self.SelectSymbols) # Schedule for preparing covered calls on Fridays at 3 pm self.Schedule.On(self.DateRules.WeekEnd(self.spy), self.TimeRules.At(15, 0), self.SelectCoveredCalls) self.sell_window_open = datetime.time(15, 1) self.sell_window_close = datetime.time(15, 58) # Universe selection self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction) def OnSecuritiesChanged(self, changes): for added_security in changes.AddedSecurities: symbol = added_security.Symbol # Add earnings data for the new symbol if needed self.earnings_data = self.AddData(EODHDUpcomingEarnings, "earnings").Symbol def ActivateEarningsCollection(self): self.collect_earnings_info_window = True def DeactivateEarningsCollection(self): self.collect_earnings_info_window = False def CoarseSelectionFunction(self, coarse): filtered = [x for x in coarse if x.HasFundamentalData and x.Price > 5 and x.DollarVolume > 1e7] sortedByDollarVolume = sorted(filtered, key=lambda x: x.DollarVolume, reverse=True) return [x.Symbol for x in sortedByDollarVolume[:20]] def FineSelectionFunction(self, fine): # Retrieve benchmark (SPY) data benchmark_history = self.History(self.spy, 30, Resolution.Daily) if benchmark_history.empty: # If no benchmark data, just return the top fine fundamentals return [f.Symbol for f in fine[:self.high_iv_stock_count]] benchmark_closes = benchmark_history['close'] benchmark_returns = benchmark_closes.pct_change().dropna() benchmark_avg_return = benchmark_returns.mean() benchmark_cum_return = (benchmark_closes[-1] / benchmark_closes[0]) - 1 if len(benchmark_closes) > 1 else 0 symbols_metrics = {} for f_obj in fine: history = self.History(f_obj.Symbol, 30, Resolution.Daily) if history.empty: continue closes = history['close'] daily_returns = closes.pct_change().dropna() if daily_returns.empty: continue # Historical Volatility (annualized) hist_volatility = daily_returns.std() * math.sqrt(252) if len(daily_returns) > 1 else 0 # Sharpe Ratio excess_returns = daily_returns.mean() - benchmark_avg_return sharpe_ratio = excess_returns / hist_volatility if hist_volatility > 0 else 0 # Relative Strength: Compare stock returns to benchmark returns stock_cum_return = (closes[-1] / closes[0]) - 1 if len(closes) > 1 else 0 # One simple RS metric: difference between stock and benchmark cumulative returns relative_strength = stock_cum_return - benchmark_cum_return # Momentum: Use cumulative return over the last 30 days momentum = stock_cum_return # Composite Score: # Adjust these weights based on preference: # For example: # Sharpe Ratio: 30% # Inverse Volatility: We actually want lower volatility, so we might consider 1/hist_vol, but let's keep it simple # Relative Strength: 25% # Momentum: 25% # Hist Vol can be included as a negative factor if we want lower volatility assets # For simplicity, let's just do a weighted combo: # We'll treat lower volatility as better by taking negative vol. w_sharpe = 0.40 w_rel_str = 0.20 w_momentum = 0.10 w_vol = 0.30 # Negative weighting for volatility still applies # Lower volatility is better, so we use negative hist_volatility composite_score = (w_sharpe * sharpe_ratio) + (w_rel_str * relative_strength) + (w_momentum * momentum) + (w_vol * (-hist_volatility)) symbols_metrics[f_obj.Symbol] = composite_score # Sort symbols by composite score and select the top N sorted_symbols = sorted(symbols_metrics.items(), key=lambda x: x[1], reverse=True) self.selected_symbols = [kv[0] for kv in sorted_symbols[:self.high_iv_stock_count]] return self.selected_symbols def SelectSymbols(self): self.selected_contracts.clear() for symbol in self.selected_symbols: price = self.Securities[symbol].Price required_cash = price * (100 - self.Portfolio[symbol].Quantity) if self.Portfolio.Cash >= required_cash: if self.Portfolio[symbol].Quantity < 100: self.MarketOrder(symbol, 100 - self.Portfolio[symbol].Quantity) def SelectCoveredCalls(self): days_until_friday = (4 - self.Time.weekday()) % 7 if days_until_friday == 0: days_until_friday = 7 next_friday = (self.Time + timedelta(days=days_until_friday)).date() for symbol in self.selected_symbols: # Check if any contract for this symbol is already invested if any(self.Portfolio[contract].Invested for contract in self.selected_contracts if contract.Underlying == symbol): continue chain = self.OptionChain(symbol, flatten=True).DataFrame if chain.empty: continue filtered_chain = chain[ (chain.expiry.dt.date == next_friday) & (chain.right == OptionRight.CALL) & (chain.delta < 0.35) & (chain.delta > 0.25) ] if not filtered_chain.empty: for contract in filtered_chain.index: self.AddOptionContract(contract, Resolution.Minute) self.selected_contracts[contract] = False self.contract_indicators[contract] = self.d(contract) def MonitorDeltas(self, data: Slice): days_until_current_friday = (4 - self.Time.weekday()) % 7 current_friday = (self.Time + timedelta(days=days_until_current_friday)).date() for contract, sold in list(self.selected_contracts.items()): if not sold: continue if self.delta_flags.get(contract, False): continue if contract in self.contract_indicators and data.ContainsKey(contract): delta_value = self.contract_indicators[contract].Current.Value if delta_value > 0.40: underlying_symbol = contract.Underlying # Liquidate the current contract open_orders = self.Transactions.GetOpenOrders(contract) for order in open_orders: if order.Type == OrderType.Limit: self.Transactions.CancelOrder(order.Id) self.Liquidate(contract) chain = self.OptionChain(underlying_symbol).DataFrame if not chain.empty: filtered_chain = chain[ (chain.expiry.dt.date == current_friday) & (chain.right == OptionRight.CALL) ] if not filtered_chain.empty: filtered_chain["delta_diff"] = abs(filtered_chain.delta - 0.30) closest_to_03 = filtered_chain.sort_values("delta_diff").iloc[0] new_contract = closest_to_03.name self.AddOptionContract(new_contract, Resolution.Minute) self.contract_indicators[new_contract] = self.d(new_contract) self.delta_flags[contract] = True self.new_contract_flags[contract] = new_contract return def ProcessNewContract(self, data: Slice): for original_contract, new_contract in list(self.new_contract_flags.items()): if data.ContainsKey(new_contract): new_delta = self.contract_indicators[new_contract].Current.Value premium = self.Securities[new_contract].Price if premium >= 0.50 and new_delta < 0.40 and new_delta > 0: self.MarketOrder(new_contract, -1) gtc_price = round(premium * 0.1, 2) self.LimitOrder(new_contract, 1, gtc_price, tag="GTC Buy Order") self.selected_contracts[new_contract] = True self.delta_flags[original_contract] = False del self.new_contract_flags[original_contract] def on_data(self, data: Slice) -> None: current_time = self.Time.time() #if self.Time.weekday() in [0, 1, 2]: # self.MonitorDeltas(data) #self.ProcessNewContract(data) if self.collect_earnings_info_window: for symbol in self.selected_symbols: upcomings_earnings_for_symbol = data.get(EODHDUpcomingEarnings).get(symbol) if upcomings_earnings_for_symbol and upcomings_earnings_for_symbol.report_date <= self.Time + timedelta(days=10): self.earnings_dict[symbol] = upcomings_earnings_for_symbol.report_date for symbol, report_date in list(self.earnings_dict.items()): if report_date < self.Time: del self.earnings_dict[symbol] if self.Time.weekday() == 4 and self.sell_window_open <= current_time <= self.sell_window_close: processed_contracts = [] for symbol in self.selected_symbols: if symbol in self.earnings_dict.keys() and self.earnings_dict[symbol] is not None: continue selected_contract = self.SelectContractClosestToDelta(symbol) if selected_contract and not self.selected_contracts[selected_contract]: if data.ContainsKey(selected_contract) and data[selected_contract] is not None: underlying_symbol = selected_contract.Underlying if self.Portfolio[underlying_symbol].Quantity >= 100: premium = data[selected_contract].Price quantity_multiplier = 1 if premium >= 0.50: delta_value = self.contract_indicators[selected_contract].Current.Value self.MarketOrder(selected_contract, -1*quantity_multiplier) gtc_price = round(premium * 0.1, 2) self.LimitOrder(selected_contract, 1*quantity_multiplier, gtc_price, tag="GTC Buy Order") processed_contracts.append(selected_contract) self.processed_symbols.append(symbol) for contract in processed_contracts: self.selected_contracts[contract] = True else: self.processed_symbols.clear() if self.Time.weekday() == 4 and current_time > self.sell_window_close: unsold_contracts = [contract for contract, sold in self.selected_contracts.items() if not sold] for contract in unsold_contracts: self.selected_contracts.pop(contract) def SelectContractClosestToDelta(self, symbol, target_delta=0.30): symbol_contracts = { contract: self.contract_indicators[contract].Current.Value for contract in self.selected_contracts if contract.Underlying == symbol } if not symbol_contracts: return None closest_contract = min(symbol_contracts, key=lambda c: abs(symbol_contracts[c] - target_delta)) return closest_contract