Overall Statistics
Total Trades
1809
Average Win
0.48%
Average Loss
-0.48%
Compounding Annual Return
53.434%
Drawdown
34.200%
Expectancy
0.567
Net Profit
1308.100%
Sharpe Ratio
1.642
Probabilistic Sharpe Ratio
81.503%
Loss Rate
21%
Win Rate
79%
Profit-Loss Ratio
0.99
Alpha
0.312
Beta
1.476
Annual Standard Deviation
0.297
Annual Variance
0.088
Information Ratio
1.99
Tracking Error
0.185
Treynor Ratio
0.33
Total Fees
$7246.28
Estimated Strategy Capacity
$1400.00
# detects bubbles then shorts
class Roboto(QCAlgorithm):
    
    # portfolio configuration
    FREE_CASH = 0.20 # adjust based on risk tolerance
    STARTING_CASH = 100000
    
    # signal configuration
    FAST = 4 # 10 # lower values, higher risk, higher returns
    SLOW = 30 # 54
    MAGNITUDE = 2.0
    
    # position configuration
    CUT_LOSS = -0.1 # -0.1% optimal
    TAKE_PROFIT = 0.55 # 0.55 optimal
    REBALANCE_BP = 0.30 # adjust based on risk tolerance
    MAX_POSITION_SIZE = -0.075 # big effect on total returns (more negative values = larger returns)
    MAX_POSITION_AGE = 45 # 45 days optimal
    MIN_TIME_IN_UNIVERSE = 730 # (2 years) 
    
    # liquidity configuration
    MIN_VOLUME = 1000000
    MIN_DOLLAR_VOLUME = 100000
    OF_TOTAL_DV = 0.05
    
    class SecurityData:
        
        def __init__(self, symbol, history):
            self.symbol = symbol
            self.fast = ExponentialMovingAverage(Roboto.FAST)
            self.slow = ExponentialMovingAverage(Roboto.SLOW)
            self.vol = ExponentialMovingAverage(Roboto.SLOW)
            
            self.isBubble = False
            self.ratio = 0
            
            for bar in history.itertuples():
                self.fast.Update(bar.Index[1], bar.close)
                self.slow.Update(bar.Index[1], bar.close)
                self.vol.Update(bar.Index[1], ((bar.open + bar.close)/2.0) * bar.volume) # approx. dollar volume
        
        def update(self, time, price, volume):
            if self.fast.Update(time, price) and self.slow.Update(time, price) and self.vol.Update(time, volume):
                self.isBubble = (self.fast.Current.Value > (Roboto.MAGNITUDE * self.slow.Current.Value)) and (price > self.slow.Current.Value)
                self.ratio = self.fast.Current.Value/self.slow.Current.Value
            

    def Initialize(self):
        self.Debug("Roboto")
        self.SetTimeZone("America/New_York")
        self.SetBrokerageModel(BrokerageName.AlphaStreams)
        
        # backtest dates 
        self.SetStartDate(2015, 1, 1)  
        #self.SetEndDate(2017, 1, 1)
        
        # portfolio 
        self.Settings.FreePortfolioValuePercentage = Roboto.FREE_CASH
        self.SetCash(Roboto.STARTING_CASH)
        
        # universe
        self.UniverseSettings.Resolution = Resolution.Hour
        self.UniverseSettings.MinimumTimeInUniverse = Roboto.MIN_TIME_IN_UNIVERSE
        self.AddUniverse(self.UniverseSelection)
        
        self.universe = {} # contains all tracked securities in the universe
        self.expiry = {} # contains age of position
        self.bp = 1.00 # buying power
        self.open = [] # positions to open based on signal
        
        self.STK_IN = self.AddEquity("TQQQ", Resolution.Hour)
    
    
    # open short positions which meet criteria
    # is called daily at 10:00AM, will fill orders at 11:00AM
    def DailyAt10(self):
        #self.Debug("{} 10:00".format(self.Time))
        
        num_pos = len([f.Key for f in self.ActiveSecurities if f.Value.Invested])
        # open new positions
        for symb in self.open:
            dynamic = -0.25/(num_pos + 1.00)
            target = max(Roboto.MAX_POSITION_SIZE, dynamic) # max of negative
            tag = "New pos. target allocation {}".format(round(target, 4))
            self.Short(symb, target, tag)
                
        self.open = []
        
        # set some portion of portfolio to hold bullish index
        remaining_allocation = max(1.00 - self.REBALANCE_BP - (num_pos * (-1 * Roboto.MAX_POSITION_SIZE)), Roboto.MAX_POSITION_SIZE)
        self.SetHoldings([PortfolioTarget(self.STK_IN.Symbol, remaining_allocation)])
        
        self.bp = self.Portfolio.MarginRemaining/self.Portfolio.TotalPortfolioValue
        self.Plot("Buying Power", "Val", self.bp)
        self.Plot("# Positions", "Val", num_pos)
        
    
    # manage portfolio based on return, age, and buying power
    def Rebalance(self):
        
        closing = set()
        invested = [f.Key for f in self.ActiveSecurities if (f.Value.Invested and (f.Value.Symbol != self.STK_IN.Symbol))]
        for symb in invested:
            holding = self.Portfolio[symb]
       
            # exit old positions
            if (self.Time - self.expiry[holding.Symbol]).days > Roboto.MAX_POSITION_AGE:
                self.Debug("{} Expired {} at {} days, {}%".format(self.Time, holding.Symbol, (self.Time - self.expiry[holding.Symbol]).days, round(holding.UnrealizedProfitPercent, 4) * 100))
                self.CancelAllOrders(holding.Symbol)
                tag = "Expired, age {} days, result {}%".format((self.Time - self.expiry[holding.Symbol]).days, round(holding.UnrealizedProfitPercent, 4) * 100)
                self.RapidExit(holding.Symbol, tag)
                closing.add(holding.Symbol)
                
            # exit positions with a large loss
            elif (holding.UnrealizedProfitPercent < Roboto.CUT_LOSS) and ((self.Time - self.expiry[symb]).days > 1):
                self.Debug("{} Cutting Losses on {} at {} days, {}%".format(self.Time, holding.Symbol, (self.Time - self.expiry[holding.Symbol]).days, round(holding.UnrealizedProfitPercent, 4) * 100))
                self.CancelAllOrders(holding.Symbol)
                tag = "Cutting loss, age {} days, result {}%".format((self.Time - self.expiry[holding.Symbol]).days, round(holding.UnrealizedProfitPercent, 4) * 100)
                self.RapidExit(holding.Symbol, tag)
                closing.add(holding.Symbol)
        
            # exit positions with a large profit
            elif (holding.UnrealizedProfitPercent > Roboto.TAKE_PROFIT):
                self.Debug("{} Taking Profit on {} at {} days, {}%".format(self.Time, holding.Symbol, (self.Time - self.expiry[holding.Symbol]).days, round(holding.UnrealizedProfitPercent, 4) * 100))
                self.CancelAllOrders(holding.Symbol)
                tag = "Taking profit, age {} days, result {}%".format((self.Time - self.expiry[holding.Symbol]).days, round(holding.UnrealizedProfitPercent, 4) * 100)
                self.Cover(holding.Symbol, tag)
                closing.add(holding.Symbol)
                
    
        # liquidate most profitable position if buying power is too low
        self.bp = self.Portfolio.MarginRemaining/self.Portfolio.TotalPortfolioValue
        if self.bp < Roboto.REBALANCE_BP:
            self.Debug("{} Rebalancing, buying power: {}".format(self.Time, self.bp))
     
            class Factor:
                def __init__(self, holding):
                    self.holding = holding
                    self.unrealized = self.holding.UnrealizedProfitPercent
            
            track = {}
            for symb in invested:
                holding = self.Portfolio[symb]
                track[holding.Symbol] = Factor(holding)
            
            values = list(set(track.values()) - set(closing)) # remove any symbols already closing
            if len(values) > 0:
                values.sort(key=lambda f: f.unrealized, reverse=True)
                self.Debug("{} Liquidating {} @ {}".format(self.Time, values[0].holding.Symbol, values[0].unrealized))
                self.CancelAllOrders(values[0].holding.Symbol)
                tag = "Liquidating, age {} days, result {}%".format((self.Time - self.expiry[holding.Symbol]).days, round(holding.UnrealizedProfitPercent, 4) * 100)
                self.RapidExit(values[0].holding.Symbol, tag)
            else:
                self.Error("{} Unable to liquidate: {} {}".format(self.Time, len(values), len(closing)))
        
                
    # runs at hourly resolution when securities are in universe
    def OnData(self, slice):
        self.Rebalance()
        
        # at 10:00 AM daily
        if self.Time.hour == 10:
            self.DailyAt10()
        

    # is called whenever the universe changes
    def OnSecuritiesChanged(self, changes):
        #self.Debug("{} Securities Changed".format(self.Time))
        
        self.open = []
        for security in changes.AddedSecurities:
            self.CancelAllOrders(security.Symbol)
            if not security.Invested and (security.Symbol != self.STK_IN.Symbol):
                self.Debug("{} Added Security {}".format(self.Time, security.Symbol))
                self.open.append(security.Symbol)
    
            else:
                pass
                #self.Error("{} Adding security already invested in {}".format(self.Time, security.Symbol))
            

    def UniverseSelection(self, coarse):
        #self.Debug("{} Universe Selection".format(self.Time))
        
        # apply hard mandatory security filters
        hard = list(filter(lambda c: (c.Market == "usa") and (c.Price > 5) and (c.HasFundamentalData), coarse)) 
        
        # save coarse fundamentals to dict
        current = {}
        for h in hard:
            current[h.Symbol] = h
        
        # apply soft filtering criteria
        soft = list(filter(lambda h: (h.Volume > Roboto.MIN_VOLUME) and (h.DollarVolume > Roboto.MIN_DOLLAR_VOLUME), hard))
        
        # add new symbols to universe
        for s in soft:
            if (s.Symbol not in self.universe):
                history = self.History(s.Symbol, Roboto.SLOW, Resolution.Daily)
                self.universe[s.Symbol] = Roboto.SecurityData(s.Symbol, history)
                
        # update security data objs and remove any securities no longer in universe
        new = {}
        for symb in self.universe:
            sd = self.universe[symb]
            if symb in current:
                sd.update(current[symb].EndTime, current[symb].AdjustedPrice, current[symb].DollarVolume)
                new[symb] = sd
                
        self.universe = new
        remaining = list(filter(lambda sd: sd.isBubble, self.universe.values()))
        remaining.sort(key = lambda sd: sd.ratio, reverse = True)
        selected = [ sd.symbol for sd in remaining ]
        
        return selected
        
        
    def CancelAllOrders(self, symbol):
        #self.Debug("{} Cancelling all orders for {}".format(self.Time, symbol))
        openOrders = self.Transactions.CancelOpenOrders(symbol)
        for oo in openOrders:
            if not (oo.Status == OrderStatus.CancelPending):
                r = oo.Cancel()
                if not r.IsSuccess:
                    self.Error("{} Failed to cancel open order {} of {} for reason: {}, {}".format(self.Time, oo.Quantity, oo.Symbol, r.ErrorMessage, r.ErrorCode))
                    

    def Short(self, symbol, target, tag = "No Tag Provided"):
        q = int(self.CalculateOrderQuantity(symbol, target))
        price = float(self.Securities[symbol].Close)
        
        odv = float(abs(q * price)) # order dollar volume
        rdv = float(Roboto.OF_TOTAL_DV * self.universe[symbol].vol.Current.Value) # securities volume (historical EMA - Roboto.SLOW)
  
        # skip any securities with daily volume less than Roboto.OF_TOTAL_DV of order volume
        if (odv < rdv):
            if q < 0:
                #self.Debug("{} Short {} {} @ {}".format(self.Time, q, symbol, price))
                self.EmitInsights(Insight.Price(symbol, timedelta(days = Roboto.MAX_POSITION_AGE), InsightDirection.Down, None, None, None, target))
                self.LimitOrder(symbol, q, price, tag)
            else:
                if q != 0:
                    self.Error("{} Received positive quantity for short order: {} {} @ {} (Target: {})".format(self.Time, q, symbol, price, target))
        else:
            self.Debug("{} Skipping {}, poor liquidity: {} > {}".format(self.Time, symbol, odv, rdv))


    def Cover(self, symbol, tag = "No Tag Provided"):
        q = -1 * int(self.Portfolio[symbol].Quantity)
        price = self.Securities[symbol].Close
        if q > 0:
            #self.Debug("{} Cover {} {} @ {}".format(self.Time, q, symbol, price))
            self.EmitInsights(Insight.Price(symbol, timedelta(days = Roboto.MAX_POSITION_AGE), InsightDirection.Flat, None, None, None, 0.00))
            self.LimitOrder(symbol, q, price, tag)
        else:
            if q != 0:
                self.Error("{} Received negative quantity for cover order: {} {} @ {}".format(self.Time, q, symbol, price))
        

    def RapidExit(self, symbol, tag = "No Tag Provided"):
        q = -1 * int(self.Portfolio[symbol].Quantity)
        if q > 0:
            #self.Debug("{} Rapid Exit {} {}".format(self.Time, q, symbol))
            self.EmitInsights(Insight.Price(symbol, timedelta(days = Roboto.MAX_POSITION_AGE), InsightDirection.Flat, None, None, None, 0.00))
            self.MarketOrder(symbol, q, False, tag)
        else:
            if q != 0:
                self.Error("{} Received negative quantity for rapid exit order: {} {}".format(self.Time, q, symbol))
        
        
    def OnOrderEvent(self, orderEvent):
        if orderEvent.Status == OrderStatus.Filled:
            order = self.Transactions.GetOrderById(orderEvent.OrderId)
            #self.Debug("{} Filled {} of {} at {}".format(self.Time, order.Quantity, order.Symbol, order.Price))
            
            # if completely liquidating position, stop tracking position age
            if not self.Portfolio[order.Symbol].Invested:
                try:
                    del self.expiry[order.Symbol]
                    #self.Debug("{} No longer tracking {}".format(self.Time, order.Symbol))
                except Error:
                    self.Error("{} Key deletion failed for {}".format(self.Time, order.Symbol))
            
            # if position is completely new, start tracking position age
            else:
                if (order.Symbol not in self.expiry):
                    self.expiry[order.Symbol] = self.Time
                else:
                    pass
                    #self.Debug("{} Key already existed for {}".format(self.Time, order.Symbol))