nautilus_model/python/reports/
position.rs1use nautilus_core::{
17 UUID4,
18 python::{IntoPyObjectNautilusExt, serialization::from_dict_pyo3},
19};
20use pyo3::{basic::CompareOp, prelude::*, types::PyDict};
21use rust_decimal::Decimal;
22
23use crate::{
24 enums::{AvgPxReconciliation, PositionSide},
25 identifiers::{AccountId, InstrumentId, PositionId},
26 reports::position::PositionStatusReport,
27 types::Quantity,
28};
29
30#[pymethods]
31#[pyo3_stub_gen::derive::gen_stub_pymethods]
32impl PositionStatusReport {
33 #[new]
38 #[pyo3(signature = (account_id, instrument_id, position_side, quantity, ts_last, ts_init, report_id=None, venue_position_id=None, avg_px_open=None, avg_px_open_reconciliation=None, avg_px_open_precision=None))]
39 #[expect(clippy::too_many_arguments)]
40 fn py_new(
41 account_id: AccountId,
42 instrument_id: InstrumentId,
43 position_side: PositionSide,
44 quantity: Quantity,
45 ts_last: u64,
46 ts_init: u64,
47 report_id: Option<UUID4>,
48 venue_position_id: Option<PositionId>,
49 avg_px_open: Option<Decimal>,
50 avg_px_open_reconciliation: Option<AvgPxReconciliation>,
51 avg_px_open_precision: Option<u8>,
52 ) -> Self {
53 let report = Self::new(
54 account_id,
55 instrument_id,
56 position_side,
57 quantity,
58 ts_last.into(),
59 ts_init.into(),
60 report_id,
61 venue_position_id,
62 avg_px_open,
63 );
64
65 Self {
66 avg_px_open_reconciliation: avg_px_open_reconciliation.unwrap_or_default(),
67 avg_px_open_precision,
68 ..report
69 }
70 }
71
72 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
73 match op {
74 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
75 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
76 _ => py.NotImplemented(),
77 }
78 }
79
80 fn __repr__(&self) -> String {
81 self.to_string()
82 }
83
84 fn __str__(&self) -> String {
85 self.to_string()
86 }
87
88 #[getter]
89 #[pyo3(name = "account_id")]
90 const fn py_account_id(&self) -> AccountId {
91 self.account_id
92 }
93
94 #[getter]
95 #[pyo3(name = "instrument_id")]
96 const fn py_instrument_id(&self) -> InstrumentId {
97 self.instrument_id
98 }
99
100 #[getter]
101 #[pyo3(name = "strategy_id")]
102 const fn py_strategy_id(&self) -> InstrumentId {
103 self.instrument_id
104 }
105
106 #[getter]
107 #[pyo3(name = "venue_position_id")]
108 const fn py_venue_position_id(&self) -> Option<PositionId> {
109 self.venue_position_id
110 }
111
112 #[getter]
113 #[pyo3(name = "position_side")]
114 fn py_position_side(&self) -> PositionSide {
115 self.position_side
116 }
117
118 #[getter]
119 #[pyo3(name = "quantity")]
120 const fn py_quantity(&self) -> Quantity {
121 self.quantity
122 }
123
124 #[getter]
125 #[pyo3(name = "avg_px_open")]
126 const fn py_avg_px_open(&self) -> Option<Decimal> {
127 self.avg_px_open
128 }
129
130 #[getter]
131 #[pyo3(name = "avg_px_open_reconciliation")]
132 const fn py_avg_px_open_reconciliation(&self) -> AvgPxReconciliation {
133 self.avg_px_open_reconciliation
134 }
135
136 #[getter]
137 #[pyo3(name = "avg_px_open_precision")]
138 const fn py_avg_px_open_precision(&self) -> Option<u8> {
139 self.avg_px_open_precision
140 }
141
142 #[getter]
143 #[pyo3(name = "report_id")]
144 const fn py_report_id(&self) -> UUID4 {
145 self.report_id
146 }
147
148 #[getter]
149 #[pyo3(name = "ts_last")]
150 const fn py_ts_last(&self) -> u64 {
151 self.ts_last.as_u64()
152 }
153
154 #[getter]
155 #[pyo3(name = "ts_init")]
156 const fn py_ts_init(&self) -> u64 {
157 self.ts_init.as_u64()
158 }
159
160 #[getter]
162 #[pyo3(name = "is_flat")]
163 fn py_is_flat(&self) -> bool {
164 self.is_flat()
165 }
166
167 #[getter]
169 #[pyo3(name = "is_long")]
170 fn py_is_long(&self) -> bool {
171 self.is_long()
172 }
173
174 #[getter]
176 #[pyo3(name = "is_short")]
177 fn py_is_short(&self) -> bool {
178 self.is_short()
179 }
180
181 #[staticmethod]
187 #[pyo3(name = "from_dict")]
188 pub fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
189 from_dict_pyo3(py, values)
190 }
191
192 #[pyo3(name = "to_dict")]
198 pub fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
199 let dict = PyDict::new(py);
200 dict.set_item("type", stringify!(PositionStatusReport))?;
201 dict.set_item("account_id", self.account_id.to_string())?;
202 dict.set_item("instrument_id", self.instrument_id.to_string())?;
203
204 match self.venue_position_id {
205 Some(venue_position_id) => {
206 dict.set_item("venue_position_id", venue_position_id.to_string())?;
207 }
208 None => dict.set_item("venue_position_id", py.None())?,
209 }
210 dict.set_item("position_side", self.position_side.to_string())?;
211 dict.set_item("quantity", self.quantity.to_string())?;
212 dict.set_item("signed_decimal_qty", self.signed_decimal_qty.to_string())?;
213 dict.set_item("report_id", self.report_id.to_string())?;
214 dict.set_item("ts_last", self.ts_last.as_u64())?;
215 dict.set_item("ts_init", self.ts_init.as_u64())?;
216 dict.set_item(
217 "avg_px_open",
218 self.avg_px_open.map(|avg_px_open| avg_px_open.to_string()),
219 )?;
220 dict.set_item(
221 "avg_px_open_reconciliation",
222 self.avg_px_open_reconciliation.to_string(),
223 )?;
224 dict.set_item("avg_px_open_precision", self.avg_px_open_precision)?;
225 Ok(dict.into())
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use rstest::rstest;
232 use rust_decimal_macros::dec;
233
234 use super::*;
235
236 #[rstest]
237 fn test_position_status_report_python_dict_round_trip() {
238 Python::initialize();
239 Python::attach(|py| {
240 let report = PositionStatusReport::py_new(
241 AccountId::from("SIM-001"),
242 InstrumentId::from("AUDUSD.SIM"),
243 PositionSide::Short,
244 Quantity::from("100"),
245 1_000_000_000,
246 2_000_000_000,
247 None,
248 Some(PositionId::from("P-001")),
249 Some(dec!(1.23456)),
250 Some(AvgPxReconciliation::OpeningOnly),
251 Some(4),
252 );
253
254 let values = report.py_to_dict(py).unwrap();
255 let restored =
256 PositionStatusReport::py_from_dict(py, values.extract(py).unwrap()).unwrap();
257
258 assert_eq!(report.avg_px_open, Some(dec!(1.23456)));
259 assert_eq!(
260 report.avg_px_open_reconciliation,
261 AvgPxReconciliation::OpeningOnly
262 );
263 assert_eq!(report.avg_px_open_precision, Some(4));
264 assert_eq!(restored, report);
265 });
266 }
267}