nautilus_trading/algorithm/
core.rs1use std::{cell::RefCell, fmt::Debug, rc::Rc};
19
20use ahash::{AHashMap, AHashSet};
21use indexmap::IndexMap;
22use nautilus_common::{
23 actor::{DataActorConfig, DataActorCore, DataActorNative},
24 cache::Cache,
25 clock::Clock,
26 msgbus::TypedHandler,
27};
28use nautilus_core::Params;
29use nautilus_model::{
30 events::{OrderEventAny, PositionEvent},
31 identifiers::{ActorId, ClientOrderId, ExecAlgorithmId, StrategyId, TraderId},
32 orders::{OrderAny, OrderList},
33 types::Quantity,
34};
35use nautilus_portfolio::portfolio::Portfolio;
36
37use super::config::ExecutionAlgorithmConfig;
38
39#[derive(Clone, Debug)]
41pub struct StrategyEventHandlers {
42 pub order_topic: String,
44 pub order_handler: TypedHandler<OrderEventAny>,
46 pub position_topic: String,
48 pub position_handler: TypedHandler<PositionEvent>,
50}
51
52#[derive(Clone, Copy, Debug)]
53pub(crate) struct SpawnReduction {
54 pub primary_id: ClientOrderId,
56 pub deducted_qty: Quantity,
58 pub spawn_was_quote_quantity: bool,
60 pub restored_qty: Option<Quantity>,
63}
64
65pub struct ExecutionAlgorithmCore {
76 pub actor: DataActorCore,
78 pub config: ExecutionAlgorithmConfig,
80 pub exec_algorithm_id: ExecAlgorithmId,
82 exec_spawn_ids: AHashMap<ClientOrderId, u32>,
84 subscribed_strategies: AHashSet<StrategyId>,
86 spawn_reductions: AHashMap<ClientOrderId, SpawnReduction>,
88 spawn_fill_debts: AHashMap<ClientOrderId, Quantity>,
91 handed_off_primaries: AHashSet<ClientOrderId>,
93 submit_params: AHashMap<ClientOrderId, Params>,
95 portfolio: Option<Rc<RefCell<Portfolio>>>,
97 strategy_event_handlers: IndexMap<StrategyId, StrategyEventHandlers>,
99}
100
101pub trait ExecutionAlgorithmNative: DataActorNative {
112 fn exec_algorithm_core(&self) -> &ExecutionAlgorithmCore;
114
115 fn exec_algorithm_core_mut(&mut self) -> &mut ExecutionAlgorithmCore;
117
118 fn portfolio_rc(&self) -> Rc<RefCell<Portfolio>> {
124 self.exec_algorithm_core()
125 .portfolio
126 .as_ref()
127 .expect("ExecutionAlgorithm not registered: Portfolio not initialized")
128 .clone()
129 }
130}
131
132impl Debug for ExecutionAlgorithmCore {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 f.debug_struct(stringify!(ExecutionAlgorithmCore))
135 .field("actor", &self.actor)
136 .field("config", &self.config)
137 .field("exec_algorithm_id", &self.exec_algorithm_id)
138 .field("exec_spawn_ids", &self.exec_spawn_ids.len())
139 .field("subscribed_strategies", &self.subscribed_strategies.len())
140 .field("spawn_reductions", &self.spawn_reductions.len())
141 .field("spawn_fill_debts", &self.spawn_fill_debts.len())
142 .field("handed_off_primaries", &self.handed_off_primaries.len())
143 .field("submit_params", &self.submit_params.len())
144 .field(
145 "strategy_event_handlers",
146 &self.strategy_event_handlers.len(),
147 )
148 .finish()
149 }
150}
151
152impl ExecutionAlgorithmCore {
153 #[must_use]
159 pub fn new(config: ExecutionAlgorithmConfig) -> Self {
160 let exec_algorithm_id = config
161 .exec_algorithm_id
162 .expect("ExecutionAlgorithmConfig must have exec_algorithm_id set");
163
164 let actor_config = DataActorConfig {
165 actor_id: Some(ActorId::new(exec_algorithm_id.inner())),
166 log_events: config.log_events,
167 log_commands: config.log_commands,
168 };
169
170 Self {
171 actor: DataActorCore::new(actor_config),
172 config,
173 exec_algorithm_id,
174 exec_spawn_ids: AHashMap::new(),
175 subscribed_strategies: AHashSet::new(),
176 spawn_reductions: AHashMap::new(),
177 spawn_fill_debts: AHashMap::new(),
178 handed_off_primaries: AHashSet::new(),
179 submit_params: AHashMap::new(),
180 portfolio: None,
181 strategy_event_handlers: IndexMap::new(),
182 }
183 }
184
185 pub fn register(
191 &mut self,
192 trader_id: TraderId,
193 clock: Rc<RefCell<dyn Clock>>,
194 cache: Rc<RefCell<Cache>>,
195 ) -> anyhow::Result<()> {
196 self.actor.register(trader_id, clock, cache)
197 }
198
199 #[must_use]
201 pub fn id(&self) -> ExecAlgorithmId {
202 self.exec_algorithm_id
203 }
204
205 pub fn set_portfolio(&mut self, portfolio: Rc<RefCell<Portfolio>>) {
207 self.portfolio = Some(portfolio);
208 }
209
210 #[must_use]
214 pub fn spawn_client_order_id(&mut self, primary_id: &ClientOrderId) -> ClientOrderId {
215 let sequence = self
216 .exec_spawn_ids
217 .entry(*primary_id)
218 .and_modify(|s| *s += 1)
219 .or_insert(1);
220
221 ClientOrderId::new(format!("{primary_id}-E{sequence}"))
222 }
223
224 #[must_use]
226 pub fn spawn_sequence(&self, primary_id: &ClientOrderId) -> Option<u32> {
227 self.exec_spawn_ids.get(primary_id).copied()
228 }
229
230 #[must_use]
232 pub fn is_strategy_subscribed(&self, strategy_id: &StrategyId) -> bool {
233 self.subscribed_strategies.contains(strategy_id)
234 }
235
236 pub fn add_subscribed_strategy(&mut self, strategy_id: StrategyId) {
238 self.subscribed_strategies.insert(strategy_id);
239 }
240
241 pub fn store_strategy_event_handlers(
243 &mut self,
244 strategy_id: StrategyId,
245 handlers: StrategyEventHandlers,
246 ) {
247 self.strategy_event_handlers.insert(strategy_id, handlers);
248 }
249
250 pub fn take_strategy_event_handlers(&mut self) -> IndexMap<StrategyId, StrategyEventHandlers> {
252 std::mem::take(&mut self.strategy_event_handlers)
253 }
254
255 pub fn clear_spawn_ids(&mut self) {
257 self.exec_spawn_ids.clear();
258 }
259
260 pub fn clear_subscribed_strategies(&mut self) {
262 self.subscribed_strategies.clear();
263 }
264
265 pub fn track_pending_spawn_reduction(
269 &mut self,
270 spawn_id: ClientOrderId,
271 primary_id: ClientOrderId,
272 quantity: Quantity,
273 spawn_was_quote_quantity: bool,
274 ) {
275 self.spawn_reductions.insert(
276 spawn_id,
277 SpawnReduction {
278 primary_id,
279 deducted_qty: quantity,
280 spawn_was_quote_quantity,
281 restored_qty: None,
282 },
283 );
284 }
285
286 #[must_use]
288 pub(crate) fn spawn_reduction(&self, spawn_id: ClientOrderId) -> Option<SpawnReduction> {
289 self.spawn_reductions.get(&spawn_id).copied()
290 }
291
292 pub(crate) fn set_spawn_reduction(
294 &mut self,
295 spawn_id: ClientOrderId,
296 reduction: SpawnReduction,
297 ) {
298 self.spawn_reductions.insert(spawn_id, reduction);
299 }
300
301 pub(crate) fn take_pending_spawn_reduction(
303 &mut self,
304 spawn_id: ClientOrderId,
305 ) -> Option<SpawnReduction> {
306 self.spawn_reductions.remove(&spawn_id)
307 }
308
309 #[must_use]
311 pub(crate) fn spawn_fill_debt(&self, primary_id: ClientOrderId) -> Option<Quantity> {
312 self.spawn_fill_debts.get(&primary_id).copied()
313 }
314
315 pub(crate) fn add_spawn_fill_debt(&mut self, primary_id: ClientOrderId, quantity: Quantity) {
317 self.spawn_fill_debts
318 .entry(primary_id)
319 .and_modify(|debt| {
320 let precision = debt.precision;
321 *debt = *debt + quantity;
322 debt.precision = precision;
323 })
324 .or_insert(quantity);
325 }
326
327 pub(crate) fn set_spawn_fill_debt(&mut self, primary_id: ClientOrderId, quantity: Quantity) {
329 if quantity.is_zero() {
330 self.spawn_fill_debts.remove(&primary_id);
331 } else {
332 self.spawn_fill_debts.insert(primary_id, quantity);
333 }
334 }
335
336 pub(crate) fn mark_primary_handed_off(&mut self, primary_id: ClientOrderId) {
338 self.clear_primary_spawn_state(primary_id);
339 self.handed_off_primaries.insert(primary_id);
340 }
341
342 pub(crate) fn clear_primary_spawn_state(&mut self, primary_id: ClientOrderId) {
344 self.spawn_reductions
345 .retain(|_, reduction| reduction.primary_id != primary_id);
346 self.spawn_fill_debts.remove(&primary_id);
347 self.handed_off_primaries.remove(&primary_id);
348 }
349
350 #[must_use]
352 pub(crate) fn primary_was_handed_off(&self, primary_id: ClientOrderId) -> bool {
353 self.handed_off_primaries.contains(&primary_id)
354 }
355
356 pub(crate) fn discard_spawn_fill_debt(&mut self, primary_id: ClientOrderId) {
358 self.spawn_fill_debts.remove(&primary_id);
359 }
360
361 pub fn clear_pending_spawn_reductions(&mut self) {
363 self.spawn_reductions.clear();
364 self.spawn_fill_debts.clear();
365 }
366
367 pub fn remember_submit_params(&mut self, primary_id: ClientOrderId, params: Option<Params>) {
371 if let Some(params) = params
372 && !params.is_empty()
373 {
374 self.submit_params.insert(primary_id, params);
375 }
376 }
377
378 #[must_use]
380 pub fn submit_params(&self, primary_id: &ClientOrderId) -> Option<Params> {
381 self.submit_params.get(primary_id).cloned()
382 }
383
384 pub fn remove_submit_params(&mut self, primary_id: &ClientOrderId) {
386 self.submit_params.remove(primary_id);
387 }
388
389 pub fn clear_submit_params(&mut self) {
391 self.submit_params.clear();
392 }
393
394 pub fn reset(&mut self) {
399 self.exec_spawn_ids.clear();
400 self.subscribed_strategies.clear();
401 self.spawn_reductions.clear();
402 self.spawn_fill_debts.clear();
403 self.handed_off_primaries.clear();
404 self.submit_params.clear();
405 self.strategy_event_handlers.clear();
406 }
407
408 pub fn get_order(&self, client_order_id: &ClientOrderId) -> anyhow::Result<OrderAny> {
414 Ok(self.cache_ref().try_order_owned(client_order_id)?)
415 }
416
417 pub fn get_orders_for_list(&self, order_list: &OrderList) -> anyhow::Result<Vec<OrderAny>> {
423 order_list
424 .client_order_ids
425 .iter()
426 .map(|id| self.get_order(id))
427 .collect()
428 }
429}
430
431impl DataActorNative for ExecutionAlgorithmCore {
432 fn core(&self) -> &DataActorCore {
433 &self.actor
434 }
435
436 fn core_mut(&mut self) -> &mut DataActorCore {
437 &mut self.actor
438 }
439}
440
441impl ExecutionAlgorithmNative for ExecutionAlgorithmCore {
442 fn exec_algorithm_core(&self) -> &ExecutionAlgorithmCore {
443 self
444 }
445
446 fn exec_algorithm_core_mut(&mut self) -> &mut ExecutionAlgorithmCore {
447 self
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use rstest::rstest;
454
455 use super::*;
456
457 fn create_test_config() -> ExecutionAlgorithmConfig {
458 ExecutionAlgorithmConfig {
459 exec_algorithm_id: Some(ExecAlgorithmId::new("TWAP")),
460 ..Default::default()
461 }
462 }
463
464 #[rstest]
465 fn test_core_new() {
466 let config = create_test_config();
467 let core = ExecutionAlgorithmCore::new(config.clone());
468
469 assert_eq!(core.exec_algorithm_id, ExecAlgorithmId::new("TWAP"));
470 assert_eq!(core.config.log_events, config.log_events);
471 assert!(core.exec_spawn_ids.is_empty());
472 assert!(core.subscribed_strategies.is_empty());
473 }
474
475 #[rstest]
476 fn test_spawn_client_order_id_sequence() {
477 let config = create_test_config();
478 let mut core = ExecutionAlgorithmCore::new(config);
479
480 let primary_id = ClientOrderId::new("O-001");
481
482 let spawn1 = core.spawn_client_order_id(&primary_id);
483 assert_eq!(spawn1.as_str(), "O-001-E1");
484
485 let spawn2 = core.spawn_client_order_id(&primary_id);
486 assert_eq!(spawn2.as_str(), "O-001-E2");
487
488 let spawn3 = core.spawn_client_order_id(&primary_id);
489 assert_eq!(spawn3.as_str(), "O-001-E3");
490 }
491
492 #[rstest]
493 fn test_spawn_client_order_id_different_primaries() {
494 let config = create_test_config();
495 let mut core = ExecutionAlgorithmCore::new(config);
496
497 let primary1 = ClientOrderId::new("O-001");
498 let primary2 = ClientOrderId::new("O-002");
499
500 let spawn1_1 = core.spawn_client_order_id(&primary1);
501 let spawn2_1 = core.spawn_client_order_id(&primary2);
502 let spawn1_2 = core.spawn_client_order_id(&primary1);
503
504 assert_eq!(spawn1_1.as_str(), "O-001-E1");
505 assert_eq!(spawn2_1.as_str(), "O-002-E1");
506 assert_eq!(spawn1_2.as_str(), "O-001-E2");
507 }
508
509 #[rstest]
510 fn test_spawn_sequence() {
511 let config = create_test_config();
512 let mut core = ExecutionAlgorithmCore::new(config);
513
514 let primary_id = ClientOrderId::new("O-001");
515
516 assert_eq!(core.spawn_sequence(&primary_id), None);
517
518 let _ = core.spawn_client_order_id(&primary_id);
519 assert_eq!(core.spawn_sequence(&primary_id), Some(1));
520
521 let _ = core.spawn_client_order_id(&primary_id);
522 assert_eq!(core.spawn_sequence(&primary_id), Some(2));
523 }
524
525 #[rstest]
526 fn test_strategy_subscription_tracking() {
527 let config = create_test_config();
528 let mut core = ExecutionAlgorithmCore::new(config);
529
530 let strategy_id = StrategyId::new("TEST-001");
531
532 assert!(!core.is_strategy_subscribed(&strategy_id));
533
534 core.add_subscribed_strategy(strategy_id);
535 assert!(core.is_strategy_subscribed(&strategy_id));
536 }
537
538 #[rstest]
539 fn test_clear_spawn_ids() {
540 let config = create_test_config();
541 let mut core = ExecutionAlgorithmCore::new(config);
542
543 let primary_id = ClientOrderId::new("O-001");
544 let _ = core.spawn_client_order_id(&primary_id);
545
546 assert!(core.spawn_sequence(&primary_id).is_some());
547
548 core.clear_spawn_ids();
549 assert!(core.spawn_sequence(&primary_id).is_none());
550 }
551
552 #[rstest]
553 fn test_remove_submit_params_only_removes_requested_primary() {
554 let config = create_test_config();
555 let mut core = ExecutionAlgorithmCore::new(config);
556 let primary1 = ClientOrderId::new("O-001");
557 let primary2 = ClientOrderId::new("O-002");
558 let mut params1 = Params::new();
559 params1.insert(
560 "route".to_string(),
561 serde_json::Value::String("A".to_string()),
562 );
563 let mut params2 = Params::new();
564 params2.insert(
565 "route".to_string(),
566 serde_json::Value::String("B".to_string()),
567 );
568
569 core.remember_submit_params(primary1, Some(params1));
570 core.remember_submit_params(primary2, Some(params2.clone()));
571 core.remove_submit_params(&primary1);
572
573 assert_eq!(core.submit_params(&primary1), None);
574 assert_eq!(core.submit_params(&primary2), Some(params2));
575 }
576
577 #[rstest]
578 fn test_primary_handoff_clears_only_its_spawn_state() {
579 let mut core = ExecutionAlgorithmCore::new(create_test_config());
580 let primary_a = ClientOrderId::from("A");
581 let primary_b = ClientOrderId::from("B");
582 let primary_c = ClientOrderId::from("C");
583 let child_a = ClientOrderId::from("A-E1");
584 let child_b = ClientOrderId::from("B-E1");
585 core.track_pending_spawn_reduction(child_a, primary_a, Quantity::from("0.3"), false);
586 core.track_pending_spawn_reduction(child_b, primary_b, Quantity::from("0.7"), true);
587 core.add_spawn_fill_debt(primary_a, Quantity::from("0.1"));
588 core.add_spawn_fill_debt(primary_b, Quantity::from("0.2"));
589
590 core.mark_primary_handed_off(primary_c);
591 core.mark_primary_handed_off(primary_a);
592
593 assert!(core.spawn_reduction(child_a).is_none());
594 assert!(core.spawn_fill_debt(primary_a).is_none());
595 assert!(core.primary_was_handed_off(primary_a));
596 assert!(!core.primary_was_handed_off(primary_b));
597 assert!(core.primary_was_handed_off(primary_c));
598 let retained = core.spawn_reduction(child_b).unwrap();
599 assert_eq!(retained.primary_id, primary_b);
600 assert_eq!(retained.deducted_qty, Quantity::from("0.7"));
601 assert!(retained.spawn_was_quote_quantity);
602 assert_eq!(retained.restored_qty, None);
603 assert_eq!(core.spawn_fill_debt(primary_b), Some(Quantity::from("0.2")));
604 }
605
606 #[rstest]
607 fn test_reset() {
608 let config = create_test_config();
609 let mut core = ExecutionAlgorithmCore::new(config);
610
611 let primary_id = ClientOrderId::new("O-001");
612 let strategy_id = StrategyId::new("TEST-001");
613
614 let _ = core.spawn_client_order_id(&primary_id);
615 core.add_subscribed_strategy(strategy_id);
616 core.mark_primary_handed_off(primary_id);
617
618 core.reset();
619
620 assert!(core.spawn_sequence(&primary_id).is_none());
621 assert!(!core.is_strategy_subscribed(&strategy_id));
622 assert!(!core.primary_was_handed_off(primary_id));
623 }
624
625 #[rstest]
626 fn test_data_actor_core_available_through_native_trait() {
627 let config = create_test_config();
628 let core = ExecutionAlgorithmCore::new(config);
629
630 assert!(DataActorNative::core(&core).trader_id().is_none());
631 }
632}