Skip to main content

nautilus_model/python/
position.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use nautilus_core::python::{IntoPyObjectNautilusExt, serialization::from_dict_pyo3};
17use pyo3::{
18    basic::CompareOp,
19    prelude::*,
20    types::{PyDict, PyList},
21};
22use rust_decimal::prelude::ToPrimitive;
23
24use super::common::commissions_from_vec;
25use crate::{
26    enums::{OrderSide, PositionSide},
27    events::{OrderFilled, PositionAdjusted},
28    identifiers::{
29        ClientOrderId, InstrumentId, PositionId, StrategyId, Symbol, TradeId, TraderId, Venue,
30        VenueOrderId,
31    },
32    position::Position,
33    python::instruments::pyobject_to_instrument_any,
34    types::{Currency, Money, Price, Quantity},
35};
36
37#[pymethods]
38#[pyo3_stub_gen::derive::gen_stub_pymethods]
39impl Position {
40    /// Represents a position in a market.
41    ///
42    /// The position ID may be assigned at the trading venue, or can be system
43    /// generated depending on a strategies OMS (Order Management System) settings.
44    #[new]
45    fn py_new(py: Python, instrument: Py<PyAny>, fill: OrderFilled) -> PyResult<Self> {
46        let instrument_any = pyobject_to_instrument_any(py, instrument)?;
47        Ok(Self::new(&instrument_any, fill))
48    }
49
50    fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> Py<PyAny> {
51        match op {
52            CompareOp::Eq => self.eq(other).into_py_any_unwrap(py),
53            CompareOp::Ne => self.ne(other).into_py_any_unwrap(py),
54            _ => py.NotImplemented(),
55        }
56    }
57
58    fn __repr__(&self) -> String {
59        self.to_string()
60    }
61
62    fn __str__(&self) -> String {
63        self.to_string()
64    }
65
66    #[getter]
67    #[pyo3(name = "trader_id")]
68    fn py_trader_id(&self) -> TraderId {
69        self.trader_id
70    }
71
72    #[getter]
73    #[pyo3(name = "strategy_id")]
74    fn py_strategy_id(&self) -> StrategyId {
75        self.strategy_id
76    }
77
78    #[getter]
79    #[pyo3(name = "instrument_id")]
80    fn py_instrument_id(&self) -> InstrumentId {
81        self.instrument_id
82    }
83
84    #[getter]
85    #[pyo3(name = "id")]
86    fn py_id(&self) -> PositionId {
87        self.id
88    }
89
90    /// Returns the instrument symbol.
91    #[getter]
92    #[pyo3(name = "symbol")]
93    fn py_symbol(&self) -> Symbol {
94        self.symbol()
95    }
96
97    /// Returns the trading venue.
98    #[getter]
99    #[pyo3(name = "venue")]
100    fn py_venue(&self) -> Venue {
101        self.venue()
102    }
103
104    #[getter]
105    #[pyo3(name = "opening_order_id")]
106    fn py_opening_order_id(&self) -> ClientOrderId {
107        self.opening_order_id
108    }
109
110    #[getter]
111    #[pyo3(name = "closing_order_id")]
112    fn py_closing_order_id(&self) -> Option<ClientOrderId> {
113        self.closing_order_id
114    }
115
116    #[getter]
117    #[pyo3(name = "entry")]
118    fn py_entry(&self) -> OrderSide {
119        self.entry
120    }
121
122    #[getter]
123    #[pyo3(name = "side")]
124    fn py_side(&self) -> PositionSide {
125        self.side
126    }
127
128    #[getter]
129    #[pyo3(name = "signed_qty")]
130    fn py_signed_qty(&self) -> f64 {
131        self.signed_qty
132    }
133
134    #[getter]
135    #[pyo3(name = "quantity")]
136    fn py_quantity(&self) -> Quantity {
137        self.quantity
138    }
139
140    #[getter]
141    #[pyo3(name = "peak_qty")]
142    fn py_peak_qty(&self) -> Quantity {
143        self.peak_qty
144    }
145
146    #[getter]
147    #[pyo3(name = "price_precision")]
148    fn py_price_precision(&self) -> u8 {
149        self.price_precision
150    }
151
152    #[getter]
153    #[pyo3(name = "size_precision")]
154    fn py_size_precision(&self) -> u8 {
155        self.size_precision
156    }
157
158    #[getter]
159    #[pyo3(name = "multiplier")]
160    fn py_multiplier(&self) -> Quantity {
161        self.multiplier
162    }
163
164    #[getter]
165    #[pyo3(name = "is_inverse")]
166    fn py_is_inverse(&self) -> bool {
167        self.is_inverse
168    }
169
170    #[getter]
171    #[pyo3(name = "base_currency")]
172    fn py_base_currency(&self) -> Option<Currency> {
173        self.base_currency
174    }
175
176    #[getter]
177    #[pyo3(name = "quote_currency")]
178    fn py_quote_currency(&self) -> Currency {
179        self.quote_currency
180    }
181
182    #[getter]
183    #[pyo3(name = "settlement_currency")]
184    fn py_settlement_currency(&self) -> Currency {
185        self.settlement_currency
186    }
187
188    #[getter]
189    #[pyo3(name = "ts_init")]
190    fn py_ts_init(&self) -> u64 {
191        self.ts_init.as_u64()
192    }
193
194    #[getter]
195    #[pyo3(name = "ts_opened")]
196    fn py_ts_opened(&self) -> u64 {
197        self.ts_opened.as_u64()
198    }
199
200    #[getter]
201    #[pyo3(name = "ts_closed")]
202    fn py_ts_closed(&self) -> Option<u64> {
203        self.ts_closed.map(std::convert::Into::into)
204    }
205
206    #[getter]
207    #[pyo3(name = "duration_ns")]
208    fn py_duration_ns(&self) -> u64 {
209        self.duration_ns
210    }
211
212    #[getter]
213    #[pyo3(name = "avg_px_open")]
214    fn py_avg_px_open(&self) -> f64 {
215        self.avg_px_open
216    }
217
218    #[getter]
219    #[pyo3(name = "avg_px_close")]
220    fn py_avg_px_close(&self) -> Option<f64> {
221        self.avg_px_close
222    }
223
224    #[getter]
225    #[pyo3(name = "realized_return")]
226    fn py_realized_return(&self) -> f64 {
227        self.realized_return
228    }
229
230    #[getter]
231    #[pyo3(name = "realized_pnl")]
232    fn py_realized_pnl(&self) -> Option<Money> {
233        self.realized_pnl
234    }
235
236    #[pyo3(name = "events")]
237    fn py_events(&self) -> Vec<OrderFilled> {
238        self.events.clone()
239    }
240
241    #[pyo3(name = "adjustments")]
242    fn py_adjustments(&self) -> Vec<PositionAdjusted> {
243        self.adjustments.clone()
244    }
245
246    /// Returns unique client order IDs from all fill events, sorted.
247    #[pyo3(name = "client_order_ids")]
248    fn py_client_order_ids(&self) -> Vec<ClientOrderId> {
249        self.client_order_ids()
250    }
251
252    /// Returns unique venue order IDs from all fill events, sorted.
253    #[pyo3(name = "venue_order_ids")]
254    fn py_venue_order_ids(&self) -> Vec<VenueOrderId> {
255        self.venue_order_ids()
256    }
257
258    /// Returns unique trade IDs from all fill events, sorted.
259    #[pyo3(name = "trade_ids")]
260    fn py_trade_ids(&self) -> Vec<TradeId> {
261        self.trade_ids()
262    }
263
264    /// Returns the last `OrderFilled` event for the position (if any after purging).
265    #[getter]
266    #[pyo3(name = "last_event")]
267    fn py_last_event(&self) -> Option<OrderFilled> {
268        self.last_event()
269    }
270
271    /// Returns the last `TradeId` for the position (if any after purging).
272    #[getter]
273    #[pyo3(name = "last_trade_id")]
274    fn py_last_trade_id(&self) -> Option<TradeId> {
275        self.last_trade_id()
276    }
277
278    /// Returns the count of order fill events applied to this position.
279    #[getter]
280    #[pyo3(name = "event_count")]
281    fn py_event_count(&self) -> usize {
282        self.events.len()
283    }
284
285    /// Returns whether the position is currently open (has quantity and no close timestamp).
286    #[getter]
287    #[pyo3(name = "is_open")]
288    fn py_is_open(&self) -> bool {
289        self.is_open()
290    }
291
292    /// Returns whether the position is closed (flat with a close timestamp).
293    #[getter]
294    #[pyo3(name = "is_closed")]
295    fn py_is_closed(&self) -> bool {
296        self.is_closed()
297    }
298
299    /// Returns whether the position is long (positive quantity).
300    #[getter]
301    #[pyo3(name = "is_long")]
302    fn py_is_long(&self) -> bool {
303        self.is_long()
304    }
305
306    /// Returns whether the position is short (negative quantity).
307    #[getter]
308    #[pyo3(name = "is_short")]
309    fn py_is_short(&self) -> bool {
310        self.is_short()
311    }
312
313    /// Returns unrealized P&L based on the last price.
314    #[pyo3(name = "unrealized_pnl")]
315    fn py_unrealized_pnl(&self, last: Price) -> Money {
316        self.unrealized_pnl(last)
317    }
318
319    /// Returns total P&L (realized + unrealized) based on the last price.
320    #[pyo3(name = "total_pnl")]
321    fn py_total_pnl(&self, last: Price) -> Money {
322        self.total_pnl(last)
323    }
324
325    /// Returns the cumulative commissions for the position as a vector.
326    #[pyo3(name = "commissions")]
327    fn py_commissions(&self) -> Vec<Money> {
328        self.commissions()
329    }
330
331    /// Applies an `OrderFilled` event to this position.
332    #[pyo3(name = "apply")]
333    fn py_apply(&mut self, fill: &OrderFilled) {
334        self.apply(fill);
335    }
336
337    /// Applies a position adjustment event.
338    ///
339    /// This method handles adjustments to position quantity or realized PnL that occur
340    /// outside of normal order fills, such as:
341    /// - Commission adjustments in base currency (crypto spot markets).
342    /// - Funding payments (perpetual futures).
343    ///
344    /// The adjustment event is stored in the position's adjustment history for full audit trail.
345    #[pyo3(name = "apply_adjustment")]
346    fn py_apply_adjustment(&mut self, adjustment: PositionAdjusted) {
347        self.apply_adjustment(adjustment);
348    }
349
350    /// Purges all order fill events for the given client order ID and recalculates derived state.
351    ///
352    /// # Warning
353    ///
354    /// This operation recalculates the entire position from scratch after removing the specified
355    /// order's fills. This is an expensive operation and should be used sparingly.
356    #[pyo3(name = "purge_events_for_order")]
357    fn py_purge_events_for_order(&mut self, client_order_id: ClientOrderId) {
358        self.purge_events_for_order(client_order_id);
359    }
360
361    /// Returns whether the given order side is opposite to the position entry side.
362    #[pyo3(name = "is_opposite_side")]
363    fn py_is_opposite_side(&self, side: OrderSide) -> bool {
364        self.is_opposite_side(side)
365    }
366
367    /// Calculates profit and loss from the given prices and quantity.
368    #[pyo3(name = "calculate_pnl")]
369    fn py_calculate_pnl(&self, avg_px_open: f64, avg_px_close: f64, quantity: Quantity) -> Money {
370        self.calculate_pnl(avg_px_open, avg_px_close, quantity)
371    }
372
373    /// Calculates the notional value based on the last price.
374    #[pyo3(name = "notional_value")]
375    fn py_notional_value(&self, price: Price) -> Money {
376        self.notional_value(price)
377    }
378
379    /// Constructs a [`Position`] from a Python dict.
380    ///
381    /// # Errors
382    ///
383    /// Returns a `PyErr` if deserialization from the Python dict fails.
384    #[staticmethod]
385    #[pyo3(name = "from_dict")]
386    pub fn py_from_dict(py: Python<'_>, values: Py<PyDict>) -> PyResult<Self> {
387        from_dict_pyo3(py, values)
388    }
389
390    /// Converts this [`Position`] into a Python dict.
391    ///
392    /// # Errors
393    ///
394    /// Returns a `PyErr` if serialization into a Python dict fails.
395    #[pyo3(name = "to_dict")]
396    fn py_to_dict(&self, py: Python<'_>) -> PyResult<Py<PyAny>> {
397        let dict = PyDict::new(py);
398        dict.set_item("type", stringify!(Position))?;
399        let events_dict: PyResult<Vec<_>> = self.events.iter().map(|e| e.py_to_dict(py)).collect();
400        dict.set_item("events", events_dict?)?;
401        let adjustments_dict: PyResult<Vec<_>> =
402            self.adjustments.iter().map(|a| a.py_to_dict(py)).collect();
403        dict.set_item("adjustments", adjustments_dict?)?;
404        dict.set_item("trader_id", self.trader_id.to_string())?;
405        dict.set_item("strategy_id", self.strategy_id.to_string())?;
406        dict.set_item("instrument_id", self.instrument_id.to_string())?;
407        dict.set_item("position_id", self.id.to_string())?;
408        dict.set_item("account_id", self.account_id.to_string())?;
409        dict.set_item("opening_order_id", self.opening_order_id.to_string())?;
410
411        match self.closing_order_id {
412            Some(closing_order_id) => {
413                dict.set_item("closing_order_id", closing_order_id.to_string())?;
414            }
415            None => dict.set_item("closing_order_id", py.None())?,
416        }
417        dict.set_item("entry", self.entry.to_string())?;
418        dict.set_item("side", self.side.to_string())?;
419        dict.set_item("signed_qty", self.signed_qty.to_f64())?;
420        dict.set_item("quantity", self.quantity.to_string())?;
421        dict.set_item("peak_qty", self.peak_qty.to_string())?;
422        dict.set_item("price_precision", self.price_precision.to_u8())?;
423        dict.set_item("size_precision", self.size_precision.to_u8())?;
424        dict.set_item("multiplier", self.multiplier.to_string())?;
425        dict.set_item("is_inverse", self.is_inverse)?;
426
427        match self.base_currency {
428            Some(base_currency) => {
429                dict.set_item("base_currency", base_currency.code.to_string())?;
430            }
431            None => dict.set_item("base_currency", py.None())?,
432        }
433        dict.set_item("quote_currency", self.quote_currency.code.to_string())?;
434        dict.set_item(
435            "settlement_currency",
436            self.settlement_currency.code.to_string(),
437        )?;
438        dict.set_item("ts_init", self.ts_init.as_u64())?;
439        dict.set_item("ts_opened", self.ts_opened.as_u64())?;
440        dict.set_item("ts_last", self.ts_last.as_u64())?;
441        match self.ts_closed {
442            Some(ts_closed) => dict.set_item("ts_closed", ts_closed.as_u64())?,
443            None => dict.set_item("ts_closed", py.None())?,
444        }
445        dict.set_item("duration_ns", self.duration_ns.to_u64())?;
446        dict.set_item("avg_px_open", self.avg_px_open)?;
447        match self.avg_px_close {
448            Some(avg_px_close) => dict.set_item("avg_px_close", avg_px_close)?,
449            None => dict.set_item("avg_px_close", py.None())?,
450        }
451        dict.set_item("realized_return", self.realized_return)?;
452        match self.realized_pnl {
453            Some(realized_pnl) => dict.set_item("realized_pnl", realized_pnl.to_string())?,
454            None => dict.set_item("realized_pnl", py.None())?,
455        }
456        let venue_order_ids_list =
457            PyList::new(py, self.venue_order_ids().iter().map(ToString::to_string))
458                .expect("Invalid `ExactSizeIterator`");
459        dict.set_item("venue_order_ids", venue_order_ids_list)?;
460        let trade_ids_list = PyList::new(py, self.trade_ids().iter().map(ToString::to_string))
461            .expect("Invalid `ExactSizeIterator`");
462        dict.set_item("trade_ids", trade_ids_list)?;
463        dict.set_item("buy_qty", self.buy_qty.to_string())?;
464        dict.set_item("sell_qty", self.sell_qty.to_string())?;
465        dict.set_item("commissions", commissions_from_vec(py, self.commissions())?)?;
466        Ok(dict.into())
467    }
468}