Docs
Open Studio

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:

FieldWhat it holds
strategyThe name you gave the position
status'completed', or 'failed' if that position was invalid or its run failed
metricscagr, sharpe_ratio and max_drawdown; empty for a failed item
provenanceHow the item was computed, including comparison_period_start and comparison_period_end
errorNone, 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 in sim().

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:

Path20-strategy elapsed (two orders)Freshness checks per runMarket prefetches per runPeak Python memory
Individual sim(metrics_only=True)13.952 s, 11.096 s202042.40 MB, 1.19 MB
sim_batch(metrics_only=True)11.888 s, 11.844 s111.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.py

Next steps

↑↓ Move Enter Open Esc Close