| 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