diff --git a/Cargo.lock b/Cargo.lock index 69b7e81..d742e0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -502,7 +502,7 @@ dependencies = [ [[package]] name = "raptorbt" -version = "0.3.1" +version = "0.3.2" dependencies = [ "approx", "criterion", diff --git a/Cargo.toml b/Cargo.toml index 314d352..a1bd8d3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "raptorbt" -version = "0.3.1" +version = "0.3.2" 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 85a9e15..c1564b0 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ [![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) + + **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. @@ -79,9 +81,10 @@ 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 +- **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 +130,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 +138,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 +146,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 +164,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 @@ -529,6 +541,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 +717,27 @@ 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. +### 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 +790,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 +926,22 @@ MIT License - see [LICENSE](LICENSE) for details. ## Changelog +### 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) diff --git a/pyproject.toml b/pyproject.toml index bdc9046..388a119 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "raptorbt" -version = "0.3.1" +version = "0.3.2" 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 628efe5..59e2da3 100644 --- a/python/raptorbt/__init__.py +++ b/python/raptorbt/__init__.py @@ -43,7 +43,7 @@ from raptorbt._raptorbt import ( rolling_max, ) -__version__ = "0.3.1" +__version__ = "0.3.2" __all__ = [ # Config classes diff --git a/src/core/types.rs b/src/core/types.rs index 79f4c28..9d96da1 100644 --- a/src/core/types.rs +++ b/src/core/types.rs @@ -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. diff --git a/src/metrics/streaming.rs b/src/metrics/streaming.rs index 77bb280..1e40678 100644 --- a/src/metrics/streaming.rs +++ b/src/metrics/streaming.rs @@ -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, } } diff --git a/src/portfolio/engine.rs b/src/portfolio/engine.rs index f587af5..198ea3b 100644 --- a/src/portfolio/engine.rs +++ b/src/portfolio/engine.rs @@ -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, } } diff --git a/src/python/bindings.rs b/src/python/bindings.rs index 5c657ea..b3c871e 100644 --- a/src/python/bindings.rs +++ b/src/python/bindings.rs @@ -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] @@ -1203,6 +1207,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 = result