1use std::{
22 any::Any,
23 cell::RefCell,
24 fmt::Debug,
25 ops::Add,
26 rc::{Rc, Weak},
27};
28
29use ahash::AHashMap;
30use chrono::{Duration, TimeDelta};
31use nautilus_common::{
32 clock::{Clock, TestClock},
33 timer::{TimeEvent, TimeEventCallback},
34};
35use nautilus_core::{
36 UnixNanos,
37 correctness::{self, FAILED},
38 datetime::{
39 add_n_months, add_n_months_nanos, add_n_years, add_n_years_nanos, subtract_n_months_nanos,
40 subtract_n_years_nanos,
41 },
42};
43use nautilus_model::{
44 data::{
45 QuoteTick, TradeTick,
46 bar::{Bar, BarType, get_bar_interval_ns, get_time_bar_start},
47 },
48 enums::{
49 AggregationSource, AggressorSide, BarAggregation, BarIntervalType,
50 ContinuousFutureAdjustmentType,
51 },
52 identifiers::InstrumentId,
53 instruments::{FixedTickScheme, TickSchemeRule},
54 types::{
55 Price, Quantity,
56 fixed::{FIXED_PRECISION, FIXED_SCALAR, mantissa_exponent_to_fixed_i128},
57 price::PriceRaw,
58 quantity::QuantityRaw,
59 },
60};
61use rust_decimal::{Decimal, prelude::ToPrimitive};
62
63type BarHandler = Box<dyn FnMut(Bar)>;
65
66pub trait BarAggregator: Any + Debug {
70 fn bar_type(&self) -> BarType;
72 fn is_running(&self) -> bool;
74 fn set_is_running(&mut self, value: bool);
76 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos);
78 fn handle_quote(&mut self, quote: QuoteTick) {
80 let spec = self.bar_type().spec();
81 let (Ok(price), Ok(size)) = (
84 quote.extract_price(spec.price_type),
85 quote.extract_size(spec.price_type),
86 ) else {
87 log::error!(
88 "Cannot aggregate quote for {}: price type {} unsupported for quotes",
89 self.bar_type(),
90 spec.price_type,
91 );
92 return;
93 };
94
95 self.update(price, size, quote.ts_init);
96 }
97 fn handle_trade(&mut self, trade: TradeTick) {
99 self.update(trade.price, trade.size, trade.ts_init);
100 }
101 fn handle_bar(&mut self, bar: Bar) {
103 self.update_bar(bar, bar.volume, bar.ts_init);
104 }
105 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos);
106 fn stop(&mut self) {}
108 fn set_historical_mode(&mut self, _historical_mode: bool, _handler: Box<dyn FnMut(Bar)>) {}
110 fn set_historical_events(&mut self, _events: Vec<TimeEvent>) {}
112 fn set_clock(&mut self, _clock: Rc<RefCell<dyn Clock>>) {}
114 fn build_bar(&mut self, _event: &TimeEvent) {}
116 fn start_timer(&mut self, _aggregator_rc: Option<Rc<RefCell<Box<dyn BarAggregator>>>>) {}
120 fn set_aggregator_weak(&mut self, _weak: Weak<RefCell<Box<dyn BarAggregator>>>) {}
123 fn set_adjustment(&mut self, _adjustment: Decimal, _mode: ContinuousFutureAdjustmentType) {}
125 fn set_build_with_no_updates(&mut self, _value: bool) {}
128 fn is_historical(&self) -> bool {
131 false
132 }
133}
134
135impl dyn BarAggregator {
136 pub fn as_any(&self) -> &dyn Any {
138 self
139 }
140 pub fn as_any_mut(&mut self) -> &mut dyn Any {
142 self
143 }
144}
145
146#[derive(Debug)]
148pub struct BarBuilder {
149 bar_type: BarType,
150 price_precision: u8,
151 size_precision: u8,
152 initialized: bool,
153 ts_last: UnixNanos,
154 count: usize,
155 last_close: Option<Price>,
156 open: Option<Price>,
157 high: Option<Price>,
158 low: Option<Price>,
159 close: Option<Price>,
160 volume: Quantity,
161 adjustment_mode: ContinuousFutureAdjustmentType,
162 adjustment_raw: PriceRaw,
163 adjustment_ratio: f64,
164 adjustment_active: bool,
165 adjustment_is_ratio: bool,
166}
167
168impl BarBuilder {
169 #[must_use]
175 pub fn new(bar_type: BarType, price_precision: u8, size_precision: u8) -> Self {
176 correctness::check_equal(
177 &bar_type.aggregation_source(),
178 &AggregationSource::Internal,
179 "bar_type.aggregation_source",
180 "AggregationSource::Internal",
181 )
182 .expect(FAILED);
183
184 Self {
185 bar_type,
186 price_precision,
187 size_precision,
188 initialized: false,
189 ts_last: UnixNanos::default(),
190 count: 0,
191 last_close: None,
192 open: None,
193 high: None,
194 low: None,
195 close: None,
196 volume: Quantity::zero(size_precision),
197 adjustment_mode: ContinuousFutureAdjustmentType::default(),
198 adjustment_raw: 0,
199 adjustment_ratio: 1.0,
200 adjustment_active: false,
201 adjustment_is_ratio: false,
202 }
203 }
204
205 pub fn set_adjustment(&mut self, adjustment: Decimal, mode: ContinuousFutureAdjustmentType) {
216 self.adjustment_mode = mode;
217
218 if mode.is_ratio() {
219 self.adjustment_is_ratio = true;
220 self.adjustment_ratio = adjustment.to_f64().unwrap_or(1.0);
221 self.adjustment_active = adjustment != Decimal::ONE;
222 return;
223 }
224
225 self.adjustment_is_ratio = false;
229 let exponent = -(adjustment.scale() as i8);
230 let raw_i128 =
231 mantissa_exponent_to_fixed_i128(adjustment.mantissa(), exponent, FIXED_PRECISION)
232 .expect("Failed to scale continuous-future adjustment to fixed precision");
233
234 #[allow(
235 clippy::useless_conversion,
236 reason = "i128 to PriceRaw is real when not high-precision"
237 )]
238 let raw: PriceRaw = raw_i128
239 .try_into()
240 .expect("Continuous-future adjustment exceeds PriceRaw range");
241
242 self.adjustment_raw = raw;
243 self.adjustment_active = self.adjustment_raw != 0;
244 }
245
246 fn apply_adjustment_to_price(&self, price: Price) -> Price {
247 if !self.adjustment_active {
248 return price;
249 }
250
251 if self.adjustment_is_ratio {
252 return Price::new(price.as_f64() * self.adjustment_ratio, price.precision);
255 }
256
257 Price::from_raw(price.raw + self.adjustment_raw, price.precision)
259 }
260
261 pub fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
267 if ts_init < self.ts_last {
268 return; }
270
271 let price = self.apply_adjustment_to_price(price);
272
273 if self.open.is_none() {
274 self.open = Some(price);
275 self.high = Some(price);
276 self.low = Some(price);
277 self.initialized = true;
278 } else {
279 if price > self.high.unwrap() {
280 self.high = Some(price);
281 }
282
283 if price < self.low.unwrap() {
284 self.low = Some(price);
285 }
286 }
287
288 self.close = Some(price);
289 self.volume = self.volume.add(size);
290 self.count += 1;
291 self.ts_last = ts_init;
292
293 debug_assert!(self.high >= self.low, "OHLC invariant violated: high < low");
294 }
295
296 pub fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
302 if ts_init < self.ts_last {
303 return; }
305
306 let bar_open = self.apply_adjustment_to_price(bar.open);
307 let bar_high = self.apply_adjustment_to_price(bar.high);
308 let bar_low = self.apply_adjustment_to_price(bar.low);
309 let bar_close = self.apply_adjustment_to_price(bar.close);
310
311 if self.open.is_none() {
312 self.open = Some(bar_open);
313 self.high = Some(bar_high);
314 self.low = Some(bar_low);
315 self.initialized = true;
316 } else {
317 if bar_high > self.high.unwrap() {
318 self.high = Some(bar_high);
319 }
320
321 if bar_low < self.low.unwrap() {
322 self.low = Some(bar_low);
323 }
324 }
325
326 self.close = Some(bar_close);
327 self.volume = self.volume.add(volume);
328 self.count += 1;
329 self.ts_last = ts_init;
330
331 debug_assert!(self.high >= self.low, "OHLC invariant violated: high < low");
332 }
333
334 pub fn reset(&mut self) {
339 self.open = None;
340 self.high = None;
341 self.low = None;
342 self.close = None;
343 self.volume = Quantity::zero(self.size_precision);
344 self.count = 0;
345 }
346
347 pub fn build_now(&mut self) -> Bar {
349 self.build(self.ts_last, self.ts_last)
350 }
351
352 pub fn build(&mut self, ts_event: UnixNanos, ts_init: UnixNanos) -> Bar {
358 if self.open.is_none() {
359 self.open = self.last_close;
360 self.high = self.last_close;
361 self.low = self.last_close;
362 self.close = self.last_close;
363 }
364
365 if let (Some(close), Some(low)) = (self.close, self.low)
366 && close < low
367 {
368 self.low = Some(close);
369 }
370
371 if let (Some(close), Some(high)) = (self.close, self.high)
372 && close > high
373 {
374 self.high = Some(close);
375 }
376
377 let bar = Bar::new(
379 self.bar_type,
380 self.open.unwrap(),
381 self.high.unwrap(),
382 self.low.unwrap(),
383 self.close.unwrap(),
384 self.volume,
385 ts_event,
386 ts_init,
387 );
388
389 self.last_close = self.close;
390 self.reset();
391 bar
392 }
393}
394
395pub struct BarAggregatorCore {
397 bar_type: BarType,
398 builder: BarBuilder,
399 handler: BarHandler,
400 is_running: bool,
401}
402
403impl Debug for BarAggregatorCore {
404 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
405 f.debug_struct(stringify!(BarAggregatorCore))
406 .field("bar_type", &self.bar_type)
407 .field("builder", &self.builder)
408 .field("is_running", &self.is_running)
409 .finish()
410 }
411}
412
413impl BarAggregatorCore {
414 pub fn new<H: FnMut(Bar) + 'static>(
424 bar_type: BarType,
425 price_precision: u8,
426 size_precision: u8,
427 handler: H,
428 ) -> Self {
429 let bar_type = bar_type.standard();
430 Self {
431 bar_type,
432 builder: BarBuilder::new(bar_type, price_precision, size_precision),
433 handler: Box::new(handler),
434 is_running: false,
435 }
436 }
437
438 pub const fn set_is_running(&mut self, value: bool) {
440 self.is_running = value;
441 }
442
443 fn set_handler(&mut self, handler: BarHandler) {
444 self.handler = handler;
445 }
446
447 fn apply_update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
448 self.builder.update(price, size, ts_init);
449 }
450
451 fn is_stale(&self, ts_init: UnixNanos) -> bool {
452 ts_init < self.builder.ts_last
453 }
454
455 fn build_now_and_send(&mut self) {
456 let bar = self.builder.build_now();
457 (self.handler)(bar);
458 }
459
460 fn build_and_send(&mut self, ts_event: UnixNanos, ts_init: UnixNanos) {
461 let bar = self.builder.build(ts_event, ts_init);
462 (self.handler)(bar);
463 }
464
465 fn set_adjustment(&mut self, adjustment: Decimal, mode: ContinuousFutureAdjustmentType) {
466 self.builder.set_adjustment(adjustment, mode);
467 }
468}
469
470macro_rules! impl_set_historical_handler {
471 () => {
472 fn set_historical_mode(&mut self, _historical_mode: bool, handler: Box<dyn FnMut(Bar)>) {
473 self.core.set_handler(handler);
474 }
475 };
476}
477
478macro_rules! impl_set_adjustment {
479 () => {
480 fn set_adjustment(&mut self, adjustment: Decimal, mode: ContinuousFutureAdjustmentType) {
481 self.core.set_adjustment(adjustment, mode);
482 }
483 };
484}
485
486pub struct TickBarAggregator {
491 core: BarAggregatorCore,
492}
493
494impl Debug for TickBarAggregator {
495 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
496 f.debug_struct(stringify!(TickBarAggregator))
497 .field("core", &self.core)
498 .finish()
499 }
500}
501
502impl TickBarAggregator {
503 pub fn new<H: FnMut(Bar) + 'static>(
509 bar_type: BarType,
510 price_precision: u8,
511 size_precision: u8,
512 handler: H,
513 ) -> Self {
514 Self {
515 core: BarAggregatorCore::new(bar_type, price_precision, size_precision, handler),
516 }
517 }
518}
519
520impl BarAggregator for TickBarAggregator {
521 fn bar_type(&self) -> BarType {
522 self.core.bar_type
523 }
524
525 fn is_running(&self) -> bool {
526 self.core.is_running
527 }
528
529 fn set_is_running(&mut self, value: bool) {
530 self.core.set_is_running(value);
531 }
532
533 impl_set_historical_handler!();
534 impl_set_adjustment!();
535
536 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
538 self.core.apply_update(price, size, ts_init);
539 let spec = self.core.bar_type.spec();
540
541 if self.core.builder.count >= spec.step.get() {
542 self.core.build_now_and_send();
543 }
544 }
545
546 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
547 self.core.builder.update_bar(bar, volume, ts_init);
548 let spec = self.core.bar_type.spec();
549
550 if self.core.builder.count >= spec.step.get() {
551 self.core.build_now_and_send();
552 }
553 }
554}
555
556pub struct TickImbalanceBarAggregator {
561 core: BarAggregatorCore,
562 imbalance: isize,
563}
564
565impl Debug for TickImbalanceBarAggregator {
566 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
567 f.debug_struct(stringify!(TickImbalanceBarAggregator))
568 .field("core", &self.core)
569 .field("imbalance", &self.imbalance)
570 .finish()
571 }
572}
573
574impl TickImbalanceBarAggregator {
575 pub fn new<H: FnMut(Bar) + 'static>(
581 bar_type: BarType,
582 price_precision: u8,
583 size_precision: u8,
584 handler: H,
585 ) -> Self {
586 Self {
587 core: BarAggregatorCore::new(bar_type, price_precision, size_precision, handler),
588 imbalance: 0,
589 }
590 }
591}
592
593impl BarAggregator for TickImbalanceBarAggregator {
594 fn bar_type(&self) -> BarType {
595 self.core.bar_type
596 }
597
598 fn is_running(&self) -> bool {
599 self.core.is_running
600 }
601
602 fn set_is_running(&mut self, value: bool) {
603 self.core.set_is_running(value);
604 }
605
606 impl_set_historical_handler!();
607 impl_set_adjustment!();
608
609 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
614 self.core.apply_update(price, size, ts_init);
615 }
616
617 fn handle_trade(&mut self, trade: TradeTick) {
618 if self.core.is_stale(trade.ts_init) {
619 return;
620 }
621
622 self.core
623 .apply_update(trade.price, trade.size, trade.ts_init);
624
625 let delta = match trade.aggressor_side {
626 AggressorSide::Buyer => 1,
627 AggressorSide::Seller => -1,
628 AggressorSide::NoAggressor => 0,
629 };
630
631 if delta == 0 {
632 return;
633 }
634
635 self.imbalance += delta;
636 let threshold = self.core.bar_type.spec().step.get();
637 if self.imbalance.unsigned_abs() >= threshold {
638 self.core.build_now_and_send();
639 self.imbalance = 0;
640 }
641 }
642
643 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
644 self.core.builder.update_bar(bar, volume, ts_init);
645 }
646}
647
648pub struct TickRunsBarAggregator {
650 core: BarAggregatorCore,
651 current_run_side: Option<AggressorSide>,
652 run_count: usize,
653}
654
655impl Debug for TickRunsBarAggregator {
656 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
657 f.debug_struct(stringify!(TickRunsBarAggregator))
658 .field("core", &self.core)
659 .field("current_run_side", &self.current_run_side)
660 .field("run_count", &self.run_count)
661 .finish()
662 }
663}
664
665impl TickRunsBarAggregator {
666 pub fn new<H: FnMut(Bar) + 'static>(
672 bar_type: BarType,
673 price_precision: u8,
674 size_precision: u8,
675 handler: H,
676 ) -> Self {
677 Self {
678 core: BarAggregatorCore::new(bar_type, price_precision, size_precision, handler),
679 current_run_side: None,
680 run_count: 0,
681 }
682 }
683}
684
685impl BarAggregator for TickRunsBarAggregator {
686 fn bar_type(&self) -> BarType {
687 self.core.bar_type
688 }
689
690 fn is_running(&self) -> bool {
691 self.core.is_running
692 }
693
694 fn set_is_running(&mut self, value: bool) {
695 self.core.set_is_running(value);
696 }
697
698 impl_set_historical_handler!();
699 impl_set_adjustment!();
700
701 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
706 self.core.apply_update(price, size, ts_init);
707 }
708
709 fn handle_trade(&mut self, trade: TradeTick) {
710 if self.core.is_stale(trade.ts_init) {
711 return;
712 }
713
714 let side = match trade.aggressor_side {
715 AggressorSide::Buyer => Some(AggressorSide::Buyer),
716 AggressorSide::Seller => Some(AggressorSide::Seller),
717 AggressorSide::NoAggressor => None,
718 };
719
720 if let Some(side) = side {
721 if self.current_run_side != Some(side) {
722 self.current_run_side = Some(side);
723 self.run_count = 0;
724 self.core.builder.reset();
725 }
726
727 self.core
728 .apply_update(trade.price, trade.size, trade.ts_init);
729 self.run_count += 1;
730
731 let threshold = self.core.bar_type.spec().step.get();
732 if self.run_count >= threshold {
733 self.core.build_now_and_send();
734 self.run_count = 0;
735 self.current_run_side = None;
736 }
737 } else {
738 self.core
739 .apply_update(trade.price, trade.size, trade.ts_init);
740 }
741 }
742
743 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
744 self.core.builder.update_bar(bar, volume, ts_init);
745 }
746}
747
748pub struct VolumeBarAggregator {
750 core: BarAggregatorCore,
751 raw_step: QuantityRaw,
752}
753
754impl Debug for VolumeBarAggregator {
755 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
756 f.debug_struct(stringify!(VolumeBarAggregator))
757 .field("core", &self.core)
758 .field("raw_step", &self.raw_step)
759 .finish()
760 }
761}
762
763impl VolumeBarAggregator {
764 pub fn new<H: FnMut(Bar) + 'static>(
770 bar_type: BarType,
771 price_precision: u8,
772 size_precision: u8,
773 handler: H,
774 ) -> Self {
775 Self {
776 core: BarAggregatorCore::new(bar_type, price_precision, size_precision, handler),
777 raw_step: step_as_quantity_raw(bar_type.spec().step.get()),
778 }
779 }
780}
781
782impl BarAggregator for VolumeBarAggregator {
783 fn bar_type(&self) -> BarType {
784 self.core.bar_type
785 }
786
787 fn is_running(&self) -> bool {
788 self.core.is_running
789 }
790
791 fn set_is_running(&mut self, value: bool) {
792 self.core.set_is_running(value);
793 }
794
795 impl_set_historical_handler!();
796 impl_set_adjustment!();
797
798 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
800 if self.core.is_stale(ts_init) {
801 return;
802 }
803
804 let mut raw_size_update = size.raw;
805 let raw_step = self.raw_step;
806
807 while raw_size_update > 0 {
808 debug_assert!(
809 self.core.builder.volume.raw < raw_step,
810 "builder volume must stay below the step threshold between emissions"
811 );
812
813 if self.core.builder.volume.raw + raw_size_update < raw_step {
814 self.core.apply_update(
815 price,
816 Quantity::from_raw(raw_size_update, size.precision),
817 ts_init,
818 );
819 break;
820 }
821
822 let raw_size_diff = raw_step - self.core.builder.volume.raw;
823 self.core.apply_update(
824 price,
825 Quantity::from_raw(raw_size_diff, size.precision),
826 ts_init,
827 );
828
829 self.core.build_now_and_send();
830 raw_size_update -= raw_size_diff;
831 }
832 }
833
834 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
835 if self.core.is_stale(ts_init) {
836 return;
837 }
838
839 let mut raw_volume_update = volume.raw;
840 let raw_step = self.raw_step;
841
842 while raw_volume_update > 0 {
843 debug_assert!(
844 self.core.builder.volume.raw < raw_step,
845 "builder volume must stay below the step threshold between emissions"
846 );
847
848 if self.core.builder.volume.raw + raw_volume_update < raw_step {
849 self.core.builder.update_bar(
850 bar,
851 Quantity::from_raw(raw_volume_update, volume.precision),
852 ts_init,
853 );
854 break;
855 }
856
857 let raw_volume_diff = raw_step - self.core.builder.volume.raw;
858 self.core.builder.update_bar(
859 bar,
860 Quantity::from_raw(raw_volume_diff, volume.precision),
861 ts_init,
862 );
863
864 self.core.build_now_and_send();
865 raw_volume_update -= raw_volume_diff;
866 }
867 }
868}
869
870pub struct VolumeImbalanceBarAggregator {
872 core: BarAggregatorCore,
873 imbalance_raw: i128,
874 raw_step: i128,
875}
876
877impl Debug for VolumeImbalanceBarAggregator {
878 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
879 f.debug_struct(stringify!(VolumeImbalanceBarAggregator))
880 .field("core", &self.core)
881 .field("imbalance_raw", &self.imbalance_raw)
882 .field("raw_step", &self.raw_step)
883 .finish()
884 }
885}
886
887impl VolumeImbalanceBarAggregator {
888 pub fn new<H: FnMut(Bar) + 'static>(
894 bar_type: BarType,
895 price_precision: u8,
896 size_precision: u8,
897 handler: H,
898 ) -> Self {
899 let raw_step = step_as_quantity_raw(bar_type.spec().step.get()) as i128;
901 Self {
902 core: BarAggregatorCore::new(bar_type, price_precision, size_precision, handler),
903 imbalance_raw: 0,
904 raw_step,
905 }
906 }
907}
908
909impl BarAggregator for VolumeImbalanceBarAggregator {
910 fn bar_type(&self) -> BarType {
911 self.core.bar_type
912 }
913
914 fn is_running(&self) -> bool {
915 self.core.is_running
916 }
917
918 fn set_is_running(&mut self, value: bool) {
919 self.core.set_is_running(value);
920 }
921
922 impl_set_historical_handler!();
923 impl_set_adjustment!();
924
925 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
930 self.core.apply_update(price, size, ts_init);
931 }
932
933 fn handle_trade(&mut self, trade: TradeTick) {
934 if self.core.is_stale(trade.ts_init) {
935 return;
936 }
937
938 let side = match trade.aggressor_side {
939 AggressorSide::Buyer => 1,
940 AggressorSide::Seller => -1,
941 AggressorSide::NoAggressor => {
942 self.core
943 .apply_update(trade.price, trade.size, trade.ts_init);
944 return;
945 }
946 };
947
948 let mut raw_remaining = trade.size.raw as i128;
949 while raw_remaining > 0 {
950 let imbalance_abs = self.imbalance_raw.abs();
951 let needed = (self.raw_step - imbalance_abs).max(1);
952 let raw_chunk = raw_remaining.min(needed);
953 let qty_chunk = Quantity::from_raw(raw_chunk as QuantityRaw, trade.size.precision);
954
955 self.core
956 .apply_update(trade.price, qty_chunk, trade.ts_init);
957
958 self.imbalance_raw += side * raw_chunk;
959 raw_remaining -= raw_chunk;
960
961 if self.imbalance_raw.abs() >= self.raw_step {
962 self.core.build_now_and_send();
963 self.imbalance_raw = 0;
964 }
965 }
966 }
967
968 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
969 self.core.builder.update_bar(bar, volume, ts_init);
970 }
971}
972
973pub struct VolumeRunsBarAggregator {
975 core: BarAggregatorCore,
976 current_run_side: Option<AggressorSide>,
977 run_volume_raw: QuantityRaw,
978 raw_step: QuantityRaw,
979}
980
981impl Debug for VolumeRunsBarAggregator {
982 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
983 f.debug_struct(stringify!(VolumeRunsBarAggregator))
984 .field("core", &self.core)
985 .field("current_run_side", &self.current_run_side)
986 .field("run_volume_raw", &self.run_volume_raw)
987 .field("raw_step", &self.raw_step)
988 .finish()
989 }
990}
991
992impl VolumeRunsBarAggregator {
993 pub fn new<H: FnMut(Bar) + 'static>(
999 bar_type: BarType,
1000 price_precision: u8,
1001 size_precision: u8,
1002 handler: H,
1003 ) -> Self {
1004 let raw_step = step_as_quantity_raw(bar_type.spec().step.get());
1005 Self {
1006 core: BarAggregatorCore::new(bar_type, price_precision, size_precision, handler),
1007 current_run_side: None,
1008 run_volume_raw: 0,
1009 raw_step,
1010 }
1011 }
1012}
1013
1014impl BarAggregator for VolumeRunsBarAggregator {
1015 fn bar_type(&self) -> BarType {
1016 self.core.bar_type
1017 }
1018
1019 fn is_running(&self) -> bool {
1020 self.core.is_running
1021 }
1022
1023 fn set_is_running(&mut self, value: bool) {
1024 self.core.set_is_running(value);
1025 }
1026
1027 impl_set_historical_handler!();
1028 impl_set_adjustment!();
1029
1030 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
1035 self.core.apply_update(price, size, ts_init);
1036 }
1037
1038 fn handle_trade(&mut self, trade: TradeTick) {
1039 if self.core.is_stale(trade.ts_init) {
1040 return;
1041 }
1042
1043 let side = match trade.aggressor_side {
1044 AggressorSide::Buyer => Some(AggressorSide::Buyer),
1045 AggressorSide::Seller => Some(AggressorSide::Seller),
1046 AggressorSide::NoAggressor => None,
1047 };
1048
1049 let Some(side) = side else {
1050 self.core
1051 .apply_update(trade.price, trade.size, trade.ts_init);
1052 return;
1053 };
1054
1055 if self.current_run_side != Some(side) {
1056 self.current_run_side = Some(side);
1057 self.run_volume_raw = 0;
1058 self.core.builder.reset();
1059 }
1060
1061 let mut raw_remaining = trade.size.raw;
1062 while raw_remaining > 0 {
1063 let needed = self.raw_step.saturating_sub(self.run_volume_raw).max(1);
1064 let raw_chunk = raw_remaining.min(needed);
1065
1066 self.core.apply_update(
1067 trade.price,
1068 Quantity::from_raw(raw_chunk, trade.size.precision),
1069 trade.ts_init,
1070 );
1071
1072 self.run_volume_raw += raw_chunk;
1073 raw_remaining -= raw_chunk;
1074
1075 if self.run_volume_raw >= self.raw_step {
1076 self.core.build_now_and_send();
1077 self.run_volume_raw = 0;
1078 self.current_run_side = None;
1079 }
1080 }
1081
1082 if self.run_volume_raw > 0 {
1086 self.current_run_side = Some(side);
1087 }
1088 }
1089
1090 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
1091 self.core.builder.update_bar(bar, volume, ts_init);
1092 }
1093}
1094
1095pub struct ValueBarAggregator {
1100 core: BarAggregatorCore,
1101 cum_value: Decimal,
1102}
1103
1104impl Debug for ValueBarAggregator {
1105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1106 f.debug_struct(stringify!(ValueBarAggregator))
1107 .field("core", &self.core)
1108 .field("cum_value", &self.cum_value)
1109 .finish()
1110 }
1111}
1112
1113impl ValueBarAggregator {
1114 pub fn new<H: FnMut(Bar) + 'static>(
1120 bar_type: BarType,
1121 price_precision: u8,
1122 size_precision: u8,
1123 handler: H,
1124 ) -> Self {
1125 Self {
1126 core: BarAggregatorCore::new(bar_type, price_precision, size_precision, handler),
1127 cum_value: Decimal::ZERO,
1128 }
1129 }
1130
1131 #[must_use]
1132 pub const fn get_cumulative_value(&self) -> Decimal {
1134 self.cum_value
1135 }
1136}
1137
1138impl BarAggregator for ValueBarAggregator {
1139 fn bar_type(&self) -> BarType {
1140 self.core.bar_type
1141 }
1142
1143 fn is_running(&self) -> bool {
1144 self.core.is_running
1145 }
1146
1147 fn set_is_running(&mut self, value: bool) {
1148 self.core.set_is_running(value);
1149 }
1150
1151 impl_set_historical_handler!();
1152 impl_set_adjustment!();
1153
1154 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
1156 if self.core.is_stale(ts_init) {
1157 return;
1158 }
1159
1160 let step_value = Decimal::from(self.core.bar_type.spec().step.get());
1161 let price_value = price.as_decimal();
1162 let mut size_update = size.as_decimal();
1163
1164 while size_update > Decimal::ZERO {
1165 debug_assert!(self.cum_value < step_value);
1169 let value_update = price_value * size_update;
1170
1171 if self.cum_value + value_update < step_value {
1172 self.cum_value += value_update;
1173 self.core.apply_update(
1174 price,
1175 quantity_from_decimal(size_update, size.precision),
1176 ts_init,
1177 );
1178 break;
1179 }
1180
1181 let value_diff = step_value - self.cum_value;
1182 let mut size_diff = size_update * (value_diff / value_update);
1183
1184 if is_below_min_size_decimal(size_diff, size.precision) {
1186 if is_below_min_size_decimal(size_update, size.precision) {
1187 break;
1188 }
1189 size_diff = min_size_decimal(size.precision);
1190 }
1191
1192 let applied = quantity_from_decimal(size_diff, size.precision);
1195 self.core.apply_update(price, applied, ts_init);
1196
1197 self.core.build_now_and_send();
1198 self.cum_value = Decimal::ZERO;
1199 size_update -= applied.as_decimal();
1200 }
1201 }
1202
1203 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
1204 if self.core.is_stale(ts_init) {
1205 return;
1206 }
1207
1208 let step_value = Decimal::from(self.core.bar_type.spec().step.get());
1209 let average_price =
1210 ((bar.high.as_decimal() + bar.low.as_decimal() + bar.close.as_decimal())
1211 / Decimal::from(3))
1212 .round_dp(u32::from(self.core.builder.price_precision));
1213 let mut volume_update = volume.as_decimal();
1214
1215 while volume_update > Decimal::ZERO {
1216 debug_assert!(self.cum_value < step_value);
1218 let value_update = average_price * volume_update;
1219
1220 if self.cum_value + value_update < step_value {
1221 self.cum_value += value_update;
1222 self.core.builder.update_bar(
1223 bar,
1224 quantity_from_decimal(volume_update, volume.precision),
1225 ts_init,
1226 );
1227 break;
1228 }
1229
1230 let value_diff = step_value - self.cum_value;
1231 let mut volume_diff = volume_update * (value_diff / value_update);
1232
1233 if is_below_min_size_decimal(volume_diff, volume.precision) {
1235 if is_below_min_size_decimal(volume_update, volume.precision) {
1236 break;
1237 }
1238 volume_diff = min_size_decimal(volume.precision);
1239 }
1240
1241 let applied = quantity_from_decimal(volume_diff, volume.precision);
1244 self.core.builder.update_bar(bar, applied, ts_init);
1245
1246 self.core.build_now_and_send();
1247 self.cum_value = Decimal::ZERO;
1248 volume_update -= applied.as_decimal();
1249 }
1250 }
1251}
1252
1253pub struct ValueImbalanceBarAggregator {
1255 core: BarAggregatorCore,
1256 imbalance_value: Decimal,
1257 step_value: Decimal,
1258}
1259
1260impl Debug for ValueImbalanceBarAggregator {
1261 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1262 f.debug_struct(stringify!(ValueImbalanceBarAggregator))
1263 .field("core", &self.core)
1264 .field("imbalance_value", &self.imbalance_value)
1265 .field("step_value", &self.step_value)
1266 .finish()
1267 }
1268}
1269
1270impl ValueImbalanceBarAggregator {
1271 pub fn new<H: FnMut(Bar) + 'static>(
1277 bar_type: BarType,
1278 price_precision: u8,
1279 size_precision: u8,
1280 handler: H,
1281 ) -> Self {
1282 Self {
1283 core: BarAggregatorCore::new(bar_type, price_precision, size_precision, handler),
1284 imbalance_value: Decimal::ZERO,
1285 step_value: Decimal::from(bar_type.spec().step.get()),
1286 }
1287 }
1288}
1289
1290impl BarAggregator for ValueImbalanceBarAggregator {
1291 fn bar_type(&self) -> BarType {
1292 self.core.bar_type
1293 }
1294
1295 fn is_running(&self) -> bool {
1296 self.core.is_running
1297 }
1298
1299 fn set_is_running(&mut self, value: bool) {
1300 self.core.set_is_running(value);
1301 }
1302
1303 impl_set_historical_handler!();
1304 impl_set_adjustment!();
1305
1306 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
1311 self.core.apply_update(price, size, ts_init);
1312 }
1313
1314 fn handle_trade(&mut self, trade: TradeTick) {
1315 if self.core.is_stale(trade.ts_init) {
1316 return;
1317 }
1318
1319 let price_value = trade.price.as_decimal();
1320 if price_value.is_zero() {
1321 self.core
1322 .apply_update(trade.price, trade.size, trade.ts_init);
1323 return;
1324 }
1325
1326 let (side_sign, side_is_buy) = match trade.aggressor_side {
1327 AggressorSide::Buyer => (Decimal::ONE, true),
1328 AggressorSide::Seller => (Decimal::NEGATIVE_ONE, false),
1329 AggressorSide::NoAggressor => {
1330 self.core
1331 .apply_update(trade.price, trade.size, trade.ts_init);
1332 return;
1333 }
1334 };
1335
1336 let precision = trade.size.precision;
1337 let mut size_remaining = trade.size.as_decimal();
1338 while size_remaining > Decimal::ZERO {
1339 let value_remaining = price_value * size_remaining;
1340
1341 if self.imbalance_value.is_zero()
1342 || self.imbalance_value.is_sign_positive() == side_is_buy
1343 {
1344 let needed = self.step_value - self.imbalance_value.abs();
1345 if value_remaining <= needed {
1346 self.imbalance_value += side_sign * value_remaining;
1347 self.core.apply_update(
1348 trade.price,
1349 quantity_from_decimal(size_remaining, precision),
1350 trade.ts_init,
1351 );
1352
1353 if self.imbalance_value.abs() >= self.step_value {
1354 self.core.build_now_and_send();
1355 self.imbalance_value = Decimal::ZERO;
1356 }
1357 break;
1358 }
1359
1360 let mut value_chunk = needed;
1361 let mut size_chunk = value_chunk / price_value;
1362
1363 if is_below_min_size_decimal(size_chunk, precision) {
1365 if is_below_min_size_decimal(size_remaining, precision) {
1366 break;
1367 }
1368 size_chunk = min_size_decimal(precision);
1369 value_chunk = price_value * size_chunk;
1370 }
1371
1372 let applied = quantity_from_decimal(size_chunk, precision);
1375 self.core.apply_update(trade.price, applied, trade.ts_init);
1376 self.imbalance_value += side_sign * value_chunk;
1377 size_remaining -= applied.as_decimal();
1378
1379 if self.imbalance_value.abs() >= self.step_value {
1380 self.core.build_now_and_send();
1381 self.imbalance_value = Decimal::ZERO;
1382 }
1383 } else {
1384 let mut value_to_flatten = self.imbalance_value.abs().min(value_remaining);
1386 let mut size_chunk = value_to_flatten / price_value;
1387
1388 if is_below_min_size_decimal(size_chunk, precision) {
1390 if is_below_min_size_decimal(size_remaining, precision) {
1391 break;
1392 }
1393 size_chunk = min_size_decimal(precision);
1394 value_to_flatten = price_value * size_chunk;
1395 }
1396
1397 let applied = quantity_from_decimal(size_chunk, precision);
1400 self.core.apply_update(trade.price, applied, trade.ts_init);
1401 self.imbalance_value += side_sign * value_to_flatten;
1402
1403 if self.imbalance_value.abs() >= self.step_value {
1405 self.core.build_now_and_send();
1406 self.imbalance_value = Decimal::ZERO;
1407 }
1408 size_remaining -= applied.as_decimal();
1409 }
1410 }
1411 }
1412
1413 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
1414 self.core.builder.update_bar(bar, volume, ts_init);
1415 }
1416}
1417
1418pub struct ValueRunsBarAggregator {
1420 core: BarAggregatorCore,
1421 current_run_side: Option<AggressorSide>,
1422 run_value: Decimal,
1423 step_value: Decimal,
1424}
1425
1426impl Debug for ValueRunsBarAggregator {
1427 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1428 f.debug_struct(stringify!(ValueRunsBarAggregator))
1429 .field("core", &self.core)
1430 .field("current_run_side", &self.current_run_side)
1431 .field("run_value", &self.run_value)
1432 .field("step_value", &self.step_value)
1433 .finish()
1434 }
1435}
1436
1437impl ValueRunsBarAggregator {
1438 pub fn new<H: FnMut(Bar) + 'static>(
1444 bar_type: BarType,
1445 price_precision: u8,
1446 size_precision: u8,
1447 handler: H,
1448 ) -> Self {
1449 Self {
1450 core: BarAggregatorCore::new(bar_type, price_precision, size_precision, handler),
1451 current_run_side: None,
1452 run_value: Decimal::ZERO,
1453 step_value: Decimal::from(bar_type.spec().step.get()),
1454 }
1455 }
1456}
1457
1458impl BarAggregator for ValueRunsBarAggregator {
1459 fn bar_type(&self) -> BarType {
1460 self.core.bar_type
1461 }
1462
1463 fn is_running(&self) -> bool {
1464 self.core.is_running
1465 }
1466
1467 fn set_is_running(&mut self, value: bool) {
1468 self.core.set_is_running(value);
1469 }
1470
1471 impl_set_historical_handler!();
1472 impl_set_adjustment!();
1473
1474 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
1479 self.core.apply_update(price, size, ts_init);
1480 }
1481
1482 fn handle_trade(&mut self, trade: TradeTick) {
1483 if self.core.is_stale(trade.ts_init) {
1484 return;
1485 }
1486
1487 let price_value = trade.price.as_decimal();
1488 if price_value.is_zero() {
1489 self.core
1490 .apply_update(trade.price, trade.size, trade.ts_init);
1491 return;
1492 }
1493
1494 let side = match trade.aggressor_side {
1495 AggressorSide::Buyer => Some(AggressorSide::Buyer),
1496 AggressorSide::Seller => Some(AggressorSide::Seller),
1497 AggressorSide::NoAggressor => None,
1498 };
1499
1500 let Some(side) = side else {
1501 self.core
1502 .apply_update(trade.price, trade.size, trade.ts_init);
1503 return;
1504 };
1505
1506 if self.current_run_side != Some(side) {
1507 self.current_run_side = Some(side);
1508 self.run_value = Decimal::ZERO;
1509 self.core.builder.reset();
1510 }
1511
1512 let precision = trade.size.precision;
1513 let mut size_remaining = trade.size.as_decimal();
1514 while size_remaining > Decimal::ZERO {
1515 let value_update = price_value * size_remaining;
1516 if self.run_value + value_update < self.step_value {
1517 self.run_value += value_update;
1518 self.core.apply_update(
1519 trade.price,
1520 quantity_from_decimal(size_remaining, precision),
1521 trade.ts_init,
1522 );
1523 break;
1524 }
1525
1526 let value_needed = self.step_value - self.run_value;
1527 let mut size_chunk = value_needed / price_value;
1528
1529 if is_below_min_size_decimal(size_chunk, precision) {
1531 if is_below_min_size_decimal(size_remaining, precision) {
1532 break;
1533 }
1534 size_chunk = min_size_decimal(precision);
1535 }
1536
1537 let applied = quantity_from_decimal(size_chunk, precision);
1540 self.core.apply_update(trade.price, applied, trade.ts_init);
1541
1542 self.core.build_now_and_send();
1543 self.run_value = Decimal::ZERO;
1544 self.current_run_side = None;
1545 size_remaining -= applied.as_decimal();
1546 }
1547
1548 if self.run_value > Decimal::ZERO {
1552 self.current_run_side = Some(side);
1553 }
1554 }
1555
1556 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
1557 self.core.builder.update_bar(bar, volume, ts_init);
1558 }
1559}
1560
1561pub struct RenkoBarAggregator {
1567 core: BarAggregatorCore,
1568 pub brick_size: PriceRaw,
1569 last_close: Option<Price>,
1570}
1571
1572impl Debug for RenkoBarAggregator {
1573 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1574 f.debug_struct(stringify!(RenkoBarAggregator))
1575 .field("core", &self.core)
1576 .field("brick_size", &self.brick_size)
1577 .field("last_close", &self.last_close)
1578 .finish()
1579 }
1580}
1581
1582impl RenkoBarAggregator {
1583 pub fn new<H: FnMut(Bar) + 'static>(
1589 bar_type: BarType,
1590 price_precision: u8,
1591 size_precision: u8,
1592 price_increment: Price,
1593 handler: H,
1594 ) -> Self {
1595 let brick_size = bar_type.spec().step.get() as PriceRaw * price_increment.raw;
1597
1598 Self {
1599 core: BarAggregatorCore::new(bar_type, price_precision, size_precision, handler),
1600 brick_size,
1601 last_close: None,
1602 }
1603 }
1604}
1605
1606impl BarAggregator for RenkoBarAggregator {
1607 fn bar_type(&self) -> BarType {
1608 self.core.bar_type
1609 }
1610
1611 fn is_running(&self) -> bool {
1612 self.core.is_running
1613 }
1614
1615 fn set_is_running(&mut self, value: bool) {
1616 self.core.set_is_running(value);
1617 }
1618
1619 impl_set_historical_handler!();
1620 impl_set_adjustment!();
1621
1622 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
1627 if self.core.is_stale(ts_init) {
1628 return;
1629 }
1630
1631 self.core.apply_update(price, size, ts_init);
1633
1634 if self.last_close.is_none() {
1636 self.last_close = Some(price);
1637 return;
1638 }
1639
1640 let last_close = self.last_close.unwrap();
1641
1642 let current_raw = price.raw;
1644 let last_close_raw = last_close.raw;
1645 let price_diff_raw = current_raw - last_close_raw;
1646 let abs_price_diff_raw = price_diff_raw.abs();
1647
1648 if abs_price_diff_raw >= self.brick_size {
1650 let num_bricks = (abs_price_diff_raw / self.brick_size) as usize;
1651 let direction = if price_diff_raw > 0 { 1.0 } else { -1.0 };
1652 let mut current_close = last_close;
1653
1654 let total_volume = self.core.builder.volume;
1656
1657 for _i in 0..num_bricks {
1658 let brick_close_raw = current_close.raw + (direction as PriceRaw) * self.brick_size;
1660 let brick_close = Price::from_raw(brick_close_raw, price.precision);
1661
1662 let (brick_high, brick_low) = if direction > 0.0 {
1664 (brick_close, current_close)
1665 } else {
1666 (current_close, brick_close)
1667 };
1668
1669 self.core.builder.reset();
1671 self.core.builder.open = Some(current_close);
1672 self.core.builder.high = Some(brick_high);
1673 self.core.builder.low = Some(brick_low);
1674 self.core.builder.close = Some(brick_close);
1675 self.core.builder.volume = total_volume; self.core.builder.count = 1;
1677 self.core.builder.ts_last = ts_init;
1678 self.core.builder.initialized = true;
1679
1680 self.core.build_and_send(ts_init, ts_init);
1682
1683 current_close = brick_close;
1685 self.last_close = Some(brick_close);
1686 }
1687 }
1688 }
1689
1690 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
1691 if self.core.is_stale(ts_init) {
1692 return;
1693 }
1694
1695 self.core.builder.update_bar(bar, volume, ts_init);
1697
1698 if self.last_close.is_none() {
1700 self.last_close = Some(bar.close);
1701 return;
1702 }
1703
1704 let last_close = self.last_close.unwrap();
1705
1706 let current_raw = bar.close.raw;
1708 let last_close_raw = last_close.raw;
1709 let price_diff_raw = current_raw - last_close_raw;
1710 let abs_price_diff_raw = price_diff_raw.abs();
1711
1712 if abs_price_diff_raw >= self.brick_size {
1714 let num_bricks = (abs_price_diff_raw / self.brick_size) as usize;
1715 let direction = if price_diff_raw > 0 { 1.0 } else { -1.0 };
1716 let mut current_close = last_close;
1717
1718 let total_volume = self.core.builder.volume;
1720
1721 for _i in 0..num_bricks {
1722 let brick_close_raw = current_close.raw + (direction as PriceRaw) * self.brick_size;
1724 let brick_close = Price::from_raw(brick_close_raw, bar.close.precision);
1725
1726 let (brick_high, brick_low) = if direction > 0.0 {
1728 (brick_close, current_close)
1729 } else {
1730 (current_close, brick_close)
1731 };
1732
1733 self.core.builder.reset();
1735 self.core.builder.open = Some(current_close);
1736 self.core.builder.high = Some(brick_high);
1737 self.core.builder.low = Some(brick_low);
1738 self.core.builder.close = Some(brick_close);
1739 self.core.builder.volume = total_volume; self.core.builder.count = 1;
1741 self.core.builder.ts_last = ts_init;
1742 self.core.builder.initialized = true;
1743
1744 self.core.build_and_send(ts_init, ts_init);
1746
1747 current_close = brick_close;
1749 self.last_close = Some(brick_close);
1750 }
1751 }
1752 }
1753}
1754
1755pub struct TimeBarAggregator {
1759 core: BarAggregatorCore,
1760 clock: Rc<RefCell<dyn Clock>>,
1761 build_with_no_updates: bool,
1762 timestamp_on_close: bool,
1763 is_left_open: bool,
1764 stored_open_ns: UnixNanos,
1765 timer_name: String,
1766 interval_ns: UnixNanos,
1767 next_close_ns: UnixNanos,
1768 first_close_ns: UnixNanos,
1769 bar_build_delay: u64,
1770 time_bars_origin_offset: Option<TimeDelta>,
1771 skip_first_non_full_bar: bool,
1772 pub historical_mode: bool,
1773 historical_events: Vec<TimeEvent>,
1774 historical_event_at_ts_init: Option<TimeEvent>,
1775 aggregator_weak: Option<Weak<RefCell<Box<dyn BarAggregator>>>>,
1776}
1777
1778impl Debug for TimeBarAggregator {
1779 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1780 f.debug_struct(stringify!(TimeBarAggregator))
1781 .field("core", &self.core)
1782 .field("build_with_no_updates", &self.build_with_no_updates)
1783 .field("timestamp_on_close", &self.timestamp_on_close)
1784 .field("is_left_open", &self.is_left_open)
1785 .field("timer_name", &self.timer_name)
1786 .field("interval_ns", &self.interval_ns)
1787 .field("bar_build_delay", &self.bar_build_delay)
1788 .field("skip_first_non_full_bar", &self.skip_first_non_full_bar)
1789 .finish()
1790 }
1791}
1792
1793impl TimeBarAggregator {
1794 #[expect(clippy::too_many_arguments)]
1800 pub fn new<H: FnMut(Bar) + 'static>(
1801 bar_type: BarType,
1802 price_precision: u8,
1803 size_precision: u8,
1804 clock: Rc<RefCell<dyn Clock>>,
1805 handler: H,
1806 build_with_no_updates: bool,
1807 timestamp_on_close: bool,
1808 interval_type: BarIntervalType,
1809 time_bars_origin_offset: Option<TimeDelta>,
1810 bar_build_delay: u64,
1811 skip_first_non_full_bar: bool,
1812 ) -> Self {
1813 let is_left_open = match interval_type {
1814 BarIntervalType::LeftOpen => true,
1815 BarIntervalType::RightOpen => false,
1816 };
1817
1818 let core = BarAggregatorCore::new(bar_type, price_precision, size_precision, handler);
1819
1820 Self {
1821 clock,
1822 build_with_no_updates,
1823 timestamp_on_close,
1824 is_left_open,
1825 stored_open_ns: UnixNanos::default(),
1826 timer_name: format!("TIME_BAR_{}", core.bar_type),
1827 interval_ns: get_bar_interval_ns(&bar_type),
1828 core,
1829 next_close_ns: UnixNanos::default(),
1830 first_close_ns: UnixNanos::default(),
1831 bar_build_delay,
1832 time_bars_origin_offset,
1833 skip_first_non_full_bar,
1834 historical_mode: false,
1835 historical_events: Vec::new(),
1836 historical_event_at_ts_init: None,
1837 aggregator_weak: None,
1838 }
1839 }
1840
1841 pub fn set_clock_internal(&mut self, clock: Rc<RefCell<dyn Clock>>) {
1843 self.clock = clock;
1844 }
1845
1846 pub fn start_timer_internal(
1855 &mut self,
1856 aggregator_rc: Option<Rc<RefCell<Box<dyn BarAggregator>>>>,
1857 ) {
1858 let aggregator_weak = if let Some(rc) = aggregator_rc {
1860 let weak = Rc::downgrade(&rc);
1862 self.aggregator_weak = Some(weak.clone());
1863 weak
1864 } else {
1865 self.aggregator_weak
1867 .as_ref()
1868 .expect("Aggregator weak reference must be set before calling start_timer()")
1869 .clone()
1870 };
1871
1872 let callback = TimeEventCallback::RustLocal(Rc::new(move |event: TimeEvent| {
1873 if let Some(agg) = aggregator_weak.upgrade() {
1874 agg.borrow_mut().build_bar(&event);
1875 }
1876 }));
1877
1878 let now = self.clock.borrow().utc_now();
1880 let mut start_time =
1881 get_time_bar_start(now, &self.bar_type(), self.time_bars_origin_offset);
1882 start_time += TimeDelta::microseconds(self.bar_build_delay as i64);
1883
1884 let fire_immediately = start_time == now;
1886
1887 let spec = &self.bar_type().spec();
1888 let start_time_ns = UnixNanos::from(start_time);
1889 let step = spec.step.get() as u32;
1890
1891 if spec.aggregation != BarAggregation::Month && spec.aggregation != BarAggregation::Year {
1892 self.clock
1893 .borrow_mut()
1894 .set_timer_ns(
1895 &self.timer_name,
1896 self.interval_ns.as_u64(),
1897 Some(start_time_ns),
1898 None,
1899 Some(callback),
1900 Some(true), Some(fire_immediately),
1902 )
1903 .expect(FAILED);
1904
1905 if fire_immediately {
1906 self.next_close_ns = start_time_ns;
1907 } else {
1908 let interval_duration = Duration::nanoseconds(self.interval_ns.as_i64());
1909 self.next_close_ns = UnixNanos::from(start_time + interval_duration);
1910 }
1911
1912 self.stored_open_ns = self.next_close_ns.saturating_sub_ns(self.interval_ns);
1913 } else {
1914 let alert_time = if fire_immediately {
1916 start_time
1917 } else if spec.aggregation == BarAggregation::Month {
1918 add_n_months(start_time, step).expect(FAILED)
1919 } else {
1920 add_n_years(start_time, step).expect(FAILED)
1921 };
1922
1923 self.clock
1924 .borrow_mut()
1925 .set_time_alert_ns(
1926 &self.timer_name,
1927 UnixNanos::from(alert_time),
1928 Some(callback),
1929 Some(true), )
1931 .expect(FAILED);
1932
1933 self.next_close_ns = UnixNanos::from(alert_time);
1934 self.stored_open_ns = if fire_immediately {
1937 if spec.aggregation == BarAggregation::Month {
1938 subtract_n_months_nanos(start_time_ns, step).expect(FAILED)
1939 } else {
1940 subtract_n_years_nanos(start_time_ns, step).expect(FAILED)
1941 }
1942 } else {
1943 start_time_ns
1944 };
1945 }
1946
1947 if self.skip_first_non_full_bar {
1948 self.first_close_ns = self.next_close_ns;
1949 }
1950
1951 log::debug!(
1952 "Started timer {}, start_time={:?}, historical_mode={}, fire_immediately={}, now={:?}, bar_build_delay={}",
1953 self.timer_name,
1954 start_time,
1955 self.historical_mode,
1956 fire_immediately,
1957 now,
1958 self.bar_build_delay
1959 );
1960 }
1961
1962 pub fn stop(&mut self) {
1964 self.clock.borrow_mut().cancel_timer(&self.timer_name);
1965 }
1966
1967 fn build_and_send(&mut self, ts_event: UnixNanos, ts_init: UnixNanos) {
1968 if self.skip_first_non_full_bar && ts_init <= self.first_close_ns {
1969 self.core.builder.reset();
1970 } else {
1971 self.skip_first_non_full_bar = false;
1974 self.core.build_and_send(ts_event, ts_init);
1975 }
1976 }
1977
1978 fn build_bar(&mut self, event: &TimeEvent) {
1979 if !self.core.builder.initialized {
1980 return;
1981 }
1982
1983 if !self.build_with_no_updates && self.core.builder.count == 0 {
1984 return; }
1986
1987 let ts_init = event.ts_event;
1988 let ts_event = if self.is_left_open {
1989 if self.timestamp_on_close {
1990 event.ts_event
1991 } else {
1992 self.stored_open_ns
1993 }
1994 } else {
1995 self.stored_open_ns
1996 };
1997
1998 self.build_and_send(ts_event, ts_init);
1999
2000 self.stored_open_ns = event.ts_event;
2002
2003 if self.bar_type().spec().aggregation == BarAggregation::Month {
2004 let step = self.bar_type().spec().step.get() as u32;
2005 let alert_time_ns = add_n_months_nanos(event.ts_event, step).expect(FAILED);
2006
2007 self.clock
2008 .borrow_mut()
2009 .set_time_alert_ns(&self.timer_name, alert_time_ns, None, None)
2010 .expect(FAILED);
2011
2012 self.next_close_ns = alert_time_ns;
2013 } else if self.bar_type().spec().aggregation == BarAggregation::Year {
2014 let step = self.bar_type().spec().step.get() as u32;
2015 let alert_time_ns = add_n_years_nanos(event.ts_event, step).expect(FAILED);
2016
2017 self.clock
2018 .borrow_mut()
2019 .set_time_alert_ns(&self.timer_name, alert_time_ns, None, None)
2020 .expect(FAILED);
2021
2022 self.next_close_ns = alert_time_ns;
2023 } else {
2024 self.next_close_ns = self
2026 .clock
2027 .borrow()
2028 .next_time_ns(&self.timer_name)
2029 .unwrap_or_default();
2030 }
2031 }
2032
2033 fn preprocess_historical_events(&mut self, ts_init: UnixNanos) {
2034 if self.clock.borrow().timestamp_ns() == UnixNanos::default() {
2035 {
2037 let mut clock_borrow = self.clock.borrow_mut();
2038 let test_clock = clock_borrow
2039 .as_any_mut()
2040 .downcast_mut::<TestClock>()
2041 .expect("Expected TestClock in historical mode");
2042 test_clock.set_time(ts_init);
2043 }
2044 self.start_timer_internal(None);
2046 }
2047
2048 let events = {
2050 let mut clock_borrow = self.clock.borrow_mut();
2051 let test_clock = clock_borrow
2052 .as_any_mut()
2053 .downcast_mut::<TestClock>()
2054 .expect("Expected TestClock in historical mode");
2055 test_clock.advance_time(ts_init, true)
2056 };
2057
2058 for event in events {
2059 if event.ts_event == ts_init {
2060 self.historical_event_at_ts_init = Some(event);
2061 } else {
2062 self.build_bar(&event);
2063 }
2064 }
2065 }
2066
2067 fn postprocess_historical_events(&mut self, _ts_init: UnixNanos) {
2068 if let Some(ref event) = self.historical_event_at_ts_init.take() {
2069 self.build_bar(event);
2070 }
2071 }
2072
2073 pub fn set_historical_events_internal(&mut self, events: Vec<TimeEvent>) {
2075 self.historical_events = events;
2076 }
2077}
2078
2079impl BarAggregator for TimeBarAggregator {
2080 fn bar_type(&self) -> BarType {
2081 self.core.bar_type
2082 }
2083
2084 fn is_running(&self) -> bool {
2085 self.core.is_running
2086 }
2087
2088 fn set_is_running(&mut self, value: bool) {
2089 self.core.set_is_running(value);
2090 }
2091
2092 fn stop(&mut self) {
2094 Self::stop(self);
2095 }
2096
2097 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
2098 if self.historical_mode {
2099 self.preprocess_historical_events(ts_init);
2100 }
2101
2102 self.core.apply_update(price, size, ts_init);
2103
2104 if self.historical_mode {
2105 self.postprocess_historical_events(ts_init);
2106 }
2107 }
2108
2109 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
2110 if self.historical_mode {
2111 self.preprocess_historical_events(ts_init);
2112 }
2113
2114 self.core.builder.update_bar(bar, volume, ts_init);
2115
2116 if self.historical_mode {
2117 self.postprocess_historical_events(ts_init);
2118 }
2119 }
2120
2121 fn set_historical_mode(&mut self, historical_mode: bool, handler: Box<dyn FnMut(Bar)>) {
2122 self.historical_mode = historical_mode;
2123 self.core.handler = handler;
2124 }
2125
2126 fn set_historical_events(&mut self, events: Vec<TimeEvent>) {
2127 self.set_historical_events_internal(events);
2128 }
2129
2130 fn set_clock(&mut self, clock: Rc<RefCell<dyn Clock>>) {
2131 self.set_clock_internal(clock);
2132 }
2133
2134 fn build_bar(&mut self, event: &TimeEvent) {
2135 {
2138 #[expect(clippy::use_self)]
2139 TimeBarAggregator::build_bar(self, event);
2140 }
2141 }
2142
2143 fn set_aggregator_weak(&mut self, weak: Weak<RefCell<Box<dyn BarAggregator>>>) {
2144 self.aggregator_weak = Some(weak);
2145 }
2146
2147 fn start_timer(&mut self, aggregator_rc: Option<Rc<RefCell<Box<dyn BarAggregator>>>>) {
2148 self.start_timer_internal(aggregator_rc);
2149 }
2150
2151 fn set_adjustment(&mut self, adjustment: Decimal, mode: ContinuousFutureAdjustmentType) {
2152 self.core.set_adjustment(adjustment, mode);
2153 }
2154
2155 fn set_build_with_no_updates(&mut self, value: bool) {
2156 self.build_with_no_updates = value;
2157 }
2158
2159 fn is_historical(&self) -> bool {
2160 self.historical_mode
2161 }
2162}
2163
2164fn is_below_min_size_decimal(size: Decimal, precision: u8) -> bool {
2165 quantity_from_decimal(size, precision).raw == 0
2166}
2167
2168fn min_size_decimal(precision: u8) -> Decimal {
2169 Decimal::new(1, u32::from(precision))
2170}
2171
2172fn quantity_from_decimal(size: Decimal, precision: u8) -> Quantity {
2173 Quantity::from_decimal_dp(size, precision).expect(FAILED)
2174}
2175
2176fn step_as_quantity_raw(step: usize) -> QuantityRaw {
2178 (FIXED_SCALAR as QuantityRaw)
2179 .checked_mul(step as QuantityRaw)
2180 .expect("`step` overflows raw quantity units for volume aggregation")
2181}
2182
2183pub trait VegaProvider {
2185 fn vega_for_leg(&self, instrument_id: InstrumentId) -> Option<f64>;
2187}
2188
2189pub trait SpreadPriceRounder {
2191 fn round_prices(&self, raw_bid: f64, raw_ask: f64, precision: u8) -> (Price, Price);
2193}
2194
2195#[derive(Debug, Default)]
2197pub struct MapVegaProvider {
2198 vegas: AHashMap<InstrumentId, f64>,
2199}
2200
2201impl MapVegaProvider {
2202 pub fn new() -> Self {
2203 Self {
2204 vegas: AHashMap::new(),
2205 }
2206 }
2207
2208 pub fn insert(&mut self, instrument_id: InstrumentId, vega: f64) {
2209 self.vegas.insert(instrument_id, vega);
2210 }
2211
2212 pub fn get(&self, instrument_id: &InstrumentId) -> Option<f64> {
2213 self.vegas.get(instrument_id).copied()
2214 }
2215}
2216
2217impl VegaProvider for MapVegaProvider {
2218 fn vega_for_leg(&self, instrument_id: InstrumentId) -> Option<f64> {
2219 self.vegas.get(&instrument_id).copied()
2220 }
2221}
2222
2223#[derive(Debug)]
2225pub struct FixedTickSchemeRounder {
2226 scheme: FixedTickScheme,
2227}
2228
2229impl FixedTickSchemeRounder {
2230 pub fn new(tick: f64) -> anyhow::Result<Self> {
2236 Ok(Self {
2237 scheme: FixedTickScheme::new(tick)?,
2238 })
2239 }
2240
2241 fn round_one(&self, raw: f64, precision: u8, use_bid_rounding: bool) -> Price {
2242 if raw >= 0.0 {
2243 let p = if use_bid_rounding {
2244 self.scheme.next_bid_price(raw, 0, precision)
2245 } else {
2246 self.scheme.next_ask_price(raw, 0, precision)
2247 };
2248 p.unwrap_or_else(|| price_from_f64(raw, precision))
2249 } else {
2250 let p = if use_bid_rounding {
2251 self.scheme.next_ask_price(-raw, 0, precision)
2252 } else {
2253 self.scheme.next_bid_price(-raw, 0, precision)
2254 };
2255 p.map_or_else(
2256 || price_from_f64(raw, precision),
2257 |q| price_from_f64(-q.as_f64(), precision),
2258 )
2259 }
2260 }
2261}
2262
2263impl SpreadPriceRounder for FixedTickSchemeRounder {
2264 fn round_prices(&self, raw_bid: f64, raw_ask: f64, precision: u8) -> (Price, Price) {
2265 let bid = self.round_one(raw_bid, precision, true);
2266 let ask = self.round_one(raw_ask, precision, false);
2267 (bid, ask)
2268 }
2269}
2270
2271pub struct SpreadQuoteAggregator {
2277 spread_instrument_id: InstrumentId,
2278 leg_ids: Vec<InstrumentId>,
2279 ratios: Vec<i64>,
2280 n_legs: usize,
2281 is_futures_spread: bool,
2282 price_precision: u8,
2283 size_precision: u8,
2284 last_quotes: AHashMap<InstrumentId, QuoteTick>,
2285 mid_prices: Vec<f64>,
2286 bid_prices: Vec<f64>,
2287 ask_prices: Vec<f64>,
2288 vegas: Vec<f64>,
2289 bid_ask_spreads: Vec<f64>,
2290 bid_sizes: Vec<f64>,
2291 ask_sizes: Vec<f64>,
2292 handler: Box<dyn FnMut(QuoteTick)>,
2293 clock: Rc<RefCell<dyn Clock>>,
2294 historical_mode: bool,
2295 update_interval_seconds: Option<u64>,
2296 quote_build_delay: u64,
2297 has_update: bool,
2298 timer_name: String,
2299 vega_pricing_timeout_timer_name: String,
2300 historical_event_at_ts_init: Option<TimeEvent>,
2301 vega_provider: Option<Box<dyn VegaProvider>>,
2302 disable_vega_pricing: bool,
2303 vega_pricing_temporarily_disabled: bool,
2304 vega_pricing_timeout_seconds: u64,
2305 price_rounder: Option<Box<dyn SpreadPriceRounder>>,
2306 is_running: bool,
2307 aggregator_weak: Option<Weak<RefCell<Self>>>,
2308}
2309
2310impl Debug for SpreadQuoteAggregator {
2311 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2312 f.debug_struct(stringify!(SpreadQuoteAggregator))
2313 .field("spread_instrument_id", &self.spread_instrument_id)
2314 .field("n_legs", &self.n_legs)
2315 .field("is_futures_spread", &self.is_futures_spread)
2316 .field("update_interval_seconds", &self.update_interval_seconds)
2317 .finish()
2318 }
2319}
2320
2321impl SpreadQuoteAggregator {
2322 #[expect(clippy::too_many_arguments)]
2328 pub fn new(
2329 spread_instrument_id: InstrumentId,
2330 legs: &[(InstrumentId, i64)],
2331 is_futures_spread: bool,
2332 price_precision: u8,
2333 size_precision: u8,
2334 handler: Box<dyn FnMut(QuoteTick)>,
2335 clock: Rc<RefCell<dyn Clock>>,
2336 historical_mode: bool,
2337 update_interval_seconds: Option<u64>,
2338 quote_build_delay: u64,
2339 disable_vega_pricing: bool,
2340 vega_pricing_timeout_seconds: u64,
2341 vega_provider: Option<Box<dyn VegaProvider>>,
2342 price_rounder: Option<Box<dyn SpreadPriceRounder>>,
2343 ) -> Self {
2344 assert!(legs.len() >= 2, "Spread must have more than one leg");
2345 let n_legs = legs.len();
2346 let leg_ids: Vec<InstrumentId> = legs.iter().map(|(id, _)| *id).collect();
2347 let ratios: Vec<i64> = legs.iter().map(|(_, r)| *r).collect();
2348 for &r in &ratios {
2349 assert!(r != 0, "Ratio cannot be zero");
2350 }
2351 let timer_name = format!("SPREAD_QUOTE_{spread_instrument_id}");
2352 let vega_pricing_timeout_timer_name =
2353 format!("VEGA_PRICING_TIMEOUT_{spread_instrument_id}");
2354 Self {
2355 spread_instrument_id,
2356 leg_ids,
2357 ratios,
2358 n_legs,
2359 is_futures_spread,
2360 price_precision,
2361 size_precision,
2362 last_quotes: AHashMap::new(),
2363 mid_prices: vec![0.0; n_legs],
2364 bid_prices: vec![0.0; n_legs],
2365 ask_prices: vec![0.0; n_legs],
2366 vegas: vec![0.0; n_legs],
2367 bid_ask_spreads: vec![0.0; n_legs],
2368 bid_sizes: vec![0.0; n_legs],
2369 ask_sizes: vec![0.0; n_legs],
2370 handler,
2371 clock,
2372 historical_mode,
2373 update_interval_seconds,
2374 quote_build_delay,
2375 has_update: false,
2376 timer_name,
2377 vega_pricing_timeout_timer_name,
2378 historical_event_at_ts_init: None,
2379 vega_provider,
2380 disable_vega_pricing,
2381 vega_pricing_temporarily_disabled: false,
2382 vega_pricing_timeout_seconds,
2383 price_rounder,
2384 is_running: false,
2385 aggregator_weak: None,
2386 }
2387 }
2388
2389 pub fn set_aggregator_weak(&mut self, weak: Weak<RefCell<Self>>) {
2392 self.aggregator_weak = Some(weak);
2393 }
2394
2395 pub fn prepare_for_timer_mode(&mut self, self_rc: &Rc<RefCell<Self>>) {
2400 self.aggregator_weak = Some(Rc::downgrade(self_rc));
2401 }
2402
2403 pub fn set_historical_mode(
2405 &mut self,
2406 historical_mode: bool,
2407 handler: Box<dyn FnMut(QuoteTick)>,
2408 vega_provider: Option<Box<dyn VegaProvider>>,
2409 ) {
2410 self.historical_mode = historical_mode;
2411 self.handler = handler;
2412
2413 if let Some(vp) = vega_provider {
2414 self.vega_provider = Some(vp);
2415 }
2416 }
2417
2418 pub fn set_running(&mut self, is_running: bool) {
2419 self.is_running = is_running;
2420 }
2421
2422 pub fn set_clock(&mut self, clock: Rc<RefCell<dyn Clock>>) {
2423 self.clock = clock;
2424 }
2425
2426 pub fn start_timer(&mut self, aggregator_rc: Option<Rc<RefCell<Self>>>) {
2435 if let Some(rc) = aggregator_rc {
2436 self.aggregator_weak = Some(Rc::downgrade(&rc));
2437 }
2438
2439 let Some(interval_secs) = self.update_interval_seconds else {
2440 return;
2441 };
2442 let aggregator_weak = self.aggregator_weak.clone().expect(
2443 "SpreadQuoteAggregator: timer mode requires prepare_for_timer_mode(rc) to be \
2444 called first with the Rc that wraps this aggregator (before feeding quotes in \
2445 historical mode or before start_timer(None)).",
2446 );
2447
2448 let callback = TimeEventCallback::RustLocal(Rc::new(move |event: TimeEvent| {
2449 if let Some(agg) = aggregator_weak.upgrade() {
2450 agg.borrow_mut().on_timer_fire(event.ts_event);
2451 }
2452 }));
2453
2454 let now_ns = self.clock.borrow().timestamp_ns();
2455 let interval_ns = interval_secs * 1_000_000_000;
2456 let start_ns = (now_ns.as_u64() / interval_ns) * interval_ns;
2457 let start_ns = start_ns + self.quote_build_delay * 1_000; let start_time = UnixNanos::from(start_ns);
2459 let fire_immediately = now_ns == start_time;
2460 self.clock
2461 .borrow_mut()
2462 .set_timer_ns(
2463 &self.timer_name,
2464 interval_ns,
2465 Some(start_time),
2466 None,
2467 Some(callback),
2468 Some(true),
2469 Some(fire_immediately),
2470 )
2471 .expect("Failed to set spread quote timer");
2472 }
2473
2474 pub fn on_timer_fire(&mut self, ts_event: UnixNanos) {
2476 if self.last_quotes.len() == self.n_legs {
2477 self.build_and_send_quote(ts_event);
2478 }
2479 }
2480
2481 pub fn stop_timer(&mut self) {
2483 if self.update_interval_seconds.is_some()
2484 && self
2485 .clock
2486 .borrow()
2487 .timer_names()
2488 .contains(&self.timer_name.as_str())
2489 {
2490 self.clock.borrow_mut().cancel_timer(&self.timer_name);
2491 }
2492
2493 if self
2494 .clock
2495 .borrow()
2496 .timer_names()
2497 .contains(&self.vega_pricing_timeout_timer_name.as_str())
2498 {
2499 self.clock
2500 .borrow_mut()
2501 .cancel_timer(&self.vega_pricing_timeout_timer_name);
2502 }
2503 }
2504
2505 pub fn handle_quote_tick(&mut self, tick: QuoteTick) {
2507 let ts_init = tick.ts_init;
2508
2509 if self.update_interval_seconds.is_some() && self.historical_mode {
2510 self.process_historical_events(ts_init);
2511 }
2512 self.last_quotes.insert(tick.instrument_id, tick);
2513 self.has_update = true;
2514
2515 if self.update_interval_seconds.is_none() && self.last_quotes.len() == self.n_legs {
2516 self.build_and_send_quote(ts_init);
2517 }
2518 }
2519
2520 pub fn flush_pending_historical_quote(&mut self) {
2526 if self.update_interval_seconds.is_none() || !self.historical_mode {
2527 return;
2528 }
2529
2530 let Some(event) = self.historical_event_at_ts_init.take() else {
2531 return;
2532 };
2533
2534 if self.last_quotes.len() == self.n_legs {
2535 self.build_and_send_quote(event.ts_event);
2536 }
2537 }
2538
2539 fn process_historical_events(&mut self, ts_init: UnixNanos) {
2545 if self.clock.borrow().timestamp_ns() == UnixNanos::default() {
2546 let mut clock_borrow = self.clock.borrow_mut();
2547 let test_clock = clock_borrow
2548 .as_any_mut()
2549 .downcast_mut::<TestClock>()
2550 .expect("Expected TestClock in historical mode");
2551 test_clock.set_time(ts_init);
2552 drop(clock_borrow);
2553 self.start_timer(None);
2554 }
2555
2556 if self.last_quotes.len() == self.n_legs
2557 && let Some(ref event) = self.historical_event_at_ts_init
2558 && event.ts_event < ts_init
2559 {
2560 let event = self.historical_event_at_ts_init.take().unwrap();
2562 self.build_and_send_quote(event.ts_event);
2563 }
2564
2565 let events = {
2566 let mut clock_borrow = self.clock.borrow_mut();
2567 let test_clock = clock_borrow
2568 .as_any_mut()
2569 .downcast_mut::<TestClock>()
2570 .expect("Expected TestClock in historical mode");
2571 test_clock.advance_time(ts_init, true)
2572 };
2573
2574 for event in events {
2575 if event.ts_event == ts_init {
2576 self.historical_event_at_ts_init = Some(event);
2577 } else if self.last_quotes.len() == self.n_legs {
2578 self.build_and_send_quote(event.ts_event);
2579 }
2580 }
2581 }
2582
2583 fn build_and_send_quote(&mut self, ts_event: UnixNanos) {
2585 if !self.has_update {
2586 return;
2587 }
2588
2589 let use_vega_pricing =
2590 !(self.disable_vega_pricing || self.vega_pricing_temporarily_disabled);
2591
2592 for (idx, &leg_id) in self.leg_ids.iter().enumerate() {
2593 let Some(tick) = self.last_quotes.get(&leg_id) else {
2594 log::error!(
2595 "SpreadQuoteAggregator[{}]: Missing quote for leg {}",
2596 self.spread_instrument_id,
2597 leg_id
2598 );
2599 return;
2600 };
2601 let ask_price = tick.ask_price.as_f64();
2602 let bid_price = tick.bid_price.as_f64();
2603 self.bid_prices[idx] = bid_price;
2604 self.ask_prices[idx] = ask_price;
2605 self.bid_sizes[idx] = tick.bid_size.as_f64();
2606 self.ask_sizes[idx] = tick.ask_size.as_f64();
2607
2608 if !self.is_futures_spread {
2609 self.mid_prices[idx] = f64::midpoint(ask_price, bid_price);
2610 self.bid_ask_spreads[idx] = ask_price - bid_price;
2611
2612 if use_vega_pricing
2613 && let Some(ref vp) = self.vega_provider
2614 && let Some(vega) = vp.vega_for_leg(leg_id)
2615 {
2616 self.vegas[idx] = vega;
2617 }
2618 }
2619 }
2620 let (raw_bid, raw_ask) = if self.is_futures_spread {
2621 self.create_futures_spread_prices()
2622 } else {
2623 self.create_option_spread_prices()
2624 };
2625 let spread_quote = self.create_quote_tick_from_raw_prices(raw_bid, raw_ask, ts_event);
2626 self.has_update = false;
2627 (self.handler)(spread_quote);
2628 }
2629
2630 fn create_option_spread_prices(&mut self) -> (f64, f64) {
2631 if self.disable_vega_pricing || self.vega_pricing_temporarily_disabled {
2632 return self.create_futures_spread_prices();
2633 }
2634
2635 let vega_multipliers: Vec<f64> = (0..self.n_legs)
2636 .map(|i| {
2637 if self.vegas[i] == 0.0 {
2638 0.0
2639 } else {
2640 self.bid_ask_spreads[i] / self.vegas[i]
2641 }
2642 })
2643 .collect();
2644 let non_zero: Vec<f64> = vega_multipliers
2645 .iter()
2646 .copied()
2647 .filter(|&x| x != 0.0)
2648 .collect();
2649
2650 if non_zero.is_empty() {
2651 log::warn!(
2652 "No vega information available for the components of {}; will generate spread quote using component quotes only, vega pricing is disabled for {} seconds, subscribe to some underlying price information for more precise quotes",
2653 self.spread_instrument_id,
2654 self.vega_pricing_timeout_seconds
2655 );
2656 self.start_vega_pricing_timeout();
2657 return self.create_futures_spread_prices();
2658 }
2659 let vega_multiplier = non_zero.iter().map(|x| x.abs()).sum::<f64>() / non_zero.len() as f64;
2660 let spread_vega = self
2661 .vegas
2662 .iter()
2663 .zip(self.ratios.iter())
2664 .map(|(v, r)| v * (*r as f64))
2665 .sum::<f64>()
2666 .abs();
2667 let bid_ask_spread = spread_vega * vega_multiplier;
2668 let spread_mid_price: f64 = self
2669 .mid_prices
2670 .iter()
2671 .zip(self.ratios.iter())
2672 .map(|(m, r)| m * (*r as f64))
2673 .sum();
2674 let raw_bid = spread_mid_price - bid_ask_spread * 0.5;
2675 let raw_ask = spread_mid_price + bid_ask_spread * 0.5;
2676 (raw_bid, raw_ask)
2677 }
2678
2679 fn clear_vega_pricing_timeout(&mut self) {
2680 self.vega_pricing_temporarily_disabled = false;
2681 }
2682
2683 fn start_vega_pricing_timeout(&mut self) {
2684 self.vega_pricing_temporarily_disabled = true;
2685
2686 if self
2687 .clock
2688 .borrow()
2689 .timer_names()
2690 .contains(&self.vega_pricing_timeout_timer_name.as_str())
2691 {
2692 return;
2693 }
2694
2695 let Some(aggregator_weak) = self.aggregator_weak.clone() else {
2696 return;
2697 };
2698 let callback = TimeEventCallback::RustLocal(Rc::new(move |_event: TimeEvent| {
2699 if let Some(agg) = aggregator_weak.upgrade() {
2700 agg.borrow_mut().clear_vega_pricing_timeout();
2701 }
2702 }));
2703 let alert_time =
2704 self.clock.borrow().timestamp_ns() + self.vega_pricing_timeout_seconds * 1_000_000_000;
2705
2706 self.clock
2707 .borrow_mut()
2708 .set_time_alert_ns(
2709 &self.vega_pricing_timeout_timer_name,
2710 alert_time,
2711 Some(callback),
2712 Some(true),
2713 )
2714 .expect("Failed to set spread quote vega pricing timeout");
2715 }
2716
2717 fn create_futures_spread_prices(&self) -> (f64, f64) {
2718 let mut raw_ask = 0.0_f64;
2719 let mut raw_bid = 0.0_f64;
2720
2721 for i in 0..self.n_legs {
2722 let r = self.ratios[i] as f64;
2723 if self.ratios[i] >= 0 {
2724 raw_ask += r * self.ask_prices[i];
2725 raw_bid += r * self.bid_prices[i];
2726 } else {
2727 raw_ask += r * self.bid_prices[i];
2728 raw_bid += r * self.ask_prices[i];
2729 }
2730 }
2731 (raw_bid, raw_ask)
2732 }
2733
2734 fn create_quote_tick_from_raw_prices(
2735 &self,
2736 raw_bid_price: f64,
2737 raw_ask_price: f64,
2738 ts_event: UnixNanos,
2739 ) -> QuoteTick {
2740 let (bid_price, ask_price) = if let Some(ref rounder) = self.price_rounder {
2741 rounder.round_prices(raw_bid_price, raw_ask_price, self.price_precision)
2742 } else {
2743 let bid = price_from_f64(raw_bid_price, self.price_precision);
2744 let ask = price_from_f64(raw_ask_price, self.price_precision);
2745 (bid, ask)
2746 };
2747 let mut min_bid_size = f64::INFINITY;
2748 let mut min_ask_size = f64::INFINITY;
2749 for i in 0..self.n_legs {
2750 let abs_ratio = self.ratios[i].unsigned_abs() as f64;
2751 if self.ratios[i] >= 0 {
2752 let b = self.bid_sizes[i] / abs_ratio;
2753 if b < min_bid_size {
2754 min_bid_size = b;
2755 }
2756 let a = self.ask_sizes[i] / abs_ratio;
2757 if a < min_ask_size {
2758 min_ask_size = a;
2759 }
2760 } else {
2761 let b = self.ask_sizes[i] / abs_ratio;
2762 if b < min_bid_size {
2763 min_bid_size = b;
2764 }
2765 let a = self.bid_sizes[i] / abs_ratio;
2766 if a < min_ask_size {
2767 min_ask_size = a;
2768 }
2769 }
2770 }
2771 let bid_size = Quantity::new(min_bid_size, self.size_precision);
2772 let ask_size = Quantity::new(min_ask_size, self.size_precision);
2773 QuoteTick::new(
2774 self.spread_instrument_id,
2775 bid_price,
2776 ask_price,
2777 bid_size,
2778 ask_size,
2779 ts_event,
2780 ts_event,
2781 )
2782 }
2783}
2784
2785fn price_from_f64(v: f64, precision: u8) -> Price {
2786 Price::new(v, precision)
2787}
2788
2789#[cfg(test)]
2790mod tests {
2791 use std::sync::{Arc, Mutex};
2792
2793 use nautilus_common::{clock::TestClock, timer::TimeEvent};
2794 use nautilus_core::{MUTEX_POISONED, UUID4, UnixNanos};
2795 use nautilus_model::{
2796 data::{BarSpecification, BarType, QuoteTick},
2797 enums::{AggregationSource, AggressorSide, BarAggregation, PriceType},
2798 identifiers::InstrumentId,
2799 instruments::{CurrencyPair, Equity, Instrument, InstrumentAny, stubs::*},
2800 types::{Price, Quantity},
2801 };
2802 use rstest::rstest;
2803 use ustr::Ustr;
2804
2805 use super::*;
2806
2807 #[rstest]
2808 fn test_bar_builder_initialization(equity_aapl: Equity) {
2809 let instrument = InstrumentAny::Equity(equity_aapl);
2810 let bar_type = BarType::new(
2811 instrument.id(),
2812 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2813 AggregationSource::Internal,
2814 );
2815 let builder = BarBuilder::new(
2816 bar_type,
2817 instrument.price_precision(),
2818 instrument.size_precision(),
2819 );
2820
2821 assert!(!builder.initialized);
2822 assert_eq!(builder.ts_last, 0);
2823 assert_eq!(builder.count, 0);
2824 }
2825
2826 #[rstest]
2827 fn test_bar_builder_maintains_ohlc_order(equity_aapl: Equity) {
2828 let instrument = InstrumentAny::Equity(equity_aapl);
2829 let bar_type = BarType::new(
2830 instrument.id(),
2831 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2832 AggregationSource::Internal,
2833 );
2834 let mut builder = BarBuilder::new(
2835 bar_type,
2836 instrument.price_precision(),
2837 instrument.size_precision(),
2838 );
2839
2840 builder.update(
2841 Price::from("100.00"),
2842 Quantity::from(1),
2843 UnixNanos::from(1000),
2844 );
2845 builder.update(
2846 Price::from("95.00"),
2847 Quantity::from(1),
2848 UnixNanos::from(2000),
2849 );
2850 builder.update(
2851 Price::from("105.00"),
2852 Quantity::from(1),
2853 UnixNanos::from(3000),
2854 );
2855
2856 let bar = builder.build_now();
2857 assert!(bar.high > bar.low);
2858 assert_eq!(bar.open, Price::from("100.00"));
2859 assert_eq!(bar.high, Price::from("105.00"));
2860 assert_eq!(bar.low, Price::from("95.00"));
2861 assert_eq!(bar.close, Price::from("105.00"));
2862 }
2863
2864 #[rstest]
2865 fn test_update_ignores_earlier_timestamps(equity_aapl: Equity) {
2866 let instrument = InstrumentAny::Equity(equity_aapl);
2867 let bar_type = BarType::new(
2868 instrument.id(),
2869 BarSpecification::new(100, BarAggregation::Tick, PriceType::Last),
2870 AggregationSource::Internal,
2871 );
2872 let mut builder = BarBuilder::new(
2873 bar_type,
2874 instrument.price_precision(),
2875 instrument.size_precision(),
2876 );
2877
2878 builder.update(Price::from("1.00000"), Quantity::from(1), 1_000.into());
2879 builder.update(Price::from("1.00001"), Quantity::from(1), 500.into());
2880
2881 assert_eq!(builder.ts_last, 1_000);
2882 assert_eq!(builder.count, 1);
2883 }
2884
2885 #[rstest]
2886 fn test_bar_builder_single_update_results_in_expected_properties(equity_aapl: Equity) {
2887 let instrument = InstrumentAny::Equity(equity_aapl);
2888 let bar_type = BarType::new(
2889 instrument.id(),
2890 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2891 AggregationSource::Internal,
2892 );
2893 let mut builder = BarBuilder::new(
2894 bar_type,
2895 instrument.price_precision(),
2896 instrument.size_precision(),
2897 );
2898
2899 builder.update(
2900 Price::from("1.00000"),
2901 Quantity::from(1),
2902 UnixNanos::default(),
2903 );
2904
2905 assert!(builder.initialized);
2906 assert_eq!(builder.ts_last, 0);
2907 assert_eq!(builder.count, 1);
2908 }
2909
2910 #[rstest]
2911 fn test_bar_builder_single_update_when_timestamp_less_than_last_update_ignores(
2912 equity_aapl: Equity,
2913 ) {
2914 let instrument = InstrumentAny::Equity(equity_aapl);
2915 let bar_type = BarType::new(
2916 instrument.id(),
2917 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2918 AggregationSource::Internal,
2919 );
2920 let mut builder = BarBuilder::new(bar_type, 2, 0);
2921
2922 builder.update(
2923 Price::from("1.00000"),
2924 Quantity::from(1),
2925 UnixNanos::from(1_000),
2926 );
2927 builder.update(
2928 Price::from("1.00001"),
2929 Quantity::from(1),
2930 UnixNanos::from(500),
2931 );
2932
2933 assert!(builder.initialized);
2934 assert_eq!(builder.ts_last, 1_000);
2935 assert_eq!(builder.count, 1);
2936 }
2937
2938 #[rstest]
2939 fn test_bar_builder_multiple_updates_correctly_increments_count(equity_aapl: Equity) {
2940 let instrument = InstrumentAny::Equity(equity_aapl);
2941 let bar_type = BarType::new(
2942 instrument.id(),
2943 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2944 AggregationSource::Internal,
2945 );
2946 let mut builder = BarBuilder::new(
2947 bar_type,
2948 instrument.price_precision(),
2949 instrument.size_precision(),
2950 );
2951
2952 for _ in 0..5 {
2953 builder.update(
2954 Price::from("1.00000"),
2955 Quantity::from(1),
2956 UnixNanos::from(1_000),
2957 );
2958 }
2959
2960 assert_eq!(builder.count, 5);
2961 }
2962
2963 #[rstest]
2964 #[should_panic]
2965 fn test_bar_builder_build_when_no_updates_panics(equity_aapl: Equity) {
2966 let instrument = InstrumentAny::Equity(equity_aapl);
2967 let bar_type = BarType::new(
2968 instrument.id(),
2969 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2970 AggregationSource::Internal,
2971 );
2972 let mut builder = BarBuilder::new(
2973 bar_type,
2974 instrument.price_precision(),
2975 instrument.size_precision(),
2976 );
2977 let _ = builder.build_now();
2978 }
2979
2980 #[rstest]
2981 fn test_bar_builder_build_when_received_updates_returns_expected_bar(equity_aapl: Equity) {
2982 let instrument = InstrumentAny::Equity(equity_aapl);
2983 let bar_type = BarType::new(
2984 instrument.id(),
2985 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2986 AggregationSource::Internal,
2987 );
2988 let mut builder = BarBuilder::new(
2989 bar_type,
2990 instrument.price_precision(),
2991 instrument.size_precision(),
2992 );
2993
2994 builder.update(
2995 Price::from("1.00001"),
2996 Quantity::from(2),
2997 UnixNanos::default(),
2998 );
2999 builder.update(
3000 Price::from("1.00002"),
3001 Quantity::from(2),
3002 UnixNanos::default(),
3003 );
3004 builder.update(
3005 Price::from("1.00000"),
3006 Quantity::from(1),
3007 UnixNanos::from(1_000_000_000),
3008 );
3009
3010 let bar = builder.build_now();
3011
3012 assert_eq!(bar.open, Price::from("1.00001"));
3013 assert_eq!(bar.high, Price::from("1.00002"));
3014 assert_eq!(bar.low, Price::from("1.00000"));
3015 assert_eq!(bar.close, Price::from("1.00000"));
3016 assert_eq!(bar.volume, Quantity::from(5));
3017 assert_eq!(bar.ts_init, 1_000_000_000);
3018 assert_eq!(builder.ts_last, 1_000_000_000);
3019 assert_eq!(builder.count, 0);
3020 }
3021
3022 #[rstest]
3023 fn test_bar_builder_build_with_previous_close(equity_aapl: Equity) {
3024 let instrument = InstrumentAny::Equity(equity_aapl);
3025 let bar_type = BarType::new(
3026 instrument.id(),
3027 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3028 AggregationSource::Internal,
3029 );
3030 let mut builder = BarBuilder::new(bar_type, 2, 0);
3031
3032 builder.update(
3033 Price::from("1.00001"),
3034 Quantity::from(1),
3035 UnixNanos::default(),
3036 );
3037 builder.build_now();
3038
3039 builder.update(
3040 Price::from("1.00000"),
3041 Quantity::from(1),
3042 UnixNanos::default(),
3043 );
3044 builder.update(
3045 Price::from("1.00003"),
3046 Quantity::from(1),
3047 UnixNanos::default(),
3048 );
3049 builder.update(
3050 Price::from("1.00002"),
3051 Quantity::from(1),
3052 UnixNanos::default(),
3053 );
3054
3055 let bar = builder.build_now();
3056
3057 assert_eq!(bar.open, Price::from("1.00000"));
3058 assert_eq!(bar.high, Price::from("1.00003"));
3059 assert_eq!(bar.low, Price::from("1.00000"));
3060 assert_eq!(bar.close, Price::from("1.00002"));
3061 assert_eq!(bar.volume, Quantity::from(3));
3062 }
3063
3064 #[rstest]
3065 fn test_bar_builder_update_bar_initializes_then_accumulates(equity_aapl: Equity) {
3066 let instrument = InstrumentAny::Equity(equity_aapl);
3067 let bar_type = BarType::new(
3068 instrument.id(),
3069 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3070 AggregationSource::Internal,
3071 );
3072 let mut builder = BarBuilder::new(
3073 bar_type,
3074 instrument.price_precision(),
3075 instrument.size_precision(),
3076 );
3077
3078 let bar_one = Bar::new(
3079 bar_type,
3080 Price::from("100.00"),
3081 Price::from("102.00"),
3082 Price::from("99.00"),
3083 Price::from("101.00"),
3084 Quantity::from(10),
3085 UnixNanos::from(1_000),
3086 UnixNanos::from(1_000),
3087 );
3088 let bar_two = Bar::new(
3089 bar_type,
3090 Price::from("101.00"),
3091 Price::from("103.00"),
3092 Price::from("98.00"),
3093 Price::from("102.00"),
3094 Quantity::from(5),
3095 UnixNanos::from(2_000),
3096 UnixNanos::from(2_000),
3097 );
3098
3099 builder.update_bar(bar_one, bar_one.volume, bar_one.ts_init);
3100 builder.update_bar(bar_two, bar_two.volume, bar_two.ts_init);
3101 let bar = builder.build_now();
3102
3103 assert_eq!(bar.open, Price::from("100.00"));
3104 assert_eq!(bar.high, Price::from("103.00"));
3105 assert_eq!(bar.low, Price::from("98.00"));
3106 assert_eq!(bar.close, Price::from("102.00"));
3107 assert_eq!(bar.volume, Quantity::from(15));
3108 assert_eq!(builder.count, 0);
3109 }
3110
3111 #[rstest]
3112 fn test_bar_builder_update_bar_ignores_earlier_timestamp(equity_aapl: Equity) {
3113 let instrument = InstrumentAny::Equity(equity_aapl);
3114 let bar_type = BarType::new(
3115 instrument.id(),
3116 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3117 AggregationSource::Internal,
3118 );
3119 let mut builder = BarBuilder::new(
3120 bar_type,
3121 instrument.price_precision(),
3122 instrument.size_precision(),
3123 );
3124
3125 let bar_later = Bar::new(
3126 bar_type,
3127 Price::from("100.00"),
3128 Price::from("101.00"),
3129 Price::from("99.00"),
3130 Price::from("100.50"),
3131 Quantity::from(10),
3132 UnixNanos::from(2_000),
3133 UnixNanos::from(2_000),
3134 );
3135 let bar_earlier = Bar::new(
3136 bar_type,
3137 Price::from("200.00"),
3138 Price::from("210.00"),
3139 Price::from("190.00"),
3140 Price::from("205.00"),
3141 Quantity::from(50),
3142 UnixNanos::from(1_000),
3143 UnixNanos::from(1_000),
3144 );
3145
3146 builder.update_bar(bar_later, bar_later.volume, bar_later.ts_init);
3147 builder.update_bar(bar_earlier, bar_earlier.volume, bar_earlier.ts_init);
3148
3149 assert_eq!(builder.ts_last, 2_000);
3150 assert_eq!(builder.count, 1);
3151 assert_eq!(builder.volume, Quantity::from(10));
3152 }
3153
3154 #[rstest]
3155 #[case::spread_zero_inactive(
3156 Decimal::ZERO,
3157 ContinuousFutureAdjustmentType::BackwardSpread,
3158 false
3159 )]
3160 #[case::spread_positive_active(
3161 Decimal::new(150, 2), ContinuousFutureAdjustmentType::BackwardSpread,
3163 true,
3164 )]
3165 #[case::spread_negative_active(
3166 Decimal::new(-250, 2), ContinuousFutureAdjustmentType::ForwardSpread,
3168 true,
3169 )]
3170 #[case::spread_sub_precision_inactive(
3171 Decimal::new(1, 28),
3173 ContinuousFutureAdjustmentType::BackwardSpread,
3174 false,
3175 )]
3176 #[case::ratio_one_inactive(Decimal::ONE, ContinuousFutureAdjustmentType::BackwardRatio, false)]
3177 #[case::ratio_non_one_active(
3178 Decimal::new(105, 2), ContinuousFutureAdjustmentType::ForwardRatio,
3180 true,
3181 )]
3182 fn test_bar_builder_set_adjustment_active_flag(
3183 equity_aapl: Equity,
3184 #[case] adjustment: Decimal,
3185 #[case] mode: ContinuousFutureAdjustmentType,
3186 #[case] expected_active: bool,
3187 ) {
3188 let instrument = InstrumentAny::Equity(equity_aapl);
3189 let bar_type = BarType::new(
3190 instrument.id(),
3191 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3192 AggregationSource::Internal,
3193 );
3194 let mut builder = BarBuilder::new(bar_type, 2, 0);
3195
3196 builder.set_adjustment(adjustment, mode);
3197
3198 assert_eq!(builder.adjustment_active, expected_active);
3199 assert_eq!(builder.adjustment_is_ratio, mode.is_ratio());
3200 assert_eq!(builder.adjustment_mode, mode);
3201 }
3202
3203 #[rstest]
3204 fn test_bar_builder_set_adjustment_mode_switch_resets_flags(equity_aapl: Equity) {
3205 let instrument = InstrumentAny::Equity(equity_aapl);
3206 let bar_type = BarType::new(
3207 instrument.id(),
3208 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3209 AggregationSource::Internal,
3210 );
3211 let mut builder = BarBuilder::new(bar_type, 2, 0);
3212
3213 builder.set_adjustment(
3215 Decimal::new(150, 2), ContinuousFutureAdjustmentType::BackwardRatio,
3217 );
3218 builder.set_adjustment(
3219 Decimal::new(50, 2), ContinuousFutureAdjustmentType::BackwardSpread,
3221 );
3222 assert!(!builder.adjustment_is_ratio);
3223 builder.update(Price::from("100.00"), Quantity::from(1), 1_000.into());
3224 assert_eq!(builder.build_now().close, Price::from("100.50"));
3225
3226 builder.set_adjustment(
3228 Decimal::new(11, 1), ContinuousFutureAdjustmentType::ForwardRatio,
3230 );
3231 assert!(builder.adjustment_is_ratio);
3232 builder.update(Price::from("100.00"), Quantity::from(1), 2_000.into());
3233 assert_eq!(builder.build_now().close, Price::from("110.00"));
3234 }
3235
3236 #[rstest]
3237 fn test_bar_builder_update_applies_backward_spread_adjustment(equity_aapl: Equity) {
3238 let instrument = InstrumentAny::Equity(equity_aapl);
3239 let bar_type = BarType::new(
3240 instrument.id(),
3241 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3242 AggregationSource::Internal,
3243 );
3244 let mut builder = BarBuilder::new(bar_type, 2, 0);
3245
3246 builder.set_adjustment(
3247 Decimal::new(250, 2), ContinuousFutureAdjustmentType::BackwardSpread,
3249 );
3250
3251 builder.update(Price::from("100.00"), Quantity::from(1), 1_000.into());
3252 builder.update(Price::from("99.00"), Quantity::from(1), 2_000.into());
3253 builder.update(Price::from("101.00"), Quantity::from(1), 3_000.into());
3254
3255 let bar = builder.build_now();
3256 assert_eq!(bar.open, Price::from("102.50"));
3257 assert_eq!(bar.high, Price::from("103.50"));
3258 assert_eq!(bar.low, Price::from("101.50"));
3259 assert_eq!(bar.close, Price::from("103.50"));
3260 }
3261
3262 #[rstest]
3263 fn test_bar_builder_update_applies_forward_ratio_adjustment(equity_aapl: Equity) {
3264 let instrument = InstrumentAny::Equity(equity_aapl);
3265 let bar_type = BarType::new(
3266 instrument.id(),
3267 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3268 AggregationSource::Internal,
3269 );
3270 let mut builder = BarBuilder::new(bar_type, 2, 0);
3271
3272 builder.set_adjustment(
3273 Decimal::new(11, 1), ContinuousFutureAdjustmentType::ForwardRatio,
3275 );
3276
3277 builder.update(Price::from("100.00"), Quantity::from(1), 1_000.into());
3278 builder.update(Price::from("90.00"), Quantity::from(1), 2_000.into());
3279 builder.update(Price::from("110.00"), Quantity::from(1), 3_000.into());
3280
3281 let bar = builder.build_now();
3282 assert_eq!(bar.open, Price::from("110.00"));
3283 assert_eq!(bar.high, Price::from("121.00"));
3284 assert_eq!(bar.low, Price::from("99.00"));
3285 assert_eq!(bar.close, Price::from("121.00"));
3286 }
3287
3288 #[rstest]
3289 fn test_bar_builder_update_bar_applies_adjustment_to_ohlc(equity_aapl: Equity) {
3290 let instrument = InstrumentAny::Equity(equity_aapl);
3291 let bar_type = BarType::new(
3292 instrument.id(),
3293 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3294 AggregationSource::Internal,
3295 );
3296 let mut builder = BarBuilder::new(bar_type, 2, 0);
3297
3298 builder.set_adjustment(
3299 Decimal::new(-100, 2), ContinuousFutureAdjustmentType::BackwardSpread,
3301 );
3302
3303 let input = Bar::new(
3304 bar_type,
3305 Price::from("100.00"),
3306 Price::from("105.00"),
3307 Price::from("99.00"),
3308 Price::from("102.00"),
3309 Quantity::from(10),
3310 UnixNanos::from(1_000),
3311 UnixNanos::from(1_000),
3312 );
3313 builder.update_bar(input, input.volume, input.ts_init);
3314
3315 let bar = builder.build_now();
3316 assert_eq!(bar.open, Price::from("99.00"));
3317 assert_eq!(bar.high, Price::from("104.00"));
3318 assert_eq!(bar.low, Price::from("98.00"));
3319 assert_eq!(bar.close, Price::from("101.00"));
3320 }
3321
3322 #[rstest]
3323 fn test_bar_builder_reset_retains_adjustment(equity_aapl: Equity) {
3324 let instrument = InstrumentAny::Equity(equity_aapl);
3325 let bar_type = BarType::new(
3326 instrument.id(),
3327 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3328 AggregationSource::Internal,
3329 );
3330 let mut builder = BarBuilder::new(bar_type, 2, 0);
3331
3332 builder.set_adjustment(
3333 Decimal::new(500, 2), ContinuousFutureAdjustmentType::BackwardSpread,
3335 );
3336 builder.update(Price::from("100.00"), Quantity::from(1), 1_000.into());
3337 let bar_one = builder.build_now();
3338 assert_eq!(bar_one.close, Price::from("105.00"));
3339
3340 assert!(builder.adjustment_active);
3342
3343 builder.update(Price::from("110.00"), Quantity::from(1), 2_000.into());
3344 let bar_two = builder.build_now();
3345 assert_eq!(bar_two.close, Price::from("115.00"));
3346 }
3347
3348 #[rstest]
3349 fn test_bar_builder_update_bar_applies_ratio_adjustment(equity_aapl: Equity) {
3350 let instrument = InstrumentAny::Equity(equity_aapl);
3351 let bar_type = BarType::new(
3352 instrument.id(),
3353 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3354 AggregationSource::Internal,
3355 );
3356 let mut builder = BarBuilder::new(bar_type, 2, 0);
3357
3358 builder.set_adjustment(
3359 Decimal::new(11, 1), ContinuousFutureAdjustmentType::ForwardRatio,
3361 );
3362
3363 let input = Bar::new(
3364 bar_type,
3365 Price::from("100.00"),
3366 Price::from("110.00"),
3367 Price::from("90.00"),
3368 Price::from("105.00"),
3369 Quantity::from(10),
3370 UnixNanos::from(1_000),
3371 UnixNanos::from(1_000),
3372 );
3373 builder.update_bar(input, input.volume, input.ts_init);
3374
3375 let bar = builder.build_now();
3376 assert_eq!(bar.open, Price::from("110.00"));
3377 assert_eq!(bar.high, Price::from("121.00"));
3378 assert_eq!(bar.low, Price::from("99.00"));
3379 assert_eq!(bar.close, Price::from("115.50"));
3380 }
3381
3382 #[rstest]
3383 fn test_bar_builder_spread_below_zero_representable(equity_aapl: Equity) {
3384 let instrument = InstrumentAny::Equity(equity_aapl);
3387 let bar_type = BarType::new(
3388 instrument.id(),
3389 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3390 AggregationSource::Internal,
3391 );
3392 let mut builder = BarBuilder::new(bar_type, 2, 0);
3393
3394 builder.set_adjustment(
3395 Decimal::new(-15000, 2), ContinuousFutureAdjustmentType::BackwardSpread,
3397 );
3398
3399 builder.update(Price::from("100.00"), Quantity::from(1), 1_000.into());
3400 let bar = builder.build_now();
3401 assert_eq!(bar.close, Price::from("-50.00"));
3402 assert!(bar.close.raw < 0);
3403 assert_eq!(bar.close.precision, 2);
3404 }
3405
3406 #[rstest]
3407 fn test_bar_builder_build_promotes_close_above_high_from_previous_close(equity_aapl: Equity) {
3408 let instrument = InstrumentAny::Equity(equity_aapl);
3409 let bar_type = BarType::new(
3410 instrument.id(),
3411 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3412 AggregationSource::Internal,
3413 );
3414 let mut builder = BarBuilder::new(bar_type, 2, 0);
3415
3416 builder.update(
3417 Price::from("110.00"),
3418 Quantity::from(1),
3419 UnixNanos::from(100),
3420 );
3421 builder.build_now();
3422
3423 builder.update(
3424 Price::from("100.00"),
3425 Quantity::from(1),
3426 UnixNanos::from(200),
3427 );
3428 builder.update(
3429 Price::from("101.00"),
3430 Quantity::from(1),
3431 UnixNanos::from(300),
3432 );
3433 builder.update(
3434 Price::from("200.00"),
3435 Quantity::from(1),
3436 UnixNanos::from(400),
3437 );
3438
3439 let bar = builder.build_now();
3440 assert_eq!(bar.open, Price::from("100.00"));
3441 assert_eq!(bar.high, Price::from("200.00"));
3442 assert_eq!(bar.low, Price::from("100.00"));
3443 assert_eq!(bar.close, Price::from("200.00"));
3444 }
3445
3446 #[rstest]
3447 fn test_bar_builder_build_clamps_low_to_close(equity_aapl: Equity) {
3448 let instrument = InstrumentAny::Equity(equity_aapl);
3452 let bar_type = BarType::new(
3453 instrument.id(),
3454 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3455 AggregationSource::Internal,
3456 );
3457 let mut builder = BarBuilder::new(bar_type, 2, 0);
3458
3459 builder.update(
3460 Price::from("100.00"),
3461 Quantity::from(1),
3462 UnixNanos::from(100),
3463 );
3464 builder.close = Some(Price::from("50.00"));
3465
3466 let bar = builder.build_now();
3467 assert_eq!(bar.low, Price::from("50.00"));
3468 assert_eq!(bar.close, Price::from("50.00"));
3469 assert!(bar.low <= bar.open);
3470 }
3471
3472 #[rstest]
3473 fn test_tick_bar_aggregator_handle_trade_when_step_count_below_threshold(equity_aapl: Equity) {
3474 let instrument = InstrumentAny::Equity(equity_aapl);
3475 let bar_spec = BarSpecification::new(3, BarAggregation::Tick, PriceType::Last);
3476 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3477 let handler = Arc::new(Mutex::new(Vec::new()));
3478 let handler_clone = Arc::clone(&handler);
3479
3480 let mut aggregator = TickBarAggregator::new(
3481 bar_type,
3482 instrument.price_precision(),
3483 instrument.size_precision(),
3484 move |bar: Bar| {
3485 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
3486 handler_guard.push(bar);
3487 },
3488 );
3489
3490 let trade = TradeTick::default();
3491 aggregator.handle_trade(trade);
3492
3493 let handler_guard = handler.lock().expect(MUTEX_POISONED);
3494 assert_eq!(handler_guard.len(), 0);
3495 }
3496
3497 #[rstest]
3498 fn test_tick_bar_aggregator_handle_trade_when_step_count_reached(equity_aapl: Equity) {
3499 let instrument = InstrumentAny::Equity(equity_aapl);
3500 let bar_spec = BarSpecification::new(3, BarAggregation::Tick, PriceType::Last);
3501 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3502 let handler = Arc::new(Mutex::new(Vec::new()));
3503 let handler_clone = Arc::clone(&handler);
3504
3505 let mut aggregator = TickBarAggregator::new(
3506 bar_type,
3507 instrument.price_precision(),
3508 instrument.size_precision(),
3509 move |bar: Bar| {
3510 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
3511 handler_guard.push(bar);
3512 },
3513 );
3514
3515 let trade = TradeTick::default();
3516 aggregator.handle_trade(trade);
3517 aggregator.handle_trade(trade);
3518 aggregator.handle_trade(trade);
3519
3520 let handler_guard = handler.lock().expect(MUTEX_POISONED);
3521 let bar = handler_guard.first().unwrap();
3522 assert_eq!(handler_guard.len(), 1);
3523 assert_eq!(bar.open, trade.price);
3524 assert_eq!(bar.high, trade.price);
3525 assert_eq!(bar.low, trade.price);
3526 assert_eq!(bar.close, trade.price);
3527 assert_eq!(bar.volume, Quantity::from(300000));
3528 assert_eq!(bar.ts_event, trade.ts_event);
3529 assert_eq!(bar.ts_init, trade.ts_init);
3530 }
3531
3532 #[rstest]
3533 fn test_tick_bar_aggregator_aggregates_to_step_size(equity_aapl: Equity) {
3534 let instrument = InstrumentAny::Equity(equity_aapl);
3535 let bar_spec = BarSpecification::new(3, BarAggregation::Tick, PriceType::Last);
3536 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3537 let handler = Arc::new(Mutex::new(Vec::new()));
3538 let handler_clone = Arc::clone(&handler);
3539
3540 let mut aggregator = TickBarAggregator::new(
3541 bar_type,
3542 instrument.price_precision(),
3543 instrument.size_precision(),
3544 move |bar: Bar| {
3545 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
3546 handler_guard.push(bar);
3547 },
3548 );
3549
3550 aggregator.update(
3551 Price::from("1.00001"),
3552 Quantity::from(1),
3553 UnixNanos::default(),
3554 );
3555 aggregator.update(
3556 Price::from("1.00002"),
3557 Quantity::from(1),
3558 UnixNanos::from(1000),
3559 );
3560 aggregator.update(
3561 Price::from("1.00003"),
3562 Quantity::from(1),
3563 UnixNanos::from(2000),
3564 );
3565
3566 let handler_guard = handler.lock().expect(MUTEX_POISONED);
3567 assert_eq!(handler_guard.len(), 1);
3568
3569 let bar = handler_guard.first().unwrap();
3570 assert_eq!(bar.open, Price::from("1.00001"));
3571 assert_eq!(bar.high, Price::from("1.00003"));
3572 assert_eq!(bar.low, Price::from("1.00001"));
3573 assert_eq!(bar.close, Price::from("1.00003"));
3574 assert_eq!(bar.volume, Quantity::from(3));
3575 }
3576
3577 #[rstest]
3578 fn test_tick_bar_aggregator_resets_after_bar_created(equity_aapl: Equity) {
3579 let instrument = InstrumentAny::Equity(equity_aapl);
3580 let bar_spec = BarSpecification::new(2, BarAggregation::Tick, PriceType::Last);
3581 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3582 let handler = Arc::new(Mutex::new(Vec::new()));
3583 let handler_clone = Arc::clone(&handler);
3584
3585 let mut aggregator = TickBarAggregator::new(
3586 bar_type,
3587 instrument.price_precision(),
3588 instrument.size_precision(),
3589 move |bar: Bar| {
3590 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
3591 handler_guard.push(bar);
3592 },
3593 );
3594
3595 aggregator.update(
3596 Price::from("1.00001"),
3597 Quantity::from(1),
3598 UnixNanos::default(),
3599 );
3600 aggregator.update(
3601 Price::from("1.00002"),
3602 Quantity::from(1),
3603 UnixNanos::from(1000),
3604 );
3605 aggregator.update(
3606 Price::from("1.00003"),
3607 Quantity::from(1),
3608 UnixNanos::from(2000),
3609 );
3610 aggregator.update(
3611 Price::from("1.00004"),
3612 Quantity::from(1),
3613 UnixNanos::from(3000),
3614 );
3615
3616 let handler_guard = handler.lock().expect(MUTEX_POISONED);
3617 assert_eq!(handler_guard.len(), 2);
3618
3619 let bar1 = &handler_guard[0];
3620 assert_eq!(bar1.open, Price::from("1.00001"));
3621 assert_eq!(bar1.close, Price::from("1.00002"));
3622 assert_eq!(bar1.volume, Quantity::from(2));
3623
3624 let bar2 = &handler_guard[1];
3625 assert_eq!(bar2.open, Price::from("1.00003"));
3626 assert_eq!(bar2.close, Price::from("1.00004"));
3627 assert_eq!(bar2.volume, Quantity::from(2));
3628 }
3629
3630 #[rstest]
3631 fn test_non_time_bar_aggregators_use_historical_handler(
3632 equity_aapl: Equity,
3633 audusd_sim: CurrencyPair,
3634 ) {
3635 let instrument = InstrumentAny::Equity(equity_aapl);
3636 let instrument_id = instrument.id();
3637 let price_precision = instrument.price_precision();
3638 let size_precision = instrument.size_precision();
3639 let make_sink = |bars: Arc<Mutex<Vec<Bar>>>| {
3640 move |bar: Bar| {
3641 bars.lock().expect(MUTEX_POISONED).push(bar);
3642 }
3643 };
3644 let make_trade = |price: &str, size: i64, ts: u64| TradeTick {
3645 instrument_id,
3646 price: Price::from(price),
3647 size: Quantity::from(size),
3648 aggressor_side: AggressorSide::Buyer,
3649 ts_event: UnixNanos::from(ts),
3650 ts_init: UnixNanos::from(ts),
3651 ..TradeTick::default()
3652 };
3653
3654 macro_rules! assert_historical_sink_receives {
3655 ($name:expr, $aggregator:expr, $update:expr) => {{
3656 let initial_bars = Arc::new(Mutex::new(Vec::new()));
3657 let historical_bars = Arc::new(Mutex::new(Vec::new()));
3658 let mut aggregator = $aggregator(Arc::clone(&initial_bars));
3659 aggregator
3660 .set_historical_mode(true, Box::new(make_sink(Arc::clone(&historical_bars))));
3661 {
3662 let aggregator: &mut dyn BarAggregator = &mut aggregator;
3663 $update(aggregator);
3664 }
3665
3666 assert_eq!(
3667 initial_bars.lock().expect(MUTEX_POISONED).len(),
3668 0,
3669 "{}",
3670 $name,
3671 );
3672 assert_eq!(
3673 historical_bars.lock().expect(MUTEX_POISONED).len(),
3674 1,
3675 "{}",
3676 $name,
3677 );
3678 }};
3679 }
3680
3681 let tick_type = BarType::new(
3682 instrument_id,
3683 BarSpecification::new(1, BarAggregation::Tick, PriceType::Last),
3684 AggregationSource::Internal,
3685 );
3686 assert_historical_sink_receives!(
3687 "TickBarAggregator",
3688 |bars| TickBarAggregator::new(
3689 tick_type,
3690 price_precision,
3691 size_precision,
3692 make_sink(bars)
3693 ),
3694 |aggregator: &mut dyn BarAggregator| {
3695 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3696 }
3697 );
3698
3699 let tick_imbalance_type = BarType::new(
3700 instrument_id,
3701 BarSpecification::new(1, BarAggregation::TickImbalance, PriceType::Last),
3702 AggregationSource::Internal,
3703 );
3704 assert_historical_sink_receives!(
3705 "TickImbalanceBarAggregator",
3706 |bars| TickImbalanceBarAggregator::new(
3707 tick_imbalance_type,
3708 price_precision,
3709 size_precision,
3710 make_sink(bars),
3711 ),
3712 |aggregator: &mut dyn BarAggregator| {
3713 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3714 }
3715 );
3716
3717 let tick_runs_type = BarType::new(
3718 instrument_id,
3719 BarSpecification::new(1, BarAggregation::TickRuns, PriceType::Last),
3720 AggregationSource::Internal,
3721 );
3722 assert_historical_sink_receives!(
3723 "TickRunsBarAggregator",
3724 |bars| TickRunsBarAggregator::new(
3725 tick_runs_type,
3726 price_precision,
3727 size_precision,
3728 make_sink(bars),
3729 ),
3730 |aggregator: &mut dyn BarAggregator| {
3731 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3732 }
3733 );
3734
3735 let volume_type = BarType::new(
3736 instrument_id,
3737 BarSpecification::new(1, BarAggregation::Volume, PriceType::Last),
3738 AggregationSource::Internal,
3739 );
3740 assert_historical_sink_receives!(
3741 "VolumeBarAggregator",
3742 |bars| VolumeBarAggregator::new(
3743 volume_type,
3744 price_precision,
3745 size_precision,
3746 make_sink(bars),
3747 ),
3748 |aggregator: &mut dyn BarAggregator| {
3749 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3750 }
3751 );
3752
3753 let volume_imbalance_type = BarType::new(
3754 instrument_id,
3755 BarSpecification::new(1, BarAggregation::VolumeImbalance, PriceType::Last),
3756 AggregationSource::Internal,
3757 );
3758 assert_historical_sink_receives!(
3759 "VolumeImbalanceBarAggregator",
3760 |bars| VolumeImbalanceBarAggregator::new(
3761 volume_imbalance_type,
3762 price_precision,
3763 size_precision,
3764 make_sink(bars),
3765 ),
3766 |aggregator: &mut dyn BarAggregator| {
3767 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3768 }
3769 );
3770
3771 let volume_runs_type = BarType::new(
3772 instrument_id,
3773 BarSpecification::new(1, BarAggregation::VolumeRuns, PriceType::Last),
3774 AggregationSource::Internal,
3775 );
3776 assert_historical_sink_receives!(
3777 "VolumeRunsBarAggregator",
3778 |bars| VolumeRunsBarAggregator::new(
3779 volume_runs_type,
3780 price_precision,
3781 size_precision,
3782 make_sink(bars),
3783 ),
3784 |aggregator: &mut dyn BarAggregator| {
3785 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3786 }
3787 );
3788
3789 let value_type = BarType::new(
3790 instrument_id,
3791 BarSpecification::new(100, BarAggregation::Value, PriceType::Last),
3792 AggregationSource::Internal,
3793 );
3794 assert_historical_sink_receives!(
3795 "ValueBarAggregator",
3796 |bars| ValueBarAggregator::new(
3797 value_type,
3798 price_precision,
3799 size_precision,
3800 make_sink(bars)
3801 ),
3802 |aggregator: &mut dyn BarAggregator| {
3803 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3804 }
3805 );
3806
3807 let value_imbalance_type = BarType::new(
3808 instrument_id,
3809 BarSpecification::new(100, BarAggregation::ValueImbalance, PriceType::Last),
3810 AggregationSource::Internal,
3811 );
3812 assert_historical_sink_receives!(
3813 "ValueImbalanceBarAggregator",
3814 |bars| ValueImbalanceBarAggregator::new(
3815 value_imbalance_type,
3816 price_precision,
3817 size_precision,
3818 make_sink(bars),
3819 ),
3820 |aggregator: &mut dyn BarAggregator| {
3821 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3822 }
3823 );
3824
3825 let value_runs_type = BarType::new(
3826 instrument_id,
3827 BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last),
3828 AggregationSource::Internal,
3829 );
3830 assert_historical_sink_receives!(
3831 "ValueRunsBarAggregator",
3832 |bars| ValueRunsBarAggregator::new(
3833 value_runs_type,
3834 price_precision,
3835 size_precision,
3836 make_sink(bars),
3837 ),
3838 |aggregator: &mut dyn BarAggregator| {
3839 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3840 }
3841 );
3842
3843 let fx = InstrumentAny::CurrencyPair(audusd_sim);
3844 let renko_type = BarType::new(
3845 fx.id(),
3846 BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid),
3847 AggregationSource::Internal,
3848 );
3849 let fx_price_precision = fx.price_precision();
3850 let fx_size_precision = fx.size_precision();
3851 let fx_price_increment = fx.price_increment();
3852 assert_historical_sink_receives!(
3853 "RenkoBarAggregator",
3854 |bars| RenkoBarAggregator::new(
3855 renko_type,
3856 fx_price_precision,
3857 fx_size_precision,
3858 fx_price_increment,
3859 make_sink(bars),
3860 ),
3861 |aggregator: &mut dyn BarAggregator| {
3862 aggregator.update(
3863 Price::from("1.00000"),
3864 Quantity::from(1),
3865 UnixNanos::from(1_000),
3866 );
3867 aggregator.update(
3868 Price::from("1.00010"),
3869 Quantity::from(1),
3870 UnixNanos::from(2_000),
3871 );
3872 }
3873 );
3874 }
3875
3876 #[rstest]
3877 fn test_tick_imbalance_bar_aggregator_emits_at_threshold(equity_aapl: Equity) {
3878 let instrument = InstrumentAny::Equity(equity_aapl);
3879 let bar_spec = BarSpecification::new(2, BarAggregation::TickImbalance, PriceType::Last);
3880 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3881 let handler = Arc::new(Mutex::new(Vec::new()));
3882 let handler_clone = Arc::clone(&handler);
3883
3884 let mut aggregator = TickImbalanceBarAggregator::new(
3885 bar_type,
3886 instrument.price_precision(),
3887 instrument.size_precision(),
3888 move |bar: Bar| {
3889 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
3890 handler_guard.push(bar);
3891 },
3892 );
3893
3894 let trade = TradeTick::default();
3895 aggregator.handle_trade(trade);
3896 aggregator.handle_trade(trade);
3897
3898 let handler_guard = handler.lock().expect(MUTEX_POISONED);
3899 assert_eq!(handler_guard.len(), 1);
3900 let bar = handler_guard.first().unwrap();
3901 assert_eq!(bar.volume, Quantity::from(200000));
3902 }
3903
3904 #[rstest]
3905 fn test_tick_imbalance_bar_aggregator_handles_seller_direction(equity_aapl: Equity) {
3906 let instrument = InstrumentAny::Equity(equity_aapl);
3907 let bar_spec = BarSpecification::new(1, BarAggregation::TickImbalance, PriceType::Last);
3908 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3909 let handler = Arc::new(Mutex::new(Vec::new()));
3910 let handler_clone = Arc::clone(&handler);
3911
3912 let mut aggregator = TickImbalanceBarAggregator::new(
3913 bar_type,
3914 instrument.price_precision(),
3915 instrument.size_precision(),
3916 move |bar: Bar| {
3917 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
3918 handler_guard.push(bar);
3919 },
3920 );
3921
3922 let sell = TradeTick {
3923 aggressor_side: AggressorSide::Seller,
3924 ..TradeTick::default()
3925 };
3926
3927 aggregator.handle_trade(sell);
3928
3929 let handler_guard = handler.lock().expect(MUTEX_POISONED);
3930 assert_eq!(handler_guard.len(), 1);
3931 }
3932
3933 #[rstest]
3934 fn test_tick_runs_bar_aggregator_resets_on_side_change(equity_aapl: Equity) {
3935 let instrument = InstrumentAny::Equity(equity_aapl);
3936 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
3937 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3938 let handler = Arc::new(Mutex::new(Vec::new()));
3939 let handler_clone = Arc::clone(&handler);
3940
3941 let mut aggregator = TickRunsBarAggregator::new(
3942 bar_type,
3943 instrument.price_precision(),
3944 instrument.size_precision(),
3945 move |bar: Bar| {
3946 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
3947 handler_guard.push(bar);
3948 },
3949 );
3950
3951 let buy = TradeTick::default();
3952 let sell = TradeTick {
3953 aggressor_side: AggressorSide::Seller,
3954 ..buy
3955 };
3956
3957 aggregator.handle_trade(buy);
3958 aggregator.handle_trade(buy);
3959 aggregator.handle_trade(sell);
3960 aggregator.handle_trade(sell);
3961
3962 let handler_guard = handler.lock().expect(MUTEX_POISONED);
3963 assert_eq!(handler_guard.len(), 2);
3964 }
3965
3966 #[rstest]
3967 fn test_tick_runs_bar_aggregator_volume_conservation(equity_aapl: Equity) {
3968 let instrument = InstrumentAny::Equity(equity_aapl);
3969 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
3970 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3971 let handler = Arc::new(Mutex::new(Vec::new()));
3972 let handler_clone = Arc::clone(&handler);
3973
3974 let mut aggregator = TickRunsBarAggregator::new(
3975 bar_type,
3976 instrument.price_precision(),
3977 instrument.size_precision(),
3978 move |bar: Bar| {
3979 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
3980 handler_guard.push(bar);
3981 },
3982 );
3983
3984 let buy = TradeTick {
3985 size: Quantity::from(1),
3986 ..TradeTick::default()
3987 };
3988 let sell = TradeTick {
3989 aggressor_side: AggressorSide::Seller,
3990 size: Quantity::from(1),
3991 ..buy
3992 };
3993
3994 aggregator.handle_trade(buy);
3995 aggregator.handle_trade(buy);
3996 aggregator.handle_trade(sell);
3997 aggregator.handle_trade(sell);
3998
3999 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4000 assert_eq!(handler_guard.len(), 2);
4001 assert_eq!(handler_guard[0].volume, Quantity::from(2));
4002 assert_eq!(handler_guard[1].volume, Quantity::from(2));
4003 }
4004
4005 #[rstest]
4006 fn test_volume_bar_aggregator_builds_multiple_bars_from_large_update(equity_aapl: Equity) {
4007 let instrument = InstrumentAny::Equity(equity_aapl);
4008 let bar_spec = BarSpecification::new(10, BarAggregation::Volume, PriceType::Last);
4009 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4010 let handler = Arc::new(Mutex::new(Vec::new()));
4011 let handler_clone = Arc::clone(&handler);
4012
4013 let mut aggregator = VolumeBarAggregator::new(
4014 bar_type,
4015 instrument.price_precision(),
4016 instrument.size_precision(),
4017 move |bar: Bar| {
4018 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4019 handler_guard.push(bar);
4020 },
4021 );
4022
4023 aggregator.update(
4024 Price::from("1.00001"),
4025 Quantity::from(25),
4026 UnixNanos::default(),
4027 );
4028
4029 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4030 assert_eq!(handler_guard.len(), 2);
4031 let bar1 = &handler_guard[0];
4032 assert_eq!(bar1.volume, Quantity::from(10));
4033 let bar2 = &handler_guard[1];
4034 assert_eq!(bar2.volume, Quantity::from(10));
4035 }
4036
4037 #[rstest]
4038 fn test_volume_bar_aggregator_zero_size_update_is_noop(equity_aapl: Equity) {
4039 let instrument = InstrumentAny::Equity(equity_aapl);
4040 let bar_spec = BarSpecification::new(10, BarAggregation::Volume, PriceType::Last);
4041 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4042 let handler = Arc::new(Mutex::new(Vec::new()));
4043 let handler_clone = Arc::clone(&handler);
4044
4045 let mut aggregator = VolumeBarAggregator::new(
4046 bar_type,
4047 instrument.price_precision(),
4048 instrument.size_precision(),
4049 move |bar: Bar| {
4050 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4051 handler_guard.push(bar);
4052 },
4053 );
4054
4055 aggregator.update(
4056 Price::from("100.00"),
4057 Quantity::from(0),
4058 UnixNanos::default(),
4059 );
4060
4061 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4062 assert_eq!(handler_guard.len(), 0);
4063 }
4064
4065 #[rstest]
4066 fn test_volume_bar_aggregator_ignores_out_of_order_update(equity_aapl: Equity) {
4067 let instrument = InstrumentAny::Equity(equity_aapl);
4068 let bar_spec = BarSpecification::new(2, BarAggregation::Volume, PriceType::Last);
4069 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4070 let handler = Arc::new(Mutex::new(Vec::new()));
4071 let handler_clone = Arc::clone(&handler);
4072
4073 let mut aggregator = VolumeBarAggregator::new(
4074 bar_type,
4075 instrument.price_precision(),
4076 instrument.size_precision(),
4077 move |bar: Bar| {
4078 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4079 handler_guard.push(bar);
4080 },
4081 );
4082
4083 aggregator.update(
4084 Price::from("100.00"),
4085 Quantity::from(1),
4086 UnixNanos::from(1_000),
4087 );
4088 aggregator.update(
4089 Price::from("200.00"),
4090 Quantity::from(3),
4091 UnixNanos::from(500),
4092 );
4093
4094 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4095 assert!(handler_guard.is_empty());
4096 assert_eq!(aggregator.core.builder.count, 1);
4097 assert_eq!(aggregator.core.builder.volume, Quantity::from(1));
4098 assert_eq!(aggregator.core.builder.close, Some(Price::from("100.00")));
4099 assert_eq!(aggregator.core.builder.ts_last, UnixNanos::from(1_000));
4100 }
4101
4102 #[rstest]
4103 fn test_volume_bar_aggregator_ignores_out_of_order_bar(equity_aapl: Equity) {
4104 let instrument = InstrumentAny::Equity(equity_aapl);
4105 let bar_spec = BarSpecification::new(2, BarAggregation::Volume, PriceType::Last);
4106 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4107 let handler = Arc::new(Mutex::new(Vec::new()));
4108 let handler_clone = Arc::clone(&handler);
4109
4110 let mut aggregator = VolumeBarAggregator::new(
4111 bar_type,
4112 instrument.price_precision(),
4113 instrument.size_precision(),
4114 move |bar: Bar| {
4115 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4116 handler_guard.push(bar);
4117 },
4118 );
4119
4120 aggregator.update(
4121 Price::from("100.00"),
4122 Quantity::from(1),
4123 UnixNanos::from(1_000),
4124 );
4125 let stale_bar = Bar::new(
4126 bar_type,
4127 Price::from("200.00"),
4128 Price::from("201.00"),
4129 Price::from("199.00"),
4130 Price::from("200.50"),
4131 Quantity::from(3),
4132 UnixNanos::from(500),
4133 UnixNanos::from(500),
4134 );
4135 aggregator.update_bar(stale_bar, stale_bar.volume, stale_bar.ts_init);
4136
4137 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4138 assert!(handler_guard.is_empty());
4139 assert_eq!(aggregator.core.builder.count, 1);
4140 assert_eq!(aggregator.core.builder.volume, Quantity::from(1));
4141 assert_eq!(aggregator.core.builder.close, Some(Price::from("100.00")));
4142 assert_eq!(aggregator.core.builder.ts_last, UnixNanos::from(1_000));
4143 }
4144
4145 #[rstest]
4146 fn test_volume_imbalance_bar_aggregator_ignores_out_of_order_trade(equity_aapl: Equity) {
4147 let instrument = InstrumentAny::Equity(equity_aapl);
4148 let bar_spec = BarSpecification::new(2, BarAggregation::VolumeImbalance, PriceType::Last);
4149 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4150 let handler = Arc::new(Mutex::new(Vec::new()));
4151 let handler_clone = Arc::clone(&handler);
4152 let mut aggregator = VolumeImbalanceBarAggregator::new(
4153 bar_type,
4154 instrument.price_precision(),
4155 instrument.size_precision(),
4156 move |bar: Bar| {
4157 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
4158 },
4159 );
4160 let first = TradeTick {
4161 price: Price::from("100.00"),
4162 size: Quantity::from(1),
4163 aggressor_side: AggressorSide::Buyer,
4164 ts_init: UnixNanos::from(1_000),
4165 ..TradeTick::default()
4166 };
4167 let stale = TradeTick {
4168 price: Price::from("200.00"),
4169 size: Quantity::from(2),
4170 aggressor_side: AggressorSide::Buyer,
4171 ts_init: UnixNanos::from(500),
4172 ..TradeTick::default()
4173 };
4174
4175 aggregator.handle_trade(first);
4176 aggregator.handle_trade(stale);
4177
4178 assert!(handler.lock().expect(MUTEX_POISONED).is_empty());
4179 assert_eq!(aggregator.imbalance_raw, Quantity::from(1).raw as i128);
4180 assert_eq!(aggregator.core.builder.volume, Quantity::from(1));
4181 assert_eq!(aggregator.core.builder.ts_last, UnixNanos::from(1_000));
4182 }
4183
4184 #[rstest]
4185 fn test_volume_bar_aggregator_exact_threshold_emits_single_bar(equity_aapl: Equity) {
4186 let instrument = InstrumentAny::Equity(equity_aapl);
4187 let bar_spec = BarSpecification::new(10, BarAggregation::Volume, PriceType::Last);
4188 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4189 let handler = Arc::new(Mutex::new(Vec::new()));
4190 let handler_clone = Arc::clone(&handler);
4191
4192 let mut aggregator = VolumeBarAggregator::new(
4193 bar_type,
4194 instrument.price_precision(),
4195 instrument.size_precision(),
4196 move |bar: Bar| {
4197 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4198 handler_guard.push(bar);
4199 },
4200 );
4201
4202 aggregator.update(
4203 Price::from("100.00"),
4204 Quantity::from(7),
4205 UnixNanos::from(1_000),
4206 );
4207 aggregator.update(
4208 Price::from("101.00"),
4209 Quantity::from(3),
4210 UnixNanos::from(2_000),
4211 );
4212
4213 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4214 assert_eq!(handler_guard.len(), 1);
4215 assert_eq!(handler_guard[0].volume, Quantity::from(10));
4216 assert_eq!(handler_guard[0].close, Price::from("101.00"));
4217 }
4218
4219 #[rstest]
4220 fn test_volume_bar_aggregator_step_of_one_emits_per_unit(equity_aapl: Equity) {
4221 let instrument = InstrumentAny::Equity(equity_aapl);
4222 let bar_spec = BarSpecification::new(1, BarAggregation::Volume, PriceType::Last);
4223 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4224 let handler = Arc::new(Mutex::new(Vec::new()));
4225 let handler_clone = Arc::clone(&handler);
4226
4227 let mut aggregator = VolumeBarAggregator::new(
4228 bar_type,
4229 instrument.price_precision(),
4230 instrument.size_precision(),
4231 move |bar: Bar| {
4232 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4233 handler_guard.push(bar);
4234 },
4235 );
4236
4237 aggregator.update(
4238 Price::from("100.00"),
4239 Quantity::from(1),
4240 UnixNanos::default(),
4241 );
4242
4243 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4244 assert_eq!(handler_guard.len(), 1);
4245 assert_eq!(handler_guard[0].volume, Quantity::from(1));
4246 }
4247
4248 #[rstest]
4249 fn test_volume_runs_bar_aggregator_side_change_resets(equity_aapl: Equity) {
4250 let instrument = InstrumentAny::Equity(equity_aapl);
4251 let bar_spec = BarSpecification::new(2, BarAggregation::VolumeRuns, PriceType::Last);
4252 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4253 let handler = Arc::new(Mutex::new(Vec::new()));
4254 let handler_clone = Arc::clone(&handler);
4255
4256 let mut aggregator = VolumeRunsBarAggregator::new(
4257 bar_type,
4258 instrument.price_precision(),
4259 instrument.size_precision(),
4260 move |bar: Bar| {
4261 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4262 handler_guard.push(bar);
4263 },
4264 );
4265
4266 let buy = TradeTick {
4267 instrument_id: instrument.id(),
4268 price: Price::from("1.0"),
4269 size: Quantity::from(1),
4270 ..TradeTick::default()
4271 };
4272 let sell = TradeTick {
4273 aggressor_side: AggressorSide::Seller,
4274 ..buy
4275 };
4276
4277 aggregator.handle_trade(buy);
4278 aggregator.handle_trade(buy); aggregator.handle_trade(sell);
4280 aggregator.handle_trade(sell); let handler_guard = handler.lock().expect(MUTEX_POISONED);
4283 assert!(handler_guard.len() >= 2);
4284 assert!(
4285 (handler_guard[0].volume.as_f64() - handler_guard[1].volume.as_f64()).abs()
4286 < f64::EPSILON
4287 );
4288 }
4289
4290 #[rstest]
4291 fn test_volume_runs_bar_aggregator_handles_large_single_trade(equity_aapl: Equity) {
4292 let instrument = InstrumentAny::Equity(equity_aapl);
4293 let bar_spec = BarSpecification::new(3, BarAggregation::VolumeRuns, PriceType::Last);
4294 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4295 let handler = Arc::new(Mutex::new(Vec::new()));
4296 let handler_clone = Arc::clone(&handler);
4297
4298 let mut aggregator = VolumeRunsBarAggregator::new(
4299 bar_type,
4300 instrument.price_precision(),
4301 instrument.size_precision(),
4302 move |bar: Bar| {
4303 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4304 handler_guard.push(bar);
4305 },
4306 );
4307
4308 let trade = TradeTick {
4309 instrument_id: instrument.id(),
4310 price: Price::from("1.0"),
4311 size: Quantity::from(5),
4312 ..TradeTick::default()
4313 };
4314
4315 aggregator.handle_trade(trade);
4316
4317 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4318 assert!(!handler_guard.is_empty());
4319 assert!(handler_guard[0].volume.as_f64() > 0.0);
4320 assert!(handler_guard[0].volume.as_f64() < trade.size.as_f64());
4321 }
4322
4323 #[rstest]
4324 fn test_volume_imbalance_bar_aggregator_splits_large_trade(equity_aapl: Equity) {
4325 let instrument = InstrumentAny::Equity(equity_aapl);
4326 let bar_spec = BarSpecification::new(2, BarAggregation::VolumeImbalance, PriceType::Last);
4327 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4328 let handler = Arc::new(Mutex::new(Vec::new()));
4329 let handler_clone = Arc::clone(&handler);
4330
4331 let mut aggregator = VolumeImbalanceBarAggregator::new(
4332 bar_type,
4333 instrument.price_precision(),
4334 instrument.size_precision(),
4335 move |bar: Bar| {
4336 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4337 handler_guard.push(bar);
4338 },
4339 );
4340
4341 let trade_small = TradeTick {
4342 instrument_id: instrument.id(),
4343 price: Price::from("1.0"),
4344 size: Quantity::from(1),
4345 ..TradeTick::default()
4346 };
4347 let trade_large = TradeTick {
4348 size: Quantity::from(3),
4349 ..trade_small
4350 };
4351
4352 aggregator.handle_trade(trade_small);
4353 aggregator.handle_trade(trade_large);
4354
4355 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4356 assert_eq!(handler_guard.len(), 2);
4357 let total_output = handler_guard
4358 .iter()
4359 .map(|bar| bar.volume.as_f64())
4360 .sum::<f64>();
4361 let total_input = trade_small.size.as_f64() + trade_large.size.as_f64();
4362 assert!((total_output - total_input).abs() < f64::EPSILON);
4363 }
4364
4365 #[rstest]
4366 fn test_value_bar_aggregator_builds_at_value_threshold(equity_aapl: Equity) {
4367 let instrument = InstrumentAny::Equity(equity_aapl);
4368 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4370 let handler = Arc::new(Mutex::new(Vec::new()));
4371 let handler_clone = Arc::clone(&handler);
4372
4373 let mut aggregator = ValueBarAggregator::new(
4374 bar_type,
4375 instrument.price_precision(),
4376 instrument.size_precision(),
4377 move |bar: Bar| {
4378 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4379 handler_guard.push(bar);
4380 },
4381 );
4382
4383 aggregator.update(
4385 Price::from("100.00"),
4386 Quantity::from(5),
4387 UnixNanos::default(),
4388 );
4389 aggregator.update(
4390 Price::from("100.00"),
4391 Quantity::from(5),
4392 UnixNanos::from(1000),
4393 );
4394
4395 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4396 assert_eq!(handler_guard.len(), 1);
4397 let bar = handler_guard.first().unwrap();
4398 assert_eq!(bar.volume, Quantity::from(10));
4399 }
4400
4401 #[rstest]
4402 fn test_value_bar_aggregator_handles_large_update(equity_aapl: Equity) {
4403 let instrument = InstrumentAny::Equity(equity_aapl);
4404 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last);
4405 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4406 let handler = Arc::new(Mutex::new(Vec::new()));
4407 let handler_clone = Arc::clone(&handler);
4408
4409 let mut aggregator = ValueBarAggregator::new(
4410 bar_type,
4411 instrument.price_precision(),
4412 instrument.size_precision(),
4413 move |bar: Bar| {
4414 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4415 handler_guard.push(bar);
4416 },
4417 );
4418
4419 aggregator.update(
4421 Price::from("100.00"),
4422 Quantity::from(25),
4423 UnixNanos::default(),
4424 );
4425
4426 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4427 assert_eq!(handler_guard.len(), 2);
4428 let remaining_value = aggregator.get_cumulative_value();
4429 assert!(remaining_value < Decimal::from(1_000)); }
4431
4432 #[rstest]
4433 fn test_value_bar_aggregator_handles_zero_price(equity_aapl: Equity) {
4434 let instrument = InstrumentAny::Equity(equity_aapl);
4435 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last);
4436 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4437 let handler = Arc::new(Mutex::new(Vec::new()));
4438 let handler_clone = Arc::clone(&handler);
4439
4440 let mut aggregator = ValueBarAggregator::new(
4441 bar_type,
4442 instrument.price_precision(),
4443 instrument.size_precision(),
4444 move |bar: Bar| {
4445 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4446 handler_guard.push(bar);
4447 },
4448 );
4449
4450 aggregator.update(
4452 Price::from("0.00"),
4453 Quantity::from(100),
4454 UnixNanos::default(),
4455 );
4456
4457 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4459 assert_eq!(handler_guard.len(), 0);
4460
4461 assert_eq!(aggregator.get_cumulative_value(), Decimal::ZERO);
4463 }
4464
4465 #[rstest]
4466 fn test_value_bar_aggregator_handles_zero_size(equity_aapl: Equity) {
4467 let instrument = InstrumentAny::Equity(equity_aapl);
4468 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last);
4469 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4470 let handler = Arc::new(Mutex::new(Vec::new()));
4471 let handler_clone = Arc::clone(&handler);
4472
4473 let mut aggregator = ValueBarAggregator::new(
4474 bar_type,
4475 instrument.price_precision(),
4476 instrument.size_precision(),
4477 move |bar: Bar| {
4478 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4479 handler_guard.push(bar);
4480 },
4481 );
4482
4483 aggregator.update(
4485 Price::from("100.00"),
4486 Quantity::from(0),
4487 UnixNanos::default(),
4488 );
4489
4490 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4492 assert_eq!(handler_guard.len(), 0);
4493
4494 assert_eq!(aggregator.get_cumulative_value(), Decimal::ZERO);
4496 }
4497
4498 #[rstest]
4499 fn test_value_bar_aggregator_conserves_volume_across_rounded_chunks(equity_aapl: Equity) {
4500 let instrument = InstrumentAny::Equity(equity_aapl);
4501 let bar_spec = BarSpecification::new(10, BarAggregation::Value, PriceType::Last);
4502 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4503 let handler = Arc::new(Mutex::new(Vec::new()));
4504 let handler_clone = Arc::clone(&handler);
4505
4506 let mut aggregator = ValueBarAggregator::new(
4507 bar_type,
4508 instrument.price_precision(),
4509 instrument.size_precision(),
4510 move |bar: Bar| {
4511 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4512 handler_guard.push(bar);
4513 },
4514 );
4515
4516 aggregator.update(
4519 Price::from("3.00"),
4520 Quantity::from(10),
4521 UnixNanos::from(1_000),
4522 );
4523
4524 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4525 assert_eq!(handler_guard.len(), 3);
4526 for bar in handler_guard.iter() {
4527 assert_eq!(bar.volume, Quantity::from(3));
4528 }
4529 assert_eq!(aggregator.core.builder.volume, Quantity::from(1));
4530 }
4531
4532 #[rstest]
4533 fn test_value_bar_aggregator_update_bar_conserves_volume_across_rounded_chunks(
4534 equity_aapl: Equity,
4535 ) {
4536 let instrument = InstrumentAny::Equity(equity_aapl);
4537 let bar_spec = BarSpecification::new(10, BarAggregation::Value, PriceType::Last);
4538 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4539 let handler = Arc::new(Mutex::new(Vec::new()));
4540 let handler_clone = Arc::clone(&handler);
4541
4542 let mut aggregator = ValueBarAggregator::new(
4543 bar_type,
4544 instrument.price_precision(),
4545 instrument.size_precision(),
4546 move |bar: Bar| {
4547 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4548 handler_guard.push(bar);
4549 },
4550 );
4551
4552 let input_bar = Bar::new(
4554 bar_type,
4555 Price::from("3.00"),
4556 Price::from("3.00"),
4557 Price::from("3.00"),
4558 Price::from("3.00"),
4559 Quantity::from(10),
4560 UnixNanos::from(1_000),
4561 UnixNanos::from(1_000),
4562 );
4563 aggregator.handle_bar(input_bar);
4564
4565 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4566 assert_eq!(handler_guard.len(), 3);
4567 for bar in handler_guard.iter() {
4568 assert_eq!(bar.volume, Quantity::from(3));
4569 }
4570 assert_eq!(aggregator.core.builder.volume, Quantity::from(1));
4571 }
4572
4573 #[rstest]
4574 fn test_value_bar_aggregator_exact_threshold_emits_one_bar(equity_aapl: Equity) {
4575 let instrument = InstrumentAny::Equity(equity_aapl);
4576 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last);
4577 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4578 let handler = Arc::new(Mutex::new(Vec::new()));
4579 let handler_clone = Arc::clone(&handler);
4580
4581 let mut aggregator = ValueBarAggregator::new(
4582 bar_type,
4583 instrument.price_precision(),
4584 instrument.size_precision(),
4585 move |bar: Bar| {
4586 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4587 handler_guard.push(bar);
4588 },
4589 );
4590
4591 aggregator.update(
4592 Price::from("100.00"),
4593 Quantity::from(5),
4594 UnixNanos::from(1_000),
4595 );
4596 aggregator.update(
4597 Price::from("100.00"),
4598 Quantity::from(5),
4599 UnixNanos::from(2_000),
4600 );
4601
4602 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4603 assert_eq!(handler_guard.len(), 1);
4604 assert_eq!(handler_guard[0].volume, Quantity::from(10));
4605 assert_eq!(aggregator.get_cumulative_value(), Decimal::ZERO);
4606 }
4607
4608 #[rstest]
4609 fn test_value_bar_aggregator_precision_boundary_min_size_clamp(equity_aapl: Equity) {
4610 let instrument = InstrumentAny::Equity(equity_aapl);
4614 let bar_spec = BarSpecification::new(100, BarAggregation::Value, PriceType::Last);
4615 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4616 let handler = Arc::new(Mutex::new(Vec::new()));
4617 let handler_clone = Arc::clone(&handler);
4618
4619 let mut aggregator = ValueBarAggregator::new(
4620 bar_type,
4621 instrument.price_precision(),
4622 instrument.size_precision(),
4623 move |bar: Bar| {
4624 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4625 handler_guard.push(bar);
4626 },
4627 );
4628
4629 aggregator.update(
4631 Price::from("100.00"),
4632 Quantity::from(4),
4633 UnixNanos::default(),
4634 );
4635
4636 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4637 assert_eq!(handler_guard.len(), 4);
4638 for bar in handler_guard.iter() {
4639 assert_eq!(bar.volume, Quantity::from(1));
4640 }
4641 }
4642
4643 #[rstest]
4644 fn test_value_imbalance_bar_aggregator_emits_on_opposing_overflow(equity_aapl: Equity) {
4645 let instrument = InstrumentAny::Equity(equity_aapl);
4646 let bar_spec = BarSpecification::new(10, BarAggregation::ValueImbalance, PriceType::Last);
4647 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4648 let handler = Arc::new(Mutex::new(Vec::new()));
4649 let handler_clone = Arc::clone(&handler);
4650
4651 let mut aggregator = ValueImbalanceBarAggregator::new(
4652 bar_type,
4653 instrument.price_precision(),
4654 instrument.size_precision(),
4655 move |bar: Bar| {
4656 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4657 handler_guard.push(bar);
4658 },
4659 );
4660
4661 let buy = TradeTick {
4662 price: Price::from("5.0"),
4663 size: Quantity::from(2), instrument_id: instrument.id(),
4665 ..TradeTick::default()
4666 };
4667 let sell = TradeTick {
4668 price: Price::from("5.0"),
4669 size: Quantity::from(2), aggressor_side: AggressorSide::Seller,
4671 instrument_id: instrument.id(),
4672 ..buy
4673 };
4674
4675 aggregator.handle_trade(buy);
4676 aggregator.handle_trade(sell);
4677
4678 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4679 assert_eq!(handler_guard.len(), 2);
4680 }
4681
4682 #[rstest]
4683 fn test_value_runs_bar_aggregator_emits_on_consecutive_side(equity_aapl: Equity) {
4684 let instrument = InstrumentAny::Equity(equity_aapl);
4685 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
4686 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4687 let handler = Arc::new(Mutex::new(Vec::new()));
4688 let handler_clone = Arc::clone(&handler);
4689
4690 let mut aggregator = ValueRunsBarAggregator::new(
4691 bar_type,
4692 instrument.price_precision(),
4693 instrument.size_precision(),
4694 move |bar: Bar| {
4695 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4696 handler_guard.push(bar);
4697 },
4698 );
4699
4700 let trade = TradeTick {
4701 price: Price::from("10.0"),
4702 size: Quantity::from(5),
4703 instrument_id: instrument.id(),
4704 ..TradeTick::default()
4705 };
4706
4707 aggregator.handle_trade(trade);
4708 aggregator.handle_trade(trade);
4709
4710 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4711 assert_eq!(handler_guard.len(), 1);
4712 let bar = handler_guard.first().unwrap();
4713 assert_eq!(bar.volume, Quantity::from(10));
4714 }
4715
4716 #[rstest]
4717 fn test_value_runs_bar_aggregator_resets_on_side_change(equity_aapl: Equity) {
4718 let instrument = InstrumentAny::Equity(equity_aapl);
4719 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
4720 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4721 let handler = Arc::new(Mutex::new(Vec::new()));
4722 let handler_clone = Arc::clone(&handler);
4723
4724 let mut aggregator = ValueRunsBarAggregator::new(
4725 bar_type,
4726 instrument.price_precision(),
4727 instrument.size_precision(),
4728 move |bar: Bar| {
4729 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4730 handler_guard.push(bar);
4731 },
4732 );
4733
4734 let buy = TradeTick {
4735 price: Price::from("10.0"),
4736 size: Quantity::from(5),
4737 instrument_id: instrument.id(),
4738 ..TradeTick::default()
4739 }; let sell = TradeTick {
4741 price: Price::from("10.0"),
4742 size: Quantity::from(10),
4743 aggressor_side: AggressorSide::Seller,
4744 ..buy
4745 }; aggregator.handle_trade(buy);
4748 aggregator.handle_trade(sell);
4749
4750 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4751 assert_eq!(handler_guard.len(), 1);
4752 assert_eq!(handler_guard[0].volume, Quantity::from(10));
4753 }
4754
4755 #[rstest]
4756 fn test_tick_runs_bar_aggregator_continues_run_after_bar_emission(equity_aapl: Equity) {
4757 let instrument = InstrumentAny::Equity(equity_aapl);
4758 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
4759 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4760 let handler = Arc::new(Mutex::new(Vec::new()));
4761 let handler_clone = Arc::clone(&handler);
4762
4763 let mut aggregator = TickRunsBarAggregator::new(
4764 bar_type,
4765 instrument.price_precision(),
4766 instrument.size_precision(),
4767 move |bar: Bar| {
4768 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4769 handler_guard.push(bar);
4770 },
4771 );
4772
4773 let buy = TradeTick::default();
4774
4775 aggregator.handle_trade(buy);
4776 aggregator.handle_trade(buy); aggregator.handle_trade(buy); aggregator.handle_trade(buy); let handler_guard = handler.lock().expect(MUTEX_POISONED);
4781 assert_eq!(handler_guard.len(), 2);
4782 }
4783
4784 #[rstest]
4785 fn test_tick_runs_bar_aggregator_handles_no_aggressor_trades(equity_aapl: Equity) {
4786 let instrument = InstrumentAny::Equity(equity_aapl);
4787 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
4788 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4789 let handler = Arc::new(Mutex::new(Vec::new()));
4790 let handler_clone = Arc::clone(&handler);
4791
4792 let mut aggregator = TickRunsBarAggregator::new(
4793 bar_type,
4794 instrument.price_precision(),
4795 instrument.size_precision(),
4796 move |bar: Bar| {
4797 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4798 handler_guard.push(bar);
4799 },
4800 );
4801
4802 let buy = TradeTick::default();
4803 let no_aggressor = TradeTick {
4804 aggressor_side: AggressorSide::NoAggressor,
4805 ..buy
4806 };
4807
4808 aggregator.handle_trade(buy);
4809 aggregator.handle_trade(no_aggressor); aggregator.handle_trade(no_aggressor); aggregator.handle_trade(buy); let handler_guard = handler.lock().expect(MUTEX_POISONED);
4814 assert_eq!(handler_guard.len(), 1);
4815 }
4816
4817 #[rstest]
4818 fn test_volume_runs_bar_aggregator_continues_run_after_bar_emission(equity_aapl: Equity) {
4819 let instrument = InstrumentAny::Equity(equity_aapl);
4820 let bar_spec = BarSpecification::new(2, BarAggregation::VolumeRuns, PriceType::Last);
4821 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4822 let handler = Arc::new(Mutex::new(Vec::new()));
4823 let handler_clone = Arc::clone(&handler);
4824
4825 let mut aggregator = VolumeRunsBarAggregator::new(
4826 bar_type,
4827 instrument.price_precision(),
4828 instrument.size_precision(),
4829 move |bar: Bar| {
4830 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4831 handler_guard.push(bar);
4832 },
4833 );
4834
4835 let buy = TradeTick {
4836 instrument_id: instrument.id(),
4837 price: Price::from("1.0"),
4838 size: Quantity::from(1),
4839 ..TradeTick::default()
4840 };
4841
4842 aggregator.handle_trade(buy);
4843 aggregator.handle_trade(buy); aggregator.handle_trade(buy); aggregator.handle_trade(buy); let handler_guard = handler.lock().expect(MUTEX_POISONED);
4848 assert_eq!(handler_guard.len(), 2);
4849 assert_eq!(handler_guard[0].volume, Quantity::from(2));
4850 assert_eq!(handler_guard[1].volume, Quantity::from(2));
4851 }
4852
4853 #[rstest]
4854 fn test_value_runs_bar_aggregator_continues_run_after_bar_emission(equity_aapl: Equity) {
4855 let instrument = InstrumentAny::Equity(equity_aapl);
4856 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
4857 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4858 let handler = Arc::new(Mutex::new(Vec::new()));
4859 let handler_clone = Arc::clone(&handler);
4860
4861 let mut aggregator = ValueRunsBarAggregator::new(
4862 bar_type,
4863 instrument.price_precision(),
4864 instrument.size_precision(),
4865 move |bar: Bar| {
4866 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4867 handler_guard.push(bar);
4868 },
4869 );
4870
4871 let buy = TradeTick {
4872 instrument_id: instrument.id(),
4873 price: Price::from("10.0"),
4874 size: Quantity::from(5),
4875 ..TradeTick::default()
4876 }; aggregator.handle_trade(buy);
4879 aggregator.handle_trade(buy); aggregator.handle_trade(buy); aggregator.handle_trade(buy); let handler_guard = handler.lock().expect(MUTEX_POISONED);
4884 assert_eq!(handler_guard.len(), 2);
4885 assert_eq!(handler_guard[0].volume, Quantity::from(10));
4886 assert_eq!(handler_guard[1].volume, Quantity::from(10));
4887 }
4888
4889 #[rstest]
4890 fn test_time_bar_aggregator_builds_at_interval(equity_aapl: Equity) {
4891 let instrument = InstrumentAny::Equity(equity_aapl);
4892 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
4894 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4895 let handler = Arc::new(Mutex::new(Vec::new()));
4896 let handler_clone = Arc::clone(&handler);
4897 let clock = Rc::new(RefCell::new(TestClock::new()));
4898
4899 let mut aggregator = TimeBarAggregator::new(
4900 bar_type,
4901 instrument.price_precision(),
4902 instrument.size_precision(),
4903 clock.clone(),
4904 move |bar: Bar| {
4905 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4906 handler_guard.push(bar);
4907 },
4908 true, false, BarIntervalType::LeftOpen,
4911 None, 15, false, );
4915
4916 aggregator.update(
4917 Price::from("100.00"),
4918 Quantity::from(1),
4919 UnixNanos::default(),
4920 );
4921
4922 let next_sec = UnixNanos::from(1_000_000_000);
4923 clock.borrow_mut().set_time(next_sec);
4924
4925 let event = TimeEvent::new(
4926 Ustr::from("1-SECOND-LAST"),
4927 UUID4::new(),
4928 next_sec,
4929 next_sec,
4930 );
4931 aggregator.build_bar(&event);
4932
4933 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4934 assert_eq!(handler_guard.len(), 1);
4935 let bar = handler_guard.first().unwrap();
4936 assert_eq!(bar.ts_event, UnixNanos::default());
4937 assert_eq!(bar.ts_init, next_sec);
4938 }
4939
4940 #[rstest]
4941 fn test_time_bar_aggregator_stop_clears_timer_and_allows_restart(equity_aapl: Equity) {
4942 let instrument = InstrumentAny::Equity(equity_aapl);
4943 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
4944 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4945 let timer_name = format!("TIME_BAR_{bar_type}");
4946 let clock = Rc::new(RefCell::new(TestClock::new()));
4947
4948 let aggregator = TimeBarAggregator::new(
4949 bar_type,
4950 instrument.price_precision(),
4951 instrument.size_precision(),
4952 clock.clone(),
4953 |_bar: Bar| {},
4954 true,
4955 false,
4956 BarIntervalType::LeftOpen,
4957 None,
4958 15,
4959 false,
4960 );
4961
4962 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
4963 let rc = Rc::new(RefCell::new(boxed));
4964
4965 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
4966 assert_eq!(clock.borrow().timer_names(), vec![timer_name.as_str()]);
4967
4968 rc.borrow_mut().stop();
4969 assert!(clock.borrow().timer_names().is_empty());
4970
4971 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
4972 assert_eq!(clock.borrow().timer_names(), vec![timer_name.as_str()]);
4973 }
4974
4975 #[rstest]
4976 fn test_time_bar_aggregator_left_open_interval(equity_aapl: Equity) {
4977 let instrument = InstrumentAny::Equity(equity_aapl);
4978 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
4979 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4980 let handler = Arc::new(Mutex::new(Vec::new()));
4981 let handler_clone = Arc::clone(&handler);
4982 let clock = Rc::new(RefCell::new(TestClock::new()));
4983
4984 let mut aggregator = TimeBarAggregator::new(
4985 bar_type,
4986 instrument.price_precision(),
4987 instrument.size_precision(),
4988 clock.clone(),
4989 move |bar: Bar| {
4990 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4991 handler_guard.push(bar);
4992 },
4993 true, true, BarIntervalType::LeftOpen,
4996 None,
4997 15,
4998 false, );
5000
5001 aggregator.update(
5003 Price::from("100.00"),
5004 Quantity::from(1),
5005 UnixNanos::default(),
5006 );
5007
5008 let ts1 = UnixNanos::from(1_000_000_000);
5010 clock.borrow_mut().set_time(ts1);
5011 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts1, ts1);
5012 aggregator.build_bar(&event);
5013
5014 aggregator.update(Price::from("101.00"), Quantity::from(1), ts1);
5016
5017 let ts2 = UnixNanos::from(2_000_000_000);
5019 clock.borrow_mut().set_time(ts2);
5020 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts2, ts2);
5021 aggregator.build_bar(&event);
5022
5023 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5024 assert_eq!(handler_guard.len(), 2);
5025
5026 let bar1 = &handler_guard[0];
5027 assert_eq!(bar1.ts_event, ts1); assert_eq!(bar1.ts_init, ts1);
5029 assert_eq!(bar1.close, Price::from("100.00"));
5030 let bar2 = &handler_guard[1];
5031 assert_eq!(bar2.ts_event, ts2);
5032 assert_eq!(bar2.ts_init, ts2);
5033 assert_eq!(bar2.close, Price::from("101.00"));
5034 }
5035
5036 #[rstest]
5037 fn test_time_bar_aggregator_right_open_interval(equity_aapl: Equity) {
5038 let instrument = InstrumentAny::Equity(equity_aapl);
5039 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
5040 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5041 let handler = Arc::new(Mutex::new(Vec::new()));
5042 let handler_clone = Arc::clone(&handler);
5043 let clock = Rc::new(RefCell::new(TestClock::new()));
5044 let mut aggregator = TimeBarAggregator::new(
5045 bar_type,
5046 instrument.price_precision(),
5047 instrument.size_precision(),
5048 clock.clone(),
5049 move |bar: Bar| {
5050 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5051 handler_guard.push(bar);
5052 },
5053 true, true, BarIntervalType::RightOpen,
5056 None,
5057 15,
5058 false, );
5060
5061 aggregator.update(
5063 Price::from("100.00"),
5064 Quantity::from(1),
5065 UnixNanos::default(),
5066 );
5067
5068 let ts1 = UnixNanos::from(1_000_000_000);
5070 clock.borrow_mut().set_time(ts1);
5071 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts1, ts1);
5072 aggregator.build_bar(&event);
5073
5074 aggregator.update(Price::from("101.00"), Quantity::from(1), ts1);
5076
5077 let ts2 = UnixNanos::from(2_000_000_000);
5079 clock.borrow_mut().set_time(ts2);
5080 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts2, ts2);
5081 aggregator.build_bar(&event);
5082
5083 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5084 assert_eq!(handler_guard.len(), 2);
5085
5086 let bar1 = &handler_guard[0];
5087 assert_eq!(bar1.ts_event, UnixNanos::default()); assert_eq!(bar1.ts_init, ts1);
5089 assert_eq!(bar1.close, Price::from("100.00"));
5090
5091 let bar2 = &handler_guard[1];
5092 assert_eq!(bar2.ts_event, ts1);
5093 assert_eq!(bar2.ts_init, ts2);
5094 assert_eq!(bar2.close, Price::from("101.00"));
5095 }
5096
5097 #[rstest]
5098 fn test_time_bar_aggregator_no_updates_behavior(equity_aapl: Equity) {
5099 let instrument = InstrumentAny::Equity(equity_aapl);
5100 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
5101 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5102 let handler = Arc::new(Mutex::new(Vec::new()));
5103 let handler_clone = Arc::clone(&handler);
5104 let clock = Rc::new(RefCell::new(TestClock::new()));
5105
5106 let mut aggregator = TimeBarAggregator::new(
5108 bar_type,
5109 instrument.price_precision(),
5110 instrument.size_precision(),
5111 clock.clone(),
5112 move |bar: Bar| {
5113 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5114 handler_guard.push(bar);
5115 },
5116 false, true, BarIntervalType::LeftOpen,
5119 None,
5120 15,
5121 false, );
5123
5124 let ts1 = UnixNanos::from(1_000_000_000);
5126 clock.borrow_mut().set_time(ts1);
5127 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts1, ts1);
5128 aggregator.build_bar(&event);
5129
5130 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5131 assert_eq!(handler_guard.len(), 0); drop(handler_guard);
5133
5134 let handler = Arc::new(Mutex::new(Vec::new()));
5136 let handler_clone = Arc::clone(&handler);
5137 let mut aggregator = TimeBarAggregator::new(
5138 bar_type,
5139 instrument.price_precision(),
5140 instrument.size_precision(),
5141 clock.clone(),
5142 move |bar: Bar| {
5143 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5144 handler_guard.push(bar);
5145 },
5146 true, true, BarIntervalType::LeftOpen,
5149 None,
5150 15,
5151 false, );
5153
5154 aggregator.update(
5155 Price::from("100.00"),
5156 Quantity::from(1),
5157 UnixNanos::default(),
5158 );
5159
5160 let ts1 = UnixNanos::from(1_000_000_000);
5162 clock.borrow_mut().set_time(ts1);
5163 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts1, ts1);
5164 aggregator.build_bar(&event);
5165
5166 let ts2 = UnixNanos::from(2_000_000_000);
5168 clock.borrow_mut().set_time(ts2);
5169 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts2, ts2);
5170 aggregator.build_bar(&event);
5171
5172 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5173 assert_eq!(handler_guard.len(), 2); let bar1 = &handler_guard[0];
5175 assert_eq!(bar1.close, Price::from("100.00"));
5176 let bar2 = &handler_guard[1];
5177 assert_eq!(bar2.close, Price::from("100.00")); }
5179
5180 #[rstest]
5181 fn test_time_bar_aggregator_respects_timestamp_on_close(equity_aapl: Equity) {
5182 let instrument = InstrumentAny::Equity(equity_aapl);
5183 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
5184 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5185 let clock = Rc::new(RefCell::new(TestClock::new()));
5186 let handler = Arc::new(Mutex::new(Vec::new()));
5187 let handler_clone = Arc::clone(&handler);
5188
5189 let mut aggregator = TimeBarAggregator::new(
5190 bar_type,
5191 instrument.price_precision(),
5192 instrument.size_precision(),
5193 clock.clone(),
5194 move |bar: Bar| {
5195 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5196 handler_guard.push(bar);
5197 },
5198 true, true, BarIntervalType::RightOpen,
5201 None,
5202 15,
5203 false, );
5205
5206 let ts1 = UnixNanos::from(1_000_000_000);
5207 aggregator.update(Price::from("100.00"), Quantity::from(1), ts1);
5208
5209 let ts2 = UnixNanos::from(2_000_000_000);
5210 clock.borrow_mut().set_time(ts2);
5211
5212 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts2, ts2);
5214 aggregator.build_bar(&event);
5215
5216 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5217 let bar = handler_guard.first().unwrap();
5218 assert_eq!(bar.ts_event, UnixNanos::default());
5219 assert_eq!(bar.ts_init, ts2);
5220 }
5221
5222 #[rstest]
5223 fn test_renko_bar_aggregator_initialization(audusd_sim: CurrencyPair) {
5224 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5225 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5227 let handler = Arc::new(Mutex::new(Vec::new()));
5228 let handler_clone = Arc::clone(&handler);
5229
5230 let aggregator = RenkoBarAggregator::new(
5231 bar_type,
5232 instrument.price_precision(),
5233 instrument.size_precision(),
5234 instrument.price_increment(),
5235 move |bar: Bar| {
5236 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5237 handler_guard.push(bar);
5238 },
5239 );
5240
5241 assert_eq!(aggregator.bar_type(), bar_type);
5242 assert!(!aggregator.is_running());
5243 let expected_brick_size = 10 * instrument.price_increment().raw;
5245 assert_eq!(aggregator.brick_size, expected_brick_size);
5246 }
5247
5248 #[rstest]
5249 fn test_renko_bar_aggregator_update_below_brick_size_no_bar(audusd_sim: CurrencyPair) {
5250 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5251 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5253 let handler = Arc::new(Mutex::new(Vec::new()));
5254 let handler_clone = Arc::clone(&handler);
5255
5256 let mut aggregator = RenkoBarAggregator::new(
5257 bar_type,
5258 instrument.price_precision(),
5259 instrument.size_precision(),
5260 instrument.price_increment(),
5261 move |bar: Bar| {
5262 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5263 handler_guard.push(bar);
5264 },
5265 );
5266
5267 aggregator.update(
5269 Price::from("1.00000"),
5270 Quantity::from(1),
5271 UnixNanos::default(),
5272 );
5273 aggregator.update(
5274 Price::from("1.00005"),
5275 Quantity::from(1),
5276 UnixNanos::from(1000),
5277 );
5278
5279 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5280 assert_eq!(handler_guard.len(), 0); }
5282
5283 #[rstest]
5284 fn test_renko_bar_aggregator_ignores_out_of_order_bar(audusd_sim: CurrencyPair) {
5285 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5286 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid);
5287 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5288 let handler = Arc::new(Mutex::new(Vec::new()));
5289 let handler_clone = Arc::clone(&handler);
5290 let mut aggregator = RenkoBarAggregator::new(
5291 bar_type,
5292 instrument.price_precision(),
5293 instrument.size_precision(),
5294 instrument.price_increment(),
5295 move |bar: Bar| {
5296 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
5297 },
5298 );
5299 let first = Bar::new(
5300 bar_type,
5301 Price::from("1.00000"),
5302 Price::from("1.00000"),
5303 Price::from("1.00000"),
5304 Price::from("1.00000"),
5305 Quantity::from(1),
5306 UnixNanos::from(1_000),
5307 UnixNanos::from(1_000),
5308 );
5309 let stale = Bar::new(
5310 bar_type,
5311 Price::from("1.00020"),
5312 Price::from("1.00020"),
5313 Price::from("1.00020"),
5314 Price::from("1.00020"),
5315 Quantity::from(1),
5316 UnixNanos::from(500),
5317 UnixNanos::from(500),
5318 );
5319
5320 aggregator.update_bar(first, first.volume, first.ts_init);
5321 aggregator.update_bar(stale, stale.volume, stale.ts_init);
5322
5323 assert!(handler.lock().expect(MUTEX_POISONED).is_empty());
5324 assert_eq!(aggregator.last_close, Some(Price::from("1.00000")));
5325 assert_eq!(aggregator.core.builder.ts_last, UnixNanos::from(1_000));
5326 }
5327
5328 #[rstest]
5329 fn test_renko_bar_aggregator_update_exceeds_brick_size_creates_bar(audusd_sim: CurrencyPair) {
5330 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5331 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5333 let handler = Arc::new(Mutex::new(Vec::new()));
5334 let handler_clone = Arc::clone(&handler);
5335
5336 let mut aggregator = RenkoBarAggregator::new(
5337 bar_type,
5338 instrument.price_precision(),
5339 instrument.size_precision(),
5340 instrument.price_increment(),
5341 move |bar: Bar| {
5342 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5343 handler_guard.push(bar);
5344 },
5345 );
5346
5347 aggregator.update(
5349 Price::from("1.00000"),
5350 Quantity::from(1),
5351 UnixNanos::default(),
5352 );
5353 aggregator.update(
5354 Price::from("1.00015"),
5355 Quantity::from(1),
5356 UnixNanos::from(1000),
5357 );
5358
5359 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5360 assert_eq!(handler_guard.len(), 1);
5361
5362 let bar = handler_guard.first().unwrap();
5363 assert_eq!(bar.open, Price::from("1.00000"));
5364 assert_eq!(bar.high, Price::from("1.00010"));
5365 assert_eq!(bar.low, Price::from("1.00000"));
5366 assert_eq!(bar.close, Price::from("1.00010"));
5367 assert_eq!(bar.volume, Quantity::from(2));
5368 assert_eq!(bar.ts_event, UnixNanos::from(1000));
5369 assert_eq!(bar.ts_init, UnixNanos::from(1000));
5370 }
5371
5372 #[rstest]
5373 fn test_renko_bar_aggregator_multiple_bricks_in_one_update(audusd_sim: CurrencyPair) {
5374 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5375 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5377 let handler = Arc::new(Mutex::new(Vec::new()));
5378 let handler_clone = Arc::clone(&handler);
5379
5380 let mut aggregator = RenkoBarAggregator::new(
5381 bar_type,
5382 instrument.price_precision(),
5383 instrument.size_precision(),
5384 instrument.price_increment(),
5385 move |bar: Bar| {
5386 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5387 handler_guard.push(bar);
5388 },
5389 );
5390
5391 aggregator.update(
5393 Price::from("1.00000"),
5394 Quantity::from(1),
5395 UnixNanos::default(),
5396 );
5397 aggregator.update(
5398 Price::from("1.00025"),
5399 Quantity::from(1),
5400 UnixNanos::from(1000),
5401 );
5402
5403 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5404 assert_eq!(handler_guard.len(), 2);
5405
5406 let bar1 = &handler_guard[0];
5407 assert_eq!(bar1.open, Price::from("1.00000"));
5408 assert_eq!(bar1.high, Price::from("1.00010"));
5409 assert_eq!(bar1.low, Price::from("1.00000"));
5410 assert_eq!(bar1.close, Price::from("1.00010"));
5411
5412 let bar2 = &handler_guard[1];
5413 assert_eq!(bar2.open, Price::from("1.00010"));
5414 assert_eq!(bar2.high, Price::from("1.00020"));
5415 assert_eq!(bar2.low, Price::from("1.00010"));
5416 assert_eq!(bar2.close, Price::from("1.00020"));
5417 }
5418
5419 #[rstest]
5420 fn test_renko_bar_aggregator_downward_movement(audusd_sim: CurrencyPair) {
5421 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5422 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5424 let handler = Arc::new(Mutex::new(Vec::new()));
5425 let handler_clone = Arc::clone(&handler);
5426
5427 let mut aggregator = RenkoBarAggregator::new(
5428 bar_type,
5429 instrument.price_precision(),
5430 instrument.size_precision(),
5431 instrument.price_increment(),
5432 move |bar: Bar| {
5433 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5434 handler_guard.push(bar);
5435 },
5436 );
5437
5438 aggregator.update(
5440 Price::from("1.00020"),
5441 Quantity::from(1),
5442 UnixNanos::default(),
5443 );
5444 aggregator.update(
5445 Price::from("1.00005"),
5446 Quantity::from(1),
5447 UnixNanos::from(1000),
5448 );
5449
5450 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5451 assert_eq!(handler_guard.len(), 1);
5452
5453 let bar = handler_guard.first().unwrap();
5454 assert_eq!(bar.open, Price::from("1.00020"));
5455 assert_eq!(bar.high, Price::from("1.00020"));
5456 assert_eq!(bar.low, Price::from("1.00010"));
5457 assert_eq!(bar.close, Price::from("1.00010"));
5458 assert_eq!(bar.volume, Quantity::from(2));
5459 }
5460
5461 #[rstest]
5462 fn test_renko_bar_aggregator_handle_bar_below_brick_size(audusd_sim: CurrencyPair) {
5463 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5464 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5466 let handler = Arc::new(Mutex::new(Vec::new()));
5467 let handler_clone = Arc::clone(&handler);
5468
5469 let mut aggregator = RenkoBarAggregator::new(
5470 bar_type,
5471 instrument.price_precision(),
5472 instrument.size_precision(),
5473 instrument.price_increment(),
5474 move |bar: Bar| {
5475 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5476 handler_guard.push(bar);
5477 },
5478 );
5479
5480 let input_bar = Bar::new(
5482 BarType::new(
5483 instrument.id(),
5484 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5485 AggregationSource::Internal,
5486 ),
5487 Price::from("1.00000"),
5488 Price::from("1.00005"),
5489 Price::from("0.99995"),
5490 Price::from("1.00005"), Quantity::from(100),
5492 UnixNanos::default(),
5493 UnixNanos::from(1000),
5494 );
5495
5496 aggregator.handle_bar(input_bar);
5497
5498 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5499 assert_eq!(handler_guard.len(), 0); }
5501
5502 #[rstest]
5503 fn test_renko_bar_aggregator_handle_bar_exceeds_brick_size(audusd_sim: CurrencyPair) {
5504 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5505 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5507 let handler = Arc::new(Mutex::new(Vec::new()));
5508 let handler_clone = Arc::clone(&handler);
5509
5510 let mut aggregator = RenkoBarAggregator::new(
5511 bar_type,
5512 instrument.price_precision(),
5513 instrument.size_precision(),
5514 instrument.price_increment(),
5515 move |bar: Bar| {
5516 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5517 handler_guard.push(bar);
5518 },
5519 );
5520
5521 let bar1 = Bar::new(
5523 BarType::new(
5524 instrument.id(),
5525 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5526 AggregationSource::Internal,
5527 ),
5528 Price::from("1.00000"),
5529 Price::from("1.00005"),
5530 Price::from("0.99995"),
5531 Price::from("1.00000"),
5532 Quantity::from(100),
5533 UnixNanos::default(),
5534 UnixNanos::default(),
5535 );
5536
5537 let bar2 = Bar::new(
5539 BarType::new(
5540 instrument.id(),
5541 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5542 AggregationSource::Internal,
5543 ),
5544 Price::from("1.00000"),
5545 Price::from("1.00015"),
5546 Price::from("0.99995"),
5547 Price::from("1.00010"), Quantity::from(50),
5549 UnixNanos::from(60_000_000_000),
5550 UnixNanos::from(60_000_000_000),
5551 );
5552
5553 aggregator.handle_bar(bar1);
5554 aggregator.handle_bar(bar2);
5555
5556 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5557 assert_eq!(handler_guard.len(), 1);
5558
5559 let bar = handler_guard.first().unwrap();
5560 assert_eq!(bar.open, Price::from("1.00000"));
5561 assert_eq!(bar.high, Price::from("1.00010"));
5562 assert_eq!(bar.low, Price::from("1.00000"));
5563 assert_eq!(bar.close, Price::from("1.00010"));
5564 assert_eq!(bar.volume, Quantity::from(150));
5565 }
5566
5567 #[rstest]
5568 fn test_renko_bar_aggregator_handle_bar_multiple_bricks(audusd_sim: CurrencyPair) {
5569 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5570 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5572 let handler = Arc::new(Mutex::new(Vec::new()));
5573 let handler_clone = Arc::clone(&handler);
5574
5575 let mut aggregator = RenkoBarAggregator::new(
5576 bar_type,
5577 instrument.price_precision(),
5578 instrument.size_precision(),
5579 instrument.price_increment(),
5580 move |bar: Bar| {
5581 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5582 handler_guard.push(bar);
5583 },
5584 );
5585
5586 let bar1 = Bar::new(
5588 BarType::new(
5589 instrument.id(),
5590 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5591 AggregationSource::Internal,
5592 ),
5593 Price::from("1.00000"),
5594 Price::from("1.00005"),
5595 Price::from("0.99995"),
5596 Price::from("1.00000"),
5597 Quantity::from(100),
5598 UnixNanos::default(),
5599 UnixNanos::default(),
5600 );
5601
5602 let bar2 = Bar::new(
5604 BarType::new(
5605 instrument.id(),
5606 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5607 AggregationSource::Internal,
5608 ),
5609 Price::from("1.00000"),
5610 Price::from("1.00035"),
5611 Price::from("0.99995"),
5612 Price::from("1.00030"), Quantity::from(50),
5614 UnixNanos::from(60_000_000_000),
5615 UnixNanos::from(60_000_000_000),
5616 );
5617
5618 aggregator.handle_bar(bar1);
5619 aggregator.handle_bar(bar2);
5620
5621 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5622 assert_eq!(handler_guard.len(), 3);
5623
5624 let bar1 = &handler_guard[0];
5625 assert_eq!(bar1.open, Price::from("1.00000"));
5626 assert_eq!(bar1.close, Price::from("1.00010"));
5627
5628 let bar2 = &handler_guard[1];
5629 assert_eq!(bar2.open, Price::from("1.00010"));
5630 assert_eq!(bar2.close, Price::from("1.00020"));
5631
5632 let bar3 = &handler_guard[2];
5633 assert_eq!(bar3.open, Price::from("1.00020"));
5634 assert_eq!(bar3.close, Price::from("1.00030"));
5635 }
5636
5637 #[rstest]
5638 fn test_renko_bar_aggregator_handle_bar_downward_movement(audusd_sim: CurrencyPair) {
5639 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5640 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5642 let handler = Arc::new(Mutex::new(Vec::new()));
5643 let handler_clone = Arc::clone(&handler);
5644
5645 let mut aggregator = RenkoBarAggregator::new(
5646 bar_type,
5647 instrument.price_precision(),
5648 instrument.size_precision(),
5649 instrument.price_increment(),
5650 move |bar: Bar| {
5651 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5652 handler_guard.push(bar);
5653 },
5654 );
5655
5656 let bar1 = Bar::new(
5658 BarType::new(
5659 instrument.id(),
5660 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5661 AggregationSource::Internal,
5662 ),
5663 Price::from("1.00020"),
5664 Price::from("1.00025"),
5665 Price::from("1.00015"),
5666 Price::from("1.00020"),
5667 Quantity::from(100),
5668 UnixNanos::default(),
5669 UnixNanos::default(),
5670 );
5671
5672 let bar2 = Bar::new(
5674 BarType::new(
5675 instrument.id(),
5676 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5677 AggregationSource::Internal,
5678 ),
5679 Price::from("1.00020"),
5680 Price::from("1.00025"),
5681 Price::from("1.00005"),
5682 Price::from("1.00010"), Quantity::from(50),
5684 UnixNanos::from(60_000_000_000),
5685 UnixNanos::from(60_000_000_000),
5686 );
5687
5688 aggregator.handle_bar(bar1);
5689 aggregator.handle_bar(bar2);
5690
5691 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5692 assert_eq!(handler_guard.len(), 1);
5693
5694 let bar = handler_guard.first().unwrap();
5695 assert_eq!(bar.open, Price::from("1.00020"));
5696 assert_eq!(bar.high, Price::from("1.00020"));
5697 assert_eq!(bar.low, Price::from("1.00010"));
5698 assert_eq!(bar.close, Price::from("1.00010"));
5699 assert_eq!(bar.volume, Quantity::from(150));
5700 }
5701
5702 #[rstest]
5703 fn test_renko_bar_aggregator_brick_size_calculation(audusd_sim: CurrencyPair) {
5704 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5705
5706 let bar_spec_5 = BarSpecification::new(5, BarAggregation::Renko, PriceType::Mid); let bar_type_5 = BarType::new(instrument.id(), bar_spec_5, AggregationSource::Internal);
5709 let handler = Arc::new(Mutex::new(Vec::new()));
5710 let handler_clone = Arc::clone(&handler);
5711
5712 let aggregator_5 = RenkoBarAggregator::new(
5713 bar_type_5,
5714 instrument.price_precision(),
5715 instrument.size_precision(),
5716 instrument.price_increment(),
5717 move |_bar: Bar| {
5718 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5719 handler_guard.push(_bar);
5720 },
5721 );
5722
5723 let expected_brick_size_5 = 5 * instrument.price_increment().raw;
5725 assert_eq!(aggregator_5.brick_size, expected_brick_size_5);
5726
5727 let bar_spec_20 = BarSpecification::new(20, BarAggregation::Renko, PriceType::Mid); let bar_type_20 = BarType::new(instrument.id(), bar_spec_20, AggregationSource::Internal);
5729 let handler2 = Arc::new(Mutex::new(Vec::new()));
5730 let handler2_clone = Arc::clone(&handler2);
5731
5732 let aggregator_20 = RenkoBarAggregator::new(
5733 bar_type_20,
5734 instrument.price_precision(),
5735 instrument.size_precision(),
5736 instrument.price_increment(),
5737 move |_bar: Bar| {
5738 let mut handler_guard = handler2_clone.lock().expect(MUTEX_POISONED);
5739 handler_guard.push(_bar);
5740 },
5741 );
5742
5743 let expected_brick_size_20 = 20 * instrument.price_increment().raw;
5745 assert_eq!(aggregator_20.brick_size, expected_brick_size_20);
5746 }
5747
5748 #[rstest]
5749 fn test_renko_bar_aggregator_sequential_updates(audusd_sim: CurrencyPair) {
5750 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5751 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5753 let handler = Arc::new(Mutex::new(Vec::new()));
5754 let handler_clone = Arc::clone(&handler);
5755
5756 let mut aggregator = RenkoBarAggregator::new(
5757 bar_type,
5758 instrument.price_precision(),
5759 instrument.size_precision(),
5760 instrument.price_increment(),
5761 move |bar: Bar| {
5762 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5763 handler_guard.push(bar);
5764 },
5765 );
5766
5767 aggregator.update(
5769 Price::from("1.00000"),
5770 Quantity::from(1),
5771 UnixNanos::from(1000),
5772 );
5773 aggregator.update(
5774 Price::from("1.00010"),
5775 Quantity::from(1),
5776 UnixNanos::from(2000),
5777 ); aggregator.update(
5779 Price::from("1.00020"),
5780 Quantity::from(1),
5781 UnixNanos::from(3000),
5782 ); aggregator.update(
5784 Price::from("1.00025"),
5785 Quantity::from(1),
5786 UnixNanos::from(4000),
5787 ); aggregator.update(
5789 Price::from("1.00030"),
5790 Quantity::from(1),
5791 UnixNanos::from(5000),
5792 ); let handler_guard = handler.lock().expect(MUTEX_POISONED);
5795 assert_eq!(handler_guard.len(), 3);
5796
5797 let bar1 = &handler_guard[0];
5798 assert_eq!(bar1.open, Price::from("1.00000"));
5799 assert_eq!(bar1.close, Price::from("1.00010"));
5800
5801 let bar2 = &handler_guard[1];
5802 assert_eq!(bar2.open, Price::from("1.00010"));
5803 assert_eq!(bar2.close, Price::from("1.00020"));
5804
5805 let bar3 = &handler_guard[2];
5806 assert_eq!(bar3.open, Price::from("1.00020"));
5807 assert_eq!(bar3.close, Price::from("1.00030"));
5808 }
5809
5810 #[rstest]
5811 fn test_renko_bar_aggregator_mixed_direction_movement(audusd_sim: CurrencyPair) {
5812 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5813 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5815 let handler = Arc::new(Mutex::new(Vec::new()));
5816 let handler_clone = Arc::clone(&handler);
5817
5818 let mut aggregator = RenkoBarAggregator::new(
5819 bar_type,
5820 instrument.price_precision(),
5821 instrument.size_precision(),
5822 instrument.price_increment(),
5823 move |bar: Bar| {
5824 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5825 handler_guard.push(bar);
5826 },
5827 );
5828
5829 aggregator.update(
5831 Price::from("1.00000"),
5832 Quantity::from(1),
5833 UnixNanos::from(1000),
5834 );
5835 aggregator.update(
5836 Price::from("1.00010"),
5837 Quantity::from(1),
5838 UnixNanos::from(2000),
5839 ); aggregator.update(
5841 Price::from("0.99990"),
5842 Quantity::from(1),
5843 UnixNanos::from(3000),
5844 ); let handler_guard = handler.lock().expect(MUTEX_POISONED);
5847 assert_eq!(handler_guard.len(), 3);
5848
5849 let bar1 = &handler_guard[0]; assert_eq!(bar1.open, Price::from("1.00000"));
5851 assert_eq!(bar1.high, Price::from("1.00010"));
5852 assert_eq!(bar1.low, Price::from("1.00000"));
5853 assert_eq!(bar1.close, Price::from("1.00010"));
5854
5855 let bar2 = &handler_guard[1]; assert_eq!(bar2.open, Price::from("1.00010"));
5857 assert_eq!(bar2.high, Price::from("1.00010"));
5858 assert_eq!(bar2.low, Price::from("1.00000"));
5859 assert_eq!(bar2.close, Price::from("1.00000"));
5860
5861 let bar3 = &handler_guard[2]; assert_eq!(bar3.open, Price::from("1.00000"));
5863 assert_eq!(bar3.high, Price::from("1.00000"));
5864 assert_eq!(bar3.low, Price::from("0.99990"));
5865 assert_eq!(bar3.close, Price::from("0.99990"));
5866 }
5867
5868 #[rstest]
5869 fn test_tick_imbalance_bar_aggregator_mixed_trades_cancel_out(equity_aapl: Equity) {
5870 let instrument = InstrumentAny::Equity(equity_aapl);
5871 let bar_spec = BarSpecification::new(3, BarAggregation::TickImbalance, PriceType::Last);
5872 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5873 let handler = Arc::new(Mutex::new(Vec::new()));
5874 let handler_clone = Arc::clone(&handler);
5875
5876 let mut aggregator = TickImbalanceBarAggregator::new(
5877 bar_type,
5878 instrument.price_precision(),
5879 instrument.size_precision(),
5880 move |bar: Bar| {
5881 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5882 handler_guard.push(bar);
5883 },
5884 );
5885
5886 let buy = TradeTick {
5887 aggressor_side: AggressorSide::Buyer,
5888 ..TradeTick::default()
5889 };
5890 let sell = TradeTick {
5891 aggressor_side: AggressorSide::Seller,
5892 ..TradeTick::default()
5893 };
5894
5895 aggregator.handle_trade(buy);
5896 aggregator.handle_trade(sell);
5897 aggregator.handle_trade(buy);
5898
5899 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5900 assert_eq!(handler_guard.len(), 0);
5901 }
5902
5903 #[rstest]
5904 fn test_tick_imbalance_bar_aggregator_no_aggressor_ignored(equity_aapl: Equity) {
5905 let instrument = InstrumentAny::Equity(equity_aapl);
5906 let bar_spec = BarSpecification::new(2, BarAggregation::TickImbalance, PriceType::Last);
5907 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5908 let handler = Arc::new(Mutex::new(Vec::new()));
5909 let handler_clone = Arc::clone(&handler);
5910
5911 let mut aggregator = TickImbalanceBarAggregator::new(
5912 bar_type,
5913 instrument.price_precision(),
5914 instrument.size_precision(),
5915 move |bar: Bar| {
5916 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5917 handler_guard.push(bar);
5918 },
5919 );
5920
5921 let buy = TradeTick {
5922 aggressor_side: AggressorSide::Buyer,
5923 ..TradeTick::default()
5924 };
5925 let no_aggressor = TradeTick {
5926 aggressor_side: AggressorSide::NoAggressor,
5927 ..TradeTick::default()
5928 };
5929
5930 aggregator.handle_trade(buy);
5931 aggregator.handle_trade(no_aggressor);
5932 aggregator.handle_trade(buy);
5933
5934 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5935 assert_eq!(handler_guard.len(), 1);
5936 }
5937
5938 #[rstest]
5939 fn test_tick_runs_bar_aggregator_multiple_consecutive_runs(equity_aapl: Equity) {
5940 let instrument = InstrumentAny::Equity(equity_aapl);
5941 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
5942 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5943 let handler = Arc::new(Mutex::new(Vec::new()));
5944 let handler_clone = Arc::clone(&handler);
5945
5946 let mut aggregator = TickRunsBarAggregator::new(
5947 bar_type,
5948 instrument.price_precision(),
5949 instrument.size_precision(),
5950 move |bar: Bar| {
5951 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5952 handler_guard.push(bar);
5953 },
5954 );
5955
5956 let buy = TradeTick {
5957 aggressor_side: AggressorSide::Buyer,
5958 ..TradeTick::default()
5959 };
5960 let sell = TradeTick {
5961 aggressor_side: AggressorSide::Seller,
5962 ..TradeTick::default()
5963 };
5964
5965 aggregator.handle_trade(buy);
5966 aggregator.handle_trade(buy);
5967 aggregator.handle_trade(sell);
5968 aggregator.handle_trade(sell);
5969
5970 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5971 assert_eq!(handler_guard.len(), 2);
5972 }
5973
5974 #[rstest]
5975 fn test_volume_imbalance_bar_aggregator_large_trade_spans_bars(equity_aapl: Equity) {
5976 let instrument = InstrumentAny::Equity(equity_aapl);
5977 let bar_spec = BarSpecification::new(10, BarAggregation::VolumeImbalance, PriceType::Last);
5978 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5979 let handler = Arc::new(Mutex::new(Vec::new()));
5980 let handler_clone = Arc::clone(&handler);
5981
5982 let mut aggregator = VolumeImbalanceBarAggregator::new(
5983 bar_type,
5984 instrument.price_precision(),
5985 instrument.size_precision(),
5986 move |bar: Bar| {
5987 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5988 handler_guard.push(bar);
5989 },
5990 );
5991
5992 let large_trade = TradeTick {
5993 size: Quantity::from(25),
5994 aggressor_side: AggressorSide::Buyer,
5995 ..TradeTick::default()
5996 };
5997
5998 aggregator.handle_trade(large_trade);
5999
6000 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6001 assert_eq!(handler_guard.len(), 2);
6002 }
6003
6004 #[rstest]
6005 fn test_volume_imbalance_bar_aggregator_no_aggressor_does_not_affect_imbalance(
6006 equity_aapl: Equity,
6007 ) {
6008 let instrument = InstrumentAny::Equity(equity_aapl);
6009 let bar_spec = BarSpecification::new(10, BarAggregation::VolumeImbalance, PriceType::Last);
6010 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6011 let handler = Arc::new(Mutex::new(Vec::new()));
6012 let handler_clone = Arc::clone(&handler);
6013
6014 let mut aggregator = VolumeImbalanceBarAggregator::new(
6015 bar_type,
6016 instrument.price_precision(),
6017 instrument.size_precision(),
6018 move |bar: Bar| {
6019 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
6020 handler_guard.push(bar);
6021 },
6022 );
6023
6024 let buy = TradeTick {
6025 size: Quantity::from(5),
6026 aggressor_side: AggressorSide::Buyer,
6027 ..TradeTick::default()
6028 };
6029 let no_aggressor = TradeTick {
6030 size: Quantity::from(3),
6031 aggressor_side: AggressorSide::NoAggressor,
6032 ..TradeTick::default()
6033 };
6034
6035 aggregator.handle_trade(buy);
6036 aggregator.handle_trade(no_aggressor);
6037 aggregator.handle_trade(buy);
6038
6039 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6040 assert_eq!(handler_guard.len(), 1);
6041 }
6042
6043 #[rstest]
6044 fn test_volume_runs_bar_aggregator_large_trade_spans_bars(equity_aapl: Equity) {
6045 let instrument = InstrumentAny::Equity(equity_aapl);
6046 let bar_spec = BarSpecification::new(10, BarAggregation::VolumeRuns, PriceType::Last);
6047 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6048 let handler = Arc::new(Mutex::new(Vec::new()));
6049 let handler_clone = Arc::clone(&handler);
6050
6051 let mut aggregator = VolumeRunsBarAggregator::new(
6052 bar_type,
6053 instrument.price_precision(),
6054 instrument.size_precision(),
6055 move |bar: Bar| {
6056 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
6057 handler_guard.push(bar);
6058 },
6059 );
6060
6061 let large_trade = TradeTick {
6062 size: Quantity::from(25),
6063 aggressor_side: AggressorSide::Buyer,
6064 ..TradeTick::default()
6065 };
6066
6067 aggregator.handle_trade(large_trade);
6068
6069 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6070 assert_eq!(handler_guard.len(), 2);
6071 }
6072
6073 #[rstest]
6074 fn test_value_runs_bar_aggregator_large_trade_spans_bars(equity_aapl: Equity) {
6075 let instrument = InstrumentAny::Equity(equity_aapl);
6076 let bar_spec = BarSpecification::new(50, BarAggregation::ValueRuns, PriceType::Last);
6077 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6078 let handler = Arc::new(Mutex::new(Vec::new()));
6079 let handler_clone = Arc::clone(&handler);
6080
6081 let mut aggregator = ValueRunsBarAggregator::new(
6082 bar_type,
6083 instrument.price_precision(),
6084 instrument.size_precision(),
6085 move |bar: Bar| {
6086 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
6087 handler_guard.push(bar);
6088 },
6089 );
6090
6091 let large_trade = TradeTick {
6092 price: Price::from("5.00"),
6093 size: Quantity::from(25),
6094 aggressor_side: AggressorSide::Buyer,
6095 ..TradeTick::default()
6096 };
6097
6098 aggregator.handle_trade(large_trade);
6099
6100 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6101 assert_eq!(handler_guard.len(), 2);
6102 }
6103
6104 #[rstest]
6105 fn test_value_runs_bar_aggregator_keeps_leftover_volume_for_same_side_run(equity_aapl: Equity) {
6106 let instrument = InstrumentAny::Equity(equity_aapl);
6107 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
6108 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6109 let handler = Arc::new(Mutex::new(Vec::new()));
6110 let handler_clone = Arc::clone(&handler);
6111
6112 let mut aggregator = ValueRunsBarAggregator::new(
6113 bar_type,
6114 instrument.price_precision(),
6115 instrument.size_precision(),
6116 move |bar: Bar| {
6117 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
6118 handler_guard.push(bar);
6119 },
6120 );
6121
6122 let first = TradeTick {
6125 price: Price::from("10.00"),
6126 size: Quantity::from(15),
6127 aggressor_side: AggressorSide::Seller,
6128 ts_event: UnixNanos::from(1_000),
6129 ts_init: UnixNanos::from(1_000),
6130 ..TradeTick::default()
6131 };
6132 aggregator.handle_trade(first);
6133
6134 let second = TradeTick {
6136 price: Price::from("10.00"),
6137 size: Quantity::from(5),
6138 aggressor_side: AggressorSide::Seller,
6139 ts_event: UnixNanos::from(2_000),
6140 ts_init: UnixNanos::from(2_000),
6141 ..TradeTick::default()
6142 };
6143 aggregator.handle_trade(second);
6144
6145 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6146 assert_eq!(handler_guard.len(), 2);
6147 assert_eq!(handler_guard[0].volume, Quantity::from(10));
6148 assert_eq!(handler_guard[1].volume, Quantity::from(10));
6149 }
6150
6151 #[rstest]
6152 fn test_value_bar_high_price_low_step_no_zero_volume_bars(equity_aapl: Equity) {
6153 let instrument = InstrumentAny::Equity(equity_aapl);
6154 let bar_spec = BarSpecification::new(100, BarAggregation::Value, PriceType::Last);
6155 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6156 let handler = Arc::new(Mutex::new(Vec::new()));
6157 let handler_clone = Arc::clone(&handler);
6158
6159 let mut aggregator = ValueBarAggregator::new(
6160 bar_type,
6161 instrument.price_precision(),
6162 instrument.size_precision(),
6163 move |bar: Bar| {
6164 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
6165 handler_guard.push(bar);
6166 },
6167 );
6168
6169 aggregator.update(
6171 Price::from("1000.00"),
6172 Quantity::from(3),
6173 UnixNanos::default(),
6174 );
6175
6176 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6178 assert_eq!(handler_guard.len(), 3);
6179 for bar in handler_guard.iter() {
6180 assert_eq!(bar.volume, Quantity::from(1));
6181 }
6182 }
6183
6184 #[rstest]
6185 fn test_value_imbalance_high_price_low_step_no_zero_volume_bars(equity_aapl: Equity) {
6186 let instrument = InstrumentAny::Equity(equity_aapl);
6187 let bar_spec = BarSpecification::new(100, BarAggregation::ValueImbalance, PriceType::Last);
6188 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6189 let handler = Arc::new(Mutex::new(Vec::new()));
6190 let handler_clone = Arc::clone(&handler);
6191
6192 let mut aggregator = ValueImbalanceBarAggregator::new(
6193 bar_type,
6194 instrument.price_precision(),
6195 instrument.size_precision(),
6196 move |bar: Bar| {
6197 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
6198 handler_guard.push(bar);
6199 },
6200 );
6201
6202 let trade = TradeTick {
6203 price: Price::from("1000.00"),
6204 size: Quantity::from(3),
6205 aggressor_side: AggressorSide::Buyer,
6206 instrument_id: instrument.id(),
6207 ..TradeTick::default()
6208 };
6209
6210 aggregator.handle_trade(trade);
6211
6212 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6213 assert_eq!(handler_guard.len(), 3);
6214 for bar in handler_guard.iter() {
6215 assert_eq!(bar.volume, Quantity::from(1));
6216 }
6217 }
6218
6219 #[rstest]
6220 fn test_value_imbalance_opposite_side_overshoot_emits_bar(equity_aapl: Equity) {
6221 let instrument = InstrumentAny::Equity(equity_aapl);
6222 let bar_spec = BarSpecification::new(100, BarAggregation::ValueImbalance, PriceType::Last);
6223 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6224 let handler = Arc::new(Mutex::new(Vec::new()));
6225 let handler_clone = Arc::clone(&handler);
6226
6227 let mut aggregator = ValueImbalanceBarAggregator::new(
6228 bar_type,
6229 instrument.price_precision(),
6230 instrument.size_precision(),
6231 move |bar: Bar| {
6232 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
6233 handler_guard.push(bar);
6234 },
6235 );
6236
6237 let sell_tick = TradeTick {
6239 price: Price::from("10.00"),
6240 size: Quantity::from(5),
6241 aggressor_side: AggressorSide::Seller,
6242 instrument_id: instrument.id(),
6243 ..TradeTick::default()
6244 };
6245
6246 let buy_tick = TradeTick {
6249 price: Price::from("1000.00"),
6250 size: Quantity::from(1),
6251 aggressor_side: AggressorSide::Buyer,
6252 instrument_id: instrument.id(),
6253 ts_init: UnixNanos::from(1),
6254 ts_event: UnixNanos::from(1),
6255 ..TradeTick::default()
6256 };
6257
6258 aggregator.handle_trade(sell_tick);
6259 aggregator.handle_trade(buy_tick);
6260
6261 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6262 assert_eq!(handler_guard.len(), 1);
6263 assert_eq!(handler_guard[0].volume, Quantity::from(6));
6264 }
6265
6266 #[rstest]
6267 fn test_value_runs_high_price_low_step_no_zero_volume_bars(equity_aapl: Equity) {
6268 let instrument = InstrumentAny::Equity(equity_aapl);
6269 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
6270 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6271 let handler = Arc::new(Mutex::new(Vec::new()));
6272 let handler_clone = Arc::clone(&handler);
6273
6274 let mut aggregator = ValueRunsBarAggregator::new(
6275 bar_type,
6276 instrument.price_precision(),
6277 instrument.size_precision(),
6278 move |bar: Bar| {
6279 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
6280 handler_guard.push(bar);
6281 },
6282 );
6283
6284 let trade = TradeTick {
6285 price: Price::from("1000.00"),
6286 size: Quantity::from(3),
6287 aggressor_side: AggressorSide::Buyer,
6288 instrument_id: instrument.id(),
6289 ..TradeTick::default()
6290 };
6291
6292 aggregator.handle_trade(trade);
6293
6294 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6295 assert_eq!(handler_guard.len(), 3);
6296 for bar in handler_guard.iter() {
6297 assert_eq!(bar.volume, Quantity::from(1));
6298 }
6299 }
6300
6301 #[rstest]
6302 fn test_value_imbalance_bar_aggregator_exact_below_step_retains_pending() {
6303 let instrument_id = InstrumentId::from("AAPL.XNAS");
6307 let bar_spec = BarSpecification::new(
6308 9_007_199_254,
6309 BarAggregation::ValueImbalance,
6310 PriceType::Last,
6311 );
6312 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6313 let handler = Arc::new(Mutex::new(Vec::new()));
6314 let handler_clone = Arc::clone(&handler);
6315
6316 let mut aggregator = ValueImbalanceBarAggregator::new(bar_type, 0, 9, move |bar: Bar| {
6317 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
6318 });
6319
6320 let below_step = TradeTick {
6321 instrument_id,
6322 price: Price::from("1"),
6323 size: Quantity::from("9007199253.999999999"),
6324 aggressor_side: AggressorSide::Buyer,
6325 ..TradeTick::default()
6326 };
6327 aggregator.handle_trade(below_step);
6328
6329 assert!(handler.lock().expect(MUTEX_POISONED).is_empty());
6330 assert_eq!(
6331 aggregator.core.builder.volume,
6332 Quantity::from("9007199253.999999999"),
6333 );
6334
6335 let one_raw_unit = TradeTick {
6338 instrument_id,
6339 price: Price::from("1"),
6340 size: Quantity::from("0.000000001"),
6341 aggressor_side: AggressorSide::Buyer,
6342 ts_event: UnixNanos::from(1),
6343 ts_init: UnixNanos::from(1),
6344 ..TradeTick::default()
6345 };
6346 aggregator.handle_trade(one_raw_unit);
6347
6348 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6349 assert_eq!(handler_guard.len(), 1);
6350 assert_eq!(
6351 handler_guard[0].volume,
6352 Quantity::from("9007199254.000000000")
6353 );
6354 assert_eq!(aggregator.core.builder.volume, Quantity::zero(9));
6355 }
6356
6357 #[rstest]
6358 fn test_value_imbalance_bar_aggregator_conserves_volume_across_split_bars() {
6359 let instrument_id = InstrumentId::from("AAPL.XNAS");
6363 let bar_spec = BarSpecification::new(4, BarAggregation::ValueImbalance, PriceType::Last);
6364 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6365 let handler = Arc::new(Mutex::new(Vec::new()));
6366 let handler_clone = Arc::clone(&handler);
6367
6368 let mut aggregator = ValueImbalanceBarAggregator::new(bar_type, 0, 9, move |bar: Bar| {
6369 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
6370 });
6371
6372 let input = Quantity::from("10.000000003");
6373 let trade = TradeTick {
6374 instrument_id,
6375 price: Price::from("1"),
6376 size: input,
6377 aggressor_side: AggressorSide::Buyer,
6378 ..TradeTick::default()
6379 };
6380 aggregator.handle_trade(trade);
6381
6382 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6383 assert_eq!(handler_guard.len(), 2);
6384 for bar in handler_guard.iter() {
6385 assert_eq!(bar.volume, Quantity::from("4.000000000"));
6386 }
6387 assert_eq!(
6388 aggregator.core.builder.volume,
6389 Quantity::from("2.000000003"),
6390 );
6391 let emitted_plus_pending = handler_guard
6392 .iter()
6393 .map(|bar| bar.volume.as_decimal())
6394 .sum::<Decimal>()
6395 + aggregator.core.builder.volume.as_decimal();
6396 assert_eq!(emitted_plus_pending, input.as_decimal());
6397 }
6398
6399 #[rstest]
6400 fn test_value_runs_bar_aggregator_exact_below_step_retains_pending() {
6401 let instrument_id = InstrumentId::from("AAPL.XNAS");
6405 let bar_spec =
6406 BarSpecification::new(9_007_199_254, BarAggregation::ValueRuns, PriceType::Last);
6407 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6408 let handler = Arc::new(Mutex::new(Vec::new()));
6409 let handler_clone = Arc::clone(&handler);
6410
6411 let mut aggregator = ValueRunsBarAggregator::new(bar_type, 0, 9, move |bar: Bar| {
6412 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
6413 });
6414
6415 let below_step = TradeTick {
6416 instrument_id,
6417 price: Price::from("1"),
6418 size: Quantity::from("9007199253.999999999"),
6419 aggressor_side: AggressorSide::Buyer,
6420 ..TradeTick::default()
6421 };
6422 aggregator.handle_trade(below_step);
6423
6424 assert!(handler.lock().expect(MUTEX_POISONED).is_empty());
6425 assert_eq!(
6426 aggregator.core.builder.volume,
6427 Quantity::from("9007199253.999999999"),
6428 );
6429
6430 let one_raw_unit = TradeTick {
6433 instrument_id,
6434 price: Price::from("1"),
6435 size: Quantity::from("0.000000001"),
6436 aggressor_side: AggressorSide::Buyer,
6437 ts_event: UnixNanos::from(1),
6438 ts_init: UnixNanos::from(1),
6439 ..TradeTick::default()
6440 };
6441 aggregator.handle_trade(one_raw_unit);
6442
6443 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6444 assert_eq!(handler_guard.len(), 1);
6445 assert_eq!(
6446 handler_guard[0].volume,
6447 Quantity::from("9007199254.000000000")
6448 );
6449 assert_eq!(aggregator.core.builder.volume, Quantity::zero(9));
6450 }
6451
6452 #[rstest]
6453 fn test_value_runs_bar_aggregator_conserves_volume_across_split_bars() {
6454 let instrument_id = InstrumentId::from("AAPL.XNAS");
6458 let bar_spec = BarSpecification::new(4, BarAggregation::ValueRuns, PriceType::Last);
6459 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6460 let handler = Arc::new(Mutex::new(Vec::new()));
6461 let handler_clone = Arc::clone(&handler);
6462
6463 let mut aggregator = ValueRunsBarAggregator::new(bar_type, 0, 9, move |bar: Bar| {
6464 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
6465 });
6466
6467 let input = Quantity::from("10.000000003");
6468 let trade = TradeTick {
6469 instrument_id,
6470 price: Price::from("1"),
6471 size: input,
6472 aggressor_side: AggressorSide::Buyer,
6473 ..TradeTick::default()
6474 };
6475 aggregator.handle_trade(trade);
6476
6477 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6478 assert_eq!(handler_guard.len(), 2);
6479 for bar in handler_guard.iter() {
6480 assert_eq!(bar.volume, Quantity::from("4.000000000"));
6481 }
6482 assert_eq!(
6483 aggregator.core.builder.volume,
6484 Quantity::from("2.000000003"),
6485 );
6486 let emitted_plus_pending = handler_guard
6487 .iter()
6488 .map(|bar| bar.volume.as_decimal())
6489 .sum::<Decimal>()
6490 + aggregator.core.builder.volume.as_decimal();
6491 assert_eq!(emitted_plus_pending, input.as_decimal());
6492 }
6493
6494 #[rstest]
6495 fn test_value_imbalance_bar_aggregator_no_aggressor_and_zero_price_fall_back_to_plain_volume() {
6496 let instrument_id = InstrumentId::from("AAPL.XNAS");
6499 let bar_spec = BarSpecification::new(100, BarAggregation::ValueImbalance, PriceType::Last);
6500 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6501 let handler = Arc::new(Mutex::new(Vec::new()));
6502 let handler_clone = Arc::clone(&handler);
6503
6504 let mut aggregator = ValueImbalanceBarAggregator::new(bar_type, 2, 0, move |bar: Bar| {
6505 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
6506 });
6507
6508 let no_aggressor = TradeTick {
6509 instrument_id,
6510 price: Price::from("10.00"),
6511 size: Quantity::from(3),
6512 aggressor_side: AggressorSide::NoAggressor,
6513 ..TradeTick::default()
6514 };
6515 let zero_price = TradeTick {
6516 instrument_id,
6517 price: Price::from("0.00"),
6518 size: Quantity::from(4),
6519 aggressor_side: AggressorSide::Buyer,
6520 ts_event: UnixNanos::from(1),
6521 ts_init: UnixNanos::from(1),
6522 ..TradeTick::default()
6523 };
6524 aggregator.handle_trade(no_aggressor);
6525 aggregator.handle_trade(zero_price);
6526
6527 assert!(handler.lock().expect(MUTEX_POISONED).is_empty());
6528 assert_eq!(aggregator.core.builder.volume, Quantity::from(7));
6529 }
6530
6531 #[rstest]
6532 fn test_value_runs_bar_aggregator_no_aggressor_and_zero_price_fall_back_to_plain_volume() {
6533 let instrument_id = InstrumentId::from("AAPL.XNAS");
6536 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
6537 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6538 let handler = Arc::new(Mutex::new(Vec::new()));
6539 let handler_clone = Arc::clone(&handler);
6540
6541 let mut aggregator = ValueRunsBarAggregator::new(bar_type, 2, 0, move |bar: Bar| {
6542 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
6543 });
6544
6545 let no_aggressor = TradeTick {
6546 instrument_id,
6547 price: Price::from("10.00"),
6548 size: Quantity::from(3),
6549 aggressor_side: AggressorSide::NoAggressor,
6550 ..TradeTick::default()
6551 };
6552 let zero_price = TradeTick {
6553 instrument_id,
6554 price: Price::from("0.00"),
6555 size: Quantity::from(4),
6556 aggressor_side: AggressorSide::Buyer,
6557 ts_event: UnixNanos::from(1),
6558 ts_init: UnixNanos::from(1),
6559 ..TradeTick::default()
6560 };
6561 aggregator.handle_trade(no_aggressor);
6562 aggregator.handle_trade(zero_price);
6563
6564 assert!(handler.lock().expect(MUTEX_POISONED).is_empty());
6565 assert_eq!(aggregator.core.builder.volume, Quantity::from(7));
6566 }
6567
6568 #[rstest]
6569 fn test_value_imbalance_bar_aggregator_conserves_volume_with_indivisible_price() {
6570 let instrument_id = InstrumentId::from("AAPL.XNAS");
6575 let bar_spec = BarSpecification::new(1, BarAggregation::ValueImbalance, PriceType::Last);
6576 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6577 let handler = Arc::new(Mutex::new(Vec::new()));
6578 let handler_clone = Arc::clone(&handler);
6579
6580 let mut aggregator = ValueImbalanceBarAggregator::new(bar_type, 2, 1, move |bar: Bar| {
6581 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
6582 });
6583
6584 let input = Quantity::from("1.0");
6585 let trade = TradeTick {
6586 instrument_id,
6587 price: Price::from("3.00"),
6588 size: input,
6589 aggressor_side: AggressorSide::Buyer,
6590 ..TradeTick::default()
6591 };
6592 aggregator.handle_trade(trade);
6593
6594 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6595 assert_eq!(handler_guard.len(), 3);
6596 for bar in handler_guard.iter() {
6597 assert_eq!(bar.volume, Quantity::from("0.3"));
6598 }
6599 assert_eq!(aggregator.core.builder.volume, Quantity::from("0.1"));
6600 let emitted_plus_pending = handler_guard
6601 .iter()
6602 .map(|bar| bar.volume.as_decimal())
6603 .sum::<Decimal>()
6604 + aggregator.core.builder.volume.as_decimal();
6605 assert_eq!(emitted_plus_pending, input.as_decimal());
6606 }
6607
6608 #[rstest]
6609 fn test_value_runs_bar_aggregator_conserves_volume_with_indivisible_price() {
6610 let instrument_id = InstrumentId::from("AAPL.XNAS");
6615 let bar_spec = BarSpecification::new(1, BarAggregation::ValueRuns, PriceType::Last);
6616 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6617 let handler = Arc::new(Mutex::new(Vec::new()));
6618 let handler_clone = Arc::clone(&handler);
6619
6620 let mut aggregator = ValueRunsBarAggregator::new(bar_type, 2, 1, move |bar: Bar| {
6621 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
6622 });
6623
6624 let input = Quantity::from("1.0");
6625 let trade = TradeTick {
6626 instrument_id,
6627 price: Price::from("3.00"),
6628 size: input,
6629 aggressor_side: AggressorSide::Buyer,
6630 ..TradeTick::default()
6631 };
6632 aggregator.handle_trade(trade);
6633
6634 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6635 assert_eq!(handler_guard.len(), 3);
6636 for bar in handler_guard.iter() {
6637 assert_eq!(bar.volume, Quantity::from("0.3"));
6638 }
6639 assert_eq!(aggregator.core.builder.volume, Quantity::from("0.1"));
6640 let emitted_plus_pending = handler_guard
6641 .iter()
6642 .map(|bar| bar.volume.as_decimal())
6643 .sum::<Decimal>()
6644 + aggregator.core.builder.volume.as_decimal();
6645 assert_eq!(emitted_plus_pending, input.as_decimal());
6646 }
6647
6648 #[rstest]
6649 #[case(1000_u64)]
6650 #[case(1500_u64)]
6651 fn test_volume_imbalance_bar_aggregator_large_step_no_overflow(
6652 equity_aapl: Equity,
6653 #[case] step: u64,
6654 ) {
6655 let instrument = InstrumentAny::Equity(equity_aapl);
6656 let bar_spec = BarSpecification::new(
6657 step as usize,
6658 BarAggregation::VolumeImbalance,
6659 PriceType::Last,
6660 );
6661 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6662 let handler = Arc::new(Mutex::new(Vec::new()));
6663 let handler_clone = Arc::clone(&handler);
6664
6665 let mut aggregator = VolumeImbalanceBarAggregator::new(
6666 bar_type,
6667 instrument.price_precision(),
6668 instrument.size_precision(),
6669 move |bar: Bar| {
6670 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
6671 handler_guard.push(bar);
6672 },
6673 );
6674
6675 let trade = TradeTick {
6676 size: Quantity::from(step * 2),
6677 aggressor_side: AggressorSide::Buyer,
6678 ..TradeTick::default()
6679 };
6680
6681 aggregator.handle_trade(trade);
6682
6683 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6684 assert_eq!(handler_guard.len(), 2);
6685 for bar in handler_guard.iter() {
6686 assert_eq!(bar.volume.as_f64(), step as f64);
6687 }
6688 }
6689
6690 #[rstest]
6691 fn test_volume_imbalance_bar_aggregator_different_large_steps_produce_different_bar_counts(
6692 equity_aapl: Equity,
6693 ) {
6694 let instrument = InstrumentAny::Equity(equity_aapl);
6695 let total_volume = 3000_u64;
6696 let mut results = Vec::new();
6697
6698 for step in [1000_usize, 1500] {
6699 let bar_spec =
6700 BarSpecification::new(step, BarAggregation::VolumeImbalance, PriceType::Last);
6701 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6702 let handler = Arc::new(Mutex::new(Vec::new()));
6703 let handler_clone = Arc::clone(&handler);
6704
6705 let mut aggregator = VolumeImbalanceBarAggregator::new(
6706 bar_type,
6707 instrument.price_precision(),
6708 instrument.size_precision(),
6709 move |bar: Bar| {
6710 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
6711 handler_guard.push(bar);
6712 },
6713 );
6714
6715 let trade = TradeTick {
6716 size: Quantity::from(total_volume),
6717 aggressor_side: AggressorSide::Buyer,
6718 ..TradeTick::default()
6719 };
6720
6721 aggregator.handle_trade(trade);
6722
6723 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6724 results.push(handler_guard.len());
6725 }
6726
6727 assert_eq!(results[0], 3); assert_eq!(results[1], 2); assert_ne!(results[0], results[1]);
6730 }
6731
6732 #[rstest]
6733 #[case(1000_u64)]
6734 #[case(1500_u64)]
6735 fn test_volume_runs_bar_aggregator_large_step_no_overflow(
6736 equity_aapl: Equity,
6737 #[case] step: u64,
6738 ) {
6739 let instrument = InstrumentAny::Equity(equity_aapl);
6740 let bar_spec =
6741 BarSpecification::new(step as usize, BarAggregation::VolumeRuns, PriceType::Last);
6742 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6743 let handler = Arc::new(Mutex::new(Vec::new()));
6744 let handler_clone = Arc::clone(&handler);
6745
6746 let mut aggregator = VolumeRunsBarAggregator::new(
6747 bar_type,
6748 instrument.price_precision(),
6749 instrument.size_precision(),
6750 move |bar: Bar| {
6751 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
6752 handler_guard.push(bar);
6753 },
6754 );
6755
6756 let trade = TradeTick {
6757 size: Quantity::from(step * 2),
6758 aggressor_side: AggressorSide::Buyer,
6759 ..TradeTick::default()
6760 };
6761
6762 aggregator.handle_trade(trade);
6763
6764 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6765 assert_eq!(handler_guard.len(), 2);
6766 for bar in handler_guard.iter() {
6767 assert_eq!(bar.volume.as_f64(), step as f64);
6768 }
6769 }
6770
6771 #[rstest]
6772 fn test_volume_runs_bar_aggregator_different_large_steps_produce_different_bar_counts(
6773 equity_aapl: Equity,
6774 ) {
6775 let instrument = InstrumentAny::Equity(equity_aapl);
6776 let total_volume = 3000_u64;
6777 let mut results = Vec::new();
6778
6779 for step in [1000_usize, 1500] {
6780 let bar_spec = BarSpecification::new(step, BarAggregation::VolumeRuns, PriceType::Last);
6781 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6782 let handler = Arc::new(Mutex::new(Vec::new()));
6783 let handler_clone = Arc::clone(&handler);
6784
6785 let mut aggregator = VolumeRunsBarAggregator::new(
6786 bar_type,
6787 instrument.price_precision(),
6788 instrument.size_precision(),
6789 move |bar: Bar| {
6790 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
6791 handler_guard.push(bar);
6792 },
6793 );
6794
6795 let trade = TradeTick {
6796 size: Quantity::from(total_volume),
6797 aggressor_side: AggressorSide::Buyer,
6798 ..TradeTick::default()
6799 };
6800
6801 aggregator.handle_trade(trade);
6802
6803 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6804 results.push(handler_guard.len());
6805 }
6806
6807 assert_eq!(results[0], 3); assert_eq!(results[1], 2); assert_ne!(results[0], results[1]);
6810 }
6811
6812 #[rstest]
6814 fn test_time_bar_historical_defers_event_at_ts_init_until_after_update(equity_aapl: Equity) {
6815 let instrument = InstrumentAny::Equity(equity_aapl);
6816 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
6817 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6818 let handler = Arc::new(Mutex::new(Vec::new()));
6819 let handler_clone = Arc::clone(&handler);
6820 let clock = Rc::new(RefCell::new(TestClock::new()));
6821
6822 let mut agg = TimeBarAggregator::new(
6823 bar_type,
6824 instrument.price_precision(),
6825 instrument.size_precision(),
6826 clock.clone(),
6827 move |bar: Bar| {
6828 let mut h = handler_clone.lock().expect(MUTEX_POISONED);
6829 h.push(bar);
6830 },
6831 true,
6832 true,
6833 BarIntervalType::LeftOpen,
6834 None,
6835 0,
6836 false,
6837 );
6838 agg.historical_mode = true;
6839 agg.set_clock_internal(clock);
6840 let boxed: Box<dyn BarAggregator> = Box::new(agg);
6841 let rc = Rc::new(RefCell::new(boxed));
6842 rc.borrow_mut().set_aggregator_weak(Rc::downgrade(&rc));
6843
6844 rc.borrow_mut().update(
6845 Price::from("100.00"),
6846 Quantity::from(1),
6847 UnixNanos::default(),
6848 );
6849 rc.borrow_mut().update(
6850 Price::from("100.00"),
6851 Quantity::from(1),
6852 UnixNanos::from(1_000_000_000),
6853 );
6854
6855 let bars = handler.lock().expect(MUTEX_POISONED);
6856 assert!(
6857 !bars.is_empty(),
6858 "deferred event at ts_init should produce a bar that includes the update"
6859 );
6860 let last_bar = bars.last().unwrap();
6861 assert_eq!(last_bar.close, Price::from("100.00"));
6862 assert!(
6863 last_bar.volume.as_f64() >= 1.0,
6864 "bar built after deferred event should include the update at ts_init"
6865 );
6866 }
6867
6868 #[rstest]
6869 fn test_spread_quote_quote_driven_emits_when_all_legs_received(equity_aapl: Equity) {
6870 let instrument = InstrumentAny::Equity(equity_aapl);
6871 let leg1 = instrument.id();
6872 let leg2 = InstrumentId::from("MSFT.XNAS");
6873 let spread_id = InstrumentId::from("SPREAD.XNAS");
6874 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
6875 let handler = Arc::new(Mutex::new(Vec::new()));
6876 let handler_clone = Arc::clone(&handler);
6877 let clock = Rc::new(RefCell::new(TestClock::new()));
6878
6879 let mut agg = SpreadQuoteAggregator::new(
6880 spread_id,
6881 &legs,
6882 true,
6883 instrument.price_precision(),
6884 0,
6885 Box::new(move |q: QuoteTick| {
6886 handler_clone.lock().expect(MUTEX_POISONED).push(q);
6887 }),
6888 clock,
6889 false,
6890 None,
6891 0,
6892 false,
6893 60,
6894 None,
6895 None,
6896 );
6897
6898 let ts = UnixNanos::from(1_000_000_000);
6899 agg.handle_quote_tick(QuoteTick::new(
6900 leg1,
6901 Price::from("100.00"),
6902 Price::from("100.10"),
6903 Quantity::from(10),
6904 Quantity::from(10),
6905 ts,
6906 ts,
6907 ));
6908 assert_eq!(handler.lock().expect(MUTEX_POISONED).len(), 0);
6909
6910 agg.handle_quote_tick(QuoteTick::new(
6911 leg2,
6912 Price::from("99.00"),
6913 Price::from("99.10"),
6914 Quantity::from(10),
6915 Quantity::from(10),
6916 ts,
6917 ts,
6918 ));
6919 let quotes = handler.lock().expect(MUTEX_POISONED);
6920 assert_eq!(quotes.len(), 1);
6921 assert_eq!(quotes[0].instrument_id, spread_id);
6922 assert!(quotes[0].bid_price < quotes[0].ask_price);
6923 }
6924
6925 #[rstest]
6926 fn test_spread_quote_futures_pricing_signed_ratios(equity_aapl: Equity) {
6927 let instrument = InstrumentAny::Equity(equity_aapl);
6928 let leg1 = instrument.id();
6929 let leg2 = InstrumentId::from("MSFT.XNAS");
6930 let spread_id = InstrumentId::from("SPREAD.XNAS");
6931 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
6932 let handler = Arc::new(Mutex::new(Vec::new()));
6933 let handler_clone = Arc::clone(&handler);
6934 let clock = Rc::new(RefCell::new(TestClock::new()));
6935
6936 let mut agg = SpreadQuoteAggregator::new(
6937 spread_id,
6938 &legs,
6939 true,
6940 instrument.price_precision(),
6941 0,
6942 Box::new(move |q: QuoteTick| {
6943 handler_clone.lock().expect(MUTEX_POISONED).push(q);
6944 }),
6945 clock,
6946 false,
6947 None,
6948 0,
6949 false,
6950 60,
6951 None,
6952 None,
6953 );
6954
6955 let ts = UnixNanos::from(1_000_000_000);
6956 agg.handle_quote_tick(QuoteTick::new(
6957 leg1,
6958 Price::from("10.00"),
6959 Price::from("10.10"),
6960 Quantity::from(100),
6961 Quantity::from(100),
6962 ts,
6963 ts,
6964 ));
6965 agg.handle_quote_tick(QuoteTick::new(
6966 leg2,
6967 Price::from("20.00"),
6968 Price::from("20.10"),
6969 Quantity::from(100),
6970 Quantity::from(100),
6971 ts,
6972 ts,
6973 ));
6974 let quotes = handler.lock().expect(MUTEX_POISONED);
6975 assert_eq!(quotes.len(), 1);
6976 let q = "es[0];
6977 assert_eq!(q.instrument_id, spread_id);
6978 assert_eq!(q.bid_price, Price::from("-10.10"));
6979 assert_eq!(q.ask_price, Price::from("-9.90"));
6980 }
6981
6982 #[rstest]
6983 fn test_spread_quote_size_calculation_non_unit_ratios(equity_aapl: Equity) {
6984 let instrument = InstrumentAny::Equity(equity_aapl);
6985 let leg1 = instrument.id();
6986 let leg2 = InstrumentId::from("MSFT.XNAS");
6987 let spread_id = InstrumentId::from("SPREAD.XNAS");
6988 let legs = vec![(leg1, 2_i64), (leg2, -1_i64)];
6989 let handler = Arc::new(Mutex::new(Vec::new()));
6990 let handler_clone = Arc::clone(&handler);
6991 let clock = Rc::new(RefCell::new(TestClock::new()));
6992
6993 let mut agg = SpreadQuoteAggregator::new(
6994 spread_id,
6995 &legs,
6996 true,
6997 instrument.price_precision(),
6998 0,
6999 Box::new(move |q: QuoteTick| {
7000 handler_clone.lock().expect(MUTEX_POISONED).push(q);
7001 }),
7002 clock,
7003 false,
7004 None,
7005 0,
7006 false,
7007 60,
7008 None,
7009 None,
7010 );
7011
7012 let ts = UnixNanos::from(1_000_000_000);
7013 agg.handle_quote_tick(QuoteTick::new(
7014 leg1,
7015 Price::from("10.00"),
7016 Price::from("10.10"),
7017 Quantity::from(100),
7018 Quantity::from(40),
7019 ts,
7020 ts,
7021 ));
7022 agg.handle_quote_tick(QuoteTick::new(
7023 leg2,
7024 Price::from("10.00"),
7025 Price::from("10.10"),
7026 Quantity::from(50),
7027 Quantity::from(30),
7028 ts,
7029 ts,
7030 ));
7031 let quotes = handler.lock().expect(MUTEX_POISONED);
7032 assert_eq!(quotes.len(), 1);
7033 let q = "es[0];
7034 assert_eq!(q.bid_size.as_f64(), 30.0);
7035 assert_eq!(q.ask_size.as_f64(), 20.0);
7036 }
7037
7038 #[rstest]
7039 fn test_spread_quote_timer_driven_emission_cadence(equity_aapl: Equity) {
7040 let instrument = InstrumentAny::Equity(equity_aapl);
7041 let leg1 = instrument.id();
7042 let leg2 = InstrumentId::from("MSFT.XNAS");
7043 let spread_id = InstrumentId::from("SPREAD.XNAS");
7044 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
7045 let handler = Arc::new(Mutex::new(Vec::new()));
7046 let handler_clone = Arc::clone(&handler);
7047 let clock = Rc::new(RefCell::new(TestClock::new()));
7048 clock.borrow_mut().set_time(UnixNanos::from(0));
7049
7050 let agg = SpreadQuoteAggregator::new(
7051 spread_id,
7052 &legs,
7053 true,
7054 instrument.price_precision(),
7055 0,
7056 Box::new(move |q: QuoteTick| {
7057 handler_clone.lock().expect(MUTEX_POISONED).push(q);
7058 }),
7059 clock.clone(),
7060 false,
7061 Some(1),
7062 0,
7063 false,
7064 60,
7065 None,
7066 None,
7067 );
7068 let rc = Rc::new(RefCell::new(agg));
7069 rc.borrow_mut().prepare_for_timer_mode(&rc);
7070 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7071
7072 for event in clock.borrow_mut().advance_time(UnixNanos::from(0), true) {
7073 rc.borrow_mut().on_timer_fire(event.ts_event);
7074 }
7075 assert_eq!(handler.lock().expect(MUTEX_POISONED).len(), 0);
7076
7077 let ts1 = UnixNanos::from(1_000_000_000);
7078 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7079 leg1,
7080 Price::from("100.00"),
7081 Price::from("100.10"),
7082 Quantity::from(10),
7083 Quantity::from(10),
7084 ts1,
7085 ts1,
7086 ));
7087 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7088 leg2,
7089 Price::from("99.00"),
7090 Price::from("99.10"),
7091 Quantity::from(10),
7092 Quantity::from(10),
7093 ts1,
7094 ts1,
7095 ));
7096
7097 for event in clock.borrow_mut().advance_time(ts1, true) {
7098 rc.borrow_mut().on_timer_fire(event.ts_event);
7099 }
7100
7101 {
7102 let quotes = handler.lock().expect(MUTEX_POISONED);
7103 assert_eq!(quotes.len(), 1);
7104 assert_eq!(quotes[0].ts_event, ts1);
7105 assert_eq!(quotes[0].ts_init, ts1);
7106 }
7107
7108 let ts2 = UnixNanos::from(2_000_000_000);
7109 for event in clock.borrow_mut().advance_time(ts2, true) {
7110 rc.borrow_mut().on_timer_fire(event.ts_event);
7111 }
7112
7113 let quotes = handler.lock().expect(MUTEX_POISONED);
7114 assert_eq!(quotes.len(), 1);
7115 }
7116
7117 #[rstest]
7118 fn test_spread_quote_historical_timer_waits_for_all_legs(equity_aapl: Equity) {
7119 let instrument = InstrumentAny::Equity(equity_aapl);
7120 let leg1 = instrument.id();
7121 let leg2 = InstrumentId::from("MSFT.XNAS");
7122 let spread_id = InstrumentId::from("SPREAD.XNAS");
7123 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
7124 let handler = Arc::new(Mutex::new(Vec::new()));
7125 let handler_clone = Arc::clone(&handler);
7126 let clock = Rc::new(RefCell::new(TestClock::new()));
7127
7128 let agg = SpreadQuoteAggregator::new(
7129 spread_id,
7130 &legs,
7131 true,
7132 instrument.price_precision(),
7133 0,
7134 Box::new(move |q: QuoteTick| {
7135 handler_clone.lock().expect(MUTEX_POISONED).push(q);
7136 }),
7137 clock.clone(),
7139 true,
7140 Some(1),
7141 0,
7142 false,
7143 60,
7144 None,
7145 None,
7146 );
7147 let rc = Rc::new(RefCell::new(agg));
7148 rc.borrow_mut().prepare_for_timer_mode(&rc);
7149 rc.borrow_mut().set_clock(clock);
7150
7151 let ts1 = UnixNanos::from(1_000_000_000);
7152 let ts2 = UnixNanos::from(2_000_000_000);
7153 let ts3 = UnixNanos::from(3_000_000_000);
7154 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7155 leg1,
7156 Price::from("100.00"),
7157 Price::from("100.10"),
7158 Quantity::from(10),
7159 Quantity::from(10),
7160 ts1,
7161 ts1,
7162 ));
7163 assert_eq!(handler.lock().expect(MUTEX_POISONED).len(), 0);
7164
7165 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7166 leg2,
7167 Price::from("99.00"),
7168 Price::from("99.10"),
7169 Quantity::from(10),
7170 Quantity::from(10),
7171 ts2,
7172 ts2,
7173 ));
7174 assert_eq!(handler.lock().expect(MUTEX_POISONED).len(), 0);
7175
7176 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7177 leg1,
7178 Price::from("100.00"),
7179 Price::from("100.10"),
7180 Quantity::from(10),
7181 Quantity::from(10),
7182 ts3,
7183 ts3,
7184 ));
7185 let quotes = handler.lock().expect(MUTEX_POISONED);
7186 assert_eq!(
7187 quotes.len(),
7188 1,
7189 "deferred event at ts2 is processed when we have all legs and advance to ts3"
7190 );
7191 }
7192
7193 #[rstest]
7194 fn test_spread_quote_historical_flush_emits_pending_final_quote(equity_aapl: Equity) {
7195 let instrument = InstrumentAny::Equity(equity_aapl);
7196 let leg1 = instrument.id();
7197 let leg2 = InstrumentId::from("MSFT.XNAS");
7198 let spread_id = InstrumentId::from("SPREAD.XNAS");
7199 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
7200 let handler = Arc::new(Mutex::new(Vec::new()));
7201 let handler_clone = Arc::clone(&handler);
7202 let clock = Rc::new(RefCell::new(TestClock::new()));
7203
7204 let agg = SpreadQuoteAggregator::new(
7205 spread_id,
7206 &legs,
7207 true,
7208 instrument.price_precision(),
7209 0,
7210 Box::new(move |q: QuoteTick| {
7211 handler_clone.lock().expect(MUTEX_POISONED).push(q);
7212 }),
7213 clock.clone(),
7215 true,
7216 Some(1),
7217 0,
7218 false,
7219 60,
7220 None,
7221 None,
7222 );
7223 let rc = Rc::new(RefCell::new(agg));
7224 rc.borrow_mut().prepare_for_timer_mode(&rc);
7225 rc.borrow_mut().set_clock(clock);
7226
7227 let ts1 = UnixNanos::from(1_000_000_000);
7228 let ts2 = UnixNanos::from(2_000_000_000);
7229 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7230 leg1,
7231 Price::from("100.00"),
7232 Price::from("100.10"),
7233 Quantity::from(10),
7234 Quantity::from(10),
7235 ts1,
7236 ts1,
7237 ));
7238 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7239 leg2,
7240 Price::from("99.00"),
7241 Price::from("99.10"),
7242 Quantity::from(10),
7243 Quantity::from(10),
7244 ts2,
7245 ts2,
7246 ));
7247
7248 assert_eq!(handler.lock().expect(MUTEX_POISONED).len(), 0);
7249
7250 rc.borrow_mut().flush_pending_historical_quote();
7251
7252 let quotes = handler.lock().expect(MUTEX_POISONED);
7253 assert_eq!(
7254 quotes.len(),
7255 1,
7256 "final historical quote should be emitted when the deferred event is flushed",
7257 );
7258 assert_eq!(quotes[0].ts_event, ts2);
7259 }
7260
7261 #[rstest]
7262 fn test_spread_quote_option_vega_weighting(equity_aapl: Equity) {
7263 let instrument = InstrumentAny::Equity(equity_aapl);
7264 let leg1 = instrument.id();
7265 let leg2 = InstrumentId::from("MSFT.XNAS");
7266 let spread_id = InstrumentId::from("SPREAD.XNAS");
7267 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
7268 let handler = Arc::new(Mutex::new(Vec::new()));
7269 let handler_clone = Arc::clone(&handler);
7270 let clock = Rc::new(RefCell::new(TestClock::new()));
7271
7272 let mut vega_provider = MapVegaProvider::new();
7273 vega_provider.insert(leg1, 0.15);
7274 vega_provider.insert(leg2, 0.12);
7275
7276 let mut agg = SpreadQuoteAggregator::new(
7277 spread_id,
7278 &legs,
7279 false,
7280 instrument.price_precision(),
7281 0,
7282 Box::new(move |q: QuoteTick| {
7283 handler_clone.lock().expect(MUTEX_POISONED).push(q);
7284 }),
7285 clock,
7286 false,
7287 None,
7288 0,
7289 false,
7290 60,
7291 Some(Box::new(vega_provider)),
7292 None,
7293 );
7294
7295 let ts = UnixNanos::from(1_000_000_000);
7296 agg.handle_quote_tick(QuoteTick::new(
7297 leg1,
7298 Price::from("10.00"),
7299 Price::from("10.20"),
7300 Quantity::from(100),
7301 Quantity::from(100),
7302 ts,
7303 ts,
7304 ));
7305 agg.handle_quote_tick(QuoteTick::new(
7306 leg2,
7307 Price::from("11.00"),
7308 Price::from("11.20"),
7309 Quantity::from(100),
7310 Quantity::from(100),
7311 ts,
7312 ts,
7313 ));
7314 let quotes = handler.lock().expect(MUTEX_POISONED);
7315 assert_eq!(quotes.len(), 1);
7316 let q = "es[0];
7317 assert!(q.bid_price < q.ask_price);
7318 assert!(q.ask_price.as_f64() - q.bid_price.as_f64() > 0.0);
7319 }
7320
7321 #[rstest]
7322 fn test_spread_quote_all_zero_vega_fallback(equity_aapl: Equity) {
7323 let instrument = InstrumentAny::Equity(equity_aapl);
7324 let leg1 = instrument.id();
7325 let leg2 = InstrumentId::from("MSFT.XNAS");
7326 let spread_id = InstrumentId::from("SPREAD.XNAS");
7327 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
7328 let handler = Arc::new(Mutex::new(Vec::new()));
7329 let handler_clone = Arc::clone(&handler);
7330 let clock = Rc::new(RefCell::new(TestClock::new()));
7331
7332 let mut vega_provider = MapVegaProvider::new();
7333 vega_provider.insert(leg1, 0.0);
7334 vega_provider.insert(leg2, 0.0);
7335
7336 let agg = SpreadQuoteAggregator::new(
7337 spread_id,
7338 &legs,
7339 false,
7340 instrument.price_precision(),
7341 0,
7342 Box::new(move |q: QuoteTick| {
7343 handler_clone.lock().expect(MUTEX_POISONED).push(q);
7344 }),
7345 clock.clone(),
7346 false,
7347 None,
7348 0,
7349 false,
7350 1,
7351 Some(Box::new(vega_provider)),
7352 None,
7353 );
7354 let rc = Rc::new(RefCell::new(agg));
7355 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7356
7357 let ts = UnixNanos::from(1_000_000_000);
7358 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7359 leg1,
7360 Price::from("10.00"),
7361 Price::from("10.10"),
7362 Quantity::from(100),
7363 Quantity::from(100),
7364 ts,
7365 ts,
7366 ));
7367 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7368 leg2,
7369 Price::from("20.00"),
7370 Price::from("20.10"),
7371 Quantity::from(100),
7372 Quantity::from(100),
7373 ts,
7374 ts,
7375 ));
7376 {
7377 let quotes = handler.lock().expect(MUTEX_POISONED);
7378 assert_eq!(quotes.len(), 1);
7379 let q = "es[0];
7380 assert_eq!(q.bid_price, Price::from("-10.10"));
7381 assert_eq!(q.ask_price, Price::from("-9.90"));
7382 }
7383 assert!(rc.borrow().vega_pricing_temporarily_disabled);
7384
7385 let timeout_name = rc.borrow().vega_pricing_timeout_timer_name.clone();
7386 assert!(
7387 clock
7388 .borrow()
7389 .timer_names()
7390 .contains(&timeout_name.as_str())
7391 );
7392
7393 let events = clock
7394 .borrow_mut()
7395 .advance_time(UnixNanos::from(2_000_000_000), true);
7396
7397 for handler in clock.borrow().match_handlers(events) {
7398 handler.run();
7399 }
7400
7401 assert!(!rc.borrow().vega_pricing_temporarily_disabled);
7402
7403 let cancel_handler = Arc::new(Mutex::new(Vec::new()));
7404 let cancel_handler_clone = Arc::clone(&cancel_handler);
7405 let mut cancel_vega_provider = MapVegaProvider::new();
7406 cancel_vega_provider.insert(leg1, 0.0);
7407 cancel_vega_provider.insert(leg2, 0.0);
7408 let cancel_agg = SpreadQuoteAggregator::new(
7409 spread_id,
7410 &legs,
7411 false,
7412 instrument.price_precision(),
7413 0,
7414 Box::new(move |q: QuoteTick| {
7415 cancel_handler_clone.lock().expect(MUTEX_POISONED).push(q);
7416 }),
7417 clock.clone(),
7418 false,
7419 None,
7420 0,
7421 false,
7422 10,
7423 Some(Box::new(cancel_vega_provider)),
7424 None,
7425 );
7426 let cancel_rc = Rc::new(RefCell::new(cancel_agg));
7427 cancel_rc
7428 .borrow_mut()
7429 .start_timer(Some(Rc::clone(&cancel_rc)));
7430 cancel_rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7431 leg1,
7432 Price::from("10.00"),
7433 Price::from("10.10"),
7434 Quantity::from(100),
7435 Quantity::from(100),
7436 ts,
7437 ts,
7438 ));
7439 cancel_rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7440 leg2,
7441 Price::from("20.00"),
7442 Price::from("20.10"),
7443 Quantity::from(100),
7444 Quantity::from(100),
7445 ts,
7446 ts,
7447 ));
7448 let cancel_timeout_name = cancel_rc.borrow().vega_pricing_timeout_timer_name.clone();
7449 assert!(
7450 clock
7451 .borrow()
7452 .timer_names()
7453 .contains(&cancel_timeout_name.as_str())
7454 );
7455 cancel_rc.borrow_mut().stop_timer();
7456 assert!(
7457 !clock
7458 .borrow()
7459 .timer_names()
7460 .contains(&cancel_timeout_name.as_str())
7461 );
7462
7463 let permanent_handler = Arc::new(Mutex::new(Vec::new()));
7464 let permanent_handler_clone = Arc::clone(&permanent_handler);
7465 let mut permanent_vega_provider = MapVegaProvider::new();
7466 permanent_vega_provider.insert(leg1, 0.15);
7467 permanent_vega_provider.insert(leg2, 0.12);
7468 let mut permanent_agg = SpreadQuoteAggregator::new(
7469 spread_id,
7470 &legs,
7471 false,
7472 instrument.price_precision(),
7473 0,
7474 Box::new(move |q: QuoteTick| {
7475 permanent_handler_clone
7476 .lock()
7477 .expect(MUTEX_POISONED)
7478 .push(q);
7479 }),
7480 Rc::new(RefCell::new(TestClock::new())),
7481 false,
7482 None,
7483 0,
7484 true,
7485 1,
7486 Some(Box::new(permanent_vega_provider)),
7487 None,
7488 );
7489
7490 permanent_agg.handle_quote_tick(QuoteTick::new(
7491 leg1,
7492 Price::from("10.00"),
7493 Price::from("10.10"),
7494 Quantity::from(100),
7495 Quantity::from(100),
7496 ts,
7497 ts,
7498 ));
7499 permanent_agg.handle_quote_tick(QuoteTick::new(
7500 leg2,
7501 Price::from("20.00"),
7502 Price::from("20.10"),
7503 Quantity::from(100),
7504 Quantity::from(100),
7505 ts,
7506 ts,
7507 ));
7508
7509 let permanent_quotes = permanent_handler.lock().expect(MUTEX_POISONED);
7510 assert_eq!(permanent_quotes.len(), 1);
7511 assert_eq!(permanent_quotes[0].bid_price, Price::from("-10.10"));
7512 assert_eq!(permanent_quotes[0].ask_price, Price::from("-9.90"));
7513 assert!(!permanent_agg.vega_pricing_temporarily_disabled);
7514 }
7515
7516 #[rstest]
7517 fn test_spread_quote_negative_prices_tick_scheme(equity_aapl: Equity) {
7518 let instrument = InstrumentAny::Equity(equity_aapl);
7519 let leg1 = instrument.id();
7520 let leg2 = InstrumentId::from("MSFT.XNAS");
7521 let spread_id = InstrumentId::from("SPREAD.XNAS");
7522 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
7523 let handler = Arc::new(Mutex::new(Vec::new()));
7524 let handler_clone = Arc::clone(&handler);
7525 let clock = Rc::new(RefCell::new(TestClock::new()));
7526 let rounder = FixedTickSchemeRounder::new(0.01).unwrap();
7527
7528 let mut agg = SpreadQuoteAggregator::new(
7529 spread_id,
7530 &legs,
7531 true,
7532 2,
7533 0,
7534 Box::new(move |q: QuoteTick| {
7535 handler_clone.lock().expect(MUTEX_POISONED).push(q);
7536 }),
7537 clock,
7538 false,
7539 None,
7540 0,
7541 false,
7542 60,
7543 None,
7544 Some(Box::new(rounder)),
7545 );
7546
7547 let ts = UnixNanos::from(1_000_000_000);
7548 agg.handle_quote_tick(QuoteTick::new(
7549 leg1,
7550 Price::from("10.00"),
7551 Price::from("10.10"),
7552 Quantity::from(100),
7553 Quantity::from(100),
7554 ts,
7555 ts,
7556 ));
7557 agg.handle_quote_tick(QuoteTick::new(
7558 leg2,
7559 Price::from("20.00"),
7560 Price::from("20.10"),
7561 Quantity::from(100),
7562 Quantity::from(100),
7563 ts,
7564 ts,
7565 ));
7566 let quotes = handler.lock().expect(MUTEX_POISONED);
7567 assert_eq!(quotes.len(), 1);
7568 let q = "es[0];
7569 assert!(q.bid_price.as_f64() < 0.0);
7570 assert!(q.ask_price.as_f64() < 0.0);
7571 assert!(q.bid_price < q.ask_price);
7572 }
7573
7574 #[rstest]
7575 #[case(BarIntervalType::LeftOpen)]
7576 #[case(BarIntervalType::RightOpen)]
7577 fn test_time_bar_skip_first_non_full_bar_noop_on_boundary(
7578 equity_aapl: Equity,
7579 #[case] interval_type: BarIntervalType,
7580 ) {
7581 let instrument = InstrumentAny::Equity(equity_aapl);
7586 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
7587 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7588 let handler = Arc::new(Mutex::new(Vec::new()));
7589 let handler_clone = Arc::clone(&handler);
7590 let clock = Rc::new(RefCell::new(TestClock::new()));
7591 clock.borrow_mut().set_time(UnixNanos::from(1_000_000_000));
7592 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7593
7594 let aggregator = TimeBarAggregator::new(
7595 bar_type,
7596 instrument.price_precision(),
7597 instrument.size_precision(),
7598 clock,
7599 move |bar: Bar| {
7600 let mut h = handler_clone.lock().expect(MUTEX_POISONED);
7601 h.push(bar);
7602 },
7603 false,
7604 false,
7605 interval_type,
7606 None,
7607 0,
7608 true, );
7610
7611 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7612 let rc = Rc::new(RefCell::new(boxed));
7613 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7614
7615 rc.borrow_mut().update(
7616 Price::from("100.00"),
7617 Quantity::from(1),
7618 UnixNanos::from(1_000_000_000),
7619 );
7620 rc.borrow_mut().build_bar(&TimeEvent::new(
7621 event_name,
7622 UUID4::new(),
7623 UnixNanos::from(2_000_000_000),
7624 UnixNanos::from(2_000_000_000),
7625 ));
7626 rc.borrow_mut().update(
7627 Price::from("101.00"),
7628 Quantity::from(1),
7629 UnixNanos::from(2_500_000_000),
7630 );
7631 rc.borrow_mut().build_bar(&TimeEvent::new(
7632 event_name,
7633 UUID4::new(),
7634 UnixNanos::from(3_000_000_000),
7635 UnixNanos::from(3_000_000_000),
7636 ));
7637
7638 let bars = handler.lock().expect(MUTEX_POISONED);
7639 assert_eq!(bars.len(), 2);
7640 assert_eq!(bars[0].close, Price::from("100.00"));
7641 assert_eq!(bars[1].close, Price::from("101.00"));
7642 }
7643
7644 #[rstest]
7645 #[case(BarIntervalType::LeftOpen)]
7646 #[case(BarIntervalType::RightOpen)]
7647 fn test_time_bar_skip_first_non_full_bar_drops_partial_bar(
7648 equity_aapl: Equity,
7649 #[case] interval_type: BarIntervalType,
7650 ) {
7651 let instrument = InstrumentAny::Equity(equity_aapl);
7655 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
7656 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7657 let handler = Arc::new(Mutex::new(Vec::new()));
7658 let handler_clone = Arc::clone(&handler);
7659 let clock = Rc::new(RefCell::new(TestClock::new()));
7660 clock.borrow_mut().set_time(UnixNanos::from(1_500_000_000));
7661 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7662
7663 let aggregator = TimeBarAggregator::new(
7664 bar_type,
7665 instrument.price_precision(),
7666 instrument.size_precision(),
7667 clock,
7668 move |bar: Bar| {
7669 let mut h = handler_clone.lock().expect(MUTEX_POISONED);
7670 h.push(bar);
7671 },
7672 false,
7673 false,
7674 interval_type,
7675 None,
7676 0,
7677 true, );
7679
7680 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7681 let rc = Rc::new(RefCell::new(boxed));
7682 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7683
7684 rc.borrow_mut().update(
7685 Price::from("100.00"),
7686 Quantity::from(1),
7687 UnixNanos::from(1_500_000_000),
7688 );
7689 rc.borrow_mut().build_bar(&TimeEvent::new(
7690 event_name,
7691 UUID4::new(),
7692 UnixNanos::from(2_000_000_000),
7693 UnixNanos::from(2_000_000_000),
7694 ));
7695 rc.borrow_mut().update(
7696 Price::from("101.00"),
7697 Quantity::from(1),
7698 UnixNanos::from(2_500_000_000),
7699 );
7700 rc.borrow_mut().build_bar(&TimeEvent::new(
7701 event_name,
7702 UUID4::new(),
7703 UnixNanos::from(3_000_000_000),
7704 UnixNanos::from(3_000_000_000),
7705 ));
7706
7707 let bars = handler.lock().expect(MUTEX_POISONED);
7708 assert_eq!(bars.len(), 1);
7709 assert_eq!(bars[0].close, Price::from("101.00"));
7710 }
7711
7712 #[rstest]
7713 fn test_time_bar_skip_first_non_full_bar_skips_every_call_before_first_close(
7714 equity_aapl: Equity,
7715 ) {
7716 let instrument = InstrumentAny::Equity(equity_aapl);
7720 let bar_spec = BarSpecification::new(10, BarAggregation::Second, PriceType::Last);
7721 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7722 let handler = Arc::new(Mutex::new(Vec::new()));
7723 let handler_clone = Arc::clone(&handler);
7724 let clock = Rc::new(RefCell::new(TestClock::new()));
7725 clock.borrow_mut().set_time(UnixNanos::from(5_000_000_000));
7726 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7727
7728 let aggregator = TimeBarAggregator::new(
7729 bar_type,
7730 instrument.price_precision(),
7731 instrument.size_precision(),
7732 clock,
7733 move |bar: Bar| {
7734 let mut h = handler_clone.lock().expect(MUTEX_POISONED);
7735 h.push(bar);
7736 },
7737 false,
7738 false,
7739 BarIntervalType::LeftOpen,
7740 None,
7741 0,
7742 true, );
7744
7745 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7746 let rc = Rc::new(RefCell::new(boxed));
7747 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7748
7749 for (price, update_ts, event_ts) in [
7753 ("100.00", 5_500_000_000_u64, 7_000_000_000_u64),
7754 ("101.00", 7_500_000_000_u64, 8_000_000_000_u64),
7755 ("102.00", 9_000_000_000_u64, 10_000_000_000_u64),
7756 ] {
7757 rc.borrow_mut().update(
7758 Price::from(price),
7759 Quantity::from(1),
7760 UnixNanos::from(update_ts),
7761 );
7762 rc.borrow_mut().build_bar(&TimeEvent::new(
7763 event_name,
7764 UUID4::new(),
7765 UnixNanos::from(event_ts),
7766 UnixNanos::from(event_ts),
7767 ));
7768 }
7769
7770 rc.borrow_mut().update(
7772 Price::from("103.00"),
7773 Quantity::from(1),
7774 UnixNanos::from(10_500_000_000),
7775 );
7776 rc.borrow_mut().build_bar(&TimeEvent::new(
7777 event_name,
7778 UUID4::new(),
7779 UnixNanos::from(11_000_000_000),
7780 UnixNanos::from(11_000_000_000),
7781 ));
7782
7783 let bars = handler.lock().expect(MUTEX_POISONED);
7784 assert_eq!(bars.len(), 1);
7785 assert_eq!(bars[0].close, Price::from("103.00"));
7786 }
7787
7788 #[rstest]
7789 fn test_time_bar_skip_first_non_full_bar_skips_when_build_delay_shifts_start(
7790 equity_aapl: Equity,
7791 ) {
7792 let instrument = InstrumentAny::Equity(equity_aapl);
7797 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
7798 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7799 let handler = Arc::new(Mutex::new(Vec::new()));
7800 let handler_clone = Arc::clone(&handler);
7801 let clock = Rc::new(RefCell::new(TestClock::new()));
7802 clock.borrow_mut().set_time(UnixNanos::from(2_000_000_000));
7803 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7804
7805 let aggregator = TimeBarAggregator::new(
7806 bar_type,
7807 instrument.price_precision(),
7808 instrument.size_precision(),
7809 clock,
7810 move |bar: Bar| {
7811 let mut h = handler_clone.lock().expect(MUTEX_POISONED);
7812 h.push(bar);
7813 },
7814 false,
7815 false,
7816 BarIntervalType::LeftOpen,
7817 None,
7818 100, true, );
7821
7822 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7823 let rc = Rc::new(RefCell::new(boxed));
7824 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7825
7826 rc.borrow_mut().update(
7828 Price::from("100.00"),
7829 Quantity::from(1),
7830 UnixNanos::from(2_500_000_000),
7831 );
7832 rc.borrow_mut().build_bar(&TimeEvent::new(
7833 event_name,
7834 UUID4::new(),
7835 UnixNanos::from(3_000_100_000),
7836 UnixNanos::from(3_000_100_000),
7837 ));
7838 rc.borrow_mut().update(
7839 Price::from("101.00"),
7840 Quantity::from(1),
7841 UnixNanos::from(3_500_000_000),
7842 );
7843 rc.borrow_mut().build_bar(&TimeEvent::new(
7844 event_name,
7845 UUID4::new(),
7846 UnixNanos::from(4_000_100_000),
7847 UnixNanos::from(4_000_100_000),
7848 ));
7849
7850 let bars = handler.lock().expect(MUTEX_POISONED);
7851 assert_eq!(bars.len(), 1);
7852 assert_eq!(bars[0].close, Price::from("101.00"));
7853 }
7854
7855 #[rstest]
7856 #[case(
7857 BarAggregation::Month,
7858 1_735_689_600_000_000_000_u64,
7859 1_733_011_200_000_000_000_u64
7860 )]
7861 #[case(
7862 BarAggregation::Year,
7863 1_735_689_600_000_000_000_u64,
7864 1_704_067_200_000_000_000_u64
7865 )]
7866 fn test_time_bar_fire_immediately_month_year_stored_open_points_to_previous_period(
7867 equity_aapl: Equity,
7868 #[case] aggregation: BarAggregation,
7869 #[case] start_ns: u64,
7870 #[case] expected_stored_open_ns: u64,
7871 ) {
7872 let instrument = InstrumentAny::Equity(equity_aapl);
7877 let bar_spec = BarSpecification::new(1, aggregation, PriceType::Last);
7878 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7879 let handler = Arc::new(Mutex::new(Vec::new()));
7880 let handler_clone = Arc::clone(&handler);
7881 let clock = Rc::new(RefCell::new(TestClock::new()));
7882 clock.borrow_mut().set_time(UnixNanos::from(start_ns));
7883 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7884
7885 let aggregator = TimeBarAggregator::new(
7886 bar_type,
7887 instrument.price_precision(),
7888 instrument.size_precision(),
7889 clock,
7890 move |bar: Bar| {
7891 let mut h = handler_clone.lock().expect(MUTEX_POISONED);
7892 h.push(bar);
7893 },
7894 false,
7895 false,
7896 BarIntervalType::RightOpen, None,
7898 0,
7899 false, );
7901
7902 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7903 let rc = Rc::new(RefCell::new(boxed));
7904 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7905
7906 rc.borrow_mut().update(
7907 Price::from("100.00"),
7908 Quantity::from(1),
7909 UnixNanos::from(start_ns),
7910 );
7911 rc.borrow_mut().build_bar(&TimeEvent::new(
7912 event_name,
7913 UUID4::new(),
7914 UnixNanos::from(start_ns),
7915 UnixNanos::from(start_ns),
7916 ));
7917
7918 let bars = handler.lock().expect(MUTEX_POISONED);
7919 assert_eq!(bars.len(), 1);
7920 assert_eq!(bars[0].ts_event, UnixNanos::from(expected_stored_open_ns));
7921 assert_eq!(bars[0].ts_init, UnixNanos::from(start_ns));
7922 }
7923
7924 #[rstest]
7925 fn test_time_bar_historical_prevents_bars_for_timer_before_last_data(equity_aapl: Equity) {
7926 let instrument = InstrumentAny::Equity(equity_aapl);
7927 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
7928 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7929 let handler = Arc::new(Mutex::new(Vec::new()));
7930 let handler_clone = Arc::clone(&handler);
7931 let clock = Rc::new(RefCell::new(TestClock::new()));
7932
7933 let mut agg = TimeBarAggregator::new(
7934 bar_type,
7935 instrument.price_precision(),
7936 instrument.size_precision(),
7937 clock.clone(),
7938 move |bar: Bar| {
7939 let mut h = handler_clone.lock().expect(MUTEX_POISONED);
7940 h.push(bar);
7941 },
7942 true,
7943 true,
7944 BarIntervalType::LeftOpen,
7945 None,
7946 0,
7947 false,
7948 );
7949 agg.historical_mode = true;
7950 agg.set_clock_internal(clock);
7951 let boxed: Box<dyn BarAggregator> = Box::new(agg);
7952 let rc = Rc::new(RefCell::new(boxed));
7953 rc.borrow_mut().set_aggregator_weak(Rc::downgrade(&rc));
7954
7955 let ts1 = UnixNanos::from(2_000_000_000);
7956 rc.borrow_mut()
7957 .update(Price::from("100.00"), Quantity::from(1), ts1);
7958
7959 let ts2 = UnixNanos::from(3_000_000_000);
7960 rc.borrow_mut()
7961 .update(Price::from("101.00"), Quantity::from(1), ts2);
7962
7963 let bars = handler.lock().expect(MUTEX_POISONED);
7964 assert!(
7965 !bars.is_empty(),
7966 "advancing time from ts1 to ts2 should produce at least one bar"
7967 );
7968 assert_eq!(bars[0].close, Price::from("100.00"));
7969 }
7970
7971 #[rstest]
7972 #[case(BarAggregation::Tick)]
7973 #[case(BarAggregation::TickImbalance)]
7974 #[case(BarAggregation::TickRuns)]
7975 #[case(BarAggregation::Volume)]
7976 #[case(BarAggregation::VolumeImbalance)]
7977 #[case(BarAggregation::VolumeRuns)]
7978 #[case(BarAggregation::Value)]
7979 #[case(BarAggregation::ValueImbalance)]
7980 #[case(BarAggregation::ValueRuns)]
7981 #[case(BarAggregation::Renko)]
7982 fn test_aggregators_standardize_composite_bar_type(
7983 equity_aapl: Equity,
7984 #[case] aggregation: BarAggregation,
7985 ) {
7986 let instrument = InstrumentAny::Equity(equity_aapl);
7987 let bar_type = BarType::new_composite(
7988 instrument.id(),
7989 BarSpecification::new(10, aggregation, PriceType::Last),
7990 AggregationSource::Internal,
7991 1,
7992 BarAggregation::Minute,
7993 AggregationSource::External,
7994 );
7995 let handler = |_: Bar| {};
7996
7997 let aggregator: Box<dyn BarAggregator> = match aggregation {
7998 BarAggregation::Tick => Box::new(TickBarAggregator::new(
7999 bar_type,
8000 instrument.price_precision(),
8001 instrument.size_precision(),
8002 handler,
8003 )),
8004 BarAggregation::TickImbalance => Box::new(TickImbalanceBarAggregator::new(
8005 bar_type,
8006 instrument.price_precision(),
8007 instrument.size_precision(),
8008 handler,
8009 )),
8010 BarAggregation::TickRuns => Box::new(TickRunsBarAggregator::new(
8011 bar_type,
8012 instrument.price_precision(),
8013 instrument.size_precision(),
8014 handler,
8015 )),
8016 BarAggregation::Volume => Box::new(VolumeBarAggregator::new(
8017 bar_type,
8018 instrument.price_precision(),
8019 instrument.size_precision(),
8020 handler,
8021 )),
8022 BarAggregation::VolumeImbalance => Box::new(VolumeImbalanceBarAggregator::new(
8023 bar_type,
8024 instrument.price_precision(),
8025 instrument.size_precision(),
8026 handler,
8027 )),
8028 BarAggregation::VolumeRuns => Box::new(VolumeRunsBarAggregator::new(
8029 bar_type,
8030 instrument.price_precision(),
8031 instrument.size_precision(),
8032 handler,
8033 )),
8034 BarAggregation::Value => Box::new(ValueBarAggregator::new(
8035 bar_type,
8036 instrument.price_precision(),
8037 instrument.size_precision(),
8038 handler,
8039 )),
8040 BarAggregation::ValueImbalance => Box::new(ValueImbalanceBarAggregator::new(
8041 bar_type,
8042 instrument.price_precision(),
8043 instrument.size_precision(),
8044 handler,
8045 )),
8046 BarAggregation::ValueRuns => Box::new(ValueRunsBarAggregator::new(
8047 bar_type,
8048 instrument.price_precision(),
8049 instrument.size_precision(),
8050 handler,
8051 )),
8052 BarAggregation::Renko => Box::new(RenkoBarAggregator::new(
8053 bar_type,
8054 instrument.price_precision(),
8055 instrument.size_precision(),
8056 Price::from("0.01"),
8057 handler,
8058 )),
8059 _ => unreachable!(),
8060 };
8061
8062 assert!(aggregator.bar_type().is_standard());
8063 assert_eq!(aggregator.bar_type(), bar_type.standard());
8064 }
8065
8066 #[rstest]
8067 fn test_composite_tick_bar_aggregator_emits_standard_bar_type(equity_aapl: Equity) {
8068 let instrument = InstrumentAny::Equity(equity_aapl);
8069 let bar_type = BarType::new_composite(
8070 instrument.id(),
8071 BarSpecification::new(1, BarAggregation::Tick, PriceType::Last),
8072 AggregationSource::Internal,
8073 1,
8074 BarAggregation::Minute,
8075 AggregationSource::External,
8076 );
8077 let handler = Arc::new(Mutex::new(Vec::new()));
8078 let handler_clone = Arc::clone(&handler);
8079
8080 let mut aggregator = TickBarAggregator::new(
8081 bar_type,
8082 instrument.price_precision(),
8083 instrument.size_precision(),
8084 move |bar: Bar| {
8085 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
8086 handler_guard.push(bar);
8087 },
8088 );
8089
8090 let input_bar = Bar::new(
8091 bar_type.composite(),
8092 Price::from("100.00"),
8093 Price::from("101.00"),
8094 Price::from("99.00"),
8095 Price::from("100.50"),
8096 Quantity::from(10),
8097 UnixNanos::from(1_000),
8098 UnixNanos::from(1_000),
8099 );
8100 aggregator.handle_bar(input_bar);
8101
8102 let handler_guard = handler.lock().expect(MUTEX_POISONED);
8103 assert_eq!(handler_guard.len(), 1);
8104 assert_eq!(handler_guard[0].bar_type, bar_type.standard());
8105 }
8106
8107 #[rstest]
8108 fn test_composite_time_bar_aggregator_uses_standard_timer_name(equity_aapl: Equity) {
8109 let instrument = InstrumentAny::Equity(equity_aapl);
8110 let bar_type = BarType::new_composite(
8111 instrument.id(),
8112 BarSpecification::new(5, BarAggregation::Minute, PriceType::Last),
8113 AggregationSource::Internal,
8114 1,
8115 BarAggregation::Minute,
8116 AggregationSource::External,
8117 );
8118 let clock = Rc::new(RefCell::new(TestClock::new()));
8119
8120 let aggregator = TimeBarAggregator::new(
8121 bar_type,
8122 instrument.price_precision(),
8123 instrument.size_precision(),
8124 clock.clone(),
8125 |_: Bar| {},
8126 false,
8127 true,
8128 BarIntervalType::LeftOpen,
8129 None,
8130 0,
8131 false,
8132 );
8133
8134 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
8135 let rc = Rc::new(RefCell::new(boxed));
8136 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
8137
8138 let expected = format!("TIME_BAR_{}", bar_type.standard());
8139 assert!(
8140 clock.borrow().timer_names().contains(&expected.as_str()),
8141 "timer names {:?} should contain {expected}",
8142 clock.borrow().timer_names(),
8143 );
8144 }
8145}
8146
8147#[cfg(test)]
8148mod property_tests {
8149 use std::{
8150 cell::RefCell,
8151 rc::Rc,
8152 sync::{Arc, Mutex},
8153 };
8154
8155 use nautilus_common::{clock::TestClock, timer::TimeEvent};
8156 use nautilus_core::{MUTEX_POISONED, UUID4, UnixNanos};
8157 use nautilus_model::{
8158 data::{Bar, BarSpecification, BarType, TradeTick, bar::get_bar_interval_ns},
8159 enums::{AggregationSource, AggressorSide, BarAggregation, BarIntervalType, PriceType},
8160 instruments::{Instrument, InstrumentAny, stubs::equity_aapl},
8161 types::{Price, Quantity},
8162 };
8163 use proptest::prelude::*;
8164 use rstest::rstest;
8165 use ustr::Ustr;
8166
8167 use super::*;
8168
8169 fn time_bar_spec_strategy() -> impl Strategy<Value = (BarAggregation, usize)> {
8170 prop_oneof![
8171 (Just(BarAggregation::Second), 1usize..=5),
8172 (Just(BarAggregation::Minute), 1usize..=5),
8173 (Just(BarAggregation::Hour), 1usize..=4),
8174 ]
8175 }
8176
8177 fn interval_type_strategy() -> impl Strategy<Value = BarIntervalType> {
8178 prop_oneof![
8179 Just(BarIntervalType::LeftOpen),
8180 Just(BarIntervalType::RightOpen),
8181 ]
8182 }
8183
8184 proptest! {
8185 #[rstest]
8186 fn prop_skip_first_drops_partial_then_emits(
8187 (aggregation, step) in time_bar_spec_strategy(),
8188 interval_type in interval_type_strategy(),
8189 skip_first in any::<bool>(),
8190 ) {
8191 let instrument = InstrumentAny::Equity(equity_aapl());
8192 let bar_spec = BarSpecification::new(step, aggregation, PriceType::Last);
8193 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8194 let interval_ns = get_bar_interval_ns(&bar_type).as_u64();
8195
8196 let now_ns = interval_ns + interval_ns / 2;
8199
8200 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
8201 let handler_clone = Arc::clone(&handler);
8202 let clock = Rc::new(RefCell::new(TestClock::new()));
8203 clock.borrow_mut().set_time(UnixNanos::from(now_ns));
8204 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
8205
8206 let aggregator = TimeBarAggregator::new(
8207 bar_type,
8208 instrument.price_precision(),
8209 instrument.size_precision(),
8210 clock,
8211 move |bar: Bar| {
8212 let mut h = handler_clone.lock().expect(MUTEX_POISONED);
8213 h.push(bar);
8214 },
8215 false,
8216 false,
8217 interval_type,
8218 None,
8219 0,
8220 skip_first,
8221 );
8222
8223 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
8224 let rc = Rc::new(RefCell::new(boxed));
8225 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
8226
8227 rc.borrow_mut().update(
8230 Price::from("100.00"),
8231 Quantity::from(1),
8232 UnixNanos::from(now_ns),
8233 );
8234 let first_close = 2 * interval_ns;
8235 rc.borrow_mut().build_bar(&TimeEvent::new(
8236 event_name,
8237 UUID4::new(),
8238 UnixNanos::from(first_close),
8239 UnixNanos::from(first_close),
8240 ));
8241
8242 rc.borrow_mut().update(
8244 Price::from("101.00"),
8245 Quantity::from(1),
8246 UnixNanos::from(first_close + interval_ns / 2),
8247 );
8248 let second_close = first_close + interval_ns;
8249 rc.borrow_mut().build_bar(&TimeEvent::new(
8250 event_name,
8251 UUID4::new(),
8252 UnixNanos::from(second_close),
8253 UnixNanos::from(second_close),
8254 ));
8255
8256 let bars = handler.lock().expect(MUTEX_POISONED);
8257 let expected = if skip_first { 1 } else { 2 };
8258 prop_assert_eq!(bars.len(), expected);
8259 prop_assert_eq!(bars.last().unwrap().close, Price::from("101.00"));
8260 for bar in bars.iter() {
8261 prop_assert!(bar.high >= bar.open);
8262 prop_assert!(bar.high >= bar.close);
8263 prop_assert!(bar.low <= bar.open);
8264 prop_assert!(bar.low <= bar.close);
8265 }
8266 }
8267
8268 #[rstest]
8269 fn prop_skip_first_noop_on_exact_boundary(
8270 (aggregation, step) in time_bar_spec_strategy(),
8271 interval_type in interval_type_strategy(),
8272 ) {
8273 let instrument = InstrumentAny::Equity(equity_aapl());
8274 let bar_spec = BarSpecification::new(step, aggregation, PriceType::Last);
8275 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8276 let interval_ns = get_bar_interval_ns(&bar_type).as_u64();
8277
8278 let now_ns = interval_ns;
8281 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
8282 let handler_clone = Arc::clone(&handler);
8283 let clock = Rc::new(RefCell::new(TestClock::new()));
8284 clock.borrow_mut().set_time(UnixNanos::from(now_ns));
8285 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
8286
8287 let aggregator = TimeBarAggregator::new(
8288 bar_type,
8289 instrument.price_precision(),
8290 instrument.size_precision(),
8291 clock,
8292 move |bar: Bar| {
8293 let mut h = handler_clone.lock().expect(MUTEX_POISONED);
8294 h.push(bar);
8295 },
8296 false,
8297 false,
8298 interval_type,
8299 None,
8300 0,
8301 true, );
8303
8304 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
8305 let rc = Rc::new(RefCell::new(boxed));
8306 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
8307
8308 rc.borrow_mut().update(
8309 Price::from("100.00"),
8310 Quantity::from(1),
8311 UnixNanos::from(now_ns),
8312 );
8313 let next_close = now_ns + interval_ns;
8314 rc.borrow_mut().build_bar(&TimeEvent::new(
8315 event_name,
8316 UUID4::new(),
8317 UnixNanos::from(next_close),
8318 UnixNanos::from(next_close),
8319 ));
8320
8321 let bars = handler.lock().expect(MUTEX_POISONED);
8322 prop_assert_eq!(bars.len(), 1);
8323 prop_assert_eq!(bars[0].close, Price::from("100.00"));
8324 }
8325
8326 #[rstest]
8327 fn prop_bar_builder_ohlc_invariants(
8328 updates in prop::collection::vec((1i64..=100_000i64, 1u64..=1_000u64), 1..=50),
8329 ) {
8330 let instrument = InstrumentAny::Equity(equity_aapl());
8331 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
8332 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8333 let mut builder = BarBuilder::new(bar_type, 2, 0);
8334
8335 let mut total_volume: u64 = 0;
8336
8337 for (i, (price_cents, size)) in updates.iter().enumerate() {
8338 let price = Price::new((*price_cents as f64) / 100.0, 2);
8339 let qty = Quantity::new(*size as f64, 0);
8340 let ts = UnixNanos::from((i as u64 + 1) * 1_000);
8341 total_volume += *size;
8342 builder.update(price, qty, ts);
8343 }
8344
8345 let bar = builder.build_now();
8346 prop_assert!(bar.low <= bar.open);
8347 prop_assert!(bar.low <= bar.close);
8348 prop_assert!(bar.high >= bar.open);
8349 prop_assert!(bar.high >= bar.close);
8350 prop_assert!(bar.low <= bar.high);
8351 prop_assert_eq!(bar.volume.as_f64(), total_volume as f64);
8352 }
8353
8354 #[rstest]
8355 fn prop_tick_bar_aggregator_volume_conservation(
8356 ticks in prop::collection::vec((1i64..=1_000i64, 1u64..=100u64), 3..=60),
8357 step in 1usize..=5,
8358 ) {
8359 let instrument = InstrumentAny::Equity(equity_aapl());
8360 let bar_spec = BarSpecification::new(step, BarAggregation::Tick, PriceType::Last);
8361 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8362 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
8363 let handler_clone = Arc::clone(&handler);
8364
8365 let mut aggregator = TickBarAggregator::new(
8366 bar_type,
8367 instrument.price_precision(),
8368 instrument.size_precision(),
8369 move |bar: Bar| {
8370 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
8371 },
8372 );
8373
8374 let mut total_input: u64 = 0;
8375
8376 for (i, (price_cents, size)) in ticks.iter().enumerate() {
8377 let price = Price::new((*price_cents as f64) / 100.0, 2);
8378 let qty = Quantity::new(*size as f64, 0);
8379 aggregator.update(price, qty, UnixNanos::from((i as u64 + 1) * 1_000));
8380 total_input += *size;
8381 }
8382
8383 let bars = handler.lock().expect(MUTEX_POISONED);
8384 let emitted_count = bars.len();
8385 prop_assert_eq!(emitted_count, ticks.len() / step);
8386
8387 let mut sum_emitted: f64 = 0.0;
8388
8389 for bar in bars.iter() {
8390 prop_assert!(bar.low <= bar.open);
8391 prop_assert!(bar.low <= bar.close);
8392 prop_assert!(bar.high >= bar.open);
8393 prop_assert!(bar.high >= bar.close);
8394 sum_emitted += bar.volume.as_f64();
8395 }
8396
8397 let pending_size: u64 = ticks.iter()
8399 .skip(emitted_count * step)
8400 .map(|(_, s)| *s)
8401 .sum();
8402 prop_assert!((sum_emitted + pending_size as f64 - total_input as f64).abs() < 1e-6);
8403 }
8404
8405 #[rstest]
8406 fn prop_volume_bar_aggregator_conservation(
8407 sizes in prop::collection::vec(1u64..=50u64, 3..=40),
8408 step in 2u64..=10u64,
8409 ) {
8410 let instrument = InstrumentAny::Equity(equity_aapl());
8411 let bar_spec = BarSpecification::new(step as usize, BarAggregation::Volume, PriceType::Last);
8412 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8413 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
8414 let handler_clone = Arc::clone(&handler);
8415
8416 let mut aggregator = VolumeBarAggregator::new(
8417 bar_type,
8418 instrument.price_precision(),
8419 instrument.size_precision(),
8420 move |bar: Bar| {
8421 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
8422 },
8423 );
8424
8425 let mut total_input: u64 = 0;
8426
8427 for (i, size) in sizes.iter().enumerate() {
8428 aggregator.update(
8429 Price::from("100.00"),
8430 Quantity::new(*size as f64, 0),
8431 UnixNanos::from((i as u64 + 1) * 1_000),
8432 );
8433 total_input += *size;
8434 }
8435
8436 let bars = handler.lock().expect(MUTEX_POISONED);
8437
8438 for bar in bars.iter() {
8440 prop_assert_eq!(bar.volume, Quantity::from(step));
8441 prop_assert!(bar.low <= bar.open);
8442 prop_assert!(bar.low <= bar.close);
8443 prop_assert!(bar.high >= bar.open);
8444 prop_assert!(bar.high >= bar.close);
8445 }
8446
8447 let emitted_total: u64 = bars.len() as u64 * step;
8449 let pending = aggregator.core.builder.volume.as_f64();
8450 prop_assert!((emitted_total as f64 + pending - total_input as f64).abs() < 1e-6);
8451 }
8452
8453 #[rstest]
8454 fn prop_volume_bar_matches_unit_trade_reference(
8455 updates in prop::collection::vec((1i64..=100_000i64, 1u64..=8u64, 0u64..=30u64), 1..=30),
8456 step in 1usize..=5,
8457 ) {
8458 let instrument = InstrumentAny::Equity(equity_aapl());
8459 let bar_spec = BarSpecification::new(step, BarAggregation::Volume, PriceType::Last);
8460 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8461 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
8462 let handler_clone = Arc::clone(&handler);
8463 let mut aggregator = VolumeBarAggregator::new(
8464 bar_type,
8465 instrument.price_precision(),
8466 instrument.size_precision(),
8467 move |bar: Bar| {
8468 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
8469 },
8470 );
8471 let price = |cents| {
8472 Price::from_decimal_dp(Decimal::new(cents, 2), 2)
8473 .expect("bounded cents must produce a valid price")
8474 };
8475 let mut last_timestamp = UnixNanos::default();
8476 let mut pending_units = Vec::new();
8477 let mut expected_bars = Vec::new();
8478
8479 for (price_cents, size, timestamp) in &updates {
8480 let timestamp = UnixNanos::from(*timestamp);
8481 aggregator.update(price(*price_cents), Quantity::from(*size), timestamp);
8482
8483 if timestamp < last_timestamp {
8484 continue;
8485 }
8486
8487 last_timestamp = timestamp;
8488 for _ in 0..*size {
8489 pending_units.push((*price_cents, timestamp));
8490 }
8491
8492 while pending_units.len() >= step {
8493 let units: Vec<_> = pending_units.drain(..step).collect();
8494 let first = units.first().unwrap();
8495 let last = units.last().unwrap();
8496 let low = units.iter().map(|(cents, _)| *cents).min().unwrap();
8497 let high = units.iter().map(|(cents, _)| *cents).max().unwrap();
8498 expected_bars.push((
8499 price(first.0),
8500 price(high),
8501 price(low),
8502 price(last.0),
8503 Quantity::from(step as u64),
8504 last.1,
8505 ));
8506 }
8507 }
8508
8509 let bars = handler.lock().expect(MUTEX_POISONED);
8510 prop_assert_eq!(bars.len(), expected_bars.len());
8511 for (actual, (open, high, low, close, volume, timestamp))
8512 in bars.iter().zip(expected_bars)
8513 {
8514 prop_assert_eq!(actual.open, open);
8515 prop_assert_eq!(actual.high, high);
8516 prop_assert_eq!(actual.low, low);
8517 prop_assert_eq!(actual.close, close);
8518 prop_assert_eq!(actual.volume, volume);
8519 prop_assert_eq!(actual.ts_event, timestamp);
8520 prop_assert_eq!(actual.ts_init, timestamp);
8521 }
8522
8523 prop_assert_eq!(aggregator.core.builder.volume, Quantity::from(pending_units.len() as u64));
8524 prop_assert_eq!(aggregator.core.builder.ts_last, last_timestamp);
8525
8526 if let Some((first, rest)) = pending_units.split_first() {
8527 let last = rest.last().unwrap_or(first);
8528 let low = pending_units.iter().map(|(cents, _)| *cents).min().unwrap();
8529 let high = pending_units.iter().map(|(cents, _)| *cents).max().unwrap();
8530 prop_assert_eq!(aggregator.core.builder.open, Some(price(first.0)));
8531 prop_assert_eq!(aggregator.core.builder.high, Some(price(high)));
8532 prop_assert_eq!(aggregator.core.builder.low, Some(price(low)));
8533 prop_assert_eq!(aggregator.core.builder.close, Some(price(last.0)));
8534 } else {
8535 prop_assert_eq!(aggregator.core.builder.open, None);
8536 prop_assert_eq!(aggregator.core.builder.high, None);
8537 prop_assert_eq!(aggregator.core.builder.low, None);
8538 prop_assert_eq!(aggregator.core.builder.close, None);
8539 }
8540 }
8541
8542 #[rstest]
8543 fn prop_bar_builder_spread_adjustment_is_additive(
8544 updates in prop::collection::vec((10_000i64..=100_000i64, 1u64..=100u64), 1..=20),
8545 spread_cents in -10_000i64..=10_000i64,
8546 backward in any::<bool>(),
8547 ) {
8548 let instrument = InstrumentAny::Equity(equity_aapl());
8549 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
8550 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8551 let mut builder = BarBuilder::new(bar_type, 2, 0);
8552
8553 let spread = Decimal::new(spread_cents, 2);
8554 let mode = if backward {
8555 ContinuousFutureAdjustmentType::BackwardSpread
8556 } else {
8557 ContinuousFutureAdjustmentType::ForwardSpread
8558 };
8559 builder.set_adjustment(spread, mode);
8560
8561 let mut min_cents = i64::MAX;
8562 let mut max_cents = i64::MIN;
8563
8564 for (i, (price_cents, size)) in updates.iter().enumerate() {
8565 if *price_cents < min_cents {
8566 min_cents = *price_cents;
8567 }
8568
8569 if *price_cents > max_cents {
8570 max_cents = *price_cents;
8571 }
8572
8573 builder.update(
8574 Price::new((*price_cents as f64) / 100.0, 2),
8575 Quantity::new(*size as f64, 0),
8576 UnixNanos::from((i as u64 + 1) * 1_000),
8577 );
8578 }
8579
8580 let bar = builder.build_now();
8581 let first_decimal = Decimal::new(updates.first().unwrap().0, 2);
8582 let last_decimal = Decimal::new(updates.last().unwrap().0, 2);
8583 let min_decimal = Decimal::new(min_cents, 2);
8584 let max_decimal = Decimal::new(max_cents, 2);
8585
8586 prop_assert_eq!(bar.open.as_decimal(), first_decimal + spread);
8587 prop_assert_eq!(bar.close.as_decimal(), last_decimal + spread);
8588 prop_assert_eq!(bar.low.as_decimal(), min_decimal + spread);
8589 prop_assert_eq!(bar.high.as_decimal(), max_decimal + spread);
8590 }
8591
8592 #[rstest]
8593 fn prop_bar_builder_inactive_adjustment_is_identity(
8594 updates in prop::collection::vec((1i64..=100_000i64, 1u64..=1_000u64), 1..=20),
8595 use_ratio in any::<bool>(),
8596 ) {
8597 let instrument = InstrumentAny::Equity(equity_aapl());
8598 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
8599 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8600
8601 let mut adjusted = BarBuilder::new(bar_type, 2, 0);
8602 let mut baseline = BarBuilder::new(bar_type, 2, 0);
8603
8604 let (input, mode) = if use_ratio {
8606 (Decimal::ONE, ContinuousFutureAdjustmentType::BackwardRatio)
8607 } else {
8608 (Decimal::ZERO, ContinuousFutureAdjustmentType::BackwardSpread)
8609 };
8610 adjusted.set_adjustment(input, mode);
8611
8612 for (i, (price_cents, size)) in updates.iter().enumerate() {
8613 let price = Price::new((*price_cents as f64) / 100.0, 2);
8614 let qty = Quantity::new(*size as f64, 0);
8615 let ts = UnixNanos::from((i as u64 + 1) * 1_000);
8616 adjusted.update(price, qty, ts);
8617 baseline.update(price, qty, ts);
8618 }
8619
8620 let bar_adjusted = adjusted.build_now();
8621 let bar_baseline = baseline.build_now();
8622 prop_assert_eq!(bar_adjusted.open, bar_baseline.open);
8623 prop_assert_eq!(bar_adjusted.high, bar_baseline.high);
8624 prop_assert_eq!(bar_adjusted.low, bar_baseline.low);
8625 prop_assert_eq!(bar_adjusted.close, bar_baseline.close);
8626 prop_assert_eq!(bar_adjusted.volume, bar_baseline.volume);
8627 }
8628
8629 #[rstest]
8630 fn prop_bar_builder_spread_preserves_raw_arithmetic(
8631 updates in prop::collection::vec((10_000i64..=100_000i64, 1u64..=100u64), 1..=20),
8632 spread_micro in -10_000i64..=10_000i64,
8635 ) {
8636 let instrument = InstrumentAny::Equity(equity_aapl());
8637 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
8638 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8639 let mut builder = BarBuilder::new(bar_type, 2, 0);
8640
8641 let spread = Decimal::new(spread_micro, 4);
8642 builder.set_adjustment(spread, ContinuousFutureAdjustmentType::BackwardSpread);
8643
8644 let adjustment_raw_i128 = mantissa_exponent_to_fixed_i128(
8645 spread.mantissa(),
8646 -(spread.scale() as i8),
8647 FIXED_PRECISION,
8648 )
8649 .expect("scale within range");
8650 #[allow(
8651 clippy::useless_conversion,
8652 reason = "i128 to PriceRaw is real when not high-precision"
8653 )]
8654 let expected_adjustment_raw: PriceRaw =
8655 adjustment_raw_i128.try_into().expect("within PriceRaw range");
8656
8657 let mut min_cents = i64::MAX;
8658 let mut max_cents = i64::MIN;
8659 let mut last_price = Price::new(0.0, 2);
8660 let mut first_price = Price::new(0.0, 2);
8661
8662 for (i, (price_cents, size)) in updates.iter().enumerate() {
8663 if *price_cents < min_cents {
8664 min_cents = *price_cents;
8665 }
8666
8667 if *price_cents > max_cents {
8668 max_cents = *price_cents;
8669 }
8670
8671 let price = Price::new((*price_cents as f64) / 100.0, 2);
8672
8673 if i == 0 {
8674 first_price = price;
8675 }
8676
8677 last_price = price;
8678 builder.update(
8679 price,
8680 Quantity::new(*size as f64, 0),
8681 UnixNanos::from((i as u64 + 1) * 1_000),
8682 );
8683 }
8684
8685 let bar = builder.build_now();
8686 let min_price = Price::new((min_cents as f64) / 100.0, 2);
8687 let max_price = Price::new((max_cents as f64) / 100.0, 2);
8688 prop_assert_eq!(bar.open.raw, first_price.raw + expected_adjustment_raw);
8689 prop_assert_eq!(bar.close.raw, last_price.raw + expected_adjustment_raw);
8690 prop_assert_eq!(bar.low.raw, min_price.raw + expected_adjustment_raw);
8691 prop_assert_eq!(bar.high.raw, max_price.raw + expected_adjustment_raw);
8692 prop_assert_eq!(bar.open.precision, 2);
8693 prop_assert_eq!(bar.high.precision, 2);
8694 prop_assert_eq!(bar.low.precision, 2);
8695 prop_assert_eq!(bar.close.precision, 2);
8696 }
8697
8698 #[rstest]
8699 fn prop_bar_builder_active_ratio_scales_each_ohlc(
8700 updates in prop::collection::vec((1_000i64..=100_000i64, 1u64..=100u64), 1..=20),
8701 ratio_centi in prop_oneof![50i64..=99i64, 101i64..=200i64],
8703 backward in any::<bool>(),
8704 ) {
8705 let instrument = InstrumentAny::Equity(equity_aapl());
8706 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
8707 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8708 let mut builder = BarBuilder::new(bar_type, 2, 0);
8709
8710 let ratio_decimal = Decimal::new(ratio_centi, 2);
8711 let ratio_f64 = (ratio_centi as f64) / 100.0;
8712 let mode = if backward {
8713 ContinuousFutureAdjustmentType::BackwardRatio
8714 } else {
8715 ContinuousFutureAdjustmentType::ForwardRatio
8716 };
8717 builder.set_adjustment(ratio_decimal, mode);
8718
8719 let mut min_cents = i64::MAX;
8720 let mut max_cents = i64::MIN;
8721 let mut first_cents = 0i64;
8722 let mut last_cents = 0i64;
8723
8724 for (i, (price_cents, size)) in updates.iter().enumerate() {
8725 if *price_cents < min_cents {
8726 min_cents = *price_cents;
8727 }
8728
8729 if *price_cents > max_cents {
8730 max_cents = *price_cents;
8731 }
8732
8733 if i == 0 {
8734 first_cents = *price_cents;
8735 }
8736
8737 last_cents = *price_cents;
8738 builder.update(
8739 Price::new((*price_cents as f64) / 100.0, 2),
8740 Quantity::new(*size as f64, 0),
8741 UnixNanos::from((i as u64 + 1) * 1_000),
8742 );
8743 }
8744
8745 let bar = builder.build_now();
8746 let expect = |cents: i64| Price::new((cents as f64) / 100.0 * ratio_f64, 2);
8748 prop_assert_eq!(bar.open, expect(first_cents));
8749 prop_assert_eq!(bar.close, expect(last_cents));
8750 prop_assert_eq!(bar.low, expect(min_cents));
8752 prop_assert_eq!(bar.high, expect(max_cents));
8753 }
8754
8755 #[rstest]
8756 fn prop_bar_builder_spread_mode_direction_is_metadata_only(
8757 updates in prop::collection::vec((10_000i64..=100_000i64, 1u64..=100u64), 1..=20),
8758 spread_cents in -10_000i64..=10_000i64,
8759 ) {
8760 let instrument = InstrumentAny::Equity(equity_aapl());
8761 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
8762 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8763
8764 let spread = Decimal::new(spread_cents, 2);
8765 let mut backward = BarBuilder::new(bar_type, 2, 0);
8766 let mut forward = BarBuilder::new(bar_type, 2, 0);
8767 backward.set_adjustment(spread, ContinuousFutureAdjustmentType::BackwardSpread);
8768 forward.set_adjustment(spread, ContinuousFutureAdjustmentType::ForwardSpread);
8769
8770 for (i, (price_cents, size)) in updates.iter().enumerate() {
8771 let price = Price::new((*price_cents as f64) / 100.0, 2);
8772 let qty = Quantity::new(*size as f64, 0);
8773 let ts = UnixNanos::from((i as u64 + 1) * 1_000);
8774 backward.update(price, qty, ts);
8775 forward.update(price, qty, ts);
8776 }
8777
8778 let bar_backward = backward.build_now();
8779 let bar_forward = forward.build_now();
8780 prop_assert_eq!(bar_backward.open, bar_forward.open);
8781 prop_assert_eq!(bar_backward.high, bar_forward.high);
8782 prop_assert_eq!(bar_backward.low, bar_forward.low);
8783 prop_assert_eq!(bar_backward.close, bar_forward.close);
8784 }
8785
8786 #[rstest]
8787 fn prop_value_bar_aggregator_ohlc_invariants(
8788 ticks in prop::collection::vec((50i64..=500i64, 1u64..=20u64), 2..=30),
8789 step in 100u64..=2_000u64,
8790 ) {
8791 let instrument = InstrumentAny::Equity(equity_aapl());
8792 let bar_spec = BarSpecification::new(step as usize, BarAggregation::Value, PriceType::Last);
8793 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8794 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
8795 let handler_clone = Arc::clone(&handler);
8796
8797 let mut aggregator = ValueBarAggregator::new(
8798 bar_type,
8799 instrument.price_precision(),
8800 instrument.size_precision(),
8801 move |bar: Bar| {
8802 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
8803 },
8804 );
8805
8806 for (i, (price_cents, size)) in ticks.iter().enumerate() {
8807 aggregator.update(
8808 Price::new((*price_cents as f64) / 100.0, 2),
8809 Quantity::new(*size as f64, 0),
8810 UnixNanos::from((i as u64 + 1) * 1_000),
8811 );
8812 }
8813
8814 let bars = handler.lock().expect(MUTEX_POISONED);
8815 for bar in bars.iter() {
8816 prop_assert!(bar.low <= bar.open);
8817 prop_assert!(bar.low <= bar.close);
8818 prop_assert!(bar.high >= bar.open);
8819 prop_assert!(bar.high >= bar.close);
8820 prop_assert!(bar.volume.as_f64() > 0.0);
8821 }
8822 }
8823
8824 #[rstest]
8825 fn prop_renko_brick_chain(
8826 moves in prop::collection::vec(-500i64..=500i64, 1..=60),
8827 step in 1usize..=10,
8828 ) {
8829 let instrument = InstrumentAny::Equity(equity_aapl());
8830 let bar_spec = BarSpecification::new(step, BarAggregation::Renko, PriceType::Last);
8831 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8832 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
8833 let handler_clone = Arc::clone(&handler);
8834
8835 let price_increment = Price::from("0.01");
8836 let mut aggregator = RenkoBarAggregator::new(
8837 bar_type,
8838 2,
8839 0,
8840 price_increment,
8841 move |bar: Bar| {
8842 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
8843 },
8844 );
8845 let brick_size = aggregator.brick_size;
8846
8847 let base_raw = Price::from("1000.00").raw;
8848 let mut cum_increments: i64 = 0;
8849 let mut first_price: Option<Price> = None;
8850
8851 for (i, delta) in moves.iter().enumerate() {
8852 cum_increments += delta;
8853 let price = Price::from_raw(
8854 base_raw + PriceRaw::from(cum_increments) * price_increment.raw,
8855 2,
8856 );
8857
8858 if first_price.is_none() {
8859 first_price = Some(price);
8860 }
8861
8862 aggregator.update(price, Quantity::from(1), UnixNanos::from((i as u64 + 1) * 1_000));
8863 }
8864
8865 let bars = handler.lock().expect(MUTEX_POISONED);
8866 let mut expected_open = first_price.unwrap();
8867
8868 for bar in bars.iter() {
8869 prop_assert_eq!(bar.open, expected_open);
8871 prop_assert_eq!((bar.close.raw - bar.open.raw).abs(), brick_size);
8873 prop_assert_eq!(bar.high, bar.open.max(bar.close));
8875 prop_assert_eq!(bar.low, bar.open.min(bar.close));
8876 expected_open = bar.close;
8877 }
8878 }
8879
8880 #[rstest]
8881 fn prop_volume_imbalance_one_sided_conservation(
8882 sizes in prop::collection::vec(1u64..=50u64, 1..=40),
8883 step in 2u64..=10u64,
8884 buyer in any::<bool>(),
8885 ) {
8886 let instrument = InstrumentAny::Equity(equity_aapl());
8887 let bar_spec = BarSpecification::new(
8888 step as usize,
8889 BarAggregation::VolumeImbalance,
8890 PriceType::Last,
8891 );
8892 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8893 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
8894 let handler_clone = Arc::clone(&handler);
8895
8896 let mut aggregator = VolumeImbalanceBarAggregator::new(
8897 bar_type,
8898 instrument.price_precision(),
8899 instrument.size_precision(),
8900 move |bar: Bar| {
8901 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
8902 },
8903 );
8904
8905 let side = if buyer { AggressorSide::Buyer } else { AggressorSide::Seller };
8906 let mut total_input: u64 = 0;
8907
8908 for (i, size) in sizes.iter().enumerate() {
8909 let trade = TradeTick {
8910 instrument_id: instrument.id(),
8911 price: Price::from("100.00"),
8912 size: Quantity::from(*size),
8913 aggressor_side: side,
8914 ts_event: UnixNanos::from((i as u64 + 1) * 1_000),
8915 ts_init: UnixNanos::from((i as u64 + 1) * 1_000),
8916 ..TradeTick::default()
8917 };
8918 aggregator.handle_trade(trade);
8919 total_input += *size;
8920 }
8921
8922 let bars = handler.lock().expect(MUTEX_POISONED);
8923
8924 for bar in bars.iter() {
8926 prop_assert_eq!(bar.volume, Quantity::from(step));
8927 }
8928
8929 let emitted: u64 = bars.len() as u64 * step;
8931 let pending = aggregator.core.builder.volume.as_f64();
8932 prop_assert!((emitted as f64 + pending - total_input as f64).abs() < 1e-9);
8933 }
8934
8935 #[rstest]
8936 fn prop_volume_runs_one_sided_conservation(
8937 sizes in prop::collection::vec(1u64..=50u64, 1..=40),
8938 step in 2u64..=10u64,
8939 buyer in any::<bool>(),
8940 ) {
8941 let instrument = InstrumentAny::Equity(equity_aapl());
8942 let bar_spec = BarSpecification::new(
8943 step as usize,
8944 BarAggregation::VolumeRuns,
8945 PriceType::Last,
8946 );
8947 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8948 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
8949 let handler_clone = Arc::clone(&handler);
8950
8951 let mut aggregator = VolumeRunsBarAggregator::new(
8952 bar_type,
8953 instrument.price_precision(),
8954 instrument.size_precision(),
8955 move |bar: Bar| {
8956 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
8957 },
8958 );
8959
8960 let side = if buyer { AggressorSide::Buyer } else { AggressorSide::Seller };
8961 let mut total_input: u64 = 0;
8962
8963 for (i, size) in sizes.iter().enumerate() {
8964 let trade = TradeTick {
8965 instrument_id: instrument.id(),
8966 price: Price::from("100.00"),
8967 size: Quantity::from(*size),
8968 aggressor_side: side,
8969 ts_event: UnixNanos::from((i as u64 + 1) * 1_000),
8970 ts_init: UnixNanos::from((i as u64 + 1) * 1_000),
8971 ..TradeTick::default()
8972 };
8973 aggregator.handle_trade(trade);
8974 total_input += *size;
8975 }
8976
8977 let bars = handler.lock().expect(MUTEX_POISONED);
8978
8979 for bar in bars.iter() {
8981 prop_assert_eq!(bar.volume, Quantity::from(step));
8982 }
8983
8984 let emitted: u64 = bars.len() as u64 * step;
8985 let pending = aggregator.core.builder.volume.as_f64();
8986 prop_assert!((emitted as f64 + pending - total_input as f64).abs() < 1e-9);
8987 }
8988
8989 #[rstest]
8990 fn prop_value_bar_cum_value_stays_below_step(
8991 ticks in prop::collection::vec((50i64..=500i64, 1u64..=20u64), 1..=30),
8992 step in 100u64..=2_000u64,
8993 ) {
8994 let instrument = InstrumentAny::Equity(equity_aapl());
8995 let bar_spec = BarSpecification::new(step as usize, BarAggregation::Value, PriceType::Last);
8996 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8997 let step_decimal = Decimal::from(step);
8998
8999 let mut aggregator = ValueBarAggregator::new(
9000 bar_type,
9001 instrument.price_precision(),
9002 instrument.size_precision(),
9003 |_: Bar| {},
9004 );
9005
9006 for (i, (price_cents, size)) in ticks.iter().enumerate() {
9007 aggregator.update(
9008 Price::new((*price_cents as f64) / 100.0, 2),
9009 Quantity::new(*size as f64, 0),
9010 UnixNanos::from((i as u64 + 1) * 1_000),
9011 );
9012
9013 prop_assert!(aggregator.get_cumulative_value() < step_decimal);
9016 }
9017 }
9018 }
9019}