2 Commits

Author SHA1 Message Date
vatsal 997e234a85 Merge pull request #12 from alphabench/feat/refining-metrics-for-portfolio
feat: add batch spread backtest with parallel execution, bump to 0.3.3
2026-02-25 15:21:29 +05:30
porcelaincode e049a2f968 feat: add batch spread backtest with parallel execution, bump to 0.3.3
Introduces PyBatchSpreadItem and batch_spread_backtest function for running multiple spread strategies in parallel using Rayon, enabling efficient multi-strategy backtesting workflows.
2026-02-25 15:19:42 +05:30
9 changed files with 236 additions and 4 deletions
Generated
+1 -1
View File
@@ -502,7 +502,7 @@ dependencies = [
[[package]]
name = "raptorbt"
version = "0.3.2"
version = "0.3.3"
dependencies = [
"approx",
"criterion",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "raptorbt"
version = "0.3.2"
version = "0.3.3"
edition = "2021"
description = "High-performance Rust backtesting engine with Python bindings. Drop-in VectorBT replacement with up insanely faster performance at fractional memory footprint."
authors = ["Alphabench <contact@alphabench.in>"]
+82
View File
@@ -80,6 +80,7 @@ RaptorBT was built to address the performance limitations of VectorBT. Benchmark
### Key Features
- **6 Strategy Types**: Single instrument, basket/collective, pairs trading, options, spreads, and multi-strategy
- **Batch Spread Backtesting**: Run multiple spread backtests in parallel via Rayon with GIL released
- **Monte Carlo Simulation**: Correlated multi-asset forward projection via GBM + Cholesky decomposition
- **33 Metrics**: Full parity with VectorBT including Sharpe, Sortino, Calmar, Omega, SQN, Payoff Ratio, Recovery Factor, and more
- **12 Technical Indicators**: SMA, EMA, RSI, MACD, Stochastic, ATR, Bollinger Bands, ADX, VWAP, Supertrend, Rolling Min, Rolling Max
@@ -404,6 +405,50 @@ result = raptorbt.run_multi_backtest(
- `weighted`: Weight signals by strategy weight
- `independent`: Run strategies independently (aggregate PnL)
### 6. Batch Spread Backtest
Run multiple spread backtests in parallel. Shared data (timestamps, underlying close) is converted once, then each item is backtested on its own Rayon thread with the GIL released for maximum throughput.
```python
import numpy as np
import raptorbt
config = raptorbt.PyBacktestConfig(initial_capital=100000, fees=0.001)
# Create batch items — one per strategy variation
items = [
raptorbt.PyBatchSpreadItem(
strategy_id="straddle_24000",
legs_premiums=[call_24000_premiums, put_24000_premiums],
leg_configs=[("CE", 24000.0, -1, 50), ("PE", 24000.0, -1, 50)],
entries=entries,
exits=exits,
spread_type="straddle",
max_loss=5000.0,
target_profit=3000.0,
),
raptorbt.PyBatchSpreadItem(
strategy_id="strangle_23500_24500",
legs_premiums=[call_24500_premiums, put_23500_premiums],
leg_configs=[("CE", 24500.0, -1, 50), ("PE", 23500.0, -1, 50)],
entries=entries,
exits=exits,
spread_type="strangle",
),
]
# Run all in parallel — returns list of (strategy_id, result) tuples
results = raptorbt.batch_spread_backtest(
timestamps=timestamps,
underlying_close=underlying_close,
items=items,
config=config,
)
for strategy_id, result in results:
print(f"{strategy_id}: {result.metrics.total_return_pct:.2f}%")
```
---
## Metrics
@@ -720,6 +765,34 @@ inst_config.set_fixed_target(0.05)
- `alloted_capital` - Per-instrument capital cap (capped at available cash).
- `existing_qty` / `avg_price` - Reserved for future live-to-backtest transitions.
### PyBatchSpreadItem
```python
item = raptorbt.PyBatchSpreadItem(
strategy_id: str, # Unique identifier for this backtest
legs_premiums: List[np.ndarray], # Premium series per leg
leg_configs: List[Tuple[str, float, int, int]], # (option_type, strike, quantity, lot_size)
entries: np.ndarray, # bool entry signals
exits: np.ndarray, # bool exit signals
spread_type: str = "custom", # Spread type string
max_loss: float = None, # Optional max loss exit
target_profit: float = None, # Optional target profit exit
)
```
### batch_spread_backtest
```python
results = raptorbt.batch_spread_backtest(
timestamps: np.ndarray, # int64 nanosecond timestamps (shared)
underlying_close: np.ndarray, # Underlying close prices (shared)
items: List[PyBatchSpreadItem], # List of spread backtest items
config: PyBacktestConfig = None, # Optional shared config
) -> List[Tuple[str, PyBacktestResult]] # (strategy_id, result) pairs
```
Runs all spread backtests in parallel via Rayon. Timestamps and underlying close are shared across all items and converted once. The GIL is released during execution for maximum Python concurrency.
### simulate_portfolio_mc
```python
@@ -924,6 +997,15 @@ MIT License - see [LICENSE](LICENSE) for details.
## Changelog
### v0.3.3
- Add `batch_spread_backtest` function for running multiple spread backtests in parallel via Rayon
- Add `PyBatchSpreadItem` class for defining individual items in a batch spread backtest
- Shared data (timestamps, underlying close) is converted once and reused across all items
- GIL released during parallel execution for maximum Python concurrency
- Each item carries its own `strategy_id`, leg configs, signals, spread type, and optional max loss / target profit
- Returns a list of `(strategy_id, PyBacktestResult)` tuples preserving result-to-input mapping
### v0.3.2
- Add `payoff_ratio` metric to `BacktestMetrics` — average winning trade return divided by average losing trade return (absolute), measures risk/reward per trade
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "raptorbt"
version = "0.3.2.post1"
version = "0.3.3"
description = "High-performance Rust backtesting engine with Python bindings. Drop-in VectorBT replacement with up insanely faster performance at fractional memory footprint."
readme = "README.md"
requires-python = ">=3.10"
+7 -1
View File
@@ -26,6 +26,9 @@ from raptorbt._raptorbt import (
run_pairs_backtest,
run_multi_backtest,
run_spread_backtest,
# Batch backtest
PyBatchSpreadItem,
batch_spread_backtest,
# Monte Carlo simulation
simulate_portfolio_mc,
# Indicator functions
@@ -43,7 +46,7 @@ from raptorbt._raptorbt import (
rolling_max,
)
__version__ = "0.3.2.post1"
__version__ = "0.3.3"
__all__ = [
# Config classes
@@ -62,6 +65,9 @@ __all__ = [
"run_pairs_backtest",
"run_multi_backtest",
"run_spread_backtest",
# Batch backtest
"PyBatchSpreadItem",
"batch_spread_backtest",
# Monte Carlo simulation
"simulate_portfolio_mc",
# Indicator functions
Binary file not shown.
Binary file not shown.
+4
View File
@@ -44,6 +44,10 @@ fn _raptorbt(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(python::bindings::run_multi_backtest, m)?)?;
m.add_function(wrap_pyfunction!(python::bindings::run_spread_backtest, m)?)?;
// Register batch spread backtest
m.add_class::<python::bindings::PyBatchSpreadItem>()?;
m.add_function(wrap_pyfunction!(python::bindings::batch_spread_backtest, m)?)?;
// Register Monte Carlo simulation
m.add_function(wrap_pyfunction!(python::bindings::simulate_portfolio_mc, m)?)?;
+140
View File
@@ -842,6 +842,146 @@ pub fn run_spread_backtest<'py>(
Ok(convert_result(result))
}
/// A single spread backtest item for batch execution.
#[pyclass]
#[derive(Clone)]
pub struct PyBatchSpreadItem {
#[pyo3(get, set)]
pub strategy_id: String,
pub legs_premiums: Vec<Vec<f64>>,
pub leg_configs: Vec<(String, f64, i32, usize)>,
pub entries: Vec<bool>,
pub exits: Vec<bool>,
#[pyo3(get, set)]
pub spread_type: String,
#[pyo3(get, set)]
pub max_loss: Option<f64>,
#[pyo3(get, set)]
pub target_profit: Option<f64>,
}
#[pymethods]
impl PyBatchSpreadItem {
#[new]
#[pyo3(signature = (strategy_id, legs_premiums, leg_configs, entries, exits,
spread_type="custom", max_loss=None, target_profit=None))]
fn new(
strategy_id: String,
legs_premiums: Vec<PyReadonlyArray1<f64>>,
leg_configs: Vec<(String, f64, i32, usize)>,
entries: PyReadonlyArray1<bool>,
exits: PyReadonlyArray1<bool>,
spread_type: &str,
max_loss: Option<f64>,
target_profit: Option<f64>,
) -> Self {
Self {
strategy_id,
legs_premiums: legs_premiums.into_iter().map(numpy_to_vec_f64).collect(),
leg_configs,
entries: numpy_to_vec_bool(entries),
exits: numpy_to_vec_bool(exits),
spread_type: spread_type.to_string(),
max_loss,
target_profit,
}
}
}
/// Run multiple spread backtests in parallel via Rayon.
///
/// Shared data (timestamps, underlying_close) is converted once, then each
/// item is backtested on its own Rayon thread with the GIL released.
///
/// Returns a Vec of (strategy_id, PyBacktestResult) tuples.
#[pyfunction]
#[pyo3(signature = (timestamps, underlying_close, items, config=None))]
pub fn batch_spread_backtest(
py: Python<'_>,
timestamps: PyReadonlyArray1<i64>,
underlying_close: PyReadonlyArray1<f64>,
items: Vec<PyBatchSpreadItem>,
config: Option<&PyBacktestConfig>,
) -> PyResult<Vec<(String, PyBacktestResult)>> {
use rayon::prelude::*;
// Convert shared data while holding GIL
let ts = numpy_to_vec_i64(timestamps);
let underlying = numpy_to_vec_f64(underlying_close);
let base_config = config.map(|c| BacktestConfig::from(c)).unwrap_or_default();
// Prepare each item into a self-contained struct for parallel execution
struct PreparedItem {
strategy_id: String,
premiums: Vec<Vec<f64>>,
entries: Vec<bool>,
exits: Vec<bool>,
spread_config: SpreadConfig,
}
let prepared: Vec<PreparedItem> = items
.into_iter()
.map(|item| {
let rust_leg_configs: Vec<LegConfig> = item
.leg_configs
.into_iter()
.map(|(opt_type, strike, quantity, lot_size)| {
let option_type =
SpreadOptionType::from_str(&opt_type).unwrap_or(SpreadOptionType::Call);
LegConfig::new(option_type, strike, quantity, lot_size)
})
.collect();
let spread_type_enum = match item.spread_type.to_lowercase().as_str() {
"straddle" => SpreadType::Straddle,
"strangle" => SpreadType::Strangle,
"vertical_call" | "verticalcall" => SpreadType::VerticalCall,
"vertical_put" | "verticalput" => SpreadType::VerticalPut,
"iron_condor" | "ironcondor" => SpreadType::IronCondor,
"iron_butterfly" | "ironbutterfly" => SpreadType::IronButterfly,
"butterfly_call" | "butterflycall" => SpreadType::ButterflyCall,
"butterfly_put" | "butterflyput" => SpreadType::ButterflyPut,
"calendar" => SpreadType::Calendar,
"diagonal" => SpreadType::Diagonal,
_ => SpreadType::Custom,
};
let spread_config = SpreadConfig {
base: base_config.clone(),
spread_type: spread_type_enum,
leg_configs: rust_leg_configs.clone(),
max_loss: item.max_loss,
target_profit: item.target_profit,
close_at_eod: false,
};
PreparedItem {
strategy_id: item.strategy_id,
premiums: item.legs_premiums,
entries: item.entries,
exits: item.exits,
spread_config,
}
})
.collect();
// Release GIL and run all backtests in parallel via Rayon
let results: Vec<(String, crate::core::types::BacktestResult)> = py.allow_threads(|| {
prepared
.into_par_iter()
.map(|item| {
let backtest = SpreadBacktest::new(item.spread_config);
let result =
backtest.run(&ts, &underlying, &item.premiums, &item.entries, &item.exits);
(item.strategy_id, result)
})
.collect()
});
// Re-acquire GIL and convert results to Python objects
Ok(results.into_iter().map(|(id, result)| (id, convert_result(result))).collect())
}
/// Run multi-strategy backtest.
#[pyfunction]
#[pyo3(signature = (timestamps, open, high, low, close, volume, strategies, config=None, combine_mode="any"))]