Skip to main content

nautilus_interactive_brokers/python/
providers.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 the Interactive Brokers instrument provider.
17
18use nautilus_common::python::config_error_to_pyvalue_err;
19use nautilus_core::python::to_pyruntime_err;
20use nautilus_model::{identifiers::InstrumentId, python::instruments::instrument_any_to_pyobject};
21use pyo3::{prelude::*, types::PyList};
22
23use crate::{
24    config::InteractiveBrokersInstrumentProviderConfig,
25    providers::instruments::InteractiveBrokersInstrumentProvider,
26    python::conversion::{contract_details_to_pyobject, py_to_contract},
27};
28
29#[cfg(feature = "python")]
30#[pymethods]
31#[pyo3_stub_gen::derive::gen_stub_pymethods]
32impl InteractiveBrokersInstrumentProvider {
33    #[new]
34    fn py_new(config: InteractiveBrokersInstrumentProviderConfig) -> PyResult<Self> {
35        Self::new(config).map_err(config_error_to_pyvalue_err)
36    }
37
38    fn __repr__(&self) -> String {
39        format!("{self:?}")
40    }
41
42    /// Find an instrument by its ID.
43    #[pyo3(name = "find")]
44    fn py_find(&self, py: Python, instrument_id: InstrumentId) -> PyResult<Option<Py<PyAny>>> {
45        match self.find(&instrument_id) {
46            Some(instrument) => Ok(Some(instrument_any_to_pyobject(py, instrument)?)),
47            None => Ok(None),
48        }
49    }
50
51    /// Find an instrument by IB contract ID.
52    #[pyo3(name = "find_by_contract_id")]
53    fn py_find_by_contract_id(&self, py: Python, contract_id: i32) -> PyResult<Option<Py<PyAny>>> {
54        match self.find_by_contract_id(contract_id) {
55            Some(instrument) => Ok(Some(instrument_any_to_pyobject(py, instrument)?)),
56            None => Ok(None),
57        }
58    }
59
60    /// Get all cached instruments.
61    #[pyo3(name = "get_all")]
62    fn py_get_all<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
63        let instruments = self.get_all();
64        let py_instruments: PyResult<Vec<_>> = instruments
65            .into_iter()
66            .map(|inst| instrument_any_to_pyobject(py, inst))
67            .collect();
68        PyList::new(py, py_instruments?)
69    }
70
71    /// Get the number of cached instruments.
72    #[pyo3(name = "count")]
73    fn py_count(&self) -> usize {
74        self.count()
75    }
76
77    /// Get price magnifier for an instrument ID.
78    #[pyo3(name = "get_price_magnifier")]
79    fn py_get_price_magnifier(&self, instrument_id: InstrumentId) -> i32 {
80        self.get_price_magnifier(&instrument_id)
81    }
82
83    /// Maintain compatibility with the legacy Python provider API.
84    ///
85    /// Contract details are fetched as part of the data/execution client load flow,
86    /// so the standalone provider has nothing to do here.
87    #[pyo3(name = "fetch_contract_details")]
88    fn py_fetch_contract_details(&self) {}
89
90    /// Determine venue from contract using provider configuration.
91    #[pyo3(name = "determine_venue")]
92    #[allow(clippy::needless_pass_by_value)]
93    fn py_determine_venue(&self, py: Python<'_>, contract: Py<PyAny>) -> PyResult<String> {
94        let rust_contract = py_to_contract(contract.bind(py))?;
95        Ok(self.determine_venue(&rust_contract, None).to_string())
96    }
97
98    /// Convert an instrument ID to cached IB contract details.
99    #[pyo3(name = "instrument_id_to_ib_contract_details")]
100    fn py_instrument_id_to_ib_contract_details(
101        &self,
102        py: Python<'_>,
103        instrument_id: InstrumentId,
104    ) -> PyResult<Option<Py<PyAny>>> {
105        self.instrument_id_to_ib_contract_details(&instrument_id)
106            .as_ref()
107            .map(|details| contract_details_to_pyobject(py, details))
108            .transpose()
109    }
110
111    /// Save the current instrument cache to disk.
112    ///
113    /// # Errors
114    ///
115    /// Returns an error if serialization or file I/O fails.
116    #[pyo3(name = "save_cache")]
117    fn py_save_cache<'py>(
118        &self,
119        py: Python<'py>,
120        cache_path: String,
121    ) -> PyResult<Bound<'py, PyAny>> {
122        let provider = self.clone();
123        pyo3_async_runtimes::tokio::future_into_py(py, async move {
124            provider
125                .save_cache(&cache_path)
126                .await
127                .map_err(to_pyruntime_err)
128        })
129    }
130
131    /// Load instrument cache from disk if valid.
132    ///
133    /// # Returns
134    ///
135    /// Returns `true` if cache was loaded successfully and is valid, `false` otherwise.
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if deserialization or file I/O fails (but treats missing file as non-error).
140    #[pyo3(name = "load_cache")]
141    fn py_load_cache<'py>(
142        &self,
143        py: Python<'py>,
144        cache_path: String,
145    ) -> PyResult<Bound<'py, PyAny>> {
146        let provider = self.clone();
147        pyo3_async_runtimes::tokio::future_into_py(py, async move {
148            provider
149                .load_cache(&cache_path)
150                .await
151                .map_err(to_pyruntime_err)
152        })
153    }
154}