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,
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_calculates_size_schedule_with_remainder() {
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();
619 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
620 params.insert(Ustr::from("interval_secs"), Ustr::from("20"));
621
622 let order = create_market_order_with_params(params);
623 let primary_id = order.client_order_id();
624
625 algo.on_order(order).unwrap();
626
627 let remaining = algo.scheduled_sizes.get(&primary_id).unwrap();
629 assert_eq!(remaining.len(), 3);
630
631 #[cfg(feature = "high-precision")]
635 {
636 assert_eq!(remaining[0].raw, 3_333_333_333_333_333);
637 assert_eq!(remaining[1].raw, 3_333_333_333_333_333);
638 assert_eq!(remaining[2].raw, 1);
639 }
640 #[cfg(not(feature = "high-precision"))]
641 {
642 assert_eq!(remaining[0].raw, 333_333_333);
643 assert_eq!(remaining[1].raw, 333_333_333);
644 assert_eq!(remaining[2].raw, 1);
645 }
646 }
647
648 #[rstest]
649 fn test_twap_on_time_event_spawns_next_slice() {
650 let mut algo = create_twap_algorithm();
651 register_algorithm(&mut algo);
652
653 add_instrument_to_cache(&algo);
654
655 let mut params = IndexMap::new();
657 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
658 params.insert(Ustr::from("interval_secs"), Ustr::from("20"));
659
660 let order = create_market_order_with_params_and_qty(params, Quantity::from("1.2"));
661 let primary_id = order.client_order_id();
662
663 algo.on_order(order).unwrap();
664
665 assert_eq!(algo.scheduled_sizes.get(&primary_id).unwrap().len(), 2);
667
668 let event = TimeEvent::new(primary_id.inner(), UUID4::new(), 0.into(), 0.into());
670 ExecutionAlgorithm::on_time_event(&mut algo, &event).unwrap();
671
672 assert_eq!(algo.scheduled_sizes.get(&primary_id).unwrap().len(), 1);
674 }
675
676 #[rstest]
677 fn test_twap_on_time_event_completes_on_final_slice() {
678 let mut algo = create_twap_algorithm();
679 register_algorithm(&mut algo);
680
681 add_instrument_to_cache(&algo);
682
683 let mut params = IndexMap::new();
685 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
686 params.insert(Ustr::from("interval_secs"), Ustr::from("30"));
687
688 let order = create_market_order_with_params(params);
689 let primary_id = order.client_order_id();
690
691 algo.on_order(order).unwrap();
692 assert_eq!(algo.scheduled_sizes.get(&primary_id).unwrap().len(), 1);
693
694 let event = TimeEvent::new(primary_id.inner(), UUID4::new(), 0.into(), 0.into());
696 ExecutionAlgorithm::on_time_event(&mut algo, &event).unwrap();
697
698 assert!(algo.scheduled_sizes.get(&primary_id).is_none());
700 }
701
702 #[rstest]
703 fn test_twap_on_time_event_completes_when_primary_closed() {
704 use nautilus_model::events::OrderCanceled;
705
706 let mut algo = create_twap_algorithm();
707 register_algorithm(&mut algo);
708
709 add_instrument_to_cache(&algo);
710
711 let mut params = IndexMap::new();
712 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
713 params.insert(Ustr::from("interval_secs"), Ustr::from("20"));
714
715 let order = create_market_order_with_params_and_qty(params, Quantity::from("1.2"));
716 let primary_id = order.client_order_id();
717
718 algo.on_order(order).unwrap();
719 assert_eq!(algo.scheduled_sizes.get(&primary_id).unwrap().len(), 2);
720
721 {
723 let cache_rc = algo.core.cache_rc();
724 let mut cache = cache_rc.borrow_mut();
725 let primary = cache.order(&primary_id).map(|o| o.clone()).unwrap();
726
727 let canceled = OrderCanceled::new(
728 primary.trader_id(),
729 primary.strategy_id(),
730 primary.instrument_id(),
731 primary.client_order_id(),
732 UUID4::new(),
733 0.into(),
734 0.into(),
735 false,
736 None,
737 None,
738 );
739 cache
740 .update_order(&OrderEventAny::Canceled(canceled))
741 .unwrap();
742 }
743
744 let event = TimeEvent::new(primary_id.inner(), UUID4::new(), 0.into(), 0.into());
746 ExecutionAlgorithm::on_time_event(&mut algo, &event).unwrap();
747
748 assert!(algo.scheduled_sizes.get(&primary_id).is_none());
750 }
751
752 #[rstest]
753 fn test_twap_on_stop_cancels_timers() {
754 let mut algo = create_twap_algorithm();
755 register_algorithm(&mut algo);
756
757 add_instrument_to_cache(&algo);
758
759 let mut params = IndexMap::new();
760 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
761 params.insert(Ustr::from("interval_secs"), Ustr::from("20"));
762
763 let order = create_market_order_with_params(params);
764 let primary_id = order.client_order_id();
765
766 algo.on_order(order).unwrap();
767
768 assert!(
770 algo.core
771 .clock()
772 .timer_names()
773 .contains(&primary_id.as_str())
774 );
775
776 ExecutionAlgorithm::on_stop(&mut algo).unwrap();
778
779 assert!(algo.core.clock().timer_names().is_empty());
781 }
782
783 #[rstest]
784 fn test_twap_fractional_interval_secs() {
785 let mut algo = create_twap_algorithm();
786 register_algorithm(&mut algo);
787
788 add_instrument_to_cache(&algo);
789
790 let mut params = IndexMap::new();
792 params.insert(Ustr::from("horizon_secs"), Ustr::from("3"));
793 params.insert(Ustr::from("interval_secs"), Ustr::from("0.5"));
794
795 let order = create_market_order_with_params(params);
796 let primary_id = order.client_order_id();
797
798 algo.on_order(order).unwrap();
800
801 let remaining = algo.scheduled_sizes.get(&primary_id).unwrap();
803 assert!(remaining.len() >= 5);
804 }
805
806 #[rstest]
807 fn test_twap_submits_entire_size_when_qty_per_interval_below_size_increment() {
808 use nautilus_model::instruments::{InstrumentAny, stubs::equity_aapl};
809
810 let mut algo = create_twap_algorithm();
811 register_algorithm(&mut algo);
812
813 let instrument = equity_aapl();
815 let instrument_id = instrument.id();
816 {
817 let cache_rc = algo.core.cache_rc();
818 let mut cache = cache_rc.borrow_mut();
819 cache
820 .add_instrument(InstrumentAny::Equity(instrument))
821 .unwrap();
822 }
823
824 let mut params = IndexMap::new();
827 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
828 params.insert(Ustr::from("interval_secs"), Ustr::from("10"));
829
830 let order = OrderAny::Market(MarketOrder::new(
831 TraderId::from("TRADER-001"),
832 StrategyId::from("STRAT-001"),
833 instrument_id,
834 ClientOrderId::from("O-002"),
835 OrderSide::Buy,
836 Quantity::from("2"),
837 TimeInForce::Gtc,
838 UUID4::new(),
839 0.into(),
840 false,
841 false,
842 None,
843 None,
844 None,
845 None,
846 Some(ExecAlgorithmId::new("TWAP")),
847 Some(params),
848 None,
849 None,
850 ));
851
852 let primary_id = order.client_order_id();
853 algo.on_order(order).unwrap();
854
855 assert!(algo.scheduled_sizes.get(&primary_id).is_none());
857 }
858
859 #[rstest]
860 fn test_twap_rejects_negative_interval_secs() {
861 let mut algo = create_twap_algorithm();
862 register_algorithm(&mut algo);
863
864 add_instrument_to_cache(&algo);
865
866 let mut params = IndexMap::new();
867 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
868 params.insert(Ustr::from("interval_secs"), Ustr::from("-0.5"));
869
870 let order = create_market_order_with_params(params);
871
872 let result = algo.on_order(order);
874 assert!(result.is_ok());
875 assert!(algo.scheduled_sizes.is_empty());
876 }
877
878 #[rstest]
879 fn test_twap_rejects_negative_horizon_secs() {
880 let mut algo = create_twap_algorithm();
881 register_algorithm(&mut algo);
882
883 add_instrument_to_cache(&algo);
884
885 let mut params = IndexMap::new();
886 params.insert(Ustr::from("horizon_secs"), Ustr::from("-10"));
887 params.insert(Ustr::from("interval_secs"), Ustr::from("1"));
888
889 let order = create_market_order_with_params(params);
890
891 let result = algo.on_order(order);
893 assert!(result.is_ok());
894 assert!(algo.scheduled_sizes.is_empty());
895 }
896
897 #[rstest]
898 fn test_twap_rejects_zero_interval_secs() {
899 let mut algo = create_twap_algorithm();
900 register_algorithm(&mut algo);
901
902 add_instrument_to_cache(&algo);
903
904 let mut params = IndexMap::new();
905 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
906 params.insert(Ustr::from("interval_secs"), Ustr::from("0"));
907
908 let order = create_market_order_with_params(params);
909
910 let result = algo.on_order(order);
912 assert!(result.is_ok());
913 assert!(algo.scheduled_sizes.is_empty());
914 }
915
916 #[rstest]
917 fn test_twap_rejects_nan_interval_secs() {
918 let mut algo = create_twap_algorithm();
919 register_algorithm(&mut algo);
920
921 add_instrument_to_cache(&algo);
922
923 let mut params = IndexMap::new();
924 params.insert(Ustr::from("horizon_secs"), Ustr::from("60"));
925 params.insert(Ustr::from("interval_secs"), Ustr::from("NaN"));
926
927 let order = create_market_order_with_params(params);
928
929 let result = algo.on_order(order);
930 assert!(result.is_ok());
931 assert!(algo.scheduled_sizes.is_empty());
932 }
933
934 #[rstest]
935 fn test_twap_rejects_infinity_horizon_secs() {
936 let mut algo = create_twap_algorithm();
937 register_algorithm(&mut algo);
938
939 add_instrument_to_cache(&algo);
940
941 let mut params = IndexMap::new();
942 params.insert(Ustr::from("horizon_secs"), Ustr::from("inf"));
943 params.insert(Ustr::from("interval_secs"), Ustr::from("10"));
944
945 let order = create_market_order_with_params(params);
946
947 let result = algo.on_order(order);
948 assert!(result.is_ok());
949 assert!(algo.scheduled_sizes.is_empty());
950 }
951}