Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a82ddc598a | ||
|
|
bb6ce05c57 |
Generated
+1
-1
@@ -502,7 +502,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "raptorbt"
|
name = "raptorbt"
|
||||||
version = "0.2.1"
|
version = "0.3.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"approx",
|
"approx",
|
||||||
"criterion",
|
"criterion",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "raptorbt"
|
name = "raptorbt"
|
||||||
version = "0.2.2"
|
version = "0.3.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "High-performance Rust backtesting engine with Python bindings. Drop-in VectorBT replacement with up insanely faster performance at fractional memory footprint."
|
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>"]
|
authors = ["Alphabench <contact@alphabench.in>"]
|
||||||
|
|||||||
@@ -265,6 +265,9 @@ trades = result.trades() # Returns list of PyTrade objects
|
|||||||
Basic long or short strategy on a single instrument.
|
Basic long or short strategy on a single instrument.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
# Optional: Instrument-specific configuration
|
||||||
|
inst_config = raptorbt.PyInstrumentConfig(lot_size=1.0)
|
||||||
|
|
||||||
result = raptorbt.run_single_backtest(
|
result = raptorbt.run_single_backtest(
|
||||||
timestamps=timestamps,
|
timestamps=timestamps,
|
||||||
open=open_prices, high=high_prices, low=low_prices,
|
open=open_prices, high=high_prices, low=low_prices,
|
||||||
@@ -274,6 +277,7 @@ result = raptorbt.run_single_backtest(
|
|||||||
weight=1.0,
|
weight=1.0,
|
||||||
symbol="SYMBOL",
|
symbol="SYMBOL",
|
||||||
config=config,
|
config=config,
|
||||||
|
instrument_config=inst_config, # Optional: lot_size rounding, capital caps
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -288,10 +292,18 @@ instruments = [
|
|||||||
(timestamps, open3, high3, low3, close3, volume3, entries3, exits3, 1, 0.34, "MSFT"),
|
(timestamps, open3, high3, low3, close3, volume3, entries3, exits3, 1, 0.34, "MSFT"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Optional: Per-instrument configs for lot_size and capital allocation
|
||||||
|
instrument_configs = {
|
||||||
|
"AAPL": raptorbt.PyInstrumentConfig(lot_size=1.0, alloted_capital=33000),
|
||||||
|
"GOOGL": raptorbt.PyInstrumentConfig(lot_size=1.0, alloted_capital=33000),
|
||||||
|
"MSFT": raptorbt.PyInstrumentConfig(lot_size=1.0, alloted_capital=34000),
|
||||||
|
}
|
||||||
|
|
||||||
result = raptorbt.run_basket_backtest(
|
result = raptorbt.run_basket_backtest(
|
||||||
instruments=instruments,
|
instruments=instruments,
|
||||||
config=config,
|
config=config,
|
||||||
sync_mode="all", # "all", "any", "majority", "master"
|
sync_mode="all", # "all", "any", "majority", "master"
|
||||||
|
instrument_configs=instrument_configs, # Optional
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -612,6 +624,29 @@ config.set_atr_target(multiplier: float, period: int)
|
|||||||
config.set_risk_reward_target(ratio: float)
|
config.set_risk_reward_target(ratio: float)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### PyInstrumentConfig
|
||||||
|
|
||||||
|
Per-instrument configuration for position sizing and risk management.
|
||||||
|
|
||||||
|
```python
|
||||||
|
inst_config = raptorbt.PyInstrumentConfig(
|
||||||
|
lot_size=1.0, # Min tradeable quantity (1 for equity, 50 for NIFTY F&O)
|
||||||
|
alloted_capital=50000.0, # Capital allocated to this instrument (optional)
|
||||||
|
existing_qty=None, # Existing position quantity (future use)
|
||||||
|
avg_price=None, # Existing position avg price (future use)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Optional: per-instrument stop/target overrides
|
||||||
|
inst_config.set_fixed_stop(0.02)
|
||||||
|
inst_config.set_trailing_stop(0.03)
|
||||||
|
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.
|
||||||
|
|
||||||
### PyBacktestResult
|
### PyBacktestResult
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -798,6 +833,32 @@ MIT License - see [LICENSE](LICENSE) for details.
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### v0.3.0
|
||||||
|
|
||||||
|
- Per-instrument configuration via `PyInstrumentConfig` (lot_size, alloted_capital, stop/target overrides)
|
||||||
|
- Position sizes now correctly rounded to lot_size multiples
|
||||||
|
- Support for per-instrument capital allocation in basket backtests
|
||||||
|
- Future-ready fields: existing_qty, avg_price for live-to-backtest transitions
|
||||||
|
|
||||||
|
### v0.2.2
|
||||||
|
|
||||||
|
- Export `run_spread_backtest` Python binding for multi-leg options spread strategies
|
||||||
|
- Export `rolling_min` and `rolling_max` indicator functions to Python
|
||||||
|
|
||||||
|
### v0.2.1
|
||||||
|
|
||||||
|
- Add `rolling_min` and `rolling_max` indicators for LLV (Lowest Low Value) and HHV (Highest High Value) support
|
||||||
|
- NaN handling for warmup period
|
||||||
|
|
||||||
|
### v0.2.0
|
||||||
|
|
||||||
|
- Add multi-leg spread backtesting (`run_spread_backtest`) supporting straddles, strangles, vertical spreads, iron condors, iron butterflies, butterfly spreads, calendar spreads, and diagonal spreads
|
||||||
|
- Coordinated entry/exit across all legs with net premium P&L calculation
|
||||||
|
- Max loss and target profit exit thresholds for spreads
|
||||||
|
- Add `SessionTracker` for intraday session management: market hours detection, squareoff time enforcement, session high/low/open tracking
|
||||||
|
- Pre-built session configs for NSE equity (9:15-15:30), MCX commodity (9:00-23:30), and CDS currency (9:00-17:00)
|
||||||
|
- Extend `StreamingMetrics` with equity/drawdown tracking, trade recording, and `finalize()` method
|
||||||
|
|
||||||
### v0.1.0
|
### v0.1.0
|
||||||
|
|
||||||
- Initial release
|
- Initial release
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "maturin"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "raptorbt"
|
name = "raptorbt"
|
||||||
version = "0.2.2"
|
version = "0.3.0"
|
||||||
description = "High-performance Rust backtesting engine with Python bindings. Drop-in VectorBT replacement with up insanely faster performance at fractional memory footprint."
|
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"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ offering significant performance improvements over vectorbt:
|
|||||||
from raptorbt._raptorbt import (
|
from raptorbt._raptorbt import (
|
||||||
# Config classes
|
# Config classes
|
||||||
PyBacktestConfig,
|
PyBacktestConfig,
|
||||||
|
PyInstrumentConfig,
|
||||||
PyStopConfig,
|
PyStopConfig,
|
||||||
PyTargetConfig,
|
PyTargetConfig,
|
||||||
# Result classes
|
# Result classes
|
||||||
@@ -40,11 +41,12 @@ from raptorbt._raptorbt import (
|
|||||||
rolling_max,
|
rolling_max,
|
||||||
)
|
)
|
||||||
|
|
||||||
__version__ = "0.2.2"
|
__version__ = "0.3.0"
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# Config classes
|
# Config classes
|
||||||
"PyBacktestConfig",
|
"PyBacktestConfig",
|
||||||
|
"PyInstrumentConfig",
|
||||||
"PyStopConfig",
|
"PyStopConfig",
|
||||||
"PyTargetConfig",
|
"PyTargetConfig",
|
||||||
# Result classes
|
# Result classes
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -244,6 +244,47 @@ impl Default for BacktestConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Per-instrument configuration for position sizing and risk management.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct InstrumentConfig {
|
||||||
|
/// Minimum tradeable quantity (1.0 for NSE EQ, 50.0 for NIFTY F&O, 0.01 for forex).
|
||||||
|
pub lot_size: Option<f64>,
|
||||||
|
/// Per-instrument capital cap.
|
||||||
|
pub alloted_capital: Option<f64>,
|
||||||
|
/// Per-instrument stop override.
|
||||||
|
pub stop: Option<StopConfig>,
|
||||||
|
/// Per-instrument target override.
|
||||||
|
pub target: Option<TargetConfig>,
|
||||||
|
/// Existing position quantity (future use).
|
||||||
|
pub existing_qty: Option<f64>,
|
||||||
|
/// Existing position average price (future use).
|
||||||
|
pub avg_price: Option<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InstrumentConfig {
|
||||||
|
/// Round a raw position size down to the nearest lot_size multiple.
|
||||||
|
/// Returns raw_size unchanged if lot_size is None or <= 0.
|
||||||
|
pub fn round_to_lot(&self, raw_size: f64) -> f64 {
|
||||||
|
match self.lot_size {
|
||||||
|
Some(lot) if lot > 0.0 => (raw_size / lot).floor() * lot,
|
||||||
|
_ => raw_size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for InstrumentConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
lot_size: None,
|
||||||
|
alloted_capital: None,
|
||||||
|
stop: None,
|
||||||
|
target: None,
|
||||||
|
existing_qty: None,
|
||||||
|
avg_price: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Stop-loss configuration.
|
/// Stop-loss configuration.
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
pub enum StopConfig {
|
pub enum StopConfig {
|
||||||
@@ -460,3 +501,49 @@ impl Default for Position {
|
|||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_round_to_lot_whole_shares() {
|
||||||
|
let config = InstrumentConfig { lot_size: Some(1.0), ..Default::default() };
|
||||||
|
assert_eq!(config.round_to_lot(242.47), 242.0);
|
||||||
|
assert_eq!(config.round_to_lot(1.0), 1.0);
|
||||||
|
assert_eq!(config.round_to_lot(0.5), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_round_to_lot_nifty_fo() {
|
||||||
|
let config = InstrumentConfig { lot_size: Some(50.0), ..Default::default() };
|
||||||
|
assert_eq!(config.round_to_lot(242.0), 200.0);
|
||||||
|
assert_eq!(config.round_to_lot(50.0), 50.0);
|
||||||
|
assert_eq!(config.round_to_lot(49.0), 0.0);
|
||||||
|
assert_eq!(config.round_to_lot(150.0), 150.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_round_to_lot_fractional() {
|
||||||
|
let config = InstrumentConfig { lot_size: Some(0.01), ..Default::default() };
|
||||||
|
assert!((config.round_to_lot(1.234) - 1.23).abs() < 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_round_to_lot_none() {
|
||||||
|
let config = InstrumentConfig::default();
|
||||||
|
assert_eq!(config.round_to_lot(242.47), 242.47);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_round_to_lot_zero() {
|
||||||
|
let config = InstrumentConfig { lot_size: Some(0.0), ..Default::default() };
|
||||||
|
assert_eq!(config.round_to_lot(242.47), 242.47);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_round_to_lot_negative() {
|
||||||
|
let config = InstrumentConfig { lot_size: Some(-1.0), ..Default::default() };
|
||||||
|
assert_eq!(config.round_to_lot(242.47), 242.47);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ pub mod strategies;
|
|||||||
fn _raptorbt(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
fn _raptorbt(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
||||||
// Register config classes
|
// Register config classes
|
||||||
m.add_class::<python::bindings::PyBacktestConfig>()?;
|
m.add_class::<python::bindings::PyBacktestConfig>()?;
|
||||||
|
m.add_class::<python::bindings::PyInstrumentConfig>()?;
|
||||||
m.add_class::<python::bindings::PyStopConfig>()?;
|
m.add_class::<python::bindings::PyStopConfig>()?;
|
||||||
m.add_class::<python::bindings::PyTargetConfig>()?;
|
m.add_class::<python::bindings::PyTargetConfig>()?;
|
||||||
|
|
||||||
|
|||||||
+72
-16
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use crate::core::types::{
|
use crate::core::types::{
|
||||||
BacktestConfig, BacktestMetrics, BacktestResult, CompiledSignals, Direction, ExitReason,
|
BacktestConfig, BacktestMetrics, BacktestResult, CompiledSignals, Direction, ExitReason,
|
||||||
OhlcvData, Price, StopConfig, TargetConfig, Trade,
|
InstrumentConfig, OhlcvData, Price, StopConfig, TargetConfig, Trade,
|
||||||
};
|
};
|
||||||
use crate::execution::{FeeModel, FillPrice, SlippageModel};
|
use crate::execution::{FeeModel, FillPrice, SlippageModel};
|
||||||
use crate::indicators::volatility::atr;
|
use crate::indicators::volatility::atr;
|
||||||
@@ -69,6 +69,24 @@ impl PortfolioEngine {
|
|||||||
/// # Returns
|
/// # Returns
|
||||||
/// Backtest result
|
/// Backtest result
|
||||||
pub fn run_single(&self, ohlcv: &OhlcvData, signals: &CompiledSignals) -> BacktestResult {
|
pub fn run_single(&self, ohlcv: &OhlcvData, signals: &CompiledSignals) -> BacktestResult {
|
||||||
|
self.run_single_with_instrument_config(ohlcv, signals, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run backtest on single instrument with optional per-instrument configuration.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `ohlcv` - OHLCV data
|
||||||
|
/// * `signals` - Compiled trading signals
|
||||||
|
/// * `inst_config` - Optional per-instrument config (lot_size, capital cap, stop/target overrides)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Backtest result
|
||||||
|
pub fn run_single_with_instrument_config(
|
||||||
|
&self,
|
||||||
|
ohlcv: &OhlcvData,
|
||||||
|
signals: &CompiledSignals,
|
||||||
|
inst_config: Option<&InstrumentConfig>,
|
||||||
|
) -> BacktestResult {
|
||||||
let n = ohlcv.len();
|
let n = ohlcv.len();
|
||||||
assert_eq!(n, signals.len(), "OHLCV and signals must have same length");
|
assert_eq!(n, signals.len(), "OHLCV and signals must have same length");
|
||||||
|
|
||||||
@@ -86,14 +104,20 @@ impl PortfolioEngine {
|
|||||||
let mut streaming = StreamingMetrics::new();
|
let mut streaming = StreamingMetrics::new();
|
||||||
let mut peak_equity = cash;
|
let mut peak_equity = cash;
|
||||||
|
|
||||||
|
// Determine effective stop/target configs (per-instrument overrides take precedence)
|
||||||
|
let effective_stop =
|
||||||
|
inst_config.and_then(|ic| ic.stop.as_ref()).unwrap_or(&self.config.stop);
|
||||||
|
let effective_target =
|
||||||
|
inst_config.and_then(|ic| ic.target.as_ref()).unwrap_or(&self.config.target);
|
||||||
|
|
||||||
// Pre-calculate ATR for ATR-based stops
|
// Pre-calculate ATR for ATR-based stops
|
||||||
let atr_values = if matches!(self.config.stop, StopConfig::Atr { .. })
|
let atr_values = if matches!(effective_stop, StopConfig::Atr { .. })
|
||||||
|| matches!(self.config.target, TargetConfig::Atr { .. })
|
|| matches!(effective_target, TargetConfig::Atr { .. })
|
||||||
{
|
{
|
||||||
let period = match self.config.stop {
|
let period = match effective_stop {
|
||||||
StopConfig::Atr { period, .. } => period,
|
StopConfig::Atr { period, .. } => *period,
|
||||||
_ => match self.config.target {
|
_ => match effective_target {
|
||||||
TargetConfig::Atr { period, .. } => period,
|
TargetConfig::Atr { period, .. } => *period,
|
||||||
_ => 14,
|
_ => 14,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -188,8 +212,8 @@ impl PortfolioEngine {
|
|||||||
|
|
||||||
// Update trailing stop if position still open
|
// Update trailing stop if position still open
|
||||||
if position.is_in_position() {
|
if position.is_in_position() {
|
||||||
if let StopConfig::Trailing { percent } = self.config.stop {
|
if let StopConfig::Trailing { percent } = effective_stop {
|
||||||
position.update_trailing_stop(percent);
|
position.update_trailing_stop(*percent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,26 +231,37 @@ impl PortfolioEngine {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Calculate position size
|
// Calculate position size
|
||||||
|
// Use per-instrument capital if set, capped at available cash
|
||||||
|
let available = inst_config
|
||||||
|
.and_then(|ic| ic.alloted_capital)
|
||||||
|
.map(|cap| cap.min(cash))
|
||||||
|
.unwrap_or(cash);
|
||||||
|
|
||||||
// VectorBT formula: size = cash / (price * (1 + fees))
|
// VectorBT formula: size = cash / (price * (1 + fees))
|
||||||
// This ensures the position value plus entry fee equals available cash
|
// This ensures the position value plus entry fee equals available cash
|
||||||
let fee_rate = self.config.fees;
|
let fee_rate = self.config.fees;
|
||||||
let size = if let Some(ref sizes) = signals.position_sizes {
|
let raw_size = if let Some(ref sizes) = signals.position_sizes {
|
||||||
sizes[i] * cash / (adjusted_price * (1.0 + fee_rate))
|
sizes[i] * available / (adjusted_price * (1.0 + fee_rate))
|
||||||
} else {
|
} else {
|
||||||
cash / (adjusted_price * (1.0 + fee_rate))
|
available / (adjusted_price * (1.0 + fee_rate))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Round to lot_size
|
||||||
|
let size = inst_config.map(|ic| ic.round_to_lot(raw_size)).unwrap_or(raw_size);
|
||||||
|
|
||||||
if size > 0.0 {
|
if size > 0.0 {
|
||||||
// Calculate entry fees
|
// Calculate entry fees
|
||||||
let entry_fees =
|
let entry_fees =
|
||||||
self.fee_model.calculate(adjusted_price, size, signals.direction);
|
self.fee_model.calculate(adjusted_price, size, signals.direction);
|
||||||
|
|
||||||
// Calculate stop and target prices
|
// Calculate stop and target prices
|
||||||
let (stop_price, target_price) = self.calculate_stop_target(
|
let (stop_price, target_price) = self.calculate_stop_target_with_config(
|
||||||
adjusted_price,
|
adjusted_price,
|
||||||
signals.direction,
|
signals.direction,
|
||||||
&atr_values,
|
&atr_values,
|
||||||
i,
|
i,
|
||||||
|
effective_stop,
|
||||||
|
effective_target,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Open position (passing entry_fees for trade PnL tracking)
|
// Open position (passing entry_fees for trade PnL tracking)
|
||||||
@@ -310,18 +345,39 @@ impl PortfolioEngine {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate stop and target prices.
|
/// Calculate stop and target prices using the global config.
|
||||||
|
#[allow(dead_code)]
|
||||||
fn calculate_stop_target(
|
fn calculate_stop_target(
|
||||||
&self,
|
&self,
|
||||||
entry_price: Price,
|
entry_price: Price,
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
atr_values: &[f64],
|
atr_values: &[f64],
|
||||||
idx: usize,
|
idx: usize,
|
||||||
|
) -> (Option<Price>, Option<Price>) {
|
||||||
|
self.calculate_stop_target_with_config(
|
||||||
|
entry_price,
|
||||||
|
direction,
|
||||||
|
atr_values,
|
||||||
|
idx,
|
||||||
|
&self.config.stop,
|
||||||
|
&self.config.target,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate stop and target prices with explicit stop/target configs.
|
||||||
|
fn calculate_stop_target_with_config(
|
||||||
|
&self,
|
||||||
|
entry_price: Price,
|
||||||
|
direction: Direction,
|
||||||
|
atr_values: &[f64],
|
||||||
|
idx: usize,
|
||||||
|
stop_config: &StopConfig,
|
||||||
|
target_config: &TargetConfig,
|
||||||
) -> (Option<Price>, Option<Price>) {
|
) -> (Option<Price>, Option<Price>) {
|
||||||
let multiplier = direction.multiplier();
|
let multiplier = direction.multiplier();
|
||||||
|
|
||||||
// Calculate stop price
|
// Calculate stop price
|
||||||
let stop_price = match self.config.stop {
|
let stop_price = match stop_config {
|
||||||
StopConfig::None => None,
|
StopConfig::None => None,
|
||||||
StopConfig::Fixed { percent } => Some(entry_price * (1.0 - multiplier * percent)),
|
StopConfig::Fixed { percent } => Some(entry_price * (1.0 - multiplier * percent)),
|
||||||
StopConfig::Atr { multiplier: m, .. } => {
|
StopConfig::Atr { multiplier: m, .. } => {
|
||||||
@@ -336,7 +392,7 @@ impl PortfolioEngine {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Calculate target price
|
// Calculate target price
|
||||||
let target_price = match self.config.target {
|
let target_price = match target_config {
|
||||||
TargetConfig::None => None,
|
TargetConfig::None => None,
|
||||||
TargetConfig::Fixed { percent } => Some(entry_price * (1.0 + multiplier * percent)),
|
TargetConfig::Fixed { percent } => Some(entry_price * (1.0 + multiplier * percent)),
|
||||||
TargetConfig::Atr { multiplier: m, .. } => {
|
TargetConfig::Atr { multiplier: m, .. } => {
|
||||||
|
|||||||
+105
-5
@@ -3,8 +3,11 @@
|
|||||||
use numpy::{PyArray1, PyReadonlyArray1};
|
use numpy::{PyArray1, PyReadonlyArray1};
|
||||||
use pyo3::prelude::*;
|
use pyo3::prelude::*;
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::core::types::{
|
use crate::core::types::{
|
||||||
BacktestConfig, CompiledSignals, Direction, OhlcvData, StopConfig, TargetConfig,
|
BacktestConfig, CompiledSignals, Direction, InstrumentConfig, OhlcvData, StopConfig,
|
||||||
|
TargetConfig,
|
||||||
};
|
};
|
||||||
use crate::indicators;
|
use crate::indicators;
|
||||||
use crate::signals::synchronizer::SyncMode;
|
use crate::signals::synchronizer::SyncMode;
|
||||||
@@ -100,6 +103,93 @@ impl From<&PyBacktestConfig> for BacktestConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Python-exposed per-instrument configuration.
|
||||||
|
#[pyclass]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PyInstrumentConfig {
|
||||||
|
#[pyo3(get, set)]
|
||||||
|
pub lot_size: Option<f64>,
|
||||||
|
#[pyo3(get, set)]
|
||||||
|
pub alloted_capital: Option<f64>,
|
||||||
|
#[pyo3(get, set)]
|
||||||
|
pub existing_qty: Option<f64>,
|
||||||
|
#[pyo3(get, set)]
|
||||||
|
pub avg_price: Option<f64>,
|
||||||
|
stop_config: Option<StopConfig>,
|
||||||
|
target_config: Option<TargetConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pymethods]
|
||||||
|
impl PyInstrumentConfig {
|
||||||
|
#[new]
|
||||||
|
#[pyo3(signature = (lot_size=None, alloted_capital=None, existing_qty=None, avg_price=None))]
|
||||||
|
fn new(
|
||||||
|
lot_size: Option<f64>,
|
||||||
|
alloted_capital: Option<f64>,
|
||||||
|
existing_qty: Option<f64>,
|
||||||
|
avg_price: Option<f64>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
lot_size,
|
||||||
|
alloted_capital,
|
||||||
|
existing_qty,
|
||||||
|
avg_price,
|
||||||
|
stop_config: None,
|
||||||
|
target_config: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set fixed percentage stop-loss override.
|
||||||
|
fn set_fixed_stop(&mut self, percent: f64) {
|
||||||
|
self.stop_config = Some(StopConfig::Fixed { percent });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set ATR-based stop-loss override.
|
||||||
|
fn set_atr_stop(&mut self, multiplier: f64, period: usize) {
|
||||||
|
self.stop_config = Some(StopConfig::Atr { multiplier, period });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set trailing stop-loss override.
|
||||||
|
fn set_trailing_stop(&mut self, percent: f64) {
|
||||||
|
self.stop_config = Some(StopConfig::Trailing { percent });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set fixed percentage take-profit override.
|
||||||
|
fn set_fixed_target(&mut self, percent: f64) {
|
||||||
|
self.target_config = Some(TargetConfig::Fixed { percent });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set ATR-based take-profit override.
|
||||||
|
fn set_atr_target(&mut self, multiplier: f64, period: usize) {
|
||||||
|
self.target_config = Some(TargetConfig::Atr { multiplier, period });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set risk-reward based take-profit override.
|
||||||
|
fn set_risk_reward_target(&mut self, ratio: f64) {
|
||||||
|
self.target_config = Some(TargetConfig::RiskReward { ratio });
|
||||||
|
}
|
||||||
|
|
||||||
|
fn __repr__(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"InstrumentConfig(lot_size={:?}, alloted_capital={:?})",
|
||||||
|
self.lot_size, self.alloted_capital
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&PyInstrumentConfig> for InstrumentConfig {
|
||||||
|
fn from(py_config: &PyInstrumentConfig) -> Self {
|
||||||
|
InstrumentConfig {
|
||||||
|
lot_size: py_config.lot_size,
|
||||||
|
alloted_capital: py_config.alloted_capital,
|
||||||
|
stop: py_config.stop_config,
|
||||||
|
target: py_config.target_config,
|
||||||
|
existing_qty: py_config.existing_qty,
|
||||||
|
avg_price: py_config.avg_price,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Python-exposed stop configuration.
|
/// Python-exposed stop configuration.
|
||||||
#[pyclass]
|
#[pyclass]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -419,7 +509,7 @@ impl PyBacktestResult {
|
|||||||
|
|
||||||
/// Run single instrument backtest.
|
/// Run single instrument backtest.
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
#[pyo3(signature = (timestamps, open, high, low, close, volume, entries, exits, direction=1, weight=1.0, symbol="UNKNOWN", config=None, position_sizes=None))]
|
#[pyo3(signature = (timestamps, open, high, low, close, volume, entries, exits, direction=1, weight=1.0, symbol="UNKNOWN", config=None, position_sizes=None, instrument_config=None))]
|
||||||
pub fn run_single_backtest<'py>(
|
pub fn run_single_backtest<'py>(
|
||||||
_py: Python<'py>,
|
_py: Python<'py>,
|
||||||
timestamps: PyReadonlyArray1<i64>,
|
timestamps: PyReadonlyArray1<i64>,
|
||||||
@@ -435,6 +525,7 @@ pub fn run_single_backtest<'py>(
|
|||||||
symbol: &str,
|
symbol: &str,
|
||||||
config: Option<&PyBacktestConfig>,
|
config: Option<&PyBacktestConfig>,
|
||||||
position_sizes: Option<PyReadonlyArray1<f64>>,
|
position_sizes: Option<PyReadonlyArray1<f64>>,
|
||||||
|
instrument_config: Option<&PyInstrumentConfig>,
|
||||||
) -> PyResult<PyBacktestResult> {
|
) -> PyResult<PyBacktestResult> {
|
||||||
let ohlcv = OhlcvData {
|
let ohlcv = OhlcvData {
|
||||||
timestamps: numpy_to_vec_i64(timestamps),
|
timestamps: numpy_to_vec_i64(timestamps),
|
||||||
@@ -457,16 +548,17 @@ pub fn run_single_backtest<'py>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let rust_config = config.map(|c| BacktestConfig::from(c)).unwrap_or_default();
|
let rust_config = config.map(|c| BacktestConfig::from(c)).unwrap_or_default();
|
||||||
|
let inst_config = instrument_config.map(InstrumentConfig::from);
|
||||||
|
|
||||||
let backtest = SingleBacktest::new(rust_config);
|
let backtest = SingleBacktest::new(rust_config);
|
||||||
let result = backtest.run(&ohlcv, &signals);
|
let result = backtest.run_with_instrument_config(&ohlcv, &signals, inst_config.as_ref());
|
||||||
|
|
||||||
Ok(convert_result(result))
|
Ok(convert_result(result))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run basket/collective backtest.
|
/// Run basket/collective backtest.
|
||||||
#[pyfunction]
|
#[pyfunction]
|
||||||
#[pyo3(signature = (instruments, config=None, sync_mode="all"))]
|
#[pyo3(signature = (instruments, config=None, sync_mode="all", instrument_configs=None))]
|
||||||
pub fn run_basket_backtest<'py>(
|
pub fn run_basket_backtest<'py>(
|
||||||
_py: Python<'py>,
|
_py: Python<'py>,
|
||||||
instruments: Vec<(
|
instruments: Vec<(
|
||||||
@@ -484,6 +576,7 @@ pub fn run_basket_backtest<'py>(
|
|||||||
)>,
|
)>,
|
||||||
config: Option<&PyBacktestConfig>,
|
config: Option<&PyBacktestConfig>,
|
||||||
sync_mode: &str,
|
sync_mode: &str,
|
||||||
|
instrument_configs: Option<HashMap<String, PyInstrumentConfig>>,
|
||||||
) -> PyResult<PyBacktestResult> {
|
) -> PyResult<PyBacktestResult> {
|
||||||
let rust_instruments: Vec<(OhlcvData, CompiledSignals)> = instruments
|
let rust_instruments: Vec<(OhlcvData, CompiledSignals)> = instruments
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -521,8 +614,15 @@ pub fn run_basket_backtest<'py>(
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Convert PyInstrumentConfig map to InstrumentConfig map
|
||||||
|
let rust_inst_configs: Option<HashMap<String, InstrumentConfig>> =
|
||||||
|
instrument_configs.map(|configs| {
|
||||||
|
configs.iter().map(|(k, v)| (k.clone(), InstrumentConfig::from(v))).collect()
|
||||||
|
});
|
||||||
|
|
||||||
let backtest = BasketBacktest::new(basket_config);
|
let backtest = BasketBacktest::new(basket_config);
|
||||||
let result = backtest.run(&rust_instruments);
|
let result =
|
||||||
|
backtest.run_with_instrument_configs(&rust_instruments, rust_inst_configs.as_ref());
|
||||||
|
|
||||||
Ok(convert_result(result))
|
Ok(convert_result(result))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,11 @@
|
|||||||
//!
|
//!
|
||||||
//! Supports multiple instruments with synchronized signals.
|
//! Supports multiple instruments with synchronized signals.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::core::types::{
|
use crate::core::types::{
|
||||||
BacktestConfig, BacktestMetrics, BacktestResult, CompiledSignals, ExitReason, OhlcvData, Trade,
|
BacktestConfig, BacktestMetrics, BacktestResult, CompiledSignals, ExitReason, InstrumentConfig,
|
||||||
|
OhlcvData, Trade,
|
||||||
};
|
};
|
||||||
use crate::execution::FeeModel;
|
use crate::execution::FeeModel;
|
||||||
use crate::metrics::streaming::StreamingMetrics;
|
use crate::metrics::streaming::StreamingMetrics;
|
||||||
@@ -74,6 +77,22 @@ impl BasketBacktest {
|
|||||||
/// # Returns
|
/// # Returns
|
||||||
/// Combined backtest result
|
/// Combined backtest result
|
||||||
pub fn run(&self, instruments: &[(OhlcvData, CompiledSignals)]) -> BacktestResult {
|
pub fn run(&self, instruments: &[(OhlcvData, CompiledSignals)]) -> BacktestResult {
|
||||||
|
self.run_with_instrument_configs(instruments, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run basket backtest with optional per-instrument configurations.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `instruments` - Vector of (OhlcvData, CompiledSignals) pairs for each instrument
|
||||||
|
/// * `instrument_configs` - Optional map of symbol -> InstrumentConfig
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Combined backtest result
|
||||||
|
pub fn run_with_instrument_configs(
|
||||||
|
&self,
|
||||||
|
instruments: &[(OhlcvData, CompiledSignals)],
|
||||||
|
instrument_configs: Option<&HashMap<String, InstrumentConfig>>,
|
||||||
|
) -> BacktestResult {
|
||||||
if instruments.is_empty() {
|
if instruments.is_empty() {
|
||||||
return self.empty_result();
|
return self.empty_result();
|
||||||
}
|
}
|
||||||
@@ -168,7 +187,15 @@ impl BasketBacktest {
|
|||||||
// Calculate position sizes
|
// Calculate position sizes
|
||||||
let prices: Vec<f64> = instruments.iter().map(|(o, _)| o.close[i]).collect();
|
let prices: Vec<f64> = instruments.iter().map(|(o, _)| o.close[i]).collect();
|
||||||
let weights: Vec<f64> = instruments.iter().map(|(_, s)| s.weight).collect();
|
let weights: Vec<f64> = instruments.iter().map(|(_, s)| s.weight).collect();
|
||||||
let sizes = self.calculate_sizes(&prices, &weights, cash);
|
let symbols: Vec<&str> =
|
||||||
|
instruments.iter().map(|(_, s)| s.symbol.as_str()).collect();
|
||||||
|
let sizes = self.calculate_sizes_with_configs(
|
||||||
|
&prices,
|
||||||
|
&weights,
|
||||||
|
cash,
|
||||||
|
&symbols,
|
||||||
|
instrument_configs,
|
||||||
|
);
|
||||||
|
|
||||||
// Enter positions
|
// Enter positions
|
||||||
for (inst_idx, (ohlcv, signals)) in instruments.iter().enumerate() {
|
for (inst_idx, (ohlcv, signals)) in instruments.iter().enumerate() {
|
||||||
@@ -249,7 +276,21 @@ impl BasketBacktest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate position sizes for each instrument.
|
/// Calculate position sizes for each instrument.
|
||||||
|
#[allow(dead_code)]
|
||||||
fn calculate_sizes(&self, prices: &[f64], weights: &[f64], available_capital: f64) -> Vec<f64> {
|
fn calculate_sizes(&self, prices: &[f64], weights: &[f64], available_capital: f64) -> Vec<f64> {
|
||||||
|
let symbols: Vec<&str> = vec![""; prices.len()];
|
||||||
|
self.calculate_sizes_with_configs(prices, weights, available_capital, &symbols, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate position sizes with optional per-instrument config (lot_size rounding, capital caps).
|
||||||
|
fn calculate_sizes_with_configs(
|
||||||
|
&self,
|
||||||
|
prices: &[f64],
|
||||||
|
weights: &[f64],
|
||||||
|
available_capital: f64,
|
||||||
|
symbols: &[&str],
|
||||||
|
instrument_configs: Option<&HashMap<String, InstrumentConfig>>,
|
||||||
|
) -> Vec<f64> {
|
||||||
let n = prices.len();
|
let n = prices.len();
|
||||||
let total_weight: f64 = weights.iter().sum();
|
let total_weight: f64 = weights.iter().sum();
|
||||||
|
|
||||||
@@ -260,12 +301,26 @@ impl BasketBacktest {
|
|||||||
prices
|
prices
|
||||||
.iter()
|
.iter()
|
||||||
.zip(weights.iter())
|
.zip(weights.iter())
|
||||||
.map(|(&price, &weight)| {
|
.enumerate()
|
||||||
|
.map(|(idx, (&price, &weight))| {
|
||||||
if price <= 0.0 {
|
if price <= 0.0 {
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
let allocation = available_capital * (weight / total_weight);
|
let default_allocation = available_capital * (weight / total_weight);
|
||||||
allocation / price
|
|
||||||
|
// Use per-instrument alloted_capital if set, capped at default allocation
|
||||||
|
let inst_config = instrument_configs
|
||||||
|
.and_then(|configs| symbols.get(idx).and_then(|sym| configs.get(*sym)));
|
||||||
|
|
||||||
|
let allocation = inst_config
|
||||||
|
.and_then(|ic| ic.alloted_capital)
|
||||||
|
.map(|cap| cap.min(default_allocation))
|
||||||
|
.unwrap_or(default_allocation);
|
||||||
|
|
||||||
|
let raw_size = allocation / price;
|
||||||
|
|
||||||
|
// Round to lot_size
|
||||||
|
inst_config.map(|ic| ic.round_to_lot(raw_size)).unwrap_or(raw_size)
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
//! Single instrument backtest implementation.
|
//! Single instrument backtest implementation.
|
||||||
|
|
||||||
use crate::core::types::{BacktestConfig, BacktestResult, CompiledSignals, OhlcvData};
|
use crate::core::types::{
|
||||||
|
BacktestConfig, BacktestResult, CompiledSignals, InstrumentConfig, OhlcvData,
|
||||||
|
};
|
||||||
use crate::portfolio::engine::PortfolioEngine;
|
use crate::portfolio::engine::PortfolioEngine;
|
||||||
|
|
||||||
/// Single instrument backtest runner.
|
/// Single instrument backtest runner.
|
||||||
@@ -28,6 +30,24 @@ impl SingleBacktest {
|
|||||||
self.engine.run_single(ohlcv, signals)
|
self.engine.run_single(ohlcv, signals)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run the backtest with per-instrument configuration.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `ohlcv` - OHLCV price data
|
||||||
|
/// * `signals` - Compiled trading signals
|
||||||
|
/// * `inst_config` - Optional per-instrument config (lot_size, capital cap, stop/target overrides)
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Backtest result with metrics, trades, and equity curve
|
||||||
|
pub fn run_with_instrument_config(
|
||||||
|
&self,
|
||||||
|
ohlcv: &OhlcvData,
|
||||||
|
signals: &CompiledSignals,
|
||||||
|
inst_config: Option<&InstrumentConfig>,
|
||||||
|
) -> BacktestResult {
|
||||||
|
self.engine.run_single_with_instrument_config(ohlcv, signals, inst_config)
|
||||||
|
}
|
||||||
|
|
||||||
/// Run backtest from raw arrays.
|
/// Run backtest from raw arrays.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
|||||||
Reference in New Issue
Block a user