Compare commits
4 Commits
Branch_反向做
...
643eab3496
| Author | SHA1 | Date | |
|---|---|---|---|
| 643eab3496 | |||
| 556a32cb7c | |||
| 7adba048c8 | |||
| 43f6b5dc4f |
@ -120,11 +120,6 @@ func (e LineApiUser) Insert(c *gin.Context) {
|
|||||||
// 设置创建人
|
// 设置创建人
|
||||||
req.SetCreateBy(user.GetUserId(c))
|
req.SetCreateBy(user.GetUserId(c))
|
||||||
|
|
||||||
if err := req.Valid(); err != nil {
|
|
||||||
e.Error(500, err, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.Insert(&req)
|
err = s.Insert(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.Error(500, err, fmt.Sprintf("创建api用户管理失败,\r\n失败信息 %s", err.Error()))
|
e.Error(500, err, fmt.Sprintf("创建api用户管理失败,\r\n失败信息 %s", err.Error()))
|
||||||
@ -158,12 +153,6 @@ func (e LineApiUser) Update(c *gin.Context) {
|
|||||||
e.Error(500, err, err.Error())
|
e.Error(500, err, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := req.Valid(); err != nil {
|
|
||||||
e.Error(500, err, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
req.SetUpdateBy(user.GetUserId(c))
|
req.SetUpdateBy(user.GetUserId(c))
|
||||||
p := actions.GetPermissionFromContext(c)
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
|
||||||
@ -278,74 +267,3 @@ func (e LineApiUser) GetMainUser(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
e.OK(list, "操作成功")
|
e.OK(list, "操作成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUnBindApiUser 获取未绑定下反单api用户
|
|
||||||
func (e LineApiUser) GetUnBindReverseApiUser(c *gin.Context) {
|
|
||||||
s := service.LineApiUser{}
|
|
||||||
req := dto.GetUnBindReverseReq{}
|
|
||||||
err := e.MakeContext(c).
|
|
||||||
MakeOrm().
|
|
||||||
Bind(&req, binding.Query, binding.Form).
|
|
||||||
MakeService(&s.Service).
|
|
||||||
Errors
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
e.Logger.Error(err)
|
|
||||||
e.Error(500, err, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
datas := make([]dto.UnBindReverseResp, 0)
|
|
||||||
|
|
||||||
err = s.GetUnBindApiUser(&req, &datas)
|
|
||||||
if err != nil {
|
|
||||||
e.Error(500, err, fmt.Sprintf("获取失败,\r\n失败信息 %s", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
e.OK(datas, "查询成功")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取反单api用户选项
|
|
||||||
func (e LineApiUser) GetReverseApiOptions(c *gin.Context) {
|
|
||||||
req := dto.GetReverseApiOptionsReq{}
|
|
||||||
s := service.LineApiUser{}
|
|
||||||
err := e.MakeContext(c).
|
|
||||||
MakeOrm().
|
|
||||||
Bind(&req).
|
|
||||||
MakeService(&s.Service).
|
|
||||||
Errors
|
|
||||||
if err != nil {
|
|
||||||
e.Logger.Error(err)
|
|
||||||
e.Error(500, err, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
list := make([]models.LineApiUser, 0)
|
|
||||||
err = s.GetReverseApiOptions(&req, &list)
|
|
||||||
if err != nil {
|
|
||||||
e.Error(500, err, fmt.Sprintf("获取失败,\r\n失败信息 %s", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e.OK(list, "操作成功")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取所有启用的反单api用户
|
|
||||||
func (e LineApiUser) GetReverseApiOptionsAll(c *gin.Context) {
|
|
||||||
s := service.LineApiUser{}
|
|
||||||
err := e.MakeContext(c).
|
|
||||||
MakeOrm().
|
|
||||||
MakeService(&s.Service).
|
|
||||||
Errors
|
|
||||||
if err != nil {
|
|
||||||
e.Logger.Error(err)
|
|
||||||
e.Error(500, err, err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
list := make([]dto.LineApiUserOptionResp, 0)
|
|
||||||
err = s.GetReverseApiOptionsAll(&list)
|
|
||||||
if err != nil {
|
|
||||||
e.Error(500, err, fmt.Sprintf("获取失败,\r\n失败信息 %s", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
e.OK(list, "操作成功")
|
|
||||||
}
|
|
||||||
|
|||||||
@ -3,9 +3,7 @@ package apis
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go-admin/common/const/rediskey"
|
|
||||||
"go-admin/common/global"
|
"go-admin/common/global"
|
||||||
"go-admin/common/helper"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@ -297,13 +295,8 @@ func (e LinePreOrder) AddPreOrder(c *gin.Context) {
|
|||||||
errs := make([]error, 0)
|
errs := make([]error, 0)
|
||||||
errStr := make([]string, 0)
|
errStr := make([]string, 0)
|
||||||
var msg string
|
var msg string
|
||||||
var tickerSymbol string
|
|
||||||
if req.SymbolType == global.SYMBOL_SPOT {
|
if err := s.AddPreOrderCheck(&req, p, &errs); err != nil {
|
||||||
tickerSymbol = helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val()
|
|
||||||
} else {
|
|
||||||
tickerSymbol = helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val()
|
|
||||||
}
|
|
||||||
if err := s.AddPreOrderCheck(&req, p, &errs, tickerSymbol); err != nil {
|
|
||||||
e.Error(500, err, err.Error())
|
e.Error(500, err, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package apis
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/go-admin-team/go-admin-core/sdk/api"
|
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||||
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
||||||
@ -194,7 +193,6 @@ func (e LinePriceLimit) Delete(c *gin.Context) {
|
|||||||
e.OK(req.GetId(), "删除成功")
|
e.OK(req.GetId(), "删除成功")
|
||||||
}
|
}
|
||||||
|
|
||||||
// aicoin数据同步
|
|
||||||
func (e LinePriceLimit) UpRange(c *gin.Context) {
|
func (e LinePriceLimit) UpRange(c *gin.Context) {
|
||||||
s := service.LinePriceLimit{}
|
s := service.LinePriceLimit{}
|
||||||
err := e.MakeContext(c).
|
err := e.MakeContext(c).
|
||||||
|
|||||||
@ -1,198 +0,0 @@
|
|||||||
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(), "删除成功")
|
|
||||||
}
|
|
||||||
@ -1,282 +0,0 @@
|
|||||||
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, "清除所有仓位成功")
|
|
||||||
}
|
|
||||||
@ -1,158 +0,0 @@
|
|||||||
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(), "修改成功")
|
|
||||||
}
|
|
||||||
@ -3,13 +3,13 @@ package apis
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go-admin/common/const/rediskey"
|
|
||||||
"go-admin/common/global"
|
"go-admin/common/global"
|
||||||
"go-admin/common/helper"
|
"go-admin/common/helper"
|
||||||
"go-admin/config/serverinit"
|
"go-admin/config/serverinit"
|
||||||
models2 "go-admin/models"
|
models2 "go-admin/models"
|
||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
"go-admin/services/binanceservice"
|
"go-admin/services/binanceservice"
|
||||||
|
"go-admin/services/cacheservice"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
@ -289,9 +289,8 @@ func (e LineSymbol) SyncSpotSymbol(c *gin.Context) {
|
|||||||
Symbol string `json:"symbol"`
|
Symbol string `json:"symbol"`
|
||||||
Price string `json:"price"`
|
Price string `json:"price"`
|
||||||
}
|
}
|
||||||
tickerSymbol := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val()
|
|
||||||
tickerSymbolMaps := make([]Ticker, 0)
|
tickerSymbolMaps, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 1)
|
||||||
sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps)
|
|
||||||
|
|
||||||
for symbol, tradeSet := range tradeSets {
|
for symbol, tradeSet := range tradeSets {
|
||||||
var lineSymbol models.LineSymbol
|
var lineSymbol models.LineSymbol
|
||||||
@ -320,11 +319,10 @@ func (e LineSymbol) SyncSpotSymbol(c *gin.Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
var tickerPrice decimal.Decimal
|
var tickerPrice decimal.Decimal
|
||||||
for _, symbolMap := range tickerSymbolMaps {
|
for _, symbolMap := range tickerSymbolMaps {
|
||||||
if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") {
|
if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") {
|
||||||
tickerPrice, _ = decimal.NewFromString(symbolMap.Price)
|
tickerPrice, _ = decimal.NewFromString(symbolMap.LastPrice)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if tickerPrice.GreaterThan(decimal.Zero) {
|
if tickerPrice.GreaterThan(decimal.Zero) {
|
||||||
@ -412,10 +410,8 @@ func (e LineSymbol) SyncFutSymbol(c *gin.Context) {
|
|||||||
Symbol string `json:"symbol"`
|
Symbol string `json:"symbol"`
|
||||||
Price string `json:"price"`
|
Price string `json:"price"`
|
||||||
}
|
}
|
||||||
tickerSymbol := helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val()
|
|
||||||
tickerSymbolMaps := make([]Ticker, 0)
|
|
||||||
sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps)
|
|
||||||
|
|
||||||
|
tickerSymbolMaps, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 2)
|
||||||
e.Orm.Model(&models.LineSymbolBlack{}).Where("type = 1").Find(&symbolBlack)
|
e.Orm.Model(&models.LineSymbolBlack{}).Where("type = 1").Find(&symbolBlack)
|
||||||
for symbol, tradeSet := range tradeSets {
|
for symbol, tradeSet := range tradeSets {
|
||||||
var lineSymbol models.LineSymbol
|
var lineSymbol models.LineSymbol
|
||||||
@ -447,7 +443,7 @@ func (e LineSymbol) SyncFutSymbol(c *gin.Context) {
|
|||||||
var tickerPrice decimal.Decimal
|
var tickerPrice decimal.Decimal
|
||||||
for _, symbolMap := range tickerSymbolMaps {
|
for _, symbolMap := range tickerSymbolMaps {
|
||||||
if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") {
|
if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") {
|
||||||
tickerPrice, _ = decimal.NewFromString(symbolMap.Price)
|
tickerPrice, _ = decimal.NewFromString(symbolMap.LastPrice)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if tickerPrice.GreaterThan(decimal.Zero) {
|
if tickerPrice.GreaterThan(decimal.Zero) {
|
||||||
|
|||||||
@ -10,8 +10,7 @@ import (
|
|||||||
"go-admin/app/admin/service/appservice"
|
"go-admin/app/admin/service/appservice"
|
||||||
"go-admin/app/admin/service/common"
|
"go-admin/app/admin/service/common"
|
||||||
"go-admin/app/admin/service/dto"
|
"go-admin/app/admin/service/dto"
|
||||||
"go-admin/common/const/rediskey"
|
"go-admin/common/global"
|
||||||
"go-admin/common/helper"
|
|
||||||
"go-admin/common/service/sysservice/authservice"
|
"go-admin/common/service/sysservice/authservice"
|
||||||
"go-admin/common/service/sysservice/sysstatuscode"
|
"go-admin/common/service/sysservice/sysstatuscode"
|
||||||
statuscode "go-admin/common/status_code"
|
statuscode "go-admin/common/status_code"
|
||||||
@ -19,7 +18,7 @@ import (
|
|||||||
"go-admin/models/coingatedto"
|
"go-admin/models/coingatedto"
|
||||||
"go-admin/pkg/cryptohelper/inttostring"
|
"go-admin/pkg/cryptohelper/inttostring"
|
||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
"go-admin/services/binanceservice"
|
"go-admin/services/cacheservice"
|
||||||
"go-admin/services/udunservice"
|
"go-admin/services/udunservice"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
@ -373,20 +372,15 @@ func (e LineUserApi) Info(c *gin.Context) {
|
|||||||
e.Logger.Error(500, err, err.Error())
|
e.Logger.Error(500, err, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//用户u的余额
|
|
||||||
var tickerData []binanceservice.Ticker
|
tickerData, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 1)
|
||||||
val := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val()
|
|
||||||
if val != "" {
|
|
||||||
sonic.Unmarshal([]byte(val), &tickerData)
|
|
||||||
}
|
|
||||||
// var usdtBalance decimal.Decimal
|
|
||||||
for i, asset := range resp {
|
for i, asset := range resp {
|
||||||
symbol := asset.Asset + "USDT"
|
symbol := asset.Asset + "USDT"
|
||||||
for _, datum := range tickerData {
|
for _, datum := range tickerData {
|
||||||
if datum.Symbol == symbol {
|
if datum.Symbol == symbol {
|
||||||
// mul := utility.StringToDecimal(datum.Price).Mul(utility.StringToDecimal(asset.Free))
|
// mul := utility.StringToDecimal(datum.Price).Mul(utility.StringToDecimal(asset.Free))
|
||||||
// usdtBalance = usdtBalance.Add(mul)
|
// usdtBalance = usdtBalance.Add(mul)
|
||||||
resp[i].UsdtValuation = datum.Price
|
resp[i].UsdtValuation = datum.LastPrice
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,6 @@ package models
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"go-admin/common/models"
|
"go-admin/common/models"
|
||||||
|
|
||||||
"github.com/shopspring/decimal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type LineApiUser struct {
|
type LineApiUser struct {
|
||||||
@ -11,20 +9,21 @@ type LineApiUser struct {
|
|||||||
|
|
||||||
ExchangeType string `json:"exchangeType" gorm:"type:varchar(20);comment:交易所类型(字典 exchange_type)"`
|
ExchangeType string `json:"exchangeType" gorm:"type:varchar(20);comment:交易所类型(字典 exchange_type)"`
|
||||||
UserId int64 `json:"userId" gorm:"type:int unsigned;comment:用户id"`
|
UserId int64 `json:"userId" gorm:"type:int unsigned;comment:用户id"`
|
||||||
|
JysId int64 `json:"jysId" gorm:"type:int;comment:关联交易所账号id"`
|
||||||
ApiName string `json:"apiName" gorm:"type:varchar(255);comment:api用户名"`
|
ApiName string `json:"apiName" gorm:"type:varchar(255);comment:api用户名"`
|
||||||
ApiKey string `json:"apiKey" gorm:"type:varchar(255);comment:apiKey"`
|
ApiKey string `json:"apiKey" gorm:"type:varchar(255);comment:apiKey"`
|
||||||
ApiSecret string `json:"apiSecret" gorm:"type:varchar(255);comment:apiSecret"`
|
ApiSecret string `json:"apiSecret" gorm:"type:varchar(255);comment:apiSecret"`
|
||||||
|
Passphrase string `json:"passphrase" gorm:"type:varchar(255);comment:api密码"`
|
||||||
IpAddress string `json:"ipAddress" gorm:"type:varchar(255);comment:代理地址"`
|
IpAddress string `json:"ipAddress" gorm:"type:varchar(255);comment:代理地址"`
|
||||||
UserPass string `json:"userPass" gorm:"type:varchar(255);comment:代码账号密码"`
|
UserPass string `json:"userPass" gorm:"type:varchar(255);comment:代码账号密码"`
|
||||||
|
AdminId int64 `json:"adminId" gorm:"type:int unsigned;comment:管理员id"`
|
||||||
Affiliation int64 `json:"affiliation" gorm:"type:int;comment:归属:1=现货,2=合约,3=现货合约"`
|
Affiliation int64 `json:"affiliation" gorm:"type:int;comment:归属:1=现货,2=合约,3=现货合约"`
|
||||||
AdminShow int64 `json:"adminShow" gorm:"type:int;comment:是否超管可见:1=是,0=否"`
|
AdminShow int64 `json:"adminShow" gorm:"type:int;comment:是否超管可见:1=是,0=否"`
|
||||||
Site string `json:"site" gorm:"type:enum('1','2','3');comment:允许下单的方向:1=多;2=空;3=多空"`
|
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=副帐号"`
|
Subordinate string `json:"subordinate" gorm:"type:enum('0','1','2');comment:从属关系:0=未绑定关系;1=主账号;2=副帐号"`
|
||||||
GroupId int64 `json:"groupId" gorm:"type:int unsigned;comment:所属组id"`
|
GroupId int64 `json:"groupId" gorm:"type:int unsigned;comment:所属组id"`
|
||||||
OpenStatus int64 `json:"openStatus" gorm:"type:int unsigned;comment:开启状态 0=关闭 1=开启"`
|
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最后通信时间
|
SpotLastTime string `json:"spotLastTime" gorm:"-"` //现货websocket最后通信时间
|
||||||
FuturesLastTime string `json:"futuresLastTime" gorm:"-"` //合约websocket最后通信时间
|
FuturesLastTime string `json:"futuresLastTime" gorm:"-"` //合约websocket最后通信时间
|
||||||
models.ModelTime
|
models.ModelTime
|
||||||
|
|||||||
@ -1,52 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,31 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -14,7 +14,7 @@ type LineStrategyTemplate struct {
|
|||||||
Percentag decimal.Decimal `json:"percentag" gorm:"type:decimal(10,2);comment:涨跌点数"`
|
Percentag decimal.Decimal `json:"percentag" gorm:"type:decimal(10,2);comment:涨跌点数"`
|
||||||
CompareType int `json:"compareType" gorm:"type:tinyint;comment:比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
|
CompareType int `json:"compareType" gorm:"type:tinyint;comment:比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
|
||||||
TimeSlotStart int `json:"timeSlotStart" gorm:"type:int;comment:时间段开始(分)"`
|
TimeSlotStart int `json:"timeSlotStart" gorm:"type:int;comment:时间段开始(分)"`
|
||||||
// TimeSlotEnd int `json:"timeSlotEnd" gorm:"type:int;comment:时间断截至(分)"`
|
TimeSlotEnd int `json:"timeSlotEnd" gorm:"type:int;comment:时间断截至(分)"`
|
||||||
models.ModelTime
|
models.ModelTime
|
||||||
models.ControlBy
|
models.ControlBy
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,11 +27,5 @@ func registerLineApiUserRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMi
|
|||||||
r.POST("bind", api.Bind) //绑定从属关系
|
r.POST("bind", api.Bind) //绑定从属关系
|
||||||
r.POST("getUser", api.GetUser) //获取未绑定的用户
|
r.POST("getUser", api.GetUser) //获取未绑定的用户
|
||||||
r.POST("getMainUser", api.GetMainUser) //获取获取主账号的用户
|
r.POST("getMainUser", api.GetMainUser) //获取获取主账号的用户
|
||||||
|
|
||||||
r.GET("unbind-reverse", api.GetUnBindReverseApiUser) //获取未绑定下反单用户
|
|
||||||
|
|
||||||
r.GET("reverse-options", api.GetReverseApiOptions) //获取可用反单api用户
|
|
||||||
|
|
||||||
r.GET("reverse-options-all", api.GetReverseApiOptionsAll) //获取全部启用的反单api用户
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,12 +1,9 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"go-admin/app/admin/models"
|
"go-admin/app/admin/models"
|
||||||
"go-admin/common/dto"
|
"go-admin/common/dto"
|
||||||
common "go-admin/common/models"
|
common "go-admin/common/models"
|
||||||
|
|
||||||
"github.com/shopspring/decimal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type LineApiUserGetPageReq struct {
|
type LineApiUserGetPageReq struct {
|
||||||
@ -53,57 +50,35 @@ type LineApiUserInsertReq struct {
|
|||||||
ApiSecret string `json:"apiSecret" comment:"apiSecret"`
|
ApiSecret string `json:"apiSecret" comment:"apiSecret"`
|
||||||
IpAddress string `json:"ipAddress" comment:"代理地址"`
|
IpAddress string `json:"ipAddress" comment:"代理地址"`
|
||||||
UserPass string `json:"userPass" comment:"代码账号密码"`
|
UserPass string `json:"userPass" comment:"代码账号密码"`
|
||||||
|
AdminId int64 `json:"adminId" comment:"管理员id"`
|
||||||
Affiliation int64 `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"`
|
Affiliation int64 `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"`
|
||||||
AdminShow int64 `json:"adminShow" comment:"是否超管可见:1=是,0=否"`
|
AdminShow int64 `json:"adminShow" comment:"是否超管可见:1=是,0=否"`
|
||||||
Site string `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"`
|
Site string `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"`
|
||||||
Subordinate string `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"`
|
Subordinate string `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"`
|
||||||
GroupId int64 `json:"groupId" comment:"所属组id"`
|
GroupId int64 `json:"groupId" comment:"所属组id"`
|
||||||
OpenStatus int64 `json:"openStatus" comment:"开启状态 0=关闭 1=开启"`
|
OpenStatus int64 `json:"openStatus" comment:"开启状态 0=关闭 1=开启"`
|
||||||
ReverseStatus int `json:"reverseStatus" comment:"反向下单状态 1-开启 2-关闭"`
|
|
||||||
ReverseApiId int `json:"reverseApiId" comment:"反向下单apiId"`
|
|
||||||
OrderProportion decimal.Decimal `json:"orderProportion" comment:"委托比例"`
|
|
||||||
common.ControlBy
|
common.ControlBy
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LineApiUserInsertReq) Valid() error {
|
|
||||||
if s.ReverseStatus == 1 {
|
|
||||||
if s.OrderProportion.Cmp(decimal.Zero) <= 0 {
|
|
||||||
return errors.New("委托比例必须大于0")
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.ReverseStatus == 1 && s.ReverseApiId == 0 {
|
|
||||||
return errors.New("反向下单api不能为空")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LineApiUserInsertReq) Generate(model *models.LineApiUser) {
|
func (s *LineApiUserInsertReq) Generate(model *models.LineApiUser) {
|
||||||
if s.Id == 0 {
|
if s.Id == 0 {
|
||||||
model.Model = common.Model{Id: s.Id}
|
model.Model = common.Model{Id: s.Id}
|
||||||
}
|
}
|
||||||
model.ExchangeType = s.ExchangeType
|
model.ExchangeType = s.ExchangeType
|
||||||
model.UserId = s.UserId
|
model.UserId = s.UserId
|
||||||
|
model.JysId = s.JysId
|
||||||
model.ApiName = s.ApiName
|
model.ApiName = s.ApiName
|
||||||
model.ApiKey = s.ApiKey
|
model.ApiKey = s.ApiKey
|
||||||
model.ApiSecret = s.ApiSecret
|
model.ApiSecret = s.ApiSecret
|
||||||
model.IpAddress = s.IpAddress
|
model.IpAddress = s.IpAddress
|
||||||
model.UserPass = s.UserPass
|
model.UserPass = s.UserPass
|
||||||
|
model.AdminId = s.AdminId
|
||||||
model.Affiliation = s.Affiliation
|
model.Affiliation = s.Affiliation
|
||||||
model.AdminShow = s.AdminShow
|
model.AdminShow = s.AdminShow
|
||||||
model.Site = s.Site
|
model.Site = s.Site
|
||||||
model.Subordinate = s.Subordinate
|
model.Subordinate = s.Subordinate
|
||||||
model.GroupId = s.GroupId
|
model.GroupId = s.GroupId
|
||||||
model.OpenStatus = s.OpenStatus
|
model.OpenStatus = s.OpenStatus
|
||||||
model.ReverseStatus = s.ReverseStatus
|
|
||||||
model.OrderProportion = s.OrderProportion
|
|
||||||
|
|
||||||
if model.ReverseStatus == 1 {
|
|
||||||
model.ReverseApiId = s.ReverseApiId
|
|
||||||
} else {
|
|
||||||
model.ReverseApiId = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
|
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,57 +96,35 @@ type LineApiUserUpdateReq struct {
|
|||||||
ApiSecret string `json:"apiSecret" comment:"apiSecret"`
|
ApiSecret string `json:"apiSecret" comment:"apiSecret"`
|
||||||
IpAddress string `json:"ipAddress" comment:"代理地址"`
|
IpAddress string `json:"ipAddress" comment:"代理地址"`
|
||||||
UserPass string `json:"userPass" comment:"代码账号密码"`
|
UserPass string `json:"userPass" comment:"代码账号密码"`
|
||||||
|
AdminId int64 `json:"adminId" comment:"管理员id"`
|
||||||
Affiliation int64 `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"`
|
Affiliation int64 `json:"affiliation" comment:"归属:1=现货,2=合约,3=现货合约"`
|
||||||
AdminShow int64 `json:"adminShow" comment:"是否超管可见:1=是,0=否"`
|
AdminShow int64 `json:"adminShow" comment:"是否超管可见:1=是,0=否"`
|
||||||
Site string `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"`
|
Site string `json:"site" comment:"允许下单的方向:1=多;2=空;3=多空"`
|
||||||
Subordinate string `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"`
|
Subordinate string `json:"subordinate" comment:"从属关系:0=未绑定关系;1=主账号;2=副帐号"`
|
||||||
GroupId int64 `json:"groupId" comment:"所属组id"`
|
GroupId int64 `json:"groupId" comment:"所属组id"`
|
||||||
OpenStatus int64 `json:"openStatus" comment:"开启状态 0=关闭 1=开启"`
|
OpenStatus int64 `json:"openStatus" comment:"开启状态 0=关闭 1=开启"`
|
||||||
ReverseStatus int `json:"reverseStatus" comment:"反向下单状态 1-开启 2-关闭"`
|
|
||||||
ReverseApiId int `json:"reverseApiId" comment:"反向下单apiId"`
|
|
||||||
OrderProportion decimal.Decimal `json:"orderProportion" comment:"委托比例"`
|
|
||||||
common.ControlBy
|
common.ControlBy
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *LineApiUserUpdateReq) Valid() error {
|
|
||||||
if s.ReverseStatus == 1 {
|
|
||||||
if s.OrderProportion.Cmp(decimal.Zero) <= 0 {
|
|
||||||
return errors.New("委托比例必须大于0")
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.ReverseStatus == 1 && s.ReverseApiId == 0 {
|
|
||||||
return errors.New("反向下单api不能为空")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LineApiUserUpdateReq) Generate(model *models.LineApiUser) {
|
func (s *LineApiUserUpdateReq) Generate(model *models.LineApiUser) {
|
||||||
if s.Id == 0 {
|
if s.Id == 0 {
|
||||||
model.Model = common.Model{Id: s.Id}
|
model.Model = common.Model{Id: s.Id}
|
||||||
}
|
}
|
||||||
model.ExchangeType = s.ExchangeType
|
model.ExchangeType = s.ExchangeType
|
||||||
model.UserId = s.UserId
|
model.UserId = s.UserId
|
||||||
|
model.JysId = s.JysId
|
||||||
model.ApiName = s.ApiName
|
model.ApiName = s.ApiName
|
||||||
model.ApiKey = s.ApiKey
|
model.ApiKey = s.ApiKey
|
||||||
model.ApiSecret = s.ApiSecret
|
model.ApiSecret = s.ApiSecret
|
||||||
model.IpAddress = s.IpAddress
|
model.IpAddress = s.IpAddress
|
||||||
model.UserPass = s.UserPass
|
model.UserPass = s.UserPass
|
||||||
|
model.AdminId = s.AdminId
|
||||||
model.Affiliation = s.Affiliation
|
model.Affiliation = s.Affiliation
|
||||||
model.AdminShow = s.AdminShow
|
model.AdminShow = s.AdminShow
|
||||||
model.Site = s.Site
|
model.Site = s.Site
|
||||||
model.Subordinate = s.Subordinate
|
model.Subordinate = s.Subordinate
|
||||||
model.GroupId = s.GroupId
|
model.GroupId = s.GroupId
|
||||||
model.OpenStatus = s.OpenStatus
|
model.OpenStatus = s.OpenStatus
|
||||||
model.ReverseStatus = s.ReverseStatus
|
|
||||||
model.OrderProportion = s.OrderProportion
|
|
||||||
|
|
||||||
if model.ReverseStatus == 1 {
|
|
||||||
model.ReverseApiId = s.ReverseApiId
|
|
||||||
} else {
|
|
||||||
model.ReverseApiId = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
|
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,26 +159,3 @@ type LineApiUserBindSubordinateReq struct {
|
|||||||
type GetMainUserReq struct {
|
type GetMainUserReq struct {
|
||||||
ExchangeType string `json:"exchangeType"`
|
ExchangeType string `json:"exchangeType"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetUnBindReverseReq struct {
|
|
||||||
ExchangeType string `json:"exchangeType" form:"exchangeType"`
|
|
||||||
ApiId int `json:"apiId" form:"apiId" comment:"当前apiId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 未绑定反向下单查询返回结构体
|
|
||||||
type UnBindReverseResp struct {
|
|
||||||
Id int `json:"id"`
|
|
||||||
UserId int64 `json:"userId"`
|
|
||||||
Disabled bool `json:"disabled"`
|
|
||||||
ApiName string `json:"apiName"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetReverseApiOptionsReq struct {
|
|
||||||
Id int `json:"apiId" form:"id"`
|
|
||||||
ApiId int `json:"apiId" form:"apiId"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LineApiUserOptionResp struct {
|
|
||||||
Id int `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|||||||
@ -197,7 +197,7 @@ type LineAddPreOrderReq struct {
|
|||||||
Site string `json:"site" ` //购买方向
|
Site string `json:"site" ` //购买方向
|
||||||
BuyPrice string `json:"buy_price" vd:"$>0"` //购买金额 U
|
BuyPrice string `json:"buy_price" vd:"$>0"` //购买金额 U
|
||||||
PricePattern string `json:"price_pattern"` //价格模式
|
PricePattern string `json:"price_pattern"` //价格模式
|
||||||
Price string `json:"price"` //下单价百分比
|
Price string `json:"price" vd:"$>0"` //下单价百分比
|
||||||
Profit string `json:"profit" vd:"$>0"` //止盈价
|
Profit string `json:"profit" vd:"$>0"` //止盈价
|
||||||
ProfitNumRatio decimal.Decimal `json:"profit_num_ratio"` //止盈数量百分比
|
ProfitNumRatio decimal.Decimal `json:"profit_num_ratio"` //止盈数量百分比
|
||||||
ProfitTpTpPriceRatio decimal.Decimal `json:"profit_tp_tp_price_ratio"` //止盈后止盈价百分比
|
ProfitTpTpPriceRatio decimal.Decimal `json:"profit_tp_tp_price_ratio"` //止盈后止盈价百分比
|
||||||
@ -295,7 +295,7 @@ func (req LineAddPreOrderReq) Valid() error {
|
|||||||
return errors.New("主单减仓数量百分比不能为空")
|
return errors.New("主单减仓数量百分比不能为空")
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.PricePattern != "mixture" && (req.ReducePriceRatio.IsZero() || req.ReducePriceRatio.Cmp(decimal.NewFromInt(100)) >= 0) {
|
if req.ReducePriceRatio.IsZero() || req.ReducePriceRatio.Cmp(decimal.NewFromInt(100)) >= 0 {
|
||||||
return errors.New("主单减仓价格百分比错误")
|
return errors.New("主单减仓价格百分比错误")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -447,7 +447,7 @@ func (req LineBatchAddPreOrderReq) CheckParams() error {
|
|||||||
return errors.New("主单减仓数量百分比不能为空")
|
return errors.New("主单减仓数量百分比不能为空")
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.PricePattern != "mixture" && (req.ReducePriceRatio.Cmp(decimal.Zero) <= 0 || req.ReducePriceRatio.Cmp(decimal.NewFromInt(100)) >= 0) {
|
if req.ReducePriceRatio.Cmp(decimal.Zero) <= 0 || req.ReducePriceRatio.Cmp(decimal.NewFromInt(100)) >= 0 {
|
||||||
return errors.New("主单减仓价格百分比错误")
|
return errors.New("主单减仓价格百分比错误")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -51,7 +51,7 @@ func (s *LineReduceStrategyItem) Valid() error {
|
|||||||
return errors.New("百分比不能小于等于0")
|
return errors.New("百分比不能小于等于0")
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.QuantityPercent.Cmp(decimal.NewFromInt(100)) >= 0 {
|
if s.QuantityPercent.Cmp(decimal.NewFromInt(100)) > 0 {
|
||||||
return errors.New("数量百分比不能大于等于100")
|
return errors.New("数量百分比不能大于等于100")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,162 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,155 +0,0 @@
|
|||||||
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"`
|
|
||||||
}
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -41,8 +41,8 @@ type LineStrategyTemplateInsertReq struct {
|
|||||||
Direction int `json:"direction" comment:"涨跌方向 1-涨 2-跌"`
|
Direction int `json:"direction" comment:"涨跌方向 1-涨 2-跌"`
|
||||||
Percentag decimal.Decimal `json:"percentag" comment:"涨跌点数"`
|
Percentag decimal.Decimal `json:"percentag" comment:"涨跌点数"`
|
||||||
CompareType int `json:"compareType" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
|
CompareType int `json:"compareType" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
|
||||||
TimeSlotStart int `json:"timeSlotStart" comment:"时间段(分)"`
|
TimeSlotStart int `json:"timeSlotStart" comment:"时间段开始(分)"`
|
||||||
// TimeSlotEnd int `json:"timeSlotEnd" comment:"时间断截至(分)"`
|
TimeSlotEnd int `json:"timeSlotEnd" comment:"时间断截至(分)"`
|
||||||
common.ControlBy
|
common.ControlBy
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,13 +63,13 @@ func (s *LineStrategyTemplateInsertReq) Valid() error {
|
|||||||
return errors.New("比较类型不合法")
|
return errors.New("比较类型不合法")
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.TimeSlotStart < 0 {
|
if s.TimeSlotStart < 0 || s.TimeSlotEnd > 59 {
|
||||||
return errors.New("时间段不合法")
|
return errors.New("时间段不合法")
|
||||||
}
|
}
|
||||||
|
|
||||||
// if s.TimeSlotEnd < s.TimeSlotStart {
|
if s.TimeSlotEnd < s.TimeSlotStart {
|
||||||
// return errors.New("时间段不合法")
|
return errors.New("时间段不合法")
|
||||||
// }
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -83,7 +83,7 @@ func (s *LineStrategyTemplateInsertReq) Generate(model *models.LineStrategyTempl
|
|||||||
model.Percentag = s.Percentag
|
model.Percentag = s.Percentag
|
||||||
model.CompareType = s.CompareType
|
model.CompareType = s.CompareType
|
||||||
model.TimeSlotStart = s.TimeSlotStart
|
model.TimeSlotStart = s.TimeSlotStart
|
||||||
// model.TimeSlotEnd = s.TimeSlotEnd
|
model.TimeSlotEnd = s.TimeSlotEnd
|
||||||
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
|
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,8 +97,8 @@ type LineStrategyTemplateUpdateReq struct {
|
|||||||
Direction int `json:"direction" comment:"涨跌方向 1-涨 2-跌"`
|
Direction int `json:"direction" comment:"涨跌方向 1-涨 2-跌"`
|
||||||
Percentag decimal.Decimal `json:"percentag" comment:"涨跌点数"`
|
Percentag decimal.Decimal `json:"percentag" comment:"涨跌点数"`
|
||||||
CompareType int `json:"compareType" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
|
CompareType int `json:"compareType" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
|
||||||
TimeSlotStart int `json:"timeSlotStart" comment:"时间段(分)"`
|
TimeSlotStart int `json:"timeSlotStart" comment:"时间段开始(分)"`
|
||||||
// TimeSlotEnd int `json:"timeSlotEnd" comment:"时间断截至(分)"`
|
TimeSlotEnd int `json:"timeSlotEnd" comment:"时间断截至(分)"`
|
||||||
common.ControlBy
|
common.ControlBy
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,13 +120,13 @@ func (s *LineStrategyTemplateUpdateReq) Valid() error {
|
|||||||
return errors.New("比较类型不合法")
|
return errors.New("比较类型不合法")
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.TimeSlotStart < 0 {
|
if s.TimeSlotStart < 0 || s.TimeSlotEnd > 59 {
|
||||||
return errors.New("时间段不合法")
|
return errors.New("时间段不合法")
|
||||||
}
|
}
|
||||||
|
|
||||||
// if s.TimeSlotEnd < s.TimeSlotStart {
|
if s.TimeSlotEnd < s.TimeSlotStart {
|
||||||
// return errors.New("时间段不合法")
|
return errors.New("时间段不合法")
|
||||||
// }
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -139,7 +139,7 @@ func (s *LineStrategyTemplateUpdateReq) Generate(model *models.LineStrategyTempl
|
|||||||
model.Percentag = s.Percentag
|
model.Percentag = s.Percentag
|
||||||
model.CompareType = s.CompareType
|
model.CompareType = s.CompareType
|
||||||
model.TimeSlotStart = s.TimeSlotStart
|
model.TimeSlotStart = s.TimeSlotStart
|
||||||
// model.TimeSlotEnd = s.TimeSlotEnd
|
model.TimeSlotEnd = s.TimeSlotEnd
|
||||||
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
|
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/go-admin-team/go-admin-core/logger"
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
"github.com/go-admin-team/go-admin-core/sdk/service"
|
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"go-admin/app/admin/models"
|
"go-admin/app/admin/models"
|
||||||
@ -26,43 +25,6 @@ type LineApiUser struct {
|
|||||||
service.Service
|
service.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取所有启用的反单api用户
|
|
||||||
func (e LineApiUser) GetReverseApiOptionsAll(user *[]dto.LineApiUserOptionResp) error {
|
|
||||||
var data models.LineApiUser
|
|
||||||
var datas []models.LineApiUser
|
|
||||||
|
|
||||||
if err := e.Orm.Model(&data).Where("open_status = 1 AND subordinate = '2'").Find(&datas).Error; err != nil {
|
|
||||||
e.Log.Errorf("LineApiUserService GetReverseApiOptionsAll error:%s \r\n", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, item := range datas {
|
|
||||||
*user = append(*user, dto.LineApiUserOptionResp{
|
|
||||||
Id: item.Id,
|
|
||||||
Name: item.ApiName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取可以绑定的api列表
|
|
||||||
func (e LineApiUser) GetReverseApiOptions(req *dto.GetReverseApiOptionsReq, user *[]models.LineApiUser) error {
|
|
||||||
query := e.Orm.Model(models.LineApiUser{}).
|
|
||||||
Where("subordinate ='0' and id != ?", req.Id)
|
|
||||||
|
|
||||||
if req.ApiId > 0 {
|
|
||||||
query = query.Or(" id =?", req.ApiId)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := query.Find(user).Error; err != nil {
|
|
||||||
e.Log.Errorf("LineApiUserService GetReverseApiOptions error:%s \r\n", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPage 获取LineApiUser列表
|
// GetPage 获取LineApiUser列表
|
||||||
func (e *LineApiUser) GetPage(c *dto.LineApiUserGetPageReq, p *actions.DataPermission, list *[]models.LineApiUser, count *int64) error {
|
func (e *LineApiUser) GetPage(c *dto.LineApiUserGetPageReq, p *actions.DataPermission, list *[]models.LineApiUser, count *int64) error {
|
||||||
var err error
|
var err error
|
||||||
@ -135,37 +97,13 @@ func (e *LineApiUser) Insert(c *dto.LineApiUserInsertReq) error {
|
|||||||
var err error
|
var err error
|
||||||
var data models.LineApiUser
|
var data models.LineApiUser
|
||||||
c.Generate(&data)
|
c.Generate(&data)
|
||||||
err = e.Orm.Transaction(func(tx *gorm.DB) error {
|
err = e.Orm.Create(&data).Error
|
||||||
if c.ReverseApiId > 0 {
|
|
||||||
var count int64
|
|
||||||
if err2 := tx.Model(models.LineApiUser{}).Where("subordinate <> '0' AND id=?", c.ReverseApiId).Count(&count).Error; err2 != nil {
|
|
||||||
return err2
|
|
||||||
}
|
|
||||||
if count > 0 {
|
|
||||||
return errors.New("反向api已被使用")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err2 := tx.Model(models.LineApiUser{}).Where("id =?", c.ReverseApiId).Update("subordinate", '2').Error; err2 != nil {
|
|
||||||
return err2
|
|
||||||
}
|
|
||||||
data.Subordinate = "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
if err2 := tx.Create(&data).Error; err2 != nil {
|
|
||||||
return err2
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.Log.Errorf("LineApiUserService Insert error:%s \r\n", err)
|
e.Log.Errorf("LineApiUserService Insert error:%s \r\n", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err2 := e.CacheRelation(); err2 != nil {
|
e.saveCache(data)
|
||||||
return err2
|
|
||||||
}
|
|
||||||
|
|
||||||
val, _ := sonic.MarshalString(&data)
|
val, _ := sonic.MarshalString(&data)
|
||||||
|
|
||||||
if val != "" {
|
if val != "" {
|
||||||
@ -193,7 +131,7 @@ func (e *LineApiUser) restartWebsocket(data models.LineApiUser) {
|
|||||||
fuSocket.Stop()
|
fuSocket.Stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
e.CacheRelation()
|
e.saveCache(data)
|
||||||
|
|
||||||
OpenUserBinanceWebsocket(data)
|
OpenUserBinanceWebsocket(data)
|
||||||
}
|
}
|
||||||
@ -211,23 +149,6 @@ func (e *LineApiUser) saveCache(data models.LineApiUser) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 缓存关系
|
|
||||||
// cacheAll 是否缓存所有关系
|
|
||||||
func (e *LineApiUser) CacheRelation() error {
|
|
||||||
var datas *[]models.LineApiUser
|
|
||||||
|
|
||||||
if err := e.Orm.Model(&models.LineApiUser{}).Where("open_status = 1").Find(&datas).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, data := range *datas {
|
|
||||||
// cacheStrs = append(cacheStrs, fmt.Sprintf(rediskey.API_USER, data.Id))
|
|
||||||
e.saveCache(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
打开用户websocket订阅
|
打开用户websocket订阅
|
||||||
*/
|
*/
|
||||||
@ -276,7 +197,6 @@ func (e *LineApiUser) Update(c *dto.LineApiUserUpdateReq, p *actions.DataPermiss
|
|||||||
actions.Permission(data.TableName(), p),
|
actions.Permission(data.TableName(), p),
|
||||||
).First(&data, c.GetId())
|
).First(&data, c.GetId())
|
||||||
oldApiKey := data.ApiKey
|
oldApiKey := data.ApiKey
|
||||||
oldReverseApiId := data.ReverseApiId
|
|
||||||
|
|
||||||
c.Generate(&data)
|
c.Generate(&data)
|
||||||
|
|
||||||
@ -284,33 +204,6 @@ func (e *LineApiUser) Update(c *dto.LineApiUserUpdateReq, p *actions.DataPermiss
|
|||||||
|
|
||||||
//事务
|
//事务
|
||||||
err = e.Orm.Transaction(func(tx *gorm.DB) error {
|
err = e.Orm.Transaction(func(tx *gorm.DB) error {
|
||||||
if data.ReverseApiId > 0 && data.ReverseApiId != oldReverseApiId {
|
|
||||||
var count int64
|
|
||||||
if err2 := tx.Model(models.LineApiUser{}).Where("subordinate <> '0' AND id=?", c.ReverseApiId).Count(&count).Error; err2 != nil {
|
|
||||||
return err2
|
|
||||||
}
|
|
||||||
if count > 0 {
|
|
||||||
return errors.New("反向api已被使用")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err2 := tx.Model(models.LineApiUser{}).Where("id =?", c.ReverseApiId).Update("subordinate", "2").Error; err2 != nil {
|
|
||||||
return err2
|
|
||||||
}
|
|
||||||
|
|
||||||
data.Subordinate = "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
if oldReverseApiId > 0 && oldReverseApiId != data.ReverseApiId {
|
|
||||||
if err2 := tx.Model(models.LineApiUser{}).Where("id =?", oldReverseApiId).Update("subordinate", "0").Error; err2 != nil {
|
|
||||||
e.Log.Errorf("解绑反向api失败 api:%d err:%v", oldReverseApiId, err2)
|
|
||||||
return fmt.Errorf("解绑反向api失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.ReverseApiId == 0 {
|
|
||||||
data.Subordinate = "0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
db := tx.Save(&data)
|
db := tx.Save(&data)
|
||||||
if err = db.Error; err != nil {
|
if err = db.Error; err != nil {
|
||||||
e.Log.Errorf("LineApiUserService Save error:%s \r\n", err)
|
e.Log.Errorf("LineApiUserService Save error:%s \r\n", err)
|
||||||
@ -333,9 +226,7 @@ func (e *LineApiUser) Update(c *dto.LineApiUserUpdateReq, p *actions.DataPermiss
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err2 := e.CacheRelation(); err2 != nil {
|
e.saveCache(data)
|
||||||
return err2
|
|
||||||
}
|
|
||||||
|
|
||||||
//旧key和新的key不一样,则关闭旧的websocket
|
//旧key和新的key不一样,则关闭旧的websocket
|
||||||
if oldApiKey != data.ApiKey {
|
if oldApiKey != data.ApiKey {
|
||||||
@ -398,14 +289,10 @@ func (e *LineApiUser) Remove(d *dto.LineApiUserDeleteReq, p *actions.DataPermiss
|
|||||||
|
|
||||||
_, err := helper.DefaultRedis.BatchDeleteKeys(delKeys)
|
_, err := helper.DefaultRedis.BatchDeleteKeys(delKeys)
|
||||||
|
|
||||||
if err != nil && !errors.Is(err, redis.Nil) {
|
if err != nil {
|
||||||
e.Log.Error("批量删除api_user key失败", err)
|
e.Log.Error("批量删除api_user key失败", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err2 := e.CacheRelation(); err2 != nil {
|
|
||||||
return err2
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -527,44 +414,3 @@ func (e *LineApiUser) GetActiveApis(apiIds []int) ([]int, error) {
|
|||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUnBindApiUser 获取未绑定反向下单的的api用户
|
|
||||||
func (e *LineApiUser) GetUnBindApiUser(req *dto.GetUnBindReverseReq, list *[]dto.UnBindReverseResp) error {
|
|
||||||
var datas []models.LineApiUser
|
|
||||||
var data models.LineApiUser
|
|
||||||
var count int64
|
|
||||||
|
|
||||||
if req.ApiId > 0 {
|
|
||||||
e.Orm.Model(data).Where("id =? AND subordinate = '2'", req.ApiId).Count(&count)
|
|
||||||
|
|
||||||
if count > 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query := e.Orm.Model(&data).
|
|
||||||
Where("open_status =1 AND exchange_type = ? ", req.ExchangeType)
|
|
||||||
|
|
||||||
if err := query.
|
|
||||||
Find(&datas).Error; err != nil {
|
|
||||||
e.Log.Error("获取未绑定用户失败:", err)
|
|
||||||
return fmt.Errorf("获取未绑定用户失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, item := range datas {
|
|
||||||
if item.Id == req.ApiId {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
listItem := dto.UnBindReverseResp{
|
|
||||||
Id: item.Id,
|
|
||||||
UserId: item.UserId,
|
|
||||||
ApiName: item.ApiName,
|
|
||||||
Disabled: item.Subordinate != "0",
|
|
||||||
}
|
|
||||||
|
|
||||||
(*list) = append((*list), listItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@ -264,8 +264,8 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi
|
|||||||
}
|
}
|
||||||
|
|
||||||
positions := map[string]positiondto.LinePreOrderPositioinDelReq{}
|
positions := map[string]positiondto.LinePreOrderPositioinDelReq{}
|
||||||
addPosition := binanceservice.AddPositionList{}
|
addPosition := positiondto.AddPositionList{}
|
||||||
reduceItem := binanceservice.ReduceListItem{}
|
reduceItem := positiondto.ReduceListItem{}
|
||||||
futAddPosition := map[int]string{}
|
futAddPosition := map[int]string{}
|
||||||
spotAddPosition := map[int]string{}
|
spotAddPosition := map[int]string{}
|
||||||
futReduces := map[int]string{}
|
futReduces := map[int]string{}
|
||||||
@ -279,8 +279,6 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi
|
|||||||
futReduceVal, _ := helper.DefaultRedis.GetAllList(spotAddPositionKey)
|
futReduceVal, _ := helper.DefaultRedis.GetAllList(spotAddPositionKey)
|
||||||
spotAddPositionVal, _ := helper.DefaultRedis.GetAllList(futReduceKey)
|
spotAddPositionVal, _ := helper.DefaultRedis.GetAllList(futReduceKey)
|
||||||
spotReduceVal, _ := helper.DefaultRedis.GetAllList(spotReduceKey)
|
spotReduceVal, _ := helper.DefaultRedis.GetAllList(spotReduceKey)
|
||||||
spotStrategyMap := e.GetStrategyOrderListMap(1)
|
|
||||||
futStrategyMap := e.GetStrategyOrderListMap(2)
|
|
||||||
|
|
||||||
for _, v := range futAddPositionVal {
|
for _, v := range futAddPositionVal {
|
||||||
sonic.Unmarshal([]byte(v), &addPosition)
|
sonic.Unmarshal([]byte(v), &addPosition)
|
||||||
@ -339,14 +337,8 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi
|
|||||||
if val, ok := spotRedces[order.Id]; ok {
|
if val, ok := spotRedces[order.Id]; ok {
|
||||||
helper.DefaultRedis.LRem(spotReduceKey, val)
|
helper.DefaultRedis.LRem(spotReduceKey, val)
|
||||||
}
|
}
|
||||||
var tradedSetKey string
|
|
||||||
if order.SymbolType == 1 {
|
|
||||||
tradedSetKey = fmt.Sprintf(global.TICKER_SPOT, order.ExchangeType, order.Symbol)
|
|
||||||
} else {
|
|
||||||
tradedSetKey = fmt.Sprintf(global.TICKER_FUTURES, order.ExchangeType, order.Symbol)
|
|
||||||
}
|
|
||||||
|
|
||||||
tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, tradedSetKey)
|
tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, fmt.Sprintf(global.TICKER_SPOT, order.ExchangeType, order.Symbol))
|
||||||
redisList.Price = utility.StringToDecimal(redisList.Price).Truncate(int32(tradeSet.PriceDigit)).String()
|
redisList.Price = utility.StringToDecimal(redisList.Price).Truncate(int32(tradeSet.PriceDigit)).String()
|
||||||
marshal, _ := sonic.Marshal(redisList)
|
marshal, _ := sonic.Marshal(redisList)
|
||||||
if order.SymbolType == 1 {
|
if order.SymbolType == 1 {
|
||||||
@ -357,13 +349,6 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi
|
|||||||
helper.DefaultRedis.LRem(listKey, string(marshal))
|
helper.DefaultRedis.LRem(listKey, string(marshal))
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
|
||||||
case order.StrategyTemplateType == 1 && order.SymbolType == 1:
|
|
||||||
e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &spotStrategyMap)
|
|
||||||
case order.StrategyTemplateType == 1 && order.SymbolType == 2:
|
|
||||||
e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &futStrategyMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
//会影响持仓的
|
//会影响持仓的
|
||||||
removeSymbolKey := fmt.Sprintf("%v_%s_%s_%s_%v", order.ApiId, order.ExchangeType, order.Symbol, order.Site, order.SymbolType)
|
removeSymbolKey := fmt.Sprintf("%v_%s_%s_%s_%v", order.ApiId, order.ExchangeType, order.Symbol, order.Site, order.SymbolType)
|
||||||
|
|
||||||
@ -378,7 +363,6 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi
|
|||||||
}
|
}
|
||||||
|
|
||||||
binanceservice.MainClosePositionClearCache(order.Id, order.SymbolType)
|
binanceservice.MainClosePositionClearCache(order.Id, order.SymbolType)
|
||||||
binanceservice.RemoveReduceReduceCacheByMainId(order.Id, order.SymbolType)
|
|
||||||
|
|
||||||
ints = append(ints, order.Id)
|
ints = append(ints, order.Id)
|
||||||
}
|
}
|
||||||
@ -411,7 +395,7 @@ func (e *LinePreOrder) Remove(d *dto.LinePreOrderDeleteReq, p *actions.DataPermi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 单个新增订单
|
// 单个新增订单
|
||||||
func (e *LinePreOrder) AddPreOrderCheck(req *dto.LineAddPreOrderReq, p *actions.DataPermission, errs *[]error, tickerSymbol string) error {
|
func (e *LinePreOrder) AddPreOrderCheck(req *dto.LineAddPreOrderReq, p *actions.DataPermission, errs *[]error) error {
|
||||||
apiUserService := LineApiUser{Service: e.Service}
|
apiUserService := LineApiUser{Service: e.Service}
|
||||||
apiIds := []int{}
|
apiIds := []int{}
|
||||||
apiUserIds := strings.Split(req.ApiUserId, ",")
|
apiUserIds := strings.Split(req.ApiUserId, ",")
|
||||||
@ -439,14 +423,14 @@ func (e *LinePreOrder) AddPreOrderCheck(req *dto.LineAddPreOrderReq, p *actions.
|
|||||||
*errs = append(*errs, fmt.Errorf("api用户未激活:%v", item))
|
*errs = append(*errs, fmt.Errorf("api用户未激活:%v", item))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
e.AddPreOrder(req, activeApiIds, p, errs, tickerSymbol)
|
e.AddPreOrder(req, activeApiIds, p, errs)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddPreOrder 单个添加
|
// AddPreOrder 单个添加
|
||||||
func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int, p *actions.DataPermission, errs *[]error, tickerSymbol string) error {
|
func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int, p *actions.DataPermission, errs *[]error) error {
|
||||||
if req.SaveTemplate == "2" || req.SaveTemplate == "1" { //2 = 只保存模板 1= 保存模板并下单
|
if req.SaveTemplate == "2" || req.SaveTemplate == "1" { //2 = 只保存模板 1= 保存模板并下单
|
||||||
var templateLog dto.LineAddPreOrderReq
|
var templateLog dto.LineAddPreOrderReq
|
||||||
copier.Copy(&templateLog, req)
|
copier.Copy(&templateLog, req)
|
||||||
@ -592,7 +576,6 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
|
|||||||
|
|
||||||
preOrderStatus := models.LinePreOrderStatus{}
|
preOrderStatus := models.LinePreOrderStatus{}
|
||||||
preOrderStatus.OrderSn = AddOrder.OrderSn
|
preOrderStatus.OrderSn = AddOrder.OrderSn
|
||||||
mainPrice := utility.StringToDecimal(AddOrder.Price)
|
|
||||||
|
|
||||||
//订单配置信息
|
//订单配置信息
|
||||||
preOrderExts := make([]models.LinePreOrderExt, 0)
|
preOrderExts := make([]models.LinePreOrderExt, 0)
|
||||||
@ -607,12 +590,6 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
|
|||||||
TpTpPriceRatio: req.ProfitTpTpPriceRatio,
|
TpTpPriceRatio: req.ProfitTpTpPriceRatio,
|
||||||
TpSlPriceRatio: req.ProfitTpSlPriceRatio,
|
TpSlPriceRatio: req.ProfitTpSlPriceRatio,
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.PricePattern == "mixture" {
|
|
||||||
defultExt.TakeProfitRatio = mainPrice.Div(utility.StrToDecimal(req.Profit)).Sub(decimal.NewFromInt(1)).Abs().Mul(decimal.NewFromInt(100)).Truncate(2)
|
|
||||||
defultExt.StopLossRatio = mainPrice.Div(req.StopLoss).Sub(decimal.NewFromInt(1)).Abs().Mul(decimal.NewFromInt(100)).Truncate(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
//减仓单
|
//减仓单
|
||||||
defultExt2 := models.LinePreOrderExt{
|
defultExt2 := models.LinePreOrderExt{
|
||||||
AddType: 2,
|
AddType: 2,
|
||||||
@ -624,12 +601,17 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
|
|||||||
TakeProfitNumRatio: decimal.NewFromInt(100), //减仓止盈默认100%
|
TakeProfitNumRatio: decimal.NewFromInt(100), //减仓止盈默认100%
|
||||||
StopLossRatio: req.ReduceStopLossRatio,
|
StopLossRatio: req.ReduceStopLossRatio,
|
||||||
}
|
}
|
||||||
|
mainPrice := utility.StringToDecimal(AddOrder.Price)
|
||||||
mainAmount := utility.SafeDiv(buyPrice, mainPrice)
|
mainAmount := utility.SafeDiv(buyPrice, mainPrice)
|
||||||
defultExt.TotalAfter = utility.StrToDecimal(AddOrder.Num).Truncate(int32(tradeSet.AmountDigit))
|
defultExt.TotalAfter = utility.StrToDecimal(AddOrder.Num).Truncate(int32(tradeSet.AmountDigit))
|
||||||
defultExt2.TotalBefore = defultExt.TotalAfter
|
defultExt2.TotalBefore = defultExt.TotalAfter
|
||||||
|
|
||||||
default2NumPercent := utility.SafeDiv(decimal.NewFromInt(100).Sub(req.ReduceNumRatio), decimal.NewFromInt(100))
|
default2NumPercent := utility.SafeDiv(decimal.NewFromInt(100).Sub(req.ReduceNumRatio), decimal.NewFromInt(100))
|
||||||
defultExt2.TotalAfter = mainAmount.Mul(default2NumPercent).Truncate(int32(tradeSet.AmountDigit))
|
defultExt2.TotalAfter = mainAmount.Mul(default2NumPercent).Truncate(int32(tradeSet.AmountDigit))
|
||||||
|
defultExt2.ReTakeRatio = utility.SafeDiv(req.ReducePriceRatio, default2NumPercent).Truncate(2)
|
||||||
|
|
||||||
|
preOrderExts = append(preOrderExts, defultExt)
|
||||||
|
preOrderExts = append(preOrderExts, defultExt2)
|
||||||
|
|
||||||
calculateResp := dto.CalculateBreakEvenRatioResp{}
|
calculateResp := dto.CalculateBreakEvenRatioResp{}
|
||||||
mainParam := dto.CalculateBreakEevenRatioReq{
|
mainParam := dto.CalculateBreakEevenRatioReq{
|
||||||
@ -645,22 +627,15 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
|
|||||||
AddPositionVal: req.ReduceNumRatio,
|
AddPositionVal: req.ReduceNumRatio,
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.PricePattern == "mixture" {
|
|
||||||
mainParam.LossEndPercent = mainPrice.Div(req.ReducePriceRatio).Sub(decimal.NewFromInt(1)).Abs().Mul(decimal.NewFromInt(100)).Truncate(2)
|
|
||||||
defultExt2.PriceRatio = mainParam.LossEndPercent
|
|
||||||
}
|
|
||||||
|
|
||||||
//计算减仓后
|
//计算减仓后
|
||||||
defultExt2.ReTakeRatio = utility.SafeDiv(mainParam.LossEndPercent, default2NumPercent).Truncate(2)
|
mainParam.LossEndPercent = req.ReducePriceRatio
|
||||||
mainParam.RemainingQuantity = mainAmount
|
mainParam.RemainingQuantity = mainAmount
|
||||||
e.CalculateBreakEvenRatio(&mainParam, &calculateResp, tradeSet)
|
e.CalculateBreakEvenRatio(&mainParam, &calculateResp, tradeSet)
|
||||||
mainParam.RemainingQuantity = calculateResp.RemainingQuantity //mainAmount.Mul(decimal.NewFromInt(100).Sub(req.ReduceNumRatio).Div(decimal.NewFromInt(100))).Truncate(int32(tradeSet.AmountDigit))
|
mainParam.RemainingQuantity = calculateResp.RemainingQuantity //mainAmount.Mul(decimal.NewFromInt(100).Sub(req.ReduceNumRatio).Div(decimal.NewFromInt(100))).Truncate(int32(tradeSet.AmountDigit))
|
||||||
mainParam.TotalLossAmountU = calculateResp.TotalLossAmountU //buyPrice.Mul(req.ReducePriceRatio.Div(decimal.NewFromInt(100)).Truncate(4)).Truncate(int32(tradeSet.PriceDigit))
|
mainParam.TotalLossAmountU = calculateResp.TotalLossAmountU //buyPrice.Mul(req.ReducePriceRatio.Div(decimal.NewFromInt(100)).Truncate(4)).Truncate(int32(tradeSet.PriceDigit))
|
||||||
req.ReduceReTakeProfitRatio = calculateResp.Ratio
|
req.ReduceReTakeProfitRatio = calculateResp.Ratio
|
||||||
mainParam.LossBeginPercent = mainParam.LossEndPercent
|
mainParam.LossBeginPercent = req.ReducePriceRatio
|
||||||
// defultExt.ReTakeRatio = calculateResp.Ratio
|
// defultExt.ReTakeRatio = calculateResp.Ratio
|
||||||
preOrderExts = append(preOrderExts, defultExt)
|
|
||||||
preOrderExts = append(preOrderExts, defultExt2)
|
|
||||||
|
|
||||||
for index, addPosition := range req.Ext {
|
for index, addPosition := range req.Ext {
|
||||||
ext := models.LinePreOrderExt{
|
ext := models.LinePreOrderExt{
|
||||||
@ -932,6 +907,7 @@ func saveOrderCache(req *dto.LineAddPreOrderReq, AddOrder models.LinePreOrder, l
|
|||||||
list.Percentag = linestrategyTemplate.Percentag
|
list.Percentag = linestrategyTemplate.Percentag
|
||||||
list.CompareType = linestrategyTemplate.CompareType
|
list.CompareType = linestrategyTemplate.CompareType
|
||||||
list.TimeSlotStart = linestrategyTemplate.TimeSlotStart
|
list.TimeSlotStart = linestrategyTemplate.TimeSlotStart
|
||||||
|
list.TimeSlotEnd = linestrategyTemplate.TimeSlotEnd
|
||||||
|
|
||||||
marshal, _ = sonic.Marshal(&list)
|
marshal, _ = sonic.Marshal(&list)
|
||||||
if AddOrder.SymbolType == global.SYMBOL_SPOT {
|
if AddOrder.SymbolType == global.SYMBOL_SPOT {
|
||||||
@ -1039,6 +1015,7 @@ func createPreReduceOrder(preOrder *models.LinePreOrder, ext models.LinePreOrder
|
|||||||
|
|
||||||
stopOrder.OrderType = 4
|
stopOrder.OrderType = 4
|
||||||
stopOrder.Status = 0
|
stopOrder.Status = 0
|
||||||
|
stopOrder.Rate = ext.PriceRatio.String()
|
||||||
stopOrder.Num = ext.TotalAfter.Sub(ext.TotalBefore).Abs().Truncate(int32(tradeSet.AmountDigit)).String()
|
stopOrder.Num = ext.TotalAfter.Sub(ext.TotalBefore).Abs().Truncate(int32(tradeSet.AmountDigit)).String()
|
||||||
stopOrder.BuyPrice = "0"
|
stopOrder.BuyPrice = "0"
|
||||||
stopOrder.Rate = ext.PriceRatio.String()
|
stopOrder.Rate = ext.PriceRatio.String()
|
||||||
@ -1273,15 +1250,7 @@ func (e *LinePreOrder) AddBatchPreOrder(batchReq *dto.LineBatchAddPreOrderReq, p
|
|||||||
batchReq.Symbol = symbolGroupInfo.Symbol
|
batchReq.Symbol = symbolGroupInfo.Symbol
|
||||||
}
|
}
|
||||||
|
|
||||||
//脚本次数
|
|
||||||
// if batchReq.OrderNum > 0 {
|
|
||||||
apiUserIds := strings.Split(batchReq.ApiUserId, ",")
|
apiUserIds := strings.Split(batchReq.ApiUserId, ",")
|
||||||
var tickerSymbol string
|
|
||||||
if batchReq.SymbolType == global.SYMBOL_SPOT {
|
|
||||||
tickerSymbol = helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val()
|
|
||||||
} else {
|
|
||||||
tickerSymbol = helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val()
|
|
||||||
}
|
|
||||||
|
|
||||||
if batchReq.Script == "1" {
|
if batchReq.Script == "1" {
|
||||||
//scriptLogs := make([]models.LinePreScript, 0)
|
//scriptLogs := make([]models.LinePreScript, 0)
|
||||||
@ -1340,15 +1309,11 @@ func (e *LinePreOrder) AddBatchPreOrder(batchReq *dto.LineBatchAddPreOrderReq, p
|
|||||||
req.ReduceTakeProfitRatio = batchReq.ReduceTakeProfitRatio
|
req.ReduceTakeProfitRatio = batchReq.ReduceTakeProfitRatio
|
||||||
req.CreateBy = batchReq.CreateBy
|
req.CreateBy = batchReq.CreateBy
|
||||||
|
|
||||||
e.AddPreOrderCheck(&req, p, errs, tickerSymbol)
|
e.AddPreOrderCheck(&req, p, errs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
// } else {
|
|
||||||
// *errs = append(*errs, errors.New("请选择运行次数"))
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// QuickAddPreOrder 模板快速下单
|
// QuickAddPreOrder 模板快速下单
|
||||||
@ -1361,13 +1326,7 @@ func (e *LinePreOrder) QuickAddPreOrder(quickReq *dto.QuickAddPreOrderReq, p *ac
|
|||||||
var addPreOrderParams dto.LineAddPreOrderReq
|
var addPreOrderParams dto.LineAddPreOrderReq
|
||||||
sonic.Unmarshal([]byte(log.Params), &addPreOrderParams)
|
sonic.Unmarshal([]byte(log.Params), &addPreOrderParams)
|
||||||
|
|
||||||
var tickerSymbol string
|
err := e.AddPreOrderCheck(&addPreOrderParams, p, errs)
|
||||||
if addPreOrderParams.OrderType == global.SYMBOL_SPOT {
|
|
||||||
tickerSymbol = helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val()
|
|
||||||
} else {
|
|
||||||
tickerSymbol = helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val()
|
|
||||||
}
|
|
||||||
err := e.AddPreOrderCheck(&addPreOrderParams, p, errs, tickerSymbol)
|
|
||||||
// err := e.AddPreOrder(&addPreOrderParams, p, errs, tickerSymbol)
|
// err := e.AddPreOrder(&addPreOrderParams, p, errs, tickerSymbol)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
*errs = append(*errs, fmt.Errorf("api_id:%s 获取交易对:%s 生成订单失败", addPreOrderParams.ApiUserId, addPreOrderParams.Symbol))
|
*errs = append(*errs, fmt.Errorf("api_id:%s 获取交易对:%s 生成订单失败", addPreOrderParams.ApiUserId, addPreOrderParams.Symbol))
|
||||||
@ -1719,23 +1678,18 @@ func (e *LinePreOrder) CalculateAmount(req dto.ManuallyCover, totalNum, lastPric
|
|||||||
} else {
|
} else {
|
||||||
decimalValue := utility.StringToDecimal(req.Value)
|
decimalValue := utility.StringToDecimal(req.Value)
|
||||||
if notUsdt {
|
if notUsdt {
|
||||||
tickerSymbolMaps := make([]dto.Ticker, 0)
|
tickerSymbolMaps, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 2)
|
||||||
tickerSymbol := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val()
|
|
||||||
if err := sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps); err != nil {
|
|
||||||
return decimal.Zero, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var tickerPrice decimal.Decimal
|
var tickerPrice decimal.Decimal
|
||||||
for _, symbolMap := range tickerSymbolMaps {
|
for _, symbolMap := range tickerSymbolMaps {
|
||||||
if symbolMap.Symbol == strings.ToUpper(symbolInfo.BaseAsset+"USDT") {
|
if symbolMap.Symbol == strings.ToUpper(symbolInfo.BaseAsset+"USDT") {
|
||||||
tickerPrice, _ = decimal.NewFromString(symbolMap.Price)
|
tickerPrice, _ = decimal.NewFromString(symbolMap.LastPrice)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, symbolMap := range tickerSymbolMaps {
|
for _, symbolMap := range tickerSymbolMaps {
|
||||||
if symbolMap.Symbol == strings.ToUpper(symbolInfo.QuoteAsset+"USDT") {
|
if symbolMap.Symbol == strings.ToUpper(symbolInfo.QuoteAsset+"USDT") {
|
||||||
uTickerPrice, _ := decimal.NewFromString(symbolMap.Price)
|
uTickerPrice, _ := decimal.NewFromString(symbolMap.LastPrice)
|
||||||
div := utility.SafeDiv(tickerPrice, decimal.NewFromInt(1).Div(uTickerPrice))
|
div := utility.SafeDiv(tickerPrice, decimal.NewFromInt(1).Div(uTickerPrice))
|
||||||
amt = utility.SafeDiv(decimalValue, div)
|
amt = utility.SafeDiv(decimalValue, div)
|
||||||
break
|
break
|
||||||
@ -2018,8 +1972,6 @@ func (e *LinePreOrder) ClearUnTriggered() error {
|
|||||||
var orderLists []models.LinePreOrder
|
var orderLists []models.LinePreOrder
|
||||||
positions := map[string]positiondto.LinePreOrderPositioinDelReq{}
|
positions := map[string]positiondto.LinePreOrderPositioinDelReq{}
|
||||||
e.Orm.Model(&models.LinePreOrder{}).Where("main_id = 0 AND pid = 0 AND status = '0'").Find(&orderLists).Unscoped().Delete(&models.LinePreOrder{})
|
e.Orm.Model(&models.LinePreOrder{}).Where("main_id = 0 AND pid = 0 AND status = '0'").Find(&orderLists).Unscoped().Delete(&models.LinePreOrder{})
|
||||||
spotStrategyMap := e.GetStrategyOrderListMap(1)
|
|
||||||
futStrategyMap := e.GetStrategyOrderListMap(2)
|
|
||||||
|
|
||||||
for _, order := range orderLists {
|
for _, order := range orderLists {
|
||||||
redisList := dto.PreOrderRedisList{
|
redisList := dto.PreOrderRedisList{
|
||||||
@ -2031,35 +1983,17 @@ func (e *LinePreOrder) ClearUnTriggered() error {
|
|||||||
OrderSn: order.OrderSn,
|
OrderSn: order.OrderSn,
|
||||||
QuoteSymbol: order.QuoteSymbol,
|
QuoteSymbol: order.QuoteSymbol,
|
||||||
}
|
}
|
||||||
|
tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, fmt.Sprintf(global.TICKER_SPOT, order.ExchangeType, order.Symbol))
|
||||||
var tradedSetKey string
|
|
||||||
if order.SymbolType == 1 {
|
|
||||||
tradedSetKey = fmt.Sprintf(global.TICKER_SPOT, order.ExchangeType, order.Symbol)
|
|
||||||
} else {
|
|
||||||
tradedSetKey = fmt.Sprintf(global.TICKER_FUTURES, order.ExchangeType, order.Symbol)
|
|
||||||
}
|
|
||||||
|
|
||||||
tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, tradedSetKey)
|
|
||||||
redisList.Price = utility.StringToDecimal(redisList.Price).Truncate(int32(tradeSet.PriceDigit)).String()
|
redisList.Price = utility.StringToDecimal(redisList.Price).Truncate(int32(tradeSet.PriceDigit)).String()
|
||||||
marshal, _ := sonic.Marshal(redisList)
|
marshal, _ := sonic.Marshal(redisList)
|
||||||
|
|
||||||
if order.SymbolType == 1 {
|
if order.SymbolType == 1 {
|
||||||
key := fmt.Sprintf(rediskey.PreFutOrderList, order.ExchangeType)
|
key := fmt.Sprintf(rediskey.PreFutOrderList, order.ExchangeType)
|
||||||
|
|
||||||
helper.DefaultRedis.LRem(key, string(marshal))
|
helper.DefaultRedis.LRem(key, string(marshal))
|
||||||
} else {
|
} else {
|
||||||
key := fmt.Sprintf(rediskey.PreSpotOrderList, order.ExchangeType)
|
key := fmt.Sprintf(rediskey.PreSpotOrderList, order.ExchangeType)
|
||||||
|
|
||||||
helper.DefaultRedis.LRem(key, string(marshal))
|
helper.DefaultRedis.LRem(key, string(marshal))
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
|
||||||
case order.StrategyTemplateType == 1 && order.SymbolType == 1:
|
|
||||||
e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &spotStrategyMap)
|
|
||||||
case order.StrategyTemplateType == 1 && order.SymbolType == 2:
|
|
||||||
e.RemoveStrategyOrderCache(order.Id, order.SymbolType, order.ExchangeType, &futStrategyMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
//会影响持仓的
|
//会影响持仓的
|
||||||
removeSymbolKey := fmt.Sprintf("%v_%s_%s_%s_%v", order.ApiId, order.ExchangeType, order.Symbol, order.Site, order.SymbolType)
|
removeSymbolKey := fmt.Sprintf("%v_%s_%s_%s_%v", order.ApiId, order.ExchangeType, order.Symbol, order.Site, order.SymbolType)
|
||||||
|
|
||||||
@ -2099,60 +2033,6 @@ func (e *LinePreOrder) ClearUnTriggered() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除待策略待触发单
|
|
||||||
func (e *LinePreOrder) RemoveStrategyOrderCache(orderId int, symbolType int, exchangeType string, caches *map[string][]dto.StrategyOrderRedisList) {
|
|
||||||
strategys, _ := (*caches)[exchangeType]
|
|
||||||
var strategyListKey string
|
|
||||||
|
|
||||||
if symbolType == 1 {
|
|
||||||
strategyListKey = fmt.Sprintf(rediskey.StrategySpotOrderList, exchangeType)
|
|
||||||
} else {
|
|
||||||
strategyListKey = fmt.Sprintf(rediskey.StrategyFutOrderList, exchangeType)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, strategy := range strategys {
|
|
||||||
if strategy.Id == orderId {
|
|
||||||
strategyVal, _ := sonic.MarshalString(strategy)
|
|
||||||
helper.DefaultRedis.LRem(strategyListKey, strategyVal)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取策略订单缓存列表
|
|
||||||
// symbolType 1现货 2合约
|
|
||||||
func (e *LinePreOrder) GetStrategyOrderListMap(symbolType int) map[string][]dto.StrategyOrderRedisList {
|
|
||||||
result := make(map[string][]dto.StrategyOrderRedisList)
|
|
||||||
var key string
|
|
||||||
exchanges := []string{global.EXCHANGE_BINANCE}
|
|
||||||
|
|
||||||
if symbolType == 1 {
|
|
||||||
key = rediskey.StrategySpotOrderList
|
|
||||||
} else {
|
|
||||||
key = rediskey.StrategyFutOrderList
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, exchange := range exchanges {
|
|
||||||
newKey := fmt.Sprintf(key, exchange)
|
|
||||||
vals, _ := helper.DefaultRedis.GetAllList(newKey)
|
|
||||||
itemData := make([]dto.StrategyOrderRedisList, 0)
|
|
||||||
item := dto.StrategyOrderRedisList{}
|
|
||||||
|
|
||||||
for _, v := range vals {
|
|
||||||
sonic.Unmarshal([]byte(v), &item)
|
|
||||||
|
|
||||||
if item.Id > 0 {
|
|
||||||
itemData = append(itemData, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(itemData) > 0 {
|
|
||||||
result[exchange] = itemData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *LinePreOrder) QueryOrder(req *dto.QueryOrderReq) (res interface{}, err error) {
|
func (e *LinePreOrder) QueryOrder(req *dto.QueryOrderReq) (res interface{}, err error) {
|
||||||
var apiUserInfo models.LineApiUser
|
var apiUserInfo models.LineApiUser
|
||||||
e.Orm.Model(&models.LineApiUser{}).Where("id = ?", req.ApiId).Find(&apiUserInfo)
|
e.Orm.Model(&models.LineApiUser{}).Where("id = ?", req.ApiId).Find(&apiUserInfo)
|
||||||
@ -2205,9 +2085,9 @@ func (e *LinePreOrder) GenerateOrder(req *dto.LineAddPreOrderReq) error {
|
|||||||
var tickerPrice decimal.Decimal
|
var tickerPrice decimal.Decimal
|
||||||
|
|
||||||
if req.SymbolType == 1 {
|
if req.SymbolType == 1 {
|
||||||
tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, req.Symbol, 0)
|
|
||||||
} else {
|
|
||||||
tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, req.Symbol, 1)
|
tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, req.Symbol, 1)
|
||||||
|
} else {
|
||||||
|
tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, req.Symbol, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tradeSet.LastPrice == "" {
|
if tradeSet.LastPrice == "" {
|
||||||
@ -2246,18 +2126,15 @@ func (e *LinePreOrder) GenerateOrder(req *dto.LineAddPreOrderReq) error {
|
|||||||
AddPositionVal: req.ReduceNumRatio,
|
AddPositionVal: req.ReduceNumRatio,
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.PricePattern == "mixture" {
|
|
||||||
mainParam.LossEndPercent = price.Div(req.ReducePriceRatio).Sub(decimal.NewFromInt(1)).Abs().Mul(decimal.NewFromInt(100)).Truncate(2)
|
|
||||||
}
|
|
||||||
|
|
||||||
//计算减仓后
|
//计算减仓后
|
||||||
|
mainParam.LossEndPercent = req.ReducePriceRatio
|
||||||
mainParam.RemainingQuantity = mainAmount
|
mainParam.RemainingQuantity = mainAmount
|
||||||
mainParam.AddType = 2
|
mainParam.AddType = 2
|
||||||
e.CalculateBreakEvenRatio(&mainParam, &calculateResp, tradeSet)
|
e.CalculateBreakEvenRatio(&mainParam, &calculateResp, tradeSet)
|
||||||
mainParam.RemainingQuantity = calculateResp.RemainingQuantity
|
mainParam.RemainingQuantity = calculateResp.RemainingQuantity
|
||||||
mainParam.TotalLossAmountU = calculateResp.TotalLossAmountU
|
mainParam.TotalLossAmountU = calculateResp.TotalLossAmountU
|
||||||
req.ReduceReTakeProfitRatio = calculateResp.Ratio
|
req.ReduceReTakeProfitRatio = calculateResp.Ratio
|
||||||
lossBeginPercent = mainParam.LossEndPercent
|
lossBeginPercent = req.ReducePriceRatio
|
||||||
|
|
||||||
//顺序排序
|
//顺序排序
|
||||||
sort.Slice(req.Ext, func(i, j int) bool {
|
sort.Slice(req.Ext, func(i, j int) bool {
|
||||||
|
|||||||
@ -1,114 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,345 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,136 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -15,7 +15,6 @@ import (
|
|||||||
"go-admin/app/admin/models"
|
"go-admin/app/admin/models"
|
||||||
"go-admin/app/admin/service/dto"
|
"go-admin/app/admin/service/dto"
|
||||||
"go-admin/common/actions"
|
"go-admin/common/actions"
|
||||||
"go-admin/common/const/rediskey"
|
|
||||||
cDto "go-admin/common/dto"
|
cDto "go-admin/common/dto"
|
||||||
"go-admin/common/global"
|
"go-admin/common/global"
|
||||||
"go-admin/common/helper"
|
"go-admin/common/helper"
|
||||||
@ -23,6 +22,7 @@ import (
|
|||||||
models2 "go-admin/models"
|
models2 "go-admin/models"
|
||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
"go-admin/services/binanceservice"
|
"go-admin/services/binanceservice"
|
||||||
|
"go-admin/services/cacheservice"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LineSymbol struct {
|
type LineSymbol struct {
|
||||||
@ -354,9 +354,10 @@ func (e *LineSymbol) ResetSpotSymbol() error {
|
|||||||
Symbol string `json:"symbol"`
|
Symbol string `json:"symbol"`
|
||||||
Price string `json:"price"`
|
Price string `json:"price"`
|
||||||
}
|
}
|
||||||
tickerSymbol := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val()
|
// tickerSymbol := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val()
|
||||||
tickerSymbolMaps := make([]Ticker, 0)
|
// tickerSymbolMaps := make([]Ticker, 0)
|
||||||
sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps)
|
// sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps)
|
||||||
|
tickerSymbolMaps, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 1)
|
||||||
oldMapSymbols, err := getMapSymbol(e, "1")
|
oldMapSymbols, err := getMapSymbol(e, "1")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -389,7 +390,7 @@ func (e *LineSymbol) ResetSpotSymbol() error {
|
|||||||
var tickerPrice decimal.Decimal
|
var tickerPrice decimal.Decimal
|
||||||
for _, symbolMap := range tickerSymbolMaps {
|
for _, symbolMap := range tickerSymbolMaps {
|
||||||
if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") {
|
if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") {
|
||||||
tickerPrice, _ = decimal.NewFromString(symbolMap.Price)
|
tickerPrice, _ = decimal.NewFromString(symbolMap.LastPrice)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if tickerPrice.GreaterThan(decimal.Zero) {
|
if tickerPrice.GreaterThan(decimal.Zero) {
|
||||||
@ -527,9 +528,10 @@ func (e *LineSymbol) ResetFuturesSymbol() error {
|
|||||||
Symbol string `json:"symbol"`
|
Symbol string `json:"symbol"`
|
||||||
Price string `json:"price"`
|
Price string `json:"price"`
|
||||||
}
|
}
|
||||||
tickerSymbol := helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val()
|
// tickerSymbol := helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val()
|
||||||
tickerSymbolMaps := make([]Ticker, 0)
|
// tickerSymbolMaps := make([]Ticker, 0)
|
||||||
sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps)
|
// sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps)
|
||||||
|
tickerSymbolMaps, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 2)
|
||||||
oldMapSymbols, err := getMapSymbol(e, "2")
|
oldMapSymbols, err := getMapSymbol(e, "2")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -560,7 +562,7 @@ func (e *LineSymbol) ResetFuturesSymbol() error {
|
|||||||
var tickerPrice decimal.Decimal
|
var tickerPrice decimal.Decimal
|
||||||
for _, symbolMap := range tickerSymbolMaps {
|
for _, symbolMap := range tickerSymbolMaps {
|
||||||
if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") {
|
if symbolMap.Symbol == strings.ToUpper(spotTicker24h.Currency+"USDT") {
|
||||||
tickerPrice, _ = decimal.NewFromString(symbolMap.Price)
|
tickerPrice, _ = decimal.NewFromString(symbolMap.LastPrice)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if tickerPrice.GreaterThan(decimal.Zero) {
|
if tickerPrice.GreaterThan(decimal.Zero) {
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import (
|
|||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
"go-admin/pkg/utility/seqs"
|
"go-admin/pkg/utility/seqs"
|
||||||
"go-admin/services/binanceservice"
|
"go-admin/services/binanceservice"
|
||||||
|
"go-admin/services/cacheservice"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -323,11 +324,13 @@ func (e *LineUser) RechargeNetworkList(req *dto.RechargeListReq) (data []models.
|
|||||||
}
|
}
|
||||||
coin := req.Coin
|
coin := req.Coin
|
||||||
resp := make([]models.VtsCoinToNetWorkResp, 0)
|
resp := make([]models.VtsCoinToNetWorkResp, 0)
|
||||||
var tickerData []binanceservice.Ticker
|
// var tickerData []binanceservice.Ticker
|
||||||
val := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val()
|
// val := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val()
|
||||||
if val != "" {
|
// if val != "" {
|
||||||
sonic.Unmarshal([]byte(val), &tickerData)
|
// sonic.Unmarshal([]byte(val), &tickerData)
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
tickerData, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 1)
|
||||||
|
|
||||||
for _, v1 := range list {
|
for _, v1 := range list {
|
||||||
if strings.EqualFold(coin, "USDT") {
|
if strings.EqualFold(coin, "USDT") {
|
||||||
@ -357,7 +360,7 @@ func (e *LineUser) RechargeNetworkList(req *dto.RechargeListReq) (data []models.
|
|||||||
TransferFee: utility.FloatCutStr(v1.TransferFee, 8),
|
TransferFee: utility.FloatCutStr(v1.TransferFee, 8),
|
||||||
//TransferFeeUsdt string `json:"transfer_fee_usdt"`
|
//TransferFeeUsdt string `json:"transfer_fee_usdt"`
|
||||||
}
|
}
|
||||||
res.TransferFeeUsdt = utility.FloatCutStr(v1.TransferFee*utility.StringToFloat64(v2.Price), 8)
|
res.TransferFeeUsdt = utility.FloatCutStr(v1.TransferFee*utility.StringToFloat64(v2.LastPrice), 8)
|
||||||
resp = append(resp, res)
|
resp = append(resp, res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -107,15 +107,3 @@ func (e *LineUserSetting) Remove(d *dto.LineUserSettingDeleteReq, p *actions.Dat
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDefault 获取默认LineUserSetting对象
|
|
||||||
func (e *LineUserSetting) GetDefault() (models.LineUserSetting, error) {
|
|
||||||
var data models.LineUserSetting
|
|
||||||
|
|
||||||
if err := e.Orm.Model(&data).First(&data).Error; err != nil {
|
|
||||||
e.Log.Errorf("GetDefault LineUserSetting error:%s \r\n", err)
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,67 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@ -9,8 +9,8 @@ import (
|
|||||||
"go-admin/common/const/rediskey"
|
"go-admin/common/const/rediskey"
|
||||||
"go-admin/common/global"
|
"go-admin/common/global"
|
||||||
"go-admin/common/helper"
|
"go-admin/common/helper"
|
||||||
|
"go-admin/models/positiondto"
|
||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
"go-admin/services/binanceservice"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
@ -38,9 +38,6 @@ func InitJob() {
|
|||||||
"MemberExpirationJob": MemberExpirationJob{}, //会员到期处理
|
"MemberExpirationJob": MemberExpirationJob{}, //会员到期处理
|
||||||
"MemberRenwalOrderExpirationJob": MemberRenwalOrderExpirationJob{}, //会员续费订单过期处理
|
"MemberRenwalOrderExpirationJob": MemberRenwalOrderExpirationJob{}, //会员续费订单过期处理
|
||||||
"TrxQueryJobs": TrxQueryJobs{}, //订单支付监听
|
"TrxQueryJobs": TrxQueryJobs{}, //订单支付监听
|
||||||
"StrategyJob": StrategyJob{}, //下单策略触发
|
|
||||||
"BinanceSpotAccountJob": BinanceSpotAccountJob{}, //币安现货划转
|
|
||||||
"BinanceFuturesAccountJob": BinanceFuturesAccountJob{}, //币安合约划转
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,10 +128,10 @@ func removeExpirateChildOrder(childOrders []models.LinePreOrder, db *gorm.DB) {
|
|||||||
spotAdPositionStr, _ := helper.DefaultRedis.GetAllList(spotAddKey)
|
spotAdPositionStr, _ := helper.DefaultRedis.GetAllList(spotAddKey)
|
||||||
futReduceStr, _ := helper.DefaultRedis.GetAllList(futReduceKey)
|
futReduceStr, _ := helper.DefaultRedis.GetAllList(futReduceKey)
|
||||||
spotReduceStr, _ := helper.DefaultRedis.GetAllList(spotReduceKey)
|
spotReduceStr, _ := helper.DefaultRedis.GetAllList(spotReduceKey)
|
||||||
futAddPositionCache := binanceservice.AddPositionList{}
|
futAddPositionCache := positiondto.AddPositionList{}
|
||||||
spotAddPositionCache := binanceservice.AddPositionList{}
|
spotAddPositionCache := positiondto.AddPositionList{}
|
||||||
futReduceCache := binanceservice.ReduceListItem{}
|
futReduceCache := positiondto.ReduceListItem{}
|
||||||
spotReduceCache := binanceservice.ReduceListItem{}
|
spotReduceCache := positiondto.ReduceListItem{}
|
||||||
futAddPositionMap := map[int]string{}
|
futAddPositionMap := map[int]string{}
|
||||||
spotAddPositionMap := map[int]string{}
|
spotAddPositionMap := map[int]string{}
|
||||||
futReduceMap := map[int]string{}
|
futReduceMap := map[int]string{}
|
||||||
|
|||||||
@ -3,7 +3,6 @@ package jobs
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
models2 "go-admin/app/jobs/models"
|
models2 "go-admin/app/jobs/models"
|
||||||
"runtime"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/go-admin-team/go-admin-core/logger"
|
log "github.com/go-admin-team/go-admin-core/logger"
|
||||||
@ -44,10 +43,7 @@ type ExecJob struct {
|
|||||||
func (e *ExecJob) Run() {
|
func (e *ExecJob) Run() {
|
||||||
defer func() {
|
defer func() {
|
||||||
if err := recover(); err != nil {
|
if err := recover(); err != nil {
|
||||||
// 获取调用栈信息
|
log.Errorf("脚本任务失败:%v", err)
|
||||||
buf := make([]byte, 1<<16) // 64KB 缓冲区
|
|
||||||
n := runtime.Stack(buf, false)
|
|
||||||
log.Errorf("脚本任务失败: %v\n%s", err, buf[:n])
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|||||||
@ -242,7 +242,7 @@ func (t LimitOrderTimeoutDuration) ReSpotOrderPlace(db *gorm.DB, order models.Li
|
|||||||
} else {
|
} else {
|
||||||
var remainingQuantity decimal.Decimal
|
var remainingQuantity decimal.Decimal
|
||||||
spotOrder, err := spotApi.GetOrderByOrderSnLoop(order.Symbol, order.OrderSn, apiUserinfo, 4)
|
spotOrder, err := spotApi.GetOrderByOrderSnLoop(order.Symbol, order.OrderSn, apiUserinfo, 4)
|
||||||
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 0)
|
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
origQty := utility.StrToDecimal(spotOrder.OrigQty)
|
origQty := utility.StrToDecimal(spotOrder.OrigQty)
|
||||||
@ -360,7 +360,7 @@ func (t LimitOrderTimeoutDuration) ReFutOrderPlace(db *gorm.DB, order models.Lin
|
|||||||
} else {
|
} else {
|
||||||
var remainingQuantity decimal.Decimal
|
var remainingQuantity decimal.Decimal
|
||||||
spotOrder, err := futApi.GetOrderByOrderSnLoop(order.Symbol, order.OrderSn, apiUserinfo, 4)
|
spotOrder, err := futApi.GetOrderByOrderSnLoop(order.Symbol, order.OrderSn, apiUserinfo, 4)
|
||||||
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1)
|
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 2)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
origQty := utility.StrToDecimal(spotOrder.OrigQty)
|
origQty := utility.StrToDecimal(spotOrder.OrigQty)
|
||||||
|
|||||||
@ -1,25 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
264
cmd/binance/binance.go
Normal file
264
cmd/binance/binance.go
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
package binance
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/app/admin/service"
|
||||||
|
"go-admin/common/const/rediskey"
|
||||||
|
"go-admin/common/database"
|
||||||
|
"go-admin/common/global"
|
||||||
|
"go-admin/common/helper"
|
||||||
|
"go-admin/common/storage"
|
||||||
|
"go-admin/config/serverinit"
|
||||||
|
"go-admin/pkg/httputils"
|
||||||
|
"go-admin/pkg/utility"
|
||||||
|
"go-admin/services/excservice"
|
||||||
|
"go-admin/services/fileservice"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
ext "go-admin/config"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
"github.com/go-admin-team/go-admin-core/config/source/file"
|
||||||
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/config"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/pkg"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
configYml string
|
||||||
|
StartCmd = &cobra.Command{
|
||||||
|
Use: "binance",
|
||||||
|
Short: "Start Binance service (market data + user private)",
|
||||||
|
Example: "go-admin binance -c config/settings.yml",
|
||||||
|
SilenceUsage: true,
|
||||||
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
setup()
|
||||||
|
},
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return run()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
StartCmd.PersistentFlags().StringVarP(&configYml, "config", "c", "config/settings.yml", "Start server with provided configuration file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup() {
|
||||||
|
// 注入配置扩展项
|
||||||
|
config.ExtendConfig = &ext.ExtConfig
|
||||||
|
//1. 读取配置
|
||||||
|
config.Setup(
|
||||||
|
file.NewSource(file.WithPath(configYml)),
|
||||||
|
database.Setup,
|
||||||
|
storage.Setup,
|
||||||
|
)
|
||||||
|
//注册监听函数
|
||||||
|
queue := sdk.Runtime.GetMemoryQueue("")
|
||||||
|
queue.Register(global.LoginLog, models.SaveLoginLog)
|
||||||
|
queue.Register(global.OperateLog, models.SaveOperaLog)
|
||||||
|
go queue.Run()
|
||||||
|
|
||||||
|
usageStr := `starting Binance service (market data + user private)...`
|
||||||
|
logger.Info(usageStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var db *gorm.DB
|
||||||
|
dbs := sdk.Runtime.GetDb()
|
||||||
|
|
||||||
|
for _, item := range dbs {
|
||||||
|
db = item
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultInit(db, ctx)
|
||||||
|
|
||||||
|
utility.SafeGo(func() {
|
||||||
|
// 启动定时任务
|
||||||
|
clearLogJob(db, ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
//自动重启websocket
|
||||||
|
// utility.SafeGo(func() {
|
||||||
|
// reconnect(ctx)
|
||||||
|
// })
|
||||||
|
|
||||||
|
//动态用户连接管理
|
||||||
|
utility.SafeGo(func() {
|
||||||
|
manageBinanceUserConnections(ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, os.Interrupt)
|
||||||
|
<-quit
|
||||||
|
|
||||||
|
fmt.Printf("%s Shutdown Server ... \r\n", pkg.GetCurrentTimeStr())
|
||||||
|
logger.Infof("Server exiting")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultInit(db *gorm.DB, ctx context.Context) {
|
||||||
|
httputils.InitProxy(ext.ExtConfig.ProxyUrl)
|
||||||
|
//初始化 默认redis
|
||||||
|
helper.InitDefaultRedis(ext.ExtConfig.Redis.Addr, ext.ExtConfig.Redis.Password, ext.ExtConfig.Redis.Db)
|
||||||
|
helper.InitLockRedisConn(ext.ExtConfig.Redis.Addr, ext.ExtConfig.Redis.Password, strconv.Itoa(ext.ExtConfig.Redis.Db))
|
||||||
|
|
||||||
|
err := helper.DefaultRedis.Ping()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Info("初始化redis失败!请检查配置")
|
||||||
|
_, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
cancel()
|
||||||
|
} else {
|
||||||
|
logger.Info("redis初始化成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
//初始化Binance服务(行情 + 用户私有)
|
||||||
|
logger.Info("=== 启动Binance完整服务 ===")
|
||||||
|
|
||||||
|
// 启动Binance行情服务
|
||||||
|
logger.Info("启动Binance行情服务...")
|
||||||
|
if err := serverinit.BinanceMarketInit(); err != nil {
|
||||||
|
logger.Infof("Binance行情服务启动失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化Binance用户连接
|
||||||
|
logger.Info("启动Binance用户服务...")
|
||||||
|
initBinanceUserConnections(db, ctx)
|
||||||
|
logger.Info("=== Binance完整服务已启动 ===")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化Binance用户连接
|
||||||
|
func initBinanceUserConnections(db *gorm.DB, ctx context.Context) {
|
||||||
|
var list []models.LineApiUser
|
||||||
|
err := db.Model(&models.LineApiUser{}).Where("open_status=1 AND exchange_type =?", global.EXCHANGE_BINANCE).Find(&list).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Info("获取Binance用户api失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof("找到 %d 个Binance用户,开始建立连接...", len(list))
|
||||||
|
for _, item := range list {
|
||||||
|
service.OpenUserBinanceWebsocket(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态Binance用户连接管理
|
||||||
|
func manageBinanceUserConnections(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(time.Second * 5)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
logger.Info("Binanceç¨æ·åç»åç§°åé")
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
deleteKeys, _ := helper.DefaultRedis.GetAllList(rediskey.ApiUserDeleteList)
|
||||||
|
activeApis, _ := helper.DefaultRedis.GetAllList(rediskey.ApiUserActiveList)
|
||||||
|
|
||||||
|
//移除Binance连接
|
||||||
|
for _, item := range deleteKeys {
|
||||||
|
logger.Infof("移除Binance用户连接: %s", item)
|
||||||
|
|
||||||
|
if wm, ok := excservice.SpotSockets[item]; ok {
|
||||||
|
wm.Stop()
|
||||||
|
delete(excservice.SpotSockets, item)
|
||||||
|
}
|
||||||
|
if wm, ok := excservice.FutureSockets[item]; ok {
|
||||||
|
wm.Stop()
|
||||||
|
delete(excservice.FutureSockets, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := helper.DefaultRedis.LRem(rediskey.ApiUserDeleteList, item); err != nil {
|
||||||
|
logger.Errorf("移除待关闭websocket失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//连接Binance用户websocket
|
||||||
|
var apiUser models.LineApiUser
|
||||||
|
for _, item := range activeApis {
|
||||||
|
if item == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sonic.Unmarshal([]byte(item), &apiUser); err != nil {
|
||||||
|
logger.Errorf("解析用户信息失败: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只处理Binance用户
|
||||||
|
if apiUser.Id > 0 && apiUser.ExchangeType == global.EXCHANGE_BINANCE {
|
||||||
|
logger.Infof("为Binance用户 %d 建立连接", apiUser.Id)
|
||||||
|
service.OpenUserBinanceWebsocket(apiUser)
|
||||||
|
|
||||||
|
if _, err := helper.DefaultRedis.LRem(rediskey.ApiUserActiveList, item); err != nil {
|
||||||
|
logger.Errorf("删除待触发的apiUser失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定时清理日志
|
||||||
|
func clearLogJob(db *gorm.DB, ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(time.Hour * 1)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
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:
|
||||||
|
// 重启Binance连接
|
||||||
|
spotSockets := excservice.SpotSockets
|
||||||
|
futuresSockets := excservice.FutureSockets
|
||||||
|
timeOut := 22 * time.Hour
|
||||||
|
|
||||||
|
for _, item := range spotSockets {
|
||||||
|
if time.Since(item.ConnectTime) > timeOut {
|
||||||
|
if err := item.ReplaceConnection(); err != nil {
|
||||||
|
logger.Errorf("Binance现货重启连接失败 key:%s,error:%s", item.GetKey(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range futuresSockets {
|
||||||
|
if time.Since(item.ConnectTime) > timeOut {
|
||||||
|
if err := item.ReplaceConnection(); err != nil {
|
||||||
|
logger.Errorf("Binance合约重启连接失败 key:%s,error:%s", item.GetKey(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
271
cmd/bitget/bitget.go
Normal file
271
cmd/bitget/bitget.go
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
package bitget
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/common/const/rediskey"
|
||||||
|
"go-admin/common/database"
|
||||||
|
"go-admin/common/global"
|
||||||
|
"go-admin/common/helper"
|
||||||
|
"go-admin/common/storage"
|
||||||
|
"go-admin/config/serverinit"
|
||||||
|
"go-admin/pkg/utility"
|
||||||
|
"go-admin/services/bitgetservice"
|
||||||
|
"go-admin/services/fileservice"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
ext "go-admin/config"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
"github.com/go-admin-team/go-admin-core/config/source/file"
|
||||||
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/config"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/pkg"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
configYml string
|
||||||
|
StartCmd = &cobra.Command{
|
||||||
|
Use: "bitget",
|
||||||
|
Short: "Start Bitget service (market data + user private)",
|
||||||
|
Example: "go-admin bitget -c config/settings.yml",
|
||||||
|
SilenceUsage: true,
|
||||||
|
PreRun: func(cmd *cobra.Command, args []string) {
|
||||||
|
setup()
|
||||||
|
},
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
return run()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
StartCmd.PersistentFlags().StringVarP(&configYml, "config", "c", "config/settings.yml", "Start server with provided configuration file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setup() {
|
||||||
|
// 注入配置扩展项
|
||||||
|
config.ExtendConfig = &ext.ExtConfig
|
||||||
|
//1. 读取配置
|
||||||
|
config.Setup(
|
||||||
|
file.NewSource(file.WithPath(configYml)),
|
||||||
|
database.Setup,
|
||||||
|
storage.Setup,
|
||||||
|
)
|
||||||
|
//注册监听函数
|
||||||
|
queue := sdk.Runtime.GetMemoryQueue("")
|
||||||
|
queue.Register(global.LoginLog, models.SaveLoginLog)
|
||||||
|
queue.Register(global.OperateLog, models.SaveOperaLog)
|
||||||
|
go queue.Run()
|
||||||
|
|
||||||
|
usageStr := `starting Bitget service (market data + user private)...`
|
||||||
|
logger.Info(usageStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var db *gorm.DB
|
||||||
|
dbs := sdk.Runtime.GetDb()
|
||||||
|
|
||||||
|
for _, item := range dbs {
|
||||||
|
db = item
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultInit(db, ctx)
|
||||||
|
|
||||||
|
utility.SafeGo(func() {
|
||||||
|
// 启动定时任务
|
||||||
|
clearLogJob(db, ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
//自动重启websocket
|
||||||
|
utility.SafeGo(func() {
|
||||||
|
reconnect(ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
//动态用户连接管理
|
||||||
|
utility.SafeGo(func() {
|
||||||
|
manageBitgetUserConnections(ctx)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, os.Interrupt)
|
||||||
|
<-quit
|
||||||
|
|
||||||
|
fmt.Printf("%s Shutdown Server ... \r\n", pkg.GetCurrentTimeStr())
|
||||||
|
logger.Info("Server exiting")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultInit(db *gorm.DB, ctx context.Context) {
|
||||||
|
//初始化 默认redis
|
||||||
|
helper.InitDefaultRedis(ext.ExtConfig.Redis.Addr, ext.ExtConfig.Redis.Password, ext.ExtConfig.Redis.Db)
|
||||||
|
helper.InitLockRedisConn(ext.ExtConfig.Redis.Addr, ext.ExtConfig.Redis.Password, strconv.Itoa(ext.ExtConfig.Redis.Db))
|
||||||
|
|
||||||
|
err := helper.DefaultRedis.Ping()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("初始化redis失败!请检查配置")
|
||||||
|
_, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
cancel()
|
||||||
|
} else {
|
||||||
|
logger.Info("redis初始化成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
//初始化Bitget服务(行情 + 用户私有)
|
||||||
|
logger.Info("=== 启动Bitget完整服务 ===")
|
||||||
|
|
||||||
|
// 启动Bitget行情服务
|
||||||
|
logger.Info("启动Bitget行情服务...")
|
||||||
|
if err := serverinit.BitgetMarketInit(); err != nil {
|
||||||
|
logger.Errorf("Bitget行情服务启动失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化Bitget用户连接
|
||||||
|
logger.Info("启动Bitget用户服务...")
|
||||||
|
initBitgetUserConnections(db, ctx)
|
||||||
|
|
||||||
|
logger.Info("=== Bitget完整服务已启动 ===")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化Bitget用户连接
|
||||||
|
func initBitgetUserConnections(db *gorm.DB, ctx context.Context) {
|
||||||
|
var list []models.LineApiUser
|
||||||
|
err := db.Model(&models.LineApiUser{}).Where("open_status=1 AND exchange_type =?", global.EXCHANGE_BITGET).Find(&list).Error
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Infof("获取Bitget用户api失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Infof("找到 %d 个Bitget用户,开始建立连接...", len(list))
|
||||||
|
for _, item := range list {
|
||||||
|
// 遵循私有WebSocket密钥传递规范:外部传入API密钥参数
|
||||||
|
createBitgetUserConnection(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建Bitget用户连接(遵循密钥传递规范)
|
||||||
|
func createBitgetUserConnection(apiUser models.LineApiUser) {
|
||||||
|
logger.Infof("为Bitget用户 %d 建立私有连接", apiUser.Id)
|
||||||
|
|
||||||
|
// 现货WebSocket (wsType=0)
|
||||||
|
spotManager := bitgetservice.GetOrCreateBitgetWebSocketManager(
|
||||||
|
0, // 现货
|
||||||
|
apiUser.ApiKey,
|
||||||
|
apiUser.ApiSecret,
|
||||||
|
apiUser.Passphrase,
|
||||||
|
"", // proxyType - 从配置获取
|
||||||
|
"", // proxyAddress - 从配置获取
|
||||||
|
)
|
||||||
|
spotManager.Start()
|
||||||
|
|
||||||
|
// 合约WebSocket (wsType=1)
|
||||||
|
futuresManager := bitgetservice.GetOrCreateBitgetWebSocketManager(
|
||||||
|
1, // 合约
|
||||||
|
apiUser.ApiKey,
|
||||||
|
apiUser.ApiSecret,
|
||||||
|
apiUser.Passphrase,
|
||||||
|
"", // proxyType
|
||||||
|
"", // proxyAddress
|
||||||
|
)
|
||||||
|
futuresManager.Start()
|
||||||
|
|
||||||
|
logger.Infof("Bitget用户 %d 私有连接已建立", apiUser.Id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态Bitget用户连接管理
|
||||||
|
func manageBitgetUserConnections(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(time.Second * 5)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
logger.Info("Bitget用户连接管理器退出")
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
deleteKeys, _ := helper.DefaultRedis.GetAllList(rediskey.ApiUserDeleteList)
|
||||||
|
activeApis, _ := helper.DefaultRedis.GetAllList(rediskey.ApiUserActiveList)
|
||||||
|
|
||||||
|
//移除Bitget连接
|
||||||
|
for _, item := range deleteKeys {
|
||||||
|
logger.Infof("移除Bitget用户连接: %s", item)
|
||||||
|
|
||||||
|
// 停止现货WebSocket
|
||||||
|
bitgetservice.StopBitgetWebSocketManager(0, item)
|
||||||
|
// 停止合约WebSocket
|
||||||
|
bitgetservice.StopBitgetWebSocketManager(1, item)
|
||||||
|
|
||||||
|
if _, err := helper.DefaultRedis.LRem(rediskey.ApiUserDeleteList, item); err != nil {
|
||||||
|
logger.Errorf("移除待关闭websocket失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//连接Bitget用户websocket
|
||||||
|
var apiUser models.LineApiUser
|
||||||
|
for _, item := range activeApis {
|
||||||
|
if item == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sonic.Unmarshal([]byte(item), &apiUser); err != nil {
|
||||||
|
logger.Errorf("解析用户信息失败: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只处理Bitget用户
|
||||||
|
if apiUser.Id > 0 && apiUser.ExchangeType == global.EXCHANGE_BITGET {
|
||||||
|
logger.Info("为Bitget用户 %d 建立连接", apiUser.Id)
|
||||||
|
createBitgetUserConnection(apiUser)
|
||||||
|
|
||||||
|
if _, err := helper.DefaultRedis.LRem(rediskey.ApiUserActiveList, item); err != nil {
|
||||||
|
logger.Errorf("删除待触发的apiUser失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定时清理日志
|
||||||
|
func clearLogJob(db *gorm.DB, ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(time.Hour * 1)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
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:
|
||||||
|
// 这里可以添加Bitget连接的重连逻辑
|
||||||
|
// 类似于Binance的重连机制
|
||||||
|
logger.Info("检查Bitget连接状态...")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
14
cmd/cobra.go
14
cmd/cobra.go
@ -3,19 +3,19 @@ package cmd
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"go-admin/cmd/api"
|
||||||
"go-admin/cmd/app"
|
"go-admin/cmd/app"
|
||||||
|
"go-admin/cmd/binance"
|
||||||
|
"go-admin/cmd/bitget"
|
||||||
|
"go-admin/cmd/config"
|
||||||
|
"go-admin/cmd/migrate"
|
||||||
"go-admin/cmd/usersubscribe"
|
"go-admin/cmd/usersubscribe"
|
||||||
|
"go-admin/cmd/version"
|
||||||
"go-admin/common/global"
|
"go-admin/common/global"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/go-admin-team/go-admin-core/sdk/pkg"
|
"github.com/go-admin-team/go-admin-core/sdk/pkg"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"go-admin/cmd/api"
|
|
||||||
"go-admin/cmd/config"
|
|
||||||
"go-admin/cmd/migrate"
|
|
||||||
"go-admin/cmd/version"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
@ -46,6 +46,8 @@ func tip() {
|
|||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(api.StartCmd)
|
rootCmd.AddCommand(api.StartCmd)
|
||||||
rootCmd.AddCommand(usersubscribe.StartCmd)
|
rootCmd.AddCommand(usersubscribe.StartCmd)
|
||||||
|
rootCmd.AddCommand(binance.StartCmd)
|
||||||
|
rootCmd.AddCommand(bitget.StartCmd)
|
||||||
rootCmd.AddCommand(migrate.StartCmd)
|
rootCmd.AddCommand(migrate.StartCmd)
|
||||||
rootCmd.AddCommand(version.StartCmd)
|
rootCmd.AddCommand(version.StartCmd)
|
||||||
rootCmd.AddCommand(config.StartCmd)
|
rootCmd.AddCommand(config.StartCmd)
|
||||||
|
|||||||
@ -90,7 +90,6 @@ func run() error {
|
|||||||
utility.SafeGo(func() {
|
utility.SafeGo(func() {
|
||||||
reconnect(ctx)
|
reconnect(ctx)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)
|
// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
signal.Notify(quit, os.Interrupt)
|
signal.Notify(quit, os.Interrupt)
|
||||||
@ -153,18 +152,3 @@ func reconnect(ctx context.Context) error {
|
|||||||
|
|
||||||
return nil
|
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,23 +0,0 @@
|
|||||||
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"
|
|
||||||
)
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
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"
|
|
||||||
)
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
package binancecode
|
|
||||||
|
|
||||||
const (
|
|
||||||
SideBuy = "BUY"
|
|
||||||
SideSell = "SELL"
|
|
||||||
)
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
package rediskey
|
|
||||||
|
|
||||||
const (
|
|
||||||
//主单api和反单关系 List {apiId}:{reverseApiId}
|
|
||||||
ApiReverseRelation = "api_reverse_relation"
|
|
||||||
)
|
|
||||||
@ -19,11 +19,13 @@ const (
|
|||||||
PCRegisterMobile = "_PCRegisterMobile_%v" // 用户注册时手机key
|
PCRegisterMobile = "_PCRegisterMobile_%v" // 用户注册时手机key
|
||||||
PCResetPwdMobile = "_PCResetPwdMobile_%v" // 用户重置密码时手机key
|
PCResetPwdMobile = "_PCResetPwdMobile_%v" // 用户重置密码时手机key
|
||||||
|
|
||||||
SpotSymbolTicker = "_SpotSymbolTicker_" // 现货交易对行情
|
// SpotSymbolTicker = "_SpotSymbolTicker_" // 现货交易对行情
|
||||||
FutSymbolTicker = "_FutSymbolTicker_" // 合约交易对行情
|
// FutSymbolTicker = "_FutSymbolTicker_" // 合约交易对行情
|
||||||
PreOrderScriptList = "_ProOrderScriptList_" // 脚本执行list
|
PreOrderScriptList = "_ProOrderScriptList_" // 脚本执行list
|
||||||
PreSpotOrderList = "_PreSpotOrderList_:%s" // 待触发的现货订单集合{交易所类型 exchange_type}
|
// 待触发的现货订单集合{交易所类型 exchange_type}
|
||||||
PreFutOrderList = "_PreFutOrderList_:%s" // 待触发的订单集合 {交易所类型 exchange_type}
|
PreSpotOrderList = "_PreSpotOrderList_:%s"
|
||||||
|
// 待触发的订单集合 {交易所类型 exchange_type}
|
||||||
|
PreFutOrderList = "_PreFutOrderList_:%s"
|
||||||
|
|
||||||
//策略现货订单集合 {交易所类型 exchange_type}
|
//策略现货订单集合 {交易所类型 exchange_type}
|
||||||
StrategySpotOrderList = "strategy_spot_order_list:%s"
|
StrategySpotOrderList = "strategy_spot_order_list:%s"
|
||||||
@ -46,9 +48,9 @@ const (
|
|||||||
SpotTrigger = "spot_trigger_lock:%v_%s" //现货触发 {apiuserid|symbol}
|
SpotTrigger = "spot_trigger_lock:%v_%s" //现货触发 {apiuserid|symbol}
|
||||||
FutTrigger = "fut_trigger_lock:%v_%s" //合约触发 {apiuserid|symbol}
|
FutTrigger = "fut_trigger_lock:%v_%s" //合约触发 {apiuserid|symbol}
|
||||||
|
|
||||||
//波段现货触发{apiuserid|ordersn}
|
//波段现货触发{apiuserid|symbol}
|
||||||
StrategySpotTriggerLock = "strategy_spot_trigger_l:%v_%s"
|
StrategySpotTriggerLock = "strategy_spot_trigger_l:%v_%s"
|
||||||
//波段合约触发{apiuserid|ordersn}
|
//波段合约触发{apiuserid|symbol}
|
||||||
StrategyFutTriggerLock = "strategy_fut_trigger_l:%v_%s"
|
StrategyFutTriggerLock = "strategy_fut_trigger_l:%v_%s"
|
||||||
|
|
||||||
//减仓波段合约触发 {apiuserid|symbol}
|
//减仓波段合约触发 {apiuserid|symbol}
|
||||||
@ -95,9 +97,9 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
//现货最后成交价 sort set key:={exchangeType,symbol} content:={utc:price}
|
//现货最后成交价 sort set {exchangeType,symbol}
|
||||||
SpotTickerLastPrice = "spot_ticker_last_price:%s:%s"
|
SpotTickerLastPrice = "spot_ticker_last_price:%s:%s"
|
||||||
//合约最后成交价 sort set {exchangeType,symbol} content:={utc:price}
|
//合约最后成交价 sort set {exchangeType,symbol}
|
||||||
FutureTickerLastPrice = "fut_ticker_last_price:%s:%s"
|
FutureTickerLastPrice = "fut_ticker_last_price:%s:%s"
|
||||||
|
|
||||||
//允许缓存交易对价格的交易对 list
|
//允许缓存交易对价格的交易对 list
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
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"
|
|
||||||
)
|
|
||||||
107
common/factory/exchange_factory.go
Normal file
107
common/factory/exchange_factory.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package factory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go-admin/common/global"
|
||||||
|
"go-admin/common/interfaces"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExchangeFactory 交易所工厂
|
||||||
|
type ExchangeFactory struct {
|
||||||
|
clients map[string]interfaces.ExchangeInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExchangeFactory 创建交易所工厂实例
|
||||||
|
func NewExchangeFactory() *ExchangeFactory {
|
||||||
|
return &ExchangeFactory{
|
||||||
|
clients: make(map[string]interfaces.ExchangeInterface),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExchange 获取指定交易所的客户端
|
||||||
|
func (f *ExchangeFactory) GetExchange(exchangeType string) (interfaces.ExchangeInterface, error) {
|
||||||
|
exchangeType = strings.ToLower(strings.TrimSpace(exchangeType))
|
||||||
|
|
||||||
|
// 检查是否已经创建了该交易所的客户端
|
||||||
|
if client, exists := f.clients[exchangeType]; exists {
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据交易所类型创建对应的客户端
|
||||||
|
var client interfaces.ExchangeInterface
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch exchangeType {
|
||||||
|
case global.EXCHANGE_BINANCE:
|
||||||
|
client, err = f.createBinanceClient()
|
||||||
|
case global.EXCHANGE_BITGET:
|
||||||
|
client, err = f.createBitgetClient()
|
||||||
|
case global.EXCHANGE_OKEX:
|
||||||
|
return nil, fmt.Errorf("OKEx integration not implemented yet")
|
||||||
|
case global.EXCHANGE_GATE:
|
||||||
|
return nil, fmt.Errorf("Gate.io integration not implemented yet")
|
||||||
|
case global.EXCHANGE_COINBASE:
|
||||||
|
return nil, fmt.Errorf("Coinbase integration not implemented yet")
|
||||||
|
case global.EXCHANGE_BITFINEX:
|
||||||
|
return nil, fmt.Errorf("Bitfinex integration not implemented yet")
|
||||||
|
case global.EXCHANGE_BITMEX:
|
||||||
|
return nil, fmt.Errorf("BitMEX integration not implemented yet")
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported exchange type: %s", exchangeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create %s client: %v", exchangeType, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存客户端实例
|
||||||
|
f.clients[exchangeType] = client
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createBinanceClient 创建Binance客户端包装器
|
||||||
|
func (f *ExchangeFactory) createBinanceClient() (interfaces.ExchangeInterface, error) {
|
||||||
|
// Binance适配器尚未完全实现,暴时返回错误
|
||||||
|
return nil, fmt.Errorf("Binance adapter not fully implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
// createBitgetClient 创建Bitget客户端
|
||||||
|
func (f *ExchangeFactory) createBitgetClient() (interfaces.ExchangeInterface, error) {
|
||||||
|
// 使用默认空参数创建Bitget客户端(公开接口不需要认证)
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("Bitget adapter not fully implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSupportedExchanges 获取支持的交易所列表
|
||||||
|
func (f *ExchangeFactory) GetSupportedExchanges() []string {
|
||||||
|
return []string{
|
||||||
|
// global.EXCHANGE_BINANCE, // Binance适配器尚未完全实现
|
||||||
|
global.EXCHANGE_BITGET,
|
||||||
|
// 其他交易所可以在未来添加
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterExchange 注册自定义交易所客户端
|
||||||
|
func (f *ExchangeFactory) RegisterExchange(exchangeType string, client interfaces.ExchangeInterface) {
|
||||||
|
f.clients[strings.ToLower(exchangeType)] = client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearCache 清理缓存的客户端
|
||||||
|
func (f *ExchangeFactory) ClearCache() {
|
||||||
|
for _, client := range f.clients {
|
||||||
|
if closer, ok := client.(interface{ Close() error }); ok {
|
||||||
|
closer.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.clients = make(map[string]interfaces.ExchangeInterface)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局工厂实例
|
||||||
|
var DefaultExchangeFactory = NewExchangeFactory()
|
||||||
|
|
||||||
|
// GetExchangeClient 获取交易所客户端的便捷函数
|
||||||
|
func GetExchangeClient(exchangeType string) (interfaces.ExchangeInterface, error) {
|
||||||
|
return DefaultExchangeFactory.GetExchange(exchangeType)
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ package global
|
|||||||
//交易所类型字典
|
//交易所类型字典
|
||||||
const (
|
const (
|
||||||
EXCHANGE_BINANCE = "binance"
|
EXCHANGE_BINANCE = "binance"
|
||||||
|
EXCHANGE_BITGET = "bitget"
|
||||||
EXCHANGE_OKEX = "okex"
|
EXCHANGE_OKEX = "okex"
|
||||||
EXCHANGE_GATE = "gate"
|
EXCHANGE_GATE = "gate"
|
||||||
EXCHANGE_COINBASE = "coinbase"
|
EXCHANGE_COINBASE = "coinbase"
|
||||||
|
|||||||
@ -17,8 +17,8 @@ const (
|
|||||||
K_SPOT = "k_spot"
|
K_SPOT = "k_spot"
|
||||||
//代币-配置
|
//代币-配置
|
||||||
COIN_DETAIL = "c_spot_detail"
|
COIN_DETAIL = "c_spot_detail"
|
||||||
//代币-现货24h涨跌幅
|
//代币-现货24h涨跌幅 {exchange}
|
||||||
COIN_PRICE_CHANGE = "c_spot_priceChange"
|
COIN_PRICE_CHANGE = "c_spot_priceChange:%s"
|
||||||
//代币-现货热门排序
|
//代币-现货热门排序
|
||||||
COIN_HOT_SORT = "c_spot_hot_sort"
|
COIN_HOT_SORT = "c_spot_hot_sort"
|
||||||
//代币-现货新币排序
|
//代币-现货新币排序
|
||||||
|
|||||||
255
common/helper/bitgethttphelper.go
Normal file
255
common/helper/bitgethttphelper.go
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BitgetClient Bitget HTTP客户端
|
||||||
|
type BitgetClient struct {
|
||||||
|
APIKey string
|
||||||
|
APISecret string
|
||||||
|
Passphrase string
|
||||||
|
HTTPClient *http.Client
|
||||||
|
BaseURL string
|
||||||
|
SignType string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBitgetClient 创建新的Bitget客户端
|
||||||
|
func NewBitgetClient(apiKey, apiSecret, passphrase string, proxyUrl string) (*BitgetClient, error) {
|
||||||
|
var proxyType, proxyAddr string
|
||||||
|
if proxyUrl != "" {
|
||||||
|
if strings.HasPrefix(proxyUrl, "http://") {
|
||||||
|
proxyType = "http"
|
||||||
|
proxyAddr = proxyUrl
|
||||||
|
} else if strings.HasPrefix(proxyUrl, "https://") {
|
||||||
|
proxyType = "https"
|
||||||
|
proxyAddr = proxyUrl
|
||||||
|
} else if strings.HasPrefix(proxyUrl, "socks5://") {
|
||||||
|
proxyType = "socks5"
|
||||||
|
proxyAddr = proxyUrl
|
||||||
|
} else {
|
||||||
|
// 默认为http代理
|
||||||
|
proxyType = "http"
|
||||||
|
proxyAddr = "http://" + proxyUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 创建HTTP客户端
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
MaxIdleConns: 1000,
|
||||||
|
IdleConnTimeout: 10 * time.Second,
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置代理
|
||||||
|
if proxyAddr != "" {
|
||||||
|
err := CreateHtppProxy(proxyType, proxyAddr, client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BitgetClient{
|
||||||
|
APIKey: apiKey,
|
||||||
|
APISecret: apiSecret,
|
||||||
|
Passphrase: passphrase,
|
||||||
|
HTTPClient: client,
|
||||||
|
BaseURL: "https://api.bitget.com",
|
||||||
|
SignType: "SHA256",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoGet 执行GET请求
|
||||||
|
func (bc *BitgetClient) DoGet(uri string, params map[string]string) ([]byte, error) {
|
||||||
|
timestamp := bc.getTimestamp()
|
||||||
|
body := bc.buildGetParams(params)
|
||||||
|
|
||||||
|
sign := bc.sign("GET", uri, body, timestamp)
|
||||||
|
requestURL := bc.BaseURL + uri + body
|
||||||
|
|
||||||
|
request, err := http.NewRequest("GET", requestURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bc.setHeaders(request, timestamp, sign)
|
||||||
|
|
||||||
|
response, err := bc.HTTPClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
return ioutil.ReadAll(response.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoPost 执行POST请求
|
||||||
|
func (bc *BitgetClient) DoPost(uri string, params interface{}) ([]byte, error) {
|
||||||
|
timestamp := bc.getTimestamp()
|
||||||
|
|
||||||
|
var body string
|
||||||
|
if params != nil {
|
||||||
|
bodyBytes, err := json.Marshal(params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
body = string(bodyBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
sign := bc.sign("POST", uri, body, timestamp)
|
||||||
|
requestURL := bc.BaseURL + uri
|
||||||
|
|
||||||
|
request, err := http.NewRequest("POST", requestURL, strings.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bc.setHeaders(request, timestamp, sign)
|
||||||
|
|
||||||
|
response, err := bc.HTTPClient.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
return ioutil.ReadAll(response.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTimestamp 获取时间戳
|
||||||
|
func (bc *BitgetClient) getTimestamp() string {
|
||||||
|
return strconv.FormatInt(time.Now().Unix()*1000, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setHeaders 设置请求头
|
||||||
|
func (bc *BitgetClient) setHeaders(request *http.Request, timestamp, sign string) {
|
||||||
|
request.Header.Add("Content-Type", "application/json")
|
||||||
|
request.Header.Add("ACCESS-KEY", bc.APIKey)
|
||||||
|
request.Header.Add("ACCESS-SIGN", sign)
|
||||||
|
request.Header.Add("ACCESS-TIMESTAMP", timestamp)
|
||||||
|
request.Header.Add("ACCESS-PASSPHRASE", bc.Passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sign 生成签名
|
||||||
|
func (bc *BitgetClient) sign(method, requestPath, body, timestamp string) string {
|
||||||
|
var payload strings.Builder
|
||||||
|
payload.WriteString(timestamp)
|
||||||
|
payload.WriteString(method)
|
||||||
|
payload.WriteString(requestPath)
|
||||||
|
if body != "" && body != "?" {
|
||||||
|
payload.WriteString(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bc.SignType == "RSA" {
|
||||||
|
return bc.signByRSA(payload.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return bc.signByHMAC(payload.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// signByHMAC HMAC签名
|
||||||
|
func (bc *BitgetClient) signByHMAC(payload string) string {
|
||||||
|
hash := hmac.New(sha256.New, []byte(bc.APISecret))
|
||||||
|
hash.Write([]byte(payload))
|
||||||
|
return base64.StdEncoding.EncodeToString(hash.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// signByRSA RSA签名
|
||||||
|
func (bc *BitgetClient) signByRSA(payload string) string {
|
||||||
|
sign, _ := bc.rsaSign([]byte(payload), []byte(bc.APISecret), crypto.SHA256)
|
||||||
|
return base64.StdEncoding.EncodeToString(sign)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rsaSign RSA签名实现
|
||||||
|
func (bc *BitgetClient) rsaSign(src []byte, priKey []byte, hash crypto.Hash) ([]byte, error) {
|
||||||
|
block, _ := pem.Decode(priKey)
|
||||||
|
if block == nil {
|
||||||
|
return nil, errors.New("key is invalid format")
|
||||||
|
}
|
||||||
|
|
||||||
|
var pkixPrivateKey interface{}
|
||||||
|
var err error
|
||||||
|
if block.Type == "RSA PRIVATE KEY" {
|
||||||
|
pkixPrivateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||||
|
} else if block.Type == "PRIVATE KEY" {
|
||||||
|
pkixPrivateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
h := hash.New()
|
||||||
|
_, err = h.Write(src)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes := h.Sum(nil)
|
||||||
|
sign, err := rsa.SignPKCS1v15(rand.Reader, pkixPrivateKey.(*rsa.PrivateKey), hash, bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sign, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildGetParams 构建GET请求参数
|
||||||
|
func (bc *BitgetClient) buildGetParams(params map[string]string) string {
|
||||||
|
if len(params) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return "?" + bc.sortParams(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sortParams 参数排序
|
||||||
|
func (bc *BitgetClient) sortParams(params map[string]string) string {
|
||||||
|
keys := make([]string, 0, len(params))
|
||||||
|
for k := range params {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
sorted := make([]string, 0, len(params))
|
||||||
|
for _, k := range keys {
|
||||||
|
sorted = append(sorted, k+"="+params[k])
|
||||||
|
}
|
||||||
|
return strings.Join(sorted, "&")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendSpotRequestAuth 发送现货认证请求
|
||||||
|
func (bc *BitgetClient) SendSpotRequestAuth(endpoint, method string, params map[string]interface{}) ([]byte, *http.Response, error) {
|
||||||
|
if method == "GET" {
|
||||||
|
paramStr := make(map[string]string)
|
||||||
|
for k, v := range params {
|
||||||
|
paramStr[k] = fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
resp, err := bc.DoGet(endpoint, paramStr)
|
||||||
|
return resp, nil, err
|
||||||
|
} else {
|
||||||
|
resp, err := bc.DoPost(endpoint, params)
|
||||||
|
return resp, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendFuturesRequestAuth 发送合约认证请求
|
||||||
|
func (bc *BitgetClient) SendFuturesRequestAuth(endpoint, method string, params map[string]interface{}) ([]byte, *http.Response, error) {
|
||||||
|
return bc.SendSpotRequestAuth(endpoint, method, params)
|
||||||
|
}
|
||||||
@ -12,7 +12,6 @@ import (
|
|||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/go-redis/redis/v8"
|
"github.com/go-redis/redis/v8"
|
||||||
"github.com/shopspring/decimal"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RedisHelper 结构体封装了 Redis 客户端及上下文
|
// RedisHelper 结构体封装了 Redis 客户端及上下文
|
||||||
@ -461,76 +460,44 @@ func (r *RedisHelper) SetNX(key string, value interface{}, expiration time.Durat
|
|||||||
return result == "OK", nil
|
return result == "OK", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetHashWithTags 改进版:支持 struct 或 map 输入
|
func getFieldsFromStruct(obj interface{}) map[string]interface{} {
|
||||||
func (r *RedisHelper) SetHashWithTags(key string, obj interface{}) error {
|
|
||||||
fields, err := getFieldsFromStruct(obj)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = r.client.HSet(r.ctx, key, fields).Result()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// getFieldsFromStruct 改进版:处理指针并进行类型检查,返回 error
|
|
||||||
func getFieldsFromStruct(obj interface{}) (map[string]interface{}, error) {
|
|
||||||
fields := make(map[string]interface{})
|
fields := make(map[string]interface{})
|
||||||
val := reflect.ValueOf(obj)
|
val := reflect.ValueOf(obj)
|
||||||
|
typ := reflect.TypeOf(obj)
|
||||||
// 如果 obj 是指针,则解引用
|
|
||||||
if val.Kind() == reflect.Ptr {
|
|
||||||
val = val.Elem()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确保我们正在处理的是一个结构体
|
|
||||||
if val.Kind() != reflect.Struct {
|
|
||||||
return nil, fmt.Errorf("期望一个结构体或结构体指针,但得到的是 %s", val.Kind())
|
|
||||||
}
|
|
||||||
|
|
||||||
typ := val.Type() // 在解引用后获取类型
|
|
||||||
|
|
||||||
for i := 0; i < val.NumField(); i++ {
|
for i := 0; i < val.NumField(); i++ {
|
||||||
field := typ.Field(i)
|
field := typ.Field(i)
|
||||||
tag := field.Tag.Get("redis") // 获取 redis tag
|
tag := field.Tag.Get("redis")
|
||||||
if tag != "" {
|
if tag != "" {
|
||||||
fieldVal := val.Field(i)
|
fieldVal := val.Field(i)
|
||||||
// 检查字段是否可导出,不可导出的字段无法直接通过 Interface() 访问
|
if fieldVal.Kind() == reflect.Slice || fieldVal.Kind() == reflect.Map {
|
||||||
if !fieldVal.CanInterface() {
|
|
||||||
continue // 跳过不可导出字段
|
|
||||||
}
|
|
||||||
|
|
||||||
switch fieldVal.Kind() {
|
|
||||||
case reflect.Slice, reflect.Map:
|
|
||||||
// 处理切片或映射类型
|
// 处理切片或映射类型
|
||||||
|
// 对于切片,使用索引作为字段名
|
||||||
if fieldVal.Kind() == reflect.Slice {
|
if fieldVal.Kind() == reflect.Slice {
|
||||||
for j := 0; j < fieldVal.Len(); j++ {
|
for j := 0; j < fieldVal.Len(); j++ {
|
||||||
elem := fieldVal.Index(j).Interface()
|
elem := fieldVal.Index(j).Interface()
|
||||||
fields[fmt.Sprintf("%s_%d", tag, j)] = elem
|
fields[fmt.Sprintf("%s_%d", tag, j)] = elem
|
||||||
}
|
}
|
||||||
} else if fieldVal.Kind() == reflect.Map {
|
} else if fieldVal.Kind() == reflect.Map {
|
||||||
|
// 对于映射,使用键作为字段名
|
||||||
for _, key := range fieldVal.MapKeys() {
|
for _, key := range fieldVal.MapKeys() {
|
||||||
elem := fieldVal.MapIndex(key).Interface()
|
elem := fieldVal.MapIndex(key).Interface()
|
||||||
fields[fmt.Sprintf("%s_%v", tag, key.Interface())] = elem
|
fields[fmt.Sprintf("%s_%v", tag, key.Interface())] = elem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 {
|
} else {
|
||||||
fields[tag] = fieldVal.Interface()
|
fields[tag] = fieldVal.Interface()
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
fields[tag] = fieldVal.Interface()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return fields, nil
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *RedisHelper) SetHashWithTags(key string, obj interface{}) error {
|
||||||
|
fields := getFieldsFromStruct(obj)
|
||||||
|
_, err := r.client.HSet(r.ctx, key, fields).Result()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// HSetField 设置哈希中的一个字段
|
// HSetField 设置哈希中的一个字段
|
||||||
@ -576,82 +543,6 @@ func (r *RedisHelper) HGetAllFields(key string) (map[string]string, error) {
|
|||||||
return fields, nil
|
return fields, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HGetAsObject 获取哈希中所有字段的值并反序列化为对象
|
|
||||||
func (r *RedisHelper) HGetAsObject(key string, out interface{}) error {
|
|
||||||
data, err := r.HGetAllFields(key)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
val := reflect.ValueOf(out)
|
|
||||||
if val.Kind() != reflect.Ptr || val.IsNil() {
|
|
||||||
return fmt.Errorf("output must be a non-nil pointer to a struct")
|
|
||||||
}
|
|
||||||
val = val.Elem()
|
|
||||||
typ := val.Type()
|
|
||||||
|
|
||||||
for i := 0; i < typ.NumField(); i++ {
|
|
||||||
field := typ.Field(i)
|
|
||||||
tag := field.Tag.Get("redis")
|
|
||||||
if tag == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
strVal, ok := data[tag]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldVal := val.Field(i)
|
|
||||||
if !fieldVal.CanSet() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
switch fieldVal.Kind() {
|
|
||||||
case reflect.String:
|
|
||||||
fieldVal.SetString(strVal)
|
|
||||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
||||||
if intVal, err := strconv.ParseInt(strVal, 10, 64); err == nil {
|
|
||||||
fieldVal.SetInt(intVal)
|
|
||||||
}
|
|
||||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
||||||
if uintVal, err := strconv.ParseUint(strVal, 10, 64); err == nil {
|
|
||||||
fieldVal.SetUint(uintVal)
|
|
||||||
}
|
|
||||||
case reflect.Float32, reflect.Float64:
|
|
||||||
if floatVal, err := strconv.ParseFloat(strVal, 64); err == nil {
|
|
||||||
fieldVal.SetFloat(floatVal)
|
|
||||||
}
|
|
||||||
case reflect.Bool:
|
|
||||||
if boolVal, err := strconv.ParseBool(strVal); err == nil {
|
|
||||||
fieldVal.SetBool(boolVal)
|
|
||||||
}
|
|
||||||
case reflect.Struct:
|
|
||||||
// 针对 time.Time 特别处理
|
|
||||||
if fieldVal.Type() == reflect.TypeOf(time.Time{}) {
|
|
||||||
if t, err := time.Parse(time.RFC3339, strVal); err == nil {
|
|
||||||
fieldVal.Set(reflect.ValueOf(t))
|
|
||||||
}
|
|
||||||
} else if fieldVal.Type() == reflect.TypeOf(decimal.Decimal{}) {
|
|
||||||
if t, err := decimal.NewFromString(strVal); err == nil {
|
|
||||||
fieldVal.Set(reflect.ValueOf(t))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 其他 struct 尝试用 json 反序列化
|
|
||||||
ptr := reflect.New(fieldVal.Type()).Interface()
|
|
||||||
if err := sonic.Unmarshal([]byte(strVal), ptr); err == nil {
|
|
||||||
fieldVal.Set(reflect.ValueOf(ptr).Elem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case reflect.Slice, reflect.Map:
|
|
||||||
ptr := reflect.New(fieldVal.Type()).Interface()
|
|
||||||
if err := sonic.Unmarshal([]byte(strVal), ptr); err == nil {
|
|
||||||
fieldVal.Set(reflect.ValueOf(ptr).Elem())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HDelField 删除哈希中的某个字段
|
// HDelField 删除哈希中的某个字段
|
||||||
func (r *RedisHelper) HDelField(key, field string) error {
|
func (r *RedisHelper) HDelField(key, field string) error {
|
||||||
_, err := r.client.HDel(r.ctx, key, field).Result()
|
_, err := r.client.HDel(r.ctx, key, field).Result()
|
||||||
|
|||||||
@ -106,9 +106,6 @@ func (rl *RedisLock) AcquireWait(ctx context.Context) (bool, error) {
|
|||||||
baseInterval = time.Second
|
baseInterval = time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
if baseInterval <= 0 {
|
|
||||||
baseInterval = time.Millisecond * 100 // 至少 100ms
|
|
||||||
}
|
|
||||||
// 随机退避
|
// 随机退避
|
||||||
retryInterval := time.Duration(rand.Int63n(int64(baseInterval))) // 随机退避
|
retryInterval := time.Duration(rand.Int63n(int64(baseInterval))) // 随机退避
|
||||||
if retryInterval < time.Millisecond*100 {
|
if retryInterval < time.Millisecond*100 {
|
||||||
@ -132,13 +129,6 @@ func (rl *RedisLock) AcquireWait(ctx context.Context) (bool, error) {
|
|||||||
return false, ErrFailed
|
return false, ErrFailed
|
||||||
}
|
}
|
||||||
|
|
||||||
func safeRandomDuration(max time.Duration) time.Duration {
|
|
||||||
if max <= 0 {
|
|
||||||
return 100 * time.Millisecond // fallback default
|
|
||||||
}
|
|
||||||
return time.Duration(rand.Int63n(int64(max)))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release 释放锁
|
// Release 释放锁
|
||||||
func (rl *RedisLock) Release() (bool, error) {
|
func (rl *RedisLock) Release() (bool, error) {
|
||||||
return rl.releaseCtx(context.Background())
|
return rl.releaseCtx(context.Background())
|
||||||
|
|||||||
170
common/interfaces/exchange.go
Normal file
170
common/interfaces/exchange.go
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
package interfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go-admin/models"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExchangeInterface 交易所通用接口抽象层
|
||||||
|
type ExchangeInterface interface {
|
||||||
|
// 基础信息接口
|
||||||
|
GetExchangeName() string
|
||||||
|
GetExchangeInfo() (ExchangeInfoResponse, error)
|
||||||
|
|
||||||
|
// 现货市场接口
|
||||||
|
SpotInterface
|
||||||
|
|
||||||
|
// 合约接口(如果支持)
|
||||||
|
FuturesInterface
|
||||||
|
|
||||||
|
// WebSocket接口
|
||||||
|
WebSocketInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
// SpotInterface 现货交易接口
|
||||||
|
type SpotInterface interface {
|
||||||
|
// 市场数据
|
||||||
|
GetSpotTicker24h(symbol string) (models.Ticker24, error)
|
||||||
|
GetSpotTickers() ([]models.TradeSet, error)
|
||||||
|
// 订单管理
|
||||||
|
PlaceSpotOrder(params SpotOrderParams) (SpotOrderResponse, error)
|
||||||
|
CancelSpotOrder(symbol, orderID string) error
|
||||||
|
GetSpotOrder(symbol, orderID string) (SpotOrderResponse, error)
|
||||||
|
GetSpotOrders(symbol string) ([]SpotOrderResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuturesInterface 合约交易接口
|
||||||
|
type FuturesInterface interface {
|
||||||
|
// 市场数据
|
||||||
|
GetFuturesTicker24h(symbol string) (models.Ticker24, error)
|
||||||
|
GetFuturesTickers() ([]models.TradeSet, error)
|
||||||
|
// 订单管理
|
||||||
|
PlaceFuturesOrder(params FuturesOrderParams) (FuturesOrderResponse, error)
|
||||||
|
CancelFuturesOrder(symbol, orderID string) error
|
||||||
|
GetFuturesOrder(symbol, orderID string) (FuturesOrderResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocketInterface WebSocket接口
|
||||||
|
type WebSocketInterface interface {
|
||||||
|
SubscribeSpotTicker(symbols []string, callback func(models.Ticker24, string, string)) error
|
||||||
|
SubscribeFuturesTicker(symbols []string, callback func(models.Ticker24, string, string)) error
|
||||||
|
SubscribeSpotDepth(symbols []string, callback func(models.DepthBin, string, string)) error
|
||||||
|
SubscribeSpotTrades(symbols []string, callback func(models.NewDealPush, string, string)) error
|
||||||
|
SubscribeKline(symbols []string, interval string, callback func(models.Kline, int, string, string)) error
|
||||||
|
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用数据结构定义
|
||||||
|
type ExchangeInfoResponse struct {
|
||||||
|
Symbols []SymbolInfo `json:"symbols"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SymbolInfo struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
BaseAsset string `json:"baseAsset"`
|
||||||
|
QuoteAsset string `json:"quoteAsset"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
MinQty string `json:"minQty"`
|
||||||
|
MaxQty string `json:"maxQty"`
|
||||||
|
StepSize string `json:"stepSize"`
|
||||||
|
MinPrice string `json:"minPrice"`
|
||||||
|
MaxPrice string `json:"maxPrice"`
|
||||||
|
TickSize string `json:"tickSize"`
|
||||||
|
MinNotional string `json:"minNotional"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 现货订单参数
|
||||||
|
type SpotOrderParams struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Side string `json:"side"` // BUY/SELL
|
||||||
|
Type string `json:"type"` // MARKET/LIMIT
|
||||||
|
Quantity decimal.Decimal `json:"quantity"`
|
||||||
|
Price decimal.Decimal `json:"price,omitempty"`
|
||||||
|
TimeInForce string `json:"timeInForce,omitempty"` // GTC/IOC/FOK
|
||||||
|
NewClientOrderId string `json:"newClientOrderId,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合约订单参数
|
||||||
|
type FuturesOrderParams struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Side string `json:"side"` // BUY/SELL
|
||||||
|
Type string `json:"type"` // MARKET/LIMIT
|
||||||
|
Quantity decimal.Decimal `json:"quantity"`
|
||||||
|
Price decimal.Decimal `json:"price,omitempty"`
|
||||||
|
TimeInForce string `json:"timeInForce,omitempty"`
|
||||||
|
PositionSide string `json:"positionSide,omitempty"` // LONG/SHORT/BOTH
|
||||||
|
NewClientOrderId string `json:"newClientOrderId,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应数据结构
|
||||||
|
type SpotAccountResponse struct {
|
||||||
|
Balances []SpotBalanceResponse `json:"balances"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpotBalanceResponse struct {
|
||||||
|
Asset string `json:"asset"`
|
||||||
|
Free string `json:"free"`
|
||||||
|
Locked string `json:"locked"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SpotOrderResponse struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
OrderId int64 `json:"orderId"`
|
||||||
|
ClientOrderId string `json:"clientOrderId"`
|
||||||
|
Price string `json:"price"`
|
||||||
|
OrigQty string `json:"origQty"`
|
||||||
|
ExecutedQty string `json:"executedQty"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
TimeInForce string `json:"timeInForce"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Side string `json:"side"`
|
||||||
|
Time int64 `json:"time"`
|
||||||
|
UpdateTime int64 `json:"updateTime"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FuturesAccountResponse struct {
|
||||||
|
TotalWalletBalance string `json:"totalWalletBalance"`
|
||||||
|
Assets []FuturesBalanceResponse `json:"assets"`
|
||||||
|
Positions []FuturesPositionResponse `json:"positions"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FuturesBalanceResponse struct {
|
||||||
|
Asset string `json:"asset"`
|
||||||
|
WalletBalance string `json:"walletBalance"`
|
||||||
|
UnrealizedProfit string `json:"unrealizedProfit"`
|
||||||
|
MarginBalance string `json:"marginBalance"`
|
||||||
|
MaintMargin string `json:"maintMargin"`
|
||||||
|
InitialMargin string `json:"initialMargin"`
|
||||||
|
PositionInitialMargin string `json:"positionInitialMargin"`
|
||||||
|
OpenOrderInitialMargin string `json:"openOrderInitialMargin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FuturesPositionResponse struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
PositionAmt string `json:"positionAmt"`
|
||||||
|
EntryPrice string `json:"entryPrice"`
|
||||||
|
MarkPrice string `json:"markPrice"`
|
||||||
|
UnrealizedProfit string `json:"unrealizedProfit"`
|
||||||
|
LiquidationPrice string `json:"liquidationPrice"`
|
||||||
|
Leverage string `json:"leverage"`
|
||||||
|
PositionSide string `json:"positionSide"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FuturesOrderResponse struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
OrderId int64 `json:"orderId"`
|
||||||
|
ClientOrderId string `json:"clientOrderId"`
|
||||||
|
Price string `json:"price"`
|
||||||
|
OrigQty string `json:"origQty"`
|
||||||
|
ExecutedQty string `json:"executedQty"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
TimeInForce string `json:"timeInForce"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Side string `json:"side"`
|
||||||
|
PositionSide string `json:"positionSide"`
|
||||||
|
ReduceOnly bool `json:"reduceOnly"`
|
||||||
|
Time int64 `json:"time"`
|
||||||
|
UpdateTime int64 `json:"updateTime"`
|
||||||
|
}
|
||||||
@ -14,7 +14,6 @@ type Extend struct {
|
|||||||
Redis RedisConfig `mapstructure:"redis"`
|
Redis RedisConfig `mapstructure:"redis"`
|
||||||
ServiceId int64 `mapstructure:"serviceId"` //雪花算法id(服务id,比如1,2,3等,当多机部署需定义唯一id)
|
ServiceId int64 `mapstructure:"serviceId"` //雪花算法id(服务id,比如1,2,3等,当多机部署需定义唯一id)
|
||||||
EmailConfig EmailConfig `mapstructure:"emailConfig"`
|
EmailConfig EmailConfig `mapstructure:"emailConfig"`
|
||||||
BinanceSet BinanceConfig `mapstructure:"binanceSet"` //binance配置
|
|
||||||
Domain string //网站域名
|
Domain string //网站域名
|
||||||
UDunConfig UDunConfig `mapstructure:"UDunConfig"`
|
UDunConfig UDunConfig `mapstructure:"UDunConfig"`
|
||||||
ProxyUrl string //代理地址
|
ProxyUrl string //代理地址
|
||||||
@ -58,12 +57,6 @@ type EmailSend struct {
|
|||||||
To string `json:"to"` // 收件地址
|
To string `json:"to"` // 收件地址
|
||||||
}
|
}
|
||||||
|
|
||||||
// BinanceConfig 币安路由配置
|
|
||||||
type BinanceConfig struct {
|
|
||||||
SpotRestURL string
|
|
||||||
FutRestURL string
|
|
||||||
}
|
|
||||||
|
|
||||||
type GoToneSmsConfig struct {
|
type GoToneSmsConfig struct {
|
||||||
SenderId string `json:"sender_id"`
|
SenderId string `json:"sender_id"`
|
||||||
APIEndpoint string `json:"api_endpoint"`
|
APIEndpoint string `json:"api_endpoint"`
|
||||||
@ -82,3 +75,14 @@ type UDunConfig struct {
|
|||||||
UDunKey string `json:"UDunKey"`
|
UDunKey string `json:"UDunKey"`
|
||||||
CurrServerIp string `json:"CurrServerIp"`
|
CurrServerIp string `json:"CurrServerIp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binance配置结构体
|
||||||
|
*/
|
||||||
|
type BinanceConfig struct {
|
||||||
|
ApiKey string `json:"apiKey" mapstructure:"apiKey"` // API密钥
|
||||||
|
ApiSecret string `json:"apiSecret" mapstructure:"apiSecret"` // API密钥
|
||||||
|
ProxyType string `json:"proxyType" mapstructure:"proxyType"` // 代理类型
|
||||||
|
ProxyAddress string `json:"proxyAddress" mapstructure:"proxyAddress"` // 代理地址
|
||||||
|
TestNet bool `json:"testNet" mapstructure:"testNet"` // 是否使用测试网
|
||||||
|
}
|
||||||
|
|||||||
@ -5,7 +5,6 @@ import (
|
|||||||
"go-admin/models"
|
"go-admin/models"
|
||||||
"go-admin/pkg/httputils"
|
"go-admin/pkg/httputils"
|
||||||
"go-admin/services/binanceservice"
|
"go-admin/services/binanceservice"
|
||||||
"go-admin/services/futureservice"
|
|
||||||
|
|
||||||
log "github.com/go-admin-team/go-admin-core/logger"
|
log "github.com/go-admin-team/go-admin-core/logger"
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
@ -33,9 +32,6 @@ func FuturesInit() error {
|
|||||||
}
|
}
|
||||||
log.Info("初始化24h行情-结束")
|
log.Info("初始化24h行情-结束")
|
||||||
|
|
||||||
log.Info("订阅合约-开始")
|
|
||||||
go futureservice.StartBinanceProWs("normal")
|
|
||||||
|
|
||||||
//定时任务
|
//定时任务
|
||||||
RunJobs()
|
RunJobs()
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
144
config/serverinit/bitget_init.go
Normal file
144
config/serverinit/bitget_init.go
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
package serverinit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go-admin/config"
|
||||||
|
"go-admin/models"
|
||||||
|
"go-admin/pkg/httputils"
|
||||||
|
"go-admin/pkg/utility"
|
||||||
|
"go-admin/services/bitgetservice"
|
||||||
|
|
||||||
|
log "github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BitgetInit 初始化Bitget交易所
|
||||||
|
func BitgetMarketInit() error {
|
||||||
|
// 设置代理(如果配置了的话)
|
||||||
|
httputils.InitProxy(config.ExtConfig.ProxyUrl)
|
||||||
|
|
||||||
|
log.Info("初始化Bitget交易对-开始")
|
||||||
|
spotData := make(map[string]models.TradeSet, 0)
|
||||||
|
futData := make(map[string]models.TradeSet, 0)
|
||||||
|
err := bitgetservice.GetAndReloadSymbols(&spotData, 1)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error("初始化Bitget交易对失败,可能是网络问题,将跳过初始化:", err)
|
||||||
|
// 不返回错误,允许程序继续运行
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err = bitgetservice.GetAndReloadSymbols(&futData, 2)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("初始化Bitget合约交易对失败,可能是网络问题,将跳过初始化:", err)
|
||||||
|
// 不返回错误,允许程序继续运行
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("初始化Bitget交易对-结束")
|
||||||
|
|
||||||
|
log.Info("初始化Bitget 24h行情-开始")
|
||||||
|
_, err = bitgetservice.InitSymbolsTicker24h(&spotData, 1)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error("初始化Bitget 24h行情失败,可能是网络问题,将跳过初始化:", err)
|
||||||
|
// 不返回错误,允许程序继续运行
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = bitgetservice.InitSymbolsTicker24h(&futData, 2)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("初始化Bitget合约24h行情失败,可能是网络问题,将跳过初始化:", err)
|
||||||
|
// 不返回错误,允许程序继续运行
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Info("初始化Bitget 24h行情-结束")
|
||||||
|
|
||||||
|
//现货行情订阅
|
||||||
|
spotSymbols := []string{}
|
||||||
|
for k, _ := range spotData {
|
||||||
|
spotSymbols = append(spotSymbols, k)
|
||||||
|
}
|
||||||
|
spotMaps := utility.SplitSlice(spotSymbols, 50)
|
||||||
|
bitgetSpot := bitgetservice.BitgetMarketSpotClient{}
|
||||||
|
|
||||||
|
for _, v := range spotMaps {
|
||||||
|
if err := bitgetSpot.Subscribe(v); err != nil {
|
||||||
|
log.Error("订阅Bitget现货行情失败", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//合约行情订阅
|
||||||
|
futSymbols := []string{}
|
||||||
|
for k, _ := range futData {
|
||||||
|
futSymbols = append(futSymbols, k)
|
||||||
|
}
|
||||||
|
futMaps := utility.SplitSlice(futSymbols, 50)
|
||||||
|
bitgetFut := bitgetservice.BitgetMarketFutureClient{}
|
||||||
|
|
||||||
|
for _, v := range futMaps {
|
||||||
|
if err := bitgetFut.Subscribe(v); err != nil {
|
||||||
|
log.Error("订阅Bitget合约行情失败", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动定时任务
|
||||||
|
runBitgetJobs()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runBitgetJobs 启动Bitget相关的定时任务
|
||||||
|
func runBitgetJobs() {
|
||||||
|
cronJob := cron.New()
|
||||||
|
|
||||||
|
// 每小时更新一次交易对信息
|
||||||
|
_, err := cronJob.AddFunc("@every 1h", func() {
|
||||||
|
log.Info("执行Bitget交易对更新任务")
|
||||||
|
data := make(map[string]models.TradeSet, 0)
|
||||||
|
if err := bitgetservice.GetAndReloadSymbols(&data, 1); err != nil {
|
||||||
|
log.Error("更新Bitget交易对失败", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error("添加Bitget交易对更新任务失败", err)
|
||||||
|
} else {
|
||||||
|
log.Info("添加Bitget交易对更新任务成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动定时任务
|
||||||
|
cronJob.Start()
|
||||||
|
log.Info("Bitget定时任务启动完成")
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateBitgetPrivateWebSocket 创建Bitget私有WebSocket连接的示例函数
|
||||||
|
func CreateBitgetPrivateWebSocket(apiKey, apiSecret, passphrase, proxyType, proxyAddress string) {
|
||||||
|
log.Info("创建Bitget私有WebSocket连接 key:", apiKey)
|
||||||
|
|
||||||
|
// 现货WebSocket (wsType=0)
|
||||||
|
spotManager := bitgetservice.GetOrCreateBitgetWebSocketManager(
|
||||||
|
0, apiKey, apiSecret, passphrase, proxyType, proxyAddress)
|
||||||
|
spotManager.Start()
|
||||||
|
|
||||||
|
// 合约WebSocket (wsType=1)
|
||||||
|
futuresManager := bitgetservice.GetOrCreateBitgetWebSocketManager(
|
||||||
|
1, apiKey, apiSecret, passphrase, proxyType, proxyAddress)
|
||||||
|
futuresManager.Start()
|
||||||
|
|
||||||
|
log.Info("Bitget私有WebSocket连接已创建")
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopBitgetPrivateWebSocket 停止Bitget私有WebSocket连接的示例函数
|
||||||
|
func StopBitgetPrivateWebSocket(apiKey string) {
|
||||||
|
log.Info("停止Bitget私有WebSocket连接 key:", apiKey)
|
||||||
|
|
||||||
|
// 停止现货WebSocket
|
||||||
|
bitgetservice.StopBitgetWebSocketManager(0, apiKey)
|
||||||
|
|
||||||
|
// 停止合约WebSocket
|
||||||
|
bitgetservice.StopBitgetWebSocketManager(1, apiKey)
|
||||||
|
|
||||||
|
log.Info("Bitget私有WebSocket连接已停止")
|
||||||
|
}
|
||||||
@ -8,12 +8,10 @@ import (
|
|||||||
"go-admin/common/helper"
|
"go-admin/common/helper"
|
||||||
"go-admin/config"
|
"go-admin/config"
|
||||||
"go-admin/pkg/httputils"
|
"go-admin/pkg/httputils"
|
||||||
"go-admin/services/binanceservice"
|
|
||||||
"go-admin/services/cacheservice"
|
"go-admin/services/cacheservice"
|
||||||
"go-admin/services/futureservice"
|
"go-admin/services/futureservice"
|
||||||
"go-admin/services/scriptservice"
|
"go-admin/services/scriptservice"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/go-admin-team/go-admin-core/logger"
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
@ -30,24 +28,6 @@ func BusinessInit(db *gorm.DB) {
|
|||||||
os.Exit(-1)
|
os.Exit(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
lineApiService := service.LineApiUser{}
|
|
||||||
lineApiService.Orm = db
|
|
||||||
lineApiService.Log = logger.NewHelper(sdk.Runtime.GetLogger()).WithFields(map[string]interface{}{})
|
|
||||||
|
|
||||||
if err := lineApiService.CacheRelation(); err != nil {
|
|
||||||
lineApiService.Log.Errorf("初始化api关系失败 err:%v", err)
|
|
||||||
os.Exit(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
reverseSettingService := service.LineReverseSetting{}
|
|
||||||
reverseSettingService.Orm = db
|
|
||||||
reverseSettingService.Log = lineApiService.Log
|
|
||||||
|
|
||||||
if err := reverseSettingService.SaveCache(models.LineReverseSetting{}); err != nil {
|
|
||||||
reverseSettingService.Log.Errorf("初始化反单设置失败 err:%v", err)
|
|
||||||
os.Exit(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
//初始化参数配置
|
//初始化参数配置
|
||||||
cacheservice.InitConfigCache(db)
|
cacheservice.InitConfigCache(db)
|
||||||
//初始化可缓存价格交易对
|
//初始化可缓存价格交易对
|
||||||
@ -56,9 +36,6 @@ func BusinessInit(db *gorm.DB) {
|
|||||||
symbolPriceService.Log = logger.NewHelper(sdk.Runtime.GetLogger()).WithFields(map[string]interface{}{})
|
symbolPriceService.Log = logger.NewHelper(sdk.Runtime.GetLogger()).WithFields(map[string]interface{}{})
|
||||||
symbolPriceService.InitCache()
|
symbolPriceService.InitCache()
|
||||||
|
|
||||||
//清理交易对缓存价格
|
|
||||||
clearSymbolPrice()
|
|
||||||
|
|
||||||
//初始化订单配置
|
//初始化订单配置
|
||||||
cacheservice.ResetSystemSetting(db)
|
cacheservice.ResetSystemSetting(db)
|
||||||
lineApiUser := service.LineApiUser{}
|
lineApiUser := service.LineApiUser{}
|
||||||
@ -68,54 +45,34 @@ func BusinessInit(db *gorm.DB) {
|
|||||||
os.Exit(-1)
|
os.Exit(-1)
|
||||||
}
|
}
|
||||||
|
|
||||||
//币安 现货交易对
|
|
||||||
SpotCurrencyInit()
|
|
||||||
//币安 合约交易对
|
|
||||||
FuturesInit()
|
|
||||||
|
|
||||||
//第一次初始化交易对价格
|
|
||||||
spotApi := binanceservice.SpotRestApi{}
|
|
||||||
futApi := binanceservice.FutRestApi{}
|
|
||||||
spotApi.Ticker()
|
|
||||||
futApi.Ticker()
|
|
||||||
|
|
||||||
//现货 订阅
|
|
||||||
SpotSubscribeInit("normal")
|
|
||||||
//合约订阅
|
|
||||||
futureservice.StartBinanceProWs("normal")
|
|
||||||
//定时任务
|
//定时任务
|
||||||
RunSpotJobs(db)
|
RunSpotJobs(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 行情监听初始化
|
||||||
|
func BinanceMarketInit() error {
|
||||||
|
//币安 现货交易对(仅数据初始化,不启动WebSocket)
|
||||||
|
SpotCurrencyInit()
|
||||||
|
//币安 合约交易对(仅数据初始化,不启动WebSocket)
|
||||||
|
FuturesInit()
|
||||||
|
|
||||||
|
//现货 订阅
|
||||||
|
// SpotSubscribeInit("normal")
|
||||||
|
//合约订阅
|
||||||
|
futureservice.StartBinanceProWs("normal")
|
||||||
|
// log.Info("订阅合约-开始")
|
||||||
|
// go futureservice.StartBinanceProWs("normal")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// 开启现货ws监听
|
// 开启现货ws监听
|
||||||
func SpotWsInit() {
|
func SpotWsInit() {
|
||||||
//启动websocket服务
|
|
||||||
//go func() {
|
|
||||||
// err := wsservice.StartServer(":3333")
|
|
||||||
// if err != nil {
|
|
||||||
// log.Info("wsservice.StartServer", zap.Error(err))
|
|
||||||
//
|
|
||||||
// _, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
// cancel()
|
|
||||||
// }
|
|
||||||
//}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定义任务
|
// 定义任务
|
||||||
func RunSpotJobs(db *gorm.DB) {
|
func RunSpotJobs(db *gorm.DB) {
|
||||||
cronJob := cron.New()
|
cronJob := cron.New()
|
||||||
//添加任务 同步交易对行情
|
|
||||||
// spotApi := spotservice.SpotRestApi{}
|
|
||||||
// futApi := futureservice.FutRestApi{}
|
|
||||||
// _, err := cronJob.AddFunc("@every 10s", func() {
|
|
||||||
// //BuySellOrderNew(db)
|
|
||||||
// spotApi.Ticker()
|
|
||||||
// futApi.Ticker()
|
|
||||||
// })
|
|
||||||
// if err != nil {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
//定时执行脚本任务
|
//定时执行脚本任务
|
||||||
_, err := cronJob.AddFunc("@every 3s", func() {
|
_, err := cronJob.AddFunc("@every 3s", func() {
|
||||||
order := scriptservice.PreOrder{}
|
order := scriptservice.PreOrder{}
|
||||||
@ -167,24 +124,3 @@ func loadApiUser(db *gorm.DB) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理交易对价格缓存
|
|
||||||
func clearSymbolPrice() error {
|
|
||||||
spotAll, _ := helper.DefaultRedis.ScanKeys("spot_ticker_last_price:*")
|
|
||||||
futAllKey, _ := helper.DefaultRedis.ScanKeys("fut_ticker_last_price:*")
|
|
||||||
beforeTimeUtc := time.Now().UnixMilli()
|
|
||||||
|
|
||||||
for _, item := range spotAll {
|
|
||||||
if _, err := helper.DefaultRedis.RemoveBeforeScore(item, float64(beforeTimeUtc)); err != nil {
|
|
||||||
logger.Error("现货 清理交易对价格缓存失败:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, item := range futAllKey {
|
|
||||||
if _, err := helper.DefaultRedis.RemoveBeforeScore(item, float64(beforeTimeUtc)); err != nil {
|
|
||||||
logger.Error("合约 清理交易对价格缓存失败:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
23
go.mod
23
go.mod
@ -1,8 +1,8 @@
|
|||||||
module go-admin
|
module go-admin
|
||||||
|
|
||||||
go 1.21.0
|
go 1.24.0
|
||||||
|
|
||||||
toolchain go1.22.5
|
toolchain go1.24.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alibaba/sentinel-golang v1.0.4
|
github.com/alibaba/sentinel-golang v1.0.4
|
||||||
@ -39,7 +39,6 @@ require (
|
|||||||
github.com/shirou/gopsutil/v3 v3.23.10
|
github.com/shirou/gopsutil/v3 v3.23.10
|
||||||
github.com/shopspring/decimal v1.2.0
|
github.com/shopspring/decimal v1.2.0
|
||||||
github.com/spf13/cobra v1.7.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/files v1.0.1
|
||||||
github.com/swaggo/gin-swagger v1.6.0
|
github.com/swaggo/gin-swagger v1.6.0
|
||||||
github.com/swaggo/swag v1.16.2
|
github.com/swaggo/swag v1.16.2
|
||||||
@ -48,8 +47,8 @@ require (
|
|||||||
github.com/vmihailenco/msgpack/v5 v5.4.1
|
github.com/vmihailenco/msgpack/v5 v5.4.1
|
||||||
github.com/xuri/excelize/v2 v2.9.0
|
github.com/xuri/excelize/v2 v2.9.0
|
||||||
go.uber.org/zap v1.26.0
|
go.uber.org/zap v1.26.0
|
||||||
golang.org/x/crypto v0.31.0
|
golang.org/x/crypto v0.43.0
|
||||||
golang.org/x/net v0.33.0
|
golang.org/x/net v0.45.0
|
||||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||||
gorm.io/driver/mysql v1.5.2
|
gorm.io/driver/mysql v1.5.2
|
||||||
gorm.io/driver/postgres v1.5.4
|
gorm.io/driver/postgres v1.5.4
|
||||||
@ -83,7 +82,6 @@ require (
|
|||||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
github.com/coreos/go-semver v0.3.1 // indirect
|
github.com/coreos/go-semver v0.3.1 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/fatih/color v1.15.0 // indirect
|
github.com/fatih/color v1.15.0 // indirect
|
||||||
github.com/fatih/structs v1.1.0 // indirect
|
github.com/fatih/structs v1.1.0 // indirect
|
||||||
@ -145,7 +143,6 @@ require (
|
|||||||
github.com/nsqio/go-nsq v1.1.0 // indirect
|
github.com/nsqio/go-nsq v1.1.0 // indirect
|
||||||
github.com/nyaruka/phonenumbers v1.0.55 // indirect
|
github.com/nyaruka/phonenumbers v1.0.55 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // 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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||||
github.com/prometheus/client_model v0.5.0 // indirect
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
github.com/prometheus/common v0.45.0 // indirect
|
github.com/prometheus/common v0.45.0 // indirect
|
||||||
@ -177,13 +174,13 @@ require (
|
|||||||
go.uber.org/multierr v1.10.0 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
golang.org/x/arch v0.12.0 // indirect
|
golang.org/x/arch v0.12.0 // indirect
|
||||||
golang.org/x/image v0.18.0 // indirect
|
golang.org/x/image v0.18.0 // indirect
|
||||||
golang.org/x/mod v0.17.0 // indirect
|
golang.org/x/mod v0.28.0 // indirect
|
||||||
golang.org/x/sync v0.10.0 // indirect
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
golang.org/x/sys v0.28.0 // indirect
|
golang.org/x/sys v0.37.0 // indirect
|
||||||
golang.org/x/term v0.27.0 // indirect
|
golang.org/x/term v0.36.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.30.0 // indirect
|
||||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
|
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 // indirect
|
||||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
golang.org/x/tools v0.37.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.1 // indirect
|
google.golang.org/protobuf v1.36.1 // indirect
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
|||||||
@ -1,11 +0,0 @@
|
|||||||
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:"转入币种"`
|
|
||||||
}
|
|
||||||
@ -20,6 +20,7 @@ type TradeSet struct {
|
|||||||
PriceDigit int `db:"pricecdigit" json:"pricecdigit"` //价格小数点位数
|
PriceDigit int `db:"pricecdigit" json:"pricecdigit"` //价格小数点位数
|
||||||
Currency string `db:"currency" json:"currency"` //法币
|
Currency string `db:"currency" json:"currency"` //法币
|
||||||
Coin string `db:"coin" json:"coin"` //币种
|
Coin string `db:"coin" json:"coin"` //币种
|
||||||
|
Symbol string `db:"symbol" json:"symbol"` //交易对
|
||||||
PriceChange float64 `db:"pricechange" json:"pricechange"` //价格波动价位
|
PriceChange float64 `db:"pricechange" json:"pricechange"` //价格波动价位
|
||||||
MinBuyVal float64 `db:"minbuyval"` //最小下单金额
|
MinBuyVal float64 `db:"minbuyval"` //最小下单金额
|
||||||
OpenPrice float64 `db:"openprice"` //开盘价格
|
OpenPrice float64 `db:"openprice"` //开盘价格
|
||||||
@ -37,10 +38,6 @@ type TradeSet struct {
|
|||||||
E int64 `json:"-"` //推送时间
|
E int64 `json:"-"` //推送时间
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *TradeSet) GetSymbol() string {
|
|
||||||
return e.Coin + e.Currency
|
|
||||||
}
|
|
||||||
|
|
||||||
//CommissionType int `db:"commissiontype"` //手续费:1买,2卖,3双向
|
//CommissionType int `db:"commissiontype"` //手续费:1买,2卖,3双向
|
||||||
//DepositNum float64 `db:"depositnum" json:"depositnum"` //保证金规模(手)
|
//DepositNum float64 `db:"depositnum" json:"depositnum"` //保证金规模(手)
|
||||||
//ForceRate float64 `db:"forcerate" json:"forcerate"` //维持保证金率1%
|
//ForceRate float64 `db:"forcerate" json:"forcerate"` //维持保证金率1%
|
||||||
|
|||||||
@ -31,3 +31,28 @@ type LinePreOrderPositioinDelReq struct {
|
|||||||
ApiId int `json:"apiId"` //apiid
|
ApiId int `json:"apiId"` //apiid
|
||||||
ExchangeType string `json:"exchangeType"` //交易所类型
|
ExchangeType string `json:"exchangeType"` //交易所类型
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 待触发加仓单
|
||||||
|
type AddPositionList struct {
|
||||||
|
Id int `json:"id"` //订单id
|
||||||
|
Pid int `json:"pid"` //父级id
|
||||||
|
MainId int `json:"mainId"` //主单Id
|
||||||
|
ApiId int `json:"apiId"` //触发账户id
|
||||||
|
Symbol string `json:"symbol"` //交易对
|
||||||
|
Price decimal.Decimal `json:"price"` //触发价
|
||||||
|
Side string `json:"side"` //买卖方向
|
||||||
|
SymbolType int `json:"type" comment:"交易对类别 1-现货 2-合约"`
|
||||||
|
OrderSn string `json:"prderSn"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReduceListItem struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
ApiId int `json:"apiId"`
|
||||||
|
MainId int `json:"mainId"`
|
||||||
|
Pid int `json:"pid"`
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Price decimal.Decimal `json:"price"`
|
||||||
|
Side string `json:"side"`
|
||||||
|
Num decimal.Decimal `json:"num"`
|
||||||
|
OrderSn string `json:"orderSn"`
|
||||||
|
}
|
||||||
|
|||||||
@ -1,46 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -28,7 +28,3 @@ func init() {
|
|||||||
func GetOrderId() int64 {
|
func GetOrderId() int64 {
|
||||||
return snowNode.Generate().Int64()
|
return snowNode.Generate().Int64()
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetOrderNo() string {
|
|
||||||
return fmt.Sprintf("%d", GetOrderId())
|
|
||||||
}
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import (
|
|||||||
"go-admin/models/spot"
|
"go-admin/models/spot"
|
||||||
"go-admin/pkg/httputils"
|
"go-admin/pkg/httputils"
|
||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
|
"go-admin/services/commonservice"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -126,7 +127,7 @@ func (e SpotRestApi) GetSpotTicker24h(tradeSet *map[string]models.TradeSet) (del
|
|||||||
log.Error("缓存交易对失败|", item.Symbol, err)
|
log.Error("缓存交易对失败|", item.Symbol, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
helper.DefaultRedis.AddSortSet(global.COIN_PRICE_CHANGE, symbol.PriceChange, symbol.Coin)
|
helper.DefaultRedis.AddSortSet(fmt.Sprintf(global.COIN_PRICE_CHANGE, global.EXCHANGE_BINANCE), symbol.PriceChange, symbol.Coin)
|
||||||
}
|
}
|
||||||
return deleteSymbols, nil
|
return deleteSymbols, nil
|
||||||
}
|
}
|
||||||
@ -169,12 +170,12 @@ type Ticker struct {
|
|||||||
Price string `json:"price"`
|
Price string `json:"price"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e SpotRestApi) Ticker() {
|
// func (e SpotRestApi) Ticker() {
|
||||||
tickerApi := fmt.Sprintf("%s%s", binanceRestApi, "/api/v3/ticker/price")
|
// tickerApi := fmt.Sprintf("%s%s", binanceRestApi, "/api/v3/ticker/price")
|
||||||
mapData, _ := httputils.NewHttpRequestWithFasthttp("GET", tickerApi, "", map[string]string{})
|
// mapData, _ := httputils.NewHttpRequestWithFasthttp("GET", tickerApi, "", map[string]string{})
|
||||||
//sonic.Unmarshal(mapData, &tickerData)
|
// //sonic.Unmarshal(mapData, &tickerData)
|
||||||
helper.DefaultRedis.SetString(rediskey.SpotSymbolTicker, string(mapData))
|
// helper.DefaultRedis.SetString(rediskey.SpotSymbolTicker, string(mapData))
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 循环下单
|
// 循环下单
|
||||||
func (e SpotRestApi) OrderPlaceLoop(db *gorm.DB, params OrderPlacementService, retryCount int) error {
|
func (e SpotRestApi) OrderPlaceLoop(db *gorm.DB, params OrderPlacementService, retryCount int) error {
|
||||||
@ -452,7 +453,7 @@ func GetApiInfo(apiId int) (DbModels.LineApiUser, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
db := GetDBConnection()
|
db := commonservice.GetDBConnection()
|
||||||
|
|
||||||
if err := db.Model(&api).Where("id =?", apiId).First(&api).Error; err != nil {
|
if err := db.Model(&api).Where("id =?", apiId).First(&api).Error; err != nil {
|
||||||
return api, err
|
return api, err
|
||||||
@ -467,21 +468,6 @@ func GetApiInfo(apiId int) (DbModels.LineApiUser, error) {
|
|||||||
return api, nil
|
return api, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetReverseApiInfo 根据apiKey获取api用户信息
|
|
||||||
func GetApiInfoByKey(apiKey string) DbModels.LineApiUser {
|
|
||||||
result := DbModels.LineApiUser{}
|
|
||||||
keys, _ := helper.DefaultRedis.GetAllKeysAndValues(fmt.Sprintf(rediskey.API_USER, "*"))
|
|
||||||
|
|
||||||
for _, key := range keys {
|
|
||||||
if strings.Contains(key, apiKey) {
|
|
||||||
sonic.UnmarshalString(key, &result)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
根据A账户获取B账号信息
|
根据A账户获取B账号信息
|
||||||
*/
|
*/
|
||||||
@ -655,43 +641,3 @@ func GetSpotUProperty(apiUserInfo DbModels.LineApiUser, data *dto.LineUserProper
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 万象划转
|
|
||||||
func TradeAmount(db *gorm.DB, req *binancedto.BinanceTransfer, apiUserInfo DbModels.LineApiUser) error {
|
|
||||||
url := "/sapi/v1/asset/transfer"
|
|
||||||
|
|
||||||
client := GetClient(&apiUserInfo)
|
|
||||||
|
|
||||||
params := map[string]string{
|
|
||||||
"type": req.Type,
|
|
||||||
"asset": req.Asset,
|
|
||||||
"amount": req.Amount.String(),
|
|
||||||
"fromSymbol": req.FromSymbol,
|
|
||||||
"toSymbol": req.ToSymbol,
|
|
||||||
"recvWindow": "10000",
|
|
||||||
}
|
|
||||||
|
|
||||||
_, code, err := client.SendSpotAuth(url, "POST", params)
|
|
||||||
if err != nil || code != 200 {
|
|
||||||
log.Error("万向划转失败 参数:", params)
|
|
||||||
log.Error("万向划转失败 code:", code)
|
|
||||||
log.Error("万向划转失败 err:", err)
|
|
||||||
dataMap := make(map[string]interface{})
|
|
||||||
if err.Error() != "" {
|
|
||||||
if err := sonic.Unmarshal([]byte(err.Error()), &dataMap); err != nil {
|
|
||||||
return fmt.Errorf("api_id:%d 万向划转失败:%+v", apiUserInfo.Id, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
code, ok := dataMap["code"]
|
|
||||||
if ok {
|
|
||||||
return fmt.Errorf("api_id:%d 万向划转失败:%s", apiUserInfo.Id, ErrorMaps[code.(float64)])
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), "Unknown order sent.") {
|
|
||||||
return fmt.Errorf("api_id:%d 万向划转失败:%+v", apiUserInfo.Id, ErrorMaps[-2011])
|
|
||||||
}
|
|
||||||
return fmt.Errorf("api_id:%d 万向划转失败:%+v", apiUserInfo.Id, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCancelFutClosePosition(t *testing.T) {
|
func TestCancelFutClosePosition(t *testing.T) {
|
||||||
dsn := "root:123456@tcp(127.0.0.1:3306)/gp-bian?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
dsn := "root:root@tcp(192.168.123.216:3306)/gp-bian?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||||
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||||
var apiUserInfo models.LineApiUser
|
var apiUserInfo models.LineApiUser
|
||||||
db.Model(&models.LineApiUser{}).Where("id = 21").Find(&apiUserInfo)
|
db.Model(&models.LineApiUser{}).Where("id = 21").Find(&apiUserInfo)
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"go-admin/common/helper"
|
"go-admin/common/helper"
|
||||||
"go-admin/models/positiondto"
|
"go-admin/models/positiondto"
|
||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
|
"go-admin/services/commonservice"
|
||||||
"go-admin/services/positionservice"
|
"go-admin/services/positionservice"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -16,7 +17,6 @@ import (
|
|||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/go-admin-team/go-admin-core/logger"
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
log "github.com/go-admin-team/go-admin-core/logger"
|
log "github.com/go-admin-team/go-admin-core/logger"
|
||||||
"github.com/go-admin-team/go-admin-core/sdk"
|
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@ -105,7 +105,7 @@ func CancelSpotOrder(symbol string, apiUserInfo *DbModels.LineApiUser, side stri
|
|||||||
}
|
}
|
||||||
client := GetClient(apiUserInfo)
|
client := GetClient(apiUserInfo)
|
||||||
resp, _, err := client.SendSpotAuth(searchEndpoint, "GET", searchParams)
|
resp, _, err := client.SendSpotAuth(searchEndpoint, "GET", searchParams)
|
||||||
db := GetDBConnection()
|
db := commonservice.GetDBConnection()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if len(resp) > 0 {
|
if len(resp) > 0 {
|
||||||
@ -209,58 +209,16 @@ func (e *AddPosition) GetFutSpotOrderInfo(req dto.ManuallyCover, symbol, orderTy
|
|||||||
return orderInfo, nil
|
return orderInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CalculateAmount 计算加仓数量
|
|
||||||
func (e *AddPosition) CalculateAmount(req dto.ManuallyCover, totalNum, lastPrice decimal.Decimal, amountDigit int, notUsdt bool, symbolInfo DbModels.LineSymbol) (decimal.Decimal, error) {
|
|
||||||
var amt decimal.Decimal
|
|
||||||
if req.CoverType == 1 {
|
|
||||||
decimalValue := utility.StringToDecimal(req.Value).Div(decimal.NewFromInt(100))
|
|
||||||
amt = totalNum.Mul(decimalValue)
|
|
||||||
} else {
|
|
||||||
decimalValue := utility.StringToDecimal(req.Value)
|
|
||||||
if notUsdt {
|
|
||||||
tickerSymbolMaps := make([]dto.Ticker, 0)
|
|
||||||
tickerSymbol := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val()
|
|
||||||
if err := sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps); err != nil {
|
|
||||||
return decimal.Zero, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var tickerPrice decimal.Decimal
|
|
||||||
for _, symbolMap := range tickerSymbolMaps {
|
|
||||||
if symbolMap.Symbol == strings.ToUpper(symbolInfo.BaseAsset+"USDT") {
|
|
||||||
tickerPrice, _ = decimal.NewFromString(symbolMap.Price)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, symbolMap := range tickerSymbolMaps {
|
|
||||||
if symbolMap.Symbol == strings.ToUpper(symbolInfo.QuoteAsset+"USDT") {
|
|
||||||
uTickerPrice, _ := decimal.NewFromString(symbolMap.Price)
|
|
||||||
div := tickerPrice.Div(decimal.NewFromInt(1).Div(uTickerPrice))
|
|
||||||
amt = decimalValue.Div(div)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
amt = decimalValue.Div(lastPrice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return amt.Truncate(int32(amountDigit)), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 主单平仓删除缓存
|
// 主单平仓删除缓存
|
||||||
// mainOrderId 主单id
|
// mainOrderId 主单id
|
||||||
// symbolType 1现货 2合约
|
// symbolType 1现货 2合约
|
||||||
func MainClosePositionClearCache(mainId int, symbolType int) {
|
func MainClosePositionClearCache(mainId int, symbolType int) {
|
||||||
strategyDto := dto.StrategyOrderRedisList{}
|
|
||||||
|
|
||||||
if symbolType == 1 {
|
if symbolType == 1 {
|
||||||
keySpotStop := fmt.Sprintf(rediskey.SpotStopLossList, global.EXCHANGE_BINANCE)
|
keySpotStop := fmt.Sprintf(rediskey.SpotStopLossList, global.EXCHANGE_BINANCE)
|
||||||
keySpotAddposition := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE)
|
keySpotAddposition := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE)
|
||||||
spotStopArray, _ := helper.DefaultRedis.GetAllList(keySpotStop)
|
spotStopArray, _ := helper.DefaultRedis.GetAllList(keySpotStop)
|
||||||
spotAddpositionArray, _ := helper.DefaultRedis.GetAllList(keySpotAddposition)
|
spotAddpositionArray, _ := helper.DefaultRedis.GetAllList(keySpotAddposition)
|
||||||
spotStrategyKey := fmt.Sprintf(rediskey.StrategySpotOrderList, global.EXCHANGE_BINANCE)
|
var position positiondto.AddPositionList
|
||||||
spotStrategyList, _ := helper.DefaultRedis.GetAllList(spotStrategyKey)
|
|
||||||
var position AddPositionList
|
|
||||||
var stop dto.StopLossRedisList
|
var stop dto.StopLossRedisList
|
||||||
|
|
||||||
for _, item := range spotAddpositionArray {
|
for _, item := range spotAddpositionArray {
|
||||||
@ -283,24 +241,12 @@ func MainClosePositionClearCache(mainId int, symbolType int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range spotStrategyList {
|
|
||||||
sonic.Unmarshal([]byte(item), &strategyDto)
|
|
||||||
|
|
||||||
if strategyDto.Id == mainId {
|
|
||||||
if _, err := helper.DefaultRedis.LRem(spotStrategyKey, item); err != nil {
|
|
||||||
logger.Errorf("id:%d 移除缓存失败,err:%v", mainId, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
keyFutStop := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE)
|
keyFutStop := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE)
|
||||||
keyFutAddposition := fmt.Sprintf(rediskey.FuturesStopLossList, global.EXCHANGE_BINANCE)
|
keyFutAddposition := fmt.Sprintf(rediskey.FuturesStopLossList, global.EXCHANGE_BINANCE)
|
||||||
futAddpositionArray, _ := helper.DefaultRedis.GetAllList(keyFutStop)
|
futAddpositionArray, _ := helper.DefaultRedis.GetAllList(keyFutStop)
|
||||||
futStopArray, _ := helper.DefaultRedis.GetAllList(keyFutAddposition)
|
futStopArray, _ := helper.DefaultRedis.GetAllList(keyFutAddposition)
|
||||||
|
var position positiondto.AddPositionList
|
||||||
futStrategyKey := fmt.Sprintf(rediskey.StrategyFutOrderList, global.EXCHANGE_BINANCE)
|
|
||||||
futStrategyList, _ := helper.DefaultRedis.GetAllList(futStrategyKey)
|
|
||||||
var position AddPositionList
|
|
||||||
var stop dto.StopLossRedisList
|
var stop dto.StopLossRedisList
|
||||||
|
|
||||||
for _, item := range futAddpositionArray {
|
for _, item := range futAddpositionArray {
|
||||||
@ -322,26 +268,7 @@ func MainClosePositionClearCache(mainId int, symbolType int) {
|
|||||||
helper.DefaultRedis.LRem(keyFutStop, item)
|
helper.DefaultRedis.LRem(keyFutStop, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range futStrategyList {
|
|
||||||
sonic.Unmarshal([]byte(item), &strategyDto)
|
|
||||||
|
|
||||||
if strategyDto.Id == mainId {
|
|
||||||
if _, err := helper.DefaultRedis.LRem(futStrategyKey, item); err != nil {
|
|
||||||
logger.Errorf("id:%d 移除缓存失败,err:%v", mainId, err)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取数据库连接
|
|
||||||
func GetDBConnection() *gorm.DB {
|
|
||||||
dbs := sdk.Runtime.GetDb()
|
|
||||||
for _, db := range dbs {
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查询订单
|
// 查询订单
|
||||||
@ -479,13 +406,3 @@ func GetOpenOrderSns(db *gorm.DB, mainIds []int) ([]string, error) {
|
|||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 回去反单默认配置
|
|
||||||
func GetReverseSetting(db *gorm.DB) (DbModels.LineReverseSetting, error) {
|
|
||||||
var setting DbModels.LineReverseSetting
|
|
||||||
if err := db.Model(&DbModels.LineReverseSetting{}).First(&setting).Error; err != nil {
|
|
||||||
return setting, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return setting, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,358 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,676 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"go-admin/common/global"
|
"go-admin/common/global"
|
||||||
"go-admin/common/helper"
|
"go-admin/common/helper"
|
||||||
"go-admin/models"
|
"go-admin/models"
|
||||||
|
"go-admin/models/positiondto"
|
||||||
"go-admin/services/cacheservice"
|
"go-admin/services/cacheservice"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -28,8 +29,8 @@ func TestFutureJudge(t *testing.T) {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE)
|
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"}`
|
item := `{"id":50,"apiId":49,"mainId":47,"pid":47,"symbol":"ADAUSDT","price":"0.5936","side":"SELL","num":"12","orderSn":"397913127842217984"}`
|
||||||
reduceOrder := ReduceListItem{}
|
reduceOrder := positiondto.ReduceListItem{}
|
||||||
futApi := FutRestApi{}
|
futApi := FutRestApi{}
|
||||||
setting, err := cacheservice.GetSystemSetting(db)
|
setting, err := cacheservice.GetSystemSetting(db)
|
||||||
|
|
||||||
|
|||||||
@ -1,125 +0,0 @@
|
|||||||
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(¶msMaps)
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
@ -5,15 +5,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
DbModels "go-admin/app/admin/models"
|
DbModels "go-admin/app/admin/models"
|
||||||
"go-admin/app/admin/service/dto"
|
"go-admin/app/admin/service/dto"
|
||||||
"go-admin/common/const/rediskey"
|
|
||||||
"go-admin/common/global"
|
"go-admin/common/global"
|
||||||
"go-admin/common/helper"
|
"go-admin/common/helper"
|
||||||
"go-admin/models"
|
"go-admin/models"
|
||||||
"go-admin/models/binancedto"
|
"go-admin/models/binancedto"
|
||||||
"go-admin/models/futuresdto"
|
"go-admin/models/futuresdto"
|
||||||
"go-admin/pkg/httputils"
|
"go-admin/pkg/httputils"
|
||||||
"go-admin/pkg/retryhelper"
|
|
||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
|
"go-admin/services/cacheservice"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -262,18 +261,18 @@ func GetAndReloadSymbols(data *map[string]models.TradeSet) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e FutRestApi) Ticker() {
|
// func (e FutRestApi) Ticker() {
|
||||||
url := fmt.Sprintf("%s%s", futureApi, "/fapi/v1/ticker/price")
|
// url := fmt.Sprintf("%s%s", futureApi, "/fapi/v1/ticker/price")
|
||||||
mapData, err := httputils.NewHttpRequestWithFasthttp("GET", url, "", map[string]string{})
|
// mapData, err := httputils.NewHttpRequestWithFasthttp("GET", url, "", map[string]string{})
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
log.Error("获取合约交易对行情失败:err:", err)
|
// log.Error("获取合约交易对行情失败:err:", err)
|
||||||
}
|
// }
|
||||||
|
|
||||||
if len(mapData) == 0 {
|
// if len(mapData) == 0 {
|
||||||
log.Error("获取合约交易对行情失败,或数量为空")
|
// log.Error("获取合约交易对行情失败,或数量为空")
|
||||||
}
|
// }
|
||||||
helper.DefaultRedis.SetString(rediskey.FutSymbolTicker, string(mapData))
|
// helper.DefaultRedis.SetString(rediskey.FutSymbolTicker, string(mapData))
|
||||||
}
|
// }
|
||||||
|
|
||||||
// 获取合约行情
|
// 获取合约行情
|
||||||
func GetTicker24h() ([]futuresdto.FutureTicker24h, error) {
|
func GetTicker24h() ([]futuresdto.FutureTicker24h, error) {
|
||||||
@ -556,6 +555,50 @@ func (e FutRestApi) OrderPlace(orm *gorm.DB, params FutOrderPlace) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClosePositionB 平仓B对应的交易对
|
||||||
|
// bApiUserInfo B 账户api-user信息
|
||||||
|
// symbol 需要平仓的交易对
|
||||||
|
// closeType 平仓模式 ALL = 全平 reduceOnly = 只减仓
|
||||||
|
// func (e FutRestApi) ClosePositionB(orm *gorm.DB, bApiUserInfo *DbModels.LineApiUser, symbol string, closeType string) error {
|
||||||
|
// if bApiUserInfo == nil {
|
||||||
|
// return errors.New("缺失平仓账户信息")
|
||||||
|
// }
|
||||||
|
// if symbol == "" {
|
||||||
|
// return errors.New("缺失平仓交易对信息")
|
||||||
|
// }
|
||||||
|
// risks, err := e.GetPositionV3(bApiUserInfo, symbol)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for _, risk := range risks {
|
||||||
|
// if risk.Symbol == strings.ToUpper(symbol) {
|
||||||
|
// //持仓数量
|
||||||
|
// positionAmt, _ := decimal.NewFromString(risk.PositionAmt)
|
||||||
|
// side := "BUY"
|
||||||
|
// if positionAmt.GreaterThan(decimal.Zero) {
|
||||||
|
// side = "SELL"
|
||||||
|
// }
|
||||||
|
// var closeAmt decimal.Decimal
|
||||||
|
// if strings.ToUpper(closeType) == "ALL" { //全部平仓
|
||||||
|
// closeAmt = positionAmt
|
||||||
|
// } else {
|
||||||
|
// //如果是只减仓的话 数量=仓位数量*比例
|
||||||
|
// closeAmt = positionAmt.Mul(decimal.NewFromFloat(reduceOnlyRate))
|
||||||
|
// }
|
||||||
|
|
||||||
|
// err = e.ClosePosition(symbol, closeAmt, side, *bApiUserInfo, "MARKET", "", decimal.Zero)
|
||||||
|
// if err != nil {
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
// orm.Model(&DbModels.LinePreOrder{}).Where("api_id = ? AND symbol = ?", bApiUserInfo.Id, symbol).Updates(map[string]interface{}{
|
||||||
|
// "status": "6",
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return nil
|
||||||
|
// }
|
||||||
|
|
||||||
// 获取合约 持仓价格、数量
|
// 获取合约 持仓价格、数量
|
||||||
// symbol:交易对
|
// symbol:交易对
|
||||||
// side:方向
|
// side:方向
|
||||||
@ -599,49 +642,6 @@ func (e FutRestApi) GetHoldeData(apiInfo *DbModels.LineApiUser, symbol, side str
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取合约 持仓价格、数量
|
|
||||||
// symbol:交易对
|
|
||||||
// positionSide:持仓方向
|
|
||||||
// holdeData:持仓数据
|
|
||||||
func (e FutRestApi) GetPositionData(apiInfo *DbModels.LineApiUser, symbol, positionSide string, holdeData *HoldeData) error {
|
|
||||||
opts := retryhelper.DefaultRetryOptions()
|
|
||||||
opts.RetryableErrFn = func(err error) bool {
|
|
||||||
if strings.Contains(err.Error(), "LOT_SIZE") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
//重试
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
holdes, err := retryhelper.RetryWithResult(func() ([]PositionRisk, error) {
|
|
||||||
return e.GetPositionV3(apiInfo, symbol)
|
|
||||||
}, opts)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, item := range holdes {
|
|
||||||
positionAmount, _ := decimal.NewFromString(item.PositionAmt)
|
|
||||||
if (positionSide == "LONG" && item.PositionSide == "BOTH" && positionAmount.Cmp(decimal.Zero) > 0) || item.PositionSide == positionSide { //多
|
|
||||||
holdeData.AveragePrice, _ = decimal.NewFromString(item.EntryPrice)
|
|
||||||
holdeData.TotalQuantity = positionAmount.Abs()
|
|
||||||
continue
|
|
||||||
} else if (positionSide == "SHORT" && item.PositionSide == "BOTH" && positionAmount.Cmp(decimal.Zero) < 0) || item.PositionSide == positionSide { //空
|
|
||||||
holdeData.AveragePrice, _ = decimal.NewFromString(item.EntryPrice)
|
|
||||||
holdeData.TotalQuantity = positionAmount.Abs()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if holdeData.AveragePrice.Cmp(decimal.Zero) == 0 {
|
|
||||||
holdesVal, _ := sonic.MarshalString(&holdes)
|
|
||||||
log.Error("均价错误 symbol:", symbol, " 数据:", holdesVal)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取代币持仓信息
|
// 获取代币持仓信息
|
||||||
func getSymbolHolde(e FutRestApi, apiInfo *DbModels.LineApiUser, symbol string, side string, holdeData *HoldeData) ([]PositionRisk, error) {
|
func getSymbolHolde(e FutRestApi, apiInfo *DbModels.LineApiUser, symbol string, side string, holdeData *HoldeData) ([]PositionRisk, error) {
|
||||||
holdes, err := e.GetPositionV3(apiInfo, symbol)
|
holdes, err := e.GetPositionV3(apiInfo, symbol)
|
||||||
@ -759,15 +759,6 @@ func (e FutRestApi) ClosePosition(symbol string, orderSn string, quantity decima
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 带重试机制的取消订单
|
|
||||||
func (e FutRestApi) CancelFutOrderRetry(apiUserInfo DbModels.LineApiUser, symbol string, newClientOrderId string) error {
|
|
||||||
opts := retryhelper.DefaultRetryOptions()
|
|
||||||
|
|
||||||
return retryhelper.Retry(func() error {
|
|
||||||
return e.CancelFutOrder(apiUserInfo, symbol, newClientOrderId)
|
|
||||||
}, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CancelFutOrder 通过单个订单号取消合约委托
|
// CancelFutOrder 通过单个订单号取消合约委托
|
||||||
// symbol 交易对
|
// symbol 交易对
|
||||||
// newClientOrderId 系统自定义订单号
|
// newClientOrderId 系统自定义订单号
|
||||||
@ -827,27 +818,6 @@ func (e FutRestApi) CancelAllFutOrder(apiUserInfo DbModels.LineApiUser, symbol s
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 带重试的批量撤销订单
|
|
||||||
func (e FutRestApi) CancelBatchFutOrderLoop(apiUserInfo DbModels.LineApiUser, symbol string, newClientOrderIdList []string) error {
|
|
||||||
opts := retryhelper.DefaultRetryOptions()
|
|
||||||
opts.RetryableErrFn = func(err error) bool {
|
|
||||||
if strings.Contains(err.Error(), "LOT_SIZE") {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
//重试
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
err := retryhelper.Retry(func() error {
|
|
||||||
return e.CancelBatchFutOrder(apiUserInfo, symbol, newClientOrderIdList)
|
|
||||||
}, opts)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CancelBatchFutOrder 批量撤销订单
|
// CancelBatchFutOrder 批量撤销订单
|
||||||
// symbol 交易对
|
// symbol 交易对
|
||||||
// newClientOrderIdList 系统自定义的订单号, 最多支持10个订单
|
// newClientOrderIdList 系统自定义的订单号, 最多支持10个订单
|
||||||
@ -892,9 +862,10 @@ func (e FutRestApi) CancelBatchFutOrder(apiUserInfo DbModels.LineApiUser, symbol
|
|||||||
// quoteSymbol 计价货币 ETHBTC -> 计价货币就是BTC
|
// quoteSymbol 计价货币 ETHBTC -> 计价货币就是BTC
|
||||||
// totalMoney 兑换的总金额
|
// totalMoney 兑换的总金额
|
||||||
func (e FutRestApi) CalcSymbolExchangeAmt(symbol string, quoteSymbol string, totalMoney decimal.Decimal) decimal.Decimal {
|
func (e FutRestApi) CalcSymbolExchangeAmt(symbol string, quoteSymbol string, totalMoney decimal.Decimal) decimal.Decimal {
|
||||||
tickerSymbol := helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val()
|
// tickerSymbol := helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val()
|
||||||
tickerSymbolMaps := make([]dto.Ticker, 0)
|
// tickerSymbolMaps := make([]dto.Ticker, 0)
|
||||||
sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps)
|
// sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps)
|
||||||
|
tickerSymbolMaps, _ := cacheservice.GetExchangeTradeSets(global.EXCHANGE_BINANCE, 1)
|
||||||
|
|
||||||
targetSymbol := strings.Replace(symbol, quoteSymbol, "USDT", 1) //ETHBTC -》 ETHUSDT
|
targetSymbol := strings.Replace(symbol, quoteSymbol, "USDT", 1) //ETHBTC -》 ETHUSDT
|
||||||
key := fmt.Sprintf("%s:%s", global.TICKER_FUTURES, targetSymbol)
|
key := fmt.Sprintf("%s:%s", global.TICKER_FUTURES, targetSymbol)
|
||||||
@ -903,7 +874,7 @@ func (e FutRestApi) CalcSymbolExchangeAmt(symbol string, quoteSymbol string, tot
|
|||||||
quantity := decimal.Zero
|
quantity := decimal.Zero
|
||||||
for _, symbolMap := range tickerSymbolMaps {
|
for _, symbolMap := range tickerSymbolMaps {
|
||||||
if symbolMap.Symbol == strings.ToUpper(targetSymbol) {
|
if symbolMap.Symbol == strings.ToUpper(targetSymbol) {
|
||||||
targetPrice, _ = decimal.NewFromString(symbolMap.Price)
|
targetPrice, _ = decimal.NewFromString(symbolMap.LastPrice)
|
||||||
quantity = totalMoney.Div(targetPrice).Truncate(int32(tradeSet.AmountDigit))
|
quantity = totalMoney.Div(targetPrice).Truncate(int32(tradeSet.AmountDigit))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,8 +10,11 @@ import (
|
|||||||
"go-admin/common/global"
|
"go-admin/common/global"
|
||||||
"go-admin/common/helper"
|
"go-admin/common/helper"
|
||||||
"go-admin/models"
|
"go-admin/models"
|
||||||
|
"go-admin/models/positiondto"
|
||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
"go-admin/services/cacheservice"
|
"go-admin/services/cacheservice"
|
||||||
|
"go-admin/services/commonservice"
|
||||||
|
"go-admin/services/orderservice"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -26,7 +29,7 @@ import (
|
|||||||
*/
|
*/
|
||||||
func JudgeFuturesPrice(tradeSet models.TradeSet) {
|
func JudgeFuturesPrice(tradeSet models.TradeSet) {
|
||||||
preOrderVal, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.PreFutOrderList, global.EXCHANGE_BINANCE))
|
preOrderVal, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.PreFutOrderList, global.EXCHANGE_BINANCE))
|
||||||
db := GetDBConnection()
|
db := commonservice.GetDBConnection()
|
||||||
|
|
||||||
if len(preOrderVal) == 0 {
|
if len(preOrderVal) == 0 {
|
||||||
return
|
return
|
||||||
@ -153,7 +156,7 @@ func JudgeFuturesReduce(trade models.TradeSet) {
|
|||||||
key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE)
|
key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE)
|
||||||
reduceVal, _ := helper.DefaultRedis.GetAllList(key)
|
reduceVal, _ := helper.DefaultRedis.GetAllList(key)
|
||||||
|
|
||||||
db := GetDBConnection()
|
db := commonservice.GetDBConnection()
|
||||||
futApi := FutRestApi{}
|
futApi := FutRestApi{}
|
||||||
setting, err := cacheservice.GetSystemSetting(db)
|
setting, err := cacheservice.GetSystemSetting(db)
|
||||||
|
|
||||||
@ -162,12 +165,14 @@ func JudgeFuturesReduce(trade models.TradeSet) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
reduceOrder := ReduceListItem{}
|
reduceOrder := positiondto.ReduceListItem{}
|
||||||
tradePrice, _ := decimal.NewFromString(trade.LastPrice)
|
tradePrice, _ := decimal.NewFromString(trade.LastPrice)
|
||||||
//减仓单减仓策略
|
//减仓单减仓策略
|
||||||
reduceReduceListKey := fmt.Sprintf(rediskey.FutOrderReduceStrategyList, global.EXCHANGE_BINANCE)
|
reduceReduceListKey := fmt.Sprintf(rediskey.FutOrderReduceStrategyList, global.EXCHANGE_BINANCE)
|
||||||
orderReduceVal, _ := helper.DefaultRedis.HGetAllFields(reduceReduceListKey)
|
orderReduceVal, _ := helper.DefaultRedis.HGetAllFields(reduceReduceListKey)
|
||||||
reduceOrderStrategy := dto.LineOrderReduceStrategyResp{}
|
reduceOrderStrategy := dto.LineOrderReduceStrategyResp{}
|
||||||
|
orderService := orderservice.OrderService{}
|
||||||
|
orderService.Orm = db
|
||||||
|
|
||||||
for _, item := range orderReduceVal {
|
for _, item := range orderReduceVal {
|
||||||
sonic.Unmarshal([]byte(item), &reduceOrderStrategy)
|
sonic.Unmarshal([]byte(item), &reduceOrderStrategy)
|
||||||
@ -193,7 +198,7 @@ func JudgeFuturesReduce(trade models.TradeSet) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
order, err := CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit)
|
order, err := orderService.CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("%d 生成订单失败", reduceOrderStrategy.OrderId)
|
log.Errorf("%d 生成订单失败", reduceOrderStrategy.OrderId)
|
||||||
@ -261,8 +266,8 @@ func JudgeFuturesReduce(trade models.TradeSet) {
|
|||||||
// 触发合约减仓
|
// 触发合约减仓
|
||||||
// isStrategy 是否是策略减仓
|
// isStrategy 是否是策略减仓
|
||||||
// reduceId 父减仓单id
|
// reduceId 父减仓单id
|
||||||
func FuturesReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, futApi FutRestApi, setting DbModels.LineSystemSetting, key, item string, isStrategy bool, reduceId int) bool {
|
func FuturesReduceTrigger(db *gorm.DB, reduceOrder positiondto.ReduceListItem, futApi FutRestApi, setting DbModels.LineSystemSetting, key, item string, isStrategy bool, reduceId int) bool {
|
||||||
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reduceOrder.Symbol, 1)
|
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reduceOrder.Symbol, 2)
|
||||||
result := true
|
result := true
|
||||||
|
|
||||||
if tradeSet.LastPrice == "" {
|
if tradeSet.LastPrice == "" {
|
||||||
@ -277,10 +282,9 @@ func FuturesReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, futApi FutRes
|
|||||||
} else if ok {
|
} else if ok {
|
||||||
defer lock.Release()
|
defer lock.Release()
|
||||||
takeOrders := make([]DbModels.LinePreOrder, 0)
|
takeOrders := make([]DbModels.LinePreOrder, 0)
|
||||||
//只取消减仓单 止盈止损减仓成功后取消
|
if err := db.Model(&DbModels.LinePreOrder{}).Where("main_id =? AND order_type IN (1,2,4) AND status IN (1,5)", reduceOrder.MainId).Find(&takeOrders).Error; err != nil {
|
||||||
if err := db.Model(&DbModels.LinePreOrder{}).Where("main_id =? AND order_type IN 4 AND status IN (1,5)", reduceOrder.MainId).Find(&takeOrders).Error; err != nil {
|
|
||||||
log.Error("查询止盈单失败")
|
log.Error("查询止盈单失败")
|
||||||
// return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasrecord bool
|
var hasrecord bool
|
||||||
@ -363,7 +367,7 @@ func FuturesReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, futApi FutRes
|
|||||||
func JudgeFutAddPosition(trade models.TradeSet) {
|
func JudgeFutAddPosition(trade models.TradeSet) {
|
||||||
key := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE)
|
key := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE)
|
||||||
preOrderVal, _ := helper.DefaultRedis.GetAllList(key)
|
preOrderVal, _ := helper.DefaultRedis.GetAllList(key)
|
||||||
db := GetDBConnection()
|
db := commonservice.GetDBConnection()
|
||||||
|
|
||||||
if len(preOrderVal) == 0 {
|
if len(preOrderVal) == 0 {
|
||||||
return
|
return
|
||||||
@ -371,7 +375,7 @@ func JudgeFutAddPosition(trade models.TradeSet) {
|
|||||||
|
|
||||||
futApi := FutRestApi{}
|
futApi := FutRestApi{}
|
||||||
for _, item := range preOrderVal {
|
for _, item := range preOrderVal {
|
||||||
preOrder := AddPositionList{}
|
preOrder := positiondto.AddPositionList{}
|
||||||
if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil {
|
if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil {
|
||||||
log.Error("反序列化失败")
|
log.Error("反序列化失败")
|
||||||
continue
|
continue
|
||||||
@ -395,7 +399,7 @@ func JudgeFutAddPosition(trade models.TradeSet) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 合约加仓触发
|
// 合约加仓触发
|
||||||
func FutAddPositionTrigger(db *gorm.DB, v *AddPositionList, item string, futApi FutRestApi) {
|
func FutAddPositionTrigger(db *gorm.DB, v *positiondto.AddPositionList, item string, futApi FutRestApi) {
|
||||||
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.FutTrigger, v.ApiId, v.Symbol), 20, 5, 100*time.Millisecond)
|
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.FutTrigger, v.ApiId, v.Symbol), 20, 5, 100*time.Millisecond)
|
||||||
|
|
||||||
if ok, err := lock.AcquireWait(context.Background()); err != nil {
|
if ok, err := lock.AcquireWait(context.Background()); err != nil {
|
||||||
@ -405,7 +409,7 @@ func FutAddPositionTrigger(db *gorm.DB, v *AddPositionList, item string, futApi
|
|||||||
defer lock.Release()
|
defer lock.Release()
|
||||||
|
|
||||||
setting, _ := cacheservice.GetSystemSetting(db)
|
setting, _ := cacheservice.GetSystemSetting(db)
|
||||||
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, v.Symbol, 1)
|
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, v.Symbol, 2)
|
||||||
|
|
||||||
if tradeSet.LastPrice == "" {
|
if tradeSet.LastPrice == "" {
|
||||||
log.Errorf("合约加仓触发 查询交易对失败 交易对:%s ordersn:%s", v.Symbol, v.OrderSn)
|
log.Errorf("合约加仓触发 查询交易对失败 交易对:%s ordersn:%s", v.Symbol, v.OrderSn)
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import (
|
|||||||
"go-admin/models/positiondto"
|
"go-admin/models/positiondto"
|
||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
"go-admin/services/cacheservice"
|
"go-admin/services/cacheservice"
|
||||||
|
"go-admin/services/commonservice"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ import (
|
|||||||
/*
|
/*
|
||||||
修改订单信息
|
修改订单信息
|
||||||
*/
|
*/
|
||||||
func ChangeFutureOrder(mapData map[string]interface{}, apiKey string) {
|
func ChangeFutureOrder(mapData map[string]interface{}) {
|
||||||
// 检查订单号是否存在
|
// 检查订单号是否存在
|
||||||
orderSn, ok := mapData["c"]
|
orderSn, ok := mapData["c"]
|
||||||
originOrderSn := mapData["C"] //取消操作 代表原始订单号
|
originOrderSn := mapData["C"] //取消操作 代表原始订单号
|
||||||
@ -42,7 +43,7 @@ func ChangeFutureOrder(mapData map[string]interface{}, apiKey string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取数据库连接
|
// 获取数据库连接
|
||||||
db := GetDBConnection()
|
db := commonservice.GetDBConnection()
|
||||||
if db == nil {
|
if db == nil {
|
||||||
logger.Error("合约订单回调失败,无法获取数据库连接")
|
logger.Error("合约订单回调失败,无法获取数据库连接")
|
||||||
return
|
return
|
||||||
@ -61,15 +62,34 @@ func ChangeFutureOrder(mapData map[string]interface{}, apiKey string) {
|
|||||||
}
|
}
|
||||||
defer lock.Release()
|
defer lock.Release()
|
||||||
|
|
||||||
//反单逻辑
|
// 查询订单
|
||||||
reverseService := ReverseService{}
|
preOrder, err := getPreOrder(db, orderSn)
|
||||||
reverseService.Orm = db
|
|
||||||
reverseService.Log = logger.NewHelper(logger.DefaultLogger)
|
|
||||||
_, err = reverseService.ReverseOrder(apiKey, mapData)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("合约订单回调失败,反单失败:%v", err)
|
logger.Error("合约订单回调失败,查询订单失败:", orderSn, " err:", err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析订单状态
|
||||||
|
status, ok := mapData["X"].(string)
|
||||||
|
if !ok {
|
||||||
|
mapStr, _ := sonic.Marshal(&mapData)
|
||||||
|
logger.Error("订单回调失败,没有状态:", string(mapStr))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 更新订单状态
|
||||||
|
orderStatus, reason := parseOrderStatus(status, mapData)
|
||||||
|
|
||||||
|
if orderStatus == 0 {
|
||||||
|
logger.Error("订单回调失败,状态错误:", orderSn, " status:", status, " reason:", reason)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := updateOrderStatus(db, preOrder, orderStatus, reason, true, mapData); err != nil {
|
||||||
|
logger.Error("修改订单状态失败:", orderSn, " err:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFutOrderByType(db, preOrder, orderStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 合约回调
|
// 合约回调
|
||||||
@ -108,7 +128,7 @@ func handleReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 1)
|
tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 2)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("handleReduceFilled 获取交易对设置失败,订单号:%s", preOrder.OrderSn)
|
logger.Errorf("handleReduceFilled 获取交易对设置失败,订单号:%s", preOrder.OrderSn)
|
||||||
@ -126,8 +146,6 @@ func handleReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
|
|||||||
}
|
}
|
||||||
// rate := utility.StringAsFloat(orderExt.AddPositionVal)
|
// rate := utility.StringAsFloat(orderExt.AddPositionVal)
|
||||||
|
|
||||||
//取消委托中的止盈止损
|
|
||||||
cancelTakeProfitByReduce(db, apiUserInfo, preOrder.Symbol, preOrder.MainId, preOrder.SymbolType)
|
|
||||||
// 100%减仓 终止流程
|
// 100%减仓 终止流程
|
||||||
if orderExt.AddPositionVal.Cmp(decimal.NewFromInt(100)) >= 0 {
|
if orderExt.AddPositionVal.Cmp(decimal.NewFromInt(100)) >= 0 {
|
||||||
//缓存
|
//缓存
|
||||||
@ -162,36 +180,6 @@ func handleReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 减仓成功后取消止盈止损
|
|
||||||
func cancelTakeProfitByReduce(db *gorm.DB, apiUserInfo DbModels.LineApiUser, symbol string, mainId int, symbolType int) {
|
|
||||||
orders, err := GetSymbolTakeAndStop(db, mainId, symbolType)
|
|
||||||
futApi := FutRestApi{}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("mainId:%d 获取委托中的止盈止损失败:%v", mainId, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
orderSns := make([]string, 0)
|
|
||||||
|
|
||||||
for _, v := range orders {
|
|
||||||
if v.OrderType != 1 && v.OrderType != 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
orderSns = append(orderSns, v.OrderSn)
|
|
||||||
}
|
|
||||||
|
|
||||||
arr := utility.SplitSlice(orderSns, 10)
|
|
||||||
|
|
||||||
for _, v := range arr {
|
|
||||||
err := futApi.CancelBatchFutOrder(apiUserInfo, symbol, v)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logger.Errorf("mainId:%d 取消止盈止损失败:%v", mainId, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 减仓处理止盈止损
|
// 减仓处理止盈止损
|
||||||
func FutTakeProfit(db *gorm.DB, preOrder *DbModels.LinePreOrder, apiUserInfo DbModels.LineApiUser, tradeSet models2.TradeSet,
|
func FutTakeProfit(db *gorm.DB, preOrder *DbModels.LinePreOrder, apiUserInfo DbModels.LineApiUser, tradeSet models2.TradeSet,
|
||||||
positionData positiondto.PositionDto, orderExt DbModels.LinePreOrderExt, manualTakeRatio, manualStopRatio decimal.Decimal) bool {
|
positionData positiondto.PositionDto, orderExt DbModels.LinePreOrderExt, manualTakeRatio, manualStopRatio decimal.Decimal) bool {
|
||||||
@ -279,7 +267,7 @@ func nextFuturesReduceTrigger(db *gorm.DB, mainId int, totalNum decimal.Decimal,
|
|||||||
//移除缓存
|
//移除缓存
|
||||||
key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE)
|
key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE)
|
||||||
vals, _ := helper.DefaultRedis.GetAllList(key)
|
vals, _ := helper.DefaultRedis.GetAllList(key)
|
||||||
item := ReduceListItem{}
|
item := positiondto.ReduceListItem{}
|
||||||
|
|
||||||
for _, val := range vals {
|
for _, val := range vals {
|
||||||
sonic.Unmarshal([]byte(val), &item)
|
sonic.Unmarshal([]byte(val), &item)
|
||||||
@ -451,8 +439,8 @@ func removeFutLossAndAddPosition(mainId int, orderSn string) {
|
|||||||
reduceKey := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE)
|
reduceKey := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE)
|
||||||
reduceVal, _ := helper.DefaultRedis.GetAllList(reduceKey)
|
reduceVal, _ := helper.DefaultRedis.GetAllList(reduceKey)
|
||||||
stoploss := dto.StopLossRedisList{}
|
stoploss := dto.StopLossRedisList{}
|
||||||
addPosition := AddPositionList{}
|
addPosition := positiondto.AddPositionList{}
|
||||||
reduce := ReduceListItem{}
|
reduce := positiondto.ReduceListItem{}
|
||||||
//移除减仓后减仓策略
|
//移除减仓后减仓策略
|
||||||
RemoveReduceReduceCacheByMainId(mainId, 2)
|
RemoveReduceReduceCacheByMainId(mainId, 2)
|
||||||
|
|
||||||
@ -496,7 +484,7 @@ func removeFutLossAndAddPosition(mainId int, orderSn string) {
|
|||||||
// 处理主单成交,处理止盈、止损、减仓订单
|
// 处理主单成交,处理止盈、止损、减仓订单
|
||||||
func handleFutMainOrderFilled(db *gorm.DB, preOrder *models.LinePreOrder, extOrderId int, first bool) {
|
func handleFutMainOrderFilled(db *gorm.DB, preOrder *models.LinePreOrder, extOrderId int, first bool) {
|
||||||
// 获取交易对配置和API信息
|
// 获取交易对配置和API信息
|
||||||
tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 1)
|
tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 2)
|
||||||
mainId := preOrder.Id
|
mainId := preOrder.Id
|
||||||
if err != nil || tradeSet.Coin == "" {
|
if err != nil || tradeSet.Coin == "" {
|
||||||
logger.Errorf("获取交易对配置失败, 回调订单号:%s, 错误信息: %v", preOrder.OrderSn, err)
|
logger.Errorf("获取交易对配置失败, 回调订单号:%s, 错误信息: %v", preOrder.OrderSn, err)
|
||||||
@ -535,7 +523,7 @@ func handleFutMainOrderFilled(db *gorm.DB, preOrder *models.LinePreOrder, extOrd
|
|||||||
keyFutAddpositionKey := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE)
|
keyFutAddpositionKey := fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BINANCE)
|
||||||
|
|
||||||
for _, addPositionOrder := range addPositionOrders {
|
for _, addPositionOrder := range addPositionOrders {
|
||||||
addPositionData := AddPositionList{
|
addPositionData := positiondto.AddPositionList{
|
||||||
Id: addPositionOrder.Id,
|
Id: addPositionOrder.Id,
|
||||||
OrderSn: addPositionOrder.OrderSn,
|
OrderSn: addPositionOrder.OrderSn,
|
||||||
MainId: addPositionOrder.MainId,
|
MainId: addPositionOrder.MainId,
|
||||||
@ -741,7 +729,7 @@ func updateOrderQuantity(db *gorm.DB, order models.LinePreOrder, preOrder *model
|
|||||||
// 减仓单
|
// 减仓单
|
||||||
func processFutReduceOrder(order DbModels.LinePreOrder, price, num decimal.Decimal) {
|
func processFutReduceOrder(order DbModels.LinePreOrder, price, num decimal.Decimal) {
|
||||||
key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE)
|
key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE)
|
||||||
item := ReduceListItem{
|
item := positiondto.ReduceListItem{
|
||||||
Id: order.Id,
|
Id: order.Id,
|
||||||
ApiId: order.ApiId,
|
ApiId: order.ApiId,
|
||||||
Pid: order.Pid,
|
Pid: order.Pid,
|
||||||
@ -769,7 +757,7 @@ func processFutReduceOrder(order DbModels.LinePreOrder, price, num decimal.Decim
|
|||||||
// 处理止盈订单
|
// 处理止盈订单
|
||||||
func processFutTakeProfitOrder(db *gorm.DB, futApi FutRestApi, order models.LinePreOrder, num decimal.Decimal) {
|
func processFutTakeProfitOrder(db *gorm.DB, futApi FutRestApi, order models.LinePreOrder, num decimal.Decimal) {
|
||||||
price, _ := decimal.NewFromString(order.Price)
|
price, _ := decimal.NewFromString(order.Price)
|
||||||
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1)
|
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 2)
|
||||||
|
|
||||||
params := FutOrderPlace{
|
params := FutOrderPlace{
|
||||||
ApiId: order.ApiId,
|
ApiId: order.ApiId,
|
||||||
@ -806,7 +794,7 @@ func processFutTakeProfitOrder(db *gorm.DB, futApi FutRestApi, order models.Line
|
|||||||
// 处理止损订单
|
// 处理止损订单
|
||||||
// order 止损单
|
// order 止损单
|
||||||
func processFutStopLossOrder(db *gorm.DB, order models.LinePreOrder, price, num decimal.Decimal) error {
|
func processFutStopLossOrder(db *gorm.DB, order models.LinePreOrder, price, num decimal.Decimal) error {
|
||||||
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1)
|
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 2)
|
||||||
|
|
||||||
params := FutOrderPlace{
|
params := FutOrderPlace{
|
||||||
ApiId: order.ApiId,
|
ApiId: order.ApiId,
|
||||||
|
|||||||
@ -1,587 +0,0 @@
|
|||||||
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("基准测试需要数据库连接")
|
|
||||||
}
|
|
||||||
@ -1,575 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@ -64,7 +64,6 @@ type FutOrderPlace struct {
|
|||||||
ApiId int `json:"api_id"` //api用户id
|
ApiId int `json:"api_id"` //api用户id
|
||||||
Symbol string `json:"symbol"` //合约交易对
|
Symbol string `json:"symbol"` //合约交易对
|
||||||
Side string `json:"side"` //购买方向
|
Side string `json:"side"` //购买方向
|
||||||
PositionSide string `json:"position_side"` //持仓方向
|
|
||||||
Quantity decimal.Decimal `json:"quantity"` //数量
|
Quantity decimal.Decimal `json:"quantity"` //数量
|
||||||
Price decimal.Decimal `json:"price"` //限价单价
|
Price decimal.Decimal `json:"price"` //限价单价
|
||||||
SideType string `json:"side_type"` //现价或者市价
|
SideType string `json:"side_type"` //现价或者市价
|
||||||
@ -73,7 +72,6 @@ type FutOrderPlace struct {
|
|||||||
StopPrice decimal.Decimal `json:"stopprice"` //止损价格
|
StopPrice decimal.Decimal `json:"stopprice"` //止损价格
|
||||||
OrderType string `json:"order_type"` //订单类型,市价或限价MARKET(市价单) TAKE_PROFIT_MARKET(市价止盈) TAKE_PROFIT(限价止盈) STOP (限价止损) STOP_MARKET(市价止损)
|
OrderType string `json:"order_type"` //订单类型,市价或限价MARKET(市价单) TAKE_PROFIT_MARKET(市价止盈) TAKE_PROFIT(限价止盈) STOP (限价止损) STOP_MARKET(市价止损)
|
||||||
NewClientOrderId string `json:"newClientOrderId"`
|
NewClientOrderId string `json:"newClientOrderId"`
|
||||||
ClosePosition bool `json:"closePosition"` //是否平仓
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s FutOrderPlace) CheckParams() error {
|
func (s FutOrderPlace) CheckParams() error {
|
||||||
@ -188,19 +186,6 @@ type OpenOrders struct {
|
|||||||
GoodTillDate int `json:"goodTillDate"` //order pre-set auot cancel time for TIF GTD order
|
GoodTillDate int `json:"goodTillDate"` //order pre-set auot cancel time for TIF GTD order
|
||||||
}
|
}
|
||||||
|
|
||||||
// 待触发加仓单
|
|
||||||
type AddPositionList struct {
|
|
||||||
Id int `json:"id"` //订单id
|
|
||||||
Pid int `json:"pid"` //父级id
|
|
||||||
MainId int `json:"mainId"` //主单Id
|
|
||||||
ApiId int `json:"apiId"` //触发账户id
|
|
||||||
Symbol string `json:"symbol"` //交易对
|
|
||||||
Price decimal.Decimal `json:"price"` //触发价
|
|
||||||
Side string `json:"side"` //买卖方向
|
|
||||||
SymbolType int `json:"type" comment:"交易对类别 1-现货 2-合约"`
|
|
||||||
OrderSn string `json:"prderSn"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// SpotAccountInfo 现货账户信息
|
// SpotAccountInfo 现货账户信息
|
||||||
type SpotAccountInfo struct {
|
type SpotAccountInfo struct {
|
||||||
MakerCommission int `json:"makerCommission"`
|
MakerCommission int `json:"makerCommission"`
|
||||||
@ -229,15 +214,3 @@ type SpotAccountInfo struct {
|
|||||||
Permissions []string `json:"permissions"`
|
Permissions []string `json:"permissions"`
|
||||||
Uid int `json:"uid"`
|
Uid int `json:"uid"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReduceListItem struct {
|
|
||||||
Id int `json:"id"`
|
|
||||||
ApiId int `json:"apiId"`
|
|
||||||
MainId int `json:"mainId"`
|
|
||||||
Pid int `json:"pid"`
|
|
||||||
Symbol string `json:"symbol"`
|
|
||||||
Price decimal.Decimal `json:"price"`
|
|
||||||
Side string `json:"side"`
|
|
||||||
Num decimal.Decimal `json:"num"`
|
|
||||||
OrderSn string `json:"orderSn"`
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,594 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@ -1,15 +1,10 @@
|
|||||||
package binanceservice
|
package binanceservice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"go-admin/app/admin/models"
|
"go-admin/app/admin/models"
|
||||||
DbModels "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/go-admin-team/go-admin-core/logger"
|
||||||
"github.com/jinzhu/copier"
|
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@ -134,7 +129,7 @@ func GetTotalLossAmount(db *gorm.DB, mainId int) (decimal.Decimal, error) {
|
|||||||
return totalLossAmountU, nil
|
return totalLossAmountU, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取交易对的 委托中的止盈止损、减仓单
|
// 获取交易对的 委托中的止盈止损
|
||||||
// mainId 主单id
|
// mainId 主单id
|
||||||
// symbolType 交易对类型
|
// symbolType 交易对类型
|
||||||
func GetSymbolTakeAndStop(db *gorm.DB, mainId int, symbolType int) ([]models.LinePreOrder, error) {
|
func GetSymbolTakeAndStop(db *gorm.DB, mainId int, symbolType int) ([]models.LinePreOrder, error) {
|
||||||
@ -175,69 +170,3 @@ func GetChildTpOrder(db *gorm.DB, pid int) (int, error) {
|
|||||||
|
|
||||||
return int(count), nil
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"go-admin/common/global"
|
"go-admin/common/global"
|
||||||
"go-admin/common/helper"
|
"go-admin/common/helper"
|
||||||
models2 "go-admin/models"
|
models2 "go-admin/models"
|
||||||
|
"go-admin/models/positiondto"
|
||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
@ -19,7 +20,7 @@ import (
|
|||||||
// 缓存减仓节点和重新生成
|
// 缓存减仓节点和重新生成
|
||||||
// reduceOrder:原始减仓单
|
// reduceOrder:原始减仓单
|
||||||
// reduceOrderStrategy:减仓策略
|
// reduceOrderStrategy:减仓策略
|
||||||
func CacheOrderStrategyAndReCreate(db *gorm.DB, reduceOrder ReduceListItem, symbolType int, tradeSet models2.TradeSet, setting models.LineSystemSetting) error {
|
func CacheOrderStrategyAndReCreate(db *gorm.DB, reduceOrder positiondto.ReduceListItem, symbolType int, tradeSet models2.TradeSet, setting models.LineSystemSetting) error {
|
||||||
reduceOrderStrategy := models.LineOrderReduceStrategy{}
|
reduceOrderStrategy := models.LineOrderReduceStrategy{}
|
||||||
var key string
|
var key string
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,34 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"go-admin/common/global"
|
"go-admin/common/global"
|
||||||
"go-admin/common/helper"
|
"go-admin/common/helper"
|
||||||
"go-admin/models"
|
"go-admin/models"
|
||||||
|
"go-admin/models/positiondto"
|
||||||
"go-admin/services/cacheservice"
|
"go-admin/services/cacheservice"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -29,7 +30,7 @@ func TestSpotJudge(t *testing.T) {
|
|||||||
|
|
||||||
key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE)
|
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"}`
|
item := `{"id":66,"apiId":49,"mainId":63,"pid":63,"symbol":"ADAUSDT","price":"0.5912","side":"SELL","num":"12.6","orderSn":"397919643961917440"}`
|
||||||
reduceOrder := ReduceListItem{}
|
reduceOrder := positiondto.ReduceListItem{}
|
||||||
spotApi := SpotRestApi{}
|
spotApi := SpotRestApi{}
|
||||||
setting, err := cacheservice.GetSystemSetting(db)
|
setting, err := cacheservice.GetSystemSetting(db)
|
||||||
|
|
||||||
|
|||||||
@ -10,8 +10,11 @@ import (
|
|||||||
"go-admin/common/global"
|
"go-admin/common/global"
|
||||||
"go-admin/common/helper"
|
"go-admin/common/helper"
|
||||||
"go-admin/models"
|
"go-admin/models"
|
||||||
|
"go-admin/models/positiondto"
|
||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
"go-admin/services/cacheservice"
|
"go-admin/services/cacheservice"
|
||||||
|
"go-admin/services/commonservice"
|
||||||
|
"go-admin/services/orderservice"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -27,13 +30,16 @@ import (
|
|||||||
func JudgeSpotPrice(trade models.TradeSet) {
|
func JudgeSpotPrice(trade models.TradeSet) {
|
||||||
key := fmt.Sprintf(rediskey.PreSpotOrderList, global.EXCHANGE_BINANCE)
|
key := fmt.Sprintf(rediskey.PreSpotOrderList, global.EXCHANGE_BINANCE)
|
||||||
preOrderVal, _ := helper.DefaultRedis.GetAllList(key)
|
preOrderVal, _ := helper.DefaultRedis.GetAllList(key)
|
||||||
db := GetDBConnection()
|
db := commonservice.GetDBConnection()
|
||||||
|
|
||||||
if len(preOrderVal) == 0 {
|
if len(preOrderVal) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
spotApi := SpotRestApi{}
|
spotApi := SpotRestApi{}
|
||||||
|
orderService := orderservice.OrderService{}
|
||||||
|
orderService.Orm = db
|
||||||
|
|
||||||
for _, item := range preOrderVal {
|
for _, item := range preOrderVal {
|
||||||
preOrder := dto.PreOrderRedisList{}
|
preOrder := dto.PreOrderRedisList{}
|
||||||
if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil {
|
if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil {
|
||||||
@ -46,6 +52,18 @@ func JudgeSpotPrice(trade models.TradeSet) {
|
|||||||
tradePrice, _ := decimal.NewFromString(trade.LastPrice)
|
tradePrice, _ := decimal.NewFromString(trade.LastPrice)
|
||||||
//买入
|
//买入
|
||||||
if strings.ToUpper(preOrder.Site) == "BUY" && orderPrice.Cmp(tradePrice) >= 0 && orderPrice.Cmp(decimal.Zero) > 0 && tradePrice.Cmp(decimal.Zero) > 0 {
|
if strings.ToUpper(preOrder.Site) == "BUY" && orderPrice.Cmp(tradePrice) >= 0 && orderPrice.Cmp(decimal.Zero) > 0 && tradePrice.Cmp(decimal.Zero) > 0 {
|
||||||
|
//判断个人websocket是否超时
|
||||||
|
|
||||||
|
apiInfo, _ := GetApiInfo(preOrder.ApiId)
|
||||||
|
if err := commonservice.JudgeWebsocketTimeout(apiInfo.ApiKey, 1); err != nil {
|
||||||
|
log.Errorf("现货行情订阅超时,apiKey:%s err:%v", err)
|
||||||
|
|
||||||
|
if err1 := orderService.ErrorTrigger(&preOrder, fmt.Sprintf("行情触发失败,err:%v", err)); err1 != nil {
|
||||||
|
log.Error("触发失败", err1)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
SpotOrderLock(db, &preOrder, item, spotApi)
|
SpotOrderLock(db, &preOrder, item, spotApi)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,7 +158,7 @@ func JudgeSpotStopLoss(trade models.TradeSet) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
db := GetDBConnection()
|
db := commonservice.GetDBConnection()
|
||||||
spotApi := SpotRestApi{}
|
spotApi := SpotRestApi{}
|
||||||
setting, err := cacheservice.GetSystemSetting(db)
|
setting, err := cacheservice.GetSystemSetting(db)
|
||||||
|
|
||||||
@ -149,7 +167,7 @@ func JudgeSpotStopLoss(trade models.TradeSet) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, trade.Coin+trade.Currency, 0)
|
tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, trade.Coin+trade.Currency, 1)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("获取交易设置失败")
|
log.Error("获取交易设置失败")
|
||||||
@ -257,7 +275,7 @@ func SpotStopLossTrigger(db *gorm.DB, stopOrder dto.StopLossRedisList, spotApi S
|
|||||||
// 判断是否触发现货减仓
|
// 判断是否触发现货减仓
|
||||||
func JudgeSpotReduce(trade models.TradeSet) {
|
func JudgeSpotReduce(trade models.TradeSet) {
|
||||||
key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE)
|
key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE)
|
||||||
db := GetDBConnection()
|
db := commonservice.GetDBConnection()
|
||||||
spotApi := SpotRestApi{}
|
spotApi := SpotRestApi{}
|
||||||
setting, err := cacheservice.GetSystemSetting(db)
|
setting, err := cacheservice.GetSystemSetting(db)
|
||||||
|
|
||||||
@ -266,12 +284,14 @@ func JudgeSpotReduce(trade models.TradeSet) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
reduceOrder := ReduceListItem{}
|
reduceOrder := positiondto.ReduceListItem{}
|
||||||
tradePrice, _ := decimal.NewFromString(trade.LastPrice)
|
tradePrice, _ := decimal.NewFromString(trade.LastPrice)
|
||||||
//减仓单减仓策略
|
//减仓单减仓策略
|
||||||
reduceReduceListKey := fmt.Sprintf(rediskey.SpotOrderReduceStrategyList, global.EXCHANGE_BINANCE)
|
reduceReduceListKey := fmt.Sprintf(rediskey.SpotOrderReduceStrategyList, global.EXCHANGE_BINANCE)
|
||||||
orderReduceVal, _ := helper.DefaultRedis.HGetAllFields(reduceReduceListKey)
|
orderReduceVal, _ := helper.DefaultRedis.HGetAllFields(reduceReduceListKey)
|
||||||
reduceOrderStrategy := dto.LineOrderReduceStrategyResp{}
|
reduceOrderStrategy := dto.LineOrderReduceStrategyResp{}
|
||||||
|
orderService := orderservice.OrderService{}
|
||||||
|
orderService.Orm = db
|
||||||
|
|
||||||
for _, item := range orderReduceVal {
|
for _, item := range orderReduceVal {
|
||||||
sonic.Unmarshal([]byte(item), &reduceOrderStrategy)
|
sonic.Unmarshal([]byte(item), &reduceOrderStrategy)
|
||||||
@ -297,7 +317,7 @@ func JudgeSpotReduce(trade models.TradeSet) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
order, err := CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit)
|
order, err := orderService.CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("%d 生成订单失败", reduceOrderStrategy.OrderId)
|
log.Errorf("%d 生成订单失败", reduceOrderStrategy.OrderId)
|
||||||
@ -367,8 +387,8 @@ func JudgeSpotReduce(trade models.TradeSet) {
|
|||||||
// 触发现货减仓
|
// 触发现货减仓
|
||||||
// isStrategy 是否是策略减仓单
|
// isStrategy 是否是策略减仓单
|
||||||
// reduceId 策略主减仓单id
|
// reduceId 策略主减仓单id
|
||||||
func SpotReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, spotApi SpotRestApi, setting DbModels.LineSystemSetting, key, item string, isStrategy bool, reduceId int) bool {
|
func SpotReduceTrigger(db *gorm.DB, reduceOrder positiondto.ReduceListItem, spotApi SpotRestApi, setting DbModels.LineSystemSetting, key, item string, isStrategy bool, reduceId int) bool {
|
||||||
tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reduceOrder.Symbol, 0)
|
tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reduceOrder.Symbol, 1)
|
||||||
result := true
|
result := true
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -470,7 +490,7 @@ func SpotReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, spotApi SpotRest
|
|||||||
func JudgeSpotAddPosition(trade models.TradeSet) {
|
func JudgeSpotAddPosition(trade models.TradeSet) {
|
||||||
key := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE)
|
key := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE)
|
||||||
preOrderVal, _ := helper.DefaultRedis.GetAllList(key)
|
preOrderVal, _ := helper.DefaultRedis.GetAllList(key)
|
||||||
db := GetDBConnection()
|
db := commonservice.GetDBConnection()
|
||||||
|
|
||||||
if len(preOrderVal) == 0 {
|
if len(preOrderVal) == 0 {
|
||||||
return
|
return
|
||||||
@ -478,7 +498,7 @@ func JudgeSpotAddPosition(trade models.TradeSet) {
|
|||||||
|
|
||||||
spotApi := SpotRestApi{}
|
spotApi := SpotRestApi{}
|
||||||
for _, item := range preOrderVal {
|
for _, item := range preOrderVal {
|
||||||
preOrder := AddPositionList{}
|
preOrder := positiondto.AddPositionList{}
|
||||||
if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil {
|
if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil {
|
||||||
log.Error("反序列化失败")
|
log.Error("反序列化失败")
|
||||||
continue
|
continue
|
||||||
@ -495,7 +515,7 @@ func JudgeSpotAddPosition(trade models.TradeSet) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func SpotAddPositionTrigger(db *gorm.DB, v *AddPositionList, item string, spotApi SpotRestApi) {
|
func SpotAddPositionTrigger(db *gorm.DB, v *positiondto.AddPositionList, item string, spotApi SpotRestApi) {
|
||||||
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, v.ApiId, v.Symbol), 20, 5, 100*time.Millisecond)
|
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, v.ApiId, v.Symbol), 20, 5, 100*time.Millisecond)
|
||||||
|
|
||||||
if ok, err := lock.AcquireWait(context.Background()); err != nil {
|
if ok, err := lock.AcquireWait(context.Background()); err != nil {
|
||||||
@ -505,7 +525,7 @@ func SpotAddPositionTrigger(db *gorm.DB, v *AddPositionList, item string, spotAp
|
|||||||
defer lock.Release()
|
defer lock.Release()
|
||||||
|
|
||||||
setting, _ := cacheservice.GetSystemSetting(db)
|
setting, _ := cacheservice.GetSystemSetting(db)
|
||||||
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, v.Symbol, 0)
|
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, v.Symbol, 1)
|
||||||
|
|
||||||
if tradeSet.LastPrice == "" {
|
if tradeSet.LastPrice == "" {
|
||||||
log.Errorf("现货加仓触发 查询交易对失败 交易对:%s ordersn:%s", v.Symbol, v.OrderSn)
|
log.Errorf("现货加仓触发 查询交易对失败 交易对:%s ordersn:%s", v.Symbol, v.OrderSn)
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import (
|
|||||||
"go-admin/models/positiondto"
|
"go-admin/models/positiondto"
|
||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
"go-admin/services/cacheservice"
|
"go-admin/services/cacheservice"
|
||||||
|
"go-admin/services/commonservice"
|
||||||
"go-admin/services/positionservice"
|
"go-admin/services/positionservice"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -21,6 +22,7 @@ import (
|
|||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/go-admin-team/go-admin-core/logger"
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
log "github.com/go-admin-team/go-admin-core/logger"
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@ -28,7 +30,7 @@ import (
|
|||||||
/*
|
/*
|
||||||
订单回调
|
订单回调
|
||||||
*/
|
*/
|
||||||
func ChangeSpotOrder(mapData map[string]interface{}, apiKey string) {
|
func ChangeSpotOrder(mapData map[string]interface{}) {
|
||||||
// 检查订单号是否存在
|
// 检查订单号是否存在
|
||||||
orderSn, ok := mapData["c"]
|
orderSn, ok := mapData["c"]
|
||||||
originOrderSn := mapData["C"] //取消操作 代表原始订单号
|
originOrderSn := mapData["C"] //取消操作 代表原始订单号
|
||||||
@ -43,7 +45,7 @@ func ChangeSpotOrder(mapData map[string]interface{}, apiKey string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取数据库连接
|
// 获取数据库连接
|
||||||
db := GetDBConnection()
|
db := commonservice.GetDBConnection()
|
||||||
if db == nil {
|
if db == nil {
|
||||||
logger.Error("订单回调失败, 无法获取数据库连接")
|
logger.Error("订单回调失败, 无法获取数据库连接")
|
||||||
return
|
return
|
||||||
@ -165,7 +167,7 @@ func handleMainReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 0)
|
tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 1)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Errorf("handleMainReduceFilled 获取交易对设置失败,订单号:%s", preOrder.OrderSn)
|
logger.Errorf("handleMainReduceFilled 获取交易对设置失败,订单号:%s", preOrder.OrderSn)
|
||||||
@ -192,7 +194,7 @@ func handleMainReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
|
|||||||
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotReduceCallback, preOrder.ApiId, preOrder.Symbol), 120, 20, 100*time.Millisecond)
|
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotReduceCallback, preOrder.ApiId, preOrder.Symbol), 120, 20, 100*time.Millisecond)
|
||||||
|
|
||||||
if ok, err := lock.AcquireWait(context.Background()); err != nil {
|
if ok, err := lock.AcquireWait(context.Background()); err != nil {
|
||||||
logger.Error("获取锁失败", err)
|
log.Error("获取锁失败", err)
|
||||||
return
|
return
|
||||||
} else if ok {
|
} else if ok {
|
||||||
defer lock.Release()
|
defer lock.Release()
|
||||||
@ -299,7 +301,7 @@ func nextSpotReduceTrigger(db *gorm.DB, mainId int, totalNum decimal.Decimal, tr
|
|||||||
//移除缓存
|
//移除缓存
|
||||||
key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE)
|
key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE)
|
||||||
vals, _ := helper.DefaultRedis.GetAllList(key)
|
vals, _ := helper.DefaultRedis.GetAllList(key)
|
||||||
item := ReduceListItem{}
|
item := positiondto.ReduceListItem{}
|
||||||
|
|
||||||
for _, val := range vals {
|
for _, val := range vals {
|
||||||
sonic.Unmarshal([]byte(val), &item)
|
sonic.Unmarshal([]byte(val), &item)
|
||||||
@ -552,8 +554,8 @@ func removeSpotLossAndAddPosition(mainId int, orderSn string) {
|
|||||||
reduceKey := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE)
|
reduceKey := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE)
|
||||||
reduceVal, _ := helper.DefaultRedis.GetAllList(reduceKey)
|
reduceVal, _ := helper.DefaultRedis.GetAllList(reduceKey)
|
||||||
stoploss := dto.StopLossRedisList{}
|
stoploss := dto.StopLossRedisList{}
|
||||||
addPosition := AddPositionList{}
|
addPosition := positiondto.AddPositionList{}
|
||||||
reduce := ReduceListItem{}
|
reduce := positiondto.ReduceListItem{}
|
||||||
|
|
||||||
//移除减仓后减仓策略
|
//移除减仓后减仓策略
|
||||||
RemoveReduceReduceCacheByMainId(mainId, 1)
|
RemoveReduceReduceCacheByMainId(mainId, 1)
|
||||||
@ -645,7 +647,7 @@ func handleMainOrderFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
|
|||||||
keySpotAddPosition := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE)
|
keySpotAddPosition := fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BINANCE)
|
||||||
|
|
||||||
for _, addPositionOrder := range addPositionOrders {
|
for _, addPositionOrder := range addPositionOrders {
|
||||||
addPositionData := AddPositionList{
|
addPositionData := positiondto.AddPositionList{
|
||||||
Id: addPositionOrder.Id,
|
Id: addPositionOrder.Id,
|
||||||
OrderSn: addPositionOrder.OrderSn,
|
OrderSn: addPositionOrder.OrderSn,
|
||||||
MainId: addPositionOrder.MainId,
|
MainId: addPositionOrder.MainId,
|
||||||
@ -743,7 +745,7 @@ func updateOrderStatus(db *gorm.DB, preOrder *models.LinePreOrder, status int, r
|
|||||||
// fist 首次止盈止损
|
// fist 首次止盈止损
|
||||||
func processTakeProfitAndStopLossOrders(db *gorm.DB, preOrder *models.LinePreOrder, positionData *positiondto.PositionDto, extOrderId int, fist bool) {
|
func processTakeProfitAndStopLossOrders(db *gorm.DB, preOrder *models.LinePreOrder, positionData *positiondto.PositionDto, extOrderId int, fist bool) {
|
||||||
orders := []models.LinePreOrder{}
|
orders := []models.LinePreOrder{}
|
||||||
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 0)
|
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, preOrder.Symbol, 1)
|
||||||
mainId := preOrder.Id
|
mainId := preOrder.Id
|
||||||
|
|
||||||
if preOrder.MainId > 0 {
|
if preOrder.MainId > 0 {
|
||||||
@ -854,7 +856,7 @@ func SetPrice(order *models.LinePreOrder, preOrder *models.LinePreOrder, tradeSe
|
|||||||
// 现货减仓
|
// 现货减仓
|
||||||
func processSpotReduceOrder(preOrder DbModels.LinePreOrder) {
|
func processSpotReduceOrder(preOrder DbModels.LinePreOrder) {
|
||||||
key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE)
|
key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE)
|
||||||
item := ReduceListItem{
|
item := positiondto.ReduceListItem{
|
||||||
Id: preOrder.Id,
|
Id: preOrder.Id,
|
||||||
ApiId: preOrder.ApiId,
|
ApiId: preOrder.ApiId,
|
||||||
Pid: preOrder.Pid,
|
Pid: preOrder.Pid,
|
||||||
@ -881,7 +883,7 @@ func processSpotReduceOrder(preOrder DbModels.LinePreOrder) {
|
|||||||
|
|
||||||
// 处理止盈订单
|
// 处理止盈订单
|
||||||
func processTakeProfitOrder(db *gorm.DB, spotApi SpotRestApi, order models.LinePreOrder) {
|
func processTakeProfitOrder(db *gorm.DB, spotApi SpotRestApi, order models.LinePreOrder) {
|
||||||
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 0)
|
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1)
|
||||||
|
|
||||||
if tradeSet.Coin == "" {
|
if tradeSet.Coin == "" {
|
||||||
logger.Error("获取交易对失败")
|
logger.Error("获取交易对失败")
|
||||||
|
|||||||
@ -12,42 +12,30 @@ import (
|
|||||||
models2 "go-admin/models"
|
models2 "go-admin/models"
|
||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
"go-admin/services/cacheservice"
|
"go-admin/services/cacheservice"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"github.com/go-admin-team/go-admin-core/logger"
|
|
||||||
"github.com/go-admin-team/go-admin-core/sdk/service"
|
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type BinanceStrategyOrderService struct {
|
type BinanceStrategyOrderService struct {
|
||||||
service.Service
|
service.Service
|
||||||
Debug bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否触发波段订单
|
// 判断是否触发波段订单
|
||||||
func (e *BinanceStrategyOrderService) TriggerStrategyOrder(exchangeType string) {
|
func (e *BinanceStrategyOrderService) TriggerStrategyOrder(exchangeType string) {
|
||||||
//现货
|
//现货
|
||||||
orderStrs, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.StrategySpotOrderList, exchangeType))
|
orderStrs, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.StrategyFutOrderList, exchangeType))
|
||||||
if len(orderStrs) > 0 {
|
|
||||||
e.DoJudge(orderStrs, 1, exchangeType)
|
e.DoJudge(orderStrs, 1, exchangeType)
|
||||||
}
|
|
||||||
|
|
||||||
//合约
|
//合约
|
||||||
futOrdedrStrs, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.StrategyFutOrderList, exchangeType))
|
futOrdedrStrs, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.StrategyFutOrderList, exchangeType))
|
||||||
|
|
||||||
if len(futOrdedrStrs) > 0 {
|
|
||||||
e.DoJudge(futOrdedrStrs, 2, exchangeType)
|
e.DoJudge(futOrdedrStrs, 2, exchangeType)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否符合条件
|
// 处理订单
|
||||||
func (e *BinanceStrategyOrderService) DoJudge(orderStrs []string, symbolType int, exchangeType string) {
|
func (e *BinanceStrategyOrderService) DoJudge(orderStrs []string, symbolType int, exchangeType string) {
|
||||||
db := GetDBConnection()
|
|
||||||
// setting, _ := cacheservice.GetSystemSetting(db)
|
|
||||||
|
|
||||||
for _, orderStr := range orderStrs {
|
for _, orderStr := range orderStrs {
|
||||||
var lockKey string
|
var lockKey string
|
||||||
orderItem := dto.StrategyOrderRedisList{}
|
orderItem := dto.StrategyOrderRedisList{}
|
||||||
@ -63,7 +51,7 @@ func (e *BinanceStrategyOrderService) DoJudge(orderStrs []string, symbolType int
|
|||||||
lockKey = rediskey.StrategyFutTriggerLock
|
lockKey = rediskey.StrategyFutTriggerLock
|
||||||
}
|
}
|
||||||
|
|
||||||
lock := helper.NewRedisLock(fmt.Sprintf(lockKey, orderItem.ApiId, orderItem.OrderSn), 60, 20, 300*time.Millisecond)
|
lock := helper.NewRedisLock(fmt.Sprintf(lockKey, orderItem.ApiId, orderItem.Symbol), 200, 50, 100*time.Millisecond)
|
||||||
|
|
||||||
if ok, err := lock.AcquireWait(context.Background()); err != nil {
|
if ok, err := lock.AcquireWait(context.Background()); err != nil {
|
||||||
e.Log.Debug("获取锁失败", err)
|
e.Log.Debug("获取锁失败", err)
|
||||||
@ -72,14 +60,14 @@ func (e *BinanceStrategyOrderService) DoJudge(orderStrs []string, symbolType int
|
|||||||
defer lock.Release()
|
defer lock.Release()
|
||||||
|
|
||||||
//判断是否符合条件
|
//判断是否符合条件
|
||||||
success, err := e.JudgeStrategy(orderItem, symbolType, exchangeType)
|
success, err := e.JudgeStrategy(orderItem, 1, exchangeType)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.Log.Errorf("order_id:%d err:%v", orderItem.Id, err)
|
e.Log.Errorf("order_id:%d err:%v", orderItem.Id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.Debug || success {
|
if success {
|
||||||
e.TriggerOrder(db, orderStr, orderItem, symbolType)
|
e.TriggerOrder(orderItem, symbolType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,16 +95,8 @@ func (e *BinanceStrategyOrderService) JudgeStrategy(order dto.StrategyOrderRedis
|
|||||||
}
|
}
|
||||||
|
|
||||||
score := lastPrices[0].Score
|
score := lastPrices[0].Score
|
||||||
var startPrice decimal.Decimal
|
startPrice := utility.StrToDecimal(beforePrice)
|
||||||
var lastPrice decimal.Decimal
|
lastPrice := utility.StrToDecimal(lastPrices[0].Member.(string))
|
||||||
startPriceArgs := strings.Split(beforePrice, ":")
|
|
||||||
endPricecArgs := strings.Split(lastPrices[0].Member.(string), ":")
|
|
||||||
|
|
||||||
if len(startPriceArgs) == 0 || len(endPricecArgs) == 0 {
|
|
||||||
return result, errors.New("获取交易对起止价格失败")
|
|
||||||
}
|
|
||||||
startPrice = utility.StrToDecimal(startPriceArgs[len(startPriceArgs)-1])
|
|
||||||
lastPrice = utility.StrToDecimal(endPricecArgs[len(endPricecArgs)-1])
|
|
||||||
|
|
||||||
//时间差超过10s
|
//时间差超过10s
|
||||||
if (nowUtc-int64(score))/1000 > 10 {
|
if (nowUtc-int64(score))/1000 > 10 {
|
||||||
@ -128,35 +108,31 @@ func (e *BinanceStrategyOrderService) JudgeStrategy(order dto.StrategyOrderRedis
|
|||||||
}
|
}
|
||||||
|
|
||||||
percentag := lastPrice.Div(startPrice).Sub(decimal.NewFromInt(1)).Truncate(6)
|
percentag := lastPrice.Div(startPrice).Sub(decimal.NewFromInt(1)).Truncate(6)
|
||||||
logger.Infof("百分比:%s", percentag.Mul(decimal.NewFromInt(100)).String())
|
|
||||||
//价格没有变动
|
//价格没有变动
|
||||||
if percentag.Cmp(decimal.Zero) == 0 {
|
if percentag.Cmp(decimal.Zero) == 0 {
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//满足条件
|
//满足条件
|
||||||
switch {
|
switch order.CompareType {
|
||||||
//涨价格大于0.5% 跌价格小于-0.5%
|
case 1:
|
||||||
case order.CompareType == 1 && order.Direction == 1 && percentag.Cmp(decimal.Zero) > 0,
|
result = percentag.Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) > 0
|
||||||
order.CompareType == 1 && order.Direction == 2 && percentag.Cmp(decimal.Zero) < 0:
|
case 2:
|
||||||
result = percentag.Abs().Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) > 0
|
result = percentag.Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) >= 0
|
||||||
|
case 5:
|
||||||
case order.CompareType == 2 && order.Direction == 1 && percentag.Cmp(decimal.Zero) > 0,
|
result = percentag.Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) == 0
|
||||||
order.CompareType == 2 && order.Direction == 2 && percentag.Cmp(decimal.Zero) < 0:
|
default:
|
||||||
result = percentag.Abs().Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) >= 0
|
return result, errors.New("没有对应的类型")
|
||||||
|
|
||||||
case order.CompareType == 5 && order.Direction == 1 && percentag.Cmp(decimal.Zero) > 0,
|
|
||||||
order.CompareType == 5 && order.Direction == 2 && percentag.Cmp(decimal.Zero) < 0:
|
|
||||||
result = percentag.Abs().Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) == 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 触发委托单
|
// 触发委托单
|
||||||
func (e *BinanceStrategyOrderService) TriggerOrder(db *gorm.DB, cacheVal string, order dto.StrategyOrderRedisList, symbolType int) error {
|
func (e *BinanceStrategyOrderService) TriggerOrder(order dto.StrategyOrderRedisList, symbolType int) error {
|
||||||
orders := make([]models.LinePreOrder, 0)
|
orders := make([]models.LinePreOrder, 0)
|
||||||
if err := e.Orm.Model(&models.LinePreOrder{}).Where("main_id =? or id =?", order.Id, order.Id).Find(&orders).Error; err != nil {
|
if err := e.Orm.Model(&models.LinePreOrder{}).Where("main_id =?", order.Id).Find(&orders).Error; err != nil {
|
||||||
e.Log.Errorf("order_id:%d 获取委托单失败:%s", order.Id, err.Error())
|
e.Log.Errorf("order_id:%d 获取委托单失败:%s", order.Id, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -167,16 +143,7 @@ func (e *BinanceStrategyOrderService) TriggerOrder(db *gorm.DB, cacheVal string,
|
|||||||
return errors.New("获取系统设置失败")
|
return errors.New("获取系统设置失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
var tradeSet models2.TradeSet
|
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, symbolType)
|
||||||
|
|
||||||
switch symbolType {
|
|
||||||
case 1:
|
|
||||||
tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 0)
|
|
||||||
case 2:
|
|
||||||
tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1)
|
|
||||||
default:
|
|
||||||
return errors.New("获取交易对行情失败,交易对类型错误")
|
|
||||||
}
|
|
||||||
|
|
||||||
if tradeSet.Coin == "" {
|
if tradeSet.Coin == "" {
|
||||||
return errors.New("获取交易对行情失败")
|
return errors.New("获取交易对行情失败")
|
||||||
@ -188,7 +155,7 @@ func (e *BinanceStrategyOrderService) TriggerOrder(db *gorm.DB, cacheVal string,
|
|||||||
return errors.New("最新成交价小于等于0")
|
return errors.New("最新成交价小于等于0")
|
||||||
}
|
}
|
||||||
|
|
||||||
mainOrder := models.LinePreOrder{}
|
var mainOrder models.LinePreOrder
|
||||||
|
|
||||||
for _, v := range orders {
|
for _, v := range orders {
|
||||||
if v.MainId == 0 {
|
if v.MainId == 0 {
|
||||||
@ -197,102 +164,9 @@ func (e *BinanceStrategyOrderService) TriggerOrder(db *gorm.DB, cacheVal string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if mainOrder.Id == 0 {
|
|
||||||
return errors.New("获取主单失败")
|
|
||||||
}
|
|
||||||
|
|
||||||
GetOrderByPid(&mainOrder, orders, mainOrder.Id)
|
GetOrderByPid(&mainOrder, orders, mainOrder.Id)
|
||||||
|
|
||||||
e.RecalculateOrder(tradeSet, &mainOrder, setting)
|
|
||||||
|
|
||||||
//事务保存
|
|
||||||
err := e.Orm.Transaction(func(tx *gorm.DB) error {
|
|
||||||
if err1 := tx.Save(&mainOrder).Error; err1 != nil {
|
|
||||||
return err1
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range mainOrder.Childs {
|
|
||||||
if err1 := tx.Save(&v).Error; err1 != nil {
|
|
||||||
return err1
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v2 := range v.Childs {
|
|
||||||
if err1 := tx.Save(&v2).Error; err1 != nil {
|
|
||||||
return err1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
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")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重新计算订单单价、数量
|
// 重新计算订单单价、数量
|
||||||
@ -301,200 +175,48 @@ func (e *BinanceStrategyOrderService) StrategyOrderPlace(db *gorm.DB, cacheVal s
|
|||||||
// setting 系统设置
|
// setting 系统设置
|
||||||
func (e *BinanceStrategyOrderService) RecalculateOrder(tradeSet models2.TradeSet, mainOrder *models.LinePreOrder, setting models.LineSystemSetting) error {
|
func (e *BinanceStrategyOrderService) RecalculateOrder(tradeSet models2.TradeSet, mainOrder *models.LinePreOrder, setting models.LineSystemSetting) error {
|
||||||
exts := make([]models.LinePreOrderExt, 0)
|
exts := make([]models.LinePreOrderExt, 0)
|
||||||
if err := e.Orm.Model(models.LinePreOrderExt{}).Where("main_order_id =?", mainOrder.Id).Find(&exts).Error; err != nil {
|
if err := e.Orm.Model(models.LinePreOrderExt{}).Where("main_id =?", mainOrder.Id).Find(&exts).Error; err != nil {
|
||||||
return errors.New("获取拓展信息失败")
|
return errors.New("获取拓展信息失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
var newPrice decimal.Decimal
|
|
||||||
lastPrice := utility.StrToDecimal(tradeSet.LastPrice)
|
lastPrice := utility.StrToDecimal(tradeSet.LastPrice)
|
||||||
rate := utility.StrToDecimal(mainOrder.Rate)
|
rate := utility.StrToDecimal(mainOrder.Rate)
|
||||||
newPrice = lastPrice.Mul(decimal.NewFromInt(1).Add(rate.Div(decimal.NewFromInt(100)).Truncate(4))).Truncate(int32(tradeSet.PriceDigit))
|
newPrice := lastPrice.Mul(decimal.NewFromInt(1).Add(rate.Div(decimal.NewFromInt(100)).Truncate(4))).Truncate(int32(tradeSet.PriceDigit))
|
||||||
buyPrice := utility.StrToDecimal(mainOrder.BuyPrice)
|
buyPrice := utility.StrToDecimal(mainOrder.BuyPrice)
|
||||||
totalNum := buyPrice.Div(newPrice).Truncate(int32(tradeSet.AmountDigit))
|
totalNum := buyPrice.Div(newPrice).Truncate(int32(tradeSet.AmountDigit))
|
||||||
|
|
||||||
mainOrder.SignPrice = lastPrice.String()
|
mainOrder.SignPrice = lastPrice.String()
|
||||||
mainOrder.Price = newPrice.String()
|
mainOrder.Price = newPrice.String()
|
||||||
mainOrder.Num = totalNum.String()
|
mainOrder.Num = totalNum.String()
|
||||||
remainQuantity := totalNum
|
|
||||||
var totalLossAmount decimal.Decimal
|
|
||||||
prePrice := lastPrice
|
|
||||||
|
|
||||||
for index := range mainOrder.Childs {
|
for index := range mainOrder.Childs {
|
||||||
|
//主单止盈
|
||||||
|
if mainOrder.Child[index].OrderType == 1 {
|
||||||
var ext models.LinePreOrderExt
|
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 {
|
for _, v := range exts {
|
||||||
if v.OrderId == extOrderId {
|
if v.OrderId == mainOrder.Child[index].Id {
|
||||||
ext = v
|
ext = v
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ext.Id <= 0 {
|
|
||||||
logger.Errorf("子订单ext不存在 id:%d", mainOrder.Childs[index].Id)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
//主单止盈、止损
|
if ext.Id > 0 {
|
||||||
if mainOrder.Childs[index].Pid == mainOrder.Childs[index].MainId && (mainOrder.Childs[index].OrderType == 1 || mainOrder.Childs[index].OrderType == 2) {
|
|
||||||
var percent decimal.Decimal
|
var percent decimal.Decimal
|
||||||
|
//多
|
||||||
switch {
|
if mainOrder.Site == "BUY" {
|
||||||
// 加价
|
|
||||||
case mainOrder.Childs[index].OrderType == 1 && mainOrder.Site == "BUY", mainOrder.Childs[index].OrderType == 2 && mainOrder.Site == "SELL":
|
|
||||||
percent = decimal.NewFromInt(100).Add(ext.TakeProfitRatio)
|
percent = decimal.NewFromInt(100).Add(ext.TakeProfitRatio)
|
||||||
//减价
|
} else {
|
||||||
case mainOrder.Childs[index].OrderType == 2 && mainOrder.Site == "BUY", mainOrder.Childs[index].OrderType == 1 && mainOrder.Site == "SELL":
|
|
||||||
percent = decimal.NewFromInt(100).Sub(ext.StopLossRatio)
|
percent = decimal.NewFromInt(100).Sub(ext.StopLossRatio)
|
||||||
}
|
}
|
||||||
|
|
||||||
childPrice := lastPrice.Mul(percent.Div(decimal.NewFromInt(100).Truncate(4))).Truncate(int32(tradeSet.PriceDigit))
|
childPrice := lastPrice.Mul(percent.Div(decimal.NewFromInt(100).Truncate(4))).Truncate(int32(tradeSet.PriceDigit))
|
||||||
mainOrder.Childs[index].Price = childPrice.String()
|
mainOrder.Child[index].Price = childPrice.String()
|
||||||
mainOrder.Childs[index].Num = totalNum.String()
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
lastNum := remainQuantity
|
//todo 重新计算
|
||||||
|
|
||||||
//过期时间
|
|
||||||
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,26 +0,0 @@
|
|||||||
package binanceservice
|
|
||||||
|
|
||||||
import (
|
|
||||||
"go-admin/common/global"
|
|
||||||
"go-admin/common/helper"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/go-admin-team/go-admin-core/sdk"
|
|
||||||
"gorm.io/driver/mysql"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 测试策略 触发单
|
|
||||||
func TestTriggerOrder(t *testing.T) {
|
|
||||||
service := BinanceStrategyOrderService{}
|
|
||||||
dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
|
||||||
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
|
||||||
helper.InitDefaultRedis("127.0.0.1:6379", "", 2)
|
|
||||||
helper.InitLockRedisConn("127.0.0.1:6379", "", "2")
|
|
||||||
sdk.Runtime.SetDb("default", db)
|
|
||||||
|
|
||||||
service.Orm = db
|
|
||||||
service.Debug = true
|
|
||||||
|
|
||||||
service.TriggerStrategyOrder(global.EXCHANGE_BINANCE)
|
|
||||||
}
|
|
||||||
@ -1,457 +0,0 @@
|
|||||||
package binanceservice
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-admin-team/go-admin-core/logger"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TransactionManager 事务管理器
|
|
||||||
type TransactionManager struct {
|
|
||||||
db *gorm.DB
|
|
||||||
metrics *MetricsCollector
|
|
||||||
config *OptimizedConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
// TransactionConfig 事务配置
|
|
||||||
type TransactionConfig struct {
|
|
||||||
Timeout time.Duration `json:"timeout"`
|
|
||||||
RetryAttempts int `json:"retry_attempts"`
|
|
||||||
RetryDelay time.Duration `json:"retry_delay"`
|
|
||||||
IsolationLevel string `json:"isolation_level"`
|
|
||||||
ReadOnly bool `json:"read_only"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TransactionContext 事务上下文
|
|
||||||
type TransactionContext struct {
|
|
||||||
Tx *gorm.DB
|
|
||||||
StartTime time.Time
|
|
||||||
Config TransactionConfig
|
|
||||||
Context context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
// TransactionResult 事务结果
|
|
||||||
type TransactionResult struct {
|
|
||||||
Success bool `json:"success"`
|
|
||||||
Duration time.Duration `json:"duration"`
|
|
||||||
Error error `json:"error"`
|
|
||||||
Retries int `json:"retries"`
|
|
||||||
Timestamp time.Time `json:"timestamp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTransactionManager 创建事务管理器
|
|
||||||
func NewTransactionManager(db *gorm.DB, metrics *MetricsCollector, config *OptimizedConfig) *TransactionManager {
|
|
||||||
return &TransactionManager{
|
|
||||||
db: db,
|
|
||||||
metrics: metrics,
|
|
||||||
config: config,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithTransaction 在事务中执行操作
|
|
||||||
func (tm *TransactionManager) WithTransaction(ctx context.Context, config TransactionConfig, operation func(*TransactionContext) error) *TransactionResult {
|
|
||||||
startTime := time.Now()
|
|
||||||
result := &TransactionResult{
|
|
||||||
Timestamp: startTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置默认配置
|
|
||||||
if config.Timeout == 0 {
|
|
||||||
config.Timeout = 30 * time.Second // 默认事务超时时间
|
|
||||||
}
|
|
||||||
if config.RetryAttempts == 0 {
|
|
||||||
config.RetryAttempts = tm.config.RetryConfig.MaxRetries
|
|
||||||
}
|
|
||||||
if config.RetryDelay == 0 {
|
|
||||||
config.RetryDelay = tm.config.RetryConfig.RetryDelay
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行事务(带重试)
|
|
||||||
for attempt := 1; attempt <= config.RetryAttempts; attempt++ {
|
|
||||||
err := tm.executeTransaction(ctx, config, operation)
|
|
||||||
if err == nil {
|
|
||||||
result.Success = true
|
|
||||||
result.Duration = time.Since(startTime)
|
|
||||||
result.Retries = attempt - 1
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Error = err
|
|
||||||
result.Retries = attempt
|
|
||||||
|
|
||||||
// 检查是否可重试
|
|
||||||
if !tm.isRetryableError(err) || attempt == config.RetryAttempts {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等待重试
|
|
||||||
logger.Warnf("事务执行失败,第 %d 次重试: %v", attempt, err)
|
|
||||||
time.Sleep(config.RetryDelay * time.Duration(attempt))
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Duration = time.Since(startTime)
|
|
||||||
|
|
||||||
// 记录指标
|
|
||||||
if tm.metrics != nil {
|
|
||||||
tm.metrics.RecordDbOperation(true, result.Duration, result.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// executeTransaction 执行单次事务
|
|
||||||
func (tm *TransactionManager) executeTransaction(ctx context.Context, config TransactionConfig, operation func(*TransactionContext) error) error {
|
|
||||||
// 设置事务超时
|
|
||||||
txCtx := ctx
|
|
||||||
if config.Timeout > 0 {
|
|
||||||
var cancel context.CancelFunc
|
|
||||||
txCtx, cancel = context.WithTimeout(ctx, config.Timeout)
|
|
||||||
defer cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 开始事务
|
|
||||||
tx := tm.db.WithContext(txCtx).Begin()
|
|
||||||
if tx.Error != nil {
|
|
||||||
return fmt.Errorf("开始事务失败: %w", tx.Error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置事务隔离级别
|
|
||||||
if config.IsolationLevel != "" {
|
|
||||||
if err := tx.Exec(fmt.Sprintf("SET TRANSACTION ISOLATION LEVEL %s", config.IsolationLevel)).Error; err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return fmt.Errorf("设置事务隔离级别失败: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置只读模式
|
|
||||||
if config.ReadOnly {
|
|
||||||
if err := tx.Exec("SET TRANSACTION READ ONLY").Error; err != nil {
|
|
||||||
tx.Rollback()
|
|
||||||
return fmt.Errorf("设置只读事务失败: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 创建事务上下文
|
|
||||||
txContext := &TransactionContext{
|
|
||||||
Tx: tx,
|
|
||||||
StartTime: time.Now(),
|
|
||||||
Config: config,
|
|
||||||
Context: txCtx,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行操作
|
|
||||||
err := operation(txContext)
|
|
||||||
if err != nil {
|
|
||||||
// 回滚事务
|
|
||||||
if rollbackErr := tx.Rollback().Error; rollbackErr != nil {
|
|
||||||
logger.Errorf("事务回滚失败: %v", rollbackErr)
|
|
||||||
return fmt.Errorf("操作失败且回滚失败: %w, 回滚错误: %v", err, rollbackErr)
|
|
||||||
}
|
|
||||||
return fmt.Errorf("事务操作失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交事务
|
|
||||||
if commitErr := tx.Commit().Error; commitErr != nil {
|
|
||||||
return fmt.Errorf("事务提交失败: %w", commitErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithOptimisticLock 使用乐观锁执行操作
|
|
||||||
func (tm *TransactionManager) WithOptimisticLock(ctx context.Context, config TransactionConfig, operation func(*TransactionContext) error) *TransactionResult {
|
|
||||||
return tm.WithTransaction(ctx, config, func(txCtx *TransactionContext) error {
|
|
||||||
// 在乐观锁模式下,我们需要在操作中检查版本号
|
|
||||||
return operation(txCtx)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveWithOptimisticLock 使用乐观锁保存数据
|
|
||||||
func (tm *TransactionManager) SaveWithOptimisticLock(ctx context.Context, model interface{}, condition string, args ...interface{}) error {
|
|
||||||
config := TransactionConfig{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
RetryAttempts: 3,
|
|
||||||
RetryDelay: 100 * time.Millisecond,
|
|
||||||
}
|
|
||||||
|
|
||||||
result := tm.WithTransaction(ctx, config, func(txCtx *TransactionContext) error {
|
|
||||||
// 使用 WHERE 条件和版本号进行更新
|
|
||||||
result := txCtx.Tx.Where(condition, args...).Save(model)
|
|
||||||
if result.Error != nil {
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否有行被更新
|
|
||||||
if result.RowsAffected == 0 {
|
|
||||||
return fmt.Errorf("乐观锁冲突: 数据已被其他进程修改")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateWithOptimisticLock 使用乐观锁更新数据
|
|
||||||
func (tm *TransactionManager) UpdateWithOptimisticLock(ctx context.Context, model interface{}, updates map[string]interface{}, condition string, args ...interface{}) error {
|
|
||||||
config := TransactionConfig{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
RetryAttempts: 3,
|
|
||||||
RetryDelay: 100 * time.Millisecond,
|
|
||||||
}
|
|
||||||
|
|
||||||
result := tm.WithTransaction(ctx, config, func(txCtx *TransactionContext) error {
|
|
||||||
// 增加版本号
|
|
||||||
if _, hasVersion := updates["version"]; !hasVersion {
|
|
||||||
updates["version"] = gorm.Expr("version + 1")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 执行更新
|
|
||||||
result := txCtx.Tx.Model(model).Where(condition, args...).Updates(updates)
|
|
||||||
if result.Error != nil {
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否有行被更新
|
|
||||||
if result.RowsAffected == 0 {
|
|
||||||
return fmt.Errorf("乐观锁冲突: 数据已被其他进程修改")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// BatchOperation 批量操作
|
|
||||||
func (tm *TransactionManager) BatchOperation(ctx context.Context, config TransactionConfig, operations []func(*TransactionContext) error) *TransactionResult {
|
|
||||||
return tm.WithTransaction(ctx, config, func(txCtx *TransactionContext) error {
|
|
||||||
for i, operation := range operations {
|
|
||||||
if err := operation(txCtx); err != nil {
|
|
||||||
return fmt.Errorf("批量操作第 %d 步失败: %w", i+1, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateSavepoint 创建保存点
|
|
||||||
func (tm *TransactionManager) CreateSavepoint(txCtx *TransactionContext, name string) error {
|
|
||||||
return txCtx.Tx.Exec(fmt.Sprintf("SAVEPOINT %s", name)).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// RollbackToSavepoint 回滚到保存点
|
|
||||||
func (tm *TransactionManager) RollbackToSavepoint(txCtx *TransactionContext, name string) error {
|
|
||||||
return txCtx.Tx.Exec(fmt.Sprintf("ROLLBACK TO SAVEPOINT %s", name)).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReleaseSavepoint 释放保存点
|
|
||||||
func (tm *TransactionManager) ReleaseSavepoint(txCtx *TransactionContext, name string) error {
|
|
||||||
return txCtx.Tx.Exec(fmt.Sprintf("RELEASE SAVEPOINT %s", name)).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// isRetryableError 判断错误是否可重试
|
|
||||||
func (tm *TransactionManager) isRetryableError(err error) bool {
|
|
||||||
if err == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
errorMsg := err.Error()
|
|
||||||
|
|
||||||
// 可重试的错误类型
|
|
||||||
retryableErrors := []string{
|
|
||||||
"deadlock",
|
|
||||||
"lock wait timeout",
|
|
||||||
"connection",
|
|
||||||
"timeout",
|
|
||||||
"temporary",
|
|
||||||
"乐观锁冲突",
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, retryableError := range retryableErrors {
|
|
||||||
if strings.Contains(errorMsg, retryableError) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTransactionStats 获取事务统计信息
|
|
||||||
func (tm *TransactionManager) GetTransactionStats() map[string]interface{} {
|
|
||||||
stats := make(map[string]interface{})
|
|
||||||
|
|
||||||
// 从指标收集器获取数据库相关统计
|
|
||||||
if tm.metrics != nil {
|
|
||||||
stats["total_transactions"] = tm.metrics.PerformanceMetrics.DbTransactions
|
|
||||||
stats["total_queries"] = tm.metrics.PerformanceMetrics.DbQueries
|
|
||||||
stats["total_errors"] = tm.metrics.PerformanceMetrics.DbErrors
|
|
||||||
stats["avg_query_time"] = tm.metrics.PerformanceMetrics.DbQueryTime
|
|
||||||
}
|
|
||||||
|
|
||||||
return stats
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预定义的事务配置
|
|
||||||
|
|
||||||
// GetDefaultTransactionConfig 获取默认事务配置
|
|
||||||
func GetDefaultTransactionConfig() TransactionConfig {
|
|
||||||
return TransactionConfig{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
RetryAttempts: 3,
|
|
||||||
RetryDelay: 100 * time.Millisecond,
|
|
||||||
IsolationLevel: "READ COMMITTED",
|
|
||||||
ReadOnly: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetReadOnlyTransactionConfig 获取只读事务配置
|
|
||||||
func GetReadOnlyTransactionConfig() TransactionConfig {
|
|
||||||
return TransactionConfig{
|
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
RetryAttempts: 1,
|
|
||||||
RetryDelay: 0,
|
|
||||||
IsolationLevel: "READ COMMITTED",
|
|
||||||
ReadOnly: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLongRunningTransactionConfig 获取长时间运行事务配置
|
|
||||||
func GetLongRunningTransactionConfig() TransactionConfig {
|
|
||||||
return TransactionConfig{
|
|
||||||
Timeout: 300 * time.Second, // 5分钟
|
|
||||||
RetryAttempts: 5,
|
|
||||||
RetryDelay: 500 * time.Millisecond,
|
|
||||||
IsolationLevel: "REPEATABLE READ",
|
|
||||||
ReadOnly: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCriticalTransactionConfig 获取关键事务配置
|
|
||||||
func GetCriticalTransactionConfig() TransactionConfig {
|
|
||||||
return TransactionConfig{
|
|
||||||
Timeout: 60 * time.Second,
|
|
||||||
RetryAttempts: 5,
|
|
||||||
RetryDelay: 200 * time.Millisecond,
|
|
||||||
IsolationLevel: "SERIALIZABLE",
|
|
||||||
ReadOnly: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全局事务管理器实例
|
|
||||||
var GlobalTransactionManager *TransactionManager
|
|
||||||
|
|
||||||
// InitTransactionManager 初始化事务管理器
|
|
||||||
func InitTransactionManager(db *gorm.DB, metrics *MetricsCollector, config *OptimizedConfig) {
|
|
||||||
GlobalTransactionManager = NewTransactionManager(db, metrics, config)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetTransactionManager 获取全局事务管理器
|
|
||||||
func GetTransactionManager() *TransactionManager {
|
|
||||||
return GlobalTransactionManager
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithDefaultTransaction 使用默认配置执行事务
|
|
||||||
func WithDefaultTransaction(ctx context.Context, operation func(*TransactionContext) error) error {
|
|
||||||
if GlobalTransactionManager == nil {
|
|
||||||
return fmt.Errorf("事务管理器未初始化")
|
|
||||||
}
|
|
||||||
|
|
||||||
config := GetDefaultTransactionConfig()
|
|
||||||
result := GlobalTransactionManager.WithTransaction(ctx, config, operation)
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithReadOnlyTransaction 使用只读配置执行事务
|
|
||||||
func WithReadOnlyTransaction(ctx context.Context, operation func(*TransactionContext) error) error {
|
|
||||||
if GlobalTransactionManager == nil {
|
|
||||||
return fmt.Errorf("事务管理器未初始化")
|
|
||||||
}
|
|
||||||
|
|
||||||
config := GetReadOnlyTransactionConfig()
|
|
||||||
result := GlobalTransactionManager.WithTransaction(ctx, config, operation)
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithCriticalTransaction 使用关键事务配置执行事务
|
|
||||||
func WithCriticalTransaction(ctx context.Context, operation func(*TransactionContext) error) error {
|
|
||||||
if GlobalTransactionManager == nil {
|
|
||||||
return fmt.Errorf("事务管理器未初始化")
|
|
||||||
}
|
|
||||||
|
|
||||||
config := GetCriticalTransactionConfig()
|
|
||||||
result := GlobalTransactionManager.WithTransaction(ctx, config, operation)
|
|
||||||
return result.Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveWithOptimisticLockGlobal 全局乐观锁保存
|
|
||||||
func SaveWithOptimisticLockGlobal(ctx context.Context, model interface{}, condition string, args ...interface{}) error {
|
|
||||||
if GlobalTransactionManager == nil {
|
|
||||||
return fmt.Errorf("事务管理器未初始化")
|
|
||||||
}
|
|
||||||
|
|
||||||
return GlobalTransactionManager.SaveWithOptimisticLock(ctx, model, condition, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateWithOptimisticLockGlobal 全局乐观锁更新
|
|
||||||
func UpdateWithOptimisticLockGlobal(ctx context.Context, model interface{}, updates map[string]interface{}, condition string, args ...interface{}) error {
|
|
||||||
if GlobalTransactionManager == nil {
|
|
||||||
return fmt.Errorf("事务管理器未初始化")
|
|
||||||
}
|
|
||||||
|
|
||||||
return GlobalTransactionManager.UpdateWithOptimisticLock(ctx, model, updates, condition, args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TransactionHelper 事务辅助函数集合
|
|
||||||
type TransactionHelper struct {
|
|
||||||
tm *TransactionManager
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTransactionHelper 创建事务辅助器
|
|
||||||
func NewTransactionHelper(tm *TransactionManager) *TransactionHelper {
|
|
||||||
return &TransactionHelper{tm: tm}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SavePosition 保存持仓(带乐观锁)
|
|
||||||
func (th *TransactionHelper) SavePosition(ctx context.Context, position interface{}, userID, symbol string, version int) error {
|
|
||||||
condition := "user_id = ? AND symbol = ? AND version = ?"
|
|
||||||
return th.tm.SaveWithOptimisticLock(ctx, position, condition, userID, symbol, version)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SaveOrder 保存订单(带乐观锁)
|
|
||||||
func (th *TransactionHelper) SaveOrder(ctx context.Context, order interface{}, orderID string, version int) error {
|
|
||||||
condition := "order_id = ? AND version = ?"
|
|
||||||
return th.tm.SaveWithOptimisticLock(ctx, order, condition, orderID, version)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateOrderStatus 更新订单状态(带乐观锁)
|
|
||||||
func (th *TransactionHelper) UpdateOrderStatus(ctx context.Context, orderID, newStatus string, version int) error {
|
|
||||||
updates := map[string]interface{}{
|
|
||||||
"status": newStatus,
|
|
||||||
"updated_at": time.Now(),
|
|
||||||
}
|
|
||||||
condition := "order_id = ? AND version = ?"
|
|
||||||
return th.tm.UpdateWithOptimisticLock(ctx, nil, updates, condition, orderID, version)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdatePositionQuantity 更新持仓数量(带乐观锁)
|
|
||||||
func (th *TransactionHelper) UpdatePositionQuantity(ctx context.Context, userID, symbol string, quantity float64, version int) error {
|
|
||||||
updates := map[string]interface{}{
|
|
||||||
"quantity": quantity,
|
|
||||||
"updated_at": time.Now(),
|
|
||||||
}
|
|
||||||
condition := "user_id = ? AND symbol = ? AND version = ?"
|
|
||||||
return th.tm.UpdateWithOptimisticLock(ctx, nil, updates, condition, userID, symbol, version)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetGlobalTransactionHelper 获取全局事务辅助器
|
|
||||||
func GetGlobalTransactionHelper() *TransactionHelper {
|
|
||||||
if GlobalTransactionManager == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return NewTransactionHelper(GlobalTransactionManager)
|
|
||||||
}
|
|
||||||
447
services/bitgetservice/bitget_client.go
Normal file
447
services/bitgetservice/bitget_client.go
Normal file
@ -0,0 +1,447 @@
|
|||||||
|
package bitgetservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"go-admin/common/global"
|
||||||
|
"go-admin/common/helper"
|
||||||
|
"go-admin/common/interfaces"
|
||||||
|
"go-admin/models"
|
||||||
|
"go-admin/pkg/utility"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BitgetClient Bitget交易所客户端实现
|
||||||
|
type BitgetClient struct {
|
||||||
|
httpClient *helper.BitgetClient
|
||||||
|
marketClient *BitgetMarketClient // 公开行情客户端
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBitgetClient 创建新的Bitget客户端实例
|
||||||
|
func NewBitgetClient(apiKey, apiSecret, passphrase, proxyUrl string) (*BitgetClient, error) {
|
||||||
|
httpClient, err := helper.NewBitgetClient(apiKey, apiSecret, passphrase, proxyUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &BitgetClient{
|
||||||
|
httpClient: httpClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化公开行情客户端
|
||||||
|
marketConfig := DefaultMarketConfig()
|
||||||
|
client.marketClient = NewBitgetMarketClient(marketConfig)
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExchangeName 获取交易所名称
|
||||||
|
func (c *BitgetClient) GetExchangeName() string {
|
||||||
|
return global.EXCHANGE_BITGET
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetExchangeInfo 获取交易所信息
|
||||||
|
func (c *BitgetClient) GetExchangeInfo() (interfaces.ExchangeInfoResponse, error) {
|
||||||
|
restApi := NewBitgetRestApi()
|
||||||
|
|
||||||
|
datas, err := restApi.GetSpotSymbols()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return interfaces.ExchangeInfoResponse{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
symbols := make([]interfaces.SymbolInfo, 0, len(datas))
|
||||||
|
for _, item := range datas {
|
||||||
|
symbols = append(symbols, interfaces.SymbolInfo{
|
||||||
|
Symbol: item.Symbol,
|
||||||
|
BaseAsset: item.BaseCoin,
|
||||||
|
QuoteAsset: item.QuoteCoin,
|
||||||
|
Status: item.Status,
|
||||||
|
MinQty: item.MinQty,
|
||||||
|
MaxQty: item.MaxQty,
|
||||||
|
StepSize: strconv.Itoa(item.QuantityDigit),
|
||||||
|
TickSize: strconv.Itoa(item.PriceDigit),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return interfaces.ExchangeInfoResponse{Symbols: symbols}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSpotTickers 获取所有现货行情
|
||||||
|
func (c *BitgetClient) GetSpotTickers() ([]models.TradeSet, error) {
|
||||||
|
params := map[string]interface{}{}
|
||||||
|
resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/market/tickers", "GET", params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取Bitget现货行情列表失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var response struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data []struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
High24h string `json:"high24h"`
|
||||||
|
Low24h string `json:"low24h"`
|
||||||
|
Close string `json:"close"`
|
||||||
|
Open string `json:"open"`
|
||||||
|
Volume24h string `json:"volume24h"`
|
||||||
|
QuoteVol string `json:"quoteVol"`
|
||||||
|
Change24h string `json:"change24h"`
|
||||||
|
Change string `json:"change"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sonic.Unmarshal(resp, &response); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析Bitget行情列表失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Code != "00000" {
|
||||||
|
return nil, fmt.Errorf("bitget API错误: %s", response.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
tradeSets := make([]models.TradeSet, 0, len(response.Data))
|
||||||
|
for _, item := range response.Data {
|
||||||
|
// 解析交易对,获取基础币种和计价币种
|
||||||
|
var coin, currency string
|
||||||
|
if strings.HasSuffix(item.Symbol, "USDT") {
|
||||||
|
coin = strings.TrimSuffix(item.Symbol, "USDT")
|
||||||
|
currency = "USDT"
|
||||||
|
} else if strings.HasSuffix(item.Symbol, "BTC") {
|
||||||
|
coin = strings.TrimSuffix(item.Symbol, "BTC")
|
||||||
|
currency = "BTC"
|
||||||
|
} else if strings.HasSuffix(item.Symbol, "ETH") {
|
||||||
|
coin = strings.TrimSuffix(item.Symbol, "ETH")
|
||||||
|
currency = "ETH"
|
||||||
|
} else {
|
||||||
|
continue // 跳过不支持的交易对
|
||||||
|
}
|
||||||
|
|
||||||
|
tradeSet := models.TradeSet{
|
||||||
|
Coin: coin,
|
||||||
|
Currency: currency,
|
||||||
|
LastPrice: item.Close,
|
||||||
|
HighPrice: item.High24h,
|
||||||
|
LowPrice: item.Low24h,
|
||||||
|
Volume: item.Volume24h,
|
||||||
|
QuoteVolume: item.QuoteVol,
|
||||||
|
OpenPrice: utility.StringAsFloat(item.Open),
|
||||||
|
PriceChange: utility.StringAsFloat(item.Change),
|
||||||
|
}
|
||||||
|
|
||||||
|
tradeSets = append(tradeSets, tradeSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tradeSets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSpotTicker24h 获取现货24小时行情
|
||||||
|
func (c *BitgetClient) GetSpotTicker24h(symbol string) (models.Ticker24, error) {
|
||||||
|
params := map[string]interface{}{
|
||||||
|
"symbol": symbol,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/market/ticker", "GET", params)
|
||||||
|
if err != nil {
|
||||||
|
return models.Ticker24{}, fmt.Errorf("获取Bitget现货24h行情失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var response struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
High24h string `json:"high24h"`
|
||||||
|
Low24h string `json:"low24h"`
|
||||||
|
Close string `json:"close"`
|
||||||
|
Open string `json:"open"`
|
||||||
|
Volume24h string `json:"volume24h"`
|
||||||
|
QuoteVol string `json:"quoteVol"`
|
||||||
|
Change24h string `json:"change24h"`
|
||||||
|
Change string `json:"change"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sonic.Unmarshal(resp, &response); err != nil {
|
||||||
|
return models.Ticker24{}, fmt.Errorf("解析Bitget 24h行情失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Code != "00000" {
|
||||||
|
return models.Ticker24{}, fmt.Errorf("Bitget API错误: %s", response.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
ticker := models.Ticker24{
|
||||||
|
HighPrice: response.Data.High24h,
|
||||||
|
LowPrice: response.Data.Low24h,
|
||||||
|
LastPrice: response.Data.Close,
|
||||||
|
OpenPrice: response.Data.Open,
|
||||||
|
Volume: response.Data.Volume24h,
|
||||||
|
QuoteVolume: response.Data.QuoteVol,
|
||||||
|
ChangePercent: response.Data.Change,
|
||||||
|
}
|
||||||
|
|
||||||
|
return ticker, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PlaceSpotOrder 下现货订单
|
||||||
|
func (c *BitgetClient) PlaceSpotOrder(params interfaces.SpotOrderParams) (interfaces.SpotOrderResponse, error) {
|
||||||
|
orderParams := map[string]interface{}{
|
||||||
|
"symbol": params.Symbol,
|
||||||
|
"side": strings.ToLower(params.Side),
|
||||||
|
"orderType": strings.ToLower(params.Type),
|
||||||
|
"quantity": params.Quantity.String(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Type == "LIMIT" {
|
||||||
|
orderParams["price"] = params.Price.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.TimeInForce != "" {
|
||||||
|
orderParams["timeInForce"] = params.TimeInForce
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.NewClientOrderId != "" {
|
||||||
|
orderParams["clientOrderId"] = params.NewClientOrderId
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/trade/orders", "POST", orderParams)
|
||||||
|
if err != nil {
|
||||||
|
return interfaces.SpotOrderResponse{}, fmt.Errorf("Bitget现货下单失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var response struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data struct {
|
||||||
|
OrderId string `json:"orderId"`
|
||||||
|
ClientOrderId string `json:"clientOrderId"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sonic.Unmarshal(resp, &response); err != nil {
|
||||||
|
return interfaces.SpotOrderResponse{}, fmt.Errorf("解析Bitget下单响应失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Code != "00000" {
|
||||||
|
return interfaces.SpotOrderResponse{}, fmt.Errorf("Bitget下单API错误: %s", response.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
orderId, _ := strconv.ParseInt(response.Data.OrderId, 10, 64)
|
||||||
|
|
||||||
|
return interfaces.SpotOrderResponse{
|
||||||
|
Symbol: params.Symbol,
|
||||||
|
OrderId: orderId,
|
||||||
|
ClientOrderId: response.Data.ClientOrderId,
|
||||||
|
Price: params.Price.String(),
|
||||||
|
OrigQty: params.Quantity.String(),
|
||||||
|
Side: params.Side,
|
||||||
|
Type: params.Type,
|
||||||
|
Status: "NEW",
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CancelSpotOrder 取消现货订单
|
||||||
|
func (c *BitgetClient) CancelSpotOrder(symbol, orderID string) error {
|
||||||
|
params := map[string]interface{}{
|
||||||
|
"symbol": symbol,
|
||||||
|
"orderId": orderID,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/trade/cancel-order", "POST", params)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Bitget取消现货订单失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var response struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sonic.Unmarshal(resp, &response); err != nil {
|
||||||
|
return fmt.Errorf("解析Bitget取消订单响应失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.Code != "00000" {
|
||||||
|
return fmt.Errorf("Bitget取消订单API错误: %s", response.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSpotOrder 获取现货订单详情
|
||||||
|
func (c *BitgetClient) GetSpotOrder(symbol, orderID string) (interfaces.SpotOrderResponse, error) {
|
||||||
|
// 实现订单查询逻辑
|
||||||
|
return interfaces.SpotOrderResponse{}, fmt.Errorf("GetSpotOrder not implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSpotOrders 获取现货订单列表
|
||||||
|
func (c *BitgetClient) GetSpotOrders(symbol string) ([]interfaces.SpotOrderResponse, error) {
|
||||||
|
params := map[string]interface{}{
|
||||||
|
"symbol": symbol,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err := c.httpClient.SendSpotRequestAuth("/api/v2/spot/trade/open-orders", "GET", params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取Bitget现货订单列表失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里需要根据实际的Bitget API响应格式来解析
|
||||||
|
return []interfaces.SpotOrderResponse{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合约相关接口实现(暂时返回错误)
|
||||||
|
func (c *BitgetClient) GetFuturesTicker24h(symbol string) (models.Ticker24, error) {
|
||||||
|
return models.Ticker24{}, fmt.Errorf("futures not implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BitgetClient) GetFuturesTickers() ([]models.TradeSet, error) {
|
||||||
|
return nil, fmt.Errorf("futures not implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BitgetClient) PlaceFuturesOrder(params interfaces.FuturesOrderParams) (interfaces.FuturesOrderResponse, error) {
|
||||||
|
return interfaces.FuturesOrderResponse{}, fmt.Errorf("futures not implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BitgetClient) CancelFuturesOrder(symbol, orderID string) error {
|
||||||
|
return fmt.Errorf("futures not implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BitgetClient) GetFuturesOrder(symbol, orderID string) (interfaces.FuturesOrderResponse, error) {
|
||||||
|
return interfaces.FuturesOrderResponse{}, fmt.Errorf("futures not implemented yet")
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocket相关接口实现(已重构为使用公开行情客户端)
|
||||||
|
// 注意:这些方法已被重构,现在使用新的公开行情客户端
|
||||||
|
// 如需私有数据订阅(如订单、持仓),请使用 BitgetWebSocketManager
|
||||||
|
|
||||||
|
// 旧的订阅方法(保留兼容性,但建议使用新方法)
|
||||||
|
func (c *BitgetClient) SubscribeSpotTickerLegacy(symbols []string, callback func(models.Ticker24, string, string)) error {
|
||||||
|
return fmt.Errorf("已废弃,请使用 SubscribeSpotTicker(symbol, listener) 方法")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BitgetClient) SubscribeFuturesTickerLegacy(symbols []string, callback func(models.Ticker24, string, string)) error {
|
||||||
|
return fmt.Errorf("已废弃,请使用 SubscribeFuturesTicker(symbol, listener) 方法")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BitgetClient) SubscribeSpotDepthLegacy(symbols []string, callback func(models.DepthBin, string, string)) error {
|
||||||
|
return fmt.Errorf("已废弃,请使用 SubscribeSpotDepth(symbol, listener) 方法")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BitgetClient) SubscribeSpotTradesLegacy(symbols []string, callback func(models.NewDealPush, string, string)) error {
|
||||||
|
return fmt.Errorf("已废弃,请使用 SubscribeSpotTrades(symbol, listener) 方法")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BitgetClient) SubscribeKlineLegacy(symbols []string, interval string, callback func(models.Kline, int, string, string)) error {
|
||||||
|
return fmt.Errorf("已废弃,请使用 SubscribeKline(symbol, interval, listener) 方法")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BitgetClient) Close() error {
|
||||||
|
// 关闭HTTP客户端等清理工作
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMarketProxy 设置市场数据代理
|
||||||
|
func (c *BitgetClient) SetMarketProxy(proxyType, proxyAddress string) {
|
||||||
|
if c.marketClient != nil {
|
||||||
|
c.marketClient.SetProxy(proxyType, proxyAddress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMarketListeners 设置市场数据监听器
|
||||||
|
func (c *BitgetClient) SetMarketListeners(msgListener, errorListener OnReceive) {
|
||||||
|
if c.marketClient != nil {
|
||||||
|
c.marketClient.SetListeners(msgListener, errorListener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConnectMarketData 连接市场数据WebSocket
|
||||||
|
func (c *BitgetClient) ConnectMarketData() error {
|
||||||
|
if c.marketClient == nil {
|
||||||
|
return errors.New("市场数据客户端未初始化")
|
||||||
|
}
|
||||||
|
return c.marketClient.Connect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeMarketData 订阅市场数据(公开行情)
|
||||||
|
func (c *BitgetClient) SubscribeMarketData(subscribeReqs []SubscribeReq, listener OnReceive) error {
|
||||||
|
if c.marketClient == nil {
|
||||||
|
return errors.New("市场数据客户端未初始化")
|
||||||
|
}
|
||||||
|
return c.marketClient.Subscribe(subscribeReqs, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsubscribeMarketData 取消订阅市场数据
|
||||||
|
func (c *BitgetClient) UnsubscribeMarketData(subscribeReqs []SubscribeReq) error {
|
||||||
|
if c.marketClient == nil {
|
||||||
|
return errors.New("市场数据客户端未初始化")
|
||||||
|
}
|
||||||
|
return c.marketClient.Unsubscribe(subscribeReqs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsMarketDataConnected 检查市场数据连接状态
|
||||||
|
func (c *BitgetClient) IsMarketDataConnected() bool {
|
||||||
|
if c.marketClient == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return c.marketClient.IsConnected()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseMarketData 关闭市场数据连接
|
||||||
|
func (c *BitgetClient) CloseMarketData() error {
|
||||||
|
if c.marketClient == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.marketClient.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeSpotTicker 订阅现货行情(使用公开行情客户端)
|
||||||
|
func (c *BitgetClient) SubscribeSpotTicker(symbol string, listener OnReceive) error {
|
||||||
|
subscribeReq := SubscribeReq{
|
||||||
|
InstType: "SPOT",
|
||||||
|
Channel: "ticker",
|
||||||
|
InstId: symbol,
|
||||||
|
}
|
||||||
|
return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeFuturesTicker 订阅合约行情(使用公开行情客户端)
|
||||||
|
func (c *BitgetClient) SubscribeFuturesTicker(symbol string, listener OnReceive) error {
|
||||||
|
subscribeReq := SubscribeReq{
|
||||||
|
InstType: "UMCBL",
|
||||||
|
Channel: "ticker",
|
||||||
|
InstId: symbol,
|
||||||
|
}
|
||||||
|
return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeSpotDepth 订阅现货深度(使用公开行情客户端)
|
||||||
|
func (c *BitgetClient) SubscribeSpotDepth(symbol string, listener OnReceive) error {
|
||||||
|
subscribeReq := SubscribeReq{
|
||||||
|
InstType: "SPOT",
|
||||||
|
Channel: "books",
|
||||||
|
InstId: symbol,
|
||||||
|
}
|
||||||
|
return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeSpotTrades 订阅现货成交(使用公开行情客户端)
|
||||||
|
func (c *BitgetClient) SubscribeSpotTrades(symbol string, listener OnReceive) error {
|
||||||
|
subscribeReq := SubscribeReq{
|
||||||
|
InstType: "SPOT",
|
||||||
|
Channel: "trade",
|
||||||
|
InstId: symbol,
|
||||||
|
}
|
||||||
|
return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubscribeKline 订阅K线数据(使用公开行情客户端)
|
||||||
|
func (c *BitgetClient) SubscribeKline(symbol, interval string, listener OnReceive) error {
|
||||||
|
subscribeReq := SubscribeReq{
|
||||||
|
InstType: "SPOT",
|
||||||
|
Channel: "candle" + interval,
|
||||||
|
InstId: symbol,
|
||||||
|
}
|
||||||
|
return c.SubscribeMarketData([]SubscribeReq{subscribeReq}, listener)
|
||||||
|
}
|
||||||
229
services/bitgetservice/bitget_example.go
Normal file
229
services/bitgetservice/bitget_example.go
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
package bitgetservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
log "github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ExampleBitgetMarketClient 展示如何使用Bitget公开行情客户端
|
||||||
|
func ExampleBitgetMarketClient() {
|
||||||
|
// 1. 创建Bitget客户端(不需要API密钥用于公开数据)
|
||||||
|
client := &BitgetClient{}
|
||||||
|
|
||||||
|
// 初始化公开行情客户端
|
||||||
|
marketConfig := DefaultMarketConfig()
|
||||||
|
client.marketClient = NewBitgetMarketClient(marketConfig)
|
||||||
|
|
||||||
|
// 2. 设置代理(可选)
|
||||||
|
// client.SetMarketProxy("socks5", "127.0.0.1:1080")
|
||||||
|
|
||||||
|
// 3. 设置消息监听器
|
||||||
|
msgListener := func(message string) {
|
||||||
|
log.Info("收到市场数据: %s", message)
|
||||||
|
// 在这里处理市场数据
|
||||||
|
}
|
||||||
|
|
||||||
|
errorListener := func(message string) {
|
||||||
|
log.Error("收到错误消息: %s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.SetMarketListeners(msgListener, errorListener)
|
||||||
|
|
||||||
|
// 4. 连接WebSocket
|
||||||
|
if err := client.ConnectMarketData(); err != nil {
|
||||||
|
log.Error("连接市场数据失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 订阅现货行情
|
||||||
|
tickerListener := func(message string) {
|
||||||
|
log.Info("收到BTC-USDT行情: %s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.SubscribeSpotTicker("BTCUSDT", tickerListener); err != nil {
|
||||||
|
log.Error("订阅现货行情失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. 订阅深度数据
|
||||||
|
depthListener := func(message string) {
|
||||||
|
log.Info("收到BTC-USDT深度: %s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.SubscribeSpotDepth("BTCUSDT", depthListener); err != nil {
|
||||||
|
log.Error("订阅深度数据失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7. 订阅成交数据
|
||||||
|
tradeListener := func(message string) {
|
||||||
|
log.Info("收到BTC-USDT成交: %s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.SubscribeSpotTrades("BTCUSDT", tradeListener); err != nil {
|
||||||
|
log.Error("订阅成交数据失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 订阅K线数据
|
||||||
|
klineListener := func(message string) {
|
||||||
|
log.Info("收到BTC-USDT K线: %s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.SubscribeKline("BTCUSDT", "1m", klineListener); err != nil {
|
||||||
|
log.Error("订阅K线数据失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("所有订阅完成,开始接收数据...")
|
||||||
|
|
||||||
|
// 保持连接(在实际应用中,你可能需要其他方式来保持程序运行)
|
||||||
|
// select {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExampleBitgetPrivateWebSocket 展示如何使用私有WebSocket(订单、持仓等)
|
||||||
|
func ExampleBitgetPrivateWebSocket() {
|
||||||
|
// 私有数据订阅仍然使用 BitgetWebSocketManager
|
||||||
|
// apiKey := "your_api_key"
|
||||||
|
// secretKey := "your_secret_key"
|
||||||
|
// passphrase := "your_passphrase"
|
||||||
|
|
||||||
|
// // 创建私有WebSocket管理器
|
||||||
|
// manager := GetOrCreateBitgetWebSocketManager(apiKey, secretKey, passphrase, "spot")
|
||||||
|
|
||||||
|
// // 启动连接
|
||||||
|
// if err := manager.Start(); err != nil {
|
||||||
|
// log.Error("启动私有WebSocket连接失败: %v", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// log.Info("私有WebSocket连接成功,可以接收订单、持仓等私有数据")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExampleMixedUsage 展示混合使用公开和私有订阅
|
||||||
|
func ExampleMixedUsage() {
|
||||||
|
// apiKey := "your_api_key"
|
||||||
|
// secretKey := "your_secret_key"
|
||||||
|
// passphrase := "your_passphrase"
|
||||||
|
|
||||||
|
// // 1. 创建完整的Bitget客户端
|
||||||
|
// client, err := NewBitgetClient(apiKey, secretKey, passphrase)
|
||||||
|
// if err != nil {
|
||||||
|
// log.Error("创建Bitget客户端失败: %v", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 2. 设置公开行情监听器
|
||||||
|
// client.SetMarketListeners(
|
||||||
|
// func(message string) {
|
||||||
|
// log.Info("公开行情数据: %s", message)
|
||||||
|
// },
|
||||||
|
// func(message string) {
|
||||||
|
// log.Error("公开行情错误: %s", message)
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
|
||||||
|
// // 3. 连接公开行情
|
||||||
|
// if err := client.ConnectMarketData(); err != nil {
|
||||||
|
// log.Error("连接公开行情失败: %v", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 4. 订阅公开行情
|
||||||
|
// if err := client.SubscribeSpotTicker("BTCUSDT", func(message string) {
|
||||||
|
// log.Info("BTC行情: %s", message)
|
||||||
|
// }); err != nil {
|
||||||
|
// log.Error("订阅行情失败: %v", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 5. 同时使用私有WebSocket(如果需要)
|
||||||
|
// privateManager, err := GetOrCreateBitgetWebSocketManager(apiKey, secretKey, passphrase, "spot")
|
||||||
|
// if err != nil {
|
||||||
|
// log.Error("创建私有WebSocket失败: %v", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if err := privateManager.Start(); err != nil {
|
||||||
|
// log.Error("启动私有WebSocket失败: %v", err)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
|
||||||
|
// log.Info("混合使用设置完成:公开行情 + 私有数据")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExampleSubscribeMultipleSymbols 展示如何订阅多个交易对
|
||||||
|
func ExampleSubscribeMultipleSymbols() {
|
||||||
|
client := &BitgetClient{}
|
||||||
|
marketConfig := DefaultMarketConfig()
|
||||||
|
client.marketClient = NewBitgetMarketClient(marketConfig)
|
||||||
|
|
||||||
|
// 连接
|
||||||
|
if err := client.ConnectMarketData(); err != nil {
|
||||||
|
log.Error("连接失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量订阅多个交易对的行情
|
||||||
|
symbols := []string{"BTCUSDT", "ETHUSDT", "ADAUSDT", "DOTUSDT"}
|
||||||
|
|
||||||
|
for _, symbol := range symbols {
|
||||||
|
listener := func(sym string) OnReceive {
|
||||||
|
return func(message string) {
|
||||||
|
log.Info("%s 行情更新: %s", sym, message)
|
||||||
|
}
|
||||||
|
}(symbol)
|
||||||
|
|
||||||
|
if err := client.SubscribeSpotTicker(symbol, listener); err != nil {
|
||||||
|
log.Error("订阅 %s 行情失败: %v", symbol, err)
|
||||||
|
} else {
|
||||||
|
log.Info("成功订阅 %s 行情", symbol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("批量订阅完成")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExampleCustomSubscription 展示如何使用自定义订阅
|
||||||
|
func ExampleCustomSubscription() {
|
||||||
|
client := &BitgetClient{}
|
||||||
|
marketConfig := DefaultMarketConfig()
|
||||||
|
client.marketClient = NewBitgetMarketClient(marketConfig)
|
||||||
|
|
||||||
|
if err := client.ConnectMarketData(); err != nil {
|
||||||
|
log.Error("连接失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义订阅请求
|
||||||
|
customSubscriptions := []SubscribeReq{
|
||||||
|
{
|
||||||
|
InstType: "SPOT",
|
||||||
|
Channel: "ticker",
|
||||||
|
InstId: "BTCUSDT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InstType: "SPOT",
|
||||||
|
Channel: "books",
|
||||||
|
InstId: "ETHUSDT",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
InstType: "UMCBL", // 合约
|
||||||
|
Channel: "ticker",
|
||||||
|
InstId: "BTCUSDT",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一的消息处理器
|
||||||
|
messageHandler := func(message string) {
|
||||||
|
log.Info("自定义订阅数据: %s", message)
|
||||||
|
// 在这里根据消息内容进行不同的处理
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量订阅
|
||||||
|
if err := client.SubscribeMarketData(customSubscriptions, messageHandler); err != nil {
|
||||||
|
log.Error("自定义订阅失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("自定义订阅完成")
|
||||||
|
}
|
||||||
326
services/bitgetservice/bitget_judge.go
Normal file
326
services/bitgetservice/bitget_judge.go
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
package bitgetservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
DbModels "go-admin/app/admin/models"
|
||||||
|
"go-admin/models"
|
||||||
|
"go-admin/models/positiondto"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JudgeCreateOrder 判断是否需要创建订单
|
||||||
|
// symbolType 1:现货 2:合约
|
||||||
|
func JudgeCreateOrder(trade models.TradeSet, symbolType int) error {
|
||||||
|
// var key string
|
||||||
|
|
||||||
|
// if symbolType == 1 {
|
||||||
|
// key = fmt.Sprintf(rediskey.PreSpotOrderList, global.EXCHANGE_BITGET)
|
||||||
|
// } else if symbolType == 2 {
|
||||||
|
// key = fmt.Sprintf(rediskey.PreFutOrderList, global.EXCHANGE_BITGET)
|
||||||
|
// } else {
|
||||||
|
// return fmt.Errorf("不支持的交易类型")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// preOrderVal, _ := helper.DefaultRedis.GetAllList(key)
|
||||||
|
// db := commonservice.GetDBConnection()
|
||||||
|
|
||||||
|
// if len(preOrderVal) == 0 {
|
||||||
|
// return fmt.Errorf("预订单列表为空")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // spotApi := SpotRestApi{}
|
||||||
|
// for _, item := range preOrderVal {
|
||||||
|
// preOrder := dto.PreOrderRedisList{}
|
||||||
|
// if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil {
|
||||||
|
// logger.Error("反序列化失败")
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if preOrder.Symbol == trade.Coin+trade.Currency {
|
||||||
|
// orderPrice, _ := decimal.NewFromString(preOrder.Price)
|
||||||
|
// tradePrice, _ := decimal.NewFromString(trade.LastPrice)
|
||||||
|
// //买入
|
||||||
|
// if strings.ToUpper(preOrder.Site) == "BUY" && orderPrice.Cmp(tradePrice) >= 0 && orderPrice.Cmp(decimal.Zero) > 0 && tradePrice.Cmp(decimal.Zero) > 0 {
|
||||||
|
// // SpotOrderLock(db, &preOrder, item, spotApi)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否触发加仓
|
||||||
|
func JudgeAddPositionOrder(trade models.TradeSet, symbolType int) error {
|
||||||
|
// var key string
|
||||||
|
|
||||||
|
// if symbolType == 1 {
|
||||||
|
// key = fmt.Sprintf(rediskey.SpotAddPositionList, global.EXCHANGE_BITGET)
|
||||||
|
// } else if symbolType == 2 {
|
||||||
|
// key = fmt.Sprintf(rediskey.FuturesAddPositionList, global.EXCHANGE_BITGET)
|
||||||
|
// } else {
|
||||||
|
// return fmt.Errorf("不支持的交易类型")
|
||||||
|
// }
|
||||||
|
// preOrderVal, _ := helper.DefaultRedis.GetAllList(key)
|
||||||
|
// db := commonservice.GetDBConnection()
|
||||||
|
|
||||||
|
// if len(preOrderVal) == 0 {
|
||||||
|
// return fmt.Errorf("预订单列表为空")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // spotApi := SpotRestApi{}
|
||||||
|
// for _, item := range preOrderVal {
|
||||||
|
// preOrder := positiondto.AddPositionList{}
|
||||||
|
// if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil {
|
||||||
|
// logger.Error("反序列化失败")
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if preOrder.Symbol == trade.Coin+trade.Currency {
|
||||||
|
// orderPrice := preOrder.Price
|
||||||
|
// tradePrice, _ := decimal.NewFromString(trade.LastPrice)
|
||||||
|
// //买入
|
||||||
|
// if strings.ToUpper(preOrder.Side) == "BUY" && orderPrice.Cmp(tradePrice) >= 0 && orderPrice.Cmp(decimal.Zero) > 0 && tradePrice.Cmp(decimal.Zero) > 0 {
|
||||||
|
// // SpotAddPositionTrigger(db, &preOrder, item, spotApi)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// JudgeReduceOrder 判断是否需要减仓
|
||||||
|
func JudgeReduceOrder(trade models.TradeSet, symbolType int) error {
|
||||||
|
// var key string
|
||||||
|
// var reduceReduceListKey string
|
||||||
|
|
||||||
|
// if symbolType == 1 {
|
||||||
|
// key = fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BITGET)
|
||||||
|
// reduceReduceListKey = fmt.Sprintf(rediskey.SpotOrderReduceStrategyList, global.EXCHANGE_BITGET)
|
||||||
|
// } else if symbolType == 2 {
|
||||||
|
// key = fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BITGET)
|
||||||
|
// reduceReduceListKey = fmt.Sprintf(rediskey.FutOrderReduceStrategyList, global.EXCHANGE_BITGET)
|
||||||
|
// } else {
|
||||||
|
// return fmt.Errorf("不支持的交易类型")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// db := commonservice.GetDBConnection()
|
||||||
|
// // spotApi := SpotRestApi{}
|
||||||
|
// setting, err := cacheservice.GetSystemSetting(db)
|
||||||
|
|
||||||
|
// if err != nil {
|
||||||
|
// logger.Error("获取系统设置失败")
|
||||||
|
// return errors.New("获取系统设置失败")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// reduceOrder := positiondto.ReduceListItem{}
|
||||||
|
// tradePrice, _ := decimal.NewFromString(trade.LastPrice)
|
||||||
|
// //减仓单减仓策略
|
||||||
|
// orderReduceVal, _ := helper.DefaultRedis.HGetAllFields(reduceReduceListKey)
|
||||||
|
// reduceOrderStrategy := dto.LineOrderReduceStrategyResp{}
|
||||||
|
// orderService := orderservice.OrderService{}
|
||||||
|
// orderService.Orm = db
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
// logger.Error("获取锁失败", err)
|
||||||
|
// return errors.New("获取锁失败")
|
||||||
|
// } else if ok {
|
||||||
|
// defer lock.Release()
|
||||||
|
// hasrecord, _ := helper.DefaultRedis.HExists(reduceReduceListKey, utility.IntTostring(reduceOrderStrategy.OrderId), item)
|
||||||
|
|
||||||
|
// if !hasrecord {
|
||||||
|
// logger.Debug("减仓缓存中不存在", item)
|
||||||
|
// return fmt.Errorf("减仓缓存中不存在")
|
||||||
|
// }
|
||||||
|
|
||||||
|
// order, err := orderService.CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit)
|
||||||
|
|
||||||
|
// if err != nil {
|
||||||
|
// logger.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 {
|
||||||
|
// logger.Errorf("删除redis reduceReduceListKey失败 %s", err.Error())
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// str, _ := sonic.MarshalString(reduceOrderStrategy)
|
||||||
|
// if err := helper.DefaultRedis.HSetField(reduceReduceListKey, orderId, str); err != nil {
|
||||||
|
// logger.Errorf("更新redis reduceReduceListKey失败 %s", err.Error())
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// //减仓单
|
||||||
|
// reduceVal, _ := helper.DefaultRedis.GetAllList(key)
|
||||||
|
|
||||||
|
// for _, item := range reduceVal {
|
||||||
|
// if err := sonic.Unmarshal([]byte(item), &reduceOrder); err != nil {
|
||||||
|
// logger.Error("反序列化失败")
|
||||||
|
// continue
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if reduceOrder.Symbol == trade.Coin+trade.Currency {
|
||||||
|
// orderPrice := reduceOrder.Price
|
||||||
|
// //买入
|
||||||
|
// 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, false, 0)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发现货减仓
|
||||||
|
// isStrategy 是否是策略减仓单
|
||||||
|
// reduceId 策略主减仓单id
|
||||||
|
func SpotReduceTrigger(db *gorm.DB, reduceOrder positiondto.ReduceListItem, setting DbModels.LineSystemSetting, key, item string, isStrategy bool, reduceId int) bool {
|
||||||
|
// tradeSet, err := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, reduceOrder.Symbol, 1)
|
||||||
|
result := true
|
||||||
|
|
||||||
|
// if err != nil {
|
||||||
|
// logger.Error("获取交易设置失败")
|
||||||
|
// 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 {
|
||||||
|
// logger.Error("获取锁失败", err)
|
||||||
|
// 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 IN (1,2,4) AND status IN (1,5)", reduceOrder.MainId).Find(&takeOrders).Error; err != nil {
|
||||||
|
// logger.Error("查询止盈单失败")
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// var hasrecord bool
|
||||||
|
|
||||||
|
// if isStrategy {
|
||||||
|
// hasrecord, _ = helper.DefaultRedis.HExists(key, utility.IntToString(reduceId), item)
|
||||||
|
// } else {
|
||||||
|
// hasrecord, _ = helper.DefaultRedis.IsElementInList(key, item)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if !hasrecord {
|
||||||
|
// logger.Debug("减仓缓存中不存在", item)
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
|
||||||
|
// apiInfo, _ := GetApiInfo(reduceOrder.ApiId)
|
||||||
|
|
||||||
|
// if apiInfo.Id == 0 {
|
||||||
|
// logger.Error("现货减仓 查询api用户不存在")
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
|
||||||
|
// for _, takeOrder := range takeOrders {
|
||||||
|
// err := CancelOpenOrderByOrderSnLoop(apiInfo, takeOrder.Symbol, takeOrder.OrderSn)
|
||||||
|
|
||||||
|
// if err != nil {
|
||||||
|
// logger.Error("现货撤单失败", err)
|
||||||
|
// return false
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// price := reduceOrder.Price.Mul(decimal.NewFromInt(1).Sub(setting.ReducePremium.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit))
|
||||||
|
|
||||||
|
// params := OrderPlacementService{
|
||||||
|
// ApiId: reduceOrder.ApiId,
|
||||||
|
// Side: reduceOrder.Side,
|
||||||
|
// Type: "LIMIT",
|
||||||
|
// TimeInForce: "GTC",
|
||||||
|
// Symbol: reduceOrder.Symbol,
|
||||||
|
// Price: price,
|
||||||
|
// Quantity: reduceOrder.Num,
|
||||||
|
// NewClientOrderId: reduceOrder.OrderSn,
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if err := spotApi.OrderPlaceLoop(db, params, 3); err != nil {
|
||||||
|
// result = false
|
||||||
|
// logger.Errorf("现货减仓挂单失败 id:%s err:%v", reduceOrder.Id, err)
|
||||||
|
|
||||||
|
// if err2 := db.Model(&DbModels.LinePreOrder{}).
|
||||||
|
// Where("id =? AND status =0", reduceOrder.Id).
|
||||||
|
// Updates(map[string]interface{}{"status": 2, "desc": err.Error()}).Error; err2 != nil {
|
||||||
|
// logger.Errorf("修改现货减仓状态失败 id:%s err:%v", reduceOrder.Id, err2)
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// if err := db.Model(&DbModels.LinePreOrder{}).Where("id =?", reduceOrder.Id).Updates(map[string]interface{}{"trigger_time": time.Now()}).Error; err != nil {
|
||||||
|
// logger.Error("更新预下单触发事件失败 ordersn:", reduceOrder.OrderSn)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if err := db.Model(&DbModels.LinePreOrder{}).
|
||||||
|
// Where("id =? AND status =0", reduceOrder.Id).
|
||||||
|
// Updates(map[string]interface{}{"status": 1}).Error; err != nil {
|
||||||
|
// logger.Errorf("修改现货减仓状态失败 id:%s err:%v", reduceOrder.Id, err)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// //处理减仓单减仓策略
|
||||||
|
// if err := CacheOrderStrategyAndReCreate(db, reduceOrder, 1, tradeSet, setting); err != nil {
|
||||||
|
// logger.Errorf("现货减仓 处理减仓策略失败 id:%v err:%v", reduceOrder.Id, err)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if _, err := helper.DefaultRedis.LRem(key, item); err != nil {
|
||||||
|
// result = false
|
||||||
|
// logger.Errorf("现货减仓 删除缓存失败 id:%v err:%v", reduceOrder.Id, err)
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// logger.Error("获取锁失败")
|
||||||
|
// result = false
|
||||||
|
// }
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// JudgeTakeOrder 判断是否止盈
|
||||||
|
func JudgeTakeOrder(trades []models.TradeSet, symbolType int) error {
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// JudgeStopOrder 判断是否止损
|
||||||
|
func JudgeStopOrder(trades []models.TradeSet, symbolType int) error {
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
430
services/bitgetservice/bitget_market_client.go
Normal file
430
services/bitgetservice/bitget_market_client.go
Normal file
@ -0,0 +1,430 @@
|
|||||||
|
package bitgetservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"go-admin/services/proxy"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
log "github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BitgetMarketClient Bitget公开行情订阅客户端
|
||||||
|
// 基于官方SDK设计,专门用于公开市场数据订阅
|
||||||
|
type BitgetMarketClient struct {
|
||||||
|
config *BitgetMarketConfig
|
||||||
|
conn *websocket.Conn
|
||||||
|
subscriptions map[SubscribeReq]OnReceive
|
||||||
|
allSubscribes *Set
|
||||||
|
listener OnReceive
|
||||||
|
errorListener OnReceive
|
||||||
|
sendMutex sync.Mutex
|
||||||
|
reconnectMutex sync.Mutex
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
connected bool
|
||||||
|
lastPongTime time.Time
|
||||||
|
proxyType string
|
||||||
|
proxyAddress string
|
||||||
|
reconnectCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBitgetMarketClient 创建新的Bitget市场数据客户端
|
||||||
|
func NewBitgetMarketClient(config *BitgetMarketConfig) *BitgetMarketClient {
|
||||||
|
if config == nil {
|
||||||
|
config = DefaultMarketConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
return &BitgetMarketClient{
|
||||||
|
config: config,
|
||||||
|
subscriptions: make(map[SubscribeReq]OnReceive),
|
||||||
|
allSubscribes: NewSet(),
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
connected: false,
|
||||||
|
lastPongTime: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetProxy 设置代理
|
||||||
|
func (c *BitgetMarketClient) SetProxy(proxyType, proxyAddress string) {
|
||||||
|
c.proxyType = proxyType
|
||||||
|
c.proxyAddress = proxyAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetListeners 设置消息监听器
|
||||||
|
func (c *BitgetMarketClient) SetListeners(msgListener, errorListener OnReceive) {
|
||||||
|
c.listener = msgListener
|
||||||
|
c.errorListener = errorListener
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect 连接WebSocket
|
||||||
|
func (c *BitgetMarketClient) Connect() error {
|
||||||
|
c.reconnectMutex.Lock()
|
||||||
|
defer c.reconnectMutex.Unlock()
|
||||||
|
|
||||||
|
if c.connected {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建WebSocket连接
|
||||||
|
dialer := websocket.DefaultDialer
|
||||||
|
|
||||||
|
// 设置代理
|
||||||
|
if c.proxyAddress != "" {
|
||||||
|
var err error
|
||||||
|
dialer, err = proxy.GetDialer(c.proxyType, c.proxyAddress)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("设置代理失败: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialer.HandshakeTimeout = 10 * time.Second
|
||||||
|
log.Infof("正在连接Bitget市场数据WebSocket: %s", c.config.WsUrl)
|
||||||
|
conn, _, err := dialer.Dial(c.config.WsUrl, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("webSocket连接失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.conn = conn
|
||||||
|
c.connected = true
|
||||||
|
c.lastPongTime = time.Now()
|
||||||
|
c.reconnectCount = 0
|
||||||
|
|
||||||
|
log.Info("Bitget市场数据WebSocket连接成功")
|
||||||
|
|
||||||
|
// 启动消息处理协程
|
||||||
|
go c.readLoop()
|
||||||
|
go c.pingLoop()
|
||||||
|
go c.monitorConnection()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subscribe 订阅市场数据
|
||||||
|
func (c *BitgetMarketClient) Subscribe(subscribeReqs []SubscribeReq, listener OnReceive) error {
|
||||||
|
if !c.connected {
|
||||||
|
if err := c.Connect(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var args []interface{}
|
||||||
|
for _, req := range subscribeReqs {
|
||||||
|
// 标准化订阅请求
|
||||||
|
normalizedReq := c.normalizeSubscribeReq(req)
|
||||||
|
args = append(args, normalizedReq)
|
||||||
|
|
||||||
|
// 保存订阅信息
|
||||||
|
c.subscriptions[normalizedReq] = listener
|
||||||
|
c.allSubscribes.Add(normalizedReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送订阅消息
|
||||||
|
wsReq := WsBaseReq{
|
||||||
|
Op: "subscribe",
|
||||||
|
Args: args,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.sendMessage(wsReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe 取消订阅
|
||||||
|
func (c *BitgetMarketClient) Unsubscribe(subscribeReqs []SubscribeReq) error {
|
||||||
|
if !c.connected {
|
||||||
|
return errors.New("WebSocket未连接")
|
||||||
|
}
|
||||||
|
|
||||||
|
var args []interface{}
|
||||||
|
for _, req := range subscribeReqs {
|
||||||
|
normalizedReq := c.normalizeSubscribeReq(req)
|
||||||
|
args = append(args, normalizedReq)
|
||||||
|
|
||||||
|
// 移除订阅信息
|
||||||
|
delete(c.subscriptions, normalizedReq)
|
||||||
|
c.allSubscribes.Remove(normalizedReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送取消订阅消息
|
||||||
|
wsReq := WsBaseReq{
|
||||||
|
Op: "unsubscribe",
|
||||||
|
Args: args,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.sendMessage(wsReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeSubscribeReq 标准化订阅请求
|
||||||
|
func (c *BitgetMarketClient) normalizeSubscribeReq(req SubscribeReq) SubscribeReq {
|
||||||
|
return SubscribeReq{
|
||||||
|
InstType: strings.ToUpper(req.InstType),
|
||||||
|
Channel: strings.ToLower(req.Channel),
|
||||||
|
InstId: strings.ToUpper(req.InstId),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendMessage 发送消息
|
||||||
|
func (c *BitgetMarketClient) sendMessage(msg interface{}) error {
|
||||||
|
if c.conn == nil {
|
||||||
|
return errors.New("WebSocket连接不可用")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.sendMutex.Lock()
|
||||||
|
defer c.sendMutex.Unlock()
|
||||||
|
|
||||||
|
data, err := sonic.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("序列化消息失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("发送消息: %s", string(data))
|
||||||
|
|
||||||
|
c.conn.SetWriteDeadline(time.Now().Add(c.config.WriteTimeout))
|
||||||
|
return c.conn.WriteMessage(websocket.TextMessage, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readLoop 消息读取循环
|
||||||
|
func (c *BitgetMarketClient) readLoop() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Error("readLoop panic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
if c.conn == nil {
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.conn.SetReadDeadline(time.Now().Add(c.config.ReadTimeout))
|
||||||
|
_, message, err := c.conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("读取消息失败: %v", err)
|
||||||
|
c.handleDisconnect()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.handleMessage(string(message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMessage 处理接收到的消息
|
||||||
|
func (c *BitgetMarketClient) handleMessage(message string) {
|
||||||
|
log.Info("收到消息: %s", message)
|
||||||
|
|
||||||
|
// 处理pong消息
|
||||||
|
if message == "pong" {
|
||||||
|
c.lastPongTime = time.Now()
|
||||||
|
log.Debug("收到pong消息")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析JSON消息
|
||||||
|
var jsonMap map[string]interface{}
|
||||||
|
if err := json.Unmarshal([]byte(message), &jsonMap); err != nil {
|
||||||
|
log.Error("解析JSON消息失败: %v", err)
|
||||||
|
if c.errorListener != nil {
|
||||||
|
c.errorListener(message)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查错误码
|
||||||
|
if code, exists := jsonMap["code"]; exists {
|
||||||
|
if codeNum, ok := code.(float64); ok && int(codeNum) != 0 {
|
||||||
|
log.Error("收到错误消息: %s", message)
|
||||||
|
if c.errorListener != nil {
|
||||||
|
c.errorListener(message)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理订阅确认消息
|
||||||
|
if event, exists := jsonMap["event"]; exists && event == "subscribe" {
|
||||||
|
log.Info("订阅确认: %s", message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理数据推送
|
||||||
|
if data, exists := jsonMap["data"]; exists && data != nil {
|
||||||
|
if arg, argExists := jsonMap["arg"]; argExists {
|
||||||
|
listener := c.getListener(arg)
|
||||||
|
if listener != nil {
|
||||||
|
listener(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认处理
|
||||||
|
if c.listener != nil {
|
||||||
|
c.listener(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getListener 获取对应的监听器
|
||||||
|
func (c *BitgetMarketClient) getListener(argJson interface{}) OnReceive {
|
||||||
|
argMap, ok := argJson.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
return c.listener
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeReq := SubscribeReq{
|
||||||
|
InstType: fmt.Sprintf("%v", argMap["instType"]),
|
||||||
|
Channel: fmt.Sprintf("%v", argMap["channel"]),
|
||||||
|
InstId: fmt.Sprintf("%v", argMap["instId"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
if listener, exists := c.subscriptions[subscribeReq]; exists {
|
||||||
|
return listener
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.listener
|
||||||
|
}
|
||||||
|
|
||||||
|
// pingLoop Ping循环
|
||||||
|
func (c *BitgetMarketClient) pingLoop() {
|
||||||
|
ticker := time.NewTicker(c.config.PingInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if c.connected && c.conn != nil {
|
||||||
|
c.sendMutex.Lock()
|
||||||
|
c.conn.SetWriteDeadline(time.Now().Add(c.config.WriteTimeout))
|
||||||
|
err := c.conn.WriteMessage(websocket.TextMessage, []byte("ping"))
|
||||||
|
c.sendMutex.Unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error("发送ping失败: %v", err)
|
||||||
|
c.handleDisconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// monitorConnection 监控连接状态
|
||||||
|
func (c *BitgetMarketClient) monitorConnection() {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if c.connected && time.Since(c.lastPongTime) > 60*time.Second {
|
||||||
|
log.Warn("长时间未收到pong消息,触发重连")
|
||||||
|
c.handleDisconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleDisconnect 处理断开连接
|
||||||
|
func (c *BitgetMarketClient) handleDisconnect() {
|
||||||
|
c.reconnectMutex.Lock()
|
||||||
|
defer c.reconnectMutex.Unlock()
|
||||||
|
|
||||||
|
if !c.connected {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Warn("WebSocket连接断开,准备重连")
|
||||||
|
c.connected = false
|
||||||
|
|
||||||
|
if c.conn != nil {
|
||||||
|
c.conn.Close()
|
||||||
|
c.conn = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重连逻辑
|
||||||
|
go c.reconnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// reconnect 重连
|
||||||
|
func (c *BitgetMarketClient) reconnect() {
|
||||||
|
for c.reconnectCount < c.config.MaxReconnectTimes {
|
||||||
|
select {
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
c.reconnectCount++
|
||||||
|
log.Info("尝试重连 (%d/%d)", c.reconnectCount, c.config.MaxReconnectTimes)
|
||||||
|
|
||||||
|
time.Sleep(c.config.ReconnectInterval)
|
||||||
|
|
||||||
|
if err := c.Connect(); err != nil {
|
||||||
|
log.Error("重连失败: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新订阅
|
||||||
|
if err := c.resubscribe(); err != nil {
|
||||||
|
log.Error("重新订阅失败: %v", err)
|
||||||
|
c.handleDisconnect()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("重连成功")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Error("达到最大重连次数,停止重连")
|
||||||
|
}
|
||||||
|
|
||||||
|
// resubscribe 重新订阅
|
||||||
|
func (c *BitgetMarketClient) resubscribe() error {
|
||||||
|
if len(c.subscriptions) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var args []interface{}
|
||||||
|
for req := range c.subscriptions {
|
||||||
|
args = append(args, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
wsReq := WsBaseReq{
|
||||||
|
Op: "subscribe",
|
||||||
|
Args: args,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.sendMessage(wsReq)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭客户端
|
||||||
|
func (c *BitgetMarketClient) Close() error {
|
||||||
|
c.cancel()
|
||||||
|
c.connected = false
|
||||||
|
|
||||||
|
if c.conn != nil {
|
||||||
|
return c.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsConnected 检查连接状态
|
||||||
|
func (c *BitgetMarketClient) IsConnected() bool {
|
||||||
|
return c.connected
|
||||||
|
}
|
||||||
61
services/bitgetservice/bitget_market_future.go
Normal file
61
services/bitgetservice/bitget_market_future.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package bitgetservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go-admin/config"
|
||||||
|
|
||||||
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BitgetMarketFutureClient struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BitgetMarketFutureClient) Subscribe(symbols []string) error {
|
||||||
|
// 1. 创建Bitget客户端(不需要API密钥用于公开数据)
|
||||||
|
client := &BitgetClient{}
|
||||||
|
|
||||||
|
// 初始化公开行情客户端
|
||||||
|
marketConfig := DefaultMarketConfig()
|
||||||
|
client.marketClient = NewBitgetMarketClient(marketConfig)
|
||||||
|
if config.ExtConfig.ProxyUrl != "" {
|
||||||
|
client.SetMarketProxy("", config.ExtConfig.ProxyUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 设置消息监听器
|
||||||
|
msgListener := func(message string) {
|
||||||
|
logger.Info("收到市场数据: %s", message)
|
||||||
|
// 在这里处理市场数据
|
||||||
|
}
|
||||||
|
|
||||||
|
errorListener := func(message string) {
|
||||||
|
logger.Error("收到错误消息: %s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.SetMarketListeners(msgListener, errorListener)
|
||||||
|
|
||||||
|
// 4. 连接WebSocket
|
||||||
|
if err := client.ConnectMarketData(); err != nil {
|
||||||
|
logger.Error("连接市场数据失败: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 订阅合约行情
|
||||||
|
tickerListener := func(message string) {
|
||||||
|
logger.Info("收到BTC-USDT合约行情: %s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
params := []SubscribeReq{}
|
||||||
|
|
||||||
|
for _, v := range symbols {
|
||||||
|
params = append(params, SubscribeReq{
|
||||||
|
InstType: "USDT-FUTURES",
|
||||||
|
Channel: "ticker",
|
||||||
|
InstId: v,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.SubscribeMarketData(params, tickerListener); err != nil {
|
||||||
|
logger.Error("订阅合约行情失败: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
126
services/bitgetservice/bitget_market_models.go
Normal file
126
services/bitgetservice/bitget_market_models.go
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
package bitgetservice
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// SubscribeReq 订阅请求结构(基于官方SDK)
|
||||||
|
type SubscribeReq struct {
|
||||||
|
InstType string `json:"instType"` // 产品类型:SPOT-现货,UMCBL-U本位合约,DMCBL-币本位合约
|
||||||
|
Channel string `json:"channel"` // 频道名称
|
||||||
|
InstId string `json:"instId"` // 产品ID
|
||||||
|
// Coin string `json:"coin"` // 币种(可选)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WsBaseReq WebSocket基础请求结构
|
||||||
|
type WsBaseReq struct {
|
||||||
|
Op string `json:"op"` // 操作类型:subscribe, unsubscribe, login
|
||||||
|
Args []interface{} `json:"args"` // 参数列表
|
||||||
|
}
|
||||||
|
|
||||||
|
// WsLoginReq WebSocket登录请求
|
||||||
|
type WsLoginReq struct {
|
||||||
|
ApiKey string `json:"apiKey"`
|
||||||
|
Passphrase string `json:"passphrase"`
|
||||||
|
Timestamp string `json:"timestamp"`
|
||||||
|
Sign string `json:"sign"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnReceive 消息接收回调函数类型
|
||||||
|
type OnReceive func(message string)
|
||||||
|
|
||||||
|
// Set 集合数据结构(简化实现)
|
||||||
|
type Set struct {
|
||||||
|
items map[SubscribeReq]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSet 创建新的集合
|
||||||
|
func NewSet() *Set {
|
||||||
|
return &Set{
|
||||||
|
items: make(map[SubscribeReq]bool),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add 添加元素到集合
|
||||||
|
func (s *Set) Add(item SubscribeReq) {
|
||||||
|
s.items[item] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove 从集合中移除元素
|
||||||
|
func (s *Set) Remove(item SubscribeReq) {
|
||||||
|
delete(s.items, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains 检查集合是否包含元素
|
||||||
|
func (s *Set) Contains(item SubscribeReq) bool {
|
||||||
|
_, exists := s.items[item]
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
// BitgetMarketData 市场数据基础结构
|
||||||
|
type BitgetMarketData struct {
|
||||||
|
Action string `json:"action"` // 推送类型
|
||||||
|
Arg SubscribeReq `json:"arg"` // 订阅参数
|
||||||
|
Data interface{} `json:"data"` // 数据内容
|
||||||
|
Ts int64 `json:"ts"` // 时间戳
|
||||||
|
}
|
||||||
|
|
||||||
|
// TickerData 行情数据
|
||||||
|
type TickerData struct {
|
||||||
|
InstId string `json:"instId"` // 产品ID
|
||||||
|
Last string `json:"last"` // 最新价
|
||||||
|
Open24h string `json:"open24h"` // 24小时开盘价
|
||||||
|
High24h string `json:"high24h"` // 24小时最高价
|
||||||
|
Low24h string `json:"low24h"` // 24小时最低价
|
||||||
|
Change24h string `json:"change24h"` // 24小时涨跌幅
|
||||||
|
Vol24h string `json:"vol24h"` // 24小时成交量
|
||||||
|
VolCcy24h string `json:"volCcy24h"` // 24小时成交额
|
||||||
|
Ts string `json:"ts"` // 时间戳
|
||||||
|
}
|
||||||
|
|
||||||
|
// DepthData 深度数据
|
||||||
|
type DepthData struct {
|
||||||
|
InstId string `json:"instId"` // 产品ID
|
||||||
|
Asks [][]string `json:"asks"` // 卖盘 [价格, 数量]
|
||||||
|
Bids [][]string `json:"bids"` // 买盘 [价格, 数量]
|
||||||
|
Ts string `json:"ts"` // 时间戳
|
||||||
|
}
|
||||||
|
|
||||||
|
// TradeData 成交数据
|
||||||
|
type TradeData struct {
|
||||||
|
InstId string `json:"instId"` // 产品ID
|
||||||
|
TradeId string `json:"tradeId"` // 成交ID
|
||||||
|
Px string `json:"px"` // 成交价格
|
||||||
|
Sz string `json:"sz"` // 成交数量
|
||||||
|
Side string `json:"side"` // 成交方向 buy/sell
|
||||||
|
Ts string `json:"ts"` // 成交时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// KlineData K线数据
|
||||||
|
type KlineData struct {
|
||||||
|
InstId string `json:"instId"` // 产品ID
|
||||||
|
Candle []string `json:"candle"` // K线数据 [时间戳, 开盘价, 最高价, 最低价, 收盘价, 成交量, 成交额]
|
||||||
|
Ts string `json:"ts"` // 时间戳
|
||||||
|
}
|
||||||
|
|
||||||
|
// BitgetMarketConfig 市场数据配置
|
||||||
|
type BitgetMarketConfig struct {
|
||||||
|
WsUrl string // WebSocket地址
|
||||||
|
ReconnectInterval time.Duration // 重连间隔
|
||||||
|
PingInterval time.Duration // Ping间隔
|
||||||
|
ReadTimeout time.Duration // 读取超时
|
||||||
|
WriteTimeout time.Duration // 写入超时
|
||||||
|
MaxReconnectTimes int // 最大重连次数
|
||||||
|
EnableCompression bool // 是否启用压缩
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultMarketConfig 默认市场数据配置
|
||||||
|
func DefaultMarketConfig() *BitgetMarketConfig {
|
||||||
|
return &BitgetMarketConfig{
|
||||||
|
WsUrl: "wss://ws.bitget.com/v2/ws/public",
|
||||||
|
ReconnectInterval: 5 * time.Second,
|
||||||
|
PingInterval: 15 * time.Second,
|
||||||
|
ReadTimeout: 60 * time.Second,
|
||||||
|
WriteTimeout: 10 * time.Second,
|
||||||
|
MaxReconnectTimes: 10,
|
||||||
|
EnableCompression: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
61
services/bitgetservice/bitget_market_spot.go
Normal file
61
services/bitgetservice/bitget_market_spot.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package bitgetservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go-admin/config"
|
||||||
|
|
||||||
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BitgetMarketSpotClient struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *BitgetMarketSpotClient) Subscribe(symbols []string) error {
|
||||||
|
// 1. 创建Bitget客户端(不需要API密钥用于公开数据)
|
||||||
|
client := &BitgetClient{}
|
||||||
|
|
||||||
|
// 初始化公开行情客户端
|
||||||
|
marketConfig := DefaultMarketConfig()
|
||||||
|
client.marketClient = NewBitgetMarketClient(marketConfig)
|
||||||
|
if config.ExtConfig.ProxyUrl != "" {
|
||||||
|
client.SetMarketProxy("", config.ExtConfig.ProxyUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 设置消息监听器
|
||||||
|
msgListener := func(message string) {
|
||||||
|
logger.Infof("收到市场数据: %s", message)
|
||||||
|
// 在这里处理市场数据
|
||||||
|
}
|
||||||
|
|
||||||
|
errorListener := func(message string) {
|
||||||
|
logger.Errorf("收到错误消息: %s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.SetMarketListeners(msgListener, errorListener)
|
||||||
|
|
||||||
|
// 4. 连接WebSocket
|
||||||
|
if err := client.ConnectMarketData(); err != nil {
|
||||||
|
logger.Errorf("连接市场数据失败: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 订阅现货行情
|
||||||
|
tickerListener := func(message string) {
|
||||||
|
logger.Infof("收到行情: %s", message)
|
||||||
|
}
|
||||||
|
|
||||||
|
params := []SubscribeReq{}
|
||||||
|
|
||||||
|
for _, v := range symbols {
|
||||||
|
params = append(params, SubscribeReq{
|
||||||
|
InstType: "SPOT",
|
||||||
|
Channel: "ticker",
|
||||||
|
InstId: v,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := client.SubscribeMarketData(params, tickerListener); err != nil {
|
||||||
|
logger.Errorf("订阅现货行情失败: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
54
services/bitgetservice/bitget_market_test.go
Normal file
54
services/bitgetservice/bitget_market_test.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package bitgetservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go-admin/common/helper"
|
||||||
|
"go-admin/config"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func initSetting() {
|
||||||
|
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")
|
||||||
|
config.ExtConfig.ProxyUrl = "http://127.0.0.1:7890"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitgetMarketFutureClient_Subscribe(t *testing.T) {
|
||||||
|
initSetting()
|
||||||
|
// 初始化Bitget合约行情客户端
|
||||||
|
bitgetFut := BitgetMarketFutureClient{}
|
||||||
|
|
||||||
|
// 测试用的合约符号列表
|
||||||
|
testSymbols := []string{"ETHUSDT"}
|
||||||
|
|
||||||
|
// 调用订阅方法
|
||||||
|
err := bitgetFut.Subscribe(testSymbols)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("订阅Bitget合约行情失败: %v", err)
|
||||||
|
}
|
||||||
|
time.Sleep(60 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBitgetMarketSpotClient_Subscribe(t *testing.T) {
|
||||||
|
initSetting()
|
||||||
|
// 初始化Bitget现货行情客户端
|
||||||
|
bitgetSpot := BitgetMarketSpotClient{}
|
||||||
|
|
||||||
|
// 测试用的现货符号列表
|
||||||
|
testSymbols := []string{"ETHUSDT"}
|
||||||
|
|
||||||
|
// 调用订阅方法
|
||||||
|
err := bitgetSpot.Subscribe(testSymbols)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("订阅Bitget现货行情失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(60 * time.Second)
|
||||||
|
}
|
||||||
417
services/bitgetservice/bitget_rest.go
Normal file
417
services/bitgetservice/bitget_rest.go
Normal file
@ -0,0 +1,417 @@
|
|||||||
|
package bitgetservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go-admin/common/global"
|
||||||
|
"go-admin/common/helper"
|
||||||
|
"go-admin/config"
|
||||||
|
"go-admin/models"
|
||||||
|
"go-admin/pkg/utility"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
log "github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BitgetRestApi Bitget REST API服务
|
||||||
|
type BitgetRestApi struct {
|
||||||
|
httpClient *helper.BitgetClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBitgetRestApi 创建新的Bitget REST API实例
|
||||||
|
func NewBitgetRestApi() *BitgetRestApi {
|
||||||
|
// 从配置中获取代理设置
|
||||||
|
proxyUrl := config.ExtConfig.ProxyUrl
|
||||||
|
|
||||||
|
// 使用代理配置创建HTTP客户端
|
||||||
|
httpClient, err := helper.NewBitgetClient("", "", "", proxyUrl)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("创建Bitget HTTP客户端失败", err)
|
||||||
|
// 如果代理创建失败,使用无代理的客户端
|
||||||
|
httpClient, _ = helper.NewBitgetClient("", "", "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BitgetRestApi{
|
||||||
|
httpClient: httpClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBitgetSimpleClient 创建简单的Bitget客户端(兼容性函数)
|
||||||
|
func NewBitgetSimpleClient() *BitgetClient {
|
||||||
|
// 从配置中获取代理设置
|
||||||
|
proxyUrl := config.ExtConfig.ProxyUrl
|
||||||
|
|
||||||
|
client, err := NewBitgetClient("", "", "", proxyUrl)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("创建Bitget简单客户端失败", err)
|
||||||
|
// 如果代理创建失败,使用无代理的客户端
|
||||||
|
client, _ = NewBitgetClient("", "", "", "")
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
// bitgetSymbolResponse 通用的符号响应结构体
|
||||||
|
type bitgetSymbolResponse struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data []struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
BaseCoin string `json:"baseCoin"`
|
||||||
|
QuoteCoin string `json:"quoteCoin"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
MinTradeAmount string `json:"minTradeAmount"`
|
||||||
|
MaxTradeAmount string `json:"maxTradeAmount"`
|
||||||
|
TakerFeeRate string `json:"takerFeeRate"`
|
||||||
|
MakerFeeRate string `json:"makerFeeRate"`
|
||||||
|
PricePrecision string `json:"pricePrecision"`
|
||||||
|
QuantityPrecision string `json:"quantityPrecision"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// getSymbolsFromAPI 通用的符号获取方法
|
||||||
|
// symbolType: "spot" 现货, "futures" 合约
|
||||||
|
func (api *BitgetRestApi) getSymbolsFromAPI(symbolType string) ([]BitgetSymbol, error) {
|
||||||
|
var url string
|
||||||
|
var errorPrefix string
|
||||||
|
|
||||||
|
switch symbolType {
|
||||||
|
case "spot":
|
||||||
|
url = "/api/v2/spot/public/symbols"
|
||||||
|
errorPrefix = "现货"
|
||||||
|
case "futures":
|
||||||
|
url = "/api/v2/mix/market/contracts?productType=USDT-FUTURES"
|
||||||
|
errorPrefix = "合约"
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("不支持的符号类型: %s", symbolType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送API请求
|
||||||
|
resp, _, err := api.httpClient.SendSpotRequestAuth(url, "GET", map[string]interface{}{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("获取bitget %s交易所信息失败: %v", errorPrefix, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应数据
|
||||||
|
var response bitgetSymbolResponse
|
||||||
|
if err := sonic.Unmarshal(resp, &response); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析bitget %s交易所信息失败: %v", errorPrefix, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查API响应状态
|
||||||
|
if response.Code != "00000" {
|
||||||
|
return nil, fmt.Errorf("bitget API错误: %s", response.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换数据格式
|
||||||
|
return api.convertToSymbols(response.Data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// convertToSymbols 将API响应数据转换为BitgetSymbol切片
|
||||||
|
func (api *BitgetRestApi) convertToSymbols(data []struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
BaseCoin string `json:"baseCoin"`
|
||||||
|
QuoteCoin string `json:"quoteCoin"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
MinTradeAmount string `json:"minTradeAmount"`
|
||||||
|
MaxTradeAmount string `json:"maxTradeAmount"`
|
||||||
|
TakerFeeRate string `json:"takerFeeRate"`
|
||||||
|
MakerFeeRate string `json:"makerFeeRate"`
|
||||||
|
PricePrecision string `json:"pricePrecision"`
|
||||||
|
QuantityPrecision string `json:"quantityPrecision"`
|
||||||
|
}) []BitgetSymbol {
|
||||||
|
symbols := make([]BitgetSymbol, 0, len(data))
|
||||||
|
|
||||||
|
for _, item := range data {
|
||||||
|
if item.Status == "online" {
|
||||||
|
priceDigit, _ := strconv.Atoi(item.PricePrecision)
|
||||||
|
quantileDigit, _ := strconv.Atoi(item.QuantityPrecision)
|
||||||
|
|
||||||
|
symbol := BitgetSymbol{
|
||||||
|
Symbol: item.Symbol,
|
||||||
|
BaseAsset: item.BaseCoin,
|
||||||
|
QuoteAsset: item.QuoteCoin,
|
||||||
|
Status: item.Status,
|
||||||
|
MinQty: item.MinTradeAmount,
|
||||||
|
MaxQty: item.MaxTradeAmount,
|
||||||
|
PriceDigit: priceDigit,
|
||||||
|
QuantityDigit: quantileDigit,
|
||||||
|
}
|
||||||
|
symbols = append(symbols, symbol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return symbols
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSpotSymbols 获取现货交易对信息
|
||||||
|
func (api *BitgetRestApi) GetSpotSymbols() ([]BitgetSymbol, error) {
|
||||||
|
return api.getSymbolsFromAPI("spot")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFutSymbols 获取合约交易对信息
|
||||||
|
func (api *BitgetRestApi) GetFutSymbols() ([]BitgetSymbol, error) {
|
||||||
|
return api.getSymbolsFromAPI("futures")
|
||||||
|
}
|
||||||
|
|
||||||
|
// symbolConfig 符号配置结构体
|
||||||
|
type symbolConfig struct {
|
||||||
|
symbolType string
|
||||||
|
quoteAssetSymbols []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildTradeSetFromSymbols 通用的TradeSet构建方法
|
||||||
|
func buildTradeSetFromSymbols(symbols []BitgetSymbol, config symbolConfig) (map[string]models.TradeSet, []string) {
|
||||||
|
tradeSets := make(map[string]models.TradeSet, len(symbols))
|
||||||
|
deleteSymbols := make([]string, 0)
|
||||||
|
|
||||||
|
for _, item := range symbols {
|
||||||
|
if utility.ContainsStr(config.quoteAssetSymbols, item.QuoteAsset) && item.Status == "online" {
|
||||||
|
tradeSet := models.TradeSet{
|
||||||
|
Coin: item.BaseAsset,
|
||||||
|
Currency: item.QuoteAsset,
|
||||||
|
PriceDigit: item.PriceDigit,
|
||||||
|
AmountDigit: item.QuantityDigit,
|
||||||
|
MinQty: utility.StringAsFloat(item.MinQty),
|
||||||
|
}
|
||||||
|
tradeSets[item.Symbol] = tradeSet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tradeSets, deleteSymbols
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSpotSymbols 获取现货交易对并构建TradeSet映射
|
||||||
|
func GetSpotSymbols() (map[string]models.TradeSet, []string, error) {
|
||||||
|
api := NewBitgetRestApi()
|
||||||
|
symbols, err := api.GetSpotSymbols()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("获取bitget规范信息失败", err)
|
||||||
|
return make(map[string]models.TradeSet, 0), []string{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config := symbolConfig{
|
||||||
|
symbolType: "spot",
|
||||||
|
quoteAssetSymbols: []string{"USDT", "ETH", "BTC", "SOL", "BNB", "DOGE"},
|
||||||
|
}
|
||||||
|
|
||||||
|
tradeSets, deleteSymbols := buildTradeSetFromSymbols(symbols, config)
|
||||||
|
return tradeSets, deleteSymbols, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFutSymbols 获取合约交易对并构建TradeSet映射
|
||||||
|
func GetFutSymbols() (map[string]models.TradeSet, []string, error) {
|
||||||
|
api := NewBitgetRestApi()
|
||||||
|
symbols, err := api.GetFutSymbols()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("获取bitget合约规范信息失败", err)
|
||||||
|
return make(map[string]models.TradeSet, 0), []string{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config := symbolConfig{
|
||||||
|
symbolType: "futures",
|
||||||
|
quoteAssetSymbols: []string{"USDT"},
|
||||||
|
}
|
||||||
|
|
||||||
|
tradeSets, deleteSymbols := buildTradeSetFromSymbols(symbols, config)
|
||||||
|
return tradeSets, deleteSymbols, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getTicker24hData 获取24小时行情数据的通用方法
|
||||||
|
func (api *BitgetRestApi) getTicker24hData(url, tickerType, description string, tradeSet *map[string]models.TradeSet) ([]string, error) {
|
||||||
|
// 定义响应数据结构
|
||||||
|
type tickerResponse struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data []struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
High24h string `json:"high24h"`
|
||||||
|
Low24h string `json:"low24h"`
|
||||||
|
Close string `json:"lastPr"`
|
||||||
|
Open string `json:"open"`
|
||||||
|
Volume24h string `json:"baseVolume"` // 基础币成交量
|
||||||
|
QuoteVol string `json:"usdtVolume"` // USDT成交量
|
||||||
|
Change24h string `json:"changeUtc24h"` // UTC 24h涨跌幅
|
||||||
|
Change string `json:"change24h"` // 24h涨跌幅
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送API请求
|
||||||
|
resp, _, err := api.httpClient.SendSpotRequestAuth(url, "GET", map[string]interface{}{})
|
||||||
|
if err != nil {
|
||||||
|
return []string{}, fmt.Errorf("获取bitget %s 24h行情失败: %v", description, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析响应数据
|
||||||
|
var response tickerResponse
|
||||||
|
if err := sonic.Unmarshal(resp, &response); err != nil {
|
||||||
|
return []string{}, fmt.Errorf("解析bitget %s 24h行情失败: %v", description, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查API响应状态
|
||||||
|
if response.Code != "00000" {
|
||||||
|
return []string{}, fmt.Errorf("bitget %s API错误: %s", description, response.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理行情数据
|
||||||
|
deleteSymbols := make([]string, 0)
|
||||||
|
processedCount := 0
|
||||||
|
errorCount := 0
|
||||||
|
|
||||||
|
for _, item := range response.Data {
|
||||||
|
// 构建缓存键
|
||||||
|
key := fmt.Sprintf(tickerType, global.EXCHANGE_BITGET, item.Symbol)
|
||||||
|
|
||||||
|
// 检查交易对是否存在
|
||||||
|
symbol, exists := (*tradeSet)[item.Symbol]
|
||||||
|
if !exists {
|
||||||
|
// 清理不存在的交易对缓存
|
||||||
|
if err := helper.DefaultRedis.DeleteString(key); err != nil {
|
||||||
|
log.Warn("删除无效缓存失败: %s, error: %v", key, err)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析交易对获取coin和currency
|
||||||
|
var coin, currency string
|
||||||
|
if strings.HasSuffix(item.Symbol, symbol.Currency) {
|
||||||
|
coin = strings.TrimSuffix(item.Symbol, symbol.Currency)
|
||||||
|
currency = symbol.Currency
|
||||||
|
} else {
|
||||||
|
deleteSymbols = append(deleteSymbols, item.Symbol)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建交易对数据
|
||||||
|
symbol.Coin = coin
|
||||||
|
symbol.Currency = currency
|
||||||
|
symbol.OpenPrice = utility.StringAsFloat(item.Open)
|
||||||
|
symbol.PriceChange = utility.StringAsFloat(item.Change)
|
||||||
|
symbol.LowPrice = item.Low24h
|
||||||
|
symbol.HighPrice = item.High24h
|
||||||
|
symbol.Volume = item.Volume24h
|
||||||
|
symbol.QuoteVolume = item.QuoteVol
|
||||||
|
symbol.LastPrice = item.Close
|
||||||
|
|
||||||
|
// 验证数据有效性
|
||||||
|
if symbol.Coin == "" || symbol.Currency == "" ||
|
||||||
|
utility.StringToFloat64(symbol.QuoteVolume) <= 0 ||
|
||||||
|
symbol.LastPrice == "" || utility.StringToFloat64(symbol.LastPrice) <= 0 {
|
||||||
|
if err := helper.DefaultRedis.DeleteString(key); err != nil {
|
||||||
|
log.Warn("删除无效数据缓存失败: %s, error: %v", key, err)
|
||||||
|
}
|
||||||
|
deleteSymbols = append(deleteSymbols, item.Symbol)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存数据
|
||||||
|
val, err := sonic.Marshal(symbol)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("序列化交易对数据失败: %s, error: %v", item.Symbol, err)
|
||||||
|
errorCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := helper.DefaultRedis.SetString(key, string(val)); err != nil {
|
||||||
|
log.Error("缓存交易对失败: %s, error: %v", item.Symbol, err)
|
||||||
|
errorCount++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
processedCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("处理%s行情数据完成: 成功=%d, 错误=%d, 删除=%d",
|
||||||
|
description, processedCount, errorCount, len(deleteSymbols))
|
||||||
|
|
||||||
|
return deleteSymbols, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSpotTicker24h 获取现货24小时行情变更并缓存
|
||||||
|
func (api *BitgetRestApi) GetSpotTicker24h(tradeSet *map[string]models.TradeSet) ([]string, error) {
|
||||||
|
return api.getTicker24hData("/api/v2/spot/market/tickers", global.TICKER_SPOT, "现货", tradeSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFutTicker24h 获取U本位合约24小时行情变更并缓存
|
||||||
|
func (api *BitgetRestApi) GetFutTicker24h(tradeSet *map[string]models.TradeSet) ([]string, error) {
|
||||||
|
return api.getTicker24hData("/api/v2/mix/market/tickers?productType=USDT-FUTURES", global.TICKER_FUTURES, "U本位合约", tradeSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BitgetSymbol Bitget交易对信息
|
||||||
|
type BitgetSymbol struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
BaseCoin string `json:"baseCoin"`
|
||||||
|
QuoteCoin string `json:"quoteCoin"`
|
||||||
|
BaseAsset string `json:"baseAsset"`
|
||||||
|
QuoteAsset string `json:"quoteAsset"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
MinQty string `json:"minQty"`
|
||||||
|
MaxQty string `json:"maxQty"`
|
||||||
|
PriceDigit int `json:"priceDigit"`
|
||||||
|
QuantityDigit int `json:"quantityDigit"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAndReloadSymbols 获取并重新加载交易对信息
|
||||||
|
// symbolType 1-现货 2-合约
|
||||||
|
func GetAndReloadSymbols(data *map[string]models.TradeSet, symbolType int) error {
|
||||||
|
var tradeSets map[string]models.TradeSet
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch symbolType {
|
||||||
|
case 1:
|
||||||
|
tradeSets, _, err = GetSpotSymbols()
|
||||||
|
case 2:
|
||||||
|
tradeSets, _, err = GetFutSymbols()
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("无效的symbolType: %d", symbolType)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*data = tradeSets
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitSymbolsTicker24h 初始化24小时行情数据
|
||||||
|
// symbolType 1-现货 2-合约
|
||||||
|
func InitSymbolsTicker24h(data *map[string]models.TradeSet, symbolType int) ([]string, error) {
|
||||||
|
api := NewBitgetRestApi()
|
||||||
|
|
||||||
|
switch symbolType {
|
||||||
|
case 1:
|
||||||
|
return api.GetSpotTicker24h(data)
|
||||||
|
case 2:
|
||||||
|
return api.GetFutTicker24h(data)
|
||||||
|
default:
|
||||||
|
return []string{}, fmt.Errorf("无效的symbolType: %d", symbolType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSpotTicker24 获取单个交易对24h行情
|
||||||
|
func (api *BitgetRestApi) GetSpotTicker24(symbol string, data *models.Ticker24, tradeSet *models.TradeSet) error {
|
||||||
|
key := fmt.Sprintf(global.TICKER_SPOT, global.EXCHANGE_BITGET, symbol)
|
||||||
|
val, err := helper.DefaultRedis.GetString(key)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sonic.Unmarshal([]byte(val), tradeSet)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tradeSet.Coin != "" {
|
||||||
|
data.HighPrice = tradeSet.HighPrice
|
||||||
|
data.ChangePercent = fmt.Sprintf("%g", tradeSet.PriceChange)
|
||||||
|
data.LastPrice = tradeSet.LastPrice
|
||||||
|
data.LowPrice = tradeSet.LowPrice
|
||||||
|
data.OpenPrice = fmt.Sprintf("%g", tradeSet.OpenPrice)
|
||||||
|
data.QuoteVolume = tradeSet.QuoteVolume
|
||||||
|
data.Volume = tradeSet.Volume
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user