Overall Statistics
class ReverseGeorgeDouglasTaylorStrategy(QCAlgorithm):
    # Strategy Rules:
    # (1) If the market is up 2 days in a row and opens higher the third day, buy at the open. 
    # (2) If the market is up 2 days in a row, doesn’t open higher, but closes higher, buy at the close.
    # (3) Exit positions on the next day's close.
    
    # Source: Perry Kaufman on the Better System Trader Podcast
    #         https://youtu.be/cE_7ykL2ybU?t=2840

    def Initialize(self):
        self.SetStartDate(2017, 1, 1)
        self.SetCash(30000)
        self.allocation_ratio = 0.5
        
        tickers = ['IWM', 'QQQ']
        self.symbol_data_by_symbol = {}
        for ticker in tickers:
            symbol = self.AddEquity(ticker, Resolution.Minute).Symbol
            self.symbol_data_by_symbol[symbol] = SymbolData(symbol, self)
        
        self.Schedule.On(self.DateRules.EveryDay("IWM"), self.TimeRules.AfterMarketOpen("IWM", 1), self.EveryDayAfterMarketOpen)
        self.Schedule.On(self.DateRules.EveryDay("IWM"), self.TimeRules.BeforeMarketClose("IWM", 1), self.EveryDayBeforeMarketClose)                            

    def EveryDayAfterMarketOpen(self):
        # Update days_held tracker for each symbol
        for symbol, symbol_data in self.symbol_data_by_symbol.items():
            if self.Securities[symbol].Invested:
                symbol_data.days_held += 1
        
        self.make_orders('open')

    def EveryDayBeforeMarketClose(self):
        self.make_orders('close')
        
        # Exit orders with days held == 1
        for symbol, symbol_data in self.symbol_data_by_symbol.items():
            if symbol_data.days_held == 1:
                symbol_data.days_held = 0
                self.Liquidate(symbol)
        
    def make_orders(self, at):
        for symbol, symbol_data in self.symbol_data_by_symbol.items():
            if at == 'open':
                price = self.Securities[symbol].Open
            else:
                price = self.Securities[symbol].Close
            signal = symbol_data.generate_signal(price, at)
            if signal:
                if self.Securities[symbol].Invested:
                    # Extend exit date
                    symbol_data.days_held -= 1
                else:
                    # Make order
                    quantity = self.CalculateOrderQuantity(symbol, self.allocation_ratio / len(self.symbol_data_by_symbol))
                    self.MarketOrder(symbol, quantity)

class SymbolData:
    open_price = None
    days_held = 0

    def __init__(self, symbol, algorithm):
        self.symbol = symbol
        self.window = RollingWindow[TradeBar](3)
        
        # Warm up history
        history = algorithm.History(symbol, 3, Resolution.Daily)
        for idx, row in history.iterrows():
            tradebar = TradeBar(idx, symbol, row.open, row.high, row.low, row.close, row.volume)
            self.window.Add(tradebar)
        
        # Setup consolidator
        self.consolidator = TradeBarConsolidator(timedelta(1))
        self.consolidator.DataConsolidated += self.ConsolidationHandler
        algorithm.SubscriptionManager.AddConsolidator(symbol, self.consolidator)
        
    def ConsolidationHandler(self, sender, consolidated):
        self.window.Add(consolidated)
        
    def generate_signal(self, price, at):
        if at == 'open':
            self.open_price = price

        # If up two days in a row
        if self.window[2].Close < self.window[1].Close and self.window[1].Close < self.window[0].Close:
            # Gaps up the third day
            if at == 'open' and self.window[0].Close < self.open_price:
                return True # Short open
            
            # Doesn't gap up the third day, but the third day is an up day
            if at == 'close' and self.window[0].Close >= self.open_price and self.window[0].Close < price:
                return True # Short close