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    fmt::Display,
20    hash::{Hash, Hasher},
21};
22
23use ahash::{AHashMap, AHashSet};
24use nautilus_core::{
25    UUID4, UnixNanos,
26    correctness::{FAILED, check_equal, check_predicate_true},
27};
28use rust_decimal::prelude::ToPrimitive;
29use serde::{Deserialize, Serialize};
30use ustr::Ustr;
31
32use crate::{
33    enums::{InstrumentClass, OrderSide, OrderSideSpecified, PositionAdjustmentType, PositionSide},
34    events::{OrderFilled, PositionAdjusted},
35    identifiers::{
36        AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, Symbol, TradeId, TraderId,
37        Venue, VenueOrderId,
38    },
39    instruments::{Instrument, InstrumentAny},
40    types::{Currency, Money, Price, Quantity},
41};
42
43/// Represents a position in a market.
44///
45/// The position ID may be assigned at the trading venue, or can be system
46/// generated depending on a strategies OMS (Order Management System) settings.
47#[repr(C)]
48#[derive(Debug, Clone, Serialize, Deserialize)]
49#[cfg_attr(
50    feature = "python",
51    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
52)]
53pub struct Position {
54    pub events: Vec<OrderFilled>,
55    pub adjustments: Vec<PositionAdjusted>,
56    pub trader_id: TraderId,
57    pub strategy_id: StrategyId,
58    pub instrument_id: InstrumentId,
59    pub id: PositionId,
60    pub account_id: AccountId,
61    pub opening_order_id: ClientOrderId,
62    pub closing_order_id: Option<ClientOrderId>,
63    pub entry: OrderSide,
64    pub side: PositionSide,
65    pub signed_qty: f64,
66    pub quantity: Quantity,
67    pub peak_qty: Quantity,
68    pub price_precision: u8,
69    pub size_precision: u8,
70    pub multiplier: Quantity,
71    pub is_inverse: bool,
72    pub is_currency_pair: bool,
73    pub instrument_class: InstrumentClass,
74    pub base_currency: Option<Currency>,
75    pub quote_currency: Currency,
76    pub settlement_currency: Currency,
77    pub ts_init: UnixNanos,
78    pub ts_opened: UnixNanos,
79    pub ts_last: UnixNanos,
80    pub ts_closed: Option<UnixNanos>,
81    pub duration_ns: u64,
82    pub avg_px_open: f64,
83    pub avg_px_close: Option<f64>,
84    pub realized_return: f64,
85    pub realized_pnl: Option<Money>,
86    pub trade_ids: Vec<TradeId>,
87    pub buy_qty: Quantity,
88    pub sell_qty: Quantity,
89    pub commissions: AHashMap<Currency, Money>,
90}
91
92impl Position {
93    /// Creates a new [`Position`] instance.
94    ///
95    /// # Panics
96    ///
97    /// This function panics if:
98    /// - The `instrument.id()` does not match the `fill.instrument_id`.
99    /// - The `fill.order_side` is `NoOrderSide`.
100    /// - The `fill.position_id` is `None`.
101    pub fn new(instrument: &InstrumentAny, fill: OrderFilled) -> Self {
102        check_equal(
103            &instrument.id(),
104            &fill.instrument_id,
105            "instrument.id()",
106            "fill.instrument_id",
107        )
108        .expect(FAILED);
109        assert_ne!(fill.order_side, OrderSide::NoOrderSide);
110
111        let position_id = fill.position_id.expect("No position ID to open `Position`");
112
113        let mut item = Self {
114            events: Vec::<OrderFilled>::new(),
115            adjustments: Vec::<PositionAdjusted>::new(),
116            trade_ids: Vec::<TradeId>::new(),
117            buy_qty: Quantity::zero(instrument.size_precision()),
118            sell_qty: Quantity::zero(instrument.size_precision()),
119            commissions: AHashMap::<Currency, Money>::new(),
120            trader_id: fill.trader_id,
121            strategy_id: fill.strategy_id,
122            instrument_id: fill.instrument_id,
123            id: position_id,
124            account_id: fill.account_id,
125            opening_order_id: fill.client_order_id,
126            closing_order_id: None,
127            entry: fill.order_side,
128            side: PositionSide::Flat,
129            signed_qty: 0.0,
130            quantity: fill.last_qty,
131            peak_qty: fill.last_qty,
132            price_precision: instrument.price_precision(),
133            size_precision: instrument.size_precision(),
134            multiplier: instrument.multiplier(),
135            is_inverse: instrument.is_inverse(),
136            is_currency_pair: matches!(instrument, InstrumentAny::CurrencyPair(_)),
137            instrument_class: instrument.instrument_class(),
138            base_currency: instrument.base_currency(),
139            quote_currency: instrument.quote_currency(),
140            settlement_currency: instrument.cost_currency(),
141            ts_init: fill.ts_init,
142            ts_opened: fill.ts_event,
143            ts_last: fill.ts_event,
144            ts_closed: None,
145            duration_ns: 0,
146            avg_px_open: fill.last_px.as_f64(),
147            avg_px_close: None,
148            realized_return: 0.0,
149            realized_pnl: None,
150        };
151        item.apply(&fill);
152        item
153    }
154
155    /// Purges all order fill events for the given client order ID and recalculates derived state.
156    ///
157    /// # Warning
158    ///
159    /// This operation recalculates the entire position from scratch after removing the specified
160    /// order's fills. This is an expensive operation and should be used sparingly.
161    ///
162    /// # Panics
163    ///
164    /// Panics if after purging, no fills remain and the position cannot be reconstructed.
165    pub fn purge_events_for_order(&mut self, client_order_id: ClientOrderId) {
166        let filtered_events: Vec<OrderFilled> = self
167            .events
168            .iter()
169            .filter(|e| e.client_order_id != client_order_id)
170            .copied()
171            .collect();
172
173        // Preserve non-commission adjustments (funding, manual adjustments, etc.)
174        // Commission adjustments will be automatically re-created when fills are replayed
175        let preserved_adjustments: Vec<PositionAdjusted> = self
176            .adjustments
177            .iter()
178            .filter(|adj| {
179                // Keep all non-commission adjustments (funding, manual, etc.)
180                // Commission adjustments will be re-created during fill replay
181                adj.adjustment_type != PositionAdjustmentType::Commission
182            })
183            .copied()
184            .collect();
185
186        // If no events remain, log warning - position should be closed/removed instead
187        if filtered_events.is_empty() {
188            log::warn!(
189                "Position {} has no fills remaining after purging order {}; consider closing the position instead",
190                self.id,
191                client_order_id
192            );
193            self.events.clear();
194            self.trade_ids.clear();
195            self.adjustments.clear();
196            self.buy_qty = Quantity::zero(self.size_precision);
197            self.sell_qty = Quantity::zero(self.size_precision);
198            self.commissions.clear();
199            self.signed_qty = 0.0;
200            self.quantity = Quantity::zero(self.size_precision);
201            self.side = PositionSide::Flat;
202            self.avg_px_close = None;
203            self.realized_pnl = None;
204            self.realized_return = 0.0;
205            self.ts_opened = UnixNanos::default();
206            self.ts_last = UnixNanos::default();
207            self.ts_closed = Some(UnixNanos::default());
208            self.duration_ns = 0;
209            return;
210        }
211
212        // Recalculate position from scratch
213        let position_id = self.id;
214        let size_precision = self.size_precision;
215
216        // Reset mutable state
217        self.events = Vec::new();
218        self.trade_ids = Vec::new();
219        self.adjustments = Vec::new();
220        self.buy_qty = Quantity::zero(size_precision);
221        self.sell_qty = Quantity::zero(size_precision);
222        self.commissions.clear();
223        self.signed_qty = 0.0;
224        self.quantity = Quantity::zero(size_precision);
225        self.peak_qty = Quantity::zero(size_precision);
226        self.side = PositionSide::Flat;
227        self.avg_px_open = 0.0;
228        self.avg_px_close = None;
229        self.realized_pnl = None;
230        self.realized_return = 0.0;
231
232        // Use the first remaining event to set opening state
233        let first_event = &filtered_events[0];
234        self.entry = first_event.order_side;
235        self.opening_order_id = first_event.client_order_id;
236        self.ts_opened = first_event.ts_event;
237        self.ts_init = first_event.ts_init;
238        self.closing_order_id = None;
239        self.ts_closed = None;
240        self.duration_ns = 0;
241
242        // Reapply all remaining fills to reconstruct state
243        for event in filtered_events {
244            self.apply(&event);
245        }
246
247        // Reapply preserved adjustments to maintain full state
248        for adjustment in preserved_adjustments {
249            self.apply_adjustment(adjustment);
250        }
251
252        log::info!(
253            "Purged fills for order {} from position {}; recalculated state: qty={}, signed_qty={}, side={:?}",
254            client_order_id,
255            position_id,
256            self.quantity,
257            self.signed_qty,
258            self.side
259        );
260    }
261
262    /// Applies an `OrderFilled` event to this position.
263    ///
264    /// # Panics
265    ///
266    /// Panics if the `fill.trade_id` is already present in the position’s `trade_ids`.
267    pub fn apply(&mut self, fill: &OrderFilled) {
268        check_predicate_true(
269            !self.trade_ids.contains(&fill.trade_id),
270            "`fill.trade_id` already contained in `trade_ids",
271        )
272        .expect(FAILED);
273        check_predicate_true(fill.ts_event >= self.ts_opened, "fill.ts_event < ts_opened")
274            .expect(FAILED);
275
276        if self.side == PositionSide::Flat {
277            // Reopening position after close
278            self.events.clear();
279            self.trade_ids.clear();
280            self.adjustments.clear();
281            self.buy_qty = Quantity::zero(self.size_precision);
282            self.sell_qty = Quantity::zero(self.size_precision);
283            self.commissions.clear();
284            self.opening_order_id = fill.client_order_id;
285            self.closing_order_id = None;
286            self.peak_qty = Quantity::zero(self.size_precision);
287            self.ts_init = fill.ts_init;
288            self.ts_opened = fill.ts_event;
289            self.ts_closed = None;
290            self.duration_ns = 0;
291            self.avg_px_open = fill.last_px.as_f64();
292            self.avg_px_close = None;
293            self.realized_return = 0.0;
294            self.realized_pnl = None;
295        }
296
297        self.events.push(*fill);
298        self.trade_ids.push(fill.trade_id);
299
300        // Calculate cumulative commissions
301        if let Some(commission) = fill.commission {
302            let commission_currency = commission.currency;
303            if let Some(existing_commission) = self.commissions.get_mut(&commission_currency) {
304                *existing_commission += commission;
305            } else {
306                self.commissions.insert(commission_currency, commission);
307            }
308        }
309
310        // Calculate avg prices, points, return, PnL
311        match fill.specified_side() {
312            OrderSideSpecified::Buy => {
313                self.handle_buy_order_fill(fill);
314            }
315            OrderSideSpecified::Sell => {
316                self.handle_sell_order_fill(fill);
317            }
318        }
319
320        // For CurrencyPair instruments, create adjustment event when commission is in base currency
321        if self.is_currency_pair
322            && let Some(commission) = fill.commission
323            && let Some(base_currency) = self.base_currency
324            && commission.currency == base_currency
325        {
326            let adjustment = PositionAdjusted::new(
327                self.trader_id,
328                self.strategy_id,
329                self.instrument_id,
330                self.id,
331                self.account_id,
332                PositionAdjustmentType::Commission,
333                Some(commission.as_decimal()),
334                None,
335                Some(Ustr::from(fill.client_order_id.as_ref())),
336                UUID4::new(),
337                fill.ts_event,
338                fill.ts_init,
339            );
340            self.apply_adjustment(adjustment);
341        }
342
343        // SAFETY: size_precision is valid from instrument
344        self.quantity = Quantity::new(self.signed_qty.abs(), self.size_precision);
345        if self.quantity > self.peak_qty {
346            self.peak_qty = self.quantity;
347        }
348
349        if self.signed_qty > 0.0 {
350            self.entry = OrderSide::Buy;
351            self.side = PositionSide::Long;
352        } else if self.signed_qty < 0.0 {
353            self.entry = OrderSide::Sell;
354            self.side = PositionSide::Short;
355        } else {
356            self.side = PositionSide::Flat;
357            self.closing_order_id = Some(fill.client_order_id);
358            self.ts_closed = Some(fill.ts_event);
359            self.duration_ns = if let Some(ts_closed) = self.ts_closed {
360                ts_closed.as_u64() - self.ts_opened.as_u64()
361            } else {
362                0
363            };
364        }
365
366        self.ts_last = fill.ts_event;
367    }
368
369    fn handle_buy_order_fill(&mut self, fill: &OrderFilled) {
370        // Handle case where commission could be None or not settlement currency
371        let mut realized_pnl = if let Some(commission) = fill.commission {
372            if commission.currency == self.settlement_currency {
373                -commission.as_f64()
374            } else {
375                0.0
376            }
377        } else {
378            0.0
379        };
380
381        let last_px = fill.last_px.as_f64();
382        let last_qty = fill.last_qty.as_f64();
383        let last_qty_object = fill.last_qty;
384
385        if self.signed_qty > 0.0 {
386            self.avg_px_open = self.calculate_avg_px_open_px(last_px, last_qty);
387        } else if self.signed_qty < 0.0 {
388            // Closing short position
389            let avg_px_close = self.calculate_avg_px_close_px(last_px, last_qty);
390            self.avg_px_close = Some(avg_px_close);
391            self.realized_return = self
392                .calculate_return(self.avg_px_open, avg_px_close)
393                .unwrap_or_else(|e| {
394                    log::error!("Error calculating return: {e}");
395                    0.0
396                });
397            realized_pnl += self
398                .calculate_pnl_raw(self.avg_px_open, last_px, last_qty)
399                .unwrap_or_else(|e| {
400                    log::error!("Error calculating PnL: {e}");
401                    0.0
402                });
403        }
404
405        let current_pnl = self.realized_pnl.map_or(0.0, |p| p.as_f64());
406        self.realized_pnl = Some(Money::new(
407            current_pnl + realized_pnl,
408            self.settlement_currency,
409        ));
410
411        self.signed_qty += last_qty;
412        self.buy_qty += last_qty_object;
413    }
414
415    fn handle_sell_order_fill(&mut self, fill: &OrderFilled) {
416        // Handle case where commission could be None or not settlement currency
417        let mut realized_pnl = if let Some(commission) = fill.commission {
418            if commission.currency == self.settlement_currency {
419                -commission.as_f64()
420            } else {
421                0.0
422            }
423        } else {
424            0.0
425        };
426
427        let last_px = fill.last_px.as_f64();
428        let last_qty = fill.last_qty.as_f64();
429        let last_qty_object = fill.last_qty;
430
431        if self.signed_qty < 0.0 {
432            self.avg_px_open = self.calculate_avg_px_open_px(last_px, last_qty);
433        } else if self.signed_qty > 0.0 {
434            // Closing long position
435            let avg_px_close = self.calculate_avg_px_close_px(last_px, last_qty);
436            self.avg_px_close = Some(avg_px_close);
437            self.realized_return = self
438                .calculate_return(self.avg_px_open, avg_px_close)
439                .unwrap_or_else(|e| {
440                    log::error!("Error calculating return: {e}");
441                    0.0
442                });
443            realized_pnl += self
444                .calculate_pnl_raw(self.avg_px_open, last_px, last_qty)
445                .unwrap_or_else(|e| {
446                    log::error!("Error calculating PnL: {e}");
447                    0.0
448                });
449        }
450
451        let current_pnl = self.realized_pnl.map_or(0.0, |p| p.as_f64());
452        self.realized_pnl = Some(Money::new(
453            current_pnl + realized_pnl,
454            self.settlement_currency,
455        ));
456
457        self.signed_qty -= last_qty;
458        self.sell_qty += last_qty_object;
459    }
460
461    /// Applies a position adjustment event.
462    ///
463    /// This method handles adjustments to position quantity or realized PnL that occur
464    /// outside of normal order fills, such as:
465    /// - Commission adjustments in base currency (crypto spot markets).
466    /// - Funding payments (perpetual futures).
467    ///
468    /// The adjustment event is stored in the position's adjustment history for full audit trail.
469    ///
470    /// # Panics
471    ///
472    /// Panics if the adjustment's `quantity_change` cannot be converted to f64.
473    pub fn apply_adjustment(&mut self, adjustment: PositionAdjusted) {
474        // Apply quantity change if present
475        if let Some(quantity_change) = adjustment.quantity_change {
476            self.signed_qty += quantity_change
477                .to_f64()
478                .expect("Failed to convert Decimal to f64");
479
480            self.quantity = Quantity::new(self.signed_qty.abs(), self.size_precision);
481
482            if self.quantity > self.peak_qty {
483                self.peak_qty = self.quantity;
484            }
485        }
486
487        // Apply PnL change if present
488        if let Some(pnl_change) = adjustment.pnl_change {
489            let current_pnl = self.realized_pnl.map_or(0.0, |p| p.as_f64());
490            self.realized_pnl = Some(Money::new(
491                current_pnl + pnl_change.as_f64(),
492                self.settlement_currency,
493            ));
494        }
495
496        // Update position state based on new signed quantity
497        if self.signed_qty > 0.0 {
498            self.side = PositionSide::Long;
499            if self.entry == OrderSide::NoOrderSide {
500                self.entry = OrderSide::Buy;
501            }
502        } else if self.signed_qty < 0.0 {
503            self.side = PositionSide::Short;
504            if self.entry == OrderSide::NoOrderSide {
505                self.entry = OrderSide::Sell;
506            }
507        } else {
508            self.side = PositionSide::Flat;
509        }
510
511        self.adjustments.push(adjustment);
512        self.ts_last = adjustment.ts_event;
513    }
514
515    /// Calculates the average price using f64 arithmetic.
516    ///
517    /// # Design Decision: f64 vs Fixed-Point Arithmetic
518    ///
519    /// This function uses f64 arithmetic which provides sufficient precision for financial
520    /// calculations in this context. While f64 can introduce precision errors, the risk
521    /// is minimal here because:
522    ///
523    /// 1. **No cumulative error**: Each calculation starts fresh from precise Price and
524    ///    Quantity objects (derived from fixed-point raw values via `as_f64()`), rather
525    ///    than carrying f64 intermediate results between calculations.
526    ///
527    /// 2. **Single operation**: This is a single weighted average calculation, not a
528    ///    chain of operations where errors would compound.
529    ///
530    /// 3. **Overflow safety**: Raw integer arithmetic (price_raw * qty_raw) would risk
531    ///    overflow even with i128 intermediates, since max values can exceed integer limits.
532    ///
533    /// 4. **f64 precision**: ~15 decimal digits is sufficient for typical financial
534    ///    calculations at this level.
535    ///
536    /// For scenarios requiring higher precision (regulatory compliance, high-frequency
537    /// micro-calculations), consider using Decimal arithmetic libraries.
538    ///
539    /// # Empirical Precision Validation
540    ///
541    /// Testing confirms f64 arithmetic maintains accuracy for typical trading scenarios:
542    /// - **Typical amounts**: No precision loss for amounts ≥ 0.01 in standard currencies.
543    /// - **High-precision instruments**: 9-decimal crypto prices preserved within 1e-6 tolerance.
544    /// - **Many fills**: 100 sequential fills show no drift (commission accuracy to 1e-10).
545    /// - **Extreme prices**: Handles range from 0.00001 to 99999.99999 without overflow/underflow.
546    /// - **Round-trip**: Open/close at same price produces exact PnL (commissions only).
547    ///
548    /// See precision validation tests: `test_position_pnl_precision_*`
549    ///
550    /// # Errors
551    ///
552    /// Returns an error if:
553    /// - Both `qty` and `last_qty` are zero.
554    /// - `last_qty` is zero (prevents division by zero).
555    /// - `total_qty` is zero or negative (arithmetic error).
556    fn calculate_avg_px(
557        &self,
558        qty: f64,
559        avg_pg: f64,
560        last_px: f64,
561        last_qty: f64,
562    ) -> anyhow::Result<f64> {
563        if qty == 0.0 && last_qty == 0.0 {
564            anyhow::bail!("Cannot calculate average price: both quantities are zero");
565        }
566
567        if last_qty == 0.0 {
568            anyhow::bail!("Cannot calculate average price: fill quantity is zero");
569        }
570
571        if qty == 0.0 {
572            return Ok(last_px);
573        }
574
575        let start_cost = avg_pg * qty;
576        let event_cost = last_px * last_qty;
577        let total_qty = qty + last_qty;
578
579        // Runtime check to prevent division by zero even in release builds
580        if total_qty <= 0.0 {
581            anyhow::bail!(
582                "Total quantity unexpectedly zero or negative in average price calculation: qty={qty}, last_qty={last_qty}, total_qty={total_qty}"
583            );
584        }
585
586        Ok((start_cost + event_cost) / total_qty)
587    }
588
589    fn calculate_avg_px_open_px(&self, last_px: f64, last_qty: f64) -> f64 {
590        self.calculate_avg_px(self.quantity.as_f64(), self.avg_px_open, last_px, last_qty)
591            .unwrap_or_else(|e| {
592                log::error!("Error calculating average open price: {e}");
593                last_px
594            })
595    }
596
597    fn calculate_avg_px_close_px(&self, last_px: f64, last_qty: f64) -> f64 {
598        let Some(avg_px_close) = self.avg_px_close else {
599            return last_px;
600        };
601        let closing_qty = if self.side == PositionSide::Long {
602            self.sell_qty
603        } else {
604            self.buy_qty
605        };
606        self.calculate_avg_px(closing_qty.as_f64(), avg_px_close, last_px, last_qty)
607            .unwrap_or_else(|e| {
608                log::error!("Error calculating average close price: {e}");
609                last_px
610            })
611    }
612
613    fn calculate_points(&self, avg_px_open: f64, avg_px_close: f64) -> f64 {
614        match self.side {
615            PositionSide::Long => avg_px_close - avg_px_open,
616            PositionSide::Short => avg_px_open - avg_px_close,
617            _ => 0.0, // FLAT
618        }
619    }
620
621    fn calculate_points_inverse(&self, avg_px_open: f64, avg_px_close: f64) -> anyhow::Result<f64> {
622        // Epsilon at the limit of IEEE f64 precision before rounding errors (f64::EPSILON ≈ 2.22e-16)
623        const EPSILON: f64 = 1e-15;
624
625        // Invalid state: zero or near-zero prices should never occur in valid market data
626        if avg_px_open.abs() < EPSILON {
627            anyhow::bail!(
628                "Cannot calculate inverse points: open price is zero or too small ({avg_px_open})"
629            );
630        }
631        if avg_px_close.abs() < EPSILON {
632            anyhow::bail!(
633                "Cannot calculate inverse points: close price is zero or too small ({avg_px_close})"
634            );
635        }
636
637        let inverse_open = 1.0 / avg_px_open;
638        let inverse_close = 1.0 / avg_px_close;
639        let result = match self.side {
640            PositionSide::Long => inverse_open - inverse_close,
641            PositionSide::Short => inverse_close - inverse_open,
642            _ => 0.0, // FLAT - this is a valid case
643        };
644        Ok(result)
645    }
646
647    fn calculate_return(&self, avg_px_open: f64, avg_px_close: f64) -> anyhow::Result<f64> {
648        // Prevent division by zero in return calculation
649        if avg_px_open == 0.0 {
650            anyhow::bail!(
651                "Cannot calculate return: open price is zero (close price: {avg_px_close})"
652            );
653        }
654        Ok(self.calculate_points(avg_px_open, avg_px_close) / avg_px_open)
655    }
656
657    fn calculate_pnl_raw(
658        &self,
659        avg_px_open: f64,
660        avg_px_close: f64,
661        quantity: f64,
662    ) -> anyhow::Result<f64> {
663        let quantity = quantity.min(self.signed_qty.abs());
664        let result = if self.is_inverse {
665            let points = self.calculate_points_inverse(avg_px_open, avg_px_close)?;
666            quantity * self.multiplier.as_f64() * points
667        } else {
668            quantity * self.multiplier.as_f64() * self.calculate_points(avg_px_open, avg_px_close)
669        };
670        Ok(result)
671    }
672
673    #[must_use]
674    pub fn calculate_pnl(&self, avg_px_open: f64, avg_px_close: f64, quantity: Quantity) -> Money {
675        let pnl_raw = self
676            .calculate_pnl_raw(avg_px_open, avg_px_close, quantity.as_f64())
677            .unwrap_or_else(|e| {
678                log::error!("Error calculating PnL: {e}");
679                0.0
680            });
681        Money::new(pnl_raw, self.settlement_currency)
682    }
683
684    #[must_use]
685    pub fn total_pnl(&self, last: Price) -> Money {
686        let realized_pnl = self.realized_pnl.map_or(0.0, |pnl| pnl.as_f64());
687        Money::new(
688            realized_pnl + self.unrealized_pnl(last).as_f64(),
689            self.settlement_currency,
690        )
691    }
692
693    #[must_use]
694    pub fn unrealized_pnl(&self, last: Price) -> Money {
695        if self.side == PositionSide::Flat {
696            Money::new(0.0, self.settlement_currency)
697        } else {
698            let avg_px_open = self.avg_px_open;
699            let avg_px_close = last.as_f64();
700            let quantity = self.quantity.as_f64();
701            let pnl = self
702                .calculate_pnl_raw(avg_px_open, avg_px_close, quantity)
703                .unwrap_or_else(|e| {
704                    log::error!("Error calculating unrealized PnL: {e}");
705                    0.0
706                });
707            Money::new(pnl, self.settlement_currency)
708        }
709    }
710
711    pub fn closing_order_side(&self) -> OrderSide {
712        match self.side {
713            PositionSide::Long => OrderSide::Sell,
714            PositionSide::Short => OrderSide::Buy,
715            _ => OrderSide::NoOrderSide,
716        }
717    }
718
719    #[must_use]
720    pub fn is_opposite_side(&self, side: OrderSide) -> bool {
721        self.entry != side
722    }
723
724    #[must_use]
725    pub fn symbol(&self) -> Symbol {
726        self.instrument_id.symbol
727    }
728
729    #[must_use]
730    pub fn venue(&self) -> Venue {
731        self.instrument_id.venue
732    }
733
734    #[must_use]
735    pub fn event_count(&self) -> usize {
736        self.events.len()
737    }
738
739    #[must_use]
740    pub fn client_order_ids(&self) -> Vec<ClientOrderId> {
741        // First to hash set to remove duplicate, then again iter to vector
742        let mut result = self
743            .events
744            .iter()
745            .map(|event| event.client_order_id)
746            .collect::<AHashSet<ClientOrderId>>()
747            .into_iter()
748            .collect::<Vec<ClientOrderId>>();
749        result.sort_unstable();
750        result
751    }
752
753    #[must_use]
754    pub fn venue_order_ids(&self) -> Vec<VenueOrderId> {
755        // First to hash set to remove duplicate, then again iter to vector
756        let mut result = self
757            .events
758            .iter()
759            .map(|event| event.venue_order_id)
760            .collect::<AHashSet<VenueOrderId>>()
761            .into_iter()
762            .collect::<Vec<VenueOrderId>>();
763        result.sort_unstable();
764        result
765    }
766
767    #[must_use]
768    pub fn trade_ids(&self) -> Vec<TradeId> {
769        let mut result = self
770            .events
771            .iter()
772            .map(|event| event.trade_id)
773            .collect::<AHashSet<TradeId>>()
774            .into_iter()
775            .collect::<Vec<TradeId>>();
776        result.sort_unstable();
777        result
778    }
779
780    /// Calculates the notional value based on the last price.
781    ///
782    /// # Panics
783    ///
784    /// Panics if `self.base_currency` is `None`.
785    #[must_use]
786    pub fn notional_value(&self, last: Price) -> Money {
787        if self.is_inverse {
788            Money::new(
789                self.quantity.as_f64() * self.multiplier.as_f64() * (1.0 / last.as_f64()),
790                self.base_currency.unwrap(),
791            )
792        } else {
793            Money::new(
794                self.quantity.as_f64() * last.as_f64() * self.multiplier.as_f64(),
795                self.quote_currency,
796            )
797        }
798    }
799
800    /// Returns the last `OrderFilled` event for the position (if any after purging).
801    #[must_use]
802    pub fn last_event(&self) -> Option<OrderFilled> {
803        self.events.last().copied()
804    }
805
806    #[must_use]
807    pub fn last_trade_id(&self) -> Option<TradeId> {
808        self.trade_ids.last().copied()
809    }
810
811    #[must_use]
812    pub fn is_long(&self) -> bool {
813        self.side == PositionSide::Long
814    }
815
816    #[must_use]
817    pub fn is_short(&self) -> bool {
818        self.side == PositionSide::Short
819    }
820
821    #[must_use]
822    pub fn is_open(&self) -> bool {
823        self.side != PositionSide::Flat && self.ts_closed.is_none()
824    }
825
826    #[must_use]
827    pub fn is_closed(&self) -> bool {
828        self.side == PositionSide::Flat && self.ts_closed.is_some()
829    }
830
831    #[must_use]
832    pub fn commissions(&self) -> Vec<Money> {
833        self.commissions.values().copied().collect()
834    }
835}
836
837impl PartialEq<Self> for Position {
838    fn eq(&self, other: &Self) -> bool {
839        self.id == other.id
840    }
841}
842
843impl Eq for Position {}
844
845impl Hash for Position {
846    fn hash<H: Hasher>(&self, state: &mut H) {
847        self.id.hash(state);
848    }
849}
850
851impl Display for Position {
852    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
853        let quantity_str = if self.quantity != Quantity::zero(self.price_precision) {
854            self.quantity.to_formatted_string() + " "
855        } else {
856            String::new()
857        };
858        write!(
859            f,
860            "Position({} {}{}, id={})",
861            self.side, quantity_str, self.instrument_id, self.id
862        )
863    }
864}
865
866#[cfg(test)]
867mod tests {
868    use std::str::FromStr;
869
870    use nautilus_core::UnixNanos;
871    use rstest::rstest;
872
873    use crate::{
874        enums::{LiquiditySide, OrderSide, OrderType, PositionAdjustmentType, PositionSide},
875        events::OrderFilled,
876        identifiers::{
877            AccountId, ClientOrderId, PositionId, StrategyId, TradeId, VenueOrderId, stubs::uuid4,
878        },
879        instruments::{CryptoPerpetual, CurrencyPair, Instrument, InstrumentAny, stubs::*},
880        orders::{Order, builder::OrderTestBuilder, stubs::TestOrderEventStubs},
881        position::Position,
882        stubs::*,
883        types::{Currency, Money, Price, Quantity},
884    };
885
886    #[rstest]
887    fn test_position_long_display(stub_position_long: Position) {
888        let display = format!("{stub_position_long}");
889        assert_eq!(display, "Position(LONG 1 AUD/USD.SIM, id=1)");
890    }
891
892    #[rstest]
893    fn test_position_short_display(stub_position_short: Position) {
894        let display = format!("{stub_position_short}");
895        assert_eq!(display, "Position(SHORT 1 AUD/USD.SIM, id=1)");
896    }
897
898    #[rstest]
899    #[should_panic(expected = "`fill.trade_id` already contained in `trade_ids")]
900    fn test_two_trades_with_same_trade_id_error(audusd_sim: CurrencyPair) {
901        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
902        let order1 = OrderTestBuilder::new(OrderType::Market)
903            .instrument_id(audusd_sim.id())
904            .side(OrderSide::Buy)
905            .quantity(Quantity::from(100_000))
906            .build();
907        let order2 = OrderTestBuilder::new(OrderType::Market)
908            .instrument_id(audusd_sim.id())
909            .side(OrderSide::Buy)
910            .quantity(Quantity::from(100_000))
911            .build();
912        let fill1 = TestOrderEventStubs::filled(
913            &order1,
914            &audusd_sim,
915            Some(TradeId::new("1")),
916            None,
917            Some(Price::from("1.00001")),
918            None,
919            None,
920            None,
921            None,
922            None,
923        );
924        let fill2 = TestOrderEventStubs::filled(
925            &order2,
926            &audusd_sim,
927            Some(TradeId::new("1")),
928            None,
929            Some(Price::from("1.00002")),
930            None,
931            None,
932            None,
933            None,
934            None,
935        );
936        let mut position = Position::new(&audusd_sim, fill1.into());
937        position.apply(&fill2.into());
938    }
939
940    #[rstest]
941    fn test_position_filled_with_buy_order(audusd_sim: CurrencyPair) {
942        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
943        let order = OrderTestBuilder::new(OrderType::Market)
944            .instrument_id(audusd_sim.id())
945            .side(OrderSide::Buy)
946            .quantity(Quantity::from(100_000))
947            .build();
948        let fill = TestOrderEventStubs::filled(
949            &order,
950            &audusd_sim,
951            None,
952            None,
953            Some(Price::from("1.00001")),
954            None,
955            None,
956            None,
957            None,
958            None,
959        );
960        let last_price = Price::from_str("1.0005").unwrap();
961        let position = Position::new(&audusd_sim, fill.into());
962        assert_eq!(position.symbol(), audusd_sim.id().symbol);
963        assert_eq!(position.venue(), audusd_sim.id().venue);
964        assert_eq!(position.closing_order_side(), OrderSide::Sell);
965        assert!(!position.is_opposite_side(OrderSide::Buy));
966        assert_eq!(position, position); // equality operator test
967        assert!(position.closing_order_id.is_none());
968        assert_eq!(position.quantity, Quantity::from(100_000));
969        assert_eq!(position.peak_qty, Quantity::from(100_000));
970        assert_eq!(position.size_precision, 0);
971        assert_eq!(position.signed_qty, 100_000.0);
972        assert_eq!(position.entry, OrderSide::Buy);
973        assert_eq!(position.side, PositionSide::Long);
974        assert_eq!(position.ts_opened.as_u64(), 0);
975        assert_eq!(position.duration_ns, 0);
976        assert_eq!(position.avg_px_open, 1.00001);
977        assert_eq!(position.event_count(), 1);
978        assert_eq!(position.id, PositionId::new("1"));
979        assert_eq!(position.events.len(), 1);
980        assert!(position.is_long());
981        assert!(!position.is_short());
982        assert!(position.is_open());
983        assert!(!position.is_closed());
984        assert_eq!(position.realized_return, 0.0);
985        assert_eq!(position.realized_pnl, Some(Money::from("-2.0 USD")));
986        assert_eq!(position.unrealized_pnl(last_price), Money::from("49.0 USD"));
987        assert_eq!(position.total_pnl(last_price), Money::from("47.0 USD"));
988        assert_eq!(position.commissions(), vec![Money::from("2.0 USD")]);
989        assert_eq!(
990            format!("{position}"),
991            "Position(LONG 100_000 AUD/USD.SIM, id=1)"
992        );
993    }
994
995    #[rstest]
996    fn test_position_filled_with_sell_order(audusd_sim: CurrencyPair) {
997        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
998        let order = OrderTestBuilder::new(OrderType::Market)
999            .instrument_id(audusd_sim.id())
1000            .side(OrderSide::Sell)
1001            .quantity(Quantity::from(100_000))
1002            .build();
1003        let fill = TestOrderEventStubs::filled(
1004            &order,
1005            &audusd_sim,
1006            None,
1007            None,
1008            Some(Price::from("1.00001")),
1009            None,
1010            None,
1011            None,
1012            None,
1013            None,
1014        );
1015        let last_price = Price::from_str("1.00050").unwrap();
1016        let position = Position::new(&audusd_sim, fill.into());
1017        assert_eq!(position.symbol(), audusd_sim.id().symbol);
1018        assert_eq!(position.venue(), audusd_sim.id().venue);
1019        assert_eq!(position.closing_order_side(), OrderSide::Buy);
1020        assert!(!position.is_opposite_side(OrderSide::Sell));
1021        assert_eq!(position, position); // Equality operator test
1022        assert!(position.closing_order_id.is_none());
1023        assert_eq!(position.quantity, Quantity::from(100_000));
1024        assert_eq!(position.peak_qty, Quantity::from(100_000));
1025        assert_eq!(position.signed_qty, -100_000.0);
1026        assert_eq!(position.entry, OrderSide::Sell);
1027        assert_eq!(position.side, PositionSide::Short);
1028        assert_eq!(position.ts_opened.as_u64(), 0);
1029        assert_eq!(position.avg_px_open, 1.00001);
1030        assert_eq!(position.event_count(), 1);
1031        assert_eq!(position.id, PositionId::new("1"));
1032        assert_eq!(position.events.len(), 1);
1033        assert!(!position.is_long());
1034        assert!(position.is_short());
1035        assert!(position.is_open());
1036        assert!(!position.is_closed());
1037        assert_eq!(position.realized_return, 0.0);
1038        assert_eq!(position.realized_pnl, Some(Money::from("-2.0 USD")));
1039        assert_eq!(
1040            position.unrealized_pnl(last_price),
1041            Money::from("-49.0 USD")
1042        );
1043        assert_eq!(position.total_pnl(last_price), Money::from("-51.0 USD"));
1044        assert_eq!(position.commissions(), vec![Money::from("2.0 USD")]);
1045        assert_eq!(
1046            format!("{position}"),
1047            "Position(SHORT 100_000 AUD/USD.SIM, id=1)"
1048        );
1049    }
1050
1051    #[rstest]
1052    fn test_position_partial_fills_with_buy_order(audusd_sim: CurrencyPair) {
1053        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1054        let order = OrderTestBuilder::new(OrderType::Market)
1055            .instrument_id(audusd_sim.id())
1056            .side(OrderSide::Buy)
1057            .quantity(Quantity::from(100_000))
1058            .build();
1059        let fill = TestOrderEventStubs::filled(
1060            &order,
1061            &audusd_sim,
1062            None,
1063            None,
1064            Some(Price::from("1.00001")),
1065            Some(Quantity::from(50_000)),
1066            None,
1067            None,
1068            None,
1069            None,
1070        );
1071        let last_price = Price::from_str("1.00048").unwrap();
1072        let position = Position::new(&audusd_sim, fill.into());
1073        assert_eq!(position.quantity, Quantity::from(50_000));
1074        assert_eq!(position.peak_qty, Quantity::from(50_000));
1075        assert_eq!(position.side, PositionSide::Long);
1076        assert_eq!(position.signed_qty, 50000.0);
1077        assert_eq!(position.avg_px_open, 1.00001);
1078        assert_eq!(position.event_count(), 1);
1079        assert_eq!(position.ts_opened.as_u64(), 0);
1080        assert!(position.is_long());
1081        assert!(!position.is_short());
1082        assert!(position.is_open());
1083        assert!(!position.is_closed());
1084        assert_eq!(position.realized_return, 0.0);
1085        assert_eq!(position.realized_pnl, Some(Money::from("-2.0 USD")));
1086        assert_eq!(position.unrealized_pnl(last_price), Money::from("23.5 USD"));
1087        assert_eq!(position.total_pnl(last_price), Money::from("21.5 USD"));
1088        assert_eq!(position.commissions(), vec![Money::from("2.0 USD")]);
1089        assert_eq!(
1090            format!("{position}"),
1091            "Position(LONG 50_000 AUD/USD.SIM, id=1)"
1092        );
1093    }
1094
1095    #[rstest]
1096    fn test_position_partial_fills_with_two_sell_orders(audusd_sim: CurrencyPair) {
1097        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1098        let order = OrderTestBuilder::new(OrderType::Market)
1099            .instrument_id(audusd_sim.id())
1100            .side(OrderSide::Sell)
1101            .quantity(Quantity::from(100_000))
1102            .build();
1103        let fill1 = TestOrderEventStubs::filled(
1104            &order,
1105            &audusd_sim,
1106            Some(TradeId::new("1")),
1107            None,
1108            Some(Price::from("1.00001")),
1109            Some(Quantity::from(50_000)),
1110            None,
1111            None,
1112            None,
1113            None,
1114        );
1115        let fill2 = TestOrderEventStubs::filled(
1116            &order,
1117            &audusd_sim,
1118            Some(TradeId::new("2")),
1119            None,
1120            Some(Price::from("1.00002")),
1121            Some(Quantity::from(50_000)),
1122            None,
1123            None,
1124            None,
1125            None,
1126        );
1127        let last_price = Price::from_str("1.0005").unwrap();
1128        let mut position = Position::new(&audusd_sim, fill1.into());
1129        position.apply(&fill2.into());
1130
1131        assert_eq!(position.quantity, Quantity::from(100_000));
1132        assert_eq!(position.peak_qty, Quantity::from(100_000));
1133        assert_eq!(position.side, PositionSide::Short);
1134        assert_eq!(position.signed_qty, -100_000.0);
1135        assert_eq!(position.avg_px_open, 1.000_015);
1136        assert_eq!(position.event_count(), 2);
1137        assert_eq!(position.ts_opened, 0);
1138        assert!(position.is_short());
1139        assert!(!position.is_long());
1140        assert!(position.is_open());
1141        assert!(!position.is_closed());
1142        assert_eq!(position.realized_return, 0.0);
1143        assert_eq!(position.realized_pnl, Some(Money::from("-4.0 USD")));
1144        assert_eq!(
1145            position.unrealized_pnl(last_price),
1146            Money::from("-48.5 USD")
1147        );
1148        assert_eq!(position.total_pnl(last_price), Money::from("-52.5 USD"));
1149        assert_eq!(position.commissions(), vec![Money::from("4.0 USD")]);
1150    }
1151
1152    #[rstest]
1153    pub fn test_position_filled_with_buy_order_then_sell_order(audusd_sim: CurrencyPair) {
1154        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1155        let order = OrderTestBuilder::new(OrderType::Market)
1156            .instrument_id(audusd_sim.id())
1157            .side(OrderSide::Buy)
1158            .quantity(Quantity::from(150_000))
1159            .build();
1160        let fill = TestOrderEventStubs::filled(
1161            &order,
1162            &audusd_sim,
1163            Some(TradeId::new("1")),
1164            Some(PositionId::new("P-1")),
1165            Some(Price::from("1.00001")),
1166            None,
1167            None,
1168            None,
1169            Some(UnixNanos::from(1_000_000_000)),
1170            None,
1171        );
1172        let mut position = Position::new(&audusd_sim, fill.into());
1173
1174        let fill2 = OrderFilled::new(
1175            order.trader_id(),
1176            StrategyId::new("S-001"),
1177            order.instrument_id(),
1178            order.client_order_id(),
1179            VenueOrderId::from("2"),
1180            order.account_id().unwrap_or(AccountId::new("SIM-001")),
1181            TradeId::new("2"),
1182            OrderSide::Sell,
1183            OrderType::Market,
1184            order.quantity(),
1185            Price::from("1.00011"),
1186            audusd_sim.quote_currency(),
1187            LiquiditySide::Taker,
1188            uuid4(),
1189            2_000_000_000.into(),
1190            0.into(),
1191            false,
1192            Some(PositionId::new("T1")),
1193            Some(Money::from("0.0 USD")),
1194        );
1195        position.apply(&fill2);
1196        let last = Price::from_str("1.0005").unwrap();
1197
1198        assert!(position.is_opposite_side(fill2.order_side));
1199        assert_eq!(
1200            position.quantity,
1201            Quantity::zero(audusd_sim.price_precision())
1202        );
1203        assert_eq!(position.size_precision, 0);
1204        assert_eq!(position.signed_qty, 0.0);
1205        assert_eq!(position.side, PositionSide::Flat);
1206        assert_eq!(position.ts_opened, 1_000_000_000);
1207        assert_eq!(position.ts_closed, Some(UnixNanos::from(2_000_000_000)));
1208        assert_eq!(position.duration_ns, 1_000_000_000);
1209        assert_eq!(position.avg_px_open, 1.00001);
1210        assert_eq!(position.avg_px_close, Some(1.00011));
1211        assert!(!position.is_long());
1212        assert!(!position.is_short());
1213        assert!(!position.is_open());
1214        assert!(position.is_closed());
1215        assert_eq!(position.realized_return, 9.999_900_000_998_888e-5);
1216        assert_eq!(position.realized_pnl, Some(Money::from("13.0 USD")));
1217        assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1218        assert_eq!(position.commissions(), vec![Money::from("2 USD")]);
1219        assert_eq!(position.total_pnl(last), Money::from("13 USD"));
1220        assert_eq!(format!("{position}"), "Position(FLAT AUD/USD.SIM, id=P-1)");
1221    }
1222
1223    #[rstest]
1224    pub fn test_position_filled_with_sell_order_then_buy_order(audusd_sim: CurrencyPair) {
1225        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1226        let order1 = OrderTestBuilder::new(OrderType::Market)
1227            .instrument_id(audusd_sim.id())
1228            .side(OrderSide::Sell)
1229            .quantity(Quantity::from(100_000))
1230            .build();
1231        let order2 = OrderTestBuilder::new(OrderType::Market)
1232            .instrument_id(audusd_sim.id())
1233            .side(OrderSide::Buy)
1234            .quantity(Quantity::from(100_000))
1235            .build();
1236        let fill1 = TestOrderEventStubs::filled(
1237            &order1,
1238            &audusd_sim,
1239            None,
1240            Some(PositionId::new("P-19700101-000000-001-001-1")),
1241            Some(Price::from("1.0")),
1242            None,
1243            None,
1244            None,
1245            None,
1246            None,
1247        );
1248        let mut position = Position::new(&audusd_sim, fill1.into());
1249        // create closing from order from different venue but same strategy
1250        let fill2 = TestOrderEventStubs::filled(
1251            &order2,
1252            &audusd_sim,
1253            Some(TradeId::new("1")),
1254            Some(PositionId::new("P-19700101-000000-001-001-1")),
1255            Some(Price::from("1.00001")),
1256            Some(Quantity::from(50_000)),
1257            None,
1258            None,
1259            None,
1260            None,
1261        );
1262        let fill3 = TestOrderEventStubs::filled(
1263            &order2,
1264            &audusd_sim,
1265            Some(TradeId::new("2")),
1266            Some(PositionId::new("P-19700101-000000-001-001-1")),
1267            Some(Price::from("1.00003")),
1268            Some(Quantity::from(50_000)),
1269            None,
1270            None,
1271            None,
1272            None,
1273        );
1274        let last = Price::from("1.0005");
1275        position.apply(&fill2.into());
1276        position.apply(&fill3.into());
1277
1278        assert_eq!(
1279            position.quantity,
1280            Quantity::zero(audusd_sim.price_precision())
1281        );
1282        assert_eq!(position.side, PositionSide::Flat);
1283        assert_eq!(position.ts_opened, 0);
1284        assert_eq!(position.avg_px_open, 1.0);
1285        assert_eq!(position.events.len(), 3);
1286        assert_eq!(position.ts_closed, Some(UnixNanos::default()));
1287        assert_eq!(position.avg_px_close, Some(1.00002));
1288        assert!(!position.is_long());
1289        assert!(!position.is_short());
1290        assert!(!position.is_open());
1291        assert!(position.is_closed());
1292        assert_eq!(position.commissions(), vec![Money::from("6.0 USD")]);
1293        assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1294        assert_eq!(position.realized_pnl, Some(Money::from("-8.0 USD")));
1295        assert_eq!(position.total_pnl(last), Money::from("-8.0 USD"));
1296        assert_eq!(
1297            format!("{position}"),
1298            "Position(FLAT AUD/USD.SIM, id=P-19700101-000000-001-001-1)"
1299        );
1300    }
1301
1302    #[rstest]
1303    fn test_position_filled_with_no_change(audusd_sim: CurrencyPair) {
1304        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1305        let order1 = OrderTestBuilder::new(OrderType::Market)
1306            .instrument_id(audusd_sim.id())
1307            .side(OrderSide::Buy)
1308            .quantity(Quantity::from(100_000))
1309            .build();
1310        let order2 = OrderTestBuilder::new(OrderType::Market)
1311            .instrument_id(audusd_sim.id())
1312            .side(OrderSide::Sell)
1313            .quantity(Quantity::from(100_000))
1314            .build();
1315        let fill1 = TestOrderEventStubs::filled(
1316            &order1,
1317            &audusd_sim,
1318            Some(TradeId::new("1")),
1319            Some(PositionId::new("P-19700101-000000-001-001-1")),
1320            Some(Price::from("1.0")),
1321            None,
1322            None,
1323            None,
1324            None,
1325            None,
1326        );
1327        let mut position = Position::new(&audusd_sim, fill1.into());
1328        let fill2 = TestOrderEventStubs::filled(
1329            &order2,
1330            &audusd_sim,
1331            Some(TradeId::new("2")),
1332            Some(PositionId::new("P-19700101-000000-001-001-1")),
1333            Some(Price::from("1.0")),
1334            None,
1335            None,
1336            None,
1337            None,
1338            None,
1339        );
1340        let last = Price::from("1.0005");
1341        position.apply(&fill2.into());
1342
1343        assert_eq!(
1344            position.quantity,
1345            Quantity::zero(audusd_sim.price_precision())
1346        );
1347        assert_eq!(position.closing_order_side(), OrderSide::NoOrderSide);
1348        assert_eq!(position.side, PositionSide::Flat);
1349        assert_eq!(position.ts_opened, 0);
1350        assert_eq!(position.avg_px_open, 1.0);
1351        assert_eq!(position.events.len(), 2);
1352        // assert_eq!(position.trade_ids, vec![fill1.trade_id, fill2.trade_id]);  // TODO
1353        assert_eq!(position.ts_closed, Some(UnixNanos::default()));
1354        assert_eq!(position.avg_px_close, Some(1.0));
1355        assert!(!position.is_long());
1356        assert!(!position.is_short());
1357        assert!(!position.is_open());
1358        assert!(position.is_closed());
1359        assert_eq!(position.commissions(), vec![Money::from("4.0 USD")]);
1360        assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1361        assert_eq!(position.realized_pnl, Some(Money::from("-4.0 USD")));
1362        assert_eq!(position.total_pnl(last), Money::from("-4.0 USD"));
1363        assert_eq!(
1364            format!("{position}"),
1365            "Position(FLAT AUD/USD.SIM, id=P-19700101-000000-001-001-1)"
1366        );
1367    }
1368
1369    #[rstest]
1370    fn test_position_long_with_multiple_filled_orders(audusd_sim: CurrencyPair) {
1371        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1372        let order1 = OrderTestBuilder::new(OrderType::Market)
1373            .instrument_id(audusd_sim.id())
1374            .side(OrderSide::Buy)
1375            .quantity(Quantity::from(100_000))
1376            .build();
1377        let order2 = OrderTestBuilder::new(OrderType::Market)
1378            .instrument_id(audusd_sim.id())
1379            .side(OrderSide::Buy)
1380            .quantity(Quantity::from(100_000))
1381            .build();
1382        let order3 = OrderTestBuilder::new(OrderType::Market)
1383            .instrument_id(audusd_sim.id())
1384            .side(OrderSide::Sell)
1385            .quantity(Quantity::from(200_000))
1386            .build();
1387        let fill1 = TestOrderEventStubs::filled(
1388            &order1,
1389            &audusd_sim,
1390            Some(TradeId::new("1")),
1391            Some(PositionId::new("P-123456")),
1392            Some(Price::from("1.0")),
1393            None,
1394            None,
1395            None,
1396            None,
1397            None,
1398        );
1399        let fill2 = TestOrderEventStubs::filled(
1400            &order2,
1401            &audusd_sim,
1402            Some(TradeId::new("2")),
1403            Some(PositionId::new("P-123456")),
1404            Some(Price::from("1.00001")),
1405            None,
1406            None,
1407            None,
1408            None,
1409            None,
1410        );
1411        let fill3 = TestOrderEventStubs::filled(
1412            &order3,
1413            &audusd_sim,
1414            Some(TradeId::new("3")),
1415            Some(PositionId::new("P-123456")),
1416            Some(Price::from("1.0001")),
1417            None,
1418            None,
1419            None,
1420            None,
1421            None,
1422        );
1423        let mut position = Position::new(&audusd_sim, fill1.into());
1424        let last = Price::from("1.0005");
1425        position.apply(&fill2.into());
1426        position.apply(&fill3.into());
1427
1428        assert_eq!(
1429            position.quantity,
1430            Quantity::zero(audusd_sim.price_precision())
1431        );
1432        assert_eq!(position.side, PositionSide::Flat);
1433        assert_eq!(position.ts_opened, 0);
1434        assert_eq!(position.avg_px_open, 1.000_005);
1435        assert_eq!(position.events.len(), 3);
1436        // assert_eq!(
1437        //     position.trade_ids,
1438        //     vec![fill1.trade_id, fill2.trade_id, fill3.trade_id]
1439        // );
1440        assert_eq!(position.ts_closed, Some(UnixNanos::default()));
1441        assert_eq!(position.avg_px_close, Some(1.0001));
1442        assert!(position.is_closed());
1443        assert!(!position.is_open());
1444        assert!(!position.is_long());
1445        assert!(!position.is_short());
1446        assert_eq!(position.commissions(), vec![Money::from("6.0 USD")]);
1447        assert_eq!(position.realized_pnl, Some(Money::from("13.0 USD")));
1448        assert_eq!(position.unrealized_pnl(last), Money::from("0 USD"));
1449        assert_eq!(position.total_pnl(last), Money::from("13 USD"));
1450        assert_eq!(
1451            format!("{position}"),
1452            "Position(FLAT AUD/USD.SIM, id=P-123456)"
1453        );
1454    }
1455
1456    #[rstest]
1457    fn test_pnl_calculation_from_trading_technologies_example(currency_pair_ethusdt: CurrencyPair) {
1458        let ethusdt = InstrumentAny::CurrencyPair(currency_pair_ethusdt);
1459        let quantity1 = Quantity::from(12);
1460        let price1 = Price::from("100.0");
1461        let order1 = OrderTestBuilder::new(OrderType::Market)
1462            .instrument_id(ethusdt.id())
1463            .side(OrderSide::Buy)
1464            .quantity(quantity1)
1465            .build();
1466        let commission1 = calculate_commission(&ethusdt, order1.quantity(), price1, None);
1467        let fill1 = TestOrderEventStubs::filled(
1468            &order1,
1469            &ethusdt,
1470            Some(TradeId::new("1")),
1471            Some(PositionId::new("P-123456")),
1472            Some(price1),
1473            None,
1474            None,
1475            Some(commission1),
1476            None,
1477            None,
1478        );
1479        let mut position = Position::new(&ethusdt, fill1.into());
1480        let quantity2 = Quantity::from(17);
1481        let order2 = OrderTestBuilder::new(OrderType::Market)
1482            .instrument_id(ethusdt.id())
1483            .side(OrderSide::Buy)
1484            .quantity(quantity2)
1485            .build();
1486        let price2 = Price::from("99.0");
1487        let commission2 = calculate_commission(&ethusdt, order2.quantity(), price2, None);
1488        let fill2 = TestOrderEventStubs::filled(
1489            &order2,
1490            &ethusdt,
1491            Some(TradeId::new("2")),
1492            Some(PositionId::new("P-123456")),
1493            Some(price2),
1494            None,
1495            None,
1496            Some(commission2),
1497            None,
1498            None,
1499        );
1500        position.apply(&fill2.into());
1501        assert_eq!(position.quantity, Quantity::from(29));
1502        assert_eq!(position.realized_pnl, Some(Money::from("-0.28830000 USDT")));
1503        assert_eq!(position.avg_px_open, 99.413_793_103_448_27);
1504        let quantity3 = Quantity::from(9);
1505        let order3 = OrderTestBuilder::new(OrderType::Market)
1506            .instrument_id(ethusdt.id())
1507            .side(OrderSide::Sell)
1508            .quantity(quantity3)
1509            .build();
1510        let price3 = Price::from("101.0");
1511        let commission3 = calculate_commission(&ethusdt, order3.quantity(), price3, None);
1512        let fill3 = TestOrderEventStubs::filled(
1513            &order3,
1514            &ethusdt,
1515            Some(TradeId::new("3")),
1516            Some(PositionId::new("P-123456")),
1517            Some(price3),
1518            None,
1519            None,
1520            Some(commission3),
1521            None,
1522            None,
1523        );
1524        position.apply(&fill3.into());
1525        assert_eq!(position.quantity, Quantity::from(20));
1526        assert_eq!(position.realized_pnl, Some(Money::from("13.89666207 USDT")));
1527        assert_eq!(position.avg_px_open, 99.413_793_103_448_27);
1528        let quantity4 = Quantity::from("4");
1529        let price4 = Price::from("105.0");
1530        let order4 = OrderTestBuilder::new(OrderType::Market)
1531            .instrument_id(ethusdt.id())
1532            .side(OrderSide::Sell)
1533            .quantity(quantity4)
1534            .build();
1535        let commission4 = calculate_commission(&ethusdt, order4.quantity(), price4, None);
1536        let fill4 = TestOrderEventStubs::filled(
1537            &order4,
1538            &ethusdt,
1539            Some(TradeId::new("4")),
1540            Some(PositionId::new("P-123456")),
1541            Some(price4),
1542            None,
1543            None,
1544            Some(commission4),
1545            None,
1546            None,
1547        );
1548        position.apply(&fill4.into());
1549        assert_eq!(position.quantity, Quantity::from("16"));
1550        assert_eq!(position.realized_pnl, Some(Money::from("36.19948966 USDT")));
1551        assert_eq!(position.avg_px_open, 99.413_793_103_448_27);
1552        let quantity5 = Quantity::from("3");
1553        let price5 = Price::from("103.0");
1554        let order5 = OrderTestBuilder::new(OrderType::Market)
1555            .instrument_id(ethusdt.id())
1556            .side(OrderSide::Buy)
1557            .quantity(quantity5)
1558            .build();
1559        let commission5 = calculate_commission(&ethusdt, order5.quantity(), price5, None);
1560        let fill5 = TestOrderEventStubs::filled(
1561            &order5,
1562            &ethusdt,
1563            Some(TradeId::new("5")),
1564            Some(PositionId::new("P-123456")),
1565            Some(price5),
1566            None,
1567            None,
1568            Some(commission5),
1569            None,
1570            None,
1571        );
1572        position.apply(&fill5.into());
1573        assert_eq!(position.quantity, Quantity::from("19"));
1574        assert_eq!(position.realized_pnl, Some(Money::from("36.16858966 USDT")));
1575        assert_eq!(position.avg_px_open, 99.980_036_297_640_65);
1576        assert_eq!(
1577            format!("{position}"),
1578            "Position(LONG 19.00000 ETHUSDT.BINANCE, id=P-123456)"
1579        );
1580    }
1581
1582    #[rstest]
1583    fn test_position_closed_and_reopened(audusd_sim: CurrencyPair) {
1584        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
1585        let quantity1 = Quantity::from(150_000);
1586        let price1 = Price::from("1.00001");
1587        let order = OrderTestBuilder::new(OrderType::Market)
1588            .instrument_id(audusd_sim.id())
1589            .side(OrderSide::Buy)
1590            .quantity(quantity1)
1591            .build();
1592        let commission1 = calculate_commission(&audusd_sim, quantity1, price1, None);
1593        let fill1 = TestOrderEventStubs::filled(
1594            &order,
1595            &audusd_sim,
1596            Some(TradeId::new("5")),
1597            Some(PositionId::new("P-123456")),
1598            Some(Price::from("1.00001")),
1599            None,
1600            None,
1601            Some(commission1),
1602            Some(UnixNanos::from(1_000_000_000)),
1603            None,
1604        );
1605        let mut position = Position::new(&audusd_sim, fill1.into());
1606
1607        let fill2 = OrderFilled::new(
1608            order.trader_id(),
1609            order.strategy_id(),
1610            order.instrument_id(),
1611            order.client_order_id(),
1612            VenueOrderId::from("2"),
1613            order.account_id().unwrap_or(AccountId::new("SIM-001")),
1614            TradeId::from("2"),
1615            OrderSide::Sell,
1616            OrderType::Market,
1617            order.quantity(),
1618            Price::from("1.00011"),
1619            audusd_sim.quote_currency(),
1620            LiquiditySide::Taker,
1621            uuid4(),
1622            UnixNanos::from(2_000_000_000),
1623            UnixNanos::default(),
1624            false,
1625            Some(PositionId::from("P-123456")),
1626            Some(Money::from("0 USD")),
1627        );
1628
1629        position.apply(&fill2);
1630
1631        let fill3 = OrderFilled::new(
1632            order.trader_id(),
1633            order.strategy_id(),
1634            order.instrument_id(),
1635            order.client_order_id(),
1636            VenueOrderId::from("2"),
1637            order.account_id().unwrap_or(AccountId::new("SIM-001")),
1638            TradeId::from("3"),
1639            OrderSide::Buy,
1640            OrderType::Market,
1641            order.quantity(),
1642            Price::from("1.00012"),
1643            audusd_sim.quote_currency(),
1644            LiquiditySide::Taker,
1645            uuid4(),
1646            UnixNanos::from(3_000_000_000),
1647            UnixNanos::default(),
1648            false,
1649            Some(PositionId::from("P-123456")),
1650            Some(Money::from("0 USD")),
1651        );
1652
1653        position.apply(&fill3);
1654
1655        let last = Price::from("1.0003");
1656        assert!(position.is_opposite_side(fill2.order_side));
1657        assert_eq!(position.quantity, Quantity::from(150_000));
1658        assert_eq!(position.peak_qty, Quantity::from(150_000));
1659        assert_eq!(position.side, PositionSide::Long);
1660        assert_eq!(position.opening_order_id, fill3.client_order_id);
1661        assert_eq!(position.closing_order_id, None);
1662        assert_eq!(position.closing_order_id, None);
1663        assert_eq!(position.ts_opened, 3_000_000_000);
1664        assert_eq!(position.duration_ns, 0);
1665        assert_eq!(position.avg_px_open, 1.00012);
1666        assert_eq!(position.event_count(), 1);
1667        assert_eq!(position.ts_closed, None);
1668        assert_eq!(position.avg_px_close, None);
1669        assert!(position.is_long());
1670        assert!(!position.is_short());
1671        assert!(position.is_open());
1672        assert!(!position.is_closed());
1673        assert_eq!(position.realized_return, 0.0);
1674        assert_eq!(position.realized_pnl, Some(Money::from("0 USD")));
1675        assert_eq!(position.unrealized_pnl(last), Money::from("27 USD"));
1676        assert_eq!(position.total_pnl(last), Money::from("27 USD"));
1677        assert_eq!(position.commissions(), vec![Money::from("0 USD")]);
1678        assert_eq!(
1679            format!("{position}"),
1680            "Position(LONG 150_000 AUD/USD.SIM, id=P-123456)"
1681        );
1682    }
1683
1684    #[rstest]
1685    fn test_position_realized_pnl_with_interleaved_order_sides(
1686        currency_pair_btcusdt: CurrencyPair,
1687    ) {
1688        let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1689        let order1 = OrderTestBuilder::new(OrderType::Market)
1690            .instrument_id(btcusdt.id())
1691            .side(OrderSide::Buy)
1692            .quantity(Quantity::from(12))
1693            .build();
1694        let commission1 =
1695            calculate_commission(&btcusdt, order1.quantity(), Price::from("10000.0"), None);
1696        let fill1 = TestOrderEventStubs::filled(
1697            &order1,
1698            &btcusdt,
1699            Some(TradeId::from("1")),
1700            Some(PositionId::from("P-19700101-000000-001-001-1")),
1701            Some(Price::from("10000.0")),
1702            None,
1703            None,
1704            Some(commission1),
1705            None,
1706            None,
1707        );
1708        let mut position = Position::new(&btcusdt, fill1.into());
1709        let order2 = OrderTestBuilder::new(OrderType::Market)
1710            .instrument_id(btcusdt.id())
1711            .side(OrderSide::Buy)
1712            .quantity(Quantity::from(17))
1713            .build();
1714        let commission2 =
1715            calculate_commission(&btcusdt, order2.quantity(), Price::from("9999.0"), None);
1716        let fill2 = TestOrderEventStubs::filled(
1717            &order2,
1718            &btcusdt,
1719            Some(TradeId::from("2")),
1720            Some(PositionId::from("P-19700101-000000-001-001-1")),
1721            Some(Price::from("9999.0")),
1722            None,
1723            None,
1724            Some(commission2),
1725            None,
1726            None,
1727        );
1728        position.apply(&fill2.into());
1729        assert_eq!(position.quantity, Quantity::from(29));
1730        assert_eq!(
1731            position.realized_pnl,
1732            Some(Money::from("-289.98300000 USDT"))
1733        );
1734        assert_eq!(position.avg_px_open, 9_999.413_793_103_447);
1735        let order3 = OrderTestBuilder::new(OrderType::Market)
1736            .instrument_id(btcusdt.id())
1737            .side(OrderSide::Sell)
1738            .quantity(Quantity::from(9))
1739            .build();
1740        let commission3 =
1741            calculate_commission(&btcusdt, order3.quantity(), Price::from("10001.0"), None);
1742        let fill3 = TestOrderEventStubs::filled(
1743            &order3,
1744            &btcusdt,
1745            Some(TradeId::from("3")),
1746            Some(PositionId::from("P-19700101-000000-001-001-1")),
1747            Some(Price::from("10001.0")),
1748            None,
1749            None,
1750            Some(commission3),
1751            None,
1752            None,
1753        );
1754        position.apply(&fill3.into());
1755        assert_eq!(position.quantity, Quantity::from(20));
1756        assert_eq!(
1757            position.realized_pnl,
1758            Some(Money::from("-365.71613793 USDT"))
1759        );
1760        assert_eq!(position.avg_px_open, 9_999.413_793_103_447);
1761        let order4 = OrderTestBuilder::new(OrderType::Market)
1762            .instrument_id(btcusdt.id())
1763            .side(OrderSide::Buy)
1764            .quantity(Quantity::from(3))
1765            .build();
1766        let commission4 =
1767            calculate_commission(&btcusdt, order4.quantity(), Price::from("10003.0"), None);
1768        let fill4 = TestOrderEventStubs::filled(
1769            &order4,
1770            &btcusdt,
1771            Some(TradeId::from("4")),
1772            Some(PositionId::from("P-19700101-000000-001-001-1")),
1773            Some(Price::from("10003.0")),
1774            None,
1775            None,
1776            Some(commission4),
1777            None,
1778            None,
1779        );
1780        position.apply(&fill4.into());
1781        assert_eq!(position.quantity, Quantity::from(23));
1782        assert_eq!(
1783            position.realized_pnl,
1784            Some(Money::from("-395.72513793 USDT"))
1785        );
1786        assert_eq!(position.avg_px_open, 9_999.881_559_220_39);
1787        let order5 = OrderTestBuilder::new(OrderType::Market)
1788            .instrument_id(btcusdt.id())
1789            .side(OrderSide::Sell)
1790            .quantity(Quantity::from(4))
1791            .build();
1792        let commission5 =
1793            calculate_commission(&btcusdt, order5.quantity(), Price::from("10005.0"), None);
1794        let fill5 = TestOrderEventStubs::filled(
1795            &order5,
1796            &btcusdt,
1797            Some(TradeId::from("5")),
1798            Some(PositionId::from("P-19700101-000000-001-001-1")),
1799            Some(Price::from("10005.0")),
1800            None,
1801            None,
1802            Some(commission5),
1803            None,
1804            None,
1805        );
1806        position.apply(&fill5.into());
1807        assert_eq!(position.quantity, Quantity::from(19));
1808        assert_eq!(
1809            position.realized_pnl,
1810            Some(Money::from("-415.27137481 USDT"))
1811        );
1812        assert_eq!(position.avg_px_open, 9_999.881_559_220_39);
1813        assert_eq!(
1814            format!("{position}"),
1815            "Position(LONG 19.000000 BTCUSDT.BINANCE, id=P-19700101-000000-001-001-1)"
1816        );
1817    }
1818
1819    #[rstest]
1820    fn test_calculate_pnl_when_given_position_side_flat_returns_zero(
1821        currency_pair_btcusdt: CurrencyPair,
1822    ) {
1823        let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1824        let order = OrderTestBuilder::new(OrderType::Market)
1825            .instrument_id(btcusdt.id())
1826            .side(OrderSide::Buy)
1827            .quantity(Quantity::from(12))
1828            .build();
1829        let fill = TestOrderEventStubs::filled(
1830            &order,
1831            &btcusdt,
1832            None,
1833            Some(PositionId::from("P-123456")),
1834            Some(Price::from("10500.0")),
1835            None,
1836            None,
1837            None,
1838            None,
1839            None,
1840        );
1841        let position = Position::new(&btcusdt, fill.into());
1842        let result = position.calculate_pnl(10500.0, 10500.0, Quantity::from("100000.0"));
1843        assert_eq!(result, Money::from("0 USDT"));
1844    }
1845
1846    #[rstest]
1847    fn test_calculate_pnl_for_long_position_win(currency_pair_btcusdt: CurrencyPair) {
1848        let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1849        let order = OrderTestBuilder::new(OrderType::Market)
1850            .instrument_id(btcusdt.id())
1851            .side(OrderSide::Buy)
1852            .quantity(Quantity::from(12))
1853            .build();
1854        let commission =
1855            calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1856        let fill = TestOrderEventStubs::filled(
1857            &order,
1858            &btcusdt,
1859            None,
1860            Some(PositionId::from("P-123456")),
1861            Some(Price::from("10500.0")),
1862            None,
1863            None,
1864            Some(commission),
1865            None,
1866            None,
1867        );
1868        let position = Position::new(&btcusdt, fill.into());
1869        let pnl = position.calculate_pnl(10500.0, 10510.0, Quantity::from("12.0"));
1870        assert_eq!(pnl, Money::from("120 USDT"));
1871        assert_eq!(position.realized_pnl, Some(Money::from("-126 USDT")));
1872        assert_eq!(
1873            position.unrealized_pnl(Price::from("10510.0")),
1874            Money::from("120.0 USDT")
1875        );
1876        assert_eq!(
1877            position.total_pnl(Price::from("10510.0")),
1878            Money::from("-6 USDT")
1879        );
1880        assert_eq!(position.commissions(), vec![Money::from("126.0 USDT")]);
1881    }
1882
1883    #[rstest]
1884    fn test_calculate_pnl_for_long_position_loss(currency_pair_btcusdt: CurrencyPair) {
1885        let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1886        let order = OrderTestBuilder::new(OrderType::Market)
1887            .instrument_id(btcusdt.id())
1888            .side(OrderSide::Buy)
1889            .quantity(Quantity::from(12))
1890            .build();
1891        let commission =
1892            calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1893        let fill = TestOrderEventStubs::filled(
1894            &order,
1895            &btcusdt,
1896            None,
1897            Some(PositionId::from("P-123456")),
1898            Some(Price::from("10500.0")),
1899            None,
1900            None,
1901            Some(commission),
1902            None,
1903            None,
1904        );
1905        let position = Position::new(&btcusdt, fill.into());
1906        let pnl = position.calculate_pnl(10500.0, 10480.5, Quantity::from("10.0"));
1907        assert_eq!(pnl, Money::from("-195 USDT"));
1908        assert_eq!(position.realized_pnl, Some(Money::from("-126 USDT")));
1909        assert_eq!(
1910            position.unrealized_pnl(Price::from("10480.50")),
1911            Money::from("-234.0 USDT")
1912        );
1913        assert_eq!(
1914            position.total_pnl(Price::from("10480.50")),
1915            Money::from("-360 USDT")
1916        );
1917        assert_eq!(position.commissions(), vec![Money::from("126.0 USDT")]);
1918    }
1919
1920    #[rstest]
1921    fn test_calculate_pnl_for_short_position_winning(currency_pair_btcusdt: CurrencyPair) {
1922        let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1923        let order = OrderTestBuilder::new(OrderType::Market)
1924            .instrument_id(btcusdt.id())
1925            .side(OrderSide::Sell)
1926            .quantity(Quantity::from("10.15"))
1927            .build();
1928        let commission =
1929            calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1930        let fill = TestOrderEventStubs::filled(
1931            &order,
1932            &btcusdt,
1933            None,
1934            Some(PositionId::from("P-123456")),
1935            Some(Price::from("10500.0")),
1936            None,
1937            None,
1938            Some(commission),
1939            None,
1940            None,
1941        );
1942        let position = Position::new(&btcusdt, fill.into());
1943        let pnl = position.calculate_pnl(10500.0, 10390.0, Quantity::from("10.15"));
1944        assert_eq!(pnl, Money::from("1116.5 USDT"));
1945        assert_eq!(
1946            position.unrealized_pnl(Price::from("10390.0")),
1947            Money::from("1116.5 USDT")
1948        );
1949        assert_eq!(position.realized_pnl, Some(Money::from("-106.575 USDT")));
1950        assert_eq!(position.commissions(), vec![Money::from("106.575 USDT")]);
1951        assert_eq!(
1952            position.notional_value(Price::from("10390.0")),
1953            Money::from("105458.5 USDT")
1954        );
1955    }
1956
1957    #[rstest]
1958    fn test_calculate_pnl_for_short_position_loss(currency_pair_btcusdt: CurrencyPair) {
1959        let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
1960        let order = OrderTestBuilder::new(OrderType::Market)
1961            .instrument_id(btcusdt.id())
1962            .side(OrderSide::Sell)
1963            .quantity(Quantity::from("10.0"))
1964            .build();
1965        let commission =
1966            calculate_commission(&btcusdt, order.quantity(), Price::from("10500.0"), None);
1967        let fill = TestOrderEventStubs::filled(
1968            &order,
1969            &btcusdt,
1970            None,
1971            Some(PositionId::from("P-123456")),
1972            Some(Price::from("10500.0")),
1973            None,
1974            None,
1975            Some(commission),
1976            None,
1977            None,
1978        );
1979        let position = Position::new(&btcusdt, fill.into());
1980        let pnl = position.calculate_pnl(10500.0, 10670.5, Quantity::from("10.0"));
1981        assert_eq!(pnl, Money::from("-1705 USDT"));
1982        assert_eq!(
1983            position.unrealized_pnl(Price::from("10670.5")),
1984            Money::from("-1705 USDT")
1985        );
1986        assert_eq!(position.realized_pnl, Some(Money::from("-105 USDT")));
1987        assert_eq!(position.commissions(), vec![Money::from("105 USDT")]);
1988        assert_eq!(
1989            position.notional_value(Price::from("10670.5")),
1990            Money::from("106705 USDT")
1991        );
1992    }
1993
1994    #[rstest]
1995    fn test_calculate_pnl_for_inverse1(xbtusd_bitmex: CryptoPerpetual) {
1996        let xbtusd_bitmex = InstrumentAny::CryptoPerpetual(xbtusd_bitmex);
1997        let order = OrderTestBuilder::new(OrderType::Market)
1998            .instrument_id(xbtusd_bitmex.id())
1999            .side(OrderSide::Sell)
2000            .quantity(Quantity::from("100000"))
2001            .build();
2002        let commission = calculate_commission(
2003            &xbtusd_bitmex,
2004            order.quantity(),
2005            Price::from("10000.0"),
2006            None,
2007        );
2008        let fill = TestOrderEventStubs::filled(
2009            &order,
2010            &xbtusd_bitmex,
2011            None,
2012            Some(PositionId::from("P-123456")),
2013            Some(Price::from("10000.0")),
2014            None,
2015            None,
2016            Some(commission),
2017            None,
2018            None,
2019        );
2020        let position = Position::new(&xbtusd_bitmex, fill.into());
2021        let pnl = position.calculate_pnl(10000.0, 11000.0, Quantity::from("100000.0"));
2022        assert_eq!(pnl, Money::from("-0.90909091 BTC"));
2023        assert_eq!(
2024            position.unrealized_pnl(Price::from("11000.0")),
2025            Money::from("-0.90909091 BTC")
2026        );
2027        assert_eq!(position.realized_pnl, Some(Money::from("-0.00750000 BTC")));
2028        assert_eq!(
2029            position.notional_value(Price::from("11000.0")),
2030            Money::from("9.09090909 BTC")
2031        );
2032    }
2033
2034    #[rstest]
2035    fn test_calculate_pnl_for_inverse2(ethusdt_bitmex: CryptoPerpetual) {
2036        let ethusdt_bitmex = InstrumentAny::CryptoPerpetual(ethusdt_bitmex);
2037        let order = OrderTestBuilder::new(OrderType::Market)
2038            .instrument_id(ethusdt_bitmex.id())
2039            .side(OrderSide::Sell)
2040            .quantity(Quantity::from("100000"))
2041            .build();
2042        let commission = calculate_commission(
2043            &ethusdt_bitmex,
2044            order.quantity(),
2045            Price::from("375.95"),
2046            None,
2047        );
2048        let fill = TestOrderEventStubs::filled(
2049            &order,
2050            &ethusdt_bitmex,
2051            None,
2052            Some(PositionId::from("P-123456")),
2053            Some(Price::from("375.95")),
2054            None,
2055            None,
2056            Some(commission),
2057            None,
2058            None,
2059        );
2060        let position = Position::new(&ethusdt_bitmex, fill.into());
2061
2062        assert_eq!(
2063            position.unrealized_pnl(Price::from("370.00")),
2064            Money::from("4.27745208 ETH")
2065        );
2066        assert_eq!(
2067            position.notional_value(Price::from("370.00")),
2068            Money::from("270.27027027 ETH")
2069        );
2070    }
2071
2072    #[rstest]
2073    fn test_calculate_unrealized_pnl_for_long(currency_pair_btcusdt: CurrencyPair) {
2074        let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
2075        let order1 = OrderTestBuilder::new(OrderType::Market)
2076            .instrument_id(btcusdt.id())
2077            .side(OrderSide::Buy)
2078            .quantity(Quantity::from("2.000000"))
2079            .build();
2080        let order2 = OrderTestBuilder::new(OrderType::Market)
2081            .instrument_id(btcusdt.id())
2082            .side(OrderSide::Buy)
2083            .quantity(Quantity::from("2.000000"))
2084            .build();
2085        let commission1 =
2086            calculate_commission(&btcusdt, order1.quantity(), Price::from("10500.0"), None);
2087        let fill1 = TestOrderEventStubs::filled(
2088            &order1,
2089            &btcusdt,
2090            Some(TradeId::new("1")),
2091            Some(PositionId::new("P-123456")),
2092            Some(Price::from("10500.00")),
2093            None,
2094            None,
2095            Some(commission1),
2096            None,
2097            None,
2098        );
2099        let commission2 =
2100            calculate_commission(&btcusdt, order2.quantity(), Price::from("10500.0"), None);
2101        let fill2 = TestOrderEventStubs::filled(
2102            &order2,
2103            &btcusdt,
2104            Some(TradeId::new("2")),
2105            Some(PositionId::new("P-123456")),
2106            Some(Price::from("10500.00")),
2107            None,
2108            None,
2109            Some(commission2),
2110            None,
2111            None,
2112        );
2113        let mut position = Position::new(&btcusdt, fill1.into());
2114        position.apply(&fill2.into());
2115        let pnl = position.unrealized_pnl(Price::from("11505.60"));
2116        assert_eq!(pnl, Money::from("4022.40000000 USDT"));
2117        assert_eq!(
2118            position.realized_pnl,
2119            Some(Money::from("-42.00000000 USDT"))
2120        );
2121        assert_eq!(
2122            position.commissions(),
2123            vec![Money::from("42.00000000 USDT")]
2124        );
2125    }
2126
2127    #[rstest]
2128    fn test_calculate_unrealized_pnl_for_short(currency_pair_btcusdt: CurrencyPair) {
2129        let btcusdt = InstrumentAny::CurrencyPair(currency_pair_btcusdt);
2130        let order = OrderTestBuilder::new(OrderType::Market)
2131            .instrument_id(btcusdt.id())
2132            .side(OrderSide::Sell)
2133            .quantity(Quantity::from("5.912000"))
2134            .build();
2135        let commission =
2136            calculate_commission(&btcusdt, order.quantity(), Price::from("10505.60"), None);
2137        let fill = TestOrderEventStubs::filled(
2138            &order,
2139            &btcusdt,
2140            Some(TradeId::new("1")),
2141            Some(PositionId::new("P-123456")),
2142            Some(Price::from("10505.60")),
2143            None,
2144            None,
2145            Some(commission),
2146            None,
2147            None,
2148        );
2149        let position = Position::new(&btcusdt, fill.into());
2150        let pnl = position.unrealized_pnl(Price::from("10407.15"));
2151        assert_eq!(pnl, Money::from("582.03640000 USDT"));
2152        assert_eq!(
2153            position.realized_pnl,
2154            Some(Money::from("-62.10910720 USDT"))
2155        );
2156        assert_eq!(
2157            position.commissions(),
2158            vec![Money::from("62.10910720 USDT")]
2159        );
2160    }
2161
2162    #[rstest]
2163    fn test_calculate_unrealized_pnl_for_long_inverse(xbtusd_bitmex: CryptoPerpetual) {
2164        let xbtusd_bitmex = InstrumentAny::CryptoPerpetual(xbtusd_bitmex);
2165        let order = OrderTestBuilder::new(OrderType::Market)
2166            .instrument_id(xbtusd_bitmex.id())
2167            .side(OrderSide::Buy)
2168            .quantity(Quantity::from("100000"))
2169            .build();
2170        let commission = calculate_commission(
2171            &xbtusd_bitmex,
2172            order.quantity(),
2173            Price::from("10500.0"),
2174            None,
2175        );
2176        let fill = TestOrderEventStubs::filled(
2177            &order,
2178            &xbtusd_bitmex,
2179            Some(TradeId::new("1")),
2180            Some(PositionId::new("P-123456")),
2181            Some(Price::from("10500.00")),
2182            None,
2183            None,
2184            Some(commission),
2185            None,
2186            None,
2187        );
2188
2189        let position = Position::new(&xbtusd_bitmex, fill.into());
2190        let pnl = position.unrealized_pnl(Price::from("11505.60"));
2191        assert_eq!(pnl, Money::from("0.83238969 BTC"));
2192        assert_eq!(position.realized_pnl, Some(Money::from("-0.00714286 BTC")));
2193        assert_eq!(position.commissions(), vec![Money::from("0.00714286 BTC")]);
2194    }
2195
2196    #[rstest]
2197    fn test_calculate_unrealized_pnl_for_short_inverse(xbtusd_bitmex: CryptoPerpetual) {
2198        let xbtusd_bitmex = InstrumentAny::CryptoPerpetual(xbtusd_bitmex);
2199        let order = OrderTestBuilder::new(OrderType::Market)
2200            .instrument_id(xbtusd_bitmex.id())
2201            .side(OrderSide::Sell)
2202            .quantity(Quantity::from("1250000"))
2203            .build();
2204        let commission = calculate_commission(
2205            &xbtusd_bitmex,
2206            order.quantity(),
2207            Price::from("15500.00"),
2208            None,
2209        );
2210        let fill = TestOrderEventStubs::filled(
2211            &order,
2212            &xbtusd_bitmex,
2213            Some(TradeId::new("1")),
2214            Some(PositionId::new("P-123456")),
2215            Some(Price::from("15500.00")),
2216            None,
2217            None,
2218            Some(commission),
2219            None,
2220            None,
2221        );
2222        let position = Position::new(&xbtusd_bitmex, fill.into());
2223        let pnl = position.unrealized_pnl(Price::from("12506.65"));
2224
2225        assert_eq!(pnl, Money::from("19.30166700 BTC"));
2226        assert_eq!(position.realized_pnl, Some(Money::from("-0.06048387 BTC")));
2227        assert_eq!(position.commissions(), vec![Money::from("0.06048387 BTC")]);
2228    }
2229
2230    #[rstest]
2231    #[case(OrderSide::Buy, 25, 25.0)]
2232    #[case(OrderSide::Sell,25,-25.0)]
2233    fn test_signed_qty_decimal_qty_for_equity(
2234        #[case] order_side: OrderSide,
2235        #[case] quantity: i64,
2236        #[case] expected: f64,
2237        audusd_sim: CurrencyPair,
2238    ) {
2239        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2240        let order = OrderTestBuilder::new(OrderType::Market)
2241            .instrument_id(audusd_sim.id())
2242            .side(order_side)
2243            .quantity(Quantity::from(quantity))
2244            .build();
2245
2246        let commission =
2247            calculate_commission(&audusd_sim, order.quantity(), Price::from("1.0"), None);
2248        let fill = TestOrderEventStubs::filled(
2249            &order,
2250            &audusd_sim,
2251            None,
2252            Some(PositionId::from("P-123456")),
2253            None,
2254            None,
2255            None,
2256            Some(commission),
2257            None,
2258            None,
2259        );
2260        let position = Position::new(&audusd_sim, fill.into());
2261        assert_eq!(position.signed_qty, expected);
2262    }
2263
2264    #[rstest]
2265    fn test_position_with_commission_none(audusd_sim: CurrencyPair) {
2266        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2267        let fill = OrderFilled {
2268            position_id: Some(PositionId::from("1")),
2269            ..Default::default()
2270        };
2271
2272        let position = Position::new(&audusd_sim, fill);
2273        assert_eq!(position.realized_pnl, Some(Money::from("0 USD")));
2274    }
2275
2276    #[rstest]
2277    fn test_position_with_commission_zero(audusd_sim: CurrencyPair) {
2278        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2279        let fill = OrderFilled {
2280            position_id: Some(PositionId::from("1")),
2281            commission: Some(Money::from("0 USD")),
2282            ..Default::default()
2283        };
2284
2285        let position = Position::new(&audusd_sim, fill);
2286        assert_eq!(position.realized_pnl, Some(Money::from("0 USD")));
2287    }
2288
2289    #[rstest]
2290    fn test_cache_purge_order_events() {
2291        let audusd_sim = audusd_sim();
2292        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2293
2294        let order1 = OrderTestBuilder::new(OrderType::Market)
2295            .client_order_id(ClientOrderId::new("O-1"))
2296            .instrument_id(audusd_sim.id())
2297            .side(OrderSide::Buy)
2298            .quantity(Quantity::from(50_000))
2299            .build();
2300
2301        let order2 = OrderTestBuilder::new(OrderType::Market)
2302            .client_order_id(ClientOrderId::new("O-2"))
2303            .instrument_id(audusd_sim.id())
2304            .side(OrderSide::Buy)
2305            .quantity(Quantity::from(50_000))
2306            .build();
2307
2308        let position_id = PositionId::new("P-123456");
2309
2310        let fill1 = TestOrderEventStubs::filled(
2311            &order1,
2312            &audusd_sim,
2313            Some(TradeId::new("1")),
2314            Some(position_id),
2315            Some(Price::from("1.00001")),
2316            None,
2317            None,
2318            None,
2319            None,
2320            None,
2321        );
2322
2323        let mut position = Position::new(&audusd_sim, fill1.into());
2324
2325        let fill2 = TestOrderEventStubs::filled(
2326            &order2,
2327            &audusd_sim,
2328            Some(TradeId::new("2")),
2329            Some(position_id),
2330            Some(Price::from("1.00002")),
2331            None,
2332            None,
2333            None,
2334            None,
2335            None,
2336        );
2337
2338        position.apply(&fill2.into());
2339        position.purge_events_for_order(order1.client_order_id());
2340
2341        assert_eq!(position.events.len(), 1);
2342        assert_eq!(position.trade_ids.len(), 1);
2343        assert_eq!(position.events[0].client_order_id, order2.client_order_id());
2344        assert_eq!(position.trade_ids[0], TradeId::new("2"));
2345    }
2346
2347    #[rstest]
2348    fn test_purge_all_events_returns_none_for_last_event_and_trade_id() {
2349        let audusd_sim = audusd_sim();
2350        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2351
2352        let order = OrderTestBuilder::new(OrderType::Market)
2353            .client_order_id(ClientOrderId::new("O-1"))
2354            .instrument_id(audusd_sim.id())
2355            .side(OrderSide::Buy)
2356            .quantity(Quantity::from(100_000))
2357            .build();
2358
2359        let position_id = PositionId::new("P-123456");
2360        let fill = TestOrderEventStubs::filled(
2361            &order,
2362            &audusd_sim,
2363            Some(TradeId::new("1")),
2364            Some(position_id),
2365            Some(Price::from("1.00050")),
2366            None,
2367            None,
2368            None,
2369            Some(UnixNanos::from(1_000_000_000)), // Explicit non-zero timestamp
2370            None,
2371        );
2372
2373        let mut position = Position::new(&audusd_sim, fill.into());
2374
2375        assert_eq!(position.events.len(), 1);
2376        assert!(position.last_event().is_some());
2377        assert!(position.last_trade_id().is_some());
2378
2379        // Store original timestamps (should be non-zero)
2380        let original_ts_opened = position.ts_opened;
2381        let original_ts_last = position.ts_last;
2382        assert_ne!(original_ts_opened, UnixNanos::default());
2383        assert_ne!(original_ts_last, UnixNanos::default());
2384
2385        position.purge_events_for_order(order.client_order_id());
2386
2387        assert_eq!(position.events.len(), 0);
2388        assert_eq!(position.trade_ids.len(), 0);
2389        assert!(position.last_event().is_none());
2390        assert!(position.last_trade_id().is_none());
2391
2392        // Verify timestamps are zeroed - empty shell has no meaningful history
2393        // ts_closed is set to Some(0) so position reports as closed and is eligible for purge
2394        assert_eq!(position.ts_opened, UnixNanos::default());
2395        assert_eq!(position.ts_last, UnixNanos::default());
2396        assert_eq!(position.ts_closed, Some(UnixNanos::default()));
2397        assert_eq!(position.duration_ns, 0);
2398
2399        // Verify empty shell reports as closed (this was the bug we fixed!)
2400        // is_closed() must return true so cache purge logic recognizes empty shells
2401        assert!(position.is_closed());
2402        assert!(!position.is_open());
2403        assert_eq!(position.side, PositionSide::Flat);
2404    }
2405
2406    #[rstest]
2407    fn test_revive_from_empty_shell(audusd_sim: CurrencyPair) {
2408        // Test adding a fill to an empty shell position
2409        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2410
2411        // Create and then purge position to get empty shell
2412        let order1 = OrderTestBuilder::new(OrderType::Market)
2413            .instrument_id(audusd_sim.id())
2414            .side(OrderSide::Buy)
2415            .quantity(Quantity::from(100_000))
2416            .build();
2417
2418        let fill1 = TestOrderEventStubs::filled(
2419            &order1,
2420            &audusd_sim,
2421            None,
2422            Some(PositionId::new("P-1")),
2423            Some(Price::from("1.00000")),
2424            None,
2425            None,
2426            None,
2427            Some(UnixNanos::from(1_000_000_000)),
2428            None,
2429        );
2430
2431        let mut position = Position::new(&audusd_sim, fill1.into());
2432        position.purge_events_for_order(order1.client_order_id());
2433
2434        // Verify it's an empty shell
2435        assert!(position.is_closed());
2436        assert_eq!(position.ts_closed, Some(UnixNanos::default()));
2437        assert_eq!(position.event_count(), 0);
2438
2439        // Add new fill to revive the position
2440        let order2 = OrderTestBuilder::new(OrderType::Market)
2441            .instrument_id(audusd_sim.id())
2442            .side(OrderSide::Buy)
2443            .quantity(Quantity::from(50_000))
2444            .build();
2445
2446        let fill2 = TestOrderEventStubs::filled(
2447            &order2,
2448            &audusd_sim,
2449            None,
2450            Some(PositionId::new("P-1")),
2451            Some(Price::from("1.00020")),
2452            None,
2453            None,
2454            None,
2455            Some(UnixNanos::from(3_000_000_000)),
2456            None,
2457        );
2458
2459        let fill2_typed: OrderFilled = fill2.clone().into();
2460        position.apply(&fill2_typed);
2461
2462        // Position should be alive with new timestamps
2463        assert!(position.is_long());
2464        assert!(!position.is_closed());
2465        assert!(position.ts_closed.is_none());
2466        assert_eq!(position.ts_opened, fill2.ts_event());
2467        assert_eq!(position.ts_last, fill2.ts_event());
2468        assert_eq!(position.event_count(), 1);
2469        assert_eq!(position.quantity, Quantity::from(50_000));
2470    }
2471
2472    #[rstest]
2473    fn test_empty_shell_position_invariants(audusd_sim: CurrencyPair) {
2474        // Property-based test: Any position with event_count == 0 must satisfy invariants
2475        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2476
2477        let order = OrderTestBuilder::new(OrderType::Market)
2478            .instrument_id(audusd_sim.id())
2479            .side(OrderSide::Buy)
2480            .quantity(Quantity::from(100_000))
2481            .build();
2482
2483        let fill = TestOrderEventStubs::filled(
2484            &order,
2485            &audusd_sim,
2486            None,
2487            Some(PositionId::new("P-1")),
2488            Some(Price::from("1.00000")),
2489            None,
2490            None,
2491            None,
2492            Some(UnixNanos::from(1_000_000_000)),
2493            None,
2494        );
2495
2496        let mut position = Position::new(&audusd_sim, fill.into());
2497        position.purge_events_for_order(order.client_order_id());
2498
2499        // INVARIANTS: When event_count == 0, the following MUST be true
2500        assert_eq!(
2501            position.event_count(),
2502            0,
2503            "Precondition: event_count must be 0"
2504        );
2505
2506        // Invariant 1: Position must report as closed
2507        assert!(
2508            position.is_closed(),
2509            "INV1: Empty shell must report is_closed() == true"
2510        );
2511        assert!(
2512            !position.is_open(),
2513            "INV1: Empty shell must report is_open() == false"
2514        );
2515
2516        // Invariant 2: Position must be FLAT
2517        assert_eq!(
2518            position.side,
2519            PositionSide::Flat,
2520            "INV2: Empty shell must be FLAT"
2521        );
2522
2523        // Invariant 3: ts_closed must be Some (not None)
2524        assert!(
2525            position.ts_closed.is_some(),
2526            "INV3: Empty shell must have ts_closed.is_some()"
2527        );
2528        assert_eq!(
2529            position.ts_closed,
2530            Some(UnixNanos::default()),
2531            "INV3: Empty shell ts_closed must be 0"
2532        );
2533
2534        // Invariant 4: All lifecycle timestamps must be zeroed
2535        assert_eq!(
2536            position.ts_opened,
2537            UnixNanos::default(),
2538            "INV4: Empty shell ts_opened must be 0"
2539        );
2540        assert_eq!(
2541            position.ts_last,
2542            UnixNanos::default(),
2543            "INV4: Empty shell ts_last must be 0"
2544        );
2545        assert_eq!(
2546            position.duration_ns, 0,
2547            "INV4: Empty shell duration_ns must be 0"
2548        );
2549
2550        // Invariant 5: Quantity must be zero
2551        assert_eq!(
2552            position.quantity,
2553            Quantity::zero(audusd_sim.size_precision()),
2554            "INV5: Empty shell quantity must be 0"
2555        );
2556
2557        // Invariant 6: No events or trade IDs
2558        assert!(
2559            position.events.is_empty(),
2560            "INV6: Empty shell must have no events"
2561        );
2562        assert!(
2563            position.trade_ids.is_empty(),
2564            "INV6: Empty shell must have no trade IDs"
2565        );
2566        assert!(
2567            position.last_event().is_none(),
2568            "INV6: Empty shell must have no last event"
2569        );
2570        assert!(
2571            position.last_trade_id().is_none(),
2572            "INV6: Empty shell must have no last trade ID"
2573        );
2574    }
2575
2576    #[rstest]
2577    fn test_position_pnl_precision_with_very_small_amounts(audusd_sim: CurrencyPair) {
2578        // Tests behavior with very small commission amounts
2579        // NOTE: Amounts below f64 epsilon (~1e-15) may be lost to precision
2580        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2581        let order = OrderTestBuilder::new(OrderType::Market)
2582            .instrument_id(audusd_sim.id())
2583            .side(OrderSide::Buy)
2584            .quantity(Quantity::from(100))
2585            .build();
2586
2587        // Test with a commission that won't be lost to Money precision (0.01 USD)
2588        let small_commission = Money::new(0.01, Currency::USD());
2589        let fill = TestOrderEventStubs::filled(
2590            &order,
2591            &audusd_sim,
2592            None,
2593            None,
2594            Some(Price::from("1.00001")),
2595            Some(Quantity::from(100)),
2596            None,
2597            Some(small_commission),
2598            None,
2599            None,
2600        );
2601
2602        let position = Position::new(&audusd_sim, fill.into());
2603
2604        // Commission is recorded and preserved in f64 arithmetic
2605        assert_eq!(position.commissions().len(), 1);
2606        let recorded_commission = position.commissions()[0];
2607        assert!(
2608            recorded_commission.as_f64() > 0.0,
2609            "Commission of 0.01 should be preserved"
2610        );
2611
2612        // Realized PnL should include commission (negative)
2613        let realized = position.realized_pnl.unwrap().as_f64();
2614        assert!(
2615            realized < 0.0,
2616            "Realized PnL should be negative due to commission"
2617        );
2618    }
2619
2620    #[rstest]
2621    fn test_position_pnl_precision_with_high_precision_instrument() {
2622        // Tests precision with high-precision crypto instrument
2623        use crate::instruments::stubs::crypto_perpetual_ethusdt;
2624        let ethusdt = crypto_perpetual_ethusdt();
2625        let ethusdt = InstrumentAny::CryptoPerpetual(ethusdt);
2626
2627        // Check instrument precision
2628        let size_precision = ethusdt.size_precision();
2629
2630        let order = OrderTestBuilder::new(OrderType::Market)
2631            .instrument_id(ethusdt.id())
2632            .side(OrderSide::Buy)
2633            .quantity(Quantity::from("1.123456789"))
2634            .build();
2635
2636        let fill = TestOrderEventStubs::filled(
2637            &order,
2638            &ethusdt,
2639            None,
2640            None,
2641            Some(Price::from("2345.123456789")),
2642            Some(Quantity::from("1.123456789")),
2643            None,
2644            Some(Money::from("0.1 USDT")),
2645            None,
2646            None,
2647        );
2648
2649        let position = Position::new(&ethusdt, fill.into());
2650
2651        // Verify high-precision price is preserved in f64 (within tolerance)
2652        let avg_px = position.avg_px_open;
2653        assert!(
2654            (avg_px - 2345.123456789).abs() < 1e-6,
2655            "High precision price should be preserved within f64 tolerance"
2656        );
2657
2658        // Quantity will be rounded to instrument's size_precision
2659        // Verify it matches the instrument's precision
2660        assert_eq!(
2661            position.quantity.precision, size_precision,
2662            "Quantity precision should match instrument"
2663        );
2664
2665        // f64 representation will be close but may have rounding based on precision
2666        let qty_f64 = position.quantity.as_f64();
2667        assert!(
2668            qty_f64 > 1.0 && qty_f64 < 2.0,
2669            "Quantity should be in expected range"
2670        );
2671    }
2672
2673    #[rstest]
2674    fn test_position_pnl_accumulation_across_many_fills(audusd_sim: CurrencyPair) {
2675        // Tests precision drift across 100 fills
2676        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2677        let order = OrderTestBuilder::new(OrderType::Market)
2678            .instrument_id(audusd_sim.id())
2679            .side(OrderSide::Buy)
2680            .quantity(Quantity::from(1000))
2681            .build();
2682
2683        let initial_fill = TestOrderEventStubs::filled(
2684            &order,
2685            &audusd_sim,
2686            Some(TradeId::new("1")),
2687            None,
2688            Some(Price::from("1.00000")),
2689            Some(Quantity::from(10)),
2690            None,
2691            Some(Money::from("0.01 USD")),
2692            None,
2693            None,
2694        );
2695
2696        let mut position = Position::new(&audusd_sim, initial_fill.into());
2697
2698        // Apply 99 more fills with varying prices
2699        for i in 2..=100 {
2700            let price_offset = (i as f64) * 0.00001;
2701            let fill = TestOrderEventStubs::filled(
2702                &order,
2703                &audusd_sim,
2704                Some(TradeId::new(i.to_string())),
2705                None,
2706                Some(Price::from(&format!("{:.5}", 1.0 + price_offset))),
2707                Some(Quantity::from(10)),
2708                None,
2709                Some(Money::from("0.01 USD")),
2710                None,
2711                None,
2712            );
2713            position.apply(&fill.into());
2714        }
2715
2716        // Verify we accumulated 100 fills
2717        assert_eq!(position.events.len(), 100);
2718        assert_eq!(position.quantity, Quantity::from(1000));
2719
2720        // Verify commissions accumulated (should be 100 * 0.01 = 1.0 USD)
2721        let total_commission: f64 = position.commissions().iter().map(|c| c.as_f64()).sum();
2722        assert!(
2723            (total_commission - 1.0).abs() < 1e-10,
2724            "Commission accumulation should be accurate: expected 1.0, was {total_commission}"
2725        );
2726
2727        // Verify average price is reasonable (should be around 1.0005)
2728        let avg_px = position.avg_px_open;
2729        assert!(
2730            avg_px > 1.0 && avg_px < 1.001,
2731            "Average price should be reasonable: got {avg_px}"
2732        );
2733    }
2734
2735    #[rstest]
2736    fn test_position_pnl_with_extreme_price_values(audusd_sim: CurrencyPair) {
2737        // Tests position handling with very large and very small prices
2738        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2739
2740        // Test with very small price
2741        let order_small = OrderTestBuilder::new(OrderType::Market)
2742            .instrument_id(audusd_sim.id())
2743            .side(OrderSide::Buy)
2744            .quantity(Quantity::from(100_000))
2745            .build();
2746
2747        let fill_small = TestOrderEventStubs::filled(
2748            &order_small,
2749            &audusd_sim,
2750            None,
2751            None,
2752            Some(Price::from("0.00001")),
2753            Some(Quantity::from(100_000)),
2754            None,
2755            None,
2756            None,
2757            None,
2758        );
2759
2760        let position_small = Position::new(&audusd_sim, fill_small.into());
2761        assert_eq!(position_small.avg_px_open, 0.00001);
2762
2763        // Verify notional calculation doesn't underflow
2764        let last_price_small = Price::from("0.00002");
2765        let unrealized = position_small.unrealized_pnl(last_price_small);
2766        assert!(
2767            unrealized.as_f64() > 0.0,
2768            "Unrealized PnL should be positive when price doubles"
2769        );
2770
2771        // Test with very large price
2772        let order_large = OrderTestBuilder::new(OrderType::Market)
2773            .instrument_id(audusd_sim.id())
2774            .side(OrderSide::Buy)
2775            .quantity(Quantity::from(100))
2776            .build();
2777
2778        let fill_large = TestOrderEventStubs::filled(
2779            &order_large,
2780            &audusd_sim,
2781            None,
2782            None,
2783            Some(Price::from("99999.99999")),
2784            Some(Quantity::from(100)),
2785            None,
2786            None,
2787            None,
2788            None,
2789        );
2790
2791        let position_large = Position::new(&audusd_sim, fill_large.into());
2792        assert!(
2793            (position_large.avg_px_open - 99999.99999).abs() < 1e-6,
2794            "Large price should be preserved within f64 tolerance"
2795        );
2796    }
2797
2798    #[rstest]
2799    fn test_position_pnl_roundtrip_precision(audusd_sim: CurrencyPair) {
2800        // Tests that opening and closing a position preserves precision
2801        let audusd_sim = InstrumentAny::CurrencyPair(audusd_sim);
2802        let buy_order = OrderTestBuilder::new(OrderType::Market)
2803            .instrument_id(audusd_sim.id())
2804            .side(OrderSide::Buy)
2805            .quantity(Quantity::from(100_000))
2806            .build();
2807
2808        let sell_order = OrderTestBuilder::new(OrderType::Market)
2809            .instrument_id(audusd_sim.id())
2810            .side(OrderSide::Sell)
2811            .quantity(Quantity::from(100_000))
2812            .build();
2813
2814        // Open at precise price
2815        let open_fill = TestOrderEventStubs::filled(
2816            &buy_order,
2817            &audusd_sim,
2818            Some(TradeId::new("1")),
2819            None,
2820            Some(Price::from("1.123456")),
2821            None,
2822            None,
2823            Some(Money::from("0.50 USD")),
2824            None,
2825            None,
2826        );
2827
2828        let mut position = Position::new(&audusd_sim, open_fill.into());
2829
2830        // Close at same price (no profit/loss except commission)
2831        let close_fill = TestOrderEventStubs::filled(
2832            &sell_order,
2833            &audusd_sim,
2834            Some(TradeId::new("2")),
2835            None,
2836            Some(Price::from("1.123456")),
2837            None,
2838            None,
2839            Some(Money::from("0.50 USD")),
2840            None,
2841            None,
2842        );
2843
2844        position.apply(&close_fill.into());
2845
2846        // Position should be flat
2847        assert!(position.is_closed());
2848
2849        // Realized PnL should be exactly -1.0 USD (two commissions of 0.50)
2850        let realized = position.realized_pnl.unwrap().as_f64();
2851        assert!(
2852            (realized - (-1.0)).abs() < 1e-10,
2853            "Realized PnL should be exactly -1.0 USD (commissions), was {realized}"
2854        );
2855    }
2856
2857    #[rstest]
2858    fn test_position_commission_in_base_currency_buy() {
2859        // Test that commission in base currency reduces position quantity on buy (SPOT only)
2860        let btc_usdt = currency_pair_btcusdt();
2861        let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
2862
2863        let order = OrderTestBuilder::new(OrderType::Market)
2864            .instrument_id(btc_usdt.id())
2865            .side(OrderSide::Buy)
2866            .quantity(Quantity::from("1.0"))
2867            .build();
2868
2869        // Buy 1.0 BTC with 0.001 BTC commission (stored as negative)
2870        let fill = TestOrderEventStubs::filled(
2871            &order,
2872            &btc_usdt,
2873            Some(TradeId::new("1")),
2874            None,
2875            Some(Price::from("50000.0")),
2876            Some(Quantity::from("1.0")),
2877            None,
2878            Some(Money::new(-0.001, btc_usdt.base_currency().unwrap())),
2879            None,
2880            None,
2881        );
2882
2883        let position = Position::new(&btc_usdt, fill.into());
2884
2885        // Position quantity should be 1.0 - 0.001 = 0.999 BTC
2886        assert!(
2887            (position.quantity.as_f64() - 0.999).abs() < 1e-9,
2888            "Position quantity should be 0.999 BTC (1.0 - 0.001 commission), was {}",
2889            position.quantity.as_f64()
2890        );
2891
2892        // Signed qty should also be 0.999
2893        assert!(
2894            (position.signed_qty - 0.999).abs() < 1e-9,
2895            "Signed qty should be 0.999, was {}",
2896            position.signed_qty
2897        );
2898
2899        // Verify PositionAdjusted event was created
2900        assert_eq!(
2901            position.adjustments.len(),
2902            1,
2903            "Should have 1 adjustment event"
2904        );
2905        let adjustment = &position.adjustments[0];
2906        assert_eq!(
2907            adjustment.adjustment_type,
2908            PositionAdjustmentType::Commission
2909        );
2910        assert_eq!(
2911            adjustment.quantity_change,
2912            Some(rust_decimal_macros::dec!(-0.001))
2913        );
2914        assert_eq!(adjustment.pnl_change, None);
2915    }
2916
2917    #[rstest]
2918    fn test_position_commission_in_base_currency_sell() {
2919        // Test that commission in base currency increases short position on sell
2920        let btc_usdt = currency_pair_btcusdt();
2921        let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
2922
2923        let order = OrderTestBuilder::new(OrderType::Market)
2924            .instrument_id(btc_usdt.id())
2925            .side(OrderSide::Sell)
2926            .quantity(Quantity::from("1.0"))
2927            .build();
2928
2929        // Sell 1.0 BTC with 0.001 BTC commission (stored as negative)
2930        let fill = TestOrderEventStubs::filled(
2931            &order,
2932            &btc_usdt,
2933            Some(TradeId::new("1")),
2934            None,
2935            Some(Price::from("50000.0")),
2936            Some(Quantity::from("1.0")),
2937            None,
2938            Some(Money::new(-0.001, btc_usdt.base_currency().unwrap())),
2939            None,
2940            None,
2941        );
2942
2943        let position = Position::new(&btc_usdt, fill.into());
2944
2945        // Position quantity should be 1.0 + 0.001 = 1.001 BTC
2946        // (you sold 1.0 and paid 0.001 commission, so total short exposure is 1.001)
2947        assert!(
2948            (position.quantity.as_f64() - 1.001).abs() < 1e-9,
2949            "Position quantity should be 1.001 BTC (1.0 + 0.001 commission), was {}",
2950            position.quantity.as_f64()
2951        );
2952
2953        // Signed qty should be -1.001 (short position)
2954        assert!(
2955            (position.signed_qty - (-1.001)).abs() < 1e-9,
2956            "Signed qty should be -1.001, was {}",
2957            position.signed_qty
2958        );
2959
2960        // Verify PositionAdjusted event was created
2961        assert_eq!(
2962            position.adjustments.len(),
2963            1,
2964            "Should have 1 adjustment event"
2965        );
2966        let adjustment = &position.adjustments[0];
2967        assert_eq!(
2968            adjustment.adjustment_type,
2969            PositionAdjustmentType::Commission
2970        );
2971        // For sell, commission increases the short (negative adjustment)
2972        assert_eq!(
2973            adjustment.quantity_change,
2974            Some(rust_decimal_macros::dec!(-0.001))
2975        );
2976        assert_eq!(adjustment.pnl_change, None);
2977    }
2978
2979    #[rstest]
2980    fn test_position_commission_in_quote_currency_no_adjustment() {
2981        // Test that commission in quote currency does NOT reduce position quantity
2982        let btc_usdt = currency_pair_btcusdt();
2983        let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
2984
2985        let order = OrderTestBuilder::new(OrderType::Market)
2986            .instrument_id(btc_usdt.id())
2987            .side(OrderSide::Buy)
2988            .quantity(Quantity::from("1.0"))
2989            .build();
2990
2991        // Buy 1.0 BTC with 50 USDT commission (in quote currency)
2992        let fill = TestOrderEventStubs::filled(
2993            &order,
2994            &btc_usdt,
2995            Some(TradeId::new("1")),
2996            None,
2997            Some(Price::from("50000.0")),
2998            Some(Quantity::from("1.0")),
2999            None,
3000            Some(Money::new(-50.0, Currency::USD())),
3001            None,
3002            None,
3003        );
3004
3005        let position = Position::new(&btc_usdt, fill.into());
3006
3007        // Position quantity should be exactly 1.0 BTC (no adjustment)
3008        assert!(
3009            (position.quantity.as_f64() - 1.0).abs() < 1e-9,
3010            "Position quantity should be 1.0 BTC (no adjustment for quote currency commission), was {}",
3011            position.quantity.as_f64()
3012        );
3013
3014        // Verify NO PositionAdjusted event was created (commission in quote currency)
3015        assert_eq!(
3016            position.adjustments.len(),
3017            0,
3018            "Should have no adjustment events for quote currency commission"
3019        );
3020    }
3021
3022    #[rstest]
3023    fn test_position_reset_clears_adjustments() {
3024        // Test that closing and reopening a position clears adjustment history
3025        let btc_usdt = currency_pair_btcusdt();
3026        let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
3027
3028        // Open long position with commission adjustment
3029        let buy_order = OrderTestBuilder::new(OrderType::Market)
3030            .instrument_id(btc_usdt.id())
3031            .side(OrderSide::Buy)
3032            .quantity(Quantity::from("1.0"))
3033            .build();
3034
3035        let buy_fill = TestOrderEventStubs::filled(
3036            &buy_order,
3037            &btc_usdt,
3038            Some(TradeId::new("1")),
3039            None,
3040            Some(Price::from("50000.0")),
3041            Some(Quantity::from("1.0")),
3042            None,
3043            Some(Money::new(-0.001, btc_usdt.base_currency().unwrap())),
3044            None,
3045            None,
3046        );
3047
3048        let mut position = Position::new(&btc_usdt, buy_fill.into());
3049        assert_eq!(position.adjustments.len(), 1, "Should have 1 adjustment");
3050
3051        // Close the position (sell the actual quantity, use quote currency commission to avoid complexity)
3052        let sell_order = OrderTestBuilder::new(OrderType::Market)
3053            .instrument_id(btc_usdt.id())
3054            .side(OrderSide::Sell)
3055            .quantity(Quantity::from("0.999"))
3056            .build();
3057
3058        let sell_fill = TestOrderEventStubs::filled(
3059            &sell_order,
3060            &btc_usdt,
3061            Some(TradeId::new("2")),
3062            None,
3063            Some(Price::from("51000.0")),
3064            Some(Quantity::from("0.999")),
3065            None,
3066            Some(Money::new(-50.0, Currency::USD())), // Quote currency commission - no adjustment
3067            None,
3068            None,
3069        );
3070
3071        position.apply(&sell_fill.into());
3072        assert_eq!(position.side, PositionSide::Flat);
3073        assert_eq!(
3074            position.adjustments.len(),
3075            1,
3076            "Should still have 1 adjustment (no new one from quote commission)"
3077        );
3078
3079        // Reopen the position - adjustments should be cleared
3080        let buy_order2 = OrderTestBuilder::new(OrderType::Market)
3081            .instrument_id(btc_usdt.id())
3082            .side(OrderSide::Buy)
3083            .quantity(Quantity::from("2.0"))
3084            .build();
3085
3086        let buy_fill2 = TestOrderEventStubs::filled(
3087            &buy_order2,
3088            &btc_usdt,
3089            Some(TradeId::new("3")),
3090            None,
3091            Some(Price::from("52000.0")),
3092            Some(Quantity::from("2.0")),
3093            None,
3094            Some(Money::new(-0.002, btc_usdt.base_currency().unwrap())),
3095            None,
3096            None,
3097        );
3098
3099        position.apply(&buy_fill2.into());
3100
3101        // Verify adjustments were cleared and only new adjustment exists
3102        assert_eq!(
3103            position.adjustments.len(),
3104            1,
3105            "Adjustments should be cleared on position reset, only new adjustment"
3106        );
3107        assert_eq!(
3108            position.adjustments[0].quantity_change,
3109            Some(rust_decimal_macros::dec!(-0.002)),
3110            "New adjustment should be for the new fill"
3111        );
3112        assert_eq!(position.events.len(), 1, "Events should also be reset");
3113    }
3114
3115    #[rstest]
3116    fn test_purge_events_for_order_clears_adjustments_when_flat() {
3117        // Test that purging all fills clears adjustment history
3118        let btc_usdt = currency_pair_btcusdt();
3119        let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
3120
3121        let order = OrderTestBuilder::new(OrderType::Market)
3122            .instrument_id(btc_usdt.id())
3123            .side(OrderSide::Buy)
3124            .quantity(Quantity::from("1.0"))
3125            .build();
3126
3127        let fill = TestOrderEventStubs::filled(
3128            &order,
3129            &btc_usdt,
3130            Some(TradeId::new("1")),
3131            None,
3132            Some(Price::from("50000.0")),
3133            Some(Quantity::from("1.0")),
3134            None,
3135            Some(Money::new(-0.001, btc_usdt.base_currency().unwrap())),
3136            None,
3137            None,
3138        );
3139
3140        let mut position = Position::new(&btc_usdt, fill.into());
3141        assert_eq!(position.adjustments.len(), 1, "Should have 1 adjustment");
3142        assert_eq!(position.events.len(), 1);
3143
3144        // Purge the only fill - should go to flat and clear everything
3145        position.purge_events_for_order(order.client_order_id());
3146
3147        assert_eq!(position.side, PositionSide::Flat);
3148        assert_eq!(position.events.len(), 0, "Events should be cleared");
3149        assert_eq!(
3150            position.adjustments.len(),
3151            0,
3152            "Adjustments should be cleared when position goes flat"
3153        );
3154        assert_eq!(position.quantity, Quantity::zero(btc_usdt.size_precision()));
3155    }
3156
3157    #[rstest]
3158    fn test_purge_events_for_order_clears_adjustments_on_rebuild() {
3159        // Test that rebuilding position from remaining fills clears and recreates adjustments
3160        let btc_usdt = currency_pair_btcusdt();
3161        let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
3162
3163        // First fill with adjustment
3164        let order1 = OrderTestBuilder::new(OrderType::Market)
3165            .instrument_id(btc_usdt.id())
3166            .side(OrderSide::Buy)
3167            .quantity(Quantity::from("1.0"))
3168            .client_order_id(ClientOrderId::new("O-001"))
3169            .build();
3170
3171        let fill1 = TestOrderEventStubs::filled(
3172            &order1,
3173            &btc_usdt,
3174            Some(TradeId::new("1")),
3175            None,
3176            Some(Price::from("50000.0")),
3177            Some(Quantity::from("1.0")),
3178            None,
3179            Some(Money::new(-0.001, btc_usdt.base_currency().unwrap())),
3180            None,
3181            None,
3182        );
3183
3184        let mut position = Position::new(&btc_usdt, fill1.into());
3185        assert_eq!(position.adjustments.len(), 1);
3186
3187        // Second fill with different order and adjustment
3188        let order2 = OrderTestBuilder::new(OrderType::Market)
3189            .instrument_id(btc_usdt.id())
3190            .side(OrderSide::Buy)
3191            .quantity(Quantity::from("2.0"))
3192            .client_order_id(ClientOrderId::new("O-002"))
3193            .build();
3194
3195        let fill2 = TestOrderEventStubs::filled(
3196            &order2,
3197            &btc_usdt,
3198            Some(TradeId::new("2")),
3199            None,
3200            Some(Price::from("51000.0")),
3201            Some(Quantity::from("2.0")),
3202            None,
3203            Some(Money::new(-0.002, btc_usdt.base_currency().unwrap())),
3204            None,
3205            None,
3206        );
3207
3208        position.apply(&fill2.into());
3209        assert_eq!(position.adjustments.len(), 2, "Should have 2 adjustments");
3210        assert_eq!(position.events.len(), 2);
3211
3212        // Purge first order - should rebuild from remaining fill
3213        position.purge_events_for_order(order1.client_order_id());
3214
3215        assert_eq!(position.events.len(), 1, "Should have 1 remaining event");
3216        assert_eq!(
3217            position.adjustments.len(),
3218            1,
3219            "Should have only the adjustment from remaining fill"
3220        );
3221        assert_eq!(
3222            position.adjustments[0].quantity_change,
3223            Some(rust_decimal_macros::dec!(-0.002)),
3224            "Should be the adjustment from order2"
3225        );
3226        assert!(
3227            (position.quantity.as_f64() - 1.998).abs() < 1e-9,
3228            "Quantity should be 2.0 - 0.002 commission"
3229        );
3230    }
3231
3232    #[rstest]
3233    fn test_purge_events_preserves_manual_adjustments() {
3234        // Test that manual adjustments (e.g., funding payments) are preserved when purging unrelated fills
3235        let btc_usdt = currency_pair_btcusdt();
3236        let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
3237
3238        // First fill
3239        let order1 = OrderTestBuilder::new(OrderType::Market)
3240            .instrument_id(btc_usdt.id())
3241            .side(OrderSide::Buy)
3242            .quantity(Quantity::from("1.0"))
3243            .client_order_id(ClientOrderId::new("O-001"))
3244            .build();
3245
3246        let fill1 = TestOrderEventStubs::filled(
3247            &order1,
3248            &btc_usdt,
3249            Some(TradeId::new("1")),
3250            None,
3251            Some(Price::from("50000.0")),
3252            Some(Quantity::from("1.0")),
3253            None,
3254            Some(Money::new(-0.001, btc_usdt.base_currency().unwrap())),
3255            None,
3256            None,
3257        );
3258
3259        let mut position = Position::new(&btc_usdt, fill1.into());
3260        assert_eq!(position.adjustments.len(), 1);
3261
3262        // Apply a manual funding payment adjustment (no reason field)
3263        use crate::events::PositionAdjusted;
3264        let funding_adjustment = PositionAdjusted::new(
3265            position.trader_id,
3266            position.strategy_id,
3267            position.instrument_id,
3268            position.id,
3269            position.account_id,
3270            PositionAdjustmentType::Funding,
3271            None,
3272            Some(Money::new(10.0, btc_usdt.quote_currency())),
3273            None, // No reason - this is a manual adjustment
3274            uuid4(),
3275            UnixNanos::default(),
3276            UnixNanos::default(),
3277        );
3278        position.apply_adjustment(funding_adjustment);
3279        assert_eq!(position.adjustments.len(), 2);
3280
3281        // Second fill with different order
3282        let order2 = OrderTestBuilder::new(OrderType::Market)
3283            .instrument_id(btc_usdt.id())
3284            .side(OrderSide::Buy)
3285            .quantity(Quantity::from("2.0"))
3286            .client_order_id(ClientOrderId::new("O-002"))
3287            .build();
3288
3289        let fill2 = TestOrderEventStubs::filled(
3290            &order2,
3291            &btc_usdt,
3292            Some(TradeId::new("2")),
3293            None,
3294            Some(Price::from("51000.0")),
3295            Some(Quantity::from("2.0")),
3296            None,
3297            Some(Money::new(-0.002, btc_usdt.base_currency().unwrap())),
3298            None,
3299            None,
3300        );
3301
3302        position.apply(&fill2.into());
3303        assert_eq!(
3304            position.adjustments.len(),
3305            3,
3306            "Should have 3 adjustments: 2 commissions + 1 funding"
3307        );
3308
3309        // Purge first order - manual funding adjustment should be preserved
3310        position.purge_events_for_order(order1.client_order_id());
3311
3312        assert_eq!(position.events.len(), 1, "Should have 1 remaining event");
3313        assert_eq!(
3314            position.adjustments.len(),
3315            2,
3316            "Should have funding adjustment + commission from remaining fill"
3317        );
3318
3319        // Verify funding adjustment is preserved
3320        let has_funding = position.adjustments.iter().any(|adj| {
3321            adj.adjustment_type == PositionAdjustmentType::Funding
3322                && adj.pnl_change == Some(Money::new(10.0, btc_usdt.quote_currency()))
3323        });
3324        assert!(has_funding, "Funding adjustment should be preserved");
3325
3326        // Verify realized_pnl includes the funding payment
3327        // Note: Commission is in BTC (base currency), so it doesn't directly affect USDT realized_pnl
3328        assert_eq!(
3329            position.realized_pnl,
3330            Some(Money::new(10.0, btc_usdt.quote_currency())),
3331            "Realized PnL should be the funding payment only (commission is in BTC, not USDT)"
3332        );
3333    }
3334
3335    #[rstest]
3336    fn test_position_commission_affects_buy_and_sell_qty() {
3337        // Test that commission in base currency affects both buy_qty and sell_qty tracking
3338        let btc_usdt = currency_pair_btcusdt();
3339        let btc_usdt = InstrumentAny::CurrencyPair(btc_usdt);
3340
3341        let buy_order = OrderTestBuilder::new(OrderType::Market)
3342            .instrument_id(btc_usdt.id())
3343            .side(OrderSide::Buy)
3344            .quantity(Quantity::from("1.0"))
3345            .build();
3346
3347        // Buy 1.0 BTC with 0.001 BTC commission
3348        let fill = TestOrderEventStubs::filled(
3349            &buy_order,
3350            &btc_usdt,
3351            Some(TradeId::new("1")),
3352            None,
3353            Some(Price::from("50000.0")),
3354            Some(Quantity::from("1.0")),
3355            None,
3356            Some(Money::new(-0.001, btc_usdt.base_currency().unwrap())),
3357            None,
3358            None,
3359        );
3360
3361        let position = Position::new(&btc_usdt, fill.into());
3362
3363        // buy_qty tracks order fills (1.0 BTC), adjustments tracked separately
3364        assert!(
3365            (position.buy_qty.as_f64() - 1.0).abs() < 1e-9,
3366            "buy_qty should be 1.0 (order fill amount), was {}",
3367            position.buy_qty.as_f64()
3368        );
3369
3370        // Position quantity reflects both order fill and commission adjustment
3371        assert!(
3372            (position.quantity.as_f64() - 0.999).abs() < 1e-9,
3373            "position.quantity should be 0.999 (1.0 - 0.001 commission), was {}",
3374            position.quantity.as_f64()
3375        );
3376
3377        // Adjustment event tracks the commission
3378        assert_eq!(position.adjustments.len(), 1);
3379        assert_eq!(
3380            position.adjustments[0].quantity_change,
3381            Some(rust_decimal_macros::dec!(-0.001))
3382        );
3383    }
3384
3385    #[rstest]
3386    fn test_position_perpetual_commission_no_adjustment() {
3387        // Test that perpetuals/futures do NOT adjust quantity for base currency commission
3388        let eth_perp = crypto_perpetual_ethusdt();
3389        let eth_perp = InstrumentAny::CryptoPerpetual(eth_perp);
3390
3391        let order = OrderTestBuilder::new(OrderType::Market)
3392            .instrument_id(eth_perp.id())
3393            .side(OrderSide::Buy)
3394            .quantity(Quantity::from("1.0"))
3395            .build();
3396
3397        // Buy 1.0 ETH-PERP contracts with 0.001 ETH commission
3398        let fill = TestOrderEventStubs::filled(
3399            &order,
3400            &eth_perp,
3401            Some(TradeId::new("1")),
3402            None,
3403            Some(Price::from("3000.0")),
3404            Some(Quantity::from("1.0")),
3405            None,
3406            Some(Money::new(-0.001, eth_perp.base_currency().unwrap())),
3407            None,
3408            None,
3409        );
3410
3411        let position = Position::new(&eth_perp, fill.into());
3412
3413        // Position quantity should be exactly 1.0 (NO adjustment for derivatives)
3414        assert!(
3415            (position.quantity.as_f64() - 1.0).abs() < 1e-9,
3416            "Perpetual position should be 1.0 contracts (no adjustment), was {}",
3417            position.quantity.as_f64()
3418        );
3419
3420        // Signed qty should also be 1.0
3421        assert!(
3422            (position.signed_qty - 1.0).abs() < 1e-9,
3423            "Signed qty should be 1.0, was {}",
3424            position.signed_qty
3425        );
3426    }
3427}