Overall Statistics
Total Trades
83
Average Win
0.61%
Average Loss
-4.44%
Compounding Annual Return
2.284%
Drawdown
70.500%
Expectancy
-0.280
Net Profit
23.245%
Sharpe Ratio
0.271
Probabilistic Sharpe Ratio
0.860%
Loss Rate
37%
Win Rate
63%
Profit-Loss Ratio
0.14
Alpha
0.019
Beta
0.981
Annual Standard Deviation
0.425
Annual Variance
0.181
Information Ratio
0.045
Tracking Error
0.374
Treynor Ratio
0.117
Total Fees
$110.00
import datetime
import math

from QuantConnect.Securities.Option import OptionPriceModels

from datetime import timedelta
from QuantConnect.Data.UniverseSelection import * 

SYMBOL = "SPY"
TARGET_CASH_UTILIZATION = 1
REBALANCE_THRESHOLD = 0.01
STARTING_CASH = 100000
TARGET_CONTRACT_DAYS_IN_FUTURE = 35
SHOULD_LOG = True

class CoveredCallAlgorithm(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2008, 1, 1)
        # self.SetEndDate(2017, 4, 1)
        self.SetCash(STARTING_CASH)
        self.TargetDelta = float(self.GetParameter("target_delta"))
        self.LimitOrderRatio = float(self.GetParameter("limit_order_ratio"))
        self.InitialSpyValue = None
        
        equity = self.AddEquity(SYMBOL, Resolution.Minute)
        option = self.AddOption(SYMBOL, Resolution.Minute)
        self.symbol = option.Symbol
        self.UniverseSettings.Resolution = Resolution.Daily

        # Look for options that are within 30 days of the target contract date.
        # Options should be at or 60 strikes above present price.
        option.SetFilter(0, 60, timedelta(max(0, TARGET_CONTRACT_DAYS_IN_FUTURE - 30)), timedelta(TARGET_CONTRACT_DAYS_IN_FUTURE + 30))
        option.PriceModel = OptionPriceModels.CrankNicolsonFD()
        
        # Give a generous warm-up to allow the options price model to initialize.
        self.SetWarmUp(TimeSpan.FromDays(10))
        
        self.SetBenchmark(equity.Symbol)
        self.LastDay = None
 
    def OnData(self, slice):
        """Processes the slice up to 1x per day."""
        if self.IsWarmingUp:
            return
        
        if self.LastDay == self.Time.day:
            return
        self.LastDay = self.Time.day
        
        self.BalancePortfolio(slice)
        self.TradeOptions(slice)
        
    def BalancePortfolio(self, slice):
        """Transacts the underlying symbol as needed to arrive at the target cash utilization.
        
        Buys or sells as necessary to keep the underlying symbol's value at TARGET_CASH_UTILIZATION
        of the total portoflio value. Does not transact unless the imbalance is greater than REBALANCE_THRESHOLD.
        """
        if SYMBOL in slice and slice[SYMBOL] is not None:
            # Get the current value and plot to main figure window.
            current_spy = slice[SYMBOL].Close
            if self.InitialSpyValue is None:
                self.InitialSpyValue = current_spy
            self.Plot("Strategy Equity", "SPY", STARTING_CASH * current_spy / self.InitialSpyValue)
            
            # Calculate how many shares should be owned to hit target cash utilization.
            value = self.Portfolio.TotalPortfolioValue
            desired_n_shares = math.floor(value / current_spy)
            actual_n_shares = self.Portfolio[SYMBOL].Quantity
            delta_shares = desired_n_shares - actual_n_shares
            
            # Transact if needed.
            if SHOULD_LOG:
                self.Log(f"Portfolio value: {value:.2f} Current spy: {current_spy:.2f} desired_n_shares: {desired_n_shares} "
                         f"actual_n_shares: {actual_n_shares} delta_shares: {delta_shares}")
            if abs(delta_shares * current_spy) > value * REBALANCE_THRESHOLD:
                self.MarketOrder(SYMBOL, delta_shares)
 
    def TradeOptions(self, slice):
        """If enough shares exist to use as collateral, sells a call and places a limit order for that call."""
            
        number_of_contracts_available = math.floor(self.Portfolio[SYMBOL].Quantity / 100)
        number_of_contracts = -sum([x.Value.Quantity for x in self.Portfolio if x.Value.Invested and x.Value.Type==SecurityType.Option])
        options_to_buy = number_of_contracts_available - number_of_contracts
        
        if options_to_buy < 1:
            return
        
        if SHOULD_LOG:
            self.Log(f"I presently own {self.Portfolio[SYMBOL].Quantity} shares of spy.")
            self.Log(f"I presently have {number_of_contracts_available} that I can hold and "
                     f"only own {number_of_contracts}. I plan to buy {options_to_buy} more")

        chain = slice.OptionChains.GetValue(self.symbol)
        contract = self.GetClosestContract(chain, self.Time + datetime.timedelta(TARGET_CONTRACT_DAYS_IN_FUTURE))
        if contract is None:
            return
        
        if SHOULD_LOG:
            self.Log(f"Selling contract: Price: {contract.AskPrice} Underlying Price: {contract.UnderlyingLastPrice:1.2f} "
                     f"Strike: {contract.Strike:1.2f} Expiry: {contract.Expiry} Delta: {contract.Greeks.Delta:1.2f}")
        self.Sell(contract.Symbol, options_to_buy)
        self.LimitOrder(contract.Symbol, options_to_buy, round(contract.AskPrice * self.LimitOrderRatio, 2))
        
    def GetClosestContract(self, chain, desired_expiration):
        """Gets the contract nearest the target delta and expiry."""
        if not chain:
            return None
            
        calls = [contract for contract in chain if contract.Right == OptionRight.Call]
        if not calls:
            return None
        
        # Calculate the option expiry date nearest the target.
        available_expirations = list({contract.Expiry for contract in calls})
        nearest_expiration = sorted(available_expirations, key=lambda expiration: abs(expiration-desired_expiration))[0]
        
        # For all contracts that match the target expiration, find the one with delta nearest target.
        calls_at_target_expiration = [contract for contract in calls if contract.Expiry == nearest_expiration]
        calls_at_target_expiration = sorted(calls_at_target_expiration, key = lambda contract: abs(contract.Greeks.Delta - self.TargetDelta))
        if not calls_at_target_expiration:
            return None
        return calls_at_target_expiration[0]
from Selection.OptionUniverseSelectionModel import OptionUniverseSelectionModel
from datetime import date, timedelta

class OptionsUniverseSelectionModel(OptionUniverseSelectionModel):
    def __init__(self, select_option_chain_symbols):
        super().__init__(timedelta(1), select_option_chain_symbols)

    def Filter(self, filter):
        ## Define options filter -- strikes +/- 3 and expiry between 0 and 180 days away
        return (filter.Strikes(-2, +2)
                      .Expiration(timedelta(0), timedelta(180)))