Back to Community
SPY 200MA Backtest w/short

Buy when price > 200MA, liquidate when price < 200MA, short when price < 200MA and price < 10MA

Clone Algorithm
143
Loading...
Backtest from to with initial capital
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
# 50/200 MA strategy


#initialize the strategy 
def initialize(context):
    context.etf = sid(8554)
    context.price={}
    context.invested = False
    context.allow_short = True
    context.max_short_pos = 10000
    context.short = False
    
    
def handle_data(context, data):
    
    price = data[context.etf].price
    longSig = price > data[context.etf].mavg(200)
    cashSig = price < data[context.etf].mavg(200)
    shortSig = cashSig and price < data[context.etf].mavg(10)
    coverSig = price > data[context.etf].mavg(10)
    
    balance_qty = context.portfolio.positions[context.etf].amount
    
    log.info("Account Balance {0}".format(context.portfolio.cash))

    if coverSig and context.allow_short and context.short:
            #cover        
            log.info("Yikes, we have to cover {0} shares".format(-balance_qty))
            order(context.etf, -balance_qty)
            log.info("Covered {0} shares at {1}, Short MA  = {2}, Long MA = {3}".format(-balance_qty, price, shortSig, longSig))
            context.short = False
        
    if longSig:
        
        if context.allow_short and context.short:
            #cover        
            log.info("Yikes, we have to cover {0} shares".format(-balance_qty))
            order(context.etf, -balance_qty)
            log.info("Covered {0} shares at {1}, Short MA  = {2}, Long MA = {3}".format(-balance_qty, price, shortSig, longSig))
            context.short = False
            
        # flip to long
        if not context.invested:
            qty = round(context.portfolio.cash/price)
            order(context.etf , qty) 
            buyAmount = round(context.portfolio.cash / price)
            log.info("Bot {0} shares at {1}, Short MA  = {2}, Long MA = {3}".format(buyAmount, price, shortSig, longSig))
            context.invested = True
    
    elif cashSig and context.invested:
        # liquidate
        order(context.etf,-(context.portfolio.positions[context.etf].amount))
        log.info("Going to cash, ETF = {0}, Short MA  = {1}, Long MA = {2}".format(price, shortSig, longSig))      
        context.invested = False
         
    elif shortSig and context.allow_short and not context.short:
            #remaining_allowed_short = context.max_short_pos - (-1 * context.portfolio.positions[context.etf].amount * price)
            short_qty = round(context.max_short_pos/price)
            order(context.etf,-short_qty)
            log.info("Going short {0} shares, ETF = {1}, Short MA  = {2}, Long MA = {3}".format(short_qty, price, shortSig, longSig))      
            log.info("Account Balance {0}".format(context.portfolio.cash))
            context.invested = False
            context.short = True  
          
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.
11 responses

Thanks for sharing these Claus. They're interesting. Was there an inspiration for these, or did you just figure them out yourself?

Disclaimer

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 Quantopian. In addition, the material offers no opinion with respect to the suitability of any security or specific investment. No information contained herein should be regarded as a suggestion to engage in or refrain from any investment-related course of action as none of Quantopian nor any of its affiliates is undertaking to provide investment advice, act as an adviser to any plan or entity subject to the Employee Retirement Income Security Act of 1974, as amended, individual retirement account or individual retirement annuity, or give advice in a fiduciary capacity with respect to the materials presented herein. If you are an individual retirement or other investor, contact your financial advisor or other fiduciary unrelated to Quantopian about whether any given investment idea, strategy, product or service described herein may be appropriate for your circumstances. All investments involve risk, including loss of principal. Quantopian 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.

what i find really interesting is how gracefully you avoided the 08/09 crash.

I'm just backtesting a couple of trend following strategies from various books, like Clenow and Covel, surprised to see how easily one could've avoided the 08/09 crash. Even without a short component, you'd still be doing great with the 200MA alone.

I switched your MA10 rule round, as equities are mean reverting in the short term. For me, it only makes sense to short them if they're ABOVE the short term (10 day) moving average. This means the short term mean reversion is in the same direction as the bear market trend.

This improves the total return and reduces the max drawdown. It reduces the Sharpe ratio though, which is unexpected! But anyway, I'd rather have a lower DD than a lower Sharpe, all other things being equal!

Clone Algorithm
52
Loading...
Backtest from to with initial capital
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
# 50/200 MA strategy


#initialize the strategy 
def initialize(context):
    context.etf = sid(8554)
    context.price={}
    context.invested = False
    context.allow_short = True
    context.max_short_pos = 10000
    context.short = False
    
    
