1use std::{
22 any::Any,
23 cell::RefCell,
24 fmt::Debug,
25 ops::Add,
26 rc::{Rc, Weak},
27};
28
29use ahash::AHashMap;
30use jiff::SignedDuration;
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::Buy => 1,
627 AggressorSide::Sell => -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::Buy => Some(AggressorSide::Buy),
716 AggressorSide::Sell => Some(AggressorSide::Sell),
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::Buy => 1,
940 AggressorSide::Sell => -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::Buy => Some(AggressorSide::Buy),
1045 AggressorSide::Sell => Some(AggressorSide::Sell),
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::Buy => (Decimal::ONE, true),
1328 AggressorSide::Sell => (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::Buy => Some(AggressorSide::Buy),
1496 AggressorSide::Sell => Some(AggressorSide::Sell),
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<SignedDuration>,
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<SignedDuration>,
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(
1854 &mut self,
1855 aggregator_rc: Option<Rc<RefCell<Box<dyn BarAggregator>>>>,
1856 ) {
1857 let aggregator_weak = if let Some(rc) = aggregator_rc {
1859 let weak = Rc::downgrade(&rc);
1861 self.aggregator_weak = Some(weak.clone());
1862 weak
1863 } else {
1864 self.aggregator_weak
1866 .as_ref()
1867 .expect("Aggregator weak reference must be set before calling start_timer()")
1868 .clone()
1869 };
1870
1871 let callback = TimeEventCallback::RustLocal(Rc::new(move |event: TimeEvent| {
1872 if let Some(agg) = aggregator_weak.upgrade() {
1873 agg.borrow_mut().build_bar(&event);
1874 }
1875 }));
1876
1877 let now = self.clock.borrow().utc_now();
1879 let mut start_time =
1880 get_time_bar_start(now, &self.bar_type(), self.time_bars_origin_offset);
1881 start_time += SignedDuration::from_micros(self.bar_build_delay as i64);
1882
1883 let fire_immediately = start_time == now;
1885
1886 let spec = &self.bar_type().spec();
1887 let start_time_ns = UnixNanos::from(start_time);
1888 let step = spec.step.get() as u32;
1889
1890 if spec.aggregation != BarAggregation::Month && spec.aggregation != BarAggregation::Year {
1891 self.clock
1892 .borrow_mut()
1893 .set_timer_ns(
1894 &self.timer_name,
1895 self.interval_ns.as_u64(),
1896 Some(start_time_ns),
1897 None,
1898 Some(callback),
1899 Some(true), Some(fire_immediately),
1901 )
1902 .expect(FAILED);
1903
1904 if fire_immediately {
1905 self.next_close_ns = start_time_ns;
1906 } else {
1907 let interval_duration = SignedDuration::from_nanos(self.interval_ns.as_i64());
1908 self.next_close_ns = UnixNanos::from(start_time + interval_duration);
1909 }
1910
1911 self.stored_open_ns = self.next_close_ns.saturating_sub_ns(self.interval_ns);
1912 } else {
1913 let alert_time = if fire_immediately {
1915 start_time
1916 } else if spec.aggregation == BarAggregation::Month {
1917 add_n_months(start_time, step).expect(FAILED)
1918 } else {
1919 add_n_years(start_time, step).expect(FAILED)
1920 };
1921
1922 self.clock
1923 .borrow_mut()
1924 .set_time_alert_ns(
1925 &self.timer_name,
1926 UnixNanos::from(alert_time),
1927 Some(callback),
1928 Some(true), )
1930 .expect(FAILED);
1931
1932 self.next_close_ns = UnixNanos::from(alert_time);
1933 self.stored_open_ns = if fire_immediately {
1936 if spec.aggregation == BarAggregation::Month {
1937 subtract_n_months_nanos(start_time_ns, step).expect(FAILED)
1938 } else {
1939 subtract_n_years_nanos(start_time_ns, step).expect(FAILED)
1940 }
1941 } else {
1942 start_time_ns
1943 };
1944 }
1945
1946 if self.skip_first_non_full_bar {
1947 self.first_close_ns = self.next_close_ns;
1948 }
1949
1950 log::debug!(
1951 "Started timer {}, start_time={:?}, historical_mode={}, fire_immediately={}, now={:?}, bar_build_delay={}",
1952 self.timer_name,
1953 start_time,
1954 self.historical_mode,
1955 fire_immediately,
1956 now,
1957 self.bar_build_delay
1958 );
1959 }
1960
1961 pub fn stop(&mut self) {
1963 self.clock.borrow_mut().cancel_timer(&self.timer_name);
1964 }
1965
1966 fn build_and_send(&mut self, ts_event: UnixNanos, ts_init: UnixNanos) {
1967 if self.skip_first_non_full_bar && ts_init <= self.first_close_ns {
1968 self.core.builder.reset();
1969 } else {
1970 self.skip_first_non_full_bar = false;
1973 self.core.build_and_send(ts_event, ts_init);
1974 }
1975 }
1976
1977 fn build_bar(&mut self, event: &TimeEvent) {
1978 if !self.core.builder.initialized {
1979 return;
1980 }
1981
1982 if !self.build_with_no_updates && self.core.builder.count == 0 {
1983 return; }
1985
1986 let ts_init = event.ts_event;
1987 let ts_event = if self.is_left_open {
1988 if self.timestamp_on_close {
1989 event.ts_event
1990 } else {
1991 self.stored_open_ns
1992 }
1993 } else {
1994 self.stored_open_ns
1995 };
1996
1997 self.build_and_send(ts_event, ts_init);
1998
1999 self.stored_open_ns = event.ts_event;
2001
2002 if self.bar_type().spec().aggregation == BarAggregation::Month {
2003 let step = self.bar_type().spec().step.get() as u32;
2004 let alert_time_ns = add_n_months_nanos(event.ts_event, step).expect(FAILED);
2005
2006 self.clock
2007 .borrow_mut()
2008 .set_time_alert_ns(&self.timer_name, alert_time_ns, None, None)
2009 .expect(FAILED);
2010
2011 self.next_close_ns = alert_time_ns;
2012 } else if self.bar_type().spec().aggregation == BarAggregation::Year {
2013 let step = self.bar_type().spec().step.get() as u32;
2014 let alert_time_ns = add_n_years_nanos(event.ts_event, step).expect(FAILED);
2015
2016 self.clock
2017 .borrow_mut()
2018 .set_time_alert_ns(&self.timer_name, alert_time_ns, None, None)
2019 .expect(FAILED);
2020
2021 self.next_close_ns = alert_time_ns;
2022 } else {
2023 self.next_close_ns = self
2025 .clock
2026 .borrow()
2027 .next_time_ns(&self.timer_name)
2028 .unwrap_or_default();
2029 }
2030 }
2031
2032 fn preprocess_historical_events(&mut self, ts_init: UnixNanos) {
2033 if self.clock.borrow().timestamp_ns() == UnixNanos::default() {
2034 {
2036 let mut clock_borrow = self.clock.borrow_mut();
2037 let test_clock = clock_borrow
2038 .as_any_mut()
2039 .downcast_mut::<TestClock>()
2040 .expect("Expected TestClock in historical mode");
2041 test_clock.set_time(ts_init);
2042 }
2043 self.start_timer_internal(None);
2045 }
2046
2047 let events = {
2049 let mut clock_borrow = self.clock.borrow_mut();
2050 let test_clock = clock_borrow
2051 .as_any_mut()
2052 .downcast_mut::<TestClock>()
2053 .expect("Expected TestClock in historical mode");
2054 test_clock.advance_time(ts_init, true)
2055 };
2056
2057 for event in events {
2058 if event.ts_event == ts_init {
2059 self.historical_event_at_ts_init = Some(event);
2060 } else {
2061 self.build_bar(&event);
2062 }
2063 }
2064 }
2065
2066 fn postprocess_historical_events(&mut self, _ts_init: UnixNanos) {
2067 if let Some(ref event) = self.historical_event_at_ts_init.take() {
2068 self.build_bar(event);
2069 }
2070 }
2071
2072 pub fn set_historical_events_internal(&mut self, events: Vec<TimeEvent>) {
2074 self.historical_events = events;
2075 }
2076}
2077
2078impl BarAggregator for TimeBarAggregator {
2079 fn bar_type(&self) -> BarType {
2080 self.core.bar_type
2081 }
2082
2083 fn is_running(&self) -> bool {
2084 self.core.is_running
2085 }
2086
2087 fn set_is_running(&mut self, value: bool) {
2088 self.core.set_is_running(value);
2089 }
2090
2091 fn stop(&mut self) {
2093 Self::stop(self);
2094 }
2095
2096 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
2097 if self.historical_mode {
2098 self.preprocess_historical_events(ts_init);
2099 }
2100
2101 self.core.apply_update(price, size, ts_init);
2102
2103 if self.historical_mode {
2104 self.postprocess_historical_events(ts_init);
2105 }
2106 }
2107
2108 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
2109 if self.historical_mode {
2110 self.preprocess_historical_events(ts_init);
2111 }
2112
2113 self.core.builder.update_bar(bar, volume, ts_init);
2114
2115 if self.historical_mode {
2116 self.postprocess_historical_events(ts_init);
2117 }
2118 }
2119
2120 fn set_historical_mode(&mut self, historical_mode: bool, handler: Box<dyn FnMut(Bar)>) {
2121 self.historical_mode = historical_mode;
2122 self.core.handler = handler;
2123 }
2124
2125 fn set_historical_events(&mut self, events: Vec<TimeEvent>) {
2126 self.set_historical_events_internal(events);
2127 }
2128
2129 fn set_clock(&mut self, clock: Rc<RefCell<dyn Clock>>) {
2130 self.set_clock_internal(clock);
2131 }
2132
2133 fn build_bar(&mut self, event: &TimeEvent) {
2134 {
2137 #[expect(clippy::use_self)]
2138 TimeBarAggregator::build_bar(self, event);
2139 }
2140 }
2141
2142 fn set_aggregator_weak(&mut self, weak: Weak<RefCell<Box<dyn BarAggregator>>>) {
2143 self.aggregator_weak = Some(weak);
2144 }
2145
2146 fn start_timer(&mut self, aggregator_rc: Option<Rc<RefCell<Box<dyn BarAggregator>>>>) {
2147 self.start_timer_internal(aggregator_rc);
2148 }
2149
2150 fn set_adjustment(&mut self, adjustment: Decimal, mode: ContinuousFutureAdjustmentType) {
2151 self.core.set_adjustment(adjustment, mode);
2152 }
2153
2154 fn set_build_with_no_updates(&mut self, value: bool) {
2155 self.build_with_no_updates = value;
2156 }
2157
2158 fn is_historical(&self) -> bool {
2159 self.historical_mode
2160 }
2161}
2162
2163fn is_below_min_size_decimal(size: Decimal, precision: u8) -> bool {
2164 quantity_from_decimal(size, precision).raw == 0
2165}
2166
2167fn min_size_decimal(precision: u8) -> Decimal {
2168 Decimal::new(1, u32::from(precision))
2169}
2170
2171fn quantity_from_decimal(size: Decimal, precision: u8) -> Quantity {
2172 Quantity::from_decimal_dp(size, precision).expect(FAILED)
2173}
2174
2175fn step_as_quantity_raw(step: usize) -> QuantityRaw {
2177 (FIXED_SCALAR as QuantityRaw)
2178 .checked_mul(step as QuantityRaw)
2179 .expect("`step` overflows raw quantity units for volume aggregation")
2180}
2181
2182pub trait VegaProvider {
2184 fn vega_for_leg(&self, instrument_id: InstrumentId) -> Option<f64>;
2186}
2187
2188pub trait SpreadPriceRounder {
2190 fn round_prices(&self, raw_bid: f64, raw_ask: f64, precision: u8) -> (Price, Price);
2192}
2193
2194#[derive(Debug, Default)]
2196pub struct MapVegaProvider {
2197 vegas: AHashMap<InstrumentId, f64>,
2198}
2199
2200impl MapVegaProvider {
2201 pub fn new() -> Self {
2202 Self {
2203 vegas: AHashMap::new(),
2204 }
2205 }
2206
2207 pub fn insert(&mut self, instrument_id: InstrumentId, vega: f64) {
2208 self.vegas.insert(instrument_id, vega);
2209 }
2210
2211 pub fn get(&self, instrument_id: &InstrumentId) -> Option<f64> {
2212 self.vegas.get(instrument_id).copied()
2213 }
2214}
2215
2216impl VegaProvider for MapVegaProvider {
2217 fn vega_for_leg(&self, instrument_id: InstrumentId) -> Option<f64> {
2218 self.vegas.get(&instrument_id).copied()
2219 }
2220}
2221
2222#[derive(Debug)]
2224pub struct FixedTickSchemeRounder {
2225 scheme: FixedTickScheme,
2226}
2227
2228impl FixedTickSchemeRounder {
2229 pub fn new(tick: f64) -> anyhow::Result<Self> {
2235 Ok(Self {
2236 scheme: FixedTickScheme::new(tick)?,
2237 })
2238 }
2239
2240 fn round_one(&self, raw: f64, precision: u8, use_bid_rounding: bool) -> Price {
2241 if raw >= 0.0 {
2242 let p = if use_bid_rounding {
2243 self.scheme.next_bid_price(raw, 0, precision)
2244 } else {
2245 self.scheme.next_ask_price(raw, 0, precision)
2246 };
2247 p.unwrap_or_else(|| price_from_f64(raw, precision))
2248 } else {
2249 let p = if use_bid_rounding {
2250 self.scheme.next_ask_price(-raw, 0, precision)
2251 } else {
2252 self.scheme.next_bid_price(-raw, 0, precision)
2253 };
2254 p.map_or_else(
2255 || price_from_f64(raw, precision),
2256 |q| price_from_f64(-q.as_f64(), precision),
2257 )
2258 }
2259 }
2260}
2261
2262impl SpreadPriceRounder for FixedTickSchemeRounder {
2263 fn round_prices(&self, raw_bid: f64, raw_ask: f64, precision: u8) -> (Price, Price) {
2264 let bid = self.round_one(raw_bid, precision, true);
2265 let ask = self.round_one(raw_ask, precision, false);
2266 (bid, ask)
2267 }
2268}
2269
2270pub struct SpreadQuoteAggregator {
2276 spread_instrument_id: InstrumentId,
2277 leg_ids: Vec<InstrumentId>,
2278 ratios: Vec<i64>,
2279 n_legs: usize,
2280 is_futures_spread: bool,
2281 price_precision: u8,
2282 size_precision: u8,
2283 last_quotes: AHashMap<InstrumentId, QuoteTick>,
2284 mid_prices: Vec<f64>,
2285 bid_prices: Vec<f64>,
2286 ask_prices: Vec<f64>,
2287 vegas: Vec<f64>,
2288 bid_ask_spreads: Vec<f64>,
2289 bid_sizes: Vec<f64>,
2290 ask_sizes: Vec<f64>,
2291 handler: Box<dyn FnMut(QuoteTick)>,
2292 clock: Rc<RefCell<dyn Clock>>,
2293 historical_mode: bool,
2294 update_interval_seconds: Option<u64>,
2295 quote_build_delay: u64,
2296 has_update: bool,
2297 timer_name: String,
2298 vega_pricing_timeout_timer_name: String,
2299 historical_event_at_ts_init: Option<TimeEvent>,
2300 vega_provider: Option<Box<dyn VegaProvider>>,
2301 disable_vega_pricing: bool,
2302 vega_pricing_temporarily_disabled: bool,
2303 vega_pricing_timeout_seconds: u64,
2304 price_rounder: Option<Box<dyn SpreadPriceRounder>>,
2305 is_running: bool,
2306 aggregator_weak: Option<Weak<RefCell<Self>>>,
2307}
2308
2309impl Debug for SpreadQuoteAggregator {
2310 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2311 f.debug_struct(stringify!(SpreadQuoteAggregator))
2312 .field("spread_instrument_id", &self.spread_instrument_id)
2313 .field("n_legs", &self.n_legs)
2314 .field("is_futures_spread", &self.is_futures_spread)
2315 .field("update_interval_seconds", &self.update_interval_seconds)
2316 .finish()
2317 }
2318}
2319
2320impl SpreadQuoteAggregator {
2321 #[expect(clippy::too_many_arguments)]
2327 pub fn new(
2328 spread_instrument_id: InstrumentId,
2329 legs: &[(InstrumentId, i64)],
2330 is_futures_spread: bool,
2331 price_precision: u8,
2332 size_precision: u8,
2333 handler: Box<dyn FnMut(QuoteTick)>,
2334 clock: Rc<RefCell<dyn Clock>>,
2335 historical_mode: bool,
2336 update_interval_seconds: Option<u64>,
2337 quote_build_delay: u64,
2338 disable_vega_pricing: bool,
2339 vega_pricing_timeout_seconds: u64,
2340 vega_provider: Option<Box<dyn VegaProvider>>,
2341 price_rounder: Option<Box<dyn SpreadPriceRounder>>,
2342 ) -> Self {
2343 assert!(legs.len() >= 2, "Spread must have more than one leg");
2344 let n_legs = legs.len();
2345 let leg_ids: Vec<InstrumentId> = legs.iter().map(|(id, _)| *id).collect();
2346 let ratios: Vec<i64> = legs.iter().map(|(_, r)| *r).collect();
2347 for &r in &ratios {
2348 assert!(r != 0, "Ratio cannot be zero");
2349 }
2350 let timer_name = format!("SPREAD_QUOTE_{spread_instrument_id}");
2351 let vega_pricing_timeout_timer_name =
2352 format!("VEGA_PRICING_TIMEOUT_{spread_instrument_id}");
2353 Self {
2354 spread_instrument_id,
2355 leg_ids,
2356 ratios,
2357 n_legs,
2358 is_futures_spread,
2359 price_precision,
2360 size_precision,
2361 last_quotes: AHashMap::new(),
2362 mid_prices: vec![0.0; n_legs],
2363 bid_prices: vec![0.0; n_legs],
2364 ask_prices: vec![0.0; n_legs],
2365 vegas: vec![0.0; n_legs],
2366 bid_ask_spreads: vec![0.0; n_legs],
2367 bid_sizes: vec![0.0; n_legs],
2368 ask_sizes: vec![0.0; n_legs],
2369 handler,
2370 clock,
2371 historical_mode,
2372 update_interval_seconds,
2373 quote_build_delay,
2374 has_update: false,
2375 timer_name,
2376 vega_pricing_timeout_timer_name,
2377 historical_event_at_ts_init: None,
2378 vega_provider,
2379 disable_vega_pricing,
2380 vega_pricing_temporarily_disabled: false,
2381 vega_pricing_timeout_seconds,
2382 price_rounder,
2383 is_running: false,
2384 aggregator_weak: None,
2385 }
2386 }
2387
2388 pub fn set_aggregator_weak(&mut self, weak: Weak<RefCell<Self>>) {
2391 self.aggregator_weak = Some(weak);
2392 }
2393
2394 pub fn prepare_for_timer_mode(&mut self, self_rc: &Rc<RefCell<Self>>) {
2399 self.aggregator_weak = Some(Rc::downgrade(self_rc));
2400 }
2401
2402 pub fn set_historical_mode(
2404 &mut self,
2405 historical_mode: bool,
2406 handler: Box<dyn FnMut(QuoteTick)>,
2407 vega_provider: Option<Box<dyn VegaProvider>>,
2408 ) {
2409 self.historical_mode = historical_mode;
2410 self.handler = handler;
2411
2412 if let Some(vp) = vega_provider {
2413 self.vega_provider = Some(vp);
2414 }
2415 }
2416
2417 pub fn set_running(&mut self, is_running: bool) {
2418 self.is_running = is_running;
2419 }
2420
2421 pub fn set_clock(&mut self, clock: Rc<RefCell<dyn Clock>>) {
2422 self.clock = clock;
2423 }
2424
2425 pub fn start_timer(&mut self, aggregator_rc: Option<Rc<RefCell<Self>>>) {
2434 if let Some(rc) = aggregator_rc {
2435 self.aggregator_weak = Some(Rc::downgrade(&rc));
2436 }
2437
2438 let Some(interval_secs) = self.update_interval_seconds else {
2439 return;
2440 };
2441 let aggregator_weak = self.aggregator_weak.clone().expect(
2442 "SpreadQuoteAggregator: timer mode requires prepare_for_timer_mode(rc) to be \
2443 called first with the Rc that wraps this aggregator (before feeding quotes in \
2444 historical mode or before start_timer(None)).",
2445 );
2446
2447 let callback = TimeEventCallback::RustLocal(Rc::new(move |event: TimeEvent| {
2448 if let Some(agg) = aggregator_weak.upgrade() {
2449 agg.borrow_mut().on_timer_fire(event.ts_event);
2450 }
2451 }));
2452
2453 let now_ns = self.clock.borrow().timestamp_ns();
2454 let interval_ns = interval_secs * 1_000_000_000;
2455 let start_ns = (now_ns.as_u64() / interval_ns) * interval_ns;
2456 let start_ns = start_ns + self.quote_build_delay * 1_000; let start_time = UnixNanos::from(start_ns);
2458 let fire_immediately = now_ns == start_time;
2459 self.clock
2460 .borrow_mut()
2461 .set_timer_ns(
2462 &self.timer_name,
2463 interval_ns,
2464 Some(start_time),
2465 None,
2466 Some(callback),
2467 Some(true),
2468 Some(fire_immediately),
2469 )
2470 .expect("Failed to set spread quote timer");
2471 }
2472
2473 pub fn on_timer_fire(&mut self, ts_event: UnixNanos) {
2475 if self.last_quotes.len() == self.n_legs {
2476 self.build_and_send_quote(ts_event);
2477 }
2478 }
2479
2480 pub fn stop_timer(&mut self) {
2482 if self.update_interval_seconds.is_some()
2483 && self
2484 .clock
2485 .borrow()
2486 .timer_names()
2487 .contains(&self.timer_name.as_str())
2488 {
2489 self.clock.borrow_mut().cancel_timer(&self.timer_name);
2490 }
2491
2492 if self
2493 .clock
2494 .borrow()
2495 .timer_names()
2496 .contains(&self.vega_pricing_timeout_timer_name.as_str())
2497 {
2498 self.clock
2499 .borrow_mut()
2500 .cancel_timer(&self.vega_pricing_timeout_timer_name);
2501 }
2502 }
2503
2504 pub fn handle_quote_tick(&mut self, tick: QuoteTick) {
2506 let ts_init = tick.ts_init;
2507
2508 if self.update_interval_seconds.is_some() && self.historical_mode {
2509 self.process_historical_events(ts_init);
2510 }
2511 self.last_quotes.insert(tick.instrument_id, tick);
2512 self.has_update = true;
2513
2514 if self.update_interval_seconds.is_none() && self.last_quotes.len() == self.n_legs {
2515 self.build_and_send_quote(ts_init);
2516 }
2517 }
2518
2519 pub fn flush_pending_historical_quote(&mut self) {
2525 if self.update_interval_seconds.is_none() || !self.historical_mode {
2526 return;
2527 }
2528
2529 let Some(event) = self.historical_event_at_ts_init.take() else {
2530 return;
2531 };
2532
2533 if self.last_quotes.len() == self.n_legs {
2534 self.build_and_send_quote(event.ts_event);
2535 }
2536 }
2537
2538 fn process_historical_events(&mut self, ts_init: UnixNanos) {
2544 if self.clock.borrow().timestamp_ns() == UnixNanos::default() {
2545 let mut clock_borrow = self.clock.borrow_mut();
2546 let test_clock = clock_borrow
2547 .as_any_mut()
2548 .downcast_mut::<TestClock>()
2549 .expect("Expected TestClock in historical mode");
2550 test_clock.set_time(ts_init);
2551 drop(clock_borrow);
2552 self.start_timer(None);
2553 }
2554
2555 if self.last_quotes.len() == self.n_legs
2556 && let Some(ref event) = self.historical_event_at_ts_init
2557 && event.ts_event < ts_init
2558 {
2559 let event = self.historical_event_at_ts_init.take().unwrap();
2561 self.build_and_send_quote(event.ts_event);
2562 }
2563
2564 let events = {
2565 let mut clock_borrow = self.clock.borrow_mut();
2566 let test_clock = clock_borrow
2567 .as_any_mut()
2568 .downcast_mut::<TestClock>()
2569 .expect("Expected TestClock in historical mode");
2570 test_clock.advance_time(ts_init, true)
2571 };
2572
2573 for event in events {
2574 if event.ts_event == ts_init {
2575 self.historical_event_at_ts_init = Some(event);
2576 } else if self.last_quotes.len() == self.n_legs {
2577 self.build_and_send_quote(event.ts_event);
2578 }
2579 }
2580 }
2581
2582 fn build_and_send_quote(&mut self, ts_event: UnixNanos) {
2584 if !self.has_update {
2585 return;
2586 }
2587
2588 let use_vega_pricing =
2589 !(self.disable_vega_pricing || self.vega_pricing_temporarily_disabled);
2590
2591 for (idx, &leg_id) in self.leg_ids.iter().enumerate() {
2592 let Some(tick) = self.last_quotes.get(&leg_id) else {
2593 log::error!(
2594 "SpreadQuoteAggregator[{}]: Missing quote for leg {}",
2595 self.spread_instrument_id,
2596 leg_id
2597 );
2598 return;
2599 };
2600 let ask_price = tick.ask_price.as_f64();
2601 let bid_price = tick.bid_price.as_f64();
2602 self.bid_prices[idx] = bid_price;
2603 self.ask_prices[idx] = ask_price;
2604 self.bid_sizes[idx] = tick.bid_size.as_f64();
2605 self.ask_sizes[idx] = tick.ask_size.as_f64();
2606
2607 if !self.is_futures_spread {
2608 self.mid_prices[idx] = f64::midpoint(ask_price, bid_price);
2609 self.bid_ask_spreads[idx] = ask_price - bid_price;
2610
2611 if use_vega_pricing
2612 && let Some(ref vp) = self.vega_provider
2613 && let Some(vega) = vp.vega_for_leg(leg_id)
2614 {
2615 self.vegas[idx] = vega;
2616 }
2617 }
2618 }
2619 let (raw_bid, raw_ask) = if self.is_futures_spread {
2620 self.create_futures_spread_prices()
2621 } else {
2622 self.create_option_spread_prices()
2623 };
2624 let spread_quote = self.create_quote_tick_from_raw_prices(raw_bid, raw_ask, ts_event);
2625 self.has_update = false;
2626 (self.handler)(spread_quote);
2627 }
2628
2629 fn create_option_spread_prices(&mut self) -> (f64, f64) {
2630 if self.disable_vega_pricing || self.vega_pricing_temporarily_disabled {
2631 return self.create_futures_spread_prices();
2632 }
2633
2634 let vega_multipliers: Vec<f64> = (0..self.n_legs)
2635 .map(|i| {
2636 if self.vegas[i] == 0.0 {
2637 0.0
2638 } else {
2639 self.bid_ask_spreads[i] / self.vegas[i]
2640 }
2641 })
2642 .collect();
2643 let non_zero: Vec<f64> = vega_multipliers
2644 .iter()
2645 .copied()
2646 .filter(|&x| x != 0.0)
2647 .collect();
2648
2649 if non_zero.is_empty() {
2650 log::warn!(
2651 "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",
2652 self.spread_instrument_id,
2653 self.vega_pricing_timeout_seconds
2654 );
2655 self.start_vega_pricing_timeout();
2656 return self.create_futures_spread_prices();
2657 }
2658 let vega_multiplier = non_zero.iter().map(|x| x.abs()).sum::<f64>() / non_zero.len() as f64;
2659 let spread_vega = self
2660 .vegas
2661 .iter()
2662 .zip(self.ratios.iter())
2663 .map(|(v, r)| v * (*r as f64))
2664 .sum::<f64>()
2665 .abs();
2666 let bid_ask_spread = spread_vega * vega_multiplier;
2667 let spread_mid_price: f64 = self
2668 .mid_prices
2669 .iter()
2670 .zip(self.ratios.iter())
2671 .map(|(m, r)| m * (*r as f64))
2672 .sum();
2673 let raw_bid = spread_mid_price - bid_ask_spread * 0.5;
2674 let raw_ask = spread_mid_price + bid_ask_spread * 0.5;
2675 (raw_bid, raw_ask)
2676 }
2677
2678 fn clear_vega_pricing_timeout(&mut self) {
2679 self.vega_pricing_temporarily_disabled = false;
2680 }
2681
2682 fn start_vega_pricing_timeout(&mut self) {
2683 self.vega_pricing_temporarily_disabled = true;
2684
2685 if self
2686 .clock
2687 .borrow()
2688 .timer_names()
2689 .contains(&self.vega_pricing_timeout_timer_name.as_str())
2690 {
2691 return;
2692 }
2693
2694 let Some(aggregator_weak) = self.aggregator_weak.clone() else {
2695 return;
2696 };
2697 let callback = TimeEventCallback::RustLocal(Rc::new(move |_event: TimeEvent| {
2698 if let Some(agg) = aggregator_weak.upgrade() {
2699 agg.borrow_mut().clear_vega_pricing_timeout();
2700 }
2701 }));
2702 let alert_time =
2703 self.clock.borrow().timestamp_ns() + self.vega_pricing_timeout_seconds * 1_000_000_000;
2704
2705 self.clock
2706 .borrow_mut()
2707 .set_time_alert_ns(
2708 &self.vega_pricing_timeout_timer_name,
2709 alert_time,
2710 Some(callback),
2711 Some(true),
2712 )
2713 .expect("Failed to set spread quote vega pricing timeout");
2714 }
2715
2716 fn create_futures_spread_prices(&self) -> (f64, f64) {
2717 let mut raw_ask = 0.0_f64;
2718 let mut raw_bid = 0.0_f64;
2719
2720 for i in 0..self.n_legs {
2721 let r = self.ratios[i] as f64;
2722 if self.ratios[i] >= 0 {
2723 raw_ask += r * self.ask_prices[i];
2724 raw_bid += r * self.bid_prices[i];
2725 } else {
2726 raw_ask += r * self.bid_prices[i];
2727 raw_bid += r * self.ask_prices[i];
2728 }
2729 }
2730 (raw_bid, raw_ask)
2731 }
2732
2733 fn create_quote_tick_from_raw_prices(
2734 &self,
2735 raw_bid_price: f64,
2736 raw_ask_price: f64,
2737 ts_event: UnixNanos,
2738 ) -> QuoteTick {
2739 let (bid_price, ask_price) = if let Some(ref rounder) = self.price_rounder {
2740 rounder.round_prices(raw_bid_price, raw_ask_price, self.price_precision)
2741 } else {
2742 let bid = price_from_f64(raw_bid_price, self.price_precision);
2743 let ask = price_from_f64(raw_ask_price, self.price_precision);
2744 (bid, ask)
2745 };
2746 let mut min_bid_size = f64::INFINITY;
2747 let mut min_ask_size = f64::INFINITY;
2748 for i in 0..self.n_legs {
2749 let abs_ratio = self.ratios[i].unsigned_abs() as f64;
2750 if self.ratios[i] >= 0 {
2751 let b = self.bid_sizes[i] / abs_ratio;
2752 if b < min_bid_size {
2753 min_bid_size = b;
2754 }
2755 let a = self.ask_sizes[i] / abs_ratio;
2756 if a < min_ask_size {
2757 min_ask_size = a;
2758 }
2759 } else {
2760 let b = self.ask_sizes[i] / abs_ratio;
2761 if b < min_bid_size {
2762 min_bid_size = b;
2763 }
2764 let a = self.bid_sizes[i] / abs_ratio;
2765 if a < min_ask_size {
2766 min_ask_size = a;
2767 }
2768 }
2769 }
2770 let bid_size = Quantity::new(min_bid_size, self.size_precision);
2771 let ask_size = Quantity::new(min_ask_size, self.size_precision);
2772 QuoteTick::new(
2773 self.spread_instrument_id,
2774 bid_price,
2775 ask_price,
2776 bid_size,
2777 ask_size,
2778 ts_event,
2779 ts_event,
2780 )
2781 }
2782}
2783
2784fn price_from_f64(v: f64, precision: u8) -> Price {
2785 Price::new(v, precision)
2786}
2787
2788#[cfg(test)]
2789mod tests {
2790 use std::sync::Arc;
2791
2792 use nautilus_common::{clock::TestClock, timer::TimeEvent};
2793 use nautilus_core::{UUID4, UnixNanos};
2794 use nautilus_model::{
2795 data::{BarSpecification, BarType, QuoteTick},
2796 enums::{AggregationSource, AggressorSide, BarAggregation, PriceType},
2797 identifiers::InstrumentId,
2798 instruments::{CurrencyPair, Equity, Instrument, InstrumentAny, stubs::*},
2799 types::{Price, Quantity},
2800 };
2801 use parking_lot::Mutex;
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);
3386 let bar_type = BarType::new(
3387 instrument.id(),
3388 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3389 AggregationSource::Internal,
3390 );
3391 let mut builder = BarBuilder::new(bar_type, 2, 0);
3392
3393 builder.set_adjustment(
3394 Decimal::new(-15000, 2), ContinuousFutureAdjustmentType::BackwardSpread,
3396 );
3397
3398 builder.update(Price::from("100.00"), Quantity::from(1), 1_000.into());
3399 let bar = builder.build_now();
3400 assert_eq!(bar.close, Price::from("-50.00"));
3401 assert!(bar.close.raw < 0);
3402 assert_eq!(bar.close.precision, 2);
3403 }
3404
3405 #[rstest]
3406 fn test_bar_builder_build_promotes_close_above_high_from_previous_close(equity_aapl: Equity) {
3407 let instrument = InstrumentAny::Equity(equity_aapl);
3408 let bar_type = BarType::new(
3409 instrument.id(),
3410 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3411 AggregationSource::Internal,
3412 );
3413 let mut builder = BarBuilder::new(bar_type, 2, 0);
3414
3415 builder.update(
3416 Price::from("110.00"),
3417 Quantity::from(1),
3418 UnixNanos::from(100),
3419 );
3420 builder.build_now();
3421
3422 builder.update(
3423 Price::from("100.00"),
3424 Quantity::from(1),
3425 UnixNanos::from(200),
3426 );
3427 builder.update(
3428 Price::from("101.00"),
3429 Quantity::from(1),
3430 UnixNanos::from(300),
3431 );
3432 builder.update(
3433 Price::from("200.00"),
3434 Quantity::from(1),
3435 UnixNanos::from(400),
3436 );
3437
3438 let bar = builder.build_now();
3439 assert_eq!(bar.open, Price::from("100.00"));
3440 assert_eq!(bar.high, Price::from("200.00"));
3441 assert_eq!(bar.low, Price::from("100.00"));
3442 assert_eq!(bar.close, Price::from("200.00"));
3443 }
3444
3445 #[rstest]
3446 fn test_bar_builder_build_clamps_low_to_close(equity_aapl: Equity) {
3447 let instrument = InstrumentAny::Equity(equity_aapl);
3451 let bar_type = BarType::new(
3452 instrument.id(),
3453 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3454 AggregationSource::Internal,
3455 );
3456 let mut builder = BarBuilder::new(bar_type, 2, 0);
3457
3458 builder.update(
3459 Price::from("100.00"),
3460 Quantity::from(1),
3461 UnixNanos::from(100),
3462 );
3463 builder.close = Some(Price::from("50.00"));
3464
3465 let bar = builder.build_now();
3466 assert_eq!(bar.low, Price::from("50.00"));
3467 assert_eq!(bar.close, Price::from("50.00"));
3468 assert!(bar.low <= bar.open);
3469 }
3470
3471 #[rstest]
3472 fn test_tick_bar_aggregator_handle_trade_when_step_count_below_threshold(equity_aapl: Equity) {
3473 let instrument = InstrumentAny::Equity(equity_aapl);
3474 let bar_spec = BarSpecification::new(3, BarAggregation::Tick, PriceType::Last);
3475 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3476 let handler = Arc::new(Mutex::new(Vec::new()));
3477 let handler_clone = Arc::clone(&handler);
3478
3479 let mut aggregator = TickBarAggregator::new(
3480 bar_type,
3481 instrument.price_precision(),
3482 instrument.size_precision(),
3483 move |bar: Bar| {
3484 let mut handler_guard = handler_clone.lock();
3485 handler_guard.push(bar);
3486 },
3487 );
3488
3489 let trade = TradeTick::default();
3490 aggregator.handle_trade(trade);
3491
3492 let handler_guard = handler.lock();
3493 assert_eq!(handler_guard.len(), 0);
3494 }
3495
3496 #[rstest]
3497 fn test_tick_bar_aggregator_handle_trade_when_step_count_reached(equity_aapl: Equity) {
3498 let instrument = InstrumentAny::Equity(equity_aapl);
3499 let bar_spec = BarSpecification::new(3, BarAggregation::Tick, PriceType::Last);
3500 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3501 let handler = Arc::new(Mutex::new(Vec::new()));
3502 let handler_clone = Arc::clone(&handler);
3503
3504 let mut aggregator = TickBarAggregator::new(
3505 bar_type,
3506 instrument.price_precision(),
3507 instrument.size_precision(),
3508 move |bar: Bar| {
3509 let mut handler_guard = handler_clone.lock();
3510 handler_guard.push(bar);
3511 },
3512 );
3513
3514 let trade = TradeTick::default();
3515 aggregator.handle_trade(trade);
3516 aggregator.handle_trade(trade);
3517 aggregator.handle_trade(trade);
3518
3519 let handler_guard = handler.lock();
3520 let bar = handler_guard.first().unwrap();
3521 assert_eq!(handler_guard.len(), 1);
3522 assert_eq!(bar.open, trade.price);
3523 assert_eq!(bar.high, trade.price);
3524 assert_eq!(bar.low, trade.price);
3525 assert_eq!(bar.close, trade.price);
3526 assert_eq!(bar.volume, Quantity::from(300000));
3527 assert_eq!(bar.ts_event, trade.ts_event);
3528 assert_eq!(bar.ts_init, trade.ts_init);
3529 }
3530
3531 #[rstest]
3532 fn test_tick_bar_aggregator_aggregates_to_step_size(equity_aapl: Equity) {
3533 let instrument = InstrumentAny::Equity(equity_aapl);
3534 let bar_spec = BarSpecification::new(3, BarAggregation::Tick, PriceType::Last);
3535 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3536 let handler = Arc::new(Mutex::new(Vec::new()));
3537 let handler_clone = Arc::clone(&handler);
3538
3539 let mut aggregator = TickBarAggregator::new(
3540 bar_type,
3541 instrument.price_precision(),
3542 instrument.size_precision(),
3543 move |bar: Bar| {
3544 let mut handler_guard = handler_clone.lock();
3545 handler_guard.push(bar);
3546 },
3547 );
3548
3549 aggregator.update(
3550 Price::from("1.00001"),
3551 Quantity::from(1),
3552 UnixNanos::default(),
3553 );
3554 aggregator.update(
3555 Price::from("1.00002"),
3556 Quantity::from(1),
3557 UnixNanos::from(1000),
3558 );
3559 aggregator.update(
3560 Price::from("1.00003"),
3561 Quantity::from(1),
3562 UnixNanos::from(2000),
3563 );
3564
3565 let handler_guard = handler.lock();
3566 assert_eq!(handler_guard.len(), 1);
3567
3568 let bar = handler_guard.first().unwrap();
3569 assert_eq!(bar.open, Price::from("1.00001"));
3570 assert_eq!(bar.high, Price::from("1.00003"));
3571 assert_eq!(bar.low, Price::from("1.00001"));
3572 assert_eq!(bar.close, Price::from("1.00003"));
3573 assert_eq!(bar.volume, Quantity::from(3));
3574 }
3575
3576 #[rstest]
3577 fn test_tick_bar_aggregator_resets_after_bar_created(equity_aapl: Equity) {
3578 let instrument = InstrumentAny::Equity(equity_aapl);
3579 let bar_spec = BarSpecification::new(2, BarAggregation::Tick, PriceType::Last);
3580 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3581 let handler = Arc::new(Mutex::new(Vec::new()));
3582 let handler_clone = Arc::clone(&handler);
3583
3584 let mut aggregator = TickBarAggregator::new(
3585 bar_type,
3586 instrument.price_precision(),
3587 instrument.size_precision(),
3588 move |bar: Bar| {
3589 let mut handler_guard = handler_clone.lock();
3590 handler_guard.push(bar);
3591 },
3592 );
3593
3594 aggregator.update(
3595 Price::from("1.00001"),
3596 Quantity::from(1),
3597 UnixNanos::default(),
3598 );
3599 aggregator.update(
3600 Price::from("1.00002"),
3601 Quantity::from(1),
3602 UnixNanos::from(1000),
3603 );
3604 aggregator.update(
3605 Price::from("1.00003"),
3606 Quantity::from(1),
3607 UnixNanos::from(2000),
3608 );
3609 aggregator.update(
3610 Price::from("1.00004"),
3611 Quantity::from(1),
3612 UnixNanos::from(3000),
3613 );
3614
3615 let handler_guard = handler.lock();
3616 assert_eq!(handler_guard.len(), 2);
3617
3618 let bar1 = &handler_guard[0];
3619 assert_eq!(bar1.open, Price::from("1.00001"));
3620 assert_eq!(bar1.close, Price::from("1.00002"));
3621 assert_eq!(bar1.volume, Quantity::from(2));
3622
3623 let bar2 = &handler_guard[1];
3624 assert_eq!(bar2.open, Price::from("1.00003"));
3625 assert_eq!(bar2.close, Price::from("1.00004"));
3626 assert_eq!(bar2.volume, Quantity::from(2));
3627 }
3628
3629 #[rstest]
3630 fn test_non_time_bar_aggregators_use_historical_handler(
3631 equity_aapl: Equity,
3632 audusd_sim: CurrencyPair,
3633 ) {
3634 let instrument = InstrumentAny::Equity(equity_aapl);
3635 let instrument_id = instrument.id();
3636 let price_precision = instrument.price_precision();
3637 let size_precision = instrument.size_precision();
3638 let make_sink = |bars: Arc<Mutex<Vec<Bar>>>| {
3639 move |bar: Bar| {
3640 bars.lock().push(bar);
3641 }
3642 };
3643 let make_trade = |price: &str, size: i64, ts: u64| TradeTick {
3644 instrument_id,
3645 price: Price::from(price),
3646 size: Quantity::from(size),
3647 aggressor_side: AggressorSide::Buy,
3648 ts_event: UnixNanos::from(ts),
3649 ts_init: UnixNanos::from(ts),
3650 ..TradeTick::default()
3651 };
3652
3653 macro_rules! assert_historical_sink_receives {
3654 ($name:expr, $aggregator:expr, $update:expr) => {{
3655 let initial_bars = Arc::new(Mutex::new(Vec::new()));
3656 let historical_bars = Arc::new(Mutex::new(Vec::new()));
3657 let mut aggregator = $aggregator(Arc::clone(&initial_bars));
3658 aggregator
3659 .set_historical_mode(true, Box::new(make_sink(Arc::clone(&historical_bars))));
3660 {
3661 let aggregator: &mut dyn BarAggregator = &mut aggregator;
3662 $update(aggregator);
3663 }
3664
3665 assert_eq!(initial_bars.lock().len(), 0, "{}", $name,);
3666 assert_eq!(historical_bars.lock().len(), 1, "{}", $name,);
3667 }};
3668 }
3669
3670 let tick_type = BarType::new(
3671 instrument_id,
3672 BarSpecification::new(1, BarAggregation::Tick, PriceType::Last),
3673 AggregationSource::Internal,
3674 );
3675 assert_historical_sink_receives!(
3676 "TickBarAggregator",
3677 |bars| TickBarAggregator::new(
3678 tick_type,
3679 price_precision,
3680 size_precision,
3681 make_sink(bars)
3682 ),
3683 |aggregator: &mut dyn BarAggregator| {
3684 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3685 }
3686 );
3687
3688 let tick_imbalance_type = BarType::new(
3689 instrument_id,
3690 BarSpecification::new(1, BarAggregation::TickImbalance, PriceType::Last),
3691 AggregationSource::Internal,
3692 );
3693 assert_historical_sink_receives!(
3694 "TickImbalanceBarAggregator",
3695 |bars| TickImbalanceBarAggregator::new(
3696 tick_imbalance_type,
3697 price_precision,
3698 size_precision,
3699 make_sink(bars),
3700 ),
3701 |aggregator: &mut dyn BarAggregator| {
3702 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3703 }
3704 );
3705
3706 let tick_runs_type = BarType::new(
3707 instrument_id,
3708 BarSpecification::new(1, BarAggregation::TickRuns, PriceType::Last),
3709 AggregationSource::Internal,
3710 );
3711 assert_historical_sink_receives!(
3712 "TickRunsBarAggregator",
3713 |bars| TickRunsBarAggregator::new(
3714 tick_runs_type,
3715 price_precision,
3716 size_precision,
3717 make_sink(bars),
3718 ),
3719 |aggregator: &mut dyn BarAggregator| {
3720 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3721 }
3722 );
3723
3724 let volume_type = BarType::new(
3725 instrument_id,
3726 BarSpecification::new(1, BarAggregation::Volume, PriceType::Last),
3727 AggregationSource::Internal,
3728 );
3729 assert_historical_sink_receives!(
3730 "VolumeBarAggregator",
3731 |bars| VolumeBarAggregator::new(
3732 volume_type,
3733 price_precision,
3734 size_precision,
3735 make_sink(bars),
3736 ),
3737 |aggregator: &mut dyn BarAggregator| {
3738 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3739 }
3740 );
3741
3742 let volume_imbalance_type = BarType::new(
3743 instrument_id,
3744 BarSpecification::new(1, BarAggregation::VolumeImbalance, PriceType::Last),
3745 AggregationSource::Internal,
3746 );
3747 assert_historical_sink_receives!(
3748 "VolumeImbalanceBarAggregator",
3749 |bars| VolumeImbalanceBarAggregator::new(
3750 volume_imbalance_type,
3751 price_precision,
3752 size_precision,
3753 make_sink(bars),
3754 ),
3755 |aggregator: &mut dyn BarAggregator| {
3756 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3757 }
3758 );
3759
3760 let volume_runs_type = BarType::new(
3761 instrument_id,
3762 BarSpecification::new(1, BarAggregation::VolumeRuns, PriceType::Last),
3763 AggregationSource::Internal,
3764 );
3765 assert_historical_sink_receives!(
3766 "VolumeRunsBarAggregator",
3767 |bars| VolumeRunsBarAggregator::new(
3768 volume_runs_type,
3769 price_precision,
3770 size_precision,
3771 make_sink(bars),
3772 ),
3773 |aggregator: &mut dyn BarAggregator| {
3774 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3775 }
3776 );
3777
3778 let value_type = BarType::new(
3779 instrument_id,
3780 BarSpecification::new(100, BarAggregation::Value, PriceType::Last),
3781 AggregationSource::Internal,
3782 );
3783 assert_historical_sink_receives!(
3784 "ValueBarAggregator",
3785 |bars| ValueBarAggregator::new(
3786 value_type,
3787 price_precision,
3788 size_precision,
3789 make_sink(bars)
3790 ),
3791 |aggregator: &mut dyn BarAggregator| {
3792 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3793 }
3794 );
3795
3796 let value_imbalance_type = BarType::new(
3797 instrument_id,
3798 BarSpecification::new(100, BarAggregation::ValueImbalance, PriceType::Last),
3799 AggregationSource::Internal,
3800 );
3801 assert_historical_sink_receives!(
3802 "ValueImbalanceBarAggregator",
3803 |bars| ValueImbalanceBarAggregator::new(
3804 value_imbalance_type,
3805 price_precision,
3806 size_precision,
3807 make_sink(bars),
3808 ),
3809 |aggregator: &mut dyn BarAggregator| {
3810 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3811 }
3812 );
3813
3814 let value_runs_type = BarType::new(
3815 instrument_id,
3816 BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last),
3817 AggregationSource::Internal,
3818 );
3819 assert_historical_sink_receives!(
3820 "ValueRunsBarAggregator",
3821 |bars| ValueRunsBarAggregator::new(
3822 value_runs_type,
3823 price_precision,
3824 size_precision,
3825 make_sink(bars),
3826 ),
3827 |aggregator: &mut dyn BarAggregator| {
3828 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3829 }
3830 );
3831
3832 let fx = InstrumentAny::CurrencyPair(audusd_sim);
3833 let renko_type = BarType::new(
3834 fx.id(),
3835 BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid),
3836 AggregationSource::Internal,
3837 );
3838 let fx_price_precision = fx.price_precision();
3839 let fx_size_precision = fx.size_precision();
3840 let fx_price_increment = fx.price_increment();
3841 assert_historical_sink_receives!(
3842 "RenkoBarAggregator",
3843 |bars| RenkoBarAggregator::new(
3844 renko_type,
3845 fx_price_precision,
3846 fx_size_precision,
3847 fx_price_increment,
3848 make_sink(bars),
3849 ),
3850 |aggregator: &mut dyn BarAggregator| {
3851 aggregator.update(
3852 Price::from("1.00000"),
3853 Quantity::from(1),
3854 UnixNanos::from(1_000),
3855 );
3856 aggregator.update(
3857 Price::from("1.00010"),
3858 Quantity::from(1),
3859 UnixNanos::from(2_000),
3860 );
3861 }
3862 );
3863 }
3864
3865 #[rstest]
3866 fn test_tick_imbalance_bar_aggregator_emits_at_threshold(equity_aapl: Equity) {
3867 let instrument = InstrumentAny::Equity(equity_aapl);
3868 let bar_spec = BarSpecification::new(2, BarAggregation::TickImbalance, PriceType::Last);
3869 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3870 let handler = Arc::new(Mutex::new(Vec::new()));
3871 let handler_clone = Arc::clone(&handler);
3872
3873 let mut aggregator = TickImbalanceBarAggregator::new(
3874 bar_type,
3875 instrument.price_precision(),
3876 instrument.size_precision(),
3877 move |bar: Bar| {
3878 let mut handler_guard = handler_clone.lock();
3879 handler_guard.push(bar);
3880 },
3881 );
3882
3883 let trade = TradeTick::default();
3884 aggregator.handle_trade(trade);
3885 aggregator.handle_trade(trade);
3886
3887 let handler_guard = handler.lock();
3888 assert_eq!(handler_guard.len(), 1);
3889 let bar = handler_guard.first().unwrap();
3890 assert_eq!(bar.volume, Quantity::from(200000));
3891 }
3892
3893 #[rstest]
3894 fn test_tick_imbalance_bar_aggregator_handles_seller_direction(equity_aapl: Equity) {
3895 let instrument = InstrumentAny::Equity(equity_aapl);
3896 let bar_spec = BarSpecification::new(1, BarAggregation::TickImbalance, PriceType::Last);
3897 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3898 let handler = Arc::new(Mutex::new(Vec::new()));
3899 let handler_clone = Arc::clone(&handler);
3900
3901 let mut aggregator = TickImbalanceBarAggregator::new(
3902 bar_type,
3903 instrument.price_precision(),
3904 instrument.size_precision(),
3905 move |bar: Bar| {
3906 let mut handler_guard = handler_clone.lock();
3907 handler_guard.push(bar);
3908 },
3909 );
3910
3911 let sell = TradeTick {
3912 aggressor_side: AggressorSide::Sell,
3913 ..TradeTick::default()
3914 };
3915
3916 aggregator.handle_trade(sell);
3917
3918 let handler_guard = handler.lock();
3919 assert_eq!(handler_guard.len(), 1);
3920 }
3921
3922 #[rstest]
3923 fn test_tick_runs_bar_aggregator_resets_on_side_change(equity_aapl: Equity) {
3924 let instrument = InstrumentAny::Equity(equity_aapl);
3925 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
3926 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3927 let handler = Arc::new(Mutex::new(Vec::new()));
3928 let handler_clone = Arc::clone(&handler);
3929
3930 let mut aggregator = TickRunsBarAggregator::new(
3931 bar_type,
3932 instrument.price_precision(),
3933 instrument.size_precision(),
3934 move |bar: Bar| {
3935 let mut handler_guard = handler_clone.lock();
3936 handler_guard.push(bar);
3937 },
3938 );
3939
3940 let buy = TradeTick::default();
3941 let sell = TradeTick {
3942 aggressor_side: AggressorSide::Sell,
3943 ..buy
3944 };
3945
3946 aggregator.handle_trade(buy);
3947 aggregator.handle_trade(buy);
3948 aggregator.handle_trade(sell);
3949 aggregator.handle_trade(sell);
3950
3951 let handler_guard = handler.lock();
3952 assert_eq!(handler_guard.len(), 2);
3953 }
3954
3955 #[rstest]
3956 fn test_tick_runs_bar_aggregator_volume_conservation(equity_aapl: Equity) {
3957 let instrument = InstrumentAny::Equity(equity_aapl);
3958 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
3959 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3960 let handler = Arc::new(Mutex::new(Vec::new()));
3961 let handler_clone = Arc::clone(&handler);
3962
3963 let mut aggregator = TickRunsBarAggregator::new(
3964 bar_type,
3965 instrument.price_precision(),
3966 instrument.size_precision(),
3967 move |bar: Bar| {
3968 let mut handler_guard = handler_clone.lock();
3969 handler_guard.push(bar);
3970 },
3971 );
3972
3973 let buy = TradeTick {
3974 size: Quantity::from(1),
3975 ..TradeTick::default()
3976 };
3977 let sell = TradeTick {
3978 aggressor_side: AggressorSide::Sell,
3979 size: Quantity::from(1),
3980 ..buy
3981 };
3982
3983 aggregator.handle_trade(buy);
3984 aggregator.handle_trade(buy);
3985 aggregator.handle_trade(sell);
3986 aggregator.handle_trade(sell);
3987
3988 let handler_guard = handler.lock();
3989 assert_eq!(handler_guard.len(), 2);
3990 assert_eq!(handler_guard[0].volume, Quantity::from(2));
3991 assert_eq!(handler_guard[1].volume, Quantity::from(2));
3992 }
3993
3994 #[rstest]
3995 fn test_volume_bar_aggregator_builds_multiple_bars_from_large_update(equity_aapl: Equity) {
3996 let instrument = InstrumentAny::Equity(equity_aapl);
3997 let bar_spec = BarSpecification::new(10, BarAggregation::Volume, PriceType::Last);
3998 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3999 let handler = Arc::new(Mutex::new(Vec::new()));
4000 let handler_clone = Arc::clone(&handler);
4001
4002 let mut aggregator = VolumeBarAggregator::new(
4003 bar_type,
4004 instrument.price_precision(),
4005 instrument.size_precision(),
4006 move |bar: Bar| {
4007 let mut handler_guard = handler_clone.lock();
4008 handler_guard.push(bar);
4009 },
4010 );
4011
4012 aggregator.update(
4013 Price::from("1.00001"),
4014 Quantity::from(25),
4015 UnixNanos::default(),
4016 );
4017
4018 let handler_guard = handler.lock();
4019 assert_eq!(handler_guard.len(), 2);
4020 let bar1 = &handler_guard[0];
4021 assert_eq!(bar1.volume, Quantity::from(10));
4022 let bar2 = &handler_guard[1];
4023 assert_eq!(bar2.volume, Quantity::from(10));
4024 }
4025
4026 #[rstest]
4027 fn test_volume_bar_aggregator_zero_size_update_is_noop(equity_aapl: Equity) {
4028 let instrument = InstrumentAny::Equity(equity_aapl);
4029 let bar_spec = BarSpecification::new(10, BarAggregation::Volume, PriceType::Last);
4030 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4031 let handler = Arc::new(Mutex::new(Vec::new()));
4032 let handler_clone = Arc::clone(&handler);
4033
4034 let mut aggregator = VolumeBarAggregator::new(
4035 bar_type,
4036 instrument.price_precision(),
4037 instrument.size_precision(),
4038 move |bar: Bar| {
4039 let mut handler_guard = handler_clone.lock();
4040 handler_guard.push(bar);
4041 },
4042 );
4043
4044 aggregator.update(
4045 Price::from("100.00"),
4046 Quantity::from(0),
4047 UnixNanos::default(),
4048 );
4049
4050 let handler_guard = handler.lock();
4051 assert_eq!(handler_guard.len(), 0);
4052 }
4053
4054 #[rstest]
4055 fn test_volume_bar_aggregator_ignores_out_of_order_update(equity_aapl: Equity) {
4056 let instrument = InstrumentAny::Equity(equity_aapl);
4057 let bar_spec = BarSpecification::new(2, BarAggregation::Volume, PriceType::Last);
4058 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4059 let handler = Arc::new(Mutex::new(Vec::new()));
4060 let handler_clone = Arc::clone(&handler);
4061
4062 let mut aggregator = VolumeBarAggregator::new(
4063 bar_type,
4064 instrument.price_precision(),
4065 instrument.size_precision(),
4066 move |bar: Bar| {
4067 let mut handler_guard = handler_clone.lock();
4068 handler_guard.push(bar);
4069 },
4070 );
4071
4072 aggregator.update(
4073 Price::from("100.00"),
4074 Quantity::from(1),
4075 UnixNanos::from(1_000),
4076 );
4077 aggregator.update(
4078 Price::from("200.00"),
4079 Quantity::from(3),
4080 UnixNanos::from(500),
4081 );
4082
4083 let handler_guard = handler.lock();
4084 assert!(handler_guard.is_empty());
4085 assert_eq!(aggregator.core.builder.count, 1);
4086 assert_eq!(aggregator.core.builder.volume, Quantity::from(1));
4087 assert_eq!(aggregator.core.builder.close, Some(Price::from("100.00")));
4088 assert_eq!(aggregator.core.builder.ts_last, UnixNanos::from(1_000));
4089 }
4090
4091 #[rstest]
4092 fn test_volume_bar_aggregator_ignores_out_of_order_bar(equity_aapl: Equity) {
4093 let instrument = InstrumentAny::Equity(equity_aapl);
4094 let bar_spec = BarSpecification::new(2, BarAggregation::Volume, PriceType::Last);
4095 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4096 let handler = Arc::new(Mutex::new(Vec::new()));
4097 let handler_clone = Arc::clone(&handler);
4098
4099 let mut aggregator = VolumeBarAggregator::new(
4100 bar_type,
4101 instrument.price_precision(),
4102 instrument.size_precision(),
4103 move |bar: Bar| {
4104 let mut handler_guard = handler_clone.lock();
4105 handler_guard.push(bar);
4106 },
4107 );
4108
4109 aggregator.update(
4110 Price::from("100.00"),
4111 Quantity::from(1),
4112 UnixNanos::from(1_000),
4113 );
4114 let stale_bar = Bar::new(
4115 bar_type,
4116 Price::from("200.00"),
4117 Price::from("201.00"),
4118 Price::from("199.00"),
4119 Price::from("200.50"),
4120 Quantity::from(3),
4121 UnixNanos::from(500),
4122 UnixNanos::from(500),
4123 );
4124 aggregator.update_bar(stale_bar, stale_bar.volume, stale_bar.ts_init);
4125
4126 let handler_guard = handler.lock();
4127 assert!(handler_guard.is_empty());
4128 assert_eq!(aggregator.core.builder.count, 1);
4129 assert_eq!(aggregator.core.builder.volume, Quantity::from(1));
4130 assert_eq!(aggregator.core.builder.close, Some(Price::from("100.00")));
4131 assert_eq!(aggregator.core.builder.ts_last, UnixNanos::from(1_000));
4132 }
4133
4134 #[rstest]
4135 fn test_volume_imbalance_bar_aggregator_ignores_out_of_order_trade(equity_aapl: Equity) {
4136 let instrument = InstrumentAny::Equity(equity_aapl);
4137 let bar_spec = BarSpecification::new(2, BarAggregation::VolumeImbalance, PriceType::Last);
4138 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4139 let handler = Arc::new(Mutex::new(Vec::new()));
4140 let handler_clone = Arc::clone(&handler);
4141 let mut aggregator = VolumeImbalanceBarAggregator::new(
4142 bar_type,
4143 instrument.price_precision(),
4144 instrument.size_precision(),
4145 move |bar: Bar| {
4146 handler_clone.lock().push(bar);
4147 },
4148 );
4149 let first = TradeTick {
4150 price: Price::from("100.00"),
4151 size: Quantity::from(1),
4152 aggressor_side: AggressorSide::Buy,
4153 ts_init: UnixNanos::from(1_000),
4154 ..TradeTick::default()
4155 };
4156 let stale = TradeTick {
4157 price: Price::from("200.00"),
4158 size: Quantity::from(2),
4159 aggressor_side: AggressorSide::Buy,
4160 ts_init: UnixNanos::from(500),
4161 ..TradeTick::default()
4162 };
4163
4164 aggregator.handle_trade(first);
4165 aggregator.handle_trade(stale);
4166
4167 assert!(handler.lock().is_empty());
4168 assert_eq!(aggregator.imbalance_raw, Quantity::from(1).raw as i128);
4169 assert_eq!(aggregator.core.builder.volume, Quantity::from(1));
4170 assert_eq!(aggregator.core.builder.ts_last, UnixNanos::from(1_000));
4171 }
4172
4173 #[rstest]
4174 fn test_volume_bar_aggregator_exact_threshold_emits_single_bar(equity_aapl: Equity) {
4175 let instrument = InstrumentAny::Equity(equity_aapl);
4176 let bar_spec = BarSpecification::new(10, BarAggregation::Volume, PriceType::Last);
4177 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4178 let handler = Arc::new(Mutex::new(Vec::new()));
4179 let handler_clone = Arc::clone(&handler);
4180
4181 let mut aggregator = VolumeBarAggregator::new(
4182 bar_type,
4183 instrument.price_precision(),
4184 instrument.size_precision(),
4185 move |bar: Bar| {
4186 let mut handler_guard = handler_clone.lock();
4187 handler_guard.push(bar);
4188 },
4189 );
4190
4191 aggregator.update(
4192 Price::from("100.00"),
4193 Quantity::from(7),
4194 UnixNanos::from(1_000),
4195 );
4196 aggregator.update(
4197 Price::from("101.00"),
4198 Quantity::from(3),
4199 UnixNanos::from(2_000),
4200 );
4201
4202 let handler_guard = handler.lock();
4203 assert_eq!(handler_guard.len(), 1);
4204 assert_eq!(handler_guard[0].volume, Quantity::from(10));
4205 assert_eq!(handler_guard[0].close, Price::from("101.00"));
4206 }
4207
4208 #[rstest]
4209 fn test_volume_bar_aggregator_step_of_one_emits_per_unit(equity_aapl: Equity) {
4210 let instrument = InstrumentAny::Equity(equity_aapl);
4211 let bar_spec = BarSpecification::new(1, BarAggregation::Volume, PriceType::Last);
4212 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4213 let handler = Arc::new(Mutex::new(Vec::new()));
4214 let handler_clone = Arc::clone(&handler);
4215
4216 let mut aggregator = VolumeBarAggregator::new(
4217 bar_type,
4218 instrument.price_precision(),
4219 instrument.size_precision(),
4220 move |bar: Bar| {
4221 let mut handler_guard = handler_clone.lock();
4222 handler_guard.push(bar);
4223 },
4224 );
4225
4226 aggregator.update(
4227 Price::from("100.00"),
4228 Quantity::from(1),
4229 UnixNanos::default(),
4230 );
4231
4232 let handler_guard = handler.lock();
4233 assert_eq!(handler_guard.len(), 1);
4234 assert_eq!(handler_guard[0].volume, Quantity::from(1));
4235 }
4236
4237 #[rstest]
4238 fn test_volume_runs_bar_aggregator_side_change_resets(equity_aapl: Equity) {
4239 let instrument = InstrumentAny::Equity(equity_aapl);
4240 let bar_spec = BarSpecification::new(2, BarAggregation::VolumeRuns, PriceType::Last);
4241 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4242 let handler = Arc::new(Mutex::new(Vec::new()));
4243 let handler_clone = Arc::clone(&handler);
4244
4245 let mut aggregator = VolumeRunsBarAggregator::new(
4246 bar_type,
4247 instrument.price_precision(),
4248 instrument.size_precision(),
4249 move |bar: Bar| {
4250 let mut handler_guard = handler_clone.lock();
4251 handler_guard.push(bar);
4252 },
4253 );
4254
4255 let buy = TradeTick {
4256 instrument_id: instrument.id(),
4257 price: Price::from("1.0"),
4258 size: Quantity::from(1),
4259 ..TradeTick::default()
4260 };
4261 let sell = TradeTick {
4262 aggressor_side: AggressorSide::Sell,
4263 ..buy
4264 };
4265
4266 aggregator.handle_trade(buy);
4267 aggregator.handle_trade(buy); aggregator.handle_trade(sell);
4269 aggregator.handle_trade(sell); let handler_guard = handler.lock();
4272 assert!(handler_guard.len() >= 2);
4273 assert!(
4274 (handler_guard[0].volume.as_f64() - handler_guard[1].volume.as_f64()).abs()
4275 < f64::EPSILON
4276 );
4277 }
4278
4279 #[rstest]
4280 fn test_volume_runs_bar_aggregator_handles_large_single_trade(equity_aapl: Equity) {
4281 let instrument = InstrumentAny::Equity(equity_aapl);
4282 let bar_spec = BarSpecification::new(3, BarAggregation::VolumeRuns, PriceType::Last);
4283 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4284 let handler = Arc::new(Mutex::new(Vec::new()));
4285 let handler_clone = Arc::clone(&handler);
4286
4287 let mut aggregator = VolumeRunsBarAggregator::new(
4288 bar_type,
4289 instrument.price_precision(),
4290 instrument.size_precision(),
4291 move |bar: Bar| {
4292 let mut handler_guard = handler_clone.lock();
4293 handler_guard.push(bar);
4294 },
4295 );
4296
4297 let trade = TradeTick {
4298 instrument_id: instrument.id(),
4299 price: Price::from("1.0"),
4300 size: Quantity::from(5),
4301 ..TradeTick::default()
4302 };
4303
4304 aggregator.handle_trade(trade);
4305
4306 let handler_guard = handler.lock();
4307 assert!(!handler_guard.is_empty());
4308 assert!(handler_guard[0].volume.as_f64() > 0.0);
4309 assert!(handler_guard[0].volume.as_f64() < trade.size.as_f64());
4310 }
4311
4312 #[rstest]
4313 fn test_volume_imbalance_bar_aggregator_splits_large_trade(equity_aapl: Equity) {
4314 let instrument = InstrumentAny::Equity(equity_aapl);
4315 let bar_spec = BarSpecification::new(2, BarAggregation::VolumeImbalance, PriceType::Last);
4316 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4317 let handler = Arc::new(Mutex::new(Vec::new()));
4318 let handler_clone = Arc::clone(&handler);
4319
4320 let mut aggregator = VolumeImbalanceBarAggregator::new(
4321 bar_type,
4322 instrument.price_precision(),
4323 instrument.size_precision(),
4324 move |bar: Bar| {
4325 let mut handler_guard = handler_clone.lock();
4326 handler_guard.push(bar);
4327 },
4328 );
4329
4330 let trade_small = TradeTick {
4331 instrument_id: instrument.id(),
4332 price: Price::from("1.0"),
4333 size: Quantity::from(1),
4334 ..TradeTick::default()
4335 };
4336 let trade_large = TradeTick {
4337 size: Quantity::from(3),
4338 ..trade_small
4339 };
4340
4341 aggregator.handle_trade(trade_small);
4342 aggregator.handle_trade(trade_large);
4343
4344 let handler_guard = handler.lock();
4345 assert_eq!(handler_guard.len(), 2);
4346 let total_output = handler_guard
4347 .iter()
4348 .map(|bar| bar.volume.as_f64())
4349 .sum::<f64>();
4350 let total_input = trade_small.size.as_f64() + trade_large.size.as_f64();
4351 assert!((total_output - total_input).abs() < f64::EPSILON);
4352 }
4353
4354 #[rstest]
4355 fn test_value_bar_aggregator_builds_at_value_threshold(equity_aapl: Equity) {
4356 let instrument = InstrumentAny::Equity(equity_aapl);
4357 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4359 let handler = Arc::new(Mutex::new(Vec::new()));
4360 let handler_clone = Arc::clone(&handler);
4361
4362 let mut aggregator = ValueBarAggregator::new(
4363 bar_type,
4364 instrument.price_precision(),
4365 instrument.size_precision(),
4366 move |bar: Bar| {
4367 let mut handler_guard = handler_clone.lock();
4368 handler_guard.push(bar);
4369 },
4370 );
4371
4372 aggregator.update(
4374 Price::from("100.00"),
4375 Quantity::from(5),
4376 UnixNanos::default(),
4377 );
4378 aggregator.update(
4379 Price::from("100.00"),
4380 Quantity::from(5),
4381 UnixNanos::from(1000),
4382 );
4383
4384 let handler_guard = handler.lock();
4385 assert_eq!(handler_guard.len(), 1);
4386 let bar = handler_guard.first().unwrap();
4387 assert_eq!(bar.volume, Quantity::from(10));
4388 }
4389
4390 #[rstest]
4391 fn test_value_bar_aggregator_handles_large_update(equity_aapl: Equity) {
4392 let instrument = InstrumentAny::Equity(equity_aapl);
4393 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last);
4394 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4395 let handler = Arc::new(Mutex::new(Vec::new()));
4396 let handler_clone = Arc::clone(&handler);
4397
4398 let mut aggregator = ValueBarAggregator::new(
4399 bar_type,
4400 instrument.price_precision(),
4401 instrument.size_precision(),
4402 move |bar: Bar| {
4403 let mut handler_guard = handler_clone.lock();
4404 handler_guard.push(bar);
4405 },
4406 );
4407
4408 aggregator.update(
4410 Price::from("100.00"),
4411 Quantity::from(25),
4412 UnixNanos::default(),
4413 );
4414
4415 let handler_guard = handler.lock();
4416 assert_eq!(handler_guard.len(), 2);
4417 let remaining_value = aggregator.get_cumulative_value();
4418 assert!(remaining_value < Decimal::from(1_000)); }
4420
4421 #[rstest]
4422 fn test_value_bar_aggregator_handles_zero_price(equity_aapl: Equity) {
4423 let instrument = InstrumentAny::Equity(equity_aapl);
4424 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last);
4425 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4426 let handler = Arc::new(Mutex::new(Vec::new()));
4427 let handler_clone = Arc::clone(&handler);
4428
4429 let mut aggregator = ValueBarAggregator::new(
4430 bar_type,
4431 instrument.price_precision(),
4432 instrument.size_precision(),
4433 move |bar: Bar| {
4434 let mut handler_guard = handler_clone.lock();
4435 handler_guard.push(bar);
4436 },
4437 );
4438
4439 aggregator.update(
4441 Price::from("0.00"),
4442 Quantity::from(100),
4443 UnixNanos::default(),
4444 );
4445
4446 let handler_guard = handler.lock();
4448 assert_eq!(handler_guard.len(), 0);
4449
4450 assert_eq!(aggregator.get_cumulative_value(), Decimal::ZERO);
4452 }
4453
4454 #[rstest]
4455 fn test_value_bar_aggregator_handles_zero_size(equity_aapl: Equity) {
4456 let instrument = InstrumentAny::Equity(equity_aapl);
4457 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last);
4458 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4459 let handler = Arc::new(Mutex::new(Vec::new()));
4460 let handler_clone = Arc::clone(&handler);
4461
4462 let mut aggregator = ValueBarAggregator::new(
4463 bar_type,
4464 instrument.price_precision(),
4465 instrument.size_precision(),
4466 move |bar: Bar| {
4467 let mut handler_guard = handler_clone.lock();
4468 handler_guard.push(bar);
4469 },
4470 );
4471
4472 aggregator.update(
4474 Price::from("100.00"),
4475 Quantity::from(0),
4476 UnixNanos::default(),
4477 );
4478
4479 let handler_guard = handler.lock();
4481 assert_eq!(handler_guard.len(), 0);
4482
4483 assert_eq!(aggregator.get_cumulative_value(), Decimal::ZERO);
4485 }
4486
4487 #[rstest]
4488 fn test_value_bar_aggregator_conserves_volume_across_rounded_chunks(equity_aapl: Equity) {
4489 let instrument = InstrumentAny::Equity(equity_aapl);
4490 let bar_spec = BarSpecification::new(10, BarAggregation::Value, PriceType::Last);
4491 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4492 let handler = Arc::new(Mutex::new(Vec::new()));
4493 let handler_clone = Arc::clone(&handler);
4494
4495 let mut aggregator = ValueBarAggregator::new(
4496 bar_type,
4497 instrument.price_precision(),
4498 instrument.size_precision(),
4499 move |bar: Bar| {
4500 let mut handler_guard = handler_clone.lock();
4501 handler_guard.push(bar);
4502 },
4503 );
4504
4505 aggregator.update(
4508 Price::from("3.00"),
4509 Quantity::from(10),
4510 UnixNanos::from(1_000),
4511 );
4512
4513 let handler_guard = handler.lock();
4514 assert_eq!(handler_guard.len(), 3);
4515 for bar in handler_guard.iter() {
4516 assert_eq!(bar.volume, Quantity::from(3));
4517 }
4518 assert_eq!(aggregator.core.builder.volume, Quantity::from(1));
4519 }
4520
4521 #[rstest]
4522 fn test_value_bar_aggregator_update_bar_conserves_volume_across_rounded_chunks(
4523 equity_aapl: Equity,
4524 ) {
4525 let instrument = InstrumentAny::Equity(equity_aapl);
4526 let bar_spec = BarSpecification::new(10, BarAggregation::Value, PriceType::Last);
4527 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4528 let handler = Arc::new(Mutex::new(Vec::new()));
4529 let handler_clone = Arc::clone(&handler);
4530
4531 let mut aggregator = ValueBarAggregator::new(
4532 bar_type,
4533 instrument.price_precision(),
4534 instrument.size_precision(),
4535 move |bar: Bar| {
4536 let mut handler_guard = handler_clone.lock();
4537 handler_guard.push(bar);
4538 },
4539 );
4540
4541 let input_bar = Bar::new(
4543 bar_type,
4544 Price::from("3.00"),
4545 Price::from("3.00"),
4546 Price::from("3.00"),
4547 Price::from("3.00"),
4548 Quantity::from(10),
4549 UnixNanos::from(1_000),
4550 UnixNanos::from(1_000),
4551 );
4552 aggregator.handle_bar(input_bar);
4553
4554 let handler_guard = handler.lock();
4555 assert_eq!(handler_guard.len(), 3);
4556 for bar in handler_guard.iter() {
4557 assert_eq!(bar.volume, Quantity::from(3));
4558 }
4559 assert_eq!(aggregator.core.builder.volume, Quantity::from(1));
4560 }
4561
4562 #[rstest]
4563 fn test_value_bar_aggregator_exact_threshold_emits_one_bar(equity_aapl: Equity) {
4564 let instrument = InstrumentAny::Equity(equity_aapl);
4565 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last);
4566 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4567 let handler = Arc::new(Mutex::new(Vec::new()));
4568 let handler_clone = Arc::clone(&handler);
4569
4570 let mut aggregator = ValueBarAggregator::new(
4571 bar_type,
4572 instrument.price_precision(),
4573 instrument.size_precision(),
4574 move |bar: Bar| {
4575 let mut handler_guard = handler_clone.lock();
4576 handler_guard.push(bar);
4577 },
4578 );
4579
4580 aggregator.update(
4581 Price::from("100.00"),
4582 Quantity::from(5),
4583 UnixNanos::from(1_000),
4584 );
4585 aggregator.update(
4586 Price::from("100.00"),
4587 Quantity::from(5),
4588 UnixNanos::from(2_000),
4589 );
4590
4591 let handler_guard = handler.lock();
4592 assert_eq!(handler_guard.len(), 1);
4593 assert_eq!(handler_guard[0].volume, Quantity::from(10));
4594 assert_eq!(aggregator.get_cumulative_value(), Decimal::ZERO);
4595 }
4596
4597 #[rstest]
4598 fn test_value_bar_aggregator_precision_boundary_min_size_clamp(equity_aapl: Equity) {
4599 let instrument = InstrumentAny::Equity(equity_aapl);
4603 let bar_spec = BarSpecification::new(100, BarAggregation::Value, PriceType::Last);
4604 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4605 let handler = Arc::new(Mutex::new(Vec::new()));
4606 let handler_clone = Arc::clone(&handler);
4607
4608 let mut aggregator = ValueBarAggregator::new(
4609 bar_type,
4610 instrument.price_precision(),
4611 instrument.size_precision(),
4612 move |bar: Bar| {
4613 let mut handler_guard = handler_clone.lock();
4614 handler_guard.push(bar);
4615 },
4616 );
4617
4618 aggregator.update(
4620 Price::from("100.00"),
4621 Quantity::from(4),
4622 UnixNanos::default(),
4623 );
4624
4625 let handler_guard = handler.lock();
4626 assert_eq!(handler_guard.len(), 4);
4627 for bar in handler_guard.iter() {
4628 assert_eq!(bar.volume, Quantity::from(1));
4629 }
4630 }
4631
4632 #[rstest]
4633 fn test_value_imbalance_bar_aggregator_emits_on_opposing_overflow(equity_aapl: Equity) {
4634 let instrument = InstrumentAny::Equity(equity_aapl);
4635 let bar_spec = BarSpecification::new(10, BarAggregation::ValueImbalance, PriceType::Last);
4636 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4637 let handler = Arc::new(Mutex::new(Vec::new()));
4638 let handler_clone = Arc::clone(&handler);
4639
4640 let mut aggregator = ValueImbalanceBarAggregator::new(
4641 bar_type,
4642 instrument.price_precision(),
4643 instrument.size_precision(),
4644 move |bar: Bar| {
4645 let mut handler_guard = handler_clone.lock();
4646 handler_guard.push(bar);
4647 },
4648 );
4649
4650 let buy = TradeTick {
4651 price: Price::from("5.0"),
4652 size: Quantity::from(2), instrument_id: instrument.id(),
4654 ..TradeTick::default()
4655 };
4656 let sell = TradeTick {
4657 price: Price::from("5.0"),
4658 size: Quantity::from(2), aggressor_side: AggressorSide::Sell,
4660 instrument_id: instrument.id(),
4661 ..buy
4662 };
4663
4664 aggregator.handle_trade(buy);
4665 aggregator.handle_trade(sell);
4666
4667 let handler_guard = handler.lock();
4668 assert_eq!(handler_guard.len(), 2);
4669 }
4670
4671 #[rstest]
4672 fn test_value_runs_bar_aggregator_emits_on_consecutive_side(equity_aapl: Equity) {
4673 let instrument = InstrumentAny::Equity(equity_aapl);
4674 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
4675 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4676 let handler = Arc::new(Mutex::new(Vec::new()));
4677 let handler_clone = Arc::clone(&handler);
4678
4679 let mut aggregator = ValueRunsBarAggregator::new(
4680 bar_type,
4681 instrument.price_precision(),
4682 instrument.size_precision(),
4683 move |bar: Bar| {
4684 let mut handler_guard = handler_clone.lock();
4685 handler_guard.push(bar);
4686 },
4687 );
4688
4689 let trade = TradeTick {
4690 price: Price::from("10.0"),
4691 size: Quantity::from(5),
4692 instrument_id: instrument.id(),
4693 ..TradeTick::default()
4694 };
4695
4696 aggregator.handle_trade(trade);
4697 aggregator.handle_trade(trade);
4698
4699 let handler_guard = handler.lock();
4700 assert_eq!(handler_guard.len(), 1);
4701 let bar = handler_guard.first().unwrap();
4702 assert_eq!(bar.volume, Quantity::from(10));
4703 }
4704
4705 #[rstest]
4706 fn test_value_runs_bar_aggregator_resets_on_side_change(equity_aapl: Equity) {
4707 let instrument = InstrumentAny::Equity(equity_aapl);
4708 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
4709 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4710 let handler = Arc::new(Mutex::new(Vec::new()));
4711 let handler_clone = Arc::clone(&handler);
4712
4713 let mut aggregator = ValueRunsBarAggregator::new(
4714 bar_type,
4715 instrument.price_precision(),
4716 instrument.size_precision(),
4717 move |bar: Bar| {
4718 let mut handler_guard = handler_clone.lock();
4719 handler_guard.push(bar);
4720 },
4721 );
4722
4723 let buy = TradeTick {
4724 price: Price::from("10.0"),
4725 size: Quantity::from(5),
4726 instrument_id: instrument.id(),
4727 ..TradeTick::default()
4728 }; let sell = TradeTick {
4730 price: Price::from("10.0"),
4731 size: Quantity::from(10),
4732 aggressor_side: AggressorSide::Sell,
4733 ..buy
4734 }; aggregator.handle_trade(buy);
4737 aggregator.handle_trade(sell);
4738
4739 let handler_guard = handler.lock();
4740 assert_eq!(handler_guard.len(), 1);
4741 assert_eq!(handler_guard[0].volume, Quantity::from(10));
4742 }
4743
4744 #[rstest]
4745 fn test_tick_runs_bar_aggregator_continues_run_after_bar_emission(equity_aapl: Equity) {
4746 let instrument = InstrumentAny::Equity(equity_aapl);
4747 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
4748 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4749 let handler = Arc::new(Mutex::new(Vec::new()));
4750 let handler_clone = Arc::clone(&handler);
4751
4752 let mut aggregator = TickRunsBarAggregator::new(
4753 bar_type,
4754 instrument.price_precision(),
4755 instrument.size_precision(),
4756 move |bar: Bar| {
4757 let mut handler_guard = handler_clone.lock();
4758 handler_guard.push(bar);
4759 },
4760 );
4761
4762 let buy = TradeTick::default();
4763
4764 aggregator.handle_trade(buy);
4765 aggregator.handle_trade(buy); aggregator.handle_trade(buy); aggregator.handle_trade(buy); let handler_guard = handler.lock();
4770 assert_eq!(handler_guard.len(), 2);
4771 }
4772
4773 #[rstest]
4774 fn test_tick_runs_bar_aggregator_handles_no_aggressor_trades(equity_aapl: Equity) {
4775 let instrument = InstrumentAny::Equity(equity_aapl);
4776 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
4777 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4778 let handler = Arc::new(Mutex::new(Vec::new()));
4779 let handler_clone = Arc::clone(&handler);
4780
4781 let mut aggregator = TickRunsBarAggregator::new(
4782 bar_type,
4783 instrument.price_precision(),
4784 instrument.size_precision(),
4785 move |bar: Bar| {
4786 let mut handler_guard = handler_clone.lock();
4787 handler_guard.push(bar);
4788 },
4789 );
4790
4791 let buy = TradeTick::default();
4792 let no_aggressor = TradeTick {
4793 aggressor_side: AggressorSide::NoAggressor,
4794 ..buy
4795 };
4796
4797 aggregator.handle_trade(buy);
4798 aggregator.handle_trade(no_aggressor); aggregator.handle_trade(no_aggressor); aggregator.handle_trade(buy); let handler_guard = handler.lock();
4803 assert_eq!(handler_guard.len(), 1);
4804 }
4805
4806 #[rstest]
4807 fn test_volume_runs_bar_aggregator_continues_run_after_bar_emission(equity_aapl: Equity) {
4808 let instrument = InstrumentAny::Equity(equity_aapl);
4809 let bar_spec = BarSpecification::new(2, BarAggregation::VolumeRuns, PriceType::Last);
4810 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4811 let handler = Arc::new(Mutex::new(Vec::new()));
4812 let handler_clone = Arc::clone(&handler);
4813
4814 let mut aggregator = VolumeRunsBarAggregator::new(
4815 bar_type,
4816 instrument.price_precision(),
4817 instrument.size_precision(),
4818 move |bar: Bar| {
4819 let mut handler_guard = handler_clone.lock();
4820 handler_guard.push(bar);
4821 },
4822 );
4823
4824 let buy = TradeTick {
4825 instrument_id: instrument.id(),
4826 price: Price::from("1.0"),
4827 size: Quantity::from(1),
4828 ..TradeTick::default()
4829 };
4830
4831 aggregator.handle_trade(buy);
4832 aggregator.handle_trade(buy); aggregator.handle_trade(buy); aggregator.handle_trade(buy); let handler_guard = handler.lock();
4837 assert_eq!(handler_guard.len(), 2);
4838 assert_eq!(handler_guard[0].volume, Quantity::from(2));
4839 assert_eq!(handler_guard[1].volume, Quantity::from(2));
4840 }
4841
4842 #[rstest]
4843 fn test_value_runs_bar_aggregator_continues_run_after_bar_emission(equity_aapl: Equity) {
4844 let instrument = InstrumentAny::Equity(equity_aapl);
4845 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
4846 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4847 let handler = Arc::new(Mutex::new(Vec::new()));
4848 let handler_clone = Arc::clone(&handler);
4849
4850 let mut aggregator = ValueRunsBarAggregator::new(
4851 bar_type,
4852 instrument.price_precision(),
4853 instrument.size_precision(),
4854 move |bar: Bar| {
4855 let mut handler_guard = handler_clone.lock();
4856 handler_guard.push(bar);
4857 },
4858 );
4859
4860 let buy = TradeTick {
4861 instrument_id: instrument.id(),
4862 price: Price::from("10.0"),
4863 size: Quantity::from(5),
4864 ..TradeTick::default()
4865 }; aggregator.handle_trade(buy);
4868 aggregator.handle_trade(buy); aggregator.handle_trade(buy); aggregator.handle_trade(buy); let handler_guard = handler.lock();
4873 assert_eq!(handler_guard.len(), 2);
4874 assert_eq!(handler_guard[0].volume, Quantity::from(10));
4875 assert_eq!(handler_guard[1].volume, Quantity::from(10));
4876 }
4877
4878 #[rstest]
4879 fn test_time_bar_aggregator_builds_at_interval(equity_aapl: Equity) {
4880 let instrument = InstrumentAny::Equity(equity_aapl);
4881 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
4883 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4884 let handler = Arc::new(Mutex::new(Vec::new()));
4885 let handler_clone = Arc::clone(&handler);
4886 let clock = Rc::new(RefCell::new(TestClock::new()));
4887
4888 let mut aggregator = TimeBarAggregator::new(
4889 bar_type,
4890 instrument.price_precision(),
4891 instrument.size_precision(),
4892 clock.clone(),
4893 move |bar: Bar| {
4894 let mut handler_guard = handler_clone.lock();
4895 handler_guard.push(bar);
4896 },
4897 true, false, BarIntervalType::LeftOpen,
4900 None, 15, false, );
4904
4905 aggregator.update(
4906 Price::from("100.00"),
4907 Quantity::from(1),
4908 UnixNanos::default(),
4909 );
4910
4911 let next_sec = UnixNanos::from(1_000_000_000);
4912 clock.borrow_mut().set_time(next_sec);
4913
4914 let event = TimeEvent::new(
4915 Ustr::from("1-SECOND-LAST"),
4916 UUID4::new(),
4917 next_sec,
4918 next_sec,
4919 );
4920 aggregator.build_bar(&event);
4921
4922 let handler_guard = handler.lock();
4923 assert_eq!(handler_guard.len(), 1);
4924 let bar = handler_guard.first().unwrap();
4925 assert_eq!(bar.ts_event, UnixNanos::default());
4926 assert_eq!(bar.ts_init, next_sec);
4927 }
4928
4929 #[rstest]
4930 fn test_time_bar_aggregator_stop_clears_timer_and_allows_restart(equity_aapl: Equity) {
4931 let instrument = InstrumentAny::Equity(equity_aapl);
4932 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
4933 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4934 let timer_name = format!("TIME_BAR_{bar_type}");
4935 let clock = Rc::new(RefCell::new(TestClock::new()));
4936
4937 let aggregator = TimeBarAggregator::new(
4938 bar_type,
4939 instrument.price_precision(),
4940 instrument.size_precision(),
4941 clock.clone(),
4942 |_bar: Bar| {},
4943 true,
4944 false,
4945 BarIntervalType::LeftOpen,
4946 None,
4947 15,
4948 false,
4949 );
4950
4951 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
4952 let rc = Rc::new(RefCell::new(boxed));
4953
4954 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
4955 assert_eq!(clock.borrow().timer_names(), vec![timer_name.as_str()]);
4956
4957 rc.borrow_mut().stop();
4958 assert!(clock.borrow().timer_names().is_empty());
4959
4960 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
4961 assert_eq!(clock.borrow().timer_names(), vec![timer_name.as_str()]);
4962 }
4963
4964 #[rstest]
4965 fn test_time_bar_aggregator_left_open_interval(equity_aapl: Equity) {
4966 let instrument = InstrumentAny::Equity(equity_aapl);
4967 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
4968 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4969 let handler = Arc::new(Mutex::new(Vec::new()));
4970 let handler_clone = Arc::clone(&handler);
4971 let clock = Rc::new(RefCell::new(TestClock::new()));
4972
4973 let mut aggregator = TimeBarAggregator::new(
4974 bar_type,
4975 instrument.price_precision(),
4976 instrument.size_precision(),
4977 clock.clone(),
4978 move |bar: Bar| {
4979 let mut handler_guard = handler_clone.lock();
4980 handler_guard.push(bar);
4981 },
4982 true, true, BarIntervalType::LeftOpen,
4985 None,
4986 15,
4987 false, );
4989
4990 aggregator.update(
4992 Price::from("100.00"),
4993 Quantity::from(1),
4994 UnixNanos::default(),
4995 );
4996
4997 let ts1 = UnixNanos::from(1_000_000_000);
4999 clock.borrow_mut().set_time(ts1);
5000 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts1, ts1);
5001 aggregator.build_bar(&event);
5002
5003 aggregator.update(Price::from("101.00"), Quantity::from(1), ts1);
5005
5006 let ts2 = UnixNanos::from(2_000_000_000);
5008 clock.borrow_mut().set_time(ts2);
5009 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts2, ts2);
5010 aggregator.build_bar(&event);
5011
5012 let handler_guard = handler.lock();
5013 assert_eq!(handler_guard.len(), 2);
5014
5015 let bar1 = &handler_guard[0];
5016 assert_eq!(bar1.ts_event, ts1); assert_eq!(bar1.ts_init, ts1);
5018 assert_eq!(bar1.close, Price::from("100.00"));
5019 let bar2 = &handler_guard[1];
5020 assert_eq!(bar2.ts_event, ts2);
5021 assert_eq!(bar2.ts_init, ts2);
5022 assert_eq!(bar2.close, Price::from("101.00"));
5023 }
5024
5025 #[rstest]
5026 fn test_time_bar_aggregator_right_open_interval(equity_aapl: Equity) {
5027 let instrument = InstrumentAny::Equity(equity_aapl);
5028 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
5029 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5030 let handler = Arc::new(Mutex::new(Vec::new()));
5031 let handler_clone = Arc::clone(&handler);
5032 let clock = Rc::new(RefCell::new(TestClock::new()));
5033 let mut aggregator = TimeBarAggregator::new(
5034 bar_type,
5035 instrument.price_precision(),
5036 instrument.size_precision(),
5037 clock.clone(),
5038 move |bar: Bar| {
5039 let mut handler_guard = handler_clone.lock();
5040 handler_guard.push(bar);
5041 },
5042 true, true, BarIntervalType::RightOpen,
5045 None,
5046 15,
5047 false, );
5049
5050 aggregator.update(
5052 Price::from("100.00"),
5053 Quantity::from(1),
5054 UnixNanos::default(),
5055 );
5056
5057 let ts1 = UnixNanos::from(1_000_000_000);
5059 clock.borrow_mut().set_time(ts1);
5060 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts1, ts1);
5061 aggregator.build_bar(&event);
5062
5063 aggregator.update(Price::from("101.00"), Quantity::from(1), ts1);
5065
5066 let ts2 = UnixNanos::from(2_000_000_000);
5068 clock.borrow_mut().set_time(ts2);
5069 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts2, ts2);
5070 aggregator.build_bar(&event);
5071
5072 let handler_guard = handler.lock();
5073 assert_eq!(handler_guard.len(), 2);
5074
5075 let bar1 = &handler_guard[0];
5076 assert_eq!(bar1.ts_event, UnixNanos::default()); assert_eq!(bar1.ts_init, ts1);
5078 assert_eq!(bar1.close, Price::from("100.00"));
5079
5080 let bar2 = &handler_guard[1];
5081 assert_eq!(bar2.ts_event, ts1);
5082 assert_eq!(bar2.ts_init, ts2);
5083 assert_eq!(bar2.close, Price::from("101.00"));
5084 }
5085
5086 #[rstest]
5087 fn test_time_bar_aggregator_no_updates_behavior(equity_aapl: Equity) {
5088 let instrument = InstrumentAny::Equity(equity_aapl);
5089 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
5090 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5091 let handler = Arc::new(Mutex::new(Vec::new()));
5092 let handler_clone = Arc::clone(&handler);
5093 let clock = Rc::new(RefCell::new(TestClock::new()));
5094
5095 let mut aggregator = TimeBarAggregator::new(
5097 bar_type,
5098 instrument.price_precision(),
5099 instrument.size_precision(),
5100 clock.clone(),
5101 move |bar: Bar| {
5102 let mut handler_guard = handler_clone.lock();
5103 handler_guard.push(bar);
5104 },
5105 false, true, BarIntervalType::LeftOpen,
5108 None,
5109 15,
5110 false, );
5112
5113 let ts1 = UnixNanos::from(1_000_000_000);
5115 clock.borrow_mut().set_time(ts1);
5116 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts1, ts1);
5117 aggregator.build_bar(&event);
5118
5119 let handler_guard = handler.lock();
5120 assert_eq!(handler_guard.len(), 0); drop(handler_guard);
5122
5123 let handler = Arc::new(Mutex::new(Vec::new()));
5125 let handler_clone = Arc::clone(&handler);
5126 let mut aggregator = TimeBarAggregator::new(
5127 bar_type,
5128 instrument.price_precision(),
5129 instrument.size_precision(),
5130 clock.clone(),
5131 move |bar: Bar| {
5132 let mut handler_guard = handler_clone.lock();
5133 handler_guard.push(bar);
5134 },
5135 true, true, BarIntervalType::LeftOpen,
5138 None,
5139 15,
5140 false, );
5142
5143 aggregator.update(
5144 Price::from("100.00"),
5145 Quantity::from(1),
5146 UnixNanos::default(),
5147 );
5148
5149 let ts1 = UnixNanos::from(1_000_000_000);
5151 clock.borrow_mut().set_time(ts1);
5152 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts1, ts1);
5153 aggregator.build_bar(&event);
5154
5155 let ts2 = UnixNanos::from(2_000_000_000);
5157 clock.borrow_mut().set_time(ts2);
5158 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts2, ts2);
5159 aggregator.build_bar(&event);
5160
5161 let handler_guard = handler.lock();
5162 assert_eq!(handler_guard.len(), 2); let bar1 = &handler_guard[0];
5164 assert_eq!(bar1.close, Price::from("100.00"));
5165 let bar2 = &handler_guard[1];
5166 assert_eq!(bar2.close, Price::from("100.00")); }
5168
5169 #[rstest]
5170 fn test_time_bar_aggregator_respects_timestamp_on_close(equity_aapl: Equity) {
5171 let instrument = InstrumentAny::Equity(equity_aapl);
5172 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
5173 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5174 let clock = Rc::new(RefCell::new(TestClock::new()));
5175 let handler = Arc::new(Mutex::new(Vec::new()));
5176 let handler_clone = Arc::clone(&handler);
5177
5178 let mut aggregator = TimeBarAggregator::new(
5179 bar_type,
5180 instrument.price_precision(),
5181 instrument.size_precision(),
5182 clock.clone(),
5183 move |bar: Bar| {
5184 let mut handler_guard = handler_clone.lock();
5185 handler_guard.push(bar);
5186 },
5187 true, true, BarIntervalType::RightOpen,
5190 None,
5191 15,
5192 false, );
5194
5195 let ts1 = UnixNanos::from(1_000_000_000);
5196 aggregator.update(Price::from("100.00"), Quantity::from(1), ts1);
5197
5198 let ts2 = UnixNanos::from(2_000_000_000);
5199 clock.borrow_mut().set_time(ts2);
5200
5201 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts2, ts2);
5203 aggregator.build_bar(&event);
5204
5205 let handler_guard = handler.lock();
5206 let bar = handler_guard.first().unwrap();
5207 assert_eq!(bar.ts_event, UnixNanos::default());
5208 assert_eq!(bar.ts_init, ts2);
5209 }
5210
5211 #[rstest]
5212 fn test_renko_bar_aggregator_initialization(audusd_sim: CurrencyPair) {
5213 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5214 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5216 let handler = Arc::new(Mutex::new(Vec::new()));
5217 let handler_clone = Arc::clone(&handler);
5218
5219 let aggregator = RenkoBarAggregator::new(
5220 bar_type,
5221 instrument.price_precision(),
5222 instrument.size_precision(),
5223 instrument.price_increment(),
5224 move |bar: Bar| {
5225 let mut handler_guard = handler_clone.lock();
5226 handler_guard.push(bar);
5227 },
5228 );
5229
5230 assert_eq!(aggregator.bar_type(), bar_type);
5231 assert!(!aggregator.is_running());
5232 let expected_brick_size = 10 * instrument.price_increment().raw;
5234 assert_eq!(aggregator.brick_size, expected_brick_size);
5235 }
5236
5237 #[rstest]
5238 fn test_renko_bar_aggregator_update_below_brick_size_no_bar(audusd_sim: CurrencyPair) {
5239 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5240 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5242 let handler = Arc::new(Mutex::new(Vec::new()));
5243 let handler_clone = Arc::clone(&handler);
5244
5245 let mut aggregator = RenkoBarAggregator::new(
5246 bar_type,
5247 instrument.price_precision(),
5248 instrument.size_precision(),
5249 instrument.price_increment(),
5250 move |bar: Bar| {
5251 let mut handler_guard = handler_clone.lock();
5252 handler_guard.push(bar);
5253 },
5254 );
5255
5256 aggregator.update(
5258 Price::from("1.00000"),
5259 Quantity::from(1),
5260 UnixNanos::default(),
5261 );
5262 aggregator.update(
5263 Price::from("1.00005"),
5264 Quantity::from(1),
5265 UnixNanos::from(1000),
5266 );
5267
5268 let handler_guard = handler.lock();
5269 assert_eq!(handler_guard.len(), 0); }
5271
5272 #[rstest]
5273 fn test_renko_bar_aggregator_ignores_out_of_order_bar(audusd_sim: CurrencyPair) {
5274 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5275 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid);
5276 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5277 let handler = Arc::new(Mutex::new(Vec::new()));
5278 let handler_clone = Arc::clone(&handler);
5279 let mut aggregator = RenkoBarAggregator::new(
5280 bar_type,
5281 instrument.price_precision(),
5282 instrument.size_precision(),
5283 instrument.price_increment(),
5284 move |bar: Bar| {
5285 handler_clone.lock().push(bar);
5286 },
5287 );
5288 let first = Bar::new(
5289 bar_type,
5290 Price::from("1.00000"),
5291 Price::from("1.00000"),
5292 Price::from("1.00000"),
5293 Price::from("1.00000"),
5294 Quantity::from(1),
5295 UnixNanos::from(1_000),
5296 UnixNanos::from(1_000),
5297 );
5298 let stale = Bar::new(
5299 bar_type,
5300 Price::from("1.00020"),
5301 Price::from("1.00020"),
5302 Price::from("1.00020"),
5303 Price::from("1.00020"),
5304 Quantity::from(1),
5305 UnixNanos::from(500),
5306 UnixNanos::from(500),
5307 );
5308
5309 aggregator.update_bar(first, first.volume, first.ts_init);
5310 aggregator.update_bar(stale, stale.volume, stale.ts_init);
5311
5312 assert!(handler.lock().is_empty());
5313 assert_eq!(aggregator.last_close, Some(Price::from("1.00000")));
5314 assert_eq!(aggregator.core.builder.ts_last, UnixNanos::from(1_000));
5315 }
5316
5317 #[rstest]
5318 fn test_renko_bar_aggregator_update_exceeds_brick_size_creates_bar(audusd_sim: CurrencyPair) {
5319 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5320 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5322 let handler = Arc::new(Mutex::new(Vec::new()));
5323 let handler_clone = Arc::clone(&handler);
5324
5325 let mut aggregator = RenkoBarAggregator::new(
5326 bar_type,
5327 instrument.price_precision(),
5328 instrument.size_precision(),
5329 instrument.price_increment(),
5330 move |bar: Bar| {
5331 let mut handler_guard = handler_clone.lock();
5332 handler_guard.push(bar);
5333 },
5334 );
5335
5336 aggregator.update(
5338 Price::from("1.00000"),
5339 Quantity::from(1),
5340 UnixNanos::default(),
5341 );
5342 aggregator.update(
5343 Price::from("1.00015"),
5344 Quantity::from(1),
5345 UnixNanos::from(1000),
5346 );
5347
5348 let handler_guard = handler.lock();
5349 assert_eq!(handler_guard.len(), 1);
5350
5351 let bar = handler_guard.first().unwrap();
5352 assert_eq!(bar.open, Price::from("1.00000"));
5353 assert_eq!(bar.high, Price::from("1.00010"));
5354 assert_eq!(bar.low, Price::from("1.00000"));
5355 assert_eq!(bar.close, Price::from("1.00010"));
5356 assert_eq!(bar.volume, Quantity::from(2));
5357 assert_eq!(bar.ts_event, UnixNanos::from(1000));
5358 assert_eq!(bar.ts_init, UnixNanos::from(1000));
5359 }
5360
5361 #[rstest]
5362 fn test_renko_bar_aggregator_multiple_bricks_in_one_update(audusd_sim: CurrencyPair) {
5363 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5364 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5366 let handler = Arc::new(Mutex::new(Vec::new()));
5367 let handler_clone = Arc::clone(&handler);
5368
5369 let mut aggregator = RenkoBarAggregator::new(
5370 bar_type,
5371 instrument.price_precision(),
5372 instrument.size_precision(),
5373 instrument.price_increment(),
5374 move |bar: Bar| {
5375 let mut handler_guard = handler_clone.lock();
5376 handler_guard.push(bar);
5377 },
5378 );
5379
5380 aggregator.update(
5382 Price::from("1.00000"),
5383 Quantity::from(1),
5384 UnixNanos::default(),
5385 );
5386 aggregator.update(
5387 Price::from("1.00025"),
5388 Quantity::from(1),
5389 UnixNanos::from(1000),
5390 );
5391
5392 let handler_guard = handler.lock();
5393 assert_eq!(handler_guard.len(), 2);
5394
5395 let bar1 = &handler_guard[0];
5396 assert_eq!(bar1.open, Price::from("1.00000"));
5397 assert_eq!(bar1.high, Price::from("1.00010"));
5398 assert_eq!(bar1.low, Price::from("1.00000"));
5399 assert_eq!(bar1.close, Price::from("1.00010"));
5400
5401 let bar2 = &handler_guard[1];
5402 assert_eq!(bar2.open, Price::from("1.00010"));
5403 assert_eq!(bar2.high, Price::from("1.00020"));
5404 assert_eq!(bar2.low, Price::from("1.00010"));
5405 assert_eq!(bar2.close, Price::from("1.00020"));
5406 }
5407
5408 #[rstest]
5409 fn test_renko_bar_aggregator_downward_movement(audusd_sim: CurrencyPair) {
5410 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5411 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5413 let handler = Arc::new(Mutex::new(Vec::new()));
5414 let handler_clone = Arc::clone(&handler);
5415
5416 let mut aggregator = RenkoBarAggregator::new(
5417 bar_type,
5418 instrument.price_precision(),
5419 instrument.size_precision(),
5420 instrument.price_increment(),
5421 move |bar: Bar| {
5422 let mut handler_guard = handler_clone.lock();
5423 handler_guard.push(bar);
5424 },
5425 );
5426
5427 aggregator.update(
5429 Price::from("1.00020"),
5430 Quantity::from(1),
5431 UnixNanos::default(),
5432 );
5433 aggregator.update(
5434 Price::from("1.00005"),
5435 Quantity::from(1),
5436 UnixNanos::from(1000),
5437 );
5438
5439 let handler_guard = handler.lock();
5440 assert_eq!(handler_guard.len(), 1);
5441
5442 let bar = handler_guard.first().unwrap();
5443 assert_eq!(bar.open, Price::from("1.00020"));
5444 assert_eq!(bar.high, Price::from("1.00020"));
5445 assert_eq!(bar.low, Price::from("1.00010"));
5446 assert_eq!(bar.close, Price::from("1.00010"));
5447 assert_eq!(bar.volume, Quantity::from(2));
5448 }
5449
5450 #[rstest]
5451 fn test_renko_bar_aggregator_handle_bar_below_brick_size(audusd_sim: CurrencyPair) {
5452 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5453 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5455 let handler = Arc::new(Mutex::new(Vec::new()));
5456 let handler_clone = Arc::clone(&handler);
5457
5458 let mut aggregator = RenkoBarAggregator::new(
5459 bar_type,
5460 instrument.price_precision(),
5461 instrument.size_precision(),
5462 instrument.price_increment(),
5463 move |bar: Bar| {
5464 let mut handler_guard = handler_clone.lock();
5465 handler_guard.push(bar);
5466 },
5467 );
5468
5469 let input_bar = Bar::new(
5471 BarType::new(
5472 instrument.id(),
5473 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5474 AggregationSource::Internal,
5475 ),
5476 Price::from("1.00000"),
5477 Price::from("1.00005"),
5478 Price::from("0.99995"),
5479 Price::from("1.00005"), Quantity::from(100),
5481 UnixNanos::default(),
5482 UnixNanos::from(1000),
5483 );
5484
5485 aggregator.handle_bar(input_bar);
5486
5487 let handler_guard = handler.lock();
5488 assert_eq!(handler_guard.len(), 0); }
5490
5491 #[rstest]
5492 fn test_renko_bar_aggregator_handle_bar_exceeds_brick_size(audusd_sim: CurrencyPair) {
5493 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5494 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5496 let handler = Arc::new(Mutex::new(Vec::new()));
5497 let handler_clone = Arc::clone(&handler);
5498
5499 let mut aggregator = RenkoBarAggregator::new(
5500 bar_type,
5501 instrument.price_precision(),
5502 instrument.size_precision(),
5503 instrument.price_increment(),
5504 move |bar: Bar| {
5505 let mut handler_guard = handler_clone.lock();
5506 handler_guard.push(bar);
5507 },
5508 );
5509
5510 let bar1 = Bar::new(
5512 BarType::new(
5513 instrument.id(),
5514 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5515 AggregationSource::Internal,
5516 ),
5517 Price::from("1.00000"),
5518 Price::from("1.00005"),
5519 Price::from("0.99995"),
5520 Price::from("1.00000"),
5521 Quantity::from(100),
5522 UnixNanos::default(),
5523 UnixNanos::default(),
5524 );
5525
5526 let bar2 = Bar::new(
5528 BarType::new(
5529 instrument.id(),
5530 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5531 AggregationSource::Internal,
5532 ),
5533 Price::from("1.00000"),
5534 Price::from("1.00015"),
5535 Price::from("0.99995"),
5536 Price::from("1.00010"), Quantity::from(50),
5538 UnixNanos::from(60_000_000_000),
5539 UnixNanos::from(60_000_000_000),
5540 );
5541
5542 aggregator.handle_bar(bar1);
5543 aggregator.handle_bar(bar2);
5544
5545 let handler_guard = handler.lock();
5546 assert_eq!(handler_guard.len(), 1);
5547
5548 let bar = handler_guard.first().unwrap();
5549 assert_eq!(bar.open, Price::from("1.00000"));
5550 assert_eq!(bar.high, Price::from("1.00010"));
5551 assert_eq!(bar.low, Price::from("1.00000"));
5552 assert_eq!(bar.close, Price::from("1.00010"));
5553 assert_eq!(bar.volume, Quantity::from(150));
5554 }
5555
5556 #[rstest]
5557 fn test_renko_bar_aggregator_handle_bar_multiple_bricks(audusd_sim: CurrencyPair) {
5558 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5559 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5561 let handler = Arc::new(Mutex::new(Vec::new()));
5562 let handler_clone = Arc::clone(&handler);
5563
5564 let mut aggregator = RenkoBarAggregator::new(
5565 bar_type,
5566 instrument.price_precision(),
5567 instrument.size_precision(),
5568 instrument.price_increment(),
5569 move |bar: Bar| {
5570 let mut handler_guard = handler_clone.lock();
5571 handler_guard.push(bar);
5572 },
5573 );
5574
5575 let bar1 = Bar::new(
5577 BarType::new(
5578 instrument.id(),
5579 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5580 AggregationSource::Internal,
5581 ),
5582 Price::from("1.00000"),
5583 Price::from("1.00005"),
5584 Price::from("0.99995"),
5585 Price::from("1.00000"),
5586 Quantity::from(100),
5587 UnixNanos::default(),
5588 UnixNanos::default(),
5589 );
5590
5591 let bar2 = Bar::new(
5593 BarType::new(
5594 instrument.id(),
5595 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5596 AggregationSource::Internal,
5597 ),
5598 Price::from("1.00000"),
5599 Price::from("1.00035"),
5600 Price::from("0.99995"),
5601 Price::from("1.00030"), Quantity::from(50),
5603 UnixNanos::from(60_000_000_000),
5604 UnixNanos::from(60_000_000_000),
5605 );
5606
5607 aggregator.handle_bar(bar1);
5608 aggregator.handle_bar(bar2);
5609
5610 let handler_guard = handler.lock();
5611 assert_eq!(handler_guard.len(), 3);
5612
5613 let bar1 = &handler_guard[0];
5614 assert_eq!(bar1.open, Price::from("1.00000"));
5615 assert_eq!(bar1.close, Price::from("1.00010"));
5616
5617 let bar2 = &handler_guard[1];
5618 assert_eq!(bar2.open, Price::from("1.00010"));
5619 assert_eq!(bar2.close, Price::from("1.00020"));
5620
5621 let bar3 = &handler_guard[2];
5622 assert_eq!(bar3.open, Price::from("1.00020"));
5623 assert_eq!(bar3.close, Price::from("1.00030"));
5624 }
5625
5626 #[rstest]
5627 fn test_renko_bar_aggregator_handle_bar_downward_movement(audusd_sim: CurrencyPair) {
5628 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5629 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5631 let handler = Arc::new(Mutex::new(Vec::new()));
5632 let handler_clone = Arc::clone(&handler);
5633
5634 let mut aggregator = RenkoBarAggregator::new(
5635 bar_type,
5636 instrument.price_precision(),
5637 instrument.size_precision(),
5638 instrument.price_increment(),
5639 move |bar: Bar| {
5640 let mut handler_guard = handler_clone.lock();
5641 handler_guard.push(bar);
5642 },
5643 );
5644
5645 let bar1 = Bar::new(
5647 BarType::new(
5648 instrument.id(),
5649 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5650 AggregationSource::Internal,
5651 ),
5652 Price::from("1.00020"),
5653 Price::from("1.00025"),
5654 Price::from("1.00015"),
5655 Price::from("1.00020"),
5656 Quantity::from(100),
5657 UnixNanos::default(),
5658 UnixNanos::default(),
5659 );
5660
5661 let bar2 = Bar::new(
5663 BarType::new(
5664 instrument.id(),
5665 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5666 AggregationSource::Internal,
5667 ),
5668 Price::from("1.00020"),
5669 Price::from("1.00025"),
5670 Price::from("1.00005"),
5671 Price::from("1.00010"), Quantity::from(50),
5673 UnixNanos::from(60_000_000_000),
5674 UnixNanos::from(60_000_000_000),
5675 );
5676
5677 aggregator.handle_bar(bar1);
5678 aggregator.handle_bar(bar2);
5679
5680 let handler_guard = handler.lock();
5681 assert_eq!(handler_guard.len(), 1);
5682
5683 let bar = handler_guard.first().unwrap();
5684 assert_eq!(bar.open, Price::from("1.00020"));
5685 assert_eq!(bar.high, Price::from("1.00020"));
5686 assert_eq!(bar.low, Price::from("1.00010"));
5687 assert_eq!(bar.close, Price::from("1.00010"));
5688 assert_eq!(bar.volume, Quantity::from(150));
5689 }
5690
5691 #[rstest]
5692 fn test_renko_bar_aggregator_brick_size_calculation(audusd_sim: CurrencyPair) {
5693 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5694
5695 let bar_spec_5 = BarSpecification::new(5, BarAggregation::Renko, PriceType::Mid); let bar_type_5 = BarType::new(instrument.id(), bar_spec_5, AggregationSource::Internal);
5698 let handler = Arc::new(Mutex::new(Vec::new()));
5699 let handler_clone = Arc::clone(&handler);
5700
5701 let aggregator_5 = RenkoBarAggregator::new(
5702 bar_type_5,
5703 instrument.price_precision(),
5704 instrument.size_precision(),
5705 instrument.price_increment(),
5706 move |_bar: Bar| {
5707 let mut handler_guard = handler_clone.lock();
5708 handler_guard.push(_bar);
5709 },
5710 );
5711
5712 let expected_brick_size_5 = 5 * instrument.price_increment().raw;
5714 assert_eq!(aggregator_5.brick_size, expected_brick_size_5);
5715
5716 let bar_spec_20 = BarSpecification::new(20, BarAggregation::Renko, PriceType::Mid); let bar_type_20 = BarType::new(instrument.id(), bar_spec_20, AggregationSource::Internal);
5718 let handler2 = Arc::new(Mutex::new(Vec::new()));
5719 let handler2_clone = Arc::clone(&handler2);
5720
5721 let aggregator_20 = RenkoBarAggregator::new(
5722 bar_type_20,
5723 instrument.price_precision(),
5724 instrument.size_precision(),
5725 instrument.price_increment(),
5726 move |_bar: Bar| {
5727 let mut handler_guard = handler2_clone.lock();
5728 handler_guard.push(_bar);
5729 },
5730 );
5731
5732 let expected_brick_size_20 = 20 * instrument.price_increment().raw;
5734 assert_eq!(aggregator_20.brick_size, expected_brick_size_20);
5735 }
5736
5737 #[rstest]
5738 fn test_renko_bar_aggregator_sequential_updates(audusd_sim: CurrencyPair) {
5739 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5740 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5742 let handler = Arc::new(Mutex::new(Vec::new()));
5743 let handler_clone = Arc::clone(&handler);
5744
5745 let mut aggregator = RenkoBarAggregator::new(
5746 bar_type,
5747 instrument.price_precision(),
5748 instrument.size_precision(),
5749 instrument.price_increment(),
5750 move |bar: Bar| {
5751 let mut handler_guard = handler_clone.lock();
5752 handler_guard.push(bar);
5753 },
5754 );
5755
5756 aggregator.update(
5758 Price::from("1.00000"),
5759 Quantity::from(1),
5760 UnixNanos::from(1000),
5761 );
5762 aggregator.update(
5763 Price::from("1.00010"),
5764 Quantity::from(1),
5765 UnixNanos::from(2000),
5766 ); aggregator.update(
5768 Price::from("1.00020"),
5769 Quantity::from(1),
5770 UnixNanos::from(3000),
5771 ); aggregator.update(
5773 Price::from("1.00025"),
5774 Quantity::from(1),
5775 UnixNanos::from(4000),
5776 ); aggregator.update(
5778 Price::from("1.00030"),
5779 Quantity::from(1),
5780 UnixNanos::from(5000),
5781 ); let handler_guard = handler.lock();
5784 assert_eq!(handler_guard.len(), 3);
5785
5786 let bar1 = &handler_guard[0];
5787 assert_eq!(bar1.open, Price::from("1.00000"));
5788 assert_eq!(bar1.close, Price::from("1.00010"));
5789
5790 let bar2 = &handler_guard[1];
5791 assert_eq!(bar2.open, Price::from("1.00010"));
5792 assert_eq!(bar2.close, Price::from("1.00020"));
5793
5794 let bar3 = &handler_guard[2];
5795 assert_eq!(bar3.open, Price::from("1.00020"));
5796 assert_eq!(bar3.close, Price::from("1.00030"));
5797 }
5798
5799 #[rstest]
5800 fn test_renko_bar_aggregator_mixed_direction_movement(audusd_sim: CurrencyPair) {
5801 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5802 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5804 let handler = Arc::new(Mutex::new(Vec::new()));
5805 let handler_clone = Arc::clone(&handler);
5806
5807 let mut aggregator = RenkoBarAggregator::new(
5808 bar_type,
5809 instrument.price_precision(),
5810 instrument.size_precision(),
5811 instrument.price_increment(),
5812 move |bar: Bar| {
5813 let mut handler_guard = handler_clone.lock();
5814 handler_guard.push(bar);
5815 },
5816 );
5817
5818 aggregator.update(
5820 Price::from("1.00000"),
5821 Quantity::from(1),
5822 UnixNanos::from(1000),
5823 );
5824 aggregator.update(
5825 Price::from("1.00010"),
5826 Quantity::from(1),
5827 UnixNanos::from(2000),
5828 ); aggregator.update(
5830 Price::from("0.99990"),
5831 Quantity::from(1),
5832 UnixNanos::from(3000),
5833 ); let handler_guard = handler.lock();
5836 assert_eq!(handler_guard.len(), 3);
5837
5838 let bar1 = &handler_guard[0]; assert_eq!(bar1.open, Price::from("1.00000"));
5840 assert_eq!(bar1.high, Price::from("1.00010"));
5841 assert_eq!(bar1.low, Price::from("1.00000"));
5842 assert_eq!(bar1.close, Price::from("1.00010"));
5843
5844 let bar2 = &handler_guard[1]; assert_eq!(bar2.open, Price::from("1.00010"));
5846 assert_eq!(bar2.high, Price::from("1.00010"));
5847 assert_eq!(bar2.low, Price::from("1.00000"));
5848 assert_eq!(bar2.close, Price::from("1.00000"));
5849
5850 let bar3 = &handler_guard[2]; assert_eq!(bar3.open, Price::from("1.00000"));
5852 assert_eq!(bar3.high, Price::from("1.00000"));
5853 assert_eq!(bar3.low, Price::from("0.99990"));
5854 assert_eq!(bar3.close, Price::from("0.99990"));
5855 }
5856
5857 #[rstest]
5858 fn test_tick_imbalance_bar_aggregator_mixed_trades_cancel_out(equity_aapl: Equity) {
5859 let instrument = InstrumentAny::Equity(equity_aapl);
5860 let bar_spec = BarSpecification::new(3, BarAggregation::TickImbalance, PriceType::Last);
5861 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5862 let handler = Arc::new(Mutex::new(Vec::new()));
5863 let handler_clone = Arc::clone(&handler);
5864
5865 let mut aggregator = TickImbalanceBarAggregator::new(
5866 bar_type,
5867 instrument.price_precision(),
5868 instrument.size_precision(),
5869 move |bar: Bar| {
5870 let mut handler_guard = handler_clone.lock();
5871 handler_guard.push(bar);
5872 },
5873 );
5874
5875 let buy = TradeTick {
5876 aggressor_side: AggressorSide::Buy,
5877 ..TradeTick::default()
5878 };
5879 let sell = TradeTick {
5880 aggressor_side: AggressorSide::Sell,
5881 ..TradeTick::default()
5882 };
5883
5884 aggregator.handle_trade(buy);
5885 aggregator.handle_trade(sell);
5886 aggregator.handle_trade(buy);
5887
5888 let handler_guard = handler.lock();
5889 assert_eq!(handler_guard.len(), 0);
5890 }
5891
5892 #[rstest]
5893 fn test_tick_imbalance_bar_aggregator_no_aggressor_ignored(equity_aapl: Equity) {
5894 let instrument = InstrumentAny::Equity(equity_aapl);
5895 let bar_spec = BarSpecification::new(2, BarAggregation::TickImbalance, PriceType::Last);
5896 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5897 let handler = Arc::new(Mutex::new(Vec::new()));
5898 let handler_clone = Arc::clone(&handler);
5899
5900 let mut aggregator = TickImbalanceBarAggregator::new(
5901 bar_type,
5902 instrument.price_precision(),
5903 instrument.size_precision(),
5904 move |bar: Bar| {
5905 let mut handler_guard = handler_clone.lock();
5906 handler_guard.push(bar);
5907 },
5908 );
5909
5910 let buy = TradeTick {
5911 aggressor_side: AggressorSide::Buy,
5912 ..TradeTick::default()
5913 };
5914 let no_aggressor = TradeTick {
5915 aggressor_side: AggressorSide::NoAggressor,
5916 ..TradeTick::default()
5917 };
5918
5919 aggregator.handle_trade(buy);
5920 aggregator.handle_trade(no_aggressor);
5921 aggregator.handle_trade(buy);
5922
5923 let handler_guard = handler.lock();
5924 assert_eq!(handler_guard.len(), 1);
5925 }
5926
5927 #[rstest]
5928 fn test_tick_runs_bar_aggregator_multiple_consecutive_runs(equity_aapl: Equity) {
5929 let instrument = InstrumentAny::Equity(equity_aapl);
5930 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
5931 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5932 let handler = Arc::new(Mutex::new(Vec::new()));
5933 let handler_clone = Arc::clone(&handler);
5934
5935 let mut aggregator = TickRunsBarAggregator::new(
5936 bar_type,
5937 instrument.price_precision(),
5938 instrument.size_precision(),
5939 move |bar: Bar| {
5940 let mut handler_guard = handler_clone.lock();
5941 handler_guard.push(bar);
5942 },
5943 );
5944
5945 let buy = TradeTick {
5946 aggressor_side: AggressorSide::Buy,
5947 ..TradeTick::default()
5948 };
5949 let sell = TradeTick {
5950 aggressor_side: AggressorSide::Sell,
5951 ..TradeTick::default()
5952 };
5953
5954 aggregator.handle_trade(buy);
5955 aggregator.handle_trade(buy);
5956 aggregator.handle_trade(sell);
5957 aggregator.handle_trade(sell);
5958
5959 let handler_guard = handler.lock();
5960 assert_eq!(handler_guard.len(), 2);
5961 }
5962
5963 #[rstest]
5964 fn test_volume_imbalance_bar_aggregator_large_trade_spans_bars(equity_aapl: Equity) {
5965 let instrument = InstrumentAny::Equity(equity_aapl);
5966 let bar_spec = BarSpecification::new(10, BarAggregation::VolumeImbalance, PriceType::Last);
5967 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5968 let handler = Arc::new(Mutex::new(Vec::new()));
5969 let handler_clone = Arc::clone(&handler);
5970
5971 let mut aggregator = VolumeImbalanceBarAggregator::new(
5972 bar_type,
5973 instrument.price_precision(),
5974 instrument.size_precision(),
5975 move |bar: Bar| {
5976 let mut handler_guard = handler_clone.lock();
5977 handler_guard.push(bar);
5978 },
5979 );
5980
5981 let large_trade = TradeTick {
5982 size: Quantity::from(25),
5983 aggressor_side: AggressorSide::Buy,
5984 ..TradeTick::default()
5985 };
5986
5987 aggregator.handle_trade(large_trade);
5988
5989 let handler_guard = handler.lock();
5990 assert_eq!(handler_guard.len(), 2);
5991 }
5992
5993 #[rstest]
5994 fn test_volume_imbalance_bar_aggregator_no_aggressor_does_not_affect_imbalance(
5995 equity_aapl: Equity,
5996 ) {
5997 let instrument = InstrumentAny::Equity(equity_aapl);
5998 let bar_spec = BarSpecification::new(10, BarAggregation::VolumeImbalance, PriceType::Last);
5999 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6000 let handler = Arc::new(Mutex::new(Vec::new()));
6001 let handler_clone = Arc::clone(&handler);
6002
6003 let mut aggregator = VolumeImbalanceBarAggregator::new(
6004 bar_type,
6005 instrument.price_precision(),
6006 instrument.size_precision(),
6007 move |bar: Bar| {
6008 let mut handler_guard = handler_clone.lock();
6009 handler_guard.push(bar);
6010 },
6011 );
6012
6013 let buy = TradeTick {
6014 size: Quantity::from(5),
6015 aggressor_side: AggressorSide::Buy,
6016 ..TradeTick::default()
6017 };
6018 let no_aggressor = TradeTick {
6019 size: Quantity::from(3),
6020 aggressor_side: AggressorSide::NoAggressor,
6021 ..TradeTick::default()
6022 };
6023
6024 aggregator.handle_trade(buy);
6025 aggregator.handle_trade(no_aggressor);
6026 aggregator.handle_trade(buy);
6027
6028 let handler_guard = handler.lock();
6029 assert_eq!(handler_guard.len(), 1);
6030 }
6031
6032 #[rstest]
6033 fn test_volume_runs_bar_aggregator_large_trade_spans_bars(equity_aapl: Equity) {
6034 let instrument = InstrumentAny::Equity(equity_aapl);
6035 let bar_spec = BarSpecification::new(10, BarAggregation::VolumeRuns, PriceType::Last);
6036 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6037 let handler = Arc::new(Mutex::new(Vec::new()));
6038 let handler_clone = Arc::clone(&handler);
6039
6040 let mut aggregator = VolumeRunsBarAggregator::new(
6041 bar_type,
6042 instrument.price_precision(),
6043 instrument.size_precision(),
6044 move |bar: Bar| {
6045 let mut handler_guard = handler_clone.lock();
6046 handler_guard.push(bar);
6047 },
6048 );
6049
6050 let large_trade = TradeTick {
6051 size: Quantity::from(25),
6052 aggressor_side: AggressorSide::Buy,
6053 ..TradeTick::default()
6054 };
6055
6056 aggregator.handle_trade(large_trade);
6057
6058 let handler_guard = handler.lock();
6059 assert_eq!(handler_guard.len(), 2);
6060 }
6061
6062 #[rstest]
6063 fn test_value_runs_bar_aggregator_large_trade_spans_bars(equity_aapl: Equity) {
6064 let instrument = InstrumentAny::Equity(equity_aapl);
6065 let bar_spec = BarSpecification::new(50, BarAggregation::ValueRuns, PriceType::Last);
6066 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6067 let handler = Arc::new(Mutex::new(Vec::new()));
6068 let handler_clone = Arc::clone(&handler);
6069
6070 let mut aggregator = ValueRunsBarAggregator::new(
6071 bar_type,
6072 instrument.price_precision(),
6073 instrument.size_precision(),
6074 move |bar: Bar| {
6075 let mut handler_guard = handler_clone.lock();
6076 handler_guard.push(bar);
6077 },
6078 );
6079
6080 let large_trade = TradeTick {
6081 price: Price::from("5.00"),
6082 size: Quantity::from(25),
6083 aggressor_side: AggressorSide::Buy,
6084 ..TradeTick::default()
6085 };
6086
6087 aggregator.handle_trade(large_trade);
6088
6089 let handler_guard = handler.lock();
6090 assert_eq!(handler_guard.len(), 2);
6091 }
6092
6093 #[rstest]
6094 fn test_value_runs_bar_aggregator_keeps_leftover_volume_for_same_side_run(equity_aapl: Equity) {
6095 let instrument = InstrumentAny::Equity(equity_aapl);
6096 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
6097 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6098 let handler = Arc::new(Mutex::new(Vec::new()));
6099 let handler_clone = Arc::clone(&handler);
6100
6101 let mut aggregator = ValueRunsBarAggregator::new(
6102 bar_type,
6103 instrument.price_precision(),
6104 instrument.size_precision(),
6105 move |bar: Bar| {
6106 let mut handler_guard = handler_clone.lock();
6107 handler_guard.push(bar);
6108 },
6109 );
6110
6111 let first = TradeTick {
6114 price: Price::from("10.00"),
6115 size: Quantity::from(15),
6116 aggressor_side: AggressorSide::Sell,
6117 ts_event: UnixNanos::from(1_000),
6118 ts_init: UnixNanos::from(1_000),
6119 ..TradeTick::default()
6120 };
6121 aggregator.handle_trade(first);
6122
6123 let second = TradeTick {
6125 price: Price::from("10.00"),
6126 size: Quantity::from(5),
6127 aggressor_side: AggressorSide::Sell,
6128 ts_event: UnixNanos::from(2_000),
6129 ts_init: UnixNanos::from(2_000),
6130 ..TradeTick::default()
6131 };
6132 aggregator.handle_trade(second);
6133
6134 let handler_guard = handler.lock();
6135 assert_eq!(handler_guard.len(), 2);
6136 assert_eq!(handler_guard[0].volume, Quantity::from(10));
6137 assert_eq!(handler_guard[1].volume, Quantity::from(10));
6138 }
6139
6140 #[rstest]
6141 fn test_value_bar_high_price_low_step_no_zero_volume_bars(equity_aapl: Equity) {
6142 let instrument = InstrumentAny::Equity(equity_aapl);
6143 let bar_spec = BarSpecification::new(100, BarAggregation::Value, PriceType::Last);
6144 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6145 let handler = Arc::new(Mutex::new(Vec::new()));
6146 let handler_clone = Arc::clone(&handler);
6147
6148 let mut aggregator = ValueBarAggregator::new(
6149 bar_type,
6150 instrument.price_precision(),
6151 instrument.size_precision(),
6152 move |bar: Bar| {
6153 let mut handler_guard = handler_clone.lock();
6154 handler_guard.push(bar);
6155 },
6156 );
6157
6158 aggregator.update(
6160 Price::from("1000.00"),
6161 Quantity::from(3),
6162 UnixNanos::default(),
6163 );
6164
6165 let handler_guard = handler.lock();
6167 assert_eq!(handler_guard.len(), 3);
6168 for bar in handler_guard.iter() {
6169 assert_eq!(bar.volume, Quantity::from(1));
6170 }
6171 }
6172
6173 #[rstest]
6174 fn test_value_imbalance_high_price_low_step_no_zero_volume_bars(equity_aapl: Equity) {
6175 let instrument = InstrumentAny::Equity(equity_aapl);
6176 let bar_spec = BarSpecification::new(100, BarAggregation::ValueImbalance, PriceType::Last);
6177 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6178 let handler = Arc::new(Mutex::new(Vec::new()));
6179 let handler_clone = Arc::clone(&handler);
6180
6181 let mut aggregator = ValueImbalanceBarAggregator::new(
6182 bar_type,
6183 instrument.price_precision(),
6184 instrument.size_precision(),
6185 move |bar: Bar| {
6186 let mut handler_guard = handler_clone.lock();
6187 handler_guard.push(bar);
6188 },
6189 );
6190
6191 let trade = TradeTick {
6192 price: Price::from("1000.00"),
6193 size: Quantity::from(3),
6194 aggressor_side: AggressorSide::Buy,
6195 instrument_id: instrument.id(),
6196 ..TradeTick::default()
6197 };
6198
6199 aggregator.handle_trade(trade);
6200
6201 let handler_guard = handler.lock();
6202 assert_eq!(handler_guard.len(), 3);
6203 for bar in handler_guard.iter() {
6204 assert_eq!(bar.volume, Quantity::from(1));
6205 }
6206 }
6207
6208 #[rstest]
6209 fn test_value_imbalance_opposite_side_overshoot_emits_bar(equity_aapl: Equity) {
6210 let instrument = InstrumentAny::Equity(equity_aapl);
6211 let bar_spec = BarSpecification::new(100, BarAggregation::ValueImbalance, PriceType::Last);
6212 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6213 let handler = Arc::new(Mutex::new(Vec::new()));
6214 let handler_clone = Arc::clone(&handler);
6215
6216 let mut aggregator = ValueImbalanceBarAggregator::new(
6217 bar_type,
6218 instrument.price_precision(),
6219 instrument.size_precision(),
6220 move |bar: Bar| {
6221 let mut handler_guard = handler_clone.lock();
6222 handler_guard.push(bar);
6223 },
6224 );
6225
6226 let sell_tick = TradeTick {
6228 price: Price::from("10.00"),
6229 size: Quantity::from(5),
6230 aggressor_side: AggressorSide::Sell,
6231 instrument_id: instrument.id(),
6232 ..TradeTick::default()
6233 };
6234
6235 let buy_tick = TradeTick {
6238 price: Price::from("1000.00"),
6239 size: Quantity::from(1),
6240 aggressor_side: AggressorSide::Buy,
6241 instrument_id: instrument.id(),
6242 ts_init: UnixNanos::from(1),
6243 ts_event: UnixNanos::from(1),
6244 ..TradeTick::default()
6245 };
6246
6247 aggregator.handle_trade(sell_tick);
6248 aggregator.handle_trade(buy_tick);
6249
6250 let handler_guard = handler.lock();
6251 assert_eq!(handler_guard.len(), 1);
6252 assert_eq!(handler_guard[0].volume, Quantity::from(6));
6253 }
6254
6255 #[rstest]
6256 fn test_value_runs_high_price_low_step_no_zero_volume_bars(equity_aapl: Equity) {
6257 let instrument = InstrumentAny::Equity(equity_aapl);
6258 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
6259 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6260 let handler = Arc::new(Mutex::new(Vec::new()));
6261 let handler_clone = Arc::clone(&handler);
6262
6263 let mut aggregator = ValueRunsBarAggregator::new(
6264 bar_type,
6265 instrument.price_precision(),
6266 instrument.size_precision(),
6267 move |bar: Bar| {
6268 let mut handler_guard = handler_clone.lock();
6269 handler_guard.push(bar);
6270 },
6271 );
6272
6273 let trade = TradeTick {
6274 price: Price::from("1000.00"),
6275 size: Quantity::from(3),
6276 aggressor_side: AggressorSide::Buy,
6277 instrument_id: instrument.id(),
6278 ..TradeTick::default()
6279 };
6280
6281 aggregator.handle_trade(trade);
6282
6283 let handler_guard = handler.lock();
6284 assert_eq!(handler_guard.len(), 3);
6285 for bar in handler_guard.iter() {
6286 assert_eq!(bar.volume, Quantity::from(1));
6287 }
6288 }
6289
6290 #[rstest]
6291 fn test_value_imbalance_bar_aggregator_exact_below_step_retains_pending() {
6292 let instrument_id = InstrumentId::from("AAPL.XNAS");
6296 let bar_spec = BarSpecification::new(
6297 9_007_199_254,
6298 BarAggregation::ValueImbalance,
6299 PriceType::Last,
6300 );
6301 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6302 let handler = Arc::new(Mutex::new(Vec::new()));
6303 let handler_clone = Arc::clone(&handler);
6304
6305 let mut aggregator = ValueImbalanceBarAggregator::new(bar_type, 0, 9, move |bar: Bar| {
6306 handler_clone.lock().push(bar);
6307 });
6308
6309 let below_step = TradeTick {
6310 instrument_id,
6311 price: Price::from("1"),
6312 size: Quantity::from("9007199253.999999999"),
6313 aggressor_side: AggressorSide::Buy,
6314 ..TradeTick::default()
6315 };
6316 aggregator.handle_trade(below_step);
6317
6318 assert!(handler.lock().is_empty());
6319 assert_eq!(
6320 aggregator.core.builder.volume,
6321 Quantity::from("9007199253.999999999"),
6322 );
6323
6324 let one_raw_unit = TradeTick {
6327 instrument_id,
6328 price: Price::from("1"),
6329 size: Quantity::from("0.000000001"),
6330 aggressor_side: AggressorSide::Buy,
6331 ts_event: UnixNanos::from(1),
6332 ts_init: UnixNanos::from(1),
6333 ..TradeTick::default()
6334 };
6335 aggregator.handle_trade(one_raw_unit);
6336
6337 let handler_guard = handler.lock();
6338 assert_eq!(handler_guard.len(), 1);
6339 assert_eq!(
6340 handler_guard[0].volume,
6341 Quantity::from("9007199254.000000000")
6342 );
6343 assert_eq!(aggregator.core.builder.volume, Quantity::zero(9));
6344 }
6345
6346 #[rstest]
6347 fn test_value_imbalance_bar_aggregator_conserves_volume_across_split_bars() {
6348 let instrument_id = InstrumentId::from("AAPL.XNAS");
6352 let bar_spec = BarSpecification::new(4, BarAggregation::ValueImbalance, PriceType::Last);
6353 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6354 let handler = Arc::new(Mutex::new(Vec::new()));
6355 let handler_clone = Arc::clone(&handler);
6356
6357 let mut aggregator = ValueImbalanceBarAggregator::new(bar_type, 0, 9, move |bar: Bar| {
6358 handler_clone.lock().push(bar);
6359 });
6360
6361 let input = Quantity::from("10.000000003");
6362 let trade = TradeTick {
6363 instrument_id,
6364 price: Price::from("1"),
6365 size: input,
6366 aggressor_side: AggressorSide::Buy,
6367 ..TradeTick::default()
6368 };
6369 aggregator.handle_trade(trade);
6370
6371 let handler_guard = handler.lock();
6372 assert_eq!(handler_guard.len(), 2);
6373 for bar in handler_guard.iter() {
6374 assert_eq!(bar.volume, Quantity::from("4.000000000"));
6375 }
6376 assert_eq!(
6377 aggregator.core.builder.volume,
6378 Quantity::from("2.000000003"),
6379 );
6380 let emitted_plus_pending = handler_guard
6381 .iter()
6382 .map(|bar| bar.volume.as_decimal())
6383 .sum::<Decimal>()
6384 + aggregator.core.builder.volume.as_decimal();
6385 assert_eq!(emitted_plus_pending, input.as_decimal());
6386 }
6387
6388 #[rstest]
6389 fn test_value_runs_bar_aggregator_exact_below_step_retains_pending() {
6390 let instrument_id = InstrumentId::from("AAPL.XNAS");
6394 let bar_spec =
6395 BarSpecification::new(9_007_199_254, BarAggregation::ValueRuns, PriceType::Last);
6396 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6397 let handler = Arc::new(Mutex::new(Vec::new()));
6398 let handler_clone = Arc::clone(&handler);
6399
6400 let mut aggregator = ValueRunsBarAggregator::new(bar_type, 0, 9, move |bar: Bar| {
6401 handler_clone.lock().push(bar);
6402 });
6403
6404 let below_step = TradeTick {
6405 instrument_id,
6406 price: Price::from("1"),
6407 size: Quantity::from("9007199253.999999999"),
6408 aggressor_side: AggressorSide::Buy,
6409 ..TradeTick::default()
6410 };
6411 aggregator.handle_trade(below_step);
6412
6413 assert!(handler.lock().is_empty());
6414 assert_eq!(
6415 aggregator.core.builder.volume,
6416 Quantity::from("9007199253.999999999"),
6417 );
6418
6419 let one_raw_unit = TradeTick {
6422 instrument_id,
6423 price: Price::from("1"),
6424 size: Quantity::from("0.000000001"),
6425 aggressor_side: AggressorSide::Buy,
6426 ts_event: UnixNanos::from(1),
6427 ts_init: UnixNanos::from(1),
6428 ..TradeTick::default()
6429 };
6430 aggregator.handle_trade(one_raw_unit);
6431
6432 let handler_guard = handler.lock();
6433 assert_eq!(handler_guard.len(), 1);
6434 assert_eq!(
6435 handler_guard[0].volume,
6436 Quantity::from("9007199254.000000000")
6437 );
6438 assert_eq!(aggregator.core.builder.volume, Quantity::zero(9));
6439 }
6440
6441 #[rstest]
6442 fn test_value_runs_bar_aggregator_conserves_volume_across_split_bars() {
6443 let instrument_id = InstrumentId::from("AAPL.XNAS");
6447 let bar_spec = BarSpecification::new(4, BarAggregation::ValueRuns, PriceType::Last);
6448 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6449 let handler = Arc::new(Mutex::new(Vec::new()));
6450 let handler_clone = Arc::clone(&handler);
6451
6452 let mut aggregator = ValueRunsBarAggregator::new(bar_type, 0, 9, move |bar: Bar| {
6453 handler_clone.lock().push(bar);
6454 });
6455
6456 let input = Quantity::from("10.000000003");
6457 let trade = TradeTick {
6458 instrument_id,
6459 price: Price::from("1"),
6460 size: input,
6461 aggressor_side: AggressorSide::Buy,
6462 ..TradeTick::default()
6463 };
6464 aggregator.handle_trade(trade);
6465
6466 let handler_guard = handler.lock();
6467 assert_eq!(handler_guard.len(), 2);
6468 for bar in handler_guard.iter() {
6469 assert_eq!(bar.volume, Quantity::from("4.000000000"));
6470 }
6471 assert_eq!(
6472 aggregator.core.builder.volume,
6473 Quantity::from("2.000000003"),
6474 );
6475 let emitted_plus_pending = handler_guard
6476 .iter()
6477 .map(|bar| bar.volume.as_decimal())
6478 .sum::<Decimal>()
6479 + aggregator.core.builder.volume.as_decimal();
6480 assert_eq!(emitted_plus_pending, input.as_decimal());
6481 }
6482
6483 #[rstest]
6484 fn test_value_imbalance_bar_aggregator_no_aggressor_and_zero_price_fall_back_to_plain_volume() {
6485 let instrument_id = InstrumentId::from("AAPL.XNAS");
6488 let bar_spec = BarSpecification::new(100, BarAggregation::ValueImbalance, PriceType::Last);
6489 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6490 let handler = Arc::new(Mutex::new(Vec::new()));
6491 let handler_clone = Arc::clone(&handler);
6492
6493 let mut aggregator = ValueImbalanceBarAggregator::new(bar_type, 2, 0, move |bar: Bar| {
6494 handler_clone.lock().push(bar);
6495 });
6496
6497 let no_aggressor = TradeTick {
6498 instrument_id,
6499 price: Price::from("10.00"),
6500 size: Quantity::from(3),
6501 aggressor_side: AggressorSide::NoAggressor,
6502 ..TradeTick::default()
6503 };
6504 let zero_price = TradeTick {
6505 instrument_id,
6506 price: Price::from("0.00"),
6507 size: Quantity::from(4),
6508 aggressor_side: AggressorSide::Buy,
6509 ts_event: UnixNanos::from(1),
6510 ts_init: UnixNanos::from(1),
6511 ..TradeTick::default()
6512 };
6513 aggregator.handle_trade(no_aggressor);
6514 aggregator.handle_trade(zero_price);
6515
6516 assert!(handler.lock().is_empty());
6517 assert_eq!(aggregator.core.builder.volume, Quantity::from(7));
6518 }
6519
6520 #[rstest]
6521 fn test_value_runs_bar_aggregator_no_aggressor_and_zero_price_fall_back_to_plain_volume() {
6522 let instrument_id = InstrumentId::from("AAPL.XNAS");
6525 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
6526 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6527 let handler = Arc::new(Mutex::new(Vec::new()));
6528 let handler_clone = Arc::clone(&handler);
6529
6530 let mut aggregator = ValueRunsBarAggregator::new(bar_type, 2, 0, move |bar: Bar| {
6531 handler_clone.lock().push(bar);
6532 });
6533
6534 let no_aggressor = TradeTick {
6535 instrument_id,
6536 price: Price::from("10.00"),
6537 size: Quantity::from(3),
6538 aggressor_side: AggressorSide::NoAggressor,
6539 ..TradeTick::default()
6540 };
6541 let zero_price = TradeTick {
6542 instrument_id,
6543 price: Price::from("0.00"),
6544 size: Quantity::from(4),
6545 aggressor_side: AggressorSide::Buy,
6546 ts_event: UnixNanos::from(1),
6547 ts_init: UnixNanos::from(1),
6548 ..TradeTick::default()
6549 };
6550 aggregator.handle_trade(no_aggressor);
6551 aggregator.handle_trade(zero_price);
6552
6553 assert!(handler.lock().is_empty());
6554 assert_eq!(aggregator.core.builder.volume, Quantity::from(7));
6555 }
6556
6557 #[rstest]
6558 fn test_value_imbalance_bar_aggregator_conserves_volume_with_indivisible_price() {
6559 let instrument_id = InstrumentId::from("AAPL.XNAS");
6564 let bar_spec = BarSpecification::new(1, BarAggregation::ValueImbalance, PriceType::Last);
6565 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6566 let handler = Arc::new(Mutex::new(Vec::new()));
6567 let handler_clone = Arc::clone(&handler);
6568
6569 let mut aggregator = ValueImbalanceBarAggregator::new(bar_type, 2, 1, move |bar: Bar| {
6570 handler_clone.lock().push(bar);
6571 });
6572
6573 let input = Quantity::from("1.0");
6574 let trade = TradeTick {
6575 instrument_id,
6576 price: Price::from("3.00"),
6577 size: input,
6578 aggressor_side: AggressorSide::Buy,
6579 ..TradeTick::default()
6580 };
6581 aggregator.handle_trade(trade);
6582
6583 let handler_guard = handler.lock();
6584 assert_eq!(handler_guard.len(), 3);
6585 for bar in handler_guard.iter() {
6586 assert_eq!(bar.volume, Quantity::from("0.3"));
6587 }
6588 assert_eq!(aggregator.core.builder.volume, Quantity::from("0.1"));
6589 let emitted_plus_pending = handler_guard
6590 .iter()
6591 .map(|bar| bar.volume.as_decimal())
6592 .sum::<Decimal>()
6593 + aggregator.core.builder.volume.as_decimal();
6594 assert_eq!(emitted_plus_pending, input.as_decimal());
6595 }
6596
6597 #[rstest]
6598 fn test_value_runs_bar_aggregator_conserves_volume_with_indivisible_price() {
6599 let instrument_id = InstrumentId::from("AAPL.XNAS");
6604 let bar_spec = BarSpecification::new(1, BarAggregation::ValueRuns, PriceType::Last);
6605 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6606 let handler = Arc::new(Mutex::new(Vec::new()));
6607 let handler_clone = Arc::clone(&handler);
6608
6609 let mut aggregator = ValueRunsBarAggregator::new(bar_type, 2, 1, move |bar: Bar| {
6610 handler_clone.lock().push(bar);
6611 });
6612
6613 let input = Quantity::from("1.0");
6614 let trade = TradeTick {
6615 instrument_id,
6616 price: Price::from("3.00"),
6617 size: input,
6618 aggressor_side: AggressorSide::Buy,
6619 ..TradeTick::default()
6620 };
6621 aggregator.handle_trade(trade);
6622
6623 let handler_guard = handler.lock();
6624 assert_eq!(handler_guard.len(), 3);
6625 for bar in handler_guard.iter() {
6626 assert_eq!(bar.volume, Quantity::from("0.3"));
6627 }
6628 assert_eq!(aggregator.core.builder.volume, Quantity::from("0.1"));
6629 let emitted_plus_pending = handler_guard
6630 .iter()
6631 .map(|bar| bar.volume.as_decimal())
6632 .sum::<Decimal>()
6633 + aggregator.core.builder.volume.as_decimal();
6634 assert_eq!(emitted_plus_pending, input.as_decimal());
6635 }
6636
6637 #[rstest]
6638 #[case(1000_u64)]
6639 #[case(1500_u64)]
6640 fn test_volume_imbalance_bar_aggregator_large_step_no_overflow(
6641 equity_aapl: Equity,
6642 #[case] step: u64,
6643 ) {
6644 let instrument = InstrumentAny::Equity(equity_aapl);
6645 let bar_spec = BarSpecification::new(
6646 step as usize,
6647 BarAggregation::VolumeImbalance,
6648 PriceType::Last,
6649 );
6650 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6651 let handler = Arc::new(Mutex::new(Vec::new()));
6652 let handler_clone = Arc::clone(&handler);
6653
6654 let mut aggregator = VolumeImbalanceBarAggregator::new(
6655 bar_type,
6656 instrument.price_precision(),
6657 instrument.size_precision(),
6658 move |bar: Bar| {
6659 let mut handler_guard = handler_clone.lock();
6660 handler_guard.push(bar);
6661 },
6662 );
6663
6664 let trade = TradeTick {
6665 size: Quantity::from(step * 2),
6666 aggressor_side: AggressorSide::Buy,
6667 ..TradeTick::default()
6668 };
6669
6670 aggregator.handle_trade(trade);
6671
6672 let handler_guard = handler.lock();
6673 assert_eq!(handler_guard.len(), 2);
6674 for bar in handler_guard.iter() {
6675 assert_eq!(bar.volume.as_f64(), step as f64);
6676 }
6677 }
6678
6679 #[rstest]
6680 fn test_volume_imbalance_bar_aggregator_different_large_steps_produce_different_bar_counts(
6681 equity_aapl: Equity,
6682 ) {
6683 let instrument = InstrumentAny::Equity(equity_aapl);
6684 let total_volume = 3000_u64;
6685 let mut results = Vec::new();
6686
6687 for step in [1000_usize, 1500] {
6688 let bar_spec =
6689 BarSpecification::new(step, BarAggregation::VolumeImbalance, PriceType::Last);
6690 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6691 let handler = Arc::new(Mutex::new(Vec::new()));
6692 let handler_clone = Arc::clone(&handler);
6693
6694 let mut aggregator = VolumeImbalanceBarAggregator::new(
6695 bar_type,
6696 instrument.price_precision(),
6697 instrument.size_precision(),
6698 move |bar: Bar| {
6699 let mut handler_guard = handler_clone.lock();
6700 handler_guard.push(bar);
6701 },
6702 );
6703
6704 let trade = TradeTick {
6705 size: Quantity::from(total_volume),
6706 aggressor_side: AggressorSide::Buy,
6707 ..TradeTick::default()
6708 };
6709
6710 aggregator.handle_trade(trade);
6711
6712 let handler_guard = handler.lock();
6713 results.push(handler_guard.len());
6714 }
6715
6716 assert_eq!(results[0], 3); assert_eq!(results[1], 2); assert_ne!(results[0], results[1]);
6719 }
6720
6721 #[rstest]
6722 #[case(1000_u64)]
6723 #[case(1500_u64)]
6724 fn test_volume_runs_bar_aggregator_large_step_no_overflow(
6725 equity_aapl: Equity,
6726 #[case] step: u64,
6727 ) {
6728 let instrument = InstrumentAny::Equity(equity_aapl);
6729 let bar_spec =
6730 BarSpecification::new(step as usize, BarAggregation::VolumeRuns, PriceType::Last);
6731 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6732 let handler = Arc::new(Mutex::new(Vec::new()));
6733 let handler_clone = Arc::clone(&handler);
6734
6735 let mut aggregator = VolumeRunsBarAggregator::new(
6736 bar_type,
6737 instrument.price_precision(),
6738 instrument.size_precision(),
6739 move |bar: Bar| {
6740 let mut handler_guard = handler_clone.lock();
6741 handler_guard.push(bar);
6742 },
6743 );
6744
6745 let trade = TradeTick {
6746 size: Quantity::from(step * 2),
6747 aggressor_side: AggressorSide::Buy,
6748 ..TradeTick::default()
6749 };
6750
6751 aggregator.handle_trade(trade);
6752
6753 let handler_guard = handler.lock();
6754 assert_eq!(handler_guard.len(), 2);
6755 for bar in handler_guard.iter() {
6756 assert_eq!(bar.volume.as_f64(), step as f64);
6757 }
6758 }
6759
6760 #[rstest]
6761 fn test_volume_runs_bar_aggregator_different_large_steps_produce_different_bar_counts(
6762 equity_aapl: Equity,
6763 ) {
6764 let instrument = InstrumentAny::Equity(equity_aapl);
6765 let total_volume = 3000_u64;
6766 let mut results = Vec::new();
6767
6768 for step in [1000_usize, 1500] {
6769 let bar_spec = BarSpecification::new(step, BarAggregation::VolumeRuns, PriceType::Last);
6770 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6771 let handler = Arc::new(Mutex::new(Vec::new()));
6772 let handler_clone = Arc::clone(&handler);
6773
6774 let mut aggregator = VolumeRunsBarAggregator::new(
6775 bar_type,
6776 instrument.price_precision(),
6777 instrument.size_precision(),
6778 move |bar: Bar| {
6779 let mut handler_guard = handler_clone.lock();
6780 handler_guard.push(bar);
6781 },
6782 );
6783
6784 let trade = TradeTick {
6785 size: Quantity::from(total_volume),
6786 aggressor_side: AggressorSide::Buy,
6787 ..TradeTick::default()
6788 };
6789
6790 aggregator.handle_trade(trade);
6791
6792 let handler_guard = handler.lock();
6793 results.push(handler_guard.len());
6794 }
6795
6796 assert_eq!(results[0], 3); assert_eq!(results[1], 2); assert_ne!(results[0], results[1]);
6799 }
6800
6801 #[rstest]
6803 fn test_time_bar_historical_defers_event_at_ts_init_until_after_update(equity_aapl: Equity) {
6804 let instrument = InstrumentAny::Equity(equity_aapl);
6805 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
6806 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6807 let handler = Arc::new(Mutex::new(Vec::new()));
6808 let handler_clone = Arc::clone(&handler);
6809 let clock = Rc::new(RefCell::new(TestClock::new()));
6810
6811 let mut agg = TimeBarAggregator::new(
6812 bar_type,
6813 instrument.price_precision(),
6814 instrument.size_precision(),
6815 clock.clone(),
6816 move |bar: Bar| {
6817 let mut h = handler_clone.lock();
6818 h.push(bar);
6819 },
6820 true,
6821 true,
6822 BarIntervalType::LeftOpen,
6823 None,
6824 0,
6825 false,
6826 );
6827 agg.historical_mode = true;
6828 agg.set_clock_internal(clock);
6829 let boxed: Box<dyn BarAggregator> = Box::new(agg);
6830 let rc = Rc::new(RefCell::new(boxed));
6831 rc.borrow_mut().set_aggregator_weak(Rc::downgrade(&rc));
6832
6833 rc.borrow_mut().update(
6834 Price::from("100.00"),
6835 Quantity::from(1),
6836 UnixNanos::default(),
6837 );
6838 rc.borrow_mut().update(
6839 Price::from("100.00"),
6840 Quantity::from(1),
6841 UnixNanos::from(1_000_000_000),
6842 );
6843
6844 let bars = handler.lock();
6845 assert!(
6846 !bars.is_empty(),
6847 "deferred event at ts_init should produce a bar that includes the update"
6848 );
6849 let last_bar = bars.last().unwrap();
6850 assert_eq!(last_bar.close, Price::from("100.00"));
6851 assert!(
6852 last_bar.volume.as_f64() >= 1.0,
6853 "bar built after deferred event should include the update at ts_init"
6854 );
6855 }
6856
6857 #[rstest]
6858 fn test_spread_quote_quote_driven_emits_when_all_legs_received(equity_aapl: Equity) {
6859 let instrument = InstrumentAny::Equity(equity_aapl);
6860 let leg1 = instrument.id();
6861 let leg2 = InstrumentId::from("MSFT.XNAS");
6862 let spread_id = InstrumentId::from("SPREAD.XNAS");
6863 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
6864 let handler = Arc::new(Mutex::new(Vec::new()));
6865 let handler_clone = Arc::clone(&handler);
6866 let clock = Rc::new(RefCell::new(TestClock::new()));
6867
6868 let mut agg = SpreadQuoteAggregator::new(
6869 spread_id,
6870 &legs,
6871 true,
6872 instrument.price_precision(),
6873 0,
6874 Box::new(move |q: QuoteTick| {
6875 handler_clone.lock().push(q);
6876 }),
6877 clock,
6878 false,
6879 None,
6880 0,
6881 false,
6882 60,
6883 None,
6884 None,
6885 );
6886
6887 let ts = UnixNanos::from(1_000_000_000);
6888 agg.handle_quote_tick(QuoteTick::new(
6889 leg1,
6890 Price::from("100.00"),
6891 Price::from("100.10"),
6892 Quantity::from(10),
6893 Quantity::from(10),
6894 ts,
6895 ts,
6896 ));
6897 assert_eq!(handler.lock().len(), 0);
6898
6899 agg.handle_quote_tick(QuoteTick::new(
6900 leg2,
6901 Price::from("99.00"),
6902 Price::from("99.10"),
6903 Quantity::from(10),
6904 Quantity::from(10),
6905 ts,
6906 ts,
6907 ));
6908 let quotes = handler.lock();
6909 assert_eq!(quotes.len(), 1);
6910 assert_eq!(quotes[0].instrument_id, spread_id);
6911 assert!(quotes[0].bid_price < quotes[0].ask_price);
6912 }
6913
6914 #[rstest]
6915 fn test_spread_quote_futures_pricing_signed_ratios(equity_aapl: Equity) {
6916 let instrument = InstrumentAny::Equity(equity_aapl);
6917 let leg1 = instrument.id();
6918 let leg2 = InstrumentId::from("MSFT.XNAS");
6919 let spread_id = InstrumentId::from("SPREAD.XNAS");
6920 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
6921 let handler = Arc::new(Mutex::new(Vec::new()));
6922 let handler_clone = Arc::clone(&handler);
6923 let clock = Rc::new(RefCell::new(TestClock::new()));
6924
6925 let mut agg = SpreadQuoteAggregator::new(
6926 spread_id,
6927 &legs,
6928 true,
6929 instrument.price_precision(),
6930 0,
6931 Box::new(move |q: QuoteTick| {
6932 handler_clone.lock().push(q);
6933 }),
6934 clock,
6935 false,
6936 None,
6937 0,
6938 false,
6939 60,
6940 None,
6941 None,
6942 );
6943
6944 let ts = UnixNanos::from(1_000_000_000);
6945 agg.handle_quote_tick(QuoteTick::new(
6946 leg1,
6947 Price::from("10.00"),
6948 Price::from("10.10"),
6949 Quantity::from(100),
6950 Quantity::from(100),
6951 ts,
6952 ts,
6953 ));
6954 agg.handle_quote_tick(QuoteTick::new(
6955 leg2,
6956 Price::from("20.00"),
6957 Price::from("20.10"),
6958 Quantity::from(100),
6959 Quantity::from(100),
6960 ts,
6961 ts,
6962 ));
6963 let quotes = handler.lock();
6964 assert_eq!(quotes.len(), 1);
6965 let q = "es[0];
6966 assert_eq!(q.instrument_id, spread_id);
6967 assert_eq!(q.bid_price, Price::from("-10.10"));
6968 assert_eq!(q.ask_price, Price::from("-9.90"));
6969 }
6970
6971 #[rstest]
6972 fn test_spread_quote_size_calculation_non_unit_ratios(equity_aapl: Equity) {
6973 let instrument = InstrumentAny::Equity(equity_aapl);
6974 let leg1 = instrument.id();
6975 let leg2 = InstrumentId::from("MSFT.XNAS");
6976 let spread_id = InstrumentId::from("SPREAD.XNAS");
6977 let legs = vec![(leg1, 2_i64), (leg2, -1_i64)];
6978 let handler = Arc::new(Mutex::new(Vec::new()));
6979 let handler_clone = Arc::clone(&handler);
6980 let clock = Rc::new(RefCell::new(TestClock::new()));
6981
6982 let mut agg = SpreadQuoteAggregator::new(
6983 spread_id,
6984 &legs,
6985 true,
6986 instrument.price_precision(),
6987 0,
6988 Box::new(move |q: QuoteTick| {
6989 handler_clone.lock().push(q);
6990 }),
6991 clock,
6992 false,
6993 None,
6994 0,
6995 false,
6996 60,
6997 None,
6998 None,
6999 );
7000
7001 let ts = UnixNanos::from(1_000_000_000);
7002 agg.handle_quote_tick(QuoteTick::new(
7003 leg1,
7004 Price::from("10.00"),
7005 Price::from("10.10"),
7006 Quantity::from(100),
7007 Quantity::from(40),
7008 ts,
7009 ts,
7010 ));
7011 agg.handle_quote_tick(QuoteTick::new(
7012 leg2,
7013 Price::from("10.00"),
7014 Price::from("10.10"),
7015 Quantity::from(50),
7016 Quantity::from(30),
7017 ts,
7018 ts,
7019 ));
7020 let quotes = handler.lock();
7021 assert_eq!(quotes.len(), 1);
7022 let q = "es[0];
7023 assert_eq!(q.bid_size.as_f64(), 30.0);
7024 assert_eq!(q.ask_size.as_f64(), 20.0);
7025 }
7026
7027 #[rstest]
7028 fn test_spread_quote_timer_driven_emission_cadence(equity_aapl: Equity) {
7029 let instrument = InstrumentAny::Equity(equity_aapl);
7030 let leg1 = instrument.id();
7031 let leg2 = InstrumentId::from("MSFT.XNAS");
7032 let spread_id = InstrumentId::from("SPREAD.XNAS");
7033 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
7034 let handler = Arc::new(Mutex::new(Vec::new()));
7035 let handler_clone = Arc::clone(&handler);
7036 let clock = Rc::new(RefCell::new(TestClock::new()));
7037 clock.borrow_mut().set_time(UnixNanos::from(0));
7038
7039 let agg = SpreadQuoteAggregator::new(
7040 spread_id,
7041 &legs,
7042 true,
7043 instrument.price_precision(),
7044 0,
7045 Box::new(move |q: QuoteTick| {
7046 handler_clone.lock().push(q);
7047 }),
7048 clock.clone(),
7049 false,
7050 Some(1),
7051 0,
7052 false,
7053 60,
7054 None,
7055 None,
7056 );
7057 let rc = Rc::new(RefCell::new(agg));
7058 rc.borrow_mut().prepare_for_timer_mode(&rc);
7059 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7060
7061 for event in clock.borrow_mut().advance_time(UnixNanos::from(0), true) {
7062 rc.borrow_mut().on_timer_fire(event.ts_event);
7063 }
7064 assert_eq!(handler.lock().len(), 0);
7065
7066 let ts1 = UnixNanos::from(1_000_000_000);
7067 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7068 leg1,
7069 Price::from("100.00"),
7070 Price::from("100.10"),
7071 Quantity::from(10),
7072 Quantity::from(10),
7073 ts1,
7074 ts1,
7075 ));
7076 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7077 leg2,
7078 Price::from("99.00"),
7079 Price::from("99.10"),
7080 Quantity::from(10),
7081 Quantity::from(10),
7082 ts1,
7083 ts1,
7084 ));
7085
7086 for event in clock.borrow_mut().advance_time(ts1, true) {
7087 rc.borrow_mut().on_timer_fire(event.ts_event);
7088 }
7089
7090 {
7091 let quotes = handler.lock();
7092 assert_eq!(quotes.len(), 1);
7093 assert_eq!(quotes[0].ts_event, ts1);
7094 assert_eq!(quotes[0].ts_init, ts1);
7095 }
7096
7097 let ts2 = UnixNanos::from(2_000_000_000);
7098 for event in clock.borrow_mut().advance_time(ts2, true) {
7099 rc.borrow_mut().on_timer_fire(event.ts_event);
7100 }
7101
7102 let quotes = handler.lock();
7103 assert_eq!(quotes.len(), 1);
7104 }
7105
7106 #[rstest]
7107 fn test_spread_quote_historical_timer_waits_for_all_legs(equity_aapl: Equity) {
7108 let instrument = InstrumentAny::Equity(equity_aapl);
7109 let leg1 = instrument.id();
7110 let leg2 = InstrumentId::from("MSFT.XNAS");
7111 let spread_id = InstrumentId::from("SPREAD.XNAS");
7112 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
7113 let handler = Arc::new(Mutex::new(Vec::new()));
7114 let handler_clone = Arc::clone(&handler);
7115 let clock = Rc::new(RefCell::new(TestClock::new()));
7116
7117 let agg = SpreadQuoteAggregator::new(
7118 spread_id,
7119 &legs,
7120 true,
7121 instrument.price_precision(),
7122 0,
7123 Box::new(move |q: QuoteTick| {
7124 handler_clone.lock().push(q);
7125 }),
7126 clock.clone(),
7128 true,
7129 Some(1),
7130 0,
7131 false,
7132 60,
7133 None,
7134 None,
7135 );
7136 let rc = Rc::new(RefCell::new(agg));
7137 rc.borrow_mut().prepare_for_timer_mode(&rc);
7138 rc.borrow_mut().set_clock(clock);
7139
7140 let ts1 = UnixNanos::from(1_000_000_000);
7141 let ts2 = UnixNanos::from(2_000_000_000);
7142 let ts3 = UnixNanos::from(3_000_000_000);
7143 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7144 leg1,
7145 Price::from("100.00"),
7146 Price::from("100.10"),
7147 Quantity::from(10),
7148 Quantity::from(10),
7149 ts1,
7150 ts1,
7151 ));
7152 assert_eq!(handler.lock().len(), 0);
7153
7154 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7155 leg2,
7156 Price::from("99.00"),
7157 Price::from("99.10"),
7158 Quantity::from(10),
7159 Quantity::from(10),
7160 ts2,
7161 ts2,
7162 ));
7163 assert_eq!(handler.lock().len(), 0);
7164
7165 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7166 leg1,
7167 Price::from("100.00"),
7168 Price::from("100.10"),
7169 Quantity::from(10),
7170 Quantity::from(10),
7171 ts3,
7172 ts3,
7173 ));
7174 let quotes = handler.lock();
7175 assert_eq!(
7176 quotes.len(),
7177 1,
7178 "deferred event at ts2 is processed when we have all legs and advance to ts3"
7179 );
7180 }
7181
7182 #[rstest]
7183 fn test_spread_quote_historical_flush_emits_pending_final_quote(equity_aapl: Equity) {
7184 let instrument = InstrumentAny::Equity(equity_aapl);
7185 let leg1 = instrument.id();
7186 let leg2 = InstrumentId::from("MSFT.XNAS");
7187 let spread_id = InstrumentId::from("SPREAD.XNAS");
7188 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
7189 let handler = Arc::new(Mutex::new(Vec::new()));
7190 let handler_clone = Arc::clone(&handler);
7191 let clock = Rc::new(RefCell::new(TestClock::new()));
7192
7193 let agg = SpreadQuoteAggregator::new(
7194 spread_id,
7195 &legs,
7196 true,
7197 instrument.price_precision(),
7198 0,
7199 Box::new(move |q: QuoteTick| {
7200 handler_clone.lock().push(q);
7201 }),
7202 clock.clone(),
7204 true,
7205 Some(1),
7206 0,
7207 false,
7208 60,
7209 None,
7210 None,
7211 );
7212 let rc = Rc::new(RefCell::new(agg));
7213 rc.borrow_mut().prepare_for_timer_mode(&rc);
7214 rc.borrow_mut().set_clock(clock);
7215
7216 let ts1 = UnixNanos::from(1_000_000_000);
7217 let ts2 = UnixNanos::from(2_000_000_000);
7218 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7219 leg1,
7220 Price::from("100.00"),
7221 Price::from("100.10"),
7222 Quantity::from(10),
7223 Quantity::from(10),
7224 ts1,
7225 ts1,
7226 ));
7227 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7228 leg2,
7229 Price::from("99.00"),
7230 Price::from("99.10"),
7231 Quantity::from(10),
7232 Quantity::from(10),
7233 ts2,
7234 ts2,
7235 ));
7236
7237 assert_eq!(handler.lock().len(), 0);
7238
7239 rc.borrow_mut().flush_pending_historical_quote();
7240
7241 let quotes = handler.lock();
7242 assert_eq!(
7243 quotes.len(),
7244 1,
7245 "final historical quote should be emitted when the deferred event is flushed",
7246 );
7247 assert_eq!(quotes[0].ts_event, ts2);
7248 }
7249
7250 #[rstest]
7251 fn test_spread_quote_option_vega_weighting(equity_aapl: Equity) {
7252 let instrument = InstrumentAny::Equity(equity_aapl);
7253 let leg1 = instrument.id();
7254 let leg2 = InstrumentId::from("MSFT.XNAS");
7255 let spread_id = InstrumentId::from("SPREAD.XNAS");
7256 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
7257 let handler = Arc::new(Mutex::new(Vec::new()));
7258 let handler_clone = Arc::clone(&handler);
7259 let clock = Rc::new(RefCell::new(TestClock::new()));
7260
7261 let mut vega_provider = MapVegaProvider::new();
7262 vega_provider.insert(leg1, 0.15);
7263 vega_provider.insert(leg2, 0.12);
7264
7265 let mut agg = SpreadQuoteAggregator::new(
7266 spread_id,
7267 &legs,
7268 false,
7269 instrument.price_precision(),
7270 0,
7271 Box::new(move |q: QuoteTick| {
7272 handler_clone.lock().push(q);
7273 }),
7274 clock,
7275 false,
7276 None,
7277 0,
7278 false,
7279 60,
7280 Some(Box::new(vega_provider)),
7281 None,
7282 );
7283
7284 let ts = UnixNanos::from(1_000_000_000);
7285 agg.handle_quote_tick(QuoteTick::new(
7286 leg1,
7287 Price::from("10.00"),
7288 Price::from("10.20"),
7289 Quantity::from(100),
7290 Quantity::from(100),
7291 ts,
7292 ts,
7293 ));
7294 agg.handle_quote_tick(QuoteTick::new(
7295 leg2,
7296 Price::from("11.00"),
7297 Price::from("11.20"),
7298 Quantity::from(100),
7299 Quantity::from(100),
7300 ts,
7301 ts,
7302 ));
7303 let quotes = handler.lock();
7304 assert_eq!(quotes.len(), 1);
7305 let q = "es[0];
7306 assert!(q.bid_price < q.ask_price);
7307 assert!(q.ask_price.as_f64() - q.bid_price.as_f64() > 0.0);
7308 }
7309
7310 #[rstest]
7311 fn test_spread_quote_all_zero_vega_fallback(equity_aapl: Equity) {
7312 let instrument = InstrumentAny::Equity(equity_aapl);
7313 let leg1 = instrument.id();
7314 let leg2 = InstrumentId::from("MSFT.XNAS");
7315 let spread_id = InstrumentId::from("SPREAD.XNAS");
7316 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
7317 let handler = Arc::new(Mutex::new(Vec::new()));
7318 let handler_clone = Arc::clone(&handler);
7319 let clock = Rc::new(RefCell::new(TestClock::new()));
7320
7321 let mut vega_provider = MapVegaProvider::new();
7322 vega_provider.insert(leg1, 0.0);
7323 vega_provider.insert(leg2, 0.0);
7324
7325 let agg = SpreadQuoteAggregator::new(
7326 spread_id,
7327 &legs,
7328 false,
7329 instrument.price_precision(),
7330 0,
7331 Box::new(move |q: QuoteTick| {
7332 handler_clone.lock().push(q);
7333 }),
7334 clock.clone(),
7335 false,
7336 None,
7337 0,
7338 false,
7339 1,
7340 Some(Box::new(vega_provider)),
7341 None,
7342 );
7343 let rc = Rc::new(RefCell::new(agg));
7344 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7345
7346 let ts = UnixNanos::from(1_000_000_000);
7347 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7348 leg1,
7349 Price::from("10.00"),
7350 Price::from("10.10"),
7351 Quantity::from(100),
7352 Quantity::from(100),
7353 ts,
7354 ts,
7355 ));
7356 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7357 leg2,
7358 Price::from("20.00"),
7359 Price::from("20.10"),
7360 Quantity::from(100),
7361 Quantity::from(100),
7362 ts,
7363 ts,
7364 ));
7365 {
7366 let quotes = handler.lock();
7367 assert_eq!(quotes.len(), 1);
7368 let q = "es[0];
7369 assert_eq!(q.bid_price, Price::from("-10.10"));
7370 assert_eq!(q.ask_price, Price::from("-9.90"));
7371 }
7372 assert!(rc.borrow().vega_pricing_temporarily_disabled);
7373
7374 let timeout_name = rc.borrow().vega_pricing_timeout_timer_name.clone();
7375 assert!(
7376 clock
7377 .borrow()
7378 .timer_names()
7379 .contains(&timeout_name.as_str())
7380 );
7381
7382 let events = clock
7383 .borrow_mut()
7384 .advance_time(UnixNanos::from(2_000_000_000), true);
7385
7386 for handler in clock.borrow().match_handlers(events) {
7387 handler.run();
7388 }
7389
7390 assert!(!rc.borrow().vega_pricing_temporarily_disabled);
7391
7392 let cancel_handler = Arc::new(Mutex::new(Vec::new()));
7393 let cancel_handler_clone = Arc::clone(&cancel_handler);
7394 let mut cancel_vega_provider = MapVegaProvider::new();
7395 cancel_vega_provider.insert(leg1, 0.0);
7396 cancel_vega_provider.insert(leg2, 0.0);
7397 let cancel_agg = SpreadQuoteAggregator::new(
7398 spread_id,
7399 &legs,
7400 false,
7401 instrument.price_precision(),
7402 0,
7403 Box::new(move |q: QuoteTick| {
7404 cancel_handler_clone.lock().push(q);
7405 }),
7406 clock.clone(),
7407 false,
7408 None,
7409 0,
7410 false,
7411 10,
7412 Some(Box::new(cancel_vega_provider)),
7413 None,
7414 );
7415 let cancel_rc = Rc::new(RefCell::new(cancel_agg));
7416 cancel_rc
7417 .borrow_mut()
7418 .start_timer(Some(Rc::clone(&cancel_rc)));
7419 cancel_rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7420 leg1,
7421 Price::from("10.00"),
7422 Price::from("10.10"),
7423 Quantity::from(100),
7424 Quantity::from(100),
7425 ts,
7426 ts,
7427 ));
7428 cancel_rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7429 leg2,
7430 Price::from("20.00"),
7431 Price::from("20.10"),
7432 Quantity::from(100),
7433 Quantity::from(100),
7434 ts,
7435 ts,
7436 ));
7437 let cancel_timeout_name = cancel_rc.borrow().vega_pricing_timeout_timer_name.clone();
7438 assert!(
7439 clock
7440 .borrow()
7441 .timer_names()
7442 .contains(&cancel_timeout_name.as_str())
7443 );
7444 cancel_rc.borrow_mut().stop_timer();
7445 assert!(
7446 !clock
7447 .borrow()
7448 .timer_names()
7449 .contains(&cancel_timeout_name.as_str())
7450 );
7451
7452 let permanent_handler = Arc::new(Mutex::new(Vec::new()));
7453 let permanent_handler_clone = Arc::clone(&permanent_handler);
7454 let mut permanent_vega_provider = MapVegaProvider::new();
7455 permanent_vega_provider.insert(leg1, 0.15);
7456 permanent_vega_provider.insert(leg2, 0.12);
7457 let mut permanent_agg = SpreadQuoteAggregator::new(
7458 spread_id,
7459 &legs,
7460 false,
7461 instrument.price_precision(),
7462 0,
7463 Box::new(move |q: QuoteTick| {
7464 permanent_handler_clone.lock().push(q);
7465 }),
7466 Rc::new(RefCell::new(TestClock::new())),
7467 false,
7468 None,
7469 0,
7470 true,
7471 1,
7472 Some(Box::new(permanent_vega_provider)),
7473 None,
7474 );
7475
7476 permanent_agg.handle_quote_tick(QuoteTick::new(
7477 leg1,
7478 Price::from("10.00"),
7479 Price::from("10.10"),
7480 Quantity::from(100),
7481 Quantity::from(100),
7482 ts,
7483 ts,
7484 ));
7485 permanent_agg.handle_quote_tick(QuoteTick::new(
7486 leg2,
7487 Price::from("20.00"),
7488 Price::from("20.10"),
7489 Quantity::from(100),
7490 Quantity::from(100),
7491 ts,
7492 ts,
7493 ));
7494
7495 let permanent_quotes = permanent_handler.lock();
7496 assert_eq!(permanent_quotes.len(), 1);
7497 assert_eq!(permanent_quotes[0].bid_price, Price::from("-10.10"));
7498 assert_eq!(permanent_quotes[0].ask_price, Price::from("-9.90"));
7499 assert!(!permanent_agg.vega_pricing_temporarily_disabled);
7500 }
7501
7502 #[rstest]
7503 fn test_spread_quote_negative_prices_tick_scheme(equity_aapl: Equity) {
7504 let instrument = InstrumentAny::Equity(equity_aapl);
7505 let leg1 = instrument.id();
7506 let leg2 = InstrumentId::from("MSFT.XNAS");
7507 let spread_id = InstrumentId::from("SPREAD.XNAS");
7508 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
7509 let handler = Arc::new(Mutex::new(Vec::new()));
7510 let handler_clone = Arc::clone(&handler);
7511 let clock = Rc::new(RefCell::new(TestClock::new()));
7512 let rounder = FixedTickSchemeRounder::new(0.01).unwrap();
7513
7514 let mut agg = SpreadQuoteAggregator::new(
7515 spread_id,
7516 &legs,
7517 true,
7518 2,
7519 0,
7520 Box::new(move |q: QuoteTick| {
7521 handler_clone.lock().push(q);
7522 }),
7523 clock,
7524 false,
7525 None,
7526 0,
7527 false,
7528 60,
7529 None,
7530 Some(Box::new(rounder)),
7531 );
7532
7533 let ts = UnixNanos::from(1_000_000_000);
7534 agg.handle_quote_tick(QuoteTick::new(
7535 leg1,
7536 Price::from("10.00"),
7537 Price::from("10.10"),
7538 Quantity::from(100),
7539 Quantity::from(100),
7540 ts,
7541 ts,
7542 ));
7543 agg.handle_quote_tick(QuoteTick::new(
7544 leg2,
7545 Price::from("20.00"),
7546 Price::from("20.10"),
7547 Quantity::from(100),
7548 Quantity::from(100),
7549 ts,
7550 ts,
7551 ));
7552 let quotes = handler.lock();
7553 assert_eq!(quotes.len(), 1);
7554 let q = "es[0];
7555 assert!(q.bid_price.as_f64() < 0.0);
7556 assert!(q.ask_price.as_f64() < 0.0);
7557 assert!(q.bid_price < q.ask_price);
7558 }
7559
7560 #[rstest]
7561 #[case(BarIntervalType::LeftOpen)]
7562 #[case(BarIntervalType::RightOpen)]
7563 fn test_time_bar_skip_first_non_full_bar_noop_on_boundary(
7564 equity_aapl: Equity,
7565 #[case] interval_type: BarIntervalType,
7566 ) {
7567 let instrument = InstrumentAny::Equity(equity_aapl);
7572 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
7573 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7574 let handler = Arc::new(Mutex::new(Vec::new()));
7575 let handler_clone = Arc::clone(&handler);
7576 let clock = Rc::new(RefCell::new(TestClock::new()));
7577 clock.borrow_mut().set_time(UnixNanos::from(1_000_000_000));
7578 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7579
7580 let aggregator = TimeBarAggregator::new(
7581 bar_type,
7582 instrument.price_precision(),
7583 instrument.size_precision(),
7584 clock,
7585 move |bar: Bar| {
7586 let mut h = handler_clone.lock();
7587 h.push(bar);
7588 },
7589 false,
7590 false,
7591 interval_type,
7592 None,
7593 0,
7594 true, );
7596
7597 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7598 let rc = Rc::new(RefCell::new(boxed));
7599 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7600
7601 rc.borrow_mut().update(
7602 Price::from("100.00"),
7603 Quantity::from(1),
7604 UnixNanos::from(1_000_000_000),
7605 );
7606 rc.borrow_mut().build_bar(&TimeEvent::new(
7607 event_name,
7608 UUID4::new(),
7609 UnixNanos::from(2_000_000_000),
7610 UnixNanos::from(2_000_000_000),
7611 ));
7612 rc.borrow_mut().update(
7613 Price::from("101.00"),
7614 Quantity::from(1),
7615 UnixNanos::from(2_500_000_000),
7616 );
7617 rc.borrow_mut().build_bar(&TimeEvent::new(
7618 event_name,
7619 UUID4::new(),
7620 UnixNanos::from(3_000_000_000),
7621 UnixNanos::from(3_000_000_000),
7622 ));
7623
7624 let bars = handler.lock();
7625 assert_eq!(bars.len(), 2);
7626 assert_eq!(bars[0].close, Price::from("100.00"));
7627 assert_eq!(bars[1].close, Price::from("101.00"));
7628 }
7629
7630 #[rstest]
7631 #[case(BarIntervalType::LeftOpen)]
7632 #[case(BarIntervalType::RightOpen)]
7633 fn test_time_bar_skip_first_non_full_bar_drops_partial_bar(
7634 equity_aapl: Equity,
7635 #[case] interval_type: BarIntervalType,
7636 ) {
7637 let instrument = InstrumentAny::Equity(equity_aapl);
7641 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
7642 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7643 let handler = Arc::new(Mutex::new(Vec::new()));
7644 let handler_clone = Arc::clone(&handler);
7645 let clock = Rc::new(RefCell::new(TestClock::new()));
7646 clock.borrow_mut().set_time(UnixNanos::from(1_500_000_000));
7647 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7648
7649 let aggregator = TimeBarAggregator::new(
7650 bar_type,
7651 instrument.price_precision(),
7652 instrument.size_precision(),
7653 clock,
7654 move |bar: Bar| {
7655 let mut h = handler_clone.lock();
7656 h.push(bar);
7657 },
7658 false,
7659 false,
7660 interval_type,
7661 None,
7662 0,
7663 true, );
7665
7666 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7667 let rc = Rc::new(RefCell::new(boxed));
7668 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7669
7670 rc.borrow_mut().update(
7671 Price::from("100.00"),
7672 Quantity::from(1),
7673 UnixNanos::from(1_500_000_000),
7674 );
7675 rc.borrow_mut().build_bar(&TimeEvent::new(
7676 event_name,
7677 UUID4::new(),
7678 UnixNanos::from(2_000_000_000),
7679 UnixNanos::from(2_000_000_000),
7680 ));
7681 rc.borrow_mut().update(
7682 Price::from("101.00"),
7683 Quantity::from(1),
7684 UnixNanos::from(2_500_000_000),
7685 );
7686 rc.borrow_mut().build_bar(&TimeEvent::new(
7687 event_name,
7688 UUID4::new(),
7689 UnixNanos::from(3_000_000_000),
7690 UnixNanos::from(3_000_000_000),
7691 ));
7692
7693 let bars = handler.lock();
7694 assert_eq!(bars.len(), 1);
7695 assert_eq!(bars[0].close, Price::from("101.00"));
7696 }
7697
7698 #[rstest]
7699 fn test_time_bar_skip_first_non_full_bar_skips_every_call_before_first_close(
7700 equity_aapl: Equity,
7701 ) {
7702 let instrument = InstrumentAny::Equity(equity_aapl);
7706 let bar_spec = BarSpecification::new(10, BarAggregation::Second, PriceType::Last);
7707 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7708 let handler = Arc::new(Mutex::new(Vec::new()));
7709 let handler_clone = Arc::clone(&handler);
7710 let clock = Rc::new(RefCell::new(TestClock::new()));
7711 clock.borrow_mut().set_time(UnixNanos::from(5_000_000_000));
7712 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7713
7714 let aggregator = TimeBarAggregator::new(
7715 bar_type,
7716 instrument.price_precision(),
7717 instrument.size_precision(),
7718 clock,
7719 move |bar: Bar| {
7720 let mut h = handler_clone.lock();
7721 h.push(bar);
7722 },
7723 false,
7724 false,
7725 BarIntervalType::LeftOpen,
7726 None,
7727 0,
7728 true, );
7730
7731 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7732 let rc = Rc::new(RefCell::new(boxed));
7733 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7734
7735 for (price, update_ts, event_ts) in [
7739 ("100.00", 5_500_000_000_u64, 7_000_000_000_u64),
7740 ("101.00", 7_500_000_000_u64, 8_000_000_000_u64),
7741 ("102.00", 9_000_000_000_u64, 10_000_000_000_u64),
7742 ] {
7743 rc.borrow_mut().update(
7744 Price::from(price),
7745 Quantity::from(1),
7746 UnixNanos::from(update_ts),
7747 );
7748 rc.borrow_mut().build_bar(&TimeEvent::new(
7749 event_name,
7750 UUID4::new(),
7751 UnixNanos::from(event_ts),
7752 UnixNanos::from(event_ts),
7753 ));
7754 }
7755
7756 rc.borrow_mut().update(
7758 Price::from("103.00"),
7759 Quantity::from(1),
7760 UnixNanos::from(10_500_000_000),
7761 );
7762 rc.borrow_mut().build_bar(&TimeEvent::new(
7763 event_name,
7764 UUID4::new(),
7765 UnixNanos::from(11_000_000_000),
7766 UnixNanos::from(11_000_000_000),
7767 ));
7768
7769 let bars = handler.lock();
7770 assert_eq!(bars.len(), 1);
7771 assert_eq!(bars[0].close, Price::from("103.00"));
7772 }
7773
7774 #[rstest]
7775 fn test_time_bar_skip_first_non_full_bar_skips_when_build_delay_shifts_start(
7776 equity_aapl: Equity,
7777 ) {
7778 let instrument = InstrumentAny::Equity(equity_aapl);
7782 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
7783 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7784 let handler = Arc::new(Mutex::new(Vec::new()));
7785 let handler_clone = Arc::clone(&handler);
7786 let clock = Rc::new(RefCell::new(TestClock::new()));
7787 clock.borrow_mut().set_time(UnixNanos::from(2_000_000_000));
7788 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7789
7790 let aggregator = TimeBarAggregator::new(
7791 bar_type,
7792 instrument.price_precision(),
7793 instrument.size_precision(),
7794 clock,
7795 move |bar: Bar| {
7796 let mut h = handler_clone.lock();
7797 h.push(bar);
7798 },
7799 false,
7800 false,
7801 BarIntervalType::LeftOpen,
7802 None,
7803 100, true, );
7806
7807 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7808 let rc = Rc::new(RefCell::new(boxed));
7809 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7810
7811 rc.borrow_mut().update(
7813 Price::from("100.00"),
7814 Quantity::from(1),
7815 UnixNanos::from(2_500_000_000),
7816 );
7817 rc.borrow_mut().build_bar(&TimeEvent::new(
7818 event_name,
7819 UUID4::new(),
7820 UnixNanos::from(3_000_100_000),
7821 UnixNanos::from(3_000_100_000),
7822 ));
7823 rc.borrow_mut().update(
7824 Price::from("101.00"),
7825 Quantity::from(1),
7826 UnixNanos::from(3_500_000_000),
7827 );
7828 rc.borrow_mut().build_bar(&TimeEvent::new(
7829 event_name,
7830 UUID4::new(),
7831 UnixNanos::from(4_000_100_000),
7832 UnixNanos::from(4_000_100_000),
7833 ));
7834
7835 let bars = handler.lock();
7836 assert_eq!(bars.len(), 1);
7837 assert_eq!(bars[0].close, Price::from("101.00"));
7838 }
7839
7840 #[rstest]
7841 #[case(
7842 BarAggregation::Month,
7843 1_735_689_600_000_000_000_u64,
7844 1_733_011_200_000_000_000_u64
7845 )]
7846 #[case(
7847 BarAggregation::Year,
7848 1_735_689_600_000_000_000_u64,
7849 1_704_067_200_000_000_000_u64
7850 )]
7851 fn test_time_bar_fire_immediately_month_year_stored_open_points_to_previous_period(
7852 equity_aapl: Equity,
7853 #[case] aggregation: BarAggregation,
7854 #[case] start_ns: u64,
7855 #[case] expected_stored_open_ns: u64,
7856 ) {
7857 let instrument = InstrumentAny::Equity(equity_aapl);
7861 let bar_spec = BarSpecification::new(1, aggregation, PriceType::Last);
7862 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7863 let handler = Arc::new(Mutex::new(Vec::new()));
7864 let handler_clone = Arc::clone(&handler);
7865 let clock = Rc::new(RefCell::new(TestClock::new()));
7866 clock.borrow_mut().set_time(UnixNanos::from(start_ns));
7867 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7868
7869 let aggregator = TimeBarAggregator::new(
7870 bar_type,
7871 instrument.price_precision(),
7872 instrument.size_precision(),
7873 clock,
7874 move |bar: Bar| {
7875 let mut h = handler_clone.lock();
7876 h.push(bar);
7877 },
7878 false,
7879 false,
7880 BarIntervalType::RightOpen, None,
7882 0,
7883 false, );
7885
7886 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7887 let rc = Rc::new(RefCell::new(boxed));
7888 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7889
7890 rc.borrow_mut().update(
7891 Price::from("100.00"),
7892 Quantity::from(1),
7893 UnixNanos::from(start_ns),
7894 );
7895 rc.borrow_mut().build_bar(&TimeEvent::new(
7896 event_name,
7897 UUID4::new(),
7898 UnixNanos::from(start_ns),
7899 UnixNanos::from(start_ns),
7900 ));
7901
7902 let bars = handler.lock();
7903 assert_eq!(bars.len(), 1);
7904 assert_eq!(bars[0].ts_event, UnixNanos::from(expected_stored_open_ns));
7905 assert_eq!(bars[0].ts_init, UnixNanos::from(start_ns));
7906 }
7907
7908 #[rstest]
7909 fn test_time_bar_historical_prevents_bars_for_timer_before_last_data(equity_aapl: Equity) {
7910 let instrument = InstrumentAny::Equity(equity_aapl);
7911 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
7912 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7913 let handler = Arc::new(Mutex::new(Vec::new()));
7914 let handler_clone = Arc::clone(&handler);
7915 let clock = Rc::new(RefCell::new(TestClock::new()));
7916
7917 let mut agg = TimeBarAggregator::new(
7918 bar_type,
7919 instrument.price_precision(),
7920 instrument.size_precision(),
7921 clock.clone(),
7922 move |bar: Bar| {
7923 let mut h = handler_clone.lock();
7924 h.push(bar);
7925 },
7926 true,
7927 true,
7928 BarIntervalType::LeftOpen,
7929 None,
7930 0,
7931 false,
7932 );
7933 agg.historical_mode = true;
7934 agg.set_clock_internal(clock);
7935 let boxed: Box<dyn BarAggregator> = Box::new(agg);
7936 let rc = Rc::new(RefCell::new(boxed));
7937 rc.borrow_mut().set_aggregator_weak(Rc::downgrade(&rc));
7938
7939 let ts1 = UnixNanos::from(2_000_000_000);
7940 rc.borrow_mut()
7941 .update(Price::from("100.00"), Quantity::from(1), ts1);
7942
7943 let ts2 = UnixNanos::from(3_000_000_000);
7944 rc.borrow_mut()
7945 .update(Price::from("101.00"), Quantity::from(1), ts2);
7946
7947 let bars = handler.lock();
7948 assert!(
7949 !bars.is_empty(),
7950 "advancing time from ts1 to ts2 should produce at least one bar"
7951 );
7952 assert_eq!(bars[0].close, Price::from("100.00"));
7953 }
7954
7955 #[rstest]
7956 #[case(BarAggregation::Tick)]
7957 #[case(BarAggregation::TickImbalance)]
7958 #[case(BarAggregation::TickRuns)]
7959 #[case(BarAggregation::Volume)]
7960 #[case(BarAggregation::VolumeImbalance)]
7961 #[case(BarAggregation::VolumeRuns)]
7962 #[case(BarAggregation::Value)]
7963 #[case(BarAggregation::ValueImbalance)]
7964 #[case(BarAggregation::ValueRuns)]
7965 #[case(BarAggregation::Renko)]
7966 fn test_aggregators_standardize_composite_bar_type(
7967 equity_aapl: Equity,
7968 #[case] aggregation: BarAggregation,
7969 ) {
7970 let instrument = InstrumentAny::Equity(equity_aapl);
7971 let bar_type = BarType::new_composite(
7972 instrument.id(),
7973 BarSpecification::new(10, aggregation, PriceType::Last),
7974 AggregationSource::Internal,
7975 1,
7976 BarAggregation::Minute,
7977 AggregationSource::External,
7978 );
7979 let handler = |_: Bar| {};
7980
7981 let aggregator: Box<dyn BarAggregator> = match aggregation {
7982 BarAggregation::Tick => Box::new(TickBarAggregator::new(
7983 bar_type,
7984 instrument.price_precision(),
7985 instrument.size_precision(),
7986 handler,
7987 )),
7988 BarAggregation::TickImbalance => Box::new(TickImbalanceBarAggregator::new(
7989 bar_type,
7990 instrument.price_precision(),
7991 instrument.size_precision(),
7992 handler,
7993 )),
7994 BarAggregation::TickRuns => Box::new(TickRunsBarAggregator::new(
7995 bar_type,
7996 instrument.price_precision(),
7997 instrument.size_precision(),
7998 handler,
7999 )),
8000 BarAggregation::Volume => Box::new(VolumeBarAggregator::new(
8001 bar_type,
8002 instrument.price_precision(),
8003 instrument.size_precision(),
8004 handler,
8005 )),
8006 BarAggregation::VolumeImbalance => Box::new(VolumeImbalanceBarAggregator::new(
8007 bar_type,
8008 instrument.price_precision(),
8009 instrument.size_precision(),
8010 handler,
8011 )),
8012 BarAggregation::VolumeRuns => Box::new(VolumeRunsBarAggregator::new(
8013 bar_type,
8014 instrument.price_precision(),
8015 instrument.size_precision(),
8016 handler,
8017 )),
8018 BarAggregation::Value => Box::new(ValueBarAggregator::new(
8019 bar_type,
8020 instrument.price_precision(),
8021 instrument.size_precision(),
8022 handler,
8023 )),
8024 BarAggregation::ValueImbalance => Box::new(ValueImbalanceBarAggregator::new(
8025 bar_type,
8026 instrument.price_precision(),
8027 instrument.size_precision(),
8028 handler,
8029 )),
8030 BarAggregation::ValueRuns => Box::new(ValueRunsBarAggregator::new(
8031 bar_type,
8032 instrument.price_precision(),
8033 instrument.size_precision(),
8034 handler,
8035 )),
8036 BarAggregation::Renko => Box::new(RenkoBarAggregator::new(
8037 bar_type,
8038 instrument.price_precision(),
8039 instrument.size_precision(),
8040 Price::from("0.01"),
8041 handler,
8042 )),
8043 _ => unreachable!(),
8044 };
8045
8046 assert!(aggregator.bar_type().is_standard());
8047 assert_eq!(aggregator.bar_type(), bar_type.standard());
8048 }
8049
8050 #[rstest]
8051 fn test_composite_tick_bar_aggregator_emits_standard_bar_type(equity_aapl: Equity) {
8052 let instrument = InstrumentAny::Equity(equity_aapl);
8053 let bar_type = BarType::new_composite(
8054 instrument.id(),
8055 BarSpecification::new(1, BarAggregation::Tick, PriceType::Last),
8056 AggregationSource::Internal,
8057 1,
8058 BarAggregation::Minute,
8059 AggregationSource::External,
8060 );
8061 let handler = Arc::new(Mutex::new(Vec::new()));
8062 let handler_clone = Arc::clone(&handler);
8063
8064 let mut aggregator = TickBarAggregator::new(
8065 bar_type,
8066 instrument.price_precision(),
8067 instrument.size_precision(),
8068 move |bar: Bar| {
8069 let mut handler_guard = handler_clone.lock();
8070 handler_guard.push(bar);
8071 },
8072 );
8073
8074 let input_bar = Bar::new(
8075 bar_type.composite(),
8076 Price::from("100.00"),
8077 Price::from("101.00"),
8078 Price::from("99.00"),
8079 Price::from("100.50"),
8080 Quantity::from(10),
8081 UnixNanos::from(1_000),
8082 UnixNanos::from(1_000),
8083 );
8084 aggregator.handle_bar(input_bar);
8085
8086 let handler_guard = handler.lock();
8087 assert_eq!(handler_guard.len(), 1);
8088 assert_eq!(handler_guard[0].bar_type, bar_type.standard());
8089 }
8090
8091 #[rstest]
8092 fn test_composite_time_bar_aggregator_uses_standard_timer_name(equity_aapl: Equity) {
8093 let instrument = InstrumentAny::Equity(equity_aapl);
8094 let bar_type = BarType::new_composite(
8095 instrument.id(),
8096 BarSpecification::new(5, BarAggregation::Minute, PriceType::Last),
8097 AggregationSource::Internal,
8098 1,
8099 BarAggregation::Minute,
8100 AggregationSource::External,
8101 );
8102 let clock = Rc::new(RefCell::new(TestClock::new()));
8103
8104 let aggregator = TimeBarAggregator::new(
8105 bar_type,
8106 instrument.price_precision(),
8107 instrument.size_precision(),
8108 clock.clone(),
8109 |_: Bar| {},
8110 false,
8111 true,
8112 BarIntervalType::LeftOpen,
8113 None,
8114 0,
8115 false,
8116 );
8117
8118 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
8119 let rc = Rc::new(RefCell::new(boxed));
8120 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
8121
8122 let expected = format!("TIME_BAR_{}", bar_type.standard());
8123 assert!(
8124 clock.borrow().timer_names().contains(&expected.as_str()),
8125 "timer names {:?} should contain {expected}",
8126 clock.borrow().timer_names(),
8127 );
8128 }
8129}
8130
8131#[cfg(test)]
8132mod property_tests {
8133 use std::{cell::RefCell, rc::Rc, sync::Arc};
8134
8135 use nautilus_common::{clock::TestClock, timer::TimeEvent};
8136 use nautilus_core::{UUID4, UnixNanos};
8137 use nautilus_model::{
8138 data::{Bar, BarSpecification, BarType, TradeTick, bar::get_bar_interval_ns},
8139 enums::{AggregationSource, AggressorSide, BarAggregation, BarIntervalType, PriceType},
8140 instruments::{Instrument, InstrumentAny, stubs::equity_aapl},
8141 types::{Price, Quantity},
8142 };
8143 use parking_lot::Mutex;
8144 use proptest::prelude::*;
8145 use rstest::rstest;
8146 use ustr::Ustr;
8147
8148 use super::*;
8149
8150 fn time_bar_spec_strategy() -> impl Strategy<Value = (BarAggregation, usize)> {
8151 prop_oneof![
8152 (Just(BarAggregation::Second), 1usize..=5),
8153 (Just(BarAggregation::Minute), 1usize..=5),
8154 (Just(BarAggregation::Hour), 1usize..=4),
8155 ]
8156 }
8157
8158 fn interval_type_strategy() -> impl Strategy<Value = BarIntervalType> {
8159 prop_oneof![
8160 Just(BarIntervalType::LeftOpen),
8161 Just(BarIntervalType::RightOpen),
8162 ]
8163 }
8164
8165 proptest! {
8166 #[rstest]
8167 fn prop_skip_first_drops_partial_then_emits(
8168 (aggregation, step) in time_bar_spec_strategy(),
8169 interval_type in interval_type_strategy(),
8170 skip_first in any::<bool>(),
8171 ) {
8172 let instrument = InstrumentAny::Equity(equity_aapl());
8173 let bar_spec = BarSpecification::new(step, aggregation, PriceType::Last);
8174 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8175 let interval_ns = get_bar_interval_ns(&bar_type).as_u64();
8176
8177 let now_ns = interval_ns + interval_ns / 2;
8180
8181 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
8182 let handler_clone = Arc::clone(&handler);
8183 let clock = Rc::new(RefCell::new(TestClock::new()));
8184 clock.borrow_mut().set_time(UnixNanos::from(now_ns));
8185 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
8186
8187 let aggregator = TimeBarAggregator::new(
8188 bar_type,
8189 instrument.price_precision(),
8190 instrument.size_precision(),
8191 clock,
8192 move |bar: Bar| {
8193 let mut h = handler_clone.lock();
8194 h.push(bar);
8195 },
8196 false,
8197 false,
8198 interval_type,
8199 None,
8200 0,
8201 skip_first,
8202 );
8203
8204 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
8205 let rc = Rc::new(RefCell::new(boxed));
8206 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
8207
8208 rc.borrow_mut().update(
8211 Price::from("100.00"),
8212 Quantity::from(1),
8213 UnixNanos::from(now_ns),
8214 );
8215 let first_close = 2 * interval_ns;
8216 rc.borrow_mut().build_bar(&TimeEvent::new(
8217 event_name,
8218 UUID4::new(),
8219 UnixNanos::from(first_close),
8220 UnixNanos::from(first_close),
8221 ));
8222
8223 rc.borrow_mut().update(
8225 Price::from("101.00"),
8226 Quantity::from(1),
8227 UnixNanos::from(first_close + interval_ns / 2),
8228 );
8229 let second_close = first_close + interval_ns;
8230 rc.borrow_mut().build_bar(&TimeEvent::new(
8231 event_name,
8232 UUID4::new(),
8233 UnixNanos::from(second_close),
8234 UnixNanos::from(second_close),
8235 ));
8236
8237 let bars = handler.lock();
8238 let expected = if skip_first { 1 } else { 2 };
8239 prop_assert_eq!(bars.len(), expected);
8240 prop_assert_eq!(bars.last().unwrap().close, Price::from("101.00"));
8241 for bar in bars.iter() {
8242 prop_assert!(bar.high >= bar.open);
8243 prop_assert!(bar.high >= bar.close);
8244 prop_assert!(bar.low <= bar.open);
8245 prop_assert!(bar.low <= bar.close);
8246 }
8247 }
8248
8249 #[rstest]
8250 fn prop_skip_first_noop_on_exact_boundary(
8251 (aggregation, step) in time_bar_spec_strategy(),
8252 interval_type in interval_type_strategy(),
8253 ) {
8254 let instrument = InstrumentAny::Equity(equity_aapl());
8255 let bar_spec = BarSpecification::new(step, aggregation, PriceType::Last);
8256 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8257 let interval_ns = get_bar_interval_ns(&bar_type).as_u64();
8258
8259 let now_ns = interval_ns;
8262 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
8263 let handler_clone = Arc::clone(&handler);
8264 let clock = Rc::new(RefCell::new(TestClock::new()));
8265 clock.borrow_mut().set_time(UnixNanos::from(now_ns));
8266 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
8267
8268 let aggregator = TimeBarAggregator::new(
8269 bar_type,
8270 instrument.price_precision(),
8271 instrument.size_precision(),
8272 clock,
8273 move |bar: Bar| {
8274 let mut h = handler_clone.lock();
8275 h.push(bar);
8276 },
8277 false,
8278 false,
8279 interval_type,
8280 None,
8281 0,
8282 true, );
8284
8285 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
8286 let rc = Rc::new(RefCell::new(boxed));
8287 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
8288
8289 rc.borrow_mut().update(
8290 Price::from("100.00"),
8291 Quantity::from(1),
8292 UnixNanos::from(now_ns),
8293 );
8294 let next_close = now_ns + interval_ns;
8295 rc.borrow_mut().build_bar(&TimeEvent::new(
8296 event_name,
8297 UUID4::new(),
8298 UnixNanos::from(next_close),
8299 UnixNanos::from(next_close),
8300 ));
8301
8302 let bars = handler.lock();
8303 prop_assert_eq!(bars.len(), 1);
8304 prop_assert_eq!(bars[0].close, Price::from("100.00"));
8305 }
8306
8307 #[rstest]
8308 fn prop_bar_builder_ohlc_invariants(
8309 updates in prop::collection::vec((1i64..=100_000i64, 1u64..=1_000u64), 1..=50),
8310 ) {
8311 let instrument = InstrumentAny::Equity(equity_aapl());
8312 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
8313 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8314 let mut builder = BarBuilder::new(bar_type, 2, 0);
8315
8316 let mut total_volume: u64 = 0;
8317
8318 for (i, (price_cents, size)) in updates.iter().enumerate() {
8319 let price = Price::new((*price_cents as f64) / 100.0, 2);
8320 let qty = Quantity::new(*size as f64, 0);
8321 let ts = UnixNanos::from((i as u64 + 1) * 1_000);
8322 total_volume += *size;
8323 builder.update(price, qty, ts);
8324 }
8325
8326 let bar = builder.build_now();
8327 prop_assert!(bar.low <= bar.open);
8328 prop_assert!(bar.low <= bar.close);
8329 prop_assert!(bar.high >= bar.open);
8330 prop_assert!(bar.high >= bar.close);
8331 prop_assert!(bar.low <= bar.high);
8332 prop_assert_eq!(bar.volume.as_f64(), total_volume as f64);
8333 }
8334
8335 #[rstest]
8336 fn prop_tick_bar_aggregator_volume_conservation(
8337 ticks in prop::collection::vec((1i64..=1_000i64, 1u64..=100u64), 3..=60),
8338 step in 1usize..=5,
8339 ) {
8340 let instrument = InstrumentAny::Equity(equity_aapl());
8341 let bar_spec = BarSpecification::new(step, BarAggregation::Tick, PriceType::Last);
8342 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8343 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
8344 let handler_clone = Arc::clone(&handler);
8345
8346 let mut aggregator = TickBarAggregator::new(
8347 bar_type,
8348 instrument.price_precision(),
8349 instrument.size_precision(),
8350 move |bar: Bar| {
8351 handler_clone.lock().push(bar);
8352 },
8353 );
8354
8355 let mut total_input: u64 = 0;
8356
8357 for (i, (price_cents, size)) in ticks.iter().enumerate() {
8358 let price = Price::new((*price_cents as f64) / 100.0, 2);
8359 let qty = Quantity::new(*size as f64, 0);
8360 aggregator.update(price, qty, UnixNanos::from((i as u64 + 1) * 1_000));
8361 total_input += *size;
8362 }
8363
8364 let bars = handler.lock();
8365 let emitted_count = bars.len();
8366 prop_assert_eq!(emitted_count, ticks.len() / step);
8367
8368 let mut sum_emitted: f64 = 0.0;
8369
8370 for bar in bars.iter() {
8371 prop_assert!(bar.low <= bar.open);
8372 prop_assert!(bar.low <= bar.close);
8373 prop_assert!(bar.high >= bar.open);
8374 prop_assert!(bar.high >= bar.close);
8375 sum_emitted += bar.volume.as_f64();
8376 }
8377
8378 let pending_size: u64 = ticks.iter()
8380 .skip(emitted_count * step)
8381 .map(|(_, s)| *s)
8382 .sum();
8383 prop_assert!((sum_emitted + pending_size as f64 - total_input as f64).abs() < 1e-6);
8384 }
8385
8386 #[rstest]
8387 fn prop_volume_bar_aggregator_conservation(
8388 sizes in prop::collection::vec(1u64..=50u64, 3..=40),
8389 step in 2u64..=10u64,
8390 ) {
8391 let instrument = InstrumentAny::Equity(equity_aapl());
8392 let bar_spec = BarSpecification::new(step as usize, BarAggregation::Volume, PriceType::Last);
8393 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8394 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
8395 let handler_clone = Arc::clone(&handler);
8396
8397 let mut aggregator = VolumeBarAggregator::new(
8398 bar_type,
8399 instrument.price_precision(),
8400 instrument.size_precision(),
8401 move |bar: Bar| {
8402 handler_clone.lock().push(bar);
8403 },
8404 );
8405
8406 let mut total_input: u64 = 0;
8407
8408 for (i, size) in sizes.iter().enumerate() {
8409 aggregator.update(
8410 Price::from("100.00"),
8411 Quantity::new(*size as f64, 0),
8412 UnixNanos::from((i as u64 + 1) * 1_000),
8413 );
8414 total_input += *size;
8415 }
8416
8417 let bars = handler.lock();
8418
8419 for bar in bars.iter() {
8421 prop_assert_eq!(bar.volume, Quantity::from(step));
8422 prop_assert!(bar.low <= bar.open);
8423 prop_assert!(bar.low <= bar.close);
8424 prop_assert!(bar.high >= bar.open);
8425 prop_assert!(bar.high >= bar.close);
8426 }
8427
8428 let emitted_total: u64 = bars.len() as u64 * step;
8430 let pending = aggregator.core.builder.volume.as_f64();
8431 prop_assert!((emitted_total as f64 + pending - total_input as f64).abs() < 1e-6);
8432 }
8433
8434 #[rstest]
8435 fn prop_volume_bar_matches_unit_trade_reference(
8436 updates in prop::collection::vec((1i64..=100_000i64, 1u64..=8u64, 0u64..=30u64), 1..=30),
8437 step in 1usize..=5,
8438 ) {
8439 let instrument = InstrumentAny::Equity(equity_aapl());
8440 let bar_spec = BarSpecification::new(step, BarAggregation::Volume, PriceType::Last);
8441 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8442 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
8443 let handler_clone = Arc::clone(&handler);
8444 let mut aggregator = VolumeBarAggregator::new(
8445 bar_type,
8446 instrument.price_precision(),
8447 instrument.size_precision(),
8448 move |bar: Bar| {
8449 handler_clone.lock().push(bar);
8450 },
8451 );
8452 let price = |cents| {
8453 Price::from_decimal_dp(Decimal::new(cents, 2), 2)
8454 .expect("bounded cents must produce a valid price")
8455 };
8456 let mut last_timestamp = UnixNanos::default();
8457 let mut pending_units = Vec::new();
8458 let mut expected_bars = Vec::new();
8459
8460 for (price_cents, size, timestamp) in &updates {
8461 let timestamp = UnixNanos::from(*timestamp);
8462 aggregator.update(price(*price_cents), Quantity::from(*size), timestamp);
8463
8464 if timestamp < last_timestamp {
8465 continue;
8466 }
8467
8468 last_timestamp = timestamp;
8469 for _ in 0..*size {
8470 pending_units.push((*price_cents, timestamp));
8471 }
8472
8473 while pending_units.len() >= step {
8474 let units: Vec<_> = pending_units.drain(..step).collect();
8475 let first = units.first().unwrap();
8476 let last = units.last().unwrap();
8477 let low = units.iter().map(|(cents, _)| *cents).min().unwrap();
8478 let high = units.iter().map(|(cents, _)| *cents).max().unwrap();
8479 expected_bars.push((
8480 price(first.0),
8481 price(high),
8482 price(low),
8483 price(last.0),
8484 Quantity::from(step as u64),
8485 last.1,
8486 ));
8487 }
8488 }
8489
8490 let bars = handler.lock();
8491 prop_assert_eq!(bars.len(), expected_bars.len());
8492 for (actual, (open, high, low, close, volume, timestamp))
8493 in bars.iter().zip(expected_bars)
8494 {
8495 prop_assert_eq!(actual.open, open);
8496 prop_assert_eq!(actual.high, high);
8497 prop_assert_eq!(actual.low, low);
8498 prop_assert_eq!(actual.close, close);
8499 prop_assert_eq!(actual.volume, volume);
8500 prop_assert_eq!(actual.ts_event, timestamp);
8501 prop_assert_eq!(actual.ts_init, timestamp);
8502 }
8503
8504 prop_assert_eq!(aggregator.core.builder.volume, Quantity::from(pending_units.len() as u64));
8505 prop_assert_eq!(aggregator.core.builder.ts_last, last_timestamp);
8506
8507 if let Some((first, rest)) = pending_units.split_first() {
8508 let last = rest.last().unwrap_or(first);
8509 let low = pending_units.iter().map(|(cents, _)| *cents).min().unwrap();
8510 let high = pending_units.iter().map(|(cents, _)| *cents).max().unwrap();
8511 prop_assert_eq!(aggregator.core.builder.open, Some(price(first.0)));
8512 prop_assert_eq!(aggregator.core.builder.high, Some(price(high)));
8513 prop_assert_eq!(aggregator.core.builder.low, Some(price(low)));
8514 prop_assert_eq!(aggregator.core.builder.close, Some(price(last.0)));
8515 } else {
8516 prop_assert_eq!(aggregator.core.builder.open, None);
8517 prop_assert_eq!(aggregator.core.builder.high, None);
8518 prop_assert_eq!(aggregator.core.builder.low, None);
8519 prop_assert_eq!(aggregator.core.builder.close, None);
8520 }
8521 }
8522
8523 #[rstest]
8524 fn prop_bar_builder_spread_adjustment_is_additive(
8525 updates in prop::collection::vec((10_000i64..=100_000i64, 1u64..=100u64), 1..=20),
8526 spread_cents in -10_000i64..=10_000i64,
8527 backward in any::<bool>(),
8528 ) {
8529 let instrument = InstrumentAny::Equity(equity_aapl());
8530 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
8531 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8532 let mut builder = BarBuilder::new(bar_type, 2, 0);
8533
8534 let spread = Decimal::new(spread_cents, 2);
8535 let mode = if backward {
8536 ContinuousFutureAdjustmentType::BackwardSpread
8537 } else {
8538 ContinuousFutureAdjustmentType::ForwardSpread
8539 };
8540 builder.set_adjustment(spread, mode);
8541
8542 let mut min_cents = i64::MAX;
8543 let mut max_cents = i64::MIN;
8544
8545 for (i, (price_cents, size)) in updates.iter().enumerate() {
8546 if *price_cents < min_cents {
8547 min_cents = *price_cents;
8548 }
8549
8550 if *price_cents > max_cents {
8551 max_cents = *price_cents;
8552 }
8553
8554 builder.update(
8555 Price::new((*price_cents as f64) / 100.0, 2),
8556 Quantity::new(*size as f64, 0),
8557 UnixNanos::from((i as u64 + 1) * 1_000),
8558 );
8559 }
8560
8561 let bar = builder.build_now();
8562 let first_decimal = Decimal::new(updates.first().unwrap().0, 2);
8563 let last_decimal = Decimal::new(updates.last().unwrap().0, 2);
8564 let min_decimal = Decimal::new(min_cents, 2);
8565 let max_decimal = Decimal::new(max_cents, 2);
8566
8567 prop_assert_eq!(bar.open.as_decimal(), first_decimal + spread);
8568 prop_assert_eq!(bar.close.as_decimal(), last_decimal + spread);
8569 prop_assert_eq!(bar.low.as_decimal(), min_decimal + spread);
8570 prop_assert_eq!(bar.high.as_decimal(), max_decimal + spread);
8571 }
8572
8573 #[rstest]
8574 fn prop_bar_builder_inactive_adjustment_is_identity(
8575 updates in prop::collection::vec((1i64..=100_000i64, 1u64..=1_000u64), 1..=20),
8576 use_ratio in any::<bool>(),
8577 ) {
8578 let instrument = InstrumentAny::Equity(equity_aapl());
8579 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
8580 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8581
8582 let mut adjusted = BarBuilder::new(bar_type, 2, 0);
8583 let mut baseline = BarBuilder::new(bar_type, 2, 0);
8584
8585 let (input, mode) = if use_ratio {
8587 (Decimal::ONE, ContinuousFutureAdjustmentType::BackwardRatio)
8588 } else {
8589 (Decimal::ZERO, ContinuousFutureAdjustmentType::BackwardSpread)
8590 };
8591 adjusted.set_adjustment(input, mode);
8592
8593 for (i, (price_cents, size)) in updates.iter().enumerate() {
8594 let price = Price::new((*price_cents as f64) / 100.0, 2);
8595 let qty = Quantity::new(*size as f64, 0);
8596 let ts = UnixNanos::from((i as u64 + 1) * 1_000);
8597 adjusted.update(price, qty, ts);
8598 baseline.update(price, qty, ts);
8599 }
8600
8601 let bar_adjusted = adjusted.build_now();
8602 let bar_baseline = baseline.build_now();
8603 prop_assert_eq!(bar_adjusted.open, bar_baseline.open);
8604 prop_assert_eq!(bar_adjusted.high, bar_baseline.high);
8605 prop_assert_eq!(bar_adjusted.low, bar_baseline.low);
8606 prop_assert_eq!(bar_adjusted.close, bar_baseline.close);
8607 prop_assert_eq!(bar_adjusted.volume, bar_baseline.volume);
8608 }
8609
8610 #[rstest]
8611 fn prop_bar_builder_spread_preserves_raw_arithmetic(
8612 updates in prop::collection::vec((10_000i64..=100_000i64, 1u64..=100u64), 1..=20),
8613 spread_micro in -10_000i64..=10_000i64,
8616 ) {
8617 let instrument = InstrumentAny::Equity(equity_aapl());
8618 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
8619 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8620 let mut builder = BarBuilder::new(bar_type, 2, 0);
8621
8622 let spread = Decimal::new(spread_micro, 4);
8623 builder.set_adjustment(spread, ContinuousFutureAdjustmentType::BackwardSpread);
8624
8625 let adjustment_raw_i128 = mantissa_exponent_to_fixed_i128(
8626 spread.mantissa(),
8627 -(spread.scale() as i8),
8628 FIXED_PRECISION,
8629 )
8630 .expect("scale within range");
8631 #[allow(
8632 clippy::useless_conversion,
8633 reason = "i128 to PriceRaw is real when not high-precision"
8634 )]
8635 let expected_adjustment_raw: PriceRaw =
8636 adjustment_raw_i128.try_into().expect("within PriceRaw range");
8637
8638 let mut min_cents = i64::MAX;
8639 let mut max_cents = i64::MIN;
8640 let mut last_price = Price::new(0.0, 2);
8641 let mut first_price = Price::new(0.0, 2);
8642
8643 for (i, (price_cents, size)) in updates.iter().enumerate() {
8644 if *price_cents < min_cents {
8645 min_cents = *price_cents;
8646 }
8647
8648 if *price_cents > max_cents {
8649 max_cents = *price_cents;
8650 }
8651
8652 let price = Price::new((*price_cents as f64) / 100.0, 2);
8653
8654 if i == 0 {
8655 first_price = price;
8656 }
8657
8658 last_price = price;
8659 builder.update(
8660 price,
8661 Quantity::new(*size as f64, 0),
8662 UnixNanos::from((i as u64 + 1) * 1_000),
8663 );
8664 }
8665
8666 let bar = builder.build_now();
8667 let min_price = Price::new((min_cents as f64) / 100.0, 2);
8668 let max_price = Price::new((max_cents as f64) / 100.0, 2);
8669 prop_assert_eq!(bar.open.raw, first_price.raw + expected_adjustment_raw);
8670 prop_assert_eq!(bar.close.raw, last_price.raw + expected_adjustment_raw);
8671 prop_assert_eq!(bar.low.raw, min_price.raw + expected_adjustment_raw);
8672 prop_assert_eq!(bar.high.raw, max_price.raw + expected_adjustment_raw);
8673 prop_assert_eq!(bar.open.precision, 2);
8674 prop_assert_eq!(bar.high.precision, 2);
8675 prop_assert_eq!(bar.low.precision, 2);
8676 prop_assert_eq!(bar.close.precision, 2);
8677 }
8678
8679 #[rstest]
8680 fn prop_bar_builder_active_ratio_scales_each_ohlc(
8681 updates in prop::collection::vec((1_000i64..=100_000i64, 1u64..=100u64), 1..=20),
8682 ratio_centi in prop_oneof![50i64..=99i64, 101i64..=200i64],
8684 backward in any::<bool>(),
8685 ) {
8686 let instrument = InstrumentAny::Equity(equity_aapl());
8687 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
8688 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8689 let mut builder = BarBuilder::new(bar_type, 2, 0);
8690
8691 let ratio_decimal = Decimal::new(ratio_centi, 2);
8692 let ratio_f64 = (ratio_centi as f64) / 100.0;
8693 let mode = if backward {
8694 ContinuousFutureAdjustmentType::BackwardRatio
8695 } else {
8696 ContinuousFutureAdjustmentType::ForwardRatio
8697 };
8698 builder.set_adjustment(ratio_decimal, mode);
8699
8700 let mut min_cents = i64::MAX;
8701 let mut max_cents = i64::MIN;
8702 let mut first_cents = 0i64;
8703 let mut last_cents = 0i64;
8704
8705 for (i, (price_cents, size)) in updates.iter().enumerate() {
8706 if *price_cents < min_cents {
8707 min_cents = *price_cents;
8708 }
8709
8710 if *price_cents > max_cents {
8711 max_cents = *price_cents;
8712 }
8713
8714 if i == 0 {
8715 first_cents = *price_cents;
8716 }
8717
8718 last_cents = *price_cents;
8719 builder.update(
8720 Price::new((*price_cents as f64) / 100.0, 2),
8721 Quantity::new(*size as f64, 0),
8722 UnixNanos::from((i as u64 + 1) * 1_000),
8723 );
8724 }
8725
8726 let bar = builder.build_now();
8727 let expect = |cents: i64| Price::new((cents as f64) / 100.0 * ratio_f64, 2);
8729 prop_assert_eq!(bar.open, expect(first_cents));
8730 prop_assert_eq!(bar.close, expect(last_cents));
8731 prop_assert_eq!(bar.low, expect(min_cents));
8733 prop_assert_eq!(bar.high, expect(max_cents));
8734 }
8735
8736 #[rstest]
8737 fn prop_bar_builder_spread_mode_direction_is_metadata_only(
8738 updates in prop::collection::vec((10_000i64..=100_000i64, 1u64..=100u64), 1..=20),
8739 spread_cents in -10_000i64..=10_000i64,
8740 ) {
8741 let instrument = InstrumentAny::Equity(equity_aapl());
8742 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
8743 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8744
8745 let spread = Decimal::new(spread_cents, 2);
8746 let mut backward = BarBuilder::new(bar_type, 2, 0);
8747 let mut forward = BarBuilder::new(bar_type, 2, 0);
8748 backward.set_adjustment(spread, ContinuousFutureAdjustmentType::BackwardSpread);
8749 forward.set_adjustment(spread, ContinuousFutureAdjustmentType::ForwardSpread);
8750
8751 for (i, (price_cents, size)) in updates.iter().enumerate() {
8752 let price = Price::new((*price_cents as f64) / 100.0, 2);
8753 let qty = Quantity::new(*size as f64, 0);
8754 let ts = UnixNanos::from((i as u64 + 1) * 1_000);
8755 backward.update(price, qty, ts);
8756 forward.update(price, qty, ts);
8757 }
8758
8759 let bar_backward = backward.build_now();
8760 let bar_forward = forward.build_now();
8761 prop_assert_eq!(bar_backward.open, bar_forward.open);
8762 prop_assert_eq!(bar_backward.high, bar_forward.high);
8763 prop_assert_eq!(bar_backward.low, bar_forward.low);
8764 prop_assert_eq!(bar_backward.close, bar_forward.close);
8765 }
8766
8767 #[rstest]
8768 fn prop_value_bar_aggregator_ohlc_invariants(
8769 ticks in prop::collection::vec((50i64..=500i64, 1u64..=20u64), 2..=30),
8770 step in 100u64..=2_000u64,
8771 ) {
8772 let instrument = InstrumentAny::Equity(equity_aapl());
8773 let bar_spec = BarSpecification::new(step as usize, BarAggregation::Value, PriceType::Last);
8774 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8775 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
8776 let handler_clone = Arc::clone(&handler);
8777
8778 let mut aggregator = ValueBarAggregator::new(
8779 bar_type,
8780 instrument.price_precision(),
8781 instrument.size_precision(),
8782 move |bar: Bar| {
8783 handler_clone.lock().push(bar);
8784 },
8785 );
8786
8787 for (i, (price_cents, size)) in ticks.iter().enumerate() {
8788 aggregator.update(
8789 Price::new((*price_cents as f64) / 100.0, 2),
8790 Quantity::new(*size as f64, 0),
8791 UnixNanos::from((i as u64 + 1) * 1_000),
8792 );
8793 }
8794
8795 let bars = handler.lock();
8796 for bar in bars.iter() {
8797 prop_assert!(bar.low <= bar.open);
8798 prop_assert!(bar.low <= bar.close);
8799 prop_assert!(bar.high >= bar.open);
8800 prop_assert!(bar.high >= bar.close);
8801 prop_assert!(bar.volume.as_f64() > 0.0);
8802 }
8803 }
8804
8805 #[rstest]
8806 fn prop_renko_brick_chain(
8807 moves in prop::collection::vec(-500i64..=500i64, 1..=60),
8808 step in 1usize..=10,
8809 ) {
8810 let instrument = InstrumentAny::Equity(equity_aapl());
8811 let bar_spec = BarSpecification::new(step, BarAggregation::Renko, PriceType::Last);
8812 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8813 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
8814 let handler_clone = Arc::clone(&handler);
8815
8816 let price_increment = Price::from("0.01");
8817 let mut aggregator = RenkoBarAggregator::new(
8818 bar_type,
8819 2,
8820 0,
8821 price_increment,
8822 move |bar: Bar| {
8823 handler_clone.lock().push(bar);
8824 },
8825 );
8826 let brick_size = aggregator.brick_size;
8827
8828 let base_raw = Price::from("1000.00").raw;
8829 let mut cum_increments: i64 = 0;
8830 let mut first_price: Option<Price> = None;
8831
8832 for (i, delta) in moves.iter().enumerate() {
8833 cum_increments += delta;
8834 let price = Price::from_raw(
8835 base_raw + PriceRaw::from(cum_increments) * price_increment.raw,
8836 2,
8837 );
8838
8839 if first_price.is_none() {
8840 first_price = Some(price);
8841 }
8842
8843 aggregator.update(price, Quantity::from(1), UnixNanos::from((i as u64 + 1) * 1_000));
8844 }
8845
8846 let bars = handler.lock();
8847 let mut expected_open = first_price.unwrap();
8848
8849 for bar in bars.iter() {
8850 prop_assert_eq!(bar.open, expected_open);
8852 prop_assert_eq!((bar.close.raw - bar.open.raw).abs(), brick_size);
8854 prop_assert_eq!(bar.high, bar.open.max(bar.close));
8856 prop_assert_eq!(bar.low, bar.open.min(bar.close));
8857 expected_open = bar.close;
8858 }
8859 }
8860
8861 #[rstest]
8862 fn prop_volume_imbalance_one_sided_conservation(
8863 sizes in prop::collection::vec(1u64..=50u64, 1..=40),
8864 step in 2u64..=10u64,
8865 buyer in any::<bool>(),
8866 ) {
8867 let instrument = InstrumentAny::Equity(equity_aapl());
8868 let bar_spec = BarSpecification::new(
8869 step as usize,
8870 BarAggregation::VolumeImbalance,
8871 PriceType::Last,
8872 );
8873 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8874 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
8875 let handler_clone = Arc::clone(&handler);
8876
8877 let mut aggregator = VolumeImbalanceBarAggregator::new(
8878 bar_type,
8879 instrument.price_precision(),
8880 instrument.size_precision(),
8881 move |bar: Bar| {
8882 handler_clone.lock().push(bar);
8883 },
8884 );
8885
8886 let side = if buyer { AggressorSide::Buy } else { AggressorSide::Sell };
8887 let mut total_input: u64 = 0;
8888
8889 for (i, size) in sizes.iter().enumerate() {
8890 let trade = TradeTick {
8891 instrument_id: instrument.id(),
8892 price: Price::from("100.00"),
8893 size: Quantity::from(*size),
8894 aggressor_side: side,
8895 ts_event: UnixNanos::from((i as u64 + 1) * 1_000),
8896 ts_init: UnixNanos::from((i as u64 + 1) * 1_000),
8897 ..TradeTick::default()
8898 };
8899 aggregator.handle_trade(trade);
8900 total_input += *size;
8901 }
8902
8903 let bars = handler.lock();
8904
8905 for bar in bars.iter() {
8907 prop_assert_eq!(bar.volume, Quantity::from(step));
8908 }
8909
8910 let emitted: u64 = bars.len() as u64 * step;
8912 let pending = aggregator.core.builder.volume.as_f64();
8913 prop_assert!((emitted as f64 + pending - total_input as f64).abs() < 1e-9);
8914 }
8915
8916 #[rstest]
8917 fn prop_volume_runs_one_sided_conservation(
8918 sizes in prop::collection::vec(1u64..=50u64, 1..=40),
8919 step in 2u64..=10u64,
8920 buyer in any::<bool>(),
8921 ) {
8922 let instrument = InstrumentAny::Equity(equity_aapl());
8923 let bar_spec = BarSpecification::new(
8924 step as usize,
8925 BarAggregation::VolumeRuns,
8926 PriceType::Last,
8927 );
8928 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8929 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
8930 let handler_clone = Arc::clone(&handler);
8931
8932 let mut aggregator = VolumeRunsBarAggregator::new(
8933 bar_type,
8934 instrument.price_precision(),
8935 instrument.size_precision(),
8936 move |bar: Bar| {
8937 handler_clone.lock().push(bar);
8938 },
8939 );
8940
8941 let side = if buyer { AggressorSide::Buy } else { AggressorSide::Sell };
8942 let mut total_input: u64 = 0;
8943
8944 for (i, size) in sizes.iter().enumerate() {
8945 let trade = TradeTick {
8946 instrument_id: instrument.id(),
8947 price: Price::from("100.00"),
8948 size: Quantity::from(*size),
8949 aggressor_side: side,
8950 ts_event: UnixNanos::from((i as u64 + 1) * 1_000),
8951 ts_init: UnixNanos::from((i as u64 + 1) * 1_000),
8952 ..TradeTick::default()
8953 };
8954 aggregator.handle_trade(trade);
8955 total_input += *size;
8956 }
8957
8958 let bars = handler.lock();
8959
8960 for bar in bars.iter() {
8962 prop_assert_eq!(bar.volume, Quantity::from(step));
8963 }
8964
8965 let emitted: u64 = bars.len() as u64 * step;
8966 let pending = aggregator.core.builder.volume.as_f64();
8967 prop_assert!((emitted as f64 + pending - total_input as f64).abs() < 1e-9);
8968 }
8969
8970 #[rstest]
8971 fn prop_value_bar_cum_value_stays_below_step(
8972 ticks in prop::collection::vec((50i64..=500i64, 1u64..=20u64), 1..=30),
8973 step in 100u64..=2_000u64,
8974 ) {
8975 let instrument = InstrumentAny::Equity(equity_aapl());
8976 let bar_spec = BarSpecification::new(step as usize, BarAggregation::Value, PriceType::Last);
8977 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8978 let step_decimal = Decimal::from(step);
8979
8980 let mut aggregator = ValueBarAggregator::new(
8981 bar_type,
8982 instrument.price_precision(),
8983 instrument.size_precision(),
8984 |_: Bar| {},
8985 );
8986
8987 for (i, (price_cents, size)) in ticks.iter().enumerate() {
8988 aggregator.update(
8989 Price::new((*price_cents as f64) / 100.0, 2),
8990 Quantity::new(*size as f64, 0),
8991 UnixNanos::from((i as u64 + 1) * 1_000),
8992 );
8993
8994 prop_assert!(aggregator.get_cumulative_value() < step_decimal);
8997 }
8998 }
8999 }
9000}