Back to Community
RISK ON /RISK OFF Algo

First off, I would like to thank Delaney Granizo-Mackenzie for his help coding this Algo. I have great trading ideas, but still leaning python.

This algo attempts to be invested in the right asset class on a monthly basis. to accomplish this, we first calculate the ratio of the two assets, and calculate the MA of that ratio. Buy Equity if Ratio > MA ( risk on) , Buy Bonds if Ratio < MA ( Risk off). In this backtest , I use EFA ( no reason) and TLT. Return and Draw down are quite acceptable for such a simple algo.

I'm working on a better version of this , and will share the results soon enough.

Cheers

Lionel

Clone Algorithm
106
Loading...
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 talib
import math


def initialize(context):

# Securities 

    context.stocks = [
                          sid(22972) , # Risk ON asset  - EFA
                          sid(23921) , # Risk OFF asset TLT - 20 yrs TSRY
                      ]
    
    set_commission(commission.PerShare(cost=0.05))
    
    schedule_function(handle_entry,date_rules.month_start(),
        time_rules.market_open(minutes=15))


     

    
def handle_data(context, data):
           
       pass
        
    
def handle_entry (context,data):
    
    prices = history(bar_count=200, frequency='1d', field='price')
    M      = prices.apply(talib.SMA,timeperiod = 88).iloc[-1]
    ratio =  data[sid(22972)].price / data[sid(23921)].price 
    ratioMA= (M[sid(22972)] / M[sid(23921)]).mean()
        

             
    #RISK ON
    if ratio > ratioMA:
        order_target_percent(sid(22972), 1) 
        order_target_percent(sid(23921), 0)
    #RISK OFF    
    elif ratio < ratioMA: 
        order_target_percent(sid(22972), 0)         
        order_target_percent(sid(23921), 1)
            
            
            
            
            
                 
There was a runtime error.
11 responses

I just realized that I used "SPY" as a benchmark. Should of used "EFA , for a better comparison.

I like the concept, I only wonder if a bond portfolio is a good hedge vs the market going forward. Look at the '08 crash, we had room to reduce interest rates while the market was tanking, this provided returns in the bond portfolio. However in my view, it is likely during the next large drawdown in the market there will be little room to drop rates - and if the fed is still trying to raise rates at that time then your bond value is only declining. I like the approach, but I wonder if there is a better asset class than bonds to hedge the market going forward?
.. Just my opinion, I'm sure many will think the complete opposite

Happy to help! Glad you got this one working.

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.

I wonder why the algoriithm backtest in daily mode will have the big difference performance?
the algorithm handle_entry through schedule_function, and through history() to calculate MA, it shouldn't have the big difference performance?
could help clarify what caused this issue?

Clone Algorithm
2
Loading...
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 talib
import math


def initialize(context):

# Securities 

    context.stocks = [
                          sid(22972) , # Risk ON asset  - EFA
                          sid(23921) , # Risk OFF asset TLT - 20 yrs TSRY
                      ]
    
    set_commission(commission.PerShare(cost=0.05))
    
    schedule_function(handle_entry,date_rules.month_start(),
        time_rules.market_open(minutes=15))


     

    
def handle_data(context, data):
           
       pass
        
    
def handle_entry (context,data):
    
    prices = history(bar_count=200, frequency='1d', field='price')
    M      = prices.apply(talib.SMA,timeperiod = 88).iloc[-1]
    ratio =  data[sid(22972)].price / data[sid(23921)].price 
    ratioMA= (M[sid(22972)] / M[sid(23921)]).mean()
        

             
    #RISK ON
    if ratio > ratioMA:
        order_target_percent(sid(22972), 1) 
        order_target_percent(sid(23921), 0)
    #RISK OFF    
    elif ratio < ratioMA: 
        order_target_percent(sid(22972), 0)         
        order_target_percent(sid(23921), 1)
            
            
            
            
            
                 
There was a runtime error.

Hey Novice, the issue here is that in daily mode, you only see prices from the market close, and orders are executed on the next market close. The delay in order execution can cause problems for some algorithms, I would just run in minute mode and use schedule_function to have the most accurate backtests. Our own Alisa explains it better than I in this thread.

@Leonel.. is this leverage?

Nope , no leverage. i added custom data to show you cash and leverage levels.

Clone Algorithm
106
Loading...
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 talib
import math


def initialize(context):

# Securities 

    context.stocks = [
                          sid(22972) , # Risk ON asset  - EFA
                          sid(23921) , # Risk OFF asset TLT - 20 yrs TSRY
                      ]
    
    set_commission(commission.PerShare(cost=0.05))
    
    schedule_function(handle_entry,date_rules.month_end(),
        time_rules.market_open(minutes=15))


    context.leverage = 1         

    
