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 ibapi::contracts::Contract;
19use jiff::Timestamp;
20use nautilus_common::live::get_runtime;
21use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err};
22use nautilus_model::{
23    data::{Bar, Data},
24    identifiers::InstrumentId,
25    instruments::any::InstrumentAny,
26    python::{data::data_to_pyobject, instruments::instrument_any_to_pyobject},
27};
28use pyo3::{prelude::*, types::PyList};
29
30use crate::{
31    common::enums::IbHistoricalTickType, historical::HistoricalInteractiveBrokersClient,
32    python::conversion::py_list_to_contracts,
33};
34
35#[pymethods]
36#[pyo3_stub_gen::derive::gen_stub_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        get_runtime()
45            .block_on(Self::connect_with_provider(instrument_provider, config))
46            .map_err(to_pyruntime_err)
47    }
48
49    fn __repr__(&self) -> String {
50        format!("{self:?}")
51    }
52
53    /// Request historical bars.
54    ///
55    /// # Continuous futures
56    ///
57    /// Continuous futures (`CONTFUT`) reject an explicit end date/time with IB
58    /// error 10339. For these contracts the end date is dropped and only the
59    /// first duration segment is requested, anchored to the current time, so
60    /// the returned bars may fall outside `[start_date_time, end_date_time]`.
61    /// A warning is logged when the requested end date/time is in the past or
62    /// the range spans more than one duration segment.
63    ///
64    /// # Arguments
65    ///
66    /// * `bar_specifications` - List of bar specifications (e.g., ["1-HOUR-LAST"])
67    /// * `end_date_time` - End date for bars
68    /// * `start_date_time` - Optional start date
69    /// * `duration` - Optional duration string (e.g., "1 D")
70    /// * `contracts` - Optional list of IB contracts (dicts with symbol, sec_type, exchange, currency, etc.)
71    /// * `instrument_ids` - Optional list of instrument IDs
72    /// * `use_rth` - Use regular trading hours only
73    /// * `timeout` - Request timeout in seconds
74    #[pyo3(signature = (bar_specifications, end_date_time, start_date_time=None, duration=None, contracts=None, instrument_ids=None, use_rth=true, timeout=60))]
75    #[pyo3(name = "request_bars")]
76    #[allow(clippy::too_many_arguments)]
77    #[allow(clippy::needless_pass_by_value)]
78    fn py_request_bars<'py>(
79        &self,
80        py: Python<'py>,
81        bar_specifications: Vec<String>,
82        end_date_time: Timestamp,
83        start_date_time: Option<Timestamp>,
84        duration: Option<String>,
85        contracts: Option<Py<PyList>>,
86        instrument_ids: Option<Vec<InstrumentId>>,
87        use_rth: bool,
88        timeout: u64,
89    ) -> PyResult<Bound<'py, PyAny>> {
90        let client = self.clone();
91        let bar_specs = bar_specifications;
92        let duration_str = duration;
93
94        // Convert Python contracts list to Rust Contracts
95        let contracts_vec: Option<Vec<Contract>> = if let Some(py_contracts) = contracts.as_ref() {
96            let py_contracts_bound = py_contracts.bind(py);
97            match py_list_to_contracts(py_contracts_bound) {
98                Ok(contracts) => Some(contracts),
99                Err(e) => {
100                    return Err(to_pyvalue_err(format!("Failed to convert contracts: {e}")));
101                }
102            }
103        } else {
104            None
105        };
106
107        pyo3_async_runtimes::tokio::future_into_py(py, async move {
108            // Convert Vec<String> to Vec<&str> for the request
109            let bar_specs_refs: Vec<&str> = bar_specs.iter().map(|s| s.as_str()).collect();
110            let bars: Vec<Bar> = client
111                .request_bars(
112                    bar_specs_refs,
113                    end_date_time,
114                    start_date_time,
115                    duration_str.as_deref(),
116                    contracts_vec,
117                    instrument_ids,
118                    use_rth,
119                    timeout,
120                )
121                .await
122                .map_err(to_pyruntime_err)?;
123            // Convert bars to Python objects
124            Ok(bars)
125        })
126    }
127
128    /// Request historical ticks (quotes or trades).
129    ///
130    /// # Arguments
131    ///
132    /// * `tick_type` - Historical tick type.
133    /// * `start_date_time` - Start date for ticks
134    /// * `end_date_time` - End date for ticks
135    /// * `contracts` - Optional list of IB contracts (dicts with symbol, sec_type, exchange, currency, etc.)
136    /// * `instrument_ids` - Optional list of instrument IDs
137    /// * `use_rth` - Use regular trading hours only
138    /// * `timeout` - Request timeout in seconds
139    /// * `limit` - Maximum number of ticks to return, or 0 for no explicit limit
140    #[pyo3(signature = (tick_type, start_date_time, end_date_time, contracts=None, instrument_ids=None, use_rth=true, timeout=60, limit=0))]
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: IbHistoricalTickType,
148        start_date_time: Timestamp,
149        end_date_time: Timestamp,
150        contracts: Option<Py<PyList>>,
151        instrument_ids: Option<Vec<InstrumentId>>,
152        use_rth: bool,
153        timeout: u64,
154        limit: usize,
155    ) -> PyResult<Bound<'py, PyAny>> {
156        let client = self.clone();
157
158        // Convert Python contracts list to Rust Contracts
159        let contracts_vec: Option<Vec<Contract>> = if let Some(py_contracts) = contracts.as_ref() {
160            let py_contracts_bound = py_contracts.bind(py);
161            match py_list_to_contracts(py_contracts_bound) {
162                Ok(contracts) => Some(contracts),
163                Err(e) => {
164                    return Err(to_pyvalue_err(format!("Failed to convert contracts: {e}")));
165                }
166            }
167        } else {
168            None
169        };
170
171        pyo3_async_runtimes::tokio::future_into_py(py, async move {
172            let data_vec: Vec<Data> = client
173                .request_ticks(
174                    tick_type,
175                    start_date_time,
176                    end_date_time,
177                    contracts_vec,
178                    instrument_ids,
179                    use_rth,
180                    timeout,
181                    limit,
182                )
183                .await
184                .map_err(to_pyruntime_err)?;
185            Python::attach(|py| -> PyResult<Py<PyList>> {
186                let py_list = PyList::empty(py);
187                for data in data_vec {
188                    py_list.append(data_to_pyobject(py, data)?)?;
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}