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    providers::instruments::InteractiveBrokersInstrumentProvider,
24    python::conversion::{contract_details_to_pyobject, py_to_contract},
25};
26
27#[cfg(feature = "python")]
28#[pymethods]
29impl InteractiveBrokersInstrumentProvider {
30    #[new]
31    fn py_new(config: crate::config::InteractiveBrokersInstrumentProviderConfig) -> Self {
32        Self::new(config)
33    }
34
35    fn __repr__(&self) -> String {
36        format!("{self:?}")
37    }
38
39    /// Find an instrument by its ID.
40    #[pyo3(name = "find")]
41    fn py_find(&self, py: Python, instrument_id: InstrumentId) -> PyResult<Option<Py<PyAny>>> {
42        match self.find(&instrument_id) {
43            Some(instrument) => Ok(Some(instrument_any_to_pyobject(py, instrument)?)),
44            None => Ok(None),
45        }
46    }
47
48    /// Find an instrument by IB contract ID.
49    #[pyo3(name = "find_by_contract_id")]
50    fn py_find_by_contract_id(&self, py: Python, contract_id: i32) -> PyResult<Option<Py<PyAny>>> {
51        match self.find_by_contract_id(contract_id) {
52            Some(instrument) => Ok(Some(instrument_any_to_pyobject(py, instrument)?)),
53            None => Ok(None),
54        }
55    }
56
57    /// Get all cached instruments.
58    #[pyo3(name = "get_all")]
59    fn py_get_all<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyList>> {
60        let instruments = self.get_all();
61        let py_instruments: PyResult<Vec<_>> = instruments
62            .into_iter()
63            .map(|inst| instrument_any_to_pyobject(py, inst))
64            .collect();
65        PyList::new(py, py_instruments?)
66    }
67
68    /// Get the number of cached instruments.
69    #[pyo3(name = "count")]
70    fn py_count(&self) -> usize {
71        self.count()
72    }
73
74    /// Get price magnifier for an instrument ID.
75    #[pyo3(name = "get_price_magnifier")]
76    fn py_get_price_magnifier(&self, instrument_id: InstrumentId) -> i32 {
77        self.get_price_magnifier(&instrument_id)
78    }
79
80    /// Maintain compatibility with the legacy Python provider API.
81    ///
82    /// Contract details are fetched as part of the data/execution client load flow,
83    /// so the standalone provider has nothing to do here.
84    #[pyo3(name = "fetch_contract_details")]
85    fn py_fetch_contract_details(&self) {}
86
87    /// Determine venue from contract using provider configuration.
88    #[pyo3(name = "determine_venue")]
89    #[allow(clippy::needless_pass_by_value)]
90    fn py_determine_venue(&self, py: Python<'_>, contract: Py<PyAny>) -> PyResult<String> {
91        let rust_contract = py_to_contract(contract.bind(py))?;
92        Ok(self.determine_venue(&rust_contract, None).to_string())
93    }
94
95    /// Convert an instrument ID to cached IB contract details.
96    #[pyo3(name = "instrument_id_to_ib_contract_details")]
97    fn py_instrument_id_to_ib_contract_details(
98        &self,
99        py: Python<'_>,
100        instrument_id: InstrumentId,
101    ) -> PyResult<Option<Py<PyAny>>> {
102        self.instrument_id_to_ib_contract_details(&instrument_id)
103            .as_ref()
104            .map(|details| contract_details_to_pyobject(py, details))
105            .transpose()
106    }
107
108    /// Batch load multiple instrument IDs.
109    ///
110    /// Note: This method requires an IB client which is not stored in the provider.
111    /// Use `data_client.batch_load()` or `execution_client.batch_load()` instead,
112    /// which have access to the IB client.
113    #[pyo3(name = "batch_load")]
114    #[allow(clippy::needless_pass_by_value)]
115    fn py_batch_load<'py>(
116        &self,
117        py: Python<'py>,
118        instrument_ids: Vec<InstrumentId>,
119    ) -> PyResult<Bound<'py, PyAny>> {
120        let _ = instrument_ids;
121        // NOTE: This method intentionally requires an IB client managed by data/execution clients.
122        // The functionality is available via data_client.batch_load() or execution_client.batch_load()
123        pyo3_async_runtimes::tokio::future_into_py(py, async move {
124            Err::<usize, _>(to_pyruntime_err(
125                "batch_load requires an IB client. Use data_client.batch_load() or execution_client.batch_load() instead.",
126            ))
127        })
128    }
129
130    /// Fetch option chain for an underlying contract with expiry filtering.
131    ///
132    /// Note: This method requires an IB client which is not stored in the provider.
133    /// Use `data_client.fetch_option_chain_by_range()` instead, which has access to the IB client.
134    #[pyo3(signature = (underlying_symbol, expiry_min=None, expiry_max=None))]
135    fn py_fetch_option_chain_by_range<'py>(
136        &self,
137        py: Python<'py>,
138        underlying_symbol: String,
139        expiry_min: Option<String>,
140        expiry_max: Option<String>,
141    ) -> PyResult<Bound<'py, PyAny>> {
142        let _ = (underlying_symbol, expiry_min, expiry_max);
143        // NOTE: This method intentionally requires an IB client managed by data/execution clients.
144        // The functionality is available via data_client.fetch_option_chain_by_range()
145        pyo3_async_runtimes::tokio::future_into_py(py, async move {
146            Err::<usize, _>(to_pyruntime_err(
147                "fetch_option_chain_by_range requires an IB client. Use data_client.fetch_option_chain_by_range() instead.",
148            ))
149        })
150    }
151
152    /// Fetch futures chain for a given underlying symbol.
153    ///
154    /// Note: This method requires an IB client which is not stored in the provider.
155    /// Use `data_client.fetch_futures_chain()` instead, which has access to the IB client.
156    #[pyo3(signature = (underlying_symbol, expiry_min=None, expiry_max=None))]
157    fn py_fetch_futures_chain<'py>(
158        &self,
159        py: Python<'py>,
160        underlying_symbol: String,
161        expiry_min: Option<String>,
162        expiry_max: Option<String>,
163    ) -> PyResult<Bound<'py, PyAny>> {
164        let _ = (underlying_symbol, expiry_min, expiry_max);
165        // NOTE: This method intentionally requires an IB client managed by data/execution clients.
166        // The functionality is available via data_client.fetch_futures_chain()
167        pyo3_async_runtimes::tokio::future_into_py(py, async move {
168            Err::<usize, _>(to_pyruntime_err(
169                "fetch_futures_chain requires an IB client. Use data_client.fetch_futures_chain() instead.",
170            ))
171        })
172    }
173
174    /// Fetch BAG (spread) contract details.
175    ///
176    /// Note: This method requires an IB client which is not stored in the provider.
177    /// Use `data_client.fetch_bag_contract()` instead, which has access to the IB client.
178    #[pyo3(signature = (bag_contract))]
179    #[allow(clippy::needless_pass_by_value)]
180    fn py_fetch_bag_contract<'py>(
181        &self,
182        py: Python<'py>,
183        bag_contract: String, // Would need proper Contract type
184    ) -> PyResult<Bound<'py, PyAny>> {
185        let _ = bag_contract;
186        // NOTE: This method intentionally requires an IB client managed by data/execution clients.
187        // The functionality is available via data_client.fetch_bag_contract()
188        pyo3_async_runtimes::tokio::future_into_py(py, async move {
189            Err::<(), _>(to_pyruntime_err(
190                "fetch_bag_contract requires an IB client. Use data_client.fetch_bag_contract() instead.",
191            ))
192        })
193    }
194
195    /// Save the current instrument cache to disk.
196    ///
197    /// # Arguments
198    ///
199    /// * `cache_path` - Path to the cache file
200    ///
201    /// # Errors
202    ///
203    /// Returns an error if serialization or file I/O fails.
204    #[pyo3(name = "save_cache")]
205    fn py_save_cache<'py>(
206        &self,
207        py: Python<'py>,
208        cache_path: String,
209    ) -> PyResult<Bound<'py, PyAny>> {
210        let provider = self.clone();
211        pyo3_async_runtimes::tokio::future_into_py(py, async move {
212            provider
213                .save_cache(&cache_path)
214                .await
215                .map_err(to_pyruntime_err)
216        })
217    }
218
219    /// Load instrument cache from disk if valid.
220    ///
221    /// # Arguments
222    ///
223    /// * `cache_path` - Path to the cache file
224    ///
225    /// # Returns
226    ///
227    /// Returns `true` if cache was loaded successfully and is valid, `false` otherwise.
228    ///
229    /// # Errors
230    ///
231    /// Returns an error if deserialization or file I/O fails (but treats missing file as non-error).
232    #[pyo3(name = "load_cache")]
233    fn py_load_cache<'py>(
234        &self,
235        py: Python<'py>,
236        cache_path: String,
237    ) -> PyResult<Bound<'py, PyAny>> {
238        let provider = self.clone();
239        pyo3_async_runtimes::tokio::future_into_py(py, async move {
240            provider
241                .load_cache(&cache_path)
242                .await
243                .map_err(to_pyruntime_err)
244        })
245    }
246}