Просмотр исходного кода

合并coinex,有警告还有底层库问题没有解决。

JiahengHe 2 лет назад
Родитель
Сommit
a8422e0290

+ 5 - 1
exchanges/Cargo.toml

@@ -21,6 +21,8 @@ tokio = { version = "1.31.0", features = ["full"] }
 chrono = "0.4.26"
 hex = "0.4"
 reqwest = { version = "0.11.14", features = ["json"] }
+# 解壓縮數據
+flate2 = "1.0"
 
 
 ring = "0.16.20"
@@ -42,4 +44,6 @@ tracing = "0.1"
 tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
 
 ##生成 xlsx
-rust_xlsxwriter = "0.58.0"
+rust_xlsxwriter = "0.58.0"
+
+once_cell = "1.18.0"

+ 592 - 0
exchanges/src/coinex_swap_rest.rs

@@ -0,0 +1,592 @@
+use std::collections::BTreeMap;
+use std::error::Error;
+use std::time::{SystemTime, UNIX_EPOCH};
+use reqwest::header::{HeaderMap, HeaderValue};
+use ring::{digest};
+use hex;
+use hmac::{Hmac, Mac, NewMac};
+use reqwest::Client;
+use rust_decimal::Decimal;
+use rust_decimal::prelude::FromPrimitive;
+use rust_decimal_macros::dec;
+use serde_json::Value;
+use crate::http_tool::RestTool;
+use crate::response_base::ResponseData;
+use sha2::{Digest, Sha256, Sha512};
+use tracing::{error, info, warn};
+
+#[derive(Clone)]
+pub struct CoinexSwapRest {
+    label: String,
+    base_url: String,
+    client: Client,
+    /*******参数*/
+    //登陆所需参数
+    login_param: BTreeMap<String, String>,
+    delays: Vec<i64>,
+    max_delay: i64,
+    avg_delay: Decimal,
+}
+
+impl CoinexSwapRest {
+    /*******************************************************************************************************/
+    /*****************************************获取一个对象****************************************************/
+    /*******************************************************************************************************/
+    pub fn new(login_param: BTreeMap<String, String>) -> CoinexSwapRest
+    {
+        return CoinexSwapRest::new_label("default-CoinexSwapRest".to_string(), login_param);
+    }
+    pub fn new_label(label: String, login_param: BTreeMap<String, String>) -> CoinexSwapRest
+    {
+        let base_url: String = String::from("https://api.coinex.com");
+
+        /*****返回结构体*******/
+        CoinexSwapRest {
+            label,
+            base_url,
+            client: Client::new(),
+            login_param,
+            delays: vec![],
+            max_delay: 0,
+            avg_delay: dec!(0.0),
+        }
+    }
+
+    /*******************************************************************************************************/
+    /*****************************************rest请求函数********************************************************/
+    /*******************************************************************************************************/
+    //获取服务器当前时间
+    pub async fn get_server_time(&mut self) -> ResponseData {
+
+        let data = self.request("GET".to_string(),
+                                "/v2".to_string(),
+                                "/time".to_string(),
+                                false,
+                                None,
+                                None
+        ).await;
+        data
+    }
+    //查询个人交易费率
+    pub async fn wallet_fee(&mut self) -> ResponseData {
+        let data = self.request("GET".to_string(),
+                                "/v2".to_string(),
+                                "/account/trade-fee-rate".to_string(),
+                                true,
+                                None,
+                                None
+        ).await;
+        data
+    }
+    //查询合约账户
+    pub async fn get_account(&mut self) -> ResponseData {
+        let data = self.request("GET".to_string(),
+                                "/v2".to_string(),
+                                "/assets/futures/balance".to_string(),
+                                true,
+                                None,
+                                None
+        ).await;
+        data
+    }
+
+    //指定币对仓位列表
+    pub async fn get_position(&mut self, market: String) -> ResponseData {
+        let params = serde_json::json!({
+            "market_type": "FUTURES"
+        });
+        let data = self.request("GET".to_string(),
+                                "/v2".to_string(),
+                                "/futures/pending-position".to_string(),
+                                true,
+                                Some(params.to_string()),
+                                None
+        ).await;
+        data
+    }
+
+    //用户仓位列表
+    pub async fn get_user_position(&mut self) -> ResponseData {
+        let params = serde_json::json!({
+            "market_type": "FUTURES"
+        });
+        let data = self.request("GET".to_string(),
+                                "/v2".to_string(),
+                                "/futures/pending-position".to_string(),
+                                true,
+                                Some(params.to_string()),
+                                None
+        ).await;
+        data
+    }
+
+    //获取所有合约交易行情统计 market 市场名列表,多个市场名之间使用英文","分隔,空字符串或不传表示查询全部市场,限制最多10个市场
+    pub async fn get_ticker(&mut self, market: String) -> ResponseData {
+        let params = serde_json::json!({
+            "market": market
+         });
+        let data = self.request("GET".to_string(),
+                                "/v2".to_string(),
+                                "/futures/ticker".to_string(),
+                                false,
+                                Some(params.to_string()),
+                                None
+        ).await;
+        data
+    }
+    //查询所有的合约信息
+    pub async fn get_market_details(&mut self, market: String) -> ResponseData {
+        let params = serde_json::json!({
+            "market": market
+         });
+        let data = self.request("GET".to_string(),
+                                "/v2".to_string(),
+                                "/futures/market".to_string(),
+                                false,
+                                Some(params.to_string()),
+                                None
+        ).await;
+        data
+    }
+    //查询单个订单详情  /spot/order-status?market=CETUSDT&order_id=13400
+    pub async fn get_order_details(&mut self, order_id: String) -> ResponseData {
+        let params = serde_json::json!({
+            "order_id": order_id
+         });
+        let data = self.request("GET".to_string(),
+                                "/v2".to_string(),
+                                "/futures/order-status".to_string(),
+                                true,
+                                Some(params.to_string()),
+                                None
+        ).await;
+        data
+    }
+    //查询未完成合约订单 /futures/pending-order?market=CETUSDT&market_type=FUTURES&side=buy&page=1&limit=10
+    pub async fn get_pending_order(&mut self, client_id: String) -> ResponseData {
+        let params = serde_json::json!({
+            "market_type": "FUTURES",
+            "client_id": client_id,
+            "limit": 10
+         });
+        let data = self.request("GET".to_string(),
+                                "/v2".to_string(),
+                                "/futures/pending-order".to_string(),
+                                true,
+                                Some(params.to_string()),
+                                None
+        ).await;
+        data
+    }
+
+    pub async fn get_pending_orders(&mut self) -> ResponseData {
+        let params = serde_json::json!({
+            "market_type": "FUTURES",
+            "limit": 100
+         });
+        let data = self.request("GET".to_string(),
+                                "/v2".to_string(),
+                                "/futures/pending-order".to_string(),
+                                true,
+                                Some(params.to_string()),
+                                None
+        ).await;
+        data
+    }
+
+    pub async fn get_finished_orders(&mut self) -> ResponseData {
+        let params = serde_json::json!({
+            "market_type": "FUTURES",
+            "limit": 100
+         });
+        let data = self.request("GET".to_string(),
+                                "/v2".to_string(),
+                                "/futures/finished-order".to_string(),
+                                true,
+                                Some(params.to_string()),
+                                None
+        ).await;
+        data
+    }
+
+    //下单
+    //  coinex swap 平仓需考虑最小下单量 只能通过close_position和position_id来平仓
+    pub async fn order(&mut self,
+                       market: String,
+                       pos_side: String,
+                       side: String,
+                       size: Decimal,
+                       price: Decimal,
+                       client_id: String
+    ) -> ResponseData
+    {
+        // 默认为限价单
+        let mut type_y = "limit".to_string();
+        // 0为市价单,
+        if price == Decimal::ZERO {
+            type_y = "market".to_string();
+        }
+        let data;
+
+
+        match format!("{}_{}", pos_side, side).as_str() {
+            "long_buy" => {//开多
+                data = self.swap_order(market, side, type_y, size, price, client_id, false).await;
+            }
+            "long_sell" => {//平多
+                data = self.close_position(market, type_y,  price, client_id, false).await;
+            }
+            "short_buy" => {//平空
+                data = self.close_position(market, type_y, price, client_id, false).await;
+            }
+            "short_sell" => {//开空
+                data = self.swap_order(market, side, type_y, size, price, client_id, false).await;
+            }
+            _ => {// 处理未知请求类型
+                error!("下单失败,数量异常! size: {}", size);
+                data = ResponseData::error(self.label.clone(), format!("下单失败, 下单参数: <market: {:?}, pos_side: {:?}, side: {:?}, size: {}, price: {:?}, client_id: {:?}>", market, pos_side, side, size, price, client_id));
+            }
+        };
+        data
+    }
+
+    // 平仓下单
+    pub async fn close_position(&mut self, market: String, type_y : String,  price: Decimal, client_id: String, is_hide: bool) -> ResponseData {
+        // 数量不传为全平
+        let param = serde_json::json!({
+            "market":market,
+            "market_type": "FUTURES",
+            "type": type_y,
+            "price":price,
+            "client_id":client_id,
+            "is_hide": is_hide
+        });
+
+        let data = self.request("POST".to_string(),
+                                "/v2".to_string(),
+                                "/futures/close-position".to_string(),
+                                true,
+                                None,
+                                Some(param.to_string())
+        ).await;
+        data
+    }
+
+    //合约交易开仓下单
+    pub async fn swap_order(&mut self, market: String, side: String, type_y : String, amount: Decimal, price: Decimal, client_id: String, is_hide: bool) -> ResponseData {
+        let param = serde_json::json!({
+            "market":market,
+            "market_type": "FUTURES",
+            "side": side,
+            "type": type_y,
+            "amount":amount,
+            "price":price,
+            "client_id":client_id,
+            "is_hide": is_hide
+        });
+
+        let data = self.request("POST".to_string(),
+                                "/v2".to_string(),
+                                "/futures/order".to_string(),
+                                true,
+                                None,
+                                Some(param.to_string())
+        ).await;
+        data
+    }
+
+    //设置持仓模式
+    pub async fn setting_dual_mode(&mut self) -> ResponseData {
+        ResponseData::error(self.label.clone(), "设置双向持仓失败, coinex没有设置双向持仓".to_string())
+    }
+    //更新双仓模式下的杠杆
+    pub async fn setting_dual_leverage(&mut self, market: String, leverage: i32) -> ResponseData {
+        let params = serde_json::json!({
+                "market": market,
+                "market_type": "FUTURES",
+                // cross: 全仓。全仓模式下,合约账户的全部可用余额都可用作当前全部仓位的共享保证金,系统会使用合约账户中的可用余额自动追加保证金,以避免仓位被强平
+                //isolated: 逐仓。逐仓模式下,仓位保证金不会共享,单个仓位的保证金仅用于当前仓位,系统不会自动追加保证金,需要手动追加。
+                "margin_mode": "cross",
+                "leverage":leverage,
+             });
+        let data = self.request("POST".to_string(),
+                                "/v2".to_string(),
+                                "/futures/adjust-position-leverage".to_string(),
+                                true,
+                                None,
+                                Some(params.to_string())
+        ).await;
+        data
+    }
+
+    //撤销单个订单
+    pub async fn cancel_order(&mut self, market: String, order_id: &str, client_id: &str) -> ResponseData {
+        if order_id != "" {  // 如果真实订单id不为空,则用真实订单id取消订单
+            let id = order_id.parse::<i64>().unwrap();
+            let params = serde_json::json!({
+                "market": market,
+                "market_type": "FUTURES",
+                "order_id": id
+            });
+            let data = self.request("POST".to_string(),
+                                    "/v2".to_string(),
+                                    "/futures/cancel-order".to_string(),
+                                    true,
+                                    None,
+                                    Some(params.to_string())
+            ).await;
+            data
+        } else if client_id != "" {  // 如果客户端id不为空,则用客户端id取消订单
+            let params = serde_json::json!({
+                "market": market,
+                "market_type": "FUTURES",
+                "client_id": client_id
+            });
+
+            let mut data = self.request("POST".to_string(),
+                                    "/v2".to_string(),
+                                    "/futures/cancel-order-by-client-id".to_string(),
+                                    true,
+                                    None,
+                                    Some(params.to_string())
+            ).await;
+            // 非空的
+            if !data.data.is_null() {
+                data.data = data.data.as_array().unwrap()[0]["data"].clone();
+            }
+            data
+        } else {  // 否则返回错误
+            error!("取消订单失败失败,id异常");
+            ResponseData::error(self.label.clone(), format!("取消订单失败失败, orderId:{:?}, clientId: {:?} ", order_id, client_id))
+        }
+    }
+
+    // 撤销所有挂单
+    pub async fn cancel_order_all(&mut self, market: String) -> ResponseData {
+        let params = serde_json::json!({
+            "market": market,
+            "market_type": "FUTURES"
+        });
+        let data = self.request("POST".to_string(),
+                                "/v2".to_string(),
+                                "/futures/cancel-all-order".to_string(),
+                                true,
+                                None,
+                                Some(params.to_string())
+        ).await;
+        data
+    }
+
+    //查询个人成交记录
+    pub async fn my_trades(&mut self, market: String, limit: i64) -> ResponseData {
+        let mut params = serde_json::json!({
+            "market": market,
+            "market_type": "FUTURES",
+            "limit": 1000
+        });
+        if limit > 0 {
+            params["limit"] = serde_json::json!(limit);
+        }
+
+        let data = self.request("GET".to_string(),
+                                "/v2".to_string(),
+                                "/futures/user-deals".to_string(),
+                                true,
+                                Some(params.to_string()), None).await;
+        data
+    }
+
+    //查询合约账户变更历史
+    pub async fn account_book(&mut self) -> ResponseData {
+        error!("查询合约账户变更历史失败,无实现");
+        ResponseData::error(self.label.clone(), "查询合约账户变更历史失败,接口没实现".to_string())
+    }
+
+
+    /*******************************************************************************************************/
+    /*****************************************工具函数********************************************************/
+    /*******************************************************************************************************/
+    pub fn get_delays(&self) -> Vec<i64> {
+        self.delays.clone()
+    }
+    pub fn get_avg_delay(&self) -> Decimal {
+        self.avg_delay.clone()
+    }
+    pub fn get_max_delay(&self) -> i64 {
+        self.max_delay.clone()
+    }
+    fn get_delay_info(&mut self) {
+        let last_100 = if self.delays.len() > 100 {
+            self.delays[self.delays.len() - 100..].to_vec()
+        } else {
+            self.delays.clone()
+        };
+
+        let max_value = last_100.iter().max().unwrap();
+        if max_value.clone().to_owned() > self.max_delay {
+            self.max_delay = max_value.clone().to_owned();
+        }
+
+        let sum: i64 = last_100.iter().sum();
+        let sum_v = Decimal::from_i64(sum).unwrap();
+        let len_v = Decimal::from_u64(last_100.len() as u64).unwrap();
+        self.avg_delay = (sum_v / len_v).round_dp(1);
+        self.delays = last_100.clone().into_iter().collect();
+    }
+
+    //调用请求
+    async fn request(&mut self,
+                     request_type: String,
+                     prefix_url: String,
+                     request_url: String,
+                     is_login: bool,
+                     params: Option<String>,
+                     body: Option<String>) -> ResponseData
+    {
+        // trace!("login_param:{:?}", self.login_param);
+        //解析账号信息
+        let mut access_key = "".to_string();
+        let mut secret_key = "".to_string();
+        if self.login_param.contains_key("access_key") {
+            access_key = self.login_param.get("access_key").unwrap().to_string();
+        }
+        if self.login_param.contains_key("secret_key") {
+            secret_key = self.login_param.get("secret_key").unwrap().to_string();
+        }
+        let mut is_login_param = true;
+        if access_key == "" || secret_key == "" {
+            is_login_param = false
+        }
+
+        let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis().to_string();
+        // url
+        let mut url_and_query = format!("{}{}", prefix_url.clone(), request_url.clone());
+        let mut headers = HeaderMap::new();
+        headers.insert("Content-Type", HeaderValue::from_static("application/json"));
+        headers.insert(
+            "X-COINEX-KEY",
+            HeaderValue::from_str(&self.login_param.get("access_key").unwrap()).unwrap(),
+        );
+        headers.insert(
+            "X-COINEX-TIMESTAMP",
+            HeaderValue::from_str(&timestamp).unwrap(),
+        );
+
+        if let Some(params) = params {
+            let query = RestTool::parse_params_to_str(params);
+            url_and_query = format!("{}?{}", url_and_query, query);
+        }
+        let body_s = if let Some(body) = body {
+            body
+        } else {
+            "".to_string()
+        };
+
+        //是否需要登陆-- 组装sing
+        if is_login {
+            if !is_login_param {
+                let e = ResponseData::error(self.label.clone(), "登陆参数错误!".to_string());
+                return e;
+            } else {//需要登陆-且登陆参数齐全
+                //组装sing
+                let sing = Self::sign(
+                    &request_type,
+                    &url_and_query,
+                    &body_s,
+                    timestamp.clone(),
+                    &secret_key
+                );
+                // trace!("sing:{}", sing);
+                //组装header
+                headers.insert("X-COINEX-SIGN", HeaderValue::from_str(&sing.unwrap()).unwrap());
+            }
+        }
+
+        let start_time = chrono::Utc::now().timestamp_millis();
+        let response = self.http_toll(
+            url_and_query,
+            request_type,
+            body_s.clone(),
+            headers,
+        ).await;
+
+        let time_array = chrono::Utc::now().timestamp_millis() - start_time;
+        self.delays.push(time_array);
+        // self.get_delay_info();
+
+        response
+    }
+    fn sign(
+        method: &String,
+        path: &String,
+        body: &String,
+        timestamp: String,
+        secret_key: &String
+    ) -> Result<String, Box<dyn Error>> {
+        let prepared_str = format!(
+            "{}{}{}{}{}",
+            method, path, body, timestamp, secret_key
+        );
+        let hash = Sha256::digest(prepared_str.as_bytes());
+        Ok(hex::encode(hash))
+    }
+
+    async fn http_toll(&mut self, request_path: String, request_type: String, body: String, headers: HeaderMap) -> ResponseData {
+        /****请求接口与 地址*/
+        let url = format!("{}{}", self.base_url.to_string(), request_path);
+        let request_type = request_type.clone().to_uppercase();
+
+
+        let request_builder = match request_type.as_str() {
+            "GET" => self.client.get(&url).headers(headers),
+            "POST" => self.client.post(&url).body(body.clone()).headers(headers),
+            "DELETE" => self.client.delete(&url).headers(headers),
+            // "PUT" => self.client.put(url.clone()).json(&params),
+            _ => {
+                panic!("{}", format!("错误的请求类型:{}", request_type.clone()))
+            }
+        };
+
+        // 读取响应的内容
+        let response = request_builder.send().await.unwrap();
+        let is_success = response.status().is_success(); // 先检查状态码
+        let text = response.text().await.unwrap();
+        let data_json: Value = serde_json::from_str(text.as_str()).unwrap();
+        return if is_success && data_json["code"].to_string() == "0"{
+            self.on_success_data(data_json["data"].clone())
+        } else {
+            self.on_error_data(&text, &url, &body)
+        }
+    }
+
+    pub fn on_success_data(&mut self, text: Value) -> ResponseData {
+        ResponseData::new(self.label.clone(),
+                          200,
+                          "success".to_string(),
+                          text)
+    }
+
+    pub fn on_error_data(&mut self, text: &String, base_url: &String, params: &String) -> ResponseData {
+        let json_value = serde_json::from_str::<Value>(&text);
+
+        match json_value {
+            Ok(data) => {
+                let message;
+                if !data["message"].is_null() {
+                    message = format!("{}:{}", data["code"].to_string(), data["message"].as_str().unwrap());
+                } else {
+                    message = data["code"].to_string();
+                }
+                let mut error = ResponseData::error(self.label.clone(), message);
+                error.message = format!("请求地址:{}, 请求参数:{}, 报错内容:{}。", base_url, params, error.message);
+                error
+            }
+            Err(e) => {
+                error!("解析错误:{:?}", e);
+                let error = ResponseData::error("".to_string(),
+                                                format!("json 解析失败:{},相关参数:{}", e, text));
+                error
+            }
+        }
+    }
+}

