diff --git a/Cargo.lock b/Cargo.lock index d742e0d..f19c1f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -502,7 +502,7 @@ dependencies = [ [[package]] name = "raptorbt" -version = "0.3.2" +version = "0.3.3" dependencies = [ "approx", "criterion", diff --git a/Cargo.toml b/Cargo.toml index a1bd8d3..f202907 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 "] diff --git a/README.md b/README.md index a275255..8a87c3c 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/pyproject.toml b/pyproject.toml index 787afe5..d5e38d6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/python/raptorbt/__init__.py b/python/raptorbt/__init__.py index 60da0a2..714d32f 100644 --- a/python/raptorbt/__init__.py +++ b/python/raptorbt/__init__.py @@ -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 diff --git a/python/raptorbt/__pycache__/__init__.cpython-311.pyc b/python/raptorbt/__pycache__/__init__.cpython-311.pyc index eb2484f..3dbc4e2 100644 Binary files a/python/raptorbt/__pycache__/__init__.cpython-311.pyc and b/python/raptorbt/__pycache__/__init__.cpython-311.pyc differ diff --git a/python/raptorbt/_raptorbt.cpython-311-darwin.so b/python/raptorbt/_raptorbt.cpython-311-darwin.so index cd56367..6378fa6 100755 Binary files a/python/raptorbt/_raptorbt.cpython-311-darwin.so and b/python/raptorbt/_raptorbt.cpython-311-darwin.so differ diff --git a/src/lib.rs b/src/lib.rs index 788eb8f..38b3ffc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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::()?; + 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)?)?; diff --git a/src/python/bindings.rs b/src/python/bindings.rs index b3c871e..80865b3 100644 --- a/src/python/bindings.rs +++ b/src/python/bindings.rs @@ -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>, + pub leg_configs: Vec<(String, f64, i32, usize)>, + pub entries: Vec, + pub exits: Vec, + #[pyo3(get, set)] + pub spread_type: String, + #[pyo3(get, set)] + pub max_loss: Option, + #[pyo3(get, set)] + pub target_profit: Option, +} + +#[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>, + leg_configs: Vec<(String, f64, i32, usize)>, + entries: PyReadonlyArray1, + exits: PyReadonlyArray1, + spread_type: &str, + max_loss: Option, + target_profit: Option, + ) -> 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, + underlying_close: PyReadonlyArray1, + items: Vec, + config: Option<&PyBacktestConfig>, +) -> PyResult> { + 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>, + entries: Vec, + exits: Vec, + spread_config: SpreadConfig, + } + + let prepared: Vec = items + .into_iter() + .map(|item| { + let rust_leg_configs: Vec = 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"))]