use crate::bar_indicators::indicator_value::IndicatorValue;
#[derive(Debug, Clone)]
pub struct SwingStop {
lookback: usize, min_swing_size: f64, offset: f64, use_percentage: bool,
highs: Vec<f64>,
lows: Vec<f64>,
last_swing_high: f64,
last_swing_low: f64,
long_stop: f64, short_stop: f64,
bars_count: usize,
is_ready: bool,
}
impl SwingStop {
pub fn new() -> Self {
Self::with_params(5, 0.0, 0.0, false)
}
pub fn with_params(lookback: usize, min_swing_size: f64, offset: f64, use_percentage: bool) -> Self {
assert!(lookback > 0, "Lookback must be greater than 0");
Self {
lookback,
min_swing_size,
offset,
use_percentage,
highs: Vec::with_capacity(1024),
lows: Vec::with_capacity(1024),
last_swing_high: 0.0,
last_swing_low: f64::MAX,
long_stop: 0.0,
short_stop: 0.0,
bars_count: 0,
is_ready: false,
}
}
pub fn for_optimization(lookback: usize, min_swing_size: f64, offset: f64, use_percentage: bool) -> Self {
Self::with_params(lookback, min_swing_size, offset, use_percentage)
}
pub fn update_bar(&mut self, _open: f64, high: f64, low: f64, _close: f64, _volume: f64) -> (f64, f64) {
self.bars_count += 1;
let buffer_size = self.lookback * 2 + 10; if self.highs.len() >= buffer_size {
self.highs.remove(0);
self.lows.remove(0);
}
self.highs.push(high);
self.lows.push(low);
if self.highs.len() >= (self.lookback * 2 + 1) {
self.find_swings();
}
self.calculate_stop_levels();
self.is_ready = self.bars_count >= (self.lookback * 2 + 1) &&
(self.last_swing_high > 0.0 || self.last_swing_low < f64::MAX);
(self.long_stop, self.short_stop)
}
fn find_swings(&mut self) {
let len = self.highs.len();
if len < (self.lookback * 2 + 1) {
return;
}
let swing_index = len - self.lookback - 1;
let candidate_high = self.highs[swing_index];
let mut is_swing_high = true;
for i in (swing_index.saturating_sub(self.lookback))..swing_index {
if self.highs[i] >= candidate_high {
is_swing_high = false;
break;
}
}
if is_swing_high {
for i in (swing_index + 1)..=(swing_index + self.lookback).min(len - 1) {
if self.highs[i] >= candidate_high {
is_swing_high = false;
break;
}
}
}
if is_swing_high && self.min_swing_size > 0.0
&& self.last_swing_high > 0.0 {
let swing_size = (candidate_high - self.last_swing_high).abs();
if swing_size < self.min_swing_size {
is_swing_high = false;
}
}
if is_swing_high {
self.last_swing_high = candidate_high;
}
let candidate_low = self.lows[swing_index];
let mut is_swing_low = true;
for i in (swing_index.saturating_sub(self.lookback))..swing_index {
if self.lows[i] <= candidate_low {
is_swing_low = false;
break;
}
}
if is_swing_low {
for i in (swing_index + 1)..=(swing_index + self.lookback).min(len - 1) {
if self.lows[i] <= candidate_low {
is_swing_low = false;
break;
}
}
}
if is_swing_low && self.min_swing_size > 0.0
&& self.last_swing_low < f64::MAX {
let swing_size = (self.last_swing_low - candidate_low).abs();
if swing_size < self.min_swing_size {
is_swing_low = false;
}
}
if is_swing_low {
self.last_swing_low = candidate_low;
}
}
fn calculate_stop_levels(&mut self) {
if self.last_swing_low < f64::MAX {
if self.use_percentage {
self.long_stop = self.last_swing_low * (1.0 - self.offset / 100.0);
} else {
self.long_stop = self.last_swing_low - self.offset;
}
}
if self.last_swing_high > 0.0 {
if self.use_percentage {
self.short_stop = self.last_swing_high * (1.0 + self.offset / 100.0);
} else {
self.short_stop = self.last_swing_high + self.offset;
}
}
}
pub fn long_stop(&self) -> f64 {
self.long_stop
}
pub fn short_stop(&self) -> f64 {
self.short_stop
}
pub fn value(&self) -> IndicatorValue {
IndicatorValue::Single(self.long_stop)
}
pub fn levels(&self) -> (f64, f64) {
(self.long_stop, self.short_stop)
}
pub fn last_swing_high(&self) -> f64 {
self.last_swing_high
}
pub fn last_swing_low(&self) -> f64 {
if self.last_swing_low == f64::MAX { 0.0 } else { self.last_swing_low }
}
pub fn is_ready(&self) -> bool {
self.is_ready
}
pub fn reset(&mut self) {
self.highs.clear();
self.lows.clear();
self.last_swing_high = 0.0;
self.last_swing_low = f64::MAX;
self.long_stop = 0.0;
self.short_stop = 0.0;
self.bars_count = 0;
self.is_ready = false;
}
pub fn params(&self) -> (usize, f64, f64, bool) {
(self.lookback, self.min_swing_size, self.offset, self.use_percentage)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_swing_stop_creation() {
let ind = SwingStop::new();
assert!(!ind.is_ready());
assert_eq!(ind.value().main(), 0.0);
}
#[test]
fn test_swing_stop_with_params() {
let ind = SwingStop::with_params(5, 0.0, 0.5, false);
assert!(!ind.is_ready());
assert_eq!(ind.params(), (5, 0.0, 0.5, false));
}
#[test]
fn test_swing_stop_warmup() {
let mut ind = SwingStop::with_params(3, 0.0, 0.0, false);
for i in 0..20 {
let price = 100.0 + (i as f64 * 0.3).sin() * 5.0;
ind.update_bar(price, price + 1.0, price - 1.0, price, 1000.0);
}
assert!(ind.is_ready());
}
#[test]
fn test_swing_stop_values_finite() {
let mut ind = SwingStop::with_params(3, 0.0, 0.0, false);
for i in 0..30 {
let price = 100.0 + (i as f64 * 0.3).sin() * 10.0;
let (long, short) = ind.update_bar(price, price + 1.0, price - 1.0, price, 1000.0);
assert!(long.is_finite());
assert!(short.is_finite());
}
}
#[test]
fn test_swing_stop_reset() {
let mut ind = SwingStop::with_params(3, 0.0, 0.0, false);
for i in 0..20 {
ind.update_bar(100.0 + (i as f64 * 0.3).sin() * 5.0, 105.0, 95.0, 101.0, 1000.0);
}
ind.reset();
assert!(!ind.is_ready());
assert_eq!(ind.value().main(), 0.0);
}
}