Skip to main content

nautilus_common/
signal.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
16//! A user signal type.
17
18use std::{any::Any, sync::Arc};
19
20use nautilus_core::UnixNanos;
21use nautilus_model::data::{HasTsInit, custom::CustomDataTrait};
22use serde::{Deserialize, Serialize};
23use ustr::Ustr;
24
25/// Represents a generic signal.
26#[repr(C)]
27#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
28#[cfg_attr(
29    feature = "python",
30    pyo3::pyclass(module = "nautilus_trader.common", from_py_object)
31)]
32#[cfg_attr(
33    feature = "python",
34    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")
35)]
36pub struct Signal {
37    pub name: Ustr,
38    pub value: String,
39    pub ts_event: UnixNanos,
40    pub ts_init: UnixNanos,
41}
42
43impl Signal {
44    /// Creates a new [`Signal`] instance.
45    #[must_use]
46    pub const fn new(name: Ustr, value: String, ts_event: UnixNanos, ts_init: UnixNanos) -> Self {
47        Self {
48            name,
49            value,
50            ts_event,
51            ts_init,
52        }
53    }
54}
55
56impl HasTsInit for Signal {
57    fn ts_init(&self) -> UnixNanos {
58        self.ts_init
59    }
60}
61
62impl CustomDataTrait for Signal {
63    fn type_name(&self) -> &'static str {
64        "Signal"
65    }
66
67    fn as_any(&self) -> &dyn Any {
68        self
69    }
70
71    fn ts_event(&self) -> UnixNanos {
72        self.ts_event
73    }
74
75    fn to_json(&self) -> anyhow::Result<String> {
76        Ok(serde_json::to_string(self)?)
77    }
78
79    fn clone_arc(&self) -> Arc<dyn CustomDataTrait> {
80        Arc::new(self.clone())
81    }
82
83    fn eq_arc(&self, other: &dyn CustomDataTrait) -> bool {
84        other
85            .as_any()
86            .downcast_ref::<Self>()
87            .is_some_and(|o| self == o)
88    }
89
90    #[cfg(feature = "python")]
91    fn to_pyobject(&self, py: pyo3::Python<'_>) -> pyo3::PyResult<pyo3::Py<pyo3::PyAny>> {
92        use pyo3::IntoPyObjectExt;
93        self.clone().into_py_any(py)
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use nautilus_model::data::stubs::StubCustomData;
100    use rstest::rstest;
101
102    use super::*;
103
104    fn test_signal() -> Signal {
105        Signal::new(
106            Ustr::from("price_alert"),
107            "TRIGGERED".to_string(),
108            UnixNanos::from(3),
109            UnixNanos::from(5),
110        )
111    }
112
113    #[rstest]
114    fn test_new_assigns_every_field() {
115        let signal = test_signal();
116
117        assert_eq!(signal.name, Ustr::from("price_alert"));
118        assert_eq!(signal.value, "TRIGGERED");
119        assert_eq!(signal.ts_event, UnixNanos::from(3));
120        assert_eq!(signal.ts_init, UnixNanos::from(5));
121    }
122
123    #[rstest]
124    fn test_custom_data_trait_accessors() {
125        let signal = test_signal();
126
127        assert_eq!(signal.type_name(), "Signal");
128        assert_eq!(CustomDataTrait::ts_event(&signal), UnixNanos::from(3));
129        assert_eq!(HasTsInit::ts_init(&signal), UnixNanos::from(5));
130        assert_eq!(
131            signal.as_any().downcast_ref::<Signal>(),
132            Some(&test_signal())
133        );
134    }
135
136    #[rstest]
137    fn test_to_json_round_trips() {
138        let signal = test_signal();
139
140        let json = signal.to_json().unwrap();
141
142        assert_eq!(serde_json::from_str::<Signal>(&json).unwrap(), signal);
143    }
144
145    #[rstest]
146    fn test_clone_arc_preserves_value() {
147        let signal = test_signal();
148
149        let cloned = signal.clone_arc();
150
151        assert_eq!(cloned.type_name(), "Signal");
152        assert_eq!(cloned.as_any().downcast_ref::<Signal>(), Some(&signal));
153    }
154
155    #[rstest]
156    #[case::name(Signal { name: Ustr::from("other_name"), ..test_signal() })]
157    #[case::value(Signal { value: "CLEARED".to_string(), ..test_signal() })]
158    #[case::ts_event(Signal { ts_event: UnixNanos::from(4), ..test_signal() })]
159    #[case::ts_init(Signal { ts_init: UnixNanos::from(6), ..test_signal() })]
160    fn test_eq_arc_rejects_any_differing_field(#[case] other: Signal) {
161        let signal = test_signal();
162
163        assert!(signal.eq_arc(&test_signal()));
164        assert!(!signal.eq_arc(&other));
165    }
166
167    #[rstest]
168    fn test_eq_arc_rejects_a_different_custom_data_type() {
169        let signal = test_signal();
170
171        let other = StubCustomData {
172            ts_init: UnixNanos::from(5),
173            value: 1,
174        };
175
176        assert!(!signal.eq_arc(&other));
177    }
178}