nautilus_model/
position.rs

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