diff --git a/app/admin/apis/line_reverse_position.go b/app/admin/apis/line_reverse_position.go index 59d9a62..f7636de 100644 --- a/app/admin/apis/line_reverse_position.go +++ b/app/admin/apis/line_reverse_position.go @@ -1,7 +1,7 @@ package apis import ( - "fmt" + "fmt" "github.com/gin-gonic/gin" "github.com/go-admin-team/go-admin-core/sdk/api" @@ -33,27 +33,27 @@ type LineReversePosition struct { // @Router /api/v1/line-reverse-position [get] // @Security Bearer func (e LineReversePosition) GetPage(c *gin.Context) { - req := dto.LineReversePositionGetPageReq{} - 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 - } + req := dto.LineReversePositionGetPageReq{} + 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 + } p := actions.GetPermissionFromContext(c) - list := make([]models.LineReversePosition, 0) + list := make([]dto.LineReversePositionListResp, 0) var count int64 err = s.GetPage(&req, p, &list, &count) if err != nil { e.Error(500, err, fmt.Sprintf("获取反单管理-仓位失败,\r\n失败信息 %s", err.Error())) - return + return } e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功") @@ -70,7 +70,7 @@ func (e LineReversePosition) GetPage(c *gin.Context) { func (e LineReversePosition) Get(c *gin.Context) { req := dto.LineReversePositionGetReq{} s := service.LineReversePosition{} - err := e.MakeContext(c). + err := e.MakeContext(c). MakeOrm(). Bind(&req). MakeService(&s.Service). @@ -86,10 +86,10 @@ func (e LineReversePosition) Get(c *gin.Context) { err = s.Get(&req, p, &object) if err != nil { e.Error(500, err, fmt.Sprintf("获取反单管理-仓位失败,\r\n失败信息 %s", err.Error())) - return + return } - e.OK( object, "查询成功") + e.OK(object, "查询成功") } // Insert 创建反单管理-仓位 @@ -103,25 +103,25 @@ func (e LineReversePosition) Get(c *gin.Context) { // @Router /api/v1/line-reverse-position [post] // @Security Bearer func (e LineReversePosition) Insert(c *gin.Context) { - req := dto.LineReversePositionInsertReq{} - 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 - } + req := dto.LineReversePositionInsertReq{} + 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 + } // 设置创建人 req.SetCreateBy(user.GetUserId(c)) err = s.Insert(&req) if err != nil { e.Error(500, err, fmt.Sprintf("创建反单管理-仓位失败,\r\n失败信息 %s", err.Error())) - return + return } e.OK(req.GetId(), "创建成功") @@ -139,27 +139,27 @@ func (e LineReversePosition) Insert(c *gin.Context) { // @Router /api/v1/line-reverse-position/{id} [put] // @Security Bearer func (e LineReversePosition) Update(c *gin.Context) { - req := dto.LineReversePositionUpdateReq{} - 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 - } + req := dto.LineReversePositionUpdateReq{} + 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 + } req.SetUpdateBy(user.GetUserId(c)) p := actions.GetPermissionFromContext(c) err = s.Update(&req, p) if err != nil { e.Error(500, err, fmt.Sprintf("修改反单管理-仓位失败,\r\n失败信息 %s", err.Error())) - return + return } - e.OK( req.GetId(), "修改成功") + e.OK(req.GetId(), "修改成功") } // Delete 删除反单管理-仓位 @@ -171,18 +171,18 @@ func (e LineReversePosition) Update(c *gin.Context) { // @Router /api/v1/line-reverse-position [delete] // @Security Bearer func (e LineReversePosition) Delete(c *gin.Context) { - s := service.LineReversePosition{} - req := dto.LineReversePositionDeleteReq{} - 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 - } + s := service.LineReversePosition{} + req := dto.LineReversePositionDeleteReq{} + 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 + } // req.SetUpdateBy(user.GetUserId(c)) p := actions.GetPermissionFromContext(c) @@ -190,7 +190,7 @@ func (e LineReversePosition) Delete(c *gin.Context) { err = s.Remove(&req, p) if err != nil { e.Error(500, err, fmt.Sprintf("删除反单管理-仓位失败,\r\n失败信息 %s", err.Error())) - return + return } - e.OK( req.GetId(), "删除成功") + e.OK(req.GetId(), "删除成功") } diff --git a/app/admin/models/line_reverse_order.go b/app/admin/models/line_reverse_order.go index 09028d0..13a5041 100644 --- a/app/admin/models/line_reverse_order.go +++ b/app/admin/models/line_reverse_order.go @@ -11,8 +11,9 @@ import ( type LineReverseOrder struct { models.Model - ApiId int `json:"apiId" gorm:"type:bigint;comment:api id"` - Category int `json:"category" gorm:"type:tinyint;comment:分类 0-原始订单 1-反单"` + PositionId int `json:"positionId" gorm:"type:bigint;comment:仓位id"` + ApiId int `json:"apiId" gorm:"type:bigint;comment:api id"` + Category int `json:"category" gorm:"type:tinyint;comment:分类 0-原始订单 1-反单"` OrderSn string `json:"orderSn" gorm:"type:varchar(50);comment:订单号 0-主单 1-止盈 2-止损"` OrderId string `json:"orderId" gorm:"type:varchar(50);comment:币安订单号"` diff --git a/app/admin/models/line_reverse_position.go b/app/admin/models/line_reverse_position.go index 1842b85..04f5fdd 100644 --- a/app/admin/models/line_reverse_position.go +++ b/app/admin/models/line_reverse_position.go @@ -9,18 +9,20 @@ import ( type LineReversePosition struct { models.Model - ApiId int `json:"apiId" gorm:"type:bigint;comment:api_id"` - TotalAmount decimal.Decimal `json:"totalAmount" gorm:"type:decimal(18,8);comment:总仓位"` - Amount decimal.Decimal `json:"amount" gorm:"type:decimal(18,8);comment:仓位"` - ReverseApiId int `json:"reverseApiId" gorm:"type:bigint;comment:反单api_id"` - TotalReverseAmount decimal.Decimal `json:"totalReverseAmount" gorm:"type:decimal(18,8);comment:总反单仓位"` - ReverseAmount decimal.Decimal `json:"reverseAmount" gorm:"type:decimal(18,8);comment:反单仓位"` - Side string `json:"side" gorm:"type:varchar(10);comment:买卖方向 BUY SELL"` - PositionSide string `json:"positionSide" gorm:"type:varchar(10);comment:持仓方向 LONG SHORT"` - Symbol string `json:"symbol" gorm:"type:varchar(20);comment:交易对"` - Status int `json:"status" gorm:"type:tinyint;comment:仓位状态 1-已开仓 2-已平仓"` - ReverseStatus int `json:"reverseStatus" gorm:"type:tinyint;comment:反单仓位状态 1-已开仓 2-已平仓"` - + PositionNo string `json:"positionNo" gorm:"type:varchar(18);comment:仓位编号"` + ApiId int `json:"apiId" gorm:"type:bigint;comment:api_id"` + TotalAmount decimal.Decimal `json:"totalAmount" gorm:"type:decimal(18,8);comment:总仓位"` + Amount decimal.Decimal `json:"amount" gorm:"type:decimal(18,8);comment:仓位"` + ReverseApiId int `json:"reverseApiId" gorm:"type:bigint;comment:反单api_id"` + TotalReverseAmount decimal.Decimal `json:"totalReverseAmount" gorm:"type:decimal(18,8);comment:总反单仓位"` + ReverseAmount decimal.Decimal `json:"reverseAmount" gorm:"type:decimal(18,8);comment:反单仓位"` + Side string `json:"side" gorm:"type:varchar(10);comment:买卖方向 BUY SELL"` + PositionSide string `json:"positionSide" gorm:"type:varchar(10);comment:持仓方向 LONG SHORT"` + Symbol string `json:"symbol" gorm:"type:varchar(20);comment:交易对"` + Status int `json:"status" gorm:"type:tinyint;comment:仓位状态 1-已开仓 2-已平仓"` + ReverseStatus int `json:"reverseStatus" gorm:"type:tinyint;comment:反单仓位状态 1-已开仓 2-已平仓"` + AveragePrice decimal.Decimal `json:"averagePrice" gorm:"type:decimal(18,8);comment:主单平均价格"` + ReverseAveragePrice decimal.Decimal `json:"reverseAveragePrice" gorm:"type:decimal(18,8);comment:反单平均价格"` models.ModelTime models.ControlBy } diff --git a/app/admin/service/dto/line_reverse_order.go b/app/admin/service/dto/line_reverse_order.go index a69e9b5..0b34463 100644 --- a/app/admin/service/dto/line_reverse_order.go +++ b/app/admin/service/dto/line_reverse_order.go @@ -19,6 +19,7 @@ type LineReverseOrderGetPageReq struct { 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-买"` 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 } @@ -52,7 +53,7 @@ func (m *LineReverseOrderGetPageReq) GetNeedSearch() interface{} { type LineReverseOrderInsertReq struct { Id int `json:"-" comment:"主键id"` // 主键id - PId int `json:"pId" comment:"主单id"` + PositionId int `json:"positionId" comment:"仓位id"` OrderSn string `json:"orderSn" comment:"订单号"` OrderId string `json:"orderId" comment:"币安订单号"` FollowOrderSn string `json:"followOrderSn" comment:"跟随币安订单号"` @@ -86,6 +87,7 @@ func (s *LineReverseOrderInsertReq) Generate(model *models.LineReverseOrder) { model.PositionSide = s.PositionSide model.Side = s.Side model.SignPrice = s.SignPrice + model.TriggerTime = &s.TriggerTime model.Status = s.Status model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的 } @@ -96,7 +98,7 @@ func (s *LineReverseOrderInsertReq) GetId() interface{} { type LineReverseOrderUpdateReq struct { Id int `uri:"id" comment:"主键id"` // 主键id - PId int `json:"pId" comment:"主单id"` + PositionId int `json:"positionId" comment:"仓位id"` OrderSn string `json:"orderSn" comment:"订单号"` OrderId string `json:"orderId" comment:"币安订单号"` FollowOrderSn string `json:"followOrderSn" comment:"跟随币安订单号"` @@ -118,7 +120,6 @@ func (s *LineReverseOrderUpdateReq) Generate(model *models.LineReverseOrder) { if s.Id == 0 { model.Model = common.Model{Id: s.Id} } - model.OrderSn = s.OrderSn model.OrderId = s.OrderId model.FollowOrderSn = s.FollowOrderSn @@ -131,6 +132,7 @@ func (s *LineReverseOrderUpdateReq) Generate(model *models.LineReverseOrder) { model.PositionSide = s.PositionSide model.Side = s.Side model.SignPrice = s.SignPrice + model.TriggerTime = &s.TriggerTime model.Status = s.Status model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的 } diff --git a/app/admin/service/dto/line_reverse_position.go b/app/admin/service/dto/line_reverse_position.go index 361abde..ee912e3 100644 --- a/app/admin/service/dto/line_reverse_position.go +++ b/app/admin/service/dto/line_reverse_position.go @@ -115,3 +115,21 @@ type LineReversePositionDeleteReq struct { func (s *LineReversePositionDeleteReq) GetId() interface{} { return s.Ids } + +type LineReversePositionListResp struct { + Id int `json:"id"` + ApiName string `json:"apiName"` + ReverseApiName string `json:"reverseApiName"` + Amount decimal.Decimal `json:"amount"` + TotalAmount decimal.Decimal `json:"totalAmount"` + ReverseAmount decimal.Decimal `json:"reverseAmount"` + TotalReverseAmount decimal.Decimal `json:"totalReverseAmount"` + Side string `json:"side"` + PositionSide string `json:"positionSide"` + Symbol string `json:"symbol"` + Status int `json:"status"` + ReverseStatus int `json:"reverseStatus"` + AveragePrice decimal.Decimal `json:"averagePrice"` + ReverseAveragePrice decimal.Decimal `json:"reverseAveragePrice"` + CreatedAt string `json:"createdAt"` +} diff --git a/app/admin/service/line_reverse_position.go b/app/admin/service/line_reverse_position.go index a073154..26f3104 100644 --- a/app/admin/service/line_reverse_position.go +++ b/app/admin/service/line_reverse_position.go @@ -3,13 +3,15 @@ package service import ( "errors" - "github.com/go-admin-team/go-admin-core/sdk/service" + "github.com/go-admin-team/go-admin-core/sdk/service" + "github.com/jinzhu/copier" "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/pkg/utility" ) type LineReversePosition struct { @@ -17,9 +19,10 @@ type LineReversePosition struct { } // GetPage 获取LineReversePosition列表 -func (e *LineReversePosition) GetPage(c *dto.LineReversePositionGetPageReq, p *actions.DataPermission, list *[]models.LineReversePosition, count *int64) error { +func (e *LineReversePosition) GetPage(c *dto.LineReversePositionGetPageReq, p *actions.DataPermission, list *[]dto.LineReversePositionListResp, count *int64) error { var err error var data models.LineReversePosition + var datas []models.LineReversePosition err = e.Orm.Model(&data). Scopes( @@ -27,12 +30,52 @@ func (e *LineReversePosition) GetPage(c *dto.LineReversePositionGetPageReq, p *a cDto.Paginate(c.GetPageSize(), c.GetPageIndex()), actions.Permission(data.TableName(), p), ). - Find(list).Limit(-1).Offset(-1). + Find(&datas).Limit(-1).Offset(-1). Count(count).Error if err != nil { e.Log.Errorf("LineReversePositionService GetPage error:%s \r\n", err) return err } + + userIds := make([]int, 0) + + for _, item := range datas { + if !utility.ContainsInt(userIds, item.ApiId) { + userIds = append(userIds, item.ApiId) + } + + if !utility.ContainsInt(userIds, item.ReverseApiId) { + userIds = append(userIds, item.ReverseApiId) + } + } + + userMap := make(map[int]string, 0) + + if len(userIds) > 0 { + var users []models.LineApiUser + e.Orm.Model(&models.LineApiUser{}). + Where("id IN (?)", userIds). + Find(&users) + + for _, item := range users { + userMap[int(item.Id)] = item.ApiName + } + } + + for _, item := range datas { + var resp dto.LineReversePositionListResp + copier.Copy(&resp, &item) + + if userName, ok := userMap[item.ApiId]; ok { + resp.ApiName = userName + } + + if userName, ok := userMap[item.ReverseApiId]; ok { + resp.ReverseApiName = userName + } + + *list = append(*list, resp) + } return nil } @@ -59,9 +102,9 @@ func (e *LineReversePosition) Get(d *dto.LineReversePositionGetReq, p *actions.D // Insert 创建LineReversePosition对象 func (e *LineReversePosition) Insert(c *dto.LineReversePositionInsertReq) error { - var err error - var data models.LineReversePosition - c.Generate(&data) + var err error + var data models.LineReversePosition + c.Generate(&data) err = e.Orm.Create(&data).Error if err != nil { e.Log.Errorf("LineReversePositionService Insert error:%s \r\n", err) @@ -72,22 +115,22 @@ func (e *LineReversePosition) Insert(c *dto.LineReversePositionInsertReq) error // Update 修改LineReversePosition对象 func (e *LineReversePosition) Update(c *dto.LineReversePositionUpdateReq, p *actions.DataPermission) error { - var err error - var data = models.LineReversePosition{} - e.Orm.Scopes( - actions.Permission(data.TableName(), p), - ).First(&data, c.GetId()) - c.Generate(&data) + var err error + var data = models.LineReversePosition{} + 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("LineReversePositionService 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("LineReversePositionService Save error:%s \r\n", err) + return err + } + if db.RowsAffected == 0 { + return errors.New("无权更新该数据") + } + return nil } // Remove 删除LineReversePosition @@ -99,11 +142,11 @@ func (e *LineReversePosition) Remove(d *dto.LineReversePositionDeleteReq, p *act actions.Permission(data.TableName(), p), ).Delete(&data, d.GetId()) if err := db.Error; err != nil { - e.Log.Errorf("Service RemoveLineReversePosition error:%s \r\n", err) - return err - } - if db.RowsAffected == 0 { - return errors.New("无权删除该数据") - } + e.Log.Errorf("Service RemoveLineReversePosition error:%s \r\n", err) + return err + } + if db.RowsAffected == 0 { + return errors.New("无权删除该数据") + } return nil } diff --git a/common/helper/redis_helper.go b/common/helper/redis_helper.go index 1202525..f4da7d1 100644 --- a/common/helper/redis_helper.go +++ b/common/helper/redis_helper.go @@ -463,22 +463,13 @@ func (r *RedisHelper) SetNX(key string, value interface{}, expiration time.Durat // SetHashWithTags 改进版:支持 struct 或 map 输入 func (r *RedisHelper) SetHashWithTags(key string, obj interface{}) error { - var fields map[string]interface{} - var err error - - // 1. 优先检查 obj 是否已经是一个 map[string]interface{} - if m, ok := obj.(map[string]interface{}); ok { - fields = m // 如果是,直接使用这个 map - } else { - // 2. 如果不是 map,则假设它是一个 struct,并尝试从 struct 中获取字段 - fields, err = getFieldsFromStruct(obj) // getFieldsFromStruct 现在需要返回 error - if err != nil { - return fmt.Errorf("从结构体获取字段失败: %w", err) - } + fields, err := getFieldsFromStruct(obj) + if err != nil { + return err } - cmd := r.client.HSet(r.ctx, key, fields) - return cmd.Err() + _, err = r.client.HSet(r.ctx, key, fields).Result() + return err } // getFieldsFromStruct 改进版:处理指针并进行类型检查,返回 error diff --git a/models/coin.go b/models/coin.go index df01733..a0e0e70 100644 --- a/models/coin.go +++ b/models/coin.go @@ -37,6 +37,10 @@ type TradeSet struct { E int64 `json:"-"` //推送时间 } +func (e *TradeSet) GetSymbol() string { + return e.Coin + e.Currency +} + //CommissionType int `db:"commissiontype"` //手续费:1买,2卖,3双向 //DepositNum float64 `db:"depositnum" json:"depositnum"` //保证金规模(手) //ForceRate float64 `db:"forcerate" json:"forcerate"` //维持保证金率1% diff --git a/pkg/utility/snowflakehelper/snowflakehelper.go b/pkg/utility/snowflakehelper/snowflakehelper.go index af21a34..2e847d8 100644 --- a/pkg/utility/snowflakehelper/snowflakehelper.go +++ b/pkg/utility/snowflakehelper/snowflakehelper.go @@ -28,3 +28,7 @@ func init() { func GetOrderId() int64 { return snowNode.Generate().Int64() } + +func GetOrderNo() string { + return fmt.Sprintf("%d", GetOrderId()) +} diff --git a/services/binanceservice/futures_reset_v2.go b/services/binanceservice/futures_reset_v2.go index 3b800a0..3cc6b17 100644 --- a/services/binanceservice/futures_reset_v2.go +++ b/services/binanceservice/futures_reset_v2.go @@ -49,7 +49,6 @@ func (e *FuturesResetV2) OrderPlace(apiUserInfo *DbModels.LineApiUser, params Fu paramsMaps := map[string]string{ "symbol": params.Symbol, "side": side, - "quantity": params.Quantity.String(), "type": orderType, "newClientOrderId": params.NewClientOrderId, "positionSide": params.PositionSide, @@ -64,6 +63,10 @@ func (e *FuturesResetV2) OrderPlace(apiUserInfo *DbModels.LineApiUser, params Fu paramsMaps["timeInForce"] = "GTC" paramsMaps["stopprice"] = params.Profit.String() paramsMaps["workingType"] = "MARK_PRICE" + + if params.ClosePosition { + paramsMaps["closePosition"] = "true" + } case "TAKE_PROFIT": paramsMaps["price"] = params.Price.String() paramsMaps["stopprice"] = params.Profit.String() @@ -72,6 +75,10 @@ func (e *FuturesResetV2) OrderPlace(apiUserInfo *DbModels.LineApiUser, params Fu paramsMaps["stopprice"] = params.StopPrice.String() paramsMaps["workingType"] = "MARK_PRICE" paramsMaps["timeInForce"] = "GTC" + + if params.ClosePosition { + paramsMaps["closePosition"] = "true" + } case "STOP": paramsMaps["price"] = params.Price.String() paramsMaps["stopprice"] = params.StopPrice.String() @@ -79,6 +86,10 @@ func (e *FuturesResetV2) OrderPlace(apiUserInfo *DbModels.LineApiUser, params Fu paramsMaps["timeInForce"] = "GTC" } + //不是平仓 + if !params.ClosePosition { + paramsMaps["quantity"] = params.Quantity.String() + } // 获取 API 信息和发送下单请求 client := GetClient(apiUserInfo) _, statusCode, err := client.SendFuturesRequestAuth("/fapi/v1/order", "POST", paramsMaps) @@ -99,7 +110,7 @@ func parseOrderError(err error, paramsMaps map[string]string, statusCode int, ap log.Error("下单失败 参数:", paramsVal) errContent := FutErrorMaps[code.(float64)] if errContent == "" { - errContent = err.Error() + errContent, _ = dataMap["msg"].(string) } return fmt.Errorf("api_id:%d 交易对:%s 下单失败:%s", apiUserInfo.Id, paramsMaps["symbol"], errContent) } diff --git a/services/binanceservice/futuresbinancerest.go b/services/binanceservice/futuresbinancerest.go index fadf352..b880a22 100644 --- a/services/binanceservice/futuresbinancerest.go +++ b/services/binanceservice/futuresbinancerest.go @@ -784,6 +784,27 @@ func (e FutRestApi) CancelAllFutOrder(apiUserInfo DbModels.LineApiUser, symbol s return nil } +// 带重试的批量撤销订单 +func (e FutRestApi) CancelBatchFutOrderLoop(apiUserInfo DbModels.LineApiUser, symbol string, newClientOrderIdList []string) error { + opts := retryhelper.DefaultRetryOptions() + opts.RetryableErrFn = func(err error) bool { + if strings.Contains(err.Error(), "LOT_SIZE") { + return false + } + //重试 + return true + } + + err := retryhelper.Retry(func() error { + return e.CancelBatchFutOrder(apiUserInfo, symbol, newClientOrderIdList) + }, opts) + + if err != nil { + return err + } + return nil +} + // CancelBatchFutOrder 批量撤销订单 // symbol 交易对 // newClientOrderIdList 系统自定义的订单号, 最多支持10个订单 diff --git a/services/binanceservice/futuresrest.go b/services/binanceservice/futuresrest.go index 544ffde..e4be363 100644 --- a/services/binanceservice/futuresrest.go +++ b/services/binanceservice/futuresrest.go @@ -70,36 +70,6 @@ func ChangeFutureOrder(mapData map[string]interface{}, apiKey string) { if err != nil { logger.Errorf("合约订单回调失败,反单失败:%v", err) } - - // 以前的下单逻辑 - // 查询订单 - // preOrder, err := getPreOrder(db, orderSn) - // if err != nil { - // logger.Error("合约订单回调失败,查询订单失败:", orderSn, " err:", err) - // return - // } - - // // 解析订单状态 - // status, ok := mapData["X"].(string) - // if !ok { - // mapStr, _ := sonic.Marshal(&mapData) - // logger.Error("订单回调失败,没有状态:", string(mapStr)) - // return - // } - // // 更新订单状态 - // orderStatus, reason := parseOrderStatus(status, mapData) - - // if orderStatus == 0 { - // logger.Error("订单回调失败,状态错误:", orderSn, " status:", status, " reason:", reason) - // return - // } - - // if err := updateOrderStatus(db, preOrder, orderStatus, reason, true, mapData); err != nil { - // logger.Error("修改订单状态失败:", orderSn, " err:", err) - // return - // } - - // handleFutOrderByType(db, preOrder, orderStatus) } // 合约回调 diff --git a/services/binanceservice/models.go b/services/binanceservice/models.go index 54f320e..de56dac 100644 --- a/services/binanceservice/models.go +++ b/services/binanceservice/models.go @@ -73,6 +73,7 @@ type FutOrderPlace struct { StopPrice decimal.Decimal `json:"stopprice"` //止损价格 OrderType string `json:"order_type"` //订单类型,市价或限价MARKET(市价单) TAKE_PROFIT_MARKET(市价止盈) TAKE_PROFIT(限价止盈) STOP (限价止损) STOP_MARKET(市价止损) NewClientOrderId string `json:"newClientOrderId"` + ClosePosition bool `json:"closePosition"` //是否平仓 } func (s FutOrderPlace) CheckParams() error { diff --git a/services/binanceservice/reverse_service.go b/services/binanceservice/reverse_service.go index 1101e68..2c8bbf9 100644 --- a/services/binanceservice/reverse_service.go +++ b/services/binanceservice/reverse_service.go @@ -7,7 +7,9 @@ import ( DbModels "go-admin/app/admin/models" "go-admin/common/global" "go-admin/common/helper" + "go-admin/models" "go-admin/pkg/maphelper" + "go-admin/pkg/utility/snowflakehelper" "go-admin/services/cacheservice" "strconv" "time" @@ -68,10 +70,23 @@ func (e *ReverseService) ReverseOrder(apiKey string, mapData map[string]interfac result, err := e.handleSimpleStatusChange(status, orderSn, mapData) //如果是 新开止盈止损 需要取消反单的止盈止损之后重下反单止盈止损 - if status == "NEW" && (ot == "TAKE_PROFIT_MARKET" || ot == "STOP_LOSS_MARKET" || ot == "TAKE_PROFIT_LIMIT" || ot == "STOP_LOSS_LIMIT") && + if status == "NEW" && (ot == "TAKE_PROFIT_MARKET" || ot == "STOP_MARKET" || ot == "TAKE_PROFIT" || ot == "STOP") && apiInfo.ReverseStatus == 1 && apiInfo.OpenStatus == 1 && apiInfo.ReverseApiId > 0 { + symbolCode, err := maphelper.GetString(mapData, "s") - if err := e.ReTakeOrStopOrder(&mapData, orderSn, &apiInfo); err != nil { + if err != nil { + e.Log.Errorf("获取symbolCode失败 symbol:%s err:%v", symbolCode, err) + return true, err + } + + symbol, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, symbolCode, 1) + + if err != nil { + e.Log.Errorf("获取symbol失败 symbol:%s err:%v", symbolCode, err) + return true, err + } + + if err := e.ReTakeOrStopOrder(&mapData, orderSn, &apiInfo, &symbol); err != nil { return true, err } } @@ -128,6 +143,7 @@ func (e *ReverseService) ReverseOrder(apiKey string, mapData map[string]interfac return true, err } + price := maphelper.GetDecimal(mapData, "ap") totalNum := maphelper.GetDecimal(mapData, "z") mainOrder := DbModels.LineReverseOrder{ @@ -136,6 +152,7 @@ func (e *ReverseService) ReverseOrder(apiKey string, mapData map[string]interfac PositionSide: positionSide, TotalNum: totalNum, Side: side, + Price: price, } e.changeOrderStatus(3, orderSn, mapData) @@ -175,8 +192,8 @@ func (e *ReverseService) changeOrderStatus(status int, orderSn string, mapData m data["order_id"] = orderId } - if ap, ok := mapData["ap"].(bool); ok { - data["final_price"] = ap + if ap, ok := mapData["ap"].(string); ok { + data["final_price"], _ = decimal.NewFromString(ap) } if num, ok := mapData["z"].(string); ok { @@ -184,6 +201,10 @@ func (e *ReverseService) changeOrderStatus(status int, orderSn string, mapData m } data["trigger_time"] = &now + } else if status == 2 { + if orderId, ok := mapData["i"].(float64); ok { + data["order_id"] = orderId + } } db := e.Orm.Model(&DbModels.LineReverseOrder{}). @@ -284,6 +305,12 @@ func (e *ReverseService) savePosition(reverseOrder *DbModels.LineReverseOrder, r side := reverseOrder.Side totalNum := reverseOrder.TotalNum + symbol, err1 := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reverseOrder.Symbol, 1) + + if err1 != nil { + e.Log.Errorf("获取交易对失败 symbol:%s err:%v", reverseOrder.Symbol, err1) + } + var querySql string sqlStr := "" @@ -310,6 +337,8 @@ func (e *ReverseService) savePosition(reverseOrder *DbModels.LineReverseOrder, r position.Status = 1 position.ReverseStatus = 0 position.PositionSide = positionSide + position.AveragePrice = reverseOrder.FinalPrice + position.PositionNo = snowflakehelper.GetOrderNo() } querySql = "api_id =? and position_side =? and symbol =? and status =1" @@ -319,9 +348,9 @@ func (e *ReverseService) savePosition(reverseOrder *DbModels.LineReverseOrder, r sqlStr = "UPDATE line_reverse_position set amount=@totalNum,updated_at=now(),status=2 where id =@id and status!=2 " } else if reducePosition { //只减仓 - sqlStr = "UPDATE line_reverse_position set amount=amount - @totalNum,updated_at=now() where id =@id and status!=2 " + sqlStr = "UPDATE line_reverse_position set amount=amount - @totalNum,updated_at=now(),average_price=@averagePrice where id =@id and status!=2 " } else { - sqlStr = "UPDATE line_reverse_position set total_amount=total_amount + @totalNum,amount=amount + @totalNum,updated_at=now() where id =@id and status!=2" + sqlStr = "UPDATE line_reverse_position set total_amount=total_amount + @totalNum,amount=amount + @totalNum,updated_at=now(),average_price=@averagePrice where id =@id and status!=2" } } else { querySql = "reverse_api_id =? and position_side =? and symbol =? and reverse_status in (0,1)" @@ -330,12 +359,14 @@ func (e *ReverseService) savePosition(reverseOrder *DbModels.LineReverseOrder, r totalNum = decimal.Zero sqlStr = "UPDATE line_reverse_position set reverse_amount=@totalNum,updated_at=now(),reverse_status=2 where id =@id and reverse_status !=2" } else if reducePosition { - sqlStr = "UPDATE line_reverse_position set reverse_amount=reverse_amount - @totalNum,updated_at=now() where id =@id and reverse_status !=2" + sqlStr = "UPDATE line_reverse_position set reverse_amount=reverse_amount - @totalNum,updated_at=now(),reverse_average_price=@averagePrice where id =@id and reverse_status !=2" } else { - sqlStr = "UPDATE line_reverse_position set total_reverse_amount=total_reverse_amount + @totalNum,reverse_amount=reverse_amount + @totalNum,updated_at=now(),reverse_status =1 where id =@id and reverse_status !=2" + sqlStr = "UPDATE line_reverse_position set total_reverse_amount=total_reverse_amount + @totalNum,reverse_amount=reverse_amount + @totalNum,updated_at=now(),reverse_status =1,reverse_average_price=@averagePrice where id =@id and reverse_status !=2" } } + var averagePrice 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 @@ -347,16 +378,69 @@ func (e *ReverseService) savePosition(reverseOrder *DbModels.LineReverseOrder, r if err2 := tx.Create(&position).Error; err2 != nil { return err2 } + + averagePrice = position.AveragePrice } else { return err1 } + } else { + var totalAmount decimal.Decimal + var totalPrice decimal.Decimal + if !position.Amount.IsZero() && !position.AveragePrice.IsZero() { + if isMain { + totalPrice = position.Amount.Mul(position.AveragePrice) + totalAmount = position.Amount + } else { + totalPrice = position.ReverseAmount.Mul(position.ReverseAveragePrice) + totalAmount = position.ReverseAmount + } + + //加仓 + if !reducePosition { + totalPrice = totalPrice.Add(reverseOrder.Price.Mul(reverseOrder.TotalNum)) + totalAmount = totalAmount.Add(reverseOrder.TotalNum) + } else if reducePosition && !closePosition { + //只减仓 + totalPrice = totalPrice.Sub(reverseOrder.Price.Mul(reverseOrder.TotalNum)) + totalAmount = totalAmount.Sub(reverseOrder.TotalNum) + } + } + + if totalAmount.IsZero() || totalPrice.IsZero() { + if isMain { + averagePrice = position.AveragePrice + } else { + if position.ReverseAveragePrice.IsZero() { + averagePrice = reverseOrder.Price + } else { + averagePrice = position.ReverseAveragePrice + } + } + } else { + //平仓 + if closePosition { + if isMain { + averagePrice = position.AveragePrice + } else { + averagePrice = position.ReverseAveragePrice + } + } else { + averagePrice = totalPrice.Div(totalAmount).Truncate(int32(symbol.PriceDigit)) + } + } } - dbResult := tx.Exec(sqlStr, sql.Named("totalNum", totalNum), sql.Named("id", position.Id)) + //关联订单的仓位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 { + return err2 + } + + dbResult := tx.Exec(sqlStr, sql.Named("totalNum", totalNum), sql.Named("id", position.Id), sql.Named("averagePrice", averagePrice)) if dbResult.Error != nil { return dbResult.Error } + reverseOrder.PositionId = position.Id if dbResult.RowsAffected == 0 { e.Log.Errorf("减仓数据 是否平仓单:%v :%v", closePosition, reverseOrder) return errors.New("没有找到对应的持仓信息") @@ -471,8 +555,8 @@ func (e *ReverseService) DoAddReverseOrder(mainOrder *DbModels.LineReverseOrder, } //反向下单百分比 - proportion = proportion.Div(decimal.NewFromInt(100)).Truncate(2) - amount = mainOrder.BuyPrice.Mul(proportion).Div(price).Truncate(int32(symbol.AmountDigit)) + proportion = proportion.Div(decimal.NewFromInt(100)).Truncate(4) + amount = mainOrder.TotalNum.Mul(proportion).Truncate(int32(symbol.AmountDigit)) if amount.Cmp(decimal.Zero) <= 0 { e.Log.Errorf("计算数量失败 symbol:%s custom:%s 数量小于0", mainOrder.Symbol, mainOrder.OrderSn) @@ -487,6 +571,7 @@ func (e *ReverseService) DoAddReverseOrder(mainOrder *DbModels.LineReverseOrder, order.Status = 1 order.Type = setting.ReverseOrderType order.SignPrice = signPrice + order.PositionId = mainOrder.PositionId if order.Remark != "" { order.Status = 8 @@ -498,12 +583,64 @@ func (e *ReverseService) DoAddReverseOrder(mainOrder *DbModels.LineReverseOrder, } if order.Status == 1 { - e.DoBianceOrder(&order, reverseApiInfo, &setting, reducePosition, closePosition) + err = e.DoBianceOrder(&order, reverseApiInfo, &setting, reducePosition, closePosition) + + //Biance下单成共 且为平仓单时 取消止盈止损 + if err == nil && closePosition { + e.DoCancelTakeProfitBatch(symbol.GetSymbol(), order.PositionSide, order.Side, -1, reverseApiInfo) + } } return nil } +// 取消止盈止损订单 +// symbol: 交易对 +// positionSide: 持仓方向 +// side: 订单方向 +// orderType: 订单类型 -1-全部 1-止盈 2-止损 +// apiInfo: 下单api +func (e *ReverseService) DoCancelTakeProfitBatch(symbol, positionSide, side string, orderType int, apiInfo *DbModels.LineApiUser) error { + orderSns := e.GetTakeProfitBatch(symbol, positionSide, side, apiInfo.Id, orderType) + + if len(orderSns) == 0 { + return nil + } + + if len(orderSns) > 0 { + futApi := FutRestApi{} + err := futApi.CancelBatchFutOrderLoop(*apiInfo, symbol, orderSns) + + if err != nil { + e.Log.Errorf("币安撤单失败 symbol:%s custom:%v :%v", symbol, orderSns, err) + return err + } + } + + return nil +} + +// 获取止盈止损订单 +// symbol: 交易对 +// positionSide: 持仓方向 +// side: 订单方向 +// apiId: 币安apiId +// orderType: 订单类型 -1-全部 1-止盈 2-止损 +func (e *ReverseService) GetTakeProfitBatch(symbol, positionSide, side string, apiId int, orderType int) []string { + var orderSns []string + orderTypes := []int{1, 2} + + if orderType == 1 { + orderTypes = []int{1} + } else if orderType == 2 { + orderTypes = []int{2} + } + + e.Orm.Model(&DbModels.LineReverseOrder{}).Where("symbol =? and position_side =? and side = ? and status =2 and order_type in ?", symbol, positionSide, side, orderTypes).Pluck("order_sn", &orderSns) + + return orderSns +} + // 处理币安订单 // order: 反单信息 // apiInfo: 币安api信息 @@ -546,13 +683,8 @@ func (e *ReverseService) DoBianceOrder(order *DbModels.LineReverseOrder, apiInfo // 重下止盈止损 // mapData: 主单止盈止损回调 -func (e *ReverseService) ReTakeOrStopOrder(mapData *map[string]interface{}, orderSn string, mainApiInfo *DbModels.LineApiUser) error { - symbol, err := maphelper.GetString(*mapData, "s") - - if err != nil { - return err - } - +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 { @@ -584,6 +716,12 @@ func (e *ReverseService) ReTakeOrStopOrder(mapData *map[string]interface{}, orde 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 { @@ -591,33 +729,96 @@ func (e *ReverseService) ReTakeOrStopOrder(mapData *map[string]interface{}, orde return err } - var reverseOrder DbModels.LineReverseOrder - e.Orm.Model(&DbModels.LineReverseOrder{}). - Where("symbol =? and api_id =? and position_side =? and side = ? and status =1", symbol, mainApiInfo.ReverseApiId, positionSide, side). - First(&reverseOrder) - - //取消旧止盈止损 - if reverseOrder.OrderSn != "" { - futApi := FutRestApi{} - err := futApi.CancelFutOrderRetry(apiInfo, symbol, reverseOrder.OrderSn) - - if err != nil { - e.Log.Errorf("币安撤单失败 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, + Symbol: symbol.GetSymbol(), PositionSide: positionSide, Side: side, OrderType: ot, - Quantity: reverseOrder.TotalNum, - Price: reverseOrder.Price, - NewClientOrderId: reverseOrder.OrderSn, + Quantity: reversePosition.TotalReverseAmount, + Price: price, + StopPrice: price, + Profit: price, + NewClientOrderId: newOrder.OrderSn, + ClosePosition: close, } futApiV2 := FuturesResetV2{Service: e.Service} - futApiV2.OrderPlace(&apiInfo, params) + 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(), positionSide, side, orderType, &apiInfo) return nil } diff --git a/services/binanceservice/reverse_service_test.go b/services/binanceservice/reverse_service_test.go index 949a472..2010778 100644 --- a/services/binanceservice/reverse_service_test.go +++ b/services/binanceservice/reverse_service_test.go @@ -1 +1,34 @@ package binanceservice + +import ( + "go-admin/common/helper" + "testing" + + "github.com/bytedance/sonic" + "github.com/go-admin-team/go-admin-core/logger" + "github.com/go-admin-team/go-admin-core/sdk" + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func initConfig() *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") + + return db +} +func TestReverseOrder(t *testing.T) { + db := initConfig() + mapData := make(map[string]interface{}) + content := `{"s":"ADAUSDT","c":"439301585084878848","S":"SELL","o":"LIMIT","f":"GTC","q":"8","p":"0.7781","ap":"0.7843","sp":"0","x":"TRADE","X":"FILLED","i":56468022240,"l":"8","z":"8","L":"0.7843","n":"0.0031372","N":"USDT","T":1753950025820,"t":1642816051,"b":"0","a":"0","m":false,"R":false,"wt":"CONTRACT_PRICE","ot":"LIMIT","ps":"SHORT","cp":false,"rp":"0","pP":false,"si":0,"ss":0,"V":"EXPIRE_MAKER","pm":"NONE","gtd":0}` + sonic.Unmarshal([]byte(content), &mapData) + + service := ReverseService{} + service.Orm = db + service.Log = logger.NewHelper(logger.DefaultLogger) + + service.ReverseOrder("c8ej2vxXzNUIlCjQCmU1iavK8LG78uZaXY3CIT4kz0PuhnXycg44HsVAbsqupHTw", mapData) +} diff --git a/services/excservice/binancesocketmanager.go b/services/excservice/binancesocketmanager.go index 09a492f..1799c8c 100644 --- a/services/excservice/binancesocketmanager.go +++ b/services/excservice/binancesocketmanager.go @@ -522,7 +522,7 @@ func (wm *BinanceWebSocketManager) Stop() { // 重连机制 func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) { - maxRetries := 5 // 最大重试次数 + maxRetries := 100 // 最大重试次数 retryCount := 0 for {