Forráskód Böngészése

添加kucoin现货参考交易(交易部分未通过)

gepangpang 2 éve
szülő
commit
599d8d055f

+ 2 - 1
standard/Cargo.toml

@@ -10,7 +10,8 @@ global = { path = "../global" }
 exchanges = { path = "../exchanges" }
 tokio = { version = "1.31.0", features = ["full"] }
 async-trait = "0.1.73"
-serde_json = "1.0.105"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0.104"
 rust_decimal = "1.32.0"
 rust_decimal_macros = "1.32.0"
 chrono = "0.4.30"

+ 5 - 0
standard/src/exchange.rs

@@ -8,6 +8,7 @@ use crate::gate_spot::GateSpot;
 use crate::gate_swap::GateSwap;
 use crate::kucoin_swap::KucoinSwap;
 use crate::bitget_spot::BitgetSpot;
+use crate::kucoin_spot::KucoinSpot;
 use crate::okx_swap::OkxSwap;
 
 /// 交易所交易模式枚举
@@ -23,6 +24,7 @@ pub enum ExchangeEnum {
     GateSwap,
     GateSpot,
     KucoinSwap,
+    KucoinSpot,
     OkxSwap,
     BitgetSpot,
 }
@@ -81,6 +83,9 @@ impl Exchange {
             ExchangeEnum::KucoinSwap => {
                 Box::new(KucoinSwap::new(symbol, is_colo, params, order_sender, error_sender).await)
             }
+            ExchangeEnum::KucoinSpot =>{
+                Box::new(KucoinSpot::new(symbol, is_colo, params, order_sender, error_sender).await)
+            }
             ExchangeEnum::OkxSwap => {
                 Box::new(OkxSwap::new(symbol, is_colo, params, order_sender, error_sender).await)
             }

+ 5 - 1
standard/src/handle_info.rs

@@ -6,7 +6,7 @@ use tracing::error;
 use exchanges::response_base::ResponseData;
 use global::public_params;
 use crate::exchange::ExchangeEnum;
-use crate::{Account, binance_handle, bitget_spot_handle, gate_handle, kucoin_handle, MarketOrder, okx_handle, Position, SpecialDepth, SpecialOrder, SpecialTicker};
+use crate::{Account, binance_handle, bitget_spot_handle, gate_handle, kucoin_handle, kucoin_spot_handle, MarketOrder, okx_handle, Position, SpecialDepth, SpecialOrder, SpecialTicker};
 
 #[allow(dead_code)]
 pub struct HandleSwapInfo;
@@ -137,6 +137,10 @@ impl HandleSwapInfo {
                 depth_asks = kucoin_handle::format_depth_items(res_data_json["asks"].clone());
                 depth_bids = kucoin_handle::format_depth_items(res_data_json["bids"].clone());
             }
+            ExchangeEnum::KucoinSpot => {
+                depth_asks = kucoin_spot_handle::format_depth_items(res_data_json["asks"].clone());
+                depth_bids = kucoin_spot_handle::format_depth_items(res_data_json["bids"].clone());
+            }
             ExchangeEnum::OkxSwap => {
                 depth_asks = okx_handle::format_depth_items(res_data_json[0]["asks"].clone());
                 depth_bids = okx_handle::format_depth_items(res_data_json[0]["bids"].clone());

+ 737 - 0
standard/src/kucoin_spot.rs

@@ -0,0 +1,737 @@
+use std::collections::{BTreeMap, HashMap};
+use std::io::{Error, ErrorKind};
+use std::str::FromStr;
+use tokio::sync::mpsc::Sender;
+use async_trait::async_trait;
+use futures::stream::FuturesUnordered;
+use futures::TryStreamExt;
+use rust_decimal::Decimal;
+use rust_decimal::prelude::FromPrimitive;
+use serde::{Deserialize, Serialize};
+use serde_json::json;
+use tracing::error;
+use exchanges::kucoin_spot_rest::KucoinSpotRest;
+use global::trace_stack::TraceStack;
+use crate::exchange::ExchangeEnum;
+use crate::{Account, Market, Order, OrderCommand, Platform, Position, Ticker, utils};
+
+/// Kucoin交易所账户信息请求数据结构
+/// 接口`"/api/v1/accounts"`;
+///
+/// struct SpotAccount
+/// - `id`: String, accountId
+/// - `currency`: String, 币种
+/// - `account_type`: String, 账户类型,资金(main)账户,现货交易(trade)账户,现货高频交易(trade_hf)账户,杠杆(margin)账户
+/// - `balance`: Decimal, 资金总额
+/// - `available`: Decimal, 可用余额
+/// - `holds`: Decimal, 冻结金额
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct SpotAccount {
+    id: String,
+    currency: String,
+    account_type: String,
+    balance: Decimal,
+    available: Decimal,
+    holds: Decimal,
+}
+
+impl SpotAccount {
+    fn new() -> SpotAccount {
+        SpotAccount {
+            id: "".to_string(),
+            currency: "".to_string(),
+            account_type: "".to_string(),
+            balance: Default::default(),
+            available: Default::default(),
+            holds: Default::default(),
+        }
+    }
+}
+
+/// Kucoin交易所Ticker信息请求数据结构
+/// 接口`"/api/v1/market/orderbook/level1"`;
+///
+/// struct SpotTicker
+/// - `sequence`: String, 序列号
+/// - `price`: Decimal, 最新成交价格
+/// - `size`: Decimal, 最新成交数量
+/// - `best_ask`: Decimal, 最佳卖一价
+/// - `best_ask_size`: Decimal, 最佳卖一数量
+/// - `best_bid`: Decimal, 最佳买一价
+/// - `best_bid_size`: Decimal, 最佳买一数量
+/// - `time`: i64, 时间戳
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct SpotTicker {
+    sequence: String,
+    price: Decimal,
+    size: Decimal,
+    best_ask: Decimal,
+    best_ask_size: Decimal,
+    best_bid: Decimal,
+    best_bid_size: Decimal,
+    time: i64,
+}
+
+/// Kucoin交易所Market信息请求数据结构
+/// 接口`"/api/v2/symbols"`;
+///
+/// struct SpotTicker
+/// - `symbol: String, 交易对唯一标识码
+/// - `name: String, 交易对名称
+/// - `base_currency: String, 商品货币
+/// - `quote_currency: String, 计价币种
+/// - `fee_currency: String, 交易计算手续费的币种
+/// - `market: String, 交易市场
+/// - `base_min_size: Decimal, 下单时size的最小值
+/// - `quote_min_size: Decimal, 下市价单,funds的最小值
+/// - `base_max_size: Decimal, 下单,size的最大值
+/// - `quote_max_size: Decimal, 下市价单,funds的最大值
+/// - `base_increment: Decimal, 数量增量,下单的size必须为数量增量的正整数倍
+/// - `quote_increment: Decimal, 市价单:资金增量,下单的funds必须为资金增量的正整数倍
+/// - `price_increment: Decimal, 限价单:价格增量,下单的price必须为价格增量的正整数倍
+/// - `price_limit_rate: Decimal, 价格保护阈值
+/// - `min_funds: Option<Decimal>, 最小交易金额
+/// - `is_margin_enabled: bool, 是否支持杠杆
+/// - `enable_trading: bool, 是否可以用于交易
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct SpotMarket {
+    symbol: String,
+    name: String,
+    base_currency: String,
+    quote_currency: String,
+    fee_currency: String,
+    market: String,
+    base_min_size: Decimal,
+    quote_min_size: Decimal,
+    base_max_size: Decimal,
+    quote_max_size: Decimal,
+    base_increment: Decimal,
+    quote_increment: Decimal,
+    price_increment: Decimal,
+    price_limit_rate: Decimal,
+    min_funds: Option<Decimal>,
+    is_margin_enabled: bool,
+    enable_trading: bool,
+}
+
+/// Kucoin交易所Order信息请求数据结构
+/// 接口`"/api/v1/orders/{orderId}"`;
+///
+/// struct SpotOrder
+/// - `id`: String,
+/// - `symbol`: String,
+/// - `op_type`: String,
+/// - `order_type`: String,
+/// - `side`: String,
+/// - `price`: Decimal,
+/// - `size`: Decimal,
+/// - `funds`: Decimal,
+/// - `deal_funds`: Decimal,
+/// - `deal_size`: Decimal,
+/// - `fee`: Decimal,
+/// - `fee_currency`: String,
+/// - `stp`: String,
+/// - `stop`: String,
+/// - `stop_triggered`: bool,
+/// - `stop_price`: Decimal,
+/// - `time_in_force`: String,
+/// - `post_only`: bool,
+/// - `hidden`: bool,
+/// - `iceberg`: bool,
+/// - `visible_size`: Decimal,
+/// - `cancel_after`: i64,
+/// - `channel`: String,
+/// - `client_oid`: String,
+/// - `remark`: String,
+/// - `tags`: String,
+/// - `is_active`: bool,
+/// - `cancel_exist`: bool,
+/// - `created_at`: i64,
+/// - `trade_type`: String,
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct SpotOrder {
+    id: String,
+    symbol: String,
+    op_type: String,
+    #[serde(rename = "type")]
+    order_type: String,
+    side: String,
+    price: Decimal,
+    size: Decimal,
+    funds: Decimal,
+    deal_funds: Decimal,
+    deal_size: Decimal,
+    fee: Decimal,
+    fee_currency: String,
+    stp: String,
+    stop: String,
+    stop_triggered: bool,
+    stop_price: Decimal,
+    time_in_force: String,
+    post_only: bool,
+    hidden: bool,
+    iceberg: bool,
+    visible_size: Decimal,
+    cancel_after: i64,
+    channel: String,
+    client_oid: String,
+    remark: String,
+    tags: String,
+    is_active: bool,
+    cancel_exist: bool,
+    created_at: i64,
+    trade_type: String,
+}
+
+#[allow(dead_code)]
+#[derive(Clone)]
+pub struct KucoinSpot {
+    exchange: ExchangeEnum,
+    symbol: String,
+    is_colo: bool,
+    params: BTreeMap<String, String>,
+    request: KucoinSpotRest,
+    market: Market,
+    order_sender: Sender<Order>,
+    error_sender: Sender<Error>,
+}
+
+impl KucoinSpot {
+    pub async fn new(symbol: String, is_colo: bool, params: BTreeMap<String, String>, order_sender: Sender<Order>, error_sender: Sender<Error>) -> KucoinSpot {
+        let market = Market::new();
+        let mut kucoin_spot = KucoinSpot {
+            exchange: ExchangeEnum::KucoinSpot,
+            symbol: symbol.to_uppercase(),
+            is_colo,
+            params: params.clone(),
+            request: KucoinSpotRest::new(is_colo, params.clone()),
+            market,
+            order_sender,
+            error_sender,
+        };
+        kucoin_spot.market = KucoinSpot::get_market(&mut kucoin_spot).await.unwrap_or(kucoin_spot.market);
+
+        return kucoin_spot;
+    }
+}
+
+#[async_trait]
+impl Platform for KucoinSpot {
+    // 克隆方法
+    fn clone_box(&self) -> Box<dyn Platform + Send + Sync> { Box::new(self.clone()) }
+    fn get_self_exchange(&self) -> ExchangeEnum { ExchangeEnum::KucoinSpot }
+    // 获取交易对
+    fn get_self_symbol(&self) -> String { self.symbol.clone() }
+    // 获取是否使用高速通道
+    fn get_self_is_colo(&self) -> bool {
+        self.is_colo
+    }
+    // 获取params信息
+    fn get_self_params(&self) -> BTreeMap<String, String> {
+        self.params.clone()
+    }
+    // 获取market信息
+    fn get_self_market(&self) -> Market { self.market.clone() }
+    // 获取请求时间
+    fn get_request_delays(&self) -> Vec<i64> { self.request.get_delays() }
+    // 获取请求平均时间
+    fn get_request_avg_delay(&self) -> Decimal { self.request.get_avg_delay() }
+    // 获取请求最大时间
+    fn get_request_max_delay(&self) -> i64 { self.request.get_max_delay() }
+    // 获取服务器时间
+    async fn get_server_time(&mut self) -> Result<String, Error> {
+        let res_data = self.request.get_server_time().await;
+        if res_data.code == "200" {
+            let res_data_str = &res_data.data;
+            let result = res_data_str.clone();
+            Ok(result)
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    async fn get_account(&mut self) -> Result<Account, Error> {
+        let coin_array: Vec<&str> = self.symbol.split("_").collect();
+        let res_data = self.request.get_accounts(coin_array[1].to_string()).await;
+        if res_data.code == "200" {
+            let res_data_str = &res_data.data;
+            let balance_info_list: Vec<SpotAccount> = serde_json::from_str(res_data_str).unwrap();
+            let mut balance_info_default = SpotAccount::new();
+            balance_info_default.currency = coin_array[1].to_string();
+            let balance_info = balance_info_list.iter().find(|&item| item.currency == coin_array[1].to_string()).unwrap_or(&balance_info_default);
+            let mut stocks_info_default = SpotAccount::new();
+            stocks_info_default.currency = coin_array[0].to_string();
+            let stocks_info = balance_info_list.iter().find(|&item| item.currency == coin_array[0].to_string()).unwrap_or(&balance_info_default);
+            let result = Account {
+                coin: format!("{}_{}", balance_info.currency, stocks_info.currency),
+                balance: balance_info.available + balance_info.holds,
+                available_balance: balance_info.available,
+                frozen_balance: balance_info.holds,
+                stocks: stocks_info.available + stocks_info.holds,
+                available_stocks: stocks_info.available,
+                frozen_stocks: stocks_info.holds,
+            };
+            Ok(result)
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    async fn get_spot_account(&mut self) -> Result<Vec<Account>, Error> {
+        let res_data = self.request.get_accounts("".to_string()).await;
+        if res_data.code == "200" {
+            let res_data_str = &res_data.data;
+            let balance_info_list: Vec<SpotAccount> = serde_json::from_str(res_data_str).unwrap();
+            let result = balance_info_list.iter().map(|item| {
+                Account {
+                    coin: item.currency.to_string(),
+                    balance: item.available + item.holds,
+                    available_balance: item.available,
+                    frozen_balance: item.holds,
+                    stocks: Decimal::ZERO,
+                    available_stocks: Decimal::ZERO,
+                    frozen_stocks: Decimal::ZERO,
+                }
+            }).collect();
+            Ok(result)
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    async fn get_position(&mut self) -> Result<Vec<Position>, Error> {
+        Err(Error::new(ErrorKind::NotFound, "kucoin_spot:该交易所方法未实现".to_string()))
+    }
+
+    async fn get_positions(&mut self) -> Result<Vec<Position>, Error> {
+        Err(Error::new(ErrorKind::NotFound, "kucoin_spot:该交易所方法未实现".to_string()))
+    }
+
+    async fn get_ticker(&mut self) -> Result<Ticker, Error> {
+        let symbol_format = utils::format_symbol(self.symbol.clone(), "-");
+        let res_data = self.request.get_level1(symbol_format).await;
+        if res_data.code == "200" {
+            let res_data_str = &res_data.data;
+            let ticker_info: SpotTicker = serde_json::from_str(res_data_str).unwrap();
+            let result = Ticker {
+                time: ticker_info.time,
+                high: ticker_info.best_ask,
+                low: ticker_info.best_bid,
+                sell: ticker_info.best_ask,
+                buy: ticker_info.best_bid,
+                last: ticker_info.price,
+                volume: ticker_info.size,
+            };
+            Ok(result)
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    async fn get_ticker_symbol(&mut self, symbol: String) -> Result<Ticker, Error> {
+        let symbol_format = utils::format_symbol(symbol.clone(), "-");
+        let res_data = self.request.get_level1(symbol_format).await;
+        if res_data.code == "200" {
+            let res_data_str = &res_data.data;
+            let ticker_info: SpotTicker = serde_json::from_str(res_data_str).unwrap();
+            let result = Ticker {
+                time: ticker_info.time,
+                high: ticker_info.best_ask,
+                low: ticker_info.best_bid,
+                sell: ticker_info.best_ask,
+                buy: ticker_info.best_bid,
+                last: ticker_info.price,
+                volume: ticker_info.size,
+            };
+            Ok(result)
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    async fn get_market(&mut self) -> Result<Market, Error> {
+        let symbol_format = utils::format_symbol(self.symbol.clone(), "-");
+        let res_data = self.request.get_symbols().await;
+        if res_data.code == "200" {
+            let res_data_str = &res_data.data;
+            let market_info_list: Vec<SpotMarket> = serde_json::from_str(res_data_str).unwrap();
+            let market_info = market_info_list.iter().find(|&item| item.symbol == symbol_format).unwrap();
+            let result = Market {
+                symbol: market_info.symbol.replace("-", "_"),
+                base_asset: market_info.base_currency.clone(),
+                quote_asset: market_info.quote_currency.clone(),
+                tick_size: market_info.price_increment,
+                amount_size: market_info.base_increment,
+                price_precision: Decimal::from_u32(market_info.price_increment.scale()).unwrap(),
+                amount_precision: Decimal::from_u32(market_info.base_increment.scale()).unwrap(),
+                min_qty: market_info.base_min_size,
+                max_qty: market_info.base_max_size,
+                min_notional: market_info.price_increment * market_info.base_min_size,
+                max_notional: market_info.price_increment * market_info.base_max_size,
+                ct_val: Decimal::ONE,
+            };
+            Ok(result)
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    async fn get_market_symbol(&mut self, symbol: String) -> Result<Market, Error> {
+        let symbol_format = utils::format_symbol(symbol.clone(), "-");
+        let res_data = self.request.get_symbols().await;
+        if res_data.code == "200" {
+            let res_data_str = &res_data.data;
+            let market_info_list: Vec<SpotMarket> = serde_json::from_str(res_data_str).unwrap();
+            let market_info = market_info_list.iter().find(|&item| item.symbol == symbol_format).unwrap();
+            let result = Market {
+                symbol: market_info.symbol.replace("-", "_"),
+                base_asset: market_info.base_currency.clone(),
+                quote_asset: market_info.quote_currency.clone(),
+                tick_size: market_info.price_increment,
+                amount_size: market_info.base_increment,
+                price_precision: Decimal::from_u32(market_info.price_increment.scale()).unwrap(),
+                amount_precision: Decimal::from_u32(market_info.base_increment.scale()).unwrap(),
+                min_qty: market_info.base_min_size,
+                max_qty: market_info.base_max_size,
+                min_notional: market_info.price_increment * market_info.base_min_size,
+                max_notional: market_info.price_increment * market_info.base_max_size,
+                ct_val: Decimal::ONE,
+            };
+            Ok(result)
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    async fn get_order_detail(&mut self, order_id: &str, custom_id: &str) -> Result<Order, Error> {
+        let res_data = if order_id != "" { self.request.get_order_by_order_id(order_id.to_string()).await } else { self.request.get_order_by_client_id(custom_id.to_string()).await };
+        if res_data.code == "200" {
+            let res_data_str = &res_data.data;
+            let result = format_order_item(res_data_str.clone());
+            Ok(result)
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    async fn get_orders_list(&mut self, _status: &str) -> Result<Vec<Order>, Error> {
+        let res_data = self.request.get_order().await;
+        if res_data.code == "200" {
+            let res_data_str = &res_data.data;
+            let order_info_list: Vec<String> = serde_json::from_str(res_data_str).unwrap();
+            let result = order_info_list.iter().map(|item| format_order_item(item.clone())).collect();
+            Ok(result)
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    async fn take_order(&mut self, custom_id: &str, origin_side: &str, price: Decimal, amount: Decimal) -> Result<Order, Error> {
+        let symbol_format = utils::format_symbol(self.symbol.clone(), "-");
+        let mut params = json!({
+            "symbol": symbol_format.to_string(),
+            "clientOid": custom_id,
+            "price": price.to_string(),
+            "size": amount.to_string()
+        });
+        if price.eq(&Decimal::ZERO) {
+            params["type"] = json!("market");
+        } else {
+            params["type"] = json!("limit");
+        };
+        match origin_side {
+            "kd" => {
+                params["side"] = json!("buy");
+            }
+            "pd" => {
+                params["side"] = json!("sell");
+            }
+            "kk" => {
+                params["side"] = json!("sell");
+            }
+            "pk" => {
+                params["side"] = json!("buy");
+            }
+            _ => { error!("下单参数错误"); }
+        };
+        let res_data = self.request.spot_order(params).await;
+        if res_data.code == "200" {
+            let res_data_str = &res_data.data;
+            let res_data_json: serde_json::Value = serde_json::from_str(res_data_str).unwrap();
+            let result = Order {
+                id: res_data_json["orderId"].as_str().unwrap().to_string(),
+                custom_id: custom_id.to_string(),
+                price: Decimal::ZERO,
+                amount: Decimal::ZERO,
+                deal_amount: Decimal::ZERO,
+                avg_price: Decimal::ZERO,
+                status: "NEW".to_string(),
+                order_type: "".to_string(),
+                trace_stack: TraceStack::default().on_special("550 kucoin_spot".to_string()),
+            };
+            Ok(result)
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    async fn take_order_symbol(&mut self, symbol: String, ct_val: Decimal, custom_id: &str, origin_side: &str, price: Decimal, amount: Decimal) -> Result<Order, Error> {
+        let symbol_format = utils::format_symbol(symbol.clone(), "-");
+        let mut params = json!({
+            "symbol": symbol_format.to_string(),
+            "clientOid": custom_id,
+            "price": price.to_string(),
+            "size": amount * ct_val,
+        });
+        if price.eq(&Decimal::ZERO) {
+            params["type"] = json!("market");
+        } else {
+            params["type"] = json!("limit");
+        };
+        match origin_side {
+            "kd" => {
+                params["side"] = json!("buy");
+            }
+            "pd" => {
+                params["side"] = json!("sell");
+            }
+            "kk" => {
+                params["side"] = json!("sell");
+            }
+            "pk" => {
+                params["side"] = json!("buy");
+            }
+            _ => { error!("下单参数错误"); }
+        };
+        let res_data = self.request.spot_order(params).await;
+        if res_data.code == "200" {
+            let res_data_str = &res_data.data;
+            let res_data_json: serde_json::Value = serde_json::from_str(res_data_str).unwrap();
+            let result = Order {
+                id: res_data_json["orderId"].as_str().unwrap().to_string(),
+                custom_id: custom_id.to_string(),
+                price: Decimal::ZERO,
+                amount: Decimal::ZERO,
+                deal_amount: Decimal::ZERO,
+                avg_price: Decimal::ZERO,
+                status: "NEW".to_string(),
+                order_type: "".to_string(),
+                trace_stack: TraceStack::default().on_special("599 kucoin_spot".to_string()),
+            };
+            Ok(result)
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    async fn cancel_order(&mut self, order_id: &str, custom_id: &str) -> Result<Order, Error> {
+        let res_data = if order_id != "" { self.request.cancel_order_by_order_id(order_id.to_string()).await } else { self.request.cancel_order_by_client_id(custom_id.to_string()).await };
+        if res_data.code == "200" {
+            let res_data_str = &res_data.data;
+            let order_info: serde_json::Value = serde_json::from_str(res_data_str).unwrap();
+            let id = if order_id != "" { order_info["cancelledOrderIds"][0].as_str().unwrap().to_string() } else { order_info["cancelledOrderId"].as_str().unwrap().to_string() };
+            let custom_id = if order_id != "" { "".to_string() } else { order_info["clientOid"].as_str().unwrap().to_string() };
+            let result = Order {
+                id,
+                custom_id,
+                price: Decimal::ZERO,
+                amount: Decimal::ZERO,
+                deal_amount: Decimal::ZERO,
+                avg_price: Decimal::ZERO,
+                status: "REMOVE".to_string(),
+                order_type: "".to_string(),
+                trace_stack: TraceStack::default().on_special("623 kucoin_spot".to_string()),
+            };
+            Ok(result)
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    async fn cancel_orders(&mut self) -> Result<Vec<Order>, Error> {
+        let symbol_format = utils::format_symbol(self.symbol.clone(), "-");
+        let res_data = self.request.cancel_order_all(symbol_format).await;
+        if res_data.code == "200" {
+            let res_data_str = &res_data.data;
+            let order_info: serde_json::Value = serde_json::from_str(res_data_str).unwrap();
+            let id_list = order_info["cancelledOrderIds"].as_array().unwrap();
+            let result = id_list.iter().map(|item| Order {
+                id: item["id"].as_str().unwrap().to_string(),
+                custom_id: "".to_string(),
+                price: Decimal::ZERO,
+                amount: Decimal::ZERO,
+                deal_amount: Decimal::ZERO,
+                avg_price: Decimal::ZERO,
+                status: "REMOVE".to_string(),
+                order_type: "".to_string(),
+                trace_stack: TraceStack::default().on_special("647 kucoin_spot".to_string()),
+            }).collect();
+            Ok(result)
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    async fn cancel_orders_all(&mut self) -> Result<Vec<Order>, Error> {
+        let res_data = self.request.cancel_order_all("".to_string()).await;
+        if res_data.code == "200" {
+            let res_data_str = &res_data.data;
+            let order_info: serde_json::Value = serde_json::from_str(res_data_str).unwrap();
+            let id_list = order_info["cancelledOrderIds"].as_array().unwrap();
+            let result = id_list.iter().map(|item| Order {
+                id: item["id"].as_str().unwrap().to_string(),
+                custom_id: "".to_string(),
+                price: Decimal::ZERO,
+                amount: Decimal::ZERO,
+                deal_amount: Decimal::ZERO,
+                avg_price: Decimal::ZERO,
+                status: "REMOVE".to_string(),
+                order_type: "".to_string(),
+                trace_stack: TraceStack::default().on_special("670 kucoin_spot".to_string()),
+            }).collect();
+            Ok(result)
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    async fn set_dual_mode(&mut self, _coin: &str, _is_dual_mode: bool) -> Result<String, Error> {
+        Err(Error::new(ErrorKind::NotFound, "kucoin_spot:该交易所方法未实现".to_string()))
+    }
+
+    async fn set_dual_leverage(&mut self, _leverage: &str) -> Result<String, Error> {
+        Err(Error::new(ErrorKind::NotFound, "kucoin_spot:该交易所方法未实现".to_string()))
+    }
+
+    async fn set_auto_deposit_status(&mut self, _status: bool) -> Result<String, Error> {
+        Err(Error::new(ErrorKind::NotFound, "kucoin_spot:该交易所方法未实现".to_string()))
+    }
+
+    async fn wallet_transfers(&mut self, _coin: &str, _from: &str, _to: &str, _amount: Decimal) -> Result<String, Error> {
+        Err(Error::new(ErrorKind::NotFound, "kucoin_spot:该交易所方法未实现".to_string()))
+    }
+
+    async fn command_order(&mut self, order_command: OrderCommand, trace_stack: TraceStack) {
+        let mut handles = vec![];
+        // 撤销订单
+        let cancel = order_command.cancel;
+        for item in cancel.keys() {
+            let mut self_clone = self.clone();
+            let cancel_clone = cancel.clone();
+            let item_clone = item.clone();
+            let order_id = cancel_clone[&item_clone].get(1).unwrap_or(&"".to_string()).clone();
+            let custom_id = cancel_clone[&item_clone].get(0).unwrap_or(&"".to_string()).clone();
+            let result_sd = self.order_sender.clone();
+            let err_sd = self.error_sender.clone();
+            let handle = tokio::spawn(async move {
+                let result = self_clone.cancel_order(&order_id, &custom_id).await;
+                match result {
+                    Ok(_) => {
+                        // result_sd.send(result).await.unwrap();
+                    }
+                    Err(error) => {
+                        // 取消失败去查订单。
+                        let query_rst = self_clone.get_order_detail(&order_id, &custom_id).await;
+                        match query_rst {
+                            Ok(order) => {
+                                result_sd.send(order).await.unwrap();
+                            }
+                            Err(_query_err) => {
+                                // error!(?_query_err);
+                                // error!("撤单失败,而且查单也失败了,bitget_spot,oid={}, cid={}。", order_id.clone(), custom_id.clone());
+                            }
+                        }
+                        err_sd.send(error).await.unwrap();
+                    }
+                }
+            });
+            handles.push(handle)
+        }
+        // 下单指令
+        let mut limits = HashMap::new();
+        limits.extend(order_command.limits_open);
+        limits.extend(order_command.limits_close);
+        for item in limits.keys() {
+            let mut self_clone = self.clone();
+            let limits_clone = limits.clone();
+            let item_clone = item.clone();
+            let result_sd = self.order_sender.clone();
+            let err_sd = self.error_sender.clone();
+            let mut ts = trace_stack.clone();
+
+            let handle = tokio::spawn(async move {
+                let value = limits_clone[&item_clone].clone();
+                let amount = Decimal::from_str(value.get(0).unwrap_or(&"0".to_string())).unwrap();
+                let side = value.get(1).unwrap();
+                let price = Decimal::from_str(value.get(2).unwrap_or(&"0".to_string())).unwrap();
+                let cid = value.get(3).unwrap();
+
+                //  order_name: [数量,方向,价格,c_id]
+                let result = self_clone.take_order(cid, side, price, amount).await;
+                match result {
+                    Ok(mut result) => {
+                        // 记录此订单完成时间
+                        ts.on_after_send();
+                        result.trace_stack = ts;
+
+                        result_sd.send(result).await.unwrap();
+                    }
+                    Err(error) => {
+                        let mut err_order = Order::new();
+                        err_order.custom_id = cid.clone();
+                        err_order.status = "REMOVE".to_string();
+
+                        result_sd.send(err_order).await.unwrap();
+                        err_sd.send(error).await.unwrap();
+                    }
+                }
+            });
+            handles.push(handle)
+        }
+        // 检查订单指令
+        let check = order_command.check;
+        for item in check.keys() {
+            let mut self_clone = self.clone();
+            let check_clone = check.clone();
+            let item_clone = item.clone();
+            let order_id = check_clone[&item_clone].get(1).unwrap_or(&"".to_string()).clone();
+            let custom_id = check_clone[&item_clone].get(0).unwrap_or(&"".to_string()).clone();
+            let result_sd = self.order_sender.clone();
+            let err_sd = self.error_sender.clone();
+            let handle = tokio::spawn(async move {
+                let result = self_clone.get_order_detail(&order_id, &custom_id).await;
+                match result {
+                    Ok(result) => {
+                        result_sd.send(result).await.unwrap();
+                    }
+                    Err(error) => {
+                        err_sd.send(error).await.unwrap();
+                    }
+                }
+            });
+            handles.push(handle)
+        }
+
+        let futures = FuturesUnordered::from_iter(handles);
+        let _: Result<Vec<_>, _> = futures.try_collect().await;
+    }
+}
+
+pub fn format_order_item(data: String) -> Order {
+    let order_info: SpotOrder = serde_json::from_str(&data).unwrap();
+    Order {
+        id: order_info.id,
+        custom_id: order_info.client_oid,
+        price: order_info.price,
+        amount: order_info.size,
+        deal_amount: order_info.deal_size,
+        avg_price: order_info.deal_funds / order_info.deal_size,
+        status: if order_info.is_active { "NEW".to_string() } else { "REMOVE".to_string() },
+        order_type: order_info.order_type,
+        trace_stack: TraceStack::default().on_special("811 kucoin_spot".to_string()),
+    }
+}

+ 131 - 0
standard/src/kucoin_spot_handle.rs

@@ -0,0 +1,131 @@
+use std::str::FromStr;
+use rust_decimal::Decimal;
+use rust_decimal_macros::dec;
+use serde_json::json;
+use tracing::trace;
+use exchanges::response_base::ResponseData;
+use global::trace_stack::TraceStack;
+use crate::{Account, MarketOrder, Order, SpecialDepth, SpecialOrder, SpecialTicker};
+use crate::exchange::ExchangeEnum;
+use crate::handle_info::HandleSwapInfo;
+
+// 处理账号信息
+pub fn handle_account_info(res_data: ResponseData, symbol: String) -> Account {
+    let symbol_upper = symbol.to_uppercase();
+    let symbol_array: Vec<&str> = symbol_upper.split("_").collect();
+    let res_data_str = res_data.data;
+    let res_data_json: Vec<serde_json::Value> = serde_json::from_str(&res_data_str).unwrap();
+    let balance_info_default = json!({"available":"0","currency": symbol_array[1],"hold":"0"});
+    let balance_info = res_data_json.iter().find(|&item| item["currency"].as_str().unwrap() == symbol_array[1]).unwrap_or(&balance_info_default);
+    let stocks_info_default = json!({"available":"0","currency": symbol_array[0],"hold":"0"});
+    let stocks_info = res_data_json.iter().find(|&item| item["currency"].as_str().unwrap() == symbol_array[0]).unwrap_or(&stocks_info_default);
+    format_account_info(balance_info.clone(), stocks_info.clone())
+}
+
+pub fn format_account_info(balance_data: serde_json::Value, stocks_data: serde_json::Value) -> Account {
+    let balance_coin = balance_data["currency"].as_str().unwrap().to_string().to_uppercase();
+    let available_balance = Decimal::from_str(balance_data["available"].as_str().unwrap()).unwrap();
+    let frozen_balance = Decimal::from_str(balance_data["hold"].as_str().unwrap()).unwrap();
+    let balance = available_balance + frozen_balance;
+
+    let stocks_coin = stocks_data["currency"].as_str().unwrap().to_string().to_uppercase();
+    let available_stocks = Decimal::from_str(stocks_data["available"].as_str().unwrap()).unwrap();
+    let frozen_stocks = Decimal::from_str(stocks_data["hold"].as_str().unwrap()).unwrap();
+    let stocks = available_stocks + frozen_stocks;
+
+    Account {
+        coin: format!("{}_{}", stocks_coin, balance_coin),
+        balance,
+        available_balance,
+        frozen_balance,
+        stocks,
+        available_stocks,
+        frozen_stocks,
+    }
+}
+
+// 处理order信息
+pub fn handle_order(res_data: ResponseData, ct_val: Decimal) -> SpecialOrder {
+    let res_data_str = res_data.data;
+    let res_data_json: Vec<serde_json::Value> = serde_json::from_str(&*res_data_str).unwrap();
+    let mut order_info = Vec::new();
+    for item in res_data_json.iter() {
+        order_info.push(format_order_item(item.clone(), ct_val));
+    }
+    trace!(?order_info);
+    SpecialOrder {
+        name: res_data.label,
+        order: order_info,
+    }
+}
+
+// 处理订单信息
+pub fn format_order_item(order: serde_json::Value, ct_val: Decimal) -> Order {
+    let price = Decimal::from_str(order["price"].as_str().unwrap()).unwrap();
+    let size = Decimal::from_str(order["size"].as_str().unwrap()).unwrap();
+    let status = order["status"].as_str().unwrap_or("");
+    let filled_size = Decimal::from_str(order["filledSize"].as_str().unwrap()).unwrap();
+
+    let avg_price = price;
+
+    let amount = size * ct_val;
+    let deal_amount = filled_size * ct_val;
+    let custom_status = if ["done"].contains(&status) {
+        "REMOVE".to_string()
+    } else if ["open", "match", "update"].contains(&status) {
+        "NEW".to_string()
+    } else {
+        "NULL".to_string()
+    };
+    Order {
+        id: order["orderId"].as_str().unwrap().to_string(),
+        custom_id: order["clientOid"].as_str().unwrap().to_string(),
+        price,
+        amount,
+        deal_amount,
+        avg_price,
+        status: custom_status,
+        order_type: order["orderType"].as_str().unwrap().to_string(),
+        trace_stack: TraceStack::default().on_special("89 bitget_spot_handle".to_string()),
+    }
+}
+
+// 处理特殊深度数据
+pub fn handle_special_depth(res_data: ResponseData) -> SpecialDepth {
+    HandleSwapInfo::handle_special_depth(ExchangeEnum::KucoinSpot, res_data)
+}
+
+// 格式化深度信息
+pub fn format_depth_items(value: serde_json::Value) -> Vec<MarketOrder> {
+    let mut depth_items: Vec<MarketOrder> = vec![];
+    for value in value.as_array().unwrap() {
+        depth_items.push(MarketOrder {
+            price: Decimal::from_str(value[0].as_str().unwrap()).unwrap(),
+            amount: Decimal::from_str(value[1].as_str().unwrap()).unwrap(),
+        })
+    }
+    return depth_items;
+}
+
+// 处理特殊Ticker信息
+pub fn handle_special_ticker(res_data: ResponseData) -> SpecialDepth {
+    let res_data_str = res_data.data;
+    let res_data_json: serde_json::Value = serde_json::from_str(&*res_data_str).unwrap();
+    format_special_ticker(res_data_json, res_data.label)
+}
+
+pub fn format_special_ticker(data: serde_json::Value, label: String) -> SpecialDepth {
+    let bp = Decimal::from_str(data["bestBid"].as_str().unwrap()).unwrap();
+    let bq = Decimal::from_str(data["bestBidSize"].as_str().unwrap()).unwrap();
+    let ap = Decimal::from_str(data["bestAsk"].as_str().unwrap()).unwrap();
+    let aq = Decimal::from_str(data["bestAskSize"].as_str().unwrap()).unwrap();
+    let mp = (bp + ap) * dec!(0.5);
+
+    let ticker_info = SpecialTicker { sell: ap, buy: bp, mid_price: mp };
+    let depth_info = vec![bp, bq, ap, aq];
+    SpecialDepth {
+        name: label,
+        depth: depth_info,
+        ticker: ticker_info,
+    }
+}

+ 2 - 0
standard/src/lib.rs

@@ -27,6 +27,8 @@ mod okx_swap;
 pub mod okx_handle;
 mod bitget_spot;
 pub mod bitget_spot_handle;
+mod kucoin_spot;
+pub mod kucoin_spot_handle;
 
 /// 持仓模式枚举
 /// - `Both`:单持仓方向

+ 95 - 15
standard/tests/exchange_test.rs

@@ -8,8 +8,8 @@ use rust_decimal_macros::dec;
 use tokio::sync::mpsc::{channel, Receiver, Sender};
 use tokio::try_join;
 use tracing::{error, trace};
-use exchanges::bitget_spot_ws::{BitgetSpotWs, BitgetSubscribeType, BitgetWsType};
-// use exchanges::kucoin_spot_ws::KucoinSubscribeType;
+// use exchanges::bitget_spot_ws::{BitgetSpotWs, BitgetSubscribeType, BitgetWsType};
+use exchanges::kucoin_spot_ws::{KucoinSpotWs, KucoinSubscribeType, KucoinWsType};
 // use exchanges::binance_spot_ws::{BinanceSpotSubscribeType, BinanceSpotWs, BinanceSpotWsType};
 // use exchanges::okx_swap_ws::{OkxSubscribeType, OkxSwapWs, OkxWsType};
 // use exchanges::kucoin_swap_ws::{KucoinSubscribeType, KucoinSwapWs, KucoinWsType};
@@ -18,7 +18,8 @@ use standard::exchange::{Exchange, ExchangeEnum};
 // use standard::{binance_handle, Order, Platform, utils};
 // use standard::{okx_handle, Order, Platform, utils};
 // use standard::{kucoin_handle, Order, Platform, utils};
-use standard::{bitget_spot_handle, Order, Platform, utils};
+use standard::{kucoin_spot_handle, Order, Platform, utils};
+// use standard::{bitget_spot_handle, Order, Platform, utils};
 
 // 创建实体
 #[allow(dead_code)]
@@ -69,6 +70,16 @@ pub async fn test_new_exchange(exchange: ExchangeEnum, symbol: &str) -> Box<dyn
             params.insert("pass_key".to_string(), pass_key);
             Exchange::new(exchange, symbol.to_string(), false, params, order_sender, error_sender).await
         }
+        ExchangeEnum::KucoinSpot => {
+            let mut params: BTreeMap<String, String> = BTreeMap::new();
+            let access_key = env::var("kucoin_access_key").unwrap_or("".to_string());
+            let secret_key = env::var("kucoin_secret_key").unwrap_or("".to_string());
+            let pass_key = env::var("kucoin_pass_key").unwrap_or("".to_string());
+            params.insert("access_key".to_string(), access_key);
+            params.insert("secret_key".to_string(), secret_key);
+            params.insert("pass_key".to_string(), pass_key);
+            Exchange::new(exchange, symbol.to_string(), false, params, order_sender, error_sender).await
+        }
         ExchangeEnum::OkxSwap => {
             let mut params: BTreeMap<String, String> = BTreeMap::new();
             let access_key = env::var("okx_access_key").unwrap_or("".to_string());
@@ -93,7 +104,7 @@ pub async fn test_new_exchange(exchange: ExchangeEnum, symbol: &str) -> Box<dyn
 }
 
 #[allow(dead_code)]
-pub async fn test_new_exchange_wss<T>(exchange: ExchangeEnum, symbol: &str, subscriber_type: T, mold: &str) where Vec<BitgetSubscribeType>: From<T> {
+pub async fn test_new_exchange_wss<T>(exchange: ExchangeEnum, symbol: &str, subscriber_type: T, mold: &str) where Vec<KucoinSubscribeType>: From<T> {
     utils::proxy_handle();
 
     match exchange {
@@ -220,26 +231,26 @@ pub async fn test_new_exchange_wss<T>(exchange: ExchangeEnum, symbol: &str, subs
             // });
             // try_join!(t1, t2).unwrap();
         }
-        ExchangeEnum::BitgetSpot => {
+        ExchangeEnum::KucoinSpot => {
             let symbol_format = utils::format_symbol(symbol.to_string(), "-").to_uppercase();
             let symbol_back = utils::format_symbol(symbol.to_string(), "_");
             trace!(symbol_format);
-            let name = format!("bitget_usdt_spot@{}", symbol.to_string().to_lowercase());
+            let name = format!("kucoin_usdt_spot@{}", symbol.to_string().to_lowercase());
             let bool_v1 = Arc::new(AtomicBool::new(true));
 
             let (res_sender, mut res_receiver): (Sender<ResponseData>, Receiver<ResponseData>) = channel(1024);
             let mut params: BTreeMap<String, String> = BTreeMap::new();
-            let access_key = env::var("bitget_access_key").unwrap_or("".to_string());
-            let secret_key = env::var("bitget_secret_key").unwrap_or("".to_string());
-            let pass_key = env::var("bitget_pass_key").unwrap_or("".to_string());
+            let access_key = env::var("kucoin_access_key").unwrap_or("".to_string());
+            let secret_key = env::var("kucoin_secret_key").unwrap_or("".to_string());
+            let pass_key = env::var("kucoin_pass_key").unwrap_or("".to_string());
             params.insert("access_key".to_string(), access_key);
             params.insert("secret_key".to_string(), secret_key);
             params.insert("pass_key".to_string(), pass_key);
 
             let mut exchange_wss = if ["depth", "ticker"].contains(&mold) {
-                BitgetSpotWs::new_label(name, false, params, BitgetWsType::Public, res_sender)
+                KucoinSpotWs::new_label(name, false, params, KucoinWsType::Public, res_sender).await
             } else {
-                BitgetSpotWs::new_label(name, false, params, BitgetWsType::Private, res_sender)
+                KucoinSpotWs::new_label(name, false, params, KucoinWsType::Private, res_sender).await
             };
 
             exchange_wss.set_subscribe(subscriber_type.into());
@@ -253,28 +264,29 @@ pub async fn test_new_exchange_wss<T>(exchange: ExchangeEnum, symbol: &str, subs
                 loop {
                     tokio::time::sleep(Duration::from_millis(1)).await;
                     if let Ok(received) = res_receiver.try_recv() {
+                        trace!(?received);
                         match mold_clone.as_str() {
                             "depth" => {
                                 if received.data != "" {
-                                    let result = bitget_spot_handle::handle_special_depth(received);
+                                    let result = kucoin_spot_handle::handle_special_depth(received);
                                     trace!(?result)
                                 }
                             }
                             "ticker" => {
                                 if received.data != "" {
-                                    let result = bitget_spot_handle::handle_special_ticker(received);
+                                    let result = kucoin_spot_handle::handle_special_ticker(received);
                                     trace!(?result)
                                 }
                             }
                             "account" => {
                                 if received.data != "" {
-                                    let result = bitget_spot_handle::handle_account_info(received, symbol_back.clone());
+                                    let result = kucoin_spot_handle::handle_account_info(received, symbol_back.clone());
                                     trace!(?result)
                                 }
                             }
                             "orders" => {
                                 if received.data != "" {
-                                    let result = bitget_spot_handle::handle_order(received, dec!(1));
+                                    let result = kucoin_spot_handle::handle_order(received, dec!(1));
                                     trace!(?result)
                                 }
                             }
@@ -288,6 +300,74 @@ pub async fn test_new_exchange_wss<T>(exchange: ExchangeEnum, symbol: &str, subs
             });
             try_join!(t1, t2).unwrap();
         }
+        ExchangeEnum::BitgetSpot => {
+            // let symbol_format = utils::format_symbol(symbol.to_string(), "-").to_uppercase();
+            // let symbol_back = utils::format_symbol(symbol.to_string(), "_");
+            // trace!(symbol_format);
+            // let name = format!("bitget_usdt_spot@{}", symbol.to_string().to_lowercase());
+            // let bool_v1 = Arc::new(AtomicBool::new(true));
+            //
+            // let (res_sender, mut res_receiver): (Sender<ResponseData>, Receiver<ResponseData>) = channel(1024);
+            // let mut params: BTreeMap<String, String> = BTreeMap::new();
+            // let access_key = env::var("bitget_access_key").unwrap_or("".to_string());
+            // let secret_key = env::var("bitget_secret_key").unwrap_or("".to_string());
+            // let pass_key = env::var("bitget_pass_key").unwrap_or("".to_string());
+            // params.insert("access_key".to_string(), access_key);
+            // params.insert("secret_key".to_string(), secret_key);
+            // params.insert("pass_key".to_string(), pass_key);
+            //
+            // let mut exchange_wss = if ["depth", "ticker"].contains(&mold) {
+            //     BitgetSpotWs::new_label(name, false, params, BitgetWsType::Public, res_sender)
+            // } else {
+            //     BitgetSpotWs::new_label(name, false, params, BitgetWsType::Private, res_sender)
+            // };
+            //
+            // exchange_wss.set_subscribe(subscriber_type.into());
+            //
+            // let t1 = tokio::spawn(async move {
+            //     exchange_wss.custom_subscribe(bool_v1, vec![symbol_format]).await;
+            // });
+            // let mold_arc = Arc::new(mold.to_string());
+            // let t2 = tokio::spawn(async move {
+            //     let mold_clone = Arc::clone(&mold_arc);
+            //     loop {
+            //         tokio::time::sleep(Duration::from_millis(1)).await;
+            //         if let Ok(received) = res_receiver.try_recv() {
+            //             match mold_clone.as_str() {
+            //                 "depth" => {
+            //                     if received.data != "" {
+            //                         let result = bitget_spot_handle::handle_special_depth(received);
+            //                         trace!(?result)
+            //                     }
+            //                 }
+            //                 "ticker" => {
+            //                     if received.data != "" {
+            //                         let result = bitget_spot_handle::handle_special_ticker(received);
+            //                         trace!(?result)
+            //                     }
+            //                 }
+            //                 "account" => {
+            //                     if received.data != "" {
+            //                         let result = bitget_spot_handle::handle_account_info(received, symbol_back.clone());
+            //                         trace!(?result)
+            //                     }
+            //                 }
+            //                 "orders" => {
+            //                     if received.data != "" {
+            //                         let result = bitget_spot_handle::handle_order(received, dec!(1));
+            //                         trace!(?result)
+            //                     }
+            //                 }
+            //                 _ => {
+            //                     error!("没有该命令!mode={}", mold_clone);
+            //                     panic!("没有该命令!mode={}", mold_clone)
+            //                 }
+            //             }
+            //         }
+            //     }
+            // });
+            // try_join!(t1, t2).unwrap();
+        }
         ExchangeEnum::OkxSwap => {
             // let symbol_format = utils::format_symbol(symbol.to_string(), "-").to_uppercase();
             // let symbol_back = utils::format_symbol(symbol.to_string(), "_");

+ 56 - 0
standard/tests/kucoin_spot_handle_test.rs

@@ -0,0 +1,56 @@
+mod exchange_test;
+
+use tracing::{instrument};
+use exchanges::kucoin_spot_ws::{KucoinSubscribeType};
+use standard::exchange::ExchangeEnum;
+use crate::exchange_test::test_new_exchange_wss;
+
+const SYMBOL: &str = "BLZ_USDT";
+
+// 测试订阅深度订阅
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+#[instrument(level = "TRACE")]
+async fn test_get_wss_depth() {
+    global::log_utils::init_log_with_trace();
+
+    let kucoin_subscribe_type = vec![
+        KucoinSubscribeType::PuSpotMarketLevel2Depth50
+    ];
+    test_new_exchange_wss(ExchangeEnum::KucoinSpot, SYMBOL, kucoin_subscribe_type, "depth").await;
+}
+
+// 测试订阅Ticker信息
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+#[instrument(level = "TRACE")]
+async fn test_get_wss_ticker() {
+    global::log_utils::init_log_with_trace();
+
+    let kucoin_subscribe_type = vec![
+        KucoinSubscribeType::PuMarketTicker
+    ];
+    test_new_exchange_wss(ExchangeEnum::KucoinSpot, SYMBOL, kucoin_subscribe_type, "ticker").await;
+}
+
+// 测试订阅Account信息
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+#[instrument(level = "TRACE")]
+async fn test_get_wss_account() {
+    global::log_utils::init_log_with_trace();
+
+    let kucoin_subscribe_type = vec![
+        KucoinSubscribeType::PrAccountBalance
+    ];
+    test_new_exchange_wss(ExchangeEnum::KucoinSpot, SYMBOL, kucoin_subscribe_type, "account").await;
+}
+
+// 测试订阅Orders信息
+#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
+#[instrument(level = "TRACE")]
+async fn test_get_wss_orders() {
+    global::log_utils::init_log_with_trace();
+
+    let kucoin_subscribe_type = vec![
+        KucoinSubscribeType::PrSpotMarketTradeOrders
+    ];
+    test_new_exchange_wss(ExchangeEnum::KucoinSpot, SYMBOL, kucoin_subscribe_type, "orders").await;
+}

+ 145 - 0
standard/tests/kucoin_spot_test.rs

@@ -0,0 +1,145 @@
+mod exchange_test;
+
+use std::collections::BTreeMap;
+use std::env;
+use std::io::Error;
+use rust_decimal_macros::dec;
+use tokio::sync::mpsc;
+use tracing::{instrument, trace};
+use standard::exchange::{Exchange, ExchangeEnum};
+use standard::{Order, OrderCommand, Platform, utils};
+use crate::exchange_test::{test_new_exchange};
+
+const SYMBOL: &str = "BLZ_USDT";
+
+// 测试获取Exchange实体
+#[tokio::test]
+#[instrument(level = "TRACE")]
+async fn test_get_self_exchange() {
+    global::log_utils::init_log_with_trace();
+
+    let kucoin_spot_exchange: Box<dyn Platform> = test_new_exchange(ExchangeEnum::KucoinSpot, SYMBOL).await;
+    let kucoin_get_self_exchange = kucoin_spot_exchange.get_self_exchange();
+    trace!(?kucoin_get_self_exchange);
+}
+
+// 测试获取交易对信息
+#[tokio::test]
+#[instrument(level = "TRACE")]
+async fn test_get_self_symbol() {
+    global::log_utils::init_log_with_trace();
+
+    let kucoin_spot_exchange: Box<dyn Platform> = test_new_exchange(ExchangeEnum::KucoinSpot, SYMBOL).await;
+    let kucoin_get_self_symbol = kucoin_spot_exchange.get_self_symbol();
+    trace!(?kucoin_get_self_symbol);
+}
+
+// 测试获取是否使用高速通道
+#[tokio::test]
+#[instrument(level = "TRACE")]
+async fn test_get_self_is_colo() {
+    global::log_utils::init_log_with_trace();
+
+    let kucoin_spot_exchange: Box<dyn Platform> = test_new_exchange(ExchangeEnum::KucoinSpot, SYMBOL).await;
+    let kucoin_get_self_is_colo = kucoin_spot_exchange.get_self_is_colo();
+    trace!(?kucoin_get_self_is_colo);
+}
+
+// 测试获取登录params信息
+#[tokio::test]
+#[instrument(level = "TRACE")]
+async fn test_get_self_params() {
+    global::log_utils::init_log_with_trace();
+
+    let kucoin_spot_exchange: Box<dyn Platform> = test_new_exchange(ExchangeEnum::KucoinSpot, SYMBOL).await;
+    let kucoin_get_self_params = kucoin_spot_exchange.get_self_params();
+    trace!("kucoin_get_self_params={:?}",kucoin_get_self_params);
+}
+
+// 测试获取Market信息
+#[tokio::test]
+#[instrument(level = "TRACE")]
+async fn test_get_self_market() {
+    global::log_utils::init_log_with_trace();
+
+    let kucoin_spot_exchange: Box<dyn Platform> = test_new_exchange(ExchangeEnum::KucoinSpot, SYMBOL).await;
+    let kucoin_get_self_market = kucoin_spot_exchange.get_self_market();
+    trace!(?kucoin_get_self_market);
+}
+
+// 测试获取请求时间信息
+#[tokio::test]
+#[instrument(level = "TRACE")]
+async fn test_get_request_delays() {
+    global::log_utils::init_log_with_trace();
+
+    let kucoin_spot_exchange: Box<dyn Platform> = test_new_exchange(ExchangeEnum::KucoinSpot, SYMBOL).await;
+    let kucoin_get_request_delays = kucoin_spot_exchange.get_request_delays();
+    trace!(?kucoin_get_request_delays);
+}
+
+// 测试获取请求平均时间信息
+#[tokio::test]
+#[instrument(level = "TRACE")]
+async fn test_get_request_avg_delay() {
+    global::log_utils::init_log_with_trace();
+
+    let kucoin_spot_exchange: Box<dyn Platform> = test_new_exchange(ExchangeEnum::KucoinSpot, SYMBOL).await;
+    let kucoin_get_request_avg_delay = kucoin_spot_exchange.get_request_avg_delay();
+    trace!(?kucoin_get_request_avg_delay);
+}
+
+// 测试获取最大请求时间信息
+#[tokio::test]
+#[instrument(level = "TRACE")]
+async fn test_get_request_max_delay() {
+    global::log_utils::init_log_with_trace();
+
+    let kucoin_spot_exchange: Box<dyn Platform> = test_new_exchange(ExchangeEnum::KucoinSpot, SYMBOL).await;
+    let kucoin_get_request_max_delay = kucoin_spot_exchange.get_request_max_delay();
+    trace!(?kucoin_get_request_max_delay);
+}
+
+// 测试获取服务器时间
+#[tokio::test]
+#[instrument(level = "TRACE")]
+async fn test_get_server_time() {
+    global::log_utils::init_log_with_trace();
+
+    let mut kucoin_spot_exchange: Box<dyn Platform> = test_new_exchange(ExchangeEnum::KucoinSpot, SYMBOL).await;
+    let kucoin_get_server_time = kucoin_spot_exchange.get_server_time().await;
+    trace!(?kucoin_get_server_time);
+}
+
+// 测试获取账号信息
+#[tokio::test]
+#[instrument(level = "TRACE")]
+async fn test_get_account() {
+    global::log_utils::init_log_with_trace();
+
+    let mut kucoin_spot_exchange: Box<dyn Platform> = test_new_exchange(ExchangeEnum::KucoinSpot, SYMBOL).await;
+    let kucoin_get_account = kucoin_spot_exchange.get_account().await;
+    trace!(?kucoin_get_account);
+}
+
+// 测试获取Ticker信息
+#[tokio::test]
+#[instrument(level = "TRACE")]
+async fn test_get_ticker() {
+    global::log_utils::init_log_with_trace();
+
+    let mut kucoin_spot_exchange: Box<dyn Platform> = test_new_exchange(ExchangeEnum::KucoinSpot, SYMBOL).await;
+    let kucoin_get_ticker = kucoin_spot_exchange.get_ticker().await;
+    trace!(?kucoin_get_ticker);
+}
+
+// 测试获取Market信息
+#[tokio::test]
+#[instrument(level = "TRACE")]
+async fn test_get_market() {
+    global::log_utils::init_log_with_trace();
+
+    let mut kucoin_spot_exchange: Box<dyn Platform> = test_new_exchange(ExchangeEnum::KucoinSpot, SYMBOL).await;
+    let kucoin_get_market = kucoin_spot_exchange.get_market().await;
+    trace!(?kucoin_get_market);
+}