Overall Statistics
Total Orders
542
Average Win
0.31%
Average Loss
-0.69%
Compounding Annual Return
108.250%
Drawdown
20.600%
Expectancy
0.041
Start Equity
400000
End Equity
770202
Net Profit
92.550%
Sharpe Ratio
2.466
Sortino Ratio
3.123
Probabilistic Sharpe Ratio
88.228%
Loss Rate
28%
Win Rate
72%
Profit-Loss Ratio
0.45
Alpha
0.477
Beta
1.288
Annual Standard Deviation
0.271
Annual Variance
0.073
Information Ratio
2.192
Tracking Error
0.237
Treynor Ratio
0.519
Total Fees
$407.50
Estimated Strategy Capacity
$0
Lowest Capacity Asset
ADBE R735QTJ8XC9X
Portfolio Turnover
3.15%
from AlgorithmImports import *
import datetime

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 = {}  # Dictionary to track delta flags per contract
        self.new_contract_flags = {}  # Track new contracts per symbol
        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)

        # Define sell window time range (15:01 - 15:58)
        self.sell_window_open = datetime.time(15, 1)
        self.sell_window_close = datetime.time(15, 58)

        # Universe selection functions
        self.AddUniverse(self.CoarseSelectionFunction, self.FineSelectionFunction)

    def OnSecuritiesChanged(self, changes):
        # Iterate over newly added securities
        for added_security in changes.AddedSecurities:
            symbol = added_security.Symbol
            # Add earnings data for the new symbol
            self.earnings_data = self.AddData(EODHDUpcomingEarnings, "earnings").Symbol
            # Store the earnings data symbol for future reference
     
    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):
        symbols_metrics = {}
        benchmark_history = self.History(self.spy, 30, Resolution.Daily)
        benchmark_returns = benchmark_history['close'].pct_change().dropna() if not benchmark_history.empty else None

        for f in fine:
            history = self.History(f.Symbol, 30, Resolution.Daily)
            if history.empty or benchmark_returns is None:
                continue

            # Calculate daily returns for the symbol
            daily_returns = history['close'].pct_change().dropna()
            if daily_returns.empty:
                continue

            # Calculate historical volatility (standard deviation of returns)
            hist_volatility = daily_returns.std() * (252 ** 0.5)  # Annualized volatility

            # Calculate Sharpe Ratio
            excess_returns = daily_returns.mean() - (benchmark_returns.mean() if not benchmark_returns.empty else 0)
            sharpe_ratio = excess_returns / hist_volatility if hist_volatility > 0 else 0

            # Combine historical volatility and Sharpe Ratio for a composite score
            composite_score = hist_volatility * 0.7 + sharpe_ratio * 0.3  # Adjust weights as needed
            symbols_metrics[f.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)
           # else:
                #self.Debug(f"Not enough cash to buy 100 shares of {symbol}. Required: {required_cash}, Available: {self.Portfolio.Cash}")

    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

            # Get the option chain for the symbol
            chain = self.OptionChain(symbol).DataFrame
            if chain.empty:
                #self.Debug(f"No options in chain for {symbol} at {self.Time}")
                continue

            # Filter contracts expiring next Friday with delta between 0.25 and 0.35
            filtered_chain = chain[
                (chain.expiry.dt.date == next_friday) & 
                (chain.right == OptionRight.CALL) & 
                (chain.delta < 0.35) & 
                (chain.delta > 0.25)
            ]

            # Add each contract that meets the criteria
            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)
                    delta_value = self.contract_indicators[contract].Current.Value

                    #self.Debug(f"Added contract with delta {delta_value} for {symbol} expiring on {next_friday}")
           # else:
                #self.Debug(f"No suitable contracts expiring on {next_friday} for {symbol}")
    def MonitorDeltas(self, data: Slice):
        # Calculate the current week's Friday
        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:  # Skip unsold contracts
                continue

            # Skip if delta flag is raised for this contract
            if self.delta_flags.get(contract, False):
                continue

            # Ensure the contract data and indicators are available
            if contract in self.contract_indicators and data.ContainsKey(contract):
                delta_value = self.contract_indicators[contract].Current.Value

                # Check if delta exceeds the threshold
                if delta_value > 0.40:
                    underlying_symbol = contract.Underlying
                    #self.Debug(f"Delta {delta_value:.2f} exceeds threshold for {underlying_symbol}")

                    if self.Portfolio[underlying_symbol].Quantity >= 100:
                        # 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)

                        # Get the option chain for the current week's Friday
                        chain = self.OptionChain(underlying_symbol).DataFrame
                        if not chain.empty:
                            # Filter chain for closest delta to 0.3
                            filtered_chain = chain[
                                (chain.expiry.dt.date == current_friday) & 
                                (chain.right == OptionRight.CALL)
                            ]
                            filtered_chain["delta_diff"] = abs(filtered_chain.delta - 0.30)
                            closest_to_03 = filtered_chain.sort_values("delta_diff").iloc[0]

                            # Select a new contract
                            new_contract = closest_to_03.name
                            self.AddOptionContract(new_contract, Resolution.Minute)

                            # Add to contract indicators and set delta flag
                            self.contract_indicators[new_contract] = self.d(new_contract)
                            self.delta_flags[contract] = True  # Raise flag for this contract
                            self.new_contract_flags[contract] = new_contract
                            #self.Debug(f"New contract added for {contract}: {new_contract}. Waiting for data.")
                            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:
                    # Place orders for the new contract
                    self.MarketOrder(new_contract, -1)
                    gtc_price = round(premium * 0.1, 2)
                    self.LimitOrder(new_contract, 1, gtc_price, tag="GTC Buy Order")

                    # Update selected contracts
                    self.selected_contracts[new_contract] = True
                    #self.Debug(f"Rolled contract to {new_contract} with delta {new_delta:.2f}")
                #else:
                    #self.Debug(f"Skipped rolling: Premium {premium:.2f} or delta {new_delta:.2f} not suitable.")

                # Reset flags for this contract
                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()

        # Perform delta monitoring on Monday, Tuesday, and Wednesday
        #if self.Time.weekday() in [0, 1, 2]:  # Monday = 0, Tuesday = 1, Wednesday = 2
            #self.MonitorDeltas(data)

        # Process new contracts once data is available
        #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

                # Remove earnings info for past dates
        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 = []
        
            # Iterate over selected contracts by symbol
            for symbol in self.selected_symbols:
                if symbol in self.earnings_dict.keys() and self.earnings_dict[symbol] is not None:
                    #self.Debug(f"Skipping covered calls for {symbol} due to upcoming earnings on {self.earnings_dict[symbol]}")
                    continue  # Skip this symbol if earnings announcement is within 7 days
                selected_contract = self.SelectContractClosestToDelta(symbol)  # Filter contracts for delta closest to 0.30

                if selected_contract and not self.selected_contracts[selected_contract]:  # Ensure contract hasn't been sold
                    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#self.Portfolio[underlying_symbol].Quantity / 100
                            if premium >= 0.50:
                                delta_value = self.contract_indicators[selected_contract].Current.Value
                                self.MarketOrder(selected_contract, -1*quantity_multiplier, tag=f"{self.contract_indicators[selected_contract].Current.Value} {self.Portfolio[underlying_symbol].Quantity} {self.Portfolio[selected_contract].Quantity}")
                                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)
        
            # Mark processed contracts as sold
            for contract in processed_contracts:
                self.selected_contracts[contract] = True
        else:
            self.processed_symbols.clear()
        # Clean up unsold contracts after the sell window closes
        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):
        # Filter for contracts in selected_contracts that match the specified symbol
        symbol_contracts = {
            contract: self.contract_indicators[contract].Current.Value
            for contract in self.selected_contracts
            if contract.Underlying == symbol
        }

        if not symbol_contracts:
            #self.Debug(f"No contracts available for {symbol} in selected contracts.")
            return None

        # Find the contract with the delta closest to target_delta (0.30)
        closest_contract = min(symbol_contracts, key=lambda c: abs(symbol_contracts[c] - target_delta))
        # self.Debug(f"Selected contract with delta {symbol_contracts[closest_contract]:.2f} closest to target delta {target_delta} for {symbol}")
        return closest_contract