Overall Statistics |
Total Trades 15541 Average Win 0.15% Average Loss -0.14% Compounding Annual Return 17.984% Drawdown 32.400% Expectancy 0.312 Net Profit 3136.429% Sharpe Ratio 0.857 Probabilistic Sharpe Ratio 15.099% Loss Rate 36% Win Rate 64% Profit-Loss Ratio 1.06 Alpha 0 Beta 0 Annual Standard Deviation 0.202 Annual Variance 0.041 Information Ratio 0.857 Tracking Error 0.202 Treynor Ratio 0 Total Fees $27741.69 |
import numpy as np from scipy import stats import pandas as pd from Execution.ImmediateExecutionModel import ImmediateExecutionModel from Risk.NullRiskManagementModel import NullRiskManagementModel from QuantConnect.Indicators import * class A0001(QCAlgorithm): def Initialize(self): self.SetStartDate(2000, 1, 20) # Set Start Date # self.SetEndDate(2017, 1, 10) # Set Start Date self.SetCash(100000) # Set Strategy Cash self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin) # Benchmark self.benchmark = Symbol.Create('SPY', SecurityType.Equity, Market.USA) self.AddEquity('SPY', Resolution.Daily) # Data resolution self.UniverseSettings.Resolution = Resolution.Daily self.Settings.RebalancePortfolioOnInsightChanges = False self.Settings.RebalancePortfolioOnSecurityChanges = False self.SetUniverseSelection(QC500UniverseSelectionModel()) self.AddAlpha(ClenowMomentumAlphaModel()) self.SetExecution(ImmediateExecutionModel()) self.SetPortfolioConstruction(InsightWeightingPortfolioConstructionModel(self.DateRules.Every(DayOfWeek.Wednesday))) self.SetRiskManagement(NullRiskManagementModel()) from QuantConnect.Data.UniverseSelection import * from Selection.FundamentalUniverseSelectionModel import FundamentalUniverseSelectionModel from itertools import groupby from math import ceil class QC500UniverseSelectionModel(FundamentalUniverseSelectionModel): '''Defines the QC500 universe as a universe selection model for framework algorithm For details: https://github.com/QuantConnect/Lean/pull/1663''' def __init__(self, filterFineData = True, universeSettings = None, securityInitializer = None): '''Initializes a new default instance of the QC500UniverseSelectionModel''' super().__init__(filterFineData, universeSettings, securityInitializer) self.numberOfSymbolsCoarse = 1000 self.numberOfSymbolsFine = 500 self.dollarVolumeBySymbol = {} self.lastMonth = -1 self.referenceSMAperiod = 200 self.referenceTicker = "SPY" def SelectCoarse(self, algorithm, coarse): if algorithm.Time.month == self.lastMonth: return Universe.Unchanged # Do not invest in stocks generating errors messages in logs filteredErrors = [x for x in coarse if x.Symbol.Value != "VIAC" and x.Symbol.Value != "BEEM" and x.Symbol.Value != "LSI" and x.Symbol.Value != "IQV" and x.Symbol.Value != "GBT" and x.Symbol.Value != "VTRS" and x.Symbol.Value != "FUBO" and x.Symbol.Value != "SPCE" and x.Symbol.Value != "TFC" and x.Symbol.Value != "PEAK"] sortedByDollarVolume = sorted([x for x in filteredErrors 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 SelectFine(self, algorithm, fine): '''Performs fine selection for the QC500 constituents The company's headquarter must in the U.S. The stock must be traded on either the NYSE or NASDAQ At least half a year since its initial public offering The stock's market cap must be greater than 500 million''' sortedBySector = sorted([x for x in fine if x.CompanyReference.CountryId == "USA" and x.CompanyReference.PrimaryExchangeID in ["NYS","NAS"] and (algorithm.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 = algorithm.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) return [x.Symbol for x in sortedByDollarVolume[:self.numberOfSymbolsFine]] from QuantConnect.Algorithm.Framework.Alphas import * class ClenowMomentumAlphaModel(AlphaModel): def __init__(self, resolution = Resolution.Daily): self.resolution = resolution self.symbolDataBySymbol = {} self.linRegPeriod = 90 self.SMAperiod = 100 self.ATRperiod = 20 self.referenceSMAperiod = 200 self.referenceTicker = "SPY" self.riskFactor = 0.1/100 self.isPositionsRebalancingWeek = True self.previousStocksBought = [] def Update(self, algorithm, data): insights = [] topSelection = [] stocksToBuy = [] # Weekly update of insights every Wednesday + rebalancing every 2 weeks if algorithm.Time.isoweekday() != 3: return insights else: algorithm.Log("Weekly Wednesday investing start") for symbol, symbolData in self.symbolDataBySymbol.items(): security_data = algorithm.History([symbol], self.linRegPeriod, Resolution.Daily) # check if candle has close component(assume others?) if 'close' not in security_data.columns: algorithm.Log("History had no Close for %s"%symbol) security_data = None if security_data is not None: # we need enough for the np.diff which removes 1 from length if len(security_data.close.index) < self.linRegPeriod: algorithm.Log("Close test had too few or no values for %s with period %d"%(symbol, self.linRegPeriod)) security_data = None if security_data is None: continue else: y = np.log(security_data.close.values) x = np.arange(len(y)) slope, intercept, r_value, p_value, std_err = stats.linregress(x,y) momentum = ((1+slope)**252)*(r_value**2) if data.ContainsKey(symbolData.Symbol): if data[symbolData.Symbol] is None: continue else: weight = self.riskFactor*data[symbolData.Symbol].Close/symbolData.ATR.Current.Value selectedStock = [] selectedStock.append(symbolData) selectedStock.append(momentum) selectedStock.append(weight) topSelection.append(selectedStock) #Rank and keep 20% of stock based on momentum indicator topSelection = sorted(topSelection, key=lambda x: x[1], reverse=True) topSelection = topSelection[:int(20*len(topSelection)/100)] AvailablePorfolioPercent = 1 # buy the previous stock first or sell according to conditions # algorithm.Log("buying previous starting with % "+str(AvailablePorfolioPercent) ) for previousStockBought in self.previousStocksBought: for selectedStock in topSelection: if previousStockBought[0] == selectedStock[0] and data[selectedStock[0].Symbol].Close > selectedStock[0].SMA.Current.Value: if self.isPositionsRebalancingWeek == True: if selectedStock[2] < AvailablePorfolioPercent: # algorithm.Log("Rebalancing with fresh stock") AvailablePorfolioPercent = AvailablePorfolioPercent - selectedStock[2] stocksToBuy.append(selectedStock) # algorithm.Log(str(selectedStock[0].Symbol) + " / " + str(selectedStock[1]) + " / " + str(selectedStock[2]) ) # algorithm.Log("previous stock / available % "+str(AvailablePorfolioPercent)) else: if previousStockBought[2] < AvailablePorfolioPercent: # algorithm.Log("Rebalancing with previous stock") AvailablePorfolioPercent = AvailablePorfolioPercent - previousStockBought[2] stocksToBuy.append(previousStockBought) # algorithm.Log(str(previousStockBought[0].Symbol) + " / " + str(previousStockBought[1]) + " / " + str(previousStockBought[2]) ) # algorithm.Log("previous stock / available % "+str(AvailablePorfolioPercent)) topSelection.remove(selectedStock) elif data[selectedStock[0].Symbol].Close < selectedStock[0].SMA.Current.Value: topSelection.remove(selectedStock) # buy the rest with money left if bull market condition is ok SPYSymbol = algorithm.AddEquity(self.referenceTicker, Resolution.Daily) referenceHistory = algorithm.History([self.referenceTicker], self.referenceSMAperiod, Resolution.Daily) referenceSMA = algorithm.SMA(self.referenceTicker, self.referenceSMAperiod, Resolution.Daily) referencePrice = None if not referenceHistory.empty: for tuple in referenceHistory.loc[self.referenceTicker].itertuples(): referenceSMA.Update(tuple.Index, tuple.close) referencePrice = tuple.close if referencePrice < referenceSMA.Current.Value: algorithm.Log(str(referencePrice)+ " / "+ str(referenceSMA.Current.Value) + " / bear market detected") else: # algorithm.Log(str(referencePrice)+ " / "+ str(referenceSMA.Current.Value) + " / bull market validation") # algorithm.Log("buying extra starting with % "+str(AvailablePorfolioPercent)) for selectedStock in topSelection: if selectedStock[2] < AvailablePorfolioPercent: stocksToBuy.append(selectedStock) AvailablePorfolioPercent = AvailablePorfolioPercent - selectedStock[2] # algorithm.Log(str(selectedStock[0].Symbol) + " / " + str(selectedStock[1]) + " / " + str(selectedStock[2]) ) # algorithm.Log("fresh stock / available % "+str(AvailablePorfolioPercent)) else: break for stockToBuy in stocksToBuy: insights.append(Insight.Price(stockToBuy[0].Symbol, timedelta(days=1), InsightDirection.Up, None, 1.00, None, stockToBuy[2])) # magnitude, confidence, weight self.previousStocksBought = stocksToBuy if self.isPositionsRebalancingWeek == True: self.isPositionsRebalancingWeek = False else: self.isPositionsRebalancingWeek = True return insights def OnSecuritiesChanged(self, algorithm, changes): # clean up data for removed securities symbols = [x.Symbol for x in changes.RemovedSecurities] if len(symbols) > 0: for subscription in algorithm.SubscriptionManager.Subscriptions: if subscription.Symbol in symbols: self.symbolDataBySymbol.pop(subscription.Symbol, None) subscription.Consolidators.Clear() # initialize data for added securities addedSymbols = [ x.Symbol for x in changes.AddedSecurities if x.Symbol not in self.symbolDataBySymbol] if len(addedSymbols) == 0: return SMAhistory = algorithm.History(addedSymbols, self.SMAperiod, self.resolution) ATRhistory = algorithm.History(addedSymbols, self.ATRperiod, self.resolution) for symbol in addedSymbols: sma = algorithm.SMA(symbol, self.SMAperiod, self.resolution) if not SMAhistory.empty: ticker = SymbolCache.GetTicker(symbol) if ticker not in SMAhistory.index.levels[0]: Log.Trace(f'SMA added on securities changed: {ticker} not found in history data frame.') continue for tuple in SMAhistory.loc[ticker].itertuples(): sma.Update(tuple.Index, tuple.close) atr = algorithm.ATR(symbol, self.ATRperiod, MovingAverageType.Simple, self.resolution) if not ATRhistory.empty: ticker = SymbolCache.GetTicker(symbol) if ticker not in ATRhistory.index.levels[0]: Log.Trace(f'ATR added on securities changed: {ticker} not found in history data frame.') continue for tuple in ATRhistory.loc[ticker].itertuples(): bar = TradeBar(tuple.Index, symbol, tuple.open, tuple.high, tuple.low, tuple.close, tuple.volume) atr.Update(bar) self.symbolDataBySymbol[symbol] = SymbolData(symbol, sma, atr) class SymbolData: def __init__(self, symbol, sma, atr): self.Symbol = symbol self.SMA = sma self.ATR = atr