nautilus_indicators/momentum/
psl.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::fmt::{Debug, Display};
17
18use nautilus_model::data::Bar;
19
20use crate::{
21    average::{MovingAverageFactory, MovingAverageType},
22    indicator::{Indicator, MovingAverage},
23};
24
25#[repr(C)]
26#[derive(Debug)]
27#[cfg_attr(
28    feature = "python",
29    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.indicators", unsendable)
30)]
31pub struct PsychologicalLine {
32    pub period: usize,
33    pub ma_type: MovingAverageType,
34    pub value: f64,
35    pub initialized: bool,
36    ma: Box<dyn MovingAverage + Send + 'static>,
37    has_inputs: bool,
38    diff: f64,
39    previous_close: f64,
40}
41
42impl Display for PsychologicalLine {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        write!(f, "{}({},{})", self.name(), self.period, self.ma_type,)
45    }
46}
47
48impl Indicator for PsychologicalLine {
49    fn name(&self) -> String {
50        stringify!(PsychologicalLine).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.close).into());
63    }
64
65    fn reset(&mut self) {
66        self.ma.reset();
67        self.diff = 0.0;
68        self.previous_close = 0.0;
69        self.value = 0.0;
70        self.has_inputs = false;
71        self.initialized = false;
72    }
73}
74
75impl PsychologicalLine {
76    /// Creates a new [`PsychologicalLine`] instance.
77    #[must_use]
78    pub fn new(period: usize, ma_type: Option<MovingAverageType>) -> Self {
79        Self {
80            period,
81            ma_type: ma_type.unwrap_or(MovingAverageType::Simple),
82            value: 0.0,
83            previous_close: 0.0,
84            ma: MovingAverageFactory::create(ma_type.unwrap_or(MovingAverageType::Simple), period),
85            has_inputs: false,
86            initialized: false,
87            diff: 0.0,
88        }
89    }
90
91    pub fn update_raw(&mut self, close: f64) {
92        if !self.has_inputs {
93            self.previous_close = close;
94        }
95
96        self.diff = close - self.previous_close;
97        if self.diff <= 0.0 {
98            self.ma.update_raw(0.0);
99        } else {
100            self.ma.update_raw(1.0);
101        }
102        self.value = 100.0 * self.ma.value();
103
104        if !self.initialized {
105            self.has_inputs = true;
106            if self.ma.initialized() {
107                self.initialized = true;
108            }
109        }
110
111        self.previous_close = close;
112    }
113}
114
115////////////////////////////////////////////////////////////////////////////////
116// Tests
117////////////////////////////////////////////////////////////////////////////////
118#[cfg(test)]
119mod tests {
120    use nautilus_model::data::Bar;
121    use rstest::rstest;
122
123    use crate::{
124        indicator::Indicator,
125        momentum::psl::PsychologicalLine,
126        stubs::{bar_ethusdt_binance_minute_bid, psl_10},
127    };
128
129    #[rstest]
130    fn test_psl_initialized(psl_10: PsychologicalLine) {
131        let display_str = format!("{psl_10}");
132        assert_eq!(display_str, "PsychologicalLine(10,SIMPLE)");
133        assert_eq!(psl_10.period, 10);
134        assert!(!psl_10.initialized);
135        assert!(!psl_10.has_inputs);
136    }
137
138    #[rstest]
139    fn test_value_with_one_input(mut psl_10: PsychologicalLine) {
140        psl_10.update_raw(1.0);
141        assert_eq!(psl_10.value, 0.0);
142    }
143
144    #[rstest]
145    fn test_value_with_three_inputs(mut psl_10: PsychologicalLine) {
146        psl_10.update_raw(1.0);
147        psl_10.update_raw(2.0);
148        psl_10.update_raw(3.0);
149        assert_eq!(psl_10.value, 66.666_666_666_666_66);
150    }
151
152    #[rstest]
153    fn test_value_with_ten_inputs(mut psl_10: PsychologicalLine) {
154        psl_10.update_raw(1.00000);
155        psl_10.update_raw(1.00010);
156        psl_10.update_raw(1.00020);
157        psl_10.update_raw(1.00030);
158        psl_10.update_raw(1.00040);
159        psl_10.update_raw(1.00050);
160        psl_10.update_raw(1.00040);
161        psl_10.update_raw(1.00030);
162        psl_10.update_raw(1.00020);
163        psl_10.update_raw(1.00010);
164        psl_10.update_raw(1.00000);
165        assert_eq!(psl_10.value, 50.0);
166    }
167
168    #[rstest]
169    fn test_initialized_with_required_input(mut psl_10: PsychologicalLine) {
170        for i in 1..10 {
171            psl_10.update_raw(f64::from(i));
172        }
173        assert!(!psl_10.initialized);
174        psl_10.update_raw(10.0);
175        assert!(psl_10.initialized);
176    }
177
178    #[rstest]
179    fn test_handle_bar(mut psl_10: PsychologicalLine, bar_ethusdt_binance_minute_bid: Bar) {
180        psl_10.handle_bar(&bar_ethusdt_binance_minute_bid);
181        assert_eq!(psl_10.value, 0.0);
182        assert!(psl_10.has_inputs);
183        assert!(!psl_10.initialized);
184    }
185
186    #[rstest]
187    fn test_reset(mut psl_10: PsychologicalLine) {
188        psl_10.update_raw(1.0);
189        psl_10.reset();
190        assert_eq!(psl_10.value, 0.0);
191        assert_eq!(psl_10.previous_close, 0.0);
192        assert_eq!(psl_10.diff, 0.0);
193        assert!(!psl_10.has_inputs);
194        assert!(!psl_10.initialized);
195    }
196}