nautilus_kraken/python/
http.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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 Kraken HTTP client.
17
18use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err};
19use nautilus_model::{
20    data::{BarType, Data},
21    identifiers::InstrumentId,
22    python::{
23        data::data_to_pycapsule,
24        instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
25    },
26};
27use pyo3::{prelude::*, types::PyList};
28
29use crate::http::{client::KrakenHttpClient, error::KrakenHttpError};
30
31#[pymethods]
32impl KrakenHttpClient {
33    #[new]
34    #[pyo3(signature = (api_key=None, api_secret=None, base_url=None, timeout_secs=None, max_retries=None, retry_delay_ms=None, retry_delay_max_ms=None, proxy_url=None))]
35    #[allow(clippy::too_many_arguments)]
36    fn py_new(
37        api_key: Option<String>,
38        api_secret: Option<String>,
39        base_url: Option<String>,
40        timeout_secs: Option<u64>,
41        max_retries: Option<u32>,
42        retry_delay_ms: Option<u64>,
43        retry_delay_max_ms: Option<u64>,
44        proxy_url: Option<String>,
45    ) -> PyResult<Self> {
46        let timeout = timeout_secs.or(Some(60));
47
48        // Try to get credentials from parameters or environment variables
49        let key = api_key.or_else(|| std::env::var("KRAKEN_API_KEY").ok());
50        let secret = api_secret.or_else(|| std::env::var("KRAKEN_API_SECRET").ok());
51
52        if let (Some(k), Some(s)) = (key, secret) {
53            Self::with_credentials(
54                k,
55                s,
56                base_url,
57                timeout,
58                max_retries,
59                retry_delay_ms,
60                retry_delay_max_ms,
61                proxy_url,
62            )
63            .map_err(to_pyvalue_err)
64        } else {
65            Self::new(
66                base_url,
67                timeout,
68                max_retries,
69                retry_delay_ms,
70                retry_delay_max_ms,
71                proxy_url,
72            )
73            .map_err(to_pyvalue_err)
74        }
75    }
76
77    #[getter]
78    #[pyo3(name = "base_url")]
79    #[must_use]
80    pub fn py_base_url(&self) -> String {
81        self.inner.base_url().to_string()
82    }
83
84    #[getter]
85    #[pyo3(name = "api_key")]
86    #[must_use]
87    pub fn py_api_key(&self) -> Option<&str> {
88        self.inner.credential().map(|c| c.api_key())
89    }
90
91    #[getter]
92    #[pyo3(name = "api_key_masked")]
93    #[must_use]
94    pub fn py_api_key_masked(&self) -> Option<String> {
95        self.inner.credential().map(|c| c.api_key_masked())
96    }
97
98    #[pyo3(name = "cache_instrument")]
99    fn py_cache_instrument(&self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
100        let inst_any = pyobject_to_instrument_any(py, instrument)?;
101        self.cache_instrument(inst_any);
102        Ok(())
103    }
104
105    #[pyo3(name = "cancel_all_requests")]
106    fn py_cancel_all_requests(&self) {
107        self.cancel_all_requests();
108    }
109
110    #[pyo3(name = "get_server_time")]
111    fn py_get_server_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
112        let client = self.clone();
113
114        pyo3_async_runtimes::tokio::future_into_py(py, async move {
115            let server_time = client
116                .inner
117                .get_server_time()
118                .await
119                .map_err(to_pyruntime_err)?;
120
121            let json_string = serde_json::to_string(&server_time)
122                .map_err(|e| to_pyruntime_err(format!("Failed to serialize response: {e}")))?;
123
124            Ok(json_string)
125        })
126    }
127
128    #[pyo3(name = "request_mark_price")]
129    fn py_request_mark_price<'py>(
130        &self,
131        py: Python<'py>,
132        instrument_id: InstrumentId,
133    ) -> PyResult<Bound<'py, PyAny>> {
134        let client = self.clone();
135
136        pyo3_async_runtimes::tokio::future_into_py(py, async move {
137            let mark_price = client
138                .request_mark_price(instrument_id)
139                .await
140                .map_err(to_pyruntime_err)?;
141
142            Ok(mark_price)
143        })
144    }
145
146    #[pyo3(name = "request_index_price")]
147    fn py_request_index_price<'py>(
148        &self,
149        py: Python<'py>,
150        instrument_id: InstrumentId,
151    ) -> PyResult<Bound<'py, PyAny>> {
152        let client = self.clone();
153
154        pyo3_async_runtimes::tokio::future_into_py(py, async move {
155            let index_price = client
156                .request_index_price(instrument_id)
157                .await
158                .map_err(to_pyruntime_err)?;
159
160            Ok(index_price)
161        })
162    }
163
164    #[pyo3(name = "request_instruments")]
165    #[pyo3(signature = (pairs=None))]
166    fn py_request_instruments<'py>(
167        &self,
168        py: Python<'py>,
169        pairs: Option<Vec<String>>,
170    ) -> PyResult<Bound<'py, PyAny>> {
171        let client = self.clone();
172
173        pyo3_async_runtimes::tokio::future_into_py(py, async move {
174            let instruments = client
175                .request_instruments(pairs)
176                .await
177                .map_err(to_pyruntime_err)?;
178
179            Python::attach(|py| {
180                let py_instruments: PyResult<Vec<_>> = instruments
181                    .into_iter()
182                    .map(|inst| instrument_any_to_pyobject(py, inst))
183                    .collect();
184                let pylist = PyList::new(py, py_instruments?).unwrap();
185                Ok(pylist.unbind())
186            })
187        })
188    }
189
190    #[pyo3(name = "request_trades")]
191    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
192    fn py_request_trades<'py>(
193        &self,
194        py: Python<'py>,
195        instrument_id: InstrumentId,
196        start: Option<u64>,
197        end: Option<u64>,
198        limit: Option<u64>,
199    ) -> PyResult<Bound<'py, PyAny>> {
200        let client = self.clone();
201
202        pyo3_async_runtimes::tokio::future_into_py(py, async move {
203            let trades = client
204                .request_trades(instrument_id, start, end, limit)
205                .await
206                .map_err(to_pyruntime_err)?;
207
208            Python::attach(|py| {
209                let py_trades: Vec<_> = trades
210                    .into_iter()
211                    .map(|trade| data_to_pycapsule(py, Data::Trade(trade)))
212                    .collect();
213                let pylist = PyList::new(py, py_trades).unwrap();
214                Ok(pylist.unbind())
215            })
216        })
217    }
218
219    #[pyo3(name = "request_bars")]
220    #[pyo3(signature = (bar_type, start=None, end=None, limit=None))]
221    fn py_request_bars<'py>(
222        &self,
223        py: Python<'py>,
224        bar_type: BarType,
225        start: Option<u64>,
226        end: Option<u64>,
227        limit: Option<u64>,
228    ) -> PyResult<Bound<'py, PyAny>> {
229        let client = self.clone();
230
231        pyo3_async_runtimes::tokio::future_into_py(py, async move {
232            let bars = client
233                .request_bars(bar_type, start, end, limit)
234                .await
235                .map_err(to_pyruntime_err)?;
236
237            Python::attach(|py| {
238                let py_bars: Vec<_> = bars
239                    .into_iter()
240                    .map(|bar| data_to_pycapsule(py, Data::Bar(bar)))
241                    .collect();
242                let pylist = PyList::new(py, py_bars).unwrap();
243                Ok(pylist.unbind())
244            })
245        })
246    }
247
248    #[pyo3(name = "request_bars_with_tick_type")]
249    #[pyo3(signature = (bar_type, start=None, end=None, limit=None, tick_type=None))]
250    fn py_request_bars_with_tick_type<'py>(
251        &self,
252        py: Python<'py>,
253        bar_type: BarType,
254        start: Option<u64>,
255        end: Option<u64>,
256        limit: Option<u64>,
257        tick_type: Option<String>,
258    ) -> PyResult<Bound<'py, PyAny>> {
259        let client = self.clone();
260
261        pyo3_async_runtimes::tokio::future_into_py(py, async move {
262            let tick_type_ref = tick_type.as_deref();
263            let bars = client
264                .request_bars_with_tick_type(bar_type, start, end, limit, tick_type_ref)
265                .await
266                .map_err(to_pyruntime_err)?;
267
268            Python::attach(|py| {
269                let py_bars: Vec<_> = bars
270                    .into_iter()
271                    .map(|bar| data_to_pycapsule(py, Data::Bar(bar)))
272                    .collect();
273                let pylist = PyList::new(py, py_bars).unwrap();
274                Ok(pylist.unbind())
275            })
276        })
277    }
278}
279
280impl From<KrakenHttpError> for PyErr {
281    fn from(error: KrakenHttpError) -> Self {
282        match error {
283            // Runtime/operational errors
284            KrakenHttpError::NetworkError(msg) => to_pyruntime_err(format!("Network error: {msg}")),
285            KrakenHttpError::ApiError(errors) => {
286                to_pyruntime_err(format!("API error: {}", errors.join(", ")))
287            }
288            // Validation/configuration errors
289            KrakenHttpError::MissingCredentials => {
290                to_pyvalue_err("Missing credentials for authenticated request")
291            }
292            KrakenHttpError::AuthenticationError(msg) => {
293                to_pyvalue_err(format!("Authentication error: {msg}"))
294            }
295            KrakenHttpError::ParseError(msg) => to_pyvalue_err(format!("Parse error: {msg}")),
296        }
297    }
298}