Overall Statistics
Total Trades
11
Average Win
0.00%
Average Loss
0%
Compounding Annual Return
0%
Drawdown
0.000%
Expectancy
0
Net Profit
0%
Sharpe Ratio
0
Probabilistic Sharpe Ratio
24.151%
Loss Rate
0%
Win Rate
100%
Profit-Loss Ratio
0
Alpha
-0
Beta
0
Annual Standard Deviation
0
Annual Variance
0
Information Ratio
-2.755
Tracking Error
0.135
Treynor Ratio
0
Total Fees
$7.00
Estimated Strategy Capacity
$1100.00
Lowest Capacity Asset
SPY 31KZK16ZBHROM|SPY R735QTJ8XC9X
from SymbolData import *


class RetrospectiveFluorescentPinkWhale(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2020, 11, 26)  # Set Start Date
        self.SetCash(100000)  # Set Strategy Cash
        
        # ~~~ static vs dyanmic equity universe ~~~
        
        # Static Universe
        # 
        # OptionChainProvider vs AddOption
        # If you're using greeks, you'll need to use AddOption
        # 
        # # AddOption leads to slow backtests, basically impossible to use with dynamic
        # 
        # OptionChainProvider - faster backtests but lack of greeks and IV
        
        # If no greeks/IV needed -> OptionChainProvider
        
        # Dynamic Equity Universe
        # OptionChainProvider leads to much faster backtests
        #
    
        # If using AddOption, generally will use bottom technique to retrieve contracts
        # data.OptionsChains[option_chain.Symbol(identifier for chain)].Contracts # list of OptionContract objects
        
        # OptionContract.Greeks.Delta, etc
        
        
        # Medium sized static universe
        
        self.benchmark = "SPY"
        
        tickers = ["SPY", "QQQ", "GLD", "TLT"]
        
        self.symbol_data = {}
        
        for ticker in tickers:
            equity = self.AddEquity(ticker, Resolution.Minute)
            # this will be done automatically 
            equity.SetDataNormalizationMode(DataNormalizationMode.Raw)
        
        
        # rebalance scheduled event
        self.Schedule.On(self.DateRules.EveryDay(self.benchmark), self.TimeRules.BeforeMarketClose(self.benchmark, 5), self.Rebalance)
        
        
        self.SetSecurityInitializer(lambda security: security.SetMarketPrice(self.GetLastKnownPrice(security)))
        
        
        ### STRATEGY ####
        # Entry: 5 minutes before close daily (35 day Current MA) > (35 day MA - 5 periods) -> Long
        # Write Weekly ATM put, and buy ~ $10 below market price
        
        # Just let it expire with either max profit or close before expiration, buy back for loss
    
        # open_option_positions = [symbol for symbol in self.Portfolio.Keys if self.Portfolio[symbol].Invested and \
        #     symbol.SecurityType == SecurityType.Option]
            
        # # 30 mins before market close on friday/OEC
        # for symbol in open_option_positions:
        #     # close out
        #     self.Liquidate(symbol)
    
    
    
    
    def Rebalance(self):
        
        
        for symbol, symbol_data in self.symbol_data.items():
                
            if symbol.Value != "SPY":
                continue;
            
            if not symbol_data.sma_window.IsReady:
                continue
            signal = symbol_data.long_signal
            
            if signal and not self.Portfolio[symbol].Invested:
                # day of week - self.Time.weekday(), from there calculate dte needed
                
                current_market_price = self.Securities[symbol].Price
                short_put = self.select_option(symbol, OptionRight.Put, current_market_price, 7)
                long_put = self.select_option(symbol, OptionRight.Put, current_market_price - 25, 7)
                
                # could put this inside of select_option
                
                # nuanced detail - when we call AddOptionContract to subscribe to
                # option data, data is not immediately available, it takes 1 time step
                # so if we try to call market order/ limit order, it won't be able to fill
                
                self.AddOptionContract(short_put)
                self.AddOptionContract(long_put)
                
                short_put_bid = self.Securities[short_put].BidPrice
                long_put_ask = self.Securities[short_put].AskPrice
                
                
                self.LimitOrder(short_put, -1,  short_put_bid)
                self.LimitOrder(long_put, 1, long_put_ask)
                
                
    
    def select_option(self, underlying_symbol, option_right, strike, expiry_dte):
        
        # underlying symbol and date of option data needed
        
        # looks into option open interest data for that underlying
        # returns that
        
        # list of Symbol objects
        contract_symbols = self.OptionChainProvider.GetOptionContractList(underlying_symbol, self.Time)
        
        
        filtered_for_right = [symbol for symbol in contract_symbols if symbol.ID.OptionRight == option_right]
        
        
        expiry_date = self.Time + timedelta(expiry_dte)
        
        
        # ascending order - reverse=False - default
        sorted_by_expiry = sorted(filtered_for_right, key=lambda s:abs(s.ID.Date - expiry_date), reverse=False)
        
        
        if len(sorted_by_expiry) > 0:
            desired_expiry_date = sorted_by_expiry[0].ID.Date
        else:
            self.Debug(f"No contracts found for {underlying_symbol}")
            return None
        
        
        contracts_with_desired_expiry = [symbol for symbol in filtered_for_right if symbol.ID.Date == desired_expiry_date]
        
        # sorted ascending again    
        sorted_by_strike = sorted(contracts_with_desired_expiry, key=lambda s: abs(s.ID.StrikePrice - strike), reverse=True)        
        
        
        if len(sorted_by_strike) > 0:
            # returns a symbol object for selected option
            return sorted_by_strike[0]
        else:
            return None
        
        
        
        
        
        
        
        

    
    def OnSecuritiesChanged(self, changes):
        
        for security in changes.AddedSecurities:
            symbol = security.Symbol
            
            if not symbol in self.symbol_data:
                self.symbol_data[symbol] = SymbolData(self, symbol)
        
        
        for security in changes.RemovedSecurities:
            symbol = security.Symbol
            
            if symbol in self.symbol_data:
                symbol_data = self.symbol_data.pop(symbol, None)
                symbol_data.kill_data_subscriptions()
    
    
    def OnData(self, data):
        '''OnData event is the primary entry point for your algorithm. Each new data point will be pumped in here.
            Arguments:
                data: Slice object keyed by symbol containing the stock data
        '''

        
