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#[cfg(test)]
116mod tests {
117    use nautilus_model::data::Bar;
118    use rstest::rstest;
119
120    use crate::{
121        indicator::Indicator,
122        momentum::psl::PsychologicalLine,
123        stubs::{bar_ethusdt_binance_minute_bid, psl_10},
124    };
125
126    #[rstest]
127    fn test_psl_initialized(psl_10: PsychologicalLine) {
128        let display_str = format!("{psl_10}");
129        assert_eq!(display_str, "PsychologicalLine(10,SIMPLE)");
130        assert_eq!(psl_10.period, 10);
131        assert!(!psl_10.initialized);
132        assert!(!psl_10.has_inputs);
133    }
134
135    #[rstest]
136    fn test_value_with_one_input(mut psl_10: PsychologicalLine) {
137        psl_10.update_raw(1.0);
138        assert_eq!(psl_10.value, 0.0);
139    }
140
141    #[rstest]
142    fn test_value_with_three_inputs(mut psl_10: PsychologicalLine) {
143        psl_10.update_raw(1.0);
144        psl_10.update_raw(2.0);
145        psl_10.update_raw(3.0);
146        assert_eq!(psl_10.value, 66.666_666_666_666_66);
147    }
148
149    #[rstest]
150    fn test_value_with_ten_inputs(mut psl_10: PsychologicalLine) {
151        psl_10.update_raw(1.00000);
152        psl_10.update_raw(1.00010);
153        psl_10.update_raw(1.00020);
154        psl_10.update_raw(1.00030);
155        psl_10.update_raw(1.00040);
156        psl_10.update_raw(1.00050);
157        psl_10.update_raw(1.00040);
158        psl_10.update_raw(1.00030);
159        psl_10.update_raw(1.00020);
160        psl_10.update_raw(1.00010);
161        psl_10.update_raw(1.00000);
162        assert_eq!(psl_10.value, 50.0);
163    }
164
165    #[rstest]
166    fn test_initialized_with_required_input(mut psl_10: PsychologicalLine) {
167        for i in 1..10 {
168            psl_10.update_raw(f64::from(i));
169        }
170        assert!(!psl_10.initialized);
171        psl_10.update_raw(10.0);
172        assert!(psl_10.initialized);
173    }
174
175    #[rstest]
176    fn test_handle_bar(mut psl_10: PsychologicalLine, bar_ethusdt_binance_minute_bid: Bar) {
177        psl_10.handle_bar(&bar_ethusdt_binance_minute_bid);
178        assert_eq!(psl_10.value, 0.0);
179        assert!(psl_10.has_inputs);
180        assert!(!psl_10.initialized);
181    }
182
183    #[rstest]
184    fn test_reset(mut psl_10: PsychologicalLine) {
185        psl_10.update_raw(1.0);
186        psl_10.reset();
187        assert_eq!(psl_10.value, 0.0);
188        assert_eq!(psl_10.previous_close, 0.0);
189        assert_eq!(psl_10.diff, 0.0);
190        assert!(!psl_10.has_inputs);
191        assert!(!psl_10.initialized);
192    }
193}