| 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)))