Skip to main content

nautilus_kraken/python/
http_futures.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 Kraken Futures HTTP client.
17
18use chrono::{DateTime, Utc};
19use nautilus_core::python::{to_pyruntime_err, to_pyvalue_err};
20use nautilus_model::{
21    data::BarType,
22    enums::{OrderSide, OrderType, TimeInForce, TriggerType},
23    identifiers::{AccountId, ClientOrderId, InstrumentId, VenueOrderId},
24    python::instruments::{instrument_any_to_pyobject, pyobject_to_instrument_any},
25    types::{Price, Quantity},
26};
27use pyo3::{
28    conversion::IntoPyObjectExt,
29    prelude::*,
30    types::{PyDict, PyList},
31};
32
33use crate::{
34    common::{credential::KrakenCredential, enums::KrakenEnvironment},
35    http::KrakenFuturesHttpClient,
36};
37
38#[pymethods]
39#[pyo3_stub_gen::derive::gen_stub_pymethods]
40impl KrakenFuturesHttpClient {
41    /// High-level HTTP client for the Kraken Futures REST API.
42    ///
43    /// This client wraps the raw client and provides Nautilus domain types.
44    /// It maintains an instrument cache and uses it to parse venue responses
45    /// into Nautilus domain objects.
46    #[new]
47    #[pyo3(signature = (api_key=None, api_secret=None, base_url=None, demo=false, timeout_secs=60, max_retries=None, retry_delay_ms=None, retry_delay_max_ms=None, proxy_url=None, max_requests_per_second=5))]
48    #[expect(clippy::too_many_arguments)]
49    fn py_new(
50        api_key: Option<String>,
51        api_secret: Option<String>,
52        base_url: Option<String>,
53        demo: bool,
54        timeout_secs: u64,
55        max_retries: Option<u32>,
56        retry_delay_ms: Option<u64>,
57        retry_delay_max_ms: Option<u64>,
58        proxy_url: Option<String>,
59        max_requests_per_second: u32,
60    ) -> PyResult<Self> {
61        let environment = if demo {
62            KrakenEnvironment::Demo
63        } else {
64            KrakenEnvironment::Mainnet
65        };
66
67        if let Some(cred) = KrakenCredential::resolve_futures(api_key, api_secret, demo) {
68            let (k, s) = cred.into_parts();
69            Self::with_credentials(
70                k,
71                s,
72                environment,
73                base_url,
74                timeout_secs,
75                max_retries,
76                retry_delay_ms,
77                retry_delay_max_ms,
78                proxy_url,
79                max_requests_per_second,
80            )
81            .map_err(to_pyvalue_err)
82        } else {
83            Self::new(
84                environment,
85                base_url,
86                timeout_secs,
87                max_retries,
88                retry_delay_ms,
89                retry_delay_max_ms,
90                proxy_url,
91                max_requests_per_second,
92            )
93            .map_err(to_pyvalue_err)
94        }
95    }
96
97    #[getter]
98    #[pyo3(name = "base_url")]
99    #[must_use]
100    pub fn py_base_url(&self) -> String {
101        self.inner.base_url().to_string()
102    }
103
104    #[getter]
105    #[pyo3(name = "api_key")]
106    #[must_use]
107    pub fn py_api_key(&self) -> Option<&str> {
108        self.inner.credential().map(|c| c.api_key())
109    }
110
111    #[getter]
112    #[pyo3(name = "api_key_masked")]
113    #[must_use]
114    pub fn py_api_key_masked(&self) -> Option<String> {
115        self.inner.credential().map(|c| c.api_key_masked())
116    }
117
118    /// Caches an instrument for symbol lookup.
119    #[pyo3(name = "cache_instrument")]
120    fn py_cache_instrument(&self, py: Python, instrument: Py<PyAny>) -> PyResult<()> {
121        let inst_any = pyobject_to_instrument_any(py, instrument)?;
122        self.cache_instrument(inst_any);
123        Ok(())
124    }
125
126    /// Cancels all pending HTTP requests.
127    #[pyo3(name = "cancel_all_requests")]
128    fn py_cancel_all_requests(&self) {
129        self.cancel_all_requests();
130    }
131
132    /// Requests tradable instruments from Kraken Futures.
133    #[pyo3(name = "request_instruments")]
134    fn py_request_instruments<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
135        let client = self.clone();
136
137        pyo3_async_runtimes::tokio::future_into_py(py, async move {
138            let instruments = client
139                .request_instruments()
140                .await
141                .map_err(to_pyruntime_err)?;
142
143            Python::attach(|py| {
144                let py_instruments: PyResult<Vec<_>> = instruments
145                    .into_iter()
146                    .map(|inst| instrument_any_to_pyobject(py, inst))
147                    .collect();
148                let pylist = PyList::new(py, py_instruments?).unwrap();
149                Ok(pylist.unbind())
150            })
151        })
152    }
153
154    /// Requests the current market status for Kraken Futures instruments.
155    #[pyo3(name = "request_instrument_statuses")]
156    fn py_request_instrument_statuses<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
157        let client = self.clone();
158
159        pyo3_async_runtimes::tokio::future_into_py(py, async move {
160            let statuses = client
161                .request_instrument_statuses()
162                .await
163                .map_err(to_pyruntime_err)?;
164
165            Python::attach(|py| {
166                let dict = PyDict::new(py);
167                for (instrument_id, action) in statuses {
168                    dict.set_item(
169                        instrument_id.into_bound_py_any(py)?,
170                        action.into_bound_py_any(py)?,
171                    )?;
172                }
173                Ok(dict.into_any().unbind())
174            })
175        })
176    }
177
178    #[pyo3(name = "request_trades")]
179    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
180    fn py_request_trades<'py>(
181        &self,
182        py: Python<'py>,
183        instrument_id: InstrumentId,
184        start: Option<DateTime<Utc>>,
185        end: Option<DateTime<Utc>>,
186        limit: Option<u64>,
187    ) -> PyResult<Bound<'py, PyAny>> {
188        let client = self.clone();
189
190        pyo3_async_runtimes::tokio::future_into_py(py, async move {
191            let trades = client
192                .request_trades(instrument_id, start, end, limit)
193                .await
194                .map_err(to_pyruntime_err)?;
195
196            Python::attach(|py| {
197                let py_trades: PyResult<Vec<_>> = trades
198                    .into_iter()
199                    .map(|trade| trade.into_py_any(py))
200                    .collect();
201                let pylist = PyList::new(py, py_trades?).unwrap().into_any().unbind();
202                Ok(pylist)
203            })
204        })
205    }
206
207    /// Requests the mark price for an instrument.
208    #[pyo3(name = "request_mark_price")]
209    fn py_request_mark_price<'py>(
210        &self,
211        py: Python<'py>,
212        instrument_id: InstrumentId,
213    ) -> PyResult<Bound<'py, PyAny>> {
214        let client = self.clone();
215
216        pyo3_async_runtimes::tokio::future_into_py(py, async move {
217            let mark_price = client
218                .request_mark_price(instrument_id)
219                .await
220                .map_err(to_pyruntime_err)?;
221
222            Ok(mark_price)
223        })
224    }
225
226    #[pyo3(name = "request_index_price")]
227    fn py_request_index_price<'py>(
228        &self,
229        py: Python<'py>,
230        instrument_id: InstrumentId,
231    ) -> PyResult<Bound<'py, PyAny>> {
232        let client = self.clone();
233
234        pyo3_async_runtimes::tokio::future_into_py(py, async move {
235            let index_price = client
236                .request_index_price(instrument_id)
237                .await
238                .map_err(to_pyruntime_err)?;
239
240            Ok(index_price)
241        })
242    }
243
244    /// Requests an order book snapshot for a futures instrument.
245    #[pyo3(name = "request_book_snapshot")]
246    #[pyo3(signature = (instrument_id, depth=None))]
247    fn py_request_book_snapshot<'py>(
248        &self,
249        py: Python<'py>,
250        instrument_id: InstrumentId,
251        depth: Option<u32>,
252    ) -> PyResult<Bound<'py, PyAny>> {
253        let client = self.clone();
254
255        pyo3_async_runtimes::tokio::future_into_py(py, async move {
256            let book = client
257                .request_book_snapshot(instrument_id, depth)
258                .await
259                .map_err(to_pyruntime_err)?;
260
261            Python::attach(|py| book.into_py_any(py))
262        })
263    }
264
265    #[pyo3(name = "request_bars")]
266    #[pyo3(signature = (bar_type, start=None, end=None, limit=None))]
267    fn py_request_bars<'py>(
268        &self,
269        py: Python<'py>,
270        bar_type: BarType,
271        start: Option<DateTime<Utc>>,
272        end: Option<DateTime<Utc>>,
273        limit: Option<u64>,
274    ) -> PyResult<Bound<'py, PyAny>> {
275        let client = self.clone();
276
277        pyo3_async_runtimes::tokio::future_into_py(py, async move {
278            let bars = client
279                .request_bars(bar_type, start, end, limit)
280                .await
281                .map_err(to_pyruntime_err)?;
282
283            Python::attach(|py| {
284                let py_bars: PyResult<Vec<_>> =
285                    bars.into_iter().map(|bar| bar.into_py_any(py)).collect();
286                let pylist = PyList::new(py, py_bars?).unwrap().into_any().unbind();
287                Ok(pylist)
288            })
289        })
290    }
291
292    /// Requests account state from the Kraken Futures exchange.
293    ///
294    /// This queries the accounts endpoint and converts the response into a
295    /// Nautilus `AccountState` event containing balances and margin info.
296    #[pyo3(name = "request_account_state")]
297    fn py_request_account_state<'py>(
298        &self,
299        py: Python<'py>,
300        account_id: AccountId,
301    ) -> PyResult<Bound<'py, PyAny>> {
302        let client = self.clone();
303
304        pyo3_async_runtimes::tokio::future_into_py(py, async move {
305            let account_state = client
306                .request_account_state(account_id)
307                .await
308                .map_err(to_pyruntime_err)?;
309
310            Python::attach(|py| account_state.into_pyobject(py).map(|o| o.unbind()))
311        })
312    }
313
314    #[pyo3(name = "request_order_status_reports")]
315    #[pyo3(signature = (account_id, instrument_id=None, start=None, end=None, open_only=false))]
316    fn py_request_order_status_reports<'py>(
317        &self,
318        py: Python<'py>,
319        account_id: AccountId,
320        instrument_id: Option<InstrumentId>,
321        start: Option<DateTime<Utc>>,
322        end: Option<DateTime<Utc>>,
323        open_only: bool,
324    ) -> PyResult<Bound<'py, PyAny>> {
325        let client = self.clone();
326
327        pyo3_async_runtimes::tokio::future_into_py(py, async move {
328            let reports = client
329                .request_order_status_reports(account_id, instrument_id, start, end, open_only)
330                .await
331                .map_err(to_pyruntime_err)?;
332
333            Python::attach(|py| {
334                let py_reports: PyResult<Vec<_>> = reports
335                    .into_iter()
336                    .map(|report| report.into_py_any(py))
337                    .collect();
338                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
339                Ok(pylist)
340            })
341        })
342    }
343
344    #[pyo3(name = "request_fill_reports")]
345    #[pyo3(signature = (account_id, instrument_id=None, start=None, end=None))]
346    fn py_request_fill_reports<'py>(
347        &self,
348        py: Python<'py>,
349        account_id: AccountId,
350        instrument_id: Option<InstrumentId>,
351        start: Option<DateTime<Utc>>,
352        end: Option<DateTime<Utc>>,
353    ) -> PyResult<Bound<'py, PyAny>> {
354        let client = self.clone();
355
356        pyo3_async_runtimes::tokio::future_into_py(py, async move {
357            let reports = client
358                .request_fill_reports(account_id, instrument_id, start, end)
359                .await
360                .map_err(to_pyruntime_err)?;
361
362            Python::attach(|py| {
363                let py_reports: PyResult<Vec<_>> = reports
364                    .into_iter()
365                    .map(|report| report.into_py_any(py))
366                    .collect();
367                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
368                Ok(pylist)
369            })
370        })
371    }
372
373    #[pyo3(name = "request_position_status_reports")]
374    #[pyo3(signature = (account_id, instrument_id=None))]
375    fn py_request_position_status_reports<'py>(
376        &self,
377        py: Python<'py>,
378        account_id: AccountId,
379        instrument_id: Option<InstrumentId>,
380    ) -> PyResult<Bound<'py, PyAny>> {
381        let client = self.clone();
382
383        pyo3_async_runtimes::tokio::future_into_py(py, async move {
384            let reports = client
385                .request_position_status_reports(account_id, instrument_id)
386                .await
387                .map_err(to_pyruntime_err)?;
388
389            Python::attach(|py| {
390                let py_reports: PyResult<Vec<_>> = reports
391                    .into_iter()
392                    .map(|report| report.into_py_any(py))
393                    .collect();
394                let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
395                Ok(pylist)
396            })
397        })
398    }
399
400    /// Submits a new order to the Kraken Futures exchange.
401    #[pyo3(name = "submit_order")]
402    #[pyo3(signature = (account_id, instrument_id, client_order_id, order_side, order_type, quantity, time_in_force, price=None, trigger_price=None, trigger_type=None, reduce_only=false, post_only=false))]
403    #[expect(clippy::too_many_arguments)]
404    fn py_submit_order<'py>(
405        &self,
406        py: Python<'py>,
407        account_id: AccountId,
408        instrument_id: InstrumentId,
409        client_order_id: ClientOrderId,
410        order_side: OrderSide,
411        order_type: OrderType,
412        quantity: Quantity,
413        time_in_force: TimeInForce,
414        price: Option<Price>,
415        trigger_price: Option<Price>,
416        trigger_type: Option<TriggerType>,
417        reduce_only: bool,
418        post_only: bool,
419    ) -> PyResult<Bound<'py, PyAny>> {
420        let client = self.clone();
421
422        pyo3_async_runtimes::tokio::future_into_py(py, async move {
423            let report = client
424                .submit_order(
425                    account_id,
426                    instrument_id,
427                    client_order_id,
428                    order_side,
429                    order_type,
430                    quantity,
431                    time_in_force,
432                    price,
433                    trigger_price,
434                    trigger_type,
435                    reduce_only,
436                    post_only,
437                )
438                .await
439                .map_err(to_pyruntime_err)?;
440
441            Python::attach(|py| report.into_pyobject(py).map(|o| o.unbind()))
442        })
443    }
444
445    /// Modifies an existing order on the Kraken Futures exchange.
446    ///
447    /// Returns the new venue order ID assigned to the modified order.
448    #[pyo3(name = "modify_order")]
449    #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None, quantity=None, price=None, trigger_price=None))]
450    #[expect(clippy::too_many_arguments)]
451    fn py_modify_order<'py>(
452        &self,
453        py: Python<'py>,
454        instrument_id: InstrumentId,
455        client_order_id: Option<ClientOrderId>,
456        venue_order_id: Option<VenueOrderId>,
457        quantity: Option<Quantity>,
458        price: Option<Price>,
459        trigger_price: Option<Price>,
460    ) -> PyResult<Bound<'py, PyAny>> {
461        let client = self.clone();
462
463        pyo3_async_runtimes::tokio::future_into_py(py, async move {
464            let new_venue_order_id = client
465                .modify_order(
466                    instrument_id,
467                    client_order_id,
468                    venue_order_id,
469                    quantity,
470                    price,
471                    trigger_price,
472                )
473                .await
474                .map_err(to_pyruntime_err)?;
475
476            Python::attach(|py| new_venue_order_id.into_pyobject(py).map(|o| o.unbind()))
477        })
478    }
479
480    /// Cancels an order on the Kraken Futures exchange.
481    #[pyo3(name = "cancel_order")]
482    #[pyo3(signature = (account_id, instrument_id, client_order_id=None, venue_order_id=None))]
483    fn py_cancel_order<'py>(
484        &self,
485        py: Python<'py>,
486        account_id: AccountId,
487        instrument_id: InstrumentId,
488        client_order_id: Option<ClientOrderId>,
489        venue_order_id: Option<VenueOrderId>,
490    ) -> PyResult<Bound<'py, PyAny>> {
491        let client = self.clone();
492
493        pyo3_async_runtimes::tokio::future_into_py(py, async move {
494            client
495                .cancel_order(account_id, instrument_id, client_order_id, venue_order_id)
496                .await
497                .map_err(to_pyruntime_err)
498        })
499    }
500
501    #[pyo3(name = "cancel_all_orders")]
502    #[pyo3(signature = (instrument_id=None))]
503    fn py_cancel_all_orders<'py>(
504        &self,
505        py: Python<'py>,
506        instrument_id: Option<InstrumentId>,
507    ) -> PyResult<Bound<'py, PyAny>> {
508        let client = self.clone();
509
510        pyo3_async_runtimes::tokio::future_into_py(py, async move {
511            let symbol = instrument_id.map(|id| id.symbol.to_string());
512            let response = client
513                .inner
514                .cancel_all_orders(symbol)
515                .await
516                .map_err(to_pyruntime_err)?;
517
518            Ok(response.cancel_status.cancelled_orders.len())
519        })
520    }
521
522    /// Cancels multiple orders on the Kraken Futures exchange.
523    ///
524    /// Automatically chunks requests into batches of 50 orders.
525    ///
526    /// # Parameters
527    /// - `venue_order_ids` - List of venue order IDs to cancel.
528    ///
529    /// # Returns
530    /// The total number of successfully cancelled orders.
531    #[pyo3(name = "cancel_orders_batch")]
532    fn py_cancel_orders_batch<'py>(
533        &self,
534        py: Python<'py>,
535        venue_order_ids: Vec<VenueOrderId>,
536    ) -> PyResult<Bound<'py, PyAny>> {
537        let client = self.clone();
538
539        pyo3_async_runtimes::tokio::future_into_py(py, async move {
540            client
541                .cancel_orders_batch(venue_order_ids)
542                .await
543                .map_err(to_pyruntime_err)
544        })
545    }
546}
547
548// Separate block to avoid pyo3_stub_gen trait bound issues with batch-order tuples.
549// Stub is maintained manually in nautilus_pyo3.pyi.
550#[pymethods]
551impl KrakenFuturesHttpClient {
552    /// Submits multiple orders in a single batch request.
553    ///
554    /// Builds batch send items from order parameters, chunks at the batch limit,
555    /// and returns per-item send statuses.
556    #[pyo3(name = "submit_orders_batch")]
557    #[expect(clippy::type_complexity)]
558    fn py_submit_orders_batch<'py>(
559        &self,
560        py: Python<'py>,
561        orders: Vec<(
562            InstrumentId,
563            ClientOrderId,
564            OrderSide,
565            OrderType,
566            Quantity,
567            TimeInForce,
568            Option<Price>,
569            Option<Price>,
570            Option<TriggerType>,
571            bool,
572            bool,
573        )>,
574    ) -> PyResult<Bound<'py, PyAny>> {
575        let client = self.clone();
576
577        pyo3_async_runtimes::tokio::future_into_py(py, async move {
578            let statuses = client
579                .submit_orders_batch(orders)
580                .await
581                .map_err(to_pyruntime_err)?;
582
583            let result: Vec<String> = statuses.into_iter().map(|s| s.status).collect();
584            Ok(result)
585        })
586    }
587
588    /// Modifies multiple orders in a single batch request.
589    #[expect(clippy::type_complexity)]
590    #[pyo3(name = "edit_orders_batch")]
591    fn py_edit_orders_batch<'py>(
592        &self,
593        py: Python<'py>,
594        orders: Vec<(
595            InstrumentId,
596            Option<ClientOrderId>,
597            Option<VenueOrderId>,
598            Option<Quantity>,
599            Option<Price>,
600            Option<Price>,
601        )>,
602    ) -> PyResult<Bound<'py, PyAny>> {
603        let client = self.clone();
604
605        pyo3_async_runtimes::tokio::future_into_py(py, async move {
606            client
607                .edit_orders_batch(orders)
608                .await
609                .map_err(to_pyruntime_err)
610        })
611    }
612}