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