def handle_data(context, data):
    
    price = data[context.etf].price
    longSig = price > data[context.etf].mavg(200)
    cashSig = price < data[context.etf].mavg(200)
    shortSig = cashSig and price > data[context.etf].mavg(10) # switched this
    coverSig = price < data[context.etf].mavg(10) # switched this
    
    balance_qty = context.portfolio.positions[context.etf].amount
    
    log.info("Account Balance {0}".format(context.portfolio.cash))

    if coverSig and context.allow_short and context.short:
            #cover        
            log.info("Yikes, we have to cover {0} shares".format(-balance_qty))
            order(context.etf, -balance_qty)
            log.info("Covered {0} shares at {1}, Short MA  = {2}, Long MA = {3}".format(-balance_qty, price, shortSig, longSig))
            context.short = False
        
    if longSig:
        
        if context.allow_short and context.short:
            #cover        
            log.info("Yikes, we have to cover {0} shares".format(-balance_qty))
            order(context.etf, -balance_qty)
            log.info("Covered {0} shares at {1}, Short MA  = {2}, Long MA = {3}".format(-balance_qty, price, shortSig, longSig))
            context.short = False
            
        # flip to long
        if not context.invested:
            qty = round(context.portfolio.cash/price)
            order(context.etf , qty) 
            buyAmount = round(context.portfolio.cash / price)
            log.info("Bot {0} shares at {1}, Short MA  = {2}, Long MA = {3}".format(buyAmount, price, shortSig, longSig))
            context.invested = True
    
    elif cashSig and context.invested:
        # liquidate
        order(context.etf,-(context.portfolio.positions[context.etf].amount))
        log.info("Going to cash, ETF = {0}, Short MA  = {1}, Long MA = {2}".format(price, shortSig, longSig))      
        context.invested = False
         
    elif shortSig and context.allow_short and not context.short:
            #remaining_allowed_short = context.max_short_pos - (-1 * context.portfolio.positions[context.etf].amount * price)
            short_qty = round(context.max_short_pos/price)
            order(context.etf,-short_qty)
            log.info("Going short {0} shares, ETF = {1}, Short MA  = {2}, Long MA = {3}".format(short_qty, price, shortSig, longSig))      
            log.info("Account Balance {0}".format(context.portfolio.cash))
            context.invested = False
            context.short = True  
          
There was a runtime error.

