Skip to main content

FINSABER financial backtesting framework and data adapters.

Project description

FINSABER

Arxiv link PyPI Documentation License Stars

Official implementation for the KDD 2026 paper: "Can LLM-based Financial Investing Strategies Outperform the Market in Long Run?"

News

  • [2026] FINSABER has been upgraded to FINSABER-2. The backtesting framework is now package-oriented, supports the new parquet dataset format, and includes explicit execution timing, adjusted OHLC handling, slippage, liquidity caps, structured result artifacts, and LLM cost accounting.
  • [24/11/2025] We are excited to announce that FINSABER has been accepted to KDD 2026.
  • [19/06/2025] Code and initial benchmarks released.

Overview

FINSABER is a framework for evaluating financial trading strategies, including traditional technical analysis, machine learning methods, and LLM-based agents. FINSABER-2 focuses on a cleaner reusable backtesting package over price, news, filings, and extensible market data.

The packaged core is intentionally limited to reusable backtesting code: data loaders, execution models, metrics, result writers, selectors, and strategy interfaces. Paper-specific FinMem, FinAgent, FinCon, and FinRL integrations live outside the core package.

Install

For framework use:

pip install finsaber

For local development:

git clone https://github.com/waylonli/FINSABER
cd FINSABER
git checkout v2.0
conda activate trading
pip install -e ".[dev,research]"

Build the wheel:

python -m build --wheel

Documentation

Read the FINSABER-2 documentation at https://waylonli.github.io/FINSABER/.

Quick Start

Run a Buy-and-Hold backtest on the parquet dataset:

from finsaber import FINSABERBt, FinsaberParquetDataset
from finsaber.strategy.timing import BuyAndHoldStrategy

data = FinsaberParquetDataset(r"I:\Data\finsaber2\sp500_2000_2025_parquet")

config = {
    "data_loader": data,
    "tickers": ["AAPL"],
    "date_from": "2024-01-02",
    "date_to": "2024-01-10",
    "setup_name": "demo_buy_hold",
    "execution_timing": "next_open",
    "slippage_perc": 0.0005,
    "liquidity_cap_pct": 0.025,
    "save_results": True,
    "silence": True,
}

results = FINSABERBt(config).run_iterative_tickers(BuyAndHoldStrategy)
print(results["AAPL"]["total_return"])

Outputs are written under backtest/output/<setup>/<strategy>/ by default:

  • run_config.json: resolved run configuration.
  • run_manifest.json: artifact schema.
  • run_summary.csv: flat per-window/per-ticker summary.
  • metrics.json: scalar metrics.
  • equity_curve.csv, trades.csv, orders.csv, rejected_orders.csv, llm_costs.csv: detailed tables when available.

Data

FINSABER expects pluggable data loaders that implement TradingData. The built-in parquet loader reads this layout:

sp500_2000_2025_parquet/
  price_daily/year=YYYY/part-000.parquet
  news_items/year=YYYY/part-000.parquet
  filingk/year=YYYY/part-000.parquet
  filingq/year=YYYY/part-000.parquet

FinsaberParquetDataset computes split-adjusted adjusted_open, adjusted_high, and adjusted_low from raw OHLC and adjusted_close. Raw volume is retained for liquidity caps.

For small or custom datasets, FinsaberDataset accepts an in-memory dictionary:

from datetime import date
from finsaber import FinsaberDataset

data = {
    date(2024, 1, 2): {
        "price": {
            "DEMO": {
                "open": 100.0,
                "high": 102.0,
                "low": 99.0,
                "close": 101.0,
                "adjusted_close": 101.0,
                "volume": 1_000_000,
            }
        },
        "news": {"DEMO": ["optional news text"]},
        "filing_k": {},
        "filing_q": {},
    }
}

loader = FinsaberDataset(data=data)

A runnable example is available at examples/custom_dataset_example.py.

Implement Your Own Data Loader

Implement TradingData when your storage format is not a date-keyed dictionary.

from finsaber import TradingData

class MyData(TradingData):
    def __init__(self, store):
        self.store = store

    def get_data_by_date(self, date):
        return self.store.get(date, {})

    def get_ticker_price_by_date(self, ticker, date, price_field=None):
        bar = self.store[date]["price"][ticker]
        return bar[price_field or "adjusted_close"]

    def get_ticker_data_by_date(self, ticker, date):
        day = self.get_data_by_date(date)
        return {name: values[ticker] for name, values in day.items() if ticker in values}

    def get_tickers_list(self):
        tickers = set()
        for day in self.store.values():
            tickers.update(day.get("price", {}))
        return sorted(tickers)

    def get_subset_by_time_range(self, start_date, end_date):
        subset = {d: v for d, v in self.store.items() if start_date <= d <= end_date}
        return MyData(subset) if subset else None

    def get_ticker_subset_by_time_range(self, ticker, start_date, end_date):
        subset = {}
        for d, day in self.store.items():
            if start_date <= d <= end_date and ticker in day.get("price", {}):
                subset[d] = {"price": {ticker: day["price"][ticker]}}
        return MyData(subset) if subset else None

    def get_date_range(self):
        return sorted(self.store)

You can add extra modalities such as earnings calls, transcripts, analyst reports, or alternative data. Keep them under the daily dictionary, for example {"earnings_call": {"AAPL": "..."}}.

Implement a Backtrader Strategy

Backtrader strategies are used by FINSABERBt. Subclass BaseStrategy, implement next(), and call post_next_actions() each bar so equity is tracked.

