nautilus_model/events/position/
changed.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use nautilus_core::{UUID4, UnixNanos};
17
18use crate::{
19    enums::{OrderSide, PositionSide},
20    events::OrderFilled,
21    identifiers::{AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TraderId},
22    position::Position,
23    types::{Currency, Money, Price, Quantity},
24};
25
26/// Represents an event where a position has changed.
27#[repr(C)]
28#[derive(Clone, PartialEq, Debug)]
29pub struct PositionChanged {
30    /// The trader ID associated with the event.
31    pub trader_id: TraderId,
32    /// The strategy ID associated with the event.
33    pub strategy_id: StrategyId,
34    /// The instrument ID associated with the event.
35    pub instrument_id: InstrumentId,
36    /// The position ID associated with the event.
37    pub position_id: PositionId,
38    /// The account ID associated with the position.
39    pub account_id: AccountId,
40    /// The client order ID for the order which opened the position.
41    pub opening_order_id: ClientOrderId,
42    /// The position entry order side.
43    pub entry: OrderSide,
44    /// The position side.
45    pub side: PositionSide,
46    /// The current signed quantity (positive for position side `LONG`, negative for `SHORT`).
47    pub signed_qty: f64,
48    /// The current open quantity.
49    pub quantity: Quantity,
50    /// The peak directional quantity reached by the position.
51    pub peak_quantity: Quantity,
52    /// The last fill quantity for the position.
53    pub last_qty: Quantity,
54    /// The last fill price for the position.
55    pub last_px: Price,
56    /// The position quote currency.
57    pub currency: Currency,
58    /// The average open price.
59    pub avg_px_open: f64,
60    /// The average close price.
61    pub avg_px_close: Option<f64>,
62    /// The realized return for the position.
63    pub realized_return: f64,
64    /// The realized PnL for the position (including commissions).
65    pub realized_pnl: Option<Money>,
66    /// The unrealized PnL for the position (including commissions).
67    pub unrealized_pnl: Money,
68    /// The unique identifier for the event.
69    pub event_id: UUID4,
70    /// UNIX timestamp (nanoseconds) when the position was opened.
71    pub ts_opened: UnixNanos,
72    /// UNIX timestamp (nanoseconds) when the event occurred.
73    pub ts_event: UnixNanos,
74    /// UNIX timestamp (nanoseconds) when the event was initialized.
75    pub ts_init: UnixNanos,
76}
77
78impl PositionChanged {
79    pub fn create(
80        position: &Position,
81        fill: &OrderFilled,
82        event_id: UUID4,
83        ts_init: UnixNanos,
84    ) -> PositionChanged {
85        PositionChanged {
86            trader_id: position.trader_id,
87            strategy_id: position.strategy_id,
88            instrument_id: position.instrument_id,
89            position_id: position.id,
90            account_id: position.account_id,
91            opening_order_id: position.opening_order_id,
92            entry: position.entry,
93            side: position.side,
94            signed_qty: position.signed_qty,
95            quantity: position.quantity,
96            peak_quantity: position.peak_qty,
97            last_qty: fill.last_qty,
98            last_px: fill.last_px,
99            currency: position.quote_currency,
100            avg_px_open: position.avg_px_open,
101            avg_px_close: position.avg_px_close,
102            realized_return: position.realized_return,
103            realized_pnl: position.realized_pnl,
104            unrealized_pnl: Money::new(0.0, position.quote_currency),
105            event_id,
106            ts_opened: position.ts_opened,
107            ts_event: fill.ts_event,
108            ts_init,
109        }
110    }
111}
112
113////////////////////////////////////////////////////////////////////////////////
114// Tests
115////////////////////////////////////////////////////////////////////////////////
116#[cfg(test)]
117mod tests {
118    use nautilus_core::UnixNanos;
119    use rstest::*;
120
121    use super::*;
122    use crate::{
123        enums::{LiquiditySide, OrderSide, OrderType, PositionSide},
124        events::OrderFilled,
125        identifiers::{
126            AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
127            VenueOrderId,
128        },
129        instruments::{InstrumentAny, stubs::audusd_sim},
130        position::Position,
131        types::{Currency, Money, Price, Quantity},
132    };
133
134    fn create_test_position_changed() -> PositionChanged {
135        PositionChanged {
136            trader_id: TraderId::from("TRADER-001"),
137            strategy_id: StrategyId::from("EMA-CROSS"),
138            instrument_id: InstrumentId::from("EURUSD.SIM"),
139            position_id: PositionId::from("P-001"),
140            account_id: AccountId::from("SIM-001"),
141            opening_order_id: ClientOrderId::from("O-19700101-000000-001-001-1"),
142            entry: OrderSide::Buy,
143            side: PositionSide::Long,
144            signed_qty: 150.0,
145            quantity: Quantity::from("150"),
146            peak_quantity: Quantity::from("150"),
147            last_qty: Quantity::from("50"),
148            last_px: Price::from("1.0550"),
149            currency: Currency::USD(),
150            avg_px_open: 1.0525,
151            avg_px_close: None,
152            realized_return: 0.0,
153            realized_pnl: None,
154            unrealized_pnl: Money::new(75.0, Currency::USD()),
155            event_id: Default::default(),
156            ts_opened: UnixNanos::from(1_000_000_000),
157            ts_event: UnixNanos::from(1_500_000_000),
158            ts_init: UnixNanos::from(2_500_000_000),
159        }
160    }
161
162    fn create_test_order_filled() -> OrderFilled {
163        OrderFilled::new(
164            TraderId::from("TRADER-001"),
165            StrategyId::from("EMA-CROSS"),
166            InstrumentId::from("AUD/USD.SIM"),
167            ClientOrderId::from("O-19700101-000000-001-001-2"),
168            VenueOrderId::from("2"),
169            AccountId::from("SIM-001"),
170            TradeId::from("T-002"),
171            OrderSide::Buy,
172            OrderType::Market,
173            Quantity::from("50"),
174            Price::from("0.8050"),
175            Currency::USD(),
176            LiquiditySide::Taker,
177            Default::default(),
178            UnixNanos::from(1_500_000_000),
179            UnixNanos::from(2_500_000_000),
180            false,
181            Some(PositionId::from("P-001")),
182            Some(Money::new(1.0, Currency::USD())),
183        )
184    }
185
186    #[rstest]
187    fn test_position_changed_new() {
188        let position_changed = create_test_position_changed();
189
190        assert_eq!(position_changed.trader_id, TraderId::from("TRADER-001"));
191        assert_eq!(position_changed.strategy_id, StrategyId::from("EMA-CROSS"));
192        assert_eq!(
193            position_changed.instrument_id,
194            InstrumentId::from("EURUSD.SIM")
195        );
196        assert_eq!(position_changed.position_id, PositionId::from("P-001"));
197        assert_eq!(position_changed.account_id, AccountId::from("SIM-001"));
198        assert_eq!(
199            position_changed.opening_order_id,
200            ClientOrderId::from("O-19700101-000000-001-001-1")
201        );
202        assert_eq!(position_changed.entry, OrderSide::Buy);
203        assert_eq!(position_changed.side, PositionSide::Long);
204        assert_eq!(position_changed.signed_qty, 150.0);
205        assert_eq!(position_changed.quantity, Quantity::from("150"));
206        assert_eq!(position_changed.peak_quantity, Quantity::from("150"));
207        assert_eq!(position_changed.last_qty, Quantity::from("50"));
208        assert_eq!(position_changed.last_px, Price::from("1.0550"));
209        assert_eq!(position_changed.currency, Currency::USD());
210        assert_eq!(position_changed.avg_px_open, 1.0525);
211        assert_eq!(position_changed.avg_px_close, None);
212        assert_eq!(position_changed.realized_return, 0.0);
213        assert_eq!(position_changed.realized_pnl, None);
214        assert_eq!(
215            position_changed.unrealized_pnl,
216            Money::new(75.0, Currency::USD())
217        );
218        assert_eq!(position_changed.ts_opened, UnixNanos::from(1_000_000_000));
219        assert_eq!(position_changed.ts_event, UnixNanos::from(1_500_000_000));
220        assert_eq!(position_changed.ts_init, UnixNanos::from(2_500_000_000));
221    }
222
223    #[rstest]
224    fn test_position_changed_create() {
225        let instrument = audusd_sim();
226        let initial_fill = OrderFilled::new(
227            TraderId::from("TRADER-001"),
228            StrategyId::from("EMA-CROSS"),
229            InstrumentId::from("AUD/USD.SIM"),
230            ClientOrderId::from("O-19700101-000000-001-001-1"),
231            VenueOrderId::from("1"),
232            AccountId::from("SIM-001"),
233            TradeId::from("T-001"),
234            OrderSide::Buy,
235            OrderType::Market,
236            Quantity::from("100"),
237            Price::from("0.8000"),
238            Currency::USD(),
239            LiquiditySide::Taker,
240            Default::default(),
241            UnixNanos::from(1_000_000_000),
242            UnixNanos::from(2_000_000_000),
243            false,
244            Some(PositionId::from("P-001")),
245            Some(Money::new(2.0, Currency::USD())),
246        );
247
248        let position = Position::new(&InstrumentAny::CurrencyPair(instrument), initial_fill);
249        let change_fill = create_test_order_filled();
250        let event_id = Default::default();
251        let ts_init = UnixNanos::from(3_000_000_000);
252
253        let position_changed = PositionChanged::create(&position, &change_fill, event_id, ts_init);
254
255        assert_eq!(position_changed.trader_id, position.trader_id);
256        assert_eq!(position_changed.strategy_id, position.strategy_id);
257        assert_eq!(position_changed.instrument_id, position.instrument_id);
258        assert_eq!(position_changed.position_id, position.id);
259        assert_eq!(position_changed.account_id, position.account_id);
260        assert_eq!(position_changed.opening_order_id, position.opening_order_id);
261        assert_eq!(position_changed.entry, position.entry);
262        assert_eq!(position_changed.side, position.side);
263        assert_eq!(position_changed.signed_qty, position.signed_qty);
264        assert_eq!(position_changed.quantity, position.quantity);
265        assert_eq!(position_changed.peak_quantity, position.peak_qty);
266        assert_eq!(position_changed.last_qty, change_fill.last_qty);
267        assert_eq!(position_changed.last_px, change_fill.last_px);
268        assert_eq!(position_changed.currency, position.quote_currency);
269        assert_eq!(position_changed.avg_px_open, position.avg_px_open);
270        assert_eq!(position_changed.avg_px_close, position.avg_px_close);
271        assert_eq!(position_changed.realized_return, position.realized_return);
272        assert_eq!(position_changed.realized_pnl, position.realized_pnl);
273        assert_eq!(
274            position_changed.unrealized_pnl,
275            Money::new(0.0, position.quote_currency)
276        );
277        assert_eq!(position_changed.event_id, event_id);
278        assert_eq!(position_changed.ts_opened, position.ts_opened);
279        assert_eq!(position_changed.ts_event, change_fill.ts_event);
280        assert_eq!(position_changed.ts_init, ts_init);
281    }
282
283    #[rstest]
284    fn test_position_changed_clone() {
285        let position_changed1 = create_test_position_changed();
286        let position_changed2 = position_changed1.clone();
287
288        assert_eq!(position_changed1, position_changed2);
289    }
290
291    #[rstest]
292    fn test_position_changed_debug() {
293        let position_changed = create_test_position_changed();
294        let debug_str = format!("{position_changed:?}");
295
296        assert!(debug_str.contains("PositionChanged"));
297        assert!(debug_str.contains("TRADER-001"));
298        assert!(debug_str.contains("EMA-CROSS"));
299        assert!(debug_str.contains("EURUSD.SIM"));
300        assert!(debug_str.contains("P-001"));
301    }
302
303    #[rstest]
304    fn test_position_changed_partial_eq() {
305        let mut position_changed1 = create_test_position_changed();
306        let mut position_changed2 = create_test_position_changed();
307        let event_id = Default::default();
308        position_changed1.event_id = event_id;
309        position_changed2.event_id = event_id;
310
311        let mut position_changed3 = create_test_position_changed();
312        position_changed3.event_id = event_id;
313        position_changed3.quantity = Quantity::from("200");
314
315        assert_eq!(position_changed1, position_changed2);
316        assert_ne!(position_changed1, position_changed3);
317    }
318
319    #[rstest]
320    fn test_position_changed_with_pnl() {
321        let mut position_changed = create_test_position_changed();
322        position_changed.realized_pnl = Some(Money::new(25.0, Currency::USD()));
323        position_changed.unrealized_pnl = Money::new(50.0, Currency::USD());
324
325        assert_eq!(
326            position_changed.realized_pnl,
327            Some(Money::new(25.0, Currency::USD()))
328        );
329        assert_eq!(
330            position_changed.unrealized_pnl,
331            Money::new(50.0, Currency::USD())
332        );
333    }
334
335    #[rstest]
336    fn test_position_changed_with_closing_prices() {
337        let mut position_changed = create_test_position_changed();
338        position_changed.avg_px_close = Some(1.0575);
339        position_changed.realized_return = 0.0048;
340
341        assert_eq!(position_changed.avg_px_close, Some(1.0575));
342        assert_eq!(position_changed.realized_return, 0.0048);
343    }
344
345    #[rstest]
346    fn test_position_changed_peak_quantity() {
347        let mut position_changed = create_test_position_changed();
348        position_changed.peak_quantity = Quantity::from("300");
349
350        assert_eq!(position_changed.peak_quantity, Quantity::from("300"));
351        assert!(position_changed.peak_quantity >= position_changed.quantity);
352    }
353
354    #[rstest]
355    fn test_position_changed_different_sides() {
356        let mut long_position = create_test_position_changed();
357        long_position.side = PositionSide::Long;
358        long_position.signed_qty = 150.0;
359
360        let mut short_position = create_test_position_changed();
361        short_position.side = PositionSide::Short;
362        short_position.signed_qty = -150.0;
363
364        assert_eq!(long_position.side, PositionSide::Long);
365        assert_eq!(long_position.signed_qty, 150.0);
366
367        assert_eq!(short_position.side, PositionSide::Short);
368        assert_eq!(short_position.signed_qty, -150.0);
369    }
370
371    #[rstest]
372    fn test_position_changed_timestamps() {
373        let position_changed = create_test_position_changed();
374
375        assert_eq!(position_changed.ts_opened, UnixNanos::from(1_000_000_000));
376        assert_eq!(position_changed.ts_event, UnixNanos::from(1_500_000_000));
377        assert_eq!(position_changed.ts_init, UnixNanos::from(2_500_000_000));
378        assert!(position_changed.ts_opened < position_changed.ts_event);
379        assert!(position_changed.ts_event < position_changed.ts_init);
380    }
381
382    #[rstest]
383    fn test_position_changed_quantities_relationship() {
384        let position_changed = create_test_position_changed();
385
386        assert!(position_changed.peak_quantity >= position_changed.quantity);
387        assert!(position_changed.last_qty <= position_changed.quantity);
388    }
389
390    #[rstest]
391    fn test_position_changed_with_zero_unrealized_pnl() {
392        let mut position_changed = create_test_position_changed();
393        position_changed.unrealized_pnl = Money::new(0.0, Currency::USD());
394
395        assert_eq!(
396            position_changed.unrealized_pnl,
397            Money::new(0.0, Currency::USD())
398        );
399    }
400}