1use std::{
17 collections::VecDeque,
18 fmt::{Debug, Display},
19};
20
21use nautilus_model::data::Bar;
22use strum::Display;
23
24use crate::{indicator::Indicator, momentum::bb::fast_std_with_mean};
25
26#[repr(C)]
27#[derive(Debug, Display, Clone, PartialEq, Eq, Copy)]
28#[strum(ascii_case_insensitive)]
29#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
30#[cfg_attr(
31 feature = "python",
32 pyo3::pyclass(eq, eq_int, module = "nautilus_trader.core.nautilus_pyo3.indicators")
33)]
34pub enum CandleBodySize {
35 None = 0,
36 Small = 1,
37 Medium = 2,
38 Large = 3,
39 Trend = 4,
40}
41
42#[repr(C)]
43#[derive(Debug, Display, Clone, PartialEq, Eq, Copy)]
44#[strum(ascii_case_insensitive)]
45#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
46#[cfg_attr(
47 feature = "python",
48 pyo3::pyclass(eq, eq_int, module = "nautilus_trader.core.nautilus_pyo3.indicators")
49)]
50pub enum CandleDirection {
51 Bull = 1,
52 None = 0,
53 Bear = -1,
54}
55
56#[repr(C)]
57#[derive(Debug, Display, Clone, PartialEq, Eq, Copy)]
58#[strum(ascii_case_insensitive)]
59#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
60#[cfg_attr(
61 feature = "python",
62 pyo3::pyclass(eq, eq_int, module = "nautilus_trader.core.nautilus_pyo3.indicators")
63)]
64pub enum CandleSize {
65 None = 0,
66 VerySmall = 1,
67 Small = 2,
68 Medium = 3,
69 Large = 4,
70 VeryLarge = 5,
71 ExtremelyLarge = 6,
72}
73
74#[repr(C)]
75#[derive(Debug, Display, Clone, PartialEq, Eq, Copy)]
76#[strum(ascii_case_insensitive)]
77#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
78#[cfg_attr(
79 feature = "python",
80 pyo3::pyclass(eq, eq_int, module = "nautilus_trader.core.nautilus_pyo3.indicators")
81)]
82pub enum CandleWickSize {
83 None = 0,
84 Small = 1,
85 Medium = 2,
86 Large = 3,
87}
88
89#[repr(C)]
90#[derive(Debug, Clone, Copy)]
91#[cfg_attr(
92 feature = "python",
93 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators")
94)]
95pub struct FuzzyCandle {
96 pub direction: CandleDirection,
97 pub size: CandleSize,
98 pub body_size: CandleBodySize,
99 pub upper_wick_size: CandleWickSize,
100 pub lower_wick_size: CandleWickSize,
101}
102
103impl Display for FuzzyCandle {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 write!(
106 f,
107 "{}({},{},{},{})",
108 self.direction, self.size, self.body_size, self.lower_wick_size, self.upper_wick_size
109 )
110 }
111}
112
113impl FuzzyCandle {
114 #[must_use]
116 pub const fn new(
117 direction: CandleDirection,
118 size: CandleSize,
119 body_size: CandleBodySize,
120 upper_wick_size: CandleWickSize,
121 lower_wick_size: CandleWickSize,
122 ) -> Self {
123 Self {
124 direction,
125 size,
126 body_size,
127 upper_wick_size,
128 lower_wick_size,
129 }
130 }
131}
132
133#[repr(C)]
134#[derive(Debug)]
135#[cfg_attr(
136 feature = "python",
137 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators")
138)]
139pub struct FuzzyCandlesticks {
140 pub period: usize,
141 pub threshold1: f64,
142 pub threshold2: f64,
143 pub threshold3: f64,
144 pub threshold4: f64,
145 pub vector: Vec<i32>,
146 pub value: FuzzyCandle,
147 pub initialized: bool,
148 has_inputs: bool,
149 lengths: VecDeque<f64>,
150 body_percents: VecDeque<f64>,
151 upper_wick_percents: VecDeque<f64>,
152 lower_wick_percents: VecDeque<f64>,
153 last_open: f64,
154 last_high: f64,
155 last_low: f64,
156 last_close: f64,
157}
158
159impl Display for FuzzyCandlesticks {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 write!(
162 f,
163 "{}({},{},{},{},{})",
164 self.name(),
165 self.period,
166 self.threshold1,
167 self.threshold2,
168 self.threshold3,
169 self.threshold4
170 )
171 }
172}
173
174impl Indicator for FuzzyCandlesticks {
175 fn name(&self) -> String {
176 stringify!(FuzzyCandlesticks).to_string()
177 }
178
179 fn has_inputs(&self) -> bool {
180 self.has_inputs
181 }
182
183 fn initialized(&self) -> bool {
184 self.initialized
185 }
186
187 fn handle_bar(&mut self, bar: &Bar) {
188 self.update_raw(
189 (&bar.open).into(),
190 (&bar.high).into(),
191 (&bar.low).into(),
192 (&bar.close).into(),
193 );
194 }
195
196 fn reset(&mut self) {
197 self.lengths.clear();
198 self.body_percents.clear();
199 self.upper_wick_percents.clear();
200 self.lower_wick_percents.clear();
201 self.last_open = 0.0;
202 self.last_high = 0.0;
203 self.last_close = 0.0;
204 self.last_low = 0.0;
205 self.has_inputs = false;
206 self.initialized = false;
207 }
208}
209
210impl FuzzyCandlesticks {
211 #[must_use]
218 pub fn new(
219 period: usize,
220 threshold1: f64,
221 threshold2: f64,
222 threshold3: f64,
223 threshold4: f64,
224 ) -> Self {
225 Self {
226 period,
227 threshold1,
228 threshold2,
229 threshold3,
230 threshold4,
231 vector: Vec::new(),
232 value: FuzzyCandle::new(
233 CandleDirection::None,
234 CandleSize::None,
235 CandleBodySize::None,
236 CandleWickSize::None,
237 CandleWickSize::None,
238 ),
239 has_inputs: false,
240 initialized: false,
241 lengths: VecDeque::with_capacity(period),
242 body_percents: VecDeque::with_capacity(period),
243 upper_wick_percents: VecDeque::with_capacity(period),
244 lower_wick_percents: VecDeque::with_capacity(period),
245 last_open: 0.0,
246 last_high: 0.0,
247 last_low: 0.0,
248 last_close: 0.0,
249 }
250 }
251
252 pub fn update_raw(&mut self, open: f64, high: f64, low: f64, close: f64) {
253 if !self.has_inputs {
255 self.last_close = close;
256 self.last_open = open;
257 self.last_high = high;
258 self.last_low = low;
259 }
260
261 self.last_close = close;
263 self.last_open = open;
264 self.last_high = high;
265 self.last_low = low;
266
267 self.lengths.push_back((high - low).abs());
269
270 if self.lengths[0] == 0.0 {
271 self.body_percents.push_back(0.0);
272 self.upper_wick_percents.push_back(0.0);
273 self.lower_wick_percents.push_back(0.0);
274 } else {
275 self.body_percents
276 .push_back((open - low / self.lengths[0]).abs());
277 self.upper_wick_percents
278 .push_back(high - f64::max(open, close) / self.lengths[0]);
279 self.lower_wick_percents
280 .push_back(f64::max(open, close) - low / self.lengths[0]);
281 }
282
283 let mean_length = self.lengths.iter().sum::<f64>() / self.period as f64;
285 let mean_body_percent = self.body_percents.iter().sum::<f64>() / self.period as f64;
286 let mean_upper_wick_percent =
287 self.upper_wick_percents.iter().sum::<f64>() / self.period as f64;
288 let mean_lower_wick_percent =
289 self.lower_wick_percents.iter().sum::<f64>() / self.period as f64;
290
291 let sd_lengths = fast_std_with_mean(self.lengths.clone(), mean_length);
292 let sd_body_percent = fast_std_with_mean(self.body_percents.clone(), mean_body_percent);
293 let sd_upper_wick_percent =
294 fast_std_with_mean(self.upper_wick_percents.clone(), mean_upper_wick_percent);
295 let sd_lower_wick_percent =
296 fast_std_with_mean(self.lower_wick_percents.clone(), mean_lower_wick_percent);
297
298 self.value = FuzzyCandle::new(
300 self.fuzzify_direction(open, close),
301 self.fuzzify_size(self.lengths[0], mean_length, sd_lengths),
302 self.fuzzify_body_size(self.body_percents[0], mean_body_percent, sd_body_percent),
303 self.fuzzify_wick_size(
304 self.upper_wick_percents[0],
305 mean_upper_wick_percent,
306 sd_upper_wick_percent,
307 ),
308 self.fuzzify_wick_size(
309 self.lower_wick_percents[0],
310 mean_lower_wick_percent,
311 sd_lower_wick_percent,
312 ),
313 );
314
315 self.vector = vec![
316 self.value.direction as i32,
317 self.value.size as i32,
318 self.value.body_size as i32,
319 self.value.upper_wick_size as i32,
320 self.value.lower_wick_size as i32,
321 ];
322 }
323
324 pub fn reset(&mut self) {
325 self.lengths.clear();
326 self.body_percents.clear();
327 self.upper_wick_percents.clear();
328 self.lower_wick_percents.clear();
329 self.value = FuzzyCandle::new(
330 CandleDirection::None,
331 CandleSize::None,
332 CandleBodySize::None,
333 CandleWickSize::None,
334 CandleWickSize::None,
335 );
336 self.vector = Vec::new();
337 self.last_open = 0.0;
338 self.last_high = 0.0;
339 self.last_close = 0.0;
340 self.last_low = 0.0;
341 self.has_inputs = false;
342 self.initialized = false;
343 }
344
345 fn fuzzify_direction(&self, open: f64, close: f64) -> CandleDirection {
346 if close > open {
347 CandleDirection::Bull
348 } else if close < open {
349 CandleDirection::Bear
350 } else {
351 CandleDirection::None
352 }
353 }
354
355 fn fuzzify_size(&self, length: f64, mean_length: f64, sd_lengths: f64) -> CandleSize {
356 if length == 0.0 {
358 return CandleSize::None;
359 }
360
361 let mut x;
362
363 x = sd_lengths.mul_add(-self.threshold2, mean_length);
367 if length <= x {
368 return CandleSize::VerySmall;
369 }
370
371 x = sd_lengths.mul_add(self.threshold1, mean_length);
373 if length <= x {
374 return CandleSize::Small;
375 }
376
377 x = sd_lengths * self.threshold2;
379 if length <= x {
380 return CandleSize::Medium;
381 }
382
383 x = sd_lengths.mul_add(self.threshold3, mean_length);
385 if length <= x {
386 return CandleSize::Large;
387 }
388
389 x = sd_lengths.mul_add(self.threshold4, mean_length);
391 if length <= x {
392 return CandleSize::VeryLarge;
393 }
394
395 CandleSize::ExtremelyLarge
396 }
397
398 fn fuzzify_body_size(
399 &self,
400 body_percent: f64,
401 mean_body_percent: f64,
402 sd_body_percent: f64,
403 ) -> CandleBodySize {
404 if body_percent == 0.0 {
406 return CandleBodySize::None;
407 }
408
409 let mut x;
410
411 x = sd_body_percent.mul_add(-self.threshold1, mean_body_percent);
415 if body_percent <= x {
416 return CandleBodySize::Small;
417 }
418
419 x = sd_body_percent.mul_add(self.threshold1, mean_body_percent);
421 if body_percent <= x {
422 return CandleBodySize::Medium;
423 }
424
425 x = sd_body_percent.mul_add(self.threshold2, mean_body_percent);
427 if body_percent <= x {
428 return CandleBodySize::Large;
429 }
430
431 CandleBodySize::Trend
432 }
433
434 fn fuzzify_wick_size(
435 &self,
436 wick_percent: f64,
437 mean_wick_percent: f64,
438 sd_wick_percents: f64,
439 ) -> CandleWickSize {
440 if wick_percent == 0.0 {
442 return CandleWickSize::None;
443 }
444
445 let mut x;
446
447 x = sd_wick_percents.mul_add(-self.threshold1, mean_wick_percent);
451 if wick_percent <= x {
452 return CandleWickSize::Small;
453 }
454
455 x = sd_wick_percents.mul_add(self.threshold2, mean_wick_percent);
457 if wick_percent <= x {
458 return CandleWickSize::Medium;
459 }
460
461 CandleWickSize::Large
462 }
463}
464
465#[cfg(test)]
469mod tests {
470 use rstest::rstest;
471
472 use super::*;
473 use crate::{stubs::fuzzy_candlesticks_10, volatility::fuzzy::FuzzyCandlesticks};
474
475 #[rstest]
476 fn test_psl_initialized(fuzzy_candlesticks_10: FuzzyCandlesticks) {
477 let display_str = format!("{fuzzy_candlesticks_10}");
478 assert_eq!(display_str, "FuzzyCandlesticks(10,0.1,0.15,0.2,0.3)");
479 assert_eq!(fuzzy_candlesticks_10.period, 10);
480 assert!(!fuzzy_candlesticks_10.initialized);
481 assert!(!fuzzy_candlesticks_10.has_inputs);
482 }
483
484 #[rstest]
485 fn test_value_with_one_input(mut fuzzy_candlesticks_10: FuzzyCandlesticks) {
486 fuzzy_candlesticks_10.update_raw(123.90, 135.79, 117.09, 125.09);
487 assert_eq!(fuzzy_candlesticks_10.value.direction, CandleDirection::Bull);
488 assert_eq!(fuzzy_candlesticks_10.value.size, CandleSize::ExtremelyLarge);
489 assert_eq!(fuzzy_candlesticks_10.value.body_size, CandleBodySize::Trend);
490 assert_eq!(
491 fuzzy_candlesticks_10.value.upper_wick_size,
492 CandleWickSize::Large
493 );
494 assert_eq!(
495 fuzzy_candlesticks_10.value.lower_wick_size,
496 CandleWickSize::Large
497 );
498
499 let expected_vec = vec![1, 6, 4, 3, 3];
500 assert_eq!(fuzzy_candlesticks_10.vector, expected_vec);
501 }
502
503 #[rstest]
504 fn test_value_with_three_inputs(mut fuzzy_candlesticks_10: FuzzyCandlesticks) {
505 fuzzy_candlesticks_10.update_raw(142.35, 145.82, 141.20, 144.75);
506 fuzzy_candlesticks_10.update_raw(144.75, 144.93, 103.55, 108.22);
507 fuzzy_candlesticks_10.update_raw(108.22, 120.15, 105.01, 119.89);
508 assert_eq!(fuzzy_candlesticks_10.value.direction, CandleDirection::Bull);
509 assert_eq!(fuzzy_candlesticks_10.value.size, CandleSize::Small);
510 assert_eq!(fuzzy_candlesticks_10.value.body_size, CandleBodySize::Trend);
511 assert_eq!(
512 fuzzy_candlesticks_10.value.upper_wick_size,
513 CandleWickSize::Large
514 );
515 assert_eq!(
516 fuzzy_candlesticks_10.value.lower_wick_size,
517 CandleWickSize::Large
518 );
519
520 let expected_vec = vec![1, 2, 4, 3, 3];
521 assert_eq!(fuzzy_candlesticks_10.vector, expected_vec);
522 }
523
524 #[rstest]
525 fn test_value_with_ten_inputs(mut fuzzy_candlesticks_10: FuzzyCandlesticks) {
526 fuzzy_candlesticks_10.update_raw(150.25, 153.40, 148.10, 152.75);
527 fuzzy_candlesticks_10.update_raw(152.80, 155.20, 151.30, 151.95);
528 fuzzy_candlesticks_10.update_raw(151.90, 152.85, 147.60, 148.20);
529 fuzzy_candlesticks_10.update_raw(148.30, 150.75, 146.90, 150.40);
530 fuzzy_candlesticks_10.update_raw(150.50, 154.30, 149.80, 153.90);
531 fuzzy_candlesticks_10.update_raw(153.95, 155.80, 152.20, 152.60);
532 fuzzy_candlesticks_10.update_raw(152.70, 153.40, 148.50, 149.10);
533 fuzzy_candlesticks_10.update_raw(149.20, 151.90, 147.30, 151.50);
534 fuzzy_candlesticks_10.update_raw(151.60, 156.40, 151.00, 155.80);
535 fuzzy_candlesticks_10.update_raw(155.90, 157.20, 153.70, 154.30);
536
537 assert_eq!(fuzzy_candlesticks_10.value.direction, CandleDirection::Bear);
538 assert_eq!(fuzzy_candlesticks_10.value.size, CandleSize::ExtremelyLarge);
539 assert_eq!(fuzzy_candlesticks_10.value.body_size, CandleBodySize::Small);
540 assert_eq!(
541 fuzzy_candlesticks_10.value.upper_wick_size,
542 CandleWickSize::Small
543 );
544 assert_eq!(
545 fuzzy_candlesticks_10.value.lower_wick_size,
546 CandleWickSize::Medium
547 );
548
549 let expected_vec = vec![-1, 6, 1, 1, 2];
550 assert_eq!(fuzzy_candlesticks_10.vector, expected_vec);
551 }
552
553 #[rstest]
554 fn test_reset(mut fuzzy_candlesticks_10: FuzzyCandlesticks) {
555 fuzzy_candlesticks_10.update_raw(151.60, 156.40, 151.00, 155.80);
556 fuzzy_candlesticks_10.reset();
557 assert_eq!(fuzzy_candlesticks_10.lengths.len(), 0);
558 assert_eq!(fuzzy_candlesticks_10.body_percents.len(), 0);
559 assert_eq!(fuzzy_candlesticks_10.upper_wick_percents.len(), 0);
560 assert_eq!(fuzzy_candlesticks_10.lower_wick_percents.len(), 0);
561 assert_eq!(fuzzy_candlesticks_10.value.direction, CandleDirection::None);
562 assert_eq!(fuzzy_candlesticks_10.value.size, CandleSize::None);
563 assert_eq!(fuzzy_candlesticks_10.value.body_size, CandleBodySize::None);
564 assert_eq!(
565 fuzzy_candlesticks_10.value.upper_wick_size,
566 CandleWickSize::None
567 );
568 assert_eq!(
569 fuzzy_candlesticks_10.value.lower_wick_size,
570 CandleWickSize::None
571 );
572 assert_eq!(fuzzy_candlesticks_10.vector.len(), 0);
573 assert_eq!(fuzzy_candlesticks_10.last_open, 0.0);
574 assert_eq!(fuzzy_candlesticks_10.last_low, 0.0);
575 assert_eq!(fuzzy_candlesticks_10.last_high, 0.0);
576 assert_eq!(fuzzy_candlesticks_10.last_close, 0.0);
577 assert!(!fuzzy_candlesticks_10.has_inputs);
578 assert!(!fuzzy_candlesticks_10.initialized);
579 }
580}