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 jiff::Timestamp;
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::Live
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 the complete tradable instrument catalog from Kraken Futures.
133    ///
134    /// # Errors
135    ///
136    /// Returns an error if the underlying request fails or any instrument definition cannot be
137    /// parsed. An instrument parse failure returns `KrakenHttpError.ParseError` without a
138    /// partial catalog.
139    #[pyo3(name = "request_instruments")]
140    fn py_request_instruments<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
141        let client = self.clone();
142
143        pyo3_async_runtimes::tokio::future_into_py(py, async move {
144            let instruments = client
145                .request_instruments()
146                .await
147                .map_err(to_pyruntime_err)?;
148
149            Python::attach(|py| {
150                let py_instruments: PyResult<Vec<_>> = instruments
151                    .into_iter()
152                    .map(|inst| instrument_any_to_pyobject(py, inst))
153                    .collect();
154                let pylist = PyList::new(py, py_instruments?)?;
155                Ok(pylist.unbind())
156            })
157        })
158    }
159
160    /// Requests the current market status for Kraken Futures instruments.
161    #[pyo3(name = "request_instrument_statuses")]
162    fn py_request_instrument_statuses<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
163        let client = self.clone();
164
165        pyo3_async_runtimes::tokio::future_into_py(py, async move {
166            let statuses = client
167                .request_instrument_statuses()
168                .await
169                .map_err(to_pyruntime_err)?;
170
171            Python::attach(|py| {
172                let dict = PyDict::new(py);
173                for (instrument_id, action) in statuses {
174                    dict.set_item(
175                        instrument_id.into_bound_py_any(py)?,
176                        action.into_bound_py_any(py)?,
177                    )?;
178                }
179                Ok(dict.into_any().unbind())
180            })
181        })
182    }
183
184    #[pyo3(name = "request_trades")]
185    #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
186    fn py_request_trades<'py>(
187        &self,
188        py: Python<'py>,
189        instrument_id: InstrumentId,
190        start: Option<Timestamp>,
191        end: Option<Timestamp>,
192        limit: Option<u64>,
193    ) -> PyResult<Bound<'py, PyAny>> {
194        let client = self.clone();
195
196        pyo3_async_runtimes::tokio::future_into_py(py, async move {
197            let trades = client
198                .request_trades(instrument_id, start, end, limit)
199                .await
200                .map_err(to_pyruntime_err)?;
201
202            Python::attach(|py| {
203                let py_trades: PyResult<Vec<_>> = trades
204                    .into_iter()
205                    .map(|trade| trade.into_py_any(py))
206                    .collect();
207                let pylist = PyList::new(py, py_trades?)?.into_any().unbind();
208                Ok(pylist)
209            })
210        })
211    }
212
213    /// Requests the mark price for an instrument.
214    #[pyo3(name = "request_mark_price")]
215    fn py_request_mark_price<'py>(
216        &self,
217        py: Python<'py>,
218        instrument_id: InstrumentId,
219    ) -> PyResult<Bound<'py, PyAny>> {
220        let client = self.clone();
221
222        pyo3_async_runtimes::tokio::future_into_py(py, async move {
223            let mark_price = client
224                .request_mark_price(instrument_id)
225                .await
226                .map_err(to_pyruntime_err)?;
227
228            Ok(mark_price)
229        })
230    }
231
232    #[pyo3(name = "request_index_price")]
233    fn py_request_index_price<'py>(
234        &self,
235        py: Python<'py>,
236        instrument_id: InstrumentId,
237    ) -> PyResult<Bound<'py, PyAny>> {
238        let client = self.clone();
239
240        pyo3_async_runtimes::tokio::future_into_py(py, async move {
241            let index_price = client
242                .request_index_price(instrument_id)
243                .await
244                .map_err(to_pyruntime_err)?;
245
246            Ok(index_price)
247        })
248    }
249
250    /// Requests an order book snapshot for a futures instrument.
251    #[pyo3(name = "request_book_snapshot")]
252    #[pyo3(signature = (instrument_id, depth=None))]
253    fn py_request_book_snapshot<'py>(
254        &self,
255        py: Python<'py>,
256        instrument_id: InstrumentId,
257        depth: Option<u32>,
258    ) -> PyResult<Bound<'py, PyAny>> {
259        let client = self.clone();
260
261        pyo3_async_runtimes::tokio::future_into_py(py, async move {
262            let book = client
263                .request_book_snapshot(instrument_id, depth)
264                .await
265                .map_err(to_pyruntime_err)?;
266
267            Python::attach(|py| book.into_py_any(py))
268        })
269    }
270
271    #[pyo3(name = "request_bars")]
272    #[pyo3(signature = (bar_type, start=None, end=None, limit=None))]
273    fn py_request_bars<'py>(
274        &self,
275        py: Python<'py>,
276        bar_type: BarType,
277        start: Option<Timestamp>,
278        end: Option<Timestamp>,
279        limit: Option<u64>,
280    ) -> PyResult<Bound<'py, PyAny>> {
281        let client = self.clone();
282
283        pyo3_async_runtimes::tokio::future_into_py(py, async move {
284            let bars = client
285                .request_bars(bar_type, start, end, limit)
286                .await
287                .map_err(to_pyruntime_err)?;
288
289            Python::attach(|py| {
290                let py_bars: PyResult<Vec<_>> =
291                    bars.into_iter().map(|bar| bar.into_py_any(py)).collect();
292                let pylist = PyList::new(py, py_bars?)?.into_any().unbind();
293                Ok(pylist)
294            })
295        })
296    }
297
298    /// Requests account state from the Kraken Futures exchange.
299    ///
300    /// This queries the accounts endpoint and converts the response into a
301    /// Nautilus `AccountState` event containing balances and margin info.
302    ///
303    /// # Errors
304    ///
305    /// Returns an error if:
306    /// - Credentials are missing.
307    /// - The request fails.
308    /// - Response parsing fails.
309    #[pyo3(name = "request_account_state")]
310    fn py_request_account_state<'py>(
311        &self,
312        py: Python<'py>,
313        account_id: AccountId,
314    ) -> PyResult<Bound<'py, PyAny>> {
315        let client = self.clone();
316
317        pyo3_async_runtimes::tokio::future_into_py(py, async move {
318            let account_state = client
319                .request_account_state(account_id)
320                .await
321                .map_err(to_pyruntime_err)?;
322
323            Python::attach(|py| account_state.into_pyobject(py).map(|o| o.unbind()))
324        })
325    }
326
327    #[pyo3(name = "request_order_status_reports")]
328    #[pyo3(signature = (account_id, instrument_id=None, start=None, end=None, open_only=false))]
329    fn py_request_order_status_reports<'py>(
330        &self,
331        py: Python<'py>,
332        account_id: AccountId,
333        instrument_id: Option<InstrumentId>,
334        start: Option<Timestamp>,
335        end: Option<Timestamp>,
336        open_only: bool,
337    ) -> PyResult<Bound<'py, PyAny>> {
338        let client = self.clone();
339
340        pyo3_async_runtimes::tokio::future_into_py(py, async move {
341            let reports = client
342                .request_order_status_reports(account_id, instrument_id, start, end, open_only)
343                .await
344                .map_err(to_pyruntime_err)?;
345
346            Python::attach(|py| {
347                let py_reports: PyResult<Vec<_>> = reports
348                    .into_iter()
349                    .map(|report| report.into_py_any(py))
350                    .collect();
351                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
352                Ok(pylist)
353            })
354        })
355    }
356
357    #[pyo3(name = "request_fill_reports")]
358    #[pyo3(signature = (account_id, instrument_id=None, start=None, end=None))]
359    fn py_request_fill_reports<'py>(
360        &self,
361        py: Python<'py>,
362        account_id: AccountId,
363        instrument_id: Option<InstrumentId>,
364        start: Option<Timestamp>,
365        end: Option<Timestamp>,
366    ) -> PyResult<Bound<'py, PyAny>> {
367        let client = self.clone();
368
369        pyo3_async_runtimes::tokio::future_into_py(py, async move {
370            let reports = client
371                .request_fill_reports(account_id, instrument_id, start, end)
372                .await
373                .map_err(to_pyruntime_err)?;
374
375            Python::attach(|py| {
376                let py_reports: PyResult<Vec<_>> = reports
377                    .into_iter()
378                    .map(|report| report.into_py_any(py))
379                    .collect();
380                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
381                Ok(pylist)
382            })
383        })
384    }
385
386    #[pyo3(name = "request_position_status_reports")]
387    #[pyo3(signature = (account_id, instrument_id=None))]
388    fn py_request_position_status_reports<'py>(
389        &self,
390        py: Python<'py>,
391        account_id: AccountId,
392        instrument_id: Option<InstrumentId>,
393    ) -> PyResult<Bound<'py, PyAny>> {
394        let client = self.clone();
395
396        pyo3_async_runtimes::tokio::future_into_py(py, async move {
397            let reports = client
398                .request_position_status_reports(account_id, instrument_id)
399                .await
400                .map_err(to_pyruntime_err)?;
401
402            Python::attach(|py| {
403                let py_reports: PyResult<Vec<_>> = reports
404                    .into_iter()
405                    .map(|report| report.into_py_any(py))
406                    .collect();
407                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
408                Ok(pylist)
409            })
410        })
411    }
412
413    /// Submits a new order to the Kraken Futures exchange.
414    ///
415    /// # Errors
416    ///
417    /// Returns an error if:
418    /// - Credentials are missing.
419    /// - The instrument is not found in cache.
420    /// - The order type or time in force is not supported.
421    /// - The request fails.
422    /// - The order is rejected.
423    #[pyo3(name = "submit_order")]
424    #[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))]
425    #[expect(clippy::too_many_arguments)]
426    fn py_submit_order<'py>(
427        &self,
428        py: Python<'py>,
429        account_id: AccountId,
430        instrument_id: InstrumentId,
431        client_order_id: ClientOrderId,
432        order_side: OrderSide,
433        order_type: OrderType,
434        quantity: Quantity,
435        time_in_force: TimeInForce,
436        price: Option<Price>,
437        trigger_price: Option<Price>,
438        trigger_type: Option<TriggerType>,
439        reduce_only: bool,
440        post_only: bool,
441    ) -> PyResult<Bound<'py, PyAny>> {
442        let client = self.clone();
443
444        pyo3_async_runtimes::tokio::future_into_py(py, async move {
445            let report = client
446                .submit_order(
447                    account_id,
448                    instrument_id,
449                    client_order_id,
450                    order_side,
451                    order_type,
452                    quantity,
453                    time_in_force,
454                    price,
455                    trigger_price,
456                    trigger_type,
457                    reduce_only,
458                    post_only,
459                )
460                .await
461                .map_err(to_pyruntime_err)?;
462
463            Python::attach(|py| report.into_pyobject(py).map(|o| o.unbind()))
464        })
465    }
466
467    /// Modifies an existing order on the Kraken Futures exchange.
468    ///
469    /// Returns the new venue order ID assigned to the modified order.
470    ///
471    /// # Errors
472    ///
473    /// Returns an error if:
474    /// - Neither `client_order_id` nor `venue_order_id` is provided.
475    /// - The instrument is not found in cache.
476    /// - The request fails.
477    /// - The edit fails on the exchange.
478    #[pyo3(name = "modify_order")]
479    #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None, quantity=None, price=None, trigger_price=None))]
480    #[expect(clippy::too_many_arguments)]
481    fn py_modify_order<'py>(
482        &self,
483        py: Python<'py>,
484        instrument_id: InstrumentId,
485        client_order_id: Option<ClientOrderId>,
486        venue_order_id: Option<VenueOrderId>,
487        quantity: Option<Quantity>,
488        price: Option<Price>,
489        trigger_price: Option<Price>,
490    ) -> PyResult<Bound<'py, PyAny>> {
491        let client = self.clone();
492
493        pyo3_async_runtimes::tokio::future_into_py(py, async move {
494            let new_venue_order_id = client
495                .modify_order(
496                    instrument_id,
497                    client_order_id,
498                    venue_order_id,
499                    quantity,
500                    price,
501                    trigger_price,
502                )
503                .await
504                .map_err(to_pyruntime_err)?;
505
506            Python::attach(|py| new_venue_order_id.into_pyobject(py).map(|o| o.unbind()))
507        })
508    }
509
510    /// Cancels an order on the Kraken Futures exchange.
511    ///
512    /// # Errors
513    ///
514    /// Returns an error if:
515    /// - Credentials are missing.
516    /// - Neither client_order_id nor venue_order_id is provided.
517    /// - The request fails.
518    /// - The order cancellation is rejected.
519    #[pyo3(name = "cancel_order")]
520    #[pyo3(signature = (account_id, instrument_id, client_order_id=None, venue_order_id=None))]
521    fn py_cancel_order<'py>(
522        &self,
523        py: Python<'py>,
524        account_id: AccountId,
525        instrument_id: InstrumentId,
526        client_order_id: Option<ClientOrderId>,
527        venue_order_id: Option<VenueOrderId>,
528    ) -> PyResult<Bound<'py, PyAny>> {
529        let client = self.clone();
530
531        pyo3_async_runtimes::tokio::future_into_py(py, async move {
532            client
533                .cancel_order(account_id, instrument_id, client_order_id, venue_order_id)
534                .await
535                .map_err(to_pyruntime_err)
536        })
537    }
538
539    #[pyo3(name = "cancel_all_orders")]
540    #[pyo3(signature = (instrument_id=None))]
541    fn py_cancel_all_orders<'py>(
542        &self,
543        py: Python<'py>,
544        instrument_id: Option<InstrumentId>,
545    ) -> PyResult<Bound<'py, PyAny>> {
546        let client = self.clone();
547
548        pyo3_async_runtimes::tokio::future_into_py(py, async move {
549            let symbol = instrument_id.map(|id| id.symbol.to_string());
550            let response = client
551                .inner
552                .cancel_all_orders(symbol)
553                .await
554                .map_err(to_pyruntime_err)?;
555
556            Ok(response.cancel_status.cancelled_orders.len())
557        })
558    }
559
560    /// Cancels multiple orders on the Kraken Futures exchange.
561    ///
562    /// Automatically chunks requests into batches of 50 orders.
563    ///
564    /// # Parameters
565    /// - `venue_order_ids` - List of venue order IDs to cancel.
566    ///
567    /// # Returns
568    /// The total number of successfully cancelled orders.
569    #[pyo3(name = "cancel_orders_batch")]
570    fn py_cancel_orders_batch<'py>(
571        &self,
572        py: Python<'py>,
573        venue_order_ids: Vec<VenueOrderId>,
574    ) -> PyResult<Bound<'py, PyAny>> {
575        let client = self.clone();
576
577        pyo3_async_runtimes::tokio::future_into_py(py, async move {
578            client
579                .cancel_orders_batch(venue_order_ids)
580                .await
581                .map_err(to_pyruntime_err)
582        })
583    }
584}
585
586// Separate block to avoid pyo3_stub_gen trait bound issues with batch-order tuples.
587// These methods are registered in DEFERRED_RUNTIME_METHODS until the generator
588// supports complex tuple parameter types.
589#[pymethods]
590impl KrakenFuturesHttpClient {
591    /// Submits multiple orders in a single batch request.
592    ///
593    /// Builds batch send items from order parameters, chunks at the batch limit,
594    /// and returns per-item send statuses.
595    ///
596    /// # Errors
597    ///
598    /// Returns an error if the batch request fails at the API level.
599    #[pyo3(name = "submit_orders_batch")]
600    #[expect(clippy::type_complexity)]
601    fn py_submit_orders_batch<'py>(
602        &self,
603        py: Python<'py>,
604        orders: Vec<(
605            InstrumentId,
606            ClientOrderId,
607            OrderSide,
608            OrderType,
609            Quantity,
610            TimeInForce,
611            Option<Price>,
612            Option<Price>,
613            Option<TriggerType>,
614            bool,
615            bool,
616        )>,
617    ) -> PyResult<Bound<'py, PyAny>> {
618        let client = self.clone();
619
620        pyo3_async_runtimes::tokio::future_into_py(py, async move {
621            let statuses = client
622                .submit_orders_batch(orders)
623                .await
624                .map_err(to_pyruntime_err)?;
625
626            let result: Vec<String> = statuses.into_iter().map(|s| s.status).collect();
627            Ok(result)
628        })
629    }
630
631    /// Modifies multiple orders in a single batch request.
632    #[expect(clippy::type_complexity)]
633    #[pyo3(name = "edit_orders_batch")]
634    fn py_edit_orders_batch<'py>(
635        &self,
636        py: Python<'py>,
637        orders: Vec<(
638            InstrumentId,
639            Option<ClientOrderId>,
640            Option<VenueOrderId>,
641            Option<Quantity>,
642            Option<Price>,
643            Option<Price>,
644        )>,
645    ) -> PyResult<Bound<'py, PyAny>> {
646        let client = self.clone();
647
648        pyo3_async_runtimes::tokio::future_into_py(py, async move {
649            client
650                .edit_orders_batch(orders)
651                .await
652                .map_err(to_pyruntime_err)
653        })
654    }
655}