nautilus_indicators/volatility/
fuzzy.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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    /// Creates a new [`FuzzyCandle`] instance.
211    ///
212    /// # Panics
213    ///
214    /// This function panics if:
215    /// - `period` is greater than `MAX_CAPACITY`.
216    /// - Period: usize : The rolling window period for the indicator (> 0).
217    /// - Threshold1: f64 : The membership function x threshold1 (> 0).
218    /// - Threshold2: f64 : The membership function x threshold2 (> threshold1).
219    /// - Threshold3: f64 : The membership function x threshold3 (> threshold2).
220    /// - Threshold4: f64 : The membership function x threshold4 (> threshold3).
221    #[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////////////////////////////////////////////////////////////////////////////////
465// Tests
466////////////////////////////////////////////////////////////////////////////////
467#[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}