nautilus_indicators/momentum/
cci.rs1use std::{
17 collections::VecDeque,
18 fmt::{Debug, Display},
19};
20
21use nautilus_core::correctness::FAILED;
22use nautilus_model::data::Bar;
23
24use crate::{
25 average::{MovingAverageFactory, MovingAverageType},
26 indicator::{Indicator, MovingAverage},
27 support::{MAX_PERIOD, is_valid_hlc, typical_price},
28};
29
30#[repr(C)]
32#[derive(Debug)]
33#[cfg_attr(
34 feature = "python",
35 pyo3::pyclass(module = "nautilus_trader.indicators", unsendable)
36)]
37#[cfg_attr(
38 feature = "python",
39 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
40)]
41pub struct CommodityChannelIndex {
42 pub period: usize,
43 pub ma_type: MovingAverageType,
44 pub scalar: f64,
45 pub value: f64,
46 pub initialized: bool,
47 ma: Box<dyn MovingAverage + Send + 'static>,
48 has_inputs: bool,
49 mad: f64,
50 prices: VecDeque<f64>,
51}
52
53impl Display for CommodityChannelIndex {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 write!(f, "{}({},{})", self.name(), self.period, self.ma_type)
56 }
57}
58
59impl Indicator for CommodityChannelIndex {
60 fn name(&self) -> String {
61 stringify!(CommodityChannelIndex).to_string()
62 }
63
64 fn has_inputs(&self) -> bool {
65 self.has_inputs
66 }
67
68 fn initialized(&self) -> bool {
69 self.initialized
70 }
71
72 fn handle_bar(&mut self, bar: &Bar) {
73 self.update_raw((&bar.high).into(), (&bar.low).into(), (&bar.close).into());
74 }
75
76 fn reset(&mut self) {
77 self.ma.reset();
78 self.mad = 0.0;
79 self.prices.clear();
80 self.value = 0.0;
81 self.has_inputs = false;
82 self.initialized = false;
83 }
84}
85
86impl CommodityChannelIndex {
87 #[must_use]
95 pub fn new(period: usize, scalar: f64, ma_type: Option<MovingAverageType>) -> Self {
96 Self::new_checked(period, scalar, ma_type).expect(FAILED)
97 }
98
99 pub(crate) fn new_checked(
100 period: usize,
101 scalar: f64,
102 ma_type: Option<MovingAverageType>,
103 ) -> anyhow::Result<Self> {
104 anyhow::ensure!(
105 (1..=MAX_PERIOD).contains(&period),
106 "period must be in 1..={MAX_PERIOD}"
107 );
108 anyhow::ensure!(
109 scalar.is_finite() && scalar > 0.0,
110 "scalar must be finite and positive"
111 );
112 let ma_type = ma_type.unwrap_or(MovingAverageType::Simple);
113 Ok(Self {
114 period,
115 scalar,
116 ma_type,
117 value: 0.0,
118 prices: VecDeque::with_capacity(period),
119 ma: MovingAverageFactory::create(ma_type, period),
120 has_inputs: false,
121 initialized: false,
122 mad: 0.0,
123 })
124 }
125
126 pub fn update_raw(&mut self, high: f64, low: f64, close: f64) {
127 if !is_valid_hlc(high, low, close) {
128 return;
129 }
130 let typical_price = typical_price(high, low, close);
131
132 if self.prices.len() == self.period {
133 let _ = self.prices.pop_front();
134 }
135 self.prices.push_back(typical_price);
136
137 self.ma.update_raw(typical_price);
138
139 self.mad = fast_mad_with_mean(self.prices.iter().copied(), self.ma.value());
140
141 if self.ma.initialized() {
142 if self.mad == 0.0 {
145 self.value = 0.0;
146 } else {
147 self.value = (typical_price - self.ma.value()) / (self.scalar * self.mad);
148 }
149 }
150
151 if !self.initialized {
152 self.has_inputs = true;
153
154 if self.ma.initialized() {
155 self.initialized = true;
156 }
157 }
158 }
159}
160
161pub fn fast_mad_with_mean<I>(values: I, mean: f64) -> f64
162where
163 I: IntoIterator<Item = f64>,
164{
165 let mut acc = 0.0_f64;
166 let mut count = 0_usize;
167
168 for v in values {
169 acc += (v - mean).abs();
170 count += 1;
171 }
172
173 if count == 0 { 0.0 } else { acc / count as f64 }
174}
175
176#[cfg(test)]
177mod tests {
178 use nautilus_model::data::Bar;
179 use rstest::rstest;
180
181 use crate::{
182 indicator::Indicator,
183 momentum::cci::CommodityChannelIndex,
184 stubs::{bar_ethusdt_binance_minute_bid, cci_10},
185 testing::assert_approx_equal,
186 };
187
188 #[rstest]
189 fn test_psl_initialized(cci_10: CommodityChannelIndex) {
190 let display_str = format!("{cci_10}");
191 assert_eq!(display_str, "CommodityChannelIndex(10,SIMPLE)");
192 assert_eq!(cci_10.period, 10);
193 assert!(!cci_10.initialized);
194 assert!(!cci_10.has_inputs);
195 }
196
197 #[rstest]
198 fn test_value_with_one_input(mut cci_10: CommodityChannelIndex) {
199 cci_10.update_raw(1.0, 0.9, 0.95);
200 assert_eq!(cci_10.value, 0.0);
201 }
202
203 #[rstest]
204 fn test_value_with_three_inputs(mut cci_10: CommodityChannelIndex) {
205 cci_10.update_raw(1.0, 0.9, 0.95);
206 cci_10.update_raw(2.0, 1.9, 1.95);
207 cci_10.update_raw(3.0, 2.9, 2.95);
208 assert_eq!(cci_10.value, 0.0);
209 }
210
211 #[rstest]
212 fn test_value_with_ten_inputs(mut cci_10: CommodityChannelIndex) {
213 cci_10.update_raw(1.00000, 0.90000, 1.00000);
214 cci_10.update_raw(1.00010, 0.90010, 1.00010);
215 cci_10.update_raw(1.00030, 0.90020, 1.00020);
216 cci_10.update_raw(1.00040, 0.90030, 1.00030);
217 cci_10.update_raw(1.00050, 0.90040, 1.00040);
218 cci_10.update_raw(1.00060, 0.90050, 1.00050);
219 cci_10.update_raw(1.00050, 0.90040, 1.00040);
220 cci_10.update_raw(1.00040, 0.90030, 1.00030);
221 cci_10.update_raw(1.00030, 0.90020, 1.00020);
222 cci_10.update_raw(1.00010, 0.90010, 1.00010);
223 cci_10.update_raw(1.00000, 0.90000, 1.00000);
224 assert_approx_equal(cci_10.value, -0.97619047619);
225 }
226
227 #[rstest]
228 fn test_initialized_with_required_input(mut cci_10: CommodityChannelIndex) {
229 for i in 1..10 {
230 cci_10.update_raw(f64::from(i), f64::from(i), f64::from(i));
231 }
232 assert!(!cci_10.initialized);
233 cci_10.update_raw(10.0, 10.0, 10.0);
234 assert!(cci_10.initialized);
235 }
236
237 #[rstest]
238 fn test_handle_bar(mut cci_10: CommodityChannelIndex, bar_ethusdt_binance_minute_bid: Bar) {
239 cci_10.handle_bar(&bar_ethusdt_binance_minute_bid);
240 assert_eq!(cci_10.value, 0.0);
241 assert!(cci_10.has_inputs);
242 assert!(!cci_10.initialized);
243 }
244
245 #[rstest]
246 fn test_reset(mut cci_10: CommodityChannelIndex) {
247 cci_10.update_raw(1.0, 0.9, 0.95);
248 cci_10.reset();
249 assert_eq!(cci_10.value, 0.0);
250 assert_eq!(cci_10.prices.len(), 0);
251 assert_eq!(cci_10.mad, 0.0);
252 assert!(!cci_10.has_inputs);
253 assert!(!cci_10.initialized);
254 }
255}