6 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
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
vatsal eb5335e809 Merge pull request #10 from alphabench/feat/refining-metrics-for-portfolio
feat: add payoff_ratio and recovery_factor metrics, bump to 0.3.2
2026-02-18 18:58:08 +05:30
porcelaincode 0ff67e7fe2 feat: add payoff_ratio and recovery_factor metrics, bump to 0.3.2
Add two new risk/reward metrics to BacktestMetrics:
- payoff_ratio: avg winning return / avg losing return (absolute)
- recovery_factor: net profit / max drawdown in absolute terms
Computed in both StreamingMetrics::finalize() and PortfolioEngine.
Exposed via PyO3 with #[pyo3(get)] on PyBacktestMetrics.
Updated README with API reference and changelog.
2026-02-18 18:57:42 +05:30
12 changed files with 409 additions and 8 deletions
Generated
+1 -1
View File
@@ -502,7 +502,7 @@ dependencies = [
[[package]]
name = "raptorbt"
version = "0.3.1"
version = "0.3.3"
dependencies = [
"approx",
"criterion",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "raptorbt"
version = "0.3.1"
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>"]
+193 -4
View File
@@ -79,9 +79,11 @@ RaptorBT was built to address the performance limitations of VectorBT. Benchmark
### Key Features
- **5 Strategy Types**: Single instrument, basket/collective, pairs trading, options, and multi-strategy
- **30+ Metrics**: Full parity with VectorBT including Sharpe, Sortino, Calmar, Omega, SQN, and more
- **10 Technical Indicators**: SMA, EMA, RSI, MACD, Stochastic, ATR, Bollinger Bands, ADX, VWAP, Supertrend
- **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
- **Stop/Target Management**: Fixed, ATR-based, and trailing stops with risk-reward targets
- **100% Deterministic**: No JIT compilation variance between runs
- **Native Parallelism**: Rayon-based parallel processing with explicit SIMD optimizations
@@ -127,6 +129,7 @@ raptorbt/
│ ├── core/ # Core types and error handling
│ │ ├── types.rs # BacktestConfig, BacktestResult, Trade, Metrics
│ │ ├── error.rs # RaptorError enum
│ │ ├── session.rs # SessionTracker, SessionConfig (intraday sessions)
│ │ └── timeseries.rs # Time series utilities
│ │
│ ├── strategies/ # Strategy implementations
@@ -134,6 +137,7 @@ raptorbt/
│ │ ├── basket.rs # Basket/collective strategies
│ │ ├── pairs.rs # Pairs trading
│ │ ├── options.rs # Options strategies
│ │ ├── spreads.rs # Multi-leg spread strategies
│ │ └── multi.rs # Multi-strategy combining
│ │
│ ├── indicators/ # Technical indicators
@@ -141,7 +145,8 @@ raptorbt/
│ │ ├── momentum.rs # RSI, MACD, Stochastic
│ │ ├── volatility.rs # ATR, Bollinger Bands
│ │ ├── strength.rs # ADX
│ │ ── volume.rs # VWAP
│ │ ── volume.rs # VWAP
│ │ └── rolling.rs # Rolling Min/Max (LLV/HHV)
│ │
│ ├── metrics/ # Performance metrics
│ │ ├── streaming.rs # Streaming metric calculations
@@ -158,6 +163,12 @@ raptorbt/
│ │ ├── atr.rs # ATR-based stops
│ │ └── trailing.rs # Trailing stops
│ │
│ ├── portfolio/ # Portfolio-level analysis
│ │ ├── monte_carlo.rs # Monte Carlo forward simulation (GBM + Cholesky)
│ │ ├── allocation.rs # Capital allocation
│ │ ├── engine.rs # Portfolio engine
│ │ └── position.rs # Position management
│ │
│ ├── python/ # PyO3 bindings
│ │ ├── bindings.rs # Python function exports
│ │ └── numpy_bridge.rs # NumPy array conversion
@@ -394,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
@@ -529,6 +584,68 @@ config.set_risk_reward_target(ratio=2.0) # 2:1 risk-reward ratio
---
## Monte Carlo Portfolio Simulation
RaptorBT includes a high-performance Monte Carlo forward simulation engine for portfolio risk analysis. It uses Geometric Brownian Motion (GBM) with Cholesky decomposition for correlated multi-asset simulation, parallelized via Rayon.
```python
import numpy as np
import raptorbt
# Historical daily returns per strategy/asset (numpy arrays)
returns = [
np.array([0.001, -0.002, 0.003, ...]), # Strategy 1 returns
np.array([0.002, 0.001, -0.001, ...]), # Strategy 2 returns
]
# Portfolio weights (must sum to 1.0)
weights = np.array([0.6, 0.4])
# Correlation matrix (N x N)
correlation_matrix = [
np.array([1.0, 0.3]),
np.array([0.3, 1.0]),
]
# Run simulation
result = raptorbt.simulate_portfolio_mc(
returns=returns,
weights=weights,
correlation_matrix=correlation_matrix,
initial_value=100000.0,
n_simulations=10000, # Number of Monte Carlo paths (default: 10,000)
horizon_days=252, # Forward projection horizon (default: 252)
seed=42, # Random seed for reproducibility (default: 42)
)
# Results
print(f"Expected Return: {result['expected_return']:.2f}%")
print(f"Probability of Loss: {result['probability_of_loss']:.2%}")
print(f"VaR (95%): {result['var_95']:.2f}%")
print(f"CVaR (95%): {result['cvar_95']:.2f}%")
# Percentile paths: list of (percentile, path_values)
# Percentiles: 5th, 25th, 50th, 75th, 95th
for pct, path in result['percentile_paths']:
print(f" P{pct:.0f} final value: {path[-1]:.2f}")
# Final values: numpy array of terminal values for all simulations
final_values = result['final_values'] # numpy array, length = n_simulations
```
### Result Fields
| Field | Type | Description |
| --------------------- | -------------------------- | ---------------------------------------------------------- |
| `expected_return` | `float` | Expected return as percentage over the horizon |
| `probability_of_loss` | `float` | Probability that final value < initial value (0.0 to 1.0) |
| `var_95` | `float` | Value at Risk at 95% confidence (percentage) |
| `cvar_95` | `float` | Conditional VaR at 95% confidence (percentage) |
| `percentile_paths` | `List[Tuple[float, List]]` | Portfolio paths at 5th, 25th, 50th, 75th, 95th percentiles |
| `final_values` | `numpy.ndarray` | Terminal portfolio values for all simulations |
---
## VectorBT Comparison
RaptorBT is designed as a drop-in replacement for VectorBT. Here's a side-by-side comparison:
@@ -643,10 +760,55 @@ inst_config.set_fixed_target(0.05)
```
**Fields:**
- `lot_size` - Minimum tradeable quantity. Position sizes are rounded down to nearest lot_size multiple. Use `1.0` for equities, `50.0` for NIFTY F&O, `0.01` for forex.
- `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
result = raptorbt.simulate_portfolio_mc(
returns: List[np.ndarray], # Per-asset daily returns (N arrays)
weights: np.ndarray, # Portfolio weights (length N, sum to 1)
correlation_matrix: List[np.ndarray], # N x N correlation matrix
initial_value: float, # Starting portfolio value
n_simulations: int = 10000, # Number of Monte Carlo paths
horizon_days: int = 252, # Forward projection horizon in days
seed: int = 42, # Random seed for reproducibility
) -> dict
```
Returns a dictionary with keys: `expected_return`, `probability_of_loss`, `var_95`, `cvar_95`, `percentile_paths`, `final_values`.
### PyBacktestResult
```python
@@ -699,6 +861,8 @@ metrics.max_consecutive_wins
metrics.max_consecutive_losses
metrics.exposure_pct
metrics.open_trade_pnl
metrics.payoff_ratio # avg win / avg loss (risk/reward per trade)
metrics.recovery_factor # net profit / max drawdown (resilience)
# Convert to dictionary (VectorBT format)
stats_dict = metrics.to_dict()
@@ -833,6 +997,31 @@ 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
- Add `recovery_factor` metric to `BacktestMetrics` — net profit divided by maximum drawdown in absolute terms, measures how many times over the strategy recovered from its worst drawdown
- Both metrics computed in `StreamingMetrics::finalize()` (single-instrument backtest) and `PortfolioEngine` (multi-strategy aggregation)
- Both metrics exposed via PyO3 as `#[pyo3(get)]` attributes on `PyBacktestMetrics`
- Handles edge cases: returns `f64::INFINITY` when denominator is zero with positive numerator, `0.0` otherwise
### v0.3.1
- Add Monte Carlo portfolio simulation (`simulate_portfolio_mc`) for forward risk projection
- Geometric Brownian Motion (GBM) with Cholesky decomposition for correlated multi-asset simulation
- Rayon-parallelized simulation paths with deterministic seeding (xoshiro256\*\*)
- Returns percentile paths (P5/P25/P50/P75/P95), VaR, CVaR, expected return, and probability of loss
- GIL released during simulation for maximum Python concurrency
### v0.3.0
- Per-instrument configuration via `PyInstrumentConfig` (lot_size, alloted_capital, stop/target overrides)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "maturin"
[project]
name = "raptorbt"
version = "0.3.1"
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.1"
__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
@@ -376,6 +376,10 @@ pub struct BacktestMetrics {
pub avg_holding_period: f64,
/// Exposure time percentage (time in market).
pub exposure_pct: f64,
/// Payoff ratio (avg win / avg loss).
pub payoff_ratio: f64,
/// Recovery factor (net profit / max drawdown).
pub recovery_factor: f64,
}
/// Complete backtest result.
+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)?)?;
+26
View File
@@ -513,6 +513,30 @@ impl StreamingMetrics {
let worst_trade_pct =
if self.worst_trade_pct == f64::INFINITY { 0.0 } else { self.worst_trade_pct };
// Payoff ratio: average win / average loss (absolute value)
let payoff_ratio = if avg_loss_pct.abs() > 0.0 {
avg_win_pct / avg_loss_pct.abs()
} else if avg_win_pct > 0.0 {
f64::INFINITY
} else {
0.0
};
// Recovery factor: net profit / max drawdown (absolute value)
let net_profit = final_value - initial_capital;
let recovery_factor = if self.max_drawdown_pct > 0.0 && initial_capital > 0.0 {
let max_dd_absolute = self.max_drawdown_pct / 100.0 * initial_capital;
if max_dd_absolute > 0.0 {
net_profit / max_dd_absolute
} else {
0.0
}
} else if net_profit > 0.0 {
f64::INFINITY
} else {
0.0
};
BacktestMetrics {
total_return_pct,
sharpe_ratio,
@@ -545,6 +569,8 @@ impl StreamingMetrics {
max_consecutive_losses: self.max_consecutive_losses,
avg_holding_period,
exposure_pct: 0.0, // TODO: calculate based on time in market
payoff_ratio,
recovery_factor,
}
}
+26
View File
@@ -591,6 +591,30 @@ impl PortfolioEngine {
0.0
};
// Payoff ratio: average win / average loss (absolute value)
let payoff_ratio = if avg_loss_pct.abs() > 0.0 {
avg_win_pct / avg_loss_pct.abs()
} else if avg_win_pct > 0.0 {
f64::INFINITY
} else {
0.0
};
// Recovery factor: net profit / max drawdown (absolute value)
let net_profit = end_value - start_value;
let recovery_factor = if max_drawdown_pct > 0.0 && start_value > 0.0 {
let max_dd_absolute = max_drawdown_pct / 100.0 * start_value;
if max_dd_absolute > 0.0 {
net_profit / max_dd_absolute
} else {
0.0
}
} else if net_profit > 0.0 {
f64::INFINITY
} else {
0.0
};
BacktestMetrics {
total_return_pct,
sharpe_ratio,
@@ -623,6 +647,8 @@ impl PortfolioEngine {
max_consecutive_losses,
avg_holding_period,
exposure_pct,
payoff_ratio,
recovery_factor,
}
}
+146
View File
@@ -419,6 +419,10 @@ pub struct PyBacktestMetrics {
pub avg_holding_period: f64,
#[pyo3(get)]
pub exposure_pct: f64,
#[pyo3(get)]
pub payoff_ratio: f64,
#[pyo3(get)]
pub recovery_factor: f64,
}
#[pymethods]
@@ -838,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"))]
@@ -1203,6 +1347,8 @@ fn convert_result(result: crate::core::types::BacktestResult) -> PyBacktestResul
max_consecutive_losses: result.metrics.max_consecutive_losses,
avg_holding_period: result.metrics.avg_holding_period,
exposure_pct: result.metrics.exposure_pct,
payoff_ratio: result.metrics.payoff_ratio,
recovery_factor: result.metrics.recovery_factor,
};
let trades: Vec<PyTrade> = result