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 = "posei_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) {
149 let mut filtered_events = Vec::new();
151 let mut filtered_trade_ids = Vec::new();
152
153 for event in &self.events {
154 if event.client_order_id != client_order_id {
155 filtered_events.push(*event);
156 filtered_trade_ids.push(event.trade_id);
157 }
158 }
159
160 self.events = filtered_events;
161 self.trade_ids = filtered_trade_ids;
162 }
163
164 pub fn apply(&mut self, fill: &OrderFilled) {
170 check_predicate_true(
171 !self.trade_ids.contains(&fill.trade_id),
172 "`fill.trade_id` already contained in `trade_ids",
173 )
174 .expect(FAILED);
175 check_predicate_true(fill.ts_event >= self.ts_opened, "fill.ts_event < ts_opened")
176 .expect(FAILED);
177
178 if self.side == PositionSide::Flat {
179 self.events.clear();
181 self.trade_ids.clear();
182 self.buy_qty = Quantity::zero(self.size_precision);
183 self.sell_qty = Quantity::zero(self.size_precision);
184 self.commissions.clear();
185 self.opening_order_id = fill.client_order_id;
186 self.closing_order_id = None;
187 self.peak_qty = Quantity::zero(self.size_precision);
188 self.ts_init = fill.ts_init;
189 self.ts_opened = fill.ts_event;
190 self.ts_closed = None;
191 self.duration_ns = 0;
192 self.avg_px_open = fill.last_px.as_f64();
193 self.avg_px_close = None;
194 self.realized_return = 0.0;
195 self.realized_pnl = None;
196 }
197
198 self.events.push(*fill);
199 self.trade_ids.push(fill.trade_id);
200
201 if let Some(commission) = fill.commission {
203 let commission_currency = commission.currency;
204 if let Some(existing_commission) = self.commissions.get_mut(&commission_currency) {
205 *existing_commission += commission;
206 } else {
207 self.commissions.insert(commission_currency, commission);
208 }
209 }
210
211 match fill.specified_side() {
213 OrderSideSpecified::Buy => {
214 self.handle_buy_order_fill(fill);
215 }
216 OrderSideSpecified::Sell => {
217 self.handle_sell_order_fill(fill);
218 }
219 }
220
221 self.quantity = Quantity::new(self.signed_qty.abs(), self.size_precision);
224 if self.quantity > self.peak_qty {
225 self.peak_qty.raw = self.quantity.raw;
226 }
227
228 if self.signed_qty > 0.0 {
230 self.entry = OrderSide::Buy;
231 self.side = PositionSide::Long;
232 } else if self.signed_qty < 0.0 {
233 self.entry = OrderSide::Sell;
234 self.side = PositionSide::Short;
235 } else {
236 self.side = PositionSide::Flat;
237 self.closing_order_id = Some(fill.client_order_id);
238 self.ts_closed = Some(fill.ts_event);
239 self.duration_ns = if let Some(ts_closed) = self.ts_closed {
240 ts_closed.as_u64() - self.ts_opened.as_u64()
241 } else {
242 0
243 };
244 }
245
246 self.ts_last = fill.ts_event;
247 }
248
249 fn handle_buy_order_fill(&mut self, fill: &OrderFilled) {
250 let mut realized_pnl = if let Some(commission) = fill.commission {
252 if commission.currency == self.settlement_currency {
253 -commission.as_f64()
254 } else {
255 0.0
256 }
257 } else {
258 0.0
259 };
260
261 let last_px = fill.last_px.as_f64();
262 let last_qty = fill.last_qty.as_f64();
263 let last_qty_object = fill.last_qty;
264
265 if self.signed_qty > 0.0 {
266 self.avg_px_open = self.calculate_avg_px_open_px(last_px, last_qty);
267 } else if self.signed_qty < 0.0 {
268 let avg_px_close = self.calculate_avg_px_close_px(last_px, last_qty);
270 self.avg_px_close = Some(avg_px_close);
271 self.realized_return = self.calculate_return(self.avg_px_open, avg_px_close);
272 realized_pnl += self.calculate_pnl_raw(self.avg_px_open, last_px, last_qty);
273 }
274
275 if self.realized_pnl.is_none() {
276 self.realized_pnl = Some(Money::new(realized_pnl, self.settlement_currency));
277 } else {
278 self.realized_pnl = Some(Money::new(
279 self.realized_pnl.unwrap().as_f64() + realized_pnl,
280 self.settlement_currency,
281 ));
282 }
283
284 self.signed_qty += last_qty;
285 self.buy_qty += last_qty_object;
286 }
287
288 fn handle_sell_order_fill(&mut self, fill: &OrderFilled) {
289 let mut realized_pnl = if let Some(commission) = fill.commission {
291 if commission.currency == self.settlement_currency {
292 -commission.as_f64()
293 } else {
294 0.0
295 }
296 } else {
297 0.0
298 };
299
300 let last_px = fill.last_px.as_f64();
301 let last_qty = fill.last_qty.as_f64();
302 let last_qty_object = fill.last_qty;
303
304 if self.signed_qty < 0.0 {
305 self.avg_px_open = self.calculate_avg_px_open_px(last_px, last_qty);
306 } else if self.signed_qty > 0.0 {
307 let avg_px_close = self.calculate_avg_px_close_px(last_px, last_qty);
308 self.avg_px_close = Some(avg_px_close);
309 self.realized_return = self.calculate_return(self.avg_px_open, avg_px_close);
310 realized_pnl += self.calculate_pnl_raw(self.avg_px_open, last_px, last_qty);
311 }
312
313 if self.realized_pnl.is_none() {
314 self.realized_pnl = Some(Money::new(realized_pnl, self.settlement_currency));
315 } else {
316 self.realized_pnl = Some(Money::new(
317 self.realized_pnl.unwrap().as_f64() + realized_pnl,
318 self.settlement_currency,
319 ));
320 }
321
322 self.signed_qty -= last_qty;
323 self.sell_qty += last_qty_object;
324 }
325
326 #[must_use]
350 fn calculate_avg_px(&self, qty: f64, avg_pg: f64, last_px: f64, last_qty: f64) -> f64 {
351 if qty == 0.0 && last_qty == 0.0 {
353 panic!("Cannot calculate average price: both quantities are zero");
354 }
355
356 if last_qty == 0.0 {
358 panic!("Cannot calculate average price: fill quantity is zero");
359 }
360
361 if qty == 0.0 {
363 return last_px;
364 }
365
366 let start_cost = avg_pg * qty;
367 let event_cost = last_px * last_qty;
368 let total_qty = qty + last_qty;
369
370 debug_assert!(
372 total_qty > 0.0,
373 "Total quantity unexpectedly zero in average price calculation"
374 );
375
376 (start_cost + event_cost) / total_qty
377 }
378
379 #[must_use]
380 fn calculate_avg_px_open_px(&self, last_px: f64, last_qty: f64) -> f64 {
381 self.calculate_avg_px(self.quantity.as_f64(), self.avg_px_open, last_px, last_qty)
382 }
383
384 #[must_use]
385 fn calculate_avg_px_close_px(&self, last_px: f64, last_qty: f64) -> f64 {
386 if self.avg_px_close.is_none() {
387 return last_px;
388 }
389 let closing_qty = if self.side == PositionSide::Long {
390 self.sell_qty
391 } else {
392 self.buy_qty
393 };
394 self.calculate_avg_px(
395 closing_qty.as_f64(),
396 self.avg_px_close.unwrap(),
397 last_px,
398 last_qty,
399 )
400 }
401
402 fn calculate_points(&self, avg_px_open: f64, avg_px_close: f64) -> f64 {
403 match self.side {
404 PositionSide::Long => avg_px_close - avg_px_open,
405 PositionSide::Short => avg_px_open - avg_px_close,
406 _ => 0.0, }
408 }
409
410 fn calculate_points_inverse(&self, avg_px_open: f64, avg_px_close: f64) -> f64 {
411 if avg_px_open == 0.0 {
413 panic!("Cannot calculate inverse points: open price is zero");
414 }
415 if avg_px_close == 0.0 {
416 panic!("Cannot calculate inverse points: close price is zero");
417 }
418
419 let inverse_open = 1.0 / avg_px_open;
420 let inverse_close = 1.0 / avg_px_close;
421 match self.side {
422 PositionSide::Long => inverse_open - inverse_close,
423 PositionSide::Short => inverse_close - inverse_open,
424 _ => 0.0, }
426 }
427
428 #[must_use]
429 fn calculate_return(&self, avg_px_open: f64, avg_px_close: f64) -> f64 {
430 self.calculate_points(avg_px_open, avg_px_close) / avg_px_open
431 }
432
433 fn calculate_pnl_raw(&self, avg_px_open: f64, avg_px_close: f64, quantity: f64) -> f64 {
434 let quantity = quantity.min(self.signed_qty.abs());
435 if self.is_inverse {
436 quantity
437 * self.multiplier.as_f64()
438 * self.calculate_points_inverse(avg_px_open, avg_px_close)
439 } else {
440 quantity * self.multiplier.as_f64() * self.calculate_points(avg_px_open, avg_px_close)
441 }
442 }
443
444 #[must_use]
445 pub fn calculate_pnl(&self, avg_px_open: f64, avg_px_close: f64, quantity: Quantity) -> Money {
446 let pnl_raw = self.calculate_pnl_raw(avg_px_open, avg_px_close, quantity.as_f64());
447 Money::new(pnl_raw, self.settlement_currency)
448 }
449
450 #[must_use]
451 pub fn total_pnl(&self, last: Price) -> Money {
452 let realized_pnl = self.realized_pnl.map_or(0.0, |pnl| pnl.as_f64());
453 Money::new(
454 realized_pnl + self.unrealized_pnl(last).as_f64(),
455 self.settlement_currency,
456 )
457 }
458
459 #[must_use]
460 pub fn unrealized_pnl(&self, last: Price) -> Money {
461 if self.side == PositionSide::Flat {
462 Money::new(0.0, self.settlement_currency)
463 } else {
464 let avg_px_open = self.avg_px_open;
465 let avg_px_close = last.as_f64();
466 let quantity = self.quantity.as_f64();
467 let pnl = self.calculate_pnl_raw(avg_px_open, avg_px_close, quantity);
468 Money::new(pnl, self.settlement_currency)
469 }
470 }
471
472 pub fn closing_order_side(&self) -> OrderSide {
473 match self.side {
474 PositionSide::Long => OrderSide::Sell,
475 PositionSide::Short => OrderSide::Buy,
476 _ => OrderSide::NoOrderSide,
477 }
478 }
479
480 #[must_use]
481 pub fn is_opposite_side(&self, side: OrderSide) -> bool {
482 self.entry != side
483 }
484
485 #[must_use]
486 pub fn symbol(&self) -> Symbol {
487 self.instrument_id.symbol
488 }
489
490 #[must_use]
491 pub fn venue(&self) -> Venue {
492 self.instrument_id.venue
493 }
494
495 #[must_use]
496 pub fn event_count(&self) -> usize {
497 self.events.len()
498 }
499
500 #[must_use]
501 pub fn client_order_ids(&self) -> Vec<ClientOrderId> {
502 let mut result = self
504 .events
505 .iter()
506 .map(|event| event.client_order_id)
507 .collect::<HashSet<ClientOrderId>>()
508 .into_iter()
509 .collect::<Vec<ClientOrderId>>();
510 result.sort_unstable();
511 result
512 }
513
514 #[must_use]
515 pub fn venue_order_ids(&self) -> Vec<VenueOrderId> {
516 let mut result = self
518 .events
519 .iter()
520 .map(|event| event.venue_order_id)
521 .collect::<HashSet<VenueOrderId>>()
522 .into_iter()
523 .collect::<Vec<VenueOrderId>>();
524 result.sort_unstable();
525 result
526 }
527
528 #[must_use]
529 pub fn trade_ids(&self) -> Vec<TradeId> {
530 let mut result = self
531 .events
532 .iter()
533 .map(|event| event.trade_id)
534 .collect::<HashSet<TradeId>>()
535 .into_iter()
536 .collect::<Vec<TradeId>>();
537 result.sort_unstable();
538 result
539 }
540
541 #[must_use]
547 pub fn notional_value(&self, last: Price) -> Money {
548 if self.is_inverse {
549 Money::new(
550 self.quantity.as_f64() * self.multiplier.as_f64() * (1.0 / last.as_f64()),
551 self.base_currency.unwrap(),
552 )
553 } else {
554 Money::new(
555 self.quantity.as_f64() * last.as_f64() * self.multiplier.as_f64(),
556 self.quote_currency,
557 )
558 }
559 }
560
561 #[must_use]
563 pub fn last_event(&self) -> Option<OrderFilled> {
564 self.events.last().copied()
565 }
566
567 #[must_use]
568 pub fn last_trade_id(&self) -> Option<TradeId> {
569 self.trade_ids.last().copied()
570 }
571
572 #[must_use]
573 pub fn is_long(&self) -> bool {
574 self.side == PositionSide::Long
575 }
576
577 #[must_use]
578 pub fn is_short(&self) -> bool {
579 self.side == PositionSide::Short
580 }
581
582 #[must_use]
583 pub fn is_open(&self) -> bool {
584 self.side != PositionSide::Flat && self.ts_closed.is_none()
585 }
586
587 #[must_use]
588 pub fn is_closed(&self) -> bool {
589 self.side == PositionSide::Flat && self.ts_closed.is_some()
590 }
591
592 #[must_use]
593 pub fn commissions(&self) -> Vec<Money> {
594 self.commissions.values().copied().collect()
595 }
596}
597
598impl PartialEq<Self> for Position {
599 fn eq(&self, other: &Self) -> bool {
600 self.id == other.id
601 }
602}
603
604impl Eq for Position {}
605
606impl Hash for Position {
607 fn hash<H: Hasher>(&self, state: &mut H) {
608 self.id.hash(state);
609 }
610}
611
612impl Display for Position {
613 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
614 let quantity_str = if self.quantity != Quantity::zero(self.price_precision) {
615 self.quantity.to_formatted_string() + " "
616 } else {
617 String::new()
618 };
619 write!(
620 f,
621 "Position({} {}{}, id={})",
622 self.side, quantity_str, self.instrument_id, self.id
623 )
624 }
625}
626
627#[cfg(test)]
631mod tests {
632 use std::str::FromStr;
633
634 use nautilus_core::UnixNanos;
635 use rstest::rstest;
636
637 use crate::{
638 enums::{LiquiditySide, OrderSide, OrderType, PositionSide},
639 events::OrderFilled,
640 identifiers::{
641 AccountId, ClientOrderId, PositionId, StrategyId, TradeId, VenueOrderId, stubs::uuid4,
642 },
643 instruments::{CryptoPerpetual, CurrencyPair, Instrument, InstrumentAny, stubs::*},
644 orders::{Order, builder::OrderTestBuilder, stubs::TestOrderEventStubs},
645 position::Position,
646 stubs::*,
647 types::{Money, Price, Quantity},
648 };
649
650 #[rstest]
651 fn test_position_long_display(stub_position_long: Position) {
652 let display = format!("{stub_position_long}");
653 assert_eq!(display, "Position(LONG 1 AUD/USD.SIM, id=1)");
654 }
655
656 #[rstest]
657 fn test_position_short_display(stub_position_short: Position) {
658 let display = format!("{stub_position_short}");
659 assert_eq!(display, "Position(SHORT 1 AUD/USD.SIM, id=1)");
660 }
661
662 #[rstest]
663 #[should_panic(expected = "`fill.trade_id` already contained in `trade_ids")]
664 fn test_two_trades_with_same_trade_id_error(audusd_sim: CurrencyPair) {
665 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
666 let order1 = OrderTestBuilder::new(OrderType::Market)
667 .instrument_id(audusd_sim.id())
668 .side(OrderSide::Buy)
669 .quantity(Quantity::from(100_000))
670 .build();
671 let order2 = OrderTestBuilder::new(OrderType::Market)
672 .instrument_id(audusd_sim.id())
673 .side(OrderSide::Buy)
674 .quantity(Quantity::from(100_000))
675 .build();
676 let fill1 = TestOrderEventStubs::filled(
677 &order1,
678 &audusd_sim,
679 Some(TradeId::new("1")),
680 None,
681 Some(Price::from("1.00001")),
682 None,
683 None,
684 None,
685 None,
686 None,
687 );
688 let fill2 = TestOrderEventStubs::filled(
689 &order2,
690 &audusd_sim,
691 Some(TradeId::new("1")),
692 None,
693 Some(Price::from("1.00002")),
694 None,
695 None,
696 None,
697 None,
698 None,
699 );
700 let mut position = Position::new(&audusd_sim, fill1.into());
701 position.apply(&fill2.into());
702 }
703
704 #[rstest]
705 fn test_position_filled_with_buy_order(audusd_sim: CurrencyPair) {
706 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
707 let order = OrderTestBuilder::new(OrderType::Market)
708 .instrument_id(audusd_sim.id())
709 .side(OrderSide::Buy)
710 .quantity(Quantity::from(100_000))
711 .build();
712 let fill = TestOrderEventStubs::filled(
713 &order,
714 &audusd_sim,
715 None,
716 None,
717 Some(Price::from("1.00001")),
718 None,
719 None,
720 None,
721 None,
722 None,
723 );
724 let last_price = Price::from_str("1.0005").unwrap();
725 let position = Position::new(&audusd_sim, fill.into());
726 assert_eq!(position.symbol(), audusd_sim.id().symbol);
727 assert_eq!(position.venue(), audusd_sim.id().venue);
728 assert_eq!(position.closing_order_side(), OrderSide::Sell);
729 assert!(!position.is_opposite_side(OrderSide::Buy));
730 assert_eq!(position, position); assert!(position.closing_order_id.is_none());
732 assert_eq!(position.quantity, Quantity::from(100_000));
733 assert_eq!(position.peak_qty, Quantity::from(100_000));
734 assert_eq!(position.size_precision, 0);
735 assert_eq!(position.signed_qty, 100_000.0);
736 assert_eq!(position.entry, OrderSide::Buy);
737 assert_eq!(position.side, PositionSide::Long);
738 assert_eq!(position.ts_opened.as_u64(), 0);
739 assert_eq!(position.duration_ns, 0);
740 assert_eq!(position.avg_px_open, 1.00001);
741 assert_eq!(position.event_count(), 1);
742 assert_eq!(position.id, PositionId::new("1"));
743 assert_eq!(position.events.len(), 1);
744 assert!(position.is_long());
745 assert!(!position.is_short());
746 assert!(position.is_open());
747 assert!(!position.is_closed());
748 assert_eq!(position.realized_return, 0.0);
749 assert_eq!(position.realized_pnl, Some(Money::from("-2.0 USD")));
750 assert_eq!(position.unrealized_pnl(last_price), Money::from("49.0 USD"));
751 assert_eq!(position.total_pnl(last_price), Money::from("47.0 USD"));
752 assert_eq!(position.commissions(), vec![Money::from("2.0 USD")]);
753 assert_eq!(
754 format!("{position}"),
755 "Position(LONG 100_000 AUD/USD.SIM, id=1)"
756 );
757 }
758
759 #[rstest]
760 fn test_position_filled_with_sell_order(audusd_sim: CurrencyPair) {
761 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
762 let order = OrderTestBuilder::new(OrderType::Market)
763 .instrument_id(audusd_sim.id())
764 .side(OrderSide::Sell)
765 .quantity(Quantity::from(100_000))
766 .build();
767 let fill = TestOrderEventStubs::filled(
768 &order,
769 &audusd_sim,
770 None,
771 None,
772 Some(Price::from("1.00001")),
773 None,
774 None,
775 None,
776 None,
777 None,
778 );
779 let last_price = Price::from_str("1.00050").unwrap();
780 let position = Position::new(&audusd_sim, fill.into());
781 assert_eq!(position.symbol(), audusd_sim.id().symbol);
782 assert_eq!(position.venue(), audusd_sim.id().venue);
783 assert_eq!(position.closing_order_side(), OrderSide::Buy);
784 assert!(!position.is_opposite_side(OrderSide::Sell));
785 assert_eq!(position, position); assert!(position.closing_order_id.is_none());
787 assert_eq!(position.quantity, Quantity::from(100_000));
788 assert_eq!(position.peak_qty, Quantity::from(100_000));
789 assert_eq!(position.signed_qty, -100_000.0);
790 assert_eq!(position.entry, OrderSide::Sell);
791 assert_eq!(position.side, PositionSide::Short);
792 assert_eq!(position.ts_opened.as_u64(), 0);
793 assert_eq!(position.avg_px_open, 1.00001);
794 assert_eq!(position.event_count(), 1);
795 assert_eq!(position.id, PositionId::new("1"));
796 assert_eq!(position.events.len(), 1);
797 assert!(!position.is_long());
798 assert!(position.is_short());
799 assert!(position.is_open());
800 assert!(!position.is_closed());
801 assert_eq!(position.realized_return, 0.0);
802 assert_eq!(position.realized_pnl, Some(Money::from("-2.0 USD")));
803 assert_eq!(
804 position.unrealized_pnl(last_price),
805 Money::from("-49.0 USD")
806 );
807 assert_eq!(position.total_pnl(last_price), Money::from("-51.0 USD"));
808 assert_eq!(position.commissions(), vec![Money::from("2.0 USD")]);
809 assert_eq!(
810 format!("{position}"),
811 "Position(SHORT 100_000 AUD/USD.SIM, id=1)"
812 );
813 }
814
815 #[rstest]
816 fn test_position_partial_fills_with_buy_order(audusd_sim: CurrencyPair) {
817 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
818 let order = OrderTestBuilder::new(OrderType::Market)
819 .instrument_id(audusd_sim.id())
820 .side(OrderSide::Buy)
821 .quantity(Quantity::from(100_000))
822 .build();
823 let fill = TestOrderEventStubs::filled(
824 &order,
825 &audusd_sim,
826 None,
827 None,
828 Some(Price::from("1.00001")),
829 Some(Quantity::from(50_000)),
830 None,
831 None,
832 None,
833 None,
834 );
835 let last_price = Price::from_str("1.00048").unwrap();
836 let position = Position::new(&audusd_sim, fill.into());
837 assert_eq!(position.quantity, Quantity::from(50_000));
838 assert_eq!(position.peak_qty, Quantity::from(50_000));
839 assert_eq!(position.side, PositionSide::Long);
840 assert_eq!(position.signed_qty, 50000.0);
841 assert_eq!(position.avg_px_open, 1.00001);
842 assert_eq!(position.event_count(), 1);
843 assert_eq!(position.ts_opened.as_u64(), 0);
844 assert!(position.is_long());
845 assert!(!position.is_short());
846 assert!(position.is_open());
847 assert!(!position.is_closed());
848 assert_eq!(position.realized_return, 0.0);
849 assert_eq!(position.realized_pnl, Some(Money::from("-2.0 USD")));
850 assert_eq!(position.unrealized_pnl(last_price), Money::from("23.5 USD"));
851 assert_eq!(position.total_pnl(last_price), Money::from("21.5 USD"));
852 assert_eq!(position.commissions(), vec![Money::from("2.0 USD")]);
853 assert_eq!(
854 format!("{position}"),
855 "Position(LONG 50_000 AUD/USD.SIM, id=1)"
856 );
857 }
858
859 #[rstest]
860 fn test_position_partial_fills_with_two_sell_orders(audusd_sim: CurrencyPair) {
861 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
862 let order = OrderTestBuilder::new(OrderType::Market)
863 .instrument_id(audusd_sim.id())
864 .side(OrderSide::Sell)
865 .quantity(Quantity::from(100_000))
866 .build();
867 let fill1 = TestOrderEventStubs::filled(
868 &order,
869 &audusd_sim,
870 Some(TradeId::new("1")),
871 None,
872 Some(Price::from("1.00001")),
873 Some(Quantity::from(50_000)),
874 None,
875 None,
876 None,
877 None,
878 );
879 let fill2 = TestOrderEventStubs::filled(
880 &order,
881 &audusd_sim,
882 Some(TradeId::new("2")),
883 None,
884 Some(Price::from("1.00002")),
885 Some(Quantity::from(50_000)),
886 None,
887 None,
888 None,
889 None,
890 );
891 let last_price = Price::from_str("1.0005").unwrap();
892 let mut position = Position::new(&audusd_sim, fill1.into());
893 position.apply(&fill2.into());
894
895 assert_eq!(position.quantity, Quantity::from(100_000));
896 assert_eq!(position.peak_qty, Quantity::from(100_000));
897 assert_eq!(position.side, PositionSide::Short);
898 assert_eq!(position.signed_qty, -100_000.0);
899 assert_eq!(position.avg_px_open, 1.000_015);
900 assert_eq!(position.event_count(), 2);
901 assert_eq!(position.ts_opened, 0);
902 assert!(position.is_short());
903 assert!(!position.is_long());
904 assert!(position.is_open());
905 assert!(!position.is_closed());
906 assert_eq!(position.realized_return, 0.0);
907 assert_eq!(position.realized_pnl, Some(Money::from("-4.0 USD")));
908 assert_eq!(
909 position.unrealized_pnl(last_price),
910 Money::from("-48.5 USD")
911 );
912 assert_eq!(position.total_pnl(last_price), Money::from("-52.5 USD"));
913 assert_eq!(position.commissions(), vec![Money::from("4.0 USD")]);
914 }
915
916 #[rstest]
917 pub fn test_position_filled_with_buy_order_then_sell_order(audusd_sim: CurrencyPair) {
918 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
919 let order = OrderTestBuilder::new(OrderType::Market)
920 .instrument_id(audusd_sim.id())
921 .side(OrderSide::Buy)
922 .quantity(Quantity::from(150_000))
923 .build();
924 let fill = TestOrderEventStubs::filled(
925 &order,
926 &audusd_sim,
927 Some(TradeId::new("1")),
928 Some(PositionId::new("P-1")),
929 Some(Price::from("1.00001")),
930 None,
931 None,
932 None,
933 Some(UnixNanos::from(1_000_000_000)),
934 None,
935 );
936 let mut position = Position::new(&audusd_sim, fill.into());
937
938 let fill2 = OrderFilled::new(
939 order.trader_id(),
940 StrategyId::new("S-001"),
941 order.instrument_id(),
942 order.client_order_id(),
943 VenueOrderId::from("2"),
944 order.account_id().unwrap_or(AccountId::new("SIM-001")),
945 TradeId::new("2"),
946 OrderSide::Sell,
947 OrderType::Market,
948 order.quantity(),
949 Price::from("1.00011"),
950 audusd_sim.quote_currency(),
951 LiquiditySide::Taker,
952 uuid4(),
953 2_000_000_000.into(),
954 0.into(),
955 false,
956 Some(PositionId::new("T1")),
957 Some(Money::from("0.0 USD")),
958 );
959 position.apply(&fill2);
960 let last = Price::from_str("1.0005").unwrap();
961
962 assert!(position.is_opposite_side(fill2.order_side));
963 assert_eq!(
964 position.quantity,
965 Quantity::zero(audusd_sim.price_precision())
966 );
967 assert_eq!(position.size_precision, 0);
968 assert_eq!(position.signed_qty, 0.0);
969 assert_eq!(position.side, PositionSide::Flat);
970 assert_eq!(position.ts_opened, 1_000_000_000);
971 assert_eq!(position.ts_closed, Some(UnixNanos::from(2_000_000_000)));
972 assert_eq!(position.duration_ns, 1_000_000_000);
973 assert_eq!(position.avg_px_open, 1.00001);
974 assert_eq!(position.avg_px_close, Some(1.00011));
975 assert!(!position.is_long());
976 assert!(!position.is_short());
977 assert!(!position.is_open());
978 assert!(position.is_closed());
979 assert_eq!(position.realized_return, 9.999_900_000_998_888e-5);
980 assert_eq!(position.realized_pnl, Some(Money::from("13.0 USD")));
981 assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
982 assert_eq!(position.commissions(), vec![Money::from("2 USD")]);
983 assert_eq!(position.total_pnl(last), Money::from("13 USD"));
984 assert_eq!(format!("{position}"), "Position(FLAT AUD/USD.SIM, id=P-1)");
985 }
986
987 #[rstest]
988 pub fn test_position_filled_with_sell_order_then_buy_order(audusd_sim: CurrencyPair) {
989 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
990 let order1 = OrderTestBuilder::new(OrderType::Market)
991 .instrument_id(audusd_sim.id())
992 .side(OrderSide::Sell)
993 .quantity(Quantity::from(100_000))
994 .build();
995 let order2 = OrderTestBuilder::new(OrderType::Market)
996 .instrument_id(audusd_sim.id())
997 .side(OrderSide::Buy)
998 .quantity(Quantity::from(100_000))
999 .build();
1000 let fill1 = TestOrderEventStubs::filled(
1001 &order1,
1002 &audusd_sim,
1003 None,
1004 Some(PositionId::new("P-19700101-000000-001-001-1")),
1005 Some(Price::from("1.0")),
1006 None,
1007 None,
1008 None,
1009 None,
1010 None,
1011 );
1012 let mut position = Position::new(&audusd_sim, fill1.into());
1013 let fill2 = TestOrderEventStubs::filled(
1015 &order2,
1016 &audusd_sim,
1017 Some(TradeId::new("1")),
1018 Some(PositionId::new("P-19700101-000000-001-001-1")),
1019 Some(Price::from("1.00001")),
1020 Some(Quantity::from(50_000)),
1021 None,
1022 None,
1023 None,
1024 None,
1025 );
1026 let fill3 = TestOrderEventStubs::filled(
1027 &order2,
1028 &audusd_sim,
1029 Some(TradeId::new("2")),
1030 Some(PositionId::new("P-19700101-000000-001-001-1")),
1031 Some(Price::from("1.00003")),
1032 Some(Quantity::from(50_000)),
1033 None,
1034 None,
1035 None,
1036 None,
1037 );
1038 let last = Price::from("1.0005");
1039 position.apply(&fill2.into());
1040 position.apply(&fill3.into());
1041
1042 assert_eq!(
1043 position.quantity,
1044 Quantity::zero(audusd_sim.price_precision())
1045 );
1046 assert_eq!(position.side, PositionSide::Flat);
1047 assert_eq!(position.ts_opened, 0);
1048 assert_eq!(position.avg_px_open, 1.0);
1049 assert_eq!(position.events.len(), 3);
1050 assert_eq!(position.ts_closed, Some(UnixNanos::default()));
1051 assert_eq!(position.avg_px_close, Some(1.00002));
1052 assert!(!position.is_long());
1053 assert!(!position.is_short());
1054 assert!(!position.is_open());
1055 assert!(position.is_closed());
1056 assert_eq!(position.commissions(), vec![Money::from("6.0 USD")]);
1057 assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1058 assert_eq!(position.realized_pnl, Some(Money::from("-8.0 USD")));
1059 assert_eq!(position.total_pnl(last), Money::from("-8.0 USD"));
1060 assert_eq!(
1061 format!("{position}"),
1062 "Position(FLAT AUD/USD.SIM, id=P-19700101-000000-001-001-1)"
1063 );
1064 }
1065
1066 #[rstest]
1067 fn test_position_filled_with_no_change(audusd_sim: CurrencyPair) {
1068 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1069 let order1 = OrderTestBuilder::new(OrderType::Market)
1070 .instrument_id(audusd_sim.id())
1071 .side(OrderSide::Buy)
1072 .quantity(Quantity::from(100_000))
1073 .build();
1074 let order2 = OrderTestBuilder::new(OrderType::Market)
1075 .instrument_id(audusd_sim.id())
1076 .side(OrderSide::Sell)
1077 .quantity(Quantity::from(100_000))
1078 .build();
1079 let fill1 = TestOrderEventStubs::filled(
1080 &order1,
1081 &audusd_sim,
1082 Some(TradeId::new("1")),
1083 Some(PositionId::new("P-19700101-000000-001-001-1")),
1084 Some(Price::from("1.0")),
1085 None,
1086 None,
1087 None,
1088 None,
1089 None,
1090 );
1091 let mut position = Position::new(&audusd_sim, fill1.into());
1092 let fill2 = TestOrderEventStubs::filled(
1093 &order2,
1094 &audusd_sim,
1095 Some(TradeId::new("2")),
1096 Some(PositionId::new("P-19700101-000000-001-001-1")),
1097 Some(Price::from("1.0")),
1098 None,
1099 None,
1100 None,
1101 None,
1102 None,
1103 );
1104 let last = Price::from("1.0005");
1105 position.apply(&fill2.into());
1106
1107 assert_eq!(
1108 position.quantity,
1109 Quantity::zero(audusd_sim.price_precision())
1110 );
1111 assert_eq!(position.closing_order_side(), OrderSide::NoOrderSide);
1112 assert_eq!(position.side, PositionSide::Flat);
1113 assert_eq!(position.ts_opened, 0);
1114 assert_eq!(position.avg_px_open, 1.0);
1115 assert_eq!(position.events.len(), 2);
1116 assert_eq!(position.ts_closed, Some(UnixNanos::default()));
1118 assert_eq!(position.avg_px_close, Some(1.0));
1119 assert!(!position.is_long());
1120 assert!(!position.is_short());
1121 assert!(!position.is_open());
1122 assert!(position.is_closed());
1123 assert_eq!(position.commissions(), vec![Money::from("4.0 USD")]);
1124 assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1125 assert_eq!(position.realized_pnl, Some(Money::from("-4.0 USD")));
1126 assert_eq!(position.total_pnl(last), Money::from("-4.0 USD"));
1127 assert_eq!(
1128 format!("{position}"),
1129 "Position(FLAT AUD/USD.SIM, id=P-19700101-000000-001-001-1)"
1130 );
1131 }
1132
1133 #[rstest]
1134 fn test_position_long_with_multiple_filled_orders(audusd_sim: CurrencyPair) {
1135 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1136 let order1 = OrderTestBuilder::new(OrderType::Market)
1137 .instrument_id(audusd_sim.id())
1138 .side(OrderSide::Buy)
1139 .quantity(Quantity::from(100_000))
1140 .build();
1141 let order2 = OrderTestBuilder::new(OrderType::Market)
1142 .instrument_id(audusd_sim.id())
1143 .side(OrderSide::Buy)
1144 .quantity(Quantity::from(100_000))
1145 .build();
1146 let order3 = OrderTestBuilder::new(OrderType::Market)
1147 .instrument_id(audusd_sim.id())
1148 .side(OrderSide::Sell)
1149 .quantity(Quantity::from(200_000))
1150 .build();
1151 let fill1 = TestOrderEventStubs::filled(
1152 &order1,
1153 &audusd_sim,
1154 Some(TradeId::new("1")),
1155 Some(PositionId::new("P-123456")),
1156 Some(Price::from("1.0")),
1157 None,
1158 None,
1159 None,
1160 None,
1161 None,
1162 );
1163 let fill2 = TestOrderEventStubs::filled(
1164 &order2,
1165 &audusd_sim,
1166 Some(TradeId::new("2")),
1167 Some(PositionId::new("P-123456")),
1168 Some(Price::from("1.00001")),
1169 None,
1170 None,
1171 None,
1172 None,
1173 None,
1174 );
1175 let fill3 = TestOrderEventStubs::filled(
1176 &order3,
1177 &audusd_sim,
1178 Some(TradeId::new("3")),
1179 Some(PositionId::new("P-123456")),
1180 Some(Price::from("1.0001")),
1181 None,
1182 None,
1183 None,
1184 None,
1185 None,
1186 );
1187 let mut position = Position::new(&audusd_sim, fill1.into());
1188 let last = Price::from("1.0005");
1189 position.apply(&fill2.into());
1190 position.apply(&fill3.into());
1191
1192 assert_eq!(
1193 position.quantity,
1194 Quantity::zero(audusd_sim.price_precision())
1195 );
1196 assert_eq!(position.side, PositionSide::Flat);
1197 assert_eq!(position.ts_opened, 0);
1198 assert_eq!(position.avg_px_open, 1.000_005);
1199 assert_eq!(position.events.len(), 3);
1200 assert_eq!(position.ts_closed, Some(UnixNanos::default()));
1205 assert_eq!(position.avg_px_close, Some(1.0001));
1206 assert!(position.is_closed());
1207 assert!(!position.is_open());
1208 assert!(!position.is_long());
1209 assert!(!position.is_short());
1210 assert_eq!(position.commissions(), vec![Money::from("6.0 USD")]);
1211 assert_eq!(position.realized_pnl, Some(Money::from("13.0 USD")));
1212 assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1213 assert_eq!(position.total_pnl(last), Money::from("13 USD"));
1214 assert_eq!(
1215 format!("{position}"),
1216 "Position(FLAT AUD/USD.SIM, id=P-123456)"
1217 );
1218 }
1219
1220 #[rstest]
1221 fn test_pnl_calculation_from_trading_technologies_example(currency_pair_ethusdt: CurrencyPair) {
1222 let ethusdt = InstrumentAny::CurrencyPair(currency_pair_ethusdt);
1223 let quantity1 = Quantity::from(12);
1224 let price1 = Price::from("100.0");
1225 let order1 = OrderTestBuilder::new(OrderType::Market)
1226 .instrument_id(ethusdt.id())
1227 .side(OrderSide::Buy)
1228 .quantity(quantity1)
1229 .build();
1230 let commission1 = calculate_commission(ðusdt, order1.quantity(), price1, None);
1231 let fill1 = TestOrderEventStubs::filled(
1232 &order1,
1233 ðusdt,
1234 Some(TradeId::new("1")),
1235 Some(PositionId::new("P-123456")),
1236 Some(price1),
1237 None,
1238 None,
1239 Some(commission1),
1240 None,
1241 None,
1242 );
1243 let mut position = Position::new(ðusdt, fill1.into());
1244 let quantity2 = Quantity::from(17);
1245 let order2 = OrderTestBuilder::new(OrderType::Market)
1246 .instrument_id(ethusdt.id())
1247 .side(OrderSide::Buy)
1248 .quantity(quantity2)
1249 .build();
1250 let price2 = Price::from("99.0");
1251 let commission2 = calculate_commission(ðusdt, order2.quantity(), price2, None);
1252 let fill2 = TestOrderEventStubs::filled(
1253 &order2,
1254 ðusdt,
1255 Some(TradeId::new("2")),
1256 Some(PositionId::new("P-123456")),
1257 Some(price2),
1258 None,
1259 None,
1260 Some(commission2),
1261 None,
1262 None,
1263 );
1264 position.apply(&fill2.into());
1265 assert_eq!(position.quantity, Quantity::from(29));
1266 assert_eq!(position.realized_pnl, Some(Money::from("-0.28830000 USDT")));
1267 assert_eq!(position.avg_px_open, 99.413_793_103_448_27);
1268 let quantity3 = Quantity::from(9);
1269 let order3 = OrderTestBuilder::new(OrderType::Market)
1270 .instrument_id(ethusdt.id())
1271 .side(OrderSide::Sell)
1272 .quantity(quantity3)
1273 .build();
1274 let price3 = Price::from("101.0");
1275 let commission3 = calculate_commission(ðusdt, order3.quantity(), price3, None);
1276 let fill3 = TestOrderEventStubs::filled(
1277 &order3,
1278 ðusdt,
1279 Some(TradeId::new("3")),
1280 Some(PositionId::new("P-123456")),
1281 Some(price3),
1282 None,
1283 None,
1284 Some(commission3),
1285 None,
1286 None,
1287 );
1288 position.apply(&fill3.into());
1289 assert_eq!(position.quantity, Quantity::from(20));
1290 assert_eq!(position.realized_pnl, Some(Money::from("13.89666207 USDT")));
1291 assert_eq!(position.avg_px_open, 99.413_793_103_448_27);
1292 let quantity4 = Quantity::from("4");
1293 let price4 = Price::from("105.0");
1294 let order4 = OrderTestBuilder::new(OrderType::Market)
1295 .instrument_id(ethusdt.id())
1296 .side(OrderSide::Sell)
1297 .quantity(quantity4)
1298 .build();
1299 let commission4 = calculate_commission(ðusdt, order4.quantity(), price4, None);
1300 let fill4 = TestOrderEventStubs::filled(
1301 &order4,
1302 ðusdt,
1303 Some(TradeId::new("4")),
1304 Some(PositionId::new("P-123456")),
1305 Some(price4),
1306 None,
1307 None,
1308 Some(commission4),
1309 None,
1310 None,
1311 );
1312 position.apply(&fill4.into());
1313 assert_eq!(position.quantity, Quantity::from("16"));
1314 assert_eq!(position.realized_pnl, Some(Money::from("36.19948966 USDT")));
1315 assert_eq!(position.avg_px_open, 99.413_793_103_448_27);
1316 let quantity5 = Quantity::from("3");
1317 let price5 = Price::from("103.0");
1318 let order5 = OrderTestBuilder::new(OrderType::Market)
1319 .instrument_id(ethusdt.id())
1320 .side(OrderSide::Buy)
1321 .quantity(quantity5)
1322 .build();
1323 let commission5 = calculate_commission(ðusdt, order5.quantity(), price5, None);
1324 let fill5 = TestOrderEventStubs::filled(
1325 &order5,
1326 ðusdt,
1327 Some(TradeId::new("5")),
1328 Some(PositionId::new("P-123456")),
1329 Some(price5),
1330 None,
1331 None,
1332 Some(commission5),
1333 None,
1334 None,
1335 );
1336 position.apply(&fill5.into());
1337 assert_eq!(position.quantity, Quantity::from("19"));
1338 assert_eq!(position.realized_pnl, Some(Money::from("36.16858966 USDT")));
1339 assert_eq!(position.avg_px_open, 99.980_036_297_640_65);
1340 assert_eq!(
1341 format!("{position}"),
1342 "Position(LONG 19.00000 ETHUSDT.BINANCE, id=P-123456)"
1343 );
1344 }
1345
1346 #[rstest]
1347 fn test_position_closed_and_reopened(audusd_sim: CurrencyPair) {
1348 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1349 let quantity1 = Quantity::from(150_000);
1350 let price1 = Price::from("1.00001");
1351 let order = OrderTestBuilder::new(OrderType::Market)
1352 .instrument_id(audusd_sim.id())
1353 .side(OrderSide::Buy)
1354 .quantity(quantity1)
1355 .build();
1356 let commission1 = calculate_commission(&audusd_sim, quantity1, price1, None);
1357 let fill1 = TestOrderEventStubs::filled(
1358 &order,
1359 &audusd_sim,
1360 Some(TradeId::new("5")),
1361 Some(PositionId::new("P-123456")),
1362 Some(Price::from("1.00001")),
1363 None,
1364 None,
1365 Some(commission1),
1366 Some(UnixNanos::from(1_000_000_000)),
1367 None,
1368 );
1369 let mut position = Position::new(&audusd_sim, fill1.into());
1370
1371 let fill2 = OrderFilled::new(
1372 order.trader_id(),
1373 order.strategy_id(),
1374 order.instrument_id(),
1375 order.client_order_id(),
1376 VenueOrderId::from("2"),
1377 order.account_id().unwrap_or(AccountId::new("SIM-001")),
1378 TradeId::from("2"),
1379 OrderSide::Sell,
1380 OrderType::Market,
1381 order.quantity(),
1382 Price::from("1.00011"),
1383 audusd_sim.quote_currency(),
1384 LiquiditySide::Taker,
1385 uuid4(),
1386 UnixNanos::from(2_000_000_000),
1387 UnixNanos::default(),
1388 false,
1389 Some(PositionId::from("P-123456")),
1390 Some(Money::from("0 USD")),
1391 );
1392
1393 position.apply(&fill2);
1394
1395 let fill3 = OrderFilled::new(
1396 order.trader_id(),
1397 order.strategy_id(),
1398 order.instrument_id(),
1399 order.client_order_id(),
1400 VenueOrderId::from("2"),
1401 order.account_id().unwrap_or(AccountId::new("SIM-001")),
1402 TradeId::from("3"),
1403 OrderSide::Buy,
1404 OrderType::Market,
1405 order.quantity(),
1406 Price::from("1.00012"),
1407 audusd_sim.quote_currency(),
1408 LiquiditySide::Taker,
1409 uuid4(),
1410 UnixNanos::from(3_000_000_000),
1411 UnixNanos::default(),
1412 false,
1413 Some(PositionId::from("P-123456")),
1414 Some(Money::from("0 USD")),
1415 );
1416
1417 position.apply(&fill3);
1418
1419 let last = Price::from("1.0003");
1420 assert!(position.is_opposite_side(fill2.order_side));
1421 assert_eq!(position.quantity, Quantity::from(150_000));
1422 assert_eq!(position.peak_qty, Quantity::from(150_000));
1423 assert_eq!(position.side, PositionSide::Long);
1424 assert_eq!(position.opening_order_id, fill3.client_order_id);
1425 assert_eq!(position.closing_order_id, None);
1426 assert_eq!(position.closing_order_id, None);
1427 assert_eq!(position.ts_opened, 3_000_000_000);
1428 assert_eq!(position.duration_ns, 0);
1429 assert_eq!(position.avg_px_open, 1.00012);
1430 assert_eq!(position.event_count(), 1);
1431 assert_eq!(position.ts_closed, None);
1432 assert_eq!(position.avg_px_close, None);
1433 assert!(position.is_long());
1434 assert!(!position.is_short());
1435 assert!(position.is_open());
1436 assert!(!position.is_closed());
1437 assert_eq!(position.realized_return, 0.0);
1438 assert_eq!(position.realized_pnl, Some(Money::from("0 USD")));
1439 assert_eq!(position.unrealized_pnl(last), Money::from("27 USD"));
1440 assert_eq!(position.total_pnl(last), Money::from("27 USD"));
1441 assert_eq!(position.commissions(), vec![Money::from("0 USD")]);
1442 assert_eq!(
1443 format!("{position}"),
1444 "Position(LONG 150_000 AUD/USD.SIM, id=P-123456)"
1445 );
1446 }
1447
1448 #[rstest]
1449 fn test_position_realized_pnl_with_interleaved_order_sides(
1450 currency_pair_btcusdt: CurrencyPair,
1451 ) {
1452 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1453 let order1 = OrderTestBuilder::new(OrderType::Market)
1454 .instrument_id(btcusdt.id())
1455 .side(OrderSide::Buy)
1456 .quantity(Quantity::from(12))
1457 .build();
1458 let commission1 =
1459 calculate_commission(&btcusdt, order1.quantity(), Price::from("10000.0"), None);
1460 let fill1 = TestOrderEventStubs::filled(
1461 &order1,
1462 &btcusdt,
1463 Some(TradeId::from("1")),
1464 Some(PositionId::from("P-19700101-000000-001-001-1")),
1465 Some(Price::from("10000.0")),
1466 None,
1467 None,
1468 Some(commission1),
1469 None,
1470 None,
1471 );
1472 let mut position = Position::new(&btcusdt, fill1.into());
1473 let order2 = OrderTestBuilder::new(OrderType::Market)
1474 .instrument_id(btcusdt.id())
1475 .side(OrderSide::Buy)
1476 .quantity(Quantity::from(17))
1477 .build();
1478 let commission2 =
1479 calculate_commission(&btcusdt, order2.quantity(), Price::from("9999.0"), None);
1480 let fill2 = TestOrderEventStubs::filled(
1481 &order2,
1482 &btcusdt,
1483 Some(TradeId::from("2")),
1484 Some(PositionId::from("P-19700101-000000-001-001-1")),
1485 Some(Price::from("9999.0")),
1486 None,
1487 None,
1488 Some(commission2),
1489 None,
1490 None,
1491 );
1492 position.apply(&fill2.into());
1493 assert_eq!(position.quantity, Quantity::from(29));
1494 assert_eq!(
1495 position.realized_pnl,
1496 Some(Money::from("-289.98300000 USDT"))
1497 );
1498 assert_eq!(position.avg_px_open, 9_999.413_793_103_447);
1499 let order3 = OrderTestBuilder::new(OrderType::Market)
1500 .instrument_id(btcusdt.id())
1501 .side(OrderSide::Sell)
1502 .quantity(Quantity::from(9))
1503 .build();
1504 let commission3 =
1505 calculate_commission(&btcusdt, order3.quantity(), Price::from("10001.0"), None);
1506 let fill3 = TestOrderEventStubs::filled(
1507 &order3,
1508 &btcusdt,
1509 Some(TradeId::from("3")),
1510 Some(PositionId::from("P-19700101-000000-001-001-1")),
1511 Some(Price::from("10001.0")),
1512 None,
1513 None,
1514 Some(commission3),
1515 None,
1516 None,
1517 );
1518 position.apply(&fill3.into());
1519 assert_eq!(position.quantity, Quantity::from(20));
1520 assert_eq!(
1521 position.realized_pnl,
1522 Some(Money::from("-365.71613793 USDT"))
1523 );
1524 assert_eq!(position.avg_px_open, 9_999.413_793_103_447);
1525 let order4 = OrderTestBuilder::new(OrderType::Market)
1526 .instrument_id(btcusdt.id())
1527 .side(OrderSide::Buy)
1528 .quantity(Quantity::from(3))
1529 .build();
1530 let commission4 =
1531 calculate_commission(&btcusdt, order4.quantity(), Price::from("10003.0"), None);
1532 let fill4 = TestOrderEventStubs::filled(
1533 &order4,
1534 &btcusdt,
1535 Some(TradeId::from("4")),
1536 Some(PositionId::from("P-19700101-000000-001-001-1")),
1537 Some(Price::from("10003.0")),
1538 None,
1539 None,
1540 Some(commission4),
1541 None,
1542 None,
1543 );
1544 position.apply(&fill4.into());
1545 assert_eq!(position.quantity, Quantity::from(23));
1546 assert_eq!(
1547 position.realized_pnl,
1548 Some(Money::from("-395.72513793 USDT"))
1549 );
1550 assert_eq!(position.avg_px_open, 9_999.881_559_220_39);
1551 let order5 = OrderTestBuilder::new(OrderType::Market)
1552 .instrument_id(btcusdt.id())
1553 .side(OrderSide::Sell)
1554 .quantity(Quantity::from(4))
1555 .build();
1556 let commission5 =
1557 calculate_commission(&btcusdt, order5.quantity(), Price::from("10005.0"), None);
1558 let fill5 = TestOrderEventStubs::filled(
1559 &order5,
1560 &btcusdt,
1561 Some(TradeId::from("5")),
1562 Some(PositionId::from("P-19700101-000000-001-001-1")),
1563 Some(Price::from("10005.0")),
1564 None,
1565 None,
1566 Some(commission5),
1567 None,
1568 None,
1569 );
1570 position.apply(&fill5.into());
1571 assert_eq!(position.quantity, Quantity::from(19));
1572 assert_eq!(
1573 position.realized_pnl,
1574 Some(Money::from("-415.27137481 USDT"))
1575 );
1576 assert_eq!(position.avg_px_open, 9_999.881_559_220_39);
1577 assert_eq!(
1578 format!("{position}"),
1579 "Position(LONG 19.000000 BTCUSDT.BINANCE, id=P-19700101-000000-001-001-1)"
1580 );
1581 }
1582
1583 #[rstest]
1584 fn test_calculate_pnl_when_given_position_side_flat_returns_zero(
1585 currency_pair_btcusdt: CurrencyPair,
1586 ) {
1587 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1588 let order = OrderTestBuilder::new(OrderType::Market)
1589 .instrument_id(btcusdt.id())
1590 .side(OrderSide::Buy)
1591 .quantity(Quantity::from(12))
1592 .build();
1593 let fill = TestOrderEventStubs::filled(
1594 &order,
1595 &btcusdt,
1596 None,
1597 Some(PositionId::from("P-123456")),
1598 Some(Price::from("10500.0")),
1599 None,
1600 None,
1601 None,
1602 None,
1603 None,
1604 );
1605 let position = Position::new(&btcusdt, fill.into());
1606 let result = position.calculate_pnl(10500.0, 10500.0, Quantity::from("100000.0"));
1607 assert_eq!(result, Money::from("0 USDT"));
1608 }
1609
1610 #[rstest]
1611 fn test_calculate_pnl_for_long_position_win(currency_pair_btcusdt: CurrencyPair) {
1612 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1613 let order = OrderTestBuilder::new(OrderType::Market)
1614 .instrument_id(btcusdt.id())
1615 .side(OrderSide::Buy)
1616 .quantity(Quantity::from(12))
1617 .build();
1618 let commission =
1619 calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1620 let fill = TestOrderEventStubs::filled(
1621 &order,
1622 &btcusdt,
1623 None,
1624 Some(PositionId::from("P-123456")),
1625 Some(Price::from("10500.0")),
1626 None,
1627 None,
1628 Some(commission),
1629 None,
1630 None,
1631 );
1632 let position = Position::new(&btcusdt, fill.into());
1633 let pnl = position.calculate_pnl(10500.0, 10510.0, Quantity::from("12.0"));
1634 assert_eq!(pnl, Money::from("120 USDT"));
1635 assert_eq!(position.realized_pnl, Some(Money::from("-126 USDT")));
1636 assert_eq!(
1637 position.unrealized_pnl(Price::from("10510.0")),
1638 Money::from("120.0 USDT")
1639 );
1640 assert_eq!(
1641 position.total_pnl(Price::from("10510.0")),
1642 Money::from("-6 USDT")
1643 );
1644 assert_eq!(position.commissions(), vec![Money::from("126.0 USDT")]);
1645 }
1646
1647 #[rstest]
1648 fn test_calculate_pnl_for_long_position_loss(currency_pair_btcusdt: CurrencyPair) {
1649 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1650 let order = OrderTestBuilder::new(OrderType::Market)
1651 .instrument_id(btcusdt.id())
1652 .side(OrderSide::Buy)
1653 .quantity(Quantity::from(12))
1654 .build();
1655 let commission =
1656 calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1657 let fill = TestOrderEventStubs::filled(
1658 &order,
1659 &btcusdt,
1660 None,
1661 Some(PositionId::from("P-123456")),
1662 Some(Price::from("10500.0")),
1663 None,
1664 None,
1665 Some(commission),
1666 None,
1667 None,
1668 );
1669 let position = Position::new(&btcusdt, fill.into());
1670 let pnl = position.calculate_pnl(10500.0, 10480.5, Quantity::from("10.0"));
1671 assert_eq!(pnl, Money::from("-195 USDT"));
1672 assert_eq!(position.realized_pnl, Some(Money::from("-126 USDT")));
1673 assert_eq!(
1674 position.unrealized_pnl(Price::from("10480.50")),
1675 Money::from("-234.0 USDT")
1676 );
1677 assert_eq!(
1678 position.total_pnl(Price::from("10480.50")),
1679 Money::from("-360 USDT")
1680 );
1681 assert_eq!(position.commissions(), vec![Money::from("126.0 USDT")]);
1682 }
1683
1684 #[rstest]
1685 fn test_calculate_pnl_for_short_position_winning(currency_pair_btcusdt: CurrencyPair) {
1686 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1687 let order = OrderTestBuilder::new(OrderType::Market)
1688 .instrument_id(btcusdt.id())
1689 .side(OrderSide::Sell)
1690 .quantity(Quantity::from("10.15"))
1691 .build();
1692 let commission =
1693 calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1694 let fill = TestOrderEventStubs::filled(
1695 &order,
1696 &btcusdt,
1697 None,
1698 Some(PositionId::from("P-123456")),
1699 Some(Price::from("10500.0")),
1700 None,
1701 None,
1702 Some(commission),
1703 None,
1704 None,
1705 );
1706 let position = Position::new(&btcusdt, fill.into());
1707 let pnl = position.calculate_pnl(10500.0, 10390.0, Quantity::from("10.15"));
1708 assert_eq!(pnl, Money::from("1116.5 USDT"));
1709 assert_eq!(
1710 position.unrealized_pnl(Price::from("10390.0")),
1711 Money::from("1116.5 USDT")
1712 );
1713 assert_eq!(position.realized_pnl, Some(Money::from("-106.575 USDT")));
1714 assert_eq!(position.commissions(), vec![Money::from("106.575 USDT")]);
1715 assert_eq!(
1716 position.notional_value(Price::from("10390.0")),
1717 Money::from("105458.5 USDT")
1718 );
1719 }
1720
1721 #[rstest]
1722 fn test_calculate_pnl_for_short_position_loss(currency_pair_btcusdt: CurrencyPair) {
1723 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1724 let order = OrderTestBuilder::new(OrderType::Market)
1725 .instrument_id(btcusdt.id())
1726 .side(OrderSide::Sell)
1727 .quantity(Quantity::from("10.0"))
1728 .build();
1729 let commission =
1730 calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1731 let fill = TestOrderEventStubs::filled(
1732 &order,
1733 &btcusdt,
1734 None,
1735 Some(PositionId::from("P-123456")),
1736 Some(Price::from("10500.0")),
1737 None,
1738 None,
1739 Some(commission),
1740 None,
1741 None,
1742 );
1743 let position = Position::new(&btcusdt, fill.into());
1744 let pnl = position.calculate_pnl(10500.0, 10670.5, Quantity::from("10.0"));
1745 assert_eq!(pnl, Money::from("-1705 USDT"));
1746 assert_eq!(
1747 position.unrealized_pnl(Price::from("10670.5")),
1748 Money::from("-1705 USDT")
1749 );
1750 assert_eq!(position.realized_pnl, Some(Money::from("-105 USDT")));
1751 assert_eq!(position.commissions(), vec![Money::from("105 USDT")]);
1752 assert_eq!(
1753 position.notional_value(Price::from("10670.5")),
1754 Money::from("106705 USDT")
1755 );
1756 }
1757
1758 #[rstest]
1759 fn test_calculate_pnl_for_inverse1(xbtusd_bitmex: CryptoPerpetual) {
1760 let xbtusd_bitmex = InstrumentAny::CryptoPerpetual(xbtusd_bitmex);
1761 let order = OrderTestBuilder::new(OrderType::Market)
1762 .instrument_id(xbtusd_bitmex.id())
1763 .side(OrderSide::Sell)
1764 .quantity(Quantity::from("100000"))
1765 .build();
1766 let commission = calculate_commission(
1767 &xbtusd_bitmex,
1768 order.quantity(),
1769 Price::from("10000.0"),
1770 None,
1771 );
1772 let fill = TestOrderEventStubs::filled(
1773 &order,
1774 &xbtusd_bitmex,
1775 None,
1776 Some(PositionId::from("P-123456")),
1777 Some(Price::from("10000.0")),
1778 None,
1779 None,
1780 Some(commission),
1781 None,
1782 None,
1783 );
1784 let position = Position::new(&xbtusd_bitmex, fill.into());
1785 let pnl = position.calculate_pnl(10000.0, 11000.0, Quantity::from("100000.0"));
1786 assert_eq!(pnl, Money::from("-0.90909091 BTC"));
1787 assert_eq!(
1788 position.unrealized_pnl(Price::from("11000.0")),
1789 Money::from("-0.90909091 BTC")
1790 );
1791 assert_eq!(position.realized_pnl, Some(Money::from("-0.00750000 BTC")));
1792 assert_eq!(
1793 position.notional_value(Price::from("11000.0")),
1794 Money::from("9.09090909 BTC")
1795 );
1796 }
1797
1798 #[rstest]
1799 fn test_calculate_pnl_for_inverse2(ethusdt_bitmex: CryptoPerpetual) {
1800 let ethusdt_bitmex = InstrumentAny::CryptoPerpetual(ethusdt_bitmex);
1801 let order = OrderTestBuilder::new(OrderType::Market)
1802 .instrument_id(ethusdt_bitmex.id())
1803 .side(OrderSide::Sell)
1804 .quantity(Quantity::from("100000"))
1805 .build();
1806 let commission = calculate_commission(
1807 ðusdt_bitmex,
1808 order.quantity(),
1809 Price::from("375.95"),
1810 None,
1811 );
1812 let fill = TestOrderEventStubs::filled(
1813 &order,
1814 ðusdt_bitmex,
1815 None,
1816 Some(PositionId::from("P-123456")),
1817 Some(Price::from("375.95")),
1818 None,
1819 None,
1820 Some(commission),
1821 None,
1822 None,
1823 );
1824 let position = Position::new(ðusdt_bitmex, fill.into());
1825
1826 assert_eq!(
1827 position.unrealized_pnl(Price::from("370.00")),
1828 Money::from("4.27745208 ETH")
1829 );
1830 assert_eq!(
1831 position.notional_value(Price::from("370.00")),
1832 Money::from("270.27027027 ETH")
1833 );
1834 }
1835
1836 #[rstest]
1837 fn test_calculate_unrealized_pnl_for_long(currency_pair_btcusdt: CurrencyPair) {
1838 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1839 let order1 = OrderTestBuilder::new(OrderType::Market)
1840 .instrument_id(btcusdt.id())
1841 .side(OrderSide::Buy)
1842 .quantity(Quantity::from("2.000000"))
1843 .build();
1844 let order2 = OrderTestBuilder::new(OrderType::Market)
1845 .instrument_id(btcusdt.id())
1846 .side(OrderSide::Buy)
1847 .quantity(Quantity::from("2.000000"))
1848 .build();
1849 let commission1 =
1850 calculate_commission(&btcusdt, order1.quantity(), Price::from("10500.0"), None);
1851 let fill1 = TestOrderEventStubs::filled(
1852 &order1,
1853 &btcusdt,
1854 Some(TradeId::new("1")),
1855 Some(PositionId::new("P-123456")),
1856 Some(Price::from("10500.00")),
1857 None,
1858 None,
1859 Some(commission1),
1860 None,
1861 None,
1862 );
1863 let commission2 =
1864 calculate_commission(&btcusdt, order2.quantity(), Price::from("10500.0"), None);
1865 let fill2 = TestOrderEventStubs::filled(
1866 &order2,
1867 &btcusdt,
1868 Some(TradeId::new("2")),
1869 Some(PositionId::new("P-123456")),
1870 Some(Price::from("10500.00")),
1871 None,
1872 None,
1873 Some(commission2),
1874 None,
1875 None,
1876 );
1877 let mut position = Position::new(&btcusdt, fill1.into());
1878 position.apply(&fill2.into());
1879 let pnl = position.unrealized_pnl(Price::from("11505.60"));
1880 assert_eq!(pnl, Money::from("4022.40000000 USDT"));
1881 assert_eq!(
1882 position.realized_pnl,
1883 Some(Money::from("-42.00000000 USDT"))
1884 );
1885 assert_eq!(
1886 position.commissions(),
1887 vec![Money::from("42.00000000 USDT")]
1888 );
1889 }
1890
1891 #[rstest]
1892 fn test_calculate_unrealized_pnl_for_short(currency_pair_btcusdt: CurrencyPair) {
1893 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1894 let order = OrderTestBuilder::new(OrderType::Market)
1895 .instrument_id(btcusdt.id())
1896 .side(OrderSide::Sell)
1897 .quantity(Quantity::from("5.912000"))
1898 .build();
1899 let commission =
1900 calculate_commission(&btcusdt, order.quantity(), Price::from("10505.60"), None);
1901 let fill = TestOrderEventStubs::filled(
1902 &order,
1903 &btcusdt,
1904 Some(TradeId::new("1")),
1905 Some(PositionId::new("P-123456")),
1906 Some(Price::from("10505.60")),
1907 None,
1908 None,
1909 Some(commission),
1910 None,
1911 None,
1912 );
1913 let position = Position::new(&btcusdt, fill.into());
1914 let pnl = position.unrealized_pnl(Price::from("10407.15"));
1915 assert_eq!(pnl, Money::from("582.03640000 USDT"));
1916 assert_eq!(
1917 position.realized_pnl,
1918 Some(Money::from("-62.10910720 USDT"))
1919 );
1920 assert_eq!(
1921 position.commissions(),
1922 vec![Money::from("62.10910720 USDT")]
1923 );
1924 }
1925
1926 #[rstest]
1927 fn test_calculate_unrealized_pnl_for_long_inverse(xbtusd_bitmex: CryptoPerpetual) {
1928 let xbtusd_bitmex = InstrumentAny::CryptoPerpetual(xbtusd_bitmex);
1929 let order = OrderTestBuilder::new(OrderType::Market)
1930 .instrument_id(xbtusd_bitmex.id())
1931 .side(OrderSide::Buy)
1932 .quantity(Quantity::from("100000"))
1933 .build();
1934 let commission = calculate_commission(
1935 &xbtusd_bitmex,
1936 order.quantity(),
1937 Price::from("10500.0"),
1938 None,
1939 );
1940 let fill = TestOrderEventStubs::filled(
1941 &order,
1942 &xbtusd_bitmex,
1943 Some(TradeId::new("1")),
1944 Some(PositionId::new("P-123456")),
1945 Some(Price::from("10500.00")),
1946 None,
1947 None,
1948 Some(commission),
1949 None,
1950 None,
1951 );
1952
1953 let position = Position::new(&xbtusd_bitmex, fill.into());
1954 let pnl = position.unrealized_pnl(Price::from("11505.60"));
1955 assert_eq!(pnl, Money::from("0.83238969 BTC"));
1956 assert_eq!(position.realized_pnl, Some(Money::from("-0.00714286 BTC")));
1957 assert_eq!(position.commissions(), vec![Money::from("0.00714286 BTC")]);
1958 }
1959
1960 #[rstest]
1961 fn test_calculate_unrealized_pnl_for_short_inverse(xbtusd_bitmex: CryptoPerpetual) {
1962 let xbtusd_bitmex = InstrumentAny::CryptoPerpetual(xbtusd_bitmex);
1963 let order = OrderTestBuilder::new(OrderType::Market)
1964 .instrument_id(xbtusd_bitmex.id())
1965 .side(OrderSide::Sell)
1966 .quantity(Quantity::from("1250000"))
1967 .build();
1968 let commission = calculate_commission(
1969 &xbtusd_bitmex,
1970 order.quantity(),
1971 Price::from("15500.00"),
1972 None,
1973 );
1974 let fill = TestOrderEventStubs::filled(
1975 &order,
1976 &xbtusd_bitmex,
1977 Some(TradeId::new("1")),
1978 Some(PositionId::new("P-123456")),
1979 Some(Price::from("15500.00")),
1980 None,
1981 None,
1982 Some(commission),
1983 None,
1984 None,
1985 );
1986 let position = Position::new(&xbtusd_bitmex, fill.into());
1987 let pnl = position.unrealized_pnl(Price::from("12506.65"));
1988
1989 assert_eq!(pnl, Money::from("19.30166700 BTC"));
1990 assert_eq!(position.realized_pnl, Some(Money::from("-0.06048387 BTC")));
1991 assert_eq!(position.commissions(), vec![Money::from("0.06048387 BTC")]);
1992 }
1993
1994 #[rstest]
1995 #[case(OrderSide::Buy, 25, 25.0)]
1996 #[case(OrderSide::Sell,25,-25.0)]
1997 fn test_signed_qty_decimal_qty_for_equity(
1998 #[case] order_side: OrderSide,
1999 #[case] quantity: i64,
2000 #[case] expected: f64,
2001 audusd_sim: CurrencyPair,
2002 ) {
2003 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2004 let order = OrderTestBuilder::new(OrderType::Market)
2005 .instrument_id(audusd_sim.id())
2006 .side(order_side)
2007 .quantity(Quantity::from(quantity))
2008 .build();
2009
2010 let commission =
2011 calculate_commission(&audusd_sim, order.quantity(), Price::from("1.0"), None);
2012 let fill = TestOrderEventStubs::filled(
2013 &order,
2014 &audusd_sim,
2015 None,
2016 Some(PositionId::from("P-123456")),
2017 None,
2018 None,
2019 None,
2020 Some(commission),
2021 None,
2022 None,
2023 );
2024 let position = Position::new(&audusd_sim, fill.into());
2025 assert_eq!(position.signed_qty, expected);
2026 }
2027
2028 #[rstest]
2029 fn test_position_with_commission_none(audusd_sim: CurrencyPair) {
2030 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2031 let mut fill = OrderFilled::default();
2032 fill.position_id = Some(PositionId::from("1"));
2033
2034 let position = Position::new(&audusd_sim, fill);
2035 assert_eq!(position.realized_pnl, Some(Money::from("0 USD")));
2036 }
2037
2038 #[rstest]
2039 fn test_position_with_commission_zero(audusd_sim: CurrencyPair) {
2040 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2041 let mut fill = OrderFilled::default();
2042 fill.position_id = Some(PositionId::from("1"));
2043 fill.commission = Some(Money::from("0 USD"));
2044
2045 let position = Position::new(&audusd_sim, fill);
2046 assert_eq!(position.realized_pnl, Some(Money::from("0 USD")));
2047 }
2048
2049 #[rstest]
2050 fn test_cache_purge_order_events() {
2051 let audusd_sim = audusd_sim();
2052 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2053
2054 let order1 = OrderTestBuilder::new(OrderType::Market)
2055 .client_order_id(ClientOrderId::new("O-1"))
2056 .instrument_id(audusd_sim.id())
2057 .side(OrderSide::Buy)
2058 .quantity(Quantity::from(50_000))
2059 .build();
2060
2061 let order2 = OrderTestBuilder::new(OrderType::Market)
2062 .client_order_id(ClientOrderId::new("O-2"))
2063 .instrument_id(audusd_sim.id())
2064 .side(OrderSide::Buy)
2065 .quantity(Quantity::from(50_000))
2066 .build();
2067
2068 let position_id = PositionId::new("P-123456");
2069
2070 let fill1 = TestOrderEventStubs::filled(
2071 &order1,
2072 &audusd_sim,
2073 Some(TradeId::new("1")),
2074 Some(position_id),
2075 Some(Price::from("1.00001")),
2076 None,
2077 None,
2078 None,
2079 None,
2080 None,
2081 );
2082
2083 let mut position = Position::new(&audusd_sim, fill1.into());
2084
2085 let fill2 = TestOrderEventStubs::filled(
2086 &order2,
2087 &audusd_sim,
2088 Some(TradeId::new("2")),
2089 Some(position_id),
2090 Some(Price::from("1.00002")),
2091 None,
2092 None,
2093 None,
2094 None,
2095 None,
2096 );
2097
2098 position.apply(&fill2.into());
2099 position.purge_events_for_order(order1.client_order_id());
2100
2101 assert_eq!(position.events.len(), 1);
2102 assert_eq!(position.trade_ids.len(), 1);
2103 assert_eq!(position.events[0].client_order_id, order2.client_order_id());
2104 assert_eq!(position.trade_ids[0], TradeId::new("2"));
2105 }
2106
2107 #[rstest]
2108 fn test_purge_all_events_returns_none_for_last_event_and_trade_id() {
2109 let audusd_sim = audusd_sim();
2110 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2111
2112 let order = OrderTestBuilder::new(OrderType::Market)
2113 .client_order_id(ClientOrderId::new("O-1"))
2114 .instrument_id(audusd_sim.id())
2115 .side(OrderSide::Buy)
2116 .quantity(Quantity::from(100_000))
2117 .build();
2118
2119 let position_id = PositionId::new("P-123456");
2120 let fill = TestOrderEventStubs::filled(
2121 &order,
2122 &audusd_sim,
2123 Some(TradeId::new("1")),
2124 Some(position_id),
2125 Some(Price::from("1.00050")),
2126 None,
2127 None,
2128 None,
2129 None,
2130 None,
2131 );
2132
2133 let mut position = Position::new(&audusd_sim, fill.into());
2134
2135 assert_eq!(position.events.len(), 1);
2136 assert!(position.last_event().is_some());
2137 assert!(position.last_trade_id().is_some());
2138
2139 position.purge_events_for_order(order.client_order_id());
2140
2141 assert_eq!(position.events.len(), 0);
2142 assert_eq!(position.trade_ids.len(), 0);
2143 assert!(position.last_event().is_none());
2144 assert!(position.last_trade_id().is_none());
2145 }
2146}