Hello Quant Connect Community,
I have the following python code, where I would like to backtest a covered call strategy with SPY and hold to maturity each time on a daily basis. However the option chain is very sparse and enable to sell call option everyday. Could I get some pointers/assistance why this is the case?
Report URL:
#region imports
from AlgorithmImports import *
#endregion
import datetime
import math
import json
from dataclasses import dataclass, asdict
from typing import List, Any
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 = 50000
TARGET_CONTRACT_DAYS_IN_FUTURE = 2
SHOULD_LOG = True
MIN_EXPIRY = 2
MAX_EXPIRY = 4
"""
1. Put everything into SPY
2. Trade covered call on weekly basis
3. If exercised, buy back into SPY
"""
# the time of backtesting is covered in the log
@dataclass
class EquitySummary:
total_value: float = -1
cash_value: float = -1
eq_buy_count: int = -1
eq_count: int = -1
eq_price: float = -1 # equity price
@dataclass
class OptionSummary:
op_count: int = -1
op_buy_count: int = -1
op_value_total: float = -1
op_inventory: List[Any] = None
class CoveredCallAlgorithm(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2023, 1, 1)
self.SetEndDate(2023, 8, 1)
self.SetCash(STARTING_CASH)
self.TargetDelta = float(self.GetParameter("target_delta"))
self.LimitOrderRatio = float(self.GetParameter("limit_order_ratio"))
self.InitialSpyValue = None
# Resolution.Daily, Resolution.Hour, Resolution.Minute
equity = self.AddEquity(SYMBOL, Resolution.Daily)
option = self.AddOption(SYMBOL, Resolution.Hour)
self.Log(f'option.Symbol: {str(option.Symbol)}')
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.
# timedelta(max(0, TARGET_CONTRACT_DAYS_IN_FUTURE - 30))
# timedelta(TARGET_CONTRACT_DAYS_IN_FUTURE + 30)
option.SetFilter(1, 10, timedelta(MIN_EXPIRY), timedelta(MAX_EXPIRY))
# option.PriceModel = OptionPriceModels.CrankNicolsonFD()
option.PriceModel = OptionPriceModels.BjerksundStensland()
# 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
equity_summary = self.BalancePortfolio(slice)
option_summary = self.TradeOptions(slice)
if SHOULD_LOG:
output = {}
output.update(asdict(equity_summary))
output.update(asdict(option_summary))
self.Log(json.dumps(output))
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 portfolio value. Does not transact unless the imbalance is greater than REBALANCE_THRESHOLD.
"""
equity_summary = EquitySummary()
equity_summary.eq_buy_count = 0
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
equity_summary.eq_price = current_spy
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
actual_n_shares = self.Portfolio[SYMBOL].Quantity
desired_n_shares = math.floor(value / current_spy)
equity_summary.total_value = value
equity_summary.cash_value = self.Portfolio.Cash
equity_summary.eq_count = actual_n_shares
delta_shares = desired_n_shares - actual_n_shares
# TODO isn't ie better to just calculate the cash?
# Always buy into SPY
if delta_shares > 1:
self.MarketOrder(SYMBOL, delta_shares)
equity_summary.eq_buy_count = delta_shares
return equity_summary
def TradeOptions(self, slice):
"""If enough shares exist to use as collateral, sells a call and places a limit order for that call."""
option_summary = OptionSummary()
number_of_contracts_available = math.floor(self.Portfolio[SYMBOL].Quantity / 100)
number_of_contracts = 0
option_summary.op_inventory = []
for symbol, holding in self.Portfolio.items():
if holding.Invested and holding.Type==SecurityType.Option:
contract = self.Securities[symbol]
strike_price = contract.StrikePrice
expiration_date = contract.Expiry.strftime('%Y-%m-%d')
sell_cost = holding.HoldingsCost
option_summary.op_inventory.append({
# 'id': str(symbol),
'strike_price': strike_price,
'expiration_date': expiration_date,
'sell_cost': sell_cost,
})
number_of_contracts += -holding.Quantity
options_to_buy = number_of_contracts_available - number_of_contracts
option_summary.op_count = int(number_of_contracts)
option_summary.op_buy_count = 0
if options_to_buy < 1:
return option_summary
option_summary.op_buy_count = options_to_buy
"""
if SHOULD_LOG:
self.Log(f"I presently own {self.Portfolio[SYMBOL].Quantity} shares of {SYMBOL}.")
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 option_summary
"""
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))
return option_summary
def GetClosestContract(self, chain, desired_expiration):
"""Gets the contract nearest the target delta and expiry."""
if not chain:
self.Log('empty chain')
return None
calls = [contract for contract in chain if contract.Right == OptionRight.Call]
if not calls:
self.Log('empty calls')
return None
for call in calls:
self.Log(f'Expiry: {call.Expiry} price: {call.Strike} ask:{call.AskPrice} bid:{call.BidPrice}')
# TODO be specific in the expiration and strike price
# 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]
William Maio
Here is the backtest
Mia Alissi
Based on the provided code, it seems to be a well-structured algorithm for a covered call strategy. However, without knowing the specific issues or errors 'William' is encountering, it's challenging to provide a precise solution.
Here are a few general points to consider:
Ensure that the parameters "target_delta" and "limit_order_ratio" are correctly set in the algorithm. These parameters are fetched using the
GetParameter
method but are not defined in the provided code.The
SHOULD_LOG
variable is used to control whether to log output or not. Make sure it's set toTrue
if you want to log the output.The
BalancePortfolio
method is used to balance the portfolio based on the current price of the SPY. It also plots the strategy equity. Make sure the SPY data is correctly fetched and the calculations are accurate.The
TradeOptions
method is used to trade options. It calculates the number of contracts available and the number of contracts to buy. Make sure the calculations are correct and the fetched option chain data is accurate.The
GetClosestContract
method is used to get the closest contract based on the desired expiration. Make sure the fetched option chain data is accurate and the filtering for call options is correct.Remember, it's crucial to understand the logic of the algorithm and ensure the data used in the calculations is accurate. If 'William' is encountering specific issues or errors, it would be helpful to know those details to provide a more precise solution.
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
William Maio
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
To unlock posting to the community forums please complete at least 30% of Boot Camp.
You can continue your Boot Camp training progress from the terminal. We hope to see you in the community soon!