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