6 Commits

Author SHA1 Message Date
vatsal 7e91293e1a Merge pull request #13 from alphabench/feat/options-settlement
feat: add option settlement handling and single-leg spread types, bump to 0.3.4
2026-03-07 06:40:10 +05:30
porcelaincode c9451069d8 feat: add option settlement handling and single-leg spread types, bump to 0.3.4
Add settlement logic for option expiry with Settlement exit reason, leg_expiry_timestamps parameter for per-leg expiry tracking, and new single-leg spread types (LongCall, LongPut, NakedCall, NakedPut). Positions are force-closed at settlement with premiums replaced by intrinsic value, and re-entry is prevented after all legs expire.
2026-03-07 06:37:31 +05:30
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
vatsal f7592d5d79 Merge pull request #11 from alphabench/feat/refining-metrics-for-portfolio
docs: remove iframe from README, post1 release
2026-02-18 19:17:46 +05:30
porcelaincode 87545683eb docs: remove iframe from README, post1 release 2026-02-18 19:15:15 +05:30
11 changed files with 283 additions and 11 deletions
Generated
+1 -1
View File
@@ -502,7 +502,7 @@ dependencies = [
[[package]]
name = "raptorbt"
version = "0.3.2"
version = "0.3.4"
dependencies = [
"approx",
"criterion",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "raptorbt"
version = "0.3.2"
version = "0.3.4"
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>"]
+91 -3
View File
@@ -6,8 +6,6 @@
[![Rust](https://img.shields.io/badge/rust-1.70+-red.svg)](https://www.rust-lang.org/)
[![PyPI Downloads](https://static.pepy.tech/personalized-badge/raptorbt?period=total&units=INTERNATIONAL_SYSTEM&left_color=GRAY&right_color=ORANGE&left_text=downloads)](https://pepy.tech/projects/raptorbt)
<iframe src="https://clickhouse-analytics.metabaseapp.com/public/dashboard/daa27bf9-c01e-43fe-9260-c69b679cfe83?project_name=raptorbt#&theme=night" frameborder="0" width="100%" height="600"></iframe>
**Blazing-fast backtesting for the modern quant.**
RaptorBT is a high-performance backtesting engine written in Rust with Python bindings via PyO3. It serves as a drop-in replacement for VectorBT — delivering **HFT-grade compute efficiency** with full metric parity.
@@ -82,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
@@ -406,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
@@ -722,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
@@ -812,7 +883,7 @@ for trade in result.trades():
print(trade.pnl) # Profit/Loss
print(trade.return_pct) # Return percentage
print(trade.fees) # Fees paid
print(trade.exit_reason) # "Signal", "StopLoss", "TakeProfit"
print(trade.exit_reason) # "Signal", "StopLoss", "TakeProfit", "TrailingStop", "EndOfData", "Settlement"
```
---
@@ -926,6 +997,23 @@ MIT License - see [LICENSE](LICENSE) for details.
## Changelog
### v0.3.4
- Add single-leg option spread types: `LongCall`, `LongPut`, `NakedCall`, `NakedPut` to `SpreadType` enum
- Add `ExitReason::Settlement` for option expiry settlement exits
- Add `leg_expiry_timestamps` parameter to `run_spread_backtest` for per-leg expiry tracking
- Positions are force-closed at settlement when any leg expires, with premiums replaced by intrinsic value
- Prevent re-entry after all legs have expired
### 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"
version = "0.3.4"
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"
__version__ = "0.3.4"
__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.
+2
View File
@@ -212,6 +212,8 @@ pub enum ExitReason {
TrailingStop,
/// End of data.
EndOfData,
/// Option expiry settlement.
Settlement,
}
/// Backtest configuration.
+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)?)?;
+152 -1
View File
@@ -782,7 +782,7 @@ pub fn run_pairs_backtest<'py>(
/// Run spread backtest (multi-leg options).
#[pyfunction]
#[pyo3(signature = (timestamps, underlying_close, legs_premiums, leg_configs, entries, exits, config=None, spread_type="custom", max_loss=None, target_profit=None))]
#[pyo3(signature = (timestamps, underlying_close, legs_premiums, leg_configs, entries, exits, config=None, spread_type="custom", max_loss=None, target_profit=None, leg_expiry_timestamps=None))]
pub fn run_spread_backtest<'py>(
_py: Python<'py>,
timestamps: PyReadonlyArray1<i64>,
@@ -795,6 +795,7 @@ pub fn run_spread_backtest<'py>(
spread_type: &str,
max_loss: Option<f64>,
target_profit: Option<f64>,
leg_expiry_timestamps: Option<Vec<i64>>,
) -> PyResult<PyBacktestResult> {
let ts = numpy_to_vec_i64(timestamps);
let underlying = numpy_to_vec_f64(underlying_close);
@@ -824,6 +825,10 @@ pub fn run_spread_backtest<'py>(
"butterfly_put" | "butterflyput" => SpreadType::ButterflyPut,
"calendar" => SpreadType::Calendar,
"diagonal" => SpreadType::Diagonal,
"long_call" | "longcall" => SpreadType::LongCall,
"long_put" | "longput" => SpreadType::LongPut,
"naked_call" | "nakedcall" => SpreadType::NakedCall,
"naked_put" | "nakedput" => SpreadType::NakedPut,
_ => SpreadType::Custom,
};
@@ -834,6 +839,7 @@ pub fn run_spread_backtest<'py>(
max_loss,
target_profit,
close_at_eod: false,
leg_expiry_timestamps,
};
let backtest = SpreadBacktest::new(spread_config);
@@ -842,6 +848,151 @@ 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,
"long_call" | "longcall" => SpreadType::LongCall,
"long_put" | "longput" => SpreadType::LongPut,
"naked_call" | "nakedcall" => SpreadType::NakedCall,
"naked_put" | "nakedput" => SpreadType::NakedPut,
_ => 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,
leg_expiry_timestamps: None,
};
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"))]
+24 -3
View File
@@ -31,6 +31,10 @@ pub enum SpreadType {
ButterflyPut,
Calendar,
Diagonal,
LongCall,
LongPut,
NakedCall,
NakedPut,
Custom,
}
@@ -95,6 +99,9 @@ pub struct SpreadConfig {
pub target_profit: Option<f64>,
/// Whether to close at end of day.
pub close_at_eod: bool,
/// Per-leg expiry timestamps in nanoseconds (optional, for settlement logic).
/// When provided, positions are force-closed at or after the earliest leg expiry.
pub leg_expiry_timestamps: Option<Vec<i64>>,
}
impl Default for SpreadConfig {
@@ -106,6 +113,7 @@ impl Default for SpreadConfig {
max_loss: None,
target_profit: None,
close_at_eod: false,
leg_expiry_timestamps: None,
}
}
}
@@ -251,9 +259,16 @@ impl SpreadBacktest {
// Calculate unrealized P&L for exit checks
let unrealized_pnl = position.as_ref().map(|p| p.total_unrealized_pnl()).unwrap_or(0.0);
// Check if any leg has expired at this bar
let is_expiry = position.is_some()
&& self.config.leg_expiry_timestamps.as_ref().map_or(false, |expiries| {
expiries.iter().any(|&exp_ts| timestamps[i] >= exp_ts)
});
// Check for exit signals or conditions
let should_exit = position.is_some()
&& (exits[i]
|| is_expiry
|| self.check_max_loss(&position, unrealized_pnl)
|| self.check_target_profit(&position, unrealized_pnl));
@@ -267,7 +282,9 @@ impl SpreadBacktest {
// Record trade
trade_id += 1;
let exit_reason = if exits[i] {
let exit_reason = if is_expiry {
ExitReason::Settlement
} else if exits[i] {
ExitReason::Signal
} else if self.check_max_loss(&Some(pos.clone()), pnl) {
ExitReason::StopLoss
@@ -311,8 +328,12 @@ impl SpreadBacktest {
}
}
// Check for entry signals
if position.is_none() && entries[i] {
// Check for entry signals (don't re-enter after all legs expired)
let all_expired =
self.config.leg_expiry_timestamps.as_ref().map_or(false, |expiries| {
expiries.iter().all(|&exp_ts| timestamps[i] >= exp_ts)
});
if position.is_none() && entries[i] && !all_expired {
let legs: Vec<LegPosition> = self
.config
.leg_configs