Overall Statistics
Total Trades
4
Average Win
0.27%
Average Loss
-0.18%
Compounding Annual Return
17.503%
Drawdown
0.200%
Expectancy
0.242
Net Profit
0.088%
Sharpe Ratio
2.166
Probabilistic Sharpe Ratio
0%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
1.48
Alpha
0.577
Beta
0.992
Annual Standard Deviation
0.055
Annual Variance
0.003
Information Ratio
1290.259
Tracking Error
0
Treynor Ratio
0.12
Total Fees
$4.00
Estimated Strategy Capacity
$320000000.00
Lowest Capacity Asset
SPY R735QTJ8XC9X
# VERSION 2 of MACD CROSSOVER STRAT: GOES LONG ON 3/6 CROSS UP SIGNAL IF NOT ALREADY INVESTED
# EXECUTION LOGIC FROM SUCCESSFUL LIVE TRADING
# FOR JOVAD 8/6 SESSION 

import pandas as pd
import decimal as d

class MACDCrossoverStratVer2(QCAlgorithm):

    stopMarketTicket = None
    stopLimitTicket = None
    profitTargetTicket = None
    stopMarketFillTime = datetime.min
    stopLimitFillTime = datetime.min
    
    def Initialize(self):
        self.SetStartDate(2021, 8, 3)
        self.SetEndDate(2021, 8, 4)
        self.SetCash(100000)
        self.spy = self.AddEquity("SPY", Resolution.Second).Symbol
        self.symbolDataBySymbol = {}
        symbolData = SymbolData(self.spy)
        self.symbolDataBySymbol[self.spy] = symbolData
        self.SetWarmUp(30000) # Warm up using 500*60 minute bars for all subscribed data

        self.__macd = self.MACD("SPY", 3*60, 6*60, 9*60, MovingAverageType.Exponential, Resolution.Second)
        self.__previous = datetime.min
        self.PlotIndicator("MACD", True, self.__macd, self.__macd.Signal)  # SWAPPED OUT FOR LINE BELOW TESTING CURRENT.VALUE BUT DIDN'T WORK
        self.PlotIndicator("SPY", self.__macd.Fast, self.__macd.Slow)

        self.entryTicket = None
        self.stopLimitTicket = None
        self.profit1Ticket = None
        self.profit2Ticket = None
        self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.BeforeMarketClose("SPY", 1), self.LiquidateAtClose)
        # removed datanormalizationmode raw

        self.lowValues = RollingWindow[float](1+60) 
        self.highValues = RollingWindow[float](5*60) 
        
    def OnData(self, data):

        if not self.__macd.IsReady:       
            return

        self.StopLossTracking()

        tolerance = 0.0025
        holdings = self.Portfolio["SPY"].Quantity
        signalDeltaPercent = (self.__macd.Current.Value - self.__macd.Signal.Current.Value)

        # Trade signal:  When MACD crosses up its signal go long 
        if holdings <= 0 and signalDeltaPercent > tolerance:         # ALT 'if not self.Portfolio.Invested' 
##            self.Liquidate("SPY")  # first liquidate any short position before going long (no longer needed for this long-only algo)
            price = self.Securities["SPY"].Price
            quantity = self.CalculateOrderQuantity("SPY", .5*.95)   # CHANGING ENTRY ORDER TO ENTER HALF ORDER AS MARKET AND HALF AS LIMIT + 0.02 TO COMPARE FILLS
            self.MarketOrder("SPY", quantity)         # Places a market order using half of portfolio buying power (ALT USE: 'self.SetHoldings("SPY", 0.95)  
##            self.LimitOrder("SPY", quantity, price + 0.02)  # Places a limit entry order using half of portfolio buying power (to compare with market price)

        elif holdings >= 0 and signalDeltaPercent < -tolerance:
            return          # deleted following code:
                            ## self.Liquidate("SPY")
                            ## self.SetHoldings("SPY", -0.95)
            
            self.Debug(str(self.Portfolio["SPY"].AveragePrice))    # print avg fill
