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