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