Skip to main content

nautilus_indicators/volatility/
dc.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use 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    /// Creates a new [`DonchianChannel`] instance.
82    ///
83    /// # Panics
84    ///
85    /// This function panics if:
86    /// - `period` is not in the range of 1 to `MAX_PERIOD` (inclusive).
87    #[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}