Back to Community
Kalman Filter multiple Pairs Trading

Hi, I built an strategy based on someone else post, which applies Kalman Filter to calculate the hedge ratio, but there is one problem of my algorithm. Also, I include more than one pair in my portfolio.

I manage to keep the leverage of my portfolio under 1, so I use the function computeHoldingsPct(yShares, xShares, yPrice, xPrice) and for each pair I keep the percentage as y_target_pct / float(context.num_pairs), but why my leverage still go over 1 all the time?

Thanks

Clone Algorithm
50
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
import pytz


def initialize(context):
    # Quantopian backtester specific variables
    set_slippage(slippage.VolumeShareSlippage(volume_limit=0.025, price_impact=0.03))
    set_commission(commission.PerShare(cost=0.0075))
    
    context.stock_pairs = [(sid(26807), sid(28320)),
                          (sid(14516), sid(14517))]
    
    context.num_pairs = len(context.stock_pairs)
    
    context.delta = 0.0001
    context.Vw = context.delta / (1 - context.delta) * np.eye(2)
    context.Ve = 0.001
    
    context.beta = [np.zeros(2)]*context.num_pairs
    context.P = [np.zeros((2, 2))]*context.num_pairs
    context.R = [None] * context.num_pairs
    
    context.pos = [None] * context.num_pairs
    context.day = None

def handle_data(context, data):
    # Record and plot the leverage of our portfolio over time.
    record(leverage = context.account.leverage)
    
    exchange_time = get_datetime().astimezone(pytz.timezone('US/Eastern'))
    
    # update Kalman filter and exectue a trade during the last 1 hour of trading each day
    if exchange_time.hour == 15 and exchange_time.minute >= 0:
        
        # only execute this once per day
        if context.day is not None and context.day == exchange_time.day:
            return
        
        context.day = exchange_time.day
        
        if get_open_orders():
            return
        
        for i in range(context.num_pairs):

            (stock_y, stock_x) = context.stock_pairs[i]
            
            x = np.asarray([data[stock_x].price, 1.0]).reshape((1, 2))
            y = data[stock_y].price

            # update Kalman filter with latest price
            if context.R[i] is not None:
                context.R[i] = context.P[i] + context.Vw
            else:
                context.R[i] = np.zeros((2, 2))
      
            yhat = x.dot(context.beta[i])
    
            Q = x.dot(context.R[i]).dot(x.T) + context.Ve
            sqrt_Q = np.sqrt(Q)
            e = y - yhat
            K = context.R[i].dot(x.T) / Q
            context.beta[i] = context.beta[i] + K.flatten() * e
            context.P[i] = context.R[i] - K * x.dot(context.R[i])
    
            if context.pos[i] is not None:
                
                if context.pos[i] == 'long' and e >= -sqrt_Q:
                    #log.info('closing long')
                    order_target(stock_x, 0)
                    order_target(stock_y, 0)
                    context.pos[i] = None
                   
                elif context.pos[i] == 'short' and e < sqrt_Q:
                    #log.info('closing short')
                    order_target(stock_x, 0)
                    order_target(stock_y, 0)
                    context.pos[i] = None
            
            
            if context.pos[i] is None:
                if e < -sqrt_Q:
                    #log.info('opening long')
                    y_target_shares = 1
                    x_target_shares = -context.beta[i][0]
                    (y_target_pct, x_target_pct) = computeHoldingsPct(y_target_shares,x_target_shares, y, x[0,0])
                                        
                    order_target_percent(stock_y , y_target_pct / float(context.num_pairs))
                    order_target_percent(stock_x , x_target_pct / float(context.num_pairs))
                    context.pos[i] = 'long'
        
                    
                elif e > sqrt_Q:
                    y_target_shares = -1
                    x_target_shares = context.beta[i][0]
                    #log.info('opening short')
                    (y_target_pct, x_target_pct) = computeHoldingsPct(y_target_shares,x_target_shares, y, x[0,0])

                    order_target_percent(stock_y , y_target_pct / float(context.num_pairs))
                    order_target_percent(stock_x , x_target_pct / float(context.num_pairs))
                    context.pos[i] = 'short'

def computeHoldingsPct(yShares, xShares, yPrice, xPrice):
    yDol = yShares * yPrice
    xDol = xShares * xPrice
    notionalDol =  abs(yDol) + abs(xDol)
    y_target_pct = yDol / notionalDol
    x_target_pct = xDol / notionalDol
    return (y_target_pct, x_target_pct)
There was a runtime error.
6 responses

