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