Skip to main content

nautilus_system/messages/
controller.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
16use std::any::Any;
17
18use nautilus_common::actor::data_actor::ImportableActorConfig;
19use nautilus_core::{UUID4, UnixNanos};
20use nautilus_model::identifiers::{ActorId, StrategyId};
21use nautilus_trading::ImportableStrategyConfig;
22use serde::{Deserialize, Serialize};
23
24#[derive(Clone, Debug, Serialize, Deserialize)]
25#[serde(tag = "type")]
26pub struct CreateActor {
27    pub actor_config: ImportableActorConfig,
28    pub start: bool,
29    pub command_id: UUID4,
30    pub ts_init: UnixNanos,
31}
32
33impl CreateActor {
34    /// Creates a new [`CreateActor`] instance.
35    #[must_use]
36    pub const fn new(
37        actor_config: ImportableActorConfig,
38        start: bool,
39        command_id: UUID4,
40        ts_init: UnixNanos,
41    ) -> Self {
42        Self {
43            actor_config,
44            start,
45            command_id,
46            ts_init,
47        }
48    }
49
50    #[must_use]
51    pub fn as_any(&self) -> &dyn Any {
52        self
53    }
54}
55
56#[derive(Clone, Debug, Serialize, Deserialize)]
57#[serde(tag = "type")]
58pub struct CreateStrategy {
59    pub strategy_config: ImportableStrategyConfig,
60    pub start: bool,
61    pub command_id: UUID4,
62    pub ts_init: UnixNanos,
63}
64
65impl CreateStrategy {
66    /// Creates a new [`CreateStrategy`] instance.
67    #[must_use]
68    pub const fn new(
69        strategy_config: ImportableStrategyConfig,
70        start: bool,
71        command_id: UUID4,
72        ts_init: UnixNanos,
73    ) -> Self {
74        Self {
75            strategy_config,
76            start,
77            command_id,
78            ts_init,
79        }
80    }
81
82    #[must_use]
83    pub fn as_any(&self) -> &dyn Any {
84        self
85    }
86}
87
88#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
89#[serde(tag = "type")]
90pub struct StartActor {
91    pub actor_id: ActorId,
92    pub command_id: UUID4,
93    pub ts_init: UnixNanos,
94}
95
96impl StartActor {
97    /// Creates a new [`StartActor`] instance.
98    #[must_use]
99    pub const fn new(actor_id: ActorId, command_id: UUID4, ts_init: UnixNanos) -> Self {
100        Self {
101            actor_id,
102            command_id,
103            ts_init,
104        }
105    }
106
107    #[must_use]
108    pub fn as_any(&self) -> &dyn Any {
109        self
110    }
111}
112
113#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
114#[serde(tag = "type")]
115pub struct StartStrategy {
116    pub strategy_id: StrategyId,
117    pub command_id: UUID4,
118    pub ts_init: UnixNanos,
119}
120
121impl StartStrategy {
122    /// Creates a new [`StartStrategy`] instance.
123    #[must_use]
124    pub const fn new(strategy_id: StrategyId, command_id: UUID4, ts_init: UnixNanos) -> Self {
125        Self {
126            strategy_id,
127            command_id,
128            ts_init,
129        }
130    }
131
132    #[must_use]
133    pub fn as_any(&self) -> &dyn Any {
134        self
135    }
136}
137
138#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
139#[serde(tag = "type")]
140pub struct StopActor {
141    pub actor_id: ActorId,
142    pub command_id: UUID4,
143    pub ts_init: UnixNanos,
144}
145
146impl StopActor {
147    /// Creates a new [`StopActor`] instance.
148    #[must_use]
149    pub const fn new(actor_id: ActorId, command_id: UUID4, ts_init: UnixNanos) -> Self {
150        Self {
151            actor_id,
152            command_id,
153            ts_init,
154        }
155    }
156
157    #[must_use]
158    pub fn as_any(&self) -> &dyn Any {
159        self
160    }
161}
162
163#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
164#[serde(tag = "type")]
165pub struct StopStrategy {
166    pub strategy_id: StrategyId,
167    pub command_id: UUID4,
168    pub ts_init: UnixNanos,
169}
170
171impl StopStrategy {
172    /// Creates a new [`StopStrategy`] instance.
173    #[must_use]
174    pub const fn new(strategy_id: StrategyId, command_id: UUID4, ts_init: UnixNanos) -> Self {
175        Self {
176            strategy_id,
177            command_id,
178            ts_init,
179        }
180    }
181
182    #[must_use]
183    pub fn as_any(&self) -> &dyn Any {
184        self
185    }
186}
187
188#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
189#[serde(tag = "type")]
190pub struct RemoveActor {
191    pub actor_id: ActorId,
192    pub command_id: UUID4,
193    pub ts_init: UnixNanos,
194}
195
196impl RemoveActor {
197    /// Creates a new [`RemoveActor`] instance.
198    #[must_use]
199    pub const fn new(actor_id: ActorId, command_id: UUID4, ts_init: UnixNanos) -> Self {
200        Self {
201            actor_id,
202            command_id,
203            ts_init,
204        }
205    }
206
207    #[must_use]
208    pub fn as_any(&self) -> &dyn Any {
209        self
210    }
211}
212
213#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
214#[serde(tag = "type")]
215pub struct RemoveStrategy {
216    pub strategy_id: StrategyId,
217    pub command_id: UUID4,
218    pub ts_init: UnixNanos,
219}
220
221impl RemoveStrategy {
222    /// Creates a new [`RemoveStrategy`] instance.
223    #[must_use]
224    pub const fn new(strategy_id: StrategyId, command_id: UUID4, ts_init: UnixNanos) -> Self {
225        Self {
226            strategy_id,
227            command_id,
228            ts_init,
229        }
230    }
231
232    #[must_use]
233    pub fn as_any(&self) -> &dyn Any {
234        self
235    }
236}
237
238/// Commands handled by the [`Controller`](crate::controller::Controller).
239#[derive(Debug, Clone)]
240pub enum ControllerCommand {
241    CreateActor(CreateActor),
242    StartActor(StartActor),
243    StopActor(StopActor),
244    RemoveActor(RemoveActor),
245    CreateStrategy(CreateStrategy),
246    StartStrategy(StartStrategy),
247    StopStrategy(StopStrategy),
248    ExitMarket(StrategyId),
249    RemoveStrategy(RemoveStrategy),
250}
251
252impl From<CreateActor> for ControllerCommand {
253    fn from(command: CreateActor) -> Self {
254        Self::CreateActor(command)
255    }
256}
257
258impl From<CreateStrategy> for ControllerCommand {
259    fn from(command: CreateStrategy) -> Self {
260        Self::CreateStrategy(command)
261    }
262}
263
264impl From<StartActor> for ControllerCommand {
265    fn from(command: StartActor) -> Self {
266        Self::StartActor(command)
267    }
268}
269
270impl From<StartStrategy> for ControllerCommand {
271    fn from(command: StartStrategy) -> Self {
272        Self::StartStrategy(command)
273    }
274}
275
276impl From<StopActor> for ControllerCommand {
277    fn from(command: StopActor) -> Self {
278        Self::StopActor(command)
279    }
280}
281
282impl From<StopStrategy> for ControllerCommand {
283    fn from(command: StopStrategy) -> Self {
284        Self::StopStrategy(command)
285    }
286}
287
288impl From<RemoveActor> for ControllerCommand {
289    fn from(command: RemoveActor) -> Self {
290        Self::RemoveActor(command)
291    }
292}
293
294impl From<RemoveStrategy> for ControllerCommand {
295    fn from(command: RemoveStrategy) -> Self {
296        Self::RemoveStrategy(command)
297    }
298}
299
300#[cfg(test)]
301mod tests {
302    use std::collections::HashMap;
303
304    use rstest::rstest;
305
306    use super::*;
307
308    #[rstest]
309    fn test_create_actor_command_fields() {
310        let actor_config = ImportableActorConfig {
311            actor_path: "tests.actors:Actor".to_string(),
312            config_path: "tests.actors:ActorConfig".to_string(),
313            config: HashMap::new(),
314        };
315        let command_id = UUID4::new();
316        let command = CreateActor::new(actor_config.clone(), false, command_id, UnixNanos::new(1));
317
318        assert_eq!(command.actor_config.actor_path, actor_config.actor_path);
319        assert_eq!(command.actor_config.config_path, actor_config.config_path);
320        assert!(!command.start);
321        assert_eq!(command.command_id, command_id);
322        assert_eq!(command.ts_init, UnixNanos::new(1));
323        assert!(matches!(
324            ControllerCommand::from(command),
325            ControllerCommand::CreateActor(_)
326        ));
327    }
328
329    #[rstest]
330    fn test_create_actor_command_serde_round_trip() {
331        let actor_config = ImportableActorConfig {
332            actor_path: "tests.actors:Actor".to_string(),
333            config_path: "tests.actors:ActorConfig".to_string(),
334            config: HashMap::from([(
335                "threshold".to_string(),
336                serde_json::Value::String("10".to_string()),
337            )]),
338        };
339        let command_id = UUID4::new();
340        let command = CreateActor::new(actor_config, true, command_id, UnixNanos::new(9));
341
342        let value = serde_json::to_value(&command).unwrap();
343        assert_eq!(value["type"], "CreateActor");
344        let round_trip: CreateActor = serde_json::from_value(value).unwrap();
345
346        assert_eq!(round_trip.actor_config.actor_path, "tests.actors:Actor");
347        assert_eq!(
348            round_trip.actor_config.config_path,
349            "tests.actors:ActorConfig"
350        );
351        assert_eq!(
352            round_trip.actor_config.config["threshold"],
353            serde_json::Value::String("10".to_string())
354        );
355        assert!(round_trip.start);
356        assert_eq!(round_trip.command_id, command_id);
357        assert_eq!(round_trip.ts_init, UnixNanos::new(9));
358    }
359
360    #[rstest]
361    fn test_create_strategy_command_fields() {
362        let strategy_config = ImportableStrategyConfig {
363            strategy_path: "tests.strategies:Strategy".to_string(),
364            config_path: "tests.strategies:StrategyConfig".to_string(),
365            config: HashMap::new(),
366        };
367        let command_id = UUID4::new();
368        let command =
369            CreateStrategy::new(strategy_config.clone(), true, command_id, UnixNanos::new(2));
370
371        assert_eq!(
372            command.strategy_config.strategy_path,
373            strategy_config.strategy_path
374        );
375        assert_eq!(
376            command.strategy_config.config_path,
377            strategy_config.config_path
378        );
379        assert!(command.start);
380        assert_eq!(command.command_id, command_id);
381        assert_eq!(command.ts_init, UnixNanos::new(2));
382        assert!(matches!(
383            ControllerCommand::from(command),
384            ControllerCommand::CreateStrategy(_)
385        ));
386    }
387
388    #[rstest]
389    fn test_create_strategy_command_serde_round_trip() {
390        let strategy_config = ImportableStrategyConfig {
391            strategy_path: "tests.strategies:Strategy".to_string(),
392            config_path: "tests.strategies:StrategyConfig".to_string(),
393            config: HashMap::from([(
394                "threshold".to_string(),
395                serde_json::Value::String("20".to_string()),
396            )]),
397        };
398        let command_id = UUID4::new();
399        let command = CreateStrategy::new(strategy_config, false, command_id, UnixNanos::new(11));
400
401        let value = serde_json::to_value(&command).unwrap();
402        assert_eq!(value["type"], "CreateStrategy");
403        let round_trip: CreateStrategy = serde_json::from_value(value).unwrap();
404
405        assert_eq!(
406            round_trip.strategy_config.strategy_path,
407            "tests.strategies:Strategy"
408        );
409        assert_eq!(
410            round_trip.strategy_config.config_path,
411            "tests.strategies:StrategyConfig"
412        );
413        assert_eq!(
414            round_trip.strategy_config.config["threshold"],
415            serde_json::Value::String("20".to_string())
416        );
417        assert!(!round_trip.start);
418        assert_eq!(round_trip.command_id, command_id);
419        assert_eq!(round_trip.ts_init, UnixNanos::new(11));
420    }
421
422    #[rstest]
423    fn test_start_actor_command_fields() {
424        let command_id = UUID4::new();
425        let command = StartActor::new(ActorId::from("Actor-001"), command_id, UnixNanos::new(3));
426
427        assert_eq!(command.actor_id, ActorId::from("Actor-001"));
428        assert_eq!(command.command_id, command_id);
429        assert_eq!(command.ts_init, UnixNanos::new(3));
430        assert!(matches!(
431            ControllerCommand::from(command),
432            ControllerCommand::StartActor(_)
433        ));
434    }
435
436    #[rstest]
437    fn test_start_actor_command_serde_round_trip() {
438        let actor_id = ActorId::from("Actor-001");
439        let command_id = UUID4::new();
440        let command = StartActor::new(actor_id, command_id, UnixNanos::new(10));
441
442        let value = serde_json::to_value(command).unwrap();
443        assert_eq!(value["type"], "StartActor");
444        let round_trip: StartActor = serde_json::from_value(value).unwrap();
445
446        assert_eq!(round_trip.actor_id, actor_id);
447        assert_eq!(round_trip.command_id, command_id);
448        assert_eq!(round_trip.ts_init, UnixNanos::new(10));
449    }
450
451    #[rstest]
452    fn test_stop_actor_command_fields() {
453        let command_id = UUID4::new();
454        let command = StopActor::new(ActorId::from("Actor-001"), command_id, UnixNanos::new(4));
455
456        assert_eq!(command.actor_id, ActorId::from("Actor-001"));
457        assert_eq!(command.command_id, command_id);
458        assert_eq!(command.ts_init, UnixNanos::new(4));
459        assert!(matches!(
460            ControllerCommand::from(command),
461            ControllerCommand::StopActor(_)
462        ));
463    }
464
465    #[rstest]
466    fn test_remove_actor_command_fields() {
467        let command_id = UUID4::new();
468        let command = RemoveActor::new(ActorId::from("Actor-001"), command_id, UnixNanos::new(5));
469
470        assert_eq!(command.actor_id, ActorId::from("Actor-001"));
471        assert_eq!(command.command_id, command_id);
472        assert_eq!(command.ts_init, UnixNanos::new(5));
473        assert!(matches!(
474            ControllerCommand::from(command),
475            ControllerCommand::RemoveActor(_)
476        ));
477    }
478
479    #[rstest]
480    fn test_start_strategy_command_fields() {
481        let command_id = UUID4::new();
482        let command = StartStrategy::new(
483            StrategyId::from("Strategy-001"),
484            command_id,
485            UnixNanos::new(6),
486        );
487
488        assert_eq!(command.strategy_id, StrategyId::from("Strategy-001"));
489        assert_eq!(command.command_id, command_id);
490        assert_eq!(command.ts_init, UnixNanos::new(6));
491        assert!(matches!(
492            ControllerCommand::from(command),
493            ControllerCommand::StartStrategy(_)
494        ));
495    }
496
497    #[rstest]
498    fn test_start_strategy_command_serde_round_trip() {
499        let strategy_id = StrategyId::from("Strategy-001");
500        let command_id = UUID4::new();
501        let command = StartStrategy::new(strategy_id, command_id, UnixNanos::new(12));
502
503        let value = serde_json::to_value(command).unwrap();
504        assert_eq!(value["type"], "StartStrategy");
505        let round_trip: StartStrategy = serde_json::from_value(value).unwrap();
506
507        assert_eq!(round_trip.strategy_id, strategy_id);
508        assert_eq!(round_trip.command_id, command_id);
509        assert_eq!(round_trip.ts_init, UnixNanos::new(12));
510    }
511
512    #[rstest]
513    fn test_stop_strategy_command_fields() {
514        let command_id = UUID4::new();
515        let command = StopStrategy::new(
516            StrategyId::from("Strategy-001"),
517            command_id,
518            UnixNanos::new(7),
519        );
520
521        assert_eq!(command.strategy_id, StrategyId::from("Strategy-001"));
522        assert_eq!(command.command_id, command_id);
523        assert_eq!(command.ts_init, UnixNanos::new(7));
524        assert!(matches!(
525            ControllerCommand::from(command),
526            ControllerCommand::StopStrategy(_)
527        ));
528    }
529
530    #[rstest]
531    fn test_remove_strategy_command_fields() {
532        let command_id = UUID4::new();
533        let command = RemoveStrategy::new(
534            StrategyId::from("Strategy-001"),
535            command_id,
536            UnixNanos::new(8),
537        );
538
539        assert_eq!(command.strategy_id, StrategyId::from("Strategy-001"));
540        assert_eq!(command.command_id, command_id);
541        assert_eq!(command.ts_init, UnixNanos::new(8));
542        assert!(matches!(
543            ControllerCommand::from(command),
544            ControllerCommand::RemoveStrategy(_)
545        ));
546    }
547}