Back to Community
Need help with a Pairs trading algo - bad at coding!

I have a code from an old trading platform that I need to convert to quantopian logic. This pits VTI:SPY as a pair trading system:

Here is the old code, sorry I'm really bad at algorithmic code and need help. This is a steady wealth accumulator. I have added notes to describe what I am trying to accomplish.

Thanks for anyone that is willing to help!

SYMLIST(VTI,SPY)

/*CALCULATE THE PAIR RATIO*/

SET{LEFT, IND(VTI,CLOSE)}
SET{RIGHT, IND(SPY,CLOSE)}
SET{RATIO, LEFT / RIGHT}
SET{PAIRSPREAD, LEFT - RIGHT}

/*DETERMINE THE 14 DAY MOVING AVERAGE FOR THE RATIO*/

SET{RATIOMA14, CMA(RATIO,14)}
SET{SPREADMA14, CMA(PAIRSPREAD,14)}

/*DETERMINE THE PERCENT ABOVE/BELOW THE MA(14) FOR THE RATIO*/

SET{RATIOPCT1, RATIO / RATIOMA14}
SET{RATIOPCT2, RATIOPCT1 - 1}
SET{RATIOPCT, RATIOPCT2 * 100}

/*DETERMINE THE 14 DAY ZSCORE FOR THE RATIO*/

SET{RATIOSTD14, CSTDDEV(RATIO,14)}
SET{RATIO2STD, 2 * RATIOSTD14}
SET{RATIOUPPERBB, RATIOMA14 + RATIO2STD}
SET{RATIOLOWERBB, RATIOMA14 - RATIO2STD}
SET{RATIODIFF14, RATIO - RATIOMA14}
SET{RATIOZSCORE, RATIODIFF14 / RATIOSTD14}

SET{SPREADSTD14, CSTDDEV(PAIRSPREAD,14)}
SET{SPREADDIFF14, PAIRSPREAD - SPREADMA14}
SET{SPREADZSCORE, SPREADDIFF14 / SPREADSTD14}

/*DETERMINE DIRECTION OF PAIR TRADE AND THE NUMBER OF SHARES TO BE BOUGHT OR SOLD*/

SET{BELOWLBB, COUNT(RATIOZSCORE BELOW -2,1)}
SET{ABOVEUBB, COUNT(RATIOZSCORE ABOVE 2,1)}

SET{LEFTSHARES1, 10000 / LEFT}
SET{LEFTSHARES, ROUND(LEFTSHARES1, 0)}
SET{LEFTLONG, BELOWLBB * LEFTSHARES}
SET{LEFTSHORT, ABOVEUBB * LEFTSHARES}

SET{RIGHTSHARES1, 10000 / RIGHT}
SET{RIGHTSHARES, ROUND(RIGHTSHARES1, 0)}
SET{RIGHTLONG, ABOVEUBB * RIGHTSHARES}
SET{RIGHTSHORT, BELOWLBB * RIGHTSHARES}

Hopefully this can be coded as it has a 100% winning record from 2011!

17 responses

What does the cma() function do? Does it return an average, or a rolling average (i.e., just a number, or a vector (series of numbers))?

Same question for the cstddev()?

Could you describe the workings of the function count()?

Thanks in advance,

Tim

CMA is custom moving average - so yes, in this case it is the custom moving average of the 14 day ratio
CSTDDEV is Custom Standard Deviation, which is again based on the 14 day ratio
Function count just counts the number of occurrences, it will either be a 1 or a 0. It is just looking to see if the ratio falls above the Upper band or below the Lower band.

Hopefully this helps!

So BELOWBB is the number of times during the past 14 days in which RATIOZSCORE was lower than -2?

Kinda. BELOWBB is looking at the actual RATIOZSCORE that was already calculated using the 14 day ratio (CMA). The count is only looking at the current day denoted by the ,1 at the end. So basically the code is looking at the RATIOZSCORE that was calculated above and looking for either below -2 or above 2 or else it stays in cash.

Also, I noticed there is no exit, but the exit is when the ratio crossed the middle ratio14 - if that makes any sense.

I can't post pictures, but I have a picture where you can see where the ratio exceeded -2 and 2 triggering buys - then retreated to the ratio14 where once it crosses you exit.

Do you know how to post pictures?

Here's something I wrote based on my current understanding of your algorithm.

Unfortunately, it does not behave quite as expected, I suppose, but perhaps it can serve as a starting point for improvements.

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


def initialize(context):
        
    schedule_function(rebalance, 
                      date_rules.every_day(),
                      time_rules.market_close(minutes = 30))  

    context.vti = sid(22739)
    context.spy = sid(8554)
    
    context.secs = [context.vti, context.spy]

    context.trading = False
    
