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