nautilus_indicators/volatility/
dc.rs1use std::fmt::Display;
17
18use arraydeque::{ArrayDeque, Wrapping};
19use nautilus_model::data::Bar;
20
21use crate::indicator::Indicator;
22
23const MAX_PERIOD: usize = 1_024;
24
25#[repr(C)]
26#[derive(Debug)]
27#[cfg_attr(
28 feature = "python",
29 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators")
30)]
31#[cfg_attr(
32 feature = "python",
33 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
34)]
35pub struct DonchianChannel {
36 pub period: usize,
37 pub upper: f64,
38 pub middle: f64,
39 pub lower: f64,
40 pub initialized: bool,
41 has_inputs: bool,
42 upper_prices: ArrayDeque<f64, MAX_PERIOD, Wrapping>,
43 lower_prices: ArrayDeque<f64, MAX_PERIOD, Wrapping>,
44}
45
46impl Display for DonchianChannel {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 write!(f, "{}({})", self.name(), self.period)
49 }
50}
51
52impl Indicator for DonchianChannel {
53 fn name(&self) -> String {
54 stringify!(DonchianChannel).to_string()
55 }
56
57 fn has_inputs(&self) -> bool {
58 self.has_inputs
59 }
60
61 fn initialized(&self) -> bool {
62 self.initialized
63 }
64
65 fn handle_bar(&mut self, bar: &Bar) {
66 self.update_raw((&bar.high).into(), (&bar.low).into());
67 }
68
69 fn reset(&mut self) {
70 self.upper_prices.clear();
71 self.lower_prices.clear();
72 self.upper = 0.0;
73 self.middle = 0.0;
74 self.lower = 0.0;
75 self.has_inputs = false;
76 self.initialized = false;
77 }
78}
79
80impl DonchianChannel {
81 #[must_use]
88 pub fn new(period: usize) -> Self {
89 assert!(
90 period > 0 && period <= MAX_PERIOD,
91 "DonchianChannel: period {period} exceeds MAX_PERIOD ({MAX_PERIOD})"
92 );
93
94 Self {
95 period,
96 upper: 0.0,
97 middle: 0.0,
98 lower: 0.0,
99 upper_prices: ArrayDeque::new(),
100 lower_prices: ArrayDeque::new(),
101 has_inputs: false,
102 initialized: false,
103 }
104 }
105
106 pub fn update_raw(&mut self, high: f64, low: f64) {
107 if self.upper_prices.len() == self.period {
108 let _ = self.upper_prices.pop_front();
109 }
110
111 if self.lower_prices.len() == self.period {
112 let _ = self.lower_prices.pop_front();
113 }
114
115 let _ = self.upper_prices.push_back(high);
116 let _ = self.lower_prices.push_back(low);
117
118 if !self.initialized {
119 self.has_inputs = true;
120
121 if self.upper_prices.len() >= self.period && self.lower_prices.len() >= self.period {
122 self.initialized = true;
123 }
124 }
125
126 self.upper = self
127 .upper_prices
128 .iter()
129 .copied()
130 .fold(f64::NEG_INFINITY, f64::max);
131 self.lower = self
132 .lower_prices
133 .iter()
134 .copied()
135 .fold(f64::INFINITY, f64::min);
136 self.middle = f64::midpoint(self.upper, self.lower);
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use nautilus_model::data::Bar;
143 use rstest::rstest;
144
145 use crate::{
146 indicator::Indicator,
147 stubs::{bar_ethusdt_binance_minute_bid, dc_10},
148 volatility::dc::DonchianChannel,
149 };
150
151 #[rstest]
152 fn test_psl_initialized(dc_10: DonchianChannel) {
153 let display_str = format!("{dc_10}");
154 assert_eq!(display_str, "DonchianChannel(10)");
155 assert_eq!(dc_10.period, 10);
156 assert!(!dc_10.initialized);
157 assert!(!dc_10.has_inputs);
158 }
159
160 #[rstest]
161 fn test_value_with_one_input(mut dc_10: DonchianChannel) {
162 dc_10.update_raw(1.0, 0.9);
163 assert_eq!(dc_10.upper, 1.0);
164 assert_eq!(dc_10.middle, 0.95);
165 assert_eq!(dc_10.lower, 0.9);
166 }
167
168 #[rstest]
169 fn test_value_with_three_inputs(mut dc_10: DonchianChannel) {
170 dc_10.update_raw(1.0, 0.9);
171 dc_10.update_raw(2.0, 1.8);
172 dc_10.update_raw(3.0, 2.7);
173 assert_eq!(dc_10.upper, 3.0);
174 assert_eq!(dc_10.middle, 1.95);
175 assert_eq!(dc_10.lower, 0.9);
176 }
177
178 #[rstest]
179 fn test_value_with_ten_inputs(mut dc_10: DonchianChannel) {
180 let high_values = [
181 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0,
182 ];
183 let low_values = [
184 0.9, 1.9, 2.9, 3.9, 4.9, 5.9, 6.9, 7.9, 8.9, 9.9, 10.1, 10.2, 10.3, 11.1, 11.4,
185 ];
186
187 for i in 0..15 {
188 dc_10.update_raw(high_values[i], low_values[i]);
189 }
190
191 assert_eq!(dc_10.upper, 15.0);
192 assert_eq!(dc_10.middle, 10.45);
193 assert_eq!(dc_10.lower, 5.9);
194 }
195
196 #[rstest]
197 fn test_value_respects_period_window() {
198 let mut dc = DonchianChannel::new(3);
199
200 dc.update_raw(1.0, 0.0);
201 dc.update_raw(100.0, 0.0);
202 dc.update_raw(2.0, 0.0);
203 dc.update_raw(3.0, 0.0);
204 dc.update_raw(4.0, 0.0);
205
206 assert_eq!(dc.upper, 4.0);
207 assert_eq!(dc.middle, 2.0);
208 assert_eq!(dc.lower, 0.0);
209 }
210
211 #[rstest]
212 fn test_handle_bar(mut dc_10: DonchianChannel, bar_ethusdt_binance_minute_bid: Bar) {
213 dc_10.handle_bar(&bar_ethusdt_binance_minute_bid);
214 assert_eq!(dc_10.upper, 1550.0);
215 assert_eq!(dc_10.middle, 1522.5);
216 assert_eq!(dc_10.lower, 1495.0);
217 assert!(dc_10.has_inputs);
218 assert!(!dc_10.initialized);
219 }
220
221 #[rstest]
222 fn test_reset(mut dc_10: DonchianChannel) {
223 dc_10.update_raw(1.0, 0.9);
224 dc_10.reset();
225 assert_eq!(dc_10.upper_prices.len(), 0);
226 assert_eq!(dc_10.lower_prices.len(), 0);
227 assert_eq!(dc_10.upper, 0.0);
228 assert_eq!(dc_10.middle, 0.0);
229 assert_eq!(dc_10.lower, 0.0);
230 assert!(!dc_10.has_inputs);
231 assert!(!dc_10.initialized);
232 }
233}