The risk metrics in the original post were incorrect due to backtester bugs (see the link in the Important Message tab). Your changes actually improve Sharpe from 0.43 (the value obtained by running the original code in today's backtester) to 0.66. Nice work!

Thanks Michael.

I just noticed the algo had a hardcoded short size of $10,000. I've changed this to half the total cash. Obviously it could be 100% of the cash, like the long side, but this is a personal preference of mine. Bear markets are often subject to very sharp reversals.

This change brings the Sharpe down slightly, but it's probably just noise.

Clone Algorithm
52
Loading...
Backtest from to with initial capital
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
# 50/200 MA strategy
import math

#initialize the strategy 
def initialize(context):
    context.etf = sid(8554)
    context.price={}
    context.invested = False
    context.allow_short = True
    context.max_short_pos = 10000
    context.short = False
    
    
def handle_data(context, data):
    
    price = data[context.etf].price
    longSig = price > data[context.etf].mavg(200)
    cashSig = price < data[context.etf].mavg(200)
    shortSig = cashSig and price > data[context.etf].mavg(10) # switched this
    coverSig = price < data[context.etf].mavg(10) # switched this
    
    balance_qty = context.portfolio.positions[context.etf].amount
    
    log.info("Account Balance {0}".format(context.portfolio.cash))

    if coverSig and context.allow_short and context.short:
            #cover        
            log.info("Yikes, we have to cover {0} shares".format(-balance_qty))
            order(context.etf, -balance_qty)
            log.info("Covered {0} shares at {1}, Short MA  = {2}, Long MA = {3}".format(-balance_qty, price, shortSig, longSig))
            context.short = False
        
    if longSig:
        
        if context.allow_short and context.short:
            #cover        
            log.info("Yikes, we have to cover {0} shares".format(-balance_qty))
            order(context.etf, -balance_qty)
            log.info("Covered {0} shares at {1}, Short MA  = {2}, Long MA = {3}".format(-balance_qty, price, shortSig, longSig))
            context.short = False
            
        # flip to long
        if not context.invested:
            qty = round(context.portfolio.cash/price )
            order(context.etf , qty) 
            buyAmount = round(context.portfolio.cash / price)
            log.info("Bot {0} shares at {1}, Short MA  = {2}, Long MA = {3}".format(buyAmount, price, shortSig, longSig))
            context.invested = True
    
    elif cashSig and context.invested:
        # liquidate
        order(context.etf,-(context.portfolio.positions[context.etf].amount))
        log.info("Going to cash, ETF = {0}, Short MA  = {1}, Long MA = {2}".format(price, shortSig, longSig))      
        context.invested = False
         
    elif shortSig and context.allow_short and not context.short:
            #remaining_allowed_short = context.max_short_pos - (-1 * context.portfolio.positions[context.etf].amount * price)
            short_qty = round(context.portfolio.cash / price / 2 )
            #short_qty = round(context.portfolio.cash / price / 2 )
            order(context.etf,-short_qty)
            log.info("Going short {0} shares, ETF = {1}, Short MA  = {2}, Long MA = {3}".format(short_qty, price, shortSig, longSig))      
            log.info("Account Balance {0}".format(context.portfolio.cash))
            context.invested = False
            context.short = True  
          
There was a runtime error.

Here's the same algo, tidied up a bit. It uses the schedule_function to run 15 minutes before market close, so it has a good estimate of the day's close, and uses order_target_percent, as recommended by Q.

Clone Algorithm
52
Loading...
Backtest from to with initial capital
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
# Called once at algo start
def initialize(context):
    context.etf = sid(8554) #SPY ETF: Total return of S&P500
    schedule_function(
        func=rebalance,  
        date_rule=date_rules.every_day(),  
        time_rule=time_rules.market_close(minutes=15),
        half_days=True
     )
    
# Called every minute: does nothing
def handle_data(context, data):
    pass

# Called 15 minutes before close every day
def rebalance(context, data):
    
    # order_target methods don't check for open orders in their calculation
    # https://www.quantopian.com/posts/order-target-percent-ordering-too-much
    if get_open_orders(context.etf):
        pass
    
    price = data[context.etf].price
    ma200 = data[context.etf].mavg(200)
    ma10  = data[context.etf].mavg(10)

    if price > ma200:
        target = 1
    elif price < ma200 and price > ma10: # bear market = direction of short-term mean reversion
        target = -1
    else:
        target = 0
        
    log.info("Adjusting {s} to {p} percent".format(s=context.etf, p=100.0*target))  
    order_target_percent(context.etf, target)

There was a runtime error.

Hi Dan,
That's very cool, I had totally forgotten that I had posted this, it's been a couple of years. I like what you did with the schedule function, I'll have to check out the new API features. Thx!

Quantopian/Python has a steep learning curve, so revisiting code is a good way for me to learn. I made the newbie error using schedule_function then running the algo in daily mode. Here's the fix, with the exposure halved on the short side.

Clone Algorithm
52
Loading...
Backtest from to with initial capital
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 numpy as np

# Called once at algo start
def initialize(context):
    context.etf = sid(8554) #SPY ETF: Total return of S&P500
    schedule_function(
        func=rebalance,  
        date_rule=date_rules.every_day(),  
        time_rule=time_rules.market_close(minutes=15),
        half_days=True
     )
    
# Called every minute: does nothing
def handle_data(context, data):
    pass

# Called 15 minutes before close every day
def rebalance(context, data):
    
    # order_target methods don't check for open orders in their calculation
    # https://www.quantopian.com/posts/order-target-percent-ordering-too-much
    if get_open_orders(context.etf):
        return
    
    price = data[context.etf].price

    # bug in mavg on minutely mode:
    # throws an exception if you start the backtest on 2002-01-03 and never recovers 
    #ma200 = data[context.etf].mavg(200)
    #ma10  = data[context.etf].mavg(10)    
    h200  = history(200,"1d","close_price")[context.etf]
    ma200 = np.mean(h200)
    ma10  = np.mean(h200.tail(10))

    if price > ma200:
        target = 1
    elif price < ma200 and price > ma10: # bear market = direction of short-term mean reversion
        target = -0.5
    else:
        target = 0
    
    # BUG: Buys and sells small lots to bring to exactly 100%, which is not my intention
    log.info("Adjusting {s} to {p} percent".format(s=context.etf, p=100.0*target))  
    order_target_percent(context.etf, target)


There was a runtime error.

Hey, it looks like you need to isolate the peak for the short buy. Notice your short on 2009's crash only netted you 1% of the potential 30% gain.

Maybe you could write in to close the short when it is above x% off of the y day moving average low to potentially close on long and short peaks?