## ISN'T ABOVE DUPE OF LINE 64?
##            self.__previous = self.Time

    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status != OrderStatus.Filled:
            return
        
        if self.entryTicket != None and self.entryTicket.OrderId == orderEvent.OrderId:

           # Stop limit exit order
            self.holdings = self.Securities["SPY"].Holdings
            self.low = self.Securities["SPY"].Low  # low of bar prior to entry #*** NEED TO ADD 60 TO THIS TO INDEX IT TO PREVIOUS MINUTE BAR INSTEAD OF PREVIOUS SECOND BAR
            self.stopPrice = self.low - 0.01 # Trigger stop limit when price falls 1ct below the low of the bar prior to entry bar (based on ask price, if this takes bid then reduce to 0 cts)
            self.limitPrice = self.low -0.03 # Sell equal or better than 3cts below the low of the bar prior to entry bar (based on ask price, if this takes bid then reduce to 2cts)
            self.stopLimitTicket = self.StopLimitOrder("SPY", -1, self.stopPrice, self.limitPrice) # ***THIS WAS CAUSING STOP LIMIT ORDER TO BE PLACED FOR QTY OF 1 SHARE AS IT WAS -1 so i replaced with '-self.holdings' and it rerturned error: bad operand for unary 'Equity HOlding'
                    
          # defines stop loss at time step 1: opening trade execution
            ''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
            lastBar = self.History(self.spy, 1, Resolution.Minute)
            self.symbolDataBySymbol[self.spy].stopPrice = lastBar["low"][0]
            self.symbolDataBySymbol[self.spy].currentTimeStep = 1
            self.symbolDataBySymbol[self.spy].entryTime = self.Time
            self.symbolDataBySymbol[self.spy].fillPrice = self.Securities[self.spy].Price
            self.symbolDataBySymbol[self.spy].entryHigh = lastBar["high"][0] 
        
            
        order = self.Transactions.GetOrderById(orderEvent.OrderId)
        if orderEvent.Status == OrderStatus.Filled: 
            self.Log("{0}: {1}: {2}".format(self.Time, order.Type, orderEvent))        
        
    def StopLossTracking(self):
        for symbol, symbolData in self.symbolDataBySymbol.items():
            if symbolData.stopPrice == None: continue
        
            # If using limit order for stop (the 'if' below) then this first 'if' statement wont be needed since it is executing market order instead
 #           if self.Securities[symbol].Price <= symbolData.stopPrice:
 #               self.Liquidate(symbol)

            symbolData.ResetVariables()
            
            # defines 1st stop loss update at time step 2 which is at close of entry bar
            if symbolData.currentTimeStep == 1 and symbolData.entryTime != self.Time:
                symbolData.stopPrice = symbolData.fillPrice
                symbolData.currentTimeStep = 2
                symbolData.stopTicket = self.StopLimitOrder(self.spy, symbolData.fillPrice)
               
            # defines 2nd stop loss update at time step 3 which is at close of 4minutes after close of entry bar
            elif symbolData.currentTimeStep == 2 and (self.Time - symbolData.entryTime).Minute >= 4:
                symbolData.stopPrice = symbolData.entryHigh
                symbolData.currentTimeStep = 3
                symbolData.stopTicket.Cancel()
                symbolData.stopTicket = self.StopLimitOrder(self.spy, symbolData.entryHigh)        

        
            self.lastOrderEvent = orderEvent  # save order
            self.Debug(orderEvent.OrderId)  # print order ID
            self.Debug(self.Securities["SPY"].Close)  # print fill prices BUT WE ALREADY HAVE THIS ABOVE

        if self.stopLimitTicket is not None and self.stopLimitTicket.OrderId == orderEvent.OrderId:

            self.stopMarketFillTime = self.Time  # store datetime
            self.stopLimitFillTime = self.Time
            self.Debug(self.stopMarketFillTime)

    def LiquidateAtClose(self):
        if self.Securities["SPY"].Price is not None:
           self.Liquidate()   #  ALT: self.MarketOnCloseOrder()
           
           
class SymbolData:
    def __init__(self, symbol):
        self.Symbol = symbol
        self.stopPrice = None
        self.currentTimeStep = 0 # 0 no position, 1, 2, 3
        self.entryTime = None
        self.fillPrice = None
        self.entryHigh = None
        self.stopTicket = None
        self.closeWindow = None  # 10:25 DEBUGGING: added this to SEE IF IF SOLVES ISSUE
        
    def ResetVariables(self):
        self.stopPrice = None
        self.currentTimeStep = 0 # 0 no position, 1, 2, 3
        self.entryTime = None
        self.fillPrice = None
        self.entryHigh = None
        if self.stopTicket != None: self.stopTicket.Cancel()