Compare commits

17 Commits

Author SHA1 Message Date
b1cca5bec7 1、状态更新错误处理 2025-08-27 14:54:03 +08:00
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
a9fc1c87f5 1 2025-04-09 14:30:15 +08:00
1512216bab 1、减仓后减仓 2025-04-09 09:09:25 +08:00
8e8c78ec0b 1、阶段减仓 2025-04-07 18:36:36 +08:00
cdd3f951a2 1、减仓策略
2、减仓后减仓节点(60%)
2025-04-03 18:32:23 +08:00
0b95e32655 1、订单增加策略模板 2025-03-31 15:37:29 +08:00
108 changed files with 13034 additions and 798 deletions

View File

@ -120,6 +120,11 @@ func (e LineApiUser) Insert(c *gin.Context) {
// 设置创建人
req.SetCreateBy(user.GetUserId(c))
if err := req.Valid(); err != nil {
e.Error(500, err, err.Error())
return
}
err = s.Insert(&req)
if err != nil {
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())
return
}
if err := req.Valid(); err != nil {
e.Error(500, err, err.Error())
return
}
req.SetUpdateBy(user.GetUserId(c))
p := actions.GetPermissionFromContext(c)
@ -267,3 +278,74 @@ func (e LineApiUser) GetMainUser(c *gin.Context) {
}
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 (
"fmt"
"github.com/gin-gonic/gin"
"github.com/go-admin-team/go-admin-core/sdk/api"
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
@ -193,6 +194,7 @@ func (e LinePriceLimit) Delete(c *gin.Context) {
e.OK(req.GetId(), "删除成功")
}
// aicoin数据同步
func (e LinePriceLimit) UpRange(c *gin.Context) {
s := service.LinePriceLimit{}
err := e.MakeContext(c).

View File

@ -0,0 +1,205 @@
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 LineReduceStrategy struct {
api.Api
}
// GetPage 获取减仓策略列表
// @Summary 获取减仓策略列表
// @Description 获取减仓策略列表
// @Tags 减仓策略
// @Param name query string false "减仓策略名称"
// @Param status query string false "状态 1-启用 2-禁用"
// @Param pageSize query int false "页条数"
// @Param pageIndex query int false "页码"
// @Success 200 {object} response.Response{data=response.Page{list=[]models.LineReduceStrategy}} "{"code": 200, "data": [...]}"
// @Router /api/v1/line-reduce-strategy [get]
// @Security Bearer
func (e LineReduceStrategy) GetPage(c *gin.Context) {
req := dto.LineReduceStrategyGetPageReq{}
s := service.LineReduceStrategy{}
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.LineReduceStrategy, 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.LineReduceStrategy} "{"code": 200, "data": [...]}"
// @Router /api/v1/line-reduce-strategy/{id} [get]
// @Security Bearer
func (e LineReduceStrategy) Get(c *gin.Context) {
req := dto.LineReduceStrategyGetReq{}
s := service.LineReduceStrategy{}
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.LineReduceStrategy
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.LineReduceStrategyInsertReq true "data"
// @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}"
// @Router /api/v1/line-reduce-strategy [post]
// @Security Bearer
func (e LineReduceStrategy) Insert(c *gin.Context) {
req := dto.LineReduceStrategyInsertReq{}
s := service.LineReduceStrategy{}
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
}
if err := req.Valid(); err != nil {
e.Error(500, err, "")
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.LineReduceStrategyUpdateReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
// @Router /api/v1/line-reduce-strategy/{id} [put]
// @Security Bearer
func (e LineReduceStrategy) Update(c *gin.Context) {
req := dto.LineReduceStrategyUpdateReq{}
s := service.LineReduceStrategy{}
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
}
if err := req.Valid(); err != nil {
e.Error(500, err, "")
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.LineReduceStrategyDeleteReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}"
// @Router /api/v1/line-reduce-strategy [delete]
// @Security Bearer
func (e LineReduceStrategy) Delete(c *gin.Context) {
s := service.LineReduceStrategy{}
req := dto.LineReduceStrategyDeleteReq{}
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,191 @@
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 LineReduceStrategyItem 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.LineReduceStrategyItem}} "{"code": 200, "data": [...]}"
// @Router /api/v1/line-reduce-strategy-item [get]
// @Security Bearer
func (e LineReduceStrategyItem) GetPage(c *gin.Context) {
req := dto.LineReduceStrategyItemGetPageReq{}
s := service.LineReduceStrategyItem{}
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.LineReduceStrategyItem, 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.LineReduceStrategyItem} "{"code": 200, "data": [...]}"
// @Router /api/v1/line-reduce-strategy-item/{id} [get]
// @Security Bearer
func (e LineReduceStrategyItem) Get(c *gin.Context) {
req := dto.LineReduceStrategyItemGetReq{}
s := service.LineReduceStrategyItem{}
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.LineReduceStrategyItem
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.LineReduceStrategyItemInsertReq true "data"
// @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}"
// @Router /api/v1/line-reduce-strategy-item [post]
// @Security Bearer
func (e LineReduceStrategyItem) Insert(c *gin.Context) {
req := dto.LineReduceStrategyItemInsertReq{}
s := service.LineReduceStrategyItem{}
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.LineReduceStrategyItemUpdateReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
// @Router /api/v1/line-reduce-strategy-item/{id} [put]
// @Security Bearer
func (e LineReduceStrategyItem) Update(c *gin.Context) {
req := dto.LineReduceStrategyItemUpdateReq{}
s := service.LineReduceStrategyItem{}
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.LineReduceStrategyItemDeleteReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}"
// @Router /api/v1/line-reduce-strategy-item [delete]
// @Security Bearer
func (e LineReduceStrategyItem) Delete(c *gin.Context) {
s := service.LineReduceStrategyItem{}
req := dto.LineReduceStrategyItemDeleteReq{}
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,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,282 @@
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, "批量平仓成功")
}
// 清除所有
func (e LineReversePosition) CleanAll(c *gin.Context) {
s := service.LineReversePosition{}
err := e.MakeContext(c).
MakeOrm().
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.CleanAll(p, userId)
if err != nil {
e.Error(500, err, fmt.Sprintf("清除所有仓位失败,\r\n失败信息 %s", err.Error()))
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

@ -0,0 +1,206 @@
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 LineStrategyTemplate struct {
api.Api
}
// GetPage 获取波段策略列表
// @Summary 获取波段策略列表
// @Description 获取波段策略列表
// @Tags 波段策略
// @Param direction query int false "涨跌方向 1-涨 2-跌"
// @Param percentag query decimal.Decimal false "涨跌点数"
// @Param compareType 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.LineStrategyTemplate}} "{"code": 200, "data": [...]}"
// @Router /api/v1/line-strategy-template [get]
// @Security Bearer
func (e LineStrategyTemplate) GetPage(c *gin.Context) {
req := dto.LineStrategyTemplateGetPageReq{}
s := service.LineStrategyTemplate{}
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.LineStrategyTemplate, 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.LineStrategyTemplate} "{"code": 200, "data": [...]}"
// @Router /api/v1/line-strategy-template/{id} [get]
// @Security Bearer
func (e LineStrategyTemplate) Get(c *gin.Context) {
req := dto.LineStrategyTemplateGetReq{}
s := service.LineStrategyTemplate{}
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.LineStrategyTemplate
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.LineStrategyTemplateInsertReq true "data"
// @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}"
// @Router /api/v1/line-strategy-template [post]
// @Security Bearer
func (e LineStrategyTemplate) Insert(c *gin.Context) {
req := dto.LineStrategyTemplateInsertReq{}
s := service.LineStrategyTemplate{}
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
}
if err := req.Valid(); err != nil {
e.Error(500, err, "")
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.LineStrategyTemplateUpdateReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
// @Router /api/v1/line-strategy-template/{id} [put]
// @Security Bearer
func (e LineStrategyTemplate) Update(c *gin.Context) {
req := dto.LineStrategyTemplateUpdateReq{}
s := service.LineStrategyTemplate{}
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
}
if err := req.Valid(); err != nil {
e.Error(500, err, "")
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.LineStrategyTemplateDeleteReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}"
// @Router /api/v1/line-strategy-template [delete]
// @Security Bearer
func (e LineStrategyTemplate) Delete(c *gin.Context) {
s := service.LineStrategyTemplate{}
req := dto.LineStrategyTemplateDeleteReq{}
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,193 @@
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 LineSymbolPrice struct {
api.Api
}
// GetPage 获取缓存价格交易对列表
// @Summary 获取缓存价格交易对列表
// @Description 获取缓存价格交易对列表
// @Tags 缓存价格交易对
// @Param symbol query string false "交易对"
// @Param status query int false "状态 1-启用 2-禁用"
// @Param pageSize query int false "页条数"
// @Param pageIndex query int false "页码"
// @Success 200 {object} response.Response{data=response.Page{list=[]models.LineSymbolPrice}} "{"code": 200, "data": [...]}"
// @Router /api/v1/line-symbol-price [get]
// @Security Bearer
func (e LineSymbolPrice) GetPage(c *gin.Context) {
req := dto.LineSymbolPriceGetPageReq{}
s := service.LineSymbolPrice{}
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.LineSymbolPrice, 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.LineSymbolPrice} "{"code": 200, "data": [...]}"
// @Router /api/v1/line-symbol-price/{id} [get]
// @Security Bearer
func (e LineSymbolPrice) Get(c *gin.Context) {
req := dto.LineSymbolPriceGetReq{}
s := service.LineSymbolPrice{}
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.LineSymbolPrice
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.LineSymbolPriceInsertReq true "data"
// @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}"
// @Router /api/v1/line-symbol-price [post]
// @Security Bearer
func (e LineSymbolPrice) Insert(c *gin.Context) {
req := dto.LineSymbolPriceInsertReq{}
s := service.LineSymbolPrice{}
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.LineSymbolPriceUpdateReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
// @Router /api/v1/line-symbol-price/{id} [put]
// @Security Bearer
func (e LineSymbolPrice) Update(c *gin.Context) {
req := dto.LineSymbolPriceUpdateReq{}
s := service.LineSymbolPrice{}
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.LineSymbolPriceDeleteReq true "body"
// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}"
// @Router /api/v1/line-symbol-price [delete]
// @Security Bearer
func (e LineSymbolPrice) Delete(c *gin.Context) {
s := service.LineSymbolPrice{}
req := dto.LineSymbolPriceDeleteReq{}
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

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

View File

@ -0,0 +1,27 @@
package models
import "go-admin/common/models"
type LineOrderReduceStrategy struct {
models.Model
OrderId int `json:"orderId" gorm:"type:bigint;not null;comment:订单ID"`
ReduceStrategyId int `json:"reduceStrategyId" gorm:"type:bigint;not null;comment:减仓策略ID"`
ItemContent string `json:"itemContent" gorm:"type:text;not null;comment:策略明细json"`
Actived int `json:"actived" gorm:"type:tinyint;comment:"是否已减仓 1=已减仓 2=未减仓""`
models.ModelTime
models.ControlBy
}
func (LineOrderReduceStrategy) TableName() string {
return "line_order_reduce_strategy"
}
func (e *LineOrderReduceStrategy) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *LineOrderReduceStrategy) GetId() interface{} {
return e.Id
}

View File

@ -9,38 +9,41 @@ import (
type LinePreOrder struct {
models.Model
ExchangeType string `json:"exchangeType" gorm:"type:varchar(20);comment:交易所类型 (字典 exchange_type"`
Pid int `json:"pid" gorm:"type:int unsigned;omitempty;comment:pid"`
MainId int `json:"mainId" gorm:"type:int;comment:主单id"`
ApiId int `json:"apiId" gorm:"type:varchar(255);omitempty;comment:api用户"`
GroupId string `json:"groupId" gorm:"type:int unsigned;omitempty;comment:交易对组id"`
Symbol string `json:"symbol" gorm:"type:varchar(255);omitempty;comment:交易对"`
QuoteSymbol string `json:"quoteSymbol" gorm:"type:varchar(255);omitempty;comment:计较货币"`
SignPrice string `json:"signPrice" gorm:"type:decimal(18,8);omitempty;comment:对标价"`
SignPriceU decimal.Decimal `json:"signPriceU" gorm:"type:decimal(18,8);omitempty;comment:交易对对标U的行情价"`
SignPriceType string `json:"signPriceType" gorm:"type:enum('new','tall','low','mixture','entrust','add');omitempty;comment:对标价类型: new=最新价格;tall=24小时最高;low=24小时最低;mixture=标记价;entrust=委托实价;add=补仓"`
Rate string `json:"rate" gorm:"type:decimal(18,2);omitempty;comment:下单百分比"`
Price string `json:"price" gorm:"type:decimal(18,8);omitempty;comment:触发价格"`
Num string `json:"num" gorm:"type:decimal(18,8);omitempty;comment:购买数量"`
BuyPrice string `json:"buyPrice" gorm:"type:decimal(18,8);omitempty;comment:购买金额"`
SymbolType int `json:"symbolType" gorm:"type:int;comment:交易对类型:1=现货;2=合约"`
OrderCategory int `json:"orderCategory" gorm:"type:int;comment:订单类型: 1=主单 2=对冲单 3-加仓单"`
Site string `json:"site" gorm:"type:enum('BUY','SELL');omitempty;comment:购买方向:BUY=买;SELL=卖"`
OrderSn string `json:"orderSn" gorm:"type:varchar(255);omitempty;comment:订单号"`
OrderType int `json:"orderType" gorm:"type:int;omitempty;comment:订单类型:0=主单 1=止盈 2=止损 3=平仓 4=减仓"`
Desc string `json:"desc" gorm:"type:text;omitempty;comment:订单描述"`
Status int `json:"status" gorm:"type:int;omitempty;comment:是否被触发:0=待触发;1=已触发;2=下单失败;4=已取消;5=委托中;6=已成交;9=已平仓"`
CoverType int `json:"coverType" gorm:"type:int unsigned;omitempty;comment:对冲类型 1= 现货对合约 2=合约对合约 3 合约对现货"`
ExpireTime time.Time `json:"expireTime" gorm:"comment:过期时间"`
MainOrderType string `json:"mainOrderType" gorm:"type:enum;comment:第一笔(主单类型) 限价LIMIT市价(MARKET)"`
LossAmount decimal.Decimal `json:"lossAmount" gorm:"type:decimal(18,8);comment:亏损金额U"`
TriggerTime *time.Time `json:"triggerTime" gorm:"type:datetime;comment:触发时间"`
Child []LinePreOrder `json:"child" gorm:"-"`
ApiName string `json:"api_name" gorm:"->"`
ChildNum int64 `json:"child_num" gorm:"->"`
AddPositionStatus int `json:"add_position_status" gorm:"->"`
ReduceStatus int `json:"reduce_status" gorm:"->"`
Childs []LinePreOrder `json:"childs" gorm:"foreignKey:pid;references:id"`
ExchangeType string `json:"exchangeType" gorm:"type:varchar(20);comment:交易所类型 (字典 exchange_type"`
StrategyTemplateType int `json:"strategyTemplateType" gorm:"type:tinyint;comment:策略类型 0-无 1-波段涨跌幅"`
Pid int `json:"pid" gorm:"type:int unsigned;omitempty;comment:pid"`
MainId int `json:"mainId" gorm:"type:int;comment:主单id"`
ApiId int `json:"apiId" gorm:"type:varchar(255);omitempty;comment:api用户"`
GroupId string `json:"groupId" gorm:"type:int unsigned;omitempty;comment:交易对组id"`
Symbol string `json:"symbol" gorm:"type:varchar(255);omitempty;comment:交易对"`
QuoteSymbol string `json:"quoteSymbol" gorm:"type:varchar(255);omitempty;comment:计较货币"`
SignPrice string `json:"signPrice" gorm:"type:decimal(18,8);omitempty;comment:对标价"`
SignPriceU decimal.Decimal `json:"signPriceU" gorm:"type:decimal(18,8);omitempty;comment:交易对对标U的行情价"`
SignPriceType string `json:"signPriceType" gorm:"type:enum('new','tall','low','mixture','entrust','add');omitempty;comment:对标价类型: new=最新价格;tall=24小时最高;low=24小时最低;mixture=标记价;entrust=委托实价;add=补仓"`
Rate string `json:"rate" gorm:"type:decimal(18,2);omitempty;comment:下单百分比"`
Price string `json:"price" gorm:"type:decimal(18,8);omitempty;comment:触发价格"`
Num string `json:"num" gorm:"type:decimal(18,8);omitempty;comment:购买数量"`
BuyPrice string `json:"buyPrice" gorm:"type:decimal(18,8);omitempty;comment:购买金额"`
SymbolType int `json:"symbolType" gorm:"type:int;comment:交易对类型:1=现货;2=合约"`
OrderCategory int `json:"orderCategory" gorm:"type:int;comment:订单类型: 1=主单 2=对冲单 3-加仓单"`
Site string `json:"site" gorm:"type:enum('BUY','SELL');omitempty;comment:购买方向:BUY=买;SELL=卖"`
OrderSn string `json:"orderSn" gorm:"type:varchar(255);omitempty;comment:订单"`
OrderType int `json:"orderType" gorm:"type:int;omitempty;comment:订单类型:0=主单 1=止盈 2=止损 3=平仓 4=减仓"`
Desc string `json:"desc" gorm:"type:text;omitempty;comment:订单描述"`
Status int `json:"status" gorm:"type:int;omitempty;comment:是否被触发:0=待触发;1=已触发;2=下单失败;4=已取消;5=委托中;6=已成交;9=已平仓"`
CoverType int `json:"coverType" gorm:"type:int unsigned;omitempty;comment:对冲类型 1= 现货对合约 2=合约对合约 3 合约对现货"`
ExpireTime time.Time `json:"expireTime" gorm:"comment:过期时间"`
MainOrderType string `json:"mainOrderType" gorm:"type:enum;comment:第一笔(主单类型) 限价LIMIT市价(MARKET)"`
LossAmount decimal.Decimal `json:"lossAmount" gorm:"type:decimal(18,8);comment:亏损金额U"`
TriggerTime *time.Time `json:"triggerTime" gorm:"type:datetime;comment:触发时间"`
Child []LinePreOrder `json:"child" gorm:"-"`
ApiName string `json:"api_name" gorm:"->"`
ChildNum int64 `json:"child_num" gorm:"->"`
AddPositionStatus int `json:"add_position_status" gorm:"->"`
ReduceStatus int `json:"reduce_status" gorm:"->"`
Childs []LinePreOrder `json:"childs" gorm:"foreignKey:pid;references:id"`
ReduceOrderId int `json:"reduceOrderId" gorm:"type:bigint;comment:主减仓单id"`
// OrderReduceStrategy LineOrderReduceStrategy `json:"-" gorm:"foreignKey:order_id;references:id"`
// LinePreOrder 线上预埋单\
models.ModelTime
models.ControlBy

View File

@ -0,0 +1,28 @@
package models
import (
"go-admin/common/models"
)
type LineReduceStrategy struct {
models.Model
Name string `json:"name" gorm:"type:varchar(50);comment:减仓策略名称"`
Status int `json:"status" gorm:"type:tinyint;comment:状态 1-启用 2-禁用"`
Items []LineReduceStrategyItem `json:"items" gorm:"foreignKey:ReduceStrategyId;references:Id"`
models.ModelTime
models.ControlBy
}
func (LineReduceStrategy) TableName() string {
return "line_reduce_strategy"
}
func (e *LineReduceStrategy) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *LineReduceStrategy) GetId() interface{} {
return e.Id
}

View File

@ -0,0 +1,32 @@
package models
import (
"go-admin/common/models"
"github.com/shopspring/decimal"
)
type LineReduceStrategyItem struct {
models.Model
ReduceStrategyId int `json:"reduceStrategyId" gorm:"type:bigint;comment:减仓策略id"`
LossPercent decimal.Decimal `json:"lossPercent" gorm:"type:decimal(10,2);comment:亏损百分比"`
QuantityPercent decimal.Decimal `json:"quantityPercent" gorm:"type:decimal(10,2);comment:减仓数量百分比"`
OrderType string `json:"orderType" gorm:"type:varchar(20);comment:订单类型 LIMIT-限价 MARKET-市价"`
ReduceStrategy LineReduceStrategy `json:"reduceStrategy" gorm:"foreignKey:ReduceStrategyId;"`
models.ModelTime
models.ControlBy
}
func (LineReduceStrategyItem) TableName() string {
return "line_reduce_strategy_item"
}
func (e *LineReduceStrategyItem) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *LineReduceStrategyItem) GetId() interface{} {
return e.Id
}

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,42 @@
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:反单平均价格"`
Version int `json:"version" gorm:"type:int;default:0;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

@ -0,0 +1,33 @@
package models
import (
"go-admin/common/models"
"github.com/shopspring/decimal"
)
type LineStrategyTemplate struct {
models.Model
Name string `json:"name" gorm:"type:varchar(50);comment:策略名称"`
Direction int `json:"direction" gorm:"type:tinyint;comment:涨跌方向 1-涨 2-跌"`
Percentag decimal.Decimal `json:"percentag" gorm:"type:decimal(10,2);comment:涨跌点数"`
CompareType int `json:"compareType" gorm:"type:tinyint;comment:比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
TimeSlotStart int `json:"timeSlotStart" gorm:"type:int;comment:时间段开始(分)"`
// TimeSlotEnd int `json:"timeSlotEnd" gorm:"type:int;comment:时间断截至(分)"`
models.ModelTime
models.ControlBy
}
func (LineStrategyTemplate) TableName() string {
return "line_strategy_template"
}
func (e *LineStrategyTemplate) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *LineStrategyTemplate) GetId() interface{} {
return e.Id
}

View File

@ -0,0 +1,29 @@
package models
import (
"go-admin/common/models"
)
type LineSymbolPrice struct {
models.Model
Symbol string `json:"symbol" gorm:"type:varchar(15);comment:交易对"`
Status int `json:"status" gorm:"type:tinyint;comment:状态 1-启用 2-禁用"`
models.ModelTime
models.ControlBy
}
func (LineSymbolPrice) TableName() string {
return "line_symbol_price"
}
func (e *LineSymbolPrice) Generate() models.ActiveRecord {
o := *e
return &o
}
func (e *LineSymbolPrice) GetId() interface{} {
return e.Id
}

View File

@ -9,13 +9,14 @@ import (
type LineSystemSetting struct {
models.Model
Time int64 `json:"time" gorm:"type:int;comment:导入:挂单时长达到时间后失效"`
BatchTime int64 `json:"batchTime" gorm:"type:int;comment:批量:挂单时长达到时间后失效"`
ProfitRate string `json:"profitRate" gorm:"type:decimal(10,2);comment:平仓盈利比例"`
CoverOrderTypeBRate string `json:"coverOrderTypeBRate" gorm:"type:decimal(10,2);comment:b账户限价补单的买入百分比"`
StopLossPremium decimal.Decimal `json:"stopLossPremium" gorm:"type:decimal(10,2);comment:限价止损溢价百分比"`
AddPositionPremium decimal.Decimal `json:"addPositionPremium" gorm:"type:decimal(10,2);comment:限价加仓溢价百分比`
ReducePremium decimal.Decimal `json:"reducePremium" gorm:"type:decimal(10,2);comment:限价减仓溢价百分比"`
Time int64 `json:"time" gorm:"type:int;comment:导入:挂单时长达到时间后失效"`
BatchTime int64 `json:"batchTime" gorm:"type:int;comment:批量:挂单时长达到时间后失效"`
ProfitRate string `json:"profitRate" gorm:"type:decimal(10,2);comment:平仓盈利比例"`
CoverOrderTypeBRate string `json:"coverOrderTypeBRate" gorm:"type:decimal(10,2);comment:b账户限价补单的买入百分比"`
StopLossPremium decimal.Decimal `json:"stopLossPremium" gorm:"type:decimal(10,2);comment:限价止损溢价百分比"`
AddPositionPremium decimal.Decimal `json:"addPositionPremium" gorm:"type:decimal(10,2);comment:限价加仓溢价百分比`
ReducePremium decimal.Decimal `json:"reducePremium" gorm:"type:decimal(10,2);comment:限价减仓溢价百分比"`
ReduceEarlyTriggerPercent decimal.Decimal `json:"reduceEarlyTriggerPercent" gorm:"type:decimal(10,2);comment:减仓提前触发百分比"`
models.ModelTime
models.ControlBy
}

View File

@ -27,5 +27,11 @@ func registerLineApiUserRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMi
r.POST("bind", api.Bind) //绑定从属关系
r.POST("getUser", api.GetUser) //获取未绑定的用户
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, registerLineReduceStrategyRouter)
}
// registerLineReduceStrategyRouter
func registerLineReduceStrategyRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
api := apis.LineReduceStrategy{}
r := v1.Group("/line-reduce-strategy").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,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, registerLineReduceStrategyItemRouter)
}
// registerLineReduceStrategyItemRouter
func registerLineReduceStrategyItemRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
api := apis.LineReduceStrategyItem{}
r := v1.Group("/line-reduce-strategy-item").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,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,33 @@
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("/clean-all", actions.PermissionAction(), api.CleanAll)
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

@ -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, registerLineStrategyTemplateRouter)
}
// registerLineStrategyTemplateRouter
func registerLineStrategyTemplateRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
api := apis.LineStrategyTemplate{}
r := v1.Group("/line-strategy-template").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,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, registerLineSymbolPriceRouter)
}
// registerLineSymbolPriceRouter
func registerLineSymbolPriceRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
api := apis.LineSymbolPrice{}
r := v1.Group("/line-symbol-price").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
import (
"errors"
"go-admin/app/admin/models"
"go-admin/common/dto"
common "go-admin/common/models"
"github.com/shopspring/decimal"
)
type LineApiUserGetPageReq struct {
@ -41,44 +44,66 @@ func (m *LineApiUserGetPageReq) GetNeedSearch() interface{} {
}
type LineApiUserInsertReq struct {
Id int `json:"-" comment:"id"` // id
ExchangeType string `json:"exchangeType" comment:"交易所code"`
UserId int64 `json:"userId" comment:"用户id"`
JysId int64 `json:"jysId" comment:"关联交易所账号id"`
ApiName string `json:"apiName" comment:"api用户名"`
ApiKey string `json:"apiKey" comment:"apiKey"`
ApiSecret string `json:"apiSecret" comment:"apiSecret"`
IpAddress string `json:"ipAddress" comment:"代理地址"`
UserPass string `json:"userPass" comment:"代码账号密码"`
AdminId int64 `json:"adminId" comment:"管理员id"`
Affiliation int64 `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"`
AdminShow int64 `json:"adminShow" comment:"是否超管可见:1=是,0=否"`
Site string `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"`
Subordinate string `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"`
GroupId int64 `json:"groupId" comment:"所属组id"`
OpenStatus int64 `json:"openStatus" comment:"开启状态 0=关闭 1=开启"`
Id int `json:"-" comment:"id"` // id
ExchangeType string `json:"exchangeType" comment:"交易所code"`
UserId int64 `json:"userId" comment:"用户id"`
JysId int64 `json:"jysId" comment:"关联交易所账号id"`
ApiName string `json:"apiName" comment:"api用户名"`
ApiKey string `json:"apiKey" comment:"apiKey"`
ApiSecret string `json:"apiSecret" comment:"apiSecret"`
IpAddress string `json:"ipAddress" comment:"代理地址"`
UserPass string `json:"userPass" comment:"代码账号密码"`
Affiliation int64 `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"`
AdminShow int64 `json:"adminShow" comment:"是否超管可见:1=是,0=否"`
Site string `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"`
Subordinate string `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"`
GroupId int64 `json:"groupId" comment:"所属组id"`
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
}
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) {
if s.Id == 0 {
model.Model = common.Model{Id: s.Id}
}
model.ExchangeType = s.ExchangeType
model.UserId = s.UserId
model.JysId = s.JysId
model.ApiName = s.ApiName
model.ApiKey = s.ApiKey
model.ApiSecret = s.ApiSecret
model.IpAddress = s.IpAddress
model.UserPass = s.UserPass
model.AdminId = s.AdminId
model.Affiliation = s.Affiliation
model.AdminShow = s.AdminShow
model.Site = s.Site
model.Subordinate = s.Subordinate
model.GroupId = s.GroupId
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 // 添加这而,需要记录是被谁创建的
}
@ -87,44 +112,66 @@ func (s *LineApiUserInsertReq) GetId() interface{} {
}
type LineApiUserUpdateReq struct {
Id int `uri:"id" comment:"id"` // id
ExchangeType string `json:"exchangeType" comment:"交易所code"`
UserId int64 `json:"userId" comment:"用户id"`
JysId int64 `json:"jysId" comment:"关联交易所账号id"`
ApiName string `json:"apiName" comment:"api用户名"`
ApiKey string `json:"apiKey" comment:"apiKey"`
ApiSecret string `json:"apiSecret" comment:"apiSecret"`
IpAddress string `json:"ipAddress" comment:"代理地址"`
UserPass string `json:"userPass" comment:"代码账号密码"`
AdminId int64 `json:"adminId" comment:"管理员id"`
Affiliation int64 `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"`
AdminShow int64 `json:"adminShow" comment:"是否超管可见:1=是,0=否"`
Site string `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"`
Subordinate string `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"`
GroupId int64 `json:"groupId" comment:"所属组id"`
OpenStatus int64 `json:"openStatus" comment:"开启状态 0=关闭 1=开启"`
Id int `uri:"id" comment:"id"` // id
ExchangeType string `json:"exchangeType" comment:"交易所code"`
UserId int64 `json:"userId" comment:"用户id"`
JysId int64 `json:"jysId" comment:"关联交易所账号id"`
ApiName string `json:"apiName" comment:"api用户名"`
ApiKey string `json:"apiKey" comment:"apiKey"`
ApiSecret string `json:"apiSecret" comment:"apiSecret"`
IpAddress string `json:"ipAddress" comment:"代理地址"`
UserPass string `json:"userPass" comment:"代码账号密码"`
Affiliation int64 `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"`
AdminShow int64 `json:"adminShow" comment:"是否超管可见:1=是,0=否"`
Site string `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"`
Subordinate string `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"`
GroupId int64 `json:"groupId" comment:"所属组id"`
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
}
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) {
if s.Id == 0 {
model.Model = common.Model{Id: s.Id}
}
model.ExchangeType = s.ExchangeType
model.UserId = s.UserId
model.JysId = s.JysId
model.ApiName = s.ApiName
model.ApiKey = s.ApiKey
model.ApiSecret = s.ApiSecret
model.IpAddress = s.IpAddress
model.UserPass = s.UserPass
model.AdminId = s.AdminId
model.Affiliation = s.Affiliation
model.AdminShow = s.AdminShow
model.Site = s.Site
model.Subordinate = s.Subordinate
model.GroupId = s.GroupId
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 // 添加这而,需要记录是被谁更新的
}
@ -159,3 +206,26 @@ type LineApiUserBindSubordinateReq struct {
type GetMainUserReq struct {
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

@ -0,0 +1,21 @@
package dto
import "github.com/shopspring/decimal"
type LineOrderReduceStrategyResp struct {
MainId int `json:"mainId" comment:"主单id"`
OrderId int `json:"orderId" comment:"减仓单id"`
Symbol string `json:"symbol"`
Side string `json:"side" comment:"BUY SELL"`
Items []LineOrderReduceStrategyRespItem `json:"items"`
}
// 减仓节点
type LineOrderReduceStrategyRespItem struct {
Price decimal.Decimal `json:"p" comment:"下单价"`
Num decimal.Decimal `json:"n" comment:"下单数量"`
TriggerPrice decimal.Decimal `json:"t" comment:"触发价"`
LossPercent decimal.Decimal `json:"l" comment:"亏损百分比"`
OrderType string `json:"o" comment:"订单类型 LIMIT-限价 MARKET-市价"`
Actived bool `json:"a" comment:"是否触发 "`
}

View File

@ -186,6 +186,9 @@ func (s *LinePreOrderDeleteReq) GetId() interface{} {
type LineAddPreOrderReq struct {
ExchangeType string `json:"exchange_type" vd:"len($)>0"` //交易所类型
StrategyTemplateType int `json:"strategy_template_type"` //策略类型 0-无 1-波段涨跌幅
StrategyTemplateId int `json:"strategy_template_id"` //策略id
ReduceStrategyId int `json:"reduce_strategy_id"` //减仓策略id
OrderType int `json:"order_type"` //订单类型
Symbol string `json:"symbol"` //交易对
ApiUserId string `json:"api_id" ` //下单用户
@ -194,11 +197,12 @@ type LineAddPreOrderReq struct {
Site string `json:"site" ` //购买方向
BuyPrice string `json:"buy_price" vd:"$>0"` //购买金额 U
PricePattern string `json:"price_pattern"` //价格模式
Price string `json:"price" vd:"$>0"` //下单价百分比
Price string `json:"price"` //下单价百分比
Profit string `json:"profit" vd:"$>0"` //止盈价
ProfitNumRatio decimal.Decimal `json:"profit_num_ratio"` //止盈数量百分比
ProfitTpTpPriceRatio decimal.Decimal `json:"profit_tp_tp_price_ratio"` //止盈后止盈价百分比
ProfitTpSlPriceRatio decimal.Decimal `json:"profit_tp_sl_price_ratio"` //止盈后止损价百分比
StopLoss decimal.Decimal `json:"stop_loss"` //止损价
PriceType string `json:"price_type"` //价格类型
SaveTemplate string `json:"save_template"` //是否保存模板
TemplateName string `json:"template_name"` //模板名字
@ -283,11 +287,15 @@ func (req LineAddPreOrderReq) Valid() error {
return errors.New("主单减仓价格百分比不能为空")
}
if req.StrategyTemplateType == 1 && req.StrategyTemplateId == 0 {
return errors.New("请选择策略")
}
if req.ReduceNumRatio.IsZero() {
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("主单减仓价格百分比错误")
}
@ -318,7 +326,9 @@ func (req LineAddPreOrderReq) Valid() error {
return fmt.Errorf("%s单下跌价格不能为空", name)
}
if v.TakeProfitRatio.IsZero() || v.TakeProfitRatio.Cmp(decimal.NewFromInt(100)) > 0 {
if (v.AddType == 2 && v.AddPositionVal.Cmp(decimal.NewFromInt(100)) < 0 && v.TakeProfitRatio.IsZero()) ||
(v.AddType == 1 && v.TakeProfitRatio.IsZero()) ||
v.TakeProfitRatio.Cmp(decimal.NewFromInt(100)) > 0 {
return errors.New("止盈价格不正确")
}
@ -367,6 +377,8 @@ type LineTreeOrder struct {
// LineBatchAddPreOrderReq 批量添加订单请求参数
type LineBatchAddPreOrderReq struct {
ExchangeType string `json:"exchange_type"` //交易所类型 字典exchange_type
StrategyTemplateType int `json:"strategy_template_type"` //策略类型 0-无 1-波段涨跌幅
StrategyTemplateId int `json:"strategy_template_id"` //策略id
SymbolType int `json:"symbol_type"` //主单交易对类型 0-现货 1-合约
OrderType int `json:"order_type"` //订单类型
SymbolGroupId string `json:"symbol_group_id"` //交易对组id
@ -404,6 +416,10 @@ func (req LineBatchAddPreOrderReq) CheckParams() error {
return errors.New("请选择交易所")
}
if req.StrategyTemplateType == 1 && req.StrategyTemplateId == 0 {
return errors.New("请选择策略")
}
if req.ApiUserId == "" {
return errors.New("选择下单用户")
}
@ -431,7 +447,7 @@ func (req LineBatchAddPreOrderReq) CheckParams() error {
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("主单减仓价格百分比错误")
}
@ -462,7 +478,9 @@ func (req LineBatchAddPreOrderReq) CheckParams() error {
return fmt.Errorf("%s单下跌价格不能为空", name)
}
if v.TakeProfitRatio.IsZero() || v.TakeProfitRatio.Cmp(decimal.NewFromInt(100)) > 0 {
if (v.AddType == 2 && v.AddPositionVal.Cmp(decimal.NewFromInt(100)) < 0 && v.TakeProfitRatio.IsZero()) ||
(v.AddType == 1 && v.TakeProfitRatio.IsZero()) ||
v.TakeProfitRatio.Cmp(decimal.NewFromInt(100)) > 0 {
return errors.New("止盈价格不正确")
}
@ -557,6 +575,17 @@ type PreOrderRedisList struct {
QuoteSymbol string `json:"quote_symbol"`
}
type StrategyOrderRedisList struct {
Id int `json:"id"`
Symbol string `json:"symbol"`
Price string `json:"price"`
Site string `json:"site"`
ApiId int `json:"api_id"`
OrderSn string `json:"order_sn"`
QuoteSymbol string `json:"quote_symbol"`
LineStrategyTemplateRedis
}
type StopLossRedisList struct {
Id int `json:"id"`
PId int `json:"pid"` //父级id

View File

@ -0,0 +1,159 @@
package dto
import (
"errors"
"go-admin/app/admin/models"
"go-admin/common/dto"
common "go-admin/common/models"
)
type LineReduceStrategyGetPageReq struct {
dto.Pagination `search:"-"`
Name string `form:"name" search:"type:contains;column:name;table:line_reduce_strategy" comment:"减仓策略名称"`
Status string `form:"status" search:"type:exact;column:status;table:line_reduce_strategy" comment:"状态 1-启用 2-禁用"`
LineReduceStrategyOrder
}
type LineReduceStrategyOrder struct {
Id string `form:"idOrder" search:"type:order;column:id;table:line_reduce_strategy"`
Name string `form:"nameOrder" search:"type:order;column:name;table:line_reduce_strategy"`
Status string `form:"statusOrder" search:"type:order;column:status;table:line_reduce_strategy"`
CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:line_reduce_strategy"`
UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:line_reduce_strategy"`
DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:line_reduce_strategy"`
CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:line_reduce_strategy"`
UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:line_reduce_strategy"`
}
func (m *LineReduceStrategyGetPageReq) GetNeedSearch() interface{} {
return *m
}
type LineReduceStrategyInsertReq struct {
Id int `json:"-" comment:"主键id"` // 主键id
Name string `json:"name" comment:"减仓策略名称"`
Status int `json:"status" comment:"状态 1-启用 2-禁用"`
Items []LineReduceStrategyItem `json:"items" comment:"减仓单节点"`
common.ControlBy
}
// 参数校验
func (s *LineReduceStrategyInsertReq) Valid() error {
if s.Name == "" {
return errors.New("减仓策略名称不能为空")
}
if s.Status < 1 || s.Status > 2 {
return errors.New("状态值不合法")
}
if len(s.Items) == 0 {
return errors.New("减仓策略节点不能为空")
}
for index, item := range s.Items {
if err := item.Valid(); err != nil {
return err
}
if index > 0 && item.LossPercent.Cmp(s.Items[index-1].LossPercent) <= 0 {
return errors.New("亏损比例必须递增")
}
}
return nil
}
func (s *LineReduceStrategyInsertReq) Generate(model *models.LineReduceStrategy) {
if s.Id == 0 {
model.Model = common.Model{Id: s.Id}
}
model.Name = s.Name
model.Status = s.Status
for _, item := range s.Items {
strategyItem := models.LineReduceStrategyItem{}
strategyItem.OrderType = item.OrderType
strategyItem.LossPercent = item.LossPercent
strategyItem.QuantityPercent = item.QuantityPercent
model.Items = append(model.Items, strategyItem)
}
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
}
func (s *LineReduceStrategyInsertReq) GetId() interface{} {
return s.Id
}
type LineReduceStrategyUpdateReq struct {
Id int `uri:"id" comment:"主键id"` // 主键id
Name string `json:"name" comment:"减仓策略名称"`
Status int `json:"status" comment:"状态 1-启用 2-禁用"`
Items []LineReduceStrategyItem `json:"items" comment:"减仓单节点"`
common.ControlBy
}
func (s *LineReduceStrategyUpdateReq) Generate(model *models.LineReduceStrategy) {
if s.Id == 0 {
model.Model = common.Model{Id: s.Id}
}
model.Name = s.Name
model.Status = s.Status
for _, item := range s.Items {
strategyItem := models.LineReduceStrategyItem{}
strategyItem.OrderType = item.OrderType
strategyItem.LossPercent = item.LossPercent
strategyItem.QuantityPercent = item.QuantityPercent
model.Items = append(model.Items, strategyItem)
}
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
}
// 参数校验
func (s *LineReduceStrategyUpdateReq) Valid() error {
if s.Name == "" {
return errors.New("减仓策略名称不能为空")
}
if s.Status < 1 || s.Status > 2 {
return errors.New("状态值不合法")
}
if len(s.Items) == 0 {
return errors.New("减仓策略节点不能为空")
}
for _, item := range s.Items {
if err := item.Valid(); err != nil {
return err
}
}
return nil
}
func (s *LineReduceStrategyUpdateReq) GetId() interface{} {
return s.Id
}
// LineReduceStrategyGetReq 功能获取请求参数
type LineReduceStrategyGetReq struct {
Id int `uri:"id"`
}
func (s *LineReduceStrategyGetReq) GetId() interface{} {
return s.Id
}
// LineReduceStrategyDeleteReq 功能删除请求参数
type LineReduceStrategyDeleteReq struct {
Ids []int `json:"ids"`
}
func (s *LineReduceStrategyDeleteReq) GetId() interface{} {
return s.Ids
}

View File

@ -0,0 +1,134 @@
package dto
import (
"errors"
"go-admin/app/admin/models"
"go-admin/common/dto"
common "go-admin/common/models"
"go-admin/pkg/utility"
"github.com/shopspring/decimal"
)
type LineReduceStrategyItemGetPageReq struct {
dto.Pagination `search:"-"`
LineReduceStrategyItemOrder
}
type LineReduceStrategyItemOrder struct {
Id string `form:"idOrder" search:"type:order;column:id;table:line_reduce_strategy_item"`
ReduceStrategyId string `form:"reduceStrategyIdOrder" search:"type:order;column:reduce_strategy_id;table:line_reduce_strategy_item"`
LossPercent string `form:"lossPercentOrder" search:"type:order;column:loss_percent;table:line_reduce_strategy_item"`
OrderType string `form:"orderTypeOrder" search:"type:order;column:order_type;table:line_reduce_strategy_item"`
CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:line_reduce_strategy_item"`
UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:line_reduce_strategy_item"`
DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:line_reduce_strategy_item"`
CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:line_reduce_strategy_item"`
UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:line_reduce_strategy_item"`
}
func (m *LineReduceStrategyItemGetPageReq) GetNeedSearch() interface{} {
return *m
}
type LineReduceStrategyItem struct {
LossPercent decimal.Decimal `json:"lossPercent" comment:"止损百分比"`
QuantityPercent decimal.Decimal `json:"quantityPercent" comment:"数量百分比"`
OrderType string `json:"orderType" comment:"订单类型 LIMIT-限价 MARKET-市价"`
}
func (s *LineReduceStrategyItem) Valid() error {
if s.LossPercent.Cmp(decimal.Zero) <= 0 {
return errors.New("百分比不能小于等于0")
}
if s.LossPercent.Cmp(decimal.NewFromFloat(100)) >= 0 {
return errors.New("百分比不能大于等于100")
}
if s.QuantityPercent.Cmp(decimal.Zero) <= 0 {
return errors.New("百分比不能小于等于0")
}
if s.QuantityPercent.Cmp(decimal.NewFromInt(100)) >= 0 {
return errors.New("数量百分比不能大于等于100")
}
keys := []string{"LIMIT", "MARKET"}
if !utility.ContainsStr(keys, s.OrderType) {
return errors.New("订单类型不正确")
}
return nil
}
type LineReduceStrategyItemResp struct {
ReduceStrategyId int `json:"reduceStrategyId" comment:"减仓策略id"`
LossPercent decimal.Decimal `json:"lossPercent" comment:"亏损百分比"`
OrderType string `json:"orderType" comment:"订单类型 LIMIT-限价 MARKET-市价"`
Actived int `json:"actived" comment:"是否已减仓 1=未减仓 2=已减仓"`
}
type LineReduceStrategyItemInsertReq struct {
Id int `json:"-" comment:"主键id"` // 主键id
ReduceStrategyId int `json:"reduceStrategyId" comment:"减仓策略id"`
LossPercent decimal.Decimal `json:"lossPercent" comment:"亏损百分比"`
OrderType string `json:"orderType" comment:"订单类型 LIMIT-限价 MARKET-市价"`
common.ControlBy
}
func (s *LineReduceStrategyItemInsertReq) Generate(model *models.LineReduceStrategyItem) {
if s.Id == 0 {
model.Model = common.Model{Id: s.Id}
}
model.ReduceStrategyId = s.ReduceStrategyId
model.LossPercent = s.LossPercent
model.OrderType = s.OrderType
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
}
func (s *LineReduceStrategyItemInsertReq) GetId() interface{} {
return s.Id
}
type LineReduceStrategyItemUpdateReq struct {
Id int `uri:"id" comment:"主键id"` // 主键id
ReduceStrategyId int `json:"reduceStrategyId" comment:"减仓策略id"`
LossPercent decimal.Decimal `json:"lossPercent" comment:"亏损百分比"`
OrderType string `json:"orderType" comment:"订单类型 LIMIT-限价 MARKET-市价"`
common.ControlBy
}
func (s *LineReduceStrategyItemUpdateReq) Generate(model *models.LineReduceStrategyItem) {
if s.Id == 0 {
model.Model = common.Model{Id: s.Id}
}
model.ReduceStrategyId = s.ReduceStrategyId
model.LossPercent = s.LossPercent
model.OrderType = s.OrderType
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
}
func (s *LineReduceStrategyItemUpdateReq) GetId() interface{} {
return s.Id
}
// LineReduceStrategyItemGetReq 功能获取请求参数
type LineReduceStrategyItemGetReq struct {
Id int `uri:"id"`
}
func (s *LineReduceStrategyItemGetReq) GetId() interface{} {
return s.Id
}
// LineReduceStrategyItemDeleteReq 功能删除请求参数
type LineReduceStrategyItemDeleteReq struct {
Ids []int `json:"ids"`
}
func (s *LineReduceStrategyItemDeleteReq) GetId() interface{} {
return s.Ids
}

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

@ -0,0 +1,174 @@
package dto
import (
"errors"
"go-admin/app/admin/models"
"go-admin/common/dto"
common "go-admin/common/models"
"github.com/shopspring/decimal"
)
type LineStrategyTemplateGetPageReq struct {
dto.Pagination `search:"-"`
Direction int `form:"direction" search:"type:exact;column:direction;table:line_strategy_template" comment:"涨跌方向 1-涨 2-跌"`
Percentag decimal.Decimal `form:"percentag" search:"type:exact;column:percentag;table:line_strategy_template" comment:"涨跌点数"`
CompareType int `form:"compareType" search:"type:exact;column:compare_type;table:line_strategy_template" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
LineStrategyTemplateOrder
}
type LineStrategyTemplateOrder struct {
Id string `form:"idOrder" search:"type:order;column:id;table:line_strategy_template"`
Direction string `form:"directionOrder" search:"type:order;column:direction;table:line_strategy_template"`
Percentag string `form:"percentagOrder" search:"type:order;column:percentag;table:line_strategy_template"`
CompareType string `form:"compareTypeOrder" search:"type:order;column:compare_type;table:line_strategy_template"`
TimeSlotStart string `form:"timeSlotStartOrder" search:"type:order;column:time_slot_start;table:line_strategy_template"`
TimeSlotEnd string `form:"timeSlotEndOrder" search:"type:order;column:time_slot_end;table:line_strategy_template"`
CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:line_strategy_template"`
UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:line_strategy_template"`
DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:line_strategy_template"`
CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:line_strategy_template"`
UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:line_strategy_template"`
}
func (m *LineStrategyTemplateGetPageReq) GetNeedSearch() interface{} {
return *m
}
type LineStrategyTemplateInsertReq struct {
Id int `json:"-" comment:"主键id"` // 主键id
Name string `json:"name" comment:"策略名称"`
Direction int `json:"direction" comment:"涨跌方向 1-涨 2-跌"`
Percentag decimal.Decimal `json:"percentag" comment:"涨跌点数"`
CompareType int `json:"compareType" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
TimeSlotStart int `json:"timeSlotStart" comment:"时间段(分)"`
// TimeSlotEnd int `json:"timeSlotEnd" comment:"时间断截至(分)"`
common.ControlBy
}
// 交易参数
func (s *LineStrategyTemplateInsertReq) Valid() error {
if s.Name == "" {
return errors.New("策略名称不能为空")
}
if len(s.Name) > 50 {
return errors.New("策略名称长度不能超过50")
}
if s.Percentag.Cmp(decimal.Zero) <= 0 {
return errors.New("涨跌点数不能为空")
}
if s.CompareType < 1 || s.CompareType > 5 {
return errors.New("比较类型不合法")
}
if s.TimeSlotStart < 0 {
return errors.New("时间段不合法")
}
// if s.TimeSlotEnd < s.TimeSlotStart {
// return errors.New("时间段不合法")
// }
return nil
}
func (s *LineStrategyTemplateInsertReq) Generate(model *models.LineStrategyTemplate) {
if s.Id == 0 {
model.Model = common.Model{Id: s.Id}
}
model.Name = s.Name
model.Direction = s.Direction
model.Percentag = s.Percentag
model.CompareType = s.CompareType
model.TimeSlotStart = s.TimeSlotStart
// model.TimeSlotEnd = s.TimeSlotEnd
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
}
func (s *LineStrategyTemplateInsertReq) GetId() interface{} {
return s.Id
}
type LineStrategyTemplateUpdateReq struct {
Id int `uri:"id" comment:"主键id"` // 主键id
Name string `json:"name" comment:"策略名称"`
Direction int `json:"direction" comment:"涨跌方向 1-涨 2-跌"`
Percentag decimal.Decimal `json:"percentag" comment:"涨跌点数"`
CompareType int `json:"compareType" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
TimeSlotStart int `json:"timeSlotStart" comment:"时间段(分)"`
// TimeSlotEnd int `json:"timeSlotEnd" comment:"时间断截至(分)"`
common.ControlBy
}
// 交易参数
func (s *LineStrategyTemplateUpdateReq) Valid() error {
if s.Name == "" {
return errors.New("策略名称不能为空")
}
if len(s.Name) > 50 {
return errors.New("策略名称长度不能超过50")
}
if s.Percentag.Cmp(decimal.Zero) <= 0 {
return errors.New("涨跌点数不能为空")
}
if s.CompareType < 1 || s.CompareType > 5 {
return errors.New("比较类型不合法")
}
if s.TimeSlotStart < 0 {
return errors.New("时间段不合法")
}
// if s.TimeSlotEnd < s.TimeSlotStart {
// return errors.New("时间段不合法")
// }
return nil
}
func (s *LineStrategyTemplateUpdateReq) Generate(model *models.LineStrategyTemplate) {
if s.Id == 0 {
model.Model = common.Model{Id: s.Id}
}
model.Name = s.Name
model.Direction = s.Direction
model.Percentag = s.Percentag
model.CompareType = s.CompareType
model.TimeSlotStart = s.TimeSlotStart
// model.TimeSlotEnd = s.TimeSlotEnd
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
}
func (s *LineStrategyTemplateUpdateReq) GetId() interface{} {
return s.Id
}
// LineStrategyTemplateGetReq 功能获取请求参数
type LineStrategyTemplateGetReq struct {
Id int `uri:"id"`
}
func (s *LineStrategyTemplateGetReq) GetId() interface{} {
return s.Id
}
// LineStrategyTemplateDeleteReq 功能删除请求参数
type LineStrategyTemplateDeleteReq struct {
Ids []int `json:"ids"`
}
func (s *LineStrategyTemplateDeleteReq) GetId() interface{} {
return s.Ids
}
type LineStrategyTemplateRedis struct {
Direction int `json:"direction" comment:"涨跌方向 1-涨 2-跌"`
Percentag decimal.Decimal `json:"percentag" comment:"涨跌点数"`
CompareType int `json:"compareType" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
TimeSlotStart int `json:"timeSlotStart" comment:"时间段开始(分)"`
TimeSlotEnd int `json:"timeSlotEnd" comment:"时间断截至(分)"`
}

View File

@ -14,6 +14,7 @@ type LineSymbolGetPageReq struct {
BaseAsset string `form:"baseAsset" search:"type:contains;column:base_asset;table:line_symbol" comment:"基础货币"`
QuoteAsset string `form:"quoteAsset" search:"type:exact;column:quote_asset;table:line_symbol" comment:"计价货币"`
Type string `form:"type" search:"type:exact;column:type;table:line_symbol" comment:"交易对类型"`
// StrategyTemplateId int `form:"strategyTemplateId" search:"-" comment:"策略id"`
LineSymbolOrder
}

View File

@ -0,0 +1,88 @@
package dto
import (
"go-admin/app/admin/models"
"go-admin/common/dto"
common "go-admin/common/models"
"strings"
)
type LineSymbolPriceGetPageReq struct {
dto.Pagination `search:"-"`
Symbol string `form:"symbol" search:"type:exact;column:symbol;table:line_symbol_price" comment:"交易对"`
Status int `form:"status" search:"type:exact;column:status;table:line_symbol_price" comment:"状态 1-启用 2-禁用"`
LineSymbolPriceOrder
}
type LineSymbolPriceOrder struct {
Id string `form:"idOrder" search:"type:order;column:id;table:line_symbol_price"`
Symbol string `form:"symbolOrder" search:"type:order;column:symbol;table:line_symbol_price"`
Status string `form:"statusOrder" search:"type:order;column:status;table:line_symbol_price"`
CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:line_symbol_price"`
UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:line_symbol_price"`
DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:line_symbol_price"`
CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:line_symbol_price"`
UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:line_symbol_price"`
}
func (m *LineSymbolPriceGetPageReq) GetNeedSearch() interface{} {
return *m
}
type LineSymbolPriceInsertReq struct {
Id int `json:"-" comment:"主键id"` // 主键id
Symbol string `json:"symbol" comment:"交易对"`
Status int `json:"status" comment:"状态 1-启用 2-禁用"`
common.ControlBy
}
func (s *LineSymbolPriceInsertReq) Generate(model *models.LineSymbolPrice) {
if s.Id == 0 {
model.Model = common.Model{Id: s.Id}
}
model.Symbol = strings.ToUpper(s.Symbol)
model.Status = s.Status
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
}
func (s *LineSymbolPriceInsertReq) GetId() interface{} {
return s.Id
}
type LineSymbolPriceUpdateReq struct {
Id int `uri:"id" comment:"主键id"` // 主键id
Symbol string `json:"symbol" comment:"交易对"`
Status int `json:"status" comment:"状态 1-启用 2-禁用"`
common.ControlBy
}
func (s *LineSymbolPriceUpdateReq) Generate(model *models.LineSymbolPrice) {
if s.Id == 0 {
model.Model = common.Model{Id: s.Id}
}
model.Symbol = strings.ToUpper(s.Symbol)
model.Status = s.Status
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
}
func (s *LineSymbolPriceUpdateReq) GetId() interface{} {
return s.Id
}
// LineSymbolPriceGetReq 功能获取请求参数
type LineSymbolPriceGetReq struct {
Id int `uri:"id"`
}
func (s *LineSymbolPriceGetReq) GetId() interface{} {
return s.Id
}
// LineSymbolPriceDeleteReq 功能删除请求参数
type LineSymbolPriceDeleteReq struct {
Ids []int `json:"ids"`
}
func (s *LineSymbolPriceDeleteReq) GetId() interface{} {
return s.Ids
}

View File

@ -57,14 +57,15 @@ func (s *LineSystemSettingInsertReq) GetId() interface{} {
}
type LineSystemSettingUpdateReq struct {
Id int `uri:"id" comment:"id"` // id
Time int64 `json:"time" comment:"导入:挂单时长达到时间后失效"`
BatchTime int64 `json:"batchTime" comment:"批量:挂单时长达到时间后失效"`
ProfitRate string `json:"profitRate" comment:"平仓盈利比例"`
CoverOrderTypeBRate string `json:"coverOrderTypeBRate" comment:"b账户限价补单的买入百分比"`
StopLossPremium decimal.Decimal `json:"stopLossPremium" comment:"限价止损溢价"`
AddPositionPremium decimal.Decimal `json:"addPositionPremium" comment:"限价加仓溢价"`
ReducePremium decimal.Decimal `json:"reducePremium" comment:"限价减仓溢价"`
Id int `uri:"id" comment:"id"` // id
Time int64 `json:"time" comment:"导入:挂单时长达到时间后失效"`
BatchTime int64 `json:"batchTime" comment:"批量:挂单时长达到时间后失效"`
ProfitRate string `json:"profitRate" comment:"平仓盈利比例"`
CoverOrderTypeBRate string `json:"coverOrderTypeBRate" comment:"b账户限价补单的买入百分比"`
StopLossPremium decimal.Decimal `json:"stopLossPremium" comment:"限价止损溢价"`
AddPositionPremium decimal.Decimal `json:"addPositionPremium" comment:"限价加仓溢价"`
ReducePremium decimal.Decimal `json:"reducePremium" comment:"限价减仓溢价"`
ReduceEarlyTriggerPercent decimal.Decimal `json:"reduceEarlyTriggerPercent" comment:"减仓提前触发百分比"`
common.ControlBy
}
@ -79,6 +80,7 @@ func (s *LineSystemSettingUpdateReq) Generate(model *models.LineSystemSetting) {
model.StopLossPremium = s.StopLossPremium
model.AddPositionPremium = s.AddPositionPremium
model.ReducePremium = s.ReducePremium
model.ReduceEarlyTriggerPercent = s.ReduceEarlyTriggerPercent
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
}

View File

@ -8,6 +8,7 @@ import (
"github.com/bytedance/sonic"
"github.com/go-admin-team/go-admin-core/logger"
"github.com/go-admin-team/go-admin-core/sdk/service"
"github.com/go-redis/redis/v8"
"gorm.io/gorm"
"go-admin/app/admin/models"
@ -25,6 +26,43 @@ type LineApiUser struct {
service.Service
}
// 获取所有启用的反单api用户
func (e LineApiUser) GetReverseApiOptionsAll(user *[]dto.LineApiUserOptionResp) error {
var data models.LineApiUser
var datas []models.LineApiUser
if err := e.Orm.Model(&data).Where("open_status = 1 AND subordinate = '2'").Find(&datas).Error; err != nil {
e.Log.Errorf("LineApiUserService GetReverseApiOptionsAll error:%s \r\n", err)
return err
}
for _, item := range datas {
*user = append(*user, dto.LineApiUserOptionResp{
Id: item.Id,
Name: item.ApiName,
})
}
return nil
}
// 获取可以绑定的api列表
func (e LineApiUser) GetReverseApiOptions(req *dto.GetReverseApiOptionsReq, user *[]models.LineApiUser) error {
query := e.Orm.Model(models.LineApiUser{}).
Where("subordinate ='0' and id != ?", req.Id)
if req.ApiId > 0 {
query = query.Or(" id =?", req.ApiId)
}
if err := query.Find(user).Error; err != nil {
e.Log.Errorf("LineApiUserService GetReverseApiOptions error:%s \r\n", err)
return err
}
return nil
}
// GetPage 获取LineApiUser列表
func (e *LineApiUser) GetPage(c *dto.LineApiUserGetPageReq, p *actions.DataPermission, list *[]models.LineApiUser, count *int64) error {
var err error
@ -97,13 +135,37 @@ func (e *LineApiUser) Insert(c *dto.LineApiUserInsertReq) error {
var err error
var data models.LineApiUser
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 {
e.Log.Errorf("LineApiUserService Insert error:%s \r\n", err)
return err
}
e.saveCache(data)
if err2 := e.CacheRelation(); err2 != nil {
return err2
}
val, _ := sonic.MarshalString(&data)
if val != "" {
@ -131,7 +193,7 @@ func (e *LineApiUser) restartWebsocket(data models.LineApiUser) {
fuSocket.Stop()
}
e.saveCache(data)
e.CacheRelation()
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订阅
*/
@ -197,6 +276,7 @@ func (e *LineApiUser) Update(c *dto.LineApiUserUpdateReq, p *actions.DataPermiss
actions.Permission(data.TableName(), p),
).First(&data, c.GetId())
oldApiKey := data.ApiKey
oldReverseApiId := data.ReverseApiId
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 {
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)
if err = db.Error; err != nil {
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
}
e.saveCache(data)
if err2 := e.CacheRelation(); err2 != nil {
return err2
}
//旧key和新的key不一样则关闭旧的websocket
if oldApiKey != data.ApiKey {
@ -289,10 +398,14 @@ func (e *LineApiUser) Remove(d *dto.LineApiUserDeleteReq, p *actions.DataPermiss
_, err := helper.DefaultRedis.BatchDeleteKeys(delKeys)
if err != nil {
if err != nil && !errors.Is(err, redis.Nil) {
e.Log.Error("批量删除api_user key失败", err)
}
if err2 := e.CacheRelation(); err2 != nil {
return err2
}
return nil
}
@ -414,3 +527,44 @@ func (e *LineApiUser) GetActiveApis(apiIds []int) ([]int, error) {
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

@ -11,6 +11,7 @@ import (
"go-admin/pkg/utility"
"go-admin/pkg/utility/snowflakehelper"
"go-admin/services/binanceservice"
"go-admin/services/cacheservice"
"sort"
"strconv"
"strings"
@ -278,6 +279,8 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi
futReduceVal, _ := helper.DefaultRedis.GetAllList(spotAddPositionKey)
spotAddPositionVal, _ := helper.DefaultRedis.GetAllList(futReduceKey)
spotReduceVal, _ := helper.DefaultRedis.GetAllList(spotReduceKey)
spotStrategyMap := e.GetStrategyOrderListMap(1)
futStrategyMap := e.GetStrategyOrderListMap(2)
for _, v := range futAddPositionVal {
sonic.Unmarshal([]byte(v), &addPosition)
@ -336,8 +339,14 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi
if val, ok := spotRedces[order.Id]; ok {
helper.DefaultRedis.LRem(spotReduceKey, val)
}
var tradedSetKey string
if order.SymbolType == 1 {
tradedSetKey = fmt.Sprintf(global.TICKER_SPOT, order.ExchangeType, order.Symbol)
} else {
tradedSetKey = fmt.Sprintf(global.TICKER_FUTURES, order.ExchangeType, order.Symbol)
}
tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, 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()
marshal, _ := sonic.Marshal(redisList)
if order.SymbolType == 1 {
@ -348,6 +357,13 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi
helper.DefaultRedis.LRem(listKey, string(marshal))
}
switch {
case order.StrategyTemplateType == 1 && order.SymbolType == 1:
e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &spotStrategyMap)
case order.StrategyTemplateType == 1 && order.SymbolType == 2:
e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &futStrategyMap)
}
//会影响持仓的
removeSymbolKey := fmt.Sprintf("%v_%s_%s_%s_%v", order.ApiId, order.ExchangeType, order.Symbol, order.Site, order.SymbolType)
@ -362,6 +378,7 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi
}
binanceservice.MainClosePositionClearCache(order.Id, order.SymbolType)
binanceservice.RemoveReduceReduceCacheByMainId(order.Id, order.SymbolType)
ints = append(ints, order.Id)
}
@ -403,6 +420,15 @@ func (e *LinePreOrder) AddPreOrderCheck(req *dto.LineAddPreOrderReq, p *actions.
apiIds = append(apiIds, apiId)
}
if req.StrategyTemplateType == 1 && req.StrategyTemplateId > 0 {
cachePriceSymbols, _ := helper.DefaultRedis.GetAllList(rediskey.CacheSymbolLastPrice)
if !utility.ContainsStr(cachePriceSymbols, req.Symbol) {
*errs = append(*errs, errors.New("交易对涨跌幅缓存不存在"))
return nil
}
}
activeApiIds, _ := apiUserService.GetActiveApis(apiIds)
if len(activeApiIds) == 0 {
@ -438,6 +464,17 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
saveTemplateParams.CreateBy = req.CreateBy
e.Orm.Model(&models.LineOrderTemplateLogs{}).Create(&saveTemplateParams)
}
linestrategyTemplate := models.LineStrategyTemplate{}
if req.StrategyTemplateId > 0 {
e.Orm.Where("id =?", req.StrategyTemplateId).First(&linestrategyTemplate)
if linestrategyTemplate.Id == 0 {
*errs = append(*errs, fmt.Errorf("策略不存在:%v", req.StrategyTemplateId))
return nil
}
}
if req.SaveTemplate == "2" {
return nil
}
@ -461,13 +498,13 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
}
var AddOrder models.LinePreOrder
var profitOrder models.LinePreOrder
var stopOrder models.LinePreOrder
var reduceOrder models.LinePreOrder
//获取交易对
tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, key)
tickerPrice := utility.StrToDecimal(tradeSet.LastPrice)
if tickerPrice.Equal(decimal.Zero) { //redis 没有这个值
*errs = append(*errs, fmt.Errorf("api_id:%s 获取交易对:%s 交易行情出错", id, req.Symbol))
*errs = append(*errs, fmt.Errorf("api_id:%d 获取交易对:%s 交易行情出错", id, req.Symbol))
continue
}
@ -540,7 +577,7 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
AddOrder.Num = utility.SafeDiv(buyPrice, fromString).Truncate(int32(tradeSet.AmountDigit)).String()
}
if utility.StringToFloat64(AddOrder.Num) < tradeSet.MinQty {
*errs = append(*errs, fmt.Errorf("api_id:%s 获取交易对:%s 小于最小下单数量", id, req.Symbol))
*errs = append(*errs, fmt.Errorf("api_id:%d 获取交易对:%s 小于最小下单数量", id, req.Symbol))
continue
}
AddOrder.OrderSn = strconv.FormatInt(snowflakehelper.GetOrderId(), 10)
@ -551,10 +588,11 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
AddOrder.GroupId = "0"
AddOrder.Status = 0
copier.Copy(&profitOrder, &AddOrder)
copier.Copy(&stopOrder, &AddOrder)
copier.Copy(&reduceOrder, &AddOrder)
preOrderStatus := models.LinePreOrderStatus{}
preOrderStatus.OrderSn = AddOrder.OrderSn
mainPrice := utility.StringToDecimal(AddOrder.Price)
//订单配置信息
preOrderExts := make([]models.LinePreOrderExt, 0)
@ -565,9 +603,16 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
OrderType: req.PriceType,
TakeProfitRatio: utility.StringToDecimal(req.Profit),
TakeProfitNumRatio: req.ProfitNumRatio,
StopLossRatio: req.StopLoss,
TpTpPriceRatio: req.ProfitTpTpPriceRatio,
TpSlPriceRatio: req.ProfitTpSlPriceRatio,
}
if req.PricePattern == "mixture" {
defultExt.TakeProfitRatio = mainPrice.Div(utility.StrToDecimal(req.Profit)).Sub(decimal.NewFromInt(1)).Abs().Mul(decimal.NewFromInt(100)).Truncate(2)
defultExt.StopLossRatio = mainPrice.Div(req.StopLoss).Sub(decimal.NewFromInt(1)).Abs().Mul(decimal.NewFromInt(100)).Truncate(2)
}
//减仓单
defultExt2 := models.LinePreOrderExt{
AddType: 2,
@ -579,18 +624,12 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
TakeProfitNumRatio: decimal.NewFromInt(100), //减仓止盈默认100%
StopLossRatio: req.ReduceStopLossRatio,
}
mainPrice := utility.StringToDecimal(AddOrder.Price)
mainAmount := utility.SafeDiv(buyPrice, mainPrice)
defultExt.TotalAfter = utility.StrToDecimal(AddOrder.Num).Truncate(int32(tradeSet.AmountDigit))
defultExt2.TotalBefore = defultExt.TotalAfter
// if decimal.NewFromInt(100).Sub(req.ReduceNumRatio).Cmp(decimal.Zero) > 0 {
default2NumPercent := utility.SafeDiv(decimal.NewFromInt(100).Sub(req.ReduceNumRatio), decimal.NewFromInt(100))
defultExt2.TotalAfter = mainAmount.Mul(default2NumPercent).Truncate(int32(tradeSet.AmountDigit))
defultExt2.ReTakeRatio = utility.SafeDiv(req.ReducePriceRatio, default2NumPercent).Truncate(2)
preOrderExts = append(preOrderExts, defultExt)
preOrderExts = append(preOrderExts, defultExt2)
calculateResp := dto.CalculateBreakEvenRatioResp{}
mainParam := dto.CalculateBreakEevenRatioReq{
@ -606,15 +645,22 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
AddPositionVal: req.ReduceNumRatio,
}
if req.PricePattern == "mixture" {
mainParam.LossEndPercent = mainPrice.Div(req.ReducePriceRatio).Sub(decimal.NewFromInt(1)).Abs().Mul(decimal.NewFromInt(100)).Truncate(2)
defultExt2.PriceRatio = mainParam.LossEndPercent
}
//计算减仓后
mainParam.LossEndPercent = req.ReducePriceRatio
defultExt2.ReTakeRatio = utility.SafeDiv(mainParam.LossEndPercent, default2NumPercent).Truncate(2)
mainParam.RemainingQuantity = mainAmount
e.CalculateBreakEvenRatio(&mainParam, &calculateResp, tradeSet)
mainParam.RemainingQuantity = calculateResp.RemainingQuantity //mainAmount.Mul(decimal.NewFromInt(100).Sub(req.ReduceNumRatio).Div(decimal.NewFromInt(100))).Truncate(int32(tradeSet.AmountDigit))
mainParam.TotalLossAmountU = calculateResp.TotalLossAmountU //buyPrice.Mul(req.ReducePriceRatio.Div(decimal.NewFromInt(100)).Truncate(4)).Truncate(int32(tradeSet.PriceDigit))
req.ReduceReTakeProfitRatio = calculateResp.Ratio
mainParam.LossBeginPercent = req.ReducePriceRatio
defultExt.ReTakeRatio = calculateResp.Ratio
mainParam.LossBeginPercent = mainParam.LossEndPercent
// defultExt.ReTakeRatio = calculateResp.Ratio
preOrderExts = append(preOrderExts, defultExt)
preOrderExts = append(preOrderExts, defultExt2)
for index, addPosition := range req.Ext {
ext := models.LinePreOrderExt{
@ -651,11 +697,20 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
preOrderExts = append(preOrderExts, ext)
}
//获取减仓策略
var reduceStrategy models.LineReduceStrategy
if req.ReduceStrategyId > 0 {
reduceStrategyService := LineReduceStrategy{Service: e.Service}
reduceStrategy, _ = reduceStrategyService.GetById(req.ReduceStrategyId)
}
//事务添加
e.Orm.Transaction(func(tx *gorm.DB) error {
reduceOrderStrategys := make([]models.LineOrderReduceStrategy, 0)
err := tx.Model(&models.LinePreOrder{}).Omit("id", "save_template", "template_name").Create(&AddOrder).Error
if err != nil {
*errs = append(*errs, fmt.Errorf("api_id:%s 获取交易对:%s 生成订单失败", id, req.Symbol))
*errs = append(*errs, fmt.Errorf("api_id:%d 获取交易对:%s 生成订单失败", id, req.Symbol))
return err
}
@ -664,117 +719,72 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
//加仓、减仓状态
tx.Model(&models.LinePreOrderStatus{}).Create(&preOrderStatus)
preOrderExts[0].OrderId = AddOrder.Id
list := dto.PreOrderRedisList{
Id: AddOrder.Id,
Symbol: AddOrder.Symbol,
Price: AddOrder.Price,
Site: AddOrder.Site,
ApiId: AddOrder.ApiId,
OrderSn: AddOrder.OrderSn,
QuoteSymbol: AddOrder.QuoteSymbol,
}
marshal, _ := sonic.Marshal(&list)
var preKey string
if AddOrder.SymbolType == global.SYMBOL_SPOT {
preKey = fmt.Sprintf(rediskey.PreSpotOrderList, AddOrder.ExchangeType)
} else {
preKey = fmt.Sprintf(rediskey.PreFutOrderList, AddOrder.ExchangeType)
}
helper.DefaultRedis.LPushList(preKey, string(marshal))
//是否有止盈止损订单
if req.Profit != "" {
if req.PricePattern == "mixture" {
mixturePrice := utility.StrToDecimal(req.Profit)
childOrders, err := makeFuturesTakeAndReduce(&AddOrder, defultExt, tradeSet)
if mixturePrice.Cmp(decimal.Zero) <= 0 {
return fmt.Errorf("止盈价不能小于等于0")
}
profitOrder.Price = mixturePrice.Truncate(int32(tradeSet.PriceDigit)).String()
profitOrder.Rate = "0"
} else {
if strings.ToUpper(req.Site) == "BUY" {
// profitOrder.Site = "SELL"
profitOrder.Price = decimal.NewFromFloat(utility.StringToFloat64(AddOrder.Price) * (1 + utility.StringToFloat64(req.Profit)/100)).Truncate(int32(tradeSet.PriceDigit)).String()
} else {
// profitOrder.Site = "BUY"
profitOrder.Price = decimal.NewFromFloat(utility.StringToFloat64(AddOrder.Price) * (1 - utility.StringToFloat64(req.Profit)/100)).Truncate(int32(tradeSet.PriceDigit)).String()
}
profitOrder.Rate = req.Profit
}
if strings.ToUpper(req.Site) == "BUY" {
profitOrder.Site = "SELL"
} else {
profitOrder.Site = "BUY"
}
profitOrder.OrderSn = strconv.FormatInt(snowflakehelper.GetOrderId(), 10)
profitOrder.Pid = AddOrder.Id
profitOrder.OrderType = 1
profitOrder.Status = 0
profitOrder.MainId = AddOrder.Id
if req.ProfitNumRatio.Cmp(decimal.Zero) > 0 {
numPercent := utility.SafeDiv(req.ProfitNumRatio, decimal.NewFromInt(100))
profitOrder.Num = utility.StrToDecimal(profitOrder.Num).Mul(numPercent).Truncate(int32(tradeSet.AmountDigit)).String()
}
tx.Model(&models.LinePreOrder{}).Omit("id", "save_template", "template_name").Create(&profitOrder)
//不全部止盈的时候
if req.ProfitNumRatio.Cmp(decimal.Zero) > 0 && req.ProfitNumRatio.Cmp(decimal.NewFromInt(100)) < 0 {
reminQuantity := utility.StrToDecimal(AddOrder.Num).Sub(utility.StrToDecimal(profitOrder.Num))
childrens, err := makeTpOrder(&profitOrder, reminQuantity, req.ProfitTpTpPriceRatio, req.ProfitTpSlPriceRatio, &tradeSet)
if err != nil {
logger.Error("生成止盈后子订单失败")
return err
}
tx.Create(&childrens)
}
if err != nil {
logger.Errorf("构建主单止盈止损失败,err:%v", err)
}
if len(childOrders) > 0 {
tx.Model(&models.LinePreOrder{}).Omit("id", "save_template", "template_name").Create(&childOrders)
for _, childOrder := range childOrders {
//不全部止盈的时候
if childOrder.OrderType == 1 && req.ProfitNumRatio.Cmp(decimal.Zero) > 0 && req.ProfitNumRatio.Cmp(decimal.NewFromInt(100)) < 0 {
reminQuantity := utility.StrToDecimal(AddOrder.Num).Sub(utility.StrToDecimal(childOrder.Num))
childrens, err := makeTpOrder(&childOrder, reminQuantity, req.ProfitTpTpPriceRatio, req.ProfitTpSlPriceRatio, &tradeSet)
if err != nil {
logger.Error("生成止盈后子订单失败")
return err
}
tx.Create(&childrens)
}
}
}
//减仓单
if req.ReducePriceRatio.Cmp(decimal.Zero) > 0 {
if req.PricePattern == "mixture" {
if req.ReducePriceRatio.Cmp(decimal.Zero) <= 0 {
return errors.New("检查价格不能小于等于0")
}
stopOrder.Price = req.ReducePriceRatio.Truncate(int32(tradeSet.PriceDigit)).String()
stopOrder.Rate = "0"
reduceOrder.Price = req.ReducePriceRatio.Truncate(int32(tradeSet.PriceDigit)).String()
reduceOrder.Rate = "0"
} else {
if strings.ToUpper(req.Site) == "BUY" {
// stopOrder.Site = "SELL"
stopOrder.Price = utility.StrToDecimal(AddOrder.Price).Mul(decimal.NewFromInt(1).Sub(utility.SafeDiv(req.ReducePriceRatio, decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)).String()
reduceOrder.Price = utility.StrToDecimal(AddOrder.Price).Mul(decimal.NewFromInt(1).Sub(utility.SafeDiv(req.ReducePriceRatio, decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)).String()
} else {
// stopOrder.Site = "BUY"
stopOrder.Price = utility.StrToDecimal(AddOrder.Price).Mul(decimal.NewFromInt(1).Add(utility.SafeDiv(req.ReducePriceRatio, decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)).String()
reduceOrder.Price = utility.StrToDecimal(AddOrder.Price).Mul(decimal.NewFromInt(1).Add(utility.SafeDiv(req.ReducePriceRatio, decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)).String()
}
stopOrder.Rate = req.ReducePriceRatio.String()
reduceOrder.Rate = req.ReducePriceRatio.String()
}
if strings.ToUpper(req.Site) == "BUY" {
stopOrder.Site = "SELL"
reduceOrder.Site = "SELL"
} else {
stopOrder.Site = "BUY"
reduceOrder.Site = "BUY"
}
stopOrder.OrderSn = strconv.FormatInt(snowflakehelper.GetOrderId(), 10)
stopOrder.Pid = AddOrder.Id
stopOrder.MainId = AddOrder.Id
stopOrder.OrderType = 4
stopOrder.Status = 0
stopOrder.BuyPrice = "0"
reduceOrder.OrderSn = strconv.FormatInt(snowflakehelper.GetOrderId(), 10)
reduceOrder.Pid = AddOrder.Id
reduceOrder.MainId = AddOrder.Id
reduceOrder.OrderType = 4
reduceOrder.Status = 0
reduceOrder.BuyPrice = "0"
stopNum := utility.StrToDecimal(AddOrder.Num).Mul(req.ReduceNumRatio.Div(decimal.NewFromInt(100)).Truncate(4))
stopOrder.Num = stopNum.Truncate(int32(tradeSet.AmountDigit)).String()
stopOrder.ExpireTime = time.Now().AddDate(10, 0, 0)
reduceOrder.Num = stopNum.Truncate(int32(tradeSet.AmountDigit)).String()
reduceOrder.ExpireTime = time.Now().AddDate(10, 0, 0)
tx.Model(&models.LinePreOrder{}).Omit("id", "save_template", "template_name").Create(&stopOrder)
preOrderExts[1].OrderId = stopOrder.Id
tx.Model(&models.LinePreOrder{}).Omit("id", "save_template", "template_name").Create(&reduceOrder)
preOrderExts[1].OrderId = reduceOrder.Id
if req.ReduceNumRatio.Cmp(decimal.Zero) > 0 && req.ReduceNumRatio.Cmp(decimal.NewFromInt(100)) < 0 {
if newOrders, err := makeReduceTakeAndStoploss(&stopOrder, defultExt2, tradeSet, false); err != nil {
if newOrders, err := makeReduceTakeAndStoploss(&reduceOrder, defultExt2, tradeSet, false); err != nil {
logger.Errorf("主单减仓生成止盈、减仓失败 err:%v", err)
return err
} else if len(newOrders) > 0 {
@ -784,9 +794,13 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
}
}
}
if reduceStrategy.Id > 0 {
reduceOrderStrategys = append(reduceOrderStrategys, initOrderReduceStrategy(reduceStrategy, reduceOrder.Id))
}
}
//添加止盈单
//添加后续节点
for index, v := range preOrderExts {
preOrderExts[index].MainOrderId = AddOrder.Id
if index < 2 {
@ -825,7 +839,7 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
}
for index := range orders {
//减仓单且 减仓比例大于0 小于100 就冲下止盈止损
//止盈后止盈止损
if orders[index].OrderType == 1 && v.TakeProfitRatio.Cmp(decimal.Zero) > 0 && v.TakeProfitRatio.Cmp(decimal.NewFromInt(100)) < 0 {
reduceChildOrders, err := makeReduceTakeAndStoploss(&(orders[index]), v, tradeSet, true)
@ -844,19 +858,112 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
}
}
}
//减仓单绑定减仓未成交策略
if newOrder.OrderType == 4 && reduceStrategy.Id > 0 {
reduceOrderStrategys = append(reduceOrderStrategys, initOrderReduceStrategy(reduceStrategy, newOrder.Id))
}
}
if len(reduceOrderStrategys) > 0 {
if err := tx.Create(reduceOrderStrategys).Error; err != nil {
logger.Error("保存减仓未成交策略失败")
return err
}
}
err = tx.Model(&models.LinePreOrderExt{}).Create(&preOrderExts).Error
if err != nil {
return err
}
return nil
//保存下单缓存
return saveOrderCache(req, AddOrder, linestrategyTemplate)
})
}
return nil
}
// 构建减仓单减仓策略
func initOrderReduceStrategy(reduceStrategy models.LineReduceStrategy, orderId int) models.LineOrderReduceStrategy {
result := models.LineOrderReduceStrategy{}
strategys := make([]dto.LineReduceStrategyItemResp, 0)
result.OrderId = orderId
result.ReduceStrategyId = reduceStrategy.Id
result.Actived = 2
for _, item := range reduceStrategy.Items {
strategys = append(strategys, dto.LineReduceStrategyItemResp{
LossPercent: item.LossPercent,
OrderType: item.OrderType,
Actived: 2,
})
}
str, _ := sonic.MarshalString(strategys)
if str != "" {
result.ItemContent = str
}
return result
}
// 保存下单缓存
func saveOrderCache(req *dto.LineAddPreOrderReq, AddOrder models.LinePreOrder, linestrategyTemplate models.LineStrategyTemplate) error {
var preKey string
var marshal []byte
switch {
//策略下单
case req.StrategyTemplateType == 1 && req.StrategyTemplateId > 0:
list := dto.StrategyOrderRedisList{
Id: AddOrder.Id,
OrderSn: AddOrder.OrderSn,
ApiId: AddOrder.ApiId,
Symbol: AddOrder.Symbol,
Price: AddOrder.Price,
Site: AddOrder.Site,
QuoteSymbol: AddOrder.QuoteSymbol,
}
list.Direction = linestrategyTemplate.Direction
list.Percentag = linestrategyTemplate.Percentag
list.CompareType = linestrategyTemplate.CompareType
list.TimeSlotStart = linestrategyTemplate.TimeSlotStart
marshal, _ = sonic.Marshal(&list)
if AddOrder.SymbolType == global.SYMBOL_SPOT {
preKey = fmt.Sprintf(rediskey.StrategySpotOrderList, AddOrder.ExchangeType)
} else {
preKey = fmt.Sprintf(rediskey.StrategyFutOrderList, AddOrder.ExchangeType)
}
//直接下单
default:
list := dto.PreOrderRedisList{
Id: AddOrder.Id,
Symbol: AddOrder.Symbol,
Price: AddOrder.Price,
Site: AddOrder.Site,
ApiId: AddOrder.ApiId,
OrderSn: AddOrder.OrderSn,
QuoteSymbol: AddOrder.QuoteSymbol,
}
marshal, _ = sonic.Marshal(&list)
if AddOrder.SymbolType == global.SYMBOL_SPOT {
preKey = fmt.Sprintf(rediskey.PreSpotOrderList, AddOrder.ExchangeType)
} else {
preKey = fmt.Sprintf(rediskey.PreFutOrderList, AddOrder.ExchangeType)
}
}
err := helper.DefaultRedis.LPushList(preKey, string(marshal))
return err
}
// 生成加仓单
func createPreAddPosition(preOrder *models.LinePreOrder, v models.LinePreOrderExt, tradeSet models2.TradeSet) models.LinePreOrder {
data := models.LinePreOrder{}
@ -932,7 +1039,6 @@ func createPreReduceOrder(preOrder *models.LinePreOrder, ext models.LinePreOrder
stopOrder.OrderType = 4
stopOrder.Status = 0
stopOrder.Rate = ext.PriceRatio.String()
stopOrder.Num = ext.TotalAfter.Sub(ext.TotalBefore).Abs().Truncate(int32(tradeSet.AmountDigit)).String()
stopOrder.BuyPrice = "0"
stopOrder.Rate = ext.PriceRatio.String()
@ -959,6 +1065,7 @@ func createPreReduceOrder(preOrder *models.LinePreOrder, ext models.LinePreOrder
// 构建止盈、止盈止损
func makeFuturesTakeAndReduce(preOrder *models.LinePreOrder, ext models.LinePreOrderExt, tradeSet models2.TradeSet) ([]models.LinePreOrder, error) {
orders := make([]models.LinePreOrder, 0)
mainId := preOrder.Id
var side string
if (preOrder.OrderType != 0 && strings.ToUpper(preOrder.Site) == "BUY") || (preOrder.OrderType == 0 && strings.ToUpper(preOrder.Site) == "SELL") {
@ -967,6 +1074,10 @@ func makeFuturesTakeAndReduce(preOrder *models.LinePreOrder, ext models.LinePreO
side = "SELL"
}
if preOrder.MainId > 0 {
mainId = preOrder.MainId
}
if ext.TakeProfitRatio.Cmp(decimal.Zero) > 0 {
// 止盈单
profitOrder := models.LinePreOrder{}
@ -977,11 +1088,7 @@ func makeFuturesTakeAndReduce(preOrder *models.LinePreOrder, ext models.LinePreO
profitOrder.Pid = preOrder.Id
profitOrder.OrderType = 1
profitOrder.Status = 0
profitOrder.MainId = preOrder.Id
if preOrder.MainId > 0 {
profitOrder.MainId = preOrder.MainId
}
profitOrder.MainId = mainId
profitOrder.BuyPrice = "0"
profitOrder.Site = side
@ -1007,7 +1114,7 @@ func makeFuturesTakeAndReduce(preOrder *models.LinePreOrder, ext models.LinePreO
lossOrder.Pid = preOrder.Id
lossOrder.OrderType = 2
lossOrder.Status = 0
lossOrder.MainId = preOrder.MainId
lossOrder.MainId = mainId
lossOrder.BuyPrice = "0"
lossOrder.Num = ext.TotalAfter.Truncate(int32(tradeSet.AmountDigit)).String()
lossOrder.Rate = ext.StopLossRatio.Truncate(2).String()
@ -1134,31 +1241,6 @@ func (e *LinePreOrder) CheckRepeatOrder(symbolType int, apiUserId, site, baseCoi
// AddBatchPreOrder 批量添加
func (e *LinePreOrder) AddBatchPreOrder(batchReq *dto.LineBatchAddPreOrderReq, p *actions.DataPermission, errs *[]error) error {
// apiIds := []int{}
// apiUserIds := strings.Split(batchReq.ApiUserId, ",")
// apiUserService := LineApiUser{Service: e.Service}
// for _, v := range apiUserIds {
// apiId, _ := strconv.Atoi(v)
// apiIds = append(apiIds, apiId)
// }
// activeIds, err := apiUserService.GetActiveApis(apiIds)
// if err != nil {
// return err
// }
// if len(activeIds) == 0 {
// return errors.New("没有可用的api")
// }
// for _, v := range apiIds {
// if !utility.ContainsInt(activeIds, v) {
// *errs = append(*errs, errors.New("api_id:"+strconv.Itoa(v)+"不可用"))
// }
// }
if batchReq.SaveTemplate == "2" || batchReq.SaveTemplate == "1" { //2 = 只保存模板 1= 保存模板并下单
var templateLog dto.LineBatchAddPreOrderReq
copier.Copy(&templateLog, batchReq)
@ -1551,6 +1633,12 @@ func (e *LinePreOrder) ClearAll() error {
"_PreFutOrderList_",
"spot_reduce_list",
"futures_reduce_list",
"spot_reduce_strategy_list",
"fut_reduce_strategy_list",
"future_position",
"spot_position",
"strategy_spot_order_list",
"strategy_fut_order_list",
}
err = helper.DefaultRedis.DeleteKeysByPrefix(prefixs...)
if err != nil {
@ -1558,9 +1646,10 @@ func (e *LinePreOrder) ClearAll() error {
return err
}
e.Orm.Model(&models.LinePreOrder{}).Exec("TRUNCATE TABLE line_pre_order") //订单表
e.Orm.Model(&models.LinePreOrder{}).Exec("TRUNCATE TABLE line_pre_order_status") //订单拓展状态
e.Orm.Model(&models.LinePreOrder{}).Exec("TRUNCATE TABLE line_pre_order_ext") //订单拓展配置
e.Orm.Model(&models.LinePreOrder{}).Exec("TRUNCATE TABLE line_pre_order") //订单表
e.Orm.Model(&models.LinePreOrder{}).Exec("TRUNCATE TABLE line_pre_order_status") //订单拓展状态
e.Orm.Model(&models.LinePreOrder{}).Exec("TRUNCATE TABLE line_pre_order_ext") //订单拓展配置
e.Orm.Model(&models.LinePreOrder{}).Exec("TRUNCATE TABLE line_order_reduce_strategy") //订单减仓策略配置
return err
}
@ -1929,6 +2018,8 @@ func (e *LinePreOrder) ClearUnTriggered() error {
var orderLists []models.LinePreOrder
positions := map[string]positiondto.LinePreOrderPositioinDelReq{}
e.Orm.Model(&models.LinePreOrder{}).Where("main_id = 0 AND pid = 0 AND status = '0'").Find(&orderLists).Unscoped().Delete(&models.LinePreOrder{})
spotStrategyMap := e.GetStrategyOrderListMap(1)
futStrategyMap := e.GetStrategyOrderListMap(2)
for _, order := range orderLists {
redisList := dto.PreOrderRedisList{
@ -1940,17 +2031,35 @@ func (e *LinePreOrder) ClearUnTriggered() error {
OrderSn: order.OrderSn,
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()
marshal, _ := sonic.Marshal(redisList)
if order.SymbolType == 1 {
key := fmt.Sprintf(rediskey.PreFutOrderList, order.ExchangeType)
helper.DefaultRedis.LRem(key, string(marshal))
} else {
key := fmt.Sprintf(rediskey.PreSpotOrderList, order.ExchangeType)
helper.DefaultRedis.LRem(key, string(marshal))
}
switch {
case order.StrategyTemplateType == 1 && order.SymbolType == 1:
e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &spotStrategyMap)
case order.StrategyTemplateType == 1 && order.SymbolType == 2:
e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &futStrategyMap)
}
//会影响持仓的
removeSymbolKey := fmt.Sprintf("%v_%s_%s_%s_%v", order.ApiId, order.ExchangeType, order.Symbol, order.Site, order.SymbolType)
@ -1990,6 +2099,60 @@ func (e *LinePreOrder) ClearUnTriggered() error {
return nil
}
// 移除待策略待触发单
func (e *LinePreOrder) RemoveStrategyOrderCache(orderId int, symbolType int, exchangeType string, caches *map[string][]dto.StrategyOrderRedisList) {
strategys, _ := (*caches)[exchangeType]
var strategyListKey string
if symbolType == 1 {
strategyListKey = fmt.Sprintf(rediskey.StrategySpotOrderList, exchangeType)
} else {
strategyListKey = fmt.Sprintf(rediskey.StrategyFutOrderList, exchangeType)
}
for _, strategy := range strategys {
if strategy.Id == orderId {
strategyVal, _ := sonic.MarshalString(strategy)
helper.DefaultRedis.LRem(strategyListKey, strategyVal)
}
}
}
// 获取策略订单缓存列表
// symbolType 1现货 2合约
func (e *LinePreOrder) GetStrategyOrderListMap(symbolType int) map[string][]dto.StrategyOrderRedisList {
result := make(map[string][]dto.StrategyOrderRedisList)
var key string
exchanges := []string{global.EXCHANGE_BINANCE}
if symbolType == 1 {
key = rediskey.StrategySpotOrderList
} else {
key = rediskey.StrategyFutOrderList
}
for _, exchange := range exchanges {
newKey := fmt.Sprintf(key, exchange)
vals, _ := helper.DefaultRedis.GetAllList(newKey)
itemData := make([]dto.StrategyOrderRedisList, 0)
item := dto.StrategyOrderRedisList{}
for _, v := range vals {
sonic.Unmarshal([]byte(v), &item)
if item.Id > 0 {
itemData = append(itemData, item)
}
}
if len(itemData) > 0 {
result[exchange] = itemData
}
}
return result
}
func (e *LinePreOrder) QueryOrder(req *dto.QueryOrderReq) (res interface{}, err error) {
var apiUserInfo models.LineApiUser
e.Orm.Model(&models.LineApiUser{}).Where("id = ?", req.ApiId).Find(&apiUserInfo)
@ -2042,9 +2205,9 @@ func (e *LinePreOrder) GenerateOrder(req *dto.LineAddPreOrderReq) error {
var tickerPrice decimal.Decimal
if req.SymbolType == 1 {
tradeSet, _ = binanceservice.GetTradeSet(req.Symbol, 0)
tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, req.Symbol, 0)
} else {
tradeSet, _ = binanceservice.GetTradeSet(req.Symbol, 1)
tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, req.Symbol, 1)
}
if tradeSet.LastPrice == "" {
@ -2083,15 +2246,18 @@ func (e *LinePreOrder) GenerateOrder(req *dto.LineAddPreOrderReq) error {
AddPositionVal: req.ReduceNumRatio,
}
if req.PricePattern == "mixture" {
mainParam.LossEndPercent = price.Div(req.ReducePriceRatio).Sub(decimal.NewFromInt(1)).Abs().Mul(decimal.NewFromInt(100)).Truncate(2)
}
//计算减仓后
mainParam.LossEndPercent = req.ReducePriceRatio
mainParam.RemainingQuantity = mainAmount
mainParam.AddType = 2
e.CalculateBreakEvenRatio(&mainParam, &calculateResp, tradeSet)
mainParam.RemainingQuantity = calculateResp.RemainingQuantity
mainParam.TotalLossAmountU = calculateResp.TotalLossAmountU
req.ReduceReTakeProfitRatio = calculateResp.Ratio
lossBeginPercent = req.ReducePriceRatio
lossBeginPercent = mainParam.LossEndPercent
//顺序排序
sort.Slice(req.Ext, func(i, j int) bool {
@ -2161,164 +2327,3 @@ func (e *LinePreOrder) CalculateBreakEvenRatio(req *dto.CalculateBreakEevenRatio
return nil
}
// // 手动加仓
// func (e *LinePreOrder) AddPosition(req *dto.LinePreOrderAddPositionReq) error {
// lastPositionOrder := models.LinePreOrder{}
// var tradeSet models2.TradeSet
// if req.OrderType == 1 {
// tradeSet, _ = binanceservice.GetTradeSet(req.Symbol, 0)
// } else if req.OrderType == 2 {
// tradeSet, _ = binanceservice.GetTradeSet(req.Symbol, 1)
// } else {
// return fmt.Errorf("交易对:%s 订单类型错误", req.Symbol)
// }
// if tradeSet.LastPrice == "" {
// return fmt.Errorf("交易对:%s 交易对配置错误", req.Symbol)
// }
// if err := e.Orm.Model(&lastPositionOrder).Where("symbol =? AND status = 6 AND site =? AND api_id =? AND symbol_type =? AND exchange_type=?",
// req.Symbol, req.Site, req.ApiUserId, req.OrderType, req.ExchangeType).Error; err != nil {
// logger.Errorf("交易对:%s查询已开仓订单失败", req.Symbol)
// return fmt.Errorf("交易对:%s 没有已开仓的订单", req.Symbol)
// }
// ext := models.LinePreOrderExt{
// MainOrderId: lastPositionOrder.Id,
// TakeProfitRatio: req.Profit,
// ReducePriceRatio: req.ReducePriceRatio,
// ReduceNumRatio: req.ReduceNumRatio,
// ReduceTakeProfitRatio: req.ReduceTakeProfitRatio,
// ReduceStopLossRatio: req.ReduceStopLossRatio,
// AddPositionOrderType: req.AddPositionOrderType,
// AddPositionType: 2,
// AddPositionVal: req.BuyPrice,
// }
// addPosition := models.LinePreOrder{
// SignPrice: tradeSet.LastPrice,
// Pid: lastPositionOrder.Id,
// MainId: lastPositionOrder.Id,
// Symbol: req.Symbol,
// QuoteSymbol: tradeSet.Currency,
// SignPriceU: utility.StrToDecimal(tradeSet.LastPrice),
// ApiId: req.ApiUserId,
// Site: req.Site,
// ExchangeType: req.ExchangeType,
// OrderType: 0,
// OrderCategory: 3,
// BuyPrice: req.BuyPrice.String(),
// Status: 0,
// SymbolType: req.OrderType,
// MainOrderType: req.AddPositionOrderType,
// ExpireTime: time.Now().Add(4),
// OrderSn: utility.Int64ToString(snowflakehelper.GetOrderId()),
// }
// tickerPrice := utility.StrToDecimal(tradeSet.LastPrice)
// if req.PricePattern == "percentage" {
// addPosition.Rate = req.Price.String()
// priceRate := req.Price.Div(decimal.NewFromInt(100)) //下单价除100 =0.1
// if strings.ToUpper(req.Site) == "BUY" { //购买方向
// //实际下单价格
// truncate := tickerPrice.Mul(decimal.NewFromInt(1).Sub(priceRate)).Truncate(int32(tradeSet.PriceDigit))
// addPosition.Price = truncate.String()
// } else {
// truncate := tickerPrice.Mul(decimal.NewFromInt(1).Add(priceRate)).Truncate(int32(tradeSet.PriceDigit))
// addPosition.Price = truncate.String()
// }
// } else { //实际价格下单
// addPosition.Price = req.Price.Truncate(int32(tradeSet.PriceDigit)).String()
// addPosition.SignPriceType = req.PricePattern
// addPosition.Rate = "0"
// }
// if tradeSet.Currency != "USDT" { //不是U本位
// //获取币本位兑换u的价格
// ticker2 := models2.TradeSet{}
// tickerVal, _ := helper.DefaultRedis.GetString(fmt.Sprintf(global.TICKER_SPOT, req.ExchangeType, strings.ToUpper(tradeSet.Coin+"USDT")))
// if tickerVal == "" {
// logger.Error("查询行情失败")
// return fmt.Errorf("交易对:%s 获取u本位行情失败", req.Symbol)
// }
// err := sonic.Unmarshal([]byte(tickerVal), &ticker2)
// if ticker2.LastPrice == "" {
// logger.Errorf("查询行情失败 %s err:%v", strings.ToUpper(tradeSet.Coin+"USDT"), err)
// return fmt.Errorf("交易对:%s 获取u本位行情 反序列化失败", req.Symbol)
// }
// //LTCBTC --> LTCUSDT
// uTickerPrice := utility.StrToDecimal(ticker2.LastPrice) //94069
// //换算成U
// //div := decimal.NewFromInt(1).Div(uTickerPrice) //0.0000106365
// //在换算成对应交易对对应的价值
// //LTCBTC --> LTCUSDT == LTCUSDT -- 100.502
// div := tickerPrice.Div(decimal.NewFromInt(1).Div(uTickerPrice))
// //计算下单数量
// addPosition.Num = req.BuyPrice.Div(div).Truncate(int32(tradeSet.AmountDigit)).String()
// } else {
// fromString, _ := decimal.NewFromString(addPosition.Price)
// addPosition.Num = req.BuyPrice.Div(fromString).Truncate(int32(tradeSet.AmountDigit)).String()
// }
// //事务保存
// err := e.Orm.Transaction(func(tx *gorm.DB) error {
// //添加加仓单
// if err := tx.Create(&addPosition).Error; err != nil {
// return err
// }
// //止盈、减仓
// orders, err := makeFuturesTakeAndReduce(&addPosition, ext, tradeSet)
// if err != nil {
// logger.Error("构造加仓单止盈、减仓失败")
// return err
// }
// if err := e.Orm.Create(&orders).Error; err != nil {
// logger.Error("保存加仓单止盈、减仓失败")
// return err
// }
// //处理减仓单
// for index := range orders {
// //减仓单且 减仓比例大于0 小于100 就冲下止盈止损
// if orders[index].OrderType == 4 && ext.ReduceNumRatio.Cmp(decimal.Zero) > 0 && ext.ReduceNumRatio.Cmp(decimal.NewFromInt(100)) < 0 {
// reduceChildOrders, err := makeReduceTakeAndStoploss(&(orders[index]), ext, tradeSet)
// if err != nil {
// logger.Error("生产加仓单止盈、减仓失败")
// return err
// }
// if len(reduceChildOrders) == 0 {
// continue
// }
// if err := e.Orm.Create(&reduceChildOrders).Error; err != nil {
// logger.Error("报错减仓后止盈止损失败")
// return err
// }
// }
// }
// ext.OrderId = addPosition.Id
// if err := tx.Create(&ext).Error; err != nil {
// return err
// }
// return nil
// })
// if err != nil {
// logger.Errorf("交易对:%s 添加加仓订单失败", req.Symbol)
// return fmt.Errorf("交易对:%s 添加加仓订单失败", req.Symbol)
// }
// return nil
// }

View File

@ -0,0 +1 @@
package service

View File

@ -0,0 +1,178 @@
package service
import (
"errors"
"fmt"
"github.com/bytedance/sonic"
"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 LineReduceStrategy struct {
service.Service
}
// GetPage 获取LineReduceStrategy列表
func (e *LineReduceStrategy) GetPage(c *dto.LineReduceStrategyGetPageReq, p *actions.DataPermission, list *[]models.LineReduceStrategy, count *int64) error {
var err error
var data models.LineReduceStrategy
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("LineReduceStrategyService GetPage error:%s \r\n", err)
return err
}
return nil
}
// Get 获取LineReduceStrategy对象
func (e *LineReduceStrategy) Get(d *dto.LineReduceStrategyGetReq, p *actions.DataPermission, model *models.LineReduceStrategy) error {
var data models.LineReduceStrategy
err := e.Orm.Model(&data).
Preload("Items").
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 GetLineReduceStrategy error:%s \r\n", err)
return err
}
if err != nil {
e.Log.Errorf("db error:%s", err)
return err
}
return nil
}
// 根据id获取对象
func (e *LineReduceStrategy) GetById(id int) (models.LineReduceStrategy, error) {
result := models.LineReduceStrategy{}
key := fmt.Sprintf(rediskey.ReduceStrategy, id)
str, _ := helper.DefaultRedis.GetString(key)
if str != "" {
sonic.Unmarshal([]byte(str), &result)
}
if result.Id == 0 {
if err := e.Orm.Model(&models.LineReduceStrategy{}).First(&result, id).Error; err != nil {
return result, err
}
}
return result, nil
}
// Insert 创建LineReduceStrategy对象
func (e *LineReduceStrategy) Insert(c *dto.LineReduceStrategyInsertReq) error {
var err error
var data models.LineReduceStrategy
var count int64
e.Orm.Model(&models.LineReduceStrategy{}).Where("name = ?", c.Name).Count(&count)
if count > 0 {
err = errors.New("策略名称重复")
return err
}
c.Generate(&data)
err = e.Orm.Create(&data).Error
if err != nil {
e.Log.Errorf("LineReduceStrategyService Insert error:%s \r\n", err)
return err
}
e.saveCache(data)
return nil
}
// Update 修改LineReduceStrategy对象
func (e *LineReduceStrategy) Update(c *dto.LineReduceStrategyUpdateReq, p *actions.DataPermission) error {
var err error
var data = models.LineReduceStrategy{}
var count int64
e.Orm.Model(&models.LineReduceStrategy{}).Where("name = ? AND id !=?", c.Name, c.GetId()).Count(&count)
if count > 0 {
err = errors.New("策略名称重复")
return err
}
e.Orm.Scopes(
actions.Permission(data.TableName(), p),
).First(&data, c.GetId())
c.Generate(&data)
err = e.Orm.Transaction(func(tx *gorm.DB) error {
if errr := tx.Delete(&models.LineReduceStrategyItem{}, "reduce_strategy_id =?", c.GetId()).Error; errr != nil {
return errr
}
db := tx.Save(&data)
if err = db.Error; err != nil {
e.Log.Errorf("LineReduceStrategyService Save error:%s \r\n", err)
return err
}
if db.RowsAffected == 0 {
return errors.New("无权更新该数据")
}
return nil
})
e.saveCache(data)
return err
}
func (e *LineReduceStrategy) saveCache(data models.LineReduceStrategy) {
str, _ := sonic.MarshalString(data)
if str != "" {
key := fmt.Sprintf(rediskey.ReduceStrategy, data.Id)
if errr := helper.DefaultRedis.SetString(key, str); errr != nil {
e.Log.Errorf("LineReduceStrategyService SetString error:%s \r\n", errr)
}
}
}
// Remove 删除LineReduceStrategy
func (e *LineReduceStrategy) Remove(d *dto.LineReduceStrategyDeleteReq, p *actions.DataPermission) error {
var data models.LineReduceStrategy
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 RemoveLineReduceStrategy error:%s \r\n", err)
return err
}
if db.RowsAffected == 0 {
return errors.New("无权删除该数据")
}
key := fmt.Sprintf(rediskey.ReduceStrategy, data.Id)
helper.DefaultRedis.DeleteString(key)
return nil
}

View File

@ -0,0 +1,109 @@
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 LineReduceStrategyItem struct {
service.Service
}
// GetPage 获取LineReduceStrategyItem列表
func (e *LineReduceStrategyItem) GetPage(c *dto.LineReduceStrategyItemGetPageReq, p *actions.DataPermission, list *[]models.LineReduceStrategyItem, count *int64) error {
var err error
var data models.LineReduceStrategyItem
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("LineReduceStrategyItemService GetPage error:%s \r\n", err)
return err
}
return nil
}
// Get 获取LineReduceStrategyItem对象
func (e *LineReduceStrategyItem) Get(d *dto.LineReduceStrategyItemGetReq, p *actions.DataPermission, model *models.LineReduceStrategyItem) error {
var data models.LineReduceStrategyItem
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 GetLineReduceStrategyItem error:%s \r\n", err)
return err
}
if err != nil {
e.Log.Errorf("db error:%s", err)
return err
}
return nil
}
// Insert 创建LineReduceStrategyItem对象
func (e *LineReduceStrategyItem) Insert(c *dto.LineReduceStrategyItemInsertReq) error {
var err error
var data models.LineReduceStrategyItem
c.Generate(&data)
err = e.Orm.Create(&data).Error
if err != nil {
e.Log.Errorf("LineReduceStrategyItemService Insert error:%s \r\n", err)
return err
}
return nil
}
// Update 修改LineReduceStrategyItem对象
func (e *LineReduceStrategyItem) Update(c *dto.LineReduceStrategyItemUpdateReq, p *actions.DataPermission) error {
var err error
var data = models.LineReduceStrategyItem{}
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("LineReduceStrategyItemService Save error:%s \r\n", err)
return err
}
if db.RowsAffected == 0 {
return errors.New("无权更新该数据")
}
return nil
}
// Remove 删除LineReduceStrategyItem
func (e *LineReduceStrategyItem) Remove(d *dto.LineReduceStrategyItemDeleteReq, p *actions.DataPermission) error {
var data models.LineReduceStrategyItem
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 RemoveLineReduceStrategyItem error:%s \r\n", err)
return err
}
if db.RowsAffected == 0 {
return errors.New("无权删除该数据")
}
return nil
}

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,345 @@
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) CleanAll(p *actions.DataPermission, userId int) error {
var count int64
if err := e.Orm.Model(&models.LineReversePosition{}).
Where("status =1 or reverse_status = 1").Count(&count).Error; err != nil {
return err
}
if count > 0 {
e.Log.Errorf("还有仓位无法清除")
return errors.New("有仓位正在进行中,不能清除所有")
}
if err := e.Orm.Exec("TRUNCATE TABLE line_reverse_position").Error; err != nil {
return err
}
return nil
}
// 批量关闭仓位
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

@ -0,0 +1,109 @@
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 LineStrategyTemplate struct {
service.Service
}
// GetPage 获取LineStrategyTemplate列表
func (e *LineStrategyTemplate) GetPage(c *dto.LineStrategyTemplateGetPageReq, p *actions.DataPermission, list *[]models.LineStrategyTemplate, count *int64) error {
var err error
var data models.LineStrategyTemplate
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("LineStrategyTemplateService GetPage error:%s \r\n", err)
return err
}
return nil
}
// Get 获取LineStrategyTemplate对象
func (e *LineStrategyTemplate) Get(d *dto.LineStrategyTemplateGetReq, p *actions.DataPermission, model *models.LineStrategyTemplate) error {
var data models.LineStrategyTemplate
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 GetLineStrategyTemplate error:%s \r\n", err)
return err
}
if err != nil {
e.Log.Errorf("db error:%s", err)
return err
}
return nil
}
// Insert 创建LineStrategyTemplate对象
func (e *LineStrategyTemplate) Insert(c *dto.LineStrategyTemplateInsertReq) error {
var err error
var data models.LineStrategyTemplate
c.Generate(&data)
err = e.Orm.Create(&data).Error
if err != nil {
e.Log.Errorf("LineStrategyTemplateService Insert error:%s \r\n", err)
return err
}
return nil
}
// Update 修改LineStrategyTemplate对象
func (e *LineStrategyTemplate) Update(c *dto.LineStrategyTemplateUpdateReq, p *actions.DataPermission) error {
var err error
var data = models.LineStrategyTemplate{}
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("LineStrategyTemplateService Save error:%s \r\n", err)
return err
}
if db.RowsAffected == 0 {
return errors.New("无权更新该数据")
}
return nil
}
// Remove 删除LineStrategyTemplate
func (e *LineStrategyTemplate) Remove(d *dto.LineStrategyTemplateDeleteReq, p *actions.DataPermission) error {
var data models.LineStrategyTemplate
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 RemoveLineStrategyTemplate error:%s \r\n", err)
return err
}
if db.RowsAffected == 0 {
return errors.New("无权删除该数据")
}
return nil
}

View File

@ -0,0 +1,147 @@
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 LineSymbolPrice struct {
service.Service
}
// GetPage 获取LineSymbolPrice列表
func (e *LineSymbolPrice) GetPage(c *dto.LineSymbolPriceGetPageReq, p *actions.DataPermission, list *[]models.LineSymbolPrice, count *int64) error {
var err error
var data models.LineSymbolPrice
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("LineSymbolPriceService GetPage error:%s \r\n", err)
return err
}
return nil
}
// Get 获取LineSymbolPrice对象
func (e *LineSymbolPrice) Get(d *dto.LineSymbolPriceGetReq, p *actions.DataPermission, model *models.LineSymbolPrice) error {
var data models.LineSymbolPrice
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 GetLineSymbolPrice error:%s \r\n", err)
return err
}
if err != nil {
e.Log.Errorf("db error:%s", err)
return err
}
return nil
}
// Insert 创建LineSymbolPrice对象
func (e *LineSymbolPrice) Insert(c *dto.LineSymbolPriceInsertReq) error {
var err error
var data models.LineSymbolPrice
c.Generate(&data)
var count int64
e.Orm.Model(&models.LineSymbolPrice{}).Where("symbol = ?", c.Symbol).Count(&count)
if count > 0 {
return errors.New("交易对已存在,请不要重复添加")
}
err = e.Orm.Create(&data).Error
if err != nil {
e.Log.Errorf("LineSymbolPriceService Insert error:%s \r\n", err)
return err
}
e.cacheList()
return nil
}
func (e *LineSymbolPrice) cacheList() {
symbols := make([]string, 0)
if err := e.Orm.Model(&models.LineSymbolPrice{}).Where("status =1").Pluck("symbol", &symbols).Error; err != nil {
e.Log.Errorf("获取可用交易对失败,err:", err)
}
if len(symbols) > 0 {
if err := helper.DefaultRedis.SetListCache(rediskey.CacheSymbolLastPrice, 0, symbols...); err != nil {
e.Log.Errorf("设置可缓存价格交易对失败")
}
}
}
// Update 修改LineSymbolPrice对象
func (e *LineSymbolPrice) Update(c *dto.LineSymbolPriceUpdateReq, p *actions.DataPermission) error {
var err error
var data = models.LineSymbolPrice{}
var count int64
e.Orm.Scopes(
actions.Permission(data.TableName(), p),
).First(&data, c.GetId())
c.Generate(&data)
e.Orm.Model(&models.LineSymbolPrice{}).Where("symbol = ? and id !=?", c.Symbol, c.GetId()).Count(&count)
if count > 0 {
return errors.New("交易对已存在,请不要重复添加")
}
db := e.Orm.Save(&data)
if err = db.Error; err != nil {
e.Log.Errorf("LineSymbolPriceService Save error:%s \r\n", err)
return err
}
if db.RowsAffected == 0 {
return errors.New("无权更新该数据")
}
e.cacheList()
return nil
}
// Remove 删除LineSymbolPrice
func (e *LineSymbolPrice) Remove(d *dto.LineSymbolPriceDeleteReq, p *actions.DataPermission) error {
var data models.LineSymbolPrice
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 RemoveLineSymbolPrice error:%s \r\n", err)
return err
}
if db.RowsAffected == 0 {
return errors.New("无权删除该数据")
}
e.cacheList()
return nil
}
func (e *LineSymbolPrice) InitCache() {
e.cacheList()
}

View File

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

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{}, //会员到期处理
"MemberRenwalOrderExpirationJob": MemberRenwalOrderExpirationJob{}, //会员续费订单过期处理
"TrxQueryJobs": TrxQueryJobs{}, //订单支付监听
"StrategyJob": StrategyJob{}, //下单策略触发
"BinanceSpotAccountJob": BinanceSpotAccountJob{}, //币安现货划转
"BinanceFuturesAccountJob": BinanceFuturesAccountJob{}, //币安合约划转
}
}

View File

@ -3,6 +3,7 @@ package jobs
import (
"fmt"
models2 "go-admin/app/jobs/models"
"runtime"
"time"
log "github.com/go-admin-team/go-admin-core/logger"
@ -43,7 +44,10 @@ type ExecJob struct {
func (e *ExecJob) Run() {
defer func() {
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])
}
}()

View File

@ -15,6 +15,7 @@ import (
"go-admin/pkg/utility"
"go-admin/pkg/utility/snowflakehelper"
"go-admin/services/binanceservice"
"go-admin/services/cacheservice"
"go-admin/services/fileservice"
"io"
"net/http"
@ -241,7 +242,7 @@ func (t LimitOrderTimeoutDuration) ReSpotOrderPlace(db *gorm.DB, order models.Li
} else {
var remainingQuantity decimal.Decimal
spotOrder, err := spotApi.GetOrderByOrderSnLoop(order.Symbol, order.OrderSn, apiUserinfo, 4)
tradeSet, _ := binanceservice.GetTradeSet(order.Symbol, 0)
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 0)
if err == nil {
origQty := utility.StrToDecimal(spotOrder.OrigQty)
@ -359,7 +360,7 @@ func (t LimitOrderTimeoutDuration) ReFutOrderPlace(db *gorm.DB, order models.Lin
} else {
var remainingQuantity decimal.Decimal
spotOrder, err := futApi.GetOrderByOrderSnLoop(order.Symbol, order.OrderSn, apiUserinfo, 4)
tradeSet, _ := binanceservice.GetTradeSet(order.Symbol, 1)
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1)
if err == nil {
origQty := utility.StrToDecimal(spotOrder.OrigQty)

12
app/jobs/order_job.go Normal file
View File

@ -0,0 +1,12 @@
package jobs
//波段交易策略任务
type StrategyOrderJob struct {
}
func (t StrategyOrderJob) Exec(arg interface{}) error {
// strategyOrderService:=
//todo 策略订单任务
return nil
}

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)
})
//自动重启websocket
utility.SafeGo(func() {
reconnect(ctx)
})
// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
@ -133,3 +138,33 @@ func clearLogJob(db *gorm.DB, ctx context.Context) {
fileservice.ClearLogs(db)
}
}
// 定时重连websocket
func reconnect(ctx context.Context) error {
ticker := time.NewTicker(time.Hour * 1)
defer ticker.Stop()
select {
case <-ctx.Done():
return nil
case <-ticker.C:
serverinit.RestartConnect()
}
return nil
}
// StartDeadCheck 假死检测
func StartDeadCheck(ctx context.Context) {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
serverinit.DeadCheck()
}
}
}

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

@ -25,6 +25,11 @@ const (
PreSpotOrderList = "_PreSpotOrderList_:%s" // 待触发的现货订单集合{交易所类型 exchange_type}
PreFutOrderList = "_PreFutOrderList_:%s" // 待触发的订单集合 {交易所类型 exchange_type}
//策略现货订单集合 {交易所类型 exchange_type}
StrategySpotOrderList = "strategy_spot_order_list:%s"
//策略合约订单集合 {交易所类型 exchange_type}
StrategyFutOrderList = "strategy_fut_order_list:%s"
API_USER = "api_user:%v" // api用户
SystemSetting = "system_setting" //系统设置
ApiGroup = "api_group:%v" //api用户组 {id}
@ -41,6 +46,16 @@ const (
SpotTrigger = "spot_trigger_lock:%v_%s" //现货触发 {apiuserid|symbol}
FutTrigger = "fut_trigger_lock:%v_%s" //合约触发 {apiuserid|symbol}
//波段现货触发{apiuserid|ordersn}
StrategySpotTriggerLock = "strategy_spot_trigger_l:%v_%s"
//波段合约触发{apiuserid|ordersn}
StrategyFutTriggerLock = "strategy_fut_trigger_l:%v_%s"
//减仓波段合约触发 {apiuserid|symbol}
ReduceStrategyFutTriggerLock = "reduce_strategy_fut_trigger_l:%v_%s"
//减仓波段现货触发 {apiuserid|symbol}
ReduceStrategySpotTriggerLock = "reduce_strategy_spot_trigger_l:%v_%s"
SpotCallBack = "spot_callback:%s" //现货回调 {ordersn}
FutCallBack = "fut_callback:%s" //合约回调 {ordersn}
@ -61,6 +76,11 @@ const (
SpotPosition = "spot_position:%s:%v:%s_%s"
//合约持仓 {exchangeType,apiuserid,symbol,side}
FuturePosition = "future_position:%s:%v:%s_%s"
//现货减仓单减仓策略 {exchangeType}
SpotOrderReduceStrategyList = "spot_reduce_strategy_list:%s"
//合约减仓单减仓策略 {exchangeType}
FutOrderReduceStrategyList = "fut_reduce_strategy_list:%s"
//需要清理键值---------END-----------------
//定时取消限价并下市价锁
@ -74,6 +94,19 @@ const (
AveRequestToken = "ave_request_token" // AVE请求token
)
const (
//现货最后成交价 sort set key:={exchangeType,symbol} content:={utc:price}
SpotTickerLastPrice = "spot_ticker_last_price:%s:%s"
//合约最后成交价 sort set {exchangeType,symbol} content:={utc:price}
FutureTickerLastPrice = "fut_ticker_last_price:%s:%s"
//允许缓存交易对价格的交易对 list
CacheSymbolLastPrice = "cache_symbol_price"
//减仓策略缓存 {id}
ReduceStrategy = "reduce_stragy:%d"
)
// 用户下单
const (
MemberShipPre = "member_ship_pre:%v" //用户开通会员预下单 单价缓存{payable_amount}

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

@ -5,12 +5,14 @@ import (
"errors"
"fmt"
"log"
"math"
"reflect"
"strconv"
"time"
"github.com/bytedance/sonic"
"github.com/go-redis/redis/v8"
"github.com/shopspring/decimal"
)
// RedisHelper 结构体封装了 Redis 客户端及上下文
@ -459,44 +461,76 @@ func (r *RedisHelper) SetNX(key string, value interface{}, expiration time.Durat
return result == "OK", nil
}
func getFieldsFromStruct(obj interface{}) map[string]interface{} {
// SetHashWithTags 改进版:支持 struct 或 map 输入
func (r *RedisHelper) SetHashWithTags(key string, obj interface{}) error {
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{})
val := reflect.ValueOf(obj)
typ := reflect.TypeOf(obj)
// 如果 obj 是指针,则解引用
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
// 确保我们正在处理的是一个结构体
if val.Kind() != reflect.Struct {
return nil, fmt.Errorf("期望一个结构体或结构体指针,但得到的是 %s", val.Kind())
}
typ := val.Type() // 在解引用后获取类型
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
tag := field.Tag.Get("redis")
tag := field.Tag.Get("redis") // 获取 redis tag
if tag != "" {
fieldVal := val.Field(i)
if fieldVal.Kind() == reflect.Slice || fieldVal.Kind() == reflect.Map {
// 检查字段是否可导出,不可导出的字段无法直接通过 Interface() 访问
if !fieldVal.CanInterface() {
continue // 跳过不可导出字段
}
switch fieldVal.Kind() {
case reflect.Slice, reflect.Map:
// 处理切片或映射类型
// 对于切片,使用索引作为字段名
if fieldVal.Kind() == reflect.Slice {
for j := 0; j < fieldVal.Len(); j++ {
elem := fieldVal.Index(j).Interface()
fields[fmt.Sprintf("%s_%d", tag, j)] = elem
}
} else if fieldVal.Kind() == reflect.Map {
// 对于映射,使用键作为字段名
for _, key := range fieldVal.MapKeys() {
elem := fieldVal.MapIndex(key).Interface()
fields[fmt.Sprintf("%s_%v", tag, key.Interface())] = elem
}
}
} else {
case reflect.Struct:
if fieldVal.Type() == reflect.TypeOf(decimal.Decimal{}) {
if decVal, ok := fieldVal.Interface().(decimal.Decimal); ok {
// 将 decimal.Decimal 直接转换为字符串来保留精度
fields[tag] = decVal.String()
} else {
// 理论上不应该发生,但作为回退
fields[tag] = fieldVal.Interface()
}
} else {
fields[tag] = fieldVal.Interface()
}
default:
fields[tag] = fieldVal.Interface()
}
}
}
return fields
}
func (r *RedisHelper) SetHashWithTags(key string, obj interface{}) error {
fields := getFieldsFromStruct(obj)
_, err := r.client.HSet(r.ctx, key, fields).Result()
return err
return fields, nil
}
// HSetField 设置哈希中的一个字段
@ -542,6 +576,82 @@ func (r *RedisHelper) HGetAllFields(key string) (map[string]string, error) {
return fields, nil
}
// HGetAsObject 获取哈希中所有字段的值并反序列化为对象
func (r *RedisHelper) HGetAsObject(key string, out interface{}) error {
data, err := r.HGetAllFields(key)
if err != nil {
return err
}
val := reflect.ValueOf(out)
if val.Kind() != reflect.Ptr || val.IsNil() {
return fmt.Errorf("output must be a non-nil pointer to a struct")
}
val = val.Elem()
typ := val.Type()
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
tag := field.Tag.Get("redis")
if tag == "" {
continue
}
strVal, ok := data[tag]
if !ok {
continue
}
fieldVal := val.Field(i)
if !fieldVal.CanSet() {
continue
}
switch fieldVal.Kind() {
case reflect.String:
fieldVal.SetString(strVal)
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if intVal, err := strconv.ParseInt(strVal, 10, 64); err == nil {
fieldVal.SetInt(intVal)
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if uintVal, err := strconv.ParseUint(strVal, 10, 64); err == nil {
fieldVal.SetUint(uintVal)
}
case reflect.Float32, reflect.Float64:
if floatVal, err := strconv.ParseFloat(strVal, 64); err == nil {
fieldVal.SetFloat(floatVal)
}
case reflect.Bool:
if boolVal, err := strconv.ParseBool(strVal); err == nil {
fieldVal.SetBool(boolVal)
}
case reflect.Struct:
// 针对 time.Time 特别处理
if fieldVal.Type() == reflect.TypeOf(time.Time{}) {
if t, err := time.Parse(time.RFC3339, strVal); err == nil {
fieldVal.Set(reflect.ValueOf(t))
}
} else if fieldVal.Type() == reflect.TypeOf(decimal.Decimal{}) {
if t, err := decimal.NewFromString(strVal); err == nil {
fieldVal.Set(reflect.ValueOf(t))
}
} else {
// 其他 struct 尝试用 json 反序列化
ptr := reflect.New(fieldVal.Type()).Interface()
if err := sonic.Unmarshal([]byte(strVal), ptr); err == nil {
fieldVal.Set(reflect.ValueOf(ptr).Elem())
}
}
case reflect.Slice, reflect.Map:
ptr := reflect.New(fieldVal.Type()).Interface()
if err := sonic.Unmarshal([]byte(strVal), ptr); err == nil {
fieldVal.Set(reflect.ValueOf(ptr).Elem())
}
}
}
return nil
}
// HDelField 删除哈希中的某个字段
func (r *RedisHelper) HDelField(key, field string) error {
_, err := r.client.HDel(r.ctx, key, field).Result()
@ -572,6 +682,33 @@ func (r *RedisHelper) HKeys(key string) ([]string, error) {
return fields, nil
}
func (r *RedisHelper) HExists(key, field, value string) (bool, error) {
exists, err := r.client.HExists(r.ctx, key, field).Result()
if err != nil {
return false, fmt.Errorf("check existence failed: %v", err)
}
if !exists {
return false, nil
}
storedValue, err := r.client.HGet(r.ctx, key, field).Result()
if err != nil {
return false, fmt.Errorf("get value failed: %v", err)
}
// 如果值是 JSON比较前反序列化
var storedObj, inputObj interface{}
if err := sonic.UnmarshalString(storedValue, &storedObj); err != nil {
return false, fmt.Errorf("unmarshal stored value failed: %v", err)
}
if err := sonic.UnmarshalString(value, &inputObj); err != nil {
return false, fmt.Errorf("unmarshal input value failed: %v", err)
}
// 比较两个对象(需要根据实际类型调整)
return fmt.Sprintf("%v", storedObj) == fmt.Sprintf("%v", inputObj), nil
}
// DelSet 从集合中删除元素
func (r *RedisHelper) DelSet(key string, value string) error {
_, err := r.client.SRem(r.ctx, key, value).Result()
@ -609,7 +746,7 @@ func (e *RedisHelper) SignelAdd(key string, score float64, member string) error
_, err := e.client.ZRemRangeByScore(e.ctx, key, scoreStr, scoreStr).Result()
if err != nil {
fmt.Printf("删除score失败", err.Error())
fmt.Printf("删除score失败,err:%s", err.Error())
}
_, err = e.client.ZAdd(e.ctx, key, &redis.Z{
Score: score,
@ -642,6 +779,46 @@ func (e *RedisHelper) DelSortSet(key, member string) error {
return e.client.ZRem(e.ctx, key, member).Err()
}
// RemoveBeforeScore 移除 Sorted Set 中分数小于等于指定值的数据
// key: Sorted Set 的键
// score: 分数上限,所有小于等于此分数的元素将被移除
// 返回值: 移除的元素数量和可能的错误
func (e *RedisHelper) RemoveBeforeScore(key string, score float64) (int64, error) {
if key == "" {
return 0, errors.New("key 不能为空")
}
if math.IsNaN(score) || math.IsInf(score, 0) {
return 0, errors.New("score 必须是有效数字")
}
// 使用 ZRemRangeByScore 移除数据
count, err := e.client.ZRemRangeByScore(e.ctx, key, "-inf", strconv.FormatFloat(score, 'f', -1, 64)).Result()
if err != nil {
return 0, fmt.Errorf("移除 Sorted Set 数据失败, key: %s, score: %f, err: %v", key, score, err)
}
return count, nil
}
// GetNextAfterScore 获取指定分数及之后的第一条数据(包含指定分数)
func (e *RedisHelper) GetNextAfterScore(key string, score float64) (string, error) {
// 使用 ZRangeByScore 获取大于等于 score 的第一条数据
zs, err := e.client.ZRangeByScoreWithScores(e.ctx, key, &redis.ZRangeBy{
Min: fmt.Sprintf("%f", score), // 包含指定分数
Max: "+inf", // 上限为正无穷
Offset: 0, // 从第 0 条开始
Count: 1, // 只取 1 条
}).Result()
if err != nil {
return "", fmt.Errorf("获取数据失败: %v", err)
}
if len(zs) == 0 {
return "", nil // 没有符合条件的元素
}
return zs[0].Member.(string), nil
}
/*
获取sort set 所有数据
*/
@ -656,6 +833,22 @@ func (e *RedisHelper) GetRevRangeScoresSortSet(key string) ([]redis.Z, error) {
return e.client.ZRevRangeWithScores(e.ctx, key, 0, -1).Result()
}
// 获取最后一条数据
func (e *RedisHelper) GetLastSortSet(key string) ([]redis.Z, error) {
// 获取最后一个元素及其分数
results, err := e.client.ZRevRangeWithScores(e.ctx, key, 0, 0).Result()
if err != nil {
return nil, fmt.Errorf("failed to get last member: %w", err)
}
// 如果没有数据,返回空
if len(results) == 0 {
return []redis.Z{}, nil
}
return results, nil
}
// 获取指定区间数据
func (e *RedisHelper) GetSortSetMembers(key string, start, stop int64) ([]string, error) {
return e.client.ZRange(e.ctx, key, start, stop).Result()

View File

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

View File

@ -13,6 +13,7 @@ import (
"go-admin/services/futureservice"
"go-admin/services/scriptservice"
"os"
"time"
"github.com/bytedance/sonic"
"github.com/go-admin-team/go-admin-core/logger"
@ -29,11 +30,37 @@ func BusinessInit(db *gorm.DB) {
os.Exit(-1)
}
lineApiService := service.LineApiUser{}
lineApiService.Orm = db
lineApiService.Log = logger.NewHelper(sdk.Runtime.GetLogger()).WithFields(map[string]interface{}{})
if err := lineApiService.CacheRelation(); err != nil {
lineApiService.Log.Errorf("初始化api关系失败 err:%v", err)
os.Exit(-1)
}
reverseSettingService := service.LineReverseSetting{}
reverseSettingService.Orm = db
reverseSettingService.Log = lineApiService.Log
if err := reverseSettingService.SaveCache(models.LineReverseSetting{}); err != nil {
reverseSettingService.Log.Errorf("初始化反单设置失败 err:%v", err)
os.Exit(-1)
}
//初始化参数配置
cacheservice.InitConfigCache(db)
//初始化可缓存价格交易对
symbolPriceService := service.LineSymbolPrice{}
symbolPriceService.Orm = db
symbolPriceService.Log = logger.NewHelper(sdk.Runtime.GetLogger()).WithFields(map[string]interface{}{})
symbolPriceService.InitCache()
//清理交易对缓存价格
clearSymbolPrice()
//初始化订单配置
binanceservice.ResetSystemSetting(db)
cacheservice.ResetSystemSetting(db)
lineApiUser := service.LineApiUser{}
lineApiUser.Orm = db
if err := lineApiUser.InitCache(); err != nil {
@ -140,3 +167,24 @@ func loadApiUser(db *gorm.DB) error {
return nil
}
// 清理交易对价格缓存
func clearSymbolPrice() error {
spotAll, _ := helper.DefaultRedis.ScanKeys("spot_ticker_last_price:*")
futAllKey, _ := helper.DefaultRedis.ScanKeys("fut_ticker_last_price:*")
beforeTimeUtc := time.Now().UnixMilli()
for _, item := range spotAll {
if _, err := helper.DefaultRedis.RemoveBeforeScore(item, float64(beforeTimeUtc)); err != nil {
logger.Error("现货 清理交易对价格缓存失败:", err)
}
}
for _, item := range futAllKey {
if _, err := helper.DefaultRedis.RemoveBeforeScore(item, float64(beforeTimeUtc)); err != nil {
logger.Error("合约 清理交易对价格缓存失败:", err)
}
}
return nil
}

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

Binary file not shown.

3
go.mod
View File

@ -39,6 +39,7 @@ require (
github.com/shirou/gopsutil/v3 v3.23.10
github.com/shopspring/decimal v1.2.0
github.com/spf13/cobra v1.7.0
github.com/stretchr/testify v1.9.0
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
github.com/swaggo/swag v1.16.2
@ -82,6 +83,7 @@ require (
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
@ -143,6 +145,7 @@ require (
github.com/nsqio/go-nsq v1.1.0 // indirect
github.com/nyaruka/phonenumbers v1.0.55 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect

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:"-"` //推送时间
}
func (e *TradeSet) GetSymbol() string {
return e.Coin + e.Currency
}
//CommissionType int `db:"commissiontype"` //手续费:1买,2卖,3双向
//DepositNum float64 `db:"depositnum" json:"depositnum"` //保证金规模(手)
//ForceRate float64 `db:"forcerate" json:"forcerate"` //维持保证金率1%

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 {
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
}
// 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账号信息
*/
@ -640,3 +655,43 @@ func GetSpotUProperty(apiUserInfo DbModels.LineApiUser, data *dto.LineUserProper
return nil
}
// 万象划转
func TradeAmount(db *gorm.DB, req *binancedto.BinanceTransfer, apiUserInfo DbModels.LineApiUser) error {
url := "/sapi/v1/asset/transfer"
client := GetClient(&apiUserInfo)
params := map[string]string{
"type": req.Type,
"asset": req.Asset,
"amount": req.Amount.String(),
"fromSymbol": req.FromSymbol,
"toSymbol": req.ToSymbol,
"recvWindow": "10000",
}
_, code, err := client.SendSpotAuth(url, "POST", params)
if err != nil || code != 200 {
log.Error("万向划转失败 参数:", params)
log.Error("万向划转失败 code:", code)
log.Error("万向划转失败 err:", err)
dataMap := make(map[string]interface{})
if err.Error() != "" {
if err := sonic.Unmarshal([]byte(err.Error()), &dataMap); err != nil {
return fmt.Errorf("api_id:%d 万向划转失败:%+v", apiUserInfo.Id, err.Error())
}
}
code, ok := dataMap["code"]
if ok {
return fmt.Errorf("api_id:%d 万向划转失败:%s", apiUserInfo.Id, ErrorMaps[code.(float64)])
}
if strings.Contains(err.Error(), "Unknown order sent.") {
return fmt.Errorf("api_id:%d 万向划转失败:%+v", apiUserInfo.Id, ErrorMaps[-2011])
}
return fmt.Errorf("api_id:%d 万向划转失败:%+v", apiUserInfo.Id, err.Error())
}
return nil
}

View File

@ -16,7 +16,7 @@ import (
)
func TestCancelFutClosePosition(t *testing.T) {
dsn := "root:root@tcp(192.168.123.216:3306)/gp-bian?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
dsn := "root:123456@tcp(127.0.0.1:3306)/gp-bian?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
var apiUserInfo models.LineApiUser
db.Model(&models.LineApiUser{}).Where("id = 21").Find(&apiUserInfo)

View File

@ -1,14 +1,12 @@
package binanceservice
import (
"errors"
"fmt"
DbModels "go-admin/app/admin/models"
"go-admin/app/admin/service/dto"
"go-admin/common/const/rediskey"
"go-admin/common/global"
"go-admin/common/helper"
"go-admin/models"
"go-admin/models/positiondto"
"go-admin/pkg/utility"
"go-admin/services/positionservice"
@ -27,32 +25,6 @@ type AddPosition struct {
Db *gorm.DB
}
// 获取缓存交易对
// symbolType 0-现货 1-合约
func GetTradeSet(symbol string, symbolType int) (models.TradeSet, error) {
result := models.TradeSet{}
val := ""
switch symbolType {
case 0:
key := fmt.Sprintf(global.TICKER_SPOT, global.EXCHANGE_BINANCE, symbol)
val, _ = helper.DefaultRedis.GetString(key)
case 1:
key := fmt.Sprintf(global.TICKER_FUTURES, global.EXCHANGE_BINANCE, symbol)
val, _ = helper.DefaultRedis.GetString(key)
}
if val != "" {
if err := sonic.Unmarshal([]byte(val), &result); err != nil {
return result, err
}
} else {
return result, errors.New("未找到交易对信息")
}
return result, nil
}
func GetRisk(futApi *FutRestApi, apiInfo *DbModels.LineApiUser, symbol string) []PositionRisk {
for x := 0; x < 5; x++ {
risks, _ := futApi.GetPositionV3(apiInfo, symbol)
@ -279,11 +251,15 @@ func (e *AddPosition) CalculateAmount(req dto.ManuallyCover, totalNum, lastPrice
// mainOrderId 主单id
// symbolType 1现货 2合约
func MainClosePositionClearCache(mainId int, symbolType int) {
strategyDto := dto.StrategyOrderRedisList{}
if symbolType == 1 {
keySpotStop := fmt.Sprintf(rediskey.SpotStopLossList, global.EXCHANGE_BINANCE)
keySpotAddposition := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE)
spotStopArray, _ := helper.DefaultRedis.GetAllList(keySpotStop)
spotAddpositionArray, _ := helper.DefaultRedis.GetAllList(keySpotAddposition)
spotStrategyKey := fmt.Sprintf(rediskey.StrategySpotOrderList, global.EXCHANGE_BINANCE)
spotStrategyList, _ := helper.DefaultRedis.GetAllList(spotStrategyKey)
var position AddPositionList
var stop dto.StopLossRedisList
@ -307,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 {
keyFutStop := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE)
keyFutAddposition := fmt.Sprintf(rediskey.FuturesStopLossList, global.EXCHANGE_BINANCE)
futAddpositionArray, _ := helper.DefaultRedis.GetAllList(keyFutStop)
futStopArray, _ := helper.DefaultRedis.GetAllList(keyFutAddposition)
futStrategyKey := fmt.Sprintf(rediskey.StrategyFutOrderList, global.EXCHANGE_BINANCE)
futStrategyList, _ := helper.DefaultRedis.GetAllList(futStrategyKey)
var position AddPositionList
var stop dto.StopLossRedisList
@ -334,6 +322,16 @@ func MainClosePositionClearCache(mainId int, symbolType int) {
helper.DefaultRedis.LRem(keyFutStop, item)
}
}
for _, item := range futStrategyList {
sonic.Unmarshal([]byte(item), &strategyDto)
if strategyDto.Id == mainId {
if _, err := helper.DefaultRedis.LRem(futStrategyKey, item); err != nil {
logger.Errorf("id:%d 移除缓存失败,err:%v", mainId, err)
}
}
}
}
}
@ -481,3 +479,13 @@ func GetOpenOrderSns(db *gorm.DB, mainIds []int) ([]string, error) {
return result, nil
}
// 回去反单默认配置
func GetReverseSetting(db *gorm.DB) (DbModels.LineReverseSetting, error) {
var setting DbModels.LineReverseSetting
if err := db.Model(&DbModels.LineReverseSetting{}).First(&setting).Error; err != nil {
return setting, err
}
return setting, nil
}

View File

@ -0,0 +1,358 @@
package binanceservice
import (
"fmt"
"time"
"github.com/shopspring/decimal"
)
// OptimizedConfig 优化配置结构
type OptimizedConfig struct {
// 锁配置
LockConfig LockConfig `json:"lock_config"`
// 重试配置
RetryConfig RetryConfig `json:"retry_config"`
// 熔断器配置
CircuitBreakerConfig CircuitBreakerConfig `json:"circuit_breaker_config"`
// 同步配置
SyncConfig SyncConfig `json:"sync_config"`
// 性能配置
PerformanceConfig PerformanceConfig `json:"performance_config"`
// 监控配置
MonitorConfig MonitorConfig `json:"monitor_config"`
}
// LockConfig 锁配置
type LockConfig struct {
// Redis分布式锁超时时间
RedisLockTimeout time.Duration `json:"redis_lock_timeout"`
// 获取锁等待时间
LockWaitTimeout time.Duration `json:"lock_wait_timeout"`
// 订单处理锁前缀
OrderLockPrefix string `json:"order_lock_prefix"`
// 持仓更新锁前缀
PositionLockPrefix string `json:"position_lock_prefix"`
// 止盈止损锁前缀
TakeProfitLockPrefix string `json:"take_profit_lock_prefix"`
}
// CircuitBreakerConfig 熔断器配置
type CircuitBreakerConfig struct {
Enabled bool `json:"enabled"`
FailureThreshold int `json:"failure_threshold"`
SuccessThreshold int `json:"success_threshold"`
Timeout time.Duration `json:"timeout"`
HalfOpenMaxCalls int `json:"half_open_max_calls"`
}
// RetryConfig 重试配置
type RetryConfig struct {
// 最大重试次数
MaxRetries int `json:"max_retries"`
// 重试延迟
RetryDelay time.Duration `json:"retry_delay"`
// 指数退避因子
BackoffFactor float64 `json:"backoff_factor"`
// 最大重试延迟
MaxRetryDelay time.Duration `json:"max_retry_delay"`
// API调用重试次数
ApiRetryCount int `json:"api_retry_count"`
// 数据库操作重试次数
DbRetryCount int `json:"db_retry_count"`
}
// SyncConfig 同步配置
type SyncConfig struct {
// 持仓同步检查间隔
PositionSyncInterval time.Duration `json:"position_sync_interval"`
// 持仓差异阈值
PositionDiffThreshold decimal.Decimal `json:"position_diff_threshold"`
// 强制同步阈值
ForceSyncThreshold decimal.Decimal `json:"force_sync_threshold"`
// 同步超时时间
SyncTimeout time.Duration `json:"sync_timeout"`
// 是否启用自动同步
AutoSyncEnabled bool `json:"auto_sync_enabled"`
}
// PerformanceConfig 性能配置
type PerformanceConfig struct {
// 批量操作大小
BatchSize int `json:"batch_size"`
// 异步处理队列大小
AsyncQueueSize int `json:"async_queue_size"`
// 工作协程数量
WorkerCount int `json:"worker_count"`
// 数据库连接池大小
DbPoolSize int `json:"db_pool_size"`
// 缓存过期时间
CacheExpiration time.Duration `json:"cache_expiration"`
// 是否启用缓存
CacheEnabled bool `json:"cache_enabled"`
}
// MonitorConfig 监控配置
type MonitorConfig struct {
// 是否启用监控
Enabled bool `json:"enabled"`
// 监控数据收集间隔
CollectInterval time.Duration `json:"collect_interval"`
// 告警阈值配置
AlertThresholds AlertThresholds `json:"alert_thresholds"`
// 日志级别
LogLevel string `json:"log_level"`
// 是否启用性能分析
ProfileEnabled bool `json:"profile_enabled"`
}
// AlertThresholds 告警阈值配置
type AlertThresholds struct {
// 订单处理失败率阈值 (百分比)
OrderFailureRate float64 `json:"order_failure_rate"`
// 持仓同步失败率阈值 (百分比)
PositionSyncFailureRate float64 `json:"position_sync_failure_rate"`
// API调用失败率阈值 (百分比)
ApiFailureRate float64 `json:"api_failure_rate"`
// 响应时间阈值 (毫秒)
ResponseTimeThreshold time.Duration `json:"response_time_threshold"`
// 内存使用率阈值 (百分比)
MemoryUsageThreshold float64 `json:"memory_usage_threshold"`
// CPU使用率阈值 (百分比)
CpuUsageThreshold float64 `json:"cpu_usage_threshold"`
}
// GetDefaultOptimizedConfig 获取默认优化配置
func GetDefaultOptimizedConfig() *OptimizedConfig {
return &OptimizedConfig{
LockConfig: LockConfig{
RedisLockTimeout: 30 * time.Second,
LockWaitTimeout: 5 * time.Second,
OrderLockPrefix: "reverse_order_lock:",
PositionLockPrefix: "position_update_lock:",
TakeProfitLockPrefix: "take_profit_lock:",
},
RetryConfig: RetryConfig{
MaxRetries: 3,
RetryDelay: time.Second,
BackoffFactor: 2.0,
MaxRetryDelay: 10 * time.Second,
ApiRetryCount: 3,
DbRetryCount: 3,
},
CircuitBreakerConfig: CircuitBreakerConfig{
Enabled: true,
FailureThreshold: 5,
SuccessThreshold: 3,
Timeout: 60 * time.Second,
HalfOpenMaxCalls: 3,
},
SyncConfig: SyncConfig{
PositionSyncInterval: 30 * time.Second,
PositionDiffThreshold: decimal.NewFromFloat(0.001),
ForceSyncThreshold: decimal.NewFromFloat(0.01),
SyncTimeout: 10 * time.Second,
AutoSyncEnabled: true,
},
PerformanceConfig: PerformanceConfig{
BatchSize: 10,
AsyncQueueSize: 1000,
WorkerCount: 5,
DbPoolSize: 20,
CacheExpiration: 5 * time.Minute,
CacheEnabled: true,
},
MonitorConfig: MonitorConfig{
Enabled: true,
CollectInterval: time.Minute,
AlertThresholds: AlertThresholds{
OrderFailureRate: 5.0, // 5%
PositionSyncFailureRate: 2.0, // 2%
ApiFailureRate: 10.0, // 10%
ResponseTimeThreshold: 5 * time.Second,
MemoryUsageThreshold: 80.0, // 80%
CpuUsageThreshold: 70.0, // 70%
},
LogLevel: "info",
ProfileEnabled: false,
},
}
}
// ValidateConfig 验证配置有效性
func (c *OptimizedConfig) ValidateConfig() error {
// 验证锁配置
if c.LockConfig.RedisLockTimeout <= 0 {
return fmt.Errorf("Redis锁超时时间必须大于0")
}
if c.LockConfig.LockWaitTimeout <= 0 {
return fmt.Errorf("锁等待超时时间必须大于0")
}
// 验证重试配置
if c.RetryConfig.MaxRetries < 0 {
return fmt.Errorf("最大重试次数不能为负数")
}
if c.RetryConfig.RetryDelay <= 0 {
return fmt.Errorf("重试延迟必须大于0")
}
if c.RetryConfig.BackoffFactor <= 1.0 {
return fmt.Errorf("退避因子必须大于1.0")
}
// 验证同步配置
if c.SyncConfig.PositionSyncInterval <= 0 {
return fmt.Errorf("持仓同步间隔必须大于0")
}
if c.SyncConfig.PositionDiffThreshold.IsNegative() {
return fmt.Errorf("持仓差异阈值不能为负数")
}
// 验证性能配置
if c.PerformanceConfig.BatchSize <= 0 {
return fmt.Errorf("批量操作大小必须大于0")
}
if c.PerformanceConfig.WorkerCount <= 0 {
return fmt.Errorf("工作协程数量必须大于0")
}
if c.PerformanceConfig.DbPoolSize <= 0 {
return fmt.Errorf("数据库连接池大小必须大于0")
}
// 验证监控配置
if c.MonitorConfig.Enabled {
if c.MonitorConfig.CollectInterval <= 0 {
return fmt.Errorf("监控数据收集间隔必须大于0")
}
if c.MonitorConfig.AlertThresholds.OrderFailureRate < 0 || c.MonitorConfig.AlertThresholds.OrderFailureRate > 100 {
return fmt.Errorf("订单失败率阈值必须在0-100之间")
}
if c.MonitorConfig.AlertThresholds.MemoryUsageThreshold < 0 || c.MonitorConfig.AlertThresholds.MemoryUsageThreshold > 100 {
return fmt.Errorf("内存使用率阈值必须在0-100之间")
}
if c.MonitorConfig.AlertThresholds.CpuUsageThreshold < 0 || c.MonitorConfig.AlertThresholds.CpuUsageThreshold > 100 {
return fmt.Errorf("CPU使用率阈值必须在0-100之间")
}
}
return nil
}
// GetLockKey 生成锁键名
func (c *OptimizedConfig) GetLockKey(lockType, identifier string) string {
switch lockType {
case "order":
return c.LockConfig.OrderLockPrefix + identifier
case "position":
return c.LockConfig.PositionLockPrefix + identifier
case "take_profit":
return c.LockConfig.TakeProfitLockPrefix + identifier
default:
return "unknown_lock:" + identifier
}
}
// IsRetryableError 判断错误是否可重试
func (c *OptimizedConfig) IsRetryableError(err error) bool {
if err == nil {
return false
}
errStr := err.Error()
// 定义不可重试的错误类型
nonRetryableErrors := []string{
"余额不足",
"订单重复",
"API-key",
"无效",
"权限",
"签名",
"参数错误",
}
for _, nonRetryable := range nonRetryableErrors {
if contains(errStr, []string{nonRetryable}) {
return false
}
}
return true
}
// GetRetryDelay 计算重试延迟时间
func (c *OptimizedConfig) GetRetryDelay(attempt int) time.Duration {
if attempt <= 0 {
return c.RetryConfig.RetryDelay
}
// 指数退避算法
delay := c.RetryConfig.RetryDelay
for i := 0; i < attempt; i++ {
delay = time.Duration(float64(delay) * c.RetryConfig.BackoffFactor)
if delay > c.RetryConfig.MaxRetryDelay {
return c.RetryConfig.MaxRetryDelay
}
}
return delay
}
// ShouldSync 判断是否需要同步持仓
func (c *OptimizedConfig) ShouldSync(exchangePosition, systemPosition decimal.Decimal) bool {
diff := exchangePosition.Sub(systemPosition).Abs()
return diff.GreaterThan(c.SyncConfig.PositionDiffThreshold)
}
// ShouldForceSync 判断是否需要强制同步
func (c *OptimizedConfig) ShouldForceSync(exchangePosition, systemPosition decimal.Decimal) bool {
diff := exchangePosition.Sub(systemPosition).Abs()
return diff.GreaterThan(c.SyncConfig.ForceSyncThreshold)
}
// 全局配置实例
var GlobalOptimizedConfig *OptimizedConfig
// InitOptimizedConfig 初始化优化配置
func InitOptimizedConfig() error {
GlobalOptimizedConfig = GetDefaultOptimizedConfig()
return GlobalOptimizedConfig.ValidateConfig()
}
// GetOptimizedConfig 获取全局优化配置
func GetOptimizedConfig() *OptimizedConfig {
if GlobalOptimizedConfig == nil {
InitOptimizedConfig()
}
return GlobalOptimizedConfig
}

View File

@ -0,0 +1,676 @@
package binanceservice
import (
"context"
"errors"
"fmt"
"net"
"strings"
"time"
"github.com/go-admin-team/go-admin-core/logger"
"gorm.io/gorm"
)
// ErrorType 错误类型
type ErrorType string
const (
ErrorTypeNetwork ErrorType = "network"
ErrorTypeDatabase ErrorType = "database"
ErrorTypeBusiness ErrorType = "business"
ErrorTypeSystem ErrorType = "system"
ErrorTypeValidation ErrorType = "validation"
ErrorTypeTimeout ErrorType = "timeout"
ErrorTypeRateLimit ErrorType = "rate_limit"
ErrorTypeAuth ErrorType = "auth"
)
// ErrorSeverity 错误严重程度
type ErrorSeverity string
const (
SeverityLow ErrorSeverity = "low"
SeverityMedium ErrorSeverity = "medium"
SeverityHigh ErrorSeverity = "high"
SeverityCritical ErrorSeverity = "critical"
)
// ErrorAction 错误处理动作
type ErrorAction string
const (
ActionRetry ErrorAction = "retry"
ActionFallback ErrorAction = "fallback"
ActionAlert ErrorAction = "alert"
ActionIgnore ErrorAction = "ignore"
ActionCircuit ErrorAction = "circuit"
ActionDegrade ErrorAction = "degrade"
)
// ErrorInfo 错误信息
type ErrorInfo struct {
Type ErrorType `json:"type"`
Severity ErrorSeverity `json:"severity"`
Message string `json:"message"`
OriginalErr error `json:"-"`
Context string `json:"context"`
Timestamp time.Time `json:"timestamp"`
Retryable bool `json:"retryable"`
Action ErrorAction `json:"action"`
}
// RetryConfig 和 CircuitBreakerConfig 已在 config_optimized.go 中定义
// ErrorHandler 错误处理器
type ErrorHandler struct {
config *OptimizedConfig
metricsCollector *MetricsCollector
circuitBreakers map[string]*CircuitBreaker
}
// CircuitBreaker 熔断器
type CircuitBreaker struct {
name string
config CircuitBreakerConfig
state CircuitState
failureCount int
successCount int
lastFailTime time.Time
nextRetry time.Time
halfOpenCalls int
}
// CircuitState 熔断器状态
type CircuitState string
const (
StateClosed CircuitState = "closed"
StateOpen CircuitState = "open"
StateHalfOpen CircuitState = "half_open"
)
// NewErrorHandler 创建错误处理器
func NewErrorHandler(config *OptimizedConfig, metricsCollector *MetricsCollector) *ErrorHandler {
return &ErrorHandler{
config: config,
metricsCollector: metricsCollector,
circuitBreakers: make(map[string]*CircuitBreaker),
}
}
// ClassifyError 分类错误
func (eh *ErrorHandler) ClassifyError(err error, context string) *ErrorInfo {
if err == nil {
return nil
}
errorInfo := &ErrorInfo{
OriginalErr: err,
Message: err.Error(),
Context: context,
Timestamp: time.Now(),
}
// 根据错误类型分类
switch {
case isNetworkError(err):
errorInfo.Type = ErrorTypeNetwork
errorInfo.Severity = SeverityMedium
errorInfo.Retryable = true
errorInfo.Action = ActionRetry
case isDatabaseError(err):
errorInfo.Type = ErrorTypeDatabase
errorInfo.Severity = SeverityHigh
errorInfo.Retryable = isRetryableDatabaseError(err)
errorInfo.Action = ActionRetry
case isTimeoutError(err):
errorInfo.Type = ErrorTypeTimeout
errorInfo.Severity = SeverityMedium
errorInfo.Retryable = true
errorInfo.Action = ActionRetry
case isRateLimitError(err):
errorInfo.Type = ErrorTypeRateLimit
errorInfo.Severity = SeverityMedium
errorInfo.Retryable = true
errorInfo.Action = ActionRetry
case isAuthError(err):
errorInfo.Type = ErrorTypeAuth
errorInfo.Severity = SeverityHigh
errorInfo.Retryable = false
errorInfo.Action = ActionAlert
case isValidationError(err):
errorInfo.Type = ErrorTypeValidation
errorInfo.Severity = SeverityLow
errorInfo.Retryable = false
errorInfo.Action = ActionIgnore
case isBusinessError(err):
errorInfo.Type = ErrorTypeBusiness
errorInfo.Severity = SeverityMedium
errorInfo.Retryable = false
errorInfo.Action = ActionFallback
default:
errorInfo.Type = ErrorTypeSystem
errorInfo.Severity = SeverityHigh
errorInfo.Retryable = true
errorInfo.Action = ActionRetry
}
return errorInfo
}
// HandleError 处理错误
func (eh *ErrorHandler) HandleError(err error, context string) *ErrorInfo {
errorInfo := eh.ClassifyError(err, context)
if errorInfo == nil {
return nil
}
// 记录错误指标
if eh.metricsCollector != nil {
eh.metricsCollector.RecordError(string(errorInfo.Type), errorInfo.Message, context)
}
// 根据错误动作处理
switch errorInfo.Action {
case ActionAlert:
eh.sendAlert(errorInfo)
case ActionCircuit:
eh.triggerCircuitBreaker(context)
case ActionDegrade:
eh.enableDegradedMode(context)
}
// 记录日志
eh.logError(errorInfo)
return errorInfo
}
// RetryWithBackoff 带退避的重试
func (eh *ErrorHandler) RetryWithBackoff(ctx context.Context, operation func() error, config RetryConfig, context string) error {
var lastErr error
delay := config.RetryDelay
for attempt := 1; attempt <= config.MaxRetries; attempt++ {
// 检查上下文是否已取消
select {
case <-ctx.Done():
return ctx.Err()
default:
}
// 检查熔断器状态
if !eh.canExecute(context) {
return fmt.Errorf("circuit breaker is open for %s", context)
}
// 执行操作
// 注意config中没有TimeoutPerTry字段这里暂时注释掉
// if config.TimeoutPerTry > 0 {
// var cancel context.CancelFunc
// operationCtx, cancel = context.WithTimeout(ctx, config.TimeoutPerTry)
// defer cancel()
// }
err := operation()
if err == nil {
// 成功,记录成功指标
if eh.metricsCollector != nil {
eh.metricsCollector.RecordRetry(true)
}
eh.recordSuccess(context)
return nil
}
lastErr = err
errorInfo := eh.ClassifyError(err, context)
// 记录失败
eh.recordFailure(context)
// 如果不可重试,直接返回
if errorInfo != nil && !errorInfo.Retryable {
logger.Warnf("不可重试的错误 [%s]: %v", context, err)
return err
}
// 最后一次尝试,不再等待
if attempt == config.MaxRetries {
break
}
// 计算下次重试延迟
nextDelay := eh.calculateDelay(delay, config)
logger.Infof("重试 %d/%d [%s] 在 %v 后,错误: %v", attempt, config.MaxRetries, context, nextDelay, err)
// 等待重试
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(nextDelay):
delay = nextDelay
}
}
// 记录重试失败指标
if eh.metricsCollector != nil {
eh.metricsCollector.RecordRetry(false)
}
return fmt.Errorf("重试 %d 次后仍然失败 [%s]: %w", config.MaxRetries, context, lastErr)
}
// calculateDelay 计算重试延迟
func (eh *ErrorHandler) calculateDelay(currentDelay time.Duration, config RetryConfig) time.Duration {
nextDelay := time.Duration(float64(currentDelay) * config.BackoffFactor)
// 限制最大延迟
if nextDelay > config.MaxRetryDelay {
nextDelay = config.MaxRetryDelay
}
// 添加10%抖动简化版本不依赖JitterEnabled字段
jitter := time.Duration(float64(nextDelay) * 0.1)
if jitter > 0 {
nextDelay += time.Duration(time.Now().UnixNano() % int64(jitter))
}
return nextDelay
}
// canExecute 检查是否可以执行操作(熔断器检查)
func (eh *ErrorHandler) canExecute(context string) bool {
cb := eh.getCircuitBreaker(context)
if cb == nil || !cb.config.Enabled {
return true
}
now := time.Now()
switch cb.state {
case StateClosed:
return true
case StateOpen:
if now.After(cb.nextRetry) {
cb.state = StateHalfOpen
cb.halfOpenCalls = 0
return true
}
return false
case StateHalfOpen:
return cb.halfOpenCalls < cb.config.HalfOpenMaxCalls
default:
return true
}
}
// recordSuccess 记录成功
func (eh *ErrorHandler) recordSuccess(context string) {
cb := eh.getCircuitBreaker(context)
if cb == nil || !cb.config.Enabled {
return
}
switch cb.state {
case StateHalfOpen:
cb.successCount++
if cb.successCount >= cb.config.SuccessThreshold {
cb.state = StateClosed
cb.failureCount = 0
cb.successCount = 0
}
case StateClosed:
cb.failureCount = 0
}
}
// recordFailure 记录失败
func (eh *ErrorHandler) recordFailure(context string) {
cb := eh.getCircuitBreaker(context)
if cb == nil || !cb.config.Enabled {
return
}
cb.failureCount++
cb.lastFailTime = time.Now()
switch cb.state {
case StateClosed:
if cb.failureCount >= cb.config.FailureThreshold {
cb.state = StateOpen
cb.nextRetry = time.Now().Add(cb.config.Timeout)
logger.Warnf("熔断器开启 [%s]: 失败次数 %d", context, cb.failureCount)
}
case StateHalfOpen:
cb.state = StateOpen
cb.nextRetry = time.Now().Add(cb.config.Timeout)
cb.successCount = 0
logger.Warnf("熔断器重新开启 [%s]", context)
}
}
// getCircuitBreaker 获取熔断器
func (eh *ErrorHandler) getCircuitBreaker(context string) *CircuitBreaker {
cb, exists := eh.circuitBreakers[context]
if !exists {
cb = &CircuitBreaker{
name: context,
config: eh.config.CircuitBreakerConfig,
state: StateClosed,
}
eh.circuitBreakers[context] = cb
}
return cb
}
// triggerCircuitBreaker 触发熔断器
func (eh *ErrorHandler) triggerCircuitBreaker(context string) {
cb := eh.getCircuitBreaker(context)
if cb != nil && cb.config.Enabled {
cb.state = StateOpen
cb.nextRetry = time.Now().Add(cb.config.Timeout)
logger.Warnf("手动触发熔断器 [%s]", context)
}
}
// sendAlert 发送告警
func (eh *ErrorHandler) sendAlert(errorInfo *ErrorInfo) {
// 这里可以集成告警系统,如钉钉、邮件等
logger.Errorf("告警: [%s] %s - %s", errorInfo.Type, errorInfo.Context, errorInfo.Message)
}
// enableDegradedMode 启用降级模式
func (eh *ErrorHandler) enableDegradedMode(context string) {
logger.Warnf("启用降级模式 [%s]", context)
// 这里可以实现具体的降级逻辑
}
// logError 记录错误日志
func (eh *ErrorHandler) logError(errorInfo *ErrorInfo) {
switch errorInfo.Severity {
case SeverityLow:
logger.Infof("[%s] %s: %s", errorInfo.Type, errorInfo.Context, errorInfo.Message)
case SeverityMedium:
logger.Warnf("[%s] %s: %s", errorInfo.Type, errorInfo.Context, errorInfo.Message)
case SeverityHigh, SeverityCritical:
logger.Errorf("[%s] %s: %s", errorInfo.Type, errorInfo.Context, errorInfo.Message)
}
}
// GetCircuitBreakerStatus 获取熔断器状态
func (eh *ErrorHandler) GetCircuitBreakerStatus() map[string]interface{} {
status := make(map[string]interface{})
for name, cb := range eh.circuitBreakers {
status[name] = map[string]interface{}{
"state": cb.state,
"failure_count": cb.failureCount,
"success_count": cb.successCount,
"last_fail_time": cb.lastFailTime,
"next_retry": cb.nextRetry,
}
}
return status
}
// 错误分类辅助函数
// isNetworkError 判断是否为网络错误
func isNetworkError(err error) bool {
if err == nil {
return false
}
// 检查网络相关错误
var netErr net.Error
if errors.As(err, &netErr) {
return true
}
// 检查常见网络错误消息
errorMsg := strings.ToLower(err.Error())
networkKeywords := []string{
"connection refused",
"connection reset",
"connection timeout",
"network unreachable",
"no route to host",
"dns",
"socket",
}
for _, keyword := range networkKeywords {
if strings.Contains(errorMsg, keyword) {
return true
}
}
return false
}
// isDatabaseError 判断是否为数据库错误
func isDatabaseError(err error) bool {
if err == nil {
return false
}
// 检查GORM错误
if errors.Is(err, gorm.ErrRecordNotFound) ||
errors.Is(err, gorm.ErrInvalidTransaction) ||
errors.Is(err, gorm.ErrNotImplemented) {
return true
}
// 检查数据库相关错误消息
errorMsg := strings.ToLower(err.Error())
dbKeywords := []string{
"database",
"sql",
"mysql",
"postgres",
"connection pool",
"deadlock",
"constraint",
"duplicate key",
}
for _, keyword := range dbKeywords {
if strings.Contains(errorMsg, keyword) {
return true
}
}
return false
}
// isRetryableDatabaseError 判断是否为可重试的数据库错误
func isRetryableDatabaseError(err error) bool {
if err == nil {
return false
}
// 不可重试的错误
if errors.Is(err, gorm.ErrRecordNotFound) {
return false
}
errorMsg := strings.ToLower(err.Error())
nonRetryableKeywords := []string{
"constraint",
"duplicate key",
"foreign key",
"syntax error",
}
for _, keyword := range nonRetryableKeywords {
if strings.Contains(errorMsg, keyword) {
return false
}
}
return true
}
// isTimeoutError 判断是否为超时错误
func isTimeoutError(err error) bool {
if err == nil {
return false
}
// 检查超时错误
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
return true
}
if errors.Is(err, context.DeadlineExceeded) {
return true
}
errorMsg := strings.ToLower(err.Error())
return strings.Contains(errorMsg, "timeout") ||
strings.Contains(errorMsg, "deadline exceeded")
}
// isRateLimitError 判断是否为限流错误
func isRateLimitError(err error) bool {
if err == nil {
return false
}
errorMsg := strings.ToLower(err.Error())
rateLimitKeywords := []string{
"rate limit",
"too many requests",
"429",
"quota exceeded",
"throttle",
}
for _, keyword := range rateLimitKeywords {
if strings.Contains(errorMsg, keyword) {
return true
}
}
return false
}
// isAuthError 判断是否为认证错误
func isAuthError(err error) bool {
if err == nil {
return false
}
errorMsg := strings.ToLower(err.Error())
authKeywords := []string{
"unauthorized",
"authentication",
"invalid signature",
"api key",
"401",
"403",
"forbidden",
}
for _, keyword := range authKeywords {
if strings.Contains(errorMsg, keyword) {
return true
}
}
return false
}
// isValidationError 判断是否为验证错误
func isValidationError(err error) bool {
if err == nil {
return false
}
errorMsg := strings.ToLower(err.Error())
validationKeywords := []string{
"validation",
"invalid parameter",
"bad request",
"400",
"missing required",
"invalid format",
}
for _, keyword := range validationKeywords {
if strings.Contains(errorMsg, keyword) {
return true
}
}
return false
}
// isBusinessError 判断是否为业务错误
func isBusinessError(err error) bool {
if err == nil {
return false
}
errorMsg := strings.ToLower(err.Error())
businessKeywords := []string{
"insufficient balance",
"order not found",
"position not found",
"invalid order status",
"market closed",
"symbol not found",
}
for _, keyword := range businessKeywords {
if strings.Contains(errorMsg, keyword) {
return true
}
}
return false
}
// 全局错误处理器实例
var GlobalErrorHandler *ErrorHandler
// InitErrorHandler 初始化错误处理器
func InitErrorHandler(config *OptimizedConfig, metricsCollector *MetricsCollector) {
GlobalErrorHandler = NewErrorHandler(config, metricsCollector)
}
// GetErrorHandler 获取全局错误处理器
func GetErrorHandler() *ErrorHandler {
return GlobalErrorHandler
}
// HandleErrorWithRetry 处理错误并重试的便捷函数
func HandleErrorWithRetry(ctx context.Context, operation func() error, context string) error {
if GlobalErrorHandler == nil {
return operation()
}
config := RetryConfig{
MaxRetries: 3,
RetryDelay: 100 * time.Millisecond,
MaxRetryDelay: 5 * time.Second,
BackoffFactor: 2.0,
ApiRetryCount: 3,
DbRetryCount: 3,
}
return GlobalErrorHandler.RetryWithBackoff(ctx, operation, config, context)
}

View File

@ -0,0 +1,64 @@
package binanceservice
import (
"fmt"
"go-admin/common/const/rediskey"
"go-admin/common/global"
"go-admin/common/helper"
"go-admin/models"
"go-admin/services/cacheservice"
"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 TestFutureJudge(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)
helper.InitLockRedisConn("127.0.0.1:6379", "", "2")
// tradeSet := models.TradeSet{
// Coin: "ADA",
// Currency: "USDT",
// LastPrice: "0.516",
// }
key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE)
item := `{"id":10,"apiId":49,"mainId":7,"pid":7,"symbol":"ADAUSDT","price":"0.6244","side":"BUY","num":"12","orderSn":"398690240274890752"}`
reduceOrder := ReduceListItem{}
futApi := FutRestApi{}
setting, err := cacheservice.GetSystemSetting(db)
if err != nil {
logger.Error("获取系统设置失败")
return
}
if err := sonic.Unmarshal([]byte(item), &reduceOrder); err != nil {
logger.Error("反序列化失败")
return
}
// JudgeFuturesReduce(tradeSet)
FuturesReduceTrigger(db, reduceOrder, futApi, setting, key, item, false, 0)
}
// 测试减仓后减仓触发
func TestFutureReduceReduce(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")
tradeSet := models.TradeSet{
Coin: "ADA",
Currency: "USDT",
LastPrice: "0.5817",
PriceDigit: 4,
}
// JudgeFuturesReduce(tradeSet)
JudgeFuturesReduce(tradeSet)
}

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

View File

@ -10,6 +10,8 @@ import (
"go-admin/common/global"
"go-admin/common/helper"
"go-admin/models"
"go-admin/pkg/utility"
"go-admin/services/cacheservice"
"strings"
"time"
@ -151,21 +153,92 @@ func JudgeFuturesReduce(trade models.TradeSet) {
key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE)
reduceVal, _ := helper.DefaultRedis.GetAllList(key)
if len(reduceVal) == 0 {
return
}
db := GetDBConnection()
futApi := FutRestApi{}
setting, err := GetSystemSetting(db)
setting, err := cacheservice.GetSystemSetting(db)
if err != nil {
log.Error("获取系统设置失败")
return
}
reduceOrder := ReduceListItem{}
tradePrice, _ := decimal.NewFromString(trade.LastPrice)
//减仓单减仓策略
reduceReduceListKey := fmt.Sprintf(rediskey.FutOrderReduceStrategyList, global.EXCHANGE_BINANCE)
orderReduceVal, _ := helper.DefaultRedis.HGetAllFields(reduceReduceListKey)
reduceOrderStrategy := dto.LineOrderReduceStrategyResp{}
for _, item := range orderReduceVal {
sonic.Unmarshal([]byte(item), &reduceOrderStrategy)
for index, item2 := range reduceOrderStrategy.Items {
if reduceOrderStrategy.Symbol == trade.Coin+trade.Currency && !item2.Actived {
//买入
if item2.TriggerPrice.Cmp(decimal.Zero) > 0 &&
tradePrice.Cmp(decimal.Zero) > 0 &&
((strings.ToUpper(reduceOrderStrategy.Side) == "SELL" && item2.TriggerPrice.Cmp(tradePrice) >= 0) ||
(strings.ToUpper(reduceOrderStrategy.Side) == "BUY" && item2.TriggerPrice.Cmp(tradePrice) <= 0)) {
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.ReduceStrategyFutTriggerLock, reduceOrder.ApiId, reduceOrder.Symbol), 50, 15, 100*time.Millisecond)
if ok, err := lock.AcquireWait(context.Background()); err != nil {
log.Error("获取锁失败", err)
return
} else if ok {
defer lock.Release()
hasrecord, _ := helper.DefaultRedis.HExists(reduceReduceListKey, utility.IntToString(reduceOrderStrategy.OrderId), item)
if !hasrecord {
log.Debug("减仓缓存中不存在", item)
return
}
order, err := CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit)
if err != nil {
log.Errorf("%d 生成订单失败", reduceOrderStrategy.OrderId)
}
reduceOrder.ApiId = order.ApiId
reduceOrder.Id = order.Id
reduceOrder.Pid = order.Pid
reduceOrder.MainId = order.MainId
reduceOrder.Symbol = order.Symbol
reduceOrder.Side = reduceOrderStrategy.Side
reduceOrder.OrderSn = order.OrderSn
reduceOrder.Price = item2.Price
reduceOrder.Num = item2.Num
//下单成功修改策略节点状态
if FuturesReduceTrigger(db, reduceOrder, futApi, setting, reduceReduceListKey, item, true, reduceOrderStrategy.OrderId) {
reduceOrderStrategy.Items[index].Actived = true
allActive := true
orderId := utility.IntToString(reduceOrderStrategy.OrderId)
for _, item3 := range reduceOrderStrategy.Items {
if !item3.Actived {
allActive = false
break
}
}
if allActive {
if err := helper.DefaultRedis.HDelField(reduceReduceListKey, orderId); err != nil {
log.Errorf("删除redis reduceReduceListKey失败 %s", err.Error())
}
} else {
str, _ := sonic.MarshalString(reduceOrderStrategy)
if err := helper.DefaultRedis.HSetField(reduceReduceListKey, orderId, str); err != nil {
log.Errorf("更新redis reduceReduceListKey失败 %s", err.Error())
}
}
}
}
}
}
}
}
for _, item := range reduceVal {
reduceOrder := ReduceListItem{}
if err := sonic.Unmarshal([]byte(item), &reduceOrder); err != nil {
log.Error("反序列化失败")
continue
@ -173,59 +246,68 @@ func JudgeFuturesReduce(trade models.TradeSet) {
if reduceOrder.Symbol == trade.Coin+trade.Currency {
orderPrice := reduceOrder.Price
tradePrice, _ := decimal.NewFromString(trade.LastPrice)
//买入
if orderPrice.Cmp(decimal.Zero) > 0 &&
tradePrice.Cmp(decimal.Zero) > 0 &&
((strings.ToUpper(reduceOrder.Side) == "SELL" && orderPrice.Cmp(tradePrice) >= 0) ||
(strings.ToUpper(reduceOrder.Side) == "BUY" && orderPrice.Cmp(tradePrice) <= 0)) {
FuturesReduceTrigger(db, reduceOrder, futApi, setting, key, item)
FuturesReduceTrigger(db, reduceOrder, futApi, setting, key, item, false, 0)
}
}
}
}
// 触发合约减仓
func FuturesReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, futApi FutRestApi, setting DbModels.LineSystemSetting, key, item string) {
tradeSet, _ := GetTradeSet(reduceOrder.Symbol, 1)
// isStrategy 是否是策略减仓
// reduceId 父减仓单id
func FuturesReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, futApi FutRestApi, setting DbModels.LineSystemSetting, key, item string, isStrategy bool, reduceId int) bool {
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reduceOrder.Symbol, 1)
result := true
if tradeSet.LastPrice == "" {
return
return false
}
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.FutTrigger, reduceOrder.ApiId, reduceOrder.Symbol), 20, 5, 100*time.Millisecond)
if ok, err := lock.AcquireWait(context.Background()); err != nil {
log.Error("获取锁失败", err)
return
return false
} else if ok {
defer lock.Release()
takeOrders := make([]DbModels.LinePreOrder, 0)
if err := db.Model(&DbModels.LinePreOrder{}).Where("main_id =? AND order_type =1 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("查询止盈单失败")
return
// return false
}
hasrecord, _ := helper.DefaultRedis.IsElementInList(key, item)
var hasrecord bool
if isStrategy {
hasrecord, _ = helper.DefaultRedis.HExists(key, utility.IntToString(reduceId), item)
} else {
hasrecord, _ = helper.DefaultRedis.IsElementInList(key, item)
}
if !hasrecord {
log.Debug("减仓缓存中不存在", item)
return
return false
}
apiInfo, _ := GetApiInfo(reduceOrder.ApiId)
if apiInfo.Id == 0 {
log.Error("现货减仓 查询api用户不存在")
return
return false
}
for _, takeOrder := range takeOrders {
err := CancelFutOrderByOrderSnLoop(apiInfo, takeOrder.Symbol, takeOrder.OrderSn)
if err != nil {
log.Error("合约止盈撤单失败", err)
return
log.Error("撤单失败", err)
return false
}
}
@ -239,18 +321,8 @@ func FuturesReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, futApi FutRes
positionSide = "LONG"
}
// params := FutOrderPlace{
// ApiId: reduceOrder.ApiId,
// Side: reduceOrder.Side,
// OrderType: "STOP",
// Symbol: reduceOrder.Symbol,
// Price: price,
// StopPrice: price,
// Quantity: num,
// NewClientOrderId: reduceOrder.OrderSn,
// }
if err := futApi.ClosePositionLoop(reduceOrder.Symbol, reduceOrder.OrderSn, num, reduceOrder.Side, positionSide, apiInfo, "LIMIT", "0", price, 3); err != nil {
result = false
log.Errorf("合约减仓挂单失败 id%s err:%v", reduceOrder.Id, err)
if err2 := db.Model(&DbModels.LinePreOrder{}).
@ -267,14 +339,24 @@ func FuturesReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, futApi FutRes
Where("id = ? AND status =0", reduceOrder.Id).Updates(map[string]interface{}{"status": 1}).Error; err != nil {
log.Errorf("合约减仓更新状态失败 id%s err:%v", reduceOrder.Id, err)
}
//处理减仓单减仓策略
if err := CacheOrderStrategyAndReCreate(db, reduceOrder, 2, tradeSet, setting); err != nil {
log.Errorf("合约减仓策略处理失败 id%s err:%v", reduceOrder.Id, err)
}
}
if _, err := helper.DefaultRedis.LRem(key, item); err != nil {
log.Errorf("合约减仓 删除缓存失败 id:%v err:%v", reduceOrder.Id, err)
if !isStrategy {
if _, err := helper.DefaultRedis.LRem(key, item); err != nil {
log.Errorf("合约减仓 删除缓存失败 id:%v err:%v", reduceOrder.Id, err)
}
}
} else {
log.Error("获取锁失败")
result = false
}
return result
}
// 判断合约加仓
@ -322,8 +404,8 @@ func FutAddPositionTrigger(db *gorm.DB, v *AddPositionList, item string, futApi
} else if ok {
defer lock.Release()
setting, _ := GetSystemSetting(db)
tradeSet, _ := GetTradeSet(v.Symbol, 1)
setting, _ := cacheservice.GetSystemSetting(db)
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, v.Symbol, 1)
if tradeSet.LastPrice == "" {
log.Errorf("合约加仓触发 查询交易对失败 交易对:%s ordersn:%s", v.Symbol, v.OrderSn)

View File

@ -13,6 +13,7 @@ import (
models2 "go-admin/models"
"go-admin/models/positiondto"
"go-admin/pkg/utility"
"go-admin/services/cacheservice"
"strings"
"time"
@ -26,7 +27,7 @@ import (
/*
修改订单信息
*/
func ChangeFutureOrder(mapData map[string]interface{}) {
func ChangeFutureOrder(mapData map[string]interface{}, apiKey string) {
// 检查订单号是否存在
orderSn, ok := mapData["c"]
originOrderSn := mapData["C"] //取消操作 代表原始订单号
@ -60,34 +61,15 @@ func ChangeFutureOrder(mapData map[string]interface{}) {
}
defer lock.Release()
// 查询订单
preOrder, err := getPreOrder(db, orderSn)
//反单逻辑
reverseService := ReverseService{}
reverseService.Orm = db
reverseService.Log = logger.NewHelper(logger.DefaultLogger)
_, err = reverseService.ReverseOrder(apiKey, mapData)
if err != nil {
logger.Error("合约订单回调失败,查询订单失败:", orderSn, " err:", err)
return
logger.Errorf("合约订单回调失败,单失败:%v", err)
}
// 解析订单状态
status, ok := mapData["X"].(string)
if !ok {
mapStr, _ := sonic.Marshal(&mapData)
logger.Error("订单回调失败,没有状态:", string(mapStr))
return
}
// 更新订单状态
orderStatus, reason := parseOrderStatus(status, mapData)
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)
}
// 合约回调
@ -126,17 +108,26 @@ func handleReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
return
}
tradeSet, err := GetTradeSet(preOrder.Symbol, 1)
tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 1)
if err != nil {
logger.Errorf("handleReduceFilled 获取交易对设置失败,订单号:%s", preOrder.OrderSn)
return
}
//修改减仓单减仓策略状态
ReduceCallBack(db, preOrder)
orderExt := models.LinePreOrderExt{}
db.Model(&orderExt).Where("order_id =?", preOrder.Id).First(&orderExt)
//减仓策略单获取主减仓单的拓展信息
if preOrder.ReduceOrderId > 0 {
db.Model(&orderExt).Where("order_id =?", preOrder.ReduceOrderId).First(&orderExt)
} else {
db.Model(&orderExt).Where("order_id =?", preOrder.Id).First(&orderExt)
}
// rate := utility.StringAsFloat(orderExt.AddPositionVal)
//取消委托中的止盈止损
cancelTakeProfitByReduce(db, apiUserInfo, preOrder.Symbol, preOrder.MainId, preOrder.SymbolType)
// 100%减仓 终止流程
if orderExt.AddPositionVal.Cmp(decimal.NewFromInt(100)) >= 0 {
//缓存
@ -160,9 +151,9 @@ func handleReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
positionData := savePosition(db, preOrder)
//市价单就跳出 市价减仓不设止盈止损
if preOrder.MainOrderType == "MARKET" {
return
}
// if preOrder.MainOrderType == "MARKET" {
// return
// }
//亏损大于0 重新计算比例
FutTakeProfit(db, preOrder, apiUserInfo, tradeSet, positionData, orderExt, decimal.Zero, decimal.Zero)
@ -171,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,
positionData positiondto.PositionDto, orderExt DbModels.LinePreOrderExt, manualTakeRatio, manualStopRatio decimal.Decimal) bool {
@ -245,7 +266,7 @@ func nextFuturesReduceTrigger(db *gorm.DB, mainId int, totalNum decimal.Decimal,
nextOrder := DbModels.LinePreOrder{}
nextExt := DbModels.LinePreOrderExt{}
if err := db.Model(&models.LinePreOrder{}).Where("main_id =? AND order_type =4 AND status=0", mainId).Order("rate asc").First(&nextOrder).Error; err != nil {
if err := db.Model(&models.LinePreOrder{}).Where("main_id =? AND order_type =4 AND status=0 AND reduce_order_id=0", mainId).Order("rate asc").First(&nextOrder).Error; err != nil {
logger.Errorf("获取下一个单失败 err:%v", err)
return
}
@ -432,6 +453,8 @@ func removeFutLossAndAddPosition(mainId int, orderSn string) {
stoploss := dto.StopLossRedisList{}
addPosition := AddPositionList{}
reduce := ReduceListItem{}
//移除减仓后减仓策略
RemoveReduceReduceCacheByMainId(mainId, 2)
//止损缓存
for _, v := range stoplossVal {
@ -473,7 +496,7 @@ func removeFutLossAndAddPosition(mainId int, orderSn string) {
// 处理主单成交,处理止盈、止损、减仓订单
func handleFutMainOrderFilled(db *gorm.DB, preOrder *models.LinePreOrder, extOrderId int, first bool) {
// 获取交易对配置和API信息
tradeSet, err := GetTradeSet(preOrder.Symbol, 1)
tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 1)
mainId := preOrder.Id
if err != nil || tradeSet.Coin == "" {
logger.Errorf("获取交易对配置失败, 回调订单号:%s, 错误信息: %v", preOrder.OrderSn, err)
@ -595,7 +618,7 @@ func handleFutMainOrderFilled(db *gorm.DB, preOrder *models.LinePreOrder, extOrd
}
}
processFutStopLossOrder(db, order, price, num)
processFutStopLossOrder(db, order, utility.StrToDecimal(order.Price), num)
// case 4: // 减仓
// processFutReduceOrder(order, price, num)
}
@ -697,9 +720,13 @@ func updateOrderQuantity(db *gorm.DB, order models.LinePreOrder, preOrder *model
// order.Num = num.String()
// } else
if first && (order.OrderCategory == 1 || order.OrderCategory == 3) && order.OrderType == 1 && ext.TakeProfitNumRatio.Cmp(decimal.Zero) > 0 && ext.TakeProfitNumRatio.Cmp(decimal.NewFromInt(100)) != 0 {
//止盈止损重算数量
if first && (order.OrderCategory == 1 || order.OrderCategory == 3) && ext.TakeProfitNumRatio.Cmp(decimal.Zero) > 0 && ext.TakeProfitNumRatio.Cmp(decimal.NewFromInt(100)) != 0 {
// 计算止盈数量
num = num.Mul(ext.TakeProfitNumRatio.Div(decimal.NewFromInt(100))).Truncate(int32(tradeSet.AmountDigit))
if order.OrderType == 1 {
num = num.Mul(ext.TakeProfitNumRatio.Div(decimal.NewFromInt(100))).Truncate(int32(tradeSet.AmountDigit))
}
order.Num = num.String()
}
@ -742,7 +769,7 @@ func processFutReduceOrder(order DbModels.LinePreOrder, price, num decimal.Decim
// 处理止盈订单
func processFutTakeProfitOrder(db *gorm.DB, futApi FutRestApi, order models.LinePreOrder, num decimal.Decimal) {
price, _ := decimal.NewFromString(order.Price)
tradeSet, _ := GetTradeSet(order.Symbol, 1)
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1)
params := FutOrderPlace{
ApiId: order.ApiId,
@ -779,7 +806,7 @@ func processFutTakeProfitOrder(db *gorm.DB, futApi FutRestApi, order models.Line
// 处理止损订单
// order 止损单
func processFutStopLossOrder(db *gorm.DB, order models.LinePreOrder, price, num decimal.Decimal) error {
tradeSet, _ := GetTradeSet(order.Symbol, 1)
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1)
params := FutOrderPlace{
ApiId: order.ApiId,

View File

@ -0,0 +1,587 @@
package binanceservice
import (
"context"
"fmt"
"sync"
"testing"
"time"
"github.com/go-admin-team/go-admin-core/logger"
"github.com/go-redis/redis/v8"
"github.com/shopspring/decimal"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
// OptimizedSystemTestSuite 优化系统测试套件
type OptimizedSystemTestSuite struct {
suite.Suite
db *gorm.DB
redisClient *redis.Client
config *OptimizedConfig
metricsCollector *MetricsCollector
errorHandler *ErrorHandler
lockManager *LockManager
txManager *TransactionManager
reverseService *ReverseServiceOptimized
}
// SetupSuite 设置测试套件
func (suite *OptimizedSystemTestSuite) SetupSuite() {
// 初始化数据库连接(测试环境)
dsn := "root:password@tcp(localhost:3306)/test_exchange?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
suite.T().Skip("跳过集成测试:无法连接数据库")
return
}
suite.db = db
// 初始化Redis连接测试环境
suite.redisClient = redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 1, // 使用测试数据库
})
// 测试Redis连接
ctx := context.Background()
if err := suite.redisClient.Ping(ctx).Err(); err != nil {
suite.T().Skip("跳过集成测试无法连接Redis")
return
}
// 初始化配置
suite.config = GetDefaultOptimizedConfig()
suite.config.MonitorConfig.Enabled = true
// 初始化组件
suite.metricsCollector = NewMetricsCollector(suite.config)
suite.errorHandler = NewErrorHandler(suite.config, suite.metricsCollector)
suite.lockManager = NewLockManager(suite.config, suite.redisClient, suite.metricsCollector)
suite.txManager = NewTransactionManager(suite.db, suite.metricsCollector, suite.config)
// 启动指标收集
suite.metricsCollector.Start()
// 创建测试表
suite.createTestTables()
}
// TearDownSuite 清理测试套件
func (suite *OptimizedSystemTestSuite) TearDownSuite() {
if suite.metricsCollector != nil {
suite.metricsCollector.Stop()
}
if suite.redisClient != nil {
suite.redisClient.Close()
}
}
// SetupTest 设置每个测试
func (suite *OptimizedSystemTestSuite) SetupTest() {
// 清理Redis测试数据
ctx := context.Background()
suite.redisClient.FlushDB(ctx)
// 清理数据库测试数据
suite.db.Exec("DELETE FROM test_orders")
suite.db.Exec("DELETE FROM test_positions")
}
// createTestTables 创建测试表
func (suite *OptimizedSystemTestSuite) createTestTables() {
// 创建测试订单表
suite.db.Exec(`
CREATE TABLE IF NOT EXISTS test_orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id VARCHAR(100) UNIQUE NOT NULL,
user_id VARCHAR(50) NOT NULL,
symbol VARCHAR(20) NOT NULL,
side VARCHAR(10) NOT NULL,
quantity DECIMAL(20,8) NOT NULL,
price DECIMAL(20,8),
status VARCHAR(20) NOT NULL,
version INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_symbol (user_id, symbol),
INDEX idx_status (status),
INDEX idx_version (version)
)
`)
// 创建测试持仓表
suite.db.Exec(`
CREATE TABLE IF NOT EXISTS test_positions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id VARCHAR(50) NOT NULL,
symbol VARCHAR(20) NOT NULL,
side VARCHAR(10) NOT NULL,
quantity DECIMAL(20,8) NOT NULL,
avg_price DECIMAL(20,8),
status VARCHAR(20) NOT NULL,
version INT DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_symbol_side (user_id, symbol, side),
INDEX idx_version (version)
)
`)
}
// TestLockManager 测试锁管理器
func (suite *OptimizedSystemTestSuite) TestLockManager() {
ctx := context.Background()
// 测试分布式锁
suite.Run("DistributedLock", func() {
config := LockManagerConfig{
Type: LockTypeDistributed,
Scope: ScopeOrder,
Key: "test_order_123",
Expiration: 10 * time.Second,
Timeout: 5 * time.Second,
RetryDelay: 100 * time.Millisecond,
}
// 获取锁
lock, err := suite.lockManager.AcquireLock(ctx, config)
assert.NoError(suite.T(), err)
assert.NotNil(suite.T(), lock)
assert.True(suite.T(), lock.IsLocked())
// 尝试获取同一个锁(应该失败)
config.Timeout = 1 * time.Second
_, err = suite.lockManager.AcquireLock(ctx, config)
assert.Error(suite.T(), err)
// 释放锁
err = suite.lockManager.ReleaseLock(ctx, lock)
assert.NoError(suite.T(), err)
assert.False(suite.T(), lock.IsLocked())
})
// 测试本地锁
suite.Run("LocalLock", func() {
config := LockManagerConfig{
Type: LockTypeLocal,
Scope: ScopeUser,
Key: "test_user_456",
Expiration: 10 * time.Second,
Timeout: 5 * time.Second,
RetryDelay: 100 * time.Millisecond,
}
lock, err := suite.lockManager.AcquireLock(ctx, config)
assert.NoError(suite.T(), err)
assert.NotNil(suite.T(), lock)
assert.True(suite.T(), lock.IsLocked())
err = suite.lockManager.ReleaseLock(ctx, lock)
assert.NoError(suite.T(), err)
})
// 测试并发锁
suite.Run("ConcurrentLocks", func() {
var wg sync.WaitGroup
successCount := 0
var mu sync.Mutex
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
config := LockManagerConfig{
Type: LockTypeDistributed,
Scope: ScopeGlobal,
Key: "concurrent_test",
Expiration: 2 * time.Second,
Timeout: 1 * time.Second,
RetryDelay: 50 * time.Millisecond,
}
lock, err := suite.lockManager.AcquireLock(ctx, config)
if err == nil {
mu.Lock()
successCount++
mu.Unlock()
time.Sleep(100 * time.Millisecond)
suite.lockManager.ReleaseLock(ctx, lock)
}
}(i)
}
wg.Wait()
assert.Equal(suite.T(), 1, successCount, "只有一个goroutine应该获得锁")
})
}
// TestTransactionManager 测试事务管理器
func (suite *OptimizedSystemTestSuite) TestTransactionManager() {
ctx := context.Background()
// 测试基本事务
suite.Run("BasicTransaction", func() {
config := GetDefaultTransactionConfig()
result := suite.txManager.WithTransaction(ctx, config, func(txCtx *TransactionContext) error {
// 插入测试数据
return txCtx.Tx.Exec(`
INSERT INTO test_orders (order_id, user_id, symbol, side, quantity, status)
VALUES (?, ?, ?, ?, ?, ?)
`, "order_001", "user_001", "BTCUSDT", "BUY", 1.0, "NEW").Error
})
assert.True(suite.T(), result.Success)
assert.NoError(suite.T(), result.Error)
// 验证数据已插入
var count int64
suite.db.Raw("SELECT COUNT(*) FROM test_orders WHERE order_id = ?", "order_001").Scan(&count)
assert.Equal(suite.T(), int64(1), count)
})
// 测试事务回滚
suite.Run("TransactionRollback", func() {
config := GetDefaultTransactionConfig()
result := suite.txManager.WithTransaction(ctx, config, func(txCtx *TransactionContext) error {
// 插入数据
if err := txCtx.Tx.Exec(`
INSERT INTO test_orders (order_id, user_id, symbol, side, quantity, status)
VALUES (?, ?, ?, ?, ?, ?)
`, "order_002", "user_002", "ETHUSDT", "SELL", 2.0, "NEW").Error; err != nil {
return err
}
// 故意返回错误以触发回滚
return fmt.Errorf("测试回滚")
})
assert.False(suite.T(), result.Success)
assert.Error(suite.T(), result.Error)
// 验证数据未插入
var count int64
suite.db.Raw("SELECT COUNT(*) FROM test_orders WHERE order_id = ?", "order_002").Scan(&count)
assert.Equal(suite.T(), int64(0), count)
})
// 测试乐观锁
suite.Run("OptimisticLock", func() {
// 先插入测试数据
suite.db.Exec(`
INSERT INTO test_positions (user_id, symbol, side, quantity, avg_price, status, version)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, "user_003", "BTCUSDT", "LONG", 1.0, 50000.0, "OPEN", 0)
// 测试正常更新
updates := map[string]interface{}{
"quantity": 2.0,
}
err := suite.txManager.UpdateWithOptimisticLock(ctx, nil, updates,
"user_id = ? AND symbol = ? AND side = ? AND version = ?",
"user_003", "BTCUSDT", "LONG", 0)
assert.NoError(suite.T(), err)
// 测试版本冲突
updates["quantity"] = 3.0
err = suite.txManager.UpdateWithOptimisticLock(ctx, nil, updates,
"user_id = ? AND symbol = ? AND side = ? AND version = ?",
"user_003", "BTCUSDT", "LONG", 0) // 使用旧版本号
assert.Error(suite.T(), err)
assert.Contains(suite.T(), err.Error(), "乐观锁冲突")
})
}
// TestErrorHandler 测试错误处理器
func (suite *OptimizedSystemTestSuite) TestErrorHandler() {
ctx := context.Background()
// 测试错误分类
suite.Run("ErrorClassification", func() {
testCases := []struct {
error error
expected ErrorType
}{
{fmt.Errorf("connection refused"), ErrorTypeNetwork},
{fmt.Errorf("database connection failed"), ErrorTypeDatabase},
{fmt.Errorf("context deadline exceeded"), ErrorTypeTimeout},
{fmt.Errorf("too many requests"), ErrorTypeRateLimit},
{fmt.Errorf("unauthorized access"), ErrorTypeAuth},
{fmt.Errorf("invalid parameter"), ErrorTypeValidation},
{fmt.Errorf("insufficient balance"), ErrorTypeBusiness},
}
for _, tc := range testCases {
errorInfo := suite.errorHandler.ClassifyError(tc.error, "test")
assert.Equal(suite.T(), tc.expected, errorInfo.Type)
}
})
// 测试重试机制
suite.Run("RetryMechanism", func() {
attempts := 0
config := RetryConfig{
MaxRetries: 3,
RetryDelay: 10 * time.Millisecond,
MaxRetryDelay: 100 * time.Millisecond,
BackoffFactor: 2.0,
ApiRetryCount: 3,
DbRetryCount: 3,
}
// 测试重试成功
err := suite.errorHandler.RetryWithBackoff(ctx, func() error {
attempts++
if attempts < 3 {
return fmt.Errorf("temporary error")
}
return nil
}, config, "test_retry")
assert.NoError(suite.T(), err)
assert.Equal(suite.T(), 3, attempts)
// 测试重试失败
attempts = 0
err = suite.errorHandler.RetryWithBackoff(ctx, func() error {
attempts++
return fmt.Errorf("persistent error")
}, config, "test_retry_fail")
assert.Error(suite.T(), err)
assert.Equal(suite.T(), 3, attempts)
})
}
// TestMetricsCollector 测试指标收集器
func (suite *OptimizedSystemTestSuite) TestMetricsCollector() {
// 测试订单指标
suite.Run("OrderMetrics", func() {
// 记录订单处理
suite.metricsCollector.RecordOrderProcessed(true, 100*time.Millisecond)
suite.metricsCollector.RecordOrderProcessed(false, 200*time.Millisecond)
suite.metricsCollector.RecordOrderCanceled()
suite.metricsCollector.RecordReverseOrder(true)
metrics := suite.metricsCollector.GetMetrics()
orderMetrics := metrics["order_metrics"].(*OrderMetrics)
assert.Equal(suite.T(), int64(2), orderMetrics.TotalProcessed)
assert.Equal(suite.T(), int64(1), orderMetrics.SuccessfulOrders)
assert.Equal(suite.T(), int64(1), orderMetrics.FailedOrders)
assert.Equal(suite.T(), int64(1), orderMetrics.CanceledOrders)
assert.Equal(suite.T(), int64(1), orderMetrics.ReverseOrdersCreated)
})
// 测试持仓指标
suite.Run("PositionMetrics", func() {
difference := decimal.NewFromFloat(0.1)
suite.metricsCollector.RecordPositionSync(true, difference)
suite.metricsCollector.RecordPositionSync(false, decimal.Zero)
suite.metricsCollector.RecordClosePosition(false)
suite.metricsCollector.RecordClosePosition(true)
metrics := suite.metricsCollector.GetMetrics()
positionMetrics := metrics["position_metrics"].(*PositionMetrics)
assert.Equal(suite.T(), int64(2), positionMetrics.SyncAttempts)
assert.Equal(suite.T(), int64(1), positionMetrics.SyncSuccesses)
assert.Equal(suite.T(), int64(1), positionMetrics.SyncFailures)
assert.Equal(suite.T(), int64(2), positionMetrics.ClosePositions)
assert.Equal(suite.T(), int64(1), positionMetrics.ForceClosePositions)
})
// 测试性能指标
suite.Run("PerformanceMetrics", func() {
suite.metricsCollector.RecordLockOperation(true, 50*time.Millisecond)
suite.metricsCollector.RecordDbOperation(false, 30*time.Millisecond, nil)
suite.metricsCollector.RecordApiCall(100*time.Millisecond, nil, 1)
suite.metricsCollector.RecordConcurrency(5)
metrics := suite.metricsCollector.GetMetrics()
perfMetrics := metrics["performance_metrics"].(*PerformanceMetrics)
assert.Equal(suite.T(), int64(1), perfMetrics.LockAcquisitions)
assert.Equal(suite.T(), int64(1), perfMetrics.DbQueries)
assert.Equal(suite.T(), int64(1), perfMetrics.ApiCalls)
assert.Equal(suite.T(), int64(5), perfMetrics.ConcurrentRequests)
})
}
// TestConcurrentOperations 测试并发操作
func (suite *OptimizedSystemTestSuite) TestConcurrentOperations() {
ctx := context.Background()
suite.Run("ConcurrentOrderProcessing", func() {
var wg sync.WaitGroup
successCount := 0
var mu sync.Mutex
// 并发处理订单
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
// 使用锁保护订单处理
lockConfig := GetOrderLockConfig(fmt.Sprintf("order_%d", id))
err := suite.lockManager.WithLock(ctx, lockConfig, func() error {
// 模拟订单处理
config := GetDefaultTransactionConfig()
result := suite.txManager.WithTransaction(ctx, config, func(txCtx *TransactionContext) error {
return txCtx.Tx.Exec(`
INSERT INTO test_orders (order_id, user_id, symbol, side, quantity, status)
VALUES (?, ?, ?, ?, ?, ?)
`, fmt.Sprintf("concurrent_order_%d", id), fmt.Sprintf("user_%d", id),
"BTCUSDT", "BUY", 1.0, "NEW").Error
})
if result.Success {
mu.Lock()
successCount++
mu.Unlock()
}
return result.Error
})
if err != nil {
logger.Errorf("并发订单处理失败 [%d]: %v", id, err)
}
}(i)
}
wg.Wait()
assert.Equal(suite.T(), 10, successCount, "所有并发订单都应该成功处理")
// 验证数据库中的记录数
var count int64
suite.db.Raw("SELECT COUNT(*) FROM test_orders WHERE order_id LIKE 'concurrent_order_%'").Scan(&count)
assert.Equal(suite.T(), int64(10), count)
})
}
// TestSystemIntegration 测试系统集成
func (suite *OptimizedSystemTestSuite) TestSystemIntegration() {
ctx := context.Background()
suite.Run("CompleteOrderFlow", func() {
userID := "integration_user"
symbol := "BTCUSDT"
orderID := "integration_order_001"
// 步骤1创建订单使用锁和事务
err := WithOrderLock(ctx, orderID, func() error {
return WithDefaultTransaction(ctx, func(txCtx *TransactionContext) error {
return txCtx.Tx.Exec(`
INSERT INTO test_orders (order_id, user_id, symbol, side, quantity, price, status)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, orderID, userID, symbol, "BUY", 1.0, 50000.0, "NEW").Error
})
})
assert.NoError(suite.T(), err)
// 步骤2更新订单状态使用乐观锁
updates := map[string]interface{}{
"status": "FILLED",
}
err = UpdateWithOptimisticLockGlobal(ctx, nil, updates,
"order_id = ? AND version = ?", orderID, 0)
assert.NoError(suite.T(), err)
// 步骤3创建持仓使用锁和事务
err = WithPositionLock(ctx, userID, symbol, func() error {
return WithDefaultTransaction(ctx, func(txCtx *TransactionContext) error {
return txCtx.Tx.Exec(`
INSERT INTO test_positions (user_id, symbol, side, quantity, avg_price, status)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
quantity = quantity + VALUES(quantity),
avg_price = (avg_price * quantity + VALUES(avg_price) * VALUES(quantity)) / (quantity + VALUES(quantity)),
version = version + 1
`, userID, symbol, "LONG", 1.0, 50000.0, "OPEN").Error
})
})
assert.NoError(suite.T(), err)
// 验证最终状态
var orderStatus string
suite.db.Raw("SELECT status FROM test_orders WHERE order_id = ?", orderID).Scan(&orderStatus)
assert.Equal(suite.T(), "FILLED", orderStatus)
var positionQuantity float64
suite.db.Raw("SELECT quantity FROM test_positions WHERE user_id = ? AND symbol = ?",
userID, symbol).Scan(&positionQuantity)
assert.Equal(suite.T(), 1.0, positionQuantity)
})
}
// TestPerformance 测试性能
func (suite *OptimizedSystemTestSuite) TestPerformance() {
ctx := context.Background()
suite.Run("LockPerformance", func() {
startTime := time.Now()
iterations := 1000
for i := 0; i < iterations; i++ {
lockConfig := GetOrderLockConfig(fmt.Sprintf("perf_order_%d", i))
lock, err := suite.lockManager.AcquireLock(ctx, lockConfig)
assert.NoError(suite.T(), err)
suite.lockManager.ReleaseLock(ctx, lock)
}
duration := time.Since(startTime)
avgDuration := duration / time.Duration(iterations)
logger.Infof("锁性能测试: %d 次操作耗时 %v平均每次 %v", iterations, duration, avgDuration)
// 平均每次操作应该在合理范围内
assert.Less(suite.T(), avgDuration, 10*time.Millisecond)
})
suite.Run("TransactionPerformance", func() {
startTime := time.Now()
iterations := 100
for i := 0; i < iterations; i++ {
config := GetDefaultTransactionConfig()
result := suite.txManager.WithTransaction(ctx, config, func(txCtx *TransactionContext) error {
return txCtx.Tx.Exec(`
INSERT INTO test_orders (order_id, user_id, symbol, side, quantity, status)
VALUES (?, ?, ?, ?, ?, ?)
`, fmt.Sprintf("perf_order_%d", i), "perf_user", "BTCUSDT", "BUY", 1.0, "NEW").Error
})
assert.True(suite.T(), result.Success)
}
duration := time.Since(startTime)
avgDuration := duration / time.Duration(iterations)
logger.Infof("事务性能测试: %d 次操作耗时 %v平均每次 %v", iterations, duration, avgDuration)
// 平均每次事务应该在合理范围内
assert.Less(suite.T(), avgDuration, 100*time.Millisecond)
})
}
// TestSuite 运行测试套件
func TestOptimizedSystemSuite(t *testing.T) {
suite.Run(t, new(OptimizedSystemTestSuite))
}
// BenchmarkLockManager 锁管理器基准测试
func BenchmarkLockManager(b *testing.B) {
// 这里可以添加基准测试
b.Skip("基准测试需要Redis连接")
}
// BenchmarkTransactionManager 事务管理器基准测试
func BenchmarkTransactionManager(b *testing.B) {
// 这里可以添加基准测试
b.Skip("基准测试需要数据库连接")
}

View File

@ -0,0 +1,575 @@
package binanceservice
import (
"context"
"fmt"
"sync"
"time"
"github.com/go-admin-team/go-admin-core/logger"
"github.com/go-redis/redis/v8"
)
// LockManagerConfig 锁管理器专用配置
type LockManagerConfig struct {
Type LockType `json:"type"`
Scope LockScope `json:"scope"`
Key string `json:"key"`
Expiration time.Duration `json:"expiration"`
Timeout time.Duration `json:"timeout"`
RetryDelay time.Duration `json:"retry_delay"`
}
// LockType 锁类型
type LockType string
const (
LockTypeLocal LockType = "local"
LockTypeDistributed LockType = "distributed"
)
// LockScope 锁范围
type LockScope string
const (
ScopeOrder LockScope = "order"
ScopePosition LockScope = "position"
ScopeUser LockScope = "user"
ScopeSymbol LockScope = "symbol"
ScopeGlobal LockScope = "global"
)
// Lock 锁接口
type Lock interface {
Lock(ctx context.Context) error
Unlock(ctx context.Context) error
TryLock(ctx context.Context) (bool, error)
IsLocked() bool
GetKey() string
GetExpiration() time.Duration
}
// LocalLock 本地锁
type LocalLock struct {
key string
mu *sync.RWMutex
locked bool
expiration time.Duration
acquiredAt time.Time
}
// DistributedLock 分布式锁
type DistributedLock struct {
key string
value string
expiration time.Duration
redisClient *redis.Client
locked bool
acquiredAt time.Time
refreshTicker *time.Ticker
stopRefresh chan struct{}
}
// LockManager 锁管理器
type LockManager struct {
config *OptimizedConfig
redisClient *redis.Client
localLocks map[string]*LocalLock
localMutex sync.RWMutex
metrics *MetricsCollector
}
// LockConfig 已在 config_optimized.go 中定义
// NewLockManager 创建锁管理器
func NewLockManager(config *OptimizedConfig, redisClient *redis.Client, metrics *MetricsCollector) *LockManager {
return &LockManager{
config: config,
redisClient: redisClient,
localLocks: make(map[string]*LocalLock),
metrics: metrics,
}
}
// CreateLock 创建锁
func (lm *LockManager) CreateLock(config LockManagerConfig) Lock {
switch config.Type {
case LockTypeDistributed:
return lm.createDistributedLock(config)
case LockTypeLocal:
return lm.createLocalLock(config)
default:
// 默认使用分布式锁
return lm.createDistributedLock(config)
}
}
// createLocalLock 创建本地锁
func (lm *LockManager) createLocalLock(config LockManagerConfig) *LocalLock {
lm.localMutex.Lock()
defer lm.localMutex.Unlock()
key := lm.buildLockKey(config.Scope, config.Key)
if lock, exists := lm.localLocks[key]; exists {
return lock
}
lock := &LocalLock{
key: key,
mu: &sync.RWMutex{},
expiration: config.Expiration,
}
lm.localLocks[key] = lock
return lock
}
// createDistributedLock 创建分布式锁
func (lm *LockManager) createDistributedLock(config LockManagerConfig) *DistributedLock {
key := lm.buildLockKey(config.Scope, config.Key)
value := fmt.Sprintf("%d_%s", time.Now().UnixNano(), generateRandomString(8))
return &DistributedLock{
key: key,
value: value,
expiration: config.Expiration,
redisClient: lm.redisClient,
stopRefresh: make(chan struct{}),
}
}
// buildLockKey 构建锁键名
func (lm *LockManager) buildLockKey(scope LockScope, key string) string {
return fmt.Sprintf("lock:%s:%s", scope, key)
}
// AcquireLock 获取锁(带超时和重试)
func (lm *LockManager) AcquireLock(ctx context.Context, config LockManagerConfig) (Lock, error) {
lock := lm.CreateLock(config)
startTime := time.Now()
// 设置超时上下文
lockCtx := ctx
if config.Timeout > 0 {
var cancel context.CancelFunc
lockCtx, cancel = context.WithTimeout(ctx, config.Timeout)
defer cancel()
}
// 重试获取锁
for {
select {
case <-lockCtx.Done():
waitTime := time.Since(startTime)
if lm.metrics != nil {
lm.metrics.RecordLockOperation(false, waitTime)
}
return nil, fmt.Errorf("获取锁超时: %s", config.Key)
default:
err := lock.Lock(lockCtx)
if err == nil {
waitTime := time.Since(startTime)
if lm.metrics != nil {
lm.metrics.RecordLockOperation(true, waitTime)
}
return lock, nil
}
// 等待重试
select {
case <-lockCtx.Done():
return nil, lockCtx.Err()
case <-time.After(config.RetryDelay):
continue
}
}
}
}
// ReleaseLock 释放锁
func (lm *LockManager) ReleaseLock(ctx context.Context, lock Lock) error {
if lock == nil {
return nil
}
return lock.Unlock(ctx)
}
// WithLock 使用锁执行操作
func (lm *LockManager) WithLock(ctx context.Context, config LockManagerConfig, operation func() error) error {
lock, err := lm.AcquireLock(ctx, config)
if err != nil {
return fmt.Errorf("获取锁失败: %w", err)
}
defer func() {
if unlockErr := lm.ReleaseLock(ctx, lock); unlockErr != nil {
logger.Errorf("释放锁失败 [%s]: %v", config.Key, unlockErr)
}
}()
return operation()
}
// CleanupExpiredLocks 清理过期的本地锁
func (lm *LockManager) CleanupExpiredLocks() {
lm.localMutex.Lock()
defer lm.localMutex.Unlock()
now := time.Now()
for key, lock := range lm.localLocks {
if lock.locked && lock.expiration > 0 && now.Sub(lock.acquiredAt) > lock.expiration {
lock.locked = false
logger.Warnf("本地锁已过期并被清理: %s", key)
}
}
}
// GetLockStatus 获取锁状态
func (lm *LockManager) GetLockStatus() map[string]interface{} {
lm.localMutex.RLock()
defer lm.localMutex.RUnlock()
status := make(map[string]interface{})
localLocks := make(map[string]interface{})
for key, lock := range lm.localLocks {
localLocks[key] = map[string]interface{}{
"locked": lock.locked,
"acquired_at": lock.acquiredAt,
"expiration": lock.expiration,
}
}
status["local_locks"] = localLocks
status["total_local_locks"] = len(lm.localLocks)
return status
}
// LocalLock 实现
// Lock 获取本地锁
func (ll *LocalLock) Lock(ctx context.Context) error {
ll.mu.Lock()
defer ll.mu.Unlock()
if ll.locked {
return fmt.Errorf("锁已被占用: %s", ll.key)
}
ll.locked = true
ll.acquiredAt = time.Now()
return nil
}
// Unlock 释放本地锁
func (ll *LocalLock) Unlock(ctx context.Context) error {
ll.mu.Lock()
defer ll.mu.Unlock()
if !ll.locked {
return fmt.Errorf("锁未被占用: %s", ll.key)
}
ll.locked = false
return nil
}
// TryLock 尝试获取本地锁
func (ll *LocalLock) TryLock(ctx context.Context) (bool, error) {
ll.mu.Lock()
defer ll.mu.Unlock()
if ll.locked {
return false, nil
}
ll.locked = true
ll.acquiredAt = time.Now()
return true, nil
}
// IsLocked 检查本地锁是否被占用
func (ll *LocalLock) IsLocked() bool {
ll.mu.RLock()
defer ll.mu.RUnlock()
return ll.locked
}
// GetKey 获取锁键名
func (ll *LocalLock) GetKey() string {
return ll.key
}
// GetExpiration 获取过期时间
func (ll *LocalLock) GetExpiration() time.Duration {
return ll.expiration
}
// DistributedLock 实现
// Lock 获取分布式锁
func (dl *DistributedLock) Lock(ctx context.Context) error {
// 使用 SET key value EX seconds NX 命令
result := dl.redisClient.SetNX(ctx, dl.key, dl.value, dl.expiration)
if result.Err() != nil {
return fmt.Errorf("获取分布式锁失败: %w", result.Err())
}
if !result.Val() {
return fmt.Errorf("分布式锁已被占用: %s", dl.key)
}
dl.locked = true
dl.acquiredAt = time.Now()
// 启动锁续期
dl.startRefresh()
return nil
}
// Unlock 释放分布式锁
func (dl *DistributedLock) Unlock(ctx context.Context) error {
if !dl.locked {
return fmt.Errorf("分布式锁未被占用: %s", dl.key)
}
// 停止续期
dl.stopRefreshProcess()
// 使用 Lua 脚本确保只删除自己的锁
luaScript := `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`
result := dl.redisClient.Eval(ctx, luaScript, []string{dl.key}, dl.value)
if result.Err() != nil {
return fmt.Errorf("释放分布式锁失败: %w", result.Err())
}
dl.locked = false
return nil
}
// TryLock 尝试获取分布式锁
func (dl *DistributedLock) TryLock(ctx context.Context) (bool, error) {
result := dl.redisClient.SetNX(ctx, dl.key, dl.value, dl.expiration)
if result.Err() != nil {
return false, fmt.Errorf("尝试获取分布式锁失败: %w", result.Err())
}
if result.Val() {
dl.locked = true
dl.acquiredAt = time.Now()
dl.startRefresh()
return true, nil
}
return false, nil
}
// IsLocked 检查分布式锁是否被占用
func (dl *DistributedLock) IsLocked() bool {
return dl.locked
}
// GetKey 获取锁键名
func (dl *DistributedLock) GetKey() string {
return dl.key
}
// GetExpiration 获取过期时间
func (dl *DistributedLock) GetExpiration() time.Duration {
return dl.expiration
}
// startRefresh 启动锁续期
func (dl *DistributedLock) startRefresh() {
if dl.expiration <= 0 {
return
}
// 每隔过期时间的1/3进行续期
refreshInterval := dl.expiration / 3
dl.refreshTicker = time.NewTicker(refreshInterval)
go func() {
defer dl.refreshTicker.Stop()
for {
select {
case <-dl.stopRefresh:
return
case <-dl.refreshTicker.C:
if !dl.locked {
return
}
// 使用 Lua 脚本续期
luaScript := `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("expire", KEYS[1], ARGV[2])
else
return 0
end
`
ctx := context.Background()
result := dl.redisClient.Eval(ctx, luaScript, []string{dl.key}, dl.value, int(dl.expiration.Seconds()))
if result.Err() != nil {
logger.Errorf("分布式锁续期失败 [%s]: %v", dl.key, result.Err())
return
}
if result.Val().(int64) == 0 {
logger.Warnf("分布式锁已被其他进程占用,停止续期 [%s]", dl.key)
dl.locked = false
return
}
}
}
}()
}
// stopRefreshProcess 停止续期进程
func (dl *DistributedLock) stopRefreshProcess() {
if dl.refreshTicker != nil {
close(dl.stopRefresh)
dl.refreshTicker.Stop()
dl.refreshTicker = nil
dl.stopRefresh = make(chan struct{})
}
}
// 辅助函数
// generateRandomString 生成随机字符串
func generateRandomString(length int) string {
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, length)
for i := range b {
b[i] = charset[time.Now().UnixNano()%int64(len(charset))]
}
return string(b)
}
// 预定义的锁配置
// GetOrderLockConfig 获取订单锁配置
func GetOrderLockConfig(orderID string) LockManagerConfig {
return LockManagerConfig{
Type: LockTypeDistributed,
Scope: ScopeOrder,
Key: orderID,
Expiration: 30 * time.Second,
Timeout: 5 * time.Second,
RetryDelay: 100 * time.Millisecond,
}
}
// GetPositionLockConfig 获取持仓锁配置
func GetPositionLockConfig(userID, symbol string) LockManagerConfig {
return LockManagerConfig{
Type: LockTypeDistributed,
Scope: ScopePosition,
Key: fmt.Sprintf("%s_%s", userID, symbol),
Expiration: 60 * time.Second,
Timeout: 10 * time.Second,
RetryDelay: 200 * time.Millisecond,
}
}
// GetUserLockConfig 获取用户锁配置
func GetUserLockConfig(userID string) LockManagerConfig {
return LockManagerConfig{
Type: LockTypeLocal,
Scope: ScopeUser,
Key: userID,
Expiration: 30 * time.Second,
Timeout: 3 * time.Second,
RetryDelay: 50 * time.Millisecond,
}
}
// GetSymbolLockConfig 获取交易对锁配置
func GetSymbolLockConfig(symbol string) LockManagerConfig {
return LockManagerConfig{
Type: LockTypeLocal,
Scope: ScopeSymbol,
Key: symbol,
Expiration: 15 * time.Second,
Timeout: 2 * time.Second,
RetryDelay: 25 * time.Millisecond,
}
}
// GetGlobalLockConfig 获取全局锁配置
func GetGlobalLockConfig(operation string) LockManagerConfig {
return LockManagerConfig{
Type: LockTypeDistributed,
Scope: ScopeGlobal,
Key: operation,
Expiration: 120 * time.Second,
Timeout: 30 * time.Second,
RetryDelay: 500 * time.Millisecond,
}
}
// 全局锁管理器实例
var GlobalLockManager *LockManager
// InitLockManager 初始化锁管理器
func InitLockManager(config *OptimizedConfig, redisClient *redis.Client, metrics *MetricsCollector) {
GlobalLockManager = NewLockManager(config, redisClient, metrics)
// 启动清理过期锁的定时任务
go func() {
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for range ticker.C {
GlobalLockManager.CleanupExpiredLocks()
}
}()
}
// GetLockManager 获取全局锁管理器
func GetLockManager() *LockManager {
return GlobalLockManager
}
// WithOrderLock 使用订单锁执行操作
func WithOrderLock(ctx context.Context, orderID string, operation func() error) error {
if GlobalLockManager == nil {
return operation()
}
config := GetOrderLockConfig(orderID)
return GlobalLockManager.WithLock(ctx, config, operation)
}
// WithPositionLock 使用持仓锁执行操作
func WithPositionLock(ctx context.Context, userID, symbol string, operation func() error) error {
if GlobalLockManager == nil {
return operation()
}
config := GetPositionLockConfig(userID, symbol)
return GlobalLockManager.WithLock(ctx, config, operation)
}
// WithUserLock 使用用户锁执行操作
func WithUserLock(ctx context.Context, userID string, operation func() error) error {
if GlobalLockManager == nil {
return operation()
}
config := GetUserLockConfig(userID)
return GlobalLockManager.WithLock(ctx, config, operation)
}

View File

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

View File

@ -0,0 +1,594 @@
package binanceservice
import (
"context"
"fmt"
"runtime"
"sync"
"sync/atomic"
"time"
"github.com/go-admin-team/go-admin-core/logger"
"github.com/shopspring/decimal"
)
// MetricsCollector 指标收集器
type MetricsCollector struct {
mu sync.RWMutex
// 业务指标
OrderMetrics *OrderMetrics
PositionMetrics *PositionMetrics
TakeProfitMetrics *TakeProfitMetrics
// 技术指标
PerformanceMetrics *PerformanceMetrics
ErrorMetrics *ErrorMetrics
// 系统指标
SystemMetrics *SystemMetrics
// 配置
config *OptimizedConfig
// 控制
ctx context.Context
cancel context.CancelFunc
startTime time.Time
}
// OrderMetrics 订单相关指标
type OrderMetrics struct {
// 订单处理统计
TotalProcessed int64 `json:"total_processed"`
SuccessfulOrders int64 `json:"successful_orders"`
FailedOrders int64 `json:"failed_orders"`
CanceledOrders int64 `json:"canceled_orders"`
// 反向订单统计
ReverseOrdersCreated int64 `json:"reverse_orders_created"`
ReverseOrdersFailed int64 `json:"reverse_orders_failed"`
// 响应时间统计
TotalResponseTime time.Duration `json:"total_response_time"`
MaxResponseTime time.Duration `json:"max_response_time"`
MinResponseTime time.Duration `json:"min_response_time"`
// 最近更新时间
LastUpdated time.Time `json:"last_updated"`
}
// PositionMetrics 持仓相关指标
type PositionMetrics struct {
// 持仓同步统计
SyncAttempts int64 `json:"sync_attempts"`
SyncSuccesses int64 `json:"sync_successes"`
SyncFailures int64 `json:"sync_failures"`
// 持仓差异统计
PositionMismatches int64 `json:"position_mismatches"`
MaxDifference decimal.Decimal `json:"max_difference"`
TotalDifference decimal.Decimal `json:"total_difference"`
// 平仓统计
ClosePositions int64 `json:"close_positions"`
ForceClosePositions int64 `json:"force_close_positions"`
LastUpdated time.Time `json:"last_updated"`
}
// TakeProfitMetrics 止盈止损相关指标
type TakeProfitMetrics struct {
// 止盈止损创建统计
TakeProfitCreated int64 `json:"take_profit_created"`
StopLossCreated int64 `json:"stop_loss_created"`
// 止盈止损执行统计
TakeProfitExecuted int64 `json:"take_profit_executed"`
StopLossExecuted int64 `json:"stop_loss_executed"`
// 取消统计
OrdersCanceled int64 `json:"orders_canceled"`
BatchCancelAttempts int64 `json:"batch_cancel_attempts"`
BatchCancelSuccesses int64 `json:"batch_cancel_successes"`
LastUpdated time.Time `json:"last_updated"`
}
// PerformanceMetrics 性能相关指标
type PerformanceMetrics struct {
// 并发统计
ConcurrentRequests int64 `json:"concurrent_requests"`
MaxConcurrency int64 `json:"max_concurrency"`
// 锁统计
LockAcquisitions int64 `json:"lock_acquisitions"`
LockWaitTime time.Duration `json:"lock_wait_time"`
LockTimeouts int64 `json:"lock_timeouts"`
// 数据库操作统计
DbQueries int64 `json:"db_queries"`
DbTransactions int64 `json:"db_transactions"`
DbQueryTime time.Duration `json:"db_query_time"`
DbErrors int64 `json:"db_errors"`
// API调用统计
ApiCalls int64 `json:"api_calls"`
ApiCallTime time.Duration `json:"api_call_time"`
ApiErrors int64 `json:"api_errors"`
ApiRetries int64 `json:"api_retries"`
LastUpdated time.Time `json:"last_updated"`
}
// ErrorMetrics 错误相关指标
type ErrorMetrics struct {
// 错误分类统计
NetworkErrors int64 `json:"network_errors"`
DatabaseErrors int64 `json:"database_errors"`
BusinessErrors int64 `json:"business_errors"`
SystemErrors int64 `json:"system_errors"`
// 重试统计
RetryAttempts int64 `json:"retry_attempts"`
RetrySuccesses int64 `json:"retry_successes"`
RetryFailures int64 `json:"retry_failures"`
// 最近错误
RecentErrors []ErrorRecord `json:"recent_errors"`
LastUpdated time.Time `json:"last_updated"`
}
// SystemMetrics 系统相关指标
type SystemMetrics struct {
// 内存使用
MemoryUsage uint64 `json:"memory_usage"`
MemoryPercent float64 `json:"memory_percent"`
// CPU使用
CpuPercent float64 `json:"cpu_percent"`
// Goroutine统计
GoroutineCount int `json:"goroutine_count"`
// GC统计
GcCount uint32 `json:"gc_count"`
GcPauseMs uint64 `json:"gc_pause_ms"`
LastUpdated time.Time `json:"last_updated"`
}
// ErrorRecord 错误记录
type ErrorRecord struct {
Timestamp time.Time `json:"timestamp"`
Type string `json:"type"`
Message string `json:"message"`
Context string `json:"context"`
}
// NewMetricsCollector 创建新的指标收集器
func NewMetricsCollector(config *OptimizedConfig) *MetricsCollector {
ctx, cancel := context.WithCancel(context.Background())
return &MetricsCollector{
OrderMetrics: &OrderMetrics{
MinResponseTime: time.Hour, // 初始化为一个大值
LastUpdated: time.Now(),
},
PositionMetrics: &PositionMetrics{
MaxDifference: decimal.Zero,
TotalDifference: decimal.Zero,
LastUpdated: time.Now(),
},
TakeProfitMetrics: &TakeProfitMetrics{
LastUpdated: time.Now(),
},
PerformanceMetrics: &PerformanceMetrics{
LastUpdated: time.Now(),
},
ErrorMetrics: &ErrorMetrics{
RecentErrors: make([]ErrorRecord, 0, 100),
LastUpdated: time.Now(),
},
SystemMetrics: &SystemMetrics{
LastUpdated: time.Now(),
},
config: config,
ctx: ctx,
cancel: cancel,
startTime: time.Now(),
}
}
// Start 启动指标收集
func (mc *MetricsCollector) Start() {
if !mc.config.MonitorConfig.Enabled {
return
}
logger.Info("启动指标收集器")
// 启动定期收集
go mc.collectLoop()
// 启动告警检查
go mc.alertLoop()
}
// Stop 停止指标收集
func (mc *MetricsCollector) Stop() {
logger.Info("停止指标收集器")
mc.cancel()
}
// collectLoop 收集循环
func (mc *MetricsCollector) collectLoop() {
ticker := time.NewTicker(mc.config.MonitorConfig.CollectInterval)
defer ticker.Stop()
for {
select {
case <-mc.ctx.Done():
return
case <-ticker.C:
mc.collectSystemMetrics()
}
}
}
// alertLoop 告警检查循环
func (mc *MetricsCollector) alertLoop() {
ticker := time.NewTicker(time.Minute) // 每分钟检查一次
defer ticker.Stop()
for {
select {
case <-mc.ctx.Done():
return
case <-ticker.C:
mc.checkAlerts()
}
}
}
// RecordOrderProcessed 记录订单处理
func (mc *MetricsCollector) RecordOrderProcessed(success bool, responseTime time.Duration) {
mc.mu.Lock()
defer mc.mu.Unlock()
atomic.AddInt64(&mc.OrderMetrics.TotalProcessed, 1)
if success {
atomic.AddInt64(&mc.OrderMetrics.SuccessfulOrders, 1)
} else {
atomic.AddInt64(&mc.OrderMetrics.FailedOrders, 1)
}
// 更新响应时间统计
mc.OrderMetrics.TotalResponseTime += responseTime
if responseTime > mc.OrderMetrics.MaxResponseTime {
mc.OrderMetrics.MaxResponseTime = responseTime
}
if responseTime < mc.OrderMetrics.MinResponseTime {
mc.OrderMetrics.MinResponseTime = responseTime
}
mc.OrderMetrics.LastUpdated = time.Now()
}
// RecordOrderCanceled 记录订单取消
func (mc *MetricsCollector) RecordOrderCanceled() {
atomic.AddInt64(&mc.OrderMetrics.CanceledOrders, 1)
mc.OrderMetrics.LastUpdated = time.Now()
}
// RecordReverseOrder 记录反向订单
func (mc *MetricsCollector) RecordReverseOrder(success bool) {
if success {
atomic.AddInt64(&mc.OrderMetrics.ReverseOrdersCreated, 1)
} else {
atomic.AddInt64(&mc.OrderMetrics.ReverseOrdersFailed, 1)
}
mc.OrderMetrics.LastUpdated = time.Now()
}
// RecordPositionSync 记录持仓同步
func (mc *MetricsCollector) RecordPositionSync(success bool, difference decimal.Decimal) {
mc.mu.Lock()
defer mc.mu.Unlock()
atomic.AddInt64(&mc.PositionMetrics.SyncAttempts, 1)
if success {
atomic.AddInt64(&mc.PositionMetrics.SyncSuccesses, 1)
} else {
atomic.AddInt64(&mc.PositionMetrics.SyncFailures, 1)
}
// 更新差异统计
if !difference.IsZero() {
atomic.AddInt64(&mc.PositionMetrics.PositionMismatches, 1)
mc.PositionMetrics.TotalDifference = mc.PositionMetrics.TotalDifference.Add(difference.Abs())
if difference.Abs().GreaterThan(mc.PositionMetrics.MaxDifference) {
mc.PositionMetrics.MaxDifference = difference.Abs()
}
}
mc.PositionMetrics.LastUpdated = time.Now()
}
// RecordClosePosition 记录平仓
func (mc *MetricsCollector) RecordClosePosition(forced bool) {
atomic.AddInt64(&mc.PositionMetrics.ClosePositions, 1)
if forced {
atomic.AddInt64(&mc.PositionMetrics.ForceClosePositions, 1)
}
mc.PositionMetrics.LastUpdated = time.Now()
}
// RecordTakeProfitCreated 记录止盈止损创建
func (mc *MetricsCollector) RecordTakeProfitCreated(orderType string) {
switch orderType {
case "TAKE_PROFIT", "TAKE_PROFIT_MARKET":
atomic.AddInt64(&mc.TakeProfitMetrics.TakeProfitCreated, 1)
case "STOP", "STOP_MARKET":
atomic.AddInt64(&mc.TakeProfitMetrics.StopLossCreated, 1)
}
mc.TakeProfitMetrics.LastUpdated = time.Now()
}
// RecordTakeProfitExecuted 记录止盈止损执行
func (mc *MetricsCollector) RecordTakeProfitExecuted(orderType string) {
switch orderType {
case "TAKE_PROFIT", "TAKE_PROFIT_MARKET":
atomic.AddInt64(&mc.TakeProfitMetrics.TakeProfitExecuted, 1)
case "STOP", "STOP_MARKET":
atomic.AddInt64(&mc.TakeProfitMetrics.StopLossExecuted, 1)
}
mc.TakeProfitMetrics.LastUpdated = time.Now()
}
// RecordOrdersCanceled 记录订单取消
func (mc *MetricsCollector) RecordOrdersCanceled(count int) {
atomic.AddInt64(&mc.TakeProfitMetrics.OrdersCanceled, int64(count))
mc.TakeProfitMetrics.LastUpdated = time.Now()
}
// RecordBatchCancel 记录批量取消
func (mc *MetricsCollector) RecordBatchCancel(success bool) {
atomic.AddInt64(&mc.TakeProfitMetrics.BatchCancelAttempts, 1)
if success {
atomic.AddInt64(&mc.TakeProfitMetrics.BatchCancelSuccesses, 1)
}
mc.TakeProfitMetrics.LastUpdated = time.Now()
}
// RecordLockOperation 记录锁操作
func (mc *MetricsCollector) RecordLockOperation(acquired bool, waitTime time.Duration) {
atomic.AddInt64(&mc.PerformanceMetrics.LockAcquisitions, 1)
mc.PerformanceMetrics.LockWaitTime += waitTime
if !acquired {
atomic.AddInt64(&mc.PerformanceMetrics.LockTimeouts, 1)
}
mc.PerformanceMetrics.LastUpdated = time.Now()
}
// RecordDbOperation 记录数据库操作
func (mc *MetricsCollector) RecordDbOperation(isTransaction bool, duration time.Duration, err error) {
if isTransaction {
atomic.AddInt64(&mc.PerformanceMetrics.DbTransactions, 1)
} else {
atomic.AddInt64(&mc.PerformanceMetrics.DbQueries, 1)
}
mc.PerformanceMetrics.DbQueryTime += duration
if err != nil {
atomic.AddInt64(&mc.PerformanceMetrics.DbErrors, 1)
mc.RecordError("database", err.Error(), "db_operation")
}
mc.PerformanceMetrics.LastUpdated = time.Now()
}
// RecordApiCall 记录API调用
func (mc *MetricsCollector) RecordApiCall(duration time.Duration, err error, retryCount int) {
atomic.AddInt64(&mc.PerformanceMetrics.ApiCalls, 1)
mc.PerformanceMetrics.ApiCallTime += duration
if err != nil {
atomic.AddInt64(&mc.PerformanceMetrics.ApiErrors, 1)
mc.RecordError("api", err.Error(), "api_call")
}
if retryCount > 0 {
atomic.AddInt64(&mc.PerformanceMetrics.ApiRetries, int64(retryCount))
}
mc.PerformanceMetrics.LastUpdated = time.Now()
}
// RecordConcurrency 记录并发数
func (mc *MetricsCollector) RecordConcurrency(current int64) {
atomic.StoreInt64(&mc.PerformanceMetrics.ConcurrentRequests, current)
if current > atomic.LoadInt64(&mc.PerformanceMetrics.MaxConcurrency) {
atomic.StoreInt64(&mc.PerformanceMetrics.MaxConcurrency, current)
}
mc.PerformanceMetrics.LastUpdated = time.Now()
}
// RecordError 记录错误
func (mc *MetricsCollector) RecordError(errorType, message, context string) {
mc.mu.Lock()
defer mc.mu.Unlock()
// 分类统计
switch errorType {
case "network":
atomic.AddInt64(&mc.ErrorMetrics.NetworkErrors, 1)
case "database":
atomic.AddInt64(&mc.ErrorMetrics.DatabaseErrors, 1)
case "business":
atomic.AddInt64(&mc.ErrorMetrics.BusinessErrors, 1)
default:
atomic.AddInt64(&mc.ErrorMetrics.SystemErrors, 1)
}
// 记录最近错误
errorRecord := ErrorRecord{
Timestamp: time.Now(),
Type: errorType,
Message: message,
Context: context,
}
// 保持最近100个错误
if len(mc.ErrorMetrics.RecentErrors) >= 100 {
mc.ErrorMetrics.RecentErrors = mc.ErrorMetrics.RecentErrors[1:]
}
mc.ErrorMetrics.RecentErrors = append(mc.ErrorMetrics.RecentErrors, errorRecord)
mc.ErrorMetrics.LastUpdated = time.Now()
}
// RecordRetry 记录重试
func (mc *MetricsCollector) RecordRetry(success bool) {
atomic.AddInt64(&mc.ErrorMetrics.RetryAttempts, 1)
if success {
atomic.AddInt64(&mc.ErrorMetrics.RetrySuccesses, 1)
} else {
atomic.AddInt64(&mc.ErrorMetrics.RetryFailures, 1)
}
mc.ErrorMetrics.LastUpdated = time.Now()
}
// collectSystemMetrics 收集系统指标
func (mc *MetricsCollector) collectSystemMetrics() {
mc.mu.Lock()
defer mc.mu.Unlock()
// 获取内存统计
var m runtime.MemStats
runtime.ReadMemStats(&m)
mc.SystemMetrics.MemoryUsage = m.Alloc
mc.SystemMetrics.GoroutineCount = runtime.NumGoroutine()
mc.SystemMetrics.GcCount = m.NumGC
mc.SystemMetrics.GcPauseMs = uint64(m.PauseNs[(m.NumGC+255)%256]) / 1000000
mc.SystemMetrics.LastUpdated = time.Now()
}
// checkAlerts 检查告警
func (mc *MetricsCollector) checkAlerts() {
thresholds := mc.config.MonitorConfig.AlertThresholds
// 检查订单失败率
if mc.OrderMetrics.TotalProcessed > 0 {
failureRate := float64(mc.OrderMetrics.FailedOrders) / float64(mc.OrderMetrics.TotalProcessed) * 100
if failureRate > thresholds.OrderFailureRate {
logger.Warnf("订单失败率告警: %.2f%% (阈值: %.2f%%)", failureRate, thresholds.OrderFailureRate)
}
}
// 检查持仓同步失败率
if mc.PositionMetrics.SyncAttempts > 0 {
syncFailureRate := float64(mc.PositionMetrics.SyncFailures) / float64(mc.PositionMetrics.SyncAttempts) * 100
if syncFailureRate > thresholds.PositionSyncFailureRate {
logger.Warnf("持仓同步失败率告警: %.2f%% (阈值: %.2f%%)", syncFailureRate, thresholds.PositionSyncFailureRate)
}
}
// 检查API失败率
if mc.PerformanceMetrics.ApiCalls > 0 {
apiFailureRate := float64(mc.PerformanceMetrics.ApiErrors) / float64(mc.PerformanceMetrics.ApiCalls) * 100
if apiFailureRate > thresholds.ApiFailureRate {
logger.Warnf("API失败率告警: %.2f%% (阈值: %.2f%%)", apiFailureRate, thresholds.ApiFailureRate)
}
}
// 检查内存使用率
if mc.SystemMetrics.MemoryPercent > thresholds.MemoryUsageThreshold {
logger.Warnf("内存使用率告警: %.2f%% (阈值: %.2f%%)", mc.SystemMetrics.MemoryPercent, thresholds.MemoryUsageThreshold)
}
// 检查CPU使用率
if mc.SystemMetrics.CpuPercent > thresholds.CpuUsageThreshold {
logger.Warnf("CPU使用率告警: %.2f%% (阈值: %.2f%%)", mc.SystemMetrics.CpuPercent, thresholds.CpuUsageThreshold)
}
}
// GetMetrics 获取所有指标
func (mc *MetricsCollector) GetMetrics() map[string]interface{} {
mc.mu.RLock()
defer mc.mu.RUnlock()
return map[string]interface{}{
"order_metrics": mc.OrderMetrics,
"position_metrics": mc.PositionMetrics,
"take_profit_metrics": mc.TakeProfitMetrics,
"performance_metrics": mc.PerformanceMetrics,
"error_metrics": mc.ErrorMetrics,
"system_metrics": mc.SystemMetrics,
"uptime": time.Since(mc.startTime).String(),
"collected_at": time.Now(),
}
}
// GetSummary 获取指标摘要
func (mc *MetricsCollector) GetSummary() map[string]interface{} {
mc.mu.RLock()
defer mc.mu.RUnlock()
summary := make(map[string]interface{})
// 订单摘要
if mc.OrderMetrics.TotalProcessed > 0 {
successRate := float64(mc.OrderMetrics.SuccessfulOrders) / float64(mc.OrderMetrics.TotalProcessed) * 100
avgResponseTime := mc.OrderMetrics.TotalResponseTime / time.Duration(mc.OrderMetrics.TotalProcessed)
summary["order_success_rate"] = fmt.Sprintf("%.2f%%", successRate)
summary["avg_response_time"] = avgResponseTime.String()
summary["total_orders"] = mc.OrderMetrics.TotalProcessed
}
// 持仓摘要
if mc.PositionMetrics.SyncAttempts > 0 {
syncSuccessRate := float64(mc.PositionMetrics.SyncSuccesses) / float64(mc.PositionMetrics.SyncAttempts) * 100
summary["position_sync_rate"] = fmt.Sprintf("%.2f%%", syncSuccessRate)
summary["position_mismatches"] = mc.PositionMetrics.PositionMismatches
}
// 性能摘要
summary["current_concurrency"] = mc.PerformanceMetrics.ConcurrentRequests
summary["max_concurrency"] = mc.PerformanceMetrics.MaxConcurrency
summary["total_api_calls"] = mc.PerformanceMetrics.ApiCalls
summary["total_db_queries"] = mc.PerformanceMetrics.DbQueries
// 系统摘要
summary["memory_usage_mb"] = mc.SystemMetrics.MemoryUsage / 1024 / 1024
summary["goroutine_count"] = mc.SystemMetrics.GoroutineCount
summary["uptime"] = time.Since(mc.startTime).String()
return summary
}
// 全局指标收集器实例
var GlobalMetricsCollector *MetricsCollector
// InitMetricsCollector 初始化指标收集器
func InitMetricsCollector(config *OptimizedConfig) {
GlobalMetricsCollector = NewMetricsCollector(config)
GlobalMetricsCollector.Start()
}
// GetMetricsCollector 获取全局指标收集器
func GetMetricsCollector() *MetricsCollector {
return GlobalMetricsCollector
}

View File

@ -1,10 +1,15 @@
package binanceservice
import (
"fmt"
"go-admin/app/admin/models"
DbModels "go-admin/app/admin/models"
"go-admin/pkg/utility"
"go-admin/pkg/utility/snowflakehelper"
"time"
"github.com/go-admin-team/go-admin-core/logger"
"github.com/jinzhu/copier"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
@ -129,7 +134,7 @@ func GetTotalLossAmount(db *gorm.DB, mainId int) (decimal.Decimal, error) {
return totalLossAmountU, nil
}
// 获取交易对的 委托中的止盈止损
// 获取交易对的 委托中的止盈止损、减仓单
// mainId 主单id
// symbolType 交易对类型
func GetSymbolTakeAndStop(db *gorm.DB, mainId int, symbolType int) ([]models.LinePreOrder, error) {
@ -170,3 +175,69 @@ func GetChildTpOrder(db *gorm.DB, pid int) (int, error) {
return int(count), nil
}
// 创建减仓后减仓单
func CreateReduceReduceOrder(db *gorm.DB, pid int, price, num decimal.Decimal, priceDigit int) (models.LinePreOrder, error) {
var preOrder models.LinePreOrder
var result models.LinePreOrder
var ext models.LinePreOrderExt
if err := db.Model(&models.LinePreOrder{}).Preload("Childs").Where("id =? ", pid).First(&preOrder).Error; err != nil {
return preOrder, err
}
if err := db.Model(&models.LinePreOrderExt{}).Where("order_id =? ", pid).Find(&ext).Error; err != nil {
return preOrder, err
}
copier.Copy(&result, &preOrder)
result.Id = 0
result.OrderSn = utility.Int64ToString(snowflakehelper.GetOrderId())
result.Status = 0
result.CreatedAt = time.Now()
result.TriggerTime = nil
result.UpdatedAt = time.Now()
result.BuyPrice = decimal.Zero.String()
result.Price = price.String()
result.Num = num.String()
result.ReduceOrderId = preOrder.Id
for index := range result.Childs {
result.Childs[index].Id = 0
result.Childs[index].OrderSn = utility.Int64ToString(snowflakehelper.GetOrderId())
result.Childs[index].Status = 0
result.Childs[index].CreatedAt = time.Now()
result.Childs[index].TriggerTime = nil
result.Childs[index].UpdatedAt = time.Now()
result.Childs[index].BuyPrice = decimal.Zero.String()
var pricePercent decimal.Decimal
if result.Childs[index].OrderType == 1 && ext.TakeProfitRatio.Cmp(decimal.Zero) > 0 {
// 减仓单卖出
if preOrder.Site == "SELL" {
pricePercent = decimal.NewFromInt(100).Add(ext.TakeProfitRatio).Div(decimal.NewFromInt(100))
} else {
pricePercent = decimal.NewFromInt(100).Sub(ext.TakeProfitRatio).Div(decimal.NewFromInt(100))
}
} else if result.Childs[index].OrderType == 2 && ext.StopLossRatio.Cmp(decimal.Zero) > 0 {
if preOrder.Site == "SELL" {
pricePercent = decimal.NewFromInt(100).Sub(ext.StopLossRatio).Div(decimal.NewFromInt(100))
} else {
pricePercent = decimal.NewFromInt(100).Add(ext.StopLossRatio).Div(decimal.NewFromInt(100))
}
}
//重新计算止盈止损价
if pricePercent.Cmp(decimal.Zero) > 0 {
result.Childs[index].Price = price.Mul(pricePercent).Truncate(int32(priceDigit)).String()
}
}
if err := db.Create(&result).Error; err != nil {
return result, fmt.Errorf("复制减仓单失败pid:%d err:%v", pid, err)
}
return result, nil
}

View File

@ -0,0 +1,178 @@
package binanceservice
import (
"fmt"
"go-admin/app/admin/models"
"go-admin/app/admin/service/dto"
"go-admin/common/const/rediskey"
"go-admin/common/global"
"go-admin/common/helper"
models2 "go-admin/models"
"go-admin/pkg/utility"
"github.com/bytedance/sonic"
"github.com/go-admin-team/go-admin-core/logger"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
// 缓存减仓节点和重新生成
// reduceOrder:原始减仓单
// reduceOrderStrategy:减仓策略
func CacheOrderStrategyAndReCreate(db *gorm.DB, reduceOrder ReduceListItem, symbolType int, tradeSet models2.TradeSet, setting models.LineSystemSetting) error {
reduceOrderStrategy := models.LineOrderReduceStrategy{}
var key string
if symbolType == 1 {
key = fmt.Sprintf(rediskey.SpotOrderReduceStrategyList, global.EXCHANGE_BINANCE)
} else {
key = fmt.Sprintf(rediskey.FutOrderReduceStrategyList, global.EXCHANGE_BINANCE)
}
if err := db.Model(&reduceOrderStrategy).Where("order_id =?", reduceOrder.Id).Find(&reduceOrderStrategy).Error; err != nil {
logger.Errorf("获取减仓策略失败,err:%v", err)
return err
}
if reduceOrderStrategy.Id > 0 {
items := make([]models.LineReduceStrategyItem, 0)
strategyCache := dto.LineOrderReduceStrategyResp{
OrderId: reduceOrder.Id,
Symbol: reduceOrder.Symbol,
MainId: reduceOrder.MainId,
Side: reduceOrder.Side,
}
sonic.Unmarshal([]byte(reduceOrderStrategy.ItemContent), &items)
for _, item := range items {
var rate decimal.Decimal
var triggerRate decimal.Decimal
if reduceOrder.Side == "SELL" {
rate = (decimal.NewFromInt(100).Sub(item.LossPercent)).Div(decimal.NewFromInt(100))
if setting.ReduceEarlyTriggerPercent.Cmp(decimal.Zero) > 0 {
triggerRate = rate.Add(setting.ReduceEarlyTriggerPercent.Div(decimal.NewFromInt(100)))
} else {
triggerRate = rate
}
} else {
rate = (decimal.NewFromInt(100).Add(item.LossPercent)).Div(decimal.NewFromInt(100))
if setting.ReduceEarlyTriggerPercent.Cmp(decimal.Zero) > 0 {
triggerRate = rate.Sub(setting.ReduceEarlyTriggerPercent.Div(decimal.NewFromInt(100)))
} else {
triggerRate = rate
}
}
price := reduceOrder.Price.Mul(rate).Truncate(int32(tradeSet.PriceDigit))
triggerPrice := reduceOrder.Price.Mul(triggerRate).Truncate(int32(tradeSet.PriceDigit))
num := reduceOrder.Num
//百分比大于0就重新计算 否则就是主减仓单数量
if item.QuantityPercent.Cmp(decimal.Zero) > 0 {
num = reduceOrder.Num.Mul(item.QuantityPercent.Div(decimal.NewFromInt(100))).Truncate(int32(tradeSet.AmountDigit))
}
strategyCache.Items = append(strategyCache.Items, dto.LineOrderReduceStrategyRespItem{
LossPercent: item.LossPercent,
OrderType: item.OrderType,
Price: price,
TriggerPrice: triggerPrice,
Num: num,
})
}
str, _ := sonic.MarshalString(strategyCache)
if str != "" {
if err := helper.DefaultRedis.HSetField(key, utility.IntToString(reduceOrder.Id), str); err != nil {
logger.Errorf("减仓单缓存减仓策略,err:%v", err)
}
}
}
return nil
}
// 根据订单id获取订单减仓策略
func GetReduceStrategyById(db *gorm.DB, orderId int) (models.LineOrderReduceStrategy, error) {
result := models.LineOrderReduceStrategy{}
if err := db.Model(result).Where("order_id =?", orderId).Select("id", "order_id").First(&result).Error; err != nil {
return result, err
}
return result, nil
}
// 减仓单成功回调
func ReduceCallBack(db *gorm.DB, preOrder *models.LinePreOrder) error {
reduceOrderId := preOrder.Id
var key string
if preOrder.ReduceOrderId > 0 {
reduceOrderId = preOrder.ReduceOrderId
}
switch preOrder.SymbolType {
case 1:
key = fmt.Sprintf(rediskey.SpotOrderReduceStrategyList, global.EXCHANGE_BINANCE)
case 2:
key = fmt.Sprintf(rediskey.FutOrderReduceStrategyList, global.EXCHANGE_BINANCE)
default:
return fmt.Errorf("交易对类型错误")
}
arrays, _ := helper.DefaultRedis.HGetAllFields(key)
cache := dto.LineOrderReduceStrategyResp{}
for _, v := range arrays {
sonic.Unmarshal([]byte(v), &cache)
if cache.OrderId == reduceOrderId {
if err := helper.DefaultRedis.HDelField(key, utility.IntToString(cache.OrderId)); err != nil {
logger.Errorf("移除减仓单减仓策略失败redis err:%v", err)
}
}
}
orderReduceStrategy, _ := GetReduceStrategyById(db, reduceOrderId)
if orderReduceStrategy.Id > 0 {
if err := db.Model(&orderReduceStrategy).Update("actived", 1).Error; err != nil {
logger.Errorf("更新减仓单减仓策略状态失败 order_id:%d err:%v", orderReduceStrategy.OrderId, err)
}
}
return nil
}
// 移除减仓后减仓策略
// mainId 主单id
// symbolType 交易对类型
func RemoveReduceReduceCacheByMainId(mainId int, symbolType int) error {
var key string
switch symbolType {
case 1:
key = fmt.Sprintf(rediskey.SpotOrderReduceStrategyList, global.EXCHANGE_BINANCE)
case 2:
key = fmt.Sprintf(rediskey.FutOrderReduceStrategyList, global.EXCHANGE_BINANCE)
default:
return fmt.Errorf("交易对类型错误")
}
arrays, _ := helper.DefaultRedis.HGetAllFields(key)
cache := dto.LineOrderReduceStrategyResp{}
for _, v := range arrays {
sonic.Unmarshal([]byte(v), &cache)
if cache.MainId == mainId {
if err := helper.DefaultRedis.HDelField(key, utility.IntToString(cache.OrderId)); err != nil {
logger.Errorf("移除减仓单减仓策略失败redis err:%v", err)
}
}
}
return nil
}

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1,64 @@
package binanceservice
import (
"fmt"
"go-admin/common/const/rediskey"
"go-admin/common/global"
"go-admin/common/helper"
"go-admin/models"
"go-admin/services/cacheservice"
"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 TestSpotJudge(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)
helper.InitLockRedisConn("127.0.0.1:6379", "", "2")
// tradeSet := models.TradeSet{
// Coin: "ADA",
// Currency: "USDT",
// LastPrice: "0.516",
// }
key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE)
item := `{"id":66,"apiId":49,"mainId":63,"pid":63,"symbol":"ADAUSDT","price":"0.5912","side":"SELL","num":"12.6","orderSn":"397919643961917440"}`
reduceOrder := ReduceListItem{}
spotApi := SpotRestApi{}
setting, err := cacheservice.GetSystemSetting(db)
if err != nil {
logger.Error("获取系统设置失败")
return
}
if err := sonic.Unmarshal([]byte(item), &reduceOrder); err != nil {
logger.Error("反序列化失败")
return
}
// JudgeFuturesReduce(tradeSet)
SpotReduceTrigger(db, reduceOrder, spotApi, setting, key, item, false, 0)
}
// 测试减仓后减仓触发
func TestSpotReduceReduce(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")
tradeSet := models.TradeSet{
Coin: "ADA",
Currency: "USDT",
LastPrice: "0.5793",
PriceDigit: 4,
}
// JudgeFuturesReduce(tradeSet)
JudgeSpotReduce(tradeSet)
}

View File

@ -11,6 +11,7 @@ import (
"go-admin/common/helper"
"go-admin/models"
"go-admin/pkg/utility"
"go-admin/services/cacheservice"
"strings"
"time"
@ -141,14 +142,14 @@ func JudgeSpotStopLoss(trade models.TradeSet) {
db := GetDBConnection()
spotApi := SpotRestApi{}
setting, err := GetSystemSetting(db)
setting, err := cacheservice.GetSystemSetting(db)
if err != nil {
log.Error("获取系统设置失败")
return
}
tradeSet, err := GetTradeSet(trade.Coin+trade.Currency, 0)
tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, trade.Coin+trade.Currency, 0)
if err != nil {
log.Error("获取交易设置失败")
@ -256,23 +257,94 @@ func SpotStopLossTrigger(db *gorm.DB, stopOrder dto.StopLossRedisList, spotApi S
// 判断是否触发现货减仓
func JudgeSpotReduce(trade models.TradeSet) {
key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE)
reduceVal, _ := helper.DefaultRedis.GetAllList(key)
if len(reduceVal) == 0 {
return
}
db := GetDBConnection()
spotApi := SpotRestApi{}
setting, err := GetSystemSetting(db)
setting, err := cacheservice.GetSystemSetting(db)
if err != nil {
log.Error("获取系统设置失败")
return
}
reduceOrder := ReduceListItem{}
tradePrice, _ := decimal.NewFromString(trade.LastPrice)
//减仓单减仓策略
reduceReduceListKey := fmt.Sprintf(rediskey.SpotOrderReduceStrategyList, global.EXCHANGE_BINANCE)
orderReduceVal, _ := helper.DefaultRedis.HGetAllFields(reduceReduceListKey)
reduceOrderStrategy := dto.LineOrderReduceStrategyResp{}
for _, item := range orderReduceVal {
sonic.Unmarshal([]byte(item), &reduceOrderStrategy)
for index, item2 := range reduceOrderStrategy.Items {
if reduceOrderStrategy.Symbol == trade.Coin+trade.Currency {
//买入
if strings.ToUpper(reduceOrderStrategy.Side) == "SELL" &&
item2.TriggerPrice.Cmp(tradePrice) >= 0 &&
item2.TriggerPrice.Cmp(decimal.Zero) > 0 &&
tradePrice.Cmp(decimal.Zero) > 0 {
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.ReduceStrategySpotTriggerLock, reduceOrder.ApiId, reduceOrder.Symbol), 50, 15, 100*time.Millisecond)
if ok, err := lock.AcquireWait(context.Background()); err != nil {
log.Error("获取锁失败", err)
return
} else if ok {
defer lock.Release()
hasrecord, _ := helper.DefaultRedis.HExists(reduceReduceListKey, utility.IntTostring(reduceOrderStrategy.OrderId), item)
if !hasrecord {
log.Debug("减仓缓存中不存在", item)
return
}
order, err := CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit)
if err != nil {
log.Errorf("%d 生成订单失败", reduceOrderStrategy.OrderId)
}
reduceOrder.ApiId = order.ApiId
reduceOrder.Id = order.Id
reduceOrder.Pid = order.Pid
reduceOrder.MainId = order.MainId
reduceOrder.Symbol = order.Symbol
reduceOrder.Side = reduceOrderStrategy.Side
reduceOrder.OrderSn = order.OrderSn
reduceOrder.Price = item2.Price
reduceOrder.Num = item2.Num
if SpotReduceTrigger(db, reduceOrder, spotApi, setting, reduceReduceListKey, item, true, reduceOrderStrategy.OrderId) {
reduceOrderStrategy.Items[index].Actived = true
allActive := true
orderId := utility.IntToString(reduceOrderStrategy.OrderId)
for _, item3 := range reduceOrderStrategy.Items {
if !item3.Actived {
allActive = false
break
}
}
if allActive {
if err := helper.DefaultRedis.HDelField(reduceReduceListKey, orderId); err != nil {
log.Errorf("删除redis reduceReduceListKey失败 %s", err.Error())
}
} else {
str, _ := sonic.MarshalString(reduceOrderStrategy)
if err := helper.DefaultRedis.HSetField(reduceReduceListKey, orderId, str); err != nil {
log.Errorf("更新redis reduceReduceListKey失败 %s", err.Error())
}
}
}
}
}
}
}
}
//减仓单
reduceVal, _ := helper.DefaultRedis.GetAllList(key)
for _, item := range reduceVal {
reduceOrder := ReduceListItem{}
if err := sonic.Unmarshal([]byte(item), &reduceOrder); err != nil {
log.Error("反序列化失败")
continue
@ -280,59 +352,67 @@ func JudgeSpotReduce(trade models.TradeSet) {
if reduceOrder.Symbol == trade.Coin+trade.Currency {
orderPrice := reduceOrder.Price
tradePrice, _ := decimal.NewFromString(trade.LastPrice)
//买入
if strings.ToUpper(reduceOrder.Side) == "SELL" &&
orderPrice.Cmp(tradePrice) >= 0 &&
orderPrice.Cmp(decimal.Zero) > 0 &&
tradePrice.Cmp(decimal.Zero) > 0 {
SpotReduceTrigger(db, reduceOrder, spotApi, setting, key, item)
SpotReduceTrigger(db, reduceOrder, spotApi, setting, key, item, false, 0)
}
}
}
}
// 触发现货减仓
func SpotReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, spotApi SpotRestApi, setting DbModels.LineSystemSetting, key, item string) {
tradeSet, err := GetTradeSet(reduceOrder.Symbol, 0)
// isStrategy 是否是策略减仓单
// reduceId 策略主减仓单id
func SpotReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, spotApi SpotRestApi, setting DbModels.LineSystemSetting, key, item string, isStrategy bool, reduceId int) bool {
tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reduceOrder.Symbol, 0)
result := true
if err != nil {
log.Error("获取交易设置失败")
return
return false
}
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, reduceOrder.ApiId, reduceOrder.Symbol), 20, 5, 100*time.Millisecond)
if ok, err := lock.AcquireWait(context.Background()); err != nil {
log.Error("获取锁失败", err)
return
return false
} else if ok {
defer lock.Release()
takeOrders := make([]DbModels.LinePreOrder, 0)
if err := db.Model(&DbModels.LinePreOrder{}).Where("main_id =? AND order_type =1 AND status IN (1,5)", reduceOrder.MainId).Find(&takeOrders).Error; err != nil {
if err := db.Model(&DbModels.LinePreOrder{}).Where("main_id =? AND order_type IN (1,2,4) AND status IN (1,5)", reduceOrder.MainId).Find(&takeOrders).Error; err != nil {
log.Error("查询止盈单失败")
return
return false
}
var hasrecord bool
if isStrategy {
hasrecord, _ = helper.DefaultRedis.HExists(key, utility.IntToString(reduceId), item)
} else {
hasrecord, _ = helper.DefaultRedis.IsElementInList(key, item)
}
hasrecord, _ := helper.DefaultRedis.IsElementInList(key, item)
if !hasrecord {
log.Debug("减仓缓存中不存在", item)
return
return false
}
apiInfo, _ := GetApiInfo(reduceOrder.ApiId)
if apiInfo.Id == 0 {
log.Error("现货减仓 查询api用户不存在")
return
return false
}
for _, takeOrder := range takeOrders {
err := CancelOpenOrderByOrderSnLoop(apiInfo, takeOrder.Symbol, takeOrder.OrderSn)
if err != nil {
log.Error("现货止盈撤单失败", err)
return
log.Error("现货撤单失败", err)
return false
}
}
price := reduceOrder.Price.Mul(decimal.NewFromInt(1).Sub(setting.ReducePremium.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit))
@ -349,6 +429,7 @@ func SpotReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, spotApi SpotRest
}
if err := spotApi.OrderPlaceLoop(db, params, 3); err != nil {
result = false
log.Errorf("现货减仓挂单失败 id%s err:%v", reduceOrder.Id, err)
if err2 := db.Model(&DbModels.LinePreOrder{}).
@ -366,14 +447,23 @@ func SpotReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, spotApi SpotRest
Updates(map[string]interface{}{"status": 1}).Error; err != nil {
log.Errorf("修改现货减仓状态失败 id%s err:%v", reduceOrder.Id, err)
}
//处理减仓单减仓策略
if err := CacheOrderStrategyAndReCreate(db, reduceOrder, 1, tradeSet, setting); err != nil {
log.Errorf("现货减仓 处理减仓策略失败 id:%v err:%v", reduceOrder.Id, err)
}
}
if _, err := helper.DefaultRedis.LRem(key, item); err != nil {
result = false
log.Errorf("现货减仓 删除缓存失败 id:%v err:%v", reduceOrder.Id, err)
}
} else {
log.Error("获取锁失败")
result = false
}
return result
}
// 判断现货加仓
@ -414,8 +504,8 @@ func SpotAddPositionTrigger(db *gorm.DB, v *AddPositionList, item string, spotAp
} else if ok {
defer lock.Release()
setting, _ := GetSystemSetting(db)
tradeSet, _ := GetTradeSet(v.Symbol, 0)
setting, _ := cacheservice.GetSystemSetting(db)
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, v.Symbol, 0)
if tradeSet.LastPrice == "" {
log.Errorf("现货加仓触发 查询交易对失败 交易对:%s ordersn:%s", v.Symbol, v.OrderSn)

View File

@ -13,6 +13,7 @@ import (
models2 "go-admin/models"
"go-admin/models/positiondto"
"go-admin/pkg/utility"
"go-admin/services/cacheservice"
"go-admin/services/positionservice"
"strconv"
"strings"
@ -20,7 +21,6 @@ import (
"github.com/bytedance/sonic"
"github.com/go-admin-team/go-admin-core/logger"
log "github.com/go-admin-team/go-admin-core/logger"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
@ -28,7 +28,7 @@ import (
/*
订单回调
*/
func ChangeSpotOrder(mapData map[string]interface{}) {
func ChangeSpotOrder(mapData map[string]interface{}, apiKey string) {
// 检查订单号是否存在
orderSn, ok := mapData["c"]
originOrderSn := mapData["C"] //取消操作 代表原始订单号
@ -165,25 +165,34 @@ func handleMainReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
return
}
tradeSet, err := GetTradeSet(preOrder.Symbol, 0)
tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 0)
if err != nil {
logger.Errorf("handleMainReduceFilled 获取交易对设置失败,订单号:%s", preOrder.OrderSn)
return
}
//修改减仓单减仓策略状态
ReduceCallBack(db, preOrder)
orderExt := models.LinePreOrderExt{}
positionData := savePosition(db, preOrder)
if err := db.Model(&DbModels.LinePreOrderStatus{}).Where("order_id =? ", preOrder.MainId).Update("reduce_status", 1).Error; err != nil {
logger.Errorf("handleMainReduceFilled 更新主单减仓状态失败,订单号:%s", preOrder.OrderSn)
}
db.Model(&orderExt).Where("order_id =?", preOrder.Id).Find(&orderExt)
//策略减仓单 获取主减仓单拓展信息
if preOrder.ReduceOrderId > 0 {
db.Model(&orderExt).Where("order_id =?", preOrder.ReduceOrderId).Find(&orderExt)
} else {
db.Model(&orderExt).Where("order_id =?", preOrder.Id).Find(&orderExt)
}
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotReduceCallback, preOrder.ApiId, preOrder.Symbol), 120, 20, 100*time.Millisecond)
if ok, err := lock.AcquireWait(context.Background()); err != nil {
log.Error("获取锁失败", err)
logger.Error("获取锁失败", err)
return
} else if ok {
defer lock.Release()
@ -278,7 +287,7 @@ func nextSpotReduceTrigger(db *gorm.DB, mainId int, totalNum decimal.Decimal, tr
nextExt := DbModels.LinePreOrderExt{}
// var percentag decimal.Decimal
if err := db.Model(&models.LinePreOrder{}).Where("main_id =? AND order_type =4 AND status=0", mainId).Order("rate asc").First(&nextOrder).Error; err != nil {
if err := db.Model(&models.LinePreOrder{}).Where("main_id =? AND order_type =4 AND status=0 AND reduce_order_id =0", mainId).Order("rate asc").First(&nextOrder).Error; err != nil {
logger.Errorf("获取下一个单失败 err:%v", err)
return true
}
@ -546,6 +555,9 @@ func removeSpotLossAndAddPosition(mainId int, orderSn string) {
addPosition := AddPositionList{}
reduce := ReduceListItem{}
//移除减仓后减仓策略
RemoveReduceReduceCacheByMainId(mainId, 1)
//止损缓存
for _, v := range stoplossVal {
sonic.Unmarshal([]byte(v), &stoploss)
@ -731,7 +743,7 @@ func updateOrderStatus(db *gorm.DB, preOrder *models.LinePreOrder, status int, r
// fist 首次止盈止损
func processTakeProfitAndStopLossOrders(db *gorm.DB, preOrder *models.LinePreOrder, positionData *positiondto.PositionDto, extOrderId int, fist bool) {
orders := []models.LinePreOrder{}
tradeSet, _ := GetTradeSet(preOrder.Symbol, 0)
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 0)
mainId := preOrder.Id
if preOrder.MainId > 0 {
@ -869,7 +881,7 @@ func processSpotReduceOrder(preOrder DbModels.LinePreOrder) {
// 处理止盈订单
func processTakeProfitOrder(db *gorm.DB, spotApi SpotRestApi, order models.LinePreOrder) {
tradeSet, _ := GetTradeSet(order.Symbol, 0)
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 0)
if tradeSet.Coin == "" {
logger.Error("获取交易对失败")
@ -941,44 +953,6 @@ func processStopLossOrder(order models.LinePreOrder) error {
return nil
}
func GetSystemSetting(db *gorm.DB) (models.LineSystemSetting, error) {
key := fmt.Sprintf(rediskey.SystemSetting)
val, _ := helper.DefaultRedis.GetString(key)
setting := models.LineSystemSetting{}
if val != "" {
sonic.UnmarshalString(val, &setting)
}
if setting.Id > 0 {
return setting, nil
}
var err error
setting, err = ResetSystemSetting(db)
if err != nil {
return setting, err
}
return setting, nil
}
func ResetSystemSetting(db *gorm.DB) (DbModels.LineSystemSetting, error) {
setting := DbModels.LineSystemSetting{}
if err := db.Model(&setting).First(&setting).Error; err != nil {
return setting, err
}
settVal, _ := sonic.MarshalString(&setting)
if settVal != "" {
if err := helper.DefaultRedis.SetString(rediskey.SystemSetting, settVal); err != nil {
logger.Error("redis添加系统设置失败", err)
}
}
return DbModels.LineSystemSetting{}, nil
}
// 循环取消订单
func CancelOpenOrderByOrderSnLoop(apiInfo DbModels.LineApiUser, symbol string, orderSn string) error {
spotApi := SpotRestApi{}

View File

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

Some files were not shown because too many files have changed in this diff Show More