def rebalance(context,data):

    P = data.history(context.secs, 'price', 14, '1d')      
                        
    ratio = P[context.vti] / P[context.spy]
    xratio = ratio.median()
    sratio = ratio.std()
    dratio = ratio.iloc[-1] - xratio
    zscore = dratio / sratio
    
    if not context.trading:
        if zscore > 2:
            order_target_percent(context.vti, 1.0)
            order_target_percent(context.spy, -1.0)
            context.trading = True
        elif zscore < -2:    
            order_target_percent(context.vti, -1.0)
            order_target_percent(context.spy, 1.0)
            context.trading = True    
    else:
        if np.abs(zscore) < 0.01:
            order_target(context.vti, 0)
            order_target(context.spy, 0)
            context.trading = False   
    
    pvr(context, data)


def handle_data(context,data):    

    pass


def pvr(context, data):  
    ''' Custom chart and/or log of profit_vs_risk returns and related information  
    '''  
    # # # # # # # # # #  Options  # # # # # # # # # #  
    record_max_lvrg = 1         # Maximum leverage encountered  
    record_leverage = 0         # Leverage (context.account.leverage)  
    record_q_return = 0         # Quantopian returns (percentage)  
    record_pvr      = 1         # Profit vs Risk returns (percentage)  
    record_pnl      = 0         # Profit-n-Loss  
    record_shorting = 1         # Total value of any shorts  
    record_overshrt = 0         # Shorts beyond longs+cash  
    record_risk     = 0         # Risked, max cash spent or shorts beyond longs+cash  
    record_risk_hi  = 1         # Highest risk overall  
    record_cash     = 0         # Cash available  
    record_cash_low = 1         # Any new lowest cash level  
    logging         = 1         # Also to logging window conditionally (1) or not (0)  
    log_method      = 'risk_hi' # 'daily' or 'risk_hi'

    from pytz import timezone   # Python will only do once, makes this portable.  
                                #   Move to top of algo for better efficiency.  
    c = context  # Brevity is the soul of wit -- Shakespeare [for efficiency, readability]  
    if 'pvr' not in c:  
        date_strt = get_environment('start').date()  
        date_end  = get_environment('end').date()  
        cash_low  = c.portfolio.starting_cash  
        mode      = get_environment('data_frequency')  
        c.pvr = {  
            'max_lvrg': 0,  
            'risk_hi' : 0,  
            'days'    : 0.0,  
            'date_prv': '',  
            'cash_low': cash_low,  
            'date_end': date_end,  
            'mode'    : mode,  
            'run_str' : '{} to {}  {}  {}'.format(date_strt,date_end,int(cash_low),mode)  
        }  
        log.info(c.pvr['run_str'])  
    pvr_rtrn     = 0            # Profit vs Risk returns based on maximum spent  
    profit_loss  = 0            # Profit-n-loss  
    shorts       = 0            # Shorts value  
    longs        = 0            # Longs  value  
    overshorts   = 0            # Shorts value beyond longs plus cash  
    new_risk_hi  = 0  
    new_cash_low = 0                           # To trigger logging in cash_low case  
    lvrg         = c.account.leverage          # Standard leverage, in-house  
    date         = get_datetime().date()       # To trigger logging in daily case  
    cash         = c.portfolio.cash  
    start        = c.portfolio.starting_cash  
    cash_dip     = int(max(0, start - cash))  
    q_rtrn       = 100 * (c.portfolio.portfolio_value - start) / start

    if int(cash) < c.pvr['cash_low']:                # New cash low  
        new_cash_low = 1  
        c.pvr['cash_low']   = int(cash)  
        if record_cash_low:  
            record(CashLow = int(c.pvr['cash_low'])) # Lowest cash level hit

    if record_max_lvrg:  
        if c.account.leverage > c.pvr['max_lvrg']:  
            c.pvr['max_lvrg'] = c.account.leverage  
            record(MaxLv = c.pvr['max_lvrg'])        # Maximum leverage

    if record_pnl:  
        profit_loss = c.portfolio.pnl  
        record(PnL = profit_loss)                    # "Profit and Loss" in dollars

    for p in c.portfolio.positions:  
        shrs = c.portfolio.positions[p].amount  
        if shrs < 0:  
            shorts += int(abs(shrs * data.current(p, 'price'))) 
        if shrs > 0:  
            longs  += int(shrs * data.current(p, 'price'))

    if shorts > longs + cash: overshorts = shorts             # Shorts when too high  
    if record_shorting: record(Shorts  = shorts)              # Shorts value as a positve  
    if record_overshrt: record(OvrShrt = overshorts)          # Shorts value as a positve  
    if record_cash:     record(Cash = int(c.portfolio.cash))  # Cash  
    if record_leverage: record(Lvrg = c.account.leverage)     # Leverage

    risk = int(max(cash_dip, shorts))  
    if record_risk: record(Risk = risk)       # Amount in play, maximum of shorts or cash used

    if risk > c.pvr['risk_hi']:  
        c.pvr['risk_hi'] = risk  
        new_risk_hi = 1

        if record_risk_hi:  
            record(RiskHi = c.pvr['risk_hi']) # Highest risk overall

    if record_pvr:      # Profit_vs_Risk returns based on max amount actually spent (risk high)  
        if c.pvr['risk_hi'] != 0:     # Avoid zero-divide  
            pvr_rtrn = 100 * (c.portfolio.portfolio_value - start) / c.pvr['risk_hi']  
            record(PvR = pvr_rtrn)            # Profit_vs_Risk returns

    if record_q_return:  
        record(QRet = q_rtrn)                 # Quantopian returns to compare to pvr returns curve

    def _minute():   # To preface each line with minute of the day.  
        if get_environment('data_frequency') == 'minute':  
            bar_dt = get_datetime().astimezone(timezone('US/Eastern'))  
            minute = (bar_dt.hour * 60) + bar_dt.minute - 570  # (-570 = 9:31a)  
            return str(minute).rjust(3)  
        return ''    # Daily mode, just leave it out.

    def _pvr_():  
            log.info('PvR {} %/day     {}'.format(  
                '%.4f' % (pvr_rtrn / c.pvr['days']), c.pvr['run_str']))  
            log.info('  Profited {} on {} activated/transacted for PvR of {}%'.format(  
                '%.0f' % (c.portfolio.portfolio_value - start), '%.0f' % c.pvr['risk_hi'],  
                '%.1f' % pvr_rtrn))  
            log.info('  QRet {} PvR {} CshLw {} MxLv {} RskHi {} Shrts {}'.format(  
                '%.2f' % q_rtrn, '%.2f' % pvr_rtrn, '%.0f' % c.pvr['cash_low'],  
                '%.2f' % c.pvr['max_lvrg'], '%.0f' % c.pvr['risk_hi'], '%.0f' % shorts))

    if logging:  
        if log_method == 'risk_hi' and new_risk_hi \
          or log_method == 'daily' and c.pvr['date_prv'] != date \
          or new_cash_low:  
            qret    = ' QRet '   + '%.1f' % q_rtrn  
            lv      = ' Lv '     + '%.1f' % lvrg              if record_leverage else ''  
            pvr     = ' PvR '    + '%.1f' % pvr_rtrn          if record_pvr      else ''  
            pnl     = ' PnL '    + '%.0f' % profit_loss       if record_pnl      else ''  
            csh     = ' Cash '   + '%.0f' % cash              if record_cash     else ''  
            shrt    = ' Shrt '   + '%.0f' % shorts            if record_shorting else ''  
            ovrshrt = ' Shrt '   + '%.0f' % overshorts        if record_overshrt else ''  
            risk    = ' Risk '   + '%.0f' % risk              if record_risk     else ''  
            mxlv    = ' MaxLv '  + '%.2f' % c.pvr['max_lvrg'] if record_max_lvrg else ''  
            csh_lw  = ' CshLw '  + '%.0f' % c.pvr['cash_low'] if record_cash_low else ''  
            rsk_hi  = ' RskHi '  + '%.0f' % c.pvr['risk_hi']  if record_risk_hi  else ''  
            log.info('{}{}{}{}{}{}{}{}{}{}{}{}'.format(_minute(),  
               lv, mxlv, qret, pvr, pnl, csh, csh_lw, shrt, ovrshrt, risk, rsk_hi))  
    if c.pvr['date_prv'] != date: c.pvr['days'] += 1.0  
    if c.pvr['days'] % 130 == 0 and _minute() == '100': _pvr_()  
    c.pvr['date_prv'] = date  
    if c.pvr['date_end'] == date:  
        # Summary on last minute of last day.  
        # If using schedule_function(), backtest last day/time may need to match for this to execute.  
        if 'pvr_summary_done' not in c: c.pvr_summary_done = 0  
        log_summary = 0  
        if c.pvr['mode'] == 'daily' and get_datetime().date() == c.pvr['date_end']:  
            log_summary = 1  
        elif c.pvr['mode'] == 'minute' and get_datetime() == get_environment('end'):  
            log_summary = 1  
        if log_summary and not c.pvr_summary_done:  
            _pvr_()  
            c.pvr_summary_done = 1

