1use nautilus_core::UnixNanos;
17use serde::{Deserialize, Serialize};
18
19use crate::{
20 enums::{OrderSide, PositionSide},
21 identifiers::{AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TraderId},
22 position::Position,
23 types::{Currency, Money, Quantity},
24};
25
26#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
28#[cfg_attr(
29 feature = "python",
30 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
31)]
32pub struct PositionSnapshot {
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_qty: Quantity,
57 pub quote_currency: Currency,
59 pub base_currency: Option<Currency>,
61 pub settlement_currency: Currency,
63 pub avg_px_open: f64,
65 pub avg_px_close: Option<f64>,
67 pub realized_return: Option<f64>,
69 pub realized_pnl: Option<Money>,
71 pub unrealized_pnl: Option<Money>,
73 pub commissions: Vec<Money>,
75 pub duration_ns: Option<u64>,
77 pub ts_opened: UnixNanos,
79 pub ts_closed: Option<UnixNanos>,
81 pub ts_init: UnixNanos,
83 pub ts_last: UnixNanos,
85}
86
87impl PositionSnapshot {
88 pub fn from(position: &Position, unrealized_pnl: Option<Money>) -> Self {
89 Self {
90 trader_id: position.trader_id,
91 strategy_id: position.strategy_id,
92 instrument_id: position.instrument_id,
93 position_id: position.id,
94 account_id: position.account_id,
95 opening_order_id: position.opening_order_id,
96 closing_order_id: position.closing_order_id,
97 entry: position.entry,
98 side: position.side,
99 signed_qty: position.signed_qty,
100 quantity: position.quantity,
101 peak_qty: position.peak_qty,
102 quote_currency: position.quote_currency,
103 base_currency: position.base_currency,
104 settlement_currency: position.settlement_currency,
105 avg_px_open: position.avg_px_open,
106 avg_px_close: position.avg_px_close,
107 realized_return: Some(position.realized_return), realized_pnl: position.realized_pnl,
109 unrealized_pnl,
110 commissions: position.commissions.values().copied().collect(), duration_ns: Some(position.duration_ns), ts_opened: position.ts_opened,
113 ts_closed: position.ts_closed,
114 ts_init: position.ts_init,
115 ts_last: position.ts_last,
116 }
117 }
118}
119
120#[cfg(test)]
121mod tests {
122 use nautilus_core::UnixNanos;
123 use rstest::*;
124
125 use super::*;
126 use crate::{
127 enums::{LiquiditySide, OrderSide, OrderType, PositionSide},
128 events::OrderFilled,
129 identifiers::{
130 AccountId, ClientOrderId, InstrumentId, PositionId, StrategyId, TradeId, TraderId,
131 VenueOrderId,
132 },
133 instruments::{InstrumentAny, stubs::audusd_sim},
134 position::Position,
135 types::{Currency, Money, Price, Quantity},
136 };
137
138 fn create_test_position_snapshot() -> PositionSnapshot {
139 PositionSnapshot {
140 trader_id: TraderId::from("TRADER-001"),
141 strategy_id: StrategyId::from("EMA-CROSS"),
142 instrument_id: InstrumentId::from("EURUSD.SIM"),
143 position_id: PositionId::from("P-001"),
144 account_id: AccountId::from("SIM-001"),
145 opening_order_id: ClientOrderId::from("O-19700101-000000-001-001-1"),
146 closing_order_id: Some(ClientOrderId::from("O-19700101-000000-001-001-2")),
147 entry: OrderSide::Buy,
148 side: PositionSide::Long,
149 signed_qty: 100.0,
150 quantity: Quantity::from("100"),
151 peak_qty: Quantity::from("100"),
152 quote_currency: Currency::USD(),
153 base_currency: Some(Currency::EUR()),
154 settlement_currency: Currency::USD(),
155 avg_px_open: 1.0500,
156 avg_px_close: Some(1.0600),
157 realized_return: Some(0.0095),
158 realized_pnl: Some(Money::new(100.0, Currency::USD())),
159 unrealized_pnl: Some(Money::new(50.0, Currency::USD())),
160 commissions: vec![Money::new(2.0, Currency::USD())],
161 duration_ns: Some(3_600_000_000_000), ts_opened: UnixNanos::from(1_000_000_000),
163 ts_closed: Some(UnixNanos::from(4_600_000_000)),
164 ts_init: UnixNanos::from(2_000_000_000),
165 ts_last: UnixNanos::from(4_600_000_000),
166 }
167 }
168
169 fn create_test_order_filled() -> OrderFilled {
170 OrderFilled::new(
171 TraderId::from("TRADER-001"),
172 StrategyId::from("EMA-CROSS"),
173 InstrumentId::from("AUD/USD.SIM"),
174 ClientOrderId::from("O-19700101-000000-001-001-1"),
175 VenueOrderId::from("1"),
176 AccountId::from("SIM-001"),
177 TradeId::from("T-001"),
178 OrderSide::Buy,
179 OrderType::Market,
180 Quantity::from("100"),
181 Price::from("0.8000"),
182 Currency::USD(),
183 LiquiditySide::Taker,
184 Default::default(),
185 UnixNanos::from(1_000_000_000),
186 UnixNanos::from(2_000_000_000),
187 false,
188 Some(PositionId::from("P-001")),
189 Some(Money::new(2.0, Currency::USD())),
190 )
191 }
192
193 #[rstest]
194 fn test_position_snapshot_new() {
195 let snapshot = create_test_position_snapshot();
196
197 assert_eq!(snapshot.trader_id, TraderId::from("TRADER-001"));
198 assert_eq!(snapshot.strategy_id, StrategyId::from("EMA-CROSS"));
199 assert_eq!(snapshot.instrument_id, InstrumentId::from("EURUSD.SIM"));
200 assert_eq!(snapshot.position_id, PositionId::from("P-001"));
201 assert_eq!(snapshot.account_id, AccountId::from("SIM-001"));
202 assert_eq!(
203 snapshot.opening_order_id,
204 ClientOrderId::from("O-19700101-000000-001-001-1")
205 );
206 assert_eq!(
207 snapshot.closing_order_id,
208 Some(ClientOrderId::from("O-19700101-000000-001-001-2"))
209 );
210 assert_eq!(snapshot.entry, OrderSide::Buy);
211 assert_eq!(snapshot.side, PositionSide::Long);
212 assert_eq!(snapshot.signed_qty, 100.0);
213 assert_eq!(snapshot.quantity, Quantity::from("100"));
214 assert_eq!(snapshot.peak_qty, Quantity::from("100"));
215 assert_eq!(snapshot.quote_currency, Currency::USD());
216 assert_eq!(snapshot.base_currency, Some(Currency::EUR()));
217 assert_eq!(snapshot.settlement_currency, Currency::USD());
218 assert_eq!(snapshot.avg_px_open, 1.0500);
219 assert_eq!(snapshot.avg_px_close, Some(1.0600));
220 assert_eq!(snapshot.realized_return, Some(0.0095));
221 assert_eq!(
222 snapshot.realized_pnl,
223 Some(Money::new(100.0, Currency::USD()))
224 );
225 assert_eq!(
226 snapshot.unrealized_pnl,
227 Some(Money::new(50.0, Currency::USD()))
228 );
229 assert_eq!(snapshot.commissions, vec![Money::new(2.0, Currency::USD())]);
230 assert_eq!(snapshot.duration_ns, Some(3_600_000_000_000));
231 assert_eq!(snapshot.ts_opened, UnixNanos::from(1_000_000_000));
232 assert_eq!(snapshot.ts_closed, Some(UnixNanos::from(4_600_000_000)));
233 assert_eq!(snapshot.ts_init, UnixNanos::from(2_000_000_000));
234 assert_eq!(snapshot.ts_last, UnixNanos::from(4_600_000_000));
235 }
236
237 #[rstest]
238 fn test_position_snapshot_from() {
239 let instrument = audusd_sim();
240 let fill = create_test_order_filled();
241 let position = Position::new(&InstrumentAny::CurrencyPair(instrument), fill);
242 let unrealized_pnl = Some(Money::new(75.0, Currency::USD()));
243
244 let snapshot = PositionSnapshot::from(&position, unrealized_pnl);
245
246 assert_eq!(snapshot.trader_id, position.trader_id);
247 assert_eq!(snapshot.strategy_id, position.strategy_id);
248 assert_eq!(snapshot.instrument_id, position.instrument_id);
249 assert_eq!(snapshot.position_id, position.id);
250 assert_eq!(snapshot.account_id, position.account_id);
251 assert_eq!(snapshot.opening_order_id, position.opening_order_id);
252 assert_eq!(snapshot.closing_order_id, position.closing_order_id);
253 assert_eq!(snapshot.entry, position.entry);
254 assert_eq!(snapshot.side, position.side);
255 assert_eq!(snapshot.signed_qty, position.signed_qty);
256 assert_eq!(snapshot.quantity, position.quantity);
257 assert_eq!(snapshot.peak_qty, position.peak_qty);
258 assert_eq!(snapshot.quote_currency, position.quote_currency);
259 assert_eq!(snapshot.base_currency, position.base_currency);
260 assert_eq!(snapshot.settlement_currency, position.settlement_currency);
261 assert_eq!(snapshot.avg_px_open, position.avg_px_open);
262 assert_eq!(snapshot.avg_px_close, position.avg_px_close);
263 assert_eq!(snapshot.realized_return, Some(position.realized_return));
264 assert_eq!(snapshot.realized_pnl, position.realized_pnl);
265 assert_eq!(snapshot.unrealized_pnl, unrealized_pnl);
266 assert_eq!(snapshot.duration_ns, Some(position.duration_ns));
267 assert_eq!(snapshot.ts_opened, position.ts_opened);
268 assert_eq!(snapshot.ts_closed, position.ts_closed);
269 assert_eq!(snapshot.ts_init, position.ts_init);
270 assert_eq!(snapshot.ts_last, position.ts_last);
271 }
272
273 #[rstest]
274 fn test_position_snapshot_from_with_no_unrealized_pnl() {
275 let instrument = audusd_sim();
276 let fill = create_test_order_filled();
277 let position = Position::new(&InstrumentAny::CurrencyPair(instrument), fill);
278
279 let snapshot = PositionSnapshot::from(&position, None);
280
281 assert_eq!(snapshot.unrealized_pnl, None);
282 }
283
284 #[rstest]
285 fn test_position_snapshot_clone() {
286 let snapshot1 = create_test_position_snapshot();
287 let snapshot2 = snapshot1.clone();
288
289 assert_eq!(snapshot1, snapshot2);
290 }
291
292 #[rstest]
293 fn test_position_snapshot_debug() {
294 let snapshot = create_test_position_snapshot();
295 let debug_str = format!("{snapshot:?}");
296
297 assert!(debug_str.contains("PositionSnapshot"));
298 assert!(debug_str.contains("TRADER-001"));
299 assert!(debug_str.contains("EMA-CROSS"));
300 assert!(debug_str.contains("EURUSD.SIM"));
301 assert!(debug_str.contains("P-001"));
302 }
303
304 #[rstest]
305 fn test_position_snapshot_partial_eq() {
306 let snapshot1 = create_test_position_snapshot();
307 let snapshot2 = create_test_position_snapshot();
308 let mut snapshot3 = create_test_position_snapshot();
309 snapshot3.quantity = Quantity::from("200");
310
311 assert_eq!(snapshot1, snapshot2);
312 assert_ne!(snapshot1, snapshot3);
313 }
314
315 #[rstest]
316 fn test_position_snapshot_with_commissions() {
317 let mut snapshot = create_test_position_snapshot();
318 snapshot.commissions = vec![
319 Money::new(1.0, Currency::USD()),
320 Money::new(0.5, Currency::USD()),
321 ];
322
323 assert_eq!(snapshot.commissions.len(), 2);
324 assert_eq!(snapshot.commissions[0], Money::new(1.0, Currency::USD()));
325 assert_eq!(snapshot.commissions[1], Money::new(0.5, Currency::USD()));
326 }
327
328 #[rstest]
329 fn test_position_snapshot_with_empty_commissions() {
330 let mut snapshot = create_test_position_snapshot();
331 snapshot.commissions = vec![];
332
333 assert!(snapshot.commissions.is_empty());
334 }
335
336 #[rstest]
337 fn test_position_snapshot_with_different_currencies() {
338 let mut snapshot = create_test_position_snapshot();
339 snapshot.quote_currency = Currency::EUR();
340 snapshot.base_currency = Some(Currency::USD());
341 snapshot.settlement_currency = Currency::EUR();
342
343 assert_eq!(snapshot.quote_currency, Currency::EUR());
344 assert_eq!(snapshot.base_currency, Some(Currency::USD()));
345 assert_eq!(snapshot.settlement_currency, Currency::EUR());
346 }
347
348 #[rstest]
349 fn test_position_snapshot_without_base_currency() {
350 let mut snapshot = create_test_position_snapshot();
351 snapshot.base_currency = None;
352
353 assert!(snapshot.base_currency.is_none());
354 }
355
356 #[rstest]
357 fn test_position_snapshot_different_position_sides() {
358 let mut long_snapshot = create_test_position_snapshot();
359 long_snapshot.side = PositionSide::Long;
360 long_snapshot.signed_qty = 100.0;
361
362 let mut short_snapshot = create_test_position_snapshot();
363 short_snapshot.side = PositionSide::Short;
364 short_snapshot.signed_qty = -100.0;
365
366 let mut flat_snapshot = create_test_position_snapshot();
367 flat_snapshot.side = PositionSide::Flat;
368 flat_snapshot.signed_qty = 0.0;
369
370 assert_eq!(long_snapshot.side, PositionSide::Long);
371 assert_eq!(short_snapshot.side, PositionSide::Short);
372 assert_eq!(flat_snapshot.side, PositionSide::Flat);
373 }
374
375 #[rstest]
376 fn test_position_snapshot_with_pnl_values() {
377 let mut snapshot = create_test_position_snapshot();
378 snapshot.realized_pnl = Some(Money::new(150.0, Currency::USD()));
379 snapshot.unrealized_pnl = Some(Money::new(-25.0, Currency::USD()));
380
381 assert_eq!(
382 snapshot.realized_pnl,
383 Some(Money::new(150.0, Currency::USD()))
384 );
385 assert_eq!(
386 snapshot.unrealized_pnl,
387 Some(Money::new(-25.0, Currency::USD()))
388 );
389 }
390
391 #[rstest]
392 fn test_position_snapshot_without_pnl_values() {
393 let mut snapshot = create_test_position_snapshot();
394 snapshot.realized_pnl = None;
395 snapshot.unrealized_pnl = None;
396
397 assert!(snapshot.realized_pnl.is_none());
398 assert!(snapshot.unrealized_pnl.is_none());
399 }
400
401 #[rstest]
402 fn test_position_snapshot_with_closing_data() {
403 let snapshot = create_test_position_snapshot();
404
405 assert!(snapshot.closing_order_id.is_some());
406 assert!(snapshot.avg_px_close.is_some());
407 assert!(snapshot.ts_closed.is_some());
408 assert!(snapshot.duration_ns.is_some());
409 }
410
411 #[rstest]
412 fn test_position_snapshot_without_closing_data() {
413 let mut snapshot = create_test_position_snapshot();
414 snapshot.closing_order_id = None;
415 snapshot.avg_px_close = None;
416 snapshot.ts_closed = None;
417
418 assert!(snapshot.closing_order_id.is_none());
419 assert!(snapshot.avg_px_close.is_none());
420 assert!(snapshot.ts_closed.is_none());
421 }
422
423 #[rstest]
424 fn test_position_snapshot_timestamps() {
425 let snapshot = create_test_position_snapshot();
426
427 assert_eq!(snapshot.ts_opened, UnixNanos::from(1_000_000_000));
428 assert_eq!(snapshot.ts_init, UnixNanos::from(2_000_000_000));
429 assert_eq!(snapshot.ts_last, UnixNanos::from(4_600_000_000));
430 assert_eq!(snapshot.ts_closed, Some(UnixNanos::from(4_600_000_000)));
431
432 assert!(snapshot.ts_opened < snapshot.ts_init);
433 assert!(snapshot.ts_init < snapshot.ts_last);
434 }
435
436 #[rstest]
437 fn test_position_snapshot_quantities() {
438 let snapshot = create_test_position_snapshot();
439
440 assert_eq!(snapshot.quantity, Quantity::from("100"));
441 assert_eq!(snapshot.peak_qty, Quantity::from("100"));
442 assert!(snapshot.peak_qty >= snapshot.quantity);
443 }
444
445 #[rstest]
446 fn test_position_snapshot_serialization() {
447 let original = create_test_position_snapshot();
448
449 let json = serde_json::to_string(&original).unwrap();
451 let deserialized: PositionSnapshot = serde_json::from_str(&json).unwrap();
452
453 assert_eq!(original, deserialized);
454 }
455
456 #[rstest]
457 fn test_position_snapshot_with_duration() {
458 let mut snapshot = create_test_position_snapshot();
459 snapshot.duration_ns = Some(7_200_000_000_000); assert_eq!(snapshot.duration_ns, Some(7_200_000_000_000));
462 }
463
464 #[rstest]
465 fn test_position_snapshot_without_duration() {
466 let mut snapshot = create_test_position_snapshot();
467 snapshot.duration_ns = None;
468
469 assert!(snapshot.duration_ns.is_none());
470 }
471}