Inspired by T Smith idea to implement Gary's Antonacci's dual momentum approach to ETF
selection in "IN OUT" strategy.
-The execution code has been completely changed to keep levarage under control and avoid
insufficient buying power warnings.
-To calculate returns I used widely used in industry momentum with excluding period.
-Modified components that are more in line with the strategy.
-The IN OUT part of the strategy has not changed except for some cosmetics
to make it more readable for myself.
"DUAL MOMENTUM IN OUT" nearly doubled "IN OUT" Net Profit while maintaining risk metrics at the same level.
Compounding Annual Return
30.164%
Sharpe Ratio
1.667
PSR
97.773%
Beta
0.057
Drawdown
19.300%
Annual Standard Deviation
0.154
Here is my second version of "DUAL MOMENTUM-IN OUT".
Vladimir
Mark hatlan
Symbols are parameters in this strategy.
You can try whatever you want and choose the ones that suit you most.
Mark hatlan
Yeah I understand that, I was just wondering if you had a specific reason for having those 2 for the stocks as opposed to any other 2, just because those 2 have behaved similarly. Thats all.
Joao Antunes
Thank you so much for sharing your algo with us. This is GREAT! I'm learning a ton just by reading through the code.
I'd like to contribute where I can, so this is my first take at it.
I converted your "v2.5 Dual Momentum with Out Days by Vladimir" into QuantConnect's new Algorithm Framework. In particular, I've split your algo into the signal generation (Alpha Model) and placing trades (Portfolio Construction Model).
The converted code is as close as possible to yours (save some optimizations here and there). Note that the insights (buy/sell) are generated for SPY by DualMomentumWithOutDaysAlphaModel. DualMomentumWithOutDaysPortfolioConstructionModel then captures these SPY insights and converts them into buy/sell orders for one of the 4 assets.
Hopefully, this makes it easier to plug in and combine different strategies. I could make it more generic too, eg, pass the list of stocks and bonds are parameters.
Let me know what you (and the community) think!
PS: I'm not sure how familiar with git folks are (eg, GitHub.com, GitLab.com), but I think there's some value in using a collaborative version control system where we could collaborate/share/comment on the code in a more structured way. Just a thought. :)
Shile Wen
Hi Joao,
For personal "version control", each time a backtest is run, the code used in execution is saved in the backtest details.
For collaborative version control, one idea is to use Skylight and and turn the local Skylight folder into a Git repository so that version control can be done.
Best,
Shile Wen
Vladimir
Joao Antunes,
Thank you for sharing your great Alpha Model v2.5 Dual Momentum with Out Days.
Will Berger
Guys,
I have been running the Model v2.5 Dual Momentum with out Days for the last month. The draw down so far is 10%+. Not insignificant.
I believe this is being caused by the rise in the 10 year treasury yield and growth stocks are feeling the impact. The 3 trade signals in the algorithm today do not take this into account and was wondering should there be a 4th and what should be the action.
In this case both equity and bonds are suffering. Would it not make sense to sell both equity and bonds when detected?
Any thoughts?
Will
Vladimir
Will Berger,
Correlation of daily returns between stock index and Treasury bonds has been negative not only
in the last 10 years.
You can find research that proves that over the past 85 years or even over the past 250 years.
But sometimes the correlation can change sign, or both go up, or both go down.
In the latter case, it makes sense to switch from Treasury bonds (TLT, TLH)
to Treasury bills (SHY, SHV).
You can try this.
Will Berger
Ok. Will try it out.
On my first point, the algorithm in my observations from the past has done a good job of not over reacting when the market declines, but I don't think it is handling the scenario we are in today where the us yield is rising and the growth stocks like qqq are being signifcantly being effected by it. Maybe choosing a value stock as the alternative dual stock will do the trick. Will play with that as well.
Thanks for the suggestion Vladimir.
Vladimir
Will Berger,
Symbols are parameters in this strategy.
You can try whatever you want and choose the ones that suit you most.
PieterStam
Neat looking code will definitely play around with this
Mohamed Ajmal
Just a theoretical suggestion - our signals are almost mostly external - not of our own assets. Can we add a signal where if our asset performance is deviating from a pattern by x margin, we return to cash/bonds too.
Alexandre Catarino
Hi Mohamed Ajmal ,
Your theoretical suggestion describes how we use alternative data in QuantConnect/Lean. For example, TiingoNews is an external signal. Could you please open a thread for the second question with mode details (e.g.: a practical example on how it should work, perhaps with a backtest)? I think it's out of the scope of this thread.
Alan chen
Dear Vladimir,
Many thanks for sharing this strategy and I have spent some time studying it. I made some minor adjustments to the code and have it live on a paper account with IB. However, I come to cross a problem and hope you can share some insights. the backtest version stayed in Bond ETF since June 2021, but the live version got in and out of equity since June 2021 multiple times during the same period. Have your code experienced such behaviors? what could the possible causes?
Many thanks
Vladimir
Alan chen,
Can you share the code with some minor adjustments?
Kamal G
I saw the same behavior on V2.5 of Dual Momentum. I haven't had a chance to troubleshoot it yet… hoping someone already has?Below is a capture of the orders on a paper trading account I ran it with for a while.
I see two issues where it quickly goes back and forth between selected equities (not altogether a problem per say but you wouldn't want it trading so often) and another where algo makes the new order before liquidating existing holdings.
2021-08-02T15:10:00.6621505Z SPY 440.31 -20 Market Filled Liquidated - Interactive Brokers Order Fill Event
2021-08-02T15:10:02.8101982Z QQQ 366.46 23 Market Filled Interactive Brokers Order Fill Event
2021-08-04T15:10:03.8015137Z SPY 0 19 Market Invalid
2021-08-04T15:10:04.8019805Z QQQ 366.93 -23 Market Filled Liquidated - Interactive Brokers Order Fill Event
2021-08-05T15:10:04.8010877Z SPY 440.89 19 Market Filled Interactive Brokers Order Fill Event
2021-08-09T15:10:05.8000722Z SPY 441.8607 -22 Market Filled Liquidated - Interactive Brokers Order Fill Event
2021-08-09T15:10:08.8963402Z QQQ 368.3075 26 Market Filled Interactive Brokers Order Fill Event
2021-08-13T15:10:14.800238Z QQQ 368.455 -26 Market Filled Liquidated - Interactive Brokers Order Fill Event
2021-08-13T15:10:15.8012856Z SHV 110.475 89 Market Filled Interactive Brokers Order Fill Event
I'd say the same problem would be happening on other versions of this algo too.
Vladimir
Kamal G,
Can you share the code with where it quickly goes back and forth between selected equities?
Kamal G
Hi Vladimir,
Attaching the version in question with a backtest starting in 2020. Points below.
Vladimir
Kamal G
I see that you modified slightly my version 2.5
But this should not affect the insufficient buying power.
Here is a slightly modified version 2.51, where I switched to minute data for the traded symbols, using your setting and SetBrokerageModel(BrokerageName.InteractiveBrokersBrokerage, AccountType.Margin).
I do not see any insufficient buying power order errors or orders with stale prices.
Alan chen
Dear Vladimir,
sorry for the late response. Was occupied by other events and unable to log in in the past couple of days.
Here is the code i used for live paper trading. Basically, i changed the code to ONData, instead of Scheule.On and had it on paper trading since Mid July 2021. Now i have two issues with the code:
===============================================
import numpy as np
class MarketTimingRuleStrategy(QCAlgorithm):
def Initialize(self):
self.SetStartDate(2021, 8, 1) # Set Start Date
#self.SetEndDate(20019, 12, 31) #Set End Date
#self.SetEndDate(2021,3,7)#set end date
self.cap = 100000
# Teck Stocks: Largest 100 companies on NASDAQ
self.STK1 = self.AddEquity('SSO', Resolution.Daily).Symbol
# Tech Stock: FDN tracks a market-cap-weighted index of the largest and most liquid U.S. Internet companies.
self.STK2 = self.AddEquity('FDN', Resolution.Daily).Symbol
# US Treasury Long Term ETF: TLT tracks a market-weighted index of debt issued by the US Treasury with remaining maturities of 20 years or more.
self.BND1 = self.AddEquity('TLT', Resolution.Daily).Symbol
# US Treasury Medium Term ETF: TLH tracks a market-weighted index of debt issued by the U.S. Treasury. Remaining maturity must be between 10 and 20 years.
self.BND2 = self.AddEquity('TLH', Resolution.Daily).Symbol
self.ASSETS = [self.STK1, self.STK2, self.BND1, self.BND2]
# Overall Market
self.MKT = self.AddEquity('SPY', Resolution.Daily).Symbol
# Industrial: XLI tracks the broad US industrial sector. A 7% drop over an approx. 3-month trading period is considered a substantial drop
self.XLI = self.AddEquity('XLI', Resolution.Daily).Symbol
# Utlities: XLU tracks a market-cap-weighted index of US utilities stocks drawn exclusively from the S&P 500.
self.XLU = self.AddEquity('XLU', Resolution.Daily).Symbol
# iShares Silver Trust (NYSEArca: SLV)
self.SLV = self.AddEquity('SLV', Resolution.Daily).Symbol
# SPDR Gold Trust (NYSEArca: GLD)
self.GLD = self.AddEquity('GLD', Resolution.Daily).Symbol
# Currencies ETF: FXA tracks the changes in value of the Australian dollar relative to the US dollar.
self.FXA = self.AddEquity('FXA', Resolution.Daily).Symbol
# Currencies ETF: FXF tracks the changes in value of the Swiss Franc relative to the US dollar.
self.FXF = self.AddEquity('FXF', Resolution.Daily).Symbol
# Resources: Investco's 3 industrial metals DBB ETF. A 7% drop over an approx. 3-month trading period is considered a substantial drop
self.DBB = self.AddEquity('DBB', Resolution.Daily).Symbol
# Currencies ETF: UUP provides inverse exposure to an index of USDX futures contracts that rises in value as the dollar appreciates relative to a basket of world currencies.
self.UUP = self.AddEquity('UUP', Resolution.Daily).Symbol
# Natural Resources: IGE tracks a market-cap-weighted index of US-listed natural-resource-related companies.
self.IGE = self.AddEquity('IGE', Resolution.Daily).Symbol
# Cost of debt: The SHY ETF (iShares 1-3 Year Treasury Bond) provides the signal. SHY tracks short-term US Treasury debt (1-3 years)
# and changes in this debt’s ‘risk-free’ interest yield should be indicative of changes in firms’ cost of debt which is based on the
# risk-free rate (risk-free rate + risk premium). A 60 basis points increase (i.e., drop in the bond price) over an approx. 3-month
# trading period is considered substantial.
self.SHY = self.AddEquity('SHY', Resolution.Daily).Symbol
self.FORPAIRS = [self.XLI, self.XLU, self.SLV, self.GLD, self.FXA, self.FXF]
self.SIGNALS = [self.XLI, self.DBB, self.IGE, self.SHY, self.UUP]
self.PAIR_LIST = ['S_G', 'I_U', 'A_F'] #Silver - Gold, Industrial - Utilities, Australia - Swiss Franc
self.INI_WAIT_DAYS = 15
self.SHIFT = 55
self.MEAN = 11
self.RET = 126
self.EXCL = 5
self.leveragePercentage = 101
self.selected_bond = self.BND1
self.selected_stock = self.STK1
self.init = 0
self.bull = 1
self.exit = None
self.count = 0
self.outday = 0
self.in_stock = 0
self.spy = []
self.wait_days = self.INI_WAIT_DAYS
self.wt = {}
self.real_wt = {}
#self.SetWarmUp(timedelta(126))
#self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen('SPY', 100),
# self.calculate_signal)
#self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen('SPY', 120),
# self.trade_out)
#self.Schedule.On(self.DateRules.WeekEnd(), self.TimeRules.AfterMarketOpen('SPY', 120),
# self.trade_in)
#self.Schedule.On(self.DateRules.EveryDay(), self.TimeRules.BeforeMarketClose('SPY', 0),
# self.record_vars)
symbols = self.SIGNALS + [self.MKT] + self.FORPAIRS
for symbol in symbols:
self.consolidator = TradeBarConsolidator(timedelta(days = 1))
self.consolidator.DataConsolidated += self.consolidation_handler
self.SubscriptionManager.AddConsolidator(symbol, self.consolidator)
self.lookback = 252
self.history = self.History(symbols, self.lookback, Resolution.Daily)
if self.history.empty or 'close' not in self.history.columns:
return
self.history = self.history['close'].unstack(level=0).dropna()
self.update_history_shift()
def consolidation_handler(self, sender, consolidated):
self.history.loc[consolidated.EndTime, consolidated.Symbol] = consolidated.Close
self.history = self.history.iloc[-self.lookback:]
self.update_history_shift()
def update_history_shift(self):
self.history_shift_mean = self.history.shift(self.SHIFT).rolling(self.MEAN).mean()
def returns(self, symbol, period, excl):
prices = self.History(symbol, TimeSpan.FromDays(period + excl), Resolution.Daily).close
return prices[-excl] / prices[0]
# edit: AK 1
def OnData(self, data):
mom = (self.history / self.history_shift_mean - 1)
mom[self.UUP] = mom[self.UUP] * (-1) # measure of USD (what is the relationship?)
mom['S_G'] = mom[self.SLV] - mom[self.GLD] # momentum of silver - gold. silver has fallen more than gold in bear markets, but risen more than gold in bull markets.
mom['I_U'] = mom[self.XLI] - mom[self.XLU] # momentum of industrial minus utilities (increase of risk taking appetite as cyclical stocks outperform defensive stocks)
mom['A_F'] = mom[self.FXA] - mom[self.FXF] # momentum of australia minus swiss franc (increase of risk taking appetite as australian is commodities currency vs franc which is a saft haven currency)
# numpy.nanpercentile(a, q, axis=None, out=None, overwrite_input=False, interpolation='linear', keepdims=<no value>)[source]
# Compute the qth percentile of the data along the specified axis, while ignoring nan values.
# Returns the qth percentile(s) of the array elements.
pctl = np.nanpercentile(mom, 1, axis=0)
# Get out (i.e. a short time out of 15 days) once there's extreme price movements in any (or some or all) of the signals, as defined by pct1,
# If this short-time out is accompanied by a clear signal from any of the 3 signal pairs, which signifies that the fundamentals have changed,
# then stay out for much longer, as the wait.days become 15*15 days.
extreme = mom.iloc[-1] < pctl
self.wait_days = int(
max(0.50 * self.wait_days,
self.INI_WAIT_DAYS * max(1,
np.where((mom[self.GLD].iloc[-1]>0) & (mom[self.SLV].iloc[-1]<0) & (mom[self.SLV].iloc[-2]>0), self.INI_WAIT_DAYS, 1),
np.where((mom[self.XLU].iloc[-1]>0) & (mom[self.XLI].iloc[-1]<0) & (mom[self.XLI].iloc[-2]>0), self.INI_WAIT_DAYS, 1),
np.where((mom[self.FXF].iloc[-1]>0) & (mom[self.FXA].iloc[-1]<0) & (mom[self.FXA].iloc[-2]>0), self.INI_WAIT_DAYS, 1)
)))
adjwaitdays = min(60, self.wait_days)
# self.Debug('{}'.format(self.wait_days))
if (extreme[self.SIGNALS + self.PAIR_LIST]).any():
#if (extreme[self.PAIR_LIST]).any():
self.exit = True
self.bull = False
self.outday = self.count
if self.count >= self.outday + adjwaitdays:
self.bull = True
self.count += 1
self.Plot("In Out", "in_market", int(self.bull))
self.Plot("In Out", "num_out_signals", extreme[self.SIGNALS + self.PAIR_LIST].sum())
self.Plot("Wait Days", "waitdays", adjwaitdays)
if self.returns(self.BND1, self.RET, self.EXCL) < self.returns(self.BND2, self.RET, self.EXCL):
self.selected_bond = self.BND2
#if self.returns(self.BND1, self.RET, self.EXCL) < self.returns(self.BND2, self.RET, self.EXCL):
# self.selected_bond = self.BND1
elif self.returns(self.BND1, self.RET, self.EXCL) > self.returns(self.BND2, self.RET, self.EXCL):
self.selected_bond = self.BND1
if self.returns(self.STK1, self.RET, self.EXCL) < self.returns(self.STK2, self.RET, self.EXCL):
self.selected_stock = self.STK2
#if self.returns(self.STK1, self.RET, self.EXCL) < self.returns(self.STK2, self.RET, self.EXCL):
# self.selected_stock = self.STK1
elif self.returns(self.STK1, self.RET, self.EXCL) > self.returns(self.STK2, self.RET, self.EXCL):
self.selected_stock = self.STK1
if not self.bull:
for sec in self.ASSETS:
self.wt[sec] = 0.99 if sec is self.selected_bond else 0
self.Debug("Exiting the market; trading BONDS")
self.Notify.Email("alan.chenhd@outlook.com", "Live Trade Executed", "Bond bought")
self.trade()
if self.bull:
for sec in self.ASSETS:
self.wt[sec] = 0.99 if sec is self.selected_stock else 0
self.Debug("Entering the market; trading STOCKS")
self.Notify.Email("alan.chenhd@outlook.com", "Live Trade Executed", "Equity bought")
self.trade()
def trade(self):
for sec, weight in self.wt.items():
if weight == 0 and self.Portfolio[sec].IsLong:
self.Liquidate(sec)
cond1 = weight == 0 and self.Portfolio[sec].IsLong
cond2 = weight > 0 and not self.Portfolio[sec].Invested
if cond1 or cond2:
self.SetHoldings(sec, weight)
if cond1:
self.Debug("Invested; selling")
elif cond2:
self.Debug("Not invested; buying")
===================
Many thanks for your help and Best wishes
Alan
Vladimir
Alan chen
You substantially changed the original code.
That may be the reason why it behaves differently.
Try the version I published above.
Vladimir
The material on this website is provided for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation or endorsement for any security or strategy, nor does it constitute an offer to provide investment advisory services by QuantConnect. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. QuantConnect makes no guarantees as to the accuracy or completeness of the views expressed in the website. The views are subject to change, and may have become unreliable for various reasons, including changes in market conditions or economic circumstances. All investments involve risk, including loss of principal. You should consult with an investment professional before making any investment decisions.
To unlock posting to the community forums please complete at least 30% of Boot Camp.
You can continue your Boot Camp training progress from the terminal. We hope to see you in the community soon!