From 643eab3496374c0468176b37c410321f252b7ead Mon Sep 17 00:00:00 2001 From: hucan <951870319@qq.com> Date: Tue, 14 Oct 2025 19:58:59 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E6=9A=82=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/apis/line_pre_order.go | 11 +- app/admin/apis/line_symbol.go | 16 +- app/admin/fronted/line_user.go | 16 +- app/admin/models/line_api_user.go | 1 + app/admin/service/line_pre_order.go | 47 +- app/admin/service/line_symbol.go | 20 +- app/admin/service/line_user.go | 15 +- app/jobs/examples.go | 10 +- app/jobs/jobs.go | 4 +- cmd/binance/binance.go | 264 +++++++ cmd/bitget/bitget.go | 271 ++++++++ cmd/cobra.go | 14 +- common/const/rediskey/redis_key.go | 12 +- common/factory/exchange_factory.go | 107 +++ common/global/exchange.go | 1 + common/global/redis_prefix.go | 4 +- common/helper/bitgethttphelper.go | 255 +++++++ common/interfaces/exchange.go | 170 +++++ config/extend.go | 18 +- config/serverinit/binance_futures_init.go | 4 - config/serverinit/bitget_init.go | 144 ++++ config/serverinit/business_init.go | 55 +- go.mod | 20 +- models/coin.go | 1 + models/positiondto/cache.go | 25 + services/binanceservice/binancerest.go | 17 +- services/binanceservice/commonservice.go | 55 +- .../futures_judge_service_test.go | 3 +- services/binanceservice/futuresbinancerest.go | 33 +- .../binanceservice/futuresjudgeservice.go | 25 +- services/binanceservice/futuresrest.go | 21 +- services/binanceservice/models.go | 25 - services/binanceservice/orderservice.go | 71 -- .../reduce_order_strategy_service.go | 3 +- .../binanceservice/spot_judge_service_test.go | 3 +- services/binanceservice/spotjudgeservice.go | 44 +- services/binanceservice/spotreset.go | 19 +- services/bitgetservice/bitget_client.go | 447 ++++++++++++ services/bitgetservice/bitget_example.go | 229 ++++++ services/bitgetservice/bitget_judge.go | 326 +++++++++ .../bitgetservice/bitget_market_client.go | 430 ++++++++++++ .../bitgetservice/bitget_market_future.go | 61 ++ .../bitgetservice/bitget_market_models.go | 126 ++++ services/bitgetservice/bitget_market_spot.go | 61 ++ services/bitgetservice/bitget_market_test.go | 54 ++ services/bitgetservice/bitget_rest.go | 417 +++++++++++ services/bitgetservice/bitget_rest_test.go | 41 ++ .../bitgetservice/bitget_socketmanager.go | 658 ++++++++++++++++++ services/cacheservice/config_service.go | 36 +- services/commonservice/commonservice.go | 15 + services/commonservice/judge.go | 63 ++ services/excservice/binancereceive.go | 36 +- services/excservice/binancesocketmanager.go | 576 ++++++++------- services/excservice/binancews.go | 6 + services/excservice/bitget_receive.go | 218 ++++++ services/orderservice/order_service.go | 108 +++ services/proxy/proxy.go | 63 ++ services/spotservice/binancemarket.go | 16 - services/tradsetservice/trade_set.go | 68 ++ test/init_settings.go | 22 + 60 files changed, 5244 insertions(+), 657 deletions(-) create mode 100644 cmd/binance/binance.go create mode 100644 cmd/bitget/bitget.go create mode 100644 common/factory/exchange_factory.go create mode 100644 common/helper/bitgethttphelper.go create mode 100644 common/interfaces/exchange.go create mode 100644 config/serverinit/bitget_init.go create mode 100644 services/bitgetservice/bitget_client.go create mode 100644 services/bitgetservice/bitget_example.go create mode 100644 services/bitgetservice/bitget_judge.go create mode 100644 services/bitgetservice/bitget_market_client.go create mode 100644 services/bitgetservice/bitget_market_future.go create mode 100644 services/bitgetservice/bitget_market_models.go create mode 100644 services/bitgetservice/bitget_market_spot.go create mode 100644 services/bitgetservice/bitget_market_test.go create mode 100644 services/bitgetservice/bitget_rest.go create mode 100644 services/bitgetservice/bitget_rest_test.go create mode 100644 services/bitgetservice/bitget_socketmanager.go create mode 100644 services/commonservice/commonservice.go create mode 100644 services/commonservice/judge.go create mode 100644 services/excservice/bitget_receive.go create mode 100644 services/orderservice/order_service.go create mode 100644 services/proxy/proxy.go create mode 100644 services/tradsetservice/trade_set.go create mode 100644 test/init_settings.go diff --git a/app/admin/apis/line_pre_order.go b/app/admin/apis/line_pre_order.go index 165f70c..cc74e4c 100644 --- a/app/admin/apis/line_pre_order.go +++ b/app/admin/apis/line_pre_order.go @@ -3,9 +3,7 @@ package apis import ( "errors" "fmt" - "go-admin/common/const/rediskey" "go-admin/common/global" - "go-admin/common/helper" "strings" "github.com/gin-gonic/gin" @@ -297,13 +295,8 @@ func (e LinePreOrder) AddPreOrder(c *gin.Context) { errs := make([]error, 0) errStr := make([]string, 0) var msg string - var tickerSymbol string - if req.SymbolType == global.SYMBOL_SPOT { - tickerSymbol = helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() - } else { - tickerSymbol = helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val() - } - if err := s.AddPreOrderCheck(&req, p, &errs, tickerSymbol); err != nil { + + if err := s.AddPreOrderCheck(&req, p, &errs); err != nil { e.Error(500, err, err.Error()) return } diff --git a/app/admin/apis/line_symbol.go b/app/admin/apis/line_symbol.go index f4ca283..e6ec1ba 100644 --- a/app/admin/apis/line_symbol.go +++ b/app/admin/apis/line_symbol.go @@ -3,13 +3,13 @@ package apis import ( "errors" "fmt" - "go-admin/common/const/rediskey" "go-admin/common/global" "go-admin/common/helper" "go-admin/config/serverinit" models2 "go-admin/models" "go-admin/pkg/utility" "go-admin/services/binanceservice" + "go-admin/services/cacheservice" "strings" "github.com/bytedance/sonic" @@ -289,9 +289,8 @@ func (e LineSymbol) SyncSpotSymbol(c *gin.Context) { Symbol string `json:"symbol"` Price string `json:"price"` } - tickerSymbol := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() - tickerSymbolMaps := make([]Ticker, 0) - sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps) + + tickerSymbolMaps, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 1) for symbol, tradeSet := range tradeSets { var lineSymbol models.LineSymbol @@ -320,11 +319,10 @@ func (e LineSymbol) SyncSpotSymbol(c *gin.Context) { continue } } else { - var tickerPrice decimal.Decimal for _, symbolMap := range tickerSymbolMaps { if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") { - tickerPrice, _ = decimal.NewFromString(symbolMap.Price) + tickerPrice, _ = decimal.NewFromString(symbolMap.LastPrice) } } if tickerPrice.GreaterThan(decimal.Zero) { @@ -412,10 +410,8 @@ func (e LineSymbol) SyncFutSymbol(c *gin.Context) { Symbol string `json:"symbol"` Price string `json:"price"` } - tickerSymbol := helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val() - tickerSymbolMaps := make([]Ticker, 0) - sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps) + tickerSymbolMaps, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 2) e.Orm.Model(&models.LineSymbolBlack{}).Where("type = 1").Find(&symbolBlack) for symbol, tradeSet := range tradeSets { var lineSymbol models.LineSymbol @@ -447,7 +443,7 @@ func (e LineSymbol) SyncFutSymbol(c *gin.Context) { var tickerPrice decimal.Decimal for _, symbolMap := range tickerSymbolMaps { if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") { - tickerPrice, _ = decimal.NewFromString(symbolMap.Price) + tickerPrice, _ = decimal.NewFromString(symbolMap.LastPrice) } } if tickerPrice.GreaterThan(decimal.Zero) { diff --git a/app/admin/fronted/line_user.go b/app/admin/fronted/line_user.go index 9176ceb..610015d 100644 --- a/app/admin/fronted/line_user.go +++ b/app/admin/fronted/line_user.go @@ -10,8 +10,7 @@ import ( "go-admin/app/admin/service/appservice" "go-admin/app/admin/service/common" "go-admin/app/admin/service/dto" - "go-admin/common/const/rediskey" - "go-admin/common/helper" + "go-admin/common/global" "go-admin/common/service/sysservice/authservice" "go-admin/common/service/sysservice/sysstatuscode" statuscode "go-admin/common/status_code" @@ -19,7 +18,7 @@ import ( "go-admin/models/coingatedto" "go-admin/pkg/cryptohelper/inttostring" "go-admin/pkg/utility" - "go-admin/services/binanceservice" + "go-admin/services/cacheservice" "go-admin/services/udunservice" "github.com/bytedance/sonic" @@ -373,20 +372,15 @@ func (e LineUserApi) Info(c *gin.Context) { e.Logger.Error(500, err, err.Error()) return } - //用户u的余额 - var tickerData []binanceservice.Ticker - val := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() - if val != "" { - sonic.Unmarshal([]byte(val), &tickerData) - } - // var usdtBalance decimal.Decimal + + tickerData, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 1) for i, asset := range resp { symbol := asset.Asset + "USDT" for _, datum := range tickerData { if datum.Symbol == symbol { // mul := utility.StringToDecimal(datum.Price).Mul(utility.StringToDecimal(asset.Free)) // usdtBalance = usdtBalance.Add(mul) - resp[i].UsdtValuation = datum.Price + resp[i].UsdtValuation = datum.LastPrice } } } diff --git a/app/admin/models/line_api_user.go b/app/admin/models/line_api_user.go index 2d29adf..3e6a673 100644 --- a/app/admin/models/line_api_user.go +++ b/app/admin/models/line_api_user.go @@ -13,6 +13,7 @@ type LineApiUser struct { ApiName string `json:"apiName" gorm:"type:varchar(255);comment:api用户名"` ApiKey string `json:"apiKey" gorm:"type:varchar(255);comment:apiKey"` ApiSecret string `json:"apiSecret" gorm:"type:varchar(255);comment:apiSecret"` + Passphrase string `json:"passphrase" gorm:"type:varchar(255);comment:api密码"` IpAddress string `json:"ipAddress" gorm:"type:varchar(255);comment:代理地址"` UserPass string `json:"userPass" gorm:"type:varchar(255);comment:代码账号密码"` AdminId int64 `json:"adminId" gorm:"type:int unsigned;comment:管理员id"` diff --git a/app/admin/service/line_pre_order.go b/app/admin/service/line_pre_order.go index f3a8cd0..2944848 100644 --- a/app/admin/service/line_pre_order.go +++ b/app/admin/service/line_pre_order.go @@ -264,8 +264,8 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi } positions := map[string]positiondto.LinePreOrderPositioinDelReq{} - addPosition := binanceservice.AddPositionList{} - reduceItem := binanceservice.ReduceListItem{} + addPosition := positiondto.AddPositionList{} + reduceItem := positiondto.ReduceListItem{} futAddPosition := map[int]string{} spotAddPosition := map[int]string{} futReduces := map[int]string{} @@ -395,7 +395,7 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi } // 单个新增订单 -func (e *LinePreOrder) AddPreOrderCheck(req *dto.LineAddPreOrderReq, p *actions.DataPermission, errs *[]error, tickerSymbol string) error { +func (e *LinePreOrder) AddPreOrderCheck(req *dto.LineAddPreOrderReq, p *actions.DataPermission, errs *[]error) error { apiUserService := LineApiUser{Service: e.Service} apiIds := []int{} apiUserIds := strings.Split(req.ApiUserId, ",") @@ -423,14 +423,14 @@ func (e *LinePreOrder) AddPreOrderCheck(req *dto.LineAddPreOrderReq, p *actions. *errs = append(*errs, fmt.Errorf("api用户未激活:%v", item)) } } - e.AddPreOrder(req, activeApiIds, p, errs, tickerSymbol) + e.AddPreOrder(req, activeApiIds, p, errs) } return nil } // AddPreOrder 单个添加 -func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int, p *actions.DataPermission, errs *[]error, tickerSymbol string) error { +func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int, p *actions.DataPermission, errs *[]error) error { if req.SaveTemplate == "2" || req.SaveTemplate == "1" { //2 = 只保存模板 1= 保存模板并下单 var templateLog dto.LineAddPreOrderReq copier.Copy(&templateLog, req) @@ -1250,15 +1250,7 @@ func (e *LinePreOrder) AddBatchPreOrder(batchReq *dto.LineBatchAddPreOrderReq, p batchReq.Symbol = symbolGroupInfo.Symbol } - //脚本次数 - // if batchReq.OrderNum > 0 { apiUserIds := strings.Split(batchReq.ApiUserId, ",") - var tickerSymbol string - if batchReq.SymbolType == global.SYMBOL_SPOT { - tickerSymbol = helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() - } else { - tickerSymbol = helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val() - } if batchReq.Script == "1" { //scriptLogs := make([]models.LinePreScript, 0) @@ -1317,15 +1309,11 @@ func (e *LinePreOrder) AddBatchPreOrder(batchReq *dto.LineBatchAddPreOrderReq, p req.ReduceTakeProfitRatio = batchReq.ReduceTakeProfitRatio req.CreateBy = batchReq.CreateBy - e.AddPreOrderCheck(&req, p, errs, tickerSymbol) + e.AddPreOrderCheck(&req, p, errs) } } } return nil - // } else { - // *errs = append(*errs, errors.New("请选择运行次数")) - // return nil - // } } // QuickAddPreOrder 模板快速下单 @@ -1338,13 +1326,7 @@ func (e *LinePreOrder) QuickAddPreOrder(quickReq *dto.QuickAddPreOrderReq, p *ac var addPreOrderParams dto.LineAddPreOrderReq sonic.Unmarshal([]byte(log.Params), &addPreOrderParams) - var tickerSymbol string - if addPreOrderParams.OrderType == global.SYMBOL_SPOT { - tickerSymbol = helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() - } else { - tickerSymbol = helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val() - } - err := e.AddPreOrderCheck(&addPreOrderParams, p, errs, tickerSymbol) + err := e.AddPreOrderCheck(&addPreOrderParams, p, errs) // err := e.AddPreOrder(&addPreOrderParams, p, errs, tickerSymbol) if err != nil { *errs = append(*errs, fmt.Errorf("api_id:%s 获取交易对:%s 生成订单失败", addPreOrderParams.ApiUserId, addPreOrderParams.Symbol)) @@ -1696,23 +1678,18 @@ func (e *LinePreOrder) CalculateAmount(req dto.ManuallyCover, totalNum, lastPric } else { decimalValue := utility.StringToDecimal(req.Value) if notUsdt { - tickerSymbolMaps := make([]dto.Ticker, 0) - tickerSymbol := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() - if err := sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps); err != nil { - return decimal.Zero, err - } - + tickerSymbolMaps, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 2) var tickerPrice decimal.Decimal for _, symbolMap := range tickerSymbolMaps { if symbolMap.Symbol == strings.ToUpper(symbolInfo.BaseAsset+"USDT") { - tickerPrice, _ = decimal.NewFromString(symbolMap.Price) + tickerPrice, _ = decimal.NewFromString(symbolMap.LastPrice) break } } for _, symbolMap := range tickerSymbolMaps { if symbolMap.Symbol == strings.ToUpper(symbolInfo.QuoteAsset+"USDT") { - uTickerPrice, _ := decimal.NewFromString(symbolMap.Price) + uTickerPrice, _ := decimal.NewFromString(symbolMap.LastPrice) div := utility.SafeDiv(tickerPrice, decimal.NewFromInt(1).Div(uTickerPrice)) amt = utility.SafeDiv(decimalValue, div) break @@ -2108,9 +2085,9 @@ func (e *LinePreOrder) GenerateOrder(req *dto.LineAddPreOrderReq) error { var tickerPrice decimal.Decimal if req.SymbolType == 1 { - tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, req.Symbol, 0) - } else { tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, req.Symbol, 1) + } else { + tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, req.Symbol, 2) } if tradeSet.LastPrice == "" { diff --git a/app/admin/service/line_symbol.go b/app/admin/service/line_symbol.go index 461f3db..3620408 100644 --- a/app/admin/service/line_symbol.go +++ b/app/admin/service/line_symbol.go @@ -15,7 +15,6 @@ import ( "go-admin/app/admin/models" "go-admin/app/admin/service/dto" "go-admin/common/actions" - "go-admin/common/const/rediskey" cDto "go-admin/common/dto" "go-admin/common/global" "go-admin/common/helper" @@ -23,6 +22,7 @@ import ( models2 "go-admin/models" "go-admin/pkg/utility" "go-admin/services/binanceservice" + "go-admin/services/cacheservice" ) type LineSymbol struct { @@ -354,9 +354,10 @@ func (e *LineSymbol) ResetSpotSymbol() error { Symbol string `json:"symbol"` Price string `json:"price"` } - tickerSymbol := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() - tickerSymbolMaps := make([]Ticker, 0) - sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps) + // tickerSymbol := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() + // tickerSymbolMaps := make([]Ticker, 0) + // sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps) + tickerSymbolMaps, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 1) oldMapSymbols, err := getMapSymbol(e, "1") if err != nil { return err @@ -389,7 +390,7 @@ func (e *LineSymbol) ResetSpotSymbol() error { var tickerPrice decimal.Decimal for _, symbolMap := range tickerSymbolMaps { if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") { - tickerPrice, _ = decimal.NewFromString(symbolMap.Price) + tickerPrice, _ = decimal.NewFromString(symbolMap.LastPrice) } } if tickerPrice.GreaterThan(decimal.Zero) { @@ -527,9 +528,10 @@ func (e *LineSymbol) ResetFuturesSymbol() error { Symbol string `json:"symbol"` Price string `json:"price"` } - tickerSymbol := helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val() - tickerSymbolMaps := make([]Ticker, 0) - sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps) + // tickerSymbol := helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val() + // tickerSymbolMaps := make([]Ticker, 0) + // sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps) + tickerSymbolMaps, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 2) oldMapSymbols, err := getMapSymbol(e, "2") if err != nil { return err @@ -560,7 +562,7 @@ func (e *LineSymbol) ResetFuturesSymbol() error { var tickerPrice decimal.Decimal for _, symbolMap := range tickerSymbolMaps { if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") { - tickerPrice, _ = decimal.NewFromString(symbolMap.Price) + tickerPrice, _ = decimal.NewFromString(symbolMap.LastPrice) } } if tickerPrice.GreaterThan(decimal.Zero) { diff --git a/app/admin/service/line_user.go b/app/admin/service/line_user.go index 9d56539..ede696f 100644 --- a/app/admin/service/line_user.go +++ b/app/admin/service/line_user.go @@ -18,6 +18,7 @@ import ( "go-admin/pkg/utility" "go-admin/pkg/utility/seqs" "go-admin/services/binanceservice" + "go-admin/services/cacheservice" "strconv" "strings" "time" @@ -323,11 +324,13 @@ func (e *LineUser) RechargeNetworkList(req *dto.RechargeListReq) (data []models. } coin := req.Coin resp := make([]models.VtsCoinToNetWorkResp, 0) - var tickerData []binanceservice.Ticker - val := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() - if val != "" { - sonic.Unmarshal([]byte(val), &tickerData) - } + // var tickerData []binanceservice.Ticker + // val := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() + // if val != "" { + // sonic.Unmarshal([]byte(val), &tickerData) + // } + + tickerData, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 1) for _, v1 := range list { if strings.EqualFold(coin, "USDT") { @@ -357,7 +360,7 @@ func (e *LineUser) RechargeNetworkList(req *dto.RechargeListReq) (data []models. TransferFee: utility.FloatCutStr(v1.TransferFee, 8), //TransferFeeUsdt string `json:"transfer_fee_usdt"` } - res.TransferFeeUsdt = utility.FloatCutStr(v1.TransferFee*utility.StringToFloat64(v2.Price), 8) + res.TransferFeeUsdt = utility.FloatCutStr(v1.TransferFee*utility.StringToFloat64(v2.LastPrice), 8) resp = append(resp, res) } } diff --git a/app/jobs/examples.go b/app/jobs/examples.go index a5a9406..b403ce5 100644 --- a/app/jobs/examples.go +++ b/app/jobs/examples.go @@ -9,8 +9,8 @@ import ( "go-admin/common/const/rediskey" "go-admin/common/global" "go-admin/common/helper" + "go-admin/models/positiondto" "go-admin/pkg/utility" - "go-admin/services/binanceservice" "time" "github.com/bytedance/sonic" @@ -128,10 +128,10 @@ func removeExpirateChildOrder(childOrders []models.LinePreOrder, db *gorm.DB) { spotAdPositionStr, _ := helper.DefaultRedis.GetAllList(spotAddKey) futReduceStr, _ := helper.DefaultRedis.GetAllList(futReduceKey) spotReduceStr, _ := helper.DefaultRedis.GetAllList(spotReduceKey) - futAddPositionCache := binanceservice.AddPositionList{} - spotAddPositionCache := binanceservice.AddPositionList{} - futReduceCache := binanceservice.ReduceListItem{} - spotReduceCache := binanceservice.ReduceListItem{} + futAddPositionCache := positiondto.AddPositionList{} + spotAddPositionCache := positiondto.AddPositionList{} + futReduceCache := positiondto.ReduceListItem{} + spotReduceCache := positiondto.ReduceListItem{} futAddPositionMap := map[int]string{} spotAddPositionMap := map[int]string{} futReduceMap := map[int]string{} diff --git a/app/jobs/jobs.go b/app/jobs/jobs.go index 9b2c257..a70ef95 100644 --- a/app/jobs/jobs.go +++ b/app/jobs/jobs.go @@ -242,7 +242,7 @@ func (t LimitOrderTimeoutDuration) ReSpotOrderPlace(db *gorm.DB, order models.Li } else { var remainingQuantity decimal.Decimal spotOrder, err := spotApi.GetOrderByOrderSnLoop(order.Symbol, order.OrderSn, apiUserinfo, 4) - tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 0) + tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1) if err == nil { origQty := utility.StrToDecimal(spotOrder.OrigQty) @@ -360,7 +360,7 @@ func (t LimitOrderTimeoutDuration) ReFutOrderPlace(db *gorm.DB, order models.Lin } else { var remainingQuantity decimal.Decimal spotOrder, err := futApi.GetOrderByOrderSnLoop(order.Symbol, order.OrderSn, apiUserinfo, 4) - tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1) + tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 2) if err == nil { origQty := utility.StrToDecimal(spotOrder.OrigQty) diff --git a/cmd/binance/binance.go b/cmd/binance/binance.go new file mode 100644 index 0000000..102e0f3 --- /dev/null +++ b/cmd/binance/binance.go @@ -0,0 +1,264 @@ +package binance + +import ( + "context" + "fmt" + "go-admin/app/admin/models" + "go-admin/app/admin/service" + "go-admin/common/const/rediskey" + "go-admin/common/database" + "go-admin/common/global" + "go-admin/common/helper" + "go-admin/common/storage" + "go-admin/config/serverinit" + "go-admin/pkg/httputils" + "go-admin/pkg/utility" + "go-admin/services/excservice" + "go-admin/services/fileservice" + "os" + "os/signal" + "strconv" + "time" + + ext "go-admin/config" + + "github.com/bytedance/sonic" + "github.com/go-admin-team/go-admin-core/config/source/file" + "github.com/go-admin-team/go-admin-core/logger" + "github.com/go-admin-team/go-admin-core/sdk" + "github.com/go-admin-team/go-admin-core/sdk/config" + "github.com/go-admin-team/go-admin-core/sdk/pkg" + "github.com/spf13/cobra" + "gorm.io/gorm" +) + +var ( + configYml string + StartCmd = &cobra.Command{ + Use: "binance", + Short: "Start Binance service (market data + user private)", + Example: "go-admin binance -c config/settings.yml", + SilenceUsage: true, + PreRun: func(cmd *cobra.Command, args []string) { + setup() + }, + RunE: func(cmd *cobra.Command, args []string) error { + return run() + }, + } +) + +func init() { + StartCmd.PersistentFlags().StringVarP(&configYml, "config", "c", "config/settings.yml", "Start server with provided configuration file") +} + +func setup() { + // 注入配置扩展项 + config.ExtendConfig = &ext.ExtConfig + //1. 读取配置 + config.Setup( + file.NewSource(file.WithPath(configYml)), + database.Setup, + storage.Setup, + ) + //注册监听函数 + queue := sdk.Runtime.GetMemoryQueue("") + queue.Register(global.LoginLog, models.SaveLoginLog) + queue.Register(global.OperateLog, models.SaveOperaLog) + go queue.Run() + + usageStr := `starting Binance service (market data + user private)...` + logger.Info(usageStr) +} + +func run() error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var db *gorm.DB + dbs := sdk.Runtime.GetDb() + + for _, item := range dbs { + db = item + break + } + + defaultInit(db, ctx) + + utility.SafeGo(func() { + // 启动定时任务 + clearLogJob(db, ctx) + }) + + //自动重启websocket + // utility.SafeGo(func() { + // reconnect(ctx) + // }) + + //动态用户连接管理 + utility.SafeGo(func() { + manageBinanceUserConnections(ctx) + }) + + // 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间) + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt) + <-quit + + fmt.Printf("%s Shutdown Server ... \r\n", pkg.GetCurrentTimeStr()) + logger.Infof("Server exiting") + + return nil +} + +func defaultInit(db *gorm.DB, ctx context.Context) { + httputils.InitProxy(ext.ExtConfig.ProxyUrl) + //初始化 默认redis + helper.InitDefaultRedis(ext.ExtConfig.Redis.Addr, ext.ExtConfig.Redis.Password, ext.ExtConfig.Redis.Db) + helper.InitLockRedisConn(ext.ExtConfig.Redis.Addr, ext.ExtConfig.Redis.Password, strconv.Itoa(ext.ExtConfig.Redis.Db)) + + err := helper.DefaultRedis.Ping() + + if err != nil { + logger.Info("初始化redis失败!请检查配置") + _, cancel := context.WithTimeout(context.Background(), 5*time.Second) + cancel() + } else { + logger.Info("redis初始化成功") + } + + //初始化Binance服务(行情 + 用户私有) + logger.Info("=== 启动Binance完整服务 ===") + + // 启动Binance行情服务 + logger.Info("启动Binance行情服务...") + if err := serverinit.BinanceMarketInit(); err != nil { + logger.Infof("Binance行情服务启动失败: %v", err) + } + + // 初始化Binance用户连接 + logger.Info("启动Binance用户服务...") + initBinanceUserConnections(db, ctx) + logger.Info("=== Binance完整服务已启动 ===") +} + +// 初始化Binance用户连接 +func initBinanceUserConnections(db *gorm.DB, ctx context.Context) { + var list []models.LineApiUser + err := db.Model(&models.LineApiUser{}).Where("open_status=1 AND exchange_type =?", global.EXCHANGE_BINANCE).Find(&list).Error + + if err != nil { + logger.Info("获取Binance用户api失败: %v", err) + return + } + + logger.Infof("找到 %d 个Binance用户,开始建立连接...", len(list)) + for _, item := range list { + service.OpenUserBinanceWebsocket(item) + } +} + +// 动态Binance用户连接管理 +func manageBinanceUserConnections(ctx context.Context) { + ticker := time.NewTicker(time.Second * 5) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + logger.Info("Binance用户分组名称发送") + return + case <-ticker.C: + deleteKeys, _ := helper.DefaultRedis.GetAllList(rediskey.ApiUserDeleteList) + activeApis, _ := helper.DefaultRedis.GetAllList(rediskey.ApiUserActiveList) + + //移除Binance连接 + for _, item := range deleteKeys { + logger.Infof("移除Binance用户连接: %s", item) + + if wm, ok := excservice.SpotSockets[item]; ok { + wm.Stop() + delete(excservice.SpotSockets, item) + } + if wm, ok := excservice.FutureSockets[item]; ok { + wm.Stop() + delete(excservice.FutureSockets, item) + } + + if _, err := helper.DefaultRedis.LRem(rediskey.ApiUserDeleteList, item); err != nil { + logger.Errorf("移除待关闭websocket失败: %v", err) + } + } + + //连接Binance用户websocket + var apiUser models.LineApiUser + for _, item := range activeApis { + if item == "" { + continue + } + + if err := sonic.Unmarshal([]byte(item), &apiUser); err != nil { + logger.Errorf("解析用户信息失败: %v", err) + continue + } + + // 只处理Binance用户 + if apiUser.Id > 0 && apiUser.ExchangeType == global.EXCHANGE_BINANCE { + logger.Infof("为Binance用户 %d 建立连接", apiUser.Id) + service.OpenUserBinanceWebsocket(apiUser) + + if _, err := helper.DefaultRedis.LRem(rediskey.ApiUserActiveList, item); err != nil { + logger.Errorf("删除待触发的apiUser失败: %v", err) + } + } + } + } + } +} + +// 定时清理日志 +func clearLogJob(db *gorm.DB, ctx context.Context) { + ticker := time.NewTicker(time.Hour * 1) + defer ticker.Stop() + + select { + case <-ctx.Done(): + return + case <-ticker.C: + fileservice.ClearLogs(db) + } +} + +// 定时重连websocket +func reconnect(ctx context.Context) error { + ticker := time.NewTicker(time.Hour * 1) + defer ticker.Stop() + + select { + case <-ctx.Done(): + return nil + case <-ticker.C: + // 重启Binance连接 + spotSockets := excservice.SpotSockets + futuresSockets := excservice.FutureSockets + timeOut := 22 * time.Hour + + for _, item := range spotSockets { + if time.Since(item.ConnectTime) > timeOut { + if err := item.ReplaceConnection(); err != nil { + logger.Errorf("Binance现货重启连接失败 key:%s,error:%s", item.GetKey(), err) + } + } + } + + for _, item := range futuresSockets { + if time.Since(item.ConnectTime) > timeOut { + if err := item.ReplaceConnection(); err != nil { + logger.Errorf("Binance合约重启连接失败 key:%s,error:%s", item.GetKey(), err) + } + } + } + } + + return nil +} diff --git a/cmd/bitget/bitget.go b/cmd/bitget/bitget.go new file mode 100644 index 0000000..980be3e --- /dev/null +++ b/cmd/bitget/bitget.go @@ -0,0 +1,271 @@ +package bitget + +import ( + "context" + "fmt" + "go-admin/app/admin/models" + "go-admin/common/const/rediskey" + "go-admin/common/database" + "go-admin/common/global" + "go-admin/common/helper" + "go-admin/common/storage" + "go-admin/config/serverinit" + "go-admin/pkg/utility" + "go-admin/services/bitgetservice" + "go-admin/services/fileservice" + "os" + "os/signal" + "strconv" + "time" + + ext "go-admin/config" + + "github.com/bytedance/sonic" + "github.com/go-admin-team/go-admin-core/config/source/file" + "github.com/go-admin-team/go-admin-core/logger" + "github.com/go-admin-team/go-admin-core/sdk" + "github.com/go-admin-team/go-admin-core/sdk/config" + "github.com/go-admin-team/go-admin-core/sdk/pkg" + "github.com/spf13/cobra" + "gorm.io/gorm" +) + +var ( + configYml string + StartCmd = &cobra.Command{ + Use: "bitget", + Short: "Start Bitget service (market data + user private)", + Example: "go-admin bitget -c config/settings.yml", + SilenceUsage: true, + PreRun: func(cmd *cobra.Command, args []string) { + setup() + }, + RunE: func(cmd *cobra.Command, args []string) error { + return run() + }, + } +) + +func init() { + StartCmd.PersistentFlags().StringVarP(&configYml, "config", "c", "config/settings.yml", "Start server with provided configuration file") +} + +func setup() { + // 注入配置扩展项 + config.ExtendConfig = &ext.ExtConfig + //1. 读取配置 + config.Setup( + file.NewSource(file.WithPath(configYml)), + database.Setup, + storage.Setup, + ) + //注册监听函数 + queue := sdk.Runtime.GetMemoryQueue("") + queue.Register(global.LoginLog, models.SaveLoginLog) + queue.Register(global.OperateLog, models.SaveOperaLog) + go queue.Run() + + usageStr := `starting Bitget service (market data + user private)...` + logger.Info(usageStr) +} + +func run() error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var db *gorm.DB + dbs := sdk.Runtime.GetDb() + + for _, item := range dbs { + db = item + break + } + + defaultInit(db, ctx) + + utility.SafeGo(func() { + // 启动定时任务 + clearLogJob(db, ctx) + }) + + //自动重启websocket + utility.SafeGo(func() { + reconnect(ctx) + }) + + //动态用户连接管理 + utility.SafeGo(func() { + manageBitgetUserConnections(ctx) + }) + + // 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间) + quit := make(chan os.Signal, 1) + signal.Notify(quit, os.Interrupt) + <-quit + + fmt.Printf("%s Shutdown Server ... \r\n", pkg.GetCurrentTimeStr()) + logger.Info("Server exiting") + + return nil +} + +func defaultInit(db *gorm.DB, ctx context.Context) { + //初始化 默认redis + helper.InitDefaultRedis(ext.ExtConfig.Redis.Addr, ext.ExtConfig.Redis.Password, ext.ExtConfig.Redis.Db) + helper.InitLockRedisConn(ext.ExtConfig.Redis.Addr, ext.ExtConfig.Redis.Password, strconv.Itoa(ext.ExtConfig.Redis.Db)) + + err := helper.DefaultRedis.Ping() + + if err != nil { + logger.Error("初始化redis失败!请检查配置") + _, cancel := context.WithTimeout(context.Background(), 5*time.Second) + cancel() + } else { + logger.Info("redis初始化成功") + } + + //初始化Bitget服务(行情 + 用户私有) + logger.Info("=== 启动Bitget完整服务 ===") + + // 启动Bitget行情服务 + logger.Info("启动Bitget行情服务...") + if err := serverinit.BitgetMarketInit(); err != nil { + logger.Errorf("Bitget行情服务启动失败: %v", err) + } + + // 初始化Bitget用户连接 + logger.Info("启动Bitget用户服务...") + initBitgetUserConnections(db, ctx) + + logger.Info("=== Bitget完整服务已启动 ===") +} + +// 初始化Bitget用户连接 +func initBitgetUserConnections(db *gorm.DB, ctx context.Context) { + var list []models.LineApiUser + err := db.Model(&models.LineApiUser{}).Where("open_status=1 AND exchange_type =?", global.EXCHANGE_BITGET).Find(&list).Error + + if err != nil { + logger.Infof("获取Bitget用户api失败: %v", err) + return + } + + logger.Infof("找到 %d 个Bitget用户,开始建立连接...", len(list)) + for _, item := range list { + // 遵循私有WebSocket密钥传递规范:外部传入API密钥参数 + createBitgetUserConnection(item) + } +} + +// 创建Bitget用户连接(遵循密钥传递规范) +func createBitgetUserConnection(apiUser models.LineApiUser) { + logger.Infof("为Bitget用户 %d 建立私有连接", apiUser.Id) + + // 现货WebSocket (wsType=0) + spotManager := bitgetservice.GetOrCreateBitgetWebSocketManager( + 0, // 现货 + apiUser.ApiKey, + apiUser.ApiSecret, + apiUser.Passphrase, + "", // proxyType - 从配置获取 + "", // proxyAddress - 从配置获取 + ) + spotManager.Start() + + // 合约WebSocket (wsType=1) + futuresManager := bitgetservice.GetOrCreateBitgetWebSocketManager( + 1, // 合约 + apiUser.ApiKey, + apiUser.ApiSecret, + apiUser.Passphrase, + "", // proxyType + "", // proxyAddress + ) + futuresManager.Start() + + logger.Infof("Bitget用户 %d 私有连接已建立", apiUser.Id) +} + +// 动态Bitget用户连接管理 +func manageBitgetUserConnections(ctx context.Context) { + ticker := time.NewTicker(time.Second * 5) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + logger.Info("Bitget用户连接管理器退出") + return + case <-ticker.C: + deleteKeys, _ := helper.DefaultRedis.GetAllList(rediskey.ApiUserDeleteList) + activeApis, _ := helper.DefaultRedis.GetAllList(rediskey.ApiUserActiveList) + + //移除Bitget连接 + for _, item := range deleteKeys { + logger.Infof("移除Bitget用户连接: %s", item) + + // 停止现货WebSocket + bitgetservice.StopBitgetWebSocketManager(0, item) + // 停止合约WebSocket + bitgetservice.StopBitgetWebSocketManager(1, item) + + if _, err := helper.DefaultRedis.LRem(rediskey.ApiUserDeleteList, item); err != nil { + logger.Errorf("移除待关闭websocket失败: %v", err) + } + } + + //连接Bitget用户websocket + var apiUser models.LineApiUser + for _, item := range activeApis { + if item == "" { + continue + } + + if err := sonic.Unmarshal([]byte(item), &apiUser); err != nil { + logger.Errorf("解析用户信息失败: %v", err) + continue + } + + // 只处理Bitget用户 + if apiUser.Id > 0 && apiUser.ExchangeType == global.EXCHANGE_BITGET { + logger.Info("为Bitget用户 %d 建立连接", apiUser.Id) + createBitgetUserConnection(apiUser) + + if _, err := helper.DefaultRedis.LRem(rediskey.ApiUserActiveList, item); err != nil { + logger.Errorf("删除待触发的apiUser失败: %v", err) + } + } + } + } + } +} + +// 定时清理日志 +func clearLogJob(db *gorm.DB, ctx context.Context) { + ticker := time.NewTicker(time.Hour * 1) + defer ticker.Stop() + + select { + case <-ctx.Done(): + return + case <-ticker.C: + fileservice.ClearLogs(db) + } +} + +// 定时重连websocket +func reconnect(ctx context.Context) error { + ticker := time.NewTicker(time.Hour * 1) + defer ticker.Stop() + + select { + case <-ctx.Done(): + return nil + case <-ticker.C: + // 这里可以添加Bitget连接的重连逻辑 + // 类似于Binance的重连机制 + logger.Info("检查Bitget连接状态...") + } + + return nil +} diff --git a/cmd/cobra.go b/cmd/cobra.go index 6ac1c03..aaf47e2 100644 --- a/cmd/cobra.go +++ b/cmd/cobra.go @@ -3,19 +3,19 @@ package cmd import ( "errors" "fmt" + "go-admin/cmd/api" "go-admin/cmd/app" + "go-admin/cmd/binance" + "go-admin/cmd/bitget" + "go-admin/cmd/config" + "go-admin/cmd/migrate" "go-admin/cmd/usersubscribe" + "go-admin/cmd/version" "go-admin/common/global" "os" "github.com/go-admin-team/go-admin-core/sdk/pkg" - "github.com/spf13/cobra" - - "go-admin/cmd/api" - "go-admin/cmd/config" - "go-admin/cmd/migrate" - "go-admin/cmd/version" ) var rootCmd = &cobra.Command{ @@ -46,6 +46,8 @@ func tip() { func init() { rootCmd.AddCommand(api.StartCmd) rootCmd.AddCommand(usersubscribe.StartCmd) + rootCmd.AddCommand(binance.StartCmd) + rootCmd.AddCommand(bitget.StartCmd) rootCmd.AddCommand(migrate.StartCmd) rootCmd.AddCommand(version.StartCmd) rootCmd.AddCommand(config.StartCmd) diff --git a/common/const/rediskey/redis_key.go b/common/const/rediskey/redis_key.go index 0290e1e..2fd94b9 100644 --- a/common/const/rediskey/redis_key.go +++ b/common/const/rediskey/redis_key.go @@ -19,11 +19,13 @@ const ( PCRegisterMobile = "_PCRegisterMobile_%v" // 用户注册时手机key PCResetPwdMobile = "_PCResetPwdMobile_%v" // 用户重置密码时手机key - SpotSymbolTicker = "_SpotSymbolTicker_" // 现货交易对行情 - FutSymbolTicker = "_FutSymbolTicker_" // 合约交易对行情 - PreOrderScriptList = "_ProOrderScriptList_" // 脚本执行list - PreSpotOrderList = "_PreSpotOrderList_:%s" // 待触发的现货订单集合{交易所类型 exchange_type} - PreFutOrderList = "_PreFutOrderList_:%s" // 待触发的订单集合 {交易所类型 exchange_type} + // SpotSymbolTicker = "_SpotSymbolTicker_" // 现货交易对行情 + // FutSymbolTicker = "_FutSymbolTicker_" // 合约交易对行情 + PreOrderScriptList = "_ProOrderScriptList_" // 脚本执行list + // 待触发的现货订单集合{交易所类型 exchange_type} + PreSpotOrderList = "_PreSpotOrderList_:%s" + // 待触发的订单集合 {交易所类型 exchange_type} + PreFutOrderList = "_PreFutOrderList_:%s" //策略现货订单集合 {交易所类型 exchange_type} StrategySpotOrderList = "strategy_spot_order_list:%s" diff --git a/common/factory/exchange_factory.go b/common/factory/exchange_factory.go new file mode 100644 index 0000000..c92336d --- /dev/null +++ b/common/factory/exchange_factory.go @@ -0,0 +1,107 @@ +package factory + +import ( + "fmt" + "go-admin/common/global" + "go-admin/common/interfaces" + "strings" +) + +// ExchangeFactory 交易所工厂 +type ExchangeFactory struct { + clients map[string]interfaces.ExchangeInterface +} + +// NewExchangeFactory 创建交易所工厂实例 +func NewExchangeFactory() *ExchangeFactory { + return &ExchangeFactory{ + clients: make(map[string]interfaces.ExchangeInterface), + } +} + +// GetExchange 获取指定交易所的客户端 +func (f *ExchangeFactory) GetExchange(exchangeType string) (interfaces.ExchangeInterface, error) { + exchangeType = strings.ToLower(strings.TrimSpace(exchangeType)) + + // 检查是否已经创建了该交易所的客户端 + if client, exists := f.clients[exchangeType]; exists { + return client, nil + } + + // 根据交易所类型创建对应的客户端 + var client interfaces.ExchangeInterface + var err error + + switch exchangeType { + case global.EXCHANGE_BINANCE: + client, err = f.createBinanceClient() + case global.EXCHANGE_BITGET: + client, err = f.createBitgetClient() + case global.EXCHANGE_OKEX: + return nil, fmt.Errorf("OKEx integration not implemented yet") + case global.EXCHANGE_GATE: + return nil, fmt.Errorf("Gate.io integration not implemented yet") + case global.EXCHANGE_COINBASE: + return nil, fmt.Errorf("Coinbase integration not implemented yet") + case global.EXCHANGE_BITFINEX: + return nil, fmt.Errorf("Bitfinex integration not implemented yet") + case global.EXCHANGE_BITMEX: + return nil, fmt.Errorf("BitMEX integration not implemented yet") + default: + return nil, fmt.Errorf("unsupported exchange type: %s", exchangeType) + } + + if err != nil { + return nil, fmt.Errorf("failed to create %s client: %v", exchangeType, err) + } + + // 缓存客户端实例 + f.clients[exchangeType] = client + + return client, nil +} + +// createBinanceClient 创建Binance客户端包装器 +func (f *ExchangeFactory) createBinanceClient() (interfaces.ExchangeInterface, error) { + // Binance适配器尚未完全实现,暴时返回错误 + return nil, fmt.Errorf("Binance adapter not fully implemented yet") +} + +// createBitgetClient 创建Bitget客户端 +func (f *ExchangeFactory) createBitgetClient() (interfaces.ExchangeInterface, error) { + // 使用默认空参数创建Bitget客户端(公开接口不需要认证) + + return nil, fmt.Errorf("Bitget adapter not fully implemented yet") +} + +// GetSupportedExchanges 获取支持的交易所列表 +func (f *ExchangeFactory) GetSupportedExchanges() []string { + return []string{ + // global.EXCHANGE_BINANCE, // Binance适配器尚未完全实现 + global.EXCHANGE_BITGET, + // 其他交易所可以在未来添加 + } +} + +// RegisterExchange 注册自定义交易所客户端 +func (f *ExchangeFactory) RegisterExchange(exchangeType string, client interfaces.ExchangeInterface) { + f.clients[strings.ToLower(exchangeType)] = client +} + +// ClearCache 清理缓存的客户端 +func (f *ExchangeFactory) ClearCache() { + for _, client := range f.clients { + if closer, ok := client.(interface{ Close() error }); ok { + closer.Close() + } + } + f.clients = make(map[string]interfaces.ExchangeInterface) +} + +// 全局工厂实例 +var DefaultExchangeFactory = NewExchangeFactory() + +// GetExchangeClient 获取交易所客户端的便捷函数 +func GetExchangeClient(exchangeType string) (interfaces.ExchangeInterface, error) { + return DefaultExchangeFactory.GetExchange(exchangeType) +} diff --git a/common/global/exchange.go b/common/global/exchange.go index 4436758..c6642f0 100644 --- a/common/global/exchange.go +++ b/common/global/exchange.go @@ -3,6 +3,7 @@ package global //交易所类型字典 const ( EXCHANGE_BINANCE = "binance" + EXCHANGE_BITGET = "bitget" EXCHANGE_OKEX = "okex" EXCHANGE_GATE = "gate" EXCHANGE_COINBASE = "coinbase" diff --git a/common/global/redis_prefix.go b/common/global/redis_prefix.go index 3181f6a..20e2f68 100644 --- a/common/global/redis_prefix.go +++ b/common/global/redis_prefix.go @@ -17,8 +17,8 @@ const ( K_SPOT = "k_spot" //代币-配置 COIN_DETAIL = "c_spot_detail" - //代币-现货24h涨跌幅 - COIN_PRICE_CHANGE = "c_spot_priceChange" + //代币-现货24h涨跌幅 {exchange} + COIN_PRICE_CHANGE = "c_spot_priceChange:%s" //代币-现货热门排序 COIN_HOT_SORT = "c_spot_hot_sort" //代币-现货新币排序 diff --git a/common/helper/bitgethttphelper.go b/common/helper/bitgethttphelper.go new file mode 100644 index 0000000..945d42d --- /dev/null +++ b/common/helper/bitgethttphelper.go @@ -0,0 +1,255 @@ +package helper + +import ( + "crypto" + "crypto/hmac" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "io/ioutil" + "net/http" + "sort" + "strconv" + "strings" + "time" +) + +// BitgetClient Bitget HTTP客户端 +type BitgetClient struct { + APIKey string + APISecret string + Passphrase string + HTTPClient *http.Client + BaseURL string + SignType string +} + +// NewBitgetClient 创建新的Bitget客户端 +func NewBitgetClient(apiKey, apiSecret, passphrase string, proxyUrl string) (*BitgetClient, error) { + var proxyType, proxyAddr string + if proxyUrl != "" { + if strings.HasPrefix(proxyUrl, "http://") { + proxyType = "http" + proxyAddr = proxyUrl + } else if strings.HasPrefix(proxyUrl, "https://") { + proxyType = "https" + proxyAddr = proxyUrl + } else if strings.HasPrefix(proxyUrl, "socks5://") { + proxyType = "socks5" + proxyAddr = proxyUrl + } else { + // 默认为http代理 + proxyType = "http" + proxyAddr = "http://" + proxyUrl + } + } + // 创建HTTP客户端 + client := &http.Client{ + Transport: &http.Transport{ + MaxIdleConns: 1000, + IdleConnTimeout: 10 * time.Second, + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + + // 设置代理 + if proxyAddr != "" { + err := CreateHtppProxy(proxyType, proxyAddr, client) + if err != nil { + return nil, err + } + } + + return &BitgetClient{ + APIKey: apiKey, + APISecret: apiSecret, + Passphrase: passphrase, + HTTPClient: client, + BaseURL: "https://api.bitget.com", + SignType: "SHA256", + }, nil +} + +// DoGet 执行GET请求 +func (bc *BitgetClient) DoGet(uri string, params map[string]string) ([]byte, error) { + timestamp := bc.getTimestamp() + body := bc.buildGetParams(params) + + sign := bc.sign("GET", uri, body, timestamp) + requestURL := bc.BaseURL + uri + body + + request, err := http.NewRequest("GET", requestURL, nil) + if err != nil { + return nil, err + } + + bc.setHeaders(request, timestamp, sign) + + response, err := bc.HTTPClient.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + return ioutil.ReadAll(response.Body) +} + +// DoPost 执行POST请求 +func (bc *BitgetClient) DoPost(uri string, params interface{}) ([]byte, error) { + timestamp := bc.getTimestamp() + + var body string + if params != nil { + bodyBytes, err := json.Marshal(params) + if err != nil { + return nil, err + } + body = string(bodyBytes) + } + + sign := bc.sign("POST", uri, body, timestamp) + requestURL := bc.BaseURL + uri + + request, err := http.NewRequest("POST", requestURL, strings.NewReader(body)) + if err != nil { + return nil, err + } + + bc.setHeaders(request, timestamp, sign) + + response, err := bc.HTTPClient.Do(request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + return ioutil.ReadAll(response.Body) +} + +// getTimestamp 获取时间戳 +func (bc *BitgetClient) getTimestamp() string { + return strconv.FormatInt(time.Now().Unix()*1000, 10) +} + +// setHeaders 设置请求头 +func (bc *BitgetClient) setHeaders(request *http.Request, timestamp, sign string) { + request.Header.Add("Content-Type", "application/json") + request.Header.Add("ACCESS-KEY", bc.APIKey) + request.Header.Add("ACCESS-SIGN", sign) + request.Header.Add("ACCESS-TIMESTAMP", timestamp) + request.Header.Add("ACCESS-PASSPHRASE", bc.Passphrase) +} + +// sign 生成签名 +func (bc *BitgetClient) sign(method, requestPath, body, timestamp string) string { + var payload strings.Builder + payload.WriteString(timestamp) + payload.WriteString(method) + payload.WriteString(requestPath) + if body != "" && body != "?" { + payload.WriteString(body) + } + + if bc.SignType == "RSA" { + return bc.signByRSA(payload.String()) + } + + return bc.signByHMAC(payload.String()) +} + +// signByHMAC HMAC签名 +func (bc *BitgetClient) signByHMAC(payload string) string { + hash := hmac.New(sha256.New, []byte(bc.APISecret)) + hash.Write([]byte(payload)) + return base64.StdEncoding.EncodeToString(hash.Sum(nil)) +} + +// signByRSA RSA签名 +func (bc *BitgetClient) signByRSA(payload string) string { + sign, _ := bc.rsaSign([]byte(payload), []byte(bc.APISecret), crypto.SHA256) + return base64.StdEncoding.EncodeToString(sign) +} + +// rsaSign RSA签名实现 +func (bc *BitgetClient) rsaSign(src []byte, priKey []byte, hash crypto.Hash) ([]byte, error) { + block, _ := pem.Decode(priKey) + if block == nil { + return nil, errors.New("key is invalid format") + } + + var pkixPrivateKey interface{} + var err error + if block.Type == "RSA PRIVATE KEY" { + pkixPrivateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + } else if block.Type == "PRIVATE KEY" { + pkixPrivateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes) + } + + if err != nil { + return nil, err + } + + h := hash.New() + _, err = h.Write(src) + if err != nil { + return nil, err + } + + bytes := h.Sum(nil) + sign, err := rsa.SignPKCS1v15(rand.Reader, pkixPrivateKey.(*rsa.PrivateKey), hash, bytes) + if err != nil { + return nil, err + } + + return sign, nil +} + +// buildGetParams 构建GET请求参数 +func (bc *BitgetClient) buildGetParams(params map[string]string) string { + if len(params) == 0 { + return "" + } + return "?" + bc.sortParams(params) +} + +// sortParams 参数排序 +func (bc *BitgetClient) sortParams(params map[string]string) string { + keys := make([]string, 0, len(params)) + for k := range params { + keys = append(keys, k) + } + sort.Strings(keys) + + sorted := make([]string, 0, len(params)) + for _, k := range keys { + sorted = append(sorted, k+"="+params[k]) + } + return strings.Join(sorted, "&") +} + +// SendSpotRequestAuth 发送现货认证请求 +func (bc *BitgetClient) SendSpotRequestAuth(endpoint, method string, params map[string]interface{}) ([]byte, *http.Response, error) { + if method == "GET" { + paramStr := make(map[string]string) + for k, v := range params { + paramStr[k] = fmt.Sprintf("%v", v) + } + resp, err := bc.DoGet(endpoint, paramStr) + return resp, nil, err + } else { + resp, err := bc.DoPost(endpoint, params) + return resp, nil, err + } +} + +// SendFuturesRequestAuth 发送合约认证请求 +func (bc *BitgetClient) SendFuturesRequestAuth(endpoint, method string, params map[string]interface{}) ([]byte, *http.Response, error) { + return bc.SendSpotRequestAuth(endpoint, method, params) +} diff --git a/common/interfaces/exchange.go b/common/interfaces/exchange.go new file mode 100644 index 0000000..2b22ea1 --- /dev/null +++ b/common/interfaces/exchange.go @@ -0,0 +1,170 @@ +package interfaces + +import ( + "go-admin/models" + + "github.com/shopspring/decimal" +) + +// ExchangeInterface 交易所通用接口抽象层 +type ExchangeInterface interface { + // 基础信息接口 + GetExchangeName() string + GetExchangeInfo() (ExchangeInfoResponse, error) + + // 现货市场接口 + SpotInterface + + // 合约接口(如果支持) + FuturesInterface + + // WebSocket接口 + WebSocketInterface +} + +// SpotInterface 现货交易接口 +type SpotInterface interface { + // 市场数据 + GetSpotTicker24h(symbol string) (models.Ticker24, error) + GetSpotTickers() ([]models.TradeSet, error) + // 订单管理 + PlaceSpotOrder(params SpotOrderParams) (SpotOrderResponse, error) + CancelSpotOrder(symbol, orderID string) error + GetSpotOrder(symbol, orderID string) (SpotOrderResponse, error) + GetSpotOrders(symbol string) ([]SpotOrderResponse, error) +} + +// FuturesInterface 合约交易接口 +type FuturesInterface interface { + // 市场数据 + GetFuturesTicker24h(symbol string) (models.Ticker24, error) + GetFuturesTickers() ([]models.TradeSet, error) + // 订单管理 + PlaceFuturesOrder(params FuturesOrderParams) (FuturesOrderResponse, error) + CancelFuturesOrder(symbol, orderID string) error + GetFuturesOrder(symbol, orderID string) (FuturesOrderResponse, error) +} + +// WebSocketInterface WebSocket接口 +type WebSocketInterface interface { + SubscribeSpotTicker(symbols []string, callback func(models.Ticker24, string, string)) error + SubscribeFuturesTicker(symbols []string, callback func(models.Ticker24, string, string)) error + SubscribeSpotDepth(symbols []string, callback func(models.DepthBin, string, string)) error + SubscribeSpotTrades(symbols []string, callback func(models.NewDealPush, string, string)) error + SubscribeKline(symbols []string, interval string, callback func(models.Kline, int, string, string)) error + + Close() error +} + +// 通用数据结构定义 +type ExchangeInfoResponse struct { + Symbols []SymbolInfo `json:"symbols"` +} + +type SymbolInfo struct { + Symbol string `json:"symbol"` + BaseAsset string `json:"baseAsset"` + QuoteAsset string `json:"quoteAsset"` + Status string `json:"status"` + MinQty string `json:"minQty"` + MaxQty string `json:"maxQty"` + StepSize string `json:"stepSize"` + MinPrice string `json:"minPrice"` + MaxPrice string `json:"maxPrice"` + TickSize string `json:"tickSize"` + MinNotional string `json:"minNotional"` +} + +// 现货订单参数 +type SpotOrderParams struct { + Symbol string `json:"symbol"` + Side string `json:"side"` // BUY/SELL + Type string `json:"type"` // MARKET/LIMIT + Quantity decimal.Decimal `json:"quantity"` + Price decimal.Decimal `json:"price,omitempty"` + TimeInForce string `json:"timeInForce,omitempty"` // GTC/IOC/FOK + NewClientOrderId string `json:"newClientOrderId,omitempty"` +} + +// 合约订单参数 +type FuturesOrderParams struct { + Symbol string `json:"symbol"` + Side string `json:"side"` // BUY/SELL + Type string `json:"type"` // MARKET/LIMIT + Quantity decimal.Decimal `json:"quantity"` + Price decimal.Decimal `json:"price,omitempty"` + TimeInForce string `json:"timeInForce,omitempty"` + PositionSide string `json:"positionSide,omitempty"` // LONG/SHORT/BOTH + NewClientOrderId string `json:"newClientOrderId,omitempty"` +} + +// 响应数据结构 +type SpotAccountResponse struct { + Balances []SpotBalanceResponse `json:"balances"` +} + +type SpotBalanceResponse struct { + Asset string `json:"asset"` + Free string `json:"free"` + Locked string `json:"locked"` +} + +type SpotOrderResponse struct { + Symbol string `json:"symbol"` + OrderId int64 `json:"orderId"` + ClientOrderId string `json:"clientOrderId"` + Price string `json:"price"` + OrigQty string `json:"origQty"` + ExecutedQty string `json:"executedQty"` + Status string `json:"status"` + TimeInForce string `json:"timeInForce"` + Type string `json:"type"` + Side string `json:"side"` + Time int64 `json:"time"` + UpdateTime int64 `json:"updateTime"` +} + +type FuturesAccountResponse struct { + TotalWalletBalance string `json:"totalWalletBalance"` + Assets []FuturesBalanceResponse `json:"assets"` + Positions []FuturesPositionResponse `json:"positions"` +} + +type FuturesBalanceResponse struct { + Asset string `json:"asset"` + WalletBalance string `json:"walletBalance"` + UnrealizedProfit string `json:"unrealizedProfit"` + MarginBalance string `json:"marginBalance"` + MaintMargin string `json:"maintMargin"` + InitialMargin string `json:"initialMargin"` + PositionInitialMargin string `json:"positionInitialMargin"` + OpenOrderInitialMargin string `json:"openOrderInitialMargin"` +} + +type FuturesPositionResponse struct { + Symbol string `json:"symbol"` + PositionAmt string `json:"positionAmt"` + EntryPrice string `json:"entryPrice"` + MarkPrice string `json:"markPrice"` + UnrealizedProfit string `json:"unrealizedProfit"` + LiquidationPrice string `json:"liquidationPrice"` + Leverage string `json:"leverage"` + PositionSide string `json:"positionSide"` +} + +type FuturesOrderResponse struct { + Symbol string `json:"symbol"` + OrderId int64 `json:"orderId"` + ClientOrderId string `json:"clientOrderId"` + Price string `json:"price"` + OrigQty string `json:"origQty"` + ExecutedQty string `json:"executedQty"` + Status string `json:"status"` + TimeInForce string `json:"timeInForce"` + Type string `json:"type"` + Side string `json:"side"` + PositionSide string `json:"positionSide"` + ReduceOnly bool `json:"reduceOnly"` + Time int64 `json:"time"` + UpdateTime int64 `json:"updateTime"` +} diff --git a/config/extend.go b/config/extend.go index 1a41904..b704453 100644 --- a/config/extend.go +++ b/config/extend.go @@ -14,7 +14,6 @@ type Extend struct { Redis RedisConfig `mapstructure:"redis"` ServiceId int64 `mapstructure:"serviceId"` //雪花算法id(服务id,比如1,2,3等,当多机部署需定义唯一id) EmailConfig EmailConfig `mapstructure:"emailConfig"` - BinanceSet BinanceConfig `mapstructure:"binanceSet"` //binance配置 Domain string //网站域名 UDunConfig UDunConfig `mapstructure:"UDunConfig"` ProxyUrl string //代理地址 @@ -58,12 +57,6 @@ type EmailSend struct { To string `json:"to"` // 收件地址 } -// BinanceConfig 币安路由配置 -type BinanceConfig struct { - SpotRestURL string - FutRestURL string -} - type GoToneSmsConfig struct { SenderId string `json:"sender_id"` APIEndpoint string `json:"api_endpoint"` @@ -82,3 +75,14 @@ type UDunConfig struct { UDunKey string `json:"UDunKey"` CurrServerIp string `json:"CurrServerIp"` } + +/** + * Binance配置结构体 + */ +type BinanceConfig struct { + ApiKey string `json:"apiKey" mapstructure:"apiKey"` // API密钥 + ApiSecret string `json:"apiSecret" mapstructure:"apiSecret"` // API密钥 + ProxyType string `json:"proxyType" mapstructure:"proxyType"` // 代理类型 + ProxyAddress string `json:"proxyAddress" mapstructure:"proxyAddress"` // 代理地址 + TestNet bool `json:"testNet" mapstructure:"testNet"` // 是否使用测试网 +} diff --git a/config/serverinit/binance_futures_init.go b/config/serverinit/binance_futures_init.go index ca20be0..7600341 100644 --- a/config/serverinit/binance_futures_init.go +++ b/config/serverinit/binance_futures_init.go @@ -5,7 +5,6 @@ import ( "go-admin/models" "go-admin/pkg/httputils" "go-admin/services/binanceservice" - "go-admin/services/futureservice" log "github.com/go-admin-team/go-admin-core/logger" "github.com/robfig/cron/v3" @@ -33,9 +32,6 @@ func FuturesInit() error { } log.Info("初始化24h行情-结束") - log.Info("订阅合约-开始") - go futureservice.StartBinanceProWs("normal") - //定时任务 RunJobs() return nil diff --git a/config/serverinit/bitget_init.go b/config/serverinit/bitget_init.go new file mode 100644 index 0000000..ce5da52 --- /dev/null +++ b/config/serverinit/bitget_init.go @@ -0,0 +1,144 @@ +package serverinit + +import ( + "go-admin/config" + "go-admin/models" + "go-admin/pkg/httputils" + "go-admin/pkg/utility" + "go-admin/services/bitgetservice" + + log "github.com/go-admin-team/go-admin-core/logger" + "github.com/robfig/cron/v3" +) + +// BitgetInit 初始化Bitget交易所 +func BitgetMarketInit() error { + // 设置代理(如果配置了的话) + httputils.InitProxy(config.ExtConfig.ProxyUrl) + + log.Info("初始化Bitget交易对-开始") + spotData := make(map[string]models.TradeSet, 0) + futData := make(map[string]models.TradeSet, 0) + err := bitgetservice.GetAndReloadSymbols(&spotData, 1) + + if err != nil { + log.Error("初始化Bitget交易对失败,可能是网络问题,将跳过初始化:", err) + // 不返回错误,允许程序继续运行 + return nil + } + + err = bitgetservice.GetAndReloadSymbols(&futData, 2) + if err != nil { + log.Error("初始化Bitget合约交易对失败,可能是网络问题,将跳过初始化:", err) + // 不返回错误,允许程序继续运行 + return nil + } + + log.Info("初始化Bitget交易对-结束") + + log.Info("初始化Bitget 24h行情-开始") + _, err = bitgetservice.InitSymbolsTicker24h(&spotData, 1) + + if err != nil { + log.Error("初始化Bitget 24h行情失败,可能是网络问题,将跳过初始化:", err) + // 不返回错误,允许程序继续运行 + return nil + } + + _, err = bitgetservice.InitSymbolsTicker24h(&futData, 2) + if err != nil { + log.Error("初始化Bitget合约24h行情失败,可能是网络问题,将跳过初始化:", err) + // 不返回错误,允许程序继续运行 + return nil + } + log.Info("初始化Bitget 24h行情-结束") + + //现货行情订阅 + spotSymbols := []string{} + for k, _ := range spotData { + spotSymbols = append(spotSymbols, k) + } + spotMaps := utility.SplitSlice(spotSymbols, 50) + bitgetSpot := bitgetservice.BitgetMarketSpotClient{} + + for _, v := range spotMaps { + if err := bitgetSpot.Subscribe(v); err != nil { + log.Error("订阅Bitget现货行情失败", err) + return err + } + } + + //合约行情订阅 + futSymbols := []string{} + for k, _ := range futData { + futSymbols = append(futSymbols, k) + } + futMaps := utility.SplitSlice(futSymbols, 50) + bitgetFut := bitgetservice.BitgetMarketFutureClient{} + + for _, v := range futMaps { + if err := bitgetFut.Subscribe(v); err != nil { + log.Error("订阅Bitget合约行情失败", err) + return err + } + } + + // 启动定时任务 + runBitgetJobs() + + return nil +} + +// runBitgetJobs 启动Bitget相关的定时任务 +func runBitgetJobs() { + cronJob := cron.New() + + // 每小时更新一次交易对信息 + _, err := cronJob.AddFunc("@every 1h", func() { + log.Info("执行Bitget交易对更新任务") + data := make(map[string]models.TradeSet, 0) + if err := bitgetservice.GetAndReloadSymbols(&data, 1); err != nil { + log.Error("更新Bitget交易对失败", err) + } + }) + + if err != nil { + log.Error("添加Bitget交易对更新任务失败", err) + } else { + log.Info("添加Bitget交易对更新任务成功") + } + + // 启动定时任务 + cronJob.Start() + log.Info("Bitget定时任务启动完成") +} + +// CreateBitgetPrivateWebSocket 创建Bitget私有WebSocket连接的示例函数 +func CreateBitgetPrivateWebSocket(apiKey, apiSecret, passphrase, proxyType, proxyAddress string) { + log.Info("创建Bitget私有WebSocket连接 key:", apiKey) + + // 现货WebSocket (wsType=0) + spotManager := bitgetservice.GetOrCreateBitgetWebSocketManager( + 0, apiKey, apiSecret, passphrase, proxyType, proxyAddress) + spotManager.Start() + + // 合约WebSocket (wsType=1) + futuresManager := bitgetservice.GetOrCreateBitgetWebSocketManager( + 1, apiKey, apiSecret, passphrase, proxyType, proxyAddress) + futuresManager.Start() + + log.Info("Bitget私有WebSocket连接已创建") +} + +// StopBitgetPrivateWebSocket 停止Bitget私有WebSocket连接的示例函数 +func StopBitgetPrivateWebSocket(apiKey string) { + log.Info("停止Bitget私有WebSocket连接 key:", apiKey) + + // 停止现货WebSocket + bitgetservice.StopBitgetWebSocketManager(0, apiKey) + + // 停止合约WebSocket + bitgetservice.StopBitgetWebSocketManager(1, apiKey) + + log.Info("Bitget私有WebSocket连接已停止") +} diff --git a/config/serverinit/business_init.go b/config/serverinit/business_init.go index c48c5fe..c34f617 100644 --- a/config/serverinit/business_init.go +++ b/config/serverinit/business_init.go @@ -8,7 +8,6 @@ import ( "go-admin/common/helper" "go-admin/config" "go-admin/pkg/httputils" - "go-admin/services/binanceservice" "go-admin/services/cacheservice" "go-admin/services/futureservice" "go-admin/services/scriptservice" @@ -46,54 +45,34 @@ func BusinessInit(db *gorm.DB) { os.Exit(-1) } - //币安 现货交易对 - SpotCurrencyInit() - //币安 合约交易对 - FuturesInit() - - //第一次初始化交易对价格 - spotApi := binanceservice.SpotRestApi{} - futApi := binanceservice.FutRestApi{} - spotApi.Ticker() - futApi.Ticker() - - //现货 订阅 - SpotSubscribeInit("normal") - //合约订阅 - futureservice.StartBinanceProWs("normal") //定时任务 RunSpotJobs(db) } +// 行情监听初始化 +func BinanceMarketInit() error { + //币安 现货交易对(仅数据初始化,不启动WebSocket) + SpotCurrencyInit() + //币安 合约交易对(仅数据初始化,不启动WebSocket) + FuturesInit() + + //现货 订阅 + // SpotSubscribeInit("normal") + //合约订阅 + futureservice.StartBinanceProWs("normal") + // log.Info("订阅合约-开始") + // go futureservice.StartBinanceProWs("normal") + + return nil +} + // 开启现货ws监听 func SpotWsInit() { - //启动websocket服务 - //go func() { - // err := wsservice.StartServer(":3333") - // if err != nil { - // log.Info("wsservice.StartServer", zap.Error(err)) - // - // _, cancel := context.WithTimeout(context.Background(), 5*time.Second) - // cancel() - // } - //}() } // 定义任务 func RunSpotJobs(db *gorm.DB) { cronJob := cron.New() - //添加任务 同步交易对行情 - // spotApi := spotservice.SpotRestApi{} - // futApi := futureservice.FutRestApi{} - // _, err := cronJob.AddFunc("@every 10s", func() { - // //BuySellOrderNew(db) - // spotApi.Ticker() - // futApi.Ticker() - // }) - // if err != nil { - // return - // } - //定时执行脚本任务 _, err := cronJob.AddFunc("@every 3s", func() { order := scriptservice.PreOrder{} diff --git a/go.mod b/go.mod index 1099a6b..236c9b2 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module go-admin -go 1.21.0 +go 1.24.0 -toolchain go1.22.5 +toolchain go1.24.2 require ( github.com/alibaba/sentinel-golang v1.0.4 @@ -47,8 +47,8 @@ require ( github.com/vmihailenco/msgpack/v5 v5.4.1 github.com/xuri/excelize/v2 v2.9.0 go.uber.org/zap v1.26.0 - golang.org/x/crypto v0.31.0 - golang.org/x/net v0.33.0 + golang.org/x/crypto v0.43.0 + golang.org/x/net v0.45.0 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gorm.io/driver/mysql v1.5.2 gorm.io/driver/postgres v1.5.4 @@ -174,13 +174,13 @@ require ( go.uber.org/multierr v1.10.0 // indirect golang.org/x/arch v0.12.0 // indirect golang.org/x/image v0.18.0 // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/term v0.27.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.36.0 // indirect + golang.org/x/text v0.30.0 // indirect golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect - golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + golang.org/x/tools v0.37.0 // indirect google.golang.org/protobuf v1.36.1 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/models/coin.go b/models/coin.go index df01733..743c367 100644 --- a/models/coin.go +++ b/models/coin.go @@ -20,6 +20,7 @@ type TradeSet struct { PriceDigit int `db:"pricecdigit" json:"pricecdigit"` //价格小数点位数 Currency string `db:"currency" json:"currency"` //法币 Coin string `db:"coin" json:"coin"` //币种 + Symbol string `db:"symbol" json:"symbol"` //交易对 PriceChange float64 `db:"pricechange" json:"pricechange"` //价格波动价位 MinBuyVal float64 `db:"minbuyval"` //最小下单金额 OpenPrice float64 `db:"openprice"` //开盘价格 diff --git a/models/positiondto/cache.go b/models/positiondto/cache.go index 364fb0e..4f0cf53 100644 --- a/models/positiondto/cache.go +++ b/models/positiondto/cache.go @@ -31,3 +31,28 @@ type LinePreOrderPositioinDelReq struct { ApiId int `json:"apiId"` //apiid ExchangeType string `json:"exchangeType"` //交易所类型 } + +// 待触发加仓单 +type AddPositionList struct { + Id int `json:"id"` //订单id + Pid int `json:"pid"` //父级id + MainId int `json:"mainId"` //主单Id + ApiId int `json:"apiId"` //触发账户id + Symbol string `json:"symbol"` //交易对 + Price decimal.Decimal `json:"price"` //触发价 + Side string `json:"side"` //买卖方向 + SymbolType int `json:"type" comment:"交易对类别 1-现货 2-合约"` + OrderSn string `json:"prderSn"` +} + +type ReduceListItem struct { + Id int `json:"id"` + ApiId int `json:"apiId"` + MainId int `json:"mainId"` + Pid int `json:"pid"` + Symbol string `json:"symbol"` + Price decimal.Decimal `json:"price"` + Side string `json:"side"` + Num decimal.Decimal `json:"num"` + OrderSn string `json:"orderSn"` +} diff --git a/services/binanceservice/binancerest.go b/services/binanceservice/binancerest.go index d833ae9..764e2c4 100644 --- a/services/binanceservice/binancerest.go +++ b/services/binanceservice/binancerest.go @@ -14,6 +14,7 @@ import ( "go-admin/models/spot" "go-admin/pkg/httputils" "go-admin/pkg/utility" + "go-admin/services/commonservice" "strings" "time" @@ -126,7 +127,7 @@ func (e SpotRestApi) GetSpotTicker24h(tradeSet *map[string]models.TradeSet) (del log.Error("缓存交易对失败|", item.Symbol, err) } - helper.DefaultRedis.AddSortSet(global.COIN_PRICE_CHANGE, symbol.PriceChange, symbol.Coin) + helper.DefaultRedis.AddSortSet(fmt.Sprintf(global.COIN_PRICE_CHANGE, global.EXCHANGE_BINANCE), symbol.PriceChange, symbol.Coin) } return deleteSymbols, nil } @@ -169,12 +170,12 @@ type Ticker struct { Price string `json:"price"` } -func (e SpotRestApi) Ticker() { - tickerApi := fmt.Sprintf("%s%s", binanceRestApi, "/api/v3/ticker/price") - mapData, _ := httputils.NewHttpRequestWithFasthttp("GET", tickerApi, "", map[string]string{}) - //sonic.Unmarshal(mapData, &tickerData) - helper.DefaultRedis.SetString(rediskey.SpotSymbolTicker, string(mapData)) -} +// func (e SpotRestApi) Ticker() { +// tickerApi := fmt.Sprintf("%s%s", binanceRestApi, "/api/v3/ticker/price") +// mapData, _ := httputils.NewHttpRequestWithFasthttp("GET", tickerApi, "", map[string]string{}) +// //sonic.Unmarshal(mapData, &tickerData) +// helper.DefaultRedis.SetString(rediskey.SpotSymbolTicker, string(mapData)) +// } // 循环下单 func (e SpotRestApi) OrderPlaceLoop(db *gorm.DB, params OrderPlacementService, retryCount int) error { @@ -452,7 +453,7 @@ func GetApiInfo(apiId int) (DbModels.LineApiUser, error) { } } - db := GetDBConnection() + db := commonservice.GetDBConnection() if err := db.Model(&api).Where("id =?", apiId).First(&api).Error; err != nil { return api, err diff --git a/services/binanceservice/commonservice.go b/services/binanceservice/commonservice.go index fa6fca1..ce1ca1f 100644 --- a/services/binanceservice/commonservice.go +++ b/services/binanceservice/commonservice.go @@ -9,6 +9,7 @@ import ( "go-admin/common/helper" "go-admin/models/positiondto" "go-admin/pkg/utility" + "go-admin/services/commonservice" "go-admin/services/positionservice" "strings" "time" @@ -16,7 +17,6 @@ import ( "github.com/bytedance/sonic" "github.com/go-admin-team/go-admin-core/logger" log "github.com/go-admin-team/go-admin-core/logger" - "github.com/go-admin-team/go-admin-core/sdk" "github.com/shopspring/decimal" "gorm.io/gorm" ) @@ -105,7 +105,7 @@ func CancelSpotOrder(symbol string, apiUserInfo *DbModels.LineApiUser, side stri } client := GetClient(apiUserInfo) resp, _, err := client.SendSpotAuth(searchEndpoint, "GET", searchParams) - db := GetDBConnection() + db := commonservice.GetDBConnection() if err != nil { if len(resp) > 0 { @@ -209,44 +209,6 @@ func (e *AddPosition) GetFutSpotOrderInfo(req dto.ManuallyCover, symbol, orderTy return orderInfo, nil } -// CalculateAmount 计算加仓数量 -func (e *AddPosition) CalculateAmount(req dto.ManuallyCover, totalNum, lastPrice decimal.Decimal, amountDigit int, notUsdt bool, symbolInfo DbModels.LineSymbol) (decimal.Decimal, error) { - var amt decimal.Decimal - if req.CoverType == 1 { - decimalValue := utility.StringToDecimal(req.Value).Div(decimal.NewFromInt(100)) - amt = totalNum.Mul(decimalValue) - } else { - decimalValue := utility.StringToDecimal(req.Value) - if notUsdt { - tickerSymbolMaps := make([]dto.Ticker, 0) - tickerSymbol := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val() - if err := sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps); err != nil { - return decimal.Zero, err - } - - var tickerPrice decimal.Decimal - for _, symbolMap := range tickerSymbolMaps { - if symbolMap.Symbol == strings.ToUpper(symbolInfo.BaseAsset+"USDT") { - tickerPrice, _ = decimal.NewFromString(symbolMap.Price) - break - } - } - - for _, symbolMap := range tickerSymbolMaps { - if symbolMap.Symbol == strings.ToUpper(symbolInfo.QuoteAsset+"USDT") { - uTickerPrice, _ := decimal.NewFromString(symbolMap.Price) - div := tickerPrice.Div(decimal.NewFromInt(1).Div(uTickerPrice)) - amt = decimalValue.Div(div) - break - } - } - } else { - amt = decimalValue.Div(lastPrice) - } - } - return amt.Truncate(int32(amountDigit)), nil -} - // 主单平仓删除缓存 // mainOrderId 主单id // symbolType 1现货 2合约 @@ -256,7 +218,7 @@ func MainClosePositionClearCache(mainId int, symbolType int) { keySpotAddposition := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE) spotStopArray, _ := helper.DefaultRedis.GetAllList(keySpotStop) spotAddpositionArray, _ := helper.DefaultRedis.GetAllList(keySpotAddposition) - var position AddPositionList + var position positiondto.AddPositionList var stop dto.StopLossRedisList for _, item := range spotAddpositionArray { @@ -284,7 +246,7 @@ func MainClosePositionClearCache(mainId int, symbolType int) { keyFutAddposition := fmt.Sprintf(rediskey.FuturesStopLossList, global.EXCHANGE_BINANCE) futAddpositionArray, _ := helper.DefaultRedis.GetAllList(keyFutStop) futStopArray, _ := helper.DefaultRedis.GetAllList(keyFutAddposition) - var position AddPositionList + var position positiondto.AddPositionList var stop dto.StopLossRedisList for _, item := range futAddpositionArray { @@ -309,15 +271,6 @@ func MainClosePositionClearCache(mainId int, symbolType int) { } } -// 获取数据库连接 -func GetDBConnection() *gorm.DB { - dbs := sdk.Runtime.GetDb() - for _, db := range dbs { - return db - } - return nil -} - // 查询订单 func getPreOrder(db *gorm.DB, orderSn interface{}) (*DbModels.LinePreOrder, error) { preOrder := &DbModels.LinePreOrder{} diff --git a/services/binanceservice/futures_judge_service_test.go b/services/binanceservice/futures_judge_service_test.go index 5e198c6..73592f4 100644 --- a/services/binanceservice/futures_judge_service_test.go +++ b/services/binanceservice/futures_judge_service_test.go @@ -6,6 +6,7 @@ import ( "go-admin/common/global" "go-admin/common/helper" "go-admin/models" + "go-admin/models/positiondto" "go-admin/services/cacheservice" "testing" @@ -29,7 +30,7 @@ func TestFutureJudge(t *testing.T) { key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE) item := `{"id":50,"apiId":49,"mainId":47,"pid":47,"symbol":"ADAUSDT","price":"0.5936","side":"SELL","num":"12","orderSn":"397913127842217984"}` - reduceOrder := ReduceListItem{} + reduceOrder := positiondto.ReduceListItem{} futApi := FutRestApi{} setting, err := cacheservice.GetSystemSetting(db) diff --git a/services/binanceservice/futuresbinancerest.go b/services/binanceservice/futuresbinancerest.go index 147a943..45a69e9 100644 --- a/services/binanceservice/futuresbinancerest.go +++ b/services/binanceservice/futuresbinancerest.go @@ -5,7 +5,6 @@ import ( "fmt" DbModels "go-admin/app/admin/models" "go-admin/app/admin/service/dto" - "go-admin/common/const/rediskey" "go-admin/common/global" "go-admin/common/helper" "go-admin/models" @@ -13,6 +12,7 @@ import ( "go-admin/models/futuresdto" "go-admin/pkg/httputils" "go-admin/pkg/utility" + "go-admin/services/cacheservice" "strconv" "strings" "time" @@ -261,18 +261,18 @@ func GetAndReloadSymbols(data *map[string]models.TradeSet) error { return nil } -func (e FutRestApi) Ticker() { - url := fmt.Sprintf("%s%s", futureApi, "/fapi/v1/ticker/price") - mapData, err := httputils.NewHttpRequestWithFasthttp("GET", url, "", map[string]string{}) - if err != nil { - log.Error("获取合约交易对行情失败:err:", err) - } +// func (e FutRestApi) Ticker() { +// url := fmt.Sprintf("%s%s", futureApi, "/fapi/v1/ticker/price") +// mapData, err := httputils.NewHttpRequestWithFasthttp("GET", url, "", map[string]string{}) +// if err != nil { +// log.Error("获取合约交易对行情失败:err:", err) +// } - if len(mapData) == 0 { - log.Error("获取合约交易对行情失败,或数量为空") - } - helper.DefaultRedis.SetString(rediskey.FutSymbolTicker, string(mapData)) -} +// if len(mapData) == 0 { +// log.Error("获取合约交易对行情失败,或数量为空") +// } +// helper.DefaultRedis.SetString(rediskey.FutSymbolTicker, string(mapData)) +// } // 获取合约行情 func GetTicker24h() ([]futuresdto.FutureTicker24h, error) { @@ -862,9 +862,10 @@ func (e FutRestApi) CancelBatchFutOrder(apiUserInfo DbModels.LineApiUser, symbol // quoteSymbol 计价货币 ETHBTC -> 计价货币就是BTC // totalMoney 兑换的总金额 func (e FutRestApi) CalcSymbolExchangeAmt(symbol string, quoteSymbol string, totalMoney decimal.Decimal) decimal.Decimal { - tickerSymbol := helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val() - tickerSymbolMaps := make([]dto.Ticker, 0) - sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps) + // tickerSymbol := helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val() + // tickerSymbolMaps := make([]dto.Ticker, 0) + // sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps) + tickerSymbolMaps, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 1) targetSymbol := strings.Replace(symbol, quoteSymbol, "USDT", 1) //ETHBTC -》 ETHUSDT key := fmt.Sprintf("%s:%s", global.TICKER_FUTURES, targetSymbol) @@ -873,7 +874,7 @@ func (e FutRestApi) CalcSymbolExchangeAmt(symbol string, quoteSymbol string, tot quantity := decimal.Zero for _, symbolMap := range tickerSymbolMaps { if symbolMap.Symbol == strings.ToUpper(targetSymbol) { - targetPrice, _ = decimal.NewFromString(symbolMap.Price) + targetPrice, _ = decimal.NewFromString(symbolMap.LastPrice) quantity = totalMoney.Div(targetPrice).Truncate(int32(tradeSet.AmountDigit)) } } diff --git a/services/binanceservice/futuresjudgeservice.go b/services/binanceservice/futuresjudgeservice.go index 935da5c..f026f85 100644 --- a/services/binanceservice/futuresjudgeservice.go +++ b/services/binanceservice/futuresjudgeservice.go @@ -10,8 +10,11 @@ import ( "go-admin/common/global" "go-admin/common/helper" "go-admin/models" + "go-admin/models/positiondto" "go-admin/pkg/utility" "go-admin/services/cacheservice" + "go-admin/services/commonservice" + "go-admin/services/orderservice" "strings" "time" @@ -26,7 +29,7 @@ import ( */ func JudgeFuturesPrice(tradeSet models.TradeSet) { preOrderVal, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.PreFutOrderList, global.EXCHANGE_BINANCE)) - db := GetDBConnection() + db := commonservice.GetDBConnection() if len(preOrderVal) == 0 { return @@ -153,7 +156,7 @@ func JudgeFuturesReduce(trade models.TradeSet) { key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE) reduceVal, _ := helper.DefaultRedis.GetAllList(key) - db := GetDBConnection() + db := commonservice.GetDBConnection() futApi := FutRestApi{} setting, err := cacheservice.GetSystemSetting(db) @@ -162,12 +165,14 @@ func JudgeFuturesReduce(trade models.TradeSet) { return } - reduceOrder := ReduceListItem{} + reduceOrder := positiondto.ReduceListItem{} tradePrice, _ := decimal.NewFromString(trade.LastPrice) //减仓单减仓策略 reduceReduceListKey := fmt.Sprintf(rediskey.FutOrderReduceStrategyList, global.EXCHANGE_BINANCE) orderReduceVal, _ := helper.DefaultRedis.HGetAllFields(reduceReduceListKey) reduceOrderStrategy := dto.LineOrderReduceStrategyResp{} + orderService := orderservice.OrderService{} + orderService.Orm = db for _, item := range orderReduceVal { sonic.Unmarshal([]byte(item), &reduceOrderStrategy) @@ -193,7 +198,7 @@ func JudgeFuturesReduce(trade models.TradeSet) { return } - order, err := CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit) + order, err := orderService.CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit) if err != nil { log.Errorf("%d 生成订单失败", reduceOrderStrategy.OrderId) @@ -261,8 +266,8 @@ func JudgeFuturesReduce(trade models.TradeSet) { // 触发合约减仓 // isStrategy 是否是策略减仓 // reduceId 父减仓单id -func FuturesReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, futApi FutRestApi, setting DbModels.LineSystemSetting, key, item string, isStrategy bool, reduceId int) bool { - tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reduceOrder.Symbol, 1) +func FuturesReduceTrigger(db *gorm.DB, reduceOrder positiondto.ReduceListItem, futApi FutRestApi, setting DbModels.LineSystemSetting, key, item string, isStrategy bool, reduceId int) bool { + tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reduceOrder.Symbol, 2) result := true if tradeSet.LastPrice == "" { @@ -362,7 +367,7 @@ func FuturesReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, futApi FutRes func JudgeFutAddPosition(trade models.TradeSet) { key := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE) preOrderVal, _ := helper.DefaultRedis.GetAllList(key) - db := GetDBConnection() + db := commonservice.GetDBConnection() if len(preOrderVal) == 0 { return @@ -370,7 +375,7 @@ func JudgeFutAddPosition(trade models.TradeSet) { futApi := FutRestApi{} for _, item := range preOrderVal { - preOrder := AddPositionList{} + preOrder := positiondto.AddPositionList{} if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil { log.Error("反序列化失败") continue @@ -394,7 +399,7 @@ func JudgeFutAddPosition(trade models.TradeSet) { } // 合约加仓触发 -func FutAddPositionTrigger(db *gorm.DB, v *AddPositionList, item string, futApi FutRestApi) { +func FutAddPositionTrigger(db *gorm.DB, v *positiondto.AddPositionList, item string, futApi FutRestApi) { lock := helper.NewRedisLock(fmt.Sprintf(rediskey.FutTrigger, v.ApiId, v.Symbol), 20, 5, 100*time.Millisecond) if ok, err := lock.AcquireWait(context.Background()); err != nil { @@ -404,7 +409,7 @@ func FutAddPositionTrigger(db *gorm.DB, v *AddPositionList, item string, futApi defer lock.Release() setting, _ := cacheservice.GetSystemSetting(db) - tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, v.Symbol, 1) + tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, v.Symbol, 2) if tradeSet.LastPrice == "" { log.Errorf("合约加仓触发 查询交易对失败 交易对:%s ordersn:%s", v.Symbol, v.OrderSn) diff --git a/services/binanceservice/futuresrest.go b/services/binanceservice/futuresrest.go index c0c3ed1..a1b02a5 100644 --- a/services/binanceservice/futuresrest.go +++ b/services/binanceservice/futuresrest.go @@ -14,6 +14,7 @@ import ( "go-admin/models/positiondto" "go-admin/pkg/utility" "go-admin/services/cacheservice" + "go-admin/services/commonservice" "strings" "time" @@ -42,7 +43,7 @@ func ChangeFutureOrder(mapData map[string]interface{}) { } // 获取数据库连接 - db := GetDBConnection() + db := commonservice.GetDBConnection() if db == nil { logger.Error("合约订单回调失败,无法获取数据库连接") return @@ -127,7 +128,7 @@ func handleReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) { return } - tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 1) + tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 2) if err != nil { logger.Errorf("handleReduceFilled 获取交易对设置失败,订单号:%s", preOrder.OrderSn) @@ -266,7 +267,7 @@ func nextFuturesReduceTrigger(db *gorm.DB, mainId int, totalNum decimal.Decimal, //移除缓存 key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE) vals, _ := helper.DefaultRedis.GetAllList(key) - item := ReduceListItem{} + item := positiondto.ReduceListItem{} for _, val := range vals { sonic.Unmarshal([]byte(val), &item) @@ -438,8 +439,8 @@ func removeFutLossAndAddPosition(mainId int, orderSn string) { reduceKey := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE) reduceVal, _ := helper.DefaultRedis.GetAllList(reduceKey) stoploss := dto.StopLossRedisList{} - addPosition := AddPositionList{} - reduce := ReduceListItem{} + addPosition := positiondto.AddPositionList{} + reduce := positiondto.ReduceListItem{} //移除减仓后减仓策略 RemoveReduceReduceCacheByMainId(mainId, 2) @@ -483,7 +484,7 @@ func removeFutLossAndAddPosition(mainId int, orderSn string) { // 处理主单成交,处理止盈、止损、减仓订单 func handleFutMainOrderFilled(db *gorm.DB, preOrder *models.LinePreOrder, extOrderId int, first bool) { // 获取交易对配置和API信息 - tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 1) + tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 2) mainId := preOrder.Id if err != nil || tradeSet.Coin == "" { logger.Errorf("获取交易对配置失败, 回调订单号:%s, 错误信息: %v", preOrder.OrderSn, err) @@ -522,7 +523,7 @@ func handleFutMainOrderFilled(db *gorm.DB, preOrder *models.LinePreOrder, extOrd keyFutAddpositionKey := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE) for _, addPositionOrder := range addPositionOrders { - addPositionData := AddPositionList{ + addPositionData := positiondto.AddPositionList{ Id: addPositionOrder.Id, OrderSn: addPositionOrder.OrderSn, MainId: addPositionOrder.MainId, @@ -728,7 +729,7 @@ func updateOrderQuantity(db *gorm.DB, order models.LinePreOrder, preOrder *model // 减仓单 func processFutReduceOrder(order DbModels.LinePreOrder, price, num decimal.Decimal) { key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE) - item := ReduceListItem{ + item := positiondto.ReduceListItem{ Id: order.Id, ApiId: order.ApiId, Pid: order.Pid, @@ -756,7 +757,7 @@ func processFutReduceOrder(order DbModels.LinePreOrder, price, num decimal.Decim // 处理止盈订单 func processFutTakeProfitOrder(db *gorm.DB, futApi FutRestApi, order models.LinePreOrder, num decimal.Decimal) { price, _ := decimal.NewFromString(order.Price) - tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1) + tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 2) params := FutOrderPlace{ ApiId: order.ApiId, @@ -793,7 +794,7 @@ func processFutTakeProfitOrder(db *gorm.DB, futApi FutRestApi, order models.Line // 处理止损订单 // order 止损单 func processFutStopLossOrder(db *gorm.DB, order models.LinePreOrder, price, num decimal.Decimal) error { - tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1) + tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 2) params := FutOrderPlace{ ApiId: order.ApiId, diff --git a/services/binanceservice/models.go b/services/binanceservice/models.go index c2d04a5..8597342 100644 --- a/services/binanceservice/models.go +++ b/services/binanceservice/models.go @@ -186,19 +186,6 @@ type OpenOrders struct { GoodTillDate int `json:"goodTillDate"` //order pre-set auot cancel time for TIF GTD order } -// 待触发加仓单 -type AddPositionList struct { - Id int `json:"id"` //订单id - Pid int `json:"pid"` //父级id - MainId int `json:"mainId"` //主单Id - ApiId int `json:"apiId"` //触发账户id - Symbol string `json:"symbol"` //交易对 - Price decimal.Decimal `json:"price"` //触发价 - Side string `json:"side"` //买卖方向 - SymbolType int `json:"type" comment:"交易对类别 1-现货 2-合约"` - OrderSn string `json:"prderSn"` -} - // SpotAccountInfo 现货账户信息 type SpotAccountInfo struct { MakerCommission int `json:"makerCommission"` @@ -227,15 +214,3 @@ type SpotAccountInfo struct { Permissions []string `json:"permissions"` Uid int `json:"uid"` } - -type ReduceListItem struct { - Id int `json:"id"` - ApiId int `json:"apiId"` - MainId int `json:"mainId"` - Pid int `json:"pid"` - Symbol string `json:"symbol"` - Price decimal.Decimal `json:"price"` - Side string `json:"side"` - Num decimal.Decimal `json:"num"` - OrderSn string `json:"orderSn"` -} diff --git a/services/binanceservice/orderservice.go b/services/binanceservice/orderservice.go index ae3d7ba..5ae881a 100644 --- a/services/binanceservice/orderservice.go +++ b/services/binanceservice/orderservice.go @@ -1,15 +1,10 @@ package binanceservice import ( - "fmt" "go-admin/app/admin/models" DbModels "go-admin/app/admin/models" - "go-admin/pkg/utility" - "go-admin/pkg/utility/snowflakehelper" - "time" "github.com/go-admin-team/go-admin-core/logger" - "github.com/jinzhu/copier" "github.com/shopspring/decimal" "gorm.io/gorm" ) @@ -175,69 +170,3 @@ func GetChildTpOrder(db *gorm.DB, pid int) (int, error) { return int(count), nil } - -// 创建减仓后减仓单 -func CreateReduceReduceOrder(db *gorm.DB, pid int, price, num decimal.Decimal, priceDigit int) (models.LinePreOrder, error) { - var preOrder models.LinePreOrder - var result models.LinePreOrder - var ext models.LinePreOrderExt - - if err := db.Model(&models.LinePreOrder{}).Preload("Childs").Where("id =? ", pid).First(&preOrder).Error; err != nil { - return preOrder, err - } - - if err := db.Model(&models.LinePreOrderExt{}).Where("order_id =? ", pid).Find(&ext).Error; err != nil { - return preOrder, err - } - - copier.Copy(&result, &preOrder) - - result.Id = 0 - result.OrderSn = utility.Int64ToString(snowflakehelper.GetOrderId()) - result.Status = 0 - result.CreatedAt = time.Now() - result.TriggerTime = nil - result.UpdatedAt = time.Now() - result.BuyPrice = decimal.Zero.String() - result.Price = price.String() - result.Num = num.String() - result.ReduceOrderId = preOrder.Id - - for index := range result.Childs { - result.Childs[index].Id = 0 - result.Childs[index].OrderSn = utility.Int64ToString(snowflakehelper.GetOrderId()) - result.Childs[index].Status = 0 - result.Childs[index].CreatedAt = time.Now() - result.Childs[index].TriggerTime = nil - result.Childs[index].UpdatedAt = time.Now() - result.Childs[index].BuyPrice = decimal.Zero.String() - var pricePercent decimal.Decimal - - if result.Childs[index].OrderType == 1 && ext.TakeProfitRatio.Cmp(decimal.Zero) > 0 { - // 减仓单卖出 - if preOrder.Site == "SELL" { - pricePercent = decimal.NewFromInt(100).Add(ext.TakeProfitRatio).Div(decimal.NewFromInt(100)) - } else { - pricePercent = decimal.NewFromInt(100).Sub(ext.TakeProfitRatio).Div(decimal.NewFromInt(100)) - } - - } else if result.Childs[index].OrderType == 2 && ext.StopLossRatio.Cmp(decimal.Zero) > 0 { - if preOrder.Site == "SELL" { - pricePercent = decimal.NewFromInt(100).Sub(ext.StopLossRatio).Div(decimal.NewFromInt(100)) - } else { - pricePercent = decimal.NewFromInt(100).Add(ext.StopLossRatio).Div(decimal.NewFromInt(100)) - } - } - - //重新计算止盈止损价 - if pricePercent.Cmp(decimal.Zero) > 0 { - result.Childs[index].Price = price.Mul(pricePercent).Truncate(int32(priceDigit)).String() - } - } - - if err := db.Create(&result).Error; err != nil { - return result, fmt.Errorf("复制减仓单失败:pid:%d err:%v", pid, err) - } - - return result, nil -} diff --git a/services/binanceservice/reduce_order_strategy_service.go b/services/binanceservice/reduce_order_strategy_service.go index 84ee9df..be4a1e9 100644 --- a/services/binanceservice/reduce_order_strategy_service.go +++ b/services/binanceservice/reduce_order_strategy_service.go @@ -8,6 +8,7 @@ import ( "go-admin/common/global" "go-admin/common/helper" models2 "go-admin/models" + "go-admin/models/positiondto" "go-admin/pkg/utility" "github.com/bytedance/sonic" @@ -19,7 +20,7 @@ import ( // 缓存减仓节点和重新生成 // reduceOrder:原始减仓单 // reduceOrderStrategy:减仓策略 -func CacheOrderStrategyAndReCreate(db *gorm.DB, reduceOrder ReduceListItem, symbolType int, tradeSet models2.TradeSet, setting models.LineSystemSetting) error { +func CacheOrderStrategyAndReCreate(db *gorm.DB, reduceOrder positiondto.ReduceListItem, symbolType int, tradeSet models2.TradeSet, setting models.LineSystemSetting) error { reduceOrderStrategy := models.LineOrderReduceStrategy{} var key string diff --git a/services/binanceservice/spot_judge_service_test.go b/services/binanceservice/spot_judge_service_test.go index d1c954e..11927fa 100644 --- a/services/binanceservice/spot_judge_service_test.go +++ b/services/binanceservice/spot_judge_service_test.go @@ -6,6 +6,7 @@ import ( "go-admin/common/global" "go-admin/common/helper" "go-admin/models" + "go-admin/models/positiondto" "go-admin/services/cacheservice" "testing" @@ -29,7 +30,7 @@ func TestSpotJudge(t *testing.T) { key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE) item := `{"id":66,"apiId":49,"mainId":63,"pid":63,"symbol":"ADAUSDT","price":"0.5912","side":"SELL","num":"12.6","orderSn":"397919643961917440"}` - reduceOrder := ReduceListItem{} + reduceOrder := positiondto.ReduceListItem{} spotApi := SpotRestApi{} setting, err := cacheservice.GetSystemSetting(db) diff --git a/services/binanceservice/spotjudgeservice.go b/services/binanceservice/spotjudgeservice.go index 57b56ed..bd685fe 100644 --- a/services/binanceservice/spotjudgeservice.go +++ b/services/binanceservice/spotjudgeservice.go @@ -10,8 +10,11 @@ import ( "go-admin/common/global" "go-admin/common/helper" "go-admin/models" + "go-admin/models/positiondto" "go-admin/pkg/utility" "go-admin/services/cacheservice" + "go-admin/services/commonservice" + "go-admin/services/orderservice" "strings" "time" @@ -27,13 +30,16 @@ import ( func JudgeSpotPrice(trade models.TradeSet) { key := fmt.Sprintf(rediskey.PreSpotOrderList, global.EXCHANGE_BINANCE) preOrderVal, _ := helper.DefaultRedis.GetAllList(key) - db := GetDBConnection() + db := commonservice.GetDBConnection() if len(preOrderVal) == 0 { return } spotApi := SpotRestApi{} + orderService := orderservice.OrderService{} + orderService.Orm = db + for _, item := range preOrderVal { preOrder := dto.PreOrderRedisList{} if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil { @@ -46,6 +52,18 @@ func JudgeSpotPrice(trade models.TradeSet) { 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 { + //判断个人websocket是否超时 + + apiInfo, _ := GetApiInfo(preOrder.ApiId) + if err := commonservice.JudgeWebsocketTimeout(apiInfo.ApiKey, 1); err != nil { + log.Errorf("现货行情订阅超时,apiKey:%s err:%v", err) + + if err1 := orderService.ErrorTrigger(&preOrder, fmt.Sprintf("行情触发失败,err:%v", err)); err1 != nil { + log.Error("触发失败", err1) + } + continue + } + SpotOrderLock(db, &preOrder, item, spotApi) } } @@ -140,7 +158,7 @@ func JudgeSpotStopLoss(trade models.TradeSet) { return } - db := GetDBConnection() + db := commonservice.GetDBConnection() spotApi := SpotRestApi{} setting, err := cacheservice.GetSystemSetting(db) @@ -149,7 +167,7 @@ func JudgeSpotStopLoss(trade models.TradeSet) { return } - tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, trade.Coin+trade.Currency, 0) + tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, trade.Coin+trade.Currency, 1) if err != nil { log.Error("获取交易设置失败") @@ -257,7 +275,7 @@ func SpotStopLossTrigger(db *gorm.DB, stopOrder dto.StopLossRedisList, spotApi S // 判断是否触发现货减仓 func JudgeSpotReduce(trade models.TradeSet) { key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE) - db := GetDBConnection() + db := commonservice.GetDBConnection() spotApi := SpotRestApi{} setting, err := cacheservice.GetSystemSetting(db) @@ -266,12 +284,14 @@ func JudgeSpotReduce(trade models.TradeSet) { return } - reduceOrder := ReduceListItem{} + reduceOrder := positiondto.ReduceListItem{} tradePrice, _ := decimal.NewFromString(trade.LastPrice) //减仓单减仓策略 reduceReduceListKey := fmt.Sprintf(rediskey.SpotOrderReduceStrategyList, global.EXCHANGE_BINANCE) orderReduceVal, _ := helper.DefaultRedis.HGetAllFields(reduceReduceListKey) reduceOrderStrategy := dto.LineOrderReduceStrategyResp{} + orderService := orderservice.OrderService{} + orderService.Orm = db for _, item := range orderReduceVal { sonic.Unmarshal([]byte(item), &reduceOrderStrategy) @@ -297,7 +317,7 @@ func JudgeSpotReduce(trade models.TradeSet) { return } - order, err := CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit) + order, err := orderService.CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit) if err != nil { log.Errorf("%d 生成订单失败", reduceOrderStrategy.OrderId) @@ -367,8 +387,8 @@ func JudgeSpotReduce(trade models.TradeSet) { // 触发现货减仓 // isStrategy 是否是策略减仓单 // reduceId 策略主减仓单id -func SpotReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, spotApi SpotRestApi, setting DbModels.LineSystemSetting, key, item string, isStrategy bool, reduceId int) bool { - tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reduceOrder.Symbol, 0) +func SpotReduceTrigger(db *gorm.DB, reduceOrder positiondto.ReduceListItem, spotApi SpotRestApi, 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 { @@ -470,7 +490,7 @@ func SpotReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, spotApi SpotRest func JudgeSpotAddPosition(trade models.TradeSet) { key := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE) preOrderVal, _ := helper.DefaultRedis.GetAllList(key) - db := GetDBConnection() + db := commonservice.GetDBConnection() if len(preOrderVal) == 0 { return @@ -478,7 +498,7 @@ func JudgeSpotAddPosition(trade models.TradeSet) { spotApi := SpotRestApi{} for _, item := range preOrderVal { - preOrder := AddPositionList{} + preOrder := positiondto.AddPositionList{} if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil { log.Error("反序列化失败") continue @@ -495,7 +515,7 @@ func JudgeSpotAddPosition(trade models.TradeSet) { } } -func SpotAddPositionTrigger(db *gorm.DB, v *AddPositionList, item string, spotApi SpotRestApi) { +func SpotAddPositionTrigger(db *gorm.DB, v *positiondto.AddPositionList, item string, spotApi SpotRestApi) { lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, v.ApiId, v.Symbol), 20, 5, 100*time.Millisecond) if ok, err := lock.AcquireWait(context.Background()); err != nil { @@ -505,7 +525,7 @@ func SpotAddPositionTrigger(db *gorm.DB, v *AddPositionList, item string, spotAp defer lock.Release() setting, _ := cacheservice.GetSystemSetting(db) - tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, v.Symbol, 0) + tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, v.Symbol, 1) if tradeSet.LastPrice == "" { log.Errorf("现货加仓触发 查询交易对失败 交易对:%s ordersn:%s", v.Symbol, v.OrderSn) diff --git a/services/binanceservice/spotreset.go b/services/binanceservice/spotreset.go index d943c80..b867787 100644 --- a/services/binanceservice/spotreset.go +++ b/services/binanceservice/spotreset.go @@ -14,6 +14,7 @@ import ( "go-admin/models/positiondto" "go-admin/pkg/utility" "go-admin/services/cacheservice" + "go-admin/services/commonservice" "go-admin/services/positionservice" "strconv" "strings" @@ -44,7 +45,7 @@ func ChangeSpotOrder(mapData map[string]interface{}) { } // 获取数据库连接 - db := GetDBConnection() + db := commonservice.GetDBConnection() if db == nil { logger.Error("订单回调失败, 无法获取数据库连接") return @@ -166,7 +167,7 @@ func handleMainReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) { return } - tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 0) + tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 1) if err != nil { logger.Errorf("handleMainReduceFilled 获取交易对设置失败,订单号:%s", preOrder.OrderSn) @@ -300,7 +301,7 @@ func nextSpotReduceTrigger(db *gorm.DB, mainId int, totalNum decimal.Decimal, tr //移除缓存 key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE) vals, _ := helper.DefaultRedis.GetAllList(key) - item := ReduceListItem{} + item := positiondto.ReduceListItem{} for _, val := range vals { sonic.Unmarshal([]byte(val), &item) @@ -553,8 +554,8 @@ func removeSpotLossAndAddPosition(mainId int, orderSn string) { reduceKey := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE) reduceVal, _ := helper.DefaultRedis.GetAllList(reduceKey) stoploss := dto.StopLossRedisList{} - addPosition := AddPositionList{} - reduce := ReduceListItem{} + addPosition := positiondto.AddPositionList{} + reduce := positiondto.ReduceListItem{} //移除减仓后减仓策略 RemoveReduceReduceCacheByMainId(mainId, 1) @@ -646,7 +647,7 @@ func handleMainOrderFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) { keySpotAddPosition := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE) for _, addPositionOrder := range addPositionOrders { - addPositionData := AddPositionList{ + addPositionData := positiondto.AddPositionList{ Id: addPositionOrder.Id, OrderSn: addPositionOrder.OrderSn, MainId: addPositionOrder.MainId, @@ -744,7 +745,7 @@ func updateOrderStatus(db *gorm.DB, preOrder *models.LinePreOrder, status int, r // fist 首次止盈止损 func processTakeProfitAndStopLossOrders(db *gorm.DB, preOrder *models.LinePreOrder, positionData *positiondto.PositionDto, extOrderId int, fist bool) { orders := []models.LinePreOrder{} - tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 0) + tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 1) mainId := preOrder.Id if preOrder.MainId > 0 { @@ -855,7 +856,7 @@ func SetPrice(order *models.LinePreOrder, preOrder *models.LinePreOrder, tradeSe // 现货减仓 func processSpotReduceOrder(preOrder DbModels.LinePreOrder) { key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE) - item := ReduceListItem{ + item := positiondto.ReduceListItem{ Id: preOrder.Id, ApiId: preOrder.ApiId, Pid: preOrder.Pid, @@ -882,7 +883,7 @@ func processSpotReduceOrder(preOrder DbModels.LinePreOrder) { // 处理止盈订单 func processTakeProfitOrder(db *gorm.DB, spotApi SpotRestApi, order models.LinePreOrder) { - tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 0) + tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1) if tradeSet.Coin == "" { logger.Error("获取交易对失败") diff --git a/services/bitgetservice/bitget_client.go b/services/bitgetservice/bitget_client.go new file mode 100644 index 0000000..677aac6 --- /dev/null +++ b/services/bitgetservice/bitget_client.go @@ -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) +} diff --git a/services/bitgetservice/bitget_example.go b/services/bitgetservice/bitget_example.go new file mode 100644 index 0000000..23110c4 --- /dev/null +++ b/services/bitgetservice/bitget_example.go @@ -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("自定义订阅完成") +} diff --git a/services/bitgetservice/bitget_judge.go b/services/bitgetservice/bitget_judge.go new file mode 100644 index 0000000..de70af7 --- /dev/null +++ b/services/bitgetservice/bitget_judge.go @@ -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 +} diff --git a/services/bitgetservice/bitget_market_client.go b/services/bitgetservice/bitget_market_client.go new file mode 100644 index 0000000..62b82f3 --- /dev/null +++ b/services/bitgetservice/bitget_market_client.go @@ -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 +} diff --git a/services/bitgetservice/bitget_market_future.go b/services/bitgetservice/bitget_market_future.go new file mode 100644 index 0000000..c90f8e1 --- /dev/null +++ b/services/bitgetservice/bitget_market_future.go @@ -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 +} diff --git a/services/bitgetservice/bitget_market_models.go b/services/bitgetservice/bitget_market_models.go new file mode 100644 index 0000000..9fe03ce --- /dev/null +++ b/services/bitgetservice/bitget_market_models.go @@ -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, + } +} diff --git a/services/bitgetservice/bitget_market_spot.go b/services/bitgetservice/bitget_market_spot.go new file mode 100644 index 0000000..df06f6e --- /dev/null +++ b/services/bitgetservice/bitget_market_spot.go @@ -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 +} diff --git a/services/bitgetservice/bitget_market_test.go b/services/bitgetservice/bitget_market_test.go new file mode 100644 index 0000000..d8d1016 --- /dev/null +++ b/services/bitgetservice/bitget_market_test.go @@ -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) +} diff --git a/services/bitgetservice/bitget_rest.go b/services/bitgetservice/bitget_rest.go new file mode 100644 index 0000000..7f8cc09 --- /dev/null +++ b/services/bitgetservice/bitget_rest.go @@ -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 +} diff --git a/services/bitgetservice/bitget_rest_test.go b/services/bitgetservice/bitget_rest_test.go new file mode 100644 index 0000000..26250e1 --- /dev/null +++ b/services/bitgetservice/bitget_rest_test.go @@ -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) +} diff --git a/services/bitgetservice/bitget_socketmanager.go b/services/bitgetservice/bitget_socketmanager.go new file mode 100644 index 0000000..4a67677 --- /dev/null +++ b/services/bitgetservice/bitget_socketmanager.go @@ -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() + } +} diff --git a/services/cacheservice/config_service.go b/services/cacheservice/config_service.go index ab80e4d..3c1cfdb 100644 --- a/services/cacheservice/config_service.go +++ b/services/cacheservice/config_service.go @@ -64,16 +64,16 @@ func GetConfigCacheByKey(db *gorm.DB, key string) models.SysConfig { } // 获取缓存交易对 -// symbolType 0-现货 1-合约 +// symbolType 1-现货 2-合约 func GetTradeSet(exchangeType string, symbol string, symbolType int) (models2.TradeSet, error) { result := models2.TradeSet{} val := "" switch symbolType { - case 0: + case 1: key := fmt.Sprintf(global.TICKER_SPOT, exchangeType, symbol) val, _ = helper.DefaultRedis.GetString(key) - case 1: + case 2: key := fmt.Sprintf(global.TICKER_FUTURES, exchangeType, symbol) val, _ = helper.DefaultRedis.GetString(key) } @@ -89,6 +89,36 @@ func GetTradeSet(exchangeType string, symbol string, symbolType int) (models2.Tr return result, nil } +// 获取交易所所有交易对 +// symbolType 1-现货 2-合约 +func GetExchangeTradeSets(exchange string, symbolType int) ([]models2.TradeSet, error) { + result := []models2.TradeSet{} + val := []string{} + + switch symbolType { + case 1: + key := fmt.Sprintf(global.TICKER_SPOT, exchange, "*") + val, _ = helper.DefaultRedis.GetAllKeysAndValues(key) + case 2: + key := fmt.Sprintf(global.TICKER_FUTURES, exchange, "*") + val, _ = helper.DefaultRedis.GetAllKeysAndValues(key) + } + + for _, v := range val { + item := models2.TradeSet{} + if err := sonic.Unmarshal([]byte(v), &item); err != nil { + return result, err + } + result = append(result, item) + } + + if len(result) == 0 { + return result, errors.New("未找到交易对信息") + } + + return result, nil +} + func GetSystemSetting(db *gorm.DB) (models.LineSystemSetting, error) { key := fmt.Sprintf(rediskey.SystemSetting) val, _ := helper.DefaultRedis.GetString(key) diff --git a/services/commonservice/commonservice.go b/services/commonservice/commonservice.go new file mode 100644 index 0000000..9e54087 --- /dev/null +++ b/services/commonservice/commonservice.go @@ -0,0 +1,15 @@ +package commonservice + +import ( + "github.com/go-admin-team/go-admin-core/sdk" + "gorm.io/gorm" +) + +// 获取数据库连接 +func GetDBConnection() *gorm.DB { + dbs := sdk.Runtime.GetDb() + for _, db := range dbs { + return db + } + return nil +} diff --git a/services/commonservice/judge.go b/services/commonservice/judge.go new file mode 100644 index 0000000..989aa0d --- /dev/null +++ b/services/commonservice/judge.go @@ -0,0 +1,63 @@ +package commonservice + +import ( + "errors" + "fmt" + "go-admin/common/global" + "go-admin/common/helper" + "go-admin/models/binancedto" + "time" + + "github.com/bytedance/sonic" +) + +// 判断api订阅是否超时 +// wsType 1 现货 2 合约 +func JudgeWebsocketTimeout(apiKey string, wsType int) error { + key := fmt.Sprintf(global.USER_SUBSCRIBE, apiKey) + cache := binancedto.UserSubscribeState{} + // 从redis中获取订阅信息 + subscribeInfo, err := helper.DefaultRedis.GetString(key) + if err != nil { + return errors.New("获取订阅信息失败") + } + + sonic.Unmarshal([]byte(subscribeInfo), &cache) + + if cache.FuturesLastTime == nil && cache.SpotLastTime == nil { + return errors.New("未订阅任何行情") + } + + var timeOutInterval time.Duration + + if wsType == 1 { + timeOutInterval = time.Minute * 2 + } else { + timeOutInterval = time.Minute * 4 + } + + switch wsType { + case 1: + // 现货 + if cache.SpotLastTime == nil { + return errors.New("未订阅现货行情") + } + + if time.Since(*cache.SpotLastTime) > timeOutInterval { + return errors.New("现货行情订阅超时") + } + case 2: + // 合约 + if cache.FuturesLastTime == nil { + return errors.New("未订阅合约行情") + } + + if time.Since(*cache.FuturesLastTime) > timeOutInterval { + return errors.New("合约行情订阅超时") + } + default: + return errors.New("无效的wsType") + } + + return nil +} diff --git a/services/excservice/binancereceive.go b/services/excservice/binancereceive.go index f2a5fb5..4e5eef2 100644 --- a/services/excservice/binancereceive.go +++ b/services/excservice/binancereceive.go @@ -1,6 +1,7 @@ package excservice import ( + "fmt" "go-admin/models/futuresdto" "go-admin/pkg/utility" "go-admin/services/binanceservice" @@ -15,27 +16,43 @@ import ( - @msg 消息内容 - @listenType 订阅类型 0-现货 1-合约 */ -func ReceiveListen(msg []byte, listenType int, apiKey string) (reconnect bool, err error) { +// ReceiveListen 处理用户订单订阅 +// 返回: +// - reconnect: bool, 是否需要立即重连 (通常用于 listenKey 过期) +// - fatal: bool, 是否是致命错误 (例如认证失败),需要停止整个服务 +// - err: error, 解析或处理过程中的错误 +func ReceiveListen(msg []byte, listenType int, apiKey string) (reconnect bool, fatal bool, err error) { var dataMap map[string]interface{} err = sonic.Unmarshal(msg, &dataMap) - if err != nil { log.Error("接收ws 反序列化失败:", err) - return + return false, false, err + } + + // 检查是否是币安的错误消息, e.g. {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."} + if code, ok := dataMap["code"].(float64); ok && code != 0 { + errMsg, _ := dataMap["msg"].(string) + log.Errorf("收到币安错误码: %d, 消息: %s, API Key: %s", int(code), errMsg, apiKey) + // -2015 是无效API Key的错误码,这是一个致命错误,不需要重连 + if int(code) == -2015 { + return false, true, fmt.Errorf("币安认证失败 (code: %d): %s", int(code), errMsg) + } + // 其他错误码可能也需要特殊处理,但暂时我们只将-2015视为致命错误 + return false, false, nil } event, exits := dataMap["e"] - if !exits { - log.Error("不存在event") - return + // 如果没有 event 字段,但也不是一个错误消息,则可能是一个未知的消息格式 + log.Warn("收到的消息中不存在event字段: %s", string(msg)) + return false, false, nil } switch event { //listenKey过期 case "listenKeyExpired": log.Info("listenKey过期", string(msg)) - return true, nil + return true, false, nil //订单变更 case "ORDER_TRADE_UPDATE": @@ -101,11 +118,12 @@ func ReceiveListen(msg []byte, listenType int, apiKey string) (reconnect bool, e case "eventStreamTerminated": log.Info("账户数据流被终止 type:", getWsTypeName(listenType)) + return true, false, nil default: log.Info("未知事件 内容:", string(msg)) log.Info("未知事件", event) - return false, nil + return false, false, nil } - return false, nil + return false, false, nil } diff --git a/services/excservice/binancesocketmanager.go b/services/excservice/binancesocketmanager.go index 139cdd3..f3f6a15 100644 --- a/services/excservice/binancesocketmanager.go +++ b/services/excservice/binancesocketmanager.go @@ -2,7 +2,6 @@ package excservice import ( "context" - "crypto/tls" "errors" "fmt" "go-admin/common/global" @@ -11,12 +10,8 @@ import ( "go-admin/models/commondto" "go-admin/pkg/jsonhelper" "go-admin/pkg/utility" + "go-admin/services/proxy" "math/rand" - "net" - "net/http" - "net/url" - "os" - "os/signal" "strconv" "strings" "sync" @@ -26,32 +21,135 @@ import ( "github.com/bytedance/sonic" log "github.com/go-admin-team/go-admin-core/logger" "github.com/gorilla/websocket" - "golang.org/x/net/proxy" ) type BinanceWebSocketManager struct { - ws *websocket.Conn - stopChannel chan struct{} - url string - /* 0-现货 1-合约 */ - wsType int - apiKey string - apiSecret string - proxyType string - proxyAddress string - reconnect chan struct{} - isStopped bool // 标记 WebSocket 是否已主动停止 - mu sync.Mutex // 用于控制并发访问 isStopped - cancelFunc context.CancelFunc - listenKey string // 新增字段 - reconnecting atomic.Bool // 防止重复重连 - ConnectTime time.Time // 当前连接建立时间 + mu sync.Mutex + stopOnce sync.Once // 新增,确保Stop只执行一次 + ws *websocket.Conn + stopChannel chan struct{} + url string + wsType int + apiKey string + apiSecret string + proxyType string + proxyAddress string + reconnect chan struct{} + isStopped bool + cancelFunc context.CancelFunc + listenKey string + reconnecting atomic.Bool + ConnectTime time.Time + lastPingTime atomic.Value + forceReconnectTimer *time.Timer +} + +// ... (省略中间代码) + +// sendPing 使用锁来确保发送ping时的线程安全 +func (wm *BinanceWebSocketManager) sendPing() error { + wm.mu.Lock() + defer wm.mu.Unlock() + if wm.ws == nil { + return errors.New("websocket connection is nil") + } + wm.lastPingTime.Store(time.Now()) + return wm.ws.WriteMessage(websocket.PingMessage, []byte("ping")) +} + +// Stop 优雅地停止WebSocket管理器 +func (wm *BinanceWebSocketManager) Stop() { + wm.stopOnce.Do(func() { + wm.mu.Lock() + if wm.isStopped { + wm.mu.Unlock() + return + } + wm.isStopped = true + wm.mu.Unlock() + + if wm.cancelFunc != nil { + wm.cancelFunc() + } + + wm.closeConn() // 统一调用带锁的关闭方法 + + // 尝试关闭stopChannel,如果已经关闭则忽略 + select { + case <-wm.stopChannel: + default: + close(wm.stopChannel) + } + log.Infof("WebSocket 已完全停止 key:%s", wm.apiKey) + }) +} + +// handleReconnect 处理重连逻辑 +func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) { + const maxRetries = 10 + backoff := time.Second + + for { + select { + case <-wm.reconnect: + if !wm.reconnecting.CompareAndSwap(false, true) { + continue // 如果已经在重连,则忽略 + } + + // 在开始重连循环之前,先安全地关闭旧的连接 + wm.closeConn() + + retryCount := 0 + for retryCount < maxRetries { + select { + case <-wm.stopChannel: + wm.reconnecting.Store(false) + return + default: + } + + log.Infof("WebSocket (%s) 正在尝试重连,第 %d 次...", wm.wsType, retryCount+1) + if err := wm.connect(ctx); err == nil { + log.Infof("WebSocket (%s) 重连成功", wm.wsType) + wm.reconnecting.Store(false) + go wm.startDeadCheck(ctx) // 重连成功后,重新启动假死检测 + break // 跳出重连循环 + } + + retryCount++ + time.Sleep(backoff) + backoff *= 2 + if backoff > time.Minute { + backoff = time.Minute + } + } + + if retryCount >= maxRetries { + log.Errorf("WebSocket (%s) 重连失败次数过多,停止重连", wm.wsType) + wm.Stop() + return + } + + case <-ctx.Done(): + wm.reconnecting.Store(false) + return + } + } } // 已有连接 var SpotSockets = map[string]*BinanceWebSocketManager{} var FutureSockets = map[string]*BinanceWebSocketManager{} +/** + * 创建新的Binance WebSocket管理器 + * @param wsType WebSocket类型:0-现货,1-合约 + * @param apiKey API密钥 + * @param apiSecret API密钥 + * @param proxyType 代理类型 + * @param proxyAddress 代理地址 + * @return WebSocket管理器实例 + */ func NewBinanceWebSocketManager(wsType int, apiKey, apiSecret, proxyType, proxyAddress string) *BinanceWebSocketManager { url := "" @@ -62,7 +160,7 @@ func NewBinanceWebSocketManager(wsType int, apiKey, apiSecret, proxyType, proxyA url = "wss://fstream.binance.com/ws" } - return &BinanceWebSocketManager{ + wm := &BinanceWebSocketManager{ stopChannel: make(chan struct{}, 10), reconnect: make(chan struct{}, 10), isStopped: false, @@ -73,18 +171,36 @@ func NewBinanceWebSocketManager(wsType int, apiKey, apiSecret, proxyType, proxyA proxyType: proxyType, proxyAddress: proxyAddress, } + + // 初始化最后ping时间 + wm.lastPingTime.Store(time.Now()) + + return wm } +/** + * 获取API密钥 + * @return API密钥 + */ func (wm *BinanceWebSocketManager) GetKey() string { return wm.apiKey } +/** + * 启动WebSocket连接 + */ func (wm *BinanceWebSocketManager) Start() { utility.SafeGo(wm.run) - // wm.run() } -// 重启连接 +/** + * 重启连接,更新配置参数 + * @param apiKey 新的API密钥 + * @param apiSecret 新的API密钥 + * @param proxyType 新的代理类型 + * @param proxyAddress 新的代理地址 + * @return WebSocket管理器实例 + */ func (wm *BinanceWebSocketManager) Restart(apiKey, apiSecret, proxyType, proxyAddress string) *BinanceWebSocketManager { wm.mu.Lock() defer wm.mu.Unlock() @@ -105,7 +221,10 @@ func (wm *BinanceWebSocketManager) Restart(apiKey, apiSecret, proxyType, proxyAd return wm } -// 触发重连 +/** + * 触发重连机制 + * @param force 是否强制重连 + */ func (wm *BinanceWebSocketManager) triggerReconnect(force bool) { if force { wm.reconnecting.Store(false) // 强制重置标志位 @@ -122,15 +241,24 @@ func (wm *BinanceWebSocketManager) triggerReconnect(force bool) { } } } + +/** + * 外部重启函数 + * @param wm WebSocket管理器实例 + */ func Restart(wm *BinanceWebSocketManager) { log.Warnf("调用restart") wm.triggerReconnect(true) } + +/** + * 主运行循环 + */ func (wm *BinanceWebSocketManager) run() { ctx, cancel := context.WithCancel(context.Background()) wm.cancelFunc = cancel - utility.SafeGo(wm.handleSignal) + // utility.SafeGo(wm.handleSignal) // 计算错误记录键 errKey := fmt.Sprintf(global.API_WEBSOCKET_ERR, wm.apiKey) @@ -163,7 +291,11 @@ func (wm *BinanceWebSocketManager) run() { } } -// handleConnectionError 处理 WebSocket 连接错误 +/** + * 处理WebSocket连接错误 + * @param errKey Redis错误记录键 + * @param err 错误信息 + */ func (wm *BinanceWebSocketManager) handleConnectionError(errKey string, err error) { // 从 Redis 获取错误记录 var errMessage commondto.WebSocketErr @@ -186,7 +318,11 @@ func (wm *BinanceWebSocketManager) handleConnectionError(errKey string, err erro log.Error("连接 %s WebSocket 时出错: %v, 错误: %v", wm.wsType, wm.apiKey, err) } -// isErrorCountExceeded 检查错误次数是否超过阈值 +/** + * 检查错误次数是否超过阈值 + * @param errKey Redis错误记录键 + * @return 是否超过阈值 + */ func (wm *BinanceWebSocketManager) isErrorCountExceeded(errKey string) bool { val, _ := helper.DefaultRedis.GetString(errKey) if val == "" { @@ -201,16 +337,13 @@ func (wm *BinanceWebSocketManager) isErrorCountExceeded(errKey string) bool { return errMessage.Count >= 5 } -// 处理终止信号 -func (wm *BinanceWebSocketManager) handleSignal() { - ch := make(chan os.Signal) - signal.Notify(ch, os.Interrupt) - <-ch - wm.Stop() -} - +/** + * 建立WebSocket连接 + * @param ctx 上下文 + * @return 错误信息 + */ func (wm *BinanceWebSocketManager) connect(ctx context.Context) error { - dialer, err := wm.getDialer() + dialer, err := proxy.GetDialer(wm.proxyType, wm.proxyAddress) if err != nil { return err } @@ -227,8 +360,9 @@ func (wm *BinanceWebSocketManager) connect(ctx context.Context) error { return err } - // 连接成功,更新连接时间 + // 连接成功,更新连接时间和ping时间 wm.ConnectTime = time.Now() + wm.lastPingTime.Store(time.Now()) log.Info(fmt.Sprintf("已连接到 Binance %s WebSocket【%s】 key:%s", getWsTypeName(wm.wsType), wm.apiKey, listenKey)) // Ping处理 @@ -238,23 +372,25 @@ func (wm *BinanceWebSocketManager) connect(ctx context.Context) error { 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("binance 回应pong失败 次数:", strconv.Itoa(x), " err:", err) - time.Sleep(time.Second * 1) continue } - break } + // 更新ping时间和Redis记录 + wm.lastPingTime.Store(time.Now()) setLastTime(wm) return nil }) + // 启动各种协程 utility.SafeGo(func() { wm.startListenKeyRenewal2(ctx) }) utility.SafeGo(func() { wm.readMessages(ctx) }) utility.SafeGo(func() { wm.handleReconnect(ctx) }) utility.SafeGo(func() { wm.startPingLoop(ctx) }) - // utility.SafeGo(func() { wm.startDeadCheck(ctx) }) + utility.SafeGo(func() { wm.startDeadCheck(ctx) }) // 连接成功后立即启动假死检测 + utility.SafeGo(func() { wm.start24HourReconnectTimer(ctx) }) // 启动24小时强制重连 return nil } @@ -278,7 +414,7 @@ func (wm *BinanceWebSocketManager) ReplaceConnection() error { return fmt.Errorf("获取新 listenKey 失败: %w", err) } - dialer, err := wm.getDialer() + dialer, err := proxy.GetDialer(wm.proxyType, wm.proxyAddress) if err != nil { return err } @@ -360,54 +496,6 @@ func setLastTime(wm *BinanceWebSocketManager) { } -func (wm *BinanceWebSocketManager) 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) - } - - transport := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, - } - - switch proxyURL.Scheme { - case "socks5": - return wm.createSocks5Dialer(proxyURL) - case "http", "https": - transport.Proxy = http.ProxyURL(proxyURL) - return &websocket.Dialer{Proxy: transport.Proxy, TLSClientConfig: transport.TLSClientConfig}, nil - default: - return nil, fmt.Errorf("unsupported proxy scheme: %s", proxyURL.Scheme) - } -} - -func (wm *BinanceWebSocketManager) 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, address string) (net.Conn, error) { - return socksDialer.Dial(network, address) - }, - }, nil -} - // 复用创建HTTP客户端的逻辑 func (wm *BinanceWebSocketManager) createBinanceClient() (*helper.BinanceClient, error) { return helper.NewBinanceClient(wm.apiKey, wm.apiSecret, wm.proxyType, wm.proxyAddress) @@ -450,7 +538,6 @@ func (wm *BinanceWebSocketManager) getListenKey() (string, error) { // 接收消息 func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) { - defer wm.ws.Close() for { select { @@ -462,20 +549,23 @@ func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) { } _, msg, err := wm.ws.ReadMessage() - if err != nil && strings.Contains(err.Error(), "websocket: close") { - if !wm.isStopped { - log.Error("收到关闭消息", err.Error()) - wm.triggerReconnect(false) + if err != nil { + // 检查是否是由于我们主动关闭连接导致的错误 + if wm.isStopped { + log.Infof("WebSocket read loop gracefully stopped for key: %s", wm.apiKey) + return } - log.Error("websocket 关闭") - return - } else if err != nil { - log.Error("读取消息时出错: %v", err) - return + // 如果不是主动关闭,再判断是否是连接关闭错误,并触发重连 + if strings.Contains(err.Error(), "websocket: close") { + log.Error("WebSocket connection closed unexpectedly, triggering reconnect for key: %s. Error: %v", wm.apiKey, err) + wm.triggerReconnect(false) + } else { + log.Error("Error reading message for key: %s. Error: %v", wm.apiKey, err) + } + return // 任何错误发生后都应退出读取循环 } wm.handleOrderUpdate(msg) - } } } @@ -483,165 +573,32 @@ func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) { func (wm *BinanceWebSocketManager) handleOrderUpdate(msg []byte) { setLastTime(wm) - if reconnect, _ := ReceiveListen(msg, wm.wsType, wm.apiKey); reconnect { - log.Errorf("收到重连请求") + if reconnect, fatal, err := ReceiveListen(msg, wm.wsType, wm.apiKey); err != nil { + log.Errorf("处理消息时出错: %v", err) + // 根据错误类型决定是否停止 + if fatal { + log.Errorf("收到致命错误,将停止 WebSocket 管理器 for key: %s", wm.apiKey) + wm.Stop() + } + return + } else if fatal { + log.Errorf("收到致命错误,将停止 WebSocket 管理器 for key: %s", wm.apiKey) + wm.Stop() + return + } else if reconnect { + log.Warnf("收到重连请求,将触发重连 for key: %s", wm.apiKey) wm.triggerReconnect(false) } } -// Stop 安全停止 WebSocket -func (wm *BinanceWebSocketManager) Stop() { +// closeConn 安全地关闭WebSocket连接 +func (wm *BinanceWebSocketManager) closeConn() { wm.mu.Lock() defer wm.mu.Unlock() - - if wm.isStopped { - return - } - wm.isStopped = true - - select { - case <-wm.stopChannel: - default: - close(wm.stopChannel) - } - - if wm.cancelFunc != nil { - wm.cancelFunc() - wm.cancelFunc = nil - } - if wm.ws != nil { - if err := wm.ws.Close(); err != nil { - log.Errorf("WebSocket Close 错误 key:%s err:%v", wm.apiKey, err) - } + _ = wm.ws.Close() wm.ws = nil } - - log.Infof("WebSocket 已完全停止 key:%s", wm.apiKey) - wm.stopChannel = make(chan struct{}, 10) -} - -// handleReconnect 使用指数退避并保持永不退出 -func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) { - const maxRetries = 100 - baseDelay := time.Second * 2 - retryCount := 0 - - for { - select { - case <-ctx.Done(): - log.Infof("handleReconnect context done: %s", wm.apiKey) - return - - case <-wm.reconnect: - wm.mu.Lock() - if wm.isStopped { - wm.mu.Unlock() - return - } - wm.mu.Unlock() - - log.Warnf("WebSocket 连接断开,准备重连 key:%s", wm.apiKey) - - for { - wm.mu.Lock() - if wm.ws != nil { - _ = wm.ws.Close() - wm.ws = nil - } - if wm.cancelFunc != nil { - wm.cancelFunc() - wm.cancelFunc = nil - } - wm.mu.Unlock() - - newCtx, cancel := context.WithCancel(context.Background()) - wm.mu.Lock() - wm.cancelFunc = cancel - wm.mu.Unlock() - - if err := wm.connect(newCtx); err != nil { - log.Errorf("🔌 重连失败(%d/%d)key:%s,err: %v", retryCount+1, maxRetries, wm.apiKey, err) - cancel() - retryCount++ - - if retryCount >= maxRetries { - log.Errorf("❌ 重连失败次数过多,停止重连逻辑 key:%s", wm.apiKey) - wm.reconnecting.Store(false) - return - } - - delay := baseDelay * time.Duration(1< time.Minute*5 { - delay = time.Minute * 5 - } - log.Warnf("等待 %v 后重试...", delay) - time.Sleep(delay) - continue - } - - log.Infof("✅ 重连成功 key:%s", wm.apiKey) - retryCount = 0 - wm.reconnecting.Store(false) - - // ✅ 重连成功后开启假死检测 - utility.SafeGo(func() { wm.startDeadCheck(newCtx) }) - - break - } - } - } -} - -// startDeadCheck 替代 Start 中的定时器,绑定连接生命周期 -func (wm *BinanceWebSocketManager) startDeadCheck(ctx context.Context) { - ticker := time.NewTicker(1 * time.Minute) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - if wm.isStopped { - return - } - wm.DeadCheck() - } - } -} - -// 假死检测 -func (wm *BinanceWebSocketManager) DeadCheck() { - subKey := fmt.Sprintf(global.USER_SUBSCRIBE, wm.apiKey) - val, _ := helper.DefaultRedis.GetString(subKey) - if val == "" { - log.Warnf("没有订阅信息,无法进行假死检测") - return - } - - var data binancedto.UserSubscribeState - _ = sonic.Unmarshal([]byte(val), &data) - - var lastTime *time.Time - if wm.wsType == 0 { - lastTime = data.SpotLastTime - } else { - lastTime = data.FuturesLastTime - } - - // 定义最大静默时间(超出视为假死) - var timeout time.Duration - if wm.wsType == 0 { - timeout = 40 * time.Second // Spot 每 20s ping,40s 足够 - } else { - timeout = 6 * time.Minute // Futures 每 3 分钟 ping - } - - if lastTime != nil && time.Since(*lastTime) > timeout { - log.Warnf("检测到假死连接 key:%s type:%v, 距离上次通信: %v, 触发重连", wm.apiKey, wm.wsType, time.Since(*lastTime)) - wm.triggerReconnect(true) - } } // 主动心跳发送机制 @@ -657,10 +614,10 @@ func (wm *BinanceWebSocketManager) startPingLoop(ctx context.Context) { if wm.isStopped { return } - err := wm.ws.WriteMessage(websocket.PingMessage, []byte("ping")) - if err != nil { + if err := wm.sendPing(); err != nil { log.Error("主动 Ping Binance 失败:", err) wm.triggerReconnect(false) + return // 退出循环,等待重连 } } } @@ -784,3 +741,112 @@ func randomHex(n int) string { } return string(bytes) } + +/** + * 启动24小时强制重连定时器 + * @param ctx 上下文 + */ +func (wm *BinanceWebSocketManager) start24HourReconnectTimer(ctx context.Context) { + // Binance要求不到24小时就需要主动断开重连 + // 设置为23小时50分钟,留出一些缓冲时间 + duration := 23*time.Hour + 50*time.Minute + + wm.forceReconnectTimer = time.NewTimer(duration) + defer func() { + if wm.forceReconnectTimer != nil { + wm.forceReconnectTimer.Stop() + } + }() + + select { + case <-ctx.Done(): + return + case <-wm.forceReconnectTimer.C: + if !wm.isStopped { + log.Warnf("24小时强制重连触发 key:%s wsType:%v", wm.apiKey, wm.wsType) + wm.triggerReconnect(true) + } + } +} + +/** + * 改进的假死检测机制 + * @param ctx 上下文 + */ +func (wm *BinanceWebSocketManager) startDeadCheck(ctx context.Context) { + // 缩短检测间隔,提高响应速度 + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if wm.isStopped { + return + } + wm.DeadCheck() + } + } +} + +/** + * 改进的假死检测逻辑 + */ +func (wm *BinanceWebSocketManager) DeadCheck() { + // 检查最后ping时间 + lastPing := wm.lastPingTime.Load().(time.Time) + + // 定义最大静默时间(根据WebSocket类型调整) + var timeout time.Duration + if wm.wsType == 0 { + timeout = 3 * time.Minute // 现货:3分钟无ping视为假死 + } else { + timeout = 6 * time.Minute // 合约:6分钟无ping视为假死 + } + + if time.Since(lastPing) > timeout { + log.Warnf("检测到假死连接(基于ping时间) key:%s type:%v, 距离上次ping: %v, 触发重连", + wm.apiKey, wm.wsType, time.Since(lastPing)) + wm.triggerReconnect(true) + return + } + + // 同时检查Redis中的记录作为备用检测 + subKey := fmt.Sprintf(global.USER_SUBSCRIBE, wm.apiKey) + val, _ := helper.DefaultRedis.GetString(subKey) + if val == "" { + log.Debugf("没有订阅信息,跳过Redis假死检测") + return + } + + var data binancedto.UserSubscribeState + _ = sonic.Unmarshal([]byte(val), &data) + + var lastTime *time.Time + if wm.wsType == 0 { + lastTime = data.SpotLastTime + } else { + lastTime = data.FuturesLastTime + } + + // Redis记录的超时时间设置得更长一些 + var redisTimeout time.Duration + if wm.wsType == 0 { + redisTimeout = 3 * time.Minute + } else { + redisTimeout = 6 * time.Minute + } + + if lastTime != nil && time.Since(*lastTime) > redisTimeout { + log.Warnf("检测到假死连接(基于Redis记录) key:%s type:%v, 距离上次通信: %v, 触发重连", + wm.apiKey, wm.wsType, time.Since(*lastTime)) + wm.triggerReconnect(true) + } +} + +/** + * 优化的重连处理逻辑 + * @param ctx 上下文 + */ diff --git a/services/excservice/binancews.go b/services/excservice/binancews.go index a0547a4..c5f46be 100644 --- a/services/excservice/binancews.go +++ b/services/excservice/binancews.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "strconv" + "sync" "time" "go-admin/models" @@ -19,6 +20,7 @@ type BinanceWs struct { proxyUrl string WorkType string wsConns []*WsConn + mu sync.Mutex // 新增互斥锁 tickerCallback func(models.Ticker24, string, string) forceCallback func(models.ForceOrder, string, string) depthCallback func(models.DepthBin, string, string) @@ -70,11 +72,15 @@ func (bnWs *BinanceWs) subscribe(endpoint string, handle func(msg []byte) error) if wsConn == nil { return } + bnWs.mu.Lock() bnWs.wsConns = append(bnWs.wsConns, wsConn) + bnWs.mu.Unlock() go bnWs.exitHandler(wsConn) } func (bnWs *BinanceWs) Close() { + bnWs.mu.Lock() + defer bnWs.mu.Unlock() for _, con := range bnWs.wsConns { con.CloseWs() } diff --git a/services/excservice/bitget_receive.go b/services/excservice/bitget_receive.go new file mode 100644 index 0000000..e742996 --- /dev/null +++ b/services/excservice/bitget_receive.go @@ -0,0 +1,218 @@ +package excservice + +import ( + "go-admin/pkg/utility" + + "github.com/bytedance/sonic" + log "github.com/go-admin-team/go-admin-core/logger" +) + +/* +Bitget用户订单订阅处理 + - @msg 消息内容 + - @listenType 订阅类型 0-现货 1-合约 +*/ +func ReceiveBitgetListen(msg []byte, listenType int, apiKey string) (reconnect bool, err error) { + var dataMap map[string]interface{} + err = sonic.Unmarshal(msg, &dataMap) + + if err != nil { + log.Error("接收Bitget ws 反序列化失败:", err) + return + } + + // 检查是否是操作响应 + if op, exists := dataMap["op"]; exists { + switch op { + case "login": + return handleBitgetLogin(dataMap, apiKey) + case "subscribe": + return handleBitgetSubscribe(dataMap, apiKey) + case "unsubscribe": + return handleBitgetUnsubscribe(dataMap, apiKey) + } + } + + // 检查是否是数据推送 + if arg, exists := dataMap["arg"]; exists { + return handleBitgetDataPush(dataMap, arg, listenType, apiKey) + } + + log.Info("未知Bitget事件 内容:", string(msg)) + return false, nil +} + +// handleBitgetLogin 处理登录响应 +func handleBitgetLogin(dataMap map[string]interface{}, apiKey string) (bool, error) { + if code, exists := dataMap["code"]; exists { + if code == "0" || code == 0 { + log.Info("Bitget WebSocket登录成功 key:%s", apiKey) + } else { + log.Error("Bitget WebSocket登录失败 key:%s code:%v", apiKey, code) + return true, nil // 需要重连 + } + } + return false, nil +} + +// handleBitgetSubscribe 处理订阅响应 +func handleBitgetSubscribe(dataMap map[string]interface{}, apiKey string) (bool, error) { + if code, exists := dataMap["code"]; exists { + if code == "0" || code == 0 { + log.Info("Bitget WebSocket订阅成功 key:%s", apiKey) + } else { + log.Error("Bitget WebSocket订阅失败 key:%s code:%v", apiKey, code) + } + } + return false, nil +} + +// handleBitgetUnsubscribe 处理取消订阅响应 +func handleBitgetUnsubscribe(dataMap map[string]interface{}, apiKey string) (bool, error) { + if code, exists := dataMap["code"]; exists { + if code == "0" || code == 0 { + log.Info("Bitget WebSocket取消订阅成功 key:%s", apiKey) + } else { + log.Error("Bitget WebSocket取消订阅失败 key:%s code:%v", apiKey, code) + } + } + return false, nil +} + +// handleBitgetDataPush 处理数据推送 +func handleBitgetDataPush(dataMap map[string]interface{}, arg interface{}, listenType int, apiKey string) (bool, error) { + argMap, ok := arg.(map[string]interface{}) + if !ok { + log.Error("Bitget数据推送参数格式错误 key:%s", apiKey) + return false, nil + } + + channel, exists := argMap["channel"] + if !exists { + log.Error("Bitget数据推送缺少channel参数 key:%s", apiKey) + return false, nil + } + + switch channel { + case "account": + return handleBitgetAccount(dataMap, listenType, apiKey) + case "orders": + return handleBitgetOrders(dataMap, listenType, apiKey) + case "positions": + return handleBitgetPositions(dataMap, listenType, apiKey) + case "balance": + return handleBitgetBalance(dataMap, listenType, apiKey) + default: + log.Info("收到Bitget未处理的频道数据 channel:%v key:%s", channel, apiKey) + } + + return false, nil +} + +// handleBitgetAccount 处理账户更新 +func handleBitgetAccount(dataMap map[string]interface{}, listenType int, apiKey string) (bool, error) { + log.Info("收到Bitget账户更新 listenType:%d key:%s", listenType, apiKey) + + data, exists := dataMap["data"] + if !exists { + return false, nil + } + + // 这里可以根据需要实现具体的账户更新处理逻辑 + // 类似于binanceservice.ChangeSpotOrder或binanceservice.ChangeFutureOrder + + utility.SafeGo(func() { + handleBitgetAccountUpdate(data, listenType, apiKey) + }) + + return false, nil +} + +// handleBitgetOrders 处理订单更新 +func handleBitgetOrders(dataMap map[string]interface{}, listenType int, apiKey string) (bool, error) { + log.Info("收到Bitget订单更新 listenType:%d key:%s", listenType, apiKey) + + data, exists := dataMap["data"] + if !exists { + return false, nil + } + + utility.SafeGo(func() { + handleBitgetOrderUpdate(data, listenType, apiKey) + }) + + return false, nil +} + +// handleBitgetPositions 处理持仓更新 +func handleBitgetPositions(dataMap map[string]interface{}, listenType int, apiKey string) (bool, error) { + log.Info("收到Bitget持仓更新 listenType:%d key:%s", listenType, apiKey) + + data, exists := dataMap["data"] + if !exists { + return false, nil + } + + utility.SafeGo(func() { + handleBitgetPositionUpdate(data, listenType, apiKey) + }) + + return false, nil +} + +// handleBitgetBalance 处理余额更新 +func handleBitgetBalance(dataMap map[string]interface{}, listenType int, apiKey string) (bool, error) { + log.Info("收到Bitget余额更新 listenType:%d key:%s", listenType, apiKey) + + data, exists := dataMap["data"] + if !exists { + return false, nil + } + + utility.SafeGo(func() { + handleBitgetBalanceUpdate(data, listenType, apiKey) + }) + + return false, nil +} + +// 具体的处理函数,可以根据业务需求实现 + +// handleBitgetAccountUpdate 处理账户更新的具体逻辑 +func handleBitgetAccountUpdate(data interface{}, listenType int, apiKey string) { + // TODO: 实现具体的账户更新处理逻辑 + log.Info("处理Bitget账户更新 listenType:%d key:%s data:%v", listenType, apiKey, data) +} + +// handleBitgetOrderUpdate 处理订单更新的具体逻辑 +func handleBitgetOrderUpdate(data interface{}, listenType int, apiKey string) { + // TODO: 实现具体的订单更新处理逻辑 + log.Info("处理Bitget订单更新 listenType:%d key:%s data:%v", listenType, apiKey, data) + + // 这里可以参考binanceservice中的订单处理逻辑 + // 例如:更新数据库中的订单状态、触发回调等 +} + +// handleBitgetPositionUpdate 处理持仓更新的具体逻辑 +func handleBitgetPositionUpdate(data interface{}, listenType int, apiKey string) { + // TODO: 实现具体的持仓更新处理逻辑 + log.Info("处理Bitget持仓更新 listenType:%d key:%s data:%v", listenType, apiKey, data) +} + +// handleBitgetBalanceUpdate 处理余额更新的具体逻辑 +func handleBitgetBalanceUpdate(data interface{}, listenType int, apiKey string) { + // TODO: 实现具体的余额更新处理逻辑 + log.Info("处理Bitget余额更新 listenType:%d key:%s data:%v", listenType, apiKey, data) +} + +// getBitgetWsTypeName 获取WebSocket类型名称 +func getBitgetWsTypeName(listenType int) string { + switch listenType { + case 0: + return "现货" + case 1: + return "合约" + default: + return "未知" + } +} diff --git a/services/orderservice/order_service.go b/services/orderservice/order_service.go new file mode 100644 index 0000000..9ef35e6 --- /dev/null +++ b/services/orderservice/order_service.go @@ -0,0 +1,108 @@ +package orderservice + +import ( + "fmt" + "go-admin/app/admin/models" + "go-admin/app/admin/service/dto" + "go-admin/pkg/utility" + "go-admin/pkg/utility/snowflakehelper" + "time" + + "github.com/go-admin-team/go-admin-core/sdk/service" + "github.com/jinzhu/copier" + "github.com/shopspring/decimal" + "gorm.io/gorm" +) + +type OrderService struct { + service.Service +} + +// 创建减仓后减仓单 +func (e *OrderService) CreateReduceReduceOrder(db *gorm.DB, pid int, price, num decimal.Decimal, priceDigit int) (models.LinePreOrder, error) { + var preOrder models.LinePreOrder + var result models.LinePreOrder + var ext models.LinePreOrderExt + + if err := db.Model(&models.LinePreOrder{}).Preload("Childs").Where("id =? ", pid).First(&preOrder).Error; err != nil { + return preOrder, err + } + + if err := db.Model(&models.LinePreOrderExt{}).Where("order_id =? ", pid).Find(&ext).Error; err != nil { + return preOrder, err + } + + copier.Copy(&result, &preOrder) + + result.Id = 0 + result.OrderSn = utility.Int64ToString(snowflakehelper.GetOrderId()) + result.Status = 0 + result.CreatedAt = time.Now() + result.TriggerTime = nil + result.UpdatedAt = time.Now() + result.BuyPrice = decimal.Zero.String() + result.Price = price.String() + result.Num = num.String() + result.ReduceOrderId = preOrder.Id + + for index := range result.Childs { + result.Childs[index].Id = 0 + result.Childs[index].OrderSn = utility.Int64ToString(snowflakehelper.GetOrderId()) + result.Childs[index].Status = 0 + result.Childs[index].CreatedAt = time.Now() + result.Childs[index].TriggerTime = nil + result.Childs[index].UpdatedAt = time.Now() + result.Childs[index].BuyPrice = decimal.Zero.String() + var pricePercent decimal.Decimal + + if result.Childs[index].OrderType == 1 && ext.TakeProfitRatio.Cmp(decimal.Zero) > 0 { + // 减仓单卖出 + if preOrder.Site == "SELL" { + pricePercent = decimal.NewFromInt(100).Add(ext.TakeProfitRatio).Div(decimal.NewFromInt(100)) + } else { + pricePercent = decimal.NewFromInt(100).Sub(ext.TakeProfitRatio).Div(decimal.NewFromInt(100)) + } + + } else if result.Childs[index].OrderType == 2 && ext.StopLossRatio.Cmp(decimal.Zero) > 0 { + if preOrder.Site == "SELL" { + pricePercent = decimal.NewFromInt(100).Sub(ext.StopLossRatio).Div(decimal.NewFromInt(100)) + } else { + pricePercent = decimal.NewFromInt(100).Add(ext.StopLossRatio).Div(decimal.NewFromInt(100)) + } + } + + //重新计算止盈止损价 + if pricePercent.Cmp(decimal.Zero) > 0 { + result.Childs[index].Price = price.Mul(pricePercent).Truncate(int32(priceDigit)).String() + } + } + + if err := db.Create(&result).Error; err != nil { + return result, fmt.Errorf("复制减仓单失败:pid:%d err:%v", pid, err) + } + + return result, nil +} + +// 触发主单失败 +func (e *OrderService) ErrorTrigger(preOrder *dto.PreOrderRedisList, errData string) error { + // var cacheOrderKey string + + // if preOrder.OrderType == 1 { + // cacheOrderKey = fmt.Sprintf(rediskey.PreSpotOrderList, preOrder.ExchangeType) + // } else { + // cacheOrderKey = fmt.Sprintf(rediskey.PreFutOrderList, preOrder.ExchangeType) + // } + + if err := e.Orm.Model(&models.LinePreOrder{}). + Where("id = ? and status =1", preOrder.Id). + Updates(map[string]interface{}{ + "status": 2, + "desc": errData, + "trigger_time": time.Now(), + }).Error; err != nil { + return err + } + + return nil +} diff --git a/services/proxy/proxy.go b/services/proxy/proxy.go new file mode 100644 index 0000000..aada0a0 --- /dev/null +++ b/services/proxy/proxy.go @@ -0,0 +1,63 @@ +package proxy + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "net/http" + "net/url" + "strings" + + "github.com/gorilla/websocket" + "golang.org/x/net/proxy" +) + +// GetDialer 获取代理拨号器 +func GetDialer(proxyType, proxyAddress string) (*websocket.Dialer, error) { + if proxyAddress == "" { + return &websocket.Dialer{}, nil + } + + if !strings.HasPrefix(proxyAddress, "http://") && !strings.HasPrefix(proxyAddress, "https://") && !strings.HasPrefix(proxyAddress, "socks5://") { + proxyAddress = proxyType + "://" + proxyAddress + } + + proxyURL, err := url.Parse(proxyAddress) + if err != nil { + return nil, fmt.Errorf("failed to parse proxy URL: %v", err) + } + + transport := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: false}, + } + + switch proxyURL.Scheme { + case "socks5": + return createSocks5Dialer(proxyURL) + case "http", "https": + transport.Proxy = http.ProxyURL(proxyURL) + return &websocket.Dialer{Proxy: transport.Proxy, TLSClientConfig: transport.TLSClientConfig}, nil + default: + return nil, fmt.Errorf("unsupported proxy scheme: %s", proxyURL.Scheme) + } +} + +func 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, address string) (net.Conn, error) { + return socksDialer.Dial(network, address) + }, + }, nil +} diff --git a/services/spotservice/binancemarket.go b/services/spotservice/binancemarket.go index 8759ad1..6568a03 100644 --- a/services/spotservice/binancemarket.go +++ b/services/spotservice/binancemarket.go @@ -182,14 +182,7 @@ func handleTickerMessage(msg []byte) { return } - pairs := make([]map[string]interface{}, 0) trades := make([]models.TradeSet, 0) - pairsVal, _ := helper.DefaultRedis.GetString(rediskey.SpotSymbolTicker) - - if err := sonic.UnmarshalString(pairsVal, &pairs); err != nil { - log.Error("解码ticker消息失败", zap.Error(err)) - return - } for _, dataMap := range streamResp.Data { symbolName, ok := dataMap["s"].(string) @@ -232,15 +225,6 @@ func handleTickerMessage(msg []byte) { tradeSet.Volume = utility.StringFloat64Cut(dataMap["v"].(string), int32(tradeSet.AmountDigit)) tradeSet.QuoteVolume = utility.StringFloat64Cut(dataMap["q"].(string), 5) - for index := range pairs { - if symbol, ok := pairs[index]["symbol"].(string); ok { - if symbol == symbolName { - pairs[index]["price"] = tradeSet.LastPrice - break - } - } - } - trades = append(trades, tradeSet) tcVal, _ = sonic.MarshalString(&tradeSet) diff --git a/services/tradsetservice/trade_set.go b/services/tradsetservice/trade_set.go new file mode 100644 index 0000000..7b96bc8 --- /dev/null +++ b/services/tradsetservice/trade_set.go @@ -0,0 +1,68 @@ +package tradsetservice + +import ( + "fmt" + "go-admin/common/global" + "go-admin/common/helper" + "go-admin/models" + + "github.com/bytedance/sonic" + "github.com/go-admin-team/go-admin-core/logger" +) + +// 获取交易所交易对 +// symbolType 1:spot 2:future +func GetExchangeTradeSets(exchange string, symbolType int) (*[]models.TradeSet, error) { + var tradeSets []models.TradeSet + var scanKey string + + if symbolType == 1 { + scanKey = fmt.Sprintf(global.TICKER_SPOT, exchange, "*") + } else if symbolType == 2 { + scanKey = fmt.Sprintf(global.TICKER_FUTURES, exchange, "*") + } else { + return nil, fmt.Errorf("symbolType error") + } + + vals, err := helper.DefaultRedis.GetAllKeysAndValues(scanKey) + if err != nil { + return nil, err + } + + for _, val := range vals { + var tradeSet models.TradeSet + if err := sonic.Unmarshal([]byte(val), &tradeSet); err != nil { + logger.Errorf("unmarshal tradeSet err:%v", err) + continue + } + tradeSets = append(tradeSets, tradeSet) + } + + return &tradeSets, nil +} + +// 获取单个交易对 +func GetExchangeTradeSet(exchange string, symbolType int, symbol string) (*models.TradeSet, error) { + var tradeSet models.TradeSet + var scanKey string + + if symbolType == 1 { + scanKey = fmt.Sprintf(global.TICKER_SPOT, exchange, symbol) + } else if symbolType == 2 { + scanKey = fmt.Sprintf(global.TICKER_FUTURES, exchange, symbol) + } else { + return nil, fmt.Errorf("symbolType error") + } + + val, err := helper.DefaultRedis.GetString(scanKey) + if err != nil { + return nil, err + } + + if err := sonic.Unmarshal([]byte(val), &tradeSet); err != nil { + logger.Errorf("unmarshal tradeSet err:%v", err) + return nil, err + } + + return &tradeSet, nil +} diff --git a/test/init_settings.go b/test/init_settings.go new file mode 100644 index 0000000..9995a33 --- /dev/null +++ b/test/init_settings.go @@ -0,0 +1,22 @@ +package test + +import ( + "go-admin/common/helper" + "go-admin/config" + + "github.com/go-admin-team/go-admin-core/sdk" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func InitSettings() *gorm.DB { + 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" + + return db +}