Classic RSI2 Mean-Reversion Strategy (SPY/TLT)

This is a simple mean-reversion strategy that uses the RSI2 technical indicator which was originally developed and popularized by Cesar Alvarez more than a decade ago. Most people are familiar with the RSI14 but the RSI2 is a much more powerful tool for short-term market timing that works well in both momentum and mean-reversion strtegies. The algorithm monitors the 2-day RSI of SPY (S&P500) and TLT (Long-Term US Treasury), both of which are well-known as mean-reverting assets. When an asset's RSI2 dips below 30 ("Oversold"), go long with 50% of the portfolio. When its RSI2 crosses above 70 ("Overbought"), sell the position and go to cash. The vanilla version of this strategy generates good risk-adjusted performance but still underperforms the S&P 500, for the most part. However, using leverage can increase the risk/reward profile in order to outperform the S&P 500 while still maintaining lower risk. I'm using 2x leverge for this strategy.

327
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
#Published By: Quant Prophet, LLC
#Author: Kory Hoang
#Developer: Jacob Lower
#Description: this is a simple mean-reversion strategy that uses the RSI2 technical indicator which was originally developed and popularized by Cesar Alvarez. Most people are familiar with the RSI14 but the RSI2 is a much more powerful tool for short-term market timing that works well in both momentum and mean-reversion strtegies. The algorithm monitors the 2-day RSI of SPY (S&P500) and TLT (Long-Term US Treasury), both of which are well-known as mean-reverting assets. When an asset's RSI2 dips below 30 ("Oversold"), go long with 50% of the portfolio. When its RSI2 crosses above 70 ("Overbought"), sell the position and go to cash. The vanilla version of this strategy generates good risk-adjusted performance but still underperforms the S&P 500, for the most part. However, using leverage can increase the risk/reward profile in order to outperform the S&P 500 while still maintaining lower risk. I'm using 2x leverge for this strategy.

import talib

def initialize(context):

schedule_function(rebalance, date_rules.every_day(), time_rules.market_open())
set_benchmark(symbol('SPY'))

context.stock = symbol('SPY') #Equity asset
context.bond = symbol('TLT') #Bond asset
context.rsi_period = 2
context.OB = 70 #Overbought threshold
context.OS = 30 #Oversold threshold
context.pct_alloc1 = 0.50
context.pct_alloc2 = 0.50
context.leverage = 2.0

stock_price = data.history(context.stock, 'price', 3, '1d')
bond_price = data.history(context.bond, 'price', 3, '1d')

rsi1 = talib.RSI(stock_price, context.rsi_period)
rsi2 = talib.RSI(bond_price, context.rsi_period)

if rsi1[-1] < context.OS and data.can_trade(context.stock):
#order_target_percent(stock, context.pct_alloc1 * leverage)
elif rsi1[-1] > context.OB and data.can_trade(context.stock):
#order_target_percent(stock, 0.00 * leverage)

if rsi2[-1] < context.OS and data.can_trade(context.bond):
#order_target_percent(bond, context.pct_alloc2 * leverage)
elif rsi2[-1] > context.OB and data.can_trade(context.bond):
#order_target_percent(bond, 0.00 * leverage)

record(leverage = context.account.leverage)

def rebalance(context, data):

order_target_percent(context.stock, context.pct_alloc1 * context.leverage)
order_target_percent(context.stock, 0.00 * context.leverage)

order_target_percent(context.bond, context.pct_alloc2 * context.leverage)
order_target_percent(context.bond, 0.00 * context.leverage)
context.sellBondAlert = False
There was a runtime error.
27 responses

Optimized version. Moved execution time to 15 minutes after open and changed Overbought/Oversold thresholds to 80/40.

285
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
#Published By: Quant Prophet, LLC
#Author: Kory Hoang
#Developer: Jacob Lower
#Description: this is a simple mean-reversion strategy that uses the RSI2 technical indicator which was originally developed and popularized by Cesar Alvarez. Most people are familiar with the RSI14 but the RSI2 is a much more powerful tool for short-term market timing that works well in both momentum and mean-reversion strtegies. The algorithm monitors the 2-day RSI of SPY (S&P500) and TLT (Long-Term US Treasury), both of which are well-known as mean-reverting assets. When an asset's RSI2 dips below 40 ("Oversold"), go long with 50% of the portfolio. When its RSI2 crosses above 80 ("Overbought"), sell the position and go to cash. The vanilla version of this strategy generates good risk-adjusted performance but still underperforms the S&P 500, for the most part. However, using leverage can increase the risk/reward profile in order to outperform the S&P 500 while still maintaining lower risk. I'm using 2x leverge for this strategy.

import talib

def initialize(context):

schedule_function(rebalance, date_rules.every_day(), time_rules.market_open(minutes=15))
set_benchmark(symbol('SPY'))

context.stock = symbol('SPY') #Equity asset
context.bond = symbol('TLT') #Bond asset
context.rsi_period = 2
context.OB = 80 #Overbought threshold
context.OS = 40 #Oversold threshold
context.pct_alloc1 = 0.50
context.pct_alloc2 = 0.50
context.leverage = 2.0

stock_price = data.history(context.stock, 'price', 3, '1d')
bond_price = data.history(context.bond, 'price', 3, '1d')

rsi1 = talib.RSI(stock_price, context.rsi_period)
rsi2 = talib.RSI(bond_price, context.rsi_period)

if rsi1[-1] < context.OS and data.can_trade(context.stock):
#order_target_percent(stock, context.pct_alloc1 * leverage)
elif rsi1[-1] > context.OB and data.can_trade(context.stock):
#order_target_percent(stock, 0.00 * leverage)

if rsi2[-1] < context.OS and data.can_trade(context.bond):
#order_target_percent(bond, context.pct_alloc2 * leverage)
elif rsi2[-1] > context.OB and data.can_trade(context.bond):
#order_target_percent(bond, 0.00 * leverage)

record(leverage = context.account.leverage)

def rebalance(context, data):

order_target_percent(context.stock, context.pct_alloc1 * context.leverage)
order_target_percent(context.stock, 0.00 * context.leverage)

order_target_percent(context.bond, context.pct_alloc2 * context.leverage)
order_target_percent(context.bond, 0.00 * context.leverage)
context.sellBondAlert = False
There was a runtime error.

"RSI2 technical indicator which was originally developed and popularized by Cesar Alvarez more than a decade ago."

It's funny how some people get credit from the work of others. The RSI was developed by J. Welles Wilder. Setting the period to 2 and calling this a new development sounds extremely weird. it's like setting the moving average period to 2 and claiming it your invention. It is so disconcerting that people in this industry do not have respect for intellectual property. At least Quantopian does a good job to offer credit to original work.

