Compare commits
	
		
			4 Commits
		
	
	
		
			7b50873de3
			...
			单边仓位_maste
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 643eab3496 | |||
| 556a32cb7c | |||
| 7adba048c8 | |||
| 43f6b5dc4f | 
@ -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
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -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) {
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -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"`
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,7 @@ type LineStrategyTemplate struct {
 | 
			
		||||
	Percentag     decimal.Decimal `json:"percentag" gorm:"type:decimal(10,2);comment:涨跌点数"`
 | 
			
		||||
	CompareType   int             `json:"compareType" gorm:"type:tinyint;comment:比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
 | 
			
		||||
	TimeSlotStart int             `json:"timeSlotStart" gorm:"type:int;comment:时间段开始(分)"`
 | 
			
		||||
	// TimeSlotEnd   int             `json:"timeSlotEnd" gorm:"type:int;comment:时间断截至(分)"`
 | 
			
		||||
	TimeSlotEnd   int             `json:"timeSlotEnd" gorm:"type:int;comment:时间断截至(分)"`
 | 
			
		||||
	models.ModelTime
 | 
			
		||||
	models.ControlBy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -197,7 +197,7 @@ type LineAddPreOrderReq struct {
 | 
			
		||||
	Site                    string          `json:"site" `                       //购买方向
 | 
			
		||||
	BuyPrice                string          `json:"buy_price" vd:"$>0"`          //购买金额 U
 | 
			
		||||
	PricePattern            string          `json:"price_pattern"`               //价格模式
 | 
			
		||||
	Price                   string          `json:"price"`                       //下单价百分比
 | 
			
		||||
	Price                   string          `json:"price" vd:"$>0"`              //下单价百分比
 | 
			
		||||
	Profit                  string          `json:"profit" vd:"$>0"`             //止盈价
 | 
			
		||||
	ProfitNumRatio          decimal.Decimal `json:"profit_num_ratio"`            //止盈数量百分比
 | 
			
		||||
	ProfitTpTpPriceRatio    decimal.Decimal `json:"profit_tp_tp_price_ratio"`    //止盈后止盈价百分比
 | 
			
		||||
@ -295,7 +295,7 @@ func (req LineAddPreOrderReq) Valid() error {
 | 
			
		||||
		return errors.New("主单减仓数量百分比不能为空")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if req.PricePattern != "mixture" && (req.ReducePriceRatio.IsZero() || req.ReducePriceRatio.Cmp(decimal.NewFromInt(100)) >= 0) {
 | 
			
		||||
	if req.ReducePriceRatio.IsZero() || req.ReducePriceRatio.Cmp(decimal.NewFromInt(100)) >= 0 {
 | 
			
		||||
		return errors.New("主单减仓价格百分比错误")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -447,7 +447,7 @@ func (req LineBatchAddPreOrderReq) CheckParams() error {
 | 
			
		||||
		return errors.New("主单减仓数量百分比不能为空")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if req.PricePattern != "mixture" && (req.ReducePriceRatio.Cmp(decimal.Zero) <= 0 || req.ReducePriceRatio.Cmp(decimal.NewFromInt(100)) >= 0) {
 | 
			
		||||
	if req.ReducePriceRatio.Cmp(decimal.Zero) <= 0 || req.ReducePriceRatio.Cmp(decimal.NewFromInt(100)) >= 0 {
 | 
			
		||||
		return errors.New("主单减仓价格百分比错误")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -51,7 +51,7 @@ func (s *LineReduceStrategyItem) Valid() error {
 | 
			
		||||
		return errors.New("百分比不能小于等于0")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if s.QuantityPercent.Cmp(decimal.NewFromInt(100)) >= 0 {
 | 
			
		||||
	if s.QuantityPercent.Cmp(decimal.NewFromInt(100)) > 0 {
 | 
			
		||||
		return errors.New("数量百分比不能大于等于100")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -41,8 +41,8 @@ type LineStrategyTemplateInsertReq struct {
 | 
			
		||||
	Direction     int             `json:"direction" comment:"涨跌方向 1-涨 2-跌"`
 | 
			
		||||
	Percentag     decimal.Decimal `json:"percentag" comment:"涨跌点数"`
 | 
			
		||||
	CompareType   int             `json:"compareType" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
 | 
			
		||||
	TimeSlotStart int             `json:"timeSlotStart" comment:"时间段(分)"`
 | 
			
		||||
	// TimeSlotEnd   int             `json:"timeSlotEnd" comment:"时间断截至(分)"`
 | 
			
		||||
	TimeSlotStart int             `json:"timeSlotStart" comment:"时间段开始(分)"`
 | 
			
		||||
	TimeSlotEnd   int             `json:"timeSlotEnd" comment:"时间断截至(分)"`
 | 
			
		||||
	common.ControlBy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -63,13 +63,13 @@ func (s *LineStrategyTemplateInsertReq) Valid() error {
 | 
			
		||||
		return errors.New("比较类型不合法")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if s.TimeSlotStart < 0 {
 | 
			
		||||
	if s.TimeSlotStart < 0 || s.TimeSlotEnd > 59 {
 | 
			
		||||
		return errors.New("时间段不合法")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// if s.TimeSlotEnd < s.TimeSlotStart {
 | 
			
		||||
	// 	return errors.New("时间段不合法")
 | 
			
		||||
	// }
 | 
			
		||||
	if s.TimeSlotEnd < s.TimeSlotStart {
 | 
			
		||||
		return errors.New("时间段不合法")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@ -83,7 +83,7 @@ func (s *LineStrategyTemplateInsertReq) Generate(model *models.LineStrategyTempl
 | 
			
		||||
	model.Percentag = s.Percentag
 | 
			
		||||
	model.CompareType = s.CompareType
 | 
			
		||||
	model.TimeSlotStart = s.TimeSlotStart
 | 
			
		||||
	// model.TimeSlotEnd = s.TimeSlotEnd
 | 
			
		||||
	model.TimeSlotEnd = s.TimeSlotEnd
 | 
			
		||||
	model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -97,8 +97,8 @@ type LineStrategyTemplateUpdateReq struct {
 | 
			
		||||
	Direction     int             `json:"direction" comment:"涨跌方向 1-涨 2-跌"`
 | 
			
		||||
	Percentag     decimal.Decimal `json:"percentag" comment:"涨跌点数"`
 | 
			
		||||
	CompareType   int             `json:"compareType" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
 | 
			
		||||
	TimeSlotStart int             `json:"timeSlotStart" comment:"时间段(分)"`
 | 
			
		||||
	// TimeSlotEnd   int             `json:"timeSlotEnd" comment:"时间断截至(分)"`
 | 
			
		||||
	TimeSlotStart int             `json:"timeSlotStart" comment:"时间段开始(分)"`
 | 
			
		||||
	TimeSlotEnd   int             `json:"timeSlotEnd" comment:"时间断截至(分)"`
 | 
			
		||||
	common.ControlBy
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -120,13 +120,13 @@ func (s *LineStrategyTemplateUpdateReq) Valid() error {
 | 
			
		||||
		return errors.New("比较类型不合法")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if s.TimeSlotStart < 0 {
 | 
			
		||||
	if s.TimeSlotStart < 0 || s.TimeSlotEnd > 59 {
 | 
			
		||||
		return errors.New("时间段不合法")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// if s.TimeSlotEnd < s.TimeSlotStart {
 | 
			
		||||
	// 	return errors.New("时间段不合法")
 | 
			
		||||
	// }
 | 
			
		||||
	if s.TimeSlotEnd < s.TimeSlotStart {
 | 
			
		||||
		return errors.New("时间段不合法")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@ -139,7 +139,7 @@ func (s *LineStrategyTemplateUpdateReq) Generate(model *models.LineStrategyTempl
 | 
			
		||||
	model.Percentag = s.Percentag
 | 
			
		||||
	model.CompareType = s.CompareType
 | 
			
		||||
	model.TimeSlotStart = s.TimeSlotStart
 | 
			
		||||
	// model.TimeSlotEnd = s.TimeSlotEnd
 | 
			
		||||
	model.TimeSlotEnd = s.TimeSlotEnd
 | 
			
		||||
	model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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{}
 | 
			
		||||
@ -279,8 +279,6 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi
 | 
			
		||||
	futReduceVal, _ := helper.DefaultRedis.GetAllList(spotAddPositionKey)
 | 
			
		||||
	spotAddPositionVal, _ := helper.DefaultRedis.GetAllList(futReduceKey)
 | 
			
		||||
	spotReduceVal, _ := helper.DefaultRedis.GetAllList(spotReduceKey)
 | 
			
		||||
	spotStrategyMap := e.GetStrategyOrderListMap(1)
 | 
			
		||||
	futStrategyMap := e.GetStrategyOrderListMap(2)
 | 
			
		||||
 | 
			
		||||
	for _, v := range futAddPositionVal {
 | 
			
		||||
		sonic.Unmarshal([]byte(v), &addPosition)
 | 
			
		||||
@ -339,14 +337,8 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi
 | 
			
		||||
		if val, ok := spotRedces[order.Id]; ok {
 | 
			
		||||
			helper.DefaultRedis.LRem(spotReduceKey, val)
 | 
			
		||||
		}
 | 
			
		||||
		var tradedSetKey string
 | 
			
		||||
		if order.SymbolType == 1 {
 | 
			
		||||
			tradedSetKey = fmt.Sprintf(global.TICKER_SPOT, order.ExchangeType, order.Symbol)
 | 
			
		||||
		} else {
 | 
			
		||||
			tradedSetKey = fmt.Sprintf(global.TICKER_FUTURES, order.ExchangeType, order.Symbol)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, tradedSetKey)
 | 
			
		||||
		tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, fmt.Sprintf(global.TICKER_SPOT, order.ExchangeType, order.Symbol))
 | 
			
		||||
		redisList.Price = utility.StringToDecimal(redisList.Price).Truncate(int32(tradeSet.PriceDigit)).String()
 | 
			
		||||
		marshal, _ := sonic.Marshal(redisList)
 | 
			
		||||
		if order.SymbolType == 1 {
 | 
			
		||||
@ -357,13 +349,6 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi
 | 
			
		||||
			helper.DefaultRedis.LRem(listKey, string(marshal))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		switch {
 | 
			
		||||
		case order.StrategyTemplateType == 1 && order.SymbolType == 1:
 | 
			
		||||
			e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &spotStrategyMap)
 | 
			
		||||
		case order.StrategyTemplateType == 1 && order.SymbolType == 2:
 | 
			
		||||
			e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &futStrategyMap)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		//会影响持仓的
 | 
			
		||||
		removeSymbolKey := fmt.Sprintf("%v_%s_%s_%s_%v", order.ApiId, order.ExchangeType, order.Symbol, order.Site, order.SymbolType)
 | 
			
		||||
 | 
			
		||||
@ -378,7 +363,6 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		binanceservice.MainClosePositionClearCache(order.Id, order.SymbolType)
 | 
			
		||||
		binanceservice.RemoveReduceReduceCacheByMainId(order.Id, order.SymbolType)
 | 
			
		||||
 | 
			
		||||
		ints = append(ints, order.Id)
 | 
			
		||||
	}
 | 
			
		||||
@ -411,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, ",")
 | 
			
		||||
@ -439,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)
 | 
			
		||||
@ -592,7 +576,6 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
 | 
			
		||||
 | 
			
		||||
		preOrderStatus := models.LinePreOrderStatus{}
 | 
			
		||||
		preOrderStatus.OrderSn = AddOrder.OrderSn
 | 
			
		||||
		mainPrice := utility.StringToDecimal(AddOrder.Price)
 | 
			
		||||
 | 
			
		||||
		//订单配置信息
 | 
			
		||||
		preOrderExts := make([]models.LinePreOrderExt, 0)
 | 
			
		||||
@ -607,12 +590,6 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
 | 
			
		||||
			TpTpPriceRatio:     req.ProfitTpTpPriceRatio,
 | 
			
		||||
			TpSlPriceRatio:     req.ProfitTpSlPriceRatio,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if req.PricePattern == "mixture" {
 | 
			
		||||
			defultExt.TakeProfitRatio = mainPrice.Div(utility.StrToDecimal(req.Profit)).Sub(decimal.NewFromInt(1)).Abs().Mul(decimal.NewFromInt(100)).Truncate(2)
 | 
			
		||||
			defultExt.StopLossRatio = mainPrice.Div(req.StopLoss).Sub(decimal.NewFromInt(1)).Abs().Mul(decimal.NewFromInt(100)).Truncate(2)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		//减仓单
 | 
			
		||||
		defultExt2 := models.LinePreOrderExt{
 | 
			
		||||
			AddType:            2,
 | 
			
		||||
@ -624,12 +601,17 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
 | 
			
		||||
			TakeProfitNumRatio: decimal.NewFromInt(100), //减仓止盈默认100%
 | 
			
		||||
			StopLossRatio:      req.ReduceStopLossRatio,
 | 
			
		||||
		}
 | 
			
		||||
		mainPrice := utility.StringToDecimal(AddOrder.Price)
 | 
			
		||||
		mainAmount := utility.SafeDiv(buyPrice, mainPrice)
 | 
			
		||||
		defultExt.TotalAfter = utility.StrToDecimal(AddOrder.Num).Truncate(int32(tradeSet.AmountDigit))
 | 
			
		||||
		defultExt2.TotalBefore = defultExt.TotalAfter
 | 
			
		||||
 | 
			
		||||
		default2NumPercent := utility.SafeDiv(decimal.NewFromInt(100).Sub(req.ReduceNumRatio), decimal.NewFromInt(100))
 | 
			
		||||
		defultExt2.TotalAfter = mainAmount.Mul(default2NumPercent).Truncate(int32(tradeSet.AmountDigit))
 | 
			
		||||
		defultExt2.ReTakeRatio = utility.SafeDiv(req.ReducePriceRatio, default2NumPercent).Truncate(2)
 | 
			
		||||
 | 
			
		||||
		preOrderExts = append(preOrderExts, defultExt)
 | 
			
		||||
		preOrderExts = append(preOrderExts, defultExt2)
 | 
			
		||||
 | 
			
		||||
		calculateResp := dto.CalculateBreakEvenRatioResp{}
 | 
			
		||||
		mainParam := dto.CalculateBreakEevenRatioReq{
 | 
			
		||||
@ -645,22 +627,15 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
 | 
			
		||||
			AddPositionVal:   req.ReduceNumRatio,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if req.PricePattern == "mixture" {
 | 
			
		||||
			mainParam.LossEndPercent = mainPrice.Div(req.ReducePriceRatio).Sub(decimal.NewFromInt(1)).Abs().Mul(decimal.NewFromInt(100)).Truncate(2)
 | 
			
		||||
			defultExt2.PriceRatio = mainParam.LossEndPercent
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		//计算减仓后
 | 
			
		||||
		defultExt2.ReTakeRatio = utility.SafeDiv(mainParam.LossEndPercent, default2NumPercent).Truncate(2)
 | 
			
		||||
		mainParam.LossEndPercent = req.ReducePriceRatio
 | 
			
		||||
		mainParam.RemainingQuantity = mainAmount
 | 
			
		||||
		e.CalculateBreakEvenRatio(&mainParam, &calculateResp, tradeSet)
 | 
			
		||||
		mainParam.RemainingQuantity = calculateResp.RemainingQuantity //mainAmount.Mul(decimal.NewFromInt(100).Sub(req.ReduceNumRatio).Div(decimal.NewFromInt(100))).Truncate(int32(tradeSet.AmountDigit))
 | 
			
		||||
		mainParam.TotalLossAmountU = calculateResp.TotalLossAmountU   //buyPrice.Mul(req.ReducePriceRatio.Div(decimal.NewFromInt(100)).Truncate(4)).Truncate(int32(tradeSet.PriceDigit))
 | 
			
		||||
		req.ReduceReTakeProfitRatio = calculateResp.Ratio
 | 
			
		||||
		mainParam.LossBeginPercent = mainParam.LossEndPercent
 | 
			
		||||
		mainParam.LossBeginPercent = req.ReducePriceRatio
 | 
			
		||||
		// defultExt.ReTakeRatio = calculateResp.Ratio
 | 
			
		||||
		preOrderExts = append(preOrderExts, defultExt)
 | 
			
		||||
		preOrderExts = append(preOrderExts, defultExt2)
 | 
			
		||||
 | 
			
		||||
		for index, addPosition := range req.Ext {
 | 
			
		||||
			ext := models.LinePreOrderExt{
 | 
			
		||||
@ -932,6 +907,7 @@ func saveOrderCache(req *dto.LineAddPreOrderReq, AddOrder models.LinePreOrder, l
 | 
			
		||||
		list.Percentag = linestrategyTemplate.Percentag
 | 
			
		||||
		list.CompareType = linestrategyTemplate.CompareType
 | 
			
		||||
		list.TimeSlotStart = linestrategyTemplate.TimeSlotStart
 | 
			
		||||
		list.TimeSlotEnd = linestrategyTemplate.TimeSlotEnd
 | 
			
		||||
 | 
			
		||||
		marshal, _ = sonic.Marshal(&list)
 | 
			
		||||
		if AddOrder.SymbolType == global.SYMBOL_SPOT {
 | 
			
		||||
@ -1039,6 +1015,7 @@ func createPreReduceOrder(preOrder *models.LinePreOrder, ext models.LinePreOrder
 | 
			
		||||
 | 
			
		||||
		stopOrder.OrderType = 4
 | 
			
		||||
		stopOrder.Status = 0
 | 
			
		||||
		stopOrder.Rate = ext.PriceRatio.String()
 | 
			
		||||
		stopOrder.Num = ext.TotalAfter.Sub(ext.TotalBefore).Abs().Truncate(int32(tradeSet.AmountDigit)).String()
 | 
			
		||||
		stopOrder.BuyPrice = "0"
 | 
			
		||||
		stopOrder.Rate = ext.PriceRatio.String()
 | 
			
		||||
@ -1078,8 +1055,6 @@ func makeFuturesTakeAndReduce(preOrder *models.LinePreOrder, ext models.LinePreO
 | 
			
		||||
		mainId = preOrder.MainId
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fmt.Println("take", ext.TakeProfitRatio.String())
 | 
			
		||||
	fmt.Println("boo", ext.TakeProfitRatio.Cmp(decimal.Zero))
 | 
			
		||||
	if ext.TakeProfitRatio.Cmp(decimal.Zero) > 0 {
 | 
			
		||||
		// 止盈单
 | 
			
		||||
		profitOrder := models.LinePreOrder{}
 | 
			
		||||
@ -1275,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)
 | 
			
		||||
@ -1342,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 模板快速下单
 | 
			
		||||
@ -1363,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))
 | 
			
		||||
@ -1721,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
 | 
			
		||||
@ -2020,8 +1972,6 @@ func (e *LinePreOrder) ClearUnTriggered() error {
 | 
			
		||||
	var orderLists []models.LinePreOrder
 | 
			
		||||
	positions := map[string]positiondto.LinePreOrderPositioinDelReq{}
 | 
			
		||||
	e.Orm.Model(&models.LinePreOrder{}).Where("main_id = 0 AND pid = 0 AND status = '0'").Find(&orderLists).Unscoped().Delete(&models.LinePreOrder{})
 | 
			
		||||
	spotStrategyMap := e.GetStrategyOrderListMap(1)
 | 
			
		||||
	futStrategyMap := e.GetStrategyOrderListMap(2)
 | 
			
		||||
 | 
			
		||||
	for _, order := range orderLists {
 | 
			
		||||
		redisList := dto.PreOrderRedisList{
 | 
			
		||||
@ -2033,35 +1983,17 @@ func (e *LinePreOrder) ClearUnTriggered() error {
 | 
			
		||||
			OrderSn:     order.OrderSn,
 | 
			
		||||
			QuoteSymbol: order.QuoteSymbol,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var tradedSetKey string
 | 
			
		||||
		if order.SymbolType == 1 {
 | 
			
		||||
			tradedSetKey = fmt.Sprintf(global.TICKER_SPOT, order.ExchangeType, order.Symbol)
 | 
			
		||||
		} else {
 | 
			
		||||
			tradedSetKey = fmt.Sprintf(global.TICKER_FUTURES, order.ExchangeType, order.Symbol)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, tradedSetKey)
 | 
			
		||||
		tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, fmt.Sprintf(global.TICKER_SPOT, order.ExchangeType, order.Symbol))
 | 
			
		||||
		redisList.Price = utility.StringToDecimal(redisList.Price).Truncate(int32(tradeSet.PriceDigit)).String()
 | 
			
		||||
		marshal, _ := sonic.Marshal(redisList)
 | 
			
		||||
 | 
			
		||||
		if order.SymbolType == 1 {
 | 
			
		||||
			key := fmt.Sprintf(rediskey.PreFutOrderList, order.ExchangeType)
 | 
			
		||||
 | 
			
		||||
			helper.DefaultRedis.LRem(key, string(marshal))
 | 
			
		||||
		} else {
 | 
			
		||||
			key := fmt.Sprintf(rediskey.PreSpotOrderList, order.ExchangeType)
 | 
			
		||||
 | 
			
		||||
			helper.DefaultRedis.LRem(key, string(marshal))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		switch {
 | 
			
		||||
		case order.StrategyTemplateType == 1 && order.SymbolType == 1:
 | 
			
		||||
			e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &spotStrategyMap)
 | 
			
		||||
		case order.StrategyTemplateType == 1 && order.SymbolType == 2:
 | 
			
		||||
			e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &futStrategyMap)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		//会影响持仓的
 | 
			
		||||
		removeSymbolKey := fmt.Sprintf("%v_%s_%s_%s_%v", order.ApiId, order.ExchangeType, order.Symbol, order.Site, order.SymbolType)
 | 
			
		||||
 | 
			
		||||
@ -2101,60 +2033,6 @@ func (e *LinePreOrder) ClearUnTriggered() error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 移除待策略待触发单
 | 
			
		||||
func (e *LinePreOrder) RemoveStrategyOrderCache(orderId int, symbolType int, exchangeType string, caches *map[string][]dto.StrategyOrderRedisList) {
 | 
			
		||||
	strategys, _ := (*caches)[exchangeType]
 | 
			
		||||
	var strategyListKey string
 | 
			
		||||
 | 
			
		||||
	if symbolType == 1 {
 | 
			
		||||
		strategyListKey = fmt.Sprintf(rediskey.StrategySpotOrderList, exchangeType)
 | 
			
		||||
	} else {
 | 
			
		||||
		strategyListKey = fmt.Sprintf(rediskey.StrategyFutOrderList, exchangeType)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, strategy := range strategys {
 | 
			
		||||
		if strategy.Id == orderId {
 | 
			
		||||
			strategyVal, _ := sonic.MarshalString(strategy)
 | 
			
		||||
			helper.DefaultRedis.LRem(strategyListKey, strategyVal)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取策略订单缓存列表
 | 
			
		||||
// symbolType 1现货 2合约
 | 
			
		||||
func (e *LinePreOrder) GetStrategyOrderListMap(symbolType int) map[string][]dto.StrategyOrderRedisList {
 | 
			
		||||
	result := make(map[string][]dto.StrategyOrderRedisList)
 | 
			
		||||
	var key string
 | 
			
		||||
	exchanges := []string{global.EXCHANGE_BINANCE}
 | 
			
		||||
 | 
			
		||||
	if symbolType == 1 {
 | 
			
		||||
		key = rediskey.StrategySpotOrderList
 | 
			
		||||
	} else {
 | 
			
		||||
		key = rediskey.StrategyFutOrderList
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, exchange := range exchanges {
 | 
			
		||||
		newKey := fmt.Sprintf(key, exchange)
 | 
			
		||||
		vals, _ := helper.DefaultRedis.GetAllList(newKey)
 | 
			
		||||
		itemData := make([]dto.StrategyOrderRedisList, 0)
 | 
			
		||||
		item := dto.StrategyOrderRedisList{}
 | 
			
		||||
 | 
			
		||||
		for _, v := range vals {
 | 
			
		||||
			sonic.Unmarshal([]byte(v), &item)
 | 
			
		||||
 | 
			
		||||
			if item.Id > 0 {
 | 
			
		||||
				itemData = append(itemData, item)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if len(itemData) > 0 {
 | 
			
		||||
			result[exchange] = itemData
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return result
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (e *LinePreOrder) QueryOrder(req *dto.QueryOrderReq) (res interface{}, err error) {
 | 
			
		||||
	var apiUserInfo models.LineApiUser
 | 
			
		||||
	e.Orm.Model(&models.LineApiUser{}).Where("id = ?", req.ApiId).Find(&apiUserInfo)
 | 
			
		||||
@ -2207,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 == "" {
 | 
			
		||||
@ -2248,18 +2126,15 @@ func (e *LinePreOrder) GenerateOrder(req *dto.LineAddPreOrderReq) error {
 | 
			
		||||
		AddPositionVal:   req.ReduceNumRatio,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if req.PricePattern == "mixture" {
 | 
			
		||||
		mainParam.LossEndPercent = price.Div(req.ReducePriceRatio).Sub(decimal.NewFromInt(1)).Abs().Mul(decimal.NewFromInt(100)).Truncate(2)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//计算减仓后
 | 
			
		||||
	mainParam.LossEndPercent = req.ReducePriceRatio
 | 
			
		||||
	mainParam.RemainingQuantity = mainAmount
 | 
			
		||||
	mainParam.AddType = 2
 | 
			
		||||
	e.CalculateBreakEvenRatio(&mainParam, &calculateResp, tradeSet)
 | 
			
		||||
	mainParam.RemainingQuantity = calculateResp.RemainingQuantity
 | 
			
		||||
	mainParam.TotalLossAmountU = calculateResp.TotalLossAmountU
 | 
			
		||||
	req.ReduceReTakeProfitRatio = calculateResp.Ratio
 | 
			
		||||
	lossBeginPercent = mainParam.LossEndPercent
 | 
			
		||||
	lossBeginPercent = req.ReducePriceRatio
 | 
			
		||||
 | 
			
		||||
	//顺序排序
 | 
			
		||||
	sort.Slice(req.Ext, func(i, j int) bool {
 | 
			
		||||
 | 
			
		||||
@ -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) {
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
@ -1,67 +0,0 @@
 | 
			
		||||
package jobs
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	binancedto "go-admin/models/binancedto"
 | 
			
		||||
	"go-admin/services/binanceservice"
 | 
			
		||||
 | 
			
		||||
	DbModels "go-admin/app/admin/models"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-admin-team/go-admin-core/logger"
 | 
			
		||||
	"github.com/shopspring/decimal"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type BinanceSpotAccountJob struct{}
 | 
			
		||||
 | 
			
		||||
type BinanceFuturesAccountJob struct{}
 | 
			
		||||
 | 
			
		||||
// 币安账户划转
 | 
			
		||||
func (t BinanceSpotAccountJob) Exec(arg interface{}) error {
 | 
			
		||||
	db := getDefaultDb()
 | 
			
		||||
	req := binancedto.BinanceTransfer{
 | 
			
		||||
		Type:       "MAIN_UMFUTURE",
 | 
			
		||||
		Asset:      "USDT",
 | 
			
		||||
		Amount:     decimal.NewFromFloat(0.1),
 | 
			
		||||
		FromSymbol: "USDT",
 | 
			
		||||
		ToSymbol:   "USDT",
 | 
			
		||||
	}
 | 
			
		||||
	var apis []DbModels.LineApiUser
 | 
			
		||||
 | 
			
		||||
	if err := db.Model(&DbModels.LineApiUser{}).Where("open_status = 1").Find(&apis).Error; err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, apiUserInfo := range apis {
 | 
			
		||||
		err := binanceservice.TradeAmount(db, &req, apiUserInfo)
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Errorf("现货划转合约失败, err: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 币安账户划转
 | 
			
		||||
func (t BinanceFuturesAccountJob) Exec(arg interface{}) error {
 | 
			
		||||
	db := getDefaultDb()
 | 
			
		||||
	req := binancedto.BinanceTransfer{
 | 
			
		||||
		Type:       "UMFUTURE_MAIN",
 | 
			
		||||
		Asset:      "USDT",
 | 
			
		||||
		Amount:     decimal.NewFromFloat(0.1),
 | 
			
		||||
		FromSymbol: "USDT",
 | 
			
		||||
		ToSymbol:   "USDT",
 | 
			
		||||
	}
 | 
			
		||||
	var apis []DbModels.LineApiUser
 | 
			
		||||
	if err := db.Model(&DbModels.LineApiUser{}).Where("open_status = 1").Find(&apis).Error; err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, apiUserInfo := range apis {
 | 
			
		||||
		err := binanceservice.TradeAmount(db, &req, apiUserInfo)
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Errorf("合约划转现货失败, err: %s", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@ -1,34 +0,0 @@
 | 
			
		||||
package jobs
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"go-admin/common/helper"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-admin-team/go-admin-core/sdk"
 | 
			
		||||
	"gorm.io/driver/mysql"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestAccountJob(t *testing.T) {
 | 
			
		||||
	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")
 | 
			
		||||
 | 
			
		||||
	accountJob := BinanceSpotAccountJob{}
 | 
			
		||||
	accountJob.Exec(nil)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestFutureAccountJob(t *testing.T) {
 | 
			
		||||
	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")
 | 
			
		||||
 | 
			
		||||
	accountJob := BinanceFuturesAccountJob{}
 | 
			
		||||
	accountJob.Exec(nil)
 | 
			
		||||
}
 | 
			
		||||
@ -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"
 | 
			
		||||
@ -38,9 +38,6 @@ func InitJob() {
 | 
			
		||||
		"MemberExpirationJob":            MemberExpirationJob{},            //会员到期处理
 | 
			
		||||
		"MemberRenwalOrderExpirationJob": MemberRenwalOrderExpirationJob{}, //会员续费订单过期处理
 | 
			
		||||
		"TrxQueryJobs":                   TrxQueryJobs{},                   //订单支付监听
 | 
			
		||||
		"StrategyJob":                    StrategyJob{},                    //下单策略触发
 | 
			
		||||
		"BinanceSpotAccountJob":          BinanceSpotAccountJob{},          //币安现货划转
 | 
			
		||||
		"BinanceFuturesAccountJob":       BinanceFuturesAccountJob{},       //币安合约划转
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -131,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{}
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,6 @@ package jobs
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	models2 "go-admin/app/jobs/models"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	log "github.com/go-admin-team/go-admin-core/logger"
 | 
			
		||||
@ -44,10 +43,7 @@ type ExecJob struct {
 | 
			
		||||
func (e *ExecJob) Run() {
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if err := recover(); err != nil {
 | 
			
		||||
			// 获取调用栈信息
 | 
			
		||||
			buf := make([]byte, 1<<16) // 64KB 缓冲区
 | 
			
		||||
			n := runtime.Stack(buf, false)
 | 
			
		||||
			log.Errorf("脚本任务失败: %v\n%s", err, buf[:n])
 | 
			
		||||
			log.Errorf("脚本任务失败:%v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -1,25 +0,0 @@
 | 
			
		||||
package jobs
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"go-admin/common/global"
 | 
			
		||||
	"go-admin/services/binanceservice"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-admin-team/go-admin-core/logger"
 | 
			
		||||
	"github.com/go-admin-team/go-admin-core/sdk"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type StrategyJob struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 策略下单任务
 | 
			
		||||
func (j StrategyJob) Exec(arg interface{}) error {
 | 
			
		||||
	strategyService := binanceservice.BinanceStrategyOrderService{}
 | 
			
		||||
	db := getDefaultDb()
 | 
			
		||||
	strategyService.Orm = db
 | 
			
		||||
	strategyService.Log = logger.NewHelper(sdk.Runtime.GetLogger()).WithFields(map[string]interface{}{})
 | 
			
		||||
 | 
			
		||||
	//触发币安策略下单
 | 
			
		||||
	strategyService.TriggerStrategyOrder(global.EXCHANGE_BINANCE)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										264
									
								
								cmd/binance/binance.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										264
									
								
								cmd/binance/binance.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										271
									
								
								cmd/bitget/bitget.go
									
									
									
									
									
										Normal 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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								cmd/cobra.go
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								cmd/cobra.go
									
									
									
									
									
								
							@ -3,19 +3,19 @@ package cmd
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"go-admin/cmd/api"
 | 
			
		||||
	"go-admin/cmd/app"
 | 
			
		||||
	"go-admin/cmd/binance"
 | 
			
		||||
	"go-admin/cmd/bitget"
 | 
			
		||||
	"go-admin/cmd/config"
 | 
			
		||||
	"go-admin/cmd/migrate"
 | 
			
		||||
	"go-admin/cmd/usersubscribe"
 | 
			
		||||
	"go-admin/cmd/version"
 | 
			
		||||
	"go-admin/common/global"
 | 
			
		||||
	"os"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-admin-team/go-admin-core/sdk/pkg"
 | 
			
		||||
 | 
			
		||||
	"github.com/spf13/cobra"
 | 
			
		||||
 | 
			
		||||
	"go-admin/cmd/api"
 | 
			
		||||
	"go-admin/cmd/config"
 | 
			
		||||
	"go-admin/cmd/migrate"
 | 
			
		||||
	"go-admin/cmd/version"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var rootCmd = &cobra.Command{
 | 
			
		||||
@ -46,6 +46,8 @@ func tip() {
 | 
			
		||||
func init() {
 | 
			
		||||
	rootCmd.AddCommand(api.StartCmd)
 | 
			
		||||
	rootCmd.AddCommand(usersubscribe.StartCmd)
 | 
			
		||||
	rootCmd.AddCommand(binance.StartCmd)
 | 
			
		||||
	rootCmd.AddCommand(bitget.StartCmd)
 | 
			
		||||
	rootCmd.AddCommand(migrate.StartCmd)
 | 
			
		||||
	rootCmd.AddCommand(version.StartCmd)
 | 
			
		||||
	rootCmd.AddCommand(config.StartCmd)
 | 
			
		||||
 | 
			
		||||
@ -86,6 +86,10 @@ func run() error {
 | 
			
		||||
		clearLogJob(db, ctx)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	//自动重启websocket
 | 
			
		||||
	utility.SafeGo(func() {
 | 
			
		||||
		reconnect(ctx)
 | 
			
		||||
	})
 | 
			
		||||
	// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)
 | 
			
		||||
	quit := make(chan os.Signal, 1)
 | 
			
		||||
	signal.Notify(quit, os.Interrupt)
 | 
			
		||||
@ -133,3 +137,18 @@ func clearLogJob(db *gorm.DB, ctx context.Context) {
 | 
			
		||||
		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:
 | 
			
		||||
		serverinit.RestartConnect()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
@ -46,9 +48,9 @@ const (
 | 
			
		||||
	SpotTrigger            = "spot_trigger_lock:%v_%s"        //现货触发 {apiuserid|symbol}
 | 
			
		||||
	FutTrigger             = "fut_trigger_lock:%v_%s"         //合约触发 {apiuserid|symbol}
 | 
			
		||||
 | 
			
		||||
	//波段现货触发{apiuserid|ordersn}
 | 
			
		||||
	//波段现货触发{apiuserid|symbol}
 | 
			
		||||
	StrategySpotTriggerLock = "strategy_spot_trigger_l:%v_%s"
 | 
			
		||||
	//波段合约触发{apiuserid|ordersn}
 | 
			
		||||
	//波段合约触发{apiuserid|symbol}
 | 
			
		||||
	StrategyFutTriggerLock = "strategy_fut_trigger_l:%v_%s"
 | 
			
		||||
 | 
			
		||||
	//减仓波段合约触发 {apiuserid|symbol}
 | 
			
		||||
@ -95,9 +97,9 @@ const (
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	//现货最后成交价 sort set key:={exchangeType,symbol}   content:={utc:price}
 | 
			
		||||
	//现货最后成交价 sort set {exchangeType,symbol}
 | 
			
		||||
	SpotTickerLastPrice = "spot_ticker_last_price:%s:%s"
 | 
			
		||||
	//合约最后成交价 sort set {exchangeType,symbol}   content:={utc:price}
 | 
			
		||||
	//合约最后成交价 sort set {exchangeType,symbol}
 | 
			
		||||
	FutureTickerLastPrice = "fut_ticker_last_price:%s:%s"
 | 
			
		||||
 | 
			
		||||
	//允许缓存交易对价格的交易对 list
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										107
									
								
								common/factory/exchange_factory.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										107
									
								
								common/factory/exchange_factory.go
									
									
									
									
									
										Normal 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)
 | 
			
		||||
}
 | 
			
		||||
@ -3,6 +3,7 @@ package global
 | 
			
		||||
//交易所类型字典
 | 
			
		||||
const (
 | 
			
		||||
	EXCHANGE_BINANCE  = "binance"
 | 
			
		||||
	EXCHANGE_BITGET   = "bitget"
 | 
			
		||||
	EXCHANGE_OKEX     = "okex"
 | 
			
		||||
	EXCHANGE_GATE     = "gate"
 | 
			
		||||
	EXCHANGE_COINBASE = "coinbase"
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
	//代币-现货新币排序
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										255
									
								
								common/helper/bitgethttphelper.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								common/helper/bitgethttphelper.go
									
									
									
									
									
										Normal 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)
 | 
			
		||||
}
 | 
			
		||||
@ -106,9 +106,6 @@ func (rl *RedisLock) AcquireWait(ctx context.Context) (bool, error) {
 | 
			
		||||
				baseInterval = time.Second
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if baseInterval <= 0 {
 | 
			
		||||
				baseInterval = time.Millisecond * 100 // 至少 100ms
 | 
			
		||||
			}
 | 
			
		||||
			// 随机退避
 | 
			
		||||
			retryInterval := time.Duration(rand.Int63n(int64(baseInterval))) // 随机退避
 | 
			
		||||
			if retryInterval < time.Millisecond*100 {
 | 
			
		||||
@ -132,13 +129,6 @@ func (rl *RedisLock) AcquireWait(ctx context.Context) (bool, error) {
 | 
			
		||||
	return false, ErrFailed
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func safeRandomDuration(max time.Duration) time.Duration {
 | 
			
		||||
	if max <= 0 {
 | 
			
		||||
		return 100 * time.Millisecond // fallback default
 | 
			
		||||
	}
 | 
			
		||||
	return time.Duration(rand.Int63n(int64(max)))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Release 释放锁
 | 
			
		||||
func (rl *RedisLock) Release() (bool, error) {
 | 
			
		||||
	return rl.releaseCtx(context.Background())
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										170
									
								
								common/interfaces/exchange.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								common/interfaces/exchange.go
									
									
									
									
									
										Normal 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"`
 | 
			
		||||
}
 | 
			
		||||
@ -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"`           // 是否使用测试网
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										144
									
								
								config/serverinit/bitget_init.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								config/serverinit/bitget_init.go
									
									
									
									
									
										Normal 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连接已停止")
 | 
			
		||||
}
 | 
			
		||||
@ -8,12 +8,10 @@ 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"
 | 
			
		||||
	"os"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/bytedance/sonic"
 | 
			
		||||
	"github.com/go-admin-team/go-admin-core/logger"
 | 
			
		||||
@ -38,9 +36,6 @@ func BusinessInit(db *gorm.DB) {
 | 
			
		||||
	symbolPriceService.Log = logger.NewHelper(sdk.Runtime.GetLogger()).WithFields(map[string]interface{}{})
 | 
			
		||||
	symbolPriceService.InitCache()
 | 
			
		||||
 | 
			
		||||
	//清理交易对缓存价格
 | 
			
		||||
	clearSymbolPrice()
 | 
			
		||||
 | 
			
		||||
	//初始化订单配置
 | 
			
		||||
	cacheservice.ResetSystemSetting(db)
 | 
			
		||||
	lineApiUser := service.LineApiUser{}
 | 
			
		||||
@ -50,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{}
 | 
			
		||||
@ -149,24 +124,3 @@ func loadApiUser(db *gorm.DB) error {
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 清理交易对价格缓存
 | 
			
		||||
func clearSymbolPrice() error {
 | 
			
		||||
	spotAll, _ := helper.DefaultRedis.ScanKeys("spot_ticker_last_price:*")
 | 
			
		||||
	futAllKey, _ := helper.DefaultRedis.ScanKeys("fut_ticker_last_price:*")
 | 
			
		||||
	beforeTimeUtc := time.Now().UnixMilli()
 | 
			
		||||
 | 
			
		||||
	for _, item := range spotAll {
 | 
			
		||||
		if _, err := helper.DefaultRedis.RemoveBeforeScore(item, float64(beforeTimeUtc)); err != nil {
 | 
			
		||||
			logger.Error("现货 清理交易对价格缓存失败:", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, item := range futAllKey {
 | 
			
		||||
		if _, err := helper.DefaultRedis.RemoveBeforeScore(item, float64(beforeTimeUtc)); err != nil {
 | 
			
		||||
			logger.Error("合约 清理交易对价格缓存失败:", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -84,3 +84,44 @@ func UserSubscribeInit(orm *gorm.DB, ctx context.Context) {
 | 
			
		||||
		}
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 重启连接
 | 
			
		||||
func RestartConnect() error {
 | 
			
		||||
	spotSockets := excservice.SpotSockets
 | 
			
		||||
	futuresSockets := excservice.FutureSockets
 | 
			
		||||
	timeOut := 22 * time.Hour
 | 
			
		||||
 | 
			
		||||
	for _, item := range spotSockets {
 | 
			
		||||
		//超过22小时,重新连接
 | 
			
		||||
		if time.Since(item.ConnectTime) > timeOut {
 | 
			
		||||
			if err := item.ReplaceConnection(); err != nil {
 | 
			
		||||
				log.Errorf("现货重启连接失败 key:%s,error:%s", item.GetKey(), err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, item := range futuresSockets {
 | 
			
		||||
		//超过22小时,重新连接
 | 
			
		||||
		if time.Since(item.ConnectTime) > timeOut {
 | 
			
		||||
			if err := item.ReplaceConnection(); err != nil {
 | 
			
		||||
				log.Errorf("合约重启连接失败 key:%s,error:%s", item.GetKey(), err)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 假死重启
 | 
			
		||||
func DeadCheck() {
 | 
			
		||||
	spotSockets := excservice.SpotSockets
 | 
			
		||||
	futuresSockets := excservice.FutureSockets
 | 
			
		||||
 | 
			
		||||
	for _, item := range spotSockets {
 | 
			
		||||
		item.DeadCheck()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, item := range futuresSockets {
 | 
			
		||||
		item.DeadCheck()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										20
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								go.mod
									
									
									
									
									
								
							@ -1,8 +1,8 @@
 | 
			
		||||
module go-admin
 | 
			
		||||
 | 
			
		||||
go 1.21.0
 | 
			
		||||
go 1.24.0
 | 
			
		||||
 | 
			
		||||
toolchain go1.22.5
 | 
			
		||||
toolchain go1.24.2
 | 
			
		||||
 | 
			
		||||
require (
 | 
			
		||||
	github.com/alibaba/sentinel-golang v1.0.4
 | 
			
		||||
@ -47,8 +47,8 @@ require (
 | 
			
		||||
	github.com/vmihailenco/msgpack/v5 v5.4.1
 | 
			
		||||
	github.com/xuri/excelize/v2 v2.9.0
 | 
			
		||||
	go.uber.org/zap v1.26.0
 | 
			
		||||
	golang.org/x/crypto v0.31.0
 | 
			
		||||
	golang.org/x/net v0.33.0
 | 
			
		||||
	golang.org/x/crypto v0.43.0
 | 
			
		||||
	golang.org/x/net v0.45.0
 | 
			
		||||
	gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
 | 
			
		||||
	gorm.io/driver/mysql v1.5.2
 | 
			
		||||
	gorm.io/driver/postgres v1.5.4
 | 
			
		||||
@ -174,13 +174,13 @@ require (
 | 
			
		||||
	go.uber.org/multierr v1.10.0 // indirect
 | 
			
		||||
	golang.org/x/arch v0.12.0 // indirect
 | 
			
		||||
	golang.org/x/image v0.18.0 // indirect
 | 
			
		||||
	golang.org/x/mod v0.17.0 // indirect
 | 
			
		||||
	golang.org/x/sync v0.10.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.28.0 // indirect
 | 
			
		||||
	golang.org/x/term v0.27.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.21.0 // indirect
 | 
			
		||||
	golang.org/x/mod v0.28.0 // indirect
 | 
			
		||||
	golang.org/x/sync v0.17.0 // indirect
 | 
			
		||||
	golang.org/x/sys v0.37.0 // indirect
 | 
			
		||||
	golang.org/x/term v0.36.0 // indirect
 | 
			
		||||
	golang.org/x/text v0.30.0 // indirect
 | 
			
		||||
	golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
 | 
			
		||||
	golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
 | 
			
		||||
	golang.org/x/tools v0.37.0 // indirect
 | 
			
		||||
	google.golang.org/protobuf v1.36.1 // indirect
 | 
			
		||||
	gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
 | 
			
		||||
	gopkg.in/yaml.v2 v2.4.0 // indirect
 | 
			
		||||
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
package binancedto
 | 
			
		||||
 | 
			
		||||
import "github.com/shopspring/decimal"
 | 
			
		||||
 | 
			
		||||
type BinanceTransfer struct {
 | 
			
		||||
	Type       string          `json:"type" content:"枚举 MAIN_UMFUTURE-现货到u合约"`
 | 
			
		||||
	Asset      string          `json:"asset" content:"币种"`
 | 
			
		||||
	Amount     decimal.Decimal `json:"amount" content:"数量"`
 | 
			
		||||
	FromSymbol string          `json:"fromSymbol" content:"转出币种"`
 | 
			
		||||
	ToSymbol   string          `json:"toSymbol" content:"转入币种"`
 | 
			
		||||
}
 | 
			
		||||
@ -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"`                      //开盘价格
 | 
			
		||||
 | 
			
		||||
@ -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"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
@ -640,43 +641,3 @@ func GetSpotUProperty(apiUserInfo DbModels.LineApiUser, data *dto.LineUserProper
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 万象划转
 | 
			
		||||
func TradeAmount(db *gorm.DB, req *binancedto.BinanceTransfer, apiUserInfo DbModels.LineApiUser) error {
 | 
			
		||||
	url := "/sapi/v1/asset/transfer"
 | 
			
		||||
 | 
			
		||||
	client := GetClient(&apiUserInfo)
 | 
			
		||||
 | 
			
		||||
	params := map[string]string{
 | 
			
		||||
		"type":       req.Type,
 | 
			
		||||
		"asset":      req.Asset,
 | 
			
		||||
		"amount":     req.Amount.String(),
 | 
			
		||||
		"fromSymbol": req.FromSymbol,
 | 
			
		||||
		"toSymbol":   req.ToSymbol,
 | 
			
		||||
		"recvWindow": "10000",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, code, err := client.SendSpotAuth(url, "POST", params)
 | 
			
		||||
	if err != nil || code != 200 {
 | 
			
		||||
		log.Error("万向划转失败 参数:", params)
 | 
			
		||||
		log.Error("万向划转失败 code:", code)
 | 
			
		||||
		log.Error("万向划转失败 err:", err)
 | 
			
		||||
		dataMap := make(map[string]interface{})
 | 
			
		||||
		if err.Error() != "" {
 | 
			
		||||
			if err := sonic.Unmarshal([]byte(err.Error()), &dataMap); err != nil {
 | 
			
		||||
				return fmt.Errorf("api_id:%d 万向划转失败:%+v", apiUserInfo.Id, err.Error())
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		code, ok := dataMap["code"]
 | 
			
		||||
		if ok {
 | 
			
		||||
			return fmt.Errorf("api_id:%d 万向划转失败:%s", apiUserInfo.Id, ErrorMaps[code.(float64)])
 | 
			
		||||
		}
 | 
			
		||||
		if strings.Contains(err.Error(), "Unknown order sent.") {
 | 
			
		||||
			return fmt.Errorf("api_id:%d 万向划转失败:%+v", apiUserInfo.Id, ErrorMaps[-2011])
 | 
			
		||||
		}
 | 
			
		||||
		return fmt.Errorf("api_id:%d 万向划转失败:%+v", apiUserInfo.Id, err.Error())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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,58 +209,16 @@ 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合约
 | 
			
		||||
func MainClosePositionClearCache(mainId int, symbolType int) {
 | 
			
		||||
	strategyDto := dto.StrategyOrderRedisList{}
 | 
			
		||||
 | 
			
		||||
	if symbolType == 1 {
 | 
			
		||||
		keySpotStop := fmt.Sprintf(rediskey.SpotStopLossList, global.EXCHANGE_BINANCE)
 | 
			
		||||
		keySpotAddposition := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE)
 | 
			
		||||
		spotStopArray, _ := helper.DefaultRedis.GetAllList(keySpotStop)
 | 
			
		||||
		spotAddpositionArray, _ := helper.DefaultRedis.GetAllList(keySpotAddposition)
 | 
			
		||||
		spotStrategyKey := fmt.Sprintf(rediskey.StrategySpotOrderList, global.EXCHANGE_BINANCE)
 | 
			
		||||
		spotStrategyList, _ := helper.DefaultRedis.GetAllList(spotStrategyKey)
 | 
			
		||||
		var position AddPositionList
 | 
			
		||||
		var position positiondto.AddPositionList
 | 
			
		||||
		var stop dto.StopLossRedisList
 | 
			
		||||
 | 
			
		||||
		for _, item := range spotAddpositionArray {
 | 
			
		||||
@ -283,24 +241,12 @@ func MainClosePositionClearCache(mainId int, symbolType int) {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, item := range spotStrategyList {
 | 
			
		||||
			sonic.Unmarshal([]byte(item), &strategyDto)
 | 
			
		||||
 | 
			
		||||
			if strategyDto.Id == mainId {
 | 
			
		||||
				if _, err := helper.DefaultRedis.LRem(spotStrategyKey, item); err != nil {
 | 
			
		||||
					logger.Errorf("id:%d 移除缓存失败,err:%v", mainId, err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		keyFutStop := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE)
 | 
			
		||||
		keyFutAddposition := fmt.Sprintf(rediskey.FuturesStopLossList, global.EXCHANGE_BINANCE)
 | 
			
		||||
		futAddpositionArray, _ := helper.DefaultRedis.GetAllList(keyFutStop)
 | 
			
		||||
		futStopArray, _ := helper.DefaultRedis.GetAllList(keyFutAddposition)
 | 
			
		||||
 | 
			
		||||
		futStrategyKey := fmt.Sprintf(rediskey.StrategyFutOrderList, global.EXCHANGE_BINANCE)
 | 
			
		||||
		futStrategyList, _ := helper.DefaultRedis.GetAllList(futStrategyKey)
 | 
			
		||||
		var position AddPositionList
 | 
			
		||||
		var position positiondto.AddPositionList
 | 
			
		||||
		var stop dto.StopLossRedisList
 | 
			
		||||
 | 
			
		||||
		for _, item := range futAddpositionArray {
 | 
			
		||||
@ -322,28 +268,9 @@ func MainClosePositionClearCache(mainId int, symbolType int) {
 | 
			
		||||
				helper.DefaultRedis.LRem(keyFutStop, item)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, item := range futStrategyList {
 | 
			
		||||
			sonic.Unmarshal([]byte(item), &strategyDto)
 | 
			
		||||
 | 
			
		||||
			if strategyDto.Id == mainId {
 | 
			
		||||
				if _, err := helper.DefaultRedis.LRem(futStrategyKey, item); err != nil {
 | 
			
		||||
					logger.Errorf("id:%d 移除缓存失败,err:%v", mainId, err)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取数据库连接
 | 
			
		||||
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{}
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
 | 
			
		||||
@ -28,8 +29,8 @@ func TestFutureJudge(t *testing.T) {
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE)
 | 
			
		||||
	item := `{"id":10,"apiId":49,"mainId":7,"pid":7,"symbol":"ADAUSDT","price":"0.6244","side":"BUY","num":"12","orderSn":"398690240274890752"}`
 | 
			
		||||
	reduceOrder := ReduceListItem{}
 | 
			
		||||
	item := `{"id":50,"apiId":49,"mainId":47,"pid":47,"symbol":"ADAUSDT","price":"0.5936","side":"SELL","num":"12","orderSn":"397913127842217984"}`
 | 
			
		||||
	reduceOrder := positiondto.ReduceListItem{}
 | 
			
		||||
	futApi := FutRestApi{}
 | 
			
		||||
	setting, err := cacheservice.GetSystemSetting(db)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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) {
 | 
			
		||||
@ -555,6 +555,50 @@ func (e FutRestApi) OrderPlace(orm *gorm.DB, params FutOrderPlace) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ClosePositionB 平仓B对应的交易对
 | 
			
		||||
// bApiUserInfo B 账户api-user信息
 | 
			
		||||
// symbol 需要平仓的交易对
 | 
			
		||||
// closeType 平仓模式 ALL = 全平 reduceOnly = 只减仓
 | 
			
		||||
// func (e FutRestApi) ClosePositionB(orm *gorm.DB, bApiUserInfo *DbModels.LineApiUser, symbol string, closeType string) error {
 | 
			
		||||
// 	if bApiUserInfo == nil {
 | 
			
		||||
// 		return errors.New("缺失平仓账户信息")
 | 
			
		||||
// 	}
 | 
			
		||||
// 	if symbol == "" {
 | 
			
		||||
// 		return errors.New("缺失平仓交易对信息")
 | 
			
		||||
// 	}
 | 
			
		||||
// 	risks, err := e.GetPositionV3(bApiUserInfo, symbol)
 | 
			
		||||
// 	if err != nil {
 | 
			
		||||
// 		return err
 | 
			
		||||
// 	}
 | 
			
		||||
 | 
			
		||||
// 	for _, risk := range risks {
 | 
			
		||||
// 		if risk.Symbol == strings.ToUpper(symbol) {
 | 
			
		||||
// 			//持仓数量
 | 
			
		||||
// 			positionAmt, _ := decimal.NewFromString(risk.PositionAmt)
 | 
			
		||||
// 			side := "BUY"
 | 
			
		||||
// 			if positionAmt.GreaterThan(decimal.Zero) {
 | 
			
		||||
// 				side = "SELL"
 | 
			
		||||
// 			}
 | 
			
		||||
// 			var closeAmt decimal.Decimal
 | 
			
		||||
// 			if strings.ToUpper(closeType) == "ALL" { //全部平仓
 | 
			
		||||
// 				closeAmt = positionAmt
 | 
			
		||||
// 			} else {
 | 
			
		||||
// 				//如果是只减仓的话 数量=仓位数量*比例
 | 
			
		||||
// 				closeAmt = positionAmt.Mul(decimal.NewFromFloat(reduceOnlyRate))
 | 
			
		||||
// 			}
 | 
			
		||||
 | 
			
		||||
// 			err = e.ClosePosition(symbol, closeAmt, side, *bApiUserInfo, "MARKET", "", decimal.Zero)
 | 
			
		||||
// 			if err != nil {
 | 
			
		||||
// 				return err
 | 
			
		||||
// 			}
 | 
			
		||||
// 			orm.Model(&DbModels.LinePreOrder{}).Where("api_id = ? AND symbol = ?", bApiUserInfo.Id, symbol).Updates(map[string]interface{}{
 | 
			
		||||
// 				"status": "6",
 | 
			
		||||
// 			})
 | 
			
		||||
// 		}
 | 
			
		||||
// 	}
 | 
			
		||||
// 	return nil
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
// 获取合约 持仓价格、数量
 | 
			
		||||
// symbol:交易对
 | 
			
		||||
// side:方向
 | 
			
		||||
@ -818,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)
 | 
			
		||||
@ -829,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))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -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 == "" {
 | 
			
		||||
@ -277,10 +282,9 @@ func FuturesReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, futApi FutRes
 | 
			
		||||
	} else if ok {
 | 
			
		||||
		defer lock.Release()
 | 
			
		||||
		takeOrders := make([]DbModels.LinePreOrder, 0)
 | 
			
		||||
		//只取消减仓单 止盈止损减仓成功后取消
 | 
			
		||||
		if err := db.Model(&DbModels.LinePreOrder{}).Where("main_id =? AND order_type IN 4 AND status IN (1,5)", reduceOrder.MainId).Find(&takeOrders).Error; err != nil {
 | 
			
		||||
		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 {
 | 
			
		||||
			log.Error("查询止盈单失败")
 | 
			
		||||
			// return false
 | 
			
		||||
			return false
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		var hasrecord bool
 | 
			
		||||
@ -363,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
 | 
			
		||||
@ -371,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
 | 
			
		||||
@ -395,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 {
 | 
			
		||||
@ -405,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)
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
@ -145,8 +146,6 @@ func handleReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
 | 
			
		||||
	}
 | 
			
		||||
	// rate := utility.StringAsFloat(orderExt.AddPositionVal)
 | 
			
		||||
 | 
			
		||||
	//取消委托中的止盈止损
 | 
			
		||||
	cancelTakeProfitByReduce(db, apiUserInfo, preOrder.Symbol, preOrder.MainId, preOrder.SymbolType)
 | 
			
		||||
	// 100%减仓 终止流程
 | 
			
		||||
	if orderExt.AddPositionVal.Cmp(decimal.NewFromInt(100)) >= 0 {
 | 
			
		||||
		//缓存
 | 
			
		||||
@ -181,36 +180,6 @@ func handleReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 减仓成功后取消止盈止损
 | 
			
		||||
func cancelTakeProfitByReduce(db *gorm.DB, apiUserInfo DbModels.LineApiUser, symbol string, mainId int, symbolType int) {
 | 
			
		||||
	orders, err := GetSymbolTakeAndStop(db, mainId, symbolType)
 | 
			
		||||
	futApi := FutRestApi{}
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		logger.Errorf("mainId:%d 获取委托中的止盈止损失败:%v", mainId, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	orderSns := make([]string, 0)
 | 
			
		||||
 | 
			
		||||
	for _, v := range orders {
 | 
			
		||||
		if v.OrderType != 1 && v.OrderType != 2 {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		orderSns = append(orderSns, v.OrderSn)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	arr := utility.SplitSlice(orderSns, 10)
 | 
			
		||||
 | 
			
		||||
	for _, v := range arr {
 | 
			
		||||
		err := futApi.CancelBatchFutOrder(apiUserInfo, symbol, v)
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Errorf("mainId:%d 取消止盈止损失败:%v", mainId, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 减仓处理止盈止损
 | 
			
		||||
func FutTakeProfit(db *gorm.DB, preOrder *DbModels.LinePreOrder, apiUserInfo DbModels.LineApiUser, tradeSet models2.TradeSet,
 | 
			
		||||
	positionData positiondto.PositionDto, orderExt DbModels.LinePreOrderExt, manualTakeRatio, manualStopRatio decimal.Decimal) bool {
 | 
			
		||||
@ -298,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)
 | 
			
		||||
@ -470,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)
 | 
			
		||||
 | 
			
		||||
@ -515,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)
 | 
			
		||||
@ -554,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,
 | 
			
		||||
@ -760,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,
 | 
			
		||||
@ -788,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,
 | 
			
		||||
@ -825,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,
 | 
			
		||||
 | 
			
		||||
@ -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"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
)
 | 
			
		||||
@ -134,7 +129,7 @@ func GetTotalLossAmount(db *gorm.DB, mainId int) (decimal.Decimal, error) {
 | 
			
		||||
	return totalLossAmountU, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取交易对的 委托中的止盈止损、减仓单
 | 
			
		||||
// 获取交易对的 委托中的止盈止损
 | 
			
		||||
// mainId 主单id
 | 
			
		||||
// symbolType 交易对类型
 | 
			
		||||
func GetSymbolTakeAndStop(db *gorm.DB, mainId int, symbolType int) ([]models.LinePreOrder, error) {
 | 
			
		||||
@ -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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -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("获取交易对失败")
 | 
			
		||||
 | 
			
		||||
@ -12,42 +12,30 @@ import (
 | 
			
		||||
	models2 "go-admin/models"
 | 
			
		||||
	"go-admin/pkg/utility"
 | 
			
		||||
	"go-admin/services/cacheservice"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/bytedance/sonic"
 | 
			
		||||
	"github.com/go-admin-team/go-admin-core/logger"
 | 
			
		||||
	"github.com/go-admin-team/go-admin-core/sdk/service"
 | 
			
		||||
	"github.com/shopspring/decimal"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type BinanceStrategyOrderService struct {
 | 
			
		||||
	service.Service
 | 
			
		||||
	Debug bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 判断是否触发波段订单
 | 
			
		||||
func (e *BinanceStrategyOrderService) TriggerStrategyOrder(exchangeType string) {
 | 
			
		||||
	//现货
 | 
			
		||||
	orderStrs, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.StrategySpotOrderList, exchangeType))
 | 
			
		||||
	if len(orderStrs) > 0 {
 | 
			
		||||
		e.DoJudge(orderStrs, 1, exchangeType)
 | 
			
		||||
	}
 | 
			
		||||
	orderStrs, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.StrategyFutOrderList, exchangeType))
 | 
			
		||||
	e.DoJudge(orderStrs, 1, exchangeType)
 | 
			
		||||
 | 
			
		||||
	//合约
 | 
			
		||||
	futOrdedrStrs, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.StrategyFutOrderList, exchangeType))
 | 
			
		||||
 | 
			
		||||
	if len(futOrdedrStrs) > 0 {
 | 
			
		||||
		e.DoJudge(futOrdedrStrs, 2, exchangeType)
 | 
			
		||||
	}
 | 
			
		||||
	e.DoJudge(futOrdedrStrs, 2, exchangeType)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 判断是否符合条件
 | 
			
		||||
// 处理订单
 | 
			
		||||
func (e *BinanceStrategyOrderService) DoJudge(orderStrs []string, symbolType int, exchangeType string) {
 | 
			
		||||
	db := GetDBConnection()
 | 
			
		||||
	// setting, _ := cacheservice.GetSystemSetting(db)
 | 
			
		||||
 | 
			
		||||
	for _, orderStr := range orderStrs {
 | 
			
		||||
		var lockKey string
 | 
			
		||||
		orderItem := dto.StrategyOrderRedisList{}
 | 
			
		||||
@ -63,7 +51,7 @@ func (e *BinanceStrategyOrderService) DoJudge(orderStrs []string, symbolType int
 | 
			
		||||
			lockKey = rediskey.StrategyFutTriggerLock
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		lock := helper.NewRedisLock(fmt.Sprintf(lockKey, orderItem.ApiId, orderItem.OrderSn), 60, 20, 300*time.Millisecond)
 | 
			
		||||
		lock := helper.NewRedisLock(fmt.Sprintf(lockKey, orderItem.ApiId, orderItem.Symbol), 200, 50, 100*time.Millisecond)
 | 
			
		||||
 | 
			
		||||
		if ok, err := lock.AcquireWait(context.Background()); err != nil {
 | 
			
		||||
			e.Log.Debug("获取锁失败", err)
 | 
			
		||||
@ -72,14 +60,14 @@ func (e *BinanceStrategyOrderService) DoJudge(orderStrs []string, symbolType int
 | 
			
		||||
			defer lock.Release()
 | 
			
		||||
 | 
			
		||||
			//判断是否符合条件
 | 
			
		||||
			success, err := e.JudgeStrategy(orderItem, symbolType, exchangeType)
 | 
			
		||||
			success, err := e.JudgeStrategy(orderItem, 1, exchangeType)
 | 
			
		||||
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				e.Log.Errorf("order_id:%d err:%v", orderItem.Id, err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if e.Debug || success {
 | 
			
		||||
				e.TriggerOrder(db, orderStr, orderItem, symbolType)
 | 
			
		||||
			if success {
 | 
			
		||||
				e.TriggerOrder(orderItem, symbolType)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@ -107,16 +95,8 @@ func (e *BinanceStrategyOrderService) JudgeStrategy(order dto.StrategyOrderRedis
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	score := lastPrices[0].Score
 | 
			
		||||
	var startPrice decimal.Decimal
 | 
			
		||||
	var lastPrice decimal.Decimal
 | 
			
		||||
	startPriceArgs := strings.Split(beforePrice, ":")
 | 
			
		||||
	endPricecArgs := strings.Split(lastPrices[0].Member.(string), ":")
 | 
			
		||||
 | 
			
		||||
	if len(startPriceArgs) == 0 || len(endPricecArgs) == 0 {
 | 
			
		||||
		return result, errors.New("获取交易对起止价格失败")
 | 
			
		||||
	}
 | 
			
		||||
	startPrice = utility.StrToDecimal(startPriceArgs[len(startPriceArgs)-1])
 | 
			
		||||
	lastPrice = utility.StrToDecimal(endPricecArgs[len(endPricecArgs)-1])
 | 
			
		||||
	startPrice := utility.StrToDecimal(beforePrice)
 | 
			
		||||
	lastPrice := utility.StrToDecimal(lastPrices[0].Member.(string))
 | 
			
		||||
 | 
			
		||||
	//时间差超过10s
 | 
			
		||||
	if (nowUtc-int64(score))/1000 > 10 {
 | 
			
		||||
@ -128,35 +108,31 @@ func (e *BinanceStrategyOrderService) JudgeStrategy(order dto.StrategyOrderRedis
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	percentag := lastPrice.Div(startPrice).Sub(decimal.NewFromInt(1)).Truncate(6)
 | 
			
		||||
	logger.Infof("百分比:%s", percentag.Mul(decimal.NewFromInt(100)).String())
 | 
			
		||||
 | 
			
		||||
	//价格没有变动
 | 
			
		||||
	if percentag.Cmp(decimal.Zero) == 0 {
 | 
			
		||||
		return result, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//满足条件
 | 
			
		||||
	switch {
 | 
			
		||||
	//涨价格大于0.5% 跌价格小于-0.5%
 | 
			
		||||
	case order.CompareType == 1 && order.Direction == 1 && percentag.Cmp(decimal.Zero) > 0,
 | 
			
		||||
		order.CompareType == 1 && order.Direction == 2 && percentag.Cmp(decimal.Zero) < 0:
 | 
			
		||||
		result = percentag.Abs().Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) > 0
 | 
			
		||||
 | 
			
		||||
	case order.CompareType == 2 && order.Direction == 1 && percentag.Cmp(decimal.Zero) > 0,
 | 
			
		||||
		order.CompareType == 2 && order.Direction == 2 && percentag.Cmp(decimal.Zero) < 0:
 | 
			
		||||
		result = percentag.Abs().Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) >= 0
 | 
			
		||||
 | 
			
		||||
	case order.CompareType == 5 && order.Direction == 1 && percentag.Cmp(decimal.Zero) > 0,
 | 
			
		||||
		order.CompareType == 5 && order.Direction == 2 && percentag.Cmp(decimal.Zero) < 0:
 | 
			
		||||
		result = percentag.Abs().Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) == 0
 | 
			
		||||
	switch order.CompareType {
 | 
			
		||||
	case 1:
 | 
			
		||||
		result = percentag.Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) > 0
 | 
			
		||||
	case 2:
 | 
			
		||||
		result = percentag.Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) >= 0
 | 
			
		||||
	case 5:
 | 
			
		||||
		result = percentag.Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) == 0
 | 
			
		||||
	default:
 | 
			
		||||
		return result, errors.New("没有对应的类型")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return result, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 触发委托单
 | 
			
		||||
func (e *BinanceStrategyOrderService) TriggerOrder(db *gorm.DB, cacheVal string, order dto.StrategyOrderRedisList, symbolType int) error {
 | 
			
		||||
func (e *BinanceStrategyOrderService) TriggerOrder(order dto.StrategyOrderRedisList, symbolType int) error {
 | 
			
		||||
	orders := make([]models.LinePreOrder, 0)
 | 
			
		||||
	if err := e.Orm.Model(&models.LinePreOrder{}).Where("main_id =? or id =?", order.Id, order.Id).Find(&orders).Error; err != nil {
 | 
			
		||||
	if err := e.Orm.Model(&models.LinePreOrder{}).Where("main_id =?", order.Id).Find(&orders).Error; err != nil {
 | 
			
		||||
		e.Log.Errorf("order_id:%d 获取委托单失败:%s", order.Id, err.Error())
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
@ -167,16 +143,7 @@ func (e *BinanceStrategyOrderService) TriggerOrder(db *gorm.DB, cacheVal string,
 | 
			
		||||
		return errors.New("获取系统设置失败")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var tradeSet models2.TradeSet
 | 
			
		||||
 | 
			
		||||
	switch symbolType {
 | 
			
		||||
	case 1:
 | 
			
		||||
		tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 0)
 | 
			
		||||
	case 2:
 | 
			
		||||
		tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1)
 | 
			
		||||
	default:
 | 
			
		||||
		return errors.New("获取交易对行情失败,交易对类型错误")
 | 
			
		||||
	}
 | 
			
		||||
	tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, symbolType)
 | 
			
		||||
 | 
			
		||||
	if tradeSet.Coin == "" {
 | 
			
		||||
		return errors.New("获取交易对行情失败")
 | 
			
		||||
@ -188,7 +155,7 @@ func (e *BinanceStrategyOrderService) TriggerOrder(db *gorm.DB, cacheVal string,
 | 
			
		||||
		return errors.New("最新成交价小于等于0")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	mainOrder := models.LinePreOrder{}
 | 
			
		||||
	var mainOrder models.LinePreOrder
 | 
			
		||||
 | 
			
		||||
	for _, v := range orders {
 | 
			
		||||
		if v.MainId == 0 {
 | 
			
		||||
@ -197,307 +164,62 @@ func (e *BinanceStrategyOrderService) TriggerOrder(db *gorm.DB, cacheVal string,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if mainOrder.Id == 0 {
 | 
			
		||||
		return errors.New("获取主单失败")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	GetOrderByPid(&mainOrder, orders, mainOrder.Id)
 | 
			
		||||
 | 
			
		||||
	e.RecalculateOrder(tradeSet, &mainOrder, setting)
 | 
			
		||||
 | 
			
		||||
	//事务保存
 | 
			
		||||
	err := e.Orm.Transaction(func(tx *gorm.DB) error {
 | 
			
		||||
		if err1 := tx.Save(&mainOrder).Error; err1 != nil {
 | 
			
		||||
			return err1
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, v := range mainOrder.Childs {
 | 
			
		||||
			if err1 := tx.Save(&v).Error; err1 != nil {
 | 
			
		||||
				return err1
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for _, v2 := range v.Childs {
 | 
			
		||||
				if err1 := tx.Save(&v2).Error; err1 != nil {
 | 
			
		||||
					return err1
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		e.Log.Errorf("order_id:%d 波段触发保存委托单失败:%s", mainOrder.Id, err.Error())
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	e.StrategyOrderPlace(db, cacheVal, mainOrder, tradeSet)
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 策略触发订单
 | 
			
		||||
// cacheVal:缓存值
 | 
			
		||||
// mainOrder:主订单
 | 
			
		||||
// tradeSet:交易对行情
 | 
			
		||||
func (e *BinanceStrategyOrderService) StrategyOrderPlace(db *gorm.DB, cacheVal string, mainOrder models.LinePreOrder, tradeSet models2.TradeSet) {
 | 
			
		||||
	price := utility.StringToDecimal(mainOrder.Price).Truncate(int32(tradeSet.PriceDigit))
 | 
			
		||||
	num := utility.StringToDecimal(mainOrder.Num).Truncate(int32(tradeSet.AmountDigit))
 | 
			
		||||
	futApi := FutRestApi{}
 | 
			
		||||
	var key string
 | 
			
		||||
 | 
			
		||||
	switch mainOrder.SymbolType {
 | 
			
		||||
	case 1:
 | 
			
		||||
		key = fmt.Sprintf(rediskey.StrategySpotOrderList, global.EXCHANGE_BINANCE)
 | 
			
		||||
	case 2:
 | 
			
		||||
		key = fmt.Sprintf(rediskey.StrategyFutOrderList, global.EXCHANGE_BINANCE)
 | 
			
		||||
	default:
 | 
			
		||||
		logger.Errorf("id:%d 交易对类型不存在:%d", mainOrder.Id, mainOrder.SymbolType)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	params := FutOrderPlace{
 | 
			
		||||
		ApiId:            mainOrder.ApiId,
 | 
			
		||||
		Symbol:           mainOrder.Symbol,
 | 
			
		||||
		Side:             mainOrder.Site,
 | 
			
		||||
		OrderType:        mainOrder.MainOrderType,
 | 
			
		||||
		SideType:         mainOrder.MainOrderType,
 | 
			
		||||
		Price:            price,
 | 
			
		||||
		Quantity:         num,
 | 
			
		||||
		NewClientOrderId: mainOrder.OrderSn,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := futApi.OrderPlaceLoop(db, params, 3); err != nil {
 | 
			
		||||
		logger.Error("下单失败", mainOrder.Symbol, " err:", err)
 | 
			
		||||
		if _, err := helper.DefaultRedis.LRem(key, cacheVal); err != nil {
 | 
			
		||||
			logger.Error("删除redis 预下单失败:", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err := db.Model(&models.LinePreOrder{}).Where("id =? and status='0'", mainOrder.Id).Updates(map[string]interface{}{"status": "2", "desc": err.Error()}).Error
 | 
			
		||||
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			logger.Error("更新预下单状态失败")
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return
 | 
			
		||||
	} else {
 | 
			
		||||
		if _, err := helper.DefaultRedis.LRem(key, cacheVal); err != nil {
 | 
			
		||||
			logger.Error("删除redis 预下单失败:", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := db.Model(&models.LinePreOrder{}).Where("id =? ", mainOrder.Id).Updates(map[string]interface{}{"trigger_time": time.Now()}).Error; err != nil {
 | 
			
		||||
		logger.Error("更新预下单状态失败 ordersn:", mainOrder.OrderSn, " status:1")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := db.Model(&models.LinePreOrder{}).Where("id =? AND status ='0'", mainOrder.Id).Updates(map[string]interface{}{"status": "1"}).Error; err != nil {
 | 
			
		||||
		logger.Error("更新预下单状态失败 ordersn:", mainOrder.OrderSn, " status:1")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 重新计算订单单价、数量
 | 
			
		||||
// tradeSet 交易对行情
 | 
			
		||||
// mainOrder 主订单
 | 
			
		||||
// setting 系统设置
 | 
			
		||||
func (e *BinanceStrategyOrderService) RecalculateOrder(tradeSet models2.TradeSet, mainOrder *models.LinePreOrder, setting models.LineSystemSetting) error {
 | 
			
		||||
	exts := make([]models.LinePreOrderExt, 0)
 | 
			
		||||
	if err := e.Orm.Model(models.LinePreOrderExt{}).Where("main_order_id =?", mainOrder.Id).Find(&exts).Error; err != nil {
 | 
			
		||||
	if err := e.Orm.Model(models.LinePreOrderExt{}).Where("main_id =?", mainOrder.Id).Find(&exts).Error; err != nil {
 | 
			
		||||
		return errors.New("获取拓展信息失败")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var newPrice decimal.Decimal
 | 
			
		||||
	lastPrice := utility.StrToDecimal(tradeSet.LastPrice)
 | 
			
		||||
	rate := utility.StrToDecimal(mainOrder.Rate)
 | 
			
		||||
	newPrice = lastPrice.Mul(decimal.NewFromInt(1).Add(rate.Div(decimal.NewFromInt(100)).Truncate(4))).Truncate(int32(tradeSet.PriceDigit))
 | 
			
		||||
	newPrice := lastPrice.Mul(decimal.NewFromInt(1).Add(rate.Div(decimal.NewFromInt(100)).Truncate(4))).Truncate(int32(tradeSet.PriceDigit))
 | 
			
		||||
	buyPrice := utility.StrToDecimal(mainOrder.BuyPrice)
 | 
			
		||||
	totalNum := buyPrice.Div(newPrice).Truncate(int32(tradeSet.AmountDigit))
 | 
			
		||||
 | 
			
		||||
	mainOrder.SignPrice = lastPrice.String()
 | 
			
		||||
	mainOrder.Price = newPrice.String()
 | 
			
		||||
	mainOrder.Num = totalNum.String()
 | 
			
		||||
	remainQuantity := totalNum
 | 
			
		||||
	var totalLossAmount decimal.Decimal
 | 
			
		||||
	prePrice := lastPrice
 | 
			
		||||
 | 
			
		||||
	for index := range mainOrder.Childs {
 | 
			
		||||
		var ext models.LinePreOrderExt
 | 
			
		||||
		extOrderId := mainOrder.Childs[index].Id
 | 
			
		||||
		takeStopArray := []int{1, 2}
 | 
			
		||||
		//主单止盈
 | 
			
		||||
		if mainOrder.Child[index].OrderType == 1 {
 | 
			
		||||
			var ext models.LinePreOrderExt
 | 
			
		||||
 | 
			
		||||
		//止盈止损 ext拓展id为 pid
 | 
			
		||||
		if utility.ContainsInt(takeStopArray, mainOrder.Childs[index].OrderType) {
 | 
			
		||||
			extOrderId = mainOrder.Childs[index].Pid
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, v := range exts {
 | 
			
		||||
			if v.OrderId == extOrderId {
 | 
			
		||||
				ext = v
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if ext.Id <= 0 {
 | 
			
		||||
			logger.Errorf("子订单ext不存在 id:%d", mainOrder.Childs[index].Id)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		//主单止盈、止损
 | 
			
		||||
		if mainOrder.Childs[index].Pid == mainOrder.Childs[index].MainId && (mainOrder.Childs[index].OrderType == 1 || mainOrder.Childs[index].OrderType == 2) {
 | 
			
		||||
			var percent decimal.Decimal
 | 
			
		||||
 | 
			
		||||
			switch {
 | 
			
		||||
			// 加价
 | 
			
		||||
			case mainOrder.Childs[index].OrderType == 1 && mainOrder.Site == "BUY", mainOrder.Childs[index].OrderType == 2 && mainOrder.Site == "SELL":
 | 
			
		||||
				percent = decimal.NewFromInt(100).Add(ext.TakeProfitRatio)
 | 
			
		||||
				//减价
 | 
			
		||||
			case mainOrder.Childs[index].OrderType == 2 && mainOrder.Site == "BUY", mainOrder.Childs[index].OrderType == 1 && mainOrder.Site == "SELL":
 | 
			
		||||
				percent = decimal.NewFromInt(100).Sub(ext.StopLossRatio)
 | 
			
		||||
			for _, v := range exts {
 | 
			
		||||
				if v.OrderId == mainOrder.Child[index].Id {
 | 
			
		||||
					ext = v
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			childPrice := lastPrice.Mul(percent.Div(decimal.NewFromInt(100).Truncate(4))).Truncate(int32(tradeSet.PriceDigit))
 | 
			
		||||
			mainOrder.Childs[index].Price = childPrice.String()
 | 
			
		||||
			mainOrder.Childs[index].Num = totalNum.String()
 | 
			
		||||
 | 
			
		||||
		} else {
 | 
			
		||||
			lastNum := remainQuantity
 | 
			
		||||
 | 
			
		||||
			//过期时间
 | 
			
		||||
			if ext.ExpirateHour <= 0 {
 | 
			
		||||
				mainOrder.Childs[index].ExpireTime = time.Now().AddDate(10, 0, 0)
 | 
			
		||||
			} else {
 | 
			
		||||
				mainOrder.Childs[index].ExpireTime = time.Now().Add(time.Hour * time.Duration(ext.ExpirateHour))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			switch {
 | 
			
		||||
			//加仓单
 | 
			
		||||
			case mainOrder.Childs[index].OrderType == 1 && mainOrder.Childs[index].OrderCategory == 3:
 | 
			
		||||
				var percentage decimal.Decimal
 | 
			
		||||
 | 
			
		||||
			if ext.Id > 0 {
 | 
			
		||||
				var percent decimal.Decimal
 | 
			
		||||
				//多
 | 
			
		||||
				if mainOrder.Site == "BUY" {
 | 
			
		||||
					percentage = decimal.NewFromInt(1).Sub(ext.PriceRatio.Div(decimal.NewFromInt(100)))
 | 
			
		||||
					percent = decimal.NewFromInt(100).Add(ext.TakeProfitRatio)
 | 
			
		||||
				} else {
 | 
			
		||||
					percentage = decimal.NewFromInt(1).Add(ext.PriceRatio.Div(decimal.NewFromInt(100)))
 | 
			
		||||
					percent = decimal.NewFromInt(100).Sub(ext.StopLossRatio)
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				dataPrice := utility.StrToDecimal(mainOrder.Price).Mul(percentage).Truncate(int32(tradeSet.PriceDigit))
 | 
			
		||||
				mainOrder.Childs[index].Price = dataPrice.String()
 | 
			
		||||
 | 
			
		||||
				priceDiff := dataPrice.Sub(prePrice).Abs()
 | 
			
		||||
				totalLossAmount = totalLossAmount.Add(lastNum.Mul(priceDiff))
 | 
			
		||||
 | 
			
		||||
				if ext.AddPositionType == 1 {
 | 
			
		||||
					buyPrice := utility.StrToDecimal(mainOrder.BuyPrice).Mul(utility.SafeDiv(ext.AddPositionVal, decimal.NewFromInt(100))).Truncate(2)
 | 
			
		||||
					mainOrder.Childs[index].Num = utility.SafeDiv(buyPrice, dataPrice).Truncate(int32(tradeSet.AmountDigit)).String()
 | 
			
		||||
 | 
			
		||||
				} else {
 | 
			
		||||
					mainOrder.Childs[index].Num = utility.SafeDiv(ext.AddPositionVal.Truncate(2), dataPrice).Truncate(int32(tradeSet.AmountDigit)).String()
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				//加库存
 | 
			
		||||
				lastNum = lastNum.Add(utility.StrToDecimal(mainOrder.Childs[index].Num))
 | 
			
		||||
				// 计算子订单
 | 
			
		||||
				if len(mainOrder.Childs[index].Childs) > 0 {
 | 
			
		||||
					calculateChildOrder(&mainOrder.Childs[index].Childs, &tradeSet, ext, lastNum, dataPrice, totalLossAmount, false)
 | 
			
		||||
				}
 | 
			
		||||
				//覆盖最近的订单价
 | 
			
		||||
				prePrice = dataPrice
 | 
			
		||||
				//减仓单
 | 
			
		||||
			case mainOrder.Childs[index].OrderType == 4:
 | 
			
		||||
				percentage := decimal.NewFromInt(1)
 | 
			
		||||
 | 
			
		||||
				if mainOrder.Site == "BUY" && ext.PriceRatio.Cmp(decimal.Zero) > 0 {
 | 
			
		||||
					percentage = decimal.NewFromInt(1).Sub(ext.PriceRatio.Div(decimal.NewFromInt(100)))
 | 
			
		||||
				} else if ext.PriceRatio.Cmp(decimal.Zero) > 0 {
 | 
			
		||||
					percentage = decimal.NewFromInt(1).Add(ext.PriceRatio.Div(decimal.NewFromInt(100)))
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				dataPrice := utility.StrToDecimal(mainOrder.Price).Mul(percentage).Truncate(int32(tradeSet.PriceDigit))
 | 
			
		||||
				mainOrder.Childs[index].Price = dataPrice.String()
 | 
			
		||||
				priceDiff := dataPrice.Sub(prePrice).Abs()
 | 
			
		||||
				totalLossAmount = totalLossAmount.Add(lastNum.Mul(priceDiff))
 | 
			
		||||
 | 
			
		||||
				//百分比减仓
 | 
			
		||||
				if ext.AddPositionType == 1 {
 | 
			
		||||
					mainOrder.Childs[index].Num = lastNum.Mul(ext.AddPositionVal.Div(decimal.NewFromInt(100))).Truncate(int32(tradeSet.AmountDigit)).String()
 | 
			
		||||
 | 
			
		||||
				} else {
 | 
			
		||||
					logger.Error("减仓不能是固定数值")
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				//减库存
 | 
			
		||||
				lastNum = lastNum.Sub(utility.StrToDecimal(mainOrder.Childs[index].Num))
 | 
			
		||||
				// 计算子订单
 | 
			
		||||
				if len(mainOrder.Childs[index].Childs) > 0 {
 | 
			
		||||
					calculateChildOrder(&mainOrder.Childs[index].Childs, &tradeSet, ext, lastNum, dataPrice, totalLossAmount, false)
 | 
			
		||||
				}
 | 
			
		||||
				//覆盖最近的订单价
 | 
			
		||||
				prePrice = dataPrice
 | 
			
		||||
				childPrice := lastPrice.Mul(percent.Div(decimal.NewFromInt(100).Truncate(4))).Truncate(int32(tradeSet.PriceDigit))
 | 
			
		||||
				mainOrder.Child[index].Price = childPrice.String()
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			//todo 重新计算
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 计算子订单信息
 | 
			
		||||
// isTpTp 是否是止盈后止损止盈
 | 
			
		||||
// totalLossAmount 累计亏损金额
 | 
			
		||||
func calculateChildOrder(orders *[]models.LinePreOrder, tradeSet *models2.TradeSet, ext models.LinePreOrderExt, lastNum decimal.Decimal, price decimal.Decimal, totalLossAmount decimal.Decimal, isTpTp bool) error {
 | 
			
		||||
	for index := range *orders {
 | 
			
		||||
		orderQuantity := lastNum.Truncate(int32(tradeSet.AmountDigit))
 | 
			
		||||
		percentage := decimal.NewFromInt(1)
 | 
			
		||||
		var addPercentage decimal.Decimal
 | 
			
		||||
 | 
			
		||||
		switch {
 | 
			
		||||
		//止盈
 | 
			
		||||
		case !isTpTp && (*orders)[index].OrderType == 1:
 | 
			
		||||
			addPercentage = ext.TakeProfitRatio
 | 
			
		||||
			if ext.TakeProfitNumRatio.Cmp(decimal.Zero) > 0 {
 | 
			
		||||
				orderQuantity = lastNum.Mul(ext.TakeProfitNumRatio.Div(decimal.NewFromInt(100))).Truncate(int32(tradeSet.AmountDigit))
 | 
			
		||||
			}
 | 
			
		||||
		//止损
 | 
			
		||||
		case !isTpTp && (*orders)[index].OrderType == 2:
 | 
			
		||||
			addPercentage = ext.StopLossRatio
 | 
			
		||||
		//止盈后止盈
 | 
			
		||||
		case isTpTp && (*orders)[index].OrderType == 1:
 | 
			
		||||
			addPercentage = ext.TpTpPriceRatio
 | 
			
		||||
		//止盈后止损
 | 
			
		||||
		case isTpTp && (*orders)[index].OrderType == 2:
 | 
			
		||||
			addPercentage = ext.TpSlPriceRatio
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		switch {
 | 
			
		||||
		//做多止盈、做空止损
 | 
			
		||||
		case (*orders)[index].OrderType == 1 && (*orders)[index].Site == "SELL", (*orders)[index].OrderType == 2 && (*orders)[index].Site == "BUY":
 | 
			
		||||
			percentage = decimal.NewFromInt(100).Add(addPercentage)
 | 
			
		||||
 | 
			
		||||
			//做多止损、做空止盈
 | 
			
		||||
		case (*orders)[index].OrderType == 2 && (*orders)[index].Site == "SELL", (*orders)[index].OrderType == 1 && (*orders)[index].Site == "BUY":
 | 
			
		||||
			percentage = decimal.NewFromInt(100).Sub(addPercentage)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		//止盈亏损回本百分比
 | 
			
		||||
		if (*orders)[index].OrderType == 1 && totalLossAmount.Cmp(decimal.Zero) > 0 {
 | 
			
		||||
			lossPercent := totalLossAmount.Div(lastNum).Mul(decimal.NewFromInt(100)).Truncate(2)
 | 
			
		||||
			percentage = percentage.Add(lossPercent)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if percentage.Cmp(decimal.Zero) > 0 {
 | 
			
		||||
			percentage = percentage.Div(decimal.NewFromInt(100))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		orderPrice := price.Mul(percentage).Truncate(int32(tradeSet.PriceDigit))
 | 
			
		||||
		(*orders)[index].Price = orderPrice.String()
 | 
			
		||||
		(*orders)[index].Num = orderQuantity.String()
 | 
			
		||||
		lastOrderQuantity := lastNum.Sub(orderQuantity).Truncate(int32(tradeSet.AmountDigit))
 | 
			
		||||
 | 
			
		||||
		//止盈后止盈、止盈后止损
 | 
			
		||||
		if len((*orders)[index].Childs) > 0 && lastOrderQuantity.Cmp(decimal.Zero) > 0 {
 | 
			
		||||
			calculateChildOrder(&(*orders)[index].Childs, tradeSet, ext, lastOrderQuantity, orderPrice, decimal.Zero, true)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 递归订单树
 | 
			
		||||
func GetOrderByPid(order *models.LinePreOrder, orders []models.LinePreOrder, pid int) {
 | 
			
		||||
	for _, v := range orders {
 | 
			
		||||
 | 
			
		||||
@ -1,26 +0,0 @@
 | 
			
		||||
package binanceservice
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"go-admin/common/global"
 | 
			
		||||
	"go-admin/common/helper"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-admin-team/go-admin-core/sdk"
 | 
			
		||||
	"gorm.io/driver/mysql"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// 测试策略 触发单
 | 
			
		||||
func TestTriggerOrder(t *testing.T) {
 | 
			
		||||
	service := BinanceStrategyOrderService{}
 | 
			
		||||
	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{})
 | 
			
		||||
	helper.InitDefaultRedis("127.0.0.1:6379", "", 2)
 | 
			
		||||
	helper.InitLockRedisConn("127.0.0.1:6379", "", "2")
 | 
			
		||||
	sdk.Runtime.SetDb("default", db)
 | 
			
		||||
 | 
			
		||||
	service.Orm = db
 | 
			
		||||
	service.Debug = true
 | 
			
		||||
 | 
			
		||||
	service.TriggerStrategyOrder(global.EXCHANGE_BINANCE)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										447
									
								
								services/bitgetservice/bitget_client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										447
									
								
								services/bitgetservice/bitget_client.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,447 @@
 | 
			
		||||
package bitgetservice
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"go-admin/common/global"
 | 
			
		||||
	"go-admin/common/helper"
 | 
			
		||||
	"go-admin/common/interfaces"
 | 
			
		||||
	"go-admin/models"
 | 
			
		||||
	"go-admin/pkg/utility"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/bytedance/sonic"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// BitgetClient Bitget交易所客户端实现
 | 
			
		||||
type BitgetClient struct {
 | 
			
		||||
	httpClient   *helper.BitgetClient
 | 
			
		||||
	marketClient *BitgetMarketClient // 公开行情客户端
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewBitgetClient 创建新的Bitget客户端实例
 | 
			
		||||
func NewBitgetClient(apiKey, apiSecret, passphrase, proxyUrl string) (*BitgetClient, error) {
 | 
			
		||||
	httpClient, err := helper.NewBitgetClient(apiKey, apiSecret, passphrase, proxyUrl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client := &BitgetClient{
 | 
			
		||||
		httpClient: httpClient,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 初始化公开行情客户端
 | 
			
		||||
	marketConfig := DefaultMarketConfig()
 | 
			
		||||
	client.marketClient = NewBitgetMarketClient(marketConfig)
 | 
			
		||||
 | 
			
		||||
	return client, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetExchangeName 获取交易所名称
 | 
			
		||||
func (c *BitgetClient) GetExchangeName() string {
 | 
			
		||||
	return global.EXCHANGE_BITGET
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetExchangeInfo 获取交易所信息
 | 
			
		||||
func (c *BitgetClient) GetExchangeInfo() (interfaces.ExchangeInfoResponse, error) {
 | 
			
		||||
	restApi := NewBitgetRestApi()
 | 
			
		||||
 | 
			
		||||
	datas, err := restApi.GetSpotSymbols()
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return interfaces.ExchangeInfoResponse{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	symbols := make([]interfaces.SymbolInfo, 0, len(datas))
 | 
			
		||||
	for _, item := range datas {
 | 
			
		||||
		symbols = append(symbols, interfaces.SymbolInfo{
 | 
			
		||||
			Symbol:     item.Symbol,
 | 
			
		||||
			BaseAsset:  item.BaseCoin,
 | 
			
		||||
			QuoteAsset: item.QuoteCoin,
 | 
			
		||||
			Status:     item.Status,
 | 
			
		||||
			MinQty:     item.MinQty,
 | 
			
		||||
			MaxQty:     item.MaxQty,
 | 
			
		||||
			StepSize:   strconv.Itoa(item.QuantityDigit),
 | 
			
		||||
			TickSize:   strconv.Itoa(item.PriceDigit),
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return interfaces.ExchangeInfoResponse{Symbols: symbols}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSpotTickers 获取所有现货行情
 | 
			
		||||
func (c *BitgetClient) GetSpotTickers() ([]models.TradeSet, error) {
 | 
			
		||||
	params := map[string]interface{}{}
 | 
			
		||||
	resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/market/tickers", "GET", params)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("获取Bitget现货行情列表失败: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var response struct {
 | 
			
		||||
		Code string `json:"code"`
 | 
			
		||||
		Msg  string `json:"msg"`
 | 
			
		||||
		Data []struct {
 | 
			
		||||
			Symbol    string `json:"symbol"`
 | 
			
		||||
			High24h   string `json:"high24h"`
 | 
			
		||||
			Low24h    string `json:"low24h"`
 | 
			
		||||
			Close     string `json:"close"`
 | 
			
		||||
			Open      string `json:"open"`
 | 
			
		||||
			Volume24h string `json:"volume24h"`
 | 
			
		||||
			QuoteVol  string `json:"quoteVol"`
 | 
			
		||||
			Change24h string `json:"change24h"`
 | 
			
		||||
			Change    string `json:"change"`
 | 
			
		||||
		} `json:"data"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := sonic.Unmarshal(resp, &response); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("解析Bitget行情列表失败: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if response.Code != "00000" {
 | 
			
		||||
		return nil, fmt.Errorf("bitget API错误: %s", response.Msg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tradeSets := make([]models.TradeSet, 0, len(response.Data))
 | 
			
		||||
	for _, item := range response.Data {
 | 
			
		||||
		// 解析交易对,获取基础币种和计价币种
 | 
			
		||||
		var coin, currency string
 | 
			
		||||
		if strings.HasSuffix(item.Symbol, "USDT") {
 | 
			
		||||
			coin = strings.TrimSuffix(item.Symbol, "USDT")
 | 
			
		||||
			currency = "USDT"
 | 
			
		||||
		} else if strings.HasSuffix(item.Symbol, "BTC") {
 | 
			
		||||
			coin = strings.TrimSuffix(item.Symbol, "BTC")
 | 
			
		||||
			currency = "BTC"
 | 
			
		||||
		} else if strings.HasSuffix(item.Symbol, "ETH") {
 | 
			
		||||
			coin = strings.TrimSuffix(item.Symbol, "ETH")
 | 
			
		||||
			currency = "ETH"
 | 
			
		||||
		} else {
 | 
			
		||||
			continue // 跳过不支持的交易对
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tradeSet := models.TradeSet{
 | 
			
		||||
			Coin:        coin,
 | 
			
		||||
			Currency:    currency,
 | 
			
		||||
			LastPrice:   item.Close,
 | 
			
		||||
			HighPrice:   item.High24h,
 | 
			
		||||
			LowPrice:    item.Low24h,
 | 
			
		||||
			Volume:      item.Volume24h,
 | 
			
		||||
			QuoteVolume: item.QuoteVol,
 | 
			
		||||
			OpenPrice:   utility.StringAsFloat(item.Open),
 | 
			
		||||
			PriceChange: utility.StringAsFloat(item.Change),
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		tradeSets = append(tradeSets, tradeSet)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return tradeSets, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSpotTicker24h 获取现货24小时行情
 | 
			
		||||
func (c *BitgetClient) GetSpotTicker24h(symbol string) (models.Ticker24, error) {
 | 
			
		||||
	params := map[string]interface{}{
 | 
			
		||||
		"symbol": symbol,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/market/ticker", "GET", params)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return models.Ticker24{}, fmt.Errorf("获取Bitget现货24h行情失败: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var response struct {
 | 
			
		||||
		Code string `json:"code"`
 | 
			
		||||
		Msg  string `json:"msg"`
 | 
			
		||||
		Data struct {
 | 
			
		||||
			Symbol    string `json:"symbol"`
 | 
			
		||||
			High24h   string `json:"high24h"`
 | 
			
		||||
			Low24h    string `json:"low24h"`
 | 
			
		||||
			Close     string `json:"close"`
 | 
			
		||||
			Open      string `json:"open"`
 | 
			
		||||
			Volume24h string `json:"volume24h"`
 | 
			
		||||
			QuoteVol  string `json:"quoteVol"`
 | 
			
		||||
			Change24h string `json:"change24h"`
 | 
			
		||||
			Change    string `json:"change"`
 | 
			
		||||
		} `json:"data"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := sonic.Unmarshal(resp, &response); err != nil {
 | 
			
		||||
		return models.Ticker24{}, fmt.Errorf("解析Bitget 24h行情失败: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if response.Code != "00000" {
 | 
			
		||||
		return models.Ticker24{}, fmt.Errorf("Bitget API错误: %s", response.Msg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ticker := models.Ticker24{
 | 
			
		||||
		HighPrice:     response.Data.High24h,
 | 
			
		||||
		LowPrice:      response.Data.Low24h,
 | 
			
		||||
		LastPrice:     response.Data.Close,
 | 
			
		||||
		OpenPrice:     response.Data.Open,
 | 
			
		||||
		Volume:        response.Data.Volume24h,
 | 
			
		||||
		QuoteVolume:   response.Data.QuoteVol,
 | 
			
		||||
		ChangePercent: response.Data.Change,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return ticker, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PlaceSpotOrder 下现货订单
 | 
			
		||||
func (c *BitgetClient) PlaceSpotOrder(params interfaces.SpotOrderParams) (interfaces.SpotOrderResponse, error) {
 | 
			
		||||
	orderParams := map[string]interface{}{
 | 
			
		||||
		"symbol":    params.Symbol,
 | 
			
		||||
		"side":      strings.ToLower(params.Side),
 | 
			
		||||
		"orderType": strings.ToLower(params.Type),
 | 
			
		||||
		"quantity":  params.Quantity.String(),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if params.Type == "LIMIT" {
 | 
			
		||||
		orderParams["price"] = params.Price.String()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if params.TimeInForce != "" {
 | 
			
		||||
		orderParams["timeInForce"] = params.TimeInForce
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if params.NewClientOrderId != "" {
 | 
			
		||||
		orderParams["clientOrderId"] = params.NewClientOrderId
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/trade/orders", "POST", orderParams)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return interfaces.SpotOrderResponse{}, fmt.Errorf("Bitget现货下单失败: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var response struct {
 | 
			
		||||
		Code string `json:"code"`
 | 
			
		||||
		Msg  string `json:"msg"`
 | 
			
		||||
		Data struct {
 | 
			
		||||
			OrderId       string `json:"orderId"`
 | 
			
		||||
			ClientOrderId string `json:"clientOrderId"`
 | 
			
		||||
		} `json:"data"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := sonic.Unmarshal(resp, &response); err != nil {
 | 
			
		||||
		return interfaces.SpotOrderResponse{}, fmt.Errorf("解析Bitget下单响应失败: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if response.Code != "00000" {
 | 
			
		||||
		return interfaces.SpotOrderResponse{}, fmt.Errorf("Bitget下单API错误: %s", response.Msg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	orderId, _ := strconv.ParseInt(response.Data.OrderId, 10, 64)
 | 
			
		||||
 | 
			
		||||
	return interfaces.SpotOrderResponse{
 | 
			
		||||
		Symbol:        params.Symbol,
 | 
			
		||||
		OrderId:       orderId,
 | 
			
		||||
		ClientOrderId: response.Data.ClientOrderId,
 | 
			
		||||
		Price:         params.Price.String(),
 | 
			
		||||
		OrigQty:       params.Quantity.String(),
 | 
			
		||||
		Side:          params.Side,
 | 
			
		||||
		Type:          params.Type,
 | 
			
		||||
		Status:        "NEW",
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CancelSpotOrder 取消现货订单
 | 
			
		||||
func (c *BitgetClient) CancelSpotOrder(symbol, orderID string) error {
 | 
			
		||||
	params := map[string]interface{}{
 | 
			
		||||
		"symbol":  symbol,
 | 
			
		||||
		"orderId": orderID,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/trade/cancel-order", "POST", params)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("Bitget取消现货订单失败: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var response struct {
 | 
			
		||||
		Code string `json:"code"`
 | 
			
		||||
		Msg  string `json:"msg"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := sonic.Unmarshal(resp, &response); err != nil {
 | 
			
		||||
		return fmt.Errorf("解析Bitget取消订单响应失败: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if response.Code != "00000" {
 | 
			
		||||
		return fmt.Errorf("Bitget取消订单API错误: %s", response.Msg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSpotOrder 获取现货订单详情
 | 
			
		||||
func (c *BitgetClient) GetSpotOrder(symbol, orderID string) (interfaces.SpotOrderResponse, error) {
 | 
			
		||||
	// 实现订单查询逻辑
 | 
			
		||||
	return interfaces.SpotOrderResponse{}, fmt.Errorf("GetSpotOrder not implemented yet")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSpotOrders 获取现货订单列表
 | 
			
		||||
func (c *BitgetClient) GetSpotOrders(symbol string) ([]interfaces.SpotOrderResponse, error) {
 | 
			
		||||
	params := map[string]interface{}{
 | 
			
		||||
		"symbol": symbol,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/trade/open-orders", "GET", params)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("获取Bitget现货订单列表失败: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 这里需要根据实际的Bitget API响应格式来解析
 | 
			
		||||
	return []interfaces.SpotOrderResponse{}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 合约相关接口实现(暂时返回错误)
 | 
			
		||||
func (c *BitgetClient) GetFuturesTicker24h(symbol string) (models.Ticker24, error) {
 | 
			
		||||
	return models.Ticker24{}, fmt.Errorf("futures not implemented yet")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *BitgetClient) GetFuturesTickers() ([]models.TradeSet, error) {
 | 
			
		||||
	return nil, fmt.Errorf("futures not implemented yet")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *BitgetClient) PlaceFuturesOrder(params interfaces.FuturesOrderParams) (interfaces.FuturesOrderResponse, error) {
 | 
			
		||||
	return interfaces.FuturesOrderResponse{}, fmt.Errorf("futures not implemented yet")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *BitgetClient) CancelFuturesOrder(symbol, orderID string) error {
 | 
			
		||||
	return fmt.Errorf("futures not implemented yet")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *BitgetClient) GetFuturesOrder(symbol, orderID string) (interfaces.FuturesOrderResponse, error) {
 | 
			
		||||
	return interfaces.FuturesOrderResponse{}, fmt.Errorf("futures not implemented yet")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WebSocket相关接口实现(已重构为使用公开行情客户端)
 | 
			
		||||
// 注意:这些方法已被重构,现在使用新的公开行情客户端
 | 
			
		||||
// 如需私有数据订阅(如订单、持仓),请使用 BitgetWebSocketManager
 | 
			
		||||
 | 
			
		||||
// 旧的订阅方法(保留兼容性,但建议使用新方法)
 | 
			
		||||
func (c *BitgetClient) SubscribeSpotTickerLegacy(symbols []string, callback func(models.Ticker24, string, string)) error {
 | 
			
		||||
	return fmt.Errorf("已废弃,请使用 SubscribeSpotTicker(symbol, listener) 方法")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *BitgetClient) SubscribeFuturesTickerLegacy(symbols []string, callback func(models.Ticker24, string, string)) error {
 | 
			
		||||
	return fmt.Errorf("已废弃,请使用 SubscribeFuturesTicker(symbol, listener) 方法")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *BitgetClient) SubscribeSpotDepthLegacy(symbols []string, callback func(models.DepthBin, string, string)) error {
 | 
			
		||||
	return fmt.Errorf("已废弃,请使用 SubscribeSpotDepth(symbol, listener) 方法")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *BitgetClient) SubscribeSpotTradesLegacy(symbols []string, callback func(models.NewDealPush, string, string)) error {
 | 
			
		||||
	return fmt.Errorf("已废弃,请使用 SubscribeSpotTrades(symbol, listener) 方法")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *BitgetClient) SubscribeKlineLegacy(symbols []string, interval string, callback func(models.Kline, int, string, string)) error {
 | 
			
		||||
	return fmt.Errorf("已废弃,请使用 SubscribeKline(symbol, interval, listener) 方法")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *BitgetClient) Close() error {
 | 
			
		||||
	// 关闭HTTP客户端等清理工作
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetMarketProxy 设置市场数据代理
 | 
			
		||||
func (c *BitgetClient) SetMarketProxy(proxyType, proxyAddress string) {
 | 
			
		||||
	if c.marketClient != nil {
 | 
			
		||||
		c.marketClient.SetProxy(proxyType, proxyAddress)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetMarketListeners 设置市场数据监听器
 | 
			
		||||
func (c *BitgetClient) SetMarketListeners(msgListener, errorListener OnReceive) {
 | 
			
		||||
	if c.marketClient != nil {
 | 
			
		||||
		c.marketClient.SetListeners(msgListener, errorListener)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ConnectMarketData 连接市场数据WebSocket
 | 
			
		||||
func (c *BitgetClient) ConnectMarketData() error {
 | 
			
		||||
	if c.marketClient == nil {
 | 
			
		||||
		return errors.New("市场数据客户端未初始化")
 | 
			
		||||
	}
 | 
			
		||||
	return c.marketClient.Connect()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SubscribeMarketData 订阅市场数据(公开行情)
 | 
			
		||||
func (c *BitgetClient) SubscribeMarketData(subscribeReqs []SubscribeReq, listener OnReceive) error {
 | 
			
		||||
	if c.marketClient == nil {
 | 
			
		||||
		return errors.New("市场数据客户端未初始化")
 | 
			
		||||
	}
 | 
			
		||||
	return c.marketClient.Subscribe(subscribeReqs, listener)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UnsubscribeMarketData 取消订阅市场数据
 | 
			
		||||
func (c *BitgetClient) UnsubscribeMarketData(subscribeReqs []SubscribeReq) error {
 | 
			
		||||
	if c.marketClient == nil {
 | 
			
		||||
		return errors.New("市场数据客户端未初始化")
 | 
			
		||||
	}
 | 
			
		||||
	return c.marketClient.Unsubscribe(subscribeReqs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsMarketDataConnected 检查市场数据连接状态
 | 
			
		||||
func (c *BitgetClient) IsMarketDataConnected() bool {
 | 
			
		||||
	if c.marketClient == nil {
 | 
			
		||||
		return false
 | 
			
		||||
	}
 | 
			
		||||
	return c.marketClient.IsConnected()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CloseMarketData 关闭市场数据连接
 | 
			
		||||
func (c *BitgetClient) CloseMarketData() error {
 | 
			
		||||
	if c.marketClient == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	return c.marketClient.Close()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SubscribeSpotTicker 订阅现货行情(使用公开行情客户端)
 | 
			
		||||
func (c *BitgetClient) SubscribeSpotTicker(symbol string, listener OnReceive) error {
 | 
			
		||||
	subscribeReq := SubscribeReq{
 | 
			
		||||
		InstType: "SPOT",
 | 
			
		||||
		Channel:  "ticker",
 | 
			
		||||
		InstId:   symbol,
 | 
			
		||||
	}
 | 
			
		||||
	return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SubscribeFuturesTicker 订阅合约行情(使用公开行情客户端)
 | 
			
		||||
func (c *BitgetClient) SubscribeFuturesTicker(symbol string, listener OnReceive) error {
 | 
			
		||||
	subscribeReq := SubscribeReq{
 | 
			
		||||
		InstType: "UMCBL",
 | 
			
		||||
		Channel:  "ticker",
 | 
			
		||||
		InstId:   symbol,
 | 
			
		||||
	}
 | 
			
		||||
	return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SubscribeSpotDepth 订阅现货深度(使用公开行情客户端)
 | 
			
		||||
func (c *BitgetClient) SubscribeSpotDepth(symbol string, listener OnReceive) error {
 | 
			
		||||
	subscribeReq := SubscribeReq{
 | 
			
		||||
		InstType: "SPOT",
 | 
			
		||||
		Channel:  "books",
 | 
			
		||||
		InstId:   symbol,
 | 
			
		||||
	}
 | 
			
		||||
	return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SubscribeSpotTrades 订阅现货成交(使用公开行情客户端)
 | 
			
		||||
func (c *BitgetClient) SubscribeSpotTrades(symbol string, listener OnReceive) error {
 | 
			
		||||
	subscribeReq := SubscribeReq{
 | 
			
		||||
		InstType: "SPOT",
 | 
			
		||||
		Channel:  "trade",
 | 
			
		||||
		InstId:   symbol,
 | 
			
		||||
	}
 | 
			
		||||
	return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SubscribeKline 订阅K线数据(使用公开行情客户端)
 | 
			
		||||
func (c *BitgetClient) SubscribeKline(symbol, interval string, listener OnReceive) error {
 | 
			
		||||
	subscribeReq := SubscribeReq{
 | 
			
		||||
		InstType: "SPOT",
 | 
			
		||||
		Channel:  "candle" + interval,
 | 
			
		||||
		InstId:   symbol,
 | 
			
		||||
	}
 | 
			
		||||
	return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										229
									
								
								services/bitgetservice/bitget_example.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								services/bitgetservice/bitget_example.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,229 @@
 | 
			
		||||
package bitgetservice
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	log "github.com/go-admin-team/go-admin-core/logger"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ExampleBitgetMarketClient 展示如何使用Bitget公开行情客户端
 | 
			
		||||
func ExampleBitgetMarketClient() {
 | 
			
		||||
	// 1. 创建Bitget客户端(不需要API密钥用于公开数据)
 | 
			
		||||
	client := &BitgetClient{}
 | 
			
		||||
 | 
			
		||||
	// 初始化公开行情客户端
 | 
			
		||||
	marketConfig := DefaultMarketConfig()
 | 
			
		||||
	client.marketClient = NewBitgetMarketClient(marketConfig)
 | 
			
		||||
 | 
			
		||||
	// 2. 设置代理(可选)
 | 
			
		||||
	// client.SetMarketProxy("socks5", "127.0.0.1:1080")
 | 
			
		||||
 | 
			
		||||
	// 3. 设置消息监听器
 | 
			
		||||
	msgListener := func(message string) {
 | 
			
		||||
		log.Info("收到市场数据: %s", message)
 | 
			
		||||
		// 在这里处理市场数据
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	errorListener := func(message string) {
 | 
			
		||||
		log.Error("收到错误消息: %s", message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client.SetMarketListeners(msgListener, errorListener)
 | 
			
		||||
 | 
			
		||||
	// 4. 连接WebSocket
 | 
			
		||||
	if err := client.ConnectMarketData(); err != nil {
 | 
			
		||||
		log.Error("连接市场数据失败: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 5. 订阅现货行情
 | 
			
		||||
	tickerListener := func(message string) {
 | 
			
		||||
		log.Info("收到BTC-USDT行情: %s", message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := client.SubscribeSpotTicker("BTCUSDT", tickerListener); err != nil {
 | 
			
		||||
		log.Error("订阅现货行情失败: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 6. 订阅深度数据
 | 
			
		||||
	depthListener := func(message string) {
 | 
			
		||||
		log.Info("收到BTC-USDT深度: %s", message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := client.SubscribeSpotDepth("BTCUSDT", depthListener); err != nil {
 | 
			
		||||
		log.Error("订阅深度数据失败: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 7. 订阅成交数据
 | 
			
		||||
	tradeListener := func(message string) {
 | 
			
		||||
		log.Info("收到BTC-USDT成交: %s", message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := client.SubscribeSpotTrades("BTCUSDT", tradeListener); err != nil {
 | 
			
		||||
		log.Error("订阅成交数据失败: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 8. 订阅K线数据
 | 
			
		||||
	klineListener := func(message string) {
 | 
			
		||||
		log.Info("收到BTC-USDT K线: %s", message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := client.SubscribeKline("BTCUSDT", "1m", klineListener); err != nil {
 | 
			
		||||
		log.Error("订阅K线数据失败: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Info("所有订阅完成,开始接收数据...")
 | 
			
		||||
 | 
			
		||||
	// 保持连接(在实际应用中,你可能需要其他方式来保持程序运行)
 | 
			
		||||
	// select {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ExampleBitgetPrivateWebSocket 展示如何使用私有WebSocket(订单、持仓等)
 | 
			
		||||
func ExampleBitgetPrivateWebSocket() {
 | 
			
		||||
	// 私有数据订阅仍然使用 BitgetWebSocketManager
 | 
			
		||||
	// apiKey := "your_api_key"
 | 
			
		||||
	// secretKey := "your_secret_key"
 | 
			
		||||
	// passphrase := "your_passphrase"
 | 
			
		||||
 | 
			
		||||
	// // 创建私有WebSocket管理器
 | 
			
		||||
	// manager := GetOrCreateBitgetWebSocketManager(apiKey, secretKey, passphrase, "spot")
 | 
			
		||||
 | 
			
		||||
	// // 启动连接
 | 
			
		||||
	// if err := manager.Start(); err != nil {
 | 
			
		||||
	// 	log.Error("启动私有WebSocket连接失败: %v", err)
 | 
			
		||||
	// 	return
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	// log.Info("私有WebSocket连接成功,可以接收订单、持仓等私有数据")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ExampleMixedUsage 展示混合使用公开和私有订阅
 | 
			
		||||
func ExampleMixedUsage() {
 | 
			
		||||
	// apiKey := "your_api_key"
 | 
			
		||||
	// secretKey := "your_secret_key"
 | 
			
		||||
	// passphrase := "your_passphrase"
 | 
			
		||||
 | 
			
		||||
	// // 1. 创建完整的Bitget客户端
 | 
			
		||||
	// client, err := NewBitgetClient(apiKey, secretKey, passphrase)
 | 
			
		||||
	// if err != nil {
 | 
			
		||||
	// 	log.Error("创建Bitget客户端失败: %v", err)
 | 
			
		||||
	// 	return
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	// // 2. 设置公开行情监听器
 | 
			
		||||
	// client.SetMarketListeners(
 | 
			
		||||
	// 	func(message string) {
 | 
			
		||||
	// 		log.Info("公开行情数据: %s", message)
 | 
			
		||||
	// 	},
 | 
			
		||||
	// 	func(message string) {
 | 
			
		||||
	// 		log.Error("公开行情错误: %s", message)
 | 
			
		||||
	// 	},
 | 
			
		||||
	// )
 | 
			
		||||
 | 
			
		||||
	// // 3. 连接公开行情
 | 
			
		||||
	// if err := client.ConnectMarketData(); err != nil {
 | 
			
		||||
	// 	log.Error("连接公开行情失败: %v", err)
 | 
			
		||||
	// 	return
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	// // 4. 订阅公开行情
 | 
			
		||||
	// if err := client.SubscribeSpotTicker("BTCUSDT", func(message string) {
 | 
			
		||||
	// 	log.Info("BTC行情: %s", message)
 | 
			
		||||
	// }); err != nil {
 | 
			
		||||
	// 	log.Error("订阅行情失败: %v", err)
 | 
			
		||||
	// 	return
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	// // 5. 同时使用私有WebSocket(如果需要)
 | 
			
		||||
	// privateManager, err := GetOrCreateBitgetWebSocketManager(apiKey, secretKey, passphrase, "spot")
 | 
			
		||||
	// if err != nil {
 | 
			
		||||
	// 	log.Error("创建私有WebSocket失败: %v", err)
 | 
			
		||||
	// 	return
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	// if err := privateManager.Start(); err != nil {
 | 
			
		||||
	// 	log.Error("启动私有WebSocket失败: %v", err)
 | 
			
		||||
	// 	return
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	// log.Info("混合使用设置完成:公开行情 + 私有数据")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ExampleSubscribeMultipleSymbols 展示如何订阅多个交易对
 | 
			
		||||
func ExampleSubscribeMultipleSymbols() {
 | 
			
		||||
	client := &BitgetClient{}
 | 
			
		||||
	marketConfig := DefaultMarketConfig()
 | 
			
		||||
	client.marketClient = NewBitgetMarketClient(marketConfig)
 | 
			
		||||
 | 
			
		||||
	// 连接
 | 
			
		||||
	if err := client.ConnectMarketData(); err != nil {
 | 
			
		||||
		log.Error("连接失败: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 批量订阅多个交易对的行情
 | 
			
		||||
	symbols := []string{"BTCUSDT", "ETHUSDT", "ADAUSDT", "DOTUSDT"}
 | 
			
		||||
 | 
			
		||||
	for _, symbol := range symbols {
 | 
			
		||||
		listener := func(sym string) OnReceive {
 | 
			
		||||
			return func(message string) {
 | 
			
		||||
				log.Info("%s 行情更新: %s", sym, message)
 | 
			
		||||
			}
 | 
			
		||||
		}(symbol)
 | 
			
		||||
 | 
			
		||||
		if err := client.SubscribeSpotTicker(symbol, listener); err != nil {
 | 
			
		||||
			log.Error("订阅 %s 行情失败: %v", symbol, err)
 | 
			
		||||
		} else {
 | 
			
		||||
			log.Info("成功订阅 %s 行情", symbol)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Info("批量订阅完成")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ExampleCustomSubscription 展示如何使用自定义订阅
 | 
			
		||||
func ExampleCustomSubscription() {
 | 
			
		||||
	client := &BitgetClient{}
 | 
			
		||||
	marketConfig := DefaultMarketConfig()
 | 
			
		||||
	client.marketClient = NewBitgetMarketClient(marketConfig)
 | 
			
		||||
 | 
			
		||||
	if err := client.ConnectMarketData(); err != nil {
 | 
			
		||||
		log.Error("连接失败: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 自定义订阅请求
 | 
			
		||||
	customSubscriptions := []SubscribeReq{
 | 
			
		||||
		{
 | 
			
		||||
			InstType: "SPOT",
 | 
			
		||||
			Channel:  "ticker",
 | 
			
		||||
			InstId:   "BTCUSDT",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			InstType: "SPOT",
 | 
			
		||||
			Channel:  "books",
 | 
			
		||||
			InstId:   "ETHUSDT",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			InstType: "UMCBL", // 合约
 | 
			
		||||
			Channel:  "ticker",
 | 
			
		||||
			InstId:   "BTCUSDT",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 统一的消息处理器
 | 
			
		||||
	messageHandler := func(message string) {
 | 
			
		||||
		log.Info("自定义订阅数据: %s", message)
 | 
			
		||||
		// 在这里根据消息内容进行不同的处理
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 批量订阅
 | 
			
		||||
	if err := client.SubscribeMarketData(customSubscriptions, messageHandler); err != nil {
 | 
			
		||||
		log.Error("自定义订阅失败: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Info("自定义订阅完成")
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										326
									
								
								services/bitgetservice/bitget_judge.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										326
									
								
								services/bitgetservice/bitget_judge.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,326 @@
 | 
			
		||||
package bitgetservice
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	DbModels "go-admin/app/admin/models"
 | 
			
		||||
	"go-admin/models"
 | 
			
		||||
	"go-admin/models/positiondto"
 | 
			
		||||
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// JudgeCreateOrder 判断是否需要创建订单
 | 
			
		||||
// symbolType 1:现货 2:合约
 | 
			
		||||
func JudgeCreateOrder(trade models.TradeSet, symbolType int) error {
 | 
			
		||||
	// var key string
 | 
			
		||||
 | 
			
		||||
	// if symbolType == 1 {
 | 
			
		||||
	// 	key = fmt.Sprintf(rediskey.PreSpotOrderList, global.EXCHANGE_BITGET)
 | 
			
		||||
	// } else if symbolType == 2 {
 | 
			
		||||
	// 	key = fmt.Sprintf(rediskey.PreFutOrderList, global.EXCHANGE_BITGET)
 | 
			
		||||
	// } else {
 | 
			
		||||
	// 	return fmt.Errorf("不支持的交易类型")
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	// preOrderVal, _ := helper.DefaultRedis.GetAllList(key)
 | 
			
		||||
	// db := commonservice.GetDBConnection()
 | 
			
		||||
 | 
			
		||||
	// if len(preOrderVal) == 0 {
 | 
			
		||||
	// 	return fmt.Errorf("预订单列表为空")
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	// // spotApi := SpotRestApi{}
 | 
			
		||||
	// for _, item := range preOrderVal {
 | 
			
		||||
	// 	preOrder := dto.PreOrderRedisList{}
 | 
			
		||||
	// 	if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil {
 | 
			
		||||
	// 		logger.Error("反序列化失败")
 | 
			
		||||
	// 		continue
 | 
			
		||||
	// 	}
 | 
			
		||||
 | 
			
		||||
	// 	if preOrder.Symbol == trade.Coin+trade.Currency {
 | 
			
		||||
	// 		orderPrice, _ := decimal.NewFromString(preOrder.Price)
 | 
			
		||||
	// 		tradePrice, _ := decimal.NewFromString(trade.LastPrice)
 | 
			
		||||
	// 		//买入
 | 
			
		||||
	// 		if strings.ToUpper(preOrder.Site) == "BUY" && orderPrice.Cmp(tradePrice) >= 0 && orderPrice.Cmp(decimal.Zero) > 0 && tradePrice.Cmp(decimal.Zero) > 0 {
 | 
			
		||||
	// 			// SpotOrderLock(db, &preOrder, item, spotApi)
 | 
			
		||||
	// 		}
 | 
			
		||||
	// 	}
 | 
			
		||||
	// }
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 判断是否触发加仓
 | 
			
		||||
func JudgeAddPositionOrder(trade models.TradeSet, symbolType int) error {
 | 
			
		||||
	// var key string
 | 
			
		||||
 | 
			
		||||
	// if symbolType == 1 {
 | 
			
		||||
	// 	key = fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BITGET)
 | 
			
		||||
	// } else if symbolType == 2 {
 | 
			
		||||
	// 	key = fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BITGET)
 | 
			
		||||
	// } else {
 | 
			
		||||
	// 	return fmt.Errorf("不支持的交易类型")
 | 
			
		||||
	// }
 | 
			
		||||
	// preOrderVal, _ := helper.DefaultRedis.GetAllList(key)
 | 
			
		||||
	// db := commonservice.GetDBConnection()
 | 
			
		||||
 | 
			
		||||
	// if len(preOrderVal) == 0 {
 | 
			
		||||
	// 	return fmt.Errorf("预订单列表为空")
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	// // spotApi := SpotRestApi{}
 | 
			
		||||
	// for _, item := range preOrderVal {
 | 
			
		||||
	// 	preOrder := positiondto.AddPositionList{}
 | 
			
		||||
	// 	if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil {
 | 
			
		||||
	// 		logger.Error("反序列化失败")
 | 
			
		||||
	// 		continue
 | 
			
		||||
	// 	}
 | 
			
		||||
 | 
			
		||||
	// 	if preOrder.Symbol == trade.Coin+trade.Currency {
 | 
			
		||||
	// 		orderPrice := preOrder.Price
 | 
			
		||||
	// 		tradePrice, _ := decimal.NewFromString(trade.LastPrice)
 | 
			
		||||
	// 		//买入
 | 
			
		||||
	// 		if strings.ToUpper(preOrder.Side) == "BUY" && orderPrice.Cmp(tradePrice) >= 0 && orderPrice.Cmp(decimal.Zero) > 0 && tradePrice.Cmp(decimal.Zero) > 0 {
 | 
			
		||||
	// 			// SpotAddPositionTrigger(db, &preOrder, item, spotApi)
 | 
			
		||||
	// 		}
 | 
			
		||||
	// 	}
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// JudgeReduceOrder 判断是否需要减仓
 | 
			
		||||
func JudgeReduceOrder(trade models.TradeSet, symbolType int) error {
 | 
			
		||||
	// var key string
 | 
			
		||||
	// var reduceReduceListKey string
 | 
			
		||||
 | 
			
		||||
	// if symbolType == 1 {
 | 
			
		||||
	// 	key = fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BITGET)
 | 
			
		||||
	// 	reduceReduceListKey = fmt.Sprintf(rediskey.SpotOrderReduceStrategyList, global.EXCHANGE_BITGET)
 | 
			
		||||
	// } else if symbolType == 2 {
 | 
			
		||||
	// 	key = fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BITGET)
 | 
			
		||||
	// 	reduceReduceListKey = fmt.Sprintf(rediskey.FutOrderReduceStrategyList, global.EXCHANGE_BITGET)
 | 
			
		||||
	// } else {
 | 
			
		||||
	// 	return fmt.Errorf("不支持的交易类型")
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	// db := commonservice.GetDBConnection()
 | 
			
		||||
	// // spotApi := SpotRestApi{}
 | 
			
		||||
	// setting, err := cacheservice.GetSystemSetting(db)
 | 
			
		||||
 | 
			
		||||
	// if err != nil {
 | 
			
		||||
	// 	logger.Error("获取系统设置失败")
 | 
			
		||||
	// 	return errors.New("获取系统设置失败")
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	// reduceOrder := positiondto.ReduceListItem{}
 | 
			
		||||
	// tradePrice, _ := decimal.NewFromString(trade.LastPrice)
 | 
			
		||||
	// //减仓单减仓策略
 | 
			
		||||
	// orderReduceVal, _ := helper.DefaultRedis.HGetAllFields(reduceReduceListKey)
 | 
			
		||||
	// reduceOrderStrategy := dto.LineOrderReduceStrategyResp{}
 | 
			
		||||
	// orderService := orderservice.OrderService{}
 | 
			
		||||
	// orderService.Orm = db
 | 
			
		||||
 | 
			
		||||
	// for _, item := range orderReduceVal {
 | 
			
		||||
	// 	sonic.Unmarshal([]byte(item), &reduceOrderStrategy)
 | 
			
		||||
 | 
			
		||||
	// 	for index, item2 := range reduceOrderStrategy.Items {
 | 
			
		||||
	// 		if reduceOrderStrategy.Symbol == trade.Coin+trade.Currency {
 | 
			
		||||
	// 			//买入
 | 
			
		||||
	// 			if strings.ToUpper(reduceOrderStrategy.Side) == "SELL" &&
 | 
			
		||||
	// 				item2.TriggerPrice.Cmp(tradePrice) >= 0 &&
 | 
			
		||||
	// 				item2.TriggerPrice.Cmp(decimal.Zero) > 0 &&
 | 
			
		||||
	// 				tradePrice.Cmp(decimal.Zero) > 0 {
 | 
			
		||||
	// 				lock := helper.NewRedisLock(fmt.Sprintf(rediskey.ReduceStrategySpotTriggerLock, reduceOrder.ApiId, reduceOrder.Symbol), 50, 15, 100*time.Millisecond)
 | 
			
		||||
 | 
			
		||||
	// 				if ok, err := lock.AcquireWait(context.Background()); err != nil {
 | 
			
		||||
	// 					logger.Error("获取锁失败", err)
 | 
			
		||||
	// 					return errors.New("获取锁失败")
 | 
			
		||||
	// 				} else if ok {
 | 
			
		||||
	// 					defer lock.Release()
 | 
			
		||||
	// 					hasrecord, _ := helper.DefaultRedis.HExists(reduceReduceListKey, utility.IntTostring(reduceOrderStrategy.OrderId), item)
 | 
			
		||||
 | 
			
		||||
	// 					if !hasrecord {
 | 
			
		||||
	// 						logger.Debug("减仓缓存中不存在", item)
 | 
			
		||||
	// 						return fmt.Errorf("减仓缓存中不存在")
 | 
			
		||||
	// 					}
 | 
			
		||||
 | 
			
		||||
	// 					order, err := orderService.CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit)
 | 
			
		||||
 | 
			
		||||
	// 					if err != nil {
 | 
			
		||||
	// 						logger.Errorf("%d 生成订单失败", reduceOrderStrategy.OrderId)
 | 
			
		||||
	// 					}
 | 
			
		||||
 | 
			
		||||
	// 					reduceOrder.ApiId = order.ApiId
 | 
			
		||||
	// 					reduceOrder.Id = order.Id
 | 
			
		||||
	// 					reduceOrder.Pid = order.Pid
 | 
			
		||||
	// 					reduceOrder.MainId = order.MainId
 | 
			
		||||
	// 					reduceOrder.Symbol = order.Symbol
 | 
			
		||||
	// 					reduceOrder.Side = reduceOrderStrategy.Side
 | 
			
		||||
	// 					reduceOrder.OrderSn = order.OrderSn
 | 
			
		||||
	// 					reduceOrder.Price = item2.Price
 | 
			
		||||
	// 					reduceOrder.Num = item2.Num
 | 
			
		||||
	// 					if SpotReduceTrigger(db, reduceOrder, spotApi, setting, reduceReduceListKey, item, true, reduceOrderStrategy.OrderId) {
 | 
			
		||||
	// 						reduceOrderStrategy.Items[index].Actived = true
 | 
			
		||||
	// 						allActive := true
 | 
			
		||||
	// 						orderId := utility.IntToString(reduceOrderStrategy.OrderId)
 | 
			
		||||
 | 
			
		||||
	// 						for _, item3 := range reduceOrderStrategy.Items {
 | 
			
		||||
	// 							if !item3.Actived {
 | 
			
		||||
	// 								allActive = false
 | 
			
		||||
	// 								break
 | 
			
		||||
	// 							}
 | 
			
		||||
	// 						}
 | 
			
		||||
 | 
			
		||||
	// 						if allActive {
 | 
			
		||||
	// 							if err := helper.DefaultRedis.HDelField(reduceReduceListKey, orderId); err != nil {
 | 
			
		||||
	// 								logger.Errorf("删除redis reduceReduceListKey失败 %s", err.Error())
 | 
			
		||||
	// 							}
 | 
			
		||||
	// 						} else {
 | 
			
		||||
	// 							str, _ := sonic.MarshalString(reduceOrderStrategy)
 | 
			
		||||
	// 							if err := helper.DefaultRedis.HSetField(reduceReduceListKey, orderId, str); err != nil {
 | 
			
		||||
	// 								logger.Errorf("更新redis reduceReduceListKey失败 %s", err.Error())
 | 
			
		||||
	// 							}
 | 
			
		||||
	// 						}
 | 
			
		||||
	// 					}
 | 
			
		||||
	// 				}
 | 
			
		||||
	// 			}
 | 
			
		||||
	// 		}
 | 
			
		||||
	// 	}
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	// //减仓单
 | 
			
		||||
	// reduceVal, _ := helper.DefaultRedis.GetAllList(key)
 | 
			
		||||
 | 
			
		||||
	// for _, item := range reduceVal {
 | 
			
		||||
	// 	if err := sonic.Unmarshal([]byte(item), &reduceOrder); err != nil {
 | 
			
		||||
	// 		logger.Error("反序列化失败")
 | 
			
		||||
	// 		continue
 | 
			
		||||
	// 	}
 | 
			
		||||
 | 
			
		||||
	// 	if reduceOrder.Symbol == trade.Coin+trade.Currency {
 | 
			
		||||
	// 		orderPrice := reduceOrder.Price
 | 
			
		||||
	// 		//买入
 | 
			
		||||
	// 		if strings.ToUpper(reduceOrder.Side) == "SELL" &&
 | 
			
		||||
	// 			orderPrice.Cmp(tradePrice) >= 0 &&
 | 
			
		||||
	// 			orderPrice.Cmp(decimal.Zero) > 0 &&
 | 
			
		||||
	// 			tradePrice.Cmp(decimal.Zero) > 0 {
 | 
			
		||||
 | 
			
		||||
	// 			// SpotReduceTrigger(db, reduceOrder, spotApi, setting, key, item, false, 0)
 | 
			
		||||
	// 		}
 | 
			
		||||
	// 	}
 | 
			
		||||
	// }
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 触发现货减仓
 | 
			
		||||
// isStrategy 是否是策略减仓单
 | 
			
		||||
// reduceId 策略主减仓单id
 | 
			
		||||
func SpotReduceTrigger(db *gorm.DB, reduceOrder positiondto.ReduceListItem, setting DbModels.LineSystemSetting, key, item string, isStrategy bool, reduceId int) bool {
 | 
			
		||||
	// tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reduceOrder.Symbol, 1)
 | 
			
		||||
	result := true
 | 
			
		||||
 | 
			
		||||
	// if err != nil {
 | 
			
		||||
	// 	logger.Error("获取交易设置失败")
 | 
			
		||||
	// 	return false
 | 
			
		||||
	// }
 | 
			
		||||
	// lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, reduceOrder.ApiId, reduceOrder.Symbol), 20, 5, 100*time.Millisecond)
 | 
			
		||||
 | 
			
		||||
	// if ok, err := lock.AcquireWait(context.Background()); err != nil {
 | 
			
		||||
	// 	logger.Error("获取锁失败", err)
 | 
			
		||||
	// 	return false
 | 
			
		||||
	// } else if ok {
 | 
			
		||||
	// 	defer lock.Release()
 | 
			
		||||
	// 	takeOrders := make([]DbModels.LinePreOrder, 0)
 | 
			
		||||
	// 	if err := db.Model(&DbModels.LinePreOrder{}).Where("main_id =? AND order_type IN (1,2,4) AND status IN (1,5)", reduceOrder.MainId).Find(&takeOrders).Error; err != nil {
 | 
			
		||||
	// 		logger.Error("查询止盈单失败")
 | 
			
		||||
	// 		return false
 | 
			
		||||
	// 	}
 | 
			
		||||
	// 	var hasrecord bool
 | 
			
		||||
 | 
			
		||||
	// 	if isStrategy {
 | 
			
		||||
	// 		hasrecord, _ = helper.DefaultRedis.HExists(key, utility.IntToString(reduceId), item)
 | 
			
		||||
	// 	} else {
 | 
			
		||||
	// 		hasrecord, _ = helper.DefaultRedis.IsElementInList(key, item)
 | 
			
		||||
	// 	}
 | 
			
		||||
 | 
			
		||||
	// 	if !hasrecord {
 | 
			
		||||
	// 		logger.Debug("减仓缓存中不存在", item)
 | 
			
		||||
	// 		return false
 | 
			
		||||
	// 	}
 | 
			
		||||
 | 
			
		||||
	// 	apiInfo, _ := GetApiInfo(reduceOrder.ApiId)
 | 
			
		||||
 | 
			
		||||
	// 	if apiInfo.Id == 0 {
 | 
			
		||||
	// 		logger.Error("现货减仓 查询api用户不存在")
 | 
			
		||||
	// 		return false
 | 
			
		||||
	// 	}
 | 
			
		||||
 | 
			
		||||
	// 	for _, takeOrder := range takeOrders {
 | 
			
		||||
	// 		err := CancelOpenOrderByOrderSnLoop(apiInfo, takeOrder.Symbol, takeOrder.OrderSn)
 | 
			
		||||
 | 
			
		||||
	// 		if err != nil {
 | 
			
		||||
	// 			logger.Error("现货撤单失败", err)
 | 
			
		||||
	// 			return false
 | 
			
		||||
	// 		}
 | 
			
		||||
	// 	}
 | 
			
		||||
	// 	price := reduceOrder.Price.Mul(decimal.NewFromInt(1).Sub(setting.ReducePremium.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit))
 | 
			
		||||
 | 
			
		||||
	// 	params := OrderPlacementService{
 | 
			
		||||
	// 		ApiId:            reduceOrder.ApiId,
 | 
			
		||||
	// 		Side:             reduceOrder.Side,
 | 
			
		||||
	// 		Type:             "LIMIT",
 | 
			
		||||
	// 		TimeInForce:      "GTC",
 | 
			
		||||
	// 		Symbol:           reduceOrder.Symbol,
 | 
			
		||||
	// 		Price:            price,
 | 
			
		||||
	// 		Quantity:         reduceOrder.Num,
 | 
			
		||||
	// 		NewClientOrderId: reduceOrder.OrderSn,
 | 
			
		||||
	// 	}
 | 
			
		||||
 | 
			
		||||
	// 	if err := spotApi.OrderPlaceLoop(db, params, 3); err != nil {
 | 
			
		||||
	// 		result = false
 | 
			
		||||
	// 		logger.Errorf("现货减仓挂单失败 id:%s err:%v", reduceOrder.Id, err)
 | 
			
		||||
 | 
			
		||||
	// 		if err2 := db.Model(&DbModels.LinePreOrder{}).
 | 
			
		||||
	// 			Where("id =? AND status =0", reduceOrder.Id).
 | 
			
		||||
	// 			Updates(map[string]interface{}{"status": 2, "desc": err.Error()}).Error; err2 != nil {
 | 
			
		||||
	// 			logger.Errorf("修改现货减仓状态失败 id:%s err:%v", reduceOrder.Id, err2)
 | 
			
		||||
	// 		}
 | 
			
		||||
	// 	} else {
 | 
			
		||||
	// 		if err := db.Model(&DbModels.LinePreOrder{}).Where("id =?", reduceOrder.Id).Updates(map[string]interface{}{"trigger_time": time.Now()}).Error; err != nil {
 | 
			
		||||
	// 			logger.Error("更新预下单触发事件失败 ordersn:", reduceOrder.OrderSn)
 | 
			
		||||
	// 		}
 | 
			
		||||
 | 
			
		||||
	// 		if err := db.Model(&DbModels.LinePreOrder{}).
 | 
			
		||||
	// 			Where("id =? AND status =0", reduceOrder.Id).
 | 
			
		||||
	// 			Updates(map[string]interface{}{"status": 1}).Error; err != nil {
 | 
			
		||||
	// 			logger.Errorf("修改现货减仓状态失败 id:%s err:%v", reduceOrder.Id, err)
 | 
			
		||||
	// 		}
 | 
			
		||||
 | 
			
		||||
	// 		//处理减仓单减仓策略
 | 
			
		||||
	// 		if err := CacheOrderStrategyAndReCreate(db, reduceOrder, 1, tradeSet, setting); err != nil {
 | 
			
		||||
	// 			logger.Errorf("现货减仓 处理减仓策略失败 id:%v err:%v", reduceOrder.Id, err)
 | 
			
		||||
	// 		}
 | 
			
		||||
	// 	}
 | 
			
		||||
 | 
			
		||||
	// 	if _, err := helper.DefaultRedis.LRem(key, item); err != nil {
 | 
			
		||||
	// 		result = false
 | 
			
		||||
	// 		logger.Errorf("现货减仓 删除缓存失败 id:%v err:%v", reduceOrder.Id, err)
 | 
			
		||||
	// 	}
 | 
			
		||||
	// } else {
 | 
			
		||||
	// 	logger.Error("获取锁失败")
 | 
			
		||||
	// 	result = false
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	return result
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// JudgeTakeOrder 判断是否止盈
 | 
			
		||||
func JudgeTakeOrder(trades []models.TradeSet, symbolType int) error {
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// JudgeStopOrder 判断是否止损
 | 
			
		||||
func JudgeStopOrder(trades []models.TradeSet, symbolType int) error {
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										430
									
								
								services/bitgetservice/bitget_market_client.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										430
									
								
								services/bitgetservice/bitget_market_client.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,430 @@
 | 
			
		||||
package bitgetservice
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"go-admin/services/proxy"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/bytedance/sonic"
 | 
			
		||||
	log "github.com/go-admin-team/go-admin-core/logger"
 | 
			
		||||
	"github.com/gorilla/websocket"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// BitgetMarketClient Bitget公开行情订阅客户端
 | 
			
		||||
// 基于官方SDK设计,专门用于公开市场数据订阅
 | 
			
		||||
type BitgetMarketClient struct {
 | 
			
		||||
	config         *BitgetMarketConfig
 | 
			
		||||
	conn           *websocket.Conn
 | 
			
		||||
	subscriptions  map[SubscribeReq]OnReceive
 | 
			
		||||
	allSubscribes  *Set
 | 
			
		||||
	listener       OnReceive
 | 
			
		||||
	errorListener  OnReceive
 | 
			
		||||
	sendMutex      sync.Mutex
 | 
			
		||||
	reconnectMutex sync.Mutex
 | 
			
		||||
	ctx            context.Context
 | 
			
		||||
	cancel         context.CancelFunc
 | 
			
		||||
	connected      bool
 | 
			
		||||
	lastPongTime   time.Time
 | 
			
		||||
	proxyType      string
 | 
			
		||||
	proxyAddress   string
 | 
			
		||||
	reconnectCount int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewBitgetMarketClient 创建新的Bitget市场数据客户端
 | 
			
		||||
func NewBitgetMarketClient(config *BitgetMarketConfig) *BitgetMarketClient {
 | 
			
		||||
	if config == nil {
 | 
			
		||||
		config = DefaultMarketConfig()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ctx, cancel := context.WithCancel(context.Background())
 | 
			
		||||
 | 
			
		||||
	return &BitgetMarketClient{
 | 
			
		||||
		config:        config,
 | 
			
		||||
		subscriptions: make(map[SubscribeReq]OnReceive),
 | 
			
		||||
		allSubscribes: NewSet(),
 | 
			
		||||
		ctx:           ctx,
 | 
			
		||||
		cancel:        cancel,
 | 
			
		||||
		connected:     false,
 | 
			
		||||
		lastPongTime:  time.Now(),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetProxy 设置代理
 | 
			
		||||
func (c *BitgetMarketClient) SetProxy(proxyType, proxyAddress string) {
 | 
			
		||||
	c.proxyType = proxyType
 | 
			
		||||
	c.proxyAddress = proxyAddress
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SetListeners 设置消息监听器
 | 
			
		||||
func (c *BitgetMarketClient) SetListeners(msgListener, errorListener OnReceive) {
 | 
			
		||||
	c.listener = msgListener
 | 
			
		||||
	c.errorListener = errorListener
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Connect 连接WebSocket
 | 
			
		||||
func (c *BitgetMarketClient) Connect() error {
 | 
			
		||||
	c.reconnectMutex.Lock()
 | 
			
		||||
	defer c.reconnectMutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	if c.connected {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 创建WebSocket连接
 | 
			
		||||
	dialer := websocket.DefaultDialer
 | 
			
		||||
 | 
			
		||||
	// 设置代理
 | 
			
		||||
	if c.proxyAddress != "" {
 | 
			
		||||
		var err error
 | 
			
		||||
		dialer, err = proxy.GetDialer(c.proxyType, c.proxyAddress)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("设置代理失败: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dialer.HandshakeTimeout = 10 * time.Second
 | 
			
		||||
	log.Infof("正在连接Bitget市场数据WebSocket: %s", c.config.WsUrl)
 | 
			
		||||
	conn, _, err := dialer.Dial(c.config.WsUrl, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("webSocket连接失败: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.conn = conn
 | 
			
		||||
	c.connected = true
 | 
			
		||||
	c.lastPongTime = time.Now()
 | 
			
		||||
	c.reconnectCount = 0
 | 
			
		||||
 | 
			
		||||
	log.Info("Bitget市场数据WebSocket连接成功")
 | 
			
		||||
 | 
			
		||||
	// 启动消息处理协程
 | 
			
		||||
	go c.readLoop()
 | 
			
		||||
	go c.pingLoop()
 | 
			
		||||
	go c.monitorConnection()
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Subscribe 订阅市场数据
 | 
			
		||||
func (c *BitgetMarketClient) Subscribe(subscribeReqs []SubscribeReq, listener OnReceive) error {
 | 
			
		||||
	if !c.connected {
 | 
			
		||||
		if err := c.Connect(); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var args []interface{}
 | 
			
		||||
	for _, req := range subscribeReqs {
 | 
			
		||||
		// 标准化订阅请求
 | 
			
		||||
		normalizedReq := c.normalizeSubscribeReq(req)
 | 
			
		||||
		args = append(args, normalizedReq)
 | 
			
		||||
 | 
			
		||||
		// 保存订阅信息
 | 
			
		||||
		c.subscriptions[normalizedReq] = listener
 | 
			
		||||
		c.allSubscribes.Add(normalizedReq)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 发送订阅消息
 | 
			
		||||
	wsReq := WsBaseReq{
 | 
			
		||||
		Op:   "subscribe",
 | 
			
		||||
		Args: args,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return c.sendMessage(wsReq)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Unsubscribe 取消订阅
 | 
			
		||||
func (c *BitgetMarketClient) Unsubscribe(subscribeReqs []SubscribeReq) error {
 | 
			
		||||
	if !c.connected {
 | 
			
		||||
		return errors.New("WebSocket未连接")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var args []interface{}
 | 
			
		||||
	for _, req := range subscribeReqs {
 | 
			
		||||
		normalizedReq := c.normalizeSubscribeReq(req)
 | 
			
		||||
		args = append(args, normalizedReq)
 | 
			
		||||
 | 
			
		||||
		// 移除订阅信息
 | 
			
		||||
		delete(c.subscriptions, normalizedReq)
 | 
			
		||||
		c.allSubscribes.Remove(normalizedReq)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 发送取消订阅消息
 | 
			
		||||
	wsReq := WsBaseReq{
 | 
			
		||||
		Op:   "unsubscribe",
 | 
			
		||||
		Args: args,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return c.sendMessage(wsReq)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// normalizeSubscribeReq 标准化订阅请求
 | 
			
		||||
func (c *BitgetMarketClient) normalizeSubscribeReq(req SubscribeReq) SubscribeReq {
 | 
			
		||||
	return SubscribeReq{
 | 
			
		||||
		InstType: strings.ToUpper(req.InstType),
 | 
			
		||||
		Channel:  strings.ToLower(req.Channel),
 | 
			
		||||
		InstId:   strings.ToUpper(req.InstId),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// sendMessage 发送消息
 | 
			
		||||
func (c *BitgetMarketClient) sendMessage(msg interface{}) error {
 | 
			
		||||
	if c.conn == nil {
 | 
			
		||||
		return errors.New("WebSocket连接不可用")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.sendMutex.Lock()
 | 
			
		||||
	defer c.sendMutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	data, err := sonic.Marshal(msg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("序列化消息失败: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Info("发送消息: %s", string(data))
 | 
			
		||||
 | 
			
		||||
	c.conn.SetWriteDeadline(time.Now().Add(c.config.WriteTimeout))
 | 
			
		||||
	return c.conn.WriteMessage(websocket.TextMessage, data)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// readLoop 消息读取循环
 | 
			
		||||
func (c *BitgetMarketClient) readLoop() {
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if r := recover(); r != nil {
 | 
			
		||||
			log.Error("readLoop panic: %v", r)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-c.ctx.Done():
 | 
			
		||||
			return
 | 
			
		||||
		default:
 | 
			
		||||
			if c.conn == nil {
 | 
			
		||||
				time.Sleep(time.Second)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			c.conn.SetReadDeadline(time.Now().Add(c.config.ReadTimeout))
 | 
			
		||||
			_, message, err := c.conn.ReadMessage()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Error("读取消息失败: %v", err)
 | 
			
		||||
				c.handleDisconnect()
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			c.handleMessage(string(message))
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleMessage 处理接收到的消息
 | 
			
		||||
func (c *BitgetMarketClient) handleMessage(message string) {
 | 
			
		||||
	log.Info("收到消息: %s", message)
 | 
			
		||||
 | 
			
		||||
	// 处理pong消息
 | 
			
		||||
	if message == "pong" {
 | 
			
		||||
		c.lastPongTime = time.Now()
 | 
			
		||||
		log.Debug("收到pong消息")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 解析JSON消息
 | 
			
		||||
	var jsonMap map[string]interface{}
 | 
			
		||||
	if err := json.Unmarshal([]byte(message), &jsonMap); err != nil {
 | 
			
		||||
		log.Error("解析JSON消息失败: %v", err)
 | 
			
		||||
		if c.errorListener != nil {
 | 
			
		||||
			c.errorListener(message)
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查错误码
 | 
			
		||||
	if code, exists := jsonMap["code"]; exists {
 | 
			
		||||
		if codeNum, ok := code.(float64); ok && int(codeNum) != 0 {
 | 
			
		||||
			log.Error("收到错误消息: %s", message)
 | 
			
		||||
			if c.errorListener != nil {
 | 
			
		||||
				c.errorListener(message)
 | 
			
		||||
			}
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 处理订阅确认消息
 | 
			
		||||
	if event, exists := jsonMap["event"]; exists && event == "subscribe" {
 | 
			
		||||
		log.Info("订阅确认: %s", message)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 处理数据推送
 | 
			
		||||
	if data, exists := jsonMap["data"]; exists && data != nil {
 | 
			
		||||
		if arg, argExists := jsonMap["arg"]; argExists {
 | 
			
		||||
			listener := c.getListener(arg)
 | 
			
		||||
			if listener != nil {
 | 
			
		||||
				listener(message)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 默认处理
 | 
			
		||||
	if c.listener != nil {
 | 
			
		||||
		c.listener(message)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getListener 获取对应的监听器
 | 
			
		||||
func (c *BitgetMarketClient) getListener(argJson interface{}) OnReceive {
 | 
			
		||||
	argMap, ok := argJson.(map[string]interface{})
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return c.listener
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	subscribeReq := SubscribeReq{
 | 
			
		||||
		InstType: fmt.Sprintf("%v", argMap["instType"]),
 | 
			
		||||
		Channel:  fmt.Sprintf("%v", argMap["channel"]),
 | 
			
		||||
		InstId:   fmt.Sprintf("%v", argMap["instId"]),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if listener, exists := c.subscriptions[subscribeReq]; exists {
 | 
			
		||||
		return listener
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return c.listener
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// pingLoop Ping循环
 | 
			
		||||
func (c *BitgetMarketClient) pingLoop() {
 | 
			
		||||
	ticker := time.NewTicker(c.config.PingInterval)
 | 
			
		||||
	defer ticker.Stop()
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-c.ctx.Done():
 | 
			
		||||
			return
 | 
			
		||||
		case <-ticker.C:
 | 
			
		||||
			if c.connected && c.conn != nil {
 | 
			
		||||
				c.sendMutex.Lock()
 | 
			
		||||
				c.conn.SetWriteDeadline(time.Now().Add(c.config.WriteTimeout))
 | 
			
		||||
				err := c.conn.WriteMessage(websocket.TextMessage, []byte("ping"))
 | 
			
		||||
				c.sendMutex.Unlock()
 | 
			
		||||
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					log.Error("发送ping失败: %v", err)
 | 
			
		||||
					c.handleDisconnect()
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// monitorConnection 监控连接状态
 | 
			
		||||
func (c *BitgetMarketClient) monitorConnection() {
 | 
			
		||||
	ticker := time.NewTicker(30 * time.Second)
 | 
			
		||||
	defer ticker.Stop()
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-c.ctx.Done():
 | 
			
		||||
			return
 | 
			
		||||
		case <-ticker.C:
 | 
			
		||||
			if c.connected && time.Since(c.lastPongTime) > 60*time.Second {
 | 
			
		||||
				log.Warn("长时间未收到pong消息,触发重连")
 | 
			
		||||
				c.handleDisconnect()
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleDisconnect 处理断开连接
 | 
			
		||||
func (c *BitgetMarketClient) handleDisconnect() {
 | 
			
		||||
	c.reconnectMutex.Lock()
 | 
			
		||||
	defer c.reconnectMutex.Unlock()
 | 
			
		||||
 | 
			
		||||
	if !c.connected {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Warn("WebSocket连接断开,准备重连")
 | 
			
		||||
	c.connected = false
 | 
			
		||||
 | 
			
		||||
	if c.conn != nil {
 | 
			
		||||
		c.conn.Close()
 | 
			
		||||
		c.conn = nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 重连逻辑
 | 
			
		||||
	go c.reconnect()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// reconnect 重连
 | 
			
		||||
func (c *BitgetMarketClient) reconnect() {
 | 
			
		||||
	for c.reconnectCount < c.config.MaxReconnectTimes {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-c.ctx.Done():
 | 
			
		||||
			return
 | 
			
		||||
		default:
 | 
			
		||||
			c.reconnectCount++
 | 
			
		||||
			log.Info("尝试重连 (%d/%d)", c.reconnectCount, c.config.MaxReconnectTimes)
 | 
			
		||||
 | 
			
		||||
			time.Sleep(c.config.ReconnectInterval)
 | 
			
		||||
 | 
			
		||||
			if err := c.Connect(); err != nil {
 | 
			
		||||
				log.Error("重连失败: %v", err)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 重新订阅
 | 
			
		||||
			if err := c.resubscribe(); err != nil {
 | 
			
		||||
				log.Error("重新订阅失败: %v", err)
 | 
			
		||||
				c.handleDisconnect()
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			log.Info("重连成功")
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Error("达到最大重连次数,停止重连")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// resubscribe 重新订阅
 | 
			
		||||
func (c *BitgetMarketClient) resubscribe() error {
 | 
			
		||||
	if len(c.subscriptions) == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var args []interface{}
 | 
			
		||||
	for req := range c.subscriptions {
 | 
			
		||||
		args = append(args, req)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wsReq := WsBaseReq{
 | 
			
		||||
		Op:   "subscribe",
 | 
			
		||||
		Args: args,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return c.sendMessage(wsReq)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Close 关闭客户端
 | 
			
		||||
func (c *BitgetMarketClient) Close() error {
 | 
			
		||||
	c.cancel()
 | 
			
		||||
	c.connected = false
 | 
			
		||||
 | 
			
		||||
	if c.conn != nil {
 | 
			
		||||
		return c.conn.Close()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsConnected 检查连接状态
 | 
			
		||||
func (c *BitgetMarketClient) IsConnected() bool {
 | 
			
		||||
	return c.connected
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										61
									
								
								services/bitgetservice/bitget_market_future.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								services/bitgetservice/bitget_market_future.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,61 @@
 | 
			
		||||
package bitgetservice
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"go-admin/config"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-admin-team/go-admin-core/logger"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type BitgetMarketFutureClient struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *BitgetMarketFutureClient) Subscribe(symbols []string) error {
 | 
			
		||||
	// 1. 创建Bitget客户端(不需要API密钥用于公开数据)
 | 
			
		||||
	client := &BitgetClient{}
 | 
			
		||||
 | 
			
		||||
	// 初始化公开行情客户端
 | 
			
		||||
	marketConfig := DefaultMarketConfig()
 | 
			
		||||
	client.marketClient = NewBitgetMarketClient(marketConfig)
 | 
			
		||||
	if config.ExtConfig.ProxyUrl != "" {
 | 
			
		||||
		client.SetMarketProxy("", config.ExtConfig.ProxyUrl)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 3. 设置消息监听器
 | 
			
		||||
	msgListener := func(message string) {
 | 
			
		||||
		logger.Info("收到市场数据: %s", message)
 | 
			
		||||
		// 在这里处理市场数据
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	errorListener := func(message string) {
 | 
			
		||||
		logger.Error("收到错误消息: %s", message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client.SetMarketListeners(msgListener, errorListener)
 | 
			
		||||
 | 
			
		||||
	// 4. 连接WebSocket
 | 
			
		||||
	if err := client.ConnectMarketData(); err != nil {
 | 
			
		||||
		logger.Error("连接市场数据失败: %v", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 5. 订阅合约行情
 | 
			
		||||
	tickerListener := func(message string) {
 | 
			
		||||
		logger.Info("收到BTC-USDT合约行情: %s", message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	params := []SubscribeReq{}
 | 
			
		||||
 | 
			
		||||
	for _, v := range symbols {
 | 
			
		||||
		params = append(params, SubscribeReq{
 | 
			
		||||
			InstType: "USDT-FUTURES",
 | 
			
		||||
			Channel:  "ticker",
 | 
			
		||||
			InstId:   v,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := client.SubscribeMarketData(params, tickerListener); err != nil {
 | 
			
		||||
		logger.Error("订阅合约行情失败: %v", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										126
									
								
								services/bitgetservice/bitget_market_models.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								services/bitgetservice/bitget_market_models.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,126 @@
 | 
			
		||||
package bitgetservice
 | 
			
		||||
 | 
			
		||||
import "time"
 | 
			
		||||
 | 
			
		||||
// SubscribeReq 订阅请求结构(基于官方SDK)
 | 
			
		||||
type SubscribeReq struct {
 | 
			
		||||
	InstType string `json:"instType"` // 产品类型:SPOT-现货,UMCBL-U本位合约,DMCBL-币本位合约
 | 
			
		||||
	Channel  string `json:"channel"`  // 频道名称
 | 
			
		||||
	InstId   string `json:"instId"`   // 产品ID
 | 
			
		||||
	// Coin     string `json:"coin"`     // 币种(可选)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WsBaseReq WebSocket基础请求结构
 | 
			
		||||
type WsBaseReq struct {
 | 
			
		||||
	Op   string        `json:"op"`   // 操作类型:subscribe, unsubscribe, login
 | 
			
		||||
	Args []interface{} `json:"args"` // 参数列表
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// WsLoginReq WebSocket登录请求
 | 
			
		||||
type WsLoginReq struct {
 | 
			
		||||
	ApiKey     string `json:"apiKey"`
 | 
			
		||||
	Passphrase string `json:"passphrase"`
 | 
			
		||||
	Timestamp  string `json:"timestamp"`
 | 
			
		||||
	Sign       string `json:"sign"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// OnReceive 消息接收回调函数类型
 | 
			
		||||
type OnReceive func(message string)
 | 
			
		||||
 | 
			
		||||
// Set 集合数据结构(简化实现)
 | 
			
		||||
type Set struct {
 | 
			
		||||
	items map[SubscribeReq]bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewSet 创建新的集合
 | 
			
		||||
func NewSet() *Set {
 | 
			
		||||
	return &Set{
 | 
			
		||||
		items: make(map[SubscribeReq]bool),
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Add 添加元素到集合
 | 
			
		||||
func (s *Set) Add(item SubscribeReq) {
 | 
			
		||||
	s.items[item] = true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Remove 从集合中移除元素
 | 
			
		||||
func (s *Set) Remove(item SubscribeReq) {
 | 
			
		||||
	delete(s.items, item)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Contains 检查集合是否包含元素
 | 
			
		||||
func (s *Set) Contains(item SubscribeReq) bool {
 | 
			
		||||
	_, exists := s.items[item]
 | 
			
		||||
	return exists
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BitgetMarketData 市场数据基础结构
 | 
			
		||||
type BitgetMarketData struct {
 | 
			
		||||
	Action string       `json:"action"` // 推送类型
 | 
			
		||||
	Arg    SubscribeReq `json:"arg"`    // 订阅参数
 | 
			
		||||
	Data   interface{}  `json:"data"`   // 数据内容
 | 
			
		||||
	Ts     int64        `json:"ts"`     // 时间戳
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TickerData 行情数据
 | 
			
		||||
type TickerData struct {
 | 
			
		||||
	InstId    string `json:"instId"`    // 产品ID
 | 
			
		||||
	Last      string `json:"last"`      // 最新价
 | 
			
		||||
	Open24h   string `json:"open24h"`   // 24小时开盘价
 | 
			
		||||
	High24h   string `json:"high24h"`   // 24小时最高价
 | 
			
		||||
	Low24h    string `json:"low24h"`    // 24小时最低价
 | 
			
		||||
	Change24h string `json:"change24h"` // 24小时涨跌幅
 | 
			
		||||
	Vol24h    string `json:"vol24h"`    // 24小时成交量
 | 
			
		||||
	VolCcy24h string `json:"volCcy24h"` // 24小时成交额
 | 
			
		||||
	Ts        string `json:"ts"`        // 时间戳
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DepthData 深度数据
 | 
			
		||||
type DepthData struct {
 | 
			
		||||
	InstId string     `json:"instId"` // 产品ID
 | 
			
		||||
	Asks   [][]string `json:"asks"`   // 卖盘 [价格, 数量]
 | 
			
		||||
	Bids   [][]string `json:"bids"`   // 买盘 [价格, 数量]
 | 
			
		||||
	Ts     string     `json:"ts"`     // 时间戳
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TradeData 成交数据
 | 
			
		||||
type TradeData struct {
 | 
			
		||||
	InstId  string `json:"instId"`  // 产品ID
 | 
			
		||||
	TradeId string `json:"tradeId"` // 成交ID
 | 
			
		||||
	Px      string `json:"px"`      // 成交价格
 | 
			
		||||
	Sz      string `json:"sz"`      // 成交数量
 | 
			
		||||
	Side    string `json:"side"`    // 成交方向 buy/sell
 | 
			
		||||
	Ts      string `json:"ts"`      // 成交时间
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// KlineData K线数据
 | 
			
		||||
type KlineData struct {
 | 
			
		||||
	InstId string   `json:"instId"` // 产品ID
 | 
			
		||||
	Candle []string `json:"candle"` // K线数据 [时间戳, 开盘价, 最高价, 最低价, 收盘价, 成交量, 成交额]
 | 
			
		||||
	Ts     string   `json:"ts"`     // 时间戳
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BitgetMarketConfig 市场数据配置
 | 
			
		||||
type BitgetMarketConfig struct {
 | 
			
		||||
	WsUrl             string        // WebSocket地址
 | 
			
		||||
	ReconnectInterval time.Duration // 重连间隔
 | 
			
		||||
	PingInterval      time.Duration // Ping间隔
 | 
			
		||||
	ReadTimeout       time.Duration // 读取超时
 | 
			
		||||
	WriteTimeout      time.Duration // 写入超时
 | 
			
		||||
	MaxReconnectTimes int           // 最大重连次数
 | 
			
		||||
	EnableCompression bool          // 是否启用压缩
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DefaultMarketConfig 默认市场数据配置
 | 
			
		||||
func DefaultMarketConfig() *BitgetMarketConfig {
 | 
			
		||||
	return &BitgetMarketConfig{
 | 
			
		||||
		WsUrl:             "wss://ws.bitget.com/v2/ws/public",
 | 
			
		||||
		ReconnectInterval: 5 * time.Second,
 | 
			
		||||
		PingInterval:      15 * time.Second,
 | 
			
		||||
		ReadTimeout:       60 * time.Second,
 | 
			
		||||
		WriteTimeout:      10 * time.Second,
 | 
			
		||||
		MaxReconnectTimes: 10,
 | 
			
		||||
		EnableCompression: false,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										61
									
								
								services/bitgetservice/bitget_market_spot.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								services/bitgetservice/bitget_market_spot.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,61 @@
 | 
			
		||||
package bitgetservice
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"go-admin/config"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-admin-team/go-admin-core/logger"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type BitgetMarketSpotClient struct {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (c *BitgetMarketSpotClient) Subscribe(symbols []string) error {
 | 
			
		||||
	// 1. 创建Bitget客户端(不需要API密钥用于公开数据)
 | 
			
		||||
	client := &BitgetClient{}
 | 
			
		||||
 | 
			
		||||
	// 初始化公开行情客户端
 | 
			
		||||
	marketConfig := DefaultMarketConfig()
 | 
			
		||||
	client.marketClient = NewBitgetMarketClient(marketConfig)
 | 
			
		||||
	if config.ExtConfig.ProxyUrl != "" {
 | 
			
		||||
		client.SetMarketProxy("", config.ExtConfig.ProxyUrl)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 3. 设置消息监听器
 | 
			
		||||
	msgListener := func(message string) {
 | 
			
		||||
		logger.Infof("收到市场数据: %s", message)
 | 
			
		||||
		// 在这里处理市场数据
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	errorListener := func(message string) {
 | 
			
		||||
		logger.Errorf("收到错误消息: %s", message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	client.SetMarketListeners(msgListener, errorListener)
 | 
			
		||||
 | 
			
		||||
	// 4. 连接WebSocket
 | 
			
		||||
	if err := client.ConnectMarketData(); err != nil {
 | 
			
		||||
		logger.Errorf("连接市场数据失败: %v", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 5. 订阅现货行情
 | 
			
		||||
	tickerListener := func(message string) {
 | 
			
		||||
		logger.Infof("收到行情: %s", message)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	params := []SubscribeReq{}
 | 
			
		||||
 | 
			
		||||
	for _, v := range symbols {
 | 
			
		||||
		params = append(params, SubscribeReq{
 | 
			
		||||
			InstType: "SPOT",
 | 
			
		||||
			Channel:  "ticker",
 | 
			
		||||
			InstId:   v,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := client.SubscribeMarketData(params, tickerListener); err != nil {
 | 
			
		||||
		logger.Errorf("订阅现货行情失败: %v", err)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										54
									
								
								services/bitgetservice/bitget_market_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								services/bitgetservice/bitget_market_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,54 @@
 | 
			
		||||
package bitgetservice
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"go-admin/common/helper"
 | 
			
		||||
	"go-admin/config"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-admin-team/go-admin-core/sdk"
 | 
			
		||||
	"gorm.io/driver/mysql"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func initSetting() {
 | 
			
		||||
	dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
 | 
			
		||||
	db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
 | 
			
		||||
	sdk.Runtime.SetDb("default", db)
 | 
			
		||||
	helper.InitDefaultRedis("127.0.0.1:6379", "", 2)
 | 
			
		||||
	helper.InitLockRedisConn("127.0.0.1:6379", "", "2")
 | 
			
		||||
	config.ExtConfig.ProxyUrl = "http://127.0.0.1:7890"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestBitgetMarketFutureClient_Subscribe(t *testing.T) {
 | 
			
		||||
	initSetting()
 | 
			
		||||
	// 初始化Bitget合约行情客户端
 | 
			
		||||
	bitgetFut := BitgetMarketFutureClient{}
 | 
			
		||||
 | 
			
		||||
	// 测试用的合约符号列表
 | 
			
		||||
	testSymbols := []string{"ETHUSDT"}
 | 
			
		||||
 | 
			
		||||
	// 调用订阅方法
 | 
			
		||||
	err := bitgetFut.Subscribe(testSymbols)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("订阅Bitget合约行情失败: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
	time.Sleep(60 * time.Second)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestBitgetMarketSpotClient_Subscribe(t *testing.T) {
 | 
			
		||||
	initSetting()
 | 
			
		||||
	// 初始化Bitget现货行情客户端
 | 
			
		||||
	bitgetSpot := BitgetMarketSpotClient{}
 | 
			
		||||
 | 
			
		||||
	// 测试用的现货符号列表
 | 
			
		||||
	testSymbols := []string{"ETHUSDT"}
 | 
			
		||||
 | 
			
		||||
	// 调用订阅方法
 | 
			
		||||
	err := bitgetSpot.Subscribe(testSymbols)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("订阅Bitget现货行情失败: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	time.Sleep(60 * time.Second)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										417
									
								
								services/bitgetservice/bitget_rest.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										417
									
								
								services/bitgetservice/bitget_rest.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,417 @@
 | 
			
		||||
package bitgetservice
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"go-admin/common/global"
 | 
			
		||||
	"go-admin/common/helper"
 | 
			
		||||
	"go-admin/config"
 | 
			
		||||
	"go-admin/models"
 | 
			
		||||
	"go-admin/pkg/utility"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/bytedance/sonic"
 | 
			
		||||
	log "github.com/go-admin-team/go-admin-core/logger"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// BitgetRestApi Bitget REST API服务
 | 
			
		||||
type BitgetRestApi struct {
 | 
			
		||||
	httpClient *helper.BitgetClient
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewBitgetRestApi 创建新的Bitget REST API实例
 | 
			
		||||
func NewBitgetRestApi() *BitgetRestApi {
 | 
			
		||||
	// 从配置中获取代理设置
 | 
			
		||||
	proxyUrl := config.ExtConfig.ProxyUrl
 | 
			
		||||
 | 
			
		||||
	// 使用代理配置创建HTTP客户端
 | 
			
		||||
	httpClient, err := helper.NewBitgetClient("", "", "", proxyUrl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("创建Bitget HTTP客户端失败", err)
 | 
			
		||||
		// 如果代理创建失败,使用无代理的客户端
 | 
			
		||||
		httpClient, _ = helper.NewBitgetClient("", "", "", "")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &BitgetRestApi{
 | 
			
		||||
		httpClient: httpClient,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewBitgetSimpleClient 创建简单的Bitget客户端(兼容性函数)
 | 
			
		||||
func NewBitgetSimpleClient() *BitgetClient {
 | 
			
		||||
	// 从配置中获取代理设置
 | 
			
		||||
	proxyUrl := config.ExtConfig.ProxyUrl
 | 
			
		||||
 | 
			
		||||
	client, err := NewBitgetClient("", "", "", proxyUrl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("创建Bitget简单客户端失败", err)
 | 
			
		||||
		// 如果代理创建失败,使用无代理的客户端
 | 
			
		||||
		client, _ = NewBitgetClient("", "", "", "")
 | 
			
		||||
	}
 | 
			
		||||
	return client
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// bitgetSymbolResponse 通用的符号响应结构体
 | 
			
		||||
type bitgetSymbolResponse struct {
 | 
			
		||||
	Code string `json:"code"`
 | 
			
		||||
	Msg  string `json:"msg"`
 | 
			
		||||
	Data []struct {
 | 
			
		||||
		Symbol            string `json:"symbol"`
 | 
			
		||||
		BaseCoin          string `json:"baseCoin"`
 | 
			
		||||
		QuoteCoin         string `json:"quoteCoin"`
 | 
			
		||||
		Status            string `json:"status"`
 | 
			
		||||
		MinTradeAmount    string `json:"minTradeAmount"`
 | 
			
		||||
		MaxTradeAmount    string `json:"maxTradeAmount"`
 | 
			
		||||
		TakerFeeRate      string `json:"takerFeeRate"`
 | 
			
		||||
		MakerFeeRate      string `json:"makerFeeRate"`
 | 
			
		||||
		PricePrecision    string `json:"pricePrecision"`
 | 
			
		||||
		QuantityPrecision string `json:"quantityPrecision"`
 | 
			
		||||
	} `json:"data"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getSymbolsFromAPI 通用的符号获取方法
 | 
			
		||||
// symbolType: "spot" 现货, "futures" 合约
 | 
			
		||||
func (api *BitgetRestApi) getSymbolsFromAPI(symbolType string) ([]BitgetSymbol, error) {
 | 
			
		||||
	var url string
 | 
			
		||||
	var errorPrefix string
 | 
			
		||||
 | 
			
		||||
	switch symbolType {
 | 
			
		||||
	case "spot":
 | 
			
		||||
		url = "/api/v2/spot/public/symbols"
 | 
			
		||||
		errorPrefix = "现货"
 | 
			
		||||
	case "futures":
 | 
			
		||||
		url = "/api/v2/mix/market/contracts?productType=USDT-FUTURES"
 | 
			
		||||
		errorPrefix = "合约"
 | 
			
		||||
	default:
 | 
			
		||||
		return nil, fmt.Errorf("不支持的符号类型: %s", symbolType)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 发送API请求
 | 
			
		||||
	resp, _, err := api.httpClient.SendSpotRequestAuth(url, "GET", map[string]interface{}{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("获取bitget %s交易所信息失败: %v", errorPrefix, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 解析响应数据
 | 
			
		||||
	var response bitgetSymbolResponse
 | 
			
		||||
	if err := sonic.Unmarshal(resp, &response); err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("解析bitget %s交易所信息失败: %v", errorPrefix, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查API响应状态
 | 
			
		||||
	if response.Code != "00000" {
 | 
			
		||||
		return nil, fmt.Errorf("bitget API错误: %s", response.Msg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 转换数据格式
 | 
			
		||||
	return api.convertToSymbols(response.Data), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// convertToSymbols 将API响应数据转换为BitgetSymbol切片
 | 
			
		||||
func (api *BitgetRestApi) convertToSymbols(data []struct {
 | 
			
		||||
	Symbol            string `json:"symbol"`
 | 
			
		||||
	BaseCoin          string `json:"baseCoin"`
 | 
			
		||||
	QuoteCoin         string `json:"quoteCoin"`
 | 
			
		||||
	Status            string `json:"status"`
 | 
			
		||||
	MinTradeAmount    string `json:"minTradeAmount"`
 | 
			
		||||
	MaxTradeAmount    string `json:"maxTradeAmount"`
 | 
			
		||||
	TakerFeeRate      string `json:"takerFeeRate"`
 | 
			
		||||
	MakerFeeRate      string `json:"makerFeeRate"`
 | 
			
		||||
	PricePrecision    string `json:"pricePrecision"`
 | 
			
		||||
	QuantityPrecision string `json:"quantityPrecision"`
 | 
			
		||||
}) []BitgetSymbol {
 | 
			
		||||
	symbols := make([]BitgetSymbol, 0, len(data))
 | 
			
		||||
	
 | 
			
		||||
	for _, item := range data {
 | 
			
		||||
		if item.Status == "online" {
 | 
			
		||||
			priceDigit, _ := strconv.Atoi(item.PricePrecision)
 | 
			
		||||
			quantileDigit, _ := strconv.Atoi(item.QuantityPrecision)
 | 
			
		||||
 | 
			
		||||
			symbol := BitgetSymbol{
 | 
			
		||||
				Symbol:        item.Symbol,
 | 
			
		||||
				BaseAsset:     item.BaseCoin,
 | 
			
		||||
				QuoteAsset:    item.QuoteCoin,
 | 
			
		||||
				Status:        item.Status,
 | 
			
		||||
				MinQty:        item.MinTradeAmount,
 | 
			
		||||
				MaxQty:        item.MaxTradeAmount,
 | 
			
		||||
				PriceDigit:    priceDigit,
 | 
			
		||||
				QuantityDigit: quantileDigit,
 | 
			
		||||
			}
 | 
			
		||||
			symbols = append(symbols, symbol)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	return symbols
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSpotSymbols 获取现货交易对信息
 | 
			
		||||
func (api *BitgetRestApi) GetSpotSymbols() ([]BitgetSymbol, error) {
 | 
			
		||||
	return api.getSymbolsFromAPI("spot")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetFutSymbols 获取合约交易对信息
 | 
			
		||||
func (api *BitgetRestApi) GetFutSymbols() ([]BitgetSymbol, error) {
 | 
			
		||||
	return api.getSymbolsFromAPI("futures")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// symbolConfig 符号配置结构体
 | 
			
		||||
type symbolConfig struct {
 | 
			
		||||
	symbolType        string
 | 
			
		||||
	quoteAssetSymbols []string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// buildTradeSetFromSymbols 通用的TradeSet构建方法
 | 
			
		||||
func buildTradeSetFromSymbols(symbols []BitgetSymbol, config symbolConfig) (map[string]models.TradeSet, []string) {
 | 
			
		||||
	tradeSets := make(map[string]models.TradeSet, len(symbols))
 | 
			
		||||
	deleteSymbols := make([]string, 0)
 | 
			
		||||
 | 
			
		||||
	for _, item := range symbols {
 | 
			
		||||
		if utility.ContainsStr(config.quoteAssetSymbols, item.QuoteAsset) && item.Status == "online" {
 | 
			
		||||
			tradeSet := models.TradeSet{
 | 
			
		||||
				Coin:        item.BaseAsset,
 | 
			
		||||
				Currency:    item.QuoteAsset,
 | 
			
		||||
				PriceDigit:  item.PriceDigit,
 | 
			
		||||
				AmountDigit: item.QuantityDigit,
 | 
			
		||||
				MinQty:      utility.StringAsFloat(item.MinQty),
 | 
			
		||||
			}
 | 
			
		||||
			tradeSets[item.Symbol] = tradeSet
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return tradeSets, deleteSymbols
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSpotSymbols 获取现货交易对并构建TradeSet映射
 | 
			
		||||
func GetSpotSymbols() (map[string]models.TradeSet, []string, error) {
 | 
			
		||||
	api := NewBitgetRestApi()
 | 
			
		||||
	symbols, err := api.GetSpotSymbols()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("获取bitget规范信息失败", err)
 | 
			
		||||
		return make(map[string]models.TradeSet, 0), []string{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	config := symbolConfig{
 | 
			
		||||
		symbolType:        "spot",
 | 
			
		||||
		quoteAssetSymbols: []string{"USDT", "ETH", "BTC", "SOL", "BNB", "DOGE"},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tradeSets, deleteSymbols := buildTradeSetFromSymbols(symbols, config)
 | 
			
		||||
	return tradeSets, deleteSymbols, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetFutSymbols 获取合约交易对并构建TradeSet映射
 | 
			
		||||
func GetFutSymbols() (map[string]models.TradeSet, []string, error) {
 | 
			
		||||
	api := NewBitgetRestApi()
 | 
			
		||||
	symbols, err := api.GetFutSymbols()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("获取bitget合约规范信息失败", err)
 | 
			
		||||
		return make(map[string]models.TradeSet, 0), []string{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	config := symbolConfig{
 | 
			
		||||
		symbolType:        "futures",
 | 
			
		||||
		quoteAssetSymbols: []string{"USDT"},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	tradeSets, deleteSymbols := buildTradeSetFromSymbols(symbols, config)
 | 
			
		||||
	return tradeSets, deleteSymbols, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getTicker24hData 获取24小时行情数据的通用方法
 | 
			
		||||
func (api *BitgetRestApi) getTicker24hData(url, tickerType, description string, tradeSet *map[string]models.TradeSet) ([]string, error) {
 | 
			
		||||
	// 定义响应数据结构
 | 
			
		||||
	type tickerResponse struct {
 | 
			
		||||
		Code string `json:"code"`
 | 
			
		||||
		Msg  string `json:"msg"`
 | 
			
		||||
		Data []struct {
 | 
			
		||||
			Symbol    string `json:"symbol"`
 | 
			
		||||
			High24h   string `json:"high24h"`
 | 
			
		||||
			Low24h    string `json:"low24h"`
 | 
			
		||||
			Close     string `json:"lastPr"`
 | 
			
		||||
			Open      string `json:"open"`
 | 
			
		||||
			Volume24h string `json:"baseVolume"`   // 基础币成交量
 | 
			
		||||
			QuoteVol  string `json:"usdtVolume"`   // USDT成交量
 | 
			
		||||
			Change24h string `json:"changeUtc24h"` // UTC 24h涨跌幅
 | 
			
		||||
			Change    string `json:"change24h"`    // 24h涨跌幅
 | 
			
		||||
		} `json:"data"`
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 发送API请求
 | 
			
		||||
	resp, _, err := api.httpClient.SendSpotRequestAuth(url, "GET", map[string]interface{}{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return []string{}, fmt.Errorf("获取bitget %s 24h行情失败: %v", description, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 解析响应数据
 | 
			
		||||
	var response tickerResponse
 | 
			
		||||
	if err := sonic.Unmarshal(resp, &response); err != nil {
 | 
			
		||||
		return []string{}, fmt.Errorf("解析bitget %s 24h行情失败: %v", description, err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查API响应状态
 | 
			
		||||
	if response.Code != "00000" {
 | 
			
		||||
		return []string{}, fmt.Errorf("bitget %s API错误: %s", description, response.Msg)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 处理行情数据
 | 
			
		||||
	deleteSymbols := make([]string, 0)
 | 
			
		||||
	processedCount := 0
 | 
			
		||||
	errorCount := 0
 | 
			
		||||
 | 
			
		||||
	for _, item := range response.Data {
 | 
			
		||||
		// 构建缓存键
 | 
			
		||||
		key := fmt.Sprintf(tickerType, global.EXCHANGE_BITGET, item.Symbol)
 | 
			
		||||
 | 
			
		||||
		// 检查交易对是否存在
 | 
			
		||||
		symbol, exists := (*tradeSet)[item.Symbol]
 | 
			
		||||
		if !exists {
 | 
			
		||||
			// 清理不存在的交易对缓存
 | 
			
		||||
			if err := helper.DefaultRedis.DeleteString(key); err != nil {
 | 
			
		||||
				log.Warn("删除无效缓存失败: %s, error: %v", key, err)
 | 
			
		||||
			}
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 解析交易对获取coin和currency
 | 
			
		||||
		var coin, currency string
 | 
			
		||||
		if strings.HasSuffix(item.Symbol, symbol.Currency) {
 | 
			
		||||
			coin = strings.TrimSuffix(item.Symbol, symbol.Currency)
 | 
			
		||||
			currency = symbol.Currency
 | 
			
		||||
		} else {
 | 
			
		||||
			deleteSymbols = append(deleteSymbols, item.Symbol)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 构建交易对数据
 | 
			
		||||
		symbol.Coin = coin
 | 
			
		||||
		symbol.Currency = currency
 | 
			
		||||
		symbol.OpenPrice = utility.StringAsFloat(item.Open)
 | 
			
		||||
		symbol.PriceChange = utility.StringAsFloat(item.Change)
 | 
			
		||||
		symbol.LowPrice = item.Low24h
 | 
			
		||||
		symbol.HighPrice = item.High24h
 | 
			
		||||
		symbol.Volume = item.Volume24h
 | 
			
		||||
		symbol.QuoteVolume = item.QuoteVol
 | 
			
		||||
		symbol.LastPrice = item.Close
 | 
			
		||||
 | 
			
		||||
		// 验证数据有效性
 | 
			
		||||
		if symbol.Coin == "" || symbol.Currency == "" ||
 | 
			
		||||
			utility.StringToFloat64(symbol.QuoteVolume) <= 0 ||
 | 
			
		||||
			symbol.LastPrice == "" || utility.StringToFloat64(symbol.LastPrice) <= 0 {
 | 
			
		||||
			if err := helper.DefaultRedis.DeleteString(key); err != nil {
 | 
			
		||||
				log.Warn("删除无效数据缓存失败: %s, error: %v", key, err)
 | 
			
		||||
			}
 | 
			
		||||
			deleteSymbols = append(deleteSymbols, item.Symbol)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 缓存数据
 | 
			
		||||
		val, err := sonic.Marshal(symbol)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Error("序列化交易对数据失败: %s, error: %v", item.Symbol, err)
 | 
			
		||||
			errorCount++
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if err := helper.DefaultRedis.SetString(key, string(val)); err != nil {
 | 
			
		||||
			log.Error("缓存交易对失败: %s, error: %v", item.Symbol, err)
 | 
			
		||||
			errorCount++
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		processedCount++
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Info("处理%s行情数据完成: 成功=%d, 错误=%d, 删除=%d",
 | 
			
		||||
		description, processedCount, errorCount, len(deleteSymbols))
 | 
			
		||||
 | 
			
		||||
	return deleteSymbols, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSpotTicker24h 获取现货24小时行情变更并缓存
 | 
			
		||||
func (api *BitgetRestApi) GetSpotTicker24h(tradeSet *map[string]models.TradeSet) ([]string, error) {
 | 
			
		||||
	return api.getTicker24hData("/api/v2/spot/market/tickers", global.TICKER_SPOT, "现货", tradeSet)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetFutTicker24h 获取U本位合约24小时行情变更并缓存
 | 
			
		||||
func (api *BitgetRestApi) GetFutTicker24h(tradeSet *map[string]models.TradeSet) ([]string, error) {
 | 
			
		||||
	return api.getTicker24hData("/api/v2/mix/market/tickers?productType=USDT-FUTURES", global.TICKER_FUTURES, "U本位合约", tradeSet)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BitgetSymbol Bitget交易对信息
 | 
			
		||||
type BitgetSymbol struct {
 | 
			
		||||
	Symbol        string `json:"symbol"`
 | 
			
		||||
	BaseCoin      string `json:"baseCoin"`
 | 
			
		||||
	QuoteCoin     string `json:"quoteCoin"`
 | 
			
		||||
	BaseAsset     string `json:"baseAsset"`
 | 
			
		||||
	QuoteAsset    string `json:"quoteAsset"`
 | 
			
		||||
	Status        string `json:"status"`
 | 
			
		||||
	MinQty        string `json:"minQty"`
 | 
			
		||||
	MaxQty        string `json:"maxQty"`
 | 
			
		||||
	PriceDigit    int    `json:"priceDigit"`
 | 
			
		||||
	QuantityDigit int    `json:"quantityDigit"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetAndReloadSymbols 获取并重新加载交易对信息
 | 
			
		||||
// symbolType 1-现货 2-合约
 | 
			
		||||
func GetAndReloadSymbols(data *map[string]models.TradeSet, symbolType int) error {
 | 
			
		||||
	var tradeSets map[string]models.TradeSet
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	switch symbolType {
 | 
			
		||||
	case 1:
 | 
			
		||||
		tradeSets, _, err = GetSpotSymbols()
 | 
			
		||||
	case 2:
 | 
			
		||||
		tradeSets, _, err = GetFutSymbols()
 | 
			
		||||
	default:
 | 
			
		||||
		return fmt.Errorf("无效的symbolType: %d", symbolType)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	*data = tradeSets
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// InitSymbolsTicker24h 初始化24小时行情数据
 | 
			
		||||
// symbolType 1-现货 2-合约
 | 
			
		||||
func InitSymbolsTicker24h(data *map[string]models.TradeSet, symbolType int) ([]string, error) {
 | 
			
		||||
	api := NewBitgetRestApi()
 | 
			
		||||
 | 
			
		||||
	switch symbolType {
 | 
			
		||||
	case 1:
 | 
			
		||||
		return api.GetSpotTicker24h(data)
 | 
			
		||||
	case 2:
 | 
			
		||||
		return api.GetFutTicker24h(data)
 | 
			
		||||
	default:
 | 
			
		||||
		return []string{}, fmt.Errorf("无效的symbolType: %d", symbolType)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetSpotTicker24 获取单个交易对24h行情
 | 
			
		||||
func (api *BitgetRestApi) GetSpotTicker24(symbol string, data *models.Ticker24, tradeSet *models.TradeSet) error {
 | 
			
		||||
	key := fmt.Sprintf(global.TICKER_SPOT, global.EXCHANGE_BITGET, symbol)
 | 
			
		||||
	val, err := helper.DefaultRedis.GetString(key)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = sonic.Unmarshal([]byte(val), tradeSet)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if tradeSet.Coin != "" {
 | 
			
		||||
		data.HighPrice = tradeSet.HighPrice
 | 
			
		||||
		data.ChangePercent = fmt.Sprintf("%g", tradeSet.PriceChange)
 | 
			
		||||
		data.LastPrice = tradeSet.LastPrice
 | 
			
		||||
		data.LowPrice = tradeSet.LowPrice
 | 
			
		||||
		data.OpenPrice = fmt.Sprintf("%g", tradeSet.OpenPrice)
 | 
			
		||||
		data.QuoteVolume = tradeSet.QuoteVolume
 | 
			
		||||
		data.Volume = tradeSet.Volume
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										41
									
								
								services/bitgetservice/bitget_rest_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								services/bitgetservice/bitget_rest_test.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
			
		||||
package bitgetservice
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"go-admin/config"
 | 
			
		||||
	"go-admin/test"
 | 
			
		||||
	"testing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestGetSpotSymbols(t *testing.T) {
 | 
			
		||||
	test.InitSettings()
 | 
			
		||||
	api := NewBitgetRestApi()
 | 
			
		||||
 | 
			
		||||
	symbols, err := api.GetSpotSymbols()
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Error(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.Log(symbols)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取交易所信息
 | 
			
		||||
func TestGetExchangeInfo(t *testing.T) {
 | 
			
		||||
	test.InitSettings()
 | 
			
		||||
 | 
			
		||||
	client, err := NewBitgetClient("", "", "", config.ExtConfig.ProxyUrl)
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Error(err)
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	data, err := client.GetExchangeInfo()
 | 
			
		||||
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Error(err)
 | 
			
		||||
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.Log(data)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										658
									
								
								services/bitgetservice/bitget_socketmanager.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										658
									
								
								services/bitgetservice/bitget_socketmanager.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,658 @@
 | 
			
		||||
package bitgetservice
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/hmac"
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"go-admin/pkg/utility"
 | 
			
		||||
	"net"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/signal"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
	"sync/atomic"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/bytedance/sonic"
 | 
			
		||||
	log "github.com/go-admin-team/go-admin-core/logger"
 | 
			
		||||
	"github.com/gorilla/websocket"
 | 
			
		||||
	"golang.org/x/net/proxy"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// BitgetWebSocketManager Bitget WebSocket管理器
 | 
			
		||||
// 类似于BinanceWebSocketManager,支持外部传入API密钥
 | 
			
		||||
type BitgetWebSocketManager struct {
 | 
			
		||||
	ws          *websocket.Conn
 | 
			
		||||
	stopChannel chan struct{}
 | 
			
		||||
	url         string
 | 
			
		||||
	/* 0-现货 1-合约 */
 | 
			
		||||
	wsType       int
 | 
			
		||||
	apiKey       string
 | 
			
		||||
	apiSecret    string
 | 
			
		||||
	passphrase   string
 | 
			
		||||
	proxyType    string
 | 
			
		||||
	proxyAddress string
 | 
			
		||||
	reconnect    chan struct{}
 | 
			
		||||
	isStopped    bool       // 标记 WebSocket 是否已主动停止
 | 
			
		||||
	mu           sync.Mutex // 用于控制并发访问 isStopped
 | 
			
		||||
	cancelFunc   context.CancelFunc
 | 
			
		||||
	reconnecting atomic.Bool // 防止重复重连
 | 
			
		||||
	ConnectTime  time.Time   // 当前连接建立时间
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 已有连接存储
 | 
			
		||||
var BitgetSpotSockets = map[string]*BitgetWebSocketManager{}
 | 
			
		||||
var BitgetFutureSockets = map[string]*BitgetWebSocketManager{}
 | 
			
		||||
 | 
			
		||||
// NewBitgetWebSocketManager 创建新的Bitget WebSocket管理器
 | 
			
		||||
func NewBitgetWebSocketManager(wsType int, apiKey, apiSecret, passphrase, proxyType, proxyAddress string) *BitgetWebSocketManager {
 | 
			
		||||
	var wsUrl string
 | 
			
		||||
 | 
			
		||||
	switch wsType {
 | 
			
		||||
	case 0: // 现货
 | 
			
		||||
		wsUrl = "wss://ws.bitget.com/spot/v1/stream"
 | 
			
		||||
	case 1: // 合约
 | 
			
		||||
		wsUrl = "wss://ws.bitget.com/mix/v1/stream"
 | 
			
		||||
	default:
 | 
			
		||||
		wsUrl = "wss://ws.bitget.com/spot/v1/stream"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &BitgetWebSocketManager{
 | 
			
		||||
		stopChannel:  make(chan struct{}, 10),
 | 
			
		||||
		reconnect:    make(chan struct{}, 10),
 | 
			
		||||
		isStopped:    false,
 | 
			
		||||
		url:          wsUrl,
 | 
			
		||||
		wsType:       wsType,
 | 
			
		||||
		apiKey:       apiKey,
 | 
			
		||||
		apiSecret:    apiSecret,
 | 
			
		||||
		passphrase:   passphrase,
 | 
			
		||||
		proxyType:    proxyType,
 | 
			
		||||
		proxyAddress: proxyAddress,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetKey 获取API Key
 | 
			
		||||
func (wm *BitgetWebSocketManager) GetKey() string {
 | 
			
		||||
	return wm.apiKey
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Start 启动WebSocket连接
 | 
			
		||||
func (wm *BitgetWebSocketManager) Start() {
 | 
			
		||||
	utility.SafeGo(wm.run)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Restart 重启连接
 | 
			
		||||
func (wm *BitgetWebSocketManager) Restart(apiKey, apiSecret, passphrase, proxyType, proxyAddress string) *BitgetWebSocketManager {
 | 
			
		||||
	wm.mu.Lock()
 | 
			
		||||
	defer wm.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	wm.apiKey = apiKey
 | 
			
		||||
	wm.apiSecret = apiSecret
 | 
			
		||||
	wm.passphrase = passphrase
 | 
			
		||||
	wm.proxyType = proxyType
 | 
			
		||||
	wm.proxyAddress = proxyAddress
 | 
			
		||||
 | 
			
		||||
	if wm.isStopped {
 | 
			
		||||
		wm.isStopped = false
 | 
			
		||||
		utility.SafeGo(wm.run)
 | 
			
		||||
	} else {
 | 
			
		||||
		log.Warnf("调用Bitget WebSocket restart")
 | 
			
		||||
		wm.triggerReconnect(true)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return wm
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// triggerReconnect 触发重连
 | 
			
		||||
func (wm *BitgetWebSocketManager) triggerReconnect(force bool) {
 | 
			
		||||
	if force {
 | 
			
		||||
		wm.reconnecting.Store(false)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if wm.reconnecting.CompareAndSwap(false, true) {
 | 
			
		||||
		log.Warnf("准备重连Bitget WebSocket key: %s wsType: %v", wm.apiKey, wm.wsType)
 | 
			
		||||
		select {
 | 
			
		||||
		case wm.reconnect <- struct{}{}:
 | 
			
		||||
		default:
 | 
			
		||||
			log.Debugf("Bitget reconnect 信号已存在,跳过 key:%s", wm.apiKey)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// run 主运行循环
 | 
			
		||||
func (wm *BitgetWebSocketManager) run() {
 | 
			
		||||
	ctx, cancel := context.WithCancel(context.Background())
 | 
			
		||||
	wm.cancelFunc = cancel
 | 
			
		||||
 | 
			
		||||
	utility.SafeGo(wm.handleSignal)
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-ctx.Done():
 | 
			
		||||
			return
 | 
			
		||||
		default:
 | 
			
		||||
			if err := wm.connect(ctx); err != nil {
 | 
			
		||||
				wm.handleConnectionError(err)
 | 
			
		||||
 | 
			
		||||
				if wm.isErrorCountExceeded() {
 | 
			
		||||
					log.Error("连接Bitget WebSocket时出错次数过多,停止WebSocket管理器: wsType=%v, key=%s", wm.wsType, wm.apiKey)
 | 
			
		||||
					wm.Stop()
 | 
			
		||||
					return
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				time.Sleep(5 * time.Second)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			<-wm.stopChannel
 | 
			
		||||
			log.Info("停止Bitget WebSocket管理器... wsType=%s", getBitgetWsTypeName(wm.wsType))
 | 
			
		||||
			wm.Stop()
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleConnectionError 处理连接错误
 | 
			
		||||
func (wm *BitgetWebSocketManager) handleConnectionError(err error) {
 | 
			
		||||
	// 这里可以添加错误记录到Redis的逻辑,类似binance的实现
 | 
			
		||||
	log.Error("连接Bitget WebSocket时出错: wsType=%v, key=%v, 错误=%v", wm.wsType, wm.apiKey, err)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// isErrorCountExceeded 检查错误次数是否超过阈值
 | 
			
		||||
func (wm *BitgetWebSocketManager) isErrorCountExceeded() bool {
 | 
			
		||||
	// 简化版本,可以根据需要实现更复杂的错误计数逻辑
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleSignal 处理终止信号
 | 
			
		||||
func (wm *BitgetWebSocketManager) handleSignal() {
 | 
			
		||||
	ch := make(chan os.Signal, 1)
 | 
			
		||||
	signal.Notify(ch, os.Interrupt)
 | 
			
		||||
	<-ch
 | 
			
		||||
	wm.Stop()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// connect 建立WebSocket连接
 | 
			
		||||
func (wm *BitgetWebSocketManager) connect(ctx context.Context) error {
 | 
			
		||||
	dialer, err := wm.getDialer()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 对于私有频道,需要进行身份验证
 | 
			
		||||
	headers := wm.getAuthHeaders()
 | 
			
		||||
 | 
			
		||||
	wm.ws, _, err = dialer.Dial(wm.url, headers)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 连接成功,更新连接时间
 | 
			
		||||
	wm.ConnectTime = time.Now()
 | 
			
		||||
	log.Info("已连接到Bitget %s WebSocket key:%s", getBitgetWsTypeName(wm.wsType), wm.apiKey)
 | 
			
		||||
 | 
			
		||||
	// 设置Ping处理器
 | 
			
		||||
	wm.ws.SetPingHandler(func(appData string) error {
 | 
			
		||||
		log.Info("收到Bitget Ping消息 wstype:%v key:%s data:%s", wm.wsType, wm.apiKey, appData)
 | 
			
		||||
 | 
			
		||||
		for x := 0; x < 5; x++ {
 | 
			
		||||
			if err := wm.ws.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(time.Second*10)); err != nil {
 | 
			
		||||
				log.Error("Bitget回应Pong失败 次数:%d err:%v", x, err)
 | 
			
		||||
				time.Sleep(time.Second * 1)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// 启动各种处理协程
 | 
			
		||||
	utility.SafeGo(func() { wm.readMessages(ctx) })
 | 
			
		||||
	utility.SafeGo(func() { wm.handleReconnect(ctx) })
 | 
			
		||||
	utility.SafeGo(func() { wm.startPingLoop(ctx) })
 | 
			
		||||
 | 
			
		||||
	// 发送登录认证消息
 | 
			
		||||
	if err := wm.authenticate(); err != nil {
 | 
			
		||||
		return fmt.Errorf("bitget WebSocket认证失败: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getDialer 获取WebSocket拨号器
 | 
			
		||||
func (wm *BitgetWebSocketManager) getDialer() (*websocket.Dialer, error) {
 | 
			
		||||
	if wm.proxyAddress == "" {
 | 
			
		||||
		return &websocket.Dialer{}, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !strings.HasPrefix(wm.proxyAddress, "http://") &&
 | 
			
		||||
		!strings.HasPrefix(wm.proxyAddress, "https://") &&
 | 
			
		||||
		!strings.HasPrefix(wm.proxyAddress, "socks5://") {
 | 
			
		||||
		wm.proxyAddress = wm.proxyType + "://" + wm.proxyAddress
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	proxyURL, err := url.Parse(wm.proxyAddress)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to parse proxy URL: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch proxyURL.Scheme {
 | 
			
		||||
	case "socks5":
 | 
			
		||||
		return wm.createSocks5Dialer(proxyURL)
 | 
			
		||||
	case "http", "https":
 | 
			
		||||
		return &websocket.Dialer{
 | 
			
		||||
			Proxy: func(req *http.Request) (*url.URL, error) {
 | 
			
		||||
				return proxyURL, nil
 | 
			
		||||
			},
 | 
			
		||||
		}, nil
 | 
			
		||||
	default:
 | 
			
		||||
		return nil, fmt.Errorf("unsupported proxy scheme: %s", proxyURL.Scheme)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// createSocks5Dialer 创建SOCKS5代理拨号器
 | 
			
		||||
func (wm *BitgetWebSocketManager) createSocks5Dialer(proxyURL *url.URL) (*websocket.Dialer, error) {
 | 
			
		||||
	auth := &proxy.Auth{}
 | 
			
		||||
	if proxyURL.User != nil {
 | 
			
		||||
		auth.User = proxyURL.User.Username()
 | 
			
		||||
		auth.Password, _ = proxyURL.User.Password()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	socksDialer, err := proxy.SOCKS5("tcp", proxyURL.Host, auth, proxy.Direct)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("failed to create SOCKS5 proxy dialer: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &websocket.Dialer{
 | 
			
		||||
		NetDialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
 | 
			
		||||
			return socksDialer.Dial(network, addr)
 | 
			
		||||
		},
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getAuthHeaders 获取认证头信息
 | 
			
		||||
func (wm *BitgetWebSocketManager) getAuthHeaders() http.Header {
 | 
			
		||||
	headers := http.Header{}
 | 
			
		||||
 | 
			
		||||
	if wm.apiKey != "" {
 | 
			
		||||
		timestamp := strconv.FormatInt(time.Now().Unix(), 10)
 | 
			
		||||
		sign := wm.generateSignature(timestamp)
 | 
			
		||||
 | 
			
		||||
		headers.Set("ACCESS-KEY", wm.apiKey)
 | 
			
		||||
		headers.Set("ACCESS-SIGN", sign)
 | 
			
		||||
		headers.Set("ACCESS-TIMESTAMP", timestamp)
 | 
			
		||||
		headers.Set("ACCESS-PASSPHRASE", wm.passphrase)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return headers
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// generateSignature 生成签名
 | 
			
		||||
func (wm *BitgetWebSocketManager) generateSignature(timestamp string) string {
 | 
			
		||||
	message := timestamp + "GET" + "/user/verify"
 | 
			
		||||
 | 
			
		||||
	h := hmac.New(sha256.New, []byte(wm.apiSecret))
 | 
			
		||||
	h.Write([]byte(message))
 | 
			
		||||
 | 
			
		||||
	return base64.StdEncoding.EncodeToString(h.Sum(nil))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// authenticate 发送认证消息
 | 
			
		||||
func (wm *BitgetWebSocketManager) authenticate() error {
 | 
			
		||||
	if wm.apiKey == "" {
 | 
			
		||||
		return nil // 公共频道不需要认证
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	timestamp := strconv.FormatInt(time.Now().Unix(), 10)
 | 
			
		||||
	sign := wm.generateSignature(timestamp)
 | 
			
		||||
 | 
			
		||||
	authMsg := map[string]interface{}{
 | 
			
		||||
		"op": "login",
 | 
			
		||||
		"args": []map[string]string{
 | 
			
		||||
			{
 | 
			
		||||
				"apiKey":     wm.apiKey,
 | 
			
		||||
				"passphrase": wm.passphrase,
 | 
			
		||||
				"timestamp":  timestamp,
 | 
			
		||||
				"sign":       sign,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	msgBytes, err := sonic.Marshal(authMsg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return wm.ws.WriteMessage(websocket.TextMessage, msgBytes)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// readMessages 读取消息
 | 
			
		||||
func (wm *BitgetWebSocketManager) readMessages(ctx context.Context) {
 | 
			
		||||
	defer func() {
 | 
			
		||||
		if r := recover(); r != nil {
 | 
			
		||||
			log.Error("Bitget WebSocket消息处理器panic: %v", r)
 | 
			
		||||
		}
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-ctx.Done():
 | 
			
		||||
			return
 | 
			
		||||
		default:
 | 
			
		||||
			wm.mu.Lock()
 | 
			
		||||
			if wm.isStopped || wm.ws == nil {
 | 
			
		||||
				wm.mu.Unlock()
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			_, message, err := wm.ws.ReadMessage()
 | 
			
		||||
			wm.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Error("Bitget WebSocket读取消息错误: %v", err)
 | 
			
		||||
				wm.triggerReconnect(false)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 处理消息
 | 
			
		||||
			wm.handleMessage(message)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleMessage 处理收到的消息
 | 
			
		||||
func (wm *BitgetWebSocketManager) handleMessage(message []byte) {
 | 
			
		||||
	// 解析消息并处理
 | 
			
		||||
	var baseMsg map[string]interface{}
 | 
			
		||||
	if err := sonic.Unmarshal(message, &baseMsg); err != nil {
 | 
			
		||||
		log.Error("解析Bitget WebSocket消息失败: %v", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查是否是认证响应
 | 
			
		||||
	if op, exists := baseMsg["op"]; exists && op == "login" {
 | 
			
		||||
		wm.handleAuthResponse(baseMsg)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 检查是否是订阅数据
 | 
			
		||||
	if arg, exists := baseMsg["arg"]; exists {
 | 
			
		||||
		wm.handleSubscriptionData(baseMsg, arg)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Info("收到Bitget WebSocket消息: %s", string(message))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleAuthResponse 处理认证响应
 | 
			
		||||
func (wm *BitgetWebSocketManager) handleAuthResponse(msg map[string]interface{}) {
 | 
			
		||||
	if code, exists := msg["code"]; exists {
 | 
			
		||||
		if code == "0" || code == 0 {
 | 
			
		||||
			log.Info("Bitget WebSocket认证成功 key:%s", wm.apiKey)
 | 
			
		||||
			// 认证成功后可以订阅私有频道
 | 
			
		||||
			wm.subscribePrivateChannels()
 | 
			
		||||
		} else {
 | 
			
		||||
			log.Error("Bitget WebSocket认证失败 key:%s code:%v", wm.apiKey, code)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleSubscriptionData 处理订阅数据
 | 
			
		||||
func (wm *BitgetWebSocketManager) handleSubscriptionData(msg map[string]interface{}, arg interface{}) {
 | 
			
		||||
	// 根据频道类型处理不同的数据
 | 
			
		||||
	// 这里可以根据具体需求实现不同频道的数据处理逻辑
 | 
			
		||||
	log.Info("收到Bitget订阅数据: %v", arg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// subscribePrivateChannels 订阅私有频道
 | 
			
		||||
func (wm *BitgetWebSocketManager) subscribePrivateChannels() {
 | 
			
		||||
	// 订阅账户更新
 | 
			
		||||
	wm.subscribeAccount()
 | 
			
		||||
	// 订阅订单更新
 | 
			
		||||
	wm.subscribeOrders()
 | 
			
		||||
	// 订阅持仓更新
 | 
			
		||||
	wm.subscribePositions()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// subscribeAccount 订阅账户更新
 | 
			
		||||
func (wm *BitgetWebSocketManager) subscribeAccount() {
 | 
			
		||||
	var channel string
 | 
			
		||||
	if wm.wsType == 0 {
 | 
			
		||||
		channel = "account"
 | 
			
		||||
	} else {
 | 
			
		||||
		channel = "account"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	subMsg := map[string]interface{}{
 | 
			
		||||
		"op": "subscribe",
 | 
			
		||||
		"args": []map[string]string{
 | 
			
		||||
			{
 | 
			
		||||
				"instType": wm.getInstType(),
 | 
			
		||||
				"channel":  channel,
 | 
			
		||||
				"instId":   "default",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wm.sendMessage(subMsg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// subscribeOrders 订阅订单更新
 | 
			
		||||
func (wm *BitgetWebSocketManager) subscribeOrders() {
 | 
			
		||||
	var channel string
 | 
			
		||||
	if wm.wsType == 0 {
 | 
			
		||||
		channel = "orders"
 | 
			
		||||
	} else {
 | 
			
		||||
		channel = "orders"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	subMsg := map[string]interface{}{
 | 
			
		||||
		"op": "subscribe",
 | 
			
		||||
		"args": []map[string]string{
 | 
			
		||||
			{
 | 
			
		||||
				"instType": wm.getInstType(),
 | 
			
		||||
				"channel":  channel,
 | 
			
		||||
				"instId":   "default",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wm.sendMessage(subMsg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// subscribePositions 订阅持仓更新(仅合约)
 | 
			
		||||
func (wm *BitgetWebSocketManager) subscribePositions() {
 | 
			
		||||
	if wm.wsType != 1 {
 | 
			
		||||
		return // 只有合约才有持仓
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	subMsg := map[string]interface{}{
 | 
			
		||||
		"op": "subscribe",
 | 
			
		||||
		"args": []map[string]string{
 | 
			
		||||
			{
 | 
			
		||||
				"instType": "umcbl",
 | 
			
		||||
				"channel":  "positions",
 | 
			
		||||
				"instId":   "default",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wm.sendMessage(subMsg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getInstType 获取产品类型
 | 
			
		||||
func (wm *BitgetWebSocketManager) getInstType() string {
 | 
			
		||||
	if wm.wsType == 0 {
 | 
			
		||||
		return "sp" // 现货
 | 
			
		||||
	}
 | 
			
		||||
	return "umcbl" // U本位合约
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SendMessage 发送消息(公开方法)
 | 
			
		||||
func (wm *BitgetWebSocketManager) SendMessage(msg interface{}) error {
 | 
			
		||||
	msgBytes, err := sonic.Marshal(msg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wm.mu.Lock()
 | 
			
		||||
	defer wm.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	if wm.ws == nil {
 | 
			
		||||
		return errors.New("WebSocket连接不可用")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return wm.ws.WriteMessage(websocket.TextMessage, msgBytes)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// sendMessage 发送消息(私有方法,保持向后兼容)
 | 
			
		||||
func (wm *BitgetWebSocketManager) sendMessage(msg interface{}) error {
 | 
			
		||||
	return wm.SendMessage(msg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleReconnect 处理重连
 | 
			
		||||
func (wm *BitgetWebSocketManager) handleReconnect(ctx context.Context) {
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-ctx.Done():
 | 
			
		||||
			return
 | 
			
		||||
		case <-wm.reconnect:
 | 
			
		||||
			wm.reconnecting.Store(false)
 | 
			
		||||
			log.Info("开始重连Bitget WebSocket key:%s", wm.apiKey)
 | 
			
		||||
 | 
			
		||||
			wm.mu.Lock()
 | 
			
		||||
			if wm.ws != nil {
 | 
			
		||||
				wm.ws.Close()
 | 
			
		||||
				wm.ws = nil
 | 
			
		||||
			}
 | 
			
		||||
			wm.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
			// 触发主循环重新连接
 | 
			
		||||
			select {
 | 
			
		||||
			case wm.stopChannel <- struct{}{}:
 | 
			
		||||
			default:
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// startPingLoop 启动心跳循环
 | 
			
		||||
func (wm *BitgetWebSocketManager) startPingLoop(ctx context.Context) {
 | 
			
		||||
	ticker := time.NewTicker(30 * time.Second)
 | 
			
		||||
	defer ticker.Stop()
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-ctx.Done():
 | 
			
		||||
			return
 | 
			
		||||
		case <-ticker.C:
 | 
			
		||||
			wm.sendPing()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// sendPing 发送心跳
 | 
			
		||||
func (wm *BitgetWebSocketManager) sendPing() {
 | 
			
		||||
	pingMsg := "ping"
 | 
			
		||||
 | 
			
		||||
	wm.mu.Lock()
 | 
			
		||||
	defer wm.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	if wm.ws == nil {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := wm.ws.WriteMessage(websocket.TextMessage, []byte(pingMsg)); err != nil {
 | 
			
		||||
		log.Error("Bitget WebSocket发送ping失败: %v", err)
 | 
			
		||||
		wm.triggerReconnect(false)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Stop 停止WebSocket连接
 | 
			
		||||
func (wm *BitgetWebSocketManager) Stop() {
 | 
			
		||||
	wm.mu.Lock()
 | 
			
		||||
	defer wm.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	if wm.isStopped {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wm.isStopped = true
 | 
			
		||||
 | 
			
		||||
	if wm.cancelFunc != nil {
 | 
			
		||||
		wm.cancelFunc()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if wm.ws != nil {
 | 
			
		||||
		wm.ws.Close()
 | 
			
		||||
		wm.ws = nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 从全局map中移除
 | 
			
		||||
	if wm.wsType == 0 {
 | 
			
		||||
		delete(BitgetSpotSockets, wm.apiKey)
 | 
			
		||||
	} else {
 | 
			
		||||
		delete(BitgetFutureSockets, wm.apiKey)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Info("Bitget WebSocket已停止 key:%s", wm.apiKey)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getBitgetWsTypeName 获取WebSocket类型名称
 | 
			
		||||
func getBitgetWsTypeName(wsType int) string {
 | 
			
		||||
	switch wsType {
 | 
			
		||||
	case 0:
 | 
			
		||||
		return "现货"
 | 
			
		||||
	case 1:
 | 
			
		||||
		return "合约"
 | 
			
		||||
	default:
 | 
			
		||||
		return "未知"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 工具函数:获取或创建Bitget WebSocket管理器
 | 
			
		||||
func GetOrCreateBitgetWebSocketManager(wsType int, apiKey, apiSecret, passphrase, proxyType, proxyAddress string) *BitgetWebSocketManager {
 | 
			
		||||
	var socketsMap map[string]*BitgetWebSocketManager
 | 
			
		||||
 | 
			
		||||
	if wsType == 0 {
 | 
			
		||||
		socketsMap = BitgetSpotSockets
 | 
			
		||||
	} else {
 | 
			
		||||
		socketsMap = BitgetFutureSockets
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if manager, exists := socketsMap[apiKey]; exists {
 | 
			
		||||
		// 如果参数有变化,重启连接
 | 
			
		||||
		if manager.apiSecret != apiSecret || manager.passphrase != passphrase ||
 | 
			
		||||
			manager.proxyType != proxyType || manager.proxyAddress != proxyAddress {
 | 
			
		||||
			manager.Restart(apiKey, apiSecret, passphrase, proxyType, proxyAddress)
 | 
			
		||||
		}
 | 
			
		||||
		return manager
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 创建新的管理器
 | 
			
		||||
	manager := NewBitgetWebSocketManager(wsType, apiKey, apiSecret, passphrase, proxyType, proxyAddress)
 | 
			
		||||
	socketsMap[apiKey] = manager
 | 
			
		||||
 | 
			
		||||
	return manager
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// StopBitgetWebSocketManager 停止指定的WebSocket管理器
 | 
			
		||||
func StopBitgetWebSocketManager(wsType int, apiKey string) {
 | 
			
		||||
	var socketsMap map[string]*BitgetWebSocketManager
 | 
			
		||||
 | 
			
		||||
	if wsType == 0 {
 | 
			
		||||
		socketsMap = BitgetSpotSockets
 | 
			
		||||
	} else {
 | 
			
		||||
		socketsMap = BitgetFutureSockets
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if manager, exists := socketsMap[apiKey]; exists {
 | 
			
		||||
		manager.Stop()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -64,33 +64,58 @@ 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) {
 | 
			
		||||
	// 定义返回结果和val变量
 | 
			
		||||
	result := models2.TradeSet{}
 | 
			
		||||
	val := ""
 | 
			
		||||
 | 
			
		||||
	// 根据交易对类型选择不同的key
 | 
			
		||||
	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)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 如果val不为空,则解析val为TradeSet结构体
 | 
			
		||||
	if val != "" {
 | 
			
		||||
		if err := sonic.Unmarshal([]byte(val), &result); err != nil {
 | 
			
		||||
			return result, err
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		// 如果val为空,则返回错误信息
 | 
			
		||||
		return result, errors.New("未找到交易对信息")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 返回结果
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										15
									
								
								services/commonservice/commonservice.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								services/commonservice/commonservice.go
									
									
									
									
									
										Normal 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
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										63
									
								
								services/commonservice/judge.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								services/commonservice/judge.go
									
									
									
									
									
										Normal 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
 | 
			
		||||
}
 | 
			
		||||
@ -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) (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) (reconnect bool, err error) {
 | 
			
		||||
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,6 @@ package excservice
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/tls"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"go-admin/common/global"
 | 
			
		||||
@ -11,44 +10,146 @@ 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"
 | 
			
		||||
	"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"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
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 // 新增字段
 | 
			
		||||
	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 := ""
 | 
			
		||||
 | 
			
		||||
@ -59,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,
 | 
			
		||||
@ -70,14 +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()
 | 
			
		||||
@ -91,20 +214,51 @@ func (wm *BinanceWebSocketManager) Restart(apiKey, apiSecret, proxyType, proxyAd
 | 
			
		||||
		wm.isStopped = false
 | 
			
		||||
		utility.SafeGo(wm.run)
 | 
			
		||||
	} else {
 | 
			
		||||
		wm.reconnect <- struct{}{}
 | 
			
		||||
		log.Warnf("调用restart")
 | 
			
		||||
		wm.triggerReconnect(true)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return wm
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Restart(wm *BinanceWebSocketManager) {
 | 
			
		||||
	wm.reconnect <- struct{}{}
 | 
			
		||||
/**
 | 
			
		||||
 * 触发重连机制
 | 
			
		||||
 * @param force 是否强制重连
 | 
			
		||||
 */
 | 
			
		||||
func (wm *BinanceWebSocketManager) triggerReconnect(force bool) {
 | 
			
		||||
	if force {
 | 
			
		||||
		wm.reconnecting.Store(false) // 强制重置标志位
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if wm.reconnecting.CompareAndSwap(false, true) {
 | 
			
		||||
		log.Warnf("准备重连 key: %s wsType: %v", wm.apiKey, wm.wsType)
 | 
			
		||||
		// 发送信号触发重连协程
 | 
			
		||||
		select {
 | 
			
		||||
		case wm.reconnect <- struct{}{}:
 | 
			
		||||
		default:
 | 
			
		||||
			// 防止阻塞,如果通道满了就跳过
 | 
			
		||||
			log.Debugf("reconnect 信号已存在,跳过 key:%s", wm.apiKey)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 外部重启函数
 | 
			
		||||
 * @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)
 | 
			
		||||
@ -137,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
 | 
			
		||||
@ -160,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 == "" {
 | 
			
		||||
@ -175,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
 | 
			
		||||
	}
 | 
			
		||||
@ -201,6 +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处理
 | 
			
		||||
@ -210,22 +372,102 @@ 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.SafeGoParam(wm.restartConnect, ctx)
 | 
			
		||||
	// 启动各种协程
 | 
			
		||||
	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.start24HourReconnectTimer(ctx) }) // 启动24小时强制重连
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ReplaceConnection 创建新连接并关闭旧连接,实现无缝连接替换
 | 
			
		||||
func (wm *BinanceWebSocketManager) ReplaceConnection() error {
 | 
			
		||||
	wm.mu.Lock()
 | 
			
		||||
	if wm.isStopped {
 | 
			
		||||
		wm.mu.Unlock()
 | 
			
		||||
		return errors.New("WebSocket 已停止")
 | 
			
		||||
	}
 | 
			
		||||
	oldCtxCancel := wm.cancelFunc
 | 
			
		||||
	oldConn := wm.ws
 | 
			
		||||
	wm.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	log.Infof("🔄 正在替换连接: %s", wm.apiKey)
 | 
			
		||||
 | 
			
		||||
	// 步骤 1:先获取新的 listenKey 和连接
 | 
			
		||||
	newListenKey, err := wm.getListenKey()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("获取新 listenKey 失败: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dialer, err := proxy.GetDialer(wm.proxyType, wm.proxyAddress)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	newURL := fmt.Sprintf("%s/%s", wm.url, newListenKey)
 | 
			
		||||
	newConn, _, err := dialer.Dial(newURL, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("新连接 Dial 失败: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 步骤 2:创建新的上下文并启动协程
 | 
			
		||||
	newCtx, newCancel := context.WithCancel(context.Background())
 | 
			
		||||
 | 
			
		||||
	// 设置 ping handler
 | 
			
		||||
	newConn.SetPingHandler(func(appData string) error {
 | 
			
		||||
		log.Infof("收到 Ping(新连接) key:%s msg:%s", wm.apiKey, appData)
 | 
			
		||||
		for x := 0; x < 5; x++ {
 | 
			
		||||
			if err := newConn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(10*time.Second)); err != nil {
 | 
			
		||||
				log.Errorf("Pong 失败 %d 次 err:%v", x, err)
 | 
			
		||||
				time.Sleep(time.Second)
 | 
			
		||||
				continue
 | 
			
		||||
			}
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
		setLastTime(wm)
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	// 步骤 3:安全切换连接
 | 
			
		||||
	wm.mu.Lock()
 | 
			
		||||
	wm.ws = newConn
 | 
			
		||||
	wm.listenKey = newListenKey
 | 
			
		||||
	wm.ConnectTime = time.Now()
 | 
			
		||||
	wm.cancelFunc = newCancel
 | 
			
		||||
	wm.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	log.Infof("✅ 替换连接成功: %s listenKey: %s", wm.apiKey, newListenKey)
 | 
			
		||||
 | 
			
		||||
	// 步骤 4:启动新连接协程
 | 
			
		||||
	go wm.startListenKeyRenewal2(newCtx)
 | 
			
		||||
	go wm.readMessages(newCtx)
 | 
			
		||||
	go wm.handleReconnect(newCtx)
 | 
			
		||||
	go wm.startPingLoop(newCtx)
 | 
			
		||||
	// go wm.startDeadCheck(newCtx)
 | 
			
		||||
 | 
			
		||||
	// 步骤 5:关闭旧连接、取消旧协程
 | 
			
		||||
	if oldCtxCancel != nil {
 | 
			
		||||
		oldCtxCancel()
 | 
			
		||||
	}
 | 
			
		||||
	if oldConn != nil {
 | 
			
		||||
		_ = oldConn.Close()
 | 
			
		||||
		log.Infof("🔒 旧连接已关闭: %s", wm.apiKey)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
@ -251,54 +493,7 @@ func setLastTime(wm *BinanceWebSocketManager) {
 | 
			
		||||
	if val != "" {
 | 
			
		||||
		helper.DefaultRedis.SetString(subKey, val)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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客户端的逻辑
 | 
			
		||||
@ -343,7 +538,6 @@ func (wm *BinanceWebSocketManager) getListenKey() (string, error) {
 | 
			
		||||
 | 
			
		||||
// 接收消息
 | 
			
		||||
func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) {
 | 
			
		||||
	defer wm.ws.Close()
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
@ -355,19 +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 {
 | 
			
		||||
					wm.reconnect <- struct{}{}
 | 
			
		||||
			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)
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -375,130 +573,73 @@ func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) {
 | 
			
		||||
func (wm *BinanceWebSocketManager) handleOrderUpdate(msg []byte) {
 | 
			
		||||
	setLastTime(wm)
 | 
			
		||||
 | 
			
		||||
	if reconnect, _ := ReceiveListen(msg, wm.wsType); reconnect {
 | 
			
		||||
		wm.reconnect <- struct{}{}
 | 
			
		||||
	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)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (wm *BinanceWebSocketManager) Stop() {
 | 
			
		||||
// closeConn 安全地关闭WebSocket连接
 | 
			
		||||
func (wm *BinanceWebSocketManager) closeConn() {
 | 
			
		||||
	wm.mu.Lock()
 | 
			
		||||
	defer wm.mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	if wm.isStopped {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wm.isStopped = true
 | 
			
		||||
	// 关闭 stopChannel(确保已经关闭,避免 panic)
 | 
			
		||||
	select {
 | 
			
		||||
	case <-wm.stopChannel:
 | 
			
		||||
	default:
 | 
			
		||||
		close(wm.stopChannel)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if wm.cancelFunc != nil {
 | 
			
		||||
		wm.cancelFunc()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if wm.ws != nil {
 | 
			
		||||
		if err := wm.ws.Close(); err != nil {
 | 
			
		||||
			log.Error(fmt.Sprintf("key【%s】close失败", wm.apiKey), err)
 | 
			
		||||
		} else {
 | 
			
		||||
			log.Info(fmt.Sprintf("key【%s】close", wm.apiKey))
 | 
			
		||||
		}
 | 
			
		||||
		_ = wm.ws.Close()
 | 
			
		||||
		wm.ws = nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// **重新创建 stopChannel,避免 Restart() 时无效**
 | 
			
		||||
	wm.stopChannel = make(chan struct{})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 重连机制
 | 
			
		||||
func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) {
 | 
			
		||||
	maxRetries := 5 // 最大重试次数
 | 
			
		||||
	retryCount := 0
 | 
			
		||||
// 主动心跳发送机制
 | 
			
		||||
func (wm *BinanceWebSocketManager) startPingLoop(ctx context.Context) {
 | 
			
		||||
	ticker := time.NewTicker(1 * time.Minute)
 | 
			
		||||
	defer ticker.Stop()
 | 
			
		||||
 | 
			
		||||
	for {
 | 
			
		||||
		select {
 | 
			
		||||
		case <-ctx.Done():
 | 
			
		||||
			return
 | 
			
		||||
		case <-wm.reconnect:
 | 
			
		||||
		case <-ticker.C:
 | 
			
		||||
			if wm.isStopped {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			log.Warn("WebSocket 连接断开,尝试重连...")
 | 
			
		||||
 | 
			
		||||
			if wm.ws != nil {
 | 
			
		||||
				wm.ws.Close()
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			// 取消旧的上下文
 | 
			
		||||
			if wm.cancelFunc != nil {
 | 
			
		||||
				wm.cancelFunc()
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			for {
 | 
			
		||||
				newCtx, cancel := context.WithCancel(context.Background())
 | 
			
		||||
				wm.cancelFunc = cancel // 更新 cancelFunc
 | 
			
		||||
 | 
			
		||||
				if err := wm.connect(newCtx); err != nil {
 | 
			
		||||
					log.Errorf("重连失败: %v", err)
 | 
			
		||||
					cancel()
 | 
			
		||||
					retryCount++
 | 
			
		||||
 | 
			
		||||
					if retryCount >= maxRetries {
 | 
			
		||||
						log.Error("重连失败次数过多,退出重连逻辑")
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					time.Sleep(5 * time.Second)
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				return
 | 
			
		||||
			if err := wm.sendPing(); err != nil {
 | 
			
		||||
				log.Error("主动 Ping Binance 失败:", err)
 | 
			
		||||
				wm.triggerReconnect(false)
 | 
			
		||||
				return // 退出循环,等待重连
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 定期删除listenkey 并重启ws
 | 
			
		||||
func (wm *BinanceWebSocketManager) startListenKeyRenewal(ctx context.Context, listenKey string) {
 | 
			
		||||
	time.Sleep(30 * time.Minute)
 | 
			
		||||
// func (wm *BinanceWebSocketManager) startListenKeyRenewal(ctx context.Context, listenKey string) {
 | 
			
		||||
// 	time.Sleep(30 * time.Minute)
 | 
			
		||||
 | 
			
		||||
	select {
 | 
			
		||||
	case <-ctx.Done():
 | 
			
		||||
		return
 | 
			
		||||
	default:
 | 
			
		||||
		if err := wm.deleteListenKey(listenKey); err != nil {
 | 
			
		||||
			log.Error("Failed to renew listenKey: ,type:%v key: %s", wm.wsType, wm.apiKey, err)
 | 
			
		||||
		} else {
 | 
			
		||||
			log.Debug("Successfully delete listenKey")
 | 
			
		||||
			wm.reconnect <- struct{}{}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
// 	select {
 | 
			
		||||
// 	case <-ctx.Done():
 | 
			
		||||
// 		return
 | 
			
		||||
// 	default:
 | 
			
		||||
// 		if err := wm.deleteListenKey(listenKey); err != nil {
 | 
			
		||||
// 			log.Error("Failed to renew listenKey: ,type:%v key: %s", wm.wsType, wm.apiKey, err)
 | 
			
		||||
// 		} else {
 | 
			
		||||
// 			log.Debug("Successfully delete listenKey")
 | 
			
		||||
// 			wm.triggerReconnect()
 | 
			
		||||
// 		}
 | 
			
		||||
// 	}
 | 
			
		||||
 | 
			
		||||
	// ticker := time.NewTicker(5 * time.Minute)
 | 
			
		||||
	// defer ticker.Stop()
 | 
			
		||||
 | 
			
		||||
	// for {
 | 
			
		||||
	// 	select {
 | 
			
		||||
	// 	case <-ticker.C:
 | 
			
		||||
	// 		if wm.isStopped {
 | 
			
		||||
	// 			return
 | 
			
		||||
	// 		}
 | 
			
		||||
 | 
			
		||||
	// 		if err := wm.deleteListenKey(listenKey); err != nil {
 | 
			
		||||
	// 			log.Error("Failed to renew listenKey: ,type:%v key: %s", wm.wsType, wm.apiKey, err)
 | 
			
		||||
	// 		} else {
 | 
			
		||||
	// 			log.Debug("Successfully delete listenKey")
 | 
			
		||||
	// 			wm.reconnect <- struct{}{}
 | 
			
		||||
	// 			return
 | 
			
		||||
	// 		}
 | 
			
		||||
	// 	case <-ctx.Done():
 | 
			
		||||
	// 		return
 | 
			
		||||
	// 	}
 | 
			
		||||
	// }
 | 
			
		||||
}
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
// 定时续期
 | 
			
		||||
func (wm *BinanceWebSocketManager) startListenKeyRenewal2(ctx context.Context) {
 | 
			
		||||
@ -527,49 +668,34 @@ func (wm *BinanceWebSocketManager) startListenKeyRenewal2(ctx context.Context) {
 | 
			
		||||
/*
 | 
			
		||||
删除listenkey
 | 
			
		||||
*/
 | 
			
		||||
func (wm *BinanceWebSocketManager) deleteListenKey(listenKey string) error {
 | 
			
		||||
	client, err := wm.createBinanceClient()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
// func (wm *BinanceWebSocketManager) deleteListenKey(listenKey string) error {
 | 
			
		||||
// 	client, err := wm.createBinanceClient()
 | 
			
		||||
// 	if err != nil {
 | 
			
		||||
// 		return err
 | 
			
		||||
// 	}
 | 
			
		||||
 | 
			
		||||
	var resp []byte
 | 
			
		||||
// 	var resp []byte
 | 
			
		||||
 | 
			
		||||
	switch wm.wsType {
 | 
			
		||||
	case 0:
 | 
			
		||||
		path := fmt.Sprintf("/api/v3/userDataStream")
 | 
			
		||||
		params := map[string]interface{}{
 | 
			
		||||
			"listenKey": listenKey,
 | 
			
		||||
		}
 | 
			
		||||
		resp, _, err = client.SendSpotRequestByKey(path, "DELETE", params)
 | 
			
		||||
// 	switch wm.wsType {
 | 
			
		||||
// 	case 0:
 | 
			
		||||
// 		path := fmt.Sprintf("/api/v3/userDataStream")
 | 
			
		||||
// 		params := map[string]interface{}{
 | 
			
		||||
// 			"listenKey": listenKey,
 | 
			
		||||
// 		}
 | 
			
		||||
// 		resp, _, err = client.SendSpotRequestByKey(path, "DELETE", params)
 | 
			
		||||
 | 
			
		||||
		log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp)))
 | 
			
		||||
	case 1:
 | 
			
		||||
		resp, _, err = client.SendFuturesRequestByKey("/fapi/v1/listenKey", "DELETE", nil)
 | 
			
		||||
		log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp)))
 | 
			
		||||
	default:
 | 
			
		||||
		return errors.New("unknown ws type")
 | 
			
		||||
	}
 | 
			
		||||
// 		log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp)))
 | 
			
		||||
// 	case 1:
 | 
			
		||||
// 		resp, _, err = client.SendFuturesRequestByKey("/fapi/v1/listenKey", "DELETE", nil)
 | 
			
		||||
// 		log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp)))
 | 
			
		||||
// 	default:
 | 
			
		||||
// 		return errors.New("unknown ws type")
 | 
			
		||||
// 	}
 | 
			
		||||
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
// 	return err
 | 
			
		||||
// }
 | 
			
		||||
 | 
			
		||||
func (wm *BinanceWebSocketManager) renewListenKey(listenKey string) error {
 | 
			
		||||
	// payloadParam := map[string]interface{}{
 | 
			
		||||
	// 	"listenKey": listenKey,
 | 
			
		||||
	// 	"apiKey":    wm.apiKey,
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	// params := map[string]interface{}{
 | 
			
		||||
	// 	"id":     getUUID(),
 | 
			
		||||
	// 	"method": "userDataStream.ping",
 | 
			
		||||
	// 	"params": payloadParam,
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	// if err := wm.ws.WriteJSON(params); err != nil {
 | 
			
		||||
	// 	return err
 | 
			
		||||
	// }
 | 
			
		||||
	// wm.ws.WriteJSON()
 | 
			
		||||
	client, err := wm.createBinanceClient()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
@ -615,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 上下文
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										218
									
								
								services/excservice/bitget_receive.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										218
									
								
								services/excservice/bitget_receive.go
									
									
									
									
									
										Normal 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 "未知"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -162,19 +162,14 @@ func handleTickerAllMessage(msg []byte) {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		val, _ := helper.DefaultRedis.GetAllList(rediskey.CacheSymbolLastPrice)
 | 
			
		||||
		//行情存储时间
 | 
			
		||||
		lastUtc := utcTime - 1000*60*60
 | 
			
		||||
		if _, err := helper.DefaultRedis.RemoveBeforeScore(lastPriceKey, float64(lastUtc)); err != nil {
 | 
			
		||||
			log.Errorf("移除 合约交易对:%s %d之前的最新成交价失败,err:%v", symbol, lastUtc, err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if utility.ContainsStr(val, symbol) {
 | 
			
		||||
			//行情存储时间
 | 
			
		||||
			lastUtc := utcTime - 1000*60*60
 | 
			
		||||
			content := fmt.Sprintf("%d:%s", utcTime, lastPrice.String())
 | 
			
		||||
			if _, err := helper.DefaultRedis.RemoveBeforeScore(lastPriceKey, float64(lastUtc)); err != nil {
 | 
			
		||||
				log.Errorf("移除 合约交易对:%s %d之前的最新成交价失败,err:%v", symbol, lastUtc, err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err := helper.DefaultRedis.AddSortSet(lastPriceKey, float64(utcTime), content); err != nil {
 | 
			
		||||
				log.Errorf("添加 合约交易对:%s %d之前的最新成交价失败,err:%v", symbol, lastUtc, err)
 | 
			
		||||
			}
 | 
			
		||||
		if err := helper.DefaultRedis.AddSortSet(lastPriceKey, float64(utcTime), lastPrice.String()); err != nil {
 | 
			
		||||
			log.Errorf("添加 合约交易对:%s %d之前的最新成交价失败,err:%v", symbol, lastUtc, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										108
									
								
								services/orderservice/order_service.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								services/orderservice/order_service.go
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										63
									
								
								services/proxy/proxy.go
									
									
									
									
									
										Normal 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
 | 
			
		||||
}
 | 
			
		||||
@ -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)
 | 
			
		||||
 | 
			
		||||
@ -250,20 +234,15 @@ func handleTickerMessage(msg []byte) {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		val, _ := helper.DefaultRedis.GetAllList(rediskey.CacheSymbolLastPrice)
 | 
			
		||||
		//行情存储时间
 | 
			
		||||
		lastUtc := utcTime - 1000*60*60
 | 
			
		||||
 | 
			
		||||
		if utility.ContainsStr(val, symbolName) {
 | 
			
		||||
			//行情存储时间
 | 
			
		||||
			lastUtc := utcTime - 1000*60*60
 | 
			
		||||
			content := fmt.Sprintf("%d:%s", utcTime, lastPrice.String())
 | 
			
		||||
		if _, err := helper.DefaultRedis.RemoveBeforeScore(lastPriceKey, float64(lastUtc)); err != nil {
 | 
			
		||||
			log.Errorf("移除 现货交易对:%s %d之前的最新成交价失败,err:%v", symbolName, lastUtc, err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
			if _, err := helper.DefaultRedis.RemoveBeforeScore(lastPriceKey, float64(lastUtc)); err != nil {
 | 
			
		||||
				log.Errorf("移除 现货交易对:%s %d之前的最新成交价失败,err:%v", symbolName, lastUtc, err)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if err := helper.DefaultRedis.AddSortSet(lastPriceKey, float64(utcTime), content); err != nil {
 | 
			
		||||
				log.Errorf("添加 现货交易对:%s %d之前的最新成交价失败,err:%v", symbolName, lastUtc, err)
 | 
			
		||||
			}
 | 
			
		||||
		if err := helper.DefaultRedis.AddSortSet(lastPriceKey, float64(utcTime), lastPrice.String()); err != nil {
 | 
			
		||||
			log.Errorf("添加 现货交易对:%s %d之前的最新成交价失败,err:%v", symbolName, lastUtc, err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										68
									
								
								services/tradsetservice/trade_set.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								services/tradsetservice/trade_set.go
									
									
									
									
									
										Normal 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
 | 
			
		||||
}
 | 
			
		||||
@ -1,22 +1,22 @@
 | 
			
		||||
package jobs
 | 
			
		||||
package test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"go-admin/common/helper"
 | 
			
		||||
	"testing"
 | 
			
		||||
	"go-admin/config"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-admin-team/go-admin-core/sdk"
 | 
			
		||||
	"gorm.io/driver/mysql"
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestStrategyJob(t *testing.T) {
 | 
			
		||||
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")
 | 
			
		||||
 | 
			
		||||
	job := StrategyJob{}
 | 
			
		||||
	config.ExtConfig.ProxyUrl = "http://127.0.0.1:7890"
 | 
			
		||||
 | 
			
		||||
	job.Exec([]string{})
 | 
			
		||||
	return db
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user