rusty_strategy/
signals.rs

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/// Represents a trading signal generated by a strategy.
10#[derive(Debug, Clone)]
11pub struct Signal {
12    /// Unique signal identifier
13    pub id: u64,
14
15    /// Strategy that generated this signal
16    pub strategy_id: String,
17
18    /// Timestamp when the signal was generated (nanosecond precision)
19    pub timestamp: u64,
20
21    /// Instrument this signal applies to
22    pub instrument_id: InstrumentId,
23
24    /// Type of the signal
25    pub signal_type: SignalType,
26
27    /// Confidence level (0.0 to 1.0)
28    pub confidence: f64,
29
30    /// Additional metadata about the signal
31    pub metadata: Option<String>,
32}
33
34impl Signal {
35    /// Creates a new signal with the current timestamp
36    #[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    /// Creates a new signal with a specified timestamp
59    #[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/// Types of trading signals that can be generated by strategies
82#[derive(Debug, Clone)]
83pub enum SignalType {
84    /// Signal to enter a new position
85    Enter {
86        /// Direction of the trade
87        side: OrderSide,
88
89        /// Order type (market, limit, etc.)
90        order_type: OrderType,
91
92        /// Price level for limit orders
93        price: Option<Decimal>,
94
95        /// Quantity to trade
96        quantity: Decimal,
97
98        /// Time in force for the order
99        time_in_force: TimeInForce,
100    },
101
102    /// Signal to exit an existing position
103    Exit {
104        /// Direction of the trade (opposite of the position direction)
105        side: OrderSide,
106
107        /// Order type (market, limit, etc.)
108        order_type: OrderType,
109
110        /// Price level for limit orders
111        price: Option<Decimal>,
112
113        /// Quantity to exit with
114        quantity: Decimal,
115
116        /// Time in force for the order
117        time_in_force: TimeInForce,
118    },
119
120    /// Signal to modify an existing position
121    Modify {
122        /// Direction of the trade
123        side: OrderSide,
124
125        /// Order type (market, limit, etc.)
126        order_type: OrderType,
127
128        /// Price level for limit orders
129        price: Option<Decimal>,
130
131        /// New quantity (absolute value, not delta)
132        quantity: Decimal,
133
134        /// Time in force for the order
135        time_in_force: TimeInForce,
136    },
137
138    /// Signal to cancel an existing order
139    Cancel {
140        /// ID of the order to cancel
141        order_id: String,
142    },
143
144    /// Informational signal (no action required)
145    Info {
146        /// Information message
147        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); // Should have a non-zero timestamp
181        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        // Check signal type
186        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)), // 50000
202            quantity: Decimal::new(1, 0),        // 1
203            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        // Check signal type
224        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        // Create first signal
250        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        // Wait a bit to ensure different timestamps
260        thread::sleep(Duration::from_millis(10));
261
262        // Create second signal
263        let signal2 = Signal::new(
264            2,
265            "test_strategy".into(),
266            instrument_id,
267            signal_type,
268            0.8,
269            None,
270        );
271
272        // Timestamps should be different and signal2's timestamp should be greater
273        assert!(signal1.timestamp < signal2.timestamp);
274    }
275
276    #[test]
277    fn test_signal_types() {
278        let instrument_id = create_test_instrument_id();
279
280        // Test Enter signal
281        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        // Test Exit signal
314        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        // Test Modify signal
347        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        // Test Cancel signal
380        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}