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 and recalculates derived state.
148    ///
149    /// # Warning
150    ///
151    /// This operation recalculates the entire position from scratch after removing the specified
152    /// order's fills. This is an expensive operation and should be used sparingly.
153    ///
154    /// # Panics
155    ///
156    /// Panics if after purging, no fills remain and the position cannot be reconstructed.
157    pub fn purge_events_for_order(&mut self, client_order_id: ClientOrderId) {
158        // Filter out events from the specified order
159        let filtered_events: Vec<OrderFilled> = self
160            .events
161            .iter()
162            .filter(|e| e.client_order_id != client_order_id)
163            .copied()
164            .collect();
165
166        // If no events remain, log warning - position should be closed/removed instead
167        if filtered_events.is_empty() {
168            log::warn!(
169                "Position {} has no fills remaining after purging order {}; consider closing the position instead",
170                self.id,
171                client_order_id
172            );
173            // Reset to flat state, clearing all history
174            self.events.clear();
175            self.trade_ids.clear();
176            self.buy_qty = Quantity::zero(self.size_precision);
177            self.sell_qty = Quantity::zero(self.size_precision);
178            self.commissions.clear();
179            self.signed_qty = 0.0;
180            self.quantity = Quantity::zero(self.size_precision);
181            self.side = PositionSide::Flat;
182            self.avg_px_close = None;
183            self.realized_pnl = None;
184            self.realized_return = 0.0;
185            self.ts_opened = UnixNanos::default();
186            self.ts_last = UnixNanos::default();
187            self.ts_closed = Some(UnixNanos::default());
188            self.duration_ns = 0;
189            return;
190        }
191
192        // Recalculate position from scratch
193        // Save immutable fields needed for reconstruction
194        let position_id = self.id;
195        let size_precision = self.size_precision;
196
197        // Reset mutable state
198        self.events = Vec::new();
199        self.trade_ids = Vec::new();
200        self.buy_qty = Quantity::zero(size_precision);
201        self.sell_qty = Quantity::zero(size_precision);
202        self.commissions.clear();
203        self.signed_qty = 0.0;
204        self.quantity = Quantity::zero(size_precision);
205        self.peak_qty = Quantity::zero(size_precision);
206        self.side = PositionSide::Flat;
207        self.avg_px_open = 0.0;
208        self.avg_px_close = None;
209        self.realized_pnl = None;
210        self.realized_return = 0.0;
211
212        // Use the first remaining event to set opening state
213        let first_event = &filtered_events[0];
214        self.entry = first_event.order_side;
215        self.opening_order_id = first_event.client_order_id;
216        self.ts_opened = first_event.ts_event;
217        self.ts_init = first_event.ts_init;
218        self.closing_order_id = None;
219        self.ts_closed = None;
220        self.duration_ns = 0;
221
222        // Reapply all remaining fills to reconstruct state
223        for event in filtered_events {
224            self.apply(&event);
225        }
226
227        log::info!(
228            "Purged fills for order {} from position {}; recalculated state: qty={}, signed_qty={}, side={:?}",
229            client_order_id,
230            position_id,
231            self.quantity,
232            self.signed_qty,
233            self.side
234        );
235    }
236
237    /// Applies an `OrderFilled` event to this position.
238    ///
239    /// # Panics
240    ///
241    /// Panics if the `fill.trade_id` is already present in the position’s `trade_ids`.
242    pub fn apply(&mut self, fill: &OrderFilled) {
243        check_predicate_true(
244            !self.trade_ids.contains(&fill.trade_id),
245            "`fill.trade_id` already contained in `trade_ids",
246        )
247        .expect(FAILED);
248        check_predicate_true(fill.ts_event >= self.ts_opened, "fill.ts_event < ts_opened")
249            .expect(FAILED);
250
251        if self.side == PositionSide::Flat {
252            // Reset position
253            self.events.clear();
254            self.trade_ids.clear();
255            self.buy_qty = Quantity::zero(self.size_precision);
256            self.sell_qty = Quantity::zero(self.size_precision);
257            self.commissions.clear();
258            self.opening_order_id = fill.client_order_id;
259            self.closing_order_id = None;
260            self.peak_qty = Quantity::zero(self.size_precision);
261            self.ts_init = fill.ts_init;
262            self.ts_opened = fill.ts_event;
263            self.ts_closed = None;
264            self.duration_ns = 0;
265            self.avg_px_open = fill.last_px.as_f64();
266            self.avg_px_close = None;
267            self.realized_return = 0.0;
268            self.realized_pnl = None;
269        }
270
271        self.events.push(*fill);
272        self.trade_ids.push(fill.trade_id);
273
274        // Calculate cumulative commissions
275        if let Some(commission) = fill.commission {
276            let commission_currency = commission.currency;
277            if let Some(existing_commission) = self.commissions.get_mut(&commission_currency) {
278                *existing_commission += commission;
279            } else {
280                self.commissions.insert(commission_currency, commission);
281            }
282        }
283
284        // Calculate avg prices, points, return, PnL
285        match fill.specified_side() {
286            OrderSideSpecified::Buy => {
287                self.handle_buy_order_fill(fill);
288            }
289            OrderSideSpecified::Sell => {
290                self.handle_sell_order_fill(fill);
291            }
292        }
293
294        // Set quantities
295        // SAFETY: size_precision is valid from instrument
296        self.quantity = Quantity::new(self.signed_qty.abs(), self.size_precision);
297        if self.quantity > self.peak_qty {
298            self.peak_qty = self.quantity;
299        }
300
301        // Set state
302        if self.signed_qty > 0.0 {
303            self.entry = OrderSide::Buy;
304            self.side = PositionSide::Long;
305        } else if self.signed_qty < 0.0 {
306            self.entry = OrderSide::Sell;
307            self.side = PositionSide::Short;
308        } else {
309            self.side = PositionSide::Flat;
310            self.closing_order_id = Some(fill.client_order_id);
311            self.ts_closed = Some(fill.ts_event);
312            self.duration_ns = if let Some(ts_closed) = self.ts_closed {
313                ts_closed.as_u64() - self.ts_opened.as_u64()
314            } else {
315                0
316            };
317        }
318
319        self.ts_last = fill.ts_event;
320    }
321
322    fn handle_buy_order_fill(&mut self, fill: &OrderFilled) {
323        // Handle case where commission could be None or not settlement currency
324        let mut realized_pnl = if let Some(commission) = fill.commission {
325            if commission.currency == self.settlement_currency {
326                -commission.as_f64()
327            } else {
328                0.0
329            }
330        } else {
331            0.0
332        };
333
334        let last_px = fill.last_px.as_f64();
335        let last_qty = fill.last_qty.as_f64();
336        let last_qty_object = fill.last_qty;
337
338        if self.signed_qty > 0.0 {
339            self.avg_px_open = self.calculate_avg_px_open_px(last_px, last_qty);
340        } else if self.signed_qty < 0.0 {
341            // SHORT POSITION
342            let avg_px_close = self.calculate_avg_px_close_px(last_px, last_qty);
343            self.avg_px_close = Some(avg_px_close);
344            self.realized_return = self
345                .calculate_return(self.avg_px_open, avg_px_close)
346                .unwrap_or_else(|e| {
347                    log::error!("Error calculating return: {e}");
348                    0.0
349                });
350            realized_pnl += self
351                .calculate_pnl_raw(self.avg_px_open, last_px, last_qty)
352                .unwrap_or_else(|e| {
353                    log::error!("Error calculating PnL: {e}");
354                    0.0
355                });
356        }
357
358        let current_pnl = self.realized_pnl.map_or(0.0, |p| p.as_f64());
359        self.realized_pnl = Some(Money::new(
360            current_pnl + realized_pnl,
361            self.settlement_currency,
362        ));
363
364        self.signed_qty += last_qty;
365        self.buy_qty += last_qty_object;
366    }
367
368    fn handle_sell_order_fill(&mut self, fill: &OrderFilled) {
369        // Handle case where commission could be None or not settlement currency
370        let mut realized_pnl = if let Some(commission) = fill.commission {
371            if commission.currency == self.settlement_currency {
372                -commission.as_f64()
373            } else {
374                0.0
375            }
376        } else {
377            0.0
378        };
379
380        let last_px = fill.last_px.as_f64();
381        let last_qty = fill.last_qty.as_f64();
382        let last_qty_object = fill.last_qty;
383
384        if self.signed_qty < 0.0 {
385            self.avg_px_open = self.calculate_avg_px_open_px(last_px, last_qty);
386        } else if self.signed_qty > 0.0 {
387            let avg_px_close = self.calculate_avg_px_close_px(last_px, last_qty);
388            self.avg_px_close = Some(avg_px_close);
389            self.realized_return = self
390                .calculate_return(self.avg_px_open, avg_px_close)
391                .unwrap_or_else(|e| {
392                    log::error!("Error calculating return: {e}");
393                    0.0
394                });
395            realized_pnl += self
396                .calculate_pnl_raw(self.avg_px_open, last_px, last_qty)
397                .unwrap_or_else(|e| {
398                    log::error!("Error calculating PnL: {e}");
399                    0.0
400                });
401        }
402
403        let current_pnl = self.realized_pnl.map_or(0.0, |p| p.as_f64());
404        self.realized_pnl = Some(Money::new(
405            current_pnl + realized_pnl,
406            self.settlement_currency,
407        ));
408
409        self.signed_qty -= last_qty;
410        self.sell_qty += last_qty_object;
411    }
412
413    /// Calculates the average price using f64 arithmetic.
414    ///
415    /// # Design Decision: f64 vs Fixed-Point Arithmetic
416    ///
417    /// This function uses f64 arithmetic which provides sufficient precision for financial
418    /// calculations in this context. While f64 can introduce precision errors, the risk
419    /// is minimal here because:
420    ///
421    /// 1. **No cumulative error**: Each calculation starts fresh from precise Price and
422    ///    Quantity objects (derived from fixed-point raw values via `as_f64()`), rather
423    ///    than carrying f64 intermediate results between calculations.
424    ///
425    /// 2. **Single operation**: This is a single weighted average calculation, not a
426    ///    chain of operations where errors would compound.
427    ///
428    /// 3. **Overflow safety**: Raw integer arithmetic (price_raw * qty_raw) would risk
429    ///    overflow even with i128 intermediates, since max values can exceed integer limits.
430    ///
431    /// 4. **f64 precision**: ~15 decimal digits is sufficient for typical financial
432    ///    calculations at this level.
433    ///
434    /// For scenarios requiring higher precision (regulatory compliance, high-frequency
435    /// micro-calculations), consider using Decimal arithmetic libraries.
436    ///
437    /// # Empirical Precision Validation
438    ///
439    /// Testing confirms f64 arithmetic maintains accuracy for typical trading scenarios:
440    /// - **Typical amounts**: No precision loss for amounts ≥ 0.01 in standard currencies.
441    /// - **High-precision instruments**: 9-decimal crypto prices preserved within 1e-6 tolerance.
442    /// - **Many fills**: 100 sequential fills show no drift (commission accuracy to 1e-10).
443    /// - **Extreme prices**: Handles range from 0.00001 to 99999.99999 without overflow/underflow.
444    /// - **Round-trip**: Open/close at same price produces exact PnL (commissions only).
445    ///
446    /// See precision validation tests: `test_position_pnl_precision_*`
447    ///
448    /// # Errors
449    ///
450    /// Returns an error if:
451    /// - Both `qty` and `last_qty` are zero.
452    /// - `last_qty` is zero (prevents division by zero).
453    /// - `total_qty` is zero or negative (arithmetic error).
454    fn calculate_avg_px(
455        &self,
456        qty: f64,
457        avg_pg: f64,
458        last_px: f64,
459        last_qty: f64,
460    ) -> anyhow::Result<f64> {
461        if qty == 0.0 && last_qty == 0.0 {
462            anyhow::bail!("Cannot calculate average price: both quantities are zero");
463        }
464
465        if last_qty == 0.0 {
466            anyhow::bail!("Cannot calculate average price: fill quantity is zero");
467        }
468
469        if qty == 0.0 {
470            return Ok(last_px);
471        }
472
473        let start_cost = avg_pg * qty;
474        let event_cost = last_px * last_qty;
475        let total_qty = qty + last_qty;
476
477        // Runtime check to prevent division by zero even in release builds
478        if total_qty <= 0.0 {
479            anyhow::bail!(
480                "Total quantity unexpectedly zero or negative in average price calculation: qty={}, last_qty={}, total_qty={}",
481                qty,
482                last_qty,
483                total_qty
484            );
485        }
486
487        Ok((start_cost + event_cost) / total_qty)
488    }
489
490    fn calculate_avg_px_open_px(&self, last_px: f64, last_qty: f64) -> f64 {
491        self.calculate_avg_px(self.quantity.as_f64(), self.avg_px_open, last_px, last_qty)
492            .unwrap_or_else(|e| {
493                log::error!("Error calculating average open price: {}", e);
494                last_px
495            })
496    }
497
498    fn calculate_avg_px_close_px(&self, last_px: f64, last_qty: f64) -> f64 {
499        let Some(avg_px_close) = self.avg_px_close else {
500            return last_px;
501        };
502        let closing_qty = if self.side == PositionSide::Long {
503            self.sell_qty
504        } else {
505            self.buy_qty
506        };
507        self.calculate_avg_px(closing_qty.as_f64(), avg_px_close, last_px, last_qty)
508            .unwrap_or_else(|e| {
509                log::error!("Error calculating average close price: {}", e);
510                last_px
511            })
512    }
513
514    fn calculate_points(&self, avg_px_open: f64, avg_px_close: f64) -> f64 {
515        match self.side {
516            PositionSide::Long => avg_px_close - avg_px_open,
517            PositionSide::Short => avg_px_open - avg_px_close,
518            _ => 0.0, // FLAT
519        }
520    }
521
522    fn calculate_points_inverse(&self, avg_px_open: f64, avg_px_close: f64) -> anyhow::Result<f64> {
523        // Epsilon at the limit of IEEE f64 precision before rounding errors (f64::EPSILON ≈ 2.22e-16)
524        const EPSILON: f64 = 1e-15;
525
526        // Invalid state: zero or near-zero prices should never occur in valid market data
527        if avg_px_open.abs() < EPSILON {
528            anyhow::bail!(
529                "Cannot calculate inverse points: open price is zero or too small ({})",
530                avg_px_open
531            );
532        }
533        if avg_px_close.abs() < EPSILON {
534            anyhow::bail!(
535                "Cannot calculate inverse points: close price is zero or too small ({})",
536                avg_px_close
537            );
538        }
539
540        let inverse_open = 1.0 / avg_px_open;
541        let inverse_close = 1.0 / avg_px_close;
542        let result = match self.side {
543            PositionSide::Long => inverse_open - inverse_close,
544            PositionSide::Short => inverse_close - inverse_open,
545            _ => 0.0, // FLAT - this is a valid case
546        };
547        Ok(result)
548    }
549
550    fn calculate_return(&self, avg_px_open: f64, avg_px_close: f64) -> anyhow::Result<f64> {
551        // Prevent division by zero in return calculation
552        if avg_px_open == 0.0 {
553            anyhow::bail!(
554                "Cannot calculate return: open price is zero (close price: {})",
555                avg_px_close
556            );
557        }
558        Ok(self.calculate_points(avg_px_open, avg_px_close) / avg_px_open)
559    }
560
561    fn calculate_pnl_raw(
562        &self,
563        avg_px_open: f64,
564        avg_px_close: f64,
565        quantity: f64,
566    ) -> anyhow::Result<f64> {
567        let quantity = quantity.min(self.signed_qty.abs());
568        let result = if self.is_inverse {
569            let points = self.calculate_points_inverse(avg_px_open, avg_px_close)?;
570            quantity * self.multiplier.as_f64() * points
571        } else {
572            quantity * self.multiplier.as_f64() * self.calculate_points(avg_px_open, avg_px_close)
573        };
574        Ok(result)
575    }
576
577    #[must_use]
578    pub fn calculate_pnl(&self, avg_px_open: f64, avg_px_close: f64, quantity: Quantity) -> Money {
579        let pnl_raw = self
580            .calculate_pnl_raw(avg_px_open, avg_px_close, quantity.as_f64())
581            .unwrap_or_else(|e| {
582                log::error!("Error calculating PnL: {e}");
583                0.0
584            });
585        Money::new(pnl_raw, self.settlement_currency)
586    }
587
588    #[must_use]
589    pub fn total_pnl(&self, last: Price) -> Money {
590        let realized_pnl = self.realized_pnl.map_or(0.0, |pnl| pnl.as_f64());
591        Money::new(
592            realized_pnl + self.unrealized_pnl(last).as_f64(),
593            self.settlement_currency,
594        )
595    }
596
597    #[must_use]
598    pub fn unrealized_pnl(&self, last: Price) -> Money {
599        if self.side == PositionSide::Flat {
600            Money::new(0.0, self.settlement_currency)
601        } else {
602            let avg_px_open = self.avg_px_open;
603            let avg_px_close = last.as_f64();
604            let quantity = self.quantity.as_f64();
605            let pnl = self
606                .calculate_pnl_raw(avg_px_open, avg_px_close, quantity)
607                .unwrap_or_else(|e| {
608                    log::error!("Error calculating unrealized PnL: {e}");
609                    0.0
610                });
611            Money::new(pnl, self.settlement_currency)
612        }
613    }
614
615    pub fn closing_order_side(&self) -> OrderSide {
616        match self.side {
617            PositionSide::Long => OrderSide::Sell,
618            PositionSide::Short => OrderSide::Buy,
619            _ => OrderSide::NoOrderSide,
620        }
621    }
622
623    #[must_use]
624    pub fn is_opposite_side(&self, side: OrderSide) -> bool {
625        self.entry != side
626    }
627
628    #[must_use]
629    pub fn symbol(&self) -> Symbol {
630        self.instrument_id.symbol
631    }
632
633    #[must_use]
634    pub fn venue(&self) -> Venue {
635        self.instrument_id.venue
636    }
637
638    #[must_use]
639    pub fn event_count(&self) -> usize {
640        self.events.len()
641    }
642
643    #[must_use]
644    pub fn client_order_ids(&self) -> Vec<ClientOrderId> {
645        // First to hash set to remove duplicate, then again iter to vector
646        let mut result = self
647            .events
648            .iter()
649            .map(|event| event.client_order_id)
650            .collect::<HashSet<ClientOrderId>>()
651            .into_iter()
652            .collect::<Vec<ClientOrderId>>();
653        result.sort_unstable();
654        result
655    }
656
657    #[must_use]
658    pub fn venue_order_ids(&self) -> Vec<VenueOrderId> {
659        // First to hash set to remove duplicate, then again iter to vector
660        let mut result = self
661            .events
662            .iter()
663            .map(|event| event.venue_order_id)
664            .collect::<HashSet<VenueOrderId>>()
665            .into_iter()
666            .collect::<Vec<VenueOrderId>>();
667        result.sort_unstable();
668        result
669    }
670
671    #[must_use]
672    pub fn trade_ids(&self) -> Vec<TradeId> {
673        let mut result = self
674            .events
675            .iter()
676            .map(|event| event.trade_id)
677            .collect::<HashSet<TradeId>>()
678            .into_iter()
679            .collect::<Vec<TradeId>>();
680        result.sort_unstable();
681        result
682    }
683
684    /// Calculates the notional value based on the last price.
685    ///
686    /// # Panics
687    ///
688    /// Panics if `self.base_currency` is `None`.
689    #[must_use]
690    pub fn notional_value(&self, last: Price) -> Money {
691        if self.is_inverse {
692            Money::new(
693                self.quantity.as_f64() * self.multiplier.as_f64() * (1.0 / last.as_f64()),
694                self.base_currency.unwrap(),
695            )
696        } else {
697            Money::new(
698                self.quantity.as_f64() * last.as_f64() * self.multiplier.as_f64(),
699                self.quote_currency,
700            )
701        }
702    }
703
704    /// Returns the last `OrderFilled` event for the position (if any after purging).
705    #[must_use]
706    pub fn last_event(&self) -> Option<OrderFilled> {
707        self.events.last().copied()
708    }
709
710    #[must_use]
711    pub fn last_trade_id(&self) -> Option<TradeId> {
712        self.trade_ids.last().copied()
713    }
714
715    #[must_use]
716    pub fn is_long(&self) -> bool {
717        self.side == PositionSide::Long
718    }
719
720    #[must_use]
721    pub fn is_short(&self) -> bool {
722        self.side == PositionSide::Short
723    }
724
725    #[must_use]
726    pub fn is_open(&self) -> bool {
727        self.side != PositionSide::Flat && self.ts_closed.is_none()
728    }
729
730    #[must_use]
731    pub fn is_closed(&self) -> bool {
732        self.side == PositionSide::Flat && self.ts_closed.is_some()
733    }
734
735    #[must_use]
736    pub fn commissions(&self) -> Vec<Money> {
737        self.commissions.values().copied().collect()
738    }
739}
740
741impl PartialEq<Self> for Position {
742    fn eq(&self, other: &Self) -> bool {
743        self.id == other.id
744    }
745}
746
747impl Eq for Position {}
748
749impl Hash for Position {
750    fn hash<H: Hasher>(&self, state: &mut H) {
751        self.id.hash(state);
752    }
753}
754
755impl Display for Position {
756    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
757        let quantity_str = if self.quantity != Quantity::zero(self.price_precision) {
758            self.quantity.to_formatted_string() + " "
759        } else {
760            String::new()
761        };
762        write!(
763            f,
764            "Position({} {}{}, id={})",
765            self.side, quantity_str, self.instrument_id, self.id
766        )
767    }
768}
769
770////////////////////////////////////////////////////////////////////////////////
771// Tests
772////////////////////////////////////////////////////////////////////////////////
773#[cfg(test)]
774mod tests {
775    use std::str::FromStr;
776
777    use nautilus_core::UnixNanos;
778    use rstest::rstest;
779
780    use crate::{
781        enums::{LiquiditySide, OrderSide, OrderType, PositionSide},
782        events::OrderFilled,
783        identifiers::{
784            AccountId, ClientOrderId, PositionId, StrategyId, TradeId, VenueOrderId, stubs::uuid4,
785        },
786        instruments::{CryptoPerpetual, CurrencyPair, Instrument, InstrumentAny, stubs::*},
787        orders::{Order, builder::OrderTestBuilder, stubs::TestOrderEventStubs},
788        position::Position,
789        stubs::*,
790        types::{Currency, Money, Price, Quantity},
791    };
792
793    #[rstest]
794    fn test_position_long_display(stub_position_long: Position) {
795        let display = format!("{stub_position_long}");
796        assert_eq!(display, "Position(LONG 1 AUD/USD.SIM, id=1)");
797    }
798
799    #[rstest]
800    fn test_position_short_display(stub_position_short: Position) {
801        let display = format!("{stub_position_short}");
802        assert_eq!(display, "Position(SHORT 1 AUD/USD.SIM, id=1)");
803    }
804
805    #[rstest]
806    #[should_panic(expected = "`fill.trade_id` already contained in `trade_ids")]
807    fn test_two_trades_with_same_trade_id_error(audusd_sim: CurrencyPair) {
808        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
809        let order1 = OrderTestBuilder::new(OrderType::Market)
810            .instrument_id(audusd_sim.id())
811            .side(OrderSide::Buy)
812            .quantity(Quantity::from(100_000))
813            .build();
814        let order2 = OrderTestBuilder::new(OrderType::Market)
815            .instrument_id(audusd_sim.id())
816            .side(OrderSide::Buy)
817            .quantity(Quantity::from(100_000))
818            .build();
819        let fill1 = TestOrderEventStubs::filled(
820            &order1,
821            &audusd_sim,
822            Some(TradeId::new("1")),
823            None,
824            Some(Price::from("1.00001")),
825            None,
826            None,
827            None,
828            None,
829            None,
830        );
831        let fill2 = TestOrderEventStubs::filled(
832            &order2,
833            &audusd_sim,
834            Some(TradeId::new("1")),
835            None,
836            Some(Price::from("1.00002")),
837            None,
838            None,
839            None,
840            None,
841            None,
842        );
843        let mut position = Position::new(&audusd_sim, fill1.into());
844        position.apply(&fill2.into());
845    }
846
847    #[rstest]
848    fn test_position_filled_with_buy_order(audusd_sim: CurrencyPair) {
849        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
850        let order = OrderTestBuilder::new(OrderType::Market)
851            .instrument_id(audusd_sim.id())
852            .side(OrderSide::Buy)
853            .quantity(Quantity::from(100_000))
854            .build();
855        let fill = TestOrderEventStubs::filled(
856            &order,
857            &audusd_sim,
858            None,
859            None,
860            Some(Price::from("1.00001")),
861            None,
862            None,
863            None,
864            None,
865            None,
866        );
867        let last_price = Price::from_str("1.0005").unwrap();
868        let position = Position::new(&audusd_sim, fill.into());
869        assert_eq!(position.symbol(), audusd_sim.id().symbol);
870        assert_eq!(position.venue(), audusd_sim.id().venue);
871        assert_eq!(position.closing_order_side(), OrderSide::Sell);
872        assert!(!position.is_opposite_side(OrderSide::Buy));
873        assert_eq!(position, position); // equality operator test
874        assert!(position.closing_order_id.is_none());
875        assert_eq!(position.quantity, Quantity::from(100_000));
876        assert_eq!(position.peak_qty, Quantity::from(100_000));
877        assert_eq!(position.size_precision, 0);
878        assert_eq!(position.signed_qty, 100_000.0);
879        assert_eq!(position.entry, OrderSide::Buy);
880        assert_eq!(position.side, PositionSide::Long);
881        assert_eq!(position.ts_opened.as_u64(), 0);
882        assert_eq!(position.duration_ns, 0);
883        assert_eq!(position.avg_px_open, 1.00001);
884        assert_eq!(position.event_count(), 1);
885        assert_eq!(position.id, PositionId::new("1"));
886        assert_eq!(position.events.len(), 1);
887        assert!(position.is_long());
888        assert!(!position.is_short());
889        assert!(position.is_open());
890        assert!(!position.is_closed());
891        assert_eq!(position.realized_return, 0.0);
892        assert_eq!(position.realized_pnl, Some(Money::from("-2.0 USD")));
893        assert_eq!(position.unrealized_pnl(last_price), Money::from("49.0 USD"));
894        assert_eq!(position.total_pnl(last_price), Money::from("47.0 USD"));
895        assert_eq!(position.commissions(), vec![Money::from("2.0 USD")]);
896        assert_eq!(
897            format!("{position}"),
898            "Position(LONG 100_000 AUD/USD.SIM, id=1)"
899        );
900    }
901
902    #[rstest]
903    fn test_position_filled_with_sell_order(audusd_sim: CurrencyPair) {
904        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
905        let order = OrderTestBuilder::new(OrderType::Market)
906            .instrument_id(audusd_sim.id())
907            .side(OrderSide::Sell)
908            .quantity(Quantity::from(100_000))
909            .build();
910        let fill = TestOrderEventStubs::filled(
911            &order,
912            &audusd_sim,
913            None,
914            None,
915            Some(Price::from("1.00001")),
916            None,
917            None,
918            None,
919            None,
920            None,
921        );
922        let last_price = Price::from_str("1.00050").unwrap();
923        let position = Position::new(&audusd_sim, fill.into());
924        assert_eq!(position.symbol(), audusd_sim.id().symbol);
925        assert_eq!(position.venue(), audusd_sim.id().venue);
926        assert_eq!(position.closing_order_side(), OrderSide::Buy);
927        assert!(!position.is_opposite_side(OrderSide::Sell));
928        assert_eq!(position, position); // Equality operator test
929        assert!(position.closing_order_id.is_none());
930        assert_eq!(position.quantity, Quantity::from(100_000));
931        assert_eq!(position.peak_qty, Quantity::from(100_000));
932        assert_eq!(position.signed_qty, -100_000.0);
933        assert_eq!(position.entry, OrderSide::Sell);
934        assert_eq!(position.side, PositionSide::Short);
935        assert_eq!(position.ts_opened.as_u64(), 0);
936        assert_eq!(position.avg_px_open, 1.00001);
937        assert_eq!(position.event_count(), 1);
938        assert_eq!(position.id, PositionId::new("1"));
939        assert_eq!(position.events.len(), 1);
940        assert!(!position.is_long());
941        assert!(position.is_short());
942        assert!(position.is_open());
943        assert!(!position.is_closed());
944        assert_eq!(position.realized_return, 0.0);
945        assert_eq!(position.realized_pnl, Some(Money::from("-2.0 USD")));
946        assert_eq!(
947            position.unrealized_pnl(last_price),
948            Money::from("-49.0 USD")
949        );
950        assert_eq!(position.total_pnl(last_price), Money::from("-51.0 USD"));
951        assert_eq!(position.commissions(), vec![Money::from("2.0 USD")]);
952        assert_eq!(
953            format!("{position}"),
954            "Position(SHORT 100_000 AUD/USD.SIM, id=1)"
955        );
956    }
957
958    #[rstest]
959    fn test_position_partial_fills_with_buy_order(audusd_sim: CurrencyPair) {
960        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
961        let order = OrderTestBuilder::new(OrderType::Market)
962            .instrument_id(audusd_sim.id())
963            .side(OrderSide::Buy)
964            .quantity(Quantity::from(100_000))
965            .build();
966        let fill = TestOrderEventStubs::filled(
967            &order,
968            &audusd_sim,
969            None,
970            None,
971            Some(Price::from("1.00001")),
972            Some(Quantity::from(50_000)),
973            None,
974            None,
975            None,
976            None,
977        );
978        let last_price = Price::from_str("1.00048").unwrap();
979        let position = Position::new(&audusd_sim, fill.into());
980        assert_eq!(position.quantity, Quantity::from(50_000));
981        assert_eq!(position.peak_qty, Quantity::from(50_000));
982        assert_eq!(position.side, PositionSide::Long);
983        assert_eq!(position.signed_qty, 50000.0);
984        assert_eq!(position.avg_px_open, 1.00001);
985        assert_eq!(position.event_count(), 1);
986        assert_eq!(position.ts_opened.as_u64(), 0);
987        assert!(position.is_long());
988        assert!(!position.is_short());
989        assert!(position.is_open());
990        assert!(!position.is_closed());
991        assert_eq!(position.realized_return, 0.0);
992        assert_eq!(position.realized_pnl, Some(Money::from("-2.0 USD")));
993        assert_eq!(position.unrealized_pnl(last_price), Money::from("23.5 USD"));
994        assert_eq!(position.total_pnl(last_price), Money::from("21.5 USD"));
995        assert_eq!(position.commissions(), vec![Money::from("2.0 USD")]);
996        assert_eq!(
997            format!("{position}"),
998            "Position(LONG 50_000 AUD/USD.SIM, id=1)"
999        );
1000    }
1001
1002    #[rstest]
1003    fn test_position_partial_fills_with_two_sell_orders(audusd_sim: CurrencyPair) {
1004        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1005        let order = OrderTestBuilder::new(OrderType::Market)
1006            .instrument_id(audusd_sim.id())
1007            .side(OrderSide::Sell)
1008            .quantity(Quantity::from(100_000))
1009            .build();
1010        let fill1 = TestOrderEventStubs::filled(
1011            &order,
1012            &audusd_sim,
1013            Some(TradeId::new("1")),
1014            None,
1015            Some(Price::from("1.00001")),
1016            Some(Quantity::from(50_000)),
1017            None,
1018            None,
1019            None,
1020            None,
1021        );
1022        let fill2 = TestOrderEventStubs::filled(
1023            &order,
1024            &audusd_sim,
1025            Some(TradeId::new("2")),
1026            None,
1027            Some(Price::from("1.00002")),
1028            Some(Quantity::from(50_000)),
1029            None,
1030            None,
1031            None,
1032            None,
1033        );
1034        let last_price = Price::from_str("1.0005").unwrap();
1035        let mut position = Position::new(&audusd_sim, fill1.into());
1036        position.apply(&fill2.into());
1037
1038        assert_eq!(position.quantity, Quantity::from(100_000));
1039        assert_eq!(position.peak_qty, Quantity::from(100_000));
1040        assert_eq!(position.side, PositionSide::Short);
1041        assert_eq!(position.signed_qty, -100_000.0);
1042        assert_eq!(position.avg_px_open, 1.000_015);
1043        assert_eq!(position.event_count(), 2);
1044        assert_eq!(position.ts_opened, 0);
1045        assert!(position.is_short());
1046        assert!(!position.is_long());
1047        assert!(position.is_open());
1048        assert!(!position.is_closed());
1049        assert_eq!(position.realized_return, 0.0);
1050        assert_eq!(position.realized_pnl, Some(Money::from("-4.0 USD")));
1051        assert_eq!(
1052            position.unrealized_pnl(last_price),
1053            Money::from("-48.5 USD")
1054        );
1055        assert_eq!(position.total_pnl(last_price), Money::from("-52.5 USD"));
1056        assert_eq!(position.commissions(), vec![Money::from("4.0 USD")]);
1057    }
1058
1059    #[rstest]
1060    pub fn test_position_filled_with_buy_order_then_sell_order(audusd_sim: CurrencyPair) {
1061        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1062        let order = OrderTestBuilder::new(OrderType::Market)
1063            .instrument_id(audusd_sim.id())
1064            .side(OrderSide::Buy)
1065            .quantity(Quantity::from(150_000))
1066            .build();
1067        let fill = TestOrderEventStubs::filled(
1068            &order,
1069            &audusd_sim,
1070            Some(TradeId::new("1")),
1071            Some(PositionId::new("P-1")),
1072            Some(Price::from("1.00001")),
1073            None,
1074            None,
1075            None,
1076            Some(UnixNanos::from(1_000_000_000)),
1077            None,
1078        );
1079        let mut position = Position::new(&audusd_sim, fill.into());
1080
1081        let fill2 = OrderFilled::new(
1082            order.trader_id(),
1083            StrategyId::new("S-001"),
1084            order.instrument_id(),
1085            order.client_order_id(),
1086            VenueOrderId::from("2"),
1087            order.account_id().unwrap_or(AccountId::new("SIM-001")),
1088            TradeId::new("2"),
1089            OrderSide::Sell,
1090            OrderType::Market,
1091            order.quantity(),
1092            Price::from("1.00011"),
1093            audusd_sim.quote_currency(),
1094            LiquiditySide::Taker,
1095            uuid4(),
1096            2_000_000_000.into(),
1097            0.into(),
1098            false,
1099            Some(PositionId::new("T1")),
1100            Some(Money::from("0.0 USD")),
1101        );
1102        position.apply(&fill2);
1103        let last = Price::from_str("1.0005").unwrap();
1104
1105        assert!(position.is_opposite_side(fill2.order_side));
1106        assert_eq!(
1107            position.quantity,
1108            Quantity::zero(audusd_sim.price_precision())
1109        );
1110        assert_eq!(position.size_precision, 0);
1111        assert_eq!(position.signed_qty, 0.0);
1112        assert_eq!(position.side, PositionSide::Flat);
1113        assert_eq!(position.ts_opened, 1_000_000_000);
1114        assert_eq!(position.ts_closed, Some(UnixNanos::from(2_000_000_000)));
1115        assert_eq!(position.duration_ns, 1_000_000_000);
1116        assert_eq!(position.avg_px_open, 1.00001);
1117        assert_eq!(position.avg_px_close, Some(1.00011));
1118        assert!(!position.is_long());
1119        assert!(!position.is_short());
1120        assert!(!position.is_open());
1121        assert!(position.is_closed());
1122        assert_eq!(position.realized_return, 9.999_900_000_998_888e-5);
1123        assert_eq!(position.realized_pnl, Some(Money::from("13.0 USD")));
1124        assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1125        assert_eq!(position.commissions(), vec![Money::from("2 USD")]);
1126        assert_eq!(position.total_pnl(last), Money::from("13 USD"));
1127        assert_eq!(format!("{position}"), "Position(FLAT AUD/USD.SIM, id=P-1)");
1128    }
1129
1130    #[rstest]
1131    pub fn test_position_filled_with_sell_order_then_buy_order(audusd_sim: CurrencyPair) {
1132        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1133        let order1 = OrderTestBuilder::new(OrderType::Market)
1134            .instrument_id(audusd_sim.id())
1135            .side(OrderSide::Sell)
1136            .quantity(Quantity::from(100_000))
1137            .build();
1138        let order2 = OrderTestBuilder::new(OrderType::Market)
1139            .instrument_id(audusd_sim.id())
1140            .side(OrderSide::Buy)
1141            .quantity(Quantity::from(100_000))
1142            .build();
1143        let fill1 = TestOrderEventStubs::filled(
1144            &order1,
1145            &audusd_sim,
1146            None,
1147            Some(PositionId::new("P-19700101-000000-001-001-1")),
1148            Some(Price::from("1.0")),
1149            None,
1150            None,
1151            None,
1152            None,
1153            None,
1154        );
1155        let mut position = Position::new(&audusd_sim, fill1.into());
1156        // create closing from order from different venue but same strategy
1157        let fill2 = TestOrderEventStubs::filled(
1158            &order2,
1159            &audusd_sim,
1160            Some(TradeId::new("1")),
1161            Some(PositionId::new("P-19700101-000000-001-001-1")),
1162            Some(Price::from("1.00001")),
1163            Some(Quantity::from(50_000)),
1164            None,
1165            None,
1166            None,
1167            None,
1168        );
1169        let fill3 = TestOrderEventStubs::filled(
1170            &order2,
1171            &audusd_sim,
1172            Some(TradeId::new("2")),
1173            Some(PositionId::new("P-19700101-000000-001-001-1")),
1174            Some(Price::from("1.00003")),
1175            Some(Quantity::from(50_000)),
1176            None,
1177            None,
1178            None,
1179            None,
1180        );
1181        let last = Price::from("1.0005");
1182        position.apply(&fill2.into());
1183        position.apply(&fill3.into());
1184
1185        assert_eq!(
1186            position.quantity,
1187            Quantity::zero(audusd_sim.price_precision())
1188        );
1189        assert_eq!(position.side, PositionSide::Flat);
1190        assert_eq!(position.ts_opened, 0);
1191        assert_eq!(position.avg_px_open, 1.0);
1192        assert_eq!(position.events.len(), 3);
1193        assert_eq!(position.ts_closed, Some(UnixNanos::default()));
1194        assert_eq!(position.avg_px_close, Some(1.00002));
1195        assert!(!position.is_long());
1196        assert!(!position.is_short());
1197        assert!(!position.is_open());
1198        assert!(position.is_closed());
1199        assert_eq!(position.commissions(), vec![Money::from("6.0 USD")]);
1200        assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1201        assert_eq!(position.realized_pnl, Some(Money::from("-8.0 USD")));
1202        assert_eq!(position.total_pnl(last), Money::from("-8.0 USD"));
1203        assert_eq!(
1204            format!("{position}"),
1205            "Position(FLAT AUD/USD.SIM, id=P-19700101-000000-001-001-1)"
1206        );
1207    }
1208
1209    #[rstest]
1210    fn test_position_filled_with_no_change(audusd_sim: CurrencyPair) {
1211        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1212        let order1 = OrderTestBuilder::new(OrderType::Market)
1213            .instrument_id(audusd_sim.id())
1214            .side(OrderSide::Buy)
1215            .quantity(Quantity::from(100_000))
1216            .build();
1217        let order2 = OrderTestBuilder::new(OrderType::Market)
1218            .instrument_id(audusd_sim.id())
1219            .side(OrderSide::Sell)
1220            .quantity(Quantity::from(100_000))
1221            .build();
1222        let fill1 = TestOrderEventStubs::filled(
1223            &order1,
1224            &audusd_sim,
1225            Some(TradeId::new("1")),
1226            Some(PositionId::new("P-19700101-000000-001-001-1")),
1227            Some(Price::from("1.0")),
1228            None,
1229            None,
1230            None,
1231            None,
1232            None,
1233        );
1234        let mut position = Position::new(&audusd_sim, fill1.into());
1235        let fill2 = TestOrderEventStubs::filled(
1236            &order2,
1237            &audusd_sim,
1238            Some(TradeId::new("2")),
1239            Some(PositionId::new("P-19700101-000000-001-001-1")),
1240            Some(Price::from("1.0")),
1241            None,
1242            None,
1243            None,
1244            None,
1245            None,
1246        );
1247        let last = Price::from("1.0005");
1248        position.apply(&fill2.into());
1249
1250        assert_eq!(
1251            position.quantity,
1252            Quantity::zero(audusd_sim.price_precision())
1253        );
1254        assert_eq!(position.closing_order_side(), OrderSide::NoOrderSide);
1255        assert_eq!(position.side, PositionSide::Flat);
1256        assert_eq!(position.ts_opened, 0);
1257        assert_eq!(position.avg_px_open, 1.0);
1258        assert_eq!(position.events.len(), 2);
1259        // assert_eq!(position.trade_ids, vec![fill1.trade_id, fill2.trade_id]);  // TODO
1260        assert_eq!(position.ts_closed, Some(UnixNanos::default()));
1261        assert_eq!(position.avg_px_close, Some(1.0));
1262        assert!(!position.is_long());
1263        assert!(!position.is_short());
1264        assert!(!position.is_open());
1265        assert!(position.is_closed());
1266        assert_eq!(position.commissions(), vec![Money::from("4.0 USD")]);
1267        assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1268        assert_eq!(position.realized_pnl, Some(Money::from("-4.0 USD")));
1269        assert_eq!(position.total_pnl(last), Money::from("-4.0 USD"));
1270        assert_eq!(
1271            format!("{position}"),
1272            "Position(FLAT AUD/USD.SIM, id=P-19700101-000000-001-001-1)"
1273        );
1274    }
1275
1276    #[rstest]
1277    fn test_position_long_with_multiple_filled_orders(audusd_sim: CurrencyPair) {
1278        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1279        let order1 = OrderTestBuilder::new(OrderType::Market)
1280            .instrument_id(audusd_sim.id())
1281            .side(OrderSide::Buy)
1282            .quantity(Quantity::from(100_000))
1283            .build();
1284        let order2 = OrderTestBuilder::new(OrderType::Market)
1285            .instrument_id(audusd_sim.id())
1286            .side(OrderSide::Buy)
1287            .quantity(Quantity::from(100_000))
1288            .build();
1289        let order3 = OrderTestBuilder::new(OrderType::Market)
1290            .instrument_id(audusd_sim.id())
1291            .side(OrderSide::Sell)
1292            .quantity(Quantity::from(200_000))
1293            .build();
1294        let fill1 = TestOrderEventStubs::filled(
1295            &order1,
1296            &audusd_sim,
1297            Some(TradeId::new("1")),
1298            Some(PositionId::new("P-123456")),
1299            Some(Price::from("1.0")),
1300            None,
1301            None,
1302            None,
1303            None,
1304            None,
1305        );
1306        let fill2 = TestOrderEventStubs::filled(
1307            &order2,
1308            &audusd_sim,
1309            Some(TradeId::new("2")),
1310            Some(PositionId::new("P-123456")),
1311            Some(Price::from("1.00001")),
1312            None,
1313            None,
1314            None,
1315            None,
1316            None,
1317        );
1318        let fill3 = TestOrderEventStubs::filled(
1319            &order3,
1320            &audusd_sim,
1321            Some(TradeId::new("3")),
1322            Some(PositionId::new("P-123456")),
1323            Some(Price::from("1.0001")),
1324            None,
1325            None,
1326            None,
1327            None,
1328            None,
1329        );
1330        let mut position = Position::new(&audusd_sim, fill1.into());
1331        let last = Price::from("1.0005");
1332        position.apply(&fill2.into());
1333        position.apply(&fill3.into());
1334
1335        assert_eq!(
1336            position.quantity,
1337            Quantity::zero(audusd_sim.price_precision())
1338        );
1339        assert_eq!(position.side, PositionSide::Flat);
1340        assert_eq!(position.ts_opened, 0);
1341        assert_eq!(position.avg_px_open, 1.000_005);
1342        assert_eq!(position.events.len(), 3);
1343        // assert_eq!(
1344        //     position.trade_ids,
1345        //     vec![fill1.trade_id, fill2.trade_id, fill3.trade_id]
1346        // );
1347        assert_eq!(position.ts_closed, Some(UnixNanos::default()));
1348        assert_eq!(position.avg_px_close, Some(1.0001));
1349        assert!(position.is_closed());
1350        assert!(!position.is_open());
1351        assert!(!position.is_long());
1352        assert!(!position.is_short());
1353        assert_eq!(position.commissions(), vec![Money::from("6.0 USD")]);
1354        assert_eq!(position.realized_pnl, Some(Money::from("13.0 USD")));
1355        assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1356        assert_eq!(position.total_pnl(last), Money::from("13 USD"));
1357        assert_eq!(
1358            format!("{position}"),
1359            "Position(FLAT AUD/USD.SIM, id=P-123456)"
1360        );
1361    }
1362
1363    #[rstest]
1364    fn test_pnl_calculation_from_trading_technologies_example(currency_pair_ethusdt: CurrencyPair) {
1365        let ethusdt = InstrumentAny::CurrencyPair(currency_pair_ethusdt);
1366        let quantity1 = Quantity::from(12);
1367        let price1 = Price::from("100.0");
1368        let order1 = OrderTestBuilder::new(OrderType::Market)
1369            .instrument_id(ethusdt.id())
1370            .side(OrderSide::Buy)
1371            .quantity(quantity1)
1372            .build();
1373        let commission1 = calculate_commission(&ethusdt, order1.quantity(), price1, None);
1374        let fill1 = TestOrderEventStubs::filled(
1375            &order1,
1376            &ethusdt,
1377            Some(TradeId::new("1")),
1378            Some(PositionId::new("P-123456")),
1379            Some(price1),
1380            None,
1381            None,
1382            Some(commission1),
1383            None,
1384            None,
1385        );
1386        let mut position = Position::new(&ethusdt, fill1.into());
1387        let quantity2 = Quantity::from(17);
1388        let order2 = OrderTestBuilder::new(OrderType::Market)
1389            .instrument_id(ethusdt.id())
1390            .side(OrderSide::Buy)
1391            .quantity(quantity2)
1392            .build();
1393        let price2 = Price::from("99.0");
1394        let commission2 = calculate_commission(&ethusdt, order2.quantity(), price2, None);
1395        let fill2 = TestOrderEventStubs::filled(
1396            &order2,
1397            &ethusdt,
1398            Some(TradeId::new("2")),
1399            Some(PositionId::new("P-123456")),
1400            Some(price2),
1401            None,
1402            None,
1403            Some(commission2),
1404            None,
1405            None,
1406        );
1407        position.apply(&fill2.into());
1408        assert_eq!(position.quantity, Quantity::from(29));
1409        assert_eq!(position.realized_pnl, Some(Money::from("-0.28830000 USDT")));
1410        assert_eq!(position.avg_px_open, 99.413_793_103_448_27);
1411        let quantity3 = Quantity::from(9);
1412        let order3 = OrderTestBuilder::new(OrderType::Market)
1413            .instrument_id(ethusdt.id())
1414            .side(OrderSide::Sell)
1415            .quantity(quantity3)
1416            .build();
1417        let price3 = Price::from("101.0");
1418        let commission3 = calculate_commission(&ethusdt, order3.quantity(), price3, None);
1419        let fill3 = TestOrderEventStubs::filled(
1420            &order3,
1421            &ethusdt,
1422            Some(TradeId::new("3")),
1423            Some(PositionId::new("P-123456")),
1424            Some(price3),
1425            None,
1426            None,
1427            Some(commission3),
1428            None,
1429            None,
1430        );
1431        position.apply(&fill3.into());
1432        assert_eq!(position.quantity, Quantity::from(20));
1433        assert_eq!(position.realized_pnl, Some(Money::from("13.89666207 USDT")));
1434        assert_eq!(position.avg_px_open, 99.413_793_103_448_27);
1435        let quantity4 = Quantity::from("4");
1436        let price4 = Price::from("105.0");
1437        let order4 = OrderTestBuilder::new(OrderType::Market)
1438            .instrument_id(ethusdt.id())
1439            .side(OrderSide::Sell)
1440            .quantity(quantity4)
1441            .build();
1442        let commission4 = calculate_commission(&ethusdt, order4.quantity(), price4, None);
1443        let fill4 = TestOrderEventStubs::filled(
1444            &order4,
1445            &ethusdt,
1446            Some(TradeId::new("4")),
1447            Some(PositionId::new("P-123456")),
1448            Some(price4),
1449            None,
1450            None,
1451            Some(commission4),
1452            None,
1453            None,
1454        );
1455        position.apply(&fill4.into());
1456        assert_eq!(position.quantity, Quantity::from("16"));
1457        assert_eq!(position.realized_pnl, Some(Money::from("36.19948966 USDT")));
1458        assert_eq!(position.avg_px_open, 99.413_793_103_448_27);
1459        let quantity5 = Quantity::from("3");
1460        let price5 = Price::from("103.0");
1461        let order5 = OrderTestBuilder::new(OrderType::Market)
1462            .instrument_id(ethusdt.id())
1463            .side(OrderSide::Buy)
1464            .quantity(quantity5)
1465            .build();
1466        let commission5 = calculate_commission(&ethusdt, order5.quantity(), price5, None);
1467        let fill5 = TestOrderEventStubs::filled(
1468            &order5,
1469            &ethusdt,
1470            Some(TradeId::new("5")),
1471            Some(PositionId::new("P-123456")),
1472            Some(price5),
1473            None,
1474            None,
1475            Some(commission5),
1476            None,
1477            None,
1478        );
1479        position.apply(&fill5.into());
1480        assert_eq!(position.quantity, Quantity::from("19"));
1481        assert_eq!(position.realized_pnl, Some(Money::from("36.16858966 USDT")));
1482        assert_eq!(position.avg_px_open, 99.980_036_297_640_65);
1483        assert_eq!(
1484            format!("{position}"),
1485            "Position(LONG 19.00000 ETHUSDT.BINANCE, id=P-123456)"
1486        );
1487    }
1488
1489    #[rstest]
1490    fn test_position_closed_and_reopened(audusd_sim: CurrencyPair) {
1491        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1492        let quantity1 = Quantity::from(150_000);
1493        let price1 = Price::from("1.00001");
1494        let order = OrderTestBuilder::new(OrderType::Market)
1495            .instrument_id(audusd_sim.id())
1496            .side(OrderSide::Buy)
1497            .quantity(quantity1)
1498            .build();
1499        let commission1 = calculate_commission(&audusd_sim, quantity1, price1, None);
1500        let fill1 = TestOrderEventStubs::filled(
1501            &order,
1502            &audusd_sim,
1503            Some(TradeId::new("5")),
1504            Some(PositionId::new("P-123456")),
1505            Some(Price::from("1.00001")),
1506            None,
1507            None,
1508            Some(commission1),
1509            Some(UnixNanos::from(1_000_000_000)),
1510            None,
1511        );
1512        let mut position = Position::new(&audusd_sim, fill1.into());
1513
1514        let fill2 = OrderFilled::new(
1515            order.trader_id(),
1516            order.strategy_id(),
1517            order.instrument_id(),
1518            order.client_order_id(),
1519            VenueOrderId::from("2"),
1520            order.account_id().unwrap_or(AccountId::new("SIM-001")),
1521            TradeId::from("2"),
1522            OrderSide::Sell,
1523            OrderType::Market,
1524            order.quantity(),
1525            Price::from("1.00011"),
1526            audusd_sim.quote_currency(),
1527            LiquiditySide::Taker,
1528            uuid4(),
1529            UnixNanos::from(2_000_000_000),
1530            UnixNanos::default(),
1531            false,
1532            Some(PositionId::from("P-123456")),
1533            Some(Money::from("0 USD")),
1534        );
1535
1536        position.apply(&fill2);
1537
1538        let fill3 = OrderFilled::new(
1539            order.trader_id(),
1540            order.strategy_id(),
1541            order.instrument_id(),
1542            order.client_order_id(),
1543            VenueOrderId::from("2"),
1544            order.account_id().unwrap_or(AccountId::new("SIM-001")),
1545            TradeId::from("3"),
1546            OrderSide::Buy,
1547            OrderType::Market,
1548            order.quantity(),
1549            Price::from("1.00012"),
1550            audusd_sim.quote_currency(),
1551            LiquiditySide::Taker,
1552            uuid4(),
1553            UnixNanos::from(3_000_000_000),
1554            UnixNanos::default(),
1555            false,
1556            Some(PositionId::from("P-123456")),
1557            Some(Money::from("0 USD")),
1558        );
1559
1560        position.apply(&fill3);
1561
1562        let last = Price::from("1.0003");
1563        assert!(position.is_opposite_side(fill2.order_side));
1564        assert_eq!(position.quantity, Quantity::from(150_000));
1565        assert_eq!(position.peak_qty, Quantity::from(150_000));
1566        assert_eq!(position.side, PositionSide::Long);
1567        assert_eq!(position.opening_order_id, fill3.client_order_id);
1568        assert_eq!(position.closing_order_id, None);
1569        assert_eq!(position.closing_order_id, None);
1570        assert_eq!(position.ts_opened, 3_000_000_000);
1571        assert_eq!(position.duration_ns, 0);
1572        assert_eq!(position.avg_px_open, 1.00012);
1573        assert_eq!(position.event_count(), 1);
1574        assert_eq!(position.ts_closed, None);
1575        assert_eq!(position.avg_px_close, None);
1576        assert!(position.is_long());
1577        assert!(!position.is_short());
1578        assert!(position.is_open());
1579        assert!(!position.is_closed());
1580        assert_eq!(position.realized_return, 0.0);
1581        assert_eq!(position.realized_pnl, Some(Money::from("0 USD")));
1582        assert_eq!(position.unrealized_pnl(last), Money::from("27 USD"));
1583        assert_eq!(position.total_pnl(last), Money::from("27 USD"));
1584        assert_eq!(position.commissions(), vec![Money::from("0 USD")]);
1585        assert_eq!(
1586            format!("{position}"),
1587            "Position(LONG 150_000 AUD/USD.SIM, id=P-123456)"
1588        );
1589    }
1590
1591    #[rstest]
1592    fn test_position_realized_pnl_with_interleaved_order_sides(
1593        currency_pair_btcusdt: CurrencyPair,
1594    ) {
1595        let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1596        let order1 = OrderTestBuilder::new(OrderType::Market)
1597            .instrument_id(btcusdt.id())
1598            .side(OrderSide::Buy)
1599            .quantity(Quantity::from(12))
1600            .build();
1601        let commission1 =
1602            calculate_commission(&btcusdt, order1.quantity(), Price::from("10000.0"), None);
1603        let fill1 = TestOrderEventStubs::filled(
1604            &order1,
1605            &btcusdt,
1606            Some(TradeId::from("1")),
1607            Some(PositionId::from("P-19700101-000000-001-001-1")),
1608            Some(Price::from("10000.0")),
1609            None,
1610            None,
1611            Some(commission1),
1612            None,
1613            None,
1614        );
1615        let mut position = Position::new(&btcusdt, fill1.into());
1616        let order2 = OrderTestBuilder::new(OrderType::Market)
1617            .instrument_id(btcusdt.id())
1618            .side(OrderSide::Buy)
1619            .quantity(Quantity::from(17))
1620            .build();
1621        let commission2 =
1622            calculate_commission(&btcusdt, order2.quantity(), Price::from("9999.0"), None);
1623        let fill2 = TestOrderEventStubs::filled(
1624            &order2,
1625            &btcusdt,
1626            Some(TradeId::from("2")),
1627            Some(PositionId::from("P-19700101-000000-001-001-1")),
1628            Some(Price::from("9999.0")),
1629            None,
1630            None,
1631            Some(commission2),
1632            None,
1633            None,
1634        );
1635        position.apply(&fill2.into());
1636        assert_eq!(position.quantity, Quantity::from(29));
1637        assert_eq!(
1638            position.realized_pnl,
1639            Some(Money::from("-289.98300000 USDT"))
1640        );
1641        assert_eq!(position.avg_px_open, 9_999.413_793_103_447);
1642        let order3 = OrderTestBuilder::new(OrderType::Market)
1643            .instrument_id(btcusdt.id())
1644            .side(OrderSide::Sell)
1645            .quantity(Quantity::from(9))
1646            .build();
1647        let commission3 =
1648            calculate_commission(&btcusdt, order3.quantity(), Price::from("10001.0"), None);
1649        let fill3 = TestOrderEventStubs::filled(
1650            &order3,
1651            &btcusdt,
1652            Some(TradeId::from("3")),
1653            Some(PositionId::from("P-19700101-000000-001-001-1")),
1654            Some(Price::from("10001.0")),
1655            None,
1656            None,
1657            Some(commission3),
1658            None,
1659            None,
1660        );
1661        position.apply(&fill3.into());
1662        assert_eq!(position.quantity, Quantity::from(20));
1663        assert_eq!(
1664            position.realized_pnl,
1665            Some(Money::from("-365.71613793 USDT"))
1666        );
1667        assert_eq!(position.avg_px_open, 9_999.413_793_103_447);
1668        let order4 = OrderTestBuilder::new(OrderType::Market)
1669            .instrument_id(btcusdt.id())
1670            .side(OrderSide::Buy)
1671            .quantity(Quantity::from(3))
1672            .build();
1673        let commission4 =
1674            calculate_commission(&btcusdt, order4.quantity(), Price::from("10003.0"), None);
1675        let fill4 = TestOrderEventStubs::filled(
1676            &order4,
1677            &btcusdt,
1678            Some(TradeId::from("4")),
1679            Some(PositionId::from("P-19700101-000000-001-001-1")),
1680            Some(Price::from("10003.0")),
1681            None,
1682            None,
1683            Some(commission4),
1684            None,
1685            None,
1686        );
1687        position.apply(&fill4.into());
1688        assert_eq!(position.quantity, Quantity::from(23));
1689        assert_eq!(
1690            position.realized_pnl,
1691            Some(Money::from("-395.72513793 USDT"))
1692        );
1693        assert_eq!(position.avg_px_open, 9_999.881_559_220_39);
1694        let order5 = OrderTestBuilder::new(OrderType::Market)
1695            .instrument_id(btcusdt.id())
1696            .side(OrderSide::Sell)
1697            .quantity(Quantity::from(4))
1698            .build();
1699        let commission5 =
1700            calculate_commission(&btcusdt, order5.quantity(), Price::from("10005.0"), None);
1701        let fill5 = TestOrderEventStubs::filled(
1702            &order5,
1703            &btcusdt,
1704            Some(TradeId::from("5")),
1705            Some(PositionId::from("P-19700101-000000-001-001-1")),
1706            Some(Price::from("10005.0")),
1707            None,
1708            None,
1709            Some(commission5),
1710            None,
1711            None,
1712        );
1713        position.apply(&fill5.into());
1714        assert_eq!(position.quantity, Quantity::from(19));
1715        assert_eq!(
1716            position.realized_pnl,
1717            Some(Money::from("-415.27137481 USDT"))
1718        );
1719        assert_eq!(position.avg_px_open, 9_999.881_559_220_39);
1720        assert_eq!(
1721            format!("{position}"),
1722            "Position(LONG 19.000000 BTCUSDT.BINANCE, id=P-19700101-000000-001-001-1)"
1723        );
1724    }
1725
1726    #[rstest]
1727    fn test_calculate_pnl_when_given_position_side_flat_returns_zero(
1728        currency_pair_btcusdt: CurrencyPair,
1729    ) {
1730        let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1731        let order = OrderTestBuilder::new(OrderType::Market)
1732            .instrument_id(btcusdt.id())
1733            .side(OrderSide::Buy)
1734            .quantity(Quantity::from(12))
1735            .build();
1736        let fill = TestOrderEventStubs::filled(
1737            &order,
1738            &btcusdt,
1739            None,
1740            Some(PositionId::from("P-123456")),
1741            Some(Price::from("10500.0")),
1742            None,
1743            None,
1744            None,
1745            None,
1746            None,
1747        );
1748        let position = Position::new(&btcusdt, fill.into());
1749        let result = position.calculate_pnl(10500.0, 10500.0, Quantity::from("100000.0"));
1750        assert_eq!(result, Money::from("0 USDT"));
1751    }
1752
1753    #[rstest]
1754    fn test_calculate_pnl_for_long_position_win(currency_pair_btcusdt: CurrencyPair) {
1755        let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1756        let order = OrderTestBuilder::new(OrderType::Market)
1757            .instrument_id(btcusdt.id())
1758            .side(OrderSide::Buy)
1759            .quantity(Quantity::from(12))
1760            .build();
1761        let commission =
1762            calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1763        let fill = TestOrderEventStubs::filled(
1764            &order,
1765            &btcusdt,
1766            None,
1767            Some(PositionId::from("P-123456")),
1768            Some(Price::from("10500.0")),
1769            None,
1770            None,
1771            Some(commission),
1772            None,
1773            None,
1774        );
1775        let position = Position::new(&btcusdt, fill.into());
1776        let pnl = position.calculate_pnl(10500.0, 10510.0, Quantity::from("12.0"));
1777        assert_eq!(pnl, Money::from("120 USDT"));
1778        assert_eq!(position.realized_pnl, Some(Money::from("-126 USDT")));
1779        assert_eq!(
1780            position.unrealized_pnl(Price::from("10510.0")),
1781            Money::from("120.0 USDT")
1782        );
1783        assert_eq!(
1784            position.total_pnl(Price::from("10510.0")),
1785            Money::from("-6 USDT")
1786        );
1787        assert_eq!(position.commissions(), vec![Money::from("126.0 USDT")]);
1788    }
1789
1790    #[rstest]
1791    fn test_calculate_pnl_for_long_position_loss(currency_pair_btcusdt: CurrencyPair) {
1792        let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1793        let order = OrderTestBuilder::new(OrderType::Market)
1794            .instrument_id(btcusdt.id())
1795            .side(OrderSide::Buy)
1796            .quantity(Quantity::from(12))
1797            .build();
1798        let commission =
1799            calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1800        let fill = TestOrderEventStubs::filled(
1801            &order,
1802            &btcusdt,
1803            None,
1804            Some(PositionId::from("P-123456")),
1805            Some(Price::from("10500.0")),
1806            None,
1807            None,
1808            Some(commission),
1809            None,
1810            None,
1811        );
1812        let position = Position::new(&btcusdt, fill.into());
1813        let pnl = position.calculate_pnl(10500.0, 10480.5, Quantity::from("10.0"));
1814        assert_eq!(pnl, Money::from("-195 USDT"));
1815        assert_eq!(position.realized_pnl, Some(Money::from("-126 USDT")));
1816        assert_eq!(
1817            position.unrealized_pnl(Price::from("10480.50")),
1818            Money::from("-234.0 USDT")
1819        );
1820        assert_eq!(
1821            position.total_pnl(Price::from("10480.50")),
1822            Money::from("-360 USDT")
1823        );
1824        assert_eq!(position.commissions(), vec![Money::from("126.0 USDT")]);
1825    }
1826
1827    #[rstest]
1828    fn test_calculate_pnl_for_short_position_winning(currency_pair_btcusdt: CurrencyPair) {
1829        let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1830        let order = OrderTestBuilder::new(OrderType::Market)
1831            .instrument_id(btcusdt.id())
1832            .side(OrderSide::Sell)
1833            .quantity(Quantity::from("10.15"))
1834            .build();
1835        let commission =
1836            calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1837        let fill = TestOrderEventStubs::filled(
1838            &order,
1839            &btcusdt,
1840            None,
1841            Some(PositionId::from("P-123456")),
1842            Some(Price::from("10500.0")),
1843            None,
1844            None,
1845            Some(commission),
1846            None,
1847            None,
1848        );
1849        let position = Position::new(&btcusdt, fill.into());
1850        let pnl = position.calculate_pnl(10500.0, 10390.0, Quantity::from("10.15"));
1851        assert_eq!(pnl, Money::from("1116.5 USDT"));
1852        assert_eq!(
1853            position.unrealized_pnl(Price::from("10390.0")),
1854            Money::from("1116.5 USDT")
1855        );
1856        assert_eq!(position.realized_pnl, Some(Money::from("-106.575 USDT")));
1857        assert_eq!(position.commissions(), vec![Money::from("106.575 USDT")]);
1858        assert_eq!(
1859            position.notional_value(Price::from("10390.0")),
1860            Money::from("105458.5 USDT")
1861        );
1862    }
1863
1864    #[rstest]
1865    fn test_calculate_pnl_for_short_position_loss(currency_pair_btcusdt: CurrencyPair) {
1866        let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1867        let order = OrderTestBuilder::new(OrderType::Market)
1868            .instrument_id(btcusdt.id())
1869            .side(OrderSide::Sell)
1870            .quantity(Quantity::from("10.0"))
1871            .build();
1872        let commission =
1873            calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1874        let fill = TestOrderEventStubs::filled(
1875            &order,
1876            &btcusdt,
1877            None,
1878            Some(PositionId::from("P-123456")),
1879            Some(Price::from("10500.0")),
1880            None,
1881            None,
1882            Some(commission),
1883            None,
1884            None,
1885        );
1886        let position = Position::new(&btcusdt, fill.into());
1887        let pnl = position.calculate_pnl(10500.0, 10670.5, Quantity::from("10.0"));
1888        assert_eq!(pnl, Money::from("-1705 USDT"));
1889        assert_eq!(
1890            position.unrealized_pnl(Price::from("10670.5")),
1891            Money::from("-1705 USDT")
1892        );
1893        assert_eq!(position.realized_pnl, Some(Money::from("-105 USDT")));
1894        assert_eq!(position.commissions(), vec![Money::from("105 USDT")]);
1895        assert_eq!(
1896            position.notional_value(Price::from("10670.5")),
1897            Money::from("106705 USDT")
1898        );
1899    }
1900
1901    #[rstest]
1902    fn test_calculate_pnl_for_inverse1(xbtusd_bitmex: CryptoPerpetual) {
1903        let xbtusd_bitmex = InstrumentAny::CryptoPerpetual(xbtusd_bitmex);
1904        let order = OrderTestBuilder::new(OrderType::Market)
1905            .instrument_id(xbtusd_bitmex.id())
1906            .side(OrderSide::Sell)
1907            .quantity(Quantity::from("100000"))
1908            .build();
1909        let commission = calculate_commission(
1910            &xbtusd_bitmex,
1911            order.quantity(),
1912            Price::from("10000.0"),
1913            None,
1914        );
1915        let fill = TestOrderEventStubs::filled(
1916            &order,
1917            &xbtusd_bitmex,
1918            None,
1919            Some(PositionId::from("P-123456")),
1920            Some(Price::from("10000.0")),
1921            None,
1922            None,
1923            Some(commission),
1924            None,
1925            None,
1926        );
1927        let position = Position::new(&xbtusd_bitmex, fill.into());
1928        let pnl = position.calculate_pnl(10000.0, 11000.0, Quantity::from("100000.0"));
1929        assert_eq!(pnl, Money::from("-0.90909091 BTC"));
1930        assert_eq!(
1931            position.unrealized_pnl(Price::from("11000.0")),
1932            Money::from("-0.90909091 BTC")
1933        );
1934        assert_eq!(position.realized_pnl, Some(Money::from("-0.00750000 BTC")));
1935        assert_eq!(
1936            position.notional_value(Price::from("11000.0")),
1937            Money::from("9.09090909 BTC")
1938        );
1939    }
1940
1941    #[rstest]
1942    fn test_calculate_pnl_for_inverse2(ethusdt_bitmex: CryptoPerpetual) {
1943        let ethusdt_bitmex = InstrumentAny::CryptoPerpetual(ethusdt_bitmex);
1944        let order = OrderTestBuilder::new(OrderType::Market)
1945            .instrument_id(ethusdt_bitmex.id())
1946            .side(OrderSide::Sell)
1947            .quantity(Quantity::from("100000"))
1948            .build();
1949        let commission = calculate_commission(
1950            &ethusdt_bitmex,
1951            order.quantity(),
1952            Price::from("375.95"),
1953            None,
1954        );
1955        let fill = TestOrderEventStubs::filled(
1956            &order,
1957            &ethusdt_bitmex,
1958            None,
1959            Some(PositionId::from("P-123456")),
1960            Some(Price::from("375.95")),
1961            None,
1962            None,
1963            Some(commission),
1964            None,
1965            None,
1966        );
1967        let position = Position::new(&ethusdt_bitmex, fill.into());
1968
1969        assert_eq!(
1970            position.unrealized_pnl(Price::from("370.00")),
1971            Money::from("4.27745208 ETH")
1972        );
1973        assert_eq!(
1974            position.notional_value(Price::from("370.00")),
1975            Money::from("270.27027027 ETH")
1976        );
1977    }
1978
1979    #[rstest]
1980    fn test_calculate_unrealized_pnl_for_long(currency_pair_btcusdt: CurrencyPair) {
1981        let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1982        let order1 = OrderTestBuilder::new(OrderType::Market)
1983            .instrument_id(btcusdt.id())
1984            .side(OrderSide::Buy)
1985            .quantity(Quantity::from("2.000000"))
1986            .build();
1987        let order2 = OrderTestBuilder::new(OrderType::Market)
1988            .instrument_id(btcusdt.id())
1989            .side(OrderSide::Buy)
1990            .quantity(Quantity::from("2.000000"))
1991            .build();
1992        let commission1 =
1993            calculate_commission(&btcusdt, order1.quantity(), Price::from("10500.0"), None);
1994        let fill1 = TestOrderEventStubs::filled(
1995            &order1,
1996            &btcusdt,
1997            Some(TradeId::new("1")),
1998            Some(PositionId::new("P-123456")),
1999            Some(Price::from("10500.00")),
2000            None,
2001            None,
2002            Some(commission1),
2003            None,
2004            None,
2005        );
2006        let commission2 =
2007            calculate_commission(&btcusdt, order2.quantity(), Price::from("10500.0"), None);
2008        let fill2 = TestOrderEventStubs::filled(
2009            &order2,
2010            &btcusdt,
2011            Some(TradeId::new("2")),
2012            Some(PositionId::new("P-123456")),
2013            Some(Price::from("10500.00")),
2014            None,
2015            None,
2016            Some(commission2),
2017            None,
2018            None,
2019        );
2020        let mut position = Position::new(&btcusdt, fill1.into());
2021        position.apply(&fill2.into());
2022        let pnl = position.unrealized_pnl(Price::from("11505.60"));
2023        assert_eq!(pnl, Money::from("4022.40000000 USDT"));
2024        assert_eq!(
2025            position.realized_pnl,
2026            Some(Money::from("-42.00000000 USDT"))
2027        );
2028        assert_eq!(
2029            position.commissions(),
2030            vec![Money::from("42.00000000 USDT")]
2031        );
2032    }
2033
2034    #[rstest]
2035    fn test_calculate_unrealized_pnl_for_short(currency_pair_btcusdt: CurrencyPair) {
2036        let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
2037        let order = OrderTestBuilder::new(OrderType::Market)
2038            .instrument_id(btcusdt.id())
2039            .side(OrderSide::Sell)
2040            .quantity(Quantity::from("5.912000"))
2041            .build();
2042        let commission =
2043            calculate_commission(&btcusdt, order.quantity(), Price::from("10505.60"), None);
2044        let fill = TestOrderEventStubs::filled(
2045            &order,
2046            &btcusdt,
2047            Some(TradeId::new("1")),
2048            Some(PositionId::new("P-123456")),
2049            Some(Price::from("10505.60")),
2050            None,
2051            None,
2052            Some(commission),
2053            None,
2054            None,
2055        );
2056        let position = Position::new(&btcusdt, fill.into());
2057        let pnl = position.unrealized_pnl(Price::from("10407.15"));
2058        assert_eq!(pnl, Money::from("582.03640000 USDT"));
2059        assert_eq!(
2060            position.realized_pnl,
2061            Some(Money::from("-62.10910720 USDT"))
2062        );
2063        assert_eq!(
2064            position.commissions(),
2065            vec![Money::from("62.10910720 USDT")]
2066        );
2067    }
2068
2069    #[rstest]
2070    fn test_calculate_unrealized_pnl_for_long_inverse(xbtusd_bitmex: CryptoPerpetual) {
2071        let xbtusd_bitmex = InstrumentAny::CryptoPerpetual(xbtusd_bitmex);
2072        let order = OrderTestBuilder::new(OrderType::Market)
2073            .instrument_id(xbtusd_bitmex.id())
2074            .side(OrderSide::Buy)
2075            .quantity(Quantity::from("100000"))
2076            .build();
2077        let commission = calculate_commission(
2078            &xbtusd_bitmex,
2079            order.quantity(),
2080            Price::from("10500.0"),
2081            None,
2082        );
2083        let fill = TestOrderEventStubs::filled(
2084            &order,
2085            &xbtusd_bitmex,
2086            Some(TradeId::new("1")),
2087            Some(PositionId::new("P-123456")),
2088            Some(Price::from("10500.00")),
2089            None,
2090            None,
2091            Some(commission),
2092            None,
2093            None,
2094        );
2095
2096        let position = Position::new(&xbtusd_bitmex, fill.into());
2097        let pnl = position.unrealized_pnl(Price::from("11505.60"));
2098        assert_eq!(pnl, Money::from("0.83238969 BTC"));
2099        assert_eq!(position.realized_pnl, Some(Money::from("-0.00714286 BTC")));
2100        assert_eq!(position.commissions(), vec![Money::from("0.00714286 BTC")]);
2101    }
2102
2103    #[rstest]
2104    fn test_calculate_unrealized_pnl_for_short_inverse(xbtusd_bitmex: CryptoPerpetual) {
2105        let xbtusd_bitmex = InstrumentAny::CryptoPerpetual(xbtusd_bitmex);
2106        let order = OrderTestBuilder::new(OrderType::Market)
2107            .instrument_id(xbtusd_bitmex.id())
2108            .side(OrderSide::Sell)
2109            .quantity(Quantity::from("1250000"))
2110            .build();
2111        let commission = calculate_commission(
2112            &xbtusd_bitmex,
2113            order.quantity(),
2114            Price::from("15500.00"),
2115            None,
2116        );
2117        let fill = TestOrderEventStubs::filled(
2118            &order,
2119            &xbtusd_bitmex,
2120            Some(TradeId::new("1")),
2121            Some(PositionId::new("P-123456")),
2122            Some(Price::from("15500.00")),
2123            None,
2124            None,
2125            Some(commission),
2126            None,
2127            None,
2128        );
2129        let position = Position::new(&xbtusd_bitmex, fill.into());
2130        let pnl = position.unrealized_pnl(Price::from("12506.65"));
2131
2132        assert_eq!(pnl, Money::from("19.30166700 BTC"));
2133        assert_eq!(position.realized_pnl, Some(Money::from("-0.06048387 BTC")));
2134        assert_eq!(position.commissions(), vec![Money::from("0.06048387 BTC")]);
2135    }
2136
2137    #[rstest]
2138    #[case(OrderSide::Buy, 25, 25.0)]
2139    #[case(OrderSide::Sell,25,-25.0)]
2140    fn test_signed_qty_decimal_qty_for_equity(
2141        #[case] order_side: OrderSide,
2142        #[case] quantity: i64,
2143        #[case] expected: f64,
2144        audusd_sim: CurrencyPair,
2145    ) {
2146        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2147        let order = OrderTestBuilder::new(OrderType::Market)
2148            .instrument_id(audusd_sim.id())
2149            .side(order_side)
2150            .quantity(Quantity::from(quantity))
2151            .build();
2152
2153        let commission =
2154            calculate_commission(&audusd_sim, order.quantity(), Price::from("1.0"), None);
2155        let fill = TestOrderEventStubs::filled(
2156            &order,
2157            &audusd_sim,
2158            None,
2159            Some(PositionId::from("P-123456")),
2160            None,
2161            None,
2162            None,
2163            Some(commission),
2164            None,
2165            None,
2166        );
2167        let position = Position::new(&audusd_sim, fill.into());
2168        assert_eq!(position.signed_qty, expected);
2169    }
2170
2171    #[rstest]
2172    fn test_position_with_commission_none(audusd_sim: CurrencyPair) {
2173        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2174        let fill = OrderFilled {
2175            position_id: Some(PositionId::from("1")),
2176            ..Default::default()
2177        };
2178
2179        let position = Position::new(&audusd_sim, fill);
2180        assert_eq!(position.realized_pnl, Some(Money::from("0 USD")));
2181    }
2182
2183    #[rstest]
2184    fn test_position_with_commission_zero(audusd_sim: CurrencyPair) {
2185        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2186        let fill = OrderFilled {
2187            position_id: Some(PositionId::from("1")),
2188            commission: Some(Money::from("0 USD")),
2189            ..Default::default()
2190        };
2191
2192        let position = Position::new(&audusd_sim, fill);
2193        assert_eq!(position.realized_pnl, Some(Money::from("0 USD")));
2194    }
2195
2196    #[rstest]
2197    fn test_cache_purge_order_events() {
2198        let audusd_sim = audusd_sim();
2199        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2200
2201        let order1 = OrderTestBuilder::new(OrderType::Market)
2202            .client_order_id(ClientOrderId::new("O-1"))
2203            .instrument_id(audusd_sim.id())
2204            .side(OrderSide::Buy)
2205            .quantity(Quantity::from(50_000))
2206            .build();
2207
2208        let order2 = OrderTestBuilder::new(OrderType::Market)
2209            .client_order_id(ClientOrderId::new("O-2"))
2210            .instrument_id(audusd_sim.id())
2211            .side(OrderSide::Buy)
2212            .quantity(Quantity::from(50_000))
2213            .build();
2214
2215        let position_id = PositionId::new("P-123456");
2216
2217        let fill1 = TestOrderEventStubs::filled(
2218            &order1,
2219            &audusd_sim,
2220            Some(TradeId::new("1")),
2221            Some(position_id),
2222            Some(Price::from("1.00001")),
2223            None,
2224            None,
2225            None,
2226            None,
2227            None,
2228        );
2229
2230        let mut position = Position::new(&audusd_sim, fill1.into());
2231
2232        let fill2 = TestOrderEventStubs::filled(
2233            &order2,
2234            &audusd_sim,
2235            Some(TradeId::new("2")),
2236            Some(position_id),
2237            Some(Price::from("1.00002")),
2238            None,
2239            None,
2240            None,
2241            None,
2242            None,
2243        );
2244
2245        position.apply(&fill2.into());
2246        position.purge_events_for_order(order1.client_order_id());
2247
2248        assert_eq!(position.events.len(), 1);
2249        assert_eq!(position.trade_ids.len(), 1);
2250        assert_eq!(position.events[0].client_order_id, order2.client_order_id());
2251        assert_eq!(position.trade_ids[0], TradeId::new("2"));
2252    }
2253
2254    #[rstest]
2255    fn test_purge_all_events_returns_none_for_last_event_and_trade_id() {
2256        let audusd_sim = audusd_sim();
2257        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2258
2259        let order = OrderTestBuilder::new(OrderType::Market)
2260            .client_order_id(ClientOrderId::new("O-1"))
2261            .instrument_id(audusd_sim.id())
2262            .side(OrderSide::Buy)
2263            .quantity(Quantity::from(100_000))
2264            .build();
2265
2266        let position_id = PositionId::new("P-123456");
2267        let fill = TestOrderEventStubs::filled(
2268            &order,
2269            &audusd_sim,
2270            Some(TradeId::new("1")),
2271            Some(position_id),
2272            Some(Price::from("1.00050")),
2273            None,
2274            None,
2275            None,
2276            Some(UnixNanos::from(1_000_000_000)), // Explicit non-zero timestamp
2277            None,
2278        );
2279
2280        let mut position = Position::new(&audusd_sim, fill.into());
2281
2282        assert_eq!(position.events.len(), 1);
2283        assert!(position.last_event().is_some());
2284        assert!(position.last_trade_id().is_some());
2285
2286        // Store original timestamps (should be non-zero)
2287        let original_ts_opened = position.ts_opened;
2288        let original_ts_last = position.ts_last;
2289        assert_ne!(original_ts_opened, UnixNanos::default());
2290        assert_ne!(original_ts_last, UnixNanos::default());
2291
2292        position.purge_events_for_order(order.client_order_id());
2293
2294        assert_eq!(position.events.len(), 0);
2295        assert_eq!(position.trade_ids.len(), 0);
2296        assert!(position.last_event().is_none());
2297        assert!(position.last_trade_id().is_none());
2298
2299        // Verify timestamps are zeroed - empty shell has no meaningful history
2300        // ts_closed is set to Some(0) so position reports as closed and is eligible for purge
2301        assert_eq!(position.ts_opened, UnixNanos::default());
2302        assert_eq!(position.ts_last, UnixNanos::default());
2303        assert_eq!(position.ts_closed, Some(UnixNanos::default()));
2304        assert_eq!(position.duration_ns, 0);
2305
2306        // CRITICAL: Verify empty shell reports as closed (this was the bug we fixed!)
2307        // is_closed() must return true so cache purge logic recognizes empty shells
2308        assert!(position.is_closed());
2309        assert!(!position.is_open());
2310        assert_eq!(position.side, PositionSide::Flat);
2311    }
2312
2313    #[rstest]
2314    fn test_revive_from_empty_shell(audusd_sim: CurrencyPair) {
2315        // Test adding a fill to an empty shell position
2316        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2317
2318        // Create and then purge position to get empty shell
2319        let order1 = OrderTestBuilder::new(OrderType::Market)
2320            .instrument_id(audusd_sim.id())
2321            .side(OrderSide::Buy)
2322            .quantity(Quantity::from(100_000))
2323            .build();
2324
2325        let fill1 = TestOrderEventStubs::filled(
2326            &order1,
2327            &audusd_sim,
2328            None,
2329            Some(PositionId::new("P-1")),
2330            Some(Price::from("1.00000")),
2331            None,
2332            None,
2333            None,
2334            Some(UnixNanos::from(1_000_000_000)),
2335            None,
2336        );
2337
2338        let mut position = Position::new(&audusd_sim, fill1.into());
2339        position.purge_events_for_order(order1.client_order_id());
2340
2341        // Verify it's an empty shell
2342        assert!(position.is_closed());
2343        assert_eq!(position.ts_closed, Some(UnixNanos::default()));
2344        assert_eq!(position.event_count(), 0);
2345
2346        // Act: Add new fill to revive the position
2347        let order2 = OrderTestBuilder::new(OrderType::Market)
2348            .instrument_id(audusd_sim.id())
2349            .side(OrderSide::Buy)
2350            .quantity(Quantity::from(50_000))
2351            .build();
2352
2353        let fill2 = TestOrderEventStubs::filled(
2354            &order2,
2355            &audusd_sim,
2356            None,
2357            Some(PositionId::new("P-1")),
2358            Some(Price::from("1.00020")),
2359            None,
2360            None,
2361            None,
2362            Some(UnixNanos::from(3_000_000_000)),
2363            None,
2364        );
2365
2366        let fill2_typed: OrderFilled = fill2.clone().into();
2367        position.apply(&fill2_typed);
2368
2369        // Assert: Position should be alive with new timestamps
2370        assert!(position.is_long());
2371        assert!(!position.is_closed());
2372        assert!(position.ts_closed.is_none());
2373        assert_eq!(position.ts_opened, fill2.ts_event());
2374        assert_eq!(position.ts_last, fill2.ts_event());
2375        assert_eq!(position.event_count(), 1);
2376        assert_eq!(position.quantity, Quantity::from(50_000));
2377    }
2378
2379    #[rstest]
2380    fn test_empty_shell_position_invariants(audusd_sim: CurrencyPair) {
2381        // Property-based test: Any position with event_count == 0 must satisfy invariants
2382        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2383
2384        let order = OrderTestBuilder::new(OrderType::Market)
2385            .instrument_id(audusd_sim.id())
2386            .side(OrderSide::Buy)
2387            .quantity(Quantity::from(100_000))
2388            .build();
2389
2390        let fill = TestOrderEventStubs::filled(
2391            &order,
2392            &audusd_sim,
2393            None,
2394            Some(PositionId::new("P-1")),
2395            Some(Price::from("1.00000")),
2396            None,
2397            None,
2398            None,
2399            Some(UnixNanos::from(1_000_000_000)),
2400            None,
2401        );
2402
2403        let mut position = Position::new(&audusd_sim, fill.into());
2404        position.purge_events_for_order(order.client_order_id());
2405
2406        // INVARIANTS: When event_count == 0, the following MUST be true
2407        assert_eq!(
2408            position.event_count(),
2409            0,
2410            "Precondition: event_count must be 0"
2411        );
2412
2413        // Invariant 1: Position must report as closed
2414        assert!(
2415            position.is_closed(),
2416            "INV1: Empty shell must report is_closed() == true"
2417        );
2418        assert!(
2419            !position.is_open(),
2420            "INV1: Empty shell must report is_open() == false"
2421        );
2422
2423        // Invariant 2: Position must be FLAT
2424        assert_eq!(
2425            position.side,
2426            PositionSide::Flat,
2427            "INV2: Empty shell must be FLAT"
2428        );
2429
2430        // Invariant 3: ts_closed must be Some (not None)
2431        assert!(
2432            position.ts_closed.is_some(),
2433            "INV3: Empty shell must have ts_closed.is_some()"
2434        );
2435        assert_eq!(
2436            position.ts_closed,
2437            Some(UnixNanos::default()),
2438            "INV3: Empty shell ts_closed must be 0"
2439        );
2440
2441        // Invariant 4: All lifecycle timestamps must be zeroed
2442        assert_eq!(
2443            position.ts_opened,
2444            UnixNanos::default(),
2445            "INV4: Empty shell ts_opened must be 0"
2446        );
2447        assert_eq!(
2448            position.ts_last,
2449            UnixNanos::default(),
2450            "INV4: Empty shell ts_last must be 0"
2451        );
2452        assert_eq!(
2453            position.duration_ns, 0,
2454            "INV4: Empty shell duration_ns must be 0"
2455        );
2456
2457        // Invariant 5: Quantity must be zero
2458        assert_eq!(
2459            position.quantity,
2460            Quantity::zero(audusd_sim.size_precision()),
2461            "INV5: Empty shell quantity must be 0"
2462        );
2463
2464        // Invariant 6: No events or trade IDs
2465        assert!(
2466            position.events.is_empty(),
2467            "INV6: Empty shell must have no events"
2468        );
2469        assert!(
2470            position.trade_ids.is_empty(),
2471            "INV6: Empty shell must have no trade IDs"
2472        );
2473        assert!(
2474            position.last_event().is_none(),
2475            "INV6: Empty shell must have no last event"
2476        );
2477        assert!(
2478            position.last_trade_id().is_none(),
2479            "INV6: Empty shell must have no last trade ID"
2480        );
2481    }
2482
2483    #[rstest]
2484    fn test_position_pnl_precision_with_very_small_amounts(audusd_sim: CurrencyPair) {
2485        // Tests behavior with very small commission amounts
2486        // NOTE: Amounts below f64 epsilon (~1e-15) may be lost to precision
2487        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2488        let order = OrderTestBuilder::new(OrderType::Market)
2489            .instrument_id(audusd_sim.id())
2490            .side(OrderSide::Buy)
2491            .quantity(Quantity::from(100))
2492            .build();
2493
2494        // Test with a commission that won't be lost to Money precision (0.01 USD)
2495        let small_commission = Money::new(0.01, Currency::USD());
2496        let fill = TestOrderEventStubs::filled(
2497            &order,
2498            &audusd_sim,
2499            None,
2500            None,
2501            Some(Price::from("1.00001")),
2502            Some(Quantity::from(100)),
2503            None,
2504            Some(small_commission),
2505            None,
2506            None,
2507        );
2508
2509        let position = Position::new(&audusd_sim, fill.into());
2510
2511        // Commission is recorded and preserved in f64 arithmetic
2512        assert_eq!(position.commissions().len(), 1);
2513        let recorded_commission = position.commissions()[0];
2514        assert!(
2515            recorded_commission.as_f64() > 0.0,
2516            "Commission of 0.01 should be preserved"
2517        );
2518
2519        // Realized PnL should include commission (negative)
2520        let realized = position.realized_pnl.unwrap().as_f64();
2521        assert!(
2522            realized < 0.0,
2523            "Realized PnL should be negative due to commission"
2524        );
2525    }
2526
2527    #[rstest]
2528    fn test_position_pnl_precision_with_high_precision_instrument() {
2529        // Tests precision with high-precision crypto instrument
2530        use crate::instruments::stubs::crypto_perpetual_ethusdt;
2531        let ethusdt = crypto_perpetual_ethusdt();
2532        let ethusdt = InstrumentAny::CryptoPerpetual(ethusdt);
2533
2534        // Check instrument precision
2535        let size_precision = ethusdt.size_precision();
2536
2537        let order = OrderTestBuilder::new(OrderType::Market)
2538            .instrument_id(ethusdt.id())
2539            .side(OrderSide::Buy)
2540            .quantity(Quantity::from("1.123456789"))
2541            .build();
2542
2543        let fill = TestOrderEventStubs::filled(
2544            &order,
2545            &ethusdt,
2546            None,
2547            None,
2548            Some(Price::from("2345.123456789")),
2549            Some(Quantity::from("1.123456789")),
2550            None,
2551            Some(Money::from("0.1 USDT")),
2552            None,
2553            None,
2554        );
2555
2556        let position = Position::new(&ethusdt, fill.into());
2557
2558        // Verify high-precision price is preserved in f64 (within tolerance)
2559        let avg_px = position.avg_px_open;
2560        assert!(
2561            (avg_px - 2345.123456789).abs() < 1e-6,
2562            "High precision price should be preserved within f64 tolerance"
2563        );
2564
2565        // Quantity will be rounded to instrument's size_precision
2566        // Verify it matches the instrument's precision
2567        assert_eq!(
2568            position.quantity.precision, size_precision,
2569            "Quantity precision should match instrument"
2570        );
2571
2572        // f64 representation will be close but may have rounding based on precision
2573        let qty_f64 = position.quantity.as_f64();
2574        assert!(
2575            qty_f64 > 1.0 && qty_f64 < 2.0,
2576            "Quantity should be in expected range"
2577        );
2578    }
2579
2580    #[rstest]
2581    fn test_position_pnl_accumulation_across_many_fills(audusd_sim: CurrencyPair) {
2582        // Tests precision drift across 100 fills
2583        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2584        let order = OrderTestBuilder::new(OrderType::Market)
2585            .instrument_id(audusd_sim.id())
2586            .side(OrderSide::Buy)
2587            .quantity(Quantity::from(1000))
2588            .build();
2589
2590        let initial_fill = TestOrderEventStubs::filled(
2591            &order,
2592            &audusd_sim,
2593            Some(TradeId::new("1")),
2594            None,
2595            Some(Price::from("1.00000")),
2596            Some(Quantity::from(10)),
2597            None,
2598            Some(Money::from("0.01 USD")),
2599            None,
2600            None,
2601        );
2602
2603        let mut position = Position::new(&audusd_sim, initial_fill.into());
2604
2605        // Apply 99 more fills with varying prices
2606        for i in 2..=100 {
2607            let price_offset = (i as f64) * 0.00001;
2608            let fill = TestOrderEventStubs::filled(
2609                &order,
2610                &audusd_sim,
2611                Some(TradeId::new(i.to_string())),
2612                None,
2613                Some(Price::from(&format!("{:.5}", 1.0 + price_offset))),
2614                Some(Quantity::from(10)),
2615                None,
2616                Some(Money::from("0.01 USD")),
2617                None,
2618                None,
2619            );
2620            position.apply(&fill.into());
2621        }
2622
2623        // Verify we accumulated 100 fills
2624        assert_eq!(position.events.len(), 100);
2625        assert_eq!(position.quantity, Quantity::from(1000));
2626
2627        // Verify commissions accumulated (should be 100 * 0.01 = 1.0 USD)
2628        let total_commission: f64 = position.commissions().iter().map(|c| c.as_f64()).sum();
2629        assert!(
2630            (total_commission - 1.0).abs() < 1e-10,
2631            "Commission accumulation should be accurate: expected 1.0, got {}",
2632            total_commission
2633        );
2634
2635        // Verify average price is reasonable (should be around 1.0005)
2636        let avg_px = position.avg_px_open;
2637        assert!(
2638            avg_px > 1.0 && avg_px < 1.001,
2639            "Average price should be reasonable: got {}",
2640            avg_px
2641        );
2642    }
2643
2644    #[rstest]
2645    fn test_position_pnl_with_extreme_price_values(audusd_sim: CurrencyPair) {
2646        // Tests position handling with very large and very small prices
2647        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2648
2649        // Test with very small price
2650        let order_small = OrderTestBuilder::new(OrderType::Market)
2651            .instrument_id(audusd_sim.id())
2652            .side(OrderSide::Buy)
2653            .quantity(Quantity::from(100_000))
2654            .build();
2655
2656        let fill_small = TestOrderEventStubs::filled(
2657            &order_small,
2658            &audusd_sim,
2659            None,
2660            None,
2661            Some(Price::from("0.00001")),
2662            Some(Quantity::from(100_000)),
2663            None,
2664            None,
2665            None,
2666            None,
2667        );
2668
2669        let position_small = Position::new(&audusd_sim, fill_small.into());
2670        assert_eq!(position_small.avg_px_open, 0.00001);
2671
2672        // Verify notional calculation doesn't underflow
2673        let last_price_small = Price::from("0.00002");
2674        let unrealized = position_small.unrealized_pnl(last_price_small);
2675        assert!(
2676            unrealized.as_f64() > 0.0,
2677            "Unrealized PnL should be positive when price doubles"
2678        );
2679
2680        // Test with very large price
2681        let order_large = OrderTestBuilder::new(OrderType::Market)
2682            .instrument_id(audusd_sim.id())
2683            .side(OrderSide::Buy)
2684            .quantity(Quantity::from(100))
2685            .build();
2686
2687        let fill_large = TestOrderEventStubs::filled(
2688            &order_large,
2689            &audusd_sim,
2690            None,
2691            None,
2692            Some(Price::from("99999.99999")),
2693            Some(Quantity::from(100)),
2694            None,
2695            None,
2696            None,
2697            None,
2698        );
2699
2700        let position_large = Position::new(&audusd_sim, fill_large.into());
2701        assert!(
2702            (position_large.avg_px_open - 99999.99999).abs() < 1e-6,
2703            "Large price should be preserved within f64 tolerance"
2704        );
2705    }
2706
2707    #[rstest]
2708    fn test_position_pnl_roundtrip_precision(audusd_sim: CurrencyPair) {
2709        // Tests that opening and closing a position preserves precision
2710        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2711        let buy_order = OrderTestBuilder::new(OrderType::Market)
2712            .instrument_id(audusd_sim.id())
2713            .side(OrderSide::Buy)
2714            .quantity(Quantity::from(100_000))
2715            .build();
2716
2717        let sell_order = OrderTestBuilder::new(OrderType::Market)
2718            .instrument_id(audusd_sim.id())
2719            .side(OrderSide::Sell)
2720            .quantity(Quantity::from(100_000))
2721            .build();
2722
2723        // Open at precise price
2724        let open_fill = TestOrderEventStubs::filled(
2725            &buy_order,
2726            &audusd_sim,
2727            Some(TradeId::new("1")),
2728            None,
2729            Some(Price::from("1.123456")),
2730            None,
2731            None,
2732            Some(Money::from("0.50 USD")),
2733            None,
2734            None,
2735        );
2736
2737        let mut position = Position::new(&audusd_sim, open_fill.into());
2738
2739        // Close at same price (no profit/loss except commission)
2740        let close_fill = TestOrderEventStubs::filled(
2741            &sell_order,
2742            &audusd_sim,
2743            Some(TradeId::new("2")),
2744            None,
2745            Some(Price::from("1.123456")),
2746            None,
2747            None,
2748            Some(Money::from("0.50 USD")),
2749            None,
2750            None,
2751        );
2752
2753        position.apply(&close_fill.into());
2754
2755        // Position should be flat
2756        assert!(position.is_closed());
2757
2758        // Realized PnL should be exactly -1.0 USD (two commissions of 0.50)
2759        let realized = position.realized_pnl.unwrap().as_f64();
2760        assert!(
2761            (realized - (-1.0)).abs() < 1e-10,
2762            "Realized PnL should be exactly -1.0 USD (commissions), got {}",
2763            realized
2764        );
2765    }
2766}