Skip to content

Latest commit

 

History

History
386 lines (293 loc) · 11.9 KB

File metadata and controls

386 lines (293 loc) · 11.9 KB

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.