1use std::{
22 any::Any,
23 cell::RefCell,
24 fmt::Debug,
25 ops::Add,
26 rc::{Rc, Weak},
27};
28
29use ahash::AHashMap;
30use jiff::SignedDuration;
31use nautilus_common::{
32 clock::{Clock, TestClock},
33 timer::{TimeEvent, TimeEventCallback},
34};
35use nautilus_core::{
36 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 self.build_bricks(price, ts_init);
1423 }
1424
1425 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
1426 if self.core.is_stale(ts_init) {
1427 return;
1428 }
1429
1430 self.core.builder.update_bar(bar, volume, ts_init);
1432 self.build_bricks(bar.close, ts_init);
1433 }
1434}
1435
1436impl RenkoBarAggregator {
1437 fn build_bricks(&mut self, price: Price, ts_init: UnixNanos) {
1438 let Some(last_close) = self.last_close else {
1439 self.last_close = Some(price);
1440 return;
1441 };
1442
1443 let rising = price > last_close;
1444
1445 let mut remaining_move = if rising {
1446 price - last_close
1447 } else {
1448 last_close - price
1449 };
1450
1451 if remaining_move < self.brick_size {
1452 return;
1453 }
1454
1455 assert!(
1456 self.brick_size.is_positive(),
1457 "Renko brick size must be positive"
1458 );
1459 let mut current_close = last_close;
1460 let total_volume = self.core.builder.volume;
1461
1462 while remaining_move >= self.brick_size {
1463 let mut brick_close = if rising {
1464 current_close + self.brick_size
1465 } else {
1466 current_close - self.brick_size
1467 };
1468
1469 brick_close.precision = price.precision;
1470
1471 let (brick_high, brick_low) = if rising {
1472 (brick_close, current_close)
1473 } else {
1474 (current_close, brick_close)
1475 };
1476
1477 self.core.builder.reset();
1478 self.core.builder.open = Some(current_close);
1479 self.core.builder.high = Some(brick_high);
1480 self.core.builder.low = Some(brick_low);
1481 self.core.builder.close = Some(brick_close);
1482 self.core.builder.volume = total_volume;
1483 self.core.builder.count = 1;
1484 self.core.builder.ts_last = ts_init;
1485 self.core.builder.initialized = true;
1486 self.core.build_and_send(ts_init, ts_init);
1487
1488 current_close = brick_close;
1489 self.last_close = Some(brick_close);
1490 remaining_move = remaining_move - self.brick_size;
1491 }
1492 }
1493}
1494
1495pub struct TimeBarAggregator {
1499 core: BarAggregatorCore,
1500 clock: Rc<RefCell<dyn Clock>>,
1501 build_with_no_updates: bool,
1502 timestamp_on_close: bool,
1503 is_left_open: bool,
1504 stored_open_ns: UnixNanos,
1505 timer_name: String,
1506 interval_ns: DurationNanos,
1507 next_close_ns: UnixNanos,
1508 first_close_ns: UnixNanos,
1509 bar_build_delay: u64,
1510 time_bars_origin_offset: Option<SignedDuration>,
1511 skip_first_non_full_bar: bool,
1512 pub historical_mode: bool,
1513 historical_events: Vec<TimeEvent>,
1514 historical_event_at_ts_init: Option<TimeEvent>,
1515 aggregator_weak: Option<Weak<RefCell<Box<dyn BarAggregator>>>>,
1516}
1517
1518impl Debug for TimeBarAggregator {
1519 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1520 f.debug_struct(stringify!(TimeBarAggregator))
1521 .field("core", &self.core)
1522 .field("build_with_no_updates", &self.build_with_no_updates)
1523 .field("timestamp_on_close", &self.timestamp_on_close)
1524 .field("is_left_open", &self.is_left_open)
1525 .field("timer_name", &self.timer_name)
1526 .field("interval_ns", &self.interval_ns)
1527 .field("bar_build_delay", &self.bar_build_delay)
1528 .field("skip_first_non_full_bar", &self.skip_first_non_full_bar)
1529 .finish()
1530 }
1531}
1532
1533impl TimeBarAggregator {
1534 #[expect(clippy::too_many_arguments)]
1540 pub fn new<H: FnMut(Bar) + 'static>(
1541 bar_type: BarType,
1542 price_precision: u8,
1543 size_precision: u8,
1544 clock: Rc<RefCell<dyn Clock>>,
1545 handler: H,
1546 build_with_no_updates: bool,
1547 timestamp_on_close: bool,
1548 interval_type: BarIntervalType,
1549 time_bars_origin_offset: Option<SignedDuration>,
1550 bar_build_delay: u64,
1551 skip_first_non_full_bar: bool,
1552 ) -> Self {
1553 let is_left_open = match interval_type {
1554 BarIntervalType::LeftOpen => true,
1555 BarIntervalType::RightOpen => false,
1556 };
1557
1558 let core = BarAggregatorCore::new(bar_type, price_precision, size_precision, handler);
1559
1560 Self {
1561 clock,
1562 build_with_no_updates,
1563 timestamp_on_close,
1564 is_left_open,
1565 stored_open_ns: UnixNanos::default(),
1566 timer_name: format!("TIME_BAR_{}", core.builder.bar_type),
1567 interval_ns: get_bar_interval_ns(&bar_type),
1568 core,
1569 next_close_ns: UnixNanos::default(),
1570 first_close_ns: UnixNanos::default(),
1571 bar_build_delay,
1572 time_bars_origin_offset,
1573 skip_first_non_full_bar,
1574 historical_mode: false,
1575 historical_events: Vec::new(),
1576 historical_event_at_ts_init: None,
1577 aggregator_weak: None,
1578 }
1579 }
1580
1581 pub fn set_clock_internal(&mut self, clock: Rc<RefCell<dyn Clock>>) {
1583 self.clock = clock;
1584 }
1585
1586 pub fn start_timer_internal(
1594 &mut self,
1595 aggregator_rc: Option<Rc<RefCell<Box<dyn BarAggregator>>>>,
1596 ) {
1597 let aggregator_weak = if let Some(rc) = aggregator_rc {
1599 let weak = Rc::downgrade(&rc);
1601 self.aggregator_weak = Some(weak.clone());
1602 weak
1603 } else {
1604 self.aggregator_weak
1606 .as_ref()
1607 .expect("Aggregator weak reference must be set before calling start_timer()")
1608 .clone()
1609 };
1610
1611 let callback = TimeEventCallback::RustLocal(Rc::new(move |event: TimeEvent| {
1612 if let Some(agg) = aggregator_weak.upgrade() {
1613 agg.borrow_mut().build_bar(&event);
1614 }
1615 }));
1616
1617 let now = self.clock.borrow().utc_now();
1619 let mut start_time =
1620 get_time_bar_start(now, &self.bar_type(), self.time_bars_origin_offset);
1621 start_time += SignedDuration::from_micros(self.bar_build_delay as i64);
1622
1623 let fire_immediately = start_time == now;
1625
1626 let spec = &self.bar_type().spec();
1627 let start_time_ns = UnixNanos::from(start_time);
1628 let step = spec.step.get() as u32;
1629
1630 if spec.aggregation != BarAggregation::Month && spec.aggregation != BarAggregation::Year {
1631 self.clock
1632 .borrow_mut()
1633 .set_timer_ns(
1634 &self.timer_name,
1635 self.interval_ns,
1636 Some(start_time_ns),
1637 None,
1638 Some(callback),
1639 Some(true), Some(fire_immediately),
1641 )
1642 .expect(FAILED);
1643
1644 if fire_immediately {
1645 self.next_close_ns = start_time_ns;
1646 } else {
1647 let interval_duration = SignedDuration::from(self.interval_ns);
1648 self.next_close_ns = UnixNanos::from(start_time + interval_duration);
1649 }
1650
1651 self.stored_open_ns = self.next_close_ns.saturating_sub(self.interval_ns);
1652 } else {
1653 let alert_time = if fire_immediately {
1655 start_time
1656 } else if spec.aggregation == BarAggregation::Month {
1657 add_n_months(start_time, step).expect(FAILED)
1658 } else {
1659 add_n_years(start_time, step).expect(FAILED)
1660 };
1661
1662 self.clock
1663 .borrow_mut()
1664 .set_time_alert_ns(
1665 &self.timer_name,
1666 UnixNanos::from(alert_time),
1667 Some(callback),
1668 Some(true), )
1670 .expect(FAILED);
1671
1672 self.next_close_ns = UnixNanos::from(alert_time);
1673 self.stored_open_ns = if fire_immediately {
1676 if spec.aggregation == BarAggregation::Month {
1677 subtract_n_months_nanos(start_time_ns, step).expect(FAILED)
1678 } else {
1679 subtract_n_years_nanos(start_time_ns, step).expect(FAILED)
1680 }
1681 } else {
1682 start_time_ns
1683 };
1684 }
1685
1686 if self.skip_first_non_full_bar {
1687 self.first_close_ns = self.next_close_ns;
1688 }
1689
1690 log::debug!(
1691 "Started timer {}, start_time={:?}, historical_mode={}, fire_immediately={}, now={:?}, bar_build_delay={}",
1692 self.timer_name,
1693 start_time,
1694 self.historical_mode,
1695 fire_immediately,
1696 now,
1697 self.bar_build_delay
1698 );
1699 }
1700
1701 pub fn stop(&mut self) {
1703 self.clock.borrow_mut().cancel_timer(&self.timer_name);
1704 }
1705
1706 fn build_and_send(&mut self, ts_event: UnixNanos, ts_init: UnixNanos) {
1707 if self.skip_first_non_full_bar && ts_init <= self.first_close_ns {
1708 self.core.builder.reset();
1709 } else {
1710 self.skip_first_non_full_bar = false;
1713 self.core.build_and_send(ts_event, ts_init);
1714 }
1715 }
1716
1717 fn build_bar(&mut self, event: &TimeEvent) {
1718 if !self.core.builder.initialized {
1719 return;
1720 }
1721
1722 if !self.build_with_no_updates && self.core.builder.count == 0 {
1723 return; }
1725
1726 let ts_init = event.ts_event;
1727 let ts_event = if self.is_left_open {
1728 if self.timestamp_on_close {
1729 event.ts_event
1730 } else {
1731 self.stored_open_ns
1732 }
1733 } else {
1734 self.stored_open_ns
1735 };
1736
1737 self.build_and_send(ts_event, ts_init);
1738
1739 self.stored_open_ns = event.ts_event;
1741
1742 if self.bar_type().spec().aggregation == BarAggregation::Month {
1743 let step = self.bar_type().spec().step.get() as u32;
1744 let alert_time_ns = add_n_months_nanos(event.ts_event, step).expect(FAILED);
1745
1746 self.clock
1747 .borrow_mut()
1748 .set_time_alert_ns(&self.timer_name, alert_time_ns, None, None)
1749 .expect(FAILED);
1750
1751 self.next_close_ns = alert_time_ns;
1752 } else if self.bar_type().spec().aggregation == BarAggregation::Year {
1753 let step = self.bar_type().spec().step.get() as u32;
1754 let alert_time_ns = add_n_years_nanos(event.ts_event, step).expect(FAILED);
1755
1756 self.clock
1757 .borrow_mut()
1758 .set_time_alert_ns(&self.timer_name, alert_time_ns, None, None)
1759 .expect(FAILED);
1760
1761 self.next_close_ns = alert_time_ns;
1762 } else {
1763 self.next_close_ns = self
1765 .clock
1766 .borrow()
1767 .next_time_ns(&self.timer_name)
1768 .unwrap_or_default();
1769 }
1770 }
1771
1772 fn preprocess_historical_events(&mut self, ts_init: UnixNanos) {
1773 if self.clock.borrow().timestamp_ns() == UnixNanos::default() {
1774 {
1776 let mut clock_borrow = self.clock.borrow_mut();
1777 let test_clock = clock_borrow
1778 .as_any_mut()
1779 .downcast_mut::<TestClock>()
1780 .expect("Expected TestClock in historical mode");
1781 test_clock.set_time(ts_init);
1782 }
1783 self.start_timer_internal(None);
1785 }
1786
1787 let events = {
1789 let mut clock_borrow = self.clock.borrow_mut();
1790 let test_clock = clock_borrow
1791 .as_any_mut()
1792 .downcast_mut::<TestClock>()
1793 .expect("Expected TestClock in historical mode");
1794 test_clock.advance_time(ts_init, true)
1795 };
1796
1797 for event in events {
1798 if event.ts_event == ts_init {
1799 self.historical_event_at_ts_init = Some(event);
1800 } else {
1801 self.build_bar(&event);
1802 }
1803 }
1804 }
1805
1806 fn postprocess_historical_events(&mut self, _ts_init: UnixNanos) {
1807 if let Some(ref event) = self.historical_event_at_ts_init.take() {
1808 self.build_bar(event);
1809 }
1810 }
1811
1812 pub fn set_historical_events_internal(&mut self, events: Vec<TimeEvent>) {
1814 self.historical_events = events;
1815 }
1816}
1817
1818impl BarAggregator for TimeBarAggregator {
1819 fn bar_type(&self) -> BarType {
1820 self.core.builder.bar_type
1821 }
1822
1823 fn is_running(&self) -> bool {
1824 self.core.is_running
1825 }
1826
1827 fn set_is_running(&mut self, value: bool) {
1828 self.core.set_is_running(value);
1829 }
1830
1831 fn stop(&mut self) {
1833 Self::stop(self);
1834 }
1835
1836 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
1837 if self.historical_mode {
1838 self.preprocess_historical_events(ts_init);
1839 }
1840
1841 self.core.builder.update(price, size, ts_init);
1842
1843 if self.historical_mode {
1844 self.postprocess_historical_events(ts_init);
1845 }
1846 }
1847
1848 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
1849 if self.historical_mode {
1850 self.preprocess_historical_events(ts_init);
1851 }
1852
1853 self.core.builder.update_bar(bar, volume, ts_init);
1854
1855 if self.historical_mode {
1856 self.postprocess_historical_events(ts_init);
1857 }
1858 }
1859
1860 fn set_historical_mode(&mut self, historical_mode: bool, handler: Box<dyn FnMut(Bar)>) {
1861 self.historical_mode = historical_mode;
1862 self.core.handler = handler;
1863 }
1864
1865 fn set_historical_events(&mut self, events: Vec<TimeEvent>) {
1866 self.set_historical_events_internal(events);
1867 }
1868
1869 fn set_clock(&mut self, clock: Rc<RefCell<dyn Clock>>) {
1870 self.set_clock_internal(clock);
1871 }
1872
1873 fn build_bar(&mut self, event: &TimeEvent) {
1874 {
1877 #[expect(clippy::use_self)]
1878 TimeBarAggregator::build_bar(self, event);
1879 }
1880 }
1881
1882 fn set_aggregator_weak(&mut self, weak: Weak<RefCell<Box<dyn BarAggregator>>>) {
1883 self.aggregator_weak = Some(weak);
1884 }
1885
1886 fn start_timer(&mut self, aggregator_rc: Option<Rc<RefCell<Box<dyn BarAggregator>>>>) {
1887 self.start_timer_internal(aggregator_rc);
1888 }
1889
1890 fn set_adjustment(&mut self, adjustment: Decimal, mode: ContinuousFutureAdjustmentType) {
1891 self.core.set_adjustment(adjustment, mode);
1892 }
1893
1894 fn set_build_with_no_updates(&mut self, value: bool) {
1895 self.build_with_no_updates = value;
1896 }
1897
1898 fn is_historical(&self) -> bool {
1899 self.historical_mode
1900 }
1901}
1902
1903fn is_below_min_size_decimal(size: Decimal, precision: u8) -> bool {
1904 quantity_from_decimal(size, precision).is_zero()
1905}
1906
1907fn min_size_decimal(precision: u8) -> Decimal {
1908 Decimal::new(1, u32::from(precision))
1909}
1910
1911fn quantity_from_decimal(size: Decimal, precision: u8) -> Quantity {
1912 Quantity::from_decimal_dp(size, precision).expect(FAILED)
1913}
1914
1915fn step_as_quantity(step: usize, precision: u8) -> Quantity {
1916 let raw = (FIXED_SCALAR as QuantityRaw)
1917 .checked_mul(step as QuantityRaw)
1918 .expect("`step` overflows raw quantity units for volume aggregation");
1919 Quantity::from_raw(raw, precision)
1920}
1921
1922pub trait VegaProvider {
1924 fn vega_for_leg(&self, instrument_id: InstrumentId) -> Option<f64>;
1926}
1927
1928pub trait SpreadPriceRounder {
1930 fn round_prices(&self, raw_bid: f64, raw_ask: f64, precision: u8) -> (Price, Price);
1932}
1933
1934#[derive(Debug, Default)]
1936pub struct MapVegaProvider {
1937 vegas: AHashMap<InstrumentId, f64>,
1938}
1939
1940impl MapVegaProvider {
1941 pub fn new() -> Self {
1942 Self::default()
1943 }
1944
1945 pub fn insert(&mut self, instrument_id: InstrumentId, vega: f64) {
1946 self.vegas.insert(instrument_id, vega);
1947 }
1948
1949 pub fn get(&self, instrument_id: &InstrumentId) -> Option<f64> {
1950 self.vegas.get(instrument_id).copied()
1951 }
1952}
1953
1954impl VegaProvider for MapVegaProvider {
1955 fn vega_for_leg(&self, instrument_id: InstrumentId) -> Option<f64> {
1956 self.vegas.get(&instrument_id).copied()
1957 }
1958}
1959
1960#[derive(Debug)]
1962pub struct FixedTickSchemeRounder {
1963 scheme: FixedTickScheme,
1964}
1965
1966impl FixedTickSchemeRounder {
1967 pub fn new(tick: f64) -> anyhow::Result<Self> {
1973 Ok(Self {
1974 scheme: FixedTickScheme::new(tick)?,
1975 })
1976 }
1977
1978 fn round_one(&self, raw: f64, precision: u8, use_bid_rounding: bool) -> Price {
1979 if raw >= 0.0 {
1980 let p = if use_bid_rounding {
1981 self.scheme.next_bid_price(raw, 0, precision)
1982 } else {
1983 self.scheme.next_ask_price(raw, 0, precision)
1984 };
1985 p.unwrap_or_else(|| Price::new(raw, precision))
1986 } else {
1987 let p = if use_bid_rounding {
1988 self.scheme.next_ask_price(-raw, 0, precision)
1989 } else {
1990 self.scheme.next_bid_price(-raw, 0, precision)
1991 };
1992 p.map_or_else(
1993 || Price::new(raw, precision),
1994 |q| Price::new(-q.as_f64(), precision),
1995 )
1996 }
1997 }
1998}
1999
2000impl SpreadPriceRounder for FixedTickSchemeRounder {
2001 fn round_prices(&self, raw_bid: f64, raw_ask: f64, precision: u8) -> (Price, Price) {
2002 (
2003 self.round_one(raw_bid, precision, true),
2004 self.round_one(raw_ask, precision, false),
2005 )
2006 }
2007}
2008
2009pub struct SpreadQuoteAggregator {
2015 spread_instrument_id: InstrumentId,
2016 leg_ids: Vec<InstrumentId>,
2017 ratios: Vec<i64>,
2018 is_futures_spread: bool,
2019 price_precision: u8,
2020 size_precision: u8,
2021 last_quotes: AHashMap<InstrumentId, QuoteTick>,
2022 mid_prices: Vec<f64>,
2023 bid_prices: Vec<f64>,
2024 ask_prices: Vec<f64>,
2025 vegas: Vec<f64>,
2026 bid_ask_spreads: Vec<f64>,
2027 bid_sizes: Vec<f64>,
2028 ask_sizes: Vec<f64>,
2029 handler: Box<dyn FnMut(QuoteTick)>,
2030 clock: Rc<RefCell<dyn Clock>>,
2031 historical_mode: bool,
2032 update_interval_seconds: Option<u64>,
2033 quote_build_delay: u64,
2034 has_update: bool,
2035 timer_name: String,
2036 vega_pricing_timeout_timer_name: String,
2037 historical_event_at_ts_init: Option<TimeEvent>,
2038 vega_provider: Option<Box<dyn VegaProvider>>,
2039 disable_vega_pricing: bool,
2040 vega_pricing_temporarily_disabled: bool,
2041 vega_pricing_timeout_seconds: u64,
2042 price_rounder: Option<Box<dyn SpreadPriceRounder>>,
2043 is_running: bool,
2044 aggregator_weak: Option<Weak<RefCell<Self>>>,
2045}
2046
2047impl Debug for SpreadQuoteAggregator {
2048 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2049 f.debug_struct(stringify!(SpreadQuoteAggregator))
2050 .field("spread_instrument_id", &self.spread_instrument_id)
2051 .field("n_legs", &self.leg_ids.len())
2052 .field("is_futures_spread", &self.is_futures_spread)
2053 .field("update_interval_seconds", &self.update_interval_seconds)
2054 .finish()
2055 }
2056}
2057
2058impl SpreadQuoteAggregator {
2059 #[expect(clippy::too_many_arguments)]
2065 pub fn new(
2066 spread_instrument_id: InstrumentId,
2067 legs: &[(InstrumentId, i64)],
2068 is_futures_spread: bool,
2069 price_precision: u8,
2070 size_precision: u8,
2071 handler: Box<dyn FnMut(QuoteTick)>,
2072 clock: Rc<RefCell<dyn Clock>>,
2073 historical_mode: bool,
2074 update_interval_seconds: Option<u64>,
2075 quote_build_delay: u64,
2076 disable_vega_pricing: bool,
2077 vega_pricing_timeout_seconds: u64,
2078 vega_provider: Option<Box<dyn VegaProvider>>,
2079 price_rounder: Option<Box<dyn SpreadPriceRounder>>,
2080 ) -> Self {
2081 assert!(legs.len() >= 2, "Spread must have more than one leg");
2082 let n_legs = legs.len();
2083 let leg_ids: Vec<InstrumentId> = legs.iter().map(|(id, _)| *id).collect();
2084 let ratios: Vec<i64> = legs.iter().map(|(_, r)| *r).collect();
2085 for &r in &ratios {
2086 assert!(r != 0, "Ratio cannot be zero");
2087 }
2088 let timer_name = format!("SPREAD_QUOTE_{spread_instrument_id}");
2089 let vega_pricing_timeout_timer_name =
2090 format!("VEGA_PRICING_TIMEOUT_{spread_instrument_id}");
2091 Self {
2092 spread_instrument_id,
2093 leg_ids,
2094 ratios,
2095 is_futures_spread,
2096 price_precision,
2097 size_precision,
2098 last_quotes: AHashMap::new(),
2099 mid_prices: vec![0.0; n_legs],
2100 bid_prices: vec![0.0; n_legs],
2101 ask_prices: vec![0.0; n_legs],
2102 vegas: vec![0.0; n_legs],
2103 bid_ask_spreads: vec![0.0; n_legs],
2104 bid_sizes: vec![0.0; n_legs],
2105 ask_sizes: vec![0.0; n_legs],
2106 handler,
2107 clock,
2108 historical_mode,
2109 update_interval_seconds,
2110 quote_build_delay,
2111 has_update: false,
2112 timer_name,
2113 vega_pricing_timeout_timer_name,
2114 historical_event_at_ts_init: None,
2115 vega_provider,
2116 disable_vega_pricing,
2117 vega_pricing_temporarily_disabled: false,
2118 vega_pricing_timeout_seconds,
2119 price_rounder,
2120 is_running: false,
2121 aggregator_weak: None,
2122 }
2123 }
2124
2125 pub fn set_aggregator_weak(&mut self, weak: Weak<RefCell<Self>>) {
2128 self.aggregator_weak = Some(weak);
2129 }
2130
2131 pub fn prepare_for_timer_mode(&mut self, self_rc: &Rc<RefCell<Self>>) {
2136 self.aggregator_weak = Some(Rc::downgrade(self_rc));
2137 }
2138
2139 pub fn set_historical_mode(
2141 &mut self,
2142 historical_mode: bool,
2143 handler: Box<dyn FnMut(QuoteTick)>,
2144 vega_provider: Option<Box<dyn VegaProvider>>,
2145 ) {
2146 self.historical_mode = historical_mode;
2147 self.handler = handler;
2148
2149 if let Some(vp) = vega_provider {
2150 self.vega_provider = Some(vp);
2151 }
2152 }
2153
2154 pub fn set_running(&mut self, is_running: bool) {
2155 self.is_running = is_running;
2156 }
2157
2158 pub fn set_clock(&mut self, clock: Rc<RefCell<dyn Clock>>) {
2159 self.clock = clock;
2160 }
2161
2162 pub fn start_timer(&mut self, aggregator_rc: Option<Rc<RefCell<Self>>>) {
2171 if let Some(rc) = aggregator_rc {
2172 self.aggregator_weak = Some(Rc::downgrade(&rc));
2173 }
2174
2175 let Some(interval_secs) = self.update_interval_seconds else {
2176 return;
2177 };
2178 let aggregator_weak = self.aggregator_weak.clone().expect(
2179 "SpreadQuoteAggregator: timer mode requires prepare_for_timer_mode(rc) to be \
2180 called first with the Rc that wraps this aggregator (before feeding quotes in \
2181 historical mode or before start_timer(None)).",
2182 );
2183
2184 let callback = TimeEventCallback::RustLocal(Rc::new(move |event: TimeEvent| {
2185 if let Some(agg) = aggregator_weak.upgrade() {
2186 agg.borrow_mut().on_timer_fire(event.ts_event);
2187 }
2188 }));
2189
2190 let now_ns = self.clock.borrow().timestamp_ns();
2191 let interval_ns = DurationNanos::from_secs(interval_secs);
2192 let start_time =
2193 now_ns.floor(interval_ns) + DurationNanos::from_micros(self.quote_build_delay);
2194 let fire_immediately = now_ns == start_time;
2195 self.clock
2196 .borrow_mut()
2197 .set_timer_ns(
2198 &self.timer_name,
2199 interval_ns,
2200 Some(start_time),
2201 None,
2202 Some(callback),
2203 Some(true),
2204 Some(fire_immediately),
2205 )
2206 .expect("Failed to set spread quote timer");
2207 }
2208
2209 pub fn on_timer_fire(&mut self, ts_event: UnixNanos) {
2211 if self.last_quotes.len() == self.leg_ids.len() {
2212 self.build_and_send_quote(ts_event);
2213 }
2214 }
2215
2216 pub fn stop_timer(&mut self) {
2218 if self.update_interval_seconds.is_some()
2219 && self
2220 .clock
2221 .borrow()
2222 .timer_names()
2223 .contains(&self.timer_name.as_str())
2224 {
2225 self.clock.borrow_mut().cancel_timer(&self.timer_name);
2226 }
2227
2228 if self
2229 .clock
2230 .borrow()
2231 .timer_names()
2232 .contains(&self.vega_pricing_timeout_timer_name.as_str())
2233 {
2234 self.clock
2235 .borrow_mut()
2236 .cancel_timer(&self.vega_pricing_timeout_timer_name);
2237 }
2238 }
2239
2240 pub fn handle_quote_tick(&mut self, tick: QuoteTick) {
2242 let ts_init = tick.ts_init;
2243
2244 if self.update_interval_seconds.is_some() && self.historical_mode {
2245 self.process_historical_events(ts_init);
2246 }
2247 self.last_quotes.insert(tick.instrument_id, tick);
2248 self.has_update = true;
2249
2250 if self.update_interval_seconds.is_none() && self.last_quotes.len() == self.leg_ids.len() {
2251 self.build_and_send_quote(ts_init);
2252 }
2253 }
2254
2255 pub fn flush_pending_historical_quote(&mut self) {
2261 if self.update_interval_seconds.is_none() || !self.historical_mode {
2262 return;
2263 }
2264
2265 let Some(event) = self.historical_event_at_ts_init.take() else {
2266 return;
2267 };
2268
2269 if self.last_quotes.len() == self.leg_ids.len() {
2270 self.build_and_send_quote(event.ts_event);
2271 }
2272 }
2273
2274 fn process_historical_events(&mut self, ts_init: UnixNanos) {
2280 if self.clock.borrow().timestamp_ns() == UnixNanos::default() {
2281 let mut clock_borrow = self.clock.borrow_mut();
2282 let test_clock = clock_borrow
2283 .as_any_mut()
2284 .downcast_mut::<TestClock>()
2285 .expect("Expected TestClock in historical mode");
2286 test_clock.set_time(ts_init);
2287 drop(clock_borrow);
2288 self.start_timer(None);
2289 }
2290
2291 if self.last_quotes.len() == self.leg_ids.len()
2292 && let Some(ref event) = self.historical_event_at_ts_init
2293 && event.ts_event < ts_init
2294 {
2295 let event = self.historical_event_at_ts_init.take().unwrap();
2297 self.build_and_send_quote(event.ts_event);
2298 }
2299
2300 let events = {
2301 let mut clock_borrow = self.clock.borrow_mut();
2302 let test_clock = clock_borrow
2303 .as_any_mut()
2304 .downcast_mut::<TestClock>()
2305 .expect("Expected TestClock in historical mode");
2306 test_clock.advance_time(ts_init, true)
2307 };
2308
2309 for event in events {
2310 if event.ts_event == ts_init {
2311 self.historical_event_at_ts_init = Some(event);
2312 } else if self.last_quotes.len() == self.leg_ids.len() {
2313 self.build_and_send_quote(event.ts_event);
2314 }
2315 }
2316 }
2317
2318 fn build_and_send_quote(&mut self, ts_event: UnixNanos) {
2320 if !self.has_update {
2321 return;
2322 }
2323
2324 let use_vega_pricing =
2325 !(self.disable_vega_pricing || self.vega_pricing_temporarily_disabled);
2326
2327 for (idx, &leg_id) in self.leg_ids.iter().enumerate() {
2328 let Some(tick) = self.last_quotes.get(&leg_id) else {
2329 log::error!(
2330 "SpreadQuoteAggregator[{}]: Missing quote for leg {}",
2331 self.spread_instrument_id,
2332 leg_id
2333 );
2334 return;
2335 };
2336 let ask_price = tick.ask_price.as_f64();
2337 let bid_price = tick.bid_price.as_f64();
2338 self.bid_prices[idx] = bid_price;
2339 self.ask_prices[idx] = ask_price;
2340 self.bid_sizes[idx] = tick.bid_size.as_f64();
2341 self.ask_sizes[idx] = tick.ask_size.as_f64();
2342
2343 if !self.is_futures_spread {
2344 self.mid_prices[idx] = f64::midpoint(ask_price, bid_price);
2345 self.bid_ask_spreads[idx] = ask_price - bid_price;
2346
2347 if use_vega_pricing
2348 && let Some(ref vp) = self.vega_provider
2349 && let Some(vega) = vp.vega_for_leg(leg_id)
2350 {
2351 self.vegas[idx] = vega;
2352 }
2353 }
2354 }
2355 let (raw_bid, raw_ask) = if self.is_futures_spread {
2356 self.create_futures_spread_prices()
2357 } else {
2358 self.create_option_spread_prices()
2359 };
2360 let spread_quote = self.create_quote_tick_from_raw_prices(raw_bid, raw_ask, ts_event);
2361 self.has_update = false;
2362 (self.handler)(spread_quote);
2363 }
2364
2365 fn create_option_spread_prices(&mut self) -> (f64, f64) {
2366 if self.disable_vega_pricing || self.vega_pricing_temporarily_disabled {
2367 return self.create_futures_spread_prices();
2368 }
2369
2370 let (vega_multiplier_sum, vega_multiplier_count) = (0..self.leg_ids.len())
2371 .filter_map(|i| {
2372 let multiplier = if self.vegas[i] == 0.0 {
2373 0.0
2374 } else {
2375 self.bid_ask_spreads[i] / self.vegas[i]
2376 };
2377 (multiplier != 0.0).then_some(multiplier.abs())
2378 })
2379 .fold((0.0, 0_usize), |(sum, count), multiplier| {
2380 (sum + multiplier, count + 1)
2381 });
2382
2383 if vega_multiplier_count == 0 {
2384 log::warn!(
2385 "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",
2386 self.spread_instrument_id,
2387 self.vega_pricing_timeout_seconds
2388 );
2389 self.start_vega_pricing_timeout();
2390 return self.create_futures_spread_prices();
2391 }
2392 let vega_multiplier = vega_multiplier_sum / vega_multiplier_count as f64;
2393 let spread_vega = self
2394 .vegas
2395 .iter()
2396 .zip(self.ratios.iter())
2397 .map(|(v, r)| v * (*r as f64))
2398 .sum::<f64>()
2399 .abs();
2400 let bid_ask_spread = spread_vega * vega_multiplier;
2401 let spread_mid_price: f64 = self
2402 .mid_prices
2403 .iter()
2404 .zip(self.ratios.iter())
2405 .map(|(m, r)| m * (*r as f64))
2406 .sum();
2407 let raw_bid = spread_mid_price - bid_ask_spread * 0.5;
2408 let raw_ask = spread_mid_price + bid_ask_spread * 0.5;
2409 (raw_bid, raw_ask)
2410 }
2411
2412 fn clear_vega_pricing_timeout(&mut self) {
2413 self.vega_pricing_temporarily_disabled = false;
2414 }
2415
2416 fn start_vega_pricing_timeout(&mut self) {
2417 self.vega_pricing_temporarily_disabled = true;
2418
2419 if self
2420 .clock
2421 .borrow()
2422 .timer_names()
2423 .contains(&self.vega_pricing_timeout_timer_name.as_str())
2424 {
2425 return;
2426 }
2427
2428 let Some(aggregator_weak) = self.aggregator_weak.clone() else {
2429 return;
2430 };
2431 let callback = TimeEventCallback::RustLocal(Rc::new(move |_event: TimeEvent| {
2432 if let Some(agg) = aggregator_weak.upgrade() {
2433 agg.borrow_mut().clear_vega_pricing_timeout();
2434 }
2435 }));
2436 let timeout = DurationNanos::try_from_secs(self.vega_pricing_timeout_seconds)
2437 .expect("vega pricing timeout exceeds the nanosecond range");
2438 let alert_time = self.clock.borrow().timestamp_ns() + timeout;
2439
2440 self.clock
2441 .borrow_mut()
2442 .set_time_alert_ns(
2443 &self.vega_pricing_timeout_timer_name,
2444 alert_time,
2445 Some(callback),
2446 Some(true),
2447 )
2448 .expect("Failed to set spread quote vega pricing timeout");
2449 }
2450
2451 fn create_futures_spread_prices(&self) -> (f64, f64) {
2452 let mut raw_ask = 0.0_f64;
2453 let mut raw_bid = 0.0_f64;
2454
2455 for i in 0..self.leg_ids.len() {
2456 let r = self.ratios[i] as f64;
2457 if self.ratios[i] >= 0 {
2458 raw_ask += r * self.ask_prices[i];
2459 raw_bid += r * self.bid_prices[i];
2460 } else {
2461 raw_ask += r * self.bid_prices[i];
2462 raw_bid += r * self.ask_prices[i];
2463 }
2464 }
2465 (raw_bid, raw_ask)
2466 }
2467
2468 fn create_quote_tick_from_raw_prices(
2469 &self,
2470 raw_bid_price: f64,
2471 raw_ask_price: f64,
2472 ts_event: UnixNanos,
2473 ) -> QuoteTick {
2474 let (bid_price, ask_price) = if let Some(ref rounder) = self.price_rounder {
2475 rounder.round_prices(raw_bid_price, raw_ask_price, self.price_precision)
2476 } else {
2477 (
2478 Price::new(raw_bid_price, self.price_precision),
2479 Price::new(raw_ask_price, self.price_precision),
2480 )
2481 };
2482 let mut min_bid_size = f64::INFINITY;
2483 let mut min_ask_size = f64::INFINITY;
2484 for i in 0..self.leg_ids.len() {
2485 let abs_ratio = self.ratios[i].unsigned_abs() as f64;
2486 let (bid_size, ask_size) = if self.ratios[i] >= 0 {
2487 (self.bid_sizes[i], self.ask_sizes[i])
2488 } else {
2489 (self.ask_sizes[i], self.bid_sizes[i])
2490 };
2491 let bid_size = bid_size / abs_ratio;
2492 if bid_size < min_bid_size {
2493 min_bid_size = bid_size;
2494 }
2495 let ask_size = ask_size / abs_ratio;
2496 if ask_size < min_ask_size {
2497 min_ask_size = ask_size;
2498 }
2499 }
2500 let bid_size = Quantity::new(min_bid_size, self.size_precision);
2501 let ask_size = Quantity::new(min_ask_size, self.size_precision);
2502 QuoteTick::new(
2503 self.spread_instrument_id,
2504 bid_price,
2505 ask_price,
2506 bid_size,
2507 ask_size,
2508 ts_event,
2509 ts_event,
2510 )
2511 }
2512}
2513
2514#[cfg(test)]
2515mod tests {
2516 use std::sync::Arc;
2517
2518 use nautilus_common::{clock::TestClock, timer::TimeEvent};
2519 use nautilus_core::{UUID4, UnixNanos};
2520 use nautilus_model::{
2521 data::{BarSpecification, BarType, QuoteTick},
2522 enums::{AggregationSource, AggressorSide, BarAggregation, PriceType},
2523 identifiers::InstrumentId,
2524 instruments::{CurrencyPair, Equity, Instrument, InstrumentAny, stubs::*},
2525 types::{Price, Quantity, price::PRICE_RAW_MAX},
2526 };
2527 use parking_lot::Mutex;
2528 use rstest::rstest;
2529 use ustr::Ustr;
2530
2531 use super::*;
2532
2533 #[rstest]
2534 fn test_bar_builder_initialization(equity_aapl: Equity) {
2535 let instrument = InstrumentAny::Equity(equity_aapl);
2536 let bar_type = BarType::new(
2537 instrument.id(),
2538 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2539 AggregationSource::Internal,
2540 );
2541 let builder = BarBuilder::new(
2542 bar_type,
2543 instrument.price_precision(),
2544 instrument.size_precision(),
2545 );
2546
2547 assert!(!builder.initialized);
2548 assert_eq!(builder.ts_last, 0);
2549 assert_eq!(builder.count, 0);
2550 }
2551
2552 #[rstest]
2553 fn test_bar_builder_maintains_ohlc_order(equity_aapl: Equity) {
2554 let instrument = InstrumentAny::Equity(equity_aapl);
2555 let bar_type = BarType::new(
2556 instrument.id(),
2557 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2558 AggregationSource::Internal,
2559 );
2560 let mut builder = BarBuilder::new(
2561 bar_type,
2562 instrument.price_precision(),
2563 instrument.size_precision(),
2564 );
2565
2566 builder.update(
2567 Price::from("100.00"),
2568 Quantity::from(1),
2569 UnixNanos::from(1000),
2570 );
2571 builder.update(
2572 Price::from("95.00"),
2573 Quantity::from(1),
2574 UnixNanos::from(2000),
2575 );
2576 builder.update(
2577 Price::from("105.00"),
2578 Quantity::from(1),
2579 UnixNanos::from(3000),
2580 );
2581
2582 let bar = builder.build_now();
2583 assert!(bar.high > bar.low);
2584 assert_eq!(bar.open, Price::from("100.00"));
2585 assert_eq!(bar.high, Price::from("105.00"));
2586 assert_eq!(bar.low, Price::from("95.00"));
2587 assert_eq!(bar.close, Price::from("105.00"));
2588 }
2589
2590 #[rstest]
2591 fn test_update_ignores_earlier_timestamps(equity_aapl: Equity) {
2592 let instrument = InstrumentAny::Equity(equity_aapl);
2593 let bar_type = BarType::new(
2594 instrument.id(),
2595 BarSpecification::new(100, BarAggregation::Tick, PriceType::Last),
2596 AggregationSource::Internal,
2597 );
2598 let mut builder = BarBuilder::new(
2599 bar_type,
2600 instrument.price_precision(),
2601 instrument.size_precision(),
2602 );
2603
2604 builder.update(Price::from("1.00000"), Quantity::from(1), 1_000.into());
2605 builder.update(Price::from("1.00001"), Quantity::from(1), 500.into());
2606
2607 assert_eq!(builder.ts_last, 1_000);
2608 assert_eq!(builder.count, 1);
2609 }
2610
2611 #[rstest]
2612 fn test_bar_builder_single_update_results_in_expected_properties(equity_aapl: Equity) {
2613 let instrument = InstrumentAny::Equity(equity_aapl);
2614 let bar_type = BarType::new(
2615 instrument.id(),
2616 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2617 AggregationSource::Internal,
2618 );
2619 let mut builder = BarBuilder::new(
2620 bar_type,
2621 instrument.price_precision(),
2622 instrument.size_precision(),
2623 );
2624
2625 builder.update(
2626 Price::from("1.00000"),
2627 Quantity::from(1),
2628 UnixNanos::default(),
2629 );
2630
2631 assert!(builder.initialized);
2632 assert_eq!(builder.ts_last, 0);
2633 assert_eq!(builder.count, 1);
2634 }
2635
2636 #[rstest]
2637 fn test_bar_builder_single_update_when_timestamp_less_than_last_update_ignores(
2638 equity_aapl: Equity,
2639 ) {
2640 let instrument = InstrumentAny::Equity(equity_aapl);
2641 let bar_type = BarType::new(
2642 instrument.id(),
2643 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2644 AggregationSource::Internal,
2645 );
2646 let mut builder = BarBuilder::new(bar_type, 2, 0);
2647
2648 builder.update(
2649 Price::from("1.00000"),
2650 Quantity::from(1),
2651 UnixNanos::from(1_000),
2652 );
2653 builder.update(
2654 Price::from("1.00001"),
2655 Quantity::from(1),
2656 UnixNanos::from(500),
2657 );
2658
2659 assert!(builder.initialized);
2660 assert_eq!(builder.ts_last, 1_000);
2661 assert_eq!(builder.count, 1);
2662 }
2663
2664 #[rstest]
2665 fn test_bar_builder_multiple_updates_correctly_increments_count(equity_aapl: Equity) {
2666 let instrument = InstrumentAny::Equity(equity_aapl);
2667 let bar_type = BarType::new(
2668 instrument.id(),
2669 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2670 AggregationSource::Internal,
2671 );
2672 let mut builder = BarBuilder::new(
2673 bar_type,
2674 instrument.price_precision(),
2675 instrument.size_precision(),
2676 );
2677
2678 for _ in 0..5 {
2679 builder.update(
2680 Price::from("1.00000"),
2681 Quantity::from(1),
2682 UnixNanos::from(1_000),
2683 );
2684 }
2685
2686 assert_eq!(builder.count, 5);
2687 }
2688
2689 #[rstest]
2690 #[should_panic]
2691 fn test_bar_builder_build_when_no_updates_panics(equity_aapl: Equity) {
2692 let instrument = InstrumentAny::Equity(equity_aapl);
2693 let bar_type = BarType::new(
2694 instrument.id(),
2695 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2696 AggregationSource::Internal,
2697 );
2698 let mut builder = BarBuilder::new(
2699 bar_type,
2700 instrument.price_precision(),
2701 instrument.size_precision(),
2702 );
2703 let _ = builder.build_now();
2704 }
2705
2706 #[rstest]
2707 fn test_bar_builder_build_when_received_updates_returns_expected_bar(equity_aapl: Equity) {
2708 let instrument = InstrumentAny::Equity(equity_aapl);
2709 let bar_type = BarType::new(
2710 instrument.id(),
2711 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2712 AggregationSource::Internal,
2713 );
2714 let mut builder = BarBuilder::new(
2715 bar_type,
2716 instrument.price_precision(),
2717 instrument.size_precision(),
2718 );
2719
2720 builder.update(
2721 Price::from("1.00001"),
2722 Quantity::from(2),
2723 UnixNanos::default(),
2724 );
2725 builder.update(
2726 Price::from("1.00002"),
2727 Quantity::from(2),
2728 UnixNanos::default(),
2729 );
2730 builder.update(
2731 Price::from("1.00000"),
2732 Quantity::from(1),
2733 UnixNanos::from(1_000_000_000),
2734 );
2735
2736 let bar = builder.build_now();
2737
2738 assert_eq!(bar.open, Price::from("1.00001"));
2739 assert_eq!(bar.high, Price::from("1.00002"));
2740 assert_eq!(bar.low, Price::from("1.00000"));
2741 assert_eq!(bar.close, Price::from("1.00000"));
2742 assert_eq!(bar.volume, Quantity::from(5));
2743 assert_eq!(bar.ts_init, 1_000_000_000);
2744 assert_eq!(builder.ts_last, 1_000_000_000);
2745 assert_eq!(builder.count, 0);
2746 }
2747
2748 #[rstest]
2749 fn test_bar_builder_build_with_previous_close(equity_aapl: Equity) {
2750 let instrument = InstrumentAny::Equity(equity_aapl);
2751 let bar_type = BarType::new(
2752 instrument.id(),
2753 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2754 AggregationSource::Internal,
2755 );
2756 let mut builder = BarBuilder::new(bar_type, 2, 0);
2757
2758 builder.update(
2759 Price::from("1.00001"),
2760 Quantity::from(1),
2761 UnixNanos::default(),
2762 );
2763 builder.build_now();
2764
2765 builder.update(
2766 Price::from("1.00000"),
2767 Quantity::from(1),
2768 UnixNanos::default(),
2769 );
2770 builder.update(
2771 Price::from("1.00003"),
2772 Quantity::from(1),
2773 UnixNanos::default(),
2774 );
2775 builder.update(
2776 Price::from("1.00002"),
2777 Quantity::from(1),
2778 UnixNanos::default(),
2779 );
2780
2781 let bar = builder.build_now();
2782
2783 assert_eq!(bar.open, Price::from("1.00000"));
2784 assert_eq!(bar.high, Price::from("1.00003"));
2785 assert_eq!(bar.low, Price::from("1.00000"));
2786 assert_eq!(bar.close, Price::from("1.00002"));
2787 assert_eq!(bar.volume, Quantity::from(3));
2788 }
2789
2790 #[rstest]
2791 fn test_bar_builder_update_bar_initializes_then_accumulates(equity_aapl: Equity) {
2792 let instrument = InstrumentAny::Equity(equity_aapl);
2793 let bar_type = BarType::new(
2794 instrument.id(),
2795 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2796 AggregationSource::Internal,
2797 );
2798 let mut builder = BarBuilder::new(
2799 bar_type,
2800 instrument.price_precision(),
2801 instrument.size_precision(),
2802 );
2803
2804 let bar_one = Bar::new(
2805 bar_type,
2806 Price::from("100.00"),
2807 Price::from("102.00"),
2808 Price::from("99.00"),
2809 Price::from("101.00"),
2810 Quantity::from(10),
2811 UnixNanos::from(1_000),
2812 UnixNanos::from(1_000),
2813 );
2814 let bar_two = Bar::new(
2815 bar_type,
2816 Price::from("101.00"),
2817 Price::from("103.00"),
2818 Price::from("98.00"),
2819 Price::from("102.00"),
2820 Quantity::from(5),
2821 UnixNanos::from(2_000),
2822 UnixNanos::from(2_000),
2823 );
2824
2825 builder.update_bar(bar_one, bar_one.volume, bar_one.ts_init);
2826 builder.update_bar(bar_two, bar_two.volume, bar_two.ts_init);
2827 let bar = builder.build_now();
2828
2829 assert_eq!(bar.open, Price::from("100.00"));
2830 assert_eq!(bar.high, Price::from("103.00"));
2831 assert_eq!(bar.low, Price::from("98.00"));
2832 assert_eq!(bar.close, Price::from("102.00"));
2833 assert_eq!(bar.volume, Quantity::from(15));
2834 assert_eq!(builder.count, 0);
2835 }
2836
2837 #[rstest]
2838 fn test_bar_builder_update_bar_ignores_earlier_timestamp(equity_aapl: Equity) {
2839 let instrument = InstrumentAny::Equity(equity_aapl);
2840 let bar_type = BarType::new(
2841 instrument.id(),
2842 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2843 AggregationSource::Internal,
2844 );
2845 let mut builder = BarBuilder::new(
2846 bar_type,
2847 instrument.price_precision(),
2848 instrument.size_precision(),
2849 );
2850
2851 let bar_later = Bar::new(
2852 bar_type,
2853 Price::from("100.00"),
2854 Price::from("101.00"),
2855 Price::from("99.00"),
2856 Price::from("100.50"),
2857 Quantity::from(10),
2858 UnixNanos::from(2_000),
2859 UnixNanos::from(2_000),
2860 );
2861 let bar_earlier = Bar::new(
2862 bar_type,
2863 Price::from("200.00"),
2864 Price::from("210.00"),
2865 Price::from("190.00"),
2866 Price::from("205.00"),
2867 Quantity::from(50),
2868 UnixNanos::from(1_000),
2869 UnixNanos::from(1_000),
2870 );
2871
2872 builder.update_bar(bar_later, bar_later.volume, bar_later.ts_init);
2873 builder.update_bar(bar_earlier, bar_earlier.volume, bar_earlier.ts_init);
2874
2875 assert_eq!(builder.ts_last, 2_000);
2876 assert_eq!(builder.count, 1);
2877 assert_eq!(builder.volume, Quantity::from(10));
2878 }
2879
2880 #[rstest]
2881 #[case::spread_zero_inactive(
2882 Decimal::ZERO,
2883 ContinuousFutureAdjustmentType::BackwardSpread,
2884 false
2885 )]
2886 #[case::spread_positive_active(
2887 Decimal::new(150, 2), ContinuousFutureAdjustmentType::BackwardSpread,
2889 true,
2890 )]
2891 #[case::spread_negative_active(
2892 Decimal::new(-250, 2), ContinuousFutureAdjustmentType::ForwardSpread,
2894 true,
2895 )]
2896 #[case::spread_sub_precision_inactive(
2897 Decimal::new(1, 28),
2899 ContinuousFutureAdjustmentType::BackwardSpread,
2900 false,
2901 )]
2902 #[case::ratio_one_inactive(Decimal::ONE, ContinuousFutureAdjustmentType::BackwardRatio, false)]
2903 #[case::ratio_non_one_active(
2904 Decimal::new(105, 2), ContinuousFutureAdjustmentType::ForwardRatio,
2906 true,
2907 )]
2908 fn test_bar_builder_set_adjustment_active_flag(
2909 equity_aapl: Equity,
2910 #[case] adjustment: Decimal,
2911 #[case] mode: ContinuousFutureAdjustmentType,
2912 #[case] expected_active: bool,
2913 ) {
2914 let instrument = InstrumentAny::Equity(equity_aapl);
2915 let bar_type = BarType::new(
2916 instrument.id(),
2917 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2918 AggregationSource::Internal,
2919 );
2920 let mut builder = BarBuilder::new(bar_type, 2, 0);
2921
2922 builder.set_adjustment(adjustment, mode);
2923
2924 assert_eq!(builder.adjustment_active, expected_active);
2925 assert_eq!(builder.adjustment_is_ratio, mode.is_ratio());
2926 }
2927
2928 #[rstest]
2929 fn test_bar_builder_set_adjustment_mode_switch_resets_flags(equity_aapl: Equity) {
2930 let instrument = InstrumentAny::Equity(equity_aapl);
2931 let bar_type = BarType::new(
2932 instrument.id(),
2933 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2934 AggregationSource::Internal,
2935 );
2936 let mut builder = BarBuilder::new(bar_type, 2, 0);
2937
2938 builder.set_adjustment(
2940 Decimal::new(150, 2), ContinuousFutureAdjustmentType::BackwardRatio,
2942 );
2943 builder.set_adjustment(
2944 Decimal::new(50, 2), ContinuousFutureAdjustmentType::BackwardSpread,
2946 );
2947 assert!(!builder.adjustment_is_ratio);
2948 builder.update(Price::from("100.00"), Quantity::from(1), 1_000.into());
2949 assert_eq!(builder.build_now().close, Price::from("100.50"));
2950
2951 builder.set_adjustment(
2953 Decimal::new(11, 1), ContinuousFutureAdjustmentType::ForwardRatio,
2955 );
2956 assert!(builder.adjustment_is_ratio);
2957 builder.update(Price::from("100.00"), Quantity::from(1), 2_000.into());
2958 assert_eq!(builder.build_now().close, Price::from("110.00"));
2959 }
2960
2961 #[cfg(feature = "defi")]
2962 #[rstest]
2963 fn test_bar_builder_spread_preserves_legacy_native_raw_units(equity_aapl: Equity) {
2964 let bar_type = BarType::new(
2965 equity_aapl.id(),
2966 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2967 AggregationSource::Internal,
2968 );
2969
2970 let mut builder = BarBuilder::new(bar_type, 18, 0);
2971 builder.set_adjustment(Decimal::ONE, ContinuousFutureAdjustmentType::BackwardSpread);
2972 let adjusted =
2973 builder.apply_adjustment_to_price(Price::from_raw(1_000_000_000_000_000_000, 18));
2974 assert_eq!(adjusted.raw(), 1_010_000_000_000_000_000);
2976 assert_eq!(adjusted.precision, 18);
2977 }
2978
2979 #[rstest]
2980 #[should_panic(expected = "Continuous-future adjustment exceeds Price bounds")]
2981 fn test_bar_builder_spread_rejects_out_of_domain_price(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, 2, 0);
2989 builder.set_adjustment(Decimal::ONE, ContinuousFutureAdjustmentType::BackwardSpread);
2990 builder.apply_adjustment_to_price(Price::from_raw(PRICE_RAW_MAX, 2));
2991 }
2992
2993 #[cfg(feature = "defi")]
2994 #[rstest]
2995 #[case(BarAggregation::Volume)]
2996 #[case(BarAggregation::VolumeImbalance)]
2997 #[case(BarAggregation::VolumeRuns)]
2998 fn test_volume_aggregators_preserve_legacy_native_raw_threshold(
2999 #[case] aggregation: BarAggregation,
3000 equity_aapl: Equity,
3001 ) {
3002 let bar_type = BarType::new(
3003 equity_aapl.id(),
3004 BarSpecification::new(1, aggregation, PriceType::Last),
3005 AggregationSource::Internal,
3006 );
3007 let (handler, record) = recording_handler();
3008
3009 let mut aggregator: Box<dyn BarAggregator> = match aggregation {
3010 BarAggregation::Volume => Box::new(VolumeBarAggregator::new(bar_type, 2, 18, record)),
3011 BarAggregation::VolumeImbalance => {
3012 Box::new(VolumeImbalanceBarAggregator::new(bar_type, 2, 18, record))
3013 }
3014 BarAggregation::VolumeRuns => {
3015 Box::new(VolumeRunsBarAggregator::new(bar_type, 2, 18, record))
3016 }
3017 _ => unreachable!(),
3018 };
3019
3020 let size = Quantity::from_raw(FIXED_SCALAR as QuantityRaw, 18);
3022 aggregator.handle_trade(TradeTick {
3023 price: Price::from("1.00"),
3024 size,
3025 aggressor_side: AggressorSide::Buy,
3026 ..TradeTick::default()
3027 });
3028
3029 let bars = handler.lock();
3030 assert_eq!(bars.len(), 1);
3031 assert_eq!(bars[0].volume, size);
3032 assert_eq!(bars[0].volume.precision, 18);
3033 }
3034
3035 #[rstest]
3036 fn test_bar_builder_update_applies_backward_spread_adjustment(equity_aapl: Equity) {
3037 let instrument = InstrumentAny::Equity(equity_aapl);
3038 let bar_type = BarType::new(
3039 instrument.id(),
3040 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3041 AggregationSource::Internal,
3042 );
3043 let mut builder = BarBuilder::new(bar_type, 2, 0);
3044
3045 builder.set_adjustment(
3046 Decimal::new(250, 2), ContinuousFutureAdjustmentType::BackwardSpread,
3048 );
3049
3050 builder.update(Price::from("100.00"), Quantity::from(1), 1_000.into());
3051 builder.update(Price::from("99.00"), Quantity::from(1), 2_000.into());
3052 builder.update(Price::from("101.00"), Quantity::from(1), 3_000.into());
3053
3054 let bar = builder.build_now();
3055 assert_eq!(bar.open, Price::from("102.50"));
3056 assert_eq!(bar.high, Price::from("103.50"));
3057 assert_eq!(bar.low, Price::from("101.50"));
3058 assert_eq!(bar.close, Price::from("103.50"));
3059 }
3060
3061 #[rstest]
3062 fn test_bar_builder_update_applies_forward_ratio_adjustment(equity_aapl: Equity) {
3063 let instrument = InstrumentAny::Equity(equity_aapl);
3064 let bar_type = BarType::new(
3065 instrument.id(),
3066 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3067 AggregationSource::Internal,
3068 );
3069 let mut builder = BarBuilder::new(bar_type, 2, 0);
3070
3071 builder.set_adjustment(
3072 Decimal::new(11, 1), ContinuousFutureAdjustmentType::ForwardRatio,
3074 );
3075
3076 builder.update(Price::from("100.00"), Quantity::from(1), 1_000.into());
3077 builder.update(Price::from("90.00"), Quantity::from(1), 2_000.into());
3078 builder.update(Price::from("110.00"), Quantity::from(1), 3_000.into());
3079
3080 let bar = builder.build_now();
3081 assert_eq!(bar.open, Price::from("110.00"));
3082 assert_eq!(bar.high, Price::from("121.00"));
3083 assert_eq!(bar.low, Price::from("99.00"));
3084 assert_eq!(bar.close, Price::from("121.00"));
3085 }
3086
3087 #[rstest]
3088 fn test_bar_builder_update_bar_applies_adjustment_to_ohlc(equity_aapl: Equity) {
3089 let instrument = InstrumentAny::Equity(equity_aapl);
3090 let bar_type = BarType::new(
3091 instrument.id(),
3092 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3093 AggregationSource::Internal,
3094 );
3095 let mut builder = BarBuilder::new(bar_type, 2, 0);
3096
3097 builder.set_adjustment(
3098 Decimal::new(-100, 2), ContinuousFutureAdjustmentType::BackwardSpread,
3100 );
3101
3102 let input = Bar::new(
3103 bar_type,
3104 Price::from("100.00"),
3105 Price::from("105.00"),
3106 Price::from("99.00"),
3107 Price::from("102.00"),
3108 Quantity::from(10),
3109 UnixNanos::from(1_000),
3110 UnixNanos::from(1_000),
3111 );
3112 builder.update_bar(input, input.volume, input.ts_init);
3113
3114 let bar = builder.build_now();
3115 assert_eq!(bar.open, Price::from("99.00"));
3116 assert_eq!(bar.high, Price::from("104.00"));
3117 assert_eq!(bar.low, Price::from("98.00"));
3118 assert_eq!(bar.close, Price::from("101.00"));
3119 }
3120
3121 #[rstest]
3122 fn test_bar_builder_reset_retains_adjustment(equity_aapl: Equity) {
3123 let instrument = InstrumentAny::Equity(equity_aapl);
3124 let bar_type = BarType::new(
3125 instrument.id(),
3126 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3127 AggregationSource::Internal,
3128 );
3129 let mut builder = BarBuilder::new(bar_type, 2, 0);
3130
3131 builder.set_adjustment(
3132 Decimal::new(500, 2), ContinuousFutureAdjustmentType::BackwardSpread,
3134 );
3135 builder.update(Price::from("100.00"), Quantity::from(1), 1_000.into());
3136 let bar_one = builder.build_now();
3137 assert_eq!(bar_one.close, Price::from("105.00"));
3138
3139 assert!(builder.adjustment_active);
3141
3142 builder.update(Price::from("110.00"), Quantity::from(1), 2_000.into());
3143 let bar_two = builder.build_now();
3144 assert_eq!(bar_two.close, Price::from("115.00"));
3145 }
3146
3147 #[rstest]
3148 fn test_bar_builder_update_bar_applies_ratio_adjustment(equity_aapl: Equity) {
3149 let instrument = InstrumentAny::Equity(equity_aapl);
3150 let bar_type = BarType::new(
3151 instrument.id(),
3152 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3153 AggregationSource::Internal,
3154 );
3155 let mut builder = BarBuilder::new(bar_type, 2, 0);
3156
3157 builder.set_adjustment(
3158 Decimal::new(11, 1), ContinuousFutureAdjustmentType::ForwardRatio,
3160 );
3161
3162 let input = Bar::new(
3163 bar_type,
3164 Price::from("100.00"),
3165 Price::from("110.00"),
3166 Price::from("90.00"),
3167 Price::from("105.00"),
3168 Quantity::from(10),
3169 UnixNanos::from(1_000),
3170 UnixNanos::from(1_000),
3171 );
3172 builder.update_bar(input, input.volume, input.ts_init);
3173
3174 let bar = builder.build_now();
3175 assert_eq!(bar.open, Price::from("110.00"));
3176 assert_eq!(bar.high, Price::from("121.00"));
3177 assert_eq!(bar.low, Price::from("99.00"));
3178 assert_eq!(bar.close, Price::from("115.50"));
3179 }
3180
3181 #[rstest]
3182 fn test_bar_builder_spread_below_zero_representable(equity_aapl: Equity) {
3183 let instrument = InstrumentAny::Equity(equity_aapl);
3185 let bar_type = BarType::new(
3186 instrument.id(),
3187 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3188 AggregationSource::Internal,
3189 );
3190 let mut builder = BarBuilder::new(bar_type, 2, 0);
3191
3192 builder.set_adjustment(
3193 Decimal::new(-15000, 2), ContinuousFutureAdjustmentType::BackwardSpread,
3195 );
3196
3197 builder.update(Price::from("100.00"), Quantity::from(1), 1_000.into());
3198 let bar = builder.build_now();
3199 assert_eq!(bar.close, Price::from("-50.00"));
3200 assert!(bar.close.is_negative());
3201 assert_eq!(bar.close.precision, 2);
3202 }
3203
3204 #[rstest]
3205 fn test_bar_builder_build_promotes_close_above_high_from_previous_close(equity_aapl: Equity) {
3206 let instrument = InstrumentAny::Equity(equity_aapl);
3207 let bar_type = BarType::new(
3208 instrument.id(),
3209 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3210 AggregationSource::Internal,
3211 );
3212 let mut builder = BarBuilder::new(bar_type, 2, 0);
3213
3214 builder.update(
3215 Price::from("110.00"),
3216 Quantity::from(1),
3217 UnixNanos::from(100),
3218 );
3219 builder.build_now();
3220
3221 builder.update(
3222 Price::from("100.00"),
3223 Quantity::from(1),
3224 UnixNanos::from(200),
3225 );
3226 builder.update(
3227 Price::from("101.00"),
3228 Quantity::from(1),
3229 UnixNanos::from(300),
3230 );
3231 builder.update(
3232 Price::from("200.00"),
3233 Quantity::from(1),
3234 UnixNanos::from(400),
3235 );
3236
3237 let bar = builder.build_now();
3238 assert_eq!(bar.open, Price::from("100.00"));
3239 assert_eq!(bar.high, Price::from("200.00"));
3240 assert_eq!(bar.low, Price::from("100.00"));
3241 assert_eq!(bar.close, Price::from("200.00"));
3242 }
3243
3244 #[rstest]
3245 fn test_bar_builder_build_clamps_low_to_close(equity_aapl: Equity) {
3246 let instrument = InstrumentAny::Equity(equity_aapl);
3250 let bar_type = BarType::new(
3251 instrument.id(),
3252 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3253 AggregationSource::Internal,
3254 );
3255 let mut builder = BarBuilder::new(bar_type, 2, 0);
3256
3257 builder.update(
3258 Price::from("100.00"),
3259 Quantity::from(1),
3260 UnixNanos::from(100),
3261 );
3262 builder.close = Some(Price::from("50.00"));
3263
3264 let bar = builder.build_now();
3265 assert_eq!(bar.low, Price::from("50.00"));
3266 assert_eq!(bar.close, Price::from("50.00"));
3267 assert!(bar.low <= bar.open);
3268 }
3269
3270 #[rstest]
3271 fn test_tick_bar_aggregator_handle_trade_when_step_count_below_threshold(equity_aapl: Equity) {
3272 let instrument = InstrumentAny::Equity(equity_aapl);
3273 let bar_spec = BarSpecification::new(3, BarAggregation::Tick, PriceType::Last);
3274 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3275 let (handler, record) = recording_handler();
3276
3277 let mut aggregator = TickBarAggregator::new(
3278 bar_type,
3279 instrument.price_precision(),
3280 instrument.size_precision(),
3281 record,
3282 );
3283
3284 let trade = TradeTick::default();
3285 aggregator.handle_trade(trade);
3286
3287 let handler_guard = handler.lock();
3288 assert_eq!(handler_guard.len(), 0);
3289 }
3290
3291 #[rstest]
3292 fn test_tick_bar_aggregator_handle_trade_when_step_count_reached(equity_aapl: Equity) {
3293 let instrument = InstrumentAny::Equity(equity_aapl);
3294 let bar_spec = BarSpecification::new(3, BarAggregation::Tick, PriceType::Last);
3295 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3296 let (handler, record) = recording_handler();
3297
3298 let mut aggregator = TickBarAggregator::new(
3299 bar_type,
3300 instrument.price_precision(),
3301 instrument.size_precision(),
3302 record,
3303 );
3304
3305 let trade = TradeTick::default();
3306 aggregator.handle_trade(trade);
3307 aggregator.handle_trade(trade);
3308 aggregator.handle_trade(trade);
3309
3310 let handler_guard = handler.lock();
3311 let bar = handler_guard.first().unwrap();
3312 assert_eq!(handler_guard.len(), 1);
3313 assert_eq!(bar.open, trade.price);
3314 assert_eq!(bar.high, trade.price);
3315 assert_eq!(bar.low, trade.price);
3316 assert_eq!(bar.close, trade.price);
3317 assert_eq!(bar.volume, Quantity::from(300000));
3318 assert_eq!(bar.ts_event, trade.ts_event);
3319 assert_eq!(bar.ts_init, trade.ts_init);
3320 }
3321
3322 #[rstest]
3323 fn test_tick_bar_aggregator_aggregates_to_step_size(equity_aapl: Equity) {
3324 let instrument = InstrumentAny::Equity(equity_aapl);
3325 let bar_spec = BarSpecification::new(3, BarAggregation::Tick, PriceType::Last);
3326 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3327 let (handler, record) = recording_handler();
3328
3329 let mut aggregator = TickBarAggregator::new(
3330 bar_type,
3331 instrument.price_precision(),
3332 instrument.size_precision(),
3333 record,
3334 );
3335
3336 aggregator.update(
3337 Price::from("1.00001"),
3338 Quantity::from(1),
3339 UnixNanos::default(),
3340 );
3341 aggregator.update(
3342 Price::from("1.00002"),
3343 Quantity::from(1),
3344 UnixNanos::from(1000),
3345 );
3346 aggregator.update(
3347 Price::from("1.00003"),
3348 Quantity::from(1),
3349 UnixNanos::from(2000),
3350 );
3351
3352 let handler_guard = handler.lock();
3353 assert_eq!(handler_guard.len(), 1);
3354
3355 let bar = handler_guard.first().unwrap();
3356 assert_eq!(bar.open, Price::from("1.00001"));
3357 assert_eq!(bar.high, Price::from("1.00003"));
3358 assert_eq!(bar.low, Price::from("1.00001"));
3359 assert_eq!(bar.close, Price::from("1.00003"));
3360 assert_eq!(bar.volume, Quantity::from(3));
3361 }
3362
3363 #[rstest]
3364 fn test_tick_bar_aggregator_resets_after_bar_created(equity_aapl: Equity) {
3365 let instrument = InstrumentAny::Equity(equity_aapl);
3366 let bar_spec = BarSpecification::new(2, BarAggregation::Tick, PriceType::Last);
3367 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3368 let (handler, record) = recording_handler();
3369
3370 let mut aggregator = TickBarAggregator::new(
3371 bar_type,
3372 instrument.price_precision(),
3373 instrument.size_precision(),
3374 record,
3375 );
3376
3377 aggregator.update(
3378 Price::from("1.00001"),
3379 Quantity::from(1),
3380 UnixNanos::default(),
3381 );
3382 aggregator.update(
3383 Price::from("1.00002"),
3384 Quantity::from(1),
3385 UnixNanos::from(1000),
3386 );
3387 aggregator.update(
3388 Price::from("1.00003"),
3389 Quantity::from(1),
3390 UnixNanos::from(2000),
3391 );
3392 aggregator.update(
3393 Price::from("1.00004"),
3394 Quantity::from(1),
3395 UnixNanos::from(3000),
3396 );
3397
3398 let handler_guard = handler.lock();
3399 assert_eq!(handler_guard.len(), 2);
3400
3401 let bar1 = &handler_guard[0];
3402 assert_eq!(bar1.open, Price::from("1.00001"));
3403 assert_eq!(bar1.close, Price::from("1.00002"));
3404 assert_eq!(bar1.volume, Quantity::from(2));
3405
3406 let bar2 = &handler_guard[1];
3407 assert_eq!(bar2.open, Price::from("1.00003"));
3408 assert_eq!(bar2.close, Price::from("1.00004"));
3409 assert_eq!(bar2.volume, Quantity::from(2));
3410 }
3411
3412 #[rstest]
3413 #[case(PriceType::Bid, Price::from("100.00"), Quantity::from(10))]
3414 #[case(PriceType::Ask, Price::from("102.00"), Quantity::from(14))]
3415 #[case(PriceType::Mid, Price::from("101.000"), Quantity::from("12.0"))]
3416 fn test_bar_aggregator_handle_quote_selects_price_and_size(
3417 equity_aapl: Equity,
3418 #[case] price_type: PriceType,
3419 #[case] expected_price: Price,
3420 #[case] expected_size: Quantity,
3421 ) {
3422 let instrument = InstrumentAny::Equity(equity_aapl);
3423 let bar_type = BarType::new(
3424 instrument.id(),
3425 BarSpecification::new(1, BarAggregation::Tick, price_type),
3426 AggregationSource::Internal,
3427 );
3428 let (handler, record) = recording_handler();
3429 let mut aggregator = TickBarAggregator::new(
3430 bar_type,
3431 instrument.price_precision(),
3432 instrument.size_precision(),
3433 record,
3434 );
3435 let ts_init = UnixNanos::from(2_000);
3436 let quote = QuoteTick::new(
3437 instrument.id(),
3438 Price::from("100.00"),
3439 Price::from("102.00"),
3440 Quantity::from(10),
3441 Quantity::from(14),
3442 UnixNanos::from(1_000),
3443 ts_init,
3444 );
3445
3446 aggregator.handle_quote(quote);
3447
3448 let bars = handler.lock();
3449 assert_eq!(bars.len(), 1);
3450 assert_eq!(bars[0].open, expected_price);
3451 assert_eq!(bars[0].high, expected_price);
3452 assert_eq!(bars[0].low, expected_price);
3453 assert_eq!(bars[0].close, expected_price);
3454 assert_eq!(bars[0].volume, expected_size);
3455 assert_eq!(bars[0].ts_event, ts_init);
3456 assert_eq!(bars[0].ts_init, ts_init);
3457 }
3458
3459 #[rstest]
3460 fn test_bar_aggregator_handle_quote_rejects_last_price(equity_aapl: Equity) {
3461 let instrument = InstrumentAny::Equity(equity_aapl);
3462 let bar_type = BarType::new(
3463 instrument.id(),
3464 BarSpecification::new(1, BarAggregation::Tick, PriceType::Last),
3465 AggregationSource::Internal,
3466 );
3467 let (handler, record) = recording_handler();
3468 let mut aggregator = TickBarAggregator::new(
3469 bar_type,
3470 instrument.price_precision(),
3471 instrument.size_precision(),
3472 record,
3473 );
3474
3475 aggregator.handle_quote(QuoteTick::new(
3476 instrument.id(),
3477 Price::from("100.00"),
3478 Price::from("102.00"),
3479 Quantity::from(10),
3480 Quantity::from(14),
3481 UnixNanos::from(1_000),
3482 UnixNanos::from(2_000),
3483 ));
3484
3485 assert!(handler.lock().is_empty());
3486 assert!(!aggregator.core.builder.initialized);
3487 assert_eq!(aggregator.core.builder.count, 0);
3488 assert_eq!(aggregator.core.builder.volume, Quantity::zero(0));
3489 }
3490
3491 #[rstest]
3492 fn test_non_time_bar_aggregators_use_historical_handler(
3493 equity_aapl: Equity,
3494 audusd_sim: CurrencyPair,
3495 ) {
3496 let instrument = InstrumentAny::Equity(equity_aapl);
3497 let instrument_id = instrument.id();
3498 let price_precision = instrument.price_precision();
3499 let size_precision = instrument.size_precision();
3500 let make_sink = |bars: Arc<Mutex<Vec<Bar>>>| {
3501 move |bar: Bar| {
3502 bars.lock().push(bar);
3503 }
3504 };
3505 let make_trade = |price: &str, size: i64, ts: u64| TradeTick {
3506 instrument_id,
3507 price: Price::from(price),
3508 size: Quantity::from(size),
3509 aggressor_side: AggressorSide::Buy,
3510 ts_event: UnixNanos::from(ts),
3511 ts_init: UnixNanos::from(ts),
3512 ..TradeTick::default()
3513 };
3514
3515 macro_rules! assert_historical_sink_receives {
3516 ($name:expr, $aggregator:expr, $update:expr) => {{
3517 let initial_bars = Arc::new(Mutex::new(Vec::new()));
3518 let historical_bars = Arc::new(Mutex::new(Vec::new()));
3519 let mut aggregator = $aggregator(Arc::clone(&initial_bars));
3520 aggregator
3521 .set_historical_mode(true, Box::new(make_sink(Arc::clone(&historical_bars))));
3522 {
3523 let aggregator: &mut dyn BarAggregator = &mut aggregator;
3524 $update(aggregator);
3525 }
3526
3527 assert_eq!(initial_bars.lock().len(), 0, "{}", $name,);
3528 assert_eq!(historical_bars.lock().len(), 1, "{}", $name,);
3529 }};
3530 }
3531
3532 let tick_type = BarType::new(
3533 instrument_id,
3534 BarSpecification::new(1, BarAggregation::Tick, PriceType::Last),
3535 AggregationSource::Internal,
3536 );
3537 assert_historical_sink_receives!(
3538 "TickBarAggregator",
3539 |bars| TickBarAggregator::new(
3540 tick_type,
3541 price_precision,
3542 size_precision,
3543 make_sink(bars)
3544 ),
3545 |aggregator: &mut dyn BarAggregator| {
3546 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3547 }
3548 );
3549
3550 let tick_imbalance_type = BarType::new(
3551 instrument_id,
3552 BarSpecification::new(1, BarAggregation::TickImbalance, PriceType::Last),
3553 AggregationSource::Internal,
3554 );
3555 assert_historical_sink_receives!(
3556 "TickImbalanceBarAggregator",
3557 |bars| TickImbalanceBarAggregator::new(
3558 tick_imbalance_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_runs_type = BarType::new(
3569 instrument_id,
3570 BarSpecification::new(1, BarAggregation::TickRuns, PriceType::Last),
3571 AggregationSource::Internal,
3572 );
3573 assert_historical_sink_receives!(
3574 "TickRunsBarAggregator",
3575 |bars| TickRunsBarAggregator::new(
3576 tick_runs_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 volume_type = BarType::new(
3587 instrument_id,
3588 BarSpecification::new(1, BarAggregation::Volume, PriceType::Last),
3589 AggregationSource::Internal,
3590 );
3591 assert_historical_sink_receives!(
3592 "VolumeBarAggregator",
3593 |bars| VolumeBarAggregator::new(
3594 volume_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_imbalance_type = BarType::new(
3605 instrument_id,
3606 BarSpecification::new(1, BarAggregation::VolumeImbalance, PriceType::Last),
3607 AggregationSource::Internal,
3608 );
3609 assert_historical_sink_receives!(
3610 "VolumeImbalanceBarAggregator",
3611 |bars| VolumeImbalanceBarAggregator::new(
3612 volume_imbalance_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_runs_type = BarType::new(
3623 instrument_id,
3624 BarSpecification::new(1, BarAggregation::VolumeRuns, PriceType::Last),
3625 AggregationSource::Internal,
3626 );
3627 assert_historical_sink_receives!(
3628 "VolumeRunsBarAggregator",
3629 |bars| VolumeRunsBarAggregator::new(
3630 volume_runs_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 value_type = BarType::new(
3641 instrument_id,
3642 BarSpecification::new(100, BarAggregation::Value, PriceType::Last),
3643 AggregationSource::Internal,
3644 );
3645 assert_historical_sink_receives!(
3646 "ValueBarAggregator",
3647 |bars| ValueBarAggregator::new(
3648 value_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_imbalance_type = BarType::new(
3659 instrument_id,
3660 BarSpecification::new(100, BarAggregation::ValueImbalance, PriceType::Last),
3661 AggregationSource::Internal,
3662 );
3663 assert_historical_sink_receives!(
3664 "ValueImbalanceBarAggregator",
3665 |bars| ValueImbalanceBarAggregator::new(
3666 value_imbalance_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_runs_type = BarType::new(
3677 instrument_id,
3678 BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last),
3679 AggregationSource::Internal,
3680 );
3681 assert_historical_sink_receives!(
3682 "ValueRunsBarAggregator",
3683 |bars| ValueRunsBarAggregator::new(
3684 value_runs_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 fx = InstrumentAny::CurrencyPair(audusd_sim);
3695 let renko_type = BarType::new(
3696 fx.id(),
3697 BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid),
3698 AggregationSource::Internal,
3699 );
3700 let fx_price_precision = fx.price_precision();
3701 let fx_size_precision = fx.size_precision();
3702 let fx_price_increment = fx.price_increment();
3703 assert_historical_sink_receives!(
3704 "RenkoBarAggregator",
3705 |bars| RenkoBarAggregator::new(
3706 renko_type,
3707 fx_price_precision,
3708 fx_size_precision,
3709 fx_price_increment,
3710 make_sink(bars),
3711 ),
3712 |aggregator: &mut dyn BarAggregator| {
3713 aggregator.update(
3714 Price::from("1.00000"),
3715 Quantity::from(1),
3716 UnixNanos::from(1_000),
3717 );
3718 aggregator.update(
3719 Price::from("1.00010"),
3720 Quantity::from(1),
3721 UnixNanos::from(2_000),
3722 );
3723 }
3724 );
3725 }
3726
3727 #[rstest]
3728 fn test_tick_imbalance_bar_aggregator_emits_at_threshold(equity_aapl: Equity) {
3729 let instrument = InstrumentAny::Equity(equity_aapl);
3730 let bar_spec = BarSpecification::new(2, BarAggregation::TickImbalance, PriceType::Last);
3731 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3732 let (handler, record) = recording_handler();
3733
3734 let mut aggregator = TickImbalanceBarAggregator::new(
3735 bar_type,
3736 instrument.price_precision(),
3737 instrument.size_precision(),
3738 record,
3739 );
3740
3741 let trade = TradeTick::default();
3742 aggregator.handle_trade(trade);
3743 aggregator.handle_trade(trade);
3744
3745 let handler_guard = handler.lock();
3746 assert_eq!(handler_guard.len(), 1);
3747 let bar = handler_guard.first().unwrap();
3748 assert_eq!(bar.volume, Quantity::from(200000));
3749 }
3750
3751 #[rstest]
3752 fn test_tick_imbalance_bar_aggregator_handles_seller_direction(equity_aapl: Equity) {
3753 let instrument = InstrumentAny::Equity(equity_aapl);
3754 let bar_spec = BarSpecification::new(1, BarAggregation::TickImbalance, PriceType::Last);
3755 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3756 let (handler, record) = recording_handler();
3757
3758 let mut aggregator = TickImbalanceBarAggregator::new(
3759 bar_type,
3760 instrument.price_precision(),
3761 instrument.size_precision(),
3762 record,
3763 );
3764
3765 let sell = TradeTick {
3766 aggressor_side: AggressorSide::Sell,
3767 ..TradeTick::default()
3768 };
3769
3770 aggregator.handle_trade(sell);
3771
3772 let handler_guard = handler.lock();
3773 assert_eq!(handler_guard.len(), 1);
3774 }
3775
3776 #[rstest]
3777 fn test_tick_runs_bar_aggregator_resets_on_side_change(equity_aapl: Equity) {
3778 let instrument = InstrumentAny::Equity(equity_aapl);
3779 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
3780 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3781 let (handler, record) = recording_handler();
3782
3783 let mut aggregator = TickRunsBarAggregator::new(
3784 bar_type,
3785 instrument.price_precision(),
3786 instrument.size_precision(),
3787 record,
3788 );
3789
3790 let buy = TradeTick {
3791 instrument_id: instrument.id(),
3792 price: Price::from("100.00"),
3793 size: Quantity::from(1),
3794 ts_event: UnixNanos::from(1_000),
3795 ts_init: UnixNanos::from(1_000),
3796 ..TradeTick::default()
3797 };
3798 let sell_one = TradeTick {
3799 price: Price::from("200.00"),
3800 size: Quantity::from(2),
3801 aggressor_side: AggressorSide::Sell,
3802 ts_event: UnixNanos::from(2_000),
3803 ts_init: UnixNanos::from(2_000),
3804 ..buy
3805 };
3806 let sell_two = TradeTick {
3807 price: Price::from("201.00"),
3808 size: Quantity::from(3),
3809 ts_event: UnixNanos::from(3_000),
3810 ts_init: UnixNanos::from(3_000),
3811 ..sell_one
3812 };
3813
3814 aggregator.handle_trade(buy);
3815 aggregator.handle_trade(sell_one);
3816 aggregator.handle_trade(sell_two);
3817
3818 let handler_guard = handler.lock();
3819 assert_eq!(handler_guard.len(), 1);
3820 assert_eq!(handler_guard[0].open, Price::from("200.00"));
3821 assert_eq!(handler_guard[0].high, Price::from("201.00"));
3822 assert_eq!(handler_guard[0].low, Price::from("200.00"));
3823 assert_eq!(handler_guard[0].close, Price::from("201.00"));
3824 assert_eq!(handler_guard[0].volume, Quantity::from(5));
3825 assert_eq!(handler_guard[0].ts_event, UnixNanos::from(3_000));
3826 assert_eq!(handler_guard[0].ts_init, UnixNanos::from(3_000));
3827 }
3828
3829 #[rstest]
3830 fn test_tick_runs_bar_aggregator_volume_conservation(equity_aapl: Equity) {
3831 let instrument = InstrumentAny::Equity(equity_aapl);
3832 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
3833 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3834 let (handler, record) = recording_handler();
3835
3836 let mut aggregator = TickRunsBarAggregator::new(
3837 bar_type,
3838 instrument.price_precision(),
3839 instrument.size_precision(),
3840 record,
3841 );
3842
3843 let buy = TradeTick {
3844 size: Quantity::from(1),
3845 ..TradeTick::default()
3846 };
3847 let sell = TradeTick {
3848 aggressor_side: AggressorSide::Sell,
3849 size: Quantity::from(1),
3850 ..buy
3851 };
3852
3853 aggregator.handle_trade(buy);
3854 aggregator.handle_trade(buy);
3855 aggregator.handle_trade(sell);
3856 aggregator.handle_trade(sell);
3857
3858 let handler_guard = handler.lock();
3859 assert_eq!(handler_guard.len(), 2);
3860 assert_eq!(handler_guard[0].volume, Quantity::from(2));
3861 assert_eq!(handler_guard[1].volume, Quantity::from(2));
3862 }
3863
3864 #[rstest]
3865 fn test_volume_bar_aggregator_builds_multiple_bars_from_large_update(equity_aapl: Equity) {
3866 let instrument = InstrumentAny::Equity(equity_aapl);
3867 let bar_spec = BarSpecification::new(10, BarAggregation::Volume, PriceType::Last);
3868 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3869 let (handler, record) = recording_handler();
3870
3871 let mut aggregator = VolumeBarAggregator::new(
3872 bar_type,
3873 instrument.price_precision(),
3874 instrument.size_precision(),
3875 record,
3876 );
3877
3878 aggregator.update(
3879 Price::from("1.00001"),
3880 Quantity::from(25),
3881 UnixNanos::default(),
3882 );
3883
3884 let handler_guard = handler.lock();
3885 assert_eq!(handler_guard.len(), 2);
3886 let bar1 = &handler_guard[0];
3887 assert_eq!(bar1.volume, Quantity::from(10));
3888 let bar2 = &handler_guard[1];
3889 assert_eq!(bar2.volume, Quantity::from(10));
3890 }
3891
3892 #[rstest]
3893 fn test_volume_bar_aggregator_zero_size_update_is_noop(equity_aapl: Equity) {
3894 let instrument = InstrumentAny::Equity(equity_aapl);
3895 let bar_spec = BarSpecification::new(10, BarAggregation::Volume, PriceType::Last);
3896 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3897 let (handler, record) = recording_handler();
3898
3899 let mut aggregator = VolumeBarAggregator::new(
3900 bar_type,
3901 instrument.price_precision(),
3902 instrument.size_precision(),
3903 record,
3904 );
3905
3906 aggregator.update(
3907 Price::from("100.00"),
3908 Quantity::from(0),
3909 UnixNanos::default(),
3910 );
3911
3912 let handler_guard = handler.lock();
3913 assert_eq!(handler_guard.len(), 0);
3914 }
3915
3916 #[rstest]
3917 fn test_volume_bar_aggregator_ignores_out_of_order_update(equity_aapl: Equity) {
3918 let instrument = InstrumentAny::Equity(equity_aapl);
3919 let bar_spec = BarSpecification::new(2, BarAggregation::Volume, PriceType::Last);
3920 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3921 let (handler, record) = recording_handler();
3922
3923 let mut aggregator = VolumeBarAggregator::new(
3924 bar_type,
3925 instrument.price_precision(),
3926 instrument.size_precision(),
3927 record,
3928 );
3929
3930 aggregator.update(
3931 Price::from("100.00"),
3932 Quantity::from(1),
3933 UnixNanos::from(1_000),
3934 );
3935 aggregator.update(
3936 Price::from("200.00"),
3937 Quantity::from(3),
3938 UnixNanos::from(500),
3939 );
3940
3941 let handler_guard = handler.lock();
3942 assert!(handler_guard.is_empty());
3943 assert_eq!(aggregator.core.builder.count, 1);
3944 assert_eq!(aggregator.core.builder.volume, Quantity::from(1));
3945 assert_eq!(aggregator.core.builder.close, Some(Price::from("100.00")));
3946 assert_eq!(aggregator.core.builder.ts_last, UnixNanos::from(1_000));
3947 }
3948
3949 #[rstest]
3950 fn test_volume_bar_aggregator_ignores_out_of_order_bar(equity_aapl: Equity) {
3951 let instrument = InstrumentAny::Equity(equity_aapl);
3952 let bar_spec = BarSpecification::new(2, BarAggregation::Volume, PriceType::Last);
3953 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3954 let (handler, record) = recording_handler();
3955
3956 let mut aggregator = VolumeBarAggregator::new(
3957 bar_type,
3958 instrument.price_precision(),
3959 instrument.size_precision(),
3960 record,
3961 );
3962
3963 aggregator.update(
3964 Price::from("100.00"),
3965 Quantity::from(1),
3966 UnixNanos::from(1_000),
3967 );
3968 let stale_bar = Bar::new(
3969 bar_type,
3970 Price::from("200.00"),
3971 Price::from("201.00"),
3972 Price::from("199.00"),
3973 Price::from("200.50"),
3974 Quantity::from(3),
3975 UnixNanos::from(500),
3976 UnixNanos::from(500),
3977 );
3978 aggregator.update_bar(stale_bar, stale_bar.volume, stale_bar.ts_init);
3979
3980 let handler_guard = handler.lock();
3981 assert!(handler_guard.is_empty());
3982 assert_eq!(aggregator.core.builder.count, 1);
3983 assert_eq!(aggregator.core.builder.volume, Quantity::from(1));
3984 assert_eq!(aggregator.core.builder.close, Some(Price::from("100.00")));
3985 assert_eq!(aggregator.core.builder.ts_last, UnixNanos::from(1_000));
3986 }
3987
3988 #[rstest]
3989 fn test_volume_imbalance_bar_aggregator_ignores_out_of_order_trade(equity_aapl: Equity) {
3990 let instrument = InstrumentAny::Equity(equity_aapl);
3991 let bar_spec = BarSpecification::new(2, BarAggregation::VolumeImbalance, PriceType::Last);
3992 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3993 let (handler, record) = recording_handler();
3994 let mut aggregator = VolumeImbalanceBarAggregator::new(
3995 bar_type,
3996 instrument.price_precision(),
3997 instrument.size_precision(),
3998 record,
3999 );
4000 let first = TradeTick {
4001 price: Price::from("100.00"),
4002 size: Quantity::from(1),
4003 aggressor_side: AggressorSide::Buy,
4004 ts_init: UnixNanos::from(1_000),
4005 ..TradeTick::default()
4006 };
4007 let stale = TradeTick {
4008 price: Price::from("200.00"),
4009 size: Quantity::from(2),
4010 aggressor_side: AggressorSide::Buy,
4011 ts_init: UnixNanos::from(500),
4012 ..TradeTick::default()
4013 };
4014
4015 aggregator.handle_trade(first);
4016 aggregator.handle_trade(stale);
4017
4018 assert!(handler.lock().is_empty());
4019 assert_eq!(aggregator.imbalance, Quantity::from(1));
4020 assert_eq!(aggregator.imbalance_side, AggressorSide::Buy);
4021 assert_eq!(aggregator.core.builder.volume, Quantity::from(1));
4022 assert_eq!(aggregator.core.builder.ts_last, UnixNanos::from(1_000));
4023 }
4024
4025 #[rstest]
4026 #[case(BarAggregation::TickImbalance)]
4027 #[case(BarAggregation::TickRuns)]
4028 #[case(BarAggregation::VolumeRuns)]
4029 #[case(BarAggregation::ValueImbalance)]
4030 #[case(BarAggregation::ValueRuns)]
4031 fn test_stateful_trade_aggregators_ignore_out_of_order_trade(
4032 equity_aapl: Equity,
4033 #[case] aggregation: BarAggregation,
4034 ) {
4035 let instrument = InstrumentAny::Equity(equity_aapl);
4036 let (step, price) = match aggregation {
4037 BarAggregation::ValueImbalance | BarAggregation::ValueRuns => {
4038 (100, Price::from("50.00"))
4039 }
4040 _ => (2, Price::from("100.00")),
4041 };
4042 let bar_type = BarType::new(
4043 instrument.id(),
4044 BarSpecification::new(step, aggregation, PriceType::Last),
4045 AggregationSource::Internal,
4046 );
4047 let (handler, record) = recording_handler();
4048 let make_handler = record;
4049 let mut aggregator: Box<dyn BarAggregator> = match aggregation {
4050 BarAggregation::TickImbalance => Box::new(TickImbalanceBarAggregator::new(
4051 bar_type,
4052 instrument.price_precision(),
4053 instrument.size_precision(),
4054 make_handler,
4055 )),
4056 BarAggregation::TickRuns => Box::new(TickRunsBarAggregator::new(
4057 bar_type,
4058 instrument.price_precision(),
4059 instrument.size_precision(),
4060 make_handler,
4061 )),
4062 BarAggregation::VolumeRuns => Box::new(VolumeRunsBarAggregator::new(
4063 bar_type,
4064 instrument.price_precision(),
4065 instrument.size_precision(),
4066 make_handler,
4067 )),
4068 BarAggregation::ValueImbalance => Box::new(ValueImbalanceBarAggregator::new(
4069 bar_type,
4070 instrument.price_precision(),
4071 instrument.size_precision(),
4072 make_handler,
4073 )),
4074 BarAggregation::ValueRuns => Box::new(ValueRunsBarAggregator::new(
4075 bar_type,
4076 instrument.price_precision(),
4077 instrument.size_precision(),
4078 make_handler,
4079 )),
4080 _ => unreachable!(),
4081 };
4082 let first = TradeTick {
4083 instrument_id: instrument.id(),
4084 price,
4085 size: Quantity::from(1),
4086 aggressor_side: AggressorSide::Buy,
4087 ts_event: UnixNanos::from(1_000),
4088 ts_init: UnixNanos::from(1_000),
4089 ..TradeTick::default()
4090 };
4091 let stale = TradeTick {
4092 price: Price::from("999.00"),
4093 ts_event: UnixNanos::from(500),
4094 ts_init: UnixNanos::from(500),
4095 ..first
4096 };
4097 let second = TradeTick {
4098 ts_event: UnixNanos::from(2_000),
4099 ts_init: UnixNanos::from(2_000),
4100 ..first
4101 };
4102
4103 aggregator.handle_trade(first);
4104 aggregator.handle_trade(stale);
4105 aggregator.handle_trade(second);
4106
4107 let bars = handler.lock();
4108 assert_eq!(bars.len(), 1);
4109 assert_eq!(bars[0].open, price);
4110 assert_eq!(bars[0].high, price);
4111 assert_eq!(bars[0].low, price);
4112 assert_eq!(bars[0].close, price);
4113 assert_eq!(bars[0].volume, Quantity::from(2));
4114 assert_eq!(bars[0].ts_event, UnixNanos::from(2_000));
4115 assert_eq!(bars[0].ts_init, UnixNanos::from(2_000));
4116 }
4117
4118 #[rstest]
4119 fn test_volume_bar_aggregator_exact_threshold_emits_single_bar(equity_aapl: Equity) {
4120 let instrument = InstrumentAny::Equity(equity_aapl);
4121 let bar_spec = BarSpecification::new(10, BarAggregation::Volume, PriceType::Last);
4122 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4123 let (handler, record) = recording_handler();
4124
4125 let mut aggregator = VolumeBarAggregator::new(
4126 bar_type,
4127 instrument.price_precision(),
4128 instrument.size_precision(),
4129 record,
4130 );
4131
4132 aggregator.update(
4133 Price::from("100.00"),
4134 Quantity::from(7),
4135 UnixNanos::from(1_000),
4136 );
4137 aggregator.update(
4138 Price::from("101.00"),
4139 Quantity::from(3),
4140 UnixNanos::from(2_000),
4141 );
4142
4143 let handler_guard = handler.lock();
4144 assert_eq!(handler_guard.len(), 1);
4145 assert_eq!(handler_guard[0].volume, Quantity::from(10));
4146 assert_eq!(handler_guard[0].close, Price::from("101.00"));
4147 }
4148
4149 #[rstest]
4150 fn test_volume_bar_aggregator_step_of_one_emits_per_unit(equity_aapl: Equity) {
4151 let instrument = InstrumentAny::Equity(equity_aapl);
4152 let bar_spec = BarSpecification::new(1, BarAggregation::Volume, PriceType::Last);
4153 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4154 let (handler, record) = recording_handler();
4155
4156 let mut aggregator = VolumeBarAggregator::new(
4157 bar_type,
4158 instrument.price_precision(),
4159 instrument.size_precision(),
4160 record,
4161 );
4162
4163 aggregator.update(
4164 Price::from("100.00"),
4165 Quantity::from(1),
4166 UnixNanos::default(),
4167 );
4168
4169 let handler_guard = handler.lock();
4170 assert_eq!(handler_guard.len(), 1);
4171 assert_eq!(handler_guard[0].volume, Quantity::from(1));
4172 }
4173
4174 #[rstest]
4175 fn test_volume_runs_bar_aggregator_side_change_resets(equity_aapl: Equity) {
4176 let instrument = InstrumentAny::Equity(equity_aapl);
4177 let bar_spec = BarSpecification::new(10, BarAggregation::VolumeRuns, PriceType::Last);
4178 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4179 let (handler, record) = recording_handler();
4180
4181 let mut aggregator = VolumeRunsBarAggregator::new(
4182 bar_type,
4183 instrument.price_precision(),
4184 instrument.size_precision(),
4185 record,
4186 );
4187
4188 let buy = TradeTick {
4189 instrument_id: instrument.id(),
4190 price: Price::from("100.00"),
4191 size: Quantity::from(4),
4192 ts_event: UnixNanos::from(1_000),
4193 ts_init: UnixNanos::from(1_000),
4194 ..TradeTick::default()
4195 };
4196 let sell_one = TradeTick {
4197 price: Price::from("200.00"),
4198 size: Quantity::from(6),
4199 aggressor_side: AggressorSide::Sell,
4200 ts_event: UnixNanos::from(2_000),
4201 ts_init: UnixNanos::from(2_000),
4202 ..buy
4203 };
4204 let sell_two = TradeTick {
4205 price: Price::from("201.00"),
4206 size: Quantity::from(4),
4207 ts_event: UnixNanos::from(3_000),
4208 ts_init: UnixNanos::from(3_000),
4209 ..sell_one
4210 };
4211
4212 aggregator.handle_trade(buy);
4213 aggregator.handle_trade(sell_one);
4214 aggregator.handle_trade(sell_two);
4215
4216 let handler_guard = handler.lock();
4217 assert_eq!(handler_guard.len(), 1);
4218 assert_eq!(handler_guard[0].open, Price::from("200.00"));
4219 assert_eq!(handler_guard[0].high, Price::from("201.00"));
4220 assert_eq!(handler_guard[0].low, Price::from("200.00"));
4221 assert_eq!(handler_guard[0].close, Price::from("201.00"));
4222 assert_eq!(handler_guard[0].volume, Quantity::from(10));
4223 assert_eq!(handler_guard[0].ts_event, UnixNanos::from(3_000));
4224 assert_eq!(handler_guard[0].ts_init, UnixNanos::from(3_000));
4225 }
4226
4227 #[rstest]
4228 fn test_volume_runs_bar_aggregator_handles_large_single_trade(equity_aapl: Equity) {
4229 let instrument = InstrumentAny::Equity(equity_aapl);
4230 let bar_spec = BarSpecification::new(3, BarAggregation::VolumeRuns, PriceType::Last);
4231 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4232 let (handler, record) = recording_handler();
4233
4234 let mut aggregator = VolumeRunsBarAggregator::new(
4235 bar_type,
4236 instrument.price_precision(),
4237 instrument.size_precision(),
4238 record,
4239 );
4240
4241 let trade = TradeTick {
4242 instrument_id: instrument.id(),
4243 price: Price::from("1.0"),
4244 size: Quantity::from(5),
4245 ..TradeTick::default()
4246 };
4247
4248 aggregator.handle_trade(trade);
4249
4250 let handler_guard = handler.lock();
4251 assert!(!handler_guard.is_empty());
4252 assert!(handler_guard[0].volume.as_f64() > 0.0);
4253 assert!(handler_guard[0].volume.as_f64() < trade.size.as_f64());
4254 }
4255
4256 #[rstest]
4257 fn test_volume_imbalance_bar_aggregator_splits_large_trade(equity_aapl: Equity) {
4258 let instrument = InstrumentAny::Equity(equity_aapl);
4259 let bar_spec = BarSpecification::new(2, BarAggregation::VolumeImbalance, PriceType::Last);
4260 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4261 let (handler, record) = recording_handler();
4262
4263 let mut aggregator = VolumeImbalanceBarAggregator::new(
4264 bar_type,
4265 instrument.price_precision(),
4266 instrument.size_precision(),
4267 record,
4268 );
4269
4270 let trade_small = TradeTick {
4271 instrument_id: instrument.id(),
4272 price: Price::from("1.0"),
4273 size: Quantity::from(1),
4274 ..TradeTick::default()
4275 };
4276 let trade_large = TradeTick {
4277 size: Quantity::from(3),
4278 ..trade_small
4279 };
4280
4281 aggregator.handle_trade(trade_small);
4282 aggregator.handle_trade(trade_large);
4283
4284 let handler_guard = handler.lock();
4285 assert_eq!(handler_guard.len(), 2);
4286 let total_output = handler_guard
4287 .iter()
4288 .map(|bar| bar.volume.as_f64())
4289 .sum::<f64>();
4290 let total_input = trade_small.size.as_f64() + trade_large.size.as_f64();
4291 assert!((total_output - total_input).abs() < f64::EPSILON);
4292 }
4293
4294 #[rstest]
4295 fn test_value_bar_aggregator_builds_at_value_threshold(equity_aapl: Equity) {
4296 let instrument = InstrumentAny::Equity(equity_aapl);
4297 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4299 let (handler, record) = recording_handler();
4300
4301 let mut aggregator = ValueBarAggregator::new(
4302 bar_type,
4303 instrument.price_precision(),
4304 instrument.size_precision(),
4305 record,
4306 );
4307
4308 aggregator.update(
4310 Price::from("100.00"),
4311 Quantity::from(5),
4312 UnixNanos::default(),
4313 );
4314 aggregator.update(
4315 Price::from("100.00"),
4316 Quantity::from(5),
4317 UnixNanos::from(1000),
4318 );
4319
4320 let handler_guard = handler.lock();
4321 assert_eq!(handler_guard.len(), 1);
4322 let bar = handler_guard.first().unwrap();
4323 assert_eq!(bar.volume, Quantity::from(10));
4324 }
4325
4326 #[rstest]
4327 fn test_value_bar_aggregator_handles_large_update(equity_aapl: Equity) {
4328 let instrument = InstrumentAny::Equity(equity_aapl);
4329 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last);
4330 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4331 let (handler, record) = recording_handler();
4332
4333 let mut aggregator = ValueBarAggregator::new(
4334 bar_type,
4335 instrument.price_precision(),
4336 instrument.size_precision(),
4337 record,
4338 );
4339
4340 aggregator.update(
4342 Price::from("100.00"),
4343 Quantity::from(25),
4344 UnixNanos::default(),
4345 );
4346
4347 let handler_guard = handler.lock();
4348 assert_eq!(handler_guard.len(), 2);
4349 let remaining_value = aggregator.get_cumulative_value();
4350 assert!(remaining_value < Decimal::from(1_000)); }
4352
4353 #[rstest]
4354 fn test_value_bar_aggregator_handles_zero_price(equity_aapl: Equity) {
4355 let instrument = InstrumentAny::Equity(equity_aapl);
4356 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last);
4357 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4358 let (handler, record) = recording_handler();
4359
4360 let mut aggregator = ValueBarAggregator::new(
4361 bar_type,
4362 instrument.price_precision(),
4363 instrument.size_precision(),
4364 record,
4365 );
4366
4367 aggregator.update(
4369 Price::from("0.00"),
4370 Quantity::from(100),
4371 UnixNanos::default(),
4372 );
4373
4374 let handler_guard = handler.lock();
4376 assert_eq!(handler_guard.len(), 0);
4377
4378 assert_eq!(aggregator.get_cumulative_value(), Decimal::ZERO);
4380 }
4381
4382 #[rstest]
4383 fn test_value_bar_aggregator_handles_zero_size(equity_aapl: Equity) {
4384 let instrument = InstrumentAny::Equity(equity_aapl);
4385 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last);
4386 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4387 let (handler, record) = recording_handler();
4388
4389 let mut aggregator = ValueBarAggregator::new(
4390 bar_type,
4391 instrument.price_precision(),
4392 instrument.size_precision(),
4393 record,
4394 );
4395
4396 aggregator.update(
4398 Price::from("100.00"),
4399 Quantity::from(0),
4400 UnixNanos::default(),
4401 );
4402
4403 let handler_guard = handler.lock();
4405 assert_eq!(handler_guard.len(), 0);
4406
4407 assert_eq!(aggregator.get_cumulative_value(), Decimal::ZERO);
4409 }
4410
4411 #[rstest]
4412 fn test_value_bar_aggregator_conserves_volume_across_rounded_chunks(equity_aapl: Equity) {
4413 let instrument = InstrumentAny::Equity(equity_aapl);
4414 let bar_spec = BarSpecification::new(10, BarAggregation::Value, PriceType::Last);
4415 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4416 let (handler, record) = recording_handler();
4417
4418 let mut aggregator = ValueBarAggregator::new(
4419 bar_type,
4420 instrument.price_precision(),
4421 instrument.size_precision(),
4422 record,
4423 );
4424
4425 aggregator.update(
4428 Price::from("3.00"),
4429 Quantity::from(10),
4430 UnixNanos::from(1_000),
4431 );
4432
4433 let handler_guard = handler.lock();
4434 assert_eq!(handler_guard.len(), 3);
4435 for bar in handler_guard.iter() {
4436 assert_eq!(bar.volume, Quantity::from(3));
4437 }
4438 assert_eq!(aggregator.core.builder.volume, Quantity::from(1));
4439 }
4440
4441 #[rstest]
4442 fn test_value_bar_aggregator_update_bar_conserves_volume_across_rounded_chunks(
4443 equity_aapl: Equity,
4444 ) {
4445 let instrument = InstrumentAny::Equity(equity_aapl);
4446 let bar_spec = BarSpecification::new(10, BarAggregation::Value, PriceType::Last);
4447 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4448 let (handler, record) = recording_handler();
4449
4450 let mut aggregator = ValueBarAggregator::new(
4451 bar_type,
4452 instrument.price_precision(),
4453 instrument.size_precision(),
4454 record,
4455 );
4456
4457 let input_bar = Bar::new(
4459 bar_type,
4460 Price::from("3.00"),
4461 Price::from("3.00"),
4462 Price::from("3.00"),
4463 Price::from("3.00"),
4464 Quantity::from(10),
4465 UnixNanos::from(1_000),
4466 UnixNanos::from(1_000),
4467 );
4468 aggregator.handle_bar(input_bar);
4469
4470 let handler_guard = handler.lock();
4471 assert_eq!(handler_guard.len(), 3);
4472 for bar in handler_guard.iter() {
4473 assert_eq!(bar.volume, Quantity::from(3));
4474 }
4475 assert_eq!(aggregator.core.builder.volume, Quantity::from(1));
4476 }
4477
4478 #[rstest]
4479 fn test_value_bar_aggregator_exact_threshold_emits_one_bar(equity_aapl: Equity) {
4480 let instrument = InstrumentAny::Equity(equity_aapl);
4481 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last);
4482 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4483 let (handler, record) = recording_handler();
4484
4485 let mut aggregator = ValueBarAggregator::new(
4486 bar_type,
4487 instrument.price_precision(),
4488 instrument.size_precision(),
4489 record,
4490 );
4491
4492 aggregator.update(
4493 Price::from("100.00"),
4494 Quantity::from(5),
4495 UnixNanos::from(1_000),
4496 );
4497 aggregator.update(
4498 Price::from("100.00"),
4499 Quantity::from(5),
4500 UnixNanos::from(2_000),
4501 );
4502
4503 let handler_guard = handler.lock();
4504 assert_eq!(handler_guard.len(), 1);
4505 assert_eq!(handler_guard[0].volume, Quantity::from(10));
4506 assert_eq!(aggregator.get_cumulative_value(), Decimal::ZERO);
4507 }
4508
4509 #[rstest]
4510 fn test_value_bar_aggregator_precision_boundary_min_size_clamp(equity_aapl: Equity) {
4511 let instrument = InstrumentAny::Equity(equity_aapl);
4515 let bar_spec = BarSpecification::new(100, BarAggregation::Value, PriceType::Last);
4516 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4517 let (handler, record) = recording_handler();
4518
4519 let mut aggregator = ValueBarAggregator::new(
4520 bar_type,
4521 instrument.price_precision(),
4522 instrument.size_precision(),
4523 record,
4524 );
4525
4526 aggregator.update(
4528 Price::from("100.00"),
4529 Quantity::from(4),
4530 UnixNanos::default(),
4531 );
4532
4533 let handler_guard = handler.lock();
4534 assert_eq!(handler_guard.len(), 4);
4535 for bar in handler_guard.iter() {
4536 assert_eq!(bar.volume, Quantity::from(1));
4537 }
4538 }
4539
4540 #[rstest]
4541 fn test_value_imbalance_bar_aggregator_emits_on_opposing_overflow(equity_aapl: Equity) {
4542 let instrument = InstrumentAny::Equity(equity_aapl);
4543 let bar_spec = BarSpecification::new(10, BarAggregation::ValueImbalance, PriceType::Last);
4544 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4545 let (handler, record) = recording_handler();
4546
4547 let mut aggregator = ValueImbalanceBarAggregator::new(
4548 bar_type,
4549 instrument.price_precision(),
4550 instrument.size_precision(),
4551 record,
4552 );
4553
4554 let buy = TradeTick {
4555 price: Price::from("5.0"),
4556 size: Quantity::from(2), instrument_id: instrument.id(),
4558 ..TradeTick::default()
4559 };
4560 let sell = TradeTick {
4561 price: Price::from("5.0"),
4562 size: Quantity::from(2), aggressor_side: AggressorSide::Sell,
4564 instrument_id: instrument.id(),
4565 ..buy
4566 };
4567
4568 aggregator.handle_trade(buy);
4569 aggregator.handle_trade(sell);
4570
4571 let handler_guard = handler.lock();
4572 assert_eq!(handler_guard.len(), 2);
4573 }
4574
4575 #[rstest]
4576 fn test_value_runs_bar_aggregator_emits_on_consecutive_side(equity_aapl: Equity) {
4577 let instrument = InstrumentAny::Equity(equity_aapl);
4578 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
4579 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4580 let (handler, record) = recording_handler();
4581
4582 let mut aggregator = ValueRunsBarAggregator::new(
4583 bar_type,
4584 instrument.price_precision(),
4585 instrument.size_precision(),
4586 record,
4587 );
4588
4589 let trade = TradeTick {
4590 price: Price::from("10.0"),
4591 size: Quantity::from(5),
4592 instrument_id: instrument.id(),
4593 ..TradeTick::default()
4594 };
4595
4596 aggregator.handle_trade(trade);
4597 aggregator.handle_trade(trade);
4598
4599 let handler_guard = handler.lock();
4600 assert_eq!(handler_guard.len(), 1);
4601 let bar = handler_guard.first().unwrap();
4602 assert_eq!(bar.volume, Quantity::from(10));
4603 }
4604
4605 #[rstest]
4606 fn test_value_runs_bar_aggregator_resets_on_side_change(equity_aapl: Equity) {
4607 let instrument = InstrumentAny::Equity(equity_aapl);
4608 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
4609 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4610 let (handler, record) = recording_handler();
4611
4612 let mut aggregator = ValueRunsBarAggregator::new(
4613 bar_type,
4614 instrument.price_precision(),
4615 instrument.size_precision(),
4616 record,
4617 );
4618
4619 let buy = TradeTick {
4620 price: Price::from("10.0"),
4621 size: Quantity::from(5),
4622 instrument_id: instrument.id(),
4623 ..TradeTick::default()
4624 }; let sell = TradeTick {
4626 price: Price::from("10.0"),
4627 size: Quantity::from(10),
4628 aggressor_side: AggressorSide::Sell,
4629 ..buy
4630 }; aggregator.handle_trade(buy);
4633 aggregator.handle_trade(sell);
4634
4635 let handler_guard = handler.lock();
4636 assert_eq!(handler_guard.len(), 1);
4637 assert_eq!(handler_guard[0].volume, Quantity::from(10));
4638 }
4639
4640 #[rstest]
4641 fn test_tick_runs_bar_aggregator_continues_run_after_bar_emission(equity_aapl: Equity) {
4642 let instrument = InstrumentAny::Equity(equity_aapl);
4643 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
4644 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4645 let (handler, record) = recording_handler();
4646
4647 let mut aggregator = TickRunsBarAggregator::new(
4648 bar_type,
4649 instrument.price_precision(),
4650 instrument.size_precision(),
4651 record,
4652 );
4653
4654 let buy = TradeTick::default();
4655
4656 aggregator.handle_trade(buy);
4657 aggregator.handle_trade(buy); aggregator.handle_trade(buy); aggregator.handle_trade(buy); let handler_guard = handler.lock();
4662 assert_eq!(handler_guard.len(), 2);
4663 }
4664
4665 #[rstest]
4666 fn test_tick_runs_bar_aggregator_handles_no_aggressor_trades(equity_aapl: Equity) {
4667 let instrument = InstrumentAny::Equity(equity_aapl);
4668 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
4669 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4670 let (handler, record) = recording_handler();
4671
4672 let mut aggregator = TickRunsBarAggregator::new(
4673 bar_type,
4674 instrument.price_precision(),
4675 instrument.size_precision(),
4676 record,
4677 );
4678
4679 let buy = TradeTick::default();
4680 let no_aggressor = TradeTick {
4681 aggressor_side: AggressorSide::NoAggressor,
4682 ..buy
4683 };
4684
4685 aggregator.handle_trade(buy);
4686 aggregator.handle_trade(no_aggressor); aggregator.handle_trade(no_aggressor); aggregator.handle_trade(buy); let handler_guard = handler.lock();
4691 assert_eq!(handler_guard.len(), 1);
4692 }
4693
4694 #[rstest]
4695 fn test_volume_runs_bar_aggregator_continues_run_after_bar_emission(equity_aapl: Equity) {
4696 let instrument = InstrumentAny::Equity(equity_aapl);
4697 let bar_spec = BarSpecification::new(2, BarAggregation::VolumeRuns, PriceType::Last);
4698 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4699 let (handler, record) = recording_handler();
4700
4701 let mut aggregator = VolumeRunsBarAggregator::new(
4702 bar_type,
4703 instrument.price_precision(),
4704 instrument.size_precision(),
4705 record,
4706 );
4707
4708 let buy = TradeTick {
4709 instrument_id: instrument.id(),
4710 price: Price::from("1.0"),
4711 size: Quantity::from(1),
4712 ..TradeTick::default()
4713 };
4714
4715 aggregator.handle_trade(buy);
4716 aggregator.handle_trade(buy); aggregator.handle_trade(buy); aggregator.handle_trade(buy); let handler_guard = handler.lock();
4721 assert_eq!(handler_guard.len(), 2);
4722 assert_eq!(handler_guard[0].volume, Quantity::from(2));
4723 assert_eq!(handler_guard[1].volume, Quantity::from(2));
4724 }
4725
4726 #[rstest]
4727 fn test_value_runs_bar_aggregator_continues_run_after_bar_emission(equity_aapl: Equity) {
4728 let instrument = InstrumentAny::Equity(equity_aapl);
4729 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
4730 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4731 let (handler, record) = recording_handler();
4732
4733 let mut aggregator = ValueRunsBarAggregator::new(
4734 bar_type,
4735 instrument.price_precision(),
4736 instrument.size_precision(),
4737 record,
4738 );
4739
4740 let buy = TradeTick {
4741 instrument_id: instrument.id(),
4742 price: Price::from("10.0"),
4743 size: Quantity::from(5),
4744 ..TradeTick::default()
4745 }; aggregator.handle_trade(buy);
4748 aggregator.handle_trade(buy); aggregator.handle_trade(buy); aggregator.handle_trade(buy); let handler_guard = handler.lock();
4753 assert_eq!(handler_guard.len(), 2);
4754 assert_eq!(handler_guard[0].volume, Quantity::from(10));
4755 assert_eq!(handler_guard[1].volume, Quantity::from(10));
4756 }
4757
4758 #[rstest]
4759 fn test_time_bar_aggregator_builds_at_interval(equity_aapl: Equity) {
4760 let instrument = InstrumentAny::Equity(equity_aapl);
4761 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
4763 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4764 let (handler, record) = recording_handler();
4765 let clock = Rc::new(RefCell::new(TestClock::new()));
4766
4767 let mut aggregator = TimeBarAggregator::new(
4768 bar_type,
4769 instrument.price_precision(),
4770 instrument.size_precision(),
4771 clock.clone(),
4772 record,
4773 true, false, BarIntervalType::LeftOpen,
4776 None, 15, false, );
4780
4781 aggregator.update(
4782 Price::from("100.00"),
4783 Quantity::from(1),
4784 UnixNanos::default(),
4785 );
4786
4787 let next_sec = UnixNanos::from(1_000_000_000);
4788 clock.borrow_mut().set_time(next_sec);
4789
4790 let event = TimeEvent::new(
4791 Ustr::from("1-SECOND-LAST"),
4792 UUID4::new(),
4793 next_sec,
4794 next_sec,
4795 );
4796 aggregator.build_bar(&event);
4797
4798 let handler_guard = handler.lock();
4799 assert_eq!(handler_guard.len(), 1);
4800 let bar = handler_guard.first().unwrap();
4801 assert_eq!(bar.ts_event, UnixNanos::default());
4802 assert_eq!(bar.ts_init, next_sec);
4803 }
4804
4805 #[rstest]
4806 fn test_time_bar_aggregator_stop_clears_timer_and_allows_restart(equity_aapl: Equity) {
4807 let instrument = InstrumentAny::Equity(equity_aapl);
4808 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
4809 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4810 let timer_name = format!("TIME_BAR_{bar_type}");
4811 let clock = Rc::new(RefCell::new(TestClock::new()));
4812
4813 let aggregator = TimeBarAggregator::new(
4814 bar_type,
4815 instrument.price_precision(),
4816 instrument.size_precision(),
4817 clock.clone(),
4818 |_bar: Bar| {},
4819 true,
4820 false,
4821 BarIntervalType::LeftOpen,
4822 None,
4823 15,
4824 false,
4825 );
4826
4827 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
4828 let rc = Rc::new(RefCell::new(boxed));
4829
4830 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
4831 assert_eq!(clock.borrow().timer_names(), vec![timer_name.as_str()]);
4832
4833 rc.borrow_mut().stop();
4834 assert!(clock.borrow().timer_names().is_empty());
4835
4836 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
4837 assert_eq!(clock.borrow().timer_names(), vec![timer_name.as_str()]);
4838 }
4839
4840 #[rstest]
4841 fn test_time_bar_aggregator_accepts_interval_above_i64_nanos(equity_aapl: Equity) {
4842 let instrument = InstrumentAny::Equity(equity_aapl);
4843 let bar_spec = BarSpecification::new(106_752, BarAggregation::Day, PriceType::Last);
4844 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4845 let interval_ns = get_bar_interval_ns(&bar_type);
4846 let timer_name = format!("TIME_BAR_{bar_type}");
4847 let clock = Rc::new(RefCell::new(TestClock::new()));
4848 clock.borrow_mut().set_time(UnixNanos::from(1));
4849 let aggregator = TimeBarAggregator::new(
4850 bar_type,
4851 instrument.price_precision(),
4852 instrument.size_precision(),
4853 clock.clone(),
4854 |_bar: Bar| {},
4855 true,
4856 false,
4857 BarIntervalType::LeftOpen,
4858 None,
4859 0,
4860 false,
4861 );
4862 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
4863 let rc = Rc::new(RefCell::new(boxed));
4864
4865 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
4866
4867 assert_eq!(
4868 clock.borrow().next_time_ns(&timer_name),
4869 UnixNanos::from(1).checked_add(interval_ns)
4870 );
4871 }
4872
4873 #[rstest]
4874 fn test_time_bar_aggregator_left_open_interval(equity_aapl: Equity) {
4875 let instrument = InstrumentAny::Equity(equity_aapl);
4876 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
4877 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4878 let (handler, record) = recording_handler();
4879 let clock = Rc::new(RefCell::new(TestClock::new()));
4880
4881 let mut aggregator = TimeBarAggregator::new(
4882 bar_type,
4883 instrument.price_precision(),
4884 instrument.size_precision(),
4885 clock.clone(),
4886 record,
4887 true, true, BarIntervalType::LeftOpen,
4890 None,
4891 15,
4892 false, );
4894
4895 aggregator.update(
4897 Price::from("100.00"),
4898 Quantity::from(1),
4899 UnixNanos::default(),
4900 );
4901
4902 let ts1 = UnixNanos::from(1_000_000_000);
4904 clock.borrow_mut().set_time(ts1);
4905 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts1, ts1);
4906 aggregator.build_bar(&event);
4907
4908 aggregator.update(Price::from("101.00"), Quantity::from(1), ts1);
4910
4911 let ts2 = UnixNanos::from(2_000_000_000);
4913 clock.borrow_mut().set_time(ts2);
4914 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts2, ts2);
4915 aggregator.build_bar(&event);
4916
4917 let handler_guard = handler.lock();
4918 assert_eq!(handler_guard.len(), 2);
4919
4920 let bar1 = &handler_guard[0];
4921 assert_eq!(bar1.ts_event, ts1); assert_eq!(bar1.ts_init, ts1);
4923 assert_eq!(bar1.close, Price::from("100.00"));
4924 let bar2 = &handler_guard[1];
4925 assert_eq!(bar2.ts_event, ts2);
4926 assert_eq!(bar2.ts_init, ts2);
4927 assert_eq!(bar2.close, Price::from("101.00"));
4928 }
4929
4930 #[rstest]
4931 fn test_time_bar_aggregator_right_open_interval(equity_aapl: Equity) {
4932 let instrument = InstrumentAny::Equity(equity_aapl);
4933 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
4934 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4935 let (handler, record) = recording_handler();
4936 let clock = Rc::new(RefCell::new(TestClock::new()));
4937 let mut aggregator = TimeBarAggregator::new(
4938 bar_type,
4939 instrument.price_precision(),
4940 instrument.size_precision(),
4941 clock.clone(),
4942 record,
4943 true, true, BarIntervalType::RightOpen,
4946 None,
4947 15,
4948 false, );
4950
4951 aggregator.update(
4953 Price::from("100.00"),
4954 Quantity::from(1),
4955 UnixNanos::default(),
4956 );
4957
4958 let ts1 = UnixNanos::from(1_000_000_000);
4960 clock.borrow_mut().set_time(ts1);
4961 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts1, ts1);
4962 aggregator.build_bar(&event);
4963
4964 aggregator.update(Price::from("101.00"), Quantity::from(1), ts1);
4966
4967 let ts2 = UnixNanos::from(2_000_000_000);
4969 clock.borrow_mut().set_time(ts2);
4970 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts2, ts2);
4971 aggregator.build_bar(&event);
4972
4973 let handler_guard = handler.lock();
4974 assert_eq!(handler_guard.len(), 2);
4975
4976 let bar1 = &handler_guard[0];
4977 assert_eq!(bar1.ts_event, UnixNanos::default()); assert_eq!(bar1.ts_init, ts1);
4979 assert_eq!(bar1.close, Price::from("100.00"));
4980
4981 let bar2 = &handler_guard[1];
4982 assert_eq!(bar2.ts_event, ts1);
4983 assert_eq!(bar2.ts_init, ts2);
4984 assert_eq!(bar2.close, Price::from("101.00"));
4985 }
4986
4987 #[rstest]
4988 fn test_time_bar_aggregator_no_updates_behavior(equity_aapl: Equity) {
4989 let instrument = InstrumentAny::Equity(equity_aapl);
4990 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
4991 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4992 let (handler, record) = recording_handler();
4993 let clock = Rc::new(RefCell::new(TestClock::new()));
4994
4995 let mut aggregator = TimeBarAggregator::new(
4997 bar_type,
4998 instrument.price_precision(),
4999 instrument.size_precision(),
5000 clock.clone(),
5001 record,
5002 false, true, BarIntervalType::LeftOpen,
5005 None,
5006 15,
5007 false, );
5009
5010 let ts1 = UnixNanos::from(1_000_000_000);
5012 clock.borrow_mut().set_time(ts1);
5013 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts1, ts1);
5014 aggregator.build_bar(&event);
5015
5016 let handler_guard = handler.lock();
5017 assert_eq!(handler_guard.len(), 0); drop(handler_guard);
5019
5020 let (handler, record) = recording_handler();
5022 let mut aggregator = TimeBarAggregator::new(
5023 bar_type,
5024 instrument.price_precision(),
5025 instrument.size_precision(),
5026 clock.clone(),
5027 record,
5028 true, true, BarIntervalType::LeftOpen,
5031 None,
5032 15,
5033 false, );
5035
5036 aggregator.update(
5037 Price::from("100.00"),
5038 Quantity::from(1),
5039 UnixNanos::default(),
5040 );
5041
5042 let ts1 = UnixNanos::from(1_000_000_000);
5044 clock.borrow_mut().set_time(ts1);
5045 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts1, ts1);
5046 aggregator.build_bar(&event);
5047
5048 let ts2 = UnixNanos::from(2_000_000_000);
5050 clock.borrow_mut().set_time(ts2);
5051 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts2, ts2);
5052 aggregator.build_bar(&event);
5053
5054 let handler_guard = handler.lock();
5055 assert_eq!(handler_guard.len(), 2); let bar1 = &handler_guard[0];
5057 assert_eq!(bar1.close, Price::from("100.00"));
5058 let bar2 = &handler_guard[1];
5059 assert_eq!(bar2.close, Price::from("100.00")); }
5061
5062 #[rstest]
5063 fn test_time_bar_aggregator_respects_timestamp_on_close(equity_aapl: Equity) {
5064 let instrument = InstrumentAny::Equity(equity_aapl);
5065 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
5066 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5067 let clock = Rc::new(RefCell::new(TestClock::new()));
5068 let (handler, record) = recording_handler();
5069
5070 let mut aggregator = TimeBarAggregator::new(
5071 bar_type,
5072 instrument.price_precision(),
5073 instrument.size_precision(),
5074 clock.clone(),
5075 record,
5076 true, true, BarIntervalType::RightOpen,
5079 None,
5080 15,
5081 false, );
5083
5084 let ts1 = UnixNanos::from(1_000_000_000);
5085 aggregator.update(Price::from("100.00"), Quantity::from(1), ts1);
5086
5087 let ts2 = UnixNanos::from(2_000_000_000);
5088 clock.borrow_mut().set_time(ts2);
5089
5090 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts2, ts2);
5092 aggregator.build_bar(&event);
5093
5094 let handler_guard = handler.lock();
5095 let bar = handler_guard.first().unwrap();
5096 assert_eq!(bar.ts_event, UnixNanos::default());
5097 assert_eq!(bar.ts_init, ts2);
5098 }
5099
5100 #[rstest]
5101 fn test_renko_brick_preserves_subprecision_increment(audusd_sim: CurrencyPair) {
5102 let bar_type = BarType::new(
5103 audusd_sim.id(),
5104 BarSpecification::new(2, BarAggregation::Renko, PriceType::Last),
5105 AggregationSource::Internal,
5106 );
5107 let mut increment = Price::from("0.015");
5108 increment.precision = 2;
5109
5110 let aggregator = RenkoBarAggregator::new(bar_type, 2, 0, increment, |_| {});
5111 assert_eq!(aggregator.brick_size, Price::from("0.03"));
5112 }
5113
5114 #[rstest]
5115 fn test_renko_bar_aggregator_initialization(audusd_sim: CurrencyPair) {
5116 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5117 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5119 let (_handler, record) = recording_handler();
5120
5121 let aggregator = RenkoBarAggregator::new(
5122 bar_type,
5123 instrument.price_precision(),
5124 instrument.size_precision(),
5125 instrument.price_increment(),
5126 record,
5127 );
5128
5129 assert_eq!(aggregator.bar_type(), bar_type);
5130 assert!(!aggregator.is_running());
5131 let expected_brick_size = Price::from_decimal_dp(
5133 instrument.price_increment() * Decimal::from(10),
5134 instrument.price_precision(),
5135 )
5136 .unwrap();
5137 assert_eq!(aggregator.brick_size, expected_brick_size);
5138 }
5139
5140 #[rstest]
5141 fn test_renko_bar_aggregator_update_below_brick_size_no_bar(audusd_sim: CurrencyPair) {
5142 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5143 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5145 let (handler, record) = recording_handler();
5146
5147 let mut aggregator = RenkoBarAggregator::new(
5148 bar_type,
5149 instrument.price_precision(),
5150 instrument.size_precision(),
5151 instrument.price_increment(),
5152 record,
5153 );
5154
5155 aggregator.update(
5157 Price::from("1.00000"),
5158 Quantity::from(1),
5159 UnixNanos::default(),
5160 );
5161 aggregator.update(
5162 Price::from("1.00005"),
5163 Quantity::from(1),
5164 UnixNanos::from(1000),
5165 );
5166
5167 let handler_guard = handler.lock();
5168 assert_eq!(handler_guard.len(), 0); }
5170
5171 #[rstest]
5172 fn test_renko_bar_aggregator_ignores_out_of_order_bar(audusd_sim: CurrencyPair) {
5173 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5174 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid);
5175 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5176 let (handler, record) = recording_handler();
5177 let mut aggregator = RenkoBarAggregator::new(
5178 bar_type,
5179 instrument.price_precision(),
5180 instrument.size_precision(),
5181 instrument.price_increment(),
5182 record,
5183 );
5184 let first = Bar::new(
5185 bar_type,
5186 Price::from("1.00000"),
5187 Price::from("1.00000"),
5188 Price::from("1.00000"),
5189 Price::from("1.00000"),
5190 Quantity::from(1),
5191 UnixNanos::from(1_000),
5192 UnixNanos::from(1_000),
5193 );
5194 let stale = Bar::new(
5195 bar_type,
5196 Price::from("1.00020"),
5197 Price::from("1.00020"),
5198 Price::from("1.00020"),
5199 Price::from("1.00020"),
5200 Quantity::from(1),
5201 UnixNanos::from(500),
5202 UnixNanos::from(500),
5203 );
5204
5205 aggregator.update_bar(first, first.volume, first.ts_init);
5206 aggregator.update_bar(stale, stale.volume, stale.ts_init);
5207
5208 assert!(handler.lock().is_empty());
5209 assert_eq!(aggregator.last_close, Some(Price::from("1.00000")));
5210 assert_eq!(aggregator.core.builder.ts_last, UnixNanos::from(1_000));
5211 }
5212
5213 #[rstest]
5214 fn test_renko_bar_aggregator_update_exceeds_brick_size_creates_bar(audusd_sim: CurrencyPair) {
5215 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5216 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5218 let (handler, record) = recording_handler();
5219
5220 let mut aggregator = RenkoBarAggregator::new(
5221 bar_type,
5222 instrument.price_precision(),
5223 instrument.size_precision(),
5224 instrument.price_increment(),
5225 record,
5226 );
5227
5228 aggregator.update(
5230 Price::from("1.00000"),
5231 Quantity::from(1),
5232 UnixNanos::default(),
5233 );
5234 aggregator.update(
5235 Price::from("1.00015"),
5236 Quantity::from(1),
5237 UnixNanos::from(1000),
5238 );
5239
5240 let handler_guard = handler.lock();
5241 assert_eq!(handler_guard.len(), 1);
5242
5243 let bar = handler_guard.first().unwrap();
5244 assert_eq!(bar.open, Price::from("1.00000"));
5245 assert_eq!(bar.high, Price::from("1.00010"));
5246 assert_eq!(bar.low, Price::from("1.00000"));
5247 assert_eq!(bar.close, Price::from("1.00010"));
5248 assert_eq!(bar.volume, Quantity::from(2));
5249 assert_eq!(bar.ts_event, UnixNanos::from(1000));
5250 assert_eq!(bar.ts_init, UnixNanos::from(1000));
5251 }
5252
5253 #[rstest]
5254 fn test_renko_bar_aggregator_multiple_bricks_in_one_update(audusd_sim: CurrencyPair) {
5255 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5256 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5258 let (handler, record) = recording_handler();
5259
5260 let mut aggregator = RenkoBarAggregator::new(
5261 bar_type,
5262 instrument.price_precision(),
5263 instrument.size_precision(),
5264 instrument.price_increment(),
5265 record,
5266 );
5267
5268 aggregator.update(
5270 Price::from("1.00000"),
5271 Quantity::from(1),
5272 UnixNanos::default(),
5273 );
5274 aggregator.update(
5275 Price::from("1.00025"),
5276 Quantity::from(1),
5277 UnixNanos::from(1000),
5278 );
5279
5280 let handler_guard = handler.lock();
5281 assert_eq!(handler_guard.len(), 2);
5282
5283 let bar1 = &handler_guard[0];
5284 assert_eq!(bar1.open, Price::from("1.00000"));
5285 assert_eq!(bar1.high, Price::from("1.00010"));
5286 assert_eq!(bar1.low, Price::from("1.00000"));
5287 assert_eq!(bar1.close, Price::from("1.00010"));
5288
5289 let bar2 = &handler_guard[1];
5290 assert_eq!(bar2.open, Price::from("1.00010"));
5291 assert_eq!(bar2.high, Price::from("1.00020"));
5292 assert_eq!(bar2.low, Price::from("1.00010"));
5293 assert_eq!(bar2.close, Price::from("1.00020"));
5294 }
5295
5296 #[rstest]
5297 fn test_renko_bar_aggregator_downward_movement(audusd_sim: CurrencyPair) {
5298 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5299 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5301 let (handler, record) = recording_handler();
5302
5303 let mut aggregator = RenkoBarAggregator::new(
5304 bar_type,
5305 instrument.price_precision(),
5306 instrument.size_precision(),
5307 instrument.price_increment(),
5308 record,
5309 );
5310
5311 aggregator.update(
5313 Price::from("1.00020"),
5314 Quantity::from(1),
5315 UnixNanos::default(),
5316 );
5317 aggregator.update(
5318 Price::from("1.00005"),
5319 Quantity::from(1),
5320 UnixNanos::from(1000),
5321 );
5322
5323 let handler_guard = handler.lock();
5324 assert_eq!(handler_guard.len(), 1);
5325
5326 let bar = handler_guard.first().unwrap();
5327 assert_eq!(bar.open, Price::from("1.00020"));
5328 assert_eq!(bar.high, Price::from("1.00020"));
5329 assert_eq!(bar.low, Price::from("1.00010"));
5330 assert_eq!(bar.close, Price::from("1.00010"));
5331 assert_eq!(bar.volume, Quantity::from(2));
5332 }
5333
5334 #[rstest]
5335 fn test_renko_bar_aggregator_handle_bar_below_brick_size(audusd_sim: CurrencyPair) {
5336 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5337 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5339 let (handler, record) = recording_handler();
5340
5341 let mut aggregator = RenkoBarAggregator::new(
5342 bar_type,
5343 instrument.price_precision(),
5344 instrument.size_precision(),
5345 instrument.price_increment(),
5346 record,
5347 );
5348
5349 let input_bar = Bar::new(
5351 BarType::new(
5352 instrument.id(),
5353 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5354 AggregationSource::Internal,
5355 ),
5356 Price::from("1.00000"),
5357 Price::from("1.00005"),
5358 Price::from("0.99995"),
5359 Price::from("1.00005"), Quantity::from(100),
5361 UnixNanos::default(),
5362 UnixNanos::from(1000),
5363 );
5364
5365 aggregator.handle_bar(input_bar);
5366
5367 let handler_guard = handler.lock();
5368 assert_eq!(handler_guard.len(), 0); }
5370
5371 #[rstest]
5372 fn test_renko_bar_aggregator_handle_bar_exceeds_brick_size(audusd_sim: CurrencyPair) {
5373 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5374 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5376 let (handler, record) = recording_handler();
5377
5378 let mut aggregator = RenkoBarAggregator::new(
5379 bar_type,
5380 instrument.price_precision(),
5381 instrument.size_precision(),
5382 instrument.price_increment(),
5383 record,
5384 );
5385
5386 let bar1 = Bar::new(
5388 BarType::new(
5389 instrument.id(),
5390 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5391 AggregationSource::Internal,
5392 ),
5393 Price::from("1.00000"),
5394 Price::from("1.00005"),
5395 Price::from("0.99995"),
5396 Price::from("1.00000"),
5397 Quantity::from(100),
5398 UnixNanos::default(),
5399 UnixNanos::default(),
5400 );
5401
5402 let bar2 = Bar::new(
5404 BarType::new(
5405 instrument.id(),
5406 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5407 AggregationSource::Internal,
5408 ),
5409 Price::from("1.00000"),
5410 Price::from("1.00015"),
5411 Price::from("0.99995"),
5412 Price::from("1.00010"), Quantity::from(50),
5414 UnixNanos::from(60_000_000_000),
5415 UnixNanos::from(60_000_000_000),
5416 );
5417
5418 aggregator.handle_bar(bar1);
5419 aggregator.handle_bar(bar2);
5420
5421 let handler_guard = handler.lock();
5422 assert_eq!(handler_guard.len(), 1);
5423
5424 let bar = handler_guard.first().unwrap();
5425 assert_eq!(bar.open, Price::from("1.00000"));
5426 assert_eq!(bar.high, Price::from("1.00010"));
5427 assert_eq!(bar.low, Price::from("1.00000"));
5428 assert_eq!(bar.close, Price::from("1.00010"));
5429 assert_eq!(bar.volume, Quantity::from(150));
5430 }
5431
5432 #[rstest]
5433 fn test_renko_bar_aggregator_handle_bar_multiple_bricks(audusd_sim: CurrencyPair) {
5434 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5435 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5437 let (handler, record) = recording_handler();
5438
5439 let mut aggregator = RenkoBarAggregator::new(
5440 bar_type,
5441 instrument.price_precision(),
5442 instrument.size_precision(),
5443 instrument.price_increment(),
5444 record,
5445 );
5446
5447 let bar1 = Bar::new(
5449 BarType::new(
5450 instrument.id(),
5451 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5452 AggregationSource::Internal,
5453 ),
5454 Price::from("1.00000"),
5455 Price::from("1.00005"),
5456 Price::from("0.99995"),
5457 Price::from("1.00000"),
5458 Quantity::from(100),
5459 UnixNanos::default(),
5460 UnixNanos::default(),
5461 );
5462
5463 let bar2 = Bar::new(
5465 BarType::new(
5466 instrument.id(),
5467 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5468 AggregationSource::Internal,
5469 ),
5470 Price::from("1.00000"),
5471 Price::from("1.00035"),
5472 Price::from("0.99995"),
5473 Price::from("1.00030"), Quantity::from(50),
5475 UnixNanos::from(60_000_000_000),
5476 UnixNanos::from(60_000_000_000),
5477 );
5478
5479 aggregator.handle_bar(bar1);
5480 aggregator.handle_bar(bar2);
5481
5482 let handler_guard = handler.lock();
5483 assert_eq!(handler_guard.len(), 3);
5484
5485 let bar1 = &handler_guard[0];
5486 assert_eq!(bar1.open, Price::from("1.00000"));
5487 assert_eq!(bar1.close, Price::from("1.00010"));
5488
5489 let bar2 = &handler_guard[1];
5490 assert_eq!(bar2.open, Price::from("1.00010"));
5491 assert_eq!(bar2.close, Price::from("1.00020"));
5492
5493 let bar3 = &handler_guard[2];
5494 assert_eq!(bar3.open, Price::from("1.00020"));
5495 assert_eq!(bar3.close, Price::from("1.00030"));
5496 }
5497
5498 #[rstest]
5499 fn test_renko_bar_aggregator_handle_bar_downward_movement(audusd_sim: CurrencyPair) {
5500 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5501 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5503 let (handler, record) = recording_handler();
5504
5505 let mut aggregator = RenkoBarAggregator::new(
5506 bar_type,
5507 instrument.price_precision(),
5508 instrument.size_precision(),
5509 instrument.price_increment(),
5510 record,
5511 );
5512
5513 let bar1 = Bar::new(
5515 BarType::new(
5516 instrument.id(),
5517 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5518 AggregationSource::Internal,
5519 ),
5520 Price::from("1.00020"),
5521 Price::from("1.00025"),
5522 Price::from("1.00015"),
5523 Price::from("1.00020"),
5524 Quantity::from(100),
5525 UnixNanos::default(),
5526 UnixNanos::default(),
5527 );
5528
5529 let bar2 = Bar::new(
5531 BarType::new(
5532 instrument.id(),
5533 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5534 AggregationSource::Internal,
5535 ),
5536 Price::from("1.00020"),
5537 Price::from("1.00025"),
5538 Price::from("1.00005"),
5539 Price::from("1.00010"), Quantity::from(50),
5541 UnixNanos::from(60_000_000_000),
5542 UnixNanos::from(60_000_000_000),
5543 );
5544
5545 aggregator.handle_bar(bar1);
5546 aggregator.handle_bar(bar2);
5547
5548 let handler_guard = handler.lock();
5549 assert_eq!(handler_guard.len(), 1);
5550
5551 let bar = handler_guard.first().unwrap();
5552 assert_eq!(bar.open, Price::from("1.00020"));
5553 assert_eq!(bar.high, Price::from("1.00020"));
5554 assert_eq!(bar.low, Price::from("1.00010"));
5555 assert_eq!(bar.close, Price::from("1.00010"));
5556 assert_eq!(bar.volume, Quantity::from(150));
5557 }
5558
5559 #[rstest]
5560 fn test_renko_bar_aggregator_brick_size_calculation(audusd_sim: CurrencyPair) {
5561 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5562
5563 let bar_spec_5 = BarSpecification::new(5, BarAggregation::Renko, PriceType::Mid); let bar_type_5 = BarType::new(instrument.id(), bar_spec_5, AggregationSource::Internal);
5566 let (_handler_5, record) = recording_handler();
5567
5568 let aggregator_5 = RenkoBarAggregator::new(
5569 bar_type_5,
5570 instrument.price_precision(),
5571 instrument.size_precision(),
5572 instrument.price_increment(),
5573 record,
5574 );
5575
5576 let expected_brick_size_5 = Price::from_decimal_dp(
5578 instrument.price_increment() * Decimal::from(5),
5579 instrument.price_precision(),
5580 )
5581 .unwrap();
5582 assert_eq!(aggregator_5.brick_size, expected_brick_size_5);
5583
5584 let bar_spec_20 = BarSpecification::new(20, BarAggregation::Renko, PriceType::Mid); let bar_type_20 = BarType::new(instrument.id(), bar_spec_20, AggregationSource::Internal);
5586 let (_handler_20, record) = recording_handler();
5587
5588 let aggregator_20 = RenkoBarAggregator::new(
5589 bar_type_20,
5590 instrument.price_precision(),
5591 instrument.size_precision(),
5592 instrument.price_increment(),
5593 record,
5594 );
5595
5596 let expected_brick_size_20 = Price::from_decimal_dp(
5598 instrument.price_increment() * Decimal::from(20),
5599 instrument.price_precision(),
5600 )
5601 .unwrap();
5602 assert_eq!(aggregator_20.brick_size, expected_brick_size_20);
5603 }
5604
5605 #[rstest]
5606 fn test_renko_bar_aggregator_sequential_updates(audusd_sim: CurrencyPair) {
5607 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5608 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5610 let (handler, record) = recording_handler();
5611
5612 let mut aggregator = RenkoBarAggregator::new(
5613 bar_type,
5614 instrument.price_precision(),
5615 instrument.size_precision(),
5616 instrument.price_increment(),
5617 record,
5618 );
5619
5620 aggregator.update(
5622 Price::from("1.00000"),
5623 Quantity::from(1),
5624 UnixNanos::from(1000),
5625 );
5626 aggregator.update(
5627 Price::from("1.00010"),
5628 Quantity::from(1),
5629 UnixNanos::from(2000),
5630 ); aggregator.update(
5632 Price::from("1.00020"),
5633 Quantity::from(1),
5634 UnixNanos::from(3000),
5635 ); aggregator.update(
5637 Price::from("1.00025"),
5638 Quantity::from(1),
5639 UnixNanos::from(4000),
5640 ); aggregator.update(
5642 Price::from("1.00030"),
5643 Quantity::from(1),
5644 UnixNanos::from(5000),
5645 ); let handler_guard = handler.lock();
5648 assert_eq!(handler_guard.len(), 3);
5649
5650 let bar1 = &handler_guard[0];
5651 assert_eq!(bar1.open, Price::from("1.00000"));
5652 assert_eq!(bar1.close, Price::from("1.00010"));
5653
5654 let bar2 = &handler_guard[1];
5655 assert_eq!(bar2.open, Price::from("1.00010"));
5656 assert_eq!(bar2.close, Price::from("1.00020"));
5657
5658 let bar3 = &handler_guard[2];
5659 assert_eq!(bar3.open, Price::from("1.00020"));
5660 assert_eq!(bar3.close, Price::from("1.00030"));
5661 }
5662
5663 #[rstest]
5664 fn test_renko_bar_aggregator_mixed_direction_movement(audusd_sim: CurrencyPair) {
5665 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5666 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5668 let (handler, record) = recording_handler();
5669
5670 let mut aggregator = RenkoBarAggregator::new(
5671 bar_type,
5672 instrument.price_precision(),
5673 instrument.size_precision(),
5674 instrument.price_increment(),
5675 record,
5676 );
5677
5678 aggregator.update(
5680 Price::from("1.00000"),
5681 Quantity::from(1),
5682 UnixNanos::from(1000),
5683 );
5684 aggregator.update(
5685 Price::from("1.00010"),
5686 Quantity::from(1),
5687 UnixNanos::from(2000),
5688 ); aggregator.update(
5690 Price::from("0.99990"),
5691 Quantity::from(1),
5692 UnixNanos::from(3000),
5693 ); let handler_guard = handler.lock();
5696 assert_eq!(handler_guard.len(), 3);
5697
5698 let bar1 = &handler_guard[0]; assert_eq!(bar1.open, Price::from("1.00000"));
5700 assert_eq!(bar1.high, Price::from("1.00010"));
5701 assert_eq!(bar1.low, Price::from("1.00000"));
5702 assert_eq!(bar1.close, Price::from("1.00010"));
5703
5704 let bar2 = &handler_guard[1]; assert_eq!(bar2.open, Price::from("1.00010"));
5706 assert_eq!(bar2.high, Price::from("1.00010"));
5707 assert_eq!(bar2.low, Price::from("1.00000"));
5708 assert_eq!(bar2.close, Price::from("1.00000"));
5709
5710 let bar3 = &handler_guard[2]; assert_eq!(bar3.open, Price::from("1.00000"));
5712 assert_eq!(bar3.high, Price::from("1.00000"));
5713 assert_eq!(bar3.low, Price::from("0.99990"));
5714 assert_eq!(bar3.close, Price::from("0.99990"));
5715 }
5716
5717 #[rstest]
5718 fn test_tick_imbalance_bar_aggregator_mixed_trades_cancel_out(equity_aapl: Equity) {
5719 let instrument = InstrumentAny::Equity(equity_aapl);
5720 let bar_spec = BarSpecification::new(3, BarAggregation::TickImbalance, PriceType::Last);
5721 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5722 let (handler, record) = recording_handler();
5723
5724 let mut aggregator = TickImbalanceBarAggregator::new(
5725 bar_type,
5726 instrument.price_precision(),
5727 instrument.size_precision(),
5728 record,
5729 );
5730
5731 let buy = TradeTick {
5732 aggressor_side: AggressorSide::Buy,
5733 ..TradeTick::default()
5734 };
5735 let sell = TradeTick {
5736 aggressor_side: AggressorSide::Sell,
5737 ..TradeTick::default()
5738 };
5739
5740 aggregator.handle_trade(buy);
5741 aggregator.handle_trade(sell);
5742 aggregator.handle_trade(buy);
5743
5744 let handler_guard = handler.lock();
5745 assert_eq!(handler_guard.len(), 0);
5746 }
5747
5748 #[rstest]
5749 fn test_tick_imbalance_bar_aggregator_no_aggressor_ignored(equity_aapl: Equity) {
5750 let instrument = InstrumentAny::Equity(equity_aapl);
5751 let bar_spec = BarSpecification::new(2, BarAggregation::TickImbalance, PriceType::Last);
5752 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5753 let (handler, record) = recording_handler();
5754
5755 let mut aggregator = TickImbalanceBarAggregator::new(
5756 bar_type,
5757 instrument.price_precision(),
5758 instrument.size_precision(),
5759 record,
5760 );
5761
5762 let buy = TradeTick {
5763 aggressor_side: AggressorSide::Buy,
5764 ..TradeTick::default()
5765 };
5766 let no_aggressor = TradeTick {
5767 aggressor_side: AggressorSide::NoAggressor,
5768 ..TradeTick::default()
5769 };
5770
5771 aggregator.handle_trade(buy);
5772 aggregator.handle_trade(no_aggressor);
5773 aggregator.handle_trade(buy);
5774
5775 let handler_guard = handler.lock();
5776 assert_eq!(handler_guard.len(), 1);
5777 }
5778
5779 #[rstest]
5780 fn test_tick_runs_bar_aggregator_multiple_consecutive_runs(equity_aapl: Equity) {
5781 let instrument = InstrumentAny::Equity(equity_aapl);
5782 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
5783 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5784 let (handler, record) = recording_handler();
5785
5786 let mut aggregator = TickRunsBarAggregator::new(
5787 bar_type,
5788 instrument.price_precision(),
5789 instrument.size_precision(),
5790 record,
5791 );
5792
5793 let buy = TradeTick {
5794 aggressor_side: AggressorSide::Buy,
5795 ..TradeTick::default()
5796 };
5797 let sell = TradeTick {
5798 aggressor_side: AggressorSide::Sell,
5799 ..TradeTick::default()
5800 };
5801
5802 aggregator.handle_trade(buy);
5803 aggregator.handle_trade(buy);
5804 aggregator.handle_trade(sell);
5805 aggregator.handle_trade(sell);
5806
5807 let handler_guard = handler.lock();
5808 assert_eq!(handler_guard.len(), 2);
5809 }
5810
5811 #[rstest]
5812 fn test_volume_imbalance_bar_aggregator_large_trade_spans_bars(equity_aapl: Equity) {
5813 let instrument = InstrumentAny::Equity(equity_aapl);
5814 let bar_spec = BarSpecification::new(10, BarAggregation::VolumeImbalance, PriceType::Last);
5815 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5816 let (handler, record) = recording_handler();
5817
5818 let mut aggregator = VolumeImbalanceBarAggregator::new(
5819 bar_type,
5820 instrument.price_precision(),
5821 instrument.size_precision(),
5822 record,
5823 );
5824
5825 let large_trade = TradeTick {
5826 size: Quantity::from(25),
5827 aggressor_side: AggressorSide::Buy,
5828 ..TradeTick::default()
5829 };
5830
5831 aggregator.handle_trade(large_trade);
5832
5833 let handler_guard = handler.lock();
5834 assert_eq!(handler_guard.len(), 2);
5835 }
5836
5837 #[rstest]
5838 fn test_volume_imbalance_bar_aggregator_no_aggressor_does_not_affect_imbalance(
5839 equity_aapl: Equity,
5840 ) {
5841 let instrument = InstrumentAny::Equity(equity_aapl);
5842 let bar_spec = BarSpecification::new(10, BarAggregation::VolumeImbalance, PriceType::Last);
5843 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5844 let (handler, record) = recording_handler();
5845
5846 let mut aggregator = VolumeImbalanceBarAggregator::new(
5847 bar_type,
5848 instrument.price_precision(),
5849 instrument.size_precision(),
5850 record,
5851 );
5852
5853 let buy = TradeTick {
5854 size: Quantity::from(5),
5855 aggressor_side: AggressorSide::Buy,
5856 ..TradeTick::default()
5857 };
5858 let no_aggressor = TradeTick {
5859 size: Quantity::from(3),
5860 aggressor_side: AggressorSide::NoAggressor,
5861 ..TradeTick::default()
5862 };
5863
5864 aggregator.handle_trade(buy);
5865 aggregator.handle_trade(no_aggressor);
5866 aggregator.handle_trade(buy);
5867
5868 let handler_guard = handler.lock();
5869 assert_eq!(handler_guard.len(), 1);
5870 }
5871
5872 #[rstest]
5873 fn test_volume_runs_bar_aggregator_large_trade_spans_bars(equity_aapl: Equity) {
5874 let instrument = InstrumentAny::Equity(equity_aapl);
5875 let bar_spec = BarSpecification::new(10, BarAggregation::VolumeRuns, PriceType::Last);
5876 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5877 let (handler, record) = recording_handler();
5878
5879 let mut aggregator = VolumeRunsBarAggregator::new(
5880 bar_type,
5881 instrument.price_precision(),
5882 instrument.size_precision(),
5883 record,
5884 );
5885
5886 let large_trade = TradeTick {
5887 size: Quantity::from(25),
5888 aggressor_side: AggressorSide::Buy,
5889 ..TradeTick::default()
5890 };
5891
5892 aggregator.handle_trade(large_trade);
5893
5894 let handler_guard = handler.lock();
5895 assert_eq!(handler_guard.len(), 2);
5896 }
5897
5898 #[rstest]
5899 fn test_value_runs_bar_aggregator_large_trade_spans_bars(equity_aapl: Equity) {
5900 let instrument = InstrumentAny::Equity(equity_aapl);
5901 let bar_spec = BarSpecification::new(50, BarAggregation::ValueRuns, PriceType::Last);
5902 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5903 let (handler, record) = recording_handler();
5904
5905 let mut aggregator = ValueRunsBarAggregator::new(
5906 bar_type,
5907 instrument.price_precision(),
5908 instrument.size_precision(),
5909 record,
5910 );
5911
5912 let large_trade = TradeTick {
5913 price: Price::from("5.00"),
5914 size: Quantity::from(25),
5915 aggressor_side: AggressorSide::Buy,
5916 ..TradeTick::default()
5917 };
5918
5919 aggregator.handle_trade(large_trade);
5920
5921 let handler_guard = handler.lock();
5922 assert_eq!(handler_guard.len(), 2);
5923 }
5924
5925 #[rstest]
5926 fn test_value_runs_bar_aggregator_keeps_leftover_volume_for_same_side_run(equity_aapl: Equity) {
5927 let instrument = InstrumentAny::Equity(equity_aapl);
5928 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
5929 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5930 let (handler, record) = recording_handler();
5931
5932 let mut aggregator = ValueRunsBarAggregator::new(
5933 bar_type,
5934 instrument.price_precision(),
5935 instrument.size_precision(),
5936 record,
5937 );
5938
5939 let first = TradeTick {
5942 price: Price::from("10.00"),
5943 size: Quantity::from(15),
5944 aggressor_side: AggressorSide::Sell,
5945 ts_event: UnixNanos::from(1_000),
5946 ts_init: UnixNanos::from(1_000),
5947 ..TradeTick::default()
5948 };
5949 aggregator.handle_trade(first);
5950
5951 let second = TradeTick {
5953 price: Price::from("10.00"),
5954 size: Quantity::from(5),
5955 aggressor_side: AggressorSide::Sell,
5956 ts_event: UnixNanos::from(2_000),
5957 ts_init: UnixNanos::from(2_000),
5958 ..TradeTick::default()
5959 };
5960 aggregator.handle_trade(second);
5961
5962 let handler_guard = handler.lock();
5963 assert_eq!(handler_guard.len(), 2);
5964 assert_eq!(handler_guard[0].volume, Quantity::from(10));
5965 assert_eq!(handler_guard[1].volume, Quantity::from(10));
5966 }
5967
5968 #[rstest]
5969 fn test_value_bar_high_price_low_step_no_zero_volume_bars(equity_aapl: Equity) {
5970 let instrument = InstrumentAny::Equity(equity_aapl);
5971 let bar_spec = BarSpecification::new(100, BarAggregation::Value, PriceType::Last);
5972 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5973 let (handler, record) = recording_handler();
5974
5975 let mut aggregator = ValueBarAggregator::new(
5976 bar_type,
5977 instrument.price_precision(),
5978 instrument.size_precision(),
5979 record,
5980 );
5981
5982 aggregator.update(
5984 Price::from("1000.00"),
5985 Quantity::from(3),
5986 UnixNanos::default(),
5987 );
5988
5989 let handler_guard = handler.lock();
5991 assert_eq!(handler_guard.len(), 3);
5992 for bar in handler_guard.iter() {
5993 assert_eq!(bar.volume, Quantity::from(1));
5994 }
5995 }
5996
5997 #[rstest]
5998 fn test_value_imbalance_high_price_low_step_no_zero_volume_bars(equity_aapl: Equity) {
5999 let instrument = InstrumentAny::Equity(equity_aapl);
6000 let bar_spec = BarSpecification::new(100, BarAggregation::ValueImbalance, PriceType::Last);
6001 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6002 let (handler, record) = recording_handler();
6003
6004 let mut aggregator = ValueImbalanceBarAggregator::new(
6005 bar_type,
6006 instrument.price_precision(),
6007 instrument.size_precision(),
6008 record,
6009 );
6010
6011 let trade = TradeTick {
6012 price: Price::from("1000.00"),
6013 size: Quantity::from(3),
6014 aggressor_side: AggressorSide::Buy,
6015 instrument_id: instrument.id(),
6016 ..TradeTick::default()
6017 };
6018
6019 aggregator.handle_trade(trade);
6020
6021 let handler_guard = handler.lock();
6022 assert_eq!(handler_guard.len(), 3);
6023 for bar in handler_guard.iter() {
6024 assert_eq!(bar.volume, Quantity::from(1));
6025 }
6026 }
6027
6028 #[rstest]
6029 fn test_value_imbalance_opposite_side_overshoot_emits_bar(equity_aapl: Equity) {
6030 let instrument = InstrumentAny::Equity(equity_aapl);
6031 let bar_spec = BarSpecification::new(100, BarAggregation::ValueImbalance, PriceType::Last);
6032 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6033 let (handler, record) = recording_handler();
6034
6035 let mut aggregator = ValueImbalanceBarAggregator::new(
6036 bar_type,
6037 instrument.price_precision(),
6038 instrument.size_precision(),
6039 record,
6040 );
6041
6042 let sell_tick = TradeTick {
6044 price: Price::from("10.00"),
6045 size: Quantity::from(5),
6046 aggressor_side: AggressorSide::Sell,
6047 instrument_id: instrument.id(),
6048 ..TradeTick::default()
6049 };
6050
6051 let buy_tick = TradeTick {
6054 price: Price::from("1000.00"),
6055 size: Quantity::from(1),
6056 aggressor_side: AggressorSide::Buy,
6057 instrument_id: instrument.id(),
6058 ts_init: UnixNanos::from(1),
6059 ts_event: UnixNanos::from(1),
6060 ..TradeTick::default()
6061 };
6062
6063 aggregator.handle_trade(sell_tick);
6064 aggregator.handle_trade(buy_tick);
6065
6066 let handler_guard = handler.lock();
6067 assert_eq!(handler_guard.len(), 1);
6068 assert_eq!(handler_guard[0].volume, Quantity::from(6));
6069 }
6070
6071 #[rstest]
6072 fn test_value_runs_high_price_low_step_no_zero_volume_bars(equity_aapl: Equity) {
6073 let instrument = InstrumentAny::Equity(equity_aapl);
6074 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
6075 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6076 let (handler, record) = recording_handler();
6077
6078 let mut aggregator = ValueRunsBarAggregator::new(
6079 bar_type,
6080 instrument.price_precision(),
6081 instrument.size_precision(),
6082 record,
6083 );
6084
6085 let trade = TradeTick {
6086 price: Price::from("1000.00"),
6087 size: Quantity::from(3),
6088 aggressor_side: AggressorSide::Buy,
6089 instrument_id: instrument.id(),
6090 ..TradeTick::default()
6091 };
6092
6093 aggregator.handle_trade(trade);
6094
6095 let handler_guard = handler.lock();
6096 assert_eq!(handler_guard.len(), 3);
6097 for bar in handler_guard.iter() {
6098 assert_eq!(bar.volume, Quantity::from(1));
6099 }
6100 }
6101
6102 #[rstest]
6103 fn test_value_imbalance_bar_aggregator_exact_below_step_retains_pending() {
6104 let instrument_id = InstrumentId::from("AAPL.XNAS");
6108 let bar_spec = BarSpecification::new(
6109 9_007_199_254,
6110 BarAggregation::ValueImbalance,
6111 PriceType::Last,
6112 );
6113 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6114 let (handler, record) = recording_handler();
6115
6116 let mut aggregator = ValueImbalanceBarAggregator::new(bar_type, 0, 9, record);
6117
6118 let below_step = TradeTick {
6119 instrument_id,
6120 price: Price::from("1"),
6121 size: Quantity::from("9007199253.999999999"),
6122 aggressor_side: AggressorSide::Buy,
6123 ..TradeTick::default()
6124 };
6125 aggregator.handle_trade(below_step);
6126
6127 assert!(handler.lock().is_empty());
6128 assert_eq!(
6129 aggregator.core.builder.volume,
6130 Quantity::from("9007199253.999999999"),
6131 );
6132
6133 let one_raw_unit = TradeTick {
6136 instrument_id,
6137 price: Price::from("1"),
6138 size: Quantity::from("0.000000001"),
6139 aggressor_side: AggressorSide::Buy,
6140 ts_event: UnixNanos::from(1),
6141 ts_init: UnixNanos::from(1),
6142 ..TradeTick::default()
6143 };
6144 aggregator.handle_trade(one_raw_unit);
6145
6146 let handler_guard = handler.lock();
6147 assert_eq!(handler_guard.len(), 1);
6148 assert_eq!(
6149 handler_guard[0].volume,
6150 Quantity::from("9007199254.000000000")
6151 );
6152 assert_eq!(aggregator.core.builder.volume, Quantity::zero(9));
6153 }
6154
6155 #[rstest]
6156 fn test_value_imbalance_bar_aggregator_conserves_volume_across_split_bars() {
6157 let instrument_id = InstrumentId::from("AAPL.XNAS");
6161 let bar_spec = BarSpecification::new(4, BarAggregation::ValueImbalance, PriceType::Last);
6162 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6163 let (handler, record) = recording_handler();
6164
6165 let mut aggregator = ValueImbalanceBarAggregator::new(bar_type, 0, 9, record);
6166
6167 let input = Quantity::from("10.000000003");
6168 let trade = TradeTick {
6169 instrument_id,
6170 price: Price::from("1"),
6171 size: input,
6172 aggressor_side: AggressorSide::Buy,
6173 ..TradeTick::default()
6174 };
6175 aggregator.handle_trade(trade);
6176
6177 let handler_guard = handler.lock();
6178 assert_eq!(handler_guard.len(), 2);
6179 for bar in handler_guard.iter() {
6180 assert_eq!(bar.volume, Quantity::from("4.000000000"));
6181 }
6182 assert_eq!(
6183 aggregator.core.builder.volume,
6184 Quantity::from("2.000000003"),
6185 );
6186 let emitted_plus_pending = handler_guard
6187 .iter()
6188 .map(|bar| bar.volume.as_decimal())
6189 .sum::<Decimal>()
6190 + aggregator.core.builder.volume.as_decimal();
6191 assert_eq!(emitted_plus_pending, input.as_decimal());
6192 }
6193
6194 #[rstest]
6195 fn test_value_runs_bar_aggregator_exact_below_step_retains_pending() {
6196 let instrument_id = InstrumentId::from("AAPL.XNAS");
6200 let bar_spec =
6201 BarSpecification::new(9_007_199_254, BarAggregation::ValueRuns, PriceType::Last);
6202 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6203 let (handler, record) = recording_handler();
6204
6205 let mut aggregator = ValueRunsBarAggregator::new(bar_type, 0, 9, record);
6206
6207 let below_step = TradeTick {
6208 instrument_id,
6209 price: Price::from("1"),
6210 size: Quantity::from("9007199253.999999999"),
6211 aggressor_side: AggressorSide::Buy,
6212 ..TradeTick::default()
6213 };
6214 aggregator.handle_trade(below_step);
6215
6216 assert!(handler.lock().is_empty());
6217 assert_eq!(
6218 aggregator.core.builder.volume,
6219 Quantity::from("9007199253.999999999"),
6220 );
6221
6222 let one_raw_unit = TradeTick {
6225 instrument_id,
6226 price: Price::from("1"),
6227 size: Quantity::from("0.000000001"),
6228 aggressor_side: AggressorSide::Buy,
6229 ts_event: UnixNanos::from(1),
6230 ts_init: UnixNanos::from(1),
6231 ..TradeTick::default()
6232 };
6233 aggregator.handle_trade(one_raw_unit);
6234
6235 let handler_guard = handler.lock();
6236 assert_eq!(handler_guard.len(), 1);
6237 assert_eq!(
6238 handler_guard[0].volume,
6239 Quantity::from("9007199254.000000000")
6240 );
6241 assert_eq!(aggregator.core.builder.volume, Quantity::zero(9));
6242 }
6243
6244 #[rstest]
6245 fn test_value_runs_bar_aggregator_conserves_volume_across_split_bars() {
6246 let instrument_id = InstrumentId::from("AAPL.XNAS");
6250 let bar_spec = BarSpecification::new(4, BarAggregation::ValueRuns, PriceType::Last);
6251 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6252 let (handler, record) = recording_handler();
6253
6254 let mut aggregator = ValueRunsBarAggregator::new(bar_type, 0, 9, record);
6255
6256 let input = Quantity::from("10.000000003");
6257 let trade = TradeTick {
6258 instrument_id,
6259 price: Price::from("1"),
6260 size: input,
6261 aggressor_side: AggressorSide::Buy,
6262 ..TradeTick::default()
6263 };
6264 aggregator.handle_trade(trade);
6265
6266 let handler_guard = handler.lock();
6267 assert_eq!(handler_guard.len(), 2);
6268 for bar in handler_guard.iter() {
6269 assert_eq!(bar.volume, Quantity::from("4.000000000"));
6270 }
6271 assert_eq!(
6272 aggregator.core.builder.volume,
6273 Quantity::from("2.000000003"),
6274 );
6275 let emitted_plus_pending = handler_guard
6276 .iter()
6277 .map(|bar| bar.volume.as_decimal())
6278 .sum::<Decimal>()
6279 + aggregator.core.builder.volume.as_decimal();
6280 assert_eq!(emitted_plus_pending, input.as_decimal());
6281 }
6282
6283 #[rstest]
6284 fn test_value_imbalance_bar_aggregator_no_aggressor_and_zero_price_fall_back_to_plain_volume() {
6285 let instrument_id = InstrumentId::from("AAPL.XNAS");
6288 let bar_spec = BarSpecification::new(100, BarAggregation::ValueImbalance, PriceType::Last);
6289 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6290 let (handler, record) = recording_handler();
6291
6292 let mut aggregator = ValueImbalanceBarAggregator::new(bar_type, 2, 0, record);
6293
6294 let no_aggressor = TradeTick {
6295 instrument_id,
6296 price: Price::from("10.00"),
6297 size: Quantity::from(3),
6298 aggressor_side: AggressorSide::NoAggressor,
6299 ..TradeTick::default()
6300 };
6301 let zero_price = TradeTick {
6302 instrument_id,
6303 price: Price::from("0.00"),
6304 size: Quantity::from(4),
6305 aggressor_side: AggressorSide::Buy,
6306 ts_event: UnixNanos::from(1),
6307 ts_init: UnixNanos::from(1),
6308 ..TradeTick::default()
6309 };
6310 aggregator.handle_trade(no_aggressor);
6311 aggregator.handle_trade(zero_price);
6312
6313 assert!(handler.lock().is_empty());
6314 assert_eq!(aggregator.core.builder.volume, Quantity::from(7));
6315 }
6316
6317 #[rstest]
6318 fn test_value_runs_bar_aggregator_no_aggressor_and_zero_price_fall_back_to_plain_volume() {
6319 let instrument_id = InstrumentId::from("AAPL.XNAS");
6322 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
6323 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6324 let (handler, record) = recording_handler();
6325
6326 let mut aggregator = ValueRunsBarAggregator::new(bar_type, 2, 0, record);
6327
6328 let no_aggressor = TradeTick {
6329 instrument_id,
6330 price: Price::from("10.00"),
6331 size: Quantity::from(3),
6332 aggressor_side: AggressorSide::NoAggressor,
6333 ..TradeTick::default()
6334 };
6335 let zero_price = TradeTick {
6336 instrument_id,
6337 price: Price::from("0.00"),
6338 size: Quantity::from(4),
6339 aggressor_side: AggressorSide::Buy,
6340 ts_event: UnixNanos::from(1),
6341 ts_init: UnixNanos::from(1),
6342 ..TradeTick::default()
6343 };
6344 aggregator.handle_trade(no_aggressor);
6345 aggregator.handle_trade(zero_price);
6346
6347 assert!(handler.lock().is_empty());
6348 assert_eq!(aggregator.core.builder.volume, Quantity::from(7));
6349 }
6350
6351 #[rstest]
6352 fn test_value_imbalance_bar_aggregator_conserves_volume_with_indivisible_price() {
6353 let instrument_id = InstrumentId::from("AAPL.XNAS");
6358 let bar_spec = BarSpecification::new(1, BarAggregation::ValueImbalance, PriceType::Last);
6359 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6360 let (handler, record) = recording_handler();
6361
6362 let mut aggregator = ValueImbalanceBarAggregator::new(bar_type, 2, 1, record);
6363
6364 let input = Quantity::from("1.0");
6365 let trade = TradeTick {
6366 instrument_id,
6367 price: Price::from("3.00"),
6368 size: input,
6369 aggressor_side: AggressorSide::Buy,
6370 ..TradeTick::default()
6371 };
6372 aggregator.handle_trade(trade);
6373
6374 let handler_guard = handler.lock();
6375 assert_eq!(handler_guard.len(), 3);
6376 for bar in handler_guard.iter() {
6377 assert_eq!(bar.volume, Quantity::from("0.3"));
6378 }
6379 assert_eq!(aggregator.core.builder.volume, Quantity::from("0.1"));
6380 let emitted_plus_pending = handler_guard
6381 .iter()
6382 .map(|bar| bar.volume.as_decimal())
6383 .sum::<Decimal>()
6384 + aggregator.core.builder.volume.as_decimal();
6385 assert_eq!(emitted_plus_pending, input.as_decimal());
6386 }
6387
6388 #[rstest]
6389 fn test_value_runs_bar_aggregator_conserves_volume_with_indivisible_price() {
6390 let instrument_id = InstrumentId::from("AAPL.XNAS");
6395 let bar_spec = BarSpecification::new(1, BarAggregation::ValueRuns, PriceType::Last);
6396 let bar_type = BarType::new(instrument_id, bar_spec, AggregationSource::Internal);
6397 let (handler, record) = recording_handler();
6398
6399 let mut aggregator = ValueRunsBarAggregator::new(bar_type, 2, 1, record);
6400
6401 let input = Quantity::from("1.0");
6402 let trade = TradeTick {
6403 instrument_id,
6404 price: Price::from("3.00"),
6405 size: input,
6406 aggressor_side: AggressorSide::Buy,
6407 ..TradeTick::default()
6408 };
6409 aggregator.handle_trade(trade);
6410
6411 let handler_guard = handler.lock();
6412 assert_eq!(handler_guard.len(), 3);
6413 for bar in handler_guard.iter() {
6414 assert_eq!(bar.volume, Quantity::from("0.3"));
6415 }
6416 assert_eq!(aggregator.core.builder.volume, Quantity::from("0.1"));
6417 let emitted_plus_pending = handler_guard
6418 .iter()
6419 .map(|bar| bar.volume.as_decimal())
6420 .sum::<Decimal>()
6421 + aggregator.core.builder.volume.as_decimal();
6422 assert_eq!(emitted_plus_pending, input.as_decimal());
6423 }
6424
6425 #[rstest]
6426 #[case(1000_u64)]
6427 #[case(1500_u64)]
6428 fn test_volume_imbalance_bar_aggregator_large_step_no_overflow(
6429 equity_aapl: Equity,
6430 #[case] step: u64,
6431 ) {
6432 let instrument = InstrumentAny::Equity(equity_aapl);
6433 let bar_spec = BarSpecification::new(
6434 step as usize,
6435 BarAggregation::VolumeImbalance,
6436 PriceType::Last,
6437 );
6438 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6439 let (handler, record) = recording_handler();
6440
6441 let mut aggregator = VolumeImbalanceBarAggregator::new(
6442 bar_type,
6443 instrument.price_precision(),
6444 instrument.size_precision(),
6445 record,
6446 );
6447
6448 let trade = TradeTick {
6449 size: Quantity::from(step * 2),
6450 aggressor_side: AggressorSide::Buy,
6451 ..TradeTick::default()
6452 };
6453
6454 aggregator.handle_trade(trade);
6455
6456 let handler_guard = handler.lock();
6457 assert_eq!(handler_guard.len(), 2);
6458 for bar in handler_guard.iter() {
6459 assert_eq!(bar.volume.as_f64(), step as f64);
6460 }
6461 }
6462
6463 #[rstest]
6464 fn test_volume_imbalance_bar_aggregator_different_large_steps_produce_different_bar_counts(
6465 equity_aapl: Equity,
6466 ) {
6467 let instrument = InstrumentAny::Equity(equity_aapl);
6468 let total_volume = 3000_u64;
6469 let mut results = Vec::new();
6470
6471 for step in [1000_usize, 1500] {
6472 let bar_spec =
6473 BarSpecification::new(step, BarAggregation::VolumeImbalance, PriceType::Last);
6474 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6475 let (handler, record) = recording_handler();
6476
6477 let mut aggregator = VolumeImbalanceBarAggregator::new(
6478 bar_type,
6479 instrument.price_precision(),
6480 instrument.size_precision(),
6481 record,
6482 );
6483
6484 let trade = TradeTick {
6485 size: Quantity::from(total_volume),
6486 aggressor_side: AggressorSide::Buy,
6487 ..TradeTick::default()
6488 };
6489
6490 aggregator.handle_trade(trade);
6491
6492 let handler_guard = handler.lock();
6493 results.push(handler_guard.len());
6494 }
6495
6496 assert_eq!(results[0], 3); assert_eq!(results[1], 2); assert_ne!(results[0], results[1]);
6499 }
6500
6501 #[rstest]
6502 #[case(1000_u64)]
6503 #[case(1500_u64)]
6504 fn test_volume_runs_bar_aggregator_large_step_no_overflow(
6505 equity_aapl: Equity,
6506 #[case] step: u64,
6507 ) {
6508 let instrument = InstrumentAny::Equity(equity_aapl);
6509 let bar_spec =
6510 BarSpecification::new(step as usize, BarAggregation::VolumeRuns, PriceType::Last);
6511 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6512 let (handler, record) = recording_handler();
6513
6514 let mut aggregator = VolumeRunsBarAggregator::new(
6515 bar_type,
6516 instrument.price_precision(),
6517 instrument.size_precision(),
6518 record,
6519 );
6520
6521 let trade = TradeTick {
6522 size: Quantity::from(step * 2),
6523 aggressor_side: AggressorSide::Buy,
6524 ..TradeTick::default()
6525 };
6526
6527 aggregator.handle_trade(trade);
6528
6529 let handler_guard = handler.lock();
6530 assert_eq!(handler_guard.len(), 2);
6531 for bar in handler_guard.iter() {
6532 assert_eq!(bar.volume.as_f64(), step as f64);
6533 }
6534 }
6535
6536 #[rstest]
6537 fn test_volume_runs_bar_aggregator_different_large_steps_produce_different_bar_counts(
6538 equity_aapl: Equity,
6539 ) {
6540 let instrument = InstrumentAny::Equity(equity_aapl);
6541 let total_volume = 3000_u64;
6542 let mut results = Vec::new();
6543
6544 for step in [1000_usize, 1500] {
6545 let bar_spec = BarSpecification::new(step, BarAggregation::VolumeRuns, PriceType::Last);
6546 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6547 let (handler, record) = recording_handler();
6548
6549 let mut aggregator = VolumeRunsBarAggregator::new(
6550 bar_type,
6551 instrument.price_precision(),
6552 instrument.size_precision(),
6553 record,
6554 );
6555
6556 let trade = TradeTick {
6557 size: Quantity::from(total_volume),
6558 aggressor_side: AggressorSide::Buy,
6559 ..TradeTick::default()
6560 };
6561
6562 aggregator.handle_trade(trade);
6563
6564 let handler_guard = handler.lock();
6565 results.push(handler_guard.len());
6566 }
6567
6568 assert_eq!(results[0], 3); assert_eq!(results[1], 2); assert_ne!(results[0], results[1]);
6571 }
6572
6573 #[rstest]
6575 fn test_time_bar_historical_defers_event_at_ts_init_until_after_update(equity_aapl: Equity) {
6576 let instrument = InstrumentAny::Equity(equity_aapl);
6577 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
6578 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6579 let (handler, record) = recording_handler();
6580 let clock = Rc::new(RefCell::new(TestClock::new()));
6581
6582 let mut agg = TimeBarAggregator::new(
6583 bar_type,
6584 instrument.price_precision(),
6585 instrument.size_precision(),
6586 clock.clone(),
6587 record,
6588 true,
6589 true,
6590 BarIntervalType::LeftOpen,
6591 None,
6592 0,
6593 false,
6594 );
6595 agg.historical_mode = true;
6596 agg.set_clock_internal(clock);
6597 let boxed: Box<dyn BarAggregator> = Box::new(agg);
6598 let rc = Rc::new(RefCell::new(boxed));
6599 rc.borrow_mut().set_aggregator_weak(Rc::downgrade(&rc));
6600
6601 rc.borrow_mut().update(
6602 Price::from("100.00"),
6603 Quantity::from(1),
6604 UnixNanos::default(),
6605 );
6606 rc.borrow_mut().update(
6607 Price::from("100.00"),
6608 Quantity::from(1),
6609 UnixNanos::from(1_000_000_000),
6610 );
6611
6612 let bars = handler.lock();
6613 assert!(
6614 !bars.is_empty(),
6615 "deferred event at ts_init should produce a bar that includes the update"
6616 );
6617 let last_bar = bars.last().unwrap();
6618 assert_eq!(last_bar.close, Price::from("100.00"));
6619 assert!(
6620 last_bar.volume.as_f64() >= 1.0,
6621 "bar built after deferred event should include the update at ts_init"
6622 );
6623 }
6624
6625 #[rstest]
6626 #[case(10.03, 10.07, Price::from("10.00"), Price::from("10.10"))]
6627 #[case(-10.07, -10.03, Price::from("-10.10"), Price::from("-10.00"))]
6628 fn test_fixed_tick_scheme_rounder_rounds_bid_and_ask_outward(
6629 #[case] raw_bid: f64,
6630 #[case] raw_ask: f64,
6631 #[case] expected_bid: Price,
6632 #[case] expected_ask: Price,
6633 ) {
6634 let rounder = FixedTickSchemeRounder::new(0.05).unwrap();
6635
6636 let (bid, ask) = rounder.round_prices(raw_bid, raw_ask, 2);
6637
6638 assert_eq!(bid, expected_bid);
6639 assert_eq!(ask, expected_ask);
6640 }
6641
6642 #[rstest]
6643 fn test_spread_quote_quote_driven_emits_when_all_legs_received(equity_aapl: Equity) {
6644 let instrument = InstrumentAny::Equity(equity_aapl);
6645 let leg1 = instrument.id();
6646 let leg2 = InstrumentId::from("MSFT.XNAS");
6647 let spread_id = InstrumentId::from("SPREAD.XNAS");
6648 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
6649 let (handler, record) = recording_handler();
6650 let clock = Rc::new(RefCell::new(TestClock::new()));
6651
6652 let mut agg = SpreadQuoteAggregator::new(
6653 spread_id,
6654 &legs,
6655 true,
6656 instrument.price_precision(),
6657 0,
6658 Box::new(record),
6659 clock,
6660 false,
6661 None,
6662 0,
6663 false,
6664 60,
6665 None,
6666 None,
6667 );
6668
6669 let ts = UnixNanos::from(1_000_000_000);
6670 agg.handle_quote_tick(QuoteTick::new(
6671 leg1,
6672 Price::from("100.00"),
6673 Price::from("100.10"),
6674 Quantity::from(10),
6675 Quantity::from(10),
6676 ts,
6677 ts,
6678 ));
6679 assert_eq!(handler.lock().len(), 0);
6680
6681 agg.handle_quote_tick(QuoteTick::new(
6682 leg2,
6683 Price::from("99.00"),
6684 Price::from("99.10"),
6685 Quantity::from(10),
6686 Quantity::from(10),
6687 ts,
6688 ts,
6689 ));
6690 let quotes = handler.lock();
6691 assert_eq!(quotes.len(), 1);
6692 assert_eq!(quotes[0].instrument_id, spread_id);
6693 assert!(quotes[0].bid_price < quotes[0].ask_price);
6694 }
6695
6696 #[rstest]
6697 fn test_spread_quote_futures_pricing_signed_ratios(equity_aapl: Equity) {
6698 let instrument = InstrumentAny::Equity(equity_aapl);
6699 let leg1 = instrument.id();
6700 let leg2 = InstrumentId::from("MSFT.XNAS");
6701 let spread_id = InstrumentId::from("SPREAD.XNAS");
6702 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
6703 let (handler, record) = recording_handler();
6704 let clock = Rc::new(RefCell::new(TestClock::new()));
6705
6706 let mut agg = SpreadQuoteAggregator::new(
6707 spread_id,
6708 &legs,
6709 true,
6710 instrument.price_precision(),
6711 0,
6712 Box::new(record),
6713 clock,
6714 false,
6715 None,
6716 0,
6717 false,
6718 60,
6719 None,
6720 None,
6721 );
6722
6723 let ts = UnixNanos::from(1_000_000_000);
6724 agg.handle_quote_tick(QuoteTick::new(
6725 leg1,
6726 Price::from("10.00"),
6727 Price::from("10.10"),
6728 Quantity::from(100),
6729 Quantity::from(100),
6730 ts,
6731 ts,
6732 ));
6733 agg.handle_quote_tick(QuoteTick::new(
6734 leg2,
6735 Price::from("20.00"),
6736 Price::from("20.10"),
6737 Quantity::from(100),
6738 Quantity::from(100),
6739 ts,
6740 ts,
6741 ));
6742 let quotes = handler.lock();
6743 assert_eq!(quotes.len(), 1);
6744 let q = "es[0];
6745 assert_eq!(q.instrument_id, spread_id);
6746 assert_eq!(q.bid_price, Price::from("-10.10"));
6747 assert_eq!(q.ask_price, Price::from("-9.90"));
6748 }
6749
6750 #[rstest]
6751 fn test_spread_quote_size_calculation_non_unit_ratios(equity_aapl: Equity) {
6752 let instrument = InstrumentAny::Equity(equity_aapl);
6753 let leg1 = instrument.id();
6754 let leg2 = InstrumentId::from("MSFT.XNAS");
6755 let spread_id = InstrumentId::from("SPREAD.XNAS");
6756 let legs = vec![(leg1, 2_i64), (leg2, -1_i64)];
6757 let (handler, record) = recording_handler();
6758 let clock = Rc::new(RefCell::new(TestClock::new()));
6759
6760 let mut agg = SpreadQuoteAggregator::new(
6761 spread_id,
6762 &legs,
6763 true,
6764 instrument.price_precision(),
6765 0,
6766 Box::new(record),
6767 clock,
6768 false,
6769 None,
6770 0,
6771 false,
6772 60,
6773 None,
6774 None,
6775 );
6776
6777 let ts = UnixNanos::from(1_000_000_000);
6778 agg.handle_quote_tick(QuoteTick::new(
6779 leg1,
6780 Price::from("10.00"),
6781 Price::from("10.10"),
6782 Quantity::from(100),
6783 Quantity::from(40),
6784 ts,
6785 ts,
6786 ));
6787 agg.handle_quote_tick(QuoteTick::new(
6788 leg2,
6789 Price::from("10.00"),
6790 Price::from("10.10"),
6791 Quantity::from(50),
6792 Quantity::from(30),
6793 ts,
6794 ts,
6795 ));
6796 let quotes = handler.lock();
6797 assert_eq!(quotes.len(), 1);
6798 let q = "es[0];
6799 assert_eq!(q.bid_size.as_f64(), 30.0);
6800 assert_eq!(q.ask_size.as_f64(), 20.0);
6801 }
6802
6803 #[rstest]
6804 fn test_spread_quote_timer_driven_emission_cadence(equity_aapl: Equity) {
6805 let instrument = InstrumentAny::Equity(equity_aapl);
6806 let leg1 = instrument.id();
6807 let leg2 = InstrumentId::from("MSFT.XNAS");
6808 let spread_id = InstrumentId::from("SPREAD.XNAS");
6809 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
6810 let (handler, record) = recording_handler();
6811 let clock = Rc::new(RefCell::new(TestClock::new()));
6812 clock.borrow_mut().set_time(UnixNanos::from(0));
6813
6814 let agg = SpreadQuoteAggregator::new(
6815 spread_id,
6816 &legs,
6817 true,
6818 instrument.price_precision(),
6819 0,
6820 Box::new(record),
6821 clock.clone(),
6822 false,
6823 Some(1),
6824 0,
6825 false,
6826 60,
6827 None,
6828 None,
6829 );
6830 let rc = Rc::new(RefCell::new(agg));
6831 rc.borrow_mut().prepare_for_timer_mode(&rc);
6832 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
6833
6834 for event in clock.borrow_mut().advance_time(UnixNanos::from(0), true) {
6835 rc.borrow_mut().on_timer_fire(event.ts_event);
6836 }
6837 assert_eq!(handler.lock().len(), 0);
6838
6839 let ts1 = UnixNanos::from(1_000_000_000);
6840 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
6841 leg1,
6842 Price::from("100.00"),
6843 Price::from("100.10"),
6844 Quantity::from(10),
6845 Quantity::from(10),
6846 ts1,
6847 ts1,
6848 ));
6849 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
6850 leg2,
6851 Price::from("99.00"),
6852 Price::from("99.10"),
6853 Quantity::from(10),
6854 Quantity::from(10),
6855 ts1,
6856 ts1,
6857 ));
6858
6859 for event in clock.borrow_mut().advance_time(ts1, true) {
6860 rc.borrow_mut().on_timer_fire(event.ts_event);
6861 }
6862
6863 {
6864 let quotes = handler.lock();
6865 assert_eq!(quotes.len(), 1);
6866 assert_eq!(quotes[0].ts_event, ts1);
6867 assert_eq!(quotes[0].ts_init, ts1);
6868 }
6869
6870 let ts2 = UnixNanos::from(2_000_000_000);
6871 for event in clock.borrow_mut().advance_time(ts2, true) {
6872 rc.borrow_mut().on_timer_fire(event.ts_event);
6873 }
6874
6875 let quotes = handler.lock();
6876 assert_eq!(quotes.len(), 1);
6877 }
6878
6879 #[rstest]
6880 fn test_spread_quote_historical_timer_waits_for_all_legs(equity_aapl: Equity) {
6881 let instrument = InstrumentAny::Equity(equity_aapl);
6882 let leg1 = instrument.id();
6883 let leg2 = InstrumentId::from("MSFT.XNAS");
6884 let spread_id = InstrumentId::from("SPREAD.XNAS");
6885 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
6886 let (handler, record) = recording_handler();
6887 let clock = Rc::new(RefCell::new(TestClock::new()));
6888
6889 let agg = SpreadQuoteAggregator::new(
6890 spread_id,
6891 &legs,
6892 true,
6893 instrument.price_precision(),
6894 0,
6895 Box::new(record),
6896 clock.clone(),
6898 true,
6899 Some(1),
6900 0,
6901 false,
6902 60,
6903 None,
6904 None,
6905 );
6906 let rc = Rc::new(RefCell::new(agg));
6907 rc.borrow_mut().prepare_for_timer_mode(&rc);
6908 rc.borrow_mut().set_clock(clock);
6909
6910 let ts1 = UnixNanos::from(1_000_000_000);
6911 let ts2 = UnixNanos::from(2_000_000_000);
6912 let ts3 = UnixNanos::from(3_000_000_000);
6913 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
6914 leg1,
6915 Price::from("100.00"),
6916 Price::from("100.10"),
6917 Quantity::from(10),
6918 Quantity::from(10),
6919 ts1,
6920 ts1,
6921 ));
6922 assert_eq!(handler.lock().len(), 0);
6923
6924 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
6925 leg2,
6926 Price::from("99.00"),
6927 Price::from("99.10"),
6928 Quantity::from(10),
6929 Quantity::from(10),
6930 ts2,
6931 ts2,
6932 ));
6933 assert_eq!(handler.lock().len(), 0);
6934
6935 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
6936 leg1,
6937 Price::from("100.00"),
6938 Price::from("100.10"),
6939 Quantity::from(10),
6940 Quantity::from(10),
6941 ts3,
6942 ts3,
6943 ));
6944 let quotes = handler.lock();
6945 assert_eq!(
6946 quotes.len(),
6947 1,
6948 "deferred event at ts2 is processed when we have all legs and advance to ts3"
6949 );
6950 }
6951
6952 #[rstest]
6953 fn test_spread_quote_historical_flush_emits_pending_final_quote(equity_aapl: Equity) {
6954 let instrument = InstrumentAny::Equity(equity_aapl);
6955 let leg1 = instrument.id();
6956 let leg2 = InstrumentId::from("MSFT.XNAS");
6957 let spread_id = InstrumentId::from("SPREAD.XNAS");
6958 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
6959 let (handler, record) = recording_handler();
6960 let clock = Rc::new(RefCell::new(TestClock::new()));
6961
6962 let agg = SpreadQuoteAggregator::new(
6963 spread_id,
6964 &legs,
6965 true,
6966 instrument.price_precision(),
6967 0,
6968 Box::new(record),
6969 clock.clone(),
6971 true,
6972 Some(1),
6973 0,
6974 false,
6975 60,
6976 None,
6977 None,
6978 );
6979 let rc = Rc::new(RefCell::new(agg));
6980 rc.borrow_mut().prepare_for_timer_mode(&rc);
6981 rc.borrow_mut().set_clock(clock);
6982
6983 let ts1 = UnixNanos::from(1_000_000_000);
6984 let ts2 = UnixNanos::from(2_000_000_000);
6985 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
6986 leg1,
6987 Price::from("100.00"),
6988 Price::from("100.10"),
6989 Quantity::from(10),
6990 Quantity::from(10),
6991 ts1,
6992 ts1,
6993 ));
6994 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
6995 leg2,
6996 Price::from("99.00"),
6997 Price::from("99.10"),
6998 Quantity::from(10),
6999 Quantity::from(10),
7000 ts2,
7001 ts2,
7002 ));
7003
7004 assert_eq!(handler.lock().len(), 0);
7005
7006 rc.borrow_mut().flush_pending_historical_quote();
7007
7008 let quotes = handler.lock();
7009 assert_eq!(
7010 quotes.len(),
7011 1,
7012 "final historical quote should be emitted when the deferred event is flushed",
7013 );
7014 assert_eq!(quotes[0].ts_event, ts2);
7015 }
7016
7017 #[rstest]
7018 fn test_spread_quote_option_vega_weighting(equity_aapl: Equity) {
7019 let instrument = InstrumentAny::Equity(equity_aapl);
7020 let leg1 = instrument.id();
7021 let leg2 = InstrumentId::from("MSFT.XNAS");
7022 let spread_id = InstrumentId::from("SPREAD.XNAS");
7023 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
7024 let (handler, record) = recording_handler();
7025 let clock = Rc::new(RefCell::new(TestClock::new()));
7026
7027 let mut vega_provider = MapVegaProvider::new();
7028 vega_provider.insert(leg1, 0.15);
7029 vega_provider.insert(leg2, 0.12);
7030
7031 let mut agg = SpreadQuoteAggregator::new(
7032 spread_id,
7033 &legs,
7034 false,
7035 instrument.price_precision(),
7036 0,
7037 Box::new(record),
7038 clock,
7039 false,
7040 None,
7041 0,
7042 false,
7043 60,
7044 Some(Box::new(vega_provider)),
7045 None,
7046 );
7047
7048 let ts = UnixNanos::from(1_000_000_000);
7049 agg.handle_quote_tick(QuoteTick::new(
7050 leg1,
7051 Price::from("10.00"),
7052 Price::from("10.20"),
7053 Quantity::from(100),
7054 Quantity::from(100),
7055 ts,
7056 ts,
7057 ));
7058 agg.handle_quote_tick(QuoteTick::new(
7059 leg2,
7060 Price::from("11.00"),
7061 Price::from("11.20"),
7062 Quantity::from(100),
7063 Quantity::from(100),
7064 ts,
7065 ts,
7066 ));
7067 let quotes = handler.lock();
7068 assert_eq!(quotes.len(), 1);
7069 let q = "es[0];
7070 assert_eq!(q.instrument_id, spread_id);
7071 assert_eq!(q.bid_price, Price::from("-1.02"));
7072 assert_eq!(q.ask_price, Price::from("-0.98"));
7073 assert_eq!(q.bid_size, Quantity::from(100));
7074 assert_eq!(q.ask_size, Quantity::from(100));
7075 assert_eq!(q.ts_event, ts);
7076 assert_eq!(q.ts_init, ts);
7077 }
7078
7079 #[rstest]
7080 fn test_spread_quote_all_zero_vega_fallback(equity_aapl: Equity) {
7081 let instrument = InstrumentAny::Equity(equity_aapl);
7082 let leg1 = instrument.id();
7083 let leg2 = InstrumentId::from("MSFT.XNAS");
7084 let spread_id = InstrumentId::from("SPREAD.XNAS");
7085 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
7086 let (handler, record) = recording_handler();
7087 let clock = Rc::new(RefCell::new(TestClock::new()));
7088
7089 let mut vega_provider = MapVegaProvider::new();
7090 vega_provider.insert(leg1, 0.0);
7091 vega_provider.insert(leg2, 0.0);
7092
7093 let agg = SpreadQuoteAggregator::new(
7094 spread_id,
7095 &legs,
7096 false,
7097 instrument.price_precision(),
7098 0,
7099 Box::new(record),
7100 clock.clone(),
7101 false,
7102 None,
7103 0,
7104 false,
7105 1,
7106 Some(Box::new(vega_provider)),
7107 None,
7108 );
7109 let rc = Rc::new(RefCell::new(agg));
7110 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7111
7112 let ts = UnixNanos::from(1_000_000_000);
7113 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7114 leg1,
7115 Price::from("10.00"),
7116 Price::from("10.10"),
7117 Quantity::from(100),
7118 Quantity::from(100),
7119 ts,
7120 ts,
7121 ));
7122 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7123 leg2,
7124 Price::from("20.00"),
7125 Price::from("20.10"),
7126 Quantity::from(100),
7127 Quantity::from(100),
7128 ts,
7129 ts,
7130 ));
7131 {
7132 let quotes = handler.lock();
7133 assert_eq!(quotes.len(), 1);
7134 let q = "es[0];
7135 assert_eq!(q.bid_price, Price::from("-10.10"));
7136 assert_eq!(q.ask_price, Price::from("-9.90"));
7137 }
7138 assert!(rc.borrow().vega_pricing_temporarily_disabled);
7139
7140 let timeout_name = rc.borrow().vega_pricing_timeout_timer_name.clone();
7141 assert!(
7142 clock
7143 .borrow()
7144 .timer_names()
7145 .contains(&timeout_name.as_str())
7146 );
7147
7148 let events = clock
7149 .borrow_mut()
7150 .advance_time(UnixNanos::from(2_000_000_000), true);
7151
7152 for handler in clock.borrow().match_handlers(events) {
7153 handler.run();
7154 }
7155
7156 assert!(!rc.borrow().vega_pricing_temporarily_disabled);
7157
7158 let (_cancel_handler, record) = recording_handler();
7159 let mut cancel_vega_provider = MapVegaProvider::new();
7160 cancel_vega_provider.insert(leg1, 0.0);
7161 cancel_vega_provider.insert(leg2, 0.0);
7162 let cancel_agg = SpreadQuoteAggregator::new(
7163 spread_id,
7164 &legs,
7165 false,
7166 instrument.price_precision(),
7167 0,
7168 Box::new(record),
7169 clock.clone(),
7170 false,
7171 None,
7172 0,
7173 false,
7174 10,
7175 Some(Box::new(cancel_vega_provider)),
7176 None,
7177 );
7178 let cancel_rc = Rc::new(RefCell::new(cancel_agg));
7179 cancel_rc
7180 .borrow_mut()
7181 .start_timer(Some(Rc::clone(&cancel_rc)));
7182 cancel_rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7183 leg1,
7184 Price::from("10.00"),
7185 Price::from("10.10"),
7186 Quantity::from(100),
7187 Quantity::from(100),
7188 ts,
7189 ts,
7190 ));
7191 cancel_rc.borrow_mut().handle_quote_tick(QuoteTick::new(
7192 leg2,
7193 Price::from("20.00"),
7194 Price::from("20.10"),
7195 Quantity::from(100),
7196 Quantity::from(100),
7197 ts,
7198 ts,
7199 ));
7200 let cancel_timeout_name = cancel_rc.borrow().vega_pricing_timeout_timer_name.clone();
7201 assert!(
7202 clock
7203 .borrow()
7204 .timer_names()
7205 .contains(&cancel_timeout_name.as_str())
7206 );
7207 cancel_rc.borrow_mut().stop_timer();
7208 assert!(
7209 !clock
7210 .borrow()
7211 .timer_names()
7212 .contains(&cancel_timeout_name.as_str())
7213 );
7214
7215 let (permanent_handler, record) = recording_handler();
7216 let mut permanent_vega_provider = MapVegaProvider::new();
7217 permanent_vega_provider.insert(leg1, 0.15);
7218 permanent_vega_provider.insert(leg2, 0.12);
7219 let mut permanent_agg = SpreadQuoteAggregator::new(
7220 spread_id,
7221 &legs,
7222 false,
7223 instrument.price_precision(),
7224 0,
7225 Box::new(record),
7226 Rc::new(RefCell::new(TestClock::new())),
7227 false,
7228 None,
7229 0,
7230 true,
7231 1,
7232 Some(Box::new(permanent_vega_provider)),
7233 None,
7234 );
7235
7236 permanent_agg.handle_quote_tick(QuoteTick::new(
7237 leg1,
7238 Price::from("10.00"),
7239 Price::from("10.10"),
7240 Quantity::from(100),
7241 Quantity::from(100),
7242 ts,
7243 ts,
7244 ));
7245 permanent_agg.handle_quote_tick(QuoteTick::new(
7246 leg2,
7247 Price::from("20.00"),
7248 Price::from("20.10"),
7249 Quantity::from(100),
7250 Quantity::from(100),
7251 ts,
7252 ts,
7253 ));
7254
7255 let permanent_quotes = permanent_handler.lock();
7256 assert_eq!(permanent_quotes.len(), 1);
7257 assert_eq!(permanent_quotes[0].bid_price, Price::from("-10.10"));
7258 assert_eq!(permanent_quotes[0].ask_price, Price::from("-9.90"));
7259 assert!(!permanent_agg.vega_pricing_temporarily_disabled);
7260 }
7261
7262 #[rstest]
7263 fn test_spread_quote_negative_prices_tick_scheme(equity_aapl: Equity) {
7264 let instrument = InstrumentAny::Equity(equity_aapl);
7265 let leg1 = instrument.id();
7266 let leg2 = InstrumentId::from("MSFT.XNAS");
7267 let spread_id = InstrumentId::from("SPREAD.XNAS");
7268 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
7269 let (handler, record) = recording_handler();
7270 let clock = Rc::new(RefCell::new(TestClock::new()));
7271 let rounder = FixedTickSchemeRounder::new(0.01).unwrap();
7272
7273 let mut agg = SpreadQuoteAggregator::new(
7274 spread_id,
7275 &legs,
7276 true,
7277 2,
7278 0,
7279 Box::new(record),
7280 clock,
7281 false,
7282 None,
7283 0,
7284 false,
7285 60,
7286 None,
7287 Some(Box::new(rounder)),
7288 );
7289
7290 let ts = UnixNanos::from(1_000_000_000);
7291 agg.handle_quote_tick(QuoteTick::new(
7292 leg1,
7293 Price::from("10.00"),
7294 Price::from("10.10"),
7295 Quantity::from(100),
7296 Quantity::from(100),
7297 ts,
7298 ts,
7299 ));
7300 agg.handle_quote_tick(QuoteTick::new(
7301 leg2,
7302 Price::from("20.00"),
7303 Price::from("20.10"),
7304 Quantity::from(100),
7305 Quantity::from(100),
7306 ts,
7307 ts,
7308 ));
7309 let quotes = handler.lock();
7310 assert_eq!(quotes.len(), 1);
7311 let q = "es[0];
7312 assert!(q.bid_price.as_f64() < 0.0);
7313 assert!(q.ask_price.as_f64() < 0.0);
7314 assert!(q.bid_price < q.ask_price);
7315 }
7316
7317 #[rstest]
7318 #[case(BarIntervalType::LeftOpen)]
7319 #[case(BarIntervalType::RightOpen)]
7320 fn test_time_bar_skip_first_non_full_bar_noop_on_boundary(
7321 equity_aapl: Equity,
7322 #[case] interval_type: BarIntervalType,
7323 ) {
7324 let instrument = InstrumentAny::Equity(equity_aapl);
7329 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
7330 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7331 let (handler, record) = recording_handler();
7332 let clock = Rc::new(RefCell::new(TestClock::new()));
7333 clock.borrow_mut().set_time(UnixNanos::from(1_000_000_000));
7334 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7335
7336 let aggregator = TimeBarAggregator::new(
7337 bar_type,
7338 instrument.price_precision(),
7339 instrument.size_precision(),
7340 clock,
7341 record,
7342 false,
7343 false,
7344 interval_type,
7345 None,
7346 0,
7347 true, );
7349
7350 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7351 let rc = Rc::new(RefCell::new(boxed));
7352 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7353
7354 rc.borrow_mut().update(
7355 Price::from("100.00"),
7356 Quantity::from(1),
7357 UnixNanos::from(1_000_000_000),
7358 );
7359 rc.borrow_mut().build_bar(&TimeEvent::new(
7360 event_name,
7361 UUID4::new(),
7362 UnixNanos::from(2_000_000_000),
7363 UnixNanos::from(2_000_000_000),
7364 ));
7365 rc.borrow_mut().update(
7366 Price::from("101.00"),
7367 Quantity::from(1),
7368 UnixNanos::from(2_500_000_000),
7369 );
7370 rc.borrow_mut().build_bar(&TimeEvent::new(
7371 event_name,
7372 UUID4::new(),
7373 UnixNanos::from(3_000_000_000),
7374 UnixNanos::from(3_000_000_000),
7375 ));
7376
7377 let bars = handler.lock();
7378 assert_eq!(bars.len(), 2);
7379 assert_eq!(bars[0].close, Price::from("100.00"));
7380 assert_eq!(bars[1].close, Price::from("101.00"));
7381 }
7382
7383 #[rstest]
7384 #[case(BarIntervalType::LeftOpen)]
7385 #[case(BarIntervalType::RightOpen)]
7386 fn test_time_bar_skip_first_non_full_bar_drops_partial_bar(
7387 equity_aapl: Equity,
7388 #[case] interval_type: BarIntervalType,
7389 ) {
7390 let instrument = InstrumentAny::Equity(equity_aapl);
7394 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
7395 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7396 let (handler, record) = recording_handler();
7397 let clock = Rc::new(RefCell::new(TestClock::new()));
7398 clock.borrow_mut().set_time(UnixNanos::from(1_500_000_000));
7399 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7400
7401 let aggregator = TimeBarAggregator::new(
7402 bar_type,
7403 instrument.price_precision(),
7404 instrument.size_precision(),
7405 clock,
7406 record,
7407 false,
7408 false,
7409 interval_type,
7410 None,
7411 0,
7412 true, );
7414
7415 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7416 let rc = Rc::new(RefCell::new(boxed));
7417 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7418
7419 rc.borrow_mut().update(
7420 Price::from("100.00"),
7421 Quantity::from(1),
7422 UnixNanos::from(1_500_000_000),
7423 );
7424 rc.borrow_mut().build_bar(&TimeEvent::new(
7425 event_name,
7426 UUID4::new(),
7427 UnixNanos::from(2_000_000_000),
7428 UnixNanos::from(2_000_000_000),
7429 ));
7430 rc.borrow_mut().update(
7431 Price::from("101.00"),
7432 Quantity::from(1),
7433 UnixNanos::from(2_500_000_000),
7434 );
7435 rc.borrow_mut().build_bar(&TimeEvent::new(
7436 event_name,
7437 UUID4::new(),
7438 UnixNanos::from(3_000_000_000),
7439 UnixNanos::from(3_000_000_000),
7440 ));
7441
7442 let bars = handler.lock();
7443 assert_eq!(bars.len(), 1);
7444 assert_eq!(bars[0].close, Price::from("101.00"));
7445 }
7446
7447 #[rstest]
7448 fn test_time_bar_skip_first_non_full_bar_skips_every_call_before_first_close(
7449 equity_aapl: Equity,
7450 ) {
7451 let instrument = InstrumentAny::Equity(equity_aapl);
7455 let bar_spec = BarSpecification::new(10, BarAggregation::Second, PriceType::Last);
7456 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7457 let (handler, record) = recording_handler();
7458 let clock = Rc::new(RefCell::new(TestClock::new()));
7459 clock.borrow_mut().set_time(UnixNanos::from(5_000_000_000));
7460 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7461
7462 let aggregator = TimeBarAggregator::new(
7463 bar_type,
7464 instrument.price_precision(),
7465 instrument.size_precision(),
7466 clock,
7467 record,
7468 false,
7469 false,
7470 BarIntervalType::LeftOpen,
7471 None,
7472 0,
7473 true, );
7475
7476 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7477 let rc = Rc::new(RefCell::new(boxed));
7478 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7479
7480 for (price, update_ts, event_ts) in [
7484 ("100.00", 5_500_000_000_u64, 7_000_000_000_u64),
7485 ("101.00", 7_500_000_000_u64, 8_000_000_000_u64),
7486 ("102.00", 9_000_000_000_u64, 10_000_000_000_u64),
7487 ] {
7488 rc.borrow_mut().update(
7489 Price::from(price),
7490 Quantity::from(1),
7491 UnixNanos::from(update_ts),
7492 );
7493 rc.borrow_mut().build_bar(&TimeEvent::new(
7494 event_name,
7495 UUID4::new(),
7496 UnixNanos::from(event_ts),
7497 UnixNanos::from(event_ts),
7498 ));
7499 }
7500
7501 rc.borrow_mut().update(
7503 Price::from("103.00"),
7504 Quantity::from(1),
7505 UnixNanos::from(10_500_000_000),
7506 );
7507 rc.borrow_mut().build_bar(&TimeEvent::new(
7508 event_name,
7509 UUID4::new(),
7510 UnixNanos::from(11_000_000_000),
7511 UnixNanos::from(11_000_000_000),
7512 ));
7513
7514 let bars = handler.lock();
7515 assert_eq!(bars.len(), 1);
7516 assert_eq!(bars[0].close, Price::from("103.00"));
7517 }
7518
7519 #[rstest]
7520 fn test_time_bar_skip_first_non_full_bar_skips_when_build_delay_shifts_start(
7521 equity_aapl: Equity,
7522 ) {
7523 let instrument = InstrumentAny::Equity(equity_aapl);
7527 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
7528 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7529 let (handler, record) = recording_handler();
7530 let clock = Rc::new(RefCell::new(TestClock::new()));
7531 clock.borrow_mut().set_time(UnixNanos::from(2_000_000_000));
7532 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7533
7534 let aggregator = TimeBarAggregator::new(
7535 bar_type,
7536 instrument.price_precision(),
7537 instrument.size_precision(),
7538 clock,
7539 record,
7540 false,
7541 false,
7542 BarIntervalType::LeftOpen,
7543 None,
7544 100, true, );
7547
7548 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7549 let rc = Rc::new(RefCell::new(boxed));
7550 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7551
7552 rc.borrow_mut().update(
7554 Price::from("100.00"),
7555 Quantity::from(1),
7556 UnixNanos::from(2_500_000_000),
7557 );
7558 rc.borrow_mut().build_bar(&TimeEvent::new(
7559 event_name,
7560 UUID4::new(),
7561 UnixNanos::from(3_000_100_000),
7562 UnixNanos::from(3_000_100_000),
7563 ));
7564 rc.borrow_mut().update(
7565 Price::from("101.00"),
7566 Quantity::from(1),
7567 UnixNanos::from(3_500_000_000),
7568 );
7569 rc.borrow_mut().build_bar(&TimeEvent::new(
7570 event_name,
7571 UUID4::new(),
7572 UnixNanos::from(4_000_100_000),
7573 UnixNanos::from(4_000_100_000),
7574 ));
7575
7576 let bars = handler.lock();
7577 assert_eq!(bars.len(), 1);
7578 assert_eq!(bars[0].close, Price::from("101.00"));
7579 }
7580
7581 #[rstest]
7582 #[case(
7583 BarAggregation::Month,
7584 1_735_689_600_000_000_000_u64,
7585 1_733_011_200_000_000_000_u64
7586 )]
7587 #[case(
7588 BarAggregation::Year,
7589 1_735_689_600_000_000_000_u64,
7590 1_704_067_200_000_000_000_u64
7591 )]
7592 fn test_time_bar_fire_immediately_month_year_stored_open_points_to_previous_period(
7593 equity_aapl: Equity,
7594 #[case] aggregation: BarAggregation,
7595 #[case] start_ns: u64,
7596 #[case] expected_stored_open_ns: u64,
7597 ) {
7598 let instrument = InstrumentAny::Equity(equity_aapl);
7602 let bar_spec = BarSpecification::new(1, aggregation, PriceType::Last);
7603 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7604 let (handler, record) = recording_handler();
7605 let clock = Rc::new(RefCell::new(TestClock::new()));
7606 clock.borrow_mut().set_time(UnixNanos::from(start_ns));
7607 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7608
7609 let aggregator = TimeBarAggregator::new(
7610 bar_type,
7611 instrument.price_precision(),
7612 instrument.size_precision(),
7613 clock,
7614 record,
7615 false,
7616 false,
7617 BarIntervalType::RightOpen, None,
7619 0,
7620 false, );
7622
7623 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7624 let rc = Rc::new(RefCell::new(boxed));
7625 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7626
7627 rc.borrow_mut().update(
7628 Price::from("100.00"),
7629 Quantity::from(1),
7630 UnixNanos::from(start_ns),
7631 );
7632 rc.borrow_mut().build_bar(&TimeEvent::new(
7633 event_name,
7634 UUID4::new(),
7635 UnixNanos::from(start_ns),
7636 UnixNanos::from(start_ns),
7637 ));
7638
7639 let bars = handler.lock();
7640 assert_eq!(bars.len(), 1);
7641 assert_eq!(bars[0].ts_event, UnixNanos::from(expected_stored_open_ns));
7642 assert_eq!(bars[0].ts_init, UnixNanos::from(start_ns));
7643 }
7644
7645 #[rstest]
7646 fn test_time_bar_historical_prevents_bars_for_timer_before_last_data(equity_aapl: Equity) {
7647 let instrument = InstrumentAny::Equity(equity_aapl);
7648 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
7649 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7650 let (handler, record) = recording_handler();
7651 let clock = Rc::new(RefCell::new(TestClock::new()));
7652
7653 let mut agg = TimeBarAggregator::new(
7654 bar_type,
7655 instrument.price_precision(),
7656 instrument.size_precision(),
7657 clock.clone(),
7658 record,
7659 true,
7660 true,
7661 BarIntervalType::LeftOpen,
7662 None,
7663 0,
7664 false,
7665 );
7666 agg.historical_mode = true;
7667 agg.set_clock_internal(clock);
7668 let boxed: Box<dyn BarAggregator> = Box::new(agg);
7669 let rc = Rc::new(RefCell::new(boxed));
7670 rc.borrow_mut().set_aggregator_weak(Rc::downgrade(&rc));
7671
7672 let ts1 = UnixNanos::from(2_000_000_000);
7673 rc.borrow_mut()
7674 .update(Price::from("100.00"), Quantity::from(1), ts1);
7675
7676 let ts2 = UnixNanos::from(3_000_000_000);
7677 rc.borrow_mut()
7678 .update(Price::from("101.00"), Quantity::from(1), ts2);
7679
7680 let bars = handler.lock();
7681 assert!(
7682 !bars.is_empty(),
7683 "advancing time from ts1 to ts2 should produce at least one bar"
7684 );
7685 assert_eq!(bars[0].close, Price::from("100.00"));
7686 }
7687
7688 #[rstest]
7689 #[case(BarAggregation::Tick)]
7690 #[case(BarAggregation::TickImbalance)]
7691 #[case(BarAggregation::TickRuns)]
7692 #[case(BarAggregation::Volume)]
7693 #[case(BarAggregation::VolumeImbalance)]
7694 #[case(BarAggregation::VolumeRuns)]
7695 #[case(BarAggregation::Value)]
7696 #[case(BarAggregation::ValueImbalance)]
7697 #[case(BarAggregation::ValueRuns)]
7698 #[case(BarAggregation::Renko)]
7699 fn test_aggregators_standardize_composite_bar_type(
7700 equity_aapl: Equity,
7701 #[case] aggregation: BarAggregation,
7702 ) {
7703 let instrument = InstrumentAny::Equity(equity_aapl);
7704 let bar_type = BarType::new_composite(
7705 instrument.id(),
7706 BarSpecification::new(10, aggregation, PriceType::Last),
7707 AggregationSource::Internal,
7708 1,
7709 BarAggregation::Minute,
7710 AggregationSource::External,
7711 );
7712 let handler = |_: Bar| {};
7713
7714 let aggregator: Box<dyn BarAggregator> = match aggregation {
7715 BarAggregation::Tick => Box::new(TickBarAggregator::new(
7716 bar_type,
7717 instrument.price_precision(),
7718 instrument.size_precision(),
7719 handler,
7720 )),
7721 BarAggregation::TickImbalance => Box::new(TickImbalanceBarAggregator::new(
7722 bar_type,
7723 instrument.price_precision(),
7724 instrument.size_precision(),
7725 handler,
7726 )),
7727 BarAggregation::TickRuns => Box::new(TickRunsBarAggregator::new(
7728 bar_type,
7729 instrument.price_precision(),
7730 instrument.size_precision(),
7731 handler,
7732 )),
7733 BarAggregation::Volume => Box::new(VolumeBarAggregator::new(
7734 bar_type,
7735 instrument.price_precision(),
7736 instrument.size_precision(),
7737 handler,
7738 )),
7739 BarAggregation::VolumeImbalance => Box::new(VolumeImbalanceBarAggregator::new(
7740 bar_type,
7741 instrument.price_precision(),
7742 instrument.size_precision(),
7743 handler,
7744 )),
7745 BarAggregation::VolumeRuns => Box::new(VolumeRunsBarAggregator::new(
7746 bar_type,
7747 instrument.price_precision(),
7748 instrument.size_precision(),
7749 handler,
7750 )),
7751 BarAggregation::Value => Box::new(ValueBarAggregator::new(
7752 bar_type,
7753 instrument.price_precision(),
7754 instrument.size_precision(),
7755 handler,
7756 )),
7757 BarAggregation::ValueImbalance => Box::new(ValueImbalanceBarAggregator::new(
7758 bar_type,
7759 instrument.price_precision(),
7760 instrument.size_precision(),
7761 handler,
7762 )),
7763 BarAggregation::ValueRuns => Box::new(ValueRunsBarAggregator::new(
7764 bar_type,
7765 instrument.price_precision(),
7766 instrument.size_precision(),
7767 handler,
7768 )),
7769 BarAggregation::Renko => Box::new(RenkoBarAggregator::new(
7770 bar_type,
7771 instrument.price_precision(),
7772 instrument.size_precision(),
7773 Price::from("0.01"),
7774 handler,
7775 )),
7776 _ => unreachable!(),
7777 };
7778
7779 assert!(aggregator.bar_type().is_standard());
7780 assert_eq!(aggregator.bar_type(), bar_type.standard());
7781 }
7782
7783 #[rstest]
7784 fn test_composite_tick_bar_aggregator_emits_standard_bar_type(equity_aapl: Equity) {
7785 let instrument = InstrumentAny::Equity(equity_aapl);
7786 let bar_type = BarType::new_composite(
7787 instrument.id(),
7788 BarSpecification::new(1, BarAggregation::Tick, PriceType::Last),
7789 AggregationSource::Internal,
7790 1,
7791 BarAggregation::Minute,
7792 AggregationSource::External,
7793 );
7794 let (handler, record) = recording_handler();
7795
7796 let mut aggregator = TickBarAggregator::new(
7797 bar_type,
7798 instrument.price_precision(),
7799 instrument.size_precision(),
7800 record,
7801 );
7802
7803 let input_bar = Bar::new(
7804 bar_type.composite(),
7805 Price::from("100.00"),
7806 Price::from("101.00"),
7807 Price::from("99.00"),
7808 Price::from("100.50"),
7809 Quantity::from(10),
7810 UnixNanos::from(1_000),
7811 UnixNanos::from(1_000),
7812 );
7813 aggregator.handle_bar(input_bar);
7814
7815 let handler_guard = handler.lock();
7816 assert_eq!(handler_guard.len(), 1);
7817 assert_eq!(handler_guard[0].bar_type, bar_type.standard());
7818 }
7819
7820 #[rstest]
7821 fn test_composite_time_bar_aggregator_uses_standard_timer_name(equity_aapl: Equity) {
7822 let instrument = InstrumentAny::Equity(equity_aapl);
7823 let bar_type = BarType::new_composite(
7824 instrument.id(),
7825 BarSpecification::new(5, BarAggregation::Minute, PriceType::Last),
7826 AggregationSource::Internal,
7827 1,
7828 BarAggregation::Minute,
7829 AggregationSource::External,
7830 );
7831 let clock = Rc::new(RefCell::new(TestClock::new()));
7832
7833 let aggregator = TimeBarAggregator::new(
7834 bar_type,
7835 instrument.price_precision(),
7836 instrument.size_precision(),
7837 clock.clone(),
7838 |_: Bar| {},
7839 false,
7840 true,
7841 BarIntervalType::LeftOpen,
7842 None,
7843 0,
7844 false,
7845 );
7846
7847 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7848 let rc = Rc::new(RefCell::new(boxed));
7849 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7850
7851 let expected = format!("TIME_BAR_{}", bar_type.standard());
7852 assert!(
7853 clock.borrow().timer_names().contains(&expected.as_str()),
7854 "timer names {:?} should contain {expected}",
7855 clock.borrow().timer_names(),
7856 );
7857 }
7858
7859 pub(super) fn recording_handler<T: 'static>() -> (Arc<Mutex<Vec<T>>>, impl FnMut(T)) {
7860 let events = Arc::new(Mutex::new(Vec::new()));
7861 let recorded_events = Arc::clone(&events);
7862 (events, move |event| recorded_events.lock().push(event))
7863 }
7864}
7865
7866#[cfg(test)]
7867mod property_tests {
7868 use std::{cell::RefCell, rc::Rc};
7869
7870 use nautilus_common::{clock::TestClock, timer::TimeEvent};
7871 use nautilus_core::{UUID4, UnixNanos};
7872 use nautilus_model::{
7873 data::{Bar, BarSpecification, BarType, TradeTick, bar::get_bar_interval_ns},
7874 enums::{AggregationSource, AggressorSide, BarAggregation, BarIntervalType, PriceType},
7875 instruments::{Instrument, InstrumentAny, stubs::equity_aapl},
7876 types::{Price, Quantity},
7877 };
7878 use proptest::prelude::*;
7879 use rstest::rstest;
7880 use ustr::Ustr;
7881
7882 use super::{tests::recording_handler, *};
7883
7884 fn time_bar_spec_strategy() -> impl Strategy<Value = (BarAggregation, usize)> {
7885 prop_oneof![
7886 (Just(BarAggregation::Second), 1usize..=5),
7887 (Just(BarAggregation::Minute), 1usize..=5),
7888 (Just(BarAggregation::Hour), 1usize..=4),
7889 ]
7890 }
7891
7892 fn interval_type_strategy() -> impl Strategy<Value = BarIntervalType> {
7893 prop_oneof![
7894 Just(BarIntervalType::LeftOpen),
7895 Just(BarIntervalType::RightOpen),
7896 ]
7897 }
7898
7899 proptest! {
7900 #[rstest]
7901 fn prop_skip_first_drops_partial_then_emits(
7902 (aggregation, step) in time_bar_spec_strategy(),
7903 interval_type in interval_type_strategy(),
7904 skip_first in any::<bool>(),
7905 ) {
7906 let instrument = InstrumentAny::Equity(equity_aapl());
7907 let bar_spec = BarSpecification::new(step, aggregation, PriceType::Last);
7908 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7909 let interval_ns = get_bar_interval_ns(&bar_type);
7910
7911 let now_ns = UnixNanos::default() + interval_ns + interval_ns / 2;
7914
7915 let (handler, record) = recording_handler();
7916 let clock = Rc::new(RefCell::new(TestClock::new()));
7917 clock.borrow_mut().set_time(now_ns);
7918 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7919
7920 let aggregator = TimeBarAggregator::new(
7921 bar_type,
7922 instrument.price_precision(),
7923 instrument.size_precision(),
7924 clock,
7925 record,
7926 false,
7927 false,
7928 interval_type,
7929 None,
7930 0,
7931 skip_first,
7932 );
7933
7934 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7935 let rc = Rc::new(RefCell::new(boxed));
7936 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7937
7938 rc.borrow_mut().update(
7941 Price::from("100.00"),
7942 Quantity::from(1),
7943 now_ns,
7944 );
7945 let first_close = UnixNanos::default() + interval_ns * 2;
7946 rc.borrow_mut().build_bar(&TimeEvent::new(
7947 event_name,
7948 UUID4::new(),
7949 first_close,
7950 first_close,
7951 ));
7952
7953 rc.borrow_mut().update(
7955 Price::from("101.00"),
7956 Quantity::from(1),
7957 first_close + interval_ns / 2,
7958 );
7959 let second_close = first_close + interval_ns;
7960 rc.borrow_mut().build_bar(&TimeEvent::new(
7961 event_name,
7962 UUID4::new(),
7963 second_close,
7964 second_close,
7965 ));
7966
7967 let bars = handler.lock();
7968 let expected = if skip_first { 1 } else { 2 };
7969 prop_assert_eq!(bars.len(), expected);
7970 prop_assert_eq!(bars.last().unwrap().close, Price::from("101.00"));
7971 for bar in bars.iter() {
7972 prop_assert!(bar.high >= bar.open);
7973 prop_assert!(bar.high >= bar.close);
7974 prop_assert!(bar.low <= bar.open);
7975 prop_assert!(bar.low <= bar.close);
7976 }
7977 }
7978
7979 #[rstest]
7980 fn prop_skip_first_noop_on_exact_boundary(
7981 (aggregation, step) in time_bar_spec_strategy(),
7982 interval_type in interval_type_strategy(),
7983 ) {
7984 let instrument = InstrumentAny::Equity(equity_aapl());
7985 let bar_spec = BarSpecification::new(step, aggregation, PriceType::Last);
7986 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7987 let interval_ns = get_bar_interval_ns(&bar_type);
7988
7989 let now_ns = UnixNanos::default() + interval_ns;
7992 let (handler, record) = recording_handler();
7993 let clock = Rc::new(RefCell::new(TestClock::new()));
7994 clock.borrow_mut().set_time(now_ns);
7995 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7996
7997 let aggregator = TimeBarAggregator::new(
7998 bar_type,
7999 instrument.price_precision(),
8000 instrument.size_precision(),
8001 clock,
8002 record,
8003 false,
8004 false,
8005 interval_type,
8006 None,
8007 0,
8008 true, );
8010
8011 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
8012 let rc = Rc::new(RefCell::new(boxed));
8013 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
8014
8015 rc.borrow_mut().update(
8016 Price::from("100.00"),
8017 Quantity::from(1),
8018 now_ns,
8019 );
8020 let next_close = now_ns + interval_ns;
8021 rc.borrow_mut().build_bar(&TimeEvent::new(
8022 event_name,
8023 UUID4::new(),
8024 next_close,
8025 next_close,
8026 ));
8027
8028 let bars = handler.lock();
8029 prop_assert_eq!(bars.len(), 1);
8030 prop_assert_eq!(bars[0].close, Price::from("100.00"));
8031 }
8032
8033 #[rstest]
8034 fn prop_bar_builder_ohlc_invariants(
8035 updates in prop::collection::vec((1i64..=100_000i64, 1u64..=1_000u64), 1..=50),
8036 ) {
8037 let instrument = InstrumentAny::Equity(equity_aapl());
8038 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
8039 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8040 let mut builder = BarBuilder::new(bar_type, 2, 0);
8041
8042 let mut total_volume: u64 = 0;
8043
8044 for (i, (price_cents, size)) in updates.iter().enumerate() {
8045 let price = Price::new((*price_cents as f64) / 100.0, 2);
8046 let qty = Quantity::new(*size as f64, 0);
8047 let ts = UnixNanos::from((i as u64 + 1) * 1_000);
8048 total_volume += *size;
8049 builder.update(price, qty, ts);
8050 }
8051
8052 let bar = builder.build_now();
8053 prop_assert!(bar.low <= bar.open);
8054 prop_assert!(bar.low <= bar.close);
8055 prop_assert!(bar.high >= bar.open);
8056 prop_assert!(bar.high >= bar.close);
8057 prop_assert!(bar.low <= bar.high);
8058 prop_assert_eq!(bar.volume.as_f64(), total_volume as f64);
8059 }
8060
8061 #[rstest]
8062 fn prop_tick_bar_aggregator_volume_conservation(
8063 ticks in prop::collection::vec((1i64..=1_000i64, 1u64..=100u64), 3..=60),
8064 step in 1usize..=5,
8065 ) {
8066 let instrument = InstrumentAny::Equity(equity_aapl());
8067 let bar_spec = BarSpecification::new(step, BarAggregation::Tick, PriceType::Last);
8068 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8069 let (handler, record) = recording_handler();
8070
8071 let mut aggregator = TickBarAggregator::new(
8072 bar_type,
8073 instrument.price_precision(),
8074 instrument.size_precision(),
8075 record,
8076 );
8077
8078 let mut total_input: u64 = 0;
8079
8080 for (i, (price_cents, size)) in ticks.iter().enumerate() {
8081 let price = Price::new((*price_cents as f64) / 100.0, 2);
8082 let qty = Quantity::new(*size as f64, 0);
8083 aggregator.update(price, qty, UnixNanos::from((i as u64 + 1) * 1_000));
8084 total_input += *size;
8085 }
8086
8087 let bars = handler.lock();
8088 let emitted_count = bars.len();
8089 prop_assert_eq!(emitted_count, ticks.len() / step);
8090
8091 let mut sum_emitted: f64 = 0.0;
8092
8093 for bar in bars.iter() {
8094 prop_assert!(bar.low <= bar.open);
8095 prop_assert!(bar.low <= bar.close);
8096 prop_assert!(bar.high >= bar.open);
8097 prop_assert!(bar.high >= bar.close);
8098 sum_emitted += bar.volume.as_f64();
8099 }
8100
8101 let pending_size: u64 = ticks.iter()
8103 .skip(emitted_count * step)
8104 .map(|(_, s)| *s)
8105 .sum();
8106 prop_assert!((sum_emitted + pending_size as f64 - total_input as f64).abs() < 1e-6);
8107 }
8108
8109 #[rstest]
8110 fn prop_volume_bar_aggregator_conservation(
8111 sizes in prop::collection::vec(1u64..=50u64, 3..=40),
8112 step in 2u64..=10u64,
8113 ) {
8114 let instrument = InstrumentAny::Equity(equity_aapl());
8115 let bar_spec = BarSpecification::new(step as usize, BarAggregation::Volume, PriceType::Last);
8116 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8117 let (handler, record) = recording_handler();
8118
8119 let mut aggregator = VolumeBarAggregator::new(
8120 bar_type,
8121 instrument.price_precision(),
8122 instrument.size_precision(),
8123 record,
8124 );
8125
8126 let mut total_input: u64 = 0;
8127
8128 for (i, size) in sizes.iter().enumerate() {
8129 aggregator.update(
8130 Price::from("100.00"),
8131 Quantity::new(*size as f64, 0),
8132 UnixNanos::from((i as u64 + 1) * 1_000),
8133 );
8134 total_input += *size;
8135 }
8136
8137 let bars = handler.lock();
8138
8139 for bar in bars.iter() {
8141 prop_assert_eq!(bar.volume, Quantity::from(step));
8142 prop_assert!(bar.low <= bar.open);
8143 prop_assert!(bar.low <= bar.close);
8144 prop_assert!(bar.high >= bar.open);
8145 prop_assert!(bar.high >= bar.close);
8146 }
8147
8148 let emitted_total: u64 = bars.len() as u64 * step;
8150 let pending = aggregator.core.builder.volume.as_f64();
8151 prop_assert!((emitted_total as f64 + pending - total_input as f64).abs() < 1e-6);
8152 }
8153
8154 #[rstest]
8155 fn prop_volume_bar_matches_unit_trade_reference(
8156 updates in prop::collection::vec((1i64..=100_000i64, 1u64..=8u64, 0u64..=30u64), 1..=30),
8157 step in 1usize..=5,
8158 ) {
8159 let instrument = InstrumentAny::Equity(equity_aapl());
8160 let bar_spec = BarSpecification::new(step, BarAggregation::Volume, PriceType::Last);
8161 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8162 let (handler, record) = recording_handler();
8163 let mut aggregator = VolumeBarAggregator::new(
8164 bar_type,
8165 instrument.price_precision(),
8166 instrument.size_precision(),
8167 record,
8168 );
8169 let price = |cents| {
8170 Price::from_decimal_dp(Decimal::new(cents, 2), 2)
8171 .expect("bounded cents must produce a valid price")
8172 };
8173 let mut last_timestamp = UnixNanos::default();
8174 let mut pending_units = Vec::new();
8175 let mut expected_bars = Vec::new();
8176
8177 for (price_cents, size, timestamp) in &updates {
8178 let timestamp = UnixNanos::from(*timestamp);
8179 aggregator.update(price(*price_cents), Quantity::from(*size), timestamp);
8180
8181 if timestamp < last_timestamp {
8182 continue;
8183 }
8184
8185 last_timestamp = timestamp;
8186 for _ in 0..*size {
8187 pending_units.push((*price_cents, timestamp));
8188 }
8189
8190 while pending_units.len() >= step {
8191 let units: Vec<_> = pending_units.drain(..step).collect();
8192 let first = units.first().unwrap();
8193 let last = units.last().unwrap();
8194 let low = units.iter().map(|(cents, _)| *cents).min().unwrap();
8195 let high = units.iter().map(|(cents, _)| *cents).max().unwrap();
8196 expected_bars.push((
8197 price(first.0),
8198 price(high),
8199 price(low),
8200 price(last.0),
8201 Quantity::from(step as u64),
8202 last.1,
8203 ));
8204 }
8205 }
8206
8207 let bars = handler.lock();
8208 prop_assert_eq!(bars.len(), expected_bars.len());
8209 for (actual, (open, high, low, close, volume, timestamp))
8210 in bars.iter().zip(expected_bars)
8211 {
8212 prop_assert_eq!(actual.open, open);
8213 prop_assert_eq!(actual.high, high);
8214 prop_assert_eq!(actual.low, low);
8215 prop_assert_eq!(actual.close, close);
8216 prop_assert_eq!(actual.volume, volume);
8217 prop_assert_eq!(actual.ts_event, timestamp);
8218 prop_assert_eq!(actual.ts_init, timestamp);
8219 }
8220
8221 prop_assert_eq!(aggregator.core.builder.volume, Quantity::from(pending_units.len() as u64));
8222 prop_assert_eq!(aggregator.core.builder.ts_last, last_timestamp);
8223
8224 if let Some((first, rest)) = pending_units.split_first() {
8225 let last = rest.last().unwrap_or(first);
8226 let low = pending_units.iter().map(|(cents, _)| *cents).min().unwrap();
8227 let high = pending_units.iter().map(|(cents, _)| *cents).max().unwrap();
8228 prop_assert_eq!(aggregator.core.builder.open, Some(price(first.0)));
8229 prop_assert_eq!(aggregator.core.builder.high, Some(price(high)));
8230 prop_assert_eq!(aggregator.core.builder.low, Some(price(low)));
8231 prop_assert_eq!(aggregator.core.builder.close, Some(price(last.0)));
8232 } else {
8233 prop_assert_eq!(aggregator.core.builder.open, None);
8234 prop_assert_eq!(aggregator.core.builder.high, None);
8235 prop_assert_eq!(aggregator.core.builder.low, None);
8236 prop_assert_eq!(aggregator.core.builder.close, None);
8237 }
8238 }
8239
8240 #[rstest]
8241 fn prop_bar_builder_spread_adjustment_is_additive(
8242 updates in prop::collection::vec((10_000i64..=100_000i64, 1u64..=100u64), 1..=20),
8243 spread_cents in -10_000i64..=10_000i64,
8244 backward in any::<bool>(),
8245 ) {
8246 let instrument = InstrumentAny::Equity(equity_aapl());
8247 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
8248 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8249 let mut builder = BarBuilder::new(bar_type, 2, 0);
8250
8251 let spread = Decimal::new(spread_cents, 2);
8252 let mode = if backward {
8253 ContinuousFutureAdjustmentType::BackwardSpread
8254 } else {
8255 ContinuousFutureAdjustmentType::ForwardSpread
8256 };
8257 builder.set_adjustment(spread, mode);
8258
8259 let mut min_cents = i64::MAX;
8260 let mut max_cents = i64::MIN;
8261
8262 for (i, (price_cents, size)) in updates.iter().enumerate() {
8263 if *price_cents < min_cents {
8264 min_cents = *price_cents;
8265 }
8266
8267 if *price_cents > max_cents {
8268 max_cents = *price_cents;
8269 }
8270
8271 builder.update(
8272 Price::new((*price_cents as f64) / 100.0, 2),
8273 Quantity::new(*size as f64, 0),
8274 UnixNanos::from((i as u64 + 1) * 1_000),
8275 );
8276 }
8277
8278 let bar = builder.build_now();
8279 let first_decimal = Decimal::new(updates.first().unwrap().0, 2);
8280 let last_decimal = Decimal::new(updates.last().unwrap().0, 2);
8281 let min_decimal = Decimal::new(min_cents, 2);
8282 let max_decimal = Decimal::new(max_cents, 2);
8283
8284 prop_assert_eq!(bar.open.as_decimal(), first_decimal + spread);
8285 prop_assert_eq!(bar.close.as_decimal(), last_decimal + spread);
8286 prop_assert_eq!(bar.low.as_decimal(), min_decimal + spread);
8287 prop_assert_eq!(bar.high.as_decimal(), max_decimal + spread);
8288 }
8289
8290 #[rstest]
8291 fn prop_bar_builder_inactive_adjustment_is_identity(
8292 updates in prop::collection::vec((1i64..=100_000i64, 1u64..=1_000u64), 1..=20),
8293 use_ratio in any::<bool>(),
8294 ) {
8295 let instrument = InstrumentAny::Equity(equity_aapl());
8296 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
8297 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8298
8299 let mut adjusted = BarBuilder::new(bar_type, 2, 0);
8300 let mut baseline = BarBuilder::new(bar_type, 2, 0);
8301
8302 let (input, mode) = if use_ratio {
8304 (Decimal::ONE, ContinuousFutureAdjustmentType::BackwardRatio)
8305 } else {
8306 (Decimal::ZERO, ContinuousFutureAdjustmentType::BackwardSpread)
8307 };
8308 adjusted.set_adjustment(input, mode);
8309
8310 for (i, (price_cents, size)) in updates.iter().enumerate() {
8311 let price = Price::new((*price_cents as f64) / 100.0, 2);
8312 let qty = Quantity::new(*size as f64, 0);
8313 let ts = UnixNanos::from((i as u64 + 1) * 1_000);
8314 adjusted.update(price, qty, ts);
8315 baseline.update(price, qty, ts);
8316 }
8317
8318 let bar_adjusted = adjusted.build_now();
8319 let bar_baseline = baseline.build_now();
8320 prop_assert_eq!(bar_adjusted.open, bar_baseline.open);
8321 prop_assert_eq!(bar_adjusted.high, bar_baseline.high);
8322 prop_assert_eq!(bar_adjusted.low, bar_baseline.low);
8323 prop_assert_eq!(bar_adjusted.close, bar_baseline.close);
8324 prop_assert_eq!(bar_adjusted.volume, bar_baseline.volume);
8325 }
8326
8327 #[rstest]
8328 fn prop_bar_builder_spread_preserves_raw_arithmetic(
8329 updates in prop::collection::vec((10_000i64..=100_000i64, 1u64..=100u64), 1..=20),
8330 spread_micro in -10_000i64..=10_000i64,
8333 ) {
8334 let instrument = InstrumentAny::Equity(equity_aapl());
8335 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
8336 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8337 let mut builder = BarBuilder::new(bar_type, 2, 0);
8338
8339 let spread = Decimal::new(spread_micro, 4);
8340 builder.set_adjustment(spread, ContinuousFutureAdjustmentType::BackwardSpread);
8341
8342 let adjustment_raw_i128 = mantissa_exponent_to_fixed_i128(
8343 spread.mantissa(),
8344 -(spread.scale() as i8),
8345 FIXED_PRECISION,
8346 )
8347 .expect("scale within range");
8348 #[allow(
8349 clippy::useless_conversion,
8350 reason = "i128 to PriceRaw is real when not high-precision"
8351 )]
8352 let expected_adjustment_raw: PriceRaw =
8353 adjustment_raw_i128.try_into().expect("within PriceRaw range");
8354
8355 let mut min_cents = i64::MAX;
8356 let mut max_cents = i64::MIN;
8357 let mut last_price = Price::new(0.0, 2);
8358 let mut first_price = Price::new(0.0, 2);
8359
8360 for (i, (price_cents, size)) in updates.iter().enumerate() {
8361 if *price_cents < min_cents {
8362 min_cents = *price_cents;
8363 }
8364
8365 if *price_cents > max_cents {
8366 max_cents = *price_cents;
8367 }
8368
8369 let price = Price::new((*price_cents as f64) / 100.0, 2);
8370
8371 if i == 0 {
8372 first_price = price;
8373 }
8374
8375 last_price = price;
8376 builder.update(
8377 price,
8378 Quantity::new(*size as f64, 0),
8379 UnixNanos::from((i as u64 + 1) * 1_000),
8380 );
8381 }
8382
8383 let bar = builder.build_now();
8384 let min_price = Price::new((min_cents as f64) / 100.0, 2);
8385 let max_price = Price::new((max_cents as f64) / 100.0, 2);
8386 prop_assert_eq!(bar.open.raw(), first_price.raw() + expected_adjustment_raw);
8387 prop_assert_eq!(bar.close.raw(), last_price.raw() + expected_adjustment_raw);
8388 prop_assert_eq!(bar.low.raw(), min_price.raw() + expected_adjustment_raw);
8389 prop_assert_eq!(bar.high.raw(), max_price.raw() + expected_adjustment_raw);
8390 prop_assert_eq!(bar.open.precision, 2);
8391 prop_assert_eq!(bar.high.precision, 2);
8392 prop_assert_eq!(bar.low.precision, 2);
8393 prop_assert_eq!(bar.close.precision, 2);
8394 }
8395
8396 #[rstest]
8397 fn prop_bar_builder_active_ratio_scales_each_ohlc(
8398 updates in prop::collection::vec((1_000i64..=100_000i64, 1u64..=100u64), 1..=20),
8399 ratio_centi in prop_oneof![50i64..=99i64, 101i64..=200i64],
8401 backward in any::<bool>(),
8402 ) {
8403 let instrument = InstrumentAny::Equity(equity_aapl());
8404 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
8405 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8406 let mut builder = BarBuilder::new(bar_type, 2, 0);
8407
8408 let ratio_decimal = Decimal::new(ratio_centi, 2);
8409 let ratio_f64 = (ratio_centi as f64) / 100.0;
8410 let mode = if backward {
8411 ContinuousFutureAdjustmentType::BackwardRatio
8412 } else {
8413 ContinuousFutureAdjustmentType::ForwardRatio
8414 };
8415 builder.set_adjustment(ratio_decimal, mode);
8416
8417 let mut min_cents = i64::MAX;
8418 let mut max_cents = i64::MIN;
8419 let mut first_cents = 0i64;
8420 let mut last_cents = 0i64;
8421
8422 for (i, (price_cents, size)) in updates.iter().enumerate() {
8423 if *price_cents < min_cents {
8424 min_cents = *price_cents;
8425 }
8426
8427 if *price_cents > max_cents {
8428 max_cents = *price_cents;
8429 }
8430
8431 if i == 0 {
8432 first_cents = *price_cents;
8433 }
8434
8435 last_cents = *price_cents;
8436 builder.update(
8437 Price::new((*price_cents as f64) / 100.0, 2),
8438 Quantity::new(*size as f64, 0),
8439 UnixNanos::from((i as u64 + 1) * 1_000),
8440 );
8441 }
8442
8443 let bar = builder.build_now();
8444 let expect = |cents: i64| Price::new((cents as f64) / 100.0 * ratio_f64, 2);
8446 prop_assert_eq!(bar.open, expect(first_cents));
8447 prop_assert_eq!(bar.close, expect(last_cents));
8448 prop_assert_eq!(bar.low, expect(min_cents));
8450 prop_assert_eq!(bar.high, expect(max_cents));
8451 }
8452
8453 #[rstest]
8454 fn prop_bar_builder_spread_mode_direction_is_metadata_only(
8455 updates in prop::collection::vec((10_000i64..=100_000i64, 1u64..=100u64), 1..=20),
8456 spread_cents in -10_000i64..=10_000i64,
8457 ) {
8458 let instrument = InstrumentAny::Equity(equity_aapl());
8459 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
8460 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8461
8462 let spread = Decimal::new(spread_cents, 2);
8463 let mut backward = BarBuilder::new(bar_type, 2, 0);
8464 let mut forward = BarBuilder::new(bar_type, 2, 0);
8465 backward.set_adjustment(spread, ContinuousFutureAdjustmentType::BackwardSpread);
8466 forward.set_adjustment(spread, ContinuousFutureAdjustmentType::ForwardSpread);
8467
8468 for (i, (price_cents, size)) in updates.iter().enumerate() {
8469 let price = Price::new((*price_cents as f64) / 100.0, 2);
8470 let qty = Quantity::new(*size as f64, 0);
8471 let ts = UnixNanos::from((i as u64 + 1) * 1_000);
8472 backward.update(price, qty, ts);
8473 forward.update(price, qty, ts);
8474 }
8475
8476 let bar_backward = backward.build_now();
8477 let bar_forward = forward.build_now();
8478 prop_assert_eq!(bar_backward.open, bar_forward.open);
8479 prop_assert_eq!(bar_backward.high, bar_forward.high);
8480 prop_assert_eq!(bar_backward.low, bar_forward.low);
8481 prop_assert_eq!(bar_backward.close, bar_forward.close);
8482 }
8483
8484 #[rstest]
8485 fn prop_value_bar_aggregator_ohlc_invariants(
8486 ticks in prop::collection::vec((50i64..=500i64, 1u64..=20u64), 2..=30),
8487 step in 100u64..=2_000u64,
8488 ) {
8489 let instrument = InstrumentAny::Equity(equity_aapl());
8490 let bar_spec = BarSpecification::new(step as usize, BarAggregation::Value, PriceType::Last);
8491 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8492 let (handler, record) = recording_handler();
8493
8494 let mut aggregator = ValueBarAggregator::new(
8495 bar_type,
8496 instrument.price_precision(),
8497 instrument.size_precision(),
8498 record,
8499 );
8500
8501 for (i, (price_cents, size)) in ticks.iter().enumerate() {
8502 aggregator.update(
8503 Price::new((*price_cents as f64) / 100.0, 2),
8504 Quantity::new(*size as f64, 0),
8505 UnixNanos::from((i as u64 + 1) * 1_000),
8506 );
8507 }
8508
8509 let bars = handler.lock();
8510 for bar in bars.iter() {
8511 prop_assert!(bar.low <= bar.open);
8512 prop_assert!(bar.low <= bar.close);
8513 prop_assert!(bar.high >= bar.open);
8514 prop_assert!(bar.high >= bar.close);
8515 prop_assert!(bar.volume.as_f64() > 0.0);
8516 }
8517 }
8518
8519 #[rstest]
8520 fn prop_renko_brick_chain(
8521 moves in prop::collection::vec(-500i64..=500i64, 1..=60),
8522 step in 1usize..=10,
8523 ) {
8524 let instrument = InstrumentAny::Equity(equity_aapl());
8525 let bar_spec = BarSpecification::new(step, BarAggregation::Renko, PriceType::Last);
8526 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8527 let (handler, record) = recording_handler();
8528
8529 let price_increment = Price::from("0.01");
8530 let mut aggregator = RenkoBarAggregator::new(
8531 bar_type,
8532 2,
8533 0,
8534 price_increment,
8535 record,
8536 );
8537 let brick_size = aggregator.brick_size;
8538
8539 let base_raw = Price::from("1000.00").raw();
8540 let mut cum_increments: i64 = 0;
8541 let mut first_price: Option<Price> = None;
8542
8543 for (i, delta) in moves.iter().enumerate() {
8544 cum_increments += delta;
8545 let price = Price::from_raw(
8546 base_raw + PriceRaw::from(cum_increments) * price_increment.raw(),
8547 2,
8548 );
8549
8550 if first_price.is_none() {
8551 first_price = Some(price);
8552 }
8553
8554 aggregator.update(price, Quantity::from(1), UnixNanos::from((i as u64 + 1) * 1_000));
8555 }
8556
8557 let bars = handler.lock();
8558 let mut expected_open = first_price.unwrap();
8559
8560 for bar in bars.iter() {
8561 prop_assert_eq!(bar.open, expected_open);
8563 let movement = if bar.close >= bar.open { bar.close - bar.open } else { bar.open - bar.close };
8565 prop_assert_eq!(movement, brick_size);
8566 prop_assert_eq!(bar.high, bar.open.max(bar.close));
8568 prop_assert_eq!(bar.low, bar.open.min(bar.close));
8569 expected_open = bar.close;
8570 }
8571 }
8572
8573 #[rstest]
8574 fn prop_volume_imbalance_one_sided_conservation(
8575 sizes in prop::collection::vec(1u64..=50u64, 1..=40),
8576 step in 2u64..=10u64,
8577 buyer in any::<bool>(),
8578 ) {
8579 let instrument = InstrumentAny::Equity(equity_aapl());
8580 let bar_spec = BarSpecification::new(
8581 step as usize,
8582 BarAggregation::VolumeImbalance,
8583 PriceType::Last,
8584 );
8585 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8586 let (handler, record) = recording_handler();
8587
8588 let mut aggregator = VolumeImbalanceBarAggregator::new(
8589 bar_type,
8590 instrument.price_precision(),
8591 instrument.size_precision(),
8592 record,
8593 );
8594
8595 let side = if buyer { AggressorSide::Buy } else { AggressorSide::Sell };
8596 let mut total_input: u64 = 0;
8597
8598 for (i, size) in sizes.iter().enumerate() {
8599 let trade = TradeTick {
8600 instrument_id: instrument.id(),
8601 price: Price::from("100.00"),
8602 size: Quantity::from(*size),
8603 aggressor_side: side,
8604 ts_event: UnixNanos::from((i as u64 + 1) * 1_000),
8605 ts_init: UnixNanos::from((i as u64 + 1) * 1_000),
8606 ..TradeTick::default()
8607 };
8608 aggregator.handle_trade(trade);
8609 total_input += *size;
8610 }
8611
8612 let bars = handler.lock();
8613
8614 for bar in bars.iter() {
8616 prop_assert_eq!(bar.volume, Quantity::from(step));
8617 }
8618
8619 let emitted: u64 = bars.len() as u64 * step;
8621 let pending = aggregator.core.builder.volume.as_f64();
8622 prop_assert!((emitted as f64 + pending - total_input as f64).abs() < 1e-9);
8623 }
8624
8625 #[rstest]
8626 fn prop_volume_runs_one_sided_conservation(
8627 sizes in prop::collection::vec(1u64..=50u64, 1..=40),
8628 step in 2u64..=10u64,
8629 buyer in any::<bool>(),
8630 ) {
8631 let instrument = InstrumentAny::Equity(equity_aapl());
8632 let bar_spec = BarSpecification::new(
8633 step as usize,
8634 BarAggregation::VolumeRuns,
8635 PriceType::Last,
8636 );
8637 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8638 let (handler, record) = recording_handler();
8639
8640 let mut aggregator = VolumeRunsBarAggregator::new(
8641 bar_type,
8642 instrument.price_precision(),
8643 instrument.size_precision(),
8644 record,
8645 );
8646
8647 let side = if buyer { AggressorSide::Buy } else { AggressorSide::Sell };
8648 let mut total_input: u64 = 0;
8649
8650 for (i, size) in sizes.iter().enumerate() {
8651 let trade = TradeTick {
8652 instrument_id: instrument.id(),
8653 price: Price::from("100.00"),
8654 size: Quantity::from(*size),
8655 aggressor_side: side,
8656 ts_event: UnixNanos::from((i as u64 + 1) * 1_000),
8657 ts_init: UnixNanos::from((i as u64 + 1) * 1_000),
8658 ..TradeTick::default()
8659 };
8660 aggregator.handle_trade(trade);
8661 total_input += *size;
8662 }
8663
8664 let bars = handler.lock();
8665
8666 for bar in bars.iter() {
8668 prop_assert_eq!(bar.volume, Quantity::from(step));
8669 }
8670
8671 let emitted: u64 = bars.len() as u64 * step;
8672 let pending = aggregator.core.builder.volume.as_f64();
8673 prop_assert!((emitted as f64 + pending - total_input as f64).abs() < 1e-9);
8674 }
8675
8676 #[rstest]
8677 fn prop_value_bar_cum_value_stays_below_step(
8678 ticks in prop::collection::vec((50i64..=500i64, 1u64..=20u64), 1..=30),
8679 step in 100u64..=2_000u64,
8680 ) {
8681 let instrument = InstrumentAny::Equity(equity_aapl());
8682 let bar_spec = BarSpecification::new(step as usize, BarAggregation::Value, PriceType::Last);
8683 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
8684 let step_decimal = Decimal::from(step);
8685
8686 let mut aggregator = ValueBarAggregator::new(
8687 bar_type,
8688 instrument.price_precision(),
8689 instrument.size_precision(),
8690 |_: Bar| {},
8691 );
8692
8693 for (i, (price_cents, size)) in ticks.iter().enumerate() {
8694 aggregator.update(
8695 Price::new((*price_cents as f64) / 100.0, 2),
8696 Quantity::new(*size as f64, 0),
8697 UnixNanos::from((i as u64 + 1) * 1_000),
8698 );
8699
8700 prop_assert!(aggregator.get_cumulative_value() < step_decimal);
8703 }
8704 }
8705 }
8706}