sfox/http/v1/
order.rs

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        // Create a comma separated list of order ids from the vector
115        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(&params))
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}