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, liquidation_price=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 liquidation_price: Option<Decimal>,
53 ) -> Self {
54 let report = Self::new(
55 account_id,
56 instrument_id,
57 position_side,
58 quantity,
59 ts_last.into(),
60 ts_init.into(),
61 report_id,
62 venue_position_id,
63 avg_px_open,
64 );
65
66 Self {
67 avg_px_open_reconciliation: avg_px_open_reconciliation.unwrap_or_default(),
68 avg_px_open_precision,
69 liquidation_price,
70 ..report
71 }
72 }
73
74 fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
75 match op {
76 CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
77 CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
78 _ => py.NotImplemented(),
79 }
80 }
81
82 fn __repr__(&self) -> String {
83 self.to_string()
84 }
85
86 fn __str__(&self) -> String {
87 self.to_string()
88 }
89
90 #[getter]
91 #[pyo3(name = "account_id")]
92 const fn py_account_id(&self) -> AccountId {
93 self.account_id
94 }
95
96 #[getter]
97 #[pyo3(name = "instrument_id")]
98 const fn py_instrument_id(&self) -> InstrumentId {
99 self.instrument_id
100 }
101
102 #[getter]
103 #[pyo3(name = "strategy_id")]
104 const fn py_strategy_id(&self) -> InstrumentId {
105 self.instrument_id
106 }
107
108 #[getter]
109 #[pyo3(name = "venue_position_id")]
110 const fn py_venue_position_id(&self) -> Option<PositionId> {
111 self.venue_position_id
112 }
113
114 #[getter]
115 #[pyo3(name = "position_side")]
116 fn py_position_side(&self) -> PositionSide {
117 self.position_side
118 }
119
120 #[getter]
121 #[pyo3(name = "quantity")]
122 const fn py_quantity(&self) -> Quantity {
123 self.quantity
124 }
125
126 #[getter]
127 #[pyo3(name = "avg_px_open")]
128 const fn py_avg_px_open(&self) -> Option<Decimal> {
129 self.avg_px_open
130 }
131
132 #[getter]
133 #[pyo3(name = "avg_px_open_reconciliation")]
134 const fn py_avg_px_open_reconciliation(&self) -> AvgPxReconciliation {
135 self.avg_px_open_reconciliation
136 }
137
138 #[getter]
139 #[pyo3(name = "avg_px_open_precision")]
140 const fn py_avg_px_open_precision(&self) -> Option<u8> {
141 self.avg_px_open_precision
142 }
143
144 #[getter]
145 #[pyo3(name = "liquidation_price")]
146 const fn py_liquidation_price(&self) -> Option<Decimal> {
147 self.liquidation_price
148 }
149
150 #[getter]
151 #[pyo3(name = "report_id")]
152 const fn py_report_id(&self) -> UUID4 {
153 self.report_id
154 }
155
156 #[getter]
157 #[pyo3(name = "ts_last")]
158 const fn py_ts_last(&self) -> u64 {
159 self.ts_last.as_u64()
160 }
161
162 #[getter]
163 #[pyo3(name = "ts_init")]
164 const fn py_ts_init(&self) -> u64 {
165 self.ts_init.as_u64()
166 }
167
168 #[getter]
170 #[pyo3(name = "is_flat")]
171 fn py_is_flat(&self) -> bool {
172 self.is_flat()
173 }
174
175 #[getter]
177 #[pyo3(name = "is_long")]
178 fn py_is_long(&self) -> bool {
179 self.is_long()
180 }
181
182 #[getter]
184 #[pyo3(name = "is_short")]
185 fn py_is_short(&self) -> bool {
186 self.is_short()
187 }
188
189 #[staticmethod]
195 #[pyo3(name = "from_dict")]
196 pub fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
197 from_dict_pyo3(py, values)
198 }
199
200 #[pyo3(name = "to_dict")]
206 pub fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
207 let dict = PyDict::new(py);
208 dict.set_item("type", stringify!(PositionStatusReport))?;
209 dict.set_item("account_id", self.account_id.to_string())?;
210 dict.set_item("instrument_id", self.instrument_id.to_string())?;
211
212 match self.venue_position_id {
213 Some(venue_position_id) => {
214 dict.set_item("venue_position_id", venue_position_id.to_string())?;
215 }
216 None => dict.set_item("venue_position_id", py.None())?,
217 }
218 dict.set_item("position_side", self.position_side.to_string())?;
219 dict.set_item("quantity", self.quantity.to_string())?;
220 dict.set_item("signed_decimal_qty", self.signed_decimal_qty.to_string())?;
221 dict.set_item("report_id", self.report_id.to_string())?;
222 dict.set_item("ts_last", self.ts_last.as_u64())?;
223 dict.set_item("ts_init", self.ts_init.as_u64())?;
224 dict.set_item(
225 "avg_px_open",
226 self.avg_px_open.map(|avg_px_open| avg_px_open.to_string()),
227 )?;
228 dict.set_item(
229 "avg_px_open_reconciliation",
230 self.avg_px_open_reconciliation.to_string(),
231 )?;
232 dict.set_item("avg_px_open_precision", self.avg_px_open_precision)?;
233 dict.set_item(
234 "liquidation_price",
235 self.liquidation_price.map(|price| price.to_string()),
236 )?;
237 Ok(dict.into())
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use rstest::rstest;
244 use rust_decimal_macros::dec;
245
246 use super::*;
247
248 #[rstest]
249 #[case::reported(Some(dec!(0.87654321987654321)))]
250 #[case::zero(Some(Decimal::ZERO))]
251 #[case::unavailable(None)]
252 fn test_position_status_report_python_dict_round_trip(
253 #[case] liquidation_price: Option<Decimal>,
254 ) {
255 Python::initialize();
256 Python::attach(|py| {
257 let report = PositionStatusReport::py_new(
258 AccountId::from("SIM-001"),
259 InstrumentId::from("AUDUSD.SIM"),
260 PositionSide::Short,
261 Quantity::from("100"),
262 1_000_000_000,
263 2_000_000_000,
264 None,
265 Some(PositionId::from("P-001")),
266 Some(dec!(1.23456)),
267 Some(AvgPxReconciliation::OpeningOnly),
268 Some(4),
269 liquidation_price,
270 );
271
272 let values = report.py_to_dict(py).unwrap();
273 let restored =
274 PositionStatusReport::py_from_dict(py, values.extract(py).unwrap()).unwrap();
275
276 assert_eq!(report.avg_px_open, Some(dec!(1.23456)));
277 assert_eq!(
278 report.avg_px_open_reconciliation,
279 AvgPxReconciliation::OpeningOnly
280 );
281 assert_eq!(report.avg_px_open_precision, Some(4));
282 assert_eq!(report.py_liquidation_price(), liquidation_price);
283 assert_eq!(restored, report);
284 });
285 }
286}