There was a runtime error.

Nice! One other thing. If the ratio is above 2 you short both Vti and SPY and you would go long Vti & SPY when the ratio is below -2

Maybe that will clear up some of the disconnect or was that already built in?

Sorry, long day at work. You would go long one and short the other. I think the original code setup which to go long and which to short

Tim,

I think you intended to use mean instead of median for the ratio's moving average. Also, this will only exit trades if the zscore = +/- 0.1 (which wouldn't capture when the zscore crosses zero but doesn't land in this range).

I made these updates but it didn't make the algo any better looking.

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
import numpy as np


def initialize(context):
        
    schedule_function(rebalance, 
                      date_rules.every_day(),
                      time_rules.market_close(minutes = 30))  

    context.vti = sid(22739)
    context.spy = sid(8554)
    
    context.secs = [context.vti, context.spy]
    context.trading = False
    context.z_trigger = 0
    
def rebalance(context,data):

    P = data.history(context.secs, 'price', 14, '1d')      
                        
    ratio = P[context.vti] / P[context.spy]
    xratio = ratio.mean()
    sratio = ratio.std()
    dratio = ratio.iloc[-1] - xratio
    
    zscore = dratio / sratio
    
    if not context.trading:
        if zscore > 2:
            order_target_percent(context.vti, 1.0)
            order_target_percent(context.spy, -1.0)
            context.z_trigger = zscore
            context.trading = True
        elif zscore < -2:    
            order_target_percent(context.vti, -1.0)
            order_target_percent(context.spy, 1.0)
            context.z_trigger = zscore
            context.trading = True    
    else:
        if (zscore > 0 and context.z_trigger < 0) or (zscore < 0 and context.z_trigger > 0): 
            order_target(context.vti, 0)
            order_target(context.spy, 0)
            context.trading = False   
    
    pvr(context, data)


