| Overall Statistics |
|
Total Trades 6 Average Win 0% Average Loss -0.28% Compounding Annual Return -35.562% Drawdown 78.800% Expectancy -1 Net Profit -3.548% Sharpe Ratio -2.839 Probabilistic Sharpe Ratio 8.238% Loss Rate 100% Win Rate 0% Profit-Loss Ratio 0 Alpha -0.027 Beta 0.908 Annual Standard Deviation 0.102 Annual Variance 0.01 Information Ratio -0.04 Tracking Error 0.016 Treynor Ratio -0.319 Total Fees $999.94 |
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from clr import AddReference
AddReference("System")
AddReference("QuantConnect.Algorithm")
AddReference("QuantConnect.Common")
from System import *
from QuantConnect import *
from QuantConnect.Data import *
from QuantConnect.Algorithm import *
from datetime import datetime, timedelta
from order_codes import (OrderTypeCodes, OrderDirectionCodes, OrderStatusCodes)
from QuantConnect.Data.Custom.CBOE import CBOE
from QuantConnect.Securities.Option import OptionPriceModels
### <summary>
### The strategy is as follows: 99.5% of the portfolio is allocated to SPY ETF and 0.5% is spent every month buying 2-month put options that are about 30% out-of-the-money. After one month, those put options are sold and new ones bought according to the same methodology.
### Buy ratios {<15: 1.0, <20: 0.5, <30: 0.25, >30: 0.1}
### 2 mo out puts 30% otm
### rolls every 30 days before expiration
### limit sells: 30x - 25%, 50x - 37.5% , 30x - 37.5%
### </summary>
### <meta name="tag" content="regression test" />
### <meta name="tag" content="options" />
class OptionExerciseAssignRegressionAlgorithm(QCAlgorithm):
def Initialize(self):
self.SetCash(1000000)
#Note: options data were added on 2009-01-27
'''
self.SetStartDate(2015,7,23)
self.SetEndDate(2015,8,20)
'''
self.SetStartDate(2015,7,22)
self.SetEndDate(2015,8,20)
self._equity_index_symbol = 'SPY'
self._equity_index_allocation = 0.995
self._equity_index = self.AddEquity(self._equity_index_symbol, Resolution.Daily)
self._equity_index.SetDataNormalizationMode(DataNormalizationMode.Raw)
self._equity_option = self.AddOption(self._equity_index_symbol)
self._num_periods_per_year = 1
self._equity_option_allocation = (1-self._equity_index_allocation) / self._num_periods_per_year
# set our strike/expiry filter for this option chain
#self._equity_option.SetFilter(-5, 0, timedelta(1), timedelta(10))
self._equity_option.SetFilter(self.UniverseFunc)
self._equity_option.PriceModel = OptionPriceModels.CrankNicolsonFD() # Pricing model to get Implied Volatility
self._equity_option_duration = 60 #duration of put contracts
self._equity_option_duration_min = round(self._equity_option_duration * 0.9)
self._equity_option_duration_max = round(self._equity_option_duration * 1.1)
self._equity_option_roll_period = round(self._equity_option_duration / 2) #number of days before expiration at which to sell options (roll period)
self._cboeVix = self.AddData(CBOE, "VIX").Symbol
self.vix = 'CBOE/VIX'
# Add Quandl VIX price (daily)
vix_symbol = self.AddData(QuandlVix, self.vix, Resolution.Daily)
self.SetBenchmark(self._equity_index_symbol )
#self.option_invested = False
self._initial_capital = self.Portfolio.TotalPortfolioValue
self._otm_pct = 0.3 # percentage for out of money options.
self._profit_pct_threshold1 = 30.0 #when profit is 30x
self._profit_pct_threshold2 = 50.0 #when profit is
self._profit_pct_threshold3 = 30.0 #when profit is
self._threshold1_achieved = False
self._threshold2_achieved = False
self._threshold3_achieved = False
self._max_option_drawdown_pct = 0.2 #used for option stop losses
# set our strike/expiry filter for this option chain
def UniverseFunc(self, universe):
return universe.Strikes(-200, 0).Expiration(timedelta(self._equity_option_duration_min), timedelta(self._equity_option_duration_max)) #
#return universe.Strikes(-100, 0).Expiration(timedelta(300/self._num_periods_per_year), timedelta(400/self._num_periods_per_year)) #
def OnData(self, slice):
if not self.Portfolio[self._equity_index_symbol].Invested:
self.SetHoldings(self._equity_index_symbol, self._equity_index_allocation)
self._equity_index_initial_price = self.Securities[self._equity_index_symbol].Open
#self.Debug('OnData: buy initial SPY allocation of {:.1%}, SPY open price={}'\
#.format(self._equity_index_allocation, self._equity_index_initial_price ))
#self.Debug('OnData: Time=%s' % self.Time)
if (self.Time.hour == 10 and self.Time.minute == 0):
#self.Debug('OnData: buy time=%s' % self.Time)
#update benchmark at 10am only
self.UpdateBenchmark()
self.Sell(slice)
self.Buy(slice)
'''
elif (self.Time.hour == 15 and self.Time.minute == 30):
#self.Debug('OnData: sell time=%s' % self.Time)
self.Sell(slice)
return
'''
def UpdateBenchmark(self):
index_open_price = self.Securities[self._equity_index_symbol].Close
scale_factor = index_open_price / self._equity_index_initial_price
benchmark = self._initial_capital * scale_factor
# make our plots
self.Plot("Strategy vs Benchmark", "Portfolio Value", self.Portfolio.TotalPortfolioValue)
self.Plot("Strategy vs Benchmark", "Benchmark", benchmark)
#self.Debug("UpdateBenchmark: index_open_price={}, scale_factor={:.3f}, pv={}, benchmark={}"\
#.format(index_open_price, scale_factor, self.Portfolio.TotalPortfolioValue, benchmark))
def Buy(self, slice):
#self.Debug('Buy: option_invested=%s' % self.option_invested)
options = [x for x in self.Portfolio if x.Value.Invested and x.Value.Type==SecurityType.Option]
#if already have options, do not buy anymore
if len(options) > 0:
return
for kvp in slice.OptionChains:
chain = kvp.Value
# filter put options
contracts = filter(lambda x:
#x.Expiry.date() == self.Time.date() and
#x.Strike < chain.Underlying.Price and
x.Right == OptionRight.Put, chain)
# sorted the contracts by their strikes, find the first strike under market price
sorted_contracts = sorted(contracts, key = lambda x: x.Strike, reverse = False)
self.Debug('Buy: sorted_contracts=%s' % len(sorted_contracts))
if not len(sorted_contracts) > 0:
return
#rebalance SPY etf if needed before buying puts
self.SetHoldings(self._equity_index_symbol, self._equity_index_allocation)
#find puts with the strike closest to otm_pct
closest = sorted(sorted_contracts, key = lambda x: abs(1-x.Strike/x.UnderlyingLastPrice-self._otm_pct))
option = closest[0]
option_symbol = option.Symbol
underlying_price = option.UnderlyingLastPrice
strike = option.Strike
otm_pct = 1 - strike/underlying_price
iv = option.ImpliedVolatility
vix = self.Securities[self.vix].Price
'''
if (iv < 0.2):
r = 1.0
elif iv < 0.4:
r = 0.5
elif iv < 0.6:
r = 0.25
else:
r = 0.1
'''
if (vix < 15):
r = 1.0
elif vix < 20:
r = 0.5
elif vix < 30:
r = 0.25
else:
r = 0.1
y = self.Time.date().year
#proxy for CAPE ratio
if y < 2010:
c = 0.25
elif y < 2012:
c = 0.5
else:
c = 1.0
quantity = self.CalculateOrderQuantity(option_symbol, self._equity_option_allocation * r * c)
days_to_expiration = (option.Expiry.date() - self.Time.date()).days
'''
if not slice.ContainsKey(self._cboeVix):
vix_close = -1
else:
vix = slice.Get(CBOE, self._cboeVix)
vix_close = vix.Close
'''
self.Debug("Buy: {} puts {}. K={}, S0={:.1f}, otm_pct={:.1%}, IV={:.0%}, r={:.1f}, c={:.1f}, days={}, vix={:.0f}"\
.format(quantity, option_symbol, strike, underlying_price, otm_pct, iv, r, c, days_to_expiration, vix))
self.MarketOrder(option_symbol, quantity, self.Time, "otm={:.0%}, IV={:.0%}, r={:.1f}, c={:.1f}, vix={:.0f}, K={}, S0={}"\
.format(otm_pct, iv, r, c, vix, strike, underlying_price)) #set BuyMarketOn order at 0:00:00 time (UTC?)
#self.option_invested = True
#reset threshold sale achievements
self._threshold1_achieved = False
self._threshold2_achieved = False
self._threshold3_achieved = False
self._equity_option_symbol = option_symbol
pv = self.Portfolio.TotalPortfolioValue
self.Debug("Buy: alloc: SPY={:.1%}, puts={:.2%}, cash={:.2%}".format(
self.Portfolio[self._equity_index_symbol].HoldingsValue/pv, self.Portfolio[self._equity_option_symbol].HoldingsValue/pv, self.Portfolio.Cash/pv))
def Sell(self, slice):
self.Debug('Sell: time={}'.format(self.Time))
#if any options are expiring tomorrow, sell them
options = [x for x in self.Portfolio if x.Value.Invested and x.Value.Type==SecurityType.Option]
if len(options) == 0:
return
vix = self.Securities[self.vix].Price
#calculate profit pct and set stop loss if crosses 5x return
option = options[0].Value
profit_pct = option.UnrealizedProfitPercent
days_to_expiration = -1 #(option.Expiry.date() - self.Time.date()).days
self.Debug('Sell: option: price={}, cost={}, profit={}, profit_pct={:.0%}, vix={:.0f}'.\
format(option.Price, option.AveragePrice, option.UnrealizedProfit, profit_pct, vix))
'''
if profit_pct >= self._profit_pct_threshold:
self.Debug('Sell: profit_pct={:.0%} >= self._profit_pct_threshold={:.0%}'.format(profit_pct, self._profit_pct_threshold))
#only set stop loss order if option price increase as compared to the previous
#simulates trailing stop loss order mechanism
if hasattr(self, '_prev_profit_pct') and profit_pct > self._prev_profit_pct:
#cancel any previous stop loss orders
self.Transactions.CancelOpenOrders(option.Symbol)
#set new stop loss
price = option.Price * 1.05
price2 = option.Price * 2.05
qty = option.Quantity/2
self.LimitOrder(option.Symbol, -qty, price)
self.LimitOrder(option.Symbol, -qty, price2)
self.Debug('Sell: set L: qty={}, price={}, symbol={}'.\
format(qty, price, option.Symbol))
self.Debug('Sell: set L: qty={}, price2={}, symbol={}'.\
format(qty, price2, option.Symbol))
self._prev_profit_pct = profit_pct #update previous profit percentage
'''
if profit_pct >= self._profit_pct_threshold1 and not self._threshold1_achieved:
self.Debug('Sell: profit_pct={:.0%} >= self._profit_pct_threshold1={:.0%}'.format(profit_pct, self._profit_pct_threshold1))
qty = option.Quantity/4
self.MarketOrder(option.Symbol, -qty, self.Time, 'profit={:.0%}, threshold={:.0%}'.format(profit_pct, self._profit_pct_threshold1))
self.Debug('Sell: T1: qty={}, price={}, threshold={}, vix={:.0f}'.\
format(qty, option.Price, self._profit_pct_threshold1, vix))
self._threshold1_achieved = True
return
if profit_pct >= self._profit_pct_threshold2 and not self._threshold2_achieved:
self.Debug('Sell: profit_pct={:.0%} >= self._profit_pct_threshold2={:.0%}'.format(profit_pct, self._profit_pct_threshold2))
qty = option.Quantity/2
self.MarketOrder(option.Symbol, -qty, self.Time, 'profit={:.0%}, threshold={:.0%}'.format(profit_pct, self._profit_pct_threshold2))
self.Debug('Sell: T2: qty={}, price={}, threshold={}, vix={:.0f}'.\
format(qty, option.Price, self._profit_pct_threshold2, vix))
self._threshold2_achieved = True
return
if profit_pct >= self._profit_pct_threshold3 and not self._threshold3_achieved:
self.Debug('Sell: profit_pct={:.0%} >= self._profit_pct_threshold3={:.0%}'.format(profit_pct, self._profit_pct_threshold3))
qty = option.Quantity
self.MarketOrder(option.Symbol, -qty, self.Time, 'profit={:.0%}, threshold={:.0%}'.format(profit_pct, self._profit_pct_threshold3))
self.Debug('Sell: T3: qty={}, price={}, threshold={}, vix={:.0f}'.\
format(qty, option.Price, self._profit_pct_threshold3, vix))
self._threshold3_achieved = True
return
#not applicable anymore: add 1 day because of a bug in early years e.g. 2009 when options with expiration date t are exercised at day t-1
sell_date = self.Time.date() + timedelta(days=self._equity_option_roll_period)
expiring_options = [x for x in options if x.Key.ID.Date.date() <= sell_date]
#self.Debug('Sell: len(options)=%d, len(expiring_options)=%d' % (len(options), len(expiring_options)))
#if len(options) > 0:
# self.Debug('Sell: options in portfolio: options[0]=%s' % (options[0].Value.Symbol))
if len(expiring_options) > 0:
expiring_option = expiring_options[0].Value
symbol = expiring_option.Symbol
qty = expiring_option.Quantity
profit_pct = expiring_option.UnrealizedProfitPercent
self.Debug('Sell: expiring_option: symbol=%s, qty=%d' % (symbol, qty))
self.MarketOrder(symbol, -qty, self.Time, '{} day roll. profit={:.0%}, vix={:.0f}'.format(self._equity_option_roll_period, profit_pct, vix))
def OnOrderEvent(self, orderEvent):
self.Log(str(orderEvent))
#self.Debug('OnOrderEvent: ' + str(orderEvent))
#order = self.Transactions.GetOrderById(orderEvent.OrderId)
#self.Debug('order.Symbol=%s' % str(order.Symbol))
#for option exercise order type is 6, for stocks it is - see order_codes.py
#self.Debug('order.Type=%s' % order.Type)
#if order.Status == list(OrderStatusCodes.keys())[list(OrderStatusCodes.values()).index('Filled')] \
#and OrderTypeCodes[order.Type] == 'OptionExercise':
# self.option_invested = False
def OnAssignmentOrderEvent(self, assignmentEvent):
self.Log(str(assignmentEvent))
self.Debug('OnAssignmentOrderEvent: ' + str(assignmentEvent))
class QuandlVix(PythonQuandl):
'''
This class is used to get the Vix Close price from Quandl data.
'''
def __init__(self):
self.ValueColumnName = "vix Close""""
This file contains QuantConnect order codes for easy conversion and more
intuitive custom order handling
References:
https://github.com/QuantConnect/Lean/blob/master/Common/Orders/OrderTypes.cs
https://github.com/QuantConnect/Lean/blob/master/Common/Orders/OrderRequestStatus.cs
"""
OrderTypeKeys = [
'Market', 'Limit', 'StopMarket', 'StopLimit', 'MarketOnOpen',
'MarketOnClose', 'OptionExercise',
]
OrderTypeCodes = dict(zip(range(len(OrderTypeKeys)), OrderTypeKeys))
OrderDirectionKeys = ['Buy', 'Sell', 'Hold']
OrderDirectionCodes = dict(zip(range(len(OrderDirectionKeys)), OrderDirectionKeys))
## NOTE ORDERSTATUS IS NOT IN SIMPLE NUMERICAL ORDER
OrderStatusCodes = {
0:'New', # new order pre-submission to the order processor
1:'Submitted', # order submitted to the market
2:'PartiallyFilled', # partially filled, in market order
3:'Filled', # completed, filled, in market order
5:'Canceled', # order cancelled before filled
6:'None', # no order state yet
7:'Invalid', # order invalidated before it hit the market (e.g. insufficient capital)
8:'CancelPending', # order waiting for confirmation of cancellation
}