1use std::time::Duration;
34
35use ahash::AHashMap;
36use nautilus_common::{
37 actor::{DataActor, DataActorNative},
38 timer::TimeEvent,
39};
40use nautilus_model::{
41 enums::OrderType,
42 identifiers::ClientOrderId,
43 instruments::Instrument,
44 orders::{Order, OrderAny},
45 types::{Quantity, quantity::QuantityRaw},
46};
47use ustr::Ustr;
48
49use super::{
50 ExecutionAlgorithm, ExecutionAlgorithmConfig, ExecutionAlgorithmCore, ExecutionAlgorithmNative,
51};
52use crate::nautilus_execution_algorithm;
53
54pub type TwapAlgorithmConfig = ExecutionAlgorithmConfig;
56
57#[derive(Debug)]
63pub struct TwapAlgorithm {
64 pub core: ExecutionAlgorithmCore,
66 scheduled_sizes: AHashMap<ClientOrderId, Vec<Quantity>>,
68}
69
70impl TwapAlgorithm {
71 #[must_use]
73 pub fn new(config: TwapAlgorithmConfig) -> Self {
74 Self {
75 core: ExecutionAlgorithmCore::new(config),
76 scheduled_sizes: AHashMap::new(),
77 }
78 }
79
80 fn complete_sequence(&mut self, primary_id: ClientOrderId) {
82 let timer_name = primary_id.as_str();
83 let core = ExecutionAlgorithmNative::exec_algorithm_core_mut(self);
84 if core.clock_mut().timer_names().contains(&timer_name) {
85 core.clock_mut().cancel_timer(timer_name);
86 }
87 self.scheduled_sizes.remove(&primary_id);
88 log::info!("Completed TWAP execution for {primary_id}");
89 }
90}
91
92impl DataActor for TwapAlgorithm {
95 fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
96 ExecutionAlgorithm::on_time_event(self, event)
97 }
98
99 fn on_stop(&mut self) -> anyhow::Result<()> {
100 ExecutionAlgorithm::on_stop(self)
101 }
102
103 fn on_reset(&mut self) -> anyhow::Result<()> {
104 ExecutionAlgorithm::on_reset(self)
105 }
106}
107
108nautilus_execution_algorithm!(TwapAlgorithm, {
109 fn on_order(&mut self, order: OrderAny) -> anyhow::Result<()> {
110 let primary_id = order.client_order_id();
111
112 if self.scheduled_sizes.contains_key(&primary_id) {
113 anyhow::bail!("Order {primary_id} already being executed");
114 }
115
116 log::info!("Received order for TWAP execution: {order:?}");
117
118 if order.order_type() != OrderType::Market {
120 log::error!(
121 "Cannot execute order: only implemented for market orders, order_type={:?}",
122 order.order_type()
123 );
124 return Ok(());
125 }
126
127 let instrument = {
128 let cache = ExecutionAlgorithmNative::exec_algorithm_core(self).cache_ref();
129 cache.instrument(&order.instrument_id()).cloned()
130 };
131
132 let Some(instrument) = instrument else {
133 log::error!(
134 "Cannot execute order: instrument {} not found",
135 order.instrument_id()
136 );
137 return Ok(());
138 };
139
140 let Some(exec_params) = order.exec_algorithm_params() else {
141 log::error!(
142 "Cannot execute order: exec_algorithm_params not found for primary order {primary_id}"
143 );
144 return Ok(());
145 };
146
147 let Some(horizon_secs_str) = exec_params.get(&Ustr::from("horizon_secs")) else {
148 log::error!("Cannot execute order: horizon_secs not found in exec_algorithm_params");
149 return Ok(());
150 };
151
152 let horizon_secs: f64 = horizon_secs_str.parse().map_err(|e| {
153 log::error!("Cannot parse horizon_secs: {e}");
154 anyhow::anyhow!("Invalid horizon_secs")
155 })?;
156
157 let Some(interval_secs_str) = exec_params.get(&Ustr::from("interval_secs")) else {
158 log::error!("Cannot execute order: interval_secs not found in exec_algorithm_params");
159 return Ok(());
160 };
161
162 let interval_secs: f64 = interval_secs_str.parse().map_err(|e| {
163 log::error!("Cannot parse interval_secs: {e}");
164 anyhow::anyhow!("Invalid interval_secs")
165 })?;
166
167 if !horizon_secs.is_finite() || horizon_secs <= 0.0 {
168 log::error!(
169 "Cannot execute order: horizon_secs={horizon_secs} must be finite and positive"
170 );
171 return Ok(());
172 }
173
174 if !interval_secs.is_finite() || interval_secs <= 0.0 {
175 log::error!(
176 "Cannot execute order: interval_secs={interval_secs} must be finite and positive"
177 );
178 return Ok(());
179 }
180
181 if horizon_secs < interval_secs {
182 log::error!(
183 "Cannot execute order: horizon_secs={horizon_secs} was less than interval_secs={interval_secs}"
184 );
185 return Ok(());
186 }
187
188 let num_intervals = (horizon_secs / interval_secs).floor() as u64;
189 if num_intervals == 0 {
190 log::error!("Cannot execute order: num_intervals is 0");
191 return Ok(());
192 }
193
194 let total_qty = order.quantity();
195 let total_raw = total_qty.raw;
196 let precision = total_qty.precision;
197
198 let qty_per_interval_raw = total_raw / (num_intervals as QuantityRaw);
199 let qty_per_interval = Quantity::from_raw(qty_per_interval_raw, precision);
200
201 if qty_per_interval == total_qty || qty_per_interval < instrument.size_increment() {
202 log::warn!(
203 "Submitting for entire size: qty_per_interval={qty_per_interval}, order_quantity={total_qty}"
204 );
205 self.submit_order(order, None, None)?;
206 return Ok(());
207 }
208
209 if let Some(min_qty) = instrument.min_quantity()
210 && qty_per_interval < min_qty
211 {
212 log::warn!(
213 "Submitting for entire size: qty_per_interval={qty_per_interval} < min_quantity={min_qty}"
214 );
215 self.submit_order(order, None, None)?;
216 return Ok(());
217 }
218
219 let mut scheduled_sizes: Vec<Quantity> = vec![qty_per_interval; num_intervals as usize];
220
221 let scheduled_total = qty_per_interval_raw * (num_intervals as QuantityRaw);
223 let remainder_raw = total_raw - scheduled_total;
224 if remainder_raw > 0 {
225 let remainder = Quantity::from_raw(remainder_raw, total_qty.precision);
226 scheduled_sizes.push(remainder);
227 }
228
229 log::info!("Order execution size schedule: {scheduled_sizes:?}");
230
231 {
234 let cache_rc = ExecutionAlgorithmNative::exec_algorithm_core(self).cache_rc();
235 let mut cache = cache_rc.borrow_mut();
236 if !cache.order_exists(&primary_id) {
237 cache.add_order(order.clone(), None, None, false)?;
238 }
239 }
240
241 self.scheduled_sizes
242 .insert(primary_id, scheduled_sizes.clone());
243
244 let first_qty = self.scheduled_sizes.get_mut(&primary_id).unwrap().remove(0);
245 let is_single_slice = self
246 .scheduled_sizes
247 .get(&primary_id)
248 .is_some_and(|s| s.is_empty());
249
250 if is_single_slice {
252 self.submit_order(order, None, None)?;
253 self.complete_sequence(primary_id);
254 return Ok(());
255 }
256
257 let tags = order.tags().map(|t| t.to_vec());
259 let time_in_force = order.time_in_force();
260 let reduce_only = order.is_reduce_only();
261 let mut order = order;
262 let spawned = self.spawn_market(
263 &mut order,
264 first_qty,
265 time_in_force,
266 reduce_only,
267 tags,
268 true,
269 );
270 self.submit_order(spawned.into(), None, None)?;
271
272 ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
273 .clock_mut()
274 .set_timer(
275 primary_id.as_str(),
276 Duration::from_secs_f64(interval_secs),
277 None,
278 None,
279 None,
280 None,
281 None,
282 )?;
283
284 log::info!(
285 "Started TWAP execution for {primary_id}: horizon_secs={horizon_secs}, interval_secs={interval_secs}"
286 );
287
288 Ok(())
289 }
290
291 fn on_time_event(&mut self, event: &TimeEvent) -> anyhow::Result<()> {
292 log::info!("Received time event: {event:?}");
293
294 let primary_id = ClientOrderId::new(event.name.as_str());
295
296 let primary = {
297 let cache = ExecutionAlgorithmNative::exec_algorithm_core(self).cache_ref();
298 cache.order(&primary_id).map(|o| o.clone())
299 };
300
301 let Some(primary) = primary else {
302 log::error!("Cannot find primary order for exec_spawn_id={primary_id}");
303 return Ok(());
304 };
305
306 if primary.is_closed() {
307 self.complete_sequence(primary_id);
308 return Ok(());
309 }
310
311 let Some(scheduled_sizes) = self.scheduled_sizes.get_mut(&primary_id) else {
312 log::error!("Cannot find scheduled sizes for exec_spawn_id={primary_id}");
313 return Ok(());
314 };
315
316 if scheduled_sizes.is_empty() {
317 log::warn!("No more size to execute for exec_spawn_id={primary_id}");
318 return Ok(());
319 }
320
321 let quantity = scheduled_sizes.remove(0);
322 let is_final_slice = scheduled_sizes.is_empty();
323
324 if is_final_slice {
326 self.submit_order(primary, None, None)?;
327 self.complete_sequence(primary_id);
328 return Ok(());
329 }
330
331 let tags = primary.tags().map(|t| t.to_vec());
333 let time_in_force = primary.time_in_force();
334 let reduce_only = primary.is_reduce_only();
335 let mut primary = primary;
336 let spawned = self.spawn_market(
337 &mut primary,
338 quantity,
339 time_in_force,
340 reduce_only,
341 tags,
342 true,
343 );
344 self.submit_order(spawned.into(), None, None)?;
345
346 Ok(())
347 }
348
349 fn on_stop(&mut self) -> anyhow::Result<()> {
350 ExecutionAlgorithmNative::exec_algorithm_core_mut(self)
351 .clock_mut()
352 .cancel_timers();
353 Ok(())
354 }
355
356 fn on_reset(&mut self) -> anyhow::Result<()> {
357 self.unsubscribe_all_strategy_events();
358 ExecutionAlgorithmNative::exec_algorithm_core_mut(self).reset();
359 self.scheduled_sizes.clear();
360 Ok(())
361 }
362});
363
364#[cfg(test)]
365mod tests {
366 use std::{cell::RefCell, rc::Rc};
367
368 use indexmap::IndexMap;
369 use nautilus_common::{
370 cache::Cache,
371 clock::{Clock, TestClock},
372 component::Component,
373 enums::ComponentTrigger,
374 };
375 use nautilus_core::UUID4;
376 use nautilus_model::{
377 enums::{OrderSide, TimeInForce},
378 events::{OrderEventAny, order::spec::OrderCanceledSpec},
379 identifiers::{ExecAlgorithmId, InstrumentId, StrategyId, TraderId},
380 orders::{LimitOrder, MarketOrder},
381 types::Price,
382 };
383 use rstest::rstest;
384 use ustr::Ustr;
385
386 use super::*;
387
388 fn create_twap_algorithm() -> TwapAlgorithm {
389 let unique_id = format!("TWAP-{}", UUID4::new());
391 let config = TwapAlgorithmConfig {
392 exec_algorithm_id: Some(ExecAlgorithmId::new(&unique_id)),
393 ..Default::default()
394 };
395 TwapAlgorithm::new(config)
396 }
397
398 fn register_algorithm(algo: &mut TwapAlgorithm) {
399 use nautilus_common::timer::TimeEventCallback;
400
401 let trader_id = TraderId::from("TRADER-001");
402 let clock = Rc::new(RefCell::new(TestClock::new()));
403 let cache = Rc::new(RefCell::new(Cache::default()));
404
405 clock
407 .borrow_mut()
408 .register_default_handler(TimeEventCallback::Rust(std::sync::Arc::new(|_| {})));
409
410 algo.core.register(trader_id, clock, cache).unwrap();
411
412 algo.transition_state(ComponentTrigger::Initialize).unwrap();
414 algo.transition_state(ComponentTrigger::Start).unwrap();
415 algo.transition_state(ComponentTrigger::StartCompleted)
416 .unwrap();
417 }
418
419 fn add_instrument_to_cache(algo: &TwapAlgorithm) {
420 use nautilus_model::instruments::{InstrumentAny, stubs::crypto_perpetual_ethusdt};
421
422 let instrument = crypto_perpetual_ethusdt();
423 let cache_rc = algo.core.cache_rc();
424 let mut cache = cache_rc.borrow_mut();
425 cache
426 .add_instrument(InstrumentAny::CryptoPerpetual(instrument))
427 .unwrap();
428 }
429
430 fn create_market_order_with_params(params: IndexMap<Ustr, Ustr>) -> OrderAny {
431 create_market_order_with_params_and_qty(params, Quantity::from("1.0"))
432 }
433
434 fn create_market_order_with_params_and_qty(
435 params: IndexMap<Ustr, Ustr>,
436 quantity: Quantity,
437 ) -> OrderAny {
438 OrderAny::Market(MarketOrder::new(
439 TraderId::from("TRADER-001"),
440 StrategyId::from("STRAT-001"),
441 InstrumentId::from("ETHUSDT-PERP.BINANCE"),
442 ClientOrderId::from("O-001"),
443 OrderSide::Buy,
444 quantity,
445 TimeInForce::Gtc,
446 UUID4::new(),
447 0.into(),
448 false,
449 false,
450 None,
451 None,
452 None,
453 None,
454 Some(ExecAlgorithmId::new("TWAP")),
455 Some(params),
456 None,
457 None,
458 ))
459 }
460
461 #[rstest]
462 fn test_twap_creation() {
463 let algo = create_twap_algorithm();
464 assert!(algo.id().inner().starts_with("TWAP"));
465 assert!(algo.scheduled_sizes.is_empty());
466 }
467
468 #[rstest]
469 fn test_twap_registration() {
470 let mut algo = create_twap_algorithm();
471 register_algorithm(&mut algo);
472
473 assert_eq!(algo.trader_id(), Some(TraderId::from("TRADER-001")));
474 }
475
476 #[rstest]
477 fn test_twap_reset_clears_scheduled_sizes() {
478 let mut algo = create_twap_algorithm();
479 let primary_id = ClientOrderId::new("O-001");
480
481 algo.scheduled_sizes
482 .insert(primary_id, vec![Quantity::from("1.0")]);
483
484 assert!(!algo.scheduled_sizes.is_empty());
485
486 DataActor::on_reset(&mut algo).unwrap();
488
489 assert!(algo.scheduled_sizes.is_empty());
490 }
491
492 #[rstest]
493 fn test_twap_rejects_non_market_orders() {
494 let mut algo = create_twap_algorithm();
495 register_algorithm(&mut algo);
496
497 let order = OrderAny::Limit(LimitOrder::new(
498 TraderId::from("TRADER-001"),
499 StrategyId::from("STRAT-001"),
500 InstrumentId::from("BTC/USDT.BINANCE"),
501 ClientOrderId::from("O-001"),
502 OrderSide::Buy,
503 Quantity::from("1.0"),
504 Price::from("50000.0"),
505 TimeInForce::Gtc,
506 None, false, false, false, None, None, None, None, None, None, None, None, None, None, None, UUID4::new(),
522 0.into(),
523 ));
524
525 let result = algo.on_order(order);
527 assert!(result.is_ok());
528 }
529
530 #[rstest]
531 fn test_twap_rejects_missing_params() {
532 let mut algo = create_twap_algorithm();
533 register_algorithm(&mut algo);
534
535 let order = OrderAny::Market(MarketOrder::new(
536 TraderId::from("TRADER-001"),
537 StrategyId::from("STRAT-001"),
538 InstrumentId::from("BTC/USDT.BINANCE"),
539 ClientOrderId::from("O-001"),
540 OrderSide::Buy,
541 Quantity::from("1.0"),
542 TimeInForce::Gtc,
543 UUID4::new(),
544 0.into(),
545 false,
546 false,
547 None,
548 None,
549 None,
550 None,
551 None,
552 None, None,
554 None,
555 ));
556
557 let result = algo.on_order(order);
559 assert!(result.is_ok());
560 }
561
562 #[rstest]
563 fn test_twap_rejects_horizon_less_than_interval() {
564 let mut algo = create_twap_algorithm();
565 register_algorithm(&mut algo);
566
567 add_instrument_to_cache(&algo);
568
569 let mut params = IndexMap::new();
570 params.insert(Ustr::from("horizon_secs"), Ustr::from("30"));
571 params.insert(Ustr::from("interval_secs"), Ustr::from("60"));
572
573 let order = create_market_order_with_params(params);
574 let result = algo.on_order(order);
575
576 assert!(result.is_ok());
577 assert!(algo.scheduled_sizes.is_empty());
578 }
579
580 #[rstest]
581 fn test_twap_rejects_duplicate_order() {
582 let mut algo = create_twap_algorithm();
583 register_algorithm(&mut algo);
584
585 add_instrument_to_cache(&algo);
586
587 let mut params = IndexMap::new();
588 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
589 params.insert(Ustr::from("interval_secs"), Ustr::from("10"));
590
591 let order1 = create_market_order_with_params(params.clone());
592 let order2 = create_market_order_with_params(params);
593
594 algo.on_order(order1).unwrap();
595 let result = algo.on_order(order2);
596
597 assert!(result.is_err());
598 assert!(
599 result
600 .unwrap_err()
601 .to_string()
602 .contains("already being executed")
603 );
604 }
605
606 #[rstest]
607 fn test_twap_calculates_size_schedule_evenly() {
608 let mut algo = create_twap_algorithm();
609 register_algorithm(&mut algo);
610
611 add_instrument_to_cache(&algo);
612
613 let mut params = IndexMap::new();
615 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
616 params.insert(Ustr::from("interval_secs"), Ustr::from("20"));
617
618 let order = create_market_order_with_params_and_qty(params, Quantity::from("1.2"));
619 let primary_id = order.client_order_id();
620
621 algo.on_order(order).unwrap();
622
623 let remaining = algo.scheduled_sizes.get(&primary_id).unwrap();
625 assert_eq!(remaining.len(), 2);
626
627 for qty in remaining {
628 assert_eq!(*qty, Quantity::from("0.4"));
629 }
630 }
631
632 #[rstest]
633 fn test_twap_reduces_cached_primary_after_first_child_spawn() {
634 let mut algo = create_twap_algorithm();
635 register_algorithm(&mut algo);
636
637 add_instrument_to_cache(&algo);
638
639 let mut params = IndexMap::new();
640 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
641 params.insert(Ustr::from("interval_secs"), Ustr::from("30"));
642
643 let order = create_market_order_with_params_and_qty(params, Quantity::from("1.2"));
644 let primary_id = order.client_order_id();
645
646 algo.on_order(order).unwrap();
647
648 let cache = algo.cache();
649 let primary = cache.order(&primary_id).unwrap();
650 let spawned = cache.order(&ClientOrderId::from("O-001-E1")).unwrap();
651
652 assert_eq!(primary.quantity(), Quantity::from("0.6"));
653 assert_eq!(spawned.quantity(), Quantity::from("0.6"));
654 assert_eq!(spawned.exec_spawn_id(), Some(primary_id));
655 }
656
657 #[rstest]
658 fn test_twap_on_order_accepts_already_cached_primary() {
659 let mut algo = create_twap_algorithm();
660 register_algorithm(&mut algo);
661
662 add_instrument_to_cache(&algo);
663
664 let mut params = IndexMap::new();
665 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
666 params.insert(Ustr::from("interval_secs"), Ustr::from("20"));
667
668 let order = create_market_order_with_params_and_qty(params, Quantity::from("1.2"));
669 let primary_id = order.client_order_id();
670
671 {
673 let cache_rc = algo.core.cache_rc();
674 let mut cache = cache_rc.borrow_mut();
675 cache.add_order(order.clone(), None, None, false).unwrap();
676 }
677
678 algo.on_order(order).unwrap();
679
680 assert_eq!(algo.scheduled_sizes.get(&primary_id).unwrap().len(), 2);
681 }
682
683 #[rstest]
684 fn test_twap_calculates_size_schedule_with_remainder() {
685 let mut algo = create_twap_algorithm();
686 register_algorithm(&mut algo);
687
688 add_instrument_to_cache(&algo);
689
690 let mut params = IndexMap::new();
693 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
694 params.insert(Ustr::from("interval_secs"), Ustr::from("20"));
695
696 let order = create_market_order_with_params(params);
697 let primary_id = order.client_order_id();
698
699 algo.on_order(order).unwrap();
700
701 let remaining = algo.scheduled_sizes.get(&primary_id).unwrap();
703 assert_eq!(remaining.len(), 3);
704
705 #[cfg(feature = "high-precision")]
709 {
710 assert_eq!(remaining[0].raw, 3_333_333_333_333_333);
711 assert_eq!(remaining[1].raw, 3_333_333_333_333_333);
712 assert_eq!(remaining[2].raw, 1);
713 }
714 #[cfg(not(feature = "high-precision"))]
715 {
716 assert_eq!(remaining[0].raw, 333_333_333);
717 assert_eq!(remaining[1].raw, 333_333_333);
718 assert_eq!(remaining[2].raw, 1);
719 }
720 }
721
722 #[rstest]
723 fn test_twap_on_time_event_spawns_next_slice() {
724 let mut algo = create_twap_algorithm();
725 register_algorithm(&mut algo);
726
727 add_instrument_to_cache(&algo);
728
729 let mut params = IndexMap::new();
731 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
732 params.insert(Ustr::from("interval_secs"), Ustr::from("20"));
733
734 let order = create_market_order_with_params_and_qty(params, Quantity::from("1.2"));
735 let primary_id = order.client_order_id();
736
737 algo.on_order(order).unwrap();
738
739 assert_eq!(algo.scheduled_sizes.get(&primary_id).unwrap().len(), 2);
741
742 let event = TimeEvent::new(primary_id.inner(), UUID4::new(), 0.into(), 0.into());
744 ExecutionAlgorithm::on_time_event(&mut algo, &event).unwrap();
745
746 assert_eq!(algo.scheduled_sizes.get(&primary_id).unwrap().len(), 1);
748 }
749
750 #[rstest]
751 fn test_twap_data_actor_dispatch_spawns_next_slice() {
752 let mut algo = create_twap_algorithm();
753 register_algorithm(&mut algo);
754
755 add_instrument_to_cache(&algo);
756
757 let mut params = IndexMap::new();
758 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
759 params.insert(Ustr::from("interval_secs"), Ustr::from("20"));
760
761 let order = create_market_order_with_params_and_qty(params, Quantity::from("1.2"));
762 let primary_id = order.client_order_id();
763
764 algo.on_order(order).unwrap();
765 assert_eq!(algo.scheduled_sizes.get(&primary_id).unwrap().len(), 2);
766
767 let event = TimeEvent::new(primary_id.inner(), UUID4::new(), 0.into(), 0.into());
769 algo.handle_time_event(&event);
770
771 assert_eq!(algo.scheduled_sizes.get(&primary_id).unwrap().len(), 1);
772 }
773
774 #[rstest]
775 fn test_twap_on_time_event_completes_on_final_slice() {
776 let mut algo = create_twap_algorithm();
777 register_algorithm(&mut algo);
778
779 add_instrument_to_cache(&algo);
780
781 let mut params = IndexMap::new();
783 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
784 params.insert(Ustr::from("interval_secs"), Ustr::from("30"));
785
786 let order = create_market_order_with_params(params);
787 let primary_id = order.client_order_id();
788
789 algo.on_order(order).unwrap();
790 assert_eq!(algo.scheduled_sizes.get(&primary_id).unwrap().len(), 1);
791
792 let event = TimeEvent::new(primary_id.inner(), UUID4::new(), 0.into(), 0.into());
794 ExecutionAlgorithm::on_time_event(&mut algo, &event).unwrap();
795
796 assert!(algo.scheduled_sizes.get(&primary_id).is_none());
798 }
799
800 #[rstest]
801 fn test_twap_on_time_event_completes_when_primary_closed() {
802 let mut algo = create_twap_algorithm();
803 register_algorithm(&mut algo);
804
805 add_instrument_to_cache(&algo);
806
807 let mut params = IndexMap::new();
808 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
809 params.insert(Ustr::from("interval_secs"), Ustr::from("20"));
810
811 let order = create_market_order_with_params_and_qty(params, Quantity::from("1.2"));
812 let primary_id = order.client_order_id();
813
814 algo.on_order(order).unwrap();
815 assert_eq!(algo.scheduled_sizes.get(&primary_id).unwrap().len(), 2);
816
817 {
819 let cache_rc = algo.core.cache_rc();
820 let mut cache = cache_rc.borrow_mut();
821 let primary = cache.order(&primary_id).map(|o| o.clone()).unwrap();
822
823 let canceled = OrderCanceledSpec::builder()
824 .trader_id(primary.trader_id())
825 .strategy_id(primary.strategy_id())
826 .instrument_id(primary.instrument_id())
827 .client_order_id(primary.client_order_id())
828 .build();
829 cache
830 .update_order(&OrderEventAny::Canceled(canceled))
831 .unwrap();
832 }
833
834 let event = TimeEvent::new(primary_id.inner(), UUID4::new(), 0.into(), 0.into());
836 ExecutionAlgorithm::on_time_event(&mut algo, &event).unwrap();
837
838 assert!(algo.scheduled_sizes.get(&primary_id).is_none());
840 }
841
842 #[rstest]
843 fn test_twap_on_stop_cancels_timers() {
844 let mut algo = create_twap_algorithm();
845 register_algorithm(&mut algo);
846
847 add_instrument_to_cache(&algo);
848
849 let mut params = IndexMap::new();
850 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
851 params.insert(Ustr::from("interval_secs"), Ustr::from("20"));
852
853 let order = create_market_order_with_params(params);
854 let primary_id = order.client_order_id();
855
856 algo.on_order(order).unwrap();
857
858 assert!(
860 algo.clock()
861 .timer_names()
862 .iter()
863 .any(|name| name.as_str() == primary_id.as_str())
864 );
865
866 DataActor::on_stop(&mut algo).unwrap();
868
869 assert!(algo.clock().timer_names().is_empty());
871 }
872
873 #[rstest]
874 fn test_twap_fractional_interval_secs() {
875 let mut algo = create_twap_algorithm();
876 register_algorithm(&mut algo);
877
878 add_instrument_to_cache(&algo);
879
880 let mut params = IndexMap::new();
882 params.insert(Ustr::from("horizon_secs"), Ustr::from("3"));
883 params.insert(Ustr::from("interval_secs"), Ustr::from("0.5"));
884
885 let order = create_market_order_with_params(params);
886 let primary_id = order.client_order_id();
887
888 algo.on_order(order).unwrap();
890
891 let remaining = algo.scheduled_sizes.get(&primary_id).unwrap();
893 assert!(remaining.len() >= 5);
894 }
895
896 #[rstest]
897 fn test_twap_submits_entire_size_when_qty_per_interval_below_size_increment() {
898 use nautilus_model::instruments::{InstrumentAny, stubs::equity_aapl};
899
900 let mut algo = create_twap_algorithm();
901 register_algorithm(&mut algo);
902
903 let instrument = equity_aapl();
905 let instrument_id = instrument.id();
906 {
907 let cache_rc = algo.core.cache_rc();
908 let mut cache = cache_rc.borrow_mut();
909 cache
910 .add_instrument(InstrumentAny::Equity(instrument))
911 .unwrap();
912 }
913
914 let mut params = IndexMap::new();
917 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
918 params.insert(Ustr::from("interval_secs"), Ustr::from("10"));
919
920 let order = OrderAny::Market(MarketOrder::new(
921 TraderId::from("TRADER-001"),
922 StrategyId::from("STRAT-001"),
923 instrument_id,
924 ClientOrderId::from("O-002"),
925 OrderSide::Buy,
926 Quantity::from("2"),
927 TimeInForce::Gtc,
928 UUID4::new(),
929 0.into(),
930 false,
931 false,
932 None,
933 None,
934 None,
935 None,
936 Some(ExecAlgorithmId::new("TWAP")),
937 Some(params),
938 None,
939 None,
940 ));
941
942 let primary_id = order.client_order_id();
943 algo.on_order(order).unwrap();
944
945 assert!(algo.scheduled_sizes.get(&primary_id).is_none());
947 }
948
949 #[rstest]
950 fn test_twap_rejects_negative_interval_secs() {
951 let mut algo = create_twap_algorithm();
952 register_algorithm(&mut algo);
953
954 add_instrument_to_cache(&algo);
955
956 let mut params = IndexMap::new();
957 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
958 params.insert(Ustr::from("interval_secs"), Ustr::from("-0.5"));
959
960 let order = create_market_order_with_params(params);
961
962 let result = algo.on_order(order);
964 assert!(result.is_ok());
965 assert!(algo.scheduled_sizes.is_empty());
966 }
967
968 #[rstest]
969 fn test_twap_rejects_negative_horizon_secs() {
970 let mut algo = create_twap_algorithm();
971 register_algorithm(&mut algo);
972
973 add_instrument_to_cache(&algo);
974
975 let mut params = IndexMap::new();
976 params.insert(Ustr::from("horizon_secs"), Ustr::from("-10"));
977 params.insert(Ustr::from("interval_secs"), Ustr::from("1"));
978
979 let order = create_market_order_with_params(params);
980
981 let result = algo.on_order(order);
983 assert!(result.is_ok());
984 assert!(algo.scheduled_sizes.is_empty());
985 }
986
987 #[rstest]
988 fn test_twap_rejects_zero_interval_secs() {
989 let mut algo = create_twap_algorithm();
990 register_algorithm(&mut algo);
991
992 add_instrument_to_cache(&algo);
993
994 let mut params = IndexMap::new();
995 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
996 params.insert(Ustr::from("interval_secs"), Ustr::from("0"));
997
998 let order = create_market_order_with_params(params);
999
1000 let result = algo.on_order(order);
1002 assert!(result.is_ok());
1003 assert!(algo.scheduled_sizes.is_empty());
1004 }
1005
1006 #[rstest]
1007 fn test_twap_rejects_nan_interval_secs() {
1008 let mut algo = create_twap_algorithm();
1009 register_algorithm(&mut algo);
1010
1011 add_instrument_to_cache(&algo);
1012
1013 let mut params = IndexMap::new();
1014 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
1015 params.insert(Ustr::from("interval_secs"), Ustr::from("NaN"));
1016
1017 let order = create_market_order_with_params(params);
1018
1019 let result = algo.on_order(order);
1020 assert!(result.is_ok());
1021 assert!(algo.scheduled_sizes.is_empty());
1022 }
1023
1024 #[rstest]
1025 fn test_twap_rejects_infinity_horizon_secs() {
1026 let mut algo = create_twap_algorithm();
1027 register_algorithm(&mut algo);
1028
1029 add_instrument_to_cache(&algo);
1030
1031 let mut params = IndexMap::new();
1032 params.insert(Ustr::from("horizon_secs"), Ustr::from("inf"));
1033 params.insert(Ustr::from("interval_secs"), Ustr::from("10"));
1034
1035 let order = create_market_order_with_params(params);
1036
1037 let result = algo.on_order(order);
1038 assert!(result.is_ok());
1039 assert!(algo.scheduled_sizes.is_empty());
1040 }
1041}