1、暂存
This commit is contained in:
447
services/bitgetservice/bitget_client.go
Normal file
447
services/bitgetservice/bitget_client.go
Normal file
@ -0,0 +1,447 @@
|
||||
package bitgetservice
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go-admin/common/global"
|
||||
"go-admin/common/helper"
|
||||
"go-admin/common/interfaces"
|
||||
"go-admin/models"
|
||||
"go-admin/pkg/utility"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
// BitgetClient Bitget交易所客户端实现
|
||||
type BitgetClient struct {
|
||||
httpClient *helper.BitgetClient
|
||||
marketClient *BitgetMarketClient // 公开行情客户端
|
||||
}
|
||||
|
||||
// NewBitgetClient 创建新的Bitget客户端实例
|
||||
func NewBitgetClient(apiKey, apiSecret, passphrase, proxyUrl string) (*BitgetClient, error) {
|
||||
httpClient, err := helper.NewBitgetClient(apiKey, apiSecret, passphrase, proxyUrl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := &BitgetClient{
|
||||
httpClient: httpClient,
|
||||
}
|
||||
|
||||
// 初始化公开行情客户端
|
||||
marketConfig := DefaultMarketConfig()
|
||||
client.marketClient = NewBitgetMarketClient(marketConfig)
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// GetExchangeName 获取交易所名称
|
||||
func (c *BitgetClient) GetExchangeName() string {
|
||||
return global.EXCHANGE_BITGET
|
||||
}
|
||||
|
||||
// GetExchangeInfo 获取交易所信息
|
||||
func (c *BitgetClient) GetExchangeInfo() (interfaces.ExchangeInfoResponse, error) {
|
||||
restApi := NewBitgetRestApi()
|
||||
|
||||
datas, err := restApi.GetSpotSymbols()
|
||||
|
||||
if err != nil {
|
||||
return interfaces.ExchangeInfoResponse{}, err
|
||||
}
|
||||
|
||||
symbols := make([]interfaces.SymbolInfo, 0, len(datas))
|
||||
for _, item := range datas {
|
||||
symbols = append(symbols, interfaces.SymbolInfo{
|
||||
Symbol: item.Symbol,
|
||||
BaseAsset: item.BaseCoin,
|
||||
QuoteAsset: item.QuoteCoin,
|
||||
Status: item.Status,
|
||||
MinQty: item.MinQty,
|
||||
MaxQty: item.MaxQty,
|
||||
StepSize: strconv.Itoa(item.QuantityDigit),
|
||||
TickSize: strconv.Itoa(item.PriceDigit),
|
||||
})
|
||||
}
|
||||
|
||||
return interfaces.ExchangeInfoResponse{Symbols: symbols}, nil
|
||||
}
|
||||
|
||||
// GetSpotTickers 获取所有现货行情
|
||||
func (c *BitgetClient) GetSpotTickers() ([]models.TradeSet, error) {
|
||||
params := map[string]interface{}{}
|
||||
resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/market/tickers", "GET", params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取Bitget现货行情列表失败: %v", err)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Code string `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data []struct {
|
||||
Symbol string `json:"symbol"`
|
||||
High24h string `json:"high24h"`
|
||||
Low24h string `json:"low24h"`
|
||||
Close string `json:"close"`
|
||||
Open string `json:"open"`
|
||||
Volume24h string `json:"volume24h"`
|
||||
QuoteVol string `json:"quoteVol"`
|
||||
Change24h string `json:"change24h"`
|
||||
Change string `json:"change"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err := sonic.Unmarshal(resp, &response); err != nil {
|
||||
return nil, fmt.Errorf("解析Bitget行情列表失败: %v", err)
|
||||
}
|
||||
|
||||
if response.Code != "00000" {
|
||||
return nil, fmt.Errorf("bitget API错误: %s", response.Msg)
|
||||
}
|
||||
|
||||
tradeSets := make([]models.TradeSet, 0, len(response.Data))
|
||||
for _, item := range response.Data {
|
||||
// 解析交易对,获取基础币种和计价币种
|
||||
var coin, currency string
|
||||
if strings.HasSuffix(item.Symbol, "USDT") {
|
||||
coin = strings.TrimSuffix(item.Symbol, "USDT")
|
||||
currency = "USDT"
|
||||
} else if strings.HasSuffix(item.Symbol, "BTC") {
|
||||
coin = strings.TrimSuffix(item.Symbol, "BTC")
|
||||
currency = "BTC"
|
||||
} else if strings.HasSuffix(item.Symbol, "ETH") {
|
||||
coin = strings.TrimSuffix(item.Symbol, "ETH")
|
||||
currency = "ETH"
|
||||
} else {
|
||||
continue // 跳过不支持的交易对
|
||||
}
|
||||
|
||||
tradeSet := models.TradeSet{
|
||||
Coin: coin,
|
||||
Currency: currency,
|
||||
LastPrice: item.Close,
|
||||
HighPrice: item.High24h,
|
||||
LowPrice: item.Low24h,
|
||||
Volume: item.Volume24h,
|
||||
QuoteVolume: item.QuoteVol,
|
||||
OpenPrice: utility.StringAsFloat(item.Open),
|
||||
PriceChange: utility.StringAsFloat(item.Change),
|
||||
}
|
||||
|
||||
tradeSets = append(tradeSets, tradeSet)
|
||||
}
|
||||
|
||||
return tradeSets, nil
|
||||
}
|
||||
|
||||
// GetSpotTicker24h 获取现货24小时行情
|
||||
func (c *BitgetClient) GetSpotTicker24h(symbol string) (models.Ticker24, error) {
|
||||
params := map[string]interface{}{
|
||||
"symbol": symbol,
|
||||
}
|
||||
|
||||
resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/market/ticker", "GET", params)
|
||||
if err != nil {
|
||||
return models.Ticker24{}, fmt.Errorf("获取Bitget现货24h行情失败: %v", err)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Code string `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
Symbol string `json:"symbol"`
|
||||
High24h string `json:"high24h"`
|
||||
Low24h string `json:"low24h"`
|
||||
Close string `json:"close"`
|
||||
Open string `json:"open"`
|
||||
Volume24h string `json:"volume24h"`
|
||||
QuoteVol string `json:"quoteVol"`
|
||||
Change24h string `json:"change24h"`
|
||||
Change string `json:"change"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err := sonic.Unmarshal(resp, &response); err != nil {
|
||||
return models.Ticker24{}, fmt.Errorf("解析Bitget 24h行情失败: %v", err)
|
||||
}
|
||||
|
||||
if response.Code != "00000" {
|
||||
return models.Ticker24{}, fmt.Errorf("Bitget API错误: %s", response.Msg)
|
||||
}
|
||||
|
||||
ticker := models.Ticker24{
|
||||
HighPrice: response.Data.High24h,
|
||||
LowPrice: response.Data.Low24h,
|
||||
LastPrice: response.Data.Close,
|
||||
OpenPrice: response.Data.Open,
|
||||
Volume: response.Data.Volume24h,
|
||||
QuoteVolume: response.Data.QuoteVol,
|
||||
ChangePercent: response.Data.Change,
|
||||
}
|
||||
|
||||
return ticker, nil
|
||||
}
|
||||
|
||||
// PlaceSpotOrder 下现货订单
|
||||
func (c *BitgetClient) PlaceSpotOrder(params interfaces.SpotOrderParams) (interfaces.SpotOrderResponse, error) {
|
||||
orderParams := map[string]interface{}{
|
||||
"symbol": params.Symbol,
|
||||
"side": strings.ToLower(params.Side),
|
||||
"orderType": strings.ToLower(params.Type),
|
||||
"quantity": params.Quantity.String(),
|
||||
}
|
||||
|
||||
if params.Type == "LIMIT" {
|
||||
orderParams["price"] = params.Price.String()
|
||||
}
|
||||
|
||||
if params.TimeInForce != "" {
|
||||
orderParams["timeInForce"] = params.TimeInForce
|
||||
}
|
||||
|
||||
if params.NewClientOrderId != "" {
|
||||
orderParams["clientOrderId"] = params.NewClientOrderId
|
||||
}
|
||||
|
||||
resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/trade/orders", "POST", orderParams)
|
||||
if err != nil {
|
||||
return interfaces.SpotOrderResponse{}, fmt.Errorf("Bitget现货下单失败: %v", err)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Code string `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
OrderId string `json:"orderId"`
|
||||
ClientOrderId string `json:"clientOrderId"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
if err := sonic.Unmarshal(resp, &response); err != nil {
|
||||
return interfaces.SpotOrderResponse{}, fmt.Errorf("解析Bitget下单响应失败: %v", err)
|
||||
}
|
||||
|
||||
if response.Code != "00000" {
|
||||
return interfaces.SpotOrderResponse{}, fmt.Errorf("Bitget下单API错误: %s", response.Msg)
|
||||
}
|
||||
|
||||
orderId, _ := strconv.ParseInt(response.Data.OrderId, 10, 64)
|
||||
|
||||
return interfaces.SpotOrderResponse{
|
||||
Symbol: params.Symbol,
|
||||
OrderId: orderId,
|
||||
ClientOrderId: response.Data.ClientOrderId,
|
||||
Price: params.Price.String(),
|
||||
OrigQty: params.Quantity.String(),
|
||||
Side: params.Side,
|
||||
Type: params.Type,
|
||||
Status: "NEW",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CancelSpotOrder 取消现货订单
|
||||
func (c *BitgetClient) CancelSpotOrder(symbol, orderID string) error {
|
||||
params := map[string]interface{}{
|
||||
"symbol": symbol,
|
||||
"orderId": orderID,
|
||||
}
|
||||
|
||||
resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/trade/cancel-order", "POST", params)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Bitget取消现货订单失败: %v", err)
|
||||
}
|
||||
|
||||
var response struct {
|
||||
Code string `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
if err := sonic.Unmarshal(resp, &response); err != nil {
|
||||
return fmt.Errorf("解析Bitget取消订单响应失败: %v", err)
|
||||
}
|
||||
|
||||
if response.Code != "00000" {
|
||||
return fmt.Errorf("Bitget取消订单API错误: %s", response.Msg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSpotOrder 获取现货订单详情
|
||||
func (c *BitgetClient) GetSpotOrder(symbol, orderID string) (interfaces.SpotOrderResponse, error) {
|
||||
// 实现订单查询逻辑
|
||||
return interfaces.SpotOrderResponse{}, fmt.Errorf("GetSpotOrder not implemented yet")
|
||||
}
|
||||
|
||||
// GetSpotOrders 获取现货订单列表
|
||||
func (c *BitgetClient) GetSpotOrders(symbol string) ([]interfaces.SpotOrderResponse, error) {
|
||||
params := map[string]interface{}{
|
||||
"symbol": symbol,
|
||||
}
|
||||
|
||||
_, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/trade/open-orders", "GET", params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取Bitget现货订单列表失败: %v", err)
|
||||
}
|
||||
|
||||
// 这里需要根据实际的Bitget API响应格式来解析
|
||||
return []interfaces.SpotOrderResponse{}, nil
|
||||
}
|
||||
|
||||
// 合约相关接口实现(暂时返回错误)
|
||||
func (c *BitgetClient) GetFuturesTicker24h(symbol string) (models.Ticker24, error) {
|
||||
return models.Ticker24{}, fmt.Errorf("futures not implemented yet")
|
||||
}
|
||||
|
||||
func (c *BitgetClient) GetFuturesTickers() ([]models.TradeSet, error) {
|
||||
return nil, fmt.Errorf("futures not implemented yet")
|
||||
}
|
||||
|
||||
func (c *BitgetClient) PlaceFuturesOrder(params interfaces.FuturesOrderParams) (interfaces.FuturesOrderResponse, error) {
|
||||
return interfaces.FuturesOrderResponse{}, fmt.Errorf("futures not implemented yet")
|
||||
}
|
||||
|
||||
func (c *BitgetClient) CancelFuturesOrder(symbol, orderID string) error {
|
||||
return fmt.Errorf("futures not implemented yet")
|
||||
}
|
||||
|
||||
func (c *BitgetClient) GetFuturesOrder(symbol, orderID string) (interfaces.FuturesOrderResponse, error) {
|
||||
return interfaces.FuturesOrderResponse{}, fmt.Errorf("futures not implemented yet")
|
||||
}
|
||||
|
||||
// WebSocket相关接口实现(已重构为使用公开行情客户端)
|
||||
// 注意:这些方法已被重构,现在使用新的公开行情客户端
|
||||
// 如需私有数据订阅(如订单、持仓),请使用 BitgetWebSocketManager
|
||||
|
||||
// 旧的订阅方法(保留兼容性,但建议使用新方法)
|
||||
func (c *BitgetClient) SubscribeSpotTickerLegacy(symbols []string, callback func(models.Ticker24, string, string)) error {
|
||||
return fmt.Errorf("已废弃,请使用 SubscribeSpotTicker(symbol, listener) 方法")
|
||||
}
|
||||
|
||||
func (c *BitgetClient) SubscribeFuturesTickerLegacy(symbols []string, callback func(models.Ticker24, string, string)) error {
|
||||
return fmt.Errorf("已废弃,请使用 SubscribeFuturesTicker(symbol, listener) 方法")
|
||||
}
|
||||
|
||||
func (c *BitgetClient) SubscribeSpotDepthLegacy(symbols []string, callback func(models.DepthBin, string, string)) error {
|
||||
return fmt.Errorf("已废弃,请使用 SubscribeSpotDepth(symbol, listener) 方法")
|
||||
}
|
||||
|
||||
func (c *BitgetClient) SubscribeSpotTradesLegacy(symbols []string, callback func(models.NewDealPush, string, string)) error {
|
||||
return fmt.Errorf("已废弃,请使用 SubscribeSpotTrades(symbol, listener) 方法")
|
||||
}
|
||||
|
||||
func (c *BitgetClient) SubscribeKlineLegacy(symbols []string, interval string, callback func(models.Kline, int, string, string)) error {
|
||||
return fmt.Errorf("已废弃,请使用 SubscribeKline(symbol, interval, listener) 方法")
|
||||
}
|
||||
|
||||
func (c *BitgetClient) Close() error {
|
||||
// 关闭HTTP客户端等清理工作
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetMarketProxy 设置市场数据代理
|
||||
func (c *BitgetClient) SetMarketProxy(proxyType, proxyAddress string) {
|
||||
if c.marketClient != nil {
|
||||
c.marketClient.SetProxy(proxyType, proxyAddress)
|
||||
}
|
||||
}
|
||||
|
||||
// SetMarketListeners 设置市场数据监听器
|
||||
func (c *BitgetClient) SetMarketListeners(msgListener, errorListener OnReceive) {
|
||||
if c.marketClient != nil {
|
||||
c.marketClient.SetListeners(msgListener, errorListener)
|
||||
}
|
||||
}
|
||||
|
||||
// ConnectMarketData 连接市场数据WebSocket
|
||||
func (c *BitgetClient) ConnectMarketData() error {
|
||||
if c.marketClient == nil {
|
||||
return errors.New("市场数据客户端未初始化")
|
||||
}
|
||||
return c.marketClient.Connect()
|
||||
}
|
||||
|
||||
// SubscribeMarketData 订阅市场数据(公开行情)
|
||||
func (c *BitgetClient) SubscribeMarketData(subscribeReqs []SubscribeReq, listener OnReceive) error {
|
||||
if c.marketClient == nil {
|
||||
return errors.New("市场数据客户端未初始化")
|
||||
}
|
||||
return c.marketClient.Subscribe(subscribeReqs, listener)
|
||||
}
|
||||
|
||||
// UnsubscribeMarketData 取消订阅市场数据
|
||||
func (c *BitgetClient) UnsubscribeMarketData(subscribeReqs []SubscribeReq) error {
|
||||
if c.marketClient == nil {
|
||||
return errors.New("市场数据客户端未初始化")
|
||||
}
|
||||
return c.marketClient.Unsubscribe(subscribeReqs)
|
||||
}
|
||||
|
||||
// IsMarketDataConnected 检查市场数据连接状态
|
||||
func (c *BitgetClient) IsMarketDataConnected() bool {
|
||||
if c.marketClient == nil {
|
||||
return false
|
||||
}
|
||||
return c.marketClient.IsConnected()
|
||||
}
|
||||
|
||||
// CloseMarketData 关闭市场数据连接
|
||||
func (c *BitgetClient) CloseMarketData() error {
|
||||
if c.marketClient == nil {
|
||||
return nil
|
||||
}
|
||||
return c.marketClient.Close()
|
||||
}
|
||||
|
||||
// SubscribeSpotTicker 订阅现货行情(使用公开行情客户端)
|
||||
func (c *BitgetClient) SubscribeSpotTicker(symbol string, listener OnReceive) error {
|
||||
subscribeReq := SubscribeReq{
|
||||
InstType: "SPOT",
|
||||
Channel: "ticker",
|
||||
InstId: symbol,
|
||||
}
|
||||
return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
|
||||
}
|
||||
|
||||
// SubscribeFuturesTicker 订阅合约行情(使用公开行情客户端)
|
||||
func (c *BitgetClient) SubscribeFuturesTicker(symbol string, listener OnReceive) error {
|
||||
subscribeReq := SubscribeReq{
|
||||
InstType: "UMCBL",
|
||||
Channel: "ticker",
|
||||
InstId: symbol,
|
||||
}
|
||||
return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
|
||||
}
|
||||
|
||||
// SubscribeSpotDepth 订阅现货深度(使用公开行情客户端)
|
||||
func (c *BitgetClient) SubscribeSpotDepth(symbol string, listener OnReceive) error {
|
||||
subscribeReq := SubscribeReq{
|
||||
InstType: "SPOT",
|
||||
Channel: "books",
|
||||
InstId: symbol,
|
||||
}
|
||||
return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
|
||||
}
|
||||
|
||||
// SubscribeSpotTrades 订阅现货成交(使用公开行情客户端)
|
||||
func (c *BitgetClient) SubscribeSpotTrades(symbol string, listener OnReceive) error {
|
||||
subscribeReq := SubscribeReq{
|
||||
InstType: "SPOT",
|
||||
Channel: "trade",
|
||||
InstId: symbol,
|
||||
}
|
||||
return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
|
||||
}
|
||||
|
||||
// SubscribeKline 订阅K线数据(使用公开行情客户端)
|
||||
func (c *BitgetClient) SubscribeKline(symbol, interval string, listener OnReceive) error {
|
||||
subscribeReq := SubscribeReq{
|
||||
InstType: "SPOT",
|
||||
Channel: "candle" + interval,
|
||||
InstId: symbol,
|
||||
}
|
||||
return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
|
||||
}
|
||||
229
services/bitgetservice/bitget_example.go
Normal file
229
services/bitgetservice/bitget_example.go
Normal file
@ -0,0 +1,229 @@
|
||||
package bitgetservice
|
||||
|
||||
import (
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
)
|
||||
|
||||
// ExampleBitgetMarketClient 展示如何使用Bitget公开行情客户端
|
||||
func ExampleBitgetMarketClient() {
|
||||
// 1. 创建Bitget客户端(不需要API密钥用于公开数据)
|
||||
client := &BitgetClient{}
|
||||
|
||||
// 初始化公开行情客户端
|
||||
marketConfig := DefaultMarketConfig()
|
||||
client.marketClient = NewBitgetMarketClient(marketConfig)
|
||||
|
||||
// 2. 设置代理(可选)
|
||||
// client.SetMarketProxy("socks5", "127.0.0.1:1080")
|
||||
|
||||
// 3. 设置消息监听器
|
||||
msgListener := func(message string) {
|
||||
log.Info("收到市场数据: %s", message)
|
||||
// 在这里处理市场数据
|
||||
}
|
||||
|
||||
errorListener := func(message string) {
|
||||
log.Error("收到错误消息: %s", message)
|
||||
}
|
||||
|
||||
client.SetMarketListeners(msgListener, errorListener)
|
||||
|
||||
// 4. 连接WebSocket
|
||||
if err := client.ConnectMarketData(); err != nil {
|
||||
log.Error("连接市场数据失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 5. 订阅现货行情
|
||||
tickerListener := func(message string) {
|
||||
log.Info("收到BTC-USDT行情: %s", message)
|
||||
}
|
||||
|
||||
if err := client.SubscribeSpotTicker("BTCUSDT", tickerListener); err != nil {
|
||||
log.Error("订阅现货行情失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 6. 订阅深度数据
|
||||
depthListener := func(message string) {
|
||||
log.Info("收到BTC-USDT深度: %s", message)
|
||||
}
|
||||
|
||||
if err := client.SubscribeSpotDepth("BTCUSDT", depthListener); err != nil {
|
||||
log.Error("订阅深度数据失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 7. 订阅成交数据
|
||||
tradeListener := func(message string) {
|
||||
log.Info("收到BTC-USDT成交: %s", message)
|
||||
}
|
||||
|
||||
if err := client.SubscribeSpotTrades("BTCUSDT", tradeListener); err != nil {
|
||||
log.Error("订阅成交数据失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 8. 订阅K线数据
|
||||
klineListener := func(message string) {
|
||||
log.Info("收到BTC-USDT K线: %s", message)
|
||||
}
|
||||
|
||||
if err := client.SubscribeKline("BTCUSDT", "1m", klineListener); err != nil {
|
||||
log.Error("订阅K线数据失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("所有订阅完成,开始接收数据...")
|
||||
|
||||
// 保持连接(在实际应用中,你可能需要其他方式来保持程序运行)
|
||||
// select {}
|
||||
}
|
||||
|
||||
// ExampleBitgetPrivateWebSocket 展示如何使用私有WebSocket(订单、持仓等)
|
||||
func ExampleBitgetPrivateWebSocket() {
|
||||
// 私有数据订阅仍然使用 BitgetWebSocketManager
|
||||
// apiKey := "your_api_key"
|
||||
// secretKey := "your_secret_key"
|
||||
// passphrase := "your_passphrase"
|
||||
|
||||
// // 创建私有WebSocket管理器
|
||||
// manager := GetOrCreateBitgetWebSocketManager(apiKey, secretKey, passphrase, "spot")
|
||||
|
||||
// // 启动连接
|
||||
// if err := manager.Start(); err != nil {
|
||||
// log.Error("启动私有WebSocket连接失败: %v", err)
|
||||
// return
|
||||
// }
|
||||
|
||||
// log.Info("私有WebSocket连接成功,可以接收订单、持仓等私有数据")
|
||||
}
|
||||
|
||||
// ExampleMixedUsage 展示混合使用公开和私有订阅
|
||||
func ExampleMixedUsage() {
|
||||
// apiKey := "your_api_key"
|
||||
// secretKey := "your_secret_key"
|
||||
// passphrase := "your_passphrase"
|
||||
|
||||
// // 1. 创建完整的Bitget客户端
|
||||
// client, err := NewBitgetClient(apiKey, secretKey, passphrase)
|
||||
// if err != nil {
|
||||
// log.Error("创建Bitget客户端失败: %v", err)
|
||||
// return
|
||||
// }
|
||||
|
||||
// // 2. 设置公开行情监听器
|
||||
// client.SetMarketListeners(
|
||||
// func(message string) {
|
||||
// log.Info("公开行情数据: %s", message)
|
||||
// },
|
||||
// func(message string) {
|
||||
// log.Error("公开行情错误: %s", message)
|
||||
// },
|
||||
// )
|
||||
|
||||
// // 3. 连接公开行情
|
||||
// if err := client.ConnectMarketData(); err != nil {
|
||||
// log.Error("连接公开行情失败: %v", err)
|
||||
// return
|
||||
// }
|
||||
|
||||
// // 4. 订阅公开行情
|
||||
// if err := client.SubscribeSpotTicker("BTCUSDT", func(message string) {
|
||||
// log.Info("BTC行情: %s", message)
|
||||
// }); err != nil {
|
||||
// log.Error("订阅行情失败: %v", err)
|
||||
// return
|
||||
// }
|
||||
|
||||
// // 5. 同时使用私有WebSocket(如果需要)
|
||||
// privateManager, err := GetOrCreateBitgetWebSocketManager(apiKey, secretKey, passphrase, "spot")
|
||||
// if err != nil {
|
||||
// log.Error("创建私有WebSocket失败: %v", err)
|
||||
// return
|
||||
// }
|
||||
|
||||
// if err := privateManager.Start(); err != nil {
|
||||
// log.Error("启动私有WebSocket失败: %v", err)
|
||||
// return
|
||||
// }
|
||||
|
||||
// log.Info("混合使用设置完成:公开行情 + 私有数据")
|
||||
}
|
||||
|
||||
// ExampleSubscribeMultipleSymbols 展示如何订阅多个交易对
|
||||
func ExampleSubscribeMultipleSymbols() {
|
||||
client := &BitgetClient{}
|
||||
marketConfig := DefaultMarketConfig()
|
||||
client.marketClient = NewBitgetMarketClient(marketConfig)
|
||||
|
||||
// 连接
|
||||
if err := client.ConnectMarketData(); err != nil {
|
||||
log.Error("连接失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 批量订阅多个交易对的行情
|
||||
symbols := []string{"BTCUSDT", "ETHUSDT", "ADAUSDT", "DOTUSDT"}
|
||||
|
||||
for _, symbol := range symbols {
|
||||
listener := func(sym string) OnReceive {
|
||||
return func(message string) {
|
||||
log.Info("%s 行情更新: %s", sym, message)
|
||||
}
|
||||
}(symbol)
|
||||
|
||||
if err := client.SubscribeSpotTicker(symbol, listener); err != nil {
|
||||
log.Error("订阅 %s 行情失败: %v", symbol, err)
|
||||
} else {
|
||||
log.Info("成功订阅 %s 行情", symbol)
|
||||
}
|
||||
}
|
||||
|
||||
log.Info("批量订阅完成")
|
||||
}
|
||||
|
||||
// ExampleCustomSubscription 展示如何使用自定义订阅
|
||||
func ExampleCustomSubscription() {
|
||||
client := &BitgetClient{}
|
||||
marketConfig := DefaultMarketConfig()
|
||||
client.marketClient = NewBitgetMarketClient(marketConfig)
|
||||
|
||||
if err := client.ConnectMarketData(); err != nil {
|
||||
log.Error("连接失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 自定义订阅请求
|
||||
customSubscriptions := []SubscribeReq{
|
||||
{
|
||||
InstType: "SPOT",
|
||||
Channel: "ticker",
|
||||
InstId: "BTCUSDT",
|
||||
},
|
||||
{
|
||||
InstType: "SPOT",
|
||||
Channel: "books",
|
||||
InstId: "ETHUSDT",
|
||||
},
|
||||
{
|
||||
InstType: "UMCBL", // 合约
|
||||
Channel: "ticker",
|
||||
InstId: "BTCUSDT",
|
||||
},
|
||||
}
|
||||
|
||||
// 统一的消息处理器
|
||||
messageHandler := func(message string) {
|
||||
log.Info("自定义订阅数据: %s", message)
|
||||
// 在这里根据消息内容进行不同的处理
|
||||
}
|
||||
|
||||
// 批量订阅
|
||||
if err := client.SubscribeMarketData(customSubscriptions, messageHandler); err != nil {
|
||||
log.Error("自定义订阅失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("自定义订阅完成")
|
||||
}
|
||||
326
services/bitgetservice/bitget_judge.go
Normal file
326
services/bitgetservice/bitget_judge.go
Normal file
@ -0,0 +1,326 @@
|
||||
package bitgetservice
|
||||
|
||||
import (
|
||||
DbModels "go-admin/app/admin/models"
|
||||
"go-admin/models"
|
||||
"go-admin/models/positiondto"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// JudgeCreateOrder 判断是否需要创建订单
|
||||
// symbolType 1:现货 2:合约
|
||||
func JudgeCreateOrder(trade models.TradeSet, symbolType int) error {
|
||||
// var key string
|
||||
|
||||
// if symbolType == 1 {
|
||||
// key = fmt.Sprintf(rediskey.PreSpotOrderList, global.EXCHANGE_BITGET)
|
||||
// } else if symbolType == 2 {
|
||||
// key = fmt.Sprintf(rediskey.PreFutOrderList, global.EXCHANGE_BITGET)
|
||||
// } else {
|
||||
// return fmt.Errorf("不支持的交易类型")
|
||||
// }
|
||||
|
||||
// preOrderVal, _ := helper.DefaultRedis.GetAllList(key)
|
||||
// db := commonservice.GetDBConnection()
|
||||
|
||||
// if len(preOrderVal) == 0 {
|
||||
// return fmt.Errorf("预订单列表为空")
|
||||
// }
|
||||
|
||||
// // spotApi := SpotRestApi{}
|
||||
// for _, item := range preOrderVal {
|
||||
// preOrder := dto.PreOrderRedisList{}
|
||||
// if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil {
|
||||
// logger.Error("反序列化失败")
|
||||
// continue
|
||||
// }
|
||||
|
||||
// if preOrder.Symbol == trade.Coin+trade.Currency {
|
||||
// orderPrice, _ := decimal.NewFromString(preOrder.Price)
|
||||
// tradePrice, _ := decimal.NewFromString(trade.LastPrice)
|
||||
// //买入
|
||||
// if strings.ToUpper(preOrder.Site) == "BUY" && orderPrice.Cmp(tradePrice) >= 0 && orderPrice.Cmp(decimal.Zero) > 0 && tradePrice.Cmp(decimal.Zero) > 0 {
|
||||
// // SpotOrderLock(db, &preOrder, item, spotApi)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
return nil
|
||||
}
|
||||
|
||||
// 判断是否触发加仓
|
||||
func JudgeAddPositionOrder(trade models.TradeSet, symbolType int) error {
|
||||
// var key string
|
||||
|
||||
// if symbolType == 1 {
|
||||
// key = fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BITGET)
|
||||
// } else if symbolType == 2 {
|
||||
// key = fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BITGET)
|
||||
// } else {
|
||||
// return fmt.Errorf("不支持的交易类型")
|
||||
// }
|
||||
// preOrderVal, _ := helper.DefaultRedis.GetAllList(key)
|
||||
// db := commonservice.GetDBConnection()
|
||||
|
||||
// if len(preOrderVal) == 0 {
|
||||
// return fmt.Errorf("预订单列表为空")
|
||||
// }
|
||||
|
||||
// // spotApi := SpotRestApi{}
|
||||
// for _, item := range preOrderVal {
|
||||
// preOrder := positiondto.AddPositionList{}
|
||||
// if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil {
|
||||
// logger.Error("反序列化失败")
|
||||
// continue
|
||||
// }
|
||||
|
||||
// if preOrder.Symbol == trade.Coin+trade.Currency {
|
||||
// orderPrice := preOrder.Price
|
||||
// tradePrice, _ := decimal.NewFromString(trade.LastPrice)
|
||||
// //买入
|
||||
// if strings.ToUpper(preOrder.Side) == "BUY" && orderPrice.Cmp(tradePrice) >= 0 && orderPrice.Cmp(decimal.Zero) > 0 && tradePrice.Cmp(decimal.Zero) > 0 {
|
||||
// // SpotAddPositionTrigger(db, &preOrder, item, spotApi)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// JudgeReduceOrder 判断是否需要减仓
|
||||
func JudgeReduceOrder(trade models.TradeSet, symbolType int) error {
|
||||
// var key string
|
||||
// var reduceReduceListKey string
|
||||
|
||||
// if symbolType == 1 {
|
||||
// key = fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BITGET)
|
||||
// reduceReduceListKey = fmt.Sprintf(rediskey.SpotOrderReduceStrategyList, global.EXCHANGE_BITGET)
|
||||
// } else if symbolType == 2 {
|
||||
// key = fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BITGET)
|
||||
// reduceReduceListKey = fmt.Sprintf(rediskey.FutOrderReduceStrategyList, global.EXCHANGE_BITGET)
|
||||
// } else {
|
||||
// return fmt.Errorf("不支持的交易类型")
|
||||
// }
|
||||
|
||||
// db := commonservice.GetDBConnection()
|
||||
// // spotApi := SpotRestApi{}
|
||||
// setting, err := cacheservice.GetSystemSetting(db)
|
||||
|
||||
// if err != nil {
|
||||
// logger.Error("获取系统设置失败")
|
||||
// return errors.New("获取系统设置失败")
|
||||
// }
|
||||
|
||||
// reduceOrder := positiondto.ReduceListItem{}
|
||||
// tradePrice, _ := decimal.NewFromString(trade.LastPrice)
|
||||
// //减仓单减仓策略
|
||||
// orderReduceVal, _ := helper.DefaultRedis.HGetAllFields(reduceReduceListKey)
|
||||
// reduceOrderStrategy := dto.LineOrderReduceStrategyResp{}
|
||||
// orderService := orderservice.OrderService{}
|
||||
// orderService.Orm = db
|
||||
|
||||
// for _, item := range orderReduceVal {
|
||||
// sonic.Unmarshal([]byte(item), &reduceOrderStrategy)
|
||||
|
||||
// for index, item2 := range reduceOrderStrategy.Items {
|
||||
// if reduceOrderStrategy.Symbol == trade.Coin+trade.Currency {
|
||||
// //买入
|
||||
// if strings.ToUpper(reduceOrderStrategy.Side) == "SELL" &&
|
||||
// item2.TriggerPrice.Cmp(tradePrice) >= 0 &&
|
||||
// item2.TriggerPrice.Cmp(decimal.Zero) > 0 &&
|
||||
// tradePrice.Cmp(decimal.Zero) > 0 {
|
||||
// lock := helper.NewRedisLock(fmt.Sprintf(rediskey.ReduceStrategySpotTriggerLock, reduceOrder.ApiId, reduceOrder.Symbol), 50, 15, 100*time.Millisecond)
|
||||
|
||||
// if ok, err := lock.AcquireWait(context.Background()); err != nil {
|
||||
// logger.Error("获取锁失败", err)
|
||||
// return errors.New("获取锁失败")
|
||||
// } else if ok {
|
||||
// defer lock.Release()
|
||||
// hasrecord, _ := helper.DefaultRedis.HExists(reduceReduceListKey, utility.IntTostring(reduceOrderStrategy.OrderId), item)
|
||||
|
||||
// if !hasrecord {
|
||||
// logger.Debug("减仓缓存中不存在", item)
|
||||
// return fmt.Errorf("减仓缓存中不存在")
|
||||
// }
|
||||
|
||||
// order, err := orderService.CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit)
|
||||
|
||||
// if err != nil {
|
||||
// logger.Errorf("%d 生成订单失败", reduceOrderStrategy.OrderId)
|
||||
// }
|
||||
|
||||
// reduceOrder.ApiId = order.ApiId
|
||||
// reduceOrder.Id = order.Id
|
||||
// reduceOrder.Pid = order.Pid
|
||||
// reduceOrder.MainId = order.MainId
|
||||
// reduceOrder.Symbol = order.Symbol
|
||||
// reduceOrder.Side = reduceOrderStrategy.Side
|
||||
// reduceOrder.OrderSn = order.OrderSn
|
||||
// reduceOrder.Price = item2.Price
|
||||
// reduceOrder.Num = item2.Num
|
||||
// if SpotReduceTrigger(db, reduceOrder, spotApi, setting, reduceReduceListKey, item, true, reduceOrderStrategy.OrderId) {
|
||||
// reduceOrderStrategy.Items[index].Actived = true
|
||||
// allActive := true
|
||||
// orderId := utility.IntToString(reduceOrderStrategy.OrderId)
|
||||
|
||||
// for _, item3 := range reduceOrderStrategy.Items {
|
||||
// if !item3.Actived {
|
||||
// allActive = false
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
|
||||
// if allActive {
|
||||
// if err := helper.DefaultRedis.HDelField(reduceReduceListKey, orderId); err != nil {
|
||||
// logger.Errorf("删除redis reduceReduceListKey失败 %s", err.Error())
|
||||
// }
|
||||
// } else {
|
||||
// str, _ := sonic.MarshalString(reduceOrderStrategy)
|
||||
// if err := helper.DefaultRedis.HSetField(reduceReduceListKey, orderId, str); err != nil {
|
||||
// logger.Errorf("更新redis reduceReduceListKey失败 %s", err.Error())
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// //减仓单
|
||||
// reduceVal, _ := helper.DefaultRedis.GetAllList(key)
|
||||
|
||||
// for _, item := range reduceVal {
|
||||
// if err := sonic.Unmarshal([]byte(item), &reduceOrder); err != nil {
|
||||
// logger.Error("反序列化失败")
|
||||
// continue
|
||||
// }
|
||||
|
||||
// if reduceOrder.Symbol == trade.Coin+trade.Currency {
|
||||
// orderPrice := reduceOrder.Price
|
||||
// //买入
|
||||
// if strings.ToUpper(reduceOrder.Side) == "SELL" &&
|
||||
// orderPrice.Cmp(tradePrice) >= 0 &&
|
||||
// orderPrice.Cmp(decimal.Zero) > 0 &&
|
||||
// tradePrice.Cmp(decimal.Zero) > 0 {
|
||||
|
||||
// // SpotReduceTrigger(db, reduceOrder, spotApi, setting, key, item, false, 0)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
return nil
|
||||
}
|
||||
|
||||
// 触发现货减仓
|
||||
// isStrategy 是否是策略减仓单
|
||||
// reduceId 策略主减仓单id
|
||||
func SpotReduceTrigger(db *gorm.DB, reduceOrder positiondto.ReduceListItem, setting DbModels.LineSystemSetting, key, item string, isStrategy bool, reduceId int) bool {
|
||||
// tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reduceOrder.Symbol, 1)
|
||||
result := true
|
||||
|
||||
// if err != nil {
|
||||
// logger.Error("获取交易设置失败")
|
||||
// return false
|
||||
// }
|
||||
// lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, reduceOrder.ApiId, reduceOrder.Symbol), 20, 5, 100*time.Millisecond)
|
||||
|
||||
// if ok, err := lock.AcquireWait(context.Background()); err != nil {
|
||||
// logger.Error("获取锁失败", err)
|
||||
// return false
|
||||
// } else if ok {
|
||||
// defer lock.Release()
|
||||
// takeOrders := make([]DbModels.LinePreOrder, 0)
|
||||
// if err := db.Model(&DbModels.LinePreOrder{}).Where("main_id =? AND order_type IN (1,2,4) AND status IN (1,5)", reduceOrder.MainId).Find(&takeOrders).Error; err != nil {
|
||||
// logger.Error("查询止盈单失败")
|
||||
// return false
|
||||
// }
|
||||
// var hasrecord bool
|
||||
|
||||
// if isStrategy {
|
||||
// hasrecord, _ = helper.DefaultRedis.HExists(key, utility.IntToString(reduceId), item)
|
||||
// } else {
|
||||
// hasrecord, _ = helper.DefaultRedis.IsElementInList(key, item)
|
||||
// }
|
||||
|
||||
// if !hasrecord {
|
||||
// logger.Debug("减仓缓存中不存在", item)
|
||||
// return false
|
||||
// }
|
||||
|
||||
// apiInfo, _ := GetApiInfo(reduceOrder.ApiId)
|
||||
|
||||
// if apiInfo.Id == 0 {
|
||||
// logger.Error("现货减仓 查询api用户不存在")
|
||||
// return false
|
||||
// }
|
||||
|
||||
// for _, takeOrder := range takeOrders {
|
||||
// err := CancelOpenOrderByOrderSnLoop(apiInfo, takeOrder.Symbol, takeOrder.OrderSn)
|
||||
|
||||
// if err != nil {
|
||||
// logger.Error("现货撤单失败", err)
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
// price := reduceOrder.Price.Mul(decimal.NewFromInt(1).Sub(setting.ReducePremium.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit))
|
||||
|
||||
// params := OrderPlacementService{
|
||||
// ApiId: reduceOrder.ApiId,
|
||||
// Side: reduceOrder.Side,
|
||||
// Type: "LIMIT",
|
||||
// TimeInForce: "GTC",
|
||||
// Symbol: reduceOrder.Symbol,
|
||||
// Price: price,
|
||||
// Quantity: reduceOrder.Num,
|
||||
// NewClientOrderId: reduceOrder.OrderSn,
|
||||
// }
|
||||
|
||||
// if err := spotApi.OrderPlaceLoop(db, params, 3); err != nil {
|
||||
// result = false
|
||||
// logger.Errorf("现货减仓挂单失败 id:%s err:%v", reduceOrder.Id, err)
|
||||
|
||||
// if err2 := db.Model(&DbModels.LinePreOrder{}).
|
||||
// Where("id =? AND status =0", reduceOrder.Id).
|
||||
// Updates(map[string]interface{}{"status": 2, "desc": err.Error()}).Error; err2 != nil {
|
||||
// logger.Errorf("修改现货减仓状态失败 id:%s err:%v", reduceOrder.Id, err2)
|
||||
// }
|
||||
// } else {
|
||||
// if err := db.Model(&DbModels.LinePreOrder{}).Where("id =?", reduceOrder.Id).Updates(map[string]interface{}{"trigger_time": time.Now()}).Error; err != nil {
|
||||
// logger.Error("更新预下单触发事件失败 ordersn:", reduceOrder.OrderSn)
|
||||
// }
|
||||
|
||||
// if err := db.Model(&DbModels.LinePreOrder{}).
|
||||
// Where("id =? AND status =0", reduceOrder.Id).
|
||||
// Updates(map[string]interface{}{"status": 1}).Error; err != nil {
|
||||
// logger.Errorf("修改现货减仓状态失败 id:%s err:%v", reduceOrder.Id, err)
|
||||
// }
|
||||
|
||||
// //处理减仓单减仓策略
|
||||
// if err := CacheOrderStrategyAndReCreate(db, reduceOrder, 1, tradeSet, setting); err != nil {
|
||||
// logger.Errorf("现货减仓 处理减仓策略失败 id:%v err:%v", reduceOrder.Id, err)
|
||||
// }
|
||||
// }
|
||||
|
||||
// if _, err := helper.DefaultRedis.LRem(key, item); err != nil {
|
||||
// result = false
|
||||
// logger.Errorf("现货减仓 删除缓存失败 id:%v err:%v", reduceOrder.Id, err)
|
||||
// }
|
||||
// } else {
|
||||
// logger.Error("获取锁失败")
|
||||
// result = false
|
||||
// }
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// JudgeTakeOrder 判断是否止盈
|
||||
func JudgeTakeOrder(trades []models.TradeSet, symbolType int) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// JudgeStopOrder 判断是否止损
|
||||
func JudgeStopOrder(trades []models.TradeSet, symbolType int) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
430
services/bitgetservice/bitget_market_client.go
Normal file
430
services/bitgetservice/bitget_market_client.go
Normal file
@ -0,0 +1,430 @@
|
||||
package bitgetservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go-admin/services/proxy"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// BitgetMarketClient Bitget公开行情订阅客户端
|
||||
// 基于官方SDK设计,专门用于公开市场数据订阅
|
||||
type BitgetMarketClient struct {
|
||||
config *BitgetMarketConfig
|
||||
conn *websocket.Conn
|
||||
subscriptions map[SubscribeReq]OnReceive
|
||||
allSubscribes *Set
|
||||
listener OnReceive
|
||||
errorListener OnReceive
|
||||
sendMutex sync.Mutex
|
||||
reconnectMutex sync.Mutex
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
connected bool
|
||||
lastPongTime time.Time
|
||||
proxyType string
|
||||
proxyAddress string
|
||||
reconnectCount int
|
||||
}
|
||||
|
||||
// NewBitgetMarketClient 创建新的Bitget市场数据客户端
|
||||
func NewBitgetMarketClient(config *BitgetMarketConfig) *BitgetMarketClient {
|
||||
if config == nil {
|
||||
config = DefaultMarketConfig()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
return &BitgetMarketClient{
|
||||
config: config,
|
||||
subscriptions: make(map[SubscribeReq]OnReceive),
|
||||
allSubscribes: NewSet(),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
connected: false,
|
||||
lastPongTime: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
// SetProxy 设置代理
|
||||
func (c *BitgetMarketClient) SetProxy(proxyType, proxyAddress string) {
|
||||
c.proxyType = proxyType
|
||||
c.proxyAddress = proxyAddress
|
||||
}
|
||||
|
||||
// SetListeners 设置消息监听器
|
||||
func (c *BitgetMarketClient) SetListeners(msgListener, errorListener OnReceive) {
|
||||
c.listener = msgListener
|
||||
c.errorListener = errorListener
|
||||
}
|
||||
|
||||
// Connect 连接WebSocket
|
||||
func (c *BitgetMarketClient) Connect() error {
|
||||
c.reconnectMutex.Lock()
|
||||
defer c.reconnectMutex.Unlock()
|
||||
|
||||
if c.connected {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 创建WebSocket连接
|
||||
dialer := websocket.DefaultDialer
|
||||
|
||||
// 设置代理
|
||||
if c.proxyAddress != "" {
|
||||
var err error
|
||||
dialer, err = proxy.GetDialer(c.proxyType, c.proxyAddress)
|
||||
if err != nil {
|
||||
return fmt.Errorf("设置代理失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
dialer.HandshakeTimeout = 10 * time.Second
|
||||
log.Infof("正在连接Bitget市场数据WebSocket: %s", c.config.WsUrl)
|
||||
conn, _, err := dialer.Dial(c.config.WsUrl, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("webSocket连接失败: %v", err)
|
||||
}
|
||||
|
||||
c.conn = conn
|
||||
c.connected = true
|
||||
c.lastPongTime = time.Now()
|
||||
c.reconnectCount = 0
|
||||
|
||||
log.Info("Bitget市场数据WebSocket连接成功")
|
||||
|
||||
// 启动消息处理协程
|
||||
go c.readLoop()
|
||||
go c.pingLoop()
|
||||
go c.monitorConnection()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Subscribe 订阅市场数据
|
||||
func (c *BitgetMarketClient) Subscribe(subscribeReqs []SubscribeReq, listener OnReceive) error {
|
||||
if !c.connected {
|
||||
if err := c.Connect(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var args []interface{}
|
||||
for _, req := range subscribeReqs {
|
||||
// 标准化订阅请求
|
||||
normalizedReq := c.normalizeSubscribeReq(req)
|
||||
args = append(args, normalizedReq)
|
||||
|
||||
// 保存订阅信息
|
||||
c.subscriptions[normalizedReq] = listener
|
||||
c.allSubscribes.Add(normalizedReq)
|
||||
}
|
||||
|
||||
// 发送订阅消息
|
||||
wsReq := WsBaseReq{
|
||||
Op: "subscribe",
|
||||
Args: args,
|
||||
}
|
||||
|
||||
return c.sendMessage(wsReq)
|
||||
}
|
||||
|
||||
// Unsubscribe 取消订阅
|
||||
func (c *BitgetMarketClient) Unsubscribe(subscribeReqs []SubscribeReq) error {
|
||||
if !c.connected {
|
||||
return errors.New("WebSocket未连接")
|
||||
}
|
||||
|
||||
var args []interface{}
|
||||
for _, req := range subscribeReqs {
|
||||
normalizedReq := c.normalizeSubscribeReq(req)
|
||||
args = append(args, normalizedReq)
|
||||
|
||||
// 移除订阅信息
|
||||
delete(c.subscriptions, normalizedReq)
|
||||
c.allSubscribes.Remove(normalizedReq)
|
||||
}
|
||||
|
||||
// 发送取消订阅消息
|
||||
wsReq := WsBaseReq{
|
||||
Op: "unsubscribe",
|
||||
Args: args,
|
||||
}
|
||||
|
||||
return c.sendMessage(wsReq)
|
||||
}
|
||||
|
||||
// normalizeSubscribeReq 标准化订阅请求
|
||||
func (c *BitgetMarketClient) normalizeSubscribeReq(req SubscribeReq) SubscribeReq {
|
||||
return SubscribeReq{
|
||||
InstType: strings.ToUpper(req.InstType),
|
||||
Channel: strings.ToLower(req.Channel),
|
||||
InstId: strings.ToUpper(req.InstId),
|
||||
}
|
||||
}
|
||||
|
||||
// sendMessage 发送消息
|
||||
func (c *BitgetMarketClient) sendMessage(msg interface{}) error {
|
||||
if c.conn == nil {
|
||||
return errors.New("WebSocket连接不可用")
|
||||
}
|
||||
|
||||
c.sendMutex.Lock()
|
||||
defer c.sendMutex.Unlock()
|
||||
|
||||
data, err := sonic.Marshal(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化消息失败: %v", err)
|
||||
}
|
||||
|
||||
log.Info("发送消息: %s", string(data))
|
||||
|
||||
c.conn.SetWriteDeadline(time.Now().Add(c.config.WriteTimeout))
|
||||
return c.conn.WriteMessage(websocket.TextMessage, data)
|
||||
}
|
||||
|
||||
// readLoop 消息读取循环
|
||||
func (c *BitgetMarketClient) readLoop() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Error("readLoop panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
default:
|
||||
if c.conn == nil {
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
c.conn.SetReadDeadline(time.Now().Add(c.config.ReadTimeout))
|
||||
_, message, err := c.conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Error("读取消息失败: %v", err)
|
||||
c.handleDisconnect()
|
||||
continue
|
||||
}
|
||||
|
||||
c.handleMessage(string(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleMessage 处理接收到的消息
|
||||
func (c *BitgetMarketClient) handleMessage(message string) {
|
||||
log.Info("收到消息: %s", message)
|
||||
|
||||
// 处理pong消息
|
||||
if message == "pong" {
|
||||
c.lastPongTime = time.Now()
|
||||
log.Debug("收到pong消息")
|
||||
return
|
||||
}
|
||||
|
||||
// 解析JSON消息
|
||||
var jsonMap map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(message), &jsonMap); err != nil {
|
||||
log.Error("解析JSON消息失败: %v", err)
|
||||
if c.errorListener != nil {
|
||||
c.errorListener(message)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 检查错误码
|
||||
if code, exists := jsonMap["code"]; exists {
|
||||
if codeNum, ok := code.(float64); ok && int(codeNum) != 0 {
|
||||
log.Error("收到错误消息: %s", message)
|
||||
if c.errorListener != nil {
|
||||
c.errorListener(message)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 处理订阅确认消息
|
||||
if event, exists := jsonMap["event"]; exists && event == "subscribe" {
|
||||
log.Info("订阅确认: %s", message)
|
||||
return
|
||||
}
|
||||
|
||||
// 处理数据推送
|
||||
if data, exists := jsonMap["data"]; exists && data != nil {
|
||||
if arg, argExists := jsonMap["arg"]; argExists {
|
||||
listener := c.getListener(arg)
|
||||
if listener != nil {
|
||||
listener(message)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 默认处理
|
||||
if c.listener != nil {
|
||||
c.listener(message)
|
||||
}
|
||||
}
|
||||
|
||||
// getListener 获取对应的监听器
|
||||
func (c *BitgetMarketClient) getListener(argJson interface{}) OnReceive {
|
||||
argMap, ok := argJson.(map[string]interface{})
|
||||
if !ok {
|
||||
return c.listener
|
||||
}
|
||||
|
||||
subscribeReq := SubscribeReq{
|
||||
InstType: fmt.Sprintf("%v", argMap["instType"]),
|
||||
Channel: fmt.Sprintf("%v", argMap["channel"]),
|
||||
InstId: fmt.Sprintf("%v", argMap["instId"]),
|
||||
}
|
||||
|
||||
if listener, exists := c.subscriptions[subscribeReq]; exists {
|
||||
return listener
|
||||
}
|
||||
|
||||
return c.listener
|
||||
}
|
||||
|
||||
// pingLoop Ping循环
|
||||
func (c *BitgetMarketClient) pingLoop() {
|
||||
ticker := time.NewTicker(c.config.PingInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if c.connected && c.conn != nil {
|
||||
c.sendMutex.Lock()
|
||||
c.conn.SetWriteDeadline(time.Now().Add(c.config.WriteTimeout))
|
||||
err := c.conn.WriteMessage(websocket.TextMessage, []byte("ping"))
|
||||
c.sendMutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
log.Error("发送ping失败: %v", err)
|
||||
c.handleDisconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// monitorConnection 监控连接状态
|
||||
func (c *BitgetMarketClient) monitorConnection() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if c.connected && time.Since(c.lastPongTime) > 60*time.Second {
|
||||
log.Warn("长时间未收到pong消息,触发重连")
|
||||
c.handleDisconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleDisconnect 处理断开连接
|
||||
func (c *BitgetMarketClient) handleDisconnect() {
|
||||
c.reconnectMutex.Lock()
|
||||
defer c.reconnectMutex.Unlock()
|
||||
|
||||
if !c.connected {
|
||||
return
|
||||
}
|
||||
|
||||
log.Warn("WebSocket连接断开,准备重连")
|
||||
c.connected = false
|
||||
|
||||
if c.conn != nil {
|
||||
c.conn.Close()
|
||||
c.conn = nil
|
||||
}
|
||||
|
||||
// 重连逻辑
|
||||
go c.reconnect()
|
||||
}
|
||||
|
||||
// reconnect 重连
|
||||
func (c *BitgetMarketClient) reconnect() {
|
||||
for c.reconnectCount < c.config.MaxReconnectTimes {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
return
|
||||
default:
|
||||
c.reconnectCount++
|
||||
log.Info("尝试重连 (%d/%d)", c.reconnectCount, c.config.MaxReconnectTimes)
|
||||
|
||||
time.Sleep(c.config.ReconnectInterval)
|
||||
|
||||
if err := c.Connect(); err != nil {
|
||||
log.Error("重连失败: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 重新订阅
|
||||
if err := c.resubscribe(); err != nil {
|
||||
log.Error("重新订阅失败: %v", err)
|
||||
c.handleDisconnect()
|
||||
continue
|
||||
}
|
||||
|
||||
log.Info("重连成功")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Error("达到最大重连次数,停止重连")
|
||||
}
|
||||
|
||||
// resubscribe 重新订阅
|
||||
func (c *BitgetMarketClient) resubscribe() error {
|
||||
if len(c.subscriptions) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var args []interface{}
|
||||
for req := range c.subscriptions {
|
||||
args = append(args, req)
|
||||
}
|
||||
|
||||
wsReq := WsBaseReq{
|
||||
Op: "subscribe",
|
||||
Args: args,
|
||||
}
|
||||
|
||||
return c.sendMessage(wsReq)
|
||||
}
|
||||
|
||||
// Close 关闭客户端
|
||||
func (c *BitgetMarketClient) Close() error {
|
||||
c.cancel()
|
||||
c.connected = false
|
||||
|
||||
if c.conn != nil {
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsConnected 检查连接状态
|
||||
func (c *BitgetMarketClient) IsConnected() bool {
|
||||
return c.connected
|
||||
}
|
||||
61
services/bitgetservice/bitget_market_future.go
Normal file
61
services/bitgetservice/bitget_market_future.go
Normal file
@ -0,0 +1,61 @@
|
||||
package bitgetservice
|
||||
|
||||
import (
|
||||
"go-admin/config"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/logger"
|
||||
)
|
||||
|
||||
type BitgetMarketFutureClient struct {
|
||||
}
|
||||
|
||||
func (c *BitgetMarketFutureClient) Subscribe(symbols []string) error {
|
||||
// 1. 创建Bitget客户端(不需要API密钥用于公开数据)
|
||||
client := &BitgetClient{}
|
||||
|
||||
// 初始化公开行情客户端
|
||||
marketConfig := DefaultMarketConfig()
|
||||
client.marketClient = NewBitgetMarketClient(marketConfig)
|
||||
if config.ExtConfig.ProxyUrl != "" {
|
||||
client.SetMarketProxy("", config.ExtConfig.ProxyUrl)
|
||||
}
|
||||
|
||||
// 3. 设置消息监听器
|
||||
msgListener := func(message string) {
|
||||
logger.Info("收到市场数据: %s", message)
|
||||
// 在这里处理市场数据
|
||||
}
|
||||
|
||||
errorListener := func(message string) {
|
||||
logger.Error("收到错误消息: %s", message)
|
||||
}
|
||||
|
||||
client.SetMarketListeners(msgListener, errorListener)
|
||||
|
||||
// 4. 连接WebSocket
|
||||
if err := client.ConnectMarketData(); err != nil {
|
||||
logger.Error("连接市场数据失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. 订阅合约行情
|
||||
tickerListener := func(message string) {
|
||||
logger.Info("收到BTC-USDT合约行情: %s", message)
|
||||
}
|
||||
|
||||
params := []SubscribeReq{}
|
||||
|
||||
for _, v := range symbols {
|
||||
params = append(params, SubscribeReq{
|
||||
InstType: "USDT-FUTURES",
|
||||
Channel: "ticker",
|
||||
InstId: v,
|
||||
})
|
||||
}
|
||||
|
||||
if err := client.SubscribeMarketData(params, tickerListener); err != nil {
|
||||
logger.Error("订阅合约行情失败: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
126
services/bitgetservice/bitget_market_models.go
Normal file
126
services/bitgetservice/bitget_market_models.go
Normal file
@ -0,0 +1,126 @@
|
||||
package bitgetservice
|
||||
|
||||
import "time"
|
||||
|
||||
// SubscribeReq 订阅请求结构(基于官方SDK)
|
||||
type SubscribeReq struct {
|
||||
InstType string `json:"instType"` // 产品类型:SPOT-现货,UMCBL-U本位合约,DMCBL-币本位合约
|
||||
Channel string `json:"channel"` // 频道名称
|
||||
InstId string `json:"instId"` // 产品ID
|
||||
// Coin string `json:"coin"` // 币种(可选)
|
||||
}
|
||||
|
||||
// WsBaseReq WebSocket基础请求结构
|
||||
type WsBaseReq struct {
|
||||
Op string `json:"op"` // 操作类型:subscribe, unsubscribe, login
|
||||
Args []interface{} `json:"args"` // 参数列表
|
||||
}
|
||||
|
||||
// WsLoginReq WebSocket登录请求
|
||||
type WsLoginReq struct {
|
||||
ApiKey string `json:"apiKey"`
|
||||
Passphrase string `json:"passphrase"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Sign string `json:"sign"`
|
||||
}
|
||||
|
||||
// OnReceive 消息接收回调函数类型
|
||||
type OnReceive func(message string)
|
||||
|
||||
// Set 集合数据结构(简化实现)
|
||||
type Set struct {
|
||||
items map[SubscribeReq]bool
|
||||
}
|
||||
|
||||
// NewSet 创建新的集合
|
||||
func NewSet() *Set {
|
||||
return &Set{
|
||||
items: make(map[SubscribeReq]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// Add 添加元素到集合
|
||||
func (s *Set) Add(item SubscribeReq) {
|
||||
s.items[item] = true
|
||||
}
|
||||
|
||||
// Remove 从集合中移除元素
|
||||
func (s *Set) Remove(item SubscribeReq) {
|
||||
delete(s.items, item)
|
||||
}
|
||||
|
||||
// Contains 检查集合是否包含元素
|
||||
func (s *Set) Contains(item SubscribeReq) bool {
|
||||
_, exists := s.items[item]
|
||||
return exists
|
||||
}
|
||||
|
||||
// BitgetMarketData 市场数据基础结构
|
||||
type BitgetMarketData struct {
|
||||
Action string `json:"action"` // 推送类型
|
||||
Arg SubscribeReq `json:"arg"` // 订阅参数
|
||||
Data interface{} `json:"data"` // 数据内容
|
||||
Ts int64 `json:"ts"` // 时间戳
|
||||
}
|
||||
|
||||
// TickerData 行情数据
|
||||
type TickerData struct {
|
||||
InstId string `json:"instId"` // 产品ID
|
||||
Last string `json:"last"` // 最新价
|
||||
Open24h string `json:"open24h"` // 24小时开盘价
|
||||
High24h string `json:"high24h"` // 24小时最高价
|
||||
Low24h string `json:"low24h"` // 24小时最低价
|
||||
Change24h string `json:"change24h"` // 24小时涨跌幅
|
||||
Vol24h string `json:"vol24h"` // 24小时成交量
|
||||
VolCcy24h string `json:"volCcy24h"` // 24小时成交额
|
||||
Ts string `json:"ts"` // 时间戳
|
||||
}
|
||||
|
||||
// DepthData 深度数据
|
||||
type DepthData struct {
|
||||
InstId string `json:"instId"` // 产品ID
|
||||
Asks [][]string `json:"asks"` // 卖盘 [价格, 数量]
|
||||
Bids [][]string `json:"bids"` // 买盘 [价格, 数量]
|
||||
Ts string `json:"ts"` // 时间戳
|
||||
}
|
||||
|
||||
// TradeData 成交数据
|
||||
type TradeData struct {
|
||||
InstId string `json:"instId"` // 产品ID
|
||||
TradeId string `json:"tradeId"` // 成交ID
|
||||
Px string `json:"px"` // 成交价格
|
||||
Sz string `json:"sz"` // 成交数量
|
||||
Side string `json:"side"` // 成交方向 buy/sell
|
||||
Ts string `json:"ts"` // 成交时间
|
||||
}
|
||||
|
||||
// KlineData K线数据
|
||||
type KlineData struct {
|
||||
InstId string `json:"instId"` // 产品ID
|
||||
Candle []string `json:"candle"` // K线数据 [时间戳, 开盘价, 最高价, 最低价, 收盘价, 成交量, 成交额]
|
||||
Ts string `json:"ts"` // 时间戳
|
||||
}
|
||||
|
||||
// BitgetMarketConfig 市场数据配置
|
||||
type BitgetMarketConfig struct {
|
||||
WsUrl string // WebSocket地址
|
||||
ReconnectInterval time.Duration // 重连间隔
|
||||
PingInterval time.Duration // Ping间隔
|
||||
ReadTimeout time.Duration // 读取超时
|
||||
WriteTimeout time.Duration // 写入超时
|
||||
MaxReconnectTimes int // 最大重连次数
|
||||
EnableCompression bool // 是否启用压缩
|
||||
}
|
||||
|
||||
// DefaultMarketConfig 默认市场数据配置
|
||||
func DefaultMarketConfig() *BitgetMarketConfig {
|
||||
return &BitgetMarketConfig{
|
||||
WsUrl: "wss://ws.bitget.com/v2/ws/public",
|
||||
ReconnectInterval: 5 * time.Second,
|
||||
PingInterval: 15 * time.Second,
|
||||
ReadTimeout: 60 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
MaxReconnectTimes: 10,
|
||||
EnableCompression: false,
|
||||
}
|
||||
}
|
||||
61
services/bitgetservice/bitget_market_spot.go
Normal file
61
services/bitgetservice/bitget_market_spot.go
Normal file
@ -0,0 +1,61 @@
|
||||
package bitgetservice
|
||||
|
||||
import (
|
||||
"go-admin/config"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/logger"
|
||||
)
|
||||
|
||||
type BitgetMarketSpotClient struct {
|
||||
}
|
||||
|
||||
func (c *BitgetMarketSpotClient) Subscribe(symbols []string) error {
|
||||
// 1. 创建Bitget客户端(不需要API密钥用于公开数据)
|
||||
client := &BitgetClient{}
|
||||
|
||||
// 初始化公开行情客户端
|
||||
marketConfig := DefaultMarketConfig()
|
||||
client.marketClient = NewBitgetMarketClient(marketConfig)
|
||||
if config.ExtConfig.ProxyUrl != "" {
|
||||
client.SetMarketProxy("", config.ExtConfig.ProxyUrl)
|
||||
}
|
||||
|
||||
// 3. 设置消息监听器
|
||||
msgListener := func(message string) {
|
||||
logger.Infof("收到市场数据: %s", message)
|
||||
// 在这里处理市场数据
|
||||
}
|
||||
|
||||
errorListener := func(message string) {
|
||||
logger.Errorf("收到错误消息: %s", message)
|
||||
}
|
||||
|
||||
client.SetMarketListeners(msgListener, errorListener)
|
||||
|
||||
// 4. 连接WebSocket
|
||||
if err := client.ConnectMarketData(); err != nil {
|
||||
logger.Errorf("连接市场数据失败: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. 订阅现货行情
|
||||
tickerListener := func(message string) {
|
||||
logger.Infof("收到行情: %s", message)
|
||||
}
|
||||
|
||||
params := []SubscribeReq{}
|
||||
|
||||
for _, v := range symbols {
|
||||
params = append(params, SubscribeReq{
|
||||
InstType: "SPOT",
|
||||
Channel: "ticker",
|
||||
InstId: v,
|
||||
})
|
||||
}
|
||||
|
||||
if err := client.SubscribeMarketData(params, tickerListener); err != nil {
|
||||
logger.Errorf("订阅现货行情失败: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
54
services/bitgetservice/bitget_market_test.go
Normal file
54
services/bitgetservice/bitget_market_test.go
Normal file
@ -0,0 +1,54 @@
|
||||
package bitgetservice
|
||||
|
||||
import (
|
||||
"go-admin/common/helper"
|
||||
"go-admin/config"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/sdk"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func initSetting() {
|
||||
dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
sdk.Runtime.SetDb("default", db)
|
||||
helper.InitDefaultRedis("127.0.0.1:6379", "", 2)
|
||||
helper.InitLockRedisConn("127.0.0.1:6379", "", "2")
|
||||
config.ExtConfig.ProxyUrl = "http://127.0.0.1:7890"
|
||||
}
|
||||
|
||||
func TestBitgetMarketFutureClient_Subscribe(t *testing.T) {
|
||||
initSetting()
|
||||
// 初始化Bitget合约行情客户端
|
||||
bitgetFut := BitgetMarketFutureClient{}
|
||||
|
||||
// 测试用的合约符号列表
|
||||
testSymbols := []string{"ETHUSDT"}
|
||||
|
||||
// 调用订阅方法
|
||||
err := bitgetFut.Subscribe(testSymbols)
|
||||
if err != nil {
|
||||
t.Errorf("订阅Bitget合约行情失败: %v", err)
|
||||
}
|
||||
time.Sleep(60 * time.Second)
|
||||
}
|
||||
|
||||
func TestBitgetMarketSpotClient_Subscribe(t *testing.T) {
|
||||
initSetting()
|
||||
// 初始化Bitget现货行情客户端
|
||||
bitgetSpot := BitgetMarketSpotClient{}
|
||||
|
||||
// 测试用的现货符号列表
|
||||
testSymbols := []string{"ETHUSDT"}
|
||||
|
||||
// 调用订阅方法
|
||||
err := bitgetSpot.Subscribe(testSymbols)
|
||||
if err != nil {
|
||||
t.Errorf("订阅Bitget现货行情失败: %v", err)
|
||||
}
|
||||
|
||||
time.Sleep(60 * time.Second)
|
||||
}
|
||||
417
services/bitgetservice/bitget_rest.go
Normal file
417
services/bitgetservice/bitget_rest.go
Normal file
@ -0,0 +1,417 @@
|
||||
package bitgetservice
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go-admin/common/global"
|
||||
"go-admin/common/helper"
|
||||
"go-admin/config"
|
||||
"go-admin/models"
|
||||
"go-admin/pkg/utility"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
)
|
||||
|
||||
// BitgetRestApi Bitget REST API服务
|
||||
type BitgetRestApi struct {
|
||||
httpClient *helper.BitgetClient
|
||||
}
|
||||
|
||||
// NewBitgetRestApi 创建新的Bitget REST API实例
|
||||
func NewBitgetRestApi() *BitgetRestApi {
|
||||
// 从配置中获取代理设置
|
||||
proxyUrl := config.ExtConfig.ProxyUrl
|
||||
|
||||
// 使用代理配置创建HTTP客户端
|
||||
httpClient, err := helper.NewBitgetClient("", "", "", proxyUrl)
|
||||
if err != nil {
|
||||
log.Error("创建Bitget HTTP客户端失败", err)
|
||||
// 如果代理创建失败,使用无代理的客户端
|
||||
httpClient, _ = helper.NewBitgetClient("", "", "", "")
|
||||
}
|
||||
|
||||
return &BitgetRestApi{
|
||||
httpClient: httpClient,
|
||||
}
|
||||
}
|
||||
|
||||
// NewBitgetSimpleClient 创建简单的Bitget客户端(兼容性函数)
|
||||
func NewBitgetSimpleClient() *BitgetClient {
|
||||
// 从配置中获取代理设置
|
||||
proxyUrl := config.ExtConfig.ProxyUrl
|
||||
|
||||
client, err := NewBitgetClient("", "", "", proxyUrl)
|
||||
if err != nil {
|
||||
log.Error("创建Bitget简单客户端失败", err)
|
||||
// 如果代理创建失败,使用无代理的客户端
|
||||
client, _ = NewBitgetClient("", "", "", "")
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
// bitgetSymbolResponse 通用的符号响应结构体
|
||||
type bitgetSymbolResponse struct {
|
||||
Code string `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data []struct {
|
||||
Symbol string `json:"symbol"`
|
||||
BaseCoin string `json:"baseCoin"`
|
||||
QuoteCoin string `json:"quoteCoin"`
|
||||
Status string `json:"status"`
|
||||
MinTradeAmount string `json:"minTradeAmount"`
|
||||
MaxTradeAmount string `json:"maxTradeAmount"`
|
||||
TakerFeeRate string `json:"takerFeeRate"`
|
||||
MakerFeeRate string `json:"makerFeeRate"`
|
||||
PricePrecision string `json:"pricePrecision"`
|
||||
QuantityPrecision string `json:"quantityPrecision"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// getSymbolsFromAPI 通用的符号获取方法
|
||||
// symbolType: "spot" 现货, "futures" 合约
|
||||
func (api *BitgetRestApi) getSymbolsFromAPI(symbolType string) ([]BitgetSymbol, error) {
|
||||
var url string
|
||||
var errorPrefix string
|
||||
|
||||
switch symbolType {
|
||||
case "spot":
|
||||
url = "/api/v2/spot/public/symbols"
|
||||
errorPrefix = "现货"
|
||||
case "futures":
|
||||
url = "/api/v2/mix/market/contracts?productType=USDT-FUTURES"
|
||||
errorPrefix = "合约"
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的符号类型: %s", symbolType)
|
||||
}
|
||||
|
||||
// 发送API请求
|
||||
resp, _, err := api.httpClient.SendSpotRequestAuth(url, "GET", map[string]interface{}{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取bitget %s交易所信息失败: %v", errorPrefix, err)
|
||||
}
|
||||
|
||||
// 解析响应数据
|
||||
var response bitgetSymbolResponse
|
||||
if err := sonic.Unmarshal(resp, &response); err != nil {
|
||||
return nil, fmt.Errorf("解析bitget %s交易所信息失败: %v", errorPrefix, err)
|
||||
}
|
||||
|
||||
// 检查API响应状态
|
||||
if response.Code != "00000" {
|
||||
return nil, fmt.Errorf("bitget API错误: %s", response.Msg)
|
||||
}
|
||||
|
||||
// 转换数据格式
|
||||
return api.convertToSymbols(response.Data), nil
|
||||
}
|
||||
|
||||
// convertToSymbols 将API响应数据转换为BitgetSymbol切片
|
||||
func (api *BitgetRestApi) convertToSymbols(data []struct {
|
||||
Symbol string `json:"symbol"`
|
||||
BaseCoin string `json:"baseCoin"`
|
||||
QuoteCoin string `json:"quoteCoin"`
|
||||
Status string `json:"status"`
|
||||
MinTradeAmount string `json:"minTradeAmount"`
|
||||
MaxTradeAmount string `json:"maxTradeAmount"`
|
||||
TakerFeeRate string `json:"takerFeeRate"`
|
||||
MakerFeeRate string `json:"makerFeeRate"`
|
||||
PricePrecision string `json:"pricePrecision"`
|
||||
QuantityPrecision string `json:"quantityPrecision"`
|
||||
}) []BitgetSymbol {
|
||||
symbols := make([]BitgetSymbol, 0, len(data))
|
||||
|
||||
for _, item := range data {
|
||||
if item.Status == "online" {
|
||||
priceDigit, _ := strconv.Atoi(item.PricePrecision)
|
||||
quantileDigit, _ := strconv.Atoi(item.QuantityPrecision)
|
||||
|
||||
symbol := BitgetSymbol{
|
||||
Symbol: item.Symbol,
|
||||
BaseAsset: item.BaseCoin,
|
||||
QuoteAsset: item.QuoteCoin,
|
||||
Status: item.Status,
|
||||
MinQty: item.MinTradeAmount,
|
||||
MaxQty: item.MaxTradeAmount,
|
||||
PriceDigit: priceDigit,
|
||||
QuantityDigit: quantileDigit,
|
||||
}
|
||||
symbols = append(symbols, symbol)
|
||||
}
|
||||
}
|
||||
|
||||
return symbols
|
||||
}
|
||||
|
||||
// GetSpotSymbols 获取现货交易对信息
|
||||
func (api *BitgetRestApi) GetSpotSymbols() ([]BitgetSymbol, error) {
|
||||
return api.getSymbolsFromAPI("spot")
|
||||
}
|
||||
|
||||
// GetFutSymbols 获取合约交易对信息
|
||||
func (api *BitgetRestApi) GetFutSymbols() ([]BitgetSymbol, error) {
|
||||
return api.getSymbolsFromAPI("futures")
|
||||
}
|
||||
|
||||
// symbolConfig 符号配置结构体
|
||||
type symbolConfig struct {
|
||||
symbolType string
|
||||
quoteAssetSymbols []string
|
||||
}
|
||||
|
||||
// buildTradeSetFromSymbols 通用的TradeSet构建方法
|
||||
func buildTradeSetFromSymbols(symbols []BitgetSymbol, config symbolConfig) (map[string]models.TradeSet, []string) {
|
||||
tradeSets := make(map[string]models.TradeSet, len(symbols))
|
||||
deleteSymbols := make([]string, 0)
|
||||
|
||||
for _, item := range symbols {
|
||||
if utility.ContainsStr(config.quoteAssetSymbols, item.QuoteAsset) && item.Status == "online" {
|
||||
tradeSet := models.TradeSet{
|
||||
Coin: item.BaseAsset,
|
||||
Currency: item.QuoteAsset,
|
||||
PriceDigit: item.PriceDigit,
|
||||
AmountDigit: item.QuantityDigit,
|
||||
MinQty: utility.StringAsFloat(item.MinQty),
|
||||
}
|
||||
tradeSets[item.Symbol] = tradeSet
|
||||
}
|
||||
}
|
||||
|
||||
return tradeSets, deleteSymbols
|
||||
}
|
||||
|
||||
// GetSpotSymbols 获取现货交易对并构建TradeSet映射
|
||||
func GetSpotSymbols() (map[string]models.TradeSet, []string, error) {
|
||||
api := NewBitgetRestApi()
|
||||
symbols, err := api.GetSpotSymbols()
|
||||
if err != nil {
|
||||
log.Error("获取bitget规范信息失败", err)
|
||||
return make(map[string]models.TradeSet, 0), []string{}, err
|
||||
}
|
||||
|
||||
config := symbolConfig{
|
||||
symbolType: "spot",
|
||||
quoteAssetSymbols: []string{"USDT", "ETH", "BTC", "SOL", "BNB", "DOGE"},
|
||||
}
|
||||
|
||||
tradeSets, deleteSymbols := buildTradeSetFromSymbols(symbols, config)
|
||||
return tradeSets, deleteSymbols, nil
|
||||
}
|
||||
|
||||
// GetFutSymbols 获取合约交易对并构建TradeSet映射
|
||||
func GetFutSymbols() (map[string]models.TradeSet, []string, error) {
|
||||
api := NewBitgetRestApi()
|
||||
symbols, err := api.GetFutSymbols()
|
||||
if err != nil {
|
||||
log.Error("获取bitget合约规范信息失败", err)
|
||||
return make(map[string]models.TradeSet, 0), []string{}, err
|
||||
}
|
||||
|
||||
config := symbolConfig{
|
||||
symbolType: "futures",
|
||||
quoteAssetSymbols: []string{"USDT"},
|
||||
}
|
||||
|
||||
tradeSets, deleteSymbols := buildTradeSetFromSymbols(symbols, config)
|
||||
return tradeSets, deleteSymbols, nil
|
||||
}
|
||||
|
||||
// getTicker24hData 获取24小时行情数据的通用方法
|
||||
func (api *BitgetRestApi) getTicker24hData(url, tickerType, description string, tradeSet *map[string]models.TradeSet) ([]string, error) {
|
||||
// 定义响应数据结构
|
||||
type tickerResponse struct {
|
||||
Code string `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data []struct {
|
||||
Symbol string `json:"symbol"`
|
||||
High24h string `json:"high24h"`
|
||||
Low24h string `json:"low24h"`
|
||||
Close string `json:"lastPr"`
|
||||
Open string `json:"open"`
|
||||
Volume24h string `json:"baseVolume"` // 基础币成交量
|
||||
QuoteVol string `json:"usdtVolume"` // USDT成交量
|
||||
Change24h string `json:"changeUtc24h"` // UTC 24h涨跌幅
|
||||
Change string `json:"change24h"` // 24h涨跌幅
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// 发送API请求
|
||||
resp, _, err := api.httpClient.SendSpotRequestAuth(url, "GET", map[string]interface{}{})
|
||||
if err != nil {
|
||||
return []string{}, fmt.Errorf("获取bitget %s 24h行情失败: %v", description, err)
|
||||
}
|
||||
|
||||
// 解析响应数据
|
||||
var response tickerResponse
|
||||
if err := sonic.Unmarshal(resp, &response); err != nil {
|
||||
return []string{}, fmt.Errorf("解析bitget %s 24h行情失败: %v", description, err)
|
||||
}
|
||||
|
||||
// 检查API响应状态
|
||||
if response.Code != "00000" {
|
||||
return []string{}, fmt.Errorf("bitget %s API错误: %s", description, response.Msg)
|
||||
}
|
||||
|
||||
// 处理行情数据
|
||||
deleteSymbols := make([]string, 0)
|
||||
processedCount := 0
|
||||
errorCount := 0
|
||||
|
||||
for _, item := range response.Data {
|
||||
// 构建缓存键
|
||||
key := fmt.Sprintf(tickerType, global.EXCHANGE_BITGET, item.Symbol)
|
||||
|
||||
// 检查交易对是否存在
|
||||
symbol, exists := (*tradeSet)[item.Symbol]
|
||||
if !exists {
|
||||
// 清理不存在的交易对缓存
|
||||
if err := helper.DefaultRedis.DeleteString(key); err != nil {
|
||||
log.Warn("删除无效缓存失败: %s, error: %v", key, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 解析交易对获取coin和currency
|
||||
var coin, currency string
|
||||
if strings.HasSuffix(item.Symbol, symbol.Currency) {
|
||||
coin = strings.TrimSuffix(item.Symbol, symbol.Currency)
|
||||
currency = symbol.Currency
|
||||
} else {
|
||||
deleteSymbols = append(deleteSymbols, item.Symbol)
|
||||
continue
|
||||
}
|
||||
|
||||
// 构建交易对数据
|
||||
symbol.Coin = coin
|
||||
symbol.Currency = currency
|
||||
symbol.OpenPrice = utility.StringAsFloat(item.Open)
|
||||
symbol.PriceChange = utility.StringAsFloat(item.Change)
|
||||
symbol.LowPrice = item.Low24h
|
||||
symbol.HighPrice = item.High24h
|
||||
symbol.Volume = item.Volume24h
|
||||
symbol.QuoteVolume = item.QuoteVol
|
||||
symbol.LastPrice = item.Close
|
||||
|
||||
// 验证数据有效性
|
||||
if symbol.Coin == "" || symbol.Currency == "" ||
|
||||
utility.StringToFloat64(symbol.QuoteVolume) <= 0 ||
|
||||
symbol.LastPrice == "" || utility.StringToFloat64(symbol.LastPrice) <= 0 {
|
||||
if err := helper.DefaultRedis.DeleteString(key); err != nil {
|
||||
log.Warn("删除无效数据缓存失败: %s, error: %v", key, err)
|
||||
}
|
||||
deleteSymbols = append(deleteSymbols, item.Symbol)
|
||||
continue
|
||||
}
|
||||
|
||||
// 缓存数据
|
||||
val, err := sonic.Marshal(symbol)
|
||||
if err != nil {
|
||||
log.Error("序列化交易对数据失败: %s, error: %v", item.Symbol, err)
|
||||
errorCount++
|
||||
continue
|
||||
}
|
||||
|
||||
if err := helper.DefaultRedis.SetString(key, string(val)); err != nil {
|
||||
log.Error("缓存交易对失败: %s, error: %v", item.Symbol, err)
|
||||
errorCount++
|
||||
continue
|
||||
}
|
||||
|
||||
processedCount++
|
||||
}
|
||||
|
||||
log.Info("处理%s行情数据完成: 成功=%d, 错误=%d, 删除=%d",
|
||||
description, processedCount, errorCount, len(deleteSymbols))
|
||||
|
||||
return deleteSymbols, nil
|
||||
}
|
||||
|
||||
// GetSpotTicker24h 获取现货24小时行情变更并缓存
|
||||
func (api *BitgetRestApi) GetSpotTicker24h(tradeSet *map[string]models.TradeSet) ([]string, error) {
|
||||
return api.getTicker24hData("/api/v2/spot/market/tickers", global.TICKER_SPOT, "现货", tradeSet)
|
||||
}
|
||||
|
||||
// GetFutTicker24h 获取U本位合约24小时行情变更并缓存
|
||||
func (api *BitgetRestApi) GetFutTicker24h(tradeSet *map[string]models.TradeSet) ([]string, error) {
|
||||
return api.getTicker24hData("/api/v2/mix/market/tickers?productType=USDT-FUTURES", global.TICKER_FUTURES, "U本位合约", tradeSet)
|
||||
}
|
||||
|
||||
// BitgetSymbol Bitget交易对信息
|
||||
type BitgetSymbol struct {
|
||||
Symbol string `json:"symbol"`
|
||||
BaseCoin string `json:"baseCoin"`
|
||||
QuoteCoin string `json:"quoteCoin"`
|
||||
BaseAsset string `json:"baseAsset"`
|
||||
QuoteAsset string `json:"quoteAsset"`
|
||||
Status string `json:"status"`
|
||||
MinQty string `json:"minQty"`
|
||||
MaxQty string `json:"maxQty"`
|
||||
PriceDigit int `json:"priceDigit"`
|
||||
QuantityDigit int `json:"quantityDigit"`
|
||||
}
|
||||
|
||||
// GetAndReloadSymbols 获取并重新加载交易对信息
|
||||
// symbolType 1-现货 2-合约
|
||||
func GetAndReloadSymbols(data *map[string]models.TradeSet, symbolType int) error {
|
||||
var tradeSets map[string]models.TradeSet
|
||||
var err error
|
||||
|
||||
switch symbolType {
|
||||
case 1:
|
||||
tradeSets, _, err = GetSpotSymbols()
|
||||
case 2:
|
||||
tradeSets, _, err = GetFutSymbols()
|
||||
default:
|
||||
return fmt.Errorf("无效的symbolType: %d", symbolType)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*data = tradeSets
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitSymbolsTicker24h 初始化24小时行情数据
|
||||
// symbolType 1-现货 2-合约
|
||||
func InitSymbolsTicker24h(data *map[string]models.TradeSet, symbolType int) ([]string, error) {
|
||||
api := NewBitgetRestApi()
|
||||
|
||||
switch symbolType {
|
||||
case 1:
|
||||
return api.GetSpotTicker24h(data)
|
||||
case 2:
|
||||
return api.GetFutTicker24h(data)
|
||||
default:
|
||||
return []string{}, fmt.Errorf("无效的symbolType: %d", symbolType)
|
||||
}
|
||||
}
|
||||
|
||||
// GetSpotTicker24 获取单个交易对24h行情
|
||||
func (api *BitgetRestApi) GetSpotTicker24(symbol string, data *models.Ticker24, tradeSet *models.TradeSet) error {
|
||||
key := fmt.Sprintf(global.TICKER_SPOT, global.EXCHANGE_BITGET, symbol)
|
||||
val, err := helper.DefaultRedis.GetString(key)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = sonic.Unmarshal([]byte(val), tradeSet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tradeSet.Coin != "" {
|
||||
data.HighPrice = tradeSet.HighPrice
|
||||
data.ChangePercent = fmt.Sprintf("%g", tradeSet.PriceChange)
|
||||
data.LastPrice = tradeSet.LastPrice
|
||||
data.LowPrice = tradeSet.LowPrice
|
||||
data.OpenPrice = fmt.Sprintf("%g", tradeSet.OpenPrice)
|
||||
data.QuoteVolume = tradeSet.QuoteVolume
|
||||
data.Volume = tradeSet.Volume
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
41
services/bitgetservice/bitget_rest_test.go
Normal file
41
services/bitgetservice/bitget_rest_test.go
Normal file
@ -0,0 +1,41 @@
|
||||
package bitgetservice
|
||||
|
||||
import (
|
||||
"go-admin/config"
|
||||
"go-admin/test"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetSpotSymbols(t *testing.T) {
|
||||
test.InitSettings()
|
||||
api := NewBitgetRestApi()
|
||||
|
||||
symbols, err := api.GetSpotSymbols()
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
t.Log(symbols)
|
||||
}
|
||||
|
||||
// 获取交易所信息
|
||||
func TestGetExchangeInfo(t *testing.T) {
|
||||
test.InitSettings()
|
||||
|
||||
client, err := NewBitgetClient("", "", "", config.ExtConfig.ProxyUrl)
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
|
||||
}
|
||||
|
||||
data, err := client.GetExchangeInfo()
|
||||
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
|
||||
}
|
||||
|
||||
t.Log(data)
|
||||
}
|
||||
658
services/bitgetservice/bitget_socketmanager.go
Normal file
658
services/bitgetservice/bitget_socketmanager.go
Normal file
@ -0,0 +1,658 @@
|
||||
package bitgetservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go-admin/pkg/utility"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
"github.com/gorilla/websocket"
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
// BitgetWebSocketManager Bitget WebSocket管理器
|
||||
// 类似于BinanceWebSocketManager,支持外部传入API密钥
|
||||
type BitgetWebSocketManager struct {
|
||||
ws *websocket.Conn
|
||||
stopChannel chan struct{}
|
||||
url string
|
||||
/* 0-现货 1-合约 */
|
||||
wsType int
|
||||
apiKey string
|
||||
apiSecret string
|
||||
passphrase string
|
||||
proxyType string
|
||||
proxyAddress string
|
||||
reconnect chan struct{}
|
||||
isStopped bool // 标记 WebSocket 是否已主动停止
|
||||
mu sync.Mutex // 用于控制并发访问 isStopped
|
||||
cancelFunc context.CancelFunc
|
||||
reconnecting atomic.Bool // 防止重复重连
|
||||
ConnectTime time.Time // 当前连接建立时间
|
||||
}
|
||||
|
||||
// 已有连接存储
|
||||
var BitgetSpotSockets = map[string]*BitgetWebSocketManager{}
|
||||
var BitgetFutureSockets = map[string]*BitgetWebSocketManager{}
|
||||
|
||||
// NewBitgetWebSocketManager 创建新的Bitget WebSocket管理器
|
||||
func NewBitgetWebSocketManager(wsType int, apiKey, apiSecret, passphrase, proxyType, proxyAddress string) *BitgetWebSocketManager {
|
||||
var wsUrl string
|
||||
|
||||
switch wsType {
|
||||
case 0: // 现货
|
||||
wsUrl = "wss://ws.bitget.com/spot/v1/stream"
|
||||
case 1: // 合约
|
||||
wsUrl = "wss://ws.bitget.com/mix/v1/stream"
|
||||
default:
|
||||
wsUrl = "wss://ws.bitget.com/spot/v1/stream"
|
||||
}
|
||||
|
||||
return &BitgetWebSocketManager{
|
||||
stopChannel: make(chan struct{}, 10),
|
||||
reconnect: make(chan struct{}, 10),
|
||||
isStopped: false,
|
||||
url: wsUrl,
|
||||
wsType: wsType,
|
||||
apiKey: apiKey,
|
||||
apiSecret: apiSecret,
|
||||
passphrase: passphrase,
|
||||
proxyType: proxyType,
|
||||
proxyAddress: proxyAddress,
|
||||
}
|
||||
}
|
||||
|
||||
// GetKey 获取API Key
|
||||
func (wm *BitgetWebSocketManager) GetKey() string {
|
||||
return wm.apiKey
|
||||
}
|
||||
|
||||
// Start 启动WebSocket连接
|
||||
func (wm *BitgetWebSocketManager) Start() {
|
||||
utility.SafeGo(wm.run)
|
||||
}
|
||||
|
||||
// Restart 重启连接
|
||||
func (wm *BitgetWebSocketManager) Restart(apiKey, apiSecret, passphrase, proxyType, proxyAddress string) *BitgetWebSocketManager {
|
||||
wm.mu.Lock()
|
||||
defer wm.mu.Unlock()
|
||||
|
||||
wm.apiKey = apiKey
|
||||
wm.apiSecret = apiSecret
|
||||
wm.passphrase = passphrase
|
||||
wm.proxyType = proxyType
|
||||
wm.proxyAddress = proxyAddress
|
||||
|
||||
if wm.isStopped {
|
||||
wm.isStopped = false
|
||||
utility.SafeGo(wm.run)
|
||||
} else {
|
||||
log.Warnf("调用Bitget WebSocket restart")
|
||||
wm.triggerReconnect(true)
|
||||
}
|
||||
|
||||
return wm
|
||||
}
|
||||
|
||||
// triggerReconnect 触发重连
|
||||
func (wm *BitgetWebSocketManager) triggerReconnect(force bool) {
|
||||
if force {
|
||||
wm.reconnecting.Store(false)
|
||||
}
|
||||
|
||||
if wm.reconnecting.CompareAndSwap(false, true) {
|
||||
log.Warnf("准备重连Bitget WebSocket key: %s wsType: %v", wm.apiKey, wm.wsType)
|
||||
select {
|
||||
case wm.reconnect <- struct{}{}:
|
||||
default:
|
||||
log.Debugf("Bitget reconnect 信号已存在,跳过 key:%s", wm.apiKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// run 主运行循环
|
||||
func (wm *BitgetWebSocketManager) run() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
wm.cancelFunc = cancel
|
||||
|
||||
utility.SafeGo(wm.handleSignal)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
if err := wm.connect(ctx); err != nil {
|
||||
wm.handleConnectionError(err)
|
||||
|
||||
if wm.isErrorCountExceeded() {
|
||||
log.Error("连接Bitget WebSocket时出错次数过多,停止WebSocket管理器: wsType=%v, key=%s", wm.wsType, wm.apiKey)
|
||||
wm.Stop()
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
<-wm.stopChannel
|
||||
log.Info("停止Bitget WebSocket管理器... wsType=%s", getBitgetWsTypeName(wm.wsType))
|
||||
wm.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleConnectionError 处理连接错误
|
||||
func (wm *BitgetWebSocketManager) handleConnectionError(err error) {
|
||||
// 这里可以添加错误记录到Redis的逻辑,类似binance的实现
|
||||
log.Error("连接Bitget WebSocket时出错: wsType=%v, key=%v, 错误=%v", wm.wsType, wm.apiKey, err)
|
||||
}
|
||||
|
||||
// isErrorCountExceeded 检查错误次数是否超过阈值
|
||||
func (wm *BitgetWebSocketManager) isErrorCountExceeded() bool {
|
||||
// 简化版本,可以根据需要实现更复杂的错误计数逻辑
|
||||
return false
|
||||
}
|
||||
|
||||
// handleSignal 处理终止信号
|
||||
func (wm *BitgetWebSocketManager) handleSignal() {
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, os.Interrupt)
|
||||
<-ch
|
||||
wm.Stop()
|
||||
}
|
||||
|
||||
// connect 建立WebSocket连接
|
||||
func (wm *BitgetWebSocketManager) connect(ctx context.Context) error {
|
||||
dialer, err := wm.getDialer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 对于私有频道,需要进行身份验证
|
||||
headers := wm.getAuthHeaders()
|
||||
|
||||
wm.ws, _, err = dialer.Dial(wm.url, headers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 连接成功,更新连接时间
|
||||
wm.ConnectTime = time.Now()
|
||||
log.Info("已连接到Bitget %s WebSocket key:%s", getBitgetWsTypeName(wm.wsType), wm.apiKey)
|
||||
|
||||
// 设置Ping处理器
|
||||
wm.ws.SetPingHandler(func(appData string) error {
|
||||
log.Info("收到Bitget Ping消息 wstype:%v key:%s data:%s", wm.wsType, wm.apiKey, appData)
|
||||
|
||||
for x := 0; x < 5; x++ {
|
||||
if err := wm.ws.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(time.Second*10)); err != nil {
|
||||
log.Error("Bitget回应Pong失败 次数:%d err:%v", x, err)
|
||||
time.Sleep(time.Second * 1)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// 启动各种处理协程
|
||||
utility.SafeGo(func() { wm.readMessages(ctx) })
|
||||
utility.SafeGo(func() { wm.handleReconnect(ctx) })
|
||||
utility.SafeGo(func() { wm.startPingLoop(ctx) })
|
||||
|
||||
// 发送登录认证消息
|
||||
if err := wm.authenticate(); err != nil {
|
||||
return fmt.Errorf("bitget WebSocket认证失败: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDialer 获取WebSocket拨号器
|
||||
func (wm *BitgetWebSocketManager) getDialer() (*websocket.Dialer, error) {
|
||||
if wm.proxyAddress == "" {
|
||||
return &websocket.Dialer{}, nil
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(wm.proxyAddress, "http://") &&
|
||||
!strings.HasPrefix(wm.proxyAddress, "https://") &&
|
||||
!strings.HasPrefix(wm.proxyAddress, "socks5://") {
|
||||
wm.proxyAddress = wm.proxyType + "://" + wm.proxyAddress
|
||||
}
|
||||
|
||||
proxyURL, err := url.Parse(wm.proxyAddress)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse proxy URL: %v", err)
|
||||
}
|
||||
|
||||
switch proxyURL.Scheme {
|
||||
case "socks5":
|
||||
return wm.createSocks5Dialer(proxyURL)
|
||||
case "http", "https":
|
||||
return &websocket.Dialer{
|
||||
Proxy: func(req *http.Request) (*url.URL, error) {
|
||||
return proxyURL, nil
|
||||
},
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported proxy scheme: %s", proxyURL.Scheme)
|
||||
}
|
||||
}
|
||||
|
||||
// createSocks5Dialer 创建SOCKS5代理拨号器
|
||||
func (wm *BitgetWebSocketManager) createSocks5Dialer(proxyURL *url.URL) (*websocket.Dialer, error) {
|
||||
auth := &proxy.Auth{}
|
||||
if proxyURL.User != nil {
|
||||
auth.User = proxyURL.User.Username()
|
||||
auth.Password, _ = proxyURL.User.Password()
|
||||
}
|
||||
|
||||
socksDialer, err := proxy.SOCKS5("tcp", proxyURL.Host, auth, proxy.Direct)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create SOCKS5 proxy dialer: %v", err)
|
||||
}
|
||||
|
||||
return &websocket.Dialer{
|
||||
NetDialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return socksDialer.Dial(network, addr)
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getAuthHeaders 获取认证头信息
|
||||
func (wm *BitgetWebSocketManager) getAuthHeaders() http.Header {
|
||||
headers := http.Header{}
|
||||
|
||||
if wm.apiKey != "" {
|
||||
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
sign := wm.generateSignature(timestamp)
|
||||
|
||||
headers.Set("ACCESS-KEY", wm.apiKey)
|
||||
headers.Set("ACCESS-SIGN", sign)
|
||||
headers.Set("ACCESS-TIMESTAMP", timestamp)
|
||||
headers.Set("ACCESS-PASSPHRASE", wm.passphrase)
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
// generateSignature 生成签名
|
||||
func (wm *BitgetWebSocketManager) generateSignature(timestamp string) string {
|
||||
message := timestamp + "GET" + "/user/verify"
|
||||
|
||||
h := hmac.New(sha256.New, []byte(wm.apiSecret))
|
||||
h.Write([]byte(message))
|
||||
|
||||
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
|
||||
// authenticate 发送认证消息
|
||||
func (wm *BitgetWebSocketManager) authenticate() error {
|
||||
if wm.apiKey == "" {
|
||||
return nil // 公共频道不需要认证
|
||||
}
|
||||
|
||||
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
sign := wm.generateSignature(timestamp)
|
||||
|
||||
authMsg := map[string]interface{}{
|
||||
"op": "login",
|
||||
"args": []map[string]string{
|
||||
{
|
||||
"apiKey": wm.apiKey,
|
||||
"passphrase": wm.passphrase,
|
||||
"timestamp": timestamp,
|
||||
"sign": sign,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
msgBytes, err := sonic.Marshal(authMsg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return wm.ws.WriteMessage(websocket.TextMessage, msgBytes)
|
||||
}
|
||||
|
||||
// readMessages 读取消息
|
||||
func (wm *BitgetWebSocketManager) readMessages(ctx context.Context) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Error("Bitget WebSocket消息处理器panic: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
wm.mu.Lock()
|
||||
if wm.isStopped || wm.ws == nil {
|
||||
wm.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
_, message, err := wm.ws.ReadMessage()
|
||||
wm.mu.Unlock()
|
||||
|
||||
if err != nil {
|
||||
log.Error("Bitget WebSocket读取消息错误: %v", err)
|
||||
wm.triggerReconnect(false)
|
||||
return
|
||||
}
|
||||
|
||||
// 处理消息
|
||||
wm.handleMessage(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleMessage 处理收到的消息
|
||||
func (wm *BitgetWebSocketManager) handleMessage(message []byte) {
|
||||
// 解析消息并处理
|
||||
var baseMsg map[string]interface{}
|
||||
if err := sonic.Unmarshal(message, &baseMsg); err != nil {
|
||||
log.Error("解析Bitget WebSocket消息失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否是认证响应
|
||||
if op, exists := baseMsg["op"]; exists && op == "login" {
|
||||
wm.handleAuthResponse(baseMsg)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否是订阅数据
|
||||
if arg, exists := baseMsg["arg"]; exists {
|
||||
wm.handleSubscriptionData(baseMsg, arg)
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("收到Bitget WebSocket消息: %s", string(message))
|
||||
}
|
||||
|
||||
// handleAuthResponse 处理认证响应
|
||||
func (wm *BitgetWebSocketManager) handleAuthResponse(msg map[string]interface{}) {
|
||||
if code, exists := msg["code"]; exists {
|
||||
if code == "0" || code == 0 {
|
||||
log.Info("Bitget WebSocket认证成功 key:%s", wm.apiKey)
|
||||
// 认证成功后可以订阅私有频道
|
||||
wm.subscribePrivateChannels()
|
||||
} else {
|
||||
log.Error("Bitget WebSocket认证失败 key:%s code:%v", wm.apiKey, code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleSubscriptionData 处理订阅数据
|
||||
func (wm *BitgetWebSocketManager) handleSubscriptionData(msg map[string]interface{}, arg interface{}) {
|
||||
// 根据频道类型处理不同的数据
|
||||
// 这里可以根据具体需求实现不同频道的数据处理逻辑
|
||||
log.Info("收到Bitget订阅数据: %v", arg)
|
||||
}
|
||||
|
||||
// subscribePrivateChannels 订阅私有频道
|
||||
func (wm *BitgetWebSocketManager) subscribePrivateChannels() {
|
||||
// 订阅账户更新
|
||||
wm.subscribeAccount()
|
||||
// 订阅订单更新
|
||||
wm.subscribeOrders()
|
||||
// 订阅持仓更新
|
||||
wm.subscribePositions()
|
||||
}
|
||||
|
||||
// subscribeAccount 订阅账户更新
|
||||
func (wm *BitgetWebSocketManager) subscribeAccount() {
|
||||
var channel string
|
||||
if wm.wsType == 0 {
|
||||
channel = "account"
|
||||
} else {
|
||||
channel = "account"
|
||||
}
|
||||
|
||||
subMsg := map[string]interface{}{
|
||||
"op": "subscribe",
|
||||
"args": []map[string]string{
|
||||
{
|
||||
"instType": wm.getInstType(),
|
||||
"channel": channel,
|
||||
"instId": "default",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
wm.sendMessage(subMsg)
|
||||
}
|
||||
|
||||
// subscribeOrders 订阅订单更新
|
||||
func (wm *BitgetWebSocketManager) subscribeOrders() {
|
||||
var channel string
|
||||
if wm.wsType == 0 {
|
||||
channel = "orders"
|
||||
} else {
|
||||
channel = "orders"
|
||||
}
|
||||
|
||||
subMsg := map[string]interface{}{
|
||||
"op": "subscribe",
|
||||
"args": []map[string]string{
|
||||
{
|
||||
"instType": wm.getInstType(),
|
||||
"channel": channel,
|
||||
"instId": "default",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
wm.sendMessage(subMsg)
|
||||
}
|
||||
|
||||
// subscribePositions 订阅持仓更新(仅合约)
|
||||
func (wm *BitgetWebSocketManager) subscribePositions() {
|
||||
if wm.wsType != 1 {
|
||||
return // 只有合约才有持仓
|
||||
}
|
||||
|
||||
subMsg := map[string]interface{}{
|
||||
"op": "subscribe",
|
||||
"args": []map[string]string{
|
||||
{
|
||||
"instType": "umcbl",
|
||||
"channel": "positions",
|
||||
"instId": "default",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
wm.sendMessage(subMsg)
|
||||
}
|
||||
|
||||
// getInstType 获取产品类型
|
||||
func (wm *BitgetWebSocketManager) getInstType() string {
|
||||
if wm.wsType == 0 {
|
||||
return "sp" // 现货
|
||||
}
|
||||
return "umcbl" // U本位合约
|
||||
}
|
||||
|
||||
// SendMessage 发送消息(公开方法)
|
||||
func (wm *BitgetWebSocketManager) SendMessage(msg interface{}) error {
|
||||
msgBytes, err := sonic.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wm.mu.Lock()
|
||||
defer wm.mu.Unlock()
|
||||
|
||||
if wm.ws == nil {
|
||||
return errors.New("WebSocket连接不可用")
|
||||
}
|
||||
|
||||
return wm.ws.WriteMessage(websocket.TextMessage, msgBytes)
|
||||
}
|
||||
|
||||
// sendMessage 发送消息(私有方法,保持向后兼容)
|
||||
func (wm *BitgetWebSocketManager) sendMessage(msg interface{}) error {
|
||||
return wm.SendMessage(msg)
|
||||
}
|
||||
|
||||
// handleReconnect 处理重连
|
||||
func (wm *BitgetWebSocketManager) handleReconnect(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-wm.reconnect:
|
||||
wm.reconnecting.Store(false)
|
||||
log.Info("开始重连Bitget WebSocket key:%s", wm.apiKey)
|
||||
|
||||
wm.mu.Lock()
|
||||
if wm.ws != nil {
|
||||
wm.ws.Close()
|
||||
wm.ws = nil
|
||||
}
|
||||
wm.mu.Unlock()
|
||||
|
||||
// 触发主循环重新连接
|
||||
select {
|
||||
case wm.stopChannel <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startPingLoop 启动心跳循环
|
||||
func (wm *BitgetWebSocketManager) startPingLoop(ctx context.Context) {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
wm.sendPing()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendPing 发送心跳
|
||||
func (wm *BitgetWebSocketManager) sendPing() {
|
||||
pingMsg := "ping"
|
||||
|
||||
wm.mu.Lock()
|
||||
defer wm.mu.Unlock()
|
||||
|
||||
if wm.ws == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := wm.ws.WriteMessage(websocket.TextMessage, []byte(pingMsg)); err != nil {
|
||||
log.Error("Bitget WebSocket发送ping失败: %v", err)
|
||||
wm.triggerReconnect(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop 停止WebSocket连接
|
||||
func (wm *BitgetWebSocketManager) Stop() {
|
||||
wm.mu.Lock()
|
||||
defer wm.mu.Unlock()
|
||||
|
||||
if wm.isStopped {
|
||||
return
|
||||
}
|
||||
|
||||
wm.isStopped = true
|
||||
|
||||
if wm.cancelFunc != nil {
|
||||
wm.cancelFunc()
|
||||
}
|
||||
|
||||
if wm.ws != nil {
|
||||
wm.ws.Close()
|
||||
wm.ws = nil
|
||||
}
|
||||
|
||||
// 从全局map中移除
|
||||
if wm.wsType == 0 {
|
||||
delete(BitgetSpotSockets, wm.apiKey)
|
||||
} else {
|
||||
delete(BitgetFutureSockets, wm.apiKey)
|
||||
}
|
||||
|
||||
log.Info("Bitget WebSocket已停止 key:%s", wm.apiKey)
|
||||
}
|
||||
|
||||
// getBitgetWsTypeName 获取WebSocket类型名称
|
||||
func getBitgetWsTypeName(wsType int) string {
|
||||
switch wsType {
|
||||
case 0:
|
||||
return "现货"
|
||||
case 1:
|
||||
return "合约"
|
||||
default:
|
||||
return "未知"
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数:获取或创建Bitget WebSocket管理器
|
||||
func GetOrCreateBitgetWebSocketManager(wsType int, apiKey, apiSecret, passphrase, proxyType, proxyAddress string) *BitgetWebSocketManager {
|
||||
var socketsMap map[string]*BitgetWebSocketManager
|
||||
|
||||
if wsType == 0 {
|
||||
socketsMap = BitgetSpotSockets
|
||||
} else {
|
||||
socketsMap = BitgetFutureSockets
|
||||
}
|
||||
|
||||
if manager, exists := socketsMap[apiKey]; exists {
|
||||
// 如果参数有变化,重启连接
|
||||
if manager.apiSecret != apiSecret || manager.passphrase != passphrase ||
|
||||
manager.proxyType != proxyType || manager.proxyAddress != proxyAddress {
|
||||
manager.Restart(apiKey, apiSecret, passphrase, proxyType, proxyAddress)
|
||||
}
|
||||
return manager
|
||||
}
|
||||
|
||||
// 创建新的管理器
|
||||
manager := NewBitgetWebSocketManager(wsType, apiKey, apiSecret, passphrase, proxyType, proxyAddress)
|
||||
socketsMap[apiKey] = manager
|
||||
|
||||
return manager
|
||||
}
|
||||
|
||||
// StopBitgetWebSocketManager 停止指定的WebSocket管理器
|
||||
func StopBitgetWebSocketManager(wsType int, apiKey string) {
|
||||
var socketsMap map[string]*BitgetWebSocketManager
|
||||
|
||||
if wsType == 0 {
|
||||
socketsMap = BitgetSpotSockets
|
||||
} else {
|
||||
socketsMap = BitgetFutureSockets
|
||||
}
|
||||
|
||||
if manager, exists := socketsMap[apiKey]; exists {
|
||||
manager.Stop()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user