1use std::{
19 fmt::Display,
20 hash::{Hash, Hasher},
21};
22
23use ahash::{AHashMap, AHashSet};
24use nautilus_core::{
25 UUID4, UnixNanos,
26 correctness::{FAILED, check_equal, check_predicate_true},
27};
28use rust_decimal::prelude::ToPrimitive;
29use serde::{Deserialize, Serialize};
30use ustr::Ustr;
31
32use crate::{
33 enums::{InstrumentClass, OrderSide, OrderSideSpecified, PositionAdjustmentType, PositionSide},
34 events::{OrderFilled, PositionAdjusted},
35 identifiers::{
36 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, Symbol, TradeId, TraderId,
37 Venue, VenueOrderId,
38 },
39 instruments::{Instrument, InstrumentAny},
40 types::{Currency, Money, Price, Quantity},
41};
42
43#[repr(C)]
48#[derive(Debug, Clone, Serialize, Deserialize)]
49#[cfg_attr(
50 feature = "python",
51 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
52)]
53pub struct Position {
54 pub events: Vec<OrderFilled>,
55 pub adjustments: Vec<PositionAdjusted>,
56 pub trader_id: TraderId,
57 pub strategy_id: StrategyId,
58 pub instrument_id: InstrumentId,
59 pub id: PositionId,
60 pub account_id: AccountId,
61 pub opening_order_id: ClientOrderId,
62 pub closing_order_id: Option<ClientOrderId>,
63 pub entry: OrderSide,
64 pub side: PositionSide,
65 pub signed_qty: f64,
66 pub quantity: Quantity,
67 pub peak_qty: Quantity,
68 pub price_precision: u8,
69 pub size_precision: u8,
70 pub multiplier: Quantity,
71 pub is_inverse: bool,
72 pub is_currency_pair: bool,
73 pub instrument_class: InstrumentClass,
74 pub base_currency: Option<Currency>,
75 pub quote_currency: Currency,
76 pub settlement_currency: Currency,
77 pub ts_init: UnixNanos,
78 pub ts_opened: UnixNanos,
79 pub ts_last: UnixNanos,
80 pub ts_closed: Option<UnixNanos>,
81 pub duration_ns: u64,
82 pub avg_px_open: f64,
83 pub avg_px_close: Option<f64>,
84 pub realized_return: f64,
85 pub realized_pnl: Option<Money>,
86 pub trade_ids: Vec<TradeId>,
87 pub buy_qty: Quantity,
88 pub sell_qty: Quantity,
89 pub commissions: AHashMap<Currency, Money>,
90}
91
92impl Position {
93 pub fn new(instrument: &InstrumentAny, fill: OrderFilled) -> Self {
102 check_equal(
103 &instrument.id(),
104 &fill.instrument_id,
105 "instrument.id()",
106 "fill.instrument_id",
107 )
108 .expect(FAILED);
109 assert_ne!(fill.order_side, OrderSide::NoOrderSide);
110
111 let position_id = fill.position_id.expect("No position ID to open `Position`");
112
113 let mut item = Self {
114 events: Vec::<OrderFilled>::new(),
115 adjustments: Vec::<PositionAdjusted>::new(),
116 trade_ids: Vec::<TradeId>::new(),
117 buy_qty: Quantity::zero(instrument.size_precision()),
118 sell_qty: Quantity::zero(instrument.size_precision()),
119 commissions: AHashMap::<Currency, Money>::new(),
120 trader_id: fill.trader_id,
121 strategy_id: fill.strategy_id,
122 instrument_id: fill.instrument_id,
123 id: position_id,
124 account_id: fill.account_id,
125 opening_order_id: fill.client_order_id,
126 closing_order_id: None,
127 entry: fill.order_side,
128 side: PositionSide::Flat,
129 signed_qty: 0.0,
130 quantity: fill.last_qty,
131 peak_qty: fill.last_qty,
132 price_precision: instrument.price_precision(),
133 size_precision: instrument.size_precision(),
134 multiplier: instrument.multiplier(),
135 is_inverse: instrument.is_inverse(),
136 is_currency_pair: matches!(instrument, InstrumentAny::CurrencyPair(_)),
137 instrument_class: instrument.instrument_class(),
138 base_currency: instrument.base_currency(),
139 quote_currency: instrument.quote_currency(),
140 settlement_currency: instrument.cost_currency(),
141 ts_init: fill.ts_init,
142 ts_opened: fill.ts_event,
143 ts_last: fill.ts_event,
144 ts_closed: None,
145 duration_ns: 0,
146 avg_px_open: fill.last_px.as_f64(),
147 avg_px_close: None,
148 realized_return: 0.0,
149 realized_pnl: None,
150 };
151 item.apply(&fill);
152 item
153 }
154
155 pub fn purge_events_for_order(&mut self, client_order_id: ClientOrderId) {
166 let filtered_events: Vec<OrderFilled> = self
167 .events
168 .iter()
169 .filter(|e| e.client_order_id != client_order_id)
170 .copied()
171 .collect();
172
173 let preserved_adjustments: Vec<PositionAdjusted> = self
176 .adjustments
177 .iter()
178 .filter(|adj| {
179 adj.adjustment_type != PositionAdjustmentType::Commission
182 })
183 .copied()
184 .collect();
185
186 if filtered_events.is_empty() {
188 log::warn!(
189 "Position {} has no fills remaining after purging order {}; consider closing the position instead",
190 self.id,
191 client_order_id
192 );
193 self.events.clear();
194 self.trade_ids.clear();
195 self.adjustments.clear();
196 self.buy_qty = Quantity::zero(self.size_precision);
197 self.sell_qty = Quantity::zero(self.size_precision);
198 self.commissions.clear();
199 self.signed_qty = 0.0;
200 self.quantity = Quantity::zero(self.size_precision);
201 self.side = PositionSide::Flat;
202 self.avg_px_close = None;
203 self.realized_pnl = None;
204 self.realized_return = 0.0;
205 self.ts_opened = UnixNanos::default();
206 self.ts_last = UnixNanos::default();
207 self.ts_closed = Some(UnixNanos::default());
208 self.duration_ns = 0;
209 return;
210 }
211
212 let position_id = self.id;
214 let size_precision = self.size_precision;
215
216 self.events = Vec::new();
218 self.trade_ids = Vec::new();
219 self.adjustments = Vec::new();
220 self.buy_qty = Quantity::zero(size_precision);
221 self.sell_qty = Quantity::zero(size_precision);
222 self.commissions.clear();
223 self.signed_qty = 0.0;
224 self.quantity = Quantity::zero(size_precision);
225 self.peak_qty = Quantity::zero(size_precision);
226 self.side = PositionSide::Flat;
227 self.avg_px_open = 0.0;
228 self.avg_px_close = None;
229 self.realized_pnl = None;
230 self.realized_return = 0.0;
231
232 let first_event = &filtered_events[0];
234 self.entry = first_event.order_side;
235 self.opening_order_id = first_event.client_order_id;
236 self.ts_opened = first_event.ts_event;
237 self.ts_init = first_event.ts_init;
238 self.closing_order_id = None;
239 self.ts_closed = None;
240 self.duration_ns = 0;
241
242 for event in filtered_events {
244 self.apply(&event);
245 }
246
247 for adjustment in preserved_adjustments {
249 self.apply_adjustment(adjustment);
250 }
251
252 log::info!(
253 "Purged fills for order {} from position {}; recalculated state: qty={}, signed_qty={}, side={:?}",
254 client_order_id,
255 position_id,
256 self.quantity,
257 self.signed_qty,
258 self.side
259 );
260 }
261
262 pub fn apply(&mut self, fill: &OrderFilled) {
268 check_predicate_true(
269 !self.trade_ids.contains(&fill.trade_id),
270 "`fill.trade_id` already contained in `trade_ids",
271 )
272 .expect(FAILED);
273 check_predicate_true(fill.ts_event >= self.ts_opened, "fill.ts_event < ts_opened")
274 .expect(FAILED);
275
276 if self.side == PositionSide::Flat {
277 self.events.clear();
279 self.trade_ids.clear();
280 self.adjustments.clear();
281 self.buy_qty = Quantity::zero(self.size_precision);
282 self.sell_qty = Quantity::zero(self.size_precision);
283 self.commissions.clear();
284 self.opening_order_id = fill.client_order_id;
285 self.closing_order_id = None;
286 self.peak_qty = Quantity::zero(self.size_precision);
287 self.ts_init = fill.ts_init;
288 self.ts_opened = fill.ts_event;
289 self.ts_closed = None;
290 self.duration_ns = 0;
291 self.avg_px_open = fill.last_px.as_f64();
292 self.avg_px_close = None;
293 self.realized_return = 0.0;
294 self.realized_pnl = None;
295 }
296
297 self.events.push(*fill);
298 self.trade_ids.push(fill.trade_id);
299
300 if let Some(commission) = fill.commission {
302 let commission_currency = commission.currency;
303 if let Some(existing_commission) = self.commissions.get_mut(&commission_currency) {
304 *existing_commission += commission;
305 } else {
306 self.commissions.insert(commission_currency, commission);
307 }
308 }
309
310 match fill.specified_side() {
312 OrderSideSpecified::Buy => {
313 self.handle_buy_order_fill(fill);
314 }
315 OrderSideSpecified::Sell => {
316 self.handle_sell_order_fill(fill);
317 }
318 }
319
320 if self.is_currency_pair
322 && let Some(commission) = fill.commission
323 && let Some(base_currency) = self.base_currency
324 && commission.currency == base_currency
325 {
326 let adjustment = PositionAdjusted::new(
327 self.trader_id,
328 self.strategy_id,
329 self.instrument_id,
330 self.id,
331 self.account_id,
332 PositionAdjustmentType::Commission,
333 Some(commission.as_decimal()),
334 None,
335 Some(Ustr::from(fill.client_order_id.as_ref())),
336 UUID4::new(),
337 fill.ts_event,
338 fill.ts_init,
339 );
340 self.apply_adjustment(adjustment);
341 }
342
343 self.quantity = Quantity::new(self.signed_qty.abs(), self.size_precision);
345 if self.quantity > self.peak_qty {
346 self.peak_qty = self.quantity;
347 }
348
349 if self.signed_qty > 0.0 {
350 self.entry = OrderSide::Buy;
351 self.side = PositionSide::Long;
352 } else if self.signed_qty < 0.0 {
353 self.entry = OrderSide::Sell;
354 self.side = PositionSide::Short;
355 } else {
356 self.side = PositionSide::Flat;
357 self.closing_order_id = Some(fill.client_order_id);
358 self.ts_closed = Some(fill.ts_event);
359 self.duration_ns = if let Some(ts_closed) = self.ts_closed {
360 ts_closed.as_u64() - self.ts_opened.as_u64()
361 } else {
362 0
363 };
364 }
365
366 self.ts_last = fill.ts_event;
367 }
368
369 fn handle_buy_order_fill(&mut self, fill: &OrderFilled) {
370 let mut realized_pnl = if let Some(commission) = fill.commission {
372 if commission.currency == self.settlement_currency {
373 -commission.as_f64()
374 } else {
375 0.0
376 }
377 } else {
378 0.0
379 };
380
381 let last_px = fill.last_px.as_f64();
382 let last_qty = fill.last_qty.as_f64();
383 let last_qty_object = fill.last_qty;
384
385 if self.signed_qty > 0.0 {
386 self.avg_px_open = self.calculate_avg_px_open_px(last_px, last_qty);
387 } else if self.signed_qty < 0.0 {
388 let avg_px_close = self.calculate_avg_px_close_px(last_px, last_qty);
390 self.avg_px_close = Some(avg_px_close);
391 self.realized_return = self
392 .calculate_return(self.avg_px_open, avg_px_close)
393 .unwrap_or_else(|e| {
394 log::error!("Error calculating return: {e}");
395 0.0
396 });
397 realized_pnl += self
398 .calculate_pnl_raw(self.avg_px_open, last_px, last_qty)
399 .unwrap_or_else(|e| {
400 log::error!("Error calculating PnL: {e}");
401 0.0
402 });
403 }
404
405 let current_pnl = self.realized_pnl.map_or(0.0, |p| p.as_f64());
406 self.realized_pnl = Some(Money::new(
407 current_pnl + realized_pnl,
408 self.settlement_currency,
409 ));
410
411 self.signed_qty += last_qty;
412 self.buy_qty += last_qty_object;
413 }
414
415 fn handle_sell_order_fill(&mut self, fill: &OrderFilled) {
416 let mut realized_pnl = if let Some(commission) = fill.commission {
418 if commission.currency == self.settlement_currency {
419 -commission.as_f64()
420 } else {
421 0.0
422 }
423 } else {
424 0.0
425 };
426
427 let last_px = fill.last_px.as_f64();
428 let last_qty = fill.last_qty.as_f64();
429 let last_qty_object = fill.last_qty;
430
431 if self.signed_qty < 0.0 {
432 self.avg_px_open = self.calculate_avg_px_open_px(last_px, last_qty);
433 } else if self.signed_qty > 0.0 {
434 let avg_px_close = self.calculate_avg_px_close_px(last_px, last_qty);
436 self.avg_px_close = Some(avg_px_close);
437 self.realized_return = self
438 .calculate_return(self.avg_px_open, avg_px_close)
439 .unwrap_or_else(|e| {
440 log::error!("Error calculating return: {e}");
441 0.0
442 });
443 realized_pnl += self
444 .calculate_pnl_raw(self.avg_px_open, last_px, last_qty)
445 .unwrap_or_else(|e| {
446 log::error!("Error calculating PnL: {e}");
447 0.0
448 });
449 }
450
451 let current_pnl = self.realized_pnl.map_or(0.0, |p| p.as_f64());
452 self.realized_pnl = Some(Money::new(
453 current_pnl + realized_pnl,
454 self.settlement_currency,
455 ));
456
457 self.signed_qty -= last_qty;
458 self.sell_qty += last_qty_object;
459 }
460
461 pub fn apply_adjustment(&mut self, adjustment: PositionAdjusted) {
474 if let Some(quantity_change) = adjustment.quantity_change {
476 self.signed_qty += quantity_change
477 .to_f64()
478 .expect("Failed to convert Decimal to f64");
479
480 self.quantity = Quantity::new(self.signed_qty.abs(), self.size_precision);
481
482 if self.quantity > self.peak_qty {
483 self.peak_qty = self.quantity;
484 }
485 }
486
487 if let Some(pnl_change) = adjustment.pnl_change {
489 let current_pnl = self.realized_pnl.map_or(0.0, |p| p.as_f64());
490 self.realized_pnl = Some(Money::new(
491 current_pnl + pnl_change.as_f64(),
492 self.settlement_currency,
493 ));
494 }
495
496 if self.signed_qty > 0.0 {
498 self.side = PositionSide::Long;
499 if self.entry == OrderSide::NoOrderSide {
500 self.entry = OrderSide::Buy;
501 }
502 } else if self.signed_qty < 0.0 {
503 self.side = PositionSide::Short;
504 if self.entry == OrderSide::NoOrderSide {
505 self.entry = OrderSide::Sell;
506 }
507 } else {
508 self.side = PositionSide::Flat;
509 }
510
511 self.adjustments.push(adjustment);
512 self.ts_last = adjustment.ts_event;
513 }
514
515 fn calculate_avg_px(
557 &self,
558 qty: f64,
559 avg_pg: f64,
560 last_px: f64,
561 last_qty: f64,
562 ) -> anyhow::Result<f64> {
563 if qty == 0.0 && last_qty == 0.0 {
564 anyhow::bail!("Cannot calculate average price: both quantities are zero");
565 }
566
567 if last_qty == 0.0 {
568 anyhow::bail!("Cannot calculate average price: fill quantity is zero");
569 }
570
571 if qty == 0.0 {
572 return Ok(last_px);
573 }
574
575 let start_cost = avg_pg * qty;
576 let event_cost = last_px * last_qty;
577 let total_qty = qty + last_qty;
578
579 if total_qty <= 0.0 {
581 anyhow::bail!(
582 "Total quantity unexpectedly zero or negative in average price calculation: qty={qty}, last_qty={last_qty}, total_qty={total_qty}"
583 );
584 }
585
586 Ok((start_cost + event_cost) / total_qty)
587 }
588
589 fn calculate_avg_px_open_px(&self, last_px: f64, last_qty: f64) -> f64 {
590 self.calculate_avg_px(self.quantity.as_f64(), self.avg_px_open, last_px, last_qty)
591 .unwrap_or_else(|e| {
592 log::error!("Error calculating average open price: {e}");
593 last_px
594 })
595 }
596
597 fn calculate_avg_px_close_px(&self, last_px: f64, last_qty: f64) -> f64 {
598 let Some(avg_px_close) = self.avg_px_close else {
599 return last_px;
600 };
601 let closing_qty = if self.side == PositionSide::Long {
602 self.sell_qty
603 } else {
604 self.buy_qty
605 };
606 self.calculate_avg_px(closing_qty.as_f64(), avg_px_close, last_px, last_qty)
607 .unwrap_or_else(|e| {
608 log::error!("Error calculating average close price: {e}");
609 last_px
610 })
611 }
612
613 fn calculate_points(&self, avg_px_open: f64, avg_px_close: f64) -> f64 {
614 match self.side {
615 PositionSide::Long => avg_px_close - avg_px_open,
616 PositionSide::Short => avg_px_open - avg_px_close,
617 _ => 0.0, }
619 }
620
621 fn calculate_points_inverse(&self, avg_px_open: f64, avg_px_close: f64) -> anyhow::Result<f64> {
622 const EPSILON: f64 = 1e-15;
624
625 if avg_px_open.abs() < EPSILON {
627 anyhow::bail!(
628 "Cannot calculate inverse points: open price is zero or too small ({avg_px_open})"
629 );
630 }
631 if avg_px_close.abs() < EPSILON {
632 anyhow::bail!(
633 "Cannot calculate inverse points: close price is zero or too small ({avg_px_close})"
634 );
635 }
636
637 let inverse_open = 1.0 / avg_px_open;
638 let inverse_close = 1.0 / avg_px_close;
639 let result = match self.side {
640 PositionSide::Long => inverse_open - inverse_close,
641 PositionSide::Short => inverse_close - inverse_open,
642 _ => 0.0, };
644 Ok(result)
645 }
646
647 fn calculate_return(&self, avg_px_open: f64, avg_px_close: f64) -> anyhow::Result<f64> {
648 if avg_px_open == 0.0 {
650 anyhow::bail!(
651 "Cannot calculate return: open price is zero (close price: {avg_px_close})"
652 );
653 }
654 Ok(self.calculate_points(avg_px_open, avg_px_close) / avg_px_open)
655 }
656
657 fn calculate_pnl_raw(
658 &self,
659 avg_px_open: f64,
660 avg_px_close: f64,
661 quantity: f64,
662 ) -> anyhow::Result<f64> {
663 let quantity = quantity.min(self.signed_qty.abs());
664 let result = if self.is_inverse {
665 let points = self.calculate_points_inverse(avg_px_open, avg_px_close)?;
666 quantity * self.multiplier.as_f64() * points
667 } else {
668 quantity * self.multiplier.as_f64() * self.calculate_points(avg_px_open, avg_px_close)
669 };
670 Ok(result)
671 }
672
673 #[must_use]
674 pub fn calculate_pnl(&self, avg_px_open: f64, avg_px_close: f64, quantity: Quantity) -> Money {
675 let pnl_raw = self
676 .calculate_pnl_raw(avg_px_open, avg_px_close, quantity.as_f64())
677 .unwrap_or_else(|e| {
678 log::error!("Error calculating PnL: {e}");
679 0.0
680 });
681 Money::new(pnl_raw, self.settlement_currency)
682 }
683
684 #[must_use]
685 pub fn total_pnl(&self, last: Price) -> Money {
686 let realized_pnl = self.realized_pnl.map_or(0.0, |pnl| pnl.as_f64());
687 Money::new(
688 realized_pnl + self.unrealized_pnl(last).as_f64(),
689 self.settlement_currency,
690 )
691 }
692
693 #[must_use]
694 pub fn unrealized_pnl(&self, last: Price) -> Money {
695 if self.side == PositionSide::Flat {
696 Money::new(0.0, self.settlement_currency)
697 } else {
698 let avg_px_open = self.avg_px_open;
699 let avg_px_close = last.as_f64();
700 let quantity = self.quantity.as_f64();
701 let pnl = self
702 .calculate_pnl_raw(avg_px_open, avg_px_close, quantity)
703 .unwrap_or_else(|e| {
704 log::error!("Error calculating unrealized PnL: {e}");
705 0.0
706 });
707 Money::new(pnl, self.settlement_currency)
708 }
709 }
710
711 pub fn closing_order_side(&self) -> OrderSide {
712 match self.side {
713 PositionSide::Long => OrderSide::Sell,
714 PositionSide::Short => OrderSide::Buy,
715 _ => OrderSide::NoOrderSide,
716 }
717 }
718
719 #[must_use]
720 pub fn is_opposite_side(&self, side: OrderSide) -> bool {
721 self.entry != side
722 }
723
724 #[must_use]
725 pub fn symbol(&self) -> Symbol {
726 self.instrument_id.symbol
727 }
728
729 #[must_use]
730 pub fn venue(&self) -> Venue {
731 self.instrument_id.venue
732 }
733
734 #[must_use]
735 pub fn event_count(&self) -> usize {
736 self.events.len()
737 }
738
739 #[must_use]
740 pub fn client_order_ids(&self) -> Vec<ClientOrderId> {
741 let mut result = self
743 .events
744 .iter()
745 .map(|event| event.client_order_id)
746 .collect::<AHashSet<ClientOrderId>>()
747 .into_iter()
748 .collect::<Vec<ClientOrderId>>();
749 result.sort_unstable();
750 result
751 }
752
753 #[must_use]
754 pub fn venue_order_ids(&self) -> Vec<VenueOrderId> {
755 let mut result = self
757 .events
758 .iter()
759 .map(|event| event.venue_order_id)
760 .collect::<AHashSet<VenueOrderId>>()
761 .into_iter()
762 .collect::<Vec<VenueOrderId>>();
763 result.sort_unstable();
764 result
765 }
766
767 #[must_use]
768 pub fn trade_ids(&self) -> Vec<TradeId> {
769 let mut result = self
770 .events
771 .iter()
772 .map(|event| event.trade_id)
773 .collect::<AHashSet<TradeId>>()
774 .into_iter()
775 .collect::<Vec<TradeId>>();
776 result.sort_unstable();
777 result
778 }
779
780 #[must_use]
786 pub fn notional_value(&self, last: Price) -> Money {
787 if self.is_inverse {
788 Money::new(
789 self.quantity.as_f64() * self.multiplier.as_f64() * (1.0 / last.as_f64()),
790 self.base_currency.unwrap(),
791 )
792 } else {
793 Money::new(
794 self.quantity.as_f64() * last.as_f64() * self.multiplier.as_f64(),
795 self.quote_currency,
796 )
797 }
798 }
799
800 #[must_use]
802 pub fn last_event(&self) -> Option<OrderFilled> {
803 self.events.last().copied()
804 }
805
806 #[must_use]
807 pub fn last_trade_id(&self) -> Option<TradeId> {
808 self.trade_ids.last().copied()
809 }
810
811 #[must_use]
812 pub fn is_long(&self) -> bool {
813 self.side == PositionSide::Long
814 }
815
816 #[must_use]
817 pub fn is_short(&self) -> bool {
818 self.side == PositionSide::Short
819 }
820
821 #[must_use]
822 pub fn is_open(&self) -> bool {
823 self.side != PositionSide::Flat && self.ts_closed.is_none()
824 }
825
826 #[must_use]
827 pub fn is_closed(&self) -> bool {
828 self.side == PositionSide::Flat && self.ts_closed.is_some()
829 }
830
831 #[must_use]
832 pub fn commissions(&self) -> Vec<Money> {
833 self.commissions.values().copied().collect()
834 }
835}
836
837impl PartialEq<Self> for Position {
838 fn eq(&self, other: &Self) -> bool {
839 self.id == other.id
840 }
841}
842
843impl Eq for Position {}
844
845impl Hash for Position {
846 fn hash<H: Hasher>(&self, state: &mut H) {
847 self.id.hash(state);
848 }
849}
850
851impl Display for Position {
852 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
853 let quantity_str = if self.quantity != Quantity::zero(self.price_precision) {
854 self.quantity.to_formatted_string() + " "
855 } else {
856 String::new()
857 };
858 write!(
859 f,
860 "Position({} {}{}, id={})",
861 self.side, quantity_str, self.instrument_id, self.id
862 )
863 }
864}
865
866#[cfg(test)]
867mod tests {
868 use std::str::FromStr;
869
870 use nautilus_core::UnixNanos;
871 use rstest::rstest;
872
873 use crate::{
874 enums::{LiquiditySide, OrderSide, OrderType, PositionAdjustmentType, PositionSide},
875 events::OrderFilled,
876 identifiers::{
877 AccountId, ClientOrderId, PositionId, StrategyId, TradeId, VenueOrderId, stubs::uuid4,
878 },
879 instruments::{CryptoPerpetual, CurrencyPair, Instrument, InstrumentAny, stubs::*},
880 orders::{Order, builder::OrderTestBuilder, stubs::TestOrderEventStubs},
881 position::Position,
882 stubs::*,
883 types::{Currency, Money, Price, Quantity},
884 };
885
886 #[rstest]
887 fn test_position_long_display(stub_position_long: Position) {
888 let display = format!("{stub_position_long}");
889 assert_eq!(display, "Position(LONG 1 AUD/USD.SIM, id=1)");
890 }
891
892 #[rstest]
893 fn test_position_short_display(stub_position_short: Position) {
894 let display = format!("{stub_position_short}");
895 assert_eq!(display, "Position(SHORT 1 AUD/USD.SIM, id=1)");
896 }
897
898 #[rstest]
899 #[should_panic(expected = "`fill.trade_id` already contained in `trade_ids")]
900 fn test_two_trades_with_same_trade_id_error(audusd_sim: CurrencyPair) {
901 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
902 let order1 = OrderTestBuilder::new(OrderType::Market)
903 .instrument_id(audusd_sim.id())
904 .side(OrderSide::Buy)
905 .quantity(Quantity::from(100_000))
906 .build();
907 let order2 = OrderTestBuilder::new(OrderType::Market)
908 .instrument_id(audusd_sim.id())
909 .side(OrderSide::Buy)
910 .quantity(Quantity::from(100_000))
911 .build();
912 let fill1 = TestOrderEventStubs::filled(
913 &order1,
914 &audusd_sim,
915 Some(TradeId::new("1")),
916 None,
917 Some(Price::from("1.00001")),
918 None,
919 None,
920 None,
921 None,
922 None,
923 );
924 let fill2 = TestOrderEventStubs::filled(
925 &order2,
926 &audusd_sim,
927 Some(TradeId::new("1")),
928 None,
929 Some(Price::from("1.00002")),
930 None,
931 None,
932 None,
933 None,
934 None,
935 );
936 let mut position = Position::new(&audusd_sim, fill1.into());
937 position.apply(&fill2.into());
938 }
939
940 #[rstest]
941 fn test_position_filled_with_buy_order(audusd_sim: CurrencyPair) {
942 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
943 let order = OrderTestBuilder::new(OrderType::Market)
944 .instrument_id(audusd_sim.id())
945 .side(OrderSide::Buy)
946 .quantity(Quantity::from(100_000))
947 .build();
948 let fill = TestOrderEventStubs::filled(
949 &order,
950 &audusd_sim,
951 None,
952 None,
953 Some(Price::from("1.00001")),
954 None,
955 None,
956 None,
957 None,
958 None,
959 );
960 let last_price = Price::from_str("1.0005").unwrap();
961 let position = Position::new(&audusd_sim, fill.into());
962 assert_eq!(position.symbol(), audusd_sim.id().symbol);
963 assert_eq!(position.venue(), audusd_sim.id().venue);
964 assert_eq!(position.closing_order_side(), OrderSide::Sell);
965 assert!(!position.is_opposite_side(OrderSide::Buy));
966 assert_eq!(position, position); assert!(position.closing_order_id.is_none());
968 assert_eq!(position.quantity, Quantity::from(100_000));
969 assert_eq!(position.peak_qty, Quantity::from(100_000));
970 assert_eq!(position.size_precision, 0);
971 assert_eq!(position.signed_qty, 100_000.0);
972 assert_eq!(position.entry, OrderSide::Buy);
973 assert_eq!(position.side, PositionSide::Long);
974 assert_eq!(position.ts_opened.as_u64(), 0);
975 assert_eq!(position.duration_ns, 0);
976 assert_eq!(position.avg_px_open, 1.00001);
977 assert_eq!(position.event_count(), 1);
978 assert_eq!(position.id, PositionId::new("1"));
979 assert_eq!(position.events.len(), 1);
980 assert!(position.is_long());
981 assert!(!position.is_short());
982 assert!(position.is_open());
983 assert!(!position.is_closed());
984 assert_eq!(position.realized_return, 0.0);
985 assert_eq!(position.realized_pnl, Some(Money::from("-2.0 USD")));
986 assert_eq!(position.unrealized_pnl(last_price), Money::from("49.0 USD"));
987 assert_eq!(position.total_pnl(last_price), Money::from("47.0 USD"));
988 assert_eq!(position.commissions(), vec![Money::from("2.0 USD")]);
989 assert_eq!(
990 format!("{position}"),
991 "Position(LONG 100_000 AUD/USD.SIM, id=1)"
992 );
993 }
994
995 #[rstest]
996 fn test_position_filled_with_sell_order(audusd_sim: CurrencyPair) {
997 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
998 let order = OrderTestBuilder::new(OrderType::Market)
999 .instrument_id(audusd_sim.id())
1000 .side(OrderSide::Sell)
1001 .quantity(Quantity::from(100_000))
1002 .build();
1003 let fill = TestOrderEventStubs::filled(
1004 &order,
1005 &audusd_sim,
1006 None,
1007 None,
1008 Some(Price::from("1.00001")),
1009 None,
1010 None,
1011 None,
1012 None,
1013 None,
1014 );
1015 let last_price = Price::from_str("1.00050").unwrap();
1016 let position = Position::new(&audusd_sim, fill.into());
1017 assert_eq!(position.symbol(), audusd_sim.id().symbol);
1018 assert_eq!(position.venue(), audusd_sim.id().venue);
1019 assert_eq!(position.closing_order_side(), OrderSide::Buy);
1020 assert!(!position.is_opposite_side(OrderSide::Sell));
1021 assert_eq!(position, position); assert!(position.closing_order_id.is_none());
1023 assert_eq!(position.quantity, Quantity::from(100_000));
1024 assert_eq!(position.peak_qty, Quantity::from(100_000));
1025 assert_eq!(position.signed_qty, -100_000.0);
1026 assert_eq!(position.entry, OrderSide::Sell);
1027 assert_eq!(position.side, PositionSide::Short);
1028 assert_eq!(position.ts_opened.as_u64(), 0);
1029 assert_eq!(position.avg_px_open, 1.00001);
1030 assert_eq!(position.event_count(), 1);
1031 assert_eq!(position.id, PositionId::new("1"));
1032 assert_eq!(position.events.len(), 1);
1033 assert!(!position.is_long());
1034 assert!(position.is_short());
1035 assert!(position.is_open());
1036 assert!(!position.is_closed());
1037 assert_eq!(position.realized_return, 0.0);
1038 assert_eq!(position.realized_pnl, Some(Money::from("-2.0 USD")));
1039 assert_eq!(
1040 position.unrealized_pnl(last_price),
1041 Money::from("-49.0 USD")
1042 );
1043 assert_eq!(position.total_pnl(last_price), Money::from("-51.0 USD"));
1044 assert_eq!(position.commissions(), vec![Money::from("2.0 USD")]);
1045 assert_eq!(
1046 format!("{position}"),
1047 "Position(SHORT 100_000 AUD/USD.SIM, id=1)"
1048 );
1049 }
1050
1051 #[rstest]
1052 fn test_position_partial_fills_with_buy_order(audusd_sim: CurrencyPair) {
1053 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1054 let order = OrderTestBuilder::new(OrderType::Market)
1055 .instrument_id(audusd_sim.id())
1056 .side(OrderSide::Buy)
1057 .quantity(Quantity::from(100_000))
1058 .build();
1059 let fill = TestOrderEventStubs::filled(
1060 &order,
1061 &audusd_sim,
1062 None,
1063 None,
1064 Some(Price::from("1.00001")),
1065 Some(Quantity::from(50_000)),
1066 None,
1067 None,
1068 None,
1069 None,
1070 );
1071 let last_price = Price::from_str("1.00048").unwrap();
1072 let position = Position::new(&audusd_sim, fill.into());
1073 assert_eq!(position.quantity, Quantity::from(50_000));
1074 assert_eq!(position.peak_qty, Quantity::from(50_000));
1075 assert_eq!(position.side, PositionSide::Long);
1076 assert_eq!(position.signed_qty, 50000.0);
1077 assert_eq!(position.avg_px_open, 1.00001);
1078 assert_eq!(position.event_count(), 1);
1079 assert_eq!(position.ts_opened.as_u64(), 0);
1080 assert!(position.is_long());
1081 assert!(!position.is_short());
1082 assert!(position.is_open());
1083 assert!(!position.is_closed());
1084 assert_eq!(position.realized_return, 0.0);
1085 assert_eq!(position.realized_pnl, Some(Money::from("-2.0 USD")));
1086 assert_eq!(position.unrealized_pnl(last_price), Money::from("23.5 USD"));
1087 assert_eq!(position.total_pnl(last_price), Money::from("21.5 USD"));
1088 assert_eq!(position.commissions(), vec![Money::from("2.0 USD")]);
1089 assert_eq!(
1090 format!("{position}"),
1091 "Position(LONG 50_000 AUD/USD.SIM, id=1)"
1092 );
1093 }
1094
1095 #[rstest]
1096 fn test_position_partial_fills_with_two_sell_orders(audusd_sim: CurrencyPair) {
1097 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1098 let order = OrderTestBuilder::new(OrderType::Market)
1099 .instrument_id(audusd_sim.id())
1100 .side(OrderSide::Sell)
1101 .quantity(Quantity::from(100_000))
1102 .build();
1103 let fill1 = TestOrderEventStubs::filled(
1104 &order,
1105 &audusd_sim,
1106 Some(TradeId::new("1")),
1107 None,
1108 Some(Price::from("1.00001")),
1109 Some(Quantity::from(50_000)),
1110 None,
1111 None,
1112 None,
1113 None,
1114 );
1115 let fill2 = TestOrderEventStubs::filled(
1116 &order,
1117 &audusd_sim,
1118 Some(TradeId::new("2")),
1119 None,
1120 Some(Price::from("1.00002")),
1121 Some(Quantity::from(50_000)),
1122 None,
1123 None,
1124 None,
1125 None,
1126 );
1127 let last_price = Price::from_str("1.0005").unwrap();
1128 let mut position = Position::new(&audusd_sim, fill1.into());
1129 position.apply(&fill2.into());
1130
1131 assert_eq!(position.quantity, Quantity::from(100_000));
1132 assert_eq!(position.peak_qty, Quantity::from(100_000));
1133 assert_eq!(position.side, PositionSide::Short);
1134 assert_eq!(position.signed_qty, -100_000.0);
1135 assert_eq!(position.avg_px_open, 1.000_015);
1136 assert_eq!(position.event_count(), 2);
1137 assert_eq!(position.ts_opened, 0);
1138 assert!(position.is_short());
1139 assert!(!position.is_long());
1140 assert!(position.is_open());
1141 assert!(!position.is_closed());
1142 assert_eq!(position.realized_return, 0.0);
1143 assert_eq!(position.realized_pnl, Some(Money::from("-4.0 USD")));
1144 assert_eq!(
1145 position.unrealized_pnl(last_price),
1146 Money::from("-48.5 USD")
1147 );
1148 assert_eq!(position.total_pnl(last_price), Money::from("-52.5 USD"));
1149 assert_eq!(position.commissions(), vec![Money::from("4.0 USD")]);
1150 }
1151
1152 #[rstest]
1153 pub fn test_position_filled_with_buy_order_then_sell_order(audusd_sim: CurrencyPair) {
1154 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1155 let order = OrderTestBuilder::new(OrderType::Market)
1156 .instrument_id(audusd_sim.id())
1157 .side(OrderSide::Buy)
1158 .quantity(Quantity::from(150_000))
1159 .build();
1160 let fill = TestOrderEventStubs::filled(
1161 &order,
1162 &audusd_sim,
1163 Some(TradeId::new("1")),
1164 Some(PositionId::new("P-1")),
1165 Some(Price::from("1.00001")),
1166 None,
1167 None,
1168 None,
1169 Some(UnixNanos::from(1_000_000_000)),
1170 None,
1171 );
1172 let mut position = Position::new(&audusd_sim, fill.into());
1173
1174 let fill2 = OrderFilled::new(
1175 order.trader_id(),
1176 StrategyId::new("S-001"),
1177 order.instrument_id(),
1178 order.client_order_id(),
1179 VenueOrderId::from("2"),
1180 order.account_id().unwrap_or(AccountId::new("SIM-001")),
1181 TradeId::new("2"),
1182 OrderSide::Sell,
1183 OrderType::Market,
1184 order.quantity(),
1185 Price::from("1.00011"),
1186 audusd_sim.quote_currency(),
1187 LiquiditySide::Taker,
1188 uuid4(),
1189 2_000_000_000.into(),
1190 0.into(),
1191 false,
1192 Some(PositionId::new("T1")),
1193 Some(Money::from("0.0 USD")),
1194 );
1195 position.apply(&fill2);
1196 let last = Price::from_str("1.0005").unwrap();
1197
1198 assert!(position.is_opposite_side(fill2.order_side));
1199 assert_eq!(
1200 position.quantity,
1201 Quantity::zero(audusd_sim.price_precision())
1202 );
1203 assert_eq!(position.size_precision, 0);
1204 assert_eq!(position.signed_qty, 0.0);
1205 assert_eq!(position.side, PositionSide::Flat);
1206 assert_eq!(position.ts_opened, 1_000_000_000);
1207 assert_eq!(position.ts_closed, Some(UnixNanos::from(2_000_000_000)));
1208 assert_eq!(position.duration_ns, 1_000_000_000);
1209 assert_eq!(position.avg_px_open, 1.00001);
1210 assert_eq!(position.avg_px_close, Some(1.00011));
1211 assert!(!position.is_long());
1212 assert!(!position.is_short());
1213 assert!(!position.is_open());
1214 assert!(position.is_closed());
1215 assert_eq!(position.realized_return, 9.999_900_000_998_888e-5);
1216 assert_eq!(position.realized_pnl, Some(Money::from("13.0 USD")));
1217 assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1218 assert_eq!(position.commissions(), vec![Money::from("2 USD")]);
1219 assert_eq!(position.total_pnl(last), Money::from("13 USD"));
1220 assert_eq!(format!("{position}"), "Position(FLAT AUD/USD.SIM, id=P-1)");
1221 }
1222
1223 #[rstest]
1224 pub fn test_position_filled_with_sell_order_then_buy_order(audusd_sim: CurrencyPair) {
1225 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1226 let order1 = OrderTestBuilder::new(OrderType::Market)
1227 .instrument_id(audusd_sim.id())
1228 .side(OrderSide::Sell)
1229 .quantity(Quantity::from(100_000))
1230 .build();
1231 let order2 = OrderTestBuilder::new(OrderType::Market)
1232 .instrument_id(audusd_sim.id())
1233 .side(OrderSide::Buy)
1234 .quantity(Quantity::from(100_000))
1235 .build();
1236 let fill1 = TestOrderEventStubs::filled(
1237 &order1,
1238 &audusd_sim,
1239 None,
1240 Some(PositionId::new("P-19700101-000000-001-001-1")),
1241 Some(Price::from("1.0")),
1242 None,
1243 None,
1244 None,
1245 None,
1246 None,
1247 );
1248 let mut position = Position::new(&audusd_sim, fill1.into());
1249 let fill2 = TestOrderEventStubs::filled(
1251 &order2,
1252 &audusd_sim,
1253 Some(TradeId::new("1")),
1254 Some(PositionId::new("P-19700101-000000-001-001-1")),
1255 Some(Price::from("1.00001")),
1256 Some(Quantity::from(50_000)),
1257 None,
1258 None,
1259 None,
1260 None,
1261 );
1262 let fill3 = TestOrderEventStubs::filled(
1263 &order2,
1264 &audusd_sim,
1265 Some(TradeId::new("2")),
1266 Some(PositionId::new("P-19700101-000000-001-001-1")),
1267 Some(Price::from("1.00003")),
1268 Some(Quantity::from(50_000)),
1269 None,
1270 None,
1271 None,
1272 None,
1273 );
1274 let last = Price::from("1.0005");
1275 position.apply(&fill2.into());
1276 position.apply(&fill3.into());
1277
1278 assert_eq!(
1279 position.quantity,
1280 Quantity::zero(audusd_sim.price_precision())
1281 );
1282 assert_eq!(position.side, PositionSide::Flat);
1283 assert_eq!(position.ts_opened, 0);
1284 assert_eq!(position.avg_px_open, 1.0);
1285 assert_eq!(position.events.len(), 3);
1286 assert_eq!(position.ts_closed, Some(UnixNanos::default()));
1287 assert_eq!(position.avg_px_close, Some(1.00002));
1288 assert!(!position.is_long());
1289 assert!(!position.is_short());
1290 assert!(!position.is_open());
1291 assert!(position.is_closed());
1292 assert_eq!(position.commissions(), vec![Money::from("6.0 USD")]);
1293 assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1294 assert_eq!(position.realized_pnl, Some(Money::from("-8.0 USD")));
1295 assert_eq!(position.total_pnl(last), Money::from("-8.0 USD"));
1296 assert_eq!(
1297 format!("{position}"),
1298 "Position(FLAT AUD/USD.SIM, id=P-19700101-000000-001-001-1)"
1299 );
1300 }
1301
1302 #[rstest]
1303 fn test_position_filled_with_no_change(audusd_sim: CurrencyPair) {
1304 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1305 let order1 = OrderTestBuilder::new(OrderType::Market)
1306 .instrument_id(audusd_sim.id())
1307 .side(OrderSide::Buy)
1308 .quantity(Quantity::from(100_000))
1309 .build();
1310 let order2 = OrderTestBuilder::new(OrderType::Market)
1311 .instrument_id(audusd_sim.id())
1312 .side(OrderSide::Sell)
1313 .quantity(Quantity::from(100_000))
1314 .build();
1315 let fill1 = TestOrderEventStubs::filled(
1316 &order1,
1317 &audusd_sim,
1318 Some(TradeId::new("1")),
1319 Some(PositionId::new("P-19700101-000000-001-001-1")),
1320 Some(Price::from("1.0")),
1321 None,
1322 None,
1323 None,
1324 None,
1325 None,
1326 );
1327 let mut position = Position::new(&audusd_sim, fill1.into());
1328 let fill2 = TestOrderEventStubs::filled(
1329 &order2,
1330 &audusd_sim,
1331 Some(TradeId::new("2")),
1332 Some(PositionId::new("P-19700101-000000-001-001-1")),
1333 Some(Price::from("1.0")),
1334 None,
1335 None,
1336 None,
1337 None,
1338 None,
1339 );
1340 let last = Price::from("1.0005");
1341 position.apply(&fill2.into());
1342
1343 assert_eq!(
1344 position.quantity,
1345 Quantity::zero(audusd_sim.price_precision())
1346 );
1347 assert_eq!(position.closing_order_side(), OrderSide::NoOrderSide);
1348 assert_eq!(position.side, PositionSide::Flat);
1349 assert_eq!(position.ts_opened, 0);
1350 assert_eq!(position.avg_px_open, 1.0);
1351 assert_eq!(position.events.len(), 2);
1352 assert_eq!(position.ts_closed, Some(UnixNanos::default()));
1354 assert_eq!(position.avg_px_close, Some(1.0));
1355 assert!(!position.is_long());
1356 assert!(!position.is_short());
1357 assert!(!position.is_open());
1358 assert!(position.is_closed());
1359 assert_eq!(position.commissions(), vec![Money::from("4.0 USD")]);
1360 assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1361 assert_eq!(position.realized_pnl, Some(Money::from("-4.0 USD")));
1362 assert_eq!(position.total_pnl(last), Money::from("-4.0 USD"));
1363 assert_eq!(
1364 format!("{position}"),
1365 "Position(FLAT AUD/USD.SIM, id=P-19700101-000000-001-001-1)"
1366 );
1367 }
1368
1369 #[rstest]
1370 fn test_position_long_with_multiple_filled_orders(audusd_sim: CurrencyPair) {
1371 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1372 let order1 = OrderTestBuilder::new(OrderType::Market)
1373 .instrument_id(audusd_sim.id())
1374 .side(OrderSide::Buy)
1375 .quantity(Quantity::from(100_000))
1376 .build();
1377 let order2 = OrderTestBuilder::new(OrderType::Market)
1378 .instrument_id(audusd_sim.id())
1379 .side(OrderSide::Buy)
1380 .quantity(Quantity::from(100_000))
1381 .build();
1382 let order3 = OrderTestBuilder::new(OrderType::Market)
1383 .instrument_id(audusd_sim.id())
1384 .side(OrderSide::Sell)
1385 .quantity(Quantity::from(200_000))
1386 .build();
1387 let fill1 = TestOrderEventStubs::filled(
1388 &order1,
1389 &audusd_sim,
1390 Some(TradeId::new("1")),
1391 Some(PositionId::new("P-123456")),
1392 Some(Price::from("1.0")),
1393 None,
1394 None,
1395 None,
1396 None,
1397 None,
1398 );
1399 let fill2 = TestOrderEventStubs::filled(
1400 &order2,
1401 &audusd_sim,
1402 Some(TradeId::new("2")),
1403 Some(PositionId::new("P-123456")),
1404 Some(Price::from("1.00001")),
1405 None,
1406 None,
1407 None,
1408 None,
1409 None,
1410 );
1411 let fill3 = TestOrderEventStubs::filled(
1412 &order3,
1413 &audusd_sim,
1414 Some(TradeId::new("3")),
1415 Some(PositionId::new("P-123456")),
1416 Some(Price::from("1.0001")),
1417 None,
1418 None,
1419 None,
1420 None,
1421 None,
1422 );
1423 let mut position = Position::new(&audusd_sim, fill1.into());
1424 let last = Price::from("1.0005");
1425 position.apply(&fill2.into());
1426 position.apply(&fill3.into());
1427
1428 assert_eq!(
1429 position.quantity,
1430 Quantity::zero(audusd_sim.price_precision())
1431 );
1432 assert_eq!(position.side, PositionSide::Flat);
1433 assert_eq!(position.ts_opened, 0);
1434 assert_eq!(position.avg_px_open, 1.000_005);
1435 assert_eq!(position.events.len(), 3);
1436 assert_eq!(position.ts_closed, Some(UnixNanos::default()));
1441 assert_eq!(position.avg_px_close, Some(1.0001));
1442 assert!(position.is_closed());
1443 assert!(!position.is_open());
1444 assert!(!position.is_long());
1445 assert!(!position.is_short());
1446 assert_eq!(position.commissions(), vec![Money::from("6.0 USD")]);
1447 assert_eq!(position.realized_pnl, Some(Money::from("13.0 USD")));
1448 assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1449 assert_eq!(position.total_pnl(last), Money::from("13 USD"));
1450 assert_eq!(
1451 format!("{position}"),
1452 "Position(FLAT AUD/USD.SIM, id=P-123456)"
1453 );
1454 }
1455
1456 #[rstest]
1457 fn test_pnl_calculation_from_trading_technologies_example(currency_pair_ethusdt: CurrencyPair) {
1458 let ethusdt = InstrumentAny::CurrencyPair(currency_pair_ethusdt);
1459 let quantity1 = Quantity::from(12);
1460 let price1 = Price::from("100.0");
1461 let order1 = OrderTestBuilder::new(OrderType::Market)
1462 .instrument_id(ethusdt.id())
1463 .side(OrderSide::Buy)
1464 .quantity(quantity1)
1465 .build();
1466 let commission1 = calculate_commission(ðusdt, order1.quantity(), price1, None);
1467 let fill1 = TestOrderEventStubs::filled(
1468 &order1,
1469 ðusdt,
1470 Some(TradeId::new("1")),
1471 Some(PositionId::new("P-123456")),
1472 Some(price1),
1473 None,
1474 None,
1475 Some(commission1),
1476 None,
1477 None,
1478 );
1479 let mut position = Position::new(ðusdt, fill1.into());
1480 let quantity2 = Quantity::from(17);
1481 let order2 = OrderTestBuilder::new(OrderType::Market)
1482 .instrument_id(ethusdt.id())
1483 .side(OrderSide::Buy)
1484 .quantity(quantity2)
1485 .build();
1486 let price2 = Price::from("99.0");
1487 let commission2 = calculate_commission(ðusdt, order2.quantity(), price2, None);
1488 let fill2 = TestOrderEventStubs::filled(
1489 &order2,
1490 ðusdt,
1491 Some(TradeId::new("2")),
1492 Some(PositionId::new("P-123456")),
1493 Some(price2),
1494 None,
1495 None,
1496 Some(commission2),
1497 None,
1498 None,
1499 );
1500 position.apply(&fill2.into());
1501 assert_eq!(position.quantity, Quantity::from(29));
1502 assert_eq!(position.realized_pnl, Some(Money::from("-0.28830000 USDT")));
1503 assert_eq!(position.avg_px_open, 99.413_793_103_448_27);
1504 let quantity3 = Quantity::from(9);
1505 let order3 = OrderTestBuilder::new(OrderType::Market)
1506 .instrument_id(ethusdt.id())
1507 .side(OrderSide::Sell)
1508 .quantity(quantity3)
1509 .build();
1510 let price3 = Price::from("101.0");
1511 let commission3 = calculate_commission(ðusdt, order3.quantity(), price3, None);
1512 let fill3 = TestOrderEventStubs::filled(
1513 &order3,
1514 ðusdt,
1515 Some(TradeId::new("3")),
1516 Some(PositionId::new("P-123456")),
1517 Some(price3),
1518 None,
1519 None,
1520 Some(commission3),
1521 None,
1522 None,
1523 );
1524 position.apply(&fill3.into());
1525 assert_eq!(position.quantity, Quantity::from(20));
1526 assert_eq!(position.realized_pnl, Some(Money::from("13.89666207 USDT")));
1527 assert_eq!(position.avg_px_open, 99.413_793_103_448_27);
1528 let quantity4 = Quantity::from("4");
1529 let price4 = Price::from("105.0");
1530 let order4 = OrderTestBuilder::new(OrderType::Market)
1531 .instrument_id(ethusdt.id())
1532 .side(OrderSide::Sell)
1533 .quantity(quantity4)
1534 .build();
1535 let commission4 = calculate_commission(ðusdt, order4.quantity(), price4, None);
1536 let fill4 = TestOrderEventStubs::filled(
1537 &order4,
1538 ðusdt,
1539 Some(TradeId::new("4")),
1540 Some(PositionId::new("P-123456")),
1541 Some(price4),
1542 None,
1543 None,
1544 Some(commission4),
1545 None,
1546 None,
1547 );
1548 position.apply(&fill4.into());
1549 assert_eq!(position.quantity, Quantity::from("16"));
1550 assert_eq!(position.realized_pnl, Some(Money::from("36.19948966 USDT")));
1551 assert_eq!(position.avg_px_open, 99.413_793_103_448_27);
1552 let quantity5 = Quantity::from("3");
1553 let price5 = Price::from("103.0");
1554 let order5 = OrderTestBuilder::new(OrderType::Market)
1555 .instrument_id(ethusdt.id())
1556 .side(OrderSide::Buy)
1557 .quantity(quantity5)
1558 .build();
1559 let commission5 = calculate_commission(ðusdt, order5.quantity(), price5, None);
1560 let fill5 = TestOrderEventStubs::filled(
1561 &order5,
1562 ðusdt,
1563 Some(TradeId::new("5")),
1564 Some(PositionId::new("P-123456")),
1565 Some(price5),
1566 None,
1567 None,
1568 Some(commission5),
1569 None,
1570 None,
1571 );
1572 position.apply(&fill5.into());
1573 assert_eq!(position.quantity, Quantity::from("19"));
1574 assert_eq!(position.realized_pnl, Some(Money::from("36.16858966 USDT")));
1575 assert_eq!(position.avg_px_open, 99.980_036_297_640_65);
1576 assert_eq!(
1577 format!("{position}"),
1578 "Position(LONG 19.00000 ETHUSDT.BINANCE, id=P-123456)"
1579 );
1580 }
1581
1582 #[rstest]
1583 fn test_position_closed_and_reopened(audusd_sim: CurrencyPair) {
1584 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1585 let quantity1 = Quantity::from(150_000);
1586 let price1 = Price::from("1.00001");
1587 let order = OrderTestBuilder::new(OrderType::Market)
1588 .instrument_id(audusd_sim.id())
1589 .side(OrderSide::Buy)
1590 .quantity(quantity1)
1591 .build();
1592 let commission1 = calculate_commission(&audusd_sim, quantity1, price1, None);
1593 let fill1 = TestOrderEventStubs::filled(
1594 &order,
1595 &audusd_sim,
1596 Some(TradeId::new("5")),
1597 Some(PositionId::new("P-123456")),
1598 Some(Price::from("1.00001")),
1599 None,
1600 None,
1601 Some(commission1),
1602 Some(UnixNanos::from(1_000_000_000)),
1603 None,
1604 );
1605 let mut position = Position::new(&audusd_sim, fill1.into());
1606
1607 let fill2 = OrderFilled::new(
1608 order.trader_id(),
1609 order.strategy_id(),
1610 order.instrument_id(),
1611 order.client_order_id(),
1612 VenueOrderId::from("2"),
1613 order.account_id().unwrap_or(AccountId::new("SIM-001")),
1614 TradeId::from("2"),
1615 OrderSide::Sell,
1616 OrderType::Market,
1617 order.quantity(),
1618 Price::from("1.00011"),
1619 audusd_sim.quote_currency(),
1620 LiquiditySide::Taker,
1621 uuid4(),
1622 UnixNanos::from(2_000_000_000),
1623 UnixNanos::default(),
1624 false,
1625 Some(PositionId::from("P-123456")),
1626 Some(Money::from("0 USD")),
1627 );
1628
1629 position.apply(&fill2);
1630
1631 let fill3 = OrderFilled::new(
1632 order.trader_id(),
1633 order.strategy_id(),
1634 order.instrument_id(),
1635 order.client_order_id(),
1636 VenueOrderId::from("2"),
1637 order.account_id().unwrap_or(AccountId::new("SIM-001")),
1638 TradeId::from("3"),
1639 OrderSide::Buy,
1640 OrderType::Market,
1641 order.quantity(),
1642 Price::from("1.00012"),
1643 audusd_sim.quote_currency(),
1644 LiquiditySide::Taker,
1645 uuid4(),
1646 UnixNanos::from(3_000_000_000),
1647 UnixNanos::default(),
1648 false,
1649 Some(PositionId::from("P-123456")),
1650 Some(Money::from("0 USD")),
1651 );
1652
1653 position.apply(&fill3);
1654
1655 let last = Price::from("1.0003");
1656 assert!(position.is_opposite_side(fill2.order_side));
1657 assert_eq!(position.quantity, Quantity::from(150_000));
1658 assert_eq!(position.peak_qty, Quantity::from(150_000));
1659 assert_eq!(position.side, PositionSide::Long);
1660 assert_eq!(position.opening_order_id, fill3.client_order_id);
1661 assert_eq!(position.closing_order_id, None);
1662 assert_eq!(position.closing_order_id, None);
1663 assert_eq!(position.ts_opened, 3_000_000_000);
1664 assert_eq!(position.duration_ns, 0);
1665 assert_eq!(position.avg_px_open, 1.00012);
1666 assert_eq!(position.event_count(), 1);
1667 assert_eq!(position.ts_closed, None);
1668 assert_eq!(position.avg_px_close, None);
1669 assert!(position.is_long());
1670 assert!(!position.is_short());
1671 assert!(position.is_open());
1672 assert!(!position.is_closed());
1673 assert_eq!(position.realized_return, 0.0);
1674 assert_eq!(position.realized_pnl, Some(Money::from("0 USD")));
1675 assert_eq!(position.unrealized_pnl(last), Money::from("27 USD"));
1676 assert_eq!(position.total_pnl(last), Money::from("27 USD"));
1677 assert_eq!(position.commissions(), vec![Money::from("0 USD")]);
1678 assert_eq!(
1679 format!("{position}"),
1680 "Position(LONG 150_000 AUD/USD.SIM, id=P-123456)"
1681 );
1682 }
1683
1684 #[rstest]
1685 fn test_position_realized_pnl_with_interleaved_order_sides(
1686 currency_pair_btcusdt: CurrencyPair,
1687 ) {
1688 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1689 let order1 = OrderTestBuilder::new(OrderType::Market)
1690 .instrument_id(btcusdt.id())
1691 .side(OrderSide::Buy)
1692 .quantity(Quantity::from(12))
1693 .build();
1694 let commission1 =
1695 calculate_commission(&btcusdt, order1.quantity(), Price::from("10000.0"), None);
1696 let fill1 = TestOrderEventStubs::filled(
1697 &order1,
1698 &btcusdt,
1699 Some(TradeId::from("1")),
1700 Some(PositionId::from("P-19700101-000000-001-001-1")),
1701 Some(Price::from("10000.0")),
1702 None,
1703 None,
1704 Some(commission1),
1705 None,
1706 None,
1707 );
1708 let mut position = Position::new(&btcusdt, fill1.into());
1709 let order2 = OrderTestBuilder::new(OrderType::Market)
1710 .instrument_id(btcusdt.id())
1711 .side(OrderSide::Buy)
1712 .quantity(Quantity::from(17))
1713 .build();
1714 let commission2 =
1715 calculate_commission(&btcusdt, order2.quantity(), Price::from("9999.0"), None);
1716 let fill2 = TestOrderEventStubs::filled(
1717 &order2,
1718 &btcusdt,
1719 Some(TradeId::from("2")),
1720 Some(PositionId::from("P-19700101-000000-001-001-1")),
1721 Some(Price::from("9999.0")),
1722 None,
1723 None,
1724 Some(commission2),
1725 None,
1726 None,
1727 );
1728 position.apply(&fill2.into());
1729 assert_eq!(position.quantity, Quantity::from(29));
1730 assert_eq!(
1731 position.realized_pnl,
1732 Some(Money::from("-289.98300000 USDT"))
1733 );
1734 assert_eq!(position.avg_px_open, 9_999.413_793_103_447);
1735 let order3 = OrderTestBuilder::new(OrderType::Market)
1736 .instrument_id(btcusdt.id())
1737 .side(OrderSide::Sell)
1738 .quantity(Quantity::from(9))
1739 .build();
1740 let commission3 =
1741 calculate_commission(&btcusdt, order3.quantity(), Price::from("10001.0"), None);
1742 let fill3 = TestOrderEventStubs::filled(
1743 &order3,
1744 &btcusdt,
1745 Some(TradeId::from("3")),
1746 Some(PositionId::from("P-19700101-000000-001-001-1")),
1747 Some(Price::from("10001.0")),
1748 None,
1749 None,
1750 Some(commission3),
1751 None,
1752 None,
1753 );
1754 position.apply(&fill3.into());
1755 assert_eq!(position.quantity, Quantity::from(20));
1756 assert_eq!(
1757 position.realized_pnl,
1758 Some(Money::from("-365.71613793 USDT"))
1759 );
1760 assert_eq!(position.avg_px_open, 9_999.413_793_103_447);
1761 let order4 = OrderTestBuilder::new(OrderType::Market)
1762 .instrument_id(btcusdt.id())
1763 .side(OrderSide::Buy)
1764 .quantity(Quantity::from(3))
1765 .build();
1766 let commission4 =
1767 calculate_commission(&btcusdt, order4.quantity(), Price::from("10003.0"), None);
1768 let fill4 = TestOrderEventStubs::filled(
1769 &order4,
1770 &btcusdt,
1771 Some(TradeId::from("4")),
1772 Some(PositionId::from("P-19700101-000000-001-001-1")),
1773 Some(Price::from("10003.0")),
1774 None,
1775 None,
1776 Some(commission4),
1777 None,
1778 None,
1779 );
1780 position.apply(&fill4.into());
1781 assert_eq!(position.quantity, Quantity::from(23));
1782 assert_eq!(
1783 position.realized_pnl,
1784 Some(Money::from("-395.72513793 USDT"))
1785 );
1786 assert_eq!(position.avg_px_open, 9_999.881_559_220_39);
1787 let order5 = OrderTestBuilder::new(OrderType::Market)
1788 .instrument_id(btcusdt.id())
1789 .side(OrderSide::Sell)
1790 .quantity(Quantity::from(4))
1791 .build();
1792 let commission5 =
1793 calculate_commission(&btcusdt, order5.quantity(), Price::from("10005.0"), None);
1794 let fill5 = TestOrderEventStubs::filled(
1795 &order5,
1796 &btcusdt,
1797 Some(TradeId::from("5")),
1798 Some(PositionId::from("P-19700101-000000-001-001-1")),
1799 Some(Price::from("10005.0")),
1800 None,
1801 None,
1802 Some(commission5),
1803 None,
1804 None,
1805 );
1806 position.apply(&fill5.into());
1807 assert_eq!(position.quantity, Quantity::from(19));
1808 assert_eq!(
1809 position.realized_pnl,
1810 Some(Money::from("-415.27137481 USDT"))
1811 );
1812 assert_eq!(position.avg_px_open, 9_999.881_559_220_39);
1813 assert_eq!(
1814 format!("{position}"),
1815 "Position(LONG 19.000000 BTCUSDT.BINANCE, id=P-19700101-000000-001-001-1)"
1816 );
1817 }
1818
1819 #[rstest]
1820 fn test_calculate_pnl_when_given_position_side_flat_returns_zero(
1821 currency_pair_btcusdt: CurrencyPair,
1822 ) {
1823 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1824 let order = OrderTestBuilder::new(OrderType::Market)
1825 .instrument_id(btcusdt.id())
1826 .side(OrderSide::Buy)
1827 .quantity(Quantity::from(12))
1828 .build();
1829 let fill = TestOrderEventStubs::filled(
1830 &order,
1831 &btcusdt,
1832 None,
1833 Some(PositionId::from("P-123456")),
1834 Some(Price::from("10500.0")),
1835 None,
1836 None,
1837 None,
1838 None,
1839 None,
1840 );
1841 let position = Position::new(&btcusdt, fill.into());
1842 let result = position.calculate_pnl(10500.0, 10500.0, Quantity::from("100000.0"));
1843 assert_eq!(result, Money::from("0 USDT"));
1844 }
1845
1846 #[rstest]
1847 fn test_calculate_pnl_for_long_position_win(currency_pair_btcusdt: CurrencyPair) {
1848 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1849 let order = OrderTestBuilder::new(OrderType::Market)
1850 .instrument_id(btcusdt.id())
1851 .side(OrderSide::Buy)
1852 .quantity(Quantity::from(12))
1853 .build();
1854 let commission =
1855 calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1856 let fill = TestOrderEventStubs::filled(
1857 &order,
1858 &btcusdt,
1859 None,
1860 Some(PositionId::from("P-123456")),
1861 Some(Price::from("10500.0")),
1862 None,
1863 None,
1864 Some(commission),
1865 None,
1866 None,
1867 );
1868 let position = Position::new(&btcusdt, fill.into());
1869 let pnl = position.calculate_pnl(10500.0, 10510.0, Quantity::from("12.0"));
1870 assert_eq!(pnl, Money::from("120 USDT"));
1871 assert_eq!(position.realized_pnl, Some(Money::from("-126 USDT")));
1872 assert_eq!(
1873 position.unrealized_pnl(Price::from("10510.0")),
1874 Money::from("120.0 USDT")
1875 );
1876 assert_eq!(
1877 position.total_pnl(Price::from("10510.0")),
1878 Money::from("-6 USDT")
1879 );
1880 assert_eq!(position.commissions(), vec![Money::from("126.0 USDT")]);
1881 }
1882
1883 #[rstest]
1884 fn test_calculate_pnl_for_long_position_loss(currency_pair_btcusdt: CurrencyPair) {
1885 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1886 let order = OrderTestBuilder::new(OrderType::Market)
1887 .instrument_id(btcusdt.id())
1888 .side(OrderSide::Buy)
1889 .quantity(Quantity::from(12))
1890 .build();
1891 let commission =
1892 calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1893 let fill = TestOrderEventStubs::filled(
1894 &order,
1895 &btcusdt,
1896 None,
1897 Some(PositionId::from("P-123456")),
1898 Some(Price::from("10500.0")),
1899 None,
1900 None,
1901 Some(commission),
1902 None,
1903 None,
1904 );
1905 let position = Position::new(&btcusdt, fill.into());
1906 let pnl = position.calculate_pnl(10500.0, 10480.5, Quantity::from("10.0"));
1907 assert_eq!(pnl, Money::from("-195 USDT"));
1908 assert_eq!(position.realized_pnl, Some(Money::from("-126 USDT")));
1909 assert_eq!(
1910 position.unrealized_pnl(Price::from("10480.50")),
1911 Money::from("-234.0 USDT")
1912 );
1913 assert_eq!(
1914 position.total_pnl(Price::from("10480.50")),
1915 Money::from("-360 USDT")
1916 );
1917 assert_eq!(position.commissions(), vec![Money::from("126.0 USDT")]);
1918 }
1919
1920 #[rstest]
1921 fn test_calculate_pnl_for_short_position_winning(currency_pair_btcusdt: CurrencyPair) {
1922 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1923 let order = OrderTestBuilder::new(OrderType::Market)
1924 .instrument_id(btcusdt.id())
1925 .side(OrderSide::Sell)
1926 .quantity(Quantity::from("10.15"))
1927 .build();
1928 let commission =
1929 calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1930 let fill = TestOrderEventStubs::filled(
1931 &order,
1932 &btcusdt,
1933 None,
1934 Some(PositionId::from("P-123456")),
1935 Some(Price::from("10500.0")),
1936 None,
1937 None,
1938 Some(commission),
1939 None,
1940 None,
1941 );
1942 let position = Position::new(&btcusdt, fill.into());
1943 let pnl = position.calculate_pnl(10500.0, 10390.0, Quantity::from("10.15"));
1944 assert_eq!(pnl, Money::from("1116.5 USDT"));
1945 assert_eq!(
1946 position.unrealized_pnl(Price::from("10390.0")),
1947 Money::from("1116.5 USDT")
1948 );
1949 assert_eq!(position.realized_pnl, Some(Money::from("-106.575 USDT")));
1950 assert_eq!(position.commissions(), vec![Money::from("106.575 USDT")]);
1951 assert_eq!(
1952 position.notional_value(Price::from("10390.0")),
1953 Money::from("105458.5 USDT")
1954 );
1955 }
1956
1957 #[rstest]
1958 fn test_calculate_pnl_for_short_position_loss(currency_pair_btcusdt: CurrencyPair) {
1959 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1960 let order = OrderTestBuilder::new(OrderType::Market)
1961 .instrument_id(btcusdt.id())
1962 .side(OrderSide::Sell)
1963 .quantity(Quantity::from("10.0"))
1964 .build();
1965 let commission =
1966 calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1967 let fill = TestOrderEventStubs::filled(
1968 &order,
1969 &btcusdt,
1970 None,
1971 Some(PositionId::from("P-123456")),
1972 Some(Price::from("10500.0")),
1973 None,
1974 None,
1975 Some(commission),
1976 None,
1977 None,
1978 );
1979 let position = Position::new(&btcusdt, fill.into());
1980 let pnl = position.calculate_pnl(10500.0, 10670.5, Quantity::from("10.0"));
1981 assert_eq!(pnl, Money::from("-1705 USDT"));
1982 assert_eq!(
1983 position.unrealized_pnl(Price::from("10670.5")),
1984 Money::from("-1705 USDT")
1985 );
1986 assert_eq!(position.realized_pnl, Some(Money::from("-105 USDT")));
1987 assert_eq!(position.commissions(), vec![Money::from("105 USDT")]);
1988 assert_eq!(
1989 position.notional_value(Price::from("10670.5")),
1990 Money::from("106705 USDT")
1991 );
1992 }
1993
1994 #[rstest]
1995 fn test_calculate_pnl_for_inverse1(xbtusd_bitmex: CryptoPerpetual) {
1996 let xbtusd_bitmex = InstrumentAny::CryptoPerpetual(xbtusd_bitmex);
1997 let order = OrderTestBuilder::new(OrderType::Market)
1998 .instrument_id(xbtusd_bitmex.id())
1999 .side(OrderSide::Sell)
2000 .quantity(Quantity::from("100000"))
2001 .build();
2002 let commission = calculate_commission(
2003 &xbtusd_bitmex,
2004 order.quantity(),
2005 Price::from("10000.0"),
2006 None,
2007 );
2008 let fill = TestOrderEventStubs::filled(
2009 &order,
2010 &xbtusd_bitmex,
2011 None,
2012 Some(PositionId::from("P-123456")),
2013 Some(Price::from("10000.0")),
2014 None,
2015 None,
2016 Some(commission),
2017 None,
2018 None,
2019 );
2020 let position = Position::new(&xbtusd_bitmex, fill.into());
2021 let pnl = position.calculate_pnl(10000.0, 11000.0, Quantity::from("100000.0"));
2022 assert_eq!(pnl, Money::from("-0.90909091 BTC"));
2023 assert_eq!(
2024 position.unrealized_pnl(Price::from("11000.0")),
2025 Money::from("-0.90909091 BTC")
2026 );
2027 assert_eq!(position.realized_pnl, Some(Money::from("-0.00750000 BTC")));
2028 assert_eq!(
2029 position.notional_value(Price::from("11000.0")),
2030 Money::from("9.09090909 BTC")
2031 );
2032 }
2033
2034 #[rstest]
2035 fn test_calculate_pnl_for_inverse2(ethusdt_bitmex: CryptoPerpetual) {
2036 let ethusdt_bitmex = InstrumentAny::CryptoPerpetual(ethusdt_bitmex);
2037 let order = OrderTestBuilder::new(OrderType::Market)
2038 .instrument_id(ethusdt_bitmex.id())
2039 .side(OrderSide::Sell)
2040 .quantity(Quantity::from("100000"))
2041 .build();
2042 let commission = calculate_commission(
2043 ðusdt_bitmex,
2044 order.quantity(),
2045 Price::from("375.95"),
2046 None,
2047 );
2048 let fill = TestOrderEventStubs::filled(
2049 &order,
2050 ðusdt_bitmex,
2051 None,
2052 Some(PositionId::from("P-123456")),
2053 Some(Price::from("375.95")),
2054 None,
2055 None,
2056 Some(commission),
2057 None,
2058 None,
2059 );
2060 let position = Position::new(ðusdt_bitmex, fill.into());
2061
2062 assert_eq!(
2063 position.unrealized_pnl(Price::from("370.00")),
2064 Money::from("4.27745208 ETH")
2065 );
2066 assert_eq!(
2067 position.notional_value(Price::from("370.00")),
2068 Money::from("270.27027027 ETH")
2069 );
2070 }
2071
2072 #[rstest]
2073 fn test_calculate_unrealized_pnl_for_long(currency_pair_btcusdt: CurrencyPair) {
2074 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
2075 let order1 = OrderTestBuilder::new(OrderType::Market)
2076 .instrument_id(btcusdt.id())
2077 .side(OrderSide::Buy)
2078 .quantity(Quantity::from("2.000000"))
2079 .build();
2080 let order2 = OrderTestBuilder::new(OrderType::Market)
2081 .instrument_id(btcusdt.id())
2082 .side(OrderSide::Buy)
2083 .quantity(Quantity::from("2.000000"))
2084 .build();
2085 let commission1 =
2086 calculate_commission(&btcusdt, order1.quantity(), Price::from("10500.0"), None);
2087 let fill1 = TestOrderEventStubs::filled(
2088 &order1,
2089 &btcusdt,
2090 Some(TradeId::new("1")),
2091 Some(PositionId::new("P-123456")),
2092 Some(Price::from("10500.00")),
2093 None,
2094 None,
2095 Some(commission1),
2096 None,
2097 None,
2098 );
2099 let commission2 =
2100 calculate_commission(&btcusdt, order2.quantity(), Price::from("10500.0"), None);
2101 let fill2 = TestOrderEventStubs::filled(
2102 &order2,
2103 &btcusdt,
2104 Some(TradeId::new("2")),
2105 Some(PositionId::new("P-123456")),
2106 Some(Price::from("10500.00")),
2107 None,
2108 None,
2109 Some(commission2),
2110 None,
2111 None,
2112 );
2113 let mut position = Position::new(&btcusdt, fill1.into());
2114 position.apply(&fill2.into());
2115 let pnl = position.unrealized_pnl(Price::from("11505.60"));
2116 assert_eq!(pnl, Money::from("4022.40000000 USDT"));
2117 assert_eq!(
2118 position.realized_pnl,
2119 Some(Money::from("-42.00000000 USDT"))
2120 );
2121 assert_eq!(
2122 position.commissions(),
2123 vec![Money::from("42.00000000 USDT")]
2124 );
2125 }
2126
2127 #[rstest]
2128 fn test_calculate_unrealized_pnl_for_short(currency_pair_btcusdt: CurrencyPair) {
2129 let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
2130 let order = OrderTestBuilder::new(OrderType::Market)
2131 .instrument_id(btcusdt.id())
2132 .side(OrderSide::Sell)
2133 .quantity(Quantity::from("5.912000"))
2134 .build();
2135 let commission =
2136 calculate_commission(&btcusdt, order.quantity(), Price::from("10505.60"), None);
2137 let fill = TestOrderEventStubs::filled(
2138 &order,
2139 &btcusdt,
2140 Some(TradeId::new("1")),
2141 Some(PositionId::new("P-123456")),
2142 Some(Price::from("10505.60")),
2143 None,
2144 None,
2145 Some(commission),
2146 None,
2147 None,
2148 );
2149 let position = Position::new(&btcusdt, fill.into());
2150 let pnl = position.unrealized_pnl(Price::from("10407.15"));
2151 assert_eq!(pnl, Money::from("582.03640000 USDT"));
2152 assert_eq!(
2153 position.realized_pnl,
2154 Some(Money::from("-62.10910720 USDT"))
2155 );
2156 assert_eq!(
2157 position.commissions(),
2158 vec![Money::from("62.10910720 USDT")]
2159 );
2160 }
2161
2162 #[rstest]
2163 fn test_calculate_unrealized_pnl_for_long_inverse(xbtusd_bitmex: CryptoPerpetual) {
2164 let xbtusd_bitmex = InstrumentAny::CryptoPerpetual(xbtusd_bitmex);
2165 let order = OrderTestBuilder::new(OrderType::Market)
2166 .instrument_id(xbtusd_bitmex.id())
2167 .side(OrderSide::Buy)
2168 .quantity(Quantity::from("100000"))
2169 .build();
2170 let commission = calculate_commission(
2171 &xbtusd_bitmex,
2172 order.quantity(),
2173 Price::from("10500.0"),
2174 None,
2175 );
2176 let fill = TestOrderEventStubs::filled(
2177 &order,
2178 &xbtusd_bitmex,
2179 Some(TradeId::new("1")),
2180 Some(PositionId::new("P-123456")),
2181 Some(Price::from("10500.00")),
2182 None,
2183 None,
2184 Some(commission),
2185 None,
2186 None,
2187 );
2188
2189 let position = Position::new(&xbtusd_bitmex, fill.into());
2190 let pnl = position.unrealized_pnl(Price::from("11505.60"));
2191 assert_eq!(pnl, Money::from("0.83238969 BTC"));
2192 assert_eq!(position.realized_pnl, Some(Money::from("-0.00714286 BTC")));
2193 assert_eq!(position.commissions(), vec![Money::from("0.00714286 BTC")]);
2194 }
2195
2196 #[rstest]
2197 fn test_calculate_unrealized_pnl_for_short_inverse(xbtusd_bitmex: CryptoPerpetual) {
2198 let xbtusd_bitmex = InstrumentAny::CryptoPerpetual(xbtusd_bitmex);
2199 let order = OrderTestBuilder::new(OrderType::Market)
2200 .instrument_id(xbtusd_bitmex.id())
2201 .side(OrderSide::Sell)
2202 .quantity(Quantity::from("1250000"))
2203 .build();
2204 let commission = calculate_commission(
2205 &xbtusd_bitmex,
2206 order.quantity(),
2207 Price::from("15500.00"),
2208 None,
2209 );
2210 let fill = TestOrderEventStubs::filled(
2211 &order,
2212 &xbtusd_bitmex,
2213 Some(TradeId::new("1")),
2214 Some(PositionId::new("P-123456")),
2215 Some(Price::from("15500.00")),
2216 None,
2217 None,
2218 Some(commission),
2219 None,
2220 None,
2221 );
2222 let position = Position::new(&xbtusd_bitmex, fill.into());
2223 let pnl = position.unrealized_pnl(Price::from("12506.65"));
2224
2225 assert_eq!(pnl, Money::from("19.30166700 BTC"));
2226 assert_eq!(position.realized_pnl, Some(Money::from("-0.06048387 BTC")));
2227 assert_eq!(position.commissions(), vec![Money::from("0.06048387 BTC")]);
2228 }
2229
2230 #[rstest]
2231 #[case(OrderSide::Buy, 25, 25.0)]
2232 #[case(OrderSide::Sell,25,-25.0)]
2233 fn test_signed_qty_decimal_qty_for_equity(
2234 #[case] order_side: OrderSide,
2235 #[case] quantity: i64,
2236 #[case] expected: f64,
2237 audusd_sim: CurrencyPair,
2238 ) {
2239 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2240 let order = OrderTestBuilder::new(OrderType::Market)
2241 .instrument_id(audusd_sim.id())
2242 .side(order_side)
2243 .quantity(Quantity::from(quantity))
2244 .build();
2245
2246 let commission =
2247 calculate_commission(&audusd_sim, order.quantity(), Price::from("1.0"), None);
2248 let fill = TestOrderEventStubs::filled(
2249 &order,
2250 &audusd_sim,
2251 None,
2252 Some(PositionId::from("P-123456")),
2253 None,
2254 None,
2255 None,
2256 Some(commission),
2257 None,
2258 None,
2259 );
2260 let position = Position::new(&audusd_sim, fill.into());
2261 assert_eq!(position.signed_qty, expected);
2262 }
2263
2264 #[rstest]
2265 fn test_position_with_commission_none(audusd_sim: CurrencyPair) {
2266 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2267 let fill = OrderFilled {
2268 position_id: Some(PositionId::from("1")),
2269 ..Default::default()
2270 };
2271
2272 let position = Position::new(&audusd_sim, fill);
2273 assert_eq!(position.realized_pnl, Some(Money::from("0 USD")));
2274 }
2275
2276 #[rstest]
2277 fn test_position_with_commission_zero(audusd_sim: CurrencyPair) {
2278 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2279 let fill = OrderFilled {
2280 position_id: Some(PositionId::from("1")),
2281 commission: Some(Money::from("0 USD")),
2282 ..Default::default()
2283 };
2284
2285 let position = Position::new(&audusd_sim, fill);
2286 assert_eq!(position.realized_pnl, Some(Money::from("0 USD")));
2287 }
2288
2289 #[rstest]
2290 fn test_cache_purge_order_events() {
2291 let audusd_sim = audusd_sim();
2292 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2293
2294 let order1 = OrderTestBuilder::new(OrderType::Market)
2295 .client_order_id(ClientOrderId::new("O-1"))
2296 .instrument_id(audusd_sim.id())
2297 .side(OrderSide::Buy)
2298 .quantity(Quantity::from(50_000))
2299 .build();
2300
2301 let order2 = OrderTestBuilder::new(OrderType::Market)
2302 .client_order_id(ClientOrderId::new("O-2"))
2303 .instrument_id(audusd_sim.id())
2304 .side(OrderSide::Buy)
2305 .quantity(Quantity::from(50_000))
2306 .build();
2307
2308 let position_id = PositionId::new("P-123456");
2309
2310 let fill1 = TestOrderEventStubs::filled(
2311 &order1,
2312 &audusd_sim,
2313 Some(TradeId::new("1")),
2314 Some(position_id),
2315 Some(Price::from("1.00001")),
2316 None,
2317 None,
2318 None,
2319 None,
2320 None,
2321 );
2322
2323 let mut position = Position::new(&audusd_sim, fill1.into());
2324
2325 let fill2 = TestOrderEventStubs::filled(
2326 &order2,
2327 &audusd_sim,
2328 Some(TradeId::new("2")),
2329 Some(position_id),
2330 Some(Price::from("1.00002")),
2331 None,
2332 None,
2333 None,
2334 None,
2335 None,
2336 );
2337
2338 position.apply(&fill2.into());
2339 position.purge_events_for_order(order1.client_order_id());
2340
2341 assert_eq!(position.events.len(), 1);
2342 assert_eq!(position.trade_ids.len(), 1);
2343 assert_eq!(position.events[0].client_order_id, order2.client_order_id());
2344 assert_eq!(position.trade_ids[0], TradeId::new("2"));
2345 }
2346
2347 #[rstest]
2348 fn test_purge_all_events_returns_none_for_last_event_and_trade_id() {
2349 let audusd_sim = audusd_sim();
2350 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2351
2352 let order = OrderTestBuilder::new(OrderType::Market)
2353 .client_order_id(ClientOrderId::new("O-1"))
2354 .instrument_id(audusd_sim.id())
2355 .side(OrderSide::Buy)
2356 .quantity(Quantity::from(100_000))
2357 .build();
2358
2359 let position_id = PositionId::new("P-123456");
2360 let fill = TestOrderEventStubs::filled(
2361 &order,
2362 &audusd_sim,
2363 Some(TradeId::new("1")),
2364 Some(position_id),
2365 Some(Price::from("1.00050")),
2366 None,
2367 None,
2368 None,
2369 Some(UnixNanos::from(1_000_000_000)), None,
2371 );
2372
2373 let mut position = Position::new(&audusd_sim, fill.into());
2374
2375 assert_eq!(position.events.len(), 1);
2376 assert!(position.last_event().is_some());
2377 assert!(position.last_trade_id().is_some());
2378
2379 let original_ts_opened = position.ts_opened;
2381 let original_ts_last = position.ts_last;
2382 assert_ne!(original_ts_opened, UnixNanos::default());
2383 assert_ne!(original_ts_last, UnixNanos::default());
2384
2385 position.purge_events_for_order(order.client_order_id());
2386
2387 assert_eq!(position.events.len(), 0);
2388 assert_eq!(position.trade_ids.len(), 0);
2389 assert!(position.last_event().is_none());
2390 assert!(position.last_trade_id().is_none());
2391
2392 assert_eq!(position.ts_opened, UnixNanos::default());
2395 assert_eq!(position.ts_last, UnixNanos::default());
2396 assert_eq!(position.ts_closed, Some(UnixNanos::default()));
2397 assert_eq!(position.duration_ns, 0);
2398
2399 assert!(position.is_closed());
2402 assert!(!position.is_open());
2403 assert_eq!(position.side, PositionSide::Flat);
2404 }
2405
2406 #[rstest]
2407 fn test_revive_from_empty_shell(audusd_sim: CurrencyPair) {
2408 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2410
2411 let order1 = OrderTestBuilder::new(OrderType::Market)
2413 .instrument_id(audusd_sim.id())
2414 .side(OrderSide::Buy)
2415 .quantity(Quantity::from(100_000))
2416 .build();
2417
2418 let fill1 = TestOrderEventStubs::filled(
2419 &order1,
2420 &audusd_sim,
2421 None,
2422 Some(PositionId::new("P-1")),
2423 Some(Price::from("1.00000")),
2424 None,
2425 None,
2426 None,
2427 Some(UnixNanos::from(1_000_000_000)),
2428 None,
2429 );
2430
2431 let mut position = Position::new(&audusd_sim, fill1.into());
2432 position.purge_events_for_order(order1.client_order_id());
2433
2434 assert!(position.is_closed());
2436 assert_eq!(position.ts_closed, Some(UnixNanos::default()));
2437 assert_eq!(position.event_count(), 0);
2438
2439 let order2 = OrderTestBuilder::new(OrderType::Market)
2441 .instrument_id(audusd_sim.id())
2442 .side(OrderSide::Buy)
2443 .quantity(Quantity::from(50_000))
2444 .build();
2445
2446 let fill2 = TestOrderEventStubs::filled(
2447 &order2,
2448 &audusd_sim,
2449 None,
2450 Some(PositionId::new("P-1")),
2451 Some(Price::from("1.00020")),
2452 None,
2453 None,
2454 None,
2455 Some(UnixNanos::from(3_000_000_000)),
2456 None,
2457 );
2458
2459 let fill2_typed: OrderFilled = fill2.clone().into();
2460 position.apply(&fill2_typed);
2461
2462 assert!(position.is_long());
2464 assert!(!position.is_closed());
2465 assert!(position.ts_closed.is_none());
2466 assert_eq!(position.ts_opened, fill2.ts_event());
2467 assert_eq!(position.ts_last, fill2.ts_event());
2468 assert_eq!(position.event_count(), 1);
2469 assert_eq!(position.quantity, Quantity::from(50_000));
2470 }
2471
2472 #[rstest]
2473 fn test_empty_shell_position_invariants(audusd_sim: CurrencyPair) {
2474 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2476
2477 let order = OrderTestBuilder::new(OrderType::Market)
2478 .instrument_id(audusd_sim.id())
2479 .side(OrderSide::Buy)
2480 .quantity(Quantity::from(100_000))
2481 .build();
2482
2483 let fill = TestOrderEventStubs::filled(
2484 &order,
2485 &audusd_sim,
2486 None,
2487 Some(PositionId::new("P-1")),
2488 Some(Price::from("1.00000")),
2489 None,
2490 None,
2491 None,
2492 Some(UnixNanos::from(1_000_000_000)),
2493 None,
2494 );
2495
2496 let mut position = Position::new(&audusd_sim, fill.into());
2497 position.purge_events_for_order(order.client_order_id());
2498
2499 assert_eq!(
2501 position.event_count(),
2502 0,
2503 "Precondition: event_count must be 0"
2504 );
2505
2506 assert!(
2508 position.is_closed(),
2509 "INV1: Empty shell must report is_closed() == true"
2510 );
2511 assert!(
2512 !position.is_open(),
2513 "INV1: Empty shell must report is_open() == false"
2514 );
2515
2516 assert_eq!(
2518 position.side,
2519 PositionSide::Flat,
2520 "INV2: Empty shell must be FLAT"
2521 );
2522
2523 assert!(
2525 position.ts_closed.is_some(),
2526 "INV3: Empty shell must have ts_closed.is_some()"
2527 );
2528 assert_eq!(
2529 position.ts_closed,
2530 Some(UnixNanos::default()),
2531 "INV3: Empty shell ts_closed must be 0"
2532 );
2533
2534 assert_eq!(
2536 position.ts_opened,
2537 UnixNanos::default(),
2538 "INV4: Empty shell ts_opened must be 0"
2539 );
2540 assert_eq!(
2541 position.ts_last,
2542 UnixNanos::default(),
2543 "INV4: Empty shell ts_last must be 0"
2544 );
2545 assert_eq!(
2546 position.duration_ns, 0,
2547 "INV4: Empty shell duration_ns must be 0"
2548 );
2549
2550 assert_eq!(
2552 position.quantity,
2553 Quantity::zero(audusd_sim.size_precision()),
2554 "INV5: Empty shell quantity must be 0"
2555 );
2556
2557 assert!(
2559 position.events.is_empty(),
2560 "INV6: Empty shell must have no events"
2561 );
2562 assert!(
2563 position.trade_ids.is_empty(),
2564 "INV6: Empty shell must have no trade IDs"
2565 );
2566 assert!(
2567 position.last_event().is_none(),
2568 "INV6: Empty shell must have no last event"
2569 );
2570 assert!(
2571 position.last_trade_id().is_none(),
2572 "INV6: Empty shell must have no last trade ID"
2573 );
2574 }
2575
2576 #[rstest]
2577 fn test_position_pnl_precision_with_very_small_amounts(audusd_sim: CurrencyPair) {
2578 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2581 let order = OrderTestBuilder::new(OrderType::Market)
2582 .instrument_id(audusd_sim.id())
2583 .side(OrderSide::Buy)
2584 .quantity(Quantity::from(100))
2585 .build();
2586
2587 let small_commission = Money::new(0.01, Currency::USD());
2589 let fill = TestOrderEventStubs::filled(
2590 &order,
2591 &audusd_sim,
2592 None,
2593 None,
2594 Some(Price::from("1.00001")),
2595 Some(Quantity::from(100)),
2596 None,
2597 Some(small_commission),
2598 None,
2599 None,
2600 );
2601
2602 let position = Position::new(&audusd_sim, fill.into());
2603
2604 assert_eq!(position.commissions().len(), 1);
2606 let recorded_commission = position.commissions()[0];
2607 assert!(
2608 recorded_commission.as_f64() > 0.0,
2609 "Commission of 0.01 should be preserved"
2610 );
2611
2612 let realized = position.realized_pnl.unwrap().as_f64();
2614 assert!(
2615 realized < 0.0,
2616 "Realized PnL should be negative due to commission"
2617 );
2618 }
2619
2620 #[rstest]
2621 fn test_position_pnl_precision_with_high_precision_instrument() {
2622 use crate::instruments::stubs::crypto_perpetual_ethusdt;
2624 let ethusdt = crypto_perpetual_ethusdt();
2625 let ethusdt = InstrumentAny::CryptoPerpetual(ethusdt);
2626
2627 let size_precision = ethusdt.size_precision();
2629
2630 let order = OrderTestBuilder::new(OrderType::Market)
2631 .instrument_id(ethusdt.id())
2632 .side(OrderSide::Buy)
2633 .quantity(Quantity::from("1.123456789"))
2634 .build();
2635
2636 let fill = TestOrderEventStubs::filled(
2637 &order,
2638 ðusdt,
2639 None,
2640 None,
2641 Some(Price::from("2345.123456789")),
2642 Some(Quantity::from("1.123456789")),
2643 None,
2644 Some(Money::from("0.1 USDT")),
2645 None,
2646 None,
2647 );
2648
2649 let position = Position::new(ðusdt, fill.into());
2650
2651 let avg_px = position.avg_px_open;
2653 assert!(
2654 (avg_px - 2345.123456789).abs() < 1e-6,
2655 "High precision price should be preserved within f64 tolerance"
2656 );
2657
2658 assert_eq!(
2661 position.quantity.precision, size_precision,
2662 "Quantity precision should match instrument"
2663 );
2664
2665 let qty_f64 = position.quantity.as_f64();
2667 assert!(
2668 qty_f64 > 1.0 && qty_f64 < 2.0,
2669 "Quantity should be in expected range"
2670 );
2671 }
2672
2673 #[rstest]
2674 fn test_position_pnl_accumulation_across_many_fills(audusd_sim: CurrencyPair) {
2675 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2677 let order = OrderTestBuilder::new(OrderType::Market)
2678 .instrument_id(audusd_sim.id())
2679 .side(OrderSide::Buy)
2680 .quantity(Quantity::from(1000))
2681 .build();
2682
2683 let initial_fill = TestOrderEventStubs::filled(
2684 &order,
2685 &audusd_sim,
2686 Some(TradeId::new("1")),
2687 None,
2688 Some(Price::from("1.00000")),
2689 Some(Quantity::from(10)),
2690 None,
2691 Some(Money::from("0.01 USD")),
2692 None,
2693 None,
2694 );
2695
2696 let mut position = Position::new(&audusd_sim, initial_fill.into());
2697
2698 for i in 2..=100 {
2700 let price_offset = (i as f64) * 0.00001;
2701 let fill = TestOrderEventStubs::filled(
2702 &order,
2703 &audusd_sim,
2704 Some(TradeId::new(i.to_string())),
2705 None,
2706 Some(Price::from(&format!("{:.5}", 1.0 + price_offset))),
2707 Some(Quantity::from(10)),
2708 None,
2709 Some(Money::from("0.01 USD")),
2710 None,
2711 None,
2712 );
2713 position.apply(&fill.into());
2714 }
2715
2716 assert_eq!(position.events.len(), 100);
2718 assert_eq!(position.quantity, Quantity::from(1000));
2719
2720 let total_commission: f64 = position.commissions().iter().map(|c| c.as_f64()).sum();
2722 assert!(
2723 (total_commission - 1.0).abs() < 1e-10,
2724 "Commission accumulation should be accurate: expected 1.0, was {total_commission}"
2725 );
2726
2727 let avg_px = position.avg_px_open;
2729 assert!(
2730 avg_px > 1.0 && avg_px < 1.001,
2731 "Average price should be reasonable: got {avg_px}"
2732 );
2733 }
2734
2735 #[rstest]
2736 fn test_position_pnl_with_extreme_price_values(audusd_sim: CurrencyPair) {
2737 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2739
2740 let order_small = OrderTestBuilder::new(OrderType::Market)
2742 .instrument_id(audusd_sim.id())
2743 .side(OrderSide::Buy)
2744 .quantity(Quantity::from(100_000))
2745 .build();
2746
2747 let fill_small = TestOrderEventStubs::filled(
2748 &order_small,
2749 &audusd_sim,
2750 None,
2751 None,
2752 Some(Price::from("0.00001")),
2753 Some(Quantity::from(100_000)),
2754 None,
2755 None,
2756 None,
2757 None,
2758 );
2759
2760 let position_small = Position::new(&audusd_sim, fill_small.into());
2761 assert_eq!(position_small.avg_px_open, 0.00001);
2762
2763 let last_price_small = Price::from("0.00002");
2765 let unrealized = position_small.unrealized_pnl(last_price_small);
2766 assert!(
2767 unrealized.as_f64() > 0.0,
2768 "Unrealized PnL should be positive when price doubles"
2769 );
2770
2771 let order_large = OrderTestBuilder::new(OrderType::Market)
2773 .instrument_id(audusd_sim.id())
2774 .side(OrderSide::Buy)
2775 .quantity(Quantity::from(100))
2776 .build();
2777
2778 let fill_large = TestOrderEventStubs::filled(
2779 &order_large,
2780 &audusd_sim,
2781 None,
2782 None,
2783 Some(Price::from("99999.99999")),
2784 Some(Quantity::from(100)),
2785 None,
2786 None,
2787 None,
2788 None,
2789 );
2790
2791 let position_large = Position::new(&audusd_sim, fill_large.into());
2792 assert!(
2793 (position_large.avg_px_open - 99999.99999).abs() < 1e-6,
2794 "Large price should be preserved within f64 tolerance"
2795 );
2796 }
2797
2798 #[rstest]
2799 fn test_position_pnl_roundtrip_precision(audusd_sim: CurrencyPair) {
2800 let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2802 let buy_order = OrderTestBuilder::new(OrderType::Market)
2803 .instrument_id(audusd_sim.id())
2804 .side(OrderSide::Buy)
2805 .quantity(Quantity::from(100_000))
2806 .build();
2807
2808 let sell_order = OrderTestBuilder::new(OrderType::Market)
2809 .instrument_id(audusd_sim.id())
2810 .side(OrderSide::Sell)
2811 .quantity(Quantity::from(100_000))
2812 .build();
2813
2814 let open_fill = TestOrderEventStubs::filled(
2816 &buy_order,
2817 &audusd_sim,
2818 Some(TradeId::new("1")),
2819 None,
2820 Some(Price::from("1.123456")),
2821 None,
2822 None,
2823 Some(Money::from("0.50 USD")),
2824 None,
2825 None,
2826 );
2827
2828 let mut position = Position::new(&audusd_sim, open_fill.into());
2829
2830 let close_fill = TestOrderEventStubs::filled(
2832 &sell_order,
2833 &audusd_sim,
2834 Some(TradeId::new("2")),
2835 None,
2836 Some(Price::from("1.123456")),
2837 None,
2838 None,
2839 Some(Money::from("0.50 USD")),
2840 None,
2841 None,
2842 );
2843
2844 position.apply(&close_fill.into());
2845
2846 assert!(position.is_closed());
2848
2849 let realized = position.realized_pnl.unwrap().as_f64();
2851 assert!(
2852 (realized - (-1.0)).abs() < 1e-10,
2853 "Realized PnL should be exactly -1.0 USD (commissions), was {realized}"
2854 );
2855 }
2856
2857 #[rstest]
2858 fn test_position_commission_in_base_currency_buy() {
2859 let btc_usdt = currency_pair_btcusdt();
2861 let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
2862
2863 let order = OrderTestBuilder::new(OrderType::Market)
2864 .instrument_id(btc_usdt.id())
2865 .side(OrderSide::Buy)
2866 .quantity(Quantity::from("1.0"))
2867 .build();
2868
2869 let fill = TestOrderEventStubs::filled(
2871 &order,
2872 &btc_usdt,
2873 Some(TradeId::new("1")),
2874 None,
2875 Some(Price::from("50000.0")),
2876 Some(Quantity::from("1.0")),
2877 None,
2878 Some(Money::new(-0.001, btc_usdt.base_currency().unwrap())),
2879 None,
2880 None,
2881 );
2882
2883 let position = Position::new(&btc_usdt, fill.into());
2884
2885 assert!(
2887 (position.quantity.as_f64() - 0.999).abs() < 1e-9,
2888 "Position quantity should be 0.999 BTC (1.0 - 0.001 commission), was {}",
2889 position.quantity.as_f64()
2890 );
2891
2892 assert!(
2894 (position.signed_qty - 0.999).abs() < 1e-9,
2895 "Signed qty should be 0.999, was {}",
2896 position.signed_qty
2897 );
2898
2899 assert_eq!(
2901 position.adjustments.len(),
2902 1,
2903 "Should have 1 adjustment event"
2904 );
2905 let adjustment = &position.adjustments[0];
2906 assert_eq!(
2907 adjustment.adjustment_type,
2908 PositionAdjustmentType::Commission
2909 );
2910 assert_eq!(
2911 adjustment.quantity_change,
2912 Some(rust_decimal_macros::dec!(-0.001))
2913 );
2914 assert_eq!(adjustment.pnl_change, None);
2915 }
2916
2917 #[rstest]
2918 fn test_position_commission_in_base_currency_sell() {
2919 let btc_usdt = currency_pair_btcusdt();
2921 let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
2922
2923 let order = OrderTestBuilder::new(OrderType::Market)
2924 .instrument_id(btc_usdt.id())
2925 .side(OrderSide::Sell)
2926 .quantity(Quantity::from("1.0"))
2927 .build();
2928
2929 let fill = TestOrderEventStubs::filled(
2931 &order,
2932 &btc_usdt,
2933 Some(TradeId::new("1")),
2934 None,
2935 Some(Price::from("50000.0")),
2936 Some(Quantity::from("1.0")),
2937 None,
2938 Some(Money::new(-0.001, btc_usdt.base_currency().unwrap())),
2939 None,
2940 None,
2941 );
2942
2943 let position = Position::new(&btc_usdt, fill.into());
2944
2945 assert!(
2948 (position.quantity.as_f64() - 1.001).abs() < 1e-9,
2949 "Position quantity should be 1.001 BTC (1.0 + 0.001 commission), was {}",
2950 position.quantity.as_f64()
2951 );
2952
2953 assert!(
2955 (position.signed_qty - (-1.001)).abs() < 1e-9,
2956 "Signed qty should be -1.001, was {}",
2957 position.signed_qty
2958 );
2959
2960 assert_eq!(
2962 position.adjustments.len(),
2963 1,
2964 "Should have 1 adjustment event"
2965 );
2966 let adjustment = &position.adjustments[0];
2967 assert_eq!(
2968 adjustment.adjustment_type,
2969 PositionAdjustmentType::Commission
2970 );
2971 assert_eq!(
2973 adjustment.quantity_change,
2974 Some(rust_decimal_macros::dec!(-0.001))
2975 );
2976 assert_eq!(adjustment.pnl_change, None);
2977 }
2978
2979 #[rstest]
2980 fn test_position_commission_in_quote_currency_no_adjustment() {
2981 let btc_usdt = currency_pair_btcusdt();
2983 let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
2984
2985 let order = OrderTestBuilder::new(OrderType::Market)
2986 .instrument_id(btc_usdt.id())
2987 .side(OrderSide::Buy)
2988 .quantity(Quantity::from("1.0"))
2989 .build();
2990
2991 let fill = TestOrderEventStubs::filled(
2993 &order,
2994 &btc_usdt,
2995 Some(TradeId::new("1")),
2996 None,
2997 Some(Price::from("50000.0")),
2998 Some(Quantity::from("1.0")),
2999 None,
3000 Some(Money::new(-50.0, Currency::USD())),
3001 None,
3002 None,
3003 );
3004
3005 let position = Position::new(&btc_usdt, fill.into());
3006
3007 assert!(
3009 (position.quantity.as_f64() - 1.0).abs() < 1e-9,
3010 "Position quantity should be 1.0 BTC (no adjustment for quote currency commission), was {}",
3011 position.quantity.as_f64()
3012 );
3013
3014 assert_eq!(
3016 position.adjustments.len(),
3017 0,
3018 "Should have no adjustment events for quote currency commission"
3019 );
3020 }
3021
3022 #[rstest]
3023 fn test_position_reset_clears_adjustments() {
3024 let btc_usdt = currency_pair_btcusdt();
3026 let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
3027
3028 let buy_order = OrderTestBuilder::new(OrderType::Market)
3030 .instrument_id(btc_usdt.id())
3031 .side(OrderSide::Buy)
3032 .quantity(Quantity::from("1.0"))
3033 .build();
3034
3035 let buy_fill = TestOrderEventStubs::filled(
3036 &buy_order,
3037 &btc_usdt,
3038 Some(TradeId::new("1")),
3039 None,
3040 Some(Price::from("50000.0")),
3041 Some(Quantity::from("1.0")),
3042 None,
3043 Some(Money::new(-0.001, btc_usdt.base_currency().unwrap())),
3044 None,
3045 None,
3046 );
3047
3048 let mut position = Position::new(&btc_usdt, buy_fill.into());
3049 assert_eq!(position.adjustments.len(), 1, "Should have 1 adjustment");
3050
3051 let sell_order = OrderTestBuilder::new(OrderType::Market)
3053 .instrument_id(btc_usdt.id())
3054 .side(OrderSide::Sell)
3055 .quantity(Quantity::from("0.999"))
3056 .build();
3057
3058 let sell_fill = TestOrderEventStubs::filled(
3059 &sell_order,
3060 &btc_usdt,
3061 Some(TradeId::new("2")),
3062 None,
3063 Some(Price::from("51000.0")),
3064 Some(Quantity::from("0.999")),
3065 None,
3066 Some(Money::new(-50.0, Currency::USD())), None,
3068 None,
3069 );
3070
3071 position.apply(&sell_fill.into());
3072 assert_eq!(position.side, PositionSide::Flat);
3073 assert_eq!(
3074 position.adjustments.len(),
3075 1,
3076 "Should still have 1 adjustment (no new one from quote commission)"
3077 );
3078
3079 let buy_order2 = OrderTestBuilder::new(OrderType::Market)
3081 .instrument_id(btc_usdt.id())
3082 .side(OrderSide::Buy)
3083 .quantity(Quantity::from("2.0"))
3084 .build();
3085
3086 let buy_fill2 = TestOrderEventStubs::filled(
3087 &buy_order2,
3088 &btc_usdt,
3089 Some(TradeId::new("3")),
3090 None,
3091 Some(Price::from("52000.0")),
3092 Some(Quantity::from("2.0")),
3093 None,
3094 Some(Money::new(-0.002, btc_usdt.base_currency().unwrap())),
3095 None,
3096 None,
3097 );
3098
3099 position.apply(&buy_fill2.into());
3100
3101 assert_eq!(
3103 position.adjustments.len(),
3104 1,
3105 "Adjustments should be cleared on position reset, only new adjustment"
3106 );
3107 assert_eq!(
3108 position.adjustments[0].quantity_change,
3109 Some(rust_decimal_macros::dec!(-0.002)),
3110 "New adjustment should be for the new fill"
3111 );
3112 assert_eq!(position.events.len(), 1, "Events should also be reset");
3113 }
3114
3115 #[rstest]
3116 fn test_purge_events_for_order_clears_adjustments_when_flat() {
3117 let btc_usdt = currency_pair_btcusdt();
3119 let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
3120
3121 let order = OrderTestBuilder::new(OrderType::Market)
3122 .instrument_id(btc_usdt.id())
3123 .side(OrderSide::Buy)
3124 .quantity(Quantity::from("1.0"))
3125 .build();
3126
3127 let fill = TestOrderEventStubs::filled(
3128 &order,
3129 &btc_usdt,
3130 Some(TradeId::new("1")),
3131 None,
3132 Some(Price::from("50000.0")),
3133 Some(Quantity::from("1.0")),
3134 None,
3135 Some(Money::new(-0.001, btc_usdt.base_currency().unwrap())),
3136 None,
3137 None,
3138 );
3139
3140 let mut position = Position::new(&btc_usdt, fill.into());
3141 assert_eq!(position.adjustments.len(), 1, "Should have 1 adjustment");
3142 assert_eq!(position.events.len(), 1);
3143
3144 position.purge_events_for_order(order.client_order_id());
3146
3147 assert_eq!(position.side, PositionSide::Flat);
3148 assert_eq!(position.events.len(), 0, "Events should be cleared");
3149 assert_eq!(
3150 position.adjustments.len(),
3151 0,
3152 "Adjustments should be cleared when position goes flat"
3153 );
3154 assert_eq!(position.quantity, Quantity::zero(btc_usdt.size_precision()));
3155 }
3156
3157 #[rstest]
3158 fn test_purge_events_for_order_clears_adjustments_on_rebuild() {
3159 let btc_usdt = currency_pair_btcusdt();
3161 let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
3162
3163 let order1 = OrderTestBuilder::new(OrderType::Market)
3165 .instrument_id(btc_usdt.id())
3166 .side(OrderSide::Buy)
3167 .quantity(Quantity::from("1.0"))
3168 .client_order_id(ClientOrderId::new("O-001"))
3169 .build();
3170
3171 let fill1 = TestOrderEventStubs::filled(
3172 &order1,
3173 &btc_usdt,
3174 Some(TradeId::new("1")),
3175 None,
3176 Some(Price::from("50000.0")),
3177 Some(Quantity::from("1.0")),
3178 None,
3179 Some(Money::new(-0.001, btc_usdt.base_currency().unwrap())),
3180 None,
3181 None,
3182 );
3183
3184 let mut position = Position::new(&btc_usdt, fill1.into());
3185 assert_eq!(position.adjustments.len(), 1);
3186
3187 let order2 = OrderTestBuilder::new(OrderType::Market)
3189 .instrument_id(btc_usdt.id())
3190 .side(OrderSide::Buy)
3191 .quantity(Quantity::from("2.0"))
3192 .client_order_id(ClientOrderId::new("O-002"))
3193 .build();
3194
3195 let fill2 = TestOrderEventStubs::filled(
3196 &order2,
3197 &btc_usdt,
3198 Some(TradeId::new("2")),
3199 None,
3200 Some(Price::from("51000.0")),
3201 Some(Quantity::from("2.0")),
3202 None,
3203 Some(Money::new(-0.002, btc_usdt.base_currency().unwrap())),
3204 None,
3205 None,
3206 );
3207
3208 position.apply(&fill2.into());
3209 assert_eq!(position.adjustments.len(), 2, "Should have 2 adjustments");
3210 assert_eq!(position.events.len(), 2);
3211
3212 position.purge_events_for_order(order1.client_order_id());
3214
3215 assert_eq!(position.events.len(), 1, "Should have 1 remaining event");
3216 assert_eq!(
3217 position.adjustments.len(),
3218 1,
3219 "Should have only the adjustment from remaining fill"
3220 );
3221 assert_eq!(
3222 position.adjustments[0].quantity_change,
3223 Some(rust_decimal_macros::dec!(-0.002)),
3224 "Should be the adjustment from order2"
3225 );
3226 assert!(
3227 (position.quantity.as_f64() - 1.998).abs() < 1e-9,
3228 "Quantity should be 2.0 - 0.002 commission"
3229 );
3230 }
3231
3232 #[rstest]
3233 fn test_purge_events_preserves_manual_adjustments() {
3234 let btc_usdt = currency_pair_btcusdt();
3236 let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
3237
3238 let order1 = OrderTestBuilder::new(OrderType::Market)
3240 .instrument_id(btc_usdt.id())
3241 .side(OrderSide::Buy)
3242 .quantity(Quantity::from("1.0"))
3243 .client_order_id(ClientOrderId::new("O-001"))
3244 .build();
3245
3246 let fill1 = TestOrderEventStubs::filled(
3247 &order1,
3248 &btc_usdt,
3249 Some(TradeId::new("1")),
3250 None,
3251 Some(Price::from("50000.0")),
3252 Some(Quantity::from("1.0")),
3253 None,
3254 Some(Money::new(-0.001, btc_usdt.base_currency().unwrap())),
3255 None,
3256 None,
3257 );
3258
3259 let mut position = Position::new(&btc_usdt, fill1.into());
3260 assert_eq!(position.adjustments.len(), 1);
3261
3262 use crate::events::PositionAdjusted;
3264 let funding_adjustment = PositionAdjusted::new(
3265 position.trader_id,
3266 position.strategy_id,
3267 position.instrument_id,
3268 position.id,
3269 position.account_id,
3270 PositionAdjustmentType::Funding,
3271 None,
3272 Some(Money::new(10.0, btc_usdt.quote_currency())),
3273 None, uuid4(),
3275 UnixNanos::default(),
3276 UnixNanos::default(),
3277 );
3278 position.apply_adjustment(funding_adjustment);
3279 assert_eq!(position.adjustments.len(), 2);
3280
3281 let order2 = OrderTestBuilder::new(OrderType::Market)
3283 .instrument_id(btc_usdt.id())
3284 .side(OrderSide::Buy)
3285 .quantity(Quantity::from("2.0"))
3286 .client_order_id(ClientOrderId::new("O-002"))
3287 .build();
3288
3289 let fill2 = TestOrderEventStubs::filled(
3290 &order2,
3291 &btc_usdt,
3292 Some(TradeId::new("2")),
3293 None,
3294 Some(Price::from("51000.0")),
3295 Some(Quantity::from("2.0")),
3296 None,
3297 Some(Money::new(-0.002, btc_usdt.base_currency().unwrap())),
3298 None,
3299 None,
3300 );
3301
3302 position.apply(&fill2.into());
3303 assert_eq!(
3304 position.adjustments.len(),
3305 3,
3306 "Should have 3 adjustments: 2 commissions + 1 funding"
3307 );
3308
3309 position.purge_events_for_order(order1.client_order_id());
3311
3312 assert_eq!(position.events.len(), 1, "Should have 1 remaining event");
3313 assert_eq!(
3314 position.adjustments.len(),
3315 2,
3316 "Should have funding adjustment + commission from remaining fill"
3317 );
3318
3319 let has_funding = position.adjustments.iter().any(|adj| {
3321 adj.adjustment_type == PositionAdjustmentType::Funding
3322 && adj.pnl_change == Some(Money::new(10.0, btc_usdt.quote_currency()))
3323 });
3324 assert!(has_funding, "Funding adjustment should be preserved");
3325
3326 assert_eq!(
3329 position.realized_pnl,
3330 Some(Money::new(10.0, btc_usdt.quote_currency())),
3331 "Realized PnL should be the funding payment only (commission is in BTC, not USDT)"
3332 );
3333 }
3334
3335 #[rstest]
3336 fn test_position_commission_affects_buy_and_sell_qty() {
3337 let btc_usdt = currency_pair_btcusdt();
3339 let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
3340
3341 let buy_order = OrderTestBuilder::new(OrderType::Market)
3342 .instrument_id(btc_usdt.id())
3343 .side(OrderSide::Buy)
3344 .quantity(Quantity::from("1.0"))
3345 .build();
3346
3347 let fill = TestOrderEventStubs::filled(
3349 &buy_order,
3350 &btc_usdt,
3351 Some(TradeId::new("1")),
3352 None,
3353 Some(Price::from("50000.0")),
3354 Some(Quantity::from("1.0")),
3355 None,
3356 Some(Money::new(-0.001, btc_usdt.base_currency().unwrap())),
3357 None,
3358 None,
3359 );
3360
3361 let position = Position::new(&btc_usdt, fill.into());
3362
3363 assert!(
3365 (position.buy_qty.as_f64() - 1.0).abs() < 1e-9,
3366 "buy_qty should be 1.0 (order fill amount), was {}",
3367 position.buy_qty.as_f64()
3368 );
3369
3370 assert!(
3372 (position.quantity.as_f64() - 0.999).abs() < 1e-9,
3373 "position.quantity should be 0.999 (1.0 - 0.001 commission), was {}",
3374 position.quantity.as_f64()
3375 );
3376
3377 assert_eq!(position.adjustments.len(), 1);
3379 assert_eq!(
3380 position.adjustments[0].quantity_change,
3381 Some(rust_decimal_macros::dec!(-0.001))
3382 );
3383 }
3384
3385 #[rstest]
3386 fn test_position_perpetual_commission_no_adjustment() {
3387 let eth_perp = crypto_perpetual_ethusdt();
3389 let eth_perp = InstrumentAny::CryptoPerpetual(eth_perp);
3390
3391 let order = OrderTestBuilder::new(OrderType::Market)
3392 .instrument_id(eth_perp.id())
3393 .side(OrderSide::Buy)
3394 .quantity(Quantity::from("1.0"))
3395 .build();
3396
3397 let fill = TestOrderEventStubs::filled(
3399 &order,
3400 ð_perp,
3401 Some(TradeId::new("1")),
3402 None,
3403 Some(Price::from("3000.0")),
3404 Some(Quantity::from("1.0")),
3405 None,
3406 Some(Money::new(-0.001, eth_perp.base_currency().unwrap())),
3407 None,
3408 None,
3409 );
3410
3411 let position = Position::new(ð_perp, fill.into());
3412
3413 assert!(
3415 (position.quantity.as_f64() - 1.0).abs() < 1e-9,
3416 "Perpetual position should be 1.0 contracts (no adjustment), was {}",
3417 position.quantity.as_f64()
3418 );
3419
3420 assert!(
3422 (position.signed_qty - 1.0).abs() < 1e-9,
3423 "Signed qty should be 1.0, was {}",
3424 position.signed_qty
3425 );
3426 }
3427}