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