1use jiff::Timestamp;
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")]
159 #[pyo3(signature = (pairs=None))]
160 fn py_request_instruments<'py>(
161 &self,
162 py: Python<'py>,
163 pairs: Option<Vec<String>>,
164 ) -> PyResult<Bound<'py, PyAny>> {
165 let client = self.clone();
166
167 pyo3_async_runtimes::tokio::future_into_py(py, async move {
168 let instruments = client
169 .request_instruments(pairs)
170 .await
171 .map_err(to_pyruntime_err)?;
172
173 Python::attach(|py| {
174 let py_instruments: PyResult<Vec<_>> = instruments
175 .into_iter()
176 .map(|inst| instrument_any_to_pyobject(py, inst))
177 .collect();
178 let pylist = PyList::new(py, py_instruments?)?;
179 Ok(pylist.unbind())
180 })
181 })
182 }
183
184 #[pyo3(name = "request_instrument_statuses")]
190 #[pyo3(signature = (pairs=None))]
191 fn py_request_instrument_statuses<'py>(
192 &self,
193 py: Python<'py>,
194 pairs: Option<Vec<String>>,
195 ) -> PyResult<Bound<'py, PyAny>> {
196 let client = self.clone();
197
198 pyo3_async_runtimes::tokio::future_into_py(py, async move {
199 let statuses = client
200 .request_instrument_statuses(pairs)
201 .await
202 .map_err(to_pyruntime_err)?;
203
204 Python::attach(|py| {
205 let dict = PyDict::new(py);
206 for (instrument_id, action) in statuses {
207 dict.set_item(
208 instrument_id.into_bound_py_any(py)?,
209 action.into_bound_py_any(py)?,
210 )?;
211 }
212 Ok(dict.into_any().unbind())
213 })
214 })
215 }
216
217 #[pyo3(name = "request_trades")]
219 #[pyo3(signature = (instrument_id, start=None, end=None, limit=None))]
220 fn py_request_trades<'py>(
221 &self,
222 py: Python<'py>,
223 instrument_id: InstrumentId,
224 start: Option<Timestamp>,
225 end: Option<Timestamp>,
226 limit: Option<u64>,
227 ) -> PyResult<Bound<'py, PyAny>> {
228 let client = self.clone();
229
230 pyo3_async_runtimes::tokio::future_into_py(py, async move {
231 let trades = client
232 .request_trades(instrument_id, start, end, limit)
233 .await
234 .map_err(to_pyruntime_err)?;
235
236 Python::attach(|py| {
237 let py_trades: PyResult<Vec<_>> = trades
238 .into_iter()
239 .map(|trade| trade.into_py_any(py))
240 .collect();
241 let pylist = PyList::new(py, py_trades?)?.into_any().unbind();
242 Ok(pylist)
243 })
244 })
245 }
246
247 #[pyo3(name = "request_book_snapshot")]
249 #[pyo3(signature = (instrument_id, depth=None))]
250 fn py_request_book_snapshot<'py>(
251 &self,
252 py: Python<'py>,
253 instrument_id: InstrumentId,
254 depth: Option<u32>,
255 ) -> PyResult<Bound<'py, PyAny>> {
256 let client = self.clone();
257
258 pyo3_async_runtimes::tokio::future_into_py(py, async move {
259 let book = client
260 .request_book_snapshot(instrument_id, depth)
261 .await
262 .map_err(to_pyruntime_err)?;
263
264 Python::attach(|py| book.into_py_any(py))
265 })
266 }
267
268 #[pyo3(name = "request_bars")]
270 #[pyo3(signature = (bar_type, start=None, end=None, limit=None))]
271 fn py_request_bars<'py>(
272 &self,
273 py: Python<'py>,
274 bar_type: BarType,
275 start: Option<Timestamp>,
276 end: Option<Timestamp>,
277 limit: Option<u64>,
278 ) -> PyResult<Bound<'py, PyAny>> {
279 let client = self.clone();
280
281 pyo3_async_runtimes::tokio::future_into_py(py, async move {
282 let bars = client
283 .request_bars(bar_type, start, end, limit)
284 .await
285 .map_err(to_pyruntime_err)?;
286
287 Python::attach(|py| {
288 let py_bars: PyResult<Vec<_>> =
289 bars.into_iter().map(|bar| bar.into_py_any(py)).collect();
290 let pylist = PyList::new(py, py_bars?)?.into_any().unbind();
291 Ok(pylist)
292 })
293 })
294 }
295
296 #[pyo3(name = "request_account_state")]
307 #[pyo3(signature = (account_id, account_type = AccountType::Cash, margin_balance_asset = None))]
308 fn py_request_account_state<'py>(
309 &self,
310 py: Python<'py>,
311 account_id: AccountId,
312 account_type: AccountType,
313 margin_balance_asset: Option<String>,
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, account_type, margin_balance_asset.as_deref())
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_margin_metrics")]
340 #[pyo3(signature = (asset = None))]
341 fn py_request_margin_metrics<'py>(
342 &self,
343 py: Python<'py>,
344 asset: Option<String>,
345 ) -> PyResult<Bound<'py, PyAny>> {
346 let client = self.clone();
347
348 pyo3_async_runtimes::tokio::future_into_py(py, async move {
349 let metrics = client
350 .request_margin_metrics(asset.as_deref())
351 .await
352 .map_err(to_pyruntime_err)?;
353
354 Python::attach(|py| {
355 let dict = pyo3::types::PyDict::new(py);
356 for (k, v) in metrics {
357 dict.set_item(k, v)?;
358 }
359 Ok::<_, PyErr>(dict.unbind().into_any())
360 })
361 })
362 }
363
364 #[pyo3(name = "request_account_state_with_metrics")]
386 #[pyo3(signature = (account_id, account_type = AccountType::Cash, margin_balance_asset = None))]
387 fn py_request_account_state_with_metrics<'py>(
388 &self,
389 py: Python<'py>,
390 account_id: AccountId,
391 account_type: AccountType,
392 margin_balance_asset: Option<String>,
393 ) -> PyResult<Bound<'py, PyAny>> {
394 let client = self.clone();
395
396 pyo3_async_runtimes::tokio::future_into_py(py, async move {
397 let (account_state, metrics) = client
398 .request_account_state_with_metrics(
399 account_id,
400 account_type,
401 margin_balance_asset.as_deref(),
402 )
403 .await
404 .map_err(to_pyruntime_err)?;
405
406 Python::attach(|py| {
407 let state_obj = account_state.into_pyobject(py)?.unbind();
408 let dict = pyo3::types::PyDict::new(py);
409 for (k, v) in metrics {
410 dict.set_item(k, v)?;
411 }
412 let tuple = pyo3::types::PyTuple::new(
413 py,
414 [state_obj.into_any(), dict.unbind().into_any()],
415 )?;
416 Ok::<_, PyErr>(tuple.unbind().into_any())
417 })
418 })
419 }
420
421 #[pyo3(name = "request_order_status_reports")]
423 #[pyo3(signature = (account_id, instrument_id=None, start=None, end=None, open_only=false))]
424 fn py_request_order_status_reports<'py>(
425 &self,
426 py: Python<'py>,
427 account_id: AccountId,
428 instrument_id: Option<InstrumentId>,
429 start: Option<Timestamp>,
430 end: Option<Timestamp>,
431 open_only: bool,
432 ) -> PyResult<Bound<'py, PyAny>> {
433 let client = self.clone();
434
435 pyo3_async_runtimes::tokio::future_into_py(py, async move {
436 let reports = client
437 .request_order_status_reports(account_id, instrument_id, start, end, open_only)
438 .await
439 .map_err(to_pyruntime_err)?;
440
441 Python::attach(|py| {
442 let py_reports: PyResult<Vec<_>> = reports
443 .into_iter()
444 .map(|report| report.into_py_any(py))
445 .collect();
446 let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
447 Ok(pylist)
448 })
449 })
450 }
451
452 #[pyo3(name = "request_fill_reports")]
454 #[pyo3(signature = (account_id, instrument_id=None, start=None, end=None))]
455 fn py_request_fill_reports<'py>(
456 &self,
457 py: Python<'py>,
458 account_id: AccountId,
459 instrument_id: Option<InstrumentId>,
460 start: Option<Timestamp>,
461 end: Option<Timestamp>,
462 ) -> PyResult<Bound<'py, PyAny>> {
463 let client = self.clone();
464
465 pyo3_async_runtimes::tokio::future_into_py(py, async move {
466 let reports = client
467 .request_fill_reports(account_id, instrument_id, start, end)
468 .await
469 .map_err(to_pyruntime_err)?;
470
471 Python::attach(|py| {
472 let py_reports: PyResult<Vec<_>> = reports
473 .into_iter()
474 .map(|report| report.into_py_any(py))
475 .collect();
476 let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
477 Ok(pylist)
478 })
479 })
480 }
481
482 #[pyo3(name = "request_position_status_reports")]
488 #[pyo3(signature = (account_id, instrument_id=None, account_type=AccountType::Cash, use_spot_position_reports=false, quote_currency="USDT"))]
489 fn py_request_position_status_reports<'py>(
490 &self,
491 py: Python<'py>,
492 account_id: AccountId,
493 instrument_id: Option<InstrumentId>,
494 account_type: AccountType,
495 use_spot_position_reports: bool,
496 quote_currency: &str,
497 ) -> PyResult<Bound<'py, PyAny>> {
498 let client = self.clone();
499 let quote_currency = Ustr::from(quote_currency);
500
501 pyo3_async_runtimes::tokio::future_into_py(py, async move {
502 let reports = client
503 .request_position_status_reports(
504 account_id,
505 instrument_id,
506 account_type,
507 use_spot_position_reports,
508 quote_currency,
509 )
510 .await
511 .map_err(to_pyruntime_err)?;
512
513 Python::attach(|py| {
514 let py_reports: PyResult<Vec<_>> = reports
515 .into_iter()
516 .map(|report| report.into_py_any(py))
517 .collect();
518 let pylist = PyList::new(py, py_reports?)?.into_any().unbind();
519 Ok(pylist)
520 })
521 })
522 }
523
524 #[pyo3(name = "submit_order")]
537 #[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))]
538 #[expect(clippy::too_many_arguments)]
539 fn py_submit_order<'py>(
540 &self,
541 py: Python<'py>,
542 account_id: AccountId,
543 instrument_id: InstrumentId,
544 client_order_id: ClientOrderId,
545 order_side: OrderSide,
546 order_type: OrderType,
547 quantity: Quantity,
548 time_in_force: TimeInForce,
549 expire_time: Option<u64>,
550 price: Option<Price>,
551 trigger_price: Option<Price>,
552 trigger_type: Option<TriggerType>,
553 trailing_offset: Option<String>,
554 limit_offset: Option<String>,
555 reduce_only: bool,
556 post_only: bool,
557 quote_quantity: bool,
558 display_qty: Option<Quantity>,
559 leverage: Option<u16>,
560 account_type: AccountType,
561 ) -> PyResult<Bound<'py, PyAny>> {
562 let client = self.clone();
563 let expire_time = expire_time.map(UnixNanos::from);
564 let trailing_offset = trailing_offset
565 .map(|s| {
566 Decimal::from_str_exact(&s)
567 .map_err(|e| to_pyvalue_err(format!("invalid trailing_offset: {e}")))
568 })
569 .transpose()?;
570 let limit_offset = limit_offset
571 .map(|s| {
572 Decimal::from_str_exact(&s)
573 .map_err(|e| to_pyvalue_err(format!("invalid limit_offset: {e}")))
574 })
575 .transpose()?;
576
577 pyo3_async_runtimes::tokio::future_into_py(py, async move {
578 let venue_order_id = client
579 .submit_order(
580 account_id,
581 instrument_id,
582 client_order_id,
583 order_side,
584 order_type,
585 quantity,
586 time_in_force,
587 expire_time,
588 price,
589 trigger_price,
590 trigger_type,
591 trailing_offset,
592 limit_offset,
593 reduce_only,
594 post_only,
595 quote_quantity,
596 display_qty,
597 leverage,
598 account_type,
599 )
600 .await
601 .map_err(to_pyruntime_err)?;
602
603 Python::attach(|py| venue_order_id.into_pyobject(py).map(|o| o.unbind()))
604 })
605 }
606
607 #[pyo3(name = "cancel_order")]
617 #[pyo3(signature = (account_id, instrument_id, client_order_id=None, venue_order_id=None))]
618 fn py_cancel_order<'py>(
619 &self,
620 py: Python<'py>,
621 account_id: AccountId,
622 instrument_id: InstrumentId,
623 client_order_id: Option<ClientOrderId>,
624 venue_order_id: Option<VenueOrderId>,
625 ) -> PyResult<Bound<'py, PyAny>> {
626 let client = self.clone();
627
628 pyo3_async_runtimes::tokio::future_into_py(py, async move {
629 client
630 .cancel_order(account_id, instrument_id, client_order_id, venue_order_id)
631 .await
632 .map_err(to_pyruntime_err)
633 })
634 }
635
636 #[pyo3(name = "cancel_all_orders")]
637 fn py_cancel_all_orders<'py>(&self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
638 let client = self.clone();
639
640 pyo3_async_runtimes::tokio::future_into_py(py, async move {
641 let response = client
642 .inner
643 .cancel_all_orders()
644 .await
645 .map_err(to_pyruntime_err)?;
646
647 Ok(response.count)
648 })
649 }
650
651 #[pyo3(name = "cancel_orders_batch")]
653 fn py_cancel_orders_batch<'py>(
654 &self,
655 py: Python<'py>,
656 venue_order_ids: Vec<VenueOrderId>,
657 ) -> PyResult<Bound<'py, PyAny>> {
658 let client = self.clone();
659
660 pyo3_async_runtimes::tokio::future_into_py(py, async move {
661 client
662 .cancel_orders_batch(venue_order_ids)
663 .await
664 .map_err(to_pyruntime_err)
665 })
666 }
667
668 #[pyo3(name = "modify_order")]
680 #[pyo3(signature = (instrument_id, client_order_id=None, venue_order_id=None, quantity=None, price=None, trigger_price=None))]
681 #[expect(clippy::too_many_arguments)]
682 fn py_modify_order<'py>(
683 &self,
684 py: Python<'py>,
685 instrument_id: InstrumentId,
686 client_order_id: Option<ClientOrderId>,
687 venue_order_id: Option<VenueOrderId>,
688 quantity: Option<Quantity>,
689 price: Option<Price>,
690 trigger_price: Option<Price>,
691 ) -> PyResult<Bound<'py, PyAny>> {
692 let client = self.clone();
693
694 pyo3_async_runtimes::tokio::future_into_py(py, async move {
695 let new_venue_order_id = client
696 .modify_order(
697 instrument_id,
698 client_order_id,
699 venue_order_id,
700 quantity,
701 price,
702 trigger_price,
703 )
704 .await
705 .map_err(to_pyruntime_err)?;
706
707 Python::attach(|py| new_venue_order_id.into_pyobject(py).map(|o| o.unbind()))
708 })
709 }
710}
711
712#[pymethods]
716impl KrakenSpotHttpClient {
717 #[pyo3(name = "submit_orders_batch", signature = (orders, leverage=None, account_type=AccountType::Cash, per_order_leverages=None, per_order_reduce_only=None))]
722 #[expect(clippy::type_complexity)]
723 fn py_submit_orders_batch<'py>(
724 &self,
725 py: Python<'py>,
726 orders: Vec<(
727 InstrumentId,
728 ClientOrderId,
729 OrderSide,
730 OrderType,
731 Quantity,
732 TimeInForce,
733 Option<Price>,
734 Option<Price>,
735 Option<TriggerType>,
736 bool,
737 bool,
738 Option<Quantity>,
739 )>,
740 leverage: Option<u16>,
741 account_type: AccountType,
742 per_order_leverages: Option<Vec<Option<u16>>>,
743 per_order_reduce_only: Option<Vec<bool>>,
744 ) -> PyResult<Bound<'py, PyAny>> {
745 let client = self.clone();
746 let n = orders.len();
747
748 if let Some(ref v) = per_order_leverages
749 && v.len() != n
750 {
751 return Err(to_pyvalue_err(format!(
752 "per_order_leverages length must equal orders length, was {} for {n} orders",
753 v.len(),
754 )));
755 }
756
757 if let Some(ref v) = per_order_reduce_only
758 && v.len() != n
759 {
760 return Err(to_pyvalue_err(format!(
761 "per_order_reduce_only length must equal orders length, was {} for {n} orders",
762 v.len(),
763 )));
764 }
765
766 let leverages: Vec<Option<u16>> = match per_order_leverages {
767 Some(per_leverages) => per_leverages.into_iter().map(|v| v.or(leverage)).collect(),
768 None => vec![leverage; n],
769 };
770 let reduce_only_flags = per_order_reduce_only.unwrap_or_else(|| vec![false; n]);
771 let expanded_orders = orders
772 .into_iter()
773 .zip(leverages)
774 .zip(reduce_only_flags)
775 .map(
776 |(
777 (
778 (
779 instrument_id,
780 client_order_id,
781 order_side,
782 order_type,
783 quantity,
784 time_in_force,
785 price,
786 trigger_price,
787 trigger_type,
788 post_only,
789 quote_quantity,
790 display_qty,
791 ),
792 order_leverage,
793 ),
794 order_reduce_only,
795 )| {
796 (
797 instrument_id,
798 client_order_id,
799 order_side,
800 order_type,
801 quantity,
802 time_in_force,
803 None,
804 price,
805 trigger_price,
806 trigger_type,
807 None,
808 None,
809 order_reduce_only,
810 post_only,
811 quote_quantity,
812 display_qty,
813 order_leverage,
814 )
815 },
816 )
817 .collect();
818
819 pyo3_async_runtimes::tokio::future_into_py(py, async move {
820 client
821 .submit_orders_batch(expanded_orders, account_type)
822 .await
823 .map_err(to_pyruntime_err)
824 })
825 }
826}