use crate::indicators::Indicator;
use crate::types::Candle;
#[derive(Debug, Clone)]
pub struct ParabolicSar {
af_start: f64,
af_step: f64,
af_max: f64,
bar_count: usize,
first_high: f64,
first_low: f64,
first_close: f64,
is_uptrend: bool,
sar: f64,
ep: f64,
af: f64,
prev1_high: f64,
prev1_low: f64,
prev2_high: f64,
prev2_low: f64,
}
impl ParabolicSar {
pub fn new(af_start: f64, af_step: f64, af_max: f64) -> Self {
assert!(af_start > 0.0, "af_start must be > 0");
assert!(af_step > 0.0, "af_step must be > 0");
assert!(af_max >= af_start, "af_max must be >= af_start");
Self {
af_start,
af_step,
af_max,
bar_count: 0,
first_high: 0.0,
first_low: 0.0,
first_close: 0.0,
is_uptrend: true,
sar: 0.0,
ep: 0.0,
af: af_start,
prev1_high: 0.0,
prev1_low: 0.0,
prev2_high: 0.0,
prev2_low: 0.0,
}
}
#[inline]
fn update(&mut self, high: f64, low: f64, close: f64) -> Option<f64> {
self.bar_count += 1;
if self.bar_count == 1 {
self.first_high = high;
self.first_low = low;
self.first_close = close;
return None;
}
if self.bar_count == 2 {
self.is_uptrend = close >= self.first_close;
if self.is_uptrend {
self.sar = self.first_low;
self.ep = high.max(self.first_high);
} else {
self.sar = self.first_high;
self.ep = low.min(self.first_low);
}
self.af = self.af_start;
self.prev2_high = high;
self.prev2_low = low;
self.prev1_high = high;
self.prev1_low = low;
return Some(self.sar);
}
let mut new_sar = self.sar + self.af * (self.ep - self.sar);
if self.is_uptrend {
new_sar = new_sar.min(self.prev1_low).min(self.prev2_low);
} else {
new_sar = new_sar.max(self.prev1_high).max(self.prev2_high);
}
if self.is_uptrend && low < new_sar {
new_sar = self.ep.max(high).max(self.prev1_high);
self.is_uptrend = false;
self.ep = low;
self.af = self.af_start;
} else if !self.is_uptrend && high > new_sar {
new_sar = self.ep.min(low).min(self.prev1_low);
self.is_uptrend = true;
self.ep = high;
self.af = self.af_start;
} else if self.is_uptrend {
if high > self.ep {
self.ep = high;
self.af = (self.af + self.af_step).min(self.af_max);
}
} else if low < self.ep {
self.ep = low;
self.af = (self.af + self.af_step).min(self.af_max);
}
self.sar = new_sar;
self.prev2_high = self.prev1_high;
self.prev2_low = self.prev1_low;
self.prev1_high = high;
self.prev1_low = low;
Some(self.sar)
}
}
impl Indicator for ParabolicSar {
type Output = f64;
fn next(&mut self, candle: &Candle) -> Option<Self::Output> {
self.update(candle.high, candle.low, candle.close)
}
fn reset(&mut self) {
self.bar_count = 0;
self.first_high = 0.0;
self.first_low = 0.0;
self.first_close = 0.0;
self.is_uptrend = true;
self.sar = 0.0;
self.ep = 0.0;
self.af = self.af_start;
self.prev1_high = 0.0;
self.prev1_low = 0.0;
self.prev2_high = 0.0;
self.prev2_low = 0.0;
}
fn warmup_period(&self) -> usize {
2
}
fn clone_boxed(&self) -> Box<dyn Indicator<Output = Self::Output>> {
Box::new(self.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn candle(i: i64, high: f64, low: f64, close: f64) -> Candle {
Candle {
timestamp: i,
open: close,
high,
low,
close,
volume: 0.0,
}
}
#[test]
fn parabolic_sar_emits_after_warmup() {
let candles: Vec<Candle> = (0..10)
.map(|i| {
let price = 100.0 + i as f64;
candle(i, price + 1.0, price - 1.0, price)
})
.collect();
let out = ParabolicSar::new(0.02, 0.02, 0.2).calculate(&candles);
assert!(out[0].is_none());
assert!(out.iter().skip(1).all(|v| v.is_some()));
}
#[test]
fn parabolic_sar_warmup_period_is_two() {
let sar = ParabolicSar::new(0.02, 0.02, 0.2);
assert_eq!(sar.warmup_period(), 2);
}
#[test]
fn parabolic_sar_reset_restores_fresh_state() {
let candles: Vec<Candle> = (0..5)
.map(|i| {
let price = 100.0 + i as f64;
candle(i, price + 1.0, price - 1.0, price)
})
.collect();
let mut sar = ParabolicSar::new(0.02, 0.02, 0.2);
assert!(sar.next(&candles[0]).is_none());
assert!(sar.next(&candles[1]).is_some());
sar.reset();
assert_eq!(sar.next(&candles[0]), None);
}
#[test]
#[should_panic(expected = "af_start must be > 0")]
fn parabolic_sar_rejects_non_positive_af_start() {
ParabolicSar::new(0.0, 0.02, 0.2);
}
#[test]
#[should_panic(expected = "af_max must be >= af_start")]
fn parabolic_sar_rejects_af_max_below_af_start() {
ParabolicSar::new(0.1, 0.02, 0.05);
}
}