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