1use std::{
22 fmt::Display,
23 hash::{Hash, Hasher},
24};
25
26use ahash::{AHashMap, AHashSet};
27use nautilus_core::{
28 UUID4, UnixNanos,
29 correctness::{FAILED, check_equal, check_predicate_true},
30};
31use rust_decimal::{Decimal, prelude::ToPrimitive};
32use serde::{Deserialize, Serialize};
33
34use crate::{
35 enums::{InstrumentClass, OrderSide, OrderSideSpecified, PositionAdjustmentType, PositionSide},
36 events::{OrderFilled, PositionAdjusted},
37 identifiers::{
38 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, Symbol, TradeId, TraderId,
39 Venue, VenueOrderId,
40 },
41 instruments::{Instrument, InstrumentAny},
42 types::{Currency, Money, Price, Quantity},
43};
44
45#[repr(C)]
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[cfg_attr(
52 feature = "python",
53 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
54)]
55pub struct Position {
56 pub events: Vec<OrderFilled>,
57 pub adjustments: Vec<PositionAdjusted>,
58 pub trader_id: TraderId,
59 pub strategy_id: StrategyId,
60 pub instrument_id: InstrumentId,
61 pub id: PositionId,
62 pub account_id: AccountId,
63 pub opening_order_id: ClientOrderId,
64 pub closing_order_id: Option<ClientOrderId>,
65 pub entry: OrderSide,
66 pub side: PositionSide,
67 pub signed_qty: f64,
68 pub quantity: Quantity,
69 pub peak_qty: Quantity,
70 pub price_precision: u8,
71 pub size_precision: u8,
72 pub multiplier: Quantity,
73 pub is_inverse: bool,
74 pub is_currency_pair: bool,
75 pub instrument_class: InstrumentClass,
76 pub base_currency: Option<Currency>,
77 pub quote_currency: Currency,
78 pub settlement_currency: Currency,
79 pub ts_init: UnixNanos,
80 pub ts_opened: UnixNanos,
81 pub ts_last: UnixNanos,
82 pub ts_closed: Option<UnixNanos>,
83 pub duration_ns: u64,
84 pub avg_px_open: f64,
85 pub avg_px_close: Option<f64>,
86 pub realized_return: f64,
87 pub realized_pnl: Option<Money>,
88 pub trade_ids: Vec<TradeId>,
89 pub buy_qty: Quantity,
90 pub sell_qty: Quantity,
91 pub commissions: AHashMap<Currency, Money>,
92}
93
94impl Position {
95 pub fn new(instrument: &InstrumentAny, fill: OrderFilled) -> Self {
104 check_equal(
105 &instrument.id(),
106 &fill.instrument_id,
107 "instrument.id()",
108 "fill.instrument_id",
109 )
110 .expect(FAILED);
111 assert_ne!(fill.order_side, OrderSide::NoOrderSide);
112
113 let position_id = fill.position_id.expect("No position ID to open `Position`");
114
115 let mut item = Self {
116 events: Vec::<OrderFilled>::new(),
117 adjustments: Vec::<PositionAdjusted>::new(),
118 trade_ids: Vec::<TradeId>::new(),
119 buy_qty: Quantity::zero(instrument.size_precision()),
120 sell_qty: Quantity::zero(instrument.size_precision()),
121 commissions: AHashMap::<Currency, Money>::new(),
122 trader_id: fill.trader_id,
123 strategy_id: fill.strategy_id,
124 instrument_id: fill.instrument_id,
125 id: position_id,
126 account_id: fill.account_id,
127 opening_order_id: fill.client_order_id,
128 closing_order_id: None,
129 entry: fill.order_side,
130 side: PositionSide::Flat,
131 signed_qty: 0.0,
132 quantity: fill.last_qty,
133 peak_qty: fill.last_qty,
134 price_precision: instrument.price_precision(),
135 size_precision: instrument.size_precision(),
136 multiplier: instrument.multiplier(),
137 is_inverse: instrument.is_inverse(),
138 is_currency_pair: matches!(instrument, InstrumentAny::CurrencyPair(_)),
139 instrument_class: instrument.instrument_class(),
140 base_currency: instrument.base_currency(),
141 quote_currency: instrument.quote_currency(),
142 settlement_currency: instrument.cost_currency(),
143 ts_init: fill.ts_init,
144 ts_opened: fill.ts_event,
145 ts_last: fill.ts_event,
146 ts_closed: None,
147 duration_ns: 0,
148 avg_px_open: fill.last_px.as_f64(),
149 avg_px_close: None,
150 realized_return: 0.0,
151 realized_pnl: None,
152 };
153 item.apply(&fill);
154 item
155 }
156
157 pub fn purge_events_for_order(&mut self, client_order_id: ClientOrderId) {
168 let filtered_events: Vec<OrderFilled> = self
169 .events
170 .iter()
171 .filter(|e| e.client_order_id != client_order_id)
172 .copied()
173 .collect();
174
175 let preserved_adjustments: Vec<PositionAdjusted> = self
178 .adjustments
179 .iter()
180 .filter(|adj| {
181 adj.adjustment_type != PositionAdjustmentType::Commission
184 })
185 .copied()
186 .collect();
187
188 if filtered_events.is_empty() {
190 log::warn!(
191 "Position {} has no fills remaining after purging order {}; consider closing the position instead",
192 self.id,
193 client_order_id
194 );
195 self.events.clear();
196 self.trade_ids.clear();
197 self.adjustments.clear();
198 self.buy_qty = Quantity::zero(self.size_precision);
199 self.sell_qty = Quantity::zero(self.size_precision);
200 self.commissions.clear();
201 self.signed_qty = 0.0;
202 self.quantity = Quantity::zero(self.size_precision);
203 self.side = PositionSide::Flat;
204 self.avg_px_close = None;
205 self.realized_pnl = None;
206 self.realized_return = 0.0;
207 self.ts_opened = UnixNanos::default();
208 self.ts_last = UnixNanos::default();
209 self.ts_closed = Some(UnixNanos::default());
210 self.duration_ns = 0;
211 return;
212 }
213
214 let position_id = self.id;
216 let size_precision = self.size_precision;
217
218 self.events = Vec::new();
220 self.trade_ids = Vec::new();
221 self.adjustments = Vec::new();
222 self.buy_qty = Quantity::zero(size_precision);
223 self.sell_qty = Quantity::zero(size_precision);
224 self.commissions.clear();
225 self.signed_qty = 0.0;
226 self.quantity = Quantity::zero(size_precision);
227 self.peak_qty = Quantity::zero(size_precision);
228 self.side = PositionSide::Flat;
229 self.avg_px_open = 0.0;
230 self.avg_px_close = None;
231 self.realized_pnl = None;
232 self.realized_return = 0.0;
233
234 let first_event = &filtered_events[0];
236 self.entry = first_event.order_side;
237 self.opening_order_id = first_event.client_order_id;
238 self.ts_opened = first_event.ts_event;
239 self.ts_init = first_event.ts_init;
240 self.closing_order_id = None;
241 self.ts_closed = None;
242 self.duration_ns = 0;
243
244 for event in filtered_events {
246 self.apply(&event);
247 }
248
249 for adjustment in preserved_adjustments {
251 self.apply_adjustment(adjustment);
252 }
253
254 log::info!(
255 "Purged fills for order {} from position {}; recalculated state: qty={}, signed_qty={}, side={:?}",
256 client_order_id,
257 position_id,
258 self.quantity,
259 self.signed_qty,
260 self.side
261 );
262 }
263
264 pub fn apply(&mut self, fill: &OrderFilled) {
270 check_predicate_true(
271 !self.trade_ids.contains(&fill.trade_id),
272 "`fill.trade_id` already contained in `trade_ids",
273 )
274 .expect(FAILED);
275 check_predicate_true(fill.ts_event >= self.ts_opened, "fill.ts_event < ts_opened")
276 .expect(FAILED);
277
278 if self.side == PositionSide::Flat {
279 self.events.clear();
281 self.trade_ids.clear();
282 self.adjustments.clear();
283 self.buy_qty = Quantity::zero(self.size_precision);
284 self.sell_qty = Quantity::zero(self.size_precision);
285 self.commissions.clear();
286 self.opening_order_id = fill.client_order_id;
287 self.closing_order_id = None;
288 self.peak_qty = Quantity::zero(self.size_precision);
289 self.ts_init = fill.ts_init;
290 self.ts_opened = fill.ts_event;
291 self.ts_closed = None;
292 self.duration_ns = 0;
293 self.avg_px_open = fill.last_px.as_f64();
294 self.avg_px_close = None;
295 self.realized_return = 0.0;
296 self.realized_pnl = None;
297 }
298
299 self.events.push(*fill);
300 self.trade_ids.push(fill.trade_id);
301
302 if let Some(commission) = fill.commission {
304 let commission_currency = commission.currency;
305 if let Some(existing_commission) = self.commissions.get_mut(&commission_currency) {
306 *existing_commission = *existing_commission + commission;
307 } else {
308 self.commissions.insert(commission_currency, commission);
309 }
310 }
311
312 match fill.specified_side() {
314 OrderSideSpecified::Buy => {
315 self.handle_buy_order_fill(fill);
316 }
317 OrderSideSpecified::Sell => {
318 self.handle_sell_order_fill(fill);
319 }
320 }
321
322 if self.is_currency_pair
324 && let Some(commission) = fill.commission
325 && let Some(base_currency) = self.base_currency
326 && commission.currency == base_currency
327 {
328 let adjustment = PositionAdjusted::new(
329 self.trader_id,
330 self.strategy_id,
331 self.instrument_id,
332 self.id,
333 self.account_id,
334 PositionAdjustmentType::Commission,
335 Some(-commission.as_decimal()),
336 None,
337 Some(fill.client_order_id.inner()),
338 UUID4::new(),
339 fill.ts_event,
340 fill.ts_init,
341 );
342 self.apply_adjustment(adjustment);
343 }
344
345 self.quantity = Quantity::new(self.signed_qty.abs(), self.size_precision);
347 if self.quantity > self.peak_qty {
348 self.peak_qty = self.quantity;
349 }
350
351 if self.quantity.is_zero() {
352 self.side = PositionSide::Flat;
353 self.signed_qty = 0.0; self.closing_order_id = Some(fill.client_order_id);
355 self.ts_closed = Some(fill.ts_event);
356 self.duration_ns = if let Some(ts_closed) = self.ts_closed {
357 ts_closed.as_u64() - self.ts_opened.as_u64()
358 } else {
359 0
360 };
361 } else if self.signed_qty > 0.0 {
362 self.entry = OrderSide::Buy;
363 self.side = PositionSide::Long;
364 } else {
365 self.entry = OrderSide::Sell;
366 self.side = PositionSide::Short;
367 }
368
369 self.ts_last = fill.ts_event;
370 }
371
372 fn handle_buy_order_fill(&mut self, fill: &OrderFilled) {
373 let mut realized_pnl = if let Some(commission) = fill.commission {
375 if commission.currency == self.settlement_currency {
376 -commission.as_f64()
377 } else {
378 0.0
379 }
380 } else {
381 0.0
382 };
383
384 let last_px = fill.last_px.as_f64();
385 let last_qty = fill.last_qty.as_f64();
386 let last_qty_object = fill.last_qty;
387
388 if self.signed_qty > 0.0 {
389 self.avg_px_open = self.calculate_avg_px_open_px(last_px, last_qty);
390 } else if self.signed_qty < 0.0 {
391 let avg_px_close = self.calculate_avg_px_close_px(last_px, last_qty);
393 self.avg_px_close = Some(avg_px_close);
394 self.realized_return = self
395 .calculate_return(self.avg_px_open, avg_px_close)
396 .unwrap_or_else(|e| {
397 log::error!("Error calculating return: {e}");
398 0.0
399 });
400 realized_pnl += self
401 .calculate_pnl_raw(self.avg_px_open, last_px, last_qty)
402 .unwrap_or_else(|e| {
403 log::error!("Error calculating PnL: {e}");
404 0.0
405 });
406 }
407
408 let current_pnl = self.realized_pnl.map_or(0.0, |p| p.as_f64());
409 self.realized_pnl = Some(Money::new(
410 current_pnl + realized_pnl,
411 self.settlement_currency,
412 ));
413
414 self.signed_qty += last_qty;
415 self.buy_qty = self.buy_qty + last_qty_object;
416 }
417
418 fn handle_sell_order_fill(&mut self, fill: &OrderFilled) {
419 let mut realized_pnl = if let Some(commission) = fill.commission {
421 if commission.currency == self.settlement_currency {
422 -commission.as_f64()
423 } else {
424 0.0
425 }
426 } else {
427 0.0
428 };
429
430 let last_px = fill.last_px.as_f64();
431 let last_qty = fill.last_qty.as_f64();
432 let last_qty_object = fill.last_qty;
433
434 if self.signed_qty < 0.0 {
435 self.avg_px_open = self.calculate_avg_px_open_px(last_px, last_qty);
436 } else if self.signed_qty > 0.0 {
437 let avg_px_close = self.calculate_avg_px_close_px(last_px, last_qty);
439 self.avg_px_close = Some(avg_px_close);
440 self.realized_return = self
441 .calculate_return(self.avg_px_open, avg_px_close)
442 .unwrap_or_else(|e| {
443 log::error!("Error calculating return: {e}");
444 0.0
445 });
446 realized_pnl += self
447 .calculate_pnl_raw(self.avg_px_open, last_px, last_qty)
448 .unwrap_or_else(|e| {
449 log::error!("Error calculating PnL: {e}");
450 0.0
451 });
452 }
453
454 let current_pnl = self.realized_pnl.map_or(0.0, |p| p.as_f64());
455 self.realized_pnl = Some(Money::new(
456 current_pnl + realized_pnl,
457 self.settlement_currency,
458 ));
459
460 self.signed_qty -= last_qty;
461 self.sell_qty = self.sell_qty + last_qty_object;
462 }
463
464 pub fn apply_adjustment(&mut self, adjustment: PositionAdjusted) {
477 if let Some(quantity_change) = adjustment.quantity_change {
479 self.signed_qty += quantity_change
480 .to_f64()
481 .expect("Failed to convert Decimal to f64");
482
483 self.quantity = Quantity::new(self.signed_qty.abs(), self.size_precision);
484
485 if self.quantity > self.peak_qty {
486 self.peak_qty = self.quantity;
487 }
488 }
489
490 if let Some(pnl_change) = adjustment.pnl_change {
492 self.realized_pnl = Some(match self.realized_pnl {
493 Some(current) => current + pnl_change,
494 None => pnl_change,
495 });
496 }
497
498 if self.quantity.is_zero() {
501 self.side = PositionSide::Flat;
502 self.signed_qty = 0.0; } else if self.signed_qty > 0.0 {
504 self.side = PositionSide::Long;
505 if self.entry == OrderSide::NoOrderSide {
506 self.entry = OrderSide::Buy;
507 }
508 } else {
509 self.side = PositionSide::Short;
510 if self.entry == OrderSide::NoOrderSide {
511 self.entry = OrderSide::Sell;
512 }
513 }
514
515 self.adjustments.push(adjustment);
516 self.ts_last = adjustment.ts_event;
517 }
518
519 fn calculate_avg_px(
561 &self,
562 qty: f64,
563 avg_pg: f64,
564 last_px: f64,
565 last_qty: f64,
566 ) -> anyhow::Result<f64> {
567 if qty == 0.0 && last_qty == 0.0 {
568 anyhow::bail!("Cannot calculate average price: both quantities are zero");
569 }
570
571 if last_qty == 0.0 {
572 anyhow::bail!("Cannot calculate average price: fill quantity is zero");
573 }
574
575 if qty == 0.0 {
576 return Ok(last_px);
577 }
578
579 let start_cost = avg_pg * qty;
580 let event_cost = last_px * last_qty;
581 let total_qty = qty + last_qty;
582
583 if total_qty <= 0.0 {
585 anyhow::bail!(
586 "Total quantity unexpectedly zero or negative in average price calculation: qty={qty}, last_qty={last_qty}, total_qty={total_qty}"
587 );
588 }
589
590 Ok((start_cost + event_cost) / total_qty)
591 }
592
593 fn calculate_avg_px_open_px(&self, last_px: f64, last_qty: f64) -> f64 {
594 self.calculate_avg_px(self.quantity.as_f64(), self.avg_px_open, last_px, last_qty)
595 .unwrap_or_else(|e| {
596 log::error!("Error calculating average open price: {e}");
597 last_px
598 })
599 }
600
601 fn calculate_avg_px_close_px(&self, last_px: f64, last_qty: f64) -> f64 {
602 let Some(avg_px_close) = self.avg_px_close else {
603 return last_px;
604 };
605 let closing_qty = if self.side == PositionSide::Long {
606 self.sell_qty
607 } else {
608 self.buy_qty
609 };
610 self.calculate_avg_px(closing_qty.as_f64(), avg_px_close, last_px, last_qty)
611 .unwrap_or_else(|e| {
612 log::error!("Error calculating average close price: {e}");
613 last_px
614 })
615 }
616
617 fn calculate_points(&self, avg_px_open: f64, avg_px_close: f64) -> f64 {
618 match self.side {
619 PositionSide::Long => avg_px_close - avg_px_open,
620 PositionSide::Short => avg_px_open - avg_px_close,
621 _ => 0.0, }
623 }
624
625 fn calculate_points_inverse(&self, avg_px_open: f64, avg_px_close: f64) -> anyhow::Result<f64> {
626 const EPSILON: f64 = 1e-15;
628
629 if avg_px_open.abs() < EPSILON {
631 anyhow::bail!(
632 "Cannot calculate inverse points: open price is zero or too small ({avg_px_open})"
633 );
634 }
635 if avg_px_close.abs() < EPSILON {
636 anyhow::bail!(
637 "Cannot calculate inverse points: close price is zero or too small ({avg_px_close})"
638 );
639 }
640
641 let inverse_open = 1.0 / avg_px_open;
642 let inverse_close = 1.0 / avg_px_close;
643 let result = match self.side {
644 PositionSide::Long => inverse_open - inverse_close,
645 PositionSide::Short => inverse_close - inverse_open,
646 _ => 0.0, };
648 Ok(result)
649 }
650
651 fn calculate_return(&self, avg_px_open: f64, avg_px_close: f64) -> anyhow::Result<f64> {
652 if avg_px_open == 0.0 {
654 anyhow::bail!(
655 "Cannot calculate return: open price is zero (close price: {avg_px_close})"
656 );
657 }
658 Ok(self.calculate_points(avg_px_open, avg_px_close) / avg_px_open)
659 }
660
661 fn calculate_pnl_raw(
662 &self,
663 avg_px_open: f64,
664 avg_px_close: f64,
665 quantity: f64,
666 ) -> anyhow::Result<f64> {
667 let quantity = quantity.min(self.signed_qty.abs());
668 let result = if self.is_inverse {
669 let points = self.calculate_points_inverse(avg_px_open, avg_px_close)?;
670 quantity * self.multiplier.as_f64() * points
671 } else {
672 quantity * self.multiplier.as_f64() * self.calculate_points(avg_px_open, avg_px_close)
673 };
674 Ok(result)
675 }
676
677 #[must_use]
679 pub fn calculate_pnl(&self, avg_px_open: f64, avg_px_close: f64, quantity: Quantity) -> Money {
680 let pnl_raw = self
681 .calculate_pnl_raw(avg_px_open, avg_px_close, quantity.as_f64())
682 .unwrap_or_else(|e| {
683 log::error!("Error calculating PnL: {e}");
684 0.0
685 });
686 Money::new(pnl_raw, self.settlement_currency)
687 }
688
689 #[must_use]
691 pub fn total_pnl(&self, last: Price) -> Money {
692 let unrealized = self.unrealized_pnl(last);
693 match self.realized_pnl {
694 Some(realized) => realized + unrealized,
695 None => unrealized,
696 }
697 }
698
699 #[must_use]
701 pub fn unrealized_pnl(&self, last: Price) -> Money {
702 if self.side == PositionSide::Flat {
703 Money::new(0.0, self.settlement_currency)
704 } else {
705 let avg_px_open = self.avg_px_open;
706 let avg_px_close = last.as_f64();
707 let quantity = self.quantity.as_f64();
708 let pnl = self
709 .calculate_pnl_raw(avg_px_open, avg_px_close, quantity)
710 .unwrap_or_else(|e| {
711 log::error!("Error calculating unrealized PnL: {e}");
712 0.0
713 });
714 Money::new(pnl, self.settlement_currency)
715 }
716 }
717
718 #[must_use]
720 pub fn closing_order_side(&self) -> OrderSide {
721 match self.side {
722 PositionSide::Long => OrderSide::Sell,
723 PositionSide::Short => OrderSide::Buy,
724 _ => OrderSide::NoOrderSide,
725 }
726 }
727
728 #[must_use]
730 pub fn is_opposite_side(&self, side: OrderSide) -> bool {
731 self.entry != side
732 }
733
734 #[must_use]
736 pub fn symbol(&self) -> Symbol {
737 self.instrument_id.symbol
738 }
739
740 #[must_use]
742 pub fn venue(&self) -> Venue {
743 self.instrument_id.venue
744 }
745
746 #[must_use]
748 pub fn event_count(&self) -> usize {
749 self.events.len()
750 }
751
752 #[must_use]
754 pub fn client_order_ids(&self) -> Vec<ClientOrderId> {
755 let mut result = self
757 .events
758 .iter()
759 .map(|event| event.client_order_id)
760 .collect::<AHashSet<ClientOrderId>>()
761 .into_iter()
762 .collect::<Vec<ClientOrderId>>();
763 result.sort_unstable();
764 result
765 }
766
767 #[must_use]
769 pub fn venue_order_ids(&self) -> Vec<VenueOrderId> {
770 let mut result = self
772 .events
773 .iter()
774 .map(|event| event.venue_order_id)
775 .collect::<AHashSet<VenueOrderId>>()
776 .into_iter()
777 .collect::<Vec<VenueOrderId>>();
778 result.sort_unstable();
779 result
780 }
781
782 #[must_use]
784 pub fn trade_ids(&self) -> Vec<TradeId> {
785 let mut result = self
786 .events
787 .iter()
788 .map(|event| event.trade_id)
789 .collect::<AHashSet<TradeId>>()
790 .into_iter()
791 .collect::<Vec<TradeId>>();
792 result.sort_unstable();
793 result
794 }
795
796 #[must_use]
803 pub fn notional_value(&self, last: Price) -> Money {
804 if self.is_inverse {
805 check_predicate_true(
806 last.is_positive(),
807 "last price must be positive for inverse instrument",
808 )
809 .expect(FAILED);
810 Money::new(
811 self.quantity.as_f64() * self.multiplier.as_f64() * (1.0 / last.as_f64()),
812 self.base_currency.unwrap(),
813 )
814 } else {
815 Money::new(
816 self.quantity.as_f64() * last.as_f64() * self.multiplier.as_f64(),
817 self.quote_currency,
818 )
819 }
820 }
821
822 #[must_use]
824 pub fn last_event(&self) -> Option<OrderFilled> {
825 self.events.last().copied()
826 }
827
828 #[must_use]
830 pub fn last_trade_id(&self) -> Option<TradeId> {
831 self.trade_ids.last().copied()
832 }
833
834 #[must_use]
836 pub fn is_long(&self) -> bool {
837 self.side == PositionSide::Long
838 }
839
840 #[must_use]
842 pub fn is_short(&self) -> bool {
843 self.side == PositionSide::Short
844 }
845
846 #[must_use]
848 pub fn is_open(&self) -> bool {
849 self.side != PositionSide::Flat && self.ts_closed.is_none()
850 }
851
852 #[must_use]
854 pub fn is_closed(&self) -> bool {
855 self.side == PositionSide::Flat && self.ts_closed.is_some()
856 }
857
858 #[must_use]
863 pub fn signed_decimal_qty(&self) -> Decimal {
864 Decimal::try_from(self.signed_qty).unwrap_or(Decimal::ZERO)
865 }
866
867 #[must_use]
869 pub fn commissions(&self) -> Vec<Money> {
870 self.commissions.values().copied().collect()
871 }
872}
873
874impl PartialEq<Self> for Position {
875 fn eq(&self, other: &Self) -> bool {
876 self.id == other.id
877 }
878}
879
880impl Eq for Position {}
881
882impl Hash for Position {
883 fn hash<H: Hasher>(&self, state: &mut H) {
884 self.id.hash(state);
885 }
886}
887
888impl Display for Position {
889 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
890 let quantity_str = if self.quantity == Quantity::zero(self.price_precision) {
891 String::new()
892 } else {
893 self.quantity.to_formatted_string() + " "
894 };
895 write!(
896 f,
897 "Position({} {}{}, id={})",
898 self.side, quantity_str, self.instrument_id, self.id
899 )
900 }
901}
902
903#[cfg(test)]
904mod tests {
905 use std::str::FromStr;
906
907 use nautilus_core::UnixNanos;
908 use rstest::rstest;
909 use rust_decimal::Decimal;
910
911 use crate::{
912 enums::{LiquiditySide, OrderSide, OrderType, PositionAdjustmentType, PositionSide},
913 events::{OrderFilled, PositionAdjusted},
914 identifiers::{
915 AccountId, ClientOrderId, PositionId, StrategyId, TradeId, VenueOrderId, stubs::uuid4,
916 },
917 instruments::{CryptoPerpetual, CurrencyPair, Instrument, InstrumentAny, stubs::*},
918 orders::{Order, builder::OrderTestBuilder, stubs::TestOrderEventStubs},
919 position::Position,
920 stubs::*,
921 types::{Currency, Money, Price, Quantity},
922 };
923
924 #[rstest]
925 fn test_position_long_display(stub_position_long: Position) {
926 let display = format!("{stub_position_long}");
927 assert_eq!(display, "Position(LONG 1 AUD/USD.SIM, id=1)");
928 }
929
930 #[rstest]
931 fn test_position_short_display(stub_position_short: Position) {
932 let display = format!("{stub_position_short}");
933 assert_eq!(display, "Position(SHORT 1 AUD/USD.SIM, id=1)");
934 }
935
936 #[rstest]
937 #[should_panic(expected = "`fill.trade_id` already contained in `trade_ids")]
938 fn test_two_trades_with_same_trade_id_error(audusd_sim: CurrencyPair) {
939 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
940 let order1 = OrderTestBuilder::new(OrderType::Market)
941 .instrument_id(audusd_sim.id())
942 .side(OrderSide::Buy)
943 .quantity(Quantity::from(100_000))
944 .build();
945 let order2 = OrderTestBuilder::new(OrderType::Market)
946 .instrument_id(audusd_sim.id())
947 .side(OrderSide::Buy)
948 .quantity(Quantity::from(100_000))
949 .build();
950 let fill1 = TestOrderEventStubs::filled(
951 &order1,
952 &audusd_sim,
953 Some(TradeId::new("1")),
954 None,
955 Some(Price::from("1.00001")),
956 None,
957 None,
958 None,
959 None,
960 None,
961 );
962 let fill2 = TestOrderEventStubs::filled(
963 &order2,
964 &audusd_sim,
965 Some(TradeId::new("1")),
966 None,
967 Some(Price::from("1.00002")),
968 None,
969 None,
970 None,
971 None,
972 None,
973 );
974 let mut position = Position::new(&audusd_sim, fill1.into());
975 position.apply(&fill2.into());
976 }
977
978 #[rstest]
979 fn test_position_filled_with_buy_order(audusd_sim: CurrencyPair) {
980 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
981 let order = OrderTestBuilder::new(OrderType::Market)
982 .instrument_id(audusd_sim.id())
983 .side(OrderSide::Buy)
984 .quantity(Quantity::from(100_000))
985 .build();
986 let fill = TestOrderEventStubs::filled(
987 &order,
988 &audusd_sim,
989 None,
990 None,
991 Some(Price::from("1.00001")),
992 None,
993 None,
994 None,
995 None,
996 None,
997 );
998 let last_price = Price::from_str("1.0005").unwrap();
999 let position = Position::new(&audusd_sim, fill.into());
1000 assert_eq!(position.symbol(), audusd_sim.id().symbol);
1001 assert_eq!(position.venue(), audusd_sim.id().venue);
1002 assert_eq!(position.closing_order_side(), OrderSide::Sell);
1003 assert!(!position.is_opposite_side(OrderSide::Buy));
1004 assert_eq!(position, position); assert!(position.closing_order_id.is_none());
1006 assert_eq!(position.quantity, Quantity::from(100_000));
1007 assert_eq!(position.peak_qty, Quantity::from(100_000));
1008 assert_eq!(position.size_precision, 0);
1009 assert_eq!(position.signed_qty, 100_000.0);
1010 assert_eq!(position.entry, OrderSide::Buy);
1011 assert_eq!(position.side, PositionSide::Long);
1012 assert_eq!(position.ts_opened.as_u64(), 0);
1013 assert_eq!(position.duration_ns, 0);
1014 assert_eq!(position.avg_px_open, 1.00001);
1015 assert_eq!(position.event_count(), 1);
1016 assert_eq!(position.id, PositionId::new("1"));
1017 assert_eq!(position.events.len(), 1);
1018 assert!(position.is_long());
1019 assert!(!position.is_short());
1020 assert!(position.is_open());
1021 assert!(!position.is_closed());
1022 assert_eq!(position.realized_return, 0.0);
1023 assert_eq!(position.realized_pnl, Some(Money::from("-2.0 USD")));
1024 assert_eq!(position.unrealized_pnl(last_price), Money::from("49.0 USD"));
1025 assert_eq!(position.total_pnl(last_price), Money::from("47.0 USD"));
1026 assert_eq!(position.commissions(), vec![Money::from("2.0 USD")]);
1027 assert_eq!(
1028 format!("{position}"),
1029 "Position(LONG 100_000 AUD/USD.SIM, id=1)"
1030 );
1031 }
1032
1033 #[rstest]
1034 fn test_position_filled_with_sell_order(audusd_sim: CurrencyPair) {
1035 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1036 let order = OrderTestBuilder::new(OrderType::Market)
1037 .instrument_id(audusd_sim.id())
1038 .side(OrderSide::Sell)
1039 .quantity(Quantity::from(100_000))
1040 .build();
1041 let fill = TestOrderEventStubs::filled(
1042 &order,
1043 &audusd_sim,
1044 None,
1045 None,
1046 Some(Price::from("1.00001")),
1047 None,
1048 None,
1049 None,
1050 None,
1051 None,
1052 );
1053 let last_price = Price::from_str("1.00050").unwrap();
1054 let position = Position::new(&audusd_sim, fill.into());
1055 assert_eq!(position.symbol(), audusd_sim.id().symbol);
1056 assert_eq!(position.venue(), audusd_sim.id().venue);
1057 assert_eq!(position.closing_order_side(), OrderSide::Buy);
1058 assert!(!position.is_opposite_side(OrderSide::Sell));
1059 assert_eq!(position, position); assert!(position.closing_order_id.is_none());
1061 assert_eq!(position.quantity, Quantity::from(100_000));
1062 assert_eq!(position.peak_qty, Quantity::from(100_000));
1063 assert_eq!(position.signed_qty, -100_000.0);
1064 assert_eq!(position.entry, OrderSide::Sell);
1065 assert_eq!(position.side, PositionSide::Short);
1066 assert_eq!(position.ts_opened.as_u64(), 0);
1067 assert_eq!(position.avg_px_open, 1.00001);
1068 assert_eq!(position.event_count(), 1);
1069 assert_eq!(position.id, PositionId::new("1"));
1070 assert_eq!(position.events.len(), 1);
1071 assert!(!position.is_long());
1072 assert!(position.is_short());
1073 assert!(position.is_open());
1074 assert!(!position.is_closed());
1075 assert_eq!(position.realized_return, 0.0);
1076 assert_eq!(position.realized_pnl, Some(Money::from("-2.0 USD")));
1077 assert_eq!(
1078 position.unrealized_pnl(last_price),
1079 Money::from("-49.0 USD")
1080 );
1081 assert_eq!(position.total_pnl(last_price), Money::from("-51.0 USD"));
1082 assert_eq!(position.commissions(), vec![Money::from("2.0 USD")]);
1083 assert_eq!(
1084 format!("{position}"),
1085 "Position(SHORT 100_000 AUD/USD.SIM, id=1)"
1086 );
1087 }
1088
1089 #[rstest]
1090 fn test_position_partial_fills_with_buy_order(audusd_sim: CurrencyPair) {
1091 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1092 let order = OrderTestBuilder::new(OrderType::Market)
1093 .instrument_id(audusd_sim.id())
1094 .side(OrderSide::Buy)
1095 .quantity(Quantity::from(100_000))
1096 .build();
1097 let fill = TestOrderEventStubs::filled(
1098 &order,
1099 &audusd_sim,
1100 None,
1101 None,
1102 Some(Price::from("1.00001")),
1103 Some(Quantity::from(50_000)),
1104 None,
1105 None,
1106 None,
1107 None,
1108 );
1109 let last_price = Price::from_str("1.00048").unwrap();
1110 let position = Position::new(&audusd_sim, fill.into());
1111 assert_eq!(position.quantity, Quantity::from(50_000));
1112 assert_eq!(position.peak_qty, Quantity::from(50_000));
1113 assert_eq!(position.side, PositionSide::Long);
1114 assert_eq!(position.signed_qty, 50000.0);
1115 assert_eq!(position.avg_px_open, 1.00001);
1116 assert_eq!(position.event_count(), 1);
1117 assert_eq!(position.ts_opened.as_u64(), 0);
1118 assert!(position.is_long());
1119 assert!(!position.is_short());
1120 assert!(position.is_open());
1121 assert!(!position.is_closed());
1122 assert_eq!(position.realized_return, 0.0);
1123 assert_eq!(position.realized_pnl, Some(Money::from("-2.0 USD")));
1124 assert_eq!(position.unrealized_pnl(last_price), Money::from("23.5 USD"));
1125 assert_eq!(position.total_pnl(last_price), Money::from("21.5 USD"));
1126 assert_eq!(position.commissions(), vec![Money::from("2.0 USD")]);
1127 assert_eq!(
1128 format!("{position}"),
1129 "Position(LONG 50_000 AUD/USD.SIM, id=1)"
1130 );
1131 }
1132
1133 #[rstest]
1134 fn test_position_partial_fills_with_two_sell_orders(audusd_sim: CurrencyPair) {
1135 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1136 let order = OrderTestBuilder::new(OrderType::Market)
1137 .instrument_id(audusd_sim.id())
1138 .side(OrderSide::Sell)
1139 .quantity(Quantity::from(100_000))
1140 .build();
1141 let fill1 = TestOrderEventStubs::filled(
1142 &order,
1143 &audusd_sim,
1144 Some(TradeId::new("1")),
1145 None,
1146 Some(Price::from("1.00001")),
1147 Some(Quantity::from(50_000)),
1148 None,
1149 None,
1150 None,
1151 None,
1152 );
1153 let fill2 = TestOrderEventStubs::filled(
1154 &order,
1155 &audusd_sim,
1156 Some(TradeId::new("2")),
1157 None,
1158 Some(Price::from("1.00002")),
1159 Some(Quantity::from(50_000)),
1160 None,
1161 None,
1162 None,
1163 None,
1164 );
1165 let last_price = Price::from_str("1.0005").unwrap();
1166 let mut position = Position::new(&audusd_sim, fill1.into());
1167 position.apply(&fill2.into());
1168
1169 assert_eq!(position.quantity, Quantity::from(100_000));
1170 assert_eq!(position.peak_qty, Quantity::from(100_000));
1171 assert_eq!(position.side, PositionSide::Short);
1172 assert_eq!(position.signed_qty, -100_000.0);
1173 assert_eq!(position.avg_px_open, 1.000_015);
1174 assert_eq!(position.event_count(), 2);
1175 assert_eq!(position.ts_opened, 0);
1176 assert!(position.is_short());
1177 assert!(!position.is_long());
1178 assert!(position.is_open());
1179 assert!(!position.is_closed());
1180 assert_eq!(position.realized_return, 0.0);
1181 assert_eq!(position.realized_pnl, Some(Money::from("-4.0 USD")));
1182 assert_eq!(
1183 position.unrealized_pnl(last_price),
1184 Money::from("-48.5 USD")
1185 );
1186 assert_eq!(position.total_pnl(last_price), Money::from("-52.5 USD"));
1187 assert_eq!(position.commissions(), vec![Money::from("4.0 USD")]);
1188 }
1189
1190 #[rstest]
1191 pub fn test_position_filled_with_buy_order_then_sell_order(audusd_sim: CurrencyPair) {
1192 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1193 let order = OrderTestBuilder::new(OrderType::Market)
1194 .instrument_id(audusd_sim.id())
1195 .side(OrderSide::Buy)
1196 .quantity(Quantity::from(150_000))
1197 .build();
1198 let fill = TestOrderEventStubs::filled(
1199 &order,
1200 &audusd_sim,
1201 Some(TradeId::new("1")),
1202 Some(PositionId::new("P-1")),
1203 Some(Price::from("1.00001")),
1204 None,
1205 None,
1206 None,
1207 Some(UnixNanos::from(1_000_000_000)),
1208 None,
1209 );
1210 let mut position = Position::new(&audusd_sim, fill.into());
1211
1212 let fill2 = OrderFilled::new(
1213 order.trader_id(),
1214 StrategyId::new("S-001"),
1215 order.instrument_id(),
1216 order.client_order_id(),
1217 VenueOrderId::from("2"),
1218 order.account_id().unwrap_or(AccountId::new("SIM-001")),
1219 TradeId::new("2"),
1220 OrderSide::Sell,
1221 OrderType::Market,
1222 order.quantity(),
1223 Price::from("1.00011"),
1224 audusd_sim.quote_currency(),
1225 LiquiditySide::Taker,
1226 uuid4(),
1227 2_000_000_000.into(),
1228 0.into(),
1229 false,
1230 Some(PositionId::new("T1")),
1231 Some(Money::from("0.0 USD")),
1232 );
1233 position.apply(&fill2);
1234 let last = Price::from_str("1.0005").unwrap();
1235
1236 assert!(position.is_opposite_side(fill2.order_side));
1237 assert_eq!(
1238 position.quantity,
1239 Quantity::zero(audusd_sim.price_precision())
1240 );
1241 assert_eq!(position.size_precision, 0);
1242 assert_eq!(position.signed_qty, 0.0);
1243 assert_eq!(position.side, PositionSide::Flat);
1244 assert_eq!(position.ts_opened, 1_000_000_000);
1245 assert_eq!(position.ts_closed, Some(UnixNanos::from(2_000_000_000)));
1246 assert_eq!(position.duration_ns, 1_000_000_000);
1247 assert_eq!(position.avg_px_open, 1.00001);
1248 assert_eq!(position.avg_px_close, Some(1.00011));
1249 assert!(!position.is_long());
1250 assert!(!position.is_short());
1251 assert!(!position.is_open());
1252 assert!(position.is_closed());
1253 assert_eq!(position.realized_return, 9.999_900_000_998_888e-5);
1254 assert_eq!(position.realized_pnl, Some(Money::from("13.0 USD")));
1255 assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1256 assert_eq!(position.commissions(), vec![Money::from("2 USD")]);
1257 assert_eq!(position.total_pnl(last), Money::from("13 USD"));
1258 assert_eq!(format!("{position}"), "Position(FLAT AUD/USD.SIM, id=P-1)");
1259 }
1260
1261 #[rstest]
1262 pub fn test_position_filled_with_sell_order_then_buy_order(audusd_sim: CurrencyPair) {
1263 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1264 let order1 = OrderTestBuilder::new(OrderType::Market)
1265 .instrument_id(audusd_sim.id())
1266 .side(OrderSide::Sell)
1267 .quantity(Quantity::from(100_000))
1268 .build();
1269 let order2 = OrderTestBuilder::new(OrderType::Market)
1270 .instrument_id(audusd_sim.id())
1271 .side(OrderSide::Buy)
1272 .quantity(Quantity::from(100_000))
1273 .build();
1274 let fill1 = TestOrderEventStubs::filled(
1275 &order1,
1276 &audusd_sim,
1277 None,
1278 Some(PositionId::new("P-19700101-000000-001-001-1")),
1279 Some(Price::from("1.0")),
1280 None,
1281 None,
1282 None,
1283 None,
1284 None,
1285 );
1286 let mut position = Position::new(&audusd_sim, fill1.into());
1287 let fill2 = TestOrderEventStubs::filled(
1289 &order2,
1290 &audusd_sim,
1291 Some(TradeId::new("1")),
1292 Some(PositionId::new("P-19700101-000000-001-001-1")),
1293 Some(Price::from("1.00001")),
1294 Some(Quantity::from(50_000)),
1295 None,
1296 None,
1297 None,
1298 None,
1299 );
1300 let fill3 = TestOrderEventStubs::filled(
1301 &order2,
1302 &audusd_sim,
1303 Some(TradeId::new("2")),
1304 Some(PositionId::new("P-19700101-000000-001-001-1")),
1305 Some(Price::from("1.00003")),
1306 Some(Quantity::from(50_000)),
1307 None,
1308 None,
1309 None,
1310 None,
1311 );
1312 let last = Price::from("1.0005");
1313 position.apply(&fill2.into());
1314 position.apply(&fill3.into());
1315
1316 assert_eq!(
1317 position.quantity,
1318 Quantity::zero(audusd_sim.price_precision())
1319 );
1320 assert_eq!(position.side, PositionSide::Flat);
1321 assert_eq!(position.ts_opened, 0);
1322 assert_eq!(position.avg_px_open, 1.0);
1323 assert_eq!(position.events.len(), 3);
1324 assert_eq!(position.ts_closed, Some(UnixNanos::default()));
1325 assert_eq!(position.avg_px_close, Some(1.00002));
1326 assert!(!position.is_long());
1327 assert!(!position.is_short());
1328 assert!(!position.is_open());
1329 assert!(position.is_closed());
1330 assert_eq!(position.commissions(), vec![Money::from("6.0 USD")]);
1331 assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1332 assert_eq!(position.realized_pnl, Some(Money::from("-8.0 USD")));
1333 assert_eq!(position.total_pnl(last), Money::from("-8.0 USD"));
1334 assert_eq!(
1335 format!("{position}"),
1336 "Position(FLAT AUD/USD.SIM, id=P-19700101-000000-001-001-1)"
1337 );
1338 }
1339
1340 #[rstest]
1341 fn test_position_filled_with_no_change(audusd_sim: CurrencyPair) {
1342 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1343 let order1 = OrderTestBuilder::new(OrderType::Market)
1344 .instrument_id(audusd_sim.id())
1345 .side(OrderSide::Buy)
1346 .quantity(Quantity::from(100_000))
1347 .build();
1348 let order2 = OrderTestBuilder::new(OrderType::Market)
1349 .instrument_id(audusd_sim.id())
1350 .side(OrderSide::Sell)
1351 .quantity(Quantity::from(100_000))
1352 .build();
1353 let fill1 = TestOrderEventStubs::filled(
1354 &order1,
1355 &audusd_sim,
1356 Some(TradeId::new("1")),
1357 Some(PositionId::new("P-19700101-000000-001-001-1")),
1358 Some(Price::from("1.0")),
1359 None,
1360 None,
1361 None,
1362 None,
1363 None,
1364 );
1365 let mut position = Position::new(&audusd_sim, fill1.into());
1366 let fill2 = TestOrderEventStubs::filled(
1367 &order2,
1368 &audusd_sim,
1369 Some(TradeId::new("2")),
1370 Some(PositionId::new("P-19700101-000000-001-001-1")),
1371 Some(Price::from("1.0")),
1372 None,
1373 None,
1374 None,
1375 None,
1376 None,
1377 );
1378 let last = Price::from("1.0005");
1379 position.apply(&fill2.into());
1380
1381 assert_eq!(
1382 position.quantity,
1383 Quantity::zero(audusd_sim.price_precision())
1384 );
1385 assert_eq!(position.closing_order_side(), OrderSide::NoOrderSide);
1386 assert_eq!(position.side, PositionSide::Flat);
1387 assert_eq!(position.ts_opened, 0);
1388 assert_eq!(position.avg_px_open, 1.0);
1389 assert_eq!(position.events.len(), 2);
1390 assert_eq!(position.ts_closed, Some(UnixNanos::default()));
1392 assert_eq!(position.avg_px_close, Some(1.0));
1393 assert!(!position.is_long());
1394 assert!(!position.is_short());
1395 assert!(!position.is_open());
1396 assert!(position.is_closed());
1397 assert_eq!(position.commissions(), vec![Money::from("4.0 USD")]);
1398 assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1399 assert_eq!(position.realized_pnl, Some(Money::from("-4.0 USD")));
1400 assert_eq!(position.total_pnl(last), Money::from("-4.0 USD"));
1401 assert_eq!(
1402 format!("{position}"),
1403 "Position(FLAT AUD/USD.SIM, id=P-19700101-000000-001-001-1)"
1404 );
1405 }
1406
1407 #[rstest]
1408 fn test_position_long_with_multiple_filled_orders(audusd_sim: CurrencyPair) {
1409 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1410 let order1 = OrderTestBuilder::new(OrderType::Market)
1411 .instrument_id(audusd_sim.id())
1412 .side(OrderSide::Buy)
1413 .quantity(Quantity::from(100_000))
1414 .build();
1415 let order2 = OrderTestBuilder::new(OrderType::Market)
1416 .instrument_id(audusd_sim.id())
1417 .side(OrderSide::Buy)
1418 .quantity(Quantity::from(100_000))
1419 .build();
1420 let order3 = OrderTestBuilder::new(OrderType::Market)
1421 .instrument_id(audusd_sim.id())
1422 .side(OrderSide::Sell)
1423 .quantity(Quantity::from(200_000))
1424 .build();
1425 let fill1 = TestOrderEventStubs::filled(
1426 &order1,
1427 &audusd_sim,
1428 Some(TradeId::new("1")),
1429 Some(PositionId::new("P-123456")),
1430 Some(Price::from("1.0")),
1431 None,
1432 None,
1433 None,
1434 None,
1435 None,
1436 );
1437 let fill2 = TestOrderEventStubs::filled(
1438 &order2,
1439 &audusd_sim,
1440 Some(TradeId::new("2")),
1441 Some(PositionId::new("P-123456")),
1442 Some(Price::from("1.00001")),
1443 None,
1444 None,
1445 None,
1446 None,
1447 None,
1448 );
1449 let fill3 = TestOrderEventStubs::filled(
1450 &order3,
1451 &audusd_sim,
1452 Some(TradeId::new("3")),
1453 Some(PositionId::new("P-123456")),
1454 Some(Price::from("1.0001")),
1455 None,
1456 None,
1457 None,
1458 None,
1459 None,
1460 );
1461 let mut position = Position::new(&audusd_sim, fill1.into());
1462 let last = Price::from("1.0005");
1463 position.apply(&fill2.into());
1464 position.apply(&fill3.into());
1465
1466 assert_eq!(
1467 position.quantity,
1468 Quantity::zero(audusd_sim.price_precision())
1469 );
1470 assert_eq!(position.side, PositionSide::Flat);
1471 assert_eq!(position.ts_opened, 0);
1472 assert_eq!(position.avg_px_open, 1.000_005);
1473 assert_eq!(position.events.len(), 3);
1474 assert_eq!(position.ts_closed, Some(UnixNanos::default()));
1479 assert_eq!(position.avg_px_close, Some(1.0001));
1480 assert!(position.is_closed());
1481 assert!(!position.is_open());
1482 assert!(!position.is_long());
1483 assert!(!position.is_short());
1484 assert_eq!(position.commissions(), vec![Money::from("6.0 USD")]);
1485 assert_eq!(position.realized_pnl, Some(Money::from("13.0 USD")));
1486 assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1487 assert_eq!(position.total_pnl(last), Money::from("13 USD"));
1488 assert_eq!(
1489 format!("{position}"),
1490 "Position(FLAT AUD/USD.SIM, id=P-123456)"
1491 );
1492 }
1493
1494 #[rstest]
1495 fn test_pnl_calculation_from_trading_technologies_example(currency_pair_ethusdt: CurrencyPair) {
1496 let ethusdt = InstrumentAny::CurrencyPair(currency_pair_ethusdt);
1497 let quantity1 = Quantity::from(12);
1498 let price1 = Price::from("100.0");
1499 let order1 = OrderTestBuilder::new(OrderType::Market)
1500 .instrument_id(ethusdt.id())
1501 .side(OrderSide::Buy)
1502 .quantity(quantity1)
1503 .build();
1504 let commission1 = calculate_commission(ðusdt, order1.quantity(), price1, None);
1505 let fill1 = TestOrderEventStubs::filled(
1506 &order1,
1507 ðusdt,
1508 Some(TradeId::new("1")),
1509 Some(PositionId::new("P-123456")),
1510 Some(price1),
1511 None,
1512 None,
1513 Some(commission1),
1514 None,
1515 None,
1516 );
1517 let mut position = Position::new(ðusdt, fill1.into());
1518 let quantity2 = Quantity::from(17);
1519 let order2 = OrderTestBuilder::new(OrderType::Market)
1520 .instrument_id(ethusdt.id())
1521 .side(OrderSide::Buy)
1522 .quantity(quantity2)
1523 .build();
1524 let price2 = Price::from("99.0");
1525 let commission2 = calculate_commission(ðusdt, order2.quantity(), price2, None);
1526 let fill2 = TestOrderEventStubs::filled(
1527 &order2,
1528 ðusdt,
1529 Some(TradeId::new("2")),
1530 Some(PositionId::new("P-123456")),
1531 Some(price2),
1532 None,
1533 None,
1534 Some(commission2),
1535 None,
1536 None,
1537 );
1538 position.apply(&fill2.into());
1539 assert_eq!(position.quantity, Quantity::from(29));
1540 assert_eq!(position.realized_pnl, Some(Money::from("-0.28830000 USDT")));
1541 assert_eq!(position.avg_px_open, 99.413_793_103_448_27);
1542 let quantity3 = Quantity::from(9);
1543 let order3 = OrderTestBuilder::new(OrderType::Market)
1544 .instrument_id(ethusdt.id())
1545 .side(OrderSide::Sell)
1546 .quantity(quantity3)
1547 .build();
1548 let price3 = Price::from("101.0");
1549 let commission3 = calculate_commission(ðusdt, order3.quantity(), price3, None);
1550 let fill3 = TestOrderEventStubs::filled(
1551 &order3,
1552 ðusdt,
1553 Some(TradeId::new("3")),
1554 Some(PositionId::new("P-123456")),
1555 Some(price3),
1556 None,
1557 None,
1558 Some(commission3),
1559 None,
1560 None,
1561 );
1562 position.apply(&fill3.into());
1563 assert_eq!(position.quantity, Quantity::from(20));
1564 assert_eq!(position.realized_pnl, Some(Money::from("13.89666207 USDT")));
1565 assert_eq!(position.avg_px_open, 99.413_793_103_448_27);
1566 let quantity4 = Quantity::from("4");
1567 let price4 = Price::from("105.0");
1568 let order4 = OrderTestBuilder::new(OrderType::Market)
1569 .instrument_id(ethusdt.id())
1570 .side(OrderSide::Sell)
1571 .quantity(quantity4)
1572 .build();
1573 let commission4 = calculate_commission(ðusdt, order4.quantity(), price4, None);
1574 let fill4 = TestOrderEventStubs::filled(
1575 &order4,
1576 ðusdt,
1577 Some(TradeId::new("4")),
1578 Some(PositionId::new("P-123456")),
1579 Some(price4),
1580 None,
1581 None,
1582 Some(commission4),
1583 None,
1584 None,
1585 );
1586 position.apply(&fill4.into());
1587 assert_eq!(position.quantity, Quantity::from("16"));
1588 assert_eq!(position.realized_pnl, Some(Money::from("36.19948966 USDT")));
1589 assert_eq!(position.avg_px_open, 99.413_793_103_448_27);
1590 let quantity5 = Quantity::from("3");
1591 let price5 = Price::from("103.0");
1592 let order5 = OrderTestBuilder::new(OrderType::Market)
1593 .instrument_id(ethusdt.id())
1594 .side(OrderSide::Buy)
1595 .quantity(quantity5)
1596 .build();
1597 let commission5 = calculate_commission(ðusdt, order5.quantity(), price5, None);
1598 let fill5 = TestOrderEventStubs::filled(
1599 &order5,
1600 ðusdt,
1601 Some(TradeId::new("5")),
1602 Some(PositionId::new("P-123456")),
1603 Some(price5),
1604 None,
1605 None,
1606 Some(commission5),
1607 None,
1608 None,
1609 );
1610 position.apply(&fill5.into());
1611 assert_eq!(position.quantity, Quantity::from("19"));
1612 assert_eq!(position.realized_pnl, Some(Money::from("36.16858966 USDT")));
1613 assert_eq!(position.avg_px_open, 99.980_036_297_640_65);
1614 assert_eq!(
1615 format!("{position}"),
1616 "Position(LONG 19.00000 ETHUSDT.BINANCE, id=P-123456)"
1617 );
1618 }
1619
1620 #[rstest]
1621 fn test_position_closed_and_reopened(audusd_sim: CurrencyPair) {
1622 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1623 let quantity1 = Quantity::from(150_000);
1624 let price1 = Price::from("1.00001");
1625 let order = OrderTestBuilder::new(OrderType::Market)
1626 .instrument_id(audusd_sim.id())
1627 .side(OrderSide::Buy)
1628 .quantity(quantity1)
1629 .build();
1630 let commission1 = calculate_commission(&audusd_sim, quantity1, price1, None);
1631 let fill1 = TestOrderEventStubs::filled(
1632 &order,
1633 &audusd_sim,
1634 Some(TradeId::new("5")),
1635 Some(PositionId::new("P-123456")),
1636 Some(Price::from("1.00001")),
1637 None,
1638 None,
1639 Some(commission1),
1640 Some(UnixNanos::from(1_000_000_000)),
1641 None,
1642 );
1643 let mut position = Position::new(&audusd_sim, fill1.into());
1644
1645 let fill2 = OrderFilled::new(
1646 order.trader_id(),
1647 order.strategy_id(),
1648 order.instrument_id(),
1649 order.client_order_id(),
1650 VenueOrderId::from("2"),
1651 order.account_id().unwrap_or(AccountId::new("SIM-001")),
1652 TradeId::from("2"),
1653 OrderSide::Sell,
1654 OrderType::Market,
1655 order.quantity(),
1656 Price::from("1.00011"),
1657 audusd_sim.quote_currency(),
1658 LiquiditySide::Taker,
1659 uuid4(),
1660 UnixNanos::from(2_000_000_000),
1661 UnixNanos::default(),
1662 false,
1663 Some(PositionId::from("P-123456")),
1664 Some(Money::from("0 USD")),
1665 );
1666
1667 position.apply(&fill2);
1668
1669 let fill3 = OrderFilled::new(
1670 order.trader_id(),
1671 order.strategy_id(),
1672 order.instrument_id(),
1673 order.client_order_id(),
1674 VenueOrderId::from("2"),
1675 order.account_id().unwrap_or(AccountId::new("SIM-001")),
1676 TradeId::from("3"),
1677 OrderSide::Buy,
1678 OrderType::Market,
1679 order.quantity(),
1680 Price::from("1.00012"),
1681 audusd_sim.quote_currency(),
1682 LiquiditySide::Taker,
1683 uuid4(),
1684 UnixNanos::from(3_000_000_000),
1685 UnixNanos::default(),
1686 false,
1687 Some(PositionId::from("P-123456")),
1688 Some(Money::from("0 USD")),
1689 );
1690
1691 position.apply(&fill3);
1692
1693 let last = Price::from("1.0003");
1694 assert!(position.is_opposite_side(fill2.order_side));
1695 assert_eq!(position.quantity, Quantity::from(150_000));
1696 assert_eq!(position.peak_qty, Quantity::from(150_000));
1697 assert_eq!(position.side, PositionSide::Long);
1698 assert_eq!(position.opening_order_id, fill3.client_order_id);
1699 assert_eq!(position.closing_order_id, None);
1700 assert_eq!(position.closing_order_id, None);
1701 assert_eq!(position.ts_opened, 3_000_000_000);
1702 assert_eq!(position.duration_ns, 0);
1703 assert_eq!(position.avg_px_open, 1.00012);
1704 assert_eq!(position.event_count(), 1);
1705 assert_eq!(position.ts_closed, None);
1706 assert_eq!(position.avg_px_close, None);
1707 assert!(position.is_long());
1708 assert!(!position.is_short());
1709 assert!(position.is_open());
1710 assert!(!position.is_closed());
1711 assert_eq!(position.realized_return, 0.0);
1712 assert_eq!(position.realized_pnl, Some(Money::from("0 USD")));
1713 assert_eq!(position.unrealized_pnl(last), Money::from("27 USD"));
1714 assert_eq!(position.total_pnl(last), Money::from("27 USD"));
1715 assert_eq!(position.commissions(), vec![Money::from("0 USD")]);
1716 assert_eq!(
1717 format!("{position}"),
1718 "Position(LONG 150_000 AUD/USD.SIM, id=P-123456)"
1719 );
1720 }
1721
1722 #[rstest]
1723 fn test_position_realized_pnl_with_interleaved_order_sides(
1724 currency_pair_btcusdt: CurrencyPair,
1725 ) {
1726 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1727 let order1 = OrderTestBuilder::new(OrderType::Market)
1728 .instrument_id(btcusdt.id())
1729 .side(OrderSide::Buy)
1730 .quantity(Quantity::from(12))
1731 .build();
1732 let commission1 =
1733 calculate_commission(&btcusdt, order1.quantity(), Price::from("10000.0"), None);
1734 let fill1 = TestOrderEventStubs::filled(
1735 &order1,
1736 &btcusdt,
1737 Some(TradeId::from("1")),
1738 Some(PositionId::from("P-19700101-000000-001-001-1")),
1739 Some(Price::from("10000.0")),
1740 None,
1741 None,
1742 Some(commission1),
1743 None,
1744 None,
1745 );
1746 let mut position = Position::new(&btcusdt, fill1.into());
1747 let order2 = OrderTestBuilder::new(OrderType::Market)
1748 .instrument_id(btcusdt.id())
1749 .side(OrderSide::Buy)
1750 .quantity(Quantity::from(17))
1751 .build();
1752 let commission2 =
1753 calculate_commission(&btcusdt, order2.quantity(), Price::from("9999.0"), None);
1754 let fill2 = TestOrderEventStubs::filled(
1755 &order2,
1756 &btcusdt,
1757 Some(TradeId::from("2")),
1758 Some(PositionId::from("P-19700101-000000-001-001-1")),
1759 Some(Price::from("9999.0")),
1760 None,
1761 None,
1762 Some(commission2),
1763 None,
1764 None,
1765 );
1766 position.apply(&fill2.into());
1767 assert_eq!(position.quantity, Quantity::from(29));
1768 assert_eq!(
1769 position.realized_pnl,
1770 Some(Money::from("-289.98300000 USDT"))
1771 );
1772 assert_eq!(position.avg_px_open, 9_999.413_793_103_447);
1773 let order3 = OrderTestBuilder::new(OrderType::Market)
1774 .instrument_id(btcusdt.id())
1775 .side(OrderSide::Sell)
1776 .quantity(Quantity::from(9))
1777 .build();
1778 let commission3 =
1779 calculate_commission(&btcusdt, order3.quantity(), Price::from("10001.0"), None);
1780 let fill3 = TestOrderEventStubs::filled(
1781 &order3,
1782 &btcusdt,
1783 Some(TradeId::from("3")),
1784 Some(PositionId::from("P-19700101-000000-001-001-1")),
1785 Some(Price::from("10001.0")),
1786 None,
1787 None,
1788 Some(commission3),
1789 None,
1790 None,
1791 );
1792 position.apply(&fill3.into());
1793 assert_eq!(position.quantity, Quantity::from(20));
1794 assert_eq!(
1795 position.realized_pnl,
1796 Some(Money::from("-365.71613793 USDT"))
1797 );
1798 assert_eq!(position.avg_px_open, 9_999.413_793_103_447);
1799 let order4 = OrderTestBuilder::new(OrderType::Market)
1800 .instrument_id(btcusdt.id())
1801 .side(OrderSide::Buy)
1802 .quantity(Quantity::from(3))
1803 .build();
1804 let commission4 =
1805 calculate_commission(&btcusdt, order4.quantity(), Price::from("10003.0"), None);
1806 let fill4 = TestOrderEventStubs::filled(
1807 &order4,
1808 &btcusdt,
1809 Some(TradeId::from("4")),
1810 Some(PositionId::from("P-19700101-000000-001-001-1")),
1811 Some(Price::from("10003.0")),
1812 None,
1813 None,
1814 Some(commission4),
1815 None,
1816 None,
1817 );
1818 position.apply(&fill4.into());
1819 assert_eq!(position.quantity, Quantity::from(23));
1820 assert_eq!(
1821 position.realized_pnl,
1822 Some(Money::from("-395.72513793 USDT"))
1823 );
1824 assert_eq!(position.avg_px_open, 9_999.881_559_220_39);
1825 let order5 = OrderTestBuilder::new(OrderType::Market)
1826 .instrument_id(btcusdt.id())
1827 .side(OrderSide::Sell)
1828 .quantity(Quantity::from(4))
1829 .build();
1830 let commission5 =
1831 calculate_commission(&btcusdt, order5.quantity(), Price::from("10005.0"), None);
1832 let fill5 = TestOrderEventStubs::filled(
1833 &order5,
1834 &btcusdt,
1835 Some(TradeId::from("5")),
1836 Some(PositionId::from("P-19700101-000000-001-001-1")),
1837 Some(Price::from("10005.0")),
1838 None,
1839 None,
1840 Some(commission5),
1841 None,
1842 None,
1843 );
1844 position.apply(&fill5.into());
1845 assert_eq!(position.quantity, Quantity::from(19));
1846 assert_eq!(
1847 position.realized_pnl,
1848 Some(Money::from("-415.27137481 USDT"))
1849 );
1850 assert_eq!(position.avg_px_open, 9_999.881_559_220_39);
1851 assert_eq!(
1852 format!("{position}"),
1853 "Position(LONG 19.000000 BTCUSDT.BINANCE, id=P-19700101-000000-001-001-1)"
1854 );
1855 }
1856
1857 #[rstest]
1858 fn test_calculate_pnl_when_given_position_side_flat_returns_zero(
1859 currency_pair_btcusdt: CurrencyPair,
1860 ) {
1861 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1862 let order = OrderTestBuilder::new(OrderType::Market)
1863 .instrument_id(btcusdt.id())
1864 .side(OrderSide::Buy)
1865 .quantity(Quantity::from(12))
1866 .build();
1867 let fill = TestOrderEventStubs::filled(
1868 &order,
1869 &btcusdt,
1870 None,
1871 Some(PositionId::from("P-123456")),
1872 Some(Price::from("10500.0")),
1873 None,
1874 None,
1875 None,
1876 None,
1877 None,
1878 );
1879 let position = Position::new(&btcusdt, fill.into());
1880 let result = position.calculate_pnl(10500.0, 10500.0, Quantity::from("100000.0"));
1881 assert_eq!(result, Money::from("0 USDT"));
1882 }
1883
1884 #[rstest]
1885 fn test_calculate_pnl_for_long_position_win(currency_pair_btcusdt: CurrencyPair) {
1886 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1887 let order = OrderTestBuilder::new(OrderType::Market)
1888 .instrument_id(btcusdt.id())
1889 .side(OrderSide::Buy)
1890 .quantity(Quantity::from(12))
1891 .build();
1892 let commission =
1893 calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1894 let fill = TestOrderEventStubs::filled(
1895 &order,
1896 &btcusdt,
1897 None,
1898 Some(PositionId::from("P-123456")),
1899 Some(Price::from("10500.0")),
1900 None,
1901 None,
1902 Some(commission),
1903 None,
1904 None,
1905 );
1906 let position = Position::new(&btcusdt, fill.into());
1907 let pnl = position.calculate_pnl(10500.0, 10510.0, Quantity::from("12.0"));
1908 assert_eq!(pnl, Money::from("120 USDT"));
1909 assert_eq!(position.realized_pnl, Some(Money::from("-126 USDT")));
1910 assert_eq!(
1911 position.unrealized_pnl(Price::from("10510.0")),
1912 Money::from("120.0 USDT")
1913 );
1914 assert_eq!(
1915 position.total_pnl(Price::from("10510.0")),
1916 Money::from("-6 USDT")
1917 );
1918 assert_eq!(position.commissions(), vec![Money::from("126.0 USDT")]);
1919 }
1920
1921 #[rstest]
1922 fn test_calculate_pnl_for_long_position_loss(currency_pair_btcusdt: CurrencyPair) {
1923 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1924 let order = OrderTestBuilder::new(OrderType::Market)
1925 .instrument_id(btcusdt.id())
1926 .side(OrderSide::Buy)
1927 .quantity(Quantity::from(12))
1928 .build();
1929 let commission =
1930 calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1931 let fill = TestOrderEventStubs::filled(
1932 &order,
1933 &btcusdt,
1934 None,
1935 Some(PositionId::from("P-123456")),
1936 Some(Price::from("10500.0")),
1937 None,
1938 None,
1939 Some(commission),
1940 None,
1941 None,
1942 );
1943 let position = Position::new(&btcusdt, fill.into());
1944 let pnl = position.calculate_pnl(10500.0, 10480.5, Quantity::from("10.0"));
1945 assert_eq!(pnl, Money::from("-195 USDT"));
1946 assert_eq!(position.realized_pnl, Some(Money::from("-126 USDT")));
1947 assert_eq!(
1948 position.unrealized_pnl(Price::from("10480.50")),
1949 Money::from("-234.0 USDT")
1950 );
1951 assert_eq!(
1952 position.total_pnl(Price::from("10480.50")),
1953 Money::from("-360 USDT")
1954 );
1955 assert_eq!(position.commissions(), vec![Money::from("126.0 USDT")]);
1956 }
1957
1958 #[rstest]
1959 fn test_calculate_pnl_for_short_position_winning(currency_pair_btcusdt: CurrencyPair) {
1960 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1961 let order = OrderTestBuilder::new(OrderType::Market)
1962 .instrument_id(btcusdt.id())
1963 .side(OrderSide::Sell)
1964 .quantity(Quantity::from("10.15"))
1965 .build();
1966 let commission =
1967 calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1968 let fill = TestOrderEventStubs::filled(
1969 &order,
1970 &btcusdt,
1971 None,
1972 Some(PositionId::from("P-123456")),
1973 Some(Price::from("10500.0")),
1974 None,
1975 None,
1976 Some(commission),
1977 None,
1978 None,
1979 );
1980 let position = Position::new(&btcusdt, fill.into());
1981 let pnl = position.calculate_pnl(10500.0, 10390.0, Quantity::from("10.15"));
1982 assert_eq!(pnl, Money::from("1116.5 USDT"));
1983 assert_eq!(
1984 position.unrealized_pnl(Price::from("10390.0")),
1985 Money::from("1116.5 USDT")
1986 );
1987 assert_eq!(position.realized_pnl, Some(Money::from("-106.575 USDT")));
1988 assert_eq!(position.commissions(), vec![Money::from("106.575 USDT")]);
1989 assert_eq!(
1990 position.notional_value(Price::from("10390.0")),
1991 Money::from("105458.5 USDT")
1992 );
1993 }
1994
1995 #[rstest]
1996 fn test_calculate_pnl_for_short_position_loss(currency_pair_btcusdt: CurrencyPair) {
1997 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1998 let order = OrderTestBuilder::new(OrderType::Market)
1999 .instrument_id(btcusdt.id())
2000 .side(OrderSide::Sell)
2001 .quantity(Quantity::from("10.0"))
2002 .build();
2003 let commission =
2004 calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
2005 let fill = TestOrderEventStubs::filled(
2006 &order,
2007 &btcusdt,
2008 None,
2009 Some(PositionId::from("P-123456")),
2010 Some(Price::from("10500.0")),
2011 None,
2012 None,
2013 Some(commission),
2014 None,
2015 None,
2016 );
2017 let position = Position::new(&btcusdt, fill.into());
2018 let pnl = position.calculate_pnl(10500.0, 10670.5, Quantity::from("10.0"));
2019 assert_eq!(pnl, Money::from("-1705 USDT"));
2020 assert_eq!(
2021 position.unrealized_pnl(Price::from("10670.5")),
2022 Money::from("-1705 USDT")
2023 );
2024 assert_eq!(position.realized_pnl, Some(Money::from("-105 USDT")));
2025 assert_eq!(position.commissions(), vec![Money::from("105 USDT")]);
2026 assert_eq!(
2027 position.notional_value(Price::from("10670.5")),
2028 Money::from("106705 USDT")
2029 );
2030 }
2031
2032 #[rstest]
2033 fn test_calculate_pnl_for_inverse1(xbtusd_bitmex: CryptoPerpetual) {
2034 let xbtusd_bitmex = InstrumentAny::CryptoPerpetual(xbtusd_bitmex);
2035 let order = OrderTestBuilder::new(OrderType::Market)
2036 .instrument_id(xbtusd_bitmex.id())
2037 .side(OrderSide::Sell)
2038 .quantity(Quantity::from("100000"))
2039 .build();
2040 let commission = calculate_commission(
2041 &xbtusd_bitmex,
2042 order.quantity(),
2043 Price::from("10000.0"),
2044 None,
2045 );
2046 let fill = TestOrderEventStubs::filled(
2047 &order,
2048 &xbtusd_bitmex,
2049 None,
2050 Some(PositionId::from("P-123456")),
2051 Some(Price::from("10000.0")),
2052 None,
2053 None,
2054 Some(commission),
2055 None,
2056 None,
2057 );
2058 let position = Position::new(&xbtusd_bitmex, fill.into());
2059 let pnl = position.calculate_pnl(10000.0, 11000.0, Quantity::from("100000.0"));
2060 assert_eq!(pnl, Money::from("-0.90909091 BTC"));
2061 assert_eq!(
2062 position.unrealized_pnl(Price::from("11000.0")),
2063 Money::from("-0.90909091 BTC")
2064 );
2065 assert_eq!(position.realized_pnl, Some(Money::from("-0.00750000 BTC")));
2066 assert_eq!(
2067 position.notional_value(Price::from("11000.0")),
2068 Money::from("9.09090909 BTC")
2069 );
2070 }
2071
2072 #[rstest]
2073 fn test_calculate_pnl_for_inverse2(ethusdt_bitmex: CryptoPerpetual) {
2074 let ethusdt_bitmex = InstrumentAny::CryptoPerpetual(ethusdt_bitmex);
2075 let order = OrderTestBuilder::new(OrderType::Market)
2076 .instrument_id(ethusdt_bitmex.id())
2077 .side(OrderSide::Sell)
2078 .quantity(Quantity::from("100000"))
2079 .build();
2080 let commission = calculate_commission(
2081 ðusdt_bitmex,
2082 order.quantity(),
2083 Price::from("375.95"),
2084 None,
2085 );
2086 let fill = TestOrderEventStubs::filled(
2087 &order,
2088 ðusdt_bitmex,
2089 None,
2090 Some(PositionId::from("P-123456")),
2091 Some(Price::from("375.95")),
2092 None,
2093 None,
2094 Some(commission),
2095 None,
2096 None,
2097 );
2098 let position = Position::new(ðusdt_bitmex, fill.into());
2099
2100 assert_eq!(
2101 position.unrealized_pnl(Price::from("370.00")),
2102 Money::from("4.27745208 ETH")
2103 );
2104 assert_eq!(
2105 position.notional_value(Price::from("370.00")),
2106 Money::from("270.27027027 ETH")
2107 );
2108 }
2109
2110 #[rstest]
2111 fn test_calculate_unrealized_pnl_for_long(currency_pair_btcusdt: CurrencyPair) {
2112 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
2113 let order1 = OrderTestBuilder::new(OrderType::Market)
2114 .instrument_id(btcusdt.id())
2115 .side(OrderSide::Buy)
2116 .quantity(Quantity::from("2.000000"))
2117 .build();
2118 let order2 = OrderTestBuilder::new(OrderType::Market)
2119 .instrument_id(btcusdt.id())
2120 .side(OrderSide::Buy)
2121 .quantity(Quantity::from("2.000000"))
2122 .build();
2123 let commission1 =
2124 calculate_commission(&btcusdt, order1.quantity(), Price::from("10500.0"), None);
2125 let fill1 = TestOrderEventStubs::filled(
2126 &order1,
2127 &btcusdt,
2128 Some(TradeId::new("1")),
2129 Some(PositionId::new("P-123456")),
2130 Some(Price::from("10500.00")),
2131 None,
2132 None,
2133 Some(commission1),
2134 None,
2135 None,
2136 );
2137 let commission2 =
2138 calculate_commission(&btcusdt, order2.quantity(), Price::from("10500.0"), None);
2139 let fill2 = TestOrderEventStubs::filled(
2140 &order2,
2141 &btcusdt,
2142 Some(TradeId::new("2")),
2143 Some(PositionId::new("P-123456")),
2144 Some(Price::from("10500.00")),
2145 None,
2146 None,
2147 Some(commission2),
2148 None,
2149 None,
2150 );
2151 let mut position = Position::new(&btcusdt, fill1.into());
2152 position.apply(&fill2.into());
2153 let pnl = position.unrealized_pnl(Price::from("11505.60"));
2154 assert_eq!(pnl, Money::from("4022.40000000 USDT"));
2155 assert_eq!(
2156 position.realized_pnl,
2157 Some(Money::from("-42.00000000 USDT"))
2158 );
2159 assert_eq!(
2160 position.commissions(),
2161 vec![Money::from("42.00000000 USDT")]
2162 );
2163 }
2164
2165 #[rstest]
2166 fn test_calculate_unrealized_pnl_for_short(currency_pair_btcusdt: CurrencyPair) {
2167 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
2168 let order = OrderTestBuilder::new(OrderType::Market)
2169 .instrument_id(btcusdt.id())
2170 .side(OrderSide::Sell)
2171 .quantity(Quantity::from("5.912000"))
2172 .build();
2173 let commission =
2174 calculate_commission(&btcusdt, order.quantity(), Price::from("10505.60"), None);
2175 let fill = TestOrderEventStubs::filled(
2176 &order,
2177 &btcusdt,
2178 Some(TradeId::new("1")),
2179 Some(PositionId::new("P-123456")),
2180 Some(Price::from("10505.60")),
2181 None,
2182 None,
2183 Some(commission),
2184 None,
2185 None,
2186 );
2187 let position = Position::new(&btcusdt, fill.into());
2188 let pnl = position.unrealized_pnl(Price::from("10407.15"));
2189 assert_eq!(pnl, Money::from("582.03640000 USDT"));
2190 assert_eq!(
2191 position.realized_pnl,
2192 Some(Money::from("-62.10910720 USDT"))
2193 );
2194 assert_eq!(
2195 position.commissions(),
2196 vec![Money::from("62.10910720 USDT")]
2197 );
2198 }
2199
2200 #[rstest]
2201 fn test_calculate_unrealized_pnl_for_long_inverse(xbtusd_bitmex: CryptoPerpetual) {
2202 let xbtusd_bitmex = InstrumentAny::CryptoPerpetual(xbtusd_bitmex);
2203 let order = OrderTestBuilder::new(OrderType::Market)
2204 .instrument_id(xbtusd_bitmex.id())
2205 .side(OrderSide::Buy)
2206 .quantity(Quantity::from("100000"))
2207 .build();
2208 let commission = calculate_commission(
2209 &xbtusd_bitmex,
2210 order.quantity(),
2211 Price::from("10500.0"),
2212 None,
2213 );
2214 let fill = TestOrderEventStubs::filled(
2215 &order,
2216 &xbtusd_bitmex,
2217 Some(TradeId::new("1")),
2218 Some(PositionId::new("P-123456")),
2219 Some(Price::from("10500.00")),
2220 None,
2221 None,
2222 Some(commission),
2223 None,
2224 None,
2225 );
2226
2227 let position = Position::new(&xbtusd_bitmex, fill.into());
2228 let pnl = position.unrealized_pnl(Price::from("11505.60"));
2229 assert_eq!(pnl, Money::from("0.83238969 BTC"));
2230 assert_eq!(position.realized_pnl, Some(Money::from("-0.00714286 BTC")));
2231 assert_eq!(position.commissions(), vec![Money::from("0.00714286 BTC")]);
2232 }
2233
2234 #[rstest]
2235 fn test_calculate_unrealized_pnl_for_short_inverse(xbtusd_bitmex: CryptoPerpetual) {
2236 let xbtusd_bitmex = InstrumentAny::CryptoPerpetual(xbtusd_bitmex);
2237 let order = OrderTestBuilder::new(OrderType::Market)
2238 .instrument_id(xbtusd_bitmex.id())
2239 .side(OrderSide::Sell)
2240 .quantity(Quantity::from("1250000"))
2241 .build();
2242 let commission = calculate_commission(
2243 &xbtusd_bitmex,
2244 order.quantity(),
2245 Price::from("15500.00"),
2246 None,
2247 );
2248 let fill = TestOrderEventStubs::filled(
2249 &order,
2250 &xbtusd_bitmex,
2251 Some(TradeId::new("1")),
2252 Some(PositionId::new("P-123456")),
2253 Some(Price::from("15500.00")),
2254 None,
2255 None,
2256 Some(commission),
2257 None,
2258 None,
2259 );
2260 let position = Position::new(&xbtusd_bitmex, fill.into());
2261 let pnl = position.unrealized_pnl(Price::from("12506.65"));
2262
2263 assert_eq!(pnl, Money::from("19.30166700 BTC"));
2264 assert_eq!(position.realized_pnl, Some(Money::from("-0.06048387 BTC")));
2265 assert_eq!(position.commissions(), vec![Money::from("0.06048387 BTC")]);
2266 }
2267
2268 #[rstest]
2269 #[case(OrderSide::Buy, 25, 25.0)]
2270 #[case(OrderSide::Sell,25,-25.0)]
2271 fn test_signed_qty_decimal_qty_for_equity(
2272 #[case] order_side: OrderSide,
2273 #[case] quantity: i64,
2274 #[case] expected: f64,
2275 audusd_sim: CurrencyPair,
2276 ) {
2277 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2278 let order = OrderTestBuilder::new(OrderType::Market)
2279 .instrument_id(audusd_sim.id())
2280 .side(order_side)
2281 .quantity(Quantity::from(quantity))
2282 .build();
2283
2284 let commission =
2285 calculate_commission(&audusd_sim, order.quantity(), Price::from("1.0"), None);
2286 let fill = TestOrderEventStubs::filled(
2287 &order,
2288 &audusd_sim,
2289 None,
2290 Some(PositionId::from("P-123456")),
2291 None,
2292 None,
2293 None,
2294 Some(commission),
2295 None,
2296 None,
2297 );
2298 let position = Position::new(&audusd_sim, fill.into());
2299 assert_eq!(position.signed_qty, expected);
2300 }
2301
2302 #[rstest]
2303 fn test_position_with_commission_none(audusd_sim: CurrencyPair) {
2304 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2305 let fill = OrderFilled {
2306 position_id: Some(PositionId::from("1")),
2307 ..Default::default()
2308 };
2309
2310 let position = Position::new(&audusd_sim, fill);
2311 assert_eq!(position.realized_pnl, Some(Money::from("0 USD")));
2312 }
2313
2314 #[rstest]
2315 fn test_position_with_commission_zero(audusd_sim: CurrencyPair) {
2316 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2317 let fill = OrderFilled {
2318 position_id: Some(PositionId::from("1")),
2319 commission: Some(Money::from("0 USD")),
2320 ..Default::default()
2321 };
2322
2323 let position = Position::new(&audusd_sim, fill);
2324 assert_eq!(position.realized_pnl, Some(Money::from("0 USD")));
2325 }
2326
2327 #[rstest]
2328 fn test_cache_purge_order_events() {
2329 let audusd_sim = audusd_sim();
2330 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2331
2332 let order1 = OrderTestBuilder::new(OrderType::Market)
2333 .client_order_id(ClientOrderId::new("O-1"))
2334 .instrument_id(audusd_sim.id())
2335 .side(OrderSide::Buy)
2336 .quantity(Quantity::from(50_000))
2337 .build();
2338
2339 let order2 = OrderTestBuilder::new(OrderType::Market)
2340 .client_order_id(ClientOrderId::new("O-2"))
2341 .instrument_id(audusd_sim.id())
2342 .side(OrderSide::Buy)
2343 .quantity(Quantity::from(50_000))
2344 .build();
2345
2346 let position_id = PositionId::new("P-123456");
2347
2348 let fill1 = TestOrderEventStubs::filled(
2349 &order1,
2350 &audusd_sim,
2351 Some(TradeId::new("1")),
2352 Some(position_id),
2353 Some(Price::from("1.00001")),
2354 None,
2355 None,
2356 None,
2357 None,
2358 None,
2359 );
2360
2361 let mut position = Position::new(&audusd_sim, fill1.into());
2362
2363 let fill2 = TestOrderEventStubs::filled(
2364 &order2,
2365 &audusd_sim,
2366 Some(TradeId::new("2")),
2367 Some(position_id),
2368 Some(Price::from("1.00002")),
2369 None,
2370 None,
2371 None,
2372 None,
2373 None,
2374 );
2375
2376 position.apply(&fill2.into());
2377 position.purge_events_for_order(order1.client_order_id());
2378
2379 assert_eq!(position.events.len(), 1);
2380 assert_eq!(position.trade_ids.len(), 1);
2381 assert_eq!(position.events[0].client_order_id, order2.client_order_id());
2382 assert_eq!(position.trade_ids[0], TradeId::new("2"));
2383 }
2384
2385 #[rstest]
2386 fn test_purge_all_events_returns_none_for_last_event_and_trade_id() {
2387 let audusd_sim = audusd_sim();
2388 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2389
2390 let order = OrderTestBuilder::new(OrderType::Market)
2391 .client_order_id(ClientOrderId::new("O-1"))
2392 .instrument_id(audusd_sim.id())
2393 .side(OrderSide::Buy)
2394 .quantity(Quantity::from(100_000))
2395 .build();
2396
2397 let position_id = PositionId::new("P-123456");
2398 let fill = TestOrderEventStubs::filled(
2399 &order,
2400 &audusd_sim,
2401 Some(TradeId::new("1")),
2402 Some(position_id),
2403 Some(Price::from("1.00050")),
2404 None,
2405 None,
2406 None,
2407 Some(UnixNanos::from(1_000_000_000)), None,
2409 );
2410
2411 let mut position = Position::new(&audusd_sim, fill.into());
2412
2413 assert_eq!(position.events.len(), 1);
2414 assert!(position.last_event().is_some());
2415 assert!(position.last_trade_id().is_some());
2416
2417 let original_ts_opened = position.ts_opened;
2419 let original_ts_last = position.ts_last;
2420 assert_ne!(original_ts_opened, UnixNanos::default());
2421 assert_ne!(original_ts_last, UnixNanos::default());
2422
2423 position.purge_events_for_order(order.client_order_id());
2424
2425 assert_eq!(position.events.len(), 0);
2426 assert_eq!(position.trade_ids.len(), 0);
2427 assert!(position.last_event().is_none());
2428 assert!(position.last_trade_id().is_none());
2429
2430 assert_eq!(position.ts_opened, UnixNanos::default());
2433 assert_eq!(position.ts_last, UnixNanos::default());
2434 assert_eq!(position.ts_closed, Some(UnixNanos::default()));
2435 assert_eq!(position.duration_ns, 0);
2436
2437 assert!(position.is_closed());
2440 assert!(!position.is_open());
2441 assert_eq!(position.side, PositionSide::Flat);
2442 }
2443
2444 #[rstest]
2445 fn test_revive_from_empty_shell(audusd_sim: CurrencyPair) {
2446 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2448
2449 let order1 = OrderTestBuilder::new(OrderType::Market)
2451 .instrument_id(audusd_sim.id())
2452 .side(OrderSide::Buy)
2453 .quantity(Quantity::from(100_000))
2454 .build();
2455
2456 let fill1 = TestOrderEventStubs::filled(
2457 &order1,
2458 &audusd_sim,
2459 None,
2460 Some(PositionId::new("P-1")),
2461 Some(Price::from("1.00000")),
2462 None,
2463 None,
2464 None,
2465 Some(UnixNanos::from(1_000_000_000)),
2466 None,
2467 );
2468
2469 let mut position = Position::new(&audusd_sim, fill1.into());
2470 position.purge_events_for_order(order1.client_order_id());
2471
2472 assert!(position.is_closed());
2474 assert_eq!(position.ts_closed, Some(UnixNanos::default()));
2475 assert_eq!(position.event_count(), 0);
2476
2477 let order2 = OrderTestBuilder::new(OrderType::Market)
2479 .instrument_id(audusd_sim.id())
2480 .side(OrderSide::Buy)
2481 .quantity(Quantity::from(50_000))
2482 .build();
2483
2484 let fill2 = TestOrderEventStubs::filled(
2485 &order2,
2486 &audusd_sim,
2487 None,
2488 Some(PositionId::new("P-1")),
2489 Some(Price::from("1.00020")),
2490 None,
2491 None,
2492 None,
2493 Some(UnixNanos::from(3_000_000_000)),
2494 None,
2495 );
2496
2497 let fill2_typed: OrderFilled = fill2.clone().into();
2498 position.apply(&fill2_typed);
2499
2500 assert!(position.is_long());
2502 assert!(!position.is_closed());
2503 assert!(position.ts_closed.is_none());
2504 assert_eq!(position.ts_opened, fill2.ts_event());
2505 assert_eq!(position.ts_last, fill2.ts_event());
2506 assert_eq!(position.event_count(), 1);
2507 assert_eq!(position.quantity, Quantity::from(50_000));
2508 }
2509
2510 #[rstest]
2511 fn test_empty_shell_position_invariants(audusd_sim: CurrencyPair) {
2512 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2514
2515 let order = OrderTestBuilder::new(OrderType::Market)
2516 .instrument_id(audusd_sim.id())
2517 .side(OrderSide::Buy)
2518 .quantity(Quantity::from(100_000))
2519 .build();
2520
2521 let fill = TestOrderEventStubs::filled(
2522 &order,
2523 &audusd_sim,
2524 None,
2525 Some(PositionId::new("P-1")),
2526 Some(Price::from("1.00000")),
2527 None,
2528 None,
2529 None,
2530 Some(UnixNanos::from(1_000_000_000)),
2531 None,
2532 );
2533
2534 let mut position = Position::new(&audusd_sim, fill.into());
2535 position.purge_events_for_order(order.client_order_id());
2536
2537 assert_eq!(
2539 position.event_count(),
2540 0,
2541 "Precondition: event_count must be 0"
2542 );
2543
2544 assert!(
2546 position.is_closed(),
2547 "INV1: Empty shell must report is_closed() == true"
2548 );
2549 assert!(
2550 !position.is_open(),
2551 "INV1: Empty shell must report is_open() == false"
2552 );
2553
2554 assert_eq!(
2556 position.side,
2557 PositionSide::Flat,
2558 "INV2: Empty shell must be FLAT"
2559 );
2560
2561 assert!(
2563 position.ts_closed.is_some(),
2564 "INV3: Empty shell must have ts_closed.is_some()"
2565 );
2566 assert_eq!(
2567 position.ts_closed,
2568 Some(UnixNanos::default()),
2569 "INV3: Empty shell ts_closed must be 0"
2570 );
2571
2572 assert_eq!(
2574 position.ts_opened,
2575 UnixNanos::default(),
2576 "INV4: Empty shell ts_opened must be 0"
2577 );
2578 assert_eq!(
2579 position.ts_last,
2580 UnixNanos::default(),
2581 "INV4: Empty shell ts_last must be 0"
2582 );
2583 assert_eq!(
2584 position.duration_ns, 0,
2585 "INV4: Empty shell duration_ns must be 0"
2586 );
2587
2588 assert_eq!(
2590 position.quantity,
2591 Quantity::zero(audusd_sim.size_precision()),
2592 "INV5: Empty shell quantity must be 0"
2593 );
2594
2595 assert!(
2597 position.events.is_empty(),
2598 "INV6: Empty shell must have no events"
2599 );
2600 assert!(
2601 position.trade_ids.is_empty(),
2602 "INV6: Empty shell must have no trade IDs"
2603 );
2604 assert!(
2605 position.last_event().is_none(),
2606 "INV6: Empty shell must have no last event"
2607 );
2608 assert!(
2609 position.last_trade_id().is_none(),
2610 "INV6: Empty shell must have no last trade ID"
2611 );
2612 }
2613
2614 #[rstest]
2615 fn test_position_pnl_precision_with_very_small_amounts(audusd_sim: CurrencyPair) {
2616 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2619 let order = OrderTestBuilder::new(OrderType::Market)
2620 .instrument_id(audusd_sim.id())
2621 .side(OrderSide::Buy)
2622 .quantity(Quantity::from(100))
2623 .build();
2624
2625 let small_commission = Money::new(0.01, Currency::USD());
2627 let fill = TestOrderEventStubs::filled(
2628 &order,
2629 &audusd_sim,
2630 None,
2631 None,
2632 Some(Price::from("1.00001")),
2633 Some(Quantity::from(100)),
2634 None,
2635 Some(small_commission),
2636 None,
2637 None,
2638 );
2639
2640 let position = Position::new(&audusd_sim, fill.into());
2641
2642 assert_eq!(position.commissions().len(), 1);
2644 let recorded_commission = position.commissions()[0];
2645 assert!(
2646 recorded_commission.as_f64() > 0.0,
2647 "Commission of 0.01 should be preserved"
2648 );
2649
2650 let realized = position.realized_pnl.unwrap().as_f64();
2652 assert!(
2653 realized < 0.0,
2654 "Realized PnL should be negative due to commission"
2655 );
2656 }
2657
2658 #[rstest]
2659 fn test_position_pnl_precision_with_high_precision_instrument() {
2660 use crate::instruments::stubs::crypto_perpetual_ethusdt;
2662 let ethusdt = crypto_perpetual_ethusdt();
2663 let ethusdt = InstrumentAny::CryptoPerpetual(ethusdt);
2664
2665 let size_precision = ethusdt.size_precision();
2667
2668 let order = OrderTestBuilder::new(OrderType::Market)
2669 .instrument_id(ethusdt.id())
2670 .side(OrderSide::Buy)
2671 .quantity(Quantity::from("1.123456789"))
2672 .build();
2673
2674 let fill = TestOrderEventStubs::filled(
2675 &order,
2676 ðusdt,
2677 None,
2678 None,
2679 Some(Price::from("2345.123456789")),
2680 Some(Quantity::from("1.123456789")),
2681 None,
2682 Some(Money::from("0.1 USDT")),
2683 None,
2684 None,
2685 );
2686
2687 let position = Position::new(ðusdt, fill.into());
2688
2689 let avg_px = position.avg_px_open;
2691 assert!(
2692 (avg_px - 2345.123456789).abs() < 1e-6,
2693 "High precision price should be preserved within f64 tolerance"
2694 );
2695
2696 assert_eq!(
2699 position.quantity.precision, size_precision,
2700 "Quantity precision should match instrument"
2701 );
2702
2703 let qty_f64 = position.quantity.as_f64();
2705 assert!(
2706 qty_f64 > 1.0 && qty_f64 < 2.0,
2707 "Quantity should be in expected range"
2708 );
2709 }
2710
2711 #[rstest]
2712 fn test_position_pnl_accumulation_across_many_fills(audusd_sim: CurrencyPair) {
2713 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2715 let order = OrderTestBuilder::new(OrderType::Market)
2716 .instrument_id(audusd_sim.id())
2717 .side(OrderSide::Buy)
2718 .quantity(Quantity::from(1000))
2719 .build();
2720
2721 let initial_fill = TestOrderEventStubs::filled(
2722 &order,
2723 &audusd_sim,
2724 Some(TradeId::new("1")),
2725 None,
2726 Some(Price::from("1.00000")),
2727 Some(Quantity::from(10)),
2728 None,
2729 Some(Money::from("0.01 USD")),
2730 None,
2731 None,
2732 );
2733
2734 let mut position = Position::new(&audusd_sim, initial_fill.into());
2735
2736 for i in 2..=100 {
2738 let price_offset = (i as f64) * 0.00001;
2739 let fill = TestOrderEventStubs::filled(
2740 &order,
2741 &audusd_sim,
2742 Some(TradeId::new(i.to_string())),
2743 None,
2744 Some(Price::from(&format!("{:.5}", 1.0 + price_offset))),
2745 Some(Quantity::from(10)),
2746 None,
2747 Some(Money::from("0.01 USD")),
2748 None,
2749 None,
2750 );
2751 position.apply(&fill.into());
2752 }
2753
2754 assert_eq!(position.events.len(), 100);
2756 assert_eq!(position.quantity, Quantity::from(1000));
2757
2758 let total_commission: f64 = position.commissions().iter().map(|c| c.as_f64()).sum();
2760 assert!(
2761 (total_commission - 1.0).abs() < 1e-10,
2762 "Commission accumulation should be accurate: expected 1.0, was {total_commission}"
2763 );
2764
2765 let avg_px = position.avg_px_open;
2767 assert!(
2768 avg_px > 1.0 && avg_px < 1.001,
2769 "Average price should be reasonable: got {avg_px}"
2770 );
2771 }
2772
2773 #[rstest]
2774 fn test_position_pnl_with_extreme_price_values(audusd_sim: CurrencyPair) {
2775 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2777
2778 let order_small = OrderTestBuilder::new(OrderType::Market)
2780 .instrument_id(audusd_sim.id())
2781 .side(OrderSide::Buy)
2782 .quantity(Quantity::from(100_000))
2783 .build();
2784
2785 let fill_small = TestOrderEventStubs::filled(
2786 &order_small,
2787 &audusd_sim,
2788 None,
2789 None,
2790 Some(Price::from("0.00001")),
2791 Some(Quantity::from(100_000)),
2792 None,
2793 None,
2794 None,
2795 None,
2796 );
2797
2798 let position_small = Position::new(&audusd_sim, fill_small.into());
2799 assert_eq!(position_small.avg_px_open, 0.00001);
2800
2801 let last_price_small = Price::from("0.00002");
2803 let unrealized = position_small.unrealized_pnl(last_price_small);
2804 assert!(
2805 unrealized.as_f64() > 0.0,
2806 "Unrealized PnL should be positive when price doubles"
2807 );
2808
2809 let order_large = OrderTestBuilder::new(OrderType::Market)
2811 .instrument_id(audusd_sim.id())
2812 .side(OrderSide::Buy)
2813 .quantity(Quantity::from(100))
2814 .build();
2815
2816 let fill_large = TestOrderEventStubs::filled(
2817 &order_large,
2818 &audusd_sim,
2819 None,
2820 None,
2821 Some(Price::from("99999.99999")),
2822 Some(Quantity::from(100)),
2823 None,
2824 None,
2825 None,
2826 None,
2827 );
2828
2829 let position_large = Position::new(&audusd_sim, fill_large.into());
2830 assert!(
2831 (position_large.avg_px_open - 99999.99999).abs() < 1e-6,
2832 "Large price should be preserved within f64 tolerance"
2833 );
2834 }
2835
2836 #[rstest]
2837 fn test_position_pnl_roundtrip_precision(audusd_sim: CurrencyPair) {
2838 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2840 let buy_order = OrderTestBuilder::new(OrderType::Market)
2841 .instrument_id(audusd_sim.id())
2842 .side(OrderSide::Buy)
2843 .quantity(Quantity::from(100_000))
2844 .build();
2845
2846 let sell_order = OrderTestBuilder::new(OrderType::Market)
2847 .instrument_id(audusd_sim.id())
2848 .side(OrderSide::Sell)
2849 .quantity(Quantity::from(100_000))
2850 .build();
2851
2852 let open_fill = TestOrderEventStubs::filled(
2854 &buy_order,
2855 &audusd_sim,
2856 Some(TradeId::new("1")),
2857 None,
2858 Some(Price::from("1.123456")),
2859 None,
2860 None,
2861 Some(Money::from("0.50 USD")),
2862 None,
2863 None,
2864 );
2865
2866 let mut position = Position::new(&audusd_sim, open_fill.into());
2867
2868 let close_fill = TestOrderEventStubs::filled(
2870 &sell_order,
2871 &audusd_sim,
2872 Some(TradeId::new("2")),
2873 None,
2874 Some(Price::from("1.123456")),
2875 None,
2876 None,
2877 Some(Money::from("0.50 USD")),
2878 None,
2879 None,
2880 );
2881
2882 position.apply(&close_fill.into());
2883
2884 assert!(position.is_closed());
2886
2887 let realized = position.realized_pnl.unwrap().as_f64();
2889 assert!(
2890 (realized - (-1.0)).abs() < 1e-10,
2891 "Realized PnL should be exactly -1.0 USD (commissions), was {realized}"
2892 );
2893 }
2894
2895 #[rstest]
2896 fn test_position_commission_in_base_currency_buy() {
2897 let btc_usdt = currency_pair_btcusdt();
2899 let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
2900
2901 let order = OrderTestBuilder::new(OrderType::Market)
2902 .instrument_id(btc_usdt.id())
2903 .side(OrderSide::Buy)
2904 .quantity(Quantity::from("1.0"))
2905 .build();
2906
2907 let fill = TestOrderEventStubs::filled(
2909 &order,
2910 &btc_usdt,
2911 Some(TradeId::new("1")),
2912 None,
2913 Some(Price::from("50000.0")),
2914 Some(Quantity::from("1.0")),
2915 None,
2916 Some(Money::new(0.001, btc_usdt.base_currency().unwrap())),
2917 None,
2918 None,
2919 );
2920
2921 let position = Position::new(&btc_usdt, fill.into());
2922
2923 assert!(
2925 (position.quantity.as_f64() - 0.999).abs() < 1e-9,
2926 "Position quantity should be 0.999 BTC (1.0 - 0.001 commission), was {}",
2927 position.quantity.as_f64()
2928 );
2929
2930 assert!(
2932 (position.signed_qty - 0.999).abs() < 1e-9,
2933 "Signed qty should be 0.999, was {}",
2934 position.signed_qty
2935 );
2936
2937 assert_eq!(
2939 position.adjustments.len(),
2940 1,
2941 "Should have 1 adjustment event"
2942 );
2943 let adjustment = &position.adjustments[0];
2944 assert_eq!(
2945 adjustment.adjustment_type,
2946 PositionAdjustmentType::Commission
2947 );
2948 assert_eq!(
2949 adjustment.quantity_change,
2950 Some(rust_decimal_macros::dec!(-0.001))
2951 );
2952 assert_eq!(adjustment.pnl_change, None);
2953 }
2954
2955 #[rstest]
2956 fn test_position_commission_in_base_currency_sell() {
2957 let btc_usdt = currency_pair_btcusdt();
2959 let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
2960
2961 let order = OrderTestBuilder::new(OrderType::Market)
2962 .instrument_id(btc_usdt.id())
2963 .side(OrderSide::Sell)
2964 .quantity(Quantity::from("1.0"))
2965 .build();
2966
2967 let fill = TestOrderEventStubs::filled(
2969 &order,
2970 &btc_usdt,
2971 Some(TradeId::new("1")),
2972 None,
2973 Some(Price::from("50000.0")),
2974 Some(Quantity::from("1.0")),
2975 None,
2976 Some(Money::new(0.001, btc_usdt.base_currency().unwrap())),
2977 None,
2978 None,
2979 );
2980
2981 let position = Position::new(&btc_usdt, fill.into());
2982
2983 assert!(
2986 (position.quantity.as_f64() - 1.001).abs() < 1e-9,
2987 "Position quantity should be 1.001 BTC (1.0 + 0.001 commission), was {}",
2988 position.quantity.as_f64()
2989 );
2990
2991 assert!(
2993 (position.signed_qty - (-1.001)).abs() < 1e-9,
2994 "Signed qty should be -1.001, was {}",
2995 position.signed_qty
2996 );
2997
2998 assert_eq!(
3000 position.adjustments.len(),
3001 1,
3002 "Should have 1 adjustment event"
3003 );
3004 let adjustment = &position.adjustments[0];
3005 assert_eq!(
3006 adjustment.adjustment_type,
3007 PositionAdjustmentType::Commission
3008 );
3009 assert_eq!(
3011 adjustment.quantity_change,
3012 Some(rust_decimal_macros::dec!(-0.001))
3013 );
3014 assert_eq!(adjustment.pnl_change, None);
3015 }
3016
3017 #[rstest]
3018 fn test_position_commission_in_quote_currency_no_adjustment() {
3019 let btc_usdt = currency_pair_btcusdt();
3021 let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
3022
3023 let order = OrderTestBuilder::new(OrderType::Market)
3024 .instrument_id(btc_usdt.id())
3025 .side(OrderSide::Buy)
3026 .quantity(Quantity::from("1.0"))
3027 .build();
3028
3029 let fill = TestOrderEventStubs::filled(
3031 &order,
3032 &btc_usdt,
3033 Some(TradeId::new("1")),
3034 None,
3035 Some(Price::from("50000.0")),
3036 Some(Quantity::from("1.0")),
3037 None,
3038 Some(Money::new(50.0, Currency::USD())),
3039 None,
3040 None,
3041 );
3042
3043 let position = Position::new(&btc_usdt, fill.into());
3044
3045 assert!(
3047 (position.quantity.as_f64() - 1.0).abs() < 1e-9,
3048 "Position quantity should be 1.0 BTC (no adjustment for quote currency commission), was {}",
3049 position.quantity.as_f64()
3050 );
3051
3052 assert_eq!(
3054 position.adjustments.len(),
3055 0,
3056 "Should have no adjustment events for quote currency commission"
3057 );
3058 }
3059
3060 #[rstest]
3061 fn test_position_reset_clears_adjustments() {
3062 let btc_usdt = currency_pair_btcusdt();
3064 let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
3065
3066 let buy_order = OrderTestBuilder::new(OrderType::Market)
3068 .instrument_id(btc_usdt.id())
3069 .side(OrderSide::Buy)
3070 .quantity(Quantity::from("1.0"))
3071 .build();
3072
3073 let buy_fill = TestOrderEventStubs::filled(
3074 &buy_order,
3075 &btc_usdt,
3076 Some(TradeId::new("1")),
3077 None,
3078 Some(Price::from("50000.0")),
3079 Some(Quantity::from("1.0")),
3080 None,
3081 Some(Money::new(0.001, btc_usdt.base_currency().unwrap())),
3082 None,
3083 None,
3084 );
3085
3086 let mut position = Position::new(&btc_usdt, buy_fill.into());
3087 assert_eq!(position.adjustments.len(), 1, "Should have 1 adjustment");
3088
3089 let sell_order = OrderTestBuilder::new(OrderType::Market)
3091 .instrument_id(btc_usdt.id())
3092 .side(OrderSide::Sell)
3093 .quantity(Quantity::from("0.999"))
3094 .build();
3095
3096 let sell_fill = TestOrderEventStubs::filled(
3097 &sell_order,
3098 &btc_usdt,
3099 Some(TradeId::new("2")),
3100 None,
3101 Some(Price::from("51000.0")),
3102 Some(Quantity::from("0.999")),
3103 None,
3104 Some(Money::new(50.0, Currency::USD())), None,
3106 None,
3107 );
3108
3109 position.apply(&sell_fill.into());
3110 assert_eq!(position.side, PositionSide::Flat);
3111 assert_eq!(
3112 position.adjustments.len(),
3113 1,
3114 "Should still have 1 adjustment (no new one from quote commission)"
3115 );
3116
3117 let buy_order2 = OrderTestBuilder::new(OrderType::Market)
3119 .instrument_id(btc_usdt.id())
3120 .side(OrderSide::Buy)
3121 .quantity(Quantity::from("2.0"))
3122 .build();
3123
3124 let buy_fill2 = TestOrderEventStubs::filled(
3125 &buy_order2,
3126 &btc_usdt,
3127 Some(TradeId::new("3")),
3128 None,
3129 Some(Price::from("52000.0")),
3130 Some(Quantity::from("2.0")),
3131 None,
3132 Some(Money::new(0.002, btc_usdt.base_currency().unwrap())),
3133 None,
3134 None,
3135 );
3136
3137 position.apply(&buy_fill2.into());
3138
3139 assert_eq!(
3141 position.adjustments.len(),
3142 1,
3143 "Adjustments should be cleared on position reset, only new adjustment"
3144 );
3145 assert_eq!(
3146 position.adjustments[0].quantity_change,
3147 Some(rust_decimal_macros::dec!(-0.002)),
3148 "New adjustment should be for the new fill"
3149 );
3150 assert_eq!(position.events.len(), 1, "Events should also be reset");
3151 }
3152
3153 #[rstest]
3154 fn test_purge_events_for_order_clears_adjustments_when_flat() {
3155 let btc_usdt = currency_pair_btcusdt();
3157 let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
3158
3159 let order = OrderTestBuilder::new(OrderType::Market)
3160 .instrument_id(btc_usdt.id())
3161 .side(OrderSide::Buy)
3162 .quantity(Quantity::from("1.0"))
3163 .build();
3164
3165 let fill = TestOrderEventStubs::filled(
3166 &order,
3167 &btc_usdt,
3168 Some(TradeId::new("1")),
3169 None,
3170 Some(Price::from("50000.0")),
3171 Some(Quantity::from("1.0")),
3172 None,
3173 Some(Money::new(0.001, btc_usdt.base_currency().unwrap())),
3174 None,
3175 None,
3176 );
3177
3178 let mut position = Position::new(&btc_usdt, fill.into());
3179 assert_eq!(position.adjustments.len(), 1, "Should have 1 adjustment");
3180 assert_eq!(position.events.len(), 1);
3181
3182 position.purge_events_for_order(order.client_order_id());
3184
3185 assert_eq!(position.side, PositionSide::Flat);
3186 assert_eq!(position.events.len(), 0, "Events should be cleared");
3187 assert_eq!(
3188 position.adjustments.len(),
3189 0,
3190 "Adjustments should be cleared when position goes flat"
3191 );
3192 assert_eq!(position.quantity, Quantity::zero(btc_usdt.size_precision()));
3193 }
3194
3195 #[rstest]
3196 fn test_purge_events_for_order_clears_adjustments_on_rebuild() {
3197 let btc_usdt = currency_pair_btcusdt();
3199 let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
3200
3201 let order1 = OrderTestBuilder::new(OrderType::Market)
3203 .instrument_id(btc_usdt.id())
3204 .side(OrderSide::Buy)
3205 .quantity(Quantity::from("1.0"))
3206 .client_order_id(ClientOrderId::new("O-001"))
3207 .build();
3208
3209 let fill1 = TestOrderEventStubs::filled(
3210 &order1,
3211 &btc_usdt,
3212 Some(TradeId::new("1")),
3213 None,
3214 Some(Price::from("50000.0")),
3215 Some(Quantity::from("1.0")),
3216 None,
3217 Some(Money::new(0.001, btc_usdt.base_currency().unwrap())),
3218 None,
3219 None,
3220 );
3221
3222 let mut position = Position::new(&btc_usdt, fill1.into());
3223 assert_eq!(position.adjustments.len(), 1);
3224
3225 let order2 = OrderTestBuilder::new(OrderType::Market)
3227 .instrument_id(btc_usdt.id())
3228 .side(OrderSide::Buy)
3229 .quantity(Quantity::from("2.0"))
3230 .client_order_id(ClientOrderId::new("O-002"))
3231 .build();
3232
3233 let fill2 = TestOrderEventStubs::filled(
3234 &order2,
3235 &btc_usdt,
3236 Some(TradeId::new("2")),
3237 None,
3238 Some(Price::from("51000.0")),
3239 Some(Quantity::from("2.0")),
3240 None,
3241 Some(Money::new(0.002, btc_usdt.base_currency().unwrap())),
3242 None,
3243 None,
3244 );
3245
3246 position.apply(&fill2.into());
3247 assert_eq!(position.adjustments.len(), 2, "Should have 2 adjustments");
3248 assert_eq!(position.events.len(), 2);
3249
3250 position.purge_events_for_order(order1.client_order_id());
3252
3253 assert_eq!(position.events.len(), 1, "Should have 1 remaining event");
3254 assert_eq!(
3255 position.adjustments.len(),
3256 1,
3257 "Should have only the adjustment from remaining fill"
3258 );
3259 assert_eq!(
3260 position.adjustments[0].quantity_change,
3261 Some(rust_decimal_macros::dec!(-0.002)),
3262 "Should be the adjustment from order2"
3263 );
3264 assert!(
3265 (position.quantity.as_f64() - 1.998).abs() < 1e-9,
3266 "Quantity should be 2.0 - 0.002 commission"
3267 );
3268 }
3269
3270 #[rstest]
3271 fn test_purge_events_preserves_manual_adjustments() {
3272 let btc_usdt = currency_pair_btcusdt();
3274 let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
3275
3276 let order1 = OrderTestBuilder::new(OrderType::Market)
3278 .instrument_id(btc_usdt.id())
3279 .side(OrderSide::Buy)
3280 .quantity(Quantity::from("1.0"))
3281 .client_order_id(ClientOrderId::new("O-001"))
3282 .build();
3283
3284 let fill1 = TestOrderEventStubs::filled(
3285 &order1,
3286 &btc_usdt,
3287 Some(TradeId::new("1")),
3288 None,
3289 Some(Price::from("50000.0")),
3290 Some(Quantity::from("1.0")),
3291 None,
3292 Some(Money::new(0.001, btc_usdt.base_currency().unwrap())),
3293 None,
3294 None,
3295 );
3296
3297 let mut position = Position::new(&btc_usdt, fill1.into());
3298 assert_eq!(position.adjustments.len(), 1);
3299
3300 let funding_adjustment = PositionAdjusted::new(
3302 position.trader_id,
3303 position.strategy_id,
3304 position.instrument_id,
3305 position.id,
3306 position.account_id,
3307 PositionAdjustmentType::Funding,
3308 None,
3309 Some(Money::new(10.0, btc_usdt.quote_currency())),
3310 None, uuid4(),
3312 UnixNanos::default(),
3313 UnixNanos::default(),
3314 );
3315 position.apply_adjustment(funding_adjustment);
3316 assert_eq!(position.adjustments.len(), 2);
3317
3318 let order2 = OrderTestBuilder::new(OrderType::Market)
3320 .instrument_id(btc_usdt.id())
3321 .side(OrderSide::Buy)
3322 .quantity(Quantity::from("2.0"))
3323 .client_order_id(ClientOrderId::new("O-002"))
3324 .build();
3325
3326 let fill2 = TestOrderEventStubs::filled(
3327 &order2,
3328 &btc_usdt,
3329 Some(TradeId::new("2")),
3330 None,
3331 Some(Price::from("51000.0")),
3332 Some(Quantity::from("2.0")),
3333 None,
3334 Some(Money::new(0.002, btc_usdt.base_currency().unwrap())),
3335 None,
3336 None,
3337 );
3338
3339 position.apply(&fill2.into());
3340 assert_eq!(
3341 position.adjustments.len(),
3342 3,
3343 "Should have 3 adjustments: 2 commissions + 1 funding"
3344 );
3345
3346 position.purge_events_for_order(order1.client_order_id());
3348
3349 assert_eq!(position.events.len(), 1, "Should have 1 remaining event");
3350 assert_eq!(
3351 position.adjustments.len(),
3352 2,
3353 "Should have funding adjustment + commission from remaining fill"
3354 );
3355
3356 let has_funding = position.adjustments.iter().any(|adj| {
3358 adj.adjustment_type == PositionAdjustmentType::Funding
3359 && adj.pnl_change == Some(Money::new(10.0, btc_usdt.quote_currency()))
3360 });
3361 assert!(has_funding, "Funding adjustment should be preserved");
3362
3363 assert_eq!(
3366 position.realized_pnl,
3367 Some(Money::new(10.0, btc_usdt.quote_currency())),
3368 "Realized PnL should be the funding payment only (commission is in BTC, not USDT)"
3369 );
3370 }
3371
3372 #[rstest]
3373 fn test_position_commission_affects_buy_and_sell_qty() {
3374 let btc_usdt = currency_pair_btcusdt();
3376 let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
3377
3378 let buy_order = OrderTestBuilder::new(OrderType::Market)
3379 .instrument_id(btc_usdt.id())
3380 .side(OrderSide::Buy)
3381 .quantity(Quantity::from("1.0"))
3382 .build();
3383
3384 let fill = TestOrderEventStubs::filled(
3386 &buy_order,
3387 &btc_usdt,
3388 Some(TradeId::new("1")),
3389 None,
3390 Some(Price::from("50000.0")),
3391 Some(Quantity::from("1.0")),
3392 None,
3393 Some(Money::new(0.001, btc_usdt.base_currency().unwrap())),
3394 None,
3395 None,
3396 );
3397
3398 let position = Position::new(&btc_usdt, fill.into());
3399
3400 assert!(
3402 (position.buy_qty.as_f64() - 1.0).abs() < 1e-9,
3403 "buy_qty should be 1.0 (order fill amount), was {}",
3404 position.buy_qty.as_f64()
3405 );
3406
3407 assert!(
3409 (position.quantity.as_f64() - 0.999).abs() < 1e-9,
3410 "position.quantity should be 0.999 (1.0 - 0.001 commission), was {}",
3411 position.quantity.as_f64()
3412 );
3413
3414 assert_eq!(position.adjustments.len(), 1);
3416 assert_eq!(
3417 position.adjustments[0].quantity_change,
3418 Some(rust_decimal_macros::dec!(-0.001))
3419 );
3420 }
3421
3422 #[rstest]
3423 fn test_position_perpetual_commission_no_adjustment() {
3424 let eth_perp = crypto_perpetual_ethusdt();
3426 let eth_perp = InstrumentAny::CryptoPerpetual(eth_perp);
3427
3428 let order = OrderTestBuilder::new(OrderType::Market)
3429 .instrument_id(eth_perp.id())
3430 .side(OrderSide::Buy)
3431 .quantity(Quantity::from("1.0"))
3432 .build();
3433
3434 let fill = TestOrderEventStubs::filled(
3436 &order,
3437 ð_perp,
3438 Some(TradeId::new("1")),
3439 None,
3440 Some(Price::from("3000.0")),
3441 Some(Quantity::from("1.0")),
3442 None,
3443 Some(Money::new(0.001, eth_perp.base_currency().unwrap())),
3444 None,
3445 None,
3446 );
3447
3448 let position = Position::new(ð_perp, fill.into());
3449
3450 assert!(
3452 (position.quantity.as_f64() - 1.0).abs() < 1e-9,
3453 "Perpetual position should be 1.0 contracts (no adjustment), was {}",
3454 position.quantity.as_f64()
3455 );
3456
3457 assert!(
3459 (position.signed_qty - 1.0).abs() < 1e-9,
3460 "Signed qty should be 1.0, was {}",
3461 position.signed_qty
3462 );
3463 }
3464
3465 #[rstest]
3466 fn test_signed_decimal_qty_long(stub_position_long: Position) {
3467 let signed_qty = stub_position_long.signed_decimal_qty();
3468 assert!(signed_qty > Decimal::ZERO);
3469 assert_eq!(
3470 signed_qty,
3471 Decimal::try_from(stub_position_long.signed_qty).unwrap()
3472 );
3473 }
3474
3475 #[rstest]
3476 fn test_signed_decimal_qty_short(stub_position_short: Position) {
3477 let signed_qty = stub_position_short.signed_decimal_qty();
3478 assert!(signed_qty < Decimal::ZERO);
3479 assert_eq!(
3480 signed_qty,
3481 Decimal::try_from(stub_position_short.signed_qty).unwrap()
3482 );
3483 }
3484
3485 #[rstest]
3486 fn test_signed_decimal_qty_flat(audusd_sim: CurrencyPair) {
3487 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
3488 let order = OrderTestBuilder::new(OrderType::Market)
3489 .instrument_id(audusd_sim.id())
3490 .side(OrderSide::Buy)
3491 .quantity(Quantity::from(100_000))
3492 .build();
3493 let fill = TestOrderEventStubs::filled(
3494 &order,
3495 &audusd_sim,
3496 Some(TradeId::new("1")),
3497 None,
3498 Some(Price::from("1.00001")),
3499 None,
3500 None,
3501 None,
3502 None,
3503 None,
3504 );
3505 let mut position = Position::new(&audusd_sim, fill.into());
3506
3507 let close_order = OrderTestBuilder::new(OrderType::Market)
3508 .instrument_id(audusd_sim.id())
3509 .side(OrderSide::Sell)
3510 .quantity(Quantity::from(100_000))
3511 .build();
3512 let close_fill = TestOrderEventStubs::filled(
3513 &close_order,
3514 &audusd_sim,
3515 Some(TradeId::new("2")),
3516 None,
3517 Some(Price::from("1.00002")),
3518 None,
3519 None,
3520 None,
3521 None,
3522 None,
3523 );
3524 position.apply(&close_fill.into());
3525
3526 assert_eq!(position.side, PositionSide::Flat);
3527 assert_eq!(position.signed_decimal_qty(), Decimal::ZERO);
3528 }
3529
3530 #[rstest]
3531 fn test_position_flat_with_floating_point_precision_edge_case() {
3532 let btc_usdt = currency_pair_btcusdt();
3536 let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
3537
3538 let order1 = OrderTestBuilder::new(OrderType::Market)
3539 .instrument_id(btc_usdt.id())
3540 .side(OrderSide::Buy)
3541 .quantity(Quantity::from("0.123456789"))
3542 .build();
3543 let fill1 = TestOrderEventStubs::filled(
3544 &order1,
3545 &btc_usdt,
3546 Some(TradeId::new("1")),
3547 None,
3548 Some(Price::from("50000.00")),
3549 None,
3550 None,
3551 None,
3552 None,
3553 None,
3554 );
3555 let mut position = Position::new(&btc_usdt, fill1.into());
3556
3557 assert_eq!(position.side, PositionSide::Long);
3558 assert!(position.quantity.is_positive());
3559
3560 let order2 = OrderTestBuilder::new(OrderType::Market)
3561 .instrument_id(btc_usdt.id())
3562 .side(OrderSide::Sell)
3563 .quantity(Quantity::from("0.123456789"))
3564 .build();
3565 let fill2 = TestOrderEventStubs::filled(
3566 &order2,
3567 &btc_usdt,
3568 Some(TradeId::new("2")),
3569 None,
3570 Some(Price::from("50000.00")),
3571 None,
3572 None,
3573 None,
3574 None,
3575 None,
3576 );
3577 position.apply(&fill2.into());
3578
3579 assert_eq!(
3580 position.side,
3581 PositionSide::Flat,
3582 "Position should be FLAT, not {:?}",
3583 position.side
3584 );
3585 assert!(
3586 position.quantity.is_zero(),
3587 "Quantity should be zero, was {}",
3588 position.quantity
3589 );
3590 assert_eq!(
3591 position.signed_qty, 0.0,
3592 "signed_qty should be normalized to 0.0, was {}",
3593 position.signed_qty
3594 );
3595 assert!(position.is_closed());
3596 }
3597
3598 #[rstest]
3599 fn test_position_adjustment_floating_point_precision_edge_case() {
3600 let btc_usdt = currency_pair_btcusdt();
3602 let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
3603
3604 let order = OrderTestBuilder::new(OrderType::Market)
3605 .instrument_id(btc_usdt.id())
3606 .side(OrderSide::Buy)
3607 .quantity(Quantity::from("1.0"))
3608 .build();
3609 let fill = TestOrderEventStubs::filled(
3610 &order,
3611 &btc_usdt,
3612 Some(TradeId::new("1")),
3613 None,
3614 Some(Price::from("50000.00")),
3615 None,
3616 None,
3617 None,
3618 None,
3619 None,
3620 );
3621 let mut position = Position::new(&btc_usdt, fill.into());
3622
3623 let adjustment = PositionAdjusted::new(
3624 position.trader_id,
3625 position.strategy_id,
3626 position.instrument_id,
3627 position.id,
3628 position.account_id,
3629 PositionAdjustmentType::Commission,
3630 Some(Decimal::from_str("-1.0").unwrap()),
3631 None,
3632 None,
3633 uuid4(),
3634 UnixNanos::default(),
3635 UnixNanos::default(),
3636 );
3637 position.apply_adjustment(adjustment);
3638
3639 assert_eq!(
3640 position.side,
3641 PositionSide::Flat,
3642 "Position should be FLAT after zeroing adjustment"
3643 );
3644 assert!(
3645 position.quantity.is_zero(),
3646 "Quantity should be zero after adjustment"
3647 );
3648 assert_eq!(
3649 position.signed_qty, 0.0,
3650 "signed_qty should be normalized to 0.0"
3651 );
3652 }
3653
3654 #[rstest]
3655 fn test_position_spot_buy_partial_fills_with_base_commission() {
3656 let eth_usdt = currency_pair_ethusdt();
3659 let eth_usdt = InstrumentAny::CurrencyPair(eth_usdt);
3660
3661 let order1 = OrderTestBuilder::new(OrderType::Market)
3662 .instrument_id(eth_usdt.id())
3663 .side(OrderSide::Buy)
3664 .quantity(Quantity::from("0.00350"))
3665 .build();
3666
3667 let fill1 = TestOrderEventStubs::filled(
3668 &order1,
3669 ð_usdt,
3670 Some(TradeId::new("1")),
3671 None,
3672 Some(Price::from("2042.69")),
3673 Some(Quantity::from("0.00350")),
3674 None,
3675 Some(Money::new(0.00001, eth_usdt.base_currency().unwrap())),
3676 None,
3677 None,
3678 );
3679
3680 let mut position = Position::new(ð_usdt, fill1.into());
3681
3682 assert_eq!(position.quantity, Quantity::from("0.00349"));
3683 assert!((position.signed_qty - 0.00349).abs() < 1e-9);
3684 assert_eq!(position.side, PositionSide::Long);
3685 assert_eq!(position.adjustments.len(), 1);
3686 assert_eq!(
3687 position.adjustments[0].quantity_change,
3688 Some(rust_decimal_macros::dec!(-0.00001))
3689 );
3690
3691 let order2 = OrderTestBuilder::new(OrderType::Market)
3692 .instrument_id(eth_usdt.id())
3693 .side(OrderSide::Buy)
3694 .quantity(Quantity::from("0.00350"))
3695 .build();
3696
3697 let fill2 = TestOrderEventStubs::filled(
3698 &order2,
3699 ð_usdt,
3700 Some(TradeId::new("2")),
3701 None,
3702 Some(Price::from("2042.69")),
3703 Some(Quantity::from("0.00350")),
3704 None,
3705 Some(Money::new(0.00001, eth_usdt.base_currency().unwrap())),
3706 None,
3707 None,
3708 );
3709
3710 position.apply(&fill2.into());
3711
3712 assert_eq!(position.quantity, Quantity::from("0.00698"));
3713 assert!((position.signed_qty - 0.00698).abs() < 1e-9);
3714 assert_eq!(position.adjustments.len(), 2);
3715
3716 let order3 = OrderTestBuilder::new(OrderType::Market)
3717 .instrument_id(eth_usdt.id())
3718 .side(OrderSide::Buy)
3719 .quantity(Quantity::from("0.00300"))
3720 .build();
3721
3722 let fill3 = TestOrderEventStubs::filled(
3723 &order3,
3724 ð_usdt,
3725 Some(TradeId::new("3")),
3726 None,
3727 Some(Price::from("2042.69")),
3728 Some(Quantity::from("0.00300")),
3729 None,
3730 Some(Money::new(0.00001, eth_usdt.base_currency().unwrap())),
3731 None,
3732 None,
3733 );
3734
3735 position.apply(&fill3.into());
3736
3737 assert_eq!(position.quantity, Quantity::from("0.00997"));
3740 assert!((position.signed_qty - 0.00997).abs() < 1e-9);
3741 assert_eq!(position.side, PositionSide::Long);
3742 assert_eq!(position.adjustments.len(), 3);
3743
3744 assert_eq!(position.buy_qty, Quantity::from("0.01000"));
3746 }
3747
3748 #[rstest]
3749 fn test_position_spot_sell_partial_fills_with_base_commission() {
3750 let btc_usdt = currency_pair_btcusdt();
3751 let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
3752
3753 let order1 = OrderTestBuilder::new(OrderType::Market)
3754 .instrument_id(btc_usdt.id())
3755 .side(OrderSide::Sell)
3756 .quantity(Quantity::from("0.5"))
3757 .build();
3758
3759 let fill1 = TestOrderEventStubs::filled(
3760 &order1,
3761 &btc_usdt,
3762 Some(TradeId::new("1")),
3763 None,
3764 Some(Price::from("50000.0")),
3765 Some(Quantity::from("0.5")),
3766 None,
3767 Some(Money::new(0.001, btc_usdt.base_currency().unwrap())),
3768 None,
3769 None,
3770 );
3771
3772 let mut position = Position::new(&btc_usdt, fill1.into());
3773
3774 assert!((position.signed_qty - (-0.501)).abs() < 1e-9);
3776 assert_eq!(position.side, PositionSide::Short);
3777 assert_eq!(position.adjustments.len(), 1);
3778
3779 let order2 = OrderTestBuilder::new(OrderType::Market)
3780 .instrument_id(btc_usdt.id())
3781 .side(OrderSide::Sell)
3782 .quantity(Quantity::from("0.5"))
3783 .build();
3784
3785 let fill2 = TestOrderEventStubs::filled(
3786 &order2,
3787 &btc_usdt,
3788 Some(TradeId::new("2")),
3789 None,
3790 Some(Price::from("50000.0")),
3791 Some(Quantity::from("0.5")),
3792 None,
3793 Some(Money::new(0.001, btc_usdt.base_currency().unwrap())),
3794 None,
3795 None,
3796 );
3797
3798 position.apply(&fill2.into());
3799
3800 assert!((position.signed_qty - (-1.002)).abs() < 1e-9);
3802 assert!((position.quantity.as_f64() - 1.002).abs() < 1e-9);
3803 assert_eq!(position.adjustments.len(), 2);
3804 assert_eq!(position.sell_qty, Quantity::from("1.0"));
3805 }
3806
3807 #[rstest]
3808 fn test_position_spot_round_trip_close_flat_with_quote_commission() {
3809 let eth_usdt = currency_pair_ethusdt();
3810 let eth_usdt = InstrumentAny::CurrencyPair(eth_usdt);
3811
3812 let buy_order = OrderTestBuilder::new(OrderType::Market)
3813 .instrument_id(eth_usdt.id())
3814 .side(OrderSide::Buy)
3815 .quantity(Quantity::from("1.00000"))
3816 .build();
3817
3818 let buy_fill = TestOrderEventStubs::filled(
3819 &buy_order,
3820 ð_usdt,
3821 Some(TradeId::new("1")),
3822 None,
3823 Some(Price::from("2000.00")),
3824 Some(Quantity::from("1.00000")),
3825 None,
3826 Some(Money::new(0.001, eth_usdt.base_currency().unwrap())),
3827 None,
3828 None,
3829 );
3830
3831 let mut position = Position::new(ð_usdt, buy_fill.into());
3832
3833 assert_eq!(position.quantity, Quantity::from("0.99900"));
3835 assert_eq!(position.side, PositionSide::Long);
3836
3837 let sell_order = OrderTestBuilder::new(OrderType::Market)
3838 .instrument_id(eth_usdt.id())
3839 .side(OrderSide::Sell)
3840 .quantity(Quantity::from("0.99900"))
3841 .build();
3842
3843 let sell_fill = TestOrderEventStubs::filled(
3844 &sell_order,
3845 ð_usdt,
3846 Some(TradeId::new("2")),
3847 None,
3848 Some(Price::from("2100.00")),
3849 Some(Quantity::from("0.99900")),
3850 None,
3851 Some(Money::new(2.0, Currency::USDT())),
3852 None,
3853 None,
3854 );
3855
3856 position.apply(&sell_fill.into());
3857
3858 assert_eq!(position.side, PositionSide::Flat);
3859 assert_eq!(position.signed_qty, 0.0);
3860 assert!(position.is_closed());
3861 assert_eq!(position.adjustments.len(), 1);
3863
3864 let realized = position.realized_pnl.unwrap().as_f64();
3866 assert!(
3867 (realized - 97.9).abs() < 0.01,
3868 "Realized PnL should be ~97.90 USDT, was {realized}"
3869 );
3870 }
3871
3872 #[rstest]
3873 fn test_position_spot_commission_accumulation_multiple_partial_fills() {
3874 let eth_usdt = currency_pair_ethusdt();
3875 let eth_usdt = InstrumentAny::CurrencyPair(eth_usdt);
3876
3877 let order1 = OrderTestBuilder::new(OrderType::Market)
3878 .instrument_id(eth_usdt.id())
3879 .side(OrderSide::Buy)
3880 .quantity(Quantity::from("0.50000"))
3881 .build();
3882
3883 let fill1 = TestOrderEventStubs::filled(
3884 &order1,
3885 ð_usdt,
3886 Some(TradeId::new("1")),
3887 None,
3888 Some(Price::from("2000.00")),
3889 Some(Quantity::from("0.50000")),
3890 None,
3891 Some(Money::new(0.0005, eth_usdt.base_currency().unwrap())),
3892 None,
3893 None,
3894 );
3895
3896 let mut position = Position::new(ð_usdt, fill1.into());
3897
3898 let order2 = OrderTestBuilder::new(OrderType::Market)
3899 .instrument_id(eth_usdt.id())
3900 .side(OrderSide::Buy)
3901 .quantity(Quantity::from("0.50000"))
3902 .build();
3903
3904 let fill2 = TestOrderEventStubs::filled(
3905 &order2,
3906 ð_usdt,
3907 Some(TradeId::new("2")),
3908 None,
3909 Some(Price::from("2010.00")),
3910 Some(Quantity::from("0.50000")),
3911 None,
3912 Some(Money::new(0.0005, eth_usdt.base_currency().unwrap())),
3913 None,
3914 None,
3915 );
3916
3917 position.apply(&fill2.into());
3918
3919 assert_eq!(position.quantity, Quantity::from("0.99900"));
3921 assert_eq!(position.buy_qty, Quantity::from("1.00000"));
3922
3923 assert_eq!(position.adjustments.len(), 2);
3924 for adj in &position.adjustments {
3925 assert_eq!(adj.adjustment_type, PositionAdjustmentType::Commission);
3926 assert_eq!(
3927 adj.quantity_change,
3928 Some(rust_decimal_macros::dec!(-0.0005))
3929 );
3930 }
3931
3932 let commissions = position.commissions();
3933 assert_eq!(commissions.len(), 1);
3934 let eth_commission = commissions[0];
3935 assert!(
3936 (eth_commission.as_f64() - 0.001).abs() < 1e-9,
3937 "Total ETH commission should be 0.001, was {}",
3938 eth_commission.as_f64()
3939 );
3940 }
3941}