Overall Statistics
Total Trades
8
Average Win
6.86%
Average Loss
-1.69%
Compounding Annual Return
8.021%
Drawdown
8.800%
Expectancy
1.525
Net Profit
10.118%
Sharpe Ratio
0.62
Probabilistic Sharpe Ratio
29.879%
Loss Rate
50%
Win Rate
50%
Profit-Loss Ratio
4.05
Alpha
-0.024
Beta
0.636
Annual Standard Deviation
0.096
Annual Variance
0.009
Information Ratio
-1.005
Tracking Error
0.071
Treynor Ratio
0.094
Total Fees
$9.41
Estimated Strategy Capacity
$1800000000.00
Lowest Capacity Asset
SPY R735QTJ8XC9X
#region imports
from AlgorithmImports import *
#endregion
class TrailingStopLoss(QCAlgorithm):
    
    def Initialize(self):
        startingCash = 100000
        self.SetCash(startingCash)  # Set Strategy Cash
        
        self.SetStartDate(2021, 1, 1)
        self.SetEndDate(2022, 4, 1)
        
        
        symbol = "SPY"
        
        self.symbol = self.AddEquity(symbol, Resolution.Daily).Symbol
        self.tli = self.AddData(TLI, "tli", Resolution.Daily).Symbol
        
        
        self.longEntryThreshhold = 0.15
        self.shortEntryThreshhold = -0.15
        self.longAllocation = 1 # 100% long
        self.shortAllocation = -1 # 100% short
        
        self.entryTicketLong = None
        self.stopMarketTicketLong = None
        
        self.entryTicketShort = None
        self.stopMarketTicketShort = None
        
        self.entryTime = datetime.min
        self.stopMarketOrderFillTime = datetime.min
        
        
        self.highestPrice = 0
        self.lowestPrice = 0

    def OnData(self, data):
        
        if self.tli not in data:
            return
        
        # wait 30 days after last exit
        if (self.Time - self.stopMarketOrderFillTime).days < 2:
            return
        
        price = self.Securities[self.symbol].Price
        
        # send long entry limit order
        if not self.Portfolio.Invested and not self.Transactions.GetOpenOrders(self.symbol):
            if data[self.tli].Value > self.longEntryThreshhold:
                quantity = self.CalculateOrderQuantity(self.symbol, 0.9)
                self.entryTicketLong = self.LimitOrder(self.symbol, quantity, price, "Entry Order Long, TLI: " + str(data[self.tli].Value)
                + " TLI_time" +str(data[self.tli].Time))
                self.entryTime = self.Time
        
        # send short entry limit order
            if data[self.tli].Value < self.shortEntryThreshhold:
                quantity = self.CalculateOrderQuantity(self.symbol, 0.9)
                self.entryTicketShort = self.LimitOrder(self.symbol, -quantity, price, "Entry Order short, TLI: " + str(data[self.tli].Value)
                + " TLI_time" +str(data[self.tli].Time))
                self.entryTime = self.Time
        
        
        # move long limit price if not filled after 1 day
        if (self.Time - self.entryTime).days > 1 and (self.entryTicketLong is not None) and self.entryTicketLong.Status != OrderStatus.Filled:
            self.entryTime = self.Time
            updateFields = UpdateOrderFields()
            updateFields.LimitPrice = price
            self.entryTicketLong.Update(updateFields)
        
        # move Short limit price if not filled after 1 day
        if (self.Time - self.entryTime).days > 1 and (self.entryTicketShort is not None) and self.entryTicketShort.Status != OrderStatus.Filled:
            self.entryTime = self.Time
            updateFields = UpdateOrderFields()
            updateFields.LimitPrice = price
            self.entryTicketShort.Update(updateFields)
        
        # move long stop limit
        if self.stopMarketTicketLong is not None and self.Portfolio.Invested:
            # move up trailing stop price
            if price > self.highestPrice:
                self.highestPrice = price
                updateFields = UpdateOrderFields()
                updateFields.StopPrice = price * 0.95
                self.stopMarketTicketLong.Update(updateFields)
                #self.Debug(updateFields.StopPrice)
                
        # move short stop limit
        if self.stopMarketTicketShort is not None and self.Portfolio.Invested:
            # move down trailing stop price
            if price < self.lowestPrice:
                self.lowestPrice = price
                updateFields = UpdateOrderFields()
                updateFields.StopPrice = price * 1.05
                self.stopMarketTicketShort.Update(updateFields)
                #self.Debug(updateFields.StopPrice)


    def OnOrderEvent(self, orderEvent):
        
        if orderEvent.Status != OrderStatus.Filled:
            return
        
        # send long stop loss order if entry limit order is filled
        if self.entryTicketLong is not None and self.entryTicketLong.OrderId == orderEvent.OrderId:
            self.stopMarketTicketLong = self.StopMarketOrder(self.symbol, -self.entryTicketLong.Quantity, 0.95 * self.entryTicketLong.AverageFillPrice)
        
        # send Short stop loss order if entry limit order is filled
        if self.entryTicketShort is not None and self.entryTicketShort.OrderId == orderEvent.OrderId:
            self.stopMarketTicketShort = self.StopMarketOrder(self.symbol, self.entryTicketShort.Quantity, 1.05 * self.entryTicketShort.AverageFillPrice)
        
        # save fill time of Long stop loss order (and reset highestPrice lowestPrice)
        if (self.stopMarketTicketLong is not None) and (self.stopMarketTicketLong.OrderId == orderEvent.OrderId): 
            self.stopMarketOrderFillTime = self.Time
            self.highestPrice = 0
            self.lowestPrice = 0
            
        # save fill time of short stop loss order (and reset highestPrice lowestPrice)
        if (self.stopMarketTicketShort is not None) and (self.stopMarketTicketShort.OrderId == orderEvent.OrderId): 
            self.stopMarketOrderFillTime = self.Time
            self.highestPrice = 0
            self.lowestPrice = 0
            
            
            
class TLI(PythonData):

    def GetSource(self, config, date, isLive):
        
        #source = "https://www.dropbox.com/s/zlm00njnufrhnko/TLI.csv?dl=1"
        source = "https://www.dropbox.com/s/q4njfg7ihs2cwb0/TLI_20220415.csv?dl=1"
        return SubscriptionDataSource(source, SubscriptionTransportMedium.RemoteFile);

    def Reader(self, config, line, date, isLive):
        if not (line.strip() and line[0].isdigit()):
            return None
        
        data = line.split(',')
        tli = TLI()
        
        try:
            tli.Symbol = config.Symbol
            # make data available Monday morning (Friday 16:00 + 66 hours) 
            # since we can't trade on weekend anyway
            tli.Time = datetime.strptime(data[0], '%Y-%m-%d %H:%M:%S') + timedelta(hours=66)
            
            tli.Value = data[1]
            
        except ValueError:
            return None
        
        return tli