wickra_core/indicators/
shark.rs1use crate::indicators::pattern_swing::{ratios_in, xabcd, SwingTracker, SWING_THRESHOLD};
4use crate::ohlcv::Candle;
5use crate::traits::Indicator;
6
7#[derive(Debug, Clone)]
21pub struct Shark {
22 swing: SwingTracker,
23 has_emitted: bool,
24}
25
26impl Shark {
27 pub const fn new() -> Self {
29 Self {
30 swing: SwingTracker::new(SWING_THRESHOLD, 5),
31 has_emitted: false,
32 }
33 }
34}
35
36impl Default for Shark {
37 fn default() -> Self {
38 Self::new()
39 }
40}
41
42impl Indicator for Shark {
43 type Input = Candle;
44 type Output = f64;
45
46 fn update(&mut self, candle: Candle) -> Option<f64> {
47 self.has_emitted = true;
48 if !self.swing.update(candle) {
49 return Some(0.0);
50 }
51 let pivots = self.swing.pivots();
52 if pivots.len() < 5 {
53 return Some(0.0);
54 }
55 let p = xabcd(pivots);
56 let xa = (p.a - p.x).abs();
57 let ab = (p.b - p.a).abs();
58 let bc = (p.c - p.b).abs();
59 let cd = (p.d - p.c).abs();
60 let ad = (p.d - p.a).abs();
61 let matched = ratios_in(&[
62 (ab / xa, 1.13, 1.618),
63 (bc / ab, 1.618, 2.24),
64 (cd / bc, 0.382, 0.886),
65 (ad / xa, 0.886, 1.13),
66 ]);
67 if matched {
68 return Some(if p.bullish { 1.0 } else { -1.0 });
69 }
70 Some(0.0)
71 }
72
73 fn reset(&mut self) {
74 self.swing.reset();
75 self.has_emitted = false;
76 }
77
78 fn warmup_period(&self) -> usize {
79 6
80 }
81
82 fn is_ready(&self) -> bool {
83 self.has_emitted
84 }
85
86 fn name(&self) -> &'static str {
87 "Shark"
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94 use crate::indicators::pattern_swing::candles_for_pivots;
95 use crate::traits::BatchExt;
96
97 fn run(pivots: &[f64]) -> Vec<f64> {
98 let mut indicator = Shark::new();
99 candles_for_pivots(pivots)
100 .into_iter()
101 .map(|c| indicator.update(c).unwrap())
102 .collect()
103 }
104
105 #[test]
106 fn accessors_and_metadata() {
107 let indicator = Shark::new();
108 assert_eq!(indicator.name(), "Shark");
109 assert_eq!(indicator.warmup_period(), 6);
110 assert!(!indicator.is_ready());
111 assert!(!Shark::default().is_ready());
112 }
113
114 #[test]
115 fn bullish_shark_is_plus_one() {
116 let out = run(&[150.0, 100.0, 140.0, 88.0, 186.8, 100.0]);
117 assert_eq!(*out.last().unwrap(), 1.0);
118 assert!(out[..out.len() - 1].iter().all(|&x| x == 0.0));
119 }
120
121 #[test]
122 fn bearish_shark_is_minus_one() {
123 let out = run(&[150.0, 110.0, 162.0, 60.2, 150.0]);
124 assert_eq!(*out.last().unwrap(), -1.0);
125 }
126
127 #[test]
128 fn out_of_ratio_does_not_trigger() {
129 let out = run(&[150.0, 100.0, 140.0, 110.0, 135.0, 105.0]);
130 assert_eq!(*out.last().unwrap(), 0.0);
131 }
132
133 #[test]
134 fn reset_clears_state() {
135 let mut indicator = Shark::new();
136 for c in candles_for_pivots(&[150.0, 100.0, 140.0]) {
137 let _ = indicator.update(c);
138 }
139 indicator.reset();
140 assert!(!indicator.is_ready());
141 let c = Candle::new(99.5, 100.0, 99.5, 99.5, 1.0, 0).unwrap();
142 assert_eq!(indicator.update(c), Some(0.0));
143 }
144
145 #[test]
146 fn batch_equals_streaming() {
147 let candles = candles_for_pivots(&[150.0, 100.0, 140.0, 88.0, 186.8, 100.0]);
148 let mut a = Shark::new();
149 let mut b = Shark::new();
150 assert_eq!(
151 a.batch(&candles),
152 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
153 );
154 }
155}