1use std::{
22 any::Any,
23 cell::RefCell,
24 fmt::Debug,
25 ops::Add,
26 rc::{Rc, Weak},
27};
28
29use ahash::AHashMap;
30use chrono::{Duration, TimeDelta};
31use nautilus_common::{
32 clock::{Clock, TestClock},
33 timer::{TimeEvent, TimeEventCallback},
34};
35use nautilus_core::{
36 UnixNanos,
37 correctness::{self, FAILED},
38 datetime::{
39 add_n_months, add_n_months_nanos, add_n_years, add_n_years_nanos, subtract_n_months_nanos,
40 subtract_n_years_nanos,
41 },
42};
43use nautilus_model::{
44 data::{
45 QuoteTick, TradeTick,
46 bar::{Bar, BarType, get_bar_interval_ns, get_time_bar_start},
47 },
48 enums::{
49 AggregationSource, AggressorSide, BarAggregation, BarIntervalType,
50 ContinuousFutureAdjustmentType,
51 },
52 identifiers::InstrumentId,
53 instruments::{FixedTickScheme, TickSchemeRule},
54 types::{
55 Price, Quantity,
56 fixed::{FIXED_PRECISION, FIXED_SCALAR, mantissa_exponent_to_fixed_i128},
57 price::PriceRaw,
58 quantity::QuantityRaw,
59 },
60};
61use rust_decimal::{Decimal, prelude::ToPrimitive};
62
63type BarHandler = Box<dyn FnMut(Bar)>;
65
66pub trait BarAggregator: Any + Debug {
70 fn bar_type(&self) -> BarType;
72 fn is_running(&self) -> bool;
74 fn set_is_running(&mut self, value: bool);
76 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos);
78 fn handle_quote(&mut self, quote: QuoteTick) {
80 let spec = self.bar_type().spec();
81 self.update(
82 quote.extract_price(spec.price_type),
83 quote.extract_size(spec.price_type),
84 quote.ts_init,
85 );
86 }
87 fn handle_trade(&mut self, trade: TradeTick) {
89 self.update(trade.price, trade.size, trade.ts_init);
90 }
91 fn handle_bar(&mut self, bar: Bar) {
93 self.update_bar(bar, bar.volume, bar.ts_init);
94 }
95 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos);
96 fn stop(&mut self) {}
98 fn set_historical_mode(&mut self, _historical_mode: bool, _handler: Box<dyn FnMut(Bar)>) {}
100 fn set_historical_events(&mut self, _events: Vec<TimeEvent>) {}
102 fn set_clock(&mut self, _clock: Rc<RefCell<dyn Clock>>) {}
104 fn build_bar(&mut self, _event: &TimeEvent) {}
106 fn start_timer(&mut self, _aggregator_rc: Option<Rc<RefCell<Box<dyn BarAggregator>>>>) {}
110 fn set_aggregator_weak(&mut self, _weak: Weak<RefCell<Box<dyn BarAggregator>>>) {}
113 fn set_adjustment(&mut self, _adjustment: Decimal, _mode: ContinuousFutureAdjustmentType) {}
115}
116
117impl dyn BarAggregator {
118 pub fn as_any(&self) -> &dyn Any {
120 self
121 }
122 pub fn as_any_mut(&mut self) -> &mut dyn Any {
124 self
125 }
126}
127
128#[derive(Debug)]
130pub struct BarBuilder {
131 bar_type: BarType,
132 price_precision: u8,
133 size_precision: u8,
134 initialized: bool,
135 ts_last: UnixNanos,
136 count: usize,
137 last_close: Option<Price>,
138 open: Option<Price>,
139 high: Option<Price>,
140 low: Option<Price>,
141 close: Option<Price>,
142 volume: Quantity,
143 adjustment_mode: ContinuousFutureAdjustmentType,
144 adjustment_raw: PriceRaw,
145 adjustment_ratio: f64,
146 adjustment_active: bool,
147 adjustment_is_ratio: bool,
148}
149
150impl BarBuilder {
151 #[must_use]
157 pub fn new(bar_type: BarType, price_precision: u8, size_precision: u8) -> Self {
158 correctness::check_equal(
159 &bar_type.aggregation_source(),
160 &AggregationSource::Internal,
161 "bar_type.aggregation_source",
162 "AggregationSource::Internal",
163 )
164 .expect(FAILED);
165
166 Self {
167 bar_type,
168 price_precision,
169 size_precision,
170 initialized: false,
171 ts_last: UnixNanos::default(),
172 count: 0,
173 last_close: None,
174 open: None,
175 high: None,
176 low: None,
177 close: None,
178 volume: Quantity::zero(size_precision),
179 adjustment_mode: ContinuousFutureAdjustmentType::default(),
180 adjustment_raw: 0,
181 adjustment_ratio: 1.0,
182 adjustment_active: false,
183 adjustment_is_ratio: false,
184 }
185 }
186
187 pub fn set_adjustment(&mut self, adjustment: Decimal, mode: ContinuousFutureAdjustmentType) {
198 self.adjustment_mode = mode;
199
200 if mode.is_ratio() {
201 self.adjustment_is_ratio = true;
202 self.adjustment_ratio = adjustment.to_f64().unwrap_or(1.0);
203 self.adjustment_active = adjustment != Decimal::ONE;
204 return;
205 }
206
207 self.adjustment_is_ratio = false;
211 let exponent = -(adjustment.scale() as i8);
212 let raw_i128 =
213 mantissa_exponent_to_fixed_i128(adjustment.mantissa(), exponent, FIXED_PRECISION)
214 .expect("Failed to scale continuous-future adjustment to fixed precision");
215
216 #[allow(
217 clippy::useless_conversion,
218 reason = "i128 to PriceRaw is real when not high-precision"
219 )]
220 let raw: PriceRaw = raw_i128
221 .try_into()
222 .expect("Continuous-future adjustment exceeds PriceRaw range");
223
224 self.adjustment_raw = raw;
225 self.adjustment_active = self.adjustment_raw != 0;
226 }
227
228 fn apply_adjustment_to_price(&self, price: Price) -> Price {
229 if !self.adjustment_active {
230 return price;
231 }
232
233 if self.adjustment_is_ratio {
234 return Price::new(price.as_f64() * self.adjustment_ratio, price.precision);
237 }
238
239 Price::from_raw(price.raw + self.adjustment_raw, price.precision)
241 }
242
243 pub fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
249 if ts_init < self.ts_last {
250 return; }
252
253 let price = self.apply_adjustment_to_price(price);
254
255 if self.open.is_none() {
256 self.open = Some(price);
257 self.high = Some(price);
258 self.low = Some(price);
259 self.initialized = true;
260 } else {
261 if price > self.high.unwrap() {
262 self.high = Some(price);
263 }
264
265 if price < self.low.unwrap() {
266 self.low = Some(price);
267 }
268 }
269
270 self.close = Some(price);
271 self.volume = self.volume.add(size);
272 self.count += 1;
273 self.ts_last = ts_init;
274 }
275
276 pub fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
282 if ts_init < self.ts_last {
283 return; }
285
286 let bar_open = self.apply_adjustment_to_price(bar.open);
287 let bar_high = self.apply_adjustment_to_price(bar.high);
288 let bar_low = self.apply_adjustment_to_price(bar.low);
289 let bar_close = self.apply_adjustment_to_price(bar.close);
290
291 if self.open.is_none() {
292 self.open = Some(bar_open);
293 self.high = Some(bar_high);
294 self.low = Some(bar_low);
295 self.initialized = true;
296 } else {
297 if bar_high > self.high.unwrap() {
298 self.high = Some(bar_high);
299 }
300
301 if bar_low < self.low.unwrap() {
302 self.low = Some(bar_low);
303 }
304 }
305
306 self.close = Some(bar_close);
307 self.volume = self.volume.add(volume);
308 self.count += 1;
309 self.ts_last = ts_init;
310 }
311
312 pub fn reset(&mut self) {
317 self.open = None;
318 self.high = None;
319 self.low = None;
320 self.volume = Quantity::zero(self.size_precision);
321 self.count = 0;
322 }
323
324 pub fn build_now(&mut self) -> Bar {
326 self.build(self.ts_last, self.ts_last)
327 }
328
329 pub fn build(&mut self, ts_event: UnixNanos, ts_init: UnixNanos) -> Bar {
335 if self.open.is_none() {
336 self.open = self.last_close;
337 self.high = self.last_close;
338 self.low = self.last_close;
339 self.close = self.last_close;
340 }
341
342 if let (Some(close), Some(low)) = (self.close, self.low)
343 && close < low
344 {
345 self.low = Some(close);
346 }
347
348 if let (Some(close), Some(high)) = (self.close, self.high)
349 && close > high
350 {
351 self.high = Some(close);
352 }
353
354 let bar = Bar::new(
356 self.bar_type,
357 self.open.unwrap(),
358 self.high.unwrap(),
359 self.low.unwrap(),
360 self.close.unwrap(),
361 self.volume,
362 ts_event,
363 ts_init,
364 );
365
366 self.last_close = self.close;
367 self.reset();
368 bar
369 }
370}
371
372pub struct BarAggregatorCore {
374 bar_type: BarType,
375 builder: BarBuilder,
376 handler: BarHandler,
377 is_running: bool,
378}
379
380impl Debug for BarAggregatorCore {
381 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
382 f.debug_struct(stringify!(BarAggregatorCore))
383 .field("bar_type", &self.bar_type)
384 .field("builder", &self.builder)
385 .field("is_running", &self.is_running)
386 .finish()
387 }
388}
389
390impl BarAggregatorCore {
391 pub fn new<H: FnMut(Bar) + 'static>(
397 bar_type: BarType,
398 price_precision: u8,
399 size_precision: u8,
400 handler: H,
401 ) -> Self {
402 Self {
403 bar_type,
404 builder: BarBuilder::new(bar_type, price_precision, size_precision),
405 handler: Box::new(handler),
406 is_running: false,
407 }
408 }
409
410 pub const fn set_is_running(&mut self, value: bool) {
412 self.is_running = value;
413 }
414
415 fn set_handler(&mut self, handler: BarHandler) {
416 self.handler = handler;
417 }
418
419 fn apply_update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
420 self.builder.update(price, size, ts_init);
421 }
422
423 fn build_now_and_send(&mut self) {
424 let bar = self.builder.build_now();
425 (self.handler)(bar);
426 }
427
428 fn build_and_send(&mut self, ts_event: UnixNanos, ts_init: UnixNanos) {
429 let bar = self.builder.build(ts_event, ts_init);
430 (self.handler)(bar);
431 }
432
433 fn set_adjustment(&mut self, adjustment: Decimal, mode: ContinuousFutureAdjustmentType) {
434 self.builder.set_adjustment(adjustment, mode);
435 }
436}
437
438macro_rules! impl_set_historical_handler {
439 () => {
440 fn set_historical_mode(&mut self, _historical_mode: bool, handler: Box<dyn FnMut(Bar)>) {
441 self.core.set_handler(handler);
442 }
443 };
444}
445
446macro_rules! impl_set_adjustment {
447 () => {
448 fn set_adjustment(&mut self, adjustment: Decimal, mode: ContinuousFutureAdjustmentType) {
449 self.core.set_adjustment(adjustment, mode);
450 }
451 };
452}
453
454pub struct TickBarAggregator {
459 core: BarAggregatorCore,
460}
461
462impl Debug for TickBarAggregator {
463 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
464 f.debug_struct(stringify!(TickBarAggregator))
465 .field("core", &self.core)
466 .finish()
467 }
468}
469
470impl TickBarAggregator {
471 pub fn new<H: FnMut(Bar) + 'static>(
477 bar_type: BarType,
478 price_precision: u8,
479 size_precision: u8,
480 handler: H,
481 ) -> Self {
482 Self {
483 core: BarAggregatorCore::new(bar_type, price_precision, size_precision, handler),
484 }
485 }
486}
487
488impl BarAggregator for TickBarAggregator {
489 fn bar_type(&self) -> BarType {
490 self.core.bar_type
491 }
492
493 fn is_running(&self) -> bool {
494 self.core.is_running
495 }
496
497 fn set_is_running(&mut self, value: bool) {
498 self.core.set_is_running(value);
499 }
500
501 impl_set_historical_handler!();
502 impl_set_adjustment!();
503
504 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
506 self.core.apply_update(price, size, ts_init);
507 let spec = self.core.bar_type.spec();
508
509 if self.core.builder.count >= spec.step.get() {
510 self.core.build_now_and_send();
511 }
512 }
513
514 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
515 self.core.builder.update_bar(bar, volume, ts_init);
516 let spec = self.core.bar_type.spec();
517
518 if self.core.builder.count >= spec.step.get() {
519 self.core.build_now_and_send();
520 }
521 }
522}
523
524pub struct TickImbalanceBarAggregator {
529 core: BarAggregatorCore,
530 imbalance: isize,
531}
532
533impl Debug for TickImbalanceBarAggregator {
534 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
535 f.debug_struct(stringify!(TickImbalanceBarAggregator))
536 .field("core", &self.core)
537 .field("imbalance", &self.imbalance)
538 .finish()
539 }
540}
541
542impl TickImbalanceBarAggregator {
543 pub fn new<H: FnMut(Bar) + 'static>(
549 bar_type: BarType,
550 price_precision: u8,
551 size_precision: u8,
552 handler: H,
553 ) -> Self {
554 Self {
555 core: BarAggregatorCore::new(bar_type, price_precision, size_precision, handler),
556 imbalance: 0,
557 }
558 }
559}
560
561impl BarAggregator for TickImbalanceBarAggregator {
562 fn bar_type(&self) -> BarType {
563 self.core.bar_type
564 }
565
566 fn is_running(&self) -> bool {
567 self.core.is_running
568 }
569
570 fn set_is_running(&mut self, value: bool) {
571 self.core.set_is_running(value);
572 }
573
574 impl_set_historical_handler!();
575 impl_set_adjustment!();
576
577 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
582 self.core.apply_update(price, size, ts_init);
583 }
584
585 fn handle_trade(&mut self, trade: TradeTick) {
586 self.core
587 .apply_update(trade.price, trade.size, trade.ts_init);
588
589 let delta = match trade.aggressor_side {
590 AggressorSide::Buyer => 1,
591 AggressorSide::Seller => -1,
592 AggressorSide::NoAggressor => 0,
593 };
594
595 if delta == 0 {
596 return;
597 }
598
599 self.imbalance += delta;
600 let threshold = self.core.bar_type.spec().step.get();
601 if self.imbalance.unsigned_abs() >= threshold {
602 self.core.build_now_and_send();
603 self.imbalance = 0;
604 }
605 }
606
607 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
608 self.core.builder.update_bar(bar, volume, ts_init);
609 }
610}
611
612pub struct TickRunsBarAggregator {
614 core: BarAggregatorCore,
615 current_run_side: Option<AggressorSide>,
616 run_count: usize,
617}
618
619impl Debug for TickRunsBarAggregator {
620 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
621 f.debug_struct(stringify!(TickRunsBarAggregator))
622 .field("core", &self.core)
623 .field("current_run_side", &self.current_run_side)
624 .field("run_count", &self.run_count)
625 .finish()
626 }
627}
628
629impl TickRunsBarAggregator {
630 pub fn new<H: FnMut(Bar) + 'static>(
636 bar_type: BarType,
637 price_precision: u8,
638 size_precision: u8,
639 handler: H,
640 ) -> Self {
641 Self {
642 core: BarAggregatorCore::new(bar_type, price_precision, size_precision, handler),
643 current_run_side: None,
644 run_count: 0,
645 }
646 }
647}
648
649impl BarAggregator for TickRunsBarAggregator {
650 fn bar_type(&self) -> BarType {
651 self.core.bar_type
652 }
653
654 fn is_running(&self) -> bool {
655 self.core.is_running
656 }
657
658 fn set_is_running(&mut self, value: bool) {
659 self.core.set_is_running(value);
660 }
661
662 impl_set_historical_handler!();
663 impl_set_adjustment!();
664
665 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
670 self.core.apply_update(price, size, ts_init);
671 }
672
673 fn handle_trade(&mut self, trade: TradeTick) {
674 let side = match trade.aggressor_side {
675 AggressorSide::Buyer => Some(AggressorSide::Buyer),
676 AggressorSide::Seller => Some(AggressorSide::Seller),
677 AggressorSide::NoAggressor => None,
678 };
679
680 if let Some(side) = side {
681 if self.current_run_side != Some(side) {
682 self.current_run_side = Some(side);
683 self.run_count = 0;
684 self.core.builder.reset();
685 }
686
687 self.core
688 .apply_update(trade.price, trade.size, trade.ts_init);
689 self.run_count += 1;
690
691 let threshold = self.core.bar_type.spec().step.get();
692 if self.run_count >= threshold {
693 self.core.build_now_and_send();
694 self.run_count = 0;
695 self.current_run_side = None;
696 }
697 } else {
698 self.core
699 .apply_update(trade.price, trade.size, trade.ts_init);
700 }
701 }
702
703 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
704 self.core.builder.update_bar(bar, volume, ts_init);
705 }
706}
707
708pub struct VolumeBarAggregator {
710 core: BarAggregatorCore,
711}
712
713impl Debug for VolumeBarAggregator {
714 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
715 f.debug_struct(stringify!(VolumeBarAggregator))
716 .field("core", &self.core)
717 .finish()
718 }
719}
720
721impl VolumeBarAggregator {
722 pub fn new<H: FnMut(Bar) + 'static>(
728 bar_type: BarType,
729 price_precision: u8,
730 size_precision: u8,
731 handler: H,
732 ) -> Self {
733 Self {
734 core: BarAggregatorCore::new(
735 bar_type.standard(),
736 price_precision,
737 size_precision,
738 handler,
739 ),
740 }
741 }
742}
743
744impl BarAggregator for VolumeBarAggregator {
745 fn bar_type(&self) -> BarType {
746 self.core.bar_type
747 }
748
749 fn is_running(&self) -> bool {
750 self.core.is_running
751 }
752
753 fn set_is_running(&mut self, value: bool) {
754 self.core.set_is_running(value);
755 }
756
757 impl_set_historical_handler!();
758 impl_set_adjustment!();
759
760 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
762 let mut raw_size_update = size.raw;
763 let spec = self.core.bar_type.spec();
764 let raw_step = (spec.step.get() as f64 * FIXED_SCALAR) as QuantityRaw;
765
766 while raw_size_update > 0 {
767 if self.core.builder.volume.raw + raw_size_update < raw_step {
768 self.core.apply_update(
769 price,
770 Quantity::from_raw(raw_size_update, size.precision),
771 ts_init,
772 );
773 break;
774 }
775
776 let raw_size_diff = raw_step - self.core.builder.volume.raw;
777 self.core.apply_update(
778 price,
779 Quantity::from_raw(raw_size_diff, size.precision),
780 ts_init,
781 );
782
783 self.core.build_now_and_send();
784 raw_size_update -= raw_size_diff;
785 }
786 }
787
788 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
789 let mut raw_volume_update = volume.raw;
790 let spec = self.core.bar_type.spec();
791 let raw_step = (spec.step.get() as f64 * FIXED_SCALAR) as QuantityRaw;
792
793 while raw_volume_update > 0 {
794 if self.core.builder.volume.raw + raw_volume_update < raw_step {
795 self.core.builder.update_bar(
796 bar,
797 Quantity::from_raw(raw_volume_update, volume.precision),
798 ts_init,
799 );
800 break;
801 }
802
803 let raw_volume_diff = raw_step - self.core.builder.volume.raw;
804 self.core.builder.update_bar(
805 bar,
806 Quantity::from_raw(raw_volume_diff, volume.precision),
807 ts_init,
808 );
809
810 self.core.build_now_and_send();
811 raw_volume_update -= raw_volume_diff;
812 }
813 }
814}
815
816pub struct VolumeImbalanceBarAggregator {
818 core: BarAggregatorCore,
819 imbalance_raw: i128,
820 raw_step: i128,
821}
822
823impl Debug for VolumeImbalanceBarAggregator {
824 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
825 f.debug_struct(stringify!(VolumeImbalanceBarAggregator))
826 .field("core", &self.core)
827 .field("imbalance_raw", &self.imbalance_raw)
828 .field("raw_step", &self.raw_step)
829 .finish()
830 }
831}
832
833impl VolumeImbalanceBarAggregator {
834 pub fn new<H: FnMut(Bar) + 'static>(
840 bar_type: BarType,
841 price_precision: u8,
842 size_precision: u8,
843 handler: H,
844 ) -> Self {
845 let raw_step = (bar_type.spec().step.get() as f64 * FIXED_SCALAR) as i128;
846 Self {
847 core: BarAggregatorCore::new(
848 bar_type.standard(),
849 price_precision,
850 size_precision,
851 handler,
852 ),
853 imbalance_raw: 0,
854 raw_step,
855 }
856 }
857}
858
859impl BarAggregator for VolumeImbalanceBarAggregator {
860 fn bar_type(&self) -> BarType {
861 self.core.bar_type
862 }
863
864 fn is_running(&self) -> bool {
865 self.core.is_running
866 }
867
868 fn set_is_running(&mut self, value: bool) {
869 self.core.set_is_running(value);
870 }
871
872 impl_set_historical_handler!();
873 impl_set_adjustment!();
874
875 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
880 self.core.apply_update(price, size, ts_init);
881 }
882
883 fn handle_trade(&mut self, trade: TradeTick) {
884 let side = match trade.aggressor_side {
885 AggressorSide::Buyer => 1,
886 AggressorSide::Seller => -1,
887 AggressorSide::NoAggressor => {
888 self.core
889 .apply_update(trade.price, trade.size, trade.ts_init);
890 return;
891 }
892 };
893
894 let mut raw_remaining = trade.size.raw as i128;
895 while raw_remaining > 0 {
896 let imbalance_abs = self.imbalance_raw.abs();
897 let needed = (self.raw_step - imbalance_abs).max(1);
898 let raw_chunk = raw_remaining.min(needed);
899 let qty_chunk = Quantity::from_raw(raw_chunk as QuantityRaw, trade.size.precision);
900
901 self.core
902 .apply_update(trade.price, qty_chunk, trade.ts_init);
903
904 self.imbalance_raw += side * raw_chunk;
905 raw_remaining -= raw_chunk;
906
907 if self.imbalance_raw.abs() >= self.raw_step {
908 self.core.build_now_and_send();
909 self.imbalance_raw = 0;
910 }
911 }
912 }
913
914 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
915 self.core.builder.update_bar(bar, volume, ts_init);
916 }
917}
918
919pub struct VolumeRunsBarAggregator {
921 core: BarAggregatorCore,
922 current_run_side: Option<AggressorSide>,
923 run_volume_raw: QuantityRaw,
924 raw_step: QuantityRaw,
925}
926
927impl Debug for VolumeRunsBarAggregator {
928 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
929 f.debug_struct(stringify!(VolumeRunsBarAggregator))
930 .field("core", &self.core)
931 .field("current_run_side", &self.current_run_side)
932 .field("run_volume_raw", &self.run_volume_raw)
933 .field("raw_step", &self.raw_step)
934 .finish()
935 }
936}
937
938impl VolumeRunsBarAggregator {
939 pub fn new<H: FnMut(Bar) + 'static>(
945 bar_type: BarType,
946 price_precision: u8,
947 size_precision: u8,
948 handler: H,
949 ) -> Self {
950 let raw_step = (bar_type.spec().step.get() as f64 * FIXED_SCALAR) as QuantityRaw;
951 Self {
952 core: BarAggregatorCore::new(
953 bar_type.standard(),
954 price_precision,
955 size_precision,
956 handler,
957 ),
958 current_run_side: None,
959 run_volume_raw: 0,
960 raw_step,
961 }
962 }
963}
964
965impl BarAggregator for VolumeRunsBarAggregator {
966 fn bar_type(&self) -> BarType {
967 self.core.bar_type
968 }
969
970 fn is_running(&self) -> bool {
971 self.core.is_running
972 }
973
974 fn set_is_running(&mut self, value: bool) {
975 self.core.set_is_running(value);
976 }
977
978 impl_set_historical_handler!();
979 impl_set_adjustment!();
980
981 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
986 self.core.apply_update(price, size, ts_init);
987 }
988
989 fn handle_trade(&mut self, trade: TradeTick) {
990 let side = match trade.aggressor_side {
991 AggressorSide::Buyer => Some(AggressorSide::Buyer),
992 AggressorSide::Seller => Some(AggressorSide::Seller),
993 AggressorSide::NoAggressor => None,
994 };
995
996 let Some(side) = side else {
997 self.core
998 .apply_update(trade.price, trade.size, trade.ts_init);
999 return;
1000 };
1001
1002 if self.current_run_side != Some(side) {
1003 self.current_run_side = Some(side);
1004 self.run_volume_raw = 0;
1005 self.core.builder.reset();
1006 }
1007
1008 let mut raw_remaining = trade.size.raw;
1009 while raw_remaining > 0 {
1010 let needed = self.raw_step.saturating_sub(self.run_volume_raw).max(1);
1011 let raw_chunk = raw_remaining.min(needed);
1012
1013 self.core.apply_update(
1014 trade.price,
1015 Quantity::from_raw(raw_chunk, trade.size.precision),
1016 trade.ts_init,
1017 );
1018
1019 self.run_volume_raw += raw_chunk;
1020 raw_remaining -= raw_chunk;
1021
1022 if self.run_volume_raw >= self.raw_step {
1023 self.core.build_now_and_send();
1024 self.run_volume_raw = 0;
1025 self.current_run_side = None;
1026 }
1027 }
1028 }
1029
1030 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
1031 self.core.builder.update_bar(bar, volume, ts_init);
1032 }
1033}
1034
1035pub struct ValueBarAggregator {
1040 core: BarAggregatorCore,
1041 cum_value: f64,
1042}
1043
1044impl Debug for ValueBarAggregator {
1045 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1046 f.debug_struct(stringify!(ValueBarAggregator))
1047 .field("core", &self.core)
1048 .field("cum_value", &self.cum_value)
1049 .finish()
1050 }
1051}
1052
1053impl ValueBarAggregator {
1054 pub fn new<H: FnMut(Bar) + 'static>(
1060 bar_type: BarType,
1061 price_precision: u8,
1062 size_precision: u8,
1063 handler: H,
1064 ) -> Self {
1065 Self {
1066 core: BarAggregatorCore::new(
1067 bar_type.standard(),
1068 price_precision,
1069 size_precision,
1070 handler,
1071 ),
1072 cum_value: 0.0,
1073 }
1074 }
1075
1076 #[must_use]
1077 pub const fn get_cumulative_value(&self) -> f64 {
1079 self.cum_value
1080 }
1081}
1082
1083impl BarAggregator for ValueBarAggregator {
1084 fn bar_type(&self) -> BarType {
1085 self.core.bar_type
1086 }
1087
1088 fn is_running(&self) -> bool {
1089 self.core.is_running
1090 }
1091
1092 fn set_is_running(&mut self, value: bool) {
1093 self.core.set_is_running(value);
1094 }
1095
1096 impl_set_historical_handler!();
1097 impl_set_adjustment!();
1098
1099 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
1101 let mut size_update = size.as_f64();
1102 let spec = self.core.bar_type.spec();
1103
1104 while size_update > 0.0 {
1105 let value_update = price.as_f64() * size_update;
1106 if value_update == 0.0 {
1107 self.core
1109 .apply_update(price, Quantity::new(size_update, size.precision), ts_init);
1110 break;
1111 }
1112
1113 if self.cum_value + value_update < spec.step.get() as f64 {
1114 self.cum_value += value_update;
1115 self.core
1116 .apply_update(price, Quantity::new(size_update, size.precision), ts_init);
1117 break;
1118 }
1119
1120 let value_diff = spec.step.get() as f64 - self.cum_value;
1121 let mut size_diff = size_update * (value_diff / value_update);
1122
1123 if is_below_min_size(size_diff, size.precision) {
1125 if is_below_min_size(size_update, size.precision) {
1126 break;
1127 }
1128 size_diff = min_size_f64(size.precision);
1129 }
1130
1131 self.core
1132 .apply_update(price, Quantity::new(size_diff, size.precision), ts_init);
1133
1134 self.core.build_now_and_send();
1135 self.cum_value = 0.0;
1136 size_update -= size_diff;
1137 }
1138 }
1139
1140 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
1141 let mut volume_update = volume;
1142 let average_price = Price::new(
1143 (bar.high.as_f64() + bar.low.as_f64() + bar.close.as_f64()) / 3.0,
1144 self.core.builder.price_precision,
1145 );
1146
1147 while volume_update.as_f64() > 0.0 {
1148 let value_update = average_price.as_f64() * volume_update.as_f64();
1149 if value_update == 0.0 {
1150 self.core.builder.update_bar(bar, volume_update, ts_init);
1152 break;
1153 }
1154
1155 if self.cum_value + value_update < self.core.bar_type.spec().step.get() as f64 {
1156 self.cum_value += value_update;
1157 self.core.builder.update_bar(bar, volume_update, ts_init);
1158 break;
1159 }
1160
1161 let value_diff = self.core.bar_type.spec().step.get() as f64 - self.cum_value;
1162 let mut volume_diff = volume_update.as_f64() * (value_diff / value_update);
1163
1164 if is_below_min_size(volume_diff, volume_update.precision) {
1166 if is_below_min_size(volume_update.as_f64(), volume_update.precision) {
1167 break;
1168 }
1169 volume_diff = min_size_f64(volume_update.precision);
1170 }
1171
1172 self.core.builder.update_bar(
1173 bar,
1174 Quantity::new(volume_diff, volume_update.precision),
1175 ts_init,
1176 );
1177
1178 self.core.build_now_and_send();
1179 self.cum_value = 0.0;
1180 volume_update = Quantity::new(
1181 volume_update.as_f64() - volume_diff,
1182 volume_update.precision,
1183 );
1184 }
1185 }
1186}
1187
1188pub struct ValueImbalanceBarAggregator {
1190 core: BarAggregatorCore,
1191 imbalance_value: f64,
1192 step_value: f64,
1193}
1194
1195impl Debug for ValueImbalanceBarAggregator {
1196 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1197 f.debug_struct(stringify!(ValueImbalanceBarAggregator))
1198 .field("core", &self.core)
1199 .field("imbalance_value", &self.imbalance_value)
1200 .field("step_value", &self.step_value)
1201 .finish()
1202 }
1203}
1204
1205impl ValueImbalanceBarAggregator {
1206 pub fn new<H: FnMut(Bar) + 'static>(
1212 bar_type: BarType,
1213 price_precision: u8,
1214 size_precision: u8,
1215 handler: H,
1216 ) -> Self {
1217 Self {
1218 core: BarAggregatorCore::new(
1219 bar_type.standard(),
1220 price_precision,
1221 size_precision,
1222 handler,
1223 ),
1224 imbalance_value: 0.0,
1225 step_value: bar_type.spec().step.get() as f64,
1226 }
1227 }
1228}
1229
1230impl BarAggregator for ValueImbalanceBarAggregator {
1231 fn bar_type(&self) -> BarType {
1232 self.core.bar_type
1233 }
1234
1235 fn is_running(&self) -> bool {
1236 self.core.is_running
1237 }
1238
1239 fn set_is_running(&mut self, value: bool) {
1240 self.core.set_is_running(value);
1241 }
1242
1243 impl_set_historical_handler!();
1244 impl_set_adjustment!();
1245
1246 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
1251 self.core.apply_update(price, size, ts_init);
1252 }
1253
1254 fn handle_trade(&mut self, trade: TradeTick) {
1255 let price_f64 = trade.price.as_f64();
1256 if price_f64 == 0.0 {
1257 self.core
1258 .apply_update(trade.price, trade.size, trade.ts_init);
1259 return;
1260 }
1261
1262 let side_sign = match trade.aggressor_side {
1263 AggressorSide::Buyer => 1.0,
1264 AggressorSide::Seller => -1.0,
1265 AggressorSide::NoAggressor => {
1266 self.core
1267 .apply_update(trade.price, trade.size, trade.ts_init);
1268 return;
1269 }
1270 };
1271
1272 let mut size_remaining = trade.size.as_f64();
1273 while size_remaining > 0.0 {
1274 let value_remaining = price_f64 * size_remaining;
1275
1276 #[expect(clippy::float_cmp, reason = "exact-zero check on accumulator")]
1277 if self.imbalance_value == 0.0 || self.imbalance_value.signum() == side_sign {
1278 let needed = self.step_value - self.imbalance_value.abs();
1279 if value_remaining <= needed {
1280 self.imbalance_value += side_sign * value_remaining;
1281 self.core.apply_update(
1282 trade.price,
1283 Quantity::new(size_remaining, trade.size.precision),
1284 trade.ts_init,
1285 );
1286
1287 if self.imbalance_value.abs() >= self.step_value {
1288 self.core.build_now_and_send();
1289 self.imbalance_value = 0.0;
1290 }
1291 break;
1292 }
1293
1294 let mut value_chunk = needed;
1295 let mut size_chunk = value_chunk / price_f64;
1296
1297 if is_below_min_size(size_chunk, trade.size.precision) {
1299 if is_below_min_size(size_remaining, trade.size.precision) {
1300 break;
1301 }
1302 size_chunk = min_size_f64(trade.size.precision);
1303 value_chunk = price_f64 * size_chunk;
1304 }
1305
1306 self.core.apply_update(
1307 trade.price,
1308 Quantity::new(size_chunk, trade.size.precision),
1309 trade.ts_init,
1310 );
1311 self.imbalance_value += side_sign * value_chunk;
1312 size_remaining -= size_chunk;
1313
1314 if self.imbalance_value.abs() >= self.step_value {
1315 self.core.build_now_and_send();
1316 self.imbalance_value = 0.0;
1317 }
1318 } else {
1319 let mut value_to_flatten = self.imbalance_value.abs().min(value_remaining);
1321 let mut size_chunk = value_to_flatten / price_f64;
1322
1323 if is_below_min_size(size_chunk, trade.size.precision) {
1325 if is_below_min_size(size_remaining, trade.size.precision) {
1326 break;
1327 }
1328 size_chunk = min_size_f64(trade.size.precision);
1329 value_to_flatten = price_f64 * size_chunk;
1330 }
1331
1332 self.core.apply_update(
1333 trade.price,
1334 Quantity::new(size_chunk, trade.size.precision),
1335 trade.ts_init,
1336 );
1337 self.imbalance_value += side_sign * value_to_flatten;
1338
1339 if self.imbalance_value.abs() >= self.step_value {
1341 self.core.build_now_and_send();
1342 self.imbalance_value = 0.0;
1343 }
1344 size_remaining -= size_chunk;
1345 }
1346 }
1347 }
1348
1349 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
1350 self.core.builder.update_bar(bar, volume, ts_init);
1351 }
1352}
1353
1354pub struct ValueRunsBarAggregator {
1356 core: BarAggregatorCore,
1357 current_run_side: Option<AggressorSide>,
1358 run_value: f64,
1359 step_value: f64,
1360}
1361
1362impl Debug for ValueRunsBarAggregator {
1363 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1364 f.debug_struct(stringify!(ValueRunsBarAggregator))
1365 .field("core", &self.core)
1366 .field("current_run_side", &self.current_run_side)
1367 .field("run_value", &self.run_value)
1368 .field("step_value", &self.step_value)
1369 .finish()
1370 }
1371}
1372
1373impl ValueRunsBarAggregator {
1374 pub fn new<H: FnMut(Bar) + 'static>(
1380 bar_type: BarType,
1381 price_precision: u8,
1382 size_precision: u8,
1383 handler: H,
1384 ) -> Self {
1385 Self {
1386 core: BarAggregatorCore::new(
1387 bar_type.standard(),
1388 price_precision,
1389 size_precision,
1390 handler,
1391 ),
1392 current_run_side: None,
1393 run_value: 0.0,
1394 step_value: bar_type.spec().step.get() as f64,
1395 }
1396 }
1397}
1398
1399impl BarAggregator for ValueRunsBarAggregator {
1400 fn bar_type(&self) -> BarType {
1401 self.core.bar_type
1402 }
1403
1404 fn is_running(&self) -> bool {
1405 self.core.is_running
1406 }
1407
1408 fn set_is_running(&mut self, value: bool) {
1409 self.core.set_is_running(value);
1410 }
1411
1412 impl_set_historical_handler!();
1413 impl_set_adjustment!();
1414
1415 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
1420 self.core.apply_update(price, size, ts_init);
1421 }
1422
1423 fn handle_trade(&mut self, trade: TradeTick) {
1424 let price_f64 = trade.price.as_f64();
1425 if price_f64 == 0.0 {
1426 self.core
1427 .apply_update(trade.price, trade.size, trade.ts_init);
1428 return;
1429 }
1430
1431 let side = match trade.aggressor_side {
1432 AggressorSide::Buyer => Some(AggressorSide::Buyer),
1433 AggressorSide::Seller => Some(AggressorSide::Seller),
1434 AggressorSide::NoAggressor => None,
1435 };
1436
1437 let Some(side) = side else {
1438 self.core
1439 .apply_update(trade.price, trade.size, trade.ts_init);
1440 return;
1441 };
1442
1443 if self.current_run_side != Some(side) {
1444 self.current_run_side = Some(side);
1445 self.run_value = 0.0;
1446 self.core.builder.reset();
1447 }
1448
1449 let mut size_remaining = trade.size.as_f64();
1450 while size_remaining > 0.0 {
1451 let value_update = price_f64 * size_remaining;
1452 if self.run_value + value_update < self.step_value {
1453 self.run_value += value_update;
1454 self.core.apply_update(
1455 trade.price,
1456 Quantity::new(size_remaining, trade.size.precision),
1457 trade.ts_init,
1458 );
1459 break;
1460 }
1461
1462 let value_needed = self.step_value - self.run_value;
1463 let mut size_chunk = value_needed / price_f64;
1464
1465 if is_below_min_size(size_chunk, trade.size.precision) {
1467 if is_below_min_size(size_remaining, trade.size.precision) {
1468 break;
1469 }
1470 size_chunk = min_size_f64(trade.size.precision);
1471 }
1472
1473 self.core.apply_update(
1474 trade.price,
1475 Quantity::new(size_chunk, trade.size.precision),
1476 trade.ts_init,
1477 );
1478
1479 self.core.build_now_and_send();
1480 self.run_value = 0.0;
1481 self.current_run_side = None;
1482 size_remaining -= size_chunk;
1483 }
1484 }
1485
1486 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
1487 self.core.builder.update_bar(bar, volume, ts_init);
1488 }
1489}
1490
1491pub struct RenkoBarAggregator {
1497 core: BarAggregatorCore,
1498 pub brick_size: PriceRaw,
1499 last_close: Option<Price>,
1500}
1501
1502impl Debug for RenkoBarAggregator {
1503 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1504 f.debug_struct(stringify!(RenkoBarAggregator))
1505 .field("core", &self.core)
1506 .field("brick_size", &self.brick_size)
1507 .field("last_close", &self.last_close)
1508 .finish()
1509 }
1510}
1511
1512impl RenkoBarAggregator {
1513 pub fn new<H: FnMut(Bar) + 'static>(
1519 bar_type: BarType,
1520 price_precision: u8,
1521 size_precision: u8,
1522 price_increment: Price,
1523 handler: H,
1524 ) -> Self {
1525 let brick_size = bar_type.spec().step.get() as PriceRaw * price_increment.raw;
1527
1528 Self {
1529 core: BarAggregatorCore::new(
1530 bar_type.standard(),
1531 price_precision,
1532 size_precision,
1533 handler,
1534 ),
1535 brick_size,
1536 last_close: None,
1537 }
1538 }
1539}
1540
1541impl BarAggregator for RenkoBarAggregator {
1542 fn bar_type(&self) -> BarType {
1543 self.core.bar_type
1544 }
1545
1546 fn is_running(&self) -> bool {
1547 self.core.is_running
1548 }
1549
1550 fn set_is_running(&mut self, value: bool) {
1551 self.core.set_is_running(value);
1552 }
1553
1554 impl_set_historical_handler!();
1555 impl_set_adjustment!();
1556
1557 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
1562 self.core.apply_update(price, size, ts_init);
1564
1565 if self.last_close.is_none() {
1567 self.last_close = Some(price);
1568 return;
1569 }
1570
1571 let last_close = self.last_close.unwrap();
1572
1573 let current_raw = price.raw;
1575 let last_close_raw = last_close.raw;
1576 let price_diff_raw = current_raw - last_close_raw;
1577 let abs_price_diff_raw = price_diff_raw.abs();
1578
1579 if abs_price_diff_raw >= self.brick_size {
1581 let num_bricks = (abs_price_diff_raw / self.brick_size) as usize;
1582 let direction = if price_diff_raw > 0 { 1.0 } else { -1.0 };
1583 let mut current_close = last_close;
1584
1585 let total_volume = self.core.builder.volume;
1587
1588 for _i in 0..num_bricks {
1589 let brick_close_raw = current_close.raw + (direction as PriceRaw) * self.brick_size;
1591 let brick_close = Price::from_raw(brick_close_raw, price.precision);
1592
1593 let (brick_high, brick_low) = if direction > 0.0 {
1595 (brick_close, current_close)
1596 } else {
1597 (current_close, brick_close)
1598 };
1599
1600 self.core.builder.reset();
1602 self.core.builder.open = Some(current_close);
1603 self.core.builder.high = Some(brick_high);
1604 self.core.builder.low = Some(brick_low);
1605 self.core.builder.close = Some(brick_close);
1606 self.core.builder.volume = total_volume; self.core.builder.count = 1;
1608 self.core.builder.ts_last = ts_init;
1609 self.core.builder.initialized = true;
1610
1611 self.core.build_and_send(ts_init, ts_init);
1613
1614 current_close = brick_close;
1616 self.last_close = Some(brick_close);
1617 }
1618 }
1619 }
1620
1621 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
1622 self.core.builder.update_bar(bar, volume, ts_init);
1624
1625 if self.last_close.is_none() {
1627 self.last_close = Some(bar.close);
1628 return;
1629 }
1630
1631 let last_close = self.last_close.unwrap();
1632
1633 let current_raw = bar.close.raw;
1635 let last_close_raw = last_close.raw;
1636 let price_diff_raw = current_raw - last_close_raw;
1637 let abs_price_diff_raw = price_diff_raw.abs();
1638
1639 if abs_price_diff_raw >= self.brick_size {
1641 let num_bricks = (abs_price_diff_raw / self.brick_size) as usize;
1642 let direction = if price_diff_raw > 0 { 1.0 } else { -1.0 };
1643 let mut current_close = last_close;
1644
1645 let total_volume = self.core.builder.volume;
1647
1648 for _i in 0..num_bricks {
1649 let brick_close_raw = current_close.raw + (direction as PriceRaw) * self.brick_size;
1651 let brick_close = Price::from_raw(brick_close_raw, bar.close.precision);
1652
1653 let (brick_high, brick_low) = if direction > 0.0 {
1655 (brick_close, current_close)
1656 } else {
1657 (current_close, brick_close)
1658 };
1659
1660 self.core.builder.reset();
1662 self.core.builder.open = Some(current_close);
1663 self.core.builder.high = Some(brick_high);
1664 self.core.builder.low = Some(brick_low);
1665 self.core.builder.close = Some(brick_close);
1666 self.core.builder.volume = total_volume; self.core.builder.count = 1;
1668 self.core.builder.ts_last = ts_init;
1669 self.core.builder.initialized = true;
1670
1671 self.core.build_and_send(ts_init, ts_init);
1673
1674 current_close = brick_close;
1676 self.last_close = Some(brick_close);
1677 }
1678 }
1679 }
1680}
1681
1682pub struct TimeBarAggregator {
1686 core: BarAggregatorCore,
1687 clock: Rc<RefCell<dyn Clock>>,
1688 build_with_no_updates: bool,
1689 timestamp_on_close: bool,
1690 is_left_open: bool,
1691 stored_open_ns: UnixNanos,
1692 timer_name: String,
1693 interval_ns: UnixNanos,
1694 next_close_ns: UnixNanos,
1695 first_close_ns: UnixNanos,
1696 bar_build_delay: u64,
1697 time_bars_origin_offset: Option<TimeDelta>,
1698 skip_first_non_full_bar: bool,
1699 pub historical_mode: bool,
1700 historical_events: Vec<TimeEvent>,
1701 historical_event_at_ts_init: Option<TimeEvent>,
1702 aggregator_weak: Option<Weak<RefCell<Box<dyn BarAggregator>>>>,
1703}
1704
1705impl Debug for TimeBarAggregator {
1706 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1707 f.debug_struct(stringify!(TimeBarAggregator))
1708 .field("core", &self.core)
1709 .field("build_with_no_updates", &self.build_with_no_updates)
1710 .field("timestamp_on_close", &self.timestamp_on_close)
1711 .field("is_left_open", &self.is_left_open)
1712 .field("timer_name", &self.timer_name)
1713 .field("interval_ns", &self.interval_ns)
1714 .field("bar_build_delay", &self.bar_build_delay)
1715 .field("skip_first_non_full_bar", &self.skip_first_non_full_bar)
1716 .finish()
1717 }
1718}
1719
1720impl TimeBarAggregator {
1721 #[expect(clippy::too_many_arguments)]
1727 pub fn new<H: FnMut(Bar) + 'static>(
1728 bar_type: BarType,
1729 price_precision: u8,
1730 size_precision: u8,
1731 clock: Rc<RefCell<dyn Clock>>,
1732 handler: H,
1733 build_with_no_updates: bool,
1734 timestamp_on_close: bool,
1735 interval_type: BarIntervalType,
1736 time_bars_origin_offset: Option<TimeDelta>,
1737 bar_build_delay: u64,
1738 skip_first_non_full_bar: bool,
1739 ) -> Self {
1740 let is_left_open = match interval_type {
1741 BarIntervalType::LeftOpen => true,
1742 BarIntervalType::RightOpen => false,
1743 };
1744
1745 let core = BarAggregatorCore::new(
1746 bar_type.standard(),
1747 price_precision,
1748 size_precision,
1749 handler,
1750 );
1751
1752 Self {
1753 core,
1754 clock,
1755 build_with_no_updates,
1756 timestamp_on_close,
1757 is_left_open,
1758 stored_open_ns: UnixNanos::default(),
1759 timer_name: format!("TIME_BAR_{bar_type}"),
1760 interval_ns: get_bar_interval_ns(&bar_type),
1761 next_close_ns: UnixNanos::default(),
1762 first_close_ns: UnixNanos::default(),
1763 bar_build_delay,
1764 time_bars_origin_offset,
1765 skip_first_non_full_bar,
1766 historical_mode: false,
1767 historical_events: Vec::new(),
1768 historical_event_at_ts_init: None,
1769 aggregator_weak: None,
1770 }
1771 }
1772
1773 pub fn set_clock_internal(&mut self, clock: Rc<RefCell<dyn Clock>>) {
1775 self.clock = clock;
1776 }
1777
1778 pub fn start_timer_internal(
1787 &mut self,
1788 aggregator_rc: Option<Rc<RefCell<Box<dyn BarAggregator>>>>,
1789 ) {
1790 let aggregator_weak = if let Some(rc) = aggregator_rc {
1792 let weak = Rc::downgrade(&rc);
1794 self.aggregator_weak = Some(weak.clone());
1795 weak
1796 } else {
1797 self.aggregator_weak
1799 .as_ref()
1800 .expect("Aggregator weak reference must be set before calling start_timer()")
1801 .clone()
1802 };
1803
1804 let callback = TimeEventCallback::RustLocal(Rc::new(move |event: TimeEvent| {
1805 if let Some(agg) = aggregator_weak.upgrade() {
1806 agg.borrow_mut().build_bar(&event);
1807 }
1808 }));
1809
1810 let now = self.clock.borrow().utc_now();
1812 let mut start_time =
1813 get_time_bar_start(now, &self.bar_type(), self.time_bars_origin_offset);
1814 start_time += TimeDelta::microseconds(self.bar_build_delay as i64);
1815
1816 let fire_immediately = start_time == now;
1818
1819 let spec = &self.bar_type().spec();
1820 let start_time_ns = UnixNanos::from(start_time);
1821 let step = spec.step.get() as u32;
1822
1823 if spec.aggregation != BarAggregation::Month && spec.aggregation != BarAggregation::Year {
1824 self.clock
1825 .borrow_mut()
1826 .set_timer_ns(
1827 &self.timer_name,
1828 self.interval_ns.as_u64(),
1829 Some(start_time_ns),
1830 None,
1831 Some(callback),
1832 Some(true), Some(fire_immediately),
1834 )
1835 .expect(FAILED);
1836
1837 if fire_immediately {
1838 self.next_close_ns = start_time_ns;
1839 } else {
1840 let interval_duration = Duration::nanoseconds(self.interval_ns.as_i64());
1841 self.next_close_ns = UnixNanos::from(start_time + interval_duration);
1842 }
1843
1844 self.stored_open_ns = self.next_close_ns.saturating_sub_ns(self.interval_ns);
1845 } else {
1846 let alert_time = if fire_immediately {
1848 start_time
1849 } else if spec.aggregation == BarAggregation::Month {
1850 add_n_months(start_time, step).expect(FAILED)
1851 } else {
1852 add_n_years(start_time, step).expect(FAILED)
1853 };
1854
1855 self.clock
1856 .borrow_mut()
1857 .set_time_alert_ns(
1858 &self.timer_name,
1859 UnixNanos::from(alert_time),
1860 Some(callback),
1861 Some(true), )
1863 .expect(FAILED);
1864
1865 self.next_close_ns = UnixNanos::from(alert_time);
1866 self.stored_open_ns = if fire_immediately {
1869 if spec.aggregation == BarAggregation::Month {
1870 subtract_n_months_nanos(start_time_ns, step).expect(FAILED)
1871 } else {
1872 subtract_n_years_nanos(start_time_ns, step).expect(FAILED)
1873 }
1874 } else {
1875 start_time_ns
1876 };
1877 }
1878
1879 if self.skip_first_non_full_bar {
1880 self.first_close_ns = self.next_close_ns;
1881 }
1882
1883 log::debug!(
1884 "Started timer {}, start_time={:?}, historical_mode={}, fire_immediately={}, now={:?}, bar_build_delay={}",
1885 self.timer_name,
1886 start_time,
1887 self.historical_mode,
1888 fire_immediately,
1889 now,
1890 self.bar_build_delay
1891 );
1892 }
1893
1894 pub fn stop(&mut self) {
1896 self.clock.borrow_mut().cancel_timer(&self.timer_name);
1897 }
1898
1899 fn build_and_send(&mut self, ts_event: UnixNanos, ts_init: UnixNanos) {
1900 if self.skip_first_non_full_bar && ts_init <= self.first_close_ns {
1901 self.core.builder.reset();
1902 } else {
1903 self.skip_first_non_full_bar = false;
1906 self.core.build_and_send(ts_event, ts_init);
1907 }
1908 }
1909
1910 fn build_bar(&mut self, event: &TimeEvent) {
1911 if !self.core.builder.initialized {
1912 return;
1913 }
1914
1915 if !self.build_with_no_updates && self.core.builder.count == 0 {
1916 return; }
1918
1919 let ts_init = event.ts_event;
1920 let ts_event = if self.is_left_open {
1921 if self.timestamp_on_close {
1922 event.ts_event
1923 } else {
1924 self.stored_open_ns
1925 }
1926 } else {
1927 self.stored_open_ns
1928 };
1929
1930 self.build_and_send(ts_event, ts_init);
1931
1932 self.stored_open_ns = event.ts_event;
1934
1935 if self.bar_type().spec().aggregation == BarAggregation::Month {
1936 let step = self.bar_type().spec().step.get() as u32;
1937 let alert_time_ns = add_n_months_nanos(event.ts_event, step).expect(FAILED);
1938
1939 self.clock
1940 .borrow_mut()
1941 .set_time_alert_ns(&self.timer_name, alert_time_ns, None, None)
1942 .expect(FAILED);
1943
1944 self.next_close_ns = alert_time_ns;
1945 } else if self.bar_type().spec().aggregation == BarAggregation::Year {
1946 let step = self.bar_type().spec().step.get() as u32;
1947 let alert_time_ns = add_n_years_nanos(event.ts_event, step).expect(FAILED);
1948
1949 self.clock
1950 .borrow_mut()
1951 .set_time_alert_ns(&self.timer_name, alert_time_ns, None, None)
1952 .expect(FAILED);
1953
1954 self.next_close_ns = alert_time_ns;
1955 } else {
1956 self.next_close_ns = self
1958 .clock
1959 .borrow()
1960 .next_time_ns(&self.timer_name)
1961 .unwrap_or_default();
1962 }
1963 }
1964
1965 fn preprocess_historical_events(&mut self, ts_init: UnixNanos) {
1966 if self.clock.borrow().timestamp_ns() == UnixNanos::default() {
1967 {
1969 let mut clock_borrow = self.clock.borrow_mut();
1970 let test_clock = clock_borrow
1971 .as_any_mut()
1972 .downcast_mut::<TestClock>()
1973 .expect("Expected TestClock in historical mode");
1974 test_clock.set_time(ts_init);
1975 }
1976 self.start_timer_internal(None);
1978 }
1979
1980 let events = {
1982 let mut clock_borrow = self.clock.borrow_mut();
1983 let test_clock = clock_borrow
1984 .as_any_mut()
1985 .downcast_mut::<TestClock>()
1986 .expect("Expected TestClock in historical mode");
1987 test_clock.advance_time(ts_init, true)
1988 };
1989
1990 for event in events {
1991 if event.ts_event == ts_init {
1992 self.historical_event_at_ts_init = Some(event);
1993 } else {
1994 self.build_bar(&event);
1995 }
1996 }
1997 }
1998
1999 fn postprocess_historical_events(&mut self, _ts_init: UnixNanos) {
2000 if let Some(ref event) = self.historical_event_at_ts_init.take() {
2001 self.build_bar(event);
2002 }
2003 }
2004
2005 pub fn set_historical_events_internal(&mut self, events: Vec<TimeEvent>) {
2007 self.historical_events = events;
2008 }
2009}
2010
2011impl BarAggregator for TimeBarAggregator {
2012 fn bar_type(&self) -> BarType {
2013 self.core.bar_type
2014 }
2015
2016 fn is_running(&self) -> bool {
2017 self.core.is_running
2018 }
2019
2020 fn set_is_running(&mut self, value: bool) {
2021 self.core.set_is_running(value);
2022 }
2023
2024 fn stop(&mut self) {
2026 Self::stop(self);
2027 }
2028
2029 fn update(&mut self, price: Price, size: Quantity, ts_init: UnixNanos) {
2030 if self.historical_mode {
2031 self.preprocess_historical_events(ts_init);
2032 }
2033
2034 self.core.apply_update(price, size, ts_init);
2035
2036 if self.historical_mode {
2037 self.postprocess_historical_events(ts_init);
2038 }
2039 }
2040
2041 fn update_bar(&mut self, bar: Bar, volume: Quantity, ts_init: UnixNanos) {
2042 if self.historical_mode {
2043 self.preprocess_historical_events(ts_init);
2044 }
2045
2046 self.core.builder.update_bar(bar, volume, ts_init);
2047
2048 if self.historical_mode {
2049 self.postprocess_historical_events(ts_init);
2050 }
2051 }
2052
2053 fn set_historical_mode(&mut self, historical_mode: bool, handler: Box<dyn FnMut(Bar)>) {
2054 self.historical_mode = historical_mode;
2055 self.core.handler = handler;
2056 }
2057
2058 fn set_historical_events(&mut self, events: Vec<TimeEvent>) {
2059 self.set_historical_events_internal(events);
2060 }
2061
2062 fn set_clock(&mut self, clock: Rc<RefCell<dyn Clock>>) {
2063 self.set_clock_internal(clock);
2064 }
2065
2066 fn build_bar(&mut self, event: &TimeEvent) {
2067 {
2070 #[expect(clippy::use_self)]
2071 TimeBarAggregator::build_bar(self, event);
2072 }
2073 }
2074
2075 fn set_aggregator_weak(&mut self, weak: Weak<RefCell<Box<dyn BarAggregator>>>) {
2076 self.aggregator_weak = Some(weak);
2077 }
2078
2079 fn start_timer(&mut self, aggregator_rc: Option<Rc<RefCell<Box<dyn BarAggregator>>>>) {
2080 self.start_timer_internal(aggregator_rc);
2081 }
2082
2083 fn set_adjustment(&mut self, adjustment: Decimal, mode: ContinuousFutureAdjustmentType) {
2084 self.core.set_adjustment(adjustment, mode);
2085 }
2086}
2087
2088fn is_below_min_size(size: f64, precision: u8) -> bool {
2089 Quantity::new(size, precision).raw == 0
2090}
2091
2092fn min_size_f64(precision: u8) -> f64 {
2093 10_f64.powi(-(precision as i32))
2094}
2095
2096pub trait VegaProvider {
2098 fn vega_for_leg(&self, instrument_id: InstrumentId) -> Option<f64>;
2100}
2101
2102pub trait SpreadPriceRounder {
2104 fn round_prices(&self, raw_bid: f64, raw_ask: f64, precision: u8) -> (Price, Price);
2106}
2107
2108#[derive(Debug, Default)]
2110pub struct MapVegaProvider {
2111 vegas: AHashMap<InstrumentId, f64>,
2112}
2113
2114impl MapVegaProvider {
2115 pub fn new() -> Self {
2116 Self {
2117 vegas: AHashMap::new(),
2118 }
2119 }
2120
2121 pub fn insert(&mut self, instrument_id: InstrumentId, vega: f64) {
2122 self.vegas.insert(instrument_id, vega);
2123 }
2124
2125 pub fn get(&self, instrument_id: &InstrumentId) -> Option<f64> {
2126 self.vegas.get(instrument_id).copied()
2127 }
2128}
2129
2130impl VegaProvider for MapVegaProvider {
2131 fn vega_for_leg(&self, instrument_id: InstrumentId) -> Option<f64> {
2132 self.vegas.get(&instrument_id).copied()
2133 }
2134}
2135
2136#[derive(Debug)]
2138pub struct FixedTickSchemeRounder {
2139 scheme: FixedTickScheme,
2140}
2141
2142impl FixedTickSchemeRounder {
2143 pub fn new(tick: f64) -> anyhow::Result<Self> {
2149 Ok(Self {
2150 scheme: FixedTickScheme::new(tick)?,
2151 })
2152 }
2153
2154 fn round_one(&self, raw: f64, precision: u8, use_bid_rounding: bool) -> Price {
2155 if raw >= 0.0 {
2156 let p = if use_bid_rounding {
2157 self.scheme.next_bid_price(raw, 0, precision)
2158 } else {
2159 self.scheme.next_ask_price(raw, 0, precision)
2160 };
2161 p.unwrap_or_else(|| price_from_f64(raw, precision))
2162 } else {
2163 let p = if use_bid_rounding {
2164 self.scheme.next_ask_price(-raw, 0, precision)
2165 } else {
2166 self.scheme.next_bid_price(-raw, 0, precision)
2167 };
2168 p.map_or_else(
2169 || price_from_f64(raw, precision),
2170 |q| price_from_f64(-q.as_f64(), precision),
2171 )
2172 }
2173 }
2174}
2175
2176impl SpreadPriceRounder for FixedTickSchemeRounder {
2177 fn round_prices(&self, raw_bid: f64, raw_ask: f64, precision: u8) -> (Price, Price) {
2178 let bid = self.round_one(raw_bid, precision, true);
2179 let ask = self.round_one(raw_ask, precision, false);
2180 (bid, ask)
2181 }
2182}
2183
2184pub struct SpreadQuoteAggregator {
2190 spread_instrument_id: InstrumentId,
2191 leg_ids: Vec<InstrumentId>,
2192 ratios: Vec<i64>,
2193 n_legs: usize,
2194 is_futures_spread: bool,
2195 price_precision: u8,
2196 size_precision: u8,
2197 last_quotes: AHashMap<InstrumentId, QuoteTick>,
2198 mid_prices: Vec<f64>,
2199 bid_prices: Vec<f64>,
2200 ask_prices: Vec<f64>,
2201 vegas: Vec<f64>,
2202 bid_ask_spreads: Vec<f64>,
2203 bid_sizes: Vec<f64>,
2204 ask_sizes: Vec<f64>,
2205 handler: Box<dyn FnMut(QuoteTick)>,
2206 clock: Rc<RefCell<dyn Clock>>,
2207 historical_mode: bool,
2208 update_interval_seconds: Option<u64>,
2209 quote_build_delay: u64,
2210 has_update: bool,
2211 timer_name: String,
2212 vega_pricing_timeout_timer_name: String,
2213 historical_event_at_ts_init: Option<TimeEvent>,
2214 vega_provider: Option<Box<dyn VegaProvider>>,
2215 disable_vega_pricing: bool,
2216 vega_pricing_temporarily_disabled: bool,
2217 vega_pricing_timeout_seconds: u64,
2218 price_rounder: Option<Box<dyn SpreadPriceRounder>>,
2219 is_running: bool,
2220 aggregator_weak: Option<Weak<RefCell<Self>>>,
2221}
2222
2223impl Debug for SpreadQuoteAggregator {
2224 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2225 f.debug_struct(stringify!(SpreadQuoteAggregator))
2226 .field("spread_instrument_id", &self.spread_instrument_id)
2227 .field("n_legs", &self.n_legs)
2228 .field("is_futures_spread", &self.is_futures_spread)
2229 .field("update_interval_seconds", &self.update_interval_seconds)
2230 .finish()
2231 }
2232}
2233
2234impl SpreadQuoteAggregator {
2235 #[expect(clippy::too_many_arguments)]
2241 pub fn new(
2242 spread_instrument_id: InstrumentId,
2243 legs: &[(InstrumentId, i64)],
2244 is_futures_spread: bool,
2245 price_precision: u8,
2246 size_precision: u8,
2247 handler: Box<dyn FnMut(QuoteTick)>,
2248 clock: Rc<RefCell<dyn Clock>>,
2249 historical_mode: bool,
2250 update_interval_seconds: Option<u64>,
2251 quote_build_delay: u64,
2252 disable_vega_pricing: bool,
2253 vega_pricing_timeout_seconds: u64,
2254 vega_provider: Option<Box<dyn VegaProvider>>,
2255 price_rounder: Option<Box<dyn SpreadPriceRounder>>,
2256 ) -> Self {
2257 assert!(legs.len() >= 2, "Spread must have more than one leg");
2258 let n_legs = legs.len();
2259 let leg_ids: Vec<InstrumentId> = legs.iter().map(|(id, _)| *id).collect();
2260 let ratios: Vec<i64> = legs.iter().map(|(_, r)| *r).collect();
2261 for &r in &ratios {
2262 assert!(r != 0, "Ratio cannot be zero");
2263 }
2264 let timer_name = format!("SPREAD_QUOTE_{spread_instrument_id}");
2265 let vega_pricing_timeout_timer_name =
2266 format!("VEGA_PRICING_TIMEOUT_{spread_instrument_id}");
2267 Self {
2268 spread_instrument_id,
2269 leg_ids,
2270 ratios,
2271 n_legs,
2272 is_futures_spread,
2273 price_precision,
2274 size_precision,
2275 last_quotes: AHashMap::new(),
2276 mid_prices: vec![0.0; n_legs],
2277 bid_prices: vec![0.0; n_legs],
2278 ask_prices: vec![0.0; n_legs],
2279 vegas: vec![0.0; n_legs],
2280 bid_ask_spreads: vec![0.0; n_legs],
2281 bid_sizes: vec![0.0; n_legs],
2282 ask_sizes: vec![0.0; n_legs],
2283 handler,
2284 clock,
2285 historical_mode,
2286 update_interval_seconds,
2287 quote_build_delay,
2288 has_update: false,
2289 timer_name,
2290 vega_pricing_timeout_timer_name,
2291 historical_event_at_ts_init: None,
2292 vega_provider,
2293 disable_vega_pricing,
2294 vega_pricing_temporarily_disabled: false,
2295 vega_pricing_timeout_seconds,
2296 price_rounder,
2297 is_running: false,
2298 aggregator_weak: None,
2299 }
2300 }
2301
2302 pub fn set_aggregator_weak(&mut self, weak: Weak<RefCell<Self>>) {
2305 self.aggregator_weak = Some(weak);
2306 }
2307
2308 pub fn prepare_for_timer_mode(&mut self, self_rc: &Rc<RefCell<Self>>) {
2313 self.aggregator_weak = Some(Rc::downgrade(self_rc));
2314 }
2315
2316 pub fn set_historical_mode(
2318 &mut self,
2319 historical_mode: bool,
2320 handler: Box<dyn FnMut(QuoteTick)>,
2321 vega_provider: Option<Box<dyn VegaProvider>>,
2322 ) {
2323 self.historical_mode = historical_mode;
2324 self.handler = handler;
2325
2326 if let Some(vp) = vega_provider {
2327 self.vega_provider = Some(vp);
2328 }
2329 }
2330
2331 pub fn set_running(&mut self, is_running: bool) {
2332 self.is_running = is_running;
2333 }
2334
2335 pub fn set_clock(&mut self, clock: Rc<RefCell<dyn Clock>>) {
2336 self.clock = clock;
2337 }
2338
2339 pub fn start_timer(&mut self, aggregator_rc: Option<Rc<RefCell<Self>>>) {
2348 if let Some(rc) = aggregator_rc {
2349 self.aggregator_weak = Some(Rc::downgrade(&rc));
2350 }
2351
2352 let Some(interval_secs) = self.update_interval_seconds else {
2353 return;
2354 };
2355 let aggregator_weak = self.aggregator_weak.clone().expect(
2356 "SpreadQuoteAggregator: timer mode requires prepare_for_timer_mode(rc) to be \
2357 called first with the Rc that wraps this aggregator (before feeding quotes in \
2358 historical mode or before start_timer(None)).",
2359 );
2360
2361 let callback = TimeEventCallback::RustLocal(Rc::new(move |event: TimeEvent| {
2362 if let Some(agg) = aggregator_weak.upgrade() {
2363 agg.borrow_mut().on_timer_fire(event.ts_event);
2364 }
2365 }));
2366
2367 let now_ns = self.clock.borrow().timestamp_ns();
2368 let interval_ns = interval_secs * 1_000_000_000;
2369 let start_ns = (now_ns.as_u64() / interval_ns) * interval_ns;
2370 let start_ns = start_ns + self.quote_build_delay * 1_000; let start_time = UnixNanos::from(start_ns);
2372 let fire_immediately = now_ns == start_time;
2373 self.clock
2374 .borrow_mut()
2375 .set_timer_ns(
2376 &self.timer_name,
2377 interval_ns,
2378 Some(start_time),
2379 None,
2380 Some(callback),
2381 Some(true),
2382 Some(fire_immediately),
2383 )
2384 .expect("Failed to set spread quote timer");
2385 }
2386
2387 pub fn on_timer_fire(&mut self, ts_event: UnixNanos) {
2389 if self.last_quotes.len() == self.n_legs {
2390 self.build_and_send_quote(ts_event);
2391 }
2392 }
2393
2394 pub fn stop_timer(&mut self) {
2396 if self.update_interval_seconds.is_some()
2397 && self
2398 .clock
2399 .borrow()
2400 .timer_names()
2401 .contains(&self.timer_name.as_str())
2402 {
2403 self.clock.borrow_mut().cancel_timer(&self.timer_name);
2404 }
2405
2406 if self
2407 .clock
2408 .borrow()
2409 .timer_names()
2410 .contains(&self.vega_pricing_timeout_timer_name.as_str())
2411 {
2412 self.clock
2413 .borrow_mut()
2414 .cancel_timer(&self.vega_pricing_timeout_timer_name);
2415 }
2416 }
2417
2418 pub fn handle_quote_tick(&mut self, tick: QuoteTick) {
2420 let ts_init = tick.ts_init;
2421
2422 if self.update_interval_seconds.is_some() && self.historical_mode {
2423 self.process_historical_events(ts_init);
2424 }
2425 self.last_quotes.insert(tick.instrument_id, tick);
2426 self.has_update = true;
2427
2428 if self.update_interval_seconds.is_none() && self.last_quotes.len() == self.n_legs {
2429 self.build_and_send_quote(ts_init);
2430 }
2431 }
2432
2433 pub fn flush_pending_historical_quote(&mut self) {
2439 if self.update_interval_seconds.is_none() || !self.historical_mode {
2440 return;
2441 }
2442
2443 let Some(event) = self.historical_event_at_ts_init.take() else {
2444 return;
2445 };
2446
2447 if self.last_quotes.len() == self.n_legs {
2448 self.build_and_send_quote(event.ts_event);
2449 }
2450 }
2451
2452 fn process_historical_events(&mut self, ts_init: UnixNanos) {
2458 if self.clock.borrow().timestamp_ns() == UnixNanos::default() {
2459 let mut clock_borrow = self.clock.borrow_mut();
2460 let test_clock = clock_borrow
2461 .as_any_mut()
2462 .downcast_mut::<TestClock>()
2463 .expect("Expected TestClock in historical mode");
2464 test_clock.set_time(ts_init);
2465 drop(clock_borrow);
2466 self.start_timer(None);
2467 }
2468
2469 if self.last_quotes.len() == self.n_legs
2470 && let Some(ref event) = self.historical_event_at_ts_init
2471 && event.ts_event < ts_init
2472 {
2473 let event = self.historical_event_at_ts_init.take().unwrap();
2475 self.build_and_send_quote(event.ts_event);
2476 }
2477
2478 let events = {
2479 let mut clock_borrow = self.clock.borrow_mut();
2480 let test_clock = clock_borrow
2481 .as_any_mut()
2482 .downcast_mut::<TestClock>()
2483 .expect("Expected TestClock in historical mode");
2484 test_clock.advance_time(ts_init, true)
2485 };
2486
2487 for event in events {
2488 if event.ts_event == ts_init {
2489 self.historical_event_at_ts_init = Some(event);
2490 } else if self.last_quotes.len() == self.n_legs {
2491 self.build_and_send_quote(event.ts_event);
2492 }
2493 }
2494 }
2495
2496 fn build_and_send_quote(&mut self, ts_event: UnixNanos) {
2498 if !self.has_update {
2499 return;
2500 }
2501
2502 let use_vega_pricing =
2503 !(self.disable_vega_pricing || self.vega_pricing_temporarily_disabled);
2504
2505 for (idx, &leg_id) in self.leg_ids.iter().enumerate() {
2506 let Some(tick) = self.last_quotes.get(&leg_id) else {
2507 log::error!(
2508 "SpreadQuoteAggregator[{}]: Missing quote for leg {}",
2509 self.spread_instrument_id,
2510 leg_id
2511 );
2512 return;
2513 };
2514 let ask_price = tick.ask_price.as_f64();
2515 let bid_price = tick.bid_price.as_f64();
2516 self.bid_prices[idx] = bid_price;
2517 self.ask_prices[idx] = ask_price;
2518 self.bid_sizes[idx] = tick.bid_size.as_f64();
2519 self.ask_sizes[idx] = tick.ask_size.as_f64();
2520
2521 if !self.is_futures_spread {
2522 self.mid_prices[idx] = f64::midpoint(ask_price, bid_price);
2523 self.bid_ask_spreads[idx] = ask_price - bid_price;
2524
2525 if use_vega_pricing
2526 && let Some(ref vp) = self.vega_provider
2527 && let Some(vega) = vp.vega_for_leg(leg_id)
2528 {
2529 self.vegas[idx] = vega;
2530 }
2531 }
2532 }
2533 let (raw_bid, raw_ask) = if self.is_futures_spread {
2534 self.create_futures_spread_prices()
2535 } else {
2536 self.create_option_spread_prices()
2537 };
2538 let spread_quote = self.create_quote_tick_from_raw_prices(raw_bid, raw_ask, ts_event);
2539 self.has_update = false;
2540 (self.handler)(spread_quote);
2541 }
2542
2543 fn create_option_spread_prices(&mut self) -> (f64, f64) {
2544 if self.disable_vega_pricing || self.vega_pricing_temporarily_disabled {
2545 return self.create_futures_spread_prices();
2546 }
2547
2548 let vega_multipliers: Vec<f64> = (0..self.n_legs)
2549 .map(|i| {
2550 if self.vegas[i] == 0.0 {
2551 0.0
2552 } else {
2553 self.bid_ask_spreads[i] / self.vegas[i]
2554 }
2555 })
2556 .collect();
2557 let non_zero: Vec<f64> = vega_multipliers
2558 .iter()
2559 .copied()
2560 .filter(|&x| x != 0.0)
2561 .collect();
2562
2563 if non_zero.is_empty() {
2564 log::warn!(
2565 "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",
2566 self.spread_instrument_id,
2567 self.vega_pricing_timeout_seconds
2568 );
2569 self.start_vega_pricing_timeout();
2570 return self.create_futures_spread_prices();
2571 }
2572 let vega_multiplier = non_zero.iter().map(|x| x.abs()).sum::<f64>() / non_zero.len() as f64;
2573 let spread_vega = self
2574 .vegas
2575 .iter()
2576 .zip(self.ratios.iter())
2577 .map(|(v, r)| v * (*r as f64))
2578 .sum::<f64>()
2579 .abs();
2580 let bid_ask_spread = spread_vega * vega_multiplier;
2581 let spread_mid_price: f64 = self
2582 .mid_prices
2583 .iter()
2584 .zip(self.ratios.iter())
2585 .map(|(m, r)| m * (*r as f64))
2586 .sum();
2587 let raw_bid = spread_mid_price - bid_ask_spread * 0.5;
2588 let raw_ask = spread_mid_price + bid_ask_spread * 0.5;
2589 (raw_bid, raw_ask)
2590 }
2591
2592 fn clear_vega_pricing_timeout(&mut self) {
2593 self.vega_pricing_temporarily_disabled = false;
2594 }
2595
2596 fn start_vega_pricing_timeout(&mut self) {
2597 self.vega_pricing_temporarily_disabled = true;
2598
2599 if self
2600 .clock
2601 .borrow()
2602 .timer_names()
2603 .contains(&self.vega_pricing_timeout_timer_name.as_str())
2604 {
2605 return;
2606 }
2607
2608 let Some(aggregator_weak) = self.aggregator_weak.clone() else {
2609 return;
2610 };
2611 let callback = TimeEventCallback::RustLocal(Rc::new(move |_event: TimeEvent| {
2612 if let Some(agg) = aggregator_weak.upgrade() {
2613 agg.borrow_mut().clear_vega_pricing_timeout();
2614 }
2615 }));
2616 let alert_time =
2617 self.clock.borrow().timestamp_ns() + self.vega_pricing_timeout_seconds * 1_000_000_000;
2618
2619 self.clock
2620 .borrow_mut()
2621 .set_time_alert_ns(
2622 &self.vega_pricing_timeout_timer_name,
2623 alert_time,
2624 Some(callback),
2625 Some(true),
2626 )
2627 .expect("Failed to set spread quote vega pricing timeout");
2628 }
2629
2630 fn create_futures_spread_prices(&self) -> (f64, f64) {
2631 let mut raw_ask = 0.0_f64;
2632 let mut raw_bid = 0.0_f64;
2633
2634 for i in 0..self.n_legs {
2635 let r = self.ratios[i] as f64;
2636 if self.ratios[i] >= 0 {
2637 raw_ask += r * self.ask_prices[i];
2638 raw_bid += r * self.bid_prices[i];
2639 } else {
2640 raw_ask += r * self.bid_prices[i];
2641 raw_bid += r * self.ask_prices[i];
2642 }
2643 }
2644 (raw_bid, raw_ask)
2645 }
2646
2647 fn create_quote_tick_from_raw_prices(
2648 &self,
2649 raw_bid_price: f64,
2650 raw_ask_price: f64,
2651 ts_event: UnixNanos,
2652 ) -> QuoteTick {
2653 let (bid_price, ask_price) = if let Some(ref rounder) = self.price_rounder {
2654 rounder.round_prices(raw_bid_price, raw_ask_price, self.price_precision)
2655 } else {
2656 let bid = price_from_f64(raw_bid_price, self.price_precision);
2657 let ask = price_from_f64(raw_ask_price, self.price_precision);
2658 (bid, ask)
2659 };
2660 let mut min_bid_size = f64::INFINITY;
2661 let mut min_ask_size = f64::INFINITY;
2662 for i in 0..self.n_legs {
2663 let abs_ratio = self.ratios[i].unsigned_abs() as f64;
2664 if self.ratios[i] >= 0 {
2665 let b = self.bid_sizes[i] / abs_ratio;
2666 if b < min_bid_size {
2667 min_bid_size = b;
2668 }
2669 let a = self.ask_sizes[i] / abs_ratio;
2670 if a < min_ask_size {
2671 min_ask_size = a;
2672 }
2673 } else {
2674 let b = self.ask_sizes[i] / abs_ratio;
2675 if b < min_bid_size {
2676 min_bid_size = b;
2677 }
2678 let a = self.bid_sizes[i] / abs_ratio;
2679 if a < min_ask_size {
2680 min_ask_size = a;
2681 }
2682 }
2683 }
2684 let bid_size = Quantity::new(min_bid_size, self.size_precision);
2685 let ask_size = Quantity::new(min_ask_size, self.size_precision);
2686 QuoteTick::new(
2687 self.spread_instrument_id,
2688 bid_price,
2689 ask_price,
2690 bid_size,
2691 ask_size,
2692 ts_event,
2693 ts_event,
2694 )
2695 }
2696}
2697
2698fn price_from_f64(v: f64, precision: u8) -> Price {
2699 Price::new(v, precision)
2700}
2701
2702#[cfg(test)]
2703mod tests {
2704 use std::sync::{Arc, Mutex};
2705
2706 use nautilus_common::{clock::TestClock, timer::TimeEvent};
2707 use nautilus_core::{MUTEX_POISONED, UUID4, UnixNanos};
2708 use nautilus_model::{
2709 data::{BarSpecification, BarType, QuoteTick},
2710 enums::{AggregationSource, AggressorSide, BarAggregation, PriceType},
2711 identifiers::InstrumentId,
2712 instruments::{CurrencyPair, Equity, Instrument, InstrumentAny, stubs::*},
2713 types::{Price, Quantity},
2714 };
2715 use rstest::rstest;
2716 use ustr::Ustr;
2717
2718 use super::*;
2719
2720 #[rstest]
2721 fn test_bar_builder_initialization(equity_aapl: Equity) {
2722 let instrument = InstrumentAny::Equity(equity_aapl);
2723 let bar_type = BarType::new(
2724 instrument.id(),
2725 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2726 AggregationSource::Internal,
2727 );
2728 let builder = BarBuilder::new(
2729 bar_type,
2730 instrument.price_precision(),
2731 instrument.size_precision(),
2732 );
2733
2734 assert!(!builder.initialized);
2735 assert_eq!(builder.ts_last, 0);
2736 assert_eq!(builder.count, 0);
2737 }
2738
2739 #[rstest]
2740 fn test_bar_builder_maintains_ohlc_order(equity_aapl: Equity) {
2741 let instrument = InstrumentAny::Equity(equity_aapl);
2742 let bar_type = BarType::new(
2743 instrument.id(),
2744 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2745 AggregationSource::Internal,
2746 );
2747 let mut builder = BarBuilder::new(
2748 bar_type,
2749 instrument.price_precision(),
2750 instrument.size_precision(),
2751 );
2752
2753 builder.update(
2754 Price::from("100.00"),
2755 Quantity::from(1),
2756 UnixNanos::from(1000),
2757 );
2758 builder.update(
2759 Price::from("95.00"),
2760 Quantity::from(1),
2761 UnixNanos::from(2000),
2762 );
2763 builder.update(
2764 Price::from("105.00"),
2765 Quantity::from(1),
2766 UnixNanos::from(3000),
2767 );
2768
2769 let bar = builder.build_now();
2770 assert!(bar.high > bar.low);
2771 assert_eq!(bar.open, Price::from("100.00"));
2772 assert_eq!(bar.high, Price::from("105.00"));
2773 assert_eq!(bar.low, Price::from("95.00"));
2774 assert_eq!(bar.close, Price::from("105.00"));
2775 }
2776
2777 #[rstest]
2778 fn test_update_ignores_earlier_timestamps(equity_aapl: Equity) {
2779 let instrument = InstrumentAny::Equity(equity_aapl);
2780 let bar_type = BarType::new(
2781 instrument.id(),
2782 BarSpecification::new(100, BarAggregation::Tick, PriceType::Last),
2783 AggregationSource::Internal,
2784 );
2785 let mut builder = BarBuilder::new(
2786 bar_type,
2787 instrument.price_precision(),
2788 instrument.size_precision(),
2789 );
2790
2791 builder.update(Price::from("1.00000"), Quantity::from(1), 1_000.into());
2792 builder.update(Price::from("1.00001"), Quantity::from(1), 500.into());
2793
2794 assert_eq!(builder.ts_last, 1_000);
2795 assert_eq!(builder.count, 1);
2796 }
2797
2798 #[rstest]
2799 fn test_bar_builder_single_update_results_in_expected_properties(equity_aapl: Equity) {
2800 let instrument = InstrumentAny::Equity(equity_aapl);
2801 let bar_type = BarType::new(
2802 instrument.id(),
2803 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2804 AggregationSource::Internal,
2805 );
2806 let mut builder = BarBuilder::new(
2807 bar_type,
2808 instrument.price_precision(),
2809 instrument.size_precision(),
2810 );
2811
2812 builder.update(
2813 Price::from("1.00000"),
2814 Quantity::from(1),
2815 UnixNanos::default(),
2816 );
2817
2818 assert!(builder.initialized);
2819 assert_eq!(builder.ts_last, 0);
2820 assert_eq!(builder.count, 1);
2821 }
2822
2823 #[rstest]
2824 fn test_bar_builder_single_update_when_timestamp_less_than_last_update_ignores(
2825 equity_aapl: Equity,
2826 ) {
2827 let instrument = InstrumentAny::Equity(equity_aapl);
2828 let bar_type = BarType::new(
2829 instrument.id(),
2830 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2831 AggregationSource::Internal,
2832 );
2833 let mut builder = BarBuilder::new(bar_type, 2, 0);
2834
2835 builder.update(
2836 Price::from("1.00000"),
2837 Quantity::from(1),
2838 UnixNanos::from(1_000),
2839 );
2840 builder.update(
2841 Price::from("1.00001"),
2842 Quantity::from(1),
2843 UnixNanos::from(500),
2844 );
2845
2846 assert!(builder.initialized);
2847 assert_eq!(builder.ts_last, 1_000);
2848 assert_eq!(builder.count, 1);
2849 }
2850
2851 #[rstest]
2852 fn test_bar_builder_multiple_updates_correctly_increments_count(equity_aapl: Equity) {
2853 let instrument = InstrumentAny::Equity(equity_aapl);
2854 let bar_type = BarType::new(
2855 instrument.id(),
2856 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2857 AggregationSource::Internal,
2858 );
2859 let mut builder = BarBuilder::new(
2860 bar_type,
2861 instrument.price_precision(),
2862 instrument.size_precision(),
2863 );
2864
2865 for _ in 0..5 {
2866 builder.update(
2867 Price::from("1.00000"),
2868 Quantity::from(1),
2869 UnixNanos::from(1_000),
2870 );
2871 }
2872
2873 assert_eq!(builder.count, 5);
2874 }
2875
2876 #[rstest]
2877 #[should_panic]
2878 fn test_bar_builder_build_when_no_updates_panics(equity_aapl: Equity) {
2879 let instrument = InstrumentAny::Equity(equity_aapl);
2880 let bar_type = BarType::new(
2881 instrument.id(),
2882 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2883 AggregationSource::Internal,
2884 );
2885 let mut builder = BarBuilder::new(
2886 bar_type,
2887 instrument.price_precision(),
2888 instrument.size_precision(),
2889 );
2890 let _ = builder.build_now();
2891 }
2892
2893 #[rstest]
2894 fn test_bar_builder_build_when_received_updates_returns_expected_bar(equity_aapl: Equity) {
2895 let instrument = InstrumentAny::Equity(equity_aapl);
2896 let bar_type = BarType::new(
2897 instrument.id(),
2898 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2899 AggregationSource::Internal,
2900 );
2901 let mut builder = BarBuilder::new(
2902 bar_type,
2903 instrument.price_precision(),
2904 instrument.size_precision(),
2905 );
2906
2907 builder.update(
2908 Price::from("1.00001"),
2909 Quantity::from(2),
2910 UnixNanos::default(),
2911 );
2912 builder.update(
2913 Price::from("1.00002"),
2914 Quantity::from(2),
2915 UnixNanos::default(),
2916 );
2917 builder.update(
2918 Price::from("1.00000"),
2919 Quantity::from(1),
2920 UnixNanos::from(1_000_000_000),
2921 );
2922
2923 let bar = builder.build_now();
2924
2925 assert_eq!(bar.open, Price::from("1.00001"));
2926 assert_eq!(bar.high, Price::from("1.00002"));
2927 assert_eq!(bar.low, Price::from("1.00000"));
2928 assert_eq!(bar.close, Price::from("1.00000"));
2929 assert_eq!(bar.volume, Quantity::from(5));
2930 assert_eq!(bar.ts_init, 1_000_000_000);
2931 assert_eq!(builder.ts_last, 1_000_000_000);
2932 assert_eq!(builder.count, 0);
2933 }
2934
2935 #[rstest]
2936 fn test_bar_builder_build_with_previous_close(equity_aapl: Equity) {
2937 let instrument = InstrumentAny::Equity(equity_aapl);
2938 let bar_type = BarType::new(
2939 instrument.id(),
2940 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2941 AggregationSource::Internal,
2942 );
2943 let mut builder = BarBuilder::new(bar_type, 2, 0);
2944
2945 builder.update(
2946 Price::from("1.00001"),
2947 Quantity::from(1),
2948 UnixNanos::default(),
2949 );
2950 builder.build_now();
2951
2952 builder.update(
2953 Price::from("1.00000"),
2954 Quantity::from(1),
2955 UnixNanos::default(),
2956 );
2957 builder.update(
2958 Price::from("1.00003"),
2959 Quantity::from(1),
2960 UnixNanos::default(),
2961 );
2962 builder.update(
2963 Price::from("1.00002"),
2964 Quantity::from(1),
2965 UnixNanos::default(),
2966 );
2967
2968 let bar = builder.build_now();
2969
2970 assert_eq!(bar.open, Price::from("1.00000"));
2971 assert_eq!(bar.high, Price::from("1.00003"));
2972 assert_eq!(bar.low, Price::from("1.00000"));
2973 assert_eq!(bar.close, Price::from("1.00002"));
2974 assert_eq!(bar.volume, Quantity::from(3));
2975 }
2976
2977 #[rstest]
2978 fn test_bar_builder_update_bar_initializes_then_accumulates(equity_aapl: Equity) {
2979 let instrument = InstrumentAny::Equity(equity_aapl);
2980 let bar_type = BarType::new(
2981 instrument.id(),
2982 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
2983 AggregationSource::Internal,
2984 );
2985 let mut builder = BarBuilder::new(
2986 bar_type,
2987 instrument.price_precision(),
2988 instrument.size_precision(),
2989 );
2990
2991 let bar_one = Bar::new(
2992 bar_type,
2993 Price::from("100.00"),
2994 Price::from("102.00"),
2995 Price::from("99.00"),
2996 Price::from("101.00"),
2997 Quantity::from(10),
2998 UnixNanos::from(1_000),
2999 UnixNanos::from(1_000),
3000 );
3001 let bar_two = Bar::new(
3002 bar_type,
3003 Price::from("101.00"),
3004 Price::from("103.00"),
3005 Price::from("98.00"),
3006 Price::from("102.00"),
3007 Quantity::from(5),
3008 UnixNanos::from(2_000),
3009 UnixNanos::from(2_000),
3010 );
3011
3012 builder.update_bar(bar_one, bar_one.volume, bar_one.ts_init);
3013 builder.update_bar(bar_two, bar_two.volume, bar_two.ts_init);
3014 let bar = builder.build_now();
3015
3016 assert_eq!(bar.open, Price::from("100.00"));
3017 assert_eq!(bar.high, Price::from("103.00"));
3018 assert_eq!(bar.low, Price::from("98.00"));
3019 assert_eq!(bar.close, Price::from("102.00"));
3020 assert_eq!(bar.volume, Quantity::from(15));
3021 assert_eq!(builder.count, 0);
3022 }
3023
3024 #[rstest]
3025 fn test_bar_builder_update_bar_ignores_earlier_timestamp(equity_aapl: Equity) {
3026 let instrument = InstrumentAny::Equity(equity_aapl);
3027 let bar_type = BarType::new(
3028 instrument.id(),
3029 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3030 AggregationSource::Internal,
3031 );
3032 let mut builder = BarBuilder::new(
3033 bar_type,
3034 instrument.price_precision(),
3035 instrument.size_precision(),
3036 );
3037
3038 let bar_later = Bar::new(
3039 bar_type,
3040 Price::from("100.00"),
3041 Price::from("101.00"),
3042 Price::from("99.00"),
3043 Price::from("100.50"),
3044 Quantity::from(10),
3045 UnixNanos::from(2_000),
3046 UnixNanos::from(2_000),
3047 );
3048 let bar_earlier = Bar::new(
3049 bar_type,
3050 Price::from("200.00"),
3051 Price::from("210.00"),
3052 Price::from("190.00"),
3053 Price::from("205.00"),
3054 Quantity::from(50),
3055 UnixNanos::from(1_000),
3056 UnixNanos::from(1_000),
3057 );
3058
3059 builder.update_bar(bar_later, bar_later.volume, bar_later.ts_init);
3060 builder.update_bar(bar_earlier, bar_earlier.volume, bar_earlier.ts_init);
3061
3062 assert_eq!(builder.ts_last, 2_000);
3063 assert_eq!(builder.count, 1);
3064 assert_eq!(builder.volume, Quantity::from(10));
3065 }
3066
3067 #[rstest]
3068 #[case::spread_zero_inactive(
3069 Decimal::ZERO,
3070 ContinuousFutureAdjustmentType::BackwardSpread,
3071 false
3072 )]
3073 #[case::spread_positive_active(
3074 Decimal::new(150, 2), ContinuousFutureAdjustmentType::BackwardSpread,
3076 true,
3077 )]
3078 #[case::spread_negative_active(
3079 Decimal::new(-250, 2), ContinuousFutureAdjustmentType::ForwardSpread,
3081 true,
3082 )]
3083 #[case::spread_sub_precision_inactive(
3084 Decimal::new(1, 28),
3086 ContinuousFutureAdjustmentType::BackwardSpread,
3087 false,
3088 )]
3089 #[case::ratio_one_inactive(Decimal::ONE, ContinuousFutureAdjustmentType::BackwardRatio, false)]
3090 #[case::ratio_non_one_active(
3091 Decimal::new(105, 2), ContinuousFutureAdjustmentType::ForwardRatio,
3093 true,
3094 )]
3095 fn test_bar_builder_set_adjustment_active_flag(
3096 equity_aapl: Equity,
3097 #[case] adjustment: Decimal,
3098 #[case] mode: ContinuousFutureAdjustmentType,
3099 #[case] expected_active: bool,
3100 ) {
3101 let instrument = InstrumentAny::Equity(equity_aapl);
3102 let bar_type = BarType::new(
3103 instrument.id(),
3104 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3105 AggregationSource::Internal,
3106 );
3107 let mut builder = BarBuilder::new(bar_type, 2, 0);
3108
3109 builder.set_adjustment(adjustment, mode);
3110
3111 assert_eq!(builder.adjustment_active, expected_active);
3112 assert_eq!(builder.adjustment_is_ratio, mode.is_ratio());
3113 assert_eq!(builder.adjustment_mode, mode);
3114 }
3115
3116 #[rstest]
3117 fn test_bar_builder_set_adjustment_mode_switch_resets_flags(equity_aapl: Equity) {
3118 let instrument = InstrumentAny::Equity(equity_aapl);
3119 let bar_type = BarType::new(
3120 instrument.id(),
3121 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3122 AggregationSource::Internal,
3123 );
3124 let mut builder = BarBuilder::new(bar_type, 2, 0);
3125
3126 builder.set_adjustment(
3128 Decimal::new(150, 2), ContinuousFutureAdjustmentType::BackwardRatio,
3130 );
3131 builder.set_adjustment(
3132 Decimal::new(50, 2), ContinuousFutureAdjustmentType::BackwardSpread,
3134 );
3135 assert!(!builder.adjustment_is_ratio);
3136 builder.update(Price::from("100.00"), Quantity::from(1), 1_000.into());
3137 assert_eq!(builder.build_now().close, Price::from("100.50"));
3138
3139 builder.set_adjustment(
3141 Decimal::new(11, 1), ContinuousFutureAdjustmentType::ForwardRatio,
3143 );
3144 assert!(builder.adjustment_is_ratio);
3145 builder.update(Price::from("100.00"), Quantity::from(1), 2_000.into());
3146 assert_eq!(builder.build_now().close, Price::from("110.00"));
3147 }
3148
3149 #[rstest]
3150 fn test_bar_builder_update_applies_backward_spread_adjustment(equity_aapl: Equity) {
3151 let instrument = InstrumentAny::Equity(equity_aapl);
3152 let bar_type = BarType::new(
3153 instrument.id(),
3154 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3155 AggregationSource::Internal,
3156 );
3157 let mut builder = BarBuilder::new(bar_type, 2, 0);
3158
3159 builder.set_adjustment(
3160 Decimal::new(250, 2), ContinuousFutureAdjustmentType::BackwardSpread,
3162 );
3163
3164 builder.update(Price::from("100.00"), Quantity::from(1), 1_000.into());
3165 builder.update(Price::from("99.00"), Quantity::from(1), 2_000.into());
3166 builder.update(Price::from("101.00"), Quantity::from(1), 3_000.into());
3167
3168 let bar = builder.build_now();
3169 assert_eq!(bar.open, Price::from("102.50"));
3170 assert_eq!(bar.high, Price::from("103.50"));
3171 assert_eq!(bar.low, Price::from("101.50"));
3172 assert_eq!(bar.close, Price::from("103.50"));
3173 }
3174
3175 #[rstest]
3176 fn test_bar_builder_update_applies_forward_ratio_adjustment(equity_aapl: Equity) {
3177 let instrument = InstrumentAny::Equity(equity_aapl);
3178 let bar_type = BarType::new(
3179 instrument.id(),
3180 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3181 AggregationSource::Internal,
3182 );
3183 let mut builder = BarBuilder::new(bar_type, 2, 0);
3184
3185 builder.set_adjustment(
3186 Decimal::new(11, 1), ContinuousFutureAdjustmentType::ForwardRatio,
3188 );
3189
3190 builder.update(Price::from("100.00"), Quantity::from(1), 1_000.into());
3191 builder.update(Price::from("90.00"), Quantity::from(1), 2_000.into());
3192 builder.update(Price::from("110.00"), Quantity::from(1), 3_000.into());
3193
3194 let bar = builder.build_now();
3195 assert_eq!(bar.open, Price::from("110.00"));
3196 assert_eq!(bar.high, Price::from("121.00"));
3197 assert_eq!(bar.low, Price::from("99.00"));
3198 assert_eq!(bar.close, Price::from("121.00"));
3199 }
3200
3201 #[rstest]
3202 fn test_bar_builder_update_bar_applies_adjustment_to_ohlc(equity_aapl: Equity) {
3203 let instrument = InstrumentAny::Equity(equity_aapl);
3204 let bar_type = BarType::new(
3205 instrument.id(),
3206 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3207 AggregationSource::Internal,
3208 );
3209 let mut builder = BarBuilder::new(bar_type, 2, 0);
3210
3211 builder.set_adjustment(
3212 Decimal::new(-100, 2), ContinuousFutureAdjustmentType::BackwardSpread,
3214 );
3215
3216 let input = Bar::new(
3217 bar_type,
3218 Price::from("100.00"),
3219 Price::from("105.00"),
3220 Price::from("99.00"),
3221 Price::from("102.00"),
3222 Quantity::from(10),
3223 UnixNanos::from(1_000),
3224 UnixNanos::from(1_000),
3225 );
3226 builder.update_bar(input, input.volume, input.ts_init);
3227
3228 let bar = builder.build_now();
3229 assert_eq!(bar.open, Price::from("99.00"));
3230 assert_eq!(bar.high, Price::from("104.00"));
3231 assert_eq!(bar.low, Price::from("98.00"));
3232 assert_eq!(bar.close, Price::from("101.00"));
3233 }
3234
3235 #[rstest]
3236 fn test_bar_builder_reset_retains_adjustment(equity_aapl: Equity) {
3237 let instrument = InstrumentAny::Equity(equity_aapl);
3238 let bar_type = BarType::new(
3239 instrument.id(),
3240 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3241 AggregationSource::Internal,
3242 );
3243 let mut builder = BarBuilder::new(bar_type, 2, 0);
3244
3245 builder.set_adjustment(
3246 Decimal::new(500, 2), ContinuousFutureAdjustmentType::BackwardSpread,
3248 );
3249 builder.update(Price::from("100.00"), Quantity::from(1), 1_000.into());
3250 let bar_one = builder.build_now();
3251 assert_eq!(bar_one.close, Price::from("105.00"));
3252
3253 assert!(builder.adjustment_active);
3255
3256 builder.update(Price::from("110.00"), Quantity::from(1), 2_000.into());
3257 let bar_two = builder.build_now();
3258 assert_eq!(bar_two.close, Price::from("115.00"));
3259 }
3260
3261 #[rstest]
3262 fn test_bar_builder_update_bar_applies_ratio_adjustment(equity_aapl: Equity) {
3263 let instrument = InstrumentAny::Equity(equity_aapl);
3264 let bar_type = BarType::new(
3265 instrument.id(),
3266 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3267 AggregationSource::Internal,
3268 );
3269 let mut builder = BarBuilder::new(bar_type, 2, 0);
3270
3271 builder.set_adjustment(
3272 Decimal::new(11, 1), ContinuousFutureAdjustmentType::ForwardRatio,
3274 );
3275
3276 let input = Bar::new(
3277 bar_type,
3278 Price::from("100.00"),
3279 Price::from("110.00"),
3280 Price::from("90.00"),
3281 Price::from("105.00"),
3282 Quantity::from(10),
3283 UnixNanos::from(1_000),
3284 UnixNanos::from(1_000),
3285 );
3286 builder.update_bar(input, input.volume, input.ts_init);
3287
3288 let bar = builder.build_now();
3289 assert_eq!(bar.open, Price::from("110.00"));
3290 assert_eq!(bar.high, Price::from("121.00"));
3291 assert_eq!(bar.low, Price::from("99.00"));
3292 assert_eq!(bar.close, Price::from("115.50"));
3293 }
3294
3295 #[rstest]
3296 fn test_bar_builder_spread_below_zero_representable(equity_aapl: Equity) {
3297 let instrument = InstrumentAny::Equity(equity_aapl);
3300 let bar_type = BarType::new(
3301 instrument.id(),
3302 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3303 AggregationSource::Internal,
3304 );
3305 let mut builder = BarBuilder::new(bar_type, 2, 0);
3306
3307 builder.set_adjustment(
3308 Decimal::new(-15000, 2), ContinuousFutureAdjustmentType::BackwardSpread,
3310 );
3311
3312 builder.update(Price::from("100.00"), Quantity::from(1), 1_000.into());
3313 let bar = builder.build_now();
3314 assert_eq!(bar.close, Price::from("-50.00"));
3315 assert!(bar.close.raw < 0);
3316 assert_eq!(bar.close.precision, 2);
3317 }
3318
3319 #[rstest]
3320 fn test_bar_builder_build_promotes_close_above_high_from_previous_close(equity_aapl: Equity) {
3321 let instrument = InstrumentAny::Equity(equity_aapl);
3322 let bar_type = BarType::new(
3323 instrument.id(),
3324 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3325 AggregationSource::Internal,
3326 );
3327 let mut builder = BarBuilder::new(bar_type, 2, 0);
3328
3329 builder.update(
3330 Price::from("110.00"),
3331 Quantity::from(1),
3332 UnixNanos::from(100),
3333 );
3334 builder.build_now();
3335
3336 builder.update(
3337 Price::from("100.00"),
3338 Quantity::from(1),
3339 UnixNanos::from(200),
3340 );
3341 builder.update(
3342 Price::from("101.00"),
3343 Quantity::from(1),
3344 UnixNanos::from(300),
3345 );
3346 builder.update(
3347 Price::from("200.00"),
3348 Quantity::from(1),
3349 UnixNanos::from(400),
3350 );
3351
3352 let bar = builder.build_now();
3353 assert_eq!(bar.open, Price::from("100.00"));
3354 assert_eq!(bar.high, Price::from("200.00"));
3355 assert_eq!(bar.low, Price::from("100.00"));
3356 assert_eq!(bar.close, Price::from("200.00"));
3357 }
3358
3359 #[rstest]
3360 fn test_bar_builder_build_clamps_low_to_close(equity_aapl: Equity) {
3361 let instrument = InstrumentAny::Equity(equity_aapl);
3365 let bar_type = BarType::new(
3366 instrument.id(),
3367 BarSpecification::new(3, BarAggregation::Tick, PriceType::Last),
3368 AggregationSource::Internal,
3369 );
3370 let mut builder = BarBuilder::new(bar_type, 2, 0);
3371
3372 builder.update(
3373 Price::from("100.00"),
3374 Quantity::from(1),
3375 UnixNanos::from(100),
3376 );
3377 builder.close = Some(Price::from("50.00"));
3378
3379 let bar = builder.build_now();
3380 assert_eq!(bar.low, Price::from("50.00"));
3381 assert_eq!(bar.close, Price::from("50.00"));
3382 assert!(bar.low <= bar.open);
3383 }
3384
3385 #[rstest]
3386 fn test_tick_bar_aggregator_handle_trade_when_step_count_below_threshold(equity_aapl: Equity) {
3387 let instrument = InstrumentAny::Equity(equity_aapl);
3388 let bar_spec = BarSpecification::new(3, BarAggregation::Tick, PriceType::Last);
3389 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3390 let handler = Arc::new(Mutex::new(Vec::new()));
3391 let handler_clone = Arc::clone(&handler);
3392
3393 let mut aggregator = TickBarAggregator::new(
3394 bar_type,
3395 instrument.price_precision(),
3396 instrument.size_precision(),
3397 move |bar: Bar| {
3398 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
3399 handler_guard.push(bar);
3400 },
3401 );
3402
3403 let trade = TradeTick::default();
3404 aggregator.handle_trade(trade);
3405
3406 let handler_guard = handler.lock().expect(MUTEX_POISONED);
3407 assert_eq!(handler_guard.len(), 0);
3408 }
3409
3410 #[rstest]
3411 fn test_tick_bar_aggregator_handle_trade_when_step_count_reached(equity_aapl: Equity) {
3412 let instrument = InstrumentAny::Equity(equity_aapl);
3413 let bar_spec = BarSpecification::new(3, BarAggregation::Tick, PriceType::Last);
3414 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3415 let handler = Arc::new(Mutex::new(Vec::new()));
3416 let handler_clone = Arc::clone(&handler);
3417
3418 let mut aggregator = TickBarAggregator::new(
3419 bar_type,
3420 instrument.price_precision(),
3421 instrument.size_precision(),
3422 move |bar: Bar| {
3423 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
3424 handler_guard.push(bar);
3425 },
3426 );
3427
3428 let trade = TradeTick::default();
3429 aggregator.handle_trade(trade);
3430 aggregator.handle_trade(trade);
3431 aggregator.handle_trade(trade);
3432
3433 let handler_guard = handler.lock().expect(MUTEX_POISONED);
3434 let bar = handler_guard.first().unwrap();
3435 assert_eq!(handler_guard.len(), 1);
3436 assert_eq!(bar.open, trade.price);
3437 assert_eq!(bar.high, trade.price);
3438 assert_eq!(bar.low, trade.price);
3439 assert_eq!(bar.close, trade.price);
3440 assert_eq!(bar.volume, Quantity::from(300000));
3441 assert_eq!(bar.ts_event, trade.ts_event);
3442 assert_eq!(bar.ts_init, trade.ts_init);
3443 }
3444
3445 #[rstest]
3446 fn test_tick_bar_aggregator_aggregates_to_step_size(equity_aapl: Equity) {
3447 let instrument = InstrumentAny::Equity(equity_aapl);
3448 let bar_spec = BarSpecification::new(3, BarAggregation::Tick, PriceType::Last);
3449 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3450 let handler = Arc::new(Mutex::new(Vec::new()));
3451 let handler_clone = Arc::clone(&handler);
3452
3453 let mut aggregator = TickBarAggregator::new(
3454 bar_type,
3455 instrument.price_precision(),
3456 instrument.size_precision(),
3457 move |bar: Bar| {
3458 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
3459 handler_guard.push(bar);
3460 },
3461 );
3462
3463 aggregator.update(
3464 Price::from("1.00001"),
3465 Quantity::from(1),
3466 UnixNanos::default(),
3467 );
3468 aggregator.update(
3469 Price::from("1.00002"),
3470 Quantity::from(1),
3471 UnixNanos::from(1000),
3472 );
3473 aggregator.update(
3474 Price::from("1.00003"),
3475 Quantity::from(1),
3476 UnixNanos::from(2000),
3477 );
3478
3479 let handler_guard = handler.lock().expect(MUTEX_POISONED);
3480 assert_eq!(handler_guard.len(), 1);
3481
3482 let bar = handler_guard.first().unwrap();
3483 assert_eq!(bar.open, Price::from("1.00001"));
3484 assert_eq!(bar.high, Price::from("1.00003"));
3485 assert_eq!(bar.low, Price::from("1.00001"));
3486 assert_eq!(bar.close, Price::from("1.00003"));
3487 assert_eq!(bar.volume, Quantity::from(3));
3488 }
3489
3490 #[rstest]
3491 fn test_tick_bar_aggregator_resets_after_bar_created(equity_aapl: Equity) {
3492 let instrument = InstrumentAny::Equity(equity_aapl);
3493 let bar_spec = BarSpecification::new(2, BarAggregation::Tick, PriceType::Last);
3494 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3495 let handler = Arc::new(Mutex::new(Vec::new()));
3496 let handler_clone = Arc::clone(&handler);
3497
3498 let mut aggregator = TickBarAggregator::new(
3499 bar_type,
3500 instrument.price_precision(),
3501 instrument.size_precision(),
3502 move |bar: Bar| {
3503 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
3504 handler_guard.push(bar);
3505 },
3506 );
3507
3508 aggregator.update(
3509 Price::from("1.00001"),
3510 Quantity::from(1),
3511 UnixNanos::default(),
3512 );
3513 aggregator.update(
3514 Price::from("1.00002"),
3515 Quantity::from(1),
3516 UnixNanos::from(1000),
3517 );
3518 aggregator.update(
3519 Price::from("1.00003"),
3520 Quantity::from(1),
3521 UnixNanos::from(2000),
3522 );
3523 aggregator.update(
3524 Price::from("1.00004"),
3525 Quantity::from(1),
3526 UnixNanos::from(3000),
3527 );
3528
3529 let handler_guard = handler.lock().expect(MUTEX_POISONED);
3530 assert_eq!(handler_guard.len(), 2);
3531
3532 let bar1 = &handler_guard[0];
3533 assert_eq!(bar1.open, Price::from("1.00001"));
3534 assert_eq!(bar1.close, Price::from("1.00002"));
3535 assert_eq!(bar1.volume, Quantity::from(2));
3536
3537 let bar2 = &handler_guard[1];
3538 assert_eq!(bar2.open, Price::from("1.00003"));
3539 assert_eq!(bar2.close, Price::from("1.00004"));
3540 assert_eq!(bar2.volume, Quantity::from(2));
3541 }
3542
3543 #[rstest]
3544 fn test_non_time_bar_aggregators_use_historical_handler(
3545 equity_aapl: Equity,
3546 audusd_sim: CurrencyPair,
3547 ) {
3548 let instrument = InstrumentAny::Equity(equity_aapl);
3549 let instrument_id = instrument.id();
3550 let price_precision = instrument.price_precision();
3551 let size_precision = instrument.size_precision();
3552 let make_sink = |bars: Arc<Mutex<Vec<Bar>>>| {
3553 move |bar: Bar| {
3554 bars.lock().expect(MUTEX_POISONED).push(bar);
3555 }
3556 };
3557 let make_trade = |price: &str, size: i64, ts: u64| TradeTick {
3558 instrument_id,
3559 price: Price::from(price),
3560 size: Quantity::from(size),
3561 aggressor_side: AggressorSide::Buyer,
3562 ts_event: UnixNanos::from(ts),
3563 ts_init: UnixNanos::from(ts),
3564 ..TradeTick::default()
3565 };
3566
3567 macro_rules! assert_historical_sink_receives {
3568 ($name:expr, $aggregator:expr, $update:expr) => {{
3569 let initial_bars = Arc::new(Mutex::new(Vec::new()));
3570 let historical_bars = Arc::new(Mutex::new(Vec::new()));
3571 let mut aggregator = $aggregator(Arc::clone(&initial_bars));
3572 aggregator
3573 .set_historical_mode(true, Box::new(make_sink(Arc::clone(&historical_bars))));
3574 {
3575 let aggregator: &mut dyn BarAggregator = &mut aggregator;
3576 $update(aggregator);
3577 }
3578
3579 assert_eq!(
3580 initial_bars.lock().expect(MUTEX_POISONED).len(),
3581 0,
3582 "{}",
3583 $name,
3584 );
3585 assert_eq!(
3586 historical_bars.lock().expect(MUTEX_POISONED).len(),
3587 1,
3588 "{}",
3589 $name,
3590 );
3591 }};
3592 }
3593
3594 let tick_type = BarType::new(
3595 instrument_id,
3596 BarSpecification::new(1, BarAggregation::Tick, PriceType::Last),
3597 AggregationSource::Internal,
3598 );
3599 assert_historical_sink_receives!(
3600 "TickBarAggregator",
3601 |bars| TickBarAggregator::new(
3602 tick_type,
3603 price_precision,
3604 size_precision,
3605 make_sink(bars)
3606 ),
3607 |aggregator: &mut dyn BarAggregator| {
3608 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3609 }
3610 );
3611
3612 let tick_imbalance_type = BarType::new(
3613 instrument_id,
3614 BarSpecification::new(1, BarAggregation::TickImbalance, PriceType::Last),
3615 AggregationSource::Internal,
3616 );
3617 assert_historical_sink_receives!(
3618 "TickImbalanceBarAggregator",
3619 |bars| TickImbalanceBarAggregator::new(
3620 tick_imbalance_type,
3621 price_precision,
3622 size_precision,
3623 make_sink(bars),
3624 ),
3625 |aggregator: &mut dyn BarAggregator| {
3626 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3627 }
3628 );
3629
3630 let tick_runs_type = BarType::new(
3631 instrument_id,
3632 BarSpecification::new(1, BarAggregation::TickRuns, PriceType::Last),
3633 AggregationSource::Internal,
3634 );
3635 assert_historical_sink_receives!(
3636 "TickRunsBarAggregator",
3637 |bars| TickRunsBarAggregator::new(
3638 tick_runs_type,
3639 price_precision,
3640 size_precision,
3641 make_sink(bars),
3642 ),
3643 |aggregator: &mut dyn BarAggregator| {
3644 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3645 }
3646 );
3647
3648 let volume_type = BarType::new(
3649 instrument_id,
3650 BarSpecification::new(1, BarAggregation::Volume, PriceType::Last),
3651 AggregationSource::Internal,
3652 );
3653 assert_historical_sink_receives!(
3654 "VolumeBarAggregator",
3655 |bars| VolumeBarAggregator::new(
3656 volume_type,
3657 price_precision,
3658 size_precision,
3659 make_sink(bars),
3660 ),
3661 |aggregator: &mut dyn BarAggregator| {
3662 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3663 }
3664 );
3665
3666 let volume_imbalance_type = BarType::new(
3667 instrument_id,
3668 BarSpecification::new(1, BarAggregation::VolumeImbalance, PriceType::Last),
3669 AggregationSource::Internal,
3670 );
3671 assert_historical_sink_receives!(
3672 "VolumeImbalanceBarAggregator",
3673 |bars| VolumeImbalanceBarAggregator::new(
3674 volume_imbalance_type,
3675 price_precision,
3676 size_precision,
3677 make_sink(bars),
3678 ),
3679 |aggregator: &mut dyn BarAggregator| {
3680 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3681 }
3682 );
3683
3684 let volume_runs_type = BarType::new(
3685 instrument_id,
3686 BarSpecification::new(1, BarAggregation::VolumeRuns, PriceType::Last),
3687 AggregationSource::Internal,
3688 );
3689 assert_historical_sink_receives!(
3690 "VolumeRunsBarAggregator",
3691 |bars| VolumeRunsBarAggregator::new(
3692 volume_runs_type,
3693 price_precision,
3694 size_precision,
3695 make_sink(bars),
3696 ),
3697 |aggregator: &mut dyn BarAggregator| {
3698 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3699 }
3700 );
3701
3702 let value_type = BarType::new(
3703 instrument_id,
3704 BarSpecification::new(100, BarAggregation::Value, PriceType::Last),
3705 AggregationSource::Internal,
3706 );
3707 assert_historical_sink_receives!(
3708 "ValueBarAggregator",
3709 |bars| ValueBarAggregator::new(
3710 value_type,
3711 price_precision,
3712 size_precision,
3713 make_sink(bars)
3714 ),
3715 |aggregator: &mut dyn BarAggregator| {
3716 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3717 }
3718 );
3719
3720 let value_imbalance_type = BarType::new(
3721 instrument_id,
3722 BarSpecification::new(100, BarAggregation::ValueImbalance, PriceType::Last),
3723 AggregationSource::Internal,
3724 );
3725 assert_historical_sink_receives!(
3726 "ValueImbalanceBarAggregator",
3727 |bars| ValueImbalanceBarAggregator::new(
3728 value_imbalance_type,
3729 price_precision,
3730 size_precision,
3731 make_sink(bars),
3732 ),
3733 |aggregator: &mut dyn BarAggregator| {
3734 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3735 }
3736 );
3737
3738 let value_runs_type = BarType::new(
3739 instrument_id,
3740 BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last),
3741 AggregationSource::Internal,
3742 );
3743 assert_historical_sink_receives!(
3744 "ValueRunsBarAggregator",
3745 |bars| ValueRunsBarAggregator::new(
3746 value_runs_type,
3747 price_precision,
3748 size_precision,
3749 make_sink(bars),
3750 ),
3751 |aggregator: &mut dyn BarAggregator| {
3752 aggregator.handle_trade(make_trade("100.00", 1, 1_000));
3753 }
3754 );
3755
3756 let fx = InstrumentAny::CurrencyPair(audusd_sim);
3757 let renko_type = BarType::new(
3758 fx.id(),
3759 BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid),
3760 AggregationSource::Internal,
3761 );
3762 let fx_price_precision = fx.price_precision();
3763 let fx_size_precision = fx.size_precision();
3764 let fx_price_increment = fx.price_increment();
3765 assert_historical_sink_receives!(
3766 "RenkoBarAggregator",
3767 |bars| RenkoBarAggregator::new(
3768 renko_type,
3769 fx_price_precision,
3770 fx_size_precision,
3771 fx_price_increment,
3772 make_sink(bars),
3773 ),
3774 |aggregator: &mut dyn BarAggregator| {
3775 aggregator.update(
3776 Price::from("1.00000"),
3777 Quantity::from(1),
3778 UnixNanos::from(1_000),
3779 );
3780 aggregator.update(
3781 Price::from("1.00010"),
3782 Quantity::from(1),
3783 UnixNanos::from(2_000),
3784 );
3785 }
3786 );
3787 }
3788
3789 #[rstest]
3790 fn test_tick_imbalance_bar_aggregator_emits_at_threshold(equity_aapl: Equity) {
3791 let instrument = InstrumentAny::Equity(equity_aapl);
3792 let bar_spec = BarSpecification::new(2, BarAggregation::TickImbalance, PriceType::Last);
3793 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3794 let handler = Arc::new(Mutex::new(Vec::new()));
3795 let handler_clone = Arc::clone(&handler);
3796
3797 let mut aggregator = TickImbalanceBarAggregator::new(
3798 bar_type,
3799 instrument.price_precision(),
3800 instrument.size_precision(),
3801 move |bar: Bar| {
3802 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
3803 handler_guard.push(bar);
3804 },
3805 );
3806
3807 let trade = TradeTick::default();
3808 aggregator.handle_trade(trade);
3809 aggregator.handle_trade(trade);
3810
3811 let handler_guard = handler.lock().expect(MUTEX_POISONED);
3812 assert_eq!(handler_guard.len(), 1);
3813 let bar = handler_guard.first().unwrap();
3814 assert_eq!(bar.volume, Quantity::from(200000));
3815 }
3816
3817 #[rstest]
3818 fn test_tick_imbalance_bar_aggregator_handles_seller_direction(equity_aapl: Equity) {
3819 let instrument = InstrumentAny::Equity(equity_aapl);
3820 let bar_spec = BarSpecification::new(1, BarAggregation::TickImbalance, PriceType::Last);
3821 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3822 let handler = Arc::new(Mutex::new(Vec::new()));
3823 let handler_clone = Arc::clone(&handler);
3824
3825 let mut aggregator = TickImbalanceBarAggregator::new(
3826 bar_type,
3827 instrument.price_precision(),
3828 instrument.size_precision(),
3829 move |bar: Bar| {
3830 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
3831 handler_guard.push(bar);
3832 },
3833 );
3834
3835 let sell = TradeTick {
3836 aggressor_side: AggressorSide::Seller,
3837 ..TradeTick::default()
3838 };
3839
3840 aggregator.handle_trade(sell);
3841
3842 let handler_guard = handler.lock().expect(MUTEX_POISONED);
3843 assert_eq!(handler_guard.len(), 1);
3844 }
3845
3846 #[rstest]
3847 fn test_tick_runs_bar_aggregator_resets_on_side_change(equity_aapl: Equity) {
3848 let instrument = InstrumentAny::Equity(equity_aapl);
3849 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
3850 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3851 let handler = Arc::new(Mutex::new(Vec::new()));
3852 let handler_clone = Arc::clone(&handler);
3853
3854 let mut aggregator = TickRunsBarAggregator::new(
3855 bar_type,
3856 instrument.price_precision(),
3857 instrument.size_precision(),
3858 move |bar: Bar| {
3859 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
3860 handler_guard.push(bar);
3861 },
3862 );
3863
3864 let buy = TradeTick::default();
3865 let sell = TradeTick {
3866 aggressor_side: AggressorSide::Seller,
3867 ..buy
3868 };
3869
3870 aggregator.handle_trade(buy);
3871 aggregator.handle_trade(buy);
3872 aggregator.handle_trade(sell);
3873 aggregator.handle_trade(sell);
3874
3875 let handler_guard = handler.lock().expect(MUTEX_POISONED);
3876 assert_eq!(handler_guard.len(), 2);
3877 }
3878
3879 #[rstest]
3880 fn test_tick_runs_bar_aggregator_volume_conservation(equity_aapl: Equity) {
3881 let instrument = InstrumentAny::Equity(equity_aapl);
3882 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
3883 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3884 let handler = Arc::new(Mutex::new(Vec::new()));
3885 let handler_clone = Arc::clone(&handler);
3886
3887 let mut aggregator = TickRunsBarAggregator::new(
3888 bar_type,
3889 instrument.price_precision(),
3890 instrument.size_precision(),
3891 move |bar: Bar| {
3892 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
3893 handler_guard.push(bar);
3894 },
3895 );
3896
3897 let buy = TradeTick {
3898 size: Quantity::from(1),
3899 ..TradeTick::default()
3900 };
3901 let sell = TradeTick {
3902 aggressor_side: AggressorSide::Seller,
3903 size: Quantity::from(1),
3904 ..buy
3905 };
3906
3907 aggregator.handle_trade(buy);
3908 aggregator.handle_trade(buy);
3909 aggregator.handle_trade(sell);
3910 aggregator.handle_trade(sell);
3911
3912 let handler_guard = handler.lock().expect(MUTEX_POISONED);
3913 assert_eq!(handler_guard.len(), 2);
3914 assert_eq!(handler_guard[0].volume, Quantity::from(2));
3915 assert_eq!(handler_guard[1].volume, Quantity::from(2));
3916 }
3917
3918 #[rstest]
3919 fn test_volume_bar_aggregator_builds_multiple_bars_from_large_update(equity_aapl: Equity) {
3920 let instrument = InstrumentAny::Equity(equity_aapl);
3921 let bar_spec = BarSpecification::new(10, BarAggregation::Volume, PriceType::Last);
3922 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3923 let handler = Arc::new(Mutex::new(Vec::new()));
3924 let handler_clone = Arc::clone(&handler);
3925
3926 let mut aggregator = VolumeBarAggregator::new(
3927 bar_type,
3928 instrument.price_precision(),
3929 instrument.size_precision(),
3930 move |bar: Bar| {
3931 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
3932 handler_guard.push(bar);
3933 },
3934 );
3935
3936 aggregator.update(
3937 Price::from("1.00001"),
3938 Quantity::from(25),
3939 UnixNanos::default(),
3940 );
3941
3942 let handler_guard = handler.lock().expect(MUTEX_POISONED);
3943 assert_eq!(handler_guard.len(), 2);
3944 let bar1 = &handler_guard[0];
3945 assert_eq!(bar1.volume, Quantity::from(10));
3946 let bar2 = &handler_guard[1];
3947 assert_eq!(bar2.volume, Quantity::from(10));
3948 }
3949
3950 #[rstest]
3951 fn test_volume_bar_aggregator_zero_size_update_is_noop(equity_aapl: Equity) {
3952 let instrument = InstrumentAny::Equity(equity_aapl);
3953 let bar_spec = BarSpecification::new(10, BarAggregation::Volume, PriceType::Last);
3954 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3955 let handler = Arc::new(Mutex::new(Vec::new()));
3956 let handler_clone = Arc::clone(&handler);
3957
3958 let mut aggregator = VolumeBarAggregator::new(
3959 bar_type,
3960 instrument.price_precision(),
3961 instrument.size_precision(),
3962 move |bar: Bar| {
3963 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
3964 handler_guard.push(bar);
3965 },
3966 );
3967
3968 aggregator.update(
3969 Price::from("100.00"),
3970 Quantity::from(0),
3971 UnixNanos::default(),
3972 );
3973
3974 let handler_guard = handler.lock().expect(MUTEX_POISONED);
3975 assert_eq!(handler_guard.len(), 0);
3976 }
3977
3978 #[rstest]
3979 fn test_volume_bar_aggregator_exact_threshold_emits_single_bar(equity_aapl: Equity) {
3980 let instrument = InstrumentAny::Equity(equity_aapl);
3981 let bar_spec = BarSpecification::new(10, BarAggregation::Volume, PriceType::Last);
3982 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
3983 let handler = Arc::new(Mutex::new(Vec::new()));
3984 let handler_clone = Arc::clone(&handler);
3985
3986 let mut aggregator = VolumeBarAggregator::new(
3987 bar_type,
3988 instrument.price_precision(),
3989 instrument.size_precision(),
3990 move |bar: Bar| {
3991 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
3992 handler_guard.push(bar);
3993 },
3994 );
3995
3996 aggregator.update(
3997 Price::from("100.00"),
3998 Quantity::from(7),
3999 UnixNanos::from(1_000),
4000 );
4001 aggregator.update(
4002 Price::from("101.00"),
4003 Quantity::from(3),
4004 UnixNanos::from(2_000),
4005 );
4006
4007 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4008 assert_eq!(handler_guard.len(), 1);
4009 assert_eq!(handler_guard[0].volume, Quantity::from(10));
4010 assert_eq!(handler_guard[0].close, Price::from("101.00"));
4011 }
4012
4013 #[rstest]
4014 fn test_volume_bar_aggregator_step_of_one_emits_per_unit(equity_aapl: Equity) {
4015 let instrument = InstrumentAny::Equity(equity_aapl);
4016 let bar_spec = BarSpecification::new(1, BarAggregation::Volume, PriceType::Last);
4017 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4018 let handler = Arc::new(Mutex::new(Vec::new()));
4019 let handler_clone = Arc::clone(&handler);
4020
4021 let mut aggregator = VolumeBarAggregator::new(
4022 bar_type,
4023 instrument.price_precision(),
4024 instrument.size_precision(),
4025 move |bar: Bar| {
4026 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4027 handler_guard.push(bar);
4028 },
4029 );
4030
4031 aggregator.update(
4032 Price::from("100.00"),
4033 Quantity::from(1),
4034 UnixNanos::default(),
4035 );
4036
4037 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4038 assert_eq!(handler_guard.len(), 1);
4039 assert_eq!(handler_guard[0].volume, Quantity::from(1));
4040 }
4041
4042 #[rstest]
4043 fn test_volume_runs_bar_aggregator_side_change_resets(equity_aapl: Equity) {
4044 let instrument = InstrumentAny::Equity(equity_aapl);
4045 let bar_spec = BarSpecification::new(2, BarAggregation::VolumeRuns, PriceType::Last);
4046 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4047 let handler = Arc::new(Mutex::new(Vec::new()));
4048 let handler_clone = Arc::clone(&handler);
4049
4050 let mut aggregator = VolumeRunsBarAggregator::new(
4051 bar_type,
4052 instrument.price_precision(),
4053 instrument.size_precision(),
4054 move |bar: Bar| {
4055 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4056 handler_guard.push(bar);
4057 },
4058 );
4059
4060 let buy = TradeTick {
4061 instrument_id: instrument.id(),
4062 price: Price::from("1.0"),
4063 size: Quantity::from(1),
4064 ..TradeTick::default()
4065 };
4066 let sell = TradeTick {
4067 aggressor_side: AggressorSide::Seller,
4068 ..buy
4069 };
4070
4071 aggregator.handle_trade(buy);
4072 aggregator.handle_trade(buy); aggregator.handle_trade(sell);
4074 aggregator.handle_trade(sell); let handler_guard = handler.lock().expect(MUTEX_POISONED);
4077 assert!(handler_guard.len() >= 2);
4078 assert!(
4079 (handler_guard[0].volume.as_f64() - handler_guard[1].volume.as_f64()).abs()
4080 < f64::EPSILON
4081 );
4082 }
4083
4084 #[rstest]
4085 fn test_volume_runs_bar_aggregator_handles_large_single_trade(equity_aapl: Equity) {
4086 let instrument = InstrumentAny::Equity(equity_aapl);
4087 let bar_spec = BarSpecification::new(3, BarAggregation::VolumeRuns, PriceType::Last);
4088 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4089 let handler = Arc::new(Mutex::new(Vec::new()));
4090 let handler_clone = Arc::clone(&handler);
4091
4092 let mut aggregator = VolumeRunsBarAggregator::new(
4093 bar_type,
4094 instrument.price_precision(),
4095 instrument.size_precision(),
4096 move |bar: Bar| {
4097 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4098 handler_guard.push(bar);
4099 },
4100 );
4101
4102 let trade = TradeTick {
4103 instrument_id: instrument.id(),
4104 price: Price::from("1.0"),
4105 size: Quantity::from(5),
4106 ..TradeTick::default()
4107 };
4108
4109 aggregator.handle_trade(trade);
4110
4111 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4112 assert!(!handler_guard.is_empty());
4113 assert!(handler_guard[0].volume.as_f64() > 0.0);
4114 assert!(handler_guard[0].volume.as_f64() < trade.size.as_f64());
4115 }
4116
4117 #[rstest]
4118 fn test_volume_imbalance_bar_aggregator_splits_large_trade(equity_aapl: Equity) {
4119 let instrument = InstrumentAny::Equity(equity_aapl);
4120 let bar_spec = BarSpecification::new(2, BarAggregation::VolumeImbalance, PriceType::Last);
4121 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4122 let handler = Arc::new(Mutex::new(Vec::new()));
4123 let handler_clone = Arc::clone(&handler);
4124
4125 let mut aggregator = VolumeImbalanceBarAggregator::new(
4126 bar_type,
4127 instrument.price_precision(),
4128 instrument.size_precision(),
4129 move |bar: Bar| {
4130 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4131 handler_guard.push(bar);
4132 },
4133 );
4134
4135 let trade_small = TradeTick {
4136 instrument_id: instrument.id(),
4137 price: Price::from("1.0"),
4138 size: Quantity::from(1),
4139 ..TradeTick::default()
4140 };
4141 let trade_large = TradeTick {
4142 size: Quantity::from(3),
4143 ..trade_small
4144 };
4145
4146 aggregator.handle_trade(trade_small);
4147 aggregator.handle_trade(trade_large);
4148
4149 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4150 assert_eq!(handler_guard.len(), 2);
4151 let total_output = handler_guard
4152 .iter()
4153 .map(|bar| bar.volume.as_f64())
4154 .sum::<f64>();
4155 let total_input = trade_small.size.as_f64() + trade_large.size.as_f64();
4156 assert!((total_output - total_input).abs() < f64::EPSILON);
4157 }
4158
4159 #[rstest]
4160 fn test_value_bar_aggregator_builds_at_value_threshold(equity_aapl: Equity) {
4161 let instrument = InstrumentAny::Equity(equity_aapl);
4162 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4164 let handler = Arc::new(Mutex::new(Vec::new()));
4165 let handler_clone = Arc::clone(&handler);
4166
4167 let mut aggregator = ValueBarAggregator::new(
4168 bar_type,
4169 instrument.price_precision(),
4170 instrument.size_precision(),
4171 move |bar: Bar| {
4172 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4173 handler_guard.push(bar);
4174 },
4175 );
4176
4177 aggregator.update(
4179 Price::from("100.00"),
4180 Quantity::from(5),
4181 UnixNanos::default(),
4182 );
4183 aggregator.update(
4184 Price::from("100.00"),
4185 Quantity::from(5),
4186 UnixNanos::from(1000),
4187 );
4188
4189 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4190 assert_eq!(handler_guard.len(), 1);
4191 let bar = handler_guard.first().unwrap();
4192 assert_eq!(bar.volume, Quantity::from(10));
4193 }
4194
4195 #[rstest]
4196 fn test_value_bar_aggregator_handles_large_update(equity_aapl: Equity) {
4197 let instrument = InstrumentAny::Equity(equity_aapl);
4198 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last);
4199 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4200 let handler = Arc::new(Mutex::new(Vec::new()));
4201 let handler_clone = Arc::clone(&handler);
4202
4203 let mut aggregator = ValueBarAggregator::new(
4204 bar_type,
4205 instrument.price_precision(),
4206 instrument.size_precision(),
4207 move |bar: Bar| {
4208 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4209 handler_guard.push(bar);
4210 },
4211 );
4212
4213 aggregator.update(
4215 Price::from("100.00"),
4216 Quantity::from(25),
4217 UnixNanos::default(),
4218 );
4219
4220 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4221 assert_eq!(handler_guard.len(), 2);
4222 let remaining_value = aggregator.get_cumulative_value();
4223 assert!(remaining_value < 1000.0); }
4225
4226 #[rstest]
4227 fn test_value_bar_aggregator_handles_zero_price(equity_aapl: Equity) {
4228 let instrument = InstrumentAny::Equity(equity_aapl);
4229 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last);
4230 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4231 let handler = Arc::new(Mutex::new(Vec::new()));
4232 let handler_clone = Arc::clone(&handler);
4233
4234 let mut aggregator = ValueBarAggregator::new(
4235 bar_type,
4236 instrument.price_precision(),
4237 instrument.size_precision(),
4238 move |bar: Bar| {
4239 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4240 handler_guard.push(bar);
4241 },
4242 );
4243
4244 aggregator.update(
4246 Price::from("0.00"),
4247 Quantity::from(100),
4248 UnixNanos::default(),
4249 );
4250
4251 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4253 assert_eq!(handler_guard.len(), 0);
4254
4255 assert_eq!(aggregator.get_cumulative_value(), 0.0);
4257 }
4258
4259 #[rstest]
4260 fn test_value_bar_aggregator_handles_zero_size(equity_aapl: Equity) {
4261 let instrument = InstrumentAny::Equity(equity_aapl);
4262 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last);
4263 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4264 let handler = Arc::new(Mutex::new(Vec::new()));
4265 let handler_clone = Arc::clone(&handler);
4266
4267 let mut aggregator = ValueBarAggregator::new(
4268 bar_type,
4269 instrument.price_precision(),
4270 instrument.size_precision(),
4271 move |bar: Bar| {
4272 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4273 handler_guard.push(bar);
4274 },
4275 );
4276
4277 aggregator.update(
4279 Price::from("100.00"),
4280 Quantity::from(0),
4281 UnixNanos::default(),
4282 );
4283
4284 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4286 assert_eq!(handler_guard.len(), 0);
4287
4288 assert_eq!(aggregator.get_cumulative_value(), 0.0);
4290 }
4291
4292 #[rstest]
4293 fn test_value_bar_aggregator_exact_threshold_emits_one_bar(equity_aapl: Equity) {
4294 let instrument = InstrumentAny::Equity(equity_aapl);
4295 let bar_spec = BarSpecification::new(1000, BarAggregation::Value, PriceType::Last);
4296 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4297 let handler = Arc::new(Mutex::new(Vec::new()));
4298 let handler_clone = Arc::clone(&handler);
4299
4300 let mut aggregator = ValueBarAggregator::new(
4301 bar_type,
4302 instrument.price_precision(),
4303 instrument.size_precision(),
4304 move |bar: Bar| {
4305 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4306 handler_guard.push(bar);
4307 },
4308 );
4309
4310 aggregator.update(
4311 Price::from("100.00"),
4312 Quantity::from(5),
4313 UnixNanos::from(1_000),
4314 );
4315 aggregator.update(
4316 Price::from("100.00"),
4317 Quantity::from(5),
4318 UnixNanos::from(2_000),
4319 );
4320
4321 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4322 assert_eq!(handler_guard.len(), 1);
4323 assert_eq!(handler_guard[0].volume, Quantity::from(10));
4324 assert_eq!(aggregator.get_cumulative_value(), 0.0);
4325 }
4326
4327 #[rstest]
4328 fn test_value_bar_aggregator_precision_boundary_min_size_clamp(equity_aapl: Equity) {
4329 let instrument = InstrumentAny::Equity(equity_aapl);
4333 let bar_spec = BarSpecification::new(100, BarAggregation::Value, PriceType::Last);
4334 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4335 let handler = Arc::new(Mutex::new(Vec::new()));
4336 let handler_clone = Arc::clone(&handler);
4337
4338 let mut aggregator = ValueBarAggregator::new(
4339 bar_type,
4340 instrument.price_precision(),
4341 instrument.size_precision(),
4342 move |bar: Bar| {
4343 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4344 handler_guard.push(bar);
4345 },
4346 );
4347
4348 aggregator.update(
4350 Price::from("100.00"),
4351 Quantity::from(4),
4352 UnixNanos::default(),
4353 );
4354
4355 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4356 assert_eq!(handler_guard.len(), 4);
4357 for bar in handler_guard.iter() {
4358 assert_eq!(bar.volume, Quantity::from(1));
4359 }
4360 }
4361
4362 #[rstest]
4363 fn test_value_imbalance_bar_aggregator_emits_on_opposing_overflow(equity_aapl: Equity) {
4364 let instrument = InstrumentAny::Equity(equity_aapl);
4365 let bar_spec = BarSpecification::new(10, BarAggregation::ValueImbalance, PriceType::Last);
4366 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4367 let handler = Arc::new(Mutex::new(Vec::new()));
4368 let handler_clone = Arc::clone(&handler);
4369
4370 let mut aggregator = ValueImbalanceBarAggregator::new(
4371 bar_type,
4372 instrument.price_precision(),
4373 instrument.size_precision(),
4374 move |bar: Bar| {
4375 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4376 handler_guard.push(bar);
4377 },
4378 );
4379
4380 let buy = TradeTick {
4381 price: Price::from("5.0"),
4382 size: Quantity::from(2), instrument_id: instrument.id(),
4384 ..TradeTick::default()
4385 };
4386 let sell = TradeTick {
4387 price: Price::from("5.0"),
4388 size: Quantity::from(2), aggressor_side: AggressorSide::Seller,
4390 instrument_id: instrument.id(),
4391 ..buy
4392 };
4393
4394 aggregator.handle_trade(buy);
4395 aggregator.handle_trade(sell);
4396
4397 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4398 assert_eq!(handler_guard.len(), 2);
4399 }
4400
4401 #[rstest]
4402 fn test_value_runs_bar_aggregator_emits_on_consecutive_side(equity_aapl: Equity) {
4403 let instrument = InstrumentAny::Equity(equity_aapl);
4404 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
4405 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4406 let handler = Arc::new(Mutex::new(Vec::new()));
4407 let handler_clone = Arc::clone(&handler);
4408
4409 let mut aggregator = ValueRunsBarAggregator::new(
4410 bar_type,
4411 instrument.price_precision(),
4412 instrument.size_precision(),
4413 move |bar: Bar| {
4414 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4415 handler_guard.push(bar);
4416 },
4417 );
4418
4419 let trade = TradeTick {
4420 price: Price::from("10.0"),
4421 size: Quantity::from(5),
4422 instrument_id: instrument.id(),
4423 ..TradeTick::default()
4424 };
4425
4426 aggregator.handle_trade(trade);
4427 aggregator.handle_trade(trade);
4428
4429 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4430 assert_eq!(handler_guard.len(), 1);
4431 let bar = handler_guard.first().unwrap();
4432 assert_eq!(bar.volume, Quantity::from(10));
4433 }
4434
4435 #[rstest]
4436 fn test_value_runs_bar_aggregator_resets_on_side_change(equity_aapl: Equity) {
4437 let instrument = InstrumentAny::Equity(equity_aapl);
4438 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
4439 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4440 let handler = Arc::new(Mutex::new(Vec::new()));
4441 let handler_clone = Arc::clone(&handler);
4442
4443 let mut aggregator = ValueRunsBarAggregator::new(
4444 bar_type,
4445 instrument.price_precision(),
4446 instrument.size_precision(),
4447 move |bar: Bar| {
4448 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4449 handler_guard.push(bar);
4450 },
4451 );
4452
4453 let buy = TradeTick {
4454 price: Price::from("10.0"),
4455 size: Quantity::from(5),
4456 instrument_id: instrument.id(),
4457 ..TradeTick::default()
4458 }; let sell = TradeTick {
4460 price: Price::from("10.0"),
4461 size: Quantity::from(10),
4462 aggressor_side: AggressorSide::Seller,
4463 ..buy
4464 }; aggregator.handle_trade(buy);
4467 aggregator.handle_trade(sell);
4468
4469 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4470 assert_eq!(handler_guard.len(), 1);
4471 assert_eq!(handler_guard[0].volume, Quantity::from(10));
4472 }
4473
4474 #[rstest]
4475 fn test_tick_runs_bar_aggregator_continues_run_after_bar_emission(equity_aapl: Equity) {
4476 let instrument = InstrumentAny::Equity(equity_aapl);
4477 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
4478 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4479 let handler = Arc::new(Mutex::new(Vec::new()));
4480 let handler_clone = Arc::clone(&handler);
4481
4482 let mut aggregator = TickRunsBarAggregator::new(
4483 bar_type,
4484 instrument.price_precision(),
4485 instrument.size_precision(),
4486 move |bar: Bar| {
4487 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4488 handler_guard.push(bar);
4489 },
4490 );
4491
4492 let buy = TradeTick::default();
4493
4494 aggregator.handle_trade(buy);
4495 aggregator.handle_trade(buy); aggregator.handle_trade(buy); aggregator.handle_trade(buy); let handler_guard = handler.lock().expect(MUTEX_POISONED);
4500 assert_eq!(handler_guard.len(), 2);
4501 }
4502
4503 #[rstest]
4504 fn test_tick_runs_bar_aggregator_handles_no_aggressor_trades(equity_aapl: Equity) {
4505 let instrument = InstrumentAny::Equity(equity_aapl);
4506 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
4507 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4508 let handler = Arc::new(Mutex::new(Vec::new()));
4509 let handler_clone = Arc::clone(&handler);
4510
4511 let mut aggregator = TickRunsBarAggregator::new(
4512 bar_type,
4513 instrument.price_precision(),
4514 instrument.size_precision(),
4515 move |bar: Bar| {
4516 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4517 handler_guard.push(bar);
4518 },
4519 );
4520
4521 let buy = TradeTick::default();
4522 let no_aggressor = TradeTick {
4523 aggressor_side: AggressorSide::NoAggressor,
4524 ..buy
4525 };
4526
4527 aggregator.handle_trade(buy);
4528 aggregator.handle_trade(no_aggressor); aggregator.handle_trade(no_aggressor); aggregator.handle_trade(buy); let handler_guard = handler.lock().expect(MUTEX_POISONED);
4533 assert_eq!(handler_guard.len(), 1);
4534 }
4535
4536 #[rstest]
4537 fn test_volume_runs_bar_aggregator_continues_run_after_bar_emission(equity_aapl: Equity) {
4538 let instrument = InstrumentAny::Equity(equity_aapl);
4539 let bar_spec = BarSpecification::new(2, BarAggregation::VolumeRuns, PriceType::Last);
4540 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4541 let handler = Arc::new(Mutex::new(Vec::new()));
4542 let handler_clone = Arc::clone(&handler);
4543
4544 let mut aggregator = VolumeRunsBarAggregator::new(
4545 bar_type,
4546 instrument.price_precision(),
4547 instrument.size_precision(),
4548 move |bar: Bar| {
4549 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4550 handler_guard.push(bar);
4551 },
4552 );
4553
4554 let buy = TradeTick {
4555 instrument_id: instrument.id(),
4556 price: Price::from("1.0"),
4557 size: Quantity::from(1),
4558 ..TradeTick::default()
4559 };
4560
4561 aggregator.handle_trade(buy);
4562 aggregator.handle_trade(buy); aggregator.handle_trade(buy); aggregator.handle_trade(buy); let handler_guard = handler.lock().expect(MUTEX_POISONED);
4567 assert_eq!(handler_guard.len(), 2);
4568 assert_eq!(handler_guard[0].volume, Quantity::from(2));
4569 assert_eq!(handler_guard[1].volume, Quantity::from(2));
4570 }
4571
4572 #[rstest]
4573 fn test_value_runs_bar_aggregator_continues_run_after_bar_emission(equity_aapl: Equity) {
4574 let instrument = InstrumentAny::Equity(equity_aapl);
4575 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
4576 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4577 let handler = Arc::new(Mutex::new(Vec::new()));
4578 let handler_clone = Arc::clone(&handler);
4579
4580 let mut aggregator = ValueRunsBarAggregator::new(
4581 bar_type,
4582 instrument.price_precision(),
4583 instrument.size_precision(),
4584 move |bar: Bar| {
4585 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4586 handler_guard.push(bar);
4587 },
4588 );
4589
4590 let buy = TradeTick {
4591 instrument_id: instrument.id(),
4592 price: Price::from("10.0"),
4593 size: Quantity::from(5),
4594 ..TradeTick::default()
4595 }; aggregator.handle_trade(buy);
4598 aggregator.handle_trade(buy); aggregator.handle_trade(buy); aggregator.handle_trade(buy); let handler_guard = handler.lock().expect(MUTEX_POISONED);
4603 assert_eq!(handler_guard.len(), 2);
4604 assert_eq!(handler_guard[0].volume, Quantity::from(10));
4605 assert_eq!(handler_guard[1].volume, Quantity::from(10));
4606 }
4607
4608 #[rstest]
4609 fn test_time_bar_aggregator_builds_at_interval(equity_aapl: Equity) {
4610 let instrument = InstrumentAny::Equity(equity_aapl);
4611 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
4613 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4614 let handler = Arc::new(Mutex::new(Vec::new()));
4615 let handler_clone = Arc::clone(&handler);
4616 let clock = Rc::new(RefCell::new(TestClock::new()));
4617
4618 let mut aggregator = TimeBarAggregator::new(
4619 bar_type,
4620 instrument.price_precision(),
4621 instrument.size_precision(),
4622 clock.clone(),
4623 move |bar: Bar| {
4624 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4625 handler_guard.push(bar);
4626 },
4627 true, false, BarIntervalType::LeftOpen,
4630 None, 15, false, );
4634
4635 aggregator.update(
4636 Price::from("100.00"),
4637 Quantity::from(1),
4638 UnixNanos::default(),
4639 );
4640
4641 let next_sec = UnixNanos::from(1_000_000_000);
4642 clock.borrow_mut().set_time(next_sec);
4643
4644 let event = TimeEvent::new(
4645 Ustr::from("1-SECOND-LAST"),
4646 UUID4::new(),
4647 next_sec,
4648 next_sec,
4649 );
4650 aggregator.build_bar(&event);
4651
4652 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4653 assert_eq!(handler_guard.len(), 1);
4654 let bar = handler_guard.first().unwrap();
4655 assert_eq!(bar.ts_event, UnixNanos::default());
4656 assert_eq!(bar.ts_init, next_sec);
4657 }
4658
4659 #[rstest]
4660 fn test_time_bar_aggregator_stop_clears_timer_and_allows_restart(equity_aapl: Equity) {
4661 let instrument = InstrumentAny::Equity(equity_aapl);
4662 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
4663 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4664 let timer_name = format!("TIME_BAR_{bar_type}");
4665 let clock = Rc::new(RefCell::new(TestClock::new()));
4666
4667 let aggregator = TimeBarAggregator::new(
4668 bar_type,
4669 instrument.price_precision(),
4670 instrument.size_precision(),
4671 clock.clone(),
4672 |_bar: Bar| {},
4673 true,
4674 false,
4675 BarIntervalType::LeftOpen,
4676 None,
4677 15,
4678 false,
4679 );
4680
4681 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
4682 let rc = Rc::new(RefCell::new(boxed));
4683
4684 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
4685 assert_eq!(clock.borrow().timer_names(), vec![timer_name.as_str()]);
4686
4687 rc.borrow_mut().stop();
4688 assert!(clock.borrow().timer_names().is_empty());
4689
4690 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
4691 assert_eq!(clock.borrow().timer_names(), vec![timer_name.as_str()]);
4692 }
4693
4694 #[rstest]
4695 fn test_time_bar_aggregator_left_open_interval(equity_aapl: Equity) {
4696 let instrument = InstrumentAny::Equity(equity_aapl);
4697 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
4698 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4699 let handler = Arc::new(Mutex::new(Vec::new()));
4700 let handler_clone = Arc::clone(&handler);
4701 let clock = Rc::new(RefCell::new(TestClock::new()));
4702
4703 let mut aggregator = TimeBarAggregator::new(
4704 bar_type,
4705 instrument.price_precision(),
4706 instrument.size_precision(),
4707 clock.clone(),
4708 move |bar: Bar| {
4709 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4710 handler_guard.push(bar);
4711 },
4712 true, true, BarIntervalType::LeftOpen,
4715 None,
4716 15,
4717 false, );
4719
4720 aggregator.update(
4722 Price::from("100.00"),
4723 Quantity::from(1),
4724 UnixNanos::default(),
4725 );
4726
4727 let ts1 = UnixNanos::from(1_000_000_000);
4729 clock.borrow_mut().set_time(ts1);
4730 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts1, ts1);
4731 aggregator.build_bar(&event);
4732
4733 aggregator.update(Price::from("101.00"), Quantity::from(1), ts1);
4735
4736 let ts2 = UnixNanos::from(2_000_000_000);
4738 clock.borrow_mut().set_time(ts2);
4739 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts2, ts2);
4740 aggregator.build_bar(&event);
4741
4742 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4743 assert_eq!(handler_guard.len(), 2);
4744
4745 let bar1 = &handler_guard[0];
4746 assert_eq!(bar1.ts_event, ts1); assert_eq!(bar1.ts_init, ts1);
4748 assert_eq!(bar1.close, Price::from("100.00"));
4749 let bar2 = &handler_guard[1];
4750 assert_eq!(bar2.ts_event, ts2);
4751 assert_eq!(bar2.ts_init, ts2);
4752 assert_eq!(bar2.close, Price::from("101.00"));
4753 }
4754
4755 #[rstest]
4756 fn test_time_bar_aggregator_right_open_interval(equity_aapl: Equity) {
4757 let instrument = InstrumentAny::Equity(equity_aapl);
4758 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
4759 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4760 let handler = Arc::new(Mutex::new(Vec::new()));
4761 let handler_clone = Arc::clone(&handler);
4762 let clock = Rc::new(RefCell::new(TestClock::new()));
4763 let mut aggregator = TimeBarAggregator::new(
4764 bar_type,
4765 instrument.price_precision(),
4766 instrument.size_precision(),
4767 clock.clone(),
4768 move |bar: Bar| {
4769 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4770 handler_guard.push(bar);
4771 },
4772 true, true, BarIntervalType::RightOpen,
4775 None,
4776 15,
4777 false, );
4779
4780 aggregator.update(
4782 Price::from("100.00"),
4783 Quantity::from(1),
4784 UnixNanos::default(),
4785 );
4786
4787 let ts1 = UnixNanos::from(1_000_000_000);
4789 clock.borrow_mut().set_time(ts1);
4790 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts1, ts1);
4791 aggregator.build_bar(&event);
4792
4793 aggregator.update(Price::from("101.00"), Quantity::from(1), ts1);
4795
4796 let ts2 = UnixNanos::from(2_000_000_000);
4798 clock.borrow_mut().set_time(ts2);
4799 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts2, ts2);
4800 aggregator.build_bar(&event);
4801
4802 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4803 assert_eq!(handler_guard.len(), 2);
4804
4805 let bar1 = &handler_guard[0];
4806 assert_eq!(bar1.ts_event, UnixNanos::default()); assert_eq!(bar1.ts_init, ts1);
4808 assert_eq!(bar1.close, Price::from("100.00"));
4809
4810 let bar2 = &handler_guard[1];
4811 assert_eq!(bar2.ts_event, ts1);
4812 assert_eq!(bar2.ts_init, ts2);
4813 assert_eq!(bar2.close, Price::from("101.00"));
4814 }
4815
4816 #[rstest]
4817 fn test_time_bar_aggregator_no_updates_behavior(equity_aapl: Equity) {
4818 let instrument = InstrumentAny::Equity(equity_aapl);
4819 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
4820 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4821 let handler = Arc::new(Mutex::new(Vec::new()));
4822 let handler_clone = Arc::clone(&handler);
4823 let clock = Rc::new(RefCell::new(TestClock::new()));
4824
4825 let mut aggregator = TimeBarAggregator::new(
4827 bar_type,
4828 instrument.price_precision(),
4829 instrument.size_precision(),
4830 clock.clone(),
4831 move |bar: Bar| {
4832 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4833 handler_guard.push(bar);
4834 },
4835 false, true, BarIntervalType::LeftOpen,
4838 None,
4839 15,
4840 false, );
4842
4843 let ts1 = UnixNanos::from(1_000_000_000);
4845 clock.borrow_mut().set_time(ts1);
4846 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts1, ts1);
4847 aggregator.build_bar(&event);
4848
4849 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4850 assert_eq!(handler_guard.len(), 0); drop(handler_guard);
4852
4853 let handler = Arc::new(Mutex::new(Vec::new()));
4855 let handler_clone = Arc::clone(&handler);
4856 let mut aggregator = TimeBarAggregator::new(
4857 bar_type,
4858 instrument.price_precision(),
4859 instrument.size_precision(),
4860 clock.clone(),
4861 move |bar: Bar| {
4862 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4863 handler_guard.push(bar);
4864 },
4865 true, true, BarIntervalType::LeftOpen,
4868 None,
4869 15,
4870 false, );
4872
4873 aggregator.update(
4874 Price::from("100.00"),
4875 Quantity::from(1),
4876 UnixNanos::default(),
4877 );
4878
4879 let ts1 = UnixNanos::from(1_000_000_000);
4881 clock.borrow_mut().set_time(ts1);
4882 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts1, ts1);
4883 aggregator.build_bar(&event);
4884
4885 let ts2 = UnixNanos::from(2_000_000_000);
4887 clock.borrow_mut().set_time(ts2);
4888 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts2, ts2);
4889 aggregator.build_bar(&event);
4890
4891 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4892 assert_eq!(handler_guard.len(), 2); let bar1 = &handler_guard[0];
4894 assert_eq!(bar1.close, Price::from("100.00"));
4895 let bar2 = &handler_guard[1];
4896 assert_eq!(bar2.close, Price::from("100.00")); }
4898
4899 #[rstest]
4900 fn test_time_bar_aggregator_respects_timestamp_on_close(equity_aapl: Equity) {
4901 let instrument = InstrumentAny::Equity(equity_aapl);
4902 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
4903 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4904 let clock = Rc::new(RefCell::new(TestClock::new()));
4905 let handler = Arc::new(Mutex::new(Vec::new()));
4906 let handler_clone = Arc::clone(&handler);
4907
4908 let mut aggregator = TimeBarAggregator::new(
4909 bar_type,
4910 instrument.price_precision(),
4911 instrument.size_precision(),
4912 clock.clone(),
4913 move |bar: Bar| {
4914 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4915 handler_guard.push(bar);
4916 },
4917 true, true, BarIntervalType::RightOpen,
4920 None,
4921 15,
4922 false, );
4924
4925 let ts1 = UnixNanos::from(1_000_000_000);
4926 aggregator.update(Price::from("100.00"), Quantity::from(1), ts1);
4927
4928 let ts2 = UnixNanos::from(2_000_000_000);
4929 clock.borrow_mut().set_time(ts2);
4930
4931 let event = TimeEvent::new(Ustr::from("1-SECOND-LAST"), UUID4::new(), ts2, ts2);
4933 aggregator.build_bar(&event);
4934
4935 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4936 let bar = handler_guard.first().unwrap();
4937 assert_eq!(bar.ts_event, UnixNanos::default());
4938 assert_eq!(bar.ts_init, ts2);
4939 }
4940
4941 #[rstest]
4942 fn test_renko_bar_aggregator_initialization(audusd_sim: CurrencyPair) {
4943 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
4944 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4946 let handler = Arc::new(Mutex::new(Vec::new()));
4947 let handler_clone = Arc::clone(&handler);
4948
4949 let aggregator = RenkoBarAggregator::new(
4950 bar_type,
4951 instrument.price_precision(),
4952 instrument.size_precision(),
4953 instrument.price_increment(),
4954 move |bar: Bar| {
4955 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4956 handler_guard.push(bar);
4957 },
4958 );
4959
4960 assert_eq!(aggregator.bar_type(), bar_type);
4961 assert!(!aggregator.is_running());
4962 let expected_brick_size = 10 * instrument.price_increment().raw;
4964 assert_eq!(aggregator.brick_size, expected_brick_size);
4965 }
4966
4967 #[rstest]
4968 fn test_renko_bar_aggregator_update_below_brick_size_no_bar(audusd_sim: CurrencyPair) {
4969 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
4970 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
4972 let handler = Arc::new(Mutex::new(Vec::new()));
4973 let handler_clone = Arc::clone(&handler);
4974
4975 let mut aggregator = RenkoBarAggregator::new(
4976 bar_type,
4977 instrument.price_precision(),
4978 instrument.size_precision(),
4979 instrument.price_increment(),
4980 move |bar: Bar| {
4981 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
4982 handler_guard.push(bar);
4983 },
4984 );
4985
4986 aggregator.update(
4988 Price::from("1.00000"),
4989 Quantity::from(1),
4990 UnixNanos::default(),
4991 );
4992 aggregator.update(
4993 Price::from("1.00005"),
4994 Quantity::from(1),
4995 UnixNanos::from(1000),
4996 );
4997
4998 let handler_guard = handler.lock().expect(MUTEX_POISONED);
4999 assert_eq!(handler_guard.len(), 0); }
5001
5002 #[rstest]
5003 fn test_renko_bar_aggregator_update_exceeds_brick_size_creates_bar(audusd_sim: CurrencyPair) {
5004 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5005 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5007 let handler = Arc::new(Mutex::new(Vec::new()));
5008 let handler_clone = Arc::clone(&handler);
5009
5010 let mut aggregator = RenkoBarAggregator::new(
5011 bar_type,
5012 instrument.price_precision(),
5013 instrument.size_precision(),
5014 instrument.price_increment(),
5015 move |bar: Bar| {
5016 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5017 handler_guard.push(bar);
5018 },
5019 );
5020
5021 aggregator.update(
5023 Price::from("1.00000"),
5024 Quantity::from(1),
5025 UnixNanos::default(),
5026 );
5027 aggregator.update(
5028 Price::from("1.00015"),
5029 Quantity::from(1),
5030 UnixNanos::from(1000),
5031 );
5032
5033 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5034 assert_eq!(handler_guard.len(), 1);
5035
5036 let bar = handler_guard.first().unwrap();
5037 assert_eq!(bar.open, Price::from("1.00000"));
5038 assert_eq!(bar.high, Price::from("1.00010"));
5039 assert_eq!(bar.low, Price::from("1.00000"));
5040 assert_eq!(bar.close, Price::from("1.00010"));
5041 assert_eq!(bar.volume, Quantity::from(2));
5042 assert_eq!(bar.ts_event, UnixNanos::from(1000));
5043 assert_eq!(bar.ts_init, UnixNanos::from(1000));
5044 }
5045
5046 #[rstest]
5047 fn test_renko_bar_aggregator_multiple_bricks_in_one_update(audusd_sim: CurrencyPair) {
5048 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5049 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5051 let handler = Arc::new(Mutex::new(Vec::new()));
5052 let handler_clone = Arc::clone(&handler);
5053
5054 let mut aggregator = RenkoBarAggregator::new(
5055 bar_type,
5056 instrument.price_precision(),
5057 instrument.size_precision(),
5058 instrument.price_increment(),
5059 move |bar: Bar| {
5060 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5061 handler_guard.push(bar);
5062 },
5063 );
5064
5065 aggregator.update(
5067 Price::from("1.00000"),
5068 Quantity::from(1),
5069 UnixNanos::default(),
5070 );
5071 aggregator.update(
5072 Price::from("1.00025"),
5073 Quantity::from(1),
5074 UnixNanos::from(1000),
5075 );
5076
5077 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5078 assert_eq!(handler_guard.len(), 2);
5079
5080 let bar1 = &handler_guard[0];
5081 assert_eq!(bar1.open, Price::from("1.00000"));
5082 assert_eq!(bar1.high, Price::from("1.00010"));
5083 assert_eq!(bar1.low, Price::from("1.00000"));
5084 assert_eq!(bar1.close, Price::from("1.00010"));
5085
5086 let bar2 = &handler_guard[1];
5087 assert_eq!(bar2.open, Price::from("1.00010"));
5088 assert_eq!(bar2.high, Price::from("1.00020"));
5089 assert_eq!(bar2.low, Price::from("1.00010"));
5090 assert_eq!(bar2.close, Price::from("1.00020"));
5091 }
5092
5093 #[rstest]
5094 fn test_renko_bar_aggregator_downward_movement(audusd_sim: CurrencyPair) {
5095 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5096 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5098 let handler = Arc::new(Mutex::new(Vec::new()));
5099 let handler_clone = Arc::clone(&handler);
5100
5101 let mut aggregator = RenkoBarAggregator::new(
5102 bar_type,
5103 instrument.price_precision(),
5104 instrument.size_precision(),
5105 instrument.price_increment(),
5106 move |bar: Bar| {
5107 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5108 handler_guard.push(bar);
5109 },
5110 );
5111
5112 aggregator.update(
5114 Price::from("1.00020"),
5115 Quantity::from(1),
5116 UnixNanos::default(),
5117 );
5118 aggregator.update(
5119 Price::from("1.00005"),
5120 Quantity::from(1),
5121 UnixNanos::from(1000),
5122 );
5123
5124 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5125 assert_eq!(handler_guard.len(), 1);
5126
5127 let bar = handler_guard.first().unwrap();
5128 assert_eq!(bar.open, Price::from("1.00020"));
5129 assert_eq!(bar.high, Price::from("1.00020"));
5130 assert_eq!(bar.low, Price::from("1.00010"));
5131 assert_eq!(bar.close, Price::from("1.00010"));
5132 assert_eq!(bar.volume, Quantity::from(2));
5133 }
5134
5135 #[rstest]
5136 fn test_renko_bar_aggregator_handle_bar_below_brick_size(audusd_sim: CurrencyPair) {
5137 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5138 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5140 let handler = Arc::new(Mutex::new(Vec::new()));
5141 let handler_clone = Arc::clone(&handler);
5142
5143 let mut aggregator = RenkoBarAggregator::new(
5144 bar_type,
5145 instrument.price_precision(),
5146 instrument.size_precision(),
5147 instrument.price_increment(),
5148 move |bar: Bar| {
5149 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5150 handler_guard.push(bar);
5151 },
5152 );
5153
5154 let input_bar = Bar::new(
5156 BarType::new(
5157 instrument.id(),
5158 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5159 AggregationSource::Internal,
5160 ),
5161 Price::from("1.00000"),
5162 Price::from("1.00005"),
5163 Price::from("0.99995"),
5164 Price::from("1.00005"), Quantity::from(100),
5166 UnixNanos::default(),
5167 UnixNanos::from(1000),
5168 );
5169
5170 aggregator.handle_bar(input_bar);
5171
5172 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5173 assert_eq!(handler_guard.len(), 0); }
5175
5176 #[rstest]
5177 fn test_renko_bar_aggregator_handle_bar_exceeds_brick_size(audusd_sim: CurrencyPair) {
5178 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5179 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5181 let handler = Arc::new(Mutex::new(Vec::new()));
5182 let handler_clone = Arc::clone(&handler);
5183
5184 let mut aggregator = RenkoBarAggregator::new(
5185 bar_type,
5186 instrument.price_precision(),
5187 instrument.size_precision(),
5188 instrument.price_increment(),
5189 move |bar: Bar| {
5190 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5191 handler_guard.push(bar);
5192 },
5193 );
5194
5195 let bar1 = Bar::new(
5197 BarType::new(
5198 instrument.id(),
5199 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5200 AggregationSource::Internal,
5201 ),
5202 Price::from("1.00000"),
5203 Price::from("1.00005"),
5204 Price::from("0.99995"),
5205 Price::from("1.00000"),
5206 Quantity::from(100),
5207 UnixNanos::default(),
5208 UnixNanos::default(),
5209 );
5210
5211 let bar2 = Bar::new(
5213 BarType::new(
5214 instrument.id(),
5215 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5216 AggregationSource::Internal,
5217 ),
5218 Price::from("1.00000"),
5219 Price::from("1.00015"),
5220 Price::from("0.99995"),
5221 Price::from("1.00010"), Quantity::from(50),
5223 UnixNanos::from(60_000_000_000),
5224 UnixNanos::from(60_000_000_000),
5225 );
5226
5227 aggregator.handle_bar(bar1);
5228 aggregator.handle_bar(bar2);
5229
5230 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5231 assert_eq!(handler_guard.len(), 1);
5232
5233 let bar = handler_guard.first().unwrap();
5234 assert_eq!(bar.open, Price::from("1.00000"));
5235 assert_eq!(bar.high, Price::from("1.00010"));
5236 assert_eq!(bar.low, Price::from("1.00000"));
5237 assert_eq!(bar.close, Price::from("1.00010"));
5238 assert_eq!(bar.volume, Quantity::from(150));
5239 }
5240
5241 #[rstest]
5242 fn test_renko_bar_aggregator_handle_bar_multiple_bricks(audusd_sim: CurrencyPair) {
5243 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5244 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5246 let handler = Arc::new(Mutex::new(Vec::new()));
5247 let handler_clone = Arc::clone(&handler);
5248
5249 let mut aggregator = RenkoBarAggregator::new(
5250 bar_type,
5251 instrument.price_precision(),
5252 instrument.size_precision(),
5253 instrument.price_increment(),
5254 move |bar: Bar| {
5255 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5256 handler_guard.push(bar);
5257 },
5258 );
5259
5260 let bar1 = Bar::new(
5262 BarType::new(
5263 instrument.id(),
5264 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5265 AggregationSource::Internal,
5266 ),
5267 Price::from("1.00000"),
5268 Price::from("1.00005"),
5269 Price::from("0.99995"),
5270 Price::from("1.00000"),
5271 Quantity::from(100),
5272 UnixNanos::default(),
5273 UnixNanos::default(),
5274 );
5275
5276 let bar2 = Bar::new(
5278 BarType::new(
5279 instrument.id(),
5280 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5281 AggregationSource::Internal,
5282 ),
5283 Price::from("1.00000"),
5284 Price::from("1.00035"),
5285 Price::from("0.99995"),
5286 Price::from("1.00030"), Quantity::from(50),
5288 UnixNanos::from(60_000_000_000),
5289 UnixNanos::from(60_000_000_000),
5290 );
5291
5292 aggregator.handle_bar(bar1);
5293 aggregator.handle_bar(bar2);
5294
5295 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5296 assert_eq!(handler_guard.len(), 3);
5297
5298 let bar1 = &handler_guard[0];
5299 assert_eq!(bar1.open, Price::from("1.00000"));
5300 assert_eq!(bar1.close, Price::from("1.00010"));
5301
5302 let bar2 = &handler_guard[1];
5303 assert_eq!(bar2.open, Price::from("1.00010"));
5304 assert_eq!(bar2.close, Price::from("1.00020"));
5305
5306 let bar3 = &handler_guard[2];
5307 assert_eq!(bar3.open, Price::from("1.00020"));
5308 assert_eq!(bar3.close, Price::from("1.00030"));
5309 }
5310
5311 #[rstest]
5312 fn test_renko_bar_aggregator_handle_bar_downward_movement(audusd_sim: CurrencyPair) {
5313 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5314 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5316 let handler = Arc::new(Mutex::new(Vec::new()));
5317 let handler_clone = Arc::clone(&handler);
5318
5319 let mut aggregator = RenkoBarAggregator::new(
5320 bar_type,
5321 instrument.price_precision(),
5322 instrument.size_precision(),
5323 instrument.price_increment(),
5324 move |bar: Bar| {
5325 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5326 handler_guard.push(bar);
5327 },
5328 );
5329
5330 let bar1 = Bar::new(
5332 BarType::new(
5333 instrument.id(),
5334 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5335 AggregationSource::Internal,
5336 ),
5337 Price::from("1.00020"),
5338 Price::from("1.00025"),
5339 Price::from("1.00015"),
5340 Price::from("1.00020"),
5341 Quantity::from(100),
5342 UnixNanos::default(),
5343 UnixNanos::default(),
5344 );
5345
5346 let bar2 = Bar::new(
5348 BarType::new(
5349 instrument.id(),
5350 BarSpecification::new(1, BarAggregation::Minute, PriceType::Mid),
5351 AggregationSource::Internal,
5352 ),
5353 Price::from("1.00020"),
5354 Price::from("1.00025"),
5355 Price::from("1.00005"),
5356 Price::from("1.00010"), Quantity::from(50),
5358 UnixNanos::from(60_000_000_000),
5359 UnixNanos::from(60_000_000_000),
5360 );
5361
5362 aggregator.handle_bar(bar1);
5363 aggregator.handle_bar(bar2);
5364
5365 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5366 assert_eq!(handler_guard.len(), 1);
5367
5368 let bar = handler_guard.first().unwrap();
5369 assert_eq!(bar.open, Price::from("1.00020"));
5370 assert_eq!(bar.high, Price::from("1.00020"));
5371 assert_eq!(bar.low, Price::from("1.00010"));
5372 assert_eq!(bar.close, Price::from("1.00010"));
5373 assert_eq!(bar.volume, Quantity::from(150));
5374 }
5375
5376 #[rstest]
5377 fn test_renko_bar_aggregator_brick_size_calculation(audusd_sim: CurrencyPair) {
5378 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5379
5380 let bar_spec_5 = BarSpecification::new(5, BarAggregation::Renko, PriceType::Mid); let bar_type_5 = BarType::new(instrument.id(), bar_spec_5, AggregationSource::Internal);
5383 let handler = Arc::new(Mutex::new(Vec::new()));
5384 let handler_clone = Arc::clone(&handler);
5385
5386 let aggregator_5 = RenkoBarAggregator::new(
5387 bar_type_5,
5388 instrument.price_precision(),
5389 instrument.size_precision(),
5390 instrument.price_increment(),
5391 move |_bar: Bar| {
5392 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5393 handler_guard.push(_bar);
5394 },
5395 );
5396
5397 let expected_brick_size_5 = 5 * instrument.price_increment().raw;
5399 assert_eq!(aggregator_5.brick_size, expected_brick_size_5);
5400
5401 let bar_spec_20 = BarSpecification::new(20, BarAggregation::Renko, PriceType::Mid); let bar_type_20 = BarType::new(instrument.id(), bar_spec_20, AggregationSource::Internal);
5403 let handler2 = Arc::new(Mutex::new(Vec::new()));
5404 let handler2_clone = Arc::clone(&handler2);
5405
5406 let aggregator_20 = RenkoBarAggregator::new(
5407 bar_type_20,
5408 instrument.price_precision(),
5409 instrument.size_precision(),
5410 instrument.price_increment(),
5411 move |_bar: Bar| {
5412 let mut handler_guard = handler2_clone.lock().expect(MUTEX_POISONED);
5413 handler_guard.push(_bar);
5414 },
5415 );
5416
5417 let expected_brick_size_20 = 20 * instrument.price_increment().raw;
5419 assert_eq!(aggregator_20.brick_size, expected_brick_size_20);
5420 }
5421
5422 #[rstest]
5423 fn test_renko_bar_aggregator_sequential_updates(audusd_sim: CurrencyPair) {
5424 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5425 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5427 let handler = Arc::new(Mutex::new(Vec::new()));
5428 let handler_clone = Arc::clone(&handler);
5429
5430 let mut aggregator = RenkoBarAggregator::new(
5431 bar_type,
5432 instrument.price_precision(),
5433 instrument.size_precision(),
5434 instrument.price_increment(),
5435 move |bar: Bar| {
5436 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5437 handler_guard.push(bar);
5438 },
5439 );
5440
5441 aggregator.update(
5443 Price::from("1.00000"),
5444 Quantity::from(1),
5445 UnixNanos::from(1000),
5446 );
5447 aggregator.update(
5448 Price::from("1.00010"),
5449 Quantity::from(1),
5450 UnixNanos::from(2000),
5451 ); aggregator.update(
5453 Price::from("1.00020"),
5454 Quantity::from(1),
5455 UnixNanos::from(3000),
5456 ); aggregator.update(
5458 Price::from("1.00025"),
5459 Quantity::from(1),
5460 UnixNanos::from(4000),
5461 ); aggregator.update(
5463 Price::from("1.00030"),
5464 Quantity::from(1),
5465 UnixNanos::from(5000),
5466 ); let handler_guard = handler.lock().expect(MUTEX_POISONED);
5469 assert_eq!(handler_guard.len(), 3);
5470
5471 let bar1 = &handler_guard[0];
5472 assert_eq!(bar1.open, Price::from("1.00000"));
5473 assert_eq!(bar1.close, Price::from("1.00010"));
5474
5475 let bar2 = &handler_guard[1];
5476 assert_eq!(bar2.open, Price::from("1.00010"));
5477 assert_eq!(bar2.close, Price::from("1.00020"));
5478
5479 let bar3 = &handler_guard[2];
5480 assert_eq!(bar3.open, Price::from("1.00020"));
5481 assert_eq!(bar3.close, Price::from("1.00030"));
5482 }
5483
5484 #[rstest]
5485 fn test_renko_bar_aggregator_mixed_direction_movement(audusd_sim: CurrencyPair) {
5486 let instrument = InstrumentAny::CurrencyPair(audusd_sim);
5487 let bar_spec = BarSpecification::new(10, BarAggregation::Renko, PriceType::Mid); let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5489 let handler = Arc::new(Mutex::new(Vec::new()));
5490 let handler_clone = Arc::clone(&handler);
5491
5492 let mut aggregator = RenkoBarAggregator::new(
5493 bar_type,
5494 instrument.price_precision(),
5495 instrument.size_precision(),
5496 instrument.price_increment(),
5497 move |bar: Bar| {
5498 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5499 handler_guard.push(bar);
5500 },
5501 );
5502
5503 aggregator.update(
5505 Price::from("1.00000"),
5506 Quantity::from(1),
5507 UnixNanos::from(1000),
5508 );
5509 aggregator.update(
5510 Price::from("1.00010"),
5511 Quantity::from(1),
5512 UnixNanos::from(2000),
5513 ); aggregator.update(
5515 Price::from("0.99990"),
5516 Quantity::from(1),
5517 UnixNanos::from(3000),
5518 ); let handler_guard = handler.lock().expect(MUTEX_POISONED);
5521 assert_eq!(handler_guard.len(), 3);
5522
5523 let bar1 = &handler_guard[0]; assert_eq!(bar1.open, Price::from("1.00000"));
5525 assert_eq!(bar1.high, Price::from("1.00010"));
5526 assert_eq!(bar1.low, Price::from("1.00000"));
5527 assert_eq!(bar1.close, Price::from("1.00010"));
5528
5529 let bar2 = &handler_guard[1]; assert_eq!(bar2.open, Price::from("1.00010"));
5531 assert_eq!(bar2.high, Price::from("1.00010"));
5532 assert_eq!(bar2.low, Price::from("1.00000"));
5533 assert_eq!(bar2.close, Price::from("1.00000"));
5534
5535 let bar3 = &handler_guard[2]; assert_eq!(bar3.open, Price::from("1.00000"));
5537 assert_eq!(bar3.high, Price::from("1.00000"));
5538 assert_eq!(bar3.low, Price::from("0.99990"));
5539 assert_eq!(bar3.close, Price::from("0.99990"));
5540 }
5541
5542 #[rstest]
5543 fn test_tick_imbalance_bar_aggregator_mixed_trades_cancel_out(equity_aapl: Equity) {
5544 let instrument = InstrumentAny::Equity(equity_aapl);
5545 let bar_spec = BarSpecification::new(3, BarAggregation::TickImbalance, PriceType::Last);
5546 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5547 let handler = Arc::new(Mutex::new(Vec::new()));
5548 let handler_clone = Arc::clone(&handler);
5549
5550 let mut aggregator = TickImbalanceBarAggregator::new(
5551 bar_type,
5552 instrument.price_precision(),
5553 instrument.size_precision(),
5554 move |bar: Bar| {
5555 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5556 handler_guard.push(bar);
5557 },
5558 );
5559
5560 let buy = TradeTick {
5561 aggressor_side: AggressorSide::Buyer,
5562 ..TradeTick::default()
5563 };
5564 let sell = TradeTick {
5565 aggressor_side: AggressorSide::Seller,
5566 ..TradeTick::default()
5567 };
5568
5569 aggregator.handle_trade(buy);
5570 aggregator.handle_trade(sell);
5571 aggregator.handle_trade(buy);
5572
5573 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5574 assert_eq!(handler_guard.len(), 0);
5575 }
5576
5577 #[rstest]
5578 fn test_tick_imbalance_bar_aggregator_no_aggressor_ignored(equity_aapl: Equity) {
5579 let instrument = InstrumentAny::Equity(equity_aapl);
5580 let bar_spec = BarSpecification::new(2, BarAggregation::TickImbalance, PriceType::Last);
5581 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5582 let handler = Arc::new(Mutex::new(Vec::new()));
5583 let handler_clone = Arc::clone(&handler);
5584
5585 let mut aggregator = TickImbalanceBarAggregator::new(
5586 bar_type,
5587 instrument.price_precision(),
5588 instrument.size_precision(),
5589 move |bar: Bar| {
5590 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5591 handler_guard.push(bar);
5592 },
5593 );
5594
5595 let buy = TradeTick {
5596 aggressor_side: AggressorSide::Buyer,
5597 ..TradeTick::default()
5598 };
5599 let no_aggressor = TradeTick {
5600 aggressor_side: AggressorSide::NoAggressor,
5601 ..TradeTick::default()
5602 };
5603
5604 aggregator.handle_trade(buy);
5605 aggregator.handle_trade(no_aggressor);
5606 aggregator.handle_trade(buy);
5607
5608 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5609 assert_eq!(handler_guard.len(), 1);
5610 }
5611
5612 #[rstest]
5613 fn test_tick_runs_bar_aggregator_multiple_consecutive_runs(equity_aapl: Equity) {
5614 let instrument = InstrumentAny::Equity(equity_aapl);
5615 let bar_spec = BarSpecification::new(2, BarAggregation::TickRuns, PriceType::Last);
5616 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5617 let handler = Arc::new(Mutex::new(Vec::new()));
5618 let handler_clone = Arc::clone(&handler);
5619
5620 let mut aggregator = TickRunsBarAggregator::new(
5621 bar_type,
5622 instrument.price_precision(),
5623 instrument.size_precision(),
5624 move |bar: Bar| {
5625 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5626 handler_guard.push(bar);
5627 },
5628 );
5629
5630 let buy = TradeTick {
5631 aggressor_side: AggressorSide::Buyer,
5632 ..TradeTick::default()
5633 };
5634 let sell = TradeTick {
5635 aggressor_side: AggressorSide::Seller,
5636 ..TradeTick::default()
5637 };
5638
5639 aggregator.handle_trade(buy);
5640 aggregator.handle_trade(buy);
5641 aggregator.handle_trade(sell);
5642 aggregator.handle_trade(sell);
5643
5644 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5645 assert_eq!(handler_guard.len(), 2);
5646 }
5647
5648 #[rstest]
5649 fn test_volume_imbalance_bar_aggregator_large_trade_spans_bars(equity_aapl: Equity) {
5650 let instrument = InstrumentAny::Equity(equity_aapl);
5651 let bar_spec = BarSpecification::new(10, BarAggregation::VolumeImbalance, PriceType::Last);
5652 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5653 let handler = Arc::new(Mutex::new(Vec::new()));
5654 let handler_clone = Arc::clone(&handler);
5655
5656 let mut aggregator = VolumeImbalanceBarAggregator::new(
5657 bar_type,
5658 instrument.price_precision(),
5659 instrument.size_precision(),
5660 move |bar: Bar| {
5661 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5662 handler_guard.push(bar);
5663 },
5664 );
5665
5666 let large_trade = TradeTick {
5667 size: Quantity::from(25),
5668 aggressor_side: AggressorSide::Buyer,
5669 ..TradeTick::default()
5670 };
5671
5672 aggregator.handle_trade(large_trade);
5673
5674 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5675 assert_eq!(handler_guard.len(), 2);
5676 }
5677
5678 #[rstest]
5679 fn test_volume_imbalance_bar_aggregator_no_aggressor_does_not_affect_imbalance(
5680 equity_aapl: Equity,
5681 ) {
5682 let instrument = InstrumentAny::Equity(equity_aapl);
5683 let bar_spec = BarSpecification::new(10, BarAggregation::VolumeImbalance, PriceType::Last);
5684 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5685 let handler = Arc::new(Mutex::new(Vec::new()));
5686 let handler_clone = Arc::clone(&handler);
5687
5688 let mut aggregator = VolumeImbalanceBarAggregator::new(
5689 bar_type,
5690 instrument.price_precision(),
5691 instrument.size_precision(),
5692 move |bar: Bar| {
5693 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5694 handler_guard.push(bar);
5695 },
5696 );
5697
5698 let buy = TradeTick {
5699 size: Quantity::from(5),
5700 aggressor_side: AggressorSide::Buyer,
5701 ..TradeTick::default()
5702 };
5703 let no_aggressor = TradeTick {
5704 size: Quantity::from(3),
5705 aggressor_side: AggressorSide::NoAggressor,
5706 ..TradeTick::default()
5707 };
5708
5709 aggregator.handle_trade(buy);
5710 aggregator.handle_trade(no_aggressor);
5711 aggregator.handle_trade(buy);
5712
5713 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5714 assert_eq!(handler_guard.len(), 1);
5715 }
5716
5717 #[rstest]
5718 fn test_volume_runs_bar_aggregator_large_trade_spans_bars(equity_aapl: Equity) {
5719 let instrument = InstrumentAny::Equity(equity_aapl);
5720 let bar_spec = BarSpecification::new(10, BarAggregation::VolumeRuns, PriceType::Last);
5721 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5722 let handler = Arc::new(Mutex::new(Vec::new()));
5723 let handler_clone = Arc::clone(&handler);
5724
5725 let mut aggregator = VolumeRunsBarAggregator::new(
5726 bar_type,
5727 instrument.price_precision(),
5728 instrument.size_precision(),
5729 move |bar: Bar| {
5730 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5731 handler_guard.push(bar);
5732 },
5733 );
5734
5735 let large_trade = TradeTick {
5736 size: Quantity::from(25),
5737 aggressor_side: AggressorSide::Buyer,
5738 ..TradeTick::default()
5739 };
5740
5741 aggregator.handle_trade(large_trade);
5742
5743 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5744 assert_eq!(handler_guard.len(), 2);
5745 }
5746
5747 #[rstest]
5748 fn test_value_runs_bar_aggregator_large_trade_spans_bars(equity_aapl: Equity) {
5749 let instrument = InstrumentAny::Equity(equity_aapl);
5750 let bar_spec = BarSpecification::new(50, BarAggregation::ValueRuns, PriceType::Last);
5751 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5752 let handler = Arc::new(Mutex::new(Vec::new()));
5753 let handler_clone = Arc::clone(&handler);
5754
5755 let mut aggregator = ValueRunsBarAggregator::new(
5756 bar_type,
5757 instrument.price_precision(),
5758 instrument.size_precision(),
5759 move |bar: Bar| {
5760 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5761 handler_guard.push(bar);
5762 },
5763 );
5764
5765 let large_trade = TradeTick {
5766 price: Price::from("5.00"),
5767 size: Quantity::from(25),
5768 aggressor_side: AggressorSide::Buyer,
5769 ..TradeTick::default()
5770 };
5771
5772 aggregator.handle_trade(large_trade);
5773
5774 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5775 assert_eq!(handler_guard.len(), 2);
5776 }
5777
5778 #[rstest]
5779 fn test_value_bar_high_price_low_step_no_zero_volume_bars(equity_aapl: Equity) {
5780 let instrument = InstrumentAny::Equity(equity_aapl);
5781 let bar_spec = BarSpecification::new(100, BarAggregation::Value, PriceType::Last);
5782 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5783 let handler = Arc::new(Mutex::new(Vec::new()));
5784 let handler_clone = Arc::clone(&handler);
5785
5786 let mut aggregator = ValueBarAggregator::new(
5787 bar_type,
5788 instrument.price_precision(),
5789 instrument.size_precision(),
5790 move |bar: Bar| {
5791 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5792 handler_guard.push(bar);
5793 },
5794 );
5795
5796 aggregator.update(
5798 Price::from("1000.00"),
5799 Quantity::from(3),
5800 UnixNanos::default(),
5801 );
5802
5803 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5805 assert_eq!(handler_guard.len(), 3);
5806 for bar in handler_guard.iter() {
5807 assert_eq!(bar.volume, Quantity::from(1));
5808 }
5809 }
5810
5811 #[rstest]
5812 fn test_value_imbalance_high_price_low_step_no_zero_volume_bars(equity_aapl: Equity) {
5813 let instrument = InstrumentAny::Equity(equity_aapl);
5814 let bar_spec = BarSpecification::new(100, BarAggregation::ValueImbalance, PriceType::Last);
5815 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5816 let handler = Arc::new(Mutex::new(Vec::new()));
5817 let handler_clone = Arc::clone(&handler);
5818
5819 let mut aggregator = ValueImbalanceBarAggregator::new(
5820 bar_type,
5821 instrument.price_precision(),
5822 instrument.size_precision(),
5823 move |bar: Bar| {
5824 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5825 handler_guard.push(bar);
5826 },
5827 );
5828
5829 let trade = TradeTick {
5830 price: Price::from("1000.00"),
5831 size: Quantity::from(3),
5832 aggressor_side: AggressorSide::Buyer,
5833 instrument_id: instrument.id(),
5834 ..TradeTick::default()
5835 };
5836
5837 aggregator.handle_trade(trade);
5838
5839 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5840 assert_eq!(handler_guard.len(), 3);
5841 for bar in handler_guard.iter() {
5842 assert_eq!(bar.volume, Quantity::from(1));
5843 }
5844 }
5845
5846 #[rstest]
5847 fn test_value_imbalance_opposite_side_overshoot_emits_bar(equity_aapl: Equity) {
5848 let instrument = InstrumentAny::Equity(equity_aapl);
5849 let bar_spec = BarSpecification::new(100, BarAggregation::ValueImbalance, PriceType::Last);
5850 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5851 let handler = Arc::new(Mutex::new(Vec::new()));
5852 let handler_clone = Arc::clone(&handler);
5853
5854 let mut aggregator = ValueImbalanceBarAggregator::new(
5855 bar_type,
5856 instrument.price_precision(),
5857 instrument.size_precision(),
5858 move |bar: Bar| {
5859 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5860 handler_guard.push(bar);
5861 },
5862 );
5863
5864 let sell_tick = TradeTick {
5866 price: Price::from("10.00"),
5867 size: Quantity::from(5),
5868 aggressor_side: AggressorSide::Seller,
5869 instrument_id: instrument.id(),
5870 ..TradeTick::default()
5871 };
5872
5873 let buy_tick = TradeTick {
5876 price: Price::from("1000.00"),
5877 size: Quantity::from(1),
5878 aggressor_side: AggressorSide::Buyer,
5879 instrument_id: instrument.id(),
5880 ts_init: UnixNanos::from(1),
5881 ts_event: UnixNanos::from(1),
5882 ..TradeTick::default()
5883 };
5884
5885 aggregator.handle_trade(sell_tick);
5886 aggregator.handle_trade(buy_tick);
5887
5888 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5889 assert_eq!(handler_guard.len(), 1);
5890 assert_eq!(handler_guard[0].volume, Quantity::from(6));
5891 }
5892
5893 #[rstest]
5894 fn test_value_runs_high_price_low_step_no_zero_volume_bars(equity_aapl: Equity) {
5895 let instrument = InstrumentAny::Equity(equity_aapl);
5896 let bar_spec = BarSpecification::new(100, BarAggregation::ValueRuns, PriceType::Last);
5897 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5898 let handler = Arc::new(Mutex::new(Vec::new()));
5899 let handler_clone = Arc::clone(&handler);
5900
5901 let mut aggregator = ValueRunsBarAggregator::new(
5902 bar_type,
5903 instrument.price_precision(),
5904 instrument.size_precision(),
5905 move |bar: Bar| {
5906 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5907 handler_guard.push(bar);
5908 },
5909 );
5910
5911 let trade = TradeTick {
5912 price: Price::from("1000.00"),
5913 size: Quantity::from(3),
5914 aggressor_side: AggressorSide::Buyer,
5915 instrument_id: instrument.id(),
5916 ..TradeTick::default()
5917 };
5918
5919 aggregator.handle_trade(trade);
5920
5921 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5922 assert_eq!(handler_guard.len(), 3);
5923 for bar in handler_guard.iter() {
5924 assert_eq!(bar.volume, Quantity::from(1));
5925 }
5926 }
5927
5928 #[rstest]
5929 #[case(1000_u64)]
5930 #[case(1500_u64)]
5931 fn test_volume_imbalance_bar_aggregator_large_step_no_overflow(
5932 equity_aapl: Equity,
5933 #[case] step: u64,
5934 ) {
5935 let instrument = InstrumentAny::Equity(equity_aapl);
5936 let bar_spec = BarSpecification::new(
5937 step as usize,
5938 BarAggregation::VolumeImbalance,
5939 PriceType::Last,
5940 );
5941 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5942 let handler = Arc::new(Mutex::new(Vec::new()));
5943 let handler_clone = Arc::clone(&handler);
5944
5945 let mut aggregator = VolumeImbalanceBarAggregator::new(
5946 bar_type,
5947 instrument.price_precision(),
5948 instrument.size_precision(),
5949 move |bar: Bar| {
5950 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5951 handler_guard.push(bar);
5952 },
5953 );
5954
5955 let trade = TradeTick {
5956 size: Quantity::from(step * 2),
5957 aggressor_side: AggressorSide::Buyer,
5958 ..TradeTick::default()
5959 };
5960
5961 aggregator.handle_trade(trade);
5962
5963 let handler_guard = handler.lock().expect(MUTEX_POISONED);
5964 assert_eq!(handler_guard.len(), 2);
5965 for bar in handler_guard.iter() {
5966 assert_eq!(bar.volume.as_f64(), step as f64);
5967 }
5968 }
5969
5970 #[rstest]
5971 fn test_volume_imbalance_bar_aggregator_different_large_steps_produce_different_bar_counts(
5972 equity_aapl: Equity,
5973 ) {
5974 let instrument = InstrumentAny::Equity(equity_aapl);
5975 let total_volume = 3000_u64;
5976 let mut results = Vec::new();
5977
5978 for step in [1000_usize, 1500] {
5979 let bar_spec =
5980 BarSpecification::new(step, BarAggregation::VolumeImbalance, PriceType::Last);
5981 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
5982 let handler = Arc::new(Mutex::new(Vec::new()));
5983 let handler_clone = Arc::clone(&handler);
5984
5985 let mut aggregator = VolumeImbalanceBarAggregator::new(
5986 bar_type,
5987 instrument.price_precision(),
5988 instrument.size_precision(),
5989 move |bar: Bar| {
5990 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
5991 handler_guard.push(bar);
5992 },
5993 );
5994
5995 let trade = TradeTick {
5996 size: Quantity::from(total_volume),
5997 aggressor_side: AggressorSide::Buyer,
5998 ..TradeTick::default()
5999 };
6000
6001 aggregator.handle_trade(trade);
6002
6003 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6004 results.push(handler_guard.len());
6005 }
6006
6007 assert_eq!(results[0], 3); assert_eq!(results[1], 2); assert_ne!(results[0], results[1]);
6010 }
6011
6012 #[rstest]
6013 #[case(1000_u64)]
6014 #[case(1500_u64)]
6015 fn test_volume_runs_bar_aggregator_large_step_no_overflow(
6016 equity_aapl: Equity,
6017 #[case] step: u64,
6018 ) {
6019 let instrument = InstrumentAny::Equity(equity_aapl);
6020 let bar_spec =
6021 BarSpecification::new(step as usize, BarAggregation::VolumeRuns, PriceType::Last);
6022 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6023 let handler = Arc::new(Mutex::new(Vec::new()));
6024 let handler_clone = Arc::clone(&handler);
6025
6026 let mut aggregator = VolumeRunsBarAggregator::new(
6027 bar_type,
6028 instrument.price_precision(),
6029 instrument.size_precision(),
6030 move |bar: Bar| {
6031 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
6032 handler_guard.push(bar);
6033 },
6034 );
6035
6036 let trade = TradeTick {
6037 size: Quantity::from(step * 2),
6038 aggressor_side: AggressorSide::Buyer,
6039 ..TradeTick::default()
6040 };
6041
6042 aggregator.handle_trade(trade);
6043
6044 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6045 assert_eq!(handler_guard.len(), 2);
6046 for bar in handler_guard.iter() {
6047 assert_eq!(bar.volume.as_f64(), step as f64);
6048 }
6049 }
6050
6051 #[rstest]
6052 fn test_volume_runs_bar_aggregator_different_large_steps_produce_different_bar_counts(
6053 equity_aapl: Equity,
6054 ) {
6055 let instrument = InstrumentAny::Equity(equity_aapl);
6056 let total_volume = 3000_u64;
6057 let mut results = Vec::new();
6058
6059 for step in [1000_usize, 1500] {
6060 let bar_spec = BarSpecification::new(step, BarAggregation::VolumeRuns, PriceType::Last);
6061 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6062 let handler = Arc::new(Mutex::new(Vec::new()));
6063 let handler_clone = Arc::clone(&handler);
6064
6065 let mut aggregator = VolumeRunsBarAggregator::new(
6066 bar_type,
6067 instrument.price_precision(),
6068 instrument.size_precision(),
6069 move |bar: Bar| {
6070 let mut handler_guard = handler_clone.lock().expect(MUTEX_POISONED);
6071 handler_guard.push(bar);
6072 },
6073 );
6074
6075 let trade = TradeTick {
6076 size: Quantity::from(total_volume),
6077 aggressor_side: AggressorSide::Buyer,
6078 ..TradeTick::default()
6079 };
6080
6081 aggregator.handle_trade(trade);
6082
6083 let handler_guard = handler.lock().expect(MUTEX_POISONED);
6084 results.push(handler_guard.len());
6085 }
6086
6087 assert_eq!(results[0], 3); assert_eq!(results[1], 2); assert_ne!(results[0], results[1]);
6090 }
6091
6092 #[rstest]
6094 fn test_time_bar_historical_defers_event_at_ts_init_until_after_update(equity_aapl: Equity) {
6095 let instrument = InstrumentAny::Equity(equity_aapl);
6096 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
6097 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6098 let handler = Arc::new(Mutex::new(Vec::new()));
6099 let handler_clone = Arc::clone(&handler);
6100 let clock = Rc::new(RefCell::new(TestClock::new()));
6101
6102 let mut agg = TimeBarAggregator::new(
6103 bar_type,
6104 instrument.price_precision(),
6105 instrument.size_precision(),
6106 clock.clone(),
6107 move |bar: Bar| {
6108 let mut h = handler_clone.lock().expect(MUTEX_POISONED);
6109 h.push(bar);
6110 },
6111 true,
6112 true,
6113 BarIntervalType::LeftOpen,
6114 None,
6115 0,
6116 false,
6117 );
6118 agg.historical_mode = true;
6119 agg.set_clock_internal(clock);
6120 let boxed: Box<dyn BarAggregator> = Box::new(agg);
6121 let rc = Rc::new(RefCell::new(boxed));
6122 rc.borrow_mut().set_aggregator_weak(Rc::downgrade(&rc));
6123
6124 rc.borrow_mut().update(
6125 Price::from("100.00"),
6126 Quantity::from(1),
6127 UnixNanos::default(),
6128 );
6129 rc.borrow_mut().update(
6130 Price::from("100.00"),
6131 Quantity::from(1),
6132 UnixNanos::from(1_000_000_000),
6133 );
6134
6135 let bars = handler.lock().expect(MUTEX_POISONED);
6136 assert!(
6137 !bars.is_empty(),
6138 "deferred event at ts_init should produce a bar that includes the update"
6139 );
6140 let last_bar = bars.last().unwrap();
6141 assert_eq!(last_bar.close, Price::from("100.00"));
6142 assert!(
6143 last_bar.volume.as_f64() >= 1.0,
6144 "bar built after deferred event should include the update at ts_init"
6145 );
6146 }
6147
6148 #[rstest]
6149 fn test_spread_quote_quote_driven_emits_when_all_legs_received(equity_aapl: Equity) {
6150 let instrument = InstrumentAny::Equity(equity_aapl);
6151 let leg1 = instrument.id();
6152 let leg2 = InstrumentId::from("MSFT.XNAS");
6153 let spread_id = InstrumentId::from("SPREAD.XNAS");
6154 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
6155 let handler = Arc::new(Mutex::new(Vec::new()));
6156 let handler_clone = Arc::clone(&handler);
6157 let clock = Rc::new(RefCell::new(TestClock::new()));
6158
6159 let mut agg = SpreadQuoteAggregator::new(
6160 spread_id,
6161 &legs,
6162 true,
6163 instrument.price_precision(),
6164 0,
6165 Box::new(move |q: QuoteTick| {
6166 handler_clone.lock().expect(MUTEX_POISONED).push(q);
6167 }),
6168 clock,
6169 false,
6170 None,
6171 0,
6172 false,
6173 60,
6174 None,
6175 None,
6176 );
6177
6178 let ts = UnixNanos::from(1_000_000_000);
6179 agg.handle_quote_tick(QuoteTick::new(
6180 leg1,
6181 Price::from("100.00"),
6182 Price::from("100.10"),
6183 Quantity::from(10),
6184 Quantity::from(10),
6185 ts,
6186 ts,
6187 ));
6188 assert_eq!(handler.lock().expect(MUTEX_POISONED).len(), 0);
6189
6190 agg.handle_quote_tick(QuoteTick::new(
6191 leg2,
6192 Price::from("99.00"),
6193 Price::from("99.10"),
6194 Quantity::from(10),
6195 Quantity::from(10),
6196 ts,
6197 ts,
6198 ));
6199 let quotes = handler.lock().expect(MUTEX_POISONED);
6200 assert_eq!(quotes.len(), 1);
6201 assert_eq!(quotes[0].instrument_id, spread_id);
6202 assert!(quotes[0].bid_price < quotes[0].ask_price);
6203 }
6204
6205 #[rstest]
6206 fn test_spread_quote_futures_pricing_signed_ratios(equity_aapl: Equity) {
6207 let instrument = InstrumentAny::Equity(equity_aapl);
6208 let leg1 = instrument.id();
6209 let leg2 = InstrumentId::from("MSFT.XNAS");
6210 let spread_id = InstrumentId::from("SPREAD.XNAS");
6211 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
6212 let handler = Arc::new(Mutex::new(Vec::new()));
6213 let handler_clone = Arc::clone(&handler);
6214 let clock = Rc::new(RefCell::new(TestClock::new()));
6215
6216 let mut agg = SpreadQuoteAggregator::new(
6217 spread_id,
6218 &legs,
6219 true,
6220 instrument.price_precision(),
6221 0,
6222 Box::new(move |q: QuoteTick| {
6223 handler_clone.lock().expect(MUTEX_POISONED).push(q);
6224 }),
6225 clock,
6226 false,
6227 None,
6228 0,
6229 false,
6230 60,
6231 None,
6232 None,
6233 );
6234
6235 let ts = UnixNanos::from(1_000_000_000);
6236 agg.handle_quote_tick(QuoteTick::new(
6237 leg1,
6238 Price::from("10.00"),
6239 Price::from("10.10"),
6240 Quantity::from(100),
6241 Quantity::from(100),
6242 ts,
6243 ts,
6244 ));
6245 agg.handle_quote_tick(QuoteTick::new(
6246 leg2,
6247 Price::from("20.00"),
6248 Price::from("20.10"),
6249 Quantity::from(100),
6250 Quantity::from(100),
6251 ts,
6252 ts,
6253 ));
6254 let quotes = handler.lock().expect(MUTEX_POISONED);
6255 assert_eq!(quotes.len(), 1);
6256 let q = "es[0];
6257 assert_eq!(q.instrument_id, spread_id);
6258 assert_eq!(q.bid_price, Price::from("-10.10"));
6259 assert_eq!(q.ask_price, Price::from("-9.90"));
6260 }
6261
6262 #[rstest]
6263 fn test_spread_quote_size_calculation_non_unit_ratios(equity_aapl: Equity) {
6264 let instrument = InstrumentAny::Equity(equity_aapl);
6265 let leg1 = instrument.id();
6266 let leg2 = InstrumentId::from("MSFT.XNAS");
6267 let spread_id = InstrumentId::from("SPREAD.XNAS");
6268 let legs = vec![(leg1, 2_i64), (leg2, -1_i64)];
6269 let handler = Arc::new(Mutex::new(Vec::new()));
6270 let handler_clone = Arc::clone(&handler);
6271 let clock = Rc::new(RefCell::new(TestClock::new()));
6272
6273 let mut agg = SpreadQuoteAggregator::new(
6274 spread_id,
6275 &legs,
6276 true,
6277 instrument.price_precision(),
6278 0,
6279 Box::new(move |q: QuoteTick| {
6280 handler_clone.lock().expect(MUTEX_POISONED).push(q);
6281 }),
6282 clock,
6283 false,
6284 None,
6285 0,
6286 false,
6287 60,
6288 None,
6289 None,
6290 );
6291
6292 let ts = UnixNanos::from(1_000_000_000);
6293 agg.handle_quote_tick(QuoteTick::new(
6294 leg1,
6295 Price::from("10.00"),
6296 Price::from("10.10"),
6297 Quantity::from(100),
6298 Quantity::from(40),
6299 ts,
6300 ts,
6301 ));
6302 agg.handle_quote_tick(QuoteTick::new(
6303 leg2,
6304 Price::from("10.00"),
6305 Price::from("10.10"),
6306 Quantity::from(50),
6307 Quantity::from(30),
6308 ts,
6309 ts,
6310 ));
6311 let quotes = handler.lock().expect(MUTEX_POISONED);
6312 assert_eq!(quotes.len(), 1);
6313 let q = "es[0];
6314 assert_eq!(q.bid_size.as_f64(), 30.0);
6315 assert_eq!(q.ask_size.as_f64(), 20.0);
6316 }
6317
6318 #[rstest]
6319 fn test_spread_quote_timer_driven_emission_cadence(equity_aapl: Equity) {
6320 let instrument = InstrumentAny::Equity(equity_aapl);
6321 let leg1 = instrument.id();
6322 let leg2 = InstrumentId::from("MSFT.XNAS");
6323 let spread_id = InstrumentId::from("SPREAD.XNAS");
6324 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
6325 let handler = Arc::new(Mutex::new(Vec::new()));
6326 let handler_clone = Arc::clone(&handler);
6327 let clock = Rc::new(RefCell::new(TestClock::new()));
6328 clock.borrow_mut().set_time(UnixNanos::from(0));
6329
6330 let agg = SpreadQuoteAggregator::new(
6331 spread_id,
6332 &legs,
6333 true,
6334 instrument.price_precision(),
6335 0,
6336 Box::new(move |q: QuoteTick| {
6337 handler_clone.lock().expect(MUTEX_POISONED).push(q);
6338 }),
6339 clock.clone(),
6340 false,
6341 Some(1),
6342 0,
6343 false,
6344 60,
6345 None,
6346 None,
6347 );
6348 let rc = Rc::new(RefCell::new(agg));
6349 rc.borrow_mut().prepare_for_timer_mode(&rc);
6350 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
6351
6352 for event in clock.borrow_mut().advance_time(UnixNanos::from(0), true) {
6353 rc.borrow_mut().on_timer_fire(event.ts_event);
6354 }
6355 assert_eq!(handler.lock().expect(MUTEX_POISONED).len(), 0);
6356
6357 let ts1 = UnixNanos::from(1_000_000_000);
6358 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
6359 leg1,
6360 Price::from("100.00"),
6361 Price::from("100.10"),
6362 Quantity::from(10),
6363 Quantity::from(10),
6364 ts1,
6365 ts1,
6366 ));
6367 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
6368 leg2,
6369 Price::from("99.00"),
6370 Price::from("99.10"),
6371 Quantity::from(10),
6372 Quantity::from(10),
6373 ts1,
6374 ts1,
6375 ));
6376
6377 for event in clock.borrow_mut().advance_time(ts1, true) {
6378 rc.borrow_mut().on_timer_fire(event.ts_event);
6379 }
6380
6381 {
6382 let quotes = handler.lock().expect(MUTEX_POISONED);
6383 assert_eq!(quotes.len(), 1);
6384 assert_eq!(quotes[0].ts_event, ts1);
6385 assert_eq!(quotes[0].ts_init, ts1);
6386 }
6387
6388 let ts2 = UnixNanos::from(2_000_000_000);
6389 for event in clock.borrow_mut().advance_time(ts2, true) {
6390 rc.borrow_mut().on_timer_fire(event.ts_event);
6391 }
6392
6393 let quotes = handler.lock().expect(MUTEX_POISONED);
6394 assert_eq!(quotes.len(), 1);
6395 }
6396
6397 #[rstest]
6398 fn test_spread_quote_historical_timer_waits_for_all_legs(equity_aapl: Equity) {
6399 let instrument = InstrumentAny::Equity(equity_aapl);
6400 let leg1 = instrument.id();
6401 let leg2 = InstrumentId::from("MSFT.XNAS");
6402 let spread_id = InstrumentId::from("SPREAD.XNAS");
6403 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
6404 let handler = Arc::new(Mutex::new(Vec::new()));
6405 let handler_clone = Arc::clone(&handler);
6406 let clock = Rc::new(RefCell::new(TestClock::new()));
6407
6408 let agg = SpreadQuoteAggregator::new(
6409 spread_id,
6410 &legs,
6411 true,
6412 instrument.price_precision(),
6413 0,
6414 Box::new(move |q: QuoteTick| {
6415 handler_clone.lock().expect(MUTEX_POISONED).push(q);
6416 }),
6417 clock.clone(),
6419 true,
6420 Some(1),
6421 0,
6422 false,
6423 60,
6424 None,
6425 None,
6426 );
6427 let rc = Rc::new(RefCell::new(agg));
6428 rc.borrow_mut().prepare_for_timer_mode(&rc);
6429 rc.borrow_mut().set_clock(clock);
6430
6431 let ts1 = UnixNanos::from(1_000_000_000);
6432 let ts2 = UnixNanos::from(2_000_000_000);
6433 let ts3 = UnixNanos::from(3_000_000_000);
6434 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
6435 leg1,
6436 Price::from("100.00"),
6437 Price::from("100.10"),
6438 Quantity::from(10),
6439 Quantity::from(10),
6440 ts1,
6441 ts1,
6442 ));
6443 assert_eq!(handler.lock().expect(MUTEX_POISONED).len(), 0);
6444
6445 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
6446 leg2,
6447 Price::from("99.00"),
6448 Price::from("99.10"),
6449 Quantity::from(10),
6450 Quantity::from(10),
6451 ts2,
6452 ts2,
6453 ));
6454 assert_eq!(handler.lock().expect(MUTEX_POISONED).len(), 0);
6455
6456 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
6457 leg1,
6458 Price::from("100.00"),
6459 Price::from("100.10"),
6460 Quantity::from(10),
6461 Quantity::from(10),
6462 ts3,
6463 ts3,
6464 ));
6465 let quotes = handler.lock().expect(MUTEX_POISONED);
6466 assert_eq!(
6467 quotes.len(),
6468 1,
6469 "deferred event at ts2 is processed when we have all legs and advance to ts3"
6470 );
6471 }
6472
6473 #[rstest]
6474 fn test_spread_quote_historical_flush_emits_pending_final_quote(equity_aapl: Equity) {
6475 let instrument = InstrumentAny::Equity(equity_aapl);
6476 let leg1 = instrument.id();
6477 let leg2 = InstrumentId::from("MSFT.XNAS");
6478 let spread_id = InstrumentId::from("SPREAD.XNAS");
6479 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
6480 let handler = Arc::new(Mutex::new(Vec::new()));
6481 let handler_clone = Arc::clone(&handler);
6482 let clock = Rc::new(RefCell::new(TestClock::new()));
6483
6484 let agg = SpreadQuoteAggregator::new(
6485 spread_id,
6486 &legs,
6487 true,
6488 instrument.price_precision(),
6489 0,
6490 Box::new(move |q: QuoteTick| {
6491 handler_clone.lock().expect(MUTEX_POISONED).push(q);
6492 }),
6493 clock.clone(),
6495 true,
6496 Some(1),
6497 0,
6498 false,
6499 60,
6500 None,
6501 None,
6502 );
6503 let rc = Rc::new(RefCell::new(agg));
6504 rc.borrow_mut().prepare_for_timer_mode(&rc);
6505 rc.borrow_mut().set_clock(clock);
6506
6507 let ts1 = UnixNanos::from(1_000_000_000);
6508 let ts2 = UnixNanos::from(2_000_000_000);
6509 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
6510 leg1,
6511 Price::from("100.00"),
6512 Price::from("100.10"),
6513 Quantity::from(10),
6514 Quantity::from(10),
6515 ts1,
6516 ts1,
6517 ));
6518 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
6519 leg2,
6520 Price::from("99.00"),
6521 Price::from("99.10"),
6522 Quantity::from(10),
6523 Quantity::from(10),
6524 ts2,
6525 ts2,
6526 ));
6527
6528 assert_eq!(handler.lock().expect(MUTEX_POISONED).len(), 0);
6529
6530 rc.borrow_mut().flush_pending_historical_quote();
6531
6532 let quotes = handler.lock().expect(MUTEX_POISONED);
6533 assert_eq!(
6534 quotes.len(),
6535 1,
6536 "final historical quote should be emitted when the deferred event is flushed",
6537 );
6538 assert_eq!(quotes[0].ts_event, ts2);
6539 }
6540
6541 #[rstest]
6542 fn test_spread_quote_option_vega_weighting(equity_aapl: Equity) {
6543 let instrument = InstrumentAny::Equity(equity_aapl);
6544 let leg1 = instrument.id();
6545 let leg2 = InstrumentId::from("MSFT.XNAS");
6546 let spread_id = InstrumentId::from("SPREAD.XNAS");
6547 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
6548 let handler = Arc::new(Mutex::new(Vec::new()));
6549 let handler_clone = Arc::clone(&handler);
6550 let clock = Rc::new(RefCell::new(TestClock::new()));
6551
6552 let mut vega_provider = MapVegaProvider::new();
6553 vega_provider.insert(leg1, 0.15);
6554 vega_provider.insert(leg2, 0.12);
6555
6556 let mut agg = SpreadQuoteAggregator::new(
6557 spread_id,
6558 &legs,
6559 false,
6560 instrument.price_precision(),
6561 0,
6562 Box::new(move |q: QuoteTick| {
6563 handler_clone.lock().expect(MUTEX_POISONED).push(q);
6564 }),
6565 clock,
6566 false,
6567 None,
6568 0,
6569 false,
6570 60,
6571 Some(Box::new(vega_provider)),
6572 None,
6573 );
6574
6575 let ts = UnixNanos::from(1_000_000_000);
6576 agg.handle_quote_tick(QuoteTick::new(
6577 leg1,
6578 Price::from("10.00"),
6579 Price::from("10.20"),
6580 Quantity::from(100),
6581 Quantity::from(100),
6582 ts,
6583 ts,
6584 ));
6585 agg.handle_quote_tick(QuoteTick::new(
6586 leg2,
6587 Price::from("11.00"),
6588 Price::from("11.20"),
6589 Quantity::from(100),
6590 Quantity::from(100),
6591 ts,
6592 ts,
6593 ));
6594 let quotes = handler.lock().expect(MUTEX_POISONED);
6595 assert_eq!(quotes.len(), 1);
6596 let q = "es[0];
6597 assert!(q.bid_price < q.ask_price);
6598 assert!(q.ask_price.as_f64() - q.bid_price.as_f64() > 0.0);
6599 }
6600
6601 #[rstest]
6602 fn test_spread_quote_all_zero_vega_fallback(equity_aapl: Equity) {
6603 let instrument = InstrumentAny::Equity(equity_aapl);
6604 let leg1 = instrument.id();
6605 let leg2 = InstrumentId::from("MSFT.XNAS");
6606 let spread_id = InstrumentId::from("SPREAD.XNAS");
6607 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
6608 let handler = Arc::new(Mutex::new(Vec::new()));
6609 let handler_clone = Arc::clone(&handler);
6610 let clock = Rc::new(RefCell::new(TestClock::new()));
6611
6612 let mut vega_provider = MapVegaProvider::new();
6613 vega_provider.insert(leg1, 0.0);
6614 vega_provider.insert(leg2, 0.0);
6615
6616 let agg = SpreadQuoteAggregator::new(
6617 spread_id,
6618 &legs,
6619 false,
6620 instrument.price_precision(),
6621 0,
6622 Box::new(move |q: QuoteTick| {
6623 handler_clone.lock().expect(MUTEX_POISONED).push(q);
6624 }),
6625 clock.clone(),
6626 false,
6627 None,
6628 0,
6629 false,
6630 1,
6631 Some(Box::new(vega_provider)),
6632 None,
6633 );
6634 let rc = Rc::new(RefCell::new(agg));
6635 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
6636
6637 let ts = UnixNanos::from(1_000_000_000);
6638 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
6639 leg1,
6640 Price::from("10.00"),
6641 Price::from("10.10"),
6642 Quantity::from(100),
6643 Quantity::from(100),
6644 ts,
6645 ts,
6646 ));
6647 rc.borrow_mut().handle_quote_tick(QuoteTick::new(
6648 leg2,
6649 Price::from("20.00"),
6650 Price::from("20.10"),
6651 Quantity::from(100),
6652 Quantity::from(100),
6653 ts,
6654 ts,
6655 ));
6656 {
6657 let quotes = handler.lock().expect(MUTEX_POISONED);
6658 assert_eq!(quotes.len(), 1);
6659 let q = "es[0];
6660 assert_eq!(q.bid_price, Price::from("-10.10"));
6661 assert_eq!(q.ask_price, Price::from("-9.90"));
6662 }
6663 assert!(rc.borrow().vega_pricing_temporarily_disabled);
6664
6665 let timeout_name = rc.borrow().vega_pricing_timeout_timer_name.clone();
6666 assert!(
6667 clock
6668 .borrow()
6669 .timer_names()
6670 .contains(&timeout_name.as_str())
6671 );
6672
6673 let events = clock
6674 .borrow_mut()
6675 .advance_time(UnixNanos::from(2_000_000_000), true);
6676
6677 for handler in clock.borrow().match_handlers(events) {
6678 handler.run();
6679 }
6680
6681 assert!(!rc.borrow().vega_pricing_temporarily_disabled);
6682
6683 let cancel_handler = Arc::new(Mutex::new(Vec::new()));
6684 let cancel_handler_clone = Arc::clone(&cancel_handler);
6685 let mut cancel_vega_provider = MapVegaProvider::new();
6686 cancel_vega_provider.insert(leg1, 0.0);
6687 cancel_vega_provider.insert(leg2, 0.0);
6688 let cancel_agg = SpreadQuoteAggregator::new(
6689 spread_id,
6690 &legs,
6691 false,
6692 instrument.price_precision(),
6693 0,
6694 Box::new(move |q: QuoteTick| {
6695 cancel_handler_clone.lock().expect(MUTEX_POISONED).push(q);
6696 }),
6697 clock.clone(),
6698 false,
6699 None,
6700 0,
6701 false,
6702 10,
6703 Some(Box::new(cancel_vega_provider)),
6704 None,
6705 );
6706 let cancel_rc = Rc::new(RefCell::new(cancel_agg));
6707 cancel_rc
6708 .borrow_mut()
6709 .start_timer(Some(Rc::clone(&cancel_rc)));
6710 cancel_rc.borrow_mut().handle_quote_tick(QuoteTick::new(
6711 leg1,
6712 Price::from("10.00"),
6713 Price::from("10.10"),
6714 Quantity::from(100),
6715 Quantity::from(100),
6716 ts,
6717 ts,
6718 ));
6719 cancel_rc.borrow_mut().handle_quote_tick(QuoteTick::new(
6720 leg2,
6721 Price::from("20.00"),
6722 Price::from("20.10"),
6723 Quantity::from(100),
6724 Quantity::from(100),
6725 ts,
6726 ts,
6727 ));
6728 let cancel_timeout_name = cancel_rc.borrow().vega_pricing_timeout_timer_name.clone();
6729 assert!(
6730 clock
6731 .borrow()
6732 .timer_names()
6733 .contains(&cancel_timeout_name.as_str())
6734 );
6735 cancel_rc.borrow_mut().stop_timer();
6736 assert!(
6737 !clock
6738 .borrow()
6739 .timer_names()
6740 .contains(&cancel_timeout_name.as_str())
6741 );
6742
6743 let permanent_handler = Arc::new(Mutex::new(Vec::new()));
6744 let permanent_handler_clone = Arc::clone(&permanent_handler);
6745 let mut permanent_vega_provider = MapVegaProvider::new();
6746 permanent_vega_provider.insert(leg1, 0.15);
6747 permanent_vega_provider.insert(leg2, 0.12);
6748 let mut permanent_agg = SpreadQuoteAggregator::new(
6749 spread_id,
6750 &legs,
6751 false,
6752 instrument.price_precision(),
6753 0,
6754 Box::new(move |q: QuoteTick| {
6755 permanent_handler_clone
6756 .lock()
6757 .expect(MUTEX_POISONED)
6758 .push(q);
6759 }),
6760 Rc::new(RefCell::new(TestClock::new())),
6761 false,
6762 None,
6763 0,
6764 true,
6765 1,
6766 Some(Box::new(permanent_vega_provider)),
6767 None,
6768 );
6769
6770 permanent_agg.handle_quote_tick(QuoteTick::new(
6771 leg1,
6772 Price::from("10.00"),
6773 Price::from("10.10"),
6774 Quantity::from(100),
6775 Quantity::from(100),
6776 ts,
6777 ts,
6778 ));
6779 permanent_agg.handle_quote_tick(QuoteTick::new(
6780 leg2,
6781 Price::from("20.00"),
6782 Price::from("20.10"),
6783 Quantity::from(100),
6784 Quantity::from(100),
6785 ts,
6786 ts,
6787 ));
6788
6789 let permanent_quotes = permanent_handler.lock().expect(MUTEX_POISONED);
6790 assert_eq!(permanent_quotes.len(), 1);
6791 assert_eq!(permanent_quotes[0].bid_price, Price::from("-10.10"));
6792 assert_eq!(permanent_quotes[0].ask_price, Price::from("-9.90"));
6793 assert!(!permanent_agg.vega_pricing_temporarily_disabled);
6794 }
6795
6796 #[rstest]
6797 fn test_spread_quote_negative_prices_tick_scheme(equity_aapl: Equity) {
6798 let instrument = InstrumentAny::Equity(equity_aapl);
6799 let leg1 = instrument.id();
6800 let leg2 = InstrumentId::from("MSFT.XNAS");
6801 let spread_id = InstrumentId::from("SPREAD.XNAS");
6802 let legs = vec![(leg1, 1_i64), (leg2, -1_i64)];
6803 let handler = Arc::new(Mutex::new(Vec::new()));
6804 let handler_clone = Arc::clone(&handler);
6805 let clock = Rc::new(RefCell::new(TestClock::new()));
6806 let rounder = FixedTickSchemeRounder::new(0.01).unwrap();
6807
6808 let mut agg = SpreadQuoteAggregator::new(
6809 spread_id,
6810 &legs,
6811 true,
6812 2,
6813 0,
6814 Box::new(move |q: QuoteTick| {
6815 handler_clone.lock().expect(MUTEX_POISONED).push(q);
6816 }),
6817 clock,
6818 false,
6819 None,
6820 0,
6821 false,
6822 60,
6823 None,
6824 Some(Box::new(rounder)),
6825 );
6826
6827 let ts = UnixNanos::from(1_000_000_000);
6828 agg.handle_quote_tick(QuoteTick::new(
6829 leg1,
6830 Price::from("10.00"),
6831 Price::from("10.10"),
6832 Quantity::from(100),
6833 Quantity::from(100),
6834 ts,
6835 ts,
6836 ));
6837 agg.handle_quote_tick(QuoteTick::new(
6838 leg2,
6839 Price::from("20.00"),
6840 Price::from("20.10"),
6841 Quantity::from(100),
6842 Quantity::from(100),
6843 ts,
6844 ts,
6845 ));
6846 let quotes = handler.lock().expect(MUTEX_POISONED);
6847 assert_eq!(quotes.len(), 1);
6848 let q = "es[0];
6849 assert!(q.bid_price.as_f64() < 0.0);
6850 assert!(q.ask_price.as_f64() < 0.0);
6851 assert!(q.bid_price < q.ask_price);
6852 }
6853
6854 #[rstest]
6855 #[case(BarIntervalType::LeftOpen)]
6856 #[case(BarIntervalType::RightOpen)]
6857 fn test_time_bar_skip_first_non_full_bar_noop_on_boundary(
6858 equity_aapl: Equity,
6859 #[case] interval_type: BarIntervalType,
6860 ) {
6861 let instrument = InstrumentAny::Equity(equity_aapl);
6866 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
6867 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6868 let handler = Arc::new(Mutex::new(Vec::new()));
6869 let handler_clone = Arc::clone(&handler);
6870 let clock = Rc::new(RefCell::new(TestClock::new()));
6871 clock.borrow_mut().set_time(UnixNanos::from(1_000_000_000));
6872 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
6873
6874 let aggregator = TimeBarAggregator::new(
6875 bar_type,
6876 instrument.price_precision(),
6877 instrument.size_precision(),
6878 clock,
6879 move |bar: Bar| {
6880 let mut h = handler_clone.lock().expect(MUTEX_POISONED);
6881 h.push(bar);
6882 },
6883 false,
6884 false,
6885 interval_type,
6886 None,
6887 0,
6888 true, );
6890
6891 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
6892 let rc = Rc::new(RefCell::new(boxed));
6893 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
6894
6895 rc.borrow_mut().update(
6896 Price::from("100.00"),
6897 Quantity::from(1),
6898 UnixNanos::from(1_000_000_000),
6899 );
6900 rc.borrow_mut().build_bar(&TimeEvent::new(
6901 event_name,
6902 UUID4::new(),
6903 UnixNanos::from(2_000_000_000),
6904 UnixNanos::from(2_000_000_000),
6905 ));
6906 rc.borrow_mut().update(
6907 Price::from("101.00"),
6908 Quantity::from(1),
6909 UnixNanos::from(2_500_000_000),
6910 );
6911 rc.borrow_mut().build_bar(&TimeEvent::new(
6912 event_name,
6913 UUID4::new(),
6914 UnixNanos::from(3_000_000_000),
6915 UnixNanos::from(3_000_000_000),
6916 ));
6917
6918 let bars = handler.lock().expect(MUTEX_POISONED);
6919 assert_eq!(bars.len(), 2);
6920 assert_eq!(bars[0].close, Price::from("100.00"));
6921 assert_eq!(bars[1].close, Price::from("101.00"));
6922 }
6923
6924 #[rstest]
6925 #[case(BarIntervalType::LeftOpen)]
6926 #[case(BarIntervalType::RightOpen)]
6927 fn test_time_bar_skip_first_non_full_bar_drops_partial_bar(
6928 equity_aapl: Equity,
6929 #[case] interval_type: BarIntervalType,
6930 ) {
6931 let instrument = InstrumentAny::Equity(equity_aapl);
6935 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
6936 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
6937 let handler = Arc::new(Mutex::new(Vec::new()));
6938 let handler_clone = Arc::clone(&handler);
6939 let clock = Rc::new(RefCell::new(TestClock::new()));
6940 clock.borrow_mut().set_time(UnixNanos::from(1_500_000_000));
6941 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
6942
6943 let aggregator = TimeBarAggregator::new(
6944 bar_type,
6945 instrument.price_precision(),
6946 instrument.size_precision(),
6947 clock,
6948 move |bar: Bar| {
6949 let mut h = handler_clone.lock().expect(MUTEX_POISONED);
6950 h.push(bar);
6951 },
6952 false,
6953 false,
6954 interval_type,
6955 None,
6956 0,
6957 true, );
6959
6960 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
6961 let rc = Rc::new(RefCell::new(boxed));
6962 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
6963
6964 rc.borrow_mut().update(
6965 Price::from("100.00"),
6966 Quantity::from(1),
6967 UnixNanos::from(1_500_000_000),
6968 );
6969 rc.borrow_mut().build_bar(&TimeEvent::new(
6970 event_name,
6971 UUID4::new(),
6972 UnixNanos::from(2_000_000_000),
6973 UnixNanos::from(2_000_000_000),
6974 ));
6975 rc.borrow_mut().update(
6976 Price::from("101.00"),
6977 Quantity::from(1),
6978 UnixNanos::from(2_500_000_000),
6979 );
6980 rc.borrow_mut().build_bar(&TimeEvent::new(
6981 event_name,
6982 UUID4::new(),
6983 UnixNanos::from(3_000_000_000),
6984 UnixNanos::from(3_000_000_000),
6985 ));
6986
6987 let bars = handler.lock().expect(MUTEX_POISONED);
6988 assert_eq!(bars.len(), 1);
6989 assert_eq!(bars[0].close, Price::from("101.00"));
6990 }
6991
6992 #[rstest]
6993 fn test_time_bar_skip_first_non_full_bar_skips_every_call_before_first_close(
6994 equity_aapl: Equity,
6995 ) {
6996 let instrument = InstrumentAny::Equity(equity_aapl);
7000 let bar_spec = BarSpecification::new(10, BarAggregation::Second, PriceType::Last);
7001 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7002 let handler = Arc::new(Mutex::new(Vec::new()));
7003 let handler_clone = Arc::clone(&handler);
7004 let clock = Rc::new(RefCell::new(TestClock::new()));
7005 clock.borrow_mut().set_time(UnixNanos::from(5_000_000_000));
7006 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7007
7008 let aggregator = TimeBarAggregator::new(
7009 bar_type,
7010 instrument.price_precision(),
7011 instrument.size_precision(),
7012 clock,
7013 move |bar: Bar| {
7014 let mut h = handler_clone.lock().expect(MUTEX_POISONED);
7015 h.push(bar);
7016 },
7017 false,
7018 false,
7019 BarIntervalType::LeftOpen,
7020 None,
7021 0,
7022 true, );
7024
7025 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7026 let rc = Rc::new(RefCell::new(boxed));
7027 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7028
7029 for (price, update_ts, event_ts) in [
7033 ("100.00", 5_500_000_000_u64, 7_000_000_000_u64),
7034 ("101.00", 7_500_000_000_u64, 8_000_000_000_u64),
7035 ("102.00", 9_000_000_000_u64, 10_000_000_000_u64),
7036 ] {
7037 rc.borrow_mut().update(
7038 Price::from(price),
7039 Quantity::from(1),
7040 UnixNanos::from(update_ts),
7041 );
7042 rc.borrow_mut().build_bar(&TimeEvent::new(
7043 event_name,
7044 UUID4::new(),
7045 UnixNanos::from(event_ts),
7046 UnixNanos::from(event_ts),
7047 ));
7048 }
7049
7050 rc.borrow_mut().update(
7052 Price::from("103.00"),
7053 Quantity::from(1),
7054 UnixNanos::from(10_500_000_000),
7055 );
7056 rc.borrow_mut().build_bar(&TimeEvent::new(
7057 event_name,
7058 UUID4::new(),
7059 UnixNanos::from(11_000_000_000),
7060 UnixNanos::from(11_000_000_000),
7061 ));
7062
7063 let bars = handler.lock().expect(MUTEX_POISONED);
7064 assert_eq!(bars.len(), 1);
7065 assert_eq!(bars[0].close, Price::from("103.00"));
7066 }
7067
7068 #[rstest]
7069 fn test_time_bar_skip_first_non_full_bar_skips_when_build_delay_shifts_start(
7070 equity_aapl: Equity,
7071 ) {
7072 let instrument = InstrumentAny::Equity(equity_aapl);
7077 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
7078 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7079 let handler = Arc::new(Mutex::new(Vec::new()));
7080 let handler_clone = Arc::clone(&handler);
7081 let clock = Rc::new(RefCell::new(TestClock::new()));
7082 clock.borrow_mut().set_time(UnixNanos::from(2_000_000_000));
7083 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7084
7085 let aggregator = TimeBarAggregator::new(
7086 bar_type,
7087 instrument.price_precision(),
7088 instrument.size_precision(),
7089 clock,
7090 move |bar: Bar| {
7091 let mut h = handler_clone.lock().expect(MUTEX_POISONED);
7092 h.push(bar);
7093 },
7094 false,
7095 false,
7096 BarIntervalType::LeftOpen,
7097 None,
7098 100, true, );
7101
7102 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7103 let rc = Rc::new(RefCell::new(boxed));
7104 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7105
7106 rc.borrow_mut().update(
7108 Price::from("100.00"),
7109 Quantity::from(1),
7110 UnixNanos::from(2_500_000_000),
7111 );
7112 rc.borrow_mut().build_bar(&TimeEvent::new(
7113 event_name,
7114 UUID4::new(),
7115 UnixNanos::from(3_000_100_000),
7116 UnixNanos::from(3_000_100_000),
7117 ));
7118 rc.borrow_mut().update(
7119 Price::from("101.00"),
7120 Quantity::from(1),
7121 UnixNanos::from(3_500_000_000),
7122 );
7123 rc.borrow_mut().build_bar(&TimeEvent::new(
7124 event_name,
7125 UUID4::new(),
7126 UnixNanos::from(4_000_100_000),
7127 UnixNanos::from(4_000_100_000),
7128 ));
7129
7130 let bars = handler.lock().expect(MUTEX_POISONED);
7131 assert_eq!(bars.len(), 1);
7132 assert_eq!(bars[0].close, Price::from("101.00"));
7133 }
7134
7135 #[rstest]
7136 #[case(
7137 BarAggregation::Month,
7138 1_735_689_600_000_000_000_u64,
7139 1_733_011_200_000_000_000_u64
7140 )]
7141 #[case(
7142 BarAggregation::Year,
7143 1_735_689_600_000_000_000_u64,
7144 1_704_067_200_000_000_000_u64
7145 )]
7146 fn test_time_bar_fire_immediately_month_year_stored_open_points_to_previous_period(
7147 equity_aapl: Equity,
7148 #[case] aggregation: BarAggregation,
7149 #[case] start_ns: u64,
7150 #[case] expected_stored_open_ns: u64,
7151 ) {
7152 let instrument = InstrumentAny::Equity(equity_aapl);
7157 let bar_spec = BarSpecification::new(1, aggregation, PriceType::Last);
7158 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7159 let handler = Arc::new(Mutex::new(Vec::new()));
7160 let handler_clone = Arc::clone(&handler);
7161 let clock = Rc::new(RefCell::new(TestClock::new()));
7162 clock.borrow_mut().set_time(UnixNanos::from(start_ns));
7163 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7164
7165 let aggregator = TimeBarAggregator::new(
7166 bar_type,
7167 instrument.price_precision(),
7168 instrument.size_precision(),
7169 clock,
7170 move |bar: Bar| {
7171 let mut h = handler_clone.lock().expect(MUTEX_POISONED);
7172 h.push(bar);
7173 },
7174 false,
7175 false,
7176 BarIntervalType::RightOpen, None,
7178 0,
7179 false, );
7181
7182 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7183 let rc = Rc::new(RefCell::new(boxed));
7184 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7185
7186 rc.borrow_mut().update(
7187 Price::from("100.00"),
7188 Quantity::from(1),
7189 UnixNanos::from(start_ns),
7190 );
7191 rc.borrow_mut().build_bar(&TimeEvent::new(
7192 event_name,
7193 UUID4::new(),
7194 UnixNanos::from(start_ns),
7195 UnixNanos::from(start_ns),
7196 ));
7197
7198 let bars = handler.lock().expect(MUTEX_POISONED);
7199 assert_eq!(bars.len(), 1);
7200 assert_eq!(bars[0].ts_event, UnixNanos::from(expected_stored_open_ns));
7201 assert_eq!(bars[0].ts_init, UnixNanos::from(start_ns));
7202 }
7203
7204 #[rstest]
7205 fn test_time_bar_historical_prevents_bars_for_timer_before_last_data(equity_aapl: Equity) {
7206 let instrument = InstrumentAny::Equity(equity_aapl);
7207 let bar_spec = BarSpecification::new(1, BarAggregation::Second, PriceType::Last);
7208 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7209 let handler = Arc::new(Mutex::new(Vec::new()));
7210 let handler_clone = Arc::clone(&handler);
7211 let clock = Rc::new(RefCell::new(TestClock::new()));
7212
7213 let mut agg = TimeBarAggregator::new(
7214 bar_type,
7215 instrument.price_precision(),
7216 instrument.size_precision(),
7217 clock.clone(),
7218 move |bar: Bar| {
7219 let mut h = handler_clone.lock().expect(MUTEX_POISONED);
7220 h.push(bar);
7221 },
7222 true,
7223 true,
7224 BarIntervalType::LeftOpen,
7225 None,
7226 0,
7227 false,
7228 );
7229 agg.historical_mode = true;
7230 agg.set_clock_internal(clock);
7231 let boxed: Box<dyn BarAggregator> = Box::new(agg);
7232 let rc = Rc::new(RefCell::new(boxed));
7233 rc.borrow_mut().set_aggregator_weak(Rc::downgrade(&rc));
7234
7235 let ts1 = UnixNanos::from(2_000_000_000);
7236 rc.borrow_mut()
7237 .update(Price::from("100.00"), Quantity::from(1), ts1);
7238
7239 let ts2 = UnixNanos::from(3_000_000_000);
7240 rc.borrow_mut()
7241 .update(Price::from("101.00"), Quantity::from(1), ts2);
7242
7243 let bars = handler.lock().expect(MUTEX_POISONED);
7244 assert!(
7245 !bars.is_empty(),
7246 "advancing time from ts1 to ts2 should produce at least one bar"
7247 );
7248 assert_eq!(bars[0].close, Price::from("100.00"));
7249 }
7250}
7251
7252#[cfg(test)]
7253mod property_tests {
7254 use std::{
7255 cell::RefCell,
7256 rc::Rc,
7257 sync::{Arc, Mutex},
7258 };
7259
7260 use nautilus_common::{clock::TestClock, timer::TimeEvent};
7261 use nautilus_core::{MUTEX_POISONED, UUID4, UnixNanos};
7262 use nautilus_model::{
7263 data::{Bar, BarSpecification, BarType, bar::get_bar_interval_ns},
7264 enums::{AggregationSource, BarAggregation, BarIntervalType, PriceType},
7265 instruments::{Instrument, InstrumentAny, stubs::equity_aapl},
7266 types::{Price, Quantity},
7267 };
7268 use proptest::prelude::*;
7269 use rstest::rstest;
7270 use ustr::Ustr;
7271
7272 use super::*;
7273
7274 fn time_bar_spec_strategy() -> impl Strategy<Value = (BarAggregation, usize)> {
7275 prop_oneof![
7276 (Just(BarAggregation::Second), 1usize..=5),
7277 (Just(BarAggregation::Minute), 1usize..=5),
7278 (Just(BarAggregation::Hour), 1usize..=4),
7279 ]
7280 }
7281
7282 fn interval_type_strategy() -> impl Strategy<Value = BarIntervalType> {
7283 prop_oneof![
7284 Just(BarIntervalType::LeftOpen),
7285 Just(BarIntervalType::RightOpen),
7286 ]
7287 }
7288
7289 proptest! {
7290 #[rstest]
7291 fn prop_skip_first_drops_partial_then_emits(
7292 (aggregation, step) in time_bar_spec_strategy(),
7293 interval_type in interval_type_strategy(),
7294 skip_first in any::<bool>(),
7295 ) {
7296 let instrument = InstrumentAny::Equity(equity_aapl());
7297 let bar_spec = BarSpecification::new(step, aggregation, PriceType::Last);
7298 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7299 let interval_ns = get_bar_interval_ns(&bar_type).as_u64();
7300
7301 let now_ns = interval_ns + interval_ns / 2;
7304
7305 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
7306 let handler_clone = Arc::clone(&handler);
7307 let clock = Rc::new(RefCell::new(TestClock::new()));
7308 clock.borrow_mut().set_time(UnixNanos::from(now_ns));
7309 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7310
7311 let aggregator = TimeBarAggregator::new(
7312 bar_type,
7313 instrument.price_precision(),
7314 instrument.size_precision(),
7315 clock,
7316 move |bar: Bar| {
7317 let mut h = handler_clone.lock().expect(MUTEX_POISONED);
7318 h.push(bar);
7319 },
7320 false,
7321 false,
7322 interval_type,
7323 None,
7324 0,
7325 skip_first,
7326 );
7327
7328 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7329 let rc = Rc::new(RefCell::new(boxed));
7330 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7331
7332 rc.borrow_mut().update(
7335 Price::from("100.00"),
7336 Quantity::from(1),
7337 UnixNanos::from(now_ns),
7338 );
7339 let first_close = 2 * interval_ns;
7340 rc.borrow_mut().build_bar(&TimeEvent::new(
7341 event_name,
7342 UUID4::new(),
7343 UnixNanos::from(first_close),
7344 UnixNanos::from(first_close),
7345 ));
7346
7347 rc.borrow_mut().update(
7349 Price::from("101.00"),
7350 Quantity::from(1),
7351 UnixNanos::from(first_close + interval_ns / 2),
7352 );
7353 let second_close = first_close + interval_ns;
7354 rc.borrow_mut().build_bar(&TimeEvent::new(
7355 event_name,
7356 UUID4::new(),
7357 UnixNanos::from(second_close),
7358 UnixNanos::from(second_close),
7359 ));
7360
7361 let bars = handler.lock().expect(MUTEX_POISONED);
7362 let expected = if skip_first { 1 } else { 2 };
7363 prop_assert_eq!(bars.len(), expected);
7364 prop_assert_eq!(bars.last().unwrap().close, Price::from("101.00"));
7365 for bar in bars.iter() {
7366 prop_assert!(bar.high >= bar.open);
7367 prop_assert!(bar.high >= bar.close);
7368 prop_assert!(bar.low <= bar.open);
7369 prop_assert!(bar.low <= bar.close);
7370 }
7371 }
7372
7373 #[rstest]
7374 fn prop_skip_first_noop_on_exact_boundary(
7375 (aggregation, step) in time_bar_spec_strategy(),
7376 interval_type in interval_type_strategy(),
7377 ) {
7378 let instrument = InstrumentAny::Equity(equity_aapl());
7379 let bar_spec = BarSpecification::new(step, aggregation, PriceType::Last);
7380 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7381 let interval_ns = get_bar_interval_ns(&bar_type).as_u64();
7382
7383 let now_ns = interval_ns;
7386 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
7387 let handler_clone = Arc::clone(&handler);
7388 let clock = Rc::new(RefCell::new(TestClock::new()));
7389 clock.borrow_mut().set_time(UnixNanos::from(now_ns));
7390 let event_name = Ustr::from(&format!("TIME_BAR_{bar_type}"));
7391
7392 let aggregator = TimeBarAggregator::new(
7393 bar_type,
7394 instrument.price_precision(),
7395 instrument.size_precision(),
7396 clock,
7397 move |bar: Bar| {
7398 let mut h = handler_clone.lock().expect(MUTEX_POISONED);
7399 h.push(bar);
7400 },
7401 false,
7402 false,
7403 interval_type,
7404 None,
7405 0,
7406 true, );
7408
7409 let boxed: Box<dyn BarAggregator> = Box::new(aggregator);
7410 let rc = Rc::new(RefCell::new(boxed));
7411 rc.borrow_mut().start_timer(Some(Rc::clone(&rc)));
7412
7413 rc.borrow_mut().update(
7414 Price::from("100.00"),
7415 Quantity::from(1),
7416 UnixNanos::from(now_ns),
7417 );
7418 let next_close = now_ns + interval_ns;
7419 rc.borrow_mut().build_bar(&TimeEvent::new(
7420 event_name,
7421 UUID4::new(),
7422 UnixNanos::from(next_close),
7423 UnixNanos::from(next_close),
7424 ));
7425
7426 let bars = handler.lock().expect(MUTEX_POISONED);
7427 prop_assert_eq!(bars.len(), 1);
7428 prop_assert_eq!(bars[0].close, Price::from("100.00"));
7429 }
7430
7431 #[rstest]
7432 fn prop_bar_builder_ohlc_invariants(
7433 updates in prop::collection::vec((1i64..=100_000i64, 1u64..=1_000u64), 1..=50),
7434 ) {
7435 let instrument = InstrumentAny::Equity(equity_aapl());
7436 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
7437 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7438 let mut builder = BarBuilder::new(bar_type, 2, 0);
7439
7440 let mut total_volume: u64 = 0;
7441
7442 for (i, (price_cents, size)) in updates.iter().enumerate() {
7443 let price = Price::new((*price_cents as f64) / 100.0, 2);
7444 let qty = Quantity::new(*size as f64, 0);
7445 let ts = UnixNanos::from((i as u64 + 1) * 1_000);
7446 total_volume += *size;
7447 builder.update(price, qty, ts);
7448 }
7449
7450 let bar = builder.build_now();
7451 prop_assert!(bar.low <= bar.open);
7452 prop_assert!(bar.low <= bar.close);
7453 prop_assert!(bar.high >= bar.open);
7454 prop_assert!(bar.high >= bar.close);
7455 prop_assert!(bar.low <= bar.high);
7456 prop_assert_eq!(bar.volume.as_f64(), total_volume as f64);
7457 }
7458
7459 #[rstest]
7460 fn prop_tick_bar_aggregator_volume_conservation(
7461 ticks in prop::collection::vec((1i64..=1_000i64, 1u64..=100u64), 3..=60),
7462 step in 1usize..=5,
7463 ) {
7464 let instrument = InstrumentAny::Equity(equity_aapl());
7465 let bar_spec = BarSpecification::new(step, BarAggregation::Tick, PriceType::Last);
7466 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7467 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
7468 let handler_clone = Arc::clone(&handler);
7469
7470 let mut aggregator = TickBarAggregator::new(
7471 bar_type,
7472 instrument.price_precision(),
7473 instrument.size_precision(),
7474 move |bar: Bar| {
7475 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
7476 },
7477 );
7478
7479 let mut total_input: u64 = 0;
7480
7481 for (i, (price_cents, size)) in ticks.iter().enumerate() {
7482 let price = Price::new((*price_cents as f64) / 100.0, 2);
7483 let qty = Quantity::new(*size as f64, 0);
7484 aggregator.update(price, qty, UnixNanos::from((i as u64 + 1) * 1_000));
7485 total_input += *size;
7486 }
7487
7488 let bars = handler.lock().expect(MUTEX_POISONED);
7489 let emitted_count = bars.len();
7490 prop_assert_eq!(emitted_count, ticks.len() / step);
7491
7492 let mut sum_emitted: f64 = 0.0;
7493
7494 for bar in bars.iter() {
7495 prop_assert!(bar.low <= bar.open);
7496 prop_assert!(bar.low <= bar.close);
7497 prop_assert!(bar.high >= bar.open);
7498 prop_assert!(bar.high >= bar.close);
7499 sum_emitted += bar.volume.as_f64();
7500 }
7501
7502 let pending_size: u64 = ticks.iter()
7504 .skip(emitted_count * step)
7505 .map(|(_, s)| *s)
7506 .sum();
7507 prop_assert!((sum_emitted + pending_size as f64 - total_input as f64).abs() < 1e-6);
7508 }
7509
7510 #[rstest]
7511 fn prop_volume_bar_aggregator_conservation(
7512 sizes in prop::collection::vec(1u64..=50u64, 3..=40),
7513 step in 2u64..=10u64,
7514 ) {
7515 let instrument = InstrumentAny::Equity(equity_aapl());
7516 let bar_spec = BarSpecification::new(step as usize, BarAggregation::Volume, PriceType::Last);
7517 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7518 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
7519 let handler_clone = Arc::clone(&handler);
7520
7521 let mut aggregator = VolumeBarAggregator::new(
7522 bar_type,
7523 instrument.price_precision(),
7524 instrument.size_precision(),
7525 move |bar: Bar| {
7526 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
7527 },
7528 );
7529
7530 let mut total_input: u64 = 0;
7531
7532 for (i, size) in sizes.iter().enumerate() {
7533 aggregator.update(
7534 Price::from("100.00"),
7535 Quantity::new(*size as f64, 0),
7536 UnixNanos::from((i as u64 + 1) * 1_000),
7537 );
7538 total_input += *size;
7539 }
7540
7541 let bars = handler.lock().expect(MUTEX_POISONED);
7542
7543 for bar in bars.iter() {
7545 prop_assert_eq!(bar.volume, Quantity::from(step));
7546 prop_assert!(bar.low <= bar.open);
7547 prop_assert!(bar.low <= bar.close);
7548 prop_assert!(bar.high >= bar.open);
7549 prop_assert!(bar.high >= bar.close);
7550 }
7551
7552 let emitted_total: u64 = bars.len() as u64 * step;
7554 let pending = aggregator.core.builder.volume.as_f64();
7555 prop_assert!((emitted_total as f64 + pending - total_input as f64).abs() < 1e-6);
7556 }
7557
7558 #[rstest]
7559 fn prop_bar_builder_spread_adjustment_is_additive(
7560 updates in prop::collection::vec((10_000i64..=100_000i64, 1u64..=100u64), 1..=20),
7561 spread_cents in -10_000i64..=10_000i64,
7562 backward in any::<bool>(),
7563 ) {
7564 let instrument = InstrumentAny::Equity(equity_aapl());
7565 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
7566 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7567 let mut builder = BarBuilder::new(bar_type, 2, 0);
7568
7569 let spread = Decimal::new(spread_cents, 2);
7570 let mode = if backward {
7571 ContinuousFutureAdjustmentType::BackwardSpread
7572 } else {
7573 ContinuousFutureAdjustmentType::ForwardSpread
7574 };
7575 builder.set_adjustment(spread, mode);
7576
7577 let mut min_cents = i64::MAX;
7578 let mut max_cents = i64::MIN;
7579
7580 for (i, (price_cents, size)) in updates.iter().enumerate() {
7581 if *price_cents < min_cents {
7582 min_cents = *price_cents;
7583 }
7584
7585 if *price_cents > max_cents {
7586 max_cents = *price_cents;
7587 }
7588
7589 builder.update(
7590 Price::new((*price_cents as f64) / 100.0, 2),
7591 Quantity::new(*size as f64, 0),
7592 UnixNanos::from((i as u64 + 1) * 1_000),
7593 );
7594 }
7595
7596 let bar = builder.build_now();
7597 let first_decimal = Decimal::new(updates.first().unwrap().0, 2);
7598 let last_decimal = Decimal::new(updates.last().unwrap().0, 2);
7599 let min_decimal = Decimal::new(min_cents, 2);
7600 let max_decimal = Decimal::new(max_cents, 2);
7601
7602 prop_assert_eq!(bar.open.as_decimal(), first_decimal + spread);
7603 prop_assert_eq!(bar.close.as_decimal(), last_decimal + spread);
7604 prop_assert_eq!(bar.low.as_decimal(), min_decimal + spread);
7605 prop_assert_eq!(bar.high.as_decimal(), max_decimal + spread);
7606 }
7607
7608 #[rstest]
7609 fn prop_bar_builder_inactive_adjustment_is_identity(
7610 updates in prop::collection::vec((1i64..=100_000i64, 1u64..=1_000u64), 1..=20),
7611 use_ratio in any::<bool>(),
7612 ) {
7613 let instrument = InstrumentAny::Equity(equity_aapl());
7614 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
7615 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7616
7617 let mut adjusted = BarBuilder::new(bar_type, 2, 0);
7618 let mut baseline = BarBuilder::new(bar_type, 2, 0);
7619
7620 let (input, mode) = if use_ratio {
7622 (Decimal::ONE, ContinuousFutureAdjustmentType::BackwardRatio)
7623 } else {
7624 (Decimal::ZERO, ContinuousFutureAdjustmentType::BackwardSpread)
7625 };
7626 adjusted.set_adjustment(input, mode);
7627
7628 for (i, (price_cents, size)) in updates.iter().enumerate() {
7629 let price = Price::new((*price_cents as f64) / 100.0, 2);
7630 let qty = Quantity::new(*size as f64, 0);
7631 let ts = UnixNanos::from((i as u64 + 1) * 1_000);
7632 adjusted.update(price, qty, ts);
7633 baseline.update(price, qty, ts);
7634 }
7635
7636 let bar_adjusted = adjusted.build_now();
7637 let bar_baseline = baseline.build_now();
7638 prop_assert_eq!(bar_adjusted.open, bar_baseline.open);
7639 prop_assert_eq!(bar_adjusted.high, bar_baseline.high);
7640 prop_assert_eq!(bar_adjusted.low, bar_baseline.low);
7641 prop_assert_eq!(bar_adjusted.close, bar_baseline.close);
7642 prop_assert_eq!(bar_adjusted.volume, bar_baseline.volume);
7643 }
7644
7645 #[rstest]
7646 fn prop_bar_builder_spread_preserves_raw_arithmetic(
7647 updates in prop::collection::vec((10_000i64..=100_000i64, 1u64..=100u64), 1..=20),
7648 spread_micro in -10_000i64..=10_000i64,
7651 ) {
7652 let instrument = InstrumentAny::Equity(equity_aapl());
7653 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
7654 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7655 let mut builder = BarBuilder::new(bar_type, 2, 0);
7656
7657 let spread = Decimal::new(spread_micro, 4);
7658 builder.set_adjustment(spread, ContinuousFutureAdjustmentType::BackwardSpread);
7659
7660 let adjustment_raw_i128 = mantissa_exponent_to_fixed_i128(
7661 spread.mantissa(),
7662 -(spread.scale() as i8),
7663 FIXED_PRECISION,
7664 )
7665 .expect("scale within range");
7666 #[allow(
7667 clippy::useless_conversion,
7668 reason = "i128 to PriceRaw is real when not high-precision"
7669 )]
7670 let expected_adjustment_raw: PriceRaw =
7671 adjustment_raw_i128.try_into().expect("within PriceRaw range");
7672
7673 let mut min_cents = i64::MAX;
7674 let mut max_cents = i64::MIN;
7675 let mut last_price = Price::new(0.0, 2);
7676 let mut first_price = Price::new(0.0, 2);
7677
7678 for (i, (price_cents, size)) in updates.iter().enumerate() {
7679 if *price_cents < min_cents {
7680 min_cents = *price_cents;
7681 }
7682
7683 if *price_cents > max_cents {
7684 max_cents = *price_cents;
7685 }
7686
7687 let price = Price::new((*price_cents as f64) / 100.0, 2);
7688
7689 if i == 0 {
7690 first_price = price;
7691 }
7692
7693 last_price = price;
7694 builder.update(
7695 price,
7696 Quantity::new(*size as f64, 0),
7697 UnixNanos::from((i as u64 + 1) * 1_000),
7698 );
7699 }
7700
7701 let bar = builder.build_now();
7702 let min_price = Price::new((min_cents as f64) / 100.0, 2);
7703 let max_price = Price::new((max_cents as f64) / 100.0, 2);
7704 prop_assert_eq!(bar.open.raw, first_price.raw + expected_adjustment_raw);
7705 prop_assert_eq!(bar.close.raw, last_price.raw + expected_adjustment_raw);
7706 prop_assert_eq!(bar.low.raw, min_price.raw + expected_adjustment_raw);
7707 prop_assert_eq!(bar.high.raw, max_price.raw + expected_adjustment_raw);
7708 prop_assert_eq!(bar.open.precision, 2);
7709 prop_assert_eq!(bar.high.precision, 2);
7710 prop_assert_eq!(bar.low.precision, 2);
7711 prop_assert_eq!(bar.close.precision, 2);
7712 }
7713
7714 #[rstest]
7715 fn prop_bar_builder_active_ratio_scales_each_ohlc(
7716 updates in prop::collection::vec((1_000i64..=100_000i64, 1u64..=100u64), 1..=20),
7717 ratio_centi in prop_oneof![50i64..=99i64, 101i64..=200i64],
7719 backward in any::<bool>(),
7720 ) {
7721 let instrument = InstrumentAny::Equity(equity_aapl());
7722 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
7723 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7724 let mut builder = BarBuilder::new(bar_type, 2, 0);
7725
7726 let ratio_decimal = Decimal::new(ratio_centi, 2);
7727 let ratio_f64 = (ratio_centi as f64) / 100.0;
7728 let mode = if backward {
7729 ContinuousFutureAdjustmentType::BackwardRatio
7730 } else {
7731 ContinuousFutureAdjustmentType::ForwardRatio
7732 };
7733 builder.set_adjustment(ratio_decimal, mode);
7734
7735 let mut min_cents = i64::MAX;
7736 let mut max_cents = i64::MIN;
7737 let mut first_cents = 0i64;
7738 let mut last_cents = 0i64;
7739
7740 for (i, (price_cents, size)) in updates.iter().enumerate() {
7741 if *price_cents < min_cents {
7742 min_cents = *price_cents;
7743 }
7744
7745 if *price_cents > max_cents {
7746 max_cents = *price_cents;
7747 }
7748
7749 if i == 0 {
7750 first_cents = *price_cents;
7751 }
7752
7753 last_cents = *price_cents;
7754 builder.update(
7755 Price::new((*price_cents as f64) / 100.0, 2),
7756 Quantity::new(*size as f64, 0),
7757 UnixNanos::from((i as u64 + 1) * 1_000),
7758 );
7759 }
7760
7761 let bar = builder.build_now();
7762 let expect = |cents: i64| Price::new((cents as f64) / 100.0 * ratio_f64, 2);
7764 prop_assert_eq!(bar.open, expect(first_cents));
7765 prop_assert_eq!(bar.close, expect(last_cents));
7766 prop_assert_eq!(bar.low, expect(min_cents));
7768 prop_assert_eq!(bar.high, expect(max_cents));
7769 }
7770
7771 #[rstest]
7772 fn prop_bar_builder_spread_mode_direction_is_metadata_only(
7773 updates in prop::collection::vec((10_000i64..=100_000i64, 1u64..=100u64), 1..=20),
7774 spread_cents in -10_000i64..=10_000i64,
7775 ) {
7776 let instrument = InstrumentAny::Equity(equity_aapl());
7777 let bar_spec = BarSpecification::new(1, BarAggregation::Tick, PriceType::Last);
7778 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7779
7780 let spread = Decimal::new(spread_cents, 2);
7781 let mut backward = BarBuilder::new(bar_type, 2, 0);
7782 let mut forward = BarBuilder::new(bar_type, 2, 0);
7783 backward.set_adjustment(spread, ContinuousFutureAdjustmentType::BackwardSpread);
7784 forward.set_adjustment(spread, ContinuousFutureAdjustmentType::ForwardSpread);
7785
7786 for (i, (price_cents, size)) in updates.iter().enumerate() {
7787 let price = Price::new((*price_cents as f64) / 100.0, 2);
7788 let qty = Quantity::new(*size as f64, 0);
7789 let ts = UnixNanos::from((i as u64 + 1) * 1_000);
7790 backward.update(price, qty, ts);
7791 forward.update(price, qty, ts);
7792 }
7793
7794 let bar_backward = backward.build_now();
7795 let bar_forward = forward.build_now();
7796 prop_assert_eq!(bar_backward.open, bar_forward.open);
7797 prop_assert_eq!(bar_backward.high, bar_forward.high);
7798 prop_assert_eq!(bar_backward.low, bar_forward.low);
7799 prop_assert_eq!(bar_backward.close, bar_forward.close);
7800 }
7801
7802 #[rstest]
7803 fn prop_value_bar_aggregator_ohlc_invariants(
7804 ticks in prop::collection::vec((50i64..=500i64, 1u64..=20u64), 2..=30),
7805 step in 100u64..=2_000u64,
7806 ) {
7807 let instrument = InstrumentAny::Equity(equity_aapl());
7808 let bar_spec = BarSpecification::new(step as usize, BarAggregation::Value, PriceType::Last);
7809 let bar_type = BarType::new(instrument.id(), bar_spec, AggregationSource::Internal);
7810 let handler = Arc::new(Mutex::new(Vec::<Bar>::new()));
7811 let handler_clone = Arc::clone(&handler);
7812
7813 let mut aggregator = ValueBarAggregator::new(
7814 bar_type,
7815 instrument.price_precision(),
7816 instrument.size_precision(),
7817 move |bar: Bar| {
7818 handler_clone.lock().expect(MUTEX_POISONED).push(bar);
7819 },
7820 );
7821
7822 for (i, (price_cents, size)) in ticks.iter().enumerate() {
7823 aggregator.update(
7824 Price::new((*price_cents as f64) / 100.0, 2),
7825 Quantity::new(*size as f64, 0),
7826 UnixNanos::from((i as u64 + 1) * 1_000),
7827 );
7828 }
7829
7830 let bars = handler.lock().expect(MUTEX_POISONED);
7831 for bar in bars.iter() {
7832 prop_assert!(bar.low <= bar.open);
7833 prop_assert!(bar.low <= bar.close);
7834 prop_assert!(bar.high >= bar.open);
7835 prop_assert!(bar.high >= bar.close);
7836 prop_assert!(bar.volume.as_f64() > 0.0);
7837 }
7838 }
7839 }
7840}