def handle_data(context,data):    

    pass


def pvr(context, data):  
    ''' Custom chart and/or log of profit_vs_risk returns and related information  
    '''  
    # # # # # # # # # #  Options  # # # # # # # # # #  
    record_max_lvrg = 1         # Maximum leverage encountered  
    record_leverage = 0         # Leverage (context.account.leverage)  
    record_q_return = 0         # Quantopian returns (percentage)  
    record_pvr      = 1         # Profit vs Risk returns (percentage)  
    record_pnl      = 0         # Profit-n-Loss  
    record_shorting = 1         # Total value of any shorts  
    record_overshrt = 0         # Shorts beyond longs+cash  
    record_risk     = 0         # Risked, max cash spent or shorts beyond longs+cash  
    record_risk_hi  = 1         # Highest risk overall  
    record_cash     = 0         # Cash available  
    record_cash_low = 1         # Any new lowest cash level  
    logging         = 1         # Also to logging window conditionally (1) or not (0)  
    log_method      = 'risk_hi' # 'daily' or 'risk_hi'

    from pytz import timezone   # Python will only do once, makes this portable.  
                                #   Move to top of algo for better efficiency.  
    c = context  # Brevity is the soul of wit -- Shakespeare [for efficiency, readability]  
    if 'pvr' not in c:  
        date_strt = get_environment('start').date()  
        date_end  = get_environment('end').date()  
        cash_low  = c.portfolio.starting_cash  
        mode      = get_environment('data_frequency')  
        c.pvr = {  
            'max_lvrg': 0,  
            'risk_hi' : 0,  
            'days'    : 0.0,  
            'date_prv': '',  
            'cash_low': cash_low,  
            'date_end': date_end,  
            'mode'    : mode,  
            'run_str' : '{} to {}  {}  {}'.format(date_strt,date_end,int(cash_low),mode)  
        }  
        log.info(c.pvr['run_str'])  
    pvr_rtrn     = 0            # Profit vs Risk returns based on maximum spent  
    profit_loss  = 0            # Profit-n-loss  
    shorts       = 0            # Shorts value  
    longs        = 0            # Longs  value  
    overshorts   = 0            # Shorts value beyond longs plus cash  
    new_risk_hi  = 0  
    new_cash_low = 0                           # To trigger logging in cash_low case  
    lvrg         = c.account.leverage          # Standard leverage, in-house  
    date         = get_datetime().date()       # To trigger logging in daily case  
    cash         = c.portfolio.cash  
    start        = c.portfolio.starting_cash  
    cash_dip     = int(max(0, start - cash))  
    q_rtrn       = 100 * (c.portfolio.portfolio_value - start) / start

    if int(cash) < c.pvr['cash_low']:                # New cash low  
        new_cash_low = 1  
        c.pvr['cash_low']   = int(cash)  
        if record_cash_low:  
            record(CashLow = int(c.pvr['cash_low'])) # Lowest cash level hit

    if record_max_lvrg:  
        if c.account.leverage > c.pvr['max_lvrg']:  
            c.pvr['max_lvrg'] = c.account.leverage  
            record(MaxLv = c.pvr['max_lvrg'])        # Maximum leverage

    if record_pnl:  
        profit_loss = c.portfolio.pnl  
        record(PnL = profit_loss)                    # "Profit and Loss" in dollars

    for p in c.portfolio.positions:  
        shrs = c.portfolio.positions[p].amount  
        if shrs < 0:  
            shorts += int(abs(shrs * data.current(p, 'price'))) 
        if shrs > 0:  
            longs  += int(shrs * data.current(p, 'price'))

    if shorts > longs + cash: overshorts = shorts             # Shorts when too high  
    if record_shorting: record(Shorts  = shorts)              # Shorts value as a positve  
    if record_overshrt: record(OvrShrt = overshorts)          # Shorts value as a positve  
    if record_cash:     record(Cash = int(c.portfolio.cash))  # Cash  
    if record_leverage: record(Lvrg = c.account.leverage)     # Leverage

    risk = int(max(cash_dip, shorts))  
    if record_risk: record(Risk = risk)       # Amount in play, maximum of shorts or cash used

    if risk > c.pvr['risk_hi']:  
        c.pvr['risk_hi'] = risk  
        new_risk_hi = 1

        if record_risk_hi:  
            record(RiskHi = c.pvr['risk_hi']) # Highest risk overall

    if record_pvr:      # Profit_vs_Risk returns based on max amount actually spent (risk high)  
        if c.pvr['risk_hi'] != 0:     # Avoid zero-divide  
            pvr_rtrn = 100 * (c.portfolio.portfolio_value - start) / c.pvr['risk_hi']  
            record(PvR = pvr_rtrn)            # Profit_vs_Risk returns

    if record_q_return:  
        record(QRet = q_rtrn)                 # Quantopian returns to compare to pvr returns curve

    def _minute():   # To preface each line with minute of the day.  
        if get_environment('data_frequency') == 'minute':  
            bar_dt = get_datetime().astimezone(timezone('US/Eastern'))  
            minute = (bar_dt.hour * 60) + bar_dt.minute - 570  # (-570 = 9:31a)  
            return str(minute).rjust(3)  
        return ''    # Daily mode, just leave it out.

    def _pvr_():  
            log.info('PvR {} %/day     {}'.format(  
                '%.4f' % (pvr_rtrn / c.pvr['days']), c.pvr['run_str']))  
            log.info('  Profited {} on {} activated/transacted for PvR of {}%'.format(  
                '%.0f' % (c.portfolio.portfolio_value - start), '%.0f' % c.pvr['risk_hi'],  
                '%.1f' % pvr_rtrn))  
            log.info('  QRet {} PvR {} CshLw {} MxLv {} RskHi {} Shrts {}'.format(  
                '%.2f' % q_rtrn, '%.2f' % pvr_rtrn, '%.0f' % c.pvr['cash_low'],  
                '%.2f' % c.pvr['max_lvrg'], '%.0f' % c.pvr['risk_hi'], '%.0f' % shorts))

    if logging:  
        if log_method == 'risk_hi' and new_risk_hi \
          or log_method == 'daily' and c.pvr['date_prv'] != date \
          or new_cash_low:  
            qret    = ' QRet '   + '%.1f' % q_rtrn  
            lv      = ' Lv '     + '%.1f' % lvrg              if record_leverage else ''  
            pvr     = ' PvR '    + '%.1f' % pvr_rtrn          if record_pvr      else ''  
            pnl     = ' PnL '    + '%.0f' % profit_loss       if record_pnl      else ''  
            csh     = ' Cash '   + '%.0f' % cash              if record_cash     else ''  
            shrt    = ' Shrt '   + '%.0f' % shorts            if record_shorting else ''  
            ovrshrt = ' Shrt '   + '%.0f' % overshorts        if record_overshrt else ''  
            risk    = ' Risk '   + '%.0f' % risk              if record_risk     else ''  
            mxlv    = ' MaxLv '  + '%.2f' % c.pvr['max_lvrg'] if record_max_lvrg else ''  
            csh_lw  = ' CshLw '  + '%.0f' % c.pvr['cash_low'] if record_cash_low else ''  
            rsk_hi  = ' RskHi '  + '%.0f' % c.pvr['risk_hi']  if record_risk_hi  else ''  
            log.info('{}{}{}{}{}{}{}{}{}{}{}{}'.format(_minute(),  
               lv, mxlv, qret, pvr, pnl, csh, csh_lw, shrt, ovrshrt, risk, rsk_hi))  
    if c.pvr['date_prv'] != date: c.pvr['days'] += 1.0  
    if c.pvr['days'] % 130 == 0 and _minute() == '100': _pvr_()  
    c.pvr['date_prv'] = date  
    if c.pvr['date_end'] == date:  
        # Summary on last minute of last day.  
        # If using schedule_function(), backtest last day/time may need to match for this to execute.  
        if 'pvr_summary_done' not in c: c.pvr_summary_done = 0  
        log_summary = 0  
        if c.pvr['mode'] == 'daily' and get_datetime().date() == c.pvr['date_end']:  
            log_summary = 1  
        elif c.pvr['mode'] == 'minute' and get_datetime() == get_environment('end'):  
            log_summary = 1  
        if log_summary and not c.pvr_summary_done:  
            _pvr_()  
            c.pvr_summary_done = 1

