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)]
527mod tests {
528 use nautilus_model::instruments::stubs::audusd_sim;
529 use rstest::{fixture, rstest};
530 use rust_decimal_macros::dec;
531
532 use super::*;
533
534 #[fixture]
535 fn instrument() -> InstrumentAny {
536 InstrumentAny::CurrencyPair(audusd_sim())
537 }
538
539 fn create_test_venue_order_id(value: &str) -> VenueOrderId {
540 VenueOrderId::new(value)
541 }
542
543 #[rstest]
544 fn test_fill_snapshot_direction() {
545 let venue_order_id = create_test_venue_order_id("ORDER1");
546 let buy_fill = FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id);
547 assert_eq!(buy_fill.direction(), 1);
548
549 let sell_fill =
550 FillSnapshot::new(2000, OrderSide::Sell, dec!(10), dec!(100), venue_order_id);
551 assert_eq!(sell_fill.direction(), -1);
552 }
553
554 #[rstest]
555 fn test_simulate_position_accumulate_long() {
556 let venue_order_id = create_test_venue_order_id("ORDER1");
557 let fills = vec![
558 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
559 FillSnapshot::new(2000, OrderSide::Buy, dec!(5), dec!(102), venue_order_id),
560 ];
561
562 let (qty, value) = simulate_position(&fills);
563 assert_eq!(qty, dec!(15));
564 assert_eq!(value, dec!(1510)); }
566
567 #[rstest]
568 fn test_simulate_position_close_and_flip() {
569 let venue_order_id = create_test_venue_order_id("ORDER1");
570 let fills = vec![
571 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
572 FillSnapshot::new(2000, OrderSide::Sell, dec!(15), dec!(102), venue_order_id),
573 ];
574
575 let (qty, value) = simulate_position(&fills);
576 assert_eq!(qty, dec!(-5)); assert_eq!(value, dec!(510)); }
579
580 #[rstest]
581 fn test_simulate_position_partial_close() {
582 let venue_order_id = create_test_venue_order_id("ORDER1");
583 let fills = vec![
584 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
585 FillSnapshot::new(2000, OrderSide::Sell, dec!(5), dec!(102), venue_order_id),
586 ];
587
588 let (qty, value) = simulate_position(&fills);
589 assert_eq!(qty, dec!(5));
590 assert_eq!(value, dec!(500)); let avg_px = value / qty;
594 assert_eq!(avg_px, dec!(100));
595 }
596
597 #[rstest]
598 fn test_simulate_position_multiple_partial_closes() {
599 let venue_order_id = create_test_venue_order_id("ORDER1");
600 let fills = vec![
601 FillSnapshot::new(1000, OrderSide::Buy, dec!(100), dec!(10.0), venue_order_id),
602 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), ];
605
606 let (qty, value) = simulate_position(&fills);
607 assert_eq!(qty, dec!(50));
608 assert!((value - dec!(500)).abs() < dec!(0.01));
612
613 let avg_px = value / qty;
615 assert!((avg_px - dec!(10.0)).abs() < dec!(0.01));
616 }
617
618 #[rstest]
619 fn test_simulate_position_short_partial_close() {
620 let venue_order_id = create_test_venue_order_id("ORDER1");
621 let fills = vec![
622 FillSnapshot::new(1000, OrderSide::Sell, dec!(10), dec!(100), venue_order_id),
623 FillSnapshot::new(2000, OrderSide::Buy, dec!(5), dec!(98), venue_order_id), ];
625
626 let (qty, value) = simulate_position(&fills);
627 assert_eq!(qty, dec!(-5));
628 assert_eq!(value, dec!(500)); let avg_px = value / qty.abs();
632 assert_eq!(avg_px, dec!(100));
633 }
634
635 #[rstest]
636 fn test_detect_zero_crossings() {
637 let venue_order_id = create_test_venue_order_id("ORDER1");
638 let fills = vec![
639 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
640 FillSnapshot::new(2000, OrderSide::Sell, dec!(10), dec!(102), venue_order_id), FillSnapshot::new(3000, OrderSide::Buy, dec!(5), dec!(103), venue_order_id),
642 FillSnapshot::new(4000, OrderSide::Sell, dec!(5), dec!(104), venue_order_id), ];
644
645 let crossings = detect_zero_crossings(&fills);
646 assert_eq!(crossings.len(), 2);
647 assert_eq!(crossings[0], 2000);
648 assert_eq!(crossings[1], 4000);
649 }
650
651 #[rstest]
652 fn test_check_position_match_exact() {
653 let result = check_position_match(dec!(10), dec!(1000), dec!(10), dec!(100), dec!(0.0001));
654 assert!(result);
655 }
656
657 #[rstest]
658 fn test_check_position_match_within_tolerance() {
659 let result =
662 check_position_match(dec!(10), dec!(1000), dec!(10), dec!(100.005), dec!(0.0001));
663 assert!(result);
664 }
665
666 #[rstest]
667 fn test_check_position_match_qty_mismatch() {
668 let result = check_position_match(dec!(10), dec!(1000), dec!(11), dec!(100), dec!(0.0001));
669 assert!(!result);
670 }
671
672 #[rstest]
673 fn test_check_position_match_both_flat() {
674 let result = check_position_match(dec!(0), dec!(0), dec!(0), dec!(0), dec!(0.0001));
675 assert!(result);
676 }
677
678 #[rstest]
679 fn test_reconciliation_price_flat_to_long(_instrument: InstrumentAny) {
680 let result = calculate_reconciliation_price(dec!(0), None, dec!(10), Some(dec!(100)));
681 assert!(result.is_some());
682 assert_eq!(result.unwrap(), dec!(100));
683 }
684
685 #[rstest]
686 fn test_reconciliation_price_no_target_avg_px(_instrument: InstrumentAny) {
687 let result = calculate_reconciliation_price(dec!(5), Some(dec!(100)), dec!(10), None);
688 assert!(result.is_none());
689 }
690
691 #[rstest]
692 fn test_reconciliation_price_no_quantity_change(_instrument: InstrumentAny) {
693 let result =
694 calculate_reconciliation_price(dec!(10), Some(dec!(100)), dec!(10), Some(dec!(105)));
695 assert!(result.is_none());
696 }
697
698 #[rstest]
699 fn test_reconciliation_price_long_position_increase(_instrument: InstrumentAny) {
700 let result =
701 calculate_reconciliation_price(dec!(10), Some(dec!(100)), dec!(15), Some(dec!(102)));
702 assert!(result.is_some());
703 assert_eq!(result.unwrap(), dec!(106));
705 }
706
707 #[rstest]
708 fn test_reconciliation_price_flat_to_short(_instrument: InstrumentAny) {
709 let result = calculate_reconciliation_price(dec!(0), None, dec!(-10), Some(dec!(100)));
710 assert!(result.is_some());
711 assert_eq!(result.unwrap(), dec!(100));
712 }
713
714 #[rstest]
715 fn test_reconciliation_price_long_to_flat(_instrument: InstrumentAny) {
716 let result =
719 calculate_reconciliation_price(dec!(100), Some(dec!(1.20)), dec!(0), Some(dec!(0)));
720 assert!(result.is_some());
721 assert_eq!(result.unwrap(), dec!(1.20));
722 }
723
724 #[rstest]
725 fn test_reconciliation_price_short_to_flat(_instrument: InstrumentAny) {
726 let result = calculate_reconciliation_price(dec!(-50), Some(dec!(2.50)), dec!(0), None);
729 assert!(result.is_some());
730 assert_eq!(result.unwrap(), dec!(2.50));
731 }
732
733 #[rstest]
734 fn test_reconciliation_price_short_position_increase(_instrument: InstrumentAny) {
735 let result = calculate_reconciliation_price(
740 dec!(-100),
741 Some(dec!(1.30)),
742 dec!(-200),
743 Some(dec!(1.28)),
744 );
745 assert!(result.is_some());
746 assert_eq!(result.unwrap(), dec!(1.26));
747 }
748
749 #[rstest]
750 fn test_reconciliation_price_long_position_decrease(_instrument: InstrumentAny) {
751 let result = calculate_reconciliation_price(
753 dec!(200),
754 Some(dec!(1.20)),
755 dec!(100),
756 Some(dec!(1.20)),
757 );
758 assert!(result.is_some());
759 assert_eq!(result.unwrap(), dec!(1.20));
760 }
761
762 #[rstest]
763 fn test_reconciliation_price_long_to_short_flip(_instrument: InstrumentAny) {
764 let result = calculate_reconciliation_price(
767 dec!(100),
768 Some(dec!(1.20)),
769 dec!(-100),
770 Some(dec!(1.25)),
771 );
772 assert!(result.is_some());
773 assert_eq!(result.unwrap(), dec!(1.25));
774 }
775
776 #[rstest]
777 fn test_reconciliation_price_short_to_long_flip(_instrument: InstrumentAny) {
778 let result = calculate_reconciliation_price(
781 dec!(-100),
782 Some(dec!(1.30)),
783 dec!(100),
784 Some(dec!(1.25)),
785 );
786 assert!(result.is_some());
787 assert_eq!(result.unwrap(), dec!(1.25));
788 }
789
790 #[rstest]
791 fn test_reconciliation_price_complex_scenario(_instrument: InstrumentAny) {
792 let result = calculate_reconciliation_price(
797 dec!(150),
798 Some(dec!(1.23456)),
799 dec!(250),
800 Some(dec!(1.24567)),
801 );
802 assert!(result.is_some());
803 assert_eq!(result.unwrap(), dec!(1.262335));
804 }
805
806 #[rstest]
807 fn test_reconciliation_price_zero_target_avg_px(_instrument: InstrumentAny) {
808 let result =
809 calculate_reconciliation_price(dec!(100), Some(dec!(1.20)), dec!(200), Some(dec!(0)));
810 assert!(result.is_none());
811 }
812
813 #[rstest]
814 fn test_reconciliation_price_negative_price(_instrument: InstrumentAny) {
815 let result = calculate_reconciliation_price(
820 dec!(100),
821 Some(dec!(2.00)),
822 dec!(200),
823 Some(dec!(1.00)),
824 );
825 assert!(result.is_none());
826 }
827
828 #[rstest]
829 fn test_reconciliation_price_flip_simulation_compatibility() {
830 let venue_order_id = create_test_venue_order_id("ORDER1");
831 let recon_px = calculate_reconciliation_price(
835 dec!(100),
836 Some(dec!(1.20)),
837 dec!(-100),
838 Some(dec!(1.25)),
839 )
840 .expect("reconciliation price");
841
842 assert_eq!(recon_px, dec!(1.25));
843
844 let fills = vec![
846 FillSnapshot::new(1000, OrderSide::Buy, dec!(100), dec!(1.20), venue_order_id),
847 FillSnapshot::new(2000, OrderSide::Sell, dec!(200), recon_px, venue_order_id),
848 ];
849
850 let (final_qty, final_value) = simulate_position(&fills);
851 assert_eq!(final_qty, dec!(-100));
852 let final_avg = final_value / final_qty.abs();
853 assert_eq!(final_avg, dec!(1.25), "Final average should match target");
854 }
855
856 #[rstest]
857 fn test_reconciliation_price_accumulation_simulation_compatibility() {
858 let venue_order_id = create_test_venue_order_id("ORDER1");
859 let recon_px = calculate_reconciliation_price(
862 dec!(100),
863 Some(dec!(1.20)),
864 dec!(200),
865 Some(dec!(1.22)),
866 )
867 .expect("reconciliation price");
868
869 let fills = vec![
871 FillSnapshot::new(1000, OrderSide::Buy, dec!(100), dec!(1.20), venue_order_id),
872 FillSnapshot::new(2000, OrderSide::Buy, dec!(100), recon_px, venue_order_id),
873 ];
874
875 let (final_qty, final_value) = simulate_position(&fills);
876 assert_eq!(final_qty, dec!(200));
877 let final_avg = final_value / final_qty.abs();
878 assert_eq!(final_avg, dec!(1.22), "Final average should match target");
879 }
880
881 #[rstest]
882 fn test_simulate_position_accumulate_short() {
883 let venue_order_id = create_test_venue_order_id("ORDER1");
884 let fills = vec![
885 FillSnapshot::new(1000, OrderSide::Sell, dec!(10), dec!(100), venue_order_id),
886 FillSnapshot::new(2000, OrderSide::Sell, dec!(5), dec!(98), venue_order_id),
887 ];
888
889 let (qty, value) = simulate_position(&fills);
890 assert_eq!(qty, dec!(-15));
891 assert_eq!(value, dec!(1490)); }
893
894 #[rstest]
895 fn test_simulate_position_short_to_long_flip() {
896 let venue_order_id = create_test_venue_order_id("ORDER1");
897 let fills = vec![
898 FillSnapshot::new(1000, OrderSide::Sell, dec!(10), dec!(100), venue_order_id),
899 FillSnapshot::new(2000, OrderSide::Buy, dec!(15), dec!(102), venue_order_id),
900 ];
901
902 let (qty, value) = simulate_position(&fills);
903 assert_eq!(qty, dec!(5)); assert_eq!(value, dec!(510)); }
906
907 #[rstest]
908 fn test_simulate_position_multiple_flips() {
909 let venue_order_id = create_test_venue_order_id("ORDER1");
910 let fills = vec![
911 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
912 FillSnapshot::new(2000, OrderSide::Sell, dec!(15), dec!(105), venue_order_id), FillSnapshot::new(3000, OrderSide::Buy, dec!(10), dec!(110), venue_order_id), ];
915
916 let (qty, value) = simulate_position(&fills);
917 assert_eq!(qty, dec!(5)); assert_eq!(value, dec!(550)); }
920
921 #[rstest]
922 fn test_simulate_position_empty_fills() {
923 let fills: Vec<FillSnapshot> = vec![];
924 let (qty, value) = simulate_position(&fills);
925 assert_eq!(qty, dec!(0));
926 assert_eq!(value, dec!(0));
927 }
928
929 #[rstest]
930 fn test_detect_zero_crossings_no_crossings() {
931 let venue_order_id = create_test_venue_order_id("ORDER1");
932 let fills = vec![
933 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
934 FillSnapshot::new(2000, OrderSide::Buy, dec!(5), dec!(102), venue_order_id),
935 ];
936
937 let crossings = detect_zero_crossings(&fills);
938 assert_eq!(crossings.len(), 0);
939 }
940
941 #[rstest]
942 fn test_detect_zero_crossings_single_crossing() {
943 let venue_order_id = create_test_venue_order_id("ORDER1");
944 let fills = vec![
945 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
946 FillSnapshot::new(2000, OrderSide::Sell, dec!(10), dec!(102), venue_order_id), ];
948
949 let crossings = detect_zero_crossings(&fills);
950 assert_eq!(crossings.len(), 1);
951 assert_eq!(crossings[0], 2000);
952 }
953
954 #[rstest]
955 fn test_detect_zero_crossings_empty_fills() {
956 let fills: Vec<FillSnapshot> = vec![];
957 let crossings = detect_zero_crossings(&fills);
958 assert_eq!(crossings.len(), 0);
959 }
960
961 #[rstest]
962 fn test_detect_zero_crossings_long_to_short_flip() {
963 let venue_order_id = create_test_venue_order_id("ORDER1");
964 let fills = vec![
966 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
967 FillSnapshot::new(2000, OrderSide::Sell, dec!(15), dec!(102), venue_order_id), ];
969
970 let crossings = detect_zero_crossings(&fills);
971 assert_eq!(crossings.len(), 1);
972 assert_eq!(crossings[0], 2000); }
974
975 #[rstest]
976 fn test_detect_zero_crossings_short_to_long_flip() {
977 let venue_order_id = create_test_venue_order_id("ORDER1");
978 let fills = vec![
980 FillSnapshot::new(1000, OrderSide::Sell, dec!(10), dec!(100), venue_order_id),
981 FillSnapshot::new(2000, OrderSide::Buy, dec!(20), dec!(102), venue_order_id), ];
983
984 let crossings = detect_zero_crossings(&fills);
985 assert_eq!(crossings.len(), 1);
986 assert_eq!(crossings[0], 2000);
987 }
988
989 #[rstest]
990 fn test_detect_zero_crossings_multiple_flips() {
991 let venue_order_id = create_test_venue_order_id("ORDER1");
992 let fills = vec![
993 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
994 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), ];
998
999 let crossings = detect_zero_crossings(&fills);
1000 assert_eq!(crossings.len(), 2);
1001 assert_eq!(crossings[0], 2000); assert_eq!(crossings[1], 4000); }
1004
1005 #[rstest]
1006 fn test_check_position_match_outside_tolerance() {
1007 let result = check_position_match(dec!(10), dec!(1000), dec!(10), dec!(101), dec!(0.0001));
1010 assert!(!result);
1011 }
1012
1013 #[rstest]
1014 fn test_check_position_match_edge_of_tolerance() {
1015 let result =
1018 check_position_match(dec!(10), dec!(1000), dec!(10), dec!(100.01), dec!(0.0001));
1019 assert!(result);
1020 }
1021
1022 #[rstest]
1023 fn test_check_position_match_zero_venue_avg_px() {
1024 let result = check_position_match(dec!(10), dec!(1000), dec!(10), dec!(0), dec!(0.0001));
1025 assert!(!result); }
1027
1028 #[rstest]
1031 fn test_adjust_fills_no_fills() {
1032 let venue_position = VenuePositionSnapshot {
1033 side: OrderSide::Buy,
1034 qty: dec!(0.02),
1035 avg_px: dec!(4100.00),
1036 };
1037 let instrument = instrument();
1038 let result =
1039 adjust_fills_for_partial_window(&[], &venue_position, &instrument, dec!(0.0001));
1040 assert!(matches!(result, FillAdjustmentResult::NoAdjustment));
1041 }
1042
1043 #[rstest]
1044 fn test_adjust_fills_flat_position() {
1045 let venue_order_id = create_test_venue_order_id("ORDER1");
1046 let fills = vec![FillSnapshot::new(
1047 1000,
1048 OrderSide::Buy,
1049 dec!(0.01),
1050 dec!(4100.00),
1051 venue_order_id,
1052 )];
1053 let venue_position = VenuePositionSnapshot {
1054 side: OrderSide::Buy,
1055 qty: dec!(0),
1056 avg_px: dec!(0),
1057 };
1058 let instrument = instrument();
1059 let result =
1060 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1061 assert!(matches!(result, FillAdjustmentResult::NoAdjustment));
1062 }
1063
1064 #[rstest]
1065 fn test_adjust_fills_complete_lifecycle_no_adjustment() {
1066 let venue_order_id = create_test_venue_order_id("ORDER1");
1067 let venue_order_id2 = create_test_venue_order_id("ORDER2");
1068 let fills = vec![
1069 FillSnapshot::new(
1070 1000,
1071 OrderSide::Buy,
1072 dec!(0.01),
1073 dec!(4100.00),
1074 venue_order_id,
1075 ),
1076 FillSnapshot::new(
1077 2000,
1078 OrderSide::Buy,
1079 dec!(0.01),
1080 dec!(4100.00),
1081 venue_order_id2,
1082 ),
1083 ];
1084 let venue_position = VenuePositionSnapshot {
1085 side: OrderSide::Buy,
1086 qty: dec!(0.02),
1087 avg_px: dec!(4100.00),
1088 };
1089 let instrument = instrument();
1090 let result =
1091 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1092 assert!(matches!(result, FillAdjustmentResult::NoAdjustment));
1093 }
1094
1095 #[rstest]
1096 fn test_adjust_fills_incomplete_lifecycle_adds_synthetic() {
1097 let venue_order_id = create_test_venue_order_id("ORDER1");
1098 let fills = vec![FillSnapshot::new(
1100 2000,
1101 OrderSide::Buy,
1102 dec!(0.02),
1103 dec!(4200.00),
1104 venue_order_id,
1105 )];
1106 let venue_position = VenuePositionSnapshot {
1107 side: OrderSide::Buy,
1108 qty: dec!(0.04),
1109 avg_px: dec!(4100.00),
1110 };
1111 let instrument = instrument();
1112 let result =
1113 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1114
1115 match result {
1116 FillAdjustmentResult::AddSyntheticOpening {
1117 synthetic_fill,
1118 existing_fills,
1119 } => {
1120 assert_eq!(synthetic_fill.side, OrderSide::Buy);
1121 assert_eq!(synthetic_fill.qty, dec!(0.02)); assert_eq!(existing_fills.len(), 1);
1123 }
1124 _ => panic!("Expected AddSyntheticOpening"),
1125 }
1126 }
1127
1128 #[rstest]
1129 fn test_adjust_fills_with_zero_crossings() {
1130 let venue_order_id1 = create_test_venue_order_id("ORDER1");
1131 let venue_order_id2 = create_test_venue_order_id("ORDER2");
1132 let venue_order_id3 = create_test_venue_order_id("ORDER3");
1133
1134 let fills = vec![
1137 FillSnapshot::new(
1138 1000,
1139 OrderSide::Buy,
1140 dec!(0.02),
1141 dec!(4100.00),
1142 venue_order_id1,
1143 ),
1144 FillSnapshot::new(
1145 2000,
1146 OrderSide::Sell,
1147 dec!(0.02),
1148 dec!(4150.00),
1149 venue_order_id2,
1150 ), FillSnapshot::new(
1152 3000,
1153 OrderSide::Buy,
1154 dec!(0.03),
1155 dec!(4200.00),
1156 venue_order_id3,
1157 ), ];
1159
1160 let venue_position = VenuePositionSnapshot {
1161 side: OrderSide::Buy,
1162 qty: dec!(0.03),
1163 avg_px: dec!(4200.00),
1164 };
1165
1166 let instrument = instrument();
1167 let result =
1168 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1169
1170 match result {
1172 FillAdjustmentResult::FilterToCurrentLifecycle {
1173 last_zero_crossing_ts,
1174 current_lifecycle_fills,
1175 } => {
1176 assert_eq!(last_zero_crossing_ts, 2000);
1177 assert_eq!(current_lifecycle_fills.len(), 1);
1178 assert_eq!(current_lifecycle_fills[0].venue_order_id, venue_order_id3);
1179 }
1180 _ => panic!("Expected FilterToCurrentLifecycle, was {:?}", result),
1181 }
1182 }
1183
1184 #[rstest]
1185 fn test_adjust_fills_multiple_zero_crossings_mismatch() {
1186 let venue_order_id1 = create_test_venue_order_id("ORDER1");
1187 let venue_order_id2 = create_test_venue_order_id("ORDER2");
1188 let _venue_order_id3 = create_test_venue_order_id("ORDER3");
1189 let venue_order_id4 = create_test_venue_order_id("ORDER4");
1190 let venue_order_id5 = create_test_venue_order_id("ORDER5");
1191
1192 let fills = vec![
1195 FillSnapshot::new(
1196 1000,
1197 OrderSide::Buy,
1198 dec!(0.05),
1199 dec!(4000.00),
1200 venue_order_id1,
1201 ),
1202 FillSnapshot::new(
1203 2000,
1204 OrderSide::Sell,
1205 dec!(0.05),
1206 dec!(4050.00),
1207 venue_order_id2,
1208 ), FillSnapshot::new(
1210 3000,
1211 OrderSide::Buy,
1212 dec!(0.05),
1213 dec!(4000.00),
1214 venue_order_id4,
1215 ), FillSnapshot::new(
1217 4000,
1218 OrderSide::Buy,
1219 dec!(0.05),
1220 dec!(4100.00),
1221 venue_order_id5,
1222 ), ];
1224
1225 let venue_position = VenuePositionSnapshot {
1226 side: OrderSide::Buy,
1227 qty: dec!(0.05),
1228 avg_px: dec!(4142.04),
1229 };
1230
1231 let instrument = instrument();
1232 let result =
1233 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1234
1235 match result {
1237 FillAdjustmentResult::ReplaceCurrentLifecycle {
1238 synthetic_fill,
1239 first_venue_order_id,
1240 } => {
1241 assert_eq!(synthetic_fill.qty, dec!(0.05));
1242 assert_eq!(synthetic_fill.px, dec!(4142.04));
1243 assert_eq!(synthetic_fill.side, OrderSide::Buy);
1244 assert_eq!(first_venue_order_id, venue_order_id4);
1245 }
1246 _ => panic!("Expected ReplaceCurrentLifecycle, was {:?}", result),
1247 }
1248 }
1249
1250 #[rstest]
1251 fn test_adjust_fills_short_position() {
1252 let venue_order_id = create_test_venue_order_id("ORDER1");
1253
1254 let fills = vec![FillSnapshot::new(
1256 1000,
1257 OrderSide::Sell,
1258 dec!(0.02),
1259 dec!(4120.00),
1260 venue_order_id,
1261 )];
1262
1263 let venue_position = VenuePositionSnapshot {
1264 side: OrderSide::Sell,
1265 qty: dec!(0.05),
1266 avg_px: dec!(4100.00),
1267 };
1268
1269 let instrument = instrument();
1270 let result =
1271 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1272
1273 match result {
1275 FillAdjustmentResult::AddSyntheticOpening {
1276 synthetic_fill,
1277 existing_fills,
1278 } => {
1279 assert_eq!(synthetic_fill.side, OrderSide::Sell);
1280 assert_eq!(synthetic_fill.qty, dec!(0.03)); assert_eq!(existing_fills.len(), 1);
1282 }
1283 _ => panic!("Expected AddSyntheticOpening, was {:?}", result),
1284 }
1285 }
1286
1287 #[rstest]
1288 fn test_adjust_fills_timestamp_underflow_protection() {
1289 let venue_order_id = create_test_venue_order_id("ORDER1");
1290
1291 let fills = vec![FillSnapshot::new(
1293 0,
1294 OrderSide::Buy,
1295 dec!(0.01),
1296 dec!(4100.00),
1297 venue_order_id,
1298 )];
1299
1300 let venue_position = VenuePositionSnapshot {
1301 side: OrderSide::Buy,
1302 qty: dec!(0.02),
1303 avg_px: dec!(4100.00),
1304 };
1305
1306 let instrument = instrument();
1307 let result =
1308 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1309
1310 match result {
1312 FillAdjustmentResult::AddSyntheticOpening { synthetic_fill, .. } => {
1313 assert_eq!(synthetic_fill.ts_event, 0); }
1315 _ => panic!("Expected AddSyntheticOpening, was {:?}", result),
1316 }
1317 }
1318
1319 #[rstest]
1320 fn test_adjust_fills_with_flip_scenario() {
1321 let venue_order_id1 = create_test_venue_order_id("ORDER1");
1322 let venue_order_id2 = create_test_venue_order_id("ORDER2");
1323
1324 let fills = vec![
1326 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id1),
1327 FillSnapshot::new(2000, OrderSide::Sell, dec!(20), dec!(105), venue_order_id2), ];
1329
1330 let venue_position = VenuePositionSnapshot {
1331 side: OrderSide::Sell,
1332 qty: dec!(10),
1333 avg_px: dec!(105),
1334 };
1335
1336 let instrument = instrument();
1337 let result =
1338 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1339
1340 match result {
1342 FillAdjustmentResult::NoAdjustment => {
1343 let (qty, value) = simulate_position(&fills);
1345 assert_eq!(qty, dec!(-10));
1346 let avg = value / qty.abs();
1347 assert_eq!(avg, dec!(105));
1348 }
1349 _ => panic!("Expected NoAdjustment for matching flip, was {:?}", result),
1350 }
1351 }
1352
1353 #[rstest]
1354 fn test_detect_zero_crossings_complex_lifecycle() {
1355 let venue_order_id = create_test_venue_order_id("ORDER1");
1356 let fills = vec![
1358 FillSnapshot::new(1000, OrderSide::Buy, dec!(100), dec!(1.20), venue_order_id),
1359 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), ];
1365
1366 let crossings = detect_zero_crossings(&fills);
1367 assert_eq!(crossings.len(), 3);
1368 assert_eq!(crossings[0], 3000); assert_eq!(crossings[1], 4000); assert_eq!(crossings[2], 6000); }
1372
1373 #[rstest]
1374 fn test_reconciliation_price_partial_close() {
1375 let venue_order_id = create_test_venue_order_id("ORDER1");
1376 let recon_px =
1378 calculate_reconciliation_price(dec!(100), Some(dec!(1.20)), dec!(50), Some(dec!(1.20)))
1379 .expect("reconciliation price");
1380
1381 let fills = vec![
1383 FillSnapshot::new(1000, OrderSide::Buy, dec!(100), dec!(1.20), venue_order_id),
1384 FillSnapshot::new(2000, OrderSide::Sell, dec!(50), recon_px, venue_order_id),
1385 ];
1386
1387 let (final_qty, final_value) = simulate_position(&fills);
1388 assert_eq!(final_qty, dec!(50));
1389 let final_avg = final_value / final_qty.abs();
1390 assert_eq!(final_avg, dec!(1.20), "Average should be maintained");
1391 }
1392}