1use std::{
19 collections::HashMap,
20 fmt::Debug,
21 num::NonZeroU32,
22 sync::{
23 Arc,
24 atomic::{AtomicBool, Ordering},
25 },
26};
27
28use ahash::AHashMap;
29use chrono::{DateTime, Utc};
30use nautilus_common::cache::InstrumentLookupError;
31use nautilus_core::{
32 AtomicMap, AtomicTime, UUID4, consts::NAUTILUS_USER_AGENT, nanos::UnixNanos,
33 time::get_atomic_clock_realtime,
34};
35use nautilus_model::{
36 data::{Bar, BarType, BookOrder, FundingRateUpdate, TradeTick},
37 enums::{
38 AccountType, BookType, CurrencyType, MarketStatusAction, OrderSide, OrderType, TimeInForce,
39 TriggerType,
40 },
41 events::AccountState,
42 identifiers::{AccountId, ClientOrderId, InstrumentId, Symbol, VenueOrderId},
43 instruments::{Instrument, InstrumentAny},
44 orderbook::OrderBook,
45 reports::{FillReport, OrderStatusReport, PositionStatusReport},
46 types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
47};
48use nautilus_network::{
49 http::{HttpClient, Method, USER_AGENT},
50 ratelimiter::quota::Quota,
51 retry::{RetryConfig, RetryManager},
52};
53use rust_decimal::{Decimal, prelude::FromPrimitive};
54use serde::de::DeserializeOwned;
55use tokio_util::sync::CancellationToken;
56use ustr::Ustr;
57
58use super::{models::*, query::*};
59use crate::{
60 common::{
61 consts::{KRAKEN_VENUE, NAUTILUS_KRAKEN_BROKER_ID},
62 credential::KrakenCredential,
63 enums::{
64 KrakenApiResult, KrakenEnvironment, KrakenFuturesOrderType, KrakenOrderSide,
65 KrakenProductType, KrakenSendStatus, KrakenTriggerSignal,
66 },
67 parse::{
68 bar_type_to_futures_resolution, parse_bar, parse_futures_fill_report,
69 parse_futures_instrument, parse_futures_order_event_status_report,
70 parse_futures_order_status_report, parse_futures_position_status_report,
71 parse_futures_public_execution, truncate_cl_ord_id,
72 },
73 urls::get_kraken_http_base_url,
74 },
75 http::{
76 apply_count_limit,
77 error::{KrakenHttpError, kraken_http_should_retry},
78 models::OhlcData,
79 },
80};
81
82pub const KRAKEN_FUTURES_DEFAULT_RATE_LIMIT_PER_SECOND: u32 = 5;
84
85const KRAKEN_GLOBAL_RATE_KEY: &str = "kraken:futures:global";
86
87const BATCH_CANCEL_LIMIT: usize = 50;
89
90const BATCH_ORDER_LIMIT: usize = 10;
92
93pub struct KrakenFuturesRawHttpClient {
98 base_url: String,
99 client: HttpClient,
100 credential: Option<KrakenCredential>,
101 retry_manager: RetryManager<KrakenHttpError>,
102 cancellation_token: CancellationToken,
103 clock: &'static AtomicTime,
104 auth_mutex: tokio::sync::Mutex<()>,
106}
107
108impl Default for KrakenFuturesRawHttpClient {
109 fn default() -> Self {
110 Self::new(
111 KrakenEnvironment::Live,
112 None,
113 60,
114 None,
115 None,
116 None,
117 None,
118 KRAKEN_FUTURES_DEFAULT_RATE_LIMIT_PER_SECOND,
119 )
120 .expect("Failed to create default KrakenFuturesRawHttpClient")
121 }
122}
123
124impl Debug for KrakenFuturesRawHttpClient {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 f.debug_struct(stringify!(KrakenFuturesRawHttpClient))
127 .field("base_url", &self.base_url)
128 .field("has_credentials", &self.credential.is_some())
129 .finish()
130 }
131}
132
133impl KrakenFuturesRawHttpClient {
134 #[expect(clippy::too_many_arguments)]
136 pub fn new(
137 environment: KrakenEnvironment,
138 base_url_override: Option<String>,
139 timeout_secs: u64,
140 max_retries: Option<u32>,
141 retry_delay_ms: Option<u64>,
142 retry_delay_max_ms: Option<u64>,
143 proxy_url: Option<String>,
144 max_requests_per_second: u32,
145 ) -> anyhow::Result<Self> {
146 let retry_config = RetryConfig {
147 max_retries: max_retries.unwrap_or(3),
148 initial_delay_ms: retry_delay_ms.unwrap_or(1000),
149 max_delay_ms: retry_delay_max_ms.unwrap_or(10_000),
150 backoff_factor: 2.0,
151 jitter_ms: 1000,
152 operation_timeout_ms: Some(60_000),
153 immediate_first: false,
154 max_elapsed_ms: Some(180_000),
155 };
156
157 let retry_manager = RetryManager::new(retry_config);
158 let base_url = base_url_override.unwrap_or_else(|| {
159 get_kraken_http_base_url(KrakenProductType::Futures, environment).to_string()
160 });
161
162 Ok(Self {
163 base_url,
164 client: HttpClient::new(
165 Self::default_headers(),
166 vec![],
167 Self::rate_limiter_quotas(max_requests_per_second)?,
168 Some(Self::default_quota(max_requests_per_second)?),
169 Some(timeout_secs),
170 proxy_url,
171 )
172 .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {e}"))?,
173 credential: None,
174 retry_manager,
175 cancellation_token: CancellationToken::new(),
176 clock: get_atomic_clock_realtime(),
177 auth_mutex: tokio::sync::Mutex::new(()),
178 })
179 }
180
181 #[expect(clippy::too_many_arguments)]
183 pub fn with_credentials(
184 api_key: String,
185 api_secret: String,
186 environment: KrakenEnvironment,
187 base_url_override: Option<String>,
188 timeout_secs: u64,
189 max_retries: Option<u32>,
190 retry_delay_ms: Option<u64>,
191 retry_delay_max_ms: Option<u64>,
192 proxy_url: Option<String>,
193 max_requests_per_second: u32,
194 ) -> anyhow::Result<Self> {
195 let retry_config = RetryConfig {
196 max_retries: max_retries.unwrap_or(3),
197 initial_delay_ms: retry_delay_ms.unwrap_or(1000),
198 max_delay_ms: retry_delay_max_ms.unwrap_or(10_000),
199 backoff_factor: 2.0,
200 jitter_ms: 1000,
201 operation_timeout_ms: Some(60_000),
202 immediate_first: false,
203 max_elapsed_ms: Some(180_000),
204 };
205
206 let retry_manager = RetryManager::new(retry_config);
207 let base_url = base_url_override.unwrap_or_else(|| {
208 get_kraken_http_base_url(KrakenProductType::Futures, environment).to_string()
209 });
210
211 Ok(Self {
212 base_url,
213 client: HttpClient::new(
214 Self::default_headers(),
215 vec![],
216 Self::rate_limiter_quotas(max_requests_per_second)?,
217 Some(Self::default_quota(max_requests_per_second)?),
218 Some(timeout_secs),
219 proxy_url,
220 )
221 .map_err(|e| anyhow::anyhow!("Failed to create HTTP client: {e}"))?,
222 credential: Some(KrakenCredential::new(api_key, api_secret)),
223 retry_manager,
224 cancellation_token: CancellationToken::new(),
225 clock: get_atomic_clock_realtime(),
226 auth_mutex: tokio::sync::Mutex::new(()),
227 })
228 }
229
230 fn generate_nonce(&self) -> u64 {
235 self.clock.get_time_ns().as_u64()
236 }
237
238 pub fn base_url(&self) -> &str {
240 &self.base_url
241 }
242
243 pub fn credential(&self) -> Option<&KrakenCredential> {
245 self.credential.as_ref()
246 }
247
248 pub fn cancel_all_requests(&self) {
250 self.cancellation_token.cancel();
251 }
252
253 pub fn cancellation_token(&self) -> &CancellationToken {
255 &self.cancellation_token
256 }
257
258 fn default_headers() -> HashMap<String, String> {
259 HashMap::from([(USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string())])
260 }
261
262 fn default_quota(max_requests_per_second: u32) -> anyhow::Result<Quota> {
263 let burst = NonZeroU32::new(max_requests_per_second).unwrap_or(
264 NonZeroU32::new(KRAKEN_FUTURES_DEFAULT_RATE_LIMIT_PER_SECOND).expect("non-zero"),
265 );
266 Quota::per_second(burst).ok_or_else(|| {
267 anyhow::anyhow!(
268 "Invalid max_requests_per_second: {max_requests_per_second} exceeds maximum"
269 )
270 })
271 }
272
273 fn rate_limiter_quotas(max_requests_per_second: u32) -> anyhow::Result<Vec<(String, Quota)>> {
274 Ok(vec![(
275 KRAKEN_GLOBAL_RATE_KEY.to_string(),
276 Self::default_quota(max_requests_per_second)?,
277 )])
278 }
279
280 fn rate_limit_keys(endpoint: &str) -> Vec<String> {
281 let normalized = endpoint.split('?').next().unwrap_or(endpoint);
282 let route = format!("kraken:futures:{normalized}");
283 vec![KRAKEN_GLOBAL_RATE_KEY.to_string(), route]
284 }
285
286 async fn send_request<T: DeserializeOwned>(
287 &self,
288 method: Method,
289 endpoint: &str,
290 url: String,
291 authenticate: bool,
292 ) -> anyhow::Result<T, KrakenHttpError> {
293 let _guard = if authenticate {
297 Some(self.auth_mutex.lock().await)
298 } else {
299 None
300 };
301
302 let endpoint = endpoint.to_string();
303 let method_clone = method.clone();
304 let url_clone = url.clone();
305 let credential = self.credential.clone();
306
307 let operation = || {
308 let url = url_clone.clone();
309 let method = method_clone.clone();
310 let endpoint = endpoint.clone();
311 let credential = credential.clone();
312
313 async move {
314 let mut headers = Self::default_headers();
315
316 if authenticate {
317 let cred = credential.as_ref().ok_or_else(|| {
318 KrakenHttpError::AuthenticationError(
319 "Missing credentials for authenticated request".to_string(),
320 )
321 })?;
322
323 let nonce = self.generate_nonce();
324
325 let signature = cred.sign_futures(&endpoint, "", nonce).map_err(|e| {
326 KrakenHttpError::AuthenticationError(format!("Failed to sign request: {e}"))
327 })?;
328
329 let base_url = &self.base_url;
330 log::debug!(
331 "Kraken Futures auth: endpoint={endpoint}, nonce={nonce}, base_url={base_url}"
332 );
333
334 headers.insert("APIKey".to_string(), cred.api_key().to_string());
335 headers.insert("Authent".to_string(), signature);
336 headers.insert("Nonce".to_string(), nonce.to_string());
337 }
338
339 let rate_limit_keys = Self::rate_limit_keys(&endpoint);
340
341 let response = self
342 .client
343 .request(
344 method,
345 url,
346 None,
347 Some(headers),
348 None,
349 None,
350 Some(rate_limit_keys),
351 )
352 .await
353 .map_err(|e| KrakenHttpError::NetworkError(e.to_string()))?;
354
355 let status = response.status.as_u16();
356 if status >= 400 {
357 let body = String::from_utf8_lossy(&response.body).to_string();
358 if status == 401 || status == 403 {
360 return Err(KrakenHttpError::AuthenticationError(format!(
361 "HTTP error {status}: {body}"
362 )));
363 }
364 return Err(KrakenHttpError::NetworkError(format!(
365 "HTTP error {status}: {body}"
366 )));
367 }
368
369 let response_text = String::from_utf8(response.body.to_vec()).map_err(|e| {
370 KrakenHttpError::ParseError(format!("Failed to parse response as UTF-8: {e}"))
371 })?;
372
373 serde_json::from_str(&response_text).map_err(|e| {
374 KrakenHttpError::ParseError(format!(
375 "Failed to deserialize futures response: {e}"
376 ))
377 })
378 }
379 };
380
381 let should_retry = kraken_http_should_retry;
382 let create_error = |msg: String| -> KrakenHttpError { KrakenHttpError::NetworkError(msg) };
383
384 self.retry_manager
385 .execute_with_retry_with_cancel(
386 &endpoint,
387 operation,
388 should_retry,
389 create_error,
390 &self.cancellation_token,
391 )
392 .await
393 }
394
395 async fn send_get_with_query<T: DeserializeOwned>(
400 &self,
401 endpoint: &str,
402 url: String,
403 query_string: &str,
404 ) -> anyhow::Result<T, KrakenHttpError> {
405 let _guard = self.auth_mutex.lock().await;
406
407 if self.cancellation_token.is_cancelled() {
408 return Err(KrakenHttpError::NetworkError(
409 "Request cancelled".to_string(),
410 ));
411 }
412
413 let credential = self.credential.as_ref().ok_or_else(|| {
414 KrakenHttpError::AuthenticationError("Missing credentials".to_string())
415 })?;
416
417 let nonce = self.generate_nonce();
418
419 let signature = credential
421 .sign_futures(endpoint, query_string, nonce)
422 .map_err(|e| {
423 KrakenHttpError::AuthenticationError(format!("Failed to sign request: {e}"))
424 })?;
425
426 log::debug!(
427 "Kraken Futures GET with query: endpoint={endpoint}, query={query_string}, nonce={nonce}"
428 );
429
430 let mut headers = Self::default_headers();
431 headers.insert("APIKey".to_string(), credential.api_key().to_string());
432 headers.insert("Authent".to_string(), signature);
433 headers.insert("Nonce".to_string(), nonce.to_string());
434
435 let rate_limit_keys = Self::rate_limit_keys(endpoint);
436
437 let response = self
438 .client
439 .request(
440 Method::GET,
441 url,
442 None,
443 Some(headers),
444 None,
445 None,
446 Some(rate_limit_keys),
447 )
448 .await
449 .map_err(|e| KrakenHttpError::NetworkError(e.to_string()))?;
450
451 let status = response.status.as_u16();
452 if status >= 400 {
453 let body = String::from_utf8_lossy(&response.body).to_string();
454
455 if status == 401 || status == 403 {
456 return Err(KrakenHttpError::AuthenticationError(format!(
457 "HTTP error {status}: {body}"
458 )));
459 }
460 return Err(KrakenHttpError::NetworkError(format!(
461 "HTTP error {status}: {body}"
462 )));
463 }
464
465 let response_text = String::from_utf8(response.body.to_vec()).map_err(|e| {
466 KrakenHttpError::ParseError(format!("Failed to parse response as UTF-8: {e}"))
467 })?;
468
469 serde_json::from_str(&response_text).map_err(|e| {
470 KrakenHttpError::ParseError(format!("Failed to deserialize futures response: {e}"))
471 })
472 }
473
474 async fn send_request_with_body<T: DeserializeOwned>(
475 &self,
476 endpoint: &str,
477 params: HashMap<String, String>,
478 ) -> anyhow::Result<T, KrakenHttpError> {
479 let post_data = serde_urlencoded::to_string(¶ms)
480 .map_err(|e| KrakenHttpError::ParseError(format!("Failed to encode params: {e}")))?;
481 self.send_authenticated_post(endpoint, post_data).await
482 }
483
484 async fn send_request_with_params<P: serde::Serialize, T: DeserializeOwned>(
486 &self,
487 endpoint: &str,
488 params: &P,
489 ) -> anyhow::Result<T, KrakenHttpError> {
490 let post_data = serde_urlencoded::to_string(params)
491 .map_err(|e| KrakenHttpError::ParseError(format!("Failed to encode params: {e}")))?;
492 self.send_authenticated_post(endpoint, post_data).await
493 }
494
495 async fn send_authenticated_post<T: DeserializeOwned>(
497 &self,
498 endpoint: &str,
499 post_data: String,
500 ) -> anyhow::Result<T, KrakenHttpError> {
501 if self.cancellation_token.is_cancelled() {
502 return Err(KrakenHttpError::NetworkError(
503 "Request cancelled".to_string(),
504 ));
505 }
506
507 let _guard = self.auth_mutex.lock().await;
509
510 if self.cancellation_token.is_cancelled() {
511 return Err(KrakenHttpError::NetworkError(
512 "Request cancelled".to_string(),
513 ));
514 }
515
516 let credential = self.credential.as_ref().ok_or_else(|| {
517 KrakenHttpError::AuthenticationError("Missing credentials".to_string())
518 })?;
519
520 let nonce = self.generate_nonce();
521 log::debug!("Generated nonce {nonce} for {endpoint}");
522
523 let signature = credential
524 .sign_futures(endpoint, &post_data, nonce)
525 .map_err(|e| {
526 KrakenHttpError::AuthenticationError(format!("Failed to sign request: {e}"))
527 })?;
528
529 let url = format!("{}{endpoint}", self.base_url);
530 let mut headers = Self::default_headers();
531 headers.insert(
532 "Content-Type".to_string(),
533 "application/x-www-form-urlencoded".to_string(),
534 );
535 headers.insert("APIKey".to_string(), credential.api_key().to_string());
536 headers.insert("Authent".to_string(), signature);
537 headers.insert("Nonce".to_string(), nonce.to_string());
538
539 let rate_limit_keys = Self::rate_limit_keys(endpoint);
540
541 let response = self
542 .client
543 .request(
544 Method::POST,
545 url,
546 None,
547 Some(headers),
548 Some(post_data.into_bytes()),
549 None,
550 Some(rate_limit_keys),
551 )
552 .await
553 .map_err(|e| KrakenHttpError::NetworkError(e.to_string()))?;
554
555 if response.status.as_u16() >= 400 {
556 let status = response.status.as_u16();
557 let body = String::from_utf8_lossy(&response.body).to_string();
558 return Err(KrakenHttpError::NetworkError(format!(
559 "HTTP error {status}: {body}"
560 )));
561 }
562
563 let response_text = String::from_utf8(response.body.to_vec()).map_err(|e| {
564 KrakenHttpError::ParseError(format!("Failed to parse response as UTF-8: {e}"))
565 })?;
566
567 serde_json::from_str(&response_text).map_err(|e| {
568 log::error!("Failed to parse response from {endpoint}: {response_text}");
569 KrakenHttpError::ParseError(format!("Failed to deserialize response: {e}"))
570 })
571 }
572
573 pub async fn get_instruments(
575 &self,
576 ) -> anyhow::Result<FuturesInstrumentsResponse, KrakenHttpError> {
577 let endpoint = "/derivatives/api/v3/instruments";
578 let url = format!("{}{endpoint}", self.base_url);
579
580 self.send_request(Method::GET, endpoint, url, false).await
581 }
582
583 pub async fn get_tickers(&self) -> anyhow::Result<FuturesTickersResponse, KrakenHttpError> {
585 let endpoint = "/derivatives/api/v3/tickers";
586 let url = format!("{}{endpoint}", self.base_url);
587
588 self.send_request(Method::GET, endpoint, url, false).await
589 }
590
591 pub async fn get_orderbook(
593 &self,
594 symbol: &str,
595 ) -> anyhow::Result<FuturesOrderBookResponse, KrakenHttpError> {
596 let endpoint = format!("/derivatives/api/v3/orderbook?symbol={symbol}");
597 let url = format!("{}{endpoint}", self.base_url);
598
599 self.send_request(Method::GET, &endpoint, url, false).await
600 }
601
602 pub async fn get_historical_funding_rates(
604 &self,
605 symbol: &str,
606 ) -> anyhow::Result<FuturesHistoricalFundingRatesResponse, KrakenHttpError> {
607 let endpoint = format!("/derivatives/api/v4/historicalfundingrates?symbol={symbol}");
608 let url = format!("{}{endpoint}", self.base_url);
609
610 self.send_request(Method::GET, &endpoint, url, false).await
611 }
612
613 pub async fn get_ohlc(
615 &self,
616 tick_type: &str,
617 symbol: &str,
618 resolution: &str,
619 from: Option<i64>,
620 to: Option<i64>,
621 ) -> anyhow::Result<FuturesCandlesResponse, KrakenHttpError> {
622 let endpoint = format!("/api/charts/v1/{tick_type}/{symbol}/{resolution}");
623
624 let mut url = format!("{}{endpoint}", self.base_url);
625
626 let mut query_params = Vec::new();
627
628 if let Some(from_ts) = from {
629 query_params.push(format!("from={from_ts}"));
630 }
631
632 if let Some(to_ts) = to {
633 query_params.push(format!("to={to_ts}"));
634 }
635
636 if !query_params.is_empty() {
637 url.push('?');
638 url.push_str(&query_params.join("&"));
639 }
640
641 self.send_request(Method::GET, &endpoint, url, false).await
642 }
643
644 pub async fn get_public_executions(
646 &self,
647 symbol: &str,
648 since: Option<i64>,
649 before: Option<i64>,
650 sort: Option<&str>,
651 continuation_token: Option<&str>,
652 ) -> anyhow::Result<FuturesPublicExecutionsResponse, KrakenHttpError> {
653 let endpoint = format!("/api/history/v3/market/{symbol}/executions");
654
655 let mut url = format!("{}{endpoint}", self.base_url);
656
657 let mut query_params = Vec::new();
658
659 if let Some(since_ts) = since {
660 query_params.push(format!("since={since_ts}"));
661 }
662
663 if let Some(before_ts) = before {
664 query_params.push(format!("before={before_ts}"));
665 }
666
667 if let Some(sort_order) = sort {
668 query_params.push(format!("sort={sort_order}"));
669 }
670
671 if let Some(token) = continuation_token {
672 query_params.push(format!("continuationToken={token}"));
673 }
674
675 if !query_params.is_empty() {
676 url.push('?');
677 url.push_str(&query_params.join("&"));
678 }
679
680 self.send_request(Method::GET, &endpoint, url, false).await
681 }
682
683 pub async fn get_open_orders(
685 &self,
686 ) -> anyhow::Result<FuturesOpenOrdersResponse, KrakenHttpError> {
687 if self.credential.is_none() {
688 return Err(KrakenHttpError::AuthenticationError(
689 "API credentials required for futures open orders".to_string(),
690 ));
691 }
692
693 let endpoint = "/derivatives/api/v3/openorders";
694 let url = format!("{}{endpoint}", self.base_url);
695
696 self.send_request(Method::GET, endpoint, url, true).await
697 }
698
699 pub async fn get_order_events(
701 &self,
702 before: Option<i64>,
703 since: Option<i64>,
704 continuation_token: Option<&str>,
705 ) -> anyhow::Result<FuturesOrderEventsResponse, KrakenHttpError> {
706 if self.credential.is_none() {
707 return Err(KrakenHttpError::AuthenticationError(
708 "API credentials required for futures order events".to_string(),
709 ));
710 }
711
712 let endpoint = "/api/history/v2/orders";
713 let mut query_params = Vec::new();
714
715 if let Some(before_ts) = before {
716 query_params.push(format!("before={before_ts}"));
717 }
718
719 if let Some(since_ts) = since {
720 query_params.push(format!("since={since_ts}"));
721 }
722
723 if let Some(token) = continuation_token {
724 query_params.push(format!("continuation_token={token}"));
725 }
726
727 let query_string = query_params.join("&");
729 let url = if query_string.is_empty() {
730 format!("{}{endpoint}", self.base_url)
731 } else {
732 format!("{}{endpoint}?{query_string}", self.base_url)
733 };
734
735 self.send_get_with_query(endpoint, url, &query_string).await
738 }
739
740 pub async fn get_fills(
742 &self,
743 last_fill_time: Option<&str>,
744 ) -> anyhow::Result<FuturesFillsResponse, KrakenHttpError> {
745 if self.credential.is_none() {
746 return Err(KrakenHttpError::AuthenticationError(
747 "API credentials required for futures fills".to_string(),
748 ));
749 }
750
751 let endpoint = "/derivatives/api/v3/fills";
752 let query_string = last_fill_time
753 .map(|t| format!("lastFillTime={t}"))
754 .unwrap_or_default();
755
756 let url = if query_string.is_empty() {
757 format!("{}{endpoint}", self.base_url)
758 } else {
759 format!("{}{endpoint}?{query_string}", self.base_url)
760 };
761
762 self.send_get_with_query(endpoint, url, &query_string).await
764 }
765
766 pub async fn get_open_positions(
768 &self,
769 ) -> anyhow::Result<FuturesOpenPositionsResponse, KrakenHttpError> {
770 if self.credential.is_none() {
771 return Err(KrakenHttpError::AuthenticationError(
772 "API credentials required for futures open positions".to_string(),
773 ));
774 }
775
776 let endpoint = "/derivatives/api/v3/openpositions";
777 let url = format!("{}{endpoint}", self.base_url);
778
779 self.send_request(Method::GET, endpoint, url, true).await
780 }
781
782 pub async fn get_accounts(&self) -> anyhow::Result<FuturesAccountsResponse, KrakenHttpError> {
784 if self.credential.is_none() {
785 return Err(KrakenHttpError::AuthenticationError(
786 "API credentials required for futures accounts".to_string(),
787 ));
788 }
789
790 let endpoint = "/derivatives/api/v3/accounts";
791 let url = format!("{}{endpoint}", self.base_url);
792
793 self.send_request(Method::GET, endpoint, url, true).await
794 }
795
796 pub async fn send_order(
798 &self,
799 params: HashMap<String, String>,
800 ) -> anyhow::Result<FuturesSendOrderResponse, KrakenHttpError> {
801 if self.credential.is_none() {
802 return Err(KrakenHttpError::AuthenticationError(
803 "API credentials required for sending orders".to_string(),
804 ));
805 }
806
807 let endpoint = "/derivatives/api/v3/sendorder";
808 self.send_request_with_body(endpoint, params).await
809 }
810
811 pub async fn send_order_params(
813 &self,
814 params: &KrakenFuturesSendOrderParams,
815 ) -> anyhow::Result<FuturesSendOrderResponse, KrakenHttpError> {
816 if self.credential.is_none() {
817 return Err(KrakenHttpError::AuthenticationError(
818 "API credentials required for sending orders".to_string(),
819 ));
820 }
821
822 let endpoint = "/derivatives/api/v3/sendorder";
823 self.send_request_with_params(endpoint, params).await
824 }
825
826 pub async fn cancel_order(
828 &self,
829 order_id: Option<String>,
830 cli_ord_id: Option<String>,
831 ) -> anyhow::Result<FuturesCancelOrderResponse, KrakenHttpError> {
832 if self.credential.is_none() {
833 return Err(KrakenHttpError::AuthenticationError(
834 "API credentials required for canceling orders".to_string(),
835 ));
836 }
837
838 let mut params = HashMap::new();
839
840 if let Some(id) = order_id {
841 params.insert("order_id".to_string(), id);
842 }
843
844 if let Some(id) = cli_ord_id {
845 params.insert("cliOrdId".to_string(), id);
846 }
847
848 let endpoint = "/derivatives/api/v3/cancelorder";
849 self.send_request_with_body(endpoint, params).await
850 }
851
852 pub async fn edit_order(
854 &self,
855 params: &KrakenFuturesEditOrderParams,
856 ) -> anyhow::Result<FuturesEditOrderResponse, KrakenHttpError> {
857 if self.credential.is_none() {
858 return Err(KrakenHttpError::AuthenticationError(
859 "API credentials required for editing orders".to_string(),
860 ));
861 }
862
863 let endpoint = "/derivatives/api/v3/editorder";
864 self.send_request_with_params(endpoint, params).await
865 }
866
867 pub async fn batch_order(
869 &self,
870 params: HashMap<String, String>,
871 ) -> anyhow::Result<FuturesBatchOrderResponse, KrakenHttpError> {
872 if self.credential.is_none() {
873 return Err(KrakenHttpError::AuthenticationError(
874 "API credentials required for batch orders".to_string(),
875 ));
876 }
877
878 let endpoint = "/derivatives/api/v3/batchorder";
879 self.send_request_with_body(endpoint, params).await
880 }
881
882 pub async fn cancel_orders_batch(
884 &self,
885 order_ids: Vec<String>,
886 ) -> anyhow::Result<FuturesBatchCancelResponse, KrakenHttpError> {
887 let batch_items: Vec<KrakenFuturesBatchCancelItem> = order_ids
888 .into_iter()
889 .map(KrakenFuturesBatchCancelItem::from_order_id)
890 .collect();
891
892 self.cancel_order_items_batch(batch_items).await
893 }
894
895 pub async fn cancel_order_items_batch(
898 &self,
899 batch_items: Vec<KrakenFuturesBatchCancelItem>,
900 ) -> anyhow::Result<FuturesBatchCancelResponse, KrakenHttpError> {
901 if self.credential.is_none() {
902 return Err(KrakenHttpError::AuthenticationError(
903 "API credentials required for batch orders".to_string(),
904 ));
905 }
906
907 let params = KrakenFuturesBatchOrderParams::new(batch_items);
908 let post_data = params
909 .to_body()
910 .map_err(|e| KrakenHttpError::ParseError(format!("Failed to serialize batch: {e}")))?;
911
912 let endpoint = "/derivatives/api/v3/batchorder";
913 self.send_authenticated_post(endpoint, post_data).await
914 }
915
916 pub async fn submit_orders_batch(
918 &self,
919 items: Vec<KrakenFuturesBatchSendItem>,
920 ) -> anyhow::Result<FuturesBatchOrderResponse, KrakenHttpError> {
921 if self.credential.is_none() {
922 return Err(KrakenHttpError::AuthenticationError(
923 "API credentials required for batch orders".to_string(),
924 ));
925 }
926
927 let params = KrakenFuturesBatchOrderParams::new(items);
928 let post_data = params
929 .to_body()
930 .map_err(|e| KrakenHttpError::ParseError(format!("Failed to serialize batch: {e}")))?;
931
932 let endpoint = "/derivatives/api/v3/batchorder";
933 self.send_authenticated_post(endpoint, post_data).await
934 }
935
936 pub async fn edit_orders_batch(
938 &self,
939 items: Vec<KrakenFuturesBatchEditItem>,
940 ) -> anyhow::Result<FuturesBatchOrderResponse, KrakenHttpError> {
941 if self.credential.is_none() {
942 return Err(KrakenHttpError::AuthenticationError(
943 "API credentials required for batch orders".to_string(),
944 ));
945 }
946
947 let params = KrakenFuturesBatchOrderParams::new(items);
948 let post_data = params
949 .to_body()
950 .map_err(|e| KrakenHttpError::ParseError(format!("Failed to serialize batch: {e}")))?;
951
952 let endpoint = "/derivatives/api/v3/batchorder";
953 self.send_authenticated_post(endpoint, post_data).await
954 }
955
956 pub async fn cancel_all_orders(
958 &self,
959 symbol: Option<String>,
960 ) -> anyhow::Result<FuturesCancelAllOrdersResponse, KrakenHttpError> {
961 if self.credential.is_none() {
962 return Err(KrakenHttpError::AuthenticationError(
963 "API credentials required for canceling orders".to_string(),
964 ));
965 }
966
967 let mut params = HashMap::new();
968
969 if let Some(sym) = symbol {
970 params.insert("symbol".to_string(), sym);
971 }
972
973 let endpoint = "/derivatives/api/v3/cancelallorders";
974 self.send_request_with_body(endpoint, params).await
975 }
976}
977
978#[cfg_attr(
984 feature = "python",
985 pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.kraken", from_py_object)
986)]
987#[cfg_attr(
988 feature = "python",
989 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.adapters.kraken")
990)]
991pub struct KrakenFuturesHttpClient {
992 pub(crate) inner: Arc<KrakenFuturesRawHttpClient>,
993 pub(crate) instruments_cache: Arc<AtomicMap<Ustr, InstrumentAny>>,
994 clock: &'static AtomicTime,
995 cache_initialized: Arc<AtomicBool>,
996}
997
998impl Clone for KrakenFuturesHttpClient {
999 fn clone(&self) -> Self {
1000 Self {
1001 inner: self.inner.clone(),
1002 instruments_cache: self.instruments_cache.clone(),
1003 cache_initialized: self.cache_initialized.clone(),
1004 clock: self.clock,
1005 }
1006 }
1007}
1008
1009impl Default for KrakenFuturesHttpClient {
1010 fn default() -> Self {
1011 Self::new(
1012 KrakenEnvironment::Live,
1013 None,
1014 60,
1015 None,
1016 None,
1017 None,
1018 None,
1019 KRAKEN_FUTURES_DEFAULT_RATE_LIMIT_PER_SECOND,
1020 )
1021 .expect("Failed to create default KrakenFuturesHttpClient")
1022 }
1023}
1024
1025impl Debug for KrakenFuturesHttpClient {
1026 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1027 f.debug_struct(stringify!(KrakenFuturesHttpClient))
1028 .field("inner", &self.inner)
1029 .finish()
1030 }
1031}
1032
1033impl KrakenFuturesHttpClient {
1034 #[expect(clippy::too_many_arguments)]
1036 pub fn new(
1037 environment: KrakenEnvironment,
1038 base_url_override: Option<String>,
1039 timeout_secs: u64,
1040 max_retries: Option<u32>,
1041 retry_delay_ms: Option<u64>,
1042 retry_delay_max_ms: Option<u64>,
1043 proxy_url: Option<String>,
1044 max_requests_per_second: u32,
1045 ) -> anyhow::Result<Self> {
1046 Ok(Self {
1047 inner: Arc::new(KrakenFuturesRawHttpClient::new(
1048 environment,
1049 base_url_override,
1050 timeout_secs,
1051 max_retries,
1052 retry_delay_ms,
1053 retry_delay_max_ms,
1054 proxy_url,
1055 max_requests_per_second,
1056 )?),
1057 instruments_cache: Arc::new(AtomicMap::new()),
1058 cache_initialized: Arc::new(AtomicBool::new(false)),
1059 clock: get_atomic_clock_realtime(),
1060 })
1061 }
1062
1063 #[expect(clippy::too_many_arguments)]
1065 pub fn with_credentials(
1066 api_key: String,
1067 api_secret: String,
1068 environment: KrakenEnvironment,
1069 base_url_override: Option<String>,
1070 timeout_secs: u64,
1071 max_retries: Option<u32>,
1072 retry_delay_ms: Option<u64>,
1073 retry_delay_max_ms: Option<u64>,
1074 proxy_url: Option<String>,
1075 max_requests_per_second: u32,
1076 ) -> anyhow::Result<Self> {
1077 Ok(Self {
1078 inner: Arc::new(KrakenFuturesRawHttpClient::with_credentials(
1079 api_key,
1080 api_secret,
1081 environment,
1082 base_url_override,
1083 timeout_secs,
1084 max_retries,
1085 retry_delay_ms,
1086 retry_delay_max_ms,
1087 proxy_url,
1088 max_requests_per_second,
1089 )?),
1090 instruments_cache: Arc::new(AtomicMap::new()),
1091 cache_initialized: Arc::new(AtomicBool::new(false)),
1092 clock: get_atomic_clock_realtime(),
1093 })
1094 }
1095
1096 #[expect(clippy::too_many_arguments)]
1103 pub fn from_env(
1104 environment: KrakenEnvironment,
1105 base_url_override: Option<String>,
1106 timeout_secs: u64,
1107 max_retries: Option<u32>,
1108 retry_delay_ms: Option<u64>,
1109 retry_delay_max_ms: Option<u64>,
1110 proxy_url: Option<String>,
1111 max_requests_per_second: u32,
1112 ) -> anyhow::Result<Self> {
1113 let demo = environment == KrakenEnvironment::Demo;
1114
1115 if let Some(credential) = KrakenCredential::from_env_futures(demo) {
1116 let (api_key, api_secret) = credential.into_parts();
1117 Self::with_credentials(
1118 api_key,
1119 api_secret,
1120 environment,
1121 base_url_override,
1122 timeout_secs,
1123 max_retries,
1124 retry_delay_ms,
1125 retry_delay_max_ms,
1126 proxy_url,
1127 max_requests_per_second,
1128 )
1129 } else {
1130 Self::new(
1131 environment,
1132 base_url_override,
1133 timeout_secs,
1134 max_retries,
1135 retry_delay_ms,
1136 retry_delay_max_ms,
1137 proxy_url,
1138 max_requests_per_second,
1139 )
1140 }
1141 }
1142
1143 pub fn cancel_all_requests(&self) {
1145 self.inner.cancel_all_requests();
1146 }
1147
1148 pub fn cancellation_token(&self) -> &CancellationToken {
1150 self.inner.cancellation_token()
1151 }
1152
1153 pub fn cache_instrument(&self, instrument: InstrumentAny) {
1155 self.instruments_cache
1156 .insert(instrument.symbol().inner(), instrument);
1157 self.cache_initialized.store(true, Ordering::Release);
1158 }
1159
1160 pub fn cache_instruments(&self, instruments: &[InstrumentAny]) {
1162 self.instruments_cache.rcu(|m| {
1163 for instrument in instruments {
1164 m.insert(instrument.symbol().inner(), instrument.clone());
1165 }
1166 });
1167 self.cache_initialized.store(true, Ordering::Release);
1168 }
1169
1170 pub fn get_cached_instrument(&self, symbol: &Ustr) -> Option<InstrumentAny> {
1172 self.instruments_cache.get_cloned(symbol)
1173 }
1174
1175 fn get_instrument_by_raw_symbol(&self, raw_symbol: &str) -> Option<InstrumentAny> {
1176 self.instruments_cache
1177 .load()
1178 .values()
1179 .find(|inst| inst.raw_symbol().as_str() == raw_symbol)
1180 .cloned()
1181 }
1182
1183 fn generate_ts_init(&self) -> UnixNanos {
1184 self.clock.get_time_ns()
1185 }
1186
1187 pub async fn request_instruments(&self) -> anyhow::Result<Vec<InstrumentAny>, KrakenHttpError> {
1189 let ts_init = self.generate_ts_init();
1190 let response = self.inner.get_instruments().await?;
1191
1192 let instruments: Vec<InstrumentAny> = response
1193 .instruments
1194 .iter()
1195 .filter_map(|fut_instrument| {
1196 match parse_futures_instrument(fut_instrument, ts_init, ts_init) {
1197 Ok(instrument) => Some(instrument),
1198 Err(e) => {
1199 let symbol = &fut_instrument.symbol;
1200 log::warn!("Failed to parse futures instrument {symbol}: {e}");
1201 None
1202 }
1203 }
1204 })
1205 .collect();
1206
1207 Ok(instruments)
1208 }
1209
1210 pub async fn request_instrument_statuses(
1212 &self,
1213 ) -> anyhow::Result<AHashMap<InstrumentId, MarketStatusAction>, KrakenHttpError> {
1214 let response = self.inner.get_instruments().await?;
1215
1216 Ok(response
1217 .instruments
1218 .iter()
1219 .map(|instrument| {
1220 let instrument_id =
1221 InstrumentId::new(Symbol::new(&instrument.symbol), *KRAKEN_VENUE);
1222 let action = if instrument.tradeable {
1223 MarketStatusAction::Trading
1224 } else {
1225 MarketStatusAction::NotAvailableForTrading
1226 };
1227
1228 (instrument_id, action)
1229 })
1230 .collect())
1231 }
1232
1233 pub async fn request_mark_price(
1235 &self,
1236 instrument_id: InstrumentId,
1237 ) -> anyhow::Result<f64, KrakenHttpError> {
1238 let instrument = self
1239 .get_cached_instrument(&instrument_id.symbol.inner())
1240 .ok_or_else(|| {
1241 KrakenHttpError::ParseError(
1242 InstrumentLookupError::not_found(instrument_id).to_string(),
1243 )
1244 })?;
1245
1246 let raw_symbol = instrument.raw_symbol().to_string();
1247 let tickers = self.inner.get_tickers().await?;
1248
1249 tickers
1250 .tickers
1251 .iter()
1252 .find(|t| t.symbol == raw_symbol)
1253 .ok_or_else(|| {
1254 KrakenHttpError::ParseError(format!("Symbol {raw_symbol} not found in tickers"))
1255 })
1256 .and_then(|t| {
1257 t.mark_price.ok_or_else(|| {
1258 KrakenHttpError::ParseError(format!(
1259 "Mark price not available for {raw_symbol} (may not be available in testnet)"
1260 ))
1261 })
1262 })
1263 }
1264
1265 pub async fn request_index_price(
1266 &self,
1267 instrument_id: InstrumentId,
1268 ) -> anyhow::Result<f64, KrakenHttpError> {
1269 let instrument = self
1270 .get_cached_instrument(&instrument_id.symbol.inner())
1271 .ok_or_else(|| {
1272 KrakenHttpError::ParseError(
1273 InstrumentLookupError::not_found(instrument_id).to_string(),
1274 )
1275 })?;
1276
1277 let raw_symbol = instrument.raw_symbol().to_string();
1278 let tickers = self.inner.get_tickers().await?;
1279
1280 tickers
1281 .tickers
1282 .iter()
1283 .find(|t| t.symbol == raw_symbol)
1284 .ok_or_else(|| {
1285 KrakenHttpError::ParseError(format!("Symbol {raw_symbol} not found in tickers"))
1286 })
1287 .and_then(|t| {
1288 t.index_price.ok_or_else(|| {
1289 KrakenHttpError::ParseError(format!(
1290 "Index price not available for {raw_symbol} (may not be available in testnet)"
1291 ))
1292 })
1293 })
1294 }
1295
1296 pub async fn request_trades(
1297 &self,
1298 instrument_id: InstrumentId,
1299 start: Option<DateTime<Utc>>,
1300 end: Option<DateTime<Utc>>,
1301 limit: Option<u64>,
1302 ) -> anyhow::Result<Vec<TradeTick>, KrakenHttpError> {
1303 let instrument = self
1304 .get_cached_instrument(&instrument_id.symbol.inner())
1305 .ok_or_else(|| {
1306 KrakenHttpError::ParseError(
1307 InstrumentLookupError::not_found(instrument_id).to_string(),
1308 )
1309 })?;
1310
1311 let raw_symbol = instrument.raw_symbol().to_string();
1312 let ts_init = self.generate_ts_init();
1313
1314 let since = start.map(|dt| dt.timestamp_millis());
1315 let before = end.map(|dt| dt.timestamp_millis());
1316
1317 let sort = if start.is_some() { "asc" } else { "desc" };
1320
1321 let response = self
1322 .inner
1323 .get_public_executions(&raw_symbol, since, before, Some(sort), None)
1324 .await?;
1325
1326 let mut trades = Vec::new();
1327
1328 for element in &response.elements {
1329 let execution = &element.event.execution.execution;
1330 match parse_futures_public_execution(execution, &instrument, ts_init) {
1331 Ok(trade_tick) => trades.push(trade_tick),
1332 Err(e) => {
1333 log::warn!("Failed to parse futures trade tick: {e}");
1334 }
1335 }
1336 }
1337
1338 if start.is_none() {
1339 trades.reverse();
1340 }
1341
1342 apply_count_limit(&mut trades, start, limit);
1343
1344 Ok(trades)
1345 }
1346
1347 pub async fn request_bars(
1348 &self,
1349 bar_type: BarType,
1350 start: Option<DateTime<Utc>>,
1351 end: Option<DateTime<Utc>>,
1352 limit: Option<u64>,
1353 ) -> anyhow::Result<Vec<Bar>, KrakenHttpError> {
1354 let instrument_id = bar_type.instrument_id();
1355 let instrument = self
1356 .get_cached_instrument(&instrument_id.symbol.inner())
1357 .ok_or_else(|| {
1358 KrakenHttpError::ParseError(
1359 InstrumentLookupError::not_found(instrument_id).to_string(),
1360 )
1361 })?;
1362
1363 let raw_symbol = instrument.raw_symbol().to_string();
1364 let ts_init = self.generate_ts_init();
1365 let tick_type = "trade";
1366 let resolution = bar_type_to_futures_resolution(bar_type)
1367 .map_err(|e| KrakenHttpError::ParseError(e.to_string()))?;
1368
1369 let from = start.map(|dt| dt.timestamp());
1371 let to = end.map(|dt| dt.timestamp());
1372 let end_ns = end.map(|dt| dt.timestamp_nanos_opt().unwrap_or(0) as u64);
1373
1374 let response = self
1375 .inner
1376 .get_ohlc(tick_type, &raw_symbol, resolution, from, to)
1377 .await?;
1378
1379 let mut bars = Vec::new();
1380
1381 for candle in response.candles {
1382 let ohlc = OhlcData {
1383 time: candle.time / 1000,
1384 open: candle.open,
1385 high: candle.high,
1386 low: candle.low,
1387 close: candle.close,
1388 vwap: "0".to_string(),
1389 volume: candle.volume,
1390 count: 0,
1391 };
1392
1393 match parse_bar(&ohlc, &instrument, bar_type, ts_init) {
1394 Ok(bar) => {
1395 if let Some(end_nanos) = end_ns
1396 && bar.ts_event.as_u64() > end_nanos
1397 {
1398 continue;
1399 }
1400 bars.push(bar);
1401 }
1402 Err(e) => {
1403 log::warn!("Failed to parse futures bar: {e}");
1404 }
1405 }
1406 }
1407
1408 apply_count_limit(&mut bars, start, limit);
1411
1412 Ok(bars)
1413 }
1414
1415 pub async fn request_book_snapshot(
1417 &self,
1418 instrument_id: InstrumentId,
1419 depth: Option<u32>,
1420 ) -> anyhow::Result<OrderBook, KrakenHttpError> {
1421 let instrument = self
1422 .get_cached_instrument(&instrument_id.symbol.inner())
1423 .ok_or_else(|| {
1424 KrakenHttpError::ParseError(
1425 InstrumentLookupError::not_found(instrument_id).to_string(),
1426 )
1427 })?;
1428
1429 let raw_symbol = instrument.raw_symbol().to_string();
1430 let price_precision = instrument.price_precision();
1431 let size_precision = instrument.size_precision();
1432 let ts_event = self.generate_ts_init();
1433
1434 let response = self.inner.get_orderbook(&raw_symbol).await?;
1435 let book_data = &response.order_book;
1436
1437 let mut book = OrderBook::new(instrument_id, BookType::L2_MBP);
1438
1439 let bid_limit = depth.map_or(book_data.bids.len(), |d| {
1440 (d as usize).min(book_data.bids.len())
1441 });
1442 let ask_limit = depth.map_or(book_data.asks.len(), |d| {
1443 (d as usize).min(book_data.asks.len())
1444 });
1445
1446 for (i, level) in book_data.bids.iter().take(bid_limit).enumerate() {
1449 let price = Price::new(level.price, price_precision);
1450 let size = Quantity::new(level.qty, size_precision);
1451 let order = BookOrder::new(OrderSide::Buy, price, size, i as u64);
1452 book.add(order, 0, 0, ts_event);
1453 }
1454
1455 for (i, level) in book_data.asks.iter().take(ask_limit).enumerate() {
1456 let price = Price::new(level.price, price_precision);
1457 let size = Quantity::new(level.qty, size_precision);
1458 let order = BookOrder::new(OrderSide::Sell, price, size, (bid_limit + i) as u64);
1459 book.add(order, 0, 0, ts_event);
1460 }
1461
1462 Ok(book)
1463 }
1464
1465 pub async fn request_funding_rates(
1470 &self,
1471 instrument_id: InstrumentId,
1472 start: Option<DateTime<Utc>>,
1473 end: Option<DateTime<Utc>>,
1474 limit: Option<usize>,
1475 ) -> anyhow::Result<Vec<FundingRateUpdate>, KrakenHttpError> {
1476 let instrument = self
1477 .get_cached_instrument(&instrument_id.symbol.inner())
1478 .ok_or_else(|| {
1479 KrakenHttpError::ParseError(
1480 InstrumentLookupError::not_found(instrument_id).to_string(),
1481 )
1482 })?;
1483
1484 let raw_symbol = instrument.raw_symbol().to_string();
1485 let ts_init = self.generate_ts_init();
1486 let start_ns = start.map(|dt| dt.timestamp_nanos_opt().unwrap_or(0) as u64);
1487 let end_ns = end.map(|dt| dt.timestamp_nanos_opt().unwrap_or(0) as u64);
1488
1489 let response = self.inner.get_historical_funding_rates(&raw_symbol).await?;
1490
1491 let mut rates = Vec::new();
1492
1493 for entry in &response.rates {
1494 let ts_event = entry
1495 .timestamp
1496 .parse::<DateTime<Utc>>()
1497 .map_or(ts_init, |dt| {
1498 UnixNanos::from(dt.timestamp_nanos_opt().unwrap_or(0) as u64)
1499 });
1500
1501 if let Some(s) = start_ns
1502 && ts_event.as_u64() < s
1503 {
1504 continue;
1505 }
1506
1507 if let Some(e) = end_ns
1508 && ts_event.as_u64() > e
1509 {
1510 continue;
1511 }
1512
1513 let Some(rate) = Decimal::from_f64(entry.relative_funding_rate) else {
1514 continue;
1515 };
1516
1517 rates.push(FundingRateUpdate::new(
1518 instrument_id,
1519 rate,
1520 None,
1521 None,
1522 ts_event,
1523 ts_init,
1524 ));
1525
1526 if let Some(lim) = limit
1527 && rates.len() >= lim
1528 {
1529 break;
1530 }
1531 }
1532
1533 rates.reverse();
1535
1536 Ok(rates)
1537 }
1538
1539 pub async fn request_account_state(
1551 &self,
1552 account_id: AccountId,
1553 ) -> anyhow::Result<AccountState> {
1554 let accounts_response = self.inner.get_accounts().await?;
1555
1556 if accounts_response.result != KrakenApiResult::Success {
1557 let error_msg = accounts_response
1558 .error
1559 .unwrap_or_else(|| "Unknown error".to_string());
1560 anyhow::bail!("Failed to get futures accounts: {error_msg}");
1561 }
1562
1563 let ts_init = self.generate_ts_init();
1564
1565 let mut balances: Vec<AccountBalance> = Vec::new();
1566 let mut margins: Vec<MarginBalance> = Vec::new();
1567
1568 for account in accounts_response.accounts.values() {
1569 match account.account_type {
1570 KrakenFuturesAccountType::MultiCollateralMarginAccount => {
1571 parse_multi_collateral_balances(account, &mut balances);
1572 parse_multi_collateral_margins(account, &mut margins);
1573 }
1574 KrakenFuturesAccountType::MarginAccount => {
1575 parse_margin_account_balances(account, &mut balances);
1576 parse_margin_account_margins(account, &mut margins);
1577 }
1578 KrakenFuturesAccountType::CashAccount => {
1579 parse_cash_account_balances(account, &mut balances);
1580 }
1581 KrakenFuturesAccountType::Unknown => {
1582 log::debug!("Unknown account type: {:?}", account.account_type);
1583 }
1584 }
1585 }
1586
1587 Ok(AccountState::new(
1588 account_id,
1589 AccountType::Margin,
1590 balances,
1591 margins,
1592 true,
1593 UUID4::new(),
1594 ts_init,
1595 ts_init,
1596 None,
1597 ))
1598 }
1599
1600 pub async fn request_order_status_reports(
1601 &self,
1602 account_id: AccountId,
1603 instrument_id: Option<InstrumentId>,
1604 start: Option<DateTime<Utc>>,
1605 end: Option<DateTime<Utc>>,
1606 open_only: bool,
1607 ) -> anyhow::Result<Vec<OrderStatusReport>> {
1608 let ts_init = self.generate_ts_init();
1609 let mut all_reports = Vec::new();
1610
1611 let response = self
1612 .inner
1613 .get_open_orders()
1614 .await
1615 .map_err(|e| anyhow::anyhow!("get_open_orders failed: {e}"))?;
1616
1617 if response.result != KrakenApiResult::Success {
1618 let error_msg = response
1619 .error
1620 .unwrap_or_else(|| "Unknown error".to_string());
1621 anyhow::bail!("Failed to get open orders: {error_msg}");
1622 }
1623
1624 for order in &response.open_orders {
1625 if let Some(ref target_id) = instrument_id {
1626 let instrument = self.get_cached_instrument(&target_id.symbol.inner());
1627 if let Some(inst) = instrument
1628 && inst.raw_symbol().as_str() != order.symbol
1629 {
1630 continue;
1631 }
1632 }
1633
1634 if let Some(instrument) = self.get_instrument_by_raw_symbol(&order.symbol) {
1635 match parse_futures_order_status_report(order, &instrument, account_id, ts_init) {
1636 Ok(report) => all_reports.push(report),
1637 Err(e) => {
1638 let order_id = &order.order_id;
1639 log::warn!("Failed to parse futures order {order_id}: {e}");
1640 }
1641 }
1642 }
1643 }
1644
1645 if !open_only {
1646 let start_ms = start.map(|dt| dt.timestamp_millis());
1648 let end_ms = end.map(|dt| dt.timestamp_millis());
1649 let response = self
1650 .inner
1651 .get_order_events(end_ms, start_ms, None)
1652 .await
1653 .map_err(|e| anyhow::anyhow!("get_order_events failed: {e}"))?;
1654
1655 for event_wrapper in response.order_events {
1656 let event = &event_wrapper.order;
1657
1658 if let Some(ref target_id) = instrument_id {
1659 let instrument = self.get_cached_instrument(&target_id.symbol.inner());
1660 if let Some(inst) = instrument
1661 && inst.raw_symbol().as_str() != event.symbol
1662 {
1663 continue;
1664 }
1665 }
1666
1667 if let Some(instrument) = self.get_instrument_by_raw_symbol(&event.symbol) {
1668 match parse_futures_order_event_status_report(
1669 event,
1670 Some(event_wrapper.event_type),
1671 &instrument,
1672 account_id,
1673 ts_init,
1674 ) {
1675 Ok(report) => all_reports.push(report),
1676 Err(e) => {
1677 let order_id = &event.order_id;
1678 log::warn!("Failed to parse futures order event {order_id}: {e}");
1679 }
1680 }
1681 }
1682 }
1683 }
1684
1685 Ok(all_reports)
1686 }
1687
1688 pub async fn request_fill_reports(
1689 &self,
1690 account_id: AccountId,
1691 instrument_id: Option<InstrumentId>,
1692 start: Option<DateTime<Utc>>,
1693 end: Option<DateTime<Utc>>,
1694 ) -> anyhow::Result<Vec<FillReport>> {
1695 let ts_init = self.generate_ts_init();
1696 let mut all_reports = Vec::new();
1697
1698 let response = self.inner.get_fills(None).await?;
1699 if response.result != KrakenApiResult::Success {
1700 let error_msg = response
1701 .error
1702 .unwrap_or_else(|| "Unknown error".to_string());
1703 anyhow::bail!("Failed to get fills: {error_msg}");
1704 }
1705
1706 let start_ms = start.map(|dt| dt.timestamp_millis());
1707 let end_ms = end.map(|dt| dt.timestamp_millis());
1708
1709 for fill in response.fills {
1710 if let Some(start_threshold) = start_ms
1711 && let Ok(fill_ts) = DateTime::parse_from_rfc3339(&fill.fill_time)
1712 {
1713 let fill_ms = fill_ts.timestamp_millis();
1714 if fill_ms < start_threshold {
1715 continue;
1716 }
1717 }
1718
1719 if let Some(end_threshold) = end_ms
1720 && let Ok(fill_ts) = DateTime::parse_from_rfc3339(&fill.fill_time)
1721 {
1722 let fill_ms = fill_ts.timestamp_millis();
1723 if fill_ms > end_threshold {
1724 continue;
1725 }
1726 }
1727
1728 if let Some(ref target_id) = instrument_id {
1729 let instrument = self.get_cached_instrument(&target_id.symbol.inner());
1730 if let Some(inst) = instrument
1731 && inst.raw_symbol().as_str() != fill.symbol
1732 {
1733 continue;
1734 }
1735 }
1736
1737 if let Some(instrument) = self.get_instrument_by_raw_symbol(&fill.symbol) {
1738 match parse_futures_fill_report(&fill, &instrument, account_id, ts_init) {
1739 Ok(report) => all_reports.push(report),
1740 Err(e) => {
1741 let fill_id = &fill.fill_id;
1742 log::warn!("Failed to parse futures fill {fill_id}: {e}");
1743 }
1744 }
1745 }
1746 }
1747
1748 Ok(all_reports)
1749 }
1750
1751 pub async fn request_position_status_reports(
1752 &self,
1753 account_id: AccountId,
1754 instrument_id: Option<InstrumentId>,
1755 ) -> anyhow::Result<Vec<PositionStatusReport>> {
1756 let ts_init = self.generate_ts_init();
1757 let mut all_reports = Vec::new();
1758
1759 let response = self.inner.get_open_positions().await?;
1760 if response.result != KrakenApiResult::Success {
1761 let error_msg = response
1762 .error
1763 .unwrap_or_else(|| "Unknown error".to_string());
1764 anyhow::bail!("Failed to get open positions: {error_msg}");
1765 }
1766
1767 for position in response.open_positions {
1768 if let Some(ref target_id) = instrument_id {
1769 let instrument = self.get_cached_instrument(&target_id.symbol.inner());
1770 if let Some(inst) = instrument
1771 && inst.raw_symbol().as_str() != position.symbol
1772 {
1773 continue;
1774 }
1775 }
1776
1777 if let Some(instrument) = self.get_instrument_by_raw_symbol(&position.symbol) {
1778 match parse_futures_position_status_report(
1779 &position,
1780 &instrument,
1781 account_id,
1782 ts_init,
1783 ) {
1784 Ok(report) => all_reports.push(report),
1785 Err(e) => {
1786 let symbol = &position.symbol;
1787 log::warn!("Failed to parse futures position {symbol}: {e}");
1788 }
1789 }
1790 }
1791 }
1792
1793 Ok(all_reports)
1794 }
1795
1796 #[expect(clippy::too_many_arguments)]
1797 fn build_send_order_params(
1798 &self,
1799 instrument_id: InstrumentId,
1800 client_order_id: ClientOrderId,
1801 order_side: OrderSide,
1802 order_type: OrderType,
1803 quantity: Quantity,
1804 time_in_force: TimeInForce,
1805 price: Option<Price>,
1806 trigger_price: Option<Price>,
1807 trigger_type: Option<TriggerType>,
1808 reduce_only: bool,
1809 post_only: bool,
1810 ) -> anyhow::Result<KrakenFuturesSendOrderParams> {
1811 let instrument = self
1812 .get_cached_instrument(&instrument_id.symbol.inner())
1813 .ok_or_else(|| InstrumentLookupError::not_found(instrument_id))?;
1814
1815 let raw_symbol = instrument.raw_symbol().inner();
1816
1817 let kraken_order_type = match order_type {
1824 OrderType::Market => KrakenFuturesOrderType::Market,
1825 OrderType::Limit => {
1826 if post_only {
1827 KrakenFuturesOrderType::Post
1828 } else {
1829 match time_in_force {
1830 TimeInForce::Ioc => KrakenFuturesOrderType::Ioc,
1831 TimeInForce::Fok => {
1832 anyhow::bail!("FOK not supported by Kraken Futures, use IOC instead")
1833 }
1834 TimeInForce::Gtd => {
1835 anyhow::bail!("GTD not supported by Kraken Futures, use GTC instead")
1836 }
1837 _ => KrakenFuturesOrderType::Limit, }
1839 }
1840 }
1841 OrderType::StopMarket | OrderType::StopLimit => KrakenFuturesOrderType::Stop,
1842 OrderType::MarketIfTouched | OrderType::LimitIfTouched => {
1843 KrakenFuturesOrderType::TakeProfit
1844 }
1845 _ => anyhow::bail!("Unsupported order type: {order_type:?}"),
1846 };
1847
1848 let kraken_side: KrakenOrderSide = order_side
1849 .try_into()
1850 .map_err(|e| anyhow::anyhow!("Invalid order side: {e}"))?;
1851
1852 let mut builder = KrakenFuturesSendOrderParamsBuilder::default();
1853 builder
1854 .cli_ord_id(truncate_cl_ord_id(&client_order_id))
1855 .broker(NAUTILUS_KRAKEN_BROKER_ID)
1856 .symbol(raw_symbol)
1857 .side(kraken_side)
1858 .size(quantity.to_string())
1859 .order_type(kraken_order_type);
1860
1861 if matches!(
1862 order_type,
1863 OrderType::StopMarket
1864 | OrderType::StopLimit
1865 | OrderType::MarketIfTouched
1866 | OrderType::LimitIfTouched
1867 ) && let Some(signal) = map_futures_trigger_signal(trigger_type)?
1868 {
1869 builder.trigger_signal(signal);
1870 }
1871
1872 match order_type {
1873 OrderType::StopMarket => {
1874 if let Some(trigger) = trigger_price {
1875 builder.stop_price(trigger.to_string());
1876 }
1877 }
1878 OrderType::StopLimit => {
1879 if let Some(trigger) = trigger_price {
1880 builder.stop_price(trigger.to_string());
1881 }
1882
1883 if let Some(limit) = price {
1884 builder.limit_price(limit.to_string());
1885 }
1886 }
1887 OrderType::MarketIfTouched | OrderType::LimitIfTouched => {
1888 if let Some(trigger) = trigger_price {
1889 builder.stop_price(trigger.to_string());
1890 }
1891
1892 if let Some(limit) = price {
1893 builder.limit_price(limit.to_string());
1894 }
1895 }
1896 _ => {
1897 if let Some(limit) = price {
1898 builder.limit_price(limit.to_string());
1899 }
1900 }
1901 }
1902
1903 if reduce_only {
1904 builder.reduce_only(true);
1905 }
1906
1907 builder
1908 .build()
1909 .map_err(|e| anyhow::anyhow!("Failed to build order params: {e}"))
1910 }
1911
1912 #[expect(clippy::too_many_arguments)]
1923 pub async fn submit_order(
1924 &self,
1925 account_id: AccountId,
1926 instrument_id: InstrumentId,
1927 client_order_id: ClientOrderId,
1928 order_side: OrderSide,
1929 order_type: OrderType,
1930 quantity: Quantity,
1931 time_in_force: TimeInForce,
1932 price: Option<Price>,
1933 trigger_price: Option<Price>,
1934 trigger_type: Option<TriggerType>,
1935 reduce_only: bool,
1936 post_only: bool,
1937 ) -> anyhow::Result<OrderStatusReport> {
1938 let instrument = self
1939 .get_cached_instrument(&instrument_id.symbol.inner())
1940 .ok_or_else(|| InstrumentLookupError::not_found(instrument_id))?;
1941
1942 let params = self.build_send_order_params(
1943 instrument_id,
1944 client_order_id,
1945 order_side,
1946 order_type,
1947 quantity,
1948 time_in_force,
1949 price,
1950 trigger_price,
1951 trigger_type,
1952 reduce_only,
1953 post_only,
1954 )?;
1955
1956 let response = self.inner.send_order_params(¶ms).await?;
1957
1958 if response.result != KrakenApiResult::Success {
1959 let error_msg = response
1960 .error
1961 .unwrap_or_else(|| "Unknown error".to_string());
1962 anyhow::bail!("Order submission failed: {error_msg}");
1963 }
1964
1965 let send_status = response
1966 .send_status
1967 .ok_or_else(|| anyhow::anyhow!("No send_status in successful response"))?;
1968
1969 let status = &send_status.status;
1970
1971 if status == "postWouldExecute" {
1973 let reason = send_status
1974 .order_events
1975 .as_ref()
1976 .and_then(|events| events.first())
1977 .and_then(|e| e.reason.clone())
1978 .unwrap_or_else(|| "Post-only order would have crossed".to_string());
1979 anyhow::bail!("POST_ONLY_REJECTED: {reason}");
1980 }
1981
1982 let venue_order_id = send_status
1983 .order_id
1984 .ok_or_else(|| anyhow::anyhow!("No order_id in send_status: {status}"))?;
1985
1986 let ts_init = self.generate_ts_init();
1987
1988 let open_orders_response = self.inner.get_open_orders().await?;
1989 if let Some(order) = open_orders_response
1990 .open_orders
1991 .iter()
1992 .find(|o| o.order_id == venue_order_id)
1993 {
1994 return parse_futures_order_status_report(order, &instrument, account_id, ts_init);
1995 }
1996
1997 if let Some(order_events) = &send_status.order_events
2000 && let Some(send_event) = order_events.first()
2001 {
2002 let event = if let Some(order_data) = &send_event.order {
2004 FuturesOrderEvent {
2005 order_id: order_data.order_id.clone(),
2006 cli_ord_id: order_data.cli_ord_id.clone(),
2007 order_type: order_data.order_type,
2008 symbol: order_data.symbol.clone(),
2009 side: order_data.side,
2010 quantity: order_data.quantity,
2011 filled: order_data.filled,
2012 limit_price: order_data.limit_price,
2013 stop_price: order_data.stop_price,
2014 timestamp: order_data.timestamp.clone(),
2015 last_update_timestamp: order_data.last_update_timestamp.clone(),
2016 reduce_only: order_data.reduce_only,
2017 }
2018 } else if let Some(trigger_data) = &send_event.order_trigger {
2019 FuturesOrderEvent {
2020 order_id: trigger_data.uid.clone(),
2021 cli_ord_id: trigger_data.client_id.clone(),
2022 order_type: trigger_data.order_type,
2023 symbol: trigger_data.symbol.clone(),
2024 side: trigger_data.side,
2025 quantity: trigger_data.quantity,
2026 filled: 0.0,
2027 limit_price: trigger_data.limit_price,
2028 stop_price: Some(trigger_data.trigger_price),
2029 timestamp: trigger_data.timestamp.clone(),
2030 last_update_timestamp: trigger_data.last_update_timestamp.clone(),
2031 reduce_only: trigger_data.reduce_only,
2032 }
2033 } else if let Some(prior_exec) = &send_event.order_prior_execution {
2034 FuturesOrderEvent {
2036 order_id: prior_exec.order_id.clone(),
2037 cli_ord_id: prior_exec.cli_ord_id.clone(),
2038 order_type: prior_exec.order_type,
2039 symbol: prior_exec.symbol.clone(),
2040 side: prior_exec.side,
2041 quantity: prior_exec.quantity,
2042 filled: send_event.amount.unwrap_or(prior_exec.quantity), limit_price: prior_exec.limit_price,
2044 stop_price: prior_exec.stop_price,
2045 timestamp: prior_exec.timestamp.clone(),
2046 last_update_timestamp: prior_exec.last_update_timestamp.clone(),
2047 reduce_only: prior_exec.reduce_only,
2048 }
2049 } else {
2050 anyhow::bail!("No order, orderTrigger, or orderPriorExecution data in event");
2051 };
2052 return parse_futures_order_event_status_report(
2053 &event,
2054 Some(send_event.event_type),
2055 &instrument,
2056 account_id,
2057 ts_init,
2058 );
2059 }
2060
2061 let events_response = self.inner.get_order_events(None, None, None).await?;
2063 let event_wrapper = events_response
2064 .order_events
2065 .iter()
2066 .find(|e| e.order.order_id == venue_order_id)
2067 .ok_or_else(|| {
2068 anyhow::anyhow!("Order not found in open orders or events: {venue_order_id}")
2069 })?;
2070
2071 parse_futures_order_event_status_report(
2072 &event_wrapper.order,
2073 Some(event_wrapper.event_type),
2074 &instrument,
2075 account_id,
2076 ts_init,
2077 )
2078 }
2079
2080 pub async fn modify_order(
2092 &self,
2093 instrument_id: InstrumentId,
2094 client_order_id: Option<ClientOrderId>,
2095 venue_order_id: Option<VenueOrderId>,
2096 quantity: Option<Quantity>,
2097 price: Option<Price>,
2098 trigger_price: Option<Price>,
2099 ) -> anyhow::Result<VenueOrderId> {
2100 let params = self.build_edit_order_params(
2101 instrument_id,
2102 client_order_id,
2103 venue_order_id,
2104 quantity,
2105 price,
2106 trigger_price,
2107 )?;
2108 let original_order_id = params.order_id.clone();
2109
2110 let response = self.inner.edit_order(¶ms).await?;
2111
2112 if response.result != KrakenApiResult::Success {
2113 let status = &response.edit_status.status;
2114 anyhow::bail!("Order modification failed: {status}");
2115 }
2116
2117 let new_venue_order_id = response
2119 .edit_status
2120 .order_id
2121 .or(original_order_id)
2122 .ok_or_else(|| anyhow::anyhow!("No order ID in edit order response"))?;
2123
2124 Ok(VenueOrderId::new(&new_venue_order_id))
2125 }
2126
2127 pub async fn cancel_order(
2137 &self,
2138 _account_id: AccountId,
2139 instrument_id: InstrumentId,
2140 client_order_id: Option<ClientOrderId>,
2141 venue_order_id: Option<VenueOrderId>,
2142 ) -> anyhow::Result<()> {
2143 let _ = self
2144 .get_cached_instrument(&instrument_id.symbol.inner())
2145 .ok_or_else(|| InstrumentLookupError::not_found(instrument_id))?;
2146
2147 let order_id = venue_order_id.as_ref().map(|id| id.to_string());
2148 let cli_ord_id = client_order_id.as_ref().map(truncate_cl_ord_id);
2149
2150 if order_id.is_none() && cli_ord_id.is_none() {
2151 anyhow::bail!("Either client_order_id or venue_order_id must be provided");
2152 }
2153
2154 let response = self.inner.cancel_order(order_id, cli_ord_id).await?;
2155
2156 if response.result != KrakenApiResult::Success {
2157 let status = &response.cancel_status.status;
2158 anyhow::bail!("Order cancellation failed: {status}");
2159 }
2160
2161 Ok(())
2162 }
2163
2164 pub async fn cancel_orders_batch(
2174 &self,
2175 venue_order_ids: Vec<VenueOrderId>,
2176 ) -> anyhow::Result<usize> {
2177 if venue_order_ids.is_empty() {
2178 return Ok(0);
2179 }
2180
2181 let mut total_cancelled = 0;
2182
2183 for chunk in venue_order_ids.chunks(BATCH_CANCEL_LIMIT) {
2184 let order_ids: Vec<String> = chunk.iter().map(|id| id.to_string()).collect();
2185 let response = self.inner.cancel_orders_batch(order_ids).await?;
2186
2187 if response.result != KrakenApiResult::Success {
2188 let error_msg = response.error.as_deref().unwrap_or("Unknown error");
2189 anyhow::bail!("Batch cancel failed: {error_msg}");
2190 }
2191
2192 let success_count = response
2193 .batch_status
2194 .iter()
2195 .filter(|s| {
2196 s.status == Some(KrakenSendStatus::Cancelled)
2197 || s.cancel_status
2198 .as_ref()
2199 .is_some_and(|cs| cs.status == KrakenSendStatus::Cancelled)
2200 })
2201 .count();
2202
2203 total_cancelled += success_count;
2204 }
2205
2206 Ok(total_cancelled)
2207 }
2208
2209 #[expect(clippy::type_complexity)]
2218 pub async fn submit_orders_batch(
2219 &self,
2220 orders: Vec<(
2221 InstrumentId,
2222 ClientOrderId,
2223 OrderSide,
2224 OrderType,
2225 Quantity,
2226 TimeInForce,
2227 Option<Price>,
2228 Option<Price>,
2229 Option<TriggerType>,
2230 bool,
2231 bool,
2232 )>,
2233 ) -> anyhow::Result<Vec<FuturesSendStatus>> {
2234 let count = orders.len();
2235 if count == 0 {
2236 return Ok(Vec::new());
2237 }
2238
2239 let mut all_statuses: Vec<Option<FuturesSendStatus>> = vec![None; count];
2242 let mut valid_items = Vec::with_capacity(count);
2243 let mut valid_indices = Vec::with_capacity(count);
2244
2245 for (
2246 idx,
2247 (
2248 instrument_id,
2249 client_order_id,
2250 order_side,
2251 order_type,
2252 quantity,
2253 time_in_force,
2254 price,
2255 trigger_price,
2256 trigger_type,
2257 reduce_only,
2258 post_only,
2259 ),
2260 ) in orders.into_iter().enumerate()
2261 {
2262 match self.build_send_order_params(
2263 instrument_id,
2264 client_order_id,
2265 order_side,
2266 order_type,
2267 quantity,
2268 time_in_force,
2269 price,
2270 trigger_price,
2271 trigger_type,
2272 reduce_only,
2273 post_only,
2274 ) {
2275 Ok(params) => {
2276 valid_items.push(KrakenFuturesBatchSendItem::from_params(
2277 params,
2278 idx.to_string(),
2279 ));
2280 valid_indices.push(idx);
2281 }
2282 Err(e) => {
2283 all_statuses[idx] = Some(FuturesSendStatus {
2284 order_id: None,
2285 status: format!("validation_error: {e}"),
2286 order_events: None,
2287 cli_ord_id: None,
2288 received_time: None,
2289 });
2290 }
2291 }
2292 }
2293
2294 if valid_items.is_empty() {
2295 return Ok(all_statuses.into_iter().flatten().collect());
2296 }
2297
2298 let mut batch_statuses: Vec<FuturesSendStatus> = Vec::with_capacity(valid_items.len());
2299
2300 for chunk in valid_items.chunks(BATCH_ORDER_LIMIT) {
2301 match self.inner.submit_orders_batch(chunk.to_vec()).await {
2302 Ok(response) => {
2303 if response.result == KrakenApiResult::Success {
2304 batch_statuses.extend(response.batch_status);
2305 } else {
2306 let error_msg = response
2307 .batch_status
2308 .first()
2309 .map_or("Unknown error", |s| s.status.as_str());
2310
2311 for _ in 0..chunk.len() {
2312 batch_statuses.push(FuturesSendStatus {
2313 order_id: None,
2314 status: format!("api_error: {error_msg}"),
2315 order_events: None,
2316 cli_ord_id: None,
2317 received_time: None,
2318 });
2319 }
2320 }
2321 }
2322 Err(e) => {
2323 let remaining = valid_items.len() - batch_statuses.len();
2325 for _ in 0..remaining {
2326 batch_statuses.push(FuturesSendStatus {
2327 order_id: None,
2328 status: format!("batch_error: {e}"),
2329 order_events: None,
2330 cli_ord_id: None,
2331 received_time: None,
2332 });
2333 }
2334 break;
2335 }
2336 }
2337 }
2338
2339 for (batch_idx, &original_idx) in valid_indices.iter().enumerate() {
2341 if let Some(status) = batch_statuses.get(batch_idx) {
2342 all_statuses[original_idx] = Some(status.clone());
2343 }
2344 }
2345
2346 Ok(all_statuses.into_iter().flatten().collect())
2347 }
2348
2349 #[expect(clippy::type_complexity)]
2351 pub async fn edit_orders_batch(
2352 &self,
2353 orders: Vec<(
2354 InstrumentId,
2355 Option<ClientOrderId>,
2356 Option<VenueOrderId>,
2357 Option<Quantity>,
2358 Option<Price>,
2359 Option<Price>,
2360 )>,
2361 ) -> anyhow::Result<Vec<String>> {
2362 let count = orders.len();
2363 if count == 0 {
2364 return Ok(Vec::new());
2365 }
2366
2367 let mut all_statuses: Vec<Option<String>> = vec![None; count];
2368 let mut valid_items = Vec::with_capacity(count);
2369 let mut valid_indices = Vec::with_capacity(count);
2370
2371 for (
2372 idx,
2373 (instrument_id, client_order_id, venue_order_id, quantity, price, trigger_price),
2374 ) in orders.into_iter().enumerate()
2375 {
2376 match self.build_edit_order_params(
2377 instrument_id,
2378 client_order_id,
2379 venue_order_id,
2380 quantity,
2381 price,
2382 trigger_price,
2383 ) {
2384 Ok(params) => {
2385 valid_items.push(KrakenFuturesBatchEditItem::from_params(
2386 params,
2387 idx.to_string(),
2388 ));
2389 valid_indices.push(idx);
2390 }
2391 Err(e) => {
2392 all_statuses[idx] = Some(format!("validation_error: {e}"));
2393 }
2394 }
2395 }
2396
2397 if valid_items.is_empty() {
2398 return Ok(all_statuses.into_iter().flatten().collect());
2399 }
2400
2401 let mut batch_statuses: Vec<String> = Vec::with_capacity(valid_items.len());
2402
2403 for chunk in valid_items.chunks(BATCH_ORDER_LIMIT) {
2404 match self.inner.edit_orders_batch(chunk.to_vec()).await {
2405 Ok(response) => {
2406 if response.result == KrakenApiResult::Success {
2407 batch_statuses.extend(response.batch_status.into_iter().map(|s| s.status));
2408 } else {
2409 let error_msg = response
2410 .batch_status
2411 .first()
2412 .map_or("Unknown error", |s| s.status.as_str());
2413
2414 for _ in 0..chunk.len() {
2415 batch_statuses.push(format!("api_error: {error_msg}"));
2416 }
2417 }
2418 }
2419 Err(e) => {
2420 let remaining = valid_items.len() - batch_statuses.len();
2421 for _ in 0..remaining {
2422 batch_statuses.push(format!("batch_error: {e}"));
2423 }
2424 break;
2425 }
2426 }
2427 }
2428
2429 for (batch_idx, &original_idx) in valid_indices.iter().enumerate() {
2430 if let Some(status) = batch_statuses.get(batch_idx) {
2431 all_statuses[original_idx] = Some(status.clone());
2432 }
2433 }
2434
2435 Ok(all_statuses.into_iter().flatten().collect())
2436 }
2437
2438 fn build_edit_order_params(
2439 &self,
2440 instrument_id: InstrumentId,
2441 client_order_id: Option<ClientOrderId>,
2442 venue_order_id: Option<VenueOrderId>,
2443 quantity: Option<Quantity>,
2444 price: Option<Price>,
2445 trigger_price: Option<Price>,
2446 ) -> anyhow::Result<KrakenFuturesEditOrderParams> {
2447 let _ = self
2448 .get_cached_instrument(&instrument_id.symbol.inner())
2449 .ok_or_else(|| InstrumentLookupError::not_found(instrument_id))?;
2450
2451 let order_id = venue_order_id.as_ref().map(|id| id.to_string());
2452 let cli_ord_id = client_order_id.as_ref().map(truncate_cl_ord_id);
2453
2454 if order_id.is_none() && cli_ord_id.is_none() {
2455 anyhow::bail!("Either client_order_id or venue_order_id must be provided");
2456 }
2457
2458 let mut builder = KrakenFuturesEditOrderParamsBuilder::default();
2459
2460 if let Some(ref id) = order_id {
2461 builder.order_id(id.clone());
2462 }
2463
2464 if let Some(ref id) = cli_ord_id {
2465 builder.cli_ord_id(id.clone());
2466 }
2467
2468 if let Some(qty) = quantity {
2469 builder.size(qty.to_string());
2470 }
2471
2472 if let Some(p) = price {
2473 builder.limit_price(p.to_string());
2474 }
2475
2476 if let Some(tp) = trigger_price {
2477 builder.stop_price(tp.to_string());
2478 }
2479
2480 builder
2481 .build()
2482 .map_err(|e| anyhow::anyhow!("Failed to build edit order params: {e}"))
2483 }
2484}
2485
2486fn map_futures_trigger_signal(
2487 trigger_type: Option<TriggerType>,
2488) -> anyhow::Result<Option<KrakenTriggerSignal>> {
2489 match trigger_type {
2490 None => Ok(None),
2491 Some(TriggerType::Default | TriggerType::LastPrice) => Ok(Some(KrakenTriggerSignal::Last)),
2492 Some(TriggerType::MarkPrice) => Ok(Some(KrakenTriggerSignal::Mark)),
2493 Some(TriggerType::IndexPrice) => Ok(Some(KrakenTriggerSignal::Index)),
2494 Some(other) => anyhow::bail!(
2495 "Unsupported trigger type for Kraken Futures: {other:?} (only LastPrice, MarkPrice, and IndexPrice supported)"
2496 ),
2497 }
2498}
2499
2500fn parse_multi_collateral_balances(account: &FuturesAccount, balances: &mut Vec<AccountBalance>) {
2501 for (currency_code, currency_info) in &account.currencies {
2502 if currency_info.quantity == 0.0 {
2503 continue;
2504 }
2505
2506 let currency = Currency::new(
2507 currency_code.as_str(),
2508 8,
2509 0,
2510 currency_code.as_str(),
2511 CurrencyType::Crypto,
2512 );
2513
2514 let total_amount = currency_info.quantity;
2515 let available_amount = currency_info.available.unwrap_or(total_amount);
2516 let locked_amount = total_amount - available_amount;
2517
2518 push_balance_from_f64(
2519 balances,
2520 total_amount,
2521 locked_amount,
2522 currency,
2523 currency_code,
2524 );
2525 }
2526
2527 if let Some(portfolio_value) = account.portfolio_value
2530 && portfolio_value > 0.0
2531 {
2532 let usd_currency = Currency::USD();
2533 let available_usd = account.available_margin.unwrap_or(portfolio_value);
2534 let locked_usd = portfolio_value - available_usd;
2535
2536 push_balance_from_f64(balances, portfolio_value, locked_usd, usd_currency, "USD");
2537 }
2538}
2539
2540fn push_balance_from_f64(
2544 balances: &mut Vec<AccountBalance>,
2545 total: f64,
2546 locked: f64,
2547 currency: Currency,
2548 ccy_label: &str,
2549) {
2550 let Some(total_dec) = Decimal::from_f64(total) else {
2551 log::warn!("Skipping {ccy_label} balance: non-finite total {total}");
2552 return;
2553 };
2554 let Some(locked_dec) = Decimal::from_f64(locked) else {
2555 log::warn!("Skipping {ccy_label} balance: non-finite locked {locked}");
2556 return;
2557 };
2558
2559 match AccountBalance::from_total_and_locked(total_dec, locked_dec, currency) {
2560 Ok(balance) => balances.push(balance),
2561 Err(e) => log::warn!("Skipping {ccy_label} balance: {e}"),
2562 }
2563}
2564
2565fn parse_multi_collateral_margins(account: &FuturesAccount, margins: &mut Vec<MarginBalance>) {
2566 if let Some(initial_margin) = account.initial_margin
2567 && initial_margin > 0.0
2568 {
2569 let usd_currency = Currency::USD();
2570 let maintenance = account
2571 .margin_requirements
2572 .as_ref()
2573 .and_then(|mr| mr.mm)
2574 .unwrap_or(0.0);
2575 margins.push(MarginBalance::new(
2578 Money::new(initial_margin, usd_currency),
2579 Money::new(maintenance, usd_currency),
2580 None,
2581 ));
2582 }
2583}
2584
2585fn parse_margin_account_balances(account: &FuturesAccount, balances: &mut Vec<AccountBalance>) {
2586 for (currency_code, &amount) in &account.balances {
2587 if amount == 0.0 {
2588 continue;
2589 }
2590
2591 let currency = Currency::new(
2592 currency_code.as_str(),
2593 8,
2594 0,
2595 currency_code.as_str(),
2596 CurrencyType::Crypto,
2597 );
2598
2599 let available = account
2600 .auxiliary
2601 .as_ref()
2602 .and_then(|aux| aux.af)
2603 .unwrap_or(amount);
2604 let locked = amount - available;
2605
2606 push_balance_from_f64(balances, amount, locked, currency, currency_code);
2607 }
2608}
2609
2610fn parse_margin_account_margins(account: &FuturesAccount, margins: &mut Vec<MarginBalance>) {
2611 if let Some(ref mr) = account.margin_requirements {
2612 let im = mr.im.unwrap_or(0.0);
2613 let mm = mr.mm.unwrap_or(0.0);
2614 if im > 0.0 || mm > 0.0 {
2615 let usd_currency = Currency::USD();
2616 margins.push(MarginBalance::new(
2617 Money::new(im, usd_currency),
2618 Money::new(mm, usd_currency),
2619 None,
2620 ));
2621 }
2622 }
2623}
2624
2625fn parse_cash_account_balances(account: &FuturesAccount, balances: &mut Vec<AccountBalance>) {
2626 for (currency_code, &amount) in &account.balances {
2627 if amount == 0.0 {
2628 continue;
2629 }
2630
2631 let currency = Currency::new(
2632 currency_code.as_str(),
2633 8,
2634 0,
2635 currency_code.as_str(),
2636 CurrencyType::Crypto,
2637 );
2638
2639 push_balance_from_f64(balances, amount, 0.0, currency, currency_code);
2640 }
2641}
2642
2643#[cfg(test)]
2644mod tests {
2645 use ahash::AHashMap;
2646 use nautilus_model::instruments::CryptoPerpetual;
2647 use rstest::rstest;
2648
2649 use super::*;
2650
2651 #[rstest]
2652 fn test_raw_client_creation() {
2653 let client = KrakenFuturesRawHttpClient::default();
2654 assert!(client.credential.is_none());
2655 assert!(client.base_url().contains("futures"));
2656 }
2657
2658 #[rstest]
2659 fn test_raw_client_with_credentials() {
2660 let client = KrakenFuturesRawHttpClient::with_credentials(
2661 "test_key".to_string(),
2662 "test_secret".to_string(),
2663 KrakenEnvironment::Live,
2664 None,
2665 60,
2666 None,
2667 None,
2668 None,
2669 None,
2670 KRAKEN_FUTURES_DEFAULT_RATE_LIMIT_PER_SECOND,
2671 )
2672 .unwrap();
2673 assert!(client.credential.is_some());
2674 }
2675
2676 #[rstest]
2677 fn test_client_creation() {
2678 let client = KrakenFuturesHttpClient::default();
2679 assert!(client.instruments_cache.is_empty());
2680 }
2681
2682 #[rstest]
2683 fn test_client_with_credentials() {
2684 let client = KrakenFuturesHttpClient::with_credentials(
2685 "test_key".to_string(),
2686 "test_secret".to_string(),
2687 KrakenEnvironment::Live,
2688 None,
2689 60,
2690 None,
2691 None,
2692 None,
2693 None,
2694 KRAKEN_FUTURES_DEFAULT_RATE_LIMIT_PER_SECOND,
2695 )
2696 .unwrap();
2697 assert!(client.instruments_cache.is_empty());
2698 }
2699
2700 #[rstest]
2701 fn test_parse_multi_collateral_margins() {
2702 let account = FuturesAccount {
2703 account_type: KrakenFuturesAccountType::MultiCollateralMarginAccount,
2704 balances: AHashMap::new(),
2705 currencies: AHashMap::new(),
2706 auxiliary: None,
2707 margin_requirements: Some(FuturesMarginRequirements {
2708 im: Some(500.0),
2709 mm: Some(250.0),
2710 lt: None,
2711 tt: None,
2712 }),
2713 portfolio_value: Some(10000.0),
2714 available_margin: Some(9500.0),
2715 initial_margin: Some(500.0),
2716 pnl: None,
2717 };
2718
2719 let mut margins = Vec::new();
2720 parse_multi_collateral_margins(&account, &mut margins);
2721
2722 assert_eq!(margins.len(), 1);
2723 let margin = &margins[0];
2724 assert!(margin.instrument_id.is_none());
2725 assert_eq!(margin.currency.code.as_str(), "USD");
2726 assert_eq!(margin.initial.as_f64(), 500.0);
2727 assert_eq!(margin.maintenance.as_f64(), 250.0);
2728 }
2729
2730 #[rstest]
2731 fn test_parse_multi_collateral_margins_zero_skipped() {
2732 let account = FuturesAccount {
2733 account_type: KrakenFuturesAccountType::MultiCollateralMarginAccount,
2734 balances: AHashMap::new(),
2735 currencies: AHashMap::new(),
2736 auxiliary: None,
2737 margin_requirements: None,
2738 portfolio_value: None,
2739 available_margin: None,
2740 initial_margin: Some(0.0),
2741 pnl: None,
2742 };
2743
2744 let mut margins = Vec::new();
2745 parse_multi_collateral_margins(&account, &mut margins);
2746
2747 assert_eq!(margins.len(), 0);
2748 }
2749
2750 #[rstest]
2751 fn test_parse_margin_account_margins() {
2752 let account = FuturesAccount {
2753 account_type: KrakenFuturesAccountType::MarginAccount,
2754 balances: AHashMap::new(),
2755 currencies: AHashMap::new(),
2756 auxiliary: None,
2757 margin_requirements: Some(FuturesMarginRequirements {
2758 im: Some(100.0),
2759 mm: Some(50.0),
2760 lt: None,
2761 tt: None,
2762 }),
2763 portfolio_value: None,
2764 available_margin: None,
2765 initial_margin: None,
2766 pnl: None,
2767 };
2768
2769 let mut margins = Vec::new();
2770 parse_margin_account_margins(&account, &mut margins);
2771
2772 assert_eq!(margins.len(), 1);
2773 let margin = &margins[0];
2774 assert_eq!(margin.initial.as_f64(), 100.0);
2775 assert_eq!(margin.maintenance.as_f64(), 50.0);
2776 }
2777
2778 #[rstest]
2779 fn test_parse_margin_account_margins_no_requirements() {
2780 let account = FuturesAccount {
2781 account_type: KrakenFuturesAccountType::MarginAccount,
2782 balances: AHashMap::new(),
2783 currencies: AHashMap::new(),
2784 auxiliary: None,
2785 margin_requirements: None,
2786 portfolio_value: None,
2787 available_margin: None,
2788 initial_margin: None,
2789 pnl: None,
2790 };
2791
2792 let mut margins = Vec::new();
2793 parse_margin_account_margins(&account, &mut margins);
2794
2795 assert_eq!(margins.len(), 0);
2796 }
2797
2798 #[rstest]
2799 fn test_parse_multi_collateral_balances() {
2800 let mut currencies = AHashMap::new();
2801 currencies.insert(
2802 "BTC".to_string(),
2803 FuturesFlexCurrency {
2804 quantity: 1.5,
2805 value: None,
2806 collateral: None,
2807 available: Some(1.2),
2808 },
2809 );
2810
2811 let account = FuturesAccount {
2812 account_type: KrakenFuturesAccountType::MultiCollateralMarginAccount,
2813 balances: AHashMap::new(),
2814 currencies,
2815 auxiliary: None,
2816 margin_requirements: None,
2817 portfolio_value: Some(50000.0),
2818 available_margin: Some(45000.0),
2819 initial_margin: None,
2820 pnl: None,
2821 };
2822
2823 let mut balances = Vec::new();
2824 parse_multi_collateral_balances(&account, &mut balances);
2825
2826 assert_eq!(balances.len(), 2);
2828 }
2829
2830 #[rstest]
2831 fn test_parse_margin_account_balances_free_is_derived_from_total_minus_locked() {
2832 let mut bals = AHashMap::new();
2837 let af_f = 35.0_f64 * 1e-9;
2845 let amount_f = 10.0_f64 + af_f;
2846 bals.insert("XBT".to_string(), amount_f);
2847
2848 let account = FuturesAccount {
2849 account_type: KrakenFuturesAccountType::MarginAccount,
2850 balances: bals,
2851 currencies: AHashMap::new(),
2852 auxiliary: Some(FuturesAuxiliary {
2853 usd: None,
2854 pv: None,
2855 pnl: None,
2856 af: Some(af_f),
2857 funding: None,
2858 }),
2859 margin_requirements: None,
2860 portfolio_value: None,
2861 available_margin: None,
2862 initial_margin: None,
2863 pnl: None,
2864 };
2865
2866 let mut balances = Vec::new();
2867 parse_margin_account_balances(&account, &mut balances);
2868
2869 assert_eq!(balances.len(), 1);
2870 let balance = &balances[0];
2871 assert_eq!(balance.total, balance.locked + balance.free);
2874 assert_eq!(balance.free, balance.total - balance.locked);
2876 }
2877
2878 #[rstest]
2879 #[case::nan_total(f64::NAN, 0.0)]
2880 #[case::infinity_total(f64::INFINITY, 0.0)]
2881 #[case::neg_infinity_total(f64::NEG_INFINITY, 0.0)]
2882 #[case::nan_locked(1.0, f64::NAN)]
2883 #[case::infinity_locked(1.0, f64::INFINITY)]
2884 fn test_push_balance_from_f64_skips_non_finite(#[case] total: f64, #[case] locked: f64) {
2885 let currency = Currency::new("BTC", 8, 0, "BTC", CurrencyType::Crypto);
2886 let mut balances = Vec::new();
2887
2888 push_balance_from_f64(&mut balances, total, locked, currency, "BTC");
2889
2890 assert!(balances.is_empty());
2891 }
2892
2893 #[rstest]
2894 fn test_parse_cash_account_balances() {
2895 let mut bals = AHashMap::new();
2896 bals.insert("ETH".to_string(), 10.0);
2897 bals.insert("BTC".to_string(), 0.0); let account = FuturesAccount {
2900 account_type: KrakenFuturesAccountType::CashAccount,
2901 balances: bals,
2902 currencies: AHashMap::new(),
2903 auxiliary: None,
2904 margin_requirements: None,
2905 portfolio_value: None,
2906 available_margin: None,
2907 initial_margin: None,
2908 pnl: None,
2909 };
2910
2911 let mut balances = Vec::new();
2912 parse_cash_account_balances(&account, &mut balances);
2913
2914 assert_eq!(balances.len(), 1);
2915 let balance = &balances[0];
2916 assert_eq!(balance.total.as_f64(), 10.0);
2917 assert_eq!(balance.locked.as_f64(), 0.0);
2918 }
2919
2920 #[rstest]
2921 #[case(None, None)]
2922 #[case(Some(TriggerType::Default), Some(KrakenTriggerSignal::Last))]
2923 #[case(Some(TriggerType::LastPrice), Some(KrakenTriggerSignal::Last))]
2924 #[case(Some(TriggerType::MarkPrice), Some(KrakenTriggerSignal::Mark))]
2925 #[case(Some(TriggerType::IndexPrice), Some(KrakenTriggerSignal::Index))]
2926 fn test_build_send_order_params_maps_supported_trigger_signals(
2927 #[case] trigger_type: Option<TriggerType>,
2928 #[case] expected_signal: Option<KrakenTriggerSignal>,
2929 ) {
2930 let client = KrakenFuturesHttpClient::default();
2931 let instrument_id = cache_test_futures_instrument(&client);
2932
2933 let params = client
2934 .build_send_order_params(
2935 instrument_id,
2936 ClientOrderId::new("futures-trigger"),
2937 OrderSide::Buy,
2938 OrderType::StopMarket,
2939 Quantity::from("1"),
2940 TimeInForce::Gtc,
2941 None,
2942 Some(Price::from("45000")),
2943 trigger_type,
2944 false,
2945 false,
2946 )
2947 .unwrap();
2948
2949 assert_eq!(params.trigger_signal, expected_signal);
2950 }
2951
2952 #[rstest]
2953 fn test_build_send_order_params_rejects_unsupported_trigger_signal() {
2954 let client = KrakenFuturesHttpClient::default();
2955 let instrument_id = cache_test_futures_instrument(&client);
2956
2957 let error = client
2958 .build_send_order_params(
2959 instrument_id,
2960 ClientOrderId::new("futures-trigger-invalid"),
2961 OrderSide::Buy,
2962 OrderType::StopMarket,
2963 Quantity::from("1"),
2964 TimeInForce::Gtc,
2965 None,
2966 Some(Price::from("45000")),
2967 Some(TriggerType::BidAsk),
2968 false,
2969 false,
2970 )
2971 .unwrap_err();
2972
2973 assert!(
2974 error
2975 .to_string()
2976 .contains("Unsupported trigger type for Kraken Futures")
2977 );
2978 }
2979
2980 fn cache_test_futures_instrument(client: &KrakenFuturesHttpClient) -> InstrumentId {
2981 let instrument_id = InstrumentId::from("PF_XBTUSD.KRAKEN");
2982
2983 client.cache_instrument(InstrumentAny::CryptoPerpetual(CryptoPerpetual::new(
2984 instrument_id,
2985 Symbol::new("PF_XBTUSD"),
2986 Currency::BTC(),
2987 Currency::USD(),
2988 Currency::USD(),
2989 false,
2990 0,
2991 4,
2992 Price::from("1"),
2993 Quantity::from("0.0001"),
2994 None,
2995 None,
2996 None,
2997 None,
2998 None,
2999 None,
3000 None,
3001 None,
3002 None,
3003 None,
3004 None,
3005 None,
3006 None,
3007 None,
3008 0.into(),
3009 0.into(),
3010 )));
3011
3012 instrument_id
3013 }
3014}