There was a runtime error.

Thanks, Martin,

Great solution for the zero-crossing problem. The median on the other hand, was used on purpose, because it is a more robust estimator than the mean when only a small number of samples is available (14). This detail does not influence the results, though, as you have pointed out. Something is missing ....
Perhaps we should test for cointegration of SPY an VTI first ...

I have simplified the zero-crossing condition a bit and have re-inserted the median.

One of the issues was that the leverage and exposure (net leverage) where not the way they should be because lots of orders went unfilled. The algo was trading at market close with a capital of 1M$. Now it is trading in the morning with 10000$ and the leverage and exposure graph looks better, although it still leaves to be desired.

I also believe that I got the signs of the long and short positions wrong initially.

With all these changes the algo's performance is now almost entirely flat ...

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


def initialize(context):
        
    schedule_function(rebalance, 
                      date_rules.every_day(),
                      time_rules.market_open(minutes = 30))  

    context.vti = sid(22739)
    context.spy = sid(8554)
    
    context.secs = [context.vti, context.spy]
    context.trading = False
    context.z_trigger = 0
    
def rebalance(context,data):

    P = data.history(context.secs, 'price', 14, '1d')      
                        
    ratio = P[context.vti] / P[context.spy]
    xratio = ratio.median()
    sratio = ratio.std()
    dratio = ratio.iloc[-2] - xratio
    
    zscore = dratio / sratio
    
    if not context.trading:
        if zscore > 2:
            order_target_percent(context.vti, -1.0)
            order_target_percent(context.spy, 1.0)
            context.z_trigger = zscore
            context.trading = True
        elif zscore < -2:    
            order_target_percent(context.vti, 1.0)
            order_target_percent(context.spy, -1.0)
            context.z_trigger = zscore
            context.trading = True    
    else:
        if zscore * context.z_trigger < 0: 
            order_target(context.vti, 0)
            order_target(context.spy, 0)
            context.trading = False   
    


