Backtested The Turtle Strategy

Krit Junsree
5 min readMay 31, 2023

--

Backtested​ The​ Turtle​ strategy​

The Turtle Trading strategy was a strategy used by Richard Dennis and Bill Eckhardt in the 1980s to prove that anyone could be trained to trade. This strategy is a trend-following strategy that was made famous in the book “The Complete Turtle Trader”.

Here’s a basic implementation of the Turtle Strategy using Python and the yfinance library to download historical price data and backtrader for backtesting. This code will backtest the Turtle Strategy on the stock price data of Apple (AAPL) for simplicity.

import backtrader as bt
import yfinance as yf

class TurtleStrategy(bt.Strategy):
params = (('long_period', 20), ('short_period', 10),)

def __init__(self):
self.order = None
self.buy_price = None
self.buy_comm = None

self.high = bt.indicators.Highest(self.data.high, period=self.p.long_period)
self.low = bt.indicators.Lowest(self.data.low, period=self.p.short_period)

def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
return

if order.status in [order.Completed]:
if order.isbuy():
self.log('BUY EXECUTED, %.2f' % order.executed.price)
elif order.issell():
self.log('SELL EXECUTED, %.2f' % order.executed.price)

self.order = None

def next(self):
if self.order:
return

if not self.position:
if self.data.close > self.high:
self.log('BUY CREATE, %.2f' % self.data.close[0])
self.order = self.buy()

else:
if self.data.close < self.low:
self.log('SELL CREATE, %.2f' % self.data.close[0])
self.order = self.sell()

def log(self, txt, dt=None):
dt = dt or self.datas[0].datetime.date(0)
print(f'{dt.isoformat()}, {txt}')


def run_backtest():
cerebro = bt.Cerebro()

data = yf.download('AAPL','2000-01-01','2023-05-31')
datafeed = bt.feeds.PandasData(dataname=data)
cerebro.adddata(datafeed)

cerebro.addstrategy(TurtleStrategy)
cerebro.broker.setcash(100000.0)

print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.run()
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

if __name__ == '__main__':
run_backtest()

This script backtests the Turtle Strategy. In the TurtleStrategy class, the Highest indicator is used to calculate the highest high over the last 20 days (for the buy signal) and the Lowest indicator is used to calculate the lowest low over the last 10 days (for the sell signal).

In the next method, if there's no active order and we're out of the market, the system checks if the current close is higher than the highest high of the last 20 days. If so, it creates a buy order. If we're in the market, the system checks if the current close is lower than the lowest low of the last 10 days, and if so, it creates a sell order.

Please note that this is a very simple implementation of the Turtle Strategy, and the real strategy involves more elements such as risk management and unit sizing.

After creating the Cerebro engine, we download the data, add the data to the engine, add the strategy to the engine, set the initial cash, print the starting portfolio value, run the backtest, and print the final portfolio value.

For advanced usage, we could add more strategies, data feeds, observers, analyzers, etc., but that’s out of the scope of this simple example.

After running the backtest, you may want to plot your backtesting result. This can be done using the plot method provided by backtrader. Here is the updated code snippet:

    # ...
print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
cerebro.plot(style='candlestick')

Note: To plot the backtesting result, you need matplotlib installed.

Additionally, you may want to include commission in your backtest. You can do this by setting the commission rate of your broker simulator, for example:

    cerebro.broker.setcommission(commission=0.001)

This sets the commission rate to 0.1%.

That’s the whole code of a simple backtest of the Turtle Strategy. I hope this helps you get started with backtesting using backtrader! Please feel free to let me know if you have any questions.

In the original Turtle strategy, they use the concept of N which is a measure of the market volatility over the recent past. N is calculated as a 20-day exponential moving average of the TrueRange, which is the greatest of:

  1. Today’s High minus today’s Low
  2. The absolute value of: Today’s High minus yesterday’s Close
  3. The absolute value of: Today’s Low minus yesterday’s Close

Once N is calculated, it can be used to calculate the position size, or ‘unit’, to be traded. A single unit is calculated as 1% of the account equity divided by N. For simplicity’s sake, we’ll assume an initial account equity of $100,000 in the following Python code:

class TurtleStrategy(bt.Strategy):
params = (('long_period', 20), ('short_period', 10),)

def __init__(self):
self.order = None
self.buy_price = None
self.buy_comm = None

self.high = bt.indicators.Highest(self.data.high, period=self.p.long_period)
self.low = bt.indicators.Lowest(self.data.low, period=self.p.short_period)
self.tr = bt.indicators.TrueRange(self.data)
self.n = bt.indicators.SimpleMovingAverage(self.tr, period=self.p.long_period)

def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
return

if order.status in [order.Completed]:
if order.isbuy():
self.log('BUY EXECUTED, %.2f' % order.executed.price)
elif order.issell():
self.log('SELL EXECUTED, %.2f' % order.executed.price)

self.order = None

def next(self):
if self.order:
return

size = (self.broker.getvalue() * 0.01) / self.n[0]

if not self.position:
if self.data.close > self.high:
self.log('BUY CREATE, %.2f' % self.data.close[0])
self.order = self.buy(size=size)

else:
if self.data.close < self.low:
self.log('SELL CREATE, %.2f' % self.data.close[0])
self.order = self.sell(size=size)

def log(self, txt, dt=None):
dt = dt or self.datas[0].datetime.date(0)
print(f'{dt.isoformat()}, {txt}')

This implementation calculates N as a 20-day Simple Moving Average of TrueRange. The position size, or ‘unit’, to be traded is calculated as 1% of the account equity divided by N. This position size is used when placing buy or sell orders.

It’s also worth mentioning that the Turtle Strategy recommends not risking more than 2% of account equity on a single trade and not initiating a new position if the total risk across all open positions would exceed 20% of account equity. However, these risk management rules aren’t included in the above Python code and are left as an exercise for you.

The Turtle Strategy employs two risk management rules:

  1. Do not risk more than 2% of account equity on a single trade.
  2. Do not initiate a new position if the total risk across all open positions would exceed 20% of account equity.

To implement this in our backtesting script, we need to adjust the position sizing logic in the next method. Here is the updated Python code:

class TurtleStrategy(bt.Strategy):
params = (('long_period', 20), ('short_period', 10),)

def __init__(self):
self.order = None
self.buy_price = None
self.buy_comm = None

self.high = bt.indicators.Highest(self.data.high, period=self.p.long_period)
self.low = bt.indicators.Lowest(self.data.low, period=self.p.short_period)
self.tr = bt.indicators.TrueRange(self.data)
self.n = bt.indicators.SimpleMovingAverage(self.tr, period=self.p.long_period)

def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
return

if order.status in [order.Completed]:
if order.isbuy():
self.log('BUY EXECUTED, %.2f' % order.executed.price)
elif order.issell():
self.log('SELL EXECUTED, %.2f' % order.executed.price)

self.order = None

def next(self):
if self.order:
return

size = (self.broker.getvalue() * 0.01) / self.n[0]
total_risk = size * self.n[0]

# Do not risk more than 2% of account equity on a single trade.
if total_risk > (self.broker.getvalue() * 0.02):
return

# Do not initiate a new position if the total risk across all open positions would exceed 20% of account equity.
if (self.broker.getvalue() - total_risk) < (self.broker.getvalue() * 0.8):
return

if not self.position:
if self.data.close > self.high:
self.log('BUY CREATE, %.2f' % self.data.close[0])
self.order = self.buy(size=size)

else:
if self.data.close < self.low:
self.log('SELL CREATE, %.2f' % self.data.close[0])
self.order = self.sell(size=size)

def log(self, txt, dt=None):
dt = dt or self.datas[0].datetime.date(0)
print(f'{dt.isoformat()}, {txt}')

In this version, before initiating a new position, we calculate the total risk for the potential new trade and check if it would violate any of the two risk management rules. If either of the rules would be violated, we skip the trade.

Keep in mind that this is a simple implementation of these rules. The actual risk management of the Turtle Strategy might be more complicated and involve other considerations.

Also, remember that it’s always important to thoroughly test any trading strategy before live trading.

--

--

Krit Junsree
Krit Junsree

Written by Krit Junsree

I has developed expertise in a variety of data analysis tools and techniques, including Excel, SQL and Python.

No responses yet