+ 353 - 0
exchanges/src/coinex_swap_ws.rs

@@ -0,0 +1,353 @@
+use std::sync::Arc;
+use std::sync::atomic::AtomicBool;
+use std::time::{Duration, SystemTime, UNIX_EPOCH};
+use chrono::Utc;
+use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
+
+use once_cell::sync::Lazy;  // 使用线程安全的版本
+use hex::encode;
+use serde_json::{json, Value};
+use sha2::{Digest, Sha256};
+use tokio::sync::Mutex;
+use tokio_tungstenite::tungstenite::{Error, Message};
+use tracing::{error, info, trace};
+use crate::gate_swap_ws::{GateSwapLogin, GateSwapSubscribeType, GateSwapWs, GateSwapWsType};
+use crate::response_base::ResponseData;
+use crate::socket_tool::{AbstractWsMode, HeartbeatType};
+
+pub(crate) static LOGIN_DATA: Lazy<Mutex<(bool, bool)>> = Lazy::new(|| {
+    println!("初始化...");
+    Mutex::new((false, false))
+});
+
+//订阅频道
+#[derive(Clone)]
+pub enum CoinexSwapSubscribeType {
+    // 深度
+    PuFuturesDepth,
+    // 公开成交
+    PuFuturesDeals,
+
+    // 订单
+    PrFuturesOrders,
+    // 仓位
+    PrFuturesPositions,
+    // 余额
+    PrFuturesBalances,
+}
+
+//账号信息
+#[derive(Clone)]
+#[allow(dead_code)]
+pub struct CoinexSwapLogin {
+    pub api_key: String,
+    pub secret: String,
+}
+
+#[derive(Clone)]
+pub struct CoinexSwapWs {
+    //类型
+    label: String,
+    //地址
+    address_url: String,
+    //账号信息
+    login_param: Option<CoinexSwapLogin>,
+    //币对
+    symbol_s: Vec<String>,
+    //订阅
+    subscribe_types: Vec<CoinexSwapSubscribeType>,
+    //心跳间隔
+    heartbeat_time: u64
+}
+
+
+impl CoinexSwapWs {
+    /*******************************************************************************************************/
+    /*****************************************实例化一个对象****************************************************/
+    /*******************************************************************************************************/
+    pub fn new(login_param: Option<CoinexSwapLogin>) -> CoinexSwapWs {
+        return CoinexSwapWs::new_label("default-CoinexSwapWs".to_string(), login_param);
+    }
+
+    pub fn new_label(label: String, login_param: Option<CoinexSwapLogin>) -> CoinexSwapWs
+    {
+        /*******公共频道-私有频道数据组装*/
+        let address_url = "wss://socket.coinex.com/v2/futures".to_string();
+        info!("走普通通道(不支持colo通道):{}", address_url);
+        CoinexSwapWs {
+            label,
+            address_url,
+            login_param,
+            symbol_s: vec![],
+            subscribe_types: vec![],
+            heartbeat_time: 1000 * 10
+        }
+    }
+
+    /*******************************************************************************************************/
+    /*****************************************订阅函数********************************************************/
+    /*******************************************************************************************************/
+    //手动添加订阅信息
+    pub fn set_subscribe(&mut self, subscribe_types: Vec<CoinexSwapSubscribeType>) {
+        self.subscribe_types.extend(subscribe_types);
+    }
+    //手动添加币对
+    pub fn set_symbols(&mut self, mut b_array: Vec<String>) {
+        for symbol in b_array.iter_mut() {
+            // 大写
+            *symbol = symbol.to_uppercase();
+            // 字符串替换
+            *symbol = symbol.replace("-", "_");
+        }
+        self.symbol_s = b_array;
+    }
+    //频道是否需要登录
+    fn contains_pr(&self) -> bool {
+        for t in self.subscribe_types.clone() {
+            if match t {
+                CoinexSwapSubscribeType::PuFuturesDepth => false,
+                CoinexSwapSubscribeType::PuFuturesDeals => false,
+
+                CoinexSwapSubscribeType::PrFuturesOrders => true,
+                CoinexSwapSubscribeType::PrFuturesPositions => true,
+                CoinexSwapSubscribeType::PrFuturesBalances => true,
+            } {
+                return true;
+            }
+        }
+        false
+    }
+
+    /*******************************************************************************************************/
+    /*****************************************工具函数********************************************************/
+    /*******************************************************************************************************/
+    //订阅枚举解析
+    pub fn enum_to_string(symbol: String, subscribe_type: CoinexSwapSubscribeType, login_param: Option<CoinexSwapLogin>) -> Value {
+        let time = chrono::Utc::now().timestamp();
+        let mut access_key = "".to_string();
+        let mut secret_key = "".to_string();
+        match login_param {
+            None => {}
+            Some(param) => {
+                access_key = param.api_key.clone();
+                secret_key = param.secret.clone();
+            }
+        }
+
+        match subscribe_type {
+            CoinexSwapSubscribeType::PuFuturesDepth => {
+                json!({
+                    "method": "depth.subscribe",
+                    "params": {
+                        "market_list": [
+                            [symbol, 50, "0.000000001", true]
+                        ]
+                    },
+                    "id": 1
+                })
+            }
+            CoinexSwapSubscribeType::PuFuturesDeals => {
+                json!({
+                    "method": "deals.subscribe",
+                    "params": {"market_list": [symbol]},
+                    "id": 1
+                })
+            }
+
+            CoinexSwapSubscribeType::PrFuturesOrders => {
+                json!({
+                  "method": "order.subscribe",
+                  "params": {"market_list": [symbol]},
+                  "id": 1
+                })
+            }
+            CoinexSwapSubscribeType::PrFuturesPositions => {
+                json!({
+                  "method": "position.subscribe",
+                  "params": {"market_list": [symbol]},
+                  "id": 1
+                })
+            }
+            CoinexSwapSubscribeType::PrFuturesBalances => {
+                json!({
+                    "method": "balance.subscribe",
+                    "params": {"ccy_list": ["USDT"]}, // 目前只用u 所以写死
+                    "id": 1
+                })
+            }
+        }
+    }
+    //订阅信息生成
+    pub fn get_subscription(&self) -> Vec<Value> {
+        let mut args = vec![];
+        // 只获取第一个
+        let symbol = self.symbol_s.get(0).unwrap().replace("_", "").to_uppercase();
+
+        for subscribe_type in &self.subscribe_types {
+            let ty_str = Self::enum_to_string(symbol.clone(),
+                                              subscribe_type.clone(),
+                                              self.login_param.clone(),
+            );
+            args.push(ty_str);
+        }
+        args
+    }
+
+    /*******************************************************************************************************/
+    /*****************************************socket基本*****************************************************/
+    /*******************************************************************************************************/
+    //链接
+    pub async fn ws_connect_async<F, Future>(&mut self,
+                                             is_shutdown_arc: Arc<AtomicBool>,
+                                             handle_function: F,
+                                             write_tx_am: &Arc<Mutex<UnboundedSender<Message>>>,
+                                             write_to_socket_rx: UnboundedReceiver<Message>) -> Result<(), Error>
+        where
+            F: Fn(ResponseData) -> Future + Clone + Send + 'static + Sync,
+            Future: std::future::Future<Output=()> + Send + 'static, // 确保 Fut 是一个 Future,且输出类型为 ()
+    {
+        let login_is = self.contains_pr();
+        let login_param = self.login_param.clone().unwrap();
+        let subscription = self.get_subscription();
+        let address_url = self.address_url.clone();
+        let label = self.label.clone();
+        let heartbeat_time = self.heartbeat_time.clone();
+
+
+        //心跳-- 方法内部线程启动
+        let write_tx_clone1 = Arc::clone(write_tx_am);
+        let write_tx_clone2 = Arc::clone(write_tx_am);
+        tokio::spawn(async move {
+            trace!("线程-异步心跳-开始");
+            let ping_str = json!({
+                "method": "server.ping",
+                "params": {},
+                "id": 1
+            });
+            AbstractWsMode::ping_or_pong(write_tx_clone1, HeartbeatType::Custom(ping_str.to_string()), heartbeat_time).await;
+            trace!("线程-异步心跳-结束");
+        });
+
+        //设置订阅
+        let mut subscribe_array = vec![];
+
+        // 需要登录
+        if login_is {
+            let mut login_data = LOGIN_DATA.lock().await;
+            login_data.0 = true;
+            let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis();
+            //登录相关
+            let prepared_str = format!("{}{}", time, login_param.secret);
+            // 创建SHA256哈希器实例
+            let mut hasher = Sha256::new();
+            // 加密字符串
+            hasher.update(prepared_str);
+            // 计算哈希值
+            let result = hasher.finalize();
+            // 将哈希值转换为十六进制小写字符串
+            let hex_str = encode(result).to_lowercase();
+
+            let login_param = json!({
+                "method": "server.sign",
+                "params": {
+                    "access_id": login_param.api_key,
+                    "signed_str": hex_str,
+                    "timestamp": time
+                },
+                "id": 1
+            });
+            let login_str = login_param.to_string();
+            info!("发起ws登录: {}", login_str);
+            let write_tx_c = Arc::clone(&write_tx_clone2);
+            AbstractWsMode::send_subscribe(write_tx_c, Message::Text(login_str)).await;
+        }
+        for s in subscription {
+            subscribe_array.push(s.to_string());
+        }
+
+        //链接
+        let t2 = tokio::spawn(async move {
+            let write_to_socket_rx_arc = Arc::new(Mutex::new(write_to_socket_rx));
+
+            info!("启动连接");
+            loop {
+                info!("coinex_usdt_swap socket 连接中……");
+
+                AbstractWsMode::ws_connect_async(is_shutdown_arc.clone(), handle_function.clone(), address_url.clone(),
+                                                 login_is, label.clone(), subscribe_array.clone(), write_to_socket_rx_arc.clone(),
+                                                 Self::message_text, Self::message_ping, Self::message_pong).await;
+                let mut login_data = LOGIN_DATA.lock().await;
+                // 断联后 设置为没有登录
+                login_data.1 = false;
+                info!("coinex_usdt_swap socket 断连,1s以后重连……");
+                error!("coinex_usdt_swap socket 断连,1s以后重连……");
+                tokio::time::sleep(Duration::from_secs(1)).await;
+            }
+        });
+        tokio::try_join!(t2).unwrap();
+        trace!("线程-心跳与链接-结束");
+
+        Ok(())
+    }
+    /*******************************************************************************************************/
+    /*****************************************数据解析*****************************************************/
+    /*******************************************************************************************************/
+    //数据解析-Text
+    pub fn message_text(text: String) -> Option<ResponseData> {
+        let response_data = Self::ok_text(text);
+        Option::from(response_data)
+    }
+    //数据解析-ping
+    pub fn message_ping(_pi: Vec<u8>) -> Option<ResponseData> {
+        return Option::from(ResponseData::new("".to_string(), -300, "success".to_string(), Value::Null));
+    }
+    //数据解析-pong
+    pub fn message_pong(_po: Vec<u8>) -> Option<ResponseData> {
+        return Option::from(ResponseData::new("".to_string(), -301, "success".to_string(), Value::Null));
+    }
+    //数据解析
+    pub fn ok_text(text: String) -> ResponseData
+    {
+        // trace!("原始数据:{}", text);
+        let mut res_data = ResponseData::new("".to_string(), 200, "success".to_string(), Value::Null);
+        let json_value: Value = serde_json::from_str(&text).unwrap();
+
+        let obj = json_value["method"].as_str();
+        match obj {
+            Some(v)=> {
+                res_data.channel = format!("{}", v);
+                res_data.code = 200;
+                res_data.data = json_value["data"].clone();
+            },
+            None => {
+                // 认证的响应没有method,只能通过id和code判断
+                match json_value["id"].as_i64() {
+                    Some(1) => {
+                        match json_value["code"].as_i64() {
+                            Some(0) =>{
+                                match json_value["data"].as_str() {
+                                    None => {
+                                        res_data.channel = "server.sign".to_string();
+                                        res_data.code = -200;
+                                    }
+                                    _ =>{
+                                        res_data.code = 400;
+                                    }
+                                }
+                            }
+                            _ => {
+                                res_data.code = 400;
+                            }
+                        }
+                    }
+                    _ => {
+                        res_data.code = 400;
+                    }
+                }
+                res_data.data = json_value;
+            }
+        }
+        res_data
+    }
+}
+

