gepangpang %!s(int64=2) %!d(string=hai) anos
pai
achega
59bffbc57a

+ 1 - 0
exchange_data_formatter/src/binance_handle.rs

@@ -47,6 +47,7 @@ pub(crate) fn handle_swap_account(data: String, symbol: String) -> Account {
         }
         Some(value) => {
             Account {
+                coin: value.asset.to_uppercase(),
                 balance: value.balance,
                 available_balance: value.available_balance,
                 frozen_balance: value.balance - value.available_balance,

+ 249 - 28
exchange_data_formatter/src/bitget_handle.rs

@@ -1,55 +1,276 @@
+use std::str::FromStr;
 use serde::{Deserialize, Serialize};
 use rust_decimal::Decimal;
+use rust_decimal::prelude::ToPrimitive;
 use rust_decimal_macros::dec;
 use tracing::error;
 use crate::{Account, Market, Order, Position, PositionModeEnum, Ticker, utils};
 
-/// Binance交易所账户信息请求数据结构
-/// - 接口`"/fapi/v2/balance"`
-///
-/// struct SwapAccount
-/// - `account_alias`: String, 账户唯一识别码
-/// - `asset`: String, 资产
-/// - `balance`: Decimal, 总余额
-/// - `cross_wallet_balance`: Decimal, 全仓余额
-/// - `cross_un_pnl`: Decimal, 全仓持仓未实现盈亏
-/// - `available_balance`: Decimal, 下单可用余额
-/// - `max_withdraw_amount`: Decimal, 最大可转出余额
-/// - `margin_available`: bool, 是否可用作联合保证金
-/// - `update_time`: i64,时间戳
+/// Bitget交易所账户信息请求数据结构
+/// - 接口`"/api/v2/spot/account/assets"`
+///
+/// struct SpotAccount
+/// - `coin`: String, 币种名称
+/// - `available`: String, 可用资产
+/// - `frozen`: Decimal, 冻结资产数量
+/// - `locked`: Decimal, 锁仓资产数量
+/// - `limit_available`: Decimal, 受限可用
+/// - `u_time`: Decimal, 更新时间
 #[derive(Debug, Deserialize, Serialize)]
 #[serde(rename_all = "camelCase")]
 struct SpotAccount {
     coin: String,
-    available: String,
+    available: Decimal,
     frozen: Decimal,
     locked: Decimal,
     limit_available: Decimal,
     u_time: String,
 }
+
 /// 处理合约本位货币账户信息
 ///
-/// pub(crate) fn handle_swap_account(data: String, symbol: String) -> Account;
+/// pub(crate) fn handle_spot_account(data: String, symbol: String) -> Account;
 ///
 /// - `data`:String, 交易所返回账户信息,JsonString
 /// - `symbol`:String, 交易币对
-pub(crate) fn handle_swap_account(data: String, symbol: String) -> Account {
-    let symbol_array: Vec<&str> = symbol.split("_").collect();
-    let balance_info_list: Vec<SwapAccount> = serde_json::from_str(&data).unwrap();
-    let balance_info = balance_info_list.iter().find(|item| item.asset == symbol_array[1]);
-    match balance_info {
+pub(crate) fn handle_spot_account(data: String) -> Vec<Account> {
+    let balance_info_list: Vec<SpotAccount> = serde_json::from_str(&data).unwrap();
+    balance_info_list.iter().map(|item| {
+        Account {
+            coin: item.coin.to_uppercase(),
+            balance: item.available + item.frozen,
+            available_balance: item.available,
+            frozen_balance: item.frozen,
+            stocks: dec!(0),
+            available_stocks: dec!(0),
+            frozen_stocks: dec!(0),
+        }
+    }).collect()
+}
+
+/// Bitget交易所行情信息请求数据结构
+/// - 接口`"/api/v2/spot/market/tickers"`
+///
+/// struct SpotTicker
+/// - `symbol`: String, 交易对名称
+/// - `high24h`: Decimal, 24小时最高价
+/// - `open`: Decimal, 	24小时开盘价
+/// - `low24h`: Decimal, 最新成交价
+/// - `last_pr`: Decimal, 24小时最低价
+/// - `quote_volume`: Decimal, 计价币成交额
+/// - `base_volume`: Decimal, 基础币成交额
+/// - `usdt_volume`: Decimal, USDT成交额
+/// - `bid_pr`: Decimal, 买一价
+/// - `ask_pr`: Decimal, 卖一价
+/// - `bid_sz`: Decimal, 买一量
+/// - `ask_sz`: Decimal, 卖一量
+/// - `open_utc`: Decimal, 零时区 开盘价
+/// - `ts`: i64, 当前时间。Unix毫秒时间戳,例如1690196141868
+/// - `change_utc24h`: Decimal, UTC0时涨跌幅, 0.01表示1%
+/// - `change24h`: Decimal, UTC0时涨跌幅, 0.01表示1%
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct SpotTicker {
+    symbol: String,
+    high24h: Decimal,
+    open: Decimal,
+    low24h: Decimal,
+    last_pr: Decimal,
+    quote_volume: Decimal,
+    base_volume: Decimal,
+    usdt_volume: Decimal,
+    bid_pr: Decimal,
+    ask_pr: Decimal,
+    bid_sz: Decimal,
+    ask_sz: Decimal,
+    open_utc: Decimal,
+    ts: String,
+    change_utc24h: Decimal,
+    change24h: Decimal,
+}
+
+/// 处理合约行情信息
+///
+/// pub(crate) fn handle_swap_ticker(data: String) -> Ticker;
+///
+/// - `data`:String, 交易所返回账户信息,JsonString
+pub(crate) fn handle_spot_ticker(data: String, symbol: String) -> Ticker {
+    let format_symbol = utils::format_symbol(symbol, "");
+    let ticker_info_list: Vec<SpotTicker> = serde_json::from_str(&data).unwrap();
+    let ticker_info = ticker_info_list.iter().find(|&item| item.symbol == format_symbol);
+    match ticker_info {
+        None => {
+            error!("Bitget:获取Ticker信息错误!\nhandle_spot_ticker:data={:?}", data);
+            panic!("Bitget:获取Ticker信息错误!\nhandle_spot_ticker:data={:?}", data)
+        }
+        Some(value) => {
+            Ticker {
+                time: value.ts.parse().unwrap(),
+                high: value.high24h,
+                low: value.low24h,
+                sell: value.ask_pr,
+                buy: value.bid_pr,
+                last: value.last_pr,
+                volume: dec!(-1),
+            }
+        }
+    }
+}
+
+/// Bitget交易所行情信息请求数据结构
+/// - 接口`"/api/v2/spot/public/symbols"`
+///
+/// struct SpotMarket
+/// - `symbol`: String, 交易对名称
+/// - `base_coin`: String, 基础币
+/// - `quote_coin`: String, 计价货币
+/// - `min_trade_amount`: Decimal, 最小交易数量
+/// - `max_trade_amount`: Decimal, 最大交易数量
+/// - `taker_fee_rate`: Decimal, 默认吃单手续费率,可被个人交易手续费率覆盖
+/// - `maker_fee_rate`: Decimal, 默认挂单手续费率,可被个人交易手续费率覆盖
+/// - `price_precision`: Decimal, 价格精度
+/// - `quantity_precision`: Decimal, 数量精度
+/// - `quote_precision`: Decimal, 右币精度
+/// - `status`: String, 上架状态
+/// - `min_trade_usdt`: Decimal, 最小USDT交易额
+/// - `buy_limit_price_ratio`: Decimal, 买入与现价的价差百分比
+/// - `sell_limit_price_ratio`: Decimal, 卖出与现价的价差百分比
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct SpotMarket {
+    symbol: String,
+    base_coin: String,
+    quote_coin: String,
+    min_trade_amount: Decimal,
+    max_trade_amount: Decimal,
+    taker_fee_rate: Decimal,
+    maker_fee_rate: Decimal,
+    price_precision: Decimal,
+    quantity_precision: Decimal,
+    quote_precision: Decimal,
+    status: String,
+    min_trade_u_s_d_t: Decimal,
+    buy_limit_price_ratio: Decimal,
+    sell_limit_price_ratio: Decimal,
+}
+
+/// 处理合约行情信息
+///
+/// pub(crate) fn handle_swap_market(data: String, symbol: String) -> Market;
+///
+/// - `data`:String, 交易所返回账户信息,JsonString
+/// - `symbol`:String, 交易币对
+pub(crate) fn handle_swap_market(data: String, symbol: String) -> Market {
+    let format_symbol = utils::format_symbol(symbol.clone(), "");
+    let market_info_list: Vec<SpotMarket> = serde_json::from_str(&data).unwrap();
+    let market_info = market_info_list.iter().find(|&item| item.symbol == format_symbol);
+    match market_info {
         None => {
-            panic!("格式化Binance账号信息错误,未找到该本位货币!\nhandle_account: balance_info={:?}", balance_info)
+            error!("Bitget:获取Market信息错误!\nhandle_spot_market:data={:?}", data);
+            panic!("Bitget:获取Market信息错误!\nhandle_spot_market:data={:?}", data)
         }
         Some(value) => {
-            Account {
-                balance: value.balance,
-                available_balance: value.available_balance,
-                frozen_balance: value.balance - value.available_balance,
-                stocks: dec!(0),
-                available_stocks: dec!(0),
-                frozen_stocks: dec!(0),
+            let tick_size = if value.price_precision > dec!(0) {
+                Decimal::from_str(&format!("0.{}1", "0".repeat(usize::try_from(value.price_precision - dec!(1)).unwrap()))).unwrap()
+            } else {
+                Decimal::ONE
+            };
+            let amount_size = if value.quantity_precision > dec!(0) {
+                Decimal::from_str(&format!("0.{}1", "0".repeat(usize::try_from(value.quantity_precision - dec!(1)).unwrap()))).unwrap()
+            } else {
+                Decimal::ONE
+            };
+            Market {
+                symbol: symbol.clone(),
+                base_asset: value.base_coin.clone(),
+                quote_asset: value.quote_coin.clone(),
+                tick_size,
+                amount_size,
+                price_precision: value.price_precision,
+                amount_precision: value.quantity_precision,
+                min_qty: value.min_trade_amount,
+                max_qty: value.max_trade_amount,
+                min_notional: value.min_trade_amount,
+                max_notional: value.max_trade_amount,
+                ct_val: Decimal::ONE,
             }
         }
     }
+}
+
+/// Bitget交易所行情信息请求数据结构
+/// - 接口`"/api/v2/spot/trade/orderInfo"`
+///
+/// struct SwapOrder
+/// `id`: i128, 合约订单 ID
+/// `create_time`: i64, 订单创建时间
+/// `finish_time`: Option<i64>, 订单结束时间,未结束订单无此字段返回
+/// `finish_as`: String, 结束方式(filled: 完全成交、cancelled: 用户撤销、liquidated: 强制平仓撤销、ioc: 未立即完全成交,因为tif设置为ioc、auto_deleveraged: 自动减仓撤销、reduce_only: 增持仓位撤销,因为设置reduce_only或平仓、position_closed: 因为仓位平掉了,所以挂单被撤掉、reduce_out: 只减仓被排除的不容易成交的挂单、stp: 订单发生自成交限制而被撤销)
+/// `status`: String, 订单状态(open: 等待处理、finished: 已结束的订单)
+/// `contract`: String, 合约标识
+/// `size`: Decimal, 必选。交易数量,正数为买入,负数为卖出。平仓委托则设置为0
+/// `iceberg`: Decimal, 冰山委托显示数量。0为完全不隐藏。注意,隐藏部分成交按照taker收取手续费
+/// `price`:Decimal, 委托价。价格为0并且tif为ioc,代表市价委托
+/// `close`: bool, 	设置为 true 的时候执行平仓操作,并且size应设置为0
+/// `is_close`: bool, 是否为平仓委托。对应请求中的close
+/// `reduce_only`: bool, 设置为 true 的时候,为只减仓委托
+/// `is_reduce_only`: bool, 是否为只减仓委托。对应请求中的reduce_only
+/// `is_liq`: bool, 是否为强制平仓委托
+/// `tif`: String, Time in force 策略,市价单当前只支持 ioc 模式(gtc: GoodTillCancelled、ioc: ImmediateOrCancelled,立即成交或者取消,只吃单不挂单、poc: PendingOrCancelled,被动委托,只挂单不吃单、fok: FillOrKill, 完全成交,或者完全取消)
+/// `left`: Decimal, 未成交数量
+/// `fill_price`: Decimal, 成交价
+/// `text`: String, 订单自定义信息,用户可以用该字段设置自定义 ID,用户自定义字段必须满足以下条件:(1. 必须以 t- 开头、2. 不计算 t- ,长度不能超过 28 字节、3. 输入内容只能包含数字、字母、下划线(_)、中划线(-) 或者点(.))
+/// `tkfr`: Decimal, 吃单费率
+/// `mkfr`: Decimal, 做单费率
+/// `auto_size`: String,双仓模式下用于设置平仓的方向,close_long 平多头, close_short 平空头,需要同时设置 size 为 0
+/// `stp_id`: i64, 订单所属的STP用户组id,同一个STP用户组内用户之间的订单不允许发生自成交。
+/// `stp_act`: String, Self-Trading Prevention Action,用户可以用该字段设置自定义限制自成交策略。
+/// `amend_text`: String 用户修改订单时备注的信息
+#[derive(Debug, Deserialize, Serialize)]
+struct SwapOrder {
+    user_id: String,
+    symbol: String,
+    order_id: String,
+    client_oid: String,
+    price: Decimal,
+    size: Decimal,
+    order_type: String,
+    side: String,
+    status: String,
+    price_avg:  Decimal,
+    base_volume:  Decimal,
+    quote_volume:  Decimal,
+    enter_point_source: String,
+    fee_detail: String,
+    order_source: String,
+    c_time: i64,
+    u_time: i64,
+}
+
+/// 处理合约订单信息
+///
+/// pub(crate) fn handle_swap_order(data: String, amount_size: Decimal) -> Order;
+///
+/// - `data`:String, 交易所返回账户信息,JsonString
+/// - `amount_size`:Decimal, 张数价值
+pub(crate) fn handle_swap_order(data: String, amount_size: Decimal) -> Order {
+    let order_info: SwapOrder = serde_json::from_str(&data).unwrap();
+    let custom_status = match order_info.status.as_str() {
+        "finished" => { "REMOVE".to_string() }
+        "open" => { "NEW".to_string() }
+        _ => {
+            error!("Gate:格式化订单状态错误!\nhandle_swap_order:status={:?}", order_info.status);
+            panic!("Gate:格式化订单状态错误!\nhandle_swap_order:status={:?}", order_info.status)
+        }
+    };
+    Order {
+        id: order_info.id.to_string(),
+        custom_id: order_info.text.replace("t-my-custom-id_", ""),
+        price: order_info.price,
+        amount: order_info.size * amount_size,
+        deal_amount: (order_info.size - order_info.left) * amount_size,
+        avg_price: order_info.fill_price,
+        status: custom_status,
+        order_type: "limit".to_string(),
+    }
 }

+ 2 - 1
exchange_data_formatter/src/gate_handle.rs

@@ -65,9 +65,10 @@ struct SwapAccount {
 ///
 /// - `data`:String, 交易所返回账户信息,JsonString
 /// - `symbol`:String, 交易币对
-pub(crate) fn handle_swap_account(data: String, _symbol: String) -> Account {
+pub(crate) fn handle_swap_account(data: String, symbol: String) -> Account {
     let balance_info: SwapAccount = serde_json::from_str(&data).unwrap();
     Account {
+        coin: balance_info.currency.to_uppercase(),
         balance: balance_info.total,
         available_balance: balance_info.available,
         frozen_balance: balance_info.total - balance_info.available,

+ 1 - 0
exchange_data_formatter/src/kucoin_handle.rs

@@ -41,6 +41,7 @@ struct SwapAccount {
 pub(crate) fn handle_swap_account(data: String, _symbol: String) -> Account {
     let balance_info: SwapAccount = serde_json::from_str(&data).unwrap();
     Account {
+        coin: balance_info.currency.to_uppercase(),
         balance: balance_info.account_equity,
         available_balance: balance_info.available_balance,
         frozen_balance: balance_info.account_equity - balance_info.available_balance,

+ 26 - 1
exchange_data_formatter/src/lib.rs

@@ -1,9 +1,10 @@
 use rust_decimal::{Decimal};
 
+mod utils;
 mod binance_handle;
 mod gate_handle;
-mod utils;
 mod kucoin_handle;
+mod bitget_handle;
 
 /// 持仓模式枚举
 /// - `Both`:单持仓方向
@@ -17,6 +18,7 @@ pub enum PositionModeEnum {
 }
 
 /// Account结构体(账户信息)
+/// - `coin(String)`: 货币;
 /// - `balance(Decimal)`: 总计计价币数量;
 /// - `available_balance(Decimal)`: 可用计价币数量;
 /// - `frozen_balance(Decimal)`: balance挂单的冻结数量
@@ -25,6 +27,7 @@ pub enum PositionModeEnum {
 /// - `frozen_stocks(Decimal)`: stocks挂单的冻结数量
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub struct Account {
+    pub coin: String,
     pub balance: Decimal,
     pub available_balance: Decimal,
     pub frozen_balance: Decimal,
@@ -36,6 +39,7 @@ pub struct Account {
 impl Account {
     pub fn new() -> Account {
         Account {
+            coin: "".to_string(),
             balance: Default::default(),
             available_balance: Default::default(),
             frozen_balance: Default::default(),
@@ -210,6 +214,8 @@ impl Order {
 /// - `GateSpotRest`: Gate交易所现货请求消息;
 /// - `KucoinSwapWss`: Kucoin交易所期货订阅消息;
 /// - `KucoinSwapRest`: Kucoin交易所期货请求消息;
+/// - `BitgetSpotWss`: Bitget交易所现货订阅消息;
+/// - `BitgetSpotRest`: Bitget交易所现货请求消息;
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub enum ExchangeEnum {
     BinanceSwapWss,
@@ -222,6 +228,9 @@ pub enum ExchangeEnum {
     GateSpotRest,
     KucoinSwapWss,
     KucoinSwapRest,
+    BitgetSpotWss,
+    BitgetSpotRest,
+
 }
 
 pub struct Exchange {
@@ -252,6 +261,16 @@ impl Exchange {
             }
         }
     }
+    pub fn handle_account_list(&self, data: String) -> Vec<Account> {
+        match self.exchange_enum {
+            ExchangeEnum::BitgetSpotRest => {
+                bitget_handle::handle_spot_account(data)
+            }
+            _ => {
+                panic!()
+            }
+        }
+    }
     pub fn handle_position(&self, data: String, amount_size: Decimal) -> Vec<Position> {
         match self.exchange_enum {
             ExchangeEnum::BinanceSwapRest => {
@@ -279,6 +298,9 @@ impl Exchange {
             ExchangeEnum::KucoinSwapRest => {
                 kucoin_handle::handle_swap_ticker(data, self.symbol.clone())
             }
+            ExchangeEnum::BitgetSpotRest => {
+                bitget_handle::handle_spot_ticker(data, self.symbol.clone())
+            }
             _ => {
                 panic!()
             }
@@ -295,6 +317,9 @@ impl Exchange {
             ExchangeEnum::KucoinSwapRest => {
                 kucoin_handle::handle_swap_market(data, self.symbol.clone())
             }
+            ExchangeEnum::BitgetSpotRest => {
+                bitget_handle::handle_swap_market(data, self.symbol.clone())
+            }
             _ => {
                 panic!()
             }

+ 41 - 0
exchange_data_formatter/tests/bitget_handle_test.rs

@@ -0,0 +1,41 @@
+use rust_decimal_macros::dec;
+use tracing::{instrument, trace};
+use exchange_data_formatter::{Exchange, ExchangeEnum};
+
+// 测试获取账号信息
+#[tokio::test]
+#[instrument(level = "TRACE")]
+async fn test_handle_account() {
+    global::log_utils::init_log_with_trace();
+
+    let data = "[{\"coin\":\"USDT\",\"available\":\"52.97676497\",\"limitAvailable\":\"0\",\"frozen\":\"0.00000000\",\"locked\":\"0.00000000\",\"uTime\":\"1698287505000\"},{\"coin\":\"CELR\",\"available\":\"0.99991120\",\"limitAvailable\":\"0\",\"frozen\":\"0.00000000\",\"locked\":\"0.00000000\",\"uTime\":\"1697703815000\"},{\"coin\":\"MINA\",\"available\":\"0.50342000\",\"limitAvailable\":\"0\",\"frozen\":\"0.00000000\",\"locked\":\"0.00000000\",\"uTime\":\"1698287505000\"}]";
+    let exchange = Exchange::new(ExchangeEnum::BitgetSpotRest, "BTC_USDT".to_string());
+    let result = exchange.handle_account_list(data.to_string());
+    trace!(?result);
+}
+
+// 测试获取行情信息
+#[tokio::test]
+#[instrument(level = "TRACE")]
+async fn test_handle_ticker() {
+    global::log_utils::init_log_with_trace();
+
+    let data = "[{\"open\":\"0.23189\",\"symbol\":\"BLZUSDT\",\"high24h\":\"0.24739\",\"low24h\":\"0.21548\",\"lastPr\":\"0.21675\",\"quoteVolume\":\"17614315\",\"baseVolume\":\"77386366\",\"usdtVolume\":\"17614314.82785\",\"ts\":\"1698374988082\",\"bidPr\":\"0.21657\",\"askPr\":\"0.2167\",\"bidSz\":\"962\",\"askSz\":\"1052\",\"openUtc\":\"0.23189\",\"changeUtc24h\":\"-0.06529\",\"change24h\":\"-0.08036\"}]";
+
+    let exchange = Exchange::new(ExchangeEnum::BitgetSpotRest, "BLZ_USDT".to_string());
+    let result = exchange.handle_ticker(data.to_string());
+    trace!(?result);
+}
+
+// 测试获取市场信息
+#[tokio::test]
+#[instrument(level = "TRACE")]
+async fn test_handle_market() {
+    global::log_utils::init_log_with_trace();
+
+    let data = "[{\"symbol\":\"BLZUSDT\",\"baseCoin\":\"BLZ\",\"quoteCoin\":\"USDT\",\"minTradeAmount\":\"0\",\"maxTradeAmount\":\"10000000000\",\"takerFeeRate\":\"0.001\",\"makerFeeRate\":\"0.001\",\"pricePrecision\":\"5\",\"quantityPrecision\":\"0\",\"quotePrecision\":\"6\",\"status\":\"online\",\"minTradeUSDT\":\"5\",\"buyLimitPriceRatio\":\"0.05\",\"sellLimitPriceRatio\":\"0.05\"}]";
+
+    let exchange = Exchange::new(ExchangeEnum::BitgetSpotRest, "BLZ_USDT".to_string());
+    let result = exchange.handle_market(data.to_string());
+    trace!(?result);
+}

+ 13 - 13
global/src/log_utils.rs

@@ -72,19 +72,19 @@ pub fn send_remote_err_log(msg: String) {
         request_json_data.insert("serverName", "As");
         request_json_data.insert("data", encoded_str.as_str());
 
-        let res = Client::new().post("https://hhh.liangjiang.cc/api/log/addError?key=d64a8sc874sa8c4as5")
-            .json(&request_json_data)
-            .send()
-            .await;
-
-        match res {
-            Ok(_resp) => {
-                // let body = _resp.text().await.unwrap();
-            }
-            Err(err) => {
-                warn!("log的error监听器发送远端报错失败:{:?}", err);
-            }
-        }
+        // let res = Client::new().post("https://hhh.liangjiang.cc/api/log/addError?key=d64a8sc874sa8c4as5")
+        //     .json(&request_json_data)
+        //     .send()
+        //     .await;
+        //
+        // match res {
+        //     Ok(_resp) => {
+        //         // let body = _resp.text().await.unwrap();
+        //     }
+        //     Err(err) => {
+        //         warn!("log的error监听器发送远端报错失败:{:?}", err);
+        //     }
+        // }
     });
 }
 

+ 1 - 0
standard/src/bitget_spot.rs

@@ -86,6 +86,7 @@ impl Platform for BitgetSpot {
         let res_data = self.request.get_account_assets().await;
         if res_data.code == "200" {
             let res_data_str = &res_data.data;
+            println!("{}", res_data_str);
             let res_data_json: Vec<serde_json::Value> = serde_json::from_str(res_data_str).unwrap();
             let result = res_data_json.iter().map(|item| bitget_spot_handle::format_account_info(item.clone())).collect();
             Ok(result)