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