def handle_data(context,data):    

    record(leverage = context.account.leverage,
           exposure = context.account.net_leverage)


def pvr(context, data):  
    ''' Custom chart and/or log of profit_vs_risk returns and related information  
    '''  
    # # # # # # # # # #  Options  # # # # # # # # # #  
    record_max_lvrg = 1         # Maximum leverage encountered  
    record_leverage = 0         # Leverage (context.account.leverage)  
    record_q_return = 0         # Quantopian returns (percentage)  
    record_pvr      = 1         # Profit vs Risk returns (percentage)  
    record_pnl      = 0         # Profit-n-Loss  
    record_shorting = 1         # Total value of any shorts  
    record_overshrt = 0         # Shorts beyond longs+cash  
    record_risk     = 0         # Risked, max cash spent or shorts beyond longs+cash  
    record_risk_hi  = 1         # Highest risk overall  
    record_cash     = 0         # Cash available  
    record_cash_low = 1         # Any new lowest cash level  
    logging         = 1         # Also to logging window conditionally (1) or not (0)  
    log_method      = 'risk_hi' # 'daily' or 'risk_hi'

    from pytz import timezone   # Python will only do once, makes this portable.  
                                #   Move to top of algo for better efficiency.  
    c = context  # Brevity is the soul of wit -- Shakespeare [for efficiency, readability]  
    if 'pvr' not in c:  
        date_strt = get_environment('start').date()  
        date_end  = get_environment('end').date()  
        cash_low  = c.portfolio.starting_cash  
        mode      = get_environment('data_frequency')  
        c.pvr = {  
            'max_lvrg': 0,  
            'risk_hi' : 0,  
            'days'    : 0.0,  
            'date_prv': '',  
            'cash_low': cash_low,  
            'date_end': date_end,  
            'mode'    : mode,  
            'run_str' : '{} to {}  {}  {}'.format(date_strt,date_end,int(cash_low),mode)  
        }  
        log.info(c.pvr['run_str'])  
    pvr_rtrn     = 0            # Profit vs Risk returns based on maximum spent  
    profit_loss  = 0            # Profit-n-loss  
    shorts       = 0            # Shorts value  
    longs        = 0            # Longs  value  
    overshorts   = 0            # Shorts value beyond longs plus cash  
    new_risk_hi  = 0  
    new_cash_low = 0                           # To trigger logging in cash_low case  
    lvrg         = c.account.leverage          # Standard leverage, in-house  
    date         = get_datetime().date()       # To trigger logging in daily case  
    cash         = c.portfolio.cash  
    start        = c.portfolio.starting_cash  
    cash_dip     = int(max(0, start - cash))  
    q_rtrn       = 100 * (c.portfolio.portfolio_value - start) / start

    if int(cash) < c.pvr['cash_low']:                # New cash low  
        new_cash_low = 1  
        c.pvr['cash_low']   = int(cash)  
        if record_cash_low:  
            record(CashLow = int(c.pvr['cash_low'])) # Lowest cash level hit

    if record_max_lvrg:  
        if c.account.leverage > c.pvr['max_lvrg']:  
            c.pvr['max_lvrg'] = c.account.leverage  
            record(MaxLv = c.pvr['max_lvrg'])        # Maximum leverage

    if record_pnl:  
        profit_loss = c.portfolio.pnl  
        record(PnL = profit_loss)                    # "Profit and Loss" in dollars

    for p in c.portfolio.positions:  
        shrs = c.portfolio.positions[p].amount  
        if shrs < 0:  
            shorts += int(abs(shrs * data.current(p, 'price'))) 
        if shrs > 0:  
            longs  += int(shrs * data.current(p, 'price'))

    if shorts > longs + cash: overshorts = shorts             # Shorts when too high  
    if record_shorting: record(Shorts  = shorts)              # Shorts value as a positve  
    if record_overshrt: record(OvrShrt = overshorts)          # Shorts value as a positve  
    if record_cash:     record(Cash = int(c.portfolio.cash))  # Cash  
    if record_leverage: record(Lvrg = c.account.leverage)     # Leverage

    risk = int(max(cash_dip, shorts))  
    if record_risk: record(Risk = risk)       # Amount in play, maximum of shorts or cash used

    if risk > c.pvr['risk_hi']:  
        c.pvr['risk_hi'] = risk  
        new_risk_hi = 1

        if record_risk_hi:  
            record(RiskHi = c.pvr['risk_hi']) # Highest risk overall

    if record_pvr:      # Profit_vs_Risk returns based on max amount actually spent (risk high)  
        if c.pvr['risk_hi'] != 0:     # Avoid zero-divide  
            pvr_rtrn = 100 * (c.portfolio.portfolio_value - start) / c.pvr['risk_hi']  
            record(PvR = pvr_rtrn)            # Profit_vs_Risk returns

    if record_q_return:  
        record(QRet = q_rtrn)                 # Quantopian returns to compare to pvr returns curve

    def _minute():   # To preface each line with minute of the day.  
        if get_environment('data_frequency') == 'minute':  
            bar_dt = get_datetime().astimezone(timezone('US/Eastern'))  
            minute = (bar_dt.hour * 60) + bar_dt.minute - 570  # (-570 = 9:31a)  
            return str(minute).rjust(3)  
        return ''    # Daily mode, just leave it out.

    def _pvr_():  
            log.info('PvR {} %/day     {}'.format(  
                '%.4f' % (pvr_rtrn / c.pvr['days']), c.pvr['run_str']))  
            log.info('  Profited {} on {} activated/transacted for PvR of {}%'.format(  
                '%.0f' % (c.portfolio.portfolio_value - start), '%.0f' % c.pvr['risk_hi'],  
                '%.1f' % pvr_rtrn))  
            log.info('  QRet {} PvR {} CshLw {} MxLv {} RskHi {} Shrts {}'.format(  
                '%.2f' % q_rtrn, '%.2f' % pvr_rtrn, '%.0f' % c.pvr['cash_low'],  
                '%.2f' % c.pvr['max_lvrg'], '%.0f' % c.pvr['risk_hi'], '%.0f' % shorts))

    if logging:  
        if log_method == 'risk_hi' and new_risk_hi \
          or log_method == 'daily' and c.pvr['date_prv'] != date \
          or new_cash_low:  
            qret    = ' QRet '   + '%.1f' % q_rtrn  
            lv      = ' Lv '     + '%.1f' % lvrg              if record_leverage else ''  
            pvr     = ' PvR '    + '%.1f' % pvr_rtrn          if record_pvr      else ''  
            pnl     = ' PnL '    + '%.0f' % profit_loss       if record_pnl      else ''  
            csh     = ' Cash '   + '%.0f' % cash              if record_cash     else ''  
            shrt    = ' Shrt '   + '%.0f' % shorts            if record_shorting else ''  
            ovrshrt = ' Shrt '   + '%.0f' % overshorts        if record_overshrt else ''  
            risk    = ' Risk '   + '%.0f' % risk              if record_risk     else ''  
            mxlv    = ' MaxLv '  + '%.2f' % c.pvr['max_lvrg'] if record_max_lvrg else ''  
            csh_lw  = ' CshLw '  + '%.0f' % c.pvr['cash_low'] if record_cash_low else ''  
            rsk_hi  = ' RskHi '  + '%.0f' % c.pvr['risk_hi']  if record_risk_hi  else ''  
            log.info('{}{}{}{}{}{}{}{}{}{}{}{}'.format(_minute(),  
               lv, mxlv, qret, pvr, pnl, csh, csh_lw, shrt, ovrshrt, risk, rsk_hi))  
    if c.pvr['date_prv'] != date: c.pvr['days'] += 1.0  
    if c.pvr['days'] % 130 == 0 and _minute() == '100': _pvr_()  
    c.pvr['date_prv'] = date  
    if c.pvr['date_end'] == date:  
        # Summary on last minute of last day.  
        # If using schedule_function(), backtest last day/time may need to match for this to execute.  
        if 'pvr_summary_done' not in c: c.pvr_summary_done = 0  
        log_summary = 0  
        if c.pvr['mode'] == 'daily' and get_datetime().date() == c.pvr['date_end']:  
            log_summary = 1  
        elif c.pvr['mode'] == 'minute' and get_datetime() == get_environment('end'):  
            log_summary = 1  
        if log_summary and not c.pvr_summary_done:  
            _pvr_()  
            c.pvr_summary_done = 1

