Overall Statistics |
Total Trades 4565 Average Win 0.11% Average Loss -0.12% Compounding Annual Return 23.000% Drawdown 25.600% Expectancy 0.235 Net Profit 102.489% Sharpe Ratio 1.035 Probabilistic Sharpe Ratio 44.974% Loss Rate 35% Win Rate 65% Profit-Loss Ratio 0.90 Alpha 0 Beta 0 Annual Standard Deviation 0.211 Annual Variance 0.044 Information Ratio 1.035 Tracking Error 0.211 Treynor Ratio 0 Total Fees $4605.02 Estimated Strategy Capacity $9500000.00 Lowest Capacity Asset DISCB TAHT8L1LVDR9 |
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): '''Initializes a new default instance of the QC500UniverseSelectionModel''' super().__init__(filterFineData, universeSettings) self.numberOfSymbolsCoarse = 1000 self.numberOfSymbolsFine = 500 self.dollarVolumeBySymbol = {} self.lastMonth = -1 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 clr import AddReference AddReference("System") AddReference("QuantConnect.Algorithm") AddReference("QuantConnect.Common") from System import * from QuantConnect import * from QuantConnect.Algorithm import * from QC500UniverseSelectionModel import QC500UniverseSelectionModel from ClenowMomentumAlphaModel import ClenowMomentumAlphaModel from Execution.ImmediateExecutionModel import ImmediateExecutionModel from Risk.NullRiskManagementModel import NullRiskManagementModel from QuantConnect.Indicators import * class A0001(QCAlgorithm): def Initialize(self): self.SetStartDate(2018, 1, 1) # Set Start Date # self.SetEndDate(2021, 1, 10) # Set End 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) self.referenceSMAperiod = 200 self.referenceTicker = "SPY" self.referenceSMA = self.SMA(self.referenceTicker, self.referenceSMAperiod, Resolution.Daily) self.linRegPeriod = 90 self.SMAperiod = 100 self.ATRperiod = 20 self.riskFactor = 0.1/100 # Data resolution self.UniverseSettings.Resolution = Resolution.Daily self.Settings.RebalancePortfolioOnInsightChanges = False self.Settings.RebalancePortfolioOnSecurityChanges = False self.SetUniverseSelection(QC500UniverseSelectionModel()) self.AddAlpha(ClenowMomentumAlphaModel(Resolution.Daily, self.linRegPeriod, self.SMAperiod, self.ATRperiod, self.referenceSMAperiod, self.referenceTicker, self.referenceSMA, self.riskFactor)) self.SetExecution(ImmediateExecutionModel()) self.SetPortfolioConstruction(InsightWeightingPortfolioConstructionModel(self.DateRules.Every(DayOfWeek.Wednesday))) self.SetRiskManagement(NullRiskManagementModel())
from collections import deque from datetime import datetime, timedelta import numpy as np from scipy import stats import pandas as pd from QuantConnect.Algorithm.Framework.Alphas import * class ClenowMomentumAlphaModel(AlphaModel): def __init__(self, resolution, linRegPeriod, SMAperiod, ATRperiod, referenceSMAperiod, referenceTicker, referenceSMA, riskFactor): self.resolution = resolution self.symbolDataBySymbol = {} self.linRegPeriod = linRegPeriod self.SMAperiod = SMAperiod self.ATRperiod = ATRperiod self.referenceSMAperiod = referenceSMAperiod self.referenceTicker = referenceTicker self.referenceSMA = referenceSMA self.referenceSymbolData = None self.riskFactor = riskFactor self.isPositionsRebalancingWeek = True self.previousStocksBought = [] self.referenceSMAisSet = False def Update(self, algorithm, data): insights = [] topSelection = [] stocksToBuy = [] if algorithm.Time.isoweekday() != 3: return insights else: # Weekly update of insights every Wednesday for symbol, symbolData in self.symbolDataBySymbol.items(): if data.ContainsKey(symbolData.Symbol): if data[symbolData.Symbol] is None: continue else: selectedStock = [] selectedStock.append(symbolData) selectedStock.append(symbolData.ClenowMomentum.Value) selectedStock.append(self.riskFactor*data[symbolData.Symbol].Close/symbolData.ATR.Current.Value) 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)] # Setup portfolio available % AvailablePorfolioPercent = 1 # buy the previous stock first or sell according to conditions 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: # Every positions rebalancing week, update ATR value for last week stocks if self.isPositionsRebalancingWeek == True: if selectedStock[2] < AvailablePorfolioPercent: AvailablePorfolioPercent = AvailablePorfolioPercent - selectedStock[2] stocksToBuy.append(selectedStock) else: # Every other week, keep the same ATR for positions if previousStockBought[2] < AvailablePorfolioPercent: AvailablePorfolioPercent = AvailablePorfolioPercent - previousStockBought[2] stocksToBuy.append(previousStockBought) topSelection.remove(selectedStock) elif data[selectedStock[0].Symbol].Close < selectedStock[0].SMA.Current.Value: topSelection.remove(selectedStock) # If market reference > referenceSMA -> bullish market -> buy more stock if data.ContainsKey(self.referenceSymbolData.Symbol): if data[self.referenceSymbolData.Symbol] is not None: if data[self.referenceSymbolData.Symbol].Close > self.referenceSymbolData.referenceSMA.Current.Value: for selectedStock in topSelection: if selectedStock[2] < AvailablePorfolioPercent: stocksToBuy.append(selectedStock) AvailablePorfolioPercent = AvailablePorfolioPercent - selectedStock[2] else: break for stockToBuy in stocksToBuy: insights.append(Insight.Price(stockToBuy[0].Symbol, timedelta(days=1), InsightDirection.Up, None, 1.00, None, stockToBuy[2])) self.previousStocksBought = stocksToBuy if self.isPositionsRebalancingWeek == True: self.isPositionsRebalancingWeek = False else: self.isPositionsRebalancingWeek = True return insights def OnSecuritiesChanged(self, algorithm, changes): # Initialize referenceTicker SMA once if self.referenceSMAisSet == False: history = algorithm.History([self.referenceTicker], max(self.SMAperiod, self.ATRperiod, self.linRegPeriod, self.referenceSMAperiod), self.resolution) tickers = history.index.levels[0] for ticker in tickers: symbol = SymbolCache.GetSymbol(ticker) if symbol not in self.symbolDataBySymbol: symbolData = SymbolData(symbol, self.SMAperiod, self.ATRperiod, self.linRegPeriod, self.referenceSMAperiod, self.referenceTicker) self.symbolDataBySymbol[symbol] = symbolData self.referenceSymbolData = symbolData symbolData.RegisterIndicators(algorithm, self.resolution) symbolData.WarmUpIndicators(history, ticker, symbol) self.referenceSMAisSet = True # initialize data for added securities addedSymbols = [x.Symbol for x in changes.AddedSecurities] if len(addedSymbols) == 0: return else: history = algorithm.History(addedSymbols, max(self.SMAperiod, self.ATRperiod, self.linRegPeriod), self.resolution) tickers = history.index.levels[0] for ticker in tickers: symbol = SymbolCache.GetSymbol(ticker) if symbol not in self.symbolDataBySymbol: symbolData = SymbolData(symbol, self.SMAperiod, self.ATRperiod, self.linRegPeriod, self.referenceSMAperiod, self.referenceTicker) self.symbolDataBySymbol[symbol] = symbolData symbolData.RegisterIndicators(algorithm, self.resolution) symbolData.WarmUpIndicators(history, ticker, symbol) # clean up data for removed securities for removed in changes.RemovedSecurities: symbolData = self.symbolDataBySymbol.pop(removed.Symbol, None) if symbolData is not None: symbolData.RemoveConsolidators(algorithm) class SymbolData: '''Contains data specific to a symbol required by this model''' def __init__(self, symbol, SMAperiod, ATRperiod, linRegPeriod, referenceSMAperiod, referenceTicker): self.Symbol = symbol self.SMA = SimpleMovingAverage(SMAperiod) self.ATR = AverageTrueRange(ATRperiod, MovingAverageType.Simple) self.ClenowMomentum = ClenowMomentum('ClenowMomentum', symbol, linRegPeriod) self.referenceSMA = SimpleMovingAverage(referenceSMAperiod) self.referenceTicker = referenceTicker self.SMAConsolidator = None self.ATRConsolidator = None self.ClenowMomentumConsolidator = None self.referenceSMAConsolidator = None self.previous = 0 def RegisterIndicators(self, algorithm, resolution): self.SMAConsolidator = algorithm.ResolveConsolidator(self.Symbol, resolution) algorithm.RegisterIndicator(self.Symbol, self.SMA, self.SMAConsolidator, Field.Close) self.ATRConsolidator = algorithm.ResolveConsolidator(self.Symbol, resolution) algorithm.RegisterIndicator(self.Symbol, self.ATR, self.ATRConsolidator) self.ClenowMomentumConsolidator = algorithm.ResolveConsolidator(self.Symbol, resolution) algorithm.RegisterIndicator(self.Symbol, self.ClenowMomentum, self.ClenowMomentumConsolidator) if self.Symbol.Value == self.referenceTicker: self.referenceSMAConsolidator = algorithm.ResolveConsolidator(self.Symbol, resolution) algorithm.RegisterIndicator(self.Symbol, self.referenceSMA, self.referenceSMAConsolidator, Field.Close) def WarmUpIndicators(self, history, ticker, symbol): for tuple in history.loc[ticker].itertuples(): self.SMA.Update(tuple.Index, tuple.close) bar = TradeBar(tuple.Index, symbol, tuple.open, tuple.high, tuple.low, tuple.close, tuple.volume) self.ATR.Update(bar) self.ClenowMomentum.Warmup(history) if self.Symbol.Value == self.referenceTicker: for tuple in history.loc[ticker].itertuples(): self.referenceSMA.Update(tuple.Index, tuple.close) def RemoveConsolidators(self, algorithm): if self.SMAConsolidator is not None: algorithm.SubscriptionManager.RemoveConsolidator(self.Symbol, self.SMAConsolidator) if self.ATRConsolidator is not None: algorithm.SubscriptionManager.RemoveConsolidator(self.Symbol, self.ATRConsolidator) if self.ClenowMomentumConsolidator is not None: algorithm.SubscriptionManager.RemoveConsolidator(self.Symbol, self.ClenowMomentumConsolidator) @property def CanEmit(self): if self.previousSMA == self.SMA.Samples: return False if self.previousATR == self.ATR.Samples: return False self.previousSMA = self.SMA.Samples self.previousATR = self.ATR.Samples return self.SMA.IsReady # Define custom indicator class ClenowMomentum: def __init__(self, name, symbol, period): self.symbol = symbol self.Name = name self.Time = datetime.min self.Value = 0 self.IsReady = False self.queue = deque(maxlen=period) self.queueTime = deque(maxlen=period) self.CurrentReturn = 0 # required update method def Update(self, input): return self.UpdateFromParameters(input.Time, input.Close) # update called for history warmup def UpdateFromParameters(self, time, value): self.queue.appendleft(value) if not len(self.queue) == self.queue.maxlen: return False self.queueTime.appendleft(time) self.Time = time count = len(self.queue) self.IsReady = count == self.queue.maxlen #start indicator calculation if self.IsReady: y = np.flipud(np.log(self.queue)) x = np.arange(len(y)) slope, intercept, r_value, p_value, std_err = stats.linregress(x, y) # Annualized exponential regression (ratio^252) x correlation self.Value = ((1+slope)**252)*(r_value**2) return self.IsReady def Warmup(self,history): for index, row in history.loc[self.symbol].iterrows(): self.UpdateFromParameters(index, row['close'])