Skip to main content

GeneralBacktest

PyPI version Python 3.8+ License: MIT

GeneralBacktest is a flexible and efficient quantitative strategy backtesting framework designed for multi-asset portfolio strategies. Its core workflow is:

weight -> trades -> NAV

It supports arbitrary rebalancing schedules, realistic trading details, vectorized performance, and rich analytics.

Documentation Languages

Key Features

  • Flexible rebalancing: rebalance on any dates, not limited to fixed intervals.
  • High performance: vectorized implementation for large universes and long histories.
  • Realistic trading simulation:
    • Rebalance threshold control (avoid tiny adjustments)
    • Separate buy/sell transaction cost
    • Slippage modeling
    • Rebalance-day PnL split (hold/sell/buy)
  • Dynamic total exposure control via position_ratio_col (v1.1.0).
  • Cash-based backtest mode via run_backtest_with_cash (v1.1.0).
  • Enhanced visualization (log-scale NAV and dual-scale NAV, v1.1.0).
  • T+0 (intraday round-trip) backtesting via TBacktest.run_t0_backtest() (v1.2.0).
  • 15+ performance metrics and 10+ plotting utilities.

Installation

Basic

pip install GeneralBacktest

With Database Support

If you need run_backtest_ETF() or run_backtest_stock():

pip install GeneralBacktest[database]

All Optional Dependencies

pip install GeneralBacktest[full]

Dependencies

  • Required: numpy, pandas, matplotlib
  • Optional:
    • quantchdb for database-backed ETF/stock data
    • openpyxl for Excel export

Quick Start

Basic Backtest with Local Data

from GeneralBacktest import GeneralBacktest
import pandas as pd

weights_data = pd.DataFrame({
    "date": ["2023-01-01", "2023-01-01", "2023-06-01", "2023-06-01"],
    "code": ["stock_A", "stock_B", "stock_A", "stock_B"],
    "weight": [0.6, 0.4, 0.3, 0.7]
})

price_data = pd.DataFrame({
    "date": pd.date_range("2023-01-01", "2023-12-31", freq="D"),
    "code": "stock_A",
    "open": [...],
    "close": [...],
    "adj_factor": [...]
})

bt = GeneralBacktest(start_date="2023-01-01", end_date="2023-12-31")

results = bt.run_backtest(
    weights_data=weights_data,
    price_data=price_data,
    buy_price="open",
    sell_price="close",
    adj_factor_col="adj_factor",
    close_price_col="close",
    rebalance_threshold=0.005,
    transaction_cost=[0.001, 0.001],
    slippage=0.0005,
    initial_capital=1.0
)

bt.print_metrics()
bt.plot_all()
bt.plot_nav_curve()
bt.plot_monthly_returns()

Advanced Features

Dynamic Total Exposure (position_ratio_col)

Set total invested ratio per rebalance date (e.g., 80% invested, 20% cash):

weights_data = pd.DataFrame({
    "date": ["2023-01-01", "2023-01-01", "2023-06-01", "2023-06-01"],
    "code": ["stock_A", "stock_B", "stock_A", "stock_B"],
    "weight": [0.6, 0.4, 0.3, 0.7],
    "position_ratio": [0.8, 0.8, 0.9, 0.9]
})

results = bt.run_backtest(
    weights_data=weights_data,
    price_data=price_data,
    position_ratio_col="position_ratio"
)

Cash Backtest (run_backtest_with_cash)

Use actual capital and lot-size constraints for execution-accurate simulation:

results = bt.run_backtest_with_cash(
    weights_data=weights_data,
    price_data=price_data,
    initial_capital=1_000_000,
    buy_price="open",
    sell_price="close",
    close_price_col="close",
    lot_size=100,
    trade_critic="weight_desc",
    transaction_cost=[0.001, 0.001],
    slippage=0.001
)

print(f"Final NAV: {results['nav_series'].iloc[-1]:,.2f}")
print(f"Final Cash: {results['cash_series'].iloc[-1]:,.2f}")
print(f"Cash Ratio: {results['metrics']['Cash Ratio']:.2%}")

Tradable Volume Constraint (v1.2.0)

Pass a volume_data DataFrame to add a strict tradable-volume upper bound per (date, code). This decouples the framework from any specific intraday model: the user computes tradable_shares externally (e.g., from minute data, VWAP participation, order-book depth) and simply hands the result to the backtester.

# volume_data columns: date, code, tradable_shares
# (any (date, code) not present -> STRICTLY 0, i.e. not tradable that day)
results = bt.run_backtest_with_cash(
    weights_data=weights_data,
    price_data=price_data,
    initial_capital=1_000_000,
    buy_price="open",
    sell_price="close",
    close_price_col="close",
    lot_size=100,
    volume_data=volume_df,
    volume_col="tradable_shares",   # or use buy_volume_col/sell_volume_col to split
)

m = results['metrics']
print(f"Avg Fill Ratio:        {m['平均订单填充率']:.2%}")
print(f"Volume-Constrained %:  {m['量约束订单占比']:.2%}")
print(f"Total Orders:          {m['订单总数']}")

Trade records add intended_shares and constraint_hit columns ('none' | 'cash' | 'volume').

Execution Order & Dual-Track Backtest (v1.3.0)

When the buy slot is earlier than the sell slot (e.g. buy_price = VWAP around 10:00 while sell_price = VWAP around 14:50), the classic "sell-first" execution implicitly uses same-day sell proceeds that have not yet physically occurred to fund the morning buy — violating the true A-share T+1 timing. v1.3.0 introduces the execution_order parameter plus a dual-track backtest to model this correctly.

# Dual-track: split initial_capital into two logical cash pools A / B
# Day T   : A buys signal_T at 10:00 with cash_A ; B sells signal_{T-1} at 14:50
# Day T+1 : B buys signal_{T+1} with cash_B ; A sells signal_T
# Each track holds for exactly 1 day -> naturally T+1 compliant
results = bt.run_backtest_with_cash(
    weights_data=weights_data,
    price_data=price_data,
    initial_capital=1_000_000,
    buy_price="vwap_1000",           # 10:00 VWAP
    sell_price="vwap_1450",          # 14:50 VWAP
    close_price_col="close",
    execution_order="buy_first",     # enable dual-track
    dual_track_config={
        "imbalance_threshold": 0.10, # rebalance if |cap_A-cap_B|/total > 0.10
        "rebalance_gain": 0.5,       # first-order convergence gain
        "initial_split": 0.5,        # initial cash_A / total
        "first_buy_track": "A",      # Day 0 first-buy track
    },
)

# Extra fields on the result dict
results["track_a"]              # {'nav_series', 'cash_series'}
results["track_b"]
results["imbalance_series"]     # per-day imbalance record
results["rebalance_events"]     # cash rebalancing events

m = results["metrics"]
print(f"Max Imbalance:      {m['最大不平衡度']:.2%}")
print(f"Rebalance Count:    {m['再平衡次数']}")

trade_records / daily_positions gain a track column ('A' / 'B') when running in dual-track mode. If execution_order is left unspecified, behavior is identical to v1.2.x.

ETF Database Backtest (Requires DB Config)

run_backtest_ETF() and run_backtest_stock() need a valid database config. For general usage, run_backtest() is recommended.

T+0 Intraday Round-Trip (TBacktest)

TBacktest supports same-day sell → buy cycles (T+0 strategies). weight is the target position, not the trading amount. The phase column controls intraday execution order:

from GeneralBacktest import TBacktest

tb = TBacktest(start_date='2024-01-01', end_date='2024-12-31')

results = tb.run_t0_backtest(
    weights_data=t0_weights,   # includes 'phase' column
    price_data=price_data,
    buy_price='close',        # configurable
    sell_price='open',        # configurable
    adj_factor_col='adj_factor',
    close_price_col='close',
    transaction_cost=[0.001, 0.001]
)

# T+0 dedicated visualization
tb.plot_intraday_trades()          # NAV + intraday trade markers
tb.plot_t0_returns_breakdown()     # sell vs buy return decomposition
tb.plot_nav_vs_benchmark()         # strategy vs benchmark comparison

Weights data format with phase column:

date code weight phase
2024-01-02 stock_A 1.0 NaN
2024-01-03 stock_A 0.5 sell
2024-01-03 stock_A 1.0 buy

A-share compliance is enforced:

  • buy_phase must follow sell_phase in time order
  • Net buy on each day: total sell ≤ total buy (no naked shorting)
  • Target positions capped at [0, 1]

T+0-specific metrics: sell win rate, buy win rate, sell/buy cumulative return, return contribution ratio, average commission rate.

Core Concepts

Total Exposure Control

When position_ratio_col is provided:

  1. Normalize per-date asset weights so they sum to 1.
  2. Multiply normalized weights by position_ratio of that date.
  3. Final sum of target weights equals position_ratio; the remaining part is cash.

Example:

  • Raw weights: A=0.6, B=0.4
  • position_ratio = 0.8
  • Final weights: A=0.48, B=0.32
  • Cash = 0.2

Standard vs Cash Backtest

Item run_backtest() run_backtest_with_cash()
Capital unit Relative weight (0-1) Absolute amount
Position tracking Weights Share quantity
Lot size constraint No Yes (lot_size)
Cash constraint No Yes
Adj factor Required for adjusted return Not required

Metrics

The framework includes return, risk, risk-adjusted, tail-risk, relative, and turnover metrics. Cash-specific metrics in run_backtest_with_cash() include:

  • Final Cash
  • Cash Ratio
  • Avg Cash Ratio

Visualization

bt.plot_all()
bt.plot_nav_curve()                    # linear scale
bt.plot_nav_curve(log_scale=True)      # log scale (v1.1.0)
bt.plot_nav_curve_dual()               # dual linear/log (v1.1.0)
bt.plot_nav_vs_benchmark()             # strategy vs benchmark
bt.plot_excess_returns()               # excess return analysis
bt.plot_monthly_returns()              # monthly heatmap
bt.plot_turnover()                     # turnover analysis
bt.plot_position_heatmap()             # holdings heatmap
bt.plot_return_distribution()          # return distribution

# T+0 specific (TBacktest)
tb.plot_intraday_trades()             # NAV + trade markers
tb.plot_t0_returns_breakdown()        # sell vs buy decomposition

API Overview

Constructor

GeneralBacktest(start_date: str, end_date: str)

Main Methods

  • run_backtest(...) — standard weight-based backtest
  • run_backtest_ETF(...) — ETF data from database
  • run_backtest_stock(...) — stock data from database
  • run_backtest_with_cash(...) — cash-constrained execution
  • TBacktest.run_t0_backtest(...) — T+0 intraday round-trip

Plotting and Reporting

  • print_metrics()
  • plot_all()
  • plot_nav_curve(log_scale=False)
  • plot_nav_curve_dual(...)
  • plot_comparison()
  • plot_excess_returns()
  • plot_monthly_returns()
  • plot_turnover()
  • plot_positions()
  • plot_return_distribution()

Backward Compatibility

All v1.1.0 and v1.2.0 features are incremental and backward compatible:

  • position_ratio_col in run_backtest() defaults to None.
  • log_scale in plot_nav_curve() defaults to False.
  • run_backtest_with_cash() and plot_nav_curve_dual() are new v1.1.0 methods.
  • TBacktest is a new v1.2.0 class — it does not affect existing GeneralBacktest usage.

Contributing

Issues and pull requests are welcome.

License

MIT License. See LICENSE.

Author

Elen Young - yang13515360252@163.com

Links

Disclaimer

This framework is for research and educational purposes only and does not constitute investment advice.

Metadata

Release files for GeneralBacktest 1.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for GeneralBacktest 1.3.0
File Size Uploaded
generalbacktest-1.3.0.tar.gz 54.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for GeneralBacktest 1.3.0
File Interpreter ABI Platform
generalbacktest-1.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 104.7 kB

Release files / generalbacktest-1.3.0.tar.gz

Download URL generalbacktest-1.3.0.tar.gz
Size 54.2 kB
Tags Source
SHA-256 checksum
How to use checksums
02f9a6d64265722e21deb4944295cc9cf50278433126f16ca8a630ad9b9ebed1
BLAKE2b-256 checksum
How to use checksums
9f22240c7a849a4e1b93e5a209e1d1ad38d04391a89cfceaca2443ef2ade9b1c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.0

Release files / generalbacktest-1.3.0-py3-none-any.whl

Download URL generalbacktest-1.3.0-py3-none-any.whl
Size 50.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2be9e09664f81cdcde841c0387d5b74b754c81b78c1c38ee922fda3231115049
BLAKE2b-256 checksum
How to use checksums
e9ba741d2f5ef3577bd65a24fb430e4ff682ee34db590ed4b62191827cd68fb6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.0

Release history Release notifications | RSS feed

1.4.0

2 release files

This release

1.3.0 This release

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page