Building a Strategy#
So you have an idea for a trading strategy. How do you code this up to use with AutoTrader? Read on to find out, where we code the MACD strategy described here.
Tip
The code for the MACD crossover strategy shown in this tutorial can be found in the demo repository.
Strategy Construction#
Strategies in AutoTrader are built as class objects. They contain the logic required to transform data into a trading signals. Generally speaking, a strategy class will be instantiated with the name of the instrument being traded and the strategy parameters, but you can customise what gets passed in using the strategy configuration.
Strategy Configuration#
Follow Along
Follow along in the demo repository: config/macd.yaml
Lets start by writing the strategy configuration .yaml
file.
Call this macd.yaml
, refering to our MACD strategy.
This file is a convenient place to define your strategy parameters (using the
PARAMETERS
key) and which instruments to trade using this strategy (using the
WATCHLIST
key). It is also used to tell AutoTrader where to find your strategy -
the MODULE
key defines the prefix of the file where your strategy is written,
and the CLASS
key defines the actual class name of your strategy.
You can also define the strategy configuration using a dictionary instead of a
yaml file, and pass that in when adding your strategy.
By separating the strategy parameters from the strategy iteself, you are able to easily tweak the strategy to your liking, or perform hyperparameter optimisation.
Attention
YAML cares about whitespace; each nested key must be indented by two spaces more than its parent.
We will put our strategy parameters under
the PARAMETERS
key. You can call these parameters whatever you want, and that
is how they will appear in your strategy. As you can see below, we define the
EMA period by the ema_period
key, the MACD settings by the MACD_fast
,
MACD_slow
and MACD_smoothing
keys, and the risk-to-reward ratio is by
the RR
key.
# macd.yaml
NAME: 'Simple Macd Strategy' # strategy name
MODULE: 'macd' # strategy module
CLASS: 'SimpleMACD' # strategy class
INTERVAL: '1h' # stategy timeframe
PERIOD: 300 # candles required by strategy
SIZING: 'risk' # sizing method
RISK_PC: 1.5 # risk per trade (%)
PARAMETERS: # strategy parameters
ema_period: 200
MACD_fast: 12
MACD_slow: 26
MACD_smoothing: 9
# Exit level parameters
RR: 1.5
WATCHLIST: ['EURUSD=X'] # strategy watchlist
This file is read by AutoTrader and passed into your strategy when it is
instantiated. We will start by backtesting on the EUR/USD currency pair
alone, as specified by the WATCHLIST
key. Note that the format
of the instruments provided here must match your data feed (in this case,
Yahoo Finance, which denotes FX with ‘=X’).
It is worth noting that we are taking advantage of AutoTrader’s automatic
position size calculation, by defining the SIZING: 'risk'
and RISK_PC: 1.5
keys. These keys tell AutoTrader to use a risk-based approach to position
sizing. As such, when an order is submitted from the strategy, AutoTrader
will use the current price and stop-loss price to calculate the appropriate
position size, capping the maximium loss to the percentage defined by
RISK_PC
. In this case, any single trade can only ever lose 1.5% of the
account.
We also define the INTERVAL: '1h'
key, meaning that our strategy will run
on the 1-hour timeframe. This value is used when retrieving price data
through AutoData. This is discussed more in the next
section.
Tip
You can find a template strategy configuration file in the Github repository.
Strategy Class#
Follow Along
Follow along in the demo repository: strategies/macd.py
Now we can write the strategy class SimpleMACD
, and
place it in a module (Python file) called macd.py
(hence MODULE: 'macd'
and CLASS: 'SimpleMACD'
in our strategy configuration). Although strategy
construction is extremely flexible, the class must contain an __init__
method, and a method named generate_signal
. The first of these methods is
called whenever the strategy is instantiated.
By default, strategies in AutoTrader are instantiated with three named arguments:
The name of the instrument being traded in this specific instance (
instrument
).The strategy parameters (
parameters
)The trading instruments data (
data
)
When backtesting, the data
provided is for the entire backtest period. This
is partly from older versions of AutoTrader, but it remains this way to allow
you to calculate all indicators for plotting purposes (as you will see below).
You shouldn’t use this data in your strategy, however, since you risk introducing
look-ahead.
Template Strategy#
We will start by filling out the template strategy shown below.
from autotrader.brokers.trading import Order
class Strategy:
def __init__(self, instrument, parameters, **kwargs):
"""Initialise the strategy."""
self.name = "Template Strategy"
self.instrument = instrument
self.params = parameters
# Construct indicators dict for plotting
self.indicators = {
"Indicator Name": {"type": "indicatortype", "data": "indicatordata"},
}
def generate_signal(self, data):
"""Define strategy logic to transform data into trading signals."""
# Initialise empty order list
orders = []
# The data passed into this method is the most up-to-date data.
# Example long market order:
long_market_order = Order(direction=1)
orders.append(long_market_order)
# Example short limit order:
short_limit = Order(direction=-1, order_type="limit", order_limit_price=1.0221)
orders.append(short_limit)
# Return any orders generated
# If no orders are generated, return an empty list [], an empty dict {},
# or a blank order Order().
return orders
Instantiation#
For the MACD crossover strategy, instantiation will look as shown below. Note the following:
we save the instrument and strategy parameters to the class attributes (using the
self
variable).the finta technical analysis package is used to calculate the 200-period EMA and the MACD. This calculation happens in a method called
generate_features
, defined later.the custom indicator
crossover
is used from the built-in indicators.an
indicators
dictionary is defined to tell AutoPlot which indicators to plot, and what to name them.
from finta import TA
from autotrader import Order, indicators
class SimpleMACD:
"""Simple MACD Strategy
Rules
------
1. Trade in direction of trend, as per 200EMA.
2. Entry signal on MACD cross below/above zero line.
3. Set stop loss at recent price swing.
4. Target 1.5 take profit.
"""
def __init__(self, parameters, data, instrument):
"""Define all indicators used in the strategy.
"""
self.name = "Simple MACD Trend Strategy"
self.params = parameters
self.instrument = instrument
# Initial feature generation (for plotting only)
self.generate_features(data)
# Construct indicators dict for plotting
self.indicators = {'MACD (12/26/9)': {'type': 'MACD',
'macd': self.MACD.MACD,
'signal': self.MACD.SIGNAL},
'EMA (200)': {'type': 'MA',
'data': self.ema}
}
Strategy Signals#
The next step is to define the signal generation function, generate_signal
.
Make sure to use this name for all your strategies, as AutoTrader will call
it when you run it. This method is where the logic of the strategy sits. It
gets passed some data using the data
argument, which you can use to generate
any trading signals. This data will always be the most up-to-date data, so you
should only create trading signals for the latest information it provides. In
most cases, this data will be
OHLC data, in the form of
a Pandas DataFrame.
Important
Strategies must contain a generate_signal
method! AutoTrader calls this method
to get trading signals from your strategy.
For the MACD strategy, this method translates the strategy rule into code.
AutoTrader supports multiple order types, multiple
stop loss types and anything else you might
encounter with your live-trade broker. To create a new order, we can use the
Order
object, imported from the
autotrader.trading
module. For this strategy, we will only
be placing market orders when we get the entry signal, which are the default
order type.
A long order can be created by specifying direction=1
when creating the
Order
, whereas a short order can be created by specifying direction=-1
.
If there is no trading signal this update, you can create an
empty order with just Order()
. We also define our exit targets
by the stop_loss
and take_profit
arguments. These price targets
come from our exit strategy, defined in the next section.
def generate_features(self, data):
"""Updates MACD indicators and saves them to the class attributes."""
# Save data for other functions
self.data = data
# 200EMA
self.ema = TA.EMA(self.data, self.params['ema_period'])
# MACD
self.MACD = TA.MACD(self.data, self.params['MACD_fast'],
self.params['MACD_slow'], self.params['MACD_smoothing'])
self.MACD_CO = indicators.crossover(self.MACD.MACD, self.MACD.SIGNAL)
self.MACD_CO_vals = indicators.cross_values(self.MACD.MACD,
self.MACD.SIGNAL,
self.MACD_CO)
# Price swings
self.swings = indicators.find_swings(self.data)
def generate_signal(self, data):
"""Define strategy to determine entry signals."""
# Feature calculation
self.generate_features(data)
# Look for entry signals (index -1 for the latest data)
if self.data.Close.values[-1] > self.ema[-1] and \
self.MACD_CO[-1] == 1 and \
self.MACD_CO_vals[-1] < 0:
# Long entry signal detected! Calculate SL and TP prices
stop, take = self.generate_exit_levels(signal=1)
new_order = Order(direction=1, stop_loss=stop, take_profit=take)
elif self.data.Close.values[-1] < self.ema[-1] and \
self.MACD_CO[-1] == -1 and \
self.MACD_CO_vals[-1] > 0:
# Short entry signal detected! Calculate SL and TP prices
stop, take = self.generate_exit_levels(signal=-1)
new_order = Order(direction=-1, stop_loss=stop, take_profit=take)
else:
# No trading signal, return a blank Order
new_order = Order()
return new_order
Exit Signals#
As with any good strategy, we must define an exit strategy to manage our risk.
In this strategy, stop losses are set at recent swings in price. Since this is a
trend following strategy, market structure tells us that price is unlikely to
break past a recent swing level, unless of course the trend is reversing. The
find_swings
indicator built into AutoTrader’s
indicator library makes this an easy task. As per our rules,
take profits are set at 1:1.5 risk-to-reward ratio. This is all completed with
the code below, which returns our target exit prices, used by our
signal generation method in the section above.
def generate_exit_levels(self, signal):
"""Function to determine stop loss and take profit prices."""
RR = self.params['RR']
if signal == 1:
# Long signal
stop = self.swings.Lows[-1]
take = self.data.Close[-1] + RR*(self.data.Close[-1] - stop)
else:
# Short signal
stop = self.swings.Highs[-1]
take = self.data.Close[-1] - RR*(stop - self.data.Close[-1])
return stop, take
Time to Trade#
You now have your very own algorithmic trading strategy ready to unleash on the world. In the next few pages, you will learn how to backtest, optimise and deploy your strategies. But first, a quick aside on data management.