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}
55
56impl PositionStatusReport {
57 #[allow(clippy::too_many_arguments)]
59 #[must_use]
60 pub fn new(
61 account_id: AccountId,
62 instrument_id: InstrumentId,
63 position_side: PositionSideSpecified,
64 quantity: Quantity,
65 venue_position_id: Option<PositionId>,
66 ts_last: UnixNanos,
67 ts_init: UnixNanos,
68 report_id: Option<UUID4>,
69 ) -> Self {
70 let signed_decimal_qty = match position_side {
72 PositionSideSpecified::Long => quantity.as_decimal(),
73 PositionSideSpecified::Short => -quantity.as_decimal(),
74 PositionSideSpecified::Flat => Decimal::ZERO,
75 };
76
77 Self {
78 account_id,
79 instrument_id,
80 position_side,
81 quantity,
82 signed_decimal_qty,
83 report_id: report_id.unwrap_or_default(),
84 ts_last,
85 ts_init,
86 venue_position_id,
87 }
88 }
89
90 #[must_use]
92 pub const fn has_venue_position_id(&self) -> bool {
93 self.venue_position_id.is_some()
94 }
95
96 #[must_use]
98 pub fn is_flat(&self) -> bool {
99 self.position_side == PositionSideSpecified::Flat
100 }
101
102 #[must_use]
104 pub fn is_long(&self) -> bool {
105 self.position_side == PositionSideSpecified::Long
106 }
107
108 #[must_use]
110 pub fn is_short(&self) -> bool {
111 self.position_side == PositionSideSpecified::Short
112 }
113}
114
115impl Display for PositionStatusReport {
116 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117 write!(
118 f,
119 "PositionStatusReport(account={}, instrument={}, side={}, qty={}, venue_pos_id={:?}, ts_last={}, ts_init={})",
120 self.account_id,
121 self.instrument_id,
122 self.position_side,
123 self.signed_decimal_qty,
124 self.venue_position_id,
125 self.ts_last,
126 self.ts_init
127 )
128 }
129}
130
131#[cfg(test)]
135mod tests {
136 use nautilus_core::UnixNanos;
137 use rstest::*;
138 use rust_decimal::Decimal;
139
140 use super::*;
141 use crate::{
142 identifiers::{AccountId, InstrumentId, PositionId},
143 types::Quantity,
144 };
145
146 fn test_position_status_report_long() -> PositionStatusReport {
147 PositionStatusReport::new(
148 AccountId::from("SIM-001"),
149 InstrumentId::from("AUDUSD.SIM"),
150 PositionSideSpecified::Long,
151 Quantity::from("100"),
152 Some(PositionId::from("P-001")),
153 UnixNanos::from(1_000_000_000),
154 UnixNanos::from(2_000_000_000),
155 None,
156 )
157 }
158
159 fn test_position_status_report_short() -> PositionStatusReport {
160 PositionStatusReport::new(
161 AccountId::from("SIM-001"),
162 InstrumentId::from("AUDUSD.SIM"),
163 PositionSideSpecified::Short,
164 Quantity::from("50"),
165 None,
166 UnixNanos::from(1_000_000_000),
167 UnixNanos::from(2_000_000_000),
168 None,
169 )
170 }
171
172 fn test_position_status_report_flat() -> PositionStatusReport {
173 PositionStatusReport::new(
174 AccountId::from("SIM-001"),
175 InstrumentId::from("AUDUSD.SIM"),
176 PositionSideSpecified::Flat,
177 Quantity::from("0"),
178 None,
179 UnixNanos::from(1_000_000_000),
180 UnixNanos::from(2_000_000_000),
181 None,
182 )
183 }
184
185 #[rstest]
186 fn test_position_status_report_new_long() {
187 let report = test_position_status_report_long();
188
189 assert_eq!(report.account_id, AccountId::from("SIM-001"));
190 assert_eq!(report.instrument_id, InstrumentId::from("AUDUSD.SIM"));
191 assert_eq!(report.position_side, PositionSideSpecified::Long);
192 assert_eq!(report.quantity, Quantity::from("100"));
193 assert_eq!(report.signed_decimal_qty, Decimal::from(100));
194 assert_eq!(report.venue_position_id, Some(PositionId::from("P-001")));
195 assert_eq!(report.ts_last, UnixNanos::from(1_000_000_000));
196 assert_eq!(report.ts_init, UnixNanos::from(2_000_000_000));
197 }
198
199 #[rstest]
200 fn test_position_status_report_new_short() {
201 let report = test_position_status_report_short();
202
203 assert_eq!(report.position_side, PositionSideSpecified::Short);
204 assert_eq!(report.quantity, Quantity::from("50"));
205 assert_eq!(report.signed_decimal_qty, Decimal::from(-50));
206 assert_eq!(report.venue_position_id, None);
207 }
208
209 #[rstest]
210 fn test_position_status_report_new_flat() {
211 let report = test_position_status_report_flat();
212
213 assert_eq!(report.position_side, PositionSideSpecified::Flat);
214 assert_eq!(report.quantity, Quantity::from("0"));
215 assert_eq!(report.signed_decimal_qty, Decimal::ZERO);
216 }
217
218 #[rstest]
219 fn test_position_status_report_with_generated_report_id() {
220 let report = PositionStatusReport::new(
221 AccountId::from("SIM-001"),
222 InstrumentId::from("AUDUSD.SIM"),
223 PositionSideSpecified::Long,
224 Quantity::from("100"),
225 None,
226 UnixNanos::from(1_000_000_000),
227 UnixNanos::from(2_000_000_000),
228 None, );
230
231 assert_ne!(
233 report.report_id.to_string(),
234 "00000000-0000-0000-0000-000000000000"
235 );
236 }
237
238 #[rstest]
239 fn test_has_venue_position_id() {
240 let mut report = test_position_status_report_long();
241 assert!(report.has_venue_position_id());
242
243 report.venue_position_id = None;
244 assert!(!report.has_venue_position_id());
245 }
246
247 #[rstest]
248 fn test_is_flat() {
249 let long_report = test_position_status_report_long();
250 let short_report = test_position_status_report_short();
251 let flat_report = test_position_status_report_flat();
252
253 let no_position_report = PositionStatusReport::new(
254 AccountId::from("SIM-001"),
255 InstrumentId::from("AUDUSD.SIM"),
256 PositionSideSpecified::Flat,
257 Quantity::from("0"),
258 None,
259 UnixNanos::from(1_000_000_000),
260 UnixNanos::from(2_000_000_000),
261 None,
262 );
263
264 assert!(!long_report.is_flat());
265 assert!(!short_report.is_flat());
266 assert!(flat_report.is_flat());
267 assert!(no_position_report.is_flat());
268 }
269
270 #[rstest]
271 fn test_is_long() {
272 let long_report = test_position_status_report_long();
273 let short_report = test_position_status_report_short();
274 let flat_report = test_position_status_report_flat();
275
276 assert!(long_report.is_long());
277 assert!(!short_report.is_long());
278 assert!(!flat_report.is_long());
279 }
280
281 #[rstest]
282 fn test_is_short() {
283 let long_report = test_position_status_report_long();
284 let short_report = test_position_status_report_short();
285 let flat_report = test_position_status_report_flat();
286
287 assert!(!long_report.is_short());
288 assert!(short_report.is_short());
289 assert!(!flat_report.is_short());
290 }
291
292 #[rstest]
293 fn test_display() {
294 let report = test_position_status_report_long();
295 let display_str = format!("{report}");
296
297 assert!(display_str.contains("PositionStatusReport"));
298 assert!(display_str.contains("SIM-001"));
299 assert!(display_str.contains("AUDUSD.SIM"));
300 assert!(display_str.contains("LONG"));
301 assert!(display_str.contains("100"));
302 assert!(display_str.contains("P-001"));
303 }
304
305 #[rstest]
306 fn test_clone_and_equality() {
307 let report1 = test_position_status_report_long();
308 let report2 = report1.clone();
309
310 assert_eq!(report1, report2);
311 }
312
313 #[rstest]
314 fn test_serialization_roundtrip() {
315 let original = test_position_status_report_long();
316
317 let json = serde_json::to_string(&original).unwrap();
319 let deserialized: PositionStatusReport = serde_json::from_str(&json).unwrap();
320 assert_eq!(original, deserialized);
321 }
322
323 #[rstest]
324 fn test_signed_decimal_qty_calculation() {
325 let long_100 = PositionStatusReport::new(
327 AccountId::from("SIM-001"),
328 InstrumentId::from("AUDUSD.SIM"),
329 PositionSideSpecified::Long,
330 Quantity::from("100.5"),
331 None,
332 UnixNanos::from(1_000_000_000),
333 UnixNanos::from(2_000_000_000),
334 None,
335 );
336
337 let short_200 = PositionStatusReport::new(
338 AccountId::from("SIM-001"),
339 InstrumentId::from("AUDUSD.SIM"),
340 PositionSideSpecified::Short,
341 Quantity::from("200.75"),
342 None,
343 UnixNanos::from(1_000_000_000),
344 UnixNanos::from(2_000_000_000),
345 None,
346 );
347
348 assert_eq!(
349 long_100.signed_decimal_qty,
350 Decimal::from_f64_retain(100.5).unwrap()
351 );
352 assert_eq!(
353 short_200.signed_decimal_qty,
354 Decimal::from_f64_retain(-200.75).unwrap()
355 );
356 }
357
358 #[rstest]
359 fn test_different_position_sides_not_equal() {
360 let long_report = test_position_status_report_long();
361 let short_report = PositionStatusReport::new(
362 AccountId::from("SIM-001"),
363 InstrumentId::from("AUDUSD.SIM"),
364 PositionSideSpecified::Short,
365 Quantity::from("100"), Some(PositionId::from("P-001")),
367 UnixNanos::from(1_000_000_000),
368 UnixNanos::from(2_000_000_000),
369 None,
370 );
371
372 assert_ne!(long_report, short_report);
373 assert_ne!(
374 long_report.signed_decimal_qty,
375 short_report.signed_decimal_qty
376 );
377 }
378}