Compare commits

11 Commits

Author SHA1 Message Date
4b28684fe4 1暂时提交 2025-08-11 09:27:32 +08:00
56a761e5ab 1、临时提交 反向下单 2025-08-01 10:30:43 +08:00
771c617da4 1、反向下单 暂时提交 2025-07-26 09:09:09 +08:00
3013486dd4 1、反向下单初始化 2025-06-17 14:09:37 +08:00
e943a47121 1、暂存 2025-05-21 09:11:10 +08:00
0f9b966fdb 1 取消注释 2025-05-20 09:13:44 +08:00
7b50873de3 1bug修复 2025-05-19 09:47:49 +08:00
44ba8bfbf1 1、波段触发修改 2025-04-21 17:32:57 +08:00
79af1ab2c1 1、移除策略u下单缓存 2025-04-12 18:32:36 +08:00
e3a737a7d6 1、 2025-04-12 11:33:12 +08:00
d4c8e692a7 1、合约止盈止损单在减仓单成交之后再取消 2025-04-11 09:06:09 +08:00
68 changed files with 5222 additions and 390 deletions

View File

@ -120,6 +120,11 @@ func (e LineApiUser) Insert(c *gin.Context) {
// 设置创建人 // 设置创建人
req.SetCreateBy(user.GetUserId(c)) req.SetCreateBy(user.GetUserId(c))
if err := req.Valid(); err != nil {
e.Error(500, err, err.Error())
return
}
err = s.Insert(&req) err = s.Insert(&req)
if err != nil { if err != nil {
e.Error(500, err, fmt.Sprintf("创建api用户管理失败\r\n失败信息 %s", err.Error())) e.Error(500, err, fmt.Sprintf("创建api用户管理失败\r\n失败信息 %s", err.Error()))
@ -153,6 +158,12 @@ func (e LineApiUser) Update(c *gin.Context) {
e.Error(500, err, err.Error()) e.Error(500, err, err.Error())
return return
} }
if err := req.Valid(); err != nil {
e.Error(500, err, err.Error())
return
}
req.SetUpdateBy(user.GetUserId(c)) req.SetUpdateBy(user.GetUserId(c))
p := actions.GetPermissionFromContext(c) p := actions.GetPermissionFromContext(c)
@ -267,3 +278,74 @@ func (e LineApiUser) GetMainUser(c *gin.Context) {
} }
e.OK(list, "操作成功") e.OK(list, "操作成功")
} }
// GetUnBindApiUser 获取未绑定下反单api用户
func (e LineApiUser) GetUnBindReverseApiUser(c *gin.Context) {
s := service.LineApiUser{}
req := dto.GetUnBindReverseReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.Query, binding.Form).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
datas := make([]dto.UnBindReverseResp, 0)
err = s.GetUnBindApiUser(&req, &datas)
if err != nil {
e.Error(500, err, fmt.Sprintf("获取失败,\r\n失败信息 %s", err.Error()))
return
}
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, "操作成功")
}
// 获取所有启用的反单api用户
func (e LineApiUser) GetReverseApiOptionsAll(c *gin.Context) {
s := service.LineApiUser{}
err := e.MakeContext(c).
MakeOrm().
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
list := make([]dto.LineApiUserOptionResp, 0)
err = s.GetReverseApiOptionsAll(&list)
if err != nil {
e.Error(500, err, fmt.Sprintf("获取失败,\r\n失败信息 %s", err.Error()))
return
}
e.OK(list, "操作成功")
}

View File

@ -2,6 +2,7 @@ package apis
import ( import (
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/api" "github.com/go-admin-team/go-admin-core/sdk/api"
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user" "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(), "删除成功") e.OK(req.GetId(), "删除成功")
} }
// aicoin数据同步
func (e LinePriceLimit) UpRange(c *gin.Context) { func (e LinePriceLimit) UpRange(c *gin.Context) {
s := service.LinePriceLimit{} s := service.LinePriceLimit{}
err := e.MakeContext(c). err := e.MakeContext(c).

View File

@ -0,0 +1,198 @@
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"
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/actions"
)
type LineReverseOrder struct {
api.Api
}
// GetPage 获取反单下单列表列表
// @Summary 获取反单下单列表列表
// @Description 获取反单下单列表列表
// @Tags 反单下单列表
// @Param orderSn query string false "订单号"
// @Param orderId query string false "币安订单号"
// @Param followOrderSn query string false "跟随币安订单号"
// @Param orderType query int false "订单类型 0-主单 1-止损单 2-加仓 3-减仓"
// @Param positionSide query string false "持仓方向 LONG-多 SHORT-空"
// @Param side query string false "买卖方向 SELL-卖 BUY-买"
// @Param status query int false "状态 1-待下单 2-已下单 3-已成交 4-已平仓 5-已止损"
// @Param pageSize query int false "页条数"
// @Param pageIndex query int false "页码"
// @Success 200 {object} response.Response{data=response.Page{list=[]models.LineReverseOrder}} "{"code": 200, "data": [...]}"
// @Router /api/v1/line-reverse-order [get]
// @Security Bearer
func (e LineReverseOrder) GetPage(c *gin.Context) {
req := dto.LineReverseOrderGetPageReq{}
s := service.LineReverseOrder{}
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.LineReverseOrder, 0)
var count int64
err = s.GetPage(&req, p, &list, &count)
if err != nil {
e.Error(500, err, fmt.Sprintf("获取反单下单列表失败,\r\n失败信息 %s", err.Error()))
return
}
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}
// Get 获取反单下单列表
// @Summary 获取反单下单列表
// @Description 获取反单下单列表
// @Tags 反单下单列表
// @Param id path int false "id"
// @Success 200 {object} response.Response{data=models.LineReverseOrder} "{"code": 200, "data": [...]}"
// @Router /api/v1/line-reverse-order/{id} [get]
// @Security Bearer
func (e LineReverseOrder) Get(c *gin.Context) {
req := dto.LineReverseOrderGetReq{}
s := service.LineReverseOrder{}
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
}
var object models.LineReverseOrder
p := actions.GetPermissionFromContext(c)
err = s.Get(&req, p, &object)
if err != nil {
e.Error(500, err, fmt.Sprintf("获取反单下单列表失败,\r\n失败信息 %s", err.Error()))
return
}
e.OK( object, "查询成功")
}
// Insert 创建反单下单列表
// @Summary 创建反单下单列表
// @Description 创建反单下单列表
// @Tags 反单下单列表
// @Accept application/json
// @Product application/json
// @Param data body dto.LineReverseOrderInsertReq true "data"
// @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}"
// @Router /api/v1/line-reverse-order [post]
// @Security Bearer
func (e LineReverseOrder) Insert(c *gin.Context) {
req := dto.LineReverseOrderInsertReq{}
s := service.LineReverseOrder{}
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
}
e.OK(req.GetId(), "创建成功")
}
// Update 修改反单下单列表
// @Summary 修改反单下单列表
// @Description 修改反单下单列表
// @Tags 反单下单列表
// @Accept application/json
// @Product application/json
// @Param id path int true "id"
// @Param data body dto.LineReverseOrderUpdateReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
// @Router /api/v1/line-reverse-order/{id} [put]
// @Security Bearer
func (e LineReverseOrder) Update(c *gin.Context) {
req := dto.LineReverseOrderUpdateReq{}
s := service.LineReverseOrder{}
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
}
e.OK( req.GetId(), "修改成功")
}
// Delete 删除反单下单列表
// @Summary 删除反单下单列表
// @Description 删除反单下单列表
// @Tags 反单下单列表
// @Param data body dto.LineReverseOrderDeleteReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}"
// @Router /api/v1/line-reverse-order [delete]
// @Security Bearer
func (e LineReverseOrder) Delete(c *gin.Context) {
s := service.LineReverseOrder{}
req := dto.LineReverseOrderDeleteReq{}
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(), "删除成功")
}

View File

@ -0,0 +1,255 @@
package apis
import (
"fmt"
"strings"
"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"
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/actions"
)
type LineReversePosition struct {
api.Api
}
// 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.LineReversePosition}} "{"code": 200, "data": [...]}"
// @Router /api/v1/line-reverse-position [get]
// @Security Bearer
func (e LineReversePosition) GetPage(c *gin.Context) {
req := dto.LineReversePositionGetPageReq{}
s := service.LineReversePosition{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
p := actions.GetPermissionFromContext(c)
list := make([]dto.LineReversePositionListResp, 0)
var count int64
err = s.GetPage(&req, p, &list, &count)
if err != nil {
e.Error(500, err, fmt.Sprintf("获取反单管理-仓位失败,\r\n失败信息 %s", err.Error()))
return
}
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}
// Get 获取反单管理-仓位
// @Summary 获取反单管理-仓位
// @Description 获取反单管理-仓位
// @Tags 反单管理-仓位
// @Param id path int false "id"
// @Success 200 {object} response.Response{data=models.LineReversePosition} "{"code": 200, "data": [...]}"
// @Router /api/v1/line-reverse-position/{id} [get]
// @Security Bearer
func (e LineReversePosition) Get(c *gin.Context) {
req := dto.LineReversePositionGetReq{}
s := service.LineReversePosition{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
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()))
return
}
e.OK(object, "查询成功")
}
// Insert 创建反单管理-仓位
// @Summary 创建反单管理-仓位
// @Description 创建反单管理-仓位
// @Tags 反单管理-仓位
// @Accept application/json
// @Product application/json
// @Param data body dto.LineReversePositionInsertReq true "data"
// @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}"
// @Router /api/v1/line-reverse-position [post]
// @Security Bearer
func (e LineReversePosition) Insert(c *gin.Context) {
req := dto.LineReversePositionInsertReq{}
s := service.LineReversePosition{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
// 设置创建人
req.SetCreateBy(user.GetUserId(c))
err = s.Insert(&req)
if err != nil {
e.Error(500, err, fmt.Sprintf("创建反单管理-仓位失败,\r\n失败信息 %s", err.Error()))
return
}
e.OK(req.GetId(), "创建成功")
}
// Update 修改反单管理-仓位
// @Summary 修改反单管理-仓位
// @Description 修改反单管理-仓位
// @Tags 反单管理-仓位
// @Accept application/json
// @Product application/json
// @Param id path int true "id"
// @Param data body dto.LineReversePositionUpdateReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
// @Router /api/v1/line-reverse-position/{id} [put]
// @Security Bearer
func (e LineReversePosition) Update(c *gin.Context) {
req := dto.LineReversePositionUpdateReq{}
s := service.LineReversePosition{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.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
}
e.OK(req.GetId(), "修改成功")
}
// Delete 删除反单管理-仓位
// @Summary 删除反单管理-仓位
// @Description 删除反单管理-仓位
// @Tags 反单管理-仓位
// @Param data body dto.LineReversePositionDeleteReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}"
// @Router /api/v1/line-reverse-position [delete]
// @Security Bearer
func (e LineReversePosition) Delete(c *gin.Context) {
s := service.LineReversePosition{}
req := dto.LineReversePositionDeleteReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
// 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(), "删除成功")
}
// ClosePosition 平仓
func (e LineReversePosition) ClosePosition(c *gin.Context) {
req := dto.LineReversePositionCloseReq{}
s := service.LineReversePosition{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
userId := user.GetUserId(c)
p := actions.GetPermissionFromContext(c)
err = s.ClosePosition(&req, p, userId)
if err != nil {
e.Error(500, err, fmt.Sprintf("平仓失败,\r\n失败信息 %s", err.Error()))
return
}
e.OK(nil, "平仓成功")
}
// ClosePositionBatch 批量平仓
func (e LineReversePosition) ClosePositionBatch(c *gin.Context) {
req := dto.LineReversePositionCloseBatchReq{}
s := service.LineReversePosition{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
userId := user.GetUserId(c)
p := actions.GetPermissionFromContext(c)
errs := make([]string, 0)
err = s.ClosePositionBatch(&req, p, userId, &errs)
if err != nil {
e.Error(500, err, fmt.Sprintf("批量平仓失败,\r\n失败信息 %s", err.Error()))
return
}
if len(errs) > 0 {
content := strings.Join(errs, "</br>")
e.OK(content, "批量平仓部分失败")
return
}
e.OK(nil, "批量平仓成功")
}

View File

@ -0,0 +1,158 @@
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"
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
"go-admin/app/admin/models"
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/actions"
)
type LineReverseSetting struct {
api.Api
}
// GetPage 获取反单下单配置列表
// @Summary 获取反单下单配置列表
// @Description 获取反单下单配置列表
// @Tags 反单下单配置
// @Param pageSize query int false "页条数"
// @Param pageIndex query int false "页码"
// @Success 200 {object} response.Response{data=response.Page{list=[]models.LineReverseSetting}} "{"code": 200, "data": [...]}"
// @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
}
p := actions.GetPermissionFromContext(c)
list := make([]models.LineReverseSetting, 0)
var count int64
err = s.GetPage(&req, p, &list, &count)
if err != nil {
e.Error(500, err, fmt.Sprintf("获取反单下单配置失败,\r\n失败信息 %s", err.Error()))
return
}
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
}
// Get 获取反单下单配置
// @Summary 获取反单下单配置
// @Description 获取反单下单配置
// @Tags 反单下单配置
// @Param id path int false "id"
// @Success 200 {object} response.Response{data=models.LineReverseSetting} "{"code": 200, "data": [...]}"
// @Router /api/v1/line-reverse-setting/{id} [get]
// @Security Bearer
func (e LineReverseSetting) Get(c *gin.Context) {
req := dto.LineReverseSettingGetReq{}
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
}
var object models.LineReverseSetting
p := actions.GetPermissionFromContext(c)
err = s.Get(&req, p, &object)
if err != nil {
e.Error(500, err, fmt.Sprintf("获取反单下单配置失败,\r\n失败信息 %s", err.Error()))
return
}
e.OK(object, "查询成功")
}
// Insert 创建反单下单配置
// @Summary 创建反单下单配置
// @Description 创建反单下单配置
// @Tags 反单下单配置
// @Accept application/json
// @Product application/json
// @Param data body dto.LineReverseSettingInsertReq true "data"
// @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}"
// @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.SetCreateBy(user.GetUserId(c))
err = s.Insert(&req)
if err != nil {
e.Error(500, err, fmt.Sprintf("创建反单下单配置失败,\r\n失败信息 %s", err.Error()))
return
}
e.OK(req.GetId(), "创建成功")
}
// Update 修改反单下单配置
// @Summary 修改反单下单配置
// @Description 修改反单下单配置
// @Tags 反单下单配置
// @Accept application/json
// @Product application/json
// @Param id path int true "id"
// @Param data body dto.LineReverseSettingUpdateReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
// @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.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
}
e.OK(req.GetId(), "修改成功")
}

View File

@ -2,29 +2,31 @@ package models
import ( import (
"go-admin/common/models" "go-admin/common/models"
"github.com/shopspring/decimal"
) )
type LineApiUser struct { type LineApiUser struct {
models.Model models.Model
ExchangeType string `json:"exchangeType" gorm:"type:varchar(20);comment:交易所类型(字典 exchange_type)"` ExchangeType string `json:"exchangeType" gorm:"type:varchar(20);comment:交易所类型(字典 exchange_type)"`
UserId int64 `json:"userId" gorm:"type:int unsigned;comment:用户id"` UserId int64 `json:"userId" gorm:"type:int unsigned;comment:用户id"`
JysId int64 `json:"jysId" gorm:"type:int;comment:关联交易所账号id"` ApiName string `json:"apiName" gorm:"type:varchar(255);comment:api用户名"`
ApiName string `json:"apiName" gorm:"type:varchar(255);comment:api用户名"` ApiKey string `json:"apiKey" gorm:"type:varchar(255);comment:apiKey"`
ApiKey string `json:"apiKey" gorm:"type:varchar(255);comment:apiKey"` ApiSecret string `json:"apiSecret" gorm:"type:varchar(255);comment:apiSecret"`
ApiSecret string `json:"apiSecret" gorm:"type:varchar(255);comment:apiSecret"` IpAddress string `json:"ipAddress" gorm:"type:varchar(255);comment:代理地址"`
IpAddress string `json:"ipAddress" gorm:"type:varchar(255);comment:代理地址"` UserPass string `json:"userPass" gorm:"type:varchar(255);comment:代码账号密码"`
UserPass string `json:"userPass" gorm:"type:varchar(255);comment:代码账号密码"` Affiliation int64 `json:"affiliation" gorm:"type:int;comment:归属:1=现货,2=合约,3=现货合约"`
AdminId int64 `json:"adminId" gorm:"type:int unsigned;comment:管理员id"` AdminShow int64 `json:"adminShow" gorm:"type:int;comment:是否超管可见:1=是,0=否"`
Affiliation int64 `json:"affiliation" gorm:"type:int;comment:归属:1=现货,2=合约,3=现货合约"` Site string `json:"site" gorm:"type:enum('1','2','3');comment:允许下单的方向:1=多;2=空;3=多空"`
AdminShow int64 `json:"adminShow" gorm:"type:int;comment:是否超管可见:1=是,0=否"` Subordinate string `json:"subordinate" gorm:"type:enum('0','1','2');comment:从属关系:0=未绑定关系;1=主账号;2=副帐号"`
Site string `json:"site" gorm:"type:enum('1','2','3');comment:允许下单的方向:1=多;2=空;3=多空"` GroupId int64 `json:"groupId" gorm:"type:int unsigned;comment:所属组id"`
Subordinate string `json:"subordinate" gorm:"type:enum('0','1','2');comment:从属关系:0=未绑定关系;1=主账号;2=副帐号"` OpenStatus int64 `json:"openStatus" gorm:"type:int unsigned;comment:开启状态 0=关闭 1=开启"`
GroupId int64 `json:"groupId" gorm:"type:int unsigned;comment:所属组id"` ReverseStatus int `json:"reverseStatus" gorm:"type:tinyint unsigned;comment:反向开仓:1=开启;2=关闭"`
OpenStatus int64 `json:"openStatus" gorm:"type:int unsigned;comment:开启状态 0=关闭 1=开启"` ReverseApiId int `json:"reverseApiId" gorm:"type:bigint;comment:反向apiId"`
OrderProportion decimal.Decimal `json:"orderProportion" gorm:"type:decimal(10,2);comment:委托比例"`
SpotLastTime string `json:"spotLastTime" gorm:"-"` //现货websocket最后通信时间 SpotLastTime string `json:"spotLastTime" gorm:"-"` //现货websocket最后通信时间
FuturesLastTime string `json:"futuresLastTime" gorm:"-"` //合约websocket最后通信时间 FuturesLastTime string `json:"futuresLastTime" gorm:"-"` //合约websocket最后通信时间
models.ModelTime models.ModelTime
models.ControlBy models.ControlBy
} }

View File

@ -0,0 +1,52 @@
package models
import (
"time"
"go-admin/common/models"
"github.com/shopspring/decimal"
)
type LineReverseOrder struct {
models.Model
PositionId int `json:"positionId" gorm:"type:bigint;comment:仓位id"`
ApiId int `json:"apiId" gorm:"type:bigint;comment:api id"`
Category int `json:"category" gorm:"type:tinyint;comment:分类 0-原始订单 1-反单"`
OrderSn string `json:"orderSn" gorm:"type:varchar(50);comment:订单号 0-主单 1-止盈 2-止损"`
OrderId string `json:"orderId" gorm:"type:varchar(50);comment:币安订单号"`
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"
}
func (e *LineReverseOrder) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *LineReverseOrder) GetId() interface{} {
return e.Id
}

View File

@ -0,0 +1,41 @@
package models
import (
"go-admin/common/models"
"github.com/shopspring/decimal"
)
type LineReversePosition struct {
models.Model
PositionNo string `json:"positionNo" gorm:"type:varchar(18);comment:仓位编号"`
ApiId int `json:"apiId" gorm:"type:bigint;comment:api_id"`
TotalAmount decimal.Decimal `json:"totalAmount" gorm:"type:decimal(18,8);comment:总仓位"`
Amount decimal.Decimal `json:"amount" gorm:"type:decimal(18,8);comment:仓位"`
ReverseApiId int `json:"reverseApiId" gorm:"type:bigint;comment:反单api_id"`
TotalReverseAmount decimal.Decimal `json:"totalReverseAmount" gorm:"type:decimal(18,8);comment:总反单仓位"`
ReverseAmount decimal.Decimal `json:"reverseAmount" gorm:"type:decimal(18,8);comment:反单仓位"`
Side string `json:"side" gorm:"type:varchar(10);comment:买卖方向 BUY SELL"`
PositionSide string `json:"positionSide" gorm:"type:varchar(10);comment:持仓方向 LONG SHORT"`
Symbol string `json:"symbol" gorm:"type:varchar(20);comment:交易对"`
Status int `json:"status" gorm:"type:tinyint;comment:仓位状态 1-已开仓 2-已平仓"`
ReverseStatus int `json:"reverseStatus" gorm:"type:tinyint;comment:反单仓位状态 1-已开仓 2-已平仓"`
AveragePrice decimal.Decimal `json:"averagePrice" gorm:"type:decimal(18,8);comment:主单平均价格"`
ReverseAveragePrice decimal.Decimal `json:"reverseAveragePrice" gorm:"type:decimal(18,8);comment:反单平均价格"`
models.ModelTime
models.ControlBy
}
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
}

View File

@ -0,0 +1,31 @@
package models
import (
"go-admin/common/models"
"github.com/shopspring/decimal"
)
type LineReverseSetting struct {
models.Model
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
}
func (LineReverseSetting) TableName() string {
return "line_reverse_setting"
}
func (e *LineReverseSetting) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *LineReverseSetting) GetId() interface{} {
return e.Id
}

View File

