Files
raptorbt/src/stops/trailing.rs
T
vatsal 0f032dd39e Fix CI workflow: use correct rust-toolchain action (#1)
* 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
2026-01-28 15:38:31 +05:30

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);
}
}