1use std::{
4 collections::HashMap,
5 sync::{
6 Arc, Mutex,
7 atomic::{AtomicBool, AtomicU64, Ordering},
8 },
9 time::Duration,
10};
11
12use serde::de::DeserializeOwned;
13use tokio::{
14 sync::{mpsc, oneshot},
15 time::timeout,
16};
17
18use crate::{
19 ServerClock, Timestamp,
20 http::{
21 AmendOrderBatchRequest, AmendOrderBatchResult, AmendOrderRequest, AmendOrderResponse,
22 CancelOrderBatchRequest, CancelOrderBatchResult, CancelOrderRequest, CancelOrderResponse,
23 List, PlaceOrderBatchRequest, PlaceOrderBatchResult, PlaceOrderRequest, PlaceOrderResponse,
24 RetExtInfo,
25 },
26 timestamp,
27 ws::{
28 Config, Error, Event, Handle, IncomingMessage, OutgoingMessage, Stream, TradeHeader,
29 TradeReply, TradeReplyHeader,
30 },
31};
32
33#[derive(Debug, Clone, PartialEq)]
38pub struct TradeResponse<T> {
39 pub data: T,
41 pub ret_ext_info: Option<RetExtInfo>,
43 pub header: Option<TradeReplyHeader>,
45}
46
47#[derive(Debug, Clone)]
90pub struct TradeClient {
91 handle: Handle,
92 shared: Arc<Shared>,
93 clock: ServerClock,
94 recv_window: Timestamp,
95 request_timeout: Duration,
96}
97
98#[derive(Debug, Default)]
99struct Shared {
100 pending: Mutex<HashMap<String, oneshot::Sender<TradeReply>>>,
102 ready: AtomicBool,
104 next_id: AtomicU64,
105 id_prefix: String,
107}
108
109impl Shared {
110 fn fail_pending(&self) {
112 self.pending.lock().unwrap().clear();
113 }
114}
115
116impl TradeClient {
117 pub fn new(config: Config) -> Result<(Self, mpsc::Receiver<Event>), Error> {
122 if config.api_key.is_none() || config.api_secret.is_none() {
123 return Err(Error::MissingCredentials);
124 }
125 let clock = config.server_clock.clone();
126 let recv_window = config.auth_recv_window;
127 let request_timeout = config.request_timeout;
128 let event_queue_size = config.event_queue_size;
129
130 let shared = Arc::new(Shared {
131 id_prefix: format!("t{:x}", timestamp()),
132 ..Default::default()
133 });
134 let (handle, driver_events) = Stream::new(config);
135 let (tx, rx) = mpsc::channel(event_queue_size);
136 tokio::spawn(route(driver_events, tx, shared.clone()));
137
138 let client = Self {
139 handle,
140 shared,
141 clock,
142 recv_window,
143 request_timeout,
144 };
145 Ok((client, rx))
146 }
147
148 pub fn handle(&self) -> &Handle {
150 &self.handle
151 }
152
153 pub async fn connect(&self) -> Result<(), Error> {
156 self.handle.connect().await
157 }
158
159 pub async fn disconnect(&self) -> Result<(), Error> {
161 self.handle.disconnect().await
162 }
163
164 pub fn is_ready(&self) -> bool {
166 self.shared.ready.load(Ordering::Relaxed)
167 }
168
169 pub async fn place_order(
171 &self,
172 request: &PlaceOrderRequest,
173 ) -> Result<TradeResponse<PlaceOrderResponse>, Error> {
174 self.call(|req_id, header| OutgoingMessage::OrderCreate {
175 req_id,
176 header,
177 args: vec![request.clone()],
178 })
179 .await
180 }
181
182 pub async fn amend_order(
184 &self,
185 request: &AmendOrderRequest,
186 ) -> Result<TradeResponse<AmendOrderResponse>, Error> {
187 self.call(|req_id, header| OutgoingMessage::OrderAmend {
188 req_id,
189 header,
190 args: vec![request.clone()],
191 })
192 .await
193 }
194
195 pub async fn cancel_order(
197 &self,
198 request: &CancelOrderRequest,
199 ) -> Result<TradeResponse<CancelOrderResponse>, Error> {
200 self.call(|req_id, header| OutgoingMessage::OrderCancel {
201 req_id,
202 header,
203 args: vec![request.clone()],
204 })
205 .await
206 }
207
208 pub async fn place_order_batch(
211 &self,
212 request: &PlaceOrderBatchRequest,
213 ) -> Result<TradeResponse<List<PlaceOrderBatchResult>>, Error> {
214 self.call(|req_id, header| OutgoingMessage::OrderCreateBatch {
215 req_id,
216 header,
217 args: vec![request.clone()],
218 })
219 .await
220 }
221
222 pub async fn amend_order_batch(
224 &self,
225 request: &AmendOrderBatchRequest,
226 ) -> Result<TradeResponse<List<AmendOrderBatchResult>>, Error> {
227 self.call(|req_id, header| OutgoingMessage::OrderAmendBatch {
228 req_id,
229 header,
230 args: vec![request.clone()],
231 })
232 .await
233 }
234
235 pub async fn cancel_order_batch(
237 &self,
238 request: &CancelOrderBatchRequest,
239 ) -> Result<TradeResponse<List<CancelOrderBatchResult>>, Error> {
240 self.call(|req_id, header| OutgoingMessage::OrderCancelBatch {
241 req_id,
242 header,
243 args: vec![request.clone()],
244 })
245 .await
246 }
247
248 async fn call<T: DeserializeOwned>(
249 &self,
250 build: impl FnOnce(String, TradeHeader) -> OutgoingMessage,
251 ) -> Result<TradeResponse<T>, Error> {
252 if !self.is_ready() {
253 return Err(Error::NotConnected);
254 }
255 let n = self.shared.next_id.fetch_add(1, Ordering::Relaxed);
256 let req_id = format!("{}-{n}", self.shared.id_prefix);
257 let (tx, rx) = oneshot::channel();
258 self.shared
259 .pending
260 .lock()
261 .unwrap()
262 .insert(req_id.clone(), tx);
263
264 let header = TradeHeader {
265 timestamp: self.clock.now().to_string(),
266 recv_window: self.recv_window.to_string(),
267 referer: None,
268 };
269 if let Err(e) = self
270 .handle
271 .send_command(build(req_id.clone(), header))
272 .await
273 {
274 self.shared.pending.lock().unwrap().remove(&req_id);
275 return Err(e);
276 }
277
278 let reply = match timeout(self.request_timeout, rx).await {
279 Ok(Ok(reply)) => reply,
280 Ok(Err(_)) => return Err(Error::ConnectionLost),
281 Err(_) => {
282 self.shared.pending.lock().unwrap().remove(&req_id);
283 return Err(Error::Timeout);
284 }
285 };
286 if reply.ret_code != 0 {
287 return Err(Error::Api {
288 code: reply.ret_code,
289 msg: reply.ret_msg,
290 });
291 }
292 let data = serde_json::from_value(reply.data.unwrap_or_default())
293 .map_err(|e| Error::InvalidReply(e.to_string()))?;
294 let ret_ext_info = reply
295 .ret_ext_info
296 .and_then(|value| serde_json::from_value(value).ok());
297 Ok(TradeResponse {
298 data,
299 ret_ext_info,
300 header: reply.header,
301 })
302 }
303}
304
305async fn route(
307 mut driver_events: mpsc::Receiver<Event>,
308 tx: mpsc::Sender<Event>,
309 shared: Arc<Shared>,
310) {
311 while let Some(event) = driver_events.recv().await {
312 match &event {
313 Event::Authenticated => shared.ready.store(true, Ordering::Relaxed),
314 Event::Connected
315 | Event::Reconnecting { .. }
316 | Event::Disconnected { .. }
317 | Event::AuthFailed { .. } => {
318 shared.ready.store(false, Ordering::Relaxed);
319 shared.fail_pending();
320 }
321 Event::Message(IncomingMessage::TradeReply(reply)) => {
322 let waiting = reply
323 .req_id
324 .as_ref()
325 .and_then(|id| shared.pending.lock().unwrap().remove(id));
326 if let Some(waiting) = waiting {
327 let _ = waiting.send((**reply).clone());
328 continue;
329 }
330 }
331 _ => {}
332 }
333 let _ = tx.send(event).await;
335 }
336 shared.ready.store(false, Ordering::Relaxed);
337 shared.fail_pending();
338}
339
340#[cfg(test)]
341mod tests {
342 use std::time::Duration;
343
344 use futures_util::{SinkExt, StreamExt};
345 use rust_decimal::dec;
346 use tokio::net::TcpListener;
347 use tokio_tungstenite::tungstenite::Message;
348
349 use super::*;
350 use crate::{Category, OrderType, Side};
351
352 #[derive(Clone, Copy)]
354 enum Answer {
355 Accept,
356 Reject,
357 Silent,
358 Drop,
360 }
361
362 type Received = Arc<Mutex<Vec<serde_json::Value>>>;
364
365 async fn spawn_trade_server(answer: Answer) -> (String, Received) {
368 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
369 let addr = listener.local_addr().unwrap();
370 let received: Received = Default::default();
371 let recorded = received.clone();
372 tokio::spawn(async move {
373 while let Ok((tcp, _)) = listener.accept().await {
374 let recorded = recorded.clone();
375 tokio::spawn(async move {
376 let Ok(mut ws) = tokio_tungstenite::accept_async(tcp).await else {
377 return;
378 };
379 while let Some(Ok(Message::Text(text))) = ws.next().await {
380 let json: serde_json::Value = serde_json::from_str(&text).unwrap();
381 recorded.lock().unwrap().push(json.clone());
382 let op = json["op"].as_str().unwrap_or_default().to_owned();
383 let reply = match op.as_str() {
384 "auth" => serde_json::json!({
385 "retCode": 0, "retMsg": "OK", "op": "auth", "connId": "c"
386 }),
387 "ping" => serde_json::json!({
388 "retCode": 0, "retMsg": "OK", "op": "pong", "connId": "c"
389 }),
390 _ => match answer {
391 Answer::Accept => serde_json::json!({
392 "reqId": json["reqId"], "retCode": 0, "retMsg": "OK",
393 "op": op, "data": {"orderId": "order-1", "orderLinkId": ""},
394 "retExtInfo": {},
395 "header": {"X-Bapi-Limit": "10", "X-Bapi-Limit-Status": "9"},
396 "connId": "c"
397 }),
398 Answer::Reject => serde_json::json!({
399 "reqId": json["reqId"], "retCode": 10001,
400 "retMsg": "params error", "op": op, "connId": "c"
401 }),
402 Answer::Silent => continue,
403 Answer::Drop => return,
404 },
405 };
406 if ws
407 .send(Message::Text(reply.to_string().into()))
408 .await
409 .is_err()
410 {
411 return;
412 }
413 }
414 });
415 }
416 });
417 (format!("ws://{addr}"), received)
418 }
419
420 fn config(url: String) -> Config {
421 Config::new(url)
422 .credentials("key", "secret")
423 .ping_interval(None)
424 .max_reconnect_attempts(0)
425 .request_timeout(Duration::from_millis(300))
426 }
427
428 async fn connected(config: Config) -> (TradeClient, mpsc::Receiver<Event>) {
430 let (trade, mut events) = TradeClient::new(config).unwrap();
431 trade.connect().await.unwrap();
432 timeout(Duration::from_secs(10), async {
433 while let Some(event) = events.recv().await {
434 if matches!(event, Event::Authenticated) {
435 return;
436 }
437 }
438 })
439 .await
440 .expect("not authenticated");
441 assert!(trade.is_ready());
442 (trade, events)
443 }
444
445 fn order() -> PlaceOrderRequest {
446 let mut order = PlaceOrderRequest::new(
447 Category::Linear,
448 "BTCUSDT",
449 Side::Buy,
450 OrderType::Limit,
451 dec!(0.001),
452 );
453 order.price = Some(dec!(10000));
454 order
455 }
456
457 #[tokio::test]
458 async fn accepted_order_returns_the_reply_data() {
459 let (url, received) = spawn_trade_server(Answer::Accept).await;
460 let (trade, _events) = connected(config(url)).await;
461
462 let reply = trade.place_order(&order()).await.unwrap();
463
464 assert_eq!(reply.data.order_id, "order-1");
465 assert_eq!(reply.header.unwrap().limit_status, Some(9));
466 let request = received.lock().unwrap().last().cloned().unwrap();
467 assert_eq!(request["op"], "order.create");
468 assert_eq!(request["args"][0]["symbol"], "BTCUSDT");
469 assert_eq!(request["header"]["X-BAPI-RECV-WINDOW"], "5000");
470 let sent: i64 = request["header"]["X-BAPI-TIMESTAMP"]
471 .as_str()
472 .unwrap()
473 .parse()
474 .unwrap();
475 assert!((sent - timestamp() as i64).abs() < 1000);
476 assert!(request["reqId"].as_str().unwrap().len() <= 36);
477 }
478
479 #[tokio::test]
480 async fn concurrent_requests_get_their_own_replies() {
481 let (url, received) = spawn_trade_server(Answer::Accept).await;
482 let (trade, _events) = connected(config(url)).await;
483
484 let order = order();
485 let (a, b) = tokio::join!(trade.place_order(&order), trade.place_order(&order));
486
487 assert!(a.is_ok() && b.is_ok());
488 let ids: Vec<_> = received
489 .lock()
490 .unwrap()
491 .iter()
492 .filter(|r| r["op"] == "order.create")
493 .map(|r| r["reqId"].as_str().unwrap().to_owned())
494 .collect();
495 assert_eq!(ids.len(), 2);
496 assert_ne!(ids[0], ids[1]);
497 }
498
499 #[tokio::test]
500 async fn rejected_order_is_an_api_error() {
501 let (url, _) = spawn_trade_server(Answer::Reject).await;
502 let (trade, _events) = connected(config(url)).await;
503
504 let err = trade.place_order(&order()).await.unwrap_err();
505
506 assert!(matches!(err, Error::Api { code: 10001, .. }), "{err:?}");
507 }
508
509 #[tokio::test]
510 async fn missing_reply_times_out() {
511 let (url, _) = spawn_trade_server(Answer::Silent).await;
512 let (trade, _events) = connected(config(url)).await;
513
514 let err = trade.place_order(&order()).await.unwrap_err();
515
516 assert!(matches!(err, Error::Timeout), "{err:?}");
517 assert!(trade.shared.pending.lock().unwrap().is_empty());
518 }
519
520 #[tokio::test]
521 async fn lost_connection_fails_the_waiting_request() {
522 let (url, _) = spawn_trade_server(Answer::Drop).await;
523 let (trade, _events) =
524 connected(config(url).request_timeout(Duration::from_secs(10))).await;
525
526 let err = trade.place_order(&order()).await.unwrap_err();
527
528 assert!(matches!(err, Error::ConnectionLost), "{err:?}");
529 assert!(!trade.is_ready());
530 }
531
532 #[tokio::test]
533 async fn requests_before_authentication_are_refused() {
534 let (url, _) = spawn_trade_server(Answer::Accept).await;
535 let (trade, _events) = TradeClient::new(config(url)).unwrap();
536
537 let err = trade.place_order(&order()).await.unwrap_err();
538
539 assert!(matches!(err, Error::NotConnected), "{err:?}");
540 }
541
542 #[tokio::test]
543 async fn credentials_are_required() {
544 let result = TradeClient::new(Config::new("ws://127.0.0.1:1"));
545
546 assert!(matches!(result, Err(Error::MissingCredentials)));
547 }
548
549 #[tokio::test]
550 async fn order_entry_heartbeat_keeps_the_connection() {
551 let (url, received) = spawn_trade_server(Answer::Accept).await;
552 let config = config(url)
553 .ping_interval(Some(Duration::from_millis(50)))
554 .pong_timeout(Duration::from_millis(200));
555 let (_trade, mut events) = connected(config).await;
556
557 tokio::time::sleep(Duration::from_millis(600)).await;
558
559 let pings = received
560 .lock()
561 .unwrap()
562 .iter()
563 .filter(|r| r["op"] == "ping")
564 .count();
565 assert!(pings >= 3, "only {pings} pings");
566 while let Ok(event) = events.try_recv() {
567 assert!(
568 !matches!(
569 event,
570 Event::Reconnecting { .. } | Event::Disconnected { .. }
571 ),
572 "{event:?}"
573 );
574 }
575 }
576}