1、暂存

This commit is contained in:
2025-10-14 19:58:59 +08:00
parent 556a32cb7c
commit 643eab3496
60 changed files with 5244 additions and 657 deletions

View File

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

View File

@ -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) {

View File

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

View File

@ -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"`

View File

@ -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 == "" {

View File

@ -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) {

View File

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

View File

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

View File

@ -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)

264
cmd/binance/binance.go Normal file
View File

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

271
cmd/bitget/bitget.go Normal file
View File

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

View File

@ -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)

View File

@ -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"

View File

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

View File

@ -3,6 +3,7 @@ package global
//交易所类型字典
const (
EXCHANGE_BINANCE = "binance"
EXCHANGE_BITGET = "bitget"
EXCHANGE_OKEX = "okex"
EXCHANGE_GATE = "gate"
EXCHANGE_COINBASE = "coinbase"

View File

@ -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"
//代币-现货新币排序

View File

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

View File

@ -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"`
}

View File

@ -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"` // 是否使用测试网
}

View File

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

View File

@ -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连接已停止")
}

View File

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

20
go.mod
View File

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

View File

@ -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"` //开盘价格

View File

@ -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"`
}

View File

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

View File

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

View File

@ -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)

View File

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

View File

@ -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)

View File

@ -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,

View File

@ -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"`
}

View File

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

View File

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

View File

@ -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)

View File

@ -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)

View File

@ -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("获取交易对失败")

View File

@ -0,0 +1,447 @@
package bitgetservice
import (
"errors"
"fmt"
"go-admin/common/global"
"go-admin/common/helper"
"go-admin/common/interfaces"
"go-admin/models"
"go-admin/pkg/utility"
"strconv"
"strings"
"github.com/bytedance/sonic"
)
// BitgetClient Bitget交易所客户端实现
type BitgetClient struct {
httpClient *helper.BitgetClient
marketClient *BitgetMarketClient // 公开行情客户端
}
// NewBitgetClient 创建新的Bitget客户端实例
func NewBitgetClient(apiKey, apiSecret, passphrase, proxyUrl string) (*BitgetClient, error) {
httpClient, err := helper.NewBitgetClient(apiKey, apiSecret, passphrase, proxyUrl)
if err != nil {
return nil, err
}
client := &BitgetClient{
httpClient: httpClient,
}
// 初始化公开行情客户端
marketConfig := DefaultMarketConfig()
client.marketClient = NewBitgetMarketClient(marketConfig)
return client, nil
}
// GetExchangeName 获取交易所名称
func (c *BitgetClient) GetExchangeName() string {
return global.EXCHANGE_BITGET
}
// GetExchangeInfo 获取交易所信息
func (c *BitgetClient) GetExchangeInfo() (interfaces.ExchangeInfoResponse, error) {
restApi := NewBitgetRestApi()
datas, err := restApi.GetSpotSymbols()
if err != nil {
return interfaces.ExchangeInfoResponse{}, err
}
symbols := make([]interfaces.SymbolInfo, 0, len(datas))
for _, item := range datas {
symbols = append(symbols, interfaces.SymbolInfo{
Symbol: item.Symbol,
BaseAsset: item.BaseCoin,
QuoteAsset: item.QuoteCoin,
Status: item.Status,
MinQty: item.MinQty,
MaxQty: item.MaxQty,
StepSize: strconv.Itoa(item.QuantityDigit),
TickSize: strconv.Itoa(item.PriceDigit),
})
}
return interfaces.ExchangeInfoResponse{Symbols: symbols}, nil
}
// GetSpotTickers 获取所有现货行情
func (c *BitgetClient) GetSpotTickers() ([]models.TradeSet, error) {
params := map[string]interface{}{}
resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/market/tickers", "GET", params)
if err != nil {
return nil, fmt.Errorf("获取Bitget现货行情列表失败: %v", err)
}
var response struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data []struct {
Symbol string `json:"symbol"`
High24h string `json:"high24h"`
Low24h string `json:"low24h"`
Close string `json:"close"`
Open string `json:"open"`
Volume24h string `json:"volume24h"`
QuoteVol string `json:"quoteVol"`
Change24h string `json:"change24h"`
Change string `json:"change"`
} `json:"data"`
}
if err := sonic.Unmarshal(resp, &response); err != nil {
return nil, fmt.Errorf("解析Bitget行情列表失败: %v", err)
}
if response.Code != "00000" {
return nil, fmt.Errorf("bitget API错误: %s", response.Msg)
}
tradeSets := make([]models.TradeSet, 0, len(response.Data))
for _, item := range response.Data {
// 解析交易对,获取基础币种和计价币种
var coin, currency string
if strings.HasSuffix(item.Symbol, "USDT") {
coin = strings.TrimSuffix(item.Symbol, "USDT")
currency = "USDT"
} else if strings.HasSuffix(item.Symbol, "BTC") {
coin = strings.TrimSuffix(item.Symbol, "BTC")
currency = "BTC"
} else if strings.HasSuffix(item.Symbol, "ETH") {
coin = strings.TrimSuffix(item.Symbol, "ETH")
currency = "ETH"
} else {
continue // 跳过不支持的交易对
}
tradeSet := models.TradeSet{
Coin: coin,
Currency: currency,
LastPrice: item.Close,
HighPrice: item.High24h,
LowPrice: item.Low24h,
Volume: item.Volume24h,
QuoteVolume: item.QuoteVol,
OpenPrice: utility.StringAsFloat(item.Open),
PriceChange: utility.StringAsFloat(item.Change),
}
tradeSets = append(tradeSets, tradeSet)
}
return tradeSets, nil
}
// GetSpotTicker24h 获取现货24小时行情
func (c *BitgetClient) GetSpotTicker24h(symbol string) (models.Ticker24, error) {
params := map[string]interface{}{
"symbol": symbol,
}
resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/market/ticker", "GET", params)
if err != nil {
return models.Ticker24{}, fmt.Errorf("获取Bitget现货24h行情失败: %v", err)
}
var response struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data struct {
Symbol string `json:"symbol"`
High24h string `json:"high24h"`
Low24h string `json:"low24h"`
Close string `json:"close"`
Open string `json:"open"`
Volume24h string `json:"volume24h"`
QuoteVol string `json:"quoteVol"`
Change24h string `json:"change24h"`
Change string `json:"change"`
} `json:"data"`
}
if err := sonic.Unmarshal(resp, &response); err != nil {
return models.Ticker24{}, fmt.Errorf("解析Bitget 24h行情失败: %v", err)
}
if response.Code != "00000" {
return models.Ticker24{}, fmt.Errorf("Bitget API错误: %s", response.Msg)
}
ticker := models.Ticker24{
HighPrice: response.Data.High24h,
LowPrice: response.Data.Low24h,
LastPrice: response.Data.Close,
OpenPrice: response.Data.Open,
Volume: response.Data.Volume24h,
QuoteVolume: response.Data.QuoteVol,
ChangePercent: response.Data.Change,
}
return ticker, nil
}
// PlaceSpotOrder 下现货订单
func (c *BitgetClient) PlaceSpotOrder(params interfaces.SpotOrderParams) (interfaces.SpotOrderResponse, error) {
orderParams := map[string]interface{}{
"symbol": params.Symbol,
"side": strings.ToLower(params.Side),
"orderType": strings.ToLower(params.Type),
"quantity": params.Quantity.String(),
}
if params.Type == "LIMIT" {
orderParams["price"] = params.Price.String()
}
if params.TimeInForce != "" {
orderParams["timeInForce"] = params.TimeInForce
}
if params.NewClientOrderId != "" {
orderParams["clientOrderId"] = params.NewClientOrderId
}
resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/trade/orders", "POST", orderParams)
if err != nil {
return interfaces.SpotOrderResponse{}, fmt.Errorf("Bitget现货下单失败: %v", err)
}
var response struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data struct {
OrderId string `json:"orderId"`
ClientOrderId string `json:"clientOrderId"`
} `json:"data"`
}
if err := sonic.Unmarshal(resp, &response); err != nil {
return interfaces.SpotOrderResponse{}, fmt.Errorf("解析Bitget下单响应失败: %v", err)
}
if response.Code != "00000" {
return interfaces.SpotOrderResponse{}, fmt.Errorf("Bitget下单API错误: %s", response.Msg)
}
orderId, _ := strconv.ParseInt(response.Data.OrderId, 10, 64)
return interfaces.SpotOrderResponse{
Symbol: params.Symbol,
OrderId: orderId,
ClientOrderId: response.Data.ClientOrderId,
Price: params.Price.String(),
OrigQty: params.Quantity.String(),
Side: params.Side,
Type: params.Type,
Status: "NEW",
}, nil
}
// CancelSpotOrder 取消现货订单
func (c *BitgetClient) CancelSpotOrder(symbol, orderID string) error {
params := map[string]interface{}{
"symbol": symbol,
"orderId": orderID,
}
resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/trade/cancel-order", "POST", params)
if err != nil {
return fmt.Errorf("Bitget取消现货订单失败: %v", err)
}
var response struct {
Code string `json:"code"`
Msg string `json:"msg"`
}
if err := sonic.Unmarshal(resp, &response); err != nil {
return fmt.Errorf("解析Bitget取消订单响应失败: %v", err)
}
if response.Code != "00000" {
return fmt.Errorf("Bitget取消订单API错误: %s", response.Msg)
}
return nil
}
// GetSpotOrder 获取现货订单详情
func (c *BitgetClient) GetSpotOrder(symbol, orderID string) (interfaces.SpotOrderResponse, error) {
// 实现订单查询逻辑
return interfaces.SpotOrderResponse{}, fmt.Errorf("GetSpotOrder not implemented yet")
}
// GetSpotOrders 获取现货订单列表
func (c *BitgetClient) GetSpotOrders(symbol string) ([]interfaces.SpotOrderResponse, error) {
params := map[string]interface{}{
"symbol": symbol,
}
_, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/trade/open-orders", "GET", params)
if err != nil {
return nil, fmt.Errorf("获取Bitget现货订单列表失败: %v", err)
}
// 这里需要根据实际的Bitget API响应格式来解析
return []interfaces.SpotOrderResponse{}, nil
}
// 合约相关接口实现(暂时返回错误)
func (c *BitgetClient) GetFuturesTicker24h(symbol string) (models.Ticker24, error) {
return models.Ticker24{}, fmt.Errorf("futures not implemented yet")
}
func (c *BitgetClient) GetFuturesTickers() ([]models.TradeSet, error) {
return nil, fmt.Errorf("futures not implemented yet")
}
func (c *BitgetClient) PlaceFuturesOrder(params interfaces.FuturesOrderParams) (interfaces.FuturesOrderResponse, error) {
return interfaces.FuturesOrderResponse{}, fmt.Errorf("futures not implemented yet")
}
func (c *BitgetClient) CancelFuturesOrder(symbol, orderID string) error {
return fmt.Errorf("futures not implemented yet")
}
func (c *BitgetClient) GetFuturesOrder(symbol, orderID string) (interfaces.FuturesOrderResponse, error) {
return interfaces.FuturesOrderResponse{}, fmt.Errorf("futures not implemented yet")
}
// WebSocket相关接口实现已重构为使用公开行情客户端
// 注意:这些方法已被重构,现在使用新的公开行情客户端
// 如需私有数据订阅(如订单、持仓),请使用 BitgetWebSocketManager
// 旧的订阅方法(保留兼容性,但建议使用新方法)
func (c *BitgetClient) SubscribeSpotTickerLegacy(symbols []string, callback func(models.Ticker24, string, string)) error {
return fmt.Errorf("已废弃,请使用 SubscribeSpotTicker(symbol, listener) 方法")
}
func (c *BitgetClient) SubscribeFuturesTickerLegacy(symbols []string, callback func(models.Ticker24, string, string)) error {
return fmt.Errorf("已废弃,请使用 SubscribeFuturesTicker(symbol, listener) 方法")
}
func (c *BitgetClient) SubscribeSpotDepthLegacy(symbols []string, callback func(models.DepthBin, string, string)) error {
return fmt.Errorf("已废弃,请使用 SubscribeSpotDepth(symbol, listener) 方法")
}
func (c *BitgetClient) SubscribeSpotTradesLegacy(symbols []string, callback func(models.NewDealPush, string, string)) error {
return fmt.Errorf("已废弃,请使用 SubscribeSpotTrades(symbol, listener) 方法")
}
func (c *BitgetClient) SubscribeKlineLegacy(symbols []string, interval string, callback func(models.Kline, int, string, string)) error {
return fmt.Errorf("已废弃,请使用 SubscribeKline(symbol, interval, listener) 方法")
}
func (c *BitgetClient) Close() error {
// 关闭HTTP客户端等清理工作
return nil
}
// SetMarketProxy 设置市场数据代理
func (c *BitgetClient) SetMarketProxy(proxyType, proxyAddress string) {
if c.marketClient != nil {
c.marketClient.SetProxy(proxyType, proxyAddress)
}
}
// SetMarketListeners 设置市场数据监听器
func (c *BitgetClient) SetMarketListeners(msgListener, errorListener OnReceive) {
if c.marketClient != nil {
c.marketClient.SetListeners(msgListener, errorListener)
}
}
// ConnectMarketData 连接市场数据WebSocket
func (c *BitgetClient) ConnectMarketData() error {
if c.marketClient == nil {
return errors.New("市场数据客户端未初始化")
}
return c.marketClient.Connect()
}
// SubscribeMarketData 订阅市场数据(公开行情)
func (c *BitgetClient) SubscribeMarketData(subscribeReqs []SubscribeReq, listener OnReceive) error {
if c.marketClient == nil {
return errors.New("市场数据客户端未初始化")
}
return c.marketClient.Subscribe(subscribeReqs, listener)
}
// UnsubscribeMarketData 取消订阅市场数据
func (c *BitgetClient) UnsubscribeMarketData(subscribeReqs []SubscribeReq) error {
if c.marketClient == nil {
return errors.New("市场数据客户端未初始化")
}
return c.marketClient.Unsubscribe(subscribeReqs)
}
// IsMarketDataConnected 检查市场数据连接状态
func (c *BitgetClient) IsMarketDataConnected() bool {
if c.marketClient == nil {
return false
}
return c.marketClient.IsConnected()
}
// CloseMarketData 关闭市场数据连接
func (c *BitgetClient) CloseMarketData() error {
if c.marketClient == nil {
return nil
}
return c.marketClient.Close()
}
// SubscribeSpotTicker 订阅现货行情(使用公开行情客户端)
func (c *BitgetClient) SubscribeSpotTicker(symbol string, listener OnReceive) error {
subscribeReq := SubscribeReq{
InstType: "SPOT",
Channel: "ticker",
InstId: symbol,
}
return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
}
// SubscribeFuturesTicker 订阅合约行情(使用公开行情客户端)
func (c *BitgetClient) SubscribeFuturesTicker(symbol string, listener OnReceive) error {
subscribeReq := SubscribeReq{
InstType: "UMCBL",
Channel: "ticker",
InstId: symbol,
}
return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
}
// SubscribeSpotDepth 订阅现货深度(使用公开行情客户端)
func (c *BitgetClient) SubscribeSpotDepth(symbol string, listener OnReceive) error {
subscribeReq := SubscribeReq{
InstType: "SPOT",
Channel: "books",
InstId: symbol,
}
return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
}
// SubscribeSpotTrades 订阅现货成交(使用公开行情客户端)
func (c *BitgetClient) SubscribeSpotTrades(symbol string, listener OnReceive) error {
subscribeReq := SubscribeReq{
InstType: "SPOT",
Channel: "trade",
InstId: symbol,
}
return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
}
// SubscribeKline 订阅K线数据使用公开行情客户端
func (c *BitgetClient) SubscribeKline(symbol, interval string, listener OnReceive) error {
subscribeReq := SubscribeReq{
InstType: "SPOT",
Channel: "candle" + interval,
InstId: symbol,
}
return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
}

View File

@ -0,0 +1,229 @@
package bitgetservice
import (
log "github.com/go-admin-team/go-admin-core/logger"
)
// ExampleBitgetMarketClient 展示如何使用Bitget公开行情客户端
func ExampleBitgetMarketClient() {
// 1. 创建Bitget客户端不需要API密钥用于公开数据
client := &BitgetClient{}
// 初始化公开行情客户端
marketConfig := DefaultMarketConfig()
client.marketClient = NewBitgetMarketClient(marketConfig)
// 2. 设置代理(可选)
// client.SetMarketProxy("socks5", "127.0.0.1:1080")
// 3. 设置消息监听器
msgListener := func(message string) {
log.Info("收到市场数据: %s", message)
// 在这里处理市场数据
}
errorListener := func(message string) {
log.Error("收到错误消息: %s", message)
}
client.SetMarketListeners(msgListener, errorListener)
// 4. 连接WebSocket
if err := client.ConnectMarketData(); err != nil {
log.Error("连接市场数据失败: %v", err)
return
}
// 5. 订阅现货行情
tickerListener := func(message string) {
log.Info("收到BTC-USDT行情: %s", message)
}
if err := client.SubscribeSpotTicker("BTCUSDT", tickerListener); err != nil {
log.Error("订阅现货行情失败: %v", err)
return
}
// 6. 订阅深度数据
depthListener := func(message string) {
log.Info("收到BTC-USDT深度: %s", message)
}
if err := client.SubscribeSpotDepth("BTCUSDT", depthListener); err != nil {
log.Error("订阅深度数据失败: %v", err)
return
}
// 7. 订阅成交数据
tradeListener := func(message string) {
log.Info("收到BTC-USDT成交: %s", message)
}
if err := client.SubscribeSpotTrades("BTCUSDT", tradeListener); err != nil {
log.Error("订阅成交数据失败: %v", err)
return
}
// 8. 订阅K线数据
klineListener := func(message string) {
log.Info("收到BTC-USDT K线: %s", message)
}
if err := client.SubscribeKline("BTCUSDT", "1m", klineListener); err != nil {
log.Error("订阅K线数据失败: %v", err)
return
}
log.Info("所有订阅完成,开始接收数据...")
// 保持连接(在实际应用中,你可能需要其他方式来保持程序运行)
// select {}
}
// ExampleBitgetPrivateWebSocket 展示如何使用私有WebSocket订单、持仓等
func ExampleBitgetPrivateWebSocket() {
// 私有数据订阅仍然使用 BitgetWebSocketManager
// apiKey := "your_api_key"
// secretKey := "your_secret_key"
// passphrase := "your_passphrase"
// // 创建私有WebSocket管理器
// manager := GetOrCreateBitgetWebSocketManager(apiKey, secretKey, passphrase, "spot")
// // 启动连接
// if err := manager.Start(); err != nil {
// log.Error("启动私有WebSocket连接失败: %v", err)
// return
// }
// log.Info("私有WebSocket连接成功可以接收订单、持仓等私有数据")
}
// ExampleMixedUsage 展示混合使用公开和私有订阅
func ExampleMixedUsage() {
// apiKey := "your_api_key"
// secretKey := "your_secret_key"
// passphrase := "your_passphrase"
// // 1. 创建完整的Bitget客户端
// client, err := NewBitgetClient(apiKey, secretKey, passphrase)
// if err != nil {
// log.Error("创建Bitget客户端失败: %v", err)
// return
// }
// // 2. 设置公开行情监听器
// client.SetMarketListeners(
// func(message string) {
// log.Info("公开行情数据: %s", message)
// },
// func(message string) {
// log.Error("公开行情错误: %s", message)
// },
// )
// // 3. 连接公开行情
// if err := client.ConnectMarketData(); err != nil {
// log.Error("连接公开行情失败: %v", err)
// return
// }
// // 4. 订阅公开行情
// if err := client.SubscribeSpotTicker("BTCUSDT", func(message string) {
// log.Info("BTC行情: %s", message)
// }); err != nil {
// log.Error("订阅行情失败: %v", err)
// return
// }
// // 5. 同时使用私有WebSocket如果需要
// privateManager, err := GetOrCreateBitgetWebSocketManager(apiKey, secretKey, passphrase, "spot")
// if err != nil {
// log.Error("创建私有WebSocket失败: %v", err)
// return
// }
// if err := privateManager.Start(); err != nil {
// log.Error("启动私有WebSocket失败: %v", err)
// return
// }
// log.Info("混合使用设置完成:公开行情 + 私有数据")
}
// ExampleSubscribeMultipleSymbols 展示如何订阅多个交易对
func ExampleSubscribeMultipleSymbols() {
client := &BitgetClient{}
marketConfig := DefaultMarketConfig()
client.marketClient = NewBitgetMarketClient(marketConfig)
// 连接
if err := client.ConnectMarketData(); err != nil {
log.Error("连接失败: %v", err)
return
}
// 批量订阅多个交易对的行情
symbols := []string{"BTCUSDT", "ETHUSDT", "ADAUSDT", "DOTUSDT"}
for _, symbol := range symbols {
listener := func(sym string) OnReceive {
return func(message string) {
log.Info("%s 行情更新: %s", sym, message)
}
}(symbol)
if err := client.SubscribeSpotTicker(symbol, listener); err != nil {
log.Error("订阅 %s 行情失败: %v", symbol, err)
} else {
log.Info("成功订阅 %s 行情", symbol)
}
}
log.Info("批量订阅完成")
}
// ExampleCustomSubscription 展示如何使用自定义订阅
func ExampleCustomSubscription() {
client := &BitgetClient{}
marketConfig := DefaultMarketConfig()
client.marketClient = NewBitgetMarketClient(marketConfig)
if err := client.ConnectMarketData(); err != nil {
log.Error("连接失败: %v", err)
return
}
// 自定义订阅请求
customSubscriptions := []SubscribeReq{
{
InstType: "SPOT",
Channel: "ticker",
InstId: "BTCUSDT",
},
{
InstType: "SPOT",
Channel: "books",
InstId: "ETHUSDT",
},
{
InstType: "UMCBL", // 合约
Channel: "ticker",
InstId: "BTCUSDT",
},
}
// 统一的消息处理器
messageHandler := func(message string) {
log.Info("自定义订阅数据: %s", message)
// 在这里根据消息内容进行不同的处理
}
// 批量订阅
if err := client.SubscribeMarketData(customSubscriptions, messageHandler); err != nil {
log.Error("自定义订阅失败: %v", err)
return
}
log.Info("自定义订阅完成")
}

View File

@ -0,0 +1,326 @@
package bitgetservice
import (
DbModels "go-admin/app/admin/models"
"go-admin/models"
"go-admin/models/positiondto"
"gorm.io/gorm"
)
// JudgeCreateOrder 判断是否需要创建订单
// symbolType 1:现货 2:合约
func JudgeCreateOrder(trade models.TradeSet, symbolType int) error {
// var key string
// if symbolType == 1 {
// key = fmt.Sprintf(rediskey.PreSpotOrderList, global.EXCHANGE_BITGET)
// } else if symbolType == 2 {
// key = fmt.Sprintf(rediskey.PreFutOrderList, global.EXCHANGE_BITGET)
// } else {
// return fmt.Errorf("不支持的交易类型")
// }
// preOrderVal, _ := helper.DefaultRedis.GetAllList(key)
// db := commonservice.GetDBConnection()
// if len(preOrderVal) == 0 {
// return fmt.Errorf("预订单列表为空")
// }
// // spotApi := SpotRestApi{}
// for _, item := range preOrderVal {
// preOrder := dto.PreOrderRedisList{}
// if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil {
// logger.Error("反序列化失败")
// continue
// }
// if preOrder.Symbol == trade.Coin+trade.Currency {
// orderPrice, _ := decimal.NewFromString(preOrder.Price)
// tradePrice, _ := decimal.NewFromString(trade.LastPrice)
// //买入
// if strings.ToUpper(preOrder.Site) == "BUY" && orderPrice.Cmp(tradePrice) >= 0 && orderPrice.Cmp(decimal.Zero) > 0 && tradePrice.Cmp(decimal.Zero) > 0 {
// // SpotOrderLock(db, &preOrder, item, spotApi)
// }
// }
// }
return nil
}
// 判断是否触发加仓
func JudgeAddPositionOrder(trade models.TradeSet, symbolType int) error {
// var key string
// if symbolType == 1 {
// key = fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BITGET)
// } else if symbolType == 2 {
// key = fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BITGET)
// } else {
// return fmt.Errorf("不支持的交易类型")
// }
// preOrderVal, _ := helper.DefaultRedis.GetAllList(key)
// db := commonservice.GetDBConnection()
// if len(preOrderVal) == 0 {
// return fmt.Errorf("预订单列表为空")
// }
// // spotApi := SpotRestApi{}
// for _, item := range preOrderVal {
// preOrder := positiondto.AddPositionList{}
// if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil {
// logger.Error("反序列化失败")
// continue
// }
// if preOrder.Symbol == trade.Coin+trade.Currency {
// orderPrice := preOrder.Price
// tradePrice, _ := decimal.NewFromString(trade.LastPrice)
// //买入
// if strings.ToUpper(preOrder.Side) == "BUY" && orderPrice.Cmp(tradePrice) >= 0 && orderPrice.Cmp(decimal.Zero) > 0 && tradePrice.Cmp(decimal.Zero) > 0 {
// // SpotAddPositionTrigger(db, &preOrder, item, spotApi)
// }
// }
// }
return nil
}
// JudgeReduceOrder 判断是否需要减仓
func JudgeReduceOrder(trade models.TradeSet, symbolType int) error {
// var key string
// var reduceReduceListKey string
// if symbolType == 1 {
// key = fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BITGET)
// reduceReduceListKey = fmt.Sprintf(rediskey.SpotOrderReduceStrategyList, global.EXCHANGE_BITGET)
// } else if symbolType == 2 {
// key = fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BITGET)
// reduceReduceListKey = fmt.Sprintf(rediskey.FutOrderReduceStrategyList, global.EXCHANGE_BITGET)
// } else {
// return fmt.Errorf("不支持的交易类型")
// }
// db := commonservice.GetDBConnection()
// // spotApi := SpotRestApi{}
// setting, err := cacheservice.GetSystemSetting(db)
// if err != nil {
// logger.Error("获取系统设置失败")
// return errors.New("获取系统设置失败")
// }
// reduceOrder := positiondto.ReduceListItem{}
// tradePrice, _ := decimal.NewFromString(trade.LastPrice)
// //减仓单减仓策略
// orderReduceVal, _ := helper.DefaultRedis.HGetAllFields(reduceReduceListKey)
// reduceOrderStrategy := dto.LineOrderReduceStrategyResp{}
// orderService := orderservice.OrderService{}
// orderService.Orm = db
// for _, item := range orderReduceVal {
// sonic.Unmarshal([]byte(item), &reduceOrderStrategy)
// for index, item2 := range reduceOrderStrategy.Items {
// if reduceOrderStrategy.Symbol == trade.Coin+trade.Currency {
// //买入
// if strings.ToUpper(reduceOrderStrategy.Side) == "SELL" &&
// item2.TriggerPrice.Cmp(tradePrice) >= 0 &&
// item2.TriggerPrice.Cmp(decimal.Zero) > 0 &&
// tradePrice.Cmp(decimal.Zero) > 0 {
// lock := helper.NewRedisLock(fmt.Sprintf(rediskey.ReduceStrategySpotTriggerLock, reduceOrder.ApiId, reduceOrder.Symbol), 50, 15, 100*time.Millisecond)
// if ok, err := lock.AcquireWait(context.Background()); err != nil {
// logger.Error("获取锁失败", err)
// return errors.New("获取锁失败")
// } else if ok {
// defer lock.Release()
// hasrecord, _ := helper.DefaultRedis.HExists(reduceReduceListKey, utility.IntTostring(reduceOrderStrategy.OrderId), item)
// if !hasrecord {
// logger.Debug("减仓缓存中不存在", item)
// return fmt.Errorf("减仓缓存中不存在")
// }
// order, err := orderService.CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit)
// if err != nil {
// logger.Errorf("%d 生成订单失败", reduceOrderStrategy.OrderId)
// }
// reduceOrder.ApiId = order.ApiId
// reduceOrder.Id = order.Id
// reduceOrder.Pid = order.Pid
// reduceOrder.MainId = order.MainId
// reduceOrder.Symbol = order.Symbol
// reduceOrder.Side = reduceOrderStrategy.Side
// reduceOrder.OrderSn = order.OrderSn
// reduceOrder.Price = item2.Price
// reduceOrder.Num = item2.Num
// if SpotReduceTrigger(db, reduceOrder, spotApi, setting, reduceReduceListKey, item, true, reduceOrderStrategy.OrderId) {
// reduceOrderStrategy.Items[index].Actived = true
// allActive := true
// orderId := utility.IntToString(reduceOrderStrategy.OrderId)
// for _, item3 := range reduceOrderStrategy.Items {
// if !item3.Actived {
// allActive = false
// break
// }
// }
// if allActive {
// if err := helper.DefaultRedis.HDelField(reduceReduceListKey, orderId); err != nil {
// logger.Errorf("删除redis reduceReduceListKey失败 %s", err.Error())
// }
// } else {
// str, _ := sonic.MarshalString(reduceOrderStrategy)
// if err := helper.DefaultRedis.HSetField(reduceReduceListKey, orderId, str); err != nil {
// logger.Errorf("更新redis reduceReduceListKey失败 %s", err.Error())
// }
// }
// }
// }
// }
// }
// }
// }
// //减仓单
// reduceVal, _ := helper.DefaultRedis.GetAllList(key)
// for _, item := range reduceVal {
// if err := sonic.Unmarshal([]byte(item), &reduceOrder); err != nil {
// logger.Error("反序列化失败")
// continue
// }
// if reduceOrder.Symbol == trade.Coin+trade.Currency {
// orderPrice := reduceOrder.Price
// //买入
// if strings.ToUpper(reduceOrder.Side) == "SELL" &&
// orderPrice.Cmp(tradePrice) >= 0 &&
// orderPrice.Cmp(decimal.Zero) > 0 &&
// tradePrice.Cmp(decimal.Zero) > 0 {
// // SpotReduceTrigger(db, reduceOrder, spotApi, setting, key, item, false, 0)
// }
// }
// }
return nil
}
// 触发现货减仓
// isStrategy 是否是策略减仓单
// reduceId 策略主减仓单id
func SpotReduceTrigger(db *gorm.DB, reduceOrder positiondto.ReduceListItem, setting DbModels.LineSystemSetting, key, item string, isStrategy bool, reduceId int) bool {
// tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reduceOrder.Symbol, 1)
result := true
// if err != nil {
// logger.Error("获取交易设置失败")
// return false
// }
// lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, reduceOrder.ApiId, reduceOrder.Symbol), 20, 5, 100*time.Millisecond)
// if ok, err := lock.AcquireWait(context.Background()); err != nil {
// logger.Error("获取锁失败", err)
// return false
// } else if ok {
// defer lock.Release()
// takeOrders := make([]DbModels.LinePreOrder, 0)
// if err := db.Model(&DbModels.LinePreOrder{}).Where("main_id =? AND order_type IN (1,2,4) AND status IN (1,5)", reduceOrder.MainId).Find(&takeOrders).Error; err != nil {
// logger.Error("查询止盈单失败")
// return false
// }
// var hasrecord bool
// if isStrategy {
// hasrecord, _ = helper.DefaultRedis.HExists(key, utility.IntToString(reduceId), item)
// } else {
// hasrecord, _ = helper.DefaultRedis.IsElementInList(key, item)
// }
// if !hasrecord {
// logger.Debug("减仓缓存中不存在", item)
// return false
// }
// apiInfo, _ := GetApiInfo(reduceOrder.ApiId)
// if apiInfo.Id == 0 {
// logger.Error("现货减仓 查询api用户不存在")
// return false
// }
// for _, takeOrder := range takeOrders {
// err := CancelOpenOrderByOrderSnLoop(apiInfo, takeOrder.Symbol, takeOrder.OrderSn)
// if err != nil {
// logger.Error("现货撤单失败", err)
// return false
// }
// }
// price := reduceOrder.Price.Mul(decimal.NewFromInt(1).Sub(setting.ReducePremium.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit))
// params := OrderPlacementService{
// ApiId: reduceOrder.ApiId,
// Side: reduceOrder.Side,
// Type: "LIMIT",
// TimeInForce: "GTC",
// Symbol: reduceOrder.Symbol,
// Price: price,
// Quantity: reduceOrder.Num,
// NewClientOrderId: reduceOrder.OrderSn,
// }
// if err := spotApi.OrderPlaceLoop(db, params, 3); err != nil {
// result = false
// logger.Errorf("现货减仓挂单失败 id%s err:%v", reduceOrder.Id, err)
// if err2 := db.Model(&DbModels.LinePreOrder{}).
// Where("id =? AND status =0", reduceOrder.Id).
// Updates(map[string]interface{}{"status": 2, "desc": err.Error()}).Error; err2 != nil {
// logger.Errorf("修改现货减仓状态失败 id%s err:%v", reduceOrder.Id, err2)
// }
// } else {
// if err := db.Model(&DbModels.LinePreOrder{}).Where("id =?", reduceOrder.Id).Updates(map[string]interface{}{"trigger_time": time.Now()}).Error; err != nil {
// logger.Error("更新预下单触发事件失败 ordersn:", reduceOrder.OrderSn)
// }
// if err := db.Model(&DbModels.LinePreOrder{}).
// Where("id =? AND status =0", reduceOrder.Id).
// Updates(map[string]interface{}{"status": 1}).Error; err != nil {
// logger.Errorf("修改现货减仓状态失败 id%s err:%v", reduceOrder.Id, err)
// }
// //处理减仓单减仓策略
// if err := CacheOrderStrategyAndReCreate(db, reduceOrder, 1, tradeSet, setting); err != nil {
// logger.Errorf("现货减仓 处理减仓策略失败 id:%v err:%v", reduceOrder.Id, err)
// }
// }
// if _, err := helper.DefaultRedis.LRem(key, item); err != nil {
// result = false
// logger.Errorf("现货减仓 删除缓存失败 id:%v err:%v", reduceOrder.Id, err)
// }
// } else {
// logger.Error("获取锁失败")
// result = false
// }
return result
}
// JudgeTakeOrder 判断是否止盈
func JudgeTakeOrder(trades []models.TradeSet, symbolType int) error {
return nil
}
// JudgeStopOrder 判断是否止损
func JudgeStopOrder(trades []models.TradeSet, symbolType int) error {
return nil
}

View File

@ -0,0 +1,430 @@
package bitgetservice
import (
"context"
"encoding/json"
"errors"
"fmt"
"go-admin/services/proxy"
"strings"
"sync"
"time"
"github.com/bytedance/sonic"
log "github.com/go-admin-team/go-admin-core/logger"
"github.com/gorilla/websocket"
)
// BitgetMarketClient Bitget公开行情订阅客户端
// 基于官方SDK设计专门用于公开市场数据订阅
type BitgetMarketClient struct {
config *BitgetMarketConfig
conn *websocket.Conn
subscriptions map[SubscribeReq]OnReceive
allSubscribes *Set
listener OnReceive
errorListener OnReceive
sendMutex sync.Mutex
reconnectMutex sync.Mutex
ctx context.Context
cancel context.CancelFunc
connected bool
lastPongTime time.Time
proxyType string
proxyAddress string
reconnectCount int
}
// NewBitgetMarketClient 创建新的Bitget市场数据客户端
func NewBitgetMarketClient(config *BitgetMarketConfig) *BitgetMarketClient {
if config == nil {
config = DefaultMarketConfig()
}
ctx, cancel := context.WithCancel(context.Background())
return &BitgetMarketClient{
config: config,
subscriptions: make(map[SubscribeReq]OnReceive),
allSubscribes: NewSet(),
ctx: ctx,
cancel: cancel,
connected: false,
lastPongTime: time.Now(),
}
}
// SetProxy 设置代理
func (c *BitgetMarketClient) SetProxy(proxyType, proxyAddress string) {
c.proxyType = proxyType
c.proxyAddress = proxyAddress
}
// SetListeners 设置消息监听器
func (c *BitgetMarketClient) SetListeners(msgListener, errorListener OnReceive) {
c.listener = msgListener
c.errorListener = errorListener
}
// Connect 连接WebSocket
func (c *BitgetMarketClient) Connect() error {
c.reconnectMutex.Lock()
defer c.reconnectMutex.Unlock()
if c.connected {
return nil
}
// 创建WebSocket连接
dialer := websocket.DefaultDialer
// 设置代理
if c.proxyAddress != "" {
var err error
dialer, err = proxy.GetDialer(c.proxyType, c.proxyAddress)
if err != nil {
return fmt.Errorf("设置代理失败: %v", err)
}
}
dialer.HandshakeTimeout = 10 * time.Second
log.Infof("正在连接Bitget市场数据WebSocket: %s", c.config.WsUrl)
conn, _, err := dialer.Dial(c.config.WsUrl, nil)
if err != nil {
return fmt.Errorf("webSocket连接失败: %v", err)
}
c.conn = conn
c.connected = true
c.lastPongTime = time.Now()
c.reconnectCount = 0
log.Info("Bitget市场数据WebSocket连接成功")
// 启动消息处理协程
go c.readLoop()
go c.pingLoop()
go c.monitorConnection()
return nil
}
// Subscribe 订阅市场数据
func (c *BitgetMarketClient) Subscribe(subscribeReqs []SubscribeReq, listener OnReceive) error {
if !c.connected {
if err := c.Connect(); err != nil {
return err
}
}
var args []interface{}
for _, req := range subscribeReqs {
// 标准化订阅请求
normalizedReq := c.normalizeSubscribeReq(req)
args = append(args, normalizedReq)
// 保存订阅信息
c.subscriptions[normalizedReq] = listener
c.allSubscribes.Add(normalizedReq)
}
// 发送订阅消息
wsReq := WsBaseReq{
Op: "subscribe",
Args: args,
}
return c.sendMessage(wsReq)
}
// Unsubscribe 取消订阅
func (c *BitgetMarketClient) Unsubscribe(subscribeReqs []SubscribeReq) error {
if !c.connected {
return errors.New("WebSocket未连接")
}
var args []interface{}
for _, req := range subscribeReqs {
normalizedReq := c.normalizeSubscribeReq(req)
args = append(args, normalizedReq)
// 移除订阅信息
delete(c.subscriptions, normalizedReq)
c.allSubscribes.Remove(normalizedReq)
}
// 发送取消订阅消息
wsReq := WsBaseReq{
Op: "unsubscribe",
Args: args,
}
return c.sendMessage(wsReq)
}
// normalizeSubscribeReq 标准化订阅请求
func (c *BitgetMarketClient) normalizeSubscribeReq(req SubscribeReq) SubscribeReq {
return SubscribeReq{
InstType: strings.ToUpper(req.InstType),
Channel: strings.ToLower(req.Channel),
InstId: strings.ToUpper(req.InstId),
}
}
// sendMessage 发送消息
func (c *BitgetMarketClient) sendMessage(msg interface{}) error {
if c.conn == nil {
return errors.New("WebSocket连接不可用")
}
c.sendMutex.Lock()
defer c.sendMutex.Unlock()
data, err := sonic.Marshal(msg)
if err != nil {
return fmt.Errorf("序列化消息失败: %v", err)
}
log.Info("发送消息: %s", string(data))
c.conn.SetWriteDeadline(time.Now().Add(c.config.WriteTimeout))
return c.conn.WriteMessage(websocket.TextMessage, data)
}
// readLoop 消息读取循环
func (c *BitgetMarketClient) readLoop() {
defer func() {
if r := recover(); r != nil {
log.Error("readLoop panic: %v", r)
}
}()
for {
select {
case <-c.ctx.Done():
return
default:
if c.conn == nil {
time.Sleep(time.Second)
continue
}
c.conn.SetReadDeadline(time.Now().Add(c.config.ReadTimeout))
_, message, err := c.conn.ReadMessage()
if err != nil {
log.Error("读取消息失败: %v", err)
c.handleDisconnect()
continue
}
c.handleMessage(string(message))
}
}
}
// handleMessage 处理接收到的消息
func (c *BitgetMarketClient) handleMessage(message string) {
log.Info("收到消息: %s", message)
// 处理pong消息
if message == "pong" {
c.lastPongTime = time.Now()
log.Debug("收到pong消息")
return
}
// 解析JSON消息
var jsonMap map[string]interface{}
if err := json.Unmarshal([]byte(message), &jsonMap); err != nil {
log.Error("解析JSON消息失败: %v", err)
if c.errorListener != nil {
c.errorListener(message)
}
return
}
// 检查错误码
if code, exists := jsonMap["code"]; exists {
if codeNum, ok := code.(float64); ok && int(codeNum) != 0 {
log.Error("收到错误消息: %s", message)
if c.errorListener != nil {
c.errorListener(message)
}
return
}
}
// 处理订阅确认消息
if event, exists := jsonMap["event"]; exists && event == "subscribe" {
log.Info("订阅确认: %s", message)
return
}
// 处理数据推送
if data, exists := jsonMap["data"]; exists && data != nil {
if arg, argExists := jsonMap["arg"]; argExists {
listener := c.getListener(arg)
if listener != nil {
listener(message)
}
}
return
}
// 默认处理
if c.listener != nil {
c.listener(message)
}
}
// getListener 获取对应的监听器
func (c *BitgetMarketClient) getListener(argJson interface{}) OnReceive {
argMap, ok := argJson.(map[string]interface{})
if !ok {
return c.listener
}
subscribeReq := SubscribeReq{
InstType: fmt.Sprintf("%v", argMap["instType"]),
Channel: fmt.Sprintf("%v", argMap["channel"]),
InstId: fmt.Sprintf("%v", argMap["instId"]),
}
if listener, exists := c.subscriptions[subscribeReq]; exists {
return listener
}
return c.listener
}
// pingLoop Ping循环
func (c *BitgetMarketClient) pingLoop() {
ticker := time.NewTicker(c.config.PingInterval)
defer ticker.Stop()
for {
select {
case <-c.ctx.Done():
return
case <-ticker.C:
if c.connected && c.conn != nil {
c.sendMutex.Lock()
c.conn.SetWriteDeadline(time.Now().Add(c.config.WriteTimeout))
err := c.conn.WriteMessage(websocket.TextMessage, []byte("ping"))
c.sendMutex.Unlock()
if err != nil {
log.Error("发送ping失败: %v", err)
c.handleDisconnect()
}
}
}
}
}
// monitorConnection 监控连接状态
func (c *BitgetMarketClient) monitorConnection() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-c.ctx.Done():
return
case <-ticker.C:
if c.connected && time.Since(c.lastPongTime) > 60*time.Second {
log.Warn("长时间未收到pong消息触发重连")
c.handleDisconnect()
}
}
}
}
// handleDisconnect 处理断开连接
func (c *BitgetMarketClient) handleDisconnect() {
c.reconnectMutex.Lock()
defer c.reconnectMutex.Unlock()
if !c.connected {
return
}
log.Warn("WebSocket连接断开准备重连")
c.connected = false
if c.conn != nil {
c.conn.Close()
c.conn = nil
}
// 重连逻辑
go c.reconnect()
}
// reconnect 重连
func (c *BitgetMarketClient) reconnect() {
for c.reconnectCount < c.config.MaxReconnectTimes {
select {
case <-c.ctx.Done():
return
default:
c.reconnectCount++
log.Info("尝试重连 (%d/%d)", c.reconnectCount, c.config.MaxReconnectTimes)
time.Sleep(c.config.ReconnectInterval)
if err := c.Connect(); err != nil {
log.Error("重连失败: %v", err)
continue
}
// 重新订阅
if err := c.resubscribe(); err != nil {
log.Error("重新订阅失败: %v", err)
c.handleDisconnect()
continue
}
log.Info("重连成功")
return
}
}
log.Error("达到最大重连次数,停止重连")
}
// resubscribe 重新订阅
func (c *BitgetMarketClient) resubscribe() error {
if len(c.subscriptions) == 0 {
return nil
}
var args []interface{}
for req := range c.subscriptions {
args = append(args, req)
}
wsReq := WsBaseReq{
Op: "subscribe",
Args: args,
}
return c.sendMessage(wsReq)
}
// Close 关闭客户端
func (c *BitgetMarketClient) Close() error {
c.cancel()
c.connected = false
if c.conn != nil {
return c.conn.Close()
}
return nil
}
// IsConnected 检查连接状态
func (c *BitgetMarketClient) IsConnected() bool {
return c.connected
}

View File

@ -0,0 +1,61 @@
package bitgetservice
import (
"go-admin/config"
"github.com/go-admin-team/go-admin-core/logger"
)
type BitgetMarketFutureClient struct {
}
func (c *BitgetMarketFutureClient) Subscribe(symbols []string) error {
// 1. 创建Bitget客户端不需要API密钥用于公开数据
client := &BitgetClient{}
// 初始化公开行情客户端
marketConfig := DefaultMarketConfig()
client.marketClient = NewBitgetMarketClient(marketConfig)
if config.ExtConfig.ProxyUrl != "" {
client.SetMarketProxy("", config.ExtConfig.ProxyUrl)
}
// 3. 设置消息监听器
msgListener := func(message string) {
logger.Info("收到市场数据: %s", message)
// 在这里处理市场数据
}
errorListener := func(message string) {
logger.Error("收到错误消息: %s", message)
}
client.SetMarketListeners(msgListener, errorListener)
// 4. 连接WebSocket
if err := client.ConnectMarketData(); err != nil {
logger.Error("连接市场数据失败: %v", err)
return err
}
// 5. 订阅合约行情
tickerListener := func(message string) {
logger.Info("收到BTC-USDT合约行情: %s", message)
}
params := []SubscribeReq{}
for _, v := range symbols {
params = append(params, SubscribeReq{
InstType: "USDT-FUTURES",
Channel: "ticker",
InstId: v,
})
}
if err := client.SubscribeMarketData(params, tickerListener); err != nil {
logger.Error("订阅合约行情失败: %v", err)
return err
}
return nil
}

View File

@ -0,0 +1,126 @@
package bitgetservice
import "time"
// SubscribeReq 订阅请求结构基于官方SDK
type SubscribeReq struct {
InstType string `json:"instType"` // 产品类型SPOT-现货UMCBL-U本位合约DMCBL-币本位合约
Channel string `json:"channel"` // 频道名称
InstId string `json:"instId"` // 产品ID
// Coin string `json:"coin"` // 币种(可选)
}
// WsBaseReq WebSocket基础请求结构
type WsBaseReq struct {
Op string `json:"op"` // 操作类型subscribe, unsubscribe, login
Args []interface{} `json:"args"` // 参数列表
}
// WsLoginReq WebSocket登录请求
type WsLoginReq struct {
ApiKey string `json:"apiKey"`
Passphrase string `json:"passphrase"`
Timestamp string `json:"timestamp"`
Sign string `json:"sign"`
}
// OnReceive 消息接收回调函数类型
type OnReceive func(message string)
// Set 集合数据结构(简化实现)
type Set struct {
items map[SubscribeReq]bool
}
// NewSet 创建新的集合
func NewSet() *Set {
return &Set{
items: make(map[SubscribeReq]bool),
}
}
// Add 添加元素到集合
func (s *Set) Add(item SubscribeReq) {
s.items[item] = true
}
// Remove 从集合中移除元素
func (s *Set) Remove(item SubscribeReq) {
delete(s.items, item)
}
// Contains 检查集合是否包含元素
func (s *Set) Contains(item SubscribeReq) bool {
_, exists := s.items[item]
return exists
}
// BitgetMarketData 市场数据基础结构
type BitgetMarketData struct {
Action string `json:"action"` // 推送类型
Arg SubscribeReq `json:"arg"` // 订阅参数
Data interface{} `json:"data"` // 数据内容
Ts int64 `json:"ts"` // 时间戳
}
// TickerData 行情数据
type TickerData struct {
InstId string `json:"instId"` // 产品ID
Last string `json:"last"` // 最新价
Open24h string `json:"open24h"` // 24小时开盘价
High24h string `json:"high24h"` // 24小时最高价
Low24h string `json:"low24h"` // 24小时最低价
Change24h string `json:"change24h"` // 24小时涨跌幅
Vol24h string `json:"vol24h"` // 24小时成交量
VolCcy24h string `json:"volCcy24h"` // 24小时成交额
Ts string `json:"ts"` // 时间戳
}
// DepthData 深度数据
type DepthData struct {
InstId string `json:"instId"` // 产品ID
Asks [][]string `json:"asks"` // 卖盘 [价格, 数量]
Bids [][]string `json:"bids"` // 买盘 [价格, 数量]
Ts string `json:"ts"` // 时间戳
}
// TradeData 成交数据
type TradeData struct {
InstId string `json:"instId"` // 产品ID
TradeId string `json:"tradeId"` // 成交ID
Px string `json:"px"` // 成交价格
Sz string `json:"sz"` // 成交数量
Side string `json:"side"` // 成交方向 buy/sell
Ts string `json:"ts"` // 成交时间
}
// KlineData K线数据
type KlineData struct {
InstId string `json:"instId"` // 产品ID
Candle []string `json:"candle"` // K线数据 [时间戳, 开盘价, 最高价, 最低价, 收盘价, 成交量, 成交额]
Ts string `json:"ts"` // 时间戳
}
// BitgetMarketConfig 市场数据配置
type BitgetMarketConfig struct {
WsUrl string // WebSocket地址
ReconnectInterval time.Duration // 重连间隔
PingInterval time.Duration // Ping间隔
ReadTimeout time.Duration // 读取超时
WriteTimeout time.Duration // 写入超时
MaxReconnectTimes int // 最大重连次数
EnableCompression bool // 是否启用压缩
}
// DefaultMarketConfig 默认市场数据配置
func DefaultMarketConfig() *BitgetMarketConfig {
return &BitgetMarketConfig{
WsUrl: "wss://ws.bitget.com/v2/ws/public",
ReconnectInterval: 5 * time.Second,
PingInterval: 15 * time.Second,
ReadTimeout: 60 * time.Second,
WriteTimeout: 10 * time.Second,
MaxReconnectTimes: 10,
EnableCompression: false,
}
}

View File

@ -0,0 +1,61 @@
package bitgetservice
import (
"go-admin/config"
"github.com/go-admin-team/go-admin-core/logger"
)
type BitgetMarketSpotClient struct {
}
func (c *BitgetMarketSpotClient) Subscribe(symbols []string) error {
// 1. 创建Bitget客户端不需要API密钥用于公开数据
client := &BitgetClient{}
// 初始化公开行情客户端
marketConfig := DefaultMarketConfig()
client.marketClient = NewBitgetMarketClient(marketConfig)
if config.ExtConfig.ProxyUrl != "" {
client.SetMarketProxy("", config.ExtConfig.ProxyUrl)
}
// 3. 设置消息监听器
msgListener := func(message string) {
logger.Infof("收到市场数据: %s", message)
// 在这里处理市场数据
}
errorListener := func(message string) {
logger.Errorf("收到错误消息: %s", message)
}
client.SetMarketListeners(msgListener, errorListener)
// 4. 连接WebSocket
if err := client.ConnectMarketData(); err != nil {
logger.Errorf("连接市场数据失败: %v", err)
return err
}
// 5. 订阅现货行情
tickerListener := func(message string) {
logger.Infof("收到行情: %s", message)
}
params := []SubscribeReq{}
for _, v := range symbols {
params = append(params, SubscribeReq{
InstType: "SPOT",
Channel: "ticker",
InstId: v,
})
}
if err := client.SubscribeMarketData(params, tickerListener); err != nil {
logger.Errorf("订阅现货行情失败: %v", err)
return err
}
return nil
}

View File

@ -0,0 +1,54 @@
package bitgetservice
import (
"go-admin/common/helper"
"go-admin/config"
"testing"
"time"
"github.com/go-admin-team/go-admin-core/sdk"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func initSetting() {
dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
sdk.Runtime.SetDb("default", db)
helper.InitDefaultRedis("127.0.0.1:6379", "", 2)
helper.InitLockRedisConn("127.0.0.1:6379", "", "2")
config.ExtConfig.ProxyUrl = "http://127.0.0.1:7890"
}
func TestBitgetMarketFutureClient_Subscribe(t *testing.T) {
initSetting()
// 初始化Bitget合约行情客户端
bitgetFut := BitgetMarketFutureClient{}
// 测试用的合约符号列表
testSymbols := []string{"ETHUSDT"}
// 调用订阅方法
err := bitgetFut.Subscribe(testSymbols)
if err != nil {
t.Errorf("订阅Bitget合约行情失败: %v", err)
}
time.Sleep(60 * time.Second)
}
func TestBitgetMarketSpotClient_Subscribe(t *testing.T) {
initSetting()
// 初始化Bitget现货行情客户端
bitgetSpot := BitgetMarketSpotClient{}
// 测试用的现货符号列表
testSymbols := []string{"ETHUSDT"}
// 调用订阅方法
err := bitgetSpot.Subscribe(testSymbols)
if err != nil {
t.Errorf("订阅Bitget现货行情失败: %v", err)
}
time.Sleep(60 * time.Second)
}

View File

@ -0,0 +1,417 @@
package bitgetservice
import (
"fmt"
"go-admin/common/global"
"go-admin/common/helper"
"go-admin/config"
"go-admin/models"
"go-admin/pkg/utility"
"strconv"
"strings"
"github.com/bytedance/sonic"
log "github.com/go-admin-team/go-admin-core/logger"
)
// BitgetRestApi Bitget REST API服务
type BitgetRestApi struct {
httpClient *helper.BitgetClient
}
// NewBitgetRestApi 创建新的Bitget REST API实例
func NewBitgetRestApi() *BitgetRestApi {
// 从配置中获取代理设置
proxyUrl := config.ExtConfig.ProxyUrl
// 使用代理配置创建HTTP客户端
httpClient, err := helper.NewBitgetClient("", "", "", proxyUrl)
if err != nil {
log.Error("创建Bitget HTTP客户端失败", err)
// 如果代理创建失败,使用无代理的客户端
httpClient, _ = helper.NewBitgetClient("", "", "", "")
}
return &BitgetRestApi{
httpClient: httpClient,
}
}
// NewBitgetSimpleClient 创建简单的Bitget客户端兼容性函数
func NewBitgetSimpleClient() *BitgetClient {
// 从配置中获取代理设置
proxyUrl := config.ExtConfig.ProxyUrl
client, err := NewBitgetClient("", "", "", proxyUrl)
if err != nil {
log.Error("创建Bitget简单客户端失败", err)
// 如果代理创建失败,使用无代理的客户端
client, _ = NewBitgetClient("", "", "", "")
}
return client
}
// bitgetSymbolResponse 通用的符号响应结构体
type bitgetSymbolResponse struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data []struct {
Symbol string `json:"symbol"`
BaseCoin string `json:"baseCoin"`
QuoteCoin string `json:"quoteCoin"`
Status string `json:"status"`
MinTradeAmount string `json:"minTradeAmount"`
MaxTradeAmount string `json:"maxTradeAmount"`
TakerFeeRate string `json:"takerFeeRate"`
MakerFeeRate string `json:"makerFeeRate"`
PricePrecision string `json:"pricePrecision"`
QuantityPrecision string `json:"quantityPrecision"`
} `json:"data"`
}
// getSymbolsFromAPI 通用的符号获取方法
// symbolType: "spot" 现货, "futures" 合约
func (api *BitgetRestApi) getSymbolsFromAPI(symbolType string) ([]BitgetSymbol, error) {
var url string
var errorPrefix string
switch symbolType {
case "spot":
url = "/api/v2/spot/public/symbols"
errorPrefix = "现货"
case "futures":
url = "/api/v2/mix/market/contracts?productType=USDT-FUTURES"
errorPrefix = "合约"
default:
return nil, fmt.Errorf("不支持的符号类型: %s", symbolType)
}
// 发送API请求
resp, _, err := api.httpClient.SendSpotRequestAuth(url, "GET", map[string]interface{}{})
if err != nil {
return nil, fmt.Errorf("获取bitget %s交易所信息失败: %v", errorPrefix, err)
}
// 解析响应数据
var response bitgetSymbolResponse
if err := sonic.Unmarshal(resp, &response); err != nil {
return nil, fmt.Errorf("解析bitget %s交易所信息失败: %v", errorPrefix, err)
}
// 检查API响应状态
if response.Code != "00000" {
return nil, fmt.Errorf("bitget API错误: %s", response.Msg)
}
// 转换数据格式
return api.convertToSymbols(response.Data), nil
}
// convertToSymbols 将API响应数据转换为BitgetSymbol切片
func (api *BitgetRestApi) convertToSymbols(data []struct {
Symbol string `json:"symbol"`
BaseCoin string `json:"baseCoin"`
QuoteCoin string `json:"quoteCoin"`
Status string `json:"status"`
MinTradeAmount string `json:"minTradeAmount"`
MaxTradeAmount string `json:"maxTradeAmount"`
TakerFeeRate string `json:"takerFeeRate"`
MakerFeeRate string `json:"makerFeeRate"`
PricePrecision string `json:"pricePrecision"`
QuantityPrecision string `json:"quantityPrecision"`
}) []BitgetSymbol {
symbols := make([]BitgetSymbol, 0, len(data))
for _, item := range data {
if item.Status == "online" {
priceDigit, _ := strconv.Atoi(item.PricePrecision)
quantileDigit, _ := strconv.Atoi(item.QuantityPrecision)
symbol := BitgetSymbol{
Symbol: item.Symbol,
BaseAsset: item.BaseCoin,
QuoteAsset: item.QuoteCoin,
Status: item.Status,
MinQty: item.MinTradeAmount,
MaxQty: item.MaxTradeAmount,
PriceDigit: priceDigit,
QuantityDigit: quantileDigit,
}
symbols = append(symbols, symbol)
}
}
return symbols
}
// GetSpotSymbols 获取现货交易对信息
func (api *BitgetRestApi) GetSpotSymbols() ([]BitgetSymbol, error) {
return api.getSymbolsFromAPI("spot")
}
// GetFutSymbols 获取合约交易对信息
func (api *BitgetRestApi) GetFutSymbols() ([]BitgetSymbol, error) {
return api.getSymbolsFromAPI("futures")
}
// symbolConfig 符号配置结构体
type symbolConfig struct {
symbolType string
quoteAssetSymbols []string
}
// buildTradeSetFromSymbols 通用的TradeSet构建方法
func buildTradeSetFromSymbols(symbols []BitgetSymbol, config symbolConfig) (map[string]models.TradeSet, []string) {
tradeSets := make(map[string]models.TradeSet, len(symbols))
deleteSymbols := make([]string, 0)
for _, item := range symbols {
if utility.ContainsStr(config.quoteAssetSymbols, item.QuoteAsset) && item.Status == "online" {
tradeSet := models.TradeSet{
Coin: item.BaseAsset,
Currency: item.QuoteAsset,
PriceDigit: item.PriceDigit,
AmountDigit: item.QuantityDigit,
MinQty: utility.StringAsFloat(item.MinQty),
}
tradeSets[item.Symbol] = tradeSet
}
}
return tradeSets, deleteSymbols
}
// GetSpotSymbols 获取现货交易对并构建TradeSet映射
func GetSpotSymbols() (map[string]models.TradeSet, []string, error) {
api := NewBitgetRestApi()
symbols, err := api.GetSpotSymbols()
if err != nil {
log.Error("获取bitget规范信息失败", err)
return make(map[string]models.TradeSet, 0), []string{}, err
}
config := symbolConfig{
symbolType: "spot",
quoteAssetSymbols: []string{"USDT", "ETH", "BTC", "SOL", "BNB", "DOGE"},
}
tradeSets, deleteSymbols := buildTradeSetFromSymbols(symbols, config)
return tradeSets, deleteSymbols, nil
}
// GetFutSymbols 获取合约交易对并构建TradeSet映射
func GetFutSymbols() (map[string]models.TradeSet, []string, error) {
api := NewBitgetRestApi()
symbols, err := api.GetFutSymbols()
if err != nil {
log.Error("获取bitget合约规范信息失败", err)
return make(map[string]models.TradeSet, 0), []string{}, err
}
config := symbolConfig{
symbolType: "futures",
quoteAssetSymbols: []string{"USDT"},
}
tradeSets, deleteSymbols := buildTradeSetFromSymbols(symbols, config)
return tradeSets, deleteSymbols, nil
}
// getTicker24hData 获取24小时行情数据的通用方法
func (api *BitgetRestApi) getTicker24hData(url, tickerType, description string, tradeSet *map[string]models.TradeSet) ([]string, error) {
// 定义响应数据结构
type tickerResponse struct {
Code string `json:"code"`
Msg string `json:"msg"`
Data []struct {
Symbol string `json:"symbol"`
High24h string `json:"high24h"`
Low24h string `json:"low24h"`
Close string `json:"lastPr"`
Open string `json:"open"`
Volume24h string `json:"baseVolume"` // 基础币成交量
QuoteVol string `json:"usdtVolume"` // USDT成交量
Change24h string `json:"changeUtc24h"` // UTC 24h涨跌幅
Change string `json:"change24h"` // 24h涨跌幅
} `json:"data"`
}
// 发送API请求
resp, _, err := api.httpClient.SendSpotRequestAuth(url, "GET", map[string]interface{}{})
if err != nil {
return []string{}, fmt.Errorf("获取bitget %s 24h行情失败: %v", description, err)
}
// 解析响应数据
var response tickerResponse
if err := sonic.Unmarshal(resp, &response); err != nil {
return []string{}, fmt.Errorf("解析bitget %s 24h行情失败: %v", description, err)
}
// 检查API响应状态
if response.Code != "00000" {
return []string{}, fmt.Errorf("bitget %s API错误: %s", description, response.Msg)
}
// 处理行情数据
deleteSymbols := make([]string, 0)
processedCount := 0
errorCount := 0
for _, item := range response.Data {
// 构建缓存键
key := fmt.Sprintf(tickerType, global.EXCHANGE_BITGET, item.Symbol)
// 检查交易对是否存在
symbol, exists := (*tradeSet)[item.Symbol]
if !exists {
// 清理不存在的交易对缓存
if err := helper.DefaultRedis.DeleteString(key); err != nil {
log.Warn("删除无效缓存失败: %s, error: %v", key, err)
}
continue
}
// 解析交易对获取coin和currency
var coin, currency string
if strings.HasSuffix(item.Symbol, symbol.Currency) {
coin = strings.TrimSuffix(item.Symbol, symbol.Currency)
currency = symbol.Currency
} else {
deleteSymbols = append(deleteSymbols, item.Symbol)
continue
}
// 构建交易对数据
symbol.Coin = coin
symbol.Currency = currency
symbol.OpenPrice = utility.StringAsFloat(item.Open)
symbol.PriceChange = utility.StringAsFloat(item.Change)
symbol.LowPrice = item.Low24h
symbol.HighPrice = item.High24h
symbol.Volume = item.Volume24h
symbol.QuoteVolume = item.QuoteVol
symbol.LastPrice = item.Close
// 验证数据有效性
if symbol.Coin == "" || symbol.Currency == "" ||
utility.StringToFloat64(symbol.QuoteVolume) <= 0 ||
symbol.LastPrice == "" || utility.StringToFloat64(symbol.LastPrice) <= 0 {
if err := helper.DefaultRedis.DeleteString(key); err != nil {
log.Warn("删除无效数据缓存失败: %s, error: %v", key, err)
}
deleteSymbols = append(deleteSymbols, item.Symbol)
continue
}
// 缓存数据
val, err := sonic.Marshal(symbol)
if err != nil {
log.Error("序列化交易对数据失败: %s, error: %v", item.Symbol, err)
errorCount++
continue
}
if err := helper.DefaultRedis.SetString(key, string(val)); err != nil {
log.Error("缓存交易对失败: %s, error: %v", item.Symbol, err)
errorCount++
continue
}
processedCount++
}
log.Info("处理%s行情数据完成: 成功=%d, 错误=%d, 删除=%d",
description, processedCount, errorCount, len(deleteSymbols))
return deleteSymbols, nil
}
// GetSpotTicker24h 获取现货24小时行情变更并缓存
func (api *BitgetRestApi) GetSpotTicker24h(tradeSet *map[string]models.TradeSet) ([]string, error) {
return api.getTicker24hData("/api/v2/spot/market/tickers", global.TICKER_SPOT, "现货", tradeSet)
}
// GetFutTicker24h 获取U本位合约24小时行情变更并缓存
func (api *BitgetRestApi) GetFutTicker24h(tradeSet *map[string]models.TradeSet) ([]string, error) {
return api.getTicker24hData("/api/v2/mix/market/tickers?productType=USDT-FUTURES", global.TICKER_FUTURES, "U本位合约", tradeSet)
}
// BitgetSymbol Bitget交易对信息
type BitgetSymbol struct {
Symbol string `json:"symbol"`
BaseCoin string `json:"baseCoin"`
QuoteCoin string `json:"quoteCoin"`
BaseAsset string `json:"baseAsset"`
QuoteAsset string `json:"quoteAsset"`
Status string `json:"status"`
MinQty string `json:"minQty"`
MaxQty string `json:"maxQty"`
PriceDigit int `json:"priceDigit"`
QuantityDigit int `json:"quantityDigit"`
}
// GetAndReloadSymbols 获取并重新加载交易对信息
// symbolType 1-现货 2-合约
func GetAndReloadSymbols(data *map[string]models.TradeSet, symbolType int) error {
var tradeSets map[string]models.TradeSet
var err error
switch symbolType {
case 1:
tradeSets, _, err = GetSpotSymbols()
case 2:
tradeSets, _, err = GetFutSymbols()
default:
return fmt.Errorf("无效的symbolType: %d", symbolType)
}
if err != nil {
return err
}
*data = tradeSets
return nil
}
// InitSymbolsTicker24h 初始化24小时行情数据
// symbolType 1-现货 2-合约
func InitSymbolsTicker24h(data *map[string]models.TradeSet, symbolType int) ([]string, error) {
api := NewBitgetRestApi()
switch symbolType {
case 1:
return api.GetSpotTicker24h(data)
case 2:
return api.GetFutTicker24h(data)
default:
return []string{}, fmt.Errorf("无效的symbolType: %d", symbolType)
}
}
// GetSpotTicker24 获取单个交易对24h行情
func (api *BitgetRestApi) GetSpotTicker24(symbol string, data *models.Ticker24, tradeSet *models.TradeSet) error {
key := fmt.Sprintf(global.TICKER_SPOT, global.EXCHANGE_BITGET, symbol)
val, err := helper.DefaultRedis.GetString(key)
if err != nil {
return err
}
err = sonic.Unmarshal([]byte(val), tradeSet)
if err != nil {
return err
}
if tradeSet.Coin != "" {
data.HighPrice = tradeSet.HighPrice
data.ChangePercent = fmt.Sprintf("%g", tradeSet.PriceChange)
data.LastPrice = tradeSet.LastPrice
data.LowPrice = tradeSet.LowPrice
data.OpenPrice = fmt.Sprintf("%g", tradeSet.OpenPrice)
data.QuoteVolume = tradeSet.QuoteVolume
data.Volume = tradeSet.Volume
}
return nil
}

View File

@ -0,0 +1,41 @@
package bitgetservice
import (
"go-admin/config"
"go-admin/test"
"testing"
)
func TestGetSpotSymbols(t *testing.T) {
test.InitSettings()
api := NewBitgetRestApi()
symbols, err := api.GetSpotSymbols()
if err != nil {
t.Error(err)
}
t.Log(symbols)
}
// 获取交易所信息
func TestGetExchangeInfo(t *testing.T) {
test.InitSettings()
client, err := NewBitgetClient("", "", "", config.ExtConfig.ProxyUrl)
if err != nil {
t.Error(err)
}
data, err := client.GetExchangeInfo()
if err != nil {
t.Error(err)
}
t.Log(data)
}

View File

@ -0,0 +1,658 @@
package bitgetservice
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"go-admin/pkg/utility"
"net"
"net/http"
"net/url"
"os"
"os/signal"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/bytedance/sonic"
log "github.com/go-admin-team/go-admin-core/logger"
"github.com/gorilla/websocket"
"golang.org/x/net/proxy"
)
// BitgetWebSocketManager Bitget WebSocket管理器
// 类似于BinanceWebSocketManager支持外部传入API密钥
type BitgetWebSocketManager struct {
ws *websocket.Conn
stopChannel chan struct{}
url string
/* 0-现货 1-合约 */
wsType int
apiKey string
apiSecret string
passphrase string
proxyType string
proxyAddress string
reconnect chan struct{}
isStopped bool // 标记 WebSocket 是否已主动停止
mu sync.Mutex // 用于控制并发访问 isStopped
cancelFunc context.CancelFunc
reconnecting atomic.Bool // 防止重复重连
ConnectTime time.Time // 当前连接建立时间
}
// 已有连接存储
var BitgetSpotSockets = map[string]*BitgetWebSocketManager{}
var BitgetFutureSockets = map[string]*BitgetWebSocketManager{}
// NewBitgetWebSocketManager 创建新的Bitget WebSocket管理器
func NewBitgetWebSocketManager(wsType int, apiKey, apiSecret, passphrase, proxyType, proxyAddress string) *BitgetWebSocketManager {
var wsUrl string
switch wsType {
case 0: // 现货
wsUrl = "wss://ws.bitget.com/spot/v1/stream"
case 1: // 合约
wsUrl = "wss://ws.bitget.com/mix/v1/stream"
default:
wsUrl = "wss://ws.bitget.com/spot/v1/stream"
}
return &BitgetWebSocketManager{
stopChannel: make(chan struct{}, 10),
reconnect: make(chan struct{}, 10),
isStopped: false,
url: wsUrl,
wsType: wsType,
apiKey: apiKey,
apiSecret: apiSecret,
passphrase: passphrase,
proxyType: proxyType,
proxyAddress: proxyAddress,
}
}
// GetKey 获取API Key
func (wm *BitgetWebSocketManager) GetKey() string {
return wm.apiKey
}
// Start 启动WebSocket连接
func (wm *BitgetWebSocketManager) Start() {
utility.SafeGo(wm.run)
}
// Restart 重启连接
func (wm *BitgetWebSocketManager) Restart(apiKey, apiSecret, passphrase, proxyType, proxyAddress string) *BitgetWebSocketManager {
wm.mu.Lock()
defer wm.mu.Unlock()
wm.apiKey = apiKey
wm.apiSecret = apiSecret
wm.passphrase = passphrase
wm.proxyType = proxyType
wm.proxyAddress = proxyAddress
if wm.isStopped {
wm.isStopped = false
utility.SafeGo(wm.run)
} else {
log.Warnf("调用Bitget WebSocket restart")
wm.triggerReconnect(true)
}
return wm
}
// triggerReconnect 触发重连
func (wm *BitgetWebSocketManager) triggerReconnect(force bool) {
if force {
wm.reconnecting.Store(false)
}
if wm.reconnecting.CompareAndSwap(false, true) {
log.Warnf("准备重连Bitget WebSocket key: %s wsType: %v", wm.apiKey, wm.wsType)
select {
case wm.reconnect <- struct{}{}:
default:
log.Debugf("Bitget reconnect 信号已存在,跳过 key:%s", wm.apiKey)
}
}
}
// run 主运行循环
func (wm *BitgetWebSocketManager) run() {
ctx, cancel := context.WithCancel(context.Background())
wm.cancelFunc = cancel
utility.SafeGo(wm.handleSignal)
for {
select {
case <-ctx.Done():
return
default:
if err := wm.connect(ctx); err != nil {
wm.handleConnectionError(err)
if wm.isErrorCountExceeded() {
log.Error("连接Bitget WebSocket时出错次数过多停止WebSocket管理器: wsType=%v, key=%s", wm.wsType, wm.apiKey)
wm.Stop()
return
}
time.Sleep(5 * time.Second)
continue
}
<-wm.stopChannel
log.Info("停止Bitget WebSocket管理器... wsType=%s", getBitgetWsTypeName(wm.wsType))
wm.Stop()
return
}
}
}
// handleConnectionError 处理连接错误
func (wm *BitgetWebSocketManager) handleConnectionError(err error) {
// 这里可以添加错误记录到Redis的逻辑类似binance的实现
log.Error("连接Bitget WebSocket时出错: wsType=%v, key=%v, 错误=%v", wm.wsType, wm.apiKey, err)
}
// isErrorCountExceeded 检查错误次数是否超过阈值
func (wm *BitgetWebSocketManager) isErrorCountExceeded() bool {
// 简化版本,可以根据需要实现更复杂的错误计数逻辑
return false
}
// handleSignal 处理终止信号
func (wm *BitgetWebSocketManager) handleSignal() {
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt)
<-ch
wm.Stop()
}
// connect 建立WebSocket连接
func (wm *BitgetWebSocketManager) connect(ctx context.Context) error {
dialer, err := wm.getDialer()
if err != nil {
return err
}
// 对于私有频道,需要进行身份验证
headers := wm.getAuthHeaders()
wm.ws, _, err = dialer.Dial(wm.url, headers)
if err != nil {
return err
}
// 连接成功,更新连接时间
wm.ConnectTime = time.Now()
log.Info("已连接到Bitget %s WebSocket key:%s", getBitgetWsTypeName(wm.wsType), wm.apiKey)
// 设置Ping处理器
wm.ws.SetPingHandler(func(appData string) error {
log.Info("收到Bitget Ping消息 wstype:%v key:%s data:%s", wm.wsType, wm.apiKey, appData)
for x := 0; x < 5; x++ {
if err := wm.ws.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(time.Second*10)); err != nil {
log.Error("Bitget回应Pong失败 次数:%d err:%v", x, err)
time.Sleep(time.Second * 1)
continue
}
break
}
return nil
})
// 启动各种处理协程
utility.SafeGo(func() { wm.readMessages(ctx) })
utility.SafeGo(func() { wm.handleReconnect(ctx) })
utility.SafeGo(func() { wm.startPingLoop(ctx) })
// 发送登录认证消息
if err := wm.authenticate(); err != nil {
return fmt.Errorf("bitget WebSocket认证失败: %v", err)
}
return nil
}
// getDialer 获取WebSocket拨号器
func (wm *BitgetWebSocketManager) getDialer() (*websocket.Dialer, error) {
if wm.proxyAddress == "" {
return &websocket.Dialer{}, nil
}
if !strings.HasPrefix(wm.proxyAddress, "http://") &&
!strings.HasPrefix(wm.proxyAddress, "https://") &&
!strings.HasPrefix(wm.proxyAddress, "socks5://") {
wm.proxyAddress = wm.proxyType + "://" + wm.proxyAddress
}
proxyURL, err := url.Parse(wm.proxyAddress)
if err != nil {
return nil, fmt.Errorf("failed to parse proxy URL: %v", err)
}
switch proxyURL.Scheme {
case "socks5":
return wm.createSocks5Dialer(proxyURL)
case "http", "https":
return &websocket.Dialer{
Proxy: func(req *http.Request) (*url.URL, error) {
return proxyURL, nil
},
}, nil
default:
return nil, fmt.Errorf("unsupported proxy scheme: %s", proxyURL.Scheme)
}
}
// createSocks5Dialer 创建SOCKS5代理拨号器
func (wm *BitgetWebSocketManager) createSocks5Dialer(proxyURL *url.URL) (*websocket.Dialer, error) {
auth := &proxy.Auth{}
if proxyURL.User != nil {
auth.User = proxyURL.User.Username()
auth.Password, _ = proxyURL.User.Password()
}
socksDialer, err := proxy.SOCKS5("tcp", proxyURL.Host, auth, proxy.Direct)
if err != nil {
return nil, fmt.Errorf("failed to create SOCKS5 proxy dialer: %v", err)
}
return &websocket.Dialer{
NetDialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return socksDialer.Dial(network, addr)
},
}, nil
}
// getAuthHeaders 获取认证头信息
func (wm *BitgetWebSocketManager) getAuthHeaders() http.Header {
headers := http.Header{}
if wm.apiKey != "" {
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
sign := wm.generateSignature(timestamp)
headers.Set("ACCESS-KEY", wm.apiKey)
headers.Set("ACCESS-SIGN", sign)
headers.Set("ACCESS-TIMESTAMP", timestamp)
headers.Set("ACCESS-PASSPHRASE", wm.passphrase)
}
return headers
}
// generateSignature 生成签名
func (wm *BitgetWebSocketManager) generateSignature(timestamp string) string {
message := timestamp + "GET" + "/user/verify"
h := hmac.New(sha256.New, []byte(wm.apiSecret))
h.Write([]byte(message))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
// authenticate 发送认证消息
func (wm *BitgetWebSocketManager) authenticate() error {
if wm.apiKey == "" {
return nil // 公共频道不需要认证
}
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
sign := wm.generateSignature(timestamp)
authMsg := map[string]interface{}{
"op": "login",
"args": []map[string]string{
{
"apiKey": wm.apiKey,
"passphrase": wm.passphrase,
"timestamp": timestamp,
"sign": sign,
},
},
}
msgBytes, err := sonic.Marshal(authMsg)
if err != nil {
return err
}
return wm.ws.WriteMessage(websocket.TextMessage, msgBytes)
}
// readMessages 读取消息
func (wm *BitgetWebSocketManager) readMessages(ctx context.Context) {
defer func() {
if r := recover(); r != nil {
log.Error("Bitget WebSocket消息处理器panic: %v", r)
}
}()
for {
select {
case <-ctx.Done():
return
default:
wm.mu.Lock()
if wm.isStopped || wm.ws == nil {
wm.mu.Unlock()
return
}
_, message, err := wm.ws.ReadMessage()
wm.mu.Unlock()
if err != nil {
log.Error("Bitget WebSocket读取消息错误: %v", err)
wm.triggerReconnect(false)
return
}
// 处理消息
wm.handleMessage(message)
}
}
}
// handleMessage 处理收到的消息
func (wm *BitgetWebSocketManager) handleMessage(message []byte) {
// 解析消息并处理
var baseMsg map[string]interface{}
if err := sonic.Unmarshal(message, &baseMsg); err != nil {
log.Error("解析Bitget WebSocket消息失败: %v", err)
return
}
// 检查是否是认证响应
if op, exists := baseMsg["op"]; exists && op == "login" {
wm.handleAuthResponse(baseMsg)
return
}
// 检查是否是订阅数据
if arg, exists := baseMsg["arg"]; exists {
wm.handleSubscriptionData(baseMsg, arg)
return
}
log.Info("收到Bitget WebSocket消息: %s", string(message))
}
// handleAuthResponse 处理认证响应
func (wm *BitgetWebSocketManager) handleAuthResponse(msg map[string]interface{}) {
if code, exists := msg["code"]; exists {
if code == "0" || code == 0 {
log.Info("Bitget WebSocket认证成功 key:%s", wm.apiKey)
// 认证成功后可以订阅私有频道
wm.subscribePrivateChannels()
} else {
log.Error("Bitget WebSocket认证失败 key:%s code:%v", wm.apiKey, code)
}
}
}
// handleSubscriptionData 处理订阅数据
func (wm *BitgetWebSocketManager) handleSubscriptionData(msg map[string]interface{}, arg interface{}) {
// 根据频道类型处理不同的数据
// 这里可以根据具体需求实现不同频道的数据处理逻辑
log.Info("收到Bitget订阅数据: %v", arg)
}
// subscribePrivateChannels 订阅私有频道
func (wm *BitgetWebSocketManager) subscribePrivateChannels() {
// 订阅账户更新
wm.subscribeAccount()
// 订阅订单更新
wm.subscribeOrders()
// 订阅持仓更新
wm.subscribePositions()
}
// subscribeAccount 订阅账户更新
func (wm *BitgetWebSocketManager) subscribeAccount() {
var channel string
if wm.wsType == 0 {
channel = "account"
} else {
channel = "account"
}
subMsg := map[string]interface{}{
"op": "subscribe",
"args": []map[string]string{
{
"instType": wm.getInstType(),
"channel": channel,
"instId": "default",
},
},
}
wm.sendMessage(subMsg)
}
// subscribeOrders 订阅订单更新
func (wm *BitgetWebSocketManager) subscribeOrders() {
var channel string
if wm.wsType == 0 {
channel = "orders"
} else {
channel = "orders"
}
subMsg := map[string]interface{}{
"op": "subscribe",
"args": []map[string]string{
{
"instType": wm.getInstType(),
"channel": channel,
"instId": "default",
},
},
}
wm.sendMessage(subMsg)
}
// subscribePositions 订阅持仓更新(仅合约)
func (wm *BitgetWebSocketManager) subscribePositions() {
if wm.wsType != 1 {
return // 只有合约才有持仓
}
subMsg := map[string]interface{}{
"op": "subscribe",
"args": []map[string]string{
{
"instType": "umcbl",
"channel": "positions",
"instId": "default",
},
},
}
wm.sendMessage(subMsg)
}
// getInstType 获取产品类型
func (wm *BitgetWebSocketManager) getInstType() string {
if wm.wsType == 0 {
return "sp" // 现货
}
return "umcbl" // U本位合约
}
// SendMessage 发送消息(公开方法)
func (wm *BitgetWebSocketManager) SendMessage(msg interface{}) error {
msgBytes, err := sonic.Marshal(msg)
if err != nil {
return err
}
wm.mu.Lock()
defer wm.mu.Unlock()
if wm.ws == nil {
return errors.New("WebSocket连接不可用")
}
return wm.ws.WriteMessage(websocket.TextMessage, msgBytes)
}
// sendMessage 发送消息(私有方法,保持向后兼容)
func (wm *BitgetWebSocketManager) sendMessage(msg interface{}) error {
return wm.SendMessage(msg)
}
// handleReconnect 处理重连
func (wm *BitgetWebSocketManager) handleReconnect(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case <-wm.reconnect:
wm.reconnecting.Store(false)
log.Info("开始重连Bitget WebSocket key:%s", wm.apiKey)
wm.mu.Lock()
if wm.ws != nil {
wm.ws.Close()
wm.ws = nil
}
wm.mu.Unlock()
// 触发主循环重新连接
select {
case wm.stopChannel <- struct{}{}:
default:
}
}
}
}
// startPingLoop 启动心跳循环
func (wm *BitgetWebSocketManager) startPingLoop(ctx context.Context) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
wm.sendPing()
}
}
}
// sendPing 发送心跳
func (wm *BitgetWebSocketManager) sendPing() {
pingMsg := "ping"
wm.mu.Lock()
defer wm.mu.Unlock()
if wm.ws == nil {
return
}
if err := wm.ws.WriteMessage(websocket.TextMessage, []byte(pingMsg)); err != nil {
log.Error("Bitget WebSocket发送ping失败: %v", err)
wm.triggerReconnect(false)
}
}
// Stop 停止WebSocket连接
func (wm *BitgetWebSocketManager) Stop() {
wm.mu.Lock()
defer wm.mu.Unlock()
if wm.isStopped {
return
}
wm.isStopped = true
if wm.cancelFunc != nil {
wm.cancelFunc()
}
if wm.ws != nil {
wm.ws.Close()
wm.ws = nil
}
// 从全局map中移除
if wm.wsType == 0 {
delete(BitgetSpotSockets, wm.apiKey)
} else {
delete(BitgetFutureSockets, wm.apiKey)
}
log.Info("Bitget WebSocket已停止 key:%s", wm.apiKey)
}
// getBitgetWsTypeName 获取WebSocket类型名称
func getBitgetWsTypeName(wsType int) string {
switch wsType {
case 0:
return "现货"
case 1:
return "合约"
default:
return "未知"
}
}
// 工具函数获取或创建Bitget WebSocket管理器
func GetOrCreateBitgetWebSocketManager(wsType int, apiKey, apiSecret, passphrase, proxyType, proxyAddress string) *BitgetWebSocketManager {
var socketsMap map[string]*BitgetWebSocketManager
if wsType == 0 {
socketsMap = BitgetSpotSockets
} else {
socketsMap = BitgetFutureSockets
}
if manager, exists := socketsMap[apiKey]; exists {
// 如果参数有变化,重启连接
if manager.apiSecret != apiSecret || manager.passphrase != passphrase ||
manager.proxyType != proxyType || manager.proxyAddress != proxyAddress {
manager.Restart(apiKey, apiSecret, passphrase, proxyType, proxyAddress)
}
return manager
}
// 创建新的管理器
manager := NewBitgetWebSocketManager(wsType, apiKey, apiSecret, passphrase, proxyType, proxyAddress)
socketsMap[apiKey] = manager
return manager
}
// StopBitgetWebSocketManager 停止指定的WebSocket管理器
func StopBitgetWebSocketManager(wsType int, apiKey string) {
var socketsMap map[string]*BitgetWebSocketManager
if wsType == 0 {
socketsMap = BitgetSpotSockets
} else {
socketsMap = BitgetFutureSockets
}
if manager, exists := socketsMap[apiKey]; exists {
manager.Stop()
}
}

View File

@ -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)

View File

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

View File

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

View File

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

View File

@ -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/%dkey:%serr: %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<<retryCount)
if delay > 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 ping40s 足够
} 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 上下文
*/

View File

@ -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()
}

View File

@ -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 "未知"
}
}

View File

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

63
services/proxy/proxy.go Normal file
View File

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

View File

@ -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)

View File

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

22
test/init_settings.go Normal file
View File

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