nautilus_model/
position.rs

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