1use std::fmt::{Debug, Display};
17
18use nautilus_core::{UUID4, UnixNanos};
19use rust_decimal::Decimal;
20use serde::{Deserialize, Serialize};
21
22use crate::{
23 enums::PositionSideSpecified,
24 identifiers::{AccountId, InstrumentId, PositionId},
25 types::Quantity,
26};
27
28#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
30#[serde(tag = "type")]
31#[cfg_attr(
32 feature = "python",
33 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model")
34)]
35pub struct PositionStatusReport {
36 pub account_id: AccountId,
38 pub instrument_id: InstrumentId,
40 pub position_side: PositionSideSpecified,
42 pub quantity: Quantity,
44 pub signed_decimal_qty: Decimal,
46 pub report_id: UUID4,
48 pub ts_last: UnixNanos,
50 pub ts_init: UnixNanos,
52 pub venue_position_id: Option<PositionId>,
54 pub avg_px_open: Option<Decimal>,
56}
57
58impl PositionStatusReport {
59 #[allow(clippy::too_many_arguments)]
61 #[must_use]
62 pub fn new(
63 account_id: AccountId,
64 instrument_id: InstrumentId,
65 position_side: PositionSideSpecified,
66 quantity: Quantity,
67 ts_last: UnixNanos,
68 ts_init: UnixNanos,
69 report_id: Option<UUID4>,
70 venue_position_id: Option<PositionId>,
71 avg_px_open: Option<Decimal>,
72 ) -> Self {
73 let signed_decimal_qty = match position_side {
75 PositionSideSpecified::Long => quantity.as_decimal(),
76 PositionSideSpecified::Short => -quantity.as_decimal(),
77 PositionSideSpecified::Flat => Decimal::ZERO,
78 };
79
80 Self {
81 account_id,
82 instrument_id,
83 position_side,
84 quantity,
85 signed_decimal_qty,
86 report_id: report_id.unwrap_or_default(),
87 ts_last,
88 ts_init,
89 venue_position_id,
90 avg_px_open,
91 }
92 }
93
94 #[must_use]
96 pub const fn has_venue_position_id(&self) -> bool {
97 self.venue_position_id.is_some()
98 }
99
100 #[must_use]
102 pub fn is_flat(&self) -> bool {
103 self.position_side == PositionSideSpecified::Flat
104 }
105
106 #[must_use]
108 pub fn is_long(&self) -> bool {
109 self.position_side == PositionSideSpecified::Long
110 }
111
112 #[must_use]
114 pub fn is_short(&self) -> bool {
115 self.position_side == PositionSideSpecified::Short
116 }
117}
118
119impl Display for PositionStatusReport {
120 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 write!(
122 f,
123 "PositionStatusReport(account={}, instrument={}, side={}, qty={}, venue_pos_id={:?}, avg_px_open={:?}, ts_last={}, ts_init={})",
124 self.account_id,
125 self.instrument_id,
126 self.position_side,
127 self.signed_decimal_qty,
128 self.venue_position_id,
129 self.avg_px_open,
130 self.ts_last,
131 self.ts_init
132 )
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use std::str::FromStr;
139
140 use nautilus_core::UnixNanos;
141 use rstest::*;
142 use rust_decimal::Decimal;
143 use rust_decimal_macros::dec;
144
145 use super::*;
146 use crate::{
147 identifiers::{AccountId, InstrumentId, PositionId},
148 types::Quantity,
149 };
150
151 fn test_position_status_report_long() -> PositionStatusReport {
152 PositionStatusReport::new(
153 AccountId::from("SIM-001"),
154 InstrumentId::from("AUDUSD.SIM"),
155 PositionSideSpecified::Long,
156 Quantity::from("100"),
157 UnixNanos::from(1_000_000_000),
158 UnixNanos::from(2_000_000_000),
159 None, Some(PositionId::from("P-001")), None, )
163 }
164
165 fn test_position_status_report_short() -> PositionStatusReport {
166 PositionStatusReport::new(
167 AccountId::from("SIM-001"),
168 InstrumentId::from("AUDUSD.SIM"),
169 PositionSideSpecified::Short,
170 Quantity::from("50"),
171 UnixNanos::from(1_000_000_000),
172 UnixNanos::from(2_000_000_000),
173 None,
174 None,
175 None,
176 )
177 }
178
179 fn test_position_status_report_flat() -> PositionStatusReport {
180 PositionStatusReport::new(
181 AccountId::from("SIM-001"),
182 InstrumentId::from("AUDUSD.SIM"),
183 PositionSideSpecified::Flat,
184 Quantity::from("0"),
185 UnixNanos::from(1_000_000_000),
186 UnixNanos::from(2_000_000_000),
187 None,
188 None,
189 None,
190 )
191 }
192
193 #[rstest]
194 fn test_position_status_report_new_long() {
195 let report = test_position_status_report_long();
196
197 assert_eq!(report.account_id, AccountId::from("SIM-001"));
198 assert_eq!(report.instrument_id, InstrumentId::from("AUDUSD.SIM"));
199 assert_eq!(report.position_side, PositionSideSpecified::Long);
200 assert_eq!(report.quantity, Quantity::from("100"));
201 assert_eq!(report.signed_decimal_qty, dec!(100));
202 assert_eq!(report.venue_position_id, Some(PositionId::from("P-001")));
203 assert_eq!(report.ts_last, UnixNanos::from(1_000_000_000));
204 assert_eq!(report.ts_init, UnixNanos::from(2_000_000_000));
205 }
206
207 #[rstest]
208 fn test_position_status_report_new_short() {
209 let report = test_position_status_report_short();
210
211 assert_eq!(report.position_side, PositionSideSpecified::Short);
212 assert_eq!(report.quantity, Quantity::from("50"));
213 assert_eq!(report.signed_decimal_qty, dec!(-50));
214 assert_eq!(report.venue_position_id, None);
215 }
216
217 #[rstest]
218 fn test_position_status_report_new_flat() {
219 let report = test_position_status_report_flat();
220
221 assert_eq!(report.position_side, PositionSideSpecified::Flat);
222 assert_eq!(report.quantity, Quantity::from("0"));
223 assert_eq!(report.signed_decimal_qty, Decimal::ZERO);
224 }
225
226 #[rstest]
227 fn test_position_status_report_with_generated_report_id() {
228 let report = PositionStatusReport::new(
229 AccountId::from("SIM-001"),
230 InstrumentId::from("AUDUSD.SIM"),
231 PositionSideSpecified::Long,
232 Quantity::from("100"),
233 UnixNanos::from(1_000_000_000),
234 UnixNanos::from(2_000_000_000),
235 None, None,
237 None,
238 );
239
240 assert_ne!(
242 report.report_id.to_string(),
243 "00000000-0000-0000-0000-000000000000"
244 );
245 }
246
247 #[rstest]
248 fn test_has_venue_position_id() {
249 let mut report = test_position_status_report_long();
250 assert!(report.has_venue_position_id());
251
252 report.venue_position_id = None;
253 assert!(!report.has_venue_position_id());
254 }
255
256 #[rstest]
257 fn test_is_flat() {
258 let long_report = test_position_status_report_long();
259 let short_report = test_position_status_report_short();
260 let flat_report = test_position_status_report_flat();
261
262 let no_position_report = PositionStatusReport::new(
263 AccountId::from("SIM-001"),
264 InstrumentId::from("AUDUSD.SIM"),
265 PositionSideSpecified::Flat,
266 Quantity::from("0"),
267 UnixNanos::from(1_000_000_000),
268 UnixNanos::from(2_000_000_000),
269 None,
270 None,
271 None,
272 );
273
274 assert!(!long_report.is_flat());
275 assert!(!short_report.is_flat());
276 assert!(flat_report.is_flat());
277 assert!(no_position_report.is_flat());
278 }
279
280 #[rstest]
281 fn test_is_long() {
282 let long_report = test_position_status_report_long();
283 let short_report = test_position_status_report_short();
284 let flat_report = test_position_status_report_flat();
285
286 assert!(long_report.is_long());
287 assert!(!short_report.is_long());
288 assert!(!flat_report.is_long());
289 }
290
291 #[rstest]
292 fn test_is_short() {
293 let long_report = test_position_status_report_long();
294 let short_report = test_position_status_report_short();
295 let flat_report = test_position_status_report_flat();
296
297 assert!(!long_report.is_short());
298 assert!(short_report.is_short());
299 assert!(!flat_report.is_short());
300 }
301
302 #[rstest]
303 fn test_display() {
304 let report = test_position_status_report_long();
305 let display_str = format!("{report}");
306
307 assert!(display_str.contains("PositionStatusReport"));
308 assert!(display_str.contains("SIM-001"));
309 assert!(display_str.contains("AUDUSD.SIM"));
310 assert!(display_str.contains("LONG"));
311 assert!(display_str.contains("100"));
312 assert!(display_str.contains("P-001"));
313 assert!(display_str.contains("avg_px_open=None"));
314 }
315
316 #[rstest]
317 fn test_clone_and_equality() {
318 let report1 = test_position_status_report_long();
319 let report2 = report1.clone();
320
321 assert_eq!(report1, report2);
322 }
323
324 #[rstest]
325 fn test_serialization_roundtrip() {
326 let original = test_position_status_report_long();
327
328 let json = serde_json::to_string(&original).unwrap();
330 let deserialized: PositionStatusReport = serde_json::from_str(&json).unwrap();
331 assert_eq!(original, deserialized);
332 }
333
334 #[rstest]
335 fn test_signed_decimal_qty_calculation() {
336 let long_100 = PositionStatusReport::new(
338 AccountId::from("SIM-001"),
339 InstrumentId::from("AUDUSD.SIM"),
340 PositionSideSpecified::Long,
341 Quantity::from("100.5"),
342 UnixNanos::from(1_000_000_000),
343 UnixNanos::from(2_000_000_000),
344 None,
345 None,
346 None,
347 );
348
349 let short_200 = PositionStatusReport::new(
350 AccountId::from("SIM-001"),
351 InstrumentId::from("AUDUSD.SIM"),
352 PositionSideSpecified::Short,
353 Quantity::from("200.75"),
354 UnixNanos::from(1_000_000_000),
355 UnixNanos::from(2_000_000_000),
356 None,
357 None,
358 None,
359 );
360
361 assert_eq!(
362 long_100.signed_decimal_qty,
363 Decimal::from_f64_retain(100.5).unwrap()
364 );
365 assert_eq!(
366 short_200.signed_decimal_qty,
367 Decimal::from_f64_retain(-200.75).unwrap()
368 );
369 }
370
371 #[rstest]
372 fn test_different_position_sides_not_equal() {
373 let long_report = test_position_status_report_long();
374 let short_report = PositionStatusReport::new(
375 AccountId::from("SIM-001"),
376 InstrumentId::from("AUDUSD.SIM"),
377 PositionSideSpecified::Short,
378 Quantity::from("100"), UnixNanos::from(1_000_000_000),
380 UnixNanos::from(2_000_000_000),
381 None, Some(PositionId::from("P-001")), None, );
385
386 assert_ne!(long_report, short_report);
387 assert_ne!(
388 long_report.signed_decimal_qty,
389 short_report.signed_decimal_qty
390 );
391 }
392
393 #[rstest]
394 fn test_with_avg_px_open() {
395 let report = PositionStatusReport::new(
396 AccountId::from("SIM-001"),
397 InstrumentId::from("AUDUSD.SIM"),
398 PositionSideSpecified::Long,
399 Quantity::from("100"),
400 UnixNanos::from(1_000_000_000),
401 UnixNanos::from(2_000_000_000),
402 None,
403 Some(PositionId::from("P-001")),
404 Some(Decimal::from_str("1.23456").unwrap()),
405 );
406
407 assert_eq!(
408 report.avg_px_open,
409 Some(rust_decimal::Decimal::from_str("1.23456").unwrap())
410 );
411 assert!(format!("{report}").contains("avg_px_open=Some(1.23456)"));
412 }
413
414 #[rstest]
415 fn test_avg_px_open_none_default() {
416 let report = PositionStatusReport::new(
417 AccountId::from("SIM-001"),
418 InstrumentId::from("AUDUSD.SIM"),
419 PositionSideSpecified::Long,
420 Quantity::from("100"),
421 UnixNanos::from(1_000_000_000),
422 UnixNanos::from(2_000_000_000),
423 None,
424 None,
425 None, );
427
428 assert_eq!(report.avg_px_open, None);
429 }
430
431 #[rstest]
432 fn test_avg_px_open_with_different_sides() {
433 let long_with_price = PositionStatusReport::new(
434 AccountId::from("SIM-001"),
435 InstrumentId::from("AUDUSD.SIM"),
436 PositionSideSpecified::Long,
437 Quantity::from("100"),
438 UnixNanos::from(1_000_000_000),
439 UnixNanos::from(2_000_000_000),
440 None,
441 None,
442 Some(Decimal::from_str("1.50000").unwrap()),
443 );
444
445 let short_with_price = PositionStatusReport::new(
446 AccountId::from("SIM-001"),
447 InstrumentId::from("AUDUSD.SIM"),
448 PositionSideSpecified::Short,
449 Quantity::from("100"),
450 UnixNanos::from(1_000_000_000),
451 UnixNanos::from(2_000_000_000),
452 None,
453 None,
454 Some(Decimal::from_str("1.60000").unwrap()),
455 );
456
457 assert_eq!(
458 long_with_price.avg_px_open,
459 Some(rust_decimal::Decimal::from_str("1.50000").unwrap())
460 );
461 assert_eq!(
462 short_with_price.avg_px_open,
463 Some(rust_decimal::Decimal::from_str("1.60000").unwrap())
464 );
465 }
466
467 #[rstest]
468 fn test_avg_px_open_serialization() {
469 let report = PositionStatusReport::new(
470 AccountId::from("SIM-001"),
471 InstrumentId::from("AUDUSD.SIM"),
472 PositionSideSpecified::Long,
473 Quantity::from("100"),
474 UnixNanos::from(1_000_000_000),
475 UnixNanos::from(2_000_000_000),
476 None,
477 None,
478 Some(Decimal::from_str("1.99999").unwrap()),
479 );
480
481 let json = serde_json::to_string(&report).unwrap();
482 let deserialized: PositionStatusReport = serde_json::from_str(&json).unwrap();
483
484 assert_eq!(report.avg_px_open, deserialized.avg_px_open);
485 }
486}