221 lines
6.8 KiB
Rust
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(
|
|
×tamps,
|
|
&open,
|
|
&high,
|
|
&low,
|
|
&close,
|
|
&volume,
|
|
&entries,
|
|
&exits,
|
|
1,
|
|
"TEST",
|
|
);
|
|
|
|
assert_eq!(result.trades.len(), 1);
|
|
}
|
|
}
|