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