1use 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 #[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 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}