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.core.nautilus_pyo3.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.core.nautilus_pyo3.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.core.nautilus_pyo3.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.core.nautilus_pyo3.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(
136 module = "nautilus_trader.core.nautilus_pyo3.indicators",
137 from_py_object
138 )
139)]
140#[cfg_attr(
141 feature = "python",
142 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
143)]
144pub struct FuzzyCandle {
145 pub direction: CandleDirection,
146 pub size: CandleSize,
147 pub body_size: CandleBodySize,
148 pub upper_wick_size: CandleWickSize,
149 pub lower_wick_size: CandleWickSize,
150}
151
152impl Display for FuzzyCandle {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 write!(
155 f,
156 "{}({},{},{},{})",
157 self.direction, self.size, self.body_size, self.lower_wick_size, self.upper_wick_size
158 )
159 }
160}
161
162impl FuzzyCandle {
163 #[must_use]
164 pub const fn new(
165 direction: CandleDirection,
166 size: CandleSize,
167 body_size: CandleBodySize,
168 upper_wick_size: CandleWickSize,
169 lower_wick_size: CandleWickSize,
170 ) -> Self {
171 Self {
172 direction,
173 size,
174 body_size,
175 upper_wick_size,
176 lower_wick_size,
177 }
178 }
179}
180
181const MAX_CAPACITY: usize = 1024;
182
183#[repr(C)]
184#[derive(Debug)]
185#[cfg_attr(
186 feature = "python",
187 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators")
188)]
189#[cfg_attr(
190 feature = "python",
191 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
192)]
193pub struct FuzzyCandlesticks {
194 pub period: usize,
195 pub threshold1: f64,
196 pub threshold2: f64,
197 pub threshold3: f64,
198 pub threshold4: f64,
199 pub vector: Vec<i32>,
200 pub value: FuzzyCandle,
201 pub initialized: bool,
202 has_inputs: bool,
203 lengths: ArrayDeque<f64, MAX_CAPACITY, Wrapping>,
204 body_percents: ArrayDeque<f64, MAX_CAPACITY, Wrapping>,
205 upper_wick_percents: ArrayDeque<f64, MAX_CAPACITY, Wrapping>,
206 lower_wick_percents: ArrayDeque<f64, MAX_CAPACITY, Wrapping>,
207 last_open: f64,
208 last_high: f64,
209 last_low: f64,
210 last_close: f64,
211}
212
213impl Display for FuzzyCandlesticks {
214 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215 write!(
216 f,
217 "{}({},{},{},{},{})",
218 self.name(),
219 self.period,
220 self.threshold1,
221 self.threshold2,
222 self.threshold3,
223 self.threshold4
224 )
225 }
226}
227
228impl Indicator for FuzzyCandlesticks {
229 fn name(&self) -> String {
230 stringify!(FuzzyCandlesticks).to_string()
231 }
232
233 fn has_inputs(&self) -> bool {
234 self.has_inputs
235 }
236
237 fn initialized(&self) -> bool {
238 self.initialized
239 }
240
241 fn handle_bar(&mut self, bar: &Bar) {
242 self.update_raw(
243 (&bar.open).into(),
244 (&bar.high).into(),
245 (&bar.low).into(),
246 (&bar.close).into(),
247 );
248 }
249
250 fn reset(&mut self) {
251 self.lengths.clear();
252 self.body_percents.clear();
253 self.upper_wick_percents.clear();
254 self.lower_wick_percents.clear();
255 self.last_open = 0.0;
256 self.last_high = 0.0;
257 self.last_close = 0.0;
258 self.last_low = 0.0;
259 self.has_inputs = false;
260 self.initialized = false;
261 }
262}
263
264impl FuzzyCandlesticks {
265 #[must_use]
277 pub fn new(
278 period: usize,
279 threshold1: f64,
280 threshold2: f64,
281 threshold3: f64,
282 threshold4: f64,
283 ) -> Self {
284 assert!(period <= MAX_CAPACITY);
285 Self {
286 period,
287 threshold1,
288 threshold2,
289 threshold3,
290 threshold4,
291 vector: Vec::new(),
292 value: FuzzyCandle::new(
293 CandleDirection::None,
294 CandleSize::None,
295 CandleBodySize::None,
296 CandleWickSize::None,
297 CandleWickSize::None,
298 ),
299 has_inputs: false,
300 initialized: false,
301 lengths: ArrayDeque::new(),
302 body_percents: ArrayDeque::new(),
303 upper_wick_percents: ArrayDeque::new(),
304 lower_wick_percents: ArrayDeque::new(),
305 last_open: 0.0,
306 last_high: 0.0,
307 last_low: 0.0,
308 last_close: 0.0,
309 }
310 }
311
312 pub fn update_raw(&mut self, open: f64, high: f64, low: f64, close: f64) {
313 if !self.has_inputs {
314 self.last_close = close;
315 self.last_open = open;
316 self.last_high = high;
317 self.last_low = low;
318 self.has_inputs = true;
319 }
320
321 self.last_close = close;
322 self.last_open = open;
323 self.last_high = high;
324 self.last_low = low;
325
326 let total = (high - low).abs();
327 let _ = self.lengths.push_back(total);
328
329 if total == 0.0 {
330 let _ = self.body_percents.push_back(0.0);
331 let _ = self.upper_wick_percents.push_back(0.0);
332 let _ = self.lower_wick_percents.push_back(0.0);
333 } else {
334 let body = (close - open).abs();
335 let upper_wick = high - f64::max(open, close);
336 let lower_wick = f64::min(open, close) - low;
337
338 let _ = self.body_percents.push_back(body / total);
339 let _ = self.upper_wick_percents.push_back(upper_wick / total);
340 let _ = self.lower_wick_percents.push_back(lower_wick / total);
341 }
342
343 if self.lengths.len() >= self.period {
344 self.initialized = true;
345 }
346
347 if !self.initialized {
349 return;
350 }
351
352 let mean_length = self.lengths.iter().sum::<f64>() / (self.period as f64);
353 let mean_body_percent = self.body_percents.iter().sum::<f64>() / (self.period as f64);
354 let mean_upper_percent =
355 self.upper_wick_percents.iter().sum::<f64>() / (self.period as f64);
356 let mean_lower_percent =
357 self.lower_wick_percents.iter().sum::<f64>() / (self.period as f64);
358
359 let sd_length = Self::std_dev(&self.lengths, mean_length);
360 let sd_body = Self::std_dev(&self.body_percents, mean_body_percent);
361 let sd_upper = Self::std_dev(&self.upper_wick_percents, mean_upper_percent);
362 let sd_lower = Self::std_dev(&self.lower_wick_percents, mean_lower_percent);
363 let latest_body = *self.body_percents.back().unwrap_or(&0.0);
364 let latest_upper = *self.upper_wick_percents.back().unwrap_or(&0.0);
365 let latest_lower = *self.lower_wick_percents.back().unwrap_or(&0.0);
366
367 self.value = FuzzyCandle::new(
368 self.fuzzify_direction(open, close),
369 self.fuzzify_size(total, mean_length, sd_length),
370 self.fuzzify_body_size(latest_body, mean_body_percent, sd_body),
371 self.fuzzify_wick_size(latest_upper, mean_upper_percent, sd_upper),
372 self.fuzzify_wick_size(latest_lower, mean_lower_percent, sd_lower),
373 );
374
375 self.vector = vec![
376 self.value.direction as i32,
377 self.value.size as i32,
378 self.value.body_size as i32,
379 self.value.upper_wick_size as i32,
380 self.value.lower_wick_size as i32,
381 ];
382 }
383
384 pub fn reset(&mut self) {
385 self.lengths.clear();
386 self.body_percents.clear();
387 self.upper_wick_percents.clear();
388 self.lower_wick_percents.clear();
389 self.value = FuzzyCandle::new(
390 CandleDirection::None,
391 CandleSize::None,
392 CandleBodySize::None,
393 CandleWickSize::None,
394 CandleWickSize::None,
395 );
396 self.vector = Vec::new();
397 self.last_open = 0.0;
398 self.last_high = 0.0;
399 self.last_close = 0.0;
400 self.last_low = 0.0;
401 self.has_inputs = false;
402 self.initialized = false;
403 }
404
405 fn fuzzify_direction(&self, open: f64, close: f64) -> CandleDirection {
406 if close > open {
407 CandleDirection::Bull
408 } else if close < open {
409 CandleDirection::Bear
410 } else {
411 CandleDirection::None
412 }
413 }
414
415 fn fuzzify_size(&self, length: f64, mean_length: f64, sd_lengths: f64) -> CandleSize {
416 if !length.is_finite() || length == 0.0 {
417 return CandleSize::None;
418 }
419
420 let thresholds = [
421 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, ];
427
428 if length <= thresholds[0] {
429 CandleSize::VerySmall
430 } else if length <= thresholds[1] {
431 CandleSize::Small
432 } else if length <= thresholds[2] {
433 CandleSize::Medium
434 } else if length <= thresholds[3] {
435 CandleSize::Large
436 } else if length <= thresholds[4] {
437 CandleSize::VeryLarge
438 } else {
439 CandleSize::ExtremelyLarge
440 }
441 }
442
443 fn fuzzify_body_size(
444 &self,
445 body_percent: f64,
446 mean_body_percent: f64,
447 sd_body_percent: f64,
448 ) -> CandleBodySize {
449 if body_percent == 0.0 {
450 return CandleBodySize::None;
451 }
452
453 let mut x;
454
455 x = sd_body_percent.mul_add(-self.threshold1, mean_body_percent);
456 if body_percent <= x {
457 return CandleBodySize::Small;
458 }
459
460 x = sd_body_percent.mul_add(self.threshold1, mean_body_percent);
461 if body_percent <= x {
462 return CandleBodySize::Medium;
463 }
464
465 x = sd_body_percent.mul_add(self.threshold2, mean_body_percent);
466 if body_percent <= x {
467 return CandleBodySize::Large;
468 }
469
470 CandleBodySize::Trend
471 }
472
473 fn fuzzify_wick_size(
474 &self,
475 wick_percent: f64,
476 mean_wick_percent: f64,
477 sd_wick_percents: f64,
478 ) -> CandleWickSize {
479 if wick_percent == 0.0 {
480 return CandleWickSize::None;
481 }
482
483 let mut x;
484 x = sd_wick_percents.mul_add(-self.threshold1, mean_wick_percent);
485 if wick_percent <= x {
486 return CandleWickSize::Small;
487 }
488
489 x = sd_wick_percents.mul_add(self.threshold2, mean_wick_percent);
490 if wick_percent <= x {
491 return CandleWickSize::Medium;
492 }
493
494 CandleWickSize::Large
495 }
496
497 fn std_dev<const CAP: usize>(buffer: &ArrayDeque<f64, CAP, Wrapping>, mean: f64) -> f64 {
498 if buffer.is_empty() {
499 return 0.0;
500 }
501 let variance = buffer
502 .iter()
503 .map(|v| {
504 let d = v - mean;
505 d * d
506 })
507 .sum::<f64>()
508 / (buffer.len() as f64);
509 variance.sqrt()
510 }
511}
512
513#[cfg(test)]
514mod tests {
515 use rstest::rstest;
516
517 use super::*;
518 use crate::{
519 stubs::{fuzzy_candlesticks_1, fuzzy_candlesticks_3, fuzzy_candlesticks_10},
520 volatility::fuzzy::FuzzyCandlesticks,
521 };
522
523 #[rstest]
524 fn test_psl_initialized(fuzzy_candlesticks_10: FuzzyCandlesticks) {
525 let display_str = format!("{fuzzy_candlesticks_10}");
526 assert_eq!(display_str, "FuzzyCandlesticks(10,0.1,0.15,0.2,0.3)");
527 assert_eq!(fuzzy_candlesticks_10.period, 10);
528 assert!(!fuzzy_candlesticks_10.initialized);
529 assert!(!fuzzy_candlesticks_10.has_inputs);
530 }
531
532 #[rstest]
533 fn test_value_with_one_input(mut fuzzy_candlesticks_1: FuzzyCandlesticks) {
534 fuzzy_candlesticks_1.update_raw(123.90, 135.79, 117.09, 125.09);
536 assert_eq!(fuzzy_candlesticks_1.value.direction, CandleDirection::Bull);
537 assert_eq!(fuzzy_candlesticks_1.value.size, CandleSize::VerySmall);
538 assert_eq!(fuzzy_candlesticks_1.value.body_size, CandleBodySize::Small);
539 assert_eq!(
540 fuzzy_candlesticks_1.value.upper_wick_size,
541 CandleWickSize::Small
542 );
543 assert_eq!(
544 fuzzy_candlesticks_1.value.lower_wick_size,
545 CandleWickSize::Small
546 );
547
548 let expected_vec = vec![1, 1, 1, 1, 1];
549 assert_eq!(fuzzy_candlesticks_1.vector, expected_vec);
550 }
551
552 #[rstest]
553 fn test_value_with_three_inputs(mut fuzzy_candlesticks_3: FuzzyCandlesticks) {
554 fuzzy_candlesticks_3.update_raw(142.35, 145.82, 141.20, 144.75);
556 fuzzy_candlesticks_3.update_raw(144.75, 144.93, 103.55, 108.22);
557 fuzzy_candlesticks_3.update_raw(108.22, 120.15, 105.01, 119.89);
558 assert_eq!(fuzzy_candlesticks_3.value.direction, CandleDirection::Bull);
559 assert_eq!(fuzzy_candlesticks_3.value.size, CandleSize::VerySmall);
560 assert_eq!(fuzzy_candlesticks_3.value.body_size, CandleBodySize::Trend);
561 assert_eq!(
562 fuzzy_candlesticks_3.value.upper_wick_size,
563 CandleWickSize::Small
564 );
565 assert_eq!(
566 fuzzy_candlesticks_3.value.lower_wick_size,
567 CandleWickSize::Large
568 );
569
570 let expected_vec = vec![1, 1, 4, 1, 3];
571 assert_eq!(fuzzy_candlesticks_3.vector, expected_vec);
572 }
573
574 #[rstest]
575 fn test_value_not_updated_before_initialization(mut fuzzy_candlesticks_10: FuzzyCandlesticks) {
576 fuzzy_candlesticks_10.update_raw(100.0, 105.0, 95.0, 102.0);
578 fuzzy_candlesticks_10.update_raw(102.0, 108.0, 100.0, 98.0);
579 fuzzy_candlesticks_10.update_raw(98.0, 101.0, 96.0, 100.0);
580
581 assert_eq!(fuzzy_candlesticks_10.vector.len(), 0);
582 assert!(
583 !fuzzy_candlesticks_10.initialized,
584 "Should not be initialized before period"
585 );
586 assert!(fuzzy_candlesticks_10.has_inputs, "Should has inputs");
587 assert_eq!(fuzzy_candlesticks_10.lengths.len(), 3);
588 assert_eq!(fuzzy_candlesticks_10.body_percents.len(), 3);
589 }
590
591 #[rstest]
592 fn test_value_with_ten_inputs(mut fuzzy_candlesticks_10: FuzzyCandlesticks) {
593 fuzzy_candlesticks_10.update_raw(150.25, 153.4, 148.1, 152.75);
594 fuzzy_candlesticks_10.update_raw(152.8, 155.2, 151.3, 151.95);
595 fuzzy_candlesticks_10.update_raw(151.9, 152.85, 147.6, 148.2);
596 fuzzy_candlesticks_10.update_raw(148.3, 150.75, 146.9, 150.4);
597 fuzzy_candlesticks_10.update_raw(150.5, 154.3, 149.8, 153.9);
598 fuzzy_candlesticks_10.update_raw(153.95, 155.8, 152.2, 152.6);
599 fuzzy_candlesticks_10.update_raw(152.7, 153.4, 148.5, 149.1);
600 fuzzy_candlesticks_10.update_raw(149.2, 151.9, 147.3, 151.5);
601 fuzzy_candlesticks_10.update_raw(151.6, 156.4, 151.0, 155.8);
602 fuzzy_candlesticks_10.update_raw(155.9, 157.2, 153.7, 154.3);
603
604 assert_eq!(fuzzy_candlesticks_10.value.direction, CandleDirection::Bear);
605 assert_eq!(fuzzy_candlesticks_10.value.size, CandleSize::VerySmall);
606 assert_eq!(fuzzy_candlesticks_10.value.body_size, CandleBodySize::Small);
607 assert_eq!(
608 fuzzy_candlesticks_10.value.upper_wick_size,
609 CandleWickSize::Large
610 );
611 assert_eq!(
612 fuzzy_candlesticks_10.value.lower_wick_size,
613 CandleWickSize::Small
614 );
615
616 let expected_vec = vec![-1, 1, 1, 3, 1];
617 assert_eq!(fuzzy_candlesticks_10.vector, expected_vec);
618 }
619
620 #[rstest]
621 fn test_reset(mut fuzzy_candlesticks_10: FuzzyCandlesticks) {
622 fuzzy_candlesticks_10.update_raw(151.6, 156.4, 151.0, 155.8);
623 fuzzy_candlesticks_10.reset();
624 assert_eq!(fuzzy_candlesticks_10.lengths.len(), 0);
625 assert_eq!(fuzzy_candlesticks_10.body_percents.len(), 0);
626 assert_eq!(fuzzy_candlesticks_10.upper_wick_percents.len(), 0);
627 assert_eq!(fuzzy_candlesticks_10.lower_wick_percents.len(), 0);
628 assert_eq!(fuzzy_candlesticks_10.value.direction, CandleDirection::None);
629 assert_eq!(fuzzy_candlesticks_10.value.size, CandleSize::None);
630 assert_eq!(fuzzy_candlesticks_10.value.body_size, CandleBodySize::None);
631 assert_eq!(
632 fuzzy_candlesticks_10.value.upper_wick_size,
633 CandleWickSize::None
634 );
635 assert_eq!(
636 fuzzy_candlesticks_10.value.lower_wick_size,
637 CandleWickSize::None
638 );
639 assert_eq!(fuzzy_candlesticks_10.vector.len(), 0);
640 assert_eq!(fuzzy_candlesticks_10.last_open, 0.0);
641 assert_eq!(fuzzy_candlesticks_10.last_low, 0.0);
642 assert_eq!(fuzzy_candlesticks_10.last_high, 0.0);
643 assert_eq!(fuzzy_candlesticks_10.last_close, 0.0);
644 assert!(!fuzzy_candlesticks_10.has_inputs);
645 assert!(!fuzzy_candlesticks_10.initialized);
646 }
647 #[rstest]
648 fn test_zero_length_candle(mut fuzzy_candlesticks_1: FuzzyCandlesticks) {
649 fuzzy_candlesticks_1.update_raw(100.0, 100.0, 100.0, 100.0); assert_eq!(fuzzy_candlesticks_1.value.size, CandleSize::None);
651 assert_eq!(fuzzy_candlesticks_1.value.body_size, CandleBodySize::None);
652 assert_eq!(
653 fuzzy_candlesticks_1.value.upper_wick_size,
654 CandleWickSize::None
655 );
656 assert_eq!(
657 fuzzy_candlesticks_1.value.lower_wick_size,
658 CandleWickSize::None
659 );
660 assert_eq!(fuzzy_candlesticks_1.value.direction, CandleDirection::None);
661 }
662
663 #[rstest]
664 fn test_constant_input_stddev_zero(mut fuzzy_candlesticks_1: FuzzyCandlesticks) {
665 for _ in 0..10 {
666 fuzzy_candlesticks_1.update_raw(100.0, 110.0, 90.0, 105.0);
667 }
668 assert!(fuzzy_candlesticks_1.lengths.iter().all(|&v| v == 20.0));
669 assert!(matches!(
670 fuzzy_candlesticks_1.value.size,
671 CandleSize::VerySmall | CandleSize::Small | CandleSize::Medium
672 ));
673 }
674
675 #[rstest]
676 fn test_nan_inf_safety(mut fuzzy_candlesticks_1: FuzzyCandlesticks) {
677 fuzzy_candlesticks_1.update_raw(f64::INFINITY, f64::INFINITY, f64::INFINITY, f64::INFINITY);
678 fuzzy_candlesticks_1.update_raw(f64::NAN, f64::NAN, f64::NAN, f64::NAN);
679 assert_eq!(fuzzy_candlesticks_1.value.direction, CandleDirection::None);
680 }
681
682 #[rstest]
683 fn test_direction_cases(mut fuzzy_candlesticks_1: FuzzyCandlesticks) {
684 fuzzy_candlesticks_1.update_raw(100.0, 105.0, 95.0, 110.0); assert_eq!(fuzzy_candlesticks_1.value.direction, CandleDirection::Bull);
686
687 fuzzy_candlesticks_1.update_raw(110.0, 115.0, 105.0, 100.0); assert_eq!(fuzzy_candlesticks_1.value.direction, CandleDirection::Bear);
689
690 fuzzy_candlesticks_1.update_raw(100.0, 110.0, 90.0, 100.0); assert_eq!(fuzzy_candlesticks_1.value.direction, CandleDirection::None);
692 }
693
694 #[rstest]
695 fn test_body_and_wick_percentages(mut fuzzy_candlesticks_1: FuzzyCandlesticks) {
696 let open: f64 = 100.0;
697 let close: f64 = 110.0;
698 let high: f64 = 120.0;
699 let low: f64 = 90.0;
700
701 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);
707
708 let actual_body = fuzzy_candlesticks_1.body_percents[0];
709 let actual_upper = fuzzy_candlesticks_1.upper_wick_percents[0];
710 let actual_lower = fuzzy_candlesticks_1.lower_wick_percents[0];
711
712 assert!(
713 (actual_body - expected_body).abs() < 1e-6,
714 "Body percent mismatch"
715 );
716 assert!(
717 (actual_upper - expected_upper_wick).abs() < 1e-6,
718 "Upper wick percent mismatch"
719 );
720 assert!(
721 (actual_lower - expected_lower_wick).abs() < 1e-6,
722 "Lower wick percent mismatch"
723 );
724 }
725
726 #[rstest]
727 fn test_body_size_large(mut fuzzy_candlesticks_3: FuzzyCandlesticks) {
728 fuzzy_candlesticks_3.update_raw(100.0, 101.0, 99.0, 100.0);
730 fuzzy_candlesticks_3.update_raw(100.0, 102.0, 98.0, 100.5);
734 fuzzy_candlesticks_3.update_raw(101.0, 105.0, 100.0, 104.8);
738 assert_eq!(fuzzy_candlesticks_3.value.body_size, CandleBodySize::Trend);
744 }
745
746 #[rstest]
747 fn test_lower_wick_size_large(mut fuzzy_candlesticks_3: FuzzyCandlesticks) {
748 fuzzy_candlesticks_3.update_raw(100.0, 101.0, 100.0, 101.0);
750 fuzzy_candlesticks_3.update_raw(102.0, 103.0, 101.5, 102.5);
754 fuzzy_candlesticks_3.update_raw(110.0, 115.0, 100.0, 114.0);
761 assert_eq!(
768 fuzzy_candlesticks_3.value.lower_wick_size,
769 CandleWickSize::Large
770 );
771 }
772
773 #[rstest]
774 fn test_upper_wick_size_large(mut fuzzy_candlesticks_3: FuzzyCandlesticks) {
775 fuzzy_candlesticks_3.update_raw(100.0, 100.0, 99.0, 100.0);
777 fuzzy_candlesticks_3.update_raw(101.0, 102.0, 100.0, 101.5);
781 fuzzy_candlesticks_3.update_raw(105.0, 115.0, 104.0, 106.0);
787 assert_eq!(
794 fuzzy_candlesticks_3.value.upper_wick_size,
795 CandleWickSize::Large
796 );
797 }
798}