Initial backtesting engine
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
//! Slippage models for realistic trade execution.
|
||||
|
||||
use crate::core::types::{Direction, Price};
|
||||
|
||||
/// Slippage model for simulating execution price deviation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SlippageModel {
|
||||
/// No slippage.
|
||||
None,
|
||||
/// Fixed percentage slippage.
|
||||
Percentage(f64),
|
||||
/// Fixed point slippage.
|
||||
Fixed(f64),
|
||||
/// Volume-based slippage (higher volume = lower slippage).
|
||||
VolumeBased { base: f64, volume_factor: f64 },
|
||||
/// Spread-based slippage (uses bid-ask spread).
|
||||
SpreadBased { half_spread: f64 },
|
||||
}
|
||||
|
||||
impl Default for SlippageModel {
|
||||
fn default() -> Self {
|
||||
SlippageModel::None
|
||||
}
|
||||
}
|
||||
|
||||
impl SlippageModel {
|
||||
/// Create a new percentage slippage model.
|
||||
pub fn percentage(rate: f64) -> Self {
|
||||
SlippageModel::Percentage(rate)
|
||||
}
|
||||
|
||||
/// Create a new fixed slippage model.
|
||||
pub fn fixed(points: f64) -> Self {
|
||||
SlippageModel::Fixed(points)
|
||||
}
|
||||
|
||||
/// Create a volume-based slippage model.
|
||||
pub fn volume_based(base: f64, volume_factor: f64) -> Self {
|
||||
SlippageModel::VolumeBased {
|
||||
base,
|
||||
volume_factor,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate slippage for a trade.
|
||||
///
|
||||
/// For long entries and short exits: slippage is ADDED to price (pay more/receive less)
|
||||
/// For short entries and long exits: slippage is SUBTRACTED from price
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `price` - Base execution price
|
||||
/// * `direction` - Trade direction
|
||||
/// * `is_entry` - Whether this is an entry or exit
|
||||
/// * `volume` - Optional volume for volume-based models
|
||||
///
|
||||
/// # Returns
|
||||
/// Slippage amount (positive = unfavorable)
|
||||
pub fn calculate(
|
||||
&self,
|
||||
price: Price,
|
||||
direction: Direction,
|
||||
is_entry: bool,
|
||||
volume: Option<f64>,
|
||||
) -> f64 {
|
||||
let base_slippage = match self {
|
||||
SlippageModel::None => 0.0,
|
||||
SlippageModel::Percentage(rate) => price * rate,
|
||||
SlippageModel::Fixed(points) => *points,
|
||||
SlippageModel::VolumeBased {
|
||||
base,
|
||||
volume_factor,
|
||||
} => {
|
||||
if let Some(vol) = volume {
|
||||
if vol > 0.0 {
|
||||
base * (1.0 / (1.0 + vol * volume_factor))
|
||||
} else {
|
||||
*base
|
||||
}
|
||||
} else {
|
||||
*base
|
||||
}
|
||||
}
|
||||
SlippageModel::SpreadBased { half_spread } => *half_spread,
|
||||
};
|
||||
|
||||
// Determine sign based on trade type
|
||||
// Long entry: pay higher price (positive slippage)
|
||||
// Long exit: receive lower price (negative slippage)
|
||||
// Short entry: receive higher price (negative slippage means worse)
|
||||
// Short exit: pay higher price
|
||||
match (direction, is_entry) {
|
||||
(Direction::Long, true) => base_slippage, // Pay more
|
||||
(Direction::Long, false) => -base_slippage, // Receive less
|
||||
(Direction::Short, true) => -base_slippage, // Receive less
|
||||
(Direction::Short, false) => base_slippage, // Pay more
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply slippage to get execution price.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `price` - Base price
|
||||
/// * `direction` - Trade direction
|
||||
/// * `is_entry` - Whether this is an entry or exit
|
||||
/// * `volume` - Optional volume for volume-based models
|
||||
///
|
||||
/// # Returns
|
||||
/// Execution price after slippage
|
||||
pub fn apply(
|
||||
&self,
|
||||
price: Price,
|
||||
direction: Direction,
|
||||
is_entry: bool,
|
||||
volume: Option<f64>,
|
||||
) -> Price {
|
||||
price + self.calculate(price, direction, is_entry, volume)
|
||||
}
|
||||
}
|
||||
|
||||
/// Market impact model for large orders.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MarketImpact {
|
||||
/// Temporary impact coefficient.
|
||||
pub temporary_impact: f64,
|
||||
/// Permanent impact coefficient.
|
||||
pub permanent_impact: f64,
|
||||
/// Average daily volume for normalization.
|
||||
pub avg_daily_volume: f64,
|
||||
}
|
||||
|
||||
impl MarketImpact {
|
||||
/// Create a new market impact model.
|
||||
pub fn new(temporary: f64, permanent: f64, adv: f64) -> Self {
|
||||
Self {
|
||||
temporary_impact: temporary,
|
||||
permanent_impact: permanent,
|
||||
avg_daily_volume: adv,
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate market impact for an order.
|
||||
///
|
||||
/// Uses simplified square-root model: impact = sigma * sqrt(Q / ADV)
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `order_size` - Number of shares/contracts
|
||||
/// * `price` - Current price
|
||||
/// * `volatility` - Price volatility (sigma)
|
||||
///
|
||||
/// # Returns
|
||||
/// Total market impact in price terms
|
||||
pub fn calculate(&self, order_size: f64, price: Price, volatility: f64) -> f64 {
|
||||
if self.avg_daily_volume <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let participation_rate = order_size / self.avg_daily_volume;
|
||||
let sqrt_participation = participation_rate.sqrt();
|
||||
|
||||
let temporary = self.temporary_impact * volatility * price * sqrt_participation;
|
||||
let permanent = self.permanent_impact * volatility * price * participation_rate;
|
||||
|
||||
temporary + permanent
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_percentage_slippage() {
|
||||
let slip = SlippageModel::percentage(0.001);
|
||||
|
||||
// Long entry: pay more
|
||||
let entry_slip = slip.calculate(100.0, Direction::Long, true, None);
|
||||
assert!((entry_slip - 0.1).abs() < 1e-10);
|
||||
|
||||
// Long exit: receive less
|
||||
let exit_slip = slip.calculate(100.0, Direction::Long, false, None);
|
||||
assert!((exit_slip - (-0.1)).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_slippage() {
|
||||
let slip = SlippageModel::percentage(0.001);
|
||||
|
||||
// Long entry at 100 should pay 100.1
|
||||
let entry_price = slip.apply(100.0, Direction::Long, true, None);
|
||||
assert!((entry_price - 100.1).abs() < 1e-10);
|
||||
|
||||
// Long exit at 100 should receive 99.9
|
||||
let exit_price = slip.apply(100.0, Direction::Long, false, None);
|
||||
assert!((exit_price - 99.9).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_slippage() {
|
||||
let slip = SlippageModel::None;
|
||||
let result = slip.apply(100.0, Direction::Long, true, None);
|
||||
assert!((result - 100.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_volume_based_slippage() {
|
||||
let slip = SlippageModel::volume_based(0.1, 0.0001);
|
||||
|
||||
// High volume should have lower slippage
|
||||
let high_vol = slip.calculate(100.0, Direction::Long, true, Some(100000.0));
|
||||
let low_vol = slip.calculate(100.0, Direction::Long, true, Some(1000.0));
|
||||
|
||||
assert!(high_vol < low_vol);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user