use std::fmt::{Debug, Display};
use arraydeque::{ArrayDeque, Wrapping};
use nautilus_model::data::Bar;
use strum::Display;
use crate::indicator::Indicator;
#[repr(C)]
#[derive(Debug, Display, Clone, Hash, PartialEq, Eq, Copy)]
#[strum(ascii_case_insensitive)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
pyo3::pyclass(
frozen,
eq,
eq_int,
hash,
module = "nautilus_trader.core.nautilus_pyo3.indicators",
from_py_object,
)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.indicators")
)]
pub enum CandleBodySize {
None = 0,
Small = 1,
Medium = 2,
Large = 3,
Trend = 4,
}
#[repr(C)]
#[derive(Debug, Display, Clone, Hash, PartialEq, Eq, Copy)]
#[strum(ascii_case_insensitive)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
pyo3::pyclass(
frozen,
eq,
eq_int,
hash,
module = "nautilus_trader.core.nautilus_pyo3.indicators",
from_py_object,
)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.indicators")
)]
pub enum CandleDirection {
Bull = 1,
None = 0,
Bear = -1,
}
#[repr(C)]
#[derive(Debug, Display, Clone, Hash, PartialEq, Eq, Copy)]
#[strum(ascii_case_insensitive)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
pyo3::pyclass(
frozen,
eq,
eq_int,
hash,
module = "nautilus_trader.core.nautilus_pyo3.indicators",
from_py_object,
)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.indicators")
)]
pub enum CandleSize {
None = 0,
VerySmall = 1,
Small = 2,
Medium = 3,
Large = 4,
VeryLarge = 5,
ExtremelyLarge = 6,
}
#[repr(C)]
#[derive(Debug, Display, Clone, Hash, PartialEq, Eq, Copy)]
#[strum(ascii_case_insensitive)]
#[strum(serialize_all = "SCREAMING_SNAKE_CASE")]
#[cfg_attr(
feature = "python",
pyo3::pyclass(
frozen,
eq,
eq_int,
hash,
module = "nautilus_trader.core.nautilus_pyo3.indicators",
from_py_object,
)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass_enum(module = "nautilus_trader.indicators")
)]
pub enum CandleWickSize {
None = 0,
Small = 1,
Medium = 2,
Large = 3,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(
module = "nautilus_trader.core.nautilus_pyo3.indicators",
from_py_object
)
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
)]
pub struct FuzzyCandle {
pub direction: CandleDirection,
pub size: CandleSize,
pub body_size: CandleBodySize,
pub upper_wick_size: CandleWickSize,
pub lower_wick_size: CandleWickSize,
}
impl Display for FuzzyCandle {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}({},{},{},{})",
self.direction, self.size, self.body_size, self.lower_wick_size, self.upper_wick_size
)
}
}
impl FuzzyCandle {
#[must_use]
pub const fn new(
direction: CandleDirection,
size: CandleSize,
body_size: CandleBodySize,
upper_wick_size: CandleWickSize,
lower_wick_size: CandleWickSize,
) -> Self {
Self {
direction,
size,
body_size,
upper_wick_size,
lower_wick_size,
}
}
}
const MAX_CAPACITY: usize = 1024;
#[repr(C)]
#[derive(Debug)]
#[cfg_attr(
feature = "python",
pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators")
)]
#[cfg_attr(
feature = "python",
pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
)]
pub struct FuzzyCandlesticks {
pub period: usize,
pub threshold1: f64,
pub threshold2: f64,
pub threshold3: f64,
pub threshold4: f64,
pub vector: Vec<i32>,
pub value: FuzzyCandle,
pub initialized: bool,
has_inputs: bool,
lengths: ArrayDeque<f64, MAX_CAPACITY, Wrapping>,
body_percents: ArrayDeque<f64, MAX_CAPACITY, Wrapping>,
upper_wick_percents: ArrayDeque<f64, MAX_CAPACITY, Wrapping>,
lower_wick_percents: ArrayDeque<f64, MAX_CAPACITY, Wrapping>,
last_open: f64,
last_high: f64,
last_low: f64,
last_close: f64,
}
impl Display for FuzzyCandlesticks {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}({},{},{},{},{})",
self.name(),
self.period,
self.threshold1,
self.threshold2,
self.threshold3,
self.threshold4
)
}
}
impl Indicator for FuzzyCandlesticks {
fn name(&self) -> String {
stringify!(FuzzyCandlesticks).to_string()
}
fn has_inputs(&self) -> bool {
self.has_inputs
}
fn initialized(&self) -> bool {
self.initialized
}
fn handle_bar(&mut self, bar: &Bar) {
self.update_raw(
(&bar.open).into(),
(&bar.high).into(),
(&bar.low).into(),
(&bar.close).into(),
);
}
fn reset(&mut self) {
self.lengths.clear();
self.body_percents.clear();
self.upper_wick_percents.clear();
self.lower_wick_percents.clear();
self.last_open = 0.0;
self.last_high = 0.0;
self.last_close = 0.0;
self.last_low = 0.0;
self.has_inputs = false;
self.initialized = false;
}
}
impl FuzzyCandlesticks {
#[must_use]
pub fn new(
period: usize,
threshold1: f64,
threshold2: f64,
threshold3: f64,
threshold4: f64,
) -> Self {
assert!(period <= MAX_CAPACITY);
Self {
period,
threshold1,
threshold2,
threshold3,
threshold4,
vector: Vec::new(),
value: FuzzyCandle::new(
CandleDirection::None,
CandleSize::None,
CandleBodySize::None,
CandleWickSize::None,
CandleWickSize::None,
),
has_inputs: false,
initialized: false,
lengths: ArrayDeque::new(),
body_percents: ArrayDeque::new(),
upper_wick_percents: ArrayDeque::new(),
lower_wick_percents: ArrayDeque::new(),
last_open: 0.0,
last_high: 0.0,
last_low: 0.0,
last_close: 0.0,
}
}
pub fn update_raw(&mut self, open: f64, high: f64, low: f64, close: f64) {
if !self.has_inputs {
self.last_close = close;
self.last_open = open;
self.last_high = high;
self.last_low = low;
self.has_inputs = true;
}
self.last_close = close;
self.last_open = open;
self.last_high = high;
self.last_low = low;
let total = (high - low).abs();
let _ = self.lengths.push_back(total);
if total == 0.0 {
let _ = self.body_percents.push_back(0.0);
let _ = self.upper_wick_percents.push_back(0.0);
let _ = self.lower_wick_percents.push_back(0.0);
} else {
let body = (close - open).abs();
let upper_wick = high - f64::max(open, close);
let lower_wick = f64::min(open, close) - low;
let _ = self.body_percents.push_back(body / total);
let _ = self.upper_wick_percents.push_back(upper_wick / total);
let _ = self.lower_wick_percents.push_back(lower_wick / total);
}
if self.lengths.len() >= self.period {
self.initialized = true;
}
if !self.initialized {
return;
}
let mean_length = self.lengths.iter().sum::<f64>() / (self.period as f64);
let mean_body_percent = self.body_percents.iter().sum::<f64>() / (self.period as f64);
let mean_upper_percent =
self.upper_wick_percents.iter().sum::<f64>() / (self.period as f64);
let mean_lower_percent =
self.lower_wick_percents.iter().sum::<f64>() / (self.period as f64);
let sd_length = Self::std_dev(&self.lengths, mean_length);
let sd_body = Self::std_dev(&self.body_percents, mean_body_percent);
let sd_upper = Self::std_dev(&self.upper_wick_percents, mean_upper_percent);
let sd_lower = Self::std_dev(&self.lower_wick_percents, mean_lower_percent);
let latest_body = *self.body_percents.back().unwrap_or(&0.0);
let latest_upper = *self.upper_wick_percents.back().unwrap_or(&0.0);
let latest_lower = *self.lower_wick_percents.back().unwrap_or(&0.0);
self.value = FuzzyCandle::new(
self.fuzzify_direction(open, close),
self.fuzzify_size(total, mean_length, sd_length),
self.fuzzify_body_size(latest_body, mean_body_percent, sd_body),
self.fuzzify_wick_size(latest_upper, mean_upper_percent, sd_upper),
self.fuzzify_wick_size(latest_lower, mean_lower_percent, sd_lower),
);
self.vector = vec![
self.value.direction as i32,
self.value.size as i32,
self.value.body_size as i32,
self.value.upper_wick_size as i32,
self.value.lower_wick_size as i32,
];
}
pub fn reset(&mut self) {
self.lengths.clear();
self.body_percents.clear();
self.upper_wick_percents.clear();
self.lower_wick_percents.clear();
self.value = FuzzyCandle::new(
CandleDirection::None,
CandleSize::None,
CandleBodySize::None,
CandleWickSize::None,
CandleWickSize::None,
);
self.vector = Vec::new();
self.last_open = 0.0;
self.last_high = 0.0;
self.last_close = 0.0;
self.last_low = 0.0;
self.has_inputs = false;
self.initialized = false;
}
fn fuzzify_direction(&self, open: f64, close: f64) -> CandleDirection {
if close > open {
CandleDirection::Bull
} else if close < open {
CandleDirection::Bear
} else {
CandleDirection::None
}
}
fn fuzzify_size(&self, length: f64, mean_length: f64, sd_lengths: f64) -> CandleSize {
if !length.is_finite() || length == 0.0 {
return CandleSize::None;
}
let thresholds = [
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, ];
if length <= thresholds[0] {
CandleSize::VerySmall
} else if length <= thresholds[1] {
CandleSize::Small
} else if length <= thresholds[2] {
CandleSize::Medium
} else if length <= thresholds[3] {
CandleSize::Large
} else if length <= thresholds[4] {
CandleSize::VeryLarge
} else {
CandleSize::ExtremelyLarge
}
}
fn fuzzify_body_size(
&self,
body_percent: f64,
mean_body_percent: f64,
sd_body_percent: f64,
) -> CandleBodySize {
if body_percent == 0.0 {
return CandleBodySize::None;
}
let mut x;
x = sd_body_percent.mul_add(-self.threshold1, mean_body_percent);
if body_percent <= x {
return CandleBodySize::Small;
}
x = sd_body_percent.mul_add(self.threshold1, mean_body_percent);
if body_percent <= x {
return CandleBodySize::Medium;
}
x = sd_body_percent.mul_add(self.threshold2, mean_body_percent);
if body_percent <= x {
return CandleBodySize::Large;
}
CandleBodySize::Trend
}
fn fuzzify_wick_size(
&self,
wick_percent: f64,
mean_wick_percent: f64,
sd_wick_percents: f64,
) -> CandleWickSize {
if wick_percent == 0.0 {
return CandleWickSize::None;
}
let mut x;
x = sd_wick_percents.mul_add(-self.threshold1, mean_wick_percent);
if wick_percent <= x {
return CandleWickSize::Small;
}
x = sd_wick_percents.mul_add(self.threshold2, mean_wick_percent);
if wick_percent <= x {
return CandleWickSize::Medium;
}
CandleWickSize::Large
}
fn std_dev<const CAP: usize>(buffer: &ArrayDeque<f64, CAP, Wrapping>, mean: f64) -> f64 {
if buffer.is_empty() {
return 0.0;
}
let variance = buffer
.iter()
.map(|v| {
let d = v - mean;
d * d
})
.sum::<f64>()
/ (buffer.len() as f64);
variance.sqrt()
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
use crate::{
stubs::{fuzzy_candlesticks_1, fuzzy_candlesticks_3, fuzzy_candlesticks_10},
volatility::fuzzy::FuzzyCandlesticks,
};
#[rstest]
fn test_psl_initialized(fuzzy_candlesticks_10: FuzzyCandlesticks) {
let display_str = format!("{fuzzy_candlesticks_10}");
assert_eq!(display_str, "FuzzyCandlesticks(10,0.1,0.15,0.2,0.3)");
assert_eq!(fuzzy_candlesticks_10.period, 10);
assert!(!fuzzy_candlesticks_10.initialized);
assert!(!fuzzy_candlesticks_10.has_inputs);
}
#[rstest]
fn test_value_with_one_input(mut fuzzy_candlesticks_1: FuzzyCandlesticks) {
fuzzy_candlesticks_1.update_raw(123.90, 135.79, 117.09, 125.09);
assert_eq!(fuzzy_candlesticks_1.value.direction, CandleDirection::Bull);
assert_eq!(fuzzy_candlesticks_1.value.size, CandleSize::VerySmall);
assert_eq!(fuzzy_candlesticks_1.value.body_size, CandleBodySize::Small);
assert_eq!(
fuzzy_candlesticks_1.value.upper_wick_size,
CandleWickSize::Small
);
assert_eq!(
fuzzy_candlesticks_1.value.lower_wick_size,
CandleWickSize::Small
);
let expected_vec = vec![1, 1, 1, 1, 1];
assert_eq!(fuzzy_candlesticks_1.vector, expected_vec);
}
#[rstest]
fn test_value_with_three_inputs(mut fuzzy_candlesticks_3: FuzzyCandlesticks) {
fuzzy_candlesticks_3.update_raw(142.35, 145.82, 141.20, 144.75);
fuzzy_candlesticks_3.update_raw(144.75, 144.93, 103.55, 108.22);
fuzzy_candlesticks_3.update_raw(108.22, 120.15, 105.01, 119.89);
assert_eq!(fuzzy_candlesticks_3.value.direction, CandleDirection::Bull);
assert_eq!(fuzzy_candlesticks_3.value.size, CandleSize::VerySmall);
assert_eq!(fuzzy_candlesticks_3.value.body_size, CandleBodySize::Trend);
assert_eq!(
fuzzy_candlesticks_3.value.upper_wick_size,
CandleWickSize::Small
);
assert_eq!(
fuzzy_candlesticks_3.value.lower_wick_size,
CandleWickSize::Large
);
let expected_vec = vec![1, 1, 4, 1, 3];
assert_eq!(fuzzy_candlesticks_3.vector, expected_vec);
}
#[rstest]
fn test_value_not_updated_before_initialization(mut fuzzy_candlesticks_10: FuzzyCandlesticks) {
fuzzy_candlesticks_10.update_raw(100.0, 105.0, 95.0, 102.0);
fuzzy_candlesticks_10.update_raw(102.0, 108.0, 100.0, 98.0);
fuzzy_candlesticks_10.update_raw(98.0, 101.0, 96.0, 100.0);
assert_eq!(fuzzy_candlesticks_10.vector.len(), 0);
assert!(
!fuzzy_candlesticks_10.initialized,
"Should not be initialized before period"
);
assert!(fuzzy_candlesticks_10.has_inputs, "Should has inputs");
assert_eq!(fuzzy_candlesticks_10.lengths.len(), 3);
assert_eq!(fuzzy_candlesticks_10.body_percents.len(), 3);
}
#[rstest]
fn test_value_with_ten_inputs(mut fuzzy_candlesticks_10: FuzzyCandlesticks) {
fuzzy_candlesticks_10.update_raw(150.25, 153.4, 148.1, 152.75);
fuzzy_candlesticks_10.update_raw(152.8, 155.2, 151.3, 151.95);
fuzzy_candlesticks_10.update_raw(151.9, 152.85, 147.6, 148.2);
fuzzy_candlesticks_10.update_raw(148.3, 150.75, 146.9, 150.4);
fuzzy_candlesticks_10.update_raw(150.5, 154.3, 149.8, 153.9);
fuzzy_candlesticks_10.update_raw(153.95, 155.8, 152.2, 152.6);
fuzzy_candlesticks_10.update_raw(152.7, 153.4, 148.5, 149.1);
fuzzy_candlesticks_10.update_raw(149.2, 151.9, 147.3, 151.5);
fuzzy_candlesticks_10.update_raw(151.6, 156.4, 151.0, 155.8);
fuzzy_candlesticks_10.update_raw(155.9, 157.2, 153.7, 154.3);
assert_eq!(fuzzy_candlesticks_10.value.direction, CandleDirection::Bear);
assert_eq!(fuzzy_candlesticks_10.value.size, CandleSize::VerySmall);
assert_eq!(fuzzy_candlesticks_10.value.body_size, CandleBodySize::Small);
assert_eq!(
fuzzy_candlesticks_10.value.upper_wick_size,
CandleWickSize::Large
);
assert_eq!(
fuzzy_candlesticks_10.value.lower_wick_size,
CandleWickSize::Small
);
let expected_vec = vec![-1, 1, 1, 3, 1];
assert_eq!(fuzzy_candlesticks_10.vector, expected_vec);
}
#[rstest]
fn test_reset(mut fuzzy_candlesticks_10: FuzzyCandlesticks) {
fuzzy_candlesticks_10.update_raw(151.6, 156.4, 151.0, 155.8);
fuzzy_candlesticks_10.reset();
assert_eq!(fuzzy_candlesticks_10.lengths.len(), 0);
assert_eq!(fuzzy_candlesticks_10.body_percents.len(), 0);
assert_eq!(fuzzy_candlesticks_10.upper_wick_percents.len(), 0);
assert_eq!(fuzzy_candlesticks_10.lower_wick_percents.len(), 0);
assert_eq!(fuzzy_candlesticks_10.value.direction, CandleDirection::None);
assert_eq!(fuzzy_candlesticks_10.value.size, CandleSize::None);
assert_eq!(fuzzy_candlesticks_10.value.body_size, CandleBodySize::None);
assert_eq!(
fuzzy_candlesticks_10.value.upper_wick_size,
CandleWickSize::None
);
assert_eq!(
fuzzy_candlesticks_10.value.lower_wick_size,
CandleWickSize::None
);
assert_eq!(fuzzy_candlesticks_10.vector.len(), 0);
assert_eq!(fuzzy_candlesticks_10.last_open, 0.0);
assert_eq!(fuzzy_candlesticks_10.last_low, 0.0);
assert_eq!(fuzzy_candlesticks_10.last_high, 0.0);
assert_eq!(fuzzy_candlesticks_10.last_close, 0.0);
assert!(!fuzzy_candlesticks_10.has_inputs);
assert!(!fuzzy_candlesticks_10.initialized);
}
#[rstest]
fn test_zero_length_candle(mut fuzzy_candlesticks_1: FuzzyCandlesticks) {
fuzzy_candlesticks_1.update_raw(100.0, 100.0, 100.0, 100.0); assert_eq!(fuzzy_candlesticks_1.value.size, CandleSize::None);
assert_eq!(fuzzy_candlesticks_1.value.body_size, CandleBodySize::None);
assert_eq!(
fuzzy_candlesticks_1.value.upper_wick_size,
CandleWickSize::None
);
assert_eq!(
fuzzy_candlesticks_1.value.lower_wick_size,
CandleWickSize::None
);
assert_eq!(fuzzy_candlesticks_1.value.direction, CandleDirection::None);
}
#[rstest]
fn test_constant_input_stddev_zero(mut fuzzy_candlesticks_1: FuzzyCandlesticks) {
for _ in 0..10 {
fuzzy_candlesticks_1.update_raw(100.0, 110.0, 90.0, 105.0);
}
assert!(fuzzy_candlesticks_1.lengths.iter().all(|&v| v == 20.0));
assert!(matches!(
fuzzy_candlesticks_1.value.size,
CandleSize::VerySmall | CandleSize::Small | CandleSize::Medium
));
}
#[rstest]
fn test_nan_inf_safety(mut fuzzy_candlesticks_1: FuzzyCandlesticks) {
fuzzy_candlesticks_1.update_raw(f64::INFINITY, f64::INFINITY, f64::INFINITY, f64::INFINITY);
fuzzy_candlesticks_1.update_raw(f64::NAN, f64::NAN, f64::NAN, f64::NAN);
assert_eq!(fuzzy_candlesticks_1.value.direction, CandleDirection::None);
}
#[rstest]
fn test_direction_cases(mut fuzzy_candlesticks_1: FuzzyCandlesticks) {
fuzzy_candlesticks_1.update_raw(100.0, 105.0, 95.0, 110.0); assert_eq!(fuzzy_candlesticks_1.value.direction, CandleDirection::Bull);
fuzzy_candlesticks_1.update_raw(110.0, 115.0, 105.0, 100.0); assert_eq!(fuzzy_candlesticks_1.value.direction, CandleDirection::Bear);
fuzzy_candlesticks_1.update_raw(100.0, 110.0, 90.0, 100.0); assert_eq!(fuzzy_candlesticks_1.value.direction, CandleDirection::None);
}
#[rstest]
fn test_body_and_wick_percentages(mut fuzzy_candlesticks_1: FuzzyCandlesticks) {
let open: f64 = 100.0;
let close: f64 = 110.0;
let high: f64 = 120.0;
let low: f64 = 90.0;
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);
let actual_body = fuzzy_candlesticks_1.body_percents[0];
let actual_upper = fuzzy_candlesticks_1.upper_wick_percents[0];
let actual_lower = fuzzy_candlesticks_1.lower_wick_percents[0];
assert!(
(actual_body - expected_body).abs() < 1e-6,
"Body percent mismatch"
);
assert!(
(actual_upper - expected_upper_wick).abs() < 1e-6,
"Upper wick percent mismatch"
);
assert!(
(actual_lower - expected_lower_wick).abs() < 1e-6,
"Lower wick percent mismatch"
);
}
#[rstest]
fn test_body_size_large(mut fuzzy_candlesticks_3: FuzzyCandlesticks) {
fuzzy_candlesticks_3.update_raw(100.0, 101.0, 99.0, 100.0);
fuzzy_candlesticks_3.update_raw(100.0, 102.0, 98.0, 100.5);
fuzzy_candlesticks_3.update_raw(101.0, 105.0, 100.0, 104.8);
assert_eq!(fuzzy_candlesticks_3.value.body_size, CandleBodySize::Trend);
}
#[rstest]
fn test_lower_wick_size_large(mut fuzzy_candlesticks_3: FuzzyCandlesticks) {
fuzzy_candlesticks_3.update_raw(100.0, 101.0, 100.0, 101.0);
fuzzy_candlesticks_3.update_raw(102.0, 103.0, 101.5, 102.5);
fuzzy_candlesticks_3.update_raw(110.0, 115.0, 100.0, 114.0);
assert_eq!(
fuzzy_candlesticks_3.value.lower_wick_size,
CandleWickSize::Large
);
}
#[rstest]
fn test_upper_wick_size_large(mut fuzzy_candlesticks_3: FuzzyCandlesticks) {
fuzzy_candlesticks_3.update_raw(100.0, 100.0, 99.0, 100.0);
fuzzy_candlesticks_3.update_raw(101.0, 102.0, 100.0, 101.5);
fuzzy_candlesticks_3.update_raw(105.0, 115.0, 104.0, 106.0);
assert_eq!(
fuzzy_candlesticks_3.value.upper_wick_size,
CandleWickSize::Large
);
}
}