Overall Statistics |
Total Trades 1433 Average Win 0.45% Average Loss -0.41% Compounding Annual Return 38.309% Drawdown 13.600% Expectancy 0.645 Net Profit 641.632% Sharpe Ratio 2.289 Probabilistic Sharpe Ratio 99.651% Loss Rate 21% Win Rate 79% Profit-Loss Ratio 1.10 Alpha 0.264 Beta 0.466 Annual Standard Deviation 0.139 Annual Variance 0.019 Information Ratio 1.373 Tracking Error 0.146 Treynor Ratio 0.685 Total Fees $3999.77 Estimated Strategy Capacity $46000.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("QQQ", 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))