Back to Community
Mean-Reversion Short - Money in bad markets

Simple mean reversion short only algorithm. In bear markets it beats the S&P 500 (first attached backtest) but in longer periods it performs worse. In sideways markets I was expecting the performance to be worse but not that much so there might be an error somewhere or I don't properly understand how shorting works in Quantopian.

Details of the algorithm:

Objective
- Trade short only on a large universe of stocks, taking advantage of overbought conditions by short selling the best stocks, and buying each back when it reverts to its mean.
- Execute trades every day.
- Beat the benchmarks, especially in bear markets

Trading universe
- All stocks from AMEX, NASDAQ, and NYSE.
- Do not trade ETFs, pink sheets or bulletin board stocks.

Filters
- Minimum Average Volume of the last 20 days is above 500.000 shares (ensure liquidity).
- Minimum price is 10 USD.

Position Sizing:
- Maximum 10 positions.
- Fixed fractional risk: 2 percent.
- Maximum position size: 10 percent.

Buys
1. Trade every day.
3. Seven day average directional index (ADX) is above 50 (short term trend strength).
4. Average true range percent of the last 10 days is above 5 percent (volatility).
5. The three day RSI is above 85 (overbought on a short term basis).
6. Rank orders by the highest three day RSI.

Sells:
1. Trade every day.
2. Sell when one of the following conditions is met:
2.1 Stop Loss: 1.5 times the ten-day ATR.
2.2 Profit Target: 4 percent or more.
2.3 Time Exit: 2 days have passed and none of the above conditions is met.

Notes:
- The rebalance is split in 2 functions with an hour gap. This is just to "ensure" sell orders are filled before we start buying.
- The only purpose of canceling open orders at the end of the trading day is to avoid the console warnings.

Other algorithms:
- Weekly Rotation S&P 500
- Mean-Reversion Long - For Bold Contrarians

Clone Algorithm
29
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
# Backtest ID: 5c523967b4a7704b00c8c232
There was a runtime error.
9 responses

The backtest of a longer period looks much worse.

Clone Algorithm
29
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
# Backtest ID: 5c5235eb9bf4464aeacbbb0b
There was a runtime error.

Good idea. I find that the leverage is sometimes higher than one? What if we add a condition that limits trade when the market is going down?

Hi,

Regarding leverage, you are right. I don't understand how to keep it under control in an "only short" algorithm.

If you want to trade just in downtrend markets, you could probably use the daily SMA band of the SPY. Just trade when the last close of the SPY is below the 200 daily SMA band (with a -2% buffer). Otherwise you exit all your positions. I used something similar to just trade uptrend markets here

@Mark I add the downtrend in the algorithm. As a result, the volatility declines, while the return decreases.

In reality, the case that the rsi is greater than 85 seems not so common for most stocks. Therefore, it is not easy to filter more than five stocks during real trading. But if I set the rsi lower, the return also declines.

To go short could be like this:
for equity in short_list:
if data.can_trade(equity):
order_target_percent(equity, -min(0.3 / len(short_list), context.MAX_IN_ONE))
for equity in long_list:
if data.can_trade(equity):
order_target_percent(equity, min(0.7 / len(long_list), context.MAX_IN_ONE))

Clone Algorithm
5
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
# Backtest ID: 5c5a7d967b9e0b4b0a5249b3
There was a runtime error.

It's very difficult to control the leverage of a short-only algo. You can under-lever so when it moves against you it's still a reasonable leverage. That or improve the predictive power of your algo so that it never moves too hard against you. Easier said than done. Holding more than 10 positions may help reduce volatility, drawdowns, and therefore leverage spikes by lowering single-name risk. Another thing you can do is diversify across sectors.

Unfortunately on Quantopian we only have one crash available in the data. However, even if there were more it's not a good idea to tailor algorithms to one-off events. The market is always doing new things -- setting new records, exhibiting new behaviors, etc. The 2019/2020 crash will likely be nothing like 2008.

Note: technically opening a short positions is "selling" and closing a short position is "buying." I use "open" and "close" when talking about shorts so there's no ambiguity.

