0f032dd39e
* Fix CI workflow: use correct rust-toolchain action * Fix CI: resolve Rust compilation and formatting issues - Fix rust-toolchain action name in CI workflow - Add missing Direction import in test modules - Add missing entry_fees argument to open_position test calls - Comment out nightly-only rustfmt options - Auto-format code with cargo fmt
396 lines
12 KiB
Rust
396 lines
12 KiB
Rust
//! 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);
|
|
}
|
|
}
|