Skip to main content

kraken_api_client/futures/rest/
client.rs

1//! Kraken Futures REST API client implementation.
2
3use std::sync::Arc;
4
5use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue, USER_AGENT};
6use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
7use reqwest_retry::{RetryTransientMiddleware, policies::ExponentialBackoff};
8use reqwest_tracing::TracingMiddleware;
9
10use crate::auth::{CredentialsProvider, IncreasingNonce, NonceProvider};
11use crate::error::KrakenError;
12use crate::futures::auth::sign_futures_request;
13use crate::futures::rest::endpoints::{FUTURES_BASE_URL, private, public};
14use crate::futures::rest::types::*;
15use crate::futures::types::*;
16
17/// The Kraken Futures REST API client.
18///
19/// This client provides access to all Kraken Futures trading REST endpoints.
20/// It handles authentication, rate limiting, and automatic retries.
21///
22/// # Example
23///
24/// ```rust,no_run
25/// use kraken_api_client::futures::rest::FuturesRestClient;
26///
27/// #[tokio::main]
28/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
29///     // Create a client for public endpoints only
30///     let client = FuturesRestClient::new();
31///
32///     // Get all tickers
33///     let tickers = client.get_tickers().await?;
34///     for ticker in &tickers {
35///         println!("{}: {}", ticker.symbol, ticker.last);
36///     }
37///
38///     Ok(())
39/// }
40/// ```
41///
42/// For private endpoints, provide credentials:
43///
44/// ```rust,no_run
45/// use kraken_api_client::futures::rest::FuturesRestClient;
46/// use kraken_api_client::auth::StaticCredentials;
47/// use std::sync::Arc;
48///
49/// #[tokio::main]
50/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
51///     let credentials = Arc::new(StaticCredentials::new("api_key", "api_secret"));
52///     let client = FuturesRestClient::builder()
53///         .credentials(credentials)
54///         .build();
55///
56///     let accounts = client.get_accounts().await?;
57///     println!("Accounts: {:?}", accounts);
58///
59///     Ok(())
60/// }
61/// ```
62#[derive(Clone)]
63pub struct FuturesRestClient {
64    http_client: ClientWithMiddleware,
65    base_url: String,
66    credentials: Option<Arc<dyn CredentialsProvider>>,
67    nonce_provider: Arc<dyn NonceProvider>,
68}
69
70impl FuturesRestClient {
71    /// Create a new client with default settings.
72    ///
73    /// This client can only access public endpoints.
74    /// Use [`FuturesRestClient::builder()`] to configure credentials for private endpoints.
75    pub fn new() -> Self {
76        Self::builder().build()
77    }
78
79    /// Create a new client builder.
80    pub fn builder() -> FuturesRestClientBuilder {
81        FuturesRestClientBuilder::new()
82    }
83
84    // HTTP request methods.
85
86    /// Make a public GET request.
87    pub(crate) async fn public_get<T>(&self, endpoint: &str) -> Result<T, KrakenError>
88    where
89        T: serde::de::DeserializeOwned,
90    {
91        let url = format!("{}{}", self.base_url, endpoint);
92        let response = self.http_client.get(&url).send().await?;
93        self.parse_futures_response(response).await
94    }
95
96    /// Make a public GET request with query parameters.
97    pub(crate) async fn public_get_with_params<T, Q>(
98        &self,
99        endpoint: &str,
100        params: &Q,
101    ) -> Result<T, KrakenError>
102    where
103        T: serde::de::DeserializeOwned,
104        Q: serde::Serialize + ?Sized,
105    {
106        let query_string = serde_urlencoded::to_string(params)
107            .map_err(|e| KrakenError::InvalidResponse(e.to_string()))?;
108        let url = if query_string.is_empty() {
109            format!("{}{}", self.base_url, endpoint)
110        } else {
111            format!("{}{}?{}", self.base_url, endpoint, query_string)
112        };
113        let response = self.http_client.get(&url).send().await?;
114        self.parse_futures_response(response).await
115    }
116
117    /// Make an authenticated GET request.
118    pub(crate) async fn private_get<T>(&self, endpoint: &str) -> Result<T, KrakenError>
119    where
120        T: serde::de::DeserializeOwned,
121    {
122        let credentials = self
123            .credentials
124            .as_ref()
125            .ok_or(KrakenError::MissingCredentials)?;
126
127        let nonce = self.nonce_provider.next_nonce();
128        let creds = credentials.get_credentials();
129
130        // Sign the request (empty post_data for GET).
131        let signature = sign_futures_request(creds, endpoint, nonce, "")?;
132
133        let url = format!("{}{}", self.base_url, endpoint);
134        let response = self
135            .http_client
136            .get(&url)
137            .header("APIKey", &creds.api_key)
138            .header("Authent", signature)
139            .header("Nonce", nonce.to_string())
140            .send()
141            .await?;
142
143        self.parse_futures_response(response).await
144    }
145
146    /// Make an authenticated POST request.
147    pub(crate) async fn private_post<T, P>(
148        &self,
149        endpoint: &str,
150        params: &P,
151    ) -> Result<T, KrakenError>
152    where
153        T: serde::de::DeserializeOwned,
154        P: serde::Serialize,
155    {
156        let credentials = self
157            .credentials
158            .as_ref()
159            .ok_or(KrakenError::MissingCredentials)?;
160
161        let nonce = self.nonce_provider.next_nonce();
162        let creds = credentials.get_credentials();
163
164        // Build the POST body.
165        let form_data = serde_urlencoded::to_string(params)
166            .map_err(|e| KrakenError::InvalidResponse(e.to_string()))?;
167
168        // Sign the request using the Futures algorithm.
169        let signature = sign_futures_request(creds, endpoint, nonce, &form_data)?;
170
171        let url = format!("{}{}", self.base_url, endpoint);
172        let response = self
173            .http_client
174            .post(&url)
175            .header("APIKey", &creds.api_key)
176            .header("Authent", signature)
177            .header("Nonce", nonce.to_string())
178            .header(CONTENT_TYPE, "application/x-www-form-urlencoded")
179            .body(form_data)
180            .send()
181            .await?;
182
183        self.parse_futures_response(response).await
184    }
185
186    /// Parse a response from the Kraken Futures API.
187    ///
188    /// Futures API has a different response format than Spot:
189    /// - Success: `{ "result": "success", ... }`
190    /// - Error: `{ "result": "error", "error": "...", "serverTime": "..." }`
191    async fn parse_futures_response<T>(&self, response: reqwest::Response) -> Result<T, KrakenError>
192    where
193        T: serde::de::DeserializeOwned,
194    {
195        let status = response.status();
196        let body = response.text().await?;
197
198        // First check if it is an error response.
199        if let Ok(error_response) = serde_json::from_str::<FuturesErrorResponse>(&body) {
200            if error_response.result == "error" {
201                return Err(KrakenError::Api(crate::error::ApiError::new(
202                    "EFutures",
203                    error_response
204                        .error
205                        .unwrap_or_else(|| "Unknown error".to_string()),
206                )));
207            }
208        }
209
210        // Try to parse as success response.
211        serde_json::from_str::<T>(&body).map_err(|e| {
212            if !status.is_success() {
213                KrakenError::InvalidResponse(format!("HTTP {}: {}", status, body))
214            } else {
215                KrakenError::InvalidResponse(format!(
216                    "Failed to parse response: {}. Body: {}",
217                    e, body
218                ))
219            }
220        })
221    }
222
223    // Public endpoints.
224
225    /// Get all tickers.
226    ///
227    /// Returns ticker data for all available futures contracts.
228    pub async fn get_tickers(&self) -> Result<Vec<FuturesTicker>, KrakenError> {
229        let response: TickersResponse = self.public_get(public::TICKERS).await?;
230        Ok(response.tickers)
231    }
232
233    /// Get ticker for a specific symbol.
234    ///
235    /// Returns ticker data for the specified symbol, or None if not found.
236    pub async fn get_ticker(&self, symbol: &str) -> Result<Option<FuturesTicker>, KrakenError> {
237        let tickers = self.get_tickers().await?;
238        Ok(tickers.into_iter().find(|t| t.symbol == symbol))
239    }
240
241    /// Get order book for a symbol.
242    ///
243    /// # Arguments
244    ///
245    /// * `symbol` - The futures symbol (e.g., "PI_XBTUSD")
246    pub async fn get_orderbook(&self, symbol: &str) -> Result<FuturesOrderBook, KrakenError> {
247        #[derive(serde::Serialize)]
248        struct Params<'a> {
249            symbol: &'a str,
250        }
251        let response: OrderBookResponse = self
252            .public_get_with_params(public::ORDERBOOK, &Params { symbol })
253            .await?;
254        Ok(response.order_book)
255    }
256
257    /// Get recent trade history for a symbol.
258    ///
259    /// # Arguments
260    ///
261    /// * `symbol` - The futures symbol (e.g., "PI_XBTUSD")
262    /// * `last_time` - Optional timestamp to get trades before
263    pub async fn get_trade_history(
264        &self,
265        symbol: &str,
266        last_time: Option<&str>,
267    ) -> Result<Vec<FuturesTrade>, KrakenError> {
268        #[derive(serde::Serialize)]
269        struct Params<'a> {
270            symbol: &'a str,
271            #[serde(skip_serializing_if = "Option::is_none")]
272            #[serde(rename = "lastTime")]
273            last_time: Option<&'a str>,
274        }
275        let response: TradeHistoryResponse = self
276            .public_get_with_params(public::HISTORY, &Params { symbol, last_time })
277            .await?;
278        Ok(response.history)
279    }
280
281    /// Get available instruments.
282    ///
283    /// Returns information about all tradeable futures contracts.
284    pub async fn get_instruments(&self) -> Result<Vec<FuturesInstrument>, KrakenError> {
285        let response: InstrumentsResponse = self.public_get(public::INSTRUMENTS).await?;
286        Ok(response.instruments)
287    }
288
289    // Private endpoints: account.
290
291    /// Get account information.
292    ///
293    /// Returns balances, margin info, and PnL for all accounts.
294    pub async fn get_accounts(&self) -> Result<AccountsResponse, KrakenError> {
295        self.private_get(private::ACCOUNTS).await
296    }
297
298    /// Get open positions.
299    ///
300    /// Returns all open futures positions.
301    pub async fn get_open_positions(&self) -> Result<Vec<FuturesPosition>, KrakenError> {
302        let response: OpenPositionsResponse = self.private_get(private::OPEN_POSITIONS).await?;
303        Ok(response.open_positions)
304    }
305
306    /// Get open orders.
307    ///
308    /// Returns all open (unfilled) orders.
309    pub async fn get_open_orders(&self) -> Result<Vec<FuturesOrder>, KrakenError> {
310        let response: OpenOrdersResponse = self.private_get(private::OPEN_ORDERS).await?;
311        Ok(response.open_orders)
312    }
313
314    /// Get fills (trade history).
315    ///
316    /// Returns fills for all futures contracts or a specific symbol.
317    ///
318    /// # Arguments
319    ///
320    /// * `request` - Optional request parameters
321    pub async fn get_fills(
322        &self,
323        request: Option<&FillsRequest>,
324    ) -> Result<Vec<FuturesFill>, KrakenError> {
325        let response: FillsResponse = match request {
326            Some(req) => self.public_get_with_params(private::FILLS, req).await?,
327            None => self.private_get(private::FILLS).await?,
328        };
329        Ok(response.fills)
330    }
331
332    // Private endpoints: trading.
333
334    /// Send a new order.
335    ///
336    /// # Arguments
337    ///
338    /// * `request` - Order parameters
339    pub async fn send_order(
340        &self,
341        request: &SendOrderRequest,
342    ) -> Result<SendOrderResponse, KrakenError> {
343        self.private_post(private::SEND_ORDER, request).await
344    }
345
346    /// Edit an existing order.
347    ///
348    /// # Arguments
349    ///
350    /// * `request` - Edit parameters
351    pub async fn edit_order(
352        &self,
353        request: &EditOrderRequest,
354    ) -> Result<EditOrderResponse, KrakenError> {
355        self.private_post(private::EDIT_ORDER, request).await
356    }
357
358    /// Cancel an order.
359    ///
360    /// # Arguments
361    ///
362    /// * `order_id` - The order ID to cancel
363    pub async fn cancel_order(&self, order_id: &str) -> Result<CancelOrderResponse, KrakenError> {
364        #[derive(serde::Serialize)]
365        struct Params<'a> {
366            order_id: &'a str,
367        }
368        self.private_post(private::CANCEL_ORDER, &Params { order_id })
369            .await
370    }
371
372    /// Cancel an order by client order ID.
373    ///
374    /// # Arguments
375    ///
376    /// * `cli_ord_id` - The client order ID to cancel
377    pub async fn cancel_order_by_cli_ord_id(
378        &self,
379        cli_ord_id: &str,
380    ) -> Result<CancelOrderResponse, KrakenError> {
381        #[derive(serde::Serialize)]
382        struct Params<'a> {
383            #[serde(rename = "cliOrdId")]
384            cli_ord_id: &'a str,
385        }
386        self.private_post(private::CANCEL_ORDER, &Params { cli_ord_id })
387            .await
388    }
389
390    /// Cancel all orders.
391    ///
392    /// Cancels all open orders for the account.
393    pub async fn cancel_all_orders(&self) -> Result<CancelAllOrdersResponse, KrakenError> {
394        #[derive(serde::Serialize)]
395        struct Empty {}
396        self.private_post(private::CANCEL_ALL_ORDERS, &Empty {})
397            .await
398    }
399
400    /// Cancel all orders for a specific symbol.
401    ///
402    /// # Arguments
403    ///
404    /// * `symbol` - The futures symbol to cancel orders for
405    pub async fn cancel_all_orders_for_symbol(
406        &self,
407        symbol: &str,
408    ) -> Result<CancelAllOrdersResponse, KrakenError> {
409        #[derive(serde::Serialize)]
410        struct Params<'a> {
411            symbol: &'a str,
412        }
413        self.private_post(private::CANCEL_ALL_ORDERS, &Params { symbol })
414            .await
415    }
416
417    /// Set dead man's switch (cancel all orders after timeout).
418    ///
419    /// # Arguments
420    ///
421    /// * `timeout_seconds` - Timeout in seconds (0 to disable)
422    pub async fn cancel_all_orders_after(
423        &self,
424        timeout_seconds: u32,
425    ) -> Result<CancelAllOrdersAfterResponse, KrakenError> {
426        #[derive(serde::Serialize)]
427        struct Params {
428            timeout: u32,
429        }
430        self.private_post(
431            private::CANCEL_ALL_ORDERS_AFTER,
432            &Params {
433                timeout: timeout_seconds,
434            },
435        )
436        .await
437    }
438
439    /// Send batch orders.
440    ///
441    /// Allows placing, editing, and cancelling multiple orders in a single request.
442    ///
443    /// # Arguments
444    ///
445    /// * `request` - Batch order request
446    pub async fn batch_order(
447        &self,
448        request: &BatchOrderRequest,
449    ) -> Result<BatchOrderResponse, KrakenError> {
450        self.private_post(private::BATCH_ORDER, request).await
451    }
452}
453
454impl Default for FuturesRestClient {
455    fn default() -> Self {
456        Self::new()
457    }
458}
459
460impl std::fmt::Debug for FuturesRestClient {
461    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
462        f.debug_struct("FuturesRestClient")
463            .field("base_url", &self.base_url)
464            .field("has_credentials", &self.credentials.is_some())
465            .finish()
466    }
467}
468
469/// Builder for [`FuturesRestClient`].
470pub struct FuturesRestClientBuilder {
471    base_url: String,
472    credentials: Option<Arc<dyn CredentialsProvider>>,
473    nonce_provider: Option<Arc<dyn NonceProvider>>,
474    user_agent: Option<String>,
475    max_retries: u32,
476}
477
478impl FuturesRestClientBuilder {
479    /// Create a new builder with default settings.
480    pub fn new() -> Self {
481        Self {
482            base_url: FUTURES_BASE_URL.to_string(),
483            credentials: None,
484            nonce_provider: None,
485            user_agent: None,
486            max_retries: 3,
487        }
488    }
489
490    /// Set the base URL (useful for testing with a mock server).
491    pub fn base_url(mut self, url: impl Into<String>) -> Self {
492        self.base_url = url.into();
493        self
494    }
495
496    /// Use the demo/testnet environment.
497    pub fn use_demo(mut self) -> Self {
498        self.base_url = crate::futures::rest::endpoints::FUTURES_DEMO_URL.to_string();
499        self
500    }
501
502    /// Set the credentials provider for authenticated requests.
503    pub fn credentials(mut self, credentials: Arc<dyn CredentialsProvider>) -> Self {
504        self.credentials = Some(credentials);
505        self
506    }
507
508    /// Set a custom nonce provider.
509    pub fn nonce_provider(mut self, provider: Arc<dyn NonceProvider>) -> Self {
510        self.nonce_provider = Some(provider);
511        self
512    }
513
514    /// Set a custom user agent.
515    pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
516        self.user_agent = Some(user_agent.into());
517        self
518    }
519
520    /// Set the maximum number of retries for transient failures.
521    pub fn max_retries(mut self, retries: u32) -> Self {
522        self.max_retries = retries;
523        self
524    }
525
526    /// Build the client.
527    pub fn build(self) -> FuturesRestClient {
528        // Build default headers.
529        let mut headers = HeaderMap::new();
530        let user_agent = self
531            .user_agent
532            .unwrap_or_else(|| format!("kraken-api-client/{}", env!("CARGO_PKG_VERSION")));
533        let header_value = HeaderValue::from_str(&user_agent)
534            .unwrap_or_else(|_| HeaderValue::from_static("kraken-api-client"));
535        headers.insert(USER_AGENT, header_value);
536
537        // Build the HTTP client with middleware.
538        let reqwest_client = reqwest::Client::builder()
539            .default_headers(headers)
540            .build()
541            .unwrap_or_else(|_| reqwest::Client::new());
542
543        let retry_policy = ExponentialBackoff::builder().build_with_max_retries(self.max_retries);
544
545        let client = ClientBuilder::new(reqwest_client)
546            .with(TracingMiddleware::default())
547            .with(RetryTransientMiddleware::new_with_policy(retry_policy))
548            .build();
549
550        let nonce_provider = self
551            .nonce_provider
552            .unwrap_or_else(|| Arc::new(IncreasingNonce::new()));
553
554        FuturesRestClient {
555            http_client: client,
556            base_url: self.base_url,
557            credentials: self.credentials,
558            nonce_provider,
559        }
560    }
561}
562
563impl Default for FuturesRestClientBuilder {
564    fn default() -> Self {
565        Self::new()
566    }
567}
568
569/// Internal error response from Futures API.
570#[derive(Debug, serde::Deserialize)]
571struct FuturesErrorResponse {
572    result: String,
573    error: Option<String>,
574    #[serde(rename = "serverTime")]
575    #[allow(dead_code)]
576    server_time: Option<String>,
577}