Back to Community
CUATS Workshop

This is a thread we can use to post backtests etc.

5 responses

Workshop pairs trading example, for you to clone and modify.

Clone Algorithm
21
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
"""
This is a basic pairs trading algorithm for use at Quantopian Workshops.
WARNING: THIS IS A LEARNING EXAMPLE ONLY. DO NOT TRY TO TRADE SOMETHING THIS SIMPLE.
https://www.quantopian.com/workshops
https://www.quantopian.com/lectures
By Delaney Granizo-Mackenzie
"""
import numpy as np
 
def initialize(context):
    """
    Called once at the start of the algorithm.
    """   
    # Check status of the pair every day 2 minutes before we rebalance
    # The 2 minutes is just because we want to be safe, and 1 minutes
    # is cutting it close
    schedule_function(check_pair_status, date_rules.every_day(), time_rules.market_close(minutes=60))
    
    context.stock1 = symbol('ABGB')
    context.stock2 = symbol('FSLR')
    
    # Our threshold for trading on the z-score
    context.entry_threshold = 0.2
    context.exit_threshold = 0.1
    
    # Moving average lengths
    context.long_ma_length = 30
    context.short_ma_length = 1
    
    # Flags to tell us if we're currently in a trade
    context.currently_long_the_spread = False
    context.currently_short_the_spread = False


def check_pair_status(context, data):
    
    # For notational convenience
    s1 = context.stock1
    s2 = context.stock2
    
    # Get pricing history
    prices = data.history([s1, s2], "price", context.long_ma_length, '1d')
    
    # Try debugging me here to see what the price
    # data structure looks like
    # To debug, click on the line number to the left of the
    # next command. Line numbers on blank lines or comments
    # won't work.
    short_prices = prices.iloc[-context.short_ma_length:]
    
    # Get the long mavg
    long_ma = np.mean(prices[s1] - prices[s2])
    # Get the std of the long window
    long_std = np.std(prices[s1] - prices[s2])
    
    
    # Get the short mavg
    short_ma = np.mean(short_prices[s1] - short_prices[s2])
    
    # Compute z-score
    if long_std > 0:
        zscore = (short_ma - long_ma)/long_std
    
        # Our two entry cases
        if zscore > context.entry_threshold and \
            not context.currently_short_the_spread:
            order_target_percent(s1, -0.5) # short top
            order_target_percent(s2, 0.5) # long bottom
            context.currently_short_the_spread = True
            context.currently_long_the_spread = False
            
        elif zscore < -context.entry_threshold and \
            not context.currently_long_the_spread:
            order_target_percent(s1, 0.5) # long top
            order_target_percent(s2, -0.5) # short bottom
            context.currently_short_the_spread = False
            context.currently_long_the_spread = True
            
        # Our exit case
        elif abs(zscore) < context.exit_threshold:
            order_target_percent(s1, 0)
            order_target_percent(s2, 0)
            context.currently_short_the_spread = False
            context.currently_long_the_spread = False
        
        record('zscore', zscore)
There was a runtime error.

