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