1use ahash::AHashMap;
19use nautilus_core::{UUID4, UnixNanos};
20use nautilus_model::{
21 enums::{LiquiditySide, OrderSide, OrderStatus, OrderType, PositionSideSpecified, TimeInForce},
22 identifiers::{AccountId, InstrumentId, TradeId, VenueOrderId},
23 instruments::{Instrument, InstrumentAny},
24 reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
25 types::{Money, Price, Quantity},
26};
27use rust_decimal::{Decimal, prelude::ToPrimitive};
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct FillSnapshot {
32 pub ts_event: u64,
34 pub side: OrderSide,
36 pub qty: Decimal,
38 pub px: Decimal,
40 pub venue_order_id: VenueOrderId,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct VenuePositionSnapshot {
47 pub side: OrderSide, pub qty: Decimal,
51 pub avg_px: Decimal,
53}
54
55#[derive(Debug, Clone, PartialEq)]
57pub enum FillAdjustmentResult {
58 NoAdjustment,
60 AddSyntheticOpening {
62 synthetic_fill: FillSnapshot,
64 existing_fills: Vec<FillSnapshot>,
66 },
67 ReplaceCurrentLifecycle {
69 synthetic_fill: FillSnapshot,
71 first_venue_order_id: VenueOrderId,
73 },
74 FilterToCurrentLifecycle {
76 last_zero_crossing_ts: u64,
78 current_lifecycle_fills: Vec<FillSnapshot>,
80 },
81}
82
83impl FillSnapshot {
84 #[must_use]
86 pub fn new(
87 ts_event: u64,
88 side: OrderSide,
89 qty: Decimal,
90 px: Decimal,
91 venue_order_id: VenueOrderId,
92 ) -> Self {
93 Self {
94 ts_event,
95 side,
96 qty,
97 px,
98 venue_order_id,
99 }
100 }
101
102 #[must_use]
104 pub fn direction(&self) -> i8 {
105 match self.side {
106 OrderSide::Buy => 1,
107 OrderSide::Sell => -1,
108 _ => 0,
109 }
110 }
111}
112
113#[must_use]
119pub fn simulate_position(fills: &[FillSnapshot]) -> (Decimal, Decimal) {
120 let mut qty = Decimal::ZERO;
121 let mut value = Decimal::ZERO;
122
123 for fill in fills {
124 let direction = Decimal::from(fill.direction());
125 let new_qty = qty + (direction * fill.qty);
126
127 if (qty >= Decimal::ZERO && direction > Decimal::ZERO)
129 || (qty <= Decimal::ZERO && direction < Decimal::ZERO)
130 {
131 value += fill.qty * fill.px;
133 qty = new_qty;
134 } else {
135 if qty.abs() >= fill.qty {
137 let close_ratio = fill.qty / qty.abs();
139 value *= Decimal::ONE - close_ratio;
140 qty = new_qty;
141 } else {
142 let remaining = fill.qty - qty.abs();
144 qty = direction * remaining;
145 value = remaining * fill.px;
146 }
147 }
148 }
149
150 (qty, value)
151}
152
153#[must_use]
162pub fn detect_zero_crossings(fills: &[FillSnapshot]) -> Vec<u64> {
163 let mut running_qty = Decimal::ZERO;
164 let mut zero_crossings = Vec::new();
165
166 for fill in fills {
167 let prev_qty = running_qty;
168 running_qty += Decimal::from(fill.direction()) * fill.qty;
169
170 if prev_qty != Decimal::ZERO {
172 if running_qty == Decimal::ZERO {
173 zero_crossings.push(fill.ts_event);
175 } else if (prev_qty > Decimal::ZERO) != (running_qty > Decimal::ZERO) {
176 zero_crossings.push(fill.ts_event);
178 }
179 }
180 }
181
182 zero_crossings
183}
184
185#[must_use]
191pub fn check_position_match(
192 simulated_qty: Decimal,
193 simulated_value: Decimal,
194 venue_qty: Decimal,
195 venue_avg_px: Decimal,
196 tolerance: Decimal,
197) -> bool {
198 if simulated_qty != venue_qty {
199 return false;
200 }
201
202 if simulated_qty == Decimal::ZERO {
203 return true; }
205
206 let abs_qty = simulated_qty.abs();
208 if abs_qty == Decimal::ZERO {
209 return false;
210 }
211
212 let simulated_avg_px = simulated_value / abs_qty;
213
214 if venue_avg_px == Decimal::ZERO {
216 return false;
217 }
218
219 let relative_diff = (simulated_avg_px - venue_avg_px).abs() / venue_avg_px;
220
221 relative_diff <= tolerance
222}
223
224pub fn calculate_reconciliation_price(
242 current_position_qty: Decimal,
243 current_position_avg_px: Option<Decimal>,
244 target_position_qty: Decimal,
245 target_position_avg_px: Option<Decimal>,
246) -> Option<Decimal> {
247 let qty_diff = target_position_qty - current_position_qty;
249
250 if qty_diff == Decimal::ZERO {
251 return None; }
253
254 if target_position_qty == Decimal::ZERO {
257 return current_position_avg_px;
258 }
259
260 let target_avg_px = target_position_avg_px?;
262 if target_avg_px == Decimal::ZERO {
263 return None;
264 }
265
266 if current_position_qty == Decimal::ZERO || current_position_avg_px.is_none() {
268 return Some(target_avg_px);
269 }
270
271 let current_avg_px = current_position_avg_px?;
272
273 let is_flip = (current_position_qty > Decimal::ZERO) != (target_position_qty > Decimal::ZERO)
276 && target_position_qty != Decimal::ZERO;
277
278 if is_flip {
279 return Some(target_avg_px);
280 }
281
282 let target_value = target_position_qty * target_avg_px;
285 let current_value = current_position_qty * current_avg_px;
286 let diff_value = target_value - current_value;
287
288 let reconciliation_px = diff_value / qty_diff;
290
291 if reconciliation_px > Decimal::ZERO {
293 return Some(reconciliation_px);
294 }
295
296 None
297}
298
299#[must_use]
312pub fn adjust_fills_for_partial_window(
313 fills: &[FillSnapshot],
314 venue_position: &VenuePositionSnapshot,
315 _instrument: &InstrumentAny,
316 tolerance: Decimal,
317) -> FillAdjustmentResult {
318 if fills.is_empty() {
320 return FillAdjustmentResult::NoAdjustment;
321 }
322
323 if venue_position.qty == Decimal::ZERO {
325 return FillAdjustmentResult::NoAdjustment;
326 }
327
328 let zero_crossings = detect_zero_crossings(fills);
330
331 let venue_qty_signed = match venue_position.side {
333 OrderSide::Buy => venue_position.qty,
334 OrderSide::Sell => -venue_position.qty,
335 _ => Decimal::ZERO,
336 };
337
338 if !zero_crossings.is_empty() {
340 let mut last_flat_crossing_ts = None;
343 let mut running_qty = Decimal::ZERO;
344
345 for fill in fills {
346 let prev_qty = running_qty;
347 running_qty += Decimal::from(fill.direction()) * fill.qty;
348
349 if prev_qty != Decimal::ZERO && running_qty == Decimal::ZERO {
350 last_flat_crossing_ts = Some(fill.ts_event);
351 }
352 }
353
354 let lifecycle_boundary_ts =
355 last_flat_crossing_ts.unwrap_or(*zero_crossings.last().unwrap());
356
357 let current_lifecycle_fills: Vec<FillSnapshot> = fills
359 .iter()
360 .filter(|f| f.ts_event > lifecycle_boundary_ts)
361 .cloned()
362 .collect();
363
364 if current_lifecycle_fills.is_empty() {
365 return FillAdjustmentResult::NoAdjustment;
366 }
367
368 let (current_qty, current_value) = simulate_position(¤t_lifecycle_fills);
370
371 if check_position_match(
373 current_qty,
374 current_value,
375 venue_qty_signed,
376 venue_position.avg_px,
377 tolerance,
378 ) {
379 return FillAdjustmentResult::FilterToCurrentLifecycle {
381 last_zero_crossing_ts: lifecycle_boundary_ts,
382 current_lifecycle_fills,
383 };
384 }
385
386 if let Some(first_fill) = current_lifecycle_fills.first() {
388 let synthetic_fill = FillSnapshot::new(
389 first_fill.ts_event.saturating_sub(1), venue_position.side,
391 venue_position.qty,
392 venue_position.avg_px,
393 first_fill.venue_order_id,
394 );
395
396 return FillAdjustmentResult::ReplaceCurrentLifecycle {
397 synthetic_fill,
398 first_venue_order_id: first_fill.venue_order_id,
399 };
400 }
401
402 return FillAdjustmentResult::NoAdjustment;
403 }
404
405 let oldest_lifecycle_fills: Vec<FillSnapshot> =
408 if let Some(&first_zero_crossing_ts) = zero_crossings.first() {
409 fills
411 .iter()
412 .filter(|f| f.ts_event <= first_zero_crossing_ts)
413 .cloned()
414 .collect()
415 } else {
416 fills.to_vec()
418 };
419
420 if oldest_lifecycle_fills.is_empty() {
421 return FillAdjustmentResult::NoAdjustment;
422 }
423
424 let (oldest_qty, oldest_value) = simulate_position(&oldest_lifecycle_fills);
426
427 if zero_crossings.is_empty() {
429 if check_position_match(
431 oldest_qty,
432 oldest_value,
433 venue_qty_signed,
434 venue_position.avg_px,
435 tolerance,
436 ) {
437 return FillAdjustmentResult::NoAdjustment;
438 }
439
440 if let Some(first_fill) = oldest_lifecycle_fills.first() {
442 let oldest_avg_px = if oldest_qty != Decimal::ZERO {
445 Some(oldest_value / oldest_qty.abs())
446 } else {
447 None
448 };
449
450 let reconciliation_price = calculate_reconciliation_price(
451 oldest_qty,
452 oldest_avg_px,
453 venue_qty_signed,
454 Some(venue_position.avg_px),
455 );
456
457 if let Some(opening_px) = reconciliation_price {
458 let opening_qty = if oldest_qty != Decimal::ZERO {
460 venue_qty_signed - oldest_qty
462 } else {
463 venue_qty_signed
464 };
465
466 if opening_qty.abs() > Decimal::ZERO {
467 let synthetic_side = if opening_qty > Decimal::ZERO {
468 OrderSide::Buy
469 } else {
470 OrderSide::Sell
471 };
472
473 let synthetic_fill = FillSnapshot::new(
474 first_fill.ts_event.saturating_sub(1),
475 synthetic_side,
476 opening_qty.abs(),
477 opening_px,
478 first_fill.venue_order_id,
479 );
480
481 return FillAdjustmentResult::AddSyntheticOpening {
482 synthetic_fill,
483 existing_fills: oldest_lifecycle_fills,
484 };
485 }
486 }
487 }
488
489 return FillAdjustmentResult::NoAdjustment;
490 }
491
492 if oldest_qty == Decimal::ZERO {
494 return FillAdjustmentResult::NoAdjustment;
496 }
497
498 if !oldest_lifecycle_fills.is_empty()
500 && let Some(&first_zero_crossing_ts) = zero_crossings.first()
501 {
502 let current_lifecycle_fills: Vec<FillSnapshot> = fills
504 .iter()
505 .filter(|f| f.ts_event > first_zero_crossing_ts)
506 .cloned()
507 .collect();
508
509 if !current_lifecycle_fills.is_empty()
510 && let Some(first_current_fill) = current_lifecycle_fills.first()
511 {
512 let synthetic_fill = FillSnapshot::new(
513 first_current_fill.ts_event.saturating_sub(1),
514 venue_position.side,
515 venue_position.qty,
516 venue_position.avg_px,
517 first_current_fill.venue_order_id,
518 );
519
520 return FillAdjustmentResult::AddSyntheticOpening {
521 synthetic_fill,
522 existing_fills: oldest_lifecycle_fills,
523 };
524 }
525 }
526
527 FillAdjustmentResult::NoAdjustment
528}
529
530#[must_use]
534pub fn create_synthetic_venue_order_id(ts_event: u64) -> VenueOrderId {
535 let uuid = UUID4::new();
536 let uuid_str = uuid.to_string();
537 let uuid_suffix = &uuid_str[..8];
538 let venue_order_id_value = format!("S-{ts_event:x}-{uuid_suffix}");
539 VenueOrderId::new(&venue_order_id_value)
540}
541
542#[must_use]
546pub fn create_synthetic_trade_id(ts_event: u64) -> TradeId {
547 let uuid = UUID4::new();
548 let uuid_str = uuid.to_string();
549 let uuid_suffix = &uuid_str[..8];
550 let trade_id_value = format!("S-{ts_event:x}-{uuid_suffix}");
551 TradeId::new(&trade_id_value)
552}
553
554pub fn create_synthetic_order_report(
560 fill: &FillSnapshot,
561 account_id: AccountId,
562 instrument_id: InstrumentId,
563 instrument: &InstrumentAny,
564 venue_order_id: VenueOrderId,
565) -> anyhow::Result<OrderStatusReport> {
566 let qty_f64 = fill
567 .qty
568 .to_f64()
569 .ok_or_else(|| anyhow::anyhow!("Failed to convert quantity to f64"))?;
570 let order_qty = Quantity::new(qty_f64, instrument.size_precision());
571
572 Ok(OrderStatusReport::new(
573 account_id,
574 instrument_id,
575 None, venue_order_id,
577 fill.side,
578 OrderType::Market,
579 TimeInForce::Gtc,
580 OrderStatus::Filled,
581 order_qty,
582 order_qty, UnixNanos::from(fill.ts_event),
584 UnixNanos::from(fill.ts_event),
585 UnixNanos::from(fill.ts_event),
586 None, ))
588}
589
590pub fn create_synthetic_fill_report(
596 fill: &FillSnapshot,
597 account_id: AccountId,
598 instrument_id: InstrumentId,
599 instrument: &InstrumentAny,
600 venue_order_id: VenueOrderId,
601) -> anyhow::Result<FillReport> {
602 let trade_id = create_synthetic_trade_id(fill.ts_event);
603
604 let qty_f64 = fill
605 .qty
606 .to_f64()
607 .ok_or_else(|| anyhow::anyhow!("Failed to convert quantity to f64"))?;
608 let px_f64 = fill
609 .px
610 .to_f64()
611 .ok_or_else(|| anyhow::anyhow!("Failed to convert price to f64"))?;
612
613 Ok(FillReport::new(
614 account_id,
615 instrument_id,
616 venue_order_id,
617 trade_id,
618 fill.side,
619 Quantity::new(qty_f64, instrument.size_precision()),
620 Price::new(px_f64, instrument.price_precision()),
621 Money::new(0.0, instrument.quote_currency()),
622 LiquiditySide::NoLiquiditySide,
623 None, None, fill.ts_event.into(),
626 fill.ts_event.into(),
627 None, ))
629}
630
631#[derive(Debug, Clone)]
633pub struct ReconciliationResult {
634 pub orders: AHashMap<VenueOrderId, OrderStatusReport>,
636 pub fills: AHashMap<VenueOrderId, Vec<FillReport>>,
638}
639
640const DEFAULT_TOLERANCE: Decimal = Decimal::from_parts(1, 0, 0, false, 4); pub fn process_mass_status_for_reconciliation(
653 mass_status: &ExecutionMassStatus,
654 instrument: &InstrumentAny,
655 tolerance: Option<Decimal>,
656) -> anyhow::Result<ReconciliationResult> {
657 let instrument_id = instrument.id();
658 let account_id = mass_status.account_id;
659 let tol = tolerance.unwrap_or(DEFAULT_TOLERANCE);
660
661 let position_reports = mass_status.position_reports();
663 let venue_position = match position_reports.get(&instrument_id).and_then(|r| r.first()) {
664 Some(report) => position_report_to_snapshot(report),
665 None => {
666 return Ok(extract_instrument_reports(mass_status, instrument_id));
668 }
669 };
670
671 let extracted = extract_fills_for_instrument(mass_status, instrument_id);
673 let fill_snapshots = extracted.snapshots;
674 let mut order_map = extracted.orders;
675 let mut fill_map = extracted.fills;
676
677 if fill_snapshots.is_empty() {
678 return Ok(ReconciliationResult {
679 orders: order_map,
680 fills: fill_map,
681 });
682 }
683
684 let result = adjust_fills_for_partial_window(&fill_snapshots, &venue_position, instrument, tol);
686
687 match result {
689 FillAdjustmentResult::NoAdjustment => {}
690
691 FillAdjustmentResult::AddSyntheticOpening {
692 synthetic_fill,
693 existing_fills: _,
694 } => {
695 let venue_order_id = create_synthetic_venue_order_id(synthetic_fill.ts_event);
696 let order = create_synthetic_order_report(
697 &synthetic_fill,
698 account_id,
699 instrument_id,
700 instrument,
701 venue_order_id,
702 )?;
703 let fill = create_synthetic_fill_report(
704 &synthetic_fill,
705 account_id,
706 instrument_id,
707 instrument,
708 venue_order_id,
709 )?;
710
711 order_map.insert(venue_order_id, order);
712 fill_map.entry(venue_order_id).or_default().insert(0, fill);
713 }
714
715 FillAdjustmentResult::ReplaceCurrentLifecycle {
716 synthetic_fill,
717 first_venue_order_id,
718 } => {
719 let order = create_synthetic_order_report(
720 &synthetic_fill,
721 account_id,
722 instrument_id,
723 instrument,
724 first_venue_order_id,
725 )?;
726 let fill = create_synthetic_fill_report(
727 &synthetic_fill,
728 account_id,
729 instrument_id,
730 instrument,
731 first_venue_order_id,
732 )?;
733
734 order_map.clear();
736 fill_map.clear();
737 order_map.insert(first_venue_order_id, order);
738 fill_map.insert(first_venue_order_id, vec![fill]);
739 }
740
741 FillAdjustmentResult::FilterToCurrentLifecycle {
742 last_zero_crossing_ts,
743 current_lifecycle_fills: _,
744 } => {
745 for fills in fill_map.values_mut() {
747 fills.retain(|f| f.ts_event.as_u64() > last_zero_crossing_ts);
748 }
749 fill_map.retain(|_, fills| !fills.is_empty());
750
751 let orders_with_fills: ahash::AHashSet<VenueOrderId> =
753 fill_map.keys().copied().collect();
754 order_map.retain(|id, order| {
755 orders_with_fills.contains(id)
756 || !matches!(
757 order.order_status,
758 OrderStatus::Denied
759 | OrderStatus::Rejected
760 | OrderStatus::Canceled
761 | OrderStatus::Expired
762 | OrderStatus::Filled
763 )
764 });
765 }
766 }
767
768 Ok(ReconciliationResult {
769 orders: order_map,
770 fills: fill_map,
771 })
772}
773
774fn position_report_to_snapshot(report: &PositionStatusReport) -> VenuePositionSnapshot {
776 let side = match report.position_side {
777 PositionSideSpecified::Long => OrderSide::Buy,
778 PositionSideSpecified::Short => OrderSide::Sell,
779 PositionSideSpecified::Flat => OrderSide::Buy,
780 };
781
782 VenuePositionSnapshot {
783 side,
784 qty: report.quantity.into(),
785 avg_px: report.avg_px_open.unwrap_or(Decimal::ZERO),
786 }
787}
788
789fn extract_instrument_reports(
791 mass_status: &ExecutionMassStatus,
792 instrument_id: InstrumentId,
793) -> ReconciliationResult {
794 let mut orders = AHashMap::new();
795 let mut fills = AHashMap::new();
796
797 for (id, order) in mass_status.order_reports() {
798 if order.instrument_id == instrument_id {
799 orders.insert(id, order.clone());
800 }
801 }
802
803 for (id, fill_list) in mass_status.fill_reports() {
804 let filtered: Vec<_> = fill_list
805 .iter()
806 .filter(|f| f.instrument_id == instrument_id)
807 .cloned()
808 .collect();
809 if !filtered.is_empty() {
810 fills.insert(id, filtered);
811 }
812 }
813
814 ReconciliationResult { orders, fills }
815}
816
817struct ExtractedFills {
819 snapshots: Vec<FillSnapshot>,
820 orders: AHashMap<VenueOrderId, OrderStatusReport>,
821 fills: AHashMap<VenueOrderId, Vec<FillReport>>,
822}
823
824fn extract_fills_for_instrument(
826 mass_status: &ExecutionMassStatus,
827 instrument_id: InstrumentId,
828) -> ExtractedFills {
829 let mut snapshots = Vec::new();
830 let mut order_map = AHashMap::new();
831 let mut fill_map = AHashMap::new();
832
833 for (id, order) in mass_status.order_reports() {
835 if order.instrument_id == instrument_id {
836 order_map.insert(id, order.clone());
837 }
838 }
839
840 for (venue_order_id, fill_reports) in mass_status.fill_reports() {
842 for fill in fill_reports {
843 if fill.instrument_id == instrument_id {
844 let side = mass_status
845 .order_reports()
846 .get(&venue_order_id)
847 .map_or(fill.order_side, |o| o.order_side);
848
849 snapshots.push(FillSnapshot::new(
850 fill.ts_event.as_u64(),
851 side,
852 fill.last_qty.into(),
853 fill.last_px.into(),
854 venue_order_id,
855 ));
856
857 fill_map
858 .entry(venue_order_id)
859 .or_insert_with(Vec::new)
860 .push(fill.clone());
861 }
862 }
863 }
864
865 snapshots.sort_by_key(|f| f.ts_event);
867
868 ExtractedFills {
869 snapshots,
870 orders: order_map,
871 fills: fill_map,
872 }
873}
874
875#[cfg(test)]
876mod tests {
877 use nautilus_model::instruments::stubs::audusd_sim;
878 use rstest::{fixture, rstest};
879 use rust_decimal_macros::dec;
880
881 use super::*;
882
883 #[fixture]
884 fn instrument() -> InstrumentAny {
885 InstrumentAny::CurrencyPair(audusd_sim())
886 }
887
888 fn create_test_venue_order_id(value: &str) -> VenueOrderId {
889 VenueOrderId::new(value)
890 }
891
892 #[rstest]
893 fn test_fill_snapshot_direction() {
894 let venue_order_id = create_test_venue_order_id("ORDER1");
895 let buy_fill = FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id);
896 assert_eq!(buy_fill.direction(), 1);
897
898 let sell_fill =
899 FillSnapshot::new(2000, OrderSide::Sell, dec!(10), dec!(100), venue_order_id);
900 assert_eq!(sell_fill.direction(), -1);
901 }
902
903 #[rstest]
904 fn test_simulate_position_accumulate_long() {
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::Buy, dec!(5), dec!(102), venue_order_id),
909 ];
910
911 let (qty, value) = simulate_position(&fills);
912 assert_eq!(qty, dec!(15));
913 assert_eq!(value, dec!(1510)); }
915
916 #[rstest]
917 fn test_simulate_position_close_and_flip() {
918 let venue_order_id = create_test_venue_order_id("ORDER1");
919 let fills = vec![
920 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
921 FillSnapshot::new(2000, OrderSide::Sell, dec!(15), dec!(102), venue_order_id),
922 ];
923
924 let (qty, value) = simulate_position(&fills);
925 assert_eq!(qty, dec!(-5)); assert_eq!(value, dec!(510)); }
928
929 #[rstest]
930 fn test_simulate_position_partial_close() {
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::Sell, dec!(5), dec!(102), venue_order_id),
935 ];
936
937 let (qty, value) = simulate_position(&fills);
938 assert_eq!(qty, dec!(5));
939 assert_eq!(value, dec!(500)); let avg_px = value / qty;
943 assert_eq!(avg_px, dec!(100));
944 }
945
946 #[rstest]
947 fn test_simulate_position_multiple_partial_closes() {
948 let venue_order_id = create_test_venue_order_id("ORDER1");
949 let fills = vec![
950 FillSnapshot::new(1000, OrderSide::Buy, dec!(100), dec!(10.0), venue_order_id),
951 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), ];
954
955 let (qty, value) = simulate_position(&fills);
956 assert_eq!(qty, dec!(50));
957 assert!((value - dec!(500)).abs() < dec!(0.01));
961
962 let avg_px = value / qty;
964 assert!((avg_px - dec!(10.0)).abs() < dec!(0.01));
965 }
966
967 #[rstest]
968 fn test_simulate_position_short_partial_close() {
969 let venue_order_id = create_test_venue_order_id("ORDER1");
970 let fills = vec![
971 FillSnapshot::new(1000, OrderSide::Sell, dec!(10), dec!(100), venue_order_id),
972 FillSnapshot::new(2000, OrderSide::Buy, dec!(5), dec!(98), venue_order_id), ];
974
975 let (qty, value) = simulate_position(&fills);
976 assert_eq!(qty, dec!(-5));
977 assert_eq!(value, dec!(500)); let avg_px = value / qty.abs();
981 assert_eq!(avg_px, dec!(100));
982 }
983
984 #[rstest]
985 fn test_detect_zero_crossings() {
986 let venue_order_id = create_test_venue_order_id("ORDER1");
987 let fills = vec![
988 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
989 FillSnapshot::new(2000, OrderSide::Sell, dec!(10), dec!(102), venue_order_id), FillSnapshot::new(3000, OrderSide::Buy, dec!(5), dec!(103), venue_order_id),
991 FillSnapshot::new(4000, OrderSide::Sell, dec!(5), dec!(104), venue_order_id), ];
993
994 let crossings = detect_zero_crossings(&fills);
995 assert_eq!(crossings.len(), 2);
996 assert_eq!(crossings[0], 2000);
997 assert_eq!(crossings[1], 4000);
998 }
999
1000 #[rstest]
1001 fn test_check_position_match_exact() {
1002 let result = check_position_match(dec!(10), dec!(1000), dec!(10), dec!(100), dec!(0.0001));
1003 assert!(result);
1004 }
1005
1006 #[rstest]
1007 fn test_check_position_match_within_tolerance() {
1008 let result =
1011 check_position_match(dec!(10), dec!(1000), dec!(10), dec!(100.005), dec!(0.0001));
1012 assert!(result);
1013 }
1014
1015 #[rstest]
1016 fn test_check_position_match_qty_mismatch() {
1017 let result = check_position_match(dec!(10), dec!(1000), dec!(11), dec!(100), dec!(0.0001));
1018 assert!(!result);
1019 }
1020
1021 #[rstest]
1022 fn test_check_position_match_both_flat() {
1023 let result = check_position_match(dec!(0), dec!(0), dec!(0), dec!(0), dec!(0.0001));
1024 assert!(result);
1025 }
1026
1027 #[rstest]
1028 fn test_reconciliation_price_flat_to_long(_instrument: InstrumentAny) {
1029 let result = calculate_reconciliation_price(dec!(0), None, dec!(10), Some(dec!(100)));
1030 assert!(result.is_some());
1031 assert_eq!(result.unwrap(), dec!(100));
1032 }
1033
1034 #[rstest]
1035 fn test_reconciliation_price_no_target_avg_px(_instrument: InstrumentAny) {
1036 let result = calculate_reconciliation_price(dec!(5), Some(dec!(100)), dec!(10), None);
1037 assert!(result.is_none());
1038 }
1039
1040 #[rstest]
1041 fn test_reconciliation_price_no_quantity_change(_instrument: InstrumentAny) {
1042 let result =
1043 calculate_reconciliation_price(dec!(10), Some(dec!(100)), dec!(10), Some(dec!(105)));
1044 assert!(result.is_none());
1045 }
1046
1047 #[rstest]
1048 fn test_reconciliation_price_long_position_increase(_instrument: InstrumentAny) {
1049 let result =
1050 calculate_reconciliation_price(dec!(10), Some(dec!(100)), dec!(15), Some(dec!(102)));
1051 assert!(result.is_some());
1052 assert_eq!(result.unwrap(), dec!(106));
1054 }
1055
1056 #[rstest]
1057 fn test_reconciliation_price_flat_to_short(_instrument: InstrumentAny) {
1058 let result = calculate_reconciliation_price(dec!(0), None, dec!(-10), Some(dec!(100)));
1059 assert!(result.is_some());
1060 assert_eq!(result.unwrap(), dec!(100));
1061 }
1062
1063 #[rstest]
1064 fn test_reconciliation_price_long_to_flat(_instrument: InstrumentAny) {
1065 let result =
1068 calculate_reconciliation_price(dec!(100), Some(dec!(1.20)), dec!(0), Some(dec!(0)));
1069 assert!(result.is_some());
1070 assert_eq!(result.unwrap(), dec!(1.20));
1071 }
1072
1073 #[rstest]
1074 fn test_reconciliation_price_short_to_flat(_instrument: InstrumentAny) {
1075 let result = calculate_reconciliation_price(dec!(-50), Some(dec!(2.50)), dec!(0), None);
1078 assert!(result.is_some());
1079 assert_eq!(result.unwrap(), dec!(2.50));
1080 }
1081
1082 #[rstest]
1083 fn test_reconciliation_price_short_position_increase(_instrument: InstrumentAny) {
1084 let result = calculate_reconciliation_price(
1089 dec!(-100),
1090 Some(dec!(1.30)),
1091 dec!(-200),
1092 Some(dec!(1.28)),
1093 );
1094 assert!(result.is_some());
1095 assert_eq!(result.unwrap(), dec!(1.26));
1096 }
1097
1098 #[rstest]
1099 fn test_reconciliation_price_long_position_decrease(_instrument: InstrumentAny) {
1100 let result = calculate_reconciliation_price(
1102 dec!(200),
1103 Some(dec!(1.20)),
1104 dec!(100),
1105 Some(dec!(1.20)),
1106 );
1107 assert!(result.is_some());
1108 assert_eq!(result.unwrap(), dec!(1.20));
1109 }
1110
1111 #[rstest]
1112 fn test_reconciliation_price_long_to_short_flip(_instrument: InstrumentAny) {
1113 let result = calculate_reconciliation_price(
1116 dec!(100),
1117 Some(dec!(1.20)),
1118 dec!(-100),
1119 Some(dec!(1.25)),
1120 );
1121 assert!(result.is_some());
1122 assert_eq!(result.unwrap(), dec!(1.25));
1123 }
1124
1125 #[rstest]
1126 fn test_reconciliation_price_short_to_long_flip(_instrument: InstrumentAny) {
1127 let result = calculate_reconciliation_price(
1130 dec!(-100),
1131 Some(dec!(1.30)),
1132 dec!(100),
1133 Some(dec!(1.25)),
1134 );
1135 assert!(result.is_some());
1136 assert_eq!(result.unwrap(), dec!(1.25));
1137 }
1138
1139 #[rstest]
1140 fn test_reconciliation_price_complex_scenario(_instrument: InstrumentAny) {
1141 let result = calculate_reconciliation_price(
1146 dec!(150),
1147 Some(dec!(1.23456)),
1148 dec!(250),
1149 Some(dec!(1.24567)),
1150 );
1151 assert!(result.is_some());
1152 assert_eq!(result.unwrap(), dec!(1.262335));
1153 }
1154
1155 #[rstest]
1156 fn test_reconciliation_price_zero_target_avg_px(_instrument: InstrumentAny) {
1157 let result =
1158 calculate_reconciliation_price(dec!(100), Some(dec!(1.20)), dec!(200), Some(dec!(0)));
1159 assert!(result.is_none());
1160 }
1161
1162 #[rstest]
1163 fn test_reconciliation_price_negative_price(_instrument: InstrumentAny) {
1164 let result = calculate_reconciliation_price(
1169 dec!(100),
1170 Some(dec!(2.00)),
1171 dec!(200),
1172 Some(dec!(1.00)),
1173 );
1174 assert!(result.is_none());
1175 }
1176
1177 #[rstest]
1178 fn test_reconciliation_price_flip_simulation_compatibility() {
1179 let venue_order_id = create_test_venue_order_id("ORDER1");
1180 let recon_px = calculate_reconciliation_price(
1184 dec!(100),
1185 Some(dec!(1.20)),
1186 dec!(-100),
1187 Some(dec!(1.25)),
1188 )
1189 .expect("reconciliation price");
1190
1191 assert_eq!(recon_px, dec!(1.25));
1192
1193 let fills = vec![
1195 FillSnapshot::new(1000, OrderSide::Buy, dec!(100), dec!(1.20), venue_order_id),
1196 FillSnapshot::new(2000, OrderSide::Sell, dec!(200), recon_px, venue_order_id),
1197 ];
1198
1199 let (final_qty, final_value) = simulate_position(&fills);
1200 assert_eq!(final_qty, dec!(-100));
1201 let final_avg = final_value / final_qty.abs();
1202 assert_eq!(final_avg, dec!(1.25), "Final average should match target");
1203 }
1204
1205 #[rstest]
1206 fn test_reconciliation_price_accumulation_simulation_compatibility() {
1207 let venue_order_id = create_test_venue_order_id("ORDER1");
1208 let recon_px = calculate_reconciliation_price(
1211 dec!(100),
1212 Some(dec!(1.20)),
1213 dec!(200),
1214 Some(dec!(1.22)),
1215 )
1216 .expect("reconciliation price");
1217
1218 let fills = vec![
1220 FillSnapshot::new(1000, OrderSide::Buy, dec!(100), dec!(1.20), venue_order_id),
1221 FillSnapshot::new(2000, OrderSide::Buy, dec!(100), recon_px, venue_order_id),
1222 ];
1223
1224 let (final_qty, final_value) = simulate_position(&fills);
1225 assert_eq!(final_qty, dec!(200));
1226 let final_avg = final_value / final_qty.abs();
1227 assert_eq!(final_avg, dec!(1.22), "Final average should match target");
1228 }
1229
1230 #[rstest]
1231 fn test_simulate_position_accumulate_short() {
1232 let venue_order_id = create_test_venue_order_id("ORDER1");
1233 let fills = vec![
1234 FillSnapshot::new(1000, OrderSide::Sell, dec!(10), dec!(100), venue_order_id),
1235 FillSnapshot::new(2000, OrderSide::Sell, dec!(5), dec!(98), venue_order_id),
1236 ];
1237
1238 let (qty, value) = simulate_position(&fills);
1239 assert_eq!(qty, dec!(-15));
1240 assert_eq!(value, dec!(1490)); }
1242
1243 #[rstest]
1244 fn test_simulate_position_short_to_long_flip() {
1245 let venue_order_id = create_test_venue_order_id("ORDER1");
1246 let fills = vec![
1247 FillSnapshot::new(1000, OrderSide::Sell, dec!(10), dec!(100), venue_order_id),
1248 FillSnapshot::new(2000, OrderSide::Buy, dec!(15), dec!(102), venue_order_id),
1249 ];
1250
1251 let (qty, value) = simulate_position(&fills);
1252 assert_eq!(qty, dec!(5)); assert_eq!(value, dec!(510)); }
1255
1256 #[rstest]
1257 fn test_simulate_position_multiple_flips() {
1258 let venue_order_id = create_test_venue_order_id("ORDER1");
1259 let fills = vec![
1260 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
1261 FillSnapshot::new(2000, OrderSide::Sell, dec!(15), dec!(105), venue_order_id), FillSnapshot::new(3000, OrderSide::Buy, dec!(10), dec!(110), venue_order_id), ];
1264
1265 let (qty, value) = simulate_position(&fills);
1266 assert_eq!(qty, dec!(5)); assert_eq!(value, dec!(550)); }
1269
1270 #[rstest]
1271 fn test_simulate_position_empty_fills() {
1272 let fills: Vec<FillSnapshot> = vec![];
1273 let (qty, value) = simulate_position(&fills);
1274 assert_eq!(qty, dec!(0));
1275 assert_eq!(value, dec!(0));
1276 }
1277
1278 #[rstest]
1279 fn test_detect_zero_crossings_no_crossings() {
1280 let venue_order_id = create_test_venue_order_id("ORDER1");
1281 let fills = vec![
1282 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
1283 FillSnapshot::new(2000, OrderSide::Buy, dec!(5), dec!(102), venue_order_id),
1284 ];
1285
1286 let crossings = detect_zero_crossings(&fills);
1287 assert_eq!(crossings.len(), 0);
1288 }
1289
1290 #[rstest]
1291 fn test_detect_zero_crossings_single_crossing() {
1292 let venue_order_id = create_test_venue_order_id("ORDER1");
1293 let fills = vec![
1294 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
1295 FillSnapshot::new(2000, OrderSide::Sell, dec!(10), dec!(102), venue_order_id), ];
1297
1298 let crossings = detect_zero_crossings(&fills);
1299 assert_eq!(crossings.len(), 1);
1300 assert_eq!(crossings[0], 2000);
1301 }
1302
1303 #[rstest]
1304 fn test_detect_zero_crossings_empty_fills() {
1305 let fills: Vec<FillSnapshot> = vec![];
1306 let crossings = detect_zero_crossings(&fills);
1307 assert_eq!(crossings.len(), 0);
1308 }
1309
1310 #[rstest]
1311 fn test_detect_zero_crossings_long_to_short_flip() {
1312 let venue_order_id = create_test_venue_order_id("ORDER1");
1313 let fills = vec![
1315 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
1316 FillSnapshot::new(2000, OrderSide::Sell, dec!(15), dec!(102), venue_order_id), ];
1318
1319 let crossings = detect_zero_crossings(&fills);
1320 assert_eq!(crossings.len(), 1);
1321 assert_eq!(crossings[0], 2000); }
1323
1324 #[rstest]
1325 fn test_detect_zero_crossings_short_to_long_flip() {
1326 let venue_order_id = create_test_venue_order_id("ORDER1");
1327 let fills = vec![
1329 FillSnapshot::new(1000, OrderSide::Sell, dec!(10), dec!(100), venue_order_id),
1330 FillSnapshot::new(2000, OrderSide::Buy, dec!(20), dec!(102), venue_order_id), ];
1332
1333 let crossings = detect_zero_crossings(&fills);
1334 assert_eq!(crossings.len(), 1);
1335 assert_eq!(crossings[0], 2000);
1336 }
1337
1338 #[rstest]
1339 fn test_detect_zero_crossings_multiple_flips() {
1340 let venue_order_id = create_test_venue_order_id("ORDER1");
1341 let fills = vec![
1342 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
1343 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), ];
1347
1348 let crossings = detect_zero_crossings(&fills);
1349 assert_eq!(crossings.len(), 2);
1350 assert_eq!(crossings[0], 2000); assert_eq!(crossings[1], 4000); }
1353
1354 #[rstest]
1355 fn test_check_position_match_outside_tolerance() {
1356 let result = check_position_match(dec!(10), dec!(1000), dec!(10), dec!(101), dec!(0.0001));
1359 assert!(!result);
1360 }
1361
1362 #[rstest]
1363 fn test_check_position_match_edge_of_tolerance() {
1364 let result =
1367 check_position_match(dec!(10), dec!(1000), dec!(10), dec!(100.01), dec!(0.0001));
1368 assert!(result);
1369 }
1370
1371 #[rstest]
1372 fn test_check_position_match_zero_venue_avg_px() {
1373 let result = check_position_match(dec!(10), dec!(1000), dec!(10), dec!(0), dec!(0.0001));
1374 assert!(!result); }
1376
1377 #[rstest]
1378 fn test_adjust_fills_no_fills() {
1379 let venue_position = VenuePositionSnapshot {
1380 side: OrderSide::Buy,
1381 qty: dec!(0.02),
1382 avg_px: dec!(4100.00),
1383 };
1384 let instrument = instrument();
1385 let result =
1386 adjust_fills_for_partial_window(&[], &venue_position, &instrument, dec!(0.0001));
1387 assert!(matches!(result, FillAdjustmentResult::NoAdjustment));
1388 }
1389
1390 #[rstest]
1391 fn test_adjust_fills_flat_position() {
1392 let venue_order_id = create_test_venue_order_id("ORDER1");
1393 let fills = vec![FillSnapshot::new(
1394 1000,
1395 OrderSide::Buy,
1396 dec!(0.01),
1397 dec!(4100.00),
1398 venue_order_id,
1399 )];
1400 let venue_position = VenuePositionSnapshot {
1401 side: OrderSide::Buy,
1402 qty: dec!(0),
1403 avg_px: dec!(0),
1404 };
1405 let instrument = instrument();
1406 let result =
1407 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1408 assert!(matches!(result, FillAdjustmentResult::NoAdjustment));
1409 }
1410
1411 #[rstest]
1412 fn test_adjust_fills_complete_lifecycle_no_adjustment() {
1413 let venue_order_id = create_test_venue_order_id("ORDER1");
1414 let venue_order_id2 = create_test_venue_order_id("ORDER2");
1415 let fills = vec![
1416 FillSnapshot::new(
1417 1000,
1418 OrderSide::Buy,
1419 dec!(0.01),
1420 dec!(4100.00),
1421 venue_order_id,
1422 ),
1423 FillSnapshot::new(
1424 2000,
1425 OrderSide::Buy,
1426 dec!(0.01),
1427 dec!(4100.00),
1428 venue_order_id2,
1429 ),
1430 ];
1431 let venue_position = VenuePositionSnapshot {
1432 side: OrderSide::Buy,
1433 qty: dec!(0.02),
1434 avg_px: dec!(4100.00),
1435 };
1436 let instrument = instrument();
1437 let result =
1438 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1439 assert!(matches!(result, FillAdjustmentResult::NoAdjustment));
1440 }
1441
1442 #[rstest]
1443 fn test_adjust_fills_incomplete_lifecycle_adds_synthetic() {
1444 let venue_order_id = create_test_venue_order_id("ORDER1");
1445 let fills = vec![FillSnapshot::new(
1447 2000,
1448 OrderSide::Buy,
1449 dec!(0.02),
1450 dec!(4200.00),
1451 venue_order_id,
1452 )];
1453 let venue_position = VenuePositionSnapshot {
1454 side: OrderSide::Buy,
1455 qty: dec!(0.04),
1456 avg_px: dec!(4100.00),
1457 };
1458 let instrument = instrument();
1459 let result =
1460 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1461
1462 match result {
1463 FillAdjustmentResult::AddSyntheticOpening {
1464 synthetic_fill,
1465 existing_fills,
1466 } => {
1467 assert_eq!(synthetic_fill.side, OrderSide::Buy);
1468 assert_eq!(synthetic_fill.qty, dec!(0.02)); assert_eq!(existing_fills.len(), 1);
1470 }
1471 _ => panic!("Expected AddSyntheticOpening"),
1472 }
1473 }
1474
1475 #[rstest]
1476 fn test_adjust_fills_with_zero_crossings() {
1477 let venue_order_id1 = create_test_venue_order_id("ORDER1");
1478 let venue_order_id2 = create_test_venue_order_id("ORDER2");
1479 let venue_order_id3 = create_test_venue_order_id("ORDER3");
1480
1481 let fills = vec![
1484 FillSnapshot::new(
1485 1000,
1486 OrderSide::Buy,
1487 dec!(0.02),
1488 dec!(4100.00),
1489 venue_order_id1,
1490 ),
1491 FillSnapshot::new(
1492 2000,
1493 OrderSide::Sell,
1494 dec!(0.02),
1495 dec!(4150.00),
1496 venue_order_id2,
1497 ), FillSnapshot::new(
1499 3000,
1500 OrderSide::Buy,
1501 dec!(0.03),
1502 dec!(4200.00),
1503 venue_order_id3,
1504 ), ];
1506
1507 let venue_position = VenuePositionSnapshot {
1508 side: OrderSide::Buy,
1509 qty: dec!(0.03),
1510 avg_px: dec!(4200.00),
1511 };
1512
1513 let instrument = instrument();
1514 let result =
1515 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1516
1517 match result {
1519 FillAdjustmentResult::FilterToCurrentLifecycle {
1520 last_zero_crossing_ts,
1521 current_lifecycle_fills,
1522 } => {
1523 assert_eq!(last_zero_crossing_ts, 2000);
1524 assert_eq!(current_lifecycle_fills.len(), 1);
1525 assert_eq!(current_lifecycle_fills[0].venue_order_id, venue_order_id3);
1526 }
1527 _ => panic!("Expected FilterToCurrentLifecycle, was {result:?}"),
1528 }
1529 }
1530
1531 #[rstest]
1532 fn test_adjust_fills_multiple_zero_crossings_mismatch() {
1533 let venue_order_id1 = create_test_venue_order_id("ORDER1");
1534 let venue_order_id2 = create_test_venue_order_id("ORDER2");
1535 let _venue_order_id3 = create_test_venue_order_id("ORDER3");
1536 let venue_order_id4 = create_test_venue_order_id("ORDER4");
1537 let venue_order_id5 = create_test_venue_order_id("ORDER5");
1538
1539 let fills = vec![
1542 FillSnapshot::new(
1543 1000,
1544 OrderSide::Buy,
1545 dec!(0.05),
1546 dec!(4000.00),
1547 venue_order_id1,
1548 ),
1549 FillSnapshot::new(
1550 2000,
1551 OrderSide::Sell,
1552 dec!(0.05),
1553 dec!(4050.00),
1554 venue_order_id2,
1555 ), FillSnapshot::new(
1557 3000,
1558 OrderSide::Buy,
1559 dec!(0.05),
1560 dec!(4000.00),
1561 venue_order_id4,
1562 ), FillSnapshot::new(
1564 4000,
1565 OrderSide::Buy,
1566 dec!(0.05),
1567 dec!(4100.00),
1568 venue_order_id5,
1569 ), ];
1571
1572 let venue_position = VenuePositionSnapshot {
1573 side: OrderSide::Buy,
1574 qty: dec!(0.05),
1575 avg_px: dec!(4142.04),
1576 };
1577
1578 let instrument = instrument();
1579 let result =
1580 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1581
1582 match result {
1584 FillAdjustmentResult::ReplaceCurrentLifecycle {
1585 synthetic_fill,
1586 first_venue_order_id,
1587 } => {
1588 assert_eq!(synthetic_fill.qty, dec!(0.05));
1589 assert_eq!(synthetic_fill.px, dec!(4142.04));
1590 assert_eq!(synthetic_fill.side, OrderSide::Buy);
1591 assert_eq!(first_venue_order_id, venue_order_id4);
1592 }
1593 _ => panic!("Expected ReplaceCurrentLifecycle, was {result:?}"),
1594 }
1595 }
1596
1597 #[rstest]
1598 fn test_adjust_fills_short_position() {
1599 let venue_order_id = create_test_venue_order_id("ORDER1");
1600
1601 let fills = vec![FillSnapshot::new(
1603 1000,
1604 OrderSide::Sell,
1605 dec!(0.02),
1606 dec!(4120.00),
1607 venue_order_id,
1608 )];
1609
1610 let venue_position = VenuePositionSnapshot {
1611 side: OrderSide::Sell,
1612 qty: dec!(0.05),
1613 avg_px: dec!(4100.00),
1614 };
1615
1616 let instrument = instrument();
1617 let result =
1618 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1619
1620 match result {
1622 FillAdjustmentResult::AddSyntheticOpening {
1623 synthetic_fill,
1624 existing_fills,
1625 } => {
1626 assert_eq!(synthetic_fill.side, OrderSide::Sell);
1627 assert_eq!(synthetic_fill.qty, dec!(0.03)); assert_eq!(existing_fills.len(), 1);
1629 }
1630 _ => panic!("Expected AddSyntheticOpening, was {result:?}"),
1631 }
1632 }
1633
1634 #[rstest]
1635 fn test_adjust_fills_timestamp_underflow_protection() {
1636 let venue_order_id = create_test_venue_order_id("ORDER1");
1637
1638 let fills = vec![FillSnapshot::new(
1640 0,
1641 OrderSide::Buy,
1642 dec!(0.01),
1643 dec!(4100.00),
1644 venue_order_id,
1645 )];
1646
1647 let venue_position = VenuePositionSnapshot {
1648 side: OrderSide::Buy,
1649 qty: dec!(0.02),
1650 avg_px: dec!(4100.00),
1651 };
1652
1653 let instrument = instrument();
1654 let result =
1655 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1656
1657 match result {
1659 FillAdjustmentResult::AddSyntheticOpening { synthetic_fill, .. } => {
1660 assert_eq!(synthetic_fill.ts_event, 0); }
1662 _ => panic!("Expected AddSyntheticOpening, was {result:?}"),
1663 }
1664 }
1665
1666 #[rstest]
1667 fn test_adjust_fills_with_flip_scenario() {
1668 let venue_order_id1 = create_test_venue_order_id("ORDER1");
1669 let venue_order_id2 = create_test_venue_order_id("ORDER2");
1670
1671 let fills = vec![
1673 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id1),
1674 FillSnapshot::new(2000, OrderSide::Sell, dec!(20), dec!(105), venue_order_id2), ];
1676
1677 let venue_position = VenuePositionSnapshot {
1678 side: OrderSide::Sell,
1679 qty: dec!(10),
1680 avg_px: dec!(105),
1681 };
1682
1683 let instrument = instrument();
1684 let result =
1685 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1686
1687 match result {
1689 FillAdjustmentResult::NoAdjustment => {
1690 let (qty, value) = simulate_position(&fills);
1692 assert_eq!(qty, dec!(-10));
1693 let avg = value / qty.abs();
1694 assert_eq!(avg, dec!(105));
1695 }
1696 _ => panic!("Expected NoAdjustment for matching flip, was {result:?}"),
1697 }
1698 }
1699
1700 #[rstest]
1701 fn test_detect_zero_crossings_complex_lifecycle() {
1702 let venue_order_id = create_test_venue_order_id("ORDER1");
1703 let fills = vec![
1705 FillSnapshot::new(1000, OrderSide::Buy, dec!(100), dec!(1.20), venue_order_id),
1706 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), ];
1712
1713 let crossings = detect_zero_crossings(&fills);
1714 assert_eq!(crossings.len(), 3);
1715 assert_eq!(crossings[0], 3000); assert_eq!(crossings[1], 4000); assert_eq!(crossings[2], 6000); }
1719
1720 #[rstest]
1721 fn test_reconciliation_price_partial_close() {
1722 let venue_order_id = create_test_venue_order_id("ORDER1");
1723 let recon_px =
1725 calculate_reconciliation_price(dec!(100), Some(dec!(1.20)), dec!(50), Some(dec!(1.20)))
1726 .expect("reconciliation price");
1727
1728 let fills = vec![
1730 FillSnapshot::new(1000, OrderSide::Buy, dec!(100), dec!(1.20), venue_order_id),
1731 FillSnapshot::new(2000, OrderSide::Sell, dec!(50), recon_px, venue_order_id),
1732 ];
1733
1734 let (final_qty, final_value) = simulate_position(&fills);
1735 assert_eq!(final_qty, dec!(50));
1736 let final_avg = final_value / final_qty.abs();
1737 assert_eq!(final_avg, dec!(1.20), "Average should be maintained");
1738 }
1739
1740 #[rstest]
1741 fn test_detect_zero_crossings_identical_timestamps() {
1742 let venue_order_id1 = create_test_venue_order_id("ORDER1");
1743 let venue_order_id2 = create_test_venue_order_id("ORDER2");
1744
1745 let fills = vec![
1747 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id1),
1748 FillSnapshot::new(2000, OrderSide::Sell, dec!(5), dec!(102), venue_order_id1),
1749 FillSnapshot::new(2000, OrderSide::Sell, dec!(5), dec!(103), venue_order_id2), ];
1751
1752 let crossings = detect_zero_crossings(&fills);
1753
1754 assert_eq!(crossings.len(), 1);
1756 assert_eq!(crossings[0], 2000);
1757
1758 let (qty, _) = simulate_position(&fills);
1760 assert_eq!(qty, dec!(0));
1761 }
1762
1763 #[rstest]
1764 fn test_detect_zero_crossings_five_lifecycles() {
1765 let venue_order_id = create_test_venue_order_id("ORDER1");
1766
1767 let fills = vec![
1769 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
1771 FillSnapshot::new(2000, OrderSide::Sell, dec!(10), dec!(101), venue_order_id),
1772 FillSnapshot::new(3000, OrderSide::Sell, dec!(20), dec!(102), venue_order_id),
1774 FillSnapshot::new(4000, OrderSide::Buy, dec!(20), dec!(101), venue_order_id),
1775 FillSnapshot::new(5000, OrderSide::Buy, dec!(15), dec!(103), venue_order_id),
1777 FillSnapshot::new(6000, OrderSide::Sell, dec!(15), dec!(104), venue_order_id),
1778 FillSnapshot::new(7000, OrderSide::Sell, dec!(25), dec!(105), venue_order_id),
1780 FillSnapshot::new(8000, OrderSide::Buy, dec!(25), dec!(104), venue_order_id),
1781 FillSnapshot::new(9000, OrderSide::Buy, dec!(30), dec!(106), venue_order_id),
1783 ];
1784
1785 let crossings = detect_zero_crossings(&fills);
1786
1787 assert_eq!(crossings.len(), 4);
1789 assert_eq!(crossings[0], 2000);
1790 assert_eq!(crossings[1], 4000);
1791 assert_eq!(crossings[2], 6000);
1792 assert_eq!(crossings[3], 8000);
1793
1794 let (qty, _) = simulate_position(&fills);
1796 assert_eq!(qty, dec!(30));
1797 }
1798
1799 #[rstest]
1800 fn test_adjust_fills_five_zero_crossings(instrument: InstrumentAny) {
1801 let venue_order_id = create_test_venue_order_id("ORDER1");
1802
1803 let fills = vec![
1805 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
1807 FillSnapshot::new(2000, OrderSide::Sell, dec!(10), dec!(101), venue_order_id),
1808 FillSnapshot::new(3000, OrderSide::Sell, dec!(20), dec!(102), venue_order_id),
1809 FillSnapshot::new(4000, OrderSide::Buy, dec!(20), dec!(101), venue_order_id),
1810 FillSnapshot::new(5000, OrderSide::Buy, dec!(15), dec!(103), venue_order_id),
1811 FillSnapshot::new(6000, OrderSide::Sell, dec!(15), dec!(104), venue_order_id),
1812 FillSnapshot::new(7000, OrderSide::Sell, dec!(25), dec!(105), venue_order_id),
1813 FillSnapshot::new(8000, OrderSide::Buy, dec!(25), dec!(104), venue_order_id),
1814 FillSnapshot::new(9000, OrderSide::Buy, dec!(30), dec!(106), venue_order_id),
1816 ];
1817
1818 let venue_position = VenuePositionSnapshot {
1819 side: OrderSide::Buy,
1820 qty: dec!(30),
1821 avg_px: dec!(106),
1822 };
1823
1824 let result =
1825 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1826
1827 match result {
1829 FillAdjustmentResult::FilterToCurrentLifecycle {
1830 last_zero_crossing_ts,
1831 current_lifecycle_fills,
1832 } => {
1833 assert_eq!(last_zero_crossing_ts, 8000);
1834 assert_eq!(current_lifecycle_fills.len(), 1);
1835 assert_eq!(current_lifecycle_fills[0].ts_event, 9000);
1836 assert_eq!(current_lifecycle_fills[0].qty, dec!(30));
1837 }
1838 _ => panic!("Expected FilterToCurrentLifecycle, was {result:?}"),
1839 }
1840 }
1841
1842 #[rstest]
1843 fn test_adjust_fills_alternating_long_short_positions(instrument: InstrumentAny) {
1844 let venue_order_id = create_test_venue_order_id("ORDER1");
1845
1846 let fills = vec![
1849 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
1850 FillSnapshot::new(2000, OrderSide::Sell, dec!(20), dec!(102), venue_order_id), FillSnapshot::new(3000, OrderSide::Buy, dec!(20), dec!(101), venue_order_id), FillSnapshot::new(4000, OrderSide::Sell, dec!(20), dec!(103), venue_order_id), FillSnapshot::new(5000, OrderSide::Buy, dec!(20), dec!(102), venue_order_id), ];
1855
1856 let venue_position = VenuePositionSnapshot {
1858 side: OrderSide::Buy,
1859 qty: dec!(10),
1860 avg_px: dec!(102),
1861 };
1862
1863 let result =
1864 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1865
1866 assert!(
1870 matches!(result, FillAdjustmentResult::NoAdjustment),
1871 "Expected NoAdjustment (continuous lifecycle with matching position), was {result:?}"
1872 );
1873 }
1874
1875 #[rstest]
1876 fn test_adjust_fills_with_flat_crossings(instrument: InstrumentAny) {
1877 let venue_order_id = create_test_venue_order_id("ORDER1");
1878
1879 let fills = vec![
1881 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), venue_order_id),
1882 FillSnapshot::new(2000, OrderSide::Sell, dec!(10), dec!(102), venue_order_id), FillSnapshot::new(3000, OrderSide::Sell, dec!(10), dec!(101), venue_order_id), FillSnapshot::new(4000, OrderSide::Buy, dec!(10), dec!(99), venue_order_id), FillSnapshot::new(5000, OrderSide::Buy, dec!(10), dec!(98), venue_order_id), ];
1887
1888 let venue_position = VenuePositionSnapshot {
1890 side: OrderSide::Buy,
1891 qty: dec!(10),
1892 avg_px: dec!(98),
1893 };
1894
1895 let result =
1896 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1897
1898 match result {
1901 FillAdjustmentResult::FilterToCurrentLifecycle {
1902 last_zero_crossing_ts,
1903 current_lifecycle_fills,
1904 } => {
1905 assert_eq!(last_zero_crossing_ts, 4000);
1906 assert_eq!(current_lifecycle_fills.len(), 1);
1907 assert_eq!(current_lifecycle_fills[0].ts_event, 5000);
1908 assert_eq!(current_lifecycle_fills[0].qty, dec!(10));
1909 }
1910 _ => panic!("Expected FilterToCurrentLifecycle, was {result:?}"),
1911 }
1912 }
1913
1914 #[rstest]
1915 fn test_replace_current_lifecycle_uses_first_venue_order_id(instrument: InstrumentAny) {
1916 let order_id_1 = create_test_venue_order_id("ORDER1");
1917 let order_id_2 = create_test_venue_order_id("ORDER2");
1918 let order_id_3 = create_test_venue_order_id("ORDER3");
1919
1920 let fills = vec![
1922 FillSnapshot::new(1000, OrderSide::Buy, dec!(10), dec!(100), order_id_1),
1923 FillSnapshot::new(2000, OrderSide::Sell, dec!(10), dec!(102), order_id_1), FillSnapshot::new(3000, OrderSide::Buy, dec!(5), dec!(103), order_id_2),
1926 FillSnapshot::new(4000, OrderSide::Buy, dec!(5), dec!(104), order_id_3),
1927 ];
1928
1929 let venue_position = VenuePositionSnapshot {
1931 side: OrderSide::Buy,
1932 qty: dec!(15),
1933 avg_px: dec!(105),
1934 };
1935
1936 let result =
1937 adjust_fills_for_partial_window(&fills, &venue_position, &instrument, dec!(0.0001));
1938
1939 match result {
1941 FillAdjustmentResult::ReplaceCurrentLifecycle {
1942 synthetic_fill,
1943 first_venue_order_id,
1944 } => {
1945 assert_eq!(first_venue_order_id, order_id_2);
1946 assert_eq!(synthetic_fill.venue_order_id, order_id_2);
1947 assert_eq!(synthetic_fill.qty, dec!(15));
1948 assert_eq!(synthetic_fill.px, dec!(105));
1949 }
1950 _ => panic!("Expected ReplaceCurrentLifecycle, was {result:?}"),
1951 }
1952 }
1953}