Skip to main content

nautilus_interactive_brokers/python/
historical.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 historical data client.
17
18use std::sync::Arc;
19
20use chrono::{DateTime, Utc};
21use ibapi::contracts::Contract;
22use nautilus_common::live::get_runtime;
23use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err};
24use nautilus_model::{
25    data::{Bar, Data},
26    identifiers::InstrumentId,
27    instruments::any::InstrumentAny,
28    python::{data::data_to_pycapsule, instruments::instrument_any_to_pyobject},
29};
30use pyo3::{prelude::*, types::PyList};
31
32use crate::{
33    historical::HistoricalInteractiveBrokersClient, python::conversion::py_list_to_contracts,
34};
35
36#[pymethods]
37impl HistoricalInteractiveBrokersClient {
38    #[new]
39    #[allow(clippy::needless_pass_by_value)]
40    fn py_new(
41        instrument_provider: crate::providers::instruments::InteractiveBrokersInstrumentProvider,
42        config: crate::config::InteractiveBrokersDataClientConfig,
43    ) -> PyResult<Self> {
44        let shared_client = get_runtime()
45            .block_on(crate::common::shared_client::get_or_connect(
46                &config.host,
47                config.port,
48                config.client_id,
49                config.connection_timeout,
50            ))
51            .map_err(to_pyruntime_err)?;
52
53        Ok(Self::new(
54            Arc::clone(shared_client.as_arc()),
55            Arc::new(instrument_provider),
56        ))
57    }
58
59    fn __repr__(&self) -> String {
60        format!("{self:?}")
61    }
62
63    /// Request historical bars.
64    ///
65    /// # Arguments
66    ///
67    /// * `bar_specifications` - List of bar specifications (e.g., ["1-HOUR-LAST"])
68    /// * `end_date_time` - End date for bars
69    /// * `start_date_time` - Optional start date
70    /// * `duration` - Optional duration string (e.g., "1 D")
71    /// * `contracts` - Optional list of IB contracts (dicts with symbol, sec_type, exchange, currency, etc.)
72    /// * `instrument_ids` - Optional list of instrument IDs
73    /// * `use_rth` - Use regular trading hours only
74    /// * `timeout` - Request timeout in seconds
75    #[pyo3(signature = (bar_specifications, end_date_time, start_date_time=None, duration=None, contracts=None, instrument_ids=None, use_rth=true, timeout=60))]
76    #[pyo3(name = "request_bars")]
77    #[allow(clippy::too_many_arguments)]
78    #[allow(clippy::needless_pass_by_value)]
79    fn py_request_bars<'py>(
80        &self,
81        py: Python<'py>,
82        bar_specifications: Vec<String>,
83        end_date_time: DateTime<Utc>,
84        start_date_time: Option<DateTime<Utc>>,
85        duration: Option<String>,
86        contracts: Option<Py<PyList>>,
87        instrument_ids: Option<Vec<InstrumentId>>,
88        use_rth: bool,
89        timeout: u64,
90    ) -> PyResult<Bound<'py, PyAny>> {
91        let client = self.clone();
92        let bar_specs = bar_specifications;
93        let duration_str = duration;
94
95        // Convert Python contracts list to Rust Contracts
96        let contracts_vec: Option<Vec<Contract>> = if let Some(py_contracts) = contracts.as_ref() {
97            let py_contracts_bound = py_contracts.bind(py);
98            match py_list_to_contracts(py_contracts_bound) {
99                Ok(contracts) => Some(contracts),
100                Err(e) => {
101                    return Err(to_pyvalue_err(format!("Failed to convert contracts: {e}")));
102                }
103            }
104        } else {
105            None
106        };
107
108        pyo3_async_runtimes::tokio::future_into_py(py, async move {
109            // Convert Vec<String> to Vec<&str> for the request
110            let bar_specs_refs: Vec<&str> = bar_specs.iter().map(|s| s.as_str()).collect();
111            let bars: Vec<Bar> = client
112                .request_bars(
113                    bar_specs_refs,
114                    end_date_time,
115                    start_date_time,
116                    duration_str.as_deref(),
117                    contracts_vec,
118                    instrument_ids,
119                    use_rth,
120                    timeout,
121                )
122                .await
123                .map_err(to_pyruntime_err)?;
124            // Convert bars to Python objects
125            Ok(bars)
126        })
127    }
128
129    /// Request historical ticks (quotes or trades).
130    ///
131    /// # Arguments
132    ///
133    /// * `tick_type` - Type of ticks: "TRADES" or "BID_ASK"
134    /// * `start_date_time` - Start date for ticks
135    /// * `end_date_time` - End date for ticks
136    /// * `contracts` - Optional list of IB contracts (dicts with symbol, sec_type, exchange, currency, etc.)
137    /// * `instrument_ids` - Optional list of instrument IDs
138    /// * `use_rth` - Use regular trading hours only
139    /// * `timeout` - Request timeout in seconds
140    #[pyo3(signature = (tick_type, start_date_time, end_date_time, contracts=None, instrument_ids=None, use_rth=true, timeout=60))]
141    #[pyo3(name = "request_ticks")]
142    #[allow(clippy::too_many_arguments)]
143    #[allow(clippy::needless_pass_by_value)]
144    fn py_request_ticks<'py>(
145        &self,
146        py: Python<'py>,
147        tick_type: String,
148        start_date_time: DateTime<Utc>,
149        end_date_time: DateTime<Utc>,
150        contracts: Option<Py<PyList>>,
151        instrument_ids: Option<Vec<InstrumentId>>,
152        use_rth: bool,
153        timeout: u64,
154    ) -> PyResult<Bound<'py, PyAny>> {
155        let client = self.clone();
156
157        // Convert Python contracts list to Rust Contracts
158        let contracts_vec: Option<Vec<Contract>> = if let Some(py_contracts) = contracts.as_ref() {
159            let py_contracts_bound = py_contracts.bind(py);
160            match py_list_to_contracts(py_contracts_bound) {
161                Ok(contracts) => Some(contracts),
162                Err(e) => {
163                    return Err(to_pyvalue_err(format!("Failed to convert contracts: {e}")));
164                }
165            }
166        } else {
167            None
168        };
169
170        pyo3_async_runtimes::tokio::future_into_py(py, async move {
171            let data_vec: Vec<Data> = client
172                .request_ticks(
173                    &tick_type,
174                    start_date_time,
175                    end_date_time,
176                    contracts_vec,
177                    instrument_ids,
178                    use_rth,
179                    timeout,
180                )
181                .await
182                .map_err(to_pyruntime_err)?;
183            // Convert Data enum to Python objects using pycapsules
184            Python::attach(|py| -> PyResult<Py<PyList>> {
185                let py_list = PyList::empty(py);
186                for data in data_vec {
187                    let py_capsule = data_to_pycapsule(py, data);
188                    py_list.append(py_capsule)?;
189                }
190                Ok(py_list.into())
191            })
192        })
193    }
194
195    /// Request instruments.
196    ///
197    /// # Arguments
198    ///
199    /// * `instrument_ids` - Optional list of instrument IDs to load
200    /// * `contracts` - Optional list of IB contracts (dicts with symbol, sec_type, exchange, currency, etc.)
201    #[pyo3(signature = (instrument_ids=None, contracts=None))]
202    #[pyo3(name = "request_instruments")]
203    #[allow(clippy::needless_pass_by_value)]
204    fn py_request_instruments<'py>(
205        &self,
206        py: Python<'py>,
207        instrument_ids: Option<Vec<InstrumentId>>,
208        contracts: Option<Py<PyList>>,
209    ) -> PyResult<Bound<'py, PyAny>> {
210        let client = self.clone();
211
212        // Convert Python contracts list to Rust Contracts
213        let contracts_vec: Option<Vec<Contract>> = if let Some(py_contracts) = contracts.as_ref() {
214            let py_contracts_bound = py_contracts.bind(py);
215            match py_list_to_contracts(py_contracts_bound) {
216                Ok(contracts) => Some(contracts),
217                Err(e) => {
218                    return Err(to_pyvalue_err(format!("Failed to convert contracts: {e}")));
219                }
220            }
221        } else {
222            None
223        };
224
225        pyo3_async_runtimes::tokio::future_into_py(py, async move {
226            let instruments: Vec<InstrumentAny> = client
227                .request_instruments(instrument_ids, contracts_vec)
228                .await
229                .map_err(to_pyruntime_err)?;
230            // Convert instruments to Python objects
231            Python::attach(|py| -> PyResult<Py<PyList>> {
232                let py_list = PyList::empty(py);
233
234                for instrument in instruments {
235                    let py_obj =
236                        instrument_any_to_pyobject(py, instrument).map_err(to_pyruntime_err)?;
237                    py_list.append(py_obj)?;
238                }
239                Ok(py_list.into())
240            })
241        })
242    }
243}