nautilus_model/events/position/
opened.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, Price, Quantity},
24};
25
26/// Represents an event where a position has been opened.
27#[repr(C)]
28#[derive(Clone, PartialEq, Debug)]
29pub struct PositionOpened {
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 last fill quantity for the position.
51    pub last_qty: Quantity,
52    /// The last fill price for the position.
53    pub last_px: Price,
54    /// The position quote currency.
55    pub currency: Currency,
56    /// The average open price.
57    pub avg_px_open: f64,
58    /// The unique identifier for the event.
59    pub event_id: UUID4,
60    /// UNIX timestamp (nanoseconds) when the event occurred.
61    pub ts_event: UnixNanos,
62    /// UNIX timestamp (nanoseconds) when the event was initialized.
63    pub ts_init: UnixNanos,
64}
65
66impl PositionOpened {
67    pub fn create(
68        position: &Position,
69        fill: &OrderFilled,
70        event_id: UUID4,
71        ts_init: UnixNanos,
72    ) -> PositionOpened {
73        PositionOpened {
74            trader_id: position.trader_id,
75            strategy_id: position.strategy_id,
76            instrument_id: position.instrument_id,
77            position_id: position.id,
78            account_id: position.account_id,
79            opening_order_id: position.opening_order_id,
80            entry: position.entry,
81            side: position.side,
82            signed_qty: position.signed_qty,
83            quantity: position.quantity,
84            last_qty: fill.last_qty,
85            last_px: fill.last_px,
86            currency: position.quote_currency,
87            avg_px_open: position.avg_px_open,
88            event_id,
89            ts_event: fill.ts_event,
90            ts_init,
91        }
92    }
93}
94
95////////////////////////////////////////////////////////////////////////////////
96// Tests
97////////////////////////////////////////////////////////////////////////////////
98#[cfg(test)]
99mod tests {
100    use nautilus_core::UnixNanos;
101    use rstest::*;
102
103    use super::*;
104    use crate::{
105        enums::{LiquiditySide, OrderSide, OrderType, PositionSide},
106        events::OrderFilled,
107        identifiers::{
108            AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
109            VenueOrderId,
110        },
111        instruments::{InstrumentAny, stubs::audusd_sim},
112        position::Position,
113        types::{Currency, Money, Price, Quantity},
114    };
115
116    fn create_test_position_opened() -> PositionOpened {
117        PositionOpened {
118            trader_id: TraderId::from("TRADER-001"),
119            strategy_id: StrategyId::from("EMA-CROSS"),
120            instrument_id: InstrumentId::from("EURUSD.SIM"),
121            position_id: PositionId::from("P-001"),
122            account_id: AccountId::from("SIM-001"),
123            opening_order_id: ClientOrderId::from("O-19700101-000000-001-001-1"),
124            entry: OrderSide::Buy,
125            side: PositionSide::Long,
126            signed_qty: 100.0,
127            quantity: Quantity::from("100"),
128            last_qty: Quantity::from("100"),
129            last_px: Price::from("1.0500"),
130            currency: Currency::USD(),
131            avg_px_open: 1.0500,
132            event_id: Default::default(),
133            ts_event: UnixNanos::from(1_000_000_000),
134            ts_init: UnixNanos::from(2_000_000_000),
135        }
136    }
137
138    fn create_test_order_filled() -> OrderFilled {
139        OrderFilled::new(
140            TraderId::from("TRADER-001"),
141            StrategyId::from("EMA-CROSS"),
142            InstrumentId::from("AUD/USD.SIM"),
143            ClientOrderId::from("O-19700101-000000-001-001-1"),
144            VenueOrderId::from("1"),
145            AccountId::from("SIM-001"),
146            TradeId::from("T-001"),
147            OrderSide::Buy,
148            OrderType::Market,
149            Quantity::from("100"),
150            Price::from("0.8000"),
151            Currency::USD(),
152            LiquiditySide::Taker,
153            Default::default(),
154            UnixNanos::from(1_000_000_000),
155            UnixNanos::from(2_000_000_000),
156            false,
157            Some(PositionId::from("P-001")),
158            Some(Money::new(2.0, Currency::USD())),
159        )
160    }
161
162    #[rstest]
163    fn test_position_opened_new() {
164        let position_opened = create_test_position_opened();
165
166        assert_eq!(position_opened.trader_id, TraderId::from("TRADER-001"));
167        assert_eq!(position_opened.strategy_id, StrategyId::from("EMA-CROSS"));
168        assert_eq!(
169            position_opened.instrument_id,
170            InstrumentId::from("EURUSD.SIM")
171        );
172        assert_eq!(position_opened.position_id, PositionId::from("P-001"));
173        assert_eq!(position_opened.account_id, AccountId::from("SIM-001"));
174        assert_eq!(
175            position_opened.opening_order_id,
176            ClientOrderId::from("O-19700101-000000-001-001-1")
177        );
178        assert_eq!(position_opened.entry, OrderSide::Buy);
179        assert_eq!(position_opened.side, PositionSide::Long);
180        assert_eq!(position_opened.signed_qty, 100.0);
181        assert_eq!(position_opened.quantity, Quantity::from("100"));
182        assert_eq!(position_opened.last_qty, Quantity::from("100"));
183        assert_eq!(position_opened.last_px, Price::from("1.0500"));
184        assert_eq!(position_opened.currency, Currency::USD());
185        assert_eq!(position_opened.avg_px_open, 1.0500);
186        assert_eq!(position_opened.ts_event, UnixNanos::from(1_000_000_000));
187        assert_eq!(position_opened.ts_init, UnixNanos::from(2_000_000_000));
188    }
189
190    #[rstest]
191    fn test_position_opened_create() {
192        let instrument = audusd_sim();
193        let fill = create_test_order_filled();
194        let position = Position::new(&InstrumentAny::CurrencyPair(instrument), fill);
195        let event_id = Default::default();
196        let ts_init = UnixNanos::from(3_000_000_000);
197
198        let position_opened = PositionOpened::create(&position, &fill, event_id, ts_init);
199
200        assert_eq!(position_opened.trader_id, position.trader_id);
201        assert_eq!(position_opened.strategy_id, position.strategy_id);
202        assert_eq!(position_opened.instrument_id, position.instrument_id);
203        assert_eq!(position_opened.position_id, position.id);
204        assert_eq!(position_opened.account_id, position.account_id);
205        assert_eq!(position_opened.opening_order_id, position.opening_order_id);
206        assert_eq!(position_opened.entry, position.entry);
207        assert_eq!(position_opened.side, position.side);
208        assert_eq!(position_opened.signed_qty, position.signed_qty);
209        assert_eq!(position_opened.quantity, position.quantity);
210        assert_eq!(position_opened.last_qty, fill.last_qty);
211        assert_eq!(position_opened.last_px, fill.last_px);
212        assert_eq!(position_opened.currency, position.quote_currency);
213        assert_eq!(position_opened.avg_px_open, position.avg_px_open);
214        assert_eq!(position_opened.event_id, event_id);
215        assert_eq!(position_opened.ts_event, fill.ts_event);
216        assert_eq!(position_opened.ts_init, ts_init);
217    }
218
219    #[rstest]
220    fn test_position_opened_clone() {
221        let position_opened1 = create_test_position_opened();
222        let position_opened2 = position_opened1.clone();
223
224        assert_eq!(position_opened1, position_opened2);
225    }
226
227    #[rstest]
228    fn test_position_opened_debug() {
229        let position_opened = create_test_position_opened();
230        let debug_str = format!("{position_opened:?}");
231
232        assert!(debug_str.contains("PositionOpened"));
233        assert!(debug_str.contains("TRADER-001"));
234        assert!(debug_str.contains("EMA-CROSS"));
235        assert!(debug_str.contains("EURUSD.SIM"));
236        assert!(debug_str.contains("P-001"));
237    }
238
239    #[rstest]
240    fn test_position_opened_partial_eq() {
241        let mut position_opened1 = create_test_position_opened();
242        let mut position_opened2 = create_test_position_opened();
243        let event_id = Default::default();
244        position_opened1.event_id = event_id;
245        position_opened2.event_id = event_id;
246
247        let mut position_opened3 = create_test_position_opened();
248        position_opened3.event_id = event_id;
249        position_opened3.quantity = Quantity::from("200");
250
251        assert_eq!(position_opened1, position_opened2);
252        assert_ne!(position_opened1, position_opened3);
253    }
254
255    #[rstest]
256    fn test_position_opened_with_different_sides() {
257        let mut long_position = create_test_position_opened();
258        long_position.side = PositionSide::Long;
259        long_position.entry = OrderSide::Buy;
260        long_position.signed_qty = 100.0;
261
262        let mut short_position = create_test_position_opened();
263        short_position.side = PositionSide::Short;
264        short_position.entry = OrderSide::Sell;
265        short_position.signed_qty = -100.0;
266
267        assert_eq!(long_position.side, PositionSide::Long);
268        assert_eq!(long_position.entry, OrderSide::Buy);
269        assert_eq!(long_position.signed_qty, 100.0);
270
271        assert_eq!(short_position.side, PositionSide::Short);
272        assert_eq!(short_position.entry, OrderSide::Sell);
273        assert_eq!(short_position.signed_qty, -100.0);
274    }
275
276    #[rstest]
277    fn test_position_opened_different_currencies() {
278        let mut usd_position = create_test_position_opened();
279        usd_position.currency = Currency::USD();
280
281        let mut eur_position = create_test_position_opened();
282        eur_position.currency = Currency::EUR();
283
284        assert_eq!(usd_position.currency, Currency::USD());
285        assert_eq!(eur_position.currency, Currency::EUR());
286        assert_ne!(usd_position, eur_position);
287    }
288
289    #[rstest]
290    fn test_position_opened_timestamps() {
291        let position_opened = create_test_position_opened();
292
293        assert_eq!(position_opened.ts_event, UnixNanos::from(1_000_000_000));
294        assert_eq!(position_opened.ts_init, UnixNanos::from(2_000_000_000));
295        assert!(position_opened.ts_event < position_opened.ts_init);
296    }
297
298    #[rstest]
299    fn test_position_opened_quantities() {
300        let mut position_opened = create_test_position_opened();
301        position_opened.quantity = Quantity::from("500");
302        position_opened.last_qty = Quantity::from("250");
303
304        assert_eq!(position_opened.quantity, Quantity::from("500"));
305        assert_eq!(position_opened.last_qty, Quantity::from("250"));
306    }
307
308    #[rstest]
309    fn test_position_opened_prices() {
310        let mut position_opened = create_test_position_opened();
311        position_opened.last_px = Price::from("1.2345");
312        position_opened.avg_px_open = 1.2345;
313
314        assert_eq!(position_opened.last_px, Price::from("1.2345"));
315        assert_eq!(position_opened.avg_px_open, 1.2345);
316    }
317}