| Overall Statistics |
|
Total Trades 339 Average Win 1.79% Average Loss -1.06% Compounding Annual Return 35.478% Drawdown 8.400% Expectancy 0.868 Net Profit 389.411% Sharpe Ratio 2.249 Probabilistic Sharpe Ratio 99.315% Loss Rate 31% Win Rate 69% Profit-Loss Ratio 1.69 Alpha 0.233 Beta 0.439 Annual Standard Deviation 0.131 Annual Variance 0.017 Information Ratio 1.057 Tracking Error 0.144 Treynor Ratio 0.673 Total Fees $338.56 Estimated Strategy Capacity $310000.00 |
# detects bubbles then shorts
class Roboto(QCAlgorithm):
# portfolio configuration
FREE_CASH = 0.20 # adjust based on risk tolerance
STARTING_CASH = 1000
# 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(2016, 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))