| Overall Statistics |
|
Total Trades 661 Average Win 0.27% Average Loss -0.34% Compounding Annual Return 8.299% Drawdown 17.400% Expectancy 0.331 Net Profit 74.080% Sharpe Ratio 0.924 Probabilistic Sharpe Ratio 38.387% Loss Rate 26% Win Rate 74% Profit-Loss Ratio 0.81 Alpha 0 Beta 0 Annual Standard Deviation 0.092 Annual Variance 0.008 Information Ratio 0.924 Tracking Error 0.092 Treynor Ratio 0 Total Fees $798.86 Estimated Strategy Capacity $130000.00 |
'''An implementation of Meb Faber's AGGRESSIVE model: Global Tactical Asset Allocation model GTAA(13) ranking
stocks on 1/3/6/12month MOM and owning TOP6 with 10-month SimpleMovingAverage Filter (200day), monthly rebalance:
https://papers.ssrn.com/sol3/papers.cfm?abstract_id=962461
"A Quantitative Approach to Tactical Asset Allocation" published May 2006.
Analysis only occurs at month End/Start, signals are NOT generated intra-month.
'''
# self.Debug(str(dir( x )))
from alpha_model import MomentumAndSMAAlphaModel
class GlobalTacticalAssetAllocation(QCAlgorithm):
def Initialize(self):
#self.SetStartDate(date(2014, 1, 29) + timedelta(days=200))
self.SetStartDate(2014, 5, 20)
#self.SetEndDate(2020, 5, 20)
self.SetCash(100000)
self.Settings.FreePortfolioValuePercentage = 0.02
self.SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin)
self.UniverseSettings.Resolution = Resolution.Daily
tickerWeightPairs = { # (1x) ETF EarliestStartDate: 2014/02
'VLUE': 0.05, # 5% US Large Value, (VLUE, 2013/05)
'MTUM': 0.05, # 5% US Large Momentum (MTUM, 2013/5)
'VBR': 0.05, # 5% US Small Cap Value (VBR)
'XSMO': 0.05, # 5% US Small Cap Momentum (XSMO)
'EFA': 0.10, # 10% Foreign Developed (EFA)
'VWO': 0.10, # 10% Foreign Emerging (VWO)
'IEF': 0.05, # 5% US 10Y Gov Bonds (IEF)
'TLT': 0.05, # 5% US 30Y Gov Bonds (TLT)
'LQD': 0.05, # 5% US Corporate Bonds (LQD)
'BWX': 0.05, # 5% Foreign 10Y Gov Bonds (BWX)
'DBC': 0.10, # 10% Commodities (DBC)
'GLD': 0.10, # 10% Gold (GLD)
'VNQ': 0.20 # 20% NAREIT (VNQ)
}
symbols = [Symbol.Create(ticker, SecurityType.Equity, Market.USA)
for ticker in [*tickerWeightPairs]]
self.AddUniverseSelection( ManualUniverseSelectionModel(symbols) )
weightsTotal = sum(tickerWeightPairs.values())
if weightsTotal != 1.0:
self.Log(f"********** Weights = {str(weightsTotal)}. WILL be scaled down to 1.0 ********** ")
self.AddAlpha( MomentumAndSMAAlphaModel( tickerWeightPairs) )
self.Settings.RebalancePortfolioOnSecurityChanges = False
self.Settings.RebalancePortfolioOnInsightChanges = False
self.SetPortfolioConstruction( InsightWeightingPortfolioConstructionModel(self.RebalanceFunction,\
PortfolioBias.Long) )
self.SetExecution( ImmediateExecutionModel() )
self.AddRiskManagement( NullRiskManagementModel() )
self.Log("GTAA(13) Initialsing... ")
# Initialise plot
#assetWeightsPlot = Chart('AssetWeights %')
#for ticker in [*tickerWeightPairs]:
# assetWeightsPlot.AddSeries(Series(ticker, SeriesType.Line, f'{ticker}%'))
def RebalanceFunction(self, time):
return Expiry.EndOfMonth(self.Time)
def OnData(self, data):
# Update Plot
self.Plot('Number of Holdings', len(self.Portfolio))
#for kvp in self.Portfolio:
# symbol = kvp.Key
# holding = kvp.Value
# self.Plot('AssetWeights %', f"{str(holding.Symbol)}%", holding.HoldingsValue/self.Portfolio.TotalPortfolioValue)class MomentumAndSMAAlphaModel(AlphaModel):
""" Alpha model(Original): Price > SMA own asset, else own RiskOff (IEF).
AggressiveModel: EqualWeight top6 ranked by average 1,3,6,12month momentum, if Price > SMA. Else RiskOff.
"""
def __init__(self, tickerWeightPairs, smaLength=200, resolution=Resolution.Daily):
'''Initializes a new instance of the SmaAlphaModel class
Args:
period: The SMA period
resolution: The reolution for the SMA'''
self.tickerWeightPairs = tickerWeightPairs
self.smaLength = smaLength
self.resolution = resolution
self.symbolDataBySymbol = {}
self.month = -1
self.riskOffAsset = "IEF"
def Update(self, algorithm, data):
'''This is called each time the algorithm receives data for (@resolution of) subscribed securities
Returns: The new insights generated.
THIS: analysis only occurs at month start, so any signals intra-month are disregarded.'''
######## Plotting
currentTotalPortfolioValue = algorithm.Portfolio.TotalPortfolioValue # get current portfolio value
totalPortfolioExposure = (algorithm.Portfolio.TotalHoldingsValue / currentTotalPortfolioValue) * 100
algorithm.Plot('Chart Total Portfolio Exposure %', 'Daily Portfolio Exposure %', totalPortfolioExposure)
########
if self.month == algorithm.Time.month:
return []
self.month = algorithm.Time.month
insights = []
risk_off_weight = 0
momentum_scores = [self.symbolDataBySymbol[x].Momentum for x in self.symbolDataBySymbol]
momentum_sort = sorted([k for k,v in self.symbolDataBySymbol.items()], # if v.BlendedMomentum.IsReady??
key=lambda x: self.symbolDataBySymbol[x].Momentum, reverse=True)
targets = momentum_sort[:6]
for symbol, symbolData in self.symbolDataBySymbol.items():
if symbol in targets:
if str(symbol) == self.riskOffAsset:
risk_off_weight += 0.16
continue
price = algorithm.Securities[symbol].Price
if price != 0 and symbolData.MovingAverage.IsReady:
if price > symbolData.MovingAverage.Current.Value:
insights.append(Insight.Price(symbol, Expiry.EndOfMonth, InsightDirection.Up, None, None, None, 0.16))
else:
insights.append(Insight.Price(symbol, Expiry.EndOfMonth, InsightDirection.Flat, None, None, None, 0))
risk_off_weight += 0.16
else:
insights.append(Insight.Price(symbol, Expiry.EndOfMonth, InsightDirection.Flat, None, None, None, 0))
risk_off_weight += 0.16
insights.append(Insight.Price(self.riskOffAsset, Expiry.EndOfMonth, InsightDirection.Up, None, None, None, risk_off_weight))
return insights
def OnSecuritiesChanged(self, algorithm, changes):
for added in changes.AddedSecurities:
weight = self.tickerWeightPairs[str(added)]
self.symbolDataBySymbol[added.Symbol] = SymbolData(added.Symbol, weight, algorithm, self.smaLength, self.resolution)
for removed in changes.RemovedSecurities:
symbolData = self.symbolDataBySymbol.pop(removed.Symbol, None)
if symbolData:
# Remove consolidator
symbolData.dispose()
class SymbolData:
def __init__(self, symbol, weight, algorithm, smaLength, resolution):
self.Symbol = symbol
self.Weight = weight
self.MovingAverage = SimpleMovingAverage(smaLength)
self.Momentum = None
self.MOMPone = MomentumPercent(21)
self.MOMPthree = MomentumPercent(63)
self.MOMPsix = MomentumPercent(126)
self.MOMPtwelve = MomentumPercent(252)
self.algorithm = algorithm
# Warm up MA
history = algorithm.History([self.Symbol], 253, resolution).loc[self.Symbol]
# Use history to build our SMA
for time, row in history.iterrows():
self.MovingAverage.Update(time, row["close"])
self.MOMPone.Update(time, row["close"])
self.MOMPthree.Update(time, row["close"])
self.MOMPsix.Update(time, row["close"])
self.MOMPtwelve.Update(time, row["close"])
# Setup indicator consolidator
self.consolidator = TradeBarConsolidator(timedelta(1))
self.consolidator.DataConsolidated += self.CustomDailyHandler
algorithm.SubscriptionManager.AddConsolidator(self.Symbol, self.consolidator)
def CustomDailyHandler(self, sender, consolidated):
self.MovingAverage.Update(consolidated.Time, consolidated.Close)
self.MOMPone.Update(consolidated.Time, consolidated.Close)
self.MOMPthree.Update(consolidated.Time, consolidated.Close)
self.MOMPsix.Update(consolidated.Time, consolidated.Close)
self.MOMPtwelve.Update(consolidated.Time, consolidated.Close)
self.Momentum = self.MOMPone.Current.Value * 12 + self.MOMPthree.Current.Value * 4 + \
self.MOMPsix.Current.Value * 2 + self.MOMPtwelve.Current.Value
def dispose(self):
self.algorithm.SubscriptionManager.RemoveConsolidator(self.Symbol, self.consolidator)