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::{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/// Donchian channel over rolling high and low prices.
24#[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    /// Creates a new [`DonchianChannel`] instance.
81    ///
82    /// # Panics
83    ///
84    /// Panics if:
85    /// - `period` is not in the range of 1 to `MAX_PERIOD` (inclusive).
86    #[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}