@ -14,7 +14,7 @@ type LineStrategyTemplate struct {
Percentag decimal.Decimal `json:"percentag" gorm:"type:decimal(10,2);comment:涨跌点数"` Percentag decimal.Decimal `json:"percentag" gorm:"type:decimal(10,2);comment:涨跌点数"`
CompareType int `json:"compareType" gorm:"type:tinyint;comment:比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "` CompareType int `json:"compareType" gorm:"type:tinyint;comment:比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
TimeSlotStart int `json:"timeSlotStart" gorm:"type:int;comment:时间段开始(分)"` TimeSlotStart int `json:"timeSlotStart" gorm:"type:int;comment:时间段开始(分)"`
TimeSlotEnd int `json:"timeSlotEnd" gorm:"type:int;comment:时间断截至(分)"` // TimeSlotEnd int `json:"timeSlotEnd" gorm:"type:int;comment:时间断截至(分)"`
models.ModelTime models.ModelTime
models.ControlBy models.ControlBy
} }

View File

@ -27,5 +27,11 @@ func registerLineApiUserRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMi
r.POST("bind", api.Bind) //绑定从属关系 r.POST("bind", api.Bind) //绑定从属关系
r.POST("getUser", api.GetUser) //获取未绑定的用户 r.POST("getUser", api.GetUser) //获取未绑定的用户
r.POST("getMainUser", api.GetMainUser) //获取获取主账号的用户 r.POST("getMainUser", api.GetMainUser) //获取获取主账号的用户
r.GET("unbind-reverse", api.GetUnBindReverseApiUser) //获取未绑定下反单用户
r.GET("reverse-options", api.GetReverseApiOptions) //获取可用反单api用户
r.GET("reverse-options-all", api.GetReverseApiOptionsAll) //获取全部启用的反单api用户
} }
} }

View File

@ -0,0 +1,27 @@
package router
import (
"github.com/gin-gonic/gin"
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"
)
func init() {
routerCheckRole = append(routerCheckRole, registerLineReverseOrderRouter)
}
// registerLineReverseOrderRouter
func registerLineReverseOrderRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
api := apis.LineReverseOrder{}
r := v1.Group("/line-reverse-order").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
{
r.GET("", actions.PermissionAction(), api.GetPage)
r.GET("/:id", actions.PermissionAction(), api.Get)
r.POST("", api.Insert)
r.PUT("/:id", actions.PermissionAction(), api.Update)
r.DELETE("", api.Delete)
}
}

View File

@ -0,0 +1,30 @@
package router
import (
"github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
"go-admin/app/admin/apis"
"go-admin/common/actions"
"go-admin/common/middleware"
)
func init() {
routerCheckRole = append(routerCheckRole, registerLineReversePositionRouter)
}
// 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)
r.POST("", api.Insert)
r.PUT("/:id", actions.PermissionAction(), api.Update)
r.DELETE("", api.Delete)
r.PUT("close/:id", actions.PermissionAction(), api.ClosePosition)
r.PUT("close-batch", actions.PermissionAction(), api.ClosePositionBatch)
}
}

View File

@ -0,0 +1,27 @@
package router
import (
"github.com/gin-gonic/gin"
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
"go-admin/app/admin/apis"
"go-admin/common/actions"
"go-admin/common/middleware"
)
func init() {
routerCheckRole = append(routerCheckRole, registerLineReverseSettingRouter)
}
// registerLineReverseSettingRouter
func registerLineReverseSettingRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
api := apis.LineReverseSetting{}
r := v1.Group("/line-reverse-setting").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
{
r.GET("", actions.PermissionAction(), api.GetPage)
r.GET("/:id", actions.PermissionAction(), api.Get)
r.POST("", api.Insert)
r.PUT("/:id", actions.PermissionAction(), api.Update)
// r.DELETE("", api.Delete)
}
}

View File

@ -1,9 +1,12 @@
package dto package dto
import ( import (
"errors"
"go-admin/app/admin/models" "go-admin/app/admin/models"
"go-admin/common/dto" "go-admin/common/dto"
common "go-admin/common/models" common "go-admin/common/models"
"github.com/shopspring/decimal"
) )
type LineApiUserGetPageReq struct { type LineApiUserGetPageReq struct {
@ -41,44 +44,66 @@ func (m *LineApiUserGetPageReq) GetNeedSearch() interface{} {
} }
type LineApiUserInsertReq struct { type LineApiUserInsertReq struct {
Id int `json:"-" comment:"id"` // id Id int `json:"-" comment:"id"` // id
ExchangeType string `json:"exchangeType" comment:"交易所code"` ExchangeType string `json:"exchangeType" comment:"交易所code"`
UserId int64 `json:"userId" comment:"用户id"` UserId int64 `json:"userId" comment:"用户id"`
JysId int64 `json:"jysId" comment:"关联交易所账号id"` JysId int64 `json:"jysId" comment:"关联交易所账号id"`
ApiName string `json:"apiName" comment:"api用户名"` ApiName string `json:"apiName" comment:"api用户名"`
ApiKey string `json:"apiKey" comment:"apiKey"` ApiKey string `json:"apiKey" comment:"apiKey"`
ApiSecret string `json:"apiSecret" comment:"apiSecret"` ApiSecret string `json:"apiSecret" comment:"apiSecret"`
IpAddress string `json:"ipAddress" comment:"代理地址"` IpAddress string `json:"ipAddress" comment:"代理地址"`
UserPass string `json:"userPass" comment:"代码账号密码"` UserPass string `json:"userPass" comment:"代码账号密码"`
AdminId int64 `json:"adminId" comment:"管理员id"` Affiliation int64 `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"`
Affiliation int64 `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"` AdminShow int64 `json:"adminShow" comment:"是否超管可见:1=是,0=否"`
AdminShow int64 `json:"adminShow" comment:"是否超管可见:1=是,0=否"` Site string `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"`
Site string `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"` Subordinate string `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"`
Subordinate string `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"` GroupId int64 `json:"groupId" comment:"所属组id"`
GroupId int64 `json:"groupId" comment:"所属组id"` OpenStatus int64 `json:"openStatus" comment:"开启状态 0=关闭 1=开启"`
OpenStatus int64 `json:"openStatus" comment:"开启状态 0=关闭 1=开启"` ReverseStatus int `json:"reverseStatus" comment:"反向下单状态 1-开启 2-关闭"`
ReverseApiId int `json:"reverseApiId" comment:"反向下单apiId"`
OrderProportion decimal.Decimal `json:"orderProportion" comment:"委托比例"`
common.ControlBy common.ControlBy
} }
func (s *LineApiUserInsertReq) Valid() error {
if s.ReverseStatus == 1 {
if s.OrderProportion.Cmp(decimal.Zero) <= 0 {
return errors.New("委托比例必须大于0")
}
if s.ReverseStatus == 1 && s.ReverseApiId == 0 {
return errors.New("反向下单api不能为空")
}
}
return nil
}
func (s *LineApiUserInsertReq) Generate(model *models.LineApiUser) { func (s *LineApiUserInsertReq) Generate(model *models.LineApiUser) {
if s.Id == 0 { if s.Id == 0 {
model.Model = common.Model{Id: s.Id} model.Model = common.Model{Id: s.Id}
} }
model.ExchangeType = s.ExchangeType model.ExchangeType = s.ExchangeType
model.UserId = s.UserId model.UserId = s.UserId
model.JysId = s.JysId
model.ApiName = s.ApiName model.ApiName = s.ApiName
model.ApiKey = s.ApiKey model.ApiKey = s.ApiKey
model.ApiSecret = s.ApiSecret model.ApiSecret = s.ApiSecret
model.IpAddress = s.IpAddress model.IpAddress = s.IpAddress
model.UserPass = s.UserPass model.UserPass = s.UserPass
model.AdminId = s.AdminId
model.Affiliation = s.Affiliation model.Affiliation = s.Affiliation
model.AdminShow = s.AdminShow model.AdminShow = s.AdminShow
model.Site = s.Site model.Site = s.Site
model.Subordinate = s.Subordinate model.Subordinate = s.Subordinate
model.GroupId = s.GroupId model.GroupId = s.GroupId
model.OpenStatus = s.OpenStatus model.OpenStatus = s.OpenStatus
model.ReverseStatus = s.ReverseStatus
model.OrderProportion = s.OrderProportion
if model.ReverseStatus == 1 {
model.ReverseApiId = s.ReverseApiId
} else {
model.ReverseApiId = 0
}
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的 model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
} }
@ -87,44 +112,66 @@ func (s *LineApiUserInsertReq) GetId() interface{} {
} }
type LineApiUserUpdateReq struct { type LineApiUserUpdateReq struct {
Id int `uri:"id" comment:"id"` // id Id int `uri:"id" comment:"id"` // id
ExchangeType string `json:"exchangeType" comment:"交易所code"` ExchangeType string `json:"exchangeType" comment:"交易所code"`
UserId int64 `json:"userId" comment:"用户id"` UserId int64 `json:"userId" comment:"用户id"`
JysId int64 `json:"jysId" comment:"关联交易所账号id"` JysId int64 `json:"jysId" comment:"关联交易所账号id"`
ApiName string `json:"apiName" comment:"api用户名"` ApiName string `json:"apiName" comment:"api用户名"`
ApiKey string `json:"apiKey" comment:"apiKey"` ApiKey string `json:"apiKey" comment:"apiKey"`
ApiSecret string `json:"apiSecret" comment:"apiSecret"` ApiSecret string `json:"apiSecret" comment:"apiSecret"`
IpAddress string `json:"ipAddress" comment:"代理地址"` IpAddress string `json:"ipAddress" comment:"代理地址"`
UserPass string `json:"userPass" comment:"代码账号密码"` UserPass string `json:"userPass" comment:"代码账号密码"`
AdminId int64 `json:"adminId" comment:"管理员id"` Affiliation int64 `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"`
Affiliation int64 `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"` AdminShow int64 `json:"adminShow" comment:"是否超管可见:1=是,0=否"`
AdminShow int64 `json:"adminShow" comment:"是否超管可见:1=是,0=否"` Site string `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"`
Site string `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"` Subordinate string `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"`
Subordinate string `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"` GroupId int64 `json:"groupId" comment:"所属组id"`
GroupId int64 `json:"groupId" comment:"所属组id"` OpenStatus int64 `json:"openStatus" comment:"开启状态 0=关闭 1=开启"`
OpenStatus int64 `json:"openStatus" comment:"开启状态 0=关闭 1=开启"` ReverseStatus int `json:"reverseStatus" comment:"反向下单状态 1-开启 2-关闭"`
ReverseApiId int `json:"reverseApiId" comment:"反向下单apiId"`
OrderProportion decimal.Decimal `json:"orderProportion" comment:"委托比例"`
common.ControlBy common.ControlBy
} }
func (s *LineApiUserUpdateReq) Valid() error {
if s.ReverseStatus == 1 {
if s.OrderProportion.Cmp(decimal.Zero) <= 0 {
return errors.New("委托比例必须大于0")
}
if s.ReverseStatus == 1 && s.ReverseApiId == 0 {
return errors.New("反向下单api不能为空")
}
}
return nil
}
func (s *LineApiUserUpdateReq) Generate(model *models.LineApiUser) { func (s *LineApiUserUpdateReq) Generate(model *models.LineApiUser) {
if s.Id == 0 { if s.Id == 0 {
model.Model = common.Model{Id: s.Id} model.Model = common.Model{Id: s.Id}
} }
model.ExchangeType = s.ExchangeType model.ExchangeType = s.ExchangeType
model.UserId = s.UserId model.UserId = s.UserId
model.JysId = s.JysId
model.ApiName = s.ApiName model.ApiName = s.ApiName
model.ApiKey = s.ApiKey model.ApiKey = s.ApiKey
model.ApiSecret = s.ApiSecret model.ApiSecret = s.ApiSecret
model.IpAddress = s.IpAddress model.IpAddress = s.IpAddress
model.UserPass = s.UserPass model.UserPass = s.UserPass
model.AdminId = s.AdminId
model.Affiliation = s.Affiliation model.Affiliation = s.Affiliation
model.AdminShow = s.AdminShow model.AdminShow = s.AdminShow
model.Site = s.Site model.Site = s.Site
model.Subordinate = s.Subordinate model.Subordinate = s.Subordinate
model.GroupId = s.GroupId model.GroupId = s.GroupId
model.OpenStatus = s.OpenStatus model.OpenStatus = s.OpenStatus
model.ReverseStatus = s.ReverseStatus
model.OrderProportion = s.OrderProportion
if model.ReverseStatus == 1 {
model.ReverseApiId = s.ReverseApiId
} else {
model.ReverseApiId = 0
}
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的 model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
} }
@ -159,3 +206,26 @@ type LineApiUserBindSubordinateReq struct {
type GetMainUserReq struct { type GetMainUserReq struct {
ExchangeType string `json:"exchangeType"` ExchangeType string `json:"exchangeType"`
} }
type GetUnBindReverseReq struct {
ExchangeType string `json:"exchangeType" form:"exchangeType"`
ApiId int `json:"apiId" form:"apiId" comment:"当前apiId"`
}
// 未绑定反向下单查询返回结构体
type UnBindReverseResp struct {
Id int `json:"id"`
UserId int64 `json:"userId"`
Disabled bool `json:"disabled"`
ApiName string `json:"apiName"`
}
type GetReverseApiOptionsReq struct {
Id int `json:"apiId" form:"id"`
ApiId int `json:"apiId" form:"apiId"`
}
type LineApiUserOptionResp struct {
Id int `json:"id"`
Name string `json:"name"`
}

View File

@ -197,7 +197,7 @@ type LineAddPreOrderReq struct {
Site string `json:"site" ` //购买方向 Site string `json:"site" ` //购买方向
BuyPrice string `json:"buy_price" vd:"$>0"` //购买金额 U BuyPrice string `json:"buy_price" vd:"$>0"` //购买金额 U
PricePattern string `json:"price_pattern"` //价格模式 PricePattern string `json:"price_pattern"` //价格模式
Price string `json:"price" vd:"$>0"` //下单价百分比 Price string `json:"price"` //下单价百分比
Profit string `json:"profit" vd:"$>0"` //止盈价 Profit string `json:"profit" vd:"$>0"` //止盈价
ProfitNumRatio decimal.Decimal `json:"profit_num_ratio"` //止盈数量百分比 ProfitNumRatio decimal.Decimal `json:"profit_num_ratio"` //止盈数量百分比
ProfitTpTpPriceRatio decimal.Decimal `json:"profit_tp_tp_price_ratio"` //止盈后止盈价百分比 ProfitTpTpPriceRatio decimal.Decimal `json:"profit_tp_tp_price_ratio"` //止盈后止盈价百分比
@ -295,7 +295,7 @@ func (req LineAddPreOrderReq) Valid() error {
return errors.New("主单减仓数量百分比不能为空") return errors.New("主单减仓数量百分比不能为空")
} }
if req.ReducePriceRatio.IsZero() || req.ReducePriceRatio.Cmp(decimal.NewFromInt(100)) >= 0 { if req.PricePattern != "mixture" && (req.ReducePriceRatio.IsZero() || req.ReducePriceRatio.Cmp(decimal.NewFromInt(100)) >= 0) {
return errors.New("主单减仓价格百分比错误") return errors.New("主单减仓价格百分比错误")
} }
@ -447,7 +447,7 @@ func (req LineBatchAddPreOrderReq) CheckParams() error {
return errors.New("主单减仓数量百分比不能为空") return errors.New("主单减仓数量百分比不能为空")
} }
if req.ReducePriceRatio.Cmp(decimal.Zero) <= 0 || req.ReducePriceRatio.Cmp(decimal.NewFromInt(100)) >= 0 { if req.PricePattern != "mixture" && (req.ReducePriceRatio.Cmp(decimal.Zero) <= 0 || req.ReducePriceRatio.Cmp(decimal.NewFromInt(100)) >= 0) {
return errors.New("主单减仓价格百分比错误") return errors.New("主单减仓价格百分比错误")
} }

View File

@ -0,0 +1,162 @@
package dto
import (
"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-买"`
Type string `form:"type" search:"type:exact;column:type;table:line_reverse_order" comment:"类型 LIMIT-限价 MARKET-市价 "`
Category int `form:"category" search:"-" comment:"类型 0-主单 1-反单"`
Status int `form:"status" search:"type:exact;column:status;table:line_reverse_order" comment:"状态 1-待下单 2-已下单 3-已成交 4-已平仓 5-已止损"`
PositionId int `form:"positionId" search:"type:exact;column:position_id;table:line_reverse_order" comment:"持仓id"`
LineReverseOrderOrder
}
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"`
}
func (m *LineReverseOrderGetPageReq) GetNeedSearch() interface{} {
return *m
}
type LineReverseOrderInsertReq struct {
Id int `json:"-" comment:"主键id"` // 主键id
PositionId int `json:"positionId" 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.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) GetId() interface{} {
return s.Id
}
type LineReverseOrderUpdateReq struct {
Id int `uri:"id" comment:"主键id"` // 主键id
PositionId int `json:"positionId" 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.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) GetId() interface{} {
return s.Id
}
// LineReverseOrderGetReq 功能获取请求参数
type LineReverseOrderGetReq struct {
Id int `uri:"id"`
}
func (s *LineReverseOrderGetReq) GetId() interface{} {
return s.Id
}
// LineReverseOrderDeleteReq 功能删除请求参数
type LineReverseOrderDeleteReq struct {
Ids []int `json:"ids"`
}
func (s *LineReverseOrderDeleteReq) GetId() interface{} {
return s.Ids
}

View File

@ -0,0 +1,155 @@
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
}
type LineReversePositionListResp struct {
Id int `json:"id"`
ApiName string `json:"apiName"`
ReverseApiName string `json:"reverseApiName"`
Amount decimal.Decimal `json:"amount"`
TotalAmount decimal.Decimal `json:"totalAmount"`
ReverseAmount decimal.Decimal `json:"reverseAmount"`
TotalReverseAmount decimal.Decimal `json:"totalReverseAmount"`
Side string `json:"side"`
PositionSide string `json:"positionSide"`
Symbol string `json:"symbol"`
Status int `json:"status"`
ReverseStatus int `json:"reverseStatus"`
AveragePrice decimal.Decimal `json:"averagePrice"`
ReverseAveragePrice decimal.Decimal `json:"reverseAveragePrice"`
CreatedAt string `json:"createdAt"`
}
type LineReversePositionCloseReq struct {
PositionId int `uri:"id" form:"id" comment:"仓位id"`
}
type LineReversePositionCloseBatchReq struct {
PositionSide string `json:"positionSide"`
Symbol string `json:"symbol"`
ReverseApiIds []int `json:"reverseApiIds" form:"reverseApiIds" comment:"反单api_id"`
}
type GetPositionSymbolReq struct {
ReverseApiId int `form:"reverseApiId" comment:"反单api_id"`
PositionSide string `form:"positionSide" comment:"持仓方向 LONG SHORT"`
}
type PositionSymbolResp struct {
Symbol string `json:"symbol"`
Code string `json:"code"`
}

View File

@ -0,0 +1,95 @@
package dto
import (
"go-admin/app/admin/models"
"go-admin/common/dto"
common "go-admin/common/models"
"github.com/shopspring/decimal"
)
type LineReverseSettingGetPageReq struct {
dto.Pagination `search:"-"`
LineReverseSettingOrder
}
type LineReverseSettingOrder struct {
Id string `form:"idOrder" search:"type:order;column:id;table:line_reverse_setting"`
ReverseOrderType string `form:"reverseOrderTypeOrder" search:"type:order;column:reverse_order_type;table:line_reverse_setting"`
ReversePremiumRatio string `form:"reversePremiumRatioOrder" search:"type:order;column:reverse_premium_ratio;table:line_reverse_setting"`
CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:line_reverse_setting"`
UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:line_reverse_setting"`
DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:line_reverse_setting"`
CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:line_reverse_setting"`
UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:line_reverse_setting"`
}
func (m *LineReverseSettingGetPageReq) GetNeedSearch() interface{} {
return *m
}
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
}
func (s *LineReverseSettingInsertReq) Generate(model *models.LineReverseSetting) {
if s.Id == 0 {
model.Model = common.Model{Id: s.Id}
}
model.ReverseOrderType = s.ReverseOrderType
model.ReversePremiumRatio = s.ReversePremiumRatio
model.StopLossRatio = s.StopLossRatio
model.TakeProfitRatio = s.TakeProfitRatio
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
}
func (s *LineReverseSettingInsertReq) GetId() interface{} {
return s.Id
}
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
}
func (s *LineReverseSettingUpdateReq) Generate(model *models.LineReverseSetting) {
if s.Id == 0 {
model.Model = common.Model{Id: s.Id}
}
model.ReverseOrderType = s.ReverseOrderType
model.ReversePremiumRatio = s.ReversePremiumRatio
model.StopLossRatio = s.StopLossRatio
model.TakeProfitRatio = s.TakeProfitRatio
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
}
func (s *LineReverseSettingUpdateReq) GetId() interface{} {
return s.Id
}
// LineReverseSettingGetReq 功能获取请求参数
type LineReverseSettingGetReq struct {
Id int `uri:"id"`
}
func (s *LineReverseSettingGetReq) GetId() interface{} {
return s.Id
}
// LineReverseSettingDeleteReq 功能删除请求参数
type LineReverseSettingDeleteReq struct {
Ids []int `json:"ids"`
}
func (s *LineReverseSettingDeleteReq) GetId() interface{} {
return s.Ids
}

View File

