1use std::collections::HashMap;
2
3use futures_util::Future;
4use serde::Deserialize;
5
6use super::super::{Client, HttpError, HttpVerb};
7
8static DONE_ORDERS_RESOURCE: &str = "orders/done";
9static LIST_ASSET_PAIRS_RESOURCE: &str = "markets/currency_pairs";
10static OPEN_ORDERS_RESOURCE: &str = "orders/open";
11static ORDERS_RESOURCE: &str = "orders";
12
13#[derive(Clone, Debug, Deserialize)]
14pub enum OrderStatus {
15 Started,
16 #[serde(rename = "Cancel pending")]
17 Pending,
18 Canceled,
19 Filled,
20 Done,
21}
22
23#[derive(Clone, Debug, Deserialize)]
24pub struct AssetPair {
25 pub formatted_symbol: String,
26 pub symbol: String,
27}
28
29#[derive(Clone, Debug, Deserialize)]
30pub struct CancelledOrderResponse {
31 pub orders: Vec<CancelledOrder>,
32}
33
34#[derive(Clone, Debug, Deserialize)]
35pub struct CancelledOrder {
36 pub id: Option<usize>,
37 pub status: OrderStatus,
38}
39
40#[derive(Clone, Debug, Deserialize)]
41pub struct CryptoDepositAddress {
42 pub address: String,
43 pub currency: String,
44}
45
46#[derive(Clone, Debug, Deserialize)]
47pub struct ExecutedQuote {
48 pub id: usize,
49 pub side_id: usize,
50 pub action: String,
51 pub algorithm_id: usize,
52 pub algorithm: String,
53 #[serde(rename = "type")]
54 pub execution_type: String,
55 pub pair: String,
56 pub quantity: f64,
57 pub price: f64,
58 pub amount: f64,
59 pub net_market_amount: f64,
60 pub filled: f64,
61 pub vwap: f64,
62 pub filled_amount: f64,
63 pub fees: f64,
64 pub net_proceeds: f64,
65 pub status: String,
66 pub status_code: usize,
67 pub routing_option: String,
68 pub routing_type: String,
69 pub time_in_force: String,
70 pub expires: Option<String>,
71 pub dateupdated: String,
72 pub client_order_id: Option<String>,
73 pub user_tx_id: Option<String>,
74 pub o_action: String,
75 pub algo_id: usize,
76 pub algorithm_options: Option<String>,
77 pub destination: Option<String>,
78}
79
80#[derive(Clone, Debug, Deserialize)]
81pub struct Order {
82 pub id: usize,
83 pub quantity: f64,
84 pub price: f64,
85 pub o_action: String,
86 pub pair: String,
87 #[serde(rename = "type")]
88 pub order_type: String,
89 pub vwap: f64,
90 pub filled: f64,
91 pub status: OrderStatus,
92}
93
94impl Client {
95 pub fn cancel_all_orders(
96 self,
97 ) -> impl Future<Output = Result<CancelledOrderResponse, HttpError>> {
98 let url = self.url_for_v1_resource(OPEN_ORDERS_RESOURCE);
99 self.request(HttpVerb::Delete, &url, None)
100 }
101
102 pub fn cancel_order(
103 self,
104 order_id: usize,
105 ) -> impl Future<Output = Result<CancelledOrder, HttpError>> {
106 let url = self.url_for_v1_resource(&format!("{}/{}", ORDERS_RESOURCE, order_id));
107 self.request(HttpVerb::Delete, &url, None)
108 }
109
110 pub fn cancel_orders(
111 self,
112 order_ids: Vec<usize>,
113 ) -> impl Future<Output = Result<CancelledOrderResponse, HttpError>> {
114 let order_ids_query_param = order_ids
116 .iter()
117 .map(|id| id.to_string())
118 .collect::<Vec<String>>()
119 .join(",");
120
121 let url = self.url_for_v1_resource(&format!(
122 "{}?ids={}",
123 ORDERS_RESOURCE, order_ids_query_param
124 ));
125
126 self.request(HttpVerb::Delete, &url, None)
127 }
128
129 pub fn open_orders(self) -> impl Future<Output = Result<Vec<Order>, HttpError>> {
130 let url = self.url_for_v1_resource(ORDERS_RESOURCE);
131 self.request(HttpVerb::Get, &url, None)
132 }
133
134 #[allow(clippy::too_many_arguments)]
135 pub fn place_order(
136 self,
137 side: &str,
138 currency_pair: &str,
139 price: f64,
140 quantity: f64,
141 routing_type: &str,
142 algorithm_id: usize,
143 client_order_id: Option<&str>,
144 ) -> impl Future<Output = Result<Order, HttpError>> {
145 let mut params = HashMap::new();
146 params.insert("currency_pair".to_string(), currency_pair.to_string());
147 params.insert("price".to_string(), price.to_string());
148 params.insert("quantity".to_string(), quantity.to_string());
149 params.insert("routing_type".to_string(), routing_type.to_string());
150 params.insert("algorithm_id".to_string(), algorithm_id.to_string());
151 if let Some(client_order_id) = client_order_id {
152 params.insert("client_order_id".to_string(), client_order_id.to_string());
153 }
154
155 let url = self.url_for_v1_resource(&format!("{}/{}", ORDERS_RESOURCE, side));
156
157 self.request(HttpVerb::Post, &url, Some(¶ms))
158 }
159
160 pub fn order_status(self, order_id: &str) -> impl Future<Output = Result<Order, HttpError>> {
161 let url = self.url_for_v1_resource(&format!("{}/{}", ORDERS_RESOURCE, order_id));
162
163 self.request(HttpVerb::Get, &url, None)
164 }
165
166 pub fn done_orders(self) -> impl Future<Output = Result<Vec<ExecutedQuote>, HttpError>> {
167 let url = self.url_for_v1_resource(DONE_ORDERS_RESOURCE);
168
169 self.request(HttpVerb::Get, &url, None)
170 }
171
172 pub fn list_asset_pairs(
173 self,
174 ) -> impl Future<Output = Result<HashMap<String, AssetPair>, HttpError>> {
175 let url = self.url_for_v1_resource(LIST_ASSET_PAIRS_RESOURCE);
176
177 self.request(HttpVerb::Get, &url, None)
178 }
179}
180
181#[cfg(test)]
182mod tests {
183 use super::*;
184
185 use crate::util::server::{new_test_server_and_client, ApiMock};
186
187 const CANCEL_PENDING_ORDER_RESPONSE_BODY: &str = r#"
188 {"id": 3, "status": "Cancel pending"}
189 "#;
190
191 const CANCEL_MULTIPLE_ORDERS_FAILED_RESPONSE_BODY: &str = r#"
192 { "error": "the order ids provided were invalid or the orders were already done/canceled" }
193 "#;
194
195 const OPEN_ORDERS_RESPONSE_BODY: &str = r#"
196 [
197 {
198 "id": 123,
199 "quantity": 1,
200 "price": 10,
201 "o_action": "Buy",
202 "pair": "btcusd",
203 "type": "Limit",
204 "vwap": 0,
205 "filled": 0,
206 "status": "Started"
207 },
208 {
209 "id": 456,
210 "quantity": 1,
211 "price": 10,
212 "o_action": "Sell",
213 "pair": "btcusd",
214 "type": "Limit",
215 "vwap": 0,
216 "filled": 0,
217 "status": "Started"
218 }
219 ]
220 "#;
221
222 const ORDERS_RESPONSE_BODY: &str = r#"
223 {
224 "orders": [
225 {"id": 2, "status": "Cancel pending"},
226 {"id": 3, "status": "Canceled"},
227 {"id": 4, "status": "Canceled"}
228 ]
229 }
230 "#;
231
232 const ORDER_RESPONSE_BODY: &str = r#"
233 {
234 "id": 123,
235 "quantity": 1,
236 "price": 10,
237 "o_action": "Buy",
238 "pair": "btcusd",
239 "type": "Limit",
240 "vwap": 0,
241 "filled": 0,
242 "status": "Started"
243 }
244 "#;
245
246 const DONE_ORDERS_RESPONSE_BODY: &str = r#"
247 [
248 {
249 "id": 701968334,
250 "side_id": 500,
251 "action": "Buy",
252 "algorithm_id": 200,
253 "algorithm": "Smart",
254 "type": "Smart",
255 "pair": "btcusd",
256 "quantity": 0.001,
257 "price": 16900.6,
258 "amount": 0,
259 "net_market_amount": 0,
260 "filled": 0.001,
261 "vwap": 16900.6,
262 "filled_amount": 16.9006,
263 "fees": 0.0338012,
264 "net_proceeds": -16.8667988,
265 "status": "Done",
266 "status_code": 300,
267 "routing_option": "BestPrice",
268 "routing_type": "NetPrice",
269 "time_in_force": "GTC",
270 "expires": null,
271 "dateupdated": "2022-11-18T01:26:40.000Z",
272 "client_order_id": "94b0e7c4-0fa7-403d-a0d0-6c4ccec76630",
273 "user_tx_id": "94b0e7c4-0fa7-403d-a0d0-6c4ccec76630",
274 "o_action": "Buy",
275 "algo_id": 200,
276 "algorithm_options": null,
277 "destination": ""
278 },
279 {
280 "id": 701945645,
281 "side_id": 500,
282 "action": "Buy",
283 "algorithm_id": 201,
284 "algorithm": "Limit",
285 "type": "Limit",
286 "pair": "btcusd",
287 "quantity": 0.01,
288 "price": 16905,
289 "amount": 0,
290 "net_market_amount": 0,
291 "filled": 0.01,
292 "vwap": 16643,
293 "filled_amount": 166.43,
294 "fees": 0.16643,
295 "net_proceeds": -166.26357,
296 "status": "Done",
297 "status_code": 300,
298 "routing_option": "BestPrice",
299 "routing_type": "NetPrice",
300 "time_in_force": "GTC",
301 "expires": null,
302 "dateupdated": "2022-11-17T19:39:18.000Z",
303 "client_order_id": "",
304 "user_tx_id": "",
305 "o_action": "Buy",
306 "algo_id": 201,
307 "algorithm_options": null,
308 "destination": ""
309 }
310 ]
311 "#;
312
313 const LIST_ASSET_PAIRS_RESPONSE_BODY: &str = r#"
314 {
315 "bchbtc": {
316 "formatted_symbol": "BCH/BTC",
317 "symbol": "bchbtc"
318 },
319 "bchusd": {
320 "formatted_symbol": "BCH/USD",
321 "symbol": "bchusd"
322 },
323 "btcusd": {
324 "formatted_symbol": "BTC/USD",
325 "symbol": "btcusd"
326 }
327 }
328 "#;
329
330 #[tokio::test]
331 async fn test_cancel_all_orders() {
332 let mock = ApiMock {
333 action: HttpVerb::Delete,
334 body: ORDERS_RESPONSE_BODY.into(),
335 path: format!("/v1/{}", OPEN_ORDERS_RESOURCE),
336 response_code: 200,
337 };
338
339 let (client, _server, mock_results) = new_test_server_and_client(vec![mock]).await;
340
341 let result = client.cancel_all_orders().await;
342
343 assert!(result.is_ok());
344
345 for mock in mock_results {
346 mock.assert_async().await;
347 }
348 }
349
350 #[tokio::test]
351 async fn test_cancel_multiple_orders() {
352 let ids = vec![2, 3];
353 let ids_param = ids
354 .iter()
355 .map(|id| id.to_string())
356 .collect::<Vec<String>>()
357 .join(",");
358
359 let mock = ApiMock {
360 action: HttpVerb::Delete,
361 body: ORDERS_RESPONSE_BODY.into(),
362 path: format!("/v1/{}?ids={}", ORDERS_RESOURCE, ids_param),
363 response_code: 200,
364 };
365
366 let (client, _server, mock_results) = new_test_server_and_client(vec![mock]).await;
367
368 let result = client.cancel_orders(vec![2, 3]).await;
369
370 assert!(result.is_ok());
371
372 for mock in mock_results {
373 mock.assert_async().await;
374 }
375 }
376
377 #[tokio::test]
378 async fn test_cancel_multiple_orders_failed() {
379 let ids = vec![2, 3];
380 let ids_param = ids
381 .iter()
382 .map(|id| id.to_string())
383 .collect::<Vec<String>>()
384 .join(",");
385
386 let mock = ApiMock {
387 action: HttpVerb::Delete,
388 body: CANCEL_MULTIPLE_ORDERS_FAILED_RESPONSE_BODY.into(),
389 path: format!("/v1/{}?ids={}", ORDERS_RESOURCE, ids_param),
390 response_code: 400,
391 };
392
393 let (client, _server, mock_results) = new_test_server_and_client(vec![mock]).await;
394
395 let result = client.cancel_orders(vec![2, 3]).await;
396
397 assert!(result.is_err());
398 let err = result.unwrap_err();
399 assert!(
400 err.to_string()
401 == "Error while making request: `\"the order ids provided were invalid or the orders were already done/canceled\"`"
402 );
403
404 for mock in mock_results {
405 mock.assert_async().await;
406 }
407 }
408
409 #[tokio::test]
410 async fn test_cancel_order() {
411 let id = 2;
412
413 let mock = ApiMock {
414 action: HttpVerb::Delete,
415 body: CANCEL_PENDING_ORDER_RESPONSE_BODY.into(),
416 path: format!("/v1/{}/{}", ORDERS_RESOURCE, id),
417 response_code: 200,
418 };
419
420 let (client, _server, mock_results) = new_test_server_and_client(vec![mock]).await;
421
422 let result = client.cancel_order(id).await;
423 assert!(result.is_ok());
424
425 for mock in mock_results {
426 mock.assert_async().await;
427 }
428 }
429
430 #[tokio::test]
431 async fn test_place_order() {
432 let side = "sell";
433
434 let mock = ApiMock {
435 action: HttpVerb::Post,
436 body: ORDER_RESPONSE_BODY.into(),
437 path: format!("/v1/{}/{}", ORDERS_RESOURCE, side),
438 response_code: 200,
439 };
440
441 let (client, _server, mock_results) = new_test_server_and_client(vec![mock]).await;
442
443 let result = client
444 .place_order(side, "ethusd", 0.123, 0.456, "NetPrice", 100, "123A".into())
445 .await;
446
447 assert!(result.is_ok());
448
449 for mock in mock_results {
450 mock.assert_async().await;
451 }
452 }
453
454 #[tokio::test]
455 async fn test_order_status() {
456 let order_id = "abc";
457 let mock = ApiMock {
458 action: HttpVerb::Get,
459 body: ORDER_RESPONSE_BODY.into(),
460 path: format!("/v1/{}/{}", ORDERS_RESOURCE, order_id),
461 response_code: 200,
462 };
463
464 let (client, _server, mock_results) = new_test_server_and_client(vec![mock]).await;
465
466 let result = client.order_status(order_id).await;
467
468 assert!(result.is_ok());
469
470 for mock in mock_results {
471 mock.assert_async().await;
472 }
473 }
474
475 #[tokio::test]
476 async fn test_open_orders() {
477 let mock = ApiMock {
478 action: HttpVerb::Get,
479 body: OPEN_ORDERS_RESPONSE_BODY.into(),
480 path: format!("/v1/{}", ORDERS_RESOURCE),
481 response_code: 200,
482 };
483
484 let (client, _server, mock_results) = new_test_server_and_client(vec![mock]).await;
485
486 let result = client.open_orders().await;
487
488 assert!(result.is_ok());
489
490 for mock in mock_results {
491 mock.assert_async().await;
492 }
493 }
494
495 #[tokio::test]
496 async fn test_done_orders() {
497 let mock = ApiMock {
498 action: HttpVerb::Get,
499 body: DONE_ORDERS_RESPONSE_BODY.into(),
500 path: format!("/v1/{}", DONE_ORDERS_RESOURCE),
501 response_code: 200,
502 };
503
504 let (client, _server, mock_results) = new_test_server_and_client(vec![mock]).await;
505
506 let result = client.done_orders().await;
507
508 assert!(result.is_ok());
509
510 for mock in mock_results {
511 mock.assert_async().await;
512 }
513 }
514
515 #[tokio::test]
516 async fn test_list_asset_pairs() {
517 let mock = ApiMock {
518 action: HttpVerb::Get,
519 body: LIST_ASSET_PAIRS_RESPONSE_BODY.into(),
520 path: format!("/v1/{}", LIST_ASSET_PAIRS_RESOURCE),
521 response_code: 200,
522 };
523
524 let (client, _server, mock_results) = new_test_server_and_client(vec![mock]).await;
525
526 let result = client.list_asset_pairs().await;
527
528 assert!(result.is_ok());
529
530 for mock in mock_results {
531 mock.assert_async().await;
532 }
533 }
534}