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    ) -> Self {
85        Self {
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#[cfg(test)]
114mod tests {
115    use nautilus_core::UnixNanos;
116    use rstest::*;
117
118    use super::*;
119    use crate::{
120        enums::{LiquiditySide, OrderSide, OrderType, PositionSide},
121        events::OrderFilled,
122        identifiers::{
123            AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
124            VenueOrderId,
125        },
126        instruments::{InstrumentAny, stubs::audusd_sim},
127        position::Position,
128        types::{Currency, Money, Price, Quantity},
129    };
130
131    fn create_test_position_changed() -> PositionChanged {
132        PositionChanged {
133            trader_id: TraderId::from("TRADER-001"),
134            strategy_id: StrategyId::from("EMA-CROSS"),
135            instrument_id: InstrumentId::from("EURUSD.SIM"),
136            position_id: PositionId::from("P-001"),
137            account_id: AccountId::from("SIM-001"),
138            opening_order_id: ClientOrderId::from("O-19700101-000000-001-001-1"),
139            entry: OrderSide::Buy,
140            side: PositionSide::Long,
141            signed_qty: 150.0,
142            quantity: Quantity::from("150"),
143            peak_quantity: Quantity::from("150"),
144            last_qty: Quantity::from("50"),
145            last_px: Price::from("1.0550"),
146            currency: Currency::USD(),
147            avg_px_open: 1.0525,
148            avg_px_close: None,
149            realized_return: 0.0,
150            realized_pnl: None,
151            unrealized_pnl: Money::new(75.0, Currency::USD()),
152            event_id: Default::default(),
153            ts_opened: UnixNanos::from(1_000_000_000),
154            ts_event: UnixNanos::from(1_500_000_000),
155            ts_init: UnixNanos::from(2_500_000_000),
156        }
157    }
158
159    fn create_test_order_filled() -> OrderFilled {
160        OrderFilled::new(
161            TraderId::from("TRADER-001"),
162            StrategyId::from("EMA-CROSS"),
163            InstrumentId::from("AUD/USD.SIM"),
164            ClientOrderId::from("O-19700101-000000-001-001-2"),
165            VenueOrderId::from("2"),
166            AccountId::from("SIM-001"),
167            TradeId::from("T-002"),
168            OrderSide::Buy,
169            OrderType::Market,
170            Quantity::from("50"),
171            Price::from("0.8050"),
172            Currency::USD(),
173            LiquiditySide::Taker,
174            Default::default(),
175            UnixNanos::from(1_500_000_000),
176            UnixNanos::from(2_500_000_000),
177            false,
178            Some(PositionId::from("P-001")),
179            Some(Money::new(1.0, Currency::USD())),
180        )
181    }
182
183    #[rstest]
184    fn test_position_changed_new() {
185        let position_changed = create_test_position_changed();
186
187        assert_eq!(position_changed.trader_id, TraderId::from("TRADER-001"));
188        assert_eq!(position_changed.strategy_id, StrategyId::from("EMA-CROSS"));
189        assert_eq!(
190            position_changed.instrument_id,
191            InstrumentId::from("EURUSD.SIM")
192        );
193        assert_eq!(position_changed.position_id, PositionId::from("P-001"));
194        assert_eq!(position_changed.account_id, AccountId::from("SIM-001"));
195        assert_eq!(
196            position_changed.opening_order_id,
197            ClientOrderId::from("O-19700101-000000-001-001-1")
198        );
199        assert_eq!(position_changed.entry, OrderSide::Buy);
200        assert_eq!(position_changed.side, PositionSide::Long);
201        assert_eq!(position_changed.signed_qty, 150.0);
202        assert_eq!(position_changed.quantity, Quantity::from("150"));
203        assert_eq!(position_changed.peak_quantity, Quantity::from("150"));
204        assert_eq!(position_changed.last_qty, Quantity::from("50"));
205        assert_eq!(position_changed.last_px, Price::from("1.0550"));
206        assert_eq!(position_changed.currency, Currency::USD());
207        assert_eq!(position_changed.avg_px_open, 1.0525);
208        assert_eq!(position_changed.avg_px_close, None);
209        assert_eq!(position_changed.realized_return, 0.0);
210        assert_eq!(position_changed.realized_pnl, None);
211        assert_eq!(
212            position_changed.unrealized_pnl,
213            Money::new(75.0, Currency::USD())
214        );
215        assert_eq!(position_changed.ts_opened, UnixNanos::from(1_000_000_000));
216        assert_eq!(position_changed.ts_event, UnixNanos::from(1_500_000_000));
217        assert_eq!(position_changed.ts_init, UnixNanos::from(2_500_000_000));
218    }
219
220    #[rstest]
221    fn test_position_changed_create() {
222        let instrument = audusd_sim();
223        let initial_fill = OrderFilled::new(
224            TraderId::from("TRADER-001"),
225            StrategyId::from("EMA-CROSS"),
226            InstrumentId::from("AUD/USD.SIM"),
227            ClientOrderId::from("O-19700101-000000-001-001-1"),
228            VenueOrderId::from("1"),
229            AccountId::from("SIM-001"),
230            TradeId::from("T-001"),
231            OrderSide::Buy,
232            OrderType::Market,
233            Quantity::from("100"),
234            Price::from("0.8000"),
235            Currency::USD(),
236            LiquiditySide::Taker,
237            Default::default(),
238            UnixNanos::from(1_000_000_000),
239            UnixNanos::from(2_000_000_000),
240            false,
241            Some(PositionId::from("P-001")),
242            Some(Money::new(2.0, Currency::USD())),
243        );
244
245        let position = Position::new(&InstrumentAny::CurrencyPair(instrument), initial_fill);
246        let change_fill = create_test_order_filled();
247        let event_id = Default::default();
248        let ts_init = UnixNanos::from(3_000_000_000);
249
250        let position_changed = PositionChanged::create(&position, &change_fill, event_id, ts_init);
251
252        assert_eq!(position_changed.trader_id, position.trader_id);
253        assert_eq!(position_changed.strategy_id, position.strategy_id);
254        assert_eq!(position_changed.instrument_id, position.instrument_id);
255        assert_eq!(position_changed.position_id, position.id);
256        assert_eq!(position_changed.account_id, position.account_id);
257        assert_eq!(position_changed.opening_order_id, position.opening_order_id);
258        assert_eq!(position_changed.entry, position.entry);
259        assert_eq!(position_changed.side, position.side);
260        assert_eq!(position_changed.signed_qty, position.signed_qty);
261        assert_eq!(position_changed.quantity, position.quantity);
262        assert_eq!(position_changed.peak_quantity, position.peak_qty);
263        assert_eq!(position_changed.last_qty, change_fill.last_qty);
264        assert_eq!(position_changed.last_px, change_fill.last_px);
265        assert_eq!(position_changed.currency, position.quote_currency);
266        assert_eq!(position_changed.avg_px_open, position.avg_px_open);
267        assert_eq!(position_changed.avg_px_close, position.avg_px_close);
268        assert_eq!(position_changed.realized_return, position.realized_return);
269        assert_eq!(position_changed.realized_pnl, position.realized_pnl);
270        assert_eq!(
271            position_changed.unrealized_pnl,
272            Money::new(0.0, position.quote_currency)
273        );
274        assert_eq!(position_changed.event_id, event_id);
275        assert_eq!(position_changed.ts_opened, position.ts_opened);
276        assert_eq!(position_changed.ts_event, change_fill.ts_event);
277        assert_eq!(position_changed.ts_init, ts_init);
278    }
279
280    #[rstest]
281    fn test_position_changed_clone() {
282        let position_changed1 = create_test_position_changed();
283        let position_changed2 = position_changed1.clone();
284
285        assert_eq!(position_changed1, position_changed2);
286    }
287
288    #[rstest]
289    fn test_position_changed_debug() {
290        let position_changed = create_test_position_changed();
291        let debug_str = format!("{position_changed:?}");
292
293        assert!(debug_str.contains("PositionChanged"));
294        assert!(debug_str.contains("TRADER-001"));
295        assert!(debug_str.contains("EMA-CROSS"));
296        assert!(debug_str.contains("EURUSD.SIM"));
297        assert!(debug_str.contains("P-001"));
298    }
299
300    #[rstest]
301    fn test_position_changed_partial_eq() {
302        let mut position_changed1 = create_test_position_changed();
303        let mut position_changed2 = create_test_position_changed();
304        let event_id = Default::default();
305        position_changed1.event_id = event_id;
306        position_changed2.event_id = event_id;
307
308        let mut position_changed3 = create_test_position_changed();
309        position_changed3.event_id = event_id;
310        position_changed3.quantity = Quantity::from("200");
311
312        assert_eq!(position_changed1, position_changed2);
313        assert_ne!(position_changed1, position_changed3);
314    }
315
316    #[rstest]
317    fn test_position_changed_with_pnl() {
318        let mut position_changed = create_test_position_changed();
319        position_changed.realized_pnl = Some(Money::new(25.0, Currency::USD()));
320        position_changed.unrealized_pnl = Money::new(50.0, Currency::USD());
321
322        assert_eq!(
323            position_changed.realized_pnl,
324            Some(Money::new(25.0, Currency::USD()))
325        );
326        assert_eq!(
327            position_changed.unrealized_pnl,
328            Money::new(50.0, Currency::USD())
329        );
330    }
331
332    #[rstest]
333    fn test_position_changed_with_closing_prices() {
334        let mut position_changed = create_test_position_changed();
335        position_changed.avg_px_close = Some(1.0575);
336        position_changed.realized_return = 0.0048;
337
338        assert_eq!(position_changed.avg_px_close, Some(1.0575));
339        assert_eq!(position_changed.realized_return, 0.0048);
340    }
341
342    #[rstest]
343    fn test_position_changed_peak_quantity() {
344        let mut position_changed = create_test_position_changed();
345        position_changed.peak_quantity = Quantity::from("300");
346
347        assert_eq!(position_changed.peak_quantity, Quantity::from("300"));
348        assert!(position_changed.peak_quantity >= position_changed.quantity);
349    }
350
351    #[rstest]
352    fn test_position_changed_different_sides() {
353        let mut long_position = create_test_position_changed();
354        long_position.side = PositionSide::Long;
355        long_position.signed_qty = 150.0;
356
357        let mut short_position = create_test_position_changed();
358        short_position.side = PositionSide::Short;
359        short_position.signed_qty = -150.0;
360
361        assert_eq!(long_position.side, PositionSide::Long);
362        assert_eq!(long_position.signed_qty, 150.0);
363
364        assert_eq!(short_position.side, PositionSide::Short);
365        assert_eq!(short_position.signed_qty, -150.0);
366    }
367
368    #[rstest]
369    fn test_position_changed_timestamps() {
370        let position_changed = create_test_position_changed();
371
372        assert_eq!(position_changed.ts_opened, UnixNanos::from(1_000_000_000));
373        assert_eq!(position_changed.ts_event, UnixNanos::from(1_500_000_000));
374        assert_eq!(position_changed.ts_init, UnixNanos::from(2_500_000_000));
375        assert!(position_changed.ts_opened < position_changed.ts_event);
376        assert!(position_changed.ts_event < position_changed.ts_init);
377    }
378
379    #[rstest]
380    fn test_position_changed_quantities_relationship() {
381        let position_changed = create_test_position_changed();
382
383        assert!(position_changed.peak_quantity >= position_changed.quantity);
384        assert!(position_changed.last_qty <= position_changed.quantity);
385    }
386
387    #[rstest]
388    fn test_position_changed_with_zero_unrealized_pnl() {
389        let mut position_changed = create_test_position_changed();
390        position_changed.unrealized_pnl = Money::new(0.0, Currency::USD());
391
392        assert_eq!(
393            position_changed.unrealized_pnl,
394            Money::new(0.0, Currency::USD())
395        );
396    }
397}