Build a Trading Bot with Python — 3. Strategies Templates
This is the third story of the “Build a Trading Bot” series. You need to know Backtrader to understand this story. If you don’t know what Backtrader is or if you want to find the other stories of this series, you should check the following story:
There’s a GitHub repo I’ve made for this series. If you want to use it to follow the code, you can find it here: Trading Bot Series.
What we will Get
At the end of the story, we will have two strategy templates:
- A base template: used to develop strategies with open and close conditions.
- A bracket template: used to develop strategies without close conditions. Instead, these strategies will use limit orders for stop-loss and take-profit.
How can we Develop Strategies Easily?
If you’ve followed the Backtrader series, you’ve seen how hard and tedious it can be to develop strategies. To minimize the work needed, the only things we want to modify from one strategy to another, are the conditions for entering/exiting a trader.
Here is an example of a strategy developed in 30 seconds thanks to a template:
class MyStrategy(BracketStrategy):
params = (
('ema_period', 20),
)
def _init_indicators(self):
self.ema = EMA(period=self.p.ema_period)
def _open_long_condition(self) -> bool:
if self.ema[0] < self.datas[0].close[0]:
return True
def _open_short_condition(self) -> bool:
if self.ema[0] > self.datas[0].close[0]:
return True
Obviously, this strategy doesn’t work, but the purpose is to understand how simple it is to develop this kind of strategy using just two conditions to enter trades.
All the stuff related to the execution of the strategy is delegated so that we only keep what we need to modify for each strategy.
BaseStrategy
Let’s start with the first template: a completely blank strategy.
We’ll first define some parameters that make sense for each strategy:
- logging: to enable/disable strategy’s logging.
- longs_enabled/shorts_enabled: to enable/disable long and short trades.
- stop loss/risk reward: 2 parameters to deal with risk management.
Let’s declare them with default values:
class BaseStrategy(bt.Strategy):
params = (
('logging', False),
('longs_enabled', True),
('shorts_enabled', True),
('stop_loss', 0),
('risk_reward', 0),
)
Now, what do we need to initialize in the __init__
method? Our empty orders list, some attributes for tracking our strategy’s profit, and our indicators:
def __init__(self):
self.orders_ref = list()
self.total_profit = 0
self.initial_cash = self.broker.cash
self._init_indicators()
The _init_indicators
method depends on the strategy, so we’ll leave it empty:
def _init_indicators(self):
pass
Now we can declare our functions used to enter/exit trades. These functions depend on the strategy, so we’ll leave them empty. To ensure proper functioning, we’ll make them abstract, so that in each strategy you have to override these functions:
from abc import abstractmethod
...
@abstractmethod
def _open_short_condition(self) -> bool:
pass
@abstractmethod
def _open_long_condition(self) -> bool:
pass
@abstractmethod
def _close_short_condition(self) -> bool:
pass
@abstractmethod
def _close_long_condition(self) -> bool:
pass
Then, we can make functions for logging. We’ll need one base log
function that we will reuse in each other logging function:
def _log(self, txt):
if self.p.logging:
print(f"{self.datas[0].datetime.datetime(0)}: {txt}")
Now we can make one function for each strategy-related action, such as:
- log_order: what to display when an order is opened/updated.
- log_trade: what to display when a trade is closed.
- log_start: what to display when we start our strategy.
- log_stop: what to display when we stop our strategy.
- log_total_profit: how total profit will be displayed for each iteration of our strategy.
- log_every_iter: what to display for each iteration.
- log_long_order: what to display when we go long.
- log_short_order: what to display when we go short.
I will give you my implementations of these functions, but you don’t need to do the same things as me, and you can even add other logging functions depending on your needs. Below are my implementations of these functions:
def _log_trade(self, trade):
self._log(colored('OPERATION PROFIT, GROSS %.2f, NET %.2f'
%(trade.pnl, trade.pnlcomm), 'green' if trade.pnl > 0 else 'red'))
def _log_total_profit(self):
self._log(colored('TOTAL PROFIT %.2f' % self.total_profit, 'green' if self.total_profit > 0 else 'red'))
def _log_order(self, order):
self._log('Order ref: {} / Type {} / Status {}'.format(
order.ref,
colored('BUY' if order.isbuy() else 'SELL', 'green' if order.isbuy() else 'red'),
order.getstatusname()
))
def _log_iter(self):
self._log(f"Close : {self.datas[0].close[0]}")
def _log_long_order(self, stop_price, take_profit_price):
self._log(colored(f"LONG ORDER SIGNAL: Stop loss: {stop_price} / Take profit: {take_profit_price}", 'green'))
def _log_short_order(self, stop_price, take_profit_price):
self._log(colored(f"SHORT ORDER SIGNAL: Stop loss: {stop_price} / Take profit: {take_profit_price}", 'red'))
def _log_start(self):
self._log(colored('Starting Portfolio Value: %.2f' % self.broker.getvalue(), 'green'))
def _log_stop(self):
portfolio_value = self.broker.getvalue()
total_return_pct = (portfolio_value - self.initial_cash) / self.initial_cash * 100
portfolio_value_color = 'green' if self.broker.getvalue() > self.initial_cash else 'red'
total_profit_color = 'green' if self.total_profit > 0 else 'red'
total_return_color = 'green' if total_return_pct > 0 else 'red'
self._log(colored('Final Portfolio Value: %.2f' % portfolio_value, portfolio_value_color))
self._log(colored(f"Total Return: {total_return_pct:.2f}%", total_return_color))
self._log_total_profit()
Note: colored
comes from the termcolor
package and is used to display colored text.
Nice, but when these functions will be called?
That’s why we need to make notifications functions. We’ve seen these through the Backtrader series, they are functions called when a specific action is done, such as when an order is updated, or when a trade is closed, etc…
Here are my implementations:
def notify_data(self, data, status, *args, **kwargs):
status = data._getstatusname(status)
print(status)
def notify_trade(self, trade):
if not trade.isclosed:
return
self._log_trade(trade)
self.total_profit += trade.pnlcomm
self._log_total_profit()
def notify_order(self, order):
self._log_order(order)
self._del_order_if_not_alive(order)
In notify_order
, we call del_order_if_not_alive
do delete useless orders, such as the stop loss order after the take profit was triggered.
def _del_order_if_not_alive(self, order):
if not order.alive() and order.ref in self.orders_ref:
self.orders_ref.remove(order.ref)
Then, we have to implement our next
method. This is the method called for each iteration of our strategy:
def next(self):
self._log_iter()
if not self.position:
self._not_yet_in_market()
else:
self._in_market()
In my implementation, I simply separate the cases based on whether I already have open orders or not.
Let’s begin with the case where we have open orders. We have to look if our close condition is respected, and if it is, we have to close our orders.
def _in_market(self):
if self._close_long_condition() and self.position.size > 0:
self.close()
if self._close_short_condition() and self.position.size < 0:
self.close()
Note: when we go long, self.position.size
is > 0, and when we go short, it is < 0. So, adding this condition ensures we don’t close a long side trade using the short condition for example.
When we are not yet in market, we have to check if our conditions are valid, and if they are, we have to calculate our orders prices and submit the orders.
def _not_yet_in_market(self):
if self._long_condition():
stop_price = self._get_long_stop_loss_price()
take_profit_price = self._get_long_take_profit_price()
self._go_long(stop_price, take_profit_price)
self._log_long_order(stop_price, take_profit_price)
if self._short_condition():
stop_price = self._get_short_stop_loss_price()
take_profit_price = self._get_short_take_profit_price()
self._go_short(stop_price, take_profit_price)
self._log_short_order(stop_price, take_profit_price)
def _long_condition(self):
return self._open_long_condition() and self.params.longs_enabled
def _short_condition(self):
return self._open_short_condition() and self.params.shorts_enabled
To calculate our stop loss and take profit prices, we use other functions. It’s useful to separate the calculation so that it allows us to change its logic. For example, instead of calculating the stop loss price using a percent of the current price, I can set the stop loss price to be equal to the last swing low.
In my default implementation, I’ve chosen to set these values to the current price, we’ll see why after.
def _get_long_stop_loss_price(self) -> float:
return self.datas[0].close[0]
def _get_long_take_profit_price(self) -> float:
return self.datas[0].close[0]
def _get_short_stop_loss_price(self) -> float:
return self.datas[0].close[0]
def _get_short_take_profit_price(self) -> float:
return self.datas[0].close[0]
Now, let’s implement go_long
and go_short
. These methods will submit the orders to the broker.
def _go_long(self, stop_price, take_profit_price):
orders = self._get_long_orders_from_stop_and_take_profit(stop_price, take_profit_price)
self.orders_ref = [order.ref for order in orders if order]
self.entry_bar = len(self)
We use another method to get our orders, then we update our orders list (if we don’t do this, we can’t cancel useless orders), and we keep track of the bar where we’ve entered a trade using an attribute.
Now, how can we submit our orders? It’s just some Backtrader code:
def _get_long_orders_from_stop_and_take_profit(self, stop_price, take_profit_price):
ACTUAL_PRICE = self.datas[0].close[0]
if stop_price != ACTUAL_PRICE and take_profit_price != ACTUAL_PRICE:
orders = self.buy_bracket(price=ACTUAL_PRICE, stopprice=stop_price, limitprice=take_profit_price)
elif stop_price != ACTUAL_PRICE and take_profit_price == ACTUAL_PRICE:
orders = [self.buy(), self.sell(exectype=bt.Order.Stop, price=stop_price)]
elif stop_price == ACTUAL_PRICE and take_profit_price != ACTUAL_PRICE:
orders = [self.buy(), self.sell(exectype=bt.Order.Limit, price=take_profit_price)]
else:
orders = [self.buy()]
return orders
Depending whether we have provided stop loss or/and take profit prices to the function, it only submits the needed orders to the broker.
For example, if in another strategy, we don’t specify any calculation for the stop loss, but we specify a calculation for the take profit, this function will return our market order + the take profit, but no stop loss.
These functions are nearly the same for the short side:
def _go_short(self, stop_price, take_profit_price):
orders = self._get_short_orders_from_stop_and_take_profit(stop_price, take_profit_price)
self.orders_ref = [order.ref for order in orders if order]
self.entry_bar = len(self)
def _get_short_orders_from_stop_and_take_profit(self, stop_price, take_profit_price):
ACTUAL_PRICE = self.datas[0].close[0]
if stop_price != ACTUAL_PRICE and take_profit_price != ACTUAL_PRICE:
orders = self.sell_bracket(price=ACTUAL_PRICE, stopprice=stop_price, limitprice=take_profit_price)
elif stop_price != ACTUAL_PRICE and take_profit_price == ACTUAL_PRICE:
orders = [self.sell(), self.buy(exectype=bt.Order.Stop, price=stop_price)]
elif stop_price == ACTUAL_PRICE and take_profit_price != ACTUAL_PRICE:
orders = [self.sell(), self.buy(exectype=bt.Order.Limit, price=take_profit_price)]
else:
orders = [self.sell()]
return orders
The last functions we need are just functions to say what to do when we start/stop using our strategy:
def start(self):
self._log_start()
def stop(self):
self._log_stop()
And that’s all, we have our first template! We’ll see now how we can use it to create another template!
BracketStrategy
A bracket strategy is a strategy that will submit 3 orders for each signal:
- A market order
- A stop loss
- A take profit
It’s my favorite type of strategy, and it can be easily implemented using our base template.
First, we need to change a little our conditions. Indeed, we don’t need close conditions because the trades will be closed using take profit or stop loss.
class BracketStrategy(BaseStrategy):
@abstractmethod
def _open_short_condition(self) -> bool:
pass
@abstractmethod
def _open_long_condition(self) -> bool:
pass
# In a bracket strat, closing is done with stop loss or take profit -> no need to implement close conditions
def _close_short_condition(self) -> bool:
pass
def _close_long_condition(self) -> bool:
pass
The last thing remaining is to change our take profit and stop loss calculations because we want to use these types of orders.
For this part, it’s just maths. If I submit a buy order at 1000€, and I want my stop loss to be 1% below, my stop loss price will be 1000€ — 1% of 1000€ = 990€. If my risk reward ratio = 2, my take profit will be at 1000€ + (1000€ — 990€) * 2 = 1020€. Here is how you turn this to code:
# Default calculation of stop price is using a stop loss in % and the actual market price
def _get_long_stop_loss_price(self):
return self.datas[0].close[0] * (1 - self.params.stop_loss / 100)
def _get_short_stop_loss_price(self):
return self.datas[0].close[0] * (1 + self.params.stop_loss / 100)
# For take profit, we use a risk reward parameter, and it's a trivial calculation
def _get_long_take_profit_price(self):
stop_price = self._get_long_stop_loss_price()
return self.datas[0].close[0] + (self.datas[0].close[0] - stop_price) * self.params.risk_reward
def _get_short_take_profit_price(self):
stop_price = self._get_short_stop_loss_price()
return self.datas[0].close[0] - (stop_price - self.datas[0].close[0]) * self.params.risk_reward
Note: as I used a risk_reward
parameter, the calculation is not the same as if the parameter was a take_profit
.
And we’re done with this template!
Strategy Example
For this part, I won’t explain much things, I will just copy-paste the code you can find on my GitHub. It’s commented, so you should understand easily.
from backtrader.indicators import MACD as MACD
from backtrader.indicators import CrossOver as CrossOver
from backtrader.indicators import ExponentialMovingAverage as EMA
from strategies.bracket_strategy import BracketStrategy
# This is a test strategy, to help you understand how to build one
class BracketStrategyExample(BracketStrategy):
# First, you define some parameters
params = (
('period_me1', 12),
('period_me2', 26),
('period_signal', 9),
('trend_ema_period', 100),
('movav', EMA),
)
# Then, you initialize indicators
def _init_indicators(self):
self.macd = MACD(period_me1=self.p.period_me1, period_me2=self.p.period_me2, period_signal=self.p.period_signal,
movav=self.p.movav)
self.ema = EMA(period=self.p.trend_ema_period)
self.cross = CrossOver(self.macd.macd, self.macd.signal)
# Finally, you implement your open conditions
# Closing is done with stop loss or take profit
def _open_long_condition(self) -> bool:
if self.ema[0] < self.datas[0].close[0] and self.cross[0] == 1:
return True
return False
def _open_short_condition(self) -> bool:
if self.ema[0] > self.datas[0].close[0] and self.cross[0] == -1:
return True
return False
Now, we can run this strategy in our bot. Here are the parameters I used, for example:
strategy = BracketStrategyExample
strategy_parameters = {
'period_me1': 12, 'logging': True, 'stop_loss': 1, 'risk_reward': range(1, 5)
}
Note: you need to provide stop_loss
and risk_reward
, else the trades won’t be closed.
It gives this kind of outputs:

Final Note
Now, you can develop complex strategies using just a few lines of code. That’s what makes this bot powerful, it’s really flexible and the bot is easy to use.
In the next story, we’ll see how to make the bot run in live. To don’t miss it, be sure to follow me!
To find the other stories of this series and more about mixing trading and Python, check this: Improve your Trading with Python
To explore more of my Python stories, click here!
If you liked the story, don’t forget to clap and maybe follow me if you want to explore more of my content :)
You can also subscribe to me via email to be notified every time I publish a new story, just click here!
If you’re not subscribed to Medium yet and wish to support me or get access to all my stories, you can use my link:
A Message from InsiderFinance

Thanks for being a part of our community! Before you go:
- 👏 Clap for the story and follow the author 👉
- 📰 View more content in the InsiderFinance Wire
- 📚 Take our FREE Masterclass
- 📈 Discover Powerful Trading Tools