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