There was a runtime error.

I simplified the trading logic a bit and the results are virtually unchanged.

An expert in pairs trading should have a look at this ...

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

import numpy as np


def initialize(context):
        
    schedule_function(rebalance, 
                      date_rules.every_day(),
                      time_rules.market_open(minutes = 30))  

    context.vti = sid(22739)
    context.spy = sid(8554)
    
    context.secs = [context.vti, context.spy]
 
    context.z_trigger = 0
    
def rebalance(context,data):

    P = data.history(context.secs, 'price', 14, '1d')      
                        
    ratio = P[context.vti] / P[context.spy]
    xratio = ratio.median()
    sratio = ratio.std()
    dratio = ratio.iloc[-2] - xratio
    
    zscore = dratio / sratio
    
    if zscore > 2:
        order_target_percent(context.vti, -1.0)
        order_target_percent(context.spy, 1.0)
        context.z_trigger = zscore
    if zscore < -2:    
        order_target_percent(context.vti, 1.0)
        order_target_percent(context.spy, -1.0)
        context.z_trigger = zscore
    if zscore * context.z_trigger < 0: 
        order_target(context.vti, 0)
        order_target(context.spy, 0)
    


def handle_data(context,data):    

    record(leverage = context.account.leverage,
           exposure = context.account.net_leverage)


