kraken_cli_rs/
client.rs

1use base64::prelude::*;
2use base64::Engine;
3use clap::ValueEnum;
4use dotenv::dotenv;
5use hmac::{Hmac, Mac};
6use reqwest;
7use reqwest::blocking::Client;
8use reqwest::header::USER_AGENT;
9use serde_derive::{Deserialize, Serialize};
10use serde_json::Value;
11use serde_urlencoded;
12use sha2::Digest;
13use sha2::{Sha256, Sha512};
14use std::collections::HashMap;
15use std::time::{SystemTime, UNIX_EPOCH};
16
17
18
19#[derive(Debug, ValueEnum, Clone)]
20pub enum Symbols {
21    Etheur,
22    Adaeur,
23    Xlmeur,
24}
25impl std::fmt::Display for Symbols {
26    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
27        match self {
28            Symbols::Etheur => write!(f, "etheur"),
29            Symbols::Adaeur => write!(f, "adaeur"),
30            Symbols::Xlmeur => write!(f, "xlmeur"),
31        }
32    }
33}
34impl std::str::FromStr for Symbols {
35    type Err = ();
36    fn from_str(s: &str) -> Result<Self, Self::Err> {
37        match s {
38            "etheur" => Ok(Symbols::Etheur),
39            "adaeur" => Ok(Symbols::Adaeur),
40            "xlmeur" => Ok(Symbols::Xlmeur),
41            _ => Err(()),
42        }
43    }
44}
45impl Symbols {
46    pub fn to_alt_string(&self) -> String {
47        match self {
48            Symbols::Etheur => "XETHZEUR".to_string(),
49            Symbols::Adaeur => "ADAEUR".to_string(),
50            Symbols::Xlmeur => "XXLMZEUR".to_string(),
51        }
52    }
53}
54#[derive(Debug, ValueEnum, Clone)]
55pub enum OrderType {
56    Market,
57    Limit,
58    StopLoss,
59    TakeProfit,
60    StopLossLimit,
61    TakeProfitLimit,
62    BestLimit,
63}
64impl std::fmt::Display for OrderType {
65    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
66        match self {
67            OrderType::Market => write!(f, "market"),
68            OrderType::Limit => write!(f, "limit"),
69            OrderType::StopLoss => write!(f, "stop_loss"),
70            OrderType::TakeProfit => write!(f, "take_profit"),
71            OrderType::StopLossLimit => write!(f, "stop_loss_limit"),
72            OrderType::TakeProfitLimit => write!(f, "take_profit_limit"),
73            OrderType::BestLimit => write!(f, "best_limit"),
74        }
75    }
76}
77
78#[derive(Debug, ValueEnum, Clone)]
79pub enum Action {
80    Buy,
81    Sell,
82}
83impl std::fmt::Display for Action {
84    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
85        match self {
86            Action::Buy => write!(f, "buy"),
87            Action::Sell => write!(f, "sell"),
88        }
89    }
90}
91
92#[derive(Serialize, Deserialize, Debug)]
93pub struct KrakenResponse {
94    error: Vec<String>,
95    result: Option<HashMap<String, Value>>,
96}
97impl KrakenResponse {
98    //
99    // Create a new KrakenResponse from a reqwest::blocking::Response. Propagate the error if any
100    //
101    pub fn from_block_response(
102        response: reqwest::blocking::Response,
103    ) -> Result<KrakenResponse, Box<dyn std::error::Error>> {
104        let body = response.text()?;
105        let kraken_response: KrakenResponse = serde_json::from_str(&body).unwrap();
106        // TODO: Custom errors?
107        match kraken_response.error.len() {
108            0 => Ok(kraken_response),
109            _ => Err(Box::new(std::io::Error::new(
110                std::io::ErrorKind::Other,
111                kraken_response.error.join("\n"),
112            ))),
113        }
114    }
115}
116
117#[derive(Debug)]
118pub struct Kraken {
119    api_key_public: String,
120    hmac_sha_key: Vec<u8>,
121}
122impl Kraken {
123    pub fn new() -> Self {
124        dotenv().ok();
125        let api_key_public = std::env::var("API_Public_Key").expect("API_Public_Key not found in enviroment variables. Make sure to have them defined or create an .env file with \n API_Public_Key=... \n API_Private_Key=... variables.");
126        let api_key_private = std::env::var("API_Private_Key").expect("API_Private_Key not found in enviroment variables. Make sure to have them defined or create an .env file with \n API_Public_Key=... \n API_Private_Key=... variables.");
127        let hmac_sha_key = BASE64_STANDARD
128            .decode(&api_key_private)
129            .expect("Error decoding API_Private_Key");
130        Self {
131            api_key_public,
132            hmac_sha_key,
133        }
134    }
135    pub fn order_book(
136        &self,
137        symbol: &Symbols,
138    ) -> Result<KrakenResponse, Box<dyn std::error::Error>> {
139        // create url request
140
141        let url = format!(
142            "https://api.kraken.com/0/public/Depth?pair={}&count=10",
143            symbol,
144        );
145        let client = reqwest::blocking::Client::new();
146        let response = client.get(url).header(USER_AGENT, "Test").send().unwrap();
147        // decode response
148        KrakenResponse::from_block_response(response)
149    }
150    pub fn list_ticks(
151        &self,
152        symbol: Symbols,
153        interval: u32,
154    ) -> Result<KrakenResponse, Box<dyn std::error::Error>> {
155        // create url request
156
157        let url = format!(
158            "https://api.kraken.com/0/public/OHLC?pair={}&interval={}",
159            symbol, interval
160        );
161        let client = reqwest::blocking::Client::new();
162        let response = client.get(url).header(USER_AGENT, "Test").send().unwrap();
163        // decode response
164        KrakenResponse::from_block_response(response)
165    }
166    ///
167    /// Generate nonce
168    ///
169    pub fn get_nonce(&self) -> String {
170        SystemTime::now()
171            .duration_since(UNIX_EPOCH)
172            .unwrap()
173            .as_millis()
174            .to_string()
175    }
176
177    pub fn request_private(
178        &self,
179        api_path: &str,
180        params: &[(&str, &String)],
181        api_nonce: &str,
182    ) -> Result<KrakenResponse, Box<dyn std::error::Error>> {
183        let api_post = serde_urlencoded::to_string(params)?;
184        let api_signature = self.get_api_signature(api_path, &api_nonce, &api_post);
185
186        let client = Client::new();
187        let response: reqwest::blocking::Response = client
188            .post(format!("https://api.kraken.com{}", api_path))
189            .body(api_post)
190            .header("API-Key", &self.api_key_public)
191            .header("API-Sign", api_signature)
192            .header("User-Agent", "Kraken trading bot example")
193            .send()?;
194        KrakenResponse::from_block_response(response)
195    }
196
197    ///
198    /// Create the API signature
199    ///
200    pub fn get_api_signature(&self, api_path: &str, api_nonce: &str, api_post: &str) -> String {
201        // based on https://docs.kraken.com/rest/#tag/Trading/operation/addOrder
202        let sha2_result = {
203            let mut hasher = Sha256::default();
204            hasher.update(&api_nonce);
205            hasher.update(&api_post);
206            hasher.finalize()
207        };
208
209        type HmacSha = Hmac<Sha512>;
210        let mut mac = HmacSha::new_from_slice(&self.hmac_sha_key).unwrap();
211        mac.update(api_path.as_bytes());
212        mac.update(&sha2_result);
213        let out = mac.finalize().into_bytes();
214
215        let api_signature = BASE64_STANDARD.encode(&out);
216        api_signature
217    }
218    pub fn place_limit_order(
219        &self,
220        symbol: Symbols,
221        volume: f64,
222        action: Action,
223        limit: f64,
224    ) -> Result<KrakenResponse, Box<dyn std::error::Error>> {
225        // load from API_Public_Key
226        let api_path = "/0/private/AddOrder";
227        let api_nonce = self.get_nonce();
228
229        // Assuming trade_symbol, make_trade, trade_size, trade_direction, and trade_leverage are defined
230        let params = [
231            ("nonce", &api_nonce),
232            ("ordertype", &"limit".to_string()),
233            ("pair", &symbol.to_string()), //&symbol.to_string()),
234            ("type", &action.to_string()),
235            ("volume", &format!("{volume:.6}")),
236            ("price", &format!("{limit:.6}")),
237        ];
238        self.request_private(api_path, &params, &api_nonce)
239    }
240    //
241    // Place a limit order at the best price available, this is used to reduce the fees
242    //
243    pub fn place_best_limit_order(
244        &self,
245        symbol: Symbols,
246        volume: f64,
247        action: Action,
248    ) -> Result<KrakenResponse, Box<dyn std::error::Error>> {
249        let orderbook = self.order_book(&symbol)?;
250        let binding = orderbook.result.unwrap();
251        let best_price = match action {
252            Action::Buy => &binding
253                .get(&symbol.to_alt_string())
254                .unwrap()
255                .get("asks")
256                .unwrap()[0][0],
257            Action::Sell => &binding
258                .get(&symbol.to_alt_string())
259                .unwrap()
260                .get("bids")
261                .unwrap()[0][0],
262        };
263        let limit = best_price.as_str().unwrap().parse().unwrap();
264        self.place_limit_order(symbol, volume, action, limit)
265
266    }
267
268    pub fn place_market_order(
269        &self,
270        symbol: Symbols,
271        volume: f64,
272        action: Action,
273    ) -> Result<KrakenResponse, Box<dyn std::error::Error>> {
274        // load from API_Public_Key
275        // TODO: this check has to be don for all private functions
276
277        let api_path = "/0/private/AddOrder";
278        let api_nonce = self.get_nonce();
279
280        // Assuming trade_symbol, make_trade, trade_size, trade_direction, and trade_leverage are defined
281        let params = [
282            ("nonce", &api_nonce),
283            ("ordertype", &"market".to_string()),
284            ("pair", &symbol.to_string()), //&symbol.to_string()),
285            ("type", &action.to_string()),
286            ("volume", &format!("{volume:.6}")),
287        ];
288        self.request_private(api_path, &params, &api_nonce)
289    }
290    pub fn cancel_order(
291        &self,
292        order_id: String,
293    ) -> Result<KrakenResponse, Box<dyn std::error::Error>> {
294        let api_path = "/0/private/CancelOrder";
295        let api_nonce = self.get_nonce();
296
297        let params = [("nonce", &api_nonce), ("txid", &order_id)];
298        self.request_private(api_path, &params, &api_nonce)
299    }
300    pub fn get_open_orders(&self) -> Result<KrakenResponse, Box<dyn std::error::Error>> {
301        println!("Open orders");
302        let api_path = "/0/private/OpenOrders";
303        let api_nonce = self.get_nonce();
304
305        let params = [("nonce", &api_nonce), ("trades", &"true".to_string())];
306        self.request_private(api_path, &params, &api_nonce)
307    }
308    pub fn get_closed_orders(&self) -> Result<KrakenResponse, Box<dyn std::error::Error>> {
309        println!("Closed orders");
310        let api_path = "/0/private/ClosedOrders";
311        let api_nonce = self.get_nonce();
312
313        let params = [("nonce", &api_nonce), ("trades", &"true".to_string())];
314        self.request_private(api_path, &params, &api_nonce)
315    }
316}