nautilus_indicators/momentum/
bias.rs1use std::fmt::{Debug, Display};
17
18use nautilus_core::correctness::FAILED;
19use nautilus_model::data::{Bar, QuoteTick, TradeTick};
20
21use crate::{
22 average::{MovingAverageFactory, MovingAverageType},
23 indicator::{Indicator, MovingAverage},
24 support::MAX_PERIOD,
25};
26
27#[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 Bias {
39 pub period: usize,
40 pub ma_type: MovingAverageType,
41 pub value: f64,
42 pub count: usize,
43 pub initialized: bool,
44 ma: Box<dyn MovingAverage + Send + 'static>,
45 has_inputs: bool,
46}
47
48impl Display for Bias {
49 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
50 write!(f, "{}({},{})", self.name(), self.period, self.ma_type)
51 }
52}
53
54impl Indicator for Bias {
55 fn name(&self) -> String {
56 stringify!(Bias).to_string()
57 }
58
59 fn has_inputs(&self) -> bool {
60 self.has_inputs
61 }
62
63 fn initialized(&self) -> bool {
64 self.initialized
65 }
66
67 fn handle_quote(&mut self, _quote: &QuoteTick) -> anyhow::Result<()> {
68 Ok(())
69 }
70
71 fn handle_trade(&mut self, _trade: &TradeTick) {}
72
73 fn handle_bar(&mut self, bar: &Bar) {
74 self.update_raw((&bar.close).into());
75 }
76
77 fn reset(&mut self) {
78 self.ma.reset();
79 self.value = 0.0;
80 self.count = 0;
81 self.has_inputs = false;
82 self.initialized = false;
83 }
84}
85
86impl Bias {
87 #[must_use]
95 pub fn new(period: usize, ma_type: Option<MovingAverageType>) -> Self {
96 Self::new_checked(period, ma_type).expect(FAILED)
97 }
98
99 pub(crate) fn new_checked(
100 period: usize,
101 ma_type: Option<MovingAverageType>,
102 ) -> anyhow::Result<Self> {
103 anyhow::ensure!(
104 (1..=MAX_PERIOD).contains(&period),
105 "period must be in 1..={MAX_PERIOD}"
106 );
107 let ma_type = ma_type.unwrap_or(MovingAverageType::Simple);
108 Ok(Self {
109 period,
110 ma_type,
111 value: 0.0,
112 count: 0,
113 ma: MovingAverageFactory::create(ma_type, period),
114 has_inputs: false,
115 initialized: false,
116 })
117 }
118
119 pub fn update_raw(&mut self, close: f64) {
120 if !close.is_finite() {
121 return;
122 }
123 self.count += 1;
124 self.ma.update_raw(close);
125 if self.ma.value() == 0.0 {
128 self.value = 0.0;
129 } else {
130 self.value = 100.0 * ((close / self.ma.value()) - 1.0);
131 }
132 self.check_initialized();
133 }
134
135 pub fn check_initialized(&mut self) {
136 if !self.initialized {
137 self.has_inputs = true;
138
139 if self.ma.initialized() {
140 self.initialized = true;
141 }
142 }
143 }
144}
145
146#[cfg(test)]
147mod tests {
148 use rstest::{fixture, rstest};
149
150 use super::*;
151
152 #[fixture]
153 fn bias() -> Bias {
154 Bias::new(10, None)
155 }
156
157 #[rstest]
158 fn test_name_returns_expected_string(bias: Bias) {
159 assert_eq!(bias.name(), "Bias");
160 }
161
162 #[rstest]
163 fn test_str_repr_returns_expected_string(bias: Bias) {
164 assert_eq!(format!("{bias}"), "Bias(10,SIMPLE)");
165 }
166
167 #[rstest]
168 fn test_period_returns_expected_value(bias: Bias) {
169 assert_eq!(bias.period, 10);
170 }
171
172 #[rstest]
173 fn test_initialized_without_inputs_returns_false(bias: Bias) {
174 assert!(!bias.initialized());
175 }
176
177 #[rstest]
178 fn test_initialized_with_required_inputs_returns_true(mut bias: Bias) {
179 for i in 1..=10 {
180 bias.update_raw(f64::from(i));
181 }
182 assert!(bias.initialized());
183 }
184
185 #[rstest]
186 fn test_value_with_one_input_returns_expected_value(mut bias: Bias) {
187 bias.update_raw(1.0);
188 assert_eq!(bias.value, 0.0);
189 }
190
191 #[rstest]
192 fn test_value_with_all_higher_inputs_returns_expected_value(mut bias: Bias) {
193 const EPS: f64 = 1e-12;
194 const EXPECTED: f64 = 0.065_473_592_317_766_28;
195
196 fn abs_diff_lt(lhs: f64, rhs: f64) -> bool {
197 (lhs - rhs).abs() < EPS
198 }
199
200 let inputs = [
201 109.93, 110.0, 109.77, 109.96, 110.29, 110.53, 110.27, 110.21, 110.06, 110.19, 109.83,
202 109.9, 110.0, 110.03, 110.13, 109.95, 109.75, 110.15, 109.9, 110.04,
203 ];
204
205 for &price in &inputs {
206 bias.update_raw(price);
207 }
208
209 assert!(
210 abs_diff_lt(bias.value, EXPECTED),
211 "bias.value = {:.16} did not match expected value",
212 bias.value
213 );
214 }
215
216 #[rstest]
217 fn test_reset_successfully_returns_indicator_to_fresh_state(mut bias: Bias) {
218 bias.update_raw(1.00020);
219 bias.update_raw(1.00030);
220 bias.update_raw(1.00050);
221
222 bias.reset();
223
224 assert!(!bias.initialized());
225 assert_eq!(bias.value, 0.0);
226 }
227
228 #[rstest]
229 fn test_reset_resets_moving_average_state() {
230 let mut bias = Bias::new(3, None);
231 bias.update_raw(1.0);
232 bias.update_raw(2.0);
233 bias.update_raw(3.0);
234 assert!(bias.ma.initialized());
235 bias.reset();
236 assert!(!bias.ma.initialized());
237 assert_eq!(bias.value, 0.0);
238 }
239
240 #[rstest]
241 fn test_count_increments_and_resets(mut bias: Bias) {
242 assert_eq!(bias.count, 0);
243 bias.update_raw(1.0);
244 assert_eq!(bias.count, 1);
245 bias.update_raw(1.1);
246 assert_eq!(bias.count, 2);
247 bias.reset();
248 assert_eq!(bias.count, 0);
249 }
250}