Skip to content
← All articles
Article

Inside BatchBacktesting: parallel runs, explicit assumptions and useful outputs

Listen to the audio story

Inside a batch backtesting engine · 2:10 · AI-written script, synthetic voices

Transcript

Ava: Here's a small trap in data analysis: you take the best value of every column and call it one result. But that row might never have existed.

Andrew: Wait, how does that happen?

Ava: It comes from a Python project called BatchBacktesting, from the AlgoETS group. Antoine read its source and wrote up what the code really does. The trap is in the analysis helper: grouping by instrument and taking the max. Each column's maximum is picked on its own, so one row can mix return, drawdown and duration from different runs.

Andrew: So the fix is to pick a real row first.

Ava: Yes. Select the row by return, then talk about its drawdown. Antoine is clear that this is something he noticed reading code, not something he measured.

Andrew: What does the engine actually do?

Ava: Think of a small data pipeline. You give it a list of instruments and a strategy. For each one it fetches prices, cleans them, runs a backtest, and writes results. It uses a thread pool, and the single-strategy runner defaults to four workers.

Andrew: Is that a speed claim?

Ava: No, and he says so. Those are code defaults, not measured throughput. Same for the assumptions: a hundred thousand in starting cash, a zero point two percent commission, exclusive orders. They live in the constructor, so they should sit next to any comparison of returns.

Andrew: Nothing about slippage, then?

Ava: Right. Slippage, short availability, delisted stocks and corporate actions all need separate review. There's also an EMA strategy detail: it computes three moving averages, twenty, eighty and a hundred fifty, but entry decisions only compare the twenty and the eighty.

Andrew: What happens when things fail?

Ava: A failed instrument just returns nothing. If every instrument fails, the later step has no data to combine. And the multi-strategy helper nests thread pools inside a pool, so API rate limits deserve attention.

Andrew: The takeaway is to read the assumptions, not just the charts.

Ava: Exactly. Antoine didn't rerun any simulation or make new return claims. The full write-up has the flowchart and links to the source files.

A previously published batch-output chart from the related indicator article
Table of contents

A batch backtest is a small data pipeline: choose instruments, obtain prices, clean the observations, run a strategy, and save comparable results. AlgoETS/BatchBacktesting makes those steps visible in Python rather than hiding them behind a dashboard.

This image is an existing article illustration, not a new benchmark run.

1. Follow a single instrument first

In src/BatchBacktesting.py[1], process_instrument selects a stock or crypto price adapter, cleans the data, and constructs a Backtest. It attaches the instrument and strategy names to the result before returning it.

flowchart TD
  I["Instrument list and strategy"] --> P["ThreadPoolExecutor"]
  P --> D["Fetch and clean historical data"]
  D --> B["Backtest per instrument"]
  B --> R["Result with Instrument and Strategy"]
  R --> C["Per-instrument CSV"]
  R --> A["Aggregate strategy CSV"]
  B --> H["Optional HTML chart"]

The single-strategy runner defaults to four workers. Plot generation is optional. Those are code defaults, not measured throughput.

2. The assumptions are in the constructor

The inspected runner uses 100,000 starting cash, 0.002 commission (0.2%), and exclusive orders. These settings belong beside any comparison of returns.

They still do not cover every market cost or data issue. Slippage, availability of short positions, delisted securities, and corporate actions need separate review.

3. An EMA detail worth noticing

strategies.py[2] computes 20-, 80-, and 150-period EMAs for the EMA class. Its entry decisions compare the 20 and 80 curves; the 150-period curve is calculated but not used in those next conditions.

The long-side rule closes the prior position and buys with a stop at 90% of the current price and a target at 125%. This is the implemented experiment, not a claim that those parameters are optimal.

4. Keep comparable rows together

The runner writes per-instrument CSVs, an aggregate CSV per strategy, and optional HTML charts. The README links a published AAPL EMA chart covering 2018-04-04 through 2023-04-03. Those are that example’s dates, not a fixed range for every run.

Open the published AAPL example[3].

One analysis trap appears in analyse.py[4]: groupby("Instrument").max() takes each column’s maximum independently. The resulting row can combine metrics from different runs. Select a real row by return before narrating its drawdown and duration.

5. Know the failure boundaries

A failed instrument returns None. If all instruments fail, the later concatenation has no frames. The multi-strategy helper also creates an outer pool around inner pools, so API rate limits and nested concurrency deserve attention.

These are observations from reading the source. I did not rerun financial simulations for this article or generate new return claims.

References

  1. AlgoETS. (). src/BatchBacktesting.py. [Source file or documentation] github.com.
  2. AlgoETS. (). strategies.py. [Source file or documentation] github.com.
  3. Open the published AAPL example. (). [Web page] algoets.github.io.
  4. AlgoETS. (). analyse.py. [Source file or documentation] github.com.
  5. AlgoETS. (). BatchBacktesting. [Software repository] github.com.

References use this article’s publication date.