418 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			418 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| 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
 | ||
| }
 |