Sample using History and Stochastics

I was having trouble figuring out Stochastics using the new (awesome) History function. Thanks to Eddie Hebert who helped me figure it out. This is a sample algorithm which you may find useful in case you have been having trouble also. Although it's not meant to be an optimized algorithm, I do think it shows how much can be done with History and some of the other new functions, and it beats buy and hold, although it suffers from whipsaw at times.

69
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
from pytz import timezone

#initialize the strategy
def initialize(context):
context.etf = sid(8554)

# Converts all time-zones into US EST to avoid confusion
loc_dt = get_datetime().astimezone(timezone('US/Eastern'))
if loc_dt.hour == 15 and loc_dt.minute == 0:
return True
else:
return False

def handle_data(context, data):

# only execute algorithm once per day

closes = history(44, '1d', 'close_price')
highs = history(44, '1d', 'high')
lows = history(44, '1d', 'low')

stoch_data = talib.STOCH(highs[context.etf],
lows[context.etf],
closes[context.etf],
fastk_period=39,
slowk_period=3,
slowk_matype=0,
slowd_period=3,
slowd_matype=0)

slowk = stoch_data[0][-1]
slowd = stoch_data[1][-1]
record(slowk=slowk,slowd=slowd,mid=50,topthreshold=80,bottomthreshold=20)

if (slowk>50) : order_target_percent(context.etf, 1)
elif (slowk<50): order_target_percent(context.etf, 0)
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.
8 responses

Great illustration of history function and statistical library.

Yet... One has to be careful when it comes to claiming a win over buy and hold. Quantopian S&P 500 benchmark does not re-invest dividends, thus over time almost any algorithm that occasionally sets the holding to 100% of SPY etf will beat the benchmark. This is the reason I have nudged the development team on several occasions to include and option for S&P 500 with re-invested dividends as a benchmark option.

Say that all algorithm does is keep the investment at 100% SPY. That means every few months a few new etf stocks will be bought, while the benchmark will be 5%, 10%, ... 50% in cash in 10 years or so. Towards the end the higher exposure to the index and zero return on dividend cash will tilt the result in algorithm's favor. That is not to say the algorithm does not make money, but that the test confirming it is invalid.

I have changed the algorithm to keep the position at 100% of s&p at the end of each day. As it turns out, it beats not only the benchmark but also the algorithm above. 41% return during the same period, vs. 33% for the original algorithm and 23.4% of s&p with no dividend re-invested. This is why the re-invested dividend s&p is such an important benchmark.

One may be able to beat the market, but it is much harder than it seems. Badly constructed benchmark leads to a wrong comparison, thus making it look easy.

11
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
from pytz import timezone

#initialize the strategy
def initialize(context):
context.etf = sid(8554)

# Converts all time-zones into US EST to avoid confusion
loc_dt = get_datetime().astimezone(timezone('US/Eastern'))
if loc_dt.hour == 15 and loc_dt.minute == 0:
return True
else:
return False

def handle_data(context, data):
# only execute algorithm once per day

order_target_percent(context.etf, 1)
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.

The change to the benchmark being used is being tested. If it passes tests, it will ship this week. If it doesn't pass the tests. . . well then we'll have to see what it didn't pass and go from there.

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.

Great information about the benchmarks. Thanks.

Thanks to good folk of Quntopian for responsiveness. Here's my updated wishlist, based on common issues found in algorithms posted.

1. S&P500 benchmark with re-invested dividend, as discussed above.
2. Other benchmark options, such as DJIA or optional ticker, so one can compare to DIA or Russel ETF's.
3. Limit borrowing to zero or some fixed percentage, so algorithms can be sand boxed as in real life
4. Open price of the day, close price of the previous day, yearly/all-time high/low and similar data would be useful. One can construct it but not always easily

Looks like #1 and #2 are to be addressed shortly, #3 should work well once connected to play or real IB account, #4 is left to be desired for.

The new history function should work easily to solve #4

Here is another sample using Bollinger Bands

14
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
from pytz import timezone

#initialize the strategy
def initialize(context):
context.etf = sid(8554)

# Converts all time-zones into US EST to avoid confusion
loc_dt = get_datetime().astimezone(timezone('US/Eastern'))
if loc_dt.hour == 15 and loc_dt.minute == 0:
return True
else:
return False

def handle_data(context, data):

# only execute algorithm once per day

closes = history(90, '1d', 'close_price')

stoch_data = talib.BBANDS(closes[context.etf],
timeperiod=50,
nbdevup=2,
nbdevdn=2)

upperband = stoch_data[0][-1]
line = stoch_data[1][-1]
lowerband = stoch_data[2][-1]
record(lower=lowerband,upper=upperband,line=line,price=data[context.etf].price)

if (data[context.etf].price>lowerband) : order_target_percent(context.etf, 1)
else : order_target_percent(context.etf, 0)
This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.

Here is another using Keltner channels. One must calculate the channels yourself, using a moving average and the ATR. But this is trivial to do. There is not trading for this one. I had to add a round function to the record to make it work, but I'm not sure why.

15
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
from pytz import timezone

#initialize the strategy
def initialize(context):
context.etf = sid(8554)

# Converts all time-zones into US EST to avoid confusion
loc_dt = get_datetime().astimezone(timezone('US/Eastern'))
if loc_dt.hour == 15 and loc_dt.minute == 0:
return True
else:
return False

def handle_data(context, data):

# only execute algorithm once per day

closes = history(20, '1d', 'close_price')
highs = history(20, '1d', 'high')
lows = history(20, '1d', 'low')
movingavg = closes.mean()
atr_data = talib.ATR(highs[context.etf],
lows[context.etf],
closes[context.etf],
timeperiod=18)
#print(atr_data)
atr = atr_data[-1]
#atr =5
#print(atr)
#print(movingavg)
lowerband = movingavg-atr*2
upperband =movingavg+atr*2
#print(lowerband)
#print(round(lowerband,2))
record(lower=round(lowerband,2),upper=round(upperband,2),sma=round(movingavg,2),price=data[context.etf].price)


This backtest was created using an older version of the backtester. Please re-run this backtest to see results using the latest backtester. Learn more about the recent changes.
There was a runtime error.