Skip to main content

nautilus_execution/python/
reconciliation.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
16//! Python bindings for reconciliation functions.
17
18use nautilus_core::{UnixNanos, python::to_pyvalue_err};
19use nautilus_model::{
20    enums::{OrderSide, OrderType},
21    identifiers::{AccountId, ClientOrderId, InstrumentId, PositionId, TradeId, VenueOrderId},
22    python::instruments::pyobject_to_instrument_any,
23    reports::ExecutionMassStatus,
24    types::{Price, Quantity},
25};
26use pyo3::{
27    IntoPyObjectExt,
28    prelude::*,
29    types::{PyDict, PyTuple},
30};
31use rust_decimal::Decimal;
32
33use crate::reconciliation::{
34    calculate_reconciliation_price, create_inferred_reconciliation_trade_id,
35    create_position_reconciliation_venue_order_id, process_mass_status_for_reconciliation,
36};
37
38/// Process mass status for position reconciliation.
39///
40/// Takes ExecutionMassStatus and Instrument, performs all reconciliation logic in Rust,
41/// and returns tuple of (order_reports, fill_reports) ready for processing.
42///
43/// # Returns
44///
45/// Tuple of `(Dict[str, OrderStatusReport], Dict[str, List[FillReport]])`
46///
47/// # Errors
48///
49/// Returns an error if instrument conversion or reconciliation fails.
50#[pyfunction(name = "adjust_fills_for_partial_window")]
51#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.execution")]
52#[pyo3(signature = (mass_status, instrument, tolerance=None))]
53pub fn py_adjust_fills_for_partial_window(
54    py: Python<'_>,
55    mass_status: &Bound<'_, PyAny>,
56    instrument: Py<PyAny>,
57    tolerance: Option<String>,
58) -> PyResult<Py<PyTuple>> {
59    let instrument_any = pyobject_to_instrument_any(py, instrument)?;
60    let mass_status_obj: ExecutionMassStatus = mass_status.extract()?;
61
62    let tol = tolerance
63        .map(|s| Decimal::from_str_exact(&s).map_err(to_pyvalue_err))
64        .transpose()?;
65
66    let result = process_mass_status_for_reconciliation(&mass_status_obj, &instrument_any, tol)
67        .map_err(to_pyvalue_err)?;
68
69    let orders_dict = PyDict::new(py);
70    for (id, order) in result.orders {
71        orders_dict.set_item(id.to_string(), order.into_py_any(py)?)?;
72    }
73
74    let fills_dict = PyDict::new(py);
75    for (id, fills) in result.fills {
76        let fills_list: Result<Vec<_>, _> = fills.into_iter().map(|f| f.into_py_any(py)).collect();
77        fills_dict.set_item(id.to_string(), fills_list?)?;
78    }
79
80    Ok(PyTuple::new(
81        py,
82        [orders_dict.into_py_any(py)?, fills_dict.into_py_any(py)?],
83    )?
84    .into())
85}
86
87/// Calculate the price needed for a reconciliation order to achieve target position.
88///
89/// This is a pure function that calculates what price a fill would need to have
90/// to move from the current position state to the target position state with the
91/// correct average price, accounting for the netting simulation logic.
92///
93/// # Returns
94///
95/// Returns `Some(Decimal)` if a valid reconciliation price can be calculated, `None` otherwise.
96///
97/// # Notes
98///
99/// The function handles four scenarios:
100/// 1. Position to flat: reconciliation_px = current_avg_px (close at current average)
101/// 2. Flat to position: reconciliation_px = target_avg_px
102/// 3. Position flip (sign change): reconciliation_px = target_avg_px (due to value reset in simulation)
103/// 4. Accumulation/reduction: weighted average formula
104#[pyfunction(name = "calculate_reconciliation_price")]
105#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.execution")]
106#[pyo3(signature = (current_position_qty, current_position_avg_px, target_position_qty, target_position_avg_px))]
107pub fn py_calculate_reconciliation_price(
108    current_position_qty: Decimal,
109    current_position_avg_px: Option<Decimal>,
110    target_position_qty: Decimal,
111    target_position_avg_px: Option<Decimal>,
112) -> Option<Decimal> {
113    calculate_reconciliation_price(
114        current_position_qty,
115        current_position_avg_px,
116        target_position_qty,
117        target_position_avg_px,
118    )
119}
120
121/// Create a deterministic `TradeId` for an inferred reconciliation fill.
122///
123/// The `account_id` scopes the ID to the venue account, preventing cross-account
124/// collisions on venues where `venue_order_id` is only account-unique. The `ts_last`
125/// (venue-provided) differentiates successive reconciliation incidents with the same
126/// shape while keeping cross-restart replays deterministic.
127#[pyfunction(name = "create_inferred_reconciliation_trade_id")]
128#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.execution")]
129#[pyo3(signature = (account_id, instrument_id, client_order_id, venue_order_id, order_side, order_type, filled_qty, last_qty, last_px, position_id, ts_last))]
130#[expect(clippy::too_many_arguments)]
131pub fn py_create_inferred_reconciliation_trade_id(
132    account_id: AccountId,
133    instrument_id: InstrumentId,
134    client_order_id: ClientOrderId,
135    venue_order_id: Option<VenueOrderId>,
136    order_side: OrderSide,
137    order_type: OrderType,
138    filled_qty: Quantity,
139    last_qty: Quantity,
140    last_px: Price,
141    position_id: PositionId,
142    ts_last: u64,
143) -> TradeId {
144    create_inferred_reconciliation_trade_id(
145        account_id,
146        instrument_id,
147        client_order_id,
148        venue_order_id,
149        order_side,
150        order_type,
151        filled_qty,
152        last_qty,
153        last_px,
154        position_id,
155        UnixNanos::from(ts_last),
156    )
157}
158
159/// The `account_id` scopes the ID to the venue account, preventing cross-account
160/// collisions where the engine would otherwise fall back to `ClientOrderId::from(venue_order_id)`
161/// and conflate orders from different accounts. The `ts_last` (venue-provided) ensures that
162/// successive reconciliation incidents with the same shape get distinct IDs, while the same
163/// logical event replayed after restart still hashes the same (venue re-reports identical ts).
164#[pyfunction(name = "create_position_reconciliation_venue_order_id")]
165#[pyo3_stub_gen::derive::gen_stub_pyfunction(module = "nautilus_trader.execution")]
166#[pyo3(signature = (account_id, instrument_id, order_side, order_type, quantity, price=None, venue_position_id=None, ts_last=0, tag=None))]
167#[expect(clippy::needless_pass_by_value, clippy::too_many_arguments)]
168pub fn py_create_position_reconciliation_venue_order_id(
169    account_id: AccountId,
170    instrument_id: InstrumentId,
171    order_side: OrderSide,
172    order_type: OrderType,
173    quantity: Quantity,
174    price: Option<Price>,
175    venue_position_id: Option<PositionId>,
176    ts_last: u64,
177    tag: Option<String>,
178) -> VenueOrderId {
179    create_position_reconciliation_venue_order_id(
180        account_id,
181        instrument_id,
182        order_side,
183        order_type,
184        quantity,
185        price,
186        venue_position_id,
187        tag.as_deref(),
188        UnixNanos::from(ts_last),
189    )
190}