+ 2 - 0
exchanges/src/lib.rs

@@ -25,4 +25,6 @@ pub mod crypto_spot_ws;
 pub mod bybit_swap_rest;
 pub mod xlsx_utils;
 pub mod bybit_swap_ws;
+pub mod coinex_swap_rest;
+pub mod coinex_swap_ws;
 

+ 45 - 12
exchanges/src/socket_tool.rs

@@ -1,9 +1,12 @@
+use std::io::Read;
 use std::net::{IpAddr, Ipv4Addr, SocketAddr};
+use std::str::from_utf8;
 use std::sync::Arc;
 use std::sync::atomic::{AtomicBool, Ordering};
 use std::time::{Duration};
 
 use chrono::Utc;
+use flate2::bufread::GzDecoder;
 use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
 use futures_util::{future, pin_mut, SinkExt, StreamExt};
 use futures_util::stream::{SplitSink, SplitStream};
@@ -103,12 +106,31 @@ impl AbstractWsMode {
                     */
                     match code {
                         -200 => {
-                            //登录成功
-                            info!("ws登录成功:{:?}", data);
-                            info!("订阅内容:{:?}", subscribe_array.clone());
-                            for s in &subscribe_array {
-                                let mut write_lock = ws_write_arc.lock().await;
-                                write_lock.send(Message::Text(s.parse().unwrap())).await.expect("订阅消息失败");
+                            let mut is_sub = false;
+                            match data.channel.as_str() {
+                                "server.sign" => { // coinex
+                                    let mut login_data = crate::coinex_swap_ws::LOGIN_DATA.lock().await;
+                                    if login_data.0 && !login_data.1 {
+                                        login_data.1 = true;
+                                        is_sub = true;
+                                    }
+                                },
+                                "login" => { // bitget
+                                    is_sub = true;
+                                },
+                                _ => {
+                                    is_sub = false;
+                                }
+                            }
+                            if is_sub {
+                                //登录成功
+                                info!("ws登录成功:{:?}", data);
+                                info!("订阅内容:{:?}", subscribe_array.clone());
+                                for s in &subscribe_array {
+                                    let mut write_lock = ws_write_arc.lock().await;
+                                    write_lock.send(Message::Text(s.parse().unwrap())).await.expect("订阅消息失败");
+                                }
+                                info!("订阅完成!");
                             }
                         }
                         -201 => {
@@ -239,12 +261,7 @@ impl AbstractWsMode {
             Ok(Message::Text(text)) => message_text(text),
             Ok(Message::Ping(pi)) => message_ping(pi),
             Ok(Message::Pong(po)) => message_pong(po),
-            Ok(Message::Binary(s)) => {
-                //二进制WebSocket消息
-                let message_str = format!("Binary:{:?}", s);
-                trace!("{:?}",message_str);
-                Option::from(ResponseData::new("".to_string(), 2, message_str, Value::Null))
-            }
+            Ok(Message::Binary(s)) => message_text(parse_zip_data(s.clone())), // 二进制压缩  解析
             Ok(Message::Close(c)) => {
                 let message_str = format!("关闭指令:{:?}", c);
                 trace!("{:?}",message_str);
@@ -272,6 +289,22 @@ impl AbstractWsMode {
     }
 }
 
+fn parse_zip_data(p0: Vec<u8>) -> String{
+    // 创建一个GzDecoder的实例,将压缩数据作为输入
+    let mut decoder = GzDecoder::new(&p0[..]);
+
+    // 创建一个缓冲区来存放解压缩后的数据
+    let mut decompressed_data = Vec::new();
+
+    // 读取解压缩的数据到缓冲区中
+    decoder.read_to_end(&mut decompressed_data).expect("解压缩失败");
+    let result = from_utf8(&decompressed_data)
+        .expect("解压缩后的数据不是有效的UTF-8");
+
+    // info!("解压缩数据 {:?}", result);
+    result.to_string()
+}
+
 //创建链接
 pub async fn ws_connect_async(address_url: String) -> (SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>,
                                                        SplitStream<WebSocketStream<MaybeTlsStream<TcpStream>>>) {

+ 253 - 0
exchanges/tests/coinex_swap_test.rs

@@ -0,0 +1,253 @@
+use std::cmp::max;
+use std::collections::BTreeMap;
+use std::io::{Error, ErrorKind};
+use std::str::FromStr;
+use std::sync::Arc;
+use std::sync::atomic::AtomicBool;
+use futures_channel::mpsc::UnboundedSender;
+use futures_util::StreamExt;
+use rust_decimal::Decimal;
+use serde_json::Value;
+use tokio::sync::Mutex;
+use tokio::time::Instant;
+use tracing::{error, info, trace};
+use exchanges::binance_swap_ws::BinanceSwapSubscribeType;
+use exchanges::coinex_swap_rest::CoinexSwapRest;
+use exchanges::coinex_swap_ws::{CoinexSwapLogin, CoinexSwapSubscribeType, CoinexSwapWs};
+use exchanges::gate_swap_rest::GateSwapRest;
+use exchanges::gate_swap_ws::{GateSwapLogin, GateSwapWs, GateSwapWsType};
+use exchanges::response_base::ResponseData;
+use global::trace_stack::TraceStack;
+
+const ACCESS_KEY: &str = "";
+const SECRET_KEY: &str = "";
+
+
+//ws-订阅公共频道信息
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
+async fn ws_custom_subscribe() {
+    global::log_utils::init_log_with_trace();
+
+    let (write_tx, write_rx) = futures_channel::mpsc::unbounded();
+    let (read_tx, mut read_rx) = futures_channel::mpsc::unbounded::<ResponseData>();
+
+    // let (write_tx, write_rx) = tokio::sync::broadcast::channel::<Message>(10);
+    // let (read_tx, mut read_rx) = tokio::sync::broadcast::channel::<ResponseData>(10);
+
+
+    let write_tx_am = Arc::new(Mutex::new(write_tx));
+    let is_shutdown_arc = Arc::new(AtomicBool::new(true));
+
+    //读取
+    let _is_shutdown_arc_clone = Arc::clone(&is_shutdown_arc);
+    let _tr = tokio::spawn(async move {
+        trace!("线程-数据读取-开启");
+        loop {
+            // 从通道中接收并丢弃所有的消息,直到通道为空
+            while let Ok(Some(data)) = read_rx.try_next() {
+
+                // 从通道中接收并丢弃所有的消息,直到通道为空
+                while let Ok(Some(_)) = read_rx.try_next() {
+                    // 消息被忽略
+                }
+            }
+        }
+        // trace!("线程-数据读取-结束");
+    });
+
+    //写数据
+    // let bool_v2_clone = Arc::clone(&is_shutdown_arc);
+    // let write_tx_clone = Arc::clone(&write_tx_am);
+    // let su = ws.get_subscription();
+    // let tw = tokio::spawn(async move {
+    //     trace!("线程-数据写入-开始");
+    //     loop {
+    //         tokio::time::sleep(Duration::from_millis(20 * 1000)).await;
+    //         // let close_frame = CloseFrame {
+    //         //     code: CloseCode::Normal,
+    //         //     reason: Cow::Borrowed("Bye bye"),
+    //         // };
+    //         // let message = Message::Close(Some(close_frame));
+    //
+    //
+    //         let message = Message::Text(su.clone());
+    //         AbstractWsMode::send_subscribe(write_tx_clone.clone(), message.clone()).await;
+    //         trace!("发送指令成功");
+    //     }
+    //     trace!("线程-数据写入-结束");
+    // });
+
+    let fun = move |data: ResponseData| {
+        async move {
+            println!("{:?}", data);
+        }
+    };
+    let param = CoinexSwapLogin {
+        api_key: ACCESS_KEY.to_string(),
+        secret: SECRET_KEY.to_string(),
+    };
+    let t1 = tokio::spawn(async move {
+        let mut ws = get_ws(Option::from(param));
+        ws.set_symbols(vec!["FTM_USDT".to_string()]);
+        ws.set_subscribe(vec![
+            // CoinexSwapSubscribeType::PuFuturesDepth,
+            CoinexSwapSubscribeType::PrFuturesOrders
+        ]);
+        //链接
+        let bool_v3_clone = Arc::clone(&is_shutdown_arc);
+        ws.ws_connect_async(bool_v3_clone, fun, &write_tx_am, write_rx).await.expect("链接失败(内部一个心跳线程应该已经关闭了)");
+        trace!("test 唯一线程结束--");
+    });
+    tokio::try_join!(t1).unwrap();
+    trace!("当此结束");
+    trace!("重启!");
+    trace!("参考交易所关闭");
+    return;
+}
+
+fn get_ws(btree_map: Option<CoinexSwapLogin>) -> CoinexSwapWs {
+    let coinex_ws = CoinexSwapWs::new(btree_map);
+    coinex_ws
+}
+
+#[tokio::test]
+async fn rest_account_book_test() {
+    let mut ret = get_rest();
+    let req_data = ret.get_account().await;
+    let res_data_json: Value = req_data.data;
+    println!("coinex--查询合约账户--{:?}", res_data_json);
+    let result = res_data_json.as_array().unwrap().get(0).unwrap();
+    println!("coinex--查询合约账户--{:?}", result["ccy"]);
+}
+
+#[tokio::test]
+async fn rest_position_test() {
+    let mut ret = get_rest();
+    let req_data = ret.get_position("BOMEUSDT".to_string()).await;
+    println!("coinex--查询仓位--{:?}", req_data);
+}
+
+#[tokio::test]
+async fn rest_positions_test() {
+    let mut ret = get_rest();
+    let req_data = ret.get_user_position().await;
+    println!("coinex--查询用户所有仓位--{:?}", req_data);
+}
+
+#[tokio::test]
+async fn rest_ticker_test() {
+    let mut ret = get_rest();
+    let req_data = ret.get_ticker("BOMEUSDT".to_string()).await;
+    println!("coinex--查询ticker--{:?}", req_data);
+}
+
+#[tokio::test]
+async fn rest_market_test() {
+    let mut ret = get_rest();
+    let req_data = ret.get_market_details("BOMEUSDT".to_string()).await;
+    println!("coinex--查询market--{:?}", req_data);
+}
+
+#[tokio::test]
+async fn rest_market_list_test() {
+    let symbol = "FTMUSDT".to_string();
+    let price = Decimal::from_str("0.92").unwrap();
+    let custom_id = "1234567890000";
+    let order_side;
+    let position_side;
+    let amount = Decimal::from_str("10").unwrap();
+    let ct_val = Decimal::from_str("1").unwrap();
+    let origin_side = "kd";
+    let size = (amount / ct_val).floor();
+    match origin_side {
+        "kd" => {
+            position_side = "long";
+            order_side = "buy";
+        }
+        "pd" => {
+            position_side = "long";
+            order_side = "sell";
+        }
+        "kk" => {
+            position_side = "short";
+            order_side = "buy";
+        }
+        "pk" => {
+            position_side = "short";
+            order_side = "sell";
+        }
+        _ => {
+            error!("下单参数错误");
+            position_side = "error";
+            order_side = "error";
+        }
+    };
+    let mut ret = get_rest();
+    let req_data = ret.order(symbol, position_side.to_string(), order_side.to_string(), size, price, custom_id.to_string()).await;
+    println!("coinex--查询swap_order--{:?}", req_data);
+}
+
+#[tokio::test]
+async fn rest_cancel_order_test() {
+    let mut ret = get_rest();
+    let req_data = ret.cancel_order("FTMUSDT".to_string(), "", "175974369as").await;
+
+    println!("coinex--查询cancel_order--{} {:?}",req_data.data.is_null(), req_data);
+}
+
+#[tokio::test]
+async fn rest_cancel_all_order_test() {
+    let mut ret = get_rest();
+    let ct_val = Decimal::ONE;
+    let orders_res_data = ret.get_pending_orders().await;
+    let mut result = vec![];
+    if orders_res_data.code == 200 {
+        let orders_res_data_json = orders_res_data.data.as_array().unwrap();
+        for order in orders_res_data_json {
+            let cancel_res_data = ret.cancel_order_all( order["market"].as_str().unwrap().to_string()).await;
+            if cancel_res_data.code == 200 {
+                result.push(order.clone())
+            }
+        }
+    }
+    println!("coinex--查询cancel_all_order--{:?}", result);
+}
+
+#[tokio::test]
+async fn rest_finish_order_list_test() {
+    let mut ret = get_rest();
+    let req_data = ret.get_finished_orders().await;
+    println!("coinex--查询finish_order--{:?}", req_data);
+}
+
+#[tokio::test]
+async fn rest_pending_order_list_test() {
+    let mut ret = get_rest();
+    let req_data = ret.get_pending_orders().await;
+    println!("coinex--查询finish_order--{:?}", req_data);
+}
+
+#[tokio::test]
+async fn rest_setting_dual_leverage_test() {
+    let mut ret = get_rest();
+    let req_data = ret.setting_dual_leverage("FTMUSDT".to_string(), 10).await;
+    println!("coinex--查询setting_dual_leverage--{:?}", req_data);
+}
+
+#[tokio::test]
+async fn rest_time() {
+    let mut ret = get_rest();
+    let res_data = ret.get_server_time().await;
+    let res_data_json: Value = res_data.data;
+    let result = res_data_json["timestamp"].to_string();
+    println!("coinex--time--{:?}", result);
+}
+
+fn get_rest() -> CoinexSwapRest {
+    let mut btree_map: BTreeMap<String, String> = BTreeMap::new();
+    btree_map.insert("access_key".to_string(), ACCESS_KEY.to_string());
+    btree_map.insert("secret_key".to_string(), SECRET_KEY.to_string());
+
+    let ba_exc = CoinexSwapRest::new(btree_map);
+    ba_exc
+}

+ 708 - 0
standard/src/coinex_swap.rs

@@ -0,0 +1,708 @@
+use std::collections::{BTreeMap};
+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, ToPrimitive};
+use serde_json::{json, Value};
+use tokio::spawn;
+use tokio::time::Instant;
+use tracing::{error, info, trace};
+use tracing_subscriber::fmt::format;
+use exchanges::coinex_swap_rest::CoinexSwapRest;
+use crate::{Platform, ExchangeEnum, Account, Position, Ticker, Market, Order, OrderCommand, PositionModeEnum};
+use global::trace_stack::TraceStack;
+use crate::utils::get_tick_size;
+
+#[allow(dead_code)]
+#[derive(Clone)]
+pub struct CoinexSwap {
+    exchange: ExchangeEnum,
+    symbol: String,
+    is_colo: bool,
+    params: BTreeMap<String, String>,
+    request: CoinexSwapRest,
+    market: Market,
+    order_sender: Sender<Order>,
+    error_sender: Sender<Error>,
+}
+
+impl CoinexSwap {
+    pub async fn new(symbol: String, is_colo: bool, params: BTreeMap<String, String>, order_sender: Sender<Order>, error_sender: Sender<Error>) -> CoinexSwap {
+        let market = Market::new();
+        let mut coinex_swap = CoinexSwap {
+            exchange: ExchangeEnum::CoinexSwap,
+            symbol: symbol.to_uppercase(),
+            is_colo,
+            params: params.clone(),
+            request: CoinexSwapRest::new(params.clone()),
+            market,
+            order_sender,
+            error_sender,
+        };
+
+        // 修改持仓模式
+        let symbol_array: Vec<&str> = symbol.split("_").collect();
+        let mode_result = coinex_swap.set_dual_mode(symbol_array[1], true).await;
+        match mode_result {
+            Ok(_) => {
+                trace!("Coinex:设置持仓模式成功!")
+            }
+            Err(error) => {
+                error!("Coinex:设置持仓模式失败!mode_result={}", error)
+            }
+        }
+        // 获取市场信息
+        coinex_swap.market = CoinexSwap::get_market(&mut coinex_swap).await.unwrap_or(coinex_swap.market);
+        return coinex_swap;
+    }
+}
+
+#[async_trait]
+impl Platform for CoinexSwap {
+    // 克隆方法
+    fn clone_box(&self) -> Box<dyn Platform + Send + Sync> { Box::new(self.clone()) }
+    // 获取交易所模式
+    fn get_self_exchange(&self) -> ExchangeEnum {
+        ExchangeEnum::CoinexSwap
+    }
+    // 获取交易对
+    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_json: Value = res_data.data;
+            let result = res_data_json["timestamp"].to_string();
+            Ok(result)
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+    // 获取账号信息
+    async fn get_account(&mut self) -> Result<Account, Error> {
+        let symbol_array: Vec<&str> = self.symbol.split("_").collect();
+        let coin = symbol_array[1].to_string().to_uppercase();
+        let res_data = self.request.get_account().await;
+        if res_data.code == 200 {
+            let res_data_array = res_data.data.as_array().unwrap();
+            for res_data_json in res_data_array.iter() {
+                if res_data_json["ccy"].as_str().unwrap() == coin {
+                    let frozen_balance= Decimal::from_str(res_data_json["frozen"].as_str().unwrap()).unwrap();
+                    let available_balance = Decimal::from_str(res_data_json["available"].as_str().unwrap()).unwrap();
+                    let balance = frozen_balance + available_balance;
+                    let result = Account {
+                        coin: symbol_array[1].to_string(),
+                        balance,
+                        available_balance,
+                        frozen_balance,
+                        stocks: Decimal::ZERO,
+                        available_stocks: Decimal::ZERO,
+                        frozen_stocks: Decimal::ZERO,
+                    };
+                    return Ok(result);
+                }
+            }
+        }
+        Err(Error::new(ErrorKind::Other, res_data.to_string()))
+    }
+
+    async fn get_spot_account(&mut self) -> Result<Vec<Account>, Error> {
+        Err(Error::new(ErrorKind::NotFound, "coinex_swap:该方法暂未实现".to_string()))
+    }
+
+    // 获取持仓信息
+    async fn get_position(&mut self) -> Result<Vec<Position>, Error> {
+        let symbol: String = self.symbol.replace("_", "").to_uppercase();
+        let ct_val = self.market.ct_val;
+        let res_data = self.request.get_position(symbol).await;
+        if res_data.code == 200 {
+            let res_data_json = res_data.data.as_array().unwrap();
+            let result = res_data_json.iter().map(|item| { format_position_item(item, ct_val) }).collect();
+            Ok(result)
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    // 获取所有持仓
+    async fn get_positions(&mut self) -> Result<Vec<Position>, Error> {
+        let res_data = self.request.get_user_position().await;
+        if res_data.code == 200 {
+            let res_data_json = res_data.data.as_array().unwrap();
+            let result = res_data_json.iter().map(|item| { format_position_item(item, Decimal::ONE) }).collect();
+            Ok(result)
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+    // 获取市场行情
+    async fn get_ticker(&mut self) -> Result<Ticker, Error> {
+        let symbol: String = self.symbol.replace("_", "");
+        let res_data = self.request.get_ticker(symbol.clone()).await;
+        if res_data.code == 200 {
+            let res_data_json = res_data.data.as_array().unwrap();
+            let ticker_info = res_data_json.iter().find(|item| item["market"].as_str().unwrap() == symbol);
+            match ticker_info {
+                None => {
+                    error!("coinex_swap:获取Ticker信息错误!\nget_ticker:res_data={:?}", res_data);
+                    Err(Error::new(ErrorKind::Other, res_data.to_string()))
+                }
+                Some(value) => {
+                    let result = Ticker {
+                        time: chrono::Utc::now().timestamp_millis(),
+                        high: Decimal::from_str(value["high"].as_str().unwrap()).unwrap(),
+                        low: Decimal::from_str(value["low"].as_str().unwrap()).unwrap(),
+                        sell: Decimal::from_str(value["last"].as_str().unwrap()).unwrap(),
+                        buy: Decimal::from_str(value["last"].as_str().unwrap()).unwrap(),
+                        last: Decimal::from_str(value["last"].as_str().unwrap()).unwrap(),
+                        volume: Decimal::from_str(value["volume"].as_str().unwrap()).unwrap(),
+                    };
+                    Ok(result)
+                }
+            }
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    async fn get_ticker_symbol(&mut self, symbol_param: String) -> Result<Ticker, Error> {
+        let symbol: String = symbol_param.replace("_", "").to_uppercase();
+        let res_data = self.request.get_ticker(symbol.clone()).await;
+        if res_data.code == 200 {
+            let res_data_json = res_data.data.as_array().unwrap();
+            let ticker_info = res_data_json.iter().find(|item| item["contract"].as_str().unwrap() == symbol);
+            match ticker_info {
+                None => {
+                    error!("coinex_swap:获取Ticker信息错误!\nget_ticker:res_data={:?}", res_data);
+                    Err(Error::new(ErrorKind::Other, res_data.to_string()))
+                }
+                Some(value) => {
+                    let result = Ticker {
+                        time: chrono::Utc::now().timestamp_millis(),
+                        high: Decimal::from_str(value["high"].as_str().unwrap()).unwrap(),
+                        low: Decimal::from_str(value["low"].as_str().unwrap()).unwrap(),
+                        sell: Decimal::from_str(value["last"].as_str().unwrap()).unwrap(),
+                        buy: Decimal::from_str(value["last"].as_str().unwrap()).unwrap(),
+                        last: Decimal::from_str(value["last"].as_str().unwrap()).unwrap(),
+                        volume: Decimal::from_str(value["volume"].as_str().unwrap()).unwrap(),
+                    };
+                    Ok(result)
+                }
+            }
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    async fn get_market(&mut self) -> Result<Market, Error> {
+        let symbol_array: Vec<&str> = self.symbol.split("_").collect();
+        let symbol = format!("{}{}", symbol_array[0], symbol_array[1]);
+        let res_data = self.request.get_market_details(symbol.clone()).await;
+        if res_data.code == 200 {
+            let res_data_json = res_data.data.as_array().unwrap();
+            let market_info = res_data_json.iter().find(|item| item["market"].as_str().unwrap() == symbol);
+            match market_info {
+                None => {
+                    error!("coinex_swap:获取Market信息错误!\nget_market:res_data={:?}", res_data);
+                    Err(Error::new(ErrorKind::Other, res_data.to_string()))
+                }
+                Some(value) => {
+                    // 报价精度字符串
+                    let price_precision_i64 = value["quote_ccy_precision"].as_i64().unwrap();
+                    // 价格最小变动数值
+                    let tick_size = get_tick_size(price_precision_i64.to_u32().unwrap());
+                    // 报价精度
+                    let price_precision = Decimal::from_i64(price_precision_i64).unwrap();
+                    // 最小数量
+                    let min_qty = Decimal::from_str(value["min_amount"].as_str().unwrap()).unwrap();
+                    // 数量没有最大值
+                    let max_qty = Decimal::MAX;
+                    // 没有张数
+                    let ct_val = Decimal::ONE;
+
+                    let amount_size = min_qty * ct_val;
+                    let amount_precision = Decimal::from_u32(amount_size.scale()).unwrap();
+                    let min_notional = min_qty * ct_val;
+                    let max_notional = max_qty * ct_val;
+
+                    let result = Market {
+                        symbol: self.symbol.clone(),
+                        base_asset: symbol_array[0].to_string(),
+                        quote_asset: symbol_array[1].to_string(),
+                        tick_size,
+                        amount_size,
+                        price_precision,
+                        amount_precision,
+                        min_qty,
+                        max_qty,
+                        min_notional,
+                        max_notional,
+                        ct_val,
+                    };
+                    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_upper = symbol.to_uppercase();
+        let symbol_array: Vec<&str> = symbol_upper.split("_").collect();
+        let symbol = format!("{}{}", symbol_array[0], symbol_array[1]);
+        let res_data = self.request.get_market_details(symbol.clone()).await;
+        if res_data.code == 200 {
+            let res_data_json = res_data.data.as_array().unwrap();
+            let market_info = res_data_json.iter().find(|item| item["name"].as_str().unwrap() == symbol.clone());
+            match market_info {
+                None => {
+                    error!("coinex_swap:获取Market信息错误!\nget_market:res_data={:?}", res_data);
+                    Err(Error::new(ErrorKind::Other, res_data.to_string()))
+                }
+                Some(value) => {
+                    let tick_size = Decimal::from_str(value["quote_ccy_precision"].as_str().unwrap()).unwrap();
+                    let min_qty = Decimal::from_str(&value["min_amount"].to_string()).unwrap();
+                    // 数量没有最大值
+                    let max_qty = Decimal::MAX;
+                    // 没有张数
+                    let ct_val = Decimal::ONE;
+
+                    let amount_size = min_qty * ct_val;
+                    let price_precision = Decimal::from_u32(tick_size.scale()).unwrap();
+                    let amount_precision = Decimal::from_u32(amount_size.scale()).unwrap();
+                    let min_notional = min_qty * ct_val;
+                    let max_notional = max_qty * ct_val;
+
+                    let result = Market {
+                        symbol: self.symbol.clone(),
+                        base_asset: symbol_array[0].to_string(),
+                        quote_asset: symbol_array[1].to_string(),
+                        tick_size,
+                        amount_size,
+                        price_precision,
+                        amount_precision,
+                        min_qty,
+                        max_qty,
+                        min_notional,
+                        max_notional,
+                        ct_val,
+                    };
+                    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 ct_val = self.market.ct_val;
+        let res_data;
+        if order_id.eq("") {
+            // 通过客户端id查询  只有未完成的订单才能查询出来
+            res_data = self.request.get_pending_order(format!("t-{}", custom_id)).await;
+        } else {
+            res_data = self.request.get_order_details(order_id.to_string()).await;
+        };
+
+        if res_data.code == 200 {
+            let res_data_json: Value = res_data.data;
+            let result = format_order_item(res_data_json, ct_val, "open");
+            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 symbol = self.symbol.replace("_", "").to_uppercase();
+        let ct_val = self.market.ct_val;
+        let status_order;
+        let res_data;
+        if(status == "pending"){
+            res_data = self.request.get_pending_orders().await;
+            status_order = "open";
+        } else if (status == "finish"){
+            res_data = self.request.get_finished_orders().await;
+            status_order = "filled";
+        }else{
+            return Err(Error::new(ErrorKind::Other, status));
+        }
+        if res_data.code == 200 {
+            let res_data_json = res_data.data.as_array().unwrap();
+            let order_info: Vec<_> = res_data_json.iter().filter(|item| item["market"].as_str().unwrap_or("") == symbol).collect();
+            let result = order_info.iter().map(|&item| format_order_item(item.clone(), ct_val, status_order)).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 = self.symbol.replace("_", "").to_uppercase();
+        let ct_val = self.market.ct_val;
+        let order_side;
+        let position_side;
+        let size = (amount / ct_val).floor();
+        match origin_side {
+            "kd" => {
+                position_side = "long";
+                order_side = "buy";
+            }
+            "pd" => {
+                position_side = "long";
+                order_side = "sell";
+            }
+            "kk" => {
+                position_side = "short";
+                order_side = "sell";
+            }
+            "pk" => {
+                position_side = "short";
+                order_side = "buy";
+            }
+            _ => {
+                error!("下单参数错误");
+                position_side = "error";
+                order_side = "error";
+            }
+        };
+        let res_data = self.request.order(symbol, position_side.to_string(), order_side.to_string(), size, price, custom_id.to_string()).await;
+        if res_data.code == 200 {
+            let res_data_json: Value = res_data.data;
+            // info!("take_order {}", res_data_json);
+            let result = format_order_item(res_data_json, ct_val, "open");
+            Ok(result)
+        } else {
+            // error!("take_order error {}", res_data.data);
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    async fn take_order_symbol(&mut self, symbol_y: String, ct_val: Decimal, custom_id: &str, origin_side: &str, price: Decimal, amount: Decimal) -> Result<Order, Error> {
+        let symbol = symbol_y.replace("_", "").to_uppercase();
+        let order_side;
+        let position_side;
+        let size = (amount / ct_val).floor();
+        match origin_side {
+            "kd" => {
+                position_side = "long";
+                order_side = "buy";
+            }
+            "pd" => {
+                position_side = "long";
+                order_side = "sell";
+            }
+            "kk" => {
+                position_side = "short";
+                order_side = "sell";
+            }
+            "pk" => {
+                position_side = "short";
+                order_side = "buy";
+            }
+            _ => {
+                error!("下单参数错误");
+                position_side = "error";
+                order_side = "error";
+            }
+        };
+        let res_data = self.request.order(symbol, position_side.to_string(), order_side.to_string(), size, price, custom_id.to_string()).await;
+        if res_data.code == 200 {
+            let res_data_json: Value = res_data.data;
+            info!("take_order_symbol {}", res_data_json);
+            let result = format_order_item(res_data_json, ct_val, "open");
+            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 symbol = self.symbol.replace("_", "").to_uppercase();
+
+        let ct_val = self.market.ct_val;
+        let res_data = self.request.cancel_order(symbol, order_id, custom_id).await;
+        if res_data.code == 200 {
+            let res_data_json: Value = res_data.data;
+            // info!("cancel_order {} order_id {} custom_id {}", res_data_json, order_id, custom_id);
+            let mut result = format_order_item(res_data_json, ct_val, "canceled");
+            result.custom_id = custom_id.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 = self.symbol.replace("_", "").to_uppercase();
+        let ct_val = self.market.ct_val;
+        let status = "canceled";
+        let res_data = self.request.cancel_order_all(symbol).await;
+        if res_data.code == 200 {
+            // let res_data_json = res_data.data.as_array().unwrap();
+            // info!("cancel_orders {:?}", res_data_json);
+            let result = vec![];
+            Ok(result)
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    async fn cancel_orders_all(&mut self) -> Result<Vec<Order>, Error> {
+        let ct_val = self.market.ct_val;
+        let orders_res_data = self.request.get_pending_orders().await;
+        let status = "canceled";
+        if orders_res_data.code == 200 {
+            let mut result = vec![];
+            let orders_res_data_json = orders_res_data.data.as_array().unwrap();
+            for order in orders_res_data_json {
+                let cancel_res_data = self.request.cancel_order_all( order["market"].as_str().unwrap().to_string()).await;
+                info!("cancel_orders_all {:?}", cancel_res_data);
+                if cancel_res_data.code == 200 {
+                    result.push(format_order_item(order.clone(), ct_val, status))
+                } else {
+                    return Err(Error::new(ErrorKind::Other, cancel_res_data.to_string()));
+                }
+            }
+            Ok(result)
+        } else {
+            Err(Error::new(ErrorKind::Other, orders_res_data.to_string()))
+        }
+    }
+
+    async fn take_stop_loss_order(&mut self, stop_price: Decimal, price: Decimal, side: &str) -> Result<Value, Error> {
+        Err(Error::new(ErrorKind::NotFound, "coin_ex:该交易所暂未实现自动订单下单".to_string()))
+    }
+
+    async fn cancel_stop_loss_order(&mut self, order_id: &str) -> Result<Value, Error> {
+        Err(Error::new(ErrorKind::NotFound, "coin_ex:该交易所暂未实现取消自动订单".to_string()))
+    }
+
+    // 设置持仓模式
+    async fn set_dual_mode(&mut self, coin: &str, is_dual_mode: bool) -> Result<String, Error> {
+        Err(Error::new(ErrorKind::NotFound, "coin_ex:该交易所只允许单向持仓,无法设置持仓模式".to_string()))
+    }
+
+    // 更新双持仓模式下杠杆
+    async fn set_dual_leverage(&mut self, leverage: &str) -> Result<String, Error> {
+        let leverage_int = leverage.parse::<i32>().unwrap();
+        let symbol = self.symbol.replace("_", "").to_uppercase();
+        let res_data = self.request.setting_dual_leverage(symbol, leverage_int).await;
+        if res_data.code == 200 {
+            let res_data_str = &res_data.data;
+            let result = res_data_str.clone();
+            Ok(result.to_string())
+        } else {
+            Err(Error::new(ErrorKind::Other, res_data.to_string()))
+        }
+    }
+
+    async fn set_auto_deposit_status(&mut self, _status: bool) -> Result<String, Error> { Err(Error::new(ErrorKind::NotFound, "Coinex:该交易所方法未实现".to_string())) }
+
+    async fn wallet_transfers(&mut self, coin: &str, from: &str, to: &str, amount: Decimal) -> Result<String, Error> {
+        Err(Error::new(ErrorKind::NotFound, "Coinex wallet_transfers:该交易所方法未实现".to_string()))
+    }
+
+    // 指令下单
+    async fn command_order(&mut self, order_command: &mut OrderCommand, trace_stack: &TraceStack) {
+        let mut handles = vec![];
+
+        // 下单指令
+        for item in order_command.limits_open.keys() {
+            let mut ts = trace_stack.clone();
+
+            let amount = Decimal::from_str(&*order_command.limits_open[item].get(0).unwrap().clone()).unwrap();
+            let side = order_command.limits_open[item].get(1).unwrap().clone();
+            let price = Decimal::from_str(&*order_command.limits_open[item].get(2).unwrap().clone()).unwrap();
+            let cid = order_command.limits_open[item].get(3).unwrap().clone();
+
+            //  order_name: [数量,方向,价格,c_id]
+            let mut self_clone = self.clone();
+            let handle = spawn(async move {
+                // info!("数量 {},方向 {},价格 {},c_id {}", amount, side, price, cid);
+                ts.on_before_send();
+                let result = self_clone.take_order(&cid, &side, price, amount).await;
+                ts.on_after_send();
+
+                match result {
+                    Ok(mut result) => {
+                        result.trace_stack = ts;
+                        // info!("数量 {},方向 {},价格 {},c_id {}", amount, side, price, cid);
+                        self_clone.order_sender.send(result).await.unwrap();
+                    }
+                    Err(error) => {
+                        let mut err_order = Order::new();
+                        err_order.custom_id = cid.clone();
+                        err_order.status = "REMOVE".to_string();
+                        // info!("err 数量 {},方向 {},价格 {},c_id {}", amount, side, price, cid);
+                        self_clone.order_sender.send(err_order).await.unwrap();
+                        self_clone.error_sender.send(error).await.unwrap();
+                    }
+                }
+            });
+            handles.push(handle)
+        }
+        let futures = FuturesUnordered::from_iter(handles);
+        // 等待所有任务完成
+        let _: Result<Vec<_>, _> = futures.try_collect().await;
+
+        // 撤销订单
+        let mut cancel_handlers = vec![];
+        for item in order_command.cancel.keys() {
+            let order_id = order_command.cancel[item].get(1).unwrap().clone();
+            let custom_id = order_command.cancel[item].get(0).unwrap().clone();
+
+            let mut self_clone = self.clone();
+            let handle = spawn(async move {
+                let result = self_clone.cancel_order(&order_id, &custom_id).await;
+                match result {
+                    Ok(_) => {
+                        // self_clone.order_sender.send(ord).await.unwrap();
+                    }
+                    Err(error) => {
+                        // 取消失败去查订单。
+                        let query_rst = self_clone.get_order_detail(&order_id, &custom_id).await;
+                        match query_rst {
+                            Ok(order) => {
+                                self_clone.order_sender.send(order).await.unwrap();
+                            }
+                            Err(_err) => {
+                                // error!("撤单失败,而且查单也失败了,Coinex_io_swap,oid={}, cid={}。", order_id.clone(), custom_id.clone());
+                                // panic!("撤单失败,而且查单也失败了,Coinex_io_swap,oid={}, cid={}。", order_id.clone(), custom_id.clone());
+                            }
+                        }
+                        self_clone.error_sender.send(error).await.unwrap();
+                    }
+                }
+            });
+            cancel_handlers.push(handle)
+        }
+        let futures = FuturesUnordered::from_iter(cancel_handlers);
+        // 等待所有任务完成
+        let _: Result<Vec<_>, _> = futures.try_collect().await;
+
+        // 检查订单指令
+        let mut check_handlers = vec![];
+        for item in order_command.check.keys() {
+            let order_id = order_command.check[item].get(1).unwrap().clone();
+            let custom_id = order_command.check[item].get(0).unwrap().clone();
+
+            let mut self_clone = self.clone();
+            let handle = spawn(async move {
+                let result = self_clone.get_order_detail(&order_id, &custom_id).await;
+                match result {
+                    Ok(result) => {
+                        self_clone.order_sender.send(result).await.unwrap();
+                    }
+                    Err(error) => {
+                        self_clone.error_sender.send(error).await.unwrap();
+                    }
+                }
+            });
+            check_handlers.push(handle)
+        }
+
+        let futures = FuturesUnordered::from_iter(check_handlers);
+        // 等待所有任务完成
+        let _: Result<Vec<_>, _> = futures.try_collect().await;
+    }
+}
+
+pub fn format_position_item(position: &Value, ct_val: Decimal) -> Position {
+    let mut position_mode = match position["side"].as_str().unwrap_or("") {
+        "long" => PositionModeEnum::Long,
+        "short" => PositionModeEnum::Short,
+        _ => {
+            error!("coinex_swap:格式化持仓模式错误!\nformat_position_item:position={:?}", position);
+            panic!("coinex_swap:格式化持仓模式错误!\nformat_position_item:position={:?}", position)
+        }
+    };
+    let size = Decimal::from_str(&position["open_interest"].as_str().unwrap()).unwrap();
+    let amount = size * ct_val;
+    Position {
+        symbol: position["market"].as_str().unwrap_or("").parse().unwrap(),
+        margin_level: Decimal::from_str(position["leverage"].as_str().unwrap()).unwrap(),
+        amount,
+        frozen_amount: Decimal::ZERO,
+        price: Decimal::from_str(position["avg_entry_price"].as_str().unwrap()).unwrap(),
+        profit: Decimal::from_str(position["unrealized_pnl"].as_str().unwrap()).unwrap(),
+        position_mode,
+        margin: Decimal::from_str(position["ath_margin_size"].as_str().unwrap()).unwrap(),
+    }
+}
+
+pub fn format_order_item(order: Value, ct_val: Decimal, status :&str) -> Order {
+    if order.is_null() {
+        return Order {
+            id: "".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: "limit".to_string(),
+            trace_stack: TraceStack::new(0, Instant::now()).on_special("688 trace_stack".to_string()),
+        }
+    }
+    // info!("order {}", order);
+    // 通过客户端id查询订单 只能查出未完成订单(没有状态字段)
+    let status = order["status"].as_str().unwrap_or(status);
+    let text = order["client_id"].as_str().unwrap_or("");
+    let size = Decimal::from_str(&order["amount"].as_str().unwrap()).unwrap();
+    let deal_amount = Decimal::from_str(&order["filled_amount"].as_str().unwrap()).unwrap();
+    let filled_value = Decimal::from_str(&order["filled_value"].as_str().unwrap()).unwrap();
+
+    let amount = size * ct_val;
+    let mut avg_price = Decimal::ZERO;
+    if deal_amount != Decimal::ZERO{
+        avg_price = filled_value/deal_amount;
+    }
+    let custom_status = if status == "filled" || status == "part_canceled" || status == "canceled" {
+        "REMOVE".to_string()
+    } else if status == "open" {
+        "NEW".to_string()
+    } else {
+        error!("coinex_swap:格式化订单状态错误!\nformat_order_item:order={:?}", order);
+        "NULL".to_string()
+    };
+    let rst_order = Order {
+        id: order["order_id"].to_string(),
+        custom_id: text.replace("t-my-custom-id_", "").replace("t-", ""),
+        price: Decimal::from_str(order["price"].as_str().unwrap()).unwrap(),
+        amount,
+        deal_amount,
+        avg_price,
+        status: custom_status,
+        order_type: "limit".to_string(),
+        trace_stack: TraceStack::new(0, Instant::now()).on_special("688 trace_stack".to_string()),
+    };
+    return rst_order;
+}

+ 159 - 0
standard/src/coinex_swap_handle.rs

@@ -0,0 +1,159 @@
+use std::str::FromStr;
+use rust_decimal::Decimal;
+use rust_decimal::prelude::FromPrimitive;
+use rust_decimal_macros::dec;
+use serde_json::Value;
+use tokio::time::Instant;
+use tracing::{error, info};
+use tracing_subscriber::fmt::format;
+use exchanges::response_base::ResponseData;
+use global::trace_stack::TraceStack;
+use crate::{Account, MarketOrder, Order, Position, PositionModeEnum, SpecialDepth, SpecialOrder, SpecialTicker};
+
+// 处理账号信息
+pub fn handle_account_info(res_data: &ResponseData, symbol: &String) -> Account {
+    let res_data_json = res_data.data["balance_list"].as_array().unwrap();
+    format_account_info(res_data_json, symbol)
+}
+
+pub fn format_account_info(data: &Vec<Value>, symbol: &String) -> Account {
+    let symbol_upper = symbol.to_uppercase();
+    let symbol_array: Vec<&str> = symbol_upper.split("_").collect();
+    let balance_info = data.iter().find(|&item| item["ccy"].as_str().unwrap().contains(symbol_array[1]));
+
+    match balance_info {
+        None => {
+            error!("Coinex:格式化账号信息错误!\nformat_account_info: data={:?}", data);
+            panic!("Coinex:格式化账号信息错误!\nformat_account_info: data={:?}", data)
+        }
+        Some(value) => {
+            let frozen_balance= Decimal::from_str(&value["frozen"].as_str().unwrap()).unwrap();
+            let available_balance = Decimal::from_str(&value["available"].as_str().unwrap()).unwrap();
+            let margin = Decimal::from_str(&value["margin"].as_str().unwrap()).unwrap();
+            let profit_unreal = Decimal::from_str(&value["unrealized_pnl"].as_str().unwrap()).unwrap();
+            let balance = frozen_balance + available_balance + margin + profit_unreal;
+            Account {
+                coin: symbol_array[1].to_string(),
+                balance,
+                available_balance: Decimal::ZERO,
+                frozen_balance: Decimal::ZERO,
+                stocks: Decimal::ZERO,
+                available_stocks: Decimal::ZERO,
+                frozen_stocks: Decimal::ZERO,
+            }
+        }
+    }
+}
+
+// 处理position信息
+pub fn handle_position(res_data: &ResponseData, ct_val: &Decimal) -> Vec<Position> {
+    let res_data_json = &res_data.data["position"];
+    let position =  format_position_item(res_data_json, ct_val);
+    vec![position]
+}
+
+pub fn format_position_item(position: &Value, ct_val: &Decimal) -> Position {
+    let mut position_mode = match position["side"].as_str().unwrap_or("") {
+        "long" => PositionModeEnum::Long,
+        "short" => PositionModeEnum::Short,
+        _ => {
+            error!("Coinex:格式化持仓模式错误!\nformat_position_item:position={:?}", position);
+            panic!("Coinex:格式化持仓模式错误!\nformat_position_item:position={:?}", position)
+        }
+    };
+    let size = Decimal::from_str(&position["open_interest"].as_str().unwrap()).unwrap();
+    let amount = size * ct_val;
+    Position {
+        symbol: position["market"].as_str().unwrap().to_string(),
+        margin_level: Decimal::from_str(&position["leverage"].as_str().unwrap()).unwrap(),
+        amount,
+        frozen_amount: Decimal::ZERO,
+        price: Decimal::from_str(&position["avg_entry_price"].as_str().unwrap()).unwrap(),
+        profit: Decimal::from_str(&position["unrealized_pnl"].as_str().unwrap()).unwrap(),
+        position_mode,
+        margin: Decimal::from_str(&position["ath_margin_size"].as_str().unwrap()).unwrap(),
+    }
+}
+
+// 处理order信息
+pub fn handle_order(res_data: ResponseData, ct_val: Decimal) -> SpecialOrder {
+    let status = res_data.data["event"].as_str().unwrap();
+    let res_data_json = &res_data.data["order"];
+    let order_info = format_order_item(res_data_json, ct_val, status);
+
+    SpecialOrder {
+        name: res_data.label,
+        order: vec![order_info],
+    }
+}
+
+pub fn format_order_item(order: &Value, ct_val: Decimal, status: &str) -> Order {
+    let text = order["client_id"].as_str().unwrap_or("");
+    let size = Decimal::from_str(order["amount"].as_str().unwrap()).unwrap();
+    let left = Decimal::from_str(order["unfilled_amount"].as_str().unwrap()).unwrap();
+    // 成交量
+    let filled_amount = Decimal::from_str(order["filled_amount"].as_str().unwrap_or("0")).unwrap();
+    // 成交额
+    let filled_value = Decimal::from_str(order["filled_value"].as_str().unwrap()).unwrap();
+    // 成交均价
+    let mut avg_price = Decimal::ZERO;
+    if filled_amount != Decimal::ZERO{
+        avg_price = filled_value/filled_amount;
+    }
+    let amount = size * ct_val;
+    let deal_amount = (size - left) * ct_val;
+    let custom_status = if status == "finish" { "REMOVE".to_string() } else if status == "put" { "NEW".to_string() } else {
+        "NULL".to_string()
+    };
+    let rst_order = Order {
+        id: order["order_id"].to_string(),
+        custom_id: text.replace("t-my-custom-id_", "").replace("t-", ""),
+        price: Decimal::from_str(order["price"].as_str().unwrap()).unwrap(),
+        amount,
+        deal_amount,
+        avg_price,
+        status: custom_status,
+        order_type: "limit".to_string(),
+        trace_stack: TraceStack::new(0, Instant::now()).on_special("120 Coinex_handle".to_string()),
+    };
+    return rst_order;
+}
+
+// 处理特殊Ticket信息
+pub fn handle_ticker(res_data: &ResponseData) -> SpecialDepth {
+    let depth = &res_data.data["depth"];
+
+    let bp = Decimal::from_str(depth["bids"][0][0].as_str().unwrap()).unwrap();
+    let bq = Decimal::from_str(depth["bids"][0][1].as_str().unwrap()).unwrap();
+    let ap = Decimal::from_str((depth["asks"][0][0].as_str().unwrap())).unwrap();
+    let aq = Decimal::from_str(depth["asks"][0][1].as_str().unwrap()).unwrap();
+    let mp = (bp + ap) * dec!(0.5);
+    let t = Decimal::from_i64(depth.get("checksum").unwrap().as_i64().unwrap_or(0i64)).unwrap();
+    let create_at = depth.get("updated_at").unwrap().as_i64().unwrap() * 1000;
+
+    let ticker_info = SpecialTicker { sell: ap, buy: bp, mid_price: mp, t, create_at };
+    let depth_info = vec![bp, bq, ap, aq];
+
+    SpecialDepth {
+        name: (*res_data).label.clone(),
+        depth: depth_info,
+        ticker: ticker_info,
+        t,
+        create_at,
+    }
+}
+
+pub fn format_depth_items(value: &Value) -> Vec<MarketOrder> {
+    if value.is_null() {
+        return vec![];
+    }
+    let mut depth_items: Vec<MarketOrder> = vec![];
+    for value in value.as_array().unwrap() {
+        let values = value.as_array().unwrap();
+        depth_items.push(MarketOrder {
+            price: Decimal::from_str(values[0].as_str().unwrap()).unwrap(),
+            amount: Decimal::from_str(values[1].as_str().unwrap()).unwrap(),
+        })
+    }
+    return depth_items;
+}

+ 5 - 0
standard/src/exchange.rs

@@ -10,6 +10,7 @@ use crate::kucoin_swap::KucoinSwap;
 // use crate::bitget_spot::BitgetSpot;
 use crate::bybit_swap::BybitSwap;
 use crate::bitget_swap::BitgetSwap;
+use crate::coinex_swap::CoinexSwap;
 // use crate::kucoin_spot::KucoinSpot;
 // use crate::okx_swap::OkxSwap;
 
@@ -29,6 +30,7 @@ pub enum ExchangeEnum {
     // KucoinSpot,
     // OkxSwap,
     // BitgetSpot,
+    CoinexSwap,
     BitgetSwap,
     BybitSwap
 }
@@ -102,6 +104,9 @@ impl Exchange {
             ExchangeEnum::BybitSwap => {
                 Box::new(BybitSwap::new(symbol, is_colo, params, order_sender, error_sender).await)
             }
+            ExchangeEnum::CoinexSwap => {
+                Box::new(CoinexSwap::new(symbol, is_colo, params, order_sender, error_sender).await)
+            }
         }
     }
 }

+ 22 - 2
standard/src/handle_info.rs

@@ -7,7 +7,7 @@ use tracing::{error, info};
 use exchanges::response_base::ResponseData;
 use global::public_params;
 use crate::exchange::ExchangeEnum;
-use crate::{binance_swap_handle, gate_swap_handle, bybit_swap_handle, bitget_swap_handle, kucoin_handle};
+use crate::{binance_swap_handle, gate_swap_handle, bybit_swap_handle, bitget_swap_handle, kucoin_handle, coinex_swap_handle};
 use crate::{Account, MarketOrder, Position, SpecialDepth, SpecialOrder, SpecialTicker};
 
 #[allow(dead_code)]
@@ -50,13 +50,16 @@ impl HandleSwapInfo {
             ExchangeEnum::BybitSwap => {
                 bybit_swap_handle::handle_account_info(res_data, symbol)
             }
+            ExchangeEnum::CoinexSwap => {
+                coinex_swap_handle::handle_account_info(res_data, symbol)
+            }
             _ => {
                 error!("未找到该交易所!handle_account_info: {:?}",exchange);
                 panic!("未找到该交易所!handle_account_info: {:?}", exchange);
             }
         }
     }
-    // 处理Ticker信息
+    // 处理特殊Ticket信息
     pub fn handle_book_ticker(exchange: ExchangeEnum, res_data: &ResponseData) -> SpecialDepth {
         match exchange {
             // ExchangeEnum::BinanceSpot => {
@@ -91,6 +94,9 @@ impl HandleSwapInfo {
             ExchangeEnum::BybitSwap => {
                 bybit_swap_handle::handle_ticker(res_data)
             }
+            ExchangeEnum::CoinexSwap => {
+                coinex_swap_handle::handle_ticker(res_data)
+            }
         }
     }
     // 处理position信息
@@ -123,6 +129,9 @@ impl HandleSwapInfo {
             ExchangeEnum::BybitSwap => {
                 bybit_swap_handle::handle_position(res_data, ct_val)
             }
+            ExchangeEnum::CoinexSwap => {
+                coinex_swap_handle::handle_position(res_data, ct_val)
+            }
         }
     }
     // 处理订单信息
@@ -153,6 +162,9 @@ impl HandleSwapInfo {
             ExchangeEnum::BybitSwap => {
                 bybit_swap_handle::handle_order(res_data, ct_val)
             }
+            ExchangeEnum::CoinexSwap => {
+                coinex_swap_handle::handle_order(res_data, ct_val)
+            }
         }
     }
 
@@ -292,6 +304,14 @@ pub fn format_depth(exchange: ExchangeEnum, res_data: &ResponseData) -> DepthPar
             depth_bids = bybit_swap_handle::format_depth_items(res_data.data["b"].clone());
             t = Decimal::from_i64(res_data.reach_time).unwrap();
             create_at = res_data.reach_time * 1000;
+        },
+        ExchangeEnum::CoinexSwap => {
+            let depth = &res_data.data["depth"];
+            depth_asks = coinex_swap_handle::format_depth_items(&depth["asks"].clone());
+            depth_bids = coinex_swap_handle::format_depth_items(&depth["bids"].clone());
+            let time = depth.get("updated_at").unwrap().as_i64().unwrap_or(0i64);
+            t = Decimal::from_i64(time).unwrap();
+            create_at = time * 1000;
         }
     }
 

+ 2 - 0
standard/src/lib.rs

@@ -36,6 +36,8 @@ mod bybit_swap;
 mod bybit_swap_handle;
 mod bitget_swap;
 mod bitget_swap_handle;
+mod coinex_swap;
+mod coinex_swap_handle;
 
 /// 持仓模式枚举
 /// - `Both`:单持仓方向

+ 6 - 0
standard/src/utils.rs

@@ -1,3 +1,4 @@
+use rust_decimal::Decimal;
 use tracing::trace;
 use exchanges::proxy;
 use crate::exchange::ExchangeEnum;
@@ -32,6 +33,11 @@ pub fn symbol_enter_mapper(exchange_enum: ExchangeEnum, symbol: &str) -> String
     }
 }
 
+// 获取指定精度下的tick_size
+pub fn get_tick_size(precision: u32) -> Decimal {
+    return Decimal::new(1, 0) / Decimal::new(10i64.pow(precision), 0);
+}
+
 /// 币种映射器
 #[allow(dead_code)]
 pub fn symbol_out_mapper(exchange_enum: ExchangeEnum, symbol: &str) -> String {

+ 5 - 5
strategy/config.toml

@@ -1,16 +1,16 @@
 broker_id = ""
 account_name = "test_account_001"
-access_key = "B5261D7FB987478FA47E14F00C4A653B"
-secret_key = "CAF69F5A23DE0EA32B7B15777AF47609C61B1EB17E5AC203"
-pass_key = "21314123"
+access_key = ""
+secret_key = ""
+pass_key = ""
 exchange = "coinex_usdt_swap"
 pair = "rose_usdt"
-open = 0.001
+open = 0.01
 close = 0.0002
 lever_rate = 1.0
 interval = 0.1
 ref_exchange = ["binance_usdt_swap"]
-ref_pair = ["rose_usdt"]
+ref_pair = ["ftm_usdt"]
 used_pct = 0.9
 index = 0
 save = 0

+ 156 - 0
strategy/src/coinex_usdt_swap.rs

@@ -0,0 +1,156 @@
+use tracing::{error, info};
+use std::collections::BTreeMap;
+use std::sync::Arc;
+use std::sync::atomic::AtomicBool;
+use rust_decimal::Decimal;
+use tokio::spawn;
+use tokio::sync::Mutex;
+use exchanges::coinex_swap_rest::CoinexSwapRest;
+use exchanges::coinex_swap_ws::{CoinexSwapLogin, CoinexSwapSubscribeType, CoinexSwapWs};
+use exchanges::response_base::ResponseData;
+use global::trace_stack::{TraceStack};
+use standard::exchange::ExchangeEnum::{CoinexSwap};
+use crate::model::{OrderInfo};
+use crate::core::Core;
+use crate::exchange_disguise::on_special_depth;
+
+// 1交易、0参考 coinex 合约 启动
+pub async fn coinex_swap_run(is_shutdown_arc: Arc<AtomicBool>,
+                           is_trade: bool,
+                           core_arc: Arc<Mutex<Core>>,
+                           name: String,
+                           symbols: Vec<String>,
+                           is_colo: bool,
+                           exchange_params: BTreeMap<String, String>) {
+    let (write_tx, write_rx) = futures_channel::mpsc::unbounded();
+
+    let write_tx_am = Arc::new(Mutex::new(write_tx));
+    let symbols_clone = symbols.clone();
+    spawn(async move {
+        let mut ws;
+        // 交易
+        if is_trade {
+            let login_param = parse_btree_map_to_coinex_swap_login(exchange_params);
+            ws = CoinexSwapWs::new_label(name.clone(), Some(login_param));
+            ws.set_subscribe(vec![
+                // CoinexSwapSubscribeType::PuFuturesDeals,
+                CoinexSwapSubscribeType::PuFuturesDepth,
+
+                CoinexSwapSubscribeType::PrFuturesOrders,
+                CoinexSwapSubscribeType::PrFuturesPositions,
+                CoinexSwapSubscribeType::PrFuturesBalances,
+            ]);
+        } else { // 参考
+            ws = CoinexSwapWs::new_label(name.clone(), None);
+            ws.set_subscribe(vec![
+                CoinexSwapSubscribeType::PuFuturesDeals,
+                CoinexSwapSubscribeType::PuFuturesDepth
+            ]);
+        }
+
+        // 读取数据
+        let mut update_flag_u = Decimal::ZERO;
+        let core_arc_clone = Arc::clone(&core_arc);
+        let multiplier = core_arc_clone.lock().await.platform_rest.get_self_market().amount_size;
+        let run_symbol = symbols.clone()[0].clone();
+
+        let fun = move |data: ResponseData| {
+            let core_arc_cc = core_arc_clone.clone();
+            // 在 async 块之前克隆 Arc
+            let mul = multiplier.clone();
+            let rs = run_symbol.clone();
+
+            async move {
+                on_data(core_arc_cc,
+                        &mut update_flag_u,
+                        &mul,
+                        &rs,
+                        data,
+                ).await
+            }
+        };
+
+        // 建立链接
+        ws.set_symbols(symbols_clone);
+        ws.ws_connect_async(is_shutdown_arc, fun, &write_tx_am, write_rx).await.expect("链接失败(内部一个心跳线程应该已经关闭了)");
+    });
+}
+
+async fn on_data(core_arc_clone: Arc<Mutex<Core>>,
+                 update_flag_u: &mut Decimal,
+                 multiplier: &Decimal,
+                 run_symbol: &String,
+                 response: ResponseData) {
+    let mut trace_stack = TraceStack::new(response.time, response.ins);
+    trace_stack.on_after_span_line();
+
+    match response.channel.as_str() {
+        "depth.update" => {
+            trace_stack.set_source("coinex_usdt_swap.order_book".to_string());
+            let special_depth = standard::handle_info::HandleSwapInfo::handle_special_depth(CoinexSwap, &response);
+            trace_stack.on_after_format();
+
+            on_special_depth(core_arc_clone, update_flag_u, &response.label, &mut trace_stack, &special_depth).await;
+        }
+        "balance.update" => {
+            let account = standard::handle_info::HandleSwapInfo::handle_account_info(CoinexSwap, &response, run_symbol);
+            let mut core = core_arc_clone.lock().await;
+            core.update_equity(account).await;
+        }
+        "order.update" => {
+            trace_stack.set_source("coinex_swap.orders".to_string());
+            let orders = standard::handle_info::HandleSwapInfo::handle_order(CoinexSwap, response.clone(), multiplier.clone());
+
+            let mut order_infos:Vec<OrderInfo> = Vec::new();
+            for mut order in orders.order {
+                if order.status == "NULL" {
+                    error!("coinex_usdt_swap 未识别的订单状态:{:?}", response);
+                    continue;
+                }
+
+                let order_info = OrderInfo::parse_order_to_order_info(&mut order);
+                order_infos.push(order_info);
+            }
+            if !order_infos.is_empty(){
+                let mut core = core_arc_clone.lock().await;
+                core.update_order(order_infos, trace_stack).await;
+            }
+        }
+        "position.update" => {
+            // println!("coinex_usdt_swap 仓位推送:{:?}", response.data);
+            let positions = standard::handle_info::HandleSwapInfo::handle_position(CoinexSwap, &response, multiplier);
+            let mut core = core_arc_clone.lock().await;
+            core.update_position(positions).await;
+        }
+        "deals.update" => {
+            // let mut core = core_arc_clone.lock().await;
+            // let str = data.label.clone();
+            // if core.is_update.contains_key(&data.label) && *core.is_update.get(str.as_str()).unwrap(){
+            //     *max_buy = Decimal::ZERO;
+            //     *min_sell = Decimal::ZERO;
+            //     core.is_update.remove(str.as_str());
+            // }
+            // let trades: Vec<OriginalTradeGa> = serde_json::from_str(data.data.as_str()).unwrap();
+            // for trade in trades {
+            //     if trade.price > *max_buy || *max_buy == Decimal::ZERO{
+            //         *max_buy = trade.price
+            //     }
+            //     if trade.price < *min_sell || *min_sell == Decimal::ZERO{
+            //         *min_sell = trade.price
+            //     }
+            // }
+            // core.max_buy_min_sell_cache.insert(data.label, vec![*max_buy, *min_sell]);
+        }
+        _ => {
+            error!("未知推送类型");
+            error!(?response);
+        }
+    }
+}
+
+fn parse_btree_map_to_coinex_swap_login(exchange_params: BTreeMap<String, String>) -> CoinexSwapLogin {
+    CoinexSwapLogin {
+        api_key: exchange_params.get("access_key").unwrap().clone(),
+        secret: exchange_params.get("secret_key").unwrap().clone()
+    }
+}

+ 4 - 1
strategy/src/core.rs

@@ -22,7 +22,7 @@ use global::public_params::{ASK_PRICE_INDEX, BID_PRICE_INDEX, LENGTH};
 use global::trace_stack::TraceStack;
 use standard::{Account, Market, Order, OrderCommand, Platform, Position, PositionModeEnum, SpecialTicker, Ticker};
 use standard::exchange::{Exchange};
-use standard::exchange::ExchangeEnum::{BinanceSwap, BitgetSwap, BybitSwap, GateSwap, KucoinSwap};
+use standard::exchange::ExchangeEnum::{BinanceSwap, BitgetSwap, BybitSwap, CoinexSwap, GateSwap, KucoinSwap};
 
 use crate::model::{LocalPosition, OrderInfo, TokenParam};
 use crate::predictor::Predictor;
@@ -230,6 +230,9 @@ impl Core {
                 "bybit_usdt_swap" => {
                     Exchange::new(BybitSwap, symbol, params.colo != 0i8, exchange_params, order_sender, error_sender).await
                 }
+                "coinex_usdt_swap" => {
+                    Exchange::new(CoinexSwap, symbol, params.colo != 0i8, exchange_params, order_sender, error_sender).await
+                }
                 _ => {
                     error!("203未找到对应的交易所rest枚举!");
                     panic!("203未找到对应的交易所rest枚举!");

+ 7 - 0
strategy/src/exchange_disguise.rs

@@ -12,6 +12,7 @@ use crate::gate_swap::gate_swap_run;
 // use crate::binance_spot::reference_binance_spot_run;
 // use crate::bitget_spot::bitget_spot_run;
 use crate::bybit_usdt_swap::bybit_swap_run;
+use crate::coinex_usdt_swap::coinex_swap_run;
 // use crate::kucoin_spot::kucoin_spot_run;
 use crate::kucoin_swap::kucoin_swap_run;
 // use crate::okx_usdt_swap::okex_swap_run;
@@ -44,6 +45,9 @@ pub async fn run_transactional_exchange(is_shutdown_arc :Arc<AtomicBool>,
         "bybit_usdt_swap" => {
             bybit_swap_run(is_shutdown_arc,true, core_arc, name, symbols, is_colo, exchange_params).await;
         }
+        "coinex_usdt_swap" => {
+            coinex_swap_run(is_shutdown_arc,true, core_arc, name, symbols, is_colo, exchange_params).await;
+        }
         _ => {
             let msg = format!("不支持的交易交易所:{}", exchange_name);
             panic!("{}", msg);
@@ -87,6 +91,9 @@ pub async fn run_reference_exchange(is_shutdown_arc: Arc<AtomicBool>,
         "bybit_usdt_swap" => {
             bybit_swap_run(is_shutdown_arc, false, core_arc, name, symbols, is_colo, exchange_params).await;
         },
+        "coinex_usdt_swap" => {
+
+        }
         _ => {
             let msg = format!("不支持的参考交易所:{}", exchange_name);
             panic!("{}", msg);

+ 1 - 0
strategy/src/lib.rs

@@ -13,3 +13,4 @@ mod bitget_spot;
 mod okx_usdt_swap;
 mod bybit_usdt_swap;
 mod bitget_usdt_swap;
+mod coinex_usdt_swap;