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

106
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?

2
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.

106
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.

5
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,
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(
to_delete.append(o.id)
elif o.id not in context.orders:  # New
context.orders[o.id] = 1
if o.limit:        # Limit order
log.info('   {} {} {} now {} limit {}'.format(
elif o.stop:       # Stop order
log.info('   {} {} {} now {} stop {}'.format(
else:              # Market order
log.info(' {}   {} {} {} at {}   cash {}'.format(minute,
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... ;)