API reference
Batch backtest screening
On this page6
finlab.backtest.sim_batch() backtests a set of positions in one call and returns CAGR, Sharpe ratio and maximum drawdown for each. It runs the same formal Cython engine as sim(): there is no Fast Mode, sampling or approximate return.
Screen several positions in one call
Pass a dict that maps a name to each position. Settings such as resample apply to every position in the call:
from finlab import data
from finlab.backtest import sim_batch
data.set_market('us')
with data.us_universe(index='SP500'):
close = data.get('price:adj_close')
positions = {
'momentum-3m': (close / close.shift(63)).is_largest(20),
'momentum-6m': (close / close.shift(126)).is_largest(20),
}
def on_progress(event):
if event['phase'] == 'item_result':
print(f"{event['completed']}/{event['total']} {event['strategy']}")
items = sim_batch(
positions,
resample='M',
metrics_only=True,
progress_callback=on_progress,
)
for item in items:
print(item['strategy'], item['status'], item['metrics'])You get back a list with one versioned, JSON-safe item per position, in the order you passed them:
| Field | What it holds |
|---|---|
strategy | The name you gave the position |
status | 'completed', or 'failed' if that position was invalid or its run failed |
metrics | cagr, sharpe_ratio and max_drawdown; empty for a failed item |
provenance | How the item was computed, including comparison_period_start and comparison_period_end |
error | None, or the failure's type and message |
An invalid or failing position does not cancel the others: each engine run is isolated. The optional progress_callback receives a dict per event; phase is 'batch_engine_start', 'running', 'item_result' (with the finished item) or 'batch_engine_complete'.
sim_batch() only computes metrics: metrics_only=False raises ValueError, and it never uploads reports or sends notifications.
Put only compatible strategies in one call
One call is one compatibility group. Market, resample and resample offset, trade price, fees and tax, position limit and all stop behavior are common keyword arguments, so strategies whose settings differ must go in separate calls.
In exchange, the batch does the shared work once:
- the package and data freshness check
- the market dataset prefetch
- the prepared market price inputs
- authentication
- the formal rebalance calendar, when positions have identical indexes
How the comparison window is chosen
Every completed item is measured over the same window on the shared market-price index, so differences in entry timing don't make the comparison unfair.
- Start: the latest first date among the positions.
- End: the earliest end among the strategies' individual
sim()equity curves. A curve ends on the last price bar while the strategy's latest signal is still live, otherwise on that signal's date. Sparse signals, such as monthly revenue or quarterly reports, therefore keep their last holding period, as insim().
Leading inactive dates are kept as flat returns, so entry timing does not silently shorten one strategy's sample. A single position that trades from its first date gets exactly the sim() metrics. The exact start and end are in each item's provenance as comparison_period_start and comparison_period_end.
If the valid positions share no price period at all, the batch raises ValueError; split those positions into separate comparison groups.
Backtest one position without a full report
sim(position, metrics_only=True, upload=False) is the matching formal path for a single position. It skips building the Report, but runs the same validation, market preparation, schedule, authorization and backtest engine. It returns the same Metrics type as report.metrics:
from finlab.backtest import sim
metrics = sim(position, metrics_only=True, upload=False)
print(metrics.annual_return(), metrics.sharpe_ratio(), metrics.max_drawdown())The batch's JSON items serialize this same formal subset as cagr, sharpe_ratio and max_drawdown.
How it was verified
scripts/benchmark_sim_batch.py runs a counterbalanced A/B test with 20 real positions, real FinLab data and the native Cython engine. On 2026-07-12 it reported:
| Path | 20-strategy elapsed (two orders) | Freshness checks per run | Market prefetches per run | Peak Python memory |
|---|---|---|---|---|
Individual sim(metrics_only=True) | 13.952 s, 11.096 s | 20 | 20 | 42.40 MB, 1.19 MB |
sim_batch(metrics_only=True) | 11.888 s, 11.844 s | 1 | 1 | 1.15 MB, 1.22 MB |
All 60 metric comparisons (CAGR, Sharpe ratio and maximum drawdown for 20 strategies) had an absolute error of 0.0, and neither path built a full Report. A separate comparison against real full Reports had a maximum absolute error of 4.440892098500626e-16, the expected floating-point rounding tolerance.
Run the benchmark yourself
Run it only in an authenticated native environment:
python scripts/benchmark_sim_batch.pyNext steps
- Backtesting Every
sim()setting, from rebalancing to stops and fees. - Strategy optimization Search parameter combinations for a strategy.
- finlab.backtest The full reference for
sim().
Was this page helpful?