1use std::{
19 collections::{HashMap, HashSet},
20 fmt::Display,
21 hash::{Hash, Hasher},
22};
23
24use nautilus_core::{
25 UnixNanos,
26 correctness::{FAILED, check_equal, check_predicate_true},
27};
28use serde::{Deserialize, Serialize};
29
30use crate::{
31 enums::{OrderSide, OrderSideSpecified, PositionSide},
32 events::OrderFilled,
33 identifiers::{
34 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, Symbol, TradeId, TraderId,
35 Venue, VenueOrderId,
36 },
37 instruments::{Instrument, InstrumentAny},
38 types::{Currency, Money, Price, Quantity},
39};
40
41#[repr(C)]
46#[derive(Debug, Clone, Serialize, Deserialize)]
47#[cfg_attr(
48 feature = "python",
49 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
50)]
51pub struct Position {
52 pub events: Vec<OrderFilled>,
53 pub trader_id: TraderId,
54 pub strategy_id: StrategyId,
55 pub instrument_id: InstrumentId,
56 pub id: PositionId,
57 pub account_id: AccountId,
58 pub opening_order_id: ClientOrderId,
59 pub closing_order_id: Option<ClientOrderId>,
60 pub entry: OrderSide,
61 pub side: PositionSide,
62 pub signed_qty: f64,
63 pub quantity: Quantity,
64 pub peak_qty: Quantity,
65 pub price_precision: u8,
66 pub size_precision: u8,
67 pub multiplier: Quantity,
68 pub is_inverse: bool,
69 pub base_currency: Option<Currency>,
70 pub quote_currency: Currency,
71 pub settlement_currency: Currency,
72 pub ts_init: UnixNanos,
73 pub ts_opened: UnixNanos,
74 pub ts_last: UnixNanos,
75 pub ts_closed: Option<UnixNanos>,
76 pub duration_ns: u64,
77 pub avg_px_open: f64,
78 pub avg_px_close: Option<f64>,
79 pub realized_return: f64,
80 pub realized_pnl: Option<Money>,
81 pub trade_ids: Vec<TradeId>,
82 pub buy_qty: Quantity,
83 pub sell_qty: Quantity,
84 pub commissions: HashMap<Currency, Money>,
85}
86
87impl Position {
88 pub fn new(instrument: &InstrumentAny, fill: OrderFilled) -> Self {
97 check_equal(
98 &instrument.id(),
99 &fill.instrument_id,
100 "instrument.id()",
101 "fill.instrument_id",
102 )
103 .expect(FAILED);
104 assert_ne!(fill.order_side, OrderSide::NoOrderSide);
105
106 let position_id = fill.position_id.expect("No position ID to open `Position`");
107
108 let mut item = Self {
109 events: Vec::<OrderFilled>::new(),
110 trade_ids: Vec::<TradeId>::new(),
111 buy_qty: Quantity::zero(instrument.size_precision()),
112 sell_qty: Quantity::zero(instrument.size_precision()),
113 commissions: HashMap::<Currency, Money>::new(),
114 trader_id: fill.trader_id,
115 strategy_id: fill.strategy_id,
116 instrument_id: fill.instrument_id,
117 id: position_id,
118 account_id: fill.account_id,
119 opening_order_id: fill.client_order_id,
120 closing_order_id: None,
121 entry: fill.order_side,
122 side: PositionSide::Flat,
123 signed_qty: 0.0,
124 quantity: fill.last_qty,
125 peak_qty: fill.last_qty,
126 price_precision: instrument.price_precision(),
127 size_precision: instrument.size_precision(),
128 multiplier: instrument.multiplier(),
129 is_inverse: instrument.is_inverse(),
130 base_currency: instrument.base_currency(),
131 quote_currency: instrument.quote_currency(),
132 settlement_currency: instrument.cost_currency(),
133 ts_init: fill.ts_init,
134 ts_opened: fill.ts_event,
135 ts_last: fill.ts_event,
136 ts_closed: None,
137 duration_ns: 0,
138 avg_px_open: fill.last_px.as_f64(),
139 avg_px_close: None,
140 realized_return: 0.0,
141 realized_pnl: None,
142 };
143 item.apply(&fill);
144 item
145 }
146
147 pub fn purge_events_for_order(&mut self, client_order_id: ClientOrderId) {
158 let filtered_events: Vec<OrderFilled> = self
160 .events
161 .iter()
162 .filter(|e| e.client_order_id != client_order_id)
163 .copied()
164 .collect();
165
166 if filtered_events.is_empty() {
168 log::warn!(
169 "Position {} has no fills remaining after purging order {}; consider closing the position instead",
170 self.id,
171 client_order_id
172 );
173 self.events.clear();
175 self.trade_ids.clear();
176 self.buy_qty = Quantity::zero(self.size_precision);
177 self.sell_qty = Quantity::zero(self.size_precision);
178 self.commissions.clear();
179 self.signed_qty = 0.0;
180 self.quantity = Quantity::zero(self.size_precision);
181 self.side = PositionSide::Flat;
182 self.avg_px_close = None;
183 self.realized_pnl = None;
184 self.realized_return = 0.0;
185 self.ts_opened = UnixNanos::default();
186 self.ts_last = UnixNanos::default();
187 self.ts_closed = Some(UnixNanos::default());
188 self.duration_ns = 0;
189 return;
190 }
191
192 let position_id = self.id;
195 let size_precision = self.size_precision;
196
197 self.events = Vec::new();
199 self.trade_ids = Vec::new();
200 self.buy_qty = Quantity::zero(size_precision);
201 self.sell_qty = Quantity::zero(size_precision);
202 self.commissions.clear();
203 self.signed_qty = 0.0;
204 self.quantity = Quantity::zero(size_precision);
205 self.peak_qty = Quantity::zero(size_precision);
206 self.side = PositionSide::Flat;
207 self.avg_px_open = 0.0;
208 self.avg_px_close = None;
209 self.realized_pnl = None;
210 self.realized_return = 0.0;
211
212 let first_event = &filtered_events[0];
214 self.entry = first_event.order_side;
215 self.opening_order_id = first_event.client_order_id;
216 self.ts_opened = first_event.ts_event;
217 self.ts_init = first_event.ts_init;
218 self.closing_order_id = None;
219 self.ts_closed = None;
220 self.duration_ns = 0;
221
222 for event in filtered_events {
224 self.apply(&event);
225 }
226
227 log::info!(
228 "Purged fills for order {} from position {}; recalculated state: qty={}, signed_qty={}, side={:?}",
229 client_order_id,
230 position_id,
231 self.quantity,
232 self.signed_qty,
233 self.side
234 );
235 }
236
237 pub fn apply(&mut self, fill: &OrderFilled) {
243 check_predicate_true(
244 !self.trade_ids.contains(&fill.trade_id),
245 "`fill.trade_id` already contained in `trade_ids",
246 )
247 .expect(FAILED);
248 check_predicate_true(fill.ts_event >= self.ts_opened, "fill.ts_event < ts_opened")
249 .expect(FAILED);
250
251 if self.side == PositionSide::Flat {
252 self.events.clear();
254 self.trade_ids.clear();
255 self.buy_qty = Quantity::zero(self.size_precision);
256 self.sell_qty = Quantity::zero(self.size_precision);
257 self.commissions.clear();
258 self.opening_order_id = fill.client_order_id;
259 self.closing_order_id = None;
260 self.peak_qty = Quantity::zero(self.size_precision);
261 self.ts_init = fill.ts_init;
262 self.ts_opened = fill.ts_event;
263 self.ts_closed = None;
264 self.duration_ns = 0;
265 self.avg_px_open = fill.last_px.as_f64();
266 self.avg_px_close = None;
267 self.realized_return = 0.0;
268 self.realized_pnl = None;
269 }
270
271 self.events.push(*fill);
272 self.trade_ids.push(fill.trade_id);
273
274 if let Some(commission) = fill.commission {
276 let commission_currency = commission.currency;
277 if let Some(existing_commission) = self.commissions.get_mut(&commission_currency) {
278 *existing_commission += commission;
279 } else {
280 self.commissions.insert(commission_currency, commission);
281 }
282 }
283
284 match fill.specified_side() {
286 OrderSideSpecified::Buy => {
287 self.handle_buy_order_fill(fill);
288 }
289 OrderSideSpecified::Sell => {
290 self.handle_sell_order_fill(fill);
291 }
292 }
293
294 self.quantity = Quantity::new(self.signed_qty.abs(), self.size_precision);
297 if self.quantity > self.peak_qty {
298 self.peak_qty = self.quantity;
299 }
300
301 if self.signed_qty > 0.0 {
303 self.entry = OrderSide::Buy;
304 self.side = PositionSide::Long;
305 } else if self.signed_qty < 0.0 {
306 self.entry = OrderSide::Sell;
307 self.side = PositionSide::Short;
308 } else {
309 self.side = PositionSide::Flat;
310 self.closing_order_id = Some(fill.client_order_id);
311 self.ts_closed = Some(fill.ts_event);
312 self.duration_ns = if let Some(ts_closed) = self.ts_closed {
313 ts_closed.as_u64() - self.ts_opened.as_u64()
314 } else {
315 0
316 };
317 }
318
319 self.ts_last = fill.ts_event;
320 }
321
322 fn handle_buy_order_fill(&mut self, fill: &OrderFilled) {
323 let mut realized_pnl = if let Some(commission) = fill.commission {
325 if commission.currency == self.settlement_currency {
326 -commission.as_f64()
327 } else {
328 0.0
329 }
330 } else {
331 0.0
332 };
333
334 let last_px = fill.last_px.as_f64();
335 let last_qty = fill.last_qty.as_f64();
336 let last_qty_object = fill.last_qty;
337
338 if self.signed_qty > 0.0 {
339 self.avg_px_open = self.calculate_avg_px_open_px(last_px, last_qty);
340 } else if self.signed_qty < 0.0 {
341 let avg_px_close = self.calculate_avg_px_close_px(last_px, last_qty);
343 self.avg_px_close = Some(avg_px_close);
344 self.realized_return = self
345 .calculate_return(self.avg_px_open, avg_px_close)
346 .unwrap_or_else(|e| {
347 log::error!("Error calculating return: {e}");
348 0.0
349 });
350 realized_pnl += self
351 .calculate_pnl_raw(self.avg_px_open, last_px, last_qty)
352 .unwrap_or_else(|e| {
353 log::error!("Error calculating PnL: {e}");
354 0.0
355 });
356 }
357
358 let current_pnl = self.realized_pnl.map_or(0.0, |p| p.as_f64());
359 self.realized_pnl = Some(Money::new(
360 current_pnl + realized_pnl,
361 self.settlement_currency,
362 ));
363
364 self.signed_qty += last_qty;
365 self.buy_qty += last_qty_object;
366 }
367
368 fn handle_sell_order_fill(&mut self, fill: &OrderFilled) {
369 let mut realized_pnl = if let Some(commission) = fill.commission {
371 if commission.currency == self.settlement_currency {
372 -commission.as_f64()
373 } else {
374 0.0
375 }
376 } else {
377 0.0
378 };
379
380 let last_px = fill.last_px.as_f64();
381 let last_qty = fill.last_qty.as_f64();
382 let last_qty_object = fill.last_qty;
383
384 if self.signed_qty < 0.0 {
385 self.avg_px_open = self.calculate_avg_px_open_px(last_px, last_qty);
386 } else if self.signed_qty > 0.0 {
387 let avg_px_close = self.calculate_avg_px_close_px(last_px, last_qty);
388 self.avg_px_close = Some(avg_px_close);
389 self.realized_return = self
390 .calculate_return(self.avg_px_open, avg_px_close)
391 .unwrap_or_else(|e| {
392 log::error!("Error calculating return: {e}");
393 0.0
394 });
395 realized_pnl += self
396 .calculate_pnl_raw(self.avg_px_open, last_px, last_qty)
397 .unwrap_or_else(|e| {
398 log::error!("Error calculating PnL: {e}");
399 0.0
400 });
401 }
402
403 let current_pnl = self.realized_pnl.map_or(0.0, |p| p.as_f64());
404 self.realized_pnl = Some(Money::new(
405 current_pnl + realized_pnl,
406 self.settlement_currency,
407 ));
408
409 self.signed_qty -= last_qty;
410 self.sell_qty += last_qty_object;
411 }
412
413 fn calculate_avg_px(
455 &self,
456 qty: f64,
457 avg_pg: f64,
458 last_px: f64,
459 last_qty: f64,
460 ) -> anyhow::Result<f64> {
461 if qty == 0.0 && last_qty == 0.0 {
462 anyhow::bail!("Cannot calculate average price: both quantities are zero");
463 }
464
465 if last_qty == 0.0 {
466 anyhow::bail!("Cannot calculate average price: fill quantity is zero");
467 }
468
469 if qty == 0.0 {
470 return Ok(last_px);
471 }
472
473 let start_cost = avg_pg * qty;
474 let event_cost = last_px * last_qty;
475 let total_qty = qty + last_qty;
476
477 if total_qty <= 0.0 {
479 anyhow::bail!(
480 "Total quantity unexpectedly zero or negative in average price calculation: qty={}, last_qty={}, total_qty={}",
481 qty,
482 last_qty,
483 total_qty
484 );
485 }
486
487 Ok((start_cost + event_cost) / total_qty)
488 }
489
490 fn calculate_avg_px_open_px(&self, last_px: f64, last_qty: f64) -> f64 {
491 self.calculate_avg_px(self.quantity.as_f64(), self.avg_px_open, last_px, last_qty)
492 .unwrap_or_else(|e| {
493 log::error!("Error calculating average open price: {}", e);
494 last_px
495 })
496 }
497
498 fn calculate_avg_px_close_px(&self, last_px: f64, last_qty: f64) -> f64 {
499 let Some(avg_px_close) = self.avg_px_close else {
500 return last_px;
501 };
502 let closing_qty = if self.side == PositionSide::Long {
503 self.sell_qty
504 } else {
505 self.buy_qty
506 };
507 self.calculate_avg_px(closing_qty.as_f64(), avg_px_close, last_px, last_qty)
508 .unwrap_or_else(|e| {
509 log::error!("Error calculating average close price: {}", e);
510 last_px
511 })
512 }
513
514 fn calculate_points(&self, avg_px_open: f64, avg_px_close: f64) -> f64 {
515 match self.side {
516 PositionSide::Long => avg_px_close - avg_px_open,
517 PositionSide::Short => avg_px_open - avg_px_close,
518 _ => 0.0, }
520 }
521
522 fn calculate_points_inverse(&self, avg_px_open: f64, avg_px_close: f64) -> anyhow::Result<f64> {
523 const EPSILON: f64 = 1e-15;
525
526 if avg_px_open.abs() < EPSILON {
528 anyhow::bail!(
529 "Cannot calculate inverse points: open price is zero or too small ({})",
530 avg_px_open
531 );
532 }
533 if avg_px_close.abs() < EPSILON {
534 anyhow::bail!(
535 "Cannot calculate inverse points: close price is zero or too small ({})",
536 avg_px_close
537 );
538 }
539
540 let inverse_open = 1.0 / avg_px_open;
541 let inverse_close = 1.0 / avg_px_close;
542 let result = match self.side {
543 PositionSide::Long => inverse_open - inverse_close,
544 PositionSide::Short => inverse_close - inverse_open,
545 _ => 0.0, };
547 Ok(result)
548 }
549
550 fn calculate_return(&self, avg_px_open: f64, avg_px_close: f64) -> anyhow::Result<f64> {
551 if avg_px_open == 0.0 {
553 anyhow::bail!(
554 "Cannot calculate return: open price is zero (close price: {})",
555 avg_px_close
556 );
557 }
558 Ok(self.calculate_points(avg_px_open, avg_px_close) / avg_px_open)
559 }
560
561 fn calculate_pnl_raw(
562 &self,
563 avg_px_open: f64,
564 avg_px_close: f64,
565 quantity: f64,
566 ) -> anyhow::Result<f64> {
567 let quantity = quantity.min(self.signed_qty.abs());
568 let result = if self.is_inverse {
569 let points = self.calculate_points_inverse(avg_px_open, avg_px_close)?;
570 quantity * self.multiplier.as_f64() * points
571 } else {
572 quantity * self.multiplier.as_f64() * self.calculate_points(avg_px_open, avg_px_close)
573 };
574 Ok(result)
575 }
576
577 #[must_use]
578 pub fn calculate_pnl(&self, avg_px_open: f64, avg_px_close: f64, quantity: Quantity) -> Money {
579 let pnl_raw = self
580 .calculate_pnl_raw(avg_px_open, avg_px_close, quantity.as_f64())
581 .unwrap_or_else(|e| {
582 log::error!("Error calculating PnL: {e}");
583 0.0
584 });
585 Money::new(pnl_raw, self.settlement_currency)
586 }
587
588 #[must_use]
589 pub fn total_pnl(&self, last: Price) -> Money {
590 let realized_pnl = self.realized_pnl.map_or(0.0, |pnl| pnl.as_f64());
591 Money::new(
592 realized_pnl + self.unrealized_pnl(last).as_f64(),
593 self.settlement_currency,
594 )
595 }
596
597 #[must_use]
598 pub fn unrealized_pnl(&self, last: Price) -> Money {
599 if self.side == PositionSide::Flat {
600 Money::new(0.0, self.settlement_currency)
601 } else {
602 let avg_px_open = self.avg_px_open;
603 let avg_px_close = last.as_f64();
604 let quantity = self.quantity.as_f64();
605 let pnl = self
606 .calculate_pnl_raw(avg_px_open, avg_px_close, quantity)
607 .unwrap_or_else(|e| {
608 log::error!("Error calculating unrealized PnL: {e}");
609 0.0
610 });
611 Money::new(pnl, self.settlement_currency)
612 }
613 }
614
615 pub fn closing_order_side(&self) -> OrderSide {
616 match self.side {
617 PositionSide::Long => OrderSide::Sell,
618 PositionSide::Short => OrderSide::Buy,
619 _ => OrderSide::NoOrderSide,
620 }
621 }
622
623 #[must_use]
624 pub fn is_opposite_side(&self, side: OrderSide) -> bool {
625 self.entry != side
626 }
627
628 #[must_use]
629 pub fn symbol(&self) -> Symbol {
630 self.instrument_id.symbol
631 }
632
633 #[must_use]
634 pub fn venue(&self) -> Venue {
635 self.instrument_id.venue
636 }
637
638 #[must_use]
639 pub fn event_count(&self) -> usize {
640 self.events.len()
641 }
642
643 #[must_use]
644 pub fn client_order_ids(&self) -> Vec<ClientOrderId> {
645 let mut result = self
647 .events
648 .iter()
649 .map(|event| event.client_order_id)
650 .collect::<HashSet<ClientOrderId>>()
651 .into_iter()
652 .collect::<Vec<ClientOrderId>>();
653 result.sort_unstable();
654 result
655 }
656
657 #[must_use]
658 pub fn venue_order_ids(&self) -> Vec<VenueOrderId> {
659 let mut result = self
661 .events
662 .iter()
663 .map(|event| event.venue_order_id)
664 .collect::<HashSet<VenueOrderId>>()
665 .into_iter()
666 .collect::<Vec<VenueOrderId>>();
667 result.sort_unstable();
668 result
669 }
670
671 #[must_use]
672 pub fn trade_ids(&self) -> Vec<TradeId> {
673 let mut result = self
674 .events
675 .iter()
676 .map(|event| event.trade_id)
677 .collect::<HashSet<TradeId>>()
678 .into_iter()
679 .collect::<Vec<TradeId>>();
680 result.sort_unstable();
681 result
682 }
683
684 #[must_use]
690 pub fn notional_value(&self, last: Price) -> Money {
691 if self.is_inverse {
692 Money::new(
693 self.quantity.as_f64() * self.multiplier.as_f64() * (1.0 / last.as_f64()),
694 self.base_currency.unwrap(),
695 )
696 } else {
697 Money::new(
698 self.quantity.as_f64() * last.as_f64() * self.multiplier.as_f64(),
699 self.quote_currency,
700 )
701 }
702 }
703
704 #[must_use]
706 pub fn last_event(&self) -> Option<OrderFilled> {
707 self.events.last().copied()
708 }
709
710 #[must_use]
711 pub fn last_trade_id(&self) -> Option<TradeId> {
712 self.trade_ids.last().copied()
713 }
714
715 #[must_use]
716 pub fn is_long(&self) -> bool {
717 self.side == PositionSide::Long
718 }
719
720 #[must_use]
721 pub fn is_short(&self) -> bool {
722 self.side == PositionSide::Short
723 }
724
725 #[must_use]
726 pub fn is_open(&self) -> bool {
727 self.side != PositionSide::Flat && self.ts_closed.is_none()
728 }
729
730 #[must_use]
731 pub fn is_closed(&self) -> bool {
732 self.side == PositionSide::Flat && self.ts_closed.is_some()
733 }
734
735 #[must_use]
736 pub fn commissions(&self) -> Vec<Money> {
737 self.commissions.values().copied().collect()
738 }
739}
740
741impl PartialEq<Self> for Position {
742 fn eq(&self, other: &Self) -> bool {
743 self.id == other.id
744 }
745}
746
747impl Eq for Position {}
748
749impl Hash for Position {
750 fn hash<H: Hasher>(&self, state: &mut H) {
751 self.id.hash(state);
752 }
753}
754
755impl Display for Position {
756 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
757 let quantity_str = if self.quantity != Quantity::zero(self.price_precision) {
758 self.quantity.to_formatted_string() + " "
759 } else {
760 String::new()
761 };
762 write!(
763 f,
764 "Position({} {}{}, id={})",
765 self.side, quantity_str, self.instrument_id, self.id
766 )
767 }
768}
769
770#[cfg(test)]
774mod tests {
775 use std::str::FromStr;
776
777 use nautilus_core::UnixNanos;
778 use rstest::rstest;
779
780 use crate::{
781 enums::{LiquiditySide, OrderSide, OrderType, PositionSide},
782 events::OrderFilled,
783 identifiers::{
784 AccountId, ClientOrderId, PositionId, StrategyId, TradeId, VenueOrderId, stubs::uuid4,
785 },
786 instruments::{CryptoPerpetual, CurrencyPair, Instrument, InstrumentAny, stubs::*},
787 orders::{Order, builder::OrderTestBuilder, stubs::TestOrderEventStubs},
788 position::Position,
789 stubs::*,
790 types::{Currency, Money, Price, Quantity},
791 };
792
793 #[rstest]
794 fn test_position_long_display(stub_position_long: Position) {
795 let display = format!("{stub_position_long}");
796 assert_eq!(display, "Position(LONG 1 AUD/USD.SIM, id=1)");
797 }
798
799 #[rstest]
800 fn test_position_short_display(stub_position_short: Position) {
801 let display = format!("{stub_position_short}");
802 assert_eq!(display, "Position(SHORT 1 AUD/USD.SIM, id=1)");
803 }
804
805 #[rstest]
806 #[should_panic(expected = "`fill.trade_id` already contained in `trade_ids")]
807 fn test_two_trades_with_same_trade_id_error(audusd_sim: CurrencyPair) {
808 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
809 let order1 = OrderTestBuilder::new(OrderType::Market)
810 .instrument_id(audusd_sim.id())
811 .side(OrderSide::Buy)
812 .quantity(Quantity::from(100_000))
813 .build();
814 let order2 = OrderTestBuilder::new(OrderType::Market)
815 .instrument_id(audusd_sim.id())
816 .side(OrderSide::Buy)
817 .quantity(Quantity::from(100_000))
818 .build();
819 let fill1 = TestOrderEventStubs::filled(
820 &order1,
821 &audusd_sim,
822 Some(TradeId::new("1")),
823 None,
824 Some(Price::from("1.00001")),
825 None,
826 None,
827 None,
828 None,
829 None,
830 );
831 let fill2 = TestOrderEventStubs::filled(
832 &order2,
833 &audusd_sim,
834 Some(TradeId::new("1")),
835 None,
836 Some(Price::from("1.00002")),
837 None,
838 None,
839 None,
840 None,
841 None,
842 );
843 let mut position = Position::new(&audusd_sim, fill1.into());
844 position.apply(&fill2.into());
845 }
846
847 #[rstest]
848 fn test_position_filled_with_buy_order(audusd_sim: CurrencyPair) {
849 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
850 let order = OrderTestBuilder::new(OrderType::Market)
851 .instrument_id(audusd_sim.id())
852 .side(OrderSide::Buy)
853 .quantity(Quantity::from(100_000))
854 .build();
855 let fill = TestOrderEventStubs::filled(
856 &order,
857 &audusd_sim,
858 None,
859 None,
860 Some(Price::from("1.00001")),
861 None,
862 None,
863 None,
864 None,
865 None,
866 );
867 let last_price = Price::from_str("1.0005").unwrap();
868 let position = Position::new(&audusd_sim, fill.into());
869 assert_eq!(position.symbol(), audusd_sim.id().symbol);
870 assert_eq!(position.venue(), audusd_sim.id().venue);
871 assert_eq!(position.closing_order_side(), OrderSide::Sell);
872 assert!(!position.is_opposite_side(OrderSide::Buy));
873 assert_eq!(position, position); assert!(position.closing_order_id.is_none());
875 assert_eq!(position.quantity, Quantity::from(100_000));
876 assert_eq!(position.peak_qty, Quantity::from(100_000));
877 assert_eq!(position.size_precision, 0);
878 assert_eq!(position.signed_qty, 100_000.0);
879 assert_eq!(position.entry, OrderSide::Buy);
880 assert_eq!(position.side, PositionSide::Long);
881 assert_eq!(position.ts_opened.as_u64(), 0);
882 assert_eq!(position.duration_ns, 0);
883 assert_eq!(position.avg_px_open, 1.00001);
884 assert_eq!(position.event_count(), 1);
885 assert_eq!(position.id, PositionId::new("1"));
886 assert_eq!(position.events.len(), 1);
887 assert!(position.is_long());
888 assert!(!position.is_short());
889 assert!(position.is_open());
890 assert!(!position.is_closed());
891 assert_eq!(position.realized_return, 0.0);
892 assert_eq!(position.realized_pnl, Some(Money::from("-2.0 USD")));
893 assert_eq!(position.unrealized_pnl(last_price), Money::from("49.0 USD"));
894 assert_eq!(position.total_pnl(last_price), Money::from("47.0 USD"));
895 assert_eq!(position.commissions(), vec![Money::from("2.0 USD")]);
896 assert_eq!(
897 format!("{position}"),
898 "Position(LONG 100_000 AUD/USD.SIM, id=1)"
899 );
900 }
901
902 #[rstest]
903 fn test_position_filled_with_sell_order(audusd_sim: CurrencyPair) {
904 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
905 let order = OrderTestBuilder::new(OrderType::Market)
906 .instrument_id(audusd_sim.id())
907 .side(OrderSide::Sell)
908 .quantity(Quantity::from(100_000))
909 .build();
910 let fill = TestOrderEventStubs::filled(
911 &order,
912 &audusd_sim,
913 None,
914 None,
915 Some(Price::from("1.00001")),
916 None,
917 None,
918 None,
919 None,
920 None,
921 );
922 let last_price = Price::from_str("1.00050").unwrap();
923 let position = Position::new(&audusd_sim, fill.into());
924 assert_eq!(position.symbol(), audusd_sim.id().symbol);
925 assert_eq!(position.venue(), audusd_sim.id().venue);
926 assert_eq!(position.closing_order_side(), OrderSide::Buy);
927 assert!(!position.is_opposite_side(OrderSide::Sell));
928 assert_eq!(position, position); assert!(position.closing_order_id.is_none());
930 assert_eq!(position.quantity, Quantity::from(100_000));
931 assert_eq!(position.peak_qty, Quantity::from(100_000));
932 assert_eq!(position.signed_qty, -100_000.0);
933 assert_eq!(position.entry, OrderSide::Sell);
934 assert_eq!(position.side, PositionSide::Short);
935 assert_eq!(position.ts_opened.as_u64(), 0);
936 assert_eq!(position.avg_px_open, 1.00001);
937 assert_eq!(position.event_count(), 1);
938 assert_eq!(position.id, PositionId::new("1"));
939 assert_eq!(position.events.len(), 1);
940 assert!(!position.is_long());
941 assert!(position.is_short());
942 assert!(position.is_open());
943 assert!(!position.is_closed());
944 assert_eq!(position.realized_return, 0.0);
945 assert_eq!(position.realized_pnl, Some(Money::from("-2.0 USD")));
946 assert_eq!(
947 position.unrealized_pnl(last_price),
948 Money::from("-49.0 USD")
949 );
950 assert_eq!(position.total_pnl(last_price), Money::from("-51.0 USD"));
951 assert_eq!(position.commissions(), vec![Money::from("2.0 USD")]);
952 assert_eq!(
953 format!("{position}"),
954 "Position(SHORT 100_000 AUD/USD.SIM, id=1)"
955 );
956 }
957
958 #[rstest]
959 fn test_position_partial_fills_with_buy_order(audusd_sim: CurrencyPair) {
960 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
961 let order = OrderTestBuilder::new(OrderType::Market)
962 .instrument_id(audusd_sim.id())
963 .side(OrderSide::Buy)
964 .quantity(Quantity::from(100_000))
965 .build();
966 let fill = TestOrderEventStubs::filled(
967 &order,
968 &audusd_sim,
969 None,
970 None,
971 Some(Price::from("1.00001")),
972 Some(Quantity::from(50_000)),
973 None,
974 None,
975 None,
976 None,
977 );
978 let last_price = Price::from_str("1.00048").unwrap();
979 let position = Position::new(&audusd_sim, fill.into());
980 assert_eq!(position.quantity, Quantity::from(50_000));
981 assert_eq!(position.peak_qty, Quantity::from(50_000));
982 assert_eq!(position.side, PositionSide::Long);
983 assert_eq!(position.signed_qty, 50000.0);
984 assert_eq!(position.avg_px_open, 1.00001);
985 assert_eq!(position.event_count(), 1);
986 assert_eq!(position.ts_opened.as_u64(), 0);
987 assert!(position.is_long());
988 assert!(!position.is_short());
989 assert!(position.is_open());
990 assert!(!position.is_closed());
991 assert_eq!(position.realized_return, 0.0);
992 assert_eq!(position.realized_pnl, Some(Money::from("-2.0 USD")));
993 assert_eq!(position.unrealized_pnl(last_price), Money::from("23.5 USD"));
994 assert_eq!(position.total_pnl(last_price), Money::from("21.5 USD"));
995 assert_eq!(position.commissions(), vec![Money::from("2.0 USD")]);
996 assert_eq!(
997 format!("{position}"),
998 "Position(LONG 50_000 AUD/USD.SIM, id=1)"
999 );
1000 }
1001
1002 #[rstest]
1003 fn test_position_partial_fills_with_two_sell_orders(audusd_sim: CurrencyPair) {
1004 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1005 let order = OrderTestBuilder::new(OrderType::Market)
1006 .instrument_id(audusd_sim.id())
1007 .side(OrderSide::Sell)
1008 .quantity(Quantity::from(100_000))
1009 .build();
1010 let fill1 = TestOrderEventStubs::filled(
1011 &order,
1012 &audusd_sim,
1013 Some(TradeId::new("1")),
1014 None,
1015 Some(Price::from("1.00001")),
1016 Some(Quantity::from(50_000)),
1017 None,
1018 None,
1019 None,
1020 None,
1021 );
1022 let fill2 = TestOrderEventStubs::filled(
1023 &order,
1024 &audusd_sim,
1025 Some(TradeId::new("2")),
1026 None,
1027 Some(Price::from("1.00002")),
1028 Some(Quantity::from(50_000)),
1029 None,
1030 None,
1031 None,
1032 None,
1033 );
1034 let last_price = Price::from_str("1.0005").unwrap();
1035 let mut position = Position::new(&audusd_sim, fill1.into());
1036 position.apply(&fill2.into());
1037
1038 assert_eq!(position.quantity, Quantity::from(100_000));
1039 assert_eq!(position.peak_qty, Quantity::from(100_000));
1040 assert_eq!(position.side, PositionSide::Short);
1041 assert_eq!(position.signed_qty, -100_000.0);
1042 assert_eq!(position.avg_px_open, 1.000_015);
1043 assert_eq!(position.event_count(), 2);
1044 assert_eq!(position.ts_opened, 0);
1045 assert!(position.is_short());
1046 assert!(!position.is_long());
1047 assert!(position.is_open());
1048 assert!(!position.is_closed());
1049 assert_eq!(position.realized_return, 0.0);
1050 assert_eq!(position.realized_pnl, Some(Money::from("-4.0 USD")));
1051 assert_eq!(
1052 position.unrealized_pnl(last_price),
1053 Money::from("-48.5 USD")
1054 );
1055 assert_eq!(position.total_pnl(last_price), Money::from("-52.5 USD"));
1056 assert_eq!(position.commissions(), vec![Money::from("4.0 USD")]);
1057 }
1058
1059 #[rstest]
1060 pub fn test_position_filled_with_buy_order_then_sell_order(audusd_sim: CurrencyPair) {
1061 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1062 let order = OrderTestBuilder::new(OrderType::Market)
1063 .instrument_id(audusd_sim.id())
1064 .side(OrderSide::Buy)
1065 .quantity(Quantity::from(150_000))
1066 .build();
1067 let fill = TestOrderEventStubs::filled(
1068 &order,
1069 &audusd_sim,
1070 Some(TradeId::new("1")),
1071 Some(PositionId::new("P-1")),
1072 Some(Price::from("1.00001")),
1073 None,
1074 None,
1075 None,
1076 Some(UnixNanos::from(1_000_000_000)),
1077 None,
1078 );
1079 let mut position = Position::new(&audusd_sim, fill.into());
1080
1081 let fill2 = OrderFilled::new(
1082 order.trader_id(),
1083 StrategyId::new("S-001"),
1084 order.instrument_id(),
1085 order.client_order_id(),
1086 VenueOrderId::from("2"),
1087 order.account_id().unwrap_or(AccountId::new("SIM-001")),
1088 TradeId::new("2"),
1089 OrderSide::Sell,
1090 OrderType::Market,
1091 order.quantity(),
1092 Price::from("1.00011"),
1093 audusd_sim.quote_currency(),
1094 LiquiditySide::Taker,
1095 uuid4(),
1096 2_000_000_000.into(),
1097 0.into(),
1098 false,
1099 Some(PositionId::new("T1")),
1100 Some(Money::from("0.0 USD")),
1101 );
1102 position.apply(&fill2);
1103 let last = Price::from_str("1.0005").unwrap();
1104
1105 assert!(position.is_opposite_side(fill2.order_side));
1106 assert_eq!(
1107 position.quantity,
1108 Quantity::zero(audusd_sim.price_precision())
1109 );
1110 assert_eq!(position.size_precision, 0);
1111 assert_eq!(position.signed_qty, 0.0);
1112 assert_eq!(position.side, PositionSide::Flat);
1113 assert_eq!(position.ts_opened, 1_000_000_000);
1114 assert_eq!(position.ts_closed, Some(UnixNanos::from(2_000_000_000)));
1115 assert_eq!(position.duration_ns, 1_000_000_000);
1116 assert_eq!(position.avg_px_open, 1.00001);
1117 assert_eq!(position.avg_px_close, Some(1.00011));
1118 assert!(!position.is_long());
1119 assert!(!position.is_short());
1120 assert!(!position.is_open());
1121 assert!(position.is_closed());
1122 assert_eq!(position.realized_return, 9.999_900_000_998_888e-5);
1123 assert_eq!(position.realized_pnl, Some(Money::from("13.0 USD")));
1124 assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1125 assert_eq!(position.commissions(), vec![Money::from("2 USD")]);
1126 assert_eq!(position.total_pnl(last), Money::from("13 USD"));
1127 assert_eq!(format!("{position}"), "Position(FLAT AUD/USD.SIM, id=P-1)");
1128 }
1129
1130 #[rstest]
1131 pub fn test_position_filled_with_sell_order_then_buy_order(audusd_sim: CurrencyPair) {
1132 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1133 let order1 = OrderTestBuilder::new(OrderType::Market)
1134 .instrument_id(audusd_sim.id())
1135 .side(OrderSide::Sell)
1136 .quantity(Quantity::from(100_000))
1137 .build();
1138 let order2 = OrderTestBuilder::new(OrderType::Market)
1139 .instrument_id(audusd_sim.id())
1140 .side(OrderSide::Buy)
1141 .quantity(Quantity::from(100_000))
1142 .build();
1143 let fill1 = TestOrderEventStubs::filled(
1144 &order1,
1145 &audusd_sim,
1146 None,
1147 Some(PositionId::new("P-19700101-000000-001-001-1")),
1148 Some(Price::from("1.0")),
1149 None,
1150 None,
1151 None,
1152 None,
1153 None,
1154 );
1155 let mut position = Position::new(&audusd_sim, fill1.into());
1156 let fill2 = TestOrderEventStubs::filled(
1158 &order2,
1159 &audusd_sim,
1160 Some(TradeId::new("1")),
1161 Some(PositionId::new("P-19700101-000000-001-001-1")),
1162 Some(Price::from("1.00001")),
1163 Some(Quantity::from(50_000)),
1164 None,
1165 None,
1166 None,
1167 None,
1168 );
1169 let fill3 = TestOrderEventStubs::filled(
1170 &order2,
1171 &audusd_sim,
1172 Some(TradeId::new("2")),
1173 Some(PositionId::new("P-19700101-000000-001-001-1")),
1174 Some(Price::from("1.00003")),
1175 Some(Quantity::from(50_000)),
1176 None,
1177 None,
1178 None,
1179 None,
1180 );
1181 let last = Price::from("1.0005");
1182 position.apply(&fill2.into());
1183 position.apply(&fill3.into());
1184
1185 assert_eq!(
1186 position.quantity,
1187 Quantity::zero(audusd_sim.price_precision())
1188 );
1189 assert_eq!(position.side, PositionSide::Flat);
1190 assert_eq!(position.ts_opened, 0);
1191 assert_eq!(position.avg_px_open, 1.0);
1192 assert_eq!(position.events.len(), 3);
1193 assert_eq!(position.ts_closed, Some(UnixNanos::default()));
1194 assert_eq!(position.avg_px_close, Some(1.00002));
1195 assert!(!position.is_long());
1196 assert!(!position.is_short());
1197 assert!(!position.is_open());
1198 assert!(position.is_closed());
1199 assert_eq!(position.commissions(), vec![Money::from("6.0 USD")]);
1200 assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1201 assert_eq!(position.realized_pnl, Some(Money::from("-8.0 USD")));
1202 assert_eq!(position.total_pnl(last), Money::from("-8.0 USD"));
1203 assert_eq!(
1204 format!("{position}"),
1205 "Position(FLAT AUD/USD.SIM, id=P-19700101-000000-001-001-1)"
1206 );
1207 }
1208
1209 #[rstest]
1210 fn test_position_filled_with_no_change(audusd_sim: CurrencyPair) {
1211 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1212 let order1 = OrderTestBuilder::new(OrderType::Market)
1213 .instrument_id(audusd_sim.id())
1214 .side(OrderSide::Buy)
1215 .quantity(Quantity::from(100_000))
1216 .build();
1217 let order2 = OrderTestBuilder::new(OrderType::Market)
1218 .instrument_id(audusd_sim.id())
1219 .side(OrderSide::Sell)
1220 .quantity(Quantity::from(100_000))
1221 .build();
1222 let fill1 = TestOrderEventStubs::filled(
1223 &order1,
1224 &audusd_sim,
1225 Some(TradeId::new("1")),
1226 Some(PositionId::new("P-19700101-000000-001-001-1")),
1227 Some(Price::from("1.0")),
1228 None,
1229 None,
1230 None,
1231 None,
1232 None,
1233 );
1234 let mut position = Position::new(&audusd_sim, fill1.into());
1235 let fill2 = TestOrderEventStubs::filled(
1236 &order2,
1237 &audusd_sim,
1238 Some(TradeId::new("2")),
1239 Some(PositionId::new("P-19700101-000000-001-001-1")),
1240 Some(Price::from("1.0")),
1241 None,
1242 None,
1243 None,
1244 None,
1245 None,
1246 );
1247 let last = Price::from("1.0005");
1248 position.apply(&fill2.into());
1249
1250 assert_eq!(
1251 position.quantity,
1252 Quantity::zero(audusd_sim.price_precision())
1253 );
1254 assert_eq!(position.closing_order_side(), OrderSide::NoOrderSide);
1255 assert_eq!(position.side, PositionSide::Flat);
1256 assert_eq!(position.ts_opened, 0);
1257 assert_eq!(position.avg_px_open, 1.0);
1258 assert_eq!(position.events.len(), 2);
1259 assert_eq!(position.ts_closed, Some(UnixNanos::default()));
1261 assert_eq!(position.avg_px_close, Some(1.0));
1262 assert!(!position.is_long());
1263 assert!(!position.is_short());
1264 assert!(!position.is_open());
1265 assert!(position.is_closed());
1266 assert_eq!(position.commissions(), vec![Money::from("4.0 USD")]);
1267 assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1268 assert_eq!(position.realized_pnl, Some(Money::from("-4.0 USD")));
1269 assert_eq!(position.total_pnl(last), Money::from("-4.0 USD"));
1270 assert_eq!(
1271 format!("{position}"),
1272 "Position(FLAT AUD/USD.SIM, id=P-19700101-000000-001-001-1)"
1273 );
1274 }
1275
1276 #[rstest]
1277 fn test_position_long_with_multiple_filled_orders(audusd_sim: CurrencyPair) {
1278 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1279 let order1 = OrderTestBuilder::new(OrderType::Market)
1280 .instrument_id(audusd_sim.id())
1281 .side(OrderSide::Buy)
1282 .quantity(Quantity::from(100_000))
1283 .build();
1284 let order2 = OrderTestBuilder::new(OrderType::Market)
1285 .instrument_id(audusd_sim.id())
1286 .side(OrderSide::Buy)
1287 .quantity(Quantity::from(100_000))
1288 .build();
1289 let order3 = OrderTestBuilder::new(OrderType::Market)
1290 .instrument_id(audusd_sim.id())
1291 .side(OrderSide::Sell)
1292 .quantity(Quantity::from(200_000))
1293 .build();
1294 let fill1 = TestOrderEventStubs::filled(
1295 &order1,
1296 &audusd_sim,
1297 Some(TradeId::new("1")),
1298 Some(PositionId::new("P-123456")),
1299 Some(Price::from("1.0")),
1300 None,
1301 None,
1302 None,
1303 None,
1304 None,
1305 );
1306 let fill2 = TestOrderEventStubs::filled(
1307 &order2,
1308 &audusd_sim,
1309 Some(TradeId::new("2")),
1310 Some(PositionId::new("P-123456")),
1311 Some(Price::from("1.00001")),
1312 None,
1313 None,
1314 None,
1315 None,
1316 None,
1317 );
1318 let fill3 = TestOrderEventStubs::filled(
1319 &order3,
1320 &audusd_sim,
1321 Some(TradeId::new("3")),
1322 Some(PositionId::new("P-123456")),
1323 Some(Price::from("1.0001")),
1324 None,
1325 None,
1326 None,
1327 None,
1328 None,
1329 );
1330 let mut position = Position::new(&audusd_sim, fill1.into());
1331 let last = Price::from("1.0005");
1332 position.apply(&fill2.into());
1333 position.apply(&fill3.into());
1334
1335 assert_eq!(
1336 position.quantity,
1337 Quantity::zero(audusd_sim.price_precision())
1338 );
1339 assert_eq!(position.side, PositionSide::Flat);
1340 assert_eq!(position.ts_opened, 0);
1341 assert_eq!(position.avg_px_open, 1.000_005);
1342 assert_eq!(position.events.len(), 3);
1343 assert_eq!(position.ts_closed, Some(UnixNanos::default()));
1348 assert_eq!(position.avg_px_close, Some(1.0001));
1349 assert!(position.is_closed());
1350 assert!(!position.is_open());
1351 assert!(!position.is_long());
1352 assert!(!position.is_short());
1353 assert_eq!(position.commissions(), vec![Money::from("6.0 USD")]);
1354 assert_eq!(position.realized_pnl, Some(Money::from("13.0 USD")));
1355 assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1356 assert_eq!(position.total_pnl(last), Money::from("13 USD"));
1357 assert_eq!(
1358 format!("{position}"),
1359 "Position(FLAT AUD/USD.SIM, id=P-123456)"
1360 );
1361 }
1362
1363 #[rstest]
1364 fn test_pnl_calculation_from_trading_technologies_example(currency_pair_ethusdt: CurrencyPair) {
1365 let ethusdt = InstrumentAny::CurrencyPair(currency_pair_ethusdt);
1366 let quantity1 = Quantity::from(12);
1367 let price1 = Price::from("100.0");
1368 let order1 = OrderTestBuilder::new(OrderType::Market)
1369 .instrument_id(ethusdt.id())
1370 .side(OrderSide::Buy)
1371 .quantity(quantity1)
1372 .build();
1373 let commission1 = calculate_commission(ðusdt, order1.quantity(), price1, None);
1374 let fill1 = TestOrderEventStubs::filled(
1375 &order1,
1376 ðusdt,
1377 Some(TradeId::new("1")),
1378 Some(PositionId::new("P-123456")),
1379 Some(price1),
1380 None,
1381 None,
1382 Some(commission1),
1383 None,
1384 None,
1385 );
1386 let mut position = Position::new(ðusdt, fill1.into());
1387 let quantity2 = Quantity::from(17);
1388 let order2 = OrderTestBuilder::new(OrderType::Market)
1389 .instrument_id(ethusdt.id())
1390 .side(OrderSide::Buy)
1391 .quantity(quantity2)
1392 .build();
1393 let price2 = Price::from("99.0");
1394 let commission2 = calculate_commission(ðusdt, order2.quantity(), price2, None);
1395 let fill2 = TestOrderEventStubs::filled(
1396 &order2,
1397 ðusdt,
1398 Some(TradeId::new("2")),
1399 Some(PositionId::new("P-123456")),
1400 Some(price2),
1401 None,
1402 None,
1403 Some(commission2),
1404 None,
1405 None,
1406 );
1407 position.apply(&fill2.into());
1408 assert_eq!(position.quantity, Quantity::from(29));
1409 assert_eq!(position.realized_pnl, Some(Money::from("-0.28830000 USDT")));
1410 assert_eq!(position.avg_px_open, 99.413_793_103_448_27);
1411 let quantity3 = Quantity::from(9);
1412 let order3 = OrderTestBuilder::new(OrderType::Market)
1413 .instrument_id(ethusdt.id())
1414 .side(OrderSide::Sell)
1415 .quantity(quantity3)
1416 .build();
1417 let price3 = Price::from("101.0");
1418 let commission3 = calculate_commission(ðusdt, order3.quantity(), price3, None);
1419 let fill3 = TestOrderEventStubs::filled(
1420 &order3,
1421 ðusdt,
1422 Some(TradeId::new("3")),
1423 Some(PositionId::new("P-123456")),
1424 Some(price3),
1425 None,
1426 None,
1427 Some(commission3),
1428 None,
1429 None,
1430 );
1431 position.apply(&fill3.into());
1432 assert_eq!(position.quantity, Quantity::from(20));
1433 assert_eq!(position.realized_pnl, Some(Money::from("13.89666207 USDT")));
1434 assert_eq!(position.avg_px_open, 99.413_793_103_448_27);
1435 let quantity4 = Quantity::from("4");
1436 let price4 = Price::from("105.0");
1437 let order4 = OrderTestBuilder::new(OrderType::Market)
1438 .instrument_id(ethusdt.id())
1439 .side(OrderSide::Sell)
1440 .quantity(quantity4)
1441 .build();
1442 let commission4 = calculate_commission(ðusdt, order4.quantity(), price4, None);
1443 let fill4 = TestOrderEventStubs::filled(
1444 &order4,
1445 ðusdt,
1446 Some(TradeId::new("4")),
1447 Some(PositionId::new("P-123456")),
1448 Some(price4),
1449 None,
1450 None,
1451 Some(commission4),
1452 None,
1453 None,
1454 );
1455 position.apply(&fill4.into());
1456 assert_eq!(position.quantity, Quantity::from("16"));
1457 assert_eq!(position.realized_pnl, Some(Money::from("36.19948966 USDT")));
1458 assert_eq!(position.avg_px_open, 99.413_793_103_448_27);
1459 let quantity5 = Quantity::from("3");
1460 let price5 = Price::from("103.0");
1461 let order5 = OrderTestBuilder::new(OrderType::Market)
1462 .instrument_id(ethusdt.id())
1463 .side(OrderSide::Buy)
1464 .quantity(quantity5)
1465 .build();
1466 let commission5 = calculate_commission(ðusdt, order5.quantity(), price5, None);
1467 let fill5 = TestOrderEventStubs::filled(
1468 &order5,
1469 ðusdt,
1470 Some(TradeId::new("5")),
1471 Some(PositionId::new("P-123456")),
1472 Some(price5),
1473 None,
1474 None,
1475 Some(commission5),
1476 None,
1477 None,
1478 );
1479 position.apply(&fill5.into());
1480 assert_eq!(position.quantity, Quantity::from("19"));
1481 assert_eq!(position.realized_pnl, Some(Money::from("36.16858966 USDT")));
1482 assert_eq!(position.avg_px_open, 99.980_036_297_640_65);
1483 assert_eq!(
1484 format!("{position}"),
1485 "Position(LONG 19.00000 ETHUSDT.BINANCE, id=P-123456)"
1486 );
1487 }
1488
1489 #[rstest]
1490 fn test_position_closed_and_reopened(audusd_sim: CurrencyPair) {
1491 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1492 let quantity1 = Quantity::from(150_000);
1493 let price1 = Price::from("1.00001");
1494 let order = OrderTestBuilder::new(OrderType::Market)
1495 .instrument_id(audusd_sim.id())
1496 .side(OrderSide::Buy)
1497 .quantity(quantity1)
1498 .build();
1499 let commission1 = calculate_commission(&audusd_sim, quantity1, price1, None);
1500 let fill1 = TestOrderEventStubs::filled(
1501 &order,
1502 &audusd_sim,
1503 Some(TradeId::new("5")),
1504 Some(PositionId::new("P-123456")),
1505 Some(Price::from("1.00001")),
1506 None,
1507 None,
1508 Some(commission1),
1509 Some(UnixNanos::from(1_000_000_000)),
1510 None,
1511 );
1512 let mut position = Position::new(&audusd_sim, fill1.into());
1513
1514 let fill2 = OrderFilled::new(
1515 order.trader_id(),
1516 order.strategy_id(),
1517 order.instrument_id(),
1518 order.client_order_id(),
1519 VenueOrderId::from("2"),
1520 order.account_id().unwrap_or(AccountId::new("SIM-001")),
1521 TradeId::from("2"),
1522 OrderSide::Sell,
1523 OrderType::Market,
1524 order.quantity(),
1525 Price::from("1.00011"),
1526 audusd_sim.quote_currency(),
1527 LiquiditySide::Taker,
1528 uuid4(),
1529 UnixNanos::from(2_000_000_000),
1530 UnixNanos::default(),
1531 false,
1532 Some(PositionId::from("P-123456")),
1533 Some(Money::from("0 USD")),
1534 );
1535
1536 position.apply(&fill2);
1537
1538 let fill3 = OrderFilled::new(
1539 order.trader_id(),
1540 order.strategy_id(),
1541 order.instrument_id(),
1542 order.client_order_id(),
1543 VenueOrderId::from("2"),
1544 order.account_id().unwrap_or(AccountId::new("SIM-001")),
1545 TradeId::from("3"),
1546 OrderSide::Buy,
1547 OrderType::Market,
1548 order.quantity(),
1549 Price::from("1.00012"),
1550 audusd_sim.quote_currency(),
1551 LiquiditySide::Taker,
1552 uuid4(),
1553 UnixNanos::from(3_000_000_000),
1554 UnixNanos::default(),
1555 false,
1556 Some(PositionId::from("P-123456")),
1557 Some(Money::from("0 USD")),
1558 );
1559
1560 position.apply(&fill3);
1561
1562 let last = Price::from("1.0003");
1563 assert!(position.is_opposite_side(fill2.order_side));
1564 assert_eq!(position.quantity, Quantity::from(150_000));
1565 assert_eq!(position.peak_qty, Quantity::from(150_000));
1566 assert_eq!(position.side, PositionSide::Long);
1567 assert_eq!(position.opening_order_id, fill3.client_order_id);
1568 assert_eq!(position.closing_order_id, None);
1569 assert_eq!(position.closing_order_id, None);
1570 assert_eq!(position.ts_opened, 3_000_000_000);
1571 assert_eq!(position.duration_ns, 0);
1572 assert_eq!(position.avg_px_open, 1.00012);
1573 assert_eq!(position.event_count(), 1);
1574 assert_eq!(position.ts_closed, None);
1575 assert_eq!(position.avg_px_close, None);
1576 assert!(position.is_long());
1577 assert!(!position.is_short());
1578 assert!(position.is_open());
1579 assert!(!position.is_closed());
1580 assert_eq!(position.realized_return, 0.0);
1581 assert_eq!(position.realized_pnl, Some(Money::from("0 USD")));
1582 assert_eq!(position.unrealized_pnl(last), Money::from("27 USD"));
1583 assert_eq!(position.total_pnl(last), Money::from("27 USD"));
1584 assert_eq!(position.commissions(), vec![Money::from("0 USD")]);
1585 assert_eq!(
1586 format!("{position}"),
1587 "Position(LONG 150_000 AUD/USD.SIM, id=P-123456)"
1588 );
1589 }
1590
1591 #[rstest]
1592 fn test_position_realized_pnl_with_interleaved_order_sides(
1593 currency_pair_btcusdt: CurrencyPair,
1594 ) {
1595 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1596 let order1 = OrderTestBuilder::new(OrderType::Market)
1597 .instrument_id(btcusdt.id())
1598 .side(OrderSide::Buy)
1599 .quantity(Quantity::from(12))
1600 .build();
1601 let commission1 =
1602 calculate_commission(&btcusdt, order1.quantity(), Price::from("10000.0"), None);
1603 let fill1 = TestOrderEventStubs::filled(
1604 &order1,
1605 &btcusdt,
1606 Some(TradeId::from("1")),
1607 Some(PositionId::from("P-19700101-000000-001-001-1")),
1608 Some(Price::from("10000.0")),
1609 None,
1610 None,
1611 Some(commission1),
1612 None,
1613 None,
1614 );
1615 let mut position = Position::new(&btcusdt, fill1.into());
1616 let order2 = OrderTestBuilder::new(OrderType::Market)
1617 .instrument_id(btcusdt.id())
1618 .side(OrderSide::Buy)
1619 .quantity(Quantity::from(17))
1620 .build();
1621 let commission2 =
1622 calculate_commission(&btcusdt, order2.quantity(), Price::from("9999.0"), None);
1623 let fill2 = TestOrderEventStubs::filled(
1624 &order2,
1625 &btcusdt,
1626 Some(TradeId::from("2")),
1627 Some(PositionId::from("P-19700101-000000-001-001-1")),
1628 Some(Price::from("9999.0")),
1629 None,
1630 None,
1631 Some(commission2),
1632 None,
1633 None,
1634 );
1635 position.apply(&fill2.into());
1636 assert_eq!(position.quantity, Quantity::from(29));
1637 assert_eq!(
1638 position.realized_pnl,
1639 Some(Money::from("-289.98300000 USDT"))
1640 );
1641 assert_eq!(position.avg_px_open, 9_999.413_793_103_447);
1642 let order3 = OrderTestBuilder::new(OrderType::Market)
1643 .instrument_id(btcusdt.id())
1644 .side(OrderSide::Sell)
1645 .quantity(Quantity::from(9))
1646 .build();
1647 let commission3 =
1648 calculate_commission(&btcusdt, order3.quantity(), Price::from("10001.0"), None);
1649 let fill3 = TestOrderEventStubs::filled(
1650 &order3,
1651 &btcusdt,
1652 Some(TradeId::from("3")),
1653 Some(PositionId::from("P-19700101-000000-001-001-1")),
1654 Some(Price::from("10001.0")),
1655 None,
1656 None,
1657 Some(commission3),
1658 None,
1659 None,
1660 );
1661 position.apply(&fill3.into());
1662 assert_eq!(position.quantity, Quantity::from(20));
1663 assert_eq!(
1664 position.realized_pnl,
1665 Some(Money::from("-365.71613793 USDT"))
1666 );
1667 assert_eq!(position.avg_px_open, 9_999.413_793_103_447);
1668 let order4 = OrderTestBuilder::new(OrderType::Market)
1669 .instrument_id(btcusdt.id())
1670 .side(OrderSide::Buy)
1671 .quantity(Quantity::from(3))
1672 .build();
1673 let commission4 =
1674 calculate_commission(&btcusdt, order4.quantity(), Price::from("10003.0"), None);
1675 let fill4 = TestOrderEventStubs::filled(
1676 &order4,
1677 &btcusdt,
1678 Some(TradeId::from("4")),
1679 Some(PositionId::from("P-19700101-000000-001-001-1")),
1680 Some(Price::from("10003.0")),
1681 None,
1682 None,
1683 Some(commission4),
1684 None,
1685 None,
1686 );
1687 position.apply(&fill4.into());
1688 assert_eq!(position.quantity, Quantity::from(23));
1689 assert_eq!(
1690 position.realized_pnl,
1691 Some(Money::from("-395.72513793 USDT"))
1692 );
1693 assert_eq!(position.avg_px_open, 9_999.881_559_220_39);
1694 let order5 = OrderTestBuilder::new(OrderType::Market)
1695 .instrument_id(btcusdt.id())
1696 .side(OrderSide::Sell)
1697 .quantity(Quantity::from(4))
1698 .build();
1699 let commission5 =
1700 calculate_commission(&btcusdt, order5.quantity(), Price::from("10005.0"), None);
1701 let fill5 = TestOrderEventStubs::filled(
1702 &order5,
1703 &btcusdt,
1704 Some(TradeId::from("5")),
1705 Some(PositionId::from("P-19700101-000000-001-001-1")),
1706 Some(Price::from("10005.0")),
1707 None,
1708 None,
1709 Some(commission5),
1710 None,
1711 None,
1712 );
1713 position.apply(&fill5.into());
1714 assert_eq!(position.quantity, Quantity::from(19));
1715 assert_eq!(
1716 position.realized_pnl,
1717 Some(Money::from("-415.27137481 USDT"))
1718 );
1719 assert_eq!(position.avg_px_open, 9_999.881_559_220_39);
1720 assert_eq!(
1721 format!("{position}"),
1722 "Position(LONG 19.000000 BTCUSDT.BINANCE, id=P-19700101-000000-001-001-1)"
1723 );
1724 }
1725
1726 #[rstest]
1727 fn test_calculate_pnl_when_given_position_side_flat_returns_zero(
1728 currency_pair_btcusdt: CurrencyPair,
1729 ) {
1730 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1731 let order = OrderTestBuilder::new(OrderType::Market)
1732 .instrument_id(btcusdt.id())
1733 .side(OrderSide::Buy)
1734 .quantity(Quantity::from(12))
1735 .build();
1736 let fill = TestOrderEventStubs::filled(
1737 &order,
1738 &btcusdt,
1739 None,
1740 Some(PositionId::from("P-123456")),
1741 Some(Price::from("10500.0")),
1742 None,
1743 None,
1744 None,
1745 None,
1746 None,
1747 );
1748 let position = Position::new(&btcusdt, fill.into());
1749 let result = position.calculate_pnl(10500.0, 10500.0, Quantity::from("100000.0"));
1750 assert_eq!(result, Money::from("0 USDT"));
1751 }
1752
1753 #[rstest]
1754 fn test_calculate_pnl_for_long_position_win(currency_pair_btcusdt: CurrencyPair) {
1755 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1756 let order = OrderTestBuilder::new(OrderType::Market)
1757 .instrument_id(btcusdt.id())
1758 .side(OrderSide::Buy)
1759 .quantity(Quantity::from(12))
1760 .build();
1761 let commission =
1762 calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1763 let fill = TestOrderEventStubs::filled(
1764 &order,
1765 &btcusdt,
1766 None,
1767 Some(PositionId::from("P-123456")),
1768 Some(Price::from("10500.0")),
1769 None,
1770 None,
1771 Some(commission),
1772 None,
1773 None,
1774 );
1775 let position = Position::new(&btcusdt, fill.into());
1776 let pnl = position.calculate_pnl(10500.0, 10510.0, Quantity::from("12.0"));
1777 assert_eq!(pnl, Money::from("120 USDT"));
1778 assert_eq!(position.realized_pnl, Some(Money::from("-126 USDT")));
1779 assert_eq!(
1780 position.unrealized_pnl(Price::from("10510.0")),
1781 Money::from("120.0 USDT")
1782 );
1783 assert_eq!(
1784 position.total_pnl(Price::from("10510.0")),
1785 Money::from("-6 USDT")
1786 );
1787 assert_eq!(position.commissions(), vec![Money::from("126.0 USDT")]);
1788 }
1789
1790 #[rstest]
1791 fn test_calculate_pnl_for_long_position_loss(currency_pair_btcusdt: CurrencyPair) {
1792 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1793 let order = OrderTestBuilder::new(OrderType::Market)
1794 .instrument_id(btcusdt.id())
1795 .side(OrderSide::Buy)
1796 .quantity(Quantity::from(12))
1797 .build();
1798 let commission =
1799 calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1800 let fill = TestOrderEventStubs::filled(
1801 &order,
1802 &btcusdt,
1803 None,
1804 Some(PositionId::from("P-123456")),
1805 Some(Price::from("10500.0")),
1806 None,
1807 None,
1808 Some(commission),
1809 None,
1810 None,
1811 );
1812 let position = Position::new(&btcusdt, fill.into());
1813 let pnl = position.calculate_pnl(10500.0, 10480.5, Quantity::from("10.0"));
1814 assert_eq!(pnl, Money::from("-195 USDT"));
1815 assert_eq!(position.realized_pnl, Some(Money::from("-126 USDT")));
1816 assert_eq!(
1817 position.unrealized_pnl(Price::from("10480.50")),
1818 Money::from("-234.0 USDT")
1819 );
1820 assert_eq!(
1821 position.total_pnl(Price::from("10480.50")),
1822 Money::from("-360 USDT")
1823 );
1824 assert_eq!(position.commissions(), vec![Money::from("126.0 USDT")]);
1825 }
1826
1827 #[rstest]
1828 fn test_calculate_pnl_for_short_position_winning(currency_pair_btcusdt: CurrencyPair) {
1829 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1830 let order = OrderTestBuilder::new(OrderType::Market)
1831 .instrument_id(btcusdt.id())
1832 .side(OrderSide::Sell)
1833 .quantity(Quantity::from("10.15"))
1834 .build();
1835 let commission =
1836 calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1837 let fill = TestOrderEventStubs::filled(
1838 &order,
1839 &btcusdt,
1840 None,
1841 Some(PositionId::from("P-123456")),
1842 Some(Price::from("10500.0")),
1843 None,
1844 None,
1845 Some(commission),
1846 None,
1847 None,
1848 );
1849 let position = Position::new(&btcusdt, fill.into());
1850 let pnl = position.calculate_pnl(10500.0, 10390.0, Quantity::from("10.15"));
1851 assert_eq!(pnl, Money::from("1116.5 USDT"));
1852 assert_eq!(
1853 position.unrealized_pnl(Price::from("10390.0")),
1854 Money::from("1116.5 USDT")
1855 );
1856 assert_eq!(position.realized_pnl, Some(Money::from("-106.575 USDT")));
1857 assert_eq!(position.commissions(), vec![Money::from("106.575 USDT")]);
1858 assert_eq!(
1859 position.notional_value(Price::from("10390.0")),
1860 Money::from("105458.5 USDT")
1861 );
1862 }
1863
1864 #[rstest]
1865 fn test_calculate_pnl_for_short_position_loss(currency_pair_btcusdt: CurrencyPair) {
1866 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1867 let order = OrderTestBuilder::new(OrderType::Market)
1868 .instrument_id(btcusdt.id())
1869 .side(OrderSide::Sell)
1870 .quantity(Quantity::from("10.0"))
1871 .build();
1872 let commission =
1873 calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1874 let fill = TestOrderEventStubs::filled(
1875 &order,
1876 &btcusdt,
1877 None,
1878 Some(PositionId::from("P-123456")),
1879 Some(Price::from("10500.0")),
1880 None,
1881 None,
1882 Some(commission),
1883 None,
1884 None,
1885 );
1886 let position = Position::new(&btcusdt, fill.into());
1887 let pnl = position.calculate_pnl(10500.0, 10670.5, Quantity::from("10.0"));
1888 assert_eq!(pnl, Money::from("-1705 USDT"));
1889 assert_eq!(
1890 position.unrealized_pnl(Price::from("10670.5")),
1891 Money::from("-1705 USDT")
1892 );
1893 assert_eq!(position.realized_pnl, Some(Money::from("-105 USDT")));
1894 assert_eq!(position.commissions(), vec![Money::from("105 USDT")]);
1895 assert_eq!(
1896 position.notional_value(Price::from("10670.5")),
1897 Money::from("106705 USDT")
1898 );
1899 }
1900
1901 #[rstest]
1902 fn test_calculate_pnl_for_inverse1(xbtusd_bitmex: CryptoPerpetual) {
1903 let xbtusd_bitmex = InstrumentAny::CryptoPerpetual(xbtusd_bitmex);
1904 let order = OrderTestBuilder::new(OrderType::Market)
1905 .instrument_id(xbtusd_bitmex.id())
1906 .side(OrderSide::Sell)
1907 .quantity(Quantity::from("100000"))
1908 .build();
1909 let commission = calculate_commission(
1910 &xbtusd_bitmex,
1911 order.quantity(),
1912 Price::from("10000.0"),
1913 None,
1914 );
1915 let fill = TestOrderEventStubs::filled(
1916 &order,
1917 &xbtusd_bitmex,
1918 None,
1919 Some(PositionId::from("P-123456")),
1920 Some(Price::from("10000.0")),
1921 None,
1922 None,
1923 Some(commission),
1924 None,
1925 None,
1926 );
1927 let position = Position::new(&xbtusd_bitmex, fill.into());
1928 let pnl = position.calculate_pnl(10000.0, 11000.0, Quantity::from("100000.0"));
1929 assert_eq!(pnl, Money::from("-0.90909091 BTC"));
1930 assert_eq!(
1931 position.unrealized_pnl(Price::from("11000.0")),
1932 Money::from("-0.90909091 BTC")
1933 );
1934 assert_eq!(position.realized_pnl, Some(Money::from("-0.00750000 BTC")));
1935 assert_eq!(
1936 position.notional_value(Price::from("11000.0")),
1937 Money::from("9.09090909 BTC")
1938 );
1939 }
1940
1941 #[rstest]
1942 fn test_calculate_pnl_for_inverse2(ethusdt_bitmex: CryptoPerpetual) {
1943 let ethusdt_bitmex = InstrumentAny::CryptoPerpetual(ethusdt_bitmex);
1944 let order = OrderTestBuilder::new(OrderType::Market)
1945 .instrument_id(ethusdt_bitmex.id())
1946 .side(OrderSide::Sell)
1947 .quantity(Quantity::from("100000"))
1948 .build();
1949 let commission = calculate_commission(
1950 ðusdt_bitmex,
1951 order.quantity(),
1952 Price::from("375.95"),
1953 None,
1954 );
1955 let fill = TestOrderEventStubs::filled(
1956 &order,
1957 ðusdt_bitmex,
1958 None,
1959 Some(PositionId::from("P-123456")),
1960 Some(Price::from("375.95")),
1961 None,
1962 None,
1963 Some(commission),
1964 None,
1965 None,
1966 );
1967 let position = Position::new(ðusdt_bitmex, fill.into());
1968
1969 assert_eq!(
1970 position.unrealized_pnl(Price::from("370.00")),
1971 Money::from("4.27745208 ETH")
1972 );
1973 assert_eq!(
1974 position.notional_value(Price::from("370.00")),
1975 Money::from("270.27027027 ETH")
1976 );
1977 }
1978
1979 #[rstest]
1980 fn test_calculate_unrealized_pnl_for_long(currency_pair_btcusdt: CurrencyPair) {
1981 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1982 let order1 = OrderTestBuilder::new(OrderType::Market)
1983 .instrument_id(btcusdt.id())
1984 .side(OrderSide::Buy)
1985 .quantity(Quantity::from("2.000000"))
1986 .build();
1987 let order2 = OrderTestBuilder::new(OrderType::Market)
1988 .instrument_id(btcusdt.id())
1989 .side(OrderSide::Buy)
1990 .quantity(Quantity::from("2.000000"))
1991 .build();
1992 let commission1 =
1993 calculate_commission(&btcusdt, order1.quantity(), Price::from("10500.0"), None);
1994 let fill1 = TestOrderEventStubs::filled(
1995 &order1,
1996 &btcusdt,
1997 Some(TradeId::new("1")),
1998 Some(PositionId::new("P-123456")),
1999 Some(Price::from("10500.00")),
2000 None,
2001 None,
2002 Some(commission1),
2003 None,
2004 None,
2005 );
2006 let commission2 =
2007 calculate_commission(&btcusdt, order2.quantity(), Price::from("10500.0"), None);
2008 let fill2 = TestOrderEventStubs::filled(
2009 &order2,
2010 &btcusdt,
2011 Some(TradeId::new("2")),
2012 Some(PositionId::new("P-123456")),
2013 Some(Price::from("10500.00")),
2014 None,
2015 None,
2016 Some(commission2),
2017 None,
2018 None,
2019 );
2020 let mut position = Position::new(&btcusdt, fill1.into());
2021 position.apply(&fill2.into());
2022 let pnl = position.unrealized_pnl(Price::from("11505.60"));
2023 assert_eq!(pnl, Money::from("4022.40000000 USDT"));
2024 assert_eq!(
2025 position.realized_pnl,
2026 Some(Money::from("-42.00000000 USDT"))
2027 );
2028 assert_eq!(
2029 position.commissions(),
2030 vec![Money::from("42.00000000 USDT")]
2031 );
2032 }
2033
2034 #[rstest]
2035 fn test_calculate_unrealized_pnl_for_short(currency_pair_btcusdt: CurrencyPair) {
2036 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
2037 let order = OrderTestBuilder::new(OrderType::Market)
2038 .instrument_id(btcusdt.id())
2039 .side(OrderSide::Sell)
2040 .quantity(Quantity::from("5.912000"))
2041 .build();
2042 let commission =
2043 calculate_commission(&btcusdt, order.quantity(), Price::from("10505.60"), None);
2044 let fill = TestOrderEventStubs::filled(
2045 &order,
2046 &btcusdt,
2047 Some(TradeId::new("1")),
2048 Some(PositionId::new("P-123456")),
2049 Some(Price::from("10505.60")),
2050 None,
2051 None,
2052 Some(commission),
2053 None,
2054 None,
2055 );
2056 let position = Position::new(&btcusdt, fill.into());
2057 let pnl = position.unrealized_pnl(Price::from("10407.15"));
2058 assert_eq!(pnl, Money::from("582.03640000 USDT"));
2059 assert_eq!(
2060 position.realized_pnl,
2061 Some(Money::from("-62.10910720 USDT"))
2062 );
2063 assert_eq!(
2064 position.commissions(),
2065 vec![Money::from("62.10910720 USDT")]
2066 );
2067 }
2068
2069 #[rstest]
2070 fn test_calculate_unrealized_pnl_for_long_inverse(xbtusd_bitmex: CryptoPerpetual) {
2071 let xbtusd_bitmex = InstrumentAny::CryptoPerpetual(xbtusd_bitmex);
2072 let order = OrderTestBuilder::new(OrderType::Market)
2073 .instrument_id(xbtusd_bitmex.id())
2074 .side(OrderSide::Buy)
2075 .quantity(Quantity::from("100000"))
2076 .build();
2077 let commission = calculate_commission(
2078 &xbtusd_bitmex,
2079 order.quantity(),
2080 Price::from("10500.0"),
2081 None,
2082 );
2083 let fill = TestOrderEventStubs::filled(
2084 &order,
2085 &xbtusd_bitmex,
2086 Some(TradeId::new("1")),
2087 Some(PositionId::new("P-123456")),
2088 Some(Price::from("10500.00")),
2089 None,
2090 None,
2091 Some(commission),
2092 None,
2093 None,
2094 );
2095
2096 let position = Position::new(&xbtusd_bitmex, fill.into());
2097 let pnl = position.unrealized_pnl(Price::from("11505.60"));
2098 assert_eq!(pnl, Money::from("0.83238969 BTC"));
2099 assert_eq!(position.realized_pnl, Some(Money::from("-0.00714286 BTC")));
2100 assert_eq!(position.commissions(), vec![Money::from("0.00714286 BTC")]);
2101 }
2102
2103 #[rstest]
2104 fn test_calculate_unrealized_pnl_for_short_inverse(xbtusd_bitmex: CryptoPerpetual) {
2105 let xbtusd_bitmex = InstrumentAny::CryptoPerpetual(xbtusd_bitmex);
2106 let order = OrderTestBuilder::new(OrderType::Market)
2107 .instrument_id(xbtusd_bitmex.id())
2108 .side(OrderSide::Sell)
2109 .quantity(Quantity::from("1250000"))
2110 .build();
2111 let commission = calculate_commission(
2112 &xbtusd_bitmex,
2113 order.quantity(),
2114 Price::from("15500.00"),
2115 None,
2116 );
2117 let fill = TestOrderEventStubs::filled(
2118 &order,
2119 &xbtusd_bitmex,
2120 Some(TradeId::new("1")),
2121 Some(PositionId::new("P-123456")),
2122 Some(Price::from("15500.00")),
2123 None,
2124 None,
2125 Some(commission),
2126 None,
2127 None,
2128 );
2129 let position = Position::new(&xbtusd_bitmex, fill.into());
2130 let pnl = position.unrealized_pnl(Price::from("12506.65"));
2131
2132 assert_eq!(pnl, Money::from("19.30166700 BTC"));
2133 assert_eq!(position.realized_pnl, Some(Money::from("-0.06048387 BTC")));
2134 assert_eq!(position.commissions(), vec![Money::from("0.06048387 BTC")]);
2135 }
2136
2137 #[rstest]
2138 #[case(OrderSide::Buy, 25, 25.0)]
2139 #[case(OrderSide::Sell,25,-25.0)]
2140 fn test_signed_qty_decimal_qty_for_equity(
2141 #[case] order_side: OrderSide,
2142 #[case] quantity: i64,
2143 #[case] expected: f64,
2144 audusd_sim: CurrencyPair,
2145 ) {
2146 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2147 let order = OrderTestBuilder::new(OrderType::Market)
2148 .instrument_id(audusd_sim.id())
2149 .side(order_side)
2150 .quantity(Quantity::from(quantity))
2151 .build();
2152
2153 let commission =
2154 calculate_commission(&audusd_sim, order.quantity(), Price::from("1.0"), None);
2155 let fill = TestOrderEventStubs::filled(
2156 &order,
2157 &audusd_sim,
2158 None,
2159 Some(PositionId::from("P-123456")),
2160 None,
2161 None,
2162 None,
2163 Some(commission),
2164 None,
2165 None,
2166 );
2167 let position = Position::new(&audusd_sim, fill.into());
2168 assert_eq!(position.signed_qty, expected);
2169 }
2170
2171 #[rstest]
2172 fn test_position_with_commission_none(audusd_sim: CurrencyPair) {
2173 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2174 let fill = OrderFilled {
2175 position_id: Some(PositionId::from("1")),
2176 ..Default::default()
2177 };
2178
2179 let position = Position::new(&audusd_sim, fill);
2180 assert_eq!(position.realized_pnl, Some(Money::from("0 USD")));
2181 }
2182
2183 #[rstest]
2184 fn test_position_with_commission_zero(audusd_sim: CurrencyPair) {
2185 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2186 let fill = OrderFilled {
2187 position_id: Some(PositionId::from("1")),
2188 commission: Some(Money::from("0 USD")),
2189 ..Default::default()
2190 };
2191
2192 let position = Position::new(&audusd_sim, fill);
2193 assert_eq!(position.realized_pnl, Some(Money::from("0 USD")));
2194 }
2195
2196 #[rstest]
2197 fn test_cache_purge_order_events() {
2198 let audusd_sim = audusd_sim();
2199 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2200
2201 let order1 = OrderTestBuilder::new(OrderType::Market)
2202 .client_order_id(ClientOrderId::new("O-1"))
2203 .instrument_id(audusd_sim.id())
2204 .side(OrderSide::Buy)
2205 .quantity(Quantity::from(50_000))
2206 .build();
2207
2208 let order2 = OrderTestBuilder::new(OrderType::Market)
2209 .client_order_id(ClientOrderId::new("O-2"))
2210 .instrument_id(audusd_sim.id())
2211 .side(OrderSide::Buy)
2212 .quantity(Quantity::from(50_000))
2213 .build();
2214
2215 let position_id = PositionId::new("P-123456");
2216
2217 let fill1 = TestOrderEventStubs::filled(
2218 &order1,
2219 &audusd_sim,
2220 Some(TradeId::new("1")),
2221 Some(position_id),
2222 Some(Price::from("1.00001")),
2223 None,
2224 None,
2225 None,
2226 None,
2227 None,
2228 );
2229
2230 let mut position = Position::new(&audusd_sim, fill1.into());
2231
2232 let fill2 = TestOrderEventStubs::filled(
2233 &order2,
2234 &audusd_sim,
2235 Some(TradeId::new("2")),
2236 Some(position_id),
2237 Some(Price::from("1.00002")),
2238 None,
2239 None,
2240 None,
2241 None,
2242 None,
2243 );
2244
2245 position.apply(&fill2.into());
2246 position.purge_events_for_order(order1.client_order_id());
2247
2248 assert_eq!(position.events.len(), 1);
2249 assert_eq!(position.trade_ids.len(), 1);
2250 assert_eq!(position.events[0].client_order_id, order2.client_order_id());
2251 assert_eq!(position.trade_ids[0], TradeId::new("2"));
2252 }
2253
2254 #[rstest]
2255 fn test_purge_all_events_returns_none_for_last_event_and_trade_id() {
2256 let audusd_sim = audusd_sim();
2257 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2258
2259 let order = OrderTestBuilder::new(OrderType::Market)
2260 .client_order_id(ClientOrderId::new("O-1"))
2261 .instrument_id(audusd_sim.id())
2262 .side(OrderSide::Buy)
2263 .quantity(Quantity::from(100_000))
2264 .build();
2265
2266 let position_id = PositionId::new("P-123456");
2267 let fill = TestOrderEventStubs::filled(
2268 &order,
2269 &audusd_sim,
2270 Some(TradeId::new("1")),
2271 Some(position_id),
2272 Some(Price::from("1.00050")),
2273 None,
2274 None,
2275 None,
2276 Some(UnixNanos::from(1_000_000_000)), None,
2278 );
2279
2280 let mut position = Position::new(&audusd_sim, fill.into());
2281
2282 assert_eq!(position.events.len(), 1);
2283 assert!(position.last_event().is_some());
2284 assert!(position.last_trade_id().is_some());
2285
2286 let original_ts_opened = position.ts_opened;
2288 let original_ts_last = position.ts_last;
2289 assert_ne!(original_ts_opened, UnixNanos::default());
2290 assert_ne!(original_ts_last, UnixNanos::default());
2291
2292 position.purge_events_for_order(order.client_order_id());
2293
2294 assert_eq!(position.events.len(), 0);
2295 assert_eq!(position.trade_ids.len(), 0);
2296 assert!(position.last_event().is_none());
2297 assert!(position.last_trade_id().is_none());
2298
2299 assert_eq!(position.ts_opened, UnixNanos::default());
2302 assert_eq!(position.ts_last, UnixNanos::default());
2303 assert_eq!(position.ts_closed, Some(UnixNanos::default()));
2304 assert_eq!(position.duration_ns, 0);
2305
2306 assert!(position.is_closed());
2309 assert!(!position.is_open());
2310 assert_eq!(position.side, PositionSide::Flat);
2311 }
2312
2313 #[rstest]
2314 fn test_revive_from_empty_shell(audusd_sim: CurrencyPair) {
2315 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2317
2318 let order1 = OrderTestBuilder::new(OrderType::Market)
2320 .instrument_id(audusd_sim.id())
2321 .side(OrderSide::Buy)
2322 .quantity(Quantity::from(100_000))
2323 .build();
2324
2325 let fill1 = TestOrderEventStubs::filled(
2326 &order1,
2327 &audusd_sim,
2328 None,
2329 Some(PositionId::new("P-1")),
2330 Some(Price::from("1.00000")),
2331 None,
2332 None,
2333 None,
2334 Some(UnixNanos::from(1_000_000_000)),
2335 None,
2336 );
2337
2338 let mut position = Position::new(&audusd_sim, fill1.into());
2339 position.purge_events_for_order(order1.client_order_id());
2340
2341 assert!(position.is_closed());
2343 assert_eq!(position.ts_closed, Some(UnixNanos::default()));
2344 assert_eq!(position.event_count(), 0);
2345
2346 let order2 = OrderTestBuilder::new(OrderType::Market)
2348 .instrument_id(audusd_sim.id())
2349 .side(OrderSide::Buy)
2350 .quantity(Quantity::from(50_000))
2351 .build();
2352
2353 let fill2 = TestOrderEventStubs::filled(
2354 &order2,
2355 &audusd_sim,
2356 None,
2357 Some(PositionId::new("P-1")),
2358 Some(Price::from("1.00020")),
2359 None,
2360 None,
2361 None,
2362 Some(UnixNanos::from(3_000_000_000)),
2363 None,
2364 );
2365
2366 let fill2_typed: OrderFilled = fill2.clone().into();
2367 position.apply(&fill2_typed);
2368
2369 assert!(position.is_long());
2371 assert!(!position.is_closed());
2372 assert!(position.ts_closed.is_none());
2373 assert_eq!(position.ts_opened, fill2.ts_event());
2374 assert_eq!(position.ts_last, fill2.ts_event());
2375 assert_eq!(position.event_count(), 1);
2376 assert_eq!(position.quantity, Quantity::from(50_000));
2377 }
2378
2379 #[rstest]
2380 fn test_empty_shell_position_invariants(audusd_sim: CurrencyPair) {
2381 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2383
2384 let order = OrderTestBuilder::new(OrderType::Market)
2385 .instrument_id(audusd_sim.id())
2386 .side(OrderSide::Buy)
2387 .quantity(Quantity::from(100_000))
2388 .build();
2389
2390 let fill = TestOrderEventStubs::filled(
2391 &order,
2392 &audusd_sim,
2393 None,
2394 Some(PositionId::new("P-1")),
2395 Some(Price::from("1.00000")),
2396 None,
2397 None,
2398 None,
2399 Some(UnixNanos::from(1_000_000_000)),
2400 None,
2401 );
2402
2403 let mut position = Position::new(&audusd_sim, fill.into());
2404 position.purge_events_for_order(order.client_order_id());
2405
2406 assert_eq!(
2408 position.event_count(),
2409 0,
2410 "Precondition: event_count must be 0"
2411 );
2412
2413 assert!(
2415 position.is_closed(),
2416 "INV1: Empty shell must report is_closed() == true"
2417 );
2418 assert!(
2419 !position.is_open(),
2420 "INV1: Empty shell must report is_open() == false"
2421 );
2422
2423 assert_eq!(
2425 position.side,
2426 PositionSide::Flat,
2427 "INV2: Empty shell must be FLAT"
2428 );
2429
2430 assert!(
2432 position.ts_closed.is_some(),
2433 "INV3: Empty shell must have ts_closed.is_some()"
2434 );
2435 assert_eq!(
2436 position.ts_closed,
2437 Some(UnixNanos::default()),
2438 "INV3: Empty shell ts_closed must be 0"
2439 );
2440
2441 assert_eq!(
2443 position.ts_opened,
2444 UnixNanos::default(),
2445 "INV4: Empty shell ts_opened must be 0"
2446 );
2447 assert_eq!(
2448 position.ts_last,
2449 UnixNanos::default(),
2450 "INV4: Empty shell ts_last must be 0"
2451 );
2452 assert_eq!(
2453 position.duration_ns, 0,
2454 "INV4: Empty shell duration_ns must be 0"
2455 );
2456
2457 assert_eq!(
2459 position.quantity,
2460 Quantity::zero(audusd_sim.size_precision()),
2461 "INV5: Empty shell quantity must be 0"
2462 );
2463
2464 assert!(
2466 position.events.is_empty(),
2467 "INV6: Empty shell must have no events"
2468 );
2469 assert!(
2470 position.trade_ids.is_empty(),
2471 "INV6: Empty shell must have no trade IDs"
2472 );
2473 assert!(
2474 position.last_event().is_none(),
2475 "INV6: Empty shell must have no last event"
2476 );
2477 assert!(
2478 position.last_trade_id().is_none(),
2479 "INV6: Empty shell must have no last trade ID"
2480 );
2481 }
2482
2483 #[rstest]
2484 fn test_position_pnl_precision_with_very_small_amounts(audusd_sim: CurrencyPair) {
2485 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2488 let order = OrderTestBuilder::new(OrderType::Market)
2489 .instrument_id(audusd_sim.id())
2490 .side(OrderSide::Buy)
2491 .quantity(Quantity::from(100))
2492 .build();
2493
2494 let small_commission = Money::new(0.01, Currency::USD());
2496 let fill = TestOrderEventStubs::filled(
2497 &order,
2498 &audusd_sim,
2499 None,
2500 None,
2501 Some(Price::from("1.00001")),
2502 Some(Quantity::from(100)),
2503 None,
2504 Some(small_commission),
2505 None,
2506 None,
2507 );
2508
2509 let position = Position::new(&audusd_sim, fill.into());
2510
2511 assert_eq!(position.commissions().len(), 1);
2513 let recorded_commission = position.commissions()[0];
2514 assert!(
2515 recorded_commission.as_f64() > 0.0,
2516 "Commission of 0.01 should be preserved"
2517 );
2518
2519 let realized = position.realized_pnl.unwrap().as_f64();
2521 assert!(
2522 realized < 0.0,
2523 "Realized PnL should be negative due to commission"
2524 );
2525 }
2526
2527 #[rstest]
2528 fn test_position_pnl_precision_with_high_precision_instrument() {
2529 use crate::instruments::stubs::crypto_perpetual_ethusdt;
2531 let ethusdt = crypto_perpetual_ethusdt();
2532 let ethusdt = InstrumentAny::CryptoPerpetual(ethusdt);
2533
2534 let size_precision = ethusdt.size_precision();
2536
2537 let order = OrderTestBuilder::new(OrderType::Market)
2538 .instrument_id(ethusdt.id())
2539 .side(OrderSide::Buy)
2540 .quantity(Quantity::from("1.123456789"))
2541 .build();
2542
2543 let fill = TestOrderEventStubs::filled(
2544 &order,
2545 ðusdt,
2546 None,
2547 None,
2548 Some(Price::from("2345.123456789")),
2549 Some(Quantity::from("1.123456789")),
2550 None,
2551 Some(Money::from("0.1 USDT")),
2552 None,
2553 None,
2554 );
2555
2556 let position = Position::new(ðusdt, fill.into());
2557
2558 let avg_px = position.avg_px_open;
2560 assert!(
2561 (avg_px - 2345.123456789).abs() < 1e-6,
2562 "High precision price should be preserved within f64 tolerance"
2563 );
2564
2565 assert_eq!(
2568 position.quantity.precision, size_precision,
2569 "Quantity precision should match instrument"
2570 );
2571
2572 let qty_f64 = position.quantity.as_f64();
2574 assert!(
2575 qty_f64 > 1.0 && qty_f64 < 2.0,
2576 "Quantity should be in expected range"
2577 );
2578 }
2579
2580 #[rstest]
2581 fn test_position_pnl_accumulation_across_many_fills(audusd_sim: CurrencyPair) {
2582 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2584 let order = OrderTestBuilder::new(OrderType::Market)
2585 .instrument_id(audusd_sim.id())
2586 .side(OrderSide::Buy)
2587 .quantity(Quantity::from(1000))
2588 .build();
2589
2590 let initial_fill = TestOrderEventStubs::filled(
2591 &order,
2592 &audusd_sim,
2593 Some(TradeId::new("1")),
2594 None,
2595 Some(Price::from("1.00000")),
2596 Some(Quantity::from(10)),
2597 None,
2598 Some(Money::from("0.01 USD")),
2599 None,
2600 None,
2601 );
2602
2603 let mut position = Position::new(&audusd_sim, initial_fill.into());
2604
2605 for i in 2..=100 {
2607 let price_offset = (i as f64) * 0.00001;
2608 let fill = TestOrderEventStubs::filled(
2609 &order,
2610 &audusd_sim,
2611 Some(TradeId::new(i.to_string())),
2612 None,
2613 Some(Price::from(&format!("{:.5}", 1.0 + price_offset))),
2614 Some(Quantity::from(10)),
2615 None,
2616 Some(Money::from("0.01 USD")),
2617 None,
2618 None,
2619 );
2620 position.apply(&fill.into());
2621 }
2622
2623 assert_eq!(position.events.len(), 100);
2625 assert_eq!(position.quantity, Quantity::from(1000));
2626
2627 let total_commission: f64 = position.commissions().iter().map(|c| c.as_f64()).sum();
2629 assert!(
2630 (total_commission - 1.0).abs() < 1e-10,
2631 "Commission accumulation should be accurate: expected 1.0, got {}",
2632 total_commission
2633 );
2634
2635 let avg_px = position.avg_px_open;
2637 assert!(
2638 avg_px > 1.0 && avg_px < 1.001,
2639 "Average price should be reasonable: got {}",
2640 avg_px
2641 );
2642 }
2643
2644 #[rstest]
2645 fn test_position_pnl_with_extreme_price_values(audusd_sim: CurrencyPair) {
2646 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2648
2649 let order_small = OrderTestBuilder::new(OrderType::Market)
2651 .instrument_id(audusd_sim.id())
2652 .side(OrderSide::Buy)
2653 .quantity(Quantity::from(100_000))
2654 .build();
2655
2656 let fill_small = TestOrderEventStubs::filled(
2657 &order_small,
2658 &audusd_sim,
2659 None,
2660 None,
2661 Some(Price::from("0.00001")),
2662 Some(Quantity::from(100_000)),
2663 None,
2664 None,
2665 None,
2666 None,
2667 );
2668
2669 let position_small = Position::new(&audusd_sim, fill_small.into());
2670 assert_eq!(position_small.avg_px_open, 0.00001);
2671
2672 let last_price_small = Price::from("0.00002");
2674 let unrealized = position_small.unrealized_pnl(last_price_small);
2675 assert!(
2676 unrealized.as_f64() > 0.0,
2677 "Unrealized PnL should be positive when price doubles"
2678 );
2679
2680 let order_large = OrderTestBuilder::new(OrderType::Market)
2682 .instrument_id(audusd_sim.id())
2683 .side(OrderSide::Buy)
2684 .quantity(Quantity::from(100))
2685 .build();
2686
2687 let fill_large = TestOrderEventStubs::filled(
2688 &order_large,
2689 &audusd_sim,
2690 None,
2691 None,
2692 Some(Price::from("99999.99999")),
2693 Some(Quantity::from(100)),
2694 None,
2695 None,
2696 None,
2697 None,
2698 );
2699
2700 let position_large = Position::new(&audusd_sim, fill_large.into());
2701 assert!(
2702 (position_large.avg_px_open - 99999.99999).abs() < 1e-6,
2703 "Large price should be preserved within f64 tolerance"
2704 );
2705 }
2706
2707 #[rstest]
2708 fn test_position_pnl_roundtrip_precision(audusd_sim: CurrencyPair) {
2709 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2711 let buy_order = OrderTestBuilder::new(OrderType::Market)
2712 .instrument_id(audusd_sim.id())
2713 .side(OrderSide::Buy)
2714 .quantity(Quantity::from(100_000))
2715 .build();
2716
2717 let sell_order = OrderTestBuilder::new(OrderType::Market)
2718 .instrument_id(audusd_sim.id())
2719 .side(OrderSide::Sell)
2720 .quantity(Quantity::from(100_000))
2721 .build();
2722
2723 let open_fill = TestOrderEventStubs::filled(
2725 &buy_order,
2726 &audusd_sim,
2727 Some(TradeId::new("1")),
2728 None,
2729 Some(Price::from("1.123456")),
2730 None,
2731 None,
2732 Some(Money::from("0.50 USD")),
2733 None,
2734 None,
2735 );
2736
2737 let mut position = Position::new(&audusd_sim, open_fill.into());
2738
2739 let close_fill = TestOrderEventStubs::filled(
2741 &sell_order,
2742 &audusd_sim,
2743 Some(TradeId::new("2")),
2744 None,
2745 Some(Price::from("1.123456")),
2746 None,
2747 None,
2748 Some(Money::from("0.50 USD")),
2749 None,
2750 None,
2751 );
2752
2753 position.apply(&close_fill.into());
2754
2755 assert!(position.is_closed());
2757
2758 let realized = position.realized_pnl.unwrap().as_f64();
2760 assert!(
2761 (realized - (-1.0)).abs() < 1e-10,
2762 "Realized PnL should be exactly -1.0 USD (commissions), got {}",
2763 realized
2764 );
2765 }
2766}