Overall Statistics
# 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
}