1use std::fmt::{Debug, Display};
17
18use arraydeque::{ArrayDeque, Wrapping};
19use nautilus_model::data::Bar;
20use strum::Display;
21
22use crate::indicator::Indicator;
23
24#[repr(C)]
25#[derive(Debug, Display, Clone, Hash, PartialEq, Eq, Copy)]
26#[strum(ascii_case_insensitive)]
27#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
28#[cfg_attr(
29 feature = "python",
30 pyo3::pyclass(
31 frozen,
32 eq,
33 eq_int,
34 hash,
35 module = "nautilus_trader.indicators",
36 from_py_object,
37 )
38)]
39#[cfg_attr(
40 feature = "python",
41 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.indicators")
42)]
43pub enum CandleBodySize {
44 None = 0,
45 Small = 1,
46 Medium = 2,
47 Large = 3,
48 Trend = 4,
49}
50
51#[repr(C)]
52#[derive(Debug, Display, Clone, Hash, PartialEq, Eq, Copy)]
53#[strum(ascii_case_insensitive)]
54#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
55#[cfg_attr(
56 feature = "python",
57 pyo3::pyclass(
58 frozen,
59 eq,
60 eq_int,
61 hash,
62 module = "nautilus_trader.indicators",
63 from_py_object,
64 )
65)]
66#[cfg_attr(
67 feature = "python",
68 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.indicators")
69)]
70pub enum CandleDirection {
71 Bull = 1,
72 None = 0,
73 Bear = -1,
74}
75
76#[repr(C)]
77#[derive(Debug, Display, Clone, Hash, PartialEq, Eq, Copy)]
78#[strum(ascii_case_insensitive)]
79#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
80#[cfg_attr(
81 feature = "python",
82 pyo3::pyclass(
83 frozen,
84 eq,
85 eq_int,
86 hash,
87 module = "nautilus_trader.indicators",
88 from_py_object,
89 )
90)]
91#[cfg_attr(
92 feature = "python",
93 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.indicators")
94)]
95pub enum CandleSize {
96 None = 0,
97 VerySmall = 1,
98 Small = 2,
99 Medium = 3,
100 Large = 4,
101 VeryLarge = 5,
102 ExtremelyLarge = 6,
103}
104
105#[repr(C)]
106#[derive(Debug, Display, Clone, Hash, PartialEq, Eq, Copy)]
107#[strum(ascii_case_insensitive)]
108#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
109#[cfg_attr(
110 feature = "python",
111 pyo3::pyclass(
112 frozen,
113 eq,
114 eq_int,
115 hash,
116 module = "nautilus_trader.indicators",
117 from_py_object,
118 )
119)]
120#[cfg_attr(
121 feature = "python",
122 pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.indicators")
123)]
124pub enum CandleWickSize {
125 None = 0,
126 Small = 1,
127 Medium = 2,
128 Large = 3,
129}
130
131#[repr(C)]
132#[derive(Debug, Clone, Copy)]
133#[cfg_attr(
134 feature = "python",
135 pyo3::pyclass(module = "nautilus_trader.indicators", from_py_object)
136)]
137#[cfg_attr(
138 feature = "python",
139 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
140)]
141pub struct FuzzyCandle {
142 pub direction: CandleDirection,
143 pub size: CandleSize,
144 pub body_size: CandleBodySize,
145 pub upper_wick_size: CandleWickSize,
146 pub lower_wick_size: CandleWickSize,
147}
148
149impl Display for FuzzyCandle {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 write!(
152 f,
153 "{}({},{},{},{})",
154 self.direction, self.size, self.body_size, self.upper_wick_size, self.lower_wick_size
155 )
156 }
157}
158
159impl FuzzyCandle {
160 #[must_use]
161 pub const fn new(
162 direction: CandleDirection,
163 size: CandleSize,
164 body_size: CandleBodySize,
165 upper_wick_size: CandleWickSize,
166 lower_wick_size: CandleWickSize,
167 ) -> Self {
168 Self {
169 direction,
170 size,
171 body_size,
172 upper_wick_size,
173 lower_wick_size,
174 }
175 }
176}
177
178const MAX_CAPACITY: usize = 1024;
179
180#[repr(C)]
181#[derive(Debug)]
182#[cfg_attr(
183 feature = "python",
184 pyo3::pyclass(module = "nautilus_trader.indicators")
185)]
186#[cfg_attr(
187 feature = "python",
188 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
189)]
190pub struct FuzzyCandlesticks {
191 pub period: usize,
192 pub threshold1: f64,
193 pub threshold2: f64,
194 pub threshold3: f64,
195 pub threshold4: f64,
196 pub vector: Vec<i32>,
197 pub value: FuzzyCandle,
198 pub initialized: bool,
199 has_inputs: bool,
200 lengths: ArrayDeque<f64, MAX_CAPACITY, Wrapping>,
201 body_percents: ArrayDeque<f64, MAX_CAPACITY, Wrapping>,
202 upper_wick_percents: ArrayDeque<f64, MAX_CAPACITY, Wrapping>,
203 lower_wick_percents: ArrayDeque<f64, MAX_CAPACITY, Wrapping>,
204 last_open: f64,
205 last_high: f64,
206 last_low: f64,
207 last_close: f64,
208}
209
210impl Display for FuzzyCandlesticks {
211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 write!(
213 f,
214 "{}({},{},{},{},{})",
215 self.name(),
216 self.period,
217 self.threshold1,
218 self.threshold2,
219 self.threshold3,
220 self.threshold4
221 )
222 }
223}
224
225impl Indicator for FuzzyCandlesticks {
226 fn name(&self) -> String {
227 stringify!(FuzzyCandlesticks).to_string()
228 }
229
230 fn has_inputs(&self) -> bool {
231 self.has_inputs
232 }
233
234 fn initialized(&self) -> bool {
235 self.initialized
236 }
237
238 fn handle_bar(&mut self, bar: &Bar) {
239 self.update_raw(
240 (&bar.open).into(),
241 (&bar.high).into(),
242 (&bar.low).into(),
243 (&bar.close).into(),
244 );
245 }
246
247 fn reset(&mut self) {
248 self.lengths.clear();
249 self.body_percents.clear();
250 self.upper_wick_percents.clear();
251 self.lower_wick_percents.clear();
252 self.value = FuzzyCandle::new(
253 CandleDirection::None,
254 CandleSize::None,
255 CandleBodySize::None,
256 CandleWickSize::None,
257 CandleWickSize::None,
258 );
259 self.vector = Vec::new();
260 self.last_open = 0.0;
261 self.last_high = 0.0;
262 self.last_close = 0.0;
263 self.last_low = 0.0;
264 self.has_inputs = false;
265 self.initialized = false;
266 }
267}
268
269impl FuzzyCandlesticks {
270 #[must_use]
282 pub fn new(
283 period: usize,
284 threshold1: f64,
285 threshold2: f64,
286 threshold3: f64,
287 threshold4: f64,
288 ) -> Self {
289 assert!(period <= MAX_CAPACITY);
290 Self {
291 period,
292 threshold1,
293 threshold2,
294 threshold3,
295 threshold4,
296 vector: Vec::new(),
297 value: FuzzyCandle::new(
298 CandleDirection::None,
299 CandleSize::None,
300 CandleBodySize::None,
301 CandleWickSize::None,
302 CandleWickSize::None,
303 ),
304 has_inputs: false,
305 initialized: false,
306 lengths: ArrayDeque::new(),
307 body_percents: ArrayDeque::new(),
308 upper_wick_percents: ArrayDeque::new(),
309 lower_wick_percents: ArrayDeque::new(),
310 last_open: 0.0,
311 last_high: 0.0,
312 last_low: 0.0,
313 last_close: 0.0,
314 }
315 }
316
317 pub fn update_raw(&mut self, open: f64, high: f64, low: f64, close: f64) {
318 if !self.has_inputs {
319 self.last_close = close;
320 self.last_open = open;
321 self.last_high = high;
322 self.last_low = low;
323 self.has_inputs = true;
324 }
325
326 self.last_close = close;
327 self.last_open = open;
328 self.last_high = high;
329 self.last_low = low;
330
331 let total = (high - low).abs();
332
333 if self.lengths.len() == self.period {
337 self.lengths.pop_front();
338 self.body_percents.pop_front();
339 self.upper_wick_percents.pop_front();
340 self.lower_wick_percents.pop_front();
341 }
342
343 let _ = self.lengths.push_back(total);
344
345 if total == 0.0 {
346 let _ = self.body_percents.push_back(0.0);
347 let _ = self.upper_wick_percents.push_back(0.0);
348 let _ = self.lower_wick_percents.push_back(0.0);
349 } else {
350 let body = (close - open).abs();
351 let upper_wick = high - f64::max(open, close);
352 let lower_wick = f64::min(open, close) - low;
353
354 let _ = self.body_percents.push_back(body / total);
355 let _ = self.upper_wick_percents.push_back(upper_wick / total);
356 let _ = self.lower_wick_percents.push_back(lower_wick / total);
357 }
358
359 if self.lengths.len() >= self.period {
360 self.initialized = true;
361 }
362
363 if !self.initialized {
365 return;
366 }
367
368 let mean_length = self.lengths.iter().sum::<f64>() / (self.period as f64);
369 let mean_body_percent = self.body_percents.iter().sum::<f64>() / (self.period as f64);
370 let mean_upper_percent =
371 self.upper_wick_percents.iter().sum::<f64>() / (self.period as f64);
372 let mean_lower_percent =
373 self.lower_wick_percents.iter().sum::<f64>() / (self.period as f64);
374
375 let sd_length = Self::std_dev(&self.lengths, mean_length);
376 let sd_body = Self::std_dev(&self.body_percents, mean_body_percent);
377 let sd_upper = Self::std_dev(&self.upper_wick_percents, mean_upper_percent);
378 let sd_lower = Self::std_dev(&self.lower_wick_percents, mean_lower_percent);
379 let latest_body = *self.body_percents.back().unwrap_or(&0.0);
380 let latest_upper = *self.upper_wick_percents.back().unwrap_or(&0.0);
381 let latest_lower = *self.lower_wick_percents.back().unwrap_or(&0.0);
382
383 self.value = FuzzyCandle::new(
384 Self::fuzzify_direction(open, close),
385 self.fuzzify_size(total, mean_length, sd_length),
386 self.fuzzify_body_size(latest_body, mean_body_percent, sd_body),
387 self.fuzzify_wick_size(latest_upper, mean_upper_percent, sd_upper),
388 self.fuzzify_wick_size(latest_lower, mean_lower_percent, sd_lower),
389 );
390
391 self.vector = vec![
392 self.value.direction as i32,
393 self.value.size as i32,
394 self.value.body_size as i32,
395 self.value.upper_wick_size as i32,
396 self.value.lower_wick_size as i32,
397 ];
398 }
399
400 pub fn reset(&mut self) {
401 Indicator::reset(self);
402 }
403
404 fn fuzzify_direction(open: f64, close: f64) -> CandleDirection {
405 if close > open {
406 CandleDirection::Bull
407 } else if close < open {
408 CandleDirection::Bear
409 } else {
410 CandleDirection::None
411 }
412 }
413
414 fn fuzzify_size(&self, length: f64, mean_length: f64, sd_lengths: f64) -> CandleSize {
415 if !length.is_finite() || length == 0.0 {
416 return CandleSize::None;
417 }
418
419 let thresholds = [
420 mean_length - self.threshold2 * sd_lengths, mean_length - self.threshold1 * sd_lengths, mean_length + self.threshold1 * sd_lengths, mean_length + self.threshold2 * sd_lengths, mean_length + self.threshold3 * sd_lengths, ];
426
427 if length <= thresholds[0] {
428 CandleSize::VerySmall
429 } else if length <= thresholds[1] {
430 CandleSize::Small
431 } else if length <= thresholds[2] {
432 CandleSize::Medium
433 } else if length <= thresholds[3] {
434 CandleSize::Large
435 } else if length <= thresholds[4] {
436 CandleSize::VeryLarge
437 } else {
438 CandleSize::ExtremelyLarge
439 }
440 }
441
442 fn fuzzify_body_size(
443 &self,
444 body_percent: f64,
445 mean_body_percent: f64,
446 sd_body_percent: f64,
447 ) -> CandleBodySize {
448 if body_percent == 0.0 {
449 return CandleBodySize::None;
450 }
451
452 let mut x;
453
454 x = sd_body_percent.mul_add(-self.threshold1, mean_body_percent);
455 if body_percent <= x {
456 return CandleBodySize::Small;
457 }
458
459 x = sd_body_percent.mul_add(self.threshold1, mean_body_percent);
460 if body_percent <= x {
461 return CandleBodySize::Medium;
462 }
463
464 x = sd_body_percent.mul_add(self.threshold2, mean_body_percent);
465 if body_percent <= x {
466 return CandleBodySize::Large;
467 }
468
469 CandleBodySize::Trend
470 }
471
472 fn fuzzify_wick_size(
473 &self,
474 wick_percent: f64,
475 mean_wick_percent: f64,
476 sd_wick_percents: f64,
477 ) -> CandleWickSize {
478 if wick_percent == 0.0 {
479 return CandleWickSize::None;
480 }
481
482 let mut x;
483 x = sd_wick_percents.mul_add(-self.threshold1, mean_wick_percent);
484 if wick_percent <= x {
485 return CandleWickSize::Small;
486 }
487
488 x = sd_wick_percents.mul_add(self.threshold2, mean_wick_percent);
489 if wick_percent <= x {
490 return CandleWickSize::Medium;
491 }
492
493 CandleWickSize::Large
494 }
495
496 fn std_dev<const CAP: usize>(buffer: &ArrayDeque<f64, CAP, Wrapping>, mean: f64) -> f64 {
497 if buffer.is_empty() {
498 return 0.0;
499 }
500 let variance = buffer
501 .iter()
502 .map(|v| {
503 let d = v - mean;
504 d * d
505 })
506 .sum::<f64>()
507 / (buffer.len() as f64);
508 variance.sqrt()
509 }
510}
511
512#[cfg(test)]
513mod tests {
514 use rstest::rstest;
515
516 use super::*;
517 use crate::{
518 stubs::{fuzzy_candlesticks_1, fuzzy_candlesticks_3, fuzzy_candlesticks_10},
519 volatility::fuzzy::FuzzyCandlesticks,
520 };
521
522 #[rstest]
523 fn test_fuzzy_candle_display_orders_wicks_upper_then_lower() {
524 let candle = FuzzyCandle::new(
528 CandleDirection::Bull,
529 CandleSize::Medium,
530 CandleBodySize::Small,
531 CandleWickSize::Large,
532 CandleWickSize::None,
533 );
534
535 assert_eq!(format!("{candle}"), "BULL(MEDIUM,SMALL,LARGE,NONE)");
536 }
537
538 #[rstest]
539 fn test_psl_initialized(fuzzy_candlesticks_10: FuzzyCandlesticks) {
540 let display_str = format!("{fuzzy_candlesticks_10}");
541 assert_eq!(display_str, "FuzzyCandlesticks(10,0.1,0.15,0.2,0.3)");
542 assert_eq!(fuzzy_candlesticks_10.period, 10);
543 assert!(!fuzzy_candlesticks_10.initialized);
544 assert!(!fuzzy_candlesticks_10.has_inputs);
545 }
546
547 #[rstest]
548 fn test_value_with_one_input(mut fuzzy_candlesticks_1: FuzzyCandlesticks) {
549 fuzzy_candlesticks_1.update_raw(123.90, 135.79, 117.09, 125.09);
551 assert_eq!(fuzzy_candlesticks_1.value.direction, CandleDirection::Bull);
552 assert_eq!(fuzzy_candlesticks_1.value.size, CandleSize::VerySmall);
553 assert_eq!(fuzzy_candlesticks_1.value.body_size, CandleBodySize::Small);
554 assert_eq!(
555 fuzzy_candlesticks_1.value.upper_wick_size,
556 CandleWickSize::Small
557 );
558 assert_eq!(
559 fuzzy_candlesticks_1.value.lower_wick_size,
560 CandleWickSize::Small
561 );
562
563 let expected_vec = vec![1, 1, 1, 1, 1];
564 assert_eq!(fuzzy_candlesticks_1.vector, expected_vec);
565 }
566
567 #[rstest]
568 fn test_value_with_three_inputs(mut fuzzy_candlesticks_3: FuzzyCandlesticks) {
569 fuzzy_candlesticks_3.update_raw(142.35, 145.82, 141.20, 144.75);
571 fuzzy_candlesticks_3.update_raw(144.75, 144.93, 103.55, 108.22);
572 fuzzy_candlesticks_3.update_raw(108.22, 120.15, 105.01, 119.89);
573 assert_eq!(fuzzy_candlesticks_3.value.direction, CandleDirection::Bull);
574 assert_eq!(fuzzy_candlesticks_3.value.size, CandleSize::VerySmall);
575 assert_eq!(fuzzy_candlesticks_3.value.body_size, CandleBodySize::Trend);
576 assert_eq!(
577 fuzzy_candlesticks_3.value.upper_wick_size,
578 CandleWickSize::Small
579 );
580 assert_eq!(
581 fuzzy_candlesticks_3.value.lower_wick_size,
582 CandleWickSize::Large
583 );
584
585 let expected_vec = vec![1, 1, 4, 1, 3];
586 assert_eq!(fuzzy_candlesticks_3.vector, expected_vec);
587 }
588
589 #[rstest]
590 fn test_value_not_updated_before_initialization(mut fuzzy_candlesticks_10: FuzzyCandlesticks) {
591 fuzzy_candlesticks_10.update_raw(100.0, 105.0, 95.0, 102.0);
593 fuzzy_candlesticks_10.update_raw(102.0, 108.0, 100.0, 98.0);
594 fuzzy_candlesticks_10.update_raw(98.0, 101.0, 96.0, 100.0);
595
596 assert_eq!(fuzzy_candlesticks_10.vector.len(), 0);
597 assert!(
598 !fuzzy_candlesticks_10.initialized,
599 "Should not be initialized before period"
600 );
601 assert!(fuzzy_candlesticks_10.has_inputs, "Should has inputs");
602 assert_eq!(fuzzy_candlesticks_10.lengths.len(), 3);
603 assert_eq!(fuzzy_candlesticks_10.body_percents.len(), 3);
604 }
605
606 #[rstest]
607 fn test_value_with_ten_inputs(mut fuzzy_candlesticks_10: FuzzyCandlesticks) {
608 fuzzy_candlesticks_10.update_raw(150.25, 153.4, 148.1, 152.75);
609 fuzzy_candlesticks_10.update_raw(152.8, 155.2, 151.3, 151.95);
610 fuzzy_candlesticks_10.update_raw(151.9, 152.85, 147.6, 148.2);
611 fuzzy_candlesticks_10.update_raw(148.3, 150.75, 146.9, 150.4);
612 fuzzy_candlesticks_10.update_raw(150.5, 154.3, 149.8, 153.9);
613 fuzzy_candlesticks_10.update_raw(153.95, 155.8, 152.2, 152.6);
614 fuzzy_candlesticks_10.update_raw(152.7, 153.4, 148.5, 149.1);
615 fuzzy_candlesticks_10.update_raw(149.2, 151.9, 147.3, 151.5);
616 fuzzy_candlesticks_10.update_raw(151.6, 156.4, 151.0, 155.8);
617 fuzzy_candlesticks_10.update_raw(155.9, 157.2, 153.7, 154.3);
618
619 assert_eq!(fuzzy_candlesticks_10.value.direction, CandleDirection::Bear);
620 assert_eq!(fuzzy_candlesticks_10.value.size, CandleSize::VerySmall);
621 assert_eq!(fuzzy_candlesticks_10.value.body_size, CandleBodySize::Small);
622 assert_eq!(
623 fuzzy_candlesticks_10.value.upper_wick_size,
624 CandleWickSize::Large
625 );
626 assert_eq!(
627 fuzzy_candlesticks_10.value.lower_wick_size,
628 CandleWickSize::Small
629 );
630
631 let expected_vec = vec![-1, 1, 1, 3, 1];
632 assert_eq!(fuzzy_candlesticks_10.vector, expected_vec);
633 }
634
635 #[rstest]
636 fn test_windows_bounded_to_period(mut fuzzy_candlesticks_10: FuzzyCandlesticks) {
637 let bars = [
642 (150.25, 153.4, 148.1, 152.75),
643 (152.8, 155.2, 151.3, 151.95),
644 (151.9, 152.85, 147.6, 148.2),
645 (148.3, 150.75, 146.9, 150.4),
646 (150.5, 154.3, 149.8, 153.9),
647 (153.95, 155.8, 152.2, 152.6),
648 (152.7, 153.4, 148.5, 149.1),
649 (149.2, 151.9, 147.3, 151.5),
650 (151.6, 156.4, 151.0, 155.8),
651 (155.9, 157.2, 153.7, 154.3),
652 (154.3, 158.0, 153.0, 157.2),
653 (157.2, 159.5, 155.1, 156.0),
654 (156.0, 156.9, 152.4, 153.1),
655 (153.1, 155.0, 150.2, 154.8),
656 (154.8, 157.7, 154.0, 156.9),
657 ];
658
659 for (open, high, low, close) in bars {
660 fuzzy_candlesticks_10.update_raw(open, high, low, close);
661 }
662
663 assert!(fuzzy_candlesticks_10.initialized());
664 assert_eq!(fuzzy_candlesticks_10.lengths.len(), 10);
665 assert_eq!(fuzzy_candlesticks_10.body_percents.len(), 10);
666 assert_eq!(fuzzy_candlesticks_10.upper_wick_percents.len(), 10);
667 assert_eq!(fuzzy_candlesticks_10.lower_wick_percents.len(), 10);
668 }
669
670 #[rstest]
671 #[case::inherent(FuzzyCandlesticks::reset)]
672 #[case::indicator(<FuzzyCandlesticks as Indicator>::reset)]
673 fn test_reset(
674 #[case] reset: fn(&mut FuzzyCandlesticks),
675 mut fuzzy_candlesticks_10: FuzzyCandlesticks,
676 ) {
677 for _ in 0..10 {
678 fuzzy_candlesticks_10.update_raw(151.6, 156.4, 151.0, 155.8);
679 }
680 assert!(fuzzy_candlesticks_10.initialized);
681 assert!(!fuzzy_candlesticks_10.vector.is_empty());
682
683 reset(&mut fuzzy_candlesticks_10);
684
685 assert_eq!(fuzzy_candlesticks_10.lengths.len(), 0);
686 assert_eq!(fuzzy_candlesticks_10.body_percents.len(), 0);
687 assert_eq!(fuzzy_candlesticks_10.upper_wick_percents.len(), 0);
688 assert_eq!(fuzzy_candlesticks_10.lower_wick_percents.len(), 0);
689 assert_eq!(fuzzy_candlesticks_10.value.direction, CandleDirection::None);
690 assert_eq!(fuzzy_candlesticks_10.value.size, CandleSize::None);
691 assert_eq!(fuzzy_candlesticks_10.value.body_size, CandleBodySize::None);
692 assert_eq!(
693 fuzzy_candlesticks_10.value.upper_wick_size,
694 CandleWickSize::None
695 );
696 assert_eq!(
697 fuzzy_candlesticks_10.value.lower_wick_size,
698 CandleWickSize::None
699 );
700 assert_eq!(fuzzy_candlesticks_10.vector.len(), 0);
701 assert_eq!(fuzzy_candlesticks_10.last_open, 0.0);
702 assert_eq!(fuzzy_candlesticks_10.last_low, 0.0);
703 assert_eq!(fuzzy_candlesticks_10.last_high, 0.0);
704 assert_eq!(fuzzy_candlesticks_10.last_close, 0.0);
705 assert!(!fuzzy_candlesticks_10.has_inputs);
706 assert!(!fuzzy_candlesticks_10.initialized);
707 }
708 #[rstest]
709 fn test_zero_length_candle(mut fuzzy_candlesticks_1: FuzzyCandlesticks) {
710 fuzzy_candlesticks_1.update_raw(100.0, 100.0, 100.0, 100.0); assert_eq!(fuzzy_candlesticks_1.value.size, CandleSize::None);
712 assert_eq!(fuzzy_candlesticks_1.value.body_size, CandleBodySize::None);
713 assert_eq!(
714 fuzzy_candlesticks_1.value.upper_wick_size,
715 CandleWickSize::None
716 );
717 assert_eq!(
718 fuzzy_candlesticks_1.value.lower_wick_size,
719 CandleWickSize::None
720 );
721 assert_eq!(fuzzy_candlesticks_1.value.direction, CandleDirection::None);
722 }
723
724 #[rstest]
725 fn test_constant_input_stddev_zero(mut fuzzy_candlesticks_1: FuzzyCandlesticks) {
726 for _ in 0..10 {
727 fuzzy_candlesticks_1.update_raw(100.0, 110.0, 90.0, 105.0);
728 }
729 assert!(fuzzy_candlesticks_1.lengths.iter().all(|&v| v == 20.0));
730 assert!(matches!(
731 fuzzy_candlesticks_1.value.size,
732 CandleSize::VerySmall | CandleSize::Small | CandleSize::Medium
733 ));
734 }
735
736 #[rstest]
737 fn test_nan_inf_safety(mut fuzzy_candlesticks_1: FuzzyCandlesticks) {
738 fuzzy_candlesticks_1.update_raw(f64::INFINITY, f64::INFINITY, f64::INFINITY, f64::INFINITY);
739 fuzzy_candlesticks_1.update_raw(f64::NAN, f64::NAN, f64::NAN, f64::NAN);
740 assert_eq!(fuzzy_candlesticks_1.value.direction, CandleDirection::None);
741 }
742
743 #[rstest]
744 fn test_direction_cases(mut fuzzy_candlesticks_1: FuzzyCandlesticks) {
745 fuzzy_candlesticks_1.update_raw(100.0, 105.0, 95.0, 110.0); assert_eq!(fuzzy_candlesticks_1.value.direction, CandleDirection::Bull);
747
748 fuzzy_candlesticks_1.update_raw(110.0, 115.0, 105.0, 100.0); assert_eq!(fuzzy_candlesticks_1.value.direction, CandleDirection::Bear);
750
751 fuzzy_candlesticks_1.update_raw(100.0, 110.0, 90.0, 100.0); assert_eq!(fuzzy_candlesticks_1.value.direction, CandleDirection::None);
753 }
754
755 #[rstest]
756 fn test_body_and_wick_percentages(mut fuzzy_candlesticks_1: FuzzyCandlesticks) {
757 let open: f64 = 100.0;
758 let close: f64 = 110.0;
759 let high: f64 = 120.0;
760 let low: f64 = 90.0;
761
762 let total = high - low; let expected_body = (close - open).abs() / total; let expected_upper_wick = (high - close.max(open)) / total; let expected_lower_wick = (open.min(close) - low) / total; fuzzy_candlesticks_1.update_raw(open, high, low, close);
768
769 let actual_body = fuzzy_candlesticks_1.body_percents[0];
770 let actual_upper = fuzzy_candlesticks_1.upper_wick_percents[0];
771 let actual_lower = fuzzy_candlesticks_1.lower_wick_percents[0];
772
773 assert!(
774 (actual_body - expected_body).abs() < 1e-6,
775 "Body percent mismatch"
776 );
777 assert!(
778 (actual_upper - expected_upper_wick).abs() < 1e-6,
779 "Upper wick percent mismatch"
780 );
781 assert!(
782 (actual_lower - expected_lower_wick).abs() < 1e-6,
783 "Lower wick percent mismatch"
784 );
785 }
786
787 #[rstest]
788 fn test_body_size_large(mut fuzzy_candlesticks_3: FuzzyCandlesticks) {
789 fuzzy_candlesticks_3.update_raw(100.0, 101.0, 99.0, 100.0);
791 fuzzy_candlesticks_3.update_raw(100.0, 102.0, 98.0, 100.5);
795 fuzzy_candlesticks_3.update_raw(101.0, 105.0, 100.0, 104.8);
799 assert_eq!(fuzzy_candlesticks_3.value.body_size, CandleBodySize::Trend);
805 }
806
807 #[rstest]
808 fn test_lower_wick_size_large(mut fuzzy_candlesticks_3: FuzzyCandlesticks) {
809 fuzzy_candlesticks_3.update_raw(100.0, 101.0, 100.0, 101.0);
811 fuzzy_candlesticks_3.update_raw(102.0, 103.0, 101.5, 102.5);
815 fuzzy_candlesticks_3.update_raw(110.0, 115.0, 100.0, 114.0);
822 assert_eq!(
829 fuzzy_candlesticks_3.value.lower_wick_size,
830 CandleWickSize::Large
831 );
832 }
833
834 #[rstest]
835 fn test_upper_wick_size_large(mut fuzzy_candlesticks_3: FuzzyCandlesticks) {
836 fuzzy_candlesticks_3.update_raw(100.0, 100.0, 99.0, 100.0);
838 fuzzy_candlesticks_3.update_raw(101.0, 102.0, 100.0, 101.5);
842 fuzzy_candlesticks_3.update_raw(105.0, 115.0, 104.0, 106.0);
848 assert_eq!(
855 fuzzy_candlesticks_3.value.upper_wick_size,
856 CandleWickSize::Large
857 );
858 }
859}