Initial backtesting engine
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
//! Trailing stop implementations.
|
||||
|
||||
use super::StopCalculator;
|
||||
use crate::core::types::{Direction, Price};
|
||||
|
||||
/// Percentage-based trailing stop.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct TrailingStop {
|
||||
/// Trail percentage (e.g., 0.05 for 5%).
|
||||
pub percent: f64,
|
||||
/// Activation threshold (optional - start trailing after this profit %).
|
||||
pub activation_threshold: Option<f64>,
|
||||
}
|
||||
|
||||
impl TrailingStop {
|
||||
/// Create a new trailing stop.
|
||||
pub fn new(percent: f64) -> Self {
|
||||
Self {
|
||||
percent: percent.abs(),
|
||||
activation_threshold: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create with activation threshold.
|
||||
pub fn with_activation(mut self, threshold: f64) -> Self {
|
||||
self.activation_threshold = Some(threshold.abs());
|
||||
self
|
||||
}
|
||||
|
||||
/// Check if trailing should be activated.
|
||||
#[allow(dead_code)]
|
||||
fn should_activate(
|
||||
&self,
|
||||
entry_price: Price,
|
||||
current_price: Price,
|
||||
direction: Direction,
|
||||
) -> bool {
|
||||
if let Some(threshold) = self.activation_threshold {
|
||||
let profit_pct = match direction {
|
||||
Direction::Long => (current_price - entry_price) / entry_price,
|
||||
Direction::Short => (entry_price - current_price) / entry_price,
|
||||
};
|
||||
profit_pct >= threshold
|
||||
} else {
|
||||
true // Always active if no threshold
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StopCalculator for TrailingStop {
|
||||
fn calculate_stop(&self, entry_price: Price, direction: Direction) -> Option<Price> {
|
||||
let stop = match direction {
|
||||
Direction::Long => entry_price * (1.0 - self.percent),
|
||||
Direction::Short => entry_price * (1.0 + self.percent),
|
||||
};
|
||||
Some(stop)
|
||||
}
|
||||
|
||||
fn update_stop(
|
||||
&self,
|
||||
current_stop: Option<Price>,
|
||||
_current_price: Price,
|
||||
high: Price,
|
||||
low: Price,
|
||||
direction: Direction,
|
||||
) -> Option<Price> {
|
||||
match direction {
|
||||
Direction::Long => {
|
||||
// Trail below the high
|
||||
let new_stop = high * (1.0 - self.percent);
|
||||
current_stop.map(|cs| cs.max(new_stop)).or(Some(new_stop))
|
||||
}
|
||||
Direction::Short => {
|
||||
// Trail above the low
|
||||
let new_stop = low * (1.0 + self.percent);
|
||||
current_stop.map(|cs| cs.min(new_stop)).or(Some(new_stop))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Point-based trailing stop (fixed point distance).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct PointTrailingStop {
|
||||
/// Trail distance in points.
|
||||
pub points: f64,
|
||||
}
|
||||
|
||||
impl PointTrailingStop {
|
||||
/// Create a new point-based trailing stop.
|
||||
pub fn new(points: f64) -> Self {
|
||||
Self {
|
||||
points: points.abs(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StopCalculator for PointTrailingStop {
|
||||
fn calculate_stop(&self, entry_price: Price, direction: Direction) -> Option<Price> {
|
||||
let stop = match direction {
|
||||
Direction::Long => entry_price - self.points,
|
||||
Direction::Short => entry_price + self.points,
|
||||
};
|
||||
Some(stop)
|
||||
}
|
||||
|
||||
fn update_stop(
|
||||
&self,
|
||||
current_stop: Option<Price>,
|
||||
_current_price: Price,
|
||||
high: Price,
|
||||
low: Price,
|
||||
direction: Direction,
|
||||
) -> Option<Price> {
|
||||
match direction {
|
||||
Direction::Long => {
|
||||
let new_stop = high - self.points;
|
||||
current_stop.map(|cs| cs.max(new_stop)).or(Some(new_stop))
|
||||
}
|
||||
Direction::Short => {
|
||||
let new_stop = low + self.points;
|
||||
current_stop.map(|cs| cs.min(new_stop)).or(Some(new_stop))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Step trailing stop (moves in discrete steps).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct StepTrailingStop {
|
||||
/// Step size percentage.
|
||||
pub step_percent: f64,
|
||||
/// Trail percentage from each step.
|
||||
pub trail_percent: f64,
|
||||
}
|
||||
|
||||
impl StepTrailingStop {
|
||||
/// Create a new step trailing stop.
|
||||
pub fn new(step_percent: f64, trail_percent: f64) -> Self {
|
||||
Self {
|
||||
step_percent: step_percent.abs(),
|
||||
trail_percent: trail_percent.abs(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate stop for a given step level.
|
||||
fn stop_for_step(&self, entry_price: Price, step: usize, direction: Direction) -> Price {
|
||||
let step_gain = self.step_percent * step as f64;
|
||||
match direction {
|
||||
Direction::Long => {
|
||||
let step_price = entry_price * (1.0 + step_gain);
|
||||
step_price * (1.0 - self.trail_percent)
|
||||
}
|
||||
Direction::Short => {
|
||||
let step_price = entry_price * (1.0 - step_gain);
|
||||
step_price * (1.0 + self.trail_percent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine current step level.
|
||||
#[allow(dead_code)]
|
||||
fn current_step(
|
||||
&self,
|
||||
entry_price: Price,
|
||||
extreme_price: Price,
|
||||
direction: Direction,
|
||||
) -> usize {
|
||||
let gain = match direction {
|
||||
Direction::Long => (extreme_price - entry_price) / entry_price,
|
||||
Direction::Short => (entry_price - extreme_price) / entry_price,
|
||||
};
|
||||
|
||||
if gain <= 0.0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
(gain / self.step_percent).floor() as usize
|
||||
}
|
||||
}
|
||||
|
||||
impl StopCalculator for StepTrailingStop {
|
||||
fn calculate_stop(&self, entry_price: Price, direction: Direction) -> Option<Price> {
|
||||
Some(self.stop_for_step(entry_price, 0, direction))
|
||||
}
|
||||
|
||||
fn update_stop(
|
||||
&self,
|
||||
current_stop: Option<Price>,
|
||||
_current_price: Price,
|
||||
high: Price,
|
||||
low: Price,
|
||||
direction: Direction,
|
||||
) -> Option<Price> {
|
||||
// This is a simplified version - full implementation would need entry price
|
||||
// For now, just use regular trailing behavior
|
||||
match direction {
|
||||
Direction::Long => {
|
||||
let new_stop = high * (1.0 - self.trail_percent);
|
||||
current_stop.map(|cs| cs.max(new_stop)).or(Some(new_stop))
|
||||
}
|
||||
Direction::Short => {
|
||||
let new_stop = low * (1.0 + self.trail_percent);
|
||||
current_stop.map(|cs| cs.min(new_stop)).or(Some(new_stop))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parabolic SAR style trailing stop.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParabolicStop {
|
||||
/// Initial acceleration factor.
|
||||
pub af_start: f64,
|
||||
/// Acceleration factor increment.
|
||||
pub af_step: f64,
|
||||
/// Maximum acceleration factor.
|
||||
pub af_max: f64,
|
||||
/// Current acceleration factor.
|
||||
current_af: f64,
|
||||
/// Current extreme point.
|
||||
extreme_point: f64,
|
||||
/// Current SAR value.
|
||||
current_sar: f64,
|
||||
}
|
||||
|
||||
impl ParabolicStop {
|
||||
/// Create a new Parabolic SAR stop with default parameters.
|
||||
pub fn new() -> Self {
|
||||
Self::with_params(0.02, 0.02, 0.2)
|
||||
}
|
||||
|
||||
/// Create with custom parameters.
|
||||
pub fn with_params(af_start: f64, af_step: f64, af_max: f64) -> Self {
|
||||
Self {
|
||||
af_start,
|
||||
af_step,
|
||||
af_max,
|
||||
current_af: af_start,
|
||||
extreme_point: 0.0,
|
||||
current_sar: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize for new position.
|
||||
pub fn init(&mut self, entry_price: Price, direction: Direction) {
|
||||
self.current_af = self.af_start;
|
||||
self.extreme_point = entry_price;
|
||||
self.current_sar = match direction {
|
||||
Direction::Long => entry_price * 0.99, // Slightly below entry
|
||||
Direction::Short => entry_price * 1.01, // Slightly above entry
|
||||
};
|
||||
}
|
||||
|
||||
/// Update SAR with new bar data.
|
||||
pub fn update_sar(&mut self, high: Price, low: Price, direction: Direction) -> Price {
|
||||
// Update extreme point
|
||||
let new_ep = match direction {
|
||||
Direction::Long => {
|
||||
if high > self.extreme_point {
|
||||
self.current_af = (self.current_af + self.af_step).min(self.af_max);
|
||||
high
|
||||
} else {
|
||||
self.extreme_point
|
||||
}
|
||||
}
|
||||
Direction::Short => {
|
||||
if low < self.extreme_point {
|
||||
self.current_af = (self.current_af + self.af_step).min(self.af_max);
|
||||
low
|
||||
} else {
|
||||
self.extreme_point
|
||||
}
|
||||
}
|
||||
};
|
||||
self.extreme_point = new_ep;
|
||||
|
||||
// Calculate new SAR
|
||||
let new_sar = self.current_sar + self.current_af * (self.extreme_point - self.current_sar);
|
||||
|
||||
// Ensure SAR doesn't cross price
|
||||
self.current_sar = match direction {
|
||||
Direction::Long => new_sar.min(low),
|
||||
Direction::Short => new_sar.max(high),
|
||||
};
|
||||
|
||||
self.current_sar
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ParabolicStop {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl StopCalculator for ParabolicStop {
|
||||
fn calculate_stop(&self, _entry_price: Price, _direction: Direction) -> Option<Price> {
|
||||
if self.current_sar > 0.0 {
|
||||
Some(self.current_sar)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn update_stop(
|
||||
&self,
|
||||
_current_stop: Option<Price>,
|
||||
_current_price: Price,
|
||||
_high: Price,
|
||||
_low: Price,
|
||||
_direction: Direction,
|
||||
) -> Option<Price> {
|
||||
// Parabolic stop is updated via update_sar method
|
||||
if self.current_sar > 0.0 {
|
||||
Some(self.current_sar)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_trailing_stop_long() {
|
||||
let stop = TrailingStop::new(0.05);
|
||||
|
||||
// Initial stop
|
||||
let initial = stop.calculate_stop(100.0, Direction::Long);
|
||||
assert!((initial.unwrap() - 95.0).abs() < 1e-10);
|
||||
|
||||
// Update with higher high
|
||||
let updated = stop.update_stop(initial, 108.0, 110.0, 105.0, Direction::Long);
|
||||
// 110 * 0.95 = 104.5
|
||||
assert!((updated.unwrap() - 104.5).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trailing_stop_short() {
|
||||
let stop = TrailingStop::new(0.05);
|
||||
|
||||
// Initial stop
|
||||
let initial = stop.calculate_stop(100.0, Direction::Short);
|
||||
assert!((initial.unwrap() - 105.0).abs() < 1e-10);
|
||||
|
||||
// Update with lower low
|
||||
let updated = stop.update_stop(initial, 92.0, 95.0, 90.0, Direction::Short);
|
||||
// 90 * 1.05 = 94.5
|
||||
assert!((updated.unwrap() - 94.5).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_trailing_stop_only_tightens() {
|
||||
let stop = TrailingStop::new(0.05);
|
||||
|
||||
let initial = stop.calculate_stop(100.0, Direction::Long);
|
||||
|
||||
// Move up
|
||||
let moved_up = stop.update_stop(initial, 110.0, 110.0, 108.0, Direction::Long);
|
||||
// 110 * 0.95 = 104.5
|
||||
assert!((moved_up.unwrap() - 104.5).abs() < 1e-10);
|
||||
|
||||
// Move down - stop should NOT move down
|
||||
let moved_down = stop.update_stop(moved_up, 105.0, 106.0, 103.0, Direction::Long);
|
||||
// Should still be 104.5 (not 106 * 0.95 = 100.7)
|
||||
assert!((moved_down.unwrap() - 104.5).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_point_trailing_stop() {
|
||||
let stop = PointTrailingStop::new(5.0);
|
||||
|
||||
// Initial stop
|
||||
let initial = stop.calculate_stop(100.0, Direction::Long);
|
||||
assert!((initial.unwrap() - 95.0).abs() < 1e-10);
|
||||
|
||||
// Update with higher high
|
||||
let updated = stop.update_stop(initial, 108.0, 110.0, 105.0, Direction::Long);
|
||||
// 110 - 5 = 105
|
||||
assert!((updated.unwrap() - 105.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parabolic_stop() {
|
||||
let mut stop = ParabolicStop::new();
|
||||
stop.init(100.0, Direction::Long);
|
||||
|
||||
// Simulate uptrend
|
||||
let sar1 = stop.update_sar(102.0, 99.0, Direction::Long);
|
||||
let sar2 = stop.update_sar(105.0, 101.0, Direction::Long);
|
||||
let sar3 = stop.update_sar(108.0, 103.0, Direction::Long);
|
||||
|
||||
// SAR should be increasing
|
||||
assert!(sar2 > sar1);
|
||||
assert!(sar3 > sar2);
|
||||
|
||||
// SAR should be below current low
|
||||
assert!(sar3 < 103.0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user