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