2 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
10 changed files with 51 additions and 9 deletions
Generated
+1 -1
View File
@@ -502,7 +502,7 @@ dependencies = [
[[package]]
name = "raptorbt"
version = "0.3.3"
version = "0.3.4"
dependencies = [
"approx",
"criterion",
+1 -1
View File
@@ -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 <contact@alphabench.in>"]
+9 -1
View File
@@ -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
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -46,7 +46,7 @@ from raptorbt._raptorbt import (
rolling_max,
)
__version__ = "0.3.3"
__version__ = "0.3.4"
__all__ = [
# Config classes
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.
+12 -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);
@@ -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 {
+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