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