class SymbolData:
    
    def __init__(self, algorithm, symbol):
        
        
        self.algorithm = algorithm
        self.symbol = symbol
        
        # self.algorithm.SMA
        self.sma = SimpleMovingAverage(35)
        
        # User defined method OnSMA will fire everytime SMA is updated
        self.sma.Updated += self.OnSMA
        
        # consolidator is subscribed to receive data
        self.daily_consolidator = QuoteBarConsolidator(timedelta(days=1))
        self.algorithm.SubscriptionManager.AddConsolidator(self.symbol, self.daily_consolidator)
        
        # consolidator will update to indicator
        self.algorithm.RegisterIndicator(self.symbol, self.sma, self.daily_consolidator)
        
        self.sma_window = RollingWindow[IndicatorDataPoint](5)
        
        # pandas dataframe stuff is buggy
        # self.warm_up_sma()
        
        
    def warm_up_sma(self):
        
        # option 1: update consolidator with minute historical data -> spit out daily bar -> update sma
        # option 2: daily historical data -> update sma
        
        # daily data in
        # 0th index is symbol, 1st index is Time
        history = self.algorithm.History(self.symbol, 35, Resolution.Daily).unstack(0).close
        
        # time series daily close data, with each column being symbol
        if not history.empty:
            
            if str(self.symbol) in history.columns:
                
                close_pandas_series_data = history[str(self.symbol)]
                
                for time, close in close_pandas_series_data:
                    
                    self.sma.Update(time, close)
    
    def OnSMA(self, sender, updated):
        
        if self.sma.IsReady:
            
            # self.sma - SimpleMovingAverage/Indicator Object
            # self.sma.Current = IndicatorDataPoint object
            # self.sma.Current.Value - float object
            
            self.sma_window.Add(self.sma.Current)
    
    
    
    def kill_data_subscriptions(self):
        
        self.algorithm.SubscriptionManager.RemoveConsolidator(self.symbol, self.daily_consolidator)
        
    
    @property
    def long_signal(self):
        
        return self.sma.Current.Value > self.sma_window[4].Value