Ryan,

I'm not 100% on how that is happening, but I think it is some interaction of order_target and order_target_percent. If you reorder your logic so that you only every make one call to order_target_percent per security, per day, then that would potentially solve the problem.

If you replace lines 88-89 and 99-100 with...

                    qty_y=int((y_target_pct / float(context.num_pairs))*context.portfolio.portfolio_value/data[stock_y].price)  
                    qty_x=int((x_target_pct / float(context.num_pairs))*context.portfolio.portfolio_value/data[stock_x].price)  
                    order(stock_y, qty_y)  
                    order(stock_x, qty_x)  

...does it work like you would expect?

You may want to examine the following.

Clone the backtest and look at the position values and transactions details for the last day.

See log file:

2011-05-31 PRINT executing  
2011-05-31 PRINT Currently have: (0, 0) of (Equity(28320 [USO]), Equity(26807 [GLD]))  
2011-05-31 handle_data:107 INFO opening short  
2011-05-31 PRINT X: Ordered 0.0 of Equity(28320 [USO]) (TARGET qty=0.0)  
2011-05-31 PRINT Y: Ordered -0.5 of Equity(26807 [GLD]) (TARGET qty=-66.8717400027)  
2011-05-31 PRINT Currently have: (0, 0) of (Equity(14517 [EWC]), Equity(14516 [EWA]))  
2011-05-31 handle_data:107 INFO opening short  
2011-05-31 PRINT X: Ordered 0.0 of Equity(14517 [EWC]) (TARGET qty=0.0)  
2011-05-31 PRINT Y: Ordered -0.5 of Equity(14516 [EWA]) (TARGET qty=-376.081233546)  
2011-06-01 PRINT executing  
2011-06-01 PRINT Currently have: (0, -66) of (Equity(28320 [USO]), Equity(26807 [GLD]))  
2011-06-01 PRINT Currently have: (0, -376) of (Equity(14517 [EWC]), Equity(14516 [EWA]))  
2011-06-02 PRINT executing  
2011-06-02 PRINT Currently have: (0, -66) of (Equity(28320 [USO]), Equity(26807 [GLD]))  
2011-06-02 handle_data:82 INFO closing short: Equity(28320 [USO]),Equity(26807 [GLD])  
2011-06-02 handle_data:90 INFO opening long  
2011-06-02 PRINT X: Ordered -0.249924421497 of Equity(28320 [USO]) (TARGET qty=-127.141834195)  
2011-06-02 PRINT Y: Ordered 0.250075578503 of Equity(26807 [GLD]) (TARGET qty=33.803291719)  
2011-06-02 PRINT Currently have: (0, -376) of (Equity(14517 [EWC]), Equity(14516 [EWA]))  
2011-06-02 handle_data:82 INFO closing short: Equity(14517 [EWC]),Equity(14516 [EWA])  
2011-06-03 PRINT executing  
2011-06-03 PRINT Currently have: (-127, 99) of (Equity(28320 [USO]), Equity(26807 [GLD]))  
2011-06-03 handle_data:76 INFO closing long: Equity(28320 [USO]),Equity(26807 [GLD])  
2011-06-03 handle_data:107 INFO opening short  
2011-06-03 PRINT X: Ordered 0.249915937777 of Equity(28320 [USO]) (TARGET qty=127.676488314)  
2011-06-03 PRINT Y: Ordered -0.250084062223 of Equity(26807 [GLD]) (TARGET qty=-33.7519884182)  
2011-06-03 PRINT Currently have: (0, 0) of (Equity(14517 [EWC]), Equity(14516 [EWA]))  
Clone Algorithm
6
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
import pytz


def initialize(context):
    # Quantopian backtester specific variables
    set_slippage(slippage.VolumeShareSlippage(volume_limit=0.025, price_impact=0.03))
    set_commission(commission.PerShare(cost=0.0075))
    
    context.stock_pairs = [(sid(26807), sid(28320)),
                          (sid(14516), sid(14517))]
    
    context.num_pairs = len(context.stock_pairs)
    
    context.delta = 0.0001
    context.Vw = context.delta / (1 - context.delta) * np.eye(2)
    context.Ve = 0.001
    
    context.beta = [np.zeros(2)]*context.num_pairs
    context.P = [np.zeros((2, 2))]*context.num_pairs
    context.R = [None] * context.num_pairs
    
    context.pos = [None] * context.num_pairs
    context.day = None

