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)]
31pub struct DonchianChannel {
32 pub period: usize,
33 pub upper: f64,
34 pub middle: f64,
35 pub lower: f64,
36 pub initialized: bool,
37 has_inputs: bool,
38 upper_prices: ArrayDeque<f64, MAX_PERIOD, Wrapping>,
39 lower_prices: ArrayDeque<f64, MAX_PERIOD, Wrapping>,
40}
41
42impl Display for DonchianChannel {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 write!(f, "{}({})", self.name(), self.period)
45 }
46}
47
48impl Indicator for DonchianChannel {
49 fn name(&self) -> String {
50 stringify!(DonchianChannel).to_string()
51 }
52
53 fn has_inputs(&self) -> bool {
54 self.has_inputs
55 }
56
57 fn initialized(&self) -> bool {
58 self.initialized
59 }
60
61 fn handle_bar(&mut self, bar: &Bar) {
62 self.update_raw((&bar.high).into(), (&bar.low).into());
63 }
64
65 fn reset(&mut self) {
66 self.upper_prices.clear();
67 self.lower_prices.clear();
68 self.upper = 0.0;
69 self.middle = 0.0;
70 self.lower = 0.0;
71 self.has_inputs = false;
72 self.initialized = false;
73 }
74}
75
76impl DonchianChannel {
77 #[must_use]
84 pub fn new(period: usize) -> Self {
85 assert!(
86 period > 0 && period <= MAX_PERIOD,
87 "DonchianChannel: period {period} exceeds MAX_PERIOD ({MAX_PERIOD})"
88 );
89
90 Self {
91 period,
92 upper: 0.0,
93 middle: 0.0,
94 lower: 0.0,
95 upper_prices: ArrayDeque::new(),
96 lower_prices: ArrayDeque::new(),
97 has_inputs: false,
98 initialized: false,
99 }
100 }
101
102 pub fn update_raw(&mut self, high: f64, low: f64) {
103 let _ = self.upper_prices.push_back(high);
104 let _ = self.lower_prices.push_back(low);
105
106 if !self.initialized {
107 self.has_inputs = true;
108 if self.upper_prices.len() >= self.period && self.lower_prices.len() >= self.period {
109 self.initialized = true;
110 }
111 }
112
113 self.upper = self
114 .upper_prices
115 .iter()
116 .copied()
117 .fold(f64::NEG_INFINITY, f64::max);
118 self.lower = self
119 .lower_prices
120 .iter()
121 .copied()
122 .fold(f64::INFINITY, f64::min);
123 self.middle = 0.5 * (self.upper + self.lower);
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use nautilus_model::data::Bar;
130 use rstest::rstest;
131
132 use crate::{
133 indicator::Indicator,
134 stubs::{bar_ethusdt_binance_minute_bid, dc_10},
135 volatility::dc::DonchianChannel,
136 };
137
138 #[rstest]
139 fn test_psl_initialized(dc_10: DonchianChannel) {
140 let display_str = format!("{dc_10}");
141 assert_eq!(display_str, "DonchianChannel(10)");
142 assert_eq!(dc_10.period, 10);
143 assert!(!dc_10.initialized);
144 assert!(!dc_10.has_inputs);
145 }
146
147 #[rstest]
148 fn test_value_with_one_input(mut dc_10: DonchianChannel) {
149 dc_10.update_raw(1.0, 0.9);
150 assert_eq!(dc_10.upper, 1.0);
151 assert_eq!(dc_10.middle, 0.95);
152 assert_eq!(dc_10.lower, 0.9);
153 }
154
155 #[rstest]
156 fn test_value_with_three_inputs(mut dc_10: DonchianChannel) {
157 dc_10.update_raw(1.0, 0.9);
158 dc_10.update_raw(2.0, 1.8);
159 dc_10.update_raw(3.0, 2.7);
160 assert_eq!(dc_10.upper, 3.0);
161 assert_eq!(dc_10.middle, 1.95);
162 assert_eq!(dc_10.lower, 0.9);
163 }
164
165 #[rstest]
166 fn test_value_with_ten_inputs(mut dc_10: DonchianChannel) {
167 let high_values = [
168 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,
169 ];
170 let low_values = [
171 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,
172 ];
173
174 for i in 0..15 {
175 dc_10.update_raw(high_values[i], low_values[i]);
176 }
177
178 assert_eq!(dc_10.upper, 15.0);
179 assert_eq!(dc_10.middle, 7.95);
180 assert_eq!(dc_10.lower, 0.9);
181 }
182
183 #[rstest]
184 fn test_handle_bar(mut dc_10: DonchianChannel, bar_ethusdt_binance_minute_bid: Bar) {
185 dc_10.handle_bar(&bar_ethusdt_binance_minute_bid);
186 assert_eq!(dc_10.upper, 1550.0);
187 assert_eq!(dc_10.middle, 1522.5);
188 assert_eq!(dc_10.lower, 1495.0);
189 assert!(dc_10.has_inputs);
190 assert!(!dc_10.initialized);
191 }
192
193 #[rstest]
194 fn test_reset(mut dc_10: DonchianChannel) {
195 dc_10.update_raw(1.0, 0.9);
196 dc_10.reset();
197 assert_eq!(dc_10.upper_prices.len(), 0);
198 assert_eq!(dc_10.lower_prices.len(), 0);
199 assert_eq!(dc_10.upper, 0.0);
200 assert_eq!(dc_10.middle, 0.0);
201 assert_eq!(dc_10.lower, 0.0);
202 assert!(!dc_10.has_inputs);
203 assert!(!dc_10.initialized);
204 }
205}