Overall Statistics
Total Trades
59
Average Win
0.84%
Average Loss
-1.92%
Compounding Annual Return
-0.947%
Drawdown
7.900%
Expectancy
0.008
Net Profit
-0.944%
Sharpe Ratio
-0.107
Loss Rate
30%
Win Rate
70%
Profit-Loss Ratio
0.44
Alpha
0.045
Beta
-2.928
Annual Standard Deviation
0.061
Annual Variance
0.004
Information Ratio
-0.397
Tracking Error
0.061
Treynor Ratio
0.002
Total Fees
$57.00
"""
https://www.quantconnect.com/forum/discussion/3245/using-option-greeks-to-select-option-contracts-to-trade

if you need Greeks:
 A) Filter and B) AddOption 
    more efficient than 
 C) OptionChainProvider and D) AddOptionContract
"""

from QuantConnect.Securities.Option import OptionPriceModels
from datetime import timedelta
import decimal as d
from my_calendar import last_trading_day
# from datetime import datetime
import datetime

class DeltaHedgedStraddleAlgo(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2017, 1, 1)
        self.SetEndDate(2017, 12, 31)
        self.SetCash(1000000)
        
        self.Log("PERIOD: 2017")
        
        # ----------------------------------------------------------------------
        # Algo params
        # ----------------------------------------------------------------------
        # self.PREMIUM = 0.01  # percentage of SPY share price
        self.MAX_EXPIRY = 30 # max num of days to expiration => for uni selection
        self._no_K = 20       # no of strikes around ATM => for uni selection
        self.resol = Resolution.Minute  # Resolution.Minute .Hour  .Daily
        self.tkr = "GOOG"  # "SPY", "GOOG", ...
        self.Lev = d.Decimal(1.0)
        # self.last_trading_day = EndData
        self.last_trading_day = datetime.date(2020,1,1)
        # self.Ntnl_perc = d.Decimal( round( 1. / (2. * self.MAX_EXPIRY/7.), 2) ) #  notional percentage, e.g. 0.08
        self.select_flag, self.hedge_flag = False, False
        self.previous_delta, self.delta_treshold = d.Decimal(0.0), d.Decimal(0.05) 
        self.profit_target = d.Decimal(0.1)
        self.enter_trade_allowed = True
        # ----------------------------------------------------------------------

        # add underlying Equity 
        equity = self.AddEquity(self.tkr, self.resol)  
        equity.SetDataNormalizationMode(DataNormalizationMode.Raw) # IMPORTANT: default
        self.equity_symbol = equity.Symbol
        
        # Add options
        option = self.AddOption(self.tkr, self.resol)
        self.option_symbol = option.Symbol

        # set our strike/expiry filter for this option chain
        option.SetFilter(-5, +5, timedelta(0), timedelta(30))

        # for greeks and pricer (needs some warmup) - https://github.com/QuantConnect/Lean/blob/21cd972e99f70f007ce689bdaeeafe3cb4ea9c77/Common/Securities/Option/OptionPriceModels.cs#L81
        option.PriceModel = OptionPriceModels.CrankNicolsonFD()  # both European & American, automatically
        # this is needed for Greeks calcs
        self.SetWarmUp(TimeSpan.FromDays(3))    # timedelta(7)

        self._assignedOption = False
        self.call, self.put = None, None
        
        # -----------------------------------------------------------------------------
        # scheduled functions
        # -----------------------------------------------------------------------------

        self.Schedule.On(self.DateRules.EveryDay(self.equity_symbol), 
                         self.TimeRules.BeforeMarketClose(self.equity_symbol, 10),      
                         Action(self.close_options))

    def check_n_days_to_expiry(self, last_trading_day, today_date):
        current_date = datetime.strptime(today_date, '%Y-%m-%d').date()
        last_trading_date = datetime.strptime(last_trading_day, '%Y-%m-%d').date()
        if (last_trading_date-current_date < 5):
            return True
        else:
            return False

    def close_options(self):
        """ Liquidate opts (with some value) and underlying
        """
        if self.total_premium == 0.0:
            # self.Log("Close_options: Total premium is 0")
            return
        if (self.Portfolio.Invested is False):
            return
        today_date = self.Time.date()
        # self.Log("Today date: " + str(today_date))
        # self.Log("Last trading date: " + str(last_trading_day))
        # exit_cond = self.check_n_days_to_expiry(self.last_trading_day, today_date)
        # check this is the last trading day
        if self.last_trading_day != today_date:
            # self.Log("Close_options:Not the last trading day")
            total_ask_premium = d.Decimal(0)
            # total_ask_premium = 0.0
            
            for x in self.Portfolio:  # symbol = x.Key; security = x.Value ## but also symbol = x.Value.Symbol
                if x.Value.Invested:  # self.Portfolio[opt].Invested, but no need for
                    # self.Log("Close_options:x.Key: " + str(x.Key))
                    # self.Securities.ContainsKey(opt)
                    # only liquidate valuable options, otherwise let them quietly expiry
                    # total_ask_premium = total_ask_premium + float(self.Securities[x.Key].AskPrice)
                    ask_premium = d.Decimal(self.qnty) * self.Securities[x.Key].AskPrice
                    # self.Log("Close_options:ask_premium: "  + str(ask_premium))
                    total_ask_premium = total_ask_premium + ask_premium
                    # self.Log("Close_options:total_ask_premium: "  + str(total_ask_premium))
                    
            # self.Log("Close_options:Total premium = " + str(self.total_premium))
            if (total_ask_premium) < self.profit_target * self.total_premium:
                self.Log("Close_options:Total premium = " + str(self.total_premium))
                self.Log("Close_options:total_ask_premium: "  + str(total_ask_premium))
                for x in self.Portfolio:  # symbol = x.Key; security = x.Value ## but also symbol = x.Value.Symbol
                    if x.Value.Invested:  # self.Portfolio[opt].Invested, but no need for
                        self.Log("Close_options: liquidating: " + str(x.Key) )
                        self.Liquidate(x.Key)
                        
                # self.Log("Close_options:Returning after liquidating " )
                self.total_premium = 0.0
                return
    
        else:
            self.enter_trade_allowed = True
            self.Log("Close_options:On last trading day: liquidate options with value and underlying ")
    
            # liquidate options (if invested and in the money [otherwise their price is min of $0.01)
            for x in self.Portfolio:  # symbol = x.Key; security = x.Value ## but also symbol = x.Value.Symbol
                if x.Value.Invested:  # self.Portfolio[opt].Invested, but no need for
                    # self.Securities.ContainsKey(opt)
                    # only liquidate valuable options, otherwise let them quietly expiry
                    if self.Securities[x.Key].AskPrice > 0.05: self.Liquidate(x.Key)
    
            # CHECK if this necessary (incorporated above)
            if self.Portfolio[self.equity_symbol].Invested:
                self.Liquidate(self.equity.Symbol)

            
    def OnData(self, slice):

        if self.IsWarmingUp: return

        # 1. deal with any early assignments
        if self._assignedOption:
            # close everything
            for x in self.Portfolio:  
                if x.Value.Invested: self.Liquidate(x.Key)
            self._assignedOption = False
        #   self.call, self.put = None, None  # stop getting Greeks

        today_date = self.Time.date()
        if today_date > self.last_trading_day:
            self.enter_trade_allowed = True
        # if self.Portfolio.Invested:
        #     self.Log("Invested: get contract")
        #     # self.get_contracts_at_strike(slice)
        #     if (not self.call) or (not self.put): return
            
        # 2. sell options, if none
        if not self.Portfolio.Invested and self.enter_trade_allowed:
            # select contract
            # self.Log("get contract")
            self.get_contracts(slice)
            # self.Log("got  the contracts contract")
            if (not self.call) or (not self.put): return
            # self.Log("getting greeks")
            # self.get_greeks(slice)
            self.Log("OnData: Selected Call at strike:" + str(self.call.Strike) + " Delta:"
                    + str(self.call.Greeks.Delta))
            self.Log("OnData:Selected Put at strike:" + str(self.put.Strike) + " Delta:"
                    + str(self.put.Greeks.Delta))
            # trade
            unit_price =  self.Securities[self.equity_symbol].Price * d.Decimal(100.0)   # share price x 100
            self.Log("Price:" + str(self.Securities[self.equity_symbol].Price))
            self.Log("unit_price:" + str(unit_price))
            self.Log("Portfolio Value:" + str(self.Portfolio.TotalPortfolioValue))
            qnty = int(self.Portfolio.TotalPortfolioValue / unit_price)   
            # call_exists, put_exists = self.call is not None, self.put is not None
            self.Log("Qnty:" + str(qnty))
            self.qnty = qnty
            self.Log("Call Ask Price:" + str(self.call.AskPrice))
            self.Log("Put Ask Price:" + str(self.put.AskPrice))
            call_premium = qnty * self.call.AskPrice
            put_premium = qnty * self.put.AskPrice
            self.total_premium = call_premium + put_premium
            self.Log("Total premium = " + str(self.total_premium))
            self.enter_trade_allowed = False
            if self.call is not None: self.Sell(self.call.Symbol, qnty)  # self.MarketOrder(self.call.Symbol, -qnty)
            if self.put is not None:  self.MarketOrder(self.put.Symbol, -qnty)
            
            
        

    def get_contracts(self, slice):
        """
        Get call and put
        """
        # self.Log("Entered get_contracts")
        for kvp in slice.OptionChains:
            # self.Log("getting the key")
            if kvp.Key != self.option_symbol: continue
            # self.Log("got the key")
            chain = kvp.Value   # option contracts for each 'subscribed' symbol/key 
            
            spot_price = chain.Underlying.Price
            self.Log("spot_price {}" .format(spot_price))

            # prefer to do in steps, rather than a nested sorted
            
            # 1. get furthest expiry            
            contracts_by_T = sorted(chain, key = lambda x: x.Expiry, reverse = True)
            if not contracts_by_T: return
            self.expiry = contracts_by_T[0].Expiry.date() # furthest expiry 
            self.last_trading_day = last_trading_day(self.expiry)
            
            # get contracts with further expiry and sort them by strike
            slice_T = [i for i in chain if i.Expiry.date() == self.expiry]
            sorted_contracts = sorted(slice_T, key = lambda x: x.Strike, reverse = False)

            self.Log("Expiry used: {} and shortest {}" .format(self.expiry, contracts_by_T[-1].Expiry.date()) )

            calls = [i for i in sorted_contracts \
                     if i.Right == OptionRight.Call and i.Strike >= spot_price]
            deltas = [i.Greeks.Delta for i in calls]
            strikes = [i.Strike for i in calls]
            # self.Log("Call Strike: {}" .format(strikes))
            # self.Log("Call Delta: {}" .format(deltas))
            
            puts = [i for i in sorted_contracts \
                     if i.Right == OptionRight.Put and i.Strike <= spot_price]
            deltas = [i.Greeks.Delta for i in puts]
            strikes = [i.Strike for i in puts]
            # self.Log("Puts Strike: {}" .format(strikes))
            # self.Log("Puts Delta: {}" .format(deltas))
            
            for i in calls:
                if i.Greeks.Delta < 0.4:
                    self.call = i
                    self.Log("Selected Call at strike:" + str(self.call.Strike) + " Delta:"
                    + str(self.call.Greeks.Delta))
                    break
                
            # puts = sorted(puts, key=puts.Strike, reverse=False)
            for i in reversed(puts):    
                if i.Greeks.Delta >- 0.4:
                    self.put = i
                    self.Log("Selected Put at strike:" + str(self.put.Strike) + " Delta:"
                    + str(self.put.Greeks.Delta))
                    break
                
            # self.call = calls[0] if calls else None
            # 2a. get the ATM closest CALL to short
            # calls = [i for i in sorted_contracts \
            #          if i.Right == OptionRight.Call and i.Strike >= spot_price]
            # self.call = calls[0] if calls else None
            # self.Log("delta call {}, self.call type {}" .format(self.call.Greeks.Delta, type(self.call)))
            # self.Log("implied vol {} " .format(self.call.ImpliedVolatility))

            # 2b. get the ATM closest put to short
            # puts = [i for i in sorted_contracts \
            #          if i.Right == OptionRight.Put and i.Strike <= spot_price]
            # self.put = puts[-1] if puts else None


    def get_greeks(self, slice):
        """
        Get greeks for invested option: self.call and self.put
        """

        if (self.call is None) or (self.put is None): return
        
        for kvp in slice.OptionChains:
            if kvp.Key != self.option_symbol: continue
            chain = kvp.Value   # option contracts for each 'subscribed' symbol/key 
            traded_contracts = filter(lambda x: x.Symbol == self.call.Symbol or 
                                         x.Symbol == self.put.Symbol, chain)
            if not traded_contracts: self.Log("No traded cointracts"); return
        
            deltas = [i.Greeks.Delta for i in traded_contracts]
            strikes = [i.Strike for i in traded_contracts]
            self.Log("Strike: {}" .format(strikes))
            self.Log("Delta: {}" .format(deltas))
            
            # self.Delta=sum(deltas)
            # self.Log("Vega: " + str([i.Greeks.Vega for i in traded_contracts]))
            # self.Log("Gamma: " + str([i.Greeks.Gamma for i in traded_contracts]))

       
    def UniverseFunc(self, universe):
        return universe.IncludeWeeklys()\
                    .Strikes(-self._no_K, self._no_K)\
                    .Expiration(timedelta(1), timedelta(self.MAX_EXPIRY))
    
    
    # ----------------------------------------------------------------------
    # Other ancillary fncts
    # ----------------------------------------------------------------------   
    def OnOrderEvent(self, orderEvent):
    #   self.Log("Order Event -> {}" .format(orderEvent))
        pass

    def OnAssignmentOrderEvent(self, assignmentEvent):
        self.Log(str(assignmentEvent))
        self._assignedOption = True
    #   if self.isMarketOpen(self.equity_symbol):
    #       self.Liquidate(self.equity_symbol)
    
    def TimeIs(self, day, hour, minute):
        return self.Time.day == day and self.Time.hour == hour and self.Time.minute == minute
    
    def HourMinuteIs(self, hour, minute):
        return self.Time.hour == hour and self.Time.minute == minute
    
    # ----------------------------------------------------------------------
        # all_symbols = [ x.Value for x in self.Portfolio.Keys ]
        # all_invested = [x.Symbol.Value for x in self.Portfolio.Values if x.Invested ]
        # for kvp in self.Securities: symbol = kvp.Key; security = kvp.Value
        #
        # orders = self.Transactions.GetOrders(None)
        # for order in orders: self.Log("order symbol {}" .format(order.Symbol))
        #
        # volatility = self.Securities[self.equity_symbol].VolatilityModel.Volatility
        # self.Log("Volatility: {}" .format(volatility))
        # set our strike/expiry filter for this option chain                        
# ------------------------------------------------------------------------------
# Business days
# ------------------------------------------------------------------------------
from datetime import timedelta #, date
from pandas.tseries.holiday import (AbstractHolidayCalendar,    # inherit from this to create your calendar
                                    Holiday, nearest_workday,   # to custom some holidays
                                    #
                                    USMartinLutherKingJr,       # already defined holidays
                                    USPresidentsDay,            # "     "   "   "   "   "
                                    GoodFriday,
                                    USMemorialDay,              # "     "   "   "   "   "
                                    USLaborDay,
                                    USThanksgivingDay           # "     "   "   "   "   "
                                    )


class USTradingCalendar(AbstractHolidayCalendar):
    rules = [
      Holiday('NewYearsDay', month=1, day=1, observance=nearest_workday),
      USMartinLutherKingJr,
      USPresidentsDay,
      GoodFriday,
      USMemorialDay,
      Holiday('USIndependenceDay', month=7, day=4, observance=nearest_workday),
      USLaborDay,
      USThanksgivingDay,
      Holiday('Christmas', month=12, day=25, observance=nearest_workday)
    ]

# TODO: to be tested
def last_trading_day(expiry):
    # American options cease trading on the third Friday, at the close of business 
    # - Weekly options expire the same day as their last trading day, which will usually be a Friday (PM-settled), [or Mondays? & Wednesdays?]
    # 
    # SPX cash index options (and other cash index options) expire on the Saturday following the third Friday of the expiration month. 
    # However, the last trading day is the Thursday before that third Friday. Settlement price Friday morning opening (AM-settled).
    # http://www.daytradingbias.com/?p=84847
    
    dd = expiry     # option.ID.Date.date()
    
    # if expiry on a Saturday (standard options), then last trading day is 1d earlier 
    if dd.weekday() == 5:
        dd -= timedelta(days=1)   # dd -= 1 * BDay()
        
    # check that Friday is not an holiday (e.g. Good Friday) and loop back
    while USTradingCalendar().holidays(dd, dd).tolist():    # if list empty (dd is not an holiday) -> False
        dd -= timedelta(days=1) 
        
    return dd