Overall Statistics
Total Trades
730
Average Win
4.11%
Average Loss
-4.47%
Compounding Annual Return
1578.626%
Drawdown
44.400%
Expectancy
0.220
Net Profit
1596.784%
Sharpe Ratio
16.345
Probabilistic Sharpe Ratio
96.370%
Loss Rate
36%
Win Rate
64%
Profit-Loss Ratio
0.92
Alpha
0
Beta
0
Annual Standard Deviation
1.133
Annual Variance
1.284
Information Ratio
16.345
Tracking Error
1.133
Treynor Ratio
0
Total Fees
$12861.01
Estimated Strategy Capacity
$140000.00
import pandas
import json
import io
import constants

STOCK_LIMIT = 3
MINIMUM_VOLUME = 100000
MARGIN_REQUIREMENT = 0.5
SHORT_LOSS_TOLERANCE = 0.33
LONG_LOSS_TOLERANCE = 0.02

EQUAL_WEIGHT = False
STOP_LOSS = False
BUY_INCREASE = False

HEALTH_TIMER = 5
LOSS_TOLERANCE = 0.1

class BurtonTrade(QCAlgorithm):
    
    def Initialize(self):
        self.SetCash(10000)
        self.SetStartDate(2019, 3, 16)
        self.SetEndDate(2020, 3, 15)
        
        self.AddEquity("SPY")
        self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)
        self.UniverseSettings.ExtendedMarketHours = True
        self.DefaultOrderProperties.TimeInForce = TimeInForce.Day
        self.AddUniverseSelection(ScheduledUniverseSelectionModel(self.DateRules.EveryDay("SPY"), self.TimeRules.At(9, 25), self.TopMovers))
        
        self.gainers = []
        self.openShortOrders = {}
        self.closeShortOrders = {}
        self.openLongOrders = {}
        self.closeLongOrders = {}
        
        self.plotDelta = round(((self.EndDate.replace(tzinfo=None) - self.StartDate.replace(tzinfo=None)).days) * 1440 / 4000)
        self.benchmarkPerformance = self.Portfolio.TotalPortfolioValue
        self.lastPortfolioHealth = self.Portfolio.TotalPortfolioValue
        self.lastBenchmarkValue = None

        self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.AfterMarketOpen("SPY", 0), self.PlaceTrades)
        self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.BeforeMarketClose("SPY", 5), self.CloseTrades)
        self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.Every(timedelta(minutes=self.plotDelta or 1)), self.PlotData)
        self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.Every(timedelta(minutes=HEALTH_TIMER)), self.PortfolioHealth)
        
        if not self.LiveMode: self.backtestSymbols = pandas.read_csv(io.StringIO(self.Download(constants.BACKTEST_URL)))
        

    def TopMovers(self, dateTime):
        symbols = []
        
        if self.LiveMode:
            movers = json.loads(self.Download(constants.BENZINGA_URL + str(STOCK_LIMIT * 2)))
            gainers = sorted(movers["result"]["gainers"], key=lambda x: x["changePercent"], reverse=True)
        
            for gainer in gainers:
                if gainer["volume"] > MINIMUM_VOLUME: symbols.append(Symbol.Create(gainer["symbol"], SecurityType.Equity, Market.USA))
        
        else:
            data = self.backtestSymbols
            
            for symbol in data[data.columns[data.columns.get_loc(dateTime.strftime("%F"))]].to_list():
                symbols.append(Symbol.Create(symbol.replace(" ", ""), SecurityType.Equity, Market.USA))
        
        return symbols[:STOCK_LIMIT]
    
    
    def PlaceTrades(self):
        self.lastPortfolioHealth = self.Portfolio.TotalPortfolioValue
        
        for kvp in self.ActiveSecurities:
            if kvp.Key == "SPY": continue

            symbol = kvp.Key
            security = kvp.Value
            
            opening = security.Price
            try: close = self.History(symbol, 1, Resolution.Daily)['close'][0]
            except: continue
        
            self.Debug("Symbol: " + str(symbol) + " Close: " + str(close) + " Opening: " + str(opening))
            
            gain = int(round(((opening - close) / (close or 1)) * 100))
            if gain > 10: self.gainers.append(SecurityWeight(security, gain))
        
        totalGain = sum([x.gain for x in self.gainers])
        self.Debug("Total Gain: " + str(totalGain))
        
        for gainer in self.gainers:
            self.Debug(str(security.Symbol) + " Gain: " + str(gainer.gain) + " Percent: " + str(gainer.gain / totalGain))
            security = gainer.security
            symbol = security.Symbol
            weight = ((gainer.gain / totalGain) if not EQUAL_WEIGHT else (1 / STOCK_LIMIT)) * (1 - MARGIN_REQUIREMENT)
            
            quantity = self.CalculateOrderQuantity(symbol, weight)
            openShort = self.LimitOrder(symbol, -quantity, security.Price * 1.05)
            
            self.openShortOrders[str(openShort.OrderId)] = weight
    
    
    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status == OrderStatus.Filled:
            symbol = orderEvent.Symbol
            orderId = str(orderEvent.OrderId)
            fillPrice = orderEvent.FillPrice
            fillQuantity = abs(orderEvent.FillQuantity)
            
            if STOP_LOSS and orderId in self.openShortOrders:
                gain = next((x for x in self.gainers if x.security.Symbol == symbol), None).gain
                #closeShort = self.StopMarketOrder(symbol, fillQuantity, fillPrice * min((1 + SHORT_LOSS_TOLERANCE * gain / 100), 1.5))
                #closeShort = self.StopMarketOrder(symbol, fillQuantity, fillPrice * (1 + SHORT_LOSS_TOLERANCE * gain / 100))
                stop = fillPrice * (1 + SHORT_LOSS_TOLERANCE * gain / 100)
                closeShort = self.StopLimitOrder(symbol, fillQuantity, stop, stop * 1.1)

                
                self.Debug("Short Loss - Symbol: " + str(symbol) +  " Gain: " + str(gain) + " Buy Price: " + str(fillPrice) + " Sell Price: " + str(fillPrice * (1 + SHORT_LOSS_TOLERANCE * gain / 100)))
                # todo figure out a good, max stopping point (i.e. if it goes up 1000%, no? -- have a maximum gain, that min thing might also be a problem)
                # todo stop limit order?
                
                self.closeShortOrders[str(closeShort.OrderId)] = self.openShortOrders[orderId]
            
            if BUY_INCREASE: 
                if orderId in self.closeShortOrders:
                    quantity = self.CalculateOrderQuantity(symbol, self.closeShortOrders[orderId])
                    openLong = self.LimitOrder(symbol, quantity, self.Securities[symbol].Price)
                    
                    self.openLongOrders[str(openLong.OrderId)] = openLong
                    self.Debug("Sold Short - Symbol: " + str(symbol) + " Price: " + str(fillPrice))
                
                elif orderId in self.openLongOrders:
                    self.Debug("Long Order - Symbol: " + str(symbol) + " Buy Price: " + str(fillPrice))

                    closeLong = self.StopLimitOrder(symbol, -fillQuantity, fillPrice * (1 - LONG_LOSS_TOLERANCE), fillPrice * (1 - LONG_LOSS_TOLERANCE * 2))
                    
                    self.closeLongOrders[str(symbol)] = CloseLongPosition(closeLong, fillPrice)
                    
                elif next((x for x in self.closeLongOrders.values() if str(x.closeLong.OrderId) == orderId), None) is not None:
                    self.Debug("Long Order - Symbol: " + str(symbol) + " Sold Price: " + str(fillPrice))

                    del self.closeLongOrders[str(symbol)]
    
    
    def OnData(self, data):
        for symbol, longPosition in self.closeLongOrders.items():
            currentPrice = self.Securities[symbol].Close
            
            if currentPrice > longPosition.highestPrice:
                self.Debug("Updating Order for Symbol: " + str(symbol) + " at Price: " + str(currentPrice))
                updateOrder = UpdateOrderFields()
                updateOrder.StopPrice = currentPrice * (1 - LONG_LOSS_TOLERANCE)
                updateOrder.LimitPrice = currentPrice * (1 - LONG_LOSS_TOLERANCE * 2)
                
                longPosition.highestPrice = currentPrice
                longPosition.closeLong.Update(updateOrder)
    
    
    def CloseTrades(self):
        self.gainers.clear()
        self.openShortOrders.clear()
        self.closeShortOrders.clear()
        self.closeLongOrders.clear()
        self.Liquidate()
        
    
    def PlotData(self):
        benchmark = self.Securities["SPY"].Price
        
        if benchmark == 0: return
        
        if self.lastBenchmarkValue is not None: self.benchmarkPerformance = self.benchmarkPerformance * (benchmark / self.lastBenchmarkValue)
        self.lastBenchmarkValue = benchmark
        
        self.Plot("Performance", "BurtonTrade", self.Portfolio.TotalPortfolioValue)
        self.Plot("Performance", "S&P 500", self.benchmarkPerformance)
    
    
    def PortfolioHealth(self):
        if self.Portfolio.TotalPortfolioValue < self.lastPortfolioHealth * (1 - LOSS_TOLERANCE): 
            for kvp in self.ActiveSecurities: 
                symbol = kvp.Key
                security = kvp.Value
                
                if security.Price > self.Portfolio[symbol].AveragePrice: self.Liquidate(symbol)


class SecurityWeight:
    def __init__(self, security, gain):
        self.security = security
        self.gain = gain


class CloseLongPosition:
    def __init__(self, closeLong, highestPrice):
        self.closeLong = closeLong
        self.highestPrice = highestPrice
BENZINGA_API_KEY = ""
BENZINGA_URL = "https://api.benzinga.com/api/v1/market/movers?session=PRE_MARKET&apikey=" + BENZINGA_API_KEY + "&maxResults="

BACKTEST_URL_LARGE = "https://tinyurl.com/uece8"
BACKTEST_URL_SMALL = "https://tinyurl.com/53r2hmj4"
BACKTEST_URL = "https://tinyurl.com/e4h7kwaw"