import backtrader as bt
from finsaber.strategy.timing.base_strategy import BaseStrategy

class MovingAverageCross(BaseStrategy):
    params = (
        ("fast", 20),
        ("slow", 60),
        ("prior_period", 252),
    )

    def __init__(self):
        super().__init__()
        self.fast_ma = bt.indicators.SMA(self.data.close, period=self.params.fast)
        self.slow_ma = bt.indicators.SMA(self.data.close, period=self.params.slow)

    def next(self):
        if self.fast_ma[0] > self.slow_ma[0] and not self.position:
            size = self._adjust_size_for_commission(int(self.broker.cash / self.data.close[0]))
            if size > 0:
                self.buy(size=size)
        elif self.fast_ma[0] < self.slow_ma[0] and self.position:
            self.close()
        self.post_next_actions()

Run it:

results = FINSABERBt(config).run_iterative_tickers(MovingAverageCross)

Implement an LLM-Style Strategy

LLM-style strategies use the pure Python engine FINSABER. They receive date-level data and submit orders through the framework object. The default execution timing is next_open, which avoids same-day close look-ahead bias.

from finsaber import FINSABER
from finsaber.strategy.timing_llm import BaseStrategyIso

class RuleBasedAgent(BaseStrategyIso):
    def __init__(self, symbol):
        super().__init__()
        self.symbol = symbol

    def on_data(self, date, today_data, framework):
        bar = today_data["price"][self.symbol]
        news = today_data.get("news", {}).get(self.symbol, [])
        if news and "upgrade" in " ".join(news).lower():
            framework.buy(date, self.symbol, bar["adjusted_close"], -1)
        elif self.symbol in framework.portfolio:
            framework.sell(date, self.symbol, bar["adjusted_close"], -1)

config = {
    "data_loader": data,
    "tickers": ["AAPL"],
    "date_from": "2024-01-02",
    "date_to": "2024-03-01",
    "setup_name": "agent_demo",
    "save_results": True,
}

results = FINSABER(config).run_iterative_tickers(
    RuleBasedAgent,
    strat_params={"symbol": "$symbol"},
)

If your strategy calls an LLM, record cost through finsaber.toolkit.llm_cost_monitor. FINSABER can include LLM costs in total_trading_cost.

Execution Settings

Important TradeConfig fields:

{
    "execution_timing": "next_open",      # or "same_close"
    "commission_per_share": 0.0049,
    "min_commission": 0.99,
    "max_commission_rate": 0.01,
    "slippage_perc": 0.0005,
    "slippage_impact": 0.0,
    "liquidity_lookback_days": 20,
    "liquidity_min_history_days": 1,
    "liquidity_cap_pct": 0.025,
    "llm_cost_as_trade_cost": True,
}

Use adjusted OHLC for price simulation and raw volume for liquidity caps. Date-only news or filing data should be treated as available no earlier than the next trading decision unless you have timestamps.

Experiment Scripts

Research and paper-style launchers are examples, not part of the package wheel:

python examples/experiments/run_baselines_exp.py --setup selected_4 --include BuyAndHoldStrategy
python examples/experiments/run_llm_traders_exp.py --setup selected_4 --strategy FinMemStrategy --strat_config_path strats_configs/finmem_config_normal.json

FinMem, FinAgent, FinCon, and FinRL integrations remain in llm_traders/ and rl_traders/ for repository experiments.

Validation

Run tests:

python -m pytest -q tests

The local FINSABER-2 parquet validation report is generated from an ignored script under tmp/ and should not be committed. Current validation flags a small number of zero-price/OHLC issues, extreme adjustment factors, and duplicate filing accessions that should be filtered or corrected before large-scale production runs.

Citation

@misc{li2025llmbasedfinancialinvestingstrategies,
      title={Can LLM-based Financial Investing Strategies Outperform the Market in Long Run?},
      author={Weixian Waylon Li and Hyeonjun Kim and Mihai Cucuringu and Tiejun Ma},
      year={2025},
      eprint={2505.07078},
      archivePrefix={arXiv},
      primaryClass={q-fin.TR},
      url={https://arxiv.org/abs/2505.07078},
}

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

finsaber-2.0.0.tar.gz (81.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

finsaber-2.0.0-py3-none-any.whl (95.7 kB view details)

Uploaded Python 3

File details

Details for the file finsaber-2.0.0.tar.gz.

File metadata

  • Download URL: finsaber-2.0.0.tar.gz
  • Upload date:
  • Size: 81.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.15

File hashes

Hashes for finsaber-2.0.0.tar.gz
Algorithm Hash digest
SHA256 0a68c886744d765009b3f1da1191784a3e6ee561767111b8af34d9e2a236b5bd
MD5 c3c3d3f83e8288210dd70a0ca2a13e6f
BLAKE2b-256 575b19982e0f1195d9fc9fa869e42d4d719497882ca29dd0fba2d59ba4c02f51

See more details on using hashes here.

File details

Details for the file finsaber-2.0.0-py3-none-any.whl.

File metadata

  • Download URL: finsaber-2.0.0-py3-none-any.whl
  • Upload date:
  • Size: 95.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.15

File hashes

Hashes for finsaber-2.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 10f9118ad2754a450203294f19ce59a67d6a49a79f8c49c2c9e94435ac73470f
MD5 18f7a61cc674efaf3bde644a25f6680a
BLAKE2b-256 18382ed9b575ee870b35ebf7b28a0196b61761689507bd539269c9a07b2cab24

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page