#![allow(clippy::doc_markdown)]
use crate::ohlcv::Candle;
use crate::traits::Indicator;
#[derive(Debug, Clone, Default)]
pub struct TdOpen {
prev: Option<Candle>,
last_value: Option<f64>,
}
impl TdOpen {
pub fn new() -> Self {
Self::default()
}
pub const fn value(&self) -> Option<f64> {
self.last_value
}
}
impl Indicator for TdOpen {
type Input = Candle;
type Output = f64;
fn update(&mut self, candle: Candle) -> Option<f64> {
let Some(prev) = self.prev else {
self.prev = Some(candle);
return None;
};
let v = if candle.open < prev.low && candle.high > prev.low {
1.0
} else if candle.open > prev.high && candle.low < prev.high {
-1.0
} else {
0.0
};
self.prev = Some(candle);
self.last_value = Some(v);
Some(v)
}
fn reset(&mut self) {
self.prev = None;
self.last_value = None;
}
fn warmup_period(&self) -> usize {
2
}
fn is_ready(&self) -> bool {
self.last_value.is_some()
}
fn name(&self) -> &'static str {
"TDOpen"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::BatchExt;
fn c(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
Candle::new_unchecked(open, high, low, close, 0.0, ts)
}
#[test]
fn buy_signal_on_gap_down_with_recovery() {
let mut td = TdOpen::new();
assert_eq!(td.update(c(10.0, 11.0, 10.0, 10.5, 0)), None);
assert_eq!(td.update(c(9.0, 11.0, 8.5, 9.5, 1)), Some(1.0));
}
#[test]
fn sell_signal_on_gap_up_with_fade() {
let mut td = TdOpen::new();
assert_eq!(td.update(c(10.0, 12.0, 9.0, 11.0, 0)), None);
assert_eq!(td.update(c(13.0, 13.5, 11.0, 11.5, 1)), Some(-1.0));
}
#[test]
fn no_signal_on_normal_open_within_range() {
let mut td = TdOpen::new();
assert_eq!(td.update(c(10.0, 12.0, 9.0, 11.0, 0)), None);
assert_eq!(td.update(c(10.5, 11.5, 9.5, 11.0, 1)), Some(0.0));
}
#[test]
fn gap_down_without_recovery_is_zero() {
let mut td = TdOpen::new();
assert_eq!(td.update(c(10.0, 12.0, 10.0, 11.0, 0)), None);
assert_eq!(td.update(c(9.0, 9.5, 8.5, 9.0, 1)), Some(0.0));
}
#[test]
fn batch_equals_streaming() {
let candles: Vec<Candle> = (0..40)
.map(|i| {
let m = 100.0 + (f64::from(i) * 0.3).sin() * 5.0;
c(m, m + 1.0, m - 1.0, m + 0.3, i64::from(i))
})
.collect();
let mut a = TdOpen::new();
let mut b = TdOpen::new();
assert_eq!(
a.batch(&candles),
candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
);
}
#[test]
fn output_only_in_canonical_set() {
let candles: Vec<Candle> = (0..120)
.map(|i| {
let m = 100.0 + (f64::from(i) * 0.5).sin() * 5.0;
c(m, m + 1.0, m - 1.0, m + 0.3, i64::from(i))
})
.collect();
let mut td = TdOpen::new();
for v in td.batch(&candles).into_iter().flatten() {
assert!(v == -1.0 || v == 0.0 || v == 1.0, "unexpected value {v}");
}
}
#[test]
fn reset_clears_state() {
let mut td = TdOpen::new();
td.update(c(10.0, 11.0, 9.0, 10.0, 0));
td.update(c(10.5, 11.5, 9.5, 10.5, 1));
assert!(td.is_ready());
td.reset();
assert!(!td.is_ready());
assert_eq!(td.update(c(10.0, 11.0, 9.0, 10.0, 2)), None);
assert_eq!(td.value(), None);
}
#[test]
fn accessors_and_metadata() {
let td = TdOpen::new();
assert_eq!(td.warmup_period(), 2);
assert_eq!(td.name(), "TDOpen");
assert_eq!(td.value(), None);
}
}