From 771c617da4ff66db46f2a54b821c0388f87ca656 Mon Sep 17 00:00:00 2001 From: hucan <951870319@qq.com> Date: Sat, 26 Jul 2025 09:09:09 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E5=8F=8D=E5=90=91=E4=B8=8B=E5=8D=95?= =?UTF-8?q?=20=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 | 23 + app/admin/apis/line_price_limit.go | 2 + ..._order_ext.go => line_reverse_position.go} | 111 ++-- app/admin/apis/line_reverse_setting.go | 121 ++-- app/admin/models/line_reverse_order.go | 52 +- app/admin/models/line_reverse_order_ext.go | 30 - app/admin/models/line_reverse_position.go | 39 ++ app/admin/models/line_reverse_setting.go | 7 +- app/admin/router/line_api_user.go | 2 + ..._order_ext.go => line_reverse_position.go} | 10 +- app/admin/router/line_reverse_setting.go | 6 +- app/admin/service/dto/line_api_user.go | 5 + app/admin/service/dto/line_reverse_order.go | 213 +++--- .../service/dto/line_reverse_order_ext.go | 91 --- .../service/dto/line_reverse_position.go | 117 ++++ app/admin/service/dto/line_reverse_setting.go | 6 + app/admin/service/line_api_user.go | 60 +- ..._order_ext.go => line_reverse_position.go} | 42 +- app/admin/service/line_reverse_setting.go | 77 ++- cmd/usersubscribe/usersubscribe.go | 35 + common/const/binancecode/event.go | 23 + common/const/binancecode/order_type.go | 20 + common/const/binancecode/side.go | 6 + common/const/rediskey/api_user.go | 6 + common/const/rediskey/reverse_position.go | 11 + common/helper/redis_helper.go | 148 ++++- config/serverinit/business_init.go | 18 + config/serverinit/usersubscribeinit.go | 41 ++ pkg/maphelper/maphelper.go | 46 ++ pkg/retryhelper/retryhelper.go | 54 ++ .../biance_reverse_futures_service.go | 15 - .../biance_reverse_spot_service.go | 12 - .../binanceservice/binance_reverse_service.go | 16 - services/binanceservice/futures_reset_v2.go | 114 ++++ services/binanceservice/futuresbinancerest.go | 10 + services/binanceservice/futuresrest.go | 57 +- services/binanceservice/models.go | 21 +- services/binanceservice/reverse_service.go | 623 ++++++++++++++++++ .../binanceservice/reverse_service_test.go | 1 + services/binanceservice/spotreset.go | 5 +- services/cacheservice/confg_server_test.go | 23 + services/cacheservice/config_service.go | 19 + services/excservice/binancereceive.go | 11 +- services/excservice/binancesocketmanager.go | 283 +++++--- 44 files changed, 2018 insertions(+), 614 deletions(-) rename app/admin/apis/{line_reverse_order_ext.go => line_reverse_position.go} (51%) delete mode 100644 app/admin/models/line_reverse_order_ext.go create mode 100644 app/admin/models/line_reverse_position.go rename app/admin/router/{line_reverse_order_ext.go => line_reverse_position.go} (59%) delete mode 100644 app/admin/service/dto/line_reverse_order_ext.go create mode 100644 app/admin/service/dto/line_reverse_position.go rename app/admin/service/{line_reverse_order_ext.go => line_reverse_position.go} (61%) create mode 100644 common/const/binancecode/event.go create mode 100644 common/const/binancecode/order_type.go create mode 100644 common/const/binancecode/side.go create mode 100644 common/const/rediskey/api_user.go create mode 100644 common/const/rediskey/reverse_position.go create mode 100644 pkg/maphelper/maphelper.go create mode 100644 pkg/retryhelper/retryhelper.go delete mode 100644 services/binanceservice/biance_reverse_futures_service.go delete mode 100644 services/binanceservice/biance_reverse_spot_service.go delete mode 100644 services/binanceservice/binance_reverse_service.go create mode 100644 services/binanceservice/futures_reset_v2.go create mode 100644 services/binanceservice/reverse_service.go create mode 100644 services/binanceservice/reverse_service_test.go create mode 100644 services/cacheservice/confg_server_test.go diff --git a/app/admin/apis/line_api_user.go b/app/admin/apis/line_api_user.go index 8e86f66..0052ce1 100644 --- a/app/admin/apis/line_api_user.go +++ b/app/admin/apis/line_api_user.go @@ -305,3 +305,26 @@ func (e LineApiUser) GetUnBindReverseApiUser(c *gin.Context) { e.OK(datas, "查询成功") } + +// 获取反单api用户选项 +func (e LineApiUser) GetReverseApiOptions(c *gin.Context) { + req := dto.GetReverseApiOptionsReq{} + s := service.LineApiUser{} + 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 + } + list := make([]models.LineApiUser, 0) + err = s.GetReverseApiOptions(&req, &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_price_limit.go b/app/admin/apis/line_price_limit.go index e59b546..88999ed 100644 --- a/app/admin/apis/line_price_limit.go +++ b/app/admin/apis/line_price_limit.go @@ -2,6 +2,7 @@ package apis import ( "fmt" + "github.com/gin-gonic/gin" "github.com/go-admin-team/go-admin-core/sdk/api" "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user" @@ -193,6 +194,7 @@ func (e LinePriceLimit) Delete(c *gin.Context) { e.OK(req.GetId(), "删除成功") } +// aicoin数据同步 func (e LinePriceLimit) UpRange(c *gin.Context) { s := service.LinePriceLimit{} err := e.MakeContext(c). diff --git a/app/admin/apis/line_reverse_order_ext.go b/app/admin/apis/line_reverse_position.go similarity index 51% rename from app/admin/apis/line_reverse_order_ext.go rename to app/admin/apis/line_reverse_position.go index ccd44a2..59d9a62 100644 --- a/app/admin/apis/line_reverse_order_ext.go +++ b/app/admin/apis/line_reverse_position.go @@ -14,22 +14,27 @@ import ( "go-admin/common/actions" ) -type LineReverseOrderExt struct { +type LineReversePosition struct { api.Api } -// GetPage 获取反单拓展信息列表 -// @Summary 获取反单拓展信息列表 -// @Description 获取反单拓展信息列表 -// @Tags 反单拓展信息 +// GetPage 获取反单管理-仓位列表 +// @Summary 获取反单管理-仓位列表 +// @Description 获取反单管理-仓位列表 +// @Tags 反单管理-仓位 +// @Param apiId query int64 false "api_id" +// @Param reverseApiId query int64 false "反单api_id" +// @Param side query string false "买卖方向 BUY SELL" +// @Param positionSide query string false "持仓方向 LONG SHORT" +// @Param symbol query string false "交易对" // @Param pageSize query int false "页条数" // @Param pageIndex query int false "页码" -// @Success 200 {object} response.Response{data=response.Page{list=[]models.LineReverseOrderExt}} "{"code": 200, "data": [...]}" -// @Router /api/v1/line-reverse-order-ext [get] +// @Success 200 {object} response.Response{data=response.Page{list=[]models.LineReversePosition}} "{"code": 200, "data": [...]}" +// @Router /api/v1/line-reverse-position [get] // @Security Bearer -func (e LineReverseOrderExt) GetPage(c *gin.Context) { - req := dto.LineReverseOrderExtGetPageReq{} - s := service.LineReverseOrderExt{} +func (e LineReversePosition) GetPage(c *gin.Context) { + req := dto.LineReversePositionGetPageReq{} + s := service.LineReversePosition{} err := e.MakeContext(c). MakeOrm(). Bind(&req). @@ -42,29 +47,29 @@ func (e LineReverseOrderExt) GetPage(c *gin.Context) { } p := actions.GetPermissionFromContext(c) - list := make([]models.LineReverseOrderExt, 0) + list := make([]models.LineReversePosition, 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())) + e.Error(500, err, fmt.Sprintf("获取反单管理-仓位失败,\r\n失败信息 %s", err.Error())) return } e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功") } -// Get 获取反单拓展信息 -// @Summary 获取反单拓展信息 -// @Description 获取反单拓展信息 -// @Tags 反单拓展信息 +// Get 获取反单管理-仓位 +// @Summary 获取反单管理-仓位 +// @Description 获取反单管理-仓位 +// @Tags 反单管理-仓位 // @Param id path int false "id" -// @Success 200 {object} response.Response{data=models.LineReverseOrderExt} "{"code": 200, "data": [...]}" -// @Router /api/v1/line-reverse-order-ext/{id} [get] +// @Success 200 {object} response.Response{data=models.LineReversePosition} "{"code": 200, "data": [...]}" +// @Router /api/v1/line-reverse-position/{id} [get] // @Security Bearer -func (e LineReverseOrderExt) Get(c *gin.Context) { - req := dto.LineReverseOrderExtGetReq{} - s := service.LineReverseOrderExt{} +func (e LineReversePosition) Get(c *gin.Context) { + req := dto.LineReversePositionGetReq{} + s := service.LineReversePosition{} err := e.MakeContext(c). MakeOrm(). Bind(&req). @@ -75,31 +80,31 @@ func (e LineReverseOrderExt) Get(c *gin.Context) { e.Error(500, err, err.Error()) return } - var object models.LineReverseOrderExt + var object models.LineReversePosition p := actions.GetPermissionFromContext(c) err = s.Get(&req, p, &object) if err != nil { - e.Error(500, err, fmt.Sprintf("获取反单拓展信息失败,\r\n失败信息 %s", err.Error())) + e.Error(500, err, fmt.Sprintf("获取反单管理-仓位失败,\r\n失败信息 %s", err.Error())) return } e.OK( object, "查询成功") } -// Insert 创建反单拓展信息 -// @Summary 创建反单拓展信息 -// @Description 创建反单拓展信息 -// @Tags 反单拓展信息 +// Insert 创建反单管理-仓位 +// @Summary 创建反单管理-仓位 +// @Description 创建反单管理-仓位 +// @Tags 反单管理-仓位 // @Accept application/json // @Product application/json -// @Param data body dto.LineReverseOrderExtInsertReq true "data" +// @Param data body dto.LineReversePositionInsertReq true "data" // @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}" -// @Router /api/v1/line-reverse-order-ext [post] +// @Router /api/v1/line-reverse-position [post] // @Security Bearer -func (e LineReverseOrderExt) Insert(c *gin.Context) { - req := dto.LineReverseOrderExtInsertReq{} - s := service.LineReverseOrderExt{} +func (e LineReversePosition) Insert(c *gin.Context) { + req := dto.LineReversePositionInsertReq{} + s := service.LineReversePosition{} err := e.MakeContext(c). MakeOrm(). Bind(&req). @@ -115,27 +120,27 @@ func (e LineReverseOrderExt) Insert(c *gin.Context) { err = s.Insert(&req) if err != nil { - e.Error(500, err, fmt.Sprintf("创建反单拓展信息失败,\r\n失败信息 %s", err.Error())) + e.Error(500, err, fmt.Sprintf("创建反单管理-仓位失败,\r\n失败信息 %s", err.Error())) return } e.OK(req.GetId(), "创建成功") } -// Update 修改反单拓展信息 -// @Summary 修改反单拓展信息 -// @Description 修改反单拓展信息 -// @Tags 反单拓展信息 +// Update 修改反单管理-仓位 +// @Summary 修改反单管理-仓位 +// @Description 修改反单管理-仓位 +// @Tags 反单管理-仓位 // @Accept application/json // @Product application/json // @Param id path int true "id" -// @Param data body dto.LineReverseOrderExtUpdateReq true "body" +// @Param data body dto.LineReversePositionUpdateReq true "body" // @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}" -// @Router /api/v1/line-reverse-order-ext/{id} [put] +// @Router /api/v1/line-reverse-position/{id} [put] // @Security Bearer -func (e LineReverseOrderExt) Update(c *gin.Context) { - req := dto.LineReverseOrderExtUpdateReq{} - s := service.LineReverseOrderExt{} +func (e LineReversePosition) Update(c *gin.Context) { + req := dto.LineReversePositionUpdateReq{} + s := service.LineReversePosition{} err := e.MakeContext(c). MakeOrm(). Bind(&req). @@ -151,23 +156,23 @@ func (e LineReverseOrderExt) Update(c *gin.Context) { err = s.Update(&req, p) if err != nil { - e.Error(500, err, fmt.Sprintf("修改反单拓展信息失败,\r\n失败信息 %s", err.Error())) + e.Error(500, err, fmt.Sprintf("修改反单管理-仓位失败,\r\n失败信息 %s", err.Error())) return } e.OK( req.GetId(), "修改成功") } -// Delete 删除反单拓展信息 -// @Summary 删除反单拓展信息 -// @Description 删除反单拓展信息 -// @Tags 反单拓展信息 -// @Param data body dto.LineReverseOrderExtDeleteReq true "body" +// Delete 删除反单管理-仓位 +// @Summary 删除反单管理-仓位 +// @Description 删除反单管理-仓位 +// @Tags 反单管理-仓位 +// @Param data body dto.LineReversePositionDeleteReq true "body" // @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}" -// @Router /api/v1/line-reverse-order-ext [delete] +// @Router /api/v1/line-reverse-position [delete] // @Security Bearer -func (e LineReverseOrderExt) Delete(c *gin.Context) { - s := service.LineReverseOrderExt{} - req := dto.LineReverseOrderExtDeleteReq{} +func (e LineReversePosition) Delete(c *gin.Context) { + s := service.LineReversePosition{} + req := dto.LineReversePositionDeleteReq{} err := e.MakeContext(c). MakeOrm(). Bind(&req). @@ -184,7 +189,7 @@ func (e LineReverseOrderExt) Delete(c *gin.Context) { err = s.Remove(&req, p) if err != nil { - e.Error(500, err, fmt.Sprintf("删除反单拓展信息失败,\r\n失败信息 %s", err.Error())) + e.Error(500, err, fmt.Sprintf("删除反单管理-仓位失败,\r\n失败信息 %s", err.Error())) return } e.OK( req.GetId(), "删除成功") diff --git a/app/admin/apis/line_reverse_setting.go b/app/admin/apis/line_reverse_setting.go index d243f7c..5aacf32 100644 --- a/app/admin/apis/line_reverse_setting.go +++ b/app/admin/apis/line_reverse_setting.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" @@ -28,18 +28,18 @@ type LineReverseSetting struct { // @Router /api/v1/line-reverse-setting [get] // @Security Bearer func (e LineReverseSetting) GetPage(c *gin.Context) { - req := dto.LineReverseSettingGetPageReq{} - s := service.LineReverseSetting{} - 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.LineReverseSettingGetPageReq{} + s := service.LineReverseSetting{} + 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.LineReverseSetting, 0) @@ -48,7 +48,7 @@ func (e LineReverseSetting) GetPage(c *gin.Context) { 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(), "查询成功") @@ -65,7 +65,7 @@ func (e LineReverseSetting) GetPage(c *gin.Context) { func (e LineReverseSetting) Get(c *gin.Context) { req := dto.LineReverseSettingGetReq{} s := service.LineReverseSetting{} - err := e.MakeContext(c). + err := e.MakeContext(c). MakeOrm(). Bind(&req). MakeService(&s.Service). @@ -81,10 +81,10 @@ func (e LineReverseSetting) 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 创建反单下单配置 @@ -98,25 +98,25 @@ func (e LineReverseSetting) Get(c *gin.Context) { // @Router /api/v1/line-reverse-setting [post] // @Security Bearer func (e LineReverseSetting) Insert(c *gin.Context) { - req := dto.LineReverseSettingInsertReq{} - s := service.LineReverseSetting{} - 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.LineReverseSettingInsertReq{} + s := service.LineReverseSetting{} + 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(), "创建成功") @@ -134,58 +134,25 @@ func (e LineReverseSetting) Insert(c *gin.Context) { // @Router /api/v1/line-reverse-setting/{id} [put] // @Security Bearer func (e LineReverseSetting) Update(c *gin.Context) { - req := dto.LineReverseSettingUpdateReq{} - s := service.LineReverseSetting{} - 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.LineReverseSettingUpdateReq{} + s := service.LineReverseSetting{} + 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(), "修改成功") -} - -// Delete 删除反单下单配置 -// @Summary 删除反单下单配置 -// @Description 删除反单下单配置 -// @Tags 反单下单配置 -// @Param data body dto.LineReverseSettingDeleteReq true "body" -// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}" -// @Router /api/v1/line-reverse-setting [delete] -// @Security Bearer -func (e LineReverseSetting) Delete(c *gin.Context) { - s := service.LineReverseSetting{} - req := dto.LineReverseSettingDeleteReq{} - 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.Remove(&req, p) - if err != nil { - e.Error(500, err, fmt.Sprintf("删除反单下单配置失败,\r\n失败信息 %s", err.Error())) - 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 e05dc42..09028d0 100644 --- a/app/admin/models/line_reverse_order.go +++ b/app/admin/models/line_reverse_order.go @@ -1,36 +1,44 @@ package models import ( - "time" + "time" "go-admin/common/models" + "github.com/shopspring/decimal" ) type LineReverseOrder struct { - models.Model - - PId int `json:"pId" gorm:"type:bigint;comment:主单id"` - OrderSn string `json:"orderSn" gorm:"type:varchar(50);comment:订单号"` - OrderId string `json:"orderId" gorm:"type:varchar(50);comment:币安订单号"` - FollowOrderSn string `json:"followOrderSn" gorm:"type:varchar(50);comment:跟随币安订单号"` - Symbol string `json:"symbol" gorm:"type:varchar(20);comment:交易对"` - OrderType int `json:"orderType" gorm:"type:tinyint;comment:订单类型 0-主单 1-止损单 2-加仓 3-减仓"` - BuyPrice decimal.Decimal `json:"buyPrice" gorm:"type:decimal(18,8);comment:购买金额"` - Price decimal.Decimal `json:"price" gorm:"type:decimal(18,8);comment:委托价格"` - PriceU decimal.Decimal `json:"priceU" gorm:"type:decimal(18,8);comment:委托价格(U)"` - FinalPrice decimal.Decimal `json:"finalPrice" gorm:"type:decimal(18,8);comment:实际成交价"` - PositionSide string `json:"positionSide" gorm:"type:varchar(10);comment:持仓方向 LONG-多 SHORT-空"` - Side string `json:"side" gorm:"type:varchar(10);comment:买卖方向 SELL-卖 BUY-买"` - SignPrice decimal.Decimal `json:"signPrice" gorm:"type:decimal(18,8);comment:行情价"` - TriggerTime time.Time `json:"triggerTime" gorm:"type:datetime;comment:触发时间"` - Status int `json:"status" gorm:"type:tinyint;comment:状态 1-待下单 2-已下单 3-已成交 4-已平仓 5-已止损"` - models.ModelTime - models.ControlBy + models.Model + + 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:币安订单号"` + FollowOrderSn string `json:"followOrderSn" gorm:"type:varchar(50);comment:跟随币安订单号"` + Symbol string `json:"symbol" gorm:"type:varchar(20);comment:交易对"` + Type string `json:"type" gorm:"type:varchar(20);comment:交易类型"` + OrderType int `json:"orderType" gorm:"type:tinyint;comment:订单类型 0-主单 1-止盈 2-止损 3-减仓单 4-平仓单"` + BuyPrice decimal.Decimal `json:"buyPrice" gorm:"type:decimal(18,8);comment:购买金额"` + Price decimal.Decimal `json:"price" gorm:"type:decimal(18,8);comment:委托价格"` + TotalNum decimal.Decimal `json:"totalNum" gorm:"type:decimal(18,8);comment:总成交数量"` + PriceU decimal.Decimal `json:"priceU" gorm:"type:decimal(18,8);comment:委托价格(U)"` + FinalPrice decimal.Decimal `json:"finalPrice" gorm:"type:decimal(18,8);comment:实际成交价"` + PositionSide string `json:"positionSide" gorm:"type:varchar(10);comment:持仓方向 LONG-多 SHORT-空"` + Side string `json:"side" gorm:"type:varchar(10);comment:买卖方向 SELL-卖 BUY-买"` + SignPrice decimal.Decimal `json:"signPrice" gorm:"type:decimal(18,8);comment:行情价"` + TriggerTime *time.Time `json:"triggerTime" gorm:"type:datetime;comment:触发时间"` + Status int `json:"status" gorm:"type:tinyint;comment:状态 1-待下单 2-已下单 3-已成交 6-已取消 7-已过期 8-已失败"` + IsAddPosition int `json:"isAddPosition" gorm:"type:tinyint;comment:是否增加仓位 1-否 2-是"` + IsReduce int `json:"isReduce" gorm:"type:tinyint;comment:是否减少仓位 1-否 2-是"` + Remark string `json:"remark" gorm:"type:varchar(255);comment:备注"` + models.ModelTime + models.ControlBy } func (LineReverseOrder) TableName() string { - return "line_reverse_order" + return "line_reverse_order" } func (e *LineReverseOrder) Generate() models.ActiveRecord { @@ -40,4 +48,4 @@ func (e *LineReverseOrder) Generate() models.ActiveRecord { func (e *LineReverseOrder) GetId() interface{} { return e.Id -} \ No newline at end of file +} diff --git a/app/admin/models/line_reverse_order_ext.go b/app/admin/models/line_reverse_order_ext.go deleted file mode 100644 index 11ed45c..0000000 --- a/app/admin/models/line_reverse_order_ext.go +++ /dev/null @@ -1,30 +0,0 @@ -package models - -import ( - - "go-admin/common/models" - -) - -type LineReverseOrderExt struct { - models.Model - - ReverseOrderId int `json:"reverseOrderId" gorm:"type:bigint;comment:下反单id"` - IsAddPosition int `json:"isAddPosition" gorm:"type:tinyint;comment:加仓状态 1-已加仓 2-未加仓"` - IsReduce int `json:"isReduce" gorm:"type:tinyint;comment:减仓状态 1-已减仓 2-未减仓"` - models.ModelTime - models.ControlBy -} - -func (LineReverseOrderExt) TableName() string { - return "line_reverse_order_ext" -} - -func (e *LineReverseOrderExt) Generate() models.ActiveRecord { - o := *e - return &o -} - -func (e *LineReverseOrderExt) GetId() interface{} { - return e.Id -} \ No newline at end of file diff --git a/app/admin/models/line_reverse_position.go b/app/admin/models/line_reverse_position.go new file mode 100644 index 0000000..1842b85 --- /dev/null +++ b/app/admin/models/line_reverse_position.go @@ -0,0 +1,39 @@ +package models + +import ( + "go-admin/common/models" + + "github.com/shopspring/decimal" +) + +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-已平仓"` + + models.ModelTime + models.ControlBy +} + +func (LineReversePosition) TableName() string { + return "line_reverse_position" +} + +func (e *LineReversePosition) Generate() models.ActiveRecord { + o := *e + return &o +} + +func (e *LineReversePosition) GetId() interface{} { + return e.Id +} diff --git a/app/admin/models/line_reverse_setting.go b/app/admin/models/line_reverse_setting.go index a3431ea..bec93c9 100644 --- a/app/admin/models/line_reverse_setting.go +++ b/app/admin/models/line_reverse_setting.go @@ -9,9 +9,10 @@ import ( type LineReverseSetting struct { models.Model - ReverseOrderType string `json:"reverseOrderType" gorm:"type:varchar(10);comment:反单下单类型 LIMIT-限价 MARKET-市价"` - ReversePremiumRatio decimal.Decimal `json:"reversePremiumRatio" gorm:"type:decimal(10,2);comment:溢价百分比"` - StopLossRatio decimal.Decimal `json:"stopLossRatio" gorm:"type:decimal(10,2);comment:止损百分比"` + ReverseOrderType string `json:"reverseOrderType" redis:"reverseOrderType" gorm:"type:varchar(10);comment:反单下单类型 LIMIT-限价 MARKET-市价"` + ReversePremiumRatio decimal.Decimal `json:"reversePremiumRatio" redis:"reversePremiumRatio" gorm:"type:decimal(10,2);comment:溢价百分比"` + TakeProfitRatio decimal.Decimal `json:"takeProfitRatio" redis:"takeProfitRatio" gorm:"type:decimal(10,2);comment:止盈百分比"` + StopLossRatio decimal.Decimal `json:"stopLossRatio" redis:"stopLossRatio" gorm:"type:decimal(10,2);comment:止损百分比"` models.ModelTime models.ControlBy } diff --git a/app/admin/router/line_api_user.go b/app/admin/router/line_api_user.go index 5f878ef..2507b33 100644 --- a/app/admin/router/line_api_user.go +++ b/app/admin/router/line_api_user.go @@ -29,5 +29,7 @@ func registerLineApiUserRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMi r.POST("getMainUser", api.GetMainUser) //获取获取主账号的用户 r.GET("unbind-reverse", api.GetUnBindReverseApiUser) //获取未绑定下反单用户 + + r.GET("reverse-options", api.GetReverseApiOptions) //获取可用反单api用户 } } diff --git a/app/admin/router/line_reverse_order_ext.go b/app/admin/router/line_reverse_position.go similarity index 59% rename from app/admin/router/line_reverse_order_ext.go rename to app/admin/router/line_reverse_position.go index 302aa71..87c81a5 100644 --- a/app/admin/router/line_reverse_order_ext.go +++ b/app/admin/router/line_reverse_position.go @@ -10,13 +10,13 @@ import ( ) func init() { - routerCheckRole = append(routerCheckRole, registerLineReverseOrderExtRouter) + routerCheckRole = append(routerCheckRole, registerLineReversePositionRouter) } -// registerLineReverseOrderExtRouter -func registerLineReverseOrderExtRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) { - api := apis.LineReverseOrderExt{} - r := v1.Group("/line-reverse-order-ext").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole()) +// registerLineReversePositionRouter +func registerLineReversePositionRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) { + api := apis.LineReversePosition{} + r := v1.Group("/line-reverse-position").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole()) { r.GET("", actions.PermissionAction(), api.GetPage) r.GET("/:id", actions.PermissionAction(), api.Get) diff --git a/app/admin/router/line_reverse_setting.go b/app/admin/router/line_reverse_setting.go index 7e6f8fb..f4d695b 100644 --- a/app/admin/router/line_reverse_setting.go +++ b/app/admin/router/line_reverse_setting.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() { @@ -22,6 +22,6 @@ func registerLineReverseSettingRouter(v1 *gin.RouterGroup, authMiddleware *jwt.G r.GET("/:id", actions.PermissionAction(), api.Get) r.POST("", api.Insert) r.PUT("/:id", actions.PermissionAction(), api.Update) - r.DELETE("", api.Delete) + // r.DELETE("", api.Delete) } -} \ 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 50d7c74..546c64d 100644 --- a/app/admin/service/dto/line_api_user.go +++ b/app/admin/service/dto/line_api_user.go @@ -219,3 +219,8 @@ type UnBindReverseResp struct { Disabled bool `json:"disabled"` ApiName string `json:"apiName"` } + +type GetReverseApiOptionsReq struct { + Id int `json:"apiId" form:"id"` + ApiId int `json:"apiId" form:"apiId"` +} diff --git a/app/admin/service/dto/line_reverse_order.go b/app/admin/service/dto/line_reverse_order.go index e6ccbd5..a69e9b5 100644 --- a/app/admin/service/dto/line_reverse_order.go +++ b/app/admin/service/dto/line_reverse_order.go @@ -1,48 +1,49 @@ package dto import ( - "time" + "time" "go-admin/app/admin/models" "go-admin/common/dto" common "go-admin/common/models" + + "github.com/shopspring/decimal" ) type LineReverseOrderGetPageReq struct { - dto.Pagination `search:"-"` - OrderSn string `form:"orderSn" search:"type:contains;column:order_sn;table:line_reverse_order" comment:"订单号"` - OrderId string `form:"orderId" search:"type:contains;column:order_id;table:line_reverse_order" comment:"币安订单号"` - FollowOrderSn string `form:"followOrderSn" search:"type:contains;column:follow_order_sn;table:line_reverse_order" comment:"跟随币安订单号"` - 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-买"` - Status int `form:"status" search:"type:exact;column:status;table:line_reverse_order" comment:"状态 1-待下单 2-已下单 3-已成交 4-已平仓 5-已止损"` - LineReverseOrderOrder + dto.Pagination `search:"-"` + OrderSn string `form:"orderSn" search:"type:contains;column:order_sn;table:line_reverse_order" comment:"订单号"` + OrderId string `form:"orderId" search:"type:contains;column:order_id;table:line_reverse_order" comment:"币安订单号"` + FollowOrderSn string `form:"followOrderSn" search:"type:contains;column:follow_order_sn;table:line_reverse_order" comment:"跟随币安订单号"` + 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-买"` + Status int `form:"status" search:"type:exact;column:status;table:line_reverse_order" comment:"状态 1-待下单 2-已下单 3-已成交 4-已平仓 5-已止损"` + LineReverseOrderOrder } type LineReverseOrderOrder struct { - Id string `form:"idOrder" search:"type:order;column:id;table:line_reverse_order"` - PId string `form:"pIdOrder" search:"type:order;column:p_id;table:line_reverse_order"` - OrderSn string `form:"orderSnOrder" search:"type:order;column:order_sn;table:line_reverse_order"` - OrderId string `form:"orderIdOrder" search:"type:order;column:order_id;table:line_reverse_order"` - FollowOrderSn string `form:"followOrderSnOrder" search:"type:order;column:follow_order_sn;table:line_reverse_order"` - Symbol string `form:"symbolOrder" search:"type:order;column:symbol;table:line_reverse_order"` - OrderType string `form:"orderTypeOrder" search:"type:order;column:order_type;table:line_reverse_order"` - BuyPrice string `form:"buyPriceOrder" search:"type:order;column:buy_price;table:line_reverse_order"` - Price string `form:"priceOrder" search:"type:order;column:price;table:line_reverse_order"` - PriceU string `form:"priceUOrder" search:"type:order;column:price_u;table:line_reverse_order"` - FinalPrice string `form:"finalPriceOrder" search:"type:order;column:final_price;table:line_reverse_order"` - PositionSide string `form:"positionSideOrder" search:"type:order;column:position_side;table:line_reverse_order"` - Side string `form:"sideOrder" search:"type:order;column:side;table:line_reverse_order"` - SignPrice string `form:"signPriceOrder" search:"type:order;column:sign_price;table:line_reverse_order"` - TriggerTime string `form:"triggerTimeOrder" search:"type:order;column:trigger_time;table:line_reverse_order"` - Status string `form:"statusOrder" search:"type:order;column:status;table:line_reverse_order"` - CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:line_reverse_order"` - UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:line_reverse_order"` - DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:line_reverse_order"` - CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:line_reverse_order"` - UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:line_reverse_order"` - + Id string `form:"idOrder" search:"type:order;column:id;table:line_reverse_order"` + PId string `form:"pIdOrder" search:"type:order;column:p_id;table:line_reverse_order"` + OrderSn string `form:"orderSnOrder" search:"type:order;column:order_sn;table:line_reverse_order"` + OrderId string `form:"orderIdOrder" search:"type:order;column:order_id;table:line_reverse_order"` + FollowOrderSn string `form:"followOrderSnOrder" search:"type:order;column:follow_order_sn;table:line_reverse_order"` + Symbol string `form:"symbolOrder" search:"type:order;column:symbol;table:line_reverse_order"` + OrderType string `form:"orderTypeOrder" search:"type:order;column:order_type;table:line_reverse_order"` + BuyPrice string `form:"buyPriceOrder" search:"type:order;column:buy_price;table:line_reverse_order"` + Price string `form:"priceOrder" search:"type:order;column:price;table:line_reverse_order"` + PriceU string `form:"priceUOrder" search:"type:order;column:price_u;table:line_reverse_order"` + FinalPrice string `form:"finalPriceOrder" search:"type:order;column:final_price;table:line_reverse_order"` + PositionSide string `form:"positionSideOrder" search:"type:order;column:position_side;table:line_reverse_order"` + Side string `form:"sideOrder" search:"type:order;column:side;table:line_reverse_order"` + SignPrice string `form:"signPriceOrder" search:"type:order;column:sign_price;table:line_reverse_order"` + TriggerTime string `form:"triggerTimeOrder" search:"type:order;column:trigger_time;table:line_reverse_order"` + Status string `form:"statusOrder" search:"type:order;column:status;table:line_reverse_order"` + CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:line_reverse_order"` + UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:line_reverse_order"` + DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:line_reverse_order"` + CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:line_reverse_order"` + UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:line_reverse_order"` } func (m *LineReverseOrderGetPageReq) GetNeedSearch() interface{} { @@ -50,45 +51,43 @@ func (m *LineReverseOrderGetPageReq) GetNeedSearch() interface{} { } type LineReverseOrderInsertReq struct { - Id int `json:"-" comment:"主键id"` // 主键id - PId int `json:"pId" comment:"主单id"` - OrderSn string `json:"orderSn" comment:"订单号"` - OrderId string `json:"orderId" comment:"币安订单号"` - FollowOrderSn string `json:"followOrderSn" comment:"跟随币安订单号"` - Symbol string `json:"symbol" comment:"交易对"` - OrderType int `json:"orderType" comment:"订单类型 0-主单 1-止损单 2-加仓 3-减仓"` - BuyPrice decimal.Decimal `json:"buyPrice" comment:"购买金额"` - Price decimal.Decimal `json:"price" comment:"委托价格"` - PriceU decimal.Decimal `json:"priceU" comment:"委托价格(U)"` - FinalPrice decimal.Decimal `json:"finalPrice" comment:"实际成交价"` - PositionSide string `json:"positionSide" comment:"持仓方向 LONG-多 SHORT-空"` - Side string `json:"side" comment:"买卖方向 SELL-卖 BUY-买"` - SignPrice decimal.Decimal `json:"signPrice" comment:"行情价"` - TriggerTime time.Time `json:"triggerTime" comment:"触发时间"` - Status int `json:"status" comment:"状态 1-待下单 2-已下单 3-已成交 4-已平仓 5-已止损"` - common.ControlBy + Id int `json:"-" comment:"主键id"` // 主键id + PId int `json:"pId" comment:"主单id"` + OrderSn string `json:"orderSn" comment:"订单号"` + OrderId string `json:"orderId" comment:"币安订单号"` + FollowOrderSn string `json:"followOrderSn" comment:"跟随币安订单号"` + Symbol string `json:"symbol" comment:"交易对"` + OrderType int `json:"orderType" comment:"订单类型 0-主单 1-止损单 2-加仓 3-减仓"` + BuyPrice decimal.Decimal `json:"buyPrice" comment:"购买金额"` + Price decimal.Decimal `json:"price" comment:"委托价格"` + PriceU decimal.Decimal `json:"priceU" comment:"委托价格(U)"` + FinalPrice decimal.Decimal `json:"finalPrice" comment:"实际成交价"` + PositionSide string `json:"positionSide" comment:"持仓方向 LONG-多 SHORT-空"` + Side string `json:"side" comment:"买卖方向 SELL-卖 BUY-买"` + SignPrice decimal.Decimal `json:"signPrice" comment:"行情价"` + TriggerTime time.Time `json:"triggerTime" comment:"触发时间"` + Status int `json:"status" comment:"状态 1-待下单 2-已下单 3-已成交 4-已平仓 5-已止损"` + common.ControlBy } -func (s *LineReverseOrderInsertReq) Generate(model *models.LineReverseOrder) { - if s.Id == 0 { - model.Model = common.Model{ Id: s.Id } - } - model.PId = s.PId - model.OrderSn = s.OrderSn - model.OrderId = s.OrderId - model.FollowOrderSn = s.FollowOrderSn - model.Symbol = s.Symbol - model.OrderType = s.OrderType - model.BuyPrice = s.BuyPrice - model.Price = s.Price - model.PriceU = s.PriceU - model.FinalPrice = s.FinalPrice - model.PositionSide = s.PositionSide - model.Side = s.Side - model.SignPrice = s.SignPrice - model.TriggerTime = s.TriggerTime - model.Status = s.Status - model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的 +func (s *LineReverseOrderInsertReq) 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 + model.Symbol = s.Symbol + model.OrderType = s.OrderType + model.BuyPrice = s.BuyPrice + model.Price = s.Price + model.PriceU = s.PriceU + model.FinalPrice = s.FinalPrice + model.PositionSide = s.PositionSide + model.Side = s.Side + model.SignPrice = s.SignPrice + model.Status = s.Status + model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的 } func (s *LineReverseOrderInsertReq) GetId() interface{} { @@ -96,45 +95,44 @@ func (s *LineReverseOrderInsertReq) GetId() interface{} { } type LineReverseOrderUpdateReq struct { - Id int `uri:"id" comment:"主键id"` // 主键id - PId int `json:"pId" comment:"主单id"` - OrderSn string `json:"orderSn" comment:"订单号"` - OrderId string `json:"orderId" comment:"币安订单号"` - FollowOrderSn string `json:"followOrderSn" comment:"跟随币安订单号"` - Symbol string `json:"symbol" comment:"交易对"` - OrderType int `json:"orderType" comment:"订单类型 0-主单 1-止损单 2-加仓 3-减仓"` - BuyPrice decimal.Decimal `json:"buyPrice" comment:"购买金额"` - Price decimal.Decimal `json:"price" comment:"委托价格"` - PriceU decimal.Decimal `json:"priceU" comment:"委托价格(U)"` - FinalPrice decimal.Decimal `json:"finalPrice" comment:"实际成交价"` - PositionSide string `json:"positionSide" comment:"持仓方向 LONG-多 SHORT-空"` - Side string `json:"side" comment:"买卖方向 SELL-卖 BUY-买"` - SignPrice decimal.Decimal `json:"signPrice" comment:"行情价"` - TriggerTime time.Time `json:"triggerTime" comment:"触发时间"` - Status int `json:"status" comment:"状态 1-待下单 2-已下单 3-已成交 4-已平仓 5-已止损"` - common.ControlBy + Id int `uri:"id" comment:"主键id"` // 主键id + PId int `json:"pId" comment:"主单id"` + OrderSn string `json:"orderSn" comment:"订单号"` + OrderId string `json:"orderId" comment:"币安订单号"` + FollowOrderSn string `json:"followOrderSn" comment:"跟随币安订单号"` + Symbol string `json:"symbol" comment:"交易对"` + OrderType int `json:"orderType" comment:"订单类型 0-主单 1-止损单 2-加仓 3-减仓"` + BuyPrice decimal.Decimal `json:"buyPrice" comment:"购买金额"` + Price decimal.Decimal `json:"price" comment:"委托价格"` + PriceU decimal.Decimal `json:"priceU" comment:"委托价格(U)"` + FinalPrice decimal.Decimal `json:"finalPrice" comment:"实际成交价"` + PositionSide string `json:"positionSide" comment:"持仓方向 LONG-多 SHORT-空"` + Side string `json:"side" comment:"买卖方向 SELL-卖 BUY-买"` + SignPrice decimal.Decimal `json:"signPrice" comment:"行情价"` + TriggerTime time.Time `json:"triggerTime" comment:"触发时间"` + Status int `json:"status" comment:"状态 1-待下单 2-已下单 3-已成交 4-已平仓 5-已止损"` + common.ControlBy } -func (s *LineReverseOrderUpdateReq) Generate(model *models.LineReverseOrder) { - if s.Id == 0 { - model.Model = common.Model{ Id: s.Id } - } - model.PId = s.PId - model.OrderSn = s.OrderSn - model.OrderId = s.OrderId - model.FollowOrderSn = s.FollowOrderSn - model.Symbol = s.Symbol - model.OrderType = s.OrderType - model.BuyPrice = s.BuyPrice - model.Price = s.Price - model.PriceU = s.PriceU - model.FinalPrice = s.FinalPrice - model.PositionSide = s.PositionSide - model.Side = s.Side - model.SignPrice = s.SignPrice - model.TriggerTime = s.TriggerTime - model.Status = s.Status - model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的 +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 + model.Symbol = s.Symbol + model.OrderType = s.OrderType + model.BuyPrice = s.BuyPrice + model.Price = s.Price + model.PriceU = s.PriceU + model.FinalPrice = s.FinalPrice + model.PositionSide = s.PositionSide + model.Side = s.Side + model.SignPrice = s.SignPrice + model.Status = s.Status + model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的 } func (s *LineReverseOrderUpdateReq) GetId() interface{} { @@ -143,8 +141,9 @@ func (s *LineReverseOrderUpdateReq) GetId() interface{} { // LineReverseOrderGetReq 功能获取请求参数 type LineReverseOrderGetReq struct { - Id int `uri:"id"` + Id int `uri:"id"` } + func (s *LineReverseOrderGetReq) GetId() interface{} { return s.Id } diff --git a/app/admin/service/dto/line_reverse_order_ext.go b/app/admin/service/dto/line_reverse_order_ext.go deleted file mode 100644 index 7cbef1d..0000000 --- a/app/admin/service/dto/line_reverse_order_ext.go +++ /dev/null @@ -1,91 +0,0 @@ -package dto - -import ( - - "go-admin/app/admin/models" - "go-admin/common/dto" - common "go-admin/common/models" -) - -type LineReverseOrderExtGetPageReq struct { - dto.Pagination `search:"-"` - LineReverseOrderExtOrder -} - -type LineReverseOrderExtOrder struct { - Id string `form:"idOrder" search:"type:order;column:id;table:line_reverse_order_ext"` - ReverseOrderId string `form:"reverseOrderIdOrder" search:"type:order;column:reverse_order_id;table:line_reverse_order_ext"` - IsAddPosition string `form:"isAddPositionOrder" search:"type:order;column:is_add_position;table:line_reverse_order_ext"` - IsReduce string `form:"isReduceOrder" search:"type:order;column:is_reduce;table:line_reverse_order_ext"` - CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:line_reverse_order_ext"` - UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:line_reverse_order_ext"` - DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:line_reverse_order_ext"` - CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:line_reverse_order_ext"` - UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:line_reverse_order_ext"` - -} - -func (m *LineReverseOrderExtGetPageReq) GetNeedSearch() interface{} { - return *m -} - -type LineReverseOrderExtInsertReq struct { - Id int `json:"-" comment:"主键id"` // 主键id - ReverseOrderId int `json:"reverseOrderId" comment:"下反单id"` - IsAddPosition int `json:"isAddPosition" comment:"加仓状态 1-已加仓 2-未加仓"` - IsReduce int `json:"isReduce" comment:"减仓状态 1-已减仓 2-未减仓"` - common.ControlBy -} - -func (s *LineReverseOrderExtInsertReq) Generate(model *models.LineReverseOrderExt) { - if s.Id == 0 { - model.Model = common.Model{ Id: s.Id } - } - model.ReverseOrderId = s.ReverseOrderId - model.IsAddPosition = s.IsAddPosition - model.IsReduce = s.IsReduce - model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的 -} - -func (s *LineReverseOrderExtInsertReq) GetId() interface{} { - return s.Id -} - -type LineReverseOrderExtUpdateReq struct { - Id int `uri:"id" comment:"主键id"` // 主键id - ReverseOrderId int `json:"reverseOrderId" comment:"下反单id"` - IsAddPosition int `json:"isAddPosition" comment:"加仓状态 1-已加仓 2-未加仓"` - IsReduce int `json:"isReduce" comment:"减仓状态 1-已减仓 2-未减仓"` - common.ControlBy -} - -func (s *LineReverseOrderExtUpdateReq) Generate(model *models.LineReverseOrderExt) { - if s.Id == 0 { - model.Model = common.Model{ Id: s.Id } - } - model.ReverseOrderId = s.ReverseOrderId - model.IsAddPosition = s.IsAddPosition - model.IsReduce = s.IsReduce - model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的 -} - -func (s *LineReverseOrderExtUpdateReq) GetId() interface{} { - return s.Id -} - -// LineReverseOrderExtGetReq 功能获取请求参数 -type LineReverseOrderExtGetReq struct { - Id int `uri:"id"` -} -func (s *LineReverseOrderExtGetReq) GetId() interface{} { - return s.Id -} - -// LineReverseOrderExtDeleteReq 功能删除请求参数 -type LineReverseOrderExtDeleteReq struct { - Ids []int `json:"ids"` -} - -func (s *LineReverseOrderExtDeleteReq) GetId() interface{} { - return s.Ids -} diff --git a/app/admin/service/dto/line_reverse_position.go b/app/admin/service/dto/line_reverse_position.go new file mode 100644 index 0000000..361abde --- /dev/null +++ b/app/admin/service/dto/line_reverse_position.go @@ -0,0 +1,117 @@ +package dto + +import ( + "go-admin/app/admin/models" + "go-admin/common/dto" + common "go-admin/common/models" + + "github.com/shopspring/decimal" +) + +type LineReversePositionGetPageReq struct { + dto.Pagination `search:"-"` + ApiId int64 `form:"apiId" search:"type:exact;column:api_id;table:line_reverse_position" comment:"api_id"` + ReverseApiId int64 `form:"reverseApiId" search:"type:exact;column:reverse_api_id;table:line_reverse_position" comment:"反单api_id"` + Side string `form:"side" search:"type:exact;column:side;table:line_reverse_position" comment:"买卖方向 BUY SELL"` + PositionSide string `form:"positionSide" search:"type:exact;column:position_side;table:line_reverse_position" comment:"持仓方向 LONG SHORT"` + Symbol string `form:"symbol" search:"type:contains;column:symbol;table:line_reverse_position" comment:"交易对"` + LineReversePositionOrder +} + +type LineReversePositionOrder struct { + Id string `form:"idOrder" search:"type:order;column:id;table:line_reverse_position"` + ApiId string `form:"apiIdOrder" search:"type:order;column:api_id;table:line_reverse_position"` + ReverseApiId string `form:"reverseApiIdOrder" search:"type:order;column:reverse_api_id;table:line_reverse_position"` + ReverseAmount string `form:"reverseAmountOrder" search:"type:order;column:reverse_amount;table:line_reverse_position"` + Side string `form:"sideOrder" search:"type:order;column:side;table:line_reverse_position"` + PositionSide string `form:"positionSideOrder" search:"type:order;column:position_side;table:line_reverse_position"` + Symbol string `form:"symbolOrder" search:"type:order;column:symbol;table:line_reverse_position"` + Status string `form:"statusOrder" search:"type:order;column:status;table:line_reverse_position"` + CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:line_reverse_position"` + UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:line_reverse_position"` + DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:line_reverse_position"` + CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:line_reverse_position"` + UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:line_reverse_position"` +} + +func (m *LineReversePositionGetPageReq) GetNeedSearch() interface{} { + return *m +} + +type LineReversePositionInsertReq struct { + Id int `json:"-" comment:"主键"` // 主键 + ApiId int `json:"apiId" comment:"api_id"` + ReverseApiId int `json:"reverseApiId" comment:"反单api_id"` + ReverseAmount decimal.Decimal `json:"reverseAmount" comment:"反单仓位"` + Side string `json:"side" comment:"买卖方向 BUY SELL"` + PositionSide string `json:"positionSide" comment:"持仓方向 LONG SHORT"` + Symbol string `json:"symbol" comment:"交易对"` + Status int `json:"status" comment:"仓位状态 1-已开仓 2-已平仓"` + common.ControlBy +} + +func (s *LineReversePositionInsertReq) Generate(model *models.LineReversePosition) { + if s.Id == 0 { + model.Model = common.Model{Id: s.Id} + } + model.ApiId = s.ApiId + model.ReverseApiId = s.ReverseApiId + model.ReverseAmount = s.ReverseAmount + model.Side = s.Side + model.PositionSide = s.PositionSide + model.Symbol = s.Symbol + model.Status = s.Status + model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的 +} + +func (s *LineReversePositionInsertReq) GetId() interface{} { + return s.Id +} + +type LineReversePositionUpdateReq struct { + Id int `uri:"id" comment:"主键"` // 主键 + ApiId int `json:"apiId" comment:"api_id"` + ReverseApiId int `json:"reverseApiId" comment:"反单api_id"` + ReverseAmount decimal.Decimal `json:"reverseAmount" comment:"反单仓位"` + Side string `json:"side" comment:"买卖方向 BUY SELL"` + PositionSide string `json:"positionSide" comment:"持仓方向 LONG SHORT"` + Symbol string `json:"symbol" comment:"交易对"` + Status int `json:"status" comment:"仓位状态 1-已开仓 2-已平仓"` + common.ControlBy +} + +func (s *LineReversePositionUpdateReq) Generate(model *models.LineReversePosition) { + if s.Id == 0 { + model.Model = common.Model{Id: s.Id} + } + model.ApiId = s.ApiId + model.ReverseApiId = s.ReverseApiId + model.ReverseAmount = s.ReverseAmount + model.Side = s.Side + model.PositionSide = s.PositionSide + model.Symbol = s.Symbol + model.Status = s.Status + model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的 +} + +func (s *LineReversePositionUpdateReq) GetId() interface{} { + return s.Id +} + +// LineReversePositionGetReq 功能获取请求参数 +type LineReversePositionGetReq struct { + Id int `uri:"id"` +} + +func (s *LineReversePositionGetReq) GetId() interface{} { + return s.Id +} + +// LineReversePositionDeleteReq 功能删除请求参数 +type LineReversePositionDeleteReq struct { + Ids []int `json:"ids"` +} + +func (s *LineReversePositionDeleteReq) GetId() interface{} { + return s.Ids +} diff --git a/app/admin/service/dto/line_reverse_setting.go b/app/admin/service/dto/line_reverse_setting.go index a168bfc..51a4a90 100644 --- a/app/admin/service/dto/line_reverse_setting.go +++ b/app/admin/service/dto/line_reverse_setting.go @@ -32,6 +32,8 @@ type LineReverseSettingInsertReq struct { Id int `json:"-" comment:"主键id"` // 主键id ReverseOrderType string `json:"reverseOrderType" comment:"反单下单类型 LIMIT-限价 MARKET-市价"` ReversePremiumRatio decimal.Decimal `json:"reversePremiumRatio" comment:"溢价百分比"` + TakeProfitRatio decimal.Decimal `json:"takeProfitRatio" comment:"止盈百分比"` + StopLossRatio decimal.Decimal `json:"stopLossRatio" comment:"止损百分比"` common.ControlBy } @@ -41,6 +43,8 @@ func (s *LineReverseSettingInsertReq) Generate(model *models.LineReverseSetting) } model.ReverseOrderType = s.ReverseOrderType model.ReversePremiumRatio = s.ReversePremiumRatio + model.StopLossRatio = s.StopLossRatio + model.TakeProfitRatio = s.TakeProfitRatio model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的 } @@ -52,6 +56,7 @@ type LineReverseSettingUpdateReq struct { Id int `uri:"id" comment:"主键id"` // 主键id ReverseOrderType string `json:"reverseOrderType" comment:"反单下单类型 LIMIT-限价 MARKET-市价"` ReversePremiumRatio decimal.Decimal `json:"reversePremiumRatio" comment:"溢价百分比"` + TakeProfitRatio decimal.Decimal `json:"takeProfitRatio" comment:"止盈百分比"` StopLossRatio decimal.Decimal `json:"stopLossRatio" comment:"止损百分比"` common.ControlBy } @@ -63,6 +68,7 @@ func (s *LineReverseSettingUpdateReq) Generate(model *models.LineReverseSetting) model.ReverseOrderType = s.ReverseOrderType model.ReversePremiumRatio = s.ReversePremiumRatio model.StopLossRatio = s.StopLossRatio + model.TakeProfitRatio = s.TakeProfitRatio model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的 } diff --git a/app/admin/service/line_api_user.go b/app/admin/service/line_api_user.go index 577abe9..107afc9 100644 --- a/app/admin/service/line_api_user.go +++ b/app/admin/service/line_api_user.go @@ -8,6 +8,7 @@ import ( "github.com/bytedance/sonic" "github.com/go-admin-team/go-admin-core/logger" "github.com/go-admin-team/go-admin-core/sdk/service" + "github.com/go-redis/redis/v8" "gorm.io/gorm" "go-admin/app/admin/models" @@ -25,6 +26,23 @@ type LineApiUser struct { service.Service } +// 获取可以绑定的api列表 +func (e LineApiUser) GetReverseApiOptions(req *dto.GetReverseApiOptionsReq, user *[]models.LineApiUser) error { + query := e.Orm.Model(models.LineApiUser{}). + Where("subordinate ='0' and id != ?", req.Id) + + if req.ApiId > 0 { + query = query.Or(" id =?", req.ApiId) + } + + if err := query.Find(user).Error; err != nil { + e.Log.Errorf("LineApiUserService GetReverseApiOptions error:%s \r\n", err) + return err + } + + return nil +} + // GetPage 获取LineApiUser列表 func (e *LineApiUser) GetPage(c *dto.LineApiUserGetPageReq, p *actions.DataPermission, list *[]models.LineApiUser, count *int64) error { var err error @@ -107,7 +125,7 @@ func (e *LineApiUser) Insert(c *dto.LineApiUserInsertReq) error { return errors.New("反向api已被使用") } - if err2 := tx.Model(models.LineApiUser{}).Where("id =?", c.ReverseApiId).Update("subordinate", 2).Error; err2 != nil { + if err2 := tx.Model(models.LineApiUser{}).Where("id =?", c.ReverseApiId).Update("subordinate", '2').Error; err2 != nil { return err2 } data.Subordinate = "1" @@ -133,6 +151,9 @@ func (e *LineApiUser) Insert(c *dto.LineApiUserInsertReq) error { } } + if err2 := e.CacheRelation(); err2 != nil { + return err2 + } return nil } @@ -170,6 +191,33 @@ 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 { + 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) + } + } + + return nil +} + /* 打开用户websocket订阅 */ @@ -292,6 +340,10 @@ func (e *LineApiUser) Update(c *dto.LineApiUserUpdateReq, p *actions.DataPermiss } } + if err2 := e.CacheRelation(); err2 != nil { + return err2 + } + return nil } @@ -338,10 +390,14 @@ func (e *LineApiUser) Remove(d *dto.LineApiUserDeleteReq, p *actions.DataPermiss _, err := helper.DefaultRedis.BatchDeleteKeys(delKeys) - if err != nil { + if err != nil && !errors.Is(err, redis.Nil) { e.Log.Error("批量删除api_user key失败", err) } + if err2 := e.CacheRelation(); err2 != nil { + return err2 + } + return nil } diff --git a/app/admin/service/line_reverse_order_ext.go b/app/admin/service/line_reverse_position.go similarity index 61% rename from app/admin/service/line_reverse_order_ext.go rename to app/admin/service/line_reverse_position.go index 36621c1..a073154 100644 --- a/app/admin/service/line_reverse_order_ext.go +++ b/app/admin/service/line_reverse_position.go @@ -12,14 +12,14 @@ import ( cDto "go-admin/common/dto" ) -type LineReverseOrderExt struct { +type LineReversePosition struct { service.Service } -// GetPage 获取LineReverseOrderExt列表 -func (e *LineReverseOrderExt) GetPage(c *dto.LineReverseOrderExtGetPageReq, p *actions.DataPermission, list *[]models.LineReverseOrderExt, count *int64) error { +// GetPage 获取LineReversePosition列表 +func (e *LineReversePosition) GetPage(c *dto.LineReversePositionGetPageReq, p *actions.DataPermission, list *[]models.LineReversePosition, count *int64) error { var err error - var data models.LineReverseOrderExt + var data models.LineReversePosition err = e.Orm.Model(&data). Scopes( @@ -30,15 +30,15 @@ func (e *LineReverseOrderExt) GetPage(c *dto.LineReverseOrderExtGetPageReq, p *a Find(list).Limit(-1).Offset(-1). Count(count).Error if err != nil { - e.Log.Errorf("LineReverseOrderExtService GetPage error:%s \r\n", err) + e.Log.Errorf("LineReversePositionService GetPage error:%s \r\n", err) return err } return nil } -// Get 获取LineReverseOrderExt对象 -func (e *LineReverseOrderExt) Get(d *dto.LineReverseOrderExtGetReq, p *actions.DataPermission, model *models.LineReverseOrderExt) error { - var data models.LineReverseOrderExt +// Get 获取LineReversePosition对象 +func (e *LineReversePosition) Get(d *dto.LineReversePositionGetReq, p *actions.DataPermission, model *models.LineReversePosition) error { + var data models.LineReversePosition err := e.Orm.Model(&data). Scopes( @@ -47,7 +47,7 @@ func (e *LineReverseOrderExt) Get(d *dto.LineReverseOrderExtGetReq, p *actions.D First(model, d.GetId()).Error if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { err = errors.New("查看对象不存在或无权查看") - e.Log.Errorf("Service GetLineReverseOrderExt error:%s \r\n", err) + e.Log.Errorf("Service GetLineReversePosition error:%s \r\n", err) return err } if err != nil { @@ -57,23 +57,23 @@ func (e *LineReverseOrderExt) Get(d *dto.LineReverseOrderExtGetReq, p *actions.D return nil } -// Insert 创建LineReverseOrderExt对象 -func (e *LineReverseOrderExt) Insert(c *dto.LineReverseOrderExtInsertReq) error { +// Insert 创建LineReversePosition对象 +func (e *LineReversePosition) Insert(c *dto.LineReversePositionInsertReq) error { var err error - var data models.LineReverseOrderExt + var data models.LineReversePosition c.Generate(&data) err = e.Orm.Create(&data).Error if err != nil { - e.Log.Errorf("LineReverseOrderExtService Insert error:%s \r\n", err) + e.Log.Errorf("LineReversePositionService Insert error:%s \r\n", err) return err } return nil } -// Update 修改LineReverseOrderExt对象 -func (e *LineReverseOrderExt) Update(c *dto.LineReverseOrderExtUpdateReq, p *actions.DataPermission) error { +// Update 修改LineReversePosition对象 +func (e *LineReversePosition) Update(c *dto.LineReversePositionUpdateReq, p *actions.DataPermission) error { var err error - var data = models.LineReverseOrderExt{} + var data = models.LineReversePosition{} e.Orm.Scopes( actions.Permission(data.TableName(), p), ).First(&data, c.GetId()) @@ -81,7 +81,7 @@ func (e *LineReverseOrderExt) Update(c *dto.LineReverseOrderExtUpdateReq, p *act db := e.Orm.Save(&data) if err = db.Error; err != nil { - e.Log.Errorf("LineReverseOrderExtService Save error:%s \r\n", err) + e.Log.Errorf("LineReversePositionService Save error:%s \r\n", err) return err } if db.RowsAffected == 0 { @@ -90,16 +90,16 @@ func (e *LineReverseOrderExt) Update(c *dto.LineReverseOrderExtUpdateReq, p *act return nil } -// Remove 删除LineReverseOrderExt -func (e *LineReverseOrderExt) Remove(d *dto.LineReverseOrderExtDeleteReq, p *actions.DataPermission) error { - var data models.LineReverseOrderExt +// Remove 删除LineReversePosition +func (e *LineReversePosition) Remove(d *dto.LineReversePositionDeleteReq, p *actions.DataPermission) error { + var data models.LineReversePosition db := e.Orm.Model(&data). Scopes( actions.Permission(data.TableName(), p), ).Delete(&data, d.GetId()) if err := db.Error; err != nil { - e.Log.Errorf("Service RemoveLineReverseOrderExt error:%s \r\n", err) + e.Log.Errorf("Service RemoveLineReversePosition error:%s \r\n", err) return err } if db.RowsAffected == 0 { diff --git a/app/admin/service/line_reverse_setting.go b/app/admin/service/line_reverse_setting.go index 544e795..a564b18 100644 --- a/app/admin/service/line_reverse_setting.go +++ b/app/admin/service/line_reverse_setting.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" "gorm.io/gorm" "go-admin/app/admin/models" "go-admin/app/admin/service/dto" "go-admin/common/actions" + "go-admin/common/const/rediskey" cDto "go-admin/common/dto" + "go-admin/common/helper" ) type LineReverseSetting struct { @@ -59,35 +61,60 @@ func (e *LineReverseSetting) Get(d *dto.LineReverseSettingGetReq, p *actions.Dat // Insert 创建LineReverseSetting对象 func (e *LineReverseSetting) Insert(c *dto.LineReverseSettingInsertReq) error { - var err error - var data models.LineReverseSetting - c.Generate(&data) + var err error + var data models.LineReverseSetting + c.Generate(&data) err = e.Orm.Create(&data).Error if err != nil { e.Log.Errorf("LineReverseSettingService Insert error:%s \r\n", err) return err } + + err = e.SaveCache(data) + if err != nil { + return err + } + return nil +} + +// 缓存数据 +func (e *LineReverseSetting) SaveCache(data models.LineReverseSetting) error { + if data.Id == 0 { + if err := e.Orm.First(&data).Error; err != nil { + return err + } + } + + if err := helper.DefaultRedis.SetHashWithTags(rediskey.ReverseSetting, &data); err != nil { + e.Log.Errorf("redis error:%s", err) + return err + } return nil } // Update 修改LineReverseSetting对象 func (e *LineReverseSetting) Update(c *dto.LineReverseSettingUpdateReq, p *actions.DataPermission) error { - var err error - var data = models.LineReverseSetting{} - e.Orm.Scopes( - actions.Permission(data.TableName(), p), - ).First(&data, c.GetId()) - c.Generate(&data) + var err error + var data = models.LineReverseSetting{} + 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("LineReverseSettingService 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("LineReverseSettingService Save error:%s \r\n", err) + return err + } + if db.RowsAffected == 0 { + return errors.New("无权更新该数据") + } + if err = helper.DefaultRedis.SetHashWithTags(rediskey.ReverseSetting, data); err != nil { + e.Log.Errorf("redis error:%s", err) + return err + } + + return nil } // Remove 删除LineReverseSetting @@ -99,11 +126,11 @@ func (e *LineReverseSetting) Remove(d *dto.LineReverseSettingDeleteReq, p *actio actions.Permission(data.TableName(), p), ).Delete(&data, d.GetId()) if err := db.Error; err != nil { - e.Log.Errorf("Service RemoveLineReverseSetting error:%s \r\n", err) - return err - } - if db.RowsAffected == 0 { - return errors.New("无权删除该数据") - } + e.Log.Errorf("Service RemoveLineReverseSetting error:%s \r\n", err) + return err + } + if db.RowsAffected == 0 { + return errors.New("无权删除该数据") + } return nil } diff --git a/cmd/usersubscribe/usersubscribe.go b/cmd/usersubscribe/usersubscribe.go index efb1da7..87d9e7b 100644 --- a/cmd/usersubscribe/usersubscribe.go +++ b/cmd/usersubscribe/usersubscribe.go @@ -86,6 +86,11 @@ func run() error { clearLogJob(db, ctx) }) + //自动重启websocket + utility.SafeGo(func() { + reconnect(ctx) + }) + // 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间) quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt) @@ -133,3 +138,33 @@ func clearLogJob(db *gorm.DB, ctx context.Context) { fileservice.ClearLogs(db) } } + +// 定时重连websocket +func reconnect(ctx context.Context) error { + ticker := time.NewTicker(time.Hour * 1) + defer ticker.Stop() + + select { + case <-ctx.Done(): + return nil + case <-ticker.C: + serverinit.RestartConnect() + } + + return nil +} + +// StartDeadCheck 假死检测 +func StartDeadCheck(ctx context.Context) { + ticker := time.NewTicker(1 * time.Minute) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + serverinit.DeadCheck() + } + } +} diff --git a/common/const/binancecode/event.go b/common/const/binancecode/event.go new file mode 100644 index 0000000..4410fec --- /dev/null +++ b/common/const/binancecode/event.go @@ -0,0 +1,23 @@ +package binancecode + +// NEW +// CANCELED 已撤 +// CALCULATED 订单 ADL 或爆仓 +// EXPIRED 订单失效 +// TRADE 交易 +// AMENDMENT 订单修改 + +const ( + //新开单 + EVENT_NEW = "NEW" + //已撤单 + EVENT_CANCELED = "CANCELED" + //订单 ADL 或爆仓 + EVENT_CALCULATED = "CALCULATED" + //订单失效 + EVENT_EXPIRED = "EXPIRED" + //交易中 + EVENT_TRADE = "TRADE" + //订单修改 + EVENT_AMENDMENT = "AMENDMENT" +) diff --git a/common/const/binancecode/order_type.go b/common/const/binancecode/order_type.go new file mode 100644 index 0000000..4945bfa --- /dev/null +++ b/common/const/binancecode/order_type.go @@ -0,0 +1,20 @@ +package binancecode + +const ( + //限价单 + ORDER_TYPE_LIMIT = "LIMIT" + //市价单 + ORDER_TYPE_MARKET = "MARKET" + //止损限价单 + ORDER_TYPE_STOP = "STOP" + //止损市价单 + ORDER_TYPE_STOP_MARKET = "STOP_MARKET" + //止盈限价单 + ORDER_TYPE_TAKE_PROFIT = "TAKE_PROFIT" + //止盈市价单 + ORDER_TYPE_TAKE_PROFIT_MARKET = "TAKE_PROFIT_MARKET" + //跟踪止损单 + ORDER_TYPE_TRAILING_STOP_MARKET = "TRAILING_STOP_MARKET" + //爆仓 + ORDER_TYPE_LIQUIDATION = "LIQUIDATION" +) diff --git a/common/const/binancecode/side.go b/common/const/binancecode/side.go new file mode 100644 index 0000000..b34762a --- /dev/null +++ b/common/const/binancecode/side.go @@ -0,0 +1,6 @@ +package binancecode + +const ( + SideBuy = "BUY" + SideSell = "SELL" +) diff --git a/common/const/rediskey/api_user.go b/common/const/rediskey/api_user.go new file mode 100644 index 0000000..d3a0e47 --- /dev/null +++ b/common/const/rediskey/api_user.go @@ -0,0 +1,6 @@ +package rediskey + +const ( + //主单api和反单关系 List {apiId}:{reverseApiId} + ApiReverseRelation = "api_reverse_relation" +) diff --git a/common/const/rediskey/reverse_position.go b/common/const/rediskey/reverse_position.go new file mode 100644 index 0000000..c99fe33 --- /dev/null +++ b/common/const/rediskey/reverse_position.go @@ -0,0 +1,11 @@ +package rediskey + +const ( + //反单仓位管理 {apiKey} hash key + ReversePositionKey = "reverse_position:%s" +) + +const ( + //反单配置 hash {order_type} {take_profit_ratio} {stop_loss_ratio} {reverse_premium_ratio} + ReverseSetting = "reverse_setting" +) diff --git a/common/helper/redis_helper.go b/common/helper/redis_helper.go index 0ce5ba6..1202525 100644 --- a/common/helper/redis_helper.go +++ b/common/helper/redis_helper.go @@ -12,6 +12,7 @@ import ( "github.com/bytedance/sonic" "github.com/go-redis/redis/v8" + "github.com/shopspring/decimal" ) // RedisHelper 结构体封装了 Redis 客户端及上下文 @@ -460,44 +461,85 @@ func (r *RedisHelper) SetNX(key string, value interface{}, expiration time.Durat return result == "OK", nil } -func getFieldsFromStruct(obj interface{}) map[string]interface{} { +// 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) + } + } + + cmd := r.client.HSet(r.ctx, key, fields) + return cmd.Err() +} + +// getFieldsFromStruct 改进版:处理指针并进行类型检查,返回 error +func getFieldsFromStruct(obj interface{}) (map[string]interface{}, error) { fields := make(map[string]interface{}) val := reflect.ValueOf(obj) - typ := reflect.TypeOf(obj) + + // 如果 obj 是指针,则解引用 + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + // 确保我们正在处理的是一个结构体 + if val.Kind() != reflect.Struct { + return nil, fmt.Errorf("期望一个结构体或结构体指针,但得到的是 %s", val.Kind()) + } + + typ := val.Type() // 在解引用后获取类型 for i := 0; i < val.NumField(); i++ { field := typ.Field(i) - tag := field.Tag.Get("redis") + tag := field.Tag.Get("redis") // 获取 redis tag if tag != "" { fieldVal := val.Field(i) - if fieldVal.Kind() == reflect.Slice || fieldVal.Kind() == reflect.Map { + // 检查字段是否可导出,不可导出的字段无法直接通过 Interface() 访问 + if !fieldVal.CanInterface() { + continue // 跳过不可导出字段 + } + + switch fieldVal.Kind() { + case reflect.Slice, reflect.Map: // 处理切片或映射类型 - // 对于切片,使用索引作为字段名 if fieldVal.Kind() == reflect.Slice { for j := 0; j < fieldVal.Len(); j++ { elem := fieldVal.Index(j).Interface() fields[fmt.Sprintf("%s_%d", tag, j)] = elem } } else if fieldVal.Kind() == reflect.Map { - // 对于映射,使用键作为字段名 for _, key := range fieldVal.MapKeys() { elem := fieldVal.MapIndex(key).Interface() fields[fmt.Sprintf("%s_%v", tag, key.Interface())] = elem } } - } else { + case reflect.Struct: + if fieldVal.Type() == reflect.TypeOf(decimal.Decimal{}) { + if decVal, ok := fieldVal.Interface().(decimal.Decimal); ok { + // 将 decimal.Decimal 直接转换为字符串来保留精度 + fields[tag] = decVal.String() + } else { + // 理论上不应该发生,但作为回退 + fields[tag] = fieldVal.Interface() + } + } else { + fields[tag] = fieldVal.Interface() + } + default: fields[tag] = fieldVal.Interface() } } } - - return fields -} - -func (r *RedisHelper) SetHashWithTags(key string, obj interface{}) error { - fields := getFieldsFromStruct(obj) - _, err := r.client.HSet(r.ctx, key, fields).Result() - return err + return fields, nil } // HSetField 设置哈希中的一个字段 @@ -543,6 +585,82 @@ func (r *RedisHelper) HGetAllFields(key string) (map[string]string, error) { return fields, nil } +// HGetAsObject 获取哈希中所有字段的值并反序列化为对象 +func (r *RedisHelper) HGetAsObject(key string, out interface{}) error { + data, err := r.HGetAllFields(key) + if err != nil { + return err + } + + val := reflect.ValueOf(out) + if val.Kind() != reflect.Ptr || val.IsNil() { + return fmt.Errorf("output must be a non-nil pointer to a struct") + } + val = val.Elem() + typ := val.Type() + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + tag := field.Tag.Get("redis") + if tag == "" { + continue + } + strVal, ok := data[tag] + if !ok { + continue + } + + fieldVal := val.Field(i) + if !fieldVal.CanSet() { + continue + } + + switch fieldVal.Kind() { + case reflect.String: + fieldVal.SetString(strVal) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + if intVal, err := strconv.ParseInt(strVal, 10, 64); err == nil { + fieldVal.SetInt(intVal) + } + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + if uintVal, err := strconv.ParseUint(strVal, 10, 64); err == nil { + fieldVal.SetUint(uintVal) + } + case reflect.Float32, reflect.Float64: + if floatVal, err := strconv.ParseFloat(strVal, 64); err == nil { + fieldVal.SetFloat(floatVal) + } + case reflect.Bool: + if boolVal, err := strconv.ParseBool(strVal); err == nil { + fieldVal.SetBool(boolVal) + } + case reflect.Struct: + // 针对 time.Time 特别处理 + if fieldVal.Type() == reflect.TypeOf(time.Time{}) { + if t, err := time.Parse(time.RFC3339, strVal); err == nil { + fieldVal.Set(reflect.ValueOf(t)) + } + } else if fieldVal.Type() == reflect.TypeOf(decimal.Decimal{}) { + if t, err := decimal.NewFromString(strVal); err == nil { + fieldVal.Set(reflect.ValueOf(t)) + } + } else { + // 其他 struct 尝试用 json 反序列化 + ptr := reflect.New(fieldVal.Type()).Interface() + if err := sonic.Unmarshal([]byte(strVal), ptr); err == nil { + fieldVal.Set(reflect.ValueOf(ptr).Elem()) + } + } + case reflect.Slice, reflect.Map: + ptr := reflect.New(fieldVal.Type()).Interface() + if err := sonic.Unmarshal([]byte(strVal), ptr); err == nil { + fieldVal.Set(reflect.ValueOf(ptr).Elem()) + } + } + } + return nil +} + // HDelField 删除哈希中的某个字段 func (r *RedisHelper) HDelField(key, field string) error { _, err := r.client.HDel(r.ctx, key, field).Result() diff --git a/config/serverinit/business_init.go b/config/serverinit/business_init.go index 95a0518..7efe151 100644 --- a/config/serverinit/business_init.go +++ b/config/serverinit/business_init.go @@ -30,6 +30,24 @@ func BusinessInit(db *gorm.DB) { os.Exit(-1) } + lineApiService := service.LineApiUser{} + lineApiService.Orm = db + lineApiService.Log = logger.NewHelper(sdk.Runtime.GetLogger()).WithFields(map[string]interface{}{}) + + if err := lineApiService.CacheRelation(); err != nil { + lineApiService.Log.Errorf("初始化api关系失败 err:%v", err) + os.Exit(-1) + } + + reverseSettingService := service.LineReverseSetting{} + reverseSettingService.Orm = db + reverseSettingService.Log = lineApiService.Log + + if err := reverseSettingService.SaveCache(models.LineReverseSetting{}); err != nil { + reverseSettingService.Log.Errorf("初始化反单设置失败 err:%v", err) + os.Exit(-1) + } + //初始化参数配置 cacheservice.InitConfigCache(db) //初始化可缓存价格交易对 diff --git a/config/serverinit/usersubscribeinit.go b/config/serverinit/usersubscribeinit.go index 465e908..c58f569 100644 --- a/config/serverinit/usersubscribeinit.go +++ b/config/serverinit/usersubscribeinit.go @@ -84,3 +84,44 @@ func UserSubscribeInit(orm *gorm.DB, ctx context.Context) { } }) } + +// 重启连接 +func RestartConnect() error { + spotSockets := excservice.SpotSockets + futuresSockets := excservice.FutureSockets + timeOut := 22 * time.Hour + + for _, item := range spotSockets { + //超过22小时,重新连接 + if time.Since(item.ConnectTime) > timeOut { + if err := item.ReplaceConnection(); err != nil { + log.Errorf("现货重启连接失败 key:%s,error:%s", item.GetKey(), err) + } + } + } + + for _, item := range futuresSockets { + //超过22小时,重新连接 + if time.Since(item.ConnectTime) > timeOut { + if err := item.ReplaceConnection(); err != nil { + log.Errorf("合约重启连接失败 key:%s,error:%s", item.GetKey(), err) + } + } + } + + return nil +} + +// 假死重启 +func DeadCheck() { + spotSockets := excservice.SpotSockets + futuresSockets := excservice.FutureSockets + + for _, item := range spotSockets { + item.DeadCheck() + } + + for _, item := range futuresSockets { + item.DeadCheck() + } +} diff --git a/pkg/maphelper/maphelper.go b/pkg/maphelper/maphelper.go new file mode 100644 index 0000000..00a22e3 --- /dev/null +++ b/pkg/maphelper/maphelper.go @@ -0,0 +1,46 @@ +package maphelper + +import ( + "fmt" + + "github.com/shopspring/decimal" +) + +func GetString(m map[string]interface{}, key string) (string, error) { + val, ok := m[key] + if !ok { + return "", fmt.Errorf("参数错误,缺少字段 %s", key) + } + strVal, ok := val.(string) + if !ok { + return "", fmt.Errorf("字段 %s 类型错误", key) + } + return strVal, nil +} + +func GetFloat64(m map[string]interface{}, key string) (float64, error) { + val, ok := m[key] + if !ok { + return 0, fmt.Errorf("参数错误,缺少字段 %s", key) + } + f, ok := val.(float64) + if !ok { + return 0, fmt.Errorf("字段 %s 类型错误", key) + } + return f, nil +} + +func GetBool(m map[string]interface{}, key string) bool { + if val, ok := m[key].(bool); ok { + return val + } + return false +} + +func GetDecimal(m map[string]interface{}, key string) decimal.Decimal { + if val, ok := m[key].(string); ok { + d, _ := decimal.NewFromString(val) + return d + } + return decimal.Zero +} diff --git a/pkg/retryhelper/retryhelper.go b/pkg/retryhelper/retryhelper.go new file mode 100644 index 0000000..170757d --- /dev/null +++ b/pkg/retryhelper/retryhelper.go @@ -0,0 +1,54 @@ +package retryhelper + +import ( + "fmt" + "math" + "time" +) + +// RetryOptions 定义重试的配置项 +type RetryOptions struct { + MaxRetries int // 最大重试次数 + InitialInterval time.Duration // 初始重试间隔 + MaxInterval time.Duration // 最大重试间隔 + BackoffFactor float64 // 指数退避的增长因子 + RetryableErrFn func(error) bool // 用于判断是否为可重试错误的函数(可选) +} + +func DefaultRetryOptions() RetryOptions { + return RetryOptions{ + MaxRetries: 3, + InitialInterval: 150 * time.Millisecond, + MaxInterval: 3 * time.Second, + BackoffFactor: 2.0, + } +} + +// Retry 重试通用函数(用于没有返回值的函数) +func Retry(op func() error, opts RetryOptions) error { + _, err := RetryWithResult(func() (struct{}, error) { + return struct{}{}, op() + }, opts) + return err +} + +// RetryWithResult 重试通用函数(用于带返回值的函数) +func RetryWithResult[T any](op func() (T, error), opts RetryOptions) (result T, err error) { + interval := opts.InitialInterval + for attempt := 0; attempt <= opts.MaxRetries; attempt++ { + result, err = op() + if err == nil { + return result, nil + } + + if opts.RetryableErrFn != nil && !opts.RetryableErrFn(err) { + return result, err + } + + if attempt < opts.MaxRetries { + time.Sleep(interval) + 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) +} diff --git a/services/binanceservice/biance_reverse_futures_service.go b/services/binanceservice/biance_reverse_futures_service.go deleted file mode 100644 index eeefb69..0000000 --- a/services/binanceservice/biance_reverse_futures_service.go +++ /dev/null @@ -1,15 +0,0 @@ -package binanceservice - -import "github.com/go-admin-team/go-admin-core/sdk/service" - -type BinanceReverseFuturesService struct { - service.Service -} - -// -func (e *BinanceReverseFuturesService) DoReverse() error { - apiInfo, ok := ShouldReverse(apiKey) - //TODO: 实现反向开仓逻辑 - - return nil -} diff --git a/services/binanceservice/biance_reverse_spot_service.go b/services/binanceservice/biance_reverse_spot_service.go deleted file mode 100644 index 3abad63..0000000 --- a/services/binanceservice/biance_reverse_spot_service.go +++ /dev/null @@ -1,12 +0,0 @@ -package binanceservice - -import "github.com/go-admin-team/go-admin-core/sdk/service" - -type BinanceReverseSpotService struct { - service.Service -} - -// JudgeApiUser 判断是否需要 -func (e *BinanceReverseSpotService) JudgeApiUser() bool { - return false -} diff --git a/services/binanceservice/binance_reverse_service.go b/services/binanceservice/binance_reverse_service.go deleted file mode 100644 index 51d7d54..0000000 --- a/services/binanceservice/binance_reverse_service.go +++ /dev/null @@ -1,16 +0,0 @@ -package binanceservice - -import DbModels "go-admin/app/admin/models" - -// ShouldReverse 判断是否需要反单 -// return apiInfo, bool -func ShouldReverse(apiKey string) (DbModels.LineApiUser, bool) { - // TODO: 实现判断是否需要反单的逻辑 - apiInfo := GetApiInfoByKey(apiKey) - - if apiInfo.ReverseStatus == 1 && apiInfo.ReverseApiId > 0 { - return apiInfo, true - } - - return DbModels.LineApiUser{}, false -} diff --git a/services/binanceservice/futures_reset_v2.go b/services/binanceservice/futures_reset_v2.go new file mode 100644 index 0000000..3b800a0 --- /dev/null +++ b/services/binanceservice/futures_reset_v2.go @@ -0,0 +1,114 @@ +package binanceservice + +import ( + "fmt" + "strings" + + DbModels "go-admin/app/admin/models" + "go-admin/pkg/retryhelper" + + "github.com/bytedance/sonic" + log "github.com/go-admin-team/go-admin-core/logger" + "github.com/go-admin-team/go-admin-core/sdk/service" +) + +type FuturesResetV2 struct { + service.Service +} + +// 带重试机制的合约下单 +func (e *FuturesResetV2) OrderPlaceLoop(apiUserInfo *DbModels.LineApiUser, params FutOrderPlace) 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.OrderPlace(apiUserInfo, params) + }, opts) + + if err != nil { + return err + } + return nil +} + +// 合约下单 +func (e *FuturesResetV2) OrderPlace(apiUserInfo *DbModels.LineApiUser, params FutOrderPlace) error { + if err := params.CheckParams(); err != nil { + return err + } + + orderType := strings.ToUpper(params.OrderType) + side := strings.ToUpper(params.Side) + + paramsMaps := map[string]string{ + "symbol": params.Symbol, + "side": side, + "quantity": params.Quantity.String(), + "type": orderType, + "newClientOrderId": params.NewClientOrderId, + "positionSide": params.PositionSide, + } + + // 设置市价和限价等类型的参数 + switch orderType { + case "LIMIT": + paramsMaps["price"] = params.Price.String() + paramsMaps["timeInForce"] = "GTC" + case "TAKE_PROFIT_MARKET": + paramsMaps["timeInForce"] = "GTC" + paramsMaps["stopprice"] = params.Profit.String() + paramsMaps["workingType"] = "MARK_PRICE" + case "TAKE_PROFIT": + paramsMaps["price"] = params.Price.String() + paramsMaps["stopprice"] = params.Profit.String() + paramsMaps["timeInForce"] = "GTC" + case "STOP_MARKET": + paramsMaps["stopprice"] = params.StopPrice.String() + paramsMaps["workingType"] = "MARK_PRICE" + paramsMaps["timeInForce"] = "GTC" + case "STOP": + paramsMaps["price"] = params.Price.String() + paramsMaps["stopprice"] = params.StopPrice.String() + paramsMaps["workingType"] = "MARK_PRICE" + paramsMaps["timeInForce"] = "GTC" + } + + // 获取 API 信息和发送下单请求 + client := GetClient(apiUserInfo) + _, statusCode, err := client.SendFuturesRequestAuth("/fapi/v1/order", "POST", paramsMaps) + + if err != nil { + return parseOrderError(err, paramsMaps, statusCode, apiUserInfo) + } + return nil +} + +// 拆出的错误解析函数 +func parseOrderError(err error, paramsMaps map[string]string, statusCode int, apiUserInfo *DbModels.LineApiUser) error { + var dataMap map[string]interface{} + + if err2 := sonic.Unmarshal([]byte(err.Error()), &dataMap); err2 == nil { + if code, ok := dataMap["code"]; ok { + paramsVal, _ := sonic.MarshalString(¶msMaps) + log.Error("下单失败 参数:", paramsVal) + errContent := FutErrorMaps[code.(float64)] + if errContent == "" { + errContent = err.Error() + } + return fmt.Errorf("api_id:%d 交易对:%s 下单失败:%s", apiUserInfo.Id, paramsMaps["symbol"], errContent) + } + } + + // 特殊处理保证金不足 + if strings.Contains(err.Error(), "Margin is insufficient.") { + return fmt.Errorf("api_id:%d 交易对:%s 下单失败:%s", apiUserInfo.Id, paramsMaps["symbol"], FutErrorMaps[-2019]) + } + + return fmt.Errorf("api_id:%d 交易对:%s statusCode:%v 下单失败:%s", apiUserInfo.Id, paramsMaps["symbol"], statusCode, err.Error()) +} diff --git a/services/binanceservice/futuresbinancerest.go b/services/binanceservice/futuresbinancerest.go index bc53433..fadf352 100644 --- a/services/binanceservice/futuresbinancerest.go +++ b/services/binanceservice/futuresbinancerest.go @@ -12,6 +12,7 @@ import ( "go-admin/models/binancedto" "go-admin/models/futuresdto" "go-admin/pkg/httputils" + "go-admin/pkg/retryhelper" "go-admin/pkg/utility" "strconv" "strings" @@ -715,6 +716,15 @@ func (e FutRestApi) ClosePosition(symbol string, orderSn string, quantity decima return nil } +// 带重试机制的取消订单 +func (e FutRestApi) CancelFutOrderRetry(apiUserInfo DbModels.LineApiUser, symbol string, newClientOrderId string) error { + opts := retryhelper.DefaultRetryOptions() + + return retryhelper.Retry(func() error { + return e.CancelFutOrder(apiUserInfo, symbol, newClientOrderId) + }, opts) +} + // CancelFutOrder 通过单个订单号取消合约委托 // symbol 交易对 // newClientOrderId 系统自定义订单号 diff --git a/services/binanceservice/futuresrest.go b/services/binanceservice/futuresrest.go index 81cdd8e..544ffde 100644 --- a/services/binanceservice/futuresrest.go +++ b/services/binanceservice/futuresrest.go @@ -27,7 +27,7 @@ import ( /* 修改订单信息 */ -func ChangeFutureOrder(mapData map[string]interface{}) { +func ChangeFutureOrder(mapData map[string]interface{}, apiKey string) { // 检查订单号是否存在 orderSn, ok := mapData["c"] originOrderSn := mapData["C"] //取消操作 代表原始订单号 @@ -61,34 +61,45 @@ func ChangeFutureOrder(mapData map[string]interface{}) { } defer lock.Release() - // 查询订单 - preOrder, err := getPreOrder(db, orderSn) + //反单逻辑 + reverseService := ReverseService{} + reverseService.Orm = db + reverseService.Log = logger.NewHelper(logger.DefaultLogger) + _, err = reverseService.ReverseOrder(apiKey, mapData) + if err != nil { - logger.Error("合约订单回调失败,查询订单失败:", orderSn, " err:", err) - return + logger.Errorf("合约订单回调失败,反单失败:%v", err) } - // 解析订单状态 - status, ok := mapData["X"].(string) - if !ok { - mapStr, _ := sonic.Marshal(&mapData) - logger.Error("订单回调失败,没有状态:", string(mapStr)) - return - } - // 更新订单状态 - orderStatus, reason := parseOrderStatus(status, mapData) + // 以前的下单逻辑 + // 查询订单 + // preOrder, err := getPreOrder(db, orderSn) + // if err != nil { + // logger.Error("合约订单回调失败,查询订单失败:", orderSn, " err:", err) + // return + // } - if orderStatus == 0 { - logger.Error("订单回调失败,状态错误:", orderSn, " status:", status, " reason:", reason) - return - } + // // 解析订单状态 + // status, ok := mapData["X"].(string) + // if !ok { + // mapStr, _ := sonic.Marshal(&mapData) + // logger.Error("订单回调失败,没有状态:", string(mapStr)) + // return + // } + // // 更新订单状态 + // orderStatus, reason := parseOrderStatus(status, mapData) - if err := updateOrderStatus(db, preOrder, orderStatus, reason, true, mapData); err != nil { - logger.Error("修改订单状态失败:", orderSn, " err:", err) - return - } + // if orderStatus == 0 { + // logger.Error("订单回调失败,状态错误:", orderSn, " status:", status, " reason:", reason) + // return + // } - handleFutOrderByType(db, preOrder, orderStatus) + // 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 c2d04a5..54f320e 100644 --- a/services/binanceservice/models.go +++ b/services/binanceservice/models.go @@ -61,16 +61,17 @@ type EntryPriceResult struct { } type FutOrderPlace struct { - ApiId int `json:"api_id"` //api用户id - Symbol string `json:"symbol"` //合约交易对 - Side string `json:"side"` //购买方向 - Quantity decimal.Decimal `json:"quantity"` //数量 - Price decimal.Decimal `json:"price"` //限价单价 - SideType string `json:"side_type"` //现价或者市价 - OpenOrder int `json:"open_order"` //是否开启限价单止盈止损 - Profit decimal.Decimal `json:"profit"` //止盈价格 - StopPrice decimal.Decimal `json:"stopprice"` //止损价格 - OrderType string `json:"order_type"` //订单类型,市价或限价MARKET(市价单) TAKE_PROFIT_MARKET(市价止盈) TAKE_PROFIT(限价止盈) STOP (限价止损) STOP_MARKET(市价止损) + ApiId int `json:"api_id"` //api用户id + Symbol string `json:"symbol"` //合约交易对 + Side string `json:"side"` //购买方向 + PositionSide string `json:"position_side"` //持仓方向 + Quantity decimal.Decimal `json:"quantity"` //数量 + Price decimal.Decimal `json:"price"` //限价单价 + SideType string `json:"side_type"` //现价或者市价 + OpenOrder int `json:"open_order"` //是否开启限价单止盈止损 + Profit decimal.Decimal `json:"profit"` //止盈价格 + StopPrice decimal.Decimal `json:"stopprice"` //止损价格 + OrderType string `json:"order_type"` //订单类型,市价或限价MARKET(市价单) TAKE_PROFIT_MARKET(市价止盈) TAKE_PROFIT(限价止盈) STOP (限价止损) STOP_MARKET(市价止损) NewClientOrderId string `json:"newClientOrderId"` } diff --git a/services/binanceservice/reverse_service.go b/services/binanceservice/reverse_service.go new file mode 100644 index 0000000..1101e68 --- /dev/null +++ b/services/binanceservice/reverse_service.go @@ -0,0 +1,623 @@ +package binanceservice + +import ( + "database/sql" + "errors" + "fmt" + DbModels "go-admin/app/admin/models" + "go-admin/common/global" + "go-admin/common/helper" + "go-admin/pkg/maphelper" + "go-admin/services/cacheservice" + "strconv" + "time" + + "github.com/go-admin-team/go-admin-core/sdk/service" + "github.com/shopspring/decimal" + "gorm.io/gorm" +) + +type ReverseService struct { + service.Service +} + +// SaveMainOrder 保存订单信息 +// mapData: 下单数据 +// status: 订单状态 +// orderSn: 自定义订单号 +func (e *ReverseService) handleSimpleStatusChange(status string, orderSn string, mapData map[string]interface{}) (bool, error) { + statusMap := map[string]int{ + "NEW": 2, + "CANCELED": 6, + "EXPIRED": 7, + "EXPIRED_IN_MATCH": 7, + } + if newStatus, ok := statusMap[status]; ok { + _ = e.changeOrderStatus(newStatus, orderSn, mapData) + return false, nil + } + return false, fmt.Errorf("不支持的订单状态 %s", status) +} + +// ReverseOrder 反向下单 +// apiKey: api key +// mapData: 下单数据 +// return apiInfo, bool +// bool: true=需要反单, false=不需要反单 +func (e *ReverseService) ReverseOrder(apiKey string, mapData map[string]interface{}) (bool, error) { + apiInfo := GetApiInfoByKey(apiKey) + + if apiInfo.Id == 0 { + e.Log.Errorf("获取apiInfo失败 %s", apiKey) + return false, nil + } + + status, _ := maphelper.GetString(mapData, "X") + orderSn, err := maphelper.GetString(mapData, "c") + if err != nil { + return true, err + } + ot, err := maphelper.GetString(mapData, "ot") + + if err != nil { + return true, err + } + + switch status { + case "NEW", "CANCELED", "EXPIRED", "EXPIRED_IN_MATCH": + 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") && + apiInfo.ReverseStatus == 1 && apiInfo.OpenStatus == 1 && apiInfo.ReverseApiId > 0 { + + if err := e.ReTakeOrStopOrder(&mapData, orderSn, &apiInfo); err != nil { + return true, err + } + } + + return result, err + case "FILLED": + if apiInfo.ReverseStatus == 1 && apiInfo.OpenStatus == 1 && apiInfo.ReverseApiId > 0 { + reverseApiInfo, err := GetApiInfo(apiInfo.ReverseApiId) + if err != nil { + e.Log.Errorf("获取反向api信息失败 reverseApiId:%d, err:%v", apiInfo.ReverseApiId, err) + return true, err + } + + mainOrder, err := e.SaveMainOrder(mapData, apiInfo) + if err != nil { + return false, err + } + + 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 { + return true, err1 + } + + e.DoAddReverseOrder(&mainOrder, &reverseApiInfo, apiInfo.OrderProportion, false, false) + } + 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 { + e.Log.Errorf("保存主订单失败: %v", err1) + return true, err1 + } + e.DoAddReverseOrder(&mainOrder, &reverseApiInfo, apiInfo.OrderProportion, true, closePosition) + } + default: + return true, errors.New("不支持的订单类型") + } + return true, nil + } else if apiInfo.Subordinate == "2" { + symbol, err := maphelper.GetString(mapData, "s") + + if err != nil { + return true, err + } + positionSide, err := maphelper.GetString(mapData, "ps") + + if err != nil { + return true, err + } + side, err := maphelper.GetString(mapData, "S") + if err != nil { + return true, err + } + + totalNum := maphelper.GetDecimal(mapData, "z") + + mainOrder := DbModels.LineReverseOrder{ + ApiId: apiInfo.Id, + Symbol: symbol, + PositionSide: positionSide, + TotalNum: totalNum, + Side: side, + } + 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 { + return true, err1 + } + } + 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, 0, false, true, closePosition); err1 != nil { + return true, err1 + } + } + default: + return true, errors.New("不支持的订单类型") + } + } + default: + return false, fmt.Errorf("不支持的订单状态 %s", status) + } + + return false, nil +} + +// 修改订单状态 +// status: 订单状态 1-待下单 2-已下单 3-已成交 6-已取消 7-已过期 +func (e *ReverseService) changeOrderStatus(status int, orderSn string, mapData map[string]interface{}) error { + data := map[string]interface{}{"status": status, "updated_at": time.Now()} + + if status == 3 { + now := time.Now() + if orderId, ok := mapData["i"].(float64); ok { + data["order_id"] = orderId + } + + if ap, ok := mapData["ap"].(bool); ok { + data["final_price"] = ap + } + + if num, ok := mapData["z"].(string); ok { + data["total_num"], _ = decimal.NewFromString(num) + } + + data["trigger_time"] = &now + } + + db := e.Orm.Model(&DbModels.LineReverseOrder{}). + Where("order_sn =? and status != 3", orderSn). + Updates(data) + + if db.Error != nil { + e.Log.Errorf("修改订单状态失败 orderSn:%s, err:%v", orderSn, db.Error) + } + + if db.RowsAffected == 0 { + e.Log.Errorf("修改订单状态失败 orderSn:%s, 未找到订单", orderSn) + } + return db.Error +} + +// 先保存主单,持仓信息 必须用双向持仓! +func (e *ReverseService) SaveMainOrder(mapData map[string]interface{}, apiInfo DbModels.LineApiUser) (DbModels.LineReverseOrder, error) { + now := time.Now() + symbol, err := maphelper.GetString(mapData, "s") + var reverseOrder DbModels.LineReverseOrder + if err != nil { + return reverseOrder, err + } + + orderSn, err := maphelper.GetString(mapData, "c") + if err != nil { + return reverseOrder, err + } + + side, err := maphelper.GetString(mapData, "S") + if err != nil { + return reverseOrder, err + } + + positionSide, err := maphelper.GetString(mapData, "ps") + if err != nil { + return reverseOrder, err + } + + mainType, _ := maphelper.GetString(mapData, "ot") + + reverseOrder = DbModels.LineReverseOrder{ + ApiId: apiInfo.Id, + Category: 0, + OrderSn: orderSn, + TriggerTime: &now, + Status: 3, + Symbol: symbol, + FollowOrderSn: "", + Type: mainType, + Price: maphelper.GetDecimal(mapData, "p"), + FinalPrice: maphelper.GetDecimal(mapData, "ap"), + TotalNum: maphelper.GetDecimal(mapData, "z"), + Side: side, + PositionSide: positionSide, + } + + reverseOrder.PriceU = reverseOrder.Price + + if !reverseOrder.Price.IsZero() && !reverseOrder.TotalNum.IsZero() { + reverseOrder.BuyPrice = reverseOrder.Price.Mul(reverseOrder.TotalNum) + } + + if id, err := maphelper.GetFloat64(mapData, "i"); err == nil { + reverseOrder.OrderId = strconv.FormatFloat(id, 'f', -1, 64) + } + + orderType, _ := maphelper.GetString(mapData, "ot") + switch orderType { + case "LIMIT", "MARKET": + reverseOrder.OrderType = 0 + case "TAKE_PROFIT_MARKET", "TAKE_PROFIT": + reverseOrder.OrderType = 1 + case "STOP_MARKET", "STOP", "TRAILING_STOP_MARKET": + reverseOrder.OrderType = 2 + default: + return reverseOrder, fmt.Errorf("不支持的订单类型: %s", orderType) + } + + if reverseOrder.PositionSide == "BOTH" { + return reverseOrder, errors.New("不支持的持仓类型,必须为双向持仓") + } + + if err := e.Orm.Create(&reverseOrder).Error; err != nil { + e.Log.Errorf("保存主单失败:%v", err) + return reverseOrder, err + } + + return reverseOrder, nil +} + +// 更新仓位信息 +// closePosition: true=平仓, false=减仓 +func (e *ReverseService) savePosition(reverseOrder *DbModels.LineReverseOrder, reverseApiId int, isMain, reducePosition, closePosition bool) error { + position := DbModels.LineReversePosition{} + positionSide := reverseOrder.PositionSide + side := reverseOrder.Side + totalNum := reverseOrder.TotalNum + + var querySql string + sqlStr := "" + + //如果是主单,存储仓位则是反单的持仓方向 + if isMain { + if reverseOrder.PositionSide == "LONG" { + positionSide = "SHORT" + } else { + positionSide = "LONG" + } + + if !reducePosition { + //反单止盈止损方向相反 + if side == "SELL" { + side = "BUY" + } else { + side = "SELL" + } + + position.ReverseApiId = reverseApiId + position.Side = side + position.ApiId = reverseOrder.ApiId + position.Symbol = reverseOrder.Symbol + position.Status = 1 + position.ReverseStatus = 0 + position.PositionSide = positionSide + } + querySql = "api_id =? and position_side =? and symbol =? and status =1" + + //平仓 + if closePosition { + totalNum = decimal.Zero + 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 " + } else { + sqlStr = "UPDATE line_reverse_position set total_amount=total_amount + @totalNum,amount=amount + @totalNum,updated_at=now() where id =@id and status!=2" + } + } else { + querySql = "reverse_api_id =? and position_side =? and symbol =? and reverse_status in (0,1)" + + if closePosition { + 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" + } 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" + } + } + + err := e.Orm.Transaction(func(tx *gorm.DB) error { + err1 := tx.Model(&position).Where(querySql, + reverseOrder.ApiId, positionSide, reverseOrder.Symbol).First(&position).Error + + if err1 != nil { + //主单仓位不存在,创建新仓位 + if isMain && errors.Is(err1, gorm.ErrRecordNotFound) && !reducePosition { + + if err2 := tx.Create(&position).Error; err2 != nil { + return err2 + } + } else { + return err1 + } + } + + dbResult := tx.Exec(sqlStr, sql.Named("totalNum", totalNum), sql.Named("id", position.Id)) + if dbResult.Error != nil { + return dbResult.Error + } + + if dbResult.RowsAffected == 0 { + e.Log.Errorf("减仓数据 是否平仓单:%v :%v", closePosition, reverseOrder) + return errors.New("没有找到对应的持仓信息") + } + + return nil + }) + + return err +} + +// 反向下单 +// mainOrder: 主单信息 +// reverseApiId: 反向apiId +// orderProportion: 反向下单比例 +// reducePosition: 是否减仓 +// closePosition: 是否平仓 +func (e *ReverseService) DoAddReverseOrder(mainOrder *DbModels.LineReverseOrder, reverseApiInfo *DbModels.LineApiUser, orderProportion decimal.Decimal, reducePosition, closePosition bool) error { + order := DbModels.LineReverseOrder{} + order.ApiId = reverseApiInfo.Id + order.Category = 1 + order.OrderSn = helper.GetOrderNo() + order.FollowOrderSn = mainOrder.OrderSn + order.Symbol = mainOrder.Symbol + order.OrderType = 0 + + switch mainOrder.PositionSide { + case "LONG": + order.PositionSide = "SHORT" + case "SHORT": + order.PositionSide = "LONG" + } + + switch mainOrder.Side { + case "SELL": + order.Side = "BUY" + case "BUY": + order.Side = "SELL" + default: + return fmt.Errorf("不支持的订单类型 side:%s", mainOrder.Side) + } + + if reducePosition && closePosition { + order.OrderType = 4 + } else if reducePosition { + order.OrderType = 3 + } + + symbol, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, mainOrder.Symbol, 1) + + if err != nil { + e.Log.Errorf("获取交易对信息失败 symbol:%s custom:%s :%v", mainOrder.Symbol, mainOrder.OrderSn, err) + return err + } + + setting, err := cacheservice.GetReverseSetting(e.Orm) + + if err != nil { + e.Log.Errorf("获取反单设置失败 symbol:%s custom:%s :%v", mainOrder.Symbol, mainOrder.OrderSn, err) + return err + } + + signPrice, _ := decimal.NewFromString(symbol.LastPrice) + price := signPrice.Truncate(int32(symbol.PriceDigit)) + + if price.Cmp(decimal.Zero) <= 0 { + e.Log.Errorf("获取最新价格失败 symbol:%s custom:%s 单价小于0", mainOrder.Symbol, mainOrder.OrderSn) + return errors.New("获取最新价格失败") + } + + var percent decimal.Decimal + switch { + case order.PositionSide == "LONG" && order.Side == "BUY", order.PositionSide == "SHORT" && order.Side == "BUY": + percent = decimal.NewFromInt(100).Add(setting.ReversePremiumRatio) + case order.PositionSide == "SHORT" && order.Side == "SELL", order.PositionSide == "LONG" && order.Side == "SELL": + percent = decimal.NewFromInt(100).Sub(setting.ReversePremiumRatio) + default: + return fmt.Errorf("不支持的订单类型 ps:%s, side:%s", order.PositionSide, order.Side) + } + + percent = percent.Div(decimal.NewFromInt(100)).Truncate(4) + //计算溢价单价 + price = price.Mul(percent).Truncate(int32(symbol.PriceDigit)) + var amount decimal.Decimal + + //平仓单直接卖出全部数量 + if closePosition { + var position DbModels.LineReversePosition + if err1 := e.Orm.Model(position). + Where("position_side =? and reverse_api_id =? and reverse_status =1", order.PositionSide, order.ApiId). + Select("reverse_amount"). + Find(&position).Error; err1 != nil { + e.Log.Errorf("获取剩余仓位失败 symbol:%s custom:%s :%v", order.Symbol, order.OrderSn, err1) + + if len(err1.Error()) < 255 { + order.Remark = err1.Error() + } else { + order.Remark = err1.Error()[:254] + } + } + + amount = position.ReverseAmount + + if amount.IsZero() { + order.Remark = "没有持仓数量" + } + } else { + proportion := decimal.NewFromInt(100) + + if orderProportion.Cmp(decimal.Zero) > 0 { + proportion = orderProportion + } + + //反向下单百分比 + proportion = proportion.Div(decimal.NewFromInt(100)).Truncate(2) + amount = mainOrder.BuyPrice.Mul(proportion).Div(price).Truncate(int32(symbol.AmountDigit)) + + if amount.Cmp(decimal.Zero) <= 0 { + e.Log.Errorf("计算数量失败 symbol:%s custom:%s 数量小于0", mainOrder.Symbol, mainOrder.OrderSn) + return errors.New("计算数量失败") + } + } + + order.TotalNum = amount + order.Price = price + order.PriceU = price + order.BuyPrice = amount.Mul(price).Truncate(int32(symbol.PriceDigit)) + order.Status = 1 + order.Type = setting.ReverseOrderType + order.SignPrice = signPrice + + if order.Remark != "" { + order.Status = 8 + } + + if err := e.Orm.Model(&order).Create(&order).Error; err != nil { + e.Log.Errorf("保存反单失败 symbol:%s custom:%s :%v", mainOrder.Symbol, mainOrder.OrderSn, err) + return err + } + + if order.Status == 1 { + e.DoBianceOrder(&order, reverseApiInfo, &setting, reducePosition, closePosition) + } + + return nil +} + +// 处理币安订单 +// order: 反单信息 +// apiInfo: 币安api信息 +func (e *ReverseService) DoBianceOrder(order *DbModels.LineReverseOrder, apiInfo *DbModels.LineApiUser, setting *DbModels.LineReverseSetting, reducePosition, closePosition bool) error { + futApiV2 := FuturesResetV2{Service: e.Service} + orderType := setting.ReverseOrderType + + if orderType == "" { + orderType = "LIMIT" + } + + params := FutOrderPlace{ + ApiId: apiInfo.Id, + Symbol: order.Symbol, + PositionSide: order.PositionSide, + Side: order.Side, + OrderType: orderType, + Quantity: order.TotalNum, + Price: order.Price, + NewClientOrderId: order.OrderSn, + } + + err := futApiV2.OrderPlaceLoop(apiInfo, params) + + if err != nil { + e.Log.Errorf("币安下单失败 symbol:%s custom:%s :%v", order.Symbol, order.OrderSn, err) + remark := err.Error() + + if len(remark) > 255 { + remark = remark[:254] + } + + if err1 := e.Orm.Model(&order).Where("id =? and status !=3", order.Id).Updates(map[string]interface{}{"status": 8, "remark": remark, "updated_at": time.Now()}).Error; err1 != nil { + e.Log.Errorf("更新订单状态失败 symbol:%s custom:%s :%v", err1) + } + } + + return nil +} + +// 重下止盈止损 +// 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 + } + + 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" + } + + apiInfo, err := GetApiInfo(mainApiInfo.ReverseApiId) + + if err != nil { + e.Log.Errorf("根据主单api获取反单api失败 symbol:%s custom:%s :%v", symbol, orderSn, err) + 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 + } + } + + params := FutOrderPlace{ + ApiId: apiInfo.Id, + Symbol: symbol, + PositionSide: positionSide, + Side: side, + OrderType: ot, + Quantity: reverseOrder.TotalNum, + Price: reverseOrder.Price, + NewClientOrderId: reverseOrder.OrderSn, + } + futApiV2 := FuturesResetV2{Service: e.Service} + futApiV2.OrderPlace(&apiInfo, params) + return nil +} diff --git a/services/binanceservice/reverse_service_test.go b/services/binanceservice/reverse_service_test.go new file mode 100644 index 0000000..949a472 --- /dev/null +++ b/services/binanceservice/reverse_service_test.go @@ -0,0 +1 @@ +package binanceservice diff --git a/services/binanceservice/spotreset.go b/services/binanceservice/spotreset.go index d943c80..656a12a 100644 --- a/services/binanceservice/spotreset.go +++ b/services/binanceservice/spotreset.go @@ -21,7 +21,6 @@ import ( "github.com/bytedance/sonic" "github.com/go-admin-team/go-admin-core/logger" - log "github.com/go-admin-team/go-admin-core/logger" "github.com/shopspring/decimal" "gorm.io/gorm" ) @@ -29,7 +28,7 @@ import ( /* 订单回调 */ -func ChangeSpotOrder(mapData map[string]interface{}) { +func ChangeSpotOrder(mapData map[string]interface{}, apiKey string) { // 检查订单号是否存在 orderSn, ok := mapData["c"] originOrderSn := mapData["C"] //取消操作 代表原始订单号 @@ -193,7 +192,7 @@ func handleMainReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) { lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotReduceCallback, preOrder.ApiId, preOrder.Symbol), 120, 20, 100*time.Millisecond) if ok, err := lock.AcquireWait(context.Background()); err != nil { - log.Error("获取锁失败", err) + logger.Error("获取锁失败", err) return } else if ok { defer lock.Release() diff --git a/services/cacheservice/confg_server_test.go b/services/cacheservice/confg_server_test.go new file mode 100644 index 0000000..1d49683 --- /dev/null +++ b/services/cacheservice/confg_server_test.go @@ -0,0 +1,23 @@ +package cacheservice + +import ( + "go-admin/common/helper" + "testing" + + "gorm.io/driver/mysql" + "gorm.io/gorm" +) + +func TestGetReverseSetting(t *testing.T) { + dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms" + db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{}) + helper.InitDefaultRedis("127.0.0.1:6379", "", 2) + + setting, err := GetReverseSetting(db) + + if err != nil { + t.Error(err) + } + + t.Log(setting) +} diff --git a/services/cacheservice/config_service.go b/services/cacheservice/config_service.go index e0db3cc..71c000d 100644 --- a/services/cacheservice/config_service.go +++ b/services/cacheservice/config_service.go @@ -130,3 +130,22 @@ func ResetSystemSetting(db *gorm.DB) (models.LineSystemSetting, error) { } return models.LineSystemSetting{}, nil } + +// 获取反单配置 +func GetReverseSetting(db *gorm.DB) (models.LineReverseSetting, error) { + setting := models.LineReverseSetting{} + + helper.DefaultRedis.HGetAsObject(rediskey.ReverseSetting, &setting) + + if setting.ReverseOrderType == "" { + if err := db.Model(&setting).First(&setting).Error; err != nil { + logger.Error("获取反单配置失败", err) + } + + if err := helper.DefaultRedis.SetHashWithTags(rediskey.ReverseSetting, &setting); err != nil { + logger.Error("redis添加反单配置失败", err) + } + } + + return setting, nil +} diff --git a/services/excservice/binancereceive.go b/services/excservice/binancereceive.go index 24d3cba..ee45f62 100644 --- a/services/excservice/binancereceive.go +++ b/services/excservice/binancereceive.go @@ -15,7 +15,7 @@ import ( - @msg 消息内容 - @listenType 订阅类型 0-现货 1-合约 */ -func ReceiveListen(msg []byte, listenType int) (reconnect bool, err error) { +func ReceiveListen(msg []byte, listenType int, apiKey string) (reconnect bool, err error) { var dataMap map[string]interface{} err = sonic.Unmarshal(msg, &dataMap) @@ -52,7 +52,7 @@ func ReceiveListen(msg []byte, listenType int) (reconnect bool, err error) { } utility.SafeGo(func() { - binanceservice.ChangeSpotOrder(mapData) + binanceservice.ChangeSpotOrder(mapData, apiKey) }) } else { var data futuresdto.OrderTradeUpdate @@ -64,7 +64,7 @@ func ReceiveListen(msg []byte, listenType int) (reconnect bool, err error) { } utility.SafeGo(func() { - binanceservice.ChangeFutureOrder(data.OrderDetails) + binanceservice.ChangeFutureOrder(data.OrderDetails, apiKey) }) } //订单更新 @@ -72,9 +72,9 @@ func ReceiveListen(msg []byte, listenType int) (reconnect bool, err error) { log.Info("executionReport 推送:", string(msg)) if listenType == 0 { //现货 - binanceservice.ChangeSpotOrder(dataMap) + binanceservice.ChangeSpotOrder(dataMap, apiKey) } else if listenType == 1 { //合约 - binanceservice.ChangeFutureOrder(dataMap) + binanceservice.ChangeFutureOrder(dataMap, apiKey) } else { log.Error("executionReport 不支持的订阅类型", strconv.Itoa(listenType)) } @@ -101,6 +101,7 @@ func ReceiveListen(msg []byte, listenType int) (reconnect bool, err error) { case "eventStreamTerminated": log.Info("账户数据流被终止 type:", getWsTypeName(listenType)) + return true, nil default: log.Info("未知事件 内容:", string(msg)) log.Info("未知事件", event) diff --git a/services/excservice/binancesocketmanager.go b/services/excservice/binancesocketmanager.go index ae94624..09a492f 100644 --- a/services/excservice/binancesocketmanager.go +++ b/services/excservice/binancesocketmanager.go @@ -20,6 +20,7 @@ import ( "strconv" "strings" "sync" + "sync/atomic" "time" "github.com/bytedance/sonic" @@ -42,7 +43,9 @@ type BinanceWebSocketManager struct { isStopped bool // 标记 WebSocket 是否已主动停止 mu sync.Mutex // 用于控制并发访问 isStopped cancelFunc context.CancelFunc - listenKey string // 新增字段 + listenKey string // 新增字段 + reconnecting atomic.Bool // 防止重复重连 + ConnectTime time.Time // 当前连接建立时间 } // 已有连接 @@ -72,6 +75,10 @@ func NewBinanceWebSocketManager(wsType int, apiKey, apiSecret, proxyType, proxyA } } +func (wm *BinanceWebSocketManager) GetKey() string { + return wm.apiKey +} + func (wm *BinanceWebSocketManager) Start() { utility.SafeGo(wm.run) // wm.run() @@ -91,14 +98,32 @@ func (wm *BinanceWebSocketManager) Restart(apiKey, apiSecret, proxyType, proxyAd wm.isStopped = false utility.SafeGo(wm.run) } else { - wm.reconnect <- struct{}{} + log.Warnf("调用restart") + wm.triggerReconnect(true) } return wm } +// 触发重连 +func (wm *BinanceWebSocketManager) triggerReconnect(force bool) { + if force { + wm.reconnecting.Store(false) // 强制重置标志位 + } + + if wm.reconnecting.CompareAndSwap(false, true) { + log.Warnf("准备重连 key: %s wsType: %v", wm.apiKey, wm.wsType) + // 发送信号触发重连协程 + select { + case wm.reconnect <- struct{}{}: + default: + // 防止阻塞,如果通道满了就跳过 + } + } +} func Restart(wm *BinanceWebSocketManager) { - wm.reconnect <- struct{}{} + log.Warnf("调用restart") + wm.triggerReconnect(true) } func (wm *BinanceWebSocketManager) run() { ctx, cancel := context.WithCancel(context.Background()) @@ -201,6 +226,8 @@ func (wm *BinanceWebSocketManager) connect(ctx context.Context) error { return err } + // 连接成功,更新连接时间 + wm.ConnectTime = time.Now() log.Info(fmt.Sprintf("已连接到 Binance %s WebSocket【%s】 key:%s", getWsTypeName(wm.wsType), wm.apiKey, listenKey)) // Ping处理 @@ -222,10 +249,88 @@ func (wm *BinanceWebSocketManager) connect(ctx context.Context) error { return nil }) - // utility.SafeGoParam(wm.restartConnect, ctx) utility.SafeGo(func() { wm.startListenKeyRenewal2(ctx) }) utility.SafeGo(func() { wm.readMessages(ctx) }) utility.SafeGo(func() { wm.handleReconnect(ctx) }) + utility.SafeGo(func() { wm.startPingLoop(ctx) }) + // utility.SafeGo(func() { wm.startDeadCheck(ctx) }) + + return nil +} + +// ReplaceConnection 创建新连接并关闭旧连接,实现无缝连接替换 +func (wm *BinanceWebSocketManager) ReplaceConnection() error { + wm.mu.Lock() + if wm.isStopped { + wm.mu.Unlock() + return errors.New("WebSocket 已停止") + } + oldCtxCancel := wm.cancelFunc + oldConn := wm.ws + wm.mu.Unlock() + + log.Infof("🔄 正在替换连接: %s", wm.apiKey) + + // 步骤 1:先获取新的 listenKey 和连接 + newListenKey, err := wm.getListenKey() + if err != nil { + return fmt.Errorf("获取新 listenKey 失败: %w", err) + } + + dialer, err := wm.getDialer() + if err != nil { + return err + } + + newURL := fmt.Sprintf("%s/%s", wm.url, newListenKey) + newConn, _, err := dialer.Dial(newURL, nil) + if err != nil { + return fmt.Errorf("新连接 Dial 失败: %w", err) + } + + // 步骤 2:创建新的上下文并启动协程 + newCtx, newCancel := context.WithCancel(context.Background()) + + // 设置 ping handler + newConn.SetPingHandler(func(appData string) error { + log.Infof("收到 Ping(新连接) key:%s msg:%s", wm.apiKey, appData) + for x := 0; x < 5; x++ { + if err := newConn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(10*time.Second)); err != nil { + log.Errorf("Pong 失败 %d 次 err:%v", x, err) + time.Sleep(time.Second) + continue + } + break + } + setLastTime(wm) + return nil + }) + + // 步骤 3:安全切换连接 + wm.mu.Lock() + wm.ws = newConn + wm.listenKey = newListenKey + wm.ConnectTime = time.Now() + wm.cancelFunc = newCancel + wm.mu.Unlock() + + log.Infof("✅ 替换连接成功: %s listenKey: %s", wm.apiKey, newListenKey) + + // 步骤 4:启动新连接协程 + go wm.startListenKeyRenewal2(newCtx) + go wm.readMessages(newCtx) + go wm.handleReconnect(newCtx) + go wm.startPingLoop(newCtx) + // go wm.startDeadCheck(newCtx) + + // 步骤 5:关闭旧连接、取消旧协程 + if oldCtxCancel != nil { + oldCtxCancel() + } + if oldConn != nil { + _ = oldConn.Close() + log.Infof("🔒 旧连接已关闭: %s", wm.apiKey) + } return nil } @@ -251,6 +356,7 @@ func setLastTime(wm *BinanceWebSocketManager) { if val != "" { helper.DefaultRedis.SetString(subKey, val) } + } func (wm *BinanceWebSocketManager) getDialer() (*websocket.Dialer, error) { @@ -357,7 +463,8 @@ func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) { _, msg, err := wm.ws.ReadMessage() if err != nil && strings.Contains(err.Error(), "websocket: close") { if !wm.isStopped { - wm.reconnect <- struct{}{} + log.Error("收到关闭消息", err.Error()) + wm.triggerReconnect(false) } log.Error("websocket 关闭") @@ -375,8 +482,9 @@ func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) { func (wm *BinanceWebSocketManager) handleOrderUpdate(msg []byte) { setLastTime(wm) - if reconnect, _ := ReceiveListen(msg, wm.wsType); reconnect { - wm.reconnect <- struct{}{} + if reconnect, _ := ReceiveListen(msg, wm.wsType, wm.apiKey); reconnect { + log.Errorf("收到重连请求") + wm.triggerReconnect(false) } } @@ -447,6 +555,7 @@ func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) { retryCount++ if retryCount >= maxRetries { + wm.reconnecting.Store(false) log.Error("重连失败次数过多,退出重连逻辑") return } @@ -455,51 +564,88 @@ func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) { continue } + // 重连成功,清除标记 + wm.reconnecting.Store(false) + retryCount = 0 return } } } } -// 定期删除listenkey 并重启ws -func (wm *BinanceWebSocketManager) startListenKeyRenewal(ctx context.Context, listenKey string) { - time.Sleep(30 * time.Minute) - - select { - case <-ctx.Done(): +// 假死检测 +func (wm *BinanceWebSocketManager) DeadCheck() { + subKey := fmt.Sprintf(global.USER_SUBSCRIBE, wm.apiKey) + val, _ := helper.DefaultRedis.GetString(subKey) + if val == "" { + log.Warnf("没有订阅信息,无法进行假死检测") return - default: - if err := wm.deleteListenKey(listenKey); err != nil { - log.Error("Failed to renew listenKey: ,type:%v key: %s", wm.wsType, wm.apiKey, err) - } else { - log.Debug("Successfully delete listenKey") - wm.reconnect <- struct{}{} - } } - // ticker := time.NewTicker(5 * time.Minute) - // defer ticker.Stop() + var data binancedto.UserSubscribeState + _ = sonic.Unmarshal([]byte(val), &data) - // for { - // select { - // case <-ticker.C: - // if wm.isStopped { - // return - // } + var lastTime *time.Time + if wm.wsType == 0 { + lastTime = data.SpotLastTime + } else { + lastTime = data.FuturesLastTime + } - // if err := wm.deleteListenKey(listenKey); err != nil { - // log.Error("Failed to renew listenKey: ,type:%v key: %s", wm.wsType, wm.apiKey, err) - // } else { - // log.Debug("Successfully delete listenKey") - // wm.reconnect <- struct{}{} - // return - // } - // case <-ctx.Done(): - // return - // } - // } + // 定义最大静默时间(超出视为假死) + var timeout time.Duration + if wm.wsType == 0 { + timeout = 40 * time.Second // Spot 每 20s ping,40s 足够 + } else { + timeout = 6 * time.Minute // Futures 每 3 分钟 ping + } + + if lastTime != nil && time.Since(*lastTime) > timeout { + log.Warnf("检测到假死连接 key:%s type:%v, 距离上次通信: %v, 触发重连", wm.apiKey, wm.wsType, time.Since(*lastTime)) + wm.triggerReconnect(true) + } } +// 主动心跳发送机制 +func (wm *BinanceWebSocketManager) startPingLoop(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 + } + err := wm.ws.WriteMessage(websocket.PingMessage, []byte("ping")) + if err != nil { + log.Error("主动 Ping Binance 失败:", err) + wm.triggerReconnect(false) + } + } + } +} + +// 定期删除listenkey 并重启ws +// func (wm *BinanceWebSocketManager) startListenKeyRenewal(ctx context.Context, listenKey string) { +// time.Sleep(30 * time.Minute) + +// select { +// case <-ctx.Done(): +// return +// default: +// if err := wm.deleteListenKey(listenKey); err != nil { +// log.Error("Failed to renew listenKey: ,type:%v key: %s", wm.wsType, wm.apiKey, err) +// } else { +// log.Debug("Successfully delete listenKey") +// wm.triggerReconnect() +// } +// } + +// } + // 定时续期 func (wm *BinanceWebSocketManager) startListenKeyRenewal2(ctx context.Context) { ticker := time.NewTicker(30 * time.Minute) @@ -527,49 +673,34 @@ func (wm *BinanceWebSocketManager) startListenKeyRenewal2(ctx context.Context) { /* 删除listenkey */ -func (wm *BinanceWebSocketManager) deleteListenKey(listenKey string) error { - client, err := wm.createBinanceClient() - if err != nil { - return err - } +// func (wm *BinanceWebSocketManager) deleteListenKey(listenKey string) error { +// client, err := wm.createBinanceClient() +// if err != nil { +// return err +// } - var resp []byte +// var resp []byte - switch wm.wsType { - case 0: - path := fmt.Sprintf("/api/v3/userDataStream") - params := map[string]interface{}{ - "listenKey": listenKey, - } - resp, _, err = client.SendSpotRequestByKey(path, "DELETE", params) +// switch wm.wsType { +// case 0: +// path := fmt.Sprintf("/api/v3/userDataStream") +// params := map[string]interface{}{ +// "listenKey": listenKey, +// } +// resp, _, err = client.SendSpotRequestByKey(path, "DELETE", params) - log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp))) - case 1: - resp, _, err = client.SendFuturesRequestByKey("/fapi/v1/listenKey", "DELETE", nil) - log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp))) - default: - return errors.New("unknown ws type") - } +// log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp))) +// case 1: +// resp, _, err = client.SendFuturesRequestByKey("/fapi/v1/listenKey", "DELETE", nil) +// log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp))) +// default: +// return errors.New("unknown ws type") +// } - return err -} +// return err +// } func (wm *BinanceWebSocketManager) renewListenKey(listenKey string) error { - // payloadParam := map[string]interface{}{ - // "listenKey": listenKey, - // "apiKey": wm.apiKey, - // } - - // params := map[string]interface{}{ - // "id": getUUID(), - // "method": "userDataStream.ping", - // "params": payloadParam, - // } - - // if err := wm.ws.WriteJSON(params); err != nil { - // return err - // } - // wm.ws.WriteJSON() client, err := wm.createBinanceClient() if err != nil { return err