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 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?)?;
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<Timestamp>,
185        end: Option<Timestamp>,
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?)?.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<Timestamp>,
272        end: Option<Timestamp>,
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?)?.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    ///
297    /// # Errors
298    ///
299    /// Returns an error if:
300    /// - Credentials are missing.
301    /// - The request fails.
302    /// - Response parsing fails.
303    #[pyo3(name = "request_account_state")]
304    fn py_request_account_state<'py>(
305        &self,
306        py: Python<'py>,
307        account_id: AccountId,
308    ) -> PyResult<Bound<'py, PyAny>> {
309        let client = self.clone();
310
311        pyo3_async_runtimes::tokio::future_into_py(py, async move {
312            let account_state = client
313                .request_account_state(account_id)
314                .await
315                .map_err(to_pyruntime_err)?;
316
317            Python::attach(|py| account_state.into_pyobject(py).map(|o| o.unbind()))
318        })
319    }
320
321    #[pyo3(name = "request_order_status_reports")]
322    #[pyo3(signature = (account_id, instrument_id=None, start=None, end=None, open_only=false))]
323    fn py_request_order_status_reports<'py>(
324        &self,
325        py: Python<'py>,
326        account_id: AccountId,
327        instrument_id: Option<InstrumentId>,
328        start: Option<Timestamp>,
329        end: Option<Timestamp>,
330        open_only: bool,
331    ) -> PyResult<Bound<'py, PyAny>> {
332        let client = self.clone();
333
334        pyo3_async_runtimes::tokio::future_into_py(py, async move {
335            let reports = client
336                .request_order_status_reports(account_id, instrument_id, start, end, open_only)
337                .await
338                .map_err(to_pyruntime_err)?;
339
340            Python::attach(|py| {
341                let py_reports: PyResult<Vec<_>> = reports
342                    .into_iter()
343                    .map(|report| report.into_py_any(py))
344                    .collect();
345                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
346                Ok(pylist)
347            })
348        })
349    }
350
351    #[pyo3(name = "request_fill_reports")]
352    #[pyo3(signature = (account_id, instrument_id=None, start=None, end=None))]
353    fn py_request_fill_reports<'py>(
354        &self,
355        py: Python<'py>,
356        account_id: AccountId,
357        instrument_id: Option<InstrumentId>,
358        start: Option<Timestamp>,
359        end: Option<Timestamp>,
360    ) -> PyResult<Bound<'py, PyAny>> {
361        let client = self.clone();
362
363        pyo3_async_runtimes::tokio::future_into_py(py, async move {
364            let reports = client
365                .request_fill_reports(account_id, instrument_id, start, end)
366                .await
367                .map_err(to_pyruntime_err)?;
368
369            Python::attach(|py| {
370                let py_reports: PyResult<Vec<_>> = reports
371                    .into_iter()
372                    .map(|report| report.into_py_any(py))
373                    .collect();
374                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
375                Ok(pylist)
376            })
377        })
378    }
379
380    #[pyo3(name = "request_position_status_reports")]
381    #[pyo3(signature = (account_id, instrument_id=None))]
382    fn py_request_position_status_reports<'py>(
383        &self,
384        py: Python<'py>,
385        account_id: AccountId,
386        instrument_id: Option<InstrumentId>,
387    ) -> PyResult<Bound<'py, PyAny>> {
388        let client = self.clone();
389
390        pyo3_async_runtimes::tokio::future_into_py(py, async move {
391            let reports = client
392                .request_position_status_reports(account_id, instrument_id)
393                .await
394                .map_err(to_pyruntime_err)?;
395
396            Python::attach(|py| {
397                let py_reports: PyResult<Vec<_>> = reports
398                    .into_iter()
399                    .map(|report| report.into_py_any(py))
400                    .collect();
401                let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
402                Ok(pylist)
403            })
404        })
405    }
406
407    /// Submits a new order to the Kraken Futures exchange.
408    ///
409    /// # Errors
410    ///
411    /// Returns an error if:
412    /// - Credentials are missing.
413    /// - The instrument is not found in cache.
414    /// - The order type or time in force is not supported.
415    /// - The request fails.
416    /// - The order is rejected.
417    #[pyo3(name = "submit_order")]
418    #[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))]
419    #[expect(clippy::too_many_arguments)]
420    fn py_submit_order<'py>(
421        &self,
422        py: Python<'py>,
423        account_id: AccountId,
424        instrument_id: InstrumentId,
425        client_order_id: ClientOrderId,
426        order_side: OrderSide,
427        order_type: OrderType,
428        quantity: Quantity,
429        time_in_force: TimeInForce,
430        price: Option<Price>,
431        trigger_price: Option<Price>,
432        trigger_type: Option<TriggerType>,
433        reduce_only: bool,
434        post_only: bool,
435    ) -> PyResult<Bound<'py, PyAny>> {
436        let client = self.clone();
437
438        pyo3_async_runtimes::tokio::future_into_py(py, async move {
439            let report = client
440                .submit_order(
441                    account_id,
442                    instrument_id,
443                    client_order_id,
444                    order_side,
445                    order_type,
446                    quantity,
447                    time_in_force,
448                    price,
449                    trigger_price,
450                    trigger_type,
451                    reduce_only,
452                    post_only,
453                )
454                .await
455                .map_err(to_pyruntime_err)?;
456
457            Python::attach(|py| report.into_pyobject(py).map(|o| o.unbind()))
458        })
459    }
460
461    /// Modifies an existing order on the Kraken Futures exchange.
462    ///
463    /// Returns the new venue order ID assigned to the modified order.
464    ///
465    /// # Errors
466    ///
467    /// Returns an error if:
468    /// - Neither `client_order_id` nor `venue_order_id` is provided.
469    /// - The instrument is not found in cache.
470    /// - The request fails.
471    /// - The edit fails on the exchange.
472    #[pyo3(name = "modify_order")]
473    #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None, quantity=None, price=None, trigger_price=None))]
474    #[expect(clippy::too_many_arguments)]
475    fn py_modify_order<'py>(
476        &self,
477        py: Python<'py>,
478        instrument_id: InstrumentId,
479        client_order_id: Option<ClientOrderId>,
480        venue_order_id: Option<VenueOrderId>,
481        quantity: Option<Quantity>,
482        price: Option<Price>,
483        trigger_price: Option<Price>,
484    ) -> PyResult<Bound<'py, PyAny>> {
485        let client = self.clone();
486
487        pyo3_async_runtimes::tokio::future_into_py(py, async move {
488            let new_venue_order_id = client
489                .modify_order(
490                    instrument_id,
491                    client_order_id,
492                    venue_order_id,
493                    quantity,
494                    price,
495                    trigger_price,
496                )
497                .await
498                .map_err(to_pyruntime_err)?;
499
500            Python::attach(|py| new_venue_order_id.into_pyobject(py).map(|o| o.unbind()))
501        })
502    }
503
504    /// Cancels an order on the Kraken Futures exchange.
505    ///
506    /// # Errors
507    ///
508    /// Returns an error if:
509    /// - Credentials are missing.
510    /// - Neither client_order_id nor venue_order_id is provided.
511    /// - The request fails.
512    /// - The order cancellation is rejected.
513    #[pyo3(name = "cancel_order")]
514    #[pyo3(signature = (account_id, instrument_id, client_order_id=None, venue_order_id=None))]
515    fn py_cancel_order<'py>(
516        &self,
517        py: Python<'py>,
518        account_id: AccountId,
519        instrument_id: InstrumentId,
520        client_order_id: Option<ClientOrderId>,
521        venue_order_id: Option<VenueOrderId>,
522    ) -> PyResult<Bound<'py, PyAny>> {
523        let client = self.clone();
524
525        pyo3_async_runtimes::tokio::future_into_py(py, async move {
526            client
527                .cancel_order(account_id, instrument_id, client_order_id, venue_order_id)
528                .await
529                .map_err(to_pyruntime_err)
530        })
531    }
532
533    #[pyo3(name = "cancel_all_orders")]
534    #[pyo3(signature = (instrument_id=None))]
535    fn py_cancel_all_orders<'py>(
536        &self,
537        py: Python<'py>,
538        instrument_id: Option<InstrumentId>,
539    ) -> PyResult<Bound<'py, PyAny>> {
540        let client = self.clone();
541
542        pyo3_async_runtimes::tokio::future_into_py(py, async move {
543            let symbol = instrument_id.map(|id| id.symbol.to_string());
544            let response = client
545                .inner
546                .cancel_all_orders(symbol)
547                .await
548                .map_err(to_pyruntime_err)?;
549
550            Ok(response.cancel_status.cancelled_orders.len())
551        })
552    }
553
554    /// Cancels multiple orders on the Kraken Futures exchange.
555    ///
556    /// Automatically chunks requests into batches of 50 orders.
557    ///
558    /// # Parameters
559    /// - `venue_order_ids` - List of venue order IDs to cancel.
560    ///
561    /// # Returns
562    /// The total number of successfully cancelled orders.
563    #[pyo3(name = "cancel_orders_batch")]
564    fn py_cancel_orders_batch<'py>(
565        &self,
566        py: Python<'py>,
567        venue_order_ids: Vec<VenueOrderId>,
568    ) -> PyResult<Bound<'py, PyAny>> {
569        let client = self.clone();
570
571        pyo3_async_runtimes::tokio::future_into_py(py, async move {
572            client
573                .cancel_orders_batch(venue_order_ids)
574                .await
575                .map_err(to_pyruntime_err)
576        })
577    }
578}
579
580// Separate block to avoid pyo3_stub_gen trait bound issues with batch-order tuples.
581// These methods are registered in DEFERRED_RUNTIME_METHODS until the generator
582// supports complex tuple parameter types.
583#[pymethods]
584impl KrakenFuturesHttpClient {
585    /// Submits multiple orders in a single batch request.
586    ///
587    /// Builds batch send items from order parameters, chunks at the batch limit,
588    /// and returns per-item send statuses.
589    ///
590    /// # Errors
591    ///
592    /// Returns an error if the batch request fails at the API level.
593    #[pyo3(name = "submit_orders_batch")]
594    #[expect(clippy::type_complexity)]
595    fn py_submit_orders_batch<'py>(
596        &self,
597        py: Python<'py>,
598        orders: Vec<(
599            InstrumentId,
600            ClientOrderId,
601            OrderSide,
602            OrderType,
603            Quantity,
604            TimeInForce,
605            Option<Price>,
606            Option<Price>,
607            Option<TriggerType>,
608            bool,
609            bool,
610        )>,
611    ) -> PyResult<Bound<'py, PyAny>> {
612        let client = self.clone();
613
614        pyo3_async_runtimes::tokio::future_into_py(py, async move {
615            let statuses = client
616                .submit_orders_batch(orders)
617                .await
618                .map_err(to_pyruntime_err)?;
619
620            let result: Vec<String> = statuses.into_iter().map(|s| s.status).collect();
621            Ok(result)
622        })
623    }
624
625    /// Modifies multiple orders in a single batch request.
626    #[expect(clippy::type_complexity)]
627    #[pyo3(name = "edit_orders_batch")]
628    fn py_edit_orders_batch<'py>(
629        &self,
630        py: Python<'py>,
631        orders: Vec<(
632            InstrumentId,
633            Option<ClientOrderId>,
634            Option<VenueOrderId>,
635            Option<Quantity>,
636            Option<Price>,
637            Option<Price>,
638        )>,
639    ) -> PyResult<Bound<'py, PyAny>> {
640        let client = self.clone();
641
642        pyo3_async_runtimes::tokio::future_into_py(py, async move {
643            client
644                .edit_orders_batch(orders)
645                .await
646                .map_err(to_pyruntime_err)
647        })
648    }
649}