Overall Statistics
Total Orders
32
Average Win
4.43%
Average Loss
-3.11%
Compounding Annual Return
11.774%
Drawdown
14.200%
Expectancy
0.617
Start Equity
100000
End Equity
139671.07
Net Profit
39.671%
Sharpe Ratio
0.689
Sortino Ratio
0.56
Probabilistic Sharpe Ratio
38.500%
Loss Rate
33%
Win Rate
67%
Profit-Loss Ratio
1.43
Alpha
0.046
Beta
0.199
Annual Standard Deviation
0.095
Annual Variance
0.009
Information Ratio
-0.171
Tracking Error
0.175
Treynor Ratio
0.328
Total Fees
$79.94
Estimated Strategy Capacity
$73000000.00
Lowest Capacity Asset
QQQ RIWIV7K5Z9LX
Portfolio Turnover
2.54%
# region imports
from AlgorithmImports import *
import datetime
# endregion

class TrailingStopLoss(QCAlgorithm):
    
    def Initialize(self):
        self.SetStartDate(2018, 1, 1)
        self.SetEndDate(2021, 1, 1)
        self.SetCash(100000)
        self.qqq = self.AddEquity("QQQ", Resolution.Hour).Symbol
        
        self.entryTicket = None #tracks the ticket of the entry order
        self.stopMarketTicket = None #tracks the ticket of the exit order

        #tracks the fill TIME of the entry and exit order
        self.entryTime = datetime.datetime.min # initialized to current time
        self.stopMarketOrderFillTime = datetime.datetime.min # initialized to current time
        self.highestPrice = 0 #keep tracks of QQQs highest price

    def OnData(self, data):
        
        # wait 30 days since our last exit has passed:
        if (self.Time - self.stopMarketOrderFillTime).days < 30:
            return
        
        price = self.Securities[self.qqq].Price
        
        # only set limit order if we are not currently invested and there are not any active orders 
        # send a limit order for as 90% of our portfolio as shares of SPY as possible
        if not self.Portfolio.Invested and not self.Transactions.GetOpenOrders(self.qqq):
            quantity = self.CalculateOrderQuantity(self.qqq, 0.9) #allocates 90% of our portfolio to qqq
            self.entryTicket = self.LimitOrder(self.qqq, quantity, price, "Entry Order")
            self.entryTime = self.Time
        
        # if limit order is not filled within a day, we move up the limit price
        # to current price to increase chances of getting filled
        if (self.Time - self.entryTime).days > 1 and self.entryTicket.Status != OrderStatus.Filled:
            self.entryTime = self.Time #update the entry time to the current time
            updateFields = UpdateOrderFields()
            updateFields.LimitPrice = price #updates the limit price to the current price of QQQ
            self.entryTicket.Update(updateFields) #.update() excecutes the change in our limit order
        
        # move up the stop loss based on highest price
        if self.stopMarketTicket is not None and self.Portfolio.Invested: #check if there is currently and order AND we are invested
            # move up trailing stop price
            if price > self.highestPrice:
                self.highestPrice = price # update the price to the current price
                updateFields = UpdateOrderFields()
                updateFields.StopPrice = price * 0.95 # 5% trailing stop loss
                self.stopMarketTicket.Update(updateFields)
                #self.Debug(updateFields.StopPrice)

    def OnOrderEvent(self, orderEvent):
        
        if orderEvent.Status != OrderStatus.Filled: 
            return #an order can be: submitted, invalid, partially filled, etc. this makes sure we only care about the fully FILLED case
        
        # all stop loss order setup is done in on order event handler
        if self.entryTicket is not None and self.entryTicket.OrderId == orderEvent.OrderId:
            self.stopMarketTicket = self.StopMarketOrder(self.qqq, -self.entryTicket.Quantity, 0.95 * self.entryTicket.AverageFillPrice)
        
        # only sets stop loss if the entry order in the onData is filled 
        if self.stopMarketTicket is not None and self.stopMarketTicket.OrderId == orderEvent.OrderId: 
            self.stopMarketOrderFillTime = self.Time #ensures we will wait 30 days
            self.highestPrice = 0 #reset the highest price variable to 0