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