@ -41,8 +41,8 @@ type LineStrategyTemplateInsertReq struct {
Direction int `json:"direction" comment:"涨跌方向 1-涨 2-跌"` Direction int `json:"direction" comment:"涨跌方向 1-涨 2-跌"`
Percentag decimal.Decimal `json:"percentag" comment:"涨跌点数"` Percentag decimal.Decimal `json:"percentag" comment:"涨跌点数"`
CompareType int `json:"compareType" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "` CompareType int `json:"compareType" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
TimeSlotStart int `json:"timeSlotStart" comment:"时间段开始(分)"` TimeSlotStart int `json:"timeSlotStart" comment:"时间段(分)"`
TimeSlotEnd int `json:"timeSlotEnd" comment:"时间断截至(分)"` // TimeSlotEnd int `json:"timeSlotEnd" comment:"时间断截至(分)"`
common.ControlBy common.ControlBy
} }
@ -63,13 +63,13 @@ func (s *LineStrategyTemplateInsertReq) Valid() error {
return errors.New("比较类型不合法") return errors.New("比较类型不合法")
} }
if s.TimeSlotStart < 0 || s.TimeSlotEnd > 59 { if s.TimeSlotStart < 0 {
return errors.New("时间段不合法") return errors.New("时间段不合法")
} }
if s.TimeSlotEnd < s.TimeSlotStart { // if s.TimeSlotEnd < s.TimeSlotStart {
return errors.New("时间段不合法") // return errors.New("时间段不合法")
} // }
return nil return nil
} }
@ -83,7 +83,7 @@ func (s *LineStrategyTemplateInsertReq) Generate(model *models.LineStrategyTempl
model.Percentag = s.Percentag model.Percentag = s.Percentag
model.CompareType = s.CompareType model.CompareType = s.CompareType
model.TimeSlotStart = s.TimeSlotStart model.TimeSlotStart = s.TimeSlotStart
model.TimeSlotEnd = s.TimeSlotEnd // model.TimeSlotEnd = s.TimeSlotEnd
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的 model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
} }
@ -97,8 +97,8 @@ type LineStrategyTemplateUpdateReq struct {
Direction int `json:"direction" comment:"涨跌方向 1-涨 2-跌"` Direction int `json:"direction" comment:"涨跌方向 1-涨 2-跌"`
Percentag decimal.Decimal `json:"percentag" comment:"涨跌点数"` Percentag decimal.Decimal `json:"percentag" comment:"涨跌点数"`
CompareType int `json:"compareType" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "` CompareType int `json:"compareType" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
TimeSlotStart int `json:"timeSlotStart" comment:"时间段开始(分)"` TimeSlotStart int `json:"timeSlotStart" comment:"时间段(分)"`
TimeSlotEnd int `json:"timeSlotEnd" comment:"时间断截至(分)"` // TimeSlotEnd int `json:"timeSlotEnd" comment:"时间断截至(分)"`
common.ControlBy common.ControlBy
} }
@ -120,13 +120,13 @@ func (s *LineStrategyTemplateUpdateReq) Valid() error {
return errors.New("比较类型不合法") return errors.New("比较类型不合法")
} }
if s.TimeSlotStart < 0 || s.TimeSlotEnd > 59 { if s.TimeSlotStart < 0 {
return errors.New("时间段不合法") return errors.New("时间段不合法")
} }
if s.TimeSlotEnd < s.TimeSlotStart { // if s.TimeSlotEnd < s.TimeSlotStart {
return errors.New("时间段不合法") // return errors.New("时间段不合法")
} // }
return nil return nil
} }
@ -139,7 +139,7 @@ func (s *LineStrategyTemplateUpdateReq) Generate(model *models.LineStrategyTempl
model.Percentag = s.Percentag model.Percentag = s.Percentag
model.CompareType = s.CompareType model.CompareType = s.CompareType
model.TimeSlotStart = s.TimeSlotStart model.TimeSlotStart = s.TimeSlotStart
model.TimeSlotEnd = s.TimeSlotEnd // model.TimeSlotEnd = s.TimeSlotEnd
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的 model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
} }

View File

