Skip to main content

nautilus_model/
position.rs

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