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