This thread is meant to continue the development of the In & Out strategy started on Quantopian. The first challenge for us will probalbly be to translate our ideas to QC code.
I'll start by attaching the version Bob Bob kindly translated on Vladimir's request.
Vladimir:
About your key error, did you also initialize UUP like this?
self.UUP = self.AddEquity('UUP', res).Symbol
Jack Pizza
This is an original version the one i've been running live, but for some reason today it failed with the above error message i posted before.
Goldie Yalamanchi
No doubt, the SHY signal WAS very valuable in the past from 2008 or even 1999+ to prevent drawdowns. The only question is will it continue to be. Again defer to the conversation on the FED and rates, etc. Your opinion may vary. Obviously you may wish to keep the signal. Yes I think the above code before we added that fix... was just trying to buy IWM before it sold QQQ, so that is resolved now right? This below code I put in one of the fixes to address that.
These changes below they were to ensure that sells happen before buys. And in the case of where QQQ was switched to IWF... the code in the trade() handles to liquidate first.
def rebalance_when_out_of_the_market(self): # Swap to 'out' assets if applicable if not self.be_in: # Only trade when changing from in to out self.trade({**dict.fromkeys(self.HLD_IN, 0), **self.HLD_OUT}) def rebalance_when_in_the_market(self): # Swap to 'in' assets if applicable if self.be_in: # Only trade when changing from out to in self.trade({**self.HLD_IN, **dict.fromkeys(self.HLD_OUT, 0)}) self.Log(f"TotalPortfolioValue: {self.Portfolio.TotalPortfolioValue}, TotalMarginUsed: {self.Portfolio.TotalMarginUsed}, MarginRemaining: {self.Portfolio.MarginRemaining}, Cash: {self.Portfolio.Cash}") for key in sorted(self.Portfolio.keys()): if self.Portfolio[key].Quantity > 0.0: self.Log(f"Symbol/Qty: {key} / {self.Portfolio[key].Quantity}, Avg: {self.Portfolio[key].AveragePrice}, Curr: { self.Portfolio[key].Price}, Profit($): {self.Portfolio[key].UnrealizedProfit}") def trade(self, weight_by_sec): if self.Portfolio.Invested: for symbol in self.Portfolio.Keys: if symbol not in weight_by_sec: self.Liquidate(symbol) buys = [] for sec, weight in weight_by_sec.items(): # Check that we have data in the algorithm to process a trade if not self.CurrentSlice.ContainsKey(sec) or self.CurrentSlice[sec] is None: continue cond1 = weight == 0 and self.Portfolio[sec].IsLong cond2 = weight > 0 and not self.Portfolio[sec].Invested if cond1 or cond2: quantity = self.CalculateOrderQuantity(sec, weight) if quantity > 0: buys.append((sec, quantity)) elif quantity < 0: self.Order(sec, quantity) for sec, quantity in buys: self.Order(sec, quantity)
T Smith
Goldie Yalamanchi cheers for the trade logic!...
Regarding the want to drop SHY as an indicator. I like the fundamental idea behind having short term bonds as an indicator and believe wishing to remove it would be a poor decision. Doesn't it defeats the point of having a deterministic strategy if we edit it based upon present unclosed trades?
Goldie Yalamanchi
You maybe right T Smith but we have thought that rates are too low and SHY is triggering over and over again. Also going forward if rates are like 0% how can TLT really make a move up? Bond funds borrow from treasury at 10 or 20 year yields and lend out at higher % to make money right? So if they are at near zero which is a move that happened from 1990 all the way to 2020 to get to 0%, how can the bond yield be used as an indicator.
Very welcome to keep SHY as an indicator. I was just discussing the merits of removing it. It will be interesting to see how the market moves forward. Either the rates go the less than 0% which has happened in Germany and pretty much sovereign funds of all developed countries. Maybe finally they will go back up and there will be rates to cut? There simply isn't enough tax revenue to do that at the moment. Idk, next market crash maybe bonds and stocks will both crash.
Jack Pizza
Goldie Yalamanchi not sure I posted the algo above that is the code I have supposedly if i remember correctly fixed any order logic, but you can confirm.
I totally agree with you there will come a time when both bonds / stocks will crash, it's just we don't know when or where. That is why I brought up the idea of implementing a tail risk strategy like Taleb.
Peter Guenther
Quickly sharing some sensitivity results: Bootstrapping
For the past couple of weeks, I was playing around with the idea of using bootstrapping to test the In & Out algo’s sensitivity to possible futures that do not play out exactly (sometimes not even remotely) like the past, a concern that I reckon is present for any trading strategy that we create. Thus, the test may have some applicability beyond our In & Out discussion here. Specifically, bootstrapping can indicate how much an algo’s total return depends on the chronology and idiosyncratic events of the past, i.e. the realization that we use in our backtests. Happy to discuss the test and how it may be expanded; any questions/comments fire away, as usual.
Bootstrapping results for the In & Out
The following chart shows the distribution of the In & Out algo’s total excess returns for 5,000 fundamentally different stock price data scenarios that each span about 13 years (i.e., equivalent length as the time period that we use in our backtests from 1 Jan 2008 to today). The algo holdings are: In = SPY and Out = TLT/IEF. The displayed total excess return (x-axis) is the algo’s total return minus the SPY total returns. An excess return of zero (i.e. the zero vertical line in the chart below) means that the algo performs at par with the market (SPY).
On average, the algo yields a positive excess return (~450%) across the 5,000 different data scenarios. This looks promising since the data scenarios can be extremely different from the past (see the technical discussion below). In data scenarios that are unfavorable for the algo, the algo underperforms the market, which happens in about 10% of the data scenarios. This finding may help to measure risk regarding using the algo for investing. At the bottom 10% percentile, the underperformance is about 30% below the market over 13 years or about 2% below the market per annum.
Technical explanations and discussion
The algo is the In & Out percentile version including the signals PRDC, METL, NRES, DEBT, USDX, the pair comparisons GOLD vs SLVA, UTIL vs INDU, SHCU vs RICU, and the flexible waitdays approach. We bootstrap via drawing random monthly segments, with replacement, from the original timeseries of returns (1 Jan 2008 – 30 Nov 2020; all relevant assets i.e. SPY, signals, pair elements). Using monthly (e.g., vs daily) segments may preserve some of that evolving character of stock prices which are not completely random from one day to the next. Drawing one bootstrap sample gives us one randomly collated new returns series spanning about 13 years (i.e., a possible future). The returns series is then converted to a price series so that the algo can make its decisions as usual. Theoretically, this series could consist of about 155 repetitions of Jan 2008 or any other month; very unlikely, but not impossible. Yet, almost always it is a random mix of months such as Nov 2012 being followed by Mar 2008, being followed by Jun 2020, and so on. When an algo’s performance strongly depends on the past chronology of events, its returns will break down as we break the chronology via random sampling. When an algo’s performance strongly depends on a few favorable (for the algo) idiosyncratic events and patterns in the timeseries, random sampling will tend to make the algo’s performance break down in many of the bootstrap samples, showing up in the distribution chart via many below zero results. For each bootstrap sample, the algo’s decisions are simulated exactly how we would perform them if the bootstrap sample was the actual (a possible future?) price series.
Vladimir
Despite the fact that I don't completely agree with the latest development of "IN OUT"
it was nice to play with "Dual Momentum IN OUT".
Thanks to T Smith.
Enjoy.
Peter Guenther
T Smith: Echoing Vladimir's post, very well done and thanks for sharing this Momentum/In & Out strategy! This is interesting insights for all, particularly I think Joseph Kravets was musing about a rotation strategy between multiple ETFs. Great stuff!
Joseph Kravets
Thanks Peter! I am starting to think that this could be overfitted but it seems effective.
Chak
To throw a wrench into the conversation, you'll notice that in 4/2013, 7/2020, and a few dates in 2015/2016 (I forget which), you'll find that the algorithm switches out of in_market stocks to out_market stocks. My perspective might be a little contraversial, but I want to argue that the reason why SPY continued going up during these times despites multiple signals firing to exit the market is that the government and/or financial institutions were pumping the market - in 4/2013, it's the start of the economic recovery from the housing bubble, and the federal government allowed/incentivized banks to sell foreclosed houses for well below market, and in 8/2020 it was announced that 3 financial institutions were intentially pumping up the market (Nasdaq, specifically) to continue to go up.
It's just a thought and a different perspective to contribute to the group.
Vladimir
Here is my DUAL MOMENTUM-IN OUT v2
Peter Guenther
Chak: Absolutely. I am wondering whether there might be an additional (hidden) indicator here that could be useful to overrule the algo's 'out' indication in certain situations. Eg similar to the notion of "don't fight the Fed", for which an indicator might be the size of the Fed's balance sheet or, ideally, the announcement of a planned expansion of the Fed's balance sheet (= pumping money into the market). Maybe such data could be matched in via a custom factor/uploaded data or via news scraping.
@Peter Guenther
Interesting results on the bootstrapping tests. It should be noted however, that bootstrapping in-sample results doesn't provide information about out-of-sample behaviour, unless the distributions underlying the in-sample results are representative out-of-sample. Bootstrapping is a resampling technique, where the observed samples are assumed to come from an independent and identically distributed population. The statistical properties of this population are estimated from the observed samples. However, in the event the original model is overfitted, the statistical proprties estimated from the model results are not representative for the out-of-sample population. So, while the bootstrapping will provide information about the model's sensitivity to the order of events, if the model is good, it will not provide such information for future events, if the model is bad.
Goldie Yalamanchi
Peter Guenther yes that must be figured out. All the variations of the algo are great, but those that include SHY as a signal have effectively stopped re-entering the market remaining in `OUT` state since 10/6/2020 and the SP500 has made a 9% gain since then, while the algo has sat out for almost 70 days. Granted there was a lot of chop, but I wonder if the algo will even trigger to back 'IN' state this year or for months into next year. There is another FOMC meeting this week, generally they will say.... we will keep rates low and SHY will keep the algo OUT (my prediction of course).
Peter Guenther
Goldie Yalamanchi: Yep, the SHY is a tricky one. Technically, it is not the Fed's low/zero interest policy/cuts that is making the SHY drift down (the algo is looking for that downdrift that falls into the bottom 1 percentile over a three month window). When the SPY drifts down it means that the market expects an increasing interest rate (= that the Fed raises the rate). A falling bonds price means a larger interest yield. The algo interprets this as a bad thing = increase in the costs of capital. And usually this would be a bad thing for equities. However, the current root cause behind the increasing interest rate expectations is actually something that is good for equities: the market expects the economy to rebound due to the latest Covid vaccine developments. So, we have two forces at play here, a positive (economic rebound) and a negative (economic rebound causing the Fed to increase rates at some stage increasing the cost of capital). Currently, the positive outweighs the negative. Yet this can flip when the market feels that the rebound (i.e. the positive side) is sufficiently priced in.

I reckon if we could disentangle investors' improving economic outlook, we might have a counter-signal that could be used to mute the SHY signal in certain situations in which it should actually be interpreted as something that is the symptom of a positive development.
Vladimir
Peter Guenther,
The latest IN-OUT strategy has 12 information sources.
The logic of decision-making is
if any one of your 12 top advisors says "out" but 11 others say "in"
the strategy should go "out".
Why you choose that rule?
Why one, not seven what is more logical?
Where in human activity you have seen such decision-making rule?
Think about it instead of looking what is wrong with "SHY".
Technically, there is nothing wrong with "SHY" chart.
The problem is in decision-making rule.
Jack Pizza
Hey Peter Guenther
Tried running this live but it doesn't enter trades, commented out warmup period, set self_bein to 1
""" Based on 'In & Out' strategy by Peter Guenther 4 Oct 2020 expanded/inspired by Tentor Testivis, Dan Whitnable (Quantopian), Vladimir, and Thomas Chang. https://www.quantopian.com/posts/new-strategy-in-and-out https://www.quantconnect.com/forum/discussion/9597/the-in-amp-out-strategy-continued-from-quantopian/p1 """ # Import packages import numpy as np import pandas as pd import scipy as sc class InOut(QCAlgorithm): def Initialize(self): self.SetStartDate(2008, 1, 1) # Set Start Date self.SetCash(100000) # Set Strategy Cash self.UniverseSettings.Resolution = Resolution.Daily res = Resolution.Minute # Feed-in constants self.INI_WAIT_DAYS = 15 # out for 3 trading weeks # Holdings ### 'Out' holdings and weights self.BND1 = self.AddEquity('TLT', res).Symbol #TLT; TMF for 3xlev self.BND2 = self.AddEquity('IEF', res).Symbol #IEF; TYD for 3xlev self.HLD_OUT = {self.BND1: .5, self.BND2: .5} ### 'In' holdings and weights (static stock selection strategy) self.STKS = self.AddEquity('QQQ', res).Symbol #SPY or QQQ; TQQQ for 3xlev self.HLD_IN = {self.STKS: 1} # Market and list of signals based on ETFs self.MRKT = self.AddEquity('SPY', res).Symbol # market self.PRDC = self.AddEquity('XLI', res).Symbol # production (industrials) self.METL = self.AddEquity('DBB', res).Symbol # input prices (metals) self.NRES = self.AddEquity('IGE', res).Symbol # input prices (natural res) self.DEBT = self.AddEquity('SHY', res).Symbol # cost of debt (bond yield) self.USDX = self.AddEquity('UUP', res).Symbol # safe haven (USD) self.GOLD = self.AddEquity('GLD', res).Symbol # gold self.SLVA = self.AddEquity('SLV', res).Symbol # vs silver self.UTIL = self.AddEquity('XLU', res).Symbol # utilities self.INDU = self.PRDC # vs industrials self.SHCU = self.AddEquity('FXF', res).Symbol # safe haven currency (CHF) self.RICU = self.AddEquity('FXA', res).Symbol # vs risk currency (AUD) self.FORPAIRS = [self.GOLD, self.SLVA, self.UTIL, self.SHCU, self.RICU] self.SIGNALS = [self.PRDC, self.METL, self.NRES, self.DEBT, self.USDX] # Initialize variables ## 'In'/'out' indicator self.be_in = 1 #initially, set to an arbitrary value different from 1 (in) and 0 (out) ## Day count variables self.dcount = 0 # count of total days since start self.outday = 0 # dcount when self.be_in=0 ## Flexi wait days self.WDadjvar = self.INI_WAIT_DAYS # set a warm-up period to initialize the indicator #self.SetWarmUp(timedelta(15)) self.Schedule.On( self.DateRules.EveryDay(), self.TimeRules.AfterMarketOpen('QQQ', 65), self.rebalance_when_out_of_the_market ) self.Schedule.On( self.DateRules.WeekEnd(), self.TimeRules.AfterMarketOpen('QQQ', 65), self.rebalance_when_in_the_market ) def rebalance_when_out_of_the_market(self): # Returns sample to detect extreme observations hist = self.History( self.SIGNALS + [self.MRKT] + self.FORPAIRS, 252, Resolution.Daily)['close'].unstack(level=0).dropna() hist_shift = hist.apply(lambda x: (x.shift(65) + x.shift(64) + x.shift(63) + x.shift(62) + x.shift( 61) + x.shift(60) + x.shift(59) + x.shift(58) + x.shift(57) + x.shift(56) + x.shift(55)) / 11) returns_sample = (hist / hist_shift - 1) # Reverse code USDX: sort largest changes to bottom returns_sample[self.USDX] = returns_sample[self.USDX] * (-1) # For pairs, take returns differential, reverse coded returns_sample['G_S'] = -(returns_sample[self.GOLD] - returns_sample[self.SLVA]) returns_sample['U_I'] = -(returns_sample[self.UTIL] - returns_sample[self.INDU]) returns_sample['C_A'] = -(returns_sample[self.SHCU] - returns_sample[self.RICU]) self.pairlist = ['G_S', 'U_I', 'C_A'] # Extreme observations; statist. significance = 1% pctl_b = np.nanpercentile(returns_sample, 1, axis=0) extreme_b = returns_sample.iloc[-1] < pctl_b # Determine waitdays empirically via safe haven excess returns, 50% decay self.WDadjvar = int( max(0.50 * self.WDadjvar, self.INI_WAIT_DAYS * max(1, np.where((returns_sample[self.GOLD].iloc[-1]>0) & (returns_sample[self.SLVA].iloc[-1]<0) & (returns_sample[self.SLVA].iloc[-2]>0), self.INI_WAIT_DAYS, 1), np.where((returns_sample[self.UTIL].iloc[-1]>0) & (returns_sample[self.INDU].iloc[-1]<0) & (returns_sample[self.INDU].iloc[-2]>0), self.INI_WAIT_DAYS, 1), np.where((returns_sample[self.SHCU].iloc[-1]>0) & (returns_sample[self.RICU].iloc[-1]<0) & (returns_sample[self.RICU].iloc[-2]>0), self.INI_WAIT_DAYS, 1) )) ) adjwaitdays = min(60, self.WDadjvar) # self.Debug('{}'.format(self.WDadjvar)) # Determine whether 'in' or 'out' of the market if (extreme_b[self.SIGNALS + self.pairlist]).any(): self.be_in = False self.outday = self.dcount if self.dcount >= self.outday + adjwaitdays: self.be_in = True self.dcount += 1 #self.be_in = True # for testing; sets the algo to being always in # Swap to 'out' assets if applicable if not self.be_in: self.wt = {**dict.fromkeys(self.HLD_IN, 0), **self.HLD_OUT} # Only trade when changing from in to out for sec, weight in self.wt.items(): cond1 = (self.Portfolio[sec].Quantity > 0) and (weight == 0) cond2 = (self.Portfolio[sec].Quantity == 0) and (weight > 0) if cond1 or cond2: self.SetHoldings(sec, weight) self.Plot("In Out", "in_market", int(self.be_in)) self.Plot("In Out", "num_out_signals", extreme_b[self.SIGNALS + self.pairlist].sum()) self.Plot("Wait Days", "waitdays", adjwaitdays) def rebalance_when_in_the_market(self): # Swap to 'in' assets if applicable if self.be_in: self.wt = {**self.HLD_IN, **dict.fromkeys(self.HLD_OUT, 0)} # Only trade when changing from out to in for sec, weight in self.wt.items(): cond1 = (self.Portfolio[sec].Quantity > 0) and (weight == 0) cond2 = (self.Portfolio[sec].Quantity == 0) and (weight > 0) if cond1 or cond2: self.SetHoldings(sec, weight)
Peter Guenther
Elsid Aliaj: One possible explanation: when the in_out indicator (self.be_in) is 1 (= says "in"), the algo waits until Friday to actually go in ('lazy trader approach'; I only reshuffle/enter equities Fridays). This happens via the second scheduling function (line 74 where it says "self.DateRules.WeekEnd()"). Another possibility is that the in_out indicator says "out" but we are past the scheduled time for the out reshuffling, which in your code happens every day at 10:35 (9:30 + 65 minutes; lines 66-70). Also in this case the algo would do nothing today and only trade tomorrow. Not sure whether this helps to identify the issue (?).
Chak
I'll hint on several things that can be done. Peter introduced the concept to quantconnect, I personally would like him to submit it as an alpha. Here are a few things I've tried:
1) If you look at the IN-OUT Plot function, it is possible to disentangle each signal's individual contribution to the algorithm's decision to exit the market. I see at least 3 ways each signal can contribute:
#1 in the extreme_b filter function that sets a hard boundary
#2 the individual contribution of each ETF to their signal
#3 in the covariance matrix to assess whether the recent signal falls with the 1%.
If you create a second plot similar to IN-OUT but have each ETF represent a line, you can see these weights.
2) if you were to omit other signals besides SHY and at each level in the decision-making process mentioned in #1, you'll find find something interesting.
3) as everyone have mentioned, yes, the model is overfitted. What can we do to resolve model overfit issues besides simply removing variables based on PSR, Sharpe Ratios, and return %?
If we can solve the below two issues, we'd be a step closer to a better model:
#1 Are there methods to set the weights of each ETFs' impact on both the 'IN' and 'OUT' strategy, while addressing the model's flexibility to adjust to future events (i.e., out of sample data)?
#2 Can we further use these weights to create a more refined strategy to include different IN/OUT Market scenarios? For example, are there instances there it's better to use TQQQ instead of QQQ when going back into the market, or to use GLD/UGL and TLT/UGL when going out of the market or you're already out of the market?
Jack Pizza
Peter Guenther Ok i'll just use the prior version i was using, but this line threw an error last time, not sure if anyone knows what the issue is, specifically close.
self.SIGNALS + [self.MRKT] + self.FORPAIRS, 252, Resolution.Daily)['close'].unstack(level=0).dropna()
Runtime Error: LiveTradingRealTimeHandler.Run(): There was an error in a scheduled event EveryDay: QQQ: 120 min after MarketOpen. The error was KeyError : 'close' Stack Trace: System.Exception: LiveTradingRealTimeHandler.Run(): There was an error in a scheduled event EveryDay: QQQ: 120 min after MarketOpen. The error was KeyError : 'close'Maybe it didn't have the closing data of QQQ that day? But it threw the error during 11:30 when it was calculating whether to stay in or out.
Tentor Testivis
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!