def handle_data(context, data):
    # Record and plot the leverage of our portfolio over time.
    record(leverage = context.account.leverage)
    
    exchange_time = get_datetime().astimezone(pytz.timezone('US/Eastern'))
    
    # update Kalman filter and exectue a trade during the last 1 hour of trading each day
    if exchange_time.hour == 15 and exchange_time.minute >= 0:
        
        # only execute this once per day
        if context.day is not None and context.day == exchange_time.day:
            return
        
        context.day = exchange_time.day
        
        if get_open_orders():
            print "had open orders"
            return
        
        print "executing"
        
        for i in range(context.num_pairs):

            (stock_y, stock_x) = context.stock_pairs[i]
            
            qty_x = context.portfolio.positions[stock_x].amount
            qty_y = context.portfolio.positions[stock_y].amount
            print "Currently have: ({}, {}) of ({}, {})".format(qty_x,qty_y,stock_x,stock_y)
            
            x = np.asarray([data[stock_x].price, 1.0]).reshape((1, 2))
            y = data[stock_y].price

            # update Kalman filter with latest price
            if context.R[i] is not None:
                context.R[i] = context.P[i] + context.Vw
            else:
                context.R[i] = np.zeros((2, 2))
      
            yhat = x.dot(context.beta[i])
    
            Q = x.dot(context.R[i]).dot(x.T) + context.Ve
            sqrt_Q = np.sqrt(Q)
            e = y - yhat
            K = context.R[i].dot(x.T) / Q
            context.beta[i] = context.beta[i] + K.flatten() * e
            context.P[i] = context.R[i] - K * x.dot(context.R[i])
    
            if context.pos[i] is not None:
                
                if context.pos[i] == 'long' and e >= -sqrt_Q:
                    log.info('closing long: {},{}'.format(stock_x,stock_y))
                    order_target(stock_x, 0)
                    order_target(stock_y, 0)
                    context.pos[i] = None
                   
                elif context.pos[i] == 'short' and e < sqrt_Q:
                    log.info('closing short: {},{}'.format(stock_x,stock_y))
                    order_target(stock_x, 0)
                    order_target(stock_y, 0)
                    context.pos[i] = None
            
            
            if context.pos[i] is None:
                if e < -sqrt_Q:
                    log.info('opening long')
                    y_target_shares = 1
                    x_target_shares = -context.beta[i][0]
                    (y_target_pct, x_target_pct) = computeHoldingsPct(y_target_shares,x_target_shares, y, x[0,0])
                    pct_x = x_target_pct / float(context.num_pairs)
                    pct_y = y_target_pct / float(context.num_pairs)
                    order_target_percent(stock_y , pct_y)
                    order_target_percent(stock_x , pct_x)
                    target_qty_x = (pct_x * context.portfolio.portfolio_value) / data[stock_x].price
                    target_qty_y = (pct_y * context.portfolio.portfolio_value) / data[stock_y].price
                    print "X: Ordered {} of {} (TARGET qty={})".format(pct_x, stock_x, target_qty_x)
                    print "Y: Ordered {} of {} (TARGET qty={})".format(pct_y, stock_y, target_qty_y)
                    
                    context.pos[i] = 'long'
        
                    
                elif e > sqrt_Q:
                    log.info('opening short')
                    y_target_shares = -1
                    x_target_shares = context.beta[i][0]
                    (y_target_pct, x_target_pct) = computeHoldingsPct(y_target_shares,x_target_shares, y, x[0,0])

                    pct_x = x_target_pct / float(context.num_pairs)
                    pct_y = y_target_pct / float(context.num_pairs)
                    order_target_percent(stock_y , pct_y)
                    order_target_percent(stock_x , pct_x)
                    target_qty_x = (pct_x * context.portfolio.portfolio_value) / data[stock_x].price
                    target_qty_y = (pct_y * context.portfolio.portfolio_value) / data[stock_y].price
                    print "X: Ordered {} of {} (TARGET qty={})".format(pct_x, stock_x, target_qty_x)
                    print "Y: Ordered {} of {} (TARGET qty={})".format(pct_y, stock_y, target_qty_y)
                    context.pos[i] = 'short'

def computeHoldingsPct(yShares, xShares, yPrice, xPrice):
    yDol = yShares * yPrice
    xDol = xShares * xPrice
    notionalDol =  abs(yDol) + abs(xDol)
    y_target_pct = yDol / notionalDol
    x_target_pct = xDol / notionalDol
    return (y_target_pct, x_target_pct)
There was a runtime error.

Sorry for so many posts - I just remembered order_target_percent doesn't account for already submitted orders, so that's why.

