| Overall Statistics |
|
Total Trades 100 Average Win 3.98% Average Loss -2.55% Compounding Annual Return -3.063% Drawdown 27.800% Expectancy -0.027 Net Profit -6.670% Sharpe Ratio -0.047 Loss Rate 62% Win Rate 38% Profit-Loss Ratio 1.56 Alpha 0.062 Beta -0.749 Annual Standard Deviation 0.186 Annual Variance 0.035 Information Ratio -0.392 Tracking Error 0.264 Treynor Ratio 0.012 Total Fees $100.00 |
import datetime
from QuantConnect.Securities.Option import OptionHolding
class StaddleStrategy(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2017, 1, 1)
self.SetEndDate(2019, 3, 21)
self.SetCash(10000)
TICKER = 'SPY'
self.underlying = self.AddEquity(TICKER, Resolution.Minute)
self.option = self.AddOption(TICKER, Resolution.Minute)
self.buy_qty = 1
self.rolling_days = 1
# Don't adjust by split and dividends, because the options strike price
# is never adjusted. I need the real price to compare with the option
# strike price
self.underlying.SetDataNormalizationMode(DataNormalizationMode.Raw)
self.option.SetFilter(-5, 5, datetime.timedelta(25), datetime.timedelta(60))
self.SetBenchmark(TICKER)
self.Schedule.On(
self.DateRules.EveryDay(TICKER),
self.TimeRules.BeforeMarketClose(TICKER, 60),
Action(self.check_sell_staddle),
)
self.Schedule.On(
self.DateRules.EveryDay(TICKER),
self.TimeRules.BeforeMarketClose(TICKER, 59),
Action(self.check_buy_staddle),
)
# Set the OnData slice. Mandatory for options.
self.slice = None
self.SetBenchmark(TICKER)
def OnData(self, slice):
self.slice = slice
def OnEndOfDay(self):
# Log leverage
# account_leverage = self.Portfolio.TotalAbsoluteHoldingsCost / self.Portfolio.TotalPortfolioValue
account_leverage = self.Portfolio.TotalHoldingsValue / self.Portfolio.TotalPortfolioValue
self.Plot("Leverage", "Leverage", account_leverage)
def check_buy_staddle(self):
if not self.Portfolio.Invested:
self.buy_staddle()
def buy_staddle(self):
call, put = self._get_staddle_option_contracts()
if put is None or call is None:
self.Log('No contract with the same Strike price')
return
self.Log('Selected contracts for Staddle: %s and %s' % (
call.Symbol.Value, put.Symbol.Value))
self.Log('Buy %d opt for each contract' % self.buy_qty)
self.MarketOrder(call.Symbol, self.buy_qty)
self.MarketOrder(put.Symbol, self.buy_qty)
def check_sell_staddle(self):
# Get all options in portfolio
all_options = list(filter(
lambda opt: isinstance(opt, OptionHolding),
self.Portfolio.Values)
)
holding_options = list(filter(lambda opt: opt.Quantity > 0,
all_options))
# Only touch my options, and not other asset of the portfolio
holding_options = list(filter(
lambda opt: opt.Symbol.Underlying == self.option.Underlying.Symbol,
holding_options
))
if len(holding_options) == 0:
# No contract to sell
return
if len(holding_options) != 2:
raise Exception('Expected to have 2 different options, but have %d' %
len(holding_options))
opt1 = holding_options[0]
opt2 = holding_options[1]
days_to_expire = (opt1.Security.Expiry - self.Time).days
if days_to_expire <= self.rolling_days:
self.Log('Options %s and %s expires in %d days' % (
opt1.Symbol.Value, opt2.Symbol.Value, days_to_expire))
self.sell_staddle([opt1, opt2])
# else:
# self.Log('X-Options %s and %s expires in %d days' % (
# opt1.Symbol.Value, opt2.Symbol.Value, days_to_expire))
def sell_staddle(self, options):
for opt in options:
self.MarketOrder(opt.Symbol, -1 * opt.Quantity)
def _get_staddle_option_contracts(self):
if self.slice is None:
self.Log('No slice. This should be a QuantConnect isssue')
return None, None
if self.slice.OptionChains is None:
self.Log('No self.slice.OptionChains. This should be a QuantConnect isssue')
return None, None
underlying_chains = list(filter(
lambda chain: chain.Key.Underlying == self.option.Underlying.Symbol,
self.slice.OptionChains,
))
if len(underlying_chains) == 0:
self.Log('No option contract for underlying %s.' %
self.option.Underlying.Symbol.Value)
return None, None
underlying_chain = underlying_chains[0].Value
# Filter options without volume
opt_volume = list(filter(
lambda opt: opt.Volume > 0,
underlying_chain,
))
# Get the puts
# Keep only the puts
puts = list(filter(
lambda opt: opt.Right == OptionRight.Put,
underlying_chain,
))
# Keep only the calls
calls = list(filter(
lambda opt: opt.Right == OptionRight.Call,
underlying_chain,
))
put_symbols_str = ', '.join([opt.Symbol.Value for opt in puts])
call_symbols_str = ', '.join([opt.Symbol.Value for opt in calls])
self.Log('calls available=%s' % call_symbols_str)
self.Log('puts available=%s' % put_symbols_str)
# Sort the puts by distance against the underlying price. I sort by
# puts because they use to be the contracts with lower volume
underlying_price = self.option.Underlying.Price
priority_puts = sorted(
puts,
key=lambda opt: abs(opt.Strike - underlying_price),
reverse=False,
)
priority_puts_str = ', '.join([opt.Symbol.Value for opt in priority_puts])
self.Log('underlying price=%f' % underlying_price)
self.Log('priority puts: %s' % priority_puts_str)
put, call = self._get_pair_contracts(priority_puts, calls)
return call, put
def _get_pair_contracts(self, list_one, list_two):
'''Find two contracts on different list with the same Strike price'''
for contract_one in list_one:
for contract_two in list_two:
if (contract_one.Strike == contract_two.Strike and
contract_one.Expiry == contract_two.Expiry):
return contract_one, contract_two
# No contracts with same strike were found
return None, None