From 4b28684fe4281e9bb772d2431bd66a8a0ea8c886 Mon Sep 17 00:00:00 2001 From: hucan <951870319@qq.com> Date: Mon, 11 Aug 2025 09:27:32 +0800 Subject: [PATCH] =?UTF-8?q?1=E6=9A=82=E6=97=B6=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/apis/line_api_user.go | 21 + app/admin/apis/line_reverse_position.go | 59 ++ app/admin/router/line_api_user.go | 2 + app/admin/router/line_reverse_position.go | 7 +- app/admin/service/dto/line_api_user.go | 5 + app/admin/service/dto/line_reverse_order.go | 2 + .../service/dto/line_reverse_position.go | 20 + app/admin/service/line_api_user.go | 54 +- app/admin/service/line_reverse_order.go | 61 +- app/admin/service/line_reverse_position.go | 172 ++++++ app/admin/service/line_user_setting.go | 62 +- pkg/retryhelper/retryhelper.go | 3 +- services/binanceservice/commonservice.go | 10 + services/binanceservice/futuresbinancerest.go | 43 ++ services/binanceservice/reverse_service.go | 580 ++++++++++++++---- services/excservice/binancesocketmanager.go | 88 ++- 16 files changed, 980 insertions(+), 209 deletions(-) diff --git a/app/admin/apis/line_api_user.go b/app/admin/apis/line_api_user.go index 0052ce1..c91ce4d 100644 --- a/app/admin/apis/line_api_user.go +++ b/app/admin/apis/line_api_user.go @@ -328,3 +328,24 @@ func (e LineApiUser) GetReverseApiOptions(c *gin.Context) { } e.OK(list, "操作成功") } + +// 获取所有启用的反单api用户 +func (e LineApiUser) GetReverseApiOptionsAll(c *gin.Context) { + s := service.LineApiUser{} + err := e.MakeContext(c). + MakeOrm(). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + list := make([]dto.LineApiUserOptionResp, 0) + err = s.GetReverseApiOptionsAll(&list) + if err != nil { + e.Error(500, err, fmt.Sprintf("获取失败,\r\n失败信息 %s", err.Error())) + return + } + e.OK(list, "操作成功") +} diff --git a/app/admin/apis/line_reverse_position.go b/app/admin/apis/line_reverse_position.go index f7636de..284a02e 100644 --- a/app/admin/apis/line_reverse_position.go +++ b/app/admin/apis/line_reverse_position.go @@ -2,6 +2,7 @@ package apis import ( "fmt" + "strings" "github.com/gin-gonic/gin" "github.com/go-admin-team/go-admin-core/sdk/api" @@ -194,3 +195,61 @@ func (e LineReversePosition) Delete(c *gin.Context) { } e.OK(req.GetId(), "删除成功") } + +// ClosePosition 平仓 +func (e LineReversePosition) ClosePosition(c *gin.Context) { + req := dto.LineReversePositionCloseReq{} + s := service.LineReversePosition{} + err := e.MakeContext(c). + MakeOrm(). + Bind(&req). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + userId := user.GetUserId(c) + p := actions.GetPermissionFromContext(c) + + err = s.ClosePosition(&req, p, userId) + if err != nil { + e.Error(500, err, fmt.Sprintf("平仓失败,\r\n失败信息 %s", err.Error())) + return + } + e.OK(nil, "平仓成功") +} + +// ClosePositionBatch 批量平仓 +func (e LineReversePosition) ClosePositionBatch(c *gin.Context) { + req := dto.LineReversePositionCloseBatchReq{} + s := service.LineReversePosition{} + err := e.MakeContext(c). + MakeOrm(). + Bind(&req). + MakeService(&s.Service). + Errors + if err != nil { + e.Logger.Error(err) + e.Error(500, err, err.Error()) + return + } + userId := user.GetUserId(c) + p := actions.GetPermissionFromContext(c) + errs := make([]string, 0) + + err = s.ClosePositionBatch(&req, p, userId, &errs) + if err != nil { + e.Error(500, err, fmt.Sprintf("批量平仓失败,\r\n失败信息 %s", err.Error())) + return + } + + if len(errs) > 0 { + content := strings.Join(errs, "
") + + e.OK(content, "批量平仓部分失败") + return + } + e.OK(nil, "批量平仓成功") +} diff --git a/app/admin/router/line_api_user.go b/app/admin/router/line_api_user.go index 2507b33..806fe5d 100644 --- a/app/admin/router/line_api_user.go +++ b/app/admin/router/line_api_user.go @@ -31,5 +31,7 @@ func registerLineApiUserRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMi r.GET("unbind-reverse", api.GetUnBindReverseApiUser) //获取未绑定下反单用户 r.GET("reverse-options", api.GetReverseApiOptions) //获取可用反单api用户 + + r.GET("reverse-options-all", api.GetReverseApiOptionsAll) //获取全部启用的反单api用户 } } diff --git a/app/admin/router/line_reverse_position.go b/app/admin/router/line_reverse_position.go index 87c81a5..0c14edd 100644 --- a/app/admin/router/line_reverse_position.go +++ b/app/admin/router/line_reverse_position.go @@ -5,8 +5,8 @@ import ( jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth" "go-admin/app/admin/apis" - "go-admin/common/middleware" "go-admin/common/actions" + "go-admin/common/middleware" ) func init() { @@ -23,5 +23,8 @@ func registerLineReversePositionRouter(v1 *gin.RouterGroup, authMiddleware *jwt. r.POST("", api.Insert) r.PUT("/:id", actions.PermissionAction(), api.Update) r.DELETE("", api.Delete) + + r.PUT("close/:id", actions.PermissionAction(), api.ClosePosition) + r.PUT("close-batch", actions.PermissionAction(), api.ClosePositionBatch) } -} \ No newline at end of file +} diff --git a/app/admin/service/dto/line_api_user.go b/app/admin/service/dto/line_api_user.go index 546c64d..9d99b0a 100644 --- a/app/admin/service/dto/line_api_user.go +++ b/app/admin/service/dto/line_api_user.go @@ -224,3 +224,8 @@ type GetReverseApiOptionsReq struct { Id int `json:"apiId" form:"id"` ApiId int `json:"apiId" form:"apiId"` } + +type LineApiUserOptionResp struct { + Id int `json:"id"` + Name string `json:"name"` +} diff --git a/app/admin/service/dto/line_reverse_order.go b/app/admin/service/dto/line_reverse_order.go index 0b34463..a8b6929 100644 --- a/app/admin/service/dto/line_reverse_order.go +++ b/app/admin/service/dto/line_reverse_order.go @@ -18,6 +18,8 @@ type LineReverseOrderGetPageReq struct { OrderType int `form:"orderType" search:"type:exact;column:order_type;table:line_reverse_order" comment:"订单类型 0-主单 1-止损单 2-加仓 3-减仓"` PositionSide string `form:"positionSide" search:"type:exact;column:position_side;table:line_reverse_order" comment:"持仓方向 LONG-多 SHORT-空"` Side string `form:"side" search:"type:exact;column:side;table:line_reverse_order" comment:"买卖方向 SELL-卖 BUY-买"` + Type string `form:"type" search:"type:exact;column:type;table:line_reverse_order" comment:"类型 LIMIT-限价 MARKET-市价 "` + Category int `form:"category" search:"-" comment:"类型 0-主单 1-反单"` Status int `form:"status" search:"type:exact;column:status;table:line_reverse_order" comment:"状态 1-待下单 2-已下单 3-已成交 4-已平仓 5-已止损"` PositionId int `form:"positionId" search:"type:exact;column:position_id;table:line_reverse_order" comment:"持仓id"` LineReverseOrderOrder diff --git a/app/admin/service/dto/line_reverse_position.go b/app/admin/service/dto/line_reverse_position.go index ee912e3..568f36e 100644 --- a/app/admin/service/dto/line_reverse_position.go +++ b/app/admin/service/dto/line_reverse_position.go @@ -133,3 +133,23 @@ type LineReversePositionListResp struct { ReverseAveragePrice decimal.Decimal `json:"reverseAveragePrice"` CreatedAt string `json:"createdAt"` } + +type LineReversePositionCloseReq struct { + PositionId int `uri:"id" form:"id" comment:"仓位id"` +} + +type LineReversePositionCloseBatchReq struct { + PositionSide string `json:"positionSide"` + Symbol string `json:"symbol"` + ReverseApiIds []int `json:"reverseApiIds" form:"reverseApiIds" comment:"反单api_id"` +} + +type GetPositionSymbolReq struct { + ReverseApiId int `form:"reverseApiId" comment:"反单api_id"` + PositionSide string `form:"positionSide" comment:"持仓方向 LONG SHORT"` +} + +type PositionSymbolResp struct { + Symbol string `json:"symbol"` + Code string `json:"code"` +} diff --git a/app/admin/service/line_api_user.go b/app/admin/service/line_api_user.go index 107afc9..3799914 100644 --- a/app/admin/service/line_api_user.go +++ b/app/admin/service/line_api_user.go @@ -26,6 +26,26 @@ type LineApiUser struct { service.Service } +// 获取所有启用的反单api用户 +func (e LineApiUser) GetReverseApiOptionsAll(user *[]dto.LineApiUserOptionResp) error { + var data models.LineApiUser + var datas []models.LineApiUser + + if err := e.Orm.Model(&data).Where("open_status = 1 AND subordinate = '2'").Find(&datas).Error; err != nil { + e.Log.Errorf("LineApiUserService GetReverseApiOptionsAll error:%s \r\n", err) + return err + } + + for _, item := range datas { + *user = append(*user, dto.LineApiUserOptionResp{ + Id: item.Id, + Name: item.ApiName, + }) + } + + return nil +} + // 获取可以绑定的api列表 func (e LineApiUser) GetReverseApiOptions(req *dto.GetReverseApiOptionsReq, user *[]models.LineApiUser) error { query := e.Orm.Model(models.LineApiUser{}). @@ -142,7 +162,10 @@ func (e *LineApiUser) Insert(c *dto.LineApiUserInsertReq) error { return err } - e.saveCache(data) + if err2 := e.CacheRelation(); err2 != nil { + return err2 + } + val, _ := sonic.MarshalString(&data) if val != "" { @@ -151,9 +174,6 @@ func (e *LineApiUser) Insert(c *dto.LineApiUserInsertReq) error { } } - if err2 := e.CacheRelation(); err2 != nil { - return err2 - } return nil } @@ -173,7 +193,7 @@ func (e *LineApiUser) restartWebsocket(data models.LineApiUser) { fuSocket.Stop() } - e.saveCache(data) + e.CacheRelation() OpenUserBinanceWebsocket(data) } @@ -195,24 +215,14 @@ func (e *LineApiUser) saveCache(data models.LineApiUser) { // cacheAll 是否缓存所有关系 func (e *LineApiUser) CacheRelation() error { var datas *[]models.LineApiUser - cacheStrs := make([]string, 0) - if err := e.Orm.Model(&models.LineApiUser{}).Where("subordinate ='1' and reverse_api_id >0 and open_status =1 and reverse_status =1").Find(&datas).Error; err != nil { + if err := e.Orm.Model(&models.LineApiUser{}).Where("open_status = 1").Find(&datas).Error; err != nil { return err } for _, data := range *datas { - cacheStrs = append(cacheStrs, fmt.Sprintf("%d:%d", data.Id, data.ReverseApiId)) - } - - if len(cacheStrs) > 0 { - if err := helper.DefaultRedis.SetListCache(rediskey.ApiReverseRelation, 0, cacheStrs...); err != nil { - e.Log.Errorf("设置缓存失败 err:%v", err) - } - } else { - if err := helper.DefaultRedis.DeleteString(rediskey.ApiReverseRelation); err != nil { - e.Log.Errorf("删除缓存失败 err:%v", err) - } + // cacheStrs = append(cacheStrs, fmt.Sprintf(rediskey.API_USER, data.Id)) + e.saveCache(data) } return nil @@ -323,7 +333,9 @@ func (e *LineApiUser) Update(c *dto.LineApiUserUpdateReq, p *actions.DataPermiss return err } - e.saveCache(data) + if err2 := e.CacheRelation(); err2 != nil { + return err2 + } //旧key和新的key不一样,则关闭旧的websocket if oldApiKey != data.ApiKey { @@ -340,10 +352,6 @@ func (e *LineApiUser) Update(c *dto.LineApiUserUpdateReq, p *actions.DataPermiss } } - if err2 := e.CacheRelation(); err2 != nil { - return err2 - } - return nil } diff --git a/app/admin/service/line_reverse_order.go b/app/admin/service/line_reverse_order.go index 615d595..2b32149 100644 --- a/app/admin/service/line_reverse_order.go +++ b/app/admin/service/line_reverse_order.go @@ -3,7 +3,7 @@ package service import ( "errors" - "github.com/go-admin-team/go-admin-core/sdk/service" + "github.com/go-admin-team/go-admin-core/sdk/service" "gorm.io/gorm" "go-admin/app/admin/models" @@ -20,13 +20,18 @@ type LineReverseOrder struct { func (e *LineReverseOrder) GetPage(c *dto.LineReverseOrderGetPageReq, p *actions.DataPermission, list *[]models.LineReverseOrder, count *int64) error { var err error var data models.LineReverseOrder - - err = e.Orm.Model(&data). + query := e.Orm.Model(&data). Scopes( cDto.MakeCondition(c.GetNeedSearch()), cDto.Paginate(c.GetPageSize(), c.GetPageIndex()), actions.Permission(data.TableName(), p), - ). + ) + + if c.Category >= 0 { + query = query.Where("category =?", c.Category) + } + + err = query. Find(list).Limit(-1).Offset(-1). Count(count).Error if err != nil { @@ -59,9 +64,9 @@ func (e *LineReverseOrder) Get(d *dto.LineReverseOrderGetReq, p *actions.DataPer // Insert 创建LineReverseOrder对象 func (e *LineReverseOrder) Insert(c *dto.LineReverseOrderInsertReq) error { - var err error - var data models.LineReverseOrder - c.Generate(&data) + var err error + var data models.LineReverseOrder + c.Generate(&data) err = e.Orm.Create(&data).Error if err != nil { e.Log.Errorf("LineReverseOrderService Insert error:%s \r\n", err) @@ -72,22 +77,22 @@ func (e *LineReverseOrder) Insert(c *dto.LineReverseOrderInsertReq) error { // Update 修改LineReverseOrder对象 func (e *LineReverseOrder) Update(c *dto.LineReverseOrderUpdateReq, p *actions.DataPermission) error { - var err error - var data = models.LineReverseOrder{} - e.Orm.Scopes( - actions.Permission(data.TableName(), p), - ).First(&data, c.GetId()) - c.Generate(&data) + var err error + var data = models.LineReverseOrder{} + e.Orm.Scopes( + actions.Permission(data.TableName(), p), + ).First(&data, c.GetId()) + c.Generate(&data) - db := e.Orm.Save(&data) - if err = db.Error; err != nil { - e.Log.Errorf("LineReverseOrderService Save error:%s \r\n", err) - return err - } - if db.RowsAffected == 0 { - return errors.New("无权更新该数据") - } - return nil + db := e.Orm.Save(&data) + if err = db.Error; err != nil { + e.Log.Errorf("LineReverseOrderService Save error:%s \r\n", err) + return err + } + if db.RowsAffected == 0 { + return errors.New("无权更新该数据") + } + return nil } // Remove 删除LineReverseOrder @@ -99,11 +104,11 @@ func (e *LineReverseOrder) Remove(d *dto.LineReverseOrderDeleteReq, p *actions.D actions.Permission(data.TableName(), p), ).Delete(&data, d.GetId()) if err := db.Error; err != nil { - e.Log.Errorf("Service RemoveLineReverseOrder error:%s \r\n", err) - return err - } - if db.RowsAffected == 0 { - return errors.New("无权删除该数据") - } + e.Log.Errorf("Service RemoveLineReverseOrder error:%s \r\n", err) + return err + } + if db.RowsAffected == 0 { + return errors.New("无权删除该数据") + } return nil } diff --git a/app/admin/service/line_reverse_position.go b/app/admin/service/line_reverse_position.go index 26f3104..b4b3b47 100644 --- a/app/admin/service/line_reverse_position.go +++ b/app/admin/service/line_reverse_position.go @@ -2,22 +2,194 @@ package service import ( "errors" + "fmt" + "time" "github.com/go-admin-team/go-admin-core/sdk/service" "github.com/jinzhu/copier" + "github.com/shopspring/decimal" "gorm.io/gorm" "go-admin/app/admin/models" "go-admin/app/admin/service/dto" "go-admin/common/actions" cDto "go-admin/common/dto" + "go-admin/common/global" "go-admin/pkg/utility" + "go-admin/pkg/utility/snowflakehelper" + "go-admin/services/binanceservice" + "go-admin/services/cacheservice" ) type LineReversePosition struct { service.Service } +// 批量关闭仓位 +func (e LineReversePosition) ClosePositionBatch(req *dto.LineReversePositionCloseBatchReq, p *actions.DataPermission, userId int, errs *[]string) error { + var positions []models.LineReversePosition + var entity models.LineReversePosition + query := e.Orm.Model(&entity). + Scopes( + actions.Permission(entity.TableName(), p), + ). + Where("reverse_status = 1 and position_side =?", req.PositionSide) + + if len(req.Symbol) > 0 { + query = query.Where("symbol =?", req.Symbol) + } + + if len(req.ReverseApiIds) > 0 { + query = query.Where("reverse_api_id in (?)", req.ReverseApiIds) + } + + if err := query.Find(&positions).Error; err != nil { + e.Log.Errorf("LineReversePositionService ClosePositionBatch error:%s \r\n", err) + return err + } + + if len(positions) == 0 { + return errors.New("没有需要关闭的仓位") + } + + for _, position := range positions { + if err1 := e.Close(position); err1 != nil { + *errs = append(*errs, err1.Error()) + } + } + + return nil +} + +// ClosePosition 关闭单个仓位 +func (e LineReversePosition) ClosePosition(req *dto.LineReversePositionCloseReq, p *actions.DataPermission, userId int) error { + var data models.LineReversePosition + err := e.Orm.Model(&data). + Scopes( + actions.Permission(data.TableName(), p), + ). + Where("id =?", req.PositionId).First(&data).Error + if err != nil { + e.Log.Errorf("LineReversePositionService ClosePosition error:%s \r\n", err) + return err + } + + err = e.Close(data) + + return err +} + +func (e *LineReversePosition) Close(data models.LineReversePosition) error { + if data.ReverseStatus != 1 { + return fmt.Errorf("%s-%s 仓位无法关闭", data.Symbol, data.PositionSide) + } + + apiInfo, err := binanceservice.GetApiInfo(data.ReverseApiId) + + if err != nil { + return fmt.Errorf("api %d不存在", data.ReverseApiId) + } + futApi := binanceservice.FutRestApi{} + futApiV2 := binanceservice.FuturesResetV2{} + holdData := binanceservice.HoldeData{} + err = futApi.GetPositionData(&apiInfo, data.Symbol, data.PositionSide, &holdData) + + if err != nil { + return fmt.Errorf("获取币安持仓失败 %v", err) + } + setting, err := cacheservice.GetReverseSetting(e.Orm) + + if err != nil { + return fmt.Errorf("获取反单设置失败") + } + + symbol, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, data.Symbol, 0) + + if err != nil { + return fmt.Errorf("获取%s的交易对信息失败", data.Symbol) + } + + lastPrice, err := decimal.NewFromString(symbol.LastPrice) + if err != nil { + return fmt.Errorf("最新价格失败,%v", lastPrice) + } + side := "" + var price decimal.Decimal + + if data.PositionSide == "LONG" { + side = "SELL" + price = decimal.NewFromInt(100).Sub(setting.ReversePremiumRatio).Div(decimal.NewFromInt(100)).Mul(lastPrice).Round(int32(symbol.PriceDigit)) + } else { + side = "BUY" + price = decimal.NewFromInt(100).Add(setting.ReversePremiumRatio).Div(decimal.NewFromInt(100)).Mul(lastPrice).Round(int32(symbol.PriceDigit)) + } + + now := time.Now() + order := models.LineReverseOrder{ + OrderSn: snowflakehelper.GetOrderNo(), + PositionId: data.Id, + PositionSide: data.PositionSide, + Symbol: data.Symbol, + TotalNum: holdData.TotalQuantity, + Category: 1, + OrderType: 4, + Side: side, + Price: price, + SignPrice: lastPrice, + Type: "LIMIT", + TriggerTime: &now, + Status: 1, + } + + if holdData.TotalQuantity.Cmp(decimal.Zero) > 0 { + if err := e.Orm.Create(&order).Error; err != nil { + return err + } + params := binanceservice.FutOrderPlace{ + ApiId: data.ReverseApiId, + Symbol: data.Symbol, + Side: order.Side, + PositionSide: order.PositionSide, + Quantity: order.TotalNum, + Price: order.Price, + SideType: order.Type, + OpenOrder: 0, + OrderType: "LIMIT", + NewClientOrderId: order.OrderSn, + } + + if err := futApiV2.OrderPlaceLoop(&apiInfo, params); err != nil { + e.Log.Errorf("币安下单失败 symbol:%s custom:%s :%v", params.Symbol, order.OrderSn, err) + if err1 := e.Orm.Model(&order).Where("status = 1").Updates(map[string]interface{}{"status": 8, "remark": err.Error(), "updated_at": time.Now()}).Error; err1 != nil { + e.Log.Errorf("更新订单状态失败 symbol:%s custom:%s :%v", params.Symbol, order.OrderSn, err1) + return err1 + } + + return err + } + } else { + order.Status = 8 + order.Remark = "已经没有持仓" + + err = e.Orm.Transaction(func(tx *gorm.DB) error { + if err1 := tx.Create(&order).Error; err1 != nil { + return err1 + } + + if err1 := tx.Model(&data).Where("reverse_status =1").Updates(map[string]interface{}{"reverse_status": 2, "updated_at": time.Now(), "reverse_amount": 0}).Error; err1 != nil { + return err1 + } + + return nil + }) + if err != nil { + e.Log.Errorf("修改失败 %v", err) + } + + } + return nil +} + // GetPage 获取LineReversePosition列表 func (e *LineReversePosition) GetPage(c *dto.LineReversePositionGetPageReq, p *actions.DataPermission, list *[]dto.LineReversePositionListResp, count *int64) error { var err error diff --git a/app/admin/service/line_user_setting.go b/app/admin/service/line_user_setting.go index a91b99b..f332323 100644 --- a/app/admin/service/line_user_setting.go +++ b/app/admin/service/line_user_setting.go @@ -3,7 +3,7 @@ package service import ( "errors" - "github.com/go-admin-team/go-admin-core/sdk/service" + "github.com/go-admin-team/go-admin-core/sdk/service" "gorm.io/gorm" "go-admin/app/admin/models" @@ -59,9 +59,9 @@ func (e *LineUserSetting) Get(d *dto.LineUserSettingGetReq, p *actions.DataPermi // Insert 创建LineUserSetting对象 func (e *LineUserSetting) Insert(c *dto.LineUserSettingInsertReq) error { - var err error - var data models.LineUserSetting - c.Generate(&data) + var err error + var data models.LineUserSetting + c.Generate(&data) err = e.Orm.Create(&data).Error if err != nil { e.Log.Errorf("LineUserSettingService Insert error:%s \r\n", err) @@ -72,22 +72,22 @@ func (e *LineUserSetting) Insert(c *dto.LineUserSettingInsertReq) error { // Update 修改LineUserSetting对象 func (e *LineUserSetting) Update(c *dto.LineUserSettingUpdateReq, p *actions.DataPermission) error { - var err error - var data = models.LineUserSetting{} - e.Orm.Scopes( - actions.Permission(data.TableName(), p), - ).First(&data, c.GetId()) - c.Generate(&data) + var err error + var data = models.LineUserSetting{} + e.Orm.Scopes( + actions.Permission(data.TableName(), p), + ).First(&data, c.GetId()) + c.Generate(&data) - db := e.Orm.Save(&data) - if err = db.Error; err != nil { - e.Log.Errorf("LineUserSettingService Save error:%s \r\n", err) - return err - } - if db.RowsAffected == 0 { - return errors.New("无权更新该数据") - } - return nil + db := e.Orm.Save(&data) + if err = db.Error; err != nil { + e.Log.Errorf("LineUserSettingService Save error:%s \r\n", err) + return err + } + if db.RowsAffected == 0 { + return errors.New("无权更新该数据") + } + return nil } // Remove 删除LineUserSetting @@ -99,11 +99,23 @@ func (e *LineUserSetting) Remove(d *dto.LineUserSettingDeleteReq, p *actions.Dat actions.Permission(data.TableName(), p), ).Delete(&data, d.GetId()) if err := db.Error; err != nil { - e.Log.Errorf("Service RemoveLineUserSetting error:%s \r\n", err) - return err - } - if db.RowsAffected == 0 { - return errors.New("无权删除该数据") - } + e.Log.Errorf("Service RemoveLineUserSetting error:%s \r\n", err) + return err + } + if db.RowsAffected == 0 { + return errors.New("无权删除该数据") + } return nil } + +// GetDefault 获取默认LineUserSetting对象 +func (e *LineUserSetting) GetDefault() (models.LineUserSetting, error) { + var data models.LineUserSetting + + if err := e.Orm.Model(&data).First(&data).Error; err != nil { + e.Log.Errorf("GetDefault LineUserSetting error:%s \r\n", err) + return data, err + } + + return data, nil +} diff --git a/pkg/retryhelper/retryhelper.go b/pkg/retryhelper/retryhelper.go index 170757d..591fb40 100644 --- a/pkg/retryhelper/retryhelper.go +++ b/pkg/retryhelper/retryhelper.go @@ -1,7 +1,6 @@ package retryhelper import ( - "fmt" "math" "time" ) @@ -50,5 +49,5 @@ func RetryWithResult[T any](op func() (T, error), opts RetryOptions) (result T, interval = time.Duration(math.Min(float64(opts.MaxInterval), float64(interval)*opts.BackoffFactor)) } } - return result, fmt.Errorf("retry failed after %d attempts, last error: %w", opts.MaxRetries+1, err) + return result, err } diff --git a/services/binanceservice/commonservice.go b/services/binanceservice/commonservice.go index 3937550..74c651d 100644 --- a/services/binanceservice/commonservice.go +++ b/services/binanceservice/commonservice.go @@ -479,3 +479,13 @@ func GetOpenOrderSns(db *gorm.DB, mainIds []int) ([]string, error) { return result, nil } + +// 回去反单默认配置 +func GetReverseSetting(db *gorm.DB) (DbModels.LineReverseSetting, error) { + var setting DbModels.LineReverseSetting + if err := db.Model(&DbModels.LineReverseSetting{}).First(&setting).Error; err != nil { + return setting, err + } + + return setting, nil +} diff --git a/services/binanceservice/futuresbinancerest.go b/services/binanceservice/futuresbinancerest.go index b880a22..6aab258 100644 --- a/services/binanceservice/futuresbinancerest.go +++ b/services/binanceservice/futuresbinancerest.go @@ -599,6 +599,49 @@ func (e FutRestApi) GetHoldeData(apiInfo *DbModels.LineApiUser, symbol, side str return nil } +// 获取合约 持仓价格、数量 +// symbol:交易对 +// positionSide:持仓方向 +// holdeData:持仓数据 +func (e FutRestApi) GetPositionData(apiInfo *DbModels.LineApiUser, symbol, positionSide string, holdeData *HoldeData) error { + opts := retryhelper.DefaultRetryOptions() + opts.RetryableErrFn = func(err error) bool { + if strings.Contains(err.Error(), "LOT_SIZE") { + return false + } + //重试 + return true + } + + holdes, err := retryhelper.RetryWithResult(func() ([]PositionRisk, error) { + return e.GetPositionV3(apiInfo, symbol) + }, opts) + + if err != nil { + return err + } + + for _, item := range holdes { + positionAmount, _ := decimal.NewFromString(item.PositionAmt) + if (positionSide == "LONG" && item.PositionSide == "BOTH" && positionAmount.Cmp(decimal.Zero) > 0) || item.PositionSide == positionSide { //多 + holdeData.AveragePrice, _ = decimal.NewFromString(item.EntryPrice) + holdeData.TotalQuantity = positionAmount.Abs() + continue + } else if (positionSide == "SHORT" && item.PositionSide == "BOTH" && positionAmount.Cmp(decimal.Zero) < 0) || item.PositionSide == positionSide { //空 + holdeData.AveragePrice, _ = decimal.NewFromString(item.EntryPrice) + holdeData.TotalQuantity = positionAmount.Abs() + continue + } + } + + if holdeData.AveragePrice.Cmp(decimal.Zero) == 0 { + holdesVal, _ := sonic.MarshalString(&holdes) + log.Error("均价错误 symbol:", symbol, " 数据:", holdesVal) + } + + return nil +} + // 获取代币持仓信息 func getSymbolHolde(e FutRestApi, apiInfo *DbModels.LineApiUser, symbol string, side string, holdeData *HoldeData) ([]PositionRisk, error) { holdes, err := e.GetPositionV3(apiInfo, symbol) diff --git a/services/binanceservice/reverse_service.go b/services/binanceservice/reverse_service.go index 2c8bbf9..881e004 100644 --- a/services/binanceservice/reverse_service.go +++ b/services/binanceservice/reverse_service.go @@ -14,6 +14,7 @@ import ( "strconv" "time" + "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" @@ -108,26 +109,36 @@ func (e *ReverseService) ReverseOrder(apiKey string, mapData map[string]interfac switch { case mainOrder.PositionSide == "LONG" && mainOrder.Side == "BUY", mainOrder.PositionSide == "SHORT" && mainOrder.Side == "SELL": if mainOrder.Category == 0 { - if err1 := e.savePosition(&mainOrder, reverseApiInfo.Id, true, false, false); err1 != nil { + needReverseOrder, closePosition, err1 := e.savePosition(&mainOrder, &apiInfo, true, false) + + if err1 != nil { return true, err1 } - e.DoAddReverseOrder(&mainOrder, &reverseApiInfo, apiInfo.OrderProportion, false, false) + if needReverseOrder { + e.DoAddReverseOrder(&mainOrder, &reverseApiInfo, apiInfo.OrderProportion, false, closePosition) + } } case mainOrder.PositionSide == "SHORT" && mainOrder.Side == "BUY", mainOrder.PositionSide == "LONG" && mainOrder.Side == "SELL": if mainOrder.Category == 0 { - closePosition := maphelper.GetBool(mapData, "R") - if err1 := e.savePosition(&mainOrder, reverseApiInfo.Id, true, true, closePosition); err1 != nil { + needReverseOrder, closePosition, err1 := e.savePosition(&mainOrder, &apiInfo, true, true) + + if err1 != nil { e.Log.Errorf("保存主订单失败: %v", err1) return true, err1 } - e.DoAddReverseOrder(&mainOrder, &reverseApiInfo, apiInfo.OrderProportion, true, closePosition) + + if needReverseOrder { + e.DoAddReverseOrder(&mainOrder, &reverseApiInfo, apiInfo.OrderProportion, true, closePosition) + } } default: return true, errors.New("不支持的订单类型") } return true, nil } else if apiInfo.Subordinate == "2" { + e.changeOrderStatus(3, orderSn, mapData) + symbol, err := maphelper.GetString(mapData, "s") if err != nil { @@ -153,20 +164,19 @@ func (e *ReverseService) ReverseOrder(apiKey string, mapData map[string]interfac TotalNum: totalNum, Side: side, Price: price, + FinalPrice: price, } - e.changeOrderStatus(3, orderSn, mapData) switch { case mainOrder.PositionSide == "LONG" && mainOrder.Side == "BUY", mainOrder.PositionSide == "SHORT" && mainOrder.Side == "SELL": if mainOrder.Category == 0 { - if err1 := e.savePosition(&mainOrder, 0, false, false, false); err1 != nil { + if _, _, err1 := e.savePosition(&mainOrder, &apiInfo, false, false); err1 != nil { return true, err1 } } - case mainOrder.PositionSide == "SHORT" && mainOrder.Side == "BUY", mainOrder.PositionSide == "LONG" && mainOrder.Side == "SELL": + case mainOrder.PositionSide == "SHORT" && mainOrder.Side == "BUY", mainOrder.PositionSide == "LONG" && mainOrder.Side == "SrgetELL": if mainOrder.Category == 0 { - closePosition := maphelper.GetBool(mapData, "R") - if err1 := e.savePosition(&mainOrder, 0, false, true, closePosition); err1 != nil { + if _, _, err1 := e.savePosition(&mainOrder, &apiInfo, false, true); err1 != nil { return true, err1 } } @@ -298,17 +308,23 @@ func (e *ReverseService) SaveMainOrder(mapData map[string]interface{}, apiInfo D } // 更新仓位信息 +// apiInfo: 当前下单api信息 +// return +// neeReverseOrder: 是否需要反单 // closePosition: true=平仓, false=减仓 -func (e *ReverseService) savePosition(reverseOrder *DbModels.LineReverseOrder, reverseApiId int, isMain, reducePosition, closePosition bool) error { +// err error 错误信息 +func (e *ReverseService) savePosition(order *DbModels.LineReverseOrder, apiInfo *DbModels.LineApiUser, isMain, reducePosition bool) (bool, bool, error) { position := DbModels.LineReversePosition{} - positionSide := reverseOrder.PositionSide - side := reverseOrder.Side - totalNum := reverseOrder.TotalNum + positionSide := order.PositionSide + side := order.Side + totalNum := order.TotalNum + closePosition := false + needReverseOrder := false - symbol, err1 := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reverseOrder.Symbol, 1) + symbol, err1 := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1) if err1 != nil { - e.Log.Errorf("获取交易对失败 symbol:%s err:%v", reverseOrder.Symbol, err1) + e.Log.Errorf("获取交易对失败 symbol:%s err:%v", order.Symbol, err1) } var querySql string @@ -316,12 +332,15 @@ func (e *ReverseService) savePosition(reverseOrder *DbModels.LineReverseOrder, r //如果是主单,存储仓位则是反单的持仓方向 if isMain { - if reverseOrder.PositionSide == "LONG" { + if order.PositionSide == "LONG" { positionSide = "SHORT" } else { positionSide = "LONG" } + //减仓 判断是否为平仓 + closePosition = e.getClosePosition(reducePosition, apiInfo, order, order.PositionSide, isMain) + if !reducePosition { //反单止盈止损方向相反 if side == "SELL" { @@ -330,14 +349,14 @@ func (e *ReverseService) savePosition(reverseOrder *DbModels.LineReverseOrder, r side = "SELL" } - position.ReverseApiId = reverseApiId + position.ReverseApiId = apiInfo.ReverseApiId position.Side = side - position.ApiId = reverseOrder.ApiId - position.Symbol = reverseOrder.Symbol + position.ApiId = order.ApiId + position.Symbol = order.Symbol position.Status = 1 position.ReverseStatus = 0 position.PositionSide = positionSide - position.AveragePrice = reverseOrder.FinalPrice + position.AveragePrice = order.FinalPrice position.PositionNo = snowflakehelper.GetOrderNo() } querySql = "api_id =? and position_side =? and symbol =? and status =1" @@ -354,6 +373,8 @@ func (e *ReverseService) savePosition(reverseOrder *DbModels.LineReverseOrder, r } } else { querySql = "reverse_api_id =? and position_side =? and symbol =? and reverse_status in (0,1)" + //减仓 判断是否为平仓 + closePosition = e.getClosePosition(reducePosition, apiInfo, order, order.PositionSide, isMain) if closePosition { totalNum = decimal.Zero @@ -366,10 +387,11 @@ func (e *ReverseService) savePosition(reverseOrder *DbModels.LineReverseOrder, r } var averagePrice decimal.Decimal + var remainQuantity decimal.Decimal err := e.Orm.Transaction(func(tx *gorm.DB) error { err1 := tx.Model(&position).Where(querySql, - reverseOrder.ApiId, positionSide, reverseOrder.Symbol).First(&position).Error + order.ApiId, positionSide, order.Symbol).First(&position).Error if err1 != nil { //主单仓位不存在,创建新仓位 @@ -397,12 +419,12 @@ func (e *ReverseService) savePosition(reverseOrder *DbModels.LineReverseOrder, r //加仓 if !reducePosition { - totalPrice = totalPrice.Add(reverseOrder.Price.Mul(reverseOrder.TotalNum)) - totalAmount = totalAmount.Add(reverseOrder.TotalNum) + totalPrice = totalPrice.Add(order.Price.Mul(order.TotalNum)) + totalAmount = totalAmount.Add(order.TotalNum) } else if reducePosition && !closePosition { //只减仓 - totalPrice = totalPrice.Sub(reverseOrder.Price.Mul(reverseOrder.TotalNum)) - totalAmount = totalAmount.Sub(reverseOrder.TotalNum) + totalPrice = totalPrice.Sub(order.Price.Mul(order.TotalNum)) + totalAmount = totalAmount.Sub(order.TotalNum) } } @@ -411,7 +433,7 @@ func (e *ReverseService) savePosition(reverseOrder *DbModels.LineReverseOrder, r averagePrice = position.AveragePrice } else { if position.ReverseAveragePrice.IsZero() { - averagePrice = reverseOrder.Price + averagePrice = order.Price } else { averagePrice = position.ReverseAveragePrice } @@ -431,7 +453,7 @@ func (e *ReverseService) savePosition(reverseOrder *DbModels.LineReverseOrder, r } //关联订单的仓位id - if err2 := tx.Exec("UPDATE line_reverse_order set position_id=@positionId where id=@orderId and position_id = 0", sql.Named("positionId", position.Id), sql.Named("orderId", reverseOrder.Id)).Error; err2 != nil { + if err2 := tx.Exec("UPDATE line_reverse_order set position_id=@positionId where id=@orderId and position_id = 0", sql.Named("positionId", position.Id), sql.Named("orderId", order.Id)).Error; err2 != nil { return err2 } @@ -440,16 +462,63 @@ func (e *ReverseService) savePosition(reverseOrder *DbModels.LineReverseOrder, r return dbResult.Error } - reverseOrder.PositionId = position.Id + order.PositionId = position.Id if dbResult.RowsAffected == 0 { - e.Log.Errorf("减仓数据 是否平仓单:%v :%v", closePosition, reverseOrder) + e.Log.Errorf("减仓数据 是否平仓单:%v :%v", closePosition, order) return errors.New("没有找到对应的持仓信息") } + if reducePosition && !isMain { + remainQuantity = position.ReverseAmount.Sub(totalNum) + } else if !isMain { + remainQuantity = position.ReverseAmount.Add(totalNum) + } + + //主单且对手单没有平仓 + if isMain && position.ReverseStatus != 2 { + needReverseOrder = true + } + return nil }) - return err + if !isMain && !closePosition { + e.doDefaultTakeStop(order, apiInfo, remainQuantity) + } else if !isMain && closePosition { + //取消剩余的委托 + e.DoCancelTakeAndStop(order.Symbol, position.PositionSide, apiInfo) + } + + return needReverseOrder, closePosition, err +} + +// 获取是否为平仓状态 +func (e *ReverseService) getClosePosition(reducePosition bool, apiInfo *DbModels.LineApiUser, order *DbModels.LineReverseOrder, positionSide string, isMain bool) bool { + closePosition := false + + if reducePosition { + futApi := FutRestApi{} + holdData := HoldeData{} + err := futApi.GetPositionData(apiInfo, order.Symbol, positionSide, &holdData) + + if err != nil { + e.Log.Errorf("获取剩余持仓信息失败 symbol:%s err:%v", order.Symbol, err) + lastPosition := DbModels.LineReversePosition{} + + if err2 := e.Orm.Model(&DbModels.LineReversePosition{}).Where("position_side =? and symbol =? and status =1", positionSide, order.Symbol).First(&lastPosition).Error; err2 != nil { + e.Log.Errorf("获取上一次持仓信息失败 symbol:%s err:%v", order.Symbol, err2) + } else if isMain && lastPosition.Amount.Cmp(order.TotalNum) <= 0 { + //如果剩余仓位小于等于 + closePosition = true + } else if !isMain && lastPosition.ReverseAmount.Cmp(order.TotalNum) <= 0 { + closePosition = true + } + } else if holdData.TotalQuantity.IsZero() { + closePosition = true + } + } + + return closePosition } // 反向下单 @@ -556,8 +625,11 @@ func (e *ReverseService) DoAddReverseOrder(mainOrder *DbModels.LineReverseOrder, //反向下单百分比 proportion = proportion.Div(decimal.NewFromInt(100)).Truncate(4) + amount = mainOrder.TotalNum.Mul(proportion).Truncate(int32(symbol.AmountDigit)) + logger.Info("反向下单比例 %d ,原始数量:%d,反向下单数量:%d", proportion, mainOrder.TotalNum, amount) + if amount.Cmp(decimal.Zero) <= 0 { e.Log.Errorf("计算数量失败 symbol:%s custom:%s 数量小于0", mainOrder.Symbol, mainOrder.OrderSn) return errors.New("计算数量失败") @@ -620,6 +692,24 @@ func (e *ReverseService) DoCancelTakeProfitBatch(symbol, positionSide, side stri return nil } +// 取消止盈和止损单 +func (e *ReverseService) DoCancelTakeAndStop(symbol, positionSide string, apiInfo *DbModels.LineApiUser) error { + var orderSns []string + + e.Orm.Model(&DbModels.LineReverseOrder{}).Where("symbol =? and position_side =? and status =2", symbol, positionSide).Pluck("order_sn", &orderSns) + + if len(orderSns) > 0 { + futApi := FutRestApi{} + + if err := futApi.CancelBatchFutOrderLoop(*apiInfo, symbol, orderSns); err != nil { + e.Log.Errorf("币安撤单失败 symbol:%s custom:%v :%v", symbol, orderSns, err) + return err + } + } + + return nil +} + // 获取止盈止损订单 // symbol: 交易对 // positionSide: 持仓方向 @@ -681,41 +771,92 @@ func (e *ReverseService) DoBianceOrder(order *DbModels.LineReverseOrder, apiInfo return nil } +// 处理默认止盈止损 +// order: 订单信息 +// apiInfo: api信息 +// Optimized Reverse Order Handling +// File: reverse_order_handler.go + +func (e *ReverseService) doDefaultTakeStop(order *DbModels.LineReverseOrder, apiInfo *DbModels.LineApiUser, totalNum decimal.Decimal) error { + if totalNum.LessThanOrEqual(decimal.Zero) { + return nil + } + + orders, err := e.getActiveReverseOrders(order.PositionId) + if err != nil { + return err + } + if len(orders) == 2 { + return nil + } + + setting, err := GetReverseSetting(e.Orm) + if err != nil { + e.Log.Errorf("获取反单设置失败:%v", err) + return err + } + + symbol, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1) + if err != nil { + e.Log.Errorf("获取交易对信息失败:%v", err) + return err + } + + side := e.getOppositeSide(order.Side) + lastPrice, _ := decimal.NewFromString(symbol.LastPrice) + now := time.Now() + + types := []struct { + Enabled bool + CheckTypes []string + OrderType string + Ratio decimal.Decimal + IsTakeProfit bool + }{ + {!setting.TakeProfitRatio.IsZero(), []string{"TAKE_PROFIT_MARKET", "TAKE_PROFIT"}, "TAKE_PROFIT_MARKET", setting.TakeProfitRatio, true}, + {!setting.StopLossRatio.IsZero(), []string{"STOP_MARKET", "STOP"}, "STOP_MARKET", setting.StopLossRatio, false}, + } + + for _, t := range types { + if t.Enabled && !e.hasOrderType(orders, t.CheckTypes...) { + price := e.calculatePrice(order.PositionSide, order.FinalPrice, t.Ratio, t.IsTakeProfit) + err := e.createReverseOrder(CreateOrderParams{ + Order: order, + ApiInfo: apiInfo, + Symbol: &symbol, + Side: side, + OrderType: t.OrderType, + Price: price, + TotalNum: totalNum, + Now: now, + LastPrice: lastPrice, + Close: true, + PositionId: order.PositionId, + }) + if err != nil { + e.Log.Errorf("止盈止损下单失败:%v", err) + } + } + } + + return nil +} + // 重下止盈止损 -// mapData: 主单止盈止损回调 +// mapData: func (e *ReverseService) ReTakeOrStopOrder(mapData *map[string]interface{}, orderSn string, mainApiInfo *DbModels.LineApiUser, symbol *models.TradeSet) error { - orderType := 0 //订单类型 1-止盈 2-止损 side, err := maphelper.GetString(*mapData, "S") - if err != nil { return err } - ot, err := maphelper.GetString(*mapData, "ot") - if err != nil { return err } - - //反单止盈止损方向相反 - if side == "SELL" { - side = "BUY" - } else { - side = "SELL" - } - positionSide, err := maphelper.GetString(*mapData, "ps") - if err != nil { return err } - - if positionSide == "LONG" { - positionSide = "SHORT" - } else { - positionSide = "LONG" - } - close := maphelper.GetBool(*mapData, "cp") stopPrice := maphelper.GetDecimal(*mapData, "sp") if stopPrice.IsZero() { @@ -723,12 +864,19 @@ func (e *ReverseService) ReTakeOrStopOrder(mapData *map[string]interface{}, orde return err } apiInfo, err := GetApiInfo(mainApiInfo.ReverseApiId) - if err != nil { e.Log.Errorf("根据主单api获取反单api失败 symbol:%s custom:%s :%v", symbol, orderSn, err) return err } + side = e.getOppositeSide(side) + if positionSide == "LONG" { + positionSide = "SHORT" + } else { + positionSide = "LONG" + } + + var orderType int switch ot { case "STOP_MARKET", "STOP": orderType = 2 @@ -739,26 +887,19 @@ func (e *ReverseService) ReTakeOrStopOrder(mapData *map[string]interface{}, orde } var reversePosition DbModels.LineReversePosition - e.Orm.Model(&reversePosition). Where("symbol =? and reverse_api_id =? and position_side =? and reverse_status =1", symbol.GetSymbol(), apiInfo.Id, positionSide). First(&reversePosition) - if reversePosition.Id == 0 { e.Log.Errorf("获取反单持仓失败 symbol:%s custom:%s :%v", symbol, orderSn, err) return err } - mainPercent := decimal.NewFromInt(1) - - if !stopPrice.IsZero() && !reversePosition.AveragePrice.IsZero() { - mainPercent = stopPrice.Div(reversePosition.AveragePrice) - mainPercent = (mainPercent.Sub(decimal.NewFromInt(1))).Abs().Truncate(4) - } + mainPercent := stopPrice.Div(reversePosition.AveragePrice) + mainPercent = (mainPercent.Sub(decimal.NewFromInt(1))).Abs().Truncate(4) var percent decimal.Decimal switch { - //做多止损 case orderType == 2 && positionSide == "LONG", orderType == 1 && positionSide == "SHORT": percent = decimal.NewFromInt(1).Sub(mainPercent) case orderType == 2 && positionSide == "SHORT", orderType == 1 && positionSide == "LONG": @@ -770,55 +911,286 @@ func (e *ReverseService) ReTakeOrStopOrder(mapData *map[string]interface{}, orde now := time.Now() price := reversePosition.AveragePrice.Mul(percent).Truncate(int32(symbol.PriceDigit)) lastPrice, _ := decimal.NewFromString(symbol.LastPrice) - newOrder := DbModels.LineReverseOrder{ - PositionId: reversePosition.Id, - OrderSn: helper.GetOrderNo(), - OrderType: orderType, - Status: 1, - Price: price, - TotalNum: reversePosition.TotalReverseAmount, - Symbol: symbol.GetSymbol(), - Side: side, - PositionSide: positionSide, - FollowOrderSn: orderSn, - Type: ot, - SignPrice: lastPrice, - Category: 1, - ApiId: apiInfo.Id, - IsAddPosition: 2, - TriggerTime: &now, - BuyPrice: reversePosition.TotalReverseAmount.Mul(price).Truncate(int32(symbol.PriceDigit)), - } - if err1 := e.Orm.Create(&newOrder).Error; err1 != nil { - e.Log.Errorf("保存反单止盈止损失败 symbol:%s custom:%s :%v", symbol, orderSn, err1) - return err1 - } - params := FutOrderPlace{ - ApiId: apiInfo.Id, - Symbol: symbol.GetSymbol(), - PositionSide: positionSide, - Side: side, - OrderType: ot, - Quantity: reversePosition.TotalReverseAmount, - Price: price, - StopPrice: price, - Profit: price, - NewClientOrderId: newOrder.OrderSn, - ClosePosition: close, - } - futApiV2 := FuturesResetV2{Service: e.Service} - err = futApiV2.OrderPlaceLoop(&apiInfo, params) + return e.createReverseOrder(CreateOrderParams{ + ApiInfo: &apiInfo, + Symbol: symbol, + Side: side, + OrderType: ot, + Price: price, + TotalNum: reversePosition.TotalReverseAmount, + Now: now, + LastPrice: lastPrice, + Close: close, + PositionId: reversePosition.Id, + OrderSn: orderSn, + PositionSide: positionSide, + }) +} +func (e *ReverseService) getActiveReverseOrders(positionId int) ([]DbModels.LineReverseOrder, error) { + var orders []DbModels.LineReverseOrder + err := e.Orm.Model(&DbModels.LineReverseOrder{}). + Where("position_id =? and status =2", positionId). + Select("id,position_side,side,type").Find(&orders).Error if err != nil { - e.Log.Errorf("币安下单失败 symbol:%s custom:%s :%v", symbol.GetSymbol(), orderSn, err) + e.Log.Errorf("获取订单信息失败:%v", err) + } + return orders, err +} - if err1 := e.Orm.Model(&newOrder).Updates(map[string]interface{}{"status": 8, "remark": err.Error(), "updated_at": time.Now()}).Error; err1 != nil { - e.Log.Errorf("更新订单状态失败 symbol:%s custom:%s :%v", newOrder.Symbol, newOrder.OrderSn, err1) +func (e *ReverseService) getOppositeSide(side string) string { + if side == "SELL" { + return "BUY" + } + return "SELL" +} + +func (e *ReverseService) hasOrderType(orders []DbModels.LineReverseOrder, types ...string) bool { + typeSet := make(map[string]bool) + for _, t := range types { + typeSet[t] = true + } + for _, o := range orders { + if typeSet[o.Type] { + return true } + } + return false +} + +func (e *ReverseService) calculatePrice(positionSide string, base decimal.Decimal, ratio decimal.Decimal, isTakeProfit bool) decimal.Decimal { + adjust := decimal.NewFromInt(100) + if positionSide == "LONG" { + if isTakeProfit { + return base.Mul(adjust.Add(ratio).Div(adjust)).Truncate(4) + } + return base.Mul(adjust.Sub(ratio).Div(adjust)).Truncate(4) + } + if isTakeProfit { + return base.Mul(adjust.Sub(ratio).Div(adjust)).Truncate(4) + } + return base.Mul(adjust.Add(ratio).Div(adjust)).Truncate(4) +} + +type CreateOrderParams struct { + ApiInfo *DbModels.LineApiUser + Order *DbModels.LineReverseOrder + Symbol *models.TradeSet + Side string + OrderType string + Price decimal.Decimal + TotalNum decimal.Decimal + Now time.Time + LastPrice decimal.Decimal + Close bool + PositionId int + OrderSn string + PositionSide string +} + +func (e *ReverseService) createReverseOrder(params CreateOrderParams) error { + orderSn := params.OrderSn + if orderSn == "" { + orderSn = helper.GetOrderNo() + } + if params.PositionSide == "" && params.Order != nil { + params.PositionSide = params.Order.PositionSide + } + newOrder := DbModels.LineReverseOrder{ + PositionId: params.PositionId, + OrderSn: orderSn, + OrderType: getOrderType(params.OrderType), + Status: 1, + Price: params.Price, + TotalNum: params.TotalNum, + Symbol: params.Symbol.GetSymbol(), + Side: params.Side, + PositionSide: params.PositionSide, + FollowOrderSn: params.OrderSn, + Type: params.OrderType, + SignPrice: params.LastPrice, + Category: 1, + ApiId: params.ApiInfo.Id, + IsAddPosition: 2, + TriggerTime: ¶ms.Now, + BuyPrice: params.TotalNum.Mul(params.Price).Truncate(int32(params.Symbol.PriceDigit)), + } + + if err := e.Orm.Create(&newOrder).Error; err != nil { + e.Log.Errorf("保存反单止盈止损失败 symbol:%s custom:%s :%v", params.Symbol.GetSymbol(), newOrder.OrderSn, err) return err } - e.DoCancelTakeProfitBatch(symbol.GetSymbol(), positionSide, side, orderType, &apiInfo) + futApiV2 := FuturesResetV2{Service: e.Service} + params2 := FutOrderPlace{ + ApiId: params.ApiInfo.Id, + Symbol: params.Symbol.GetSymbol(), + PositionSide: newOrder.PositionSide, + Side: newOrder.Side, + OrderType: newOrder.Type, + Quantity: newOrder.TotalNum, + Price: newOrder.Price, + StopPrice: newOrder.Price, + Profit: newOrder.Price, + NewClientOrderId: newOrder.OrderSn, + ClosePosition: params.Close, + } + err := futApiV2.OrderPlaceLoop(params.ApiInfo, params2) + if err != nil { + e.Log.Errorf("币安下单失败 symbol:%s custom:%s :%v", params.Symbol.GetSymbol(), newOrder.OrderSn, err) + e.Orm.Model(&newOrder).Updates(map[string]interface{}{"status": 8, "remark": err.Error(), "updated_at": time.Now()}) + return err + } + e.DoCancelTakeProfitBatch(params.Symbol.GetSymbol(), newOrder.PositionSide, newOrder.Side, newOrder.OrderType, params.ApiInfo) return nil } + +func getOrderType(t string) int { + if t == "TAKE_PROFIT_MARKET" || t == "TAKE_PROFIT" { + return 1 + } + return 2 +} + +// // 重下止盈止损 +// // mapData: 主单止盈止损回调 +// func (e *ReverseService) ReTakeOrStopOrder(mapData *map[string]interface{}, orderSn string, mainApiInfo *DbModels.LineApiUser, symbol *models.TradeSet) error { +// orderType := 0 //订单类型 1-止盈 2-止损 +// side, err := maphelper.GetString(*mapData, "S") + +// if err != nil { +// return err +// } + +// ot, err := maphelper.GetString(*mapData, "ot") + +// if err != nil { +// return err +// } + +// //反单止盈止损方向相反 +// if side == "SELL" { +// side = "BUY" +// } else { +// side = "SELL" +// } + +// positionSide, err := maphelper.GetString(*mapData, "ps") + +// if err != nil { +// return err +// } + +// if positionSide == "LONG" { +// positionSide = "SHORT" +// } else { +// positionSide = "LONG" +// } + +// close := maphelper.GetBool(*mapData, "cp") +// stopPrice := maphelper.GetDecimal(*mapData, "sp") +// if stopPrice.IsZero() { +// e.Log.Errorf("获取止盈止损单触发价失败 symbol:%s custom:%s :%v", symbol, orderSn, err) +// return err +// } +// apiInfo, err := GetApiInfo(mainApiInfo.ReverseApiId) + +// if err != nil { +// e.Log.Errorf("根据主单api获取反单api失败 symbol:%s custom:%s :%v", symbol, orderSn, err) +// return err +// } + +// switch ot { +// case "STOP_MARKET", "STOP": +// orderType = 2 +// case "TAKE_PROFIT_MARKET", "TAKE_PROFIT": +// orderType = 1 +// default: +// return fmt.Errorf("不支持的订单类型 ot:%s", ot) +// } + +// var reversePosition DbModels.LineReversePosition + +// e.Orm.Model(&reversePosition). +// Where("symbol =? and reverse_api_id =? and position_side =? and reverse_status =1", symbol.GetSymbol(), apiInfo.Id, positionSide). +// First(&reversePosition) + +// if reversePosition.Id == 0 { +// e.Log.Errorf("获取反单持仓失败 symbol:%s custom:%s :%v", symbol, orderSn, err) +// return err +// } + +// mainPercent := decimal.NewFromInt(1) + +// if !stopPrice.IsZero() && !reversePosition.AveragePrice.IsZero() { +// mainPercent = stopPrice.Div(reversePosition.AveragePrice) +// mainPercent = (mainPercent.Sub(decimal.NewFromInt(1))).Abs().Truncate(4) +// } + +// var percent decimal.Decimal +// switch { +// //做多止损 +// case orderType == 2 && positionSide == "LONG", orderType == 1 && positionSide == "SHORT": +// percent = decimal.NewFromInt(1).Sub(mainPercent) +// case orderType == 2 && positionSide == "SHORT", orderType == 1 && positionSide == "LONG": +// percent = decimal.NewFromInt(1).Add(mainPercent) +// default: +// return fmt.Errorf("不支持的订单类型 ot:%s, ps:%s", ot, positionSide) +// } + +// now := time.Now() +// price := reversePosition.AveragePrice.Mul(percent).Truncate(int32(symbol.PriceDigit)) +// lastPrice, _ := decimal.NewFromString(symbol.LastPrice) +// newOrder := DbModels.LineReverseOrder{ +// PositionId: reversePosition.Id, +// OrderSn: helper.GetOrderNo(), +// OrderType: orderType, +// Status: 1, +// Price: price, +// TotalNum: reversePosition.TotalReverseAmount, +// Symbol: symbol.GetSymbol(), +// Side: side, +// PositionSide: positionSide, +// FollowOrderSn: orderSn, +// Type: ot, +// SignPrice: lastPrice, +// Category: 1, +// ApiId: apiInfo.Id, +// IsAddPosition: 2, +// TriggerTime: &now, +// BuyPrice: reversePosition.TotalReverseAmount.Mul(price).Truncate(int32(symbol.PriceDigit)), +// } + +// if err1 := e.Orm.Create(&newOrder).Error; err1 != nil { +// e.Log.Errorf("保存反单止盈止损失败 symbol:%s custom:%s :%v", symbol, orderSn, err1) +// return err1 +// } +// params := FutOrderPlace{ +// ApiId: apiInfo.Id, +// Symbol: symbol.GetSymbol(), +// PositionSide: newOrder.PositionSide, +// Side: newOrder.Side, +// OrderType: ot, +// Quantity: newOrder.TotalNum, +// Price: price, +// StopPrice: price, +// Profit: price, +// NewClientOrderId: newOrder.OrderSn, +// ClosePosition: close, +// } +// futApiV2 := FuturesResetV2{Service: e.Service} +// err = futApiV2.OrderPlaceLoop(&apiInfo, params) + +// if err != nil { +// e.Log.Errorf("币安下单失败 symbol:%s custom:%s :%v", symbol.GetSymbol(), orderSn, err) + +// if err1 := e.Orm.Model(&newOrder).Updates(map[string]interface{}{"status": 8, "remark": err.Error(), "updated_at": time.Now()}).Error; err1 != nil { +// e.Log.Errorf("更新订单状态失败 symbol:%s custom:%s :%v", newOrder.Symbol, newOrder.OrderSn, err1) +// } +// return err +// } + +// e.DoCancelTakeProfitBatch(symbol.GetSymbol(), newOrder.PositionSide, newOrder.Side, newOrder.OrderType, &apiInfo) +// return nil +// } diff --git a/services/excservice/binancesocketmanager.go b/services/excservice/binancesocketmanager.go index 1799c8c..139cdd3 100644 --- a/services/excservice/binancesocketmanager.go +++ b/services/excservice/binancesocketmanager.go @@ -118,6 +118,7 @@ func (wm *BinanceWebSocketManager) triggerReconnect(force bool) { case wm.reconnect <- struct{}{}: default: // 防止阻塞,如果通道满了就跳过 + log.Debugf("reconnect 信号已存在,跳过 key:%s", wm.apiKey) } } } @@ -488,6 +489,7 @@ func (wm *BinanceWebSocketManager) handleOrderUpdate(msg []byte) { } } +// Stop 安全停止 WebSocket func (wm *BinanceWebSocketManager) Stop() { wm.mu.Lock() defer wm.mu.Unlock() @@ -495,9 +497,8 @@ func (wm *BinanceWebSocketManager) Stop() { if wm.isStopped { return } - wm.isStopped = true - // 关闭 stopChannel(确保已经关闭,避免 panic) + select { case <-wm.stopChannel: default: @@ -506,69 +507,106 @@ func (wm *BinanceWebSocketManager) Stop() { if wm.cancelFunc != nil { wm.cancelFunc() + wm.cancelFunc = nil } 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)) + log.Errorf("WebSocket Close 错误 key:%s err:%v", wm.apiKey, err) } + wm.ws = nil } - // **重新创建 stopChannel,避免 Restart() 时无效** - wm.stopChannel = make(chan struct{}) + log.Infof("WebSocket 已完全停止 key:%s", wm.apiKey) + wm.stopChannel = make(chan struct{}, 10) } -// 重连机制 +// handleReconnect 使用指数退避并保持永不退出 func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) { - maxRetries := 100 // 最大重试次数 + const maxRetries = 100 + baseDelay := time.Second * 2 retryCount := 0 for { select { case <-ctx.Done(): + log.Infof("handleReconnect context done: %s", wm.apiKey) return + case <-wm.reconnect: + wm.mu.Lock() if wm.isStopped { + wm.mu.Unlock() return } + wm.mu.Unlock() - log.Warn("WebSocket 连接断开,尝试重连...") - - if wm.ws != nil { - wm.ws.Close() - } - - // 取消旧的上下文 - if wm.cancelFunc != nil { - wm.cancelFunc() - } + log.Warnf("WebSocket 连接断开,准备重连 key:%s", wm.apiKey) for { + wm.mu.Lock() + if wm.ws != nil { + _ = wm.ws.Close() + wm.ws = nil + } + if wm.cancelFunc != nil { + wm.cancelFunc() + wm.cancelFunc = nil + } + wm.mu.Unlock() + newCtx, cancel := context.WithCancel(context.Background()) - wm.cancelFunc = cancel // 更新 cancelFunc + wm.mu.Lock() + wm.cancelFunc = cancel + wm.mu.Unlock() if err := wm.connect(newCtx); err != nil { - log.Errorf("重连失败: %v", err) + log.Errorf("🔌 重连失败(%d/%d)key:%s,err: %v", retryCount+1, maxRetries, wm.apiKey, err) cancel() retryCount++ if retryCount >= maxRetries { + log.Errorf("❌ 重连失败次数过多,停止重连逻辑 key:%s", wm.apiKey) wm.reconnecting.Store(false) - log.Error("重连失败次数过多,退出重连逻辑") return } - time.Sleep(5 * time.Second) + delay := baseDelay * time.Duration(1< time.Minute*5 { + delay = time.Minute * 5 + } + log.Warnf("等待 %v 后重试...", delay) + time.Sleep(delay) continue } - // 重连成功,清除标记 - wm.reconnecting.Store(false) + log.Infof("✅ 重连成功 key:%s", wm.apiKey) retryCount = 0 + wm.reconnecting.Store(false) + + // ✅ 重连成功后开启假死检测 + utility.SafeGo(func() { wm.startDeadCheck(newCtx) }) + + break + } + } + } +} + +// startDeadCheck 替代 Start 中的定时器,绑定连接生命周期 +func (wm *BinanceWebSocketManager) startDeadCheck(ctx context.Context) { + ticker := time.NewTicker(1 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + if wm.isStopped { return } + wm.DeadCheck() } } }