use crate::bar_indicators::indicator_value::IndicatorValue;
#[derive(Debug, Clone)]
pub struct DonchianStop {
upper_period: usize, lower_period: usize, offset: f64, use_percentage: bool,
highs: Vec<f64>,
lows: Vec<f64>,
upper_line: f64, lower_line: f64, middle_line: f64,
upper_stop: f64, lower_stop: f64,
bars_count: usize,
is_ready: bool,
}
impl DonchianStop {
pub fn new() -> Self {
Self::with_params(20, 0.0, false)
}
pub fn with_params(period: usize, offset: f64, use_percentage: bool) -> Self {
Self::with_different_periods(period, period, offset, use_percentage)
}
pub fn with_different_periods(
upper_period: usize,
lower_period: usize,
offset: f64,
use_percentage: bool,
) -> Self {
assert!(upper_period > 0, "Upper period must be greater than 0");
assert!(lower_period > 0, "Lower period must be greater than 0");
let _max_period = upper_period.max(lower_period);
Self {
upper_period,
lower_period,
offset,
use_percentage,
highs: Vec::with_capacity(512),
lows: Vec::with_capacity(512),
upper_line: 0.0,
lower_line: 0.0,
middle_line: 0.0,
upper_stop: 0.0,
lower_stop: 0.0,
bars_count: 0,
is_ready: false,
}
}
pub fn for_optimization(
upper_period: usize,
lower_period: usize,
offset: f64,
use_percentage: bool,
) -> Self {
Self::with_different_periods(upper_period, lower_period, offset, use_percentage)
}
pub fn update_bar(&mut self, _open: f64, high: f64, low: f64, _close: f64, _volume: f64) -> (f64, f64, f64) {
self.bars_count += 1;
let max_period = self.upper_period.max(self.lower_period);
if self.highs.len() >= max_period {
self.highs.remove(0);
self.lows.remove(0);
}
self.highs.push(high);
self.lows.push(low);
if self.highs.len() >= self.upper_period {
let start_idx = self.highs.len().saturating_sub(self.upper_period);
self.upper_line = self.highs[start_idx..]
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max);
}
if self.lows.len() >= self.lower_period {
let start_idx = self.lows.len().saturating_sub(self.lower_period);
self.lower_line = self.lows[start_idx..]
.iter()
.cloned()
.fold(f64::INFINITY, f64::min);
}
if self.upper_line > 0.0 && self.lower_line < f64::INFINITY {
self.middle_line = (self.upper_line + self.lower_line) / 2.0;
}
self.calculate_stop_levels();
let min_period = self.upper_period.min(self.lower_period);
self.is_ready = self.bars_count >= min_period &&
self.upper_line > 0.0 &&
self.lower_line < f64::INFINITY;
(self.lower_stop, self.upper_stop, self.middle_line)
}
fn calculate_stop_levels(&mut self) {
if self.upper_line > 0.0 {
if self.use_percentage {
self.upper_stop = self.upper_line * (1.0 + self.offset / 100.0);
} else {
self.upper_stop = self.upper_line + self.offset;
}
}
if self.lower_line < f64::INFINITY {
if self.use_percentage {
self.lower_stop = self.lower_line * (1.0 - self.offset / 100.0);
} else {
self.lower_stop = self.lower_line - self.offset;
}
}
}
pub fn upper_line(&self) -> f64 {
self.upper_line
}
pub fn lower_line(&self) -> f64 {
if self.lower_line == f64::INFINITY { 0.0 } else { self.lower_line }
}
pub fn middle_line(&self) -> f64 {
self.middle_line
}
pub fn upper_stop(&self) -> f64 {
self.upper_stop
}
pub fn lower_stop(&self) -> f64 {
self.lower_stop
}
pub fn value(&self) -> IndicatorValue {
IndicatorValue::Single(self.lower_stop)
}
pub fn levels(&self) -> (f64, f64, f64) {
(self.lower_stop, self.upper_stop, self.middle_line)
}
pub fn channel_width(&self) -> f64 {
if self.upper_line > 0.0 && self.lower_line < f64::INFINITY {
self.upper_line - self.lower_line
} else {
0.0
}
}
pub fn price_position(&self, price: f64) -> f64 {
let width = self.channel_width();
if width == 0.0 {
return 0.5;
}
(price - self.lower_line) / width
}
pub fn is_above_upper(&self, price: f64) -> bool {
price > self.upper_line
}
pub fn is_below_lower(&self, price: f64) -> bool {
price < self.lower_line
}
pub fn is_inside_channel(&self, price: f64) -> bool {
price >= self.lower_line && price <= self.upper_line
}
pub fn is_ready(&self) -> bool {
self.is_ready
}
pub fn reset(&mut self) {
self.highs.clear();
self.lows.clear();
self.upper_line = 0.0;
self.lower_line = 0.0;
self.middle_line = 0.0;
self.upper_stop = 0.0;
self.lower_stop = 0.0;
self.bars_count = 0;
self.is_ready = false;
}
pub fn params(&self) -> (usize, usize, f64, bool) {
(self.upper_period, self.lower_period, self.offset, self.use_percentage)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_donchian_stop_creation() {
let ind = DonchianStop::new();
assert!(!ind.is_ready());
assert_eq!(ind.value().main(), 0.0);
}
#[test]
fn test_donchian_stop_with_params() {
let ind = DonchianStop::with_params(20, 0.5, false);
assert!(!ind.is_ready());
assert_eq!(ind.params(), (20, 20, 0.5, false));
}
#[test]
fn test_donchian_stop_warmup() {
let mut ind = DonchianStop::with_params(10, 0.0, false);
for i in 0..15 {
let price = 100.0 + (i as f64 * 0.1).sin() * 5.0;
ind.update_bar(price, price + 1.0, price - 1.0, price, 1000.0);
}
assert!(ind.is_ready());
}
#[test]
fn test_donchian_stop_values_finite() {
let mut ind = DonchianStop::with_params(10, 0.0, false);
for i in 0..25 {
let price = 100.0 + (i as f64 * 0.2).sin() * 10.0;
let (lower, upper, mid) = ind.update_bar(price, price + 1.0, price - 1.0, price, 1000.0);
assert!(lower.is_finite());
assert!(upper.is_finite());
assert!(mid.is_finite());
}
}
#[test]
fn test_donchian_stop_reset() {
let mut ind = DonchianStop::with_params(10, 0.0, false);
for i in 0..20 {
ind.update_bar(100.0 + i as f64, 105.0, 95.0, 101.0, 1000.0);
}
ind.reset();
assert!(!ind.is_ready());
assert_eq!(ind.value().main(), 0.0);
}
}