Stochastic Crossover with 2x ETFs and low drawdown

I cloned an Stochastic Crossover algorithm and fine tuned it with a basket of four 2x ETFs.

The main intention was to
- buy at absolute lows (whenever there is a crossover under 10)
- sell whenever there is a crossover over 80 OR the position has a 10% loss

So far the strategy looks great. Very few trades with low drawdown ( < 10% ) but still matches or beats the S&P.

Since there a long periods with no trades, I was thinking that we can hold some other ETF's (like SPLV,SHY or SPY itself) in the interim. Basically buy SPY as long as the 2x ETF stoch is over 25 and sell it whenever the 2x ETF stoch is less than 25 so that we prepare to buy the 2x ETF if it meets the conditions above.

Can someone collaborate & help program those conditions and see if it increases the returns and still keeps the drawdowns low.

140
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
# This algorithm uses talib's STOCH function to determine entry and exit points.

# When the stochastic oscillator dips below 10, the stock is determined to be oversold
# and a long position is opened. The position is exited when the indicator rises above 90
# because the stock is thought to be overbought.

# Because this algorithm uses the history function, it will only run in minute mode.
# We will constrain the trading to once per day at market open in this example.

import talib
import numpy as np
import pandas as pd

# Setup our variables
def initialize(context):
context.stocks = [sid(33208),sid(33265),sid(32270),sid(32272)]

# Set the percent of the account to be invested per stock
context.long_pct_per_stock = 1.0 / len(context.stocks)

# Create a variable to track the date change
context.date = None

def handle_data(context, data):
todays_date = get_datetime().date()

# Do nothing unless the date has changed
if todays_date == context.date:
return
# Set the new date
context.date = todays_date

# Load historical data for the stocks
high = history(30, '1d', 'high')
low = history(30, '1d', 'low')
close = history(30, '1d', 'close_price')

# Iterate over our list of stocks
for stock in context.stocks:
current_position = context.portfolio.positions[stock].amount
slowk, slowd = talib.STOCH(high[stock],
low[stock],
close[stock],
fastk_period=14,
slowk_period=3,
slowk_matype=0,
slowd_period=3,
slowd_matype=0)

# get the most recent value
slowk = slowk[-1]
slowd = slowd[-1]

# If either the slowk or slowd are less than 10, the stock is
# 'oversold,' a long position is opened if there are no shares
# in the portfolio.
if ((slowk < 10 or slowd < 10) and slowk > slowd) and current_position <= 0:
order_target_percent(stock, context.long_pct_per_stock)
#sell whenever you have crossover over 80 OR position is down 10%
elif current_position > 0 and (((slowk > 80 or slowd > 80) and slowk < slowd) or (context.portfolio.positions[stock].last_sale_price/context.portfolio.positions[stock].cost_basis) < 0.90):
order_target(stock, 0)


There was a runtime error.
3 responses

I replaced the double leveraged ETFs with single leveraged ETFs double leveraged (e.g. instead of SSO, use 2x SPY) since Quantopian doesn't allow double leveraged ETFs.

I also replaced cash with IEF and ran a longer backtest. Your algorithm doesn't really perform at all well until 2010.

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
# This algorithm uses talib's STOCH function to determine entry and exit points.

# When the stochastic oscillator dips below 10, the stock is determined to be oversold
# and a long position is opened. The position is exited when the indicator rises above 90
# because the stock is thought to be overbought.

# Because this algorithm uses the history function, it will only run in minute mode.
# We will constrain the trading to once per day at market open in this example.

import talib
import numpy as np
import pandas as pd

# Setup our variables
def initialize(context):
context.stocks = [
sid(21519),
sid(25904),
sid(8554),
sid(19920)
]

# Set the percent of the account to be invested per stock
context.long_pct_per_stock = 2.0 / len(context.stocks)

# Create a variable to track the date change
context.date = None

def handle_data(context, data):
record(lev=context.account.leverage)
todays_date = get_datetime().date()

# Do nothing unless the date has changed
if todays_date == context.date:
return
# Set the new date
context.date = todays_date

# Load historical data for the stocks
high = history(30, '1d', 'high')
low = history(30, '1d', 'low')
close = history(30, '1d', 'close_price')

# Iterate over our list of stocks
for stock in context.stocks:
current_position = context.portfolio.positions[stock].amount
slowk, slowd = talib.STOCH(high[stock],
low[stock],
close[stock],
fastk_period=14,
slowk_period=3,
slowk_matype=0,
slowd_period=3,
slowd_matype=0)

# get the most recent value
slowk = slowk[-1]
slowd = slowd[-1]

# If either the slowk or slowd are less than 10, the stock is
# 'oversold,' a long position is opened if there are no shares
# in the portfolio.
if ((slowk < 10 or slowd < 10) and slowk > slowd) and current_position <= 0:
order_target_percent(stock, context.long_pct_per_stock)
#sell whenever you have crossover over 80 OR position is down 10%
elif current_position > 0 and (((slowk > 80 or slowd > 80) and slowk < slowd) or (context.portfolio.positions[stock].last_sale_price/context.portfolio.positions[stock].cost_basis) < 0.90):
order_target(stock, 0)

if context.account.leverage == 0:
if not get_open_orders(sid(23870)):
order_target_percent(sid(23870), 2.0)


There was a runtime error.

PvR is only 28%. Leverage over 5.
It started with $10K, spent an additional ~$48K, and wound up with $26,350. The benchmark happened to be 100% over that time period. PvR is a profit for the amount risked / put on the table ($58K) returns calculation, the rubber-meets-the-road amount an investor would care about. In some cases, we are our own investor.

The original code was daily instead of minute, different time period as noted, and didn't have the margin problem much.

Thank you Lucas and garyha

You guys bought a whole new perspective to my initial algorithm. Appreciate your help.