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", from_py_object)
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!(long_100.signed_decimal_qty, dec!(100.5));
362 assert_eq!(short_200.signed_decimal_qty, dec!(-200.75));
363 }
364
365 #[rstest]
366 fn test_different_position_sides_not_equal() {
367 let long_report = test_position_status_report_long();
368 let short_report = PositionStatusReport::new(
369 AccountId::from("SIM-001"),
370 InstrumentId::from("AUDUSD.SIM"),
371 PositionSideSpecified::Short,
372 Quantity::from("100"), UnixNanos::from(1_000_000_000),
374 UnixNanos::from(2_000_000_000),
375 None, Some(PositionId::from("P-001")), None, );
379
380 assert_ne!(long_report, short_report);
381 assert_ne!(
382 long_report.signed_decimal_qty,
383 short_report.signed_decimal_qty
384 );
385 }
386
387 #[rstest]
388 fn test_with_avg_px_open() {
389 let report = PositionStatusReport::new(
390 AccountId::from("SIM-001"),
391 InstrumentId::from("AUDUSD.SIM"),
392 PositionSideSpecified::Long,
393 Quantity::from("100"),
394 UnixNanos::from(1_000_000_000),
395 UnixNanos::from(2_000_000_000),
396 None,
397 Some(PositionId::from("P-001")),
398 Some(Decimal::from_str("1.23456").unwrap()),
399 );
400
401 assert_eq!(
402 report.avg_px_open,
403 Some(rust_decimal::Decimal::from_str("1.23456").unwrap())
404 );
405 assert!(format!("{report}").contains("avg_px_open=Some(1.23456)"));
406 }
407
408 #[rstest]
409 fn test_avg_px_open_none_default() {
410 let report = PositionStatusReport::new(
411 AccountId::from("SIM-001"),
412 InstrumentId::from("AUDUSD.SIM"),
413 PositionSideSpecified::Long,
414 Quantity::from("100"),
415 UnixNanos::from(1_000_000_000),
416 UnixNanos::from(2_000_000_000),
417 None,
418 None,
419 None, );
421
422 assert_eq!(report.avg_px_open, None);
423 }
424
425 #[rstest]
426 fn test_avg_px_open_with_different_sides() {
427 let long_with_price = PositionStatusReport::new(
428 AccountId::from("SIM-001"),
429 InstrumentId::from("AUDUSD.SIM"),
430 PositionSideSpecified::Long,
431 Quantity::from("100"),
432 UnixNanos::from(1_000_000_000),
433 UnixNanos::from(2_000_000_000),
434 None,
435 None,
436 Some(Decimal::from_str("1.50000").unwrap()),
437 );
438
439 let short_with_price = PositionStatusReport::new(
440 AccountId::from("SIM-001"),
441 InstrumentId::from("AUDUSD.SIM"),
442 PositionSideSpecified::Short,
443 Quantity::from("100"),
444 UnixNanos::from(1_000_000_000),
445 UnixNanos::from(2_000_000_000),
446 None,
447 None,
448 Some(Decimal::from_str("1.60000").unwrap()),
449 );
450
451 assert_eq!(
452 long_with_price.avg_px_open,
453 Some(rust_decimal::Decimal::from_str("1.50000").unwrap())
454 );
455 assert_eq!(
456 short_with_price.avg_px_open,
457 Some(rust_decimal::Decimal::from_str("1.60000").unwrap())
458 );
459 }
460
461 #[rstest]
462 fn test_avg_px_open_serialization() {
463 let report = PositionStatusReport::new(
464 AccountId::from("SIM-001"),
465 InstrumentId::from("AUDUSD.SIM"),
466 PositionSideSpecified::Long,
467 Quantity::from("100"),
468 UnixNanos::from(1_000_000_000),
469 UnixNanos::from(2_000_000_000),
470 None,
471 None,
472 Some(Decimal::from_str("1.99999").unwrap()),
473 );
474
475 let json = serde_json::to_string(&report).unwrap();
476 let deserialized: PositionStatusReport = serde_json::from_str(&json).unwrap();
477
478 assert_eq!(report.avg_px_open, deserialized.avg_px_open);
479 }
480}