How would you fix this then...i'm shooting a blank

"There's more than one way to do it"

Clone Algorithm
16
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
import pytz


def initialize(context):
    # Quantopian backtester specific variables
    set_slippage(slippage.VolumeShareSlippage(volume_limit=0.025, price_impact=0.03))
    set_commission(commission.PerShare(cost=0.0075))
    
    context.stock_pairs = [(sid(26807), sid(28320)),
                          (sid(14516), sid(14517))]
    
    context.num_pairs = len(context.stock_pairs)
    
    context.delta = 0.0001
    context.Vw = context.delta / (1 - context.delta) * np.eye(2)
    context.Ve = 0.001
    
    context.beta = [np.zeros(2)]*context.num_pairs
    context.P = [np.zeros((2, 2))]*context.num_pairs
    context.R = [None] * context.num_pairs
    
    context.pos = [None] * context.num_pairs
    context.day = None

def handle_data(context, data):
    # Record and plot the leverage of our portfolio over time.
    record(leverage = context.account.leverage)
    
    exchange_time = get_datetime().astimezone(pytz.timezone('US/Eastern'))
    
    # update Kalman filter and exectue a trade during the last 1 hour of trading each day
    if exchange_time.hour == 15 and exchange_time.minute >= 0:
        
        # only execute this once per day
        if context.day is not None and context.day == exchange_time.day:
            return
        
        context.day = exchange_time.day
        
        if get_open_orders():
            return
        
        for i in range(context.num_pairs):

            OrderTargetPct = {} # keep track of what to order (if anything)
            
            (stock_y, stock_x) = context.stock_pairs[i]
            
            x = np.asarray([data[stock_x].price, 1.0]).reshape((1, 2))
            y = data[stock_y].price

            # update Kalman filter with latest price
            if context.R[i] is not None:
                context.R[i] = context.P[i] + context.Vw
            else:
                context.R[i] = np.zeros((2, 2))
      
            yhat = x.dot(context.beta[i])
    
            Q = x.dot(context.R[i]).dot(x.T) + context.Ve
            sqrt_Q = np.sqrt(Q)
            e = y - yhat
            K = context.R[i].dot(x.T) / Q
            context.beta[i] = context.beta[i] + K.flatten() * e
            context.P[i] = context.R[i] - K * x.dot(context.R[i])
    
            if context.pos[i] is not None:
                
                if context.pos[i] == 'long' and e >= -sqrt_Q:
                    #log.info('closing long')
                    OrderTargetPct[stock_x] = 0
                    OrderTargetPct[stock_y] = 0
                    context.pos[i] = None
                   
                elif context.pos[i] == 'short' and e < sqrt_Q:
                    #log.info('closing short')
                    OrderTargetPct[stock_x] = 0
                    OrderTargetPct[stock_y] = 0
                    context.pos[i] = None
            
            
            if context.pos[i] is None:
                if e < -sqrt_Q:
                    #log.info('opening long')
                    y_target_shares = 1
                    x_target_shares = -context.beta[i][0]
                    (y_target_pct, x_target_pct) = computeHoldingsPct(y_target_shares,x_target_shares, y, x[0,0])
                                        
                    OrderTargetPct[stock_x] = x_target_pct / float(context.num_pairs)
                    OrderTargetPct[stock_y] = y_target_pct / float(context.num_pairs)
                        
                    context.pos[i] = 'long'
        
                    
                elif e > sqrt_Q:
                    y_target_shares = -1
                    x_target_shares = context.beta[i][0]
                    #log.info('opening short')
                    (y_target_pct, x_target_pct) = computeHoldingsPct(y_target_shares,x_target_shares, y, x[0,0])

                    OrderTargetPct[stock_x] = x_target_pct / float(context.num_pairs)
                    OrderTargetPct[stock_y] = y_target_pct / float(context.num_pairs)
                    
                    context.pos[i] = 'short'
                    
            # order
            for sid in OrderTargetPct.keys():
                order_target_percent(sid, OrderTargetPct[sid])
                

def computeHoldingsPct(yShares, xShares, yPrice, xPrice):
    yDol = yShares * yPrice
    xDol = xShares * xPrice
    notionalDol =  abs(yDol) + abs(xDol)
    y_target_pct = yDol / notionalDol
    x_target_pct = xDol / notionalDol
    return (y_target_pct, x_target_pct)
There was a runtime error.

@James, thanks, so what does the code below do exactly?

for sid in OrderTargetPct.keys():
order_target_percent(sid, OrderTargetPct[sid])