1use nautilus_model::{enums::OrderSide, identifiers::VenueOrderId, instruments::InstrumentAny};
19use rust_decimal::Decimal;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct FillSnapshot {
24 pub ts_event: u64,
26 pub side: OrderSide,
28 pub qty: Decimal,
30 pub px: Decimal,
32 pub venue_order_id: VenueOrderId,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct VenuePositionSnapshot {
39 pub side: OrderSide, pub qty: Decimal,
43 pub avg_px: Decimal,
45}
46
47#[derive(Debug, Clone, PartialEq)]
49pub enum FillAdjustmentResult {
50 NoAdjustment,
52 AddSyntheticOpening {
54 synthetic_fill: FillSnapshot,
56 existing_fills: Vec<FillSnapshot>,
58 },
59 ReplaceCurrentLifecycle {
61 synthetic_fill: FillSnapshot,
63 first_venue_order_id: VenueOrderId,
65 },
66 FilterToCurrentLifecycle {
68 last_zero_crossing_ts: u64,
70 current_lifecycle_fills: Vec<FillSnapshot>,
72 },
73}
74
75impl FillSnapshot {
76 #[must_use]
78 pub fn new(
79 ts_event: u64,
80 side: OrderSide,
81 qty: Decimal,
82 px: Decimal,
83 venue_order_id: VenueOrderId,
84 ) -> Self {
85 Self {
86 ts_event,
87 side,
88 qty,
89 px,
90 venue_order_id,
91 }
92 }
93
94 #[must_use]
96 pub fn direction(&self) -> i8 {
97 match self.side {
98 OrderSide::Buy => 1,
99 OrderSide::Sell => -1,
100 _ => 0,
101 }
102 }
103}
104
105#[must_use]
111pub fn simulate_position(fills: &[FillSnapshot]) -> (Decimal, Decimal) {
112 let mut qty = Decimal::ZERO;
113 let mut value = Decimal::ZERO;
114
115 for fill in fills {
116 let direction = Decimal::from(fill.direction());
117 let new_qty = qty + (direction * fill.qty);
118
119 if (qty >= Decimal::ZERO && direction > Decimal::ZERO)
121 || (qty <= Decimal::ZERO && direction < Decimal::ZERO)
122 {
123 value += fill.qty * fill.px;
125 qty = new_qty;
126 } else {
127 if qty.abs() >= fill.qty {
129 let close_ratio = fill.qty / qty.abs();
131 value *= Decimal::ONE - close_ratio;
132 qty = new_qty;
133 } else {
134 let remaining = fill.qty - qty.abs();
136 qty = direction * remaining;
137 value = remaining * fill.px;
138 }
139 }
140 }
141
142 (qty, value)
143}
144
145#[must_use]
154pub fn detect_zero_crossings(fills: &[FillSnapshot]) -> Vec<u64> {
155 let mut running_qty = Decimal::ZERO;
156 let mut zero_crossings = Vec::new();
157
158 for fill in fills {
159 let prev_qty = running_qty;
160 running_qty += Decimal::from(fill.direction()) * fill.qty;
161
162 if prev_qty != Decimal::ZERO {
164 if running_qty == Decimal::ZERO {
165 zero_crossings.push(fill.ts_event);
167 } else if (prev_qty > Decimal::ZERO) != (running_qty > Decimal::ZERO) {
168 zero_crossings.push(fill.ts_event);
170 }
171 }
172 }
173
174 zero_crossings
175}
176
177#[must_use]
183pub fn check_position_match(
184 simulated_qty: Decimal,
185 simulated_value: Decimal,
186 venue_qty: Decimal,
187 venue_avg_px: Decimal,
188 tolerance: Decimal,
189) -> bool {
190 if simulated_qty != venue_qty {
191 return false;
192 }
193
194 if simulated_qty == Decimal::ZERO {
195 return true; }
197
198 let abs_qty = simulated_qty.abs();
200 if abs_qty == Decimal::ZERO {
201 return false;
202 }
203
204 let simulated_avg_px = simulated_value / abs_qty;
205
206 if venue_avg_px == Decimal::ZERO {
208 return false;
209 }
210
211 let relative_diff = (simulated_avg_px - venue_avg_px).abs() / venue_avg_px;
212
213 relative_diff <= tolerance
214}
215
216pub fn calculate_reconciliation_price(
234 current_position_qty: Decimal,
235 current_position_avg_px: Option<Decimal>,
236 target_position_qty: Decimal,
237 target_position_avg_px: Option<Decimal>,
238) -> Option<Decimal> {
239 let qty_diff = target_position_qty - current_position_qty;
241
242 if qty_diff == Decimal::ZERO {
243 return None; }
245
246 if target_position_qty == Decimal::ZERO {
249 return current_position_avg_px;
250 }
251
252 let target_avg_px = target_position_avg_px?;
254 if target_avg_px == Decimal::ZERO {
255 return None;
256 }
257
258 if current_position_qty == Decimal::ZERO || current_position_avg_px.is_none() {
260 return Some(target_avg_px);
261 }
262
263 let current_avg_px = current_position_avg_px?;
264
265 let is_flip = (current_position_qty > Decimal::ZERO) != (target_position_qty > Decimal::ZERO)
268 && target_position_qty != Decimal::ZERO;
269
270 if is_flip {
271 return Some(target_avg_px);
272 }
273
274 let target_value = target_position_qty * target_avg_px;
277 let current_value = current_position_qty * current_avg_px;
278 let diff_value = target_value - current_value;
279
280 let reconciliation_px = diff_value / qty_diff;
282
283 if reconciliation_px > Decimal::ZERO {
285 return Some(reconciliation_px);
286 }
287
288 None
289}
290
291#[must_use]
304pub fn adjust_fills_for_partial_window(
305 fills: &[FillSnapshot],
306 venue_position: &VenuePositionSnapshot,
307 _instrument: &InstrumentAny,
308 tolerance: Decimal,
309) -> FillAdjustmentResult {
310 if fills.is_empty() {
312 return FillAdjustmentResult::NoAdjustment;
313 }
314
315 if venue_position.qty == Decimal::ZERO {
317 return FillAdjustmentResult::NoAdjustment;
318 }
319
320 let zero_crossings = detect_zero_crossings(fills);
322
323 let venue_qty_signed = match venue_position.side {
325 OrderSide::Buy => venue_position.qty,
326 OrderSide::Sell => -venue_position.qty,
327 _ => Decimal::ZERO,
328 };
329
330 if !zero_crossings.is_empty() {
332 let mut last_flat_crossing_ts = None;
335 let mut running_qty = Decimal::ZERO;
336
337 for fill in fills {
338 let prev_qty = running_qty;
339 running_qty += Decimal::from(fill.direction()) * fill.qty;
340
341 if prev_qty != Decimal::ZERO && running_qty == Decimal::ZERO {
342 last_flat_crossing_ts = Some(fill.ts_event);
343 }
344 }
345
346 let lifecycle_boundary_ts =
347 last_flat_crossing_ts.unwrap_or(*zero_crossings.last().unwrap());
348
349 let current_lifecycle_fills: Vec<FillSnapshot> = fills
351 .iter()
352 .filter(|f| f.ts_event > lifecycle_boundary_ts)
353 .cloned()
354 .collect();
355
356 if current_lifecycle_fills.is_empty() {
357 return FillAdjustmentResult::NoAdjustment;
358 }
359
360 let (current_qty, current_value) = simulate_position(¤t_lifecycle_fills);
362
363 if check_position_match(
365 current_qty,
366 current_value,
367 venue_qty_signed,
368 venue_position.avg_px,
369 tolerance,
370 ) {
371 return FillAdjustmentResult::FilterToCurrentLifecycle {
373 last_zero_crossing_ts: lifecycle_boundary_ts,
374 current_lifecycle_fills,
375 };
376 }
377
378 if let Some(first_fill) = current_lifecycle_fills.first() {
380 let synthetic_fill = FillSnapshot::new(
381 first_fill.ts_event.saturating_sub(1), venue_position.side,
383 venue_position.qty,
384 venue_position.avg_px,
385 first_fill.venue_order_id,
386 );
387
388 return FillAdjustmentResult::ReplaceCurrentLifecycle {
389 synthetic_fill,
390 first_venue_order_id: first_fill.venue_order_id,
391 };
392 }
393
394 return FillAdjustmentResult::NoAdjustment;
395 }
396
397 let oldest_lifecycle_fills: Vec<FillSnapshot> =
400 if let Some(&first_zero_crossing_ts) = zero_crossings.first() {
401 fills
403 .iter()
404 .filter(|f| f.ts_event <= first_zero_crossing_ts)
405 .cloned()
406 .collect()
407 } else {
408 fills.to_vec()
410 };
411
412 if oldest_lifecycle_fills.is_empty() {
413 return FillAdjustmentResult::NoAdjustment;
414 }
415
416 let (oldest_qty, oldest_value) = simulate_position(&oldest_lifecycle_fills);
418
419 if zero_crossings.is_empty() {
421 if check_position_match(
423 oldest_qty,
424 oldest_value,
425 venue_qty_signed,
426 venue_position.avg_px,
427 tolerance,
428 ) {
429 return FillAdjustmentResult::NoAdjustment;
430 }
431
432 if let Some(first_fill) = oldest_lifecycle_fills.first() {
434 let oldest_avg_px = if oldest_qty != Decimal::ZERO {
437 Some(oldest_value / oldest_qty.abs())
438 } else {
439 None
440 };
441
442 let reconciliation_price = calculate_reconciliation_price(
443 oldest_qty,
444 oldest_avg_px,
445 venue_qty_signed,
446 Some(venue_position.avg_px),
447 );
448
449 if let Some(opening_px) = reconciliation_price {
450 let opening_qty = if oldest_qty != Decimal::ZERO {
452 venue_qty_signed - oldest_qty
454 } else {
455 venue_qty_signed
456 };
457
458 if opening_qty.abs() > Decimal::ZERO {
459 let synthetic_side = if opening_qty > Decimal::ZERO {
460 OrderSide::Buy
461 } else {
462 OrderSide::Sell
463 };
464
465 let synthetic_fill = FillSnapshot::new(
466 first_fill.ts_event.saturating_sub(1),
467 synthetic_side,
468 opening_qty.abs(),
469 opening_px,
470 first_fill.venue_order_id,
471 );
472
473 return FillAdjustmentResult::AddSyntheticOpening {
474 synthetic_fill,
475 existing_fills: oldest_lifecycle_fills,
476 };
477 }
478 }
479 }
480
481 return FillAdjustmentResult::NoAdjustment;
482 }
483
484 if oldest_qty == Decimal::ZERO {
486 return FillAdjustmentResult::NoAdjustment;
488 }
489
490 if !oldest_lifecycle_fills.is_empty()
492 && let Some(&first_zero_crossing_ts) = zero_crossings.first()
493 {
494 let current_lifecycle_fills: Vec<FillSnapshot> = fills
496 .iter()
497 .filter(|f| f.ts_event > first_zero_crossing_ts)
498 .cloned()
499 .collect();
500
501 if !current_lifecycle_fills.is_empty()
502 && let Some(first_current_fill) = current_lifecycle_fills.first()
503 {
504 let synthetic_fill = FillSnapshot::new(
505 first_current_fill.ts_event.saturating_sub(1),
506 venue_position.side,
507 venue_position.qty,
508 venue_position.avg_px,
509 first_current_fill.venue_order_id,
510 );
511
512 return FillAdjustmentResult::AddSyntheticOpening {
513 synthetic_fill,
514 existing_fills: oldest_lifecycle_fills,
515 };
516 }
517 }
518
519 FillAdjustmentResult::NoAdjustment
520}
521
522#[cfg(test)]
523mod tests {
524 use nautilus_model::instruments::stubs::audusd_sim;
525 use rstest::{fixture, rstest};
526 use rust_decimal_macros::dec;
527
528 use super::*;
529
530 #[fixture]
531 fn instrument() -> InstrumentAny {
532 InstrumentAny::CurrencyPair(audusd_sim())
533 }
534
535 fn create_test_venue_order_id(value: &str) -> VenueOrderId {
536 VenueOrderId::new(value)
537 }
538
539 #[rstest]
540 fn test_fill_snapshot_direction() {
541 let venue_order_id = create_test_venue_order_id("ORDER1");
542 let buy_fill = FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id);
543 assert_eq!(buy_fill.direction(), 1);
544
545 let sell_fill =
546 FillSnapshot::new(2000, OrderSide::Sell, dec!(10), dec!(100), venue_order_id);
547 assert_eq!(sell_fill.direction(), -1);
548 }
549
550 #[rstest]
551 fn test_simulate_position_accumulate_long() {
552 let venue_order_id = create_test_venue_order_id("ORDER1");
553 let fills = vec![
554 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
555 FillSnapshot::new(2000, OrderSide::Buy, dec!(5), dec!(102), venue_order_id),
556 ];
557
558 let (qty, value) = simulate_position(&fills);
559 assert_eq!(qty, dec!(15));
560 assert_eq!(value, dec!(1510)); }
562
563 #[rstest]
564 fn test_simulate_position_close_and_flip() {
565 let venue_order_id = create_test_venue_order_id("ORDER1");
566 let fills = vec![
567 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
568 FillSnapshot::new(2000, OrderSide::Sell, dec!(15), dec!(102), venue_order_id),
569 ];
570
571 let (qty, value) = simulate_position(&fills);
572 assert_eq!(qty, dec!(-5)); assert_eq!(value, dec!(510)); }
575
576 #[rstest]
577 fn test_simulate_position_partial_close() {
578 let venue_order_id = create_test_venue_order_id("ORDER1");
579 let fills = vec![
580 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
581 FillSnapshot::new(2000, OrderSide::Sell, dec!(5), dec!(102), venue_order_id),
582 ];
583
584 let (qty, value) = simulate_position(&fills);
585 assert_eq!(qty, dec!(5));
586 assert_eq!(value, dec!(500)); let avg_px = value / qty;
590 assert_eq!(avg_px, dec!(100));
591 }
592
593 #[rstest]
594 fn test_simulate_position_multiple_partial_closes() {
595 let venue_order_id = create_test_venue_order_id("ORDER1");
596 let fills = vec![
597 FillSnapshot::new(1000, OrderSide::Buy, dec!(100), dec!(10.0), venue_order_id),
598 FillSnapshot::new(2000, OrderSide::Sell, dec!(25), dec!(11.0), venue_order_id), FillSnapshot::new(3000, OrderSide::Sell, dec!(25), dec!(12.0), venue_order_id), ];
601
602 let (qty, value) = simulate_position(&fills);
603 assert_eq!(qty, dec!(50));
604 assert!((value - dec!(500)).abs() < dec!(0.01));
608
609 let avg_px = value / qty;
611 assert!((avg_px - dec!(10.0)).abs() < dec!(0.01));
612 }
613
614 #[rstest]
615 fn test_simulate_position_short_partial_close() {
616 let venue_order_id = create_test_venue_order_id("ORDER1");
617 let fills = vec![
618 FillSnapshot::new(1000, OrderSide::Sell, dec!(10), dec!(100), venue_order_id),
619 FillSnapshot::new(2000, OrderSide::Buy, dec!(5), dec!(98), venue_order_id), ];
621
622 let (qty, value) = simulate_position(&fills);
623 assert_eq!(qty, dec!(-5));
624 assert_eq!(value, dec!(500)); let avg_px = value / qty.abs();
628 assert_eq!(avg_px, dec!(100));
629 }
630
631 #[rstest]
632 fn test_detect_zero_crossings() {
633 let venue_order_id = create_test_venue_order_id("ORDER1");
634 let fills = vec![
635 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
636 FillSnapshot::new(2000, OrderSide::Sell, dec!(10), dec!(102), venue_order_id), FillSnapshot::new(3000, OrderSide::Buy, dec!(5), dec!(103), venue_order_id),
638 FillSnapshot::new(4000, OrderSide::Sell, dec!(5), dec!(104), venue_order_id), ];
640
641 let crossings = detect_zero_crossings(&fills);
642 assert_eq!(crossings.len(), 2);
643 assert_eq!(crossings[0], 2000);
644 assert_eq!(crossings[1], 4000);
645 }
646
647 #[rstest]
648 fn test_check_position_match_exact() {
649 let result = check_position_match(dec!(10), dec!(1000), dec!(10), dec!(100), dec!(0.0001));
650 assert!(result);
651 }
652
653 #[rstest]
654 fn test_check_position_match_within_tolerance() {
655 let result =
658 check_position_match(dec!(10), dec!(1000), dec!(10), dec!(100.005), dec!(0.0001));
659 assert!(result);
660 }
661
662 #[rstest]
663 fn test_check_position_match_qty_mismatch() {
664 let result = check_position_match(dec!(10), dec!(1000), dec!(11), dec!(100), dec!(0.0001));
665 assert!(!result);
666 }
667
668 #[rstest]
669 fn test_check_position_match_both_flat() {
670 let result = check_position_match(dec!(0), dec!(0), dec!(0), dec!(0), dec!(0.0001));
671 assert!(result);
672 }
673
674 #[rstest]
675 fn test_reconciliation_price_flat_to_long(_instrument: InstrumentAny) {
676 let result = calculate_reconciliation_price(dec!(0), None, dec!(10), Some(dec!(100)));
677 assert!(result.is_some());
678 assert_eq!(result.unwrap(), dec!(100));
679 }
680
681 #[rstest]
682 fn test_reconciliation_price_no_target_avg_px(_instrument: InstrumentAny) {
683 let result = calculate_reconciliation_price(dec!(5), Some(dec!(100)), dec!(10), None);
684 assert!(result.is_none());
685 }
686
687 #[rstest]
688 fn test_reconciliation_price_no_quantity_change(_instrument: InstrumentAny) {
689 let result =
690 calculate_reconciliation_price(dec!(10), Some(dec!(100)), dec!(10), Some(dec!(105)));
691 assert!(result.is_none());
692 }
693
694 #[rstest]
695 fn test_reconciliation_price_long_position_increase(_instrument: InstrumentAny) {
696 let result =
697 calculate_reconciliation_price(dec!(10), Some(dec!(100)), dec!(15), Some(dec!(102)));
698 assert!(result.is_some());
699 assert_eq!(result.unwrap(), dec!(106));
701 }
702
703 #[rstest]
704 fn test_reconciliation_price_flat_to_short(_instrument: InstrumentAny) {
705 let result = calculate_reconciliation_price(dec!(0), None, dec!(-10), Some(dec!(100)));
706 assert!(result.is_some());
707 assert_eq!(result.unwrap(), dec!(100));
708 }
709
710 #[rstest]
711 fn test_reconciliation_price_long_to_flat(_instrument: InstrumentAny) {
712 let result =
715 calculate_reconciliation_price(dec!(100), Some(dec!(1.20)), dec!(0), Some(dec!(0)));
716 assert!(result.is_some());
717 assert_eq!(result.unwrap(), dec!(1.20));
718 }
719
720 #[rstest]
721 fn test_reconciliation_price_short_to_flat(_instrument: InstrumentAny) {
722 let result = calculate_reconciliation_price(dec!(-50), Some(dec!(2.50)), dec!(0), None);
725 assert!(result.is_some());
726 assert_eq!(result.unwrap(), dec!(2.50));
727 }
728
729 #[rstest]
730 fn test_reconciliation_price_short_position_increase(_instrument: InstrumentAny) {
731 let result = calculate_reconciliation_price(
736 dec!(-100),
737 Some(dec!(1.30)),
738 dec!(-200),
739 Some(dec!(1.28)),
740 );
741 assert!(result.is_some());
742 assert_eq!(result.unwrap(), dec!(1.26));
743 }
744
745 #[rstest]
746 fn test_reconciliation_price_long_position_decrease(_instrument: InstrumentAny) {
747 let result = calculate_reconciliation_price(
749 dec!(200),
750 Some(dec!(1.20)),
751 dec!(100),
752 Some(dec!(1.20)),
753 );
754 assert!(result.is_some());
755 assert_eq!(result.unwrap(), dec!(1.20));
756 }
757
758 #[rstest]
759 fn test_reconciliation_price_long_to_short_flip(_instrument: InstrumentAny) {
760 let result = calculate_reconciliation_price(
763 dec!(100),
764 Some(dec!(1.20)),
765 dec!(-100),
766 Some(dec!(1.25)),
767 );
768 assert!(result.is_some());
769 assert_eq!(result.unwrap(), dec!(1.25));
770 }
771
772 #[rstest]
773 fn test_reconciliation_price_short_to_long_flip(_instrument: InstrumentAny) {
774 let result = calculate_reconciliation_price(
777 dec!(-100),
778 Some(dec!(1.30)),
779 dec!(100),
780 Some(dec!(1.25)),
781 );
782 assert!(result.is_some());
783 assert_eq!(result.unwrap(), dec!(1.25));
784 }
785
786 #[rstest]
787 fn test_reconciliation_price_complex_scenario(_instrument: InstrumentAny) {
788 let result = calculate_reconciliation_price(
793 dec!(150),
794 Some(dec!(1.23456)),
795 dec!(250),
796 Some(dec!(1.24567)),
797 );
798 assert!(result.is_some());
799 assert_eq!(result.unwrap(), dec!(1.262335));
800 }
801
802 #[rstest]
803 fn test_reconciliation_price_zero_target_avg_px(_instrument: InstrumentAny) {
804 let result =
805 calculate_reconciliation_price(dec!(100), Some(dec!(1.20)), dec!(200), Some(dec!(0)));
806 assert!(result.is_none());
807 }
808
809 #[rstest]
810 fn test_reconciliation_price_negative_price(_instrument: InstrumentAny) {
811 let result = calculate_reconciliation_price(
816 dec!(100),
817 Some(dec!(2.00)),
818 dec!(200),
819 Some(dec!(1.00)),
820 );
821 assert!(result.is_none());
822 }
823
824 #[rstest]
825 fn test_reconciliation_price_flip_simulation_compatibility() {
826 let venue_order_id = create_test_venue_order_id("ORDER1");
827 let recon_px = calculate_reconciliation_price(
831 dec!(100),
832 Some(dec!(1.20)),
833 dec!(-100),
834 Some(dec!(1.25)),
835 )
836 .expect("reconciliation price");
837
838 assert_eq!(recon_px, dec!(1.25));
839
840 let fills = vec![
842 FillSnapshot::new(1000, OrderSide::Buy, dec!(100), dec!(1.20), venue_order_id),
843 FillSnapshot::new(2000, OrderSide::Sell, dec!(200), recon_px, venue_order_id),
844 ];
845
846 let (final_qty, final_value) = simulate_position(&fills);
847 assert_eq!(final_qty, dec!(-100));
848 let final_avg = final_value / final_qty.abs();
849 assert_eq!(final_avg, dec!(1.25), "Final average should match target");
850 }
851
852 #[rstest]
853 fn test_reconciliation_price_accumulation_simulation_compatibility() {
854 let venue_order_id = create_test_venue_order_id("ORDER1");
855 let recon_px = calculate_reconciliation_price(
858 dec!(100),
859 Some(dec!(1.20)),
860 dec!(200),
861 Some(dec!(1.22)),
862 )
863 .expect("reconciliation price");
864
865 let fills = vec![
867 FillSnapshot::new(1000, OrderSide::Buy, dec!(100), dec!(1.20), venue_order_id),
868 FillSnapshot::new(2000, OrderSide::Buy, dec!(100), recon_px, venue_order_id),
869 ];
870
871 let (final_qty, final_value) = simulate_position(&fills);
872 assert_eq!(final_qty, dec!(200));
873 let final_avg = final_value / final_qty.abs();
874 assert_eq!(final_avg, dec!(1.22), "Final average should match target");
875 }
876
877 #[rstest]
878 fn test_simulate_position_accumulate_short() {
879 let venue_order_id = create_test_venue_order_id("ORDER1");
880 let fills = vec![
881 FillSnapshot::new(1000, OrderSide::Sell, dec!(10), dec!(100), venue_order_id),
882 FillSnapshot::new(2000, OrderSide::Sell, dec!(5), dec!(98), venue_order_id),
883 ];
884
885 let (qty, value) = simulate_position(&fills);
886 assert_eq!(qty, dec!(-15));
887 assert_eq!(value, dec!(1490)); }
889
890 #[rstest]
891 fn test_simulate_position_short_to_long_flip() {
892 let venue_order_id = create_test_venue_order_id("ORDER1");
893 let fills = vec![
894 FillSnapshot::new(1000, OrderSide::Sell, dec!(10), dec!(100), venue_order_id),
895 FillSnapshot::new(2000, OrderSide::Buy, dec!(15), dec!(102), venue_order_id),
896 ];
897
898 let (qty, value) = simulate_position(&fills);
899 assert_eq!(qty, dec!(5)); assert_eq!(value, dec!(510)); }
902
903 #[rstest]
904 fn test_simulate_position_multiple_flips() {
905 let venue_order_id = create_test_venue_order_id("ORDER1");
906 let fills = vec![
907 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
908 FillSnapshot::new(2000, OrderSide::Sell, dec!(15), dec!(105), venue_order_id), FillSnapshot::new(3000, OrderSide::Buy, dec!(10), dec!(110), venue_order_id), ];
911
912 let (qty, value) = simulate_position(&fills);
913 assert_eq!(qty, dec!(5)); assert_eq!(value, dec!(550)); }
916
917 #[rstest]
918 fn test_simulate_position_empty_fills() {
919 let fills: Vec<FillSnapshot> = vec![];
920 let (qty, value) = simulate_position(&fills);
921 assert_eq!(qty, dec!(0));
922 assert_eq!(value, dec!(0));
923 }
924
925 #[rstest]
926 fn test_detect_zero_crossings_no_crossings() {
927 let venue_order_id = create_test_venue_order_id("ORDER1");
928 let fills = vec![
929 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
930 FillSnapshot::new(2000, OrderSide::Buy, dec!(5), dec!(102), venue_order_id),
931 ];
932
933 let crossings = detect_zero_crossings(&fills);
934 assert_eq!(crossings.len(), 0);
935 }
936
937 #[rstest]
938 fn test_detect_zero_crossings_single_crossing() {
939 let venue_order_id = create_test_venue_order_id("ORDER1");
940 let fills = vec![
941 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
942 FillSnapshot::new(2000, OrderSide::Sell, dec!(10), dec!(102), venue_order_id), ];
944
945 let crossings = detect_zero_crossings(&fills);
946 assert_eq!(crossings.len(), 1);
947 assert_eq!(crossings[0], 2000);
948 }
949
950 #[rstest]
951 fn test_detect_zero_crossings_empty_fills() {
952 let fills: Vec<FillSnapshot> = vec![];
953 let crossings = detect_zero_crossings(&fills);
954 assert_eq!(crossings.len(), 0);
955 }
956
957 #[rstest]
958 fn test_detect_zero_crossings_long_to_short_flip() {
959 let venue_order_id = create_test_venue_order_id("ORDER1");
960 let fills = vec![
962 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
963 FillSnapshot::new(2000, OrderSide::Sell, dec!(15), dec!(102), venue_order_id), ];
965
966 let crossings = detect_zero_crossings(&fills);
967 assert_eq!(crossings.len(), 1);
968 assert_eq!(crossings[0], 2000); }
970
971 #[rstest]
972 fn test_detect_zero_crossings_short_to_long_flip() {
973 let venue_order_id = create_test_venue_order_id("ORDER1");
974 let fills = vec![
976 FillSnapshot::new(1000, OrderSide::Sell, dec!(10), dec!(100), venue_order_id),
977 FillSnapshot::new(2000, OrderSide::Buy, dec!(20), dec!(102), venue_order_id), ];
979
980 let crossings = detect_zero_crossings(&fills);
981 assert_eq!(crossings.len(), 1);
982 assert_eq!(crossings[0], 2000);
983 }
984
985 #[rstest]
986 fn test_detect_zero_crossings_multiple_flips() {
987 let venue_order_id = create_test_venue_order_id("ORDER1");
988 let fills = vec![
989 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
990 FillSnapshot::new(2000, OrderSide::Sell, dec!(10), dec!(102), venue_order_id), FillSnapshot::new(3000, OrderSide::Sell, dec!(5), dec!(103), venue_order_id), FillSnapshot::new(4000, OrderSide::Buy, dec!(15), dec!(104), venue_order_id), ];
994
995 let crossings = detect_zero_crossings(&fills);
996 assert_eq!(crossings.len(), 2);
997 assert_eq!(crossings[0], 2000); assert_eq!(crossings[1], 4000); }
1000
1001 #[rstest]
1002 fn test_check_position_match_outside_tolerance() {
1003 let result = check_position_match(dec!(10), dec!(1000), dec!(10), dec!(101), dec!(0.0001));
1006 assert!(!result);
1007 }
1008
1009 #[rstest]
1010 fn test_check_position_match_edge_of_tolerance() {
1011 let result =
1014 check_position_match(dec!(10), dec!(1000), dec!(10), dec!(100.01), dec!(0.0001));
1015 assert!(result);
1016 }
1017
1018 #[rstest]
1019 fn test_check_position_match_zero_venue_avg_px() {
1020 let result = check_position_match(dec!(10), dec!(1000), dec!(10), dec!(0), dec!(0.0001));
1021 assert!(!result); }
1023
1024 #[rstest]
1025 fn test_adjust_fills_no_fills() {
1026 let venue_position = VenuePositionSnapshot {
1027 side: OrderSide::Buy,
1028 qty: dec!(0.02),
1029 avg_px: dec!(4100.00),
1030 };
1031 let instrument = instrument();
1032 let result =
1033 adjust_fills_for_partial_window(&[], &venue_position, &instrument, dec!(0.0001));
1034 assert!(matches!(result, FillAdjustmentResult::NoAdjustment));
1035 }
1036
1037 #[rstest]
1038 fn test_adjust_fills_flat_position() {
1039 let venue_order_id = create_test_venue_order_id("ORDER1");
1040 let fills = vec![FillSnapshot::new(
1041 1000,
1042 OrderSide::Buy,
1043 dec!(0.01),
1044 dec!(4100.00),
1045 venue_order_id,
1046 )];
1047 let venue_position = VenuePositionSnapshot {
1048 side: OrderSide::Buy,
1049 qty: dec!(0),
1050 avg_px: dec!(0),
1051 };
1052 let instrument = instrument();
1053 let result =
1054 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1055 assert!(matches!(result, FillAdjustmentResult::NoAdjustment));
1056 }
1057
1058 #[rstest]
1059 fn test_adjust_fills_complete_lifecycle_no_adjustment() {
1060 let venue_order_id = create_test_venue_order_id("ORDER1");
1061 let venue_order_id2 = create_test_venue_order_id("ORDER2");
1062 let fills = vec![
1063 FillSnapshot::new(
1064 1000,
1065 OrderSide::Buy,
1066 dec!(0.01),
1067 dec!(4100.00),
1068 venue_order_id,
1069 ),
1070 FillSnapshot::new(
1071 2000,
1072 OrderSide::Buy,
1073 dec!(0.01),
1074 dec!(4100.00),
1075 venue_order_id2,
1076 ),
1077 ];
1078 let venue_position = VenuePositionSnapshot {
1079 side: OrderSide::Buy,
1080 qty: dec!(0.02),
1081 avg_px: dec!(4100.00),
1082 };
1083 let instrument = instrument();
1084 let result =
1085 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1086 assert!(matches!(result, FillAdjustmentResult::NoAdjustment));
1087 }
1088
1089 #[rstest]
1090 fn test_adjust_fills_incomplete_lifecycle_adds_synthetic() {
1091 let venue_order_id = create_test_venue_order_id("ORDER1");
1092 let fills = vec![FillSnapshot::new(
1094 2000,
1095 OrderSide::Buy,
1096 dec!(0.02),
1097 dec!(4200.00),
1098 venue_order_id,
1099 )];
1100 let venue_position = VenuePositionSnapshot {
1101 side: OrderSide::Buy,
1102 qty: dec!(0.04),
1103 avg_px: dec!(4100.00),
1104 };
1105 let instrument = instrument();
1106 let result =
1107 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1108
1109 match result {
1110 FillAdjustmentResult::AddSyntheticOpening {
1111 synthetic_fill,
1112 existing_fills,
1113 } => {
1114 assert_eq!(synthetic_fill.side, OrderSide::Buy);
1115 assert_eq!(synthetic_fill.qty, dec!(0.02)); assert_eq!(existing_fills.len(), 1);
1117 }
1118 _ => panic!("Expected AddSyntheticOpening"),
1119 }
1120 }
1121
1122 #[rstest]
1123 fn test_adjust_fills_with_zero_crossings() {
1124 let venue_order_id1 = create_test_venue_order_id("ORDER1");
1125 let venue_order_id2 = create_test_venue_order_id("ORDER2");
1126 let venue_order_id3 = create_test_venue_order_id("ORDER3");
1127
1128 let fills = vec![
1131 FillSnapshot::new(
1132 1000,
1133 OrderSide::Buy,
1134 dec!(0.02),
1135 dec!(4100.00),
1136 venue_order_id1,
1137 ),
1138 FillSnapshot::new(
1139 2000,
1140 OrderSide::Sell,
1141 dec!(0.02),
1142 dec!(4150.00),
1143 venue_order_id2,
1144 ), FillSnapshot::new(
1146 3000,
1147 OrderSide::Buy,
1148 dec!(0.03),
1149 dec!(4200.00),
1150 venue_order_id3,
1151 ), ];
1153
1154 let venue_position = VenuePositionSnapshot {
1155 side: OrderSide::Buy,
1156 qty: dec!(0.03),
1157 avg_px: dec!(4200.00),
1158 };
1159
1160 let instrument = instrument();
1161 let result =
1162 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1163
1164 match result {
1166 FillAdjustmentResult::FilterToCurrentLifecycle {
1167 last_zero_crossing_ts,
1168 current_lifecycle_fills,
1169 } => {
1170 assert_eq!(last_zero_crossing_ts, 2000);
1171 assert_eq!(current_lifecycle_fills.len(), 1);
1172 assert_eq!(current_lifecycle_fills[0].venue_order_id, venue_order_id3);
1173 }
1174 _ => panic!("Expected FilterToCurrentLifecycle, was {result:?}"),
1175 }
1176 }
1177
1178 #[rstest]
1179 fn test_adjust_fills_multiple_zero_crossings_mismatch() {
1180 let venue_order_id1 = create_test_venue_order_id("ORDER1");
1181 let venue_order_id2 = create_test_venue_order_id("ORDER2");
1182 let _venue_order_id3 = create_test_venue_order_id("ORDER3");
1183 let venue_order_id4 = create_test_venue_order_id("ORDER4");
1184 let venue_order_id5 = create_test_venue_order_id("ORDER5");
1185
1186 let fills = vec![
1189 FillSnapshot::new(
1190 1000,
1191 OrderSide::Buy,
1192 dec!(0.05),
1193 dec!(4000.00),
1194 venue_order_id1,
1195 ),
1196 FillSnapshot::new(
1197 2000,
1198 OrderSide::Sell,
1199 dec!(0.05),
1200 dec!(4050.00),
1201 venue_order_id2,
1202 ), FillSnapshot::new(
1204 3000,
1205 OrderSide::Buy,
1206 dec!(0.05),
1207 dec!(4000.00),
1208 venue_order_id4,
1209 ), FillSnapshot::new(
1211 4000,
1212 OrderSide::Buy,
1213 dec!(0.05),
1214 dec!(4100.00),
1215 venue_order_id5,
1216 ), ];
1218
1219 let venue_position = VenuePositionSnapshot {
1220 side: OrderSide::Buy,
1221 qty: dec!(0.05),
1222 avg_px: dec!(4142.04),
1223 };
1224
1225 let instrument = instrument();
1226 let result =
1227 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1228
1229 match result {
1231 FillAdjustmentResult::ReplaceCurrentLifecycle {
1232 synthetic_fill,
1233 first_venue_order_id,
1234 } => {
1235 assert_eq!(synthetic_fill.qty, dec!(0.05));
1236 assert_eq!(synthetic_fill.px, dec!(4142.04));
1237 assert_eq!(synthetic_fill.side, OrderSide::Buy);
1238 assert_eq!(first_venue_order_id, venue_order_id4);
1239 }
1240 _ => panic!("Expected ReplaceCurrentLifecycle, was {result:?}"),
1241 }
1242 }
1243
1244 #[rstest]
1245 fn test_adjust_fills_short_position() {
1246 let venue_order_id = create_test_venue_order_id("ORDER1");
1247
1248 let fills = vec![FillSnapshot::new(
1250 1000,
1251 OrderSide::Sell,
1252 dec!(0.02),
1253 dec!(4120.00),
1254 venue_order_id,
1255 )];
1256
1257 let venue_position = VenuePositionSnapshot {
1258 side: OrderSide::Sell,
1259 qty: dec!(0.05),
1260 avg_px: dec!(4100.00),
1261 };
1262
1263 let instrument = instrument();
1264 let result =
1265 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1266
1267 match result {
1269 FillAdjustmentResult::AddSyntheticOpening {
1270 synthetic_fill,
1271 existing_fills,
1272 } => {
1273 assert_eq!(synthetic_fill.side, OrderSide::Sell);
1274 assert_eq!(synthetic_fill.qty, dec!(0.03)); assert_eq!(existing_fills.len(), 1);
1276 }
1277 _ => panic!("Expected AddSyntheticOpening, was {result:?}"),
1278 }
1279 }
1280
1281 #[rstest]
1282 fn test_adjust_fills_timestamp_underflow_protection() {
1283 let venue_order_id = create_test_venue_order_id("ORDER1");
1284
1285 let fills = vec![FillSnapshot::new(
1287 0,
1288 OrderSide::Buy,
1289 dec!(0.01),
1290 dec!(4100.00),
1291 venue_order_id,
1292 )];
1293
1294 let venue_position = VenuePositionSnapshot {
1295 side: OrderSide::Buy,
1296 qty: dec!(0.02),
1297 avg_px: dec!(4100.00),
1298 };
1299
1300 let instrument = instrument();
1301 let result =
1302 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1303
1304 match result {
1306 FillAdjustmentResult::AddSyntheticOpening { synthetic_fill, .. } => {
1307 assert_eq!(synthetic_fill.ts_event, 0); }
1309 _ => panic!("Expected AddSyntheticOpening, was {result:?}"),
1310 }
1311 }
1312
1313 #[rstest]
1314 fn test_adjust_fills_with_flip_scenario() {
1315 let venue_order_id1 = create_test_venue_order_id("ORDER1");
1316 let venue_order_id2 = create_test_venue_order_id("ORDER2");
1317
1318 let fills = vec![
1320 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id1),
1321 FillSnapshot::new(2000, OrderSide::Sell, dec!(20), dec!(105), venue_order_id2), ];
1323
1324 let venue_position = VenuePositionSnapshot {
1325 side: OrderSide::Sell,
1326 qty: dec!(10),
1327 avg_px: dec!(105),
1328 };
1329
1330 let instrument = instrument();
1331 let result =
1332 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1333
1334 match result {
1336 FillAdjustmentResult::NoAdjustment => {
1337 let (qty, value) = simulate_position(&fills);
1339 assert_eq!(qty, dec!(-10));
1340 let avg = value / qty.abs();
1341 assert_eq!(avg, dec!(105));
1342 }
1343 _ => panic!("Expected NoAdjustment for matching flip, was {result:?}"),
1344 }
1345 }
1346
1347 #[rstest]
1348 fn test_detect_zero_crossings_complex_lifecycle() {
1349 let venue_order_id = create_test_venue_order_id("ORDER1");
1350 let fills = vec![
1352 FillSnapshot::new(1000, OrderSide::Buy, dec!(100), dec!(1.20), venue_order_id),
1353 FillSnapshot::new(2000, OrderSide::Sell, dec!(50), dec!(1.25), venue_order_id), FillSnapshot::new(3000, OrderSide::Sell, dec!(100), dec!(1.30), venue_order_id), FillSnapshot::new(4000, OrderSide::Buy, dec!(50), dec!(1.28), venue_order_id), FillSnapshot::new(5000, OrderSide::Buy, dec!(75), dec!(1.22), venue_order_id), FillSnapshot::new(6000, OrderSide::Sell, dec!(150), dec!(1.24), venue_order_id), ];
1359
1360 let crossings = detect_zero_crossings(&fills);
1361 assert_eq!(crossings.len(), 3);
1362 assert_eq!(crossings[0], 3000); assert_eq!(crossings[1], 4000); assert_eq!(crossings[2], 6000); }
1366
1367 #[rstest]
1368 fn test_reconciliation_price_partial_close() {
1369 let venue_order_id = create_test_venue_order_id("ORDER1");
1370 let recon_px =
1372 calculate_reconciliation_price(dec!(100), Some(dec!(1.20)), dec!(50), Some(dec!(1.20)))
1373 .expect("reconciliation price");
1374
1375 let fills = vec![
1377 FillSnapshot::new(1000, OrderSide::Buy, dec!(100), dec!(1.20), venue_order_id),
1378 FillSnapshot::new(2000, OrderSide::Sell, dec!(50), recon_px, venue_order_id),
1379 ];
1380
1381 let (final_qty, final_value) = simulate_position(&fills);
1382 assert_eq!(final_qty, dec!(50));
1383 let final_avg = final_value / final_qty.abs();
1384 assert_eq!(final_avg, dec!(1.20), "Average should be maintained");
1385 }
1386}