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
|
|||
|
|
}
|