Hi all,
I have inserted a further filter based on the vix and 50 moving average, looking for more stability in gains.. try to take a look .. thanks

Clone Algorithm
8
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
# Backtest ID: 5c6e5e36e39f694a7dd655b4
There was a runtime error.

@daniele carabini
Well done. But I think there is bias if you backtest only through 2008-2009.

Forever's filter exchange screens out OTCPK and OTCBB leaving NYSE, NAS, AMEX:
filter_exchange = nasdaq_filter | amex_filter | nyse_filter

The modification in the most recent--and this is surely counter-intuitive--causes them to all be NYSE, keeping just the last one:
filter_exchange = nasdaq_filter and amex_filter and nyse_filter

Another way to go about it, where the tilde (~) means not:
filter_exchange = ~Fundamentals.exchange_id.latest.startswith('OTC') # not_otc

Meanwhile on a larger scale if I may, before collections starting with Q500US and eventually QTradableStocksUS, there was a time when Q's filters/mask would look not too much unlike this:

    f = Fundamentals  
    m &= (  # mask  
           f.is_primary_share                                       # primary_share  
        & ~f.is_depositary_receipt.latest                           # not_depositary  
        & ~f.exchange_id          .latest.startswith('OTC')         # not_otc  
        & ~f.symbol               .latest.endswith('.WI')           # not_wi  
        & ~f.standard_name        .latest.matches('.* L[. ]?P.?$')  # not_limited partnership  
        &  f.security_type        .latest.eq('ST00000001')          # common_stock  
        &  f.market_cap           .latest.notnull()                 # has market_cap  
    )  

To use those where the mask is progressively applied to each operation, a route I prefer, can sometimes be not just an increase in speed with fewer and fewer stocks being processed in each step but also very important, as without it can result in unintended results. It's hard to explain, I just recall a long time ago the hours and hours I spent experimenting around and finding that weights or included stocks would be way off at times until I began adhering to this pattern. So I've become a cheerleader for it, adding to m (mask) each time and always using/applying it whenever possible. You would find, for example, that the RSI( ... mask=m) step would be chewing on fewer stocks. A custom factor doing calculations on how all of the stock's values relate to each other and/or filtering them could otherwise be operating on stocks that may later be tossed out, so maybe that makes the point well enough.
I haven't tested this pipe for returns but the pipeline is indeed different, 33 in number vs 42 on first day.

def make_pipeline():  
    avg_volume = SimpleMovingAverage(inputs=[USEquityPricing.volume],window_length=20)

    # mask is 'm', initialized  
    m = avg_volume > 500000

    f = Fundamentals  
    m &= (  # mask  
           f.is_primary_share                                       # primary_share  
        & ~f.is_depositary_receipt.latest                           # not_depositary  
        & ~f.exchange_id          .latest.startswith('OTC')         # not_otc  
        & ~f.symbol               .latest.endswith('.WI')           # not_wi  
        & ~f.standard_name        .latest.matches('.* L[. ]?P.?$')  # not_limited partnership  
        &  f.security_type        .latest.eq('ST00000001')          # common_stock  
        &  f.market_cap           .latest.notnull()                 # has market_cap  
    )

    last_price = Latest(inputs=[USEquityPricing.close], window_length=1, mask=m) # using the mask  
    m &= last_price >= 1  # adding to mask

    atr_10_percent = atr_10days_percent(mask=m)  
    m &= atr_10_percent > 5

    rsi = RSI(inputs=[USEquityPricing.close], window_length=3, mask=m)  
    m &= rsi > 70

    atr_10 = atr_10days(mask=m)

    return Pipeline(  
        columns = {  
            'rsi': rsi,  
            'atr': atr_10,  
        },  
        screen = m  
    )  

I also found that sending out orders 1 hour before the market opens and 1 hour after the market opens can help to avoid some unnecessary volatility.

*Note: I also decreased the initial capital to reflect a strategy that I might actually trade with.

Clone Algorithm
5
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
# Backtest ID: 5c8530e61f787d4b03229832
There was a runtime error.