A high-performance portfolio backtesting system implemented in Rust with Python bindings.
- Fast backtesting engine written in Rust for optimal performance
- Python bindings using PyO3 for easy integration
- Polars DataFrame support for efficient data handling
- Date-based rebalancing with irregular rebalancing interval support
- Long and short positions with support for leveraged portfolios
- Transaction cost modeling:
- Slippage costs in basis points (e.g., 1.0 = 0.01%)
- Commission models (IBKR Pro Fixed pricing)
- Comprehensive metrics including returns, volatility, Sharpe ratio, drawdowns, turnover, and trading costs
- Multiple output DataFrames for results, positions, weights, and metrics
Install the latest release from PyPI. This will download a pre-compiled binary wheel for your platform, avoiding the need to compile Rust.
pip install hawk_backtesterSupported Platforms:
- Linux (x86_64, aarch64)
- macOS (x86_64, Apple Silicon)
- Windows (x64)
- Python 3.8+
If you are on an unsupported platform or want to modify the code, see CONTRIBUTING.md for build instructions.
import polars as pl
from hawk_backtester import HawkBacktester
# Load your price data with a timestamp column (YYYY-MM-DD format)
# and columns for each asset's price
prices_df = pl.read_csv("data/prices.csv")
# Load your weight data with a timestamp column (YYYY-MM-DD format)
# and columns for each asset's weight
weights_df = pl.read_csv("data/weights.csv")
# Recommended data cleaning process
# For prices: forward fill first to avoid look-ahead bias, then backfill missing data
prices_df = prices_df.fill_null(strategy="forward")
prices_df = prices_df.fill_null(strategy="backward")
# For weights: drop null values or fill with 0.0, depending on desired behavior
weights_df = weights_df.drop_nulls()
# Initialize backtester with optional slippage costs (in basis points)
# Note: Add fee_model="ibkr_pro_fixed" to include commission costs (see example below)
backtester = HawkBacktester(initial_value=1_000_000, slippage_bps=1.0)
results = backtester.run(prices_df, weights_df)
# Access the result DataFrames
results_df = results["backtest_results"] # Performance metrics over time
metrics_df = results["backtest_metrics"] # Summary statistics
positions_df = results["backtest_positions"] # Dollar allocations per asset
weights_df = results["backtest_weights"] # Portfolio weights per asset# Initialize backtester with slippage AND commission costs
backtester = HawkBacktester(
initial_value=1_000_000,
slippage_bps=1.0,
fee_model="ibkr_pro_fixed" # IBKR Pro Fixed pricing model
)
results = backtester.run(prices_df, weights_df)
# Extract metrics to compare costs
def get_metric(metrics_df, metric_name):
return metrics_df.filter(pl.col("metric") == metric_name)["value"][0]
total_return = get_metric(results["backtest_metrics"], "total_return")
slippage_cost = get_metric(results["backtest_metrics"], "cumulative_slippage_cost")
commission_cost = get_metric(results["backtest_metrics"], "cumulative_commission_cost")
print(f"Total Return: {total_return:.2%}")
print(f"Slippage Cost: ${slippage_cost:,.2f}")
print(f"Commission Cost: ${commission_cost:,.2f}")The price DataFrame should have the following structure:
- A
datecolumn with dates in YYYY-MM-DD format (e.g., "2023-01-01") - One column per asset with the price at that timestamp
Example:
date,AAPL,MSFT,GOOG,AMZN
2023-01-01,150.00,250.00,2000.00,100.00
2023-01-02,152.50,255.00,2020.00,102.00
...
The weight DataFrame should have the following structure:
- A
datecolumn with dates in YYYY-MM-DD format (e.g., "2023-01-01") - One column per asset with the target weight at that timestamp
- Weights represent the fraction of portfolio value to allocate to each asset
- Leverage support: Weights can exceed [-1.0, 1.0] range for leveraged positions
- Short positions: Use negative weights (e.g., -0.3 for a 30% short position)
- Any unallocated portion (1.0 - sum of weights) is held as cash
Example:
date,AAPL,MSFT,GOOG,AMZN
2023-01-04,0.30,0.30,0.20,0.10
2023-01-05,0.25,0.35,0.20,0.15
...
Note: Both DataFrames must use the same date format (YYYY-MM-DD) and column name (date) for consistency.
The backtester returns a dictionary containing four DataFrames:
Performance metrics over time with the following columns:
date: Timestamp in YYYY-MM-DD formatportfolio_value: Total portfolio valuedaily_return: Daily arithmetic returndaily_log_return: Daily logarithmic returncumulative_return: Cumulative arithmetic return since inceptioncumulative_log_return: Cumulative logarithmic return since inceptiondrawdown: Current drawdown from peakvolume_traded: Absolute dollar volume traded on rebalance daysdaily_slippage_cost: Slippage cost incurred each daydaily_commission_cost: Commission cost incurred each day
Summary statistics in metric-value format:
- Performance:
total_return,annualized_return,annualized_volatility,sharpe_ratio,sortino_ratio - Risk:
max_drawdown,avg_drawdown,avg_daily_return,win_rate - Trading:
num_trades,cumulative_volume_traded,portfolio_turnover,holding_period_years - Costs:
cumulative_slippage_cost,cumulative_commission_cost - Simulation stats:
num_price_points,num_weight_events, timing metrics
Dollar allocations over time:
date: Timestamp- One column per asset with dollar value allocated
cash: Cash balance (can be negative with leverage)
Portfolio weights over time:
date: Timestamp- One column per asset with percentage weight
cash: Cash weight
The ibkr_pro_fixed fee model implements Interactive Brokers Pro Fixed pricing for U.S. stocks/ETFs:
- Cost per share: $0.005 per share
- Minimum per order: $1.00
- Maximum per order: 1% of trade value
This model is suitable for simulating realistic trading costs for retail and small institutional traders.
This project is licensed under the Apache 2.0 License - see the LICENSE file for details.