1use crate::clock::ServerClock;
23use crate::credentials::Credentials;
24use crate::error::{Error, Result};
25use crate::events::{BookDelta, BookLevel, Event, OrderBookSnapshot, TradePrint};
26use crate::feeds::{
27 DerivativesChannel, DerivativesFeed, FundingRate, LongShortRatio, MarkIndex, OpenInterest,
28};
29use crate::normalize::{format_decimal, parse_decimal};
30use crate::options::{ExchangeOptions, MarginMode, MarketType, PositionMode, SelfTradePrevention};
31use crate::positions::{Position, PositionSide};
32use crate::signing::{hmac_sha512_hex, sha512_hex};
33use crate::symbol::Symbol;
34use crate::traits::{
35 AdvancedOrders, Derivatives, DerivativesStream, Exchange, Execution, MarketData, WsExecution,
36 WsUserData,
37};
38use crate::transport::{
39 HttpMethod, HttpRequest, HttpResponse, HttpTransport, WsConnection, WsTransport,
40};
41use crate::types::{
42 Balance, OcoRequest, Order, OrderRequest, OrderSide, OrderStatus, OrderType, Ticker,
43 TimeInForce,
44};
45use rust_decimal::prelude::ToPrimitive;
46use rust_decimal::Decimal;
47use serde::Deserialize;
48use std::cell::RefCell;
49use std::collections::HashMap;
50use std::fmt;
51use wickra_core::Candle;
52
53fn system_now_ms() -> i64 {
54 use std::time::{SystemTime, UNIX_EPOCH};
55 SystemTime::now()
56 .duration_since(UNIX_EPOCH)
57 .expect("system clock is before the Unix epoch")
58 .as_millis() as i64
59}
60
61pub struct Gate {
63 http: Box<dyn HttpTransport>,
64 ws: Option<Box<dyn WsTransport>>,
65 rest_base: String,
66 market_type: MarketType,
67 position_mode: PositionMode,
70 credentials: Option<Credentials>,
71 now_ms: Box<dyn Fn() -> i64 + Send + Sync>,
72 clock: ServerClock,
75 connection: Option<Box<dyn WsConnection>>,
76 sub_messages: Vec<String>,
77 subscriptions: Vec<(String, Symbol)>,
78 futures_user_id: RefCell<Option<String>>,
83 derivatives_channels: Vec<(String, DerivativesChannel)>,
88 leverage: u32,
92 private_connection: Option<Box<dyn WsConnection>>,
96 user_data_active: bool,
99 ws_api_connection: Option<Box<dyn WsConnection>>,
103}
104
105impl fmt::Debug for Gate {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 f.debug_struct("Gate")
113 .field("ws", &self.ws.is_some())
114 .field("rest_base", &self.rest_base)
115 .field("market_type", &self.market_type)
116 .field("authenticated", &self.credentials.is_some())
117 .field("clock_offset_ms", &self.clock.offset_ms())
118 .field("connection", &self.connection.is_some())
119 .field("sub_messages", &self.sub_messages.len())
120 .field("subscriptions", &self.subscriptions.len())
121 .field("leverage", &self.leverage)
122 .field("private_connection", &self.private_connection.is_some())
123 .field("user_data_active", &self.user_data_active)
124 .field("ws_api_connection", &self.ws_api_connection.is_some())
125 .finish_non_exhaustive()
126 }
127}
128
129impl Gate {
130 fn ensure_market_is_routed(&self) -> Result<()> {
138 super::ensure_market_is_routed("Gate.io", self.market_type, super::SPOT_AND_LINEAR)
139 }
140
141 fn build(
142 http: Box<dyn HttpTransport>,
143 options: &ExchangeOptions,
144 credentials: Option<Credentials>,
145 ) -> Self {
146 Self {
147 http,
148 ws: None,
149 rest_base: "https://api.gateio.ws".to_string(),
150 market_type: options.market_type,
151 position_mode: options.position_mode,
152 credentials,
153 now_ms: Box::new(system_now_ms),
154 clock: ServerClock::new(),
155 connection: None,
156 sub_messages: Vec::new(),
157 subscriptions: Vec::new(),
158 futures_user_id: RefCell::new(None),
159 derivatives_channels: Vec::new(),
160 leverage: 1,
161 private_connection: None,
162 user_data_active: false,
163 ws_api_connection: None,
164 }
165 }
166
167 fn signed_now_ms(&self) -> i64 {
176 self.clock.server_time_ms((self.now_ms)())
177 }
178
179 pub fn sync_time(&mut self) -> Result<i64> {
191 let local_before = (self.now_ms)();
192 let value = self.get("/api/v4/spot/time", "")?;
193 let server_ms = value
194 .get("server_time")
195 .and_then(serde_json::Value::as_i64)
196 .ok_or_else(|| Error::Deserialization("spot/time: no server_time".into()))?;
197 self.clock.sync(local_before, server_ms);
198 Ok(self.clock.offset_ms())
199 }
200
201 fn is_futures(&self) -> bool {
204 self.market_type.is_derivatives()
205 }
206
207 #[must_use]
209 pub fn with_http(http: Box<dyn HttpTransport>, options: &ExchangeOptions) -> Self {
210 Self::build(http, options, None)
211 }
212
213 #[must_use]
215 pub fn with_credentials(
216 http: Box<dyn HttpTransport>,
217 options: &ExchangeOptions,
218 credentials: Credentials,
219 ) -> Self {
220 Self::build(http, options, Some(credentials))
221 }
222
223 #[must_use]
225 pub fn with_clock(mut self, now_ms: Box<dyn Fn() -> i64 + Send + Sync>) -> Self {
226 self.now_ms = now_ms;
227 self
228 }
229
230 #[must_use]
232 pub fn with_ws(mut self, ws: Box<dyn WsTransport>) -> Self {
233 self.ws = Some(ws);
234 self
235 }
236
237 #[must_use]
239 pub fn wire_symbol(symbol: &Symbol) -> String {
240 format!("{}_{}", symbol.base(), symbol.quote())
241 }
242
243 pub fn ticker(&self, symbol: &Symbol) -> Result<Ticker> {
248 if self.is_futures() {
249 return self.futures_ticker(symbol);
250 }
251 let query = format!("currency_pair={}", Self::wire_symbol(symbol));
252 let value = self.get("/api/v4/spot/tickers", &query)?;
253 let list: Vec<RawTicker> = parse_json(value)?;
254 let entry = list
255 .into_iter()
256 .next()
257 .ok_or_else(|| Error::NotFound(format!("no ticker for {symbol}")))?;
258 Ok(Ticker {
259 symbol: symbol.clone(),
260 last: parse_decimal(&entry.last)?,
261 bid: parse_decimal(&entry.highest_bid)?,
262 ask: parse_decimal(&entry.lowest_ask)?,
263 volume: parse_decimal(&entry.base_volume)?,
264 timestamp: 0,
267 })
268 }
269
270 fn futures_ticker(&self, symbol: &Symbol) -> Result<Ticker> {
273 let contract = Self::wire_symbol(symbol);
274 let value = self.get(
275 "/api/v4/futures/usdt/tickers",
276 &format!("contract={contract}"),
277 )?;
278 let list: Vec<RawFuturesTicker> = parse_json(value)?;
279 let entry = list
280 .into_iter()
281 .next()
282 .ok_or_else(|| Error::NotFound(format!("no ticker for {symbol}")))?;
283 let book = self.order_book(symbol, 1)?;
284 Ok(Ticker {
285 symbol: symbol.clone(),
286 last: parse_decimal(&entry.last)?,
287 bid: book.bids.first().map_or(Decimal::ZERO, |l| l.price),
288 ask: book.asks.first().map_or(Decimal::ZERO, |l| l.price),
289 volume: parse_decimal(&entry.volume_24h_base)?,
290 timestamp: 0,
292 })
293 }
294
295 pub fn klines(&self, symbol: &Symbol, interval: &str, limit: u32) -> Result<Vec<Candle>> {
301 if self.is_futures() {
302 let query = format!(
303 "contract={}&interval={}&limit={limit}",
304 Self::wire_symbol(symbol),
305 map_interval(interval),
306 );
307 let value = self.get("/api/v4/futures/usdt/candlesticks", &query)?;
308 let rows: Vec<RawFuturesCandle> = parse_json(value)?;
309 return rows.iter().map(parse_futures_candle).collect();
310 }
311 let query = format!(
312 "currency_pair={}&interval={}&limit={limit}",
313 Self::wire_symbol(symbol),
314 map_interval(interval),
315 );
316 let value = self.get("/api/v4/spot/candlesticks", &query)?;
317 let rows: Vec<Vec<String>> = parse_json(value)?;
318 rows.iter().map(|row| parse_kline_row(row)).collect()
319 }
320
321 pub fn order_book(&self, symbol: &Symbol, depth: u32) -> Result<OrderBookSnapshot> {
326 if self.is_futures() {
327 let query = format!("contract={}&limit={depth}", Self::wire_symbol(symbol));
328 let value = self.get("/api/v4/futures/usdt/order_book", &query)?;
329 let raw: RawFuturesDepth = parse_json(value)?;
330 return Ok(OrderBookSnapshot {
331 symbol: symbol.clone(),
332 last_update_id: raw.id,
333 bids: parse_futures_levels(&raw.bids)?,
334 asks: parse_futures_levels(&raw.asks)?,
335 timestamp: 0,
336 });
337 }
338 let query = format!("currency_pair={}&limit={depth}", Self::wire_symbol(symbol));
339 let value = self.get("/api/v4/spot/order_book", &query)?;
340 let raw: RawDepth = parse_json(value)?;
341 Ok(OrderBookSnapshot {
342 symbol: symbol.clone(),
343 last_update_id: raw.update,
344 bids: parse_levels(&raw.bids)?,
345 asks: parse_levels(&raw.asks)?,
346 timestamp: i64::try_from(raw.update).unwrap_or(0),
349 })
350 }
351
352 pub fn subscribe_derivatives(
368 &mut self,
369 symbol: &Symbol,
370 channel: DerivativesChannel,
371 ) -> Result<()> {
372 if !self.is_futures() {
373 return Err(Error::unsupported_field(
374 "Gate.io",
375 "a derivatives channel on a spot client",
376 "funding and mark/index exist only on the futures venue",
377 ));
378 }
379 if channel == DerivativesChannel::Liquidations {
380 return Err(Error::unsupported_field(
381 "Gate.io",
382 "a liquidations channel",
383 "Gate's `futures.liquidates` needs authentication and reports the \
384 caller's own liquidations, not the venue's forced flow",
385 ));
386 }
387 let wire = Self::wire_symbol(symbol);
388 if !self
389 .derivatives_channels
390 .iter()
391 .any(|(w, c)| w == &wire && *c == channel)
392 {
393 self.derivatives_channels.push((wire, channel));
394 }
395 self.subscribe(symbol, "spot.tickers")
396 }
397
398 pub fn open_interest(&self, symbol: &Symbol) -> Result<OpenInterest> {
409 if !self.is_futures() {
410 return Err(Error::unsupported_field(
411 "Gate.io",
412 "open interest on a spot client",
413 "open interest is a futures figure",
414 ));
415 }
416 let point = self.contract_stats(symbol)?;
417 Ok(OpenInterest {
418 symbol: symbol.clone(),
419 open_interest: parse_decimal(
420 &point
421 .get("open_interest")
422 .and_then(serde_json::Value::as_i64)
423 .ok_or_else(|| Error::Deserialization("no open_interest".to_string()))?
424 .to_string(),
425 )?,
426 timestamp: stats_millis(&point),
427 })
428 }
429
430 pub fn long_short_ratio(&self, symbol: &Symbol) -> Result<LongShortRatio> {
441 if !self.is_futures() {
442 return Err(Error::unsupported_field(
443 "Gate.io",
444 "long/short positioning on a spot client",
445 "positioning is a futures figure",
446 ));
447 }
448 let point = self.contract_stats(symbol)?;
449 let ratio = point
450 .get("lsr_account")
451 .and_then(serde_json::Value::as_f64)
452 .and_then(|r| parse_decimal(&r.to_string()).ok())
453 .ok_or_else(|| Error::Deserialization("no lsr_account".to_string()))?;
454 let total = Decimal::ONE + ratio;
455 if total.is_zero() {
456 return Err(Error::Deserialization(
457 "a long/short ratio of -1 has no proportions".to_string(),
458 ));
459 }
460 Ok(LongShortRatio {
461 symbol: symbol.clone(),
462 long_size: ratio / total,
463 short_size: Decimal::ONE / total,
464 timestamp: stats_millis(&point),
465 })
466 }
467
468 fn contract_stats(&self, symbol: &Symbol) -> Result<serde_json::Value> {
470 let query = format!("contract={}&limit=1", Self::wire_symbol(symbol));
471 let value = self.get("/futures/usdt/contract_stats", &query)?;
472 value
473 .as_array()
474 .and_then(|rows| rows.first())
475 .cloned()
476 .ok_or_else(|| Error::NotFound("no contract-stats data point".to_string()))
477 }
478
479 pub fn subscribe_trades(&mut self, symbol: &Symbol) -> Result<()> {
484 self.subscribe(symbol, "spot.trades")
485 }
486
487 pub fn subscribe_book(&mut self, symbol: &Symbol) -> Result<()> {
492 self.subscribe(symbol, "spot.order_book_update")
493 }
494
495 pub fn subscribe_ticker(&mut self, symbol: &Symbol) -> Result<()> {
500 self.subscribe(symbol, "spot.tickers")
501 }
502
503 fn subscribe(&mut self, symbol: &Symbol, channel: &str) -> Result<()> {
504 self.ensure_market_is_routed()?;
505 let futures = self.is_futures();
510 let channel = if futures {
511 &channel.replace("spot.", "futures.")
512 } else {
513 channel
514 };
515 let wire = Self::wire_symbol(symbol);
516 if self.connection.is_none() {
517 let ws = self.ws.as_ref().ok_or(Error::NotConnected)?;
518 let url = if futures {
519 FUTURES_WS_URL
520 } else {
521 "wss://api.gateio.ws/ws/v4/"
522 };
523 let connection = ws.connect(url)?;
524 self.connection = Some(connection);
525 }
526 let time = self.signed_now_ms() / 1000;
527 let payload = if channel == "futures.order_book_update" {
530 format!(r#""{wire}","100ms","20""#)
531 } else {
532 format!(r#""{wire}""#)
533 };
534 let message = format!(
535 r#"{{"time":{time},"channel":"{channel}","event":"subscribe","payload":[{payload}]}}"#
536 );
537 self.connection
538 .as_mut()
539 .expect("connection just ensured")
540 .send(&message)?;
541 if !self.sub_messages.contains(&message) {
542 self.sub_messages.push(message.clone());
543 }
544 if !self.subscriptions.iter().any(|(w, _)| w == &wire) {
545 self.subscriptions.push((wire, symbol.clone()));
546 }
547 Ok(())
548 }
549
550 pub fn poll_events(&mut self) -> Vec<Event> {
552 let subscriptions: HashMap<String, Symbol> = self.subscriptions.iter().cloned().collect();
553 let resolve = |wire: &str| {
554 subscriptions
555 .get(wire)
556 .cloned()
557 .unwrap_or_else(|| Symbol::new(wire, ""))
558 };
559 let channels = self.derivatives_channels.clone();
560 let subscribed = |wire: &str, channel: DerivativesChannel| {
561 channels.iter().any(|(w, c)| w == wire && *c == channel)
562 };
563 let mut events = Vec::new();
564 if let Some(connection) = self.connection.as_mut() {
565 while let Ok(Some(frame)) = connection.recv() {
566 if let Ok(mut parsed) = parse_ws_message(&frame, &resolve, &subscribed) {
567 events.append(&mut parsed);
568 }
569 }
570 }
571 if let Some(connection) = self.private_connection.as_mut() {
573 while let Ok(Some(frame)) = connection.recv() {
574 if let Ok(mut parsed) = parse_ws_message(&frame, &resolve, &subscribed) {
575 events.append(&mut parsed);
576 }
577 }
578 }
579 if self.user_data_active
582 && self
583 .private_connection
584 .as_ref()
585 .is_some_and(|c| !c.is_connected())
586 {
587 events.push(Event::Disconnected);
588 self.private_connection = None;
589 if self.subscribe_user_data().is_ok() {
590 events.push(Event::Reconnected);
591 }
592 }
593 let url = "wss://api.gateio.ws/ws/v4/";
594 crate::wsutil::reconnect_if_dropped(
595 self.ws.as_deref(),
596 url,
597 &mut self.connection,
598 &self.sub_messages,
599 &mut events,
600 );
601 events
602 }
603
604 pub fn subscribe_user_data(&mut self) -> Result<()> {
621 self.ensure_market_is_routed()?;
622 let futures = self.is_futures();
630 let user_id = if futures {
631 Some(self.futures_user_id()?)
632 } else {
633 None
634 };
635 let creds = self.credentials.as_ref().ok_or(Error::InvalidCredentials(
636 "user-data stream requires credentials",
637 ))?;
638 let time = self.signed_now_ms() / 1000;
639 let url = if futures {
642 FUTURES_WS_URL
643 } else {
644 "wss://api.gateio.ws/ws/v4/"
645 };
646 let ws = self.ws.as_ref().ok_or(Error::NotConnected)?;
647 let mut connection = ws.connect(url)?;
648 let futures_orders = user_id
652 .as_ref()
653 .map(|id| format!(r#"["{id}","!all"]"#))
654 .unwrap_or_default();
655 let futures_balances = user_id
656 .as_ref()
657 .map(|id| format!(r#"["{id}"]"#))
658 .unwrap_or_default();
659 let channels: [(&str, &str); 2] = if futures {
660 [
661 ("futures.orders", futures_orders.as_str()),
662 ("futures.balances", futures_balances.as_str()),
663 ]
664 } else {
665 [("spot.orders", r#"["!all"]"#), ("spot.balances", "[]")]
666 };
667 for (channel, payload) in channels {
668 let sign_string = format!("channel={channel}&event=subscribe&time={time}");
669 let sign = hmac_sha512_hex(creds.api_secret.as_bytes(), sign_string.as_bytes());
670 let message = format!(
671 r#"{{"time":{time},"channel":"{channel}","event":"subscribe","payload":{payload},"auth":{{"method":"api_key","KEY":"{}","SIGN":"{sign}"}}}}"#,
672 creds.api_key
673 );
674 connection.send(&message)?;
675 }
676 self.private_connection = Some(connection);
677 self.user_data_active = true;
678 Ok(())
679 }
680
681 pub fn keepalive_user_data(&mut self) -> Result<()> {
688 if let Some(connection) = self.private_connection.as_mut() {
689 connection.send("ping")?;
690 }
691 Ok(())
692 }
693
694 pub fn place_order_ws(&mut self, request: &OrderRequest) -> Result<Order> {
704 if request.order_type.is_trigger() {
705 return Err(Error::unsupported_trigger("Gate.io"));
706 }
707 self.ensure_reduce_only_is_reducible(request)?;
708 request.validate()?;
709 if self.is_futures() {
710 return Err(Error::Exchange {
711 code: "unsupported".to_string(),
712 message: "Gate's WebSocket order API is wired for spot here; place \
713 futures orders over REST (/api/v4/futures/usdt/orders)"
714 .to_string(),
715 });
716 }
717 let mut req_param = serde_json::json!({
718 "currency_pair": Self::wire_symbol(&request.symbol),
719 "type": order_type_str(request.order_type),
720 "account": "spot",
721 "side": side_str(request.side),
722 "amount": format_decimal(request.quantity),
723 });
724 if let Some(price) = request.price {
725 req_param["price"] = serde_json::json!(format_decimal(price));
726 }
727 if let Some(tif) = tif_str(request)? {
728 req_param["time_in_force"] = serde_json::json!(tif);
729 }
730 if let Some(id) = &request.client_order_id {
731 let text = if id.starts_with("t-") {
732 id.clone()
733 } else {
734 format!("t-{id}")
735 };
736 req_param["text"] = serde_json::json!(text);
737 }
738 if let Some(act) = stp_act_str(request.stp) {
739 req_param["stp_act"] = serde_json::json!(act);
740 }
741 let result = self.ws_api_request("spot.order_place", &req_param)?;
742 let raw: RawOrder =
743 serde_json::from_value(result).map_err(|e| Error::Deserialization(e.to_string()))?;
744 order_from_raw(request.symbol.clone(), &raw)
745 }
746
747 pub fn cancel_order_ws(&mut self, symbol: &Symbol, order_id: &str) -> Result<()> {
753 if self.is_futures() {
754 return Err(Error::Exchange {
755 code: "unsupported".to_string(),
756 message: "Gate's WebSocket order API is wired for spot here; cancel \
757 futures orders over REST"
758 .to_string(),
759 });
760 }
761 let req_param = serde_json::json!({
762 "order_id": order_id,
763 "currency_pair": Self::wire_symbol(symbol),
764 });
765 self.ws_api_request("spot.order_cancel", &req_param)?;
766 Ok(())
767 }
768
769 fn ensure_ws_api(&mut self) -> Result<()> {
773 self.ensure_market_is_routed()?;
774 if self.ws_api_connection.is_some() {
775 return Ok(());
776 }
777 let creds = self.credentials.as_ref().ok_or(Error::InvalidCredentials(
778 "WebSocket order API requires credentials",
779 ))?;
780 let timestamp = self.signed_now_ms() / 1000;
781 let signature = hmac_sha512_hex(
782 creds.api_secret.as_bytes(),
783 format!("api\nspot.login\n\n{timestamp}").as_bytes(),
784 );
785 let req_id = format!("wkex-login-{}", (self.now_ms)());
786 let login = format!(
787 r#"{{"time":{timestamp},"channel":"spot.login","event":"api","payload":{{"req_id":"{req_id}","api_key":"{}","signature":"{signature}","timestamp":"{timestamp}"}}}}"#,
788 creds.api_key
789 );
790 let ws = self.ws.as_ref().ok_or(Error::NotConnected)?;
791 let mut connection = ws.connect("wss://api.gateio.ws/ws/v4/")?;
792 connection.send(&login)?;
793 connection.recv()?; self.ws_api_connection = Some(connection);
795 Ok(())
796 }
797
798 fn ws_api_request(
801 &mut self,
802 channel: &str,
803 req_param: &serde_json::Value,
804 ) -> Result<serde_json::Value> {
805 self.ensure_ws_api()?;
806 let time = self.signed_now_ms() / 1000;
807 let req_id = format!("wkex-{}", (self.now_ms)());
808 let frame = serde_json::json!({
809 "time": time,
810 "channel": channel,
811 "event": "api",
812 "payload": { "req_id": req_id, "req_param": req_param },
813 })
814 .to_string();
815 let connection = self
816 .ws_api_connection
817 .as_mut()
818 .expect("ws order connection just ensured");
819 connection.send(&frame)?;
820 let Some(response) = connection.recv()? else {
821 return Err(Error::NotConnected);
822 };
823 let value: serde_json::Value =
824 serde_json::from_str(&response).map_err(|e| Error::Deserialization(e.to_string()))?;
825 let status = value
826 .get("header")
827 .and_then(|header| header.get("status"))
828 .and_then(serde_json::Value::as_str)
829 .unwrap_or("");
830 if status == "200" {
831 Ok(value
832 .get("data")
833 .and_then(|data| data.get("result"))
834 .cloned()
835 .unwrap_or(serde_json::Value::Null))
836 } else {
837 let errs = value.get("data").and_then(|data| data.get("errs"));
838 let label = errs
839 .and_then(|e| e.get("label"))
840 .and_then(serde_json::Value::as_str)
841 .unwrap_or("ws");
842 let message = errs
843 .and_then(|e| e.get("message"))
844 .and_then(serde_json::Value::as_str)
845 .unwrap_or("order rejected")
846 .to_string();
847 Err(Error::OrderRejected {
848 code: label.to_string(),
849 message,
850 })
851 }
852 }
853
854 fn ensure_reduce_only_is_reducible(&self, request: &OrderRequest) -> Result<()> {
866 if request.reduce_only && !self.is_futures() {
867 return Err(Error::unsupported_field(
868 "Gate.io",
869 "reduce_only on a spot order",
870 "spot holds balances, not positions, and has none to reduce",
871 ));
872 }
873 Ok(())
874 }
875
876 fn place_price_order(&self, request: &OrderRequest) -> Result<Order> {
896 let stop = request
897 .stop_price
898 .ok_or(Error::InvalidOrder("a trigger order requires a stop price"))?;
899 let rule = match request.side {
902 OrderSide::Sell => 2,
903 OrderSide::Buy => 1,
904 };
905 let (path, body) = if self.is_futures() {
906 let size = decimal_to_contracts(request.quantity)?;
907 let size = match request.side {
908 OrderSide::Buy => size,
909 OrderSide::Sell => -size,
910 };
911 (
912 "/api/v4/futures/usdt/price_orders",
913 serde_json::json!({
914 "initial": {
915 "contract": Self::wire_symbol(&request.symbol),
916 "size": size,
917 "price": request.price.map_or_else(
920 || "0".to_string(),
921 format_decimal,
922 ),
923 },
924 "trigger": {
925 "strategy_type": 0,
926 "price_type": 0,
928 "price": format_decimal(stop),
929 "rule": rule,
930 },
931 }),
932 )
933 } else {
934 let Some(price) = request.price else {
935 return Err(Error::unsupported_field(
936 "Gate.io",
937 "a spot stop-market order",
938 "Gate's spot price order can only place a limit, and choosing \
939 one here would decide how much slippage the caller's stop may take",
940 ));
941 };
942 (
943 "/api/v4/spot/price_orders",
944 serde_json::json!({
945 "market": Self::wire_symbol(&request.symbol),
946 "trigger": {
947 "price": format_decimal(stop),
948 "rule": if rule == 2 { "<=" } else { ">=" },
949 "expiration": 0,
951 },
952 "put": {
953 "type": "limit",
954 "side": side_str(request.side),
955 "price": format_decimal(price),
956 "amount": format_decimal(request.quantity),
957 "account": "normal",
958 "time_in_force": "gtc",
959 },
960 }),
961 )
962 };
963 let value = self.signed_request(HttpMethod::Post, path, "", &body.to_string())?;
964 Ok(Order {
965 id: value
966 .get("id")
967 .map(|id| match id.as_str() {
968 Some(text) => text.to_string(),
969 None => id.to_string(),
970 })
971 .unwrap_or_default(),
972 client_order_id: request.client_order_id.clone(),
973 symbol: request.symbol.clone(),
974 side: request.side,
975 order_type: request.order_type,
976 status: OrderStatus::New,
977 quantity: request.quantity,
978 filled_quantity: Decimal::ZERO,
979 price: request.price,
980 average_price: None,
981 })
982 }
983
984 pub fn place_order(&self, request: &OrderRequest) -> Result<Order> {
985 self.ensure_reduce_only_is_reducible(request)?;
986 request.validate()?;
987 if request.order_type.is_trigger() {
989 return self.place_price_order(request);
990 }
991 if self.is_futures() {
992 return self.place_futures_order(request);
993 }
994 let mut body = serde_json::json!({
995 "currency_pair": Self::wire_symbol(&request.symbol),
996 "side": side_str(request.side),
997 "type": order_type_str(request.order_type),
998 "amount": format_decimal(request.quantity),
999 });
1000 if let Some(price) = request.price {
1001 body["price"] = serde_json::json!(format_decimal(price));
1002 }
1003 if let Some(tif) = tif_str(request)? {
1004 body["time_in_force"] = serde_json::json!(tif);
1005 }
1006 if let Some(id) = &request.client_order_id {
1007 let text = if id.starts_with("t-") {
1008 id.clone()
1009 } else {
1010 format!("t-{id}")
1011 };
1012 body["text"] = serde_json::json!(text);
1013 }
1014 if let Some(act) = stp_act_str(request.stp) {
1015 body["stp_act"] = serde_json::json!(act);
1016 }
1017 let value = self.signed_request(
1018 HttpMethod::Post,
1019 "/api/v4/spot/orders",
1020 "",
1021 &body.to_string(),
1022 )?;
1023 let raw: RawOrder = parse_json(value)?;
1024 order_from_raw(request.symbol.clone(), &raw)
1025 }
1026
1027 pub fn cancel_order(&self, symbol: &Symbol, order_id: &str) -> Result<()> {
1032 if self.is_futures() {
1033 let path = format!("/api/v4/futures/usdt/orders/{order_id}");
1034 self.signed_request(HttpMethod::Delete, &path, "", "")?;
1035 return Ok(());
1036 }
1037 let path = format!("/api/v4/spot/orders/{order_id}");
1038 let query = format!("currency_pair={}", Self::wire_symbol(symbol));
1039 self.signed_request(HttpMethod::Delete, &path, &query, "")?;
1040 Ok(())
1041 }
1042
1043 pub fn query_order(&self, symbol: &Symbol, order_id: &str) -> Result<Order> {
1048 if self.is_futures() {
1049 let path = format!("/api/v4/futures/usdt/orders/{order_id}");
1050 let value = self.signed_request(HttpMethod::Get, &path, "", "")?;
1051 let raw: RawFuturesOrder = parse_json(value)?;
1052 return futures_order_from_raw(symbol.clone(), &raw);
1053 }
1054 let path = format!("/api/v4/spot/orders/{order_id}");
1055 let query = format!("currency_pair={}", Self::wire_symbol(symbol));
1056 let value = self.signed_request(HttpMethod::Get, &path, &query, "")?;
1057 let raw: RawOrder = parse_json(value)?;
1058 order_from_raw(symbol.clone(), &raw)
1059 }
1060
1061 pub fn open_orders(&self, symbol: Option<&Symbol>) -> Result<Vec<Order>> {
1067 let sym = symbol.ok_or(Error::InvalidOrder("Gate open_orders requires a symbol"))?;
1068 if self.is_futures() {
1069 let query = format!("contract={}&status=open", Self::wire_symbol(sym));
1070 let value =
1071 self.signed_request(HttpMethod::Get, "/api/v4/futures/usdt/orders", &query, "")?;
1072 let list: Vec<RawFuturesOrder> = parse_json(value)?;
1073 return list
1074 .iter()
1075 .map(|raw| futures_order_from_raw(sym.clone(), raw))
1076 .collect();
1077 }
1078 let query = format!("currency_pair={}&status=open", Self::wire_symbol(sym));
1079 let value = self.signed_request(HttpMethod::Get, "/api/v4/spot/orders", &query, "")?;
1080 let list: Vec<RawOrder> = parse_json(value)?;
1081 list.iter()
1082 .map(|raw| order_from_raw(sym.clone(), raw))
1083 .collect()
1084 }
1085
1086 fn futures_user_id(&self) -> Result<String> {
1095 if let Some(id) = self.futures_user_id.borrow().clone() {
1096 return Ok(id);
1097 }
1098 let value =
1099 self.signed_request(HttpMethod::Get, "/api/v4/futures/usdt/accounts", "", "")?;
1100 let id = value
1101 .get("user")
1102 .map(|user| {
1103 user.as_str()
1104 .map_or_else(|| user.to_string(), str::to_string)
1105 })
1106 .ok_or_else(|| Error::Deserialization("futures account has no user id".to_string()))?;
1107 *self.futures_user_id.borrow_mut() = Some(id.clone());
1108 Ok(id)
1109 }
1110
1111 pub fn balances(&self) -> Result<Vec<Balance>> {
1116 if self.is_futures() {
1117 let value =
1118 self.signed_request(HttpMethod::Get, "/api/v4/futures/usdt/accounts", "", "")?;
1119 let acct: RawFuturesAccount = parse_json(value)?;
1120 let total = dec_or_zero(&acct.total);
1121 let available = dec_or_zero(&acct.available);
1122 return Ok(vec![Balance {
1123 asset: acct.currency,
1124 free: available,
1125 locked: (total - available).max(Decimal::ZERO),
1126 }]);
1127 }
1128 let value = self.signed_request(HttpMethod::Get, "/api/v4/spot/accounts", "", "")?;
1129 let list: Vec<RawAccount> = parse_json(value)?;
1130 Ok(list
1131 .iter()
1132 .map(|a| Balance {
1133 asset: a.currency.clone(),
1134 free: dec_or_zero(&a.available),
1135 locked: dec_or_zero(&a.locked),
1136 })
1137 .collect())
1138 }
1139
1140 fn place_futures_order(&self, request: &OrderRequest) -> Result<Order> {
1144 let contract = Self::wire_symbol(&request.symbol);
1145 let magnitude = decimal_to_contracts(request.quantity)?;
1146 let size = match request.side {
1147 OrderSide::Buy => magnitude,
1148 OrderSide::Sell => -magnitude,
1149 };
1150 let mut body = if self.position_mode == PositionMode::Hedge && request.reduce_only {
1151 let auto_size = match PositionSide::for_order(request.side, true) {
1156 PositionSide::Long => "close_long",
1157 PositionSide::Short => "close_short",
1158 };
1159 serde_json::json!({
1160 "contract": contract,
1161 "size": 0,
1162 "auto_size": auto_size,
1163 "reduce_only": true,
1164 })
1165 } else {
1166 serde_json::json!({
1169 "contract": contract,
1170 "size": size,
1171 "reduce_only": request.reduce_only,
1172 })
1173 };
1174 match request.order_type {
1175 OrderType::Market | OrderType::StopMarket => {
1176 if request.time_in_force == TimeInForce::Fok {
1180 return Err(Error::unsupported_field(
1181 "Gate.io",
1182 "a FOK time-in-force on a futures market order",
1183 "the futures market order is a zero-price IOC",
1184 ));
1185 }
1186 body["price"] = serde_json::json!("0");
1187 body["tif"] = serde_json::json!("ioc");
1188 }
1189 OrderType::Limit | OrderType::StopLimit => {
1190 let price = request
1191 .price
1192 .ok_or(Error::InvalidOrder("limit order requires a price"))?;
1193 body["price"] = serde_json::json!(format_decimal(price));
1194 if let Some(tif) = tif_str(request)? {
1195 body["tif"] = serde_json::json!(tif);
1196 }
1197 }
1198 }
1199 if let Some(id) = &request.client_order_id {
1200 let text = if id.starts_with("t-") {
1201 id.clone()
1202 } else {
1203 format!("t-{id}")
1204 };
1205 body["text"] = serde_json::json!(text);
1206 }
1207 let value = self.signed_request(
1208 HttpMethod::Post,
1209 "/api/v4/futures/usdt/orders",
1210 "",
1211 &body.to_string(),
1212 )?;
1213 let raw: RawFuturesOrder = parse_json(value)?;
1214 futures_order_from_raw(request.symbol.clone(), &raw)
1215 }
1216
1217 pub fn positions(&self, symbol: Option<&Symbol>) -> Result<Vec<Position>> {
1223 let value =
1224 self.signed_request(HttpMethod::Get, "/api/v4/futures/usdt/positions", "", "")?;
1225 let list: Vec<RawFuturesPosition> = parse_json(value)?;
1226 let wanted = symbol.map(Self::wire_symbol);
1227 Ok(list
1228 .iter()
1229 .filter(|p| p.size != 0)
1230 .filter(|p| wanted.as_ref().is_none_or(|w| &p.contract == w))
1231 .map(parse_futures_position)
1232 .collect())
1233 }
1234
1235 pub fn set_leverage(&mut self, symbol: &Symbol, leverage: u32) -> Result<()> {
1240 self.leverage = leverage.max(1);
1241 self.apply_leverage(symbol, leverage)
1242 }
1243
1244 pub fn set_margin_mode(&mut self, symbol: &Symbol, mode: MarginMode) -> Result<()> {
1250 let leverage = match mode {
1251 MarginMode::Cross => 0,
1252 MarginMode::Isolated => self.leverage.max(1),
1253 };
1254 self.apply_leverage(symbol, leverage)
1255 }
1256
1257 fn apply_leverage(&self, symbol: &Symbol, leverage: u32) -> Result<()> {
1258 let path = format!(
1259 "/api/v4/futures/usdt/positions/{}/leverage",
1260 Self::wire_symbol(symbol)
1261 );
1262 let query = format!("leverage={leverage}");
1263 self.signed_request(HttpMethod::Post, &path, &query, "")?;
1264 Ok(())
1265 }
1266
1267 pub fn close_position(&self, symbol: &Symbol) -> Result<Order> {
1273 let position = self
1274 .positions(Some(symbol))?
1275 .into_iter()
1276 .find(|p| &p.symbol == symbol)
1277 .ok_or_else(|| Error::NotFound(format!("no open position for {symbol}")))?;
1278 let request = match position.side {
1279 PositionSide::Long => OrderRequest::market_sell(symbol.clone(), position.quantity),
1280 PositionSide::Short => OrderRequest::market_buy(symbol.clone(), position.quantity),
1281 }
1282 .reduce_only();
1283 self.place_order(&request)
1284 }
1285
1286 fn get(&self, path: &str, query: &str) -> Result<serde_json::Value> {
1287 self.ensure_market_is_routed()?;
1288 let url = format!("{}{path}?{query}", self.rest_base);
1289 let response = self.http.execute(&HttpRequest::get(url))?;
1290 parse_body(&response)
1291 }
1292
1293 fn signed_request(
1296 &self,
1297 method: HttpMethod,
1298 path: &str,
1299 query: &str,
1300 body: &str,
1301 ) -> Result<serde_json::Value> {
1302 self.ensure_market_is_routed()?;
1303 let creds = self.credentials.as_ref().ok_or(Error::InvalidCredentials(
1304 "signed endpoint requires credentials",
1305 ))?;
1306 let timestamp = (self.signed_now_ms() / 1000).to_string();
1307 let body_hash = sha512_hex(body.as_bytes());
1308 let sign_string = format!(
1309 "{}\n{path}\n{query}\n{body_hash}\n{timestamp}",
1310 method.as_str()
1311 );
1312 let signature = hmac_sha512_hex(creds.api_secret.as_bytes(), sign_string.as_bytes());
1313 let url = if query.is_empty() {
1314 format!("{}{path}", self.rest_base)
1315 } else {
1316 format!("{}{path}?{query}", self.rest_base)
1317 };
1318 let mut request = HttpRequest::new(method, url)
1319 .with_header("KEY", creds.api_key.clone())
1320 .with_header("SIGN", signature)
1321 .with_header("Timestamp", timestamp);
1322 if !body.is_empty() {
1323 request = request
1324 .with_header("Content-Type", "application/json")
1325 .with_body(body.to_string());
1326 }
1327 let response = self.http.execute(&request)?;
1328 parse_body(&response)
1329 }
1330}
1331
1332fn parse_body(response: &HttpResponse) -> Result<serde_json::Value> {
1334 if response.is_success() {
1335 serde_json::from_str(&response.body).map_err(|e| Error::Deserialization(e.to_string()))
1336 } else {
1337 let err: GateError = serde_json::from_str(&response.body).unwrap_or(GateError {
1338 label: response.status.to_string(),
1339 message: response.body.clone(),
1340 });
1341 Err(map_error(&err.label, &err.message).with_retry_after(response.retry_after()))
1342 }
1343}
1344
1345fn parse_json<T: for<'de> Deserialize<'de>>(value: serde_json::Value) -> Result<T> {
1346 serde_json::from_value(value).map_err(|e| Error::Deserialization(e.to_string()))
1347}
1348
1349fn map_interval(interval: &str) -> String {
1350 match interval {
1351 "1w" => "7d",
1352 other => other,
1353 }
1354 .to_string()
1355}
1356
1357fn map_error(label: &str, message: &str) -> Error {
1358 match label {
1359 "TOO_MANY_REQUESTS" | "RATE_LIMIT" => Error::RateLimited { retry_after: None },
1360 "INVALID_KEY" | "INVALID_SIGNATURE" | "SIGN_MISMATCH" | "AUTHENTICATION_FAILED" => {
1361 Error::Auth(message.to_string())
1362 }
1363 "BALANCE_NOT_ENOUGH" | "INSUFFICIENT_AVAILABLE" => Error::InsufficientBalance,
1364 "ORDER_NOT_FOUND" => Error::NotFound(message.to_string()),
1365 "INVALID_CURRENCY_PAIR" => Error::InvalidSymbol(message.to_string()),
1366 other => Error::Exchange {
1367 code: other.to_string(),
1368 message: message.to_string(),
1369 },
1370 }
1371}
1372
1373fn side_str(side: OrderSide) -> &'static str {
1374 match side {
1375 OrderSide::Buy => "buy",
1376 OrderSide::Sell => "sell",
1377 }
1378}
1379
1380fn stp_act_str(stp: SelfTradePrevention) -> Option<&'static str> {
1383 match stp {
1384 SelfTradePrevention::None => None,
1385 SelfTradePrevention::ExpireMaker => Some("co"),
1386 SelfTradePrevention::ExpireTaker => Some("cn"),
1387 SelfTradePrevention::ExpireBoth => Some("cb"),
1388 }
1389}
1390
1391fn tif_str(request: &OrderRequest) -> Result<Option<&'static str>> {
1400 match (request.post_only, request.time_in_force) {
1401 (true, TimeInForce::Gtc) => Ok(Some("poc")),
1402 (true, _) => Err(Error::unsupported_field(
1403 "Gate.io",
1404 "post_only together with a non-GTC time-in-force",
1405 "`poc` is Gate's post-only and is itself a time-in-force",
1406 )),
1407 (false, TimeInForce::Gtc) => Ok(None),
1408 (false, TimeInForce::Ioc) => Ok(Some("ioc")),
1409 (false, TimeInForce::Fok) => Ok(Some("fok")),
1410 }
1411}
1412
1413fn order_type_str(order_type: OrderType) -> &'static str {
1414 match order_type {
1415 OrderType::Market | OrderType::StopMarket => "market",
1416 OrderType::Limit | OrderType::StopLimit => "limit",
1417 }
1418}
1419
1420fn parse_side(raw: &str) -> Result<OrderSide> {
1421 match raw {
1422 "buy" => Ok(OrderSide::Buy),
1423 "sell" => Ok(OrderSide::Sell),
1424 other => Err(Error::Deserialization(format!("unknown side {other:?}"))),
1425 }
1426}
1427
1428fn symbol_from_wire(wire: &str) -> Symbol {
1430 match wire.split_once('_') {
1431 Some((base, quote)) if !base.is_empty() && !quote.is_empty() => Symbol::new(base, quote),
1432 _ => Symbol::new(wire, ""),
1433 }
1434}
1435
1436fn parse_order_type(raw: &str) -> Result<OrderType> {
1437 match raw {
1438 "market" => Ok(OrderType::Market),
1439 "limit" => Ok(OrderType::Limit),
1440 other => Err(Error::Deserialization(format!(
1441 "unknown order type {other:?}"
1442 ))),
1443 }
1444}
1445
1446fn dec_or_zero(raw: &str) -> Decimal {
1447 crate::normalize::parse_opt_decimal(Some(raw))
1448 .ok()
1449 .flatten()
1450 .unwrap_or(Decimal::ZERO)
1451}
1452
1453fn nonzero_decimal(raw: &str) -> Option<Decimal> {
1454 crate::normalize::parse_opt_decimal(Some(raw))
1455 .ok()
1456 .flatten()
1457 .filter(|d| *d > Decimal::ZERO)
1458}
1459
1460fn parse_levels(levels: &[[String; 2]]) -> Result<Vec<BookLevel>> {
1461 levels
1462 .iter()
1463 .map(|[price, qty]| {
1464 Ok(BookLevel {
1465 price: parse_decimal(price)?,
1466 quantity: parse_decimal(qty)?,
1467 })
1468 })
1469 .collect()
1470}
1471
1472fn parse_kline_row(row: &[String]) -> Result<Candle> {
1473 if row.len() < 7 {
1475 return Err(Error::Deserialization("kline row too short".to_string()));
1476 }
1477 let ts = row[0]
1478 .parse::<i64>()
1479 .map_err(|e| Error::Deserialization(format!("kline ts not an integer: {e}")))?;
1480 let f = |i: usize| -> Result<f64> {
1481 row[i]
1482 .parse::<f64>()
1483 .map_err(|e| Error::Deserialization(format!("kline field not a number: {e}")))
1484 };
1485 Candle::new(f(5)?, f(3)?, f(4)?, f(2)?, f(6)?, ts)
1487 .map_err(|e| Error::Deserialization(e.to_string()))
1488}
1489
1490fn order_from_raw(symbol: Symbol, raw: &RawOrder) -> Result<Order> {
1491 let filled = if raw.filled_amount.is_empty() && !raw.left.is_empty() {
1494 (dec_or_zero(&raw.amount) - dec_or_zero(&raw.left)).max(Decimal::ZERO)
1495 } else {
1496 dec_or_zero(&raw.filled_amount)
1497 };
1498 let status = match raw.status.as_str() {
1499 "cancelled" => OrderStatus::Canceled,
1500 "closed" => OrderStatus::Filled,
1501 "open" => {
1502 if filled > Decimal::ZERO {
1503 OrderStatus::PartiallyFilled
1504 } else {
1505 OrderStatus::New
1506 }
1507 }
1508 other => return Err(Error::Deserialization(format!("unknown status {other:?}"))),
1509 };
1510 Ok(Order {
1511 id: raw.id.clone(),
1512 client_order_id: (!raw.text.is_empty()).then(|| raw.text.clone()),
1513 symbol,
1514 side: parse_side(&raw.side)?,
1515 order_type: parse_order_type(&raw.order_type)?,
1516 status,
1517 quantity: parse_decimal(&raw.amount)?,
1518 filled_quantity: filled,
1519 price: nonzero_decimal(&raw.price),
1520 average_price: nonzero_decimal(&raw.avg_deal_price),
1521 })
1522}
1523
1524fn field_str<'a>(value: &'a serde_json::Value, key: &str) -> Result<&'a str> {
1525 value
1526 .get(key)
1527 .and_then(serde_json::Value::as_str)
1528 .ok_or_else(|| Error::Deserialization(format!("missing string field {key:?}")))
1529}
1530
1531fn opt_str<'a>(value: &'a serde_json::Value, key: &str) -> &'a str {
1532 value
1533 .get(key)
1534 .and_then(serde_json::Value::as_str)
1535 .unwrap_or("")
1536}
1537
1538fn opt_u64(value: &serde_json::Value, key: &str) -> u64 {
1539 value
1540 .get(key)
1541 .and_then(|v| {
1542 v.as_u64()
1543 .or_else(|| v.as_str().and_then(|s| s.parse().ok()))
1544 })
1545 .unwrap_or(0)
1546}
1547
1548fn opt_i64(value: &serde_json::Value, key: &str) -> i64 {
1549 value
1550 .get(key)
1551 .and_then(|v| {
1552 v.as_i64()
1553 .or_else(|| v.as_str().and_then(|s| s.parse().ok()))
1554 })
1555 .unwrap_or(0)
1556}
1557
1558fn parse_ws_levels(value: Option<&serde_json::Value>) -> Result<Vec<BookLevel>> {
1559 let array = value
1560 .and_then(serde_json::Value::as_array)
1561 .ok_or_else(|| Error::Deserialization("missing depth levels".to_string()))?;
1562 array
1563 .iter()
1564 .map(|level| {
1565 let pair = level
1566 .as_array()
1567 .ok_or_else(|| Error::Deserialization("depth level not an array".to_string()))?;
1568 let price = parse_decimal(
1569 pair.first()
1570 .and_then(serde_json::Value::as_str)
1571 .ok_or_else(|| Error::Deserialization("depth price missing".to_string()))?,
1572 )?;
1573 let quantity =
1574 parse_decimal(pair.get(1).and_then(serde_json::Value::as_str).ok_or_else(
1575 || Error::Deserialization("depth quantity missing".to_string()),
1576 )?)?;
1577 Ok(BookLevel { price, quantity })
1578 })
1579 .collect()
1580}
1581
1582fn stats_millis(point: &serde_json::Value) -> i64 {
1588 point
1589 .get("time")
1590 .and_then(serde_json::Value::as_i64)
1591 .unwrap_or(0)
1592 * 1000
1593}
1594
1595const FUTURES_WS_URL: &str = "wss://fx-ws.gateio.ws/v4/ws/usdt";
1597
1598fn futures_signed_size(value: &serde_json::Value, key: &str) -> Option<(Decimal, OrderSide)> {
1606 let raw = value.get(key)?;
1607 let text = raw
1608 .as_str()
1609 .map(ToString::to_string)
1610 .or_else(|| raw.as_i64().map(|n| n.to_string()))?;
1611 let size = parse_decimal(&text).ok()?;
1612 let side = if size.is_sign_negative() {
1613 OrderSide::Sell
1614 } else {
1615 OrderSide::Buy
1616 };
1617 Some((size.abs(), side))
1618}
1619
1620fn futures_levels(value: Option<&serde_json::Value>) -> Vec<BookLevel> {
1623 value
1624 .and_then(serde_json::Value::as_array)
1625 .map(|rows| {
1626 rows.iter()
1627 .filter_map(|row| {
1628 let size = row.get("s")?;
1629 let size = size
1630 .as_str()
1631 .map(ToString::to_string)
1632 .or_else(|| size.as_i64().map(|n| n.to_string()))?;
1633 Some(BookLevel {
1634 price: parse_decimal(row.get("p")?.as_str()?).ok()?,
1635 quantity: parse_decimal(&size).ok()?,
1636 })
1637 })
1638 .collect()
1639 })
1640 .unwrap_or_default()
1641}
1642
1643fn parse_ws_message(
1644 text: &str,
1645 resolve: &impl Fn(&str) -> Symbol,
1646 subscribed: &impl Fn(&str, DerivativesChannel) -> bool,
1647) -> Result<Vec<Event>> {
1648 let value: serde_json::Value =
1649 serde_json::from_str(text).map_err(|e| Error::Deserialization(e.to_string()))?;
1650 if value.get("event").and_then(serde_json::Value::as_str) != Some("update") {
1651 return Ok(Vec::new()); }
1653 let Some(channel) = value.get("channel").and_then(serde_json::Value::as_str) else {
1654 return Ok(Vec::new());
1655 };
1656 let null = serde_json::Value::Null;
1657 let result = value.get("result").unwrap_or(&null);
1658
1659 match channel {
1660 "futures.trades" => Ok(result
1663 .as_array()
1664 .map(|trades| {
1665 trades
1666 .iter()
1667 .filter_map(|trade| {
1668 let (quantity, aggressor) = futures_signed_size(trade, "size")?;
1669 Some(Event::Trade(TradePrint {
1670 symbol: resolve(trade.get("contract")?.as_str()?),
1671 price: parse_decimal(trade.get("price")?.as_str()?).ok()?,
1672 quantity,
1673 aggressor,
1674 timestamp: trade
1675 .get("create_time_ms")
1676 .and_then(serde_json::Value::as_i64)
1677 .unwrap_or(0),
1678 }))
1679 })
1680 .collect()
1681 })
1682 .unwrap_or_default()),
1683 "futures.order_book_update" => {
1684 let Some(contract) = result.get("s").and_then(serde_json::Value::as_str) else {
1685 return Ok(Vec::new());
1686 };
1687 Ok(vec![Event::BookDelta(BookDelta {
1688 symbol: resolve(contract),
1689 first_update_id: result
1690 .get("U")
1691 .and_then(serde_json::Value::as_u64)
1692 .unwrap_or(0),
1693 final_update_id: result
1694 .get("u")
1695 .and_then(serde_json::Value::as_u64)
1696 .unwrap_or(0),
1697 bids: futures_levels(result.get("b")),
1698 asks: futures_levels(result.get("a")),
1699 timestamp: result
1700 .get("t")
1701 .and_then(serde_json::Value::as_i64)
1702 .unwrap_or(0),
1703 })])
1704 }
1705 "futures.tickers" => {
1709 let empty = Vec::new();
1710 let mut out = Vec::new();
1711 for ticker in result.as_array().unwrap_or(&empty) {
1712 let Some(wire) = ticker.get("contract").and_then(serde_json::Value::as_str) else {
1713 continue;
1714 };
1715 let symbol = resolve(wire);
1716 let timestamp = ticker
1717 .get("t")
1718 .and_then(serde_json::Value::as_i64)
1719 .unwrap_or(0);
1720 let field = |key: &str| {
1721 ticker
1722 .get(key)
1723 .and_then(serde_json::Value::as_str)
1724 .and_then(|v| parse_decimal(v).ok())
1725 };
1726 if let Some(last) = field("last") {
1727 out.push(Event::Ticker(Ticker {
1728 symbol: symbol.clone(),
1729 last,
1730 bid: Decimal::ZERO,
1734 ask: Decimal::ZERO,
1735 volume: field("volume_24h_base").unwrap_or_default(),
1736 timestamp,
1737 }));
1738 }
1739 let mark = field("mark_price");
1740 if subscribed(wire, DerivativesChannel::Funding) {
1741 if let (Some(rate), Some(mark_price)) = (field("funding_rate"), mark) {
1742 out.push(Event::Derivatives(DerivativesFeed::Funding(FundingRate {
1743 symbol: symbol.clone(),
1744 rate,
1745 mark_price,
1746 timestamp,
1747 })));
1748 }
1749 }
1750 if subscribed(wire, DerivativesChannel::MarkIndex) {
1751 if let (Some(mark_price), Some(index_price)) = (mark, field("index_price")) {
1752 out.push(Event::Derivatives(DerivativesFeed::MarkIndex(MarkIndex {
1753 symbol,
1754 mark_price,
1755 index_price,
1756 timestamp,
1757 })));
1758 }
1759 }
1760 }
1761 Ok(out)
1762 }
1763 "spot.trades" => Ok(vec![Event::Trade(TradePrint {
1764 symbol: resolve(field_str(result, "currency_pair")?),
1765 price: parse_decimal(field_str(result, "price")?)?,
1766 quantity: parse_decimal(field_str(result, "amount")?)?,
1767 aggressor: parse_side(field_str(result, "side")?)?,
1768 timestamp: opt_i64(result, "create_time_ms"),
1769 })]),
1770 "spot.tickers" => Ok(vec![Event::Ticker(Ticker {
1771 symbol: resolve(field_str(result, "currency_pair")?),
1772 last: parse_decimal(field_str(result, "last")?)?,
1773 bid: dec_or_zero(opt_str(result, "highest_bid")),
1774 ask: dec_or_zero(opt_str(result, "lowest_ask")),
1775 volume: dec_or_zero(opt_str(result, "base_volume")),
1776 timestamp: 0,
1777 })]),
1778 "spot.order_book_update" => {
1779 let update_id = opt_u64(result, "u");
1780 Ok(vec![Event::BookDelta(BookDelta {
1781 symbol: resolve(opt_str(result, "s")),
1782 first_update_id: update_id,
1783 final_update_id: update_id,
1784 bids: parse_ws_levels(result.get("b"))?,
1785 asks: parse_ws_levels(result.get("a"))?,
1786 timestamp: opt_u64(result, "t").try_into().unwrap_or(0),
1787 })])
1788 }
1789 "spot.orders" => result
1792 .as_array()
1793 .ok_or_else(|| Error::Deserialization("spot.orders result not an array".to_string()))?
1794 .iter()
1795 .map(|order| {
1796 let symbol = symbol_from_wire(field_str(order, "currency_pair")?);
1799 let raw: RawOrder = serde_json::from_value(order.clone())
1800 .map_err(|e| Error::Deserialization(e.to_string()))?;
1801 Ok(Event::OrderUpdate(order_from_raw(symbol, &raw)?))
1802 })
1803 .collect(),
1804 "futures.orders" => result
1811 .as_array()
1812 .ok_or_else(|| {
1813 Error::Deserialization("futures.orders result not an array".to_string())
1814 })?
1815 .iter()
1816 .map(|order| Ok(Event::OrderUpdate(futures_order_from_frame(order)?)))
1817 .collect(),
1818 "futures.balances" => {
1822 let balances = result
1823 .as_array()
1824 .ok_or_else(|| {
1825 Error::Deserialization("futures.balances result not an array".to_string())
1826 })?
1827 .iter()
1828 .map(|entry| {
1829 Ok(Balance {
1830 asset: field_str(entry, "currency")?.to_string(),
1831 free: number_or_string(entry, "balance"),
1832 locked: Decimal::ZERO,
1833 })
1834 })
1835 .collect::<Result<Vec<_>>>()?;
1836 Ok(vec![Event::BalanceUpdate(balances)])
1837 }
1838 "spot.balances" => {
1841 let balances = result
1842 .as_array()
1843 .ok_or_else(|| {
1844 Error::Deserialization("spot.balances result not an array".to_string())
1845 })?
1846 .iter()
1847 .map(|entry| {
1848 let total = dec_or_zero(opt_str(entry, "total"));
1849 let available = dec_or_zero(opt_str(entry, "available"));
1850 Ok(Balance {
1851 asset: field_str(entry, "currency")?.to_string(),
1852 free: available,
1853 locked: (total - available).max(Decimal::ZERO),
1854 })
1855 })
1856 .collect::<Result<Vec<_>>>()?;
1857 Ok(vec![Event::BalanceUpdate(balances)])
1858 }
1859 _ => Ok(Vec::new()),
1860 }
1861}
1862
1863#[derive(Deserialize)]
1864struct GateError {
1865 #[serde(default)]
1866 label: String,
1867 #[serde(default)]
1868 message: String,
1869}
1870
1871#[derive(Deserialize)]
1872struct RawTicker {
1873 last: String,
1874 highest_bid: String,
1875 lowest_ask: String,
1876 base_volume: String,
1877}
1878
1879#[derive(Deserialize)]
1880struct RawDepth {
1881 #[serde(default)]
1882 update: u64,
1883 bids: Vec<[String; 2]>,
1884 asks: Vec<[String; 2]>,
1885}
1886
1887fn futures_order_from_frame(order: &serde_json::Value) -> Result<Order> {
1894 let size = order
1895 .get("size")
1896 .and_then(serde_json::Value::as_i64)
1897 .ok_or_else(|| Error::Deserialization("futures order has no size".to_string()))?;
1898 let side = if size < 0 {
1899 OrderSide::Sell
1900 } else {
1901 OrderSide::Buy
1902 };
1903 let quantity = Decimal::from(size.abs());
1904 let left = number_or_string(order, "left").abs();
1905 let price = nonzero_decimal(opt_str(order, "price"));
1906 let text = opt_str(order, "text");
1907 Ok(Order {
1908 id: order
1909 .get("id")
1910 .map(|id| id.as_str().map_or_else(|| id.to_string(), str::to_string))
1911 .ok_or_else(|| Error::Deserialization("futures order has no id".to_string()))?,
1912 client_order_id: text.starts_with("t-").then(|| text.to_string()),
1915 symbol: symbol_from_wire(field_str(order, "contract")?),
1916 side,
1917 order_type: if price.is_some() {
1919 OrderType::Limit
1920 } else {
1921 OrderType::Market
1922 },
1923 status: futures_order_status(
1924 opt_str(order, "status"),
1925 opt_str(order, "finish_as"),
1926 (quantity - left).max(Decimal::ZERO),
1927 ),
1928 quantity,
1929 filled_quantity: (quantity - left).max(Decimal::ZERO),
1930 price,
1931 average_price: nonzero_decimal(opt_str(order, "fill_price")),
1932 })
1933}
1934
1935fn futures_order_status(status: &str, finish_as: &str, filled: Decimal) -> OrderStatus {
1943 match status {
1944 "finished" if finish_as == "filled" => OrderStatus::Filled,
1945 "finished" => OrderStatus::Canceled,
1946 _ if filled > Decimal::ZERO => OrderStatus::PartiallyFilled,
1947 _ => OrderStatus::New,
1948 }
1949}
1950
1951fn number_or_string(value: &serde_json::Value, key: &str) -> Decimal {
1953 value.get(key).map_or(Decimal::ZERO, |field| {
1954 field.as_str().map_or_else(
1955 || {
1956 field
1957 .as_f64()
1958 .and_then(|n| parse_decimal(&n.to_string()).ok())
1959 .unwrap_or(Decimal::ZERO)
1960 },
1961 |text| parse_decimal(text).unwrap_or(Decimal::ZERO),
1962 )
1963 })
1964}
1965
1966#[derive(Deserialize)]
1967struct RawOrder {
1968 id: String,
1969 #[serde(default)]
1970 text: String,
1971 side: String,
1972 #[serde(rename = "type")]
1973 order_type: String,
1974 status: String,
1975 amount: String,
1976 #[serde(rename = "filled_amount", default)]
1977 filled_amount: String,
1978 #[serde(default)]
1981 left: String,
1982 #[serde(default)]
1983 price: String,
1984 #[serde(rename = "avg_deal_price", default)]
1985 avg_deal_price: String,
1986}
1987
1988#[derive(Deserialize)]
1989struct RawAccount {
1990 currency: String,
1991 #[serde(default)]
1992 available: String,
1993 #[serde(default)]
1994 locked: String,
1995}
1996
1997#[derive(Deserialize)]
1998struct RawFuturesTicker {
1999 last: String,
2000 #[serde(rename = "volume_24h_base", default)]
2001 volume_24h_base: String,
2002}
2003
2004#[derive(Deserialize)]
2005struct RawFuturesCandle {
2006 t: i64,
2007 o: String,
2008 h: String,
2009 l: String,
2010 c: String,
2011 #[serde(default)]
2012 v: f64,
2013}
2014
2015#[derive(Deserialize)]
2016struct RawFuturesLevel {
2017 p: String,
2018 s: i64,
2019}
2020
2021#[derive(Deserialize)]
2022struct RawFuturesDepth {
2023 #[serde(default)]
2024 id: u64,
2025 bids: Vec<RawFuturesLevel>,
2026 asks: Vec<RawFuturesLevel>,
2027}
2028
2029#[derive(Deserialize)]
2030struct RawFuturesOrder {
2031 id: i64,
2032 size: i64,
2033 #[serde(default)]
2034 left: i64,
2035 #[serde(default)]
2036 price: String,
2037 #[serde(rename = "fill_price", default)]
2038 fill_price: String,
2039 status: String,
2040 #[serde(rename = "finish_as", default)]
2041 finish_as: String,
2042 #[serde(default)]
2043 text: String,
2044}
2045
2046#[derive(Deserialize)]
2047struct RawFuturesAccount {
2048 #[serde(default = "usdt_currency")]
2049 currency: String,
2050 #[serde(default)]
2051 total: String,
2052 #[serde(default)]
2053 available: String,
2054}
2055
2056fn usdt_currency() -> String {
2057 "USDT".to_string()
2058}
2059
2060#[derive(Deserialize)]
2061struct RawFuturesPosition {
2062 contract: String,
2063 size: i64,
2064 #[serde(default)]
2065 leverage: String,
2066 #[serde(rename = "cross_leverage_limit", default)]
2067 cross_leverage_limit: String,
2068 #[serde(rename = "entry_price", default)]
2069 entry_price: String,
2070 #[serde(rename = "mark_price", default)]
2071 mark_price: String,
2072 #[serde(rename = "unrealised_pnl", default)]
2073 unrealised_pnl: String,
2074}
2075
2076fn decimal_to_contracts(quantity: Decimal) -> Result<i64> {
2078 let contracts = quantity
2079 .round()
2080 .to_i64()
2081 .filter(|c| *c != 0)
2082 .ok_or(Error::InvalidOrder("futures size rounds to zero contracts"))?;
2083 Ok(contracts.abs())
2084}
2085
2086fn parse_futures_candle(row: &RawFuturesCandle) -> Result<Candle> {
2087 let f = |s: &str| -> Result<f64> {
2088 s.parse::<f64>()
2089 .map_err(|e| Error::Deserialization(format!("candle field not a number: {e}")))
2090 };
2091 Candle::new(f(&row.o)?, f(&row.h)?, f(&row.l)?, f(&row.c)?, row.v, row.t)
2092 .map_err(|e| Error::Deserialization(e.to_string()))
2093}
2094
2095fn parse_futures_levels(levels: &[RawFuturesLevel]) -> Result<Vec<BookLevel>> {
2096 levels
2097 .iter()
2098 .map(|level| {
2099 Ok(BookLevel {
2100 price: parse_decimal(&level.p)?,
2101 quantity: Decimal::from(level.s.abs()),
2102 })
2103 })
2104 .collect()
2105}
2106
2107fn futures_order_from_raw(symbol: Symbol, raw: &RawFuturesOrder) -> Result<Order> {
2108 let side = if raw.size >= 0 {
2109 OrderSide::Buy
2110 } else {
2111 OrderSide::Sell
2112 };
2113 let quantity = Decimal::from(raw.size.abs());
2114 let filled = Decimal::from(raw.size.abs() - raw.left.abs());
2115 let order_type = if raw.price == "0" || raw.price.is_empty() {
2116 OrderType::Market
2117 } else {
2118 OrderType::Limit
2119 };
2120 let status = match raw.status.as_str() {
2121 "open" => {
2122 if filled > Decimal::ZERO {
2123 OrderStatus::PartiallyFilled
2124 } else {
2125 OrderStatus::New
2126 }
2127 }
2128 "finished" => {
2129 if raw.finish_as == "filled" {
2130 OrderStatus::Filled
2131 } else {
2132 OrderStatus::Canceled
2133 }
2134 }
2135 other => return Err(Error::Deserialization(format!("unknown status {other:?}"))),
2136 };
2137 Ok(Order {
2138 id: raw.id.to_string(),
2139 client_order_id: (!raw.text.is_empty()).then(|| raw.text.clone()),
2140 symbol,
2141 side,
2142 order_type,
2143 status,
2144 quantity,
2145 filled_quantity: filled,
2146 price: nonzero_decimal(&raw.price),
2147 average_price: nonzero_decimal(&raw.fill_price),
2148 })
2149}
2150
2151fn parse_futures_position(raw: &RawFuturesPosition) -> Position {
2152 let side = if raw.size > 0 {
2153 PositionSide::Long
2154 } else {
2155 PositionSide::Short
2156 };
2157 let is_cross = raw.leverage.is_empty() || raw.leverage == "0";
2160 let leverage = if is_cross {
2161 dec_or_zero(&raw.cross_leverage_limit)
2162 } else {
2163 dec_or_zero(&raw.leverage)
2164 };
2165 Position {
2166 symbol: symbol_from_contract(&raw.contract),
2167 side,
2168 quantity: Decimal::from(raw.size.abs()),
2169 entry_price: dec_or_zero(&raw.entry_price),
2170 mark_price: dec_or_zero(&raw.mark_price),
2171 leverage,
2172 unrealized_pnl: dec_or_zero(&raw.unrealised_pnl),
2173 margin_mode: if is_cross {
2174 MarginMode::Cross
2175 } else {
2176 MarginMode::Isolated
2177 },
2178 }
2179}
2180
2181fn symbol_from_contract(contract: &str) -> Symbol {
2183 match contract.split_once('_') {
2184 Some((base, quote)) => Symbol::new(base, quote),
2185 None => Symbol::new(contract, ""),
2186 }
2187}
2188
2189impl MarketData for Gate {
2190 fn ticker(&mut self, symbol: &Symbol) -> Result<Ticker> {
2191 Gate::ticker(self, symbol)
2192 }
2193 fn klines(&mut self, symbol: &Symbol, interval: &str, limit: u32) -> Result<Vec<Candle>> {
2194 Gate::klines(self, symbol, interval, limit)
2195 }
2196 fn order_book(&mut self, symbol: &Symbol, depth: u32) -> Result<OrderBookSnapshot> {
2197 Gate::order_book(self, symbol, depth)
2198 }
2199 fn subscribe_trades(&mut self, symbol: &Symbol) -> Result<()> {
2200 Gate::subscribe_trades(self, symbol)
2201 }
2202 fn subscribe_book(&mut self, symbol: &Symbol) -> Result<()> {
2203 Gate::subscribe_book(self, symbol)
2204 }
2205 fn subscribe_ticker(&mut self, symbol: &Symbol) -> Result<()> {
2206 Gate::subscribe_ticker(self, symbol)
2207 }
2208 fn poll_events(&mut self) -> Vec<Event> {
2209 Gate::poll_events(self)
2210 }
2211}
2212
2213impl Execution for Gate {
2214 fn place_order(&mut self, request: &OrderRequest) -> Result<Order> {
2215 Gate::place_order(self, request)
2216 }
2217 fn cancel_order(&mut self, symbol: &Symbol, order_id: &str) -> Result<()> {
2218 Gate::cancel_order(self, symbol, order_id)
2219 }
2220 fn query_order(&mut self, symbol: &Symbol, order_id: &str) -> Result<Order> {
2221 Gate::query_order(self, symbol, order_id)
2222 }
2223 fn open_orders(&mut self, symbol: Option<&Symbol>) -> Result<Vec<Order>> {
2224 Gate::open_orders(self, symbol)
2225 }
2226 fn balances(&mut self) -> Result<Vec<Balance>> {
2227 Gate::balances(self)
2228 }
2229}
2230
2231impl Exchange for Gate {
2232 fn name(&self) -> &'static str {
2233 "gate"
2234 }
2235}
2236
2237impl WsUserData for Gate {
2238 fn subscribe_user_data(&mut self) -> Result<()> {
2239 Gate::subscribe_user_data(self)
2240 }
2241 fn keepalive_user_data(&mut self) -> Result<()> {
2242 Gate::keepalive_user_data(self)
2243 }
2244}
2245
2246impl WsExecution for Gate {
2247 fn place_order_ws(&mut self, request: &OrderRequest) -> Result<Order> {
2248 Gate::place_order_ws(self, request)
2249 }
2250 fn cancel_order_ws(&mut self, symbol: &Symbol, order_id: &str) -> Result<()> {
2251 Gate::cancel_order_ws(self, symbol, order_id)
2252 }
2253}
2254
2255impl Derivatives for Gate {
2256 fn positions(&mut self, symbol: Option<&Symbol>) -> Result<Vec<Position>> {
2257 Gate::positions(self, symbol)
2258 }
2259 fn set_leverage(&mut self, symbol: &Symbol, leverage: u32) -> Result<()> {
2260 Gate::set_leverage(self, symbol, leverage)
2261 }
2262 fn set_margin_mode(&mut self, symbol: &Symbol, mode: MarginMode) -> Result<()> {
2263 Gate::set_margin_mode(self, symbol, mode)
2264 }
2265 fn close_position(&mut self, symbol: &Symbol) -> Result<Order> {
2266 Gate::close_position(self, symbol)
2267 }
2268}
2269
2270impl DerivativesStream for Gate {
2271 fn subscribe_derivatives(
2272 &mut self,
2273 symbol: &Symbol,
2274 channel: DerivativesChannel,
2275 ) -> Result<()> {
2276 Gate::subscribe_derivatives(self, symbol, channel)
2277 }
2278 fn open_interest(&mut self, symbol: &Symbol) -> Result<OpenInterest> {
2279 Gate::open_interest(self, symbol)
2280 }
2281 fn long_short_ratio(&mut self, symbol: &Symbol) -> Result<LongShortRatio> {
2282 Gate::long_short_ratio(self, symbol)
2283 }
2284}
2285
2286impl Gate {
2287 pub fn amend_order(
2293 &self,
2294 symbol: &Symbol,
2295 order_id: &str,
2296 new_price: Option<Decimal>,
2297 new_quantity: Option<Decimal>,
2298 ) -> Result<Order> {
2299 let mut body = serde_json::json!({});
2300 if let Some(q) = new_quantity {
2301 body["amount"] = serde_json::json!(format_decimal(q));
2302 }
2303 if let Some(p) = new_price {
2304 body["price"] = serde_json::json!(format_decimal(p));
2305 }
2306 let path = format!("/api/v4/spot/orders/{order_id}");
2307 let query = format!("currency_pair={}", Self::wire_symbol(symbol));
2308 let value = self.signed_request(HttpMethod::Patch, &path, &query, &body.to_string())?;
2309 let raw: RawOrder = parse_json(value)?;
2310 order_from_raw(symbol.clone(), &raw)
2311 }
2312
2313 pub fn place_batch(&self, requests: &[OrderRequest]) -> Result<Vec<Result<Order>>> {
2320 if requests.iter().any(|r| r.order_type.is_trigger()) {
2321 return Err(Error::unsupported_trigger("Gate.io"));
2322 }
2323 for request in requests {
2324 self.ensure_reduce_only_is_reducible(request)?;
2325 }
2326 let tifs = requests.iter().map(tif_str).collect::<Result<Vec<_>>>()?;
2329 let items: Vec<serde_json::Value> = requests
2330 .iter()
2331 .zip(&tifs)
2332 .map(|(r, tif)| {
2333 let mut o = serde_json::json!({
2334 "currency_pair": Self::wire_symbol(&r.symbol),
2335 "side": side_str(r.side),
2336 "type": order_type_str(r.order_type),
2337 "amount": format_decimal(r.quantity),
2338 });
2339 if let Some(price) = r.price {
2340 o["price"] = serde_json::json!(format_decimal(price));
2341 }
2342 if let Some(tif) = tif {
2343 o["time_in_force"] = serde_json::json!(*tif);
2344 }
2345 if let Some(id) = &r.client_order_id {
2346 let text = if id.starts_with("t-") {
2347 id.clone()
2348 } else {
2349 format!("t-{id}")
2350 };
2351 o["text"] = serde_json::json!(text);
2352 }
2353 if let Some(act) = stp_act_str(r.stp) {
2354 o["stp_act"] = serde_json::json!(act);
2355 }
2356 o
2357 })
2358 .collect();
2359 let body = serde_json::Value::Array(items).to_string();
2360 let value =
2361 self.signed_request(HttpMethod::Post, "/api/v4/spot/batch_orders", "", &body)?;
2362 let elements: Vec<serde_json::Value> = parse_json(value)?;
2363 Ok(requests
2364 .iter()
2365 .zip(elements)
2366 .map(|(req, elem)| {
2367 let succeeded = elem
2368 .get("succeeded")
2369 .and_then(serde_json::Value::as_bool)
2370 .unwrap_or(true);
2371 if !succeeded {
2372 let message = elem
2373 .get("message")
2374 .or_else(|| elem.get("label"))
2375 .and_then(serde_json::Value::as_str)
2376 .unwrap_or("order rejected in batch")
2377 .to_string();
2378 return Err(Error::OrderRejected {
2379 code: "batch".to_string(),
2380 message,
2381 });
2382 }
2383 let raw: RawOrder = serde_json::from_value(elem)
2384 .map_err(|e| Error::Deserialization(e.to_string()))?;
2385 order_from_raw(req.symbol.clone(), &raw)
2386 })
2387 .collect())
2388 }
2389
2390 pub fn cancel_batch(&self, symbol: &Symbol, order_ids: &[String]) -> Result<()> {
2396 let pair = Self::wire_symbol(symbol);
2397 let items: Vec<serde_json::Value> = order_ids
2398 .iter()
2399 .map(|id| serde_json::json!({ "currency_pair": pair, "id": id }))
2400 .collect();
2401 let body = serde_json::Value::Array(items).to_string();
2402 self.signed_request(
2403 HttpMethod::Post,
2404 "/api/v4/spot/cancel_batch_orders",
2405 "",
2406 &body,
2407 )?;
2408 Ok(())
2409 }
2410}
2411
2412impl AdvancedOrders for Gate {
2413 fn amend_order(
2414 &mut self,
2415 symbol: &Symbol,
2416 order_id: &str,
2417 new_price: Option<Decimal>,
2418 new_quantity: Option<Decimal>,
2419 ) -> Result<Order> {
2420 Gate::amend_order(self, symbol, order_id, new_price, new_quantity)
2421 }
2422 fn place_batch(&mut self, requests: &[OrderRequest]) -> Result<Vec<Result<Order>>> {
2423 Gate::place_batch(self, requests)
2424 }
2425 fn cancel_batch(&mut self, symbol: &Symbol, order_ids: &[String]) -> Result<()> {
2426 Gate::cancel_batch(self, symbol, order_ids)
2427 }
2428 fn place_oco(&mut self, _request: &OcoRequest) -> Result<Vec<Order>> {
2431 Err(Error::Exchange {
2432 code: "unsupported".to_string(),
2433 message: "Gate has no OCO order-list; use price-triggered orders".to_string(),
2434 })
2435 }
2436}
2437
2438#[cfg(test)]
2439mod tests {
2440 use super::*;
2441 use crate::transport::{MockHttpTransport, MockWsTransport};
2442 use rust_decimal_macros::dec;
2443 use std::sync::Arc;
2444
2445 struct ArcTransport(Arc<MockHttpTransport>);
2446 impl HttpTransport for ArcTransport {
2447 fn execute(&self, request: &HttpRequest) -> Result<HttpResponse> {
2448 self.0.execute(request)
2449 }
2450 }
2451 struct ArcWs(Arc<MockWsTransport>);
2452 impl WsTransport for ArcWs {
2453 fn connect(&self, url: &str) -> Result<Box<dyn WsConnection>> {
2454 self.0.connect(url)
2455 }
2456 }
2457
2458 fn symbol() -> Symbol {
2459 Symbol::new("BTC", "USDT")
2460 }
2461
2462 fn client() -> (Gate, Arc<MockHttpTransport>) {
2463 let mock = Arc::new(MockHttpTransport::new());
2464 let opts = ExchangeOptions::mainnet(crate::MarketType::Spot);
2465 (
2466 Gate::with_http(Box::new(ArcTransport(Arc::clone(&mock))), &opts),
2467 mock,
2468 )
2469 }
2470
2471 fn futures_ws_client() -> (Gate, Arc<MockWsTransport>) {
2472 let http = Arc::new(MockHttpTransport::new());
2473 let ws = Arc::new(MockWsTransport::new());
2474 let opts = ExchangeOptions::mainnet(crate::MarketType::UsdMFutures);
2475 let gate = Gate::with_http(Box::new(ArcTransport(http)), &opts)
2476 .with_ws(Box::new(ArcWs(Arc::clone(&ws))));
2477 (gate, ws)
2478 }
2479
2480 #[test]
2482 fn gate_polled_reads_report_what_went_wrong() {
2483 let http = Arc::new(MockHttpTransport::new());
2484 let opts = ExchangeOptions::mainnet(crate::MarketType::UsdMFutures);
2485 let gate = Gate::with_http(Box::new(ArcTransport(Arc::clone(&http))), &opts);
2486
2487 http.push_json(200, "[]");
2488 assert!(matches!(
2489 Gate::open_interest(&gate, &symbol()),
2490 Err(Error::NotFound(_))
2491 ));
2492
2493 http.push_json(200, r#"[{"time":1788464400}]"#);
2494 assert!(matches!(
2495 Gate::open_interest(&gate, &symbol()),
2496 Err(Error::Deserialization(_))
2497 ));
2498
2499 http.push_json(200, r#"[{"time":1788464400,"open_interest":1}]"#);
2500 assert!(matches!(
2501 Gate::long_short_ratio(&gate, &symbol()),
2502 Err(Error::Deserialization(_))
2503 ));
2504
2505 http.push_json(200, r#"[{"time":1788464400,"lsr_account":-1.0}]"#);
2508 assert!(matches!(
2509 Gate::long_short_ratio(&gate, &symbol()),
2510 Err(Error::Deserialization(_))
2511 ));
2512 }
2513
2514 #[test]
2516 fn gate_drops_futures_frames_it_cannot_read() {
2517 let (mut gate, ws) = futures_ws_client();
2518 ws.push_connection(vec![
2519 Ok(Some(
2521 r#"{"time":1,"channel":"futures.order_book_update","event":"update",
2522 "result":{"t":1,"U":1,"u":2,"a":[],"b":[]}}"#
2523 .to_string(),
2524 )),
2525 Ok(Some(
2527 r#"{"time":1,"channel":"futures.trades","event":"update",
2528 "result":[{"size":1,"contract":"BTC_USDT"}]}"#
2529 .to_string(),
2530 )),
2531 Ok(Some(
2533 r#"{"time":1,"channel":"futures.tickers","event":"update",
2534 "result":[{"contract":"BTC_USDT"}]}"#
2535 .to_string(),
2536 )),
2537 Ok(Some(
2539 r#"{"time":1,"channel":"futures.order_book_update","event":"update",
2540 "result":{"t":1,"U":1,"u":2,"s":"BTC_USDT","a":[{"p":"1.0"}],"b":[]}}"#
2541 .to_string(),
2542 )),
2543 ]);
2544 Gate::subscribe_book(&mut gate, &symbol()).unwrap();
2545
2546 let events = Gate::poll_events(&mut gate);
2547 assert_eq!(events.len(), 1, "{events:?}");
2550 let Event::BookDelta(delta) = &events[0] else {
2551 panic!("expected a delta");
2552 };
2553 assert!(delta.asks.is_empty() && delta.bids.is_empty());
2554 }
2555
2556 #[test]
2558 fn the_futures_ticker_answers_funding_and_mark_index_on_gate() {
2559 let (mut gate, ws) = futures_ws_client();
2560 ws.push_connection(vec![Ok(Some(
2561 r#"{"time":1788462378,"channel":"futures.tickers","event":"update",
2562 "result":[{"contract":"BTC_USDT","last":"81231.4","mark_price":"81238.4",
2563 "index_price":"81271.1","funding_rate":"0.000073",
2564 "volume_24h_base":"81628.265","t":1788462378855}]}"#
2565 .to_string(),
2566 ))]);
2567 Gate::subscribe_derivatives(&mut gate, &symbol(), DerivativesChannel::Funding).unwrap();
2568 Gate::subscribe_derivatives(&mut gate, &symbol(), DerivativesChannel::MarkIndex).unwrap();
2569
2570 let events = Gate::poll_events(&mut gate);
2571 assert_eq!(events.len(), 3, "ticker, funding, mark/index: {events:?}");
2572
2573 let Event::Derivatives(DerivativesFeed::Funding(funding)) = &events[1] else {
2574 panic!("expected funding, got {:?}", events[1]);
2575 };
2576 assert_eq!(funding.rate, dec!(0.000073));
2577 assert_eq!(funding.mark_price, dec!(81238.4));
2578
2579 let Event::Derivatives(DerivativesFeed::MarkIndex(mark)) = &events[2] else {
2580 panic!("expected mark/index, got {:?}", events[2]);
2581 };
2582 assert_eq!(mark.index_price, dec!(81271.1));
2583 }
2584
2585 #[test]
2589 fn gate_refuses_liquidations() {
2590 let (mut gate, ws) = futures_ws_client();
2591 let err =
2592 Gate::subscribe_derivatives(&mut gate, &symbol(), DerivativesChannel::Liquidations)
2593 .expect_err("Gate publishes no public forced-order feed");
2594 assert!(matches!(err, Error::Exchange { ref code, .. } if code == "unsupported"));
2595 assert_eq!(ws.sent(), Vec::<String>::new());
2596 }
2597
2598 #[test]
2600 fn the_polled_figures_come_from_one_row_stamped_in_seconds() {
2601 let http = Arc::new(MockHttpTransport::new());
2602 let opts = ExchangeOptions::mainnet(crate::MarketType::UsdMFutures);
2603 let gate = Gate::with_http(Box::new(ArcTransport(Arc::clone(&http))), &opts);
2604 http.push_json(
2605 200,
2606 r#"[{"time":1788464400,"lsr_taker":1.6098,"lsr_account":3.0,
2607 "open_interest":626095712,"mark_price":81477,
2608 "open_interest_usd":5101240032.6624}]"#,
2609 );
2610 let open_interest = Gate::open_interest(&gate, &symbol()).unwrap();
2611 assert_eq!(open_interest.open_interest, dec!(626095712));
2612 assert_eq!(open_interest.timestamp, 1_788_464_400_000);
2614
2615 http.push_json(
2616 200,
2617 r#"[{"time":1788464400,"lsr_account":3.0,"open_interest":626095712}]"#,
2618 );
2619 let ratio = Gate::long_short_ratio(&gate, &symbol()).unwrap();
2620 assert_eq!(ratio.long_size, dec!(0.75));
2622 assert_eq!(ratio.short_size, dec!(0.25));
2623 assert_eq!(ratio.long_size + ratio.short_size, Decimal::ONE);
2624 }
2625
2626 #[test]
2627 fn a_spot_client_refuses_the_gate_derivatives_surface() {
2628 let ws = Arc::new(MockWsTransport::new());
2629 let http = Arc::new(MockHttpTransport::new());
2630 let opts = ExchangeOptions::mainnet(crate::MarketType::Spot);
2631 let mut gate = Gate::with_http(Box::new(ArcTransport(Arc::clone(&http))), &opts)
2632 .with_ws(Box::new(ArcWs(Arc::clone(&ws))));
2633 for channel in [
2634 DerivativesChannel::Funding,
2635 DerivativesChannel::MarkIndex,
2636 DerivativesChannel::Liquidations,
2637 ] {
2638 assert!(Gate::subscribe_derivatives(&mut gate, &symbol(), channel).is_err());
2639 }
2640 assert!(Gate::open_interest(&gate, &symbol()).is_err());
2641 assert!(Gate::long_short_ratio(&gate, &symbol()).is_err());
2642 }
2643
2644 #[test]
2646 fn a_futures_client_subscribes_on_the_futures_host() {
2647 let (mut gate, ws) = futures_ws_client();
2648 Gate::subscribe_trades(&mut gate, &symbol()).unwrap();
2649 Gate::subscribe_book(&mut gate, &symbol()).unwrap();
2650
2651 assert_eq!(ws.connected_urls()[0], "wss://fx-ws.gateio.ws/v4/ws/usdt");
2652 assert!(ws.sent()[0].contains(r#""channel":"futures.trades""#));
2653 assert!(!ws.sent()[0].contains("spot."));
2654 assert!(ws.sent()[1].contains(r#""channel":"futures.order_book_update""#));
2656 assert!(ws.sent()[1].contains(r#""payload":["BTC_USDT","100ms","20"]"#));
2657 }
2658
2659 #[test]
2662 fn a_futures_trade_takes_its_side_from_the_sign_of_the_size() {
2663 let (mut gate, ws) = futures_ws_client();
2664 ws.push_connection(vec![
2665 Ok(Some(
2666 r#"{"time":1788462378,"time_ms":1788462378855,"channel":"futures.trades",
2667 "event":"update","result":[{"id":826990332,"size":14372,
2668 "create_time":1788462378,"create_time_ms":1788462378855,
2669 "price":"81231.4","contract":"BTC_USDT"}]}"#
2670 .to_string(),
2671 )),
2672 Ok(Some(
2673 r#"{"time":1788462379,"channel":"futures.trades","event":"update",
2674 "result":[{"id":826990333,"size":-25,"create_time_ms":1788462379000,
2675 "price":"81230.0","contract":"BTC_USDT"}]}"#
2676 .to_string(),
2677 )),
2678 ]);
2679 Gate::subscribe_trades(&mut gate, &symbol()).unwrap();
2680
2681 let events = Gate::poll_events(&mut gate);
2682 let Event::Trade(bought) = &events[0] else {
2683 panic!("expected a trade, got {:?}", events[0]);
2684 };
2685 assert_eq!(bought.aggressor, OrderSide::Buy);
2686 assert_eq!(bought.quantity, dec!(14372));
2687 assert_eq!(bought.price, dec!(81231.4));
2688 assert_eq!(bought.timestamp, 1_788_462_378_855);
2689
2690 let Event::Trade(sold) = &events[1] else {
2691 panic!("expected a trade, got {:?}", events[1]);
2692 };
2693 assert_eq!(sold.aggressor, OrderSide::Sell);
2694 assert_eq!(sold.quantity, dec!(25));
2696 }
2697
2698 #[test]
2699 fn a_futures_book_update_parses_both_sides() {
2700 let (mut gate, ws) = futures_ws_client();
2701 ws.push_connection(vec![Ok(Some(
2702 r#"{"time":1788462378,"channel":"futures.order_book_update","event":"update",
2703 "result":{"t":1788462378780,"U":124074489866,"u":124074489905,"s":"BTC_USDT",
2704 "a":[{"p":"81231.4","s":4572},{"p":"81239.3","s":0}],
2705 "b":[{"p":"81230.0","s":12}],"l":"20"}}"#
2706 .to_string(),
2707 ))]);
2708 Gate::subscribe_book(&mut gate, &symbol()).unwrap();
2709
2710 let events = Gate::poll_events(&mut gate);
2711 let Event::BookDelta(delta) = &events[0] else {
2712 panic!("expected a delta, got {:?}", events[0]);
2713 };
2714 assert_eq!(delta.asks.len(), 2);
2715 assert_eq!(delta.asks[0].price, dec!(81231.4));
2716 assert_eq!(delta.asks[1].quantity, Decimal::ZERO);
2718 assert_eq!(delta.bids.len(), 1);
2719 assert_eq!(delta.first_update_id, 124_074_489_866);
2720 assert_eq!(delta.final_update_id, 124_074_489_905);
2721 }
2722
2723 #[test]
2726 fn a_futures_ticker_reports_no_top_of_book() {
2727 let (mut gate, ws) = futures_ws_client();
2728 ws.push_connection(vec![Ok(Some(
2729 r#"{"time":1788462378,"channel":"futures.tickers","event":"update",
2730 "result":[{"contract":"BTC_USDT","last":"81231.4","mark_price":"81238.4",
2731 "index_price":"81271.1","funding_rate":"0.000073","volume_24h_base":"81628.265",
2732 "t":1788462378855}]}"#
2733 .to_string(),
2734 ))]);
2735 Gate::subscribe_ticker(&mut gate, &symbol()).unwrap();
2736
2737 let events = Gate::poll_events(&mut gate);
2738 let Event::Ticker(ticker) = &events[0] else {
2739 panic!("expected a ticker, got {:?}", events[0]);
2740 };
2741 assert_eq!(ticker.last, dec!(81231.4));
2742 assert_eq!(ticker.volume, dec!(81628.265));
2743 assert_eq!(ticker.bid, Decimal::ZERO);
2744 assert_eq!(ticker.ask, Decimal::ZERO);
2745 assert_eq!(ticker.timestamp, 1_788_462_378_855);
2746 }
2747
2748 #[test]
2750 fn a_spot_client_still_subscribes_to_spot_channels() {
2751 let ws = Arc::new(MockWsTransport::new());
2752 let http = Arc::new(MockHttpTransport::new());
2753 let opts = ExchangeOptions::mainnet(crate::MarketType::Spot);
2754 let mut gate = Gate::with_http(Box::new(ArcTransport(http)), &opts)
2755 .with_ws(Box::new(ArcWs(Arc::clone(&ws))));
2756 Gate::subscribe_trades(&mut gate, &symbol()).unwrap();
2757
2758 assert_eq!(ws.connected_urls()[0], "wss://api.gateio.ws/ws/v4/");
2759 assert!(ws.sent()[0].contains(r#""channel":"spot.trades""#));
2760 }
2761
2762 fn signed_client(now_ms: i64) -> (Gate, Arc<MockHttpTransport>) {
2763 let mock = Arc::new(MockHttpTransport::new());
2764 let opts = ExchangeOptions::mainnet(crate::MarketType::Spot);
2765 let gate = Gate::with_credentials(
2766 Box::new(ArcTransport(Arc::clone(&mock))),
2767 &opts,
2768 Credentials::new("APIKEY", "SECRET"),
2769 )
2770 .with_clock(Box::new(move || now_ms));
2771 (gate, mock)
2772 }
2773
2774 fn signed_ws_client(now_ms: i64) -> (Gate, Arc<MockWsTransport>) {
2775 let http = Arc::new(MockHttpTransport::new());
2776 let ws = Arc::new(MockWsTransport::new());
2777 let opts = ExchangeOptions::mainnet(crate::MarketType::Spot);
2778 let gate = Gate::with_credentials(
2779 Box::new(ArcTransport(http)),
2780 &opts,
2781 Credentials::new("APIKEY", "SECRET"),
2782 )
2783 .with_ws(Box::new(ArcWs(Arc::clone(&ws))))
2784 .with_clock(Box::new(move || now_ms));
2785 (gate, ws)
2786 }
2787
2788 #[test]
2789 fn subscribe_user_data_authenticates_and_streams_orders_and_balances() {
2790 let (mut gate, ws) = signed_ws_client(1_700_000_000_000);
2791 ws.push_connection(vec![
2792 Ok(Some(r#"{"channel":"spot.orders","event":"subscribe"}"#.to_string())),
2793 Ok(Some(r#"{"channel":"spot.balances","event":"subscribe"}"#.to_string())),
2794 Ok(Some(
2795 r#"{"time":1,"channel":"spot.orders","event":"update","result":[{"id":"55",
2796 "text":"my","currency_pair":"BTC_USDT","side":"buy","type":"limit","status":"closed",
2797 "amount":"1","left":"0","price":"100","avg_deal_price":"100"}]}"#
2798 .to_string(),
2799 )),
2800 Ok(Some(
2801 r#"{"time":2,"channel":"spot.balances","event":"update","result":[{"currency":"USDT",
2802 "available":"900","total":"950"}]}"#
2803 .to_string(),
2804 )),
2805 ]);
2806 gate.subscribe_user_data().unwrap();
2807 assert_eq!(ws.connected_urls()[0], "wss://api.gateio.ws/ws/v4/");
2808 assert!(ws.sent()[0].contains(r#""channel":"spot.orders""#));
2809 assert!(ws.sent()[0].contains(r#""KEY":"APIKEY""#));
2810 assert!(ws.sent()[0].contains(r#""SIGN""#));
2811 assert!(ws.sent()[1].contains(r#""channel":"spot.balances""#));
2812
2813 let events = gate.poll_events();
2814 assert_eq!(events.len(), 2);
2815 let Event::OrderUpdate(order) = &events[0] else {
2816 panic!("first event must be an order update");
2817 };
2818 assert_eq!(order.id, "55");
2819 assert_eq!(order.symbol, symbol());
2820 assert_eq!(order.side, OrderSide::Buy);
2821 assert_eq!(order.status, OrderStatus::Filled);
2822 assert_eq!(order.filled_quantity, dec!(1));
2823 assert_eq!(order.average_price, Some(dec!(100)));
2824 let Event::BalanceUpdate(balances) = &events[1] else {
2825 panic!("second event must be a balance update");
2826 };
2827 assert_eq!(balances.len(), 1);
2828 assert_eq!(balances[0].asset, "USDT");
2829 assert_eq!(balances[0].free, dec!(900));
2830 assert_eq!(balances[0].locked, dec!(50));
2831 }
2832
2833 #[test]
2834 fn subscribe_user_data_requires_credentials() {
2835 let http = Arc::new(MockHttpTransport::new());
2836 let ws = Arc::new(MockWsTransport::new());
2837 let opts = ExchangeOptions::mainnet(crate::MarketType::Spot);
2838 let mut gate = Gate::with_http(Box::new(ArcTransport(http)), &opts)
2839 .with_ws(Box::new(ArcWs(Arc::clone(&ws))));
2840 assert!(matches!(
2841 gate.subscribe_user_data().unwrap_err(),
2842 Error::InvalidCredentials(_)
2843 ));
2844 }
2845
2846 #[test]
2847 fn keepalive_user_data_pings_the_private_stream() {
2848 let (mut gate, ws) = signed_ws_client(1_700_000_000_000);
2849 ws.push_connection(vec![]);
2850 gate.subscribe_user_data().unwrap();
2851 gate.keepalive_user_data().unwrap();
2852 assert!(ws.sent().iter().any(|f| f == "ping"));
2853 }
2854
2855 #[test]
2856 fn keepalive_user_data_is_a_noop_before_subscribe() {
2857 let (mut gate, ws) = signed_ws_client(1_700_000_000_000);
2858 gate.keepalive_user_data().unwrap();
2859 assert_eq!(ws.sent(), Vec::<String>::new());
2860 }
2861
2862 #[test]
2863 fn dropped_user_data_stream_reconnects_with_fresh_signed_frames() {
2864 let (mut gate, ws) = signed_ws_client(1_700_000_000_000);
2865 ws.push_connection(vec![Ok(None)]);
2868 ws.push_connection(vec![]);
2869 gate.subscribe_user_data().unwrap();
2870
2871 let events = gate.poll_events();
2872 assert!(events.contains(&Event::Disconnected));
2873 assert!(events.contains(&Event::Reconnected));
2874 let signed_subs = ws
2877 .sent()
2878 .into_iter()
2879 .filter(|f| f.contains(r#""SIGN""#))
2880 .count();
2881 assert_eq!(signed_subs, 4);
2882 assert_eq!(ws.connected_urls().len(), 2);
2883 assert_eq!(ws.connected_urls()[1], "wss://api.gateio.ws/ws/v4/");
2884 }
2885
2886 #[test]
2887 fn the_websocket_frame_carries_the_stp_policy() {
2888 let (mut gate, ws) = signed_ws_client(1_700_000_000_000);
2891 ws.push_connection(vec![
2892 Ok(Some(
2893 r#"{"request_id":"login","header":{"status":"200","channel":"spot.login",
2894 "event":"api"},"data":{"result":{"api_key":"APIKEY"}}}"#
2895 .to_string(),
2896 )),
2897 Ok(Some(
2898 r#"{"request_id":"b","header":{"status":"200","channel":"spot.order_place",
2899 "event":"api"},"data":{"result":{"id":"55","text":"","currency_pair":"BTC_USDT",
2900 "side":"buy","type":"limit","status":"open","amount":"1","left":"1","price":"100",
2901 "avg_deal_price":"0"}}}"#
2902 .to_string(),
2903 )),
2904 ]);
2905 gate.place_order_ws(
2906 &OrderRequest::limit_buy(symbol(), dec!(1), dec!(100))
2907 .with_stp(SelfTradePrevention::ExpireTaker),
2908 )
2909 .unwrap();
2910 assert!(ws.sent()[1].contains(r#""stp_act":"cn""#));
2911 }
2912
2913 #[test]
2914 fn place_and_cancel_order_over_ws() {
2915 let (mut gate, ws) = signed_ws_client(1_700_000_000_000);
2916 ws.push_connection(vec![
2917 Ok(Some(
2918 r#"{"request_id":"login","header":{"status":"200","channel":"spot.login",
2919 "event":"api"},"data":{"result":{"api_key":"APIKEY"}}}"#
2920 .to_string(),
2921 )),
2922 Ok(Some(
2923 r#"{"request_id":"o","header":{"status":"200","channel":"spot.order_place",
2924 "event":"api"},"data":{"result":{"id":"55","text":"t-my","currency_pair":"BTC_USDT",
2925 "side":"buy","type":"limit","status":"closed","amount":"1","left":"0","price":"100",
2926 "avg_deal_price":"100"}}}"#
2927 .to_string(),
2928 )),
2929 Ok(Some(
2930 r#"{"request_id":"c","header":{"status":"200","channel":"spot.order_cancel",
2931 "event":"api"},"data":{"result":{"id":"55"}}}"#
2932 .to_string(),
2933 )),
2934 ]);
2935 let order = gate
2936 .place_order_ws(&OrderRequest::limit_buy(symbol(), dec!(1), dec!(100)))
2937 .unwrap();
2938 assert_eq!(order.id, "55");
2939 assert_eq!(order.symbol, symbol());
2940 assert_eq!(order.status, OrderStatus::Filled);
2941 assert_eq!(order.filled_quantity, dec!(1));
2942 assert_eq!(ws.connected_urls()[0], "wss://api.gateio.ws/ws/v4/");
2943 assert!(ws.sent()[0].contains(r#""channel":"spot.login""#));
2945 assert!(ws.sent()[0].contains(r#""signature""#));
2946 assert!(ws.sent()[1].contains(r#""channel":"spot.order_place""#));
2947 assert!(ws.sent()[1].contains(r#""currency_pair":"BTC_USDT""#));
2948
2949 gate.cancel_order_ws(&symbol(), "55").unwrap();
2950 assert!(ws.sent()[2].contains(r#""channel":"spot.order_cancel""#));
2951 assert!(ws.sent()[2].contains(r#""order_id":"55""#));
2952 }
2953
2954 #[test]
2955 fn ws_order_surfaces_rejection() {
2956 let (mut gate, ws) = signed_ws_client(1000);
2957 ws.push_connection(vec![
2958 Ok(Some(
2959 r#"{"header":{"status":"200","channel":"spot.login","event":"api"}}"#.to_string(),
2960 )),
2961 Ok(Some(
2962 r#"{"header":{"status":"400","channel":"spot.order_place","event":"api"},
2963 "data":{"errs":{"label":"BALANCE_NOT_ENOUGH","message":"not enough balance"}}}"#
2964 .to_string(),
2965 )),
2966 ]);
2967 assert!(matches!(
2968 gate.place_order_ws(&OrderRequest::limit_buy(symbol(), dec!(1), dec!(100)))
2969 .unwrap_err(),
2970 Error::OrderRejected { .. }
2971 ));
2972 }
2973
2974 #[test]
2975 fn ws_order_requires_credentials() {
2976 let http = Arc::new(MockHttpTransport::new());
2977 let ws = Arc::new(MockWsTransport::new());
2978 let opts = ExchangeOptions::mainnet(crate::MarketType::Spot);
2979 let mut gate = Gate::with_http(Box::new(ArcTransport(http)), &opts)
2980 .with_ws(Box::new(ArcWs(Arc::clone(&ws))));
2981 assert!(matches!(
2982 gate.place_order_ws(&OrderRequest::limit_buy(symbol(), dec!(1), dec!(100)))
2983 .unwrap_err(),
2984 Error::InvalidCredentials(_)
2985 ));
2986 }
2987
2988 fn signed_futures_client(now_ms: i64) -> (Gate, Arc<MockHttpTransport>) {
2989 let mock = Arc::new(MockHttpTransport::new());
2990 let opts = ExchangeOptions::mainnet(crate::MarketType::UsdMFutures);
2991 let gate = Gate::with_credentials(
2992 Box::new(ArcTransport(Arc::clone(&mock))),
2993 &opts,
2994 Credentials::new("APIKEY", "SECRET"),
2995 )
2996 .with_clock(Box::new(move || now_ms));
2997 (gate, mock)
2998 }
2999
3000 #[test]
3007 fn a_spot_trigger_goes_to_the_price_order_endpoint_with_its_rule() {
3008 for (side, rule) in [(OrderSide::Sell, "<="), (OrderSide::Buy, ">=")] {
3009 let (gate, mock) = signed_client(1_000_000);
3010 mock.push_json(200, r#"{"id":77}"#);
3011 let request = OrderRequest {
3012 order_type: OrderType::StopLimit,
3013 stop_price: Some(dec!(19000)),
3014 side,
3015 ..OrderRequest::limit_sell(symbol(), dec!(1), dec!(18900))
3016 };
3017 let order = gate.place_order(&request).unwrap();
3018 assert_eq!(order.id, "77");
3019
3020 let sent = &mock.recorded_requests()[0];
3021 assert!(
3022 sent.url.contains("/api/v4/spot/price_orders"),
3023 "{}",
3024 sent.url
3025 );
3026 let body = sent.body.clone().unwrap();
3027 assert!(body.contains(r#""price":"19000""#), "{body}");
3028 assert!(
3029 body.contains(&format!(r#""rule":"{rule}""#)),
3030 "a {side:?} stop fires the other way: {body}"
3031 );
3032 assert!(body.contains(r#""price":"18900""#), "{body}");
3034 }
3035 }
3036
3037 #[test]
3043 fn a_spot_stop_market_is_refused_rather_than_given_an_invented_price() {
3044 let (gate, mock) = signed_client(1_000_000);
3045 let request = OrderRequest {
3046 order_type: OrderType::StopMarket,
3047 stop_price: Some(dec!(19000)),
3048 ..OrderRequest::market_sell(symbol(), dec!(1))
3049 };
3050 let err = gate.place_order(&request).unwrap_err();
3051 assert!(matches!(err, Error::Exchange { ref code, .. } if code == "unsupported"));
3052 assert_eq!(mock.recorded_requests(), Vec::new());
3053 }
3054
3055 #[test]
3059 fn a_futures_stop_market_takes_the_market_and_signs_its_size() {
3060 let (gate, mock) = signed_futures_client(1_000_000);
3061 mock.push_json(200, r#"{"id":78}"#);
3062 let request = OrderRequest {
3063 order_type: OrderType::StopMarket,
3064 stop_price: Some(dec!(19000)),
3065 ..OrderRequest::market_sell(symbol(), dec!(1))
3066 };
3067 gate.place_order(&request).unwrap();
3068
3069 let sent = &mock.recorded_requests()[0];
3070 assert!(
3071 sent.url.contains("/api/v4/futures/usdt/price_orders"),
3072 "{}",
3073 sent.url
3074 );
3075 let body = sent.body.clone().unwrap();
3076 assert!(body.contains(r#""price":"0""#), "{body}");
3077 assert!(
3078 body.contains(r#""size":-1"#),
3079 "a sell is a negative size: {body}"
3080 );
3081 assert!(body.contains(r#""rule":2"#), "{body}");
3082 }
3083
3084 #[test]
3085 fn a_gate_trigger_without_a_stop_price_is_refused() {
3086 let (gate, mock) = signed_futures_client(1_000_000);
3087 let request = OrderRequest {
3088 order_type: OrderType::StopMarket,
3089 stop_price: None,
3090 ..OrderRequest::market_sell(symbol(), dec!(1))
3091 };
3092 assert!(gate.place_order(&request).is_err());
3093 assert_eq!(mock.recorded_requests(), Vec::new());
3094 }
3095
3096 fn signed_futures_ws_client(
3097 now_ms: i64,
3098 ) -> (Gate, Arc<MockHttpTransport>, Arc<MockWsTransport>) {
3099 let http = Arc::new(MockHttpTransport::new());
3100 let ws = Arc::new(MockWsTransport::new());
3101 let opts = ExchangeOptions::mainnet(crate::MarketType::UsdMFutures);
3102 let gate = Gate::with_credentials(
3103 Box::new(ArcTransport(Arc::clone(&http))),
3104 &opts,
3105 Credentials::new("APIKEY", "SECRET"),
3106 )
3107 .with_ws(Box::new(ArcWs(Arc::clone(&ws))))
3108 .with_clock(Box::new(move || now_ms));
3109 (gate, http, ws)
3110 }
3111
3112 #[test]
3123 fn a_futures_client_subscribes_to_the_futures_channels_for_its_own_user() {
3124 let (mut gate, http, ws) = signed_futures_ws_client(1_700_000_000_000);
3125 http.push_json(
3126 200,
3127 r#"{"user":4242,"currency":"USDT","total":"1","available":"1"}"#,
3128 );
3129 gate.subscribe_user_data().unwrap();
3130
3131 let looked_up = &http.recorded_requests()[0];
3132 assert!(
3133 looked_up.url.contains("/api/v4/futures/usdt/accounts"),
3134 "{}",
3135 looked_up.url
3136 );
3137 assert_eq!(ws.connected_urls()[0], "wss://fx-ws.gateio.ws/v4/ws/usdt");
3138 let sent = ws.sent();
3139 assert!(
3140 sent[0].contains(r#""channel":"futures.orders""#),
3141 "{}",
3142 sent[0]
3143 );
3144 assert!(
3145 sent[0].contains(r#""payload":["4242","!all"]"#),
3146 "{}",
3147 sent[0]
3148 );
3149 assert!(
3150 sent[1].contains(r#""channel":"futures.balances""#),
3151 "{}",
3152 sent[1]
3153 );
3154 assert!(
3155 !sent.iter().any(|frame| frame.contains("spot.")),
3156 "a futures client subscribed to a spot channel: {sent:?}"
3157 );
3158 }
3159
3160 #[test]
3162 fn the_futures_user_id_is_looked_up_once() {
3163 let (mut gate, http, ws) = signed_futures_ws_client(1_700_000_000_000);
3164 http.push_json(
3165 200,
3166 r#"{"user":4242,"currency":"USDT","total":"1","available":"1"}"#,
3167 );
3168 gate.subscribe_user_data().unwrap();
3169 gate.subscribe_user_data().unwrap();
3170 assert_eq!(http.recorded_requests().len(), 1);
3171 assert_eq!(ws.sent().len(), 4);
3172 }
3173
3174 #[test]
3178 fn a_futures_order_frame_becomes_an_order_update() {
3179 let (mut gate, http, ws) = signed_futures_ws_client(1_700_000_000_000);
3180 http.push_json(
3181 200,
3182 r#"{"user":1,"currency":"USDT","total":"1","available":"1"}"#,
3183 );
3184 ws.push_connection(vec![Ok(Some(
3185 r#"{"time":1700000000,"channel":"futures.orders","event":"update","result":[
3186 {"id":88,"contract":"BTC_USDT","size":-4,"left":1,"price":"19000",
3187 "status":"open","text":"t-mine","fill_price":"19010"}]}"#
3188 .to_string(),
3189 ))]);
3190 gate.subscribe_user_data().unwrap();
3191
3192 let events = gate.poll_events();
3193 let Some(Event::OrderUpdate(order)) = events.first() else {
3194 panic!("expected an order update, got {events:?}");
3195 };
3196 assert_eq!(order.id, "88");
3197 assert_eq!(order.symbol, Symbol::new("BTC", "USDT"));
3198 assert_eq!(order.side, OrderSide::Sell);
3200 assert_eq!(order.quantity, dec!(4));
3201 assert_eq!(order.filled_quantity, dec!(3));
3203 assert_eq!(order.status, OrderStatus::PartiallyFilled);
3204 assert_eq!(order.client_order_id.as_deref(), Some("t-mine"));
3205 }
3206
3207 #[test]
3211 fn a_finished_futures_order_is_a_fill_or_a_cancel_by_finish_as() {
3212 assert_eq!(
3213 futures_order_status("finished", "filled", dec!(4)),
3214 OrderStatus::Filled
3215 );
3216 assert_eq!(
3217 futures_order_status("finished", "cancelled", dec!(0)),
3218 OrderStatus::Canceled
3219 );
3220 assert_eq!(
3222 futures_order_status("finished", "reduce_only", dec!(0)),
3223 OrderStatus::Canceled
3224 );
3225 assert_eq!(futures_order_status("open", "", dec!(0)), OrderStatus::New);
3226 }
3227
3228 #[test]
3229 fn a_futures_balance_frame_becomes_a_balance_update() {
3230 let (mut gate, http, ws) = signed_futures_ws_client(1_700_000_000_000);
3231 http.push_json(
3232 200,
3233 r#"{"user":1,"currency":"USDT","total":"1","available":"1"}"#,
3234 );
3235 ws.push_connection(vec![Ok(Some(
3236 r#"{"time":1700000000,"channel":"futures.balances","event":"update","result":[
3237 {"balance":9999.5,"change":"-0.5","currency":"USDT","type":"fee"}]}"#
3238 .to_string(),
3239 ))]);
3240 gate.subscribe_user_data().unwrap();
3241
3242 let events = gate.poll_events();
3243 let Some(Event::BalanceUpdate(balances)) = events.first() else {
3244 panic!("expected a balance update, got {events:?}");
3245 };
3246 assert_eq!(balances[0].asset, "USDT");
3247 assert_eq!(balances[0].free, dec!(9999.5));
3249 }
3250
3251 const GATE_ORDER: &str = r#"{"id":"1","text":"","side":"buy","type":"limit","status":"open",
3252 "amount":"1","filled_amount":"0","price":"100","avg_deal_price":"0"}"#;
3253
3254 #[test]
3255 fn stp_maps_to_stp_act() {
3256 let (gate, mock) = signed_client(1_000_000);
3257 mock.push_json(200, GATE_ORDER);
3258 gate.place_order(
3259 &OrderRequest::limit_buy(symbol(), dec!(1), dec!(100))
3260 .with_stp(SelfTradePrevention::ExpireBoth),
3261 )
3262 .unwrap();
3263 let reqs = mock.recorded_requests();
3264 assert!(reqs[0].body.as_ref().unwrap().contains(r#""stp_act":"cb""#));
3265 }
3266
3267 #[test]
3268 fn amend_order_patches_in_place() {
3269 let (gate, mock) = signed_client(1_000_000);
3270 mock.push_json(
3271 200,
3272 r#"{"id":"1","text":"","side":"buy","type":"limit","status":"open",
3273 "amount":"2","filled_amount":"0","price":"101","avg_deal_price":"0"}"#,
3274 );
3275 let order = gate
3276 .amend_order(&symbol(), "1", Some(dec!(101)), Some(dec!(2)))
3277 .unwrap();
3278 assert_eq!(order.quantity, dec!(2));
3279 assert_eq!(order.price, Some(dec!(101)));
3280 let reqs = mock.recorded_requests();
3281 assert_eq!(reqs[0].method, HttpMethod::Patch);
3282 assert!(reqs[0]
3283 .url
3284 .contains("/api/v4/spot/orders/1?currency_pair=BTC_USDT"));
3285 let body = reqs[0].body.as_ref().unwrap();
3286 assert!(body.contains(r#""amount":"2""#));
3287 assert!(body.contains(r#""price":"101""#));
3288 }
3289
3290 #[test]
3293 fn futures_orders_carry_the_time_in_force_or_refuse_it() {
3294 let (gate, mock) = signed_futures_client(1000);
3295 mock.push_json(200, "{}");
3296 let ioc = OrderRequest::limit_buy(symbol(), dec!(1), dec!(100))
3297 .with_time_in_force(TimeInForce::Ioc);
3298 let _ = gate.place_order(&ioc);
3299 let body = mock.recorded_requests()[0].body.clone().unwrap_or_default();
3300 assert!(body.contains(r#""tif":"ioc""#));
3301
3302 let (gate, mock) = signed_futures_client(1000);
3303 mock.push_json(200, "{}");
3304 let poc = OrderRequest::limit_buy(symbol(), dec!(1), dec!(100)).post_only();
3305 let _ = gate.place_order(&poc);
3306 let body = mock.recorded_requests()[0].body.clone().unwrap_or_default();
3307 assert!(body.contains(r#""tif":"poc""#));
3308
3309 let (gate, mock) = signed_futures_client(1000);
3310 let fok = OrderRequest::market_buy(symbol(), dec!(1)).with_time_in_force(TimeInForce::Fok);
3311 let err = gate.place_order(&fok).unwrap_err();
3312 assert!(matches!(err, Error::Exchange { ref code, .. } if code == "unsupported"));
3313 assert_eq!(mock.recorded_requests(), Vec::new());
3314 }
3315
3316 #[test]
3317 fn place_batch_reads_succeeded_flag() {
3318 let (gate, mock) = signed_client(1_000_000);
3319 mock.push_json(
3320 200,
3321 r#"[{"succeeded":true,"id":"o1","text":"","side":"buy","type":"limit","status":"open",
3322 "amount":"1","filled_amount":"0","price":"100","avg_deal_price":"0"},
3323 {"succeeded":false,"label":"BALANCE_NOT_ENOUGH","message":"no funds"}]"#,
3324 );
3325 let results = gate
3326 .place_batch(&[
3327 OrderRequest::limit_buy(symbol(), dec!(1), dec!(100)),
3328 OrderRequest::limit_buy(symbol(), dec!(1), dec!(101)),
3329 ])
3330 .unwrap();
3331 assert_eq!(results[0].as_ref().unwrap().id, "o1");
3332 assert!(matches!(
3333 results[1].as_ref().unwrap_err(),
3334 Error::OrderRejected { .. }
3335 ));
3336 assert!(mock.recorded_requests()[0]
3337 .url
3338 .contains("/api/v4/spot/batch_orders"));
3339 }
3340
3341 #[test]
3342 fn cancel_batch_is_one_call() {
3343 let (gate, mock) = signed_client(1_000_000);
3344 mock.push_json(200, "[]");
3345 gate.cancel_batch(&symbol(), &["1".to_string(), "2".to_string()])
3346 .unwrap();
3347 let reqs = mock.recorded_requests();
3348 assert_eq!(reqs.len(), 1);
3349 assert!(reqs[0].url.contains("/api/v4/spot/cancel_batch_orders"));
3350 }
3351
3352 #[test]
3353 fn place_oco_is_unsupported() {
3354 let (mut gate, _mock) = signed_client(1_000_000);
3355 assert!(matches!(
3356 AdvancedOrders::place_oco(
3357 &mut gate,
3358 &OcoRequest::new(symbol(), OrderSide::Sell, dec!(1), dec!(110), dec!(95))
3359 )
3360 .unwrap_err(),
3361 Error::Exchange { .. }
3362 ));
3363 }
3364
3365 fn futures_client() -> (Gate, Arc<MockHttpTransport>) {
3366 let mock = Arc::new(MockHttpTransport::new());
3367 let opts = ExchangeOptions::mainnet(crate::MarketType::UsdMFutures);
3368 (
3369 Gate::with_http(Box::new(ArcTransport(Arc::clone(&mock))), &opts),
3370 mock,
3371 )
3372 }
3373
3374 #[test]
3375 fn futures_ticker_takes_bid_ask_from_the_book() {
3376 let (gate, mock) = futures_client();
3377 mock.push_json(
3378 200,
3379 r#"[{"contract":"BTC_USDT","last":"20000","volume_24h_base":"1234"}]"#,
3380 );
3381 mock.push_json(
3382 200,
3383 r#"{"id":66,"bids":[{"p":"19999","s":10}],"asks":[{"p":"20001","s":8}]}"#,
3384 );
3385 let ticker = gate.ticker(&symbol()).unwrap();
3386 assert_eq!(ticker.last, dec!(20000));
3387 assert_eq!(ticker.bid, dec!(19999));
3388 assert_eq!(ticker.ask, dec!(20001));
3389 assert_eq!(ticker.volume, dec!(1234));
3390 assert!(mock.recorded_requests()[0]
3391 .url
3392 .contains("/api/v4/futures/usdt/tickers?contract=BTC_USDT"));
3393 }
3394
3395 #[test]
3396 fn futures_klines_parse_object_rows() {
3397 let (gate, mock) = futures_client();
3398 mock.push_json(
3399 200,
3400 r#"[{"t":1700000000,"v":12,"o":"100","h":"110","l":"95","c":"105"}]"#,
3401 );
3402 let candles = gate.klines(&symbol(), "1h", 1).unwrap();
3403 assert!((candles[0].open - 100.0).abs() < 1e-9);
3404 assert!((candles[0].high - 110.0).abs() < 1e-9);
3405 assert!((candles[0].close - 105.0).abs() < 1e-9);
3406 assert_eq!(candles[0].timestamp, 1_700_000_000);
3407 assert!(mock.recorded_requests()[0]
3408 .url
3409 .contains("/api/v4/futures/usdt/candlesticks"));
3410 }
3411
3412 #[test]
3413 fn futures_order_book_parses_object_levels() {
3414 let (gate, mock) = futures_client();
3415 mock.push_json(
3416 200,
3417 r#"{"id":77,"bids":[{"p":"100","s":1}],"asks":[{"p":"101","s":2}]}"#,
3418 );
3419 let book = gate.order_book(&symbol(), 5).unwrap();
3420 assert_eq!(book.last_update_id, 77);
3421 assert_eq!(book.bids[0], BookLevel::new(dec!(100), dec!(1)));
3422 assert_eq!(book.asks[0], BookLevel::new(dec!(101), dec!(2)));
3423 }
3424
3425 fn hedged_futures_client(now_ms: i64) -> (Gate, Arc<MockHttpTransport>) {
3426 let mock = Arc::new(MockHttpTransport::new());
3427 let mut opts = ExchangeOptions::mainnet(crate::MarketType::UsdMFutures);
3428 opts.position_mode = crate::PositionMode::Hedge;
3429 let gate = Gate::with_credentials(
3430 Box::new(ArcTransport(Arc::clone(&mock))),
3431 &opts,
3432 Credentials::new("APIKEY", "SECRET"),
3433 )
3434 .with_clock(Box::new(move || now_ms));
3435 (gate, mock)
3436 }
3437
3438 const GATE_FUTURES_FILL: &str = r#"{"id":88,"size":2,"left":0,"price":"0",
3439 "fill_price":"20000","status":"finished","finish_as":"filled","text":""}"#;
3440
3441 #[test]
3442 fn dual_mode_closes_with_auto_size_and_opens_by_sign() {
3443 let (hedged, mock) = hedged_futures_client(1_000_000);
3447 for _ in 0..2 {
3448 mock.push_json(200, GATE_FUTURES_FILL);
3449 }
3450 hedged
3451 .place_order(&OrderRequest::market_sell(symbol(), dec!(2)).reduce_only())
3452 .unwrap();
3453 hedged
3454 .place_order(&OrderRequest::market_buy(symbol(), dec!(2)))
3455 .unwrap();
3456
3457 let reqs = mock.recorded_requests();
3458 let close = reqs[0].body.as_deref().unwrap();
3459 assert!(close.contains(r#""auto_size":"close_long""#));
3460 assert!(close.contains(r#""size":0"#));
3461 let open = reqs[1].body.as_deref().unwrap();
3462 assert!(open.contains(r#""size":2"#));
3463 assert!(!open.contains("auto_size"));
3464
3465 mock.push_json(200, GATE_FUTURES_FILL);
3467 hedged
3468 .place_order(&OrderRequest::market_buy(symbol(), dec!(2)).reduce_only())
3469 .unwrap();
3470 assert!(mock.recorded_requests()[2]
3471 .body
3472 .as_deref()
3473 .unwrap()
3474 .contains(r#""auto_size":"close_short""#));
3475
3476 let (one_way, one_way_mock) = signed_futures_client(1_000_000);
3478 one_way_mock.push_json(200, GATE_FUTURES_FILL);
3479 one_way
3480 .place_order(&OrderRequest::market_sell(symbol(), dec!(2)).reduce_only())
3481 .unwrap();
3482 let body = one_way_mock.recorded_requests()[0].body.clone().unwrap();
3483 assert!(body.contains(r#""reduce_only":true"#));
3484 assert!(body.contains(r#""size":-2"#));
3485 assert!(!body.contains("auto_size"));
3486 }
3487
3488 #[test]
3489 fn futures_market_order_uses_signed_size_and_ioc() {
3490 let (gate, mock) = signed_futures_client(1_000_000);
3491 mock.push_json(
3492 200,
3493 r#"{"id":88,"size":2,"left":0,"price":"0","fill_price":"20000",
3494 "status":"finished","finish_as":"filled","text":""}"#,
3495 );
3496 let order = gate
3497 .place_order(&OrderRequest::market_buy(symbol(), dec!(2)))
3498 .unwrap();
3499 assert_eq!(order.id, "88");
3500 assert_eq!(order.status, OrderStatus::Filled);
3501 assert_eq!(order.side, OrderSide::Buy);
3502 assert_eq!(order.average_price, Some(dec!(20000)));
3503 let req = &mock.recorded_requests()[0];
3504 assert!(req.url.contains("/api/v4/futures/usdt/orders"));
3505 let body = req.body.as_ref().unwrap();
3506 assert!(body.contains(r#""contract":"BTC_USDT""#));
3507 assert!(body.contains(r#""size":2"#));
3508 assert!(body.contains(r#""price":"0""#));
3509 assert!(body.contains(r#""tif":"ioc""#));
3510 assert!(body.contains(r#""reduce_only":false"#));
3511 }
3512
3513 #[test]
3514 fn futures_limit_sell_signs_size_negative() {
3515 let (gate, mock) = signed_futures_client(1_000_000);
3516 mock.push_json(
3517 200,
3518 r#"{"id":90,"size":-3,"left":3,"price":"21000","fill_price":"0",
3519 "status":"open","finish_as":"","text":""}"#,
3520 );
3521 let order = gate
3522 .place_order(&OrderRequest::limit_sell(symbol(), dec!(3), dec!(21000)))
3523 .unwrap();
3524 assert_eq!(order.side, OrderSide::Sell);
3525 assert_eq!(order.status, OrderStatus::New);
3526 assert_eq!(order.order_type, OrderType::Limit);
3527 let reqs = mock.recorded_requests();
3528 let body = reqs[0].body.as_ref().unwrap();
3529 assert!(body.contains(r#""size":-3"#));
3530 assert!(body.contains(r#""price":"21000""#));
3531 }
3532
3533 #[test]
3534 fn derivatives_positions_parse_cross_and_isolated() {
3535 let (mut gate, mock) = signed_futures_client(1_000_000);
3536 mock.push_json(
3537 200,
3538 r#"[{"contract":"BTC_USDT","size":3,"leverage":"10","entry_price":"20000",
3539 "mark_price":"20100","unrealised_pnl":"30"},
3540 {"contract":"ETH_USDT","size":-2,"leverage":"0","cross_leverage_limit":"5",
3541 "entry_price":"3000","mark_price":"2900","unrealised_pnl":"200"}]"#,
3542 );
3543 let positions = Derivatives::positions(&mut gate, None).unwrap();
3544 assert_eq!(positions.len(), 2);
3545 assert_eq!(positions[0].symbol, Symbol::new("BTC", "USDT"));
3546 assert_eq!(positions[0].side, PositionSide::Long);
3547 assert_eq!(positions[0].quantity, dec!(3));
3548 assert_eq!(positions[0].leverage, dec!(10));
3549 assert_eq!(positions[0].margin_mode, MarginMode::Isolated);
3550 assert_eq!(positions[1].side, PositionSide::Short);
3551 assert_eq!(positions[1].leverage, dec!(5));
3552 assert_eq!(positions[1].margin_mode, MarginMode::Cross);
3553 assert!(mock.recorded_requests()[0]
3554 .url
3555 .contains("/api/v4/futures/usdt/positions"));
3556 }
3557
3558 #[test]
3559 fn derivatives_set_leverage_and_cross_switch() {
3560 let (mut gate, mock) = signed_futures_client(1_000_000);
3561 mock.push_json(200, "{}");
3562 Derivatives::set_leverage(&mut gate, &symbol(), 5).unwrap();
3563 assert!(mock.recorded_requests()[0]
3564 .url
3565 .contains("/api/v4/futures/usdt/positions/BTC_USDT/leverage?leverage=5"));
3566
3567 mock.push_json(200, "{}");
3568 Derivatives::set_margin_mode(&mut gate, &symbol(), MarginMode::Cross).unwrap();
3569 assert!(mock.recorded_requests()[1]
3570 .url
3571 .contains("leverage?leverage=0"));
3572 }
3573
3574 #[test]
3575 fn derivatives_close_position_is_reduce_only_opposite() {
3576 let (mut gate, mock) = signed_futures_client(1_000_000);
3577 mock.push_json(
3578 200,
3579 r#"[{"contract":"BTC_USDT","size":3,"leverage":"10","entry_price":"20000",
3580 "mark_price":"20100","unrealised_pnl":"30"}]"#,
3581 );
3582 mock.push_json(
3583 200,
3584 r#"{"id":99,"size":-3,"left":0,"price":"0","fill_price":"20100",
3585 "status":"finished","finish_as":"filled","text":""}"#,
3586 );
3587 let order = Derivatives::close_position(&mut gate, &symbol()).unwrap();
3588 assert_eq!(order.status, OrderStatus::Filled);
3589 let reqs = mock.recorded_requests();
3590 let body = reqs[1].body.as_ref().unwrap();
3591 assert!(body.contains(r#""size":-3"#));
3592 assert!(body.contains(r#""reduce_only":true"#));
3593 }
3594
3595 #[test]
3596 fn futures_balances_split_total_and_available() {
3597 let (gate, mock) = signed_futures_client(1_000_000);
3598 mock.push_json(
3599 200,
3600 r#"{"currency":"USDT","total":"1000","available":"800"}"#,
3601 );
3602 let bals = gate.balances().unwrap();
3603 assert_eq!(bals[0].asset, "USDT");
3604 assert_eq!(bals[0].free, dec!(800));
3605 assert_eq!(bals[0].locked, dec!(200));
3606 assert!(mock.recorded_requests()[0]
3607 .url
3608 .contains("/api/v4/futures/usdt/accounts"));
3609 }
3610
3611 #[test]
3612 fn futures_query_order_uses_futures_endpoint_and_shape() {
3613 let (gate, mock) = signed_futures_client(1_000_000);
3614 mock.push_json(
3615 200,
3616 r#"{"id":88,"size":2,"left":0,"price":"100","fill_price":"100",
3617 "status":"finished","finish_as":"filled","text":""}"#,
3618 );
3619 let order = gate.query_order(&symbol(), "88").unwrap();
3620 assert_eq!(order.id, "88");
3621 assert_eq!(order.side, OrderSide::Buy);
3622 assert_eq!(order.status, OrderStatus::Filled);
3623 assert!(mock.recorded_requests()[0]
3624 .url
3625 .contains("/api/v4/futures/usdt/orders/88"));
3626 }
3627
3628 #[test]
3629 fn futures_cancel_order_targets_futures_endpoint() {
3630 let (gate, mock) = signed_futures_client(1_000_000);
3631 mock.push_json(
3632 200,
3633 r#"{"id":88,"size":0,"left":0,"status":"finished","finish_as":"cancelled"}"#,
3634 );
3635 gate.cancel_order(&symbol(), "88").unwrap();
3636 let req = &mock.recorded_requests()[0];
3637 assert_eq!(req.method, HttpMethod::Delete);
3638 assert!(req.url.contains("/api/v4/futures/usdt/orders/88"));
3639 }
3640
3641 #[test]
3642 fn futures_open_orders_parse_contract_shape() {
3643 let (gate, mock) = signed_futures_client(1_000_000);
3644 mock.push_json(
3645 200,
3646 r#"[{"id":90,"size":-3,"left":3,"price":"21000","fill_price":"0",
3647 "status":"open","finish_as":"","text":""}]"#,
3648 );
3649 let orders = gate.open_orders(Some(&symbol())).unwrap();
3650 assert_eq!(orders.len(), 1);
3651 assert_eq!(orders[0].side, OrderSide::Sell);
3652 assert_eq!(orders[0].status, OrderStatus::New);
3653 assert!(mock.recorded_requests()[0]
3654 .url
3655 .contains("/api/v4/futures/usdt/orders?contract=BTC_USDT&status=open"));
3656 }
3657
3658 #[test]
3659 fn ticker_parses() {
3660 let (gate, mock) = client();
3661 mock.push_json(
3662 200,
3663 r#"[{"currency_pair":"BTC_USDT","last":"20000","highest_bid":"19999",
3664 "lowest_ask":"20001","base_volume":"1234"}]"#,
3665 );
3666 let ticker = gate.ticker(&symbol()).unwrap();
3667 assert_eq!(ticker.last, dec!(20000));
3668 assert_eq!(ticker.bid, dec!(19999));
3669 let req = &mock.recorded_requests()[0];
3670 assert_eq!(
3671 req.url,
3672 "https://api.gateio.ws/api/v4/spot/tickers?currency_pair=BTC_USDT"
3673 );
3674 }
3675
3676 #[test]
3677 fn klines_field_order() {
3678 let (gate, mock) = client();
3679 mock.push_json(
3681 200,
3682 r#"[["1700000000","0","105","110","95","100","12","true"]]"#,
3683 );
3684 let candles = gate.klines(&symbol(), "1h", 1).unwrap();
3685 assert!((candles[0].open - 100.0).abs() < 1e-9);
3686 assert!((candles[0].high - 110.0).abs() < 1e-9);
3687 assert!((candles[0].close - 105.0).abs() < 1e-9);
3688 assert_eq!(candles[0].timestamp, 1_700_000_000);
3689 }
3690
3691 #[test]
3692 fn order_book_parses() {
3693 let (gate, mock) = client();
3694 mock.push_json(
3695 200,
3696 r#"{"update":66,"bids":[["100","1"]],"asks":[["101","2"]]}"#,
3697 );
3698 let book = gate.order_book(&symbol(), 5).unwrap();
3699 assert_eq!(book.last_update_id, 66);
3700 assert_eq!(book.bids[0], BookLevel::new(dec!(100), dec!(1)));
3701 }
3702
3703 #[test]
3704 fn http_error_maps_to_taxonomy() {
3705 let (gate, mock) = client();
3706 mock.push_json(400, r#"{"label":"BALANCE_NOT_ENOUGH","message":"nope"}"#);
3707 assert!(matches!(
3708 gate.ticker(&symbol()).unwrap_err(),
3709 Error::InsufficientBalance
3710 ));
3711 }
3712
3713 #[test]
3714 fn place_order_signs_with_sha512_body_hash() {
3715 let (gate, mock) = signed_client(1_000_000); mock.push_json(
3717 200,
3718 r#"{"id":"77","text":"t-abc","side":"buy","type":"limit","status":"open",
3719 "amount":"1","filled_amount":"0","price":"100","avg_deal_price":"0"}"#,
3720 );
3721 let order = gate
3722 .place_order(
3723 &OrderRequest::limit_buy(symbol(), dec!(1), dec!(100)).with_client_order_id("abc"),
3724 )
3725 .unwrap();
3726 assert_eq!(order.id, "77");
3727 assert_eq!(order.status, OrderStatus::New);
3728
3729 let req = &mock.recorded_requests()[0];
3730 let header = |name: &str| {
3731 req.headers
3732 .iter()
3733 .find(|(k, _)| k == name)
3734 .map(|(_, v)| v.as_str())
3735 .unwrap()
3736 };
3737 assert_eq!(header("Timestamp"), "1000");
3738 assert_eq!(header("KEY"), "APIKEY");
3739 let body = req.body.as_ref().unwrap();
3740 let body_hash = sha512_hex(body.as_bytes());
3741 let sign_string = format!("POST\n/api/v4/spot/orders\n\n{body_hash}\n1000");
3742 assert_eq!(
3743 header("SIGN"),
3744 hmac_sha512_hex(b"SECRET", sign_string.as_bytes())
3745 );
3746 assert!(body.contains(r#""text":"t-abc""#));
3747 }
3748
3749 #[test]
3750 fn query_and_balances() {
3751 let (gate, mock) = signed_client(1_000_000);
3752 mock.push_json(
3753 200,
3754 r#"{"id":"77","text":"","side":"sell","type":"market","status":"closed",
3755 "amount":"2","filled_amount":"2","price":"0","avg_deal_price":"100"}"#,
3756 );
3757 let order = gate.query_order(&symbol(), "77").unwrap();
3758 assert_eq!(order.status, OrderStatus::Filled);
3759 assert_eq!(order.average_price, Some(dec!(100)));
3760
3761 mock.push_json(
3762 200,
3763 r#"[{"currency":"USDT","available":"100.5","locked":"25.5"}]"#,
3764 );
3765 let bals = gate.balances().unwrap();
3766 assert_eq!(bals[0].total(), dec!(126));
3767 }
3768
3769 #[test]
3770 fn open_orders_requires_symbol() {
3771 let (gate, _mock) = signed_client(1_000_000);
3772 assert!(matches!(
3773 gate.open_orders(None).unwrap_err(),
3774 Error::InvalidOrder(_)
3775 ));
3776 }
3777
3778 #[test]
3779 fn signed_requires_credentials() {
3780 let (gate, _) = client();
3781 assert!(matches!(
3782 gate.balances().unwrap_err(),
3783 Error::InvalidCredentials(_)
3784 ));
3785 }
3786
3787 #[test]
3788 fn ws_parses_trade_and_book() {
3789 let ws = Arc::new(MockWsTransport::new());
3790 ws.push_connection(vec![
3791 Ok(Some(
3792 r#"{"time":1,"channel":"spot.trades","event":"update","result":
3793 {"currency_pair":"BTC_USDT","side":"buy","amount":"0.5","price":"100","create_time_ms":1700}}"#
3794 .to_string(),
3795 )),
3796 Ok(Some(
3797 r#"{"time":2,"channel":"spot.order_book_update","event":"update","result":
3798 {"s":"BTC_USDT","u":9,"b":[["100","1"]],"a":[["101","2"]]}}"#
3799 .to_string(),
3800 )),
3801 Ok(Some(r#"{"channel":"spot.trades","event":"subscribe"}"#.to_string())),
3802 ]);
3803 let http = Arc::new(MockHttpTransport::new());
3804 let opts = ExchangeOptions::mainnet(crate::MarketType::Spot);
3805 let mut gate = Gate::with_http(Box::new(ArcTransport(http)), &opts)
3806 .with_ws(Box::new(ArcWs(Arc::clone(&ws))));
3807 gate.subscribe_trades(&symbol()).unwrap();
3808 assert!(ws.sent()[0].contains(r#""channel":"spot.trades""#));
3809
3810 let events = gate.poll_events();
3811 assert_eq!(events.len(), 2);
3812 let Event::Trade(t) = &events[0] else {
3813 panic!("expected trade")
3814 };
3815 assert_eq!(t.aggressor, OrderSide::Buy);
3816 assert_eq!(t.timestamp, 1700);
3817 let Event::BookDelta(d) = &events[1] else {
3818 panic!("expected book delta")
3819 };
3820 assert_eq!(d.final_update_id, 9);
3821 }
3822
3823 #[test]
3824 fn works_as_a_boxed_exchange() {
3825 let (gate, mock) = signed_client(1_000_000);
3826 mock.push_json(
3827 200,
3828 r#"{"id":"1","text":"","side":"buy","type":"limit","status":"open",
3829 "amount":"1","filled_amount":"0","price":"100","avg_deal_price":"0"}"#,
3830 );
3831 let mut exchange: Box<dyn Exchange> = Box::new(gate);
3832 assert_eq!(exchange.name(), "gate");
3833 let order = exchange
3834 .place_order(&OrderRequest::limit_buy(symbol(), dec!(1), dec!(100)))
3835 .unwrap();
3836 assert_eq!(order.id, "1");
3837 }
3838
3839 #[test]
3840 fn system_clock_is_sane() {
3841 assert!(system_now_ms() > 1_600_000_000_000);
3842 }
3843
3844 #[test]
3848 fn debug_reports_state_without_credentials() {
3849 let (client, _http) = signed_client(1_700_000_000_000);
3850 let rendered = format!("{client:?}");
3851
3852 assert!(rendered.starts_with("Gate {"));
3853 assert!(rendered.contains("authenticated: true"));
3854 assert!(!rendered.contains("SECRET"));
3855 assert!(!rendered.contains("APIKEY"));
3856 }
3857
3858 #[test]
3865 fn sync_time_shifts_signed_timestamps() {
3866 let (mut gate, http) = signed_client(1_000_000);
3867
3868 http.push_json(200, r#"{"server_time":1004500}"#);
3870 let offset = gate.sync_time().expect("sync must succeed");
3871 assert_eq!(offset, 4_500, "the venue is 4.5 s ahead of this machine");
3872
3873 http.push_json(200, r"[]");
3874 gate.balances().expect("balances must succeed");
3875
3876 let requests = http.recorded_requests();
3877 let signed = requests.last().expect("a signed request was recorded");
3878 let header = signed
3879 .headers
3880 .iter()
3881 .find(|(name, _)| name == "Timestamp")
3882 .map(|(_, value)| value.as_str())
3883 .expect("signed request carries Timestamp");
3884 assert_eq!(header, "1004", "seconds of the venue's time, not ours");
3885 }
3886
3887 #[test]
3895 fn rate_limit_carries_the_venues_advised_wait() {
3896 let (gate, mock) = client();
3897 mock.push_response(
3898 crate::transport::HttpResponse::new(
3899 429,
3900 r#"{"label":"TOO_MANY_REQUESTS","message":"slow down"}"#,
3901 )
3902 .with_header("Retry-After", "2.5"),
3903 );
3904 let err = gate.ticker(&symbol()).unwrap_err();
3905 let wait = std::time::Duration::from_millis(2500);
3906 assert!(matches!(err, Error::RateLimited { retry_after: Some(d) } if d == wait));
3907 }
3908}