2014 was a bad year :( - joel

Clone Algorithm
2
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
def initialize(context):
 
    context.security_list = [sid(5061), sid(7792), sid(1941), sid(1746),sid(46631)]

    schedule_function(rebalance,
                      date_rules.every_day(),
                      time_rules.market_open())
    
    set_commission(commission.PerShare(cost=0, min_trade_cost=0))
    set_commission(commission.PerTrade(cost=0))
    #set_slippage(slippage.FixedSlippage(spread=0.00))
    #set_slippage(slippage.VolumeShareSlippage(volume_limit=0, price_impact=0))
    
    
    
def Signal(context,data):
    hist = data.history(context.security_list, 'price', 60, '1d')
    
    prices_20 = hist[-20:]
    prices_60 = hist[-60:]
    
    sma_20 = prices_20.mean()
    sma_60 = prices_60.mean()

    return sma_20-sma_60
    
def Signal_yday(context,data):
    hist = data.history(context.security_list, 'price', 61, '1d')
    
    prices_20_yday = hist[-21:-1]
    prices_60_yday = hist[-61:-1]
    
    sma_20_yday = prices_20_yday.mean()
    sma_60_yday = prices_60_yday.mean()
    
    return sma_20_yday-sma_60_yday

def compute_weights(context, data):
    """
    Compute weights for each security that we want to order.
    """

    # Get the 30-day price history for each security in our list.
    hist = data.history(context.security_list, 'price', 200, '1d')

    prices_60 = hist[-60:]
    prices_200 = hist
    
    sma_60 = prices_60.mean()
    sma_200 = prices_200.mean()

    # Weights are based on the relative difference between the short and long SMAs
    weights = (sma_200-sma_60)/sma_200

    # Normalize our weights
    normalized_weights = weights / weights.abs().sum()
    # Return our normalized weights. These will be used when placing orders later.
    return normalized_weights

def rebalance(context, data):
    signals_yesterday = Signal_yday(context,data)
    signals_today = Signal(context, data)
    
    weights = compute_weights(context, data)
    
    for security in context.security_list:
        if data.can_trade(security):
            #record(sma_diff=signals_today[security])
            
            if signals_yesterday[security] <= 0 and signals_today[security] > 0:                   
                order_target_percent(security,weights[security])
                print 'buy'
                print security
            elif signals_yesterday[security] >=0 and signals_today[security] < 0:
                order_target_percent(security,weights[security])
                print 'sell'
                print security
                
                
There was a runtime error.

Florian Kreyssig

Clone Algorithm
1
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
"""
This is a basic pairs trading algorithm for use at Quantopian Workshops.
WARNING: THIS IS A LEARNING EXAMPLE ONLY. DO NOT TRY TO TRADE SOMETHING THIS SIMPLE.
https://www.quantopian.com/workshops
https://www.quantopian.com/lectures
By Delaney Granizo-Mackenzie
"""
import numpy as np
import math

def initialize(context):
    """
    Called once at the start of the algorithm.
    """   
    # Check status of the pair every day 2 minutes before we rebalance
    # The 2 minutes is just because we want to be safe, and 1 minutes
    # is cutting it close
    schedule_function(check_pair_status, date_rules.every_day(), time_rules.market_close(minutes=60))
    
    s1 = sid(4283) #Coke
    s2 = sid(5885) #Pepsi
    s3 = sid(25006) #JP morgan
    s4 = sid(1335) #Citigroup
    s5 = sid(3766) #IBM
    s6 = sid(5061) #MSFT
    s7 = sid(24) #AAPL
    s8 = sid(45815) #TWITTER
    s9 = sid(5938) #Procter and Gamble
    s10 = sid(1582) #Colgate Palmolive
    context.stocks = []
    context.stocks.append(s1)
    context.stocks.append(s2)
    context.stocks.append(s3)
    context.stocks.append(s4)
    context.stocks.append(s5)
    context.stocks.append(s6)
    context.stocks.append(s7)
    context.stocks.append(s8)
    context.stocks.append(s9)
    context.stocks.append(s10)
    
    # Our threshold for trading on the z-score
    context.entry_threshold = 0.2
    context.exit_threshold = 0.1
    
    # Moving average lengths
    context.long_ma_length = 30
    context.short_ma_length = 1
    
    # Flags to tell us if we're currently in a trade
    context.currently_long_the_spread = False
    context.currently_short_the_spread = False


def check_pair_status(context, data):
    
    # For notational convenience
    stocks = context.stocks
    percentage = (1/(float(round(math.floor(len(stocks)/2)))))
    # Get pricing history
    prices = []
    for stockpair in range(0,int(round(math.floor(len(stocks)/2)))):
        s1 = stocks[2*stockpair]
        s2 = stocks[2*stockpair+1]
        prices = data.history([s1, s2], "price", context.long_ma_length, '1d')
        short_prices = prices.iloc[-context.short_ma_length:]
    
        # Get the long mavg
        long_ma = np.mean(prices[s1] - prices[s2])
        # Get the std of the long window
        long_std = np.std(prices[s1] - prices[s2])
    
    
        # Get the short mavg
        short_ma = np.mean(short_prices[s1] - short_prices[s2])
        print (s1)
        print (s2)
        print (percentage)
        # Compute z-score
        if long_std > 0:
            zscore = (short_ma - long_ma)/long_std
    
            # Our two entry cases
            if zscore > context.entry_threshold and \
                not context.currently_short_the_spread:
                order_target_percent(s1, -percentage) # short top
                order_target_percent(s2, percentage) # long bottom
                context.currently_short_the_spread = True
                context.currently_long_the_spread = False
            
            elif zscore < -context.entry_threshold and \
                not context.currently_long_the_spread:
                order_target_percent(s1, percentage) # long top
                order_target_percent(s2, -percentage) # short bottom
                context.currently_short_the_spread = False
                context.currently_long_the_spread = True
            
            # Our exit case
            elif abs(zscore) < context.exit_threshold:
                order_target_percent(s1, 0)
                order_target_percent(s2, 0)
                context.currently_short_the_spread = False
                context.currently_long_the_spread = False
There was a runtime error.

Quick update on this. I've reported the issue with Pyfolio to Quantopian. They usually respond pretty quickly.

And the winner is: Florian!

Both Joel and Florian had worked on the pairs trading algorithm we went through in the workshop. Florian's version had an overall better return, but just as importantly, it did so with lower risk. By diversifying his trades across pairs from different sectors, he ended up with a consistently low beta to market, lower volatility, and lower position concentration.

Florian, I will connect you to Delaney to claim your prize. I recommend you try to hedge your pairs using the rolling window beta calculation we did in the workshop. Currently they are 1:1 hedged, which I suspect can be improved upon. You could also try hedging with the ratio of the stocks individual standard deviations. Since the pairs are closely related companies from the same industries, they may have similar leverage and risk factors, so their volatility may contain enough information to hedge them, and may be more stable than the OLS beta.

Note: I can't attach the PyFolio analysis just yet, as there seems to be another bug in Research.