Overall Statistics
Total Trades
12
Average Win
0.01%
Average Loss
-0.02%
Compounding Annual Return
-0.197%
Drawdown
0.100%
Expectancy
-0.549
Net Profit
-0.080%
Sharpe Ratio
-2.106
Probabilistic Sharpe Ratio
0.040%
Loss Rate
67%
Win Rate
33%
Profit-Loss Ratio
0.35
Alpha
-0.002
Beta
-0
Annual Standard Deviation
0.001
Annual Variance
0
Information Ratio
-2.504
Tracking Error
0.144
Treynor Ratio
4.211
Total Fees
$12.00
Estimated Strategy Capacity
$11000000.00
Lowest Capacity Asset
SPY R735QTJ8XC9X
class LogicalLightBrownDolphin(QCAlgorithm):

    def Initialize(self):
        self.SetStartDate(2021, 1, 1)
        self.SetCash(100000) 
        equity = self.AddEquity("SPY", Resolution.Minute)
        self.spy = equity.Symbol
        # define our 30 minute trade bar consolidator. we can
        # access the 30 minute bar from the DataConsolidated events
        thirtyMinuteConsolidator = TradeBarConsolidator(timedelta(minutes=30))
        
        # attach our event handler. the event handler is a function that will
        # be called each time we produce a new consolidated piece of data.
        thirtyMinuteConsolidator.DataConsolidated += self.ThirtyMinuteBarHandler

        # this call adds our 30 minute consolidator to
        # the manager to receive updates from the engine
        #self.SubscriptionManager.AddConsolidator("SPY", thirtyMinuteConsolidator)
        
        # Indicators
        self.rsi = self.RSI("SPY", 14)
        self.ema200 = self.EMA("SPY", 200)
        self.sto = self.STO("SPY", 14, 14, 3)
        self.atr = self.ATR(self.spy, 14)
        
        # register indicators with consolidator so they are updated on the same time frame
        self.RegisterIndicator(self.spy, self.rsi, thirtyMinuteConsolidator)
        self.RegisterIndicator(self.spy, self.ema200, thirtyMinuteConsolidator)
        self.RegisterIndicator(self.spy, self.sto, thirtyMinuteConsolidator)
        self.RegisterIndicator(self.spy, self.atr, thirtyMinuteConsolidator)
        
        # Indicator rolling windows
        self.rsiWin = RollingWindow[float](25)
        self.priceWin = RollingWindow[float](25)
        self.stoKWin = RollingWindow[float](25)
        self.stoDWin = RollingWindow[float](25)
        self.atrWin = RollingWindow[float](25)
        self.ema200Win = RollingWindow[float](25)
        
        # buy flags
        self.priceActionTrigger = False
        self.rsiTrigger = False
        
        self.holdCount = 0
        
        # orders
        self.currentOrder = None
        self.stopLossOrder = None
        self.takeProfitOrder = None
        
        # This defines a scheduled event which fires self.ClosePositions everyday SPY is trading 1 minute before SPY stops trading
        #self.Schedule.On(self.DateRules.EveryDay(self.spy), self.TimeRules.BeforeMarketClose(self.spy, 1), self.ClosePositions)
        
        #overlayPlot = Chart("Overlay Plot")
        #overlayPlot.AddSeries(Series("SPY", SeriesType.Line, 0))
        #overlayPlot.AddSeries(Series("Buy", SeriesType.Scatter, 0))
        #overlayPlot.AddSeries(Series("Sell", SeriesType.Scatter, 0))
        #overlayPlot.AddSeries(Series("ema200", SeriesType.Scatter, 0))
        #overlayPlot.AddSeries(Series("rsi", SeriesType.Line, 1))
        #overlayPlot.AddSeries(Series("sto", SeriesType.Line, 2))
        #self.AddChart(overlayPlot)
        
        

    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
        '''
        pass
        
        
    def ClosePositions(self):     
        # don't hold any positions over night
        if self.Portfolio.Invested:
            self.Liquidate()   
       
    def ThirtyMinuteBarHandler(self, sender, consolidated):
        #if self.Portfolio.Invested:
        #    if self.holdCount >= 10:
        #        self.Liquidate()
        #        self.holdCount = 0
        #    else:
        #        self.holdCount += 1
        
        if self.IsEveryoneReady(consolidated) and not self.Portfolio.Invested:
            self.Log("Consolidated and ema200 delta: " + str(abs(consolidated.Close - self.ema200.Current.Value)))
            #self.Plot("Overlay Plot", "rsi", self.rsi.Current.Value)
            #self.Plot("Overlay Plot", "sto", self.sto.Current.Value)
            #self.Plot("Overlay Plot", "ema200", self.ema200.Current.Value)
            # Check if we're not invested and then put portfolio 100% in the SPY ETF.      
            
            # if price is lower then previous 5 candles but on an uptrend,
            # but RSI is diverging on a downtrend,
            # use Stochastic crossover for entry signal
            (isPriceActionSignal, lowest_price_idx) = self.IsPriceActionSignaling(consolidated)
            if isPriceActionSignal or self.priceActionTrigger and lowest_price_idx > -1:
                #self.Log("Price Action Signaling")
                self.priceActionTrigger = True
                if self.IsRSI_Signaling(lowest_price_idx):
                    #self.Log("RSI Signaling")
                    self.rsiTrigger = True
            
                #self.Plot("Overlay Plot", "Buy", consolidated.Close)
            if self.priceActionTrigger and self.rsiTrigger and self.IsStoCrossOver():
                stopLossPrice = self.FindStopLossPrice()
                takeProfitPrice = self.FindTakeProfitPrice(stopLossPrice)
                if takeProfitPrice != -1:
                    self.PlaceOrder(stopLossPrice, takeProfitPrice)
                self.priceActionTrigger = False
                self.rsiTrigger = False
            return
        else:
            return
        
    def OnOrderEvent(self, orderEvent):
        # ignore events that are not closed
        if orderEvent.Status != OrderStatus.Filled:
            return
        
        # sanity check
        if self.takeProfitOrder == None or self.stopLossOrder == None:
            return
        
        filledOrderId = orderEvent.OrderId
        
        # if takeProfitOrder was filled, cancel stopLossOrder
        if self.takeProfitOrder.OrderId == filledOrderId and orderEvent.Status == OrderStatus.Filled:
            self.stopLossOrder.Cancel()
        
        # if stopLossOrder was filled, cancel takeProfitOrder
        if self.stopLossOrder.OrderId == filledOrderId and orderEvent.Status == OrderStatus.Filled:
            self.takeProfitOrder.Cancel()
        
    def PlaceOrder(self, stopLossPrice, takeProfitPrice):
        self.Log("Placing Order")
        self.Log("Current Order Price: " + str(self.priceWin[0]))
        self.Log("Stop Loss Price: " + str(stopLossPrice))
        self.Log("Take Profit Price: " + str(takeProfitPrice))
        self.Log("ATR: " + str(self.atrWin[0]))
        
        order_quantity = 50
        # place order
        self.currentOrder = self.MarketOrder(self.spy, order_quantity)
        
        # create stop loss order
        self.stopLossOrder = self.StopMarketOrder(self.spy, -order_quantity, stopLossPrice)
        
        # create take profit order
        self.takeProfitOrder = self.LimitOrder(self.spy, -order_quantity, takeProfitPrice)
        
    def FindStopLossPrice(self):
        #return self.priceWin[0] - (multiple * self.atrWin[0]) 
        lowestPrice = self.priceWin[0]
        # stop loss is the most recent swing low point
        for i in range(5):
            # making the assumption that the most recent bottom is in the last 10 time units
            if self.priceWin[i] < lowestPrice:
                lowestPrice = self.priceWin[i]
                
        if lowestPrice == self.priceWin[0]:
            multiple = 2
            lowestPrice = lowestPrice - (self.atrWin[0] * multiple)
            
        return lowestPrice
        
    def FindTakeProfitPrice(self, stopLossPrice):
        #return (multiple * self.atrWin[0]) + self.priceWin[0]
        takeProfitPrice = -1
        # we aim for a profit to loss ratio of 2:1
        currentPrice = self.priceWin[0]
        if stopLossPrice != currentPrice:
            delta = currentPrice - stopLossPrice
            takeProfitPrice = (2 * delta) + currentPrice
        
        return takeProfitPrice
    
    # has stochastic crossover occurred in the last 10 time units        
    def IsStoCrossOver(self):
        return self.stoKWin[0] < self.stoDWin[0] and self.stoKWin[10] > self.stoDWin[10]
        
    # is rsi on a downtrend
    def IsRSI_Signaling(self, lowest_price_idx):
        lowest_rsi = self.rsiWin[lowest_price_idx]
        return lowest_rsi > self.rsiWin[0]
       
    def IsPriceActionSignaling(self, consolidated):
        # price must be above 200 ema
        if consolidated.Close > self.ema200.Current.Value:
            self.Log("Price Action is signaling")
            self.Log("Consolidated Price: " + str(consolidated.Close))
            self.Log("ema200 Price: " + str(self.ema200.Current.Value))
            # price must be lower then previous 5 closes
            priceBottom = True
            for i in range(5):
                if consolidated.Close > self.priceWin[i]:
                    priceBottom = False
            
            # price must be higher then previous low
            lowestLow = min(self.priceWin)
            idx = list(self.priceWin).index(lowestLow)
            if priceBottom and lowestLow < consolidated.Close:
                return (True, idx)
                
        return (False, -1)
    
    def IsEveryoneReady(self, consolidated):
        # Are indicators ready?
        if self.rsi.IsReady and self.sto.IsReady:
            # if indicators are ready, create rolling window for them
            self.rsiWin.Add(self.rsi.Current.Value)
            self.priceWin.Add(consolidated.Close)
            self.stoKWin.Add(self.sto.StochK.Current.Value)
            self.stoDWin.Add(self.sto.StochD.Current.Value)
            self.atrWin.Add(self.atr.Current.Value)
            self.ema200Win.Add(self.ema200.Current.Value)
            
            if self.rsiWin.IsReady and self.priceWin.IsReady:
                
                return True and self.ema200.IsReady
        
        return False