@ -8,6 +8,7 @@ import (
"github.com/bytedance/sonic" "github.com/bytedance/sonic"
"github.com/go-admin-team/go-admin-core/logger" "github.com/go-admin-team/go-admin-core/logger"
"github.com/go-admin-team/go-admin-core/sdk/service" "github.com/go-admin-team/go-admin-core/sdk/service"
"github.com/go-redis/redis/v8"
"gorm.io/gorm" "gorm.io/gorm"
"go-admin/app/admin/models" "go-admin/app/admin/models"
@ -25,6 +26,43 @@ type LineApiUser struct {
service.Service service.Service
} }
// 获取所有启用的反单api用户
func (e LineApiUser) GetReverseApiOptionsAll(user *[]dto.LineApiUserOptionResp) error {
var data models.LineApiUser
var datas []models.LineApiUser
if err := e.Orm.Model(&data).Where("open_status = 1 AND subordinate = '2'").Find(&datas).Error; err != nil {
e.Log.Errorf("LineApiUserService GetReverseApiOptionsAll error:%s \r\n", err)
return err
}
for _, item := range datas {
*user = append(*user, dto.LineApiUserOptionResp{
Id: item.Id,
Name: item.ApiName,
})
}
return nil
}
// 获取可以绑定的api列表
func (e LineApiUser) GetReverseApiOptions(req *dto.GetReverseApiOptionsReq, user *[]models.LineApiUser) error {
query := e.Orm.Model(models.LineApiUser{}).
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列表 // GetPage 获取LineApiUser列表
func (e *LineApiUser) GetPage(c *dto.LineApiUserGetPageReq, p *actions.DataPermission, list *[]models.LineApiUser, count *int64) error { func (e *LineApiUser) GetPage(c *dto.LineApiUserGetPageReq, p *actions.DataPermission, list *[]models.LineApiUser, count *int64) error {
var err error var err error
@ -97,13 +135,37 @@ func (e *LineApiUser) Insert(c *dto.LineApiUserInsertReq) error {
var err error var err error
var data models.LineApiUser var data models.LineApiUser
c.Generate(&data) c.Generate(&data)
err = e.Orm.Create(&data).Error err = e.Orm.Transaction(func(tx *gorm.DB) error {
if c.ReverseApiId > 0 {
var count int64
if err2 := tx.Model(models.LineApiUser{}).Where("subordinate <> '0' AND id=?", c.ReverseApiId).Count(&count).Error; err2 != nil {
return err2
}
if count > 0 {
return errors.New("反向api已被使用")
}
if err2 := tx.Model(models.LineApiUser{}).Where("id =?", c.ReverseApiId).Update("subordinate", '2').Error; err2 != nil {
return err2
}
data.Subordinate = "1"
}
if err2 := tx.Create(&data).Error; err2 != nil {
return err2
}
return nil
})
if err != nil { if err != nil {
e.Log.Errorf("LineApiUserService Insert error:%s \r\n", err) e.Log.Errorf("LineApiUserService Insert error:%s \r\n", err)
return err return err
} }
e.saveCache(data) if err2 := e.CacheRelation(); err2 != nil {
return err2
}
val, _ := sonic.MarshalString(&data) val, _ := sonic.MarshalString(&data)
if val != "" { if val != "" {
@ -131,7 +193,7 @@ func (e *LineApiUser) restartWebsocket(data models.LineApiUser) {
fuSocket.Stop() fuSocket.Stop()
} }
e.saveCache(data) e.CacheRelation()
OpenUserBinanceWebsocket(data) OpenUserBinanceWebsocket(data)
} }
@ -149,6 +211,23 @@ func (e *LineApiUser) saveCache(data models.LineApiUser) {
} }
} }
// 缓存关系
// cacheAll 是否缓存所有关系
func (e *LineApiUser) CacheRelation() error {
var datas *[]models.LineApiUser
if err := e.Orm.Model(&models.LineApiUser{}).Where("open_status = 1").Find(&datas).Error; err != nil {
return err
}
for _, data := range *datas {
// cacheStrs = append(cacheStrs, fmt.Sprintf(rediskey.API_USER, data.Id))
e.saveCache(data)
}
return nil
}
/* /*
打开用户websocket订阅 打开用户websocket订阅
*/ */
@ -197,6 +276,7 @@ func (e *LineApiUser) Update(c *dto.LineApiUserUpdateReq, p *actions.DataPermiss
actions.Permission(data.TableName(), p), actions.Permission(data.TableName(), p),
).First(&data, c.GetId()) ).First(&data, c.GetId())
oldApiKey := data.ApiKey oldApiKey := data.ApiKey
oldReverseApiId := data.ReverseApiId
c.Generate(&data) c.Generate(&data)
@ -204,6 +284,33 @@ func (e *LineApiUser) Update(c *dto.LineApiUserUpdateReq, p *actions.DataPermiss
//事务 //事务
err = e.Orm.Transaction(func(tx *gorm.DB) error { err = e.Orm.Transaction(func(tx *gorm.DB) error {
if data.ReverseApiId > 0 && data.ReverseApiId != oldReverseApiId {
var count int64
if err2 := tx.Model(models.LineApiUser{}).Where("subordinate <> '0' AND id=?", c.ReverseApiId).Count(&count).Error; err2 != nil {
return err2
}
if count > 0 {
return errors.New("反向api已被使用")
}
if err2 := tx.Model(models.LineApiUser{}).Where("id =?", c.ReverseApiId).Update("subordinate", "2").Error; err2 != nil {
return err2
}
data.Subordinate = "1"
}
if oldReverseApiId > 0 && oldReverseApiId != data.ReverseApiId {
if err2 := tx.Model(models.LineApiUser{}).Where("id =?", oldReverseApiId).Update("subordinate", "0").Error; err2 != nil {
e.Log.Errorf("解绑反向api失败 api:%d err:%v", oldReverseApiId, err2)
return fmt.Errorf("解绑反向api失败")
}
if data.ReverseApiId == 0 {
data.Subordinate = "0"
}
}
db := tx.Save(&data) db := tx.Save(&data)
if err = db.Error; err != nil { if err = db.Error; err != nil {
e.Log.Errorf("LineApiUserService Save error:%s \r\n", err) e.Log.Errorf("LineApiUserService Save error:%s \r\n", err)
@ -226,7 +333,9 @@ func (e *LineApiUser) Update(c *dto.LineApiUserUpdateReq, p *actions.DataPermiss
return err return err
} }
e.saveCache(data) if err2 := e.CacheRelation(); err2 != nil {
return err2
}
//旧key和新的key不一样则关闭旧的websocket //旧key和新的key不一样则关闭旧的websocket
if oldApiKey != data.ApiKey { if oldApiKey != data.ApiKey {
@ -289,10 +398,14 @@ func (e *LineApiUser) Remove(d *dto.LineApiUserDeleteReq, p *actions.DataPermiss
_, err := helper.DefaultRedis.BatchDeleteKeys(delKeys) _, err := helper.DefaultRedis.BatchDeleteKeys(delKeys)
if err != nil { if err != nil && !errors.Is(err, redis.Nil) {
e.Log.Error("批量删除api_user key失败", err) e.Log.Error("批量删除api_user key失败", err)
} }
if err2 := e.CacheRelation(); err2 != nil {
return err2
}
return nil return nil
} }
@ -414,3 +527,44 @@ func (e *LineApiUser) GetActiveApis(apiIds []int) ([]int, error) {
return result, nil return result, nil
} }
// GetUnBindApiUser 获取未绑定反向下单的的api用户
func (e *LineApiUser) GetUnBindApiUser(req *dto.GetUnBindReverseReq, list *[]dto.UnBindReverseResp) error {
var datas []models.LineApiUser
var data models.LineApiUser
var count int64
if req.ApiId > 0 {
e.Orm.Model(data).Where("id =? AND subordinate = '2'", req.ApiId).Count(&count)
if count > 0 {
return nil
}
}
query := e.Orm.Model(&data).
Where("open_status =1 AND exchange_type = ? ", req.ExchangeType)
if err := query.
Find(&datas).Error; err != nil {
e.Log.Error("获取未绑定用户失败:", err)
return fmt.Errorf("获取未绑定用户失败")
}
for _, item := range datas {
if item.Id == req.ApiId {
continue
}
listItem := dto.UnBindReverseResp{
Id: item.Id,
UserId: item.UserId,
ApiName: item.ApiName,
Disabled: item.Subordinate != "0",
}
(*list) = append((*list), listItem)
}
return nil
}

View File

@ -279,6 +279,8 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi
futReduceVal, _ := helper.DefaultRedis.GetAllList(spotAddPositionKey) futReduceVal, _ := helper.DefaultRedis.GetAllList(spotAddPositionKey)
spotAddPositionVal, _ := helper.DefaultRedis.GetAllList(futReduceKey) spotAddPositionVal, _ := helper.DefaultRedis.GetAllList(futReduceKey)
spotReduceVal, _ := helper.DefaultRedis.GetAllList(spotReduceKey) spotReduceVal, _ := helper.DefaultRedis.GetAllList(spotReduceKey)
spotStrategyMap := e.GetStrategyOrderListMap(1)
futStrategyMap := e.GetStrategyOrderListMap(2)
for _, v := range futAddPositionVal { for _, v := range futAddPositionVal {
sonic.Unmarshal([]byte(v), &addPosition) sonic.Unmarshal([]byte(v), &addPosition)
@ -337,8 +339,14 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi
if val, ok := spotRedces[order.Id]; ok { if val, ok := spotRedces[order.Id]; ok {
helper.DefaultRedis.LRem(spotReduceKey, val) helper.DefaultRedis.LRem(spotReduceKey, val)
} }
var tradedSetKey string
if order.SymbolType == 1 {
tradedSetKey = fmt.Sprintf(global.TICKER_SPOT, order.ExchangeType, order.Symbol)
} else {
tradedSetKey = fmt.Sprintf(global.TICKER_FUTURES, order.ExchangeType, order.Symbol)
}
tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, fmt.Sprintf(global.TICKER_SPOT, order.ExchangeType, order.Symbol)) tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, tradedSetKey)
redisList.Price = utility.StringToDecimal(redisList.Price).Truncate(int32(tradeSet.PriceDigit)).String() redisList.Price = utility.StringToDecimal(redisList.Price).Truncate(int32(tradeSet.PriceDigit)).String()
marshal, _ := sonic.Marshal(redisList) marshal, _ := sonic.Marshal(redisList)
if order.SymbolType == 1 { if order.SymbolType == 1 {
@ -349,6 +357,13 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi
helper.DefaultRedis.LRem(listKey, string(marshal)) helper.DefaultRedis.LRem(listKey, string(marshal))
} }
switch {
case order.StrategyTemplateType == 1 && order.SymbolType == 1:
e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &spotStrategyMap)
case order.StrategyTemplateType == 1 && order.SymbolType == 2:
e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &futStrategyMap)
}
//会影响持仓的 //会影响持仓的
removeSymbolKey := fmt.Sprintf("%v_%s_%s_%s_%v", order.ApiId, order.ExchangeType, order.Symbol, order.Site, order.SymbolType) removeSymbolKey := fmt.Sprintf("%v_%s_%s_%s_%v", order.ApiId, order.ExchangeType, order.Symbol, order.Site, order.SymbolType)
@ -363,6 +378,7 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi
} }
binanceservice.MainClosePositionClearCache(order.Id, order.SymbolType) binanceservice.MainClosePositionClearCache(order.Id, order.SymbolType)
binanceservice.RemoveReduceReduceCacheByMainId(order.Id, order.SymbolType)
ints = append(ints, order.Id) ints = append(ints, order.Id)
} }
@ -576,6 +592,7 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
preOrderStatus := models.LinePreOrderStatus{} preOrderStatus := models.LinePreOrderStatus{}
preOrderStatus.OrderSn = AddOrder.OrderSn preOrderStatus.OrderSn = AddOrder.OrderSn
mainPrice := utility.StringToDecimal(AddOrder.Price)
//订单配置信息 //订单配置信息
preOrderExts := make([]models.LinePreOrderExt, 0) preOrderExts := make([]models.LinePreOrderExt, 0)
@ -590,6 +607,12 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
TpTpPriceRatio: req.ProfitTpTpPriceRatio, TpTpPriceRatio: req.ProfitTpTpPriceRatio,
TpSlPriceRatio: req.ProfitTpSlPriceRatio, TpSlPriceRatio: req.ProfitTpSlPriceRatio,
} }
if req.PricePattern == "mixture" {
defultExt.TakeProfitRatio = mainPrice.Div(utility.StrToDecimal(req.Profit)).Sub(decimal.NewFromInt(1)).Abs().Mul(decimal.NewFromInt(100)).Truncate(2)
defultExt.StopLossRatio = mainPrice.Div(req.StopLoss).Sub(decimal.NewFromInt(1)).Abs().Mul(decimal.NewFromInt(100)).Truncate(2)
}
//减仓单 //减仓单
defultExt2 := models.LinePreOrderExt{ defultExt2 := models.LinePreOrderExt{
AddType: 2, AddType: 2,
@ -601,17 +624,12 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
TakeProfitNumRatio: decimal.NewFromInt(100), //减仓止盈默认100% TakeProfitNumRatio: decimal.NewFromInt(100), //减仓止盈默认100%
StopLossRatio: req.ReduceStopLossRatio, StopLossRatio: req.ReduceStopLossRatio,
} }
mainPrice := utility.StringToDecimal(AddOrder.Price)
mainAmount := utility.SafeDiv(buyPrice, mainPrice) mainAmount := utility.SafeDiv(buyPrice, mainPrice)
defultExt.TotalAfter = utility.StrToDecimal(AddOrder.Num).Truncate(int32(tradeSet.AmountDigit)) defultExt.TotalAfter = utility.StrToDecimal(AddOrder.Num).Truncate(int32(tradeSet.AmountDigit))
defultExt2.TotalBefore = defultExt.TotalAfter defultExt2.TotalBefore = defultExt.TotalAfter
default2NumPercent := utility.SafeDiv(decimal.NewFromInt(100).Sub(req.ReduceNumRatio), decimal.NewFromInt(100)) default2NumPercent := utility.SafeDiv(decimal.NewFromInt(100).Sub(req.ReduceNumRatio), decimal.NewFromInt(100))
defultExt2.TotalAfter = mainAmount.Mul(default2NumPercent).Truncate(int32(tradeSet.AmountDigit)) defultExt2.TotalAfter = mainAmount.Mul(default2NumPercent).Truncate(int32(tradeSet.AmountDigit))
defultExt2.ReTakeRatio = utility.SafeDiv(req.ReducePriceRatio, default2NumPercent).Truncate(2)
preOrderExts = append(preOrderExts, defultExt)
preOrderExts = append(preOrderExts, defultExt2)
calculateResp := dto.CalculateBreakEvenRatioResp{} calculateResp := dto.CalculateBreakEvenRatioResp{}
mainParam := dto.CalculateBreakEevenRatioReq{ mainParam := dto.CalculateBreakEevenRatioReq{
@ -627,15 +645,22 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
AddPositionVal: req.ReduceNumRatio, AddPositionVal: req.ReduceNumRatio,
} }
if req.PricePattern == "mixture" {
mainParam.LossEndPercent = mainPrice.Div(req.ReducePriceRatio).Sub(decimal.NewFromInt(1)).Abs().Mul(decimal.NewFromInt(100)).Truncate(2)
defultExt2.PriceRatio = mainParam.LossEndPercent
}
//计算减仓后 //计算减仓后
mainParam.LossEndPercent = req.ReducePriceRatio defultExt2.ReTakeRatio = utility.SafeDiv(mainParam.LossEndPercent, default2NumPercent).Truncate(2)
mainParam.RemainingQuantity = mainAmount mainParam.RemainingQuantity = mainAmount
e.CalculateBreakEvenRatio(&mainParam, &calculateResp, tradeSet) e.CalculateBreakEvenRatio(&mainParam, &calculateResp, tradeSet)
mainParam.RemainingQuantity = calculateResp.RemainingQuantity //mainAmount.Mul(decimal.NewFromInt(100).Sub(req.ReduceNumRatio).Div(decimal.NewFromInt(100))).Truncate(int32(tradeSet.AmountDigit)) mainParam.RemainingQuantity = calculateResp.RemainingQuantity //mainAmount.Mul(decimal.NewFromInt(100).Sub(req.ReduceNumRatio).Div(decimal.NewFromInt(100))).Truncate(int32(tradeSet.AmountDigit))
mainParam.TotalLossAmountU = calculateResp.TotalLossAmountU //buyPrice.Mul(req.ReducePriceRatio.Div(decimal.NewFromInt(100)).Truncate(4)).Truncate(int32(tradeSet.PriceDigit)) mainParam.TotalLossAmountU = calculateResp.TotalLossAmountU //buyPrice.Mul(req.ReducePriceRatio.Div(decimal.NewFromInt(100)).Truncate(4)).Truncate(int32(tradeSet.PriceDigit))
req.ReduceReTakeProfitRatio = calculateResp.Ratio req.ReduceReTakeProfitRatio = calculateResp.Ratio
mainParam.LossBeginPercent = req.ReducePriceRatio mainParam.LossBeginPercent = mainParam.LossEndPercent
// defultExt.ReTakeRatio = calculateResp.Ratio // defultExt.ReTakeRatio = calculateResp.Ratio
preOrderExts = append(preOrderExts, defultExt)
preOrderExts = append(preOrderExts, defultExt2)
for index, addPosition := range req.Ext { for index, addPosition := range req.Ext {
ext := models.LinePreOrderExt{ ext := models.LinePreOrderExt{
@ -907,7 +932,6 @@ func saveOrderCache(req *dto.LineAddPreOrderReq, AddOrder models.LinePreOrder, l
list.Percentag = linestrategyTemplate.Percentag list.Percentag = linestrategyTemplate.Percentag
list.CompareType = linestrategyTemplate.CompareType list.CompareType = linestrategyTemplate.CompareType
list.TimeSlotStart = linestrategyTemplate.TimeSlotStart list.TimeSlotStart = linestrategyTemplate.TimeSlotStart
list.TimeSlotEnd = linestrategyTemplate.TimeSlotEnd
marshal, _ = sonic.Marshal(&list) marshal, _ = sonic.Marshal(&list)
if AddOrder.SymbolType == global.SYMBOL_SPOT { if AddOrder.SymbolType == global.SYMBOL_SPOT {
@ -1015,7 +1039,6 @@ func createPreReduceOrder(preOrder *models.LinePreOrder, ext models.LinePreOrder
stopOrder.OrderType = 4 stopOrder.OrderType = 4
stopOrder.Status = 0 stopOrder.Status = 0
stopOrder.Rate = ext.PriceRatio.String()
stopOrder.Num = ext.TotalAfter.Sub(ext.TotalBefore).Abs().Truncate(int32(tradeSet.AmountDigit)).String() stopOrder.Num = ext.TotalAfter.Sub(ext.TotalBefore).Abs().Truncate(int32(tradeSet.AmountDigit)).String()
stopOrder.BuyPrice = "0" stopOrder.BuyPrice = "0"
stopOrder.Rate = ext.PriceRatio.String() stopOrder.Rate = ext.PriceRatio.String()
@ -1995,6 +2018,8 @@ func (e *LinePreOrder) ClearUnTriggered() error {
var orderLists []models.LinePreOrder var orderLists []models.LinePreOrder
positions := map[string]positiondto.LinePreOrderPositioinDelReq{} positions := map[string]positiondto.LinePreOrderPositioinDelReq{}
e.Orm.Model(&models.LinePreOrder{}).Where("main_id = 0 AND pid = 0 AND status = '0'").Find(&orderLists).Unscoped().Delete(&models.LinePreOrder{}) e.Orm.Model(&models.LinePreOrder{}).Where("main_id = 0 AND pid = 0 AND status = '0'").Find(&orderLists).Unscoped().Delete(&models.LinePreOrder{})
spotStrategyMap := e.GetStrategyOrderListMap(1)
futStrategyMap := e.GetStrategyOrderListMap(2)
for _, order := range orderLists { for _, order := range orderLists {
redisList := dto.PreOrderRedisList{ redisList := dto.PreOrderRedisList{
@ -2006,17 +2031,35 @@ func (e *LinePreOrder) ClearUnTriggered() error {
OrderSn: order.OrderSn, OrderSn: order.OrderSn,
QuoteSymbol: order.QuoteSymbol, QuoteSymbol: order.QuoteSymbol,
} }
tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, fmt.Sprintf(global.TICKER_SPOT, order.ExchangeType, order.Symbol))
var tradedSetKey string
if order.SymbolType == 1 {
tradedSetKey = fmt.Sprintf(global.TICKER_SPOT, order.ExchangeType, order.Symbol)
} else {
tradedSetKey = fmt.Sprintf(global.TICKER_FUTURES, order.ExchangeType, order.Symbol)
}
tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, tradedSetKey)
redisList.Price = utility.StringToDecimal(redisList.Price).Truncate(int32(tradeSet.PriceDigit)).String() redisList.Price = utility.StringToDecimal(redisList.Price).Truncate(int32(tradeSet.PriceDigit)).String()
marshal, _ := sonic.Marshal(redisList) marshal, _ := sonic.Marshal(redisList)
if order.SymbolType == 1 { if order.SymbolType == 1 {
key := fmt.Sprintf(rediskey.PreFutOrderList, order.ExchangeType) key := fmt.Sprintf(rediskey.PreFutOrderList, order.ExchangeType)
helper.DefaultRedis.LRem(key, string(marshal)) helper.DefaultRedis.LRem(key, string(marshal))
} else { } else {
key := fmt.Sprintf(rediskey.PreSpotOrderList, order.ExchangeType) key := fmt.Sprintf(rediskey.PreSpotOrderList, order.ExchangeType)
helper.DefaultRedis.LRem(key, string(marshal)) helper.DefaultRedis.LRem(key, string(marshal))
} }
switch {
case order.StrategyTemplateType == 1 && order.SymbolType == 1:
e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &spotStrategyMap)
case order.StrategyTemplateType == 1 && order.SymbolType == 2:
e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &futStrategyMap)
}
//会影响持仓的 //会影响持仓的
removeSymbolKey := fmt.Sprintf("%v_%s_%s_%s_%v", order.ApiId, order.ExchangeType, order.Symbol, order.Site, order.SymbolType) removeSymbolKey := fmt.Sprintf("%v_%s_%s_%s_%v", order.ApiId, order.ExchangeType, order.Symbol, order.Site, order.SymbolType)
@ -2056,6 +2099,60 @@ func (e *LinePreOrder) ClearUnTriggered() error {
return nil return nil
} }
// 移除待策略待触发单
func (e *LinePreOrder) RemoveStrategyOrderCache(orderId int, symbolType int, exchangeType string, caches *map[string][]dto.StrategyOrderRedisList) {
strategys, _ := (*caches)[exchangeType]
var strategyListKey string
if symbolType == 1 {
strategyListKey = fmt.Sprintf(rediskey.StrategySpotOrderList, exchangeType)
} else {
strategyListKey = fmt.Sprintf(rediskey.StrategyFutOrderList, exchangeType)
}
for _, strategy := range strategys {
if strategy.Id == orderId {
strategyVal, _ := sonic.MarshalString(strategy)
helper.DefaultRedis.LRem(strategyListKey, strategyVal)
}
}
}
// 获取策略订单缓存列表
// symbolType 1现货 2合约
func (e *LinePreOrder) GetStrategyOrderListMap(symbolType int) map[string][]dto.StrategyOrderRedisList {
result := make(map[string][]dto.StrategyOrderRedisList)
var key string
exchanges := []string{global.EXCHANGE_BINANCE}
if symbolType == 1 {
key = rediskey.StrategySpotOrderList
} else {
key = rediskey.StrategyFutOrderList
}
for _, exchange := range exchanges {
newKey := fmt.Sprintf(key, exchange)
vals, _ := helper.DefaultRedis.GetAllList(newKey)
itemData := make([]dto.StrategyOrderRedisList, 0)
item := dto.StrategyOrderRedisList{}
for _, v := range vals {
sonic.Unmarshal([]byte(v), &item)
if item.Id > 0 {
itemData = append(itemData, item)
}
}
if len(itemData) > 0 {
result[exchange] = itemData
}
}
return result
}
func (e *LinePreOrder) QueryOrder(req *dto.QueryOrderReq) (res interface{}, err error) { func (e *LinePreOrder) QueryOrder(req *dto.QueryOrderReq) (res interface{}, err error) {
var apiUserInfo models.LineApiUser var apiUserInfo models.LineApiUser
e.Orm.Model(&models.LineApiUser{}).Where("id = ?", req.ApiId).Find(&apiUserInfo) e.Orm.Model(&models.LineApiUser{}).Where("id = ?", req.ApiId).Find(&apiUserInfo)
@ -2149,15 +2246,18 @@ func (e *LinePreOrder) GenerateOrder(req *dto.LineAddPreOrderReq) error {
AddPositionVal: req.ReduceNumRatio, AddPositionVal: req.ReduceNumRatio,
} }
if req.PricePattern == "mixture" {
mainParam.LossEndPercent = price.Div(req.ReducePriceRatio).Sub(decimal.NewFromInt(1)).Abs().Mul(decimal.NewFromInt(100)).Truncate(2)
}
//计算减仓后 //计算减仓后
mainParam.LossEndPercent = req.ReducePriceRatio
mainParam.RemainingQuantity = mainAmount mainParam.RemainingQuantity = mainAmount
mainParam.AddType = 2 mainParam.AddType = 2
e.CalculateBreakEvenRatio(&mainParam, &calculateResp, tradeSet) e.CalculateBreakEvenRatio(&mainParam, &calculateResp, tradeSet)
mainParam.RemainingQuantity = calculateResp.RemainingQuantity mainParam.RemainingQuantity = calculateResp.RemainingQuantity
mainParam.TotalLossAmountU = calculateResp.TotalLossAmountU mainParam.TotalLossAmountU = calculateResp.TotalLossAmountU
req.ReduceReTakeProfitRatio = calculateResp.Ratio req.ReduceReTakeProfitRatio = calculateResp.Ratio
lossBeginPercent = req.ReducePriceRatio lossBeginPercent = mainParam.LossEndPercent
//顺序排序 //顺序排序
sort.Slice(req.Ext, func(i, j int) bool { sort.Slice(req.Ext, func(i, j int) bool {

View File

@ -0,0 +1,114 @@
package service
import (
"errors"
"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"
cDto "go-admin/common/dto"
)
type LineReverseOrder struct {
service.Service
}
// GetPage 获取LineReverseOrder列表
func (e *LineReverseOrder) GetPage(c *dto.LineReverseOrderGetPageReq, p *actions.DataPermission, list *[]models.LineReverseOrder, count *int64) error {
var err error
var data models.LineReverseOrder
query := e.Orm.Model(&data).
Scopes(
cDto.MakeCondition(c.GetNeedSearch()),
cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
actions.Permission(data.TableName(), p),
)
if c.Category >= 0 {
query = query.Where("category =?", c.Category)
}
err = query.
Find(list).Limit(-1).Offset(-1).
Count(count).Error
if err != nil {
e.Log.Errorf("LineReverseOrderService GetPage error:%s \r\n", err)
return err
}
return nil
}
// Get 获取LineReverseOrder对象
func (e *LineReverseOrder) Get(d *dto.LineReverseOrderGetReq, p *actions.DataPermission, model *models.LineReverseOrder) error {
var data models.LineReverseOrder
err := e.Orm.Model(&data).
Scopes(
actions.Permission(data.TableName(), p),
).
First(model, d.GetId()).Error
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
err = errors.New("查看对象不存在或无权查看")
e.Log.Errorf("Service GetLineReverseOrder error:%s \r\n", err)
return err
}
if err != nil {
e.Log.Errorf("db error:%s", err)
return err
}
return nil
}
// Insert 创建LineReverseOrder对象
func (e *LineReverseOrder) Insert(c *dto.LineReverseOrderInsertReq) error {
var err error
var data models.LineReverseOrder
c.Generate(&data)
err = e.Orm.Create(&data).Error
if err != nil {
e.Log.Errorf("LineReverseOrderService Insert error:%s \r\n", err)
return err
}
return nil
}
// Update 修改LineReverseOrder对象
func (e *LineReverseOrder) Update(c *dto.LineReverseOrderUpdateReq, p *actions.DataPermission) error {
var err error
var data = models.LineReverseOrder{}
e.Orm.Scopes(
actions.Permission(data.TableName(), p),
).First(&data, c.GetId())
c.Generate(&data)
db := e.Orm.Save(&data)
if err = db.Error; err != nil {
e.Log.Errorf("LineReverseOrderService Save error:%s \r\n", err)
return err
}
if db.RowsAffected == 0 {
return errors.New("无权更新该数据")
}
return nil
}
// Remove 删除LineReverseOrder
func (e *LineReverseOrder) Remove(d *dto.LineReverseOrderDeleteReq, p *actions.DataPermission) error {
var data models.LineReverseOrder
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 RemoveLineReverseOrder error:%s \r\n", err)
return err
}
if db.RowsAffected == 0 {
return errors.New("无权删除该数据")
}
return nil
}

View File

@ -0,0 +1,324 @@
package service
import (
"errors"
"fmt"
"time"
"github.com/go-admin-team/go-admin-core/sdk/service"
"github.com/jinzhu/copier"
"github.com/shopspring/decimal"
"gorm.io/gorm"
"go-admin/app/admin/models"
"go-admin/app/admin/service/dto"
"go-admin/common/actions"
cDto "go-admin/common/dto"
"go-admin/common/global"
"go-admin/pkg/utility"
"go-admin/pkg/utility/snowflakehelper"
"go-admin/services/binanceservice"
"go-admin/services/cacheservice"
)
type LineReversePosition struct {
service.Service
}
// 批量关闭仓位
func (e LineReversePosition) ClosePositionBatch(req *dto.LineReversePositionCloseBatchReq, p *actions.DataPermission, userId int, errs *[]string) error {
var positions []models.LineReversePosition
var entity models.LineReversePosition
query := e.Orm.Model(&entity).
Scopes(
actions.Permission(entity.TableName(), p),
).
Where("reverse_status = 1 and position_side =?", req.PositionSide)
if len(req.Symbol) > 0 {
query = query.Where("symbol =?", req.Symbol)
}
if len(req.ReverseApiIds) > 0 {
query = query.Where("reverse_api_id in (?)", req.ReverseApiIds)
}
if err := query.Find(&positions).Error; err != nil {
e.Log.Errorf("LineReversePositionService ClosePositionBatch error:%s \r\n", err)
return err
}
if len(positions) == 0 {
return errors.New("没有需要关闭的仓位")
}
for _, position := range positions {
if err1 := e.Close(position); err1 != nil {
*errs = append(*errs, err1.Error())
}
}
return nil
}
// ClosePosition 关闭单个仓位
func (e LineReversePosition) ClosePosition(req *dto.LineReversePositionCloseReq, p *actions.DataPermission, userId int) error {
var data models.LineReversePosition
err := e.Orm.Model(&data).
Scopes(
actions.Permission(data.TableName(), p),
).
Where("id =?", req.PositionId).First(&data).Error
if err != nil {
e.Log.Errorf("LineReversePositionService ClosePosition error:%s \r\n", err)
return err
}
err = e.Close(data)
return err
}
func (e *LineReversePosition) Close(data models.LineReversePosition) error {
if data.ReverseStatus != 1 {
return fmt.Errorf("%s-%s 仓位无法关闭", data.Symbol, data.PositionSide)
}
apiInfo, err := binanceservice.GetApiInfo(data.ReverseApiId)
if err != nil {
return fmt.Errorf("api %d不存在", data.ReverseApiId)
}
futApi := binanceservice.FutRestApi{}
futApiV2 := binanceservice.FuturesResetV2{}
holdData := binanceservice.HoldeData{}
err = futApi.GetPositionData(&apiInfo, data.Symbol, data.PositionSide, &holdData)
if err != nil {
return fmt.Errorf("获取币安持仓失败 %v", err)
}
setting, err := cacheservice.GetReverseSetting(e.Orm)
if err != nil {
return fmt.Errorf("获取反单设置失败")
}
symbol, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, data.Symbol, 0)
if err != nil {
return fmt.Errorf("获取%s的交易对信息失败", data.Symbol)
}
lastPrice, err := decimal.NewFromString(symbol.LastPrice)
if err != nil {
return fmt.Errorf("最新价格失败,%v", lastPrice)
}
side := ""
var price decimal.Decimal
if data.PositionSide == "LONG" {
side = "SELL"
price = decimal.NewFromInt(100).Sub(setting.ReversePremiumRatio).Div(decimal.NewFromInt(100)).Mul(lastPrice).Round(int32(symbol.PriceDigit))
} else {
side = "BUY"
price = decimal.NewFromInt(100).Add(setting.ReversePremiumRatio).Div(decimal.NewFromInt(100)).Mul(lastPrice).Round(int32(symbol.PriceDigit))
}
now := time.Now()
order := models.LineReverseOrder{
OrderSn: snowflakehelper.GetOrderNo(),
PositionId: data.Id,
PositionSide: data.PositionSide,
Symbol: data.Symbol,
TotalNum: holdData.TotalQuantity,
Category: 1,
OrderType: 4,
Side: side,
Price: price,
SignPrice: lastPrice,
Type: "LIMIT",
TriggerTime: &now,
Status: 1,
}
if holdData.TotalQuantity.Cmp(decimal.Zero) > 0 {
if err := e.Orm.Create(&order).Error; err != nil {
return err
}
params := binanceservice.FutOrderPlace{
ApiId: data.ReverseApiId,
Symbol: data.Symbol,
Side: order.Side,
PositionSide: order.PositionSide,
Quantity: order.TotalNum,
Price: order.Price,
SideType: order.Type,
OpenOrder: 0,
OrderType: "LIMIT",
NewClientOrderId: order.OrderSn,
}
if err := futApiV2.OrderPlaceLoop(&apiInfo, params); err != nil {
e.Log.Errorf("币安下单失败 symbol:%s custom:%s :%v", params.Symbol, order.OrderSn, err)
if err1 := e.Orm.Model(&order).Where("status = 1").Updates(map[string]interface{}{"status": 8, "remark": err.Error(), "updated_at": time.Now()}).Error; err1 != nil {
e.Log.Errorf("更新订单状态失败 symbol:%s custom:%s :%v", params.Symbol, order.OrderSn, err1)
return err1
}
return err
}
} else {
order.Status = 8
order.Remark = "已经没有持仓"
err = e.Orm.Transaction(func(tx *gorm.DB) error {
if err1 := tx.Create(&order).Error; err1 != nil {
return err1
}
if err1 := tx.Model(&data).Where("reverse_status =1").Updates(map[string]interface{}{"reverse_status": 2, "updated_at": time.Now(), "reverse_amount": 0}).Error; err1 != nil {
return err1
}
return nil
})
if err != nil {
e.Log.Errorf("修改失败 %v", err)
}
}
return nil
}
// GetPage 获取LineReversePosition列表
func (e *LineReversePosition) GetPage(c *dto.LineReversePositionGetPageReq, p *actions.DataPermission, list *[]dto.LineReversePositionListResp, count *int64) error {
var err error
var data models.LineReversePosition
var datas []models.LineReversePosition
err = e.Orm.Model(&data).
Scopes(
cDto.MakeCondition(c.GetNeedSearch()),
cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
actions.Permission(data.TableName(), p),
).
Find(&datas).Limit(-1).Offset(-1).
Count(count).Error
if err != nil {
e.Log.Errorf("LineReversePositionService GetPage error:%s \r\n", err)
return err
}
userIds := make([]int, 0)
for _, item := range datas {
if !utility.ContainsInt(userIds, item.ApiId) {
userIds = append(userIds, item.ApiId)
}
if !utility.ContainsInt(userIds, item.ReverseApiId) {
userIds = append(userIds, item.ReverseApiId)
}
}
userMap := make(map[int]string, 0)
if len(userIds) > 0 {
var users []models.LineApiUser
e.Orm.Model(&models.LineApiUser{}).
Where("id IN (?)", userIds).
Find(&users)
for _, item := range users {
userMap[int(item.Id)] = item.ApiName
}
}
for _, item := range datas {
var resp dto.LineReversePositionListResp
copier.Copy(&resp, &item)
if userName, ok := userMap[item.ApiId]; ok {
resp.ApiName = userName
}
if userName, ok := userMap[item.ReverseApiId]; ok {
resp.ReverseApiName = userName
}
*list = append(*list, resp)
}
return nil
}
// 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(
actions.Permission(data.TableName(), p),
).
First(model, d.GetId()).Error
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
err = errors.New("查看对象不存在或无权查看")
e.Log.Errorf("Service GetLineReversePosition error:%s \r\n", err)
return err
}
if err != nil {
e.Log.Errorf("db error:%s", err)
return err
}
return nil
}
// Insert 创建LineReversePosition对象
func (e *LineReversePosition) Insert(c *dto.LineReversePositionInsertReq) error {
var err error
var data models.LineReversePosition
c.Generate(&data)
err = e.Orm.Create(&data).Error
if err != nil {
e.Log.Errorf("LineReversePositionService Insert error:%s \r\n", err)
return err
}
return nil
}
// Update 修改LineReversePosition对象
func (e *LineReversePosition) Update(c *dto.LineReversePositionUpdateReq, p *actions.DataPermission) error {
var err error
var data = models.LineReversePosition{}
e.Orm.Scopes(
actions.Permission(data.TableName(), p),
).First(&data, c.GetId())
c.Generate(&data)
db := e.Orm.Save(&data)
if err = db.Error; err != nil {
e.Log.Errorf("LineReversePositionService Save error:%s \r\n", err)
return err
}
if db.RowsAffected == 0 {
return errors.New("无权更新该数据")
}
return nil
}
// Remove 删除LineReversePosition
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 RemoveLineReversePosition error:%s \r\n", err)
return err
}
if db.RowsAffected == 0 {
return errors.New("无权删除该数据")
}
return nil
}

View File

@ -0,0 +1,136 @@
package service
import (
"errors"
"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 {
service.Service
}
// GetPage 获取LineReverseSetting列表
func (e *LineReverseSetting) GetPage(c *dto.LineReverseSettingGetPageReq, p *actions.DataPermission, list *[]models.LineReverseSetting, count *int64) error {
var err error
var data models.LineReverseSetting
err = e.Orm.Model(&data).
Scopes(
cDto.MakeCondition(c.GetNeedSearch()),
cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
actions.Permission(data.TableName(), p),
).
Find(list).Limit(-1).Offset(-1).
Count(count).Error
if err != nil {
e.Log.Errorf("LineReverseSettingService GetPage error:%s \r\n", err)
return err
}
return nil
}
// Get 获取LineReverseSetting对象
func (e *LineReverseSetting) Get(d *dto.LineReverseSettingGetReq, p *actions.DataPermission, model *models.LineReverseSetting) error {
var data models.LineReverseSetting
err := e.Orm.Model(&data).
Scopes(
actions.Permission(data.TableName(), p),
).
First(model, d.GetId()).Error
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
err = errors.New("查看对象不存在或无权查看")
e.Log.Errorf("Service GetLineReverseSetting error:%s \r\n", err)
return err
}
if err != nil {
e.Log.Errorf("db error:%s", err)
return err
}
return nil
}
// Insert 创建LineReverseSetting对象
func (e *LineReverseSetting) Insert(c *dto.LineReverseSettingInsertReq) error {
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)
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
func (e *LineReverseSetting) Remove(d *dto.LineReverseSettingDeleteReq, p *actions.DataPermission) error {
var data models.LineReverseSetting
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 RemoveLineReverseSetting error:%s \r\n", err)
return err
}
if db.RowsAffected == 0 {
return errors.New("无权删除该数据")
}
return nil
}

View File

@ -3,7 +3,7 @@ package service
import ( import (
"errors" "errors"
"github.com/go-admin-team/go-admin-core/sdk/service" "github.com/go-admin-team/go-admin-core/sdk/service"
"gorm.io/gorm" "gorm.io/gorm"
"go-admin/app/admin/models" "go-admin/app/admin/models"
@ -59,9 +59,9 @@ func (e *LineUserSetting) Get(d *dto.LineUserSettingGetReq, p *actions.DataPermi
// Insert 创建LineUserSetting对象 // Insert 创建LineUserSetting对象
func (e *LineUserSetting) Insert(c *dto.LineUserSettingInsertReq) error { func (e *LineUserSetting) Insert(c *dto.LineUserSettingInsertReq) error {
var err error var err error
var data models.LineUserSetting var data models.LineUserSetting
c.Generate(&data) c.Generate(&data)
err = e.Orm.Create(&data).Error err = e.Orm.Create(&data).Error
if err != nil { if err != nil {
e.Log.Errorf("LineUserSettingService Insert error:%s \r\n", err) e.Log.Errorf("LineUserSettingService Insert error:%s \r\n", err)
@ -72,22 +72,22 @@ func (e *LineUserSetting) Insert(c *dto.LineUserSettingInsertReq) error {
// Update 修改LineUserSetting对象 // Update 修改LineUserSetting对象
func (e *LineUserSetting) Update(c *dto.LineUserSettingUpdateReq, p *actions.DataPermission) error { func (e *LineUserSetting) Update(c *dto.LineUserSettingUpdateReq, p *actions.DataPermission) error {
var err error var err error
var data = models.LineUserSetting{} var data = models.LineUserSetting{}
e.Orm.Scopes( e.Orm.Scopes(
actions.Permission(data.TableName(), p), actions.Permission(data.TableName(), p),
).First(&data, c.GetId()) ).First(&data, c.GetId())
c.Generate(&data) c.Generate(&data)
db := e.Orm.Save(&data) db := e.Orm.Save(&data)
if err = db.Error; err != nil { if err = db.Error; err != nil {
e.Log.Errorf("LineUserSettingService Save error:%s \r\n", err) e.Log.Errorf("LineUserSettingService Save error:%s \r\n", err)
return err return err
} }
if db.RowsAffected == 0 { if db.RowsAffected == 0 {
return errors.New("无权更新该数据") return errors.New("无权更新该数据")
} }
return nil return nil
} }
// Remove 删除LineUserSetting // Remove 删除LineUserSetting
@ -99,11 +99,23 @@ func (e *LineUserSetting) Remove(d *dto.LineUserSettingDeleteReq, p *actions.Dat
actions.Permission(data.TableName(), p), actions.Permission(data.TableName(), p),
).Delete(&data, d.GetId()) ).Delete(&data, d.GetId())
if err := db.Error; err != nil { if err := db.Error; err != nil {
e.Log.Errorf("Service RemoveLineUserSetting error:%s \r\n", err) e.Log.Errorf("Service RemoveLineUserSetting error:%s \r\n", err)
return err return err
} }
if db.RowsAffected == 0 { if db.RowsAffected == 0 {
return errors.New("无权删除该数据") return errors.New("无权删除该数据")
} }
return nil return nil
} }
// GetDefault 获取默认LineUserSetting对象
func (e *LineUserSetting) GetDefault() (models.LineUserSetting, error) {
var data models.LineUserSetting
if err := e.Orm.Model(&data).First(&data).Error; err != nil {
e.Log.Errorf("GetDefault LineUserSetting error:%s \r\n", err)
return data, err
}
return data, nil
}

67
app/jobs/account_job.go Normal file
View File

@ -0,0 +1,67 @@
package jobs
import (
binancedto "go-admin/models/binancedto"
"go-admin/services/binanceservice"
DbModels "go-admin/app/admin/models"
"github.com/go-admin-team/go-admin-core/logger"
"github.com/shopspring/decimal"
)
type BinanceSpotAccountJob struct{}
type BinanceFuturesAccountJob struct{}
// 币安账户划转
func (t BinanceSpotAccountJob) Exec(arg interface{}) error {
db := getDefaultDb()
req := binancedto.BinanceTransfer{
Type: "MAIN_UMFUTURE",
Asset: "USDT",
Amount: decimal.NewFromFloat(0.1),
FromSymbol: "USDT",
ToSymbol: "USDT",
}
var apis []DbModels.LineApiUser
if err := db.Model(&DbModels.LineApiUser{}).Where("open_status = 1").Find(&apis).Error; err != nil {
return err
}
for _, apiUserInfo := range apis {
err := binanceservice.TradeAmount(db, &req, apiUserInfo)
if err != nil {
logger.Errorf("现货划转合约失败, err: %s", err)
}
}
return nil
}
// 币安账户划转
func (t BinanceFuturesAccountJob) Exec(arg interface{}) error {
db := getDefaultDb()
req := binancedto.BinanceTransfer{
Type: "UMFUTURE_MAIN",
Asset: "USDT",
Amount: decimal.NewFromFloat(0.1),
FromSymbol: "USDT",
ToSymbol: "USDT",
}
var apis []DbModels.LineApiUser
if err := db.Model(&DbModels.LineApiUser{}).Where("open_status = 1").Find(&apis).Error; err != nil {
return err
}
for _, apiUserInfo := range apis {
err := binanceservice.TradeAmount(db, &req, apiUserInfo)
if err != nil {
logger.Errorf("合约划转现货失败, err: %s", err)
}
}
return nil
}

View File

@ -0,0 +1,34 @@
package jobs
import (
"go-admin/common/helper"
"testing"
"github.com/go-admin-team/go-admin-core/sdk"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func TestAccountJob(t *testing.T) {
dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
sdk.Runtime.SetDb("default", db)
helper.InitDefaultRedis("127.0.0.1:6379", "", 2)
helper.InitLockRedisConn("127.0.0.1:6379", "", "2")
accountJob := BinanceSpotAccountJob{}
accountJob.Exec(nil)
}
func TestFutureAccountJob(t *testing.T) {
dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
sdk.Runtime.SetDb("default", db)
helper.InitDefaultRedis("127.0.0.1:6379", "", 2)
helper.InitLockRedisConn("127.0.0.1:6379", "", "2")
accountJob := BinanceFuturesAccountJob{}
accountJob.Exec(nil)
}

View File

@ -38,6 +38,9 @@ func InitJob() {
"MemberExpirationJob": MemberExpirationJob{}, //会员到期处理 "MemberExpirationJob": MemberExpirationJob{}, //会员到期处理
"MemberRenwalOrderExpirationJob": MemberRenwalOrderExpirationJob{}, //会员续费订单过期处理 "MemberRenwalOrderExpirationJob": MemberRenwalOrderExpirationJob{}, //会员续费订单过期处理
"TrxQueryJobs": TrxQueryJobs{}, //订单支付监听 "TrxQueryJobs": TrxQueryJobs{}, //订单支付监听
"StrategyJob": StrategyJob{}, //下单策略触发
"BinanceSpotAccountJob": BinanceSpotAccountJob{}, //币安现货划转
"BinanceFuturesAccountJob": BinanceFuturesAccountJob{}, //币安合约划转
} }
} }

View File

@ -3,6 +3,7 @@ package jobs
import ( import (
"fmt" "fmt"
models2 "go-admin/app/jobs/models" models2 "go-admin/app/jobs/models"
"runtime"
"time" "time"
log "github.com/go-admin-team/go-admin-core/logger" log "github.com/go-admin-team/go-admin-core/logger"
@ -43,7 +44,10 @@ type ExecJob struct {
func (e *ExecJob) Run() { func (e *ExecJob) Run() {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
log.Errorf("脚本任务失败:%v", err) // 获取调用栈信息
buf := make([]byte, 1<<16) // 64KB 缓冲区
n := runtime.Stack(buf, false)
log.Errorf("脚本任务失败: %v\n%s", err, buf[:n])
} }
}() }()

25
app/jobs/strategy_job.go Normal file
View File

@ -0,0 +1,25 @@
package jobs
import (
"go-admin/common/global"
"go-admin/services/binanceservice"
"github.com/go-admin-team/go-admin-core/logger"
"github.com/go-admin-team/go-admin-core/sdk"
)
type StrategyJob struct {
}
// 策略下单任务
func (j StrategyJob) Exec(arg interface{}) error {
strategyService := binanceservice.BinanceStrategyOrderService{}
db := getDefaultDb()
strategyService.Orm = db
strategyService.Log = logger.NewHelper(sdk.Runtime.GetLogger()).WithFields(map[string]interface{}{})
//触发币安策略下单
strategyService.TriggerStrategyOrder(global.EXCHANGE_BINANCE)
return nil
}

View File

@ -0,0 +1,22 @@
package jobs
import (
"go-admin/common/helper"
"testing"
"github.com/go-admin-team/go-admin-core/sdk"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func TestStrategyJob(t *testing.T) {
dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
sdk.Runtime.SetDb("default", db)
helper.InitDefaultRedis("127.0.0.1:6379", "", 2)
helper.InitLockRedisConn("127.0.0.1:6379", "", "2")
job := StrategyJob{}
job.Exec([]string{})
}

View File

@ -86,6 +86,11 @@ func run() error {
clearLogJob(db, ctx) clearLogJob(db, ctx)
}) })
//自动重启websocket
utility.SafeGo(func() {
reconnect(ctx)
})
// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间) // 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)
quit := make(chan os.Signal, 1) quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt) signal.Notify(quit, os.Interrupt)
@ -133,3 +138,33 @@ func clearLogJob(db *gorm.DB, ctx context.Context) {
fileservice.ClearLogs(db) 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()
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,6 @@
package binancecode
const (
SideBuy = "BUY"
SideSell = "SELL"
)

View File

@ -0,0 +1,6 @@
package rediskey
const (
//主单api和反单关系 List {apiId}:{reverseApiId}
ApiReverseRelation = "api_reverse_relation"
)

View File

@ -46,9 +46,9 @@ const (
SpotTrigger = "spot_trigger_lock:%v_%s" //现货触发 {apiuserid|symbol} SpotTrigger = "spot_trigger_lock:%v_%s" //现货触发 {apiuserid|symbol}
FutTrigger = "fut_trigger_lock:%v_%s" //合约触发 {apiuserid|symbol} FutTrigger = "fut_trigger_lock:%v_%s" //合约触发 {apiuserid|symbol}
//波段现货触发{apiuserid|symbol} //波段现货触发{apiuserid|ordersn}
StrategySpotTriggerLock = "strategy_spot_trigger_l:%v_%s" StrategySpotTriggerLock = "strategy_spot_trigger_l:%v_%s"
//波段合约触发{apiuserid|symbol} //波段合约触发{apiuserid|ordersn}
StrategyFutTriggerLock = "strategy_fut_trigger_l:%v_%s" StrategyFutTriggerLock = "strategy_fut_trigger_l:%v_%s"
//减仓波段合约触发 {apiuserid|symbol} //减仓波段合约触发 {apiuserid|symbol}
@ -95,9 +95,9 @@ const (
) )
const ( const (
//现货最后成交价 sort set {exchangeType,symbol} //现货最后成交价 sort set key:={exchangeType,symbol} content:={utc:price}
SpotTickerLastPrice = "spot_ticker_last_price:%s:%s" SpotTickerLastPrice = "spot_ticker_last_price:%s:%s"
//合约最后成交价 sort set {exchangeType,symbol} //合约最后成交价 sort set {exchangeType,symbol} content:={utc:price}
FutureTickerLastPrice = "fut_ticker_last_price:%s:%s" FutureTickerLastPrice = "fut_ticker_last_price:%s:%s"
//允许缓存交易对价格的交易对 list //允许缓存交易对价格的交易对 list

View File

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

View File

@ -12,6 +12,7 @@ import (
"github.com/bytedance/sonic" "github.com/bytedance/sonic"
"github.com/go-redis/redis/v8" "github.com/go-redis/redis/v8"
"github.com/shopspring/decimal"
) )
// RedisHelper 结构体封装了 Redis 客户端及上下文 // RedisHelper 结构体封装了 Redis 客户端及上下文
@ -460,44 +461,76 @@ func (r *RedisHelper) SetNX(key string, value interface{}, expiration time.Durat
return result == "OK", nil return result == "OK", nil
} }
func getFieldsFromStruct(obj interface{}) map[string]interface{} { // SetHashWithTags 改进版:支持 struct 或 map 输入
func (r *RedisHelper) SetHashWithTags(key string, obj interface{}) error {
fields, err := getFieldsFromStruct(obj)
if err != nil {
return err
}
_, err = r.client.HSet(r.ctx, key, fields).Result()
return err
}
// getFieldsFromStruct 改进版:处理指针并进行类型检查,返回 error
func getFieldsFromStruct(obj interface{}) (map[string]interface{}, error) {
fields := make(map[string]interface{}) fields := make(map[string]interface{})
val := reflect.ValueOf(obj) 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++ { for i := 0; i < val.NumField(); i++ {
field := typ.Field(i) field := typ.Field(i)
tag := field.Tag.Get("redis") tag := field.Tag.Get("redis") // 获取 redis tag
if tag != "" { if tag != "" {
fieldVal := val.Field(i) 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 { if fieldVal.Kind() == reflect.Slice {
for j := 0; j < fieldVal.Len(); j++ { for j := 0; j < fieldVal.Len(); j++ {
elem := fieldVal.Index(j).Interface() elem := fieldVal.Index(j).Interface()
fields[fmt.Sprintf("%s_%d", tag, j)] = elem fields[fmt.Sprintf("%s_%d", tag, j)] = elem
} }
} else if fieldVal.Kind() == reflect.Map { } else if fieldVal.Kind() == reflect.Map {
// 对于映射,使用键作为字段名
for _, key := range fieldVal.MapKeys() { for _, key := range fieldVal.MapKeys() {
elem := fieldVal.MapIndex(key).Interface() elem := fieldVal.MapIndex(key).Interface()
fields[fmt.Sprintf("%s_%v", tag, key.Interface())] = elem 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() fields[tag] = fieldVal.Interface()
} }
} }
} }
return fields, nil
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
} }
// HSetField 设置哈希中的一个字段 // HSetField 设置哈希中的一个字段
@ -543,6 +576,82 @@ func (r *RedisHelper) HGetAllFields(key string) (map[string]string, error) {
return fields, nil 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 删除哈希中的某个字段 // HDelField 删除哈希中的某个字段
func (r *RedisHelper) HDelField(key, field string) error { func (r *RedisHelper) HDelField(key, field string) error {
_, err := r.client.HDel(r.ctx, key, field).Result() _, err := r.client.HDel(r.ctx, key, field).Result()

View File

@ -106,6 +106,9 @@ func (rl *RedisLock) AcquireWait(ctx context.Context) (bool, error) {
baseInterval = time.Second baseInterval = time.Second
} }
if baseInterval <= 0 {
baseInterval = time.Millisecond * 100 // 至少 100ms
}
// 随机退避 // 随机退避
retryInterval := time.Duration(rand.Int63n(int64(baseInterval))) // 随机退避 retryInterval := time.Duration(rand.Int63n(int64(baseInterval))) // 随机退避
if retryInterval < time.Millisecond*100 { if retryInterval < time.Millisecond*100 {
@ -129,6 +132,13 @@ func (rl *RedisLock) AcquireWait(ctx context.Context) (bool, error) {
return false, ErrFailed return false, ErrFailed
} }
func safeRandomDuration(max time.Duration) time.Duration {
if max <= 0 {
return 100 * time.Millisecond // fallback default
}
return time.Duration(rand.Int63n(int64(max)))
}
// Release 释放锁 // Release 释放锁
func (rl *RedisLock) Release() (bool, error) { func (rl *RedisLock) Release() (bool, error) {
return rl.releaseCtx(context.Background()) return rl.releaseCtx(context.Background())

View File

@ -13,6 +13,7 @@ import (
"go-admin/services/futureservice" "go-admin/services/futureservice"
"go-admin/services/scriptservice" "go-admin/services/scriptservice"
"os" "os"
"time"
"github.com/bytedance/sonic" "github.com/bytedance/sonic"
"github.com/go-admin-team/go-admin-core/logger" "github.com/go-admin-team/go-admin-core/logger"
@ -29,6 +30,24 @@ func BusinessInit(db *gorm.DB) {
os.Exit(-1) 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) cacheservice.InitConfigCache(db)
//初始化可缓存价格交易对 //初始化可缓存价格交易对
@ -37,6 +56,9 @@ func BusinessInit(db *gorm.DB) {
symbolPriceService.Log = logger.NewHelper(sdk.Runtime.GetLogger()).WithFields(map[string]interface{}{}) symbolPriceService.Log = logger.NewHelper(sdk.Runtime.GetLogger()).WithFields(map[string]interface{}{})
symbolPriceService.InitCache() symbolPriceService.InitCache()
//清理交易对缓存价格
clearSymbolPrice()
//初始化订单配置 //初始化订单配置
cacheservice.ResetSystemSetting(db) cacheservice.ResetSystemSetting(db)
lineApiUser := service.LineApiUser{} lineApiUser := service.LineApiUser{}
@ -145,3 +167,24 @@ func loadApiUser(db *gorm.DB) error {
return nil return nil
} }
// 清理交易对价格缓存
func clearSymbolPrice() error {
spotAll, _ := helper.DefaultRedis.ScanKeys("spot_ticker_last_price:*")
futAllKey, _ := helper.DefaultRedis.ScanKeys("fut_ticker_last_price:*")
beforeTimeUtc := time.Now().UnixMilli()
for _, item := range spotAll {
if _, err := helper.DefaultRedis.RemoveBeforeScore(item, float64(beforeTimeUtc)); err != nil {
logger.Error("现货 清理交易对价格缓存失败:", err)
}
}
for _, item := range futAllKey {
if _, err := helper.DefaultRedis.RemoveBeforeScore(item, float64(beforeTimeUtc)); err != nil {
logger.Error("合约 清理交易对价格缓存失败:", err)
}
}
return nil
}

View File

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

View File

@ -0,0 +1,11 @@
package binancedto
import "github.com/shopspring/decimal"
type BinanceTransfer struct {
Type string `json:"type" content:"枚举 MAIN_UMFUTURE-现货到u合约"`
Asset string `json:"asset" content:"币种"`
Amount decimal.Decimal `json:"amount" content:"数量"`
FromSymbol string `json:"fromSymbol" content:"转出币种"`
ToSymbol string `json:"toSymbol" content:"转入币种"`
}

View File

@ -37,6 +37,10 @@ type TradeSet struct {
E int64 `json:"-"` //推送时间 E int64 `json:"-"` //推送时间
} }
func (e *TradeSet) GetSymbol() string {
return e.Coin + e.Currency
}
//CommissionType int `db:"commissiontype"` //手续费:1买,2卖,3双向 //CommissionType int `db:"commissiontype"` //手续费:1买,2卖,3双向
//DepositNum float64 `db:"depositnum" json:"depositnum"` //保证金规模(手) //DepositNum float64 `db:"depositnum" json:"depositnum"` //保证金规模(手)
//ForceRate float64 `db:"forcerate" json:"forcerate"` //维持保证金率1% //ForceRate float64 `db:"forcerate" json:"forcerate"` //维持保证金率1%

View File

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

View File

@ -0,0 +1,53 @@
package retryhelper
import (
"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, err
}

View File

@ -28,3 +28,7 @@ func init() {
func GetOrderId() int64 { func GetOrderId() int64 {
return snowNode.Generate().Int64() return snowNode.Generate().Int64()
} }
func GetOrderNo() string {
return fmt.Sprintf("%d", GetOrderId())
}

View File

@ -467,6 +467,21 @@ func GetApiInfo(apiId int) (DbModels.LineApiUser, error) {
return api, nil return api, nil
} }
// GetReverseApiInfo 根据apiKey获取api用户信息
func GetApiInfoByKey(apiKey string) DbModels.LineApiUser {
result := DbModels.LineApiUser{}
keys, _ := helper.DefaultRedis.GetAllKeysAndValues(fmt.Sprintf(rediskey.API_USER, "*"))
for _, key := range keys {
if strings.Contains(key, apiKey) {
sonic.UnmarshalString(key, &result)
break
}
}
return result
}
/* /*
根据A账户获取B账号信息 根据A账户获取B账号信息
*/ */
@ -640,3 +655,43 @@ func GetSpotUProperty(apiUserInfo DbModels.LineApiUser, data *dto.LineUserProper
return nil return nil
} }
// 万象划转
func TradeAmount(db *gorm.DB, req *binancedto.BinanceTransfer, apiUserInfo DbModels.LineApiUser) error {
url := "/sapi/v1/asset/transfer"
client := GetClient(&apiUserInfo)
params := map[string]string{
"type": req.Type,
"asset": req.Asset,
"amount": req.Amount.String(),
"fromSymbol": req.FromSymbol,
"toSymbol": req.ToSymbol,
"recvWindow": "10000",
}
_, code, err := client.SendSpotAuth(url, "POST", params)
if err != nil || code != 200 {
log.Error("万向划转失败 参数:", params)
log.Error("万向划转失败 code:", code)
log.Error("万向划转失败 err:", err)
dataMap := make(map[string]interface{})
if err.Error() != "" {
if err := sonic.Unmarshal([]byte(err.Error()), &dataMap); err != nil {
return fmt.Errorf("api_id:%d 万向划转失败:%+v", apiUserInfo.Id, err.Error())
}
}
code, ok := dataMap["code"]
if ok {
return fmt.Errorf("api_id:%d 万向划转失败:%s", apiUserInfo.Id, ErrorMaps[code.(float64)])
}
if strings.Contains(err.Error(), "Unknown order sent.") {
return fmt.Errorf("api_id:%d 万向划转失败:%+v", apiUserInfo.Id, ErrorMaps[-2011])
}
return fmt.Errorf("api_id:%d 万向划转失败:%+v", apiUserInfo.Id, err.Error())
}
return nil
}

View File

@ -251,11 +251,15 @@ func (e *AddPosition) CalculateAmount(req dto.ManuallyCover, totalNum, lastPrice
// mainOrderId 主单id // mainOrderId 主单id
// symbolType 1现货 2合约 // symbolType 1现货 2合约
func MainClosePositionClearCache(mainId int, symbolType int) { func MainClosePositionClearCache(mainId int, symbolType int) {
strategyDto := dto.StrategyOrderRedisList{}
if symbolType == 1 { if symbolType == 1 {
keySpotStop := fmt.Sprintf(rediskey.SpotStopLossList, global.EXCHANGE_BINANCE) keySpotStop := fmt.Sprintf(rediskey.SpotStopLossList, global.EXCHANGE_BINANCE)
keySpotAddposition := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE) keySpotAddposition := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE)
spotStopArray, _ := helper.DefaultRedis.GetAllList(keySpotStop) spotStopArray, _ := helper.DefaultRedis.GetAllList(keySpotStop)
spotAddpositionArray, _ := helper.DefaultRedis.GetAllList(keySpotAddposition) spotAddpositionArray, _ := helper.DefaultRedis.GetAllList(keySpotAddposition)
spotStrategyKey := fmt.Sprintf(rediskey.StrategySpotOrderList, global.EXCHANGE_BINANCE)
spotStrategyList, _ := helper.DefaultRedis.GetAllList(spotStrategyKey)
var position AddPositionList var position AddPositionList
var stop dto.StopLossRedisList var stop dto.StopLossRedisList
@ -279,11 +283,23 @@ func MainClosePositionClearCache(mainId int, symbolType int) {
} }
} }
for _, item := range spotStrategyList {
sonic.Unmarshal([]byte(item), &strategyDto)
if strategyDto.Id == mainId {
if _, err := helper.DefaultRedis.LRem(spotStrategyKey, item); err != nil {
logger.Errorf("id:%d 移除缓存失败,err:%v", mainId, err)
}
}
}
} else { } else {
keyFutStop := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE) keyFutStop := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE)
keyFutAddposition := fmt.Sprintf(rediskey.FuturesStopLossList, global.EXCHANGE_BINANCE) keyFutAddposition := fmt.Sprintf(rediskey.FuturesStopLossList, global.EXCHANGE_BINANCE)
futAddpositionArray, _ := helper.DefaultRedis.GetAllList(keyFutStop) futAddpositionArray, _ := helper.DefaultRedis.GetAllList(keyFutStop)
futStopArray, _ := helper.DefaultRedis.GetAllList(keyFutAddposition) futStopArray, _ := helper.DefaultRedis.GetAllList(keyFutAddposition)
futStrategyKey := fmt.Sprintf(rediskey.StrategyFutOrderList, global.EXCHANGE_BINANCE)
futStrategyList, _ := helper.DefaultRedis.GetAllList(futStrategyKey)
var position AddPositionList var position AddPositionList
var stop dto.StopLossRedisList var stop dto.StopLossRedisList
@ -306,6 +322,16 @@ func MainClosePositionClearCache(mainId int, symbolType int) {
helper.DefaultRedis.LRem(keyFutStop, item) helper.DefaultRedis.LRem(keyFutStop, item)
} }
} }
for _, item := range futStrategyList {
sonic.Unmarshal([]byte(item), &strategyDto)
if strategyDto.Id == mainId {
if _, err := helper.DefaultRedis.LRem(futStrategyKey, item); err != nil {
logger.Errorf("id:%d 移除缓存失败,err:%v", mainId, err)
}
}
}
} }
} }
@ -453,3 +479,13 @@ func GetOpenOrderSns(db *gorm.DB, mainIds []int) ([]string, error) {
return result, nil return result, nil
} }
// 回去反单默认配置
func GetReverseSetting(db *gorm.DB) (DbModels.LineReverseSetting, error) {
var setting DbModels.LineReverseSetting
if err := db.Model(&DbModels.LineReverseSetting{}).First(&setting).Error; err != nil {
return setting, err
}
return setting, nil
}

