Overall Statistics |
Total Trades 1746 Average Win 0.35% Average Loss -0.47% Compounding Annual Return 3.991% Drawdown 36.900% Expectancy 0.050 Net Profit 29.059% Sharpe Ratio 0.289 Probabilistic Sharpe Ratio 3.192% Loss Rate 40% Win Rate 60% Profit-Loss Ratio 0.76 Alpha 0 Beta 0 Annual Standard Deviation 0.254 Annual Variance 0.065 Information Ratio 0.289 Tracking Error 0.254 Treynor Ratio 0 Total Fees $9530.14 Estimated Strategy Capacity $1800000.00 Lowest Capacity Asset FUV WO2JC60VYY5H |
import numpy as np import pandas as pd from itertools import groupby from math import ceil class CalmAsparagusAnt(QCAlgorithm): def Initialize(self): self.SetStartDate(2015, 3, 11) # Set Start Date self.SetCash(1000000) # Set Strategy Cash self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin) ief = self.AddEquity("IEF", Resolution.Daily).Symbol tlt = self.AddEquity("TLT", Resolution.Daily).Symbol self.UniverseSettings.Resolution = Resolution.Daily self.BONDS = [ief, tlt] self.TARGET_SECURITIES = 25 self.TOP_ROE_QTY = 100 #First sort by ROE self.numberOfSymbolsCoarse = 3000 self.numberOfSymbolsFine = 1500 self.dollarVolumeBySymbol = {} self.activeUniverse = [] self.lastMonth = -1 self.trend_up = False # This is for the trend following filter self.SPY = self.AddEquity("SPY", Resolution.Daily).Symbol self.TF_LOOKBACK = 126 self.TF_CURRENT_LOOKBACK = 20 history = self.History(self.SPY, self.TF_LOOKBACK, Resolution.Daily) self.spy_ma50_slice = self.SMA(self.SPY, self.TF_CURRENT_LOOKBACK, Resolution.Daily) self.spy_ma200_slice = self.SMA(self.SPY, self.TF_LOOKBACK, Resolution.Daily) for tuple in history.loc[self.SPY].itertuples(): self.spy_ma50_slice.Update(tuple.Index, tuple.close) self.spy_ma200_slice.Update(tuple.Index, tuple.close) self.trend_up = self.spy_ma50_slice.Current.Value > self.spy_ma200_slice.Current.Value # This is for the determining momentum self.MOMENTUM_LOOKBACK_DAYS = 126 #Momentum lookback self.MOMENTUM_SKIP_DAYS = 2 # Initialize any other variables before being used self.stock_weights = pd.Series() self.bond_weights = pd.Series() # Should probably comment out the slippage and using the default # set_slippage(slippage.FixedSlippage(spread = 0.0)) # Create and attach pipeline for fetching all data # Schedule functions # Separate the stock selection from the execution for flexibility self.Schedule.On(self.DateRules.MonthEnd("SPY", 0), self.TimeRules.BeforeMarketClose(self.SPY, 0), self.SelectStocksAndSetWeights) self.Schedule.On(self.DateRules.EveryDay("SPY"), self.TimeRules.BeforeMarketClose(self.SPY, 0), self.RecordVars) self.lastMonth = -1 self.AddUniverse(self.Coarse, self.Fine) def OnData(self, data): pass def SelectStocksAndSetWeights(self): # Get pipeline output and select stocks #df = algo.pipeline_output('pipeline') ''' Pick a new Universe ?????? every week ''' current_holdings = [x.Key for x in self.Portfolio if x.Value.Invested] # Define our rule to open/hold positions # top momentum and don't open in a downturn but, if held, then keep rule = 'top_quality_momentum & (trend_up or (not trend_up & index in @current_holdings))' stocks_to_hold = self.activeUniverse self.trend_up = self.spy_ma50_slice.Current.Value > self.spy_ma200_slice.Current.Value if self.trend_up == False: return # Set desired stock weights # Equally weight stock_weight = 1.0 / (self.TARGET_SECURITIES) self.weights = {} for x in stocks_to_hold: self.weights[x] = stock_weight # Set desired bond weight # Open bond position to fill unused portfolio balance # But always have at least 1 'share' of bonds ### bond_weight = max(1.0 - context.stock_weights.sum(), stock_weight) / len(context.BONDS) bond_weight = (1.0 - stock_weight) / len(self.BONDS) for x in self.BONDS: self.weights[x] = bond_weight self.Trade() def Trade(self): makeInvestments = sorted([x for x in self.weights.keys()], key = lambda x: self.weights[x], reverse = False) total = 0 for stock in makeInvestments: weight = self.weights[stock] if weight == 0: self.Liquidate(stock) else: self.SetHoldings(stock, weight) total += weight self.Debug(total) def RecordVars(self): pass def OnSecuritiesChanged(self, changes): for security in changes.AddedSecurities: symbol = security.Symbol if symbol == self.SPY or symbol in self.BONDS: continue if symbol not in self.activeUniverse: self.activeUniverse.append(symbol) for security in changes.RemovedSecurities: symbol = security.Symbol self.Liquidate(symbol) if symbol in self.activeUniverse: self.activeUniverse.remove(symbol) def Coarse(self, coarse): if self.lastMonth == self.Time.month: return Universe.Unchanged sortedByDollarVolume = sorted([x for x in coarse if x.HasFundamentalData and x.Volume > 0 and x.Price > 0], key = lambda x: x.DollarVolume, reverse=True)[:self.numberOfSymbolsCoarse] self.dollarVolumeBySymbol = {x.Symbol:x.DollarVolume for x in sortedByDollarVolume} # If no security has met the QC500 criteria, the universe is unchanged. # A new selection will be attempted on the next trading day as self.lastMonth is not updated if len(self.dollarVolumeBySymbol) == 0: return Universe.Unchanged # return the symbol objects our sorted collection return list(self.dollarVolumeBySymbol.keys()) def Fine(self, fine): sortedBySector = sorted([x for x in fine if x.CompanyReference.CountryId == "USA" and x.CompanyReference.PrimaryExchangeID in ["NYS","NAS"] and (self.Time - x.SecurityReference.IPODate).days > 180 and x.MarketCap > 5e8], key = lambda x: x.CompanyReference.IndustryTemplateCode) count = len(sortedBySector) # If no security has met the QC500 criteria, the universe is unchanged. # A new selection will be attempted on the next trading day as self.lastMonth is not updated if count == 0: return Universe.Unchanged # Update self.lastMonth after all QC500 criteria checks passed self.lastMonth = self.Time.month percent = self.numberOfSymbolsFine / count sortedByDollarVolume = [] # select stocks with top dollar volume in every single sector for code, g in groupby(sortedBySector, lambda x: x.CompanyReference.IndustryTemplateCode): y = sorted(g, key = lambda x: self.dollarVolumeBySymbol[x.Symbol], reverse = True) c = ceil(len(y) * percent) sortedByDollarVolume.extend(y[:c]) sortedByDollarVolume = sorted(sortedByDollarVolume, key = lambda x: self.dollarVolumeBySymbol[x.Symbol], reverse=True) Q1500US = [x for x in sortedByDollarVolume[:self.numberOfSymbolsFine]] df = {} stocks = [] df["Stocks"] = [] df["Cash Return"] = [] df["FcfYield"] = [] df["ROIC"] = [] df["LtdToEq"] = [] for x in Q1500US: symbol = x.Symbol stocks.append(symbol) df["Stocks"].append(symbol) df["Cash Return"].append(x.ValuationRatios.CashReturn) df["FcfYield"].append(x.ValuationRatios.FCFYield) df["ROIC"].append(x.OperationRatios.ROIC.ThreeMonths) df["LtdToEq"].append(x.OperationRatios.LongTermDebtEquityRatio.ThreeMonths) df = pd.DataFrame.from_dict(df) df["Cash Return"] = df["Cash Return"].rank() df["FcfYield"] = df["FcfYield"].rank() df["ROIC"] = df["ROIC"].rank() df["LtdToEq"] = df["LtdToEq"].rank() df["Value"] = (df["Cash Return"] + df["FcfYield"]).rank() df["Quality"] = df["ROIC"] + df["LtdToEq"] + df["Value"] top_quality = df.sort_values(by=["Quality"]) top_quality = top_quality["Stocks"][:self.TOP_ROE_QTY] topStocks = [x for x in top_quality] history = self.History(topStocks, self.MOMENTUM_LOOKBACK_DAYS+self.MOMENTUM_SKIP_DAYS, Resolution.Daily) returns_overall = {} returns_recent = {} for symbol in topStocks: mompLong = MomentumPercent(self.MOMENTUM_LOOKBACK_DAYS+self.MOMENTUM_SKIP_DAYS) mompShort = MomentumPercent(self.MOMENTUM_SKIP_DAYS) for tuple in history.loc[symbol].itertuples(): mompLong.Update(tuple.Index, tuple.close) mompShort.Update(tuple.Index, tuple.close) returns_overall[symbol] = mompLong.Current.Value returns_recent[symbol] = mompShort.Current.Value final = sorted(topStocks, key = lambda x: returns_overall[x] - returns_overall[x], reverse = True) final = final[:self.TARGET_SECURITIES] return final