nautilus_indicators/volatility/
dc.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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::{
17    collections::VecDeque,
18    fmt::{Debug, Display},
19};
20
21use nautilus_model::data::Bar;
22
23use crate::indicator::Indicator;
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: VecDeque<f64>,
39    lower_prices: VecDeque<f64>,
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    /// Creates a new [`DonchianChannel`] instance.
78    #[must_use]
79    pub fn new(period: usize) -> Self {
80        Self {
81            period,
82            upper: 0.0,
83            middle: 0.0,
84            lower: 0.0,
85            upper_prices: VecDeque::with_capacity(period),
86            lower_prices: VecDeque::with_capacity(period),
87            has_inputs: false,
88            initialized: false,
89        }
90    }
91
92    pub fn update_raw(&mut self, high: f64, low: f64) {
93        self.upper_prices.push_back(high);
94        self.lower_prices.push_back(low);
95
96        // Initialization logic
97        if !self.initialized {
98            self.has_inputs = true;
99            if self.upper_prices.len() >= self.period && self.lower_prices.len() >= self.period {
100                self.initialized = true;
101            }
102        }
103
104        // Set values
105        self.upper = self
106            .upper_prices
107            .iter()
108            .copied()
109            .fold(f64::NEG_INFINITY, f64::max);
110        self.lower = self
111            .lower_prices
112            .iter()
113            .copied()
114            .fold(f64::INFINITY, f64::min);
115        self.middle = (self.upper + self.lower) / 2.0;
116    }
117}
118
119////////////////////////////////////////////////////////////////////////////////
120// Tests
121////////////////////////////////////////////////////////////////////////////////
122#[cfg(test)]
123mod tests {
124    use nautilus_model::data::Bar;
125    use rstest::rstest;
126
127    use crate::{
128        indicator::Indicator,
129        stubs::{bar_ethusdt_binance_minute_bid, dc_10},
130        volatility::dc::DonchianChannel,
131    };
132
133    #[rstest]
134    fn test_psl_initialized(dc_10: DonchianChannel) {
135        let display_str = format!("{dc_10}");
136        assert_eq!(display_str, "DonchianChannel(10)");
137        assert_eq!(dc_10.period, 10);
138        assert!(!dc_10.initialized);
139        assert!(!dc_10.has_inputs);
140    }
141
142    #[rstest]
143    fn test_value_with_one_input(mut dc_10: DonchianChannel) {
144        dc_10.update_raw(1.0, 0.9);
145        assert_eq!(dc_10.upper, 1.0);
146        assert_eq!(dc_10.middle, 0.95);
147        assert_eq!(dc_10.lower, 0.9);
148    }
149
150    #[rstest]
151    fn test_value_with_three_inputs(mut dc_10: DonchianChannel) {
152        dc_10.update_raw(1.0, 0.9);
153        dc_10.update_raw(2.0, 1.8);
154        dc_10.update_raw(3.0, 2.7);
155        assert_eq!(dc_10.upper, 3.0);
156        assert_eq!(dc_10.middle, 1.95);
157        assert_eq!(dc_10.lower, 0.9);
158    }
159
160    #[rstest]
161    fn test_value_with_ten_inputs(mut dc_10: DonchianChannel) {
162        let high_values = [
163            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,
164        ];
165        let low_values = [
166            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,
167        ];
168
169        for i in 0..15 {
170            dc_10.update_raw(high_values[i], low_values[i]);
171        }
172
173        assert_eq!(dc_10.upper, 15.0);
174        assert_eq!(dc_10.middle, 7.95);
175        assert_eq!(dc_10.lower, 0.9);
176    }
177
178    #[rstest]
179    fn test_handle_bar(mut dc_10: DonchianChannel, bar_ethusdt_binance_minute_bid: Bar) {
180        dc_10.handle_bar(&bar_ethusdt_binance_minute_bid);
181        assert_eq!(dc_10.upper, 1550.0);
182        assert_eq!(dc_10.middle, 1522.5);
183        assert_eq!(dc_10.lower, 1495.0);
184        assert!(dc_10.has_inputs);
185        assert!(!dc_10.initialized);
186    }
187
188    #[rstest]
189    fn test_reset(mut dc_10: DonchianChannel) {
190        dc_10.update_raw(1.0, 0.9);
191        dc_10.reset();
192        assert_eq!(dc_10.upper_prices.len(), 0);
193        assert_eq!(dc_10.lower_prices.len(), 0);
194        assert_eq!(dc_10.upper, 0.0);
195        assert_eq!(dc_10.middle, 0.0);
196        assert_eq!(dc_10.lower, 0.0);
197        assert!(!dc_10.has_inputs);
198        assert!(!dc_10.initialized);
199    }
200}