nautilus_model/
position.rs

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