nautilus_common/
signal.rs1use 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#[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 #[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}