1use nautilus_core::{
17 UUID4,
18 nanos::{DurationNanos, UnixNanos},
19};
20
21use crate::{
22 enums::{OrderSide, PositionSide},
23 events::OrderFilled,
24 identifiers::{AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TraderId},
25 position::Position,
26 types::{Currency, Money, Price, Quantity},
27};
28
29#[repr(C)]
31#[derive(Clone, PartialEq, Debug)]
32pub struct PositionClosed {
33 pub trader_id: TraderId,
35 pub strategy_id: StrategyId,
37 pub instrument_id: InstrumentId,
39 pub position_id: PositionId,
41 pub account_id: AccountId,
43 pub opening_order_id: ClientOrderId,
45 pub closing_order_id: Option<ClientOrderId>,
47 pub entry: OrderSide,
49 pub side: PositionSide,
51 pub signed_qty: f64,
53 pub quantity: Quantity,
55 pub peak_quantity: Quantity,
57 pub last_qty: Quantity,
59 pub last_px: Price,
61 pub currency: Currency,
63 pub avg_px_open: f64,
65 pub avg_px_close: Option<f64>,
67 pub realized_return: f64,
69 pub realized_pnl: Option<Money>,
71 pub unrealized_pnl: Money,
73 pub duration: DurationNanos,
75 pub event_id: UUID4,
77 pub ts_opened: UnixNanos,
79 pub ts_closed: Option<UnixNanos>,
81 pub ts_event: UnixNanos,
83 pub ts_init: UnixNanos,
85}
86
87impl PositionClosed {
88 pub fn create(
89 position: &Position,
90 fill: &OrderFilled,
91 event_id: UUID4,
92 ts_init: UnixNanos,
93 ) -> Self {
94 Self {
95 trader_id: position.trader_id,
96 strategy_id: position.strategy_id,
97 instrument_id: position.instrument_id,
98 position_id: position.id,
99 account_id: position.account_id,
100 opening_order_id: position.opening_order_id,
101 closing_order_id: position.closing_order_id,
102 entry: position.entry,
103 side: position.side,
104 signed_qty: position.signed_qty,
105 quantity: position.quantity,
106 peak_quantity: position.peak_qty,
107 last_qty: fill.last_qty,
108 last_px: fill.last_px,
109 currency: position.quote_currency,
110 avg_px_open: position.avg_px_open,
111 avg_px_close: position.avg_px_close,
112 realized_return: position.realized_return,
113 realized_pnl: position.realized_pnl,
114 unrealized_pnl: Money::new(0.0, position.quote_currency),
115 duration: position.duration_ns,
116 event_id,
117 ts_opened: position.ts_opened,
118 ts_closed: position.ts_closed,
119 ts_event: fill.ts_event,
120 ts_init,
121 }
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use nautilus_core::UnixNanos;
128 use rstest::*;
129
130 use super::*;
131 use crate::{
132 enums::{LiquiditySide, OrderSide, OrderType, PositionSide},
133 events::OrderFilled,
134 identifiers::{
135 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
136 VenueOrderId,
137 },
138 instruments::{InstrumentAny, stubs::audusd_sim},
139 position::Position,
140 types::{Currency, Money, Price, Quantity},
141 };
142
143 fn create_test_position_closed() -> PositionClosed {
144 PositionClosed {
145 trader_id: TraderId::from("TRADER-001"),
146 strategy_id: StrategyId::from("EMA-CROSS"),
147 instrument_id: InstrumentId::from("EURUSD.SIM"),
148 position_id: PositionId::from("P-001"),
149 account_id: AccountId::from("SIM-001"),
150 opening_order_id: ClientOrderId::from("O-19700101-000000-001-001-1"),
151 closing_order_id: Some(ClientOrderId::from("O-19700101-000000-001-001-2")),
152 entry: OrderSide::Buy,
153 side: PositionSide::Flat,
154 signed_qty: 0.0,
155 quantity: Quantity::from("0"),
156 peak_quantity: Quantity::from("150"),
157 last_qty: Quantity::from("150"),
158 last_px: Price::from("1.0600"),
159 currency: Currency::USD(),
160 avg_px_open: 1.0525,
161 avg_px_close: Some(1.0600),
162 realized_return: 0.0071,
163 realized_pnl: Some(Money::new(112.50, Currency::USD())),
164 unrealized_pnl: Money::new(0.0, Currency::USD()),
165 duration: 3_600_000_000_000, event_id: Default::default(),
167 ts_opened: UnixNanos::from(1_000_000_000),
168 ts_closed: Some(UnixNanos::from(4_600_000_000)),
169 ts_event: UnixNanos::from(4_600_000_000),
170 ts_init: UnixNanos::from(5_000_000_000),
171 }
172 }
173
174 fn create_test_order_filled() -> OrderFilled {
175 OrderFilled::new(
176 TraderId::from("TRADER-001"),
177 StrategyId::from("EMA-CROSS"),
178 InstrumentId::from("EURUSD.SIM"),
179 ClientOrderId::from("O-19700101-000000-001-001-2"),
180 VenueOrderId::from("2"),
181 AccountId::from("SIM-001"),
182 TradeId::from("T-002"),
183 OrderSide::Sell,
184 OrderType::Market,
185 Quantity::from("150"),
186 Price::from("1.0600"),
187 Currency::USD(),
188 LiquiditySide::Taker,
189 Default::default(),
190 UnixNanos::from(4_600_000_000),
191 UnixNanos::from(5_000_000_000),
192 false,
193 Some(PositionId::from("P-001")),
194 Some(Money::new(2.5, Currency::USD())),
195 )
196 }
197
198 #[rstest]
199 fn test_position_closed_new() {
200 let position_closed = create_test_position_closed();
201
202 assert_eq!(position_closed.trader_id, TraderId::from("TRADER-001"));
203 assert_eq!(position_closed.strategy_id, StrategyId::from("EMA-CROSS"));
204 assert_eq!(
205 position_closed.instrument_id,
206 InstrumentId::from("EURUSD.SIM")
207 );
208 assert_eq!(position_closed.position_id, PositionId::from("P-001"));
209 assert_eq!(position_closed.account_id, AccountId::from("SIM-001"));
210 assert_eq!(
211 position_closed.opening_order_id,
212 ClientOrderId::from("O-19700101-000000-001-001-1")
213 );
214 assert_eq!(
215 position_closed.closing_order_id,
216 Some(ClientOrderId::from("O-19700101-000000-001-001-2"))
217 );
218 assert_eq!(position_closed.entry, OrderSide::Buy);
219 assert_eq!(position_closed.side, PositionSide::Flat);
220 assert_eq!(position_closed.signed_qty, 0.0);
221 assert_eq!(position_closed.quantity, Quantity::from("0"));
222 assert_eq!(position_closed.peak_quantity, Quantity::from("150"));
223 assert_eq!(position_closed.last_qty, Quantity::from("150"));
224 assert_eq!(position_closed.last_px, Price::from("1.0600"));
225 assert_eq!(position_closed.currency, Currency::USD());
226 assert_eq!(position_closed.avg_px_open, 1.0525);
227 assert_eq!(position_closed.avg_px_close, Some(1.0600));
228 assert_eq!(position_closed.realized_return, 0.0071);
229 assert_eq!(
230 position_closed.realized_pnl,
231 Some(Money::new(112.50, Currency::USD()))
232 );
233 assert_eq!(
234 position_closed.unrealized_pnl,
235 Money::new(0.0, Currency::USD())
236 );
237 assert_eq!(position_closed.duration, 3_600_000_000_000);
238 assert_eq!(position_closed.ts_opened, UnixNanos::from(1_000_000_000));
239 assert_eq!(
240 position_closed.ts_closed,
241 Some(UnixNanos::from(4_600_000_000))
242 );
243 assert_eq!(position_closed.ts_event, UnixNanos::from(4_600_000_000));
244 assert_eq!(position_closed.ts_init, UnixNanos::from(5_000_000_000));
245 }
246
247 #[rstest]
248 fn test_position_closed_create() {
249 let instrument = audusd_sim();
250 let initial_fill = OrderFilled::new(
251 TraderId::from("TRADER-001"),
252 StrategyId::from("EMA-CROSS"),
253 InstrumentId::from("AUD/USD.SIM"),
254 ClientOrderId::from("O-19700101-000000-001-001-1"),
255 VenueOrderId::from("1"),
256 AccountId::from("SIM-001"),
257 TradeId::from("T-001"),
258 OrderSide::Buy,
259 OrderType::Market,
260 Quantity::from("100"),
261 Price::from("0.8000"),
262 Currency::USD(),
263 LiquiditySide::Taker,
264 Default::default(),
265 UnixNanos::from(1_000_000_000),
266 UnixNanos::from(2_000_000_000),
267 false,
268 Some(PositionId::from("P-001")),
269 Some(Money::new(2.0, Currency::USD())),
270 );
271
272 let position = Position::new(&InstrumentAny::CurrencyPair(instrument), initial_fill);
273 let closing_fill = create_test_order_filled();
274 let event_id = Default::default();
275 let ts_init = UnixNanos::from(6_000_000_000);
276
277 let position_closed = PositionClosed::create(&position, &closing_fill, event_id, ts_init);
278
279 assert_eq!(position_closed.trader_id, position.trader_id);
280 assert_eq!(position_closed.strategy_id, position.strategy_id);
281 assert_eq!(position_closed.instrument_id, position.instrument_id);
282 assert_eq!(position_closed.position_id, position.id);
283 assert_eq!(position_closed.account_id, position.account_id);
284 assert_eq!(position_closed.opening_order_id, position.opening_order_id);
285 assert_eq!(position_closed.closing_order_id, position.closing_order_id);
286 assert_eq!(position_closed.entry, position.entry);
287 assert_eq!(position_closed.side, position.side);
288 assert_eq!(position_closed.signed_qty, position.signed_qty);
289 assert_eq!(position_closed.quantity, position.quantity);
290 assert_eq!(position_closed.peak_quantity, position.peak_qty);
291 assert_eq!(position_closed.last_qty, closing_fill.last_qty);
292 assert_eq!(position_closed.last_px, closing_fill.last_px);
293 assert_eq!(position_closed.currency, position.quote_currency);
294 assert_eq!(position_closed.avg_px_open, position.avg_px_open);
295 assert_eq!(position_closed.avg_px_close, position.avg_px_close);
296 assert_eq!(position_closed.realized_return, position.realized_return);
297 assert_eq!(position_closed.realized_pnl, position.realized_pnl);
298 assert_eq!(
299 position_closed.unrealized_pnl,
300 Money::new(0.0, position.quote_currency)
301 );
302 assert_eq!(position_closed.duration, position.duration_ns);
303 assert_eq!(position_closed.event_id, event_id);
304 assert_eq!(position_closed.ts_opened, position.ts_opened);
305 assert_eq!(position_closed.ts_closed, position.ts_closed);
306 assert_eq!(position_closed.ts_event, closing_fill.ts_event);
307 assert_eq!(position_closed.ts_init, ts_init);
308 }
309
310 #[rstest]
311 fn test_position_closed_clone() {
312 let position_closed1 = create_test_position_closed();
313 let position_closed2 = position_closed1.clone();
314
315 assert_eq!(position_closed1, position_closed2);
316 }
317
318 #[rstest]
319 fn test_position_closed_debug() {
320 let position_closed = create_test_position_closed();
321 let debug_str = format!("{position_closed:?}");
322
323 assert!(debug_str.contains("PositionClosed"));
324 assert!(debug_str.contains("TRADER-001"));
325 assert!(debug_str.contains("EMA-CROSS"));
326 assert!(debug_str.contains("EURUSD.SIM"));
327 assert!(debug_str.contains("P-001"));
328 }
329
330 #[rstest]
331 fn test_position_closed_partial_eq() {
332 let mut position_closed1 = create_test_position_closed();
333 let mut position_closed2 = create_test_position_closed();
334 let event_id = Default::default();
335 position_closed1.event_id = event_id;
336 position_closed2.event_id = event_id;
337
338 let mut position_closed3 = create_test_position_closed();
339 position_closed3.event_id = event_id;
340 position_closed3.realized_return = 0.01;
341
342 assert_eq!(position_closed1, position_closed2);
343 assert_ne!(position_closed1, position_closed3);
344 }
345
346 #[rstest]
347 fn test_position_closed_flat_position() {
348 let position_closed = create_test_position_closed();
349
350 assert_eq!(position_closed.side, PositionSide::Flat);
351 assert_eq!(position_closed.signed_qty, 0.0);
352 assert_eq!(position_closed.quantity, Quantity::from("0"));
353 assert_eq!(
354 position_closed.unrealized_pnl,
355 Money::new(0.0, Currency::USD())
356 );
357 }
358
359 #[rstest]
360 fn test_position_closed_with_closing_order_id() {
361 let position_closed = create_test_position_closed();
362
363 assert!(position_closed.closing_order_id.is_some());
364 assert_eq!(
365 position_closed.closing_order_id,
366 Some(ClientOrderId::from("O-19700101-000000-001-001-2"))
367 );
368 }
369
370 #[rstest]
371 fn test_position_closed_without_closing_order_id() {
372 let mut position_closed = create_test_position_closed();
373 position_closed.closing_order_id = None;
374
375 assert!(position_closed.closing_order_id.is_none());
376 }
377
378 #[rstest]
379 fn test_position_closed_with_realized_pnl() {
380 let position_closed = create_test_position_closed();
381
382 assert!(position_closed.realized_pnl.is_some());
383 assert_eq!(
384 position_closed.realized_pnl,
385 Some(Money::new(112.50, Currency::USD()))
386 );
387 assert!(position_closed.realized_return > 0.0);
388 }
389
390 #[rstest]
391 fn test_position_closed_loss_scenario() {
392 let mut position_closed = create_test_position_closed();
393 position_closed.avg_px_close = Some(1.0400); position_closed.realized_return = -0.0119;
395 position_closed.realized_pnl = Some(Money::new(-187.50, Currency::USD()));
396
397 assert_eq!(position_closed.avg_px_close, Some(1.0400));
398 assert!(position_closed.realized_return < 0.0);
399 assert_eq!(
400 position_closed.realized_pnl,
401 Some(Money::new(-187.50, Currency::USD()))
402 );
403 }
404
405 #[rstest]
406 fn test_position_closed_duration() {
407 let position_closed = create_test_position_closed();
408
409 assert_eq!(position_closed.duration, 3_600_000_000_000); assert!(position_closed.duration > 0);
411 }
412
413 #[rstest]
414 fn test_position_closed_timestamps() {
415 let position_closed = create_test_position_closed();
416
417 assert_eq!(position_closed.ts_opened, UnixNanos::from(1_000_000_000));
418 assert_eq!(
419 position_closed.ts_closed,
420 Some(UnixNanos::from(4_600_000_000))
421 );
422 assert_eq!(position_closed.ts_event, UnixNanos::from(4_600_000_000));
423 assert_eq!(position_closed.ts_init, UnixNanos::from(5_000_000_000));
424
425 assert!(position_closed.ts_opened < position_closed.ts_closed.unwrap());
426 assert_eq!(position_closed.ts_closed.unwrap(), position_closed.ts_event);
427 assert!(position_closed.ts_event < position_closed.ts_init);
428 }
429
430 #[rstest]
431 fn test_position_closed_peak_quantity() {
432 let position_closed = create_test_position_closed();
433
434 assert_eq!(position_closed.peak_quantity, Quantity::from("150"));
435 assert!(position_closed.peak_quantity >= position_closed.quantity);
436 assert_eq!(position_closed.last_qty, position_closed.peak_quantity);
437 }
438
439 #[rstest]
440 fn test_position_closed_different_currencies() {
441 let mut usd_position = create_test_position_closed();
442 usd_position.currency = Currency::USD();
443
444 let mut eur_position = create_test_position_closed();
445 eur_position.currency = Currency::EUR();
446 eur_position.unrealized_pnl = Money::new(0.0, Currency::EUR());
447
448 assert_eq!(usd_position.currency, Currency::USD());
449 assert_eq!(eur_position.currency, Currency::EUR());
450 assert_ne!(usd_position, eur_position);
451 }
452
453 #[rstest]
454 fn test_position_closed_entry_sides() {
455 let mut buy_entry = create_test_position_closed();
456 buy_entry.entry = OrderSide::Buy;
457
458 let mut sell_entry = create_test_position_closed();
459 sell_entry.entry = OrderSide::Sell;
460
461 assert_eq!(buy_entry.entry, OrderSide::Buy);
462 assert_eq!(sell_entry.entry, OrderSide::Sell);
463 }
464
465 #[rstest]
466 fn test_position_closed_prices() {
467 let position_closed = create_test_position_closed();
468
469 assert_eq!(position_closed.avg_px_open, 1.0525);
470 assert_eq!(position_closed.avg_px_close, Some(1.0600));
471 assert_eq!(position_closed.last_px, Price::from("1.0600"));
472
473 assert!(position_closed.avg_px_close.unwrap() > position_closed.avg_px_open);
474 }
475
476 #[rstest]
477 fn test_position_closed_without_ts_closed() {
478 let mut position_closed = create_test_position_closed();
479 position_closed.ts_closed = None;
480
481 assert!(position_closed.ts_closed.is_none());
482 }
483}