View File

@ -28,7 +28,7 @@ func TestFutureJudge(t *testing.T) {
// } // }
key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE) key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE)
item := `{"id":50,"apiId":49,"mainId":47,"pid":47,"symbol":"ADAUSDT","price":"0.5936","side":"SELL","num":"12","orderSn":"397913127842217984"}` item := `{"id":10,"apiId":49,"mainId":7,"pid":7,"symbol":"ADAUSDT","price":"0.6244","side":"BUY","num":"12","orderSn":"398690240274890752"}`
reduceOrder := ReduceListItem{} reduceOrder := ReduceListItem{}
futApi := FutRestApi{} futApi := FutRestApi{}
setting, err := cacheservice.GetSystemSetting(db) setting, err := cacheservice.GetSystemSetting(db)

View File

@ -0,0 +1,125 @@
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,
"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"
if params.ClosePosition {
paramsMaps["closePosition"] = "true"
}
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"
if params.ClosePosition {
paramsMaps["closePosition"] = "true"
}
case "STOP":
paramsMaps["price"] = params.Price.String()
paramsMaps["stopprice"] = params.StopPrice.String()
paramsMaps["workingType"] = "MARK_PRICE"
paramsMaps["timeInForce"] = "GTC"
}
//不是平仓
if !params.ClosePosition {
paramsMaps["quantity"] = params.Quantity.String()
}
// 获取 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(&paramsMaps)
log.Error("下单失败 参数:", paramsVal)
errContent := FutErrorMaps[code.(float64)]
if errContent == "" {
errContent, _ = dataMap["msg"].(string)
}
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())
}

View File

@ -12,6 +12,7 @@ import (
"go-admin/models/binancedto" "go-admin/models/binancedto"
"go-admin/models/futuresdto" "go-admin/models/futuresdto"
"go-admin/pkg/httputils" "go-admin/pkg/httputils"
"go-admin/pkg/retryhelper"
"go-admin/pkg/utility" "go-admin/pkg/utility"
"strconv" "strconv"
"strings" "strings"
@ -555,50 +556,6 @@ func (e FutRestApi) OrderPlace(orm *gorm.DB, params FutOrderPlace) error {
return nil return nil
} }
// ClosePositionB 平仓B对应的交易对
// bApiUserInfo B 账户api-user信息
// symbol 需要平仓的交易对
// closeType 平仓模式 ALL = 全平 reduceOnly = 只减仓
// func (e FutRestApi) ClosePositionB(orm *gorm.DB, bApiUserInfo *DbModels.LineApiUser, symbol string, closeType string) error {
// if bApiUserInfo == nil {
// return errors.New("缺失平仓账户信息")
// }
// if symbol == "" {
// return errors.New("缺失平仓交易对信息")
// }
// risks, err := e.GetPositionV3(bApiUserInfo, symbol)
// if err != nil {
// return err
// }
// for _, risk := range risks {
// if risk.Symbol == strings.ToUpper(symbol) {
// //持仓数量
// positionAmt, _ := decimal.NewFromString(risk.PositionAmt)
// side := "BUY"
// if positionAmt.GreaterThan(decimal.Zero) {
// side = "SELL"
// }
// var closeAmt decimal.Decimal
// if strings.ToUpper(closeType) == "ALL" { //全部平仓
// closeAmt = positionAmt
// } else {
// //如果是只减仓的话 数量=仓位数量*比例
// closeAmt = positionAmt.Mul(decimal.NewFromFloat(reduceOnlyRate))
// }
// err = e.ClosePosition(symbol, closeAmt, side, *bApiUserInfo, "MARKET", "", decimal.Zero)
// if err != nil {
// return err
// }
// orm.Model(&DbModels.LinePreOrder{}).Where("api_id = ? AND symbol = ?", bApiUserInfo.Id, symbol).Updates(map[string]interface{}{
// "status": "6",
// })
// }
// }
// return nil
// }
// 获取合约 持仓价格、数量 // 获取合约 持仓价格、数量
// symbol:交易对 // symbol:交易对
// side:方向 // side:方向
@ -642,6 +599,49 @@ func (e FutRestApi) GetHoldeData(apiInfo *DbModels.LineApiUser, symbol, side str
return nil return nil
} }
// 获取合约 持仓价格、数量
// symbol:交易对
// positionSide:持仓方向
// holdeData:持仓数据
func (e FutRestApi) GetPositionData(apiInfo *DbModels.LineApiUser, symbol, positionSide string, holdeData *HoldeData) error {
opts := retryhelper.DefaultRetryOptions()
opts.RetryableErrFn = func(err error) bool {
if strings.Contains(err.Error(), "LOT_SIZE") {
return false
}
//重试
return true
}
holdes, err := retryhelper.RetryWithResult(func() ([]PositionRisk, error) {
return e.GetPositionV3(apiInfo, symbol)
}, opts)
if err != nil {
return err
}
for _, item := range holdes {
positionAmount, _ := decimal.NewFromString(item.PositionAmt)
if (positionSide == "LONG" && item.PositionSide == "BOTH" && positionAmount.Cmp(decimal.Zero) > 0) || item.PositionSide == positionSide { //多
holdeData.AveragePrice, _ = decimal.NewFromString(item.EntryPrice)
holdeData.TotalQuantity = positionAmount.Abs()
continue
} else if (positionSide == "SHORT" && item.PositionSide == "BOTH" && positionAmount.Cmp(decimal.Zero) < 0) || item.PositionSide == positionSide { //空
holdeData.AveragePrice, _ = decimal.NewFromString(item.EntryPrice)
holdeData.TotalQuantity = positionAmount.Abs()
continue
}
}
if holdeData.AveragePrice.Cmp(decimal.Zero) == 0 {
holdesVal, _ := sonic.MarshalString(&holdes)
log.Error("均价错误 symbol:", symbol, " 数据:", holdesVal)
}
return nil
}
// 获取代币持仓信息 // 获取代币持仓信息
func getSymbolHolde(e FutRestApi, apiInfo *DbModels.LineApiUser, symbol string, side string, holdeData *HoldeData) ([]PositionRisk, error) { func getSymbolHolde(e FutRestApi, apiInfo *DbModels.LineApiUser, symbol string, side string, holdeData *HoldeData) ([]PositionRisk, error) {
holdes, err := e.GetPositionV3(apiInfo, symbol) holdes, err := e.GetPositionV3(apiInfo, symbol)
@ -759,6 +759,15 @@ func (e FutRestApi) ClosePosition(symbol string, orderSn string, quantity decima
return nil 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 通过单个订单号取消合约委托 // CancelFutOrder 通过单个订单号取消合约委托
// symbol 交易对 // symbol 交易对
// newClientOrderId 系统自定义订单号 // newClientOrderId 系统自定义订单号
@ -818,6 +827,27 @@ func (e FutRestApi) CancelAllFutOrder(apiUserInfo DbModels.LineApiUser, symbol s
return nil return nil
} }
// 带重试的批量撤销订单
func (e FutRestApi) CancelBatchFutOrderLoop(apiUserInfo DbModels.LineApiUser, symbol string, newClientOrderIdList []string) error {
opts := retryhelper.DefaultRetryOptions()
opts.RetryableErrFn = func(err error) bool {
if strings.Contains(err.Error(), "LOT_SIZE") {
return false
}
//重试
return true
}
err := retryhelper.Retry(func() error {
return e.CancelBatchFutOrder(apiUserInfo, symbol, newClientOrderIdList)
}, opts)
if err != nil {
return err
}
return nil
}
// CancelBatchFutOrder 批量撤销订单 // CancelBatchFutOrder 批量撤销订单
// symbol 交易对 // symbol 交易对
// newClientOrderIdList 系统自定义的订单号, 最多支持10个订单 // newClientOrderIdList 系统自定义的订单号, 最多支持10个订单

View File

@ -277,9 +277,10 @@ func FuturesReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, futApi FutRes
} else if ok { } else if ok {
defer lock.Release() defer lock.Release()
takeOrders := make([]DbModels.LinePreOrder, 0) takeOrders := make([]DbModels.LinePreOrder, 0)
if err := db.Model(&DbModels.LinePreOrder{}).Where("main_id =? AND order_type IN (1,2,4) AND status IN (1,5)", reduceOrder.MainId).Find(&takeOrders).Error; err != nil { //只取消减仓单 止盈止损减仓成功后取消
if err := db.Model(&DbModels.LinePreOrder{}).Where("main_id =? AND order_type IN 4 AND status IN (1,5)", reduceOrder.MainId).Find(&takeOrders).Error; err != nil {
log.Error("查询止盈单失败") log.Error("查询止盈单失败")
return false // return false
} }
var hasrecord bool var hasrecord bool

View File

@ -27,7 +27,7 @@ import (
/* /*
修改订单信息 修改订单信息
*/ */
func ChangeFutureOrder(mapData map[string]interface{}) { func ChangeFutureOrder(mapData map[string]interface{}, apiKey string) {
// 检查订单号是否存在 // 检查订单号是否存在
orderSn, ok := mapData["c"] orderSn, ok := mapData["c"]
originOrderSn := mapData["C"] //取消操作 代表原始订单号 originOrderSn := mapData["C"] //取消操作 代表原始订单号
@ -61,34 +61,15 @@ func ChangeFutureOrder(mapData map[string]interface{}) {
} }
defer lock.Release() 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 { if err != nil {
logger.Error("合约订单回调失败,查询订单失败:", orderSn, " err:", err) logger.Errorf("合约订单回调失败,单失败:%v", err)
return
} }
// 解析订单状态
status, ok := mapData["X"].(string)
if !ok {
mapStr, _ := sonic.Marshal(&mapData)
logger.Error("订单回调失败,没有状态:", string(mapStr))
return
}
// 更新订单状态
orderStatus, reason := parseOrderStatus(status, mapData)
if orderStatus == 0 {
logger.Error("订单回调失败,状态错误:", orderSn, " status:", status, " reason:", reason)
return
}
if err := updateOrderStatus(db, preOrder, orderStatus, reason, true, mapData); err != nil {
logger.Error("修改订单状态失败:", orderSn, " err:", err)
return
}
handleFutOrderByType(db, preOrder, orderStatus)
} }
// 合约回调 // 合约回调
@ -145,6 +126,8 @@ func handleReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
} }
// rate := utility.StringAsFloat(orderExt.AddPositionVal) // rate := utility.StringAsFloat(orderExt.AddPositionVal)
//取消委托中的止盈止损
cancelTakeProfitByReduce(db, apiUserInfo, preOrder.Symbol, preOrder.MainId, preOrder.SymbolType)
// 100%减仓 终止流程 // 100%减仓 终止流程
if orderExt.AddPositionVal.Cmp(decimal.NewFromInt(100)) >= 0 { if orderExt.AddPositionVal.Cmp(decimal.NewFromInt(100)) >= 0 {
//缓存 //缓存
@ -179,6 +162,36 @@ func handleReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
} }
} }
// 减仓成功后取消止盈止损
func cancelTakeProfitByReduce(db *gorm.DB, apiUserInfo DbModels.LineApiUser, symbol string, mainId int, symbolType int) {
orders, err := GetSymbolTakeAndStop(db, mainId, symbolType)
futApi := FutRestApi{}
if err != nil {
logger.Errorf("mainId:%d 获取委托中的止盈止损失败:%v", mainId, err)
}
orderSns := make([]string, 0)
for _, v := range orders {
if v.OrderType != 1 && v.OrderType != 2 {
continue
}
orderSns = append(orderSns, v.OrderSn)
}
arr := utility.SplitSlice(orderSns, 10)
for _, v := range arr {
err := futApi.CancelBatchFutOrder(apiUserInfo, symbol, v)
if err != nil {
logger.Errorf("mainId:%d 取消止盈止损失败:%v", mainId, err)
}
}
}
// 减仓处理止盈止损 // 减仓处理止盈止损
func FutTakeProfit(db *gorm.DB, preOrder *DbModels.LinePreOrder, apiUserInfo DbModels.LineApiUser, tradeSet models2.TradeSet, func FutTakeProfit(db *gorm.DB, preOrder *DbModels.LinePreOrder, apiUserInfo DbModels.LineApiUser, tradeSet models2.TradeSet,
positionData positiondto.PositionDto, orderExt DbModels.LinePreOrderExt, manualTakeRatio, manualStopRatio decimal.Decimal) bool { positionData positiondto.PositionDto, orderExt DbModels.LinePreOrderExt, manualTakeRatio, manualStopRatio decimal.Decimal) bool {

View File

@ -61,17 +61,19 @@ type EntryPriceResult struct {
} }
type FutOrderPlace struct { type FutOrderPlace struct {
ApiId int `json:"api_id"` //api用户id ApiId int `json:"api_id"` //api用户id
Symbol string `json:"symbol"` //合约交易对 Symbol string `json:"symbol"` //合约交易对
Side string `json:"side"` //购买方向 Side string `json:"side"` //购买方向
Quantity decimal.Decimal `json:"quantity"` //数量 PositionSide string `json:"position_side"` //持仓方向
Price decimal.Decimal `json:"price"` //限价单价 Quantity decimal.Decimal `json:"quantity"` //数量
SideType string `json:"side_type"` //现价或者市 Price decimal.Decimal `json:"price"` //限价单
OpenOrder int `json:"open_order"` //是否开启限价单止盈止损 SideType string `json:"side_type"` //现价或者市价
Profit decimal.Decimal `json:"profit"` //止盈价格 OpenOrder int `json:"open_order"` //是否开启限价单止盈止损
StopPrice decimal.Decimal `json:"stopprice"` //止价格 Profit decimal.Decimal `json:"profit"` //止价格
OrderType string `json:"order_type"` //订单类型市价或限价MARKET(市价单) TAKE_PROFIT_MARKET市价止盈 TAKE_PROFIT(限价止盈) STOP (限价止损) STOP_MARKET市价止损 StopPrice decimal.Decimal `json:"stopprice"` //止损价格
OrderType string `json:"order_type"` //订单类型市价或限价MARKET(市价单) TAKE_PROFIT_MARKET市价止盈 TAKE_PROFIT(限价止盈) STOP (限价止损) STOP_MARKET市价止损
NewClientOrderId string `json:"newClientOrderId"` NewClientOrderId string `json:"newClientOrderId"`
ClosePosition bool `json:"closePosition"` //是否平仓
} }
func (s FutOrderPlace) CheckParams() error { func (s FutOrderPlace) CheckParams() error {

View File

@ -134,7 +134,7 @@ func GetTotalLossAmount(db *gorm.DB, mainId int) (decimal.Decimal, error) {
return totalLossAmountU, nil return totalLossAmountU, nil
} }
// 获取交易对的 委托中的止盈止损 // 获取交易对的 委托中的止盈止损、减仓单
// mainId 主单id // mainId 主单id
// symbolType 交易对类型 // symbolType 交易对类型
func GetSymbolTakeAndStop(db *gorm.DB, mainId int, symbolType int) ([]models.LinePreOrder, error) { func GetSymbolTakeAndStop(db *gorm.DB, mainId int, symbolType int) ([]models.LinePreOrder, error) {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
package binanceservice
import (
"go-admin/common/helper"
"testing"
"github.com/bytedance/sonic"
"github.com/go-admin-team/go-admin-core/logger"
"github.com/go-admin-team/go-admin-core/sdk"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func initConfig() *gorm.DB {
dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
sdk.Runtime.SetDb("default", db)
helper.InitDefaultRedis("127.0.0.1:6379", "", 2)
helper.InitLockRedisConn("127.0.0.1:6379", "", "2")
return db
}
func TestReverseOrder(t *testing.T) {
db := initConfig()
mapData := make(map[string]interface{})
content := `{"s":"ADAUSDT","c":"439301585084878848","S":"SELL","o":"LIMIT","f":"GTC","q":"8","p":"0.7781","ap":"0.7843","sp":"0","x":"TRADE","X":"FILLED","i":56468022240,"l":"8","z":"8","L":"0.7843","n":"0.0031372","N":"USDT","T":1753950025820,"t":1642816051,"b":"0","a":"0","m":false,"R":false,"wt":"CONTRACT_PRICE","ot":"LIMIT","ps":"SHORT","cp":false,"rp":"0","pP":false,"si":0,"ss":0,"V":"EXPIRE_MAKER","pm":"NONE","gtd":0}`
sonic.Unmarshal([]byte(content), &mapData)
service := ReverseService{}
service.Orm = db
service.Log = logger.NewHelper(logger.DefaultLogger)
service.ReverseOrder("c8ej2vxXzNUIlCjQCmU1iavK8LG78uZaXY3CIT4kz0PuhnXycg44HsVAbsqupHTw", mapData)
}

View File

@ -21,7 +21,6 @@ import (
"github.com/bytedance/sonic" "github.com/bytedance/sonic"
"github.com/go-admin-team/go-admin-core/logger" "github.com/go-admin-team/go-admin-core/logger"
log "github.com/go-admin-team/go-admin-core/logger"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"gorm.io/gorm" "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"] orderSn, ok := mapData["c"]
originOrderSn := 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) 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 { if ok, err := lock.AcquireWait(context.Background()); err != nil {
log.Error("获取锁失败", err) logger.Error("获取锁失败", err)
return return
} else if ok { } else if ok {
defer lock.Release() defer lock.Release()

View File

@ -12,30 +12,42 @@ import (
models2 "go-admin/models" models2 "go-admin/models"
"go-admin/pkg/utility" "go-admin/pkg/utility"
"go-admin/services/cacheservice" "go-admin/services/cacheservice"
"strings"
"time" "time"
"github.com/bytedance/sonic" "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-admin-team/go-admin-core/sdk/service"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"gorm.io/gorm"
) )
type BinanceStrategyOrderService struct { type BinanceStrategyOrderService struct {
service.Service service.Service
Debug bool
} }
// 判断是否触发波段订单 // 判断是否触发波段订单
func (e *BinanceStrategyOrderService) TriggerStrategyOrder(exchangeType string) { func (e *BinanceStrategyOrderService) TriggerStrategyOrder(exchangeType string) {
//现货 //现货
orderStrs, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.StrategyFutOrderList, exchangeType)) orderStrs, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.StrategySpotOrderList, exchangeType))
e.DoJudge(orderStrs, 1, exchangeType) if len(orderStrs) > 0 {
e.DoJudge(orderStrs, 1, exchangeType)
}
//合约 //合约
futOrdedrStrs, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.StrategyFutOrderList, exchangeType)) futOrdedrStrs, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.StrategyFutOrderList, exchangeType))
e.DoJudge(futOrdedrStrs, 2, exchangeType)
if len(futOrdedrStrs) > 0 {
e.DoJudge(futOrdedrStrs, 2, exchangeType)
}
} }
// 处理订单 // 判断是否符合条件
func (e *BinanceStrategyOrderService) DoJudge(orderStrs []string, symbolType int, exchangeType string) { func (e *BinanceStrategyOrderService) DoJudge(orderStrs []string, symbolType int, exchangeType string) {
db := GetDBConnection()
// setting, _ := cacheservice.GetSystemSetting(db)
for _, orderStr := range orderStrs { for _, orderStr := range orderStrs {
var lockKey string var lockKey string
orderItem := dto.StrategyOrderRedisList{} orderItem := dto.StrategyOrderRedisList{}
@ -51,7 +63,7 @@ func (e *BinanceStrategyOrderService) DoJudge(orderStrs []string, symbolType int
lockKey = rediskey.StrategyFutTriggerLock lockKey = rediskey.StrategyFutTriggerLock
} }
lock := helper.NewRedisLock(fmt.Sprintf(lockKey, orderItem.ApiId, orderItem.Symbol), 200, 50, 100*time.Millisecond) lock := helper.NewRedisLock(fmt.Sprintf(lockKey, orderItem.ApiId, orderItem.OrderSn), 60, 20, 300*time.Millisecond)
if ok, err := lock.AcquireWait(context.Background()); err != nil { if ok, err := lock.AcquireWait(context.Background()); err != nil {
e.Log.Debug("获取锁失败", err) e.Log.Debug("获取锁失败", err)
@ -60,14 +72,14 @@ func (e *BinanceStrategyOrderService) DoJudge(orderStrs []string, symbolType int
defer lock.Release() defer lock.Release()
//判断是否符合条件 //判断是否符合条件
success, err := e.JudgeStrategy(orderItem, 1, exchangeType) success, err := e.JudgeStrategy(orderItem, symbolType, exchangeType)
if err != nil { if err != nil {
e.Log.Errorf("order_id:%d err:%v", orderItem.Id, err) e.Log.Errorf("order_id:%d err:%v", orderItem.Id, err)
} }
if success { if e.Debug || success {
e.TriggerOrder(orderItem, symbolType) e.TriggerOrder(db, orderStr, orderItem, symbolType)
} }
} }
} }
@ -95,8 +107,16 @@ func (e *BinanceStrategyOrderService) JudgeStrategy(order dto.StrategyOrderRedis
} }
score := lastPrices[0].Score score := lastPrices[0].Score
startPrice := utility.StrToDecimal(beforePrice) var startPrice decimal.Decimal
lastPrice := utility.StrToDecimal(lastPrices[0].Member.(string)) var lastPrice decimal.Decimal
startPriceArgs := strings.Split(beforePrice, ":")
endPricecArgs := strings.Split(lastPrices[0].Member.(string), ":")
if len(startPriceArgs) == 0 || len(endPricecArgs) == 0 {
return result, errors.New("获取交易对起止价格失败")
}
startPrice = utility.StrToDecimal(startPriceArgs[len(startPriceArgs)-1])
lastPrice = utility.StrToDecimal(endPricecArgs[len(endPricecArgs)-1])
//时间差超过10s //时间差超过10s
if (nowUtc-int64(score))/1000 > 10 { if (nowUtc-int64(score))/1000 > 10 {
@ -108,31 +128,35 @@ func (e *BinanceStrategyOrderService) JudgeStrategy(order dto.StrategyOrderRedis
} }
percentag := lastPrice.Div(startPrice).Sub(decimal.NewFromInt(1)).Truncate(6) percentag := lastPrice.Div(startPrice).Sub(decimal.NewFromInt(1)).Truncate(6)
logger.Infof("百分比:%s", percentag.Mul(decimal.NewFromInt(100)).String())
//价格没有变动 //价格没有变动
if percentag.Cmp(decimal.Zero) == 0 { if percentag.Cmp(decimal.Zero) == 0 {
return result, nil return result, nil
} }
//满足条件 //满足条件
switch order.CompareType { switch {
case 1: //涨价格大于0.5% 跌价格小于-0.5%
result = percentag.Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) > 0 case order.CompareType == 1 && order.Direction == 1 && percentag.Cmp(decimal.Zero) > 0,
case 2: order.CompareType == 1 && order.Direction == 2 && percentag.Cmp(decimal.Zero) < 0:
result = percentag.Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) >= 0 result = percentag.Abs().Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) > 0
case 5:
result = percentag.Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) == 0 case order.CompareType == 2 && order.Direction == 1 && percentag.Cmp(decimal.Zero) > 0,
default: order.CompareType == 2 && order.Direction == 2 && percentag.Cmp(decimal.Zero) < 0:
return result, errors.New("没有对应的类型") result = percentag.Abs().Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) >= 0
case order.CompareType == 5 && order.Direction == 1 && percentag.Cmp(decimal.Zero) > 0,
order.CompareType == 5 && order.Direction == 2 && percentag.Cmp(decimal.Zero) < 0:
result = percentag.Abs().Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) == 0
} }
return result, nil return result, nil
} }
// 触发委托单 // 触发委托单
func (e *BinanceStrategyOrderService) TriggerOrder(order dto.StrategyOrderRedisList, symbolType int) error { func (e *BinanceStrategyOrderService) TriggerOrder(db *gorm.DB, cacheVal string, order dto.StrategyOrderRedisList, symbolType int) error {
orders := make([]models.LinePreOrder, 0) orders := make([]models.LinePreOrder, 0)
if err := e.Orm.Model(&models.LinePreOrder{}).Where("main_id =?", order.Id).Find(&orders).Error; err != nil { if err := e.Orm.Model(&models.LinePreOrder{}).Where("main_id =? or id =?", order.Id, order.Id).Find(&orders).Error; err != nil {
e.Log.Errorf("order_id:%d 获取委托单失败:%s", order.Id, err.Error()) e.Log.Errorf("order_id:%d 获取委托单失败:%s", order.Id, err.Error())
return err return err
} }
@ -143,7 +167,16 @@ func (e *BinanceStrategyOrderService) TriggerOrder(order dto.StrategyOrderRedisL
return errors.New("获取系统设置失败") return errors.New("获取系统设置失败")
} }
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, symbolType) var tradeSet models2.TradeSet
switch symbolType {
case 1:
tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 0)
case 2:
tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1)
default:
return errors.New("获取交易对行情失败,交易对类型错误")
}
if tradeSet.Coin == "" { if tradeSet.Coin == "" {
return errors.New("获取交易对行情失败") return errors.New("获取交易对行情失败")
@ -155,7 +188,7 @@ func (e *BinanceStrategyOrderService) TriggerOrder(order dto.StrategyOrderRedisL
return errors.New("最新成交价小于等于0") return errors.New("最新成交价小于等于0")
} }
var mainOrder models.LinePreOrder mainOrder := models.LinePreOrder{}
for _, v := range orders { for _, v := range orders {
if v.MainId == 0 { if v.MainId == 0 {
@ -164,62 +197,307 @@ func (e *BinanceStrategyOrderService) TriggerOrder(order dto.StrategyOrderRedisL
} }
} }
if mainOrder.Id == 0 {
return errors.New("获取主单失败")
}
GetOrderByPid(&mainOrder, orders, mainOrder.Id) GetOrderByPid(&mainOrder, orders, mainOrder.Id)
e.RecalculateOrder(tradeSet, &mainOrder, setting)
//事务保存
err := e.Orm.Transaction(func(tx *gorm.DB) error {
if err1 := tx.Save(&mainOrder).Error; err1 != nil {
return err1
}
for _, v := range mainOrder.Childs {
if err1 := tx.Save(&v).Error; err1 != nil {
return err1
}
for _, v2 := range v.Childs {
if err1 := tx.Save(&v2).Error; err1 != nil {
return err1
}
}
}
return nil
})
if err != nil {
e.Log.Errorf("order_id:%d 波段触发保存委托单失败:%s", mainOrder.Id, err.Error())
return err
}
e.StrategyOrderPlace(db, cacheVal, mainOrder, tradeSet)
return nil return nil
} }
// 策略触发订单
// cacheVal:缓存值
// mainOrder:主订单
// tradeSet:交易对行情
func (e *BinanceStrategyOrderService) StrategyOrderPlace(db *gorm.DB, cacheVal string, mainOrder models.LinePreOrder, tradeSet models2.TradeSet) {
price := utility.StringToDecimal(mainOrder.Price).Truncate(int32(tradeSet.PriceDigit))
num := utility.StringToDecimal(mainOrder.Num).Truncate(int32(tradeSet.AmountDigit))
futApi := FutRestApi{}
var key string
switch mainOrder.SymbolType {
case 1:
key = fmt.Sprintf(rediskey.StrategySpotOrderList, global.EXCHANGE_BINANCE)
case 2:
key = fmt.Sprintf(rediskey.StrategyFutOrderList, global.EXCHANGE_BINANCE)
default:
logger.Errorf("id:%d 交易对类型不存在:%d", mainOrder.Id, mainOrder.SymbolType)
return
}
params := FutOrderPlace{
ApiId: mainOrder.ApiId,
Symbol: mainOrder.Symbol,
Side: mainOrder.Site,
OrderType: mainOrder.MainOrderType,
SideType: mainOrder.MainOrderType,
Price: price,
Quantity: num,
NewClientOrderId: mainOrder.OrderSn,
}
if err := futApi.OrderPlaceLoop(db, params, 3); err != nil {
logger.Error("下单失败", mainOrder.Symbol, " err:", err)
if _, err := helper.DefaultRedis.LRem(key, cacheVal); err != nil {
logger.Error("删除redis 预下单失败:", err)
}
err := db.Model(&models.LinePreOrder{}).Where("id =? and status='0'", mainOrder.Id).Updates(map[string]interface{}{"status": "2", "desc": err.Error()}).Error
if err != nil {
logger.Error("更新预下单状态失败")
}
return
} else {
if _, err := helper.DefaultRedis.LRem(key, cacheVal); err != nil {
logger.Error("删除redis 预下单失败:", err)
}
}
if err := db.Model(&models.LinePreOrder{}).Where("id =? ", mainOrder.Id).Updates(map[string]interface{}{"trigger_time": time.Now()}).Error; err != nil {
logger.Error("更新预下单状态失败 ordersn:", mainOrder.OrderSn, " status:1")
}
if err := db.Model(&models.LinePreOrder{}).Where("id =? AND status ='0'", mainOrder.Id).Updates(map[string]interface{}{"status": "1"}).Error; err != nil {
logger.Error("更新预下单状态失败 ordersn:", mainOrder.OrderSn, " status:1")
}
}
// 重新计算订单单价、数量 // 重新计算订单单价、数量
// tradeSet 交易对行情 // tradeSet 交易对行情
// mainOrder 主订单 // mainOrder 主订单
// setting 系统设置 // setting 系统设置
func (e *BinanceStrategyOrderService) RecalculateOrder(tradeSet models2.TradeSet, mainOrder *models.LinePreOrder, setting models.LineSystemSetting) error { func (e *BinanceStrategyOrderService) RecalculateOrder(tradeSet models2.TradeSet, mainOrder *models.LinePreOrder, setting models.LineSystemSetting) error {
exts := make([]models.LinePreOrderExt, 0) exts := make([]models.LinePreOrderExt, 0)
if err := e.Orm.Model(models.LinePreOrderExt{}).Where("main_id =?", mainOrder.Id).Find(&exts).Error; err != nil { if err := e.Orm.Model(models.LinePreOrderExt{}).Where("main_order_id =?", mainOrder.Id).Find(&exts).Error; err != nil {
return errors.New("获取拓展信息失败") return errors.New("获取拓展信息失败")
} }
var newPrice decimal.Decimal
lastPrice := utility.StrToDecimal(tradeSet.LastPrice) lastPrice := utility.StrToDecimal(tradeSet.LastPrice)
rate := utility.StrToDecimal(mainOrder.Rate) rate := utility.StrToDecimal(mainOrder.Rate)
newPrice := lastPrice.Mul(decimal.NewFromInt(1).Add(rate.Div(decimal.NewFromInt(100)).Truncate(4))).Truncate(int32(tradeSet.PriceDigit)) newPrice = lastPrice.Mul(decimal.NewFromInt(1).Add(rate.Div(decimal.NewFromInt(100)).Truncate(4))).Truncate(int32(tradeSet.PriceDigit))
buyPrice := utility.StrToDecimal(mainOrder.BuyPrice) buyPrice := utility.StrToDecimal(mainOrder.BuyPrice)
totalNum := buyPrice.Div(newPrice).Truncate(int32(tradeSet.AmountDigit)) totalNum := buyPrice.Div(newPrice).Truncate(int32(tradeSet.AmountDigit))
mainOrder.SignPrice = lastPrice.String() mainOrder.SignPrice = lastPrice.String()
mainOrder.Price = newPrice.String() mainOrder.Price = newPrice.String()
mainOrder.Num = totalNum.String() mainOrder.Num = totalNum.String()
remainQuantity := totalNum
var totalLossAmount decimal.Decimal
prePrice := lastPrice
for index := range mainOrder.Childs { for index := range mainOrder.Childs {
//主单止盈 var ext models.LinePreOrderExt
if mainOrder.Child[index].OrderType == 1 { extOrderId := mainOrder.Childs[index].Id
var ext models.LinePreOrderExt takeStopArray := []int{1, 2}
for _, v := range exts { //止盈止损 ext拓展id为 pid
if v.OrderId == mainOrder.Child[index].Id { if utility.ContainsInt(takeStopArray, mainOrder.Childs[index].OrderType) {
ext = v extOrderId = mainOrder.Childs[index].Pid
break }
}
for _, v := range exts {
if v.OrderId == extOrderId {
ext = v
break
}
}
if ext.Id <= 0 {
logger.Errorf("子订单ext不存在 id:%d", mainOrder.Childs[index].Id)
continue
}
//主单止盈、止损
if mainOrder.Childs[index].Pid == mainOrder.Childs[index].MainId && (mainOrder.Childs[index].OrderType == 1 || mainOrder.Childs[index].OrderType == 2) {
var percent decimal.Decimal
switch {
// 加价
case mainOrder.Childs[index].OrderType == 1 && mainOrder.Site == "BUY", mainOrder.Childs[index].OrderType == 2 && mainOrder.Site == "SELL":
percent = decimal.NewFromInt(100).Add(ext.TakeProfitRatio)
//减价
case mainOrder.Childs[index].OrderType == 2 && mainOrder.Site == "BUY", mainOrder.Childs[index].OrderType == 1 && mainOrder.Site == "SELL":
percent = decimal.NewFromInt(100).Sub(ext.StopLossRatio)
} }
if ext.Id > 0 { childPrice := lastPrice.Mul(percent.Div(decimal.NewFromInt(100).Truncate(4))).Truncate(int32(tradeSet.PriceDigit))
var percent decimal.Decimal mainOrder.Childs[index].Price = childPrice.String()
//多 mainOrder.Childs[index].Num = totalNum.String()
if mainOrder.Site == "BUY" {
percent = decimal.NewFromInt(100).Add(ext.TakeProfitRatio)
} else {
percent = decimal.NewFromInt(100).Sub(ext.StopLossRatio)
}
childPrice := lastPrice.Mul(percent.Div(decimal.NewFromInt(100).Truncate(4))).Truncate(int32(tradeSet.PriceDigit))
mainOrder.Child[index].Price = childPrice.String()
}
} else { } else {
//todo 重新计算 lastNum := remainQuantity
//过期时间
if ext.ExpirateHour <= 0 {
mainOrder.Childs[index].ExpireTime = time.Now().AddDate(10, 0, 0)
} else {
mainOrder.Childs[index].ExpireTime = time.Now().Add(time.Hour * time.Duration(ext.ExpirateHour))
}
switch {
//加仓单
case mainOrder.Childs[index].OrderType == 1 && mainOrder.Childs[index].OrderCategory == 3:
var percentage decimal.Decimal
if mainOrder.Site == "BUY" {
percentage = decimal.NewFromInt(1).Sub(ext.PriceRatio.Div(decimal.NewFromInt(100)))
} else {
percentage = decimal.NewFromInt(1).Add(ext.PriceRatio.Div(decimal.NewFromInt(100)))
}
dataPrice := utility.StrToDecimal(mainOrder.Price).Mul(percentage).Truncate(int32(tradeSet.PriceDigit))
mainOrder.Childs[index].Price = dataPrice.String()
priceDiff := dataPrice.Sub(prePrice).Abs()
totalLossAmount = totalLossAmount.Add(lastNum.Mul(priceDiff))
if ext.AddPositionType == 1 {
buyPrice := utility.StrToDecimal(mainOrder.BuyPrice).Mul(utility.SafeDiv(ext.AddPositionVal, decimal.NewFromInt(100))).Truncate(2)
mainOrder.Childs[index].Num = utility.SafeDiv(buyPrice, dataPrice).Truncate(int32(tradeSet.AmountDigit)).String()
} else {
mainOrder.Childs[index].Num = utility.SafeDiv(ext.AddPositionVal.Truncate(2), dataPrice).Truncate(int32(tradeSet.AmountDigit)).String()
}
//加库存
lastNum = lastNum.Add(utility.StrToDecimal(mainOrder.Childs[index].Num))
// 计算子订单
if len(mainOrder.Childs[index].Childs) > 0 {
calculateChildOrder(&mainOrder.Childs[index].Childs, &tradeSet, ext, lastNum, dataPrice, totalLossAmount, false)
}
//覆盖最近的订单价
prePrice = dataPrice
//减仓单
case mainOrder.Childs[index].OrderType == 4:
percentage := decimal.NewFromInt(1)
if mainOrder.Site == "BUY" && ext.PriceRatio.Cmp(decimal.Zero) > 0 {
percentage = decimal.NewFromInt(1).Sub(ext.PriceRatio.Div(decimal.NewFromInt(100)))
} else if ext.PriceRatio.Cmp(decimal.Zero) > 0 {
percentage = decimal.NewFromInt(1).Add(ext.PriceRatio.Div(decimal.NewFromInt(100)))
}
dataPrice := utility.StrToDecimal(mainOrder.Price).Mul(percentage).Truncate(int32(tradeSet.PriceDigit))
mainOrder.Childs[index].Price = dataPrice.String()
priceDiff := dataPrice.Sub(prePrice).Abs()
totalLossAmount = totalLossAmount.Add(lastNum.Mul(priceDiff))
//百分比减仓
if ext.AddPositionType == 1 {
mainOrder.Childs[index].Num = lastNum.Mul(ext.AddPositionVal.Div(decimal.NewFromInt(100))).Truncate(int32(tradeSet.AmountDigit)).String()
} else {
logger.Error("减仓不能是固定数值")
}
//减库存
lastNum = lastNum.Sub(utility.StrToDecimal(mainOrder.Childs[index].Num))
// 计算子订单
if len(mainOrder.Childs[index].Childs) > 0 {
calculateChildOrder(&mainOrder.Childs[index].Childs, &tradeSet, ext, lastNum, dataPrice, totalLossAmount, false)
}
//覆盖最近的订单价
prePrice = dataPrice
}
} }
} }
return nil return nil
} }
// 计算子订单信息
// isTpTp 是否是止盈后止损止盈
// totalLossAmount 累计亏损金额
func calculateChildOrder(orders *[]models.LinePreOrder, tradeSet *models2.TradeSet, ext models.LinePreOrderExt, lastNum decimal.Decimal, price decimal.Decimal, totalLossAmount decimal.Decimal, isTpTp bool) error {
for index := range *orders {
orderQuantity := lastNum.Truncate(int32(tradeSet.AmountDigit))
percentage := decimal.NewFromInt(1)
var addPercentage decimal.Decimal
switch {
//止盈
case !isTpTp && (*orders)[index].OrderType == 1:
addPercentage = ext.TakeProfitRatio
if ext.TakeProfitNumRatio.Cmp(decimal.Zero) > 0 {
orderQuantity = lastNum.Mul(ext.TakeProfitNumRatio.Div(decimal.NewFromInt(100))).Truncate(int32(tradeSet.AmountDigit))
}
//止损
case !isTpTp && (*orders)[index].OrderType == 2:
addPercentage = ext.StopLossRatio
//止盈后止盈
case isTpTp && (*orders)[index].OrderType == 1:
addPercentage = ext.TpTpPriceRatio
//止盈后止损
case isTpTp && (*orders)[index].OrderType == 2:
addPercentage = ext.TpSlPriceRatio
}
switch {
//做多止盈、做空止损
case (*orders)[index].OrderType == 1 && (*orders)[index].Site == "SELL", (*orders)[index].OrderType == 2 && (*orders)[index].Site == "BUY":
percentage = decimal.NewFromInt(100).Add(addPercentage)
//做多止损、做空止盈
case (*orders)[index].OrderType == 2 && (*orders)[index].Site == "SELL", (*orders)[index].OrderType == 1 && (*orders)[index].Site == "BUY":
percentage = decimal.NewFromInt(100).Sub(addPercentage)
}
//止盈亏损回本百分比
if (*orders)[index].OrderType == 1 && totalLossAmount.Cmp(decimal.Zero) > 0 {
lossPercent := totalLossAmount.Div(lastNum).Mul(decimal.NewFromInt(100)).Truncate(2)
percentage = percentage.Add(lossPercent)
}
if percentage.Cmp(decimal.Zero) > 0 {
percentage = percentage.Div(decimal.NewFromInt(100))
}
orderPrice := price.Mul(percentage).Truncate(int32(tradeSet.PriceDigit))
(*orders)[index].Price = orderPrice.String()
(*orders)[index].Num = orderQuantity.String()
lastOrderQuantity := lastNum.Sub(orderQuantity).Truncate(int32(tradeSet.AmountDigit))
//止盈后止盈、止盈后止损
if len((*orders)[index].Childs) > 0 && lastOrderQuantity.Cmp(decimal.Zero) > 0 {
calculateChildOrder(&(*orders)[index].Childs, tradeSet, ext, lastOrderQuantity, orderPrice, decimal.Zero, true)
}
}
return nil
}
// 递归订单树 // 递归订单树
func GetOrderByPid(order *models.LinePreOrder, orders []models.LinePreOrder, pid int) { func GetOrderByPid(order *models.LinePreOrder, orders []models.LinePreOrder, pid int) {
for _, v := range orders { for _, v := range orders {

View File

@ -0,0 +1,26 @@
package binanceservice
import (
"go-admin/common/global"
"go-admin/common/helper"
"testing"
"github.com/go-admin-team/go-admin-core/sdk"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// 测试策略 触发单
func TestTriggerOrder(t *testing.T) {
service := BinanceStrategyOrderService{}
dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
helper.InitDefaultRedis("127.0.0.1:6379", "", 2)
helper.InitLockRedisConn("127.0.0.1:6379", "", "2")
sdk.Runtime.SetDb("default", db)
service.Orm = db
service.Debug = true
service.TriggerStrategyOrder(global.EXCHANGE_BINANCE)
}

View File

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

View File

@ -66,9 +66,11 @@ func GetConfigCacheByKey(db *gorm.DB, key string) models.SysConfig {
// 获取缓存交易对 // 获取缓存交易对
// symbolType 0-现货 1-合约 // symbolType 0-现货 1-合约
func GetTradeSet(exchangeType string, symbol string, symbolType int) (models2.TradeSet, error) { func GetTradeSet(exchangeType string, symbol string, symbolType int) (models2.TradeSet, error) {
// 定义返回结果和val变量
result := models2.TradeSet{} result := models2.TradeSet{}
val := "" val := ""
// 根据交易对类型选择不同的key
switch symbolType { switch symbolType {
case 0: case 0:
key := fmt.Sprintf(global.TICKER_SPOT, exchangeType, symbol) key := fmt.Sprintf(global.TICKER_SPOT, exchangeType, symbol)
@ -78,14 +80,17 @@ func GetTradeSet(exchangeType string, symbol string, symbolType int) (models2.Tr
val, _ = helper.DefaultRedis.GetString(key) val, _ = helper.DefaultRedis.GetString(key)
} }
// 如果val不为空则解析val为TradeSet结构体
if val != "" { if val != "" {
if err := sonic.Unmarshal([]byte(val), &result); err != nil { if err := sonic.Unmarshal([]byte(val), &result); err != nil {
return result, err return result, err
} }
} else { } else {
// 如果val为空则返回错误信息
return result, errors.New("未找到交易对信息") return result, errors.New("未找到交易对信息")
} }
// 返回结果
return result, nil return result, nil
} }
@ -125,3 +130,22 @@ func ResetSystemSetting(db *gorm.DB) (models.LineSystemSetting, error) {
} }
return models.LineSystemSetting{}, nil 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
}

View File

@ -15,7 +15,7 @@ import (
- @msg 消息内容 - @msg 消息内容
- @listenType 订阅类型 0-现货 1-合约 - @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{} var dataMap map[string]interface{}
err = sonic.Unmarshal(msg, &dataMap) err = sonic.Unmarshal(msg, &dataMap)
@ -52,7 +52,7 @@ func ReceiveListen(msg []byte, listenType int) (reconnect bool, err error) {
} }
utility.SafeGo(func() { utility.SafeGo(func() {
binanceservice.ChangeSpotOrder(mapData) binanceservice.ChangeSpotOrder(mapData, apiKey)
}) })
} else { } else {
var data futuresdto.OrderTradeUpdate var data futuresdto.OrderTradeUpdate
@ -64,7 +64,7 @@ func ReceiveListen(msg []byte, listenType int) (reconnect bool, err error) {
} }
utility.SafeGo(func() { 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)) log.Info("executionReport 推送:", string(msg))
if listenType == 0 { //现货 if listenType == 0 { //现货
binanceservice.ChangeSpotOrder(dataMap) binanceservice.ChangeSpotOrder(dataMap, apiKey)
} else if listenType == 1 { //合约 } else if listenType == 1 { //合约
binanceservice.ChangeFutureOrder(dataMap) binanceservice.ChangeFutureOrder(dataMap, apiKey)
} else { } else {
log.Error("executionReport 不支持的订阅类型", strconv.Itoa(listenType)) log.Error("executionReport 不支持的订阅类型", strconv.Itoa(listenType))
} }
@ -101,6 +101,7 @@ func ReceiveListen(msg []byte, listenType int) (reconnect bool, err error) {
case "eventStreamTerminated": case "eventStreamTerminated":
log.Info("账户数据流被终止 type:", getWsTypeName(listenType)) log.Info("账户数据流被终止 type:", getWsTypeName(listenType))
return true, nil
default: default:
log.Info("未知事件 内容:", string(msg)) log.Info("未知事件 内容:", string(msg))
log.Info("未知事件", event) log.Info("未知事件", event)

View File

@ -20,6 +20,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/bytedance/sonic" "github.com/bytedance/sonic"
@ -42,7 +43,9 @@ type BinanceWebSocketManager struct {
isStopped bool // 标记 WebSocket 是否已主动停止 isStopped bool // 标记 WebSocket 是否已主动停止
mu sync.Mutex // 用于控制并发访问 isStopped mu sync.Mutex // 用于控制并发访问 isStopped
cancelFunc context.CancelFunc 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() { func (wm *BinanceWebSocketManager) Start() {
utility.SafeGo(wm.run) utility.SafeGo(wm.run)
// wm.run() // wm.run()
@ -91,14 +98,33 @@ func (wm *BinanceWebSocketManager) Restart(apiKey, apiSecret, proxyType, proxyAd
wm.isStopped = false wm.isStopped = false
utility.SafeGo(wm.run) utility.SafeGo(wm.run)
} else { } else {
wm.reconnect <- struct{}{} log.Warnf("调用restart")
wm.triggerReconnect(true)
} }
return wm 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:
// 防止阻塞,如果通道满了就跳过
log.Debugf("reconnect 信号已存在,跳过 key:%s", wm.apiKey)
}
}
}
func Restart(wm *BinanceWebSocketManager) { func Restart(wm *BinanceWebSocketManager) {
wm.reconnect <- struct{}{} log.Warnf("调用restart")
wm.triggerReconnect(true)
} }
func (wm *BinanceWebSocketManager) run() { func (wm *BinanceWebSocketManager) run() {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
@ -201,6 +227,8 @@ func (wm *BinanceWebSocketManager) connect(ctx context.Context) error {
return err return err
} }
// 连接成功,更新连接时间
wm.ConnectTime = time.Now()
log.Info(fmt.Sprintf("已连接到 Binance %s WebSocket【%s】 key:%s", getWsTypeName(wm.wsType), wm.apiKey, listenKey)) log.Info(fmt.Sprintf("已连接到 Binance %s WebSocket【%s】 key:%s", getWsTypeName(wm.wsType), wm.apiKey, listenKey))
// Ping处理 // Ping处理
@ -222,10 +250,88 @@ func (wm *BinanceWebSocketManager) connect(ctx context.Context) error {
return nil return nil
}) })
// utility.SafeGoParam(wm.restartConnect, ctx)
utility.SafeGo(func() { wm.startListenKeyRenewal2(ctx) }) utility.SafeGo(func() { wm.startListenKeyRenewal2(ctx) })
utility.SafeGo(func() { wm.readMessages(ctx) }) utility.SafeGo(func() { wm.readMessages(ctx) })
utility.SafeGo(func() { wm.handleReconnect(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 return nil
} }
@ -251,6 +357,7 @@ func setLastTime(wm *BinanceWebSocketManager) {
if val != "" { if val != "" {
helper.DefaultRedis.SetString(subKey, val) helper.DefaultRedis.SetString(subKey, val)
} }
} }
func (wm *BinanceWebSocketManager) getDialer() (*websocket.Dialer, error) { func (wm *BinanceWebSocketManager) getDialer() (*websocket.Dialer, error) {
@ -357,7 +464,8 @@ func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) {
_, msg, err := wm.ws.ReadMessage() _, msg, err := wm.ws.ReadMessage()
if err != nil && strings.Contains(err.Error(), "websocket: close") { if err != nil && strings.Contains(err.Error(), "websocket: close") {
if !wm.isStopped { if !wm.isStopped {
wm.reconnect <- struct{}{} log.Error("收到关闭消息", err.Error())
wm.triggerReconnect(false)
} }
log.Error("websocket 关闭") log.Error("websocket 关闭")
@ -375,11 +483,13 @@ func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) {
func (wm *BinanceWebSocketManager) handleOrderUpdate(msg []byte) { func (wm *BinanceWebSocketManager) handleOrderUpdate(msg []byte) {
setLastTime(wm) setLastTime(wm)
if reconnect, _ := ReceiveListen(msg, wm.wsType); reconnect { if reconnect, _ := ReceiveListen(msg, wm.wsType, wm.apiKey); reconnect {
wm.reconnect <- struct{}{} log.Errorf("收到重连请求")
wm.triggerReconnect(false)
} }
} }
// Stop 安全停止 WebSocket
func (wm *BinanceWebSocketManager) Stop() { func (wm *BinanceWebSocketManager) Stop() {
wm.mu.Lock() wm.mu.Lock()
defer wm.mu.Unlock() defer wm.mu.Unlock()
@ -387,9 +497,8 @@ func (wm *BinanceWebSocketManager) Stop() {
if wm.isStopped { if wm.isStopped {
return return
} }
wm.isStopped = true wm.isStopped = true
// 关闭 stopChannel确保已经关闭避免 panic
select { select {
case <-wm.stopChannel: case <-wm.stopChannel:
default: default:
@ -398,107 +507,182 @@ func (wm *BinanceWebSocketManager) Stop() {
if wm.cancelFunc != nil { if wm.cancelFunc != nil {
wm.cancelFunc() wm.cancelFunc()
wm.cancelFunc = nil
} }
if wm.ws != nil { if wm.ws != nil {
if err := wm.ws.Close(); err != nil { if err := wm.ws.Close(); err != nil {
log.Error(fmt.Sprintf("key【%s】close失败", wm.apiKey), err) log.Errorf("WebSocket Close 错误 key:%s err:%v", wm.apiKey, err)
} else {
log.Info(fmt.Sprintf("key【%s】close", wm.apiKey))
} }
wm.ws = nil
} }
// **重新创建 stopChannel避免 Restart() 时无效** log.Infof("WebSocket 已完全停止 key:%s", wm.apiKey)
wm.stopChannel = make(chan struct{}) wm.stopChannel = make(chan struct{}, 10)
} }
// 重连机制 // handleReconnect 使用指数退避并保持永不退出
func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) { func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) {
maxRetries := 5 // 最大重试次数 const maxRetries = 100
baseDelay := time.Second * 2
retryCount := 0 retryCount := 0
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
log.Infof("handleReconnect context done: %s", wm.apiKey)
return return
case <-wm.reconnect: case <-wm.reconnect:
wm.mu.Lock()
if wm.isStopped { if wm.isStopped {
wm.mu.Unlock()
return return
} }
wm.mu.Unlock()
log.Warn("WebSocket 连接断开,尝试重连...") log.Warnf("WebSocket 连接断开,准备重连 key:%s", wm.apiKey)
if wm.ws != nil {
wm.ws.Close()
}
// 取消旧的上下文
if wm.cancelFunc != nil {
wm.cancelFunc()
}
for { for {
wm.mu.Lock()
if wm.ws != nil {
_ = wm.ws.Close()
wm.ws = nil
}
if wm.cancelFunc != nil {
wm.cancelFunc()
wm.cancelFunc = nil
}
wm.mu.Unlock()
newCtx, cancel := context.WithCancel(context.Background()) newCtx, cancel := context.WithCancel(context.Background())
wm.cancelFunc = cancel // 更新 cancelFunc wm.mu.Lock()
wm.cancelFunc = cancel
wm.mu.Unlock()
if err := wm.connect(newCtx); err != nil { if err := wm.connect(newCtx); err != nil {
log.Errorf("重连失败: %v", err) log.Errorf("🔌 重连失败%d/%dkey:%serr: %v", retryCount+1, maxRetries, wm.apiKey, err)
cancel() cancel()
retryCount++ retryCount++
if retryCount >= maxRetries { if retryCount >= maxRetries {
log.Error("重连失败次数过多,退出重连逻辑") log.Errorf("重连失败次数过多,停止重连逻辑 key:%s", wm.apiKey)
wm.reconnecting.Store(false)
return return
} }
time.Sleep(5 * time.Second) delay := baseDelay * time.Duration(1<<retryCount)
if delay > time.Minute*5 {
delay = time.Minute * 5
}
log.Warnf("等待 %v 后重试...", delay)
time.Sleep(delay)
continue continue
} }
log.Infof("✅ 重连成功 key:%s", wm.apiKey)
retryCount = 0
wm.reconnecting.Store(false)
// ✅ 重连成功后开启假死检测
utility.SafeGo(func() { wm.startDeadCheck(newCtx) })
break
}
}
}
}
// startDeadCheck 替代 Start 中的定时器,绑定连接生命周期
func (wm *BinanceWebSocketManager) startDeadCheck(ctx context.Context) {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if wm.isStopped {
return return
} }
wm.DeadCheck()
}
}
}
// 假死检测
func (wm *BinanceWebSocketManager) DeadCheck() {
subKey := fmt.Sprintf(global.USER_SUBSCRIBE, wm.apiKey)
val, _ := helper.DefaultRedis.GetString(subKey)
if val == "" {
log.Warnf("没有订阅信息,无法进行假死检测")
return
}
var data binancedto.UserSubscribeState
_ = sonic.Unmarshal([]byte(val), &data)
var lastTime *time.Time
if wm.wsType == 0 {
lastTime = data.SpotLastTime
} else {
lastTime = data.FuturesLastTime
}
// 定义最大静默时间(超出视为假死)
var timeout time.Duration
if wm.wsType == 0 {
timeout = 40 * time.Second // Spot 每 20s ping40s 足够
} else {
timeout = 6 * time.Minute // Futures 每 3 分钟 ping
}
if lastTime != nil && time.Since(*lastTime) > timeout {
log.Warnf("检测到假死连接 key:%s type:%v, 距离上次通信: %v, 触发重连", wm.apiKey, wm.wsType, time.Since(*lastTime))
wm.triggerReconnect(true)
}
}
// 主动心跳发送机制
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 // 定期删除listenkey 并重启ws
func (wm *BinanceWebSocketManager) startListenKeyRenewal(ctx context.Context, listenKey string) { // func (wm *BinanceWebSocketManager) startListenKeyRenewal(ctx context.Context, listenKey string) {
time.Sleep(30 * time.Minute) // time.Sleep(30 * time.Minute)
select { // select {
case <-ctx.Done(): // case <-ctx.Done():
return // return
default: // default:
if err := wm.deleteListenKey(listenKey); err != nil { // if err := wm.deleteListenKey(listenKey); err != nil {
log.Error("Failed to renew listenKey: ,type:%v key: %s", wm.wsType, wm.apiKey, err) // log.Error("Failed to renew listenKey: ,type:%v key: %s", wm.wsType, wm.apiKey, err)
} else { // } else {
log.Debug("Successfully delete listenKey") // log.Debug("Successfully delete listenKey")
wm.reconnect <- struct{}{} // wm.triggerReconnect()
} // }
} // }
// ticker := time.NewTicker(5 * time.Minute) // }
// defer ticker.Stop()
// for {
// select {
// case <-ticker.C:
// if wm.isStopped {
// return
// }
// if err := wm.deleteListenKey(listenKey); err != nil {
// log.Error("Failed to renew listenKey: ,type:%v key: %s", wm.wsType, wm.apiKey, err)
// } else {
// log.Debug("Successfully delete listenKey")
// wm.reconnect <- struct{}{}
// return
// }
// case <-ctx.Done():
// return
// }
// }
}
// 定时续期 // 定时续期
func (wm *BinanceWebSocketManager) startListenKeyRenewal2(ctx context.Context) { func (wm *BinanceWebSocketManager) startListenKeyRenewal2(ctx context.Context) {
@ -527,49 +711,34 @@ func (wm *BinanceWebSocketManager) startListenKeyRenewal2(ctx context.Context) {
/* /*
删除listenkey 删除listenkey
*/ */
func (wm *BinanceWebSocketManager) deleteListenKey(listenKey string) error { // func (wm *BinanceWebSocketManager) deleteListenKey(listenKey string) error {
client, err := wm.createBinanceClient() // client, err := wm.createBinanceClient()
if err != nil { // if err != nil {
return err // return err
} // }
var resp []byte // var resp []byte
switch wm.wsType { // switch wm.wsType {
case 0: // case 0:
path := fmt.Sprintf("/api/v3/userDataStream") // path := fmt.Sprintf("/api/v3/userDataStream")
params := map[string]interface{}{ // params := map[string]interface{}{
"listenKey": listenKey, // "listenKey": listenKey,
} // }
resp, _, err = client.SendSpotRequestByKey(path, "DELETE", params) // resp, _, err = client.SendSpotRequestByKey(path, "DELETE", params)
log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp))) // log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp)))
case 1: // case 1:
resp, _, err = client.SendFuturesRequestByKey("/fapi/v1/listenKey", "DELETE", nil) // resp, _, err = client.SendFuturesRequestByKey("/fapi/v1/listenKey", "DELETE", nil)
log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp))) // log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp)))
default: // default:
return errors.New("unknown ws type") // return errors.New("unknown ws type")
} // }
return err // return err
} // }
func (wm *BinanceWebSocketManager) renewListenKey(listenKey string) error { 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() client, err := wm.createBinanceClient()
if err != nil { if err != nil {
return err return err

View File

@ -162,14 +162,19 @@ func handleTickerAllMessage(msg []byte) {
} }
} }
//行情存储时间 val, _ := helper.DefaultRedis.GetAllList(rediskey.CacheSymbolLastPrice)
lastUtc := utcTime - 1000*60*60
if _, err := helper.DefaultRedis.RemoveBeforeScore(lastPriceKey, float64(lastUtc)); err != nil {
log.Errorf("移除 合约交易对:%s %d之前的最新成交价失败,err:%v", symbol, lastUtc, err)
}
if err := helper.DefaultRedis.AddSortSet(lastPriceKey, float64(utcTime), lastPrice.String()); err != nil { if utility.ContainsStr(val, symbol) {
log.Errorf("添加 合约交易对:%s %d之前的最新成交价失败,err:%v", symbol, lastUtc, err) //行情存储时间
lastUtc := utcTime - 1000*60*60
content := fmt.Sprintf("%d:%s", utcTime, lastPrice.String())
if _, err := helper.DefaultRedis.RemoveBeforeScore(lastPriceKey, float64(lastUtc)); err != nil {
log.Errorf("移除 合约交易对:%s %d之前的最新成交价失败,err:%v", symbol, lastUtc, err)
}
if err := helper.DefaultRedis.AddSortSet(lastPriceKey, float64(utcTime), content); err != nil {
log.Errorf("添加 合约交易对:%s %d之前的最新成交价失败,err:%v", symbol, lastUtc, err)
}
} }
} }

View File

@ -250,15 +250,20 @@ func handleTickerMessage(msg []byte) {
} }
} }
//行情存储时间 val, _ := helper.DefaultRedis.GetAllList(rediskey.CacheSymbolLastPrice)
lastUtc := utcTime - 1000*60*60
if _, err := helper.DefaultRedis.RemoveBeforeScore(lastPriceKey, float64(lastUtc)); err != nil { if utility.ContainsStr(val, symbolName) {
log.Errorf("移除 现货交易对:%s %d之前的最新成交价失败,err:%v", symbolName, lastUtc, err) //行情存储时间
} lastUtc := utcTime - 1000*60*60
content := fmt.Sprintf("%d:%s", utcTime, lastPrice.String())
if err := helper.DefaultRedis.AddSortSet(lastPriceKey, float64(utcTime), lastPrice.String()); err != nil { if _, err := helper.DefaultRedis.RemoveBeforeScore(lastPriceKey, float64(lastUtc)); err != nil {
log.Errorf("添加 现货交易对:%s %d之前的最新成交价失败,err:%v", symbolName, lastUtc, err) log.Errorf("移除 现货交易对:%s %d之前的最新成交价失败,err:%v", symbolName, lastUtc, err)
}
if err := helper.DefaultRedis.AddSortSet(lastPriceKey, float64(utcTime), content); err != nil {
log.Errorf("添加 现货交易对:%s %d之前的最新成交价失败,err:%v", symbolName, lastUtc, err)
}
} }
} }