Files
raptorbt/src/strategies/single.rs
T

221 lines
6.8 KiB
Rust

//! Single instrument backtest implementation.
use crate::core::types::{
BacktestConfig, BacktestResult, CompiledSignals, InstrumentConfig, OhlcvData,
};
use crate::portfolio::engine::PortfolioEngine;
/// Single instrument backtest runner.
#[derive(Debug)]
pub struct SingleBacktest {
/// Portfolio engine.
engine: PortfolioEngine,
}
impl SingleBacktest {
/// Create a new single instrument backtest.
pub fn new(config: BacktestConfig) -> Self {
Self { engine: PortfolioEngine::new(config) }
}
/// Run the backtest.
///
/// # Arguments
/// * `ohlcv` - OHLCV price data
/// * `signals` - Compiled trading signals
///
/// # Returns
/// Backtest result with metrics, trades, and equity curve
pub fn run(&self, ohlcv: &OhlcvData, signals: &CompiledSignals) -> BacktestResult {
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.
///
/// # Arguments
/// * `timestamps` - Timestamp array
/// * `open` - Open prices
/// * `high` - High prices
/// * `low` - Low prices
/// * `close` - Close prices
/// * `volume` - Volume
/// * `entries` - Entry signals
/// * `exits` - Exit signals
/// * `direction` - Trade direction (1 = long, -1 = short)
/// * `symbol` - Symbol name
///
/// # Returns
/// Backtest result
pub fn run_from_arrays(
&self,
timestamps: &[i64],
open: &[f64],
high: &[f64],
low: &[f64],
close: &[f64],
volume: &[f64],
entries: &[bool],
exits: &[bool],
direction: i32,
symbol: &str,
) -> BacktestResult {
let ohlcv = OhlcvData {
timestamps: timestamps.to_vec(),
open: open.to_vec(),
high: high.to_vec(),
low: low.to_vec(),
close: close.to_vec(),
volume: volume.to_vec(),
};
let dir = crate::core::types::Direction::from_int(direction)
.unwrap_or(crate::core::types::Direction::Long);
let signals = CompiledSignals {
symbol: symbol.to_string(),
entries: entries.to_vec(),
exits: exits.to_vec(),
position_sizes: None,
direction: dir,
weight: 1.0,
};
self.run(&ohlcv, &signals)
}
/// Run backtest with position sizing.
///
/// # Arguments
/// * `ohlcv` - OHLCV price data
/// * `signals` - Compiled trading signals
/// * `position_sizes` - Position size for each bar (fraction of capital)
///
/// # Returns
/// Backtest result
pub fn run_with_sizing(
&self,
ohlcv: &OhlcvData,
signals: &CompiledSignals,
position_sizes: Vec<f64>,
) -> BacktestResult {
let mut signals_with_sizing = signals.clone();
signals_with_sizing.position_sizes = Some(position_sizes);
self.engine.run_single(ohlcv, &signals_with_sizing)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::types::{Direction, StopConfig, TargetConfig};
fn sample_data() -> (OhlcvData, CompiledSignals) {
let ohlcv = OhlcvData {
timestamps: (0..20).map(|i| i as i64).collect(),
open: vec![
100.0, 101.0, 102.0, 103.0, 104.0, 105.0, 104.0, 103.0, 102.0, 101.0, 100.0, 101.0,
102.0, 103.0, 104.0, 105.0, 106.0, 107.0, 108.0, 109.0,
],
high: vec![
101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 105.0, 104.0, 103.0, 102.0, 101.0, 102.0,
103.0, 104.0, 105.0, 106.0, 107.0, 108.0, 109.0, 110.0,
],
low: vec![
99.0, 100.0, 101.0, 102.0, 103.0, 104.0, 103.0, 102.0, 101.0, 100.0, 99.0, 100.0,
101.0, 102.0, 103.0, 104.0, 105.0, 106.0, 107.0, 108.0,
],
close: vec![
100.5, 101.5, 102.5, 103.5, 104.5, 105.0, 104.0, 103.0, 102.0, 101.0, 100.5, 101.5,
102.5, 103.5, 104.5, 105.5, 106.5, 107.5, 108.5, 109.5,
],
volume: vec![1000.0; 20],
};
let signals = CompiledSignals {
symbol: "TEST".to_string(),
entries: vec![
false, true, false, false, false, false, false, false, false, false, false, true,
false, false, false, false, false, false, false, false,
],
exits: vec![
false, false, false, false, false, true, false, false, false, false, false, false,
false, false, false, true, false, false, false, false,
],
position_sizes: None,
direction: Direction::Long,
weight: 1.0,
};
(ohlcv, signals)
}
#[test]
fn test_single_backtest() {
let config = BacktestConfig {
initial_capital: 100_000.0,
fees: 0.0,
slippage: 0.0,
stop: StopConfig::None,
target: TargetConfig::None,
upon_bar_close: true,
};
let backtest = SingleBacktest::new(config);
let (ohlcv, signals) = sample_data();
let result = backtest.run(&ohlcv, &signals);
assert_eq!(result.trades.len(), 2);
assert!(result.metrics.total_return_pct > 0.0);
}
#[test]
fn test_from_arrays() {
let config = BacktestConfig::default();
let backtest = SingleBacktest::new(config);
let timestamps: Vec<i64> = (0..10).collect();
let close: Vec<f64> = (100..110).map(|x| x as f64).collect();
let open = close.clone();
let high: Vec<f64> = close.iter().map(|x| x + 1.0).collect();
let low: Vec<f64> = close.iter().map(|x| x - 1.0).collect();
let volume = vec![1000.0; 10];
let entries = vec![false, true, false, false, false, false, false, false, false, false];
let exits = vec![false, false, false, false, false, true, false, false, false, false];
let result = backtest.run_from_arrays(
&timestamps,
&open,
&high,
&low,
&close,
&volume,
&entries,
&exits,
1,
"TEST",
);
assert_eq!(result.trades.len(), 1);
}
}