There was a runtime error.

Tim, sorry I have been travelling for business. I just checked the backtest vs my records and the buy/sell dates are a little off. I think it should be at the open after the day of the cross (ratio above or below -2/2). Same for the open after the cross above/below 0 of the ratio.

Also, it was shorting VTI and going long SPY - when it should've been the other way around.

Again, I am terrible with Quant coding and appreciate all your help as well as anyone else that is willing to convert this to quant code.

Alright, trying to revive this.

Is there a way to have the code check based on minutes instead of "30 minutes after open"?

I have found that making the settings more restrictive (2.5 SD as the entry), The algorithm needs to track every 1 MIN bar and enters/exits using the DAILY thresholds of ma(14) of the pair, 2.5 SD away from mean for entry, cross back over/under mean for exit. The nice thing about using the 14 day MA is that the thresholds are set based on the prior days close, and do not move throughout the day.

So we need the 1 minute data on the ratio difference and marry it up with the MA(14) of the ratio based on the prior close.

I am going to try and talk through the code again:

  1. We need to figure out the ratio of VTI/SPY
    Ratio VTI divided by SPY

  2. We need to determine the 14 day moving average of the ratio (from above)
    Ratio - Need the history of VTI divided by SPY for last 14 days

  3. Now we need to create our 2.5% standard deviation (upper and lower bands)
    We need to take the 14 day moving average of the ratio (from above) and multiply by 2.5
    Then take from the calculation right above and add/subtract to our 14 day moving average of the ratio get our upper/lower bands

Now this calculation needs to happen based on the close of the day prior and not update!
So we now have our upper and lower band based on 2.5 standard deviation from the 14 day ratio moving average.

We now need to calculate the ratio - based on each minute - and when it goes above or below the 2.5 standard deviation of the 14 day ratio moving average we need to make our long/short position

IF the ratio goes above the upper standard deviation we need to short VTI and go long SPY
IF the ratio goes below the lower standard deviation we need to go long VTI and go short SPY

Also, shares based on the old code is split between each position (I.E. 20,000 would be 10,000 into each position - short and long) - we need to ensure the starting amount is divided evenly between the long/short position.

The signal to exit would be when the ratio retreats back below or above the 2.5 standard deviation of the 14 day ratio moving average.

Does this simplify it a little more?

Is this something that is even possible in Quantopian. To have a 14 day moving average of the ratio +/- 2.5 standard deviations from that 14 day moving average and take the current minute data and compare for buy/sell signals?

Obviously this could introduce multiple buy/sells in the same day.

Yes, that is totally possible.

Anybody got anything? To summarize we need to take the vti/spy ratio 14 day moving average - take that and multiply by 2.5 then add/subtract to get your upper/lower bands. This needs to be based on the close of the prior day. Then we need the ratio vti/spy in minute mode for the current day to find when it goes above or below this ratio band and place orders detailed above. Then the exit is a retreat back below or above the ratio band.

This is a constant winner from 2011 to current and based on my manual trades made 2k in June alone! Let's get this coded to run in robinhood!