Backtest your Trading Systems with Python — Optimization
This story is the fourth of the backtrader series. You can find the other stories here: Improve your Trading with Python.
Currently, you know how to:
- Get data and add it to your backtests
- Configure backtesting parameters (initial cash, commissions, etc…)
- Develop a strategy and backtest it
- Plot the backtest
- Analyze the results of your backtests
An important thing is still missing. You don’t know how to optimize a strategy.
What is optimization
Imagine you have a strategy requiring parameters such as an Exponential Moving Average period or a Relative Strength Index threshold for example. You can manually input these parameters. But you can also try to find the best parameters for your dataset. This is called optimization. Your strategy will be backtested for 3 different EMA periods for example, and you will use the period giving the higher profit.
⚠️: over-optimization is not good. You don’t want to fit your strategy to the dataset you’re backtesting. A good thing to do is to keep things as simple as possible, with fewer parameters possible.
Before starting: some refactoring
Quick reminder: you can find this series repo here: Backtrader Series.
Let’s make our code a bit cleaner. We’ll begin by moving our strategy to another file: strategy.py . Just copy-paste all the strategy’s code (and don’t forget the imports).
Then, we’ll extract our backtesting code to write a function:
def backtest_strategy_non_optimized(strategy, **parameters):
btc_eur = yf.Ticker("BTC-EUR")
data = btc_eur.history(period="1y")
pandas_data = btfeeds.PandasData(dataname=data)
cerebro = bt.Cerebro()
cerebro.adddata(pandas_data)
cerebro.addstrategy(strategy, **parameters)
cerebro.broker.setcash(100_000)
cerebro.broker.setcommission(commission=0.001)
cerebro.broker.set_slippage_perc(0.001)
cerebro.addsizer(bt.sizers.FixedSize, stake=1)
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trade')
result = cerebro.run()
return result**parameters is used to pass keyword arguments to our functions, which correspond to our strategy’s parameters. Let’s try if everything works now:
result = backtest_strategy_non_optimized(MyStrategy, ema_period=10)Optimization
Now, we’ll see how to run multiple backtests easily to optimize our strategy. Let’s create another function:
def backtest_strategy_optimized(strategy, **parameters):A big part of the code is the same. There are just two lines changing. The first one:
cerebro.addstrategy(strategy, **parameters)It becomes:
cerebro.optstrategy(strategy, **parameters)addstrategy and optstrategy are basically the same functions. But when you want to run an optimized backtest, use optstrategy .
The second line changing is where we call cerebro.run() . We have to provide some arguments:
- maxcpus: the max number of processor’s cores to use to run the backtest. I always set it to 1. It’s an optional parameter, but sometimes the program won’t work if this parameter isn’t provided, so I set it to 1 by default even if I don’t need it.
- optreturn: a boolean used to change the return values of the backtest. If you don’t want an optimized return, provide this parameter with the
Falsevalue. By default, it is set toTrue. An optimized return is an object containing less data than a classic return (but enough for basic uses, so you can let it toTrueif you want.
result = cerebro.run(maxcpus=1, optreturn=True)Now, you have to tell your program how to optimize the parameters. To do it, you use iterators when you add your strategies. A backtest will be run for each value the iterator goes through. If you optimize several parameters, a backtest will be run for each possible combination of parameters. For example, we can run our new function this way:
result = backtest_strategy_optimized(MyStrategy, ema_period=range(5, 10))5 backtests will be run:
- ema_period = 5
- ema_period = 6
- …
- ema_period = 9
What about the result?
You can try to print the result:
print(result)
# [[<backtrader.cerebro.OptReturn object at 0x00000142FFB22400>],...It’s a list of results. Each one of the results corresponds to one backtest, so one combination of parameters.
So now, we can display the profit for each backtest:
for res in result:
strat_result = res[0]
analysis = strat_result.analyzers.trade.get_analysis()
total_net_pnl = analysis.pnl.net.total
print(f"Total Net PNL: {total_net_pnl}")# Total Net PNL: -7303.4707200877165
# Total Net PNL: -13232.36786234555
...Well, it’s not very intuitive. We don’t know which parameters correspond to which profit. Unfortunately, backtrader has some limitations, and some simple functions such as this are not implemented.
I implemented my own function to retrieve parameters of a strategy result, but it violates encapsulation. I haven’t found another way to write such a function…
def get_params_from_strategy_result(strategy_result):
params = strategy_result.params
return dict(params._getkwargs())I’ve just put it into a utils.py file.
Now, we can modify a bit our previous loop:
for res in result:
strat_result = res[0]
params = get_params_from_strategy_result(strat_result)
analysis = strat_result.analyzers.trade.get_analysis()
total_net_pnl = analysis.pnl.net.total
print(f"Parameters: {params} | Total Net PNL: {total_net_pnl}")# Parameters: {'ema_period': 5} | Total Net PNL: -7303.4707200877165
# Parameters: {'ema_period': 6} | Total Net PNL: -13232.36786234555
...It’s clearer now!
What if I want to see each strategy’s chart?
We can call cerebro.plot() and it will create a chart for each strategy. But it won’t work with optimized returns. So, to plot our strategies, we’ll modify the code a bit.
In backtest_strategy_optimized
result = cerebro.run(maxcpus=1, optreturn=False)
return result, cerebroThen, we can retrieve our cerebro:
result, cerebro = backtest_strategy_optimized(MyStrategy, ema_period=range(5, 10))And we can call cerebro.plot() anywhere in our code.
Final note
Now you have some tools to backtest and optimize your strategies. Some of them are still missing. That’s why we will see in the next stories how you can develop custom indicators, analyzers… or backtest multiple strategies at the same time.
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






