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