1use quanta::Clock;
2use rust_decimal::Decimal;
3use rusty_model::{
4 enums::{OrderSide, OrderType, TimeInForce},
5 instruments::InstrumentId,
6};
7use smartstring::alias::String;
8
9#[derive(Debug, Clone)]
11pub struct Signal {
12 pub id: u64,
14
15 pub strategy_id: String,
17
18 pub timestamp: u64,
20
21 pub instrument_id: InstrumentId,
23
24 pub signal_type: SignalType,
26
27 pub confidence: f64,
29
30 pub metadata: Option<String>,
32}
33
34impl Signal {
35 #[must_use]
37 pub fn new(
38 id: u64,
39 strategy_id: String,
40 instrument_id: InstrumentId,
41 signal_type: SignalType,
42 confidence: f64,
43 metadata: Option<String>,
44 ) -> Self {
45 let clock = Clock::new();
46
47 Self {
48 id,
49 strategy_id,
50 timestamp: clock.raw(),
51 instrument_id,
52 signal_type,
53 confidence,
54 metadata,
55 }
56 }
57
58 #[must_use]
60 pub const fn with_timestamp(
61 id: u64,
62 strategy_id: String,
63 timestamp: u64,
64 instrument_id: InstrumentId,
65 signal_type: SignalType,
66 confidence: f64,
67 metadata: Option<String>,
68 ) -> Self {
69 Self {
70 id,
71 strategy_id,
72 timestamp,
73 instrument_id,
74 signal_type,
75 confidence,
76 metadata,
77 }
78 }
79}
80
81#[derive(Debug, Clone)]
83pub enum SignalType {
84 Enter {
86 side: OrderSide,
88
89 order_type: OrderType,
91
92 price: Option<Decimal>,
94
95 quantity: Decimal,
97
98 time_in_force: TimeInForce,
100 },
101
102 Exit {
104 side: OrderSide,
106
107 order_type: OrderType,
109
110 price: Option<Decimal>,
112
113 quantity: Decimal,
115
116 time_in_force: TimeInForce,
118 },
119
120 Modify {
122 side: OrderSide,
124
125 order_type: OrderType,
127
128 price: Option<Decimal>,
130
131 quantity: Decimal,
133
134 time_in_force: TimeInForce,
136 },
137
138 Cancel {
140 order_id: String,
142 },
143
144 Info {
146 message: String,
148 },
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use rusty_model::venues::Venue;
155 use std::thread;
156 use std::time::Duration;
157
158 fn create_test_instrument_id() -> InstrumentId {
159 InstrumentId::new("BTCUSDT", Venue::Binance)
160 }
161
162 #[test]
163 fn test_signal_new() {
164 let instrument_id = create_test_instrument_id();
165 let signal_type = SignalType::Info {
166 message: "Test signal".into(),
167 };
168
169 let signal = Signal::new(
170 1,
171 "test_strategy".into(),
172 instrument_id.clone(),
173 signal_type,
174 0.8,
175 Some("Test metadata".into()),
176 );
177
178 assert_eq!(signal.id, 1);
179 assert_eq!(signal.strategy_id, "test_strategy");
180 assert!(signal.timestamp > 0); assert_eq!(signal.instrument_id, instrument_id);
182 assert_eq!(signal.confidence, 0.8);
183 assert_eq!(signal.metadata, Some("Test metadata".into()));
184
185 match signal.signal_type {
187 SignalType::Info { ref message } => {
188 assert_eq!(message, "Test signal");
189 }
190 _ => panic!("Unexpected signal type"),
191 }
192 }
193
194 #[test]
195 fn test_signal_with_timestamp() {
196 let instrument_id = create_test_instrument_id();
197 let timestamp = 1_234_567_890;
198 let signal_type = SignalType::Enter {
199 side: OrderSide::Buy,
200 order_type: OrderType::Limit,
201 price: Some(Decimal::new(50000, 0)), quantity: Decimal::new(1, 0), time_in_force: TimeInForce::GTC,
204 };
205
206 let signal = Signal::with_timestamp(
207 2,
208 "test_strategy".into(),
209 timestamp,
210 instrument_id.clone(),
211 signal_type,
212 0.9,
213 None,
214 );
215
216 assert_eq!(signal.id, 2);
217 assert_eq!(signal.strategy_id, "test_strategy");
218 assert_eq!(signal.timestamp, timestamp);
219 assert_eq!(signal.instrument_id, instrument_id);
220 assert_eq!(signal.confidence, 0.9);
221 assert_eq!(signal.metadata, None);
222
223 match signal.signal_type {
225 SignalType::Enter {
226 side,
227 order_type,
228 price,
229 quantity,
230 time_in_force,
231 } => {
232 assert_eq!(side, OrderSide::Buy);
233 assert_eq!(order_type, OrderType::Limit);
234 assert_eq!(price, Some(Decimal::new(50000, 0)));
235 assert_eq!(quantity, Decimal::new(1, 0));
236 assert_eq!(time_in_force, TimeInForce::GTC);
237 }
238 _ => panic!("Unexpected signal type"),
239 }
240 }
241
242 #[test]
243 fn test_signal_timestamp_generation() {
244 let instrument_id = create_test_instrument_id();
245 let signal_type = SignalType::Info {
246 message: "Test signal".into(),
247 };
248
249 let signal1 = Signal::new(
251 1,
252 "test_strategy".into(),
253 instrument_id.clone(),
254 signal_type.clone(),
255 0.8,
256 None,
257 );
258
259 thread::sleep(Duration::from_millis(10));
261
262 let signal2 = Signal::new(
264 2,
265 "test_strategy".into(),
266 instrument_id,
267 signal_type,
268 0.8,
269 None,
270 );
271
272 assert!(signal1.timestamp < signal2.timestamp);
274 }
275
276 #[test]
277 fn test_signal_types() {
278 let instrument_id = create_test_instrument_id();
279
280 let enter_signal = Signal::new(
282 1,
283 "test_strategy".into(),
284 instrument_id.clone(),
285 SignalType::Enter {
286 side: OrderSide::Buy,
287 order_type: OrderType::Market,
288 price: None,
289 quantity: Decimal::new(2, 0),
290 time_in_force: TimeInForce::IOC,
291 },
292 0.8,
293 None,
294 );
295
296 match enter_signal.signal_type {
297 SignalType::Enter {
298 side,
299 order_type,
300 price,
301 quantity,
302 time_in_force,
303 } => {
304 assert_eq!(side, OrderSide::Buy);
305 assert_eq!(order_type, OrderType::Market);
306 assert_eq!(price, None);
307 assert_eq!(quantity, Decimal::new(2, 0));
308 assert_eq!(time_in_force, TimeInForce::IOC);
309 }
310 _ => panic!("Unexpected signal type"),
311 }
312
313 let exit_signal = Signal::new(
315 2,
316 "test_strategy".into(),
317 instrument_id.clone(),
318 SignalType::Exit {
319 side: OrderSide::Sell,
320 order_type: OrderType::Limit,
321 price: Some(Decimal::new(49000, 0)),
322 quantity: Decimal::new(2, 0),
323 time_in_force: TimeInForce::GTC,
324 },
325 0.9,
326 None,
327 );
328
329 match exit_signal.signal_type {
330 SignalType::Exit {
331 side,
332 order_type,
333 price,
334 quantity,
335 time_in_force,
336 } => {
337 assert_eq!(side, OrderSide::Sell);
338 assert_eq!(order_type, OrderType::Limit);
339 assert_eq!(price, Some(Decimal::new(49000, 0)));
340 assert_eq!(quantity, Decimal::new(2, 0));
341 assert_eq!(time_in_force, TimeInForce::GTC);
342 }
343 _ => panic!("Unexpected signal type"),
344 }
345
346 let modify_signal = Signal::new(
348 3,
349 "test_strategy".into(),
350 instrument_id.clone(),
351 SignalType::Modify {
352 side: OrderSide::Buy,
353 order_type: OrderType::Limit,
354 price: Some(Decimal::new(51000, 0)),
355 quantity: Decimal::new(3, 0),
356 time_in_force: TimeInForce::GTC,
357 },
358 0.7,
359 None,
360 );
361
362 match modify_signal.signal_type {
363 SignalType::Modify {
364 side,
365 order_type,
366 price,
367 quantity,
368 time_in_force,
369 } => {
370 assert_eq!(side, OrderSide::Buy);
371 assert_eq!(order_type, OrderType::Limit);
372 assert_eq!(price, Some(Decimal::new(51000, 0)));
373 assert_eq!(quantity, Decimal::new(3, 0));
374 assert_eq!(time_in_force, TimeInForce::GTC);
375 }
376 _ => panic!("Unexpected signal type"),
377 }
378
379 let cancel_signal = Signal::new(
381 4,
382 "test_strategy".into(),
383 instrument_id,
384 SignalType::Cancel {
385 order_id: "order123".into(),
386 },
387 1.0,
388 None,
389 );
390
391 match cancel_signal.signal_type {
392 SignalType::Cancel { order_id } => {
393 assert_eq!(order_id, "order123");
394 }
395 _ => panic!("Unexpected signal type"),
396 }
397 }
398}