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