(Incomplete) New Strategy — “In & Out”

Copyright reserved by Peter Guenther - posted Oct 4, 2020 in Quantopian

(This post is incomplete without original code posted by the author Peter Guenther. The code posted below is an amended version by other community members, and so may not reflect original author’s intention.

Any users’ help (the best from original author!) on providing original codes is appreciated.)

Intuitively, it might be possible to generate excess returns via cleverly timed entries and exits in and out of the equity market. This algo may be a first step toward developing a strategy that derives optimal moves in an out of the market on the basis of early indicators of equity market downturns. At the very least, the algo could start an interesting discussion regarding the sense and nonsense of trying to time market entry and exit in this way. Either way, your contribution is highly appreciated.

Backdrop for the initial code:

  • Resources and industrial products are early in the value chain and
    market value drops in corresponding firms are early indicators of
    growth worries that ultimately affect other sectors and the broader
  • Equity market value growth benefits substantially from cheap debt,
    such that increases in bond yields (i.e., drops in bond prices)
    should be an early indicator of a slowdown in growth
  • Yet, no matter what these signals say, if the market drops by 30%
    (‘once in a decade opportunity’), we want to be in

Measures used in the initial code:

  • Resources: The DBB ETF (Invesco DB Base Metals Fund) provides the
    signal. DBB tracks the prices of three key industrial metals:
    aluminum, copper and zinc. A 7% drop over an approx. 3-month trading
    period is considered a substantial drop.
  • Industrials: The XLI ETF (Industrial Select Sector SPDR Fund)
    provides the signal. XLI tracks the broad US industrial sector. A 7%
    drop over an approx. 3-month trading period is considered a
    substantial drop.
  • 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.

Rules of the algo:

  • In terms of equity, only the market (SPY) is traded
  • If out of the market, the money is invested in bonds (IEF and TLT)
  • If any of the indicators drops substantially, we go out of the
    market and into bonds. We wait for 3 trading weeks for the dust to
    settle, unless the market drops by 30% during the waiting period in
    which case we enter immediately
  • Notes: this algo’s focus is only on the market ‘entry vs exit’
    decision, i.e. not on the ‘what equities do I select’ decision. The
    assumption is that you will be able to plug your equity selection
    logic into this algo and get an additional boost in terms of your
    strategy’s returns. However, finding an optimal equity selection is
    not the focus here (if you are interested in equity selection, see
    other community forum contributions such as “Quality Companies in an
    Uptrend” or “Uncovering Momentum”, among others). Scheduling
    functions: Whether we ‘go out’ is checked daily since equity prices
    usually drop quickly when things deteriorate and hence speed seems
    paramount. In contrast, whether we ‘go back in’ is checked weekly and
    this is a personal preference so that complex equity purchases only
    have to be executed once a week, at the end of the week. This is so
    that a ‘lazy’ trader who does not have the time to execute complex
    reshuffles (other than doing a ‘sell all’ and going into bonds) of
    the portfolio on a daily basis can combine the algo with a more
    sophisticated equity selection strategy.

Outcomes for the initial algo:

  • from 1 Jan 2008 to 2 Oct 2020, the total return is approx. 860% vs
    190% for the SPY (= being always in)
  • the backtest indicates a beta of 0.34 and the tear sheet shows an
    alpha of 20%
  • Note: regarding the backtest period, consider the launch dates of
    the different ETFs. All of them should be available from 1 Jan 2008
    onwards but some may be unavailable in earlier periods, creating a
    limit regarding testing the algo in earlier time periods

Brainstorming regarding improvement opportunities:

  • different ETFs/ways to measure prices of resources (e.g.,
    additional key resources such as oil), industrial goods (e.g., a
    stronger focus on industrial capital goods), and bonds (e.g.,
    corporate bonds instead of government bonds)
  • additional aspects—other than resources, industrial goods, and bond
    yields—that could provide early indicators of equity downturns
  • improvement of settings (e.g., waiting period, %-points indicating
    ‘substantial’ drops)
  • code improvements (errors/unintended outcomes, efficiency)
Based on 'In & Out' strategy by Peter Guenther 10-04-2020
expanded/inspired by Tentor Testivis, Dan Whitnable (Quantopian), Vladimir, and Thomas Chang.


# Import packages
import numpy as np
import pandas as pd
import scipy as sc

def initialize(context):
# Feed-in constants
context.INI_WAIT_DAYS = 15 # out for 3 trading weeks

# 'In' and 'out' holdings incl. weights
context.HLD_IN = {symbol('SPY'): 1.0}
context.HLD_OUT = {symbol('TLT'): .5, symbol('IEF'): .5}

# Market and list of signals based on ETFs
context.MRKT = symbol('SPY')
context.PRDC = symbol('XLI') # production (industrials)
context.METL = symbol('DBB') # input prices (metals)
context.NRES = symbol('IGE') # input prices (natural res)
context.DEBT = symbol('SHY') # cost of debt (bond yield)
context.USDX = symbol('UUP') # safe haven (USD)
context.SIGNALS = [context.PRDC, context.METL, context.NRES, context.DEBT, context.USDX]

# Pairs for comparative returns signals
context.GOLD = symbol('GLD') # gold
context.SLVA = symbol('SLV') # VS silver
context.UTIL = symbol('XLU') # utilities
context.INDU = context.PRDC # vs industrials
context.SHCU = symbol('FXF') # safe haven (CHF)
context.RICU = symbol('FXA') # risk currency (AUD)
context.FORPAIRS = [context.GOLD, context.SLVA, context.UTIL, context.SHCU, context.RICU]

# Initialize variables
## 'In'/'out' indicator
context.be_in = 1
## Day count variables
context.dcount = 0 # count of total days since start
context.outday = 0 # dcount when context.be_in=0
## Flexi wait days
context.WDadjvar = context.INI_WAIT_DAYS

# Commission
set_commission(commission.PerShare(cost=0.0075, min_trade_cost=1.00))

# Schedule functions
# daily rebalance if OUT of the market
time_rules.market_open(minutes = 75)
# weekly rebalance if IN the market
time_rules.market_open(minutes = 75)

def rebalance_when_out_of_the_market(context, data):
# Returns sample to detect extreme observations
hist = data.history(context.SIGNALS+[context.MRKT]+context.FORPAIRS, 'close', 253, '1d').iloc[:-1]
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[context.USDX] = returns_sample[context.USDX]*(-1)
# For pairs, take returns differential, reverse coded
returns_sample['G_S'] = -(returns_sample[context.GOLD] - returns_sample[context.SLVA])
returns_sample['U_I'] = -(returns_sample[context.UTIL] - returns_sample[context.INDU])
returns_sample['C_A'] = -(returns_sample[context.SHCU] - returns_sample[context.RICU])
context.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
context.WDadjvar = int(max(0.50*context.WDadjvar, context.INI_WAIT_DAYS * max(1,returns_sample[context.GOLD].iloc[-1] / returns_sample[context.SLVA].iloc[-1],returns_sample[context.UTIL].iloc[-1] / returns_sample[context.INDU].iloc[-1],returns_sample[context.SHCU].iloc[-1] / returns_sample[context.RICU].iloc[-1])))
adjwaitdays = min(60, context.WDadjvar)

# Determine whether 'in' or 'out' of the market
if (extreme_b[context.SIGNALS+context.pairlist]).any():
context.be_in = False
context.outday = context.dcount
if context.dcount >= context.outday + adjwaitdays:
context.be_in = True
context.dcount += 1

# Swap to 'out' assets if applicable
if not context.be_in:
for asset, weight in context.HLD_OUT.items():
order_target_percent(asset, weight)
for asset in context.portfolio.positions:
# Close 'In' holdings
if asset not in context.HLD_OUT:
order_target_percent(asset, 0)

# Record
record(in_market=context.be_in, num_out_signals=extreme_b[context.SIGNALS+context.pairlist].sum(), waitdays=adjwaitdays)

def rebalance_when_in_the_market(context, data):
# Swap to 'in' assets if applicable
if context.be_in:
for asset, weight in context.HLD_IN.items():
order_target_percent(asset, weight)
for asset in context.portfolio.positions:
# Close 'Out' holdings
if asset not in context.HLD_IN:
order_target_percent(asset, 0)


The people involved in the discussion on Quantopian have actually taken to Quantconnect… the discussion is continuing over there.

Yes I am aware of the post. However, that post was without the description of the strategy but focused on migrating to Quantconnect, so that might be more difficult for completely new user to understand.

Your right of course!


I took the In-and-Out strategy, as condensed by Vladimir, in Quantopian code and translated it to zipline for backtesting, then translated that to live trading code on IB, using an HCA box.

Made a video out of the process and uploaded the files used to google drive.

Files for In-and-Out:

Comments welcome!