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