1use 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#[repr(C)]
28#[derive(Clone, PartialEq, Debug)]
29pub struct PositionChanged {
30 pub trader_id: TraderId,
32 pub strategy_id: StrategyId,
34 pub instrument_id: InstrumentId,
36 pub position_id: PositionId,
38 pub account_id: AccountId,
40 pub opening_order_id: ClientOrderId,
42 pub entry: OrderSide,
44 pub side: PositionSide,
46 pub signed_qty: f64,
48 pub quantity: Quantity,
50 pub peak_quantity: Quantity,
52 pub last_qty: Quantity,
54 pub last_px: Price,
56 pub currency: Currency,
58 pub avg_px_open: f64,
60 pub avg_px_close: Option<f64>,
62 pub realized_return: f64,
64 pub realized_pnl: Option<Money>,
66 pub unrealized_pnl: Money,
68 pub event_id: UUID4,
70 pub ts_opened: UnixNanos,
72 pub ts_event: UnixNanos,
74 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#[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}