Overall Statistics
Total Orders
47
Average Win
1.66%
Average Loss
-2.18%
Compounding Annual Return
8.503%
Drawdown
29.300%
Expectancy
-0.847
Start Equity
100000
End Equity
150418.37
Net Profit
50.418%
Sharpe Ratio
0.241
Sortino Ratio
0.281
Probabilistic Sharpe Ratio
11.458%
Loss Rate
91%
Win Rate
9%
Profit-Loss Ratio
0.76
Alpha
-0.024
Beta
0.684
Annual Standard Deviation
0.114
Annual Variance
0.013
Information Ratio
-0.637
Tracking Error
0.075
Treynor Ratio
0.04
Total Fees
$85.71
Estimated Strategy Capacity
$71000000.00
Lowest Capacity Asset
SPY R735QTJ8XC9X
Portfolio Turnover
0.09%
Drawdown Recovery
894
# region imports
from AlgorithmImports import *
# endregion
# Watch my Tutorial: https://youtu.be/Lq-Ri7YU5fU


class OptionChainProviderPutProtection(QCAlgorithm):

    def initialize(self):
        self.set_start_date(self.end_date - timedelta(5*365))
        self.set_cash(100_000)
        self.settings.seed_initial_prices = True
        self.settings.automatic_indicator_warm_up = True
        # parameters ------------------------------------------------------------
        self._days_before_exp = 2 # number of days before expiry to exit
        self._dte = 25 # target days till expiration
        self._otm = 0.01 # target percentage OTM of put
        self._lookback_iv = 150 # lookback length of IV indicator
        self._iv_lvl = 0.5 # enter position at this lvl of IV indicator
        self._percentage = 0.9 # percentage of portfolio for underlying asset
        self._options_alloc = 90 # 1 option for X num of shares (balanced would be 100)
        # ------------------------------------------------------------------------
        # Add the underlying asset.
        self._equity = self.add_equity("SPY", data_normalization_mode=DataNormalizationMode.RAW)
        # Add VIX data.
        self._vix = self.add_data(CBOE, "VIX")
        # Initialize the IV rank indicator.
        self._rank = 0
        # Create a member to trade the Option contract.
        self._contract = None
        # Schedule plotting 30 minutes after every market open.
        date_rule = self.date_rules.every_day(self._equity)
        time_rule = self.time_rules.after_market_open(self._equity, 30)
        self.schedule.on(date_rule, time_rule, self._plotting)
        # Update the IV Rank 30 minutes after every market open.
        self.schedule.on(date_rule, time_rule, self._vix_rank)
        # warmup for IV indicator of data
        self.set_warm_up(timedelta(self._lookback_iv)) 

    def _vix_rank(self):
        history = self.history(CBOE, self._vix, self._lookback_iv, Resolution.DAILY)
        # (Current - Min) / (Max - Min)
        self._rank = ((self._vix.price - min(history["low"])) / (max(history["high"]) - min(history["low"])))
 
    def on_data(self, data):
        if self.is_warming_up:
            return
        # Buy the underlying asset if we don't already hold it.
        if not self._equity.invested:
            self.set_holdings(self._equity, self._percentage)
        # Buy a put if VIX is relatively high.
        if self._rank > self._iv_lvl:
            self._buy_put(data)
        # Close the put before it expires.
        if (self._contract and 
            (self._contract.expiry - self.time) <= timedelta(self._days_before_exp)):
            self.liquidate(self._contract)
            self._contract = None

    def _buy_put(self, data):
        # Get the Option contract.
        if not self._contract:
            self._contract = self._options_filter(data)        
        # If not invested and we have Option data, buy the Option.
        elif not self.portfolio[self._contract].invested and self._contract in data:
            self.buy(self._contract, round(self._equity.holdings.quantity / self._options_alloc))

    def _options_filter(self, data):
        chain = self.option_chain(self._equity)
        # Filter the OTM put Options from the contract list,
        # which expire close to self._dte num of days from now.
        otm_puts = [
            c for c in chain 
            if (c.right == OptionRight.PUT and
                self._equity.price - c.strike > self._otm * self._equity.price and
                self._dte - 8 < (c.expiry - data.time).days < self._dte + 8)
        ]
        if otm_puts:
            # Sort the contracts by closest to self._dte days 
            # from now and desired strike, and pick the first one.
            contract = sorted(
                sorted(otm_puts, key=lambda x: abs((x.expiry - self.time).days - self._dte)),
                key=lambda x: self._equity.price - x.strike
            )[0]
            self.add_option_contract(contract)
            return contract

    def _plotting(self):
        # plot IV indicator
        self.plot("Vol Chart", "Rank", self._rank)
        # plot indicator entry level
        self.plot("Vol Chart", "lvl", self._iv_lvl)
        # plot underlying's price
        self.plot("Data Chart", str(self._equity.symbol), self._equity.close)
        # plot strike of put option
        option_invested = [
            symbol for symbol, holding in self.portfolio.items() 
            if holding.invested and holding.type==SecurityType.OPTION
        ]
        if option_invested:
            self.plot("Data Chart", "strike", option_invested[0].id.strike_price)