FinLab 2.0.0
Release 2.0.0 - measured on this build

Research faster. Ship safer. Trade with cleaner primitives.

FinLab 2.0.0 turns strategy research into a faster feedback loop: rebalanced strategies can compute only the dates they need, datasets can load together, factor tools are richer, backtest internals are safer, and wheels are ready for desktop, Linux, Windows, macOS, and Pyodide.

3.5xlazy strategy expression speedup on the measured wide-data benchmark
2.0xreal `data.gets()` benchmark versus two sequential `data.get()` calls
102+new tests covering accessors, lazy paths, batch fetch, and release fixes
26compiled wheels downloaded for Python and Pyodide targets
Less work

Add `.lazy()`. Keep the strategy shape.

For monthly rebalancing, the engine does not need to calculate every daily intermediate result. Add `.lazy()` to the source dataframe and keep the rest of the strategy readable.

Beforecalculate every day
from finlab import data
from finlab.backtest import sim

close = data.get("price:收盤價")

position = (
    close.rank(axis=1, pct=True)
    [close.average(20) > close.average(60)]
    .is_largest(20)
)

report = sim(position, resample="M", upload=False)
Afteronly add `.lazy()`
from finlab import data
from finlab.backtest import sim

close = data.get("price:收盤價").lazy()

position = (
    close.rank(axis=1, pct=True)
    [close.average(20) > close.average(60)]
    .is_largest(20)
)

report = sim(position, resample="M", upload=False)
Benchmark
3.5xperformance improvement

Test-style synthetic benchmark: 2,500 business days x 1,200 symbols, rolling 20/60 averages, percentile rank, boolean mask, top 20, monthly rebalance. Median of 5 runs on this local checkout.

Full daily calc
1.0x
Rebalance-only calc
3.5x

3.5x faster. Results were checked to match; only the dataframe container type is different.

Execution plan

Compute where the portfolio changes

Rebalanced strategies do not need every intermediate daily row. The same expression is evaluated at target rebalance dates, while preserving familiar operations: rolling, shift, rank, masks, arithmetic, and top-N selection.

Source rows
2500
Monthly rows
115
Parallel data

`data.gets()` makes multi-dataset loading feel built-in.

2.0.0 adds a batch fetch path with one metadata request, parallel dataset materialization, and pip-style progress bars in terminal or HTML progress in notebooks.

Beforesequential requests
close = data.get("price:收盤價")
volume = data.get("price:成交股數")
revenue = data.get("monthly_revenue:當月營收")
Afterparallel batch fetch
close, volume, revenue = data.gets(
    "price:收盤價",
    "price:成交股數",
    "monthly_revenue:當月營收",
)
Fetching datasets (2/2)
security_categories
111.4 kB
security_industry_themes
98.4 kB
Real fetch benchmark
2.0xperformance improvement

Measured with the integration-test datasets `security_categories` and `security_industry_themes`, `force_download=True`, isolated in-memory storage, median of 3 runs.

One by one
1.0x
Together
2.0x

2.0x faster on this measured path. Larger independent downloads can benefit more from fetching together.

Research toolkit

More factor language, fewer custom loops.

The dataframe API now covers more of the daily quant workflow: cross-sectional transforms, sector-aware transforms, and portfolio weight construction live next to the data they operate on.

df.cs

Cross-sectional transforms

`rank()`, `winsorize()`, `bucket()`, `zscore()`, and `demean()` for every date across the stock universe.

Example: turn a raw factor into a clean cross-sectional score before selecting names.

score = (
    pe.cs.winsorize(0.02, 0.98)
    .cs.zscore()
    .cs.rank()
)

signal = score.is_smallest(20)
df.sector

Industry-aware signals

Aggregate and transform within sectors using `mean`, `std`, `median`, `sum`, `min`, `max`, `count`, and sector ranks.

Example: rank profitability within each industry, then keep only sector leaders.

quality = (
    roe.sector.winsorize(0.05, 0.95)
    .sector.zscore()
    .sector.rank()
)

signal = quality > 0.8
df.weight

Portfolio construction

Cap industries, clip by liquidity, inverse-vol weight, risk parity, target volatility, turnover limits, and drawdown control.

Example: compose sizing, risk, liquidity, and turnover controls in one readable chain.

weights = (
    signal.is_largest(20)
    .weight.inverse_volatility(60)
    .weight.cap_industry(0.30)
    .weight.clip_by_volume(1e8)
    .weight.limit_turnover(0.20)
)
Engineering

The release is faster, but it is also safer.

2.0.0 puts production-facing pieces behind clearer contracts: structured exceptions, safer parsers, a staged backtest pipeline, isolated data context, stronger lint rules, and CI coverage gates.

Backtest stages`sim()` is split into validation, price prep, normalization, execution, and report-building stages.
DataContextThirteen module globals are folded into one object with `override()` for controlled temporary state.
No eval`optimize.combinations` now uses an AST-based safe condition parser instead of arbitrary code execution.
Structured errors`finlab.exceptions` adds explicit error classes for data, backtest, broker, auth, portfolio, and config failures.
Release confidence

Validated for the 2.0.0 package build.

The package was validated locally, merged to `main`, merged to `stable`, and compiled through GitHub Actions. Wheels were downloaded into `wheelhouse/` for release handling.

Local validation

1073unit tests passed
436integration tests passed
2Pyodide scripts passed

Ruff passed on edited files and pre-commit hooks. Integration path: `uv run pytest -m "not unit and not real_order"`.

Compiled wheels

`wheelhouse/` contains 26 wheels for CPython 3.9-3.14 across macOS universal2, Linux x86_64, Linux aarch64, Windows amd64, plus Pyodide wasm32 wheels.

FinLab 2.0.0 is a release for faster iteration.

Keep your strategy readable, fetch the data together, and let the engine do less unnecessary work.

See the `.lazy()` change