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