Skip to main content

nautilus_interactive_brokers/python/
execution.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
15use std::collections::HashMap;
16
17use nautilus_common::{clients::ExecutionClient, live::get_runtime};
18use nautilus_core::python::to_pyruntime_err;
19use nautilus_model::{
20    enums::OrderSide,
21    identifiers::{
22        ClientId, ClientOrderId, ExecAlgorithmId, InstrumentId, PositionId, StrategyId, TraderId,
23        VenueOrderId,
24    },
25    python::orders::pyobject_to_order_any,
26    types::{Price, Quantity},
27};
28use pyo3::prelude::*;
29
30use crate::execution::InteractiveBrokersExecutionClient;
31
32#[cfg(feature = "python")]
33#[pymethods]
34impl InteractiveBrokersExecutionClient {
35    #[new]
36    #[pyo3(signature = (_msgbus, _cache, _clock, instrument_provider, config))]
37    fn py_new(
38        _msgbus: Py<PyAny>,
39        _cache: Py<PyAny>,
40        _clock: Py<PyAny>,
41        instrument_provider: crate::providers::instruments::InteractiveBrokersInstrumentProvider,
42        config: crate::config::InteractiveBrokersExecClientConfig,
43    ) -> PyResult<Self> {
44        Self::new_for_python(config, instrument_provider).map_err(to_pyruntime_err)
45    }
46
47    #[pyo3(name = "set_event_callback")]
48    fn py_set_event_callback(&self, callback: Py<PyAny>) {
49        self.register_python_event_callback(callback);
50    }
51
52    /// Returns the client ID.
53    #[getter]
54    pub fn client_id(&self) -> ClientId {
55        ExecutionClient::client_id(self)
56    }
57
58    /// Returns whether the client is connected.
59    #[getter]
60    pub fn is_connected(&self) -> bool {
61        ExecutionClient::is_connected(self)
62    }
63
64    /// Returns whether the client is disconnected.
65    #[getter]
66    pub fn is_disconnected(&self) -> bool {
67        !ExecutionClient::is_connected(self)
68    }
69
70    #[pyo3(name = "connect")]
71    fn py_connect(&mut self) -> PyResult<()> {
72        get_runtime()
73            .block_on(ExecutionClient::connect(self))
74            .map_err(to_pyruntime_err)
75    }
76
77    #[pyo3(name = "disconnect")]
78    fn py_disconnect(&mut self) -> PyResult<()> {
79        get_runtime()
80            .block_on(ExecutionClient::disconnect(self))
81            .map_err(to_pyruntime_err)
82    }
83
84    /// Submit a single order.
85    ///
86    /// # Arguments
87    ///
88    /// * `trader_id` - The trader ID
89    /// * `order` - The order to submit (as PyO3 OrderAny)
90    /// * `instrument_id` - The instrument ID
91    /// * `strategy_id` - The strategy ID
92    /// * `exec_algorithm_id` - Optional execution algorithm ID
93    /// * `position_id` - Optional position ID
94    /// * `params` - Optional parameters dictionary
95    ///
96    /// # Errors
97    ///
98    /// Returns an error if submission fails.
99    #[pyo3(name = "submit_order")]
100    fn py_submit_order(
101        &self,
102        py: Python,
103        trader_id: TraderId,
104        order: Py<PyAny>,
105        instrument_id: InstrumentId,
106        strategy_id: StrategyId,
107        exec_algorithm_id: Option<ExecAlgorithmId>,
108        position_id: Option<PositionId>,
109        params: Option<HashMap<String, String>>,
110    ) -> PyResult<()> {
111        let order_any = pyobject_to_order_any(py, order)?;
112        self.submit_order_for_python(
113            trader_id,
114            order_any,
115            instrument_id,
116            strategy_id,
117            exec_algorithm_id,
118            position_id,
119            params,
120        )
121        .map_err(to_pyruntime_err)
122    }
123
124    /// Submit a list of orders (OCA group).
125    ///
126    /// # Arguments
127    ///
128    /// * `trader_id` - The trader ID
129    /// * `strategy_id` - The strategy ID
130    /// * `orders` - List of orders (Python list of Order objects)
131    /// * `exec_algorithm_id` - Optional execution algorithm ID
132    /// * `position_id` - Optional position ID
133    /// * `params` - Optional parameters dictionary
134    ///
135    /// # Errors
136    ///
137    /// Returns an error if submission fails.
138    #[pyo3(name = "submit_order_list")]
139    fn py_submit_order_list(
140        &self,
141        py: Python,
142        trader_id: TraderId,
143        strategy_id: StrategyId,
144        orders: Vec<Py<PyAny>>,
145        exec_algorithm_id: Option<ExecAlgorithmId>,
146        position_id: Option<PositionId>,
147        params: Option<HashMap<String, String>>,
148    ) -> PyResult<()> {
149        let mut order_anys = Vec::new();
150        for order_py in orders {
151            order_anys.push(pyobject_to_order_any(py, order_py)?);
152        }
153        self.submit_order_list_for_python(
154            trader_id,
155            strategy_id,
156            order_anys,
157            exec_algorithm_id,
158            position_id,
159            params,
160        )
161        .map_err(to_pyruntime_err)
162    }
163
164    /// Modify an existing order.
165    ///
166    /// # Arguments
167    ///
168    /// * `client_order_id` - The client order ID to modify
169    /// * `venue_order_id` - The venue order ID
170    /// * `instrument_id` - The instrument ID
171    /// * `quantity` - Optional new quantity
172    /// * `price` - Optional new price
173    /// * `trigger_price` - Optional new trigger price
174    ///
175    /// # Errors
176    ///
177    /// Returns an error if modification fails.
178    #[pyo3(name = "modify_order")]
179    fn py_modify_order(
180        &self,
181        trader_id: TraderId,
182        strategy_id: StrategyId,
183        client_order_id: ClientOrderId,
184        venue_order_id: Option<VenueOrderId>,
185        instrument_id: InstrumentId,
186        quantity: Option<Quantity>,
187        price: Option<Price>,
188        trigger_price: Option<Price>,
189        params: Option<HashMap<String, String>>,
190    ) -> PyResult<()> {
191        self.modify_order_for_python(
192            trader_id,
193            strategy_id,
194            client_order_id,
195            venue_order_id,
196            instrument_id,
197            quantity,
198            price,
199            trigger_price,
200            params,
201        )
202        .map_err(to_pyruntime_err)
203    }
204
205    /// Cancel a specific order.
206    ///
207    /// # Arguments
208    ///
209    /// * `client_order_id` - The client order ID to cancel
210    /// * `venue_order_id` - The venue order ID
211    /// * `instrument_id` - The instrument ID
212    ///
213    /// # Errors
214    ///
215    /// Returns an error if cancellation fails.
216    #[pyo3(name = "cancel_order")]
217    fn py_cancel_order(
218        &self,
219        trader_id: TraderId,
220        strategy_id: StrategyId,
221        client_order_id: ClientOrderId,
222        venue_order_id: Option<VenueOrderId>,
223        instrument_id: InstrumentId,
224        params: Option<HashMap<String, String>>,
225    ) -> PyResult<()> {
226        self.cancel_order_for_python(
227            trader_id,
228            strategy_id,
229            client_order_id,
230            venue_order_id,
231            instrument_id,
232            params,
233        )
234        .map_err(to_pyruntime_err)
235    }
236
237    /// Cancel all orders for an instrument.
238    ///
239    /// # Arguments
240    ///
241    /// * `instrument_id` - The instrument ID
242    ///
243    /// # Errors
244    ///
245    /// Returns an error if cancellation fails.
246    #[pyo3(name = "cancel_all_orders")]
247    fn py_cancel_all_orders(
248        &self,
249        trader_id: TraderId,
250        strategy_id: StrategyId,
251        instrument_id: InstrumentId,
252        order_side: OrderSide,
253        params: Option<HashMap<String, String>>,
254    ) -> PyResult<()> {
255        self.cancel_all_orders_for_python(trader_id, strategy_id, instrument_id, order_side, params)
256            .map_err(to_pyruntime_err)
257    }
258
259    /// Batch cancel multiple orders.
260    ///
261    /// # Arguments
262    ///
263    /// * `client_order_ids` - List of client order IDs to cancel
264    ///
265    /// # Errors
266    ///
267    /// Returns an error if cancellation fails.
268    #[pyo3(name = "batch_cancel_orders")]
269    fn py_batch_cancel_orders(
270        &self,
271        trader_id: TraderId,
272        strategy_id: StrategyId,
273        instrument_id: InstrumentId,
274        client_order_ids: Vec<ClientOrderId>,
275        params: Option<HashMap<String, String>>,
276    ) -> PyResult<()> {
277        self.batch_cancel_orders_for_python(
278            trader_id,
279            strategy_id,
280            instrument_id,
281            client_order_ids,
282            params,
283        )
284        .map_err(to_pyruntime_err)
285    }
286
287    /// Query the status of an account.
288    ///
289    /// # Errors
290    ///
291    /// Returns an error if the query fails.
292    #[pyo3(name = "query_account")]
293    fn py_query_account(&self, trader_id: TraderId) -> PyResult<()> {
294        self.query_account_for_python(trader_id)
295            .map_err(to_pyruntime_err)
296    }
297
298    /// Query the status of an order.
299    ///
300    /// # Arguments
301    ///
302    /// * `trader_id` - The trader ID
303    /// * `strategy_id` - The strategy ID
304    /// * `instrument_id` - The instrument ID
305    /// * `client_order_id` - The client order ID
306    /// * `venue_order_id` - Optional venue order ID (IB order id)
307    ///
308    /// # Errors
309    ///
310    /// Returns an error if the query fails.
311    #[pyo3(name = "query_order")]
312    fn py_query_order(
313        &self,
314        trader_id: TraderId,
315        strategy_id: StrategyId,
316        instrument_id: InstrumentId,
317        client_order_id: ClientOrderId,
318        venue_order_id: Option<VenueOrderId>,
319    ) -> PyResult<()> {
320        self.query_order_for_python(
321            trader_id,
322            strategy_id,
323            instrument_id,
324            client_order_id,
325            venue_order_id,
326        )
327        .map_err(to_pyruntime_err)
328    }
329
330    /// Generate execution mass status (order reports, fill reports, position reports).
331    ///
332    /// # Arguments
333    ///
334    /// * `lookback_mins` - Optional lookback in minutes for closed orders/fills/positions
335    ///
336    /// # Returns
337    ///
338    /// Returns an ExecutionMassStatus if successful, None otherwise.
339    ///
340    /// # Errors
341    ///
342    /// Returns an error if generation fails.
343    #[pyo3(name = "generate_mass_status")]
344    fn py_generate_mass_status<'py>(
345        &self,
346        py: Python<'py>,
347        lookback_mins: Option<u64>,
348    ) -> PyResult<Bound<'py, PyAny>> {
349        let client = self;
350        let fut = async move { client.generate_mass_status(lookback_mins).await };
351        let rt = get_runtime();
352
353        match rt.block_on(fut) {
354            Ok(Some(mass_status)) => {
355                let py_mass_status = Py::new(py, mass_status).map_err(to_pyruntime_err)?;
356                Ok(py_mass_status.bind(py).as_any().to_owned())
357            }
358            Ok(None) => Ok(py.None().bind(py).as_any().to_owned()),
359            Err(e) => Err(to_pyruntime_err(format!(
360                "Failed to generate mass status: {e}"
361            ))),
362        }
363    }
364
365    /// Generate a single order status report.
366    ///
367    /// # Arguments
368    ///
369    /// * `instrument_id` - Optional instrument ID
370    /// * `client_order_id` - Optional client order ID
371    /// * `venue_order_id` - Optional venue order ID
372    ///
373    /// # Returns
374    ///
375    /// Returns the order status report if found, `None` otherwise.
376    ///
377    /// # Errors
378    ///
379    /// Returns an error if report generation fails.
380    #[pyo3(name = "generate_order_status_report")]
381    fn py_generate_order_status_report<'py>(
382        &self,
383        py: Python<'py>,
384        instrument_id: Option<InstrumentId>,
385        client_order_id: Option<ClientOrderId>,
386        venue_order_id: Option<VenueOrderId>,
387    ) -> PyResult<Bound<'py, PyAny>> {
388        let client = self;
389        let fut = async move {
390            client
391                .generate_order_status_report_for_python(
392                    instrument_id,
393                    client_order_id,
394                    venue_order_id,
395                )
396                .await
397        };
398        let rt = get_runtime();
399
400        match rt.block_on(fut) {
401            Ok(Some(report)) => {
402                let py_report = Py::new(py, report).map_err(to_pyruntime_err)?;
403                Ok(py_report.bind(py).as_any().to_owned())
404            }
405            Ok(None) => Ok(py.None().bind(py).as_any().to_owned()),
406            Err(e) => Err(to_pyruntime_err(format!(
407                "Failed to generate order_status_report: {e}"
408            ))),
409        }
410    }
411
412    /// Generate multiple order status reports.
413    ///
414    /// # Arguments
415    ///
416    /// * `open_only` - Whether to return only open orders
417    /// * `instrument_id` - Optional instrument ID to filter by
418    /// * `start` - Optional start timestamp (Unix nanoseconds)
419    /// * `end` - Optional end timestamp (Unix nanoseconds)
420    ///
421    /// # Returns
422    ///
423    /// Returns a list of order status reports.
424    ///
425    /// # Errors
426    ///
427    /// Returns an error if report generation fails.
428    #[pyo3(name = "generate_order_status_reports")]
429    fn py_generate_order_status_reports<'py>(
430        &self,
431        py: Python<'py>,
432        open_only: bool,
433        instrument_id: Option<InstrumentId>,
434        start: Option<u64>,
435        end: Option<u64>,
436    ) -> PyResult<Bound<'py, PyAny>> {
437        let client = self;
438        let fut = async move {
439            client
440                .generate_order_status_reports_for_python(open_only, instrument_id, start, end)
441                .await
442        };
443        let rt = get_runtime();
444
445        match rt.block_on(fut) {
446            Ok(reports) => {
447                let py_reports: Result<Vec<_>, _> =
448                    reports.into_iter().map(|r| Py::new(py, r)).collect();
449                let py_list = pyo3::types::PyList::new(py, py_reports.map_err(to_pyruntime_err)?)
450                    .map_err(to_pyruntime_err)?;
451                Ok(py_list.as_any().to_owned())
452            }
453            Err(e) => Err(to_pyruntime_err(format!(
454                "Failed to generate order status reports: {e}"
455            ))),
456        }
457    }
458
459    /// Generate fill reports.
460    ///
461    /// # Arguments
462    ///
463    /// * `instrument_id` - Optional instrument ID to filter by
464    /// * `venue_order_id` - Optional venue order ID to filter by
465    /// * `start` - Optional start timestamp (Unix nanoseconds)
466    /// * `end` - Optional end timestamp (Unix nanoseconds)
467    ///
468    /// # Returns
469    ///
470    /// Returns a list of fill reports.
471    ///
472    /// # Errors
473    ///
474    /// Returns an error if report generation fails.
475    #[pyo3(name = "generate_fill_reports")]
476    fn py_generate_fill_reports<'py>(
477        &self,
478        py: Python<'py>,
479        instrument_id: Option<InstrumentId>,
480        venue_order_id: Option<VenueOrderId>,
481        start: Option<u64>,
482        end: Option<u64>,
483    ) -> PyResult<Bound<'py, PyAny>> {
484        let client = self;
485        let fut = async move {
486            client
487                .generate_fill_reports_for_python(instrument_id, venue_order_id, start, end)
488                .await
489        };
490        let rt = get_runtime();
491
492        match rt.block_on(fut) {
493            Ok(reports) => {
494                // Convert to Python list
495                let py_reports: Result<Vec<_>, _> =
496                    reports.into_iter().map(|r| Py::new(py, r)).collect();
497                let py_list = pyo3::types::PyList::new(py, py_reports.map_err(to_pyruntime_err)?)
498                    .map_err(to_pyruntime_err)?;
499                Ok(py_list.as_any().to_owned())
500            }
501            Err(e) => Err(to_pyruntime_err(format!(
502                "Failed to generate fill reports: {e}"
503            ))),
504        }
505    }
506
507    /// Generate position status reports.
508    ///
509    /// # Arguments
510    ///
511    /// * `instrument_id` - Optional instrument ID to filter by
512    /// * `start` - Optional start timestamp (Unix nanoseconds)
513    /// * `end` - Optional end timestamp (Unix nanoseconds)
514    ///
515    /// # Returns
516    ///
517    /// Returns a list of position status reports.
518    ///
519    /// # Errors
520    ///
521    /// Returns an error if report generation fails.
522    #[pyo3(name = "generate_position_status_reports")]
523    fn py_generate_position_status_reports<'py>(
524        &self,
525        py: Python<'py>,
526        instrument_id: Option<InstrumentId>,
527        start: Option<u64>,
528        end: Option<u64>,
529    ) -> PyResult<Bound<'py, PyAny>> {
530        let client = self;
531        let fut = async move {
532            client
533                .generate_position_status_reports_for_python(instrument_id, start, end)
534                .await
535        };
536        let rt = get_runtime();
537
538        match rt.block_on(fut) {
539            Ok(reports) => {
540                // Convert to Python list
541                let py_reports: Result<Vec<_>, _> =
542                    reports.into_iter().map(|r| Py::new(py, r)).collect();
543                let py_list = pyo3::types::PyList::new(py, py_reports.map_err(to_pyruntime_err)?)
544                    .map_err(to_pyruntime_err)?;
545                Ok(py_list.as_any().to_owned())
546            }
547            Err(e) => Err(to_pyruntime_err(format!(
548                "Failed to generate position status reports: {e}"
549            ))),
550        }
551    }
552}