Skip to main content

nautilus_indicators/momentum/
pressure.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::{Debug, Display};
17
18use nautilus_core::correctness::FAILED;
19use nautilus_model::data::Bar;
20
21use crate::{
22    average::{MovingAverageFactory, MovingAverageType},
23    indicator::{Indicator, MovingAverage},
24    support::is_valid_hlc,
25    volatility::atr::AverageTrueRange,
26};
27
28#[repr(C)]
29#[derive(Debug)]
30#[cfg_attr(
31    feature = "python",
32    pyo3::pyclass(module = "nautilus_trader.indicators", unsendable)
33)]
34#[cfg_attr(
35    feature = "python",
36    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.indicators")
37)]
38pub struct Pressure {
39    pub period: usize,
40    pub ma_type: MovingAverageType,
41    pub atr_floor: f64,
42    pub value: f64,
43    pub value_cumulative: f64,
44    pub initialized: bool,
45    atr: AverageTrueRange,
46    average_volume: Box<dyn MovingAverage + Send + 'static>,
47    has_inputs: bool,
48}
49
50impl Display for Pressure {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        write!(f, "{}({},{})", self.name(), self.period, self.ma_type)
53    }
54}
55
56impl Indicator for Pressure {
57    fn name(&self) -> String {
58        stringify!(Pressure).to_string()
59    }
60
61    fn has_inputs(&self) -> bool {
62        self.has_inputs
63    }
64
65    fn initialized(&self) -> bool {
66        self.initialized
67    }
68
69    fn handle_bar(&mut self, bar: &Bar) {
70        self.update_raw(
71            (&bar.high).into(),
72            (&bar.low).into(),
73            (&bar.close).into(),
74            (&bar.volume).into(),
75        );
76    }
77
78    fn reset(&mut self) {
79        self.atr.reset();
80        self.average_volume.reset();
81        self.value = 0.0;
82        self.value_cumulative = 0.0;
83        self.has_inputs = false;
84        self.initialized = false;
85    }
86}
87
88impl Pressure {
89    /// Creates a new [`Pressure`] instance.
90    ///
91    /// # Panics
92    ///
93    /// Panics if `period` is outside `1..=MAX_PERIOD` or the ATR floor is negative or non-finite.
94    #[must_use]
95    pub fn new(period: usize, ma_type: Option<MovingAverageType>, atr_floor: Option<f64>) -> Self {
96        Self::new_checked(period, ma_type, atr_floor).expect(FAILED)
97    }
98
99    pub(crate) fn new_checked(
100        period: usize,
101        ma_type: Option<MovingAverageType>,
102        atr_floor: Option<f64>,
103    ) -> anyhow::Result<Self> {
104        anyhow::ensure!(period > 0, "Pressure: period must be > 0");
105        let ma_type = ma_type.unwrap_or(MovingAverageType::Exponential);
106        let atr = AverageTrueRange::new_checked(period, Some(ma_type), Some(true), atr_floor)?;
107        Ok(Self {
108            period,
109            ma_type,
110            atr_floor: atr_floor.unwrap_or(0.0),
111            value: 0.0,
112            value_cumulative: 0.0,
113            atr,
114            average_volume: MovingAverageFactory::create(ma_type, period),
115            has_inputs: false,
116            initialized: false,
117        })
118    }
119
120    pub fn update_raw(&mut self, high: f64, low: f64, close: f64, volume: f64) {
121        if !is_valid_hlc(high, low, close) || !volume.is_finite() || volume < 0.0 {
122            return;
123        }
124
125        self.atr.update_raw(high, low, close);
126        self.average_volume.update_raw(volume);
127
128        self.has_inputs = true;
129
130        let avg_vol = self.average_volume.value();
131        if avg_vol == 0.0 {
132            self.value = 0.0;
133            return;
134        }
135
136        let atr_val = if self.atr.value > 0.0 {
137            self.atr.value
138        } else {
139            (high - low).abs().max(self.atr_floor)
140        };
141
142        if atr_val == 0.0 {
143            self.value = 0.0;
144            return;
145        }
146
147        let relative_volume = volume / avg_vol;
148        let buy_pressure = ((close - low) / atr_val) * relative_volume;
149        let sell_pressure = ((high - close) / atr_val) * relative_volume;
150
151        self.value = buy_pressure - sell_pressure;
152        self.value_cumulative += self.value;
153
154        if self.atr.initialized && self.average_volume.initialized() && !self.initialized {
155            self.initialized = true;
156        }
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use rstest::rstest;
163
164    use super::*;
165    use crate::{
166        stubs::{bar_ethusdt_binance_minute_bid, pressure_10},
167        testing::assert_approx_equal,
168    };
169
170    #[rstest]
171    fn test_name_returns_expected_string(pressure_10: Pressure) {
172        assert_eq!(pressure_10.name(), "Pressure");
173    }
174
175    #[rstest]
176    fn test_str_repr_returns_expected_string() {
177        let pressure = Pressure::new(10, Some(MovingAverageType::Exponential), None);
178        assert_eq!(format!("{pressure}"), "Pressure(10,EXPONENTIAL)");
179    }
180
181    #[rstest]
182    fn test_period_returns_expected_value(pressure_10: Pressure) {
183        assert_eq!(pressure_10.period, 10);
184    }
185
186    #[rstest]
187    fn test_initialized_without_inputs_returns_false(pressure_10: Pressure) {
188        assert!(!pressure_10.initialized());
189    }
190
191    #[rstest]
192    fn test_value_with_all_higher_inputs_returns_expected_value() {
193        let mut pressure = Pressure::new(10, Some(MovingAverageType::Exponential), None);
194
195        let high_values = [
196            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,
197        ];
198        let low_values = [
199            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,
200        ];
201        let close_values = [
202            0.95, 1.95, 2.95, 3.95, 4.95, 5.95, 6.95, 7.95, 8.95, 9.95, 10.95, 11.95, 12.95, 13.95,
203            14.95,
204        ];
205        let volume_values = [
206            100.0, 200.0, 300.0, 400.0, 500.0, 600.0, 700.0, 800.0, 900.0, 1000.0, 1100.0, 1200.0,
207            1300.0, 1400.0, 1500.0,
208        ];
209
210        let mut expected_cumulative = 0.0;
211        let mut expected_last = 0.0;
212
213        for i in 0..15 {
214            pressure.update_raw(
215                high_values[i],
216                low_values[i],
217                close_values[i],
218                volume_values[i],
219            );
220
221            let atr_val = if pressure.atr.value > 0.0 {
222                pressure.atr.value
223            } else {
224                (high_values[i] - low_values[i])
225                    .abs()
226                    .max(pressure.atr_floor)
227            };
228            let avg_vol = pressure.average_volume.value();
229            if avg_vol != 0.0 && atr_val != 0.0 {
230                let relative_volume = volume_values[i] / avg_vol;
231                let buy_pressure = ((close_values[i] - low_values[i]) / atr_val) * relative_volume;
232                let sell_pressure =
233                    ((high_values[i] - close_values[i]) / atr_val) * relative_volume;
234                let bar_value = buy_pressure - sell_pressure;
235                expected_cumulative += bar_value;
236                expected_last = bar_value;
237            }
238        }
239
240        assert!(pressure.initialized());
241        assert!((pressure.value - expected_last).abs() < 1e-6);
242        assert!((pressure.value_cumulative - expected_cumulative).abs() < 1e-6);
243    }
244
245    #[rstest]
246    fn test_handle_bar(mut pressure_10: Pressure, bar_ethusdt_binance_minute_bid: Bar) {
247        pressure_10.handle_bar(&bar_ethusdt_binance_minute_bid);
248        assert_eq!(pressure_10.value, 0.0);
249        assert_eq!(pressure_10.value_cumulative, 0.0);
250        assert!(pressure_10.has_inputs);
251        assert!(!pressure_10.initialized);
252    }
253
254    #[rstest]
255    fn test_reset_successfully_returns_indicator_to_fresh_state(mut pressure_10: Pressure) {
256        for _ in 0..9 {
257            pressure_10.update_raw(11.0, 9.0, 10.0, 100.0);
258        }
259        pressure_10.update_raw(11.0, 9.0, 10.5, 100.0);
260        // Mid-range closes balance; the last close sits 1.5 above the low and 0.5
261        // below the high on an ATR of 2
262        assert!(pressure_10.initialized());
263        assert_eq!(pressure_10.value, 0.5);
264        assert_eq!(pressure_10.value_cumulative, 0.5);
265
266        pressure_10.reset();
267
268        assert!(!pressure_10.initialized());
269        assert_eq!(pressure_10.value, 0.0);
270        assert_eq!(pressure_10.value_cumulative, 0.0);
271        assert!(!pressure_10.has_inputs);
272    }
273
274    #[rstest]
275    fn test_ma_type_default_and_override() {
276        let pressure_default = Pressure::new(10, None, None);
277        assert_eq!(pressure_default.ma_type, MovingAverageType::Exponential);
278
279        let pressure_simple = Pressure::new(10, Some(MovingAverageType::Simple), None);
280        assert_eq!(pressure_simple.ma_type, MovingAverageType::Simple);
281    }
282
283    #[rstest]
284    fn test_initialized_after_enough_inputs() {
285        let mut pressure = Pressure::new(3, Some(MovingAverageType::Exponential), None);
286        for _ in 0..3 {
287            pressure.update_raw(1.3, 1.0, 1.1, 100.0);
288        }
289        assert!(pressure.initialized());
290    }
291
292    #[rstest]
293    #[case(1.0, 1.5, 1.2, 100.0)]
294    #[case(1.5, 1.0, 1.6, 100.0)]
295    #[case(1.5, 1.0, f64::NAN, 100.0)]
296    #[case(1.5, 1.0, 1.2, -1.0)]
297    #[case(1.5, 1.0, 1.2, f64::INFINITY)]
298    fn test_rejected_input_leaves_state_unchanged(
299        #[case] high: f64,
300        #[case] low: f64,
301        #[case] close: f64,
302        #[case] volume: f64,
303    ) {
304        let mut pressure = Pressure::new(1, Some(MovingAverageType::Simple), Some(0.5));
305        pressure.update_raw(1.5, 1.0, 1.2, 100.0);
306        pressure.update_raw(high, low, close, volume);
307
308        assert_eq!(pressure.atr.count, 1);
309        assert_eq!(pressure.average_volume.count(), 1);
310        assert!((pressure.value + 0.2).abs() < 1e-6);
311        assert!((pressure.value_cumulative + 0.2).abs() < 1e-6);
312    }
313
314    #[rstest]
315    fn test_atr_floor_applied_to_zero_range() {
316        let mut pressure = Pressure::new(1, Some(MovingAverageType::Simple), Some(0.5));
317        pressure.update_raw(1.5, 1.0, 1.2, 100.0);
318        assert!((pressure.value + 0.2).abs() < 1e-6);
319        assert!(!pressure.value_cumulative.is_nan());
320    }
321
322    #[rstest]
323    fn test_new_defaults_use_seeded_averages() {
324        let mut pressure = Pressure::new(10, None, None);
325        let high_values = [
326            100.75, 102.5, 102.0, 103.0, 104.0, 102.25, 101.25, 103.0, 105.75, 104.5, 106.0, 105.5,
327            107.25, 106.5, 108.25, 107.0, 109.0, 108.75, 110.0, 109.5,
328        ];
329        let low_values = [
330            99.5, 100.75, 100.25, 101.5, 102.5, 100.25, 100.0, 101.25, 104.0, 103.0, 104.5, 103.5,
331            106.0, 104.75, 106.5, 105.5, 107.5, 106.75, 108.75, 107.75,
332        ];
333        let close_values = [
334            100.0, 101.5, 100.75, 102.25, 103.0, 101.0, 100.5, 102.0, 104.5, 103.75, 105.0, 104.25,
335            106.5, 105.5, 107.0, 106.25, 108.0, 107.5, 109.25, 108.5,
336        ];
337        let volume_values = [
338            1200.0, 1500.0, 900.0, 1800.0, 2100.0, 1300.0, 1100.0, 1600.0, 2500.0, 1700.0, 2000.0,
339            1400.0, 2300.0, 1500.0, 2200.0, 1600.0, 2400.0, 1800.0, 2600.0, 1900.0,
340        ];
341
342        for i in 0..20 {
343            pressure.update_raw(
344                high_values[i],
345                low_values[i],
346                close_values[i],
347                volume_values[i],
348            );
349        }
350
351        assert!(pressure.atr.use_previous);
352        assert!(pressure.initialized());
353        assert_approx_equal(pressure.value, -0.111000235499);
354        assert_approx_equal(pressure.value_cumulative, -1.90133577924);
355    }
356}