def handle_data(context, data):
           
       pass
        
    
def handle_entry (context,data):
    
    prices = history(bar_count=200, frequency='1d', field='price')
    M      = prices.apply(talib.SMA,timeperiod = 88).iloc[-1]
    ratio =  data[sid(12915)].price / data[sid(23921)].price 
    ratioMA= (M[sid(12915)] / M[sid(23921)]).mean()
   

              
    record(current_cash = context.portfolio.cash)
    record(current_lvg = context.account.leverage)


             
    #RISK ON
    if ratio > ratioMA:
        order_target_percent(sid(12915), 1) 
        order_target_percent(sid(23921), 0)
    #RISK OFF    
    elif ratio < ratioMA: 
        order_target_percent(sid(12915), 0)         
        order_target_percent(sid(23921), 1)
            
            
            
            
            
                 
There was a runtime error.

Yup, leverage of 1.99 actually.

Lionel, I don't want you to feel bad about this, we have ALL been cruising along thinking context.account.leverage was gold, not true in the custom chart because it only records the last value (of 390 possible) of the day when in minute mode. You are helping reveal this for the first time. It's going to help a lot of people down the line. It's a great example because it is very clear. You provided a great service in this algo (and it might be repairable now).

Problem

That result would not be achieved without requiring ~2x the starting capital.
In the first 1 single solitary minute that TLT went unfilled, MDY took cash to negative -$9,888.
Then back again 4 minutes later when TLT sold.
Margin account required, with borrowing costs.

It is an illustration of intraday over-leverage unseen by context.account.leverage

The value of context.account.leverage is relied upon and trusted in many algorithms on Quantopian currently and missing leverage intraday spikes in record(). (Would be seen in logging).

1970-01-01 initialize:32 INFO 2005-01-03 to 2005-03-30  10000  minute  
2005-01-31 track_orders:113 INFO  15   Buy 109 TLT at 91.38       cash 10000  
2005-01-31 track_orders:85  INFO  16      Bot 109 TLT at 91.41    cash 30  
2005-02-28 track_orders:113 INFO  15   Sell -109 TLT at 91.05     cash 67  
2005-02-28 track_orders:113 INFO  15   Buy 81 MDY at 122.95       cash 67  
2005-02-28 track_orders:89  INFO  16         TLT -109 unfilled        <==== Due to this  
2005-02-28 track_orders:85  INFO  16      Bot 81 MDY at 122.87    cash -9888  <==== Negative  
2005-02-28 track_orders:89  INFO  17         TLT -109 unfilled  
2005-02-28 track_orders:89  INFO  18         TLT -109 unfilled  
2005-02-28 track_orders:89  INFO  19         TLT -109 unfilled  
2005-02-28 track_orders:85  INFO  20      Sold -109 TLT at 90.97  cash 20  
End of logs.  

It would produce very different results at Interactive Brokers vs. the backtest (except for margin accounts).

Solution

A request that Quantopian provide context.account.leverage_max to make those spikes visible more easily may need to come from you, the members.

This can be dropped into any algo for a quick sanity check in the interim:

def handle_data(context, data):  
    if 'mx_lvrg' not in context:  
        context.mx_lvrg = 0                  # init this instead in initialize() for better efficiency  
    if context.account.leverage > context.mx_lvrg:  
        context.mx_lvrg = context.account.leverage  
        record(mx_lvrg = context.mx_lvrg)    # Record maximum leverage encountered  

This would be easier:

    record(max_lvg = context.account.leverage_max)  

Yes, I too was being fooled by context.account.leverage although I felt like something was off, it wasn't clear until working thru this code and adding track_orders() along with the modification to add each minute and portfolio cash.

There's quite a bit of code added here as I was trying to wrap my head around what was happening, don't let it intimidate you, if anyone would like this to be commented more, I'd be happy to.

Please mouse-over the custom chart and look at the values.

Clone Algorithm
5
Loading...
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
'''
Original: https://www.quantopian.com/posts/risk-on-slash-risk-off-algo
Modifications: garyha https://www.quantopian.com/users/52b0e0e0271a27fc88000049

An illustration of intraday over-leverage unseen by context.account.leverage

1970-01-01 initialize:32 INFO 2005-01-03 to 2005-03-30  10000  minute
2005-01-31 track_orders:113 INFO  15   Buy 109 TLT at 91.38       cash 10000
2005-01-31 track_orders:85  INFO  16      Bot 109 TLT at 91.41    cash 30
2005-02-28 track_orders:113 INFO  15   Sell -109 TLT at 91.05     cash 67
2005-02-28 track_orders:113 INFO  15   Buy 81 MDY at 122.95       cash 67
2005-02-28 track_orders:89  INFO  16         TLT -109 unfilled        <==== Due to this
2005-02-28 track_orders:85  INFO  16      Bot 81 MDY at 122.87    cash -9888  <==== Negative
2005-02-28 track_orders:89  INFO  17         TLT -109 unfilled
2005-02-28 track_orders:89  INFO  18         TLT -109 unfilled
2005-02-28 track_orders:89  INFO  19         TLT -109 unfilled
2005-02-28 track_orders:85  INFO  20      Sold -109 TLT at 90.97  cash 20
End of logs.

'''

import talib
import math
from pytz import timezone    # for track_orders()

def initialize(context):
    # Securities
    context.stocks = [
                          sid(22972) , # Risk ON asset  - EFA
                          sid(23921) , # Risk OFF asset TLT - 20 yrs TSRY
                      ]
    set_commission(commission.PerShare(cost=0.05))
    schedule_function(handle_entry, date_rules.month_end(), time_rules.market_open(minutes=15))
    context.leverage = 1
    context.mx_lvrg  = 0
    context.orders   = {}    # for track_orders()

    # for pvr()
    c = context
    c.max_lvrg = 0
    c.risk_hi  = 0
    c.date_prv = ''
    c.cash_low = c.portfolio.starting_cash
    c.date_end = str(get_environment('end').date())
    log.info('{} to {}  {}  {}'.format(str(get_datetime().date()), c.date_end,
        int(c.cash_low), get_environment('data_frequency')))

def handle_entry (context,data):
    prices  = history(bar_count=200, frequency='1d', field='price')
    M       = prices.apply(talib.SMA,timeperiod = 88).iloc[-1]
    ratio   =  data[sid(12915)].price / data[sid(23921)].price
    ratioMA = (M[sid(12915)] / M[sid(23921)]).mean()

    #RISK ON
    if ratio > ratioMA:
        order_target_percent(sid(12915), 1)
        order_target_percent(sid(23921), 0)
    #RISK OFF
    elif ratio < ratioMA:
        order_target_percent(sid(12915), 0)
        order_target_percent(sid(23921), 1)

    track_orders(context, data)

def handle_data(context, data):
    track_orders(context, data)

    #pvr(context, data) ; return    # try uncommenting this

    record(current_cash = context.portfolio.cash)
    record(current_lvg = context.account.leverage)

    if context.account.leverage > context.mx_lvrg:
        context.mx_lvrg = context.account.leverage
        record(max_lvrg = context.mx_lvrg)
    if context.portfolio.cash < context.cash_low:
        context.cash_low = context.portfolio.cash
        record(cash_low = context.cash_low)

    return

def track_orders(context, data):  # Log orders created or filled.
    # https://www.quantopian.com/posts/track-orders
    #   modified here to log minute and cash

    #if 'orders' not in context:
    #    context.orders = {}

    to_delete = []
    for id in context.orders:
        o   = get_order(id)
        sec = o.sid
        sym = sec.symbol
        bar_dt = get_datetime().astimezone(timezone('US/Eastern'))
        minute = (bar_dt.hour * 60) + bar_dt.minute - 570  # (-570 = 9:30a)
        if o.filled:        # Filled at least some, status 1 is Filled
            trade = 'Bot' if o.amount > 0 else 'Sold'
            log.info(' {}      {} {} {} at {}   cash {}'.format(minute,
                    trade, o.filled, sym, data[sec].price, int(context.portfolio.cash)))
            to_delete.append(o.id)
        else:
            log.info(' {}         {} {} unfilled'.format(
                    minute, o.sid.symbol, o.amount))

    for sec, oo_for_sid in get_open_orders().iteritems(): # Open orders
        sym = sec.symbol
        for o in oo_for_sid: # Orders per security
            bar_dt = get_datetime().astimezone(timezone('US/Eastern'))
            minute = (bar_dt.hour * 60) + bar_dt.minute - 570  # (-570 = 9:30a)
            if o.id in to_delete:
                continue
            if o.status == 2:                 # Cancelled
                log.info('    Cancelled {} {} order'.format(
                        trade, o.amount, sym, data[sec].price))
                to_delete.append(o.id)
            elif o.id not in context.orders:  # New
                context.orders[o.id] = 1
                trade = 'Buy' if o.amount > 0 else 'Sell'
                if o.limit:        # Limit order
                    log.info('   {} {} {} now {} limit {}'.format(
                            trade, o.amount, sym, data[sec].price, o.limit))
                elif o.stop:       # Stop order
                    log.info('   {} {} {} now {} stop {}'.format(
                            trade, o.amount, sym, data[sec].price, o.stop))
                else:              # Market order
                    log.info(' {}   {} {} {} at {}   cash {}'.format(minute,
                        trade, o.amount, sym, data[sec].price, int(context.portfolio.cash)))
    for d in to_delete:
        del context.orders[d]

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_risk     = 0          # Risked, maximum cash spent or shorts in excess of cash at any time
    record_risk_hi  = 1          # Highest risk overall
    record_cash     = 0          # Cash available
    record_cash_low = 1          # Any new lowest cash level
    logging         = 1          # Also log to the logging window conditionally (1) or not (0)
    log_method      = 'risk_hi'  # 'daily' or 'risk_hi'

    c = context                          # For brevity
    new_cash_low = 0                     # To trigger logging in cash_low case
    date = str(get_datetime().date())    # To trigger logging in daily case
    cash = c.portfolio.cash

    if int(cash) < c.cash_low:    # New cash low
        new_cash_low = 1
        c.cash_low   = int(cash)
        if record_cash_low:
            record(CashLow = int(c.cash_low))

    pvr_rtrn      = 0        # Profit vs Risk returns based on maximum spent
    profit_loss   = 0        # Profit-n-loss
    shorts        = 0        # Shorts value
    start         = c.portfolio.starting_cash
    cash_dip      = int(max(0, start - cash))

    if record_cash:
        record(Cash = int(c.portfolio.cash))  # Cash

    if record_leverage:
        record(Lvrg = c.account.leverage)     # Leverage

    if record_max_lvrg:
        if c.account.leverage > c.max_lvrg:
            c.max_lvrg = c.account.leverage
            record(MaxLv = c.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[p].price))

    if record_shorting:
        record(Shorts = shorts)               # Shorts value as a positve

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

    new_risk_hi = 0
    if risk > c.risk_hi:
        c.risk_hi = risk
        new_risk_hi = 1

        if record_risk_hi:
            record(RiskHi = c.risk_hi)        # Highest risk overall

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

    q_rtrn = 100 * (c.portfolio.portfolio_value - start) / start
    if record_q_return:
        record(QRet = q_rtrn)                 # Quantopian returns to compare to pvr returns curve

    # from pytz import timezone  # already imported in this algo
    if logging:
        if log_method == 'risk_hi' and new_risk_hi \
          or log_method == 'daily' and c.date_prv != date \
          or c.date_end == date \
          or new_cash_low:
            qret   = 'QRet '    + '%.1f' % q_rtrn
            mxlv   = 'MaxLv '   + '%.1f' % c.max_lvrg   if record_max_lvrg 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 ''
            csh_lw = 'CshLw '   + '%.0f' % c.cash_low   if record_cash_low else ''
            shrt   = 'Shrt '    + '%.0f' % shorts       if record_shorting else ''
            risk   = 'Risk '    + '%.0f' % risk         if record_risk     else ''
            rsk_hi = 'RskHi '   + '%.0f' % c.risk_hi    if record_risk_hi  else ''
            minute = get_datetime().astimezone(timezone('US/Eastern')).time().minute
            log.info('{} {} {} {} {} {} {} {} {} {}'.format(
                    minute, mxlv, qret, pvr, pnl, csh, csh_lw, shrt, risk, rsk_hi))

    if c.date_end == date:    # Log on last day, like cash 125199  portfolio 126890
        log.info('cash {}  portfolio {}'.format(
                int(cash), int(c.portfolio.portfolio_value)))

    c.date_prv = date

There was a runtime error.

I too Garyha.. figure... something was off... although Im not a familiar... with python code... and how algo works.. but it doesn't seem.... to add up...
That's why... Im asking if this was leverage... since his algo perform.. well... in short amount of time... I suspect its leverage.... but could not confirm...
since there's no leverage written in his algo... by the way... I dont know how... to diagnose... or print the inner workings of algo like Garyha... whether... the algo perform... correctly as shown.. in the illustration.. as evidence... thanks Garyha... for addressing my concern... about the leverage issue... and not completely... ignoring it as an anomaly....

@garyha

Thank you for exposing this. It's a huge eye opener for me and might delay the live deployment of another Algo I have. I',m sending you a private message to see if you could take a second look at my code.

thanks again,

Lionel

@ lionel.. pls post your corrected algo without the leverage on... thanks... ;)