diff --git a/Cargo.lock b/Cargo.lock index f19c1f9..5aa5f03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -502,7 +502,7 @@ dependencies = [ [[package]] name = "raptorbt" -version = "0.3.3" +version = "0.3.4" dependencies = [ "approx", "criterion", diff --git a/Cargo.toml b/Cargo.toml index f202907..2d8c493 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "raptorbt" -version = "0.3.3" +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 "] diff --git a/README.md b/README.md index 8a87c3c..3f02fd4 100644 --- a/README.md +++ b/README.md @@ -883,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" ``` --- @@ -997,6 +997,14 @@ 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 diff --git a/pyproject.toml b/pyproject.toml index d5e38d6..71b7948 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "raptorbt" -version = "0.3.3" +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" diff --git a/python/raptorbt/__init__.py b/python/raptorbt/__init__.py index 714d32f..3b9045b 100644 --- a/python/raptorbt/__init__.py +++ b/python/raptorbt/__init__.py @@ -46,7 +46,7 @@ from raptorbt._raptorbt import ( rolling_max, ) -__version__ = "0.3.3" +__version__ = "0.3.4" __all__ = [ # Config classes diff --git a/python/raptorbt/__pycache__/__init__.cpython-311.pyc b/python/raptorbt/__pycache__/__init__.cpython-311.pyc index 3dbc4e2..2635b04 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 6378fa6..29d75cf 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/core/types.rs b/src/core/types.rs index 9d96da1..dab37b6 100644 --- a/src/core/types.rs +++ b/src/core/types.rs @@ -212,6 +212,8 @@ pub enum ExitReason { TrailingStop, /// End of data. EndOfData, + /// Option expiry settlement. + Settlement, } /// Backtest configuration. diff --git a/src/python/bindings.rs b/src/python/bindings.rs index 80865b3..83e4c92 100644 --- a/src/python/bindings.rs +++ b/src/python/bindings.rs @@ -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, @@ -795,6 +795,7 @@ pub fn run_spread_backtest<'py>( spread_type: &str, max_loss: Option, target_profit: Option, + leg_expiry_timestamps: Option>, ) -> PyResult { 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); @@ -943,6 +949,10 @@ pub fn batch_spread_backtest( "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, }; @@ -953,6 +963,7 @@ pub fn batch_spread_backtest( max_loss: item.max_loss, target_profit: item.target_profit, close_at_eod: false, + leg_expiry_timestamps: None, }; PreparedItem { diff --git a/src/strategies/spreads.rs b/src/strategies/spreads.rs index 71769ee..c7afced 100644 --- a/src/strategies/spreads.rs +++ b/src/strategies/spreads.rs @@ -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, /// 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>, } 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 = self .config .leg_configs