Now, a system with 26% max drawdown , leverage and NO stops is the ultimate NO NO. You have more chances going to a casino and betting all your money on a single roulette number. This system is NOT market neutral, repeat, not market neutral. If long SPY and there is flash crash but not long TLT, you can end up owing the broker multiples of your net worth.

I just wanted to share by opinions with you.

Now, a system with 26% max drawdown , leverage and NO stops is the ultimate NO NO. You have more chances going to a casino and betting all your money on a single roulette number. This system is NOT market neutral, repeat, not market neutral. If long SPY and there is flash crash but not long TLT, you can end up owing the broker multiples of your net worth.

This seems to be a pretty systemic issue on these forums. I think this algo is worth looking at and contains valuable information but like most algorithms here needs to enforce strict trading controls. I usually spend 95% of my time on an algo working on execution and risk control, 4% bug fixing, and 1% tuning parameters. Sometimes I will find an algo that I like here, spend a few days making it trade realistically only to find out that by doing so you remove all alpha and it's worthless. Getting unachievable returns doesn't help anyone. but I will say that this one is better than many

Ricardo,

Thank you for your comments on my system. There are a couple things I'd like to point out:

It's funny how some people get credit from the work of others. The RSI was developed by J. Welles Wilder. Setting the period to 2 and calling this a new development sounds extremely weird. it's like setting the moving average period to 2 and claiming it your invention. It is so disconcerting that people in this industry do not have respect for intellectual property. At least Quantopian does a good job to offer credit to original work.

I did not say that Cesar Alvarez developed the RSI. Everyone and their grandma in the quant community knows the RSI has been around for decades and was originally created by the legendary J. Welles Wilder. It was the particular technique of using a 2-period setting for the RSI that was popularized during the early 2000s by Cesar Alvarez, another quant legend who is also a good friend that lives near me. Notice that he did not rename it to the Alvarez RSI because he didn't change anything fundamental to the RSI's formula. Now on a different note, Cesar did help create another variation of the RSI called the "Connors RSI" while he was working under Larry Connors (yet another quant legend), but I digress.

Now, a system with 26% max drawdown , leverage and NO stops is the ultimate NO NO. You have more chances going to a casino and betting all your money on a single roulette number. This system is NOT market neutral, repeat, not market neutral. If long SPY and there is flash crash but not long TLT, you can end up owing the broker multiples of your net worth.

I did not claim this system was market neutral, nor did I recommend its use to anyone. I simply presented an interesting mean-reversion strategy that is extremely simple at its core to demonstrate the concept of using the RSI2 to generate trade signals. Obviously, if you wanted to trade this, you would need to add risk management techniques like hedging, using stop loss and/or scaling into positions, etc.

1) I implied leverage was optional, not required. I even stated that the non-leveraged version would not outperform SPY. If you wanted to undertake the risk of using leverage to increase return then that's your call but that option is there. I wanted to show that even the 2x leveraged version has hypothetically outperformed SPY in the last 15 years while maintaining a similar level of volatility and even a smaller Max DD during 2008.

2) This system's base allocation to SPY is only 50%, so even at 2x leverage, your exposure to SPY is the same as if you were going to buy and hold the ETF with 100% of your account. If a Black Swan happens then the SPY portion is only going to suffer just as much as the ETF itself. Not to mention that there could also be an open TLT position at the same time that could potentially hedge the Black Swan (unless all correlation goes to 1 which means pretty much everyone is screwed anyway). I won't deny this simple system's vulnerability to a Black Swan event because if both SPY and TLT tank at the same time then yes, you could get wiped out, but so would many others in that kind of an event. That is why you diversify little bits of your money across many different systems/strategies to spread out your risk.

3) Unlike an SPY/TLT buy-and-hold strategy, this system is not in the market 100% of the times. In fact, if i recall correctly, it is exposed to either asset only about 50% of the time (gotta double check my numbers). So just statistically speaking, it automatically has half the odds of getting struck by a Black Swan.

If you have any ideas or improvements to add to this strategy to make it less risky then please feel free and contribute.

~Kory

Here's a strictly non-leveraged version with reasonable returns.

132
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 pandas as pd
import numpy as np
import talib

def initialize(context):

schedule_function(rebalance, date_rules.every_day(), time_rules.market_open(minutes=20))

schedule_function(display, date_rules.every_day(), time_rules.market_close())

context.stock = symbol('SPY') #Equity asset
context.bond = symbol('TLT') #Bond asset
context.rsi_period = 2
context.OB = 80 #Overbought threshold
context.OS = 40 #Oversold threshold

stock_price = data.history(context.stock, 'price', 3, '1d')
bond_price = data.history(context.bond, 'price', 3, '1d')

rsi1 = talib.RSI(stock_price, context.rsi_period)
rsi2 = talib.RSI(bond_price, context.rsi_period)

if rsi1[-1] < context.OS:
elif rsi1[-1] > context.OB:

if rsi2[-1] < context.OS:
elif rsi2[-1] > context.OB:

def rebalance(context, data):

if get_open_orders():
return

w = pd.Series([0, 0], index = [context.stock, context.bond])

w[context.stock] = 1.0

w[context.bond] = 1.0

q = np.nan_to_num(w)
w = pd.Series(q, index = w.index)
u = w.abs().sum()
if u > 0: w  = w / u

for s in w.index:
continue
order_target_percent(s, w[s])

def display(context, data):

record(leverage = context.account.leverage)


There was a runtime error.

Tim, when I run your algo with the PvR routine there is still leverage as buys happen before sells. When I update the code to ensure it looks for available cash before making purchases the returns are severely reduced.

@Kory thanks for this was looking into the 2 RSI myself when i found a post of it on trading view. I really don't understand some people on these forums, 25% DD is TOO MUCH, then invest in bonds, it blows my mind how a system that out performs the SPY, has half the DD of 08 and people complain, like what??? What system, or where on earth would you have been invested in 08 to get any sort of decent return and avoid 50% DD? Literally survived 08 beautifully made a profit in 08 lol, not to mention it has survived every single black swan to date in a superb manner.

Also, people talk about black swans, listen if a black swan event blows out your account guess what you are going to have much more important things to worry about like food, water, shelter, bullets, and not your worthless paper account which will probably be worth nothing through inflation, war, whatever.

Lastly, you can get leverage through many different methods don't require you owing money to your broker, options, futures, 2x-3x etfs.

Thanks, Tyler, very interesting. Would you be willing to post your version of the algorithm?

@Elsid Aliaj: I mostly agree with you, although a non-world-ending Black Swan like 1987 could cause major damage as well. The key word here is diversification across multiple strategies. Also, here's a tear sheet for the strategy:

10

The common practice of recording context.account.leverage misses 389 of the 390 minutes of the trading day.
You can use https://www.quantopian.com/posts/max-intraday-leverage to avoid that mistake.

Yesterday's algo is worthwhile however it was said:

Here's a strictly non-leveraged version with reasonable returns.

It was actually leverage of 2 almost right away into the run, the 2.0 leverage intraday made real returns half of what is shown.
2017-01-17 06:31 pvr:122 INFO PvRp 0.0985 %/day 2003-01-02 to 2017-01-17 $20000 2017-01-22 02:10 US/Eastern 2017-01-17 06:31 pvr:123 INFO Profited 139226 on 39999 activated/transacted for PvR of 348.1% 2017-01-17 06:31 pvr:124 INFO QRet 696.13 PvR 348.07 CshLw -129115 MxLv 2.00 RskHi 39999 Shrts 0 @KORY This was the strategy i found on tradingview, don't know how much of the ideas spoken about there you have applied but might be worthwhile to mess around with some of them. https://www.tradingview.com/chart/CELG/EVXQPaR9-Larry-Connors-RSI-2-Trading-System-Surprising-Win-Rate/ The best goal would be to get the same returns and Drawdowns without TLT, the issue is what happens when we get a bear market in bonds, and correlations as safe heavens break down. Nov 2017 update: 181 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 #Published By: Quant Prophet, LLC #Author: Kory Hoang #Description: this is a simple mean-reversion strategy that uses the RSI2 technical indicator which was originally developed and popularized by Cesar Alvarez. Most people are familiar with the RSI14 but the RSI2 is a much more powerful tool for short-term market timing that works well in both momentum and mean-reversion strtegies. The algorithm monitors the 2-day RSI of SPY (S&P500) and TLT (Long-Term US Treasury), both of which are well-known as mean-reverting assets. When an asset's RSI2 dips below 30 ("Oversold"), go long with 50% of the portfolio. When its RSI2 crosses above 70 ("Overbought"), sell the position and go to cash. The vanilla version of this strategy generates good risk-adjusted performance but still underperforms the S&P 500, for the most part. However, using leverage can increase the risk/reward profile in order to outperform the S&P 500 while still maintaining lower risk. I'm using 2x leverge for this strategy. import talib def initialize(context): schedule_function(setAlerts, date_rules.every_day(), time_rules.market_close()) schedule_function(rebalance, date_rules.every_day(), time_rules.market_open()) set_commission(commission.PerShare(cost=0.005, min_trade_cost=1.00)) #IB commissions set_slippage(slippage.VolumeShareSlippage(volume_limit=0.025, price_impact=0.1)) set_benchmark(symbol('SPY')) context.stock = symbol('SPY') #Equity asset context.bond = symbol('TLT') #Bond asset context.rsi_period = 2 context.OB1 = 80 #Overbought threshold 1 context.OS1 = 40 #Oversold threshold 1 context.OB2 = 60 #Overbought threshold 2 context.OS2 = 30 #Oversold threshold 2 context.pct_alloc1 = 0.50 context.pct_alloc2 = 0.50 context.leverage = 2.00 #Alert to buy or sell next day. context.buyStockAlert = False context.sellStockAlert = False context.buyBondAlert = False context.sellBondAlert = False def setAlerts(context, data): stock_price = data.history(context.stock, 'price', 3, '1d') bond_price = data.history(context.bond, 'price', 3, '1d') rsi1 = talib.RSI(stock_price, context.rsi_period) rsi2 = talib.RSI(bond_price, context.rsi_period) if rsi1[-1] < context.OS1 and data.can_trade(context.stock): #order_target_percent(stock, context.pct_alloc1 * leverage) context.buyStockAlert = True elif rsi1[-1] > context.OB1 and data.can_trade(context.stock): #order_target_percent(stock, 0.00 * leverage) context.sellStockAlert = True if rsi2[-1] < context.OS2 and data.can_trade(context.bond): #order_target_percent(bond, context.pct_alloc2 * leverage) context.buyBondAlert = True elif rsi2[-1] > context.OB2 and data.can_trade(context.bond): #order_target_percent(bond, 0.00 * leverage) context.sellBondAlert = True record(leverage = context.account.leverage) def rebalance(context, data): if context.buyStockAlert and data.can_trade(context.stock) and context.portfolio.positions[context.stock].amount == 0: order_target_percent(context.stock, context.pct_alloc1 * context.leverage) context.buyStockAlert = False if context.sellStockAlert and data.can_trade(context.stock): order_target_percent(context.stock, 0.00 * context.leverage) context.sellStockAlert = False if context.buyBondAlert and data.can_trade(context.bond) and context.portfolio.positions[context.bond].amount == 0: order_target_percent(context.bond, context.pct_alloc2 * context.leverage) context.buyBondAlert = False if context.sellBondAlert and data.can_trade(context.bond): order_target_percent(context.bond, 0.00 * context.leverage) context.sellBondAlert = False There was a runtime error. @Kory it would be better could help migration the strategy into Zipline, or where are you living trading now ? Could you share your living trading experience . I enjoyed reading various aspects of the debate this thread has produced, but I will pick up on just one aspect and that is the concept of the black swan event or, in other words, "your worst draw-down is still ahead somewhere, just waiting for you", and then go on to consider what to do about it? Personally, with every system that I actually trade, I always like to run it through the whole period from 1980 until now. If it fails the "October '87 Crash" test, then it simply goes into the rubbish bin, irrespective of how good the rest of it looks. Some people say this is nonsense because, a) the markets are different now [yes, I agree], b) the Oct '87 crash will not repeat itself [yes I agree, and yes, "next time" will be different .... maybe even worse], and c) you must be a real old fossil to even care about pre-historic 1987 [yes, I am, and I was trading it actually, while the dinosaurs were running around shouting their heads off on the floor]. The point is, how can we get as wide as possible a range of real (not synthetic) data for building our systems / algos? Answer: use as much real historic data as possible, going back as far as possible, and taken from as many diverse markets as possible, even if they are not the same markets as we are really intending to trade. The fact that some market behavior actually happened in market X at some time in the past means that it is a possible market behavior that just might possibly be closely approximated by the behavior of our chosen market at some future time. The aim of using a lot of other market data is not necessarily because I want to build a "one system fits everything" model, but because I want to expose my systems / algos to as wide a range of possible market conditions as I can, to ensure they are robust. Comments / agreement / disagreement ? ..... all welcome. @Tony Bravo, It seems Quantopian is made up of ADD Crypto currency chasing gains crowd, if you have been around the early current crypto community you know what I'm talking about. It just gamblers mentalities throughout majority of this community, mostly focused on VIX from 2010+ which is utter insanity. This strategy is one of the ones I like most, but again I don't like that fact that it depends on Bonds for sooooooo much of it's gains. To truly test this I would run it from 86+ and then get proper bond data such as prices during % rate increases, given that even if rates go up bonds can still manage ok. Theres a reason RenTech spends probably half or almost all their resources on Data gathering/cleanup, majority here think they can do better. Elsid writes: "To truly test this I would run it from 86+ and then get proper bond data such as prices during % rate increases" This is good advice and highlights several issues that some people might have forgotten, or perhaps were never even aware of, specifically: • Take a look at the S&P500 for the last 5 years --- its all Bull market except for a couple of little blips. • Go back as far as March 2009 --- its still all Bull market except for a few little blips. • That means anyone who has been trading for < about 8 years has never experienced anything except a bull market, so presumably they consider current market behavior and think: a) "This is normal" and b) The last 8 years is what "typical market behavior" looks like, and c) 8 years is more than enough for back-testing.[ a) No, it isn't, b) No, it doesn't , c) No, it's not]. • OK, so then lets look back as far as 2007. Now at least we see a (brief) Bear market in the S&P500 (from Mid 2007 to March 2009) and so, if we can handle that, then we should be able to handle anything, right? Well, sorry, but I don't think so. • Take a look at the US 30year T-Bond ($TYX). Go back to 1988 and notice what you see. Sure it bumps up and down a bit, but the overall trend in bond yields is generally all down. We have been in a declining interest-rate environment for the last 30 YEARS!!! Declining interest rates means lower cost of borrowing and that drives business expansion, and this has been fueling the stockmarkets of the world for the past 30 years!! During that time, we do not have any experience of a rising interest rate environment that has lasted longer than about a year or so. Therefore, unless you believe that interest rates can actually continue on and on going down, then we are at the edge of "uncharted territory".

As a result, unless we have either been trading for > 30 years or have studied the behaviors of other markets, then we really cannot expect that everything we know (or think we know) about market behavior from the last 30 years will actually continue to hold in future, and certainly this problem is greatly exacerbated if all we know from our trading experience is just in the bull market of the last few years.

I think that Elsid writes wisely about the need to look back a considerable time, and especially about looking at rising (as well as falling) interest rate environments. We ignore his words about this at our peril.
Message: Whatever timeframes you are looking at now for development & testing, it would be wise to look at even longer ones.... preferably MUCH longer ones!

Hey Tony,

Yeah 87 is just a starting point would love to see data going back to the 70's and beyond with sky rocketing interest rates. Thing is though Data is the issue, I even posted on here about actual data issues from Quantopian's historical data actually being wrong for some days, and it was basically thrown under the bus well we have to sacrifice data to allow people to prototype their ideas faster whatever that means.

I'm not even a trading pro, but it seems this whole platform was set up very amateurishly if the goal was to research anything robust, given that a single data point failure yet alone multiple, could throw your whole hypothesis out of wack. I guess a good amount of people I know switched over to quantconnect for whatever multiple shortcomings Quantopian offered.

What we really needed was a almost perfect research environment, live trading, contests, allocations could of been developed outside of here if something showed true promise. Just glad someone else actually sees the value in Data creating something robust as opposed to just throwing money at it, even if it's a small allocation, what happens if 15 our of your 20 strategies all suffer massive drawdowns, still almost a blown account or insane drawdowns, because it looked great for the last 5-10 years.

I also don't buy the excuse of not being aware of things, especially in the day and age of Google, I was born in 87' not even in the US, it's just common sense for any person who studies markets at least somewhat. I've seen strategies on here that literally show a 1-2 year backtest, because going further completely blows the algorithm up. I just have no idea what peoples purposes of posting them is, it's not like they mention it hey does good, but has issues beyond this some try to pass it off as look how awesome this is.

Anyway rant over lol

Well, I'm a "new kid" on this (Quantopian) block and I don't know anything about the history of how things were set up here originally and I don't care to speculate or comment on that, but I certainly do agree that we need data going back as far as possible, at least for the major US stocks, indices and interest rates, some of which should certainly be available back as far as pre-1929. IMHO, notwithstanding all the limitations that one can think of, that old data provides some very useful long-term "calibration" points, despite the fact that some people might believe that anything pre- Janet Yellen's reign is too much like ancient history to be relevant anymore. As Warren Buffett says: "When the tide goes out, that's when we see who's been swimming naked". Well, the tide hasn't been out for a while (i.e. any extended period of rising interest rates). I don't know about seeing naked swimmers, but I'm sure that in due course we will see plenty of mud, and I certainly don't want to get stuck in it! I will ask about longer term historic data. Thanks for bringing up this issue. Cheers, Tony.

@Kory,
my apologies, it was not my intention to subvert this thread away from RSI2 to the topic of long-term data. As far as i'm concerned, please feel welcome to ask Dan to split the last few posts out into a separate thread if you want to.

@Dan,
I know some people might disagree, but I think the topic of long-term (i.e. multi-decade) data going back as far as possible is important when it comes to designing ROBUST trading systems / algos that will at least have a good chance of being able to stand up to significantly different market environments compared to what we have today. Please can you advise (either here or in a separate thread) how far back we actually have data for at least the Dow30 stocks, US indices, and Bonds, and any other long-term data sets? Best regards, Tony M.

Kory its a pretty solid strategy, having a maximum DD of 26% (about half of buy and hold) and blowing away buy and hold returns is nothing to sneeze at!

Hello Kory,
I'm not sure if you've noticed, but Quantopian's new slippage model has had a significant impact on the backtested performance of this system. Using the new model results in a 11.5% CAGR vs 20.3% in your backtest. That's quite a difference.

Have you traded this system (or a similar one) to compare live performance to Quantopian's backtest results? I would be interested to see how accurate they are, as one thing that this disparity demonstrates is how important it is to be able estimate transaction costs for systems with high turnover.

64
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
#Published By: Quant Prophet, LLC
#Author: Kory Hoang
#Developer: Jacob Lower
#Description: this is a simple mean-reversion strategy that uses the RSI2 technical indicator which was originally developed and popularized by Cesar Alvarez. Most people are familiar with the RSI14 but the RSI2 is a much more powerful tool for short-term market timing that works well in both momentum and mean-reversion strtegies. The algorithm monitors the 2-day RSI of SPY (S&P500) and TLT (Long-Term US Treasury), both of which are well-known as mean-reverting assets. When an asset's RSI2 dips below 30 ("Oversold"), go long with 50% of the portfolio. When its RSI2 crosses above 70 ("Overbought"), sell the position and go to cash. The vanilla version of this strategy generates good risk-adjusted performance but still underperforms the S&P 500, for the most part. However, using leverage can increase the risk/reward profile in order to outperform the S&P 500 while still maintaining lower risk. I'm using 2x leverge for this strategy.

import talib

def initialize(context):

schedule_function(rebalance, date_rules.every_day(), time_rules.market_open())
set_benchmark(symbol('SPY'))

context.stock = symbol('SPY') #Equity asset
context.bond = symbol('TLT') #Bond asset
context.rsi_period = 2
context.OB = 70 #Overbought threshold
context.OS = 30 #Oversold threshold
context.pct_alloc1 = 0.50
context.pct_alloc2 = 0.50
context.leverage = 2.0

stock_price = data.history(context.stock, 'price', 3, '1d')
bond_price = data.history(context.bond, 'price', 3, '1d')

rsi1 = talib.RSI(stock_price, context.rsi_period)
rsi2 = talib.RSI(bond_price, context.rsi_period)

if rsi1[-1] < context.OS and data.can_trade(context.stock):
#order_target_percent(stock, context.pct_alloc1 * leverage)
elif rsi1[-1] > context.OB and data.can_trade(context.stock):
#order_target_percent(stock, 0.00 * leverage)

if rsi2[-1] < context.OS and data.can_trade(context.bond):
#order_target_percent(bond, context.pct_alloc2 * leverage)
elif rsi2[-1] > context.OB and data.can_trade(context.bond):
#order_target_percent(bond, 0.00 * leverage)

record(leverage = context.account.leverage)

def rebalance(context, data):

order_target_percent(context.stock, context.pct_alloc1 * context.leverage)
order_target_percent(context.stock, 0.00 * context.leverage)

order_target_percent(context.bond, context.pct_alloc2 * context.leverage)
order_target_percent(context.bond, 0.00 * context.leverage)
context.sellBondAlert = False
There was a runtime error.

Just to show that the difference wasn't caused by the custom transaction cost settings in your algorithm, here is a backtest from before Quantopian's update but with the default settings.

13
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
#Published By: Quant Prophet, LLC
#Author: Kory Hoang
#Developer: Jacob Lower
#Description: this is a simple mean-reversion strategy that uses the RSI2 technical indicator which was originally developed and popularized by Cesar Alvarez. Most people are familiar with the RSI14 but the RSI2 is a much more powerful tool for short-term market timing that works well in both momentum and mean-reversion strtegies. The algorithm monitors the 2-day RSI of SPY (S&P500) and TLT (Long-Term US Treasury), both of which are well-known as mean-reverting assets. When an asset's RSI2 dips below 30 ("Oversold"), go long with 50% of the portfolio. When its RSI2 crosses above 70 ("Overbought"), sell the position and go to cash. The vanilla version of this strategy generates good risk-adjusted performance but still underperforms the S&P 500, for the most part. However, using leverage can increase the risk/reward profile in order to outperform the S&P 500 while still maintaining lower risk. I'm using 2x leverge for this strategy.

import talib

def initialize(context):

schedule_function(rebalance, date_rules.every_day(), time_rules.market_open())
set_benchmark(symbol('SPY'))

context.stock = symbol('SPY') #Equity asset
context.bond = symbol('TLT') #Bond asset
context.rsi_period = 2
context.OB = 70 #Overbought threshold
context.OS = 30 #Oversold threshold
context.pct_alloc1 = 0.50
context.pct_alloc2 = 0.50
context.leverage = 2.0

stock_price = data.history(context.stock, 'price', 3, '1d')
bond_price = data.history(context.bond, 'price', 3, '1d')

rsi1 = talib.RSI(stock_price, context.rsi_period)
rsi2 = talib.RSI(bond_price, context.rsi_period)

if rsi1[-1] < context.OS and data.can_trade(context.stock):
#order_target_percent(stock, context.pct_alloc1 * leverage)
elif rsi1[-1] > context.OB and data.can_trade(context.stock):
#order_target_percent(stock, 0.00 * leverage)

if rsi2[-1] < context.OS and data.can_trade(context.bond):
#order_target_percent(bond, context.pct_alloc2 * leverage)
elif rsi2[-1] > context.OB and data.can_trade(context.bond):
#order_target_percent(bond, 0.00 * leverage)

record(leverage = context.account.leverage)

def rebalance(context, data):

order_target_percent(context.stock, context.pct_alloc1 * context.leverage)
order_target_percent(context.stock, 0.00 * context.leverage)

order_target_percent(context.bond, context.pct_alloc2 * context.leverage)
order_target_percent(context.bond, 0.00 * context.leverage)
context.sellBondAlert = False
There was a runtime error.

Slippage should either be at 0 or 0.01. SPY and TLT are extremely liquid with the bid rarely ever being more than 0.01.

Quantopian's default slippage model has always been useless and ultra aggressive, I guess they were always thinking of trading billion dollar trades each time. You aren't going to get any slippage on securities that trade Billions of dollars each day, i mean you might on rare circumstances, but not on every single trade, unless you are trading multi million per trade.

Tyler,
The new model assumes a 5 basis point price impact. Setting the impact to 1 basis point brings the performance back up to the original results (even using the new FixedBasisPointsSlippage class). Quantopian discloses that an assumption of their model is that all securities have the same slippage. It wouldn't surprise me if SPY and TLT fill with lower than average slippage, but I haven't found any data on what that might be. Do you have any data/sources supporting that it is .01 or less?

Elsid,
The model takes volume into account, simulating with minute bars to limit fills to <= 10% of the bars volume. Smaller orders can fill right away, while larger orders will take more bars to fill. You can play with the account setting size in the backtester to see this in action.

Kory,
I separated this post from the on above because it's for a completely different issue. I found a flaw in the logical flow of the algorithm. The setAlerts function only sets true values, not false. The alerts are only set to false after orders are executed in the rebalance function. But the rebalance function does not set them to false if a position is already held. This means that if the oversold condition persists on the day of the order the alert will be set back to true at the end of the day. It will remain true until the position is closed and then a new position will be arbitrarily opened the next day.
I changed the setAlerts function so that the alert is based on the condition, whether it be true or false. The results are still positive, but some of the performance is lost. I can only assume that it is because this is a long only algorithm, run on two assets which have gained over the time period. Even random orders on average should come out positive.

*Slippage was set to 1 basis point for better comparison with the original results. I'm still not sure on what the optimal value is here.

64
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
#Published By: Quant Prophet, LLC
#Author: Kory Hoang
#Developer: Jacob Lower
#Description: this is a simple mean-reversion strategy that uses the RSI2 technical indicator which was originally developed and popularized by Cesar Alvarez. Most people are familiar with the RSI14 but the RSI2 is a much more powerful tool for short-term market timing that works well in both momentum and mean-reversion strtegies. The algorithm monitors the 2-day RSI of SPY (S&P500) and TLT (Long-Term US Treasury), both of which are well-known as mean-reverting assets. When an asset's RSI2 dips below 30 ("Oversold"), go long with 50% of the portfolio. When its RSI2 crosses above 70 ("Overbought"), sell the position and go to cash. The vanilla version of this strategy generates good risk-adjusted performance but still underperforms the S&P 500, for the most part. However, using leverage can increase the risk/reward profile in order to outperform the S&P 500 while still maintaining lower risk. I'm using 2x leverge for this strategy.

import talib

def initialize(context):

schedule_function(rebalance, date_rules.every_day(), time_rules.market_open())
set_slippage(slippage.FixedBasisPointsSlippage(basis_points=1, volume_limit=0.1))
set_benchmark(symbol('SPY'))

context.stock = symbol('SPY') #Equity asset
context.bond = symbol('TLT') #Bond asset
context.rsi_period = 2
context.OB = 70 #Overbought threshold
context.OS = 30 #Oversold threshold
context.pct_alloc1 = 0.50
context.pct_alloc2 = 0.50
context.leverage = 2.0

stock_price = data.history(context.stock, 'price', 3, '1d')
bond_price = data.history(context.bond, 'price', 3, '1d')

rsi1 = talib.RSI(stock_price, context.rsi_period)
rsi2 = talib.RSI(bond_price, context.rsi_period)

record(leverage = context.account.leverage)

def rebalance(context, data):

order_target_percent(context.stock, context.pct_alloc1 * context.leverage)
order_target_percent(context.stock, 0.00 * context.leverage)

order_target_percent(context.bond, context.pct_alloc2 * context.leverage)
order_target_percent(context.bond, 0.00 * context.leverage)
context.sellBondAlert = False
There was a runtime error.

In case you might find the trade detail useful ...

2003-01-02 06:31 pvr:228 INFO 2003-01-02 to 2017-01-13  100000 2018-03-10 02:05 US/Pacific 2003-01-03 06:31 _trac:104 INFO 1 Buy 1159 TLT _ at 86.27 77de 2003-01-03 06:32 _trac:104 INFO 2 Bot 280/1159 TLT (280) at 86.15 77de 2003-01-03 06:36 _trac:104 INFO 6 Bot 100/1159 TLT (380) at 86.11 77de 2003-01-03 06:37 _trac:104 INFO 7 Bot 100/1159 TLT (480) at 86.11 77de 2003-01-03 06:38 _trac:104 INFO 8 Bot 20/1159 TLT (500) at 86.13 77de 2003-01-03 06:39 _trac:104 INFO 9 Bot 10/1159 TLT (510) at 86.05 77de 2003-01-03 06:40 _trac:104 INFO 10 Bot 30/1159 TLT (540) at 86.00 77de 2003-01-03 06:42 _trac:104 INFO 12 Bot 230/1159 TLT (770) at 86.00 77de 2003-01-03 06:44 _trac:104 INFO 14 Bot 60/1159 TLT (830) at 86.09 77de 2003-01-03 06:49 _trac:104 INFO 19 Bot 30/1159 TLT (860) at 86.07 77de 2003-01-03 06:54 _trac:104 INFO 24 Bot 100/1159 TLT (960) at 86.08 77de 2003-01-03 06:59 _trac:104 INFO 29 Bot 20/1159 TLT (980) at 86.09 77de 2003-01-03 07:00 _trac:104 INFO 30 Bot 100/1159 TLT (1080) at 86.00 77de 2003-01-03 07:04 _trac:104 INFO 34 Bot all 79/1159 TLT (1159) at 85.83 77de 2003-01-09 06:31 _trac:104 INFO 1 Sell -1159 TLT (1159) at 86.94 3fae 2003-01-09 06:31 _trac:104 INFO 1 Buy 1099 SPY _ at 91.90 f6b9 [...]  9 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 #Published By: Quant Prophet, LLC #Author: Kory Hoang #Developer: Jacob Lower #Description: this is a simple mean-reversion strategy that uses the RSI2 technical indicator which was originally developed and popularized by Cesar Alvarez. Most people are familiar with the RSI14 but the RSI2 is a much more powerful tool for short-term market timing that works well in both momentum and mean-reversion strtegies. The algorithm monitors the 2-day RSI of SPY (S&P500) and TLT (Long-Term US Treasury), both of which are well-known as mean-reverting assets. When an asset's RSI2 dips below 30 ("Oversold"), go long with 50% of the portfolio. When its RSI2 crosses above 70 ("Overbought"), sell the position and go to cash. The vanilla version of this strategy generates good risk-adjusted performance but still underperforms the S&P 500, for the most part. However, using leverage can increase the risk/reward profile in order to outperform the S&P 500 while still maintaining lower risk. I'm using 2x leverge for this strategy. import talib def initialize(context): schedule_function(setAlerts, date_rules.every_day(), time_rules.market_close()) schedule_function(rebalance, date_rules.every_day(), time_rules.market_open()) # set_commission(commission.PerShare(cost=0.005, min_trade_cost=1.00)) #IB commissions set_slippage(slippage.FixedBasisPointsSlippage(basis_points=1, volume_limit=0.1)) set_benchmark(symbol('SPY')) context.stock = symbol('SPY') #Equity asset context.bond = symbol('TLT') #Bond asset context.rsi_period = 2 context.OB = 70 #Overbought threshold context.OS = 30 #Oversold threshold context.pct_alloc1 = 0.50 context.pct_alloc2 = 0.50 context.leverage = 2.0 #Alert to buy or sell next day. context.buyStockAlert = False context.sellStockAlert = False context.buyBondAlert = False context.sellBondAlert = False for i in range(1, 391): schedule_function(pvr, date_rules.every_day(), time_rules.market_open(minutes=i)) schedule_function(track_orders, date_rules.every_day(), time_rules.market_open(minutes=i)) def setAlerts(context, data): stock_price = data.history(context.stock, 'price', 3, '1d') bond_price = data.history(context.bond, 'price', 3, '1d') rsi1 = talib.RSI(stock_price, context.rsi_period) rsi2 = talib.RSI(bond_price, context.rsi_period) context.buyStockAlert = rsi1[-1] < context.OS context.sellStockAlert = rsi1[-1] > context.OB context.buyBondAlert = rsi2[-1] < context.OS context.sellBondAlert = rsi2[-1] > context.OB #record(leverage = context.account.leverage) def rebalance(context, data): if data.can_trade(context.stock): if context.buyStockAlert and context.portfolio.positions[context.stock].amount == 0: order_target_percent(context.stock, context.pct_alloc1 * context.leverage) context.buyStockAlert = False if context.sellStockAlert: order_target(context.stock, 0.00) context.sellStockAlert = False if data.can_trade(context.bond): if context.buyBondAlert and context.portfolio.positions[context.bond].amount == 0: order_target_percent(context.bond, context.pct_alloc2 * context.leverage) context.buyBondAlert = False if context.sellBondAlert: order_target(context.bond, 0.00) context.sellBondAlert = False def track_orders(context, data): ''' Show orders when made and filled. Info: https://www.quantopian.com/posts/track-orders ''' c = context if 'trac' not in c: c.t_options = { # __________ O P T I O N S __________ 'log_neg_cash': 1, # Show cash only when negative. 'log_cash' : 0, # Show cash values in logging window or not. 'log_ids' : 1, # Include order id's in logging window or not. 'log_unfilled': 1, # When orders are unfilled. (stop & limit excluded). } # Move these to initialize() for better efficiency. c.trac = {} c.t_dates = { # To not overwhelm the log window, start/stop dates can be entered. 'active': 0, 'start' : [], # Start dates, option like ['2007-05-07', '2010-04-26'] 'stop' : [] # Stop dates, option like ['2008-02-13', '2010-11-15'] } from pytz import timezone # Python only does once, makes this portable. # Move to top of algo for better efficiency. # If 'start' or 'stop' lists have something in them, triggers ... if c.t_dates['start'] or c.t_dates['stop']: date = str(get_datetime().date()) if date in c.t_dates['start']: # See if there's a match to start c.t_dates['active'] = 1 elif date in c.t_dates['stop']: # ... or to stop c.t_dates['active'] = 0 else: c.t_dates['active'] = 1 # Set to active b/c no conditions. if c.t_dates['active'] == 0: return # Skip if not active. def _minute(): # To preface each line with the minute of the day. bar_dt = get_datetime().astimezone(timezone('US/Eastern')) return str((bar_dt.hour * 60) + bar_dt.minute - 570).rjust(3) # (-570 = 9:31a) def _trac(to_log): # So all logging comes from the same line number, log.info(to_log) # for vertical alignment in the logging window. for oid in c.trac.copy(): # Existing known orders o = get_order(oid) if o.dt == o.created: continue # No chance of fill yet. cash = '' prc = data.current(o.sid, 'price') if data.can_trade(o.sid) else c.portfolio.positions[o.sid].last_sale_price if (c.t_options['log_neg_cash'] and c.portfolio.cash < 0) or c.t_options['log_cash']: cash = 'cash {}'.format(int(c.portfolio.cash)) if o.status == 2: # Canceled do = 'Buy' if o.amount > 0 else 'Sell' ; style = '' if o.stop: style = ' stop {}'.format(o.stop) if o.limit: style = ' stop {} limit {}'.format(o.stop, o.limit) elif o.limit: style = ' limit {}'.format(o.limit) _trac(' {} Canceled {} {} {}{} at {} {} {}'.format(_minute(), do, o.amount, o.sid.symbol, style, prc, cash, o.id[-4:] if c.t_options['log_ids'] else '')) del c.trac[o.id] elif o.filled: # Filled at least some. filled = '{}'.format(o.amount) filled_amt = 0 if o.status == 1: # Complete if 0 < c.trac[o.id] < o.amount: filled = 'all {}/{}'.format(o.filled - c.trac[o.id], o.amount) filled_amt = o.filled else: # c.trac[o.id] value is previously filled total filled_amt = o.filled - c.trac[o.id] # filled this time, can be 0 c.trac[o.id] = o.filled # save fill value for increments math filled = '{}/{}'.format(filled_amt, o.amount) if filled_amt: now = ' ({})'.format(c.portfolio.positions[o.sid].amount) if c.portfolio.positions[o.sid].amount else ' _' pnl = '' # for the trade only amt = c.portfolio.positions[o.sid].amount ; style = '' if (amt - o.filled) * o.filled < 0: # Profit-taking scenario including short-buyback cb = c.portfolio.positions[o.sid].cost_basis if cb: pnl = -filled_amt * (prc - cb) sign = '+' if pnl > 0 else '-' pnl = ' ({}{})'.format(sign, '%.0f' % abs(pnl)) if o.stop: style = ' stop {}'.format(o.stop) if o.limit: style = ' stop () limit {}'.format(o.stop, o.limit) elif o.limit: style = ' limit {}'.format(o.limit) if o.filled == o.amount: del c.trac[o.id] _trac(' {} {} {} {}{} at {}{}{}'.format(_minute(), 'Bot' if o.amount > 0 else 'Sold', filled, o.sid.symbol, now, '%.2f' % prc, pnl, style).ljust(52) + ' {} {}'.format(cash, o.id[-4:] if c.t_options['log_ids'] else '')) elif c.t_options['log_unfilled'] and not (o.stop or o.limit): _trac(' {} {} {}{} unfilled {}'.format(_minute(), o.sid.symbol, o.amount, ' limit' if o.limit else '', o.id[-4:] if c.t_options['log_ids'] else '')) oo = get_open_orders().values() if not oo: return # Handle new orders cash = '' if (c.t_options['log_neg_cash'] and c.portfolio.cash < 0) or c.t_options['log_cash']: cash = 'cash {}'.format(int(c.portfolio.cash)) for oo_list in oo: for o in oo_list: if o.id in c.trac: continue # Only new orders beyond this point prc = data.current(o.sid, 'price') if data.can_trade(o.sid) else c.portfolio.positions[o.sid].last_sale_price c.trac[o.id] = 0 ; style = '' now = ' ({})'.format(c.portfolio.positions[o.sid].amount) if c.portfolio.positions[o.sid].amount else ' _' if o.stop: style = ' stop {}'.format(o.stop) if o.limit: style = ' stop {} limit {}'.format(o.stop, o.limit) elif o.limit: style = ' limit {}'.format(o.limit) _trac(' {} {} {} {}{} at {}{}'.format(_minute(), 'Buy' if o.amount > 0 else 'Sell', o.amount, o.sid.symbol, now, '%.2f' % prc, style).ljust(52) + ' {} {}'.format(cash, o.id[-4:] if c.t_options['log_ids'] else '')) def pvr(context, data): ''' Custom chart and/or logging of profit_vs_risk returns and related information http://quantopian.com/posts/pvr ''' import time from datetime import datetime as _dt 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 readability] if 'pvr' not in c: # For real money, you can modify this to total cash input minus any withdrawals manual_cash = c.portfolio.starting_cash time_zone = 'US/Pacific' # Optionally change to your own time zone for wall clock time c.pvr = { 'options': { # # # # # # # # # # Options # # # # # # # # # # 'logging' : 0, # Info to logging window with some new maximums 'log_summary' : 126, # Summary every x days. 252/yr 'record_pvr' : 1, # Profit vs Risk returns (percentage) 'record_pvrp' : 0, # PvR (p)roportional neg cash vs portfolio value 'record_cash' : 0, # Cash available 'record_max_lvrg' : 1, # Maximum leverage encountered 'record_max_risk' : 0, # Highest risk overall 'record_shorting' : 0, # Total value of any shorts 'record_max_shrt' : 1, # Max value of shorting total 'record_cash_low' : 1, # Any new lowest cash level 'record_q_return' : 0, # Quantopian returns (percentage) 'record_pnl' : 0, # Profit-n-Loss 'record_risk' : 0, # Risked, max cash spent or shorts beyond longs+cash 'record_leverage' : 1, # End of day leverage (context.account.leverage) # All records are end-of-day or the last data sent to chart during any day. # The way the chart operates, only the last value of the day will be seen. # # # # # # # # # End options # # # # # # # # # }, 'pvr' : 0, # Profit vs Risk returns based on maximum spent 'cagr' : 0, 'max_lvrg' : 0, 'max_shrt' : 0, 'max_risk' : 0, 'days' : 0.0, 'date_prv' : '', 'date_end' : get_environment('end').date(), 'cash_low' : manual_cash, 'cash' : manual_cash, 'start' : manual_cash, 'tz' : time_zone, 'begin' : time.time(), # For run time 'run_str' : '{} to {}{}  {} {}'.format(get_environment('start').date(), get_environment('end').date(), int(manual_cash), _dt.now(timezone(time_zone)).strftime("%Y-%m-%d %H:%M"), time_zone)
}
if c.pvr['options']['record_pvrp']: c.pvr['options']['record_pvr'] = 0 # if pvrp is active, straight pvr is off
if get_environment('arena') not in ['backtest', 'live']: c.pvr['options']['log_summary'] = 1 # Every day when real money
log.info(c.pvr['run_str'])
p = c.pvr ; o = c.pvr['options'] ; pf = c.portfolio ; pnl = pf.portfolio_value - p['start']
def _pvr(c):
p['cagr'] = ((pf.portfolio_value / p['start']) ** (1 / (p['days'] / 252.))) - 1
ptype = 'PvR' if o['record_pvr'] else 'PvRp'
log.info('{} {} %/day   cagr {}   Portfolio value {}   PnL {}'.format(ptype, '%.4f' % (p['pvr'] / p['days']), '%.3f' % p['cagr'], '%.0f' % pf.portfolio_value, '%.0f' % pnl))
log.info('  Profited {} on {} activated/transacted for PvR of {}%'.format('%.0f' % pnl, '%.0f' % p['max_risk'], '%.1f' % p['pvr']))
log.info('  QRet {} PvR {} CshLw {} MxLv {} MxRisk {} MxShrt {}'.format('%.2f' % (100 * pf.returns), '%.2f' % p['pvr'], '%.0f' % p['cash_low'], '%.2f' % p['max_lvrg'], '%.0f' % p['max_risk'], '%.0f' % p['max_shrt']))
def _minut():
dt = get_datetime().astimezone(timezone(p['tz']))
return str((dt.hour * 60) + dt.minute - 570).rjust(3)  # (-570 = 9:31a)
date = get_datetime().date()
if p['date_prv'] != date:
p['date_prv'] = date
p['days'] += 1.0
do_summary = 0
if o['log_summary'] and p['days'] % o['log_summary'] == 0 and _minut() == '100':
do_summary = 1              # Log summary every x days
if do_summary or date == p['date_end']:
p['cash'] = pf.cash
elif p['cash'] == pf.cash and not o['logging']: return  # for speed

shorts = sum([z.amount * z.last_sale_price for s, z in pf.positions.items() if z.amount < 0])
new_key_hi = 0                  # To trigger logging if on.
cash       = pf.cash
cash_dip   = int(max(0, p['start'] - cash))
risk       = int(max(cash_dip, -shorts))

if o['record_pvrp'] and cash < 0:   # Let negative cash ding less when portfolio is up.
cash_dip = int(max(0, cash_dip * p['start'] / pf.portfolio_value))
# Imagine: Start with 10, grows to 1000, goes negative to -10, should not be 200% risk.

if int(cash) < p['cash_low']:             # New cash low
new_key_hi = 1
p['cash_low'] = int(cash)             # Lowest cash level hit
if o['record_cash_low']: record(CashLow = p['cash_low'])

if c.account.leverage > p['max_lvrg']:
new_key_hi = 1
p['max_lvrg'] = c.account.leverage    # Maximum intraday leverage
if o['record_max_lvrg']: record(MxLv    = p['max_lvrg'])

if shorts < p['max_shrt']:
new_key_hi = 1
p['max_shrt'] = shorts                # Maximum shorts value
if o['record_max_shrt']: record(MxShrt  = p['max_shrt'])

if risk > p['max_risk']:
new_key_hi = 1
p['max_risk'] = risk                  # Highest risk overall
if o['record_max_risk']:  record(MxRisk = p['max_risk'])

# Profit_vs_Risk returns based on max amount actually invested, long or short
if p['max_risk'] != 0: # Avoid zero-divide
p['pvr'] = 100 * pnl / p['max_risk']
ptype = 'PvRp' if o['record_pvrp'] else 'PvR'
if o['record_pvr'] or o['record_pvrp']: record(**{ptype: p['pvr']})

if o['record_shorting']: record(Shorts = shorts)             # Shorts value as a positve
if o['record_leverage']: record(Lv     = c.account.leverage) # Leverage
if o['record_cash']    : record(Cash   = cash)               # Cash
if o['record_risk']    : record(Risk   = risk)  # Amount in play, maximum of shorts or cash used
if o['record_q_return']: record(QRet   = 100 * pf.returns)
if o['record_pnl']     : record(PnL    = pnl)                # Profit|Loss

if o['logging'] and new_key_hi:
log.info('{}{}{}{}{}{}{}{}{}{}{}{}'.format(_minut(),
' Lv '     + '%.1f' % c.account.leverage,
' MxLv '   + '%.2f' % p['max_lvrg'],
' QRet '   + '%.1f' % (100 * pf.returns),
' PvR '    + '%.1f' % p['pvr'],
' PnL '    + '%.0f' % pnl,
' Cash '   + '%.0f' % cash,
' CshLw '  + '%.0f' % p['cash_low'],
' Shrt '   + '%.0f' % shorts,
' MxShrt ' + '%.0f' % p['max_shrt'],
' Risk '   + '%.0f' % risk,
' MxRisk ' + '%.0f' % p['max_risk']
))
if do_summary: _pvr(c)
if get_datetime() == get_environment('end'):   # Summary at end of run
_pvr(c) ; elapsed = (time.time() - p['begin']) / 60  # minutes
log.info( '{}\nRuntime {} hr {} min'.format(p['run_str'], int(elapsed / 60), '%.1f' % (elapsed % 60)))

There was a runtime error.