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