Overall Statistics
Total Trades
6
Average Win
8.37%
Average Loss
0%
Compounding Annual Return
17.211%
Drawdown
28.300%
Expectancy
0
Net Profit
17.211%
Sharpe Ratio
0.798
Probabilistic Sharpe Ratio
39.019%
Loss Rate
0%
Win Rate
100%
Profit-Loss Ratio
0
Alpha
-0.06
Beta
0.894
Annual Standard Deviation
0.255
Annual Variance
0.065
Information Ratio
-0.88
Tracking Error
0.104
Treynor Ratio
0.227
Total Fees
$132.75
Estimated Strategy Capacity
$260000000.00
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 *
import numpy as np
from datetime import timedelta

from QuantConnect.Securities.Option import OptionPriceModels
import pandas as pd
import numpy as np

class BasicTemplateIndexOptionsAlgorithm(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2009, 1, 1)
        self.SetEndDate(2009, 12, 31)
        self.initial_cash = 1000000
        self.SetCash(self.initial_cash)
        self.benchmark_shares = None

        # Set securities and universe
        self.spy = self.AddEquity('SPY', Resolution.Minute).Symbol
        self.Securities['SPY'].SetDataNormalizationMode(DataNormalizationMode.Raw);
        self.spx = self.AddIndex('SPX', Resolution.Minute).Symbol
        self.underlying = self.spy

        # Set parameters
        self.spy_percentage = 1.0 # % invested in SPY
        self.max_leverage = 1.05
        self.min_leverage = 0.95
        self.target_expiry = 365
        self.multiplier = 100 # 100 for SPY and 1000 for SPX since SPX is 10 times largest

        # Set helper amounts
        self.opts_to_trade = pd.Series()
        self.opts_to_sell = pd.Series()
        self.opt_invested = False

        # Use SPY as the benchmark
        self.SetBenchmark('SPY')
        self.SetWarmUp(TimeSpan.FromDays(30))

        # Schedule OTM put hedge every week 30 mins prior to market close
        self.Schedule.On(self.DateRules.EveryDay(),
                         self.TimeRules.BeforeMarketClose(self.underlying,30),
                         self.opt_replication)

        self.Schedule.On(self.DateRules.EveryDay(),
                         self.TimeRules.BeforeMarketClose(self.underlying,15),
                         self.check_expiries)
                         
#        self.Schedule.On(self.DateRules.EveryDay(),
#                         self.TimeRules.BeforeMarketClose(self.underlying,5),
#                         self.check_leverage)

    def OnAssignmentOrderEvent(self, assignmentEvent):
        # If we get assigned, close out all positions
        self.Liquidate()
        self.Log(str(assignmentEvent))
        self.opt_invested = False

    def OnData(self, data):
        if self.IsWarmingUp:
            return

        if self.underlying not in data.Bars:
            return

        if not self.opts_to_trade.empty:
            remaining_opts = self.opts_to_trade.copy(True)
            for opt, amount in self.opts_to_trade.iteritems():
                if data.ContainsKey(opt):
                    self.MarketOrder(opt, amount)
                    del remaining_opts[opt]
                    self.opt_invested = True
            self.opts_to_trade = remaining_opts

    def get_benchmark_performance(self): 
        price = self.Securities[self.underlying].Close

        if not self.benchmark_shares:
            self.benchmark_shares = self.initial_cash / price
            
        return self.benchmark_shares * price
        
    def log_holdings(self):
        # Log holdings, amount of portfolio hedged, etc.
        return

    def check_expiries(self):
        opt_pos = [x.Key for x in self.Portfolio if x.Value.Invested and x.Value.Type==SecurityType.Option]
        if opt_pos:
            # Get current time
            curr_dt = self.Time
            close_out_opts = [x for x in opt_pos if (x.ID.Date - curr_dt) < timedelta(0)]
            for opt in close_out_opts:
                # Get SPY level to see if option is ITM
                underlying_px = self.Securities[self.underlying].Price
                if opt.ID.OptionRight == 1:
                    if opt.ID.StrikePrice >= underlying_px:
                        self.Liquidate(opt)
                        self.opt_invested = False
                else:
                    if opt.ID.StrikePrice <= underlying_px:
                        self.Liquidate(opt)
                        self.opt_invested = False
                

    def opt_replication(self):
        if self.opt_invested:
            return
        
        # Get contract list and convert to DF
        contracts = self.OptionChainProvider.GetOptionContractList(self.underlying, self.Time)
        if len(contracts) == 0:
            return
        opts = contract_list_to_df(contracts)
        puts = opts[opts['Right'] == 1]
        calls = opts[opts['Right'] == 0]

        # Get index level
        curr_idx_lvl = self.Securities[self.underlying].Price

        # Get puts 1 year out
        filtered_puts_expiry = self.select_closest_expiry(puts, self.target_expiry)
        filtered_puts_expiry = puts.loc[filtered_puts_expiry.index]

        # Get calls 1 year out
        filtered_calls_expiry = self.select_closest_expiry(calls, self.target_expiry)
        filtered_calls_expiry = calls.loc[filtered_calls_expiry.index]

        # Get ATM put
        puts_to_sell = self.select_closest_strike(filtered_puts_expiry, curr_idx_lvl)
        puts_to_sell_info = puts.loc[puts_to_sell]
        puts_to_sell_symbol = puts_to_sell_info['Symbol']

        # Get ATM call
        calls_to_buy = self.select_closest_strike(filtered_calls_expiry, curr_idx_lvl)
        calls_to_buy_info = calls.loc[calls_to_buy]
        calls_to_buy_symbol = calls_to_buy_info['Symbol']

        # Calculate size to buy
        puts_to_sell_amount = -self.Portfolio.TotalPortfolioValue / (self.multiplier * curr_idx_lvl)
        calls_to_buy_amount = self.Portfolio.TotalPortfolioValue / (self.multiplier * curr_idx_lvl)
        
        # Create series of weights
        opts_to_trade = pd.Series(index = puts_to_sell_symbol, data = puts_to_sell_amount)
        opts_to_trade = opts_to_trade.append(pd.Series(index = calls_to_buy_symbol, data = calls_to_buy_amount))
        
        # Calculate final amounts
        # opts_to_buy *= opts_to_buy_amount
        opts_to_trade = opts_to_trade.round(0)

        # Get rid of zero quantity
        opts_to_trade = opts_to_trade[opts_to_trade.abs() > 0]

        # Add security
        [self.AddOptionContract(sym, Resolution.Minute) for sym in opts_to_trade.index]
        self.opts_to_trade = opts_to_trade

    def select_closest_strike(self, options, strike):
        strike_distance = (options['Strike'] - strike).abs()
        opts_to_trade = strike_distance.groupby(options['Expiry']).idxmin().values
        return opts_to_trade

    def select_closest_expiry(self, options, expiry_time):
        """Select option closest to desired expiration

        Args:
            options (pd.DataFrame): df with options information
            expiry_time (int): number of calendar days until expiry desired

        Returns:
            pd.Series: series with days to expiration as values and option symbols as index
        """
        # Get current time
        curr_dt = self.Time

        # Get desired expiration date
        desired_expiry = curr_dt + pd.Timedelta(days = expiry_time)
        desired_expiry = desired_expiry.replace(hour = 0, minute = 0)

        # Find distance to expiration
        expiry_distances = options['Expiry'] - desired_expiry

        # Get only expiries 1 year out
        #expiry_distances = expiry_distances[expiry_distances > pd.Timedelta(0)]

        # Get lowest expiration distance and options expiring on that date
        min_expiry_days = expiry_distances.abs().min()
        min_expiry = expiry_distances.abs().idxmin()
        min_expiry_date = expiry_distances.loc[min_expiry]
        
        # If min expiry is after desired time, then get expiry before desired time
        if min_expiry_days.days > 0:
            second_min_expiry_days = expiry_distances[expiry_distances < min_expiry_days].max()
            second_min_expiry = expiry_distances[expiry_distances < min_expiry_days].idxmax()
            second_min_expiry_date = expiry_distances.loc[second_min_expiry]
        else:
            second_min_expiry_days = expiry_distances[expiry_distances > min_expiry_days].min()
            second_min_expiry = expiry_distances[expiry_distances > min_expiry_days].idxmin()
            second_min_expiry_date = expiry_distances.loc[second_min_expiry]

        # If the expiry is more than 5 days out, then create custom expiry
        # by trading two options of different expiries
        if min_expiry_days.days > 5:
            opts_to_trade = expiry_distances[expiry_distances.isin([min_expiry_date, second_min_expiry_date])]
        else:
            opts_to_trade = expiry_distances[expiry_distances == min_expiry_date]

        return opts_to_trade

    def OnEndOfAlgorithm(self) -> None:
        spy_pos = self.Portfolio['SPY'].Quantity
        self.Log('SPY quantity: {0}'.format(spy_pos))

def contract_list_to_df(contract_list):
    return pd.DataFrame([[x.ID.OptionRight,
                          float(x.ID.StrikePrice),
                          x.ID.Date,
                          x.ID.ToString()
                          ] for x in contract_list],
                        index=[x.ID for x in contract_list],
                        columns=['Right', 'Strike', 'Expiry',
                                 'Symbol'])