Back to Community
Shorting SPY to Reduce Draw-down during Crisis

Introduction

Suppose we have a way to select good stocks for the long term, e.g. as in my post A Cloud & AI strategy.

Unfortunately, even if we are right, if crisis hits, as in 2008, we will experience a large draw-down.
As suggested in the book, "Stocks for the Long Run", by Jeremy Siegel, shorting SPY, or selling your SPY might be
a way out.

Here I wish to explore if shorting SPY actually helps during crisis to reduce the draw-down.

The answer is YES, it does.

Note: There is also an answer to the post A Cloud & AI strategy that shows that the use of leverage and shorting can boost the returns and reduce draw-down. The current post was also inspired by this answer in addition to the cited book.

Details

  • The baseline strategy re-balances on average every 6 months between a fixed predefined set of stocks. Those are Cloud & AI stocks, that in hindsight, as we know, have fared very well
  • We use the SPY 1 year average return. If this value drop below 0, we short SPY, causing a leverage of maximum 2.

Does Shorting Help?

As it can be seen from the table below, shorting SPY based on leverage, reduced draw-down from -0.54 to -0.28, while
it did not affect the returns.

Metric No Shorting (Original) Shorting (Current)
Total Returns 13.92 13.26
Alpha 0.11 0.14
Beta 1.09 0.61
Max Draw-down -0.54 -0.28
Volatility 0.24 0.20
Max Leverage 1.0 2.0

Caveats

  • See the original post
  • Additionally, we may not be able to short stocks. Then keeping 50% in SPY and selling it during crisis might work, but this has not been investigated here.

Disclaimer

This presentation is for informational purposes only and does not constitute an offer to sell, a solicitation to buy, or a recommendation for any security; nor does it constitute an offer to provide investment advisory or other services by the author or anyone else. Nothing contained herein constitutes investment advice or offers any opinion with respect to the suitability of any security, and any views expressed herein should not be taken as advice to buy, sell, or hold any security or as an endorsement of any security or company. This disclaimer was adapted from Quantopian's own disclaimer.

Clone Algorithm
24
Loading...
Total Returns
--
Alpha
--
Beta
--
Sharpe
--
Sortino
--
Max Drawdown
--
Benchmark Returns
--
Volatility
--
Returns 1 Month 3 Month 6 Month 12 Month
Alpha 1 Month 3 Month 6 Month 12 Month
Beta 1 Month 3 Month 6 Month 12 Month
Sharpe 1 Month 3 Month 6 Month 12 Month
Sortino 1 Month 3 Month 6 Month 12 Month
Volatility 1 Month 3 Month 6 Month 12 Month
Max Drawdown 1 Month 3 Month 6 Month 12 Month
import quantopian.algorithm as algo
from quantopian.pipeline import Pipeline
from quantopian.pipeline.data.builtin import USEquityPricing
from quantopian.pipeline.filters import QTradableStocksUS
from quantopian.pipeline.factors import AverageDollarVolume, SimpleMovingAverage, Returns
from quantopian.pipeline.factors import CustomFactor
from quantopian.pipeline.data import USEquityPricing
from quantopian.pipeline.filters import QTradableStocksUS, StaticAssets
 
import numpy as np
import pandas as pd
 
from quantopian.pipeline import Pipeline
 
from quantopian.pipeline.filters import Q1500US
 
from quantopian.pipeline.data import Fundamentals
from quantopian.pipeline.data.builtin import USEquityPricing
 
def initialize(context):
    context.i = 0
    context.changes = 0
    # parameters:
    # which cloud stocks we want to use as our universe
    context.cloud_tickers = symbols('MSFT', 'ADBE', 'AMZN', 'VMW', 'IBM', 'GOOG', 'CRM', 'SAP', 'RHT', 'ORCL', 'RAX')
    # data & AI, no infra
    context.cloud_tickers = symbols('MSFT', 'ADBE', 'AMZN', 'IBM', 'GOOG', 'CRM', 'SAP')
    
    context.spy_tickers = [sid(8554)] 
    # how many days back we use average dollar volume as filter
    context.average_dollar_volume_formation_period = 270
    context.spy_return_formation_period = 270 #120 #270
    context.rebalance_days = 270
    context.holds_spy = False
    
    algo.schedule_function(
        rebalance,
        algo.date_rules.every_day(),
        algo.time_rules.market_open(hours=1),
    )
 
    algo.schedule_function(
        record_vars,
        algo.date_rules.every_day(),
        algo.time_rules.market_close(),
    )
 
    algo.attach_pipeline(make_pipeline(context), 'pipeline')
 
 
def make_pipeline(context):
    q1500us = Q1500US()
    cloud_tickers = StaticAssets(context.cloud_tickers)
    spy_tickers = StaticAssets(context.spy_tickers)
    
    dollar_volume = AverageDollarVolume(mask=(q1500us), window_length=context.average_dollar_volume_formation_period)
   

    
    spy_returns = Returns(window_length=context.spy_return_formation_period, mask=spy_tickers) 

    screen = (cloud_tickers & q1500us) | spy_tickers
    pipe = Pipeline(
            columns = {'dv': dollar_volume, 'spyrets': spy_returns})       
 
    pipe.set_screen(screen)
    return pipe
 
 
def before_trading_start(context, data):
    """
    Called every day before market open.
    """
    context.output = algo.pipeline_output('pipeline')['dv']
    context.security_list = context.output.index
    
    context.spyrets = algo.pipeline_output('pipeline')['spyrets']
 
def rebalance(context, data):
    spy_ret = context.spyrets[context.spy_tickers[0]]
    
    context.plot_spy_ret = spy_ret
    
    context.i += 1
    selected_stocks = []
    for stock in context.output.index:  
        if stock != context.spy_tickers[0]:
            v = context.output.at[stock] 
            if v > 0:
                selected_stocks.append(stock)
            
    old_stocks = list(context.portfolio.positions.iterkeys())
    old_stocks = [s for s in old_stocks if s != context.spy_tickers[0]]
    
    for stock in old_stocks:
        if stock not in selected_stocks:
            print("closing position for " + str(stock))
            order_target(stock, 0)
    

    cond_1 = set(old_stocks) != set(selected_stocks)

    cond_2 = (context.i > context.rebalance_days)

    if cond_1:
        print("old stocks != new stocks")
    if cond_2:
        print("rebalance after {} days".format(context.rebalance_days))
    
    if not context.holds_spy and spy_ret < 0.0:
        spy_cond = True
    elif context.holds_spy and spy_ret < 0.05:
        spy_cond = True
    else:
        spy_cond = False
        
    #spy_cond = (spy_ret < 0.0) 
        
    cond_3 = context.holds_spy != spy_cond
    context.holds_spy = spy_cond
    
    cond = cond_1 or cond_2 #or cond_3
    if cond:
        context.i = 0
        context.changes += 1
        for stock in selected_stocks:
            v = 1.0/(len(selected_stocks))
            print("open position for " + str(stock) + " " + str(v))
            order_target_percent(stock, v)
    if cond_3:
        if spy_ret < 0.0:
            # short spy
            v = -1.0
            context.action_on_spy = 1 
        else:
            v = 0.0
            # sell our spy
            context.action_on_spy = 0 
        print("SPY order: " + str(v))
        order_target_percent(context.spy_tickers[0], v)
    else:
        # neutral on spy
        context.action_on_spy = -1
def record_vars(context, data):
    """
    Plot variables at the end of each day.
    """
    record(leverage=context.account.leverage)
    record(changes=context.changes)
    record(action_on_spy = context.action_on_spy)
    
 
 
def handle_data(context, data):
    """
    Called every minute.
    """
    pass
There was a runtime error.
2 responses

@Stefan, that is a good start.

Great post, thank you!