nautilus_indicators/volatility/
fuzzy.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.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::{
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    /// Creates a new [`FuzzyCandle`] instance.
115    #[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    /// Creates a new [`FuzzyCandle`] instance.
212    /// - Period: usize : The rolling window period for the indicator (> 0).
213    /// - Threshold1: f64 : The membership function x threshold1 (>= 0).
214    /// - Threshold2: f64 : The membership function x threshold2 (> threshold1).
215    /// - Threshold3: f64 : The membership function x threshold3 (> threshold2).
216    /// - Threshold4: f64 : The membership function x threshold4 (> threshold3).
217    #[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        //check if this is the first input
254        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        // Update last prices
262        self.last_close = close;
263        self.last_open = open;
264        self.last_high = high;
265        self.last_low = low;
266
267        // Update measurements
268        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        // Calculate statistics for bars
284        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        // Create fuzzy candle
299        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        // Fuzzify the candle size from the given inputs
357        if length == 0.0 {
358            return CandleSize::None;
359        }
360
361        let mut x;
362
363        // Determine CandleSize fuzzy membership
364        // -------------------------------------
365        // CandleSize::VerySmall
366        x = sd_lengths.mul_add(-self.threshold2, mean_length);
367        if length <= x {
368            return CandleSize::VerySmall;
369        }
370
371        // CandleSize::Small
372        x = sd_lengths.mul_add(self.threshold1, mean_length);
373        if length <= x {
374            return CandleSize::Small;
375        }
376
377        // CandleSize::Medium
378        x = sd_lengths * self.threshold2;
379        if length <= x {
380            return CandleSize::Medium;
381        }
382
383        // CandleSize.Large
384        x = sd_lengths.mul_add(self.threshold3, mean_length);
385        if length <= x {
386            return CandleSize::Large;
387        }
388
389        // CandleSize::VeryLarge
390        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        // Fuzzify the candle body size from the given inputs
405        if body_percent == 0.0 {
406            return CandleBodySize::None;
407        }
408
409        let mut x;
410
411        // Determine CandleBodySize fuzzy membership
412        // -------------------------------------
413        // CandleBodySize::Small
414        x = sd_body_percent.mul_add(-self.threshold1, mean_body_percent);
415        if body_percent <= x {
416            return CandleBodySize::Small;
417        }
418
419        // CandleBodySize::Medium
420        x = sd_body_percent.mul_add(self.threshold1, mean_body_percent);
421        if body_percent <= x {
422            return CandleBodySize::Medium;
423        }
424
425        // CandleBodySize::Large
426        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        // Fuzzify the candle wick size from the given inputs
441        if wick_percent == 0.0 {
442            return CandleWickSize::None;
443        }
444
445        let mut x;
446
447        // Determine CandleWickSize fuzzy membership
448        // -------------------------------------
449        // CandleWickSize::Small
450        x = sd_wick_percents.mul_add(-self.threshold1, mean_wick_percent);
451        if wick_percent <= x {
452            return CandleWickSize::Small;
453        }
454
455        // CandleWickSize::Medium
456        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////////////////////////////////////////////////////////////////////////////////
466// Tests
467////////////////////////////////////////////////////////////////////////////////
468#[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}