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