1use 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::{AccountType, 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;
36use ustr::Ustr;
37
38use crate::{
39 common::{credential::KrakenCredential, enums::KrakenEnvironment},
40 http::KrakenSpotHttpClient,
41};
42
43#[pymethods]
44#[pyo3_stub_gen::derive::gen_stub_pymethods]
45impl KrakenSpotHttpClient {
46 #[new]
52 #[pyo3(signature = (api_key=None, api_secret=None, base_url=None, timeout_secs=60, max_retries=None, retry_delay_ms=None, retry_delay_max_ms=None, proxy_url=None, max_requests_per_second=5))]
53 #[expect(clippy::too_many_arguments)]
54 fn py_new(
55 api_key: Option<String>,
56 api_secret: Option<String>,
57 base_url: Option<String>,
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 = KrakenEnvironment::Live;
66
67 if let Some(cred) = KrakenCredential::resolve_spot(api_key, api_secret) {
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 #[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 #[pyo3(name = "cancel_all_requests")]
128 fn py_cancel_all_requests(&self) {
129 self.cancel_all_requests();
130 }
131
132 #[pyo3(name = "get_server_time")]
133 fn py_get_server_time<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
134 let client = self.clone();
135
136 pyo3_async_runtimes::tokio::future_into_py(py, async move {
137 let server_time = client
138 .inner
139 .get_server_time()
140 .await
141 .map_err(to_pyruntime_err)?;
142
143 let json_string = serde_json::to_string(&server_time)
144 .map_err(|e| to_pyruntime_err(format!("Failed to serialize response: {e}")))?;
145
146 Ok(json_string)
147 })
148 }
149
150 #[pyo3(name = "request_instruments")]
155 #[pyo3(signature = (pairs=None))]
156 fn py_request_instruments<'py>(
157 &self,
158 py: Python<'py>,
159 pairs: Option<Vec<String>>,
160 ) -> PyResult<Bound<'py, PyAny>> {
161 let client = self.clone();
162
163 pyo3_async_runtimes::tokio::future_into_py(py, async move {
164 let instruments = client
165 .request_instruments(pairs)
166 .await
167 .map_err(to_pyruntime_err)?;
168
169 Python::attach(|py| {
170 let py_instruments: PyResult<Vec<_>> = instruments
171 .into_iter()
172 .map(|inst| instrument_any_to_pyobject(py, inst))
173 .collect();
174 let pylist = PyList::new(py, py_instruments?).unwrap();
175 Ok(pylist.unbind())
176 })
177 })
178 }
179
180 #[pyo3(name = "request_instrument_statuses")]
186 #[pyo3(signature = (pairs=None))]
187 fn py_request_instrument_statuses<'py>(
188 &self,
189 py: Python<'py>,
190 pairs: Option<Vec<String>>,
191 ) -> PyResult<Bound<'py, PyAny>> {
192 let client = self.clone();
193
194 pyo3_async_runtimes::tokio::future_into_py(py, async move {
195 let statuses = client
196 .request_instrument_statuses(pairs)
197 .await
198 .map_err(to_pyruntime_err)?;
199
200 Python::attach(|py| {
201 let dict = PyDict::new(py);
202 for (instrument_id, action) in statuses {
203 dict.set_item(
204 instrument_id.into_bound_py_any(py)?,
205 action.into_bound_py_any(py)?,
206 )?;
207 }
208 Ok(dict.into_any().unbind())
209 })
210 })
211 }
212
213 #[pyo3(name = "request_trades")]
215 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
216 fn py_request_trades<'py>(
217 &self,
218 py: Python<'py>,
219 instrument_id: InstrumentId,
220 start: Option<DateTime<Utc>>,
221 end: Option<DateTime<Utc>>,
222 limit: Option<u64>,
223 ) -> PyResult<Bound<'py, PyAny>> {
224 let client = self.clone();
225
226 pyo3_async_runtimes::tokio::future_into_py(py, async move {
227 let trades = client
228 .request_trades(instrument_id, start, end, limit)
229 .await
230 .map_err(to_pyruntime_err)?;
231
232 Python::attach(|py| {
233 let py_trades: PyResult<Vec<_>> = trades
234 .into_iter()
235 .map(|trade| trade.into_py_any(py))
236 .collect();
237 let pylist = PyList::new(py, py_trades?).unwrap().into_any().unbind();
238 Ok(pylist)
239 })
240 })
241 }
242
243 #[pyo3(name = "request_book_snapshot")]
245 #[pyo3(signature = (instrument_id, depth=None))]
246 fn py_request_book_snapshot<'py>(
247 &self,
248 py: Python<'py>,
249 instrument_id: InstrumentId,
250 depth: Option<u32>,
251 ) -> PyResult<Bound<'py, PyAny>> {
252 let client = self.clone();
253
254 pyo3_async_runtimes::tokio::future_into_py(py, async move {
255 let book = client
256 .request_book_snapshot(instrument_id, depth)
257 .await
258 .map_err(to_pyruntime_err)?;
259
260 Python::attach(|py| book.into_py_any(py))
261 })
262 }
263
264 #[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 #[pyo3(name = "request_account_state")]
303 #[pyo3(signature = (account_id, account_type = AccountType::Cash, margin_balance_asset = None))]
304 fn py_request_account_state<'py>(
305 &self,
306 py: Python<'py>,
307 account_id: AccountId,
308 account_type: AccountType,
309 margin_balance_asset: Option<String>,
310 ) -> PyResult<Bound<'py, PyAny>> {
311 let client = self.clone();
312
313 pyo3_async_runtimes::tokio::future_into_py(py, async move {
314 let account_state = client
315 .request_account_state(account_id, account_type, margin_balance_asset.as_deref())
316 .await
317 .map_err(to_pyruntime_err)?;
318
319 Python::attach(|py| account_state.into_pyobject(py).map(|o| o.unbind()))
320 })
321 }
322
323 #[pyo3(name = "request_margin_metrics")]
336 #[pyo3(signature = (asset = None))]
337 fn py_request_margin_metrics<'py>(
338 &self,
339 py: Python<'py>,
340 asset: Option<String>,
341 ) -> PyResult<Bound<'py, PyAny>> {
342 let client = self.clone();
343
344 pyo3_async_runtimes::tokio::future_into_py(py, async move {
345 let metrics = client
346 .request_margin_metrics(asset.as_deref())
347 .await
348 .map_err(to_pyruntime_err)?;
349
350 Python::attach(|py| {
351 let dict = pyo3::types::PyDict::new(py);
352 for (k, v) in metrics {
353 dict.set_item(k, v)?;
354 }
355 Ok::<_, PyErr>(dict.unbind().into_any())
356 })
357 })
358 }
359
360 #[pyo3(name = "request_account_state_with_metrics")]
376 #[pyo3(signature = (account_id, account_type = AccountType::Cash, margin_balance_asset = None))]
377 fn py_request_account_state_with_metrics<'py>(
378 &self,
379 py: Python<'py>,
380 account_id: AccountId,
381 account_type: AccountType,
382 margin_balance_asset: Option<String>,
383 ) -> PyResult<Bound<'py, PyAny>> {
384 let client = self.clone();
385
386 pyo3_async_runtimes::tokio::future_into_py(py, async move {
387 let (account_state, metrics) = client
388 .request_account_state_with_metrics(
389 account_id,
390 account_type,
391 margin_balance_asset.as_deref(),
392 )
393 .await
394 .map_err(to_pyruntime_err)?;
395
396 Python::attach(|py| {
397 let state_obj = account_state.into_pyobject(py)?.unbind();
398 let dict = pyo3::types::PyDict::new(py);
399 for (k, v) in metrics {
400 dict.set_item(k, v)?;
401 }
402 let tuple = pyo3::types::PyTuple::new(
403 py,
404 [state_obj.into_any(), dict.unbind().into_any()],
405 )?;
406 Ok::<_, PyErr>(tuple.unbind().into_any())
407 })
408 })
409 }
410
411 #[pyo3(name = "request_order_status_reports")]
413 #[pyo3(signature = (account_id, instrument_id=None, start=None, end=None, open_only=false))]
414 fn py_request_order_status_reports<'py>(
415 &self,
416 py: Python<'py>,
417 account_id: AccountId,
418 instrument_id: Option<InstrumentId>,
419 start: Option<DateTime<Utc>>,
420 end: Option<DateTime<Utc>>,
421 open_only: bool,
422 ) -> PyResult<Bound<'py, PyAny>> {
423 let client = self.clone();
424
425 pyo3_async_runtimes::tokio::future_into_py(py, async move {
426 let reports = client
427 .request_order_status_reports(account_id, instrument_id, start, end, open_only)
428 .await
429 .map_err(to_pyruntime_err)?;
430
431 Python::attach(|py| {
432 let py_reports: PyResult<Vec<_>> = reports
433 .into_iter()
434 .map(|report| report.into_py_any(py))
435 .collect();
436 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
437 Ok(pylist)
438 })
439 })
440 }
441
442 #[pyo3(name = "request_fill_reports")]
444 #[pyo3(signature = (account_id, instrument_id=None, start=None, end=None))]
445 fn py_request_fill_reports<'py>(
446 &self,
447 py: Python<'py>,
448 account_id: AccountId,
449 instrument_id: Option<InstrumentId>,
450 start: Option<DateTime<Utc>>,
451 end: Option<DateTime<Utc>>,
452 ) -> PyResult<Bound<'py, PyAny>> {
453 let client = self.clone();
454
455 pyo3_async_runtimes::tokio::future_into_py(py, async move {
456 let reports = client
457 .request_fill_reports(account_id, instrument_id, start, end)
458 .await
459 .map_err(to_pyruntime_err)?;
460
461 Python::attach(|py| {
462 let py_reports: PyResult<Vec<_>> = reports
463 .into_iter()
464 .map(|report| report.into_py_any(py))
465 .collect();
466 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
467 Ok(pylist)
468 })
469 })
470 }
471
472 #[pyo3(name = "request_position_status_reports")]
478 #[pyo3(signature = (account_id, instrument_id=None, account_type=AccountType::Cash, use_spot_position_reports=false, quote_currency="USDT"))]
479 fn py_request_position_status_reports<'py>(
480 &self,
481 py: Python<'py>,
482 account_id: AccountId,
483 instrument_id: Option<InstrumentId>,
484 account_type: AccountType,
485 use_spot_position_reports: bool,
486 quote_currency: &str,
487 ) -> PyResult<Bound<'py, PyAny>> {
488 let client = self.clone();
489 let quote_currency = Ustr::from(quote_currency);
490
491 pyo3_async_runtimes::tokio::future_into_py(py, async move {
492 let reports = client
493 .request_position_status_reports(
494 account_id,
495 instrument_id,
496 account_type,
497 use_spot_position_reports,
498 quote_currency,
499 )
500 .await
501 .map_err(to_pyruntime_err)?;
502
503 Python::attach(|py| {
504 let py_reports: PyResult<Vec<_>> = reports
505 .into_iter()
506 .map(|report| report.into_py_any(py))
507 .collect();
508 let pylist = PyList::new(py, py_reports?).unwrap().into_any().unbind();
509 Ok(pylist)
510 })
511 })
512 }
513
514 #[pyo3(name = "submit_order")]
518 #[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, leverage=None, account_type=AccountType::Cash))]
519 #[expect(clippy::too_many_arguments)]
520 fn py_submit_order<'py>(
521 &self,
522 py: Python<'py>,
523 account_id: AccountId,
524 instrument_id: InstrumentId,
525 client_order_id: ClientOrderId,
526 order_side: OrderSide,
527 order_type: OrderType,
528 quantity: Quantity,
529 time_in_force: TimeInForce,
530 expire_time: Option<u64>,
531 price: Option<Price>,
532 trigger_price: Option<Price>,
533 trigger_type: Option<TriggerType>,
534 trailing_offset: Option<String>,
535 limit_offset: Option<String>,
536 reduce_only: bool,
537 post_only: bool,
538 quote_quantity: bool,
539 display_qty: Option<Quantity>,
540 leverage: Option<u16>,
541 account_type: AccountType,
542 ) -> PyResult<Bound<'py, PyAny>> {
543 let client = self.clone();
544 let expire_time = expire_time.map(UnixNanos::from);
545 let trailing_offset = trailing_offset
546 .map(|s| {
547 Decimal::from_str_exact(&s)
548 .map_err(|e| to_pyvalue_err(format!("invalid trailing_offset: {e}")))
549 })
550 .transpose()?;
551 let limit_offset = limit_offset
552 .map(|s| {
553 Decimal::from_str_exact(&s)
554 .map_err(|e| to_pyvalue_err(format!("invalid limit_offset: {e}")))
555 })
556 .transpose()?;
557
558 pyo3_async_runtimes::tokio::future_into_py(py, async move {
559 let venue_order_id = client
560 .submit_order(
561 account_id,
562 instrument_id,
563 client_order_id,
564 order_side,
565 order_type,
566 quantity,
567 time_in_force,
568 expire_time,
569 price,
570 trigger_price,
571 trigger_type,
572 trailing_offset,
573 limit_offset,
574 reduce_only,
575 post_only,
576 quote_quantity,
577 display_qty,
578 leverage,
579 account_type,
580 )
581 .await
582 .map_err(to_pyruntime_err)?;
583
584 Python::attach(|py| venue_order_id.into_pyobject(py).map(|o| o.unbind()))
585 })
586 }
587
588 #[pyo3(name = "cancel_order")]
590 #[pyo3(signature = (account_id, instrument_id, client_order_id=None, venue_order_id=None))]
591 fn py_cancel_order<'py>(
592 &self,
593 py: Python<'py>,
594 account_id: AccountId,
595 instrument_id: InstrumentId,
596 client_order_id: Option<ClientOrderId>,
597 venue_order_id: Option<VenueOrderId>,
598 ) -> PyResult<Bound<'py, PyAny>> {
599 let client = self.clone();
600
601 pyo3_async_runtimes::tokio::future_into_py(py, async move {
602 client
603 .cancel_order(account_id, instrument_id, client_order_id, venue_order_id)
604 .await
605 .map_err(to_pyruntime_err)
606 })
607 }
608
609 #[pyo3(name = "cancel_all_orders")]
610 fn py_cancel_all_orders<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
611 let client = self.clone();
612
613 pyo3_async_runtimes::tokio::future_into_py(py, async move {
614 let response = client
615 .inner
616 .cancel_all_orders()
617 .await
618 .map_err(to_pyruntime_err)?;
619
620 Ok(response.count)
621 })
622 }
623
624 #[pyo3(name = "cancel_orders_batch")]
626 fn py_cancel_orders_batch<'py>(
627 &self,
628 py: Python<'py>,
629 venue_order_ids: Vec<VenueOrderId>,
630 ) -> PyResult<Bound<'py, PyAny>> {
631 let client = self.clone();
632
633 pyo3_async_runtimes::tokio::future_into_py(py, async move {
634 client
635 .cancel_orders_batch(venue_order_ids)
636 .await
637 .map_err(to_pyruntime_err)
638 })
639 }
640
641 #[pyo3(name = "modify_order")]
646 #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None, quantity=None, price=None, trigger_price=None))]
647 #[expect(clippy::too_many_arguments)]
648 fn py_modify_order<'py>(
649 &self,
650 py: Python<'py>,
651 instrument_id: InstrumentId,
652 client_order_id: Option<ClientOrderId>,
653 venue_order_id: Option<VenueOrderId>,
654 quantity: Option<Quantity>,
655 price: Option<Price>,
656 trigger_price: Option<Price>,
657 ) -> PyResult<Bound<'py, PyAny>> {
658 let client = self.clone();
659
660 pyo3_async_runtimes::tokio::future_into_py(py, async move {
661 let new_venue_order_id = client
662 .modify_order(
663 instrument_id,
664 client_order_id,
665 venue_order_id,
666 quantity,
667 price,
668 trigger_price,
669 )
670 .await
671 .map_err(to_pyruntime_err)?;
672
673 Python::attach(|py| new_venue_order_id.into_pyobject(py).map(|o| o.unbind()))
674 })
675 }
676}
677
678#[pymethods]
681impl KrakenSpotHttpClient {
682 #[pyo3(name = "submit_orders_batch", signature = (orders, leverage=None, account_type=AccountType::Cash, per_order_leverages=None, per_order_reduce_only=None))]
687 #[expect(clippy::type_complexity)]
688 fn py_submit_orders_batch<'py>(
689 &self,
690 py: Python<'py>,
691 orders: Vec<(
692 InstrumentId,
693 ClientOrderId,
694 OrderSide,
695 OrderType,
696 Quantity,
697 TimeInForce,
698 Option<Price>,
699 Option<Price>,
700 Option<TriggerType>,
701 bool,
702 bool,
703 Option<Quantity>,
704 )>,
705 leverage: Option<u16>,
706 account_type: AccountType,
707 per_order_leverages: Option<Vec<Option<u16>>>,
708 per_order_reduce_only: Option<Vec<bool>>,
709 ) -> PyResult<Bound<'py, PyAny>> {
710 let client = self.clone();
711 let n = orders.len();
712
713 if let Some(ref v) = per_order_leverages
714 && v.len() != n
715 {
716 return Err(to_pyvalue_err(format!(
717 "per_order_leverages length must equal orders length, was {} for {n} orders",
718 v.len(),
719 )));
720 }
721
722 if let Some(ref v) = per_order_reduce_only
723 && v.len() != n
724 {
725 return Err(to_pyvalue_err(format!(
726 "per_order_reduce_only length must equal orders length, was {} for {n} orders",
727 v.len(),
728 )));
729 }
730
731 let leverages: Vec<Option<u16>> = match per_order_leverages {
732 Some(per_leverages) => per_leverages.into_iter().map(|v| v.or(leverage)).collect(),
733 None => vec![leverage; n],
734 };
735 let reduce_only_flags = per_order_reduce_only.unwrap_or_else(|| vec![false; n]);
736 let expanded_orders = orders
737 .into_iter()
738 .zip(leverages)
739 .zip(reduce_only_flags)
740 .map(
741 |(
742 (
743 (
744 instrument_id,
745 client_order_id,
746 order_side,
747 order_type,
748 quantity,
749 time_in_force,
750 price,
751 trigger_price,
752 trigger_type,
753 post_only,
754 quote_quantity,
755 display_qty,
756 ),
757 order_leverage,
758 ),
759 order_reduce_only,
760 )| {
761 (
762 instrument_id,
763 client_order_id,
764 order_side,
765 order_type,
766 quantity,
767 time_in_force,
768 None,
769 price,
770 trigger_price,
771 trigger_type,
772 None,
773 None,
774 order_reduce_only,
775 post_only,
776 quote_quantity,
777 display_qty,
778 order_leverage,
779 )
780 },
781 )
782 .collect();
783
784 pyo3_async_runtimes::tokio::future_into_py(py, async move {
785 client
786 .submit_orders_batch(expanded_orders, account_type)
787 .await
788 .map_err(to_pyruntime_err)
789 })
790 }
791}