Skip to main content

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