Compare commits
9 Commits
9ca1cd9a19
...
单边仓位_maste
| Author | SHA1 | Date | |
|---|---|---|---|
| 643eab3496 | |||
| 556a32cb7c | |||
| 7adba048c8 | |||
| 43f6b5dc4f | |||
| a9fc1c87f5 | |||
| 1512216bab | |||
| 8e8c78ec0b | |||
| cdd3f951a2 | |||
| 0b95e32655 |
@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
205
app/admin/apis/line_reduce_strategy.go
Normal file
205
app/admin/apis/line_reduce_strategy.go
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
package apis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
||||||
|
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
|
||||||
|
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/app/admin/service"
|
||||||
|
"go-admin/app/admin/service/dto"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReduceStrategy struct {
|
||||||
|
api.Api
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPage 获取减仓策略列表
|
||||||
|
// @Summary 获取减仓策略列表
|
||||||
|
// @Description 获取减仓策略列表
|
||||||
|
// @Tags 减仓策略
|
||||||
|
// @Param name query string false "减仓策略名称"
|
||||||
|
// @Param status query string false "状态 1-启用 2-禁用"
|
||||||
|
// @Param pageSize query int false "页条数"
|
||||||
|
// @Param pageIndex query int false "页码"
|
||||||
|
// @Success 200 {object} response.Response{data=response.Page{list=[]models.LineReduceStrategy}} "{"code": 200, "data": [...]}"
|
||||||
|
// @Router /api/v1/line-reduce-strategy [get]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReduceStrategy) GetPage(c *gin.Context) {
|
||||||
|
req := dto.LineReduceStrategyGetPageReq{}
|
||||||
|
s := service.LineReduceStrategy{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
list := make([]models.LineReduceStrategy, 0)
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
err = s.GetPage(&req, p, &list, &count)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("获取减仓策略失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取减仓策略
|
||||||
|
// @Summary 获取减仓策略
|
||||||
|
// @Description 获取减仓策略
|
||||||
|
// @Tags 减仓策略
|
||||||
|
// @Param id path int false "id"
|
||||||
|
// @Success 200 {object} response.Response{data=models.LineReduceStrategy} "{"code": 200, "data": [...]}"
|
||||||
|
// @Router /api/v1/line-reduce-strategy/{id} [get]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReduceStrategy) Get(c *gin.Context) {
|
||||||
|
req := dto.LineReduceStrategyGetReq{}
|
||||||
|
s := service.LineReduceStrategy{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var object models.LineReduceStrategy
|
||||||
|
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
err = s.Get(&req, p, &object)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("获取减仓策略失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.OK(object, "查询成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 创建减仓策略
|
||||||
|
// @Summary 创建减仓策略
|
||||||
|
// @Description 创建减仓策略
|
||||||
|
// @Tags 减仓策略
|
||||||
|
// @Accept application/json
|
||||||
|
// @Product application/json
|
||||||
|
// @Param data body dto.LineReduceStrategyInsertReq true "data"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}"
|
||||||
|
// @Router /api/v1/line-reduce-strategy [post]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReduceStrategy) Insert(c *gin.Context) {
|
||||||
|
req := dto.LineReduceStrategyInsertReq{}
|
||||||
|
s := service.LineReduceStrategy{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := req.Valid(); err != nil {
|
||||||
|
e.Error(500, err, "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置创建人
|
||||||
|
req.SetCreateBy(user.GetUserId(c))
|
||||||
|
|
||||||
|
err = s.Insert(&req)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("创建减仓策略失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.OK(req.GetId(), "创建成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 修改减仓策略
|
||||||
|
// @Summary 修改减仓策略
|
||||||
|
// @Description 修改减仓策略
|
||||||
|
// @Tags 减仓策略
|
||||||
|
// @Accept application/json
|
||||||
|
// @Product application/json
|
||||||
|
// @Param id path int true "id"
|
||||||
|
// @Param data body dto.LineReduceStrategyUpdateReq true "body"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
|
||||||
|
// @Router /api/v1/line-reduce-strategy/{id} [put]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReduceStrategy) Update(c *gin.Context) {
|
||||||
|
req := dto.LineReduceStrategyUpdateReq{}
|
||||||
|
s := service.LineReduceStrategy{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := req.Valid(); err != nil {
|
||||||
|
e.Error(500, err, "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.SetUpdateBy(user.GetUserId(c))
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
|
||||||
|
err = s.Update(&req, p)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("修改减仓策略失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.OK(req.GetId(), "修改成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除减仓策略
|
||||||
|
// @Summary 删除减仓策略
|
||||||
|
// @Description 删除减仓策略
|
||||||
|
// @Tags 减仓策略
|
||||||
|
// @Param data body dto.LineReduceStrategyDeleteReq true "body"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}"
|
||||||
|
// @Router /api/v1/line-reduce-strategy [delete]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReduceStrategy) Delete(c *gin.Context) {
|
||||||
|
s := service.LineReduceStrategy{}
|
||||||
|
req := dto.LineReduceStrategyDeleteReq{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// req.SetUpdateBy(user.GetUserId(c))
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
|
||||||
|
err = s.Remove(&req, p)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("删除减仓策略失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.OK(req.GetId(), "删除成功")
|
||||||
|
}
|
||||||
191
app/admin/apis/line_reduce_strategy_item.go
Normal file
191
app/admin/apis/line_reduce_strategy_item.go
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
package apis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
||||||
|
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
|
||||||
|
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/app/admin/service"
|
||||||
|
"go-admin/app/admin/service/dto"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReduceStrategyItem struct {
|
||||||
|
api.Api
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPage 获取减仓策略明细列表
|
||||||
|
// @Summary 获取减仓策略明细列表
|
||||||
|
// @Description 获取减仓策略明细列表
|
||||||
|
// @Tags 减仓策略明细
|
||||||
|
// @Param pageSize query int false "页条数"
|
||||||
|
// @Param pageIndex query int false "页码"
|
||||||
|
// @Success 200 {object} response.Response{data=response.Page{list=[]models.LineReduceStrategyItem}} "{"code": 200, "data": [...]}"
|
||||||
|
// @Router /api/v1/line-reduce-strategy-item [get]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReduceStrategyItem) GetPage(c *gin.Context) {
|
||||||
|
req := dto.LineReduceStrategyItemGetPageReq{}
|
||||||
|
s := service.LineReduceStrategyItem{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
list := make([]models.LineReduceStrategyItem, 0)
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
err = s.GetPage(&req, p, &list, &count)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("获取减仓策略明细失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取减仓策略明细
|
||||||
|
// @Summary 获取减仓策略明细
|
||||||
|
// @Description 获取减仓策略明细
|
||||||
|
// @Tags 减仓策略明细
|
||||||
|
// @Param id path int false "id"
|
||||||
|
// @Success 200 {object} response.Response{data=models.LineReduceStrategyItem} "{"code": 200, "data": [...]}"
|
||||||
|
// @Router /api/v1/line-reduce-strategy-item/{id} [get]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReduceStrategyItem) Get(c *gin.Context) {
|
||||||
|
req := dto.LineReduceStrategyItemGetReq{}
|
||||||
|
s := service.LineReduceStrategyItem{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var object models.LineReduceStrategyItem
|
||||||
|
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
err = s.Get(&req, p, &object)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("获取减仓策略明细失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.OK( object, "查询成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 创建减仓策略明细
|
||||||
|
// @Summary 创建减仓策略明细
|
||||||
|
// @Description 创建减仓策略明细
|
||||||
|
// @Tags 减仓策略明细
|
||||||
|
// @Accept application/json
|
||||||
|
// @Product application/json
|
||||||
|
// @Param data body dto.LineReduceStrategyItemInsertReq true "data"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}"
|
||||||
|
// @Router /api/v1/line-reduce-strategy-item [post]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReduceStrategyItem) Insert(c *gin.Context) {
|
||||||
|
req := dto.LineReduceStrategyItemInsertReq{}
|
||||||
|
s := service.LineReduceStrategyItem{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 设置创建人
|
||||||
|
req.SetCreateBy(user.GetUserId(c))
|
||||||
|
|
||||||
|
err = s.Insert(&req)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("创建减仓策略明细失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.OK(req.GetId(), "创建成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 修改减仓策略明细
|
||||||
|
// @Summary 修改减仓策略明细
|
||||||
|
// @Description 修改减仓策略明细
|
||||||
|
// @Tags 减仓策略明细
|
||||||
|
// @Accept application/json
|
||||||
|
// @Product application/json
|
||||||
|
// @Param id path int true "id"
|
||||||
|
// @Param data body dto.LineReduceStrategyItemUpdateReq true "body"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
|
||||||
|
// @Router /api/v1/line-reduce-strategy-item/{id} [put]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReduceStrategyItem) Update(c *gin.Context) {
|
||||||
|
req := dto.LineReduceStrategyItemUpdateReq{}
|
||||||
|
s := service.LineReduceStrategyItem{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.SetUpdateBy(user.GetUserId(c))
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
|
||||||
|
err = s.Update(&req, p)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("修改减仓策略明细失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.OK( req.GetId(), "修改成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除减仓策略明细
|
||||||
|
// @Summary 删除减仓策略明细
|
||||||
|
// @Description 删除减仓策略明细
|
||||||
|
// @Tags 减仓策略明细
|
||||||
|
// @Param data body dto.LineReduceStrategyItemDeleteReq true "body"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}"
|
||||||
|
// @Router /api/v1/line-reduce-strategy-item [delete]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineReduceStrategyItem) Delete(c *gin.Context) {
|
||||||
|
s := service.LineReduceStrategyItem{}
|
||||||
|
req := dto.LineReduceStrategyItemDeleteReq{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// req.SetUpdateBy(user.GetUserId(c))
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
|
||||||
|
err = s.Remove(&req, p)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("删除减仓策略明细失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.OK( req.GetId(), "删除成功")
|
||||||
|
}
|
||||||
206
app/admin/apis/line_strategy_template.go
Normal file
206
app/admin/apis/line_strategy_template.go
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
package apis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
||||||
|
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
|
||||||
|
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/app/admin/service"
|
||||||
|
"go-admin/app/admin/service/dto"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineStrategyTemplate struct {
|
||||||
|
api.Api
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPage 获取波段策略列表
|
||||||
|
// @Summary 获取波段策略列表
|
||||||
|
// @Description 获取波段策略列表
|
||||||
|
// @Tags 波段策略
|
||||||
|
// @Param direction query int false "涨跌方向 1-涨 2-跌"
|
||||||
|
// @Param percentag query decimal.Decimal false "涨跌点数"
|
||||||
|
// @Param compareType query int false "比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "
|
||||||
|
// @Param pageSize query int false "页条数"
|
||||||
|
// @Param pageIndex query int false "页码"
|
||||||
|
// @Success 200 {object} response.Response{data=response.Page{list=[]models.LineStrategyTemplate}} "{"code": 200, "data": [...]}"
|
||||||
|
// @Router /api/v1/line-strategy-template [get]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineStrategyTemplate) GetPage(c *gin.Context) {
|
||||||
|
req := dto.LineStrategyTemplateGetPageReq{}
|
||||||
|
s := service.LineStrategyTemplate{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
list := make([]models.LineStrategyTemplate, 0)
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
err = s.GetPage(&req, p, &list, &count)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("获取波段策略失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取波段策略
|
||||||
|
// @Summary 获取波段策略
|
||||||
|
// @Description 获取波段策略
|
||||||
|
// @Tags 波段策略
|
||||||
|
// @Param id path int false "id"
|
||||||
|
// @Success 200 {object} response.Response{data=models.LineStrategyTemplate} "{"code": 200, "data": [...]}"
|
||||||
|
// @Router /api/v1/line-strategy-template/{id} [get]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineStrategyTemplate) Get(c *gin.Context) {
|
||||||
|
req := dto.LineStrategyTemplateGetReq{}
|
||||||
|
s := service.LineStrategyTemplate{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var object models.LineStrategyTemplate
|
||||||
|
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
err = s.Get(&req, p, &object)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("获取波段策略失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.OK(object, "查询成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 创建波段策略
|
||||||
|
// @Summary 创建波段策略
|
||||||
|
// @Description 创建波段策略
|
||||||
|
// @Tags 波段策略
|
||||||
|
// @Accept application/json
|
||||||
|
// @Product application/json
|
||||||
|
// @Param data body dto.LineStrategyTemplateInsertReq true "data"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}"
|
||||||
|
// @Router /api/v1/line-strategy-template [post]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineStrategyTemplate) Insert(c *gin.Context) {
|
||||||
|
req := dto.LineStrategyTemplateInsertReq{}
|
||||||
|
s := service.LineStrategyTemplate{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := req.Valid(); err != nil {
|
||||||
|
e.Error(500, err, "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置创建人
|
||||||
|
req.SetCreateBy(user.GetUserId(c))
|
||||||
|
|
||||||
|
err = s.Insert(&req)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("创建波段策略失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.OK(req.GetId(), "创建成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 修改波段策略
|
||||||
|
// @Summary 修改波段策略
|
||||||
|
// @Description 修改波段策略
|
||||||
|
// @Tags 波段策略
|
||||||
|
// @Accept application/json
|
||||||
|
// @Product application/json
|
||||||
|
// @Param id path int true "id"
|
||||||
|
// @Param data body dto.LineStrategyTemplateUpdateReq true "body"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
|
||||||
|
// @Router /api/v1/line-strategy-template/{id} [put]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineStrategyTemplate) Update(c *gin.Context) {
|
||||||
|
req := dto.LineStrategyTemplateUpdateReq{}
|
||||||
|
s := service.LineStrategyTemplate{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := req.Valid(); err != nil {
|
||||||
|
e.Error(500, err, "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
req.SetUpdateBy(user.GetUserId(c))
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
|
||||||
|
err = s.Update(&req, p)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("修改波段策略失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.OK(req.GetId(), "修改成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除波段策略
|
||||||
|
// @Summary 删除波段策略
|
||||||
|
// @Description 删除波段策略
|
||||||
|
// @Tags 波段策略
|
||||||
|
// @Param data body dto.LineStrategyTemplateDeleteReq true "body"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}"
|
||||||
|
// @Router /api/v1/line-strategy-template [delete]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineStrategyTemplate) Delete(c *gin.Context) {
|
||||||
|
s := service.LineStrategyTemplate{}
|
||||||
|
req := dto.LineStrategyTemplateDeleteReq{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// req.SetUpdateBy(user.GetUserId(c))
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
|
||||||
|
err = s.Remove(&req, p)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("删除波段策略失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.OK(req.GetId(), "删除成功")
|
||||||
|
}
|
||||||
@ -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) {
|
||||||
|
|||||||
193
app/admin/apis/line_symbol_price.go
Normal file
193
app/admin/apis/line_symbol_price.go
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
package apis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/api"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
|
||||||
|
_ "github.com/go-admin-team/go-admin-core/sdk/pkg/response"
|
||||||
|
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/app/admin/service"
|
||||||
|
"go-admin/app/admin/service/dto"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineSymbolPrice struct {
|
||||||
|
api.Api
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPage 获取缓存价格交易对列表
|
||||||
|
// @Summary 获取缓存价格交易对列表
|
||||||
|
// @Description 获取缓存价格交易对列表
|
||||||
|
// @Tags 缓存价格交易对
|
||||||
|
// @Param symbol query string false "交易对"
|
||||||
|
// @Param status query int false "状态 1-启用 2-禁用"
|
||||||
|
// @Param pageSize query int false "页条数"
|
||||||
|
// @Param pageIndex query int false "页码"
|
||||||
|
// @Success 200 {object} response.Response{data=response.Page{list=[]models.LineSymbolPrice}} "{"code": 200, "data": [...]}"
|
||||||
|
// @Router /api/v1/line-symbol-price [get]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineSymbolPrice) GetPage(c *gin.Context) {
|
||||||
|
req := dto.LineSymbolPriceGetPageReq{}
|
||||||
|
s := service.LineSymbolPrice{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
list := make([]models.LineSymbolPrice, 0)
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
err = s.GetPage(&req, p, &list, &count)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("获取缓存价格交易对失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.PageOK(list, int(count), req.GetPageIndex(), req.GetPageSize(), "查询成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取缓存价格交易对
|
||||||
|
// @Summary 获取缓存价格交易对
|
||||||
|
// @Description 获取缓存价格交易对
|
||||||
|
// @Tags 缓存价格交易对
|
||||||
|
// @Param id path int false "id"
|
||||||
|
// @Success 200 {object} response.Response{data=models.LineSymbolPrice} "{"code": 200, "data": [...]}"
|
||||||
|
// @Router /api/v1/line-symbol-price/{id} [get]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineSymbolPrice) Get(c *gin.Context) {
|
||||||
|
req := dto.LineSymbolPriceGetReq{}
|
||||||
|
s := service.LineSymbolPrice{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var object models.LineSymbolPrice
|
||||||
|
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
err = s.Get(&req, p, &object)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("获取缓存价格交易对失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.OK( object, "查询成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 创建缓存价格交易对
|
||||||
|
// @Summary 创建缓存价格交易对
|
||||||
|
// @Description 创建缓存价格交易对
|
||||||
|
// @Tags 缓存价格交易对
|
||||||
|
// @Accept application/json
|
||||||
|
// @Product application/json
|
||||||
|
// @Param data body dto.LineSymbolPriceInsertReq true "data"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "添加成功"}"
|
||||||
|
// @Router /api/v1/line-symbol-price [post]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineSymbolPrice) Insert(c *gin.Context) {
|
||||||
|
req := dto.LineSymbolPriceInsertReq{}
|
||||||
|
s := service.LineSymbolPrice{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 设置创建人
|
||||||
|
req.SetCreateBy(user.GetUserId(c))
|
||||||
|
|
||||||
|
err = s.Insert(&req)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("创建缓存价格交易对失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.OK(req.GetId(), "创建成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 修改缓存价格交易对
|
||||||
|
// @Summary 修改缓存价格交易对
|
||||||
|
// @Description 修改缓存价格交易对
|
||||||
|
// @Tags 缓存价格交易对
|
||||||
|
// @Accept application/json
|
||||||
|
// @Product application/json
|
||||||
|
// @Param id path int true "id"
|
||||||
|
// @Param data body dto.LineSymbolPriceUpdateReq true "body"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "修改成功"}"
|
||||||
|
// @Router /api/v1/line-symbol-price/{id} [put]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineSymbolPrice) Update(c *gin.Context) {
|
||||||
|
req := dto.LineSymbolPriceUpdateReq{}
|
||||||
|
s := service.LineSymbolPrice{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.SetUpdateBy(user.GetUserId(c))
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
|
||||||
|
err = s.Update(&req, p)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("修改缓存价格交易对失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.OK( req.GetId(), "修改成功")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除缓存价格交易对
|
||||||
|
// @Summary 删除缓存价格交易对
|
||||||
|
// @Description 删除缓存价格交易对
|
||||||
|
// @Tags 缓存价格交易对
|
||||||
|
// @Param data body dto.LineSymbolPriceDeleteReq true "body"
|
||||||
|
// @Success 200 {object} response.Response "{"code": 200, "message": "删除成功"}"
|
||||||
|
// @Router /api/v1/line-symbol-price [delete]
|
||||||
|
// @Security Bearer
|
||||||
|
func (e LineSymbolPrice) Delete(c *gin.Context) {
|
||||||
|
s := service.LineSymbolPrice{}
|
||||||
|
req := dto.LineSymbolPriceDeleteReq{}
|
||||||
|
err := e.MakeContext(c).
|
||||||
|
MakeOrm().
|
||||||
|
Bind(&req).
|
||||||
|
MakeService(&s.Service).
|
||||||
|
Errors
|
||||||
|
if err != nil {
|
||||||
|
e.Logger.Error(err)
|
||||||
|
e.Error(500, err, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// req.SetUpdateBy(user.GetUserId(c))
|
||||||
|
p := actions.GetPermissionFromContext(c)
|
||||||
|
|
||||||
|
err = s.Remove(&req, p)
|
||||||
|
if err != nil {
|
||||||
|
e.Error(500, err, fmt.Sprintf("删除缓存价格交易对失败,\r\n失败信息 %s", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.OK( req.GetId(), "删除成功")
|
||||||
|
}
|
||||||
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ type LineApiUser struct {
|
|||||||
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"`
|
AdminId int64 `json:"adminId" gorm:"type:int unsigned;comment:管理员id"`
|
||||||
|
|||||||
27
app/admin/models/line_order_reduce_strategy.go
Normal file
27
app/admin/models/line_order_reduce_strategy.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import "go-admin/common/models"
|
||||||
|
|
||||||
|
type LineOrderReduceStrategy struct {
|
||||||
|
models.Model
|
||||||
|
|
||||||
|
OrderId int `json:"orderId" gorm:"type:bigint;not null;comment:订单ID"`
|
||||||
|
ReduceStrategyId int `json:"reduceStrategyId" gorm:"type:bigint;not null;comment:减仓策略ID"`
|
||||||
|
ItemContent string `json:"itemContent" gorm:"type:text;not null;comment:策略明细json"`
|
||||||
|
Actived int `json:"actived" gorm:"type:tinyint;comment:"是否已减仓 1=已减仓 2=未减仓""`
|
||||||
|
models.ModelTime
|
||||||
|
models.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LineOrderReduceStrategy) TableName() string {
|
||||||
|
return "line_order_reduce_strategy"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineOrderReduceStrategy) Generate() models.ActiveRecord {
|
||||||
|
o := *e
|
||||||
|
return &o
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineOrderReduceStrategy) GetId() interface{} {
|
||||||
|
return e.Id
|
||||||
|
}
|
||||||
@ -9,38 +9,41 @@ import (
|
|||||||
|
|
||||||
type LinePreOrder struct {
|
type LinePreOrder struct {
|
||||||
models.Model
|
models.Model
|
||||||
ExchangeType string `json:"exchangeType" gorm:"type:varchar(20);comment:交易所类型 (字典 exchange_type)"`
|
ExchangeType string `json:"exchangeType" gorm:"type:varchar(20);comment:交易所类型 (字典 exchange_type)"`
|
||||||
Pid int `json:"pid" gorm:"type:int unsigned;omitempty;comment:pid"`
|
StrategyTemplateType int `json:"strategyTemplateType" gorm:"type:tinyint;comment:策略类型 0-无 1-波段涨跌幅"`
|
||||||
MainId int `json:"mainId" gorm:"type:int;comment:主单id"`
|
Pid int `json:"pid" gorm:"type:int unsigned;omitempty;comment:pid"`
|
||||||
ApiId int `json:"apiId" gorm:"type:varchar(255);omitempty;comment:api用户"`
|
MainId int `json:"mainId" gorm:"type:int;comment:主单id"`
|
||||||
GroupId string `json:"groupId" gorm:"type:int unsigned;omitempty;comment:交易对组id"`
|
ApiId int `json:"apiId" gorm:"type:varchar(255);omitempty;comment:api用户"`
|
||||||
Symbol string `json:"symbol" gorm:"type:varchar(255);omitempty;comment:交易对"`
|
GroupId string `json:"groupId" gorm:"type:int unsigned;omitempty;comment:交易对组id"`
|
||||||
QuoteSymbol string `json:"quoteSymbol" gorm:"type:varchar(255);omitempty;comment:计较货币"`
|
Symbol string `json:"symbol" gorm:"type:varchar(255);omitempty;comment:交易对"`
|
||||||
SignPrice string `json:"signPrice" gorm:"type:decimal(18,8);omitempty;comment:对标价"`
|
QuoteSymbol string `json:"quoteSymbol" gorm:"type:varchar(255);omitempty;comment:计较货币"`
|
||||||
SignPriceU decimal.Decimal `json:"signPriceU" gorm:"type:decimal(18,8);omitempty;comment:交易对对标U的行情价"`
|
SignPrice string `json:"signPrice" gorm:"type:decimal(18,8);omitempty;comment:对标价"`
|
||||||
SignPriceType string `json:"signPriceType" gorm:"type:enum('new','tall','low','mixture','entrust','add');omitempty;comment:对标价类型: new=最新价格;tall=24小时最高;low=24小时最低;mixture=标记价;entrust=委托实价;add=补仓"`
|
SignPriceU decimal.Decimal `json:"signPriceU" gorm:"type:decimal(18,8);omitempty;comment:交易对对标U的行情价"`
|
||||||
Rate string `json:"rate" gorm:"type:decimal(18,2);omitempty;comment:下单百分比"`
|
SignPriceType string `json:"signPriceType" gorm:"type:enum('new','tall','low','mixture','entrust','add');omitempty;comment:对标价类型: new=最新价格;tall=24小时最高;low=24小时最低;mixture=标记价;entrust=委托实价;add=补仓"`
|
||||||
Price string `json:"price" gorm:"type:decimal(18,8);omitempty;comment:触发价格"`
|
Rate string `json:"rate" gorm:"type:decimal(18,2);omitempty;comment:下单百分比"`
|
||||||
Num string `json:"num" gorm:"type:decimal(18,8);omitempty;comment:购买数量"`
|
Price string `json:"price" gorm:"type:decimal(18,8);omitempty;comment:触发价格"`
|
||||||
BuyPrice string `json:"buyPrice" gorm:"type:decimal(18,8);omitempty;comment:购买金额"`
|
Num string `json:"num" gorm:"type:decimal(18,8);omitempty;comment:购买数量"`
|
||||||
SymbolType int `json:"symbolType" gorm:"type:int;comment:交易对类型:1=现货;2=合约"`
|
BuyPrice string `json:"buyPrice" gorm:"type:decimal(18,8);omitempty;comment:购买金额"`
|
||||||
OrderCategory int `json:"orderCategory" gorm:"type:int;comment:订单类型: 1=主单 2=对冲单 3-加仓单"`
|
SymbolType int `json:"symbolType" gorm:"type:int;comment:交易对类型:1=现货;2=合约"`
|
||||||
Site string `json:"site" gorm:"type:enum('BUY','SELL');omitempty;comment:购买方向:BUY=买;SELL=卖"`
|
OrderCategory int `json:"orderCategory" gorm:"type:int;comment:订单类型: 1=主单 2=对冲单 3-加仓单"`
|
||||||
OrderSn string `json:"orderSn" gorm:"type:varchar(255);omitempty;comment:订单号"`
|
Site string `json:"site" gorm:"type:enum('BUY','SELL');omitempty;comment:购买方向:BUY=买;SELL=卖"`
|
||||||
OrderType int `json:"orderType" gorm:"type:int;omitempty;comment:订单类型:0=主单 1=止盈 2=止损 3=平仓 4=减仓"`
|
OrderSn string `json:"orderSn" gorm:"type:varchar(255);omitempty;comment:订单号"`
|
||||||
Desc string `json:"desc" gorm:"type:text;omitempty;comment:订单描述"`
|
OrderType int `json:"orderType" gorm:"type:int;omitempty;comment:订单类型:0=主单 1=止盈 2=止损 3=平仓 4=减仓"`
|
||||||
Status int `json:"status" gorm:"type:int;omitempty;comment:是否被触发:0=待触发;1=已触发;2=下单失败;4=已取消;5=委托中;6=已成交;9=已平仓"`
|
Desc string `json:"desc" gorm:"type:text;omitempty;comment:订单描述"`
|
||||||
CoverType int `json:"coverType" gorm:"type:int unsigned;omitempty;comment:对冲类型 1= 现货对合约 2=合约对合约 3 合约对现货"`
|
Status int `json:"status" gorm:"type:int;omitempty;comment:是否被触发:0=待触发;1=已触发;2=下单失败;4=已取消;5=委托中;6=已成交;9=已平仓"`
|
||||||
ExpireTime time.Time `json:"expireTime" gorm:"comment:过期时间"`
|
CoverType int `json:"coverType" gorm:"type:int unsigned;omitempty;comment:对冲类型 1= 现货对合约 2=合约对合约 3 合约对现货"`
|
||||||
MainOrderType string `json:"mainOrderType" gorm:"type:enum;comment:第一笔(主单类型) 限价(LIMIT)市价(MARKET)"`
|
ExpireTime time.Time `json:"expireTime" gorm:"comment:过期时间"`
|
||||||
LossAmount decimal.Decimal `json:"lossAmount" gorm:"type:decimal(18,8);comment:亏损金额(U)"`
|
MainOrderType string `json:"mainOrderType" gorm:"type:enum;comment:第一笔(主单类型) 限价(LIMIT)市价(MARKET)"`
|
||||||
TriggerTime *time.Time `json:"triggerTime" gorm:"type:datetime;comment:触发时间"`
|
LossAmount decimal.Decimal `json:"lossAmount" gorm:"type:decimal(18,8);comment:亏损金额(U)"`
|
||||||
Child []LinePreOrder `json:"child" gorm:"-"`
|
TriggerTime *time.Time `json:"triggerTime" gorm:"type:datetime;comment:触发时间"`
|
||||||
ApiName string `json:"api_name" gorm:"->"`
|
Child []LinePreOrder `json:"child" gorm:"-"`
|
||||||
ChildNum int64 `json:"child_num" gorm:"->"`
|
ApiName string `json:"api_name" gorm:"->"`
|
||||||
AddPositionStatus int `json:"add_position_status" gorm:"->"`
|
ChildNum int64 `json:"child_num" gorm:"->"`
|
||||||
ReduceStatus int `json:"reduce_status" gorm:"->"`
|
AddPositionStatus int `json:"add_position_status" gorm:"->"`
|
||||||
Childs []LinePreOrder `json:"childs" gorm:"foreignKey:pid;references:id"`
|
ReduceStatus int `json:"reduce_status" gorm:"->"`
|
||||||
|
Childs []LinePreOrder `json:"childs" gorm:"foreignKey:pid;references:id"`
|
||||||
|
ReduceOrderId int `json:"reduceOrderId" gorm:"type:bigint;comment:主减仓单id"`
|
||||||
|
// OrderReduceStrategy LineOrderReduceStrategy `json:"-" gorm:"foreignKey:order_id;references:id"`
|
||||||
// LinePreOrder 线上预埋单\
|
// LinePreOrder 线上预埋单\
|
||||||
models.ModelTime
|
models.ModelTime
|
||||||
models.ControlBy
|
models.ControlBy
|
||||||
|
|||||||
28
app/admin/models/line_reduce_strategy.go
Normal file
28
app/admin/models/line_reduce_strategy.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go-admin/common/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReduceStrategy struct {
|
||||||
|
models.Model
|
||||||
|
|
||||||
|
Name string `json:"name" gorm:"type:varchar(50);comment:减仓策略名称"`
|
||||||
|
Status int `json:"status" gorm:"type:tinyint;comment:状态 1-启用 2-禁用"`
|
||||||
|
Items []LineReduceStrategyItem `json:"items" gorm:"foreignKey:ReduceStrategyId;references:Id"`
|
||||||
|
models.ModelTime
|
||||||
|
models.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LineReduceStrategy) TableName() string {
|
||||||
|
return "line_reduce_strategy"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineReduceStrategy) Generate() models.ActiveRecord {
|
||||||
|
o := *e
|
||||||
|
return &o
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineReduceStrategy) GetId() interface{} {
|
||||||
|
return e.Id
|
||||||
|
}
|
||||||
32
app/admin/models/line_reduce_strategy_item.go
Normal file
32
app/admin/models/line_reduce_strategy_item.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go-admin/common/models"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReduceStrategyItem struct {
|
||||||
|
models.Model
|
||||||
|
|
||||||
|
ReduceStrategyId int `json:"reduceStrategyId" gorm:"type:bigint;comment:减仓策略id"`
|
||||||
|
LossPercent decimal.Decimal `json:"lossPercent" gorm:"type:decimal(10,2);comment:亏损百分比"`
|
||||||
|
QuantityPercent decimal.Decimal `json:"quantityPercent" gorm:"type:decimal(10,2);comment:减仓数量百分比"`
|
||||||
|
OrderType string `json:"orderType" gorm:"type:varchar(20);comment:订单类型 LIMIT-限价 MARKET-市价"`
|
||||||
|
ReduceStrategy LineReduceStrategy `json:"reduceStrategy" gorm:"foreignKey:ReduceStrategyId;"`
|
||||||
|
models.ModelTime
|
||||||
|
models.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LineReduceStrategyItem) TableName() string {
|
||||||
|
return "line_reduce_strategy_item"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineReduceStrategyItem) Generate() models.ActiveRecord {
|
||||||
|
o := *e
|
||||||
|
return &o
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineReduceStrategyItem) GetId() interface{} {
|
||||||
|
return e.Id
|
||||||
|
}
|
||||||
33
app/admin/models/line_strategy_template.go
Normal file
33
app/admin/models/line_strategy_template.go
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go-admin/common/models"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineStrategyTemplate struct {
|
||||||
|
models.Model
|
||||||
|
|
||||||
|
Name string `json:"name" gorm:"type:varchar(50);comment:策略名称"`
|
||||||
|
Direction int `json:"direction" gorm:"type:tinyint;comment:涨跌方向 1-涨 2-跌"`
|
||||||
|
Percentag decimal.Decimal `json:"percentag" gorm:"type:decimal(10,2);comment:涨跌点数"`
|
||||||
|
CompareType int `json:"compareType" gorm:"type:tinyint;comment:比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
|
||||||
|
TimeSlotStart int `json:"timeSlotStart" gorm:"type:int;comment:时间段开始(分)"`
|
||||||
|
TimeSlotEnd int `json:"timeSlotEnd" gorm:"type:int;comment:时间断截至(分)"`
|
||||||
|
models.ModelTime
|
||||||
|
models.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LineStrategyTemplate) TableName() string {
|
||||||
|
return "line_strategy_template"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineStrategyTemplate) Generate() models.ActiveRecord {
|
||||||
|
o := *e
|
||||||
|
return &o
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineStrategyTemplate) GetId() interface{} {
|
||||||
|
return e.Id
|
||||||
|
}
|
||||||
29
app/admin/models/line_symbol_price.go
Normal file
29
app/admin/models/line_symbol_price.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
|
||||||
|
"go-admin/common/models"
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineSymbolPrice struct {
|
||||||
|
models.Model
|
||||||
|
|
||||||
|
Symbol string `json:"symbol" gorm:"type:varchar(15);comment:交易对"`
|
||||||
|
Status int `json:"status" gorm:"type:tinyint;comment:状态 1-启用 2-禁用"`
|
||||||
|
models.ModelTime
|
||||||
|
models.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (LineSymbolPrice) TableName() string {
|
||||||
|
return "line_symbol_price"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineSymbolPrice) Generate() models.ActiveRecord {
|
||||||
|
o := *e
|
||||||
|
return &o
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineSymbolPrice) GetId() interface{} {
|
||||||
|
return e.Id
|
||||||
|
}
|
||||||
@ -9,13 +9,14 @@ import (
|
|||||||
type LineSystemSetting struct {
|
type LineSystemSetting struct {
|
||||||
models.Model
|
models.Model
|
||||||
|
|
||||||
Time int64 `json:"time" gorm:"type:int;comment:导入:挂单时长达到时间后失效"`
|
Time int64 `json:"time" gorm:"type:int;comment:导入:挂单时长达到时间后失效"`
|
||||||
BatchTime int64 `json:"batchTime" gorm:"type:int;comment:批量:挂单时长达到时间后失效"`
|
BatchTime int64 `json:"batchTime" gorm:"type:int;comment:批量:挂单时长达到时间后失效"`
|
||||||
ProfitRate string `json:"profitRate" gorm:"type:decimal(10,2);comment:平仓盈利比例"`
|
ProfitRate string `json:"profitRate" gorm:"type:decimal(10,2);comment:平仓盈利比例"`
|
||||||
CoverOrderTypeBRate string `json:"coverOrderTypeBRate" gorm:"type:decimal(10,2);comment:b账户限价补单的买入百分比"`
|
CoverOrderTypeBRate string `json:"coverOrderTypeBRate" gorm:"type:decimal(10,2);comment:b账户限价补单的买入百分比"`
|
||||||
StopLossPremium decimal.Decimal `json:"stopLossPremium" gorm:"type:decimal(10,2);comment:限价止损溢价百分比"`
|
StopLossPremium decimal.Decimal `json:"stopLossPremium" gorm:"type:decimal(10,2);comment:限价止损溢价百分比"`
|
||||||
AddPositionPremium decimal.Decimal `json:"addPositionPremium" gorm:"type:decimal(10,2);comment:限价加仓溢价百分比`
|
AddPositionPremium decimal.Decimal `json:"addPositionPremium" gorm:"type:decimal(10,2);comment:限价加仓溢价百分比`
|
||||||
ReducePremium decimal.Decimal `json:"reducePremium" gorm:"type:decimal(10,2);comment:限价减仓溢价百分比"`
|
ReducePremium decimal.Decimal `json:"reducePremium" gorm:"type:decimal(10,2);comment:限价减仓溢价百分比"`
|
||||||
|
ReduceEarlyTriggerPercent decimal.Decimal `json:"reduceEarlyTriggerPercent" gorm:"type:decimal(10,2);comment:减仓提前触发百分比"`
|
||||||
models.ModelTime
|
models.ModelTime
|
||||||
models.ControlBy
|
models.ControlBy
|
||||||
}
|
}
|
||||||
|
|||||||
27
app/admin/router/line_reduce_strategy.go
Normal file
27
app/admin/router/line_reduce_strategy.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||||
|
|
||||||
|
"go-admin/app/admin/apis"
|
||||||
|
"go-admin/common/middleware"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
routerCheckRole = append(routerCheckRole, registerLineReduceStrategyRouter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerLineReduceStrategyRouter
|
||||||
|
func registerLineReduceStrategyRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
|
||||||
|
api := apis.LineReduceStrategy{}
|
||||||
|
r := v1.Group("/line-reduce-strategy").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
|
||||||
|
{
|
||||||
|
r.GET("", actions.PermissionAction(), api.GetPage)
|
||||||
|
r.GET("/:id", actions.PermissionAction(), api.Get)
|
||||||
|
r.POST("", api.Insert)
|
||||||
|
r.PUT("/:id", actions.PermissionAction(), api.Update)
|
||||||
|
r.DELETE("", api.Delete)
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/admin/router/line_reduce_strategy_item.go
Normal file
27
app/admin/router/line_reduce_strategy_item.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||||
|
|
||||||
|
"go-admin/app/admin/apis"
|
||||||
|
"go-admin/common/middleware"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
routerCheckRole = append(routerCheckRole, registerLineReduceStrategyItemRouter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerLineReduceStrategyItemRouter
|
||||||
|
func registerLineReduceStrategyItemRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
|
||||||
|
api := apis.LineReduceStrategyItem{}
|
||||||
|
r := v1.Group("/line-reduce-strategy-item").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
|
||||||
|
{
|
||||||
|
r.GET("", actions.PermissionAction(), api.GetPage)
|
||||||
|
r.GET("/:id", actions.PermissionAction(), api.Get)
|
||||||
|
r.POST("", api.Insert)
|
||||||
|
r.PUT("/:id", actions.PermissionAction(), api.Update)
|
||||||
|
r.DELETE("", api.Delete)
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/admin/router/line_strategy_template.go
Normal file
27
app/admin/router/line_strategy_template.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||||
|
|
||||||
|
"go-admin/app/admin/apis"
|
||||||
|
"go-admin/common/middleware"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
routerCheckRole = append(routerCheckRole, registerLineStrategyTemplateRouter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerLineStrategyTemplateRouter
|
||||||
|
func registerLineStrategyTemplateRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
|
||||||
|
api := apis.LineStrategyTemplate{}
|
||||||
|
r := v1.Group("/line-strategy-template").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
|
||||||
|
{
|
||||||
|
r.GET("", actions.PermissionAction(), api.GetPage)
|
||||||
|
r.GET("/:id", actions.PermissionAction(), api.Get)
|
||||||
|
r.POST("", api.Insert)
|
||||||
|
r.PUT("/:id", actions.PermissionAction(), api.Update)
|
||||||
|
r.DELETE("", api.Delete)
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/admin/router/line_symbol_price.go
Normal file
27
app/admin/router/line_symbol_price.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
jwt "github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth"
|
||||||
|
|
||||||
|
"go-admin/app/admin/apis"
|
||||||
|
"go-admin/common/middleware"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
routerCheckRole = append(routerCheckRole, registerLineSymbolPriceRouter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// registerLineSymbolPriceRouter
|
||||||
|
func registerLineSymbolPriceRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
|
||||||
|
api := apis.LineSymbolPrice{}
|
||||||
|
r := v1.Group("/line-symbol-price").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
|
||||||
|
{
|
||||||
|
r.GET("", actions.PermissionAction(), api.GetPage)
|
||||||
|
r.GET("/:id", actions.PermissionAction(), api.Get)
|
||||||
|
r.POST("", api.Insert)
|
||||||
|
r.PUT("/:id", actions.PermissionAction(), api.Update)
|
||||||
|
r.DELETE("", api.Delete)
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/admin/service/dto/line_order_reduce_strategy.go
Normal file
21
app/admin/service/dto/line_order_reduce_strategy.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import "github.com/shopspring/decimal"
|
||||||
|
|
||||||
|
type LineOrderReduceStrategyResp struct {
|
||||||
|
MainId int `json:"mainId" comment:"主单id"`
|
||||||
|
OrderId int `json:"orderId" comment:"减仓单id"`
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Side string `json:"side" comment:"BUY SELL"`
|
||||||
|
Items []LineOrderReduceStrategyRespItem `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 减仓节点
|
||||||
|
type LineOrderReduceStrategyRespItem struct {
|
||||||
|
Price decimal.Decimal `json:"p" comment:"下单价"`
|
||||||
|
Num decimal.Decimal `json:"n" comment:"下单数量"`
|
||||||
|
TriggerPrice decimal.Decimal `json:"t" comment:"触发价"`
|
||||||
|
LossPercent decimal.Decimal `json:"l" comment:"亏损百分比"`
|
||||||
|
OrderType string `json:"o" comment:"订单类型 LIMIT-限价 MARKET-市价"`
|
||||||
|
Actived bool `json:"a" comment:"是否触发 "`
|
||||||
|
}
|
||||||
@ -186,6 +186,9 @@ func (s *LinePreOrderDeleteReq) GetId() interface{} {
|
|||||||
|
|
||||||
type LineAddPreOrderReq struct {
|
type LineAddPreOrderReq struct {
|
||||||
ExchangeType string `json:"exchange_type" vd:"len($)>0"` //交易所类型
|
ExchangeType string `json:"exchange_type" vd:"len($)>0"` //交易所类型
|
||||||
|
StrategyTemplateType int `json:"strategy_template_type"` //策略类型 0-无 1-波段涨跌幅
|
||||||
|
StrategyTemplateId int `json:"strategy_template_id"` //策略id
|
||||||
|
ReduceStrategyId int `json:"reduce_strategy_id"` //减仓策略id
|
||||||
OrderType int `json:"order_type"` //订单类型
|
OrderType int `json:"order_type"` //订单类型
|
||||||
Symbol string `json:"symbol"` //交易对
|
Symbol string `json:"symbol"` //交易对
|
||||||
ApiUserId string `json:"api_id" ` //下单用户
|
ApiUserId string `json:"api_id" ` //下单用户
|
||||||
@ -199,6 +202,7 @@ type LineAddPreOrderReq struct {
|
|||||||
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"` //止盈后止盈价百分比
|
||||||
ProfitTpSlPriceRatio decimal.Decimal `json:"profit_tp_sl_price_ratio"` //止盈后止损价百分比
|
ProfitTpSlPriceRatio decimal.Decimal `json:"profit_tp_sl_price_ratio"` //止盈后止损价百分比
|
||||||
|
StopLoss decimal.Decimal `json:"stop_loss"` //止损价
|
||||||
PriceType string `json:"price_type"` //价格类型
|
PriceType string `json:"price_type"` //价格类型
|
||||||
SaveTemplate string `json:"save_template"` //是否保存模板
|
SaveTemplate string `json:"save_template"` //是否保存模板
|
||||||
TemplateName string `json:"template_name"` //模板名字
|
TemplateName string `json:"template_name"` //模板名字
|
||||||
@ -283,6 +287,10 @@ func (req LineAddPreOrderReq) Valid() error {
|
|||||||
return errors.New("主单减仓价格百分比不能为空")
|
return errors.New("主单减仓价格百分比不能为空")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.StrategyTemplateType == 1 && req.StrategyTemplateId == 0 {
|
||||||
|
return errors.New("请选择策略")
|
||||||
|
}
|
||||||
|
|
||||||
if req.ReduceNumRatio.IsZero() {
|
if req.ReduceNumRatio.IsZero() {
|
||||||
return errors.New("主单减仓数量百分比不能为空")
|
return errors.New("主单减仓数量百分比不能为空")
|
||||||
}
|
}
|
||||||
@ -318,7 +326,9 @@ func (req LineAddPreOrderReq) Valid() error {
|
|||||||
return fmt.Errorf("%s单下跌价格不能为空", name)
|
return fmt.Errorf("%s单下跌价格不能为空", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.TakeProfitRatio.IsZero() || v.TakeProfitRatio.Cmp(decimal.NewFromInt(100)) > 0 {
|
if (v.AddType == 2 && v.AddPositionVal.Cmp(decimal.NewFromInt(100)) < 0 && v.TakeProfitRatio.IsZero()) ||
|
||||||
|
(v.AddType == 1 && v.TakeProfitRatio.IsZero()) ||
|
||||||
|
v.TakeProfitRatio.Cmp(decimal.NewFromInt(100)) > 0 {
|
||||||
return errors.New("止盈价格不正确")
|
return errors.New("止盈价格不正确")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -367,6 +377,8 @@ type LineTreeOrder struct {
|
|||||||
// LineBatchAddPreOrderReq 批量添加订单请求参数
|
// LineBatchAddPreOrderReq 批量添加订单请求参数
|
||||||
type LineBatchAddPreOrderReq struct {
|
type LineBatchAddPreOrderReq struct {
|
||||||
ExchangeType string `json:"exchange_type"` //交易所类型 字典exchange_type
|
ExchangeType string `json:"exchange_type"` //交易所类型 字典exchange_type
|
||||||
|
StrategyTemplateType int `json:"strategy_template_type"` //策略类型 0-无 1-波段涨跌幅
|
||||||
|
StrategyTemplateId int `json:"strategy_template_id"` //策略id
|
||||||
SymbolType int `json:"symbol_type"` //主单交易对类型 0-现货 1-合约
|
SymbolType int `json:"symbol_type"` //主单交易对类型 0-现货 1-合约
|
||||||
OrderType int `json:"order_type"` //订单类型
|
OrderType int `json:"order_type"` //订单类型
|
||||||
SymbolGroupId string `json:"symbol_group_id"` //交易对组id
|
SymbolGroupId string `json:"symbol_group_id"` //交易对组id
|
||||||
@ -404,6 +416,10 @@ func (req LineBatchAddPreOrderReq) CheckParams() error {
|
|||||||
return errors.New("请选择交易所")
|
return errors.New("请选择交易所")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.StrategyTemplateType == 1 && req.StrategyTemplateId == 0 {
|
||||||
|
return errors.New("请选择策略")
|
||||||
|
}
|
||||||
|
|
||||||
if req.ApiUserId == "" {
|
if req.ApiUserId == "" {
|
||||||
return errors.New("选择下单用户")
|
return errors.New("选择下单用户")
|
||||||
}
|
}
|
||||||
@ -462,7 +478,9 @@ func (req LineBatchAddPreOrderReq) CheckParams() error {
|
|||||||
return fmt.Errorf("%s单下跌价格不能为空", name)
|
return fmt.Errorf("%s单下跌价格不能为空", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
if v.TakeProfitRatio.IsZero() || v.TakeProfitRatio.Cmp(decimal.NewFromInt(100)) > 0 {
|
if (v.AddType == 2 && v.AddPositionVal.Cmp(decimal.NewFromInt(100)) < 0 && v.TakeProfitRatio.IsZero()) ||
|
||||||
|
(v.AddType == 1 && v.TakeProfitRatio.IsZero()) ||
|
||||||
|
v.TakeProfitRatio.Cmp(decimal.NewFromInt(100)) > 0 {
|
||||||
return errors.New("止盈价格不正确")
|
return errors.New("止盈价格不正确")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -557,6 +575,17 @@ type PreOrderRedisList struct {
|
|||||||
QuoteSymbol string `json:"quote_symbol"`
|
QuoteSymbol string `json:"quote_symbol"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type StrategyOrderRedisList struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
Price string `json:"price"`
|
||||||
|
Site string `json:"site"`
|
||||||
|
ApiId int `json:"api_id"`
|
||||||
|
OrderSn string `json:"order_sn"`
|
||||||
|
QuoteSymbol string `json:"quote_symbol"`
|
||||||
|
LineStrategyTemplateRedis
|
||||||
|
}
|
||||||
|
|
||||||
type StopLossRedisList struct {
|
type StopLossRedisList struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
PId int `json:"pid"` //父级id
|
PId int `json:"pid"` //父级id
|
||||||
|
|||||||
159
app/admin/service/dto/line_reduce_strategy.go
Normal file
159
app/admin/service/dto/line_reduce_strategy.go
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/common/dto"
|
||||||
|
common "go-admin/common/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReduceStrategyGetPageReq struct {
|
||||||
|
dto.Pagination `search:"-"`
|
||||||
|
Name string `form:"name" search:"type:contains;column:name;table:line_reduce_strategy" comment:"减仓策略名称"`
|
||||||
|
Status string `form:"status" search:"type:exact;column:status;table:line_reduce_strategy" comment:"状态 1-启用 2-禁用"`
|
||||||
|
LineReduceStrategyOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReduceStrategyOrder struct {
|
||||||
|
Id string `form:"idOrder" search:"type:order;column:id;table:line_reduce_strategy"`
|
||||||
|
Name string `form:"nameOrder" search:"type:order;column:name;table:line_reduce_strategy"`
|
||||||
|
Status string `form:"statusOrder" search:"type:order;column:status;table:line_reduce_strategy"`
|
||||||
|
CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:line_reduce_strategy"`
|
||||||
|
UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:line_reduce_strategy"`
|
||||||
|
DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:line_reduce_strategy"`
|
||||||
|
CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:line_reduce_strategy"`
|
||||||
|
UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:line_reduce_strategy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LineReduceStrategyGetPageReq) GetNeedSearch() interface{} {
|
||||||
|
return *m
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReduceStrategyInsertReq struct {
|
||||||
|
Id int `json:"-" comment:"主键id"` // 主键id
|
||||||
|
Name string `json:"name" comment:"减仓策略名称"`
|
||||||
|
Status int `json:"status" comment:"状态 1-启用 2-禁用"`
|
||||||
|
Items []LineReduceStrategyItem `json:"items" comment:"减仓单节点"`
|
||||||
|
common.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
// 参数校验
|
||||||
|
func (s *LineReduceStrategyInsertReq) Valid() error {
|
||||||
|
if s.Name == "" {
|
||||||
|
return errors.New("减仓策略名称不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Status < 1 || s.Status > 2 {
|
||||||
|
return errors.New("状态值不合法")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.Items) == 0 {
|
||||||
|
return errors.New("减仓策略节点不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, item := range s.Items {
|
||||||
|
if err := item.Valid(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if index > 0 && item.LossPercent.Cmp(s.Items[index-1].LossPercent) <= 0 {
|
||||||
|
return errors.New("亏损比例必须递增")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReduceStrategyInsertReq) Generate(model *models.LineReduceStrategy) {
|
||||||
|
if s.Id == 0 {
|
||||||
|
model.Model = common.Model{Id: s.Id}
|
||||||
|
}
|
||||||
|
model.Name = s.Name
|
||||||
|
model.Status = s.Status
|
||||||
|
|
||||||
|
for _, item := range s.Items {
|
||||||
|
strategyItem := models.LineReduceStrategyItem{}
|
||||||
|
strategyItem.OrderType = item.OrderType
|
||||||
|
strategyItem.LossPercent = item.LossPercent
|
||||||
|
strategyItem.QuantityPercent = item.QuantityPercent
|
||||||
|
|
||||||
|
model.Items = append(model.Items, strategyItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReduceStrategyInsertReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReduceStrategyUpdateReq struct {
|
||||||
|
Id int `uri:"id" comment:"主键id"` // 主键id
|
||||||
|
Name string `json:"name" comment:"减仓策略名称"`
|
||||||
|
Status int `json:"status" comment:"状态 1-启用 2-禁用"`
|
||||||
|
Items []LineReduceStrategyItem `json:"items" comment:"减仓单节点"`
|
||||||
|
common.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReduceStrategyUpdateReq) Generate(model *models.LineReduceStrategy) {
|
||||||
|
if s.Id == 0 {
|
||||||
|
model.Model = common.Model{Id: s.Id}
|
||||||
|
}
|
||||||
|
model.Name = s.Name
|
||||||
|
model.Status = s.Status
|
||||||
|
|
||||||
|
for _, item := range s.Items {
|
||||||
|
strategyItem := models.LineReduceStrategyItem{}
|
||||||
|
strategyItem.OrderType = item.OrderType
|
||||||
|
strategyItem.LossPercent = item.LossPercent
|
||||||
|
strategyItem.QuantityPercent = item.QuantityPercent
|
||||||
|
|
||||||
|
model.Items = append(model.Items, strategyItem)
|
||||||
|
}
|
||||||
|
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
|
||||||
|
}
|
||||||
|
|
||||||
|
// 参数校验
|
||||||
|
func (s *LineReduceStrategyUpdateReq) Valid() error {
|
||||||
|
if s.Name == "" {
|
||||||
|
return errors.New("减仓策略名称不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Status < 1 || s.Status > 2 {
|
||||||
|
return errors.New("状态值不合法")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.Items) == 0 {
|
||||||
|
return errors.New("减仓策略节点不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range s.Items {
|
||||||
|
if err := item.Valid(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReduceStrategyUpdateReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineReduceStrategyGetReq 功能获取请求参数
|
||||||
|
type LineReduceStrategyGetReq struct {
|
||||||
|
Id int `uri:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReduceStrategyGetReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineReduceStrategyDeleteReq 功能删除请求参数
|
||||||
|
type LineReduceStrategyDeleteReq struct {
|
||||||
|
Ids []int `json:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReduceStrategyDeleteReq) GetId() interface{} {
|
||||||
|
return s.Ids
|
||||||
|
}
|
||||||
134
app/admin/service/dto/line_reduce_strategy_item.go
Normal file
134
app/admin/service/dto/line_reduce_strategy_item.go
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/common/dto"
|
||||||
|
common "go-admin/common/models"
|
||||||
|
"go-admin/pkg/utility"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReduceStrategyItemGetPageReq struct {
|
||||||
|
dto.Pagination `search:"-"`
|
||||||
|
LineReduceStrategyItemOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReduceStrategyItemOrder struct {
|
||||||
|
Id string `form:"idOrder" search:"type:order;column:id;table:line_reduce_strategy_item"`
|
||||||
|
ReduceStrategyId string `form:"reduceStrategyIdOrder" search:"type:order;column:reduce_strategy_id;table:line_reduce_strategy_item"`
|
||||||
|
LossPercent string `form:"lossPercentOrder" search:"type:order;column:loss_percent;table:line_reduce_strategy_item"`
|
||||||
|
OrderType string `form:"orderTypeOrder" search:"type:order;column:order_type;table:line_reduce_strategy_item"`
|
||||||
|
CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:line_reduce_strategy_item"`
|
||||||
|
UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:line_reduce_strategy_item"`
|
||||||
|
DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:line_reduce_strategy_item"`
|
||||||
|
CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:line_reduce_strategy_item"`
|
||||||
|
UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:line_reduce_strategy_item"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LineReduceStrategyItemGetPageReq) GetNeedSearch() interface{} {
|
||||||
|
return *m
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReduceStrategyItem struct {
|
||||||
|
LossPercent decimal.Decimal `json:"lossPercent" comment:"止损百分比"`
|
||||||
|
QuantityPercent decimal.Decimal `json:"quantityPercent" comment:"数量百分比"`
|
||||||
|
OrderType string `json:"orderType" comment:"订单类型 LIMIT-限价 MARKET-市价"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReduceStrategyItem) Valid() error {
|
||||||
|
|
||||||
|
if s.LossPercent.Cmp(decimal.Zero) <= 0 {
|
||||||
|
return errors.New("百分比不能小于等于0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.LossPercent.Cmp(decimal.NewFromFloat(100)) >= 0 {
|
||||||
|
return errors.New("百分比不能大于等于100")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.QuantityPercent.Cmp(decimal.Zero) <= 0 {
|
||||||
|
return errors.New("百分比不能小于等于0")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.QuantityPercent.Cmp(decimal.NewFromInt(100)) > 0 {
|
||||||
|
return errors.New("数量百分比不能大于等于100")
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := []string{"LIMIT", "MARKET"}
|
||||||
|
|
||||||
|
if !utility.ContainsStr(keys, s.OrderType) {
|
||||||
|
return errors.New("订单类型不正确")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReduceStrategyItemResp struct {
|
||||||
|
ReduceStrategyId int `json:"reduceStrategyId" comment:"减仓策略id"`
|
||||||
|
LossPercent decimal.Decimal `json:"lossPercent" comment:"亏损百分比"`
|
||||||
|
OrderType string `json:"orderType" comment:"订单类型 LIMIT-限价 MARKET-市价"`
|
||||||
|
Actived int `json:"actived" comment:"是否已减仓 1=未减仓 2=已减仓"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReduceStrategyItemInsertReq struct {
|
||||||
|
Id int `json:"-" comment:"主键id"` // 主键id
|
||||||
|
ReduceStrategyId int `json:"reduceStrategyId" comment:"减仓策略id"`
|
||||||
|
LossPercent decimal.Decimal `json:"lossPercent" comment:"亏损百分比"`
|
||||||
|
OrderType string `json:"orderType" comment:"订单类型 LIMIT-限价 MARKET-市价"`
|
||||||
|
common.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReduceStrategyItemInsertReq) Generate(model *models.LineReduceStrategyItem) {
|
||||||
|
if s.Id == 0 {
|
||||||
|
model.Model = common.Model{Id: s.Id}
|
||||||
|
}
|
||||||
|
model.ReduceStrategyId = s.ReduceStrategyId
|
||||||
|
model.LossPercent = s.LossPercent
|
||||||
|
model.OrderType = s.OrderType
|
||||||
|
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReduceStrategyItemInsertReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineReduceStrategyItemUpdateReq struct {
|
||||||
|
Id int `uri:"id" comment:"主键id"` // 主键id
|
||||||
|
ReduceStrategyId int `json:"reduceStrategyId" comment:"减仓策略id"`
|
||||||
|
LossPercent decimal.Decimal `json:"lossPercent" comment:"亏损百分比"`
|
||||||
|
OrderType string `json:"orderType" comment:"订单类型 LIMIT-限价 MARKET-市价"`
|
||||||
|
common.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReduceStrategyItemUpdateReq) Generate(model *models.LineReduceStrategyItem) {
|
||||||
|
if s.Id == 0 {
|
||||||
|
model.Model = common.Model{Id: s.Id}
|
||||||
|
}
|
||||||
|
model.ReduceStrategyId = s.ReduceStrategyId
|
||||||
|
model.LossPercent = s.LossPercent
|
||||||
|
model.OrderType = s.OrderType
|
||||||
|
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReduceStrategyItemUpdateReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineReduceStrategyItemGetReq 功能获取请求参数
|
||||||
|
type LineReduceStrategyItemGetReq struct {
|
||||||
|
Id int `uri:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReduceStrategyItemGetReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineReduceStrategyItemDeleteReq 功能删除请求参数
|
||||||
|
type LineReduceStrategyItemDeleteReq struct {
|
||||||
|
Ids []int `json:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineReduceStrategyItemDeleteReq) GetId() interface{} {
|
||||||
|
return s.Ids
|
||||||
|
}
|
||||||
174
app/admin/service/dto/line_strategy_template.go
Normal file
174
app/admin/service/dto/line_strategy_template.go
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/common/dto"
|
||||||
|
common "go-admin/common/models"
|
||||||
|
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineStrategyTemplateGetPageReq struct {
|
||||||
|
dto.Pagination `search:"-"`
|
||||||
|
Direction int `form:"direction" search:"type:exact;column:direction;table:line_strategy_template" comment:"涨跌方向 1-涨 2-跌"`
|
||||||
|
Percentag decimal.Decimal `form:"percentag" search:"type:exact;column:percentag;table:line_strategy_template" comment:"涨跌点数"`
|
||||||
|
CompareType int `form:"compareType" search:"type:exact;column:compare_type;table:line_strategy_template" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
|
||||||
|
LineStrategyTemplateOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineStrategyTemplateOrder struct {
|
||||||
|
Id string `form:"idOrder" search:"type:order;column:id;table:line_strategy_template"`
|
||||||
|
Direction string `form:"directionOrder" search:"type:order;column:direction;table:line_strategy_template"`
|
||||||
|
Percentag string `form:"percentagOrder" search:"type:order;column:percentag;table:line_strategy_template"`
|
||||||
|
CompareType string `form:"compareTypeOrder" search:"type:order;column:compare_type;table:line_strategy_template"`
|
||||||
|
TimeSlotStart string `form:"timeSlotStartOrder" search:"type:order;column:time_slot_start;table:line_strategy_template"`
|
||||||
|
TimeSlotEnd string `form:"timeSlotEndOrder" search:"type:order;column:time_slot_end;table:line_strategy_template"`
|
||||||
|
CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:line_strategy_template"`
|
||||||
|
UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:line_strategy_template"`
|
||||||
|
DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:line_strategy_template"`
|
||||||
|
CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:line_strategy_template"`
|
||||||
|
UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:line_strategy_template"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LineStrategyTemplateGetPageReq) GetNeedSearch() interface{} {
|
||||||
|
return *m
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineStrategyTemplateInsertReq struct {
|
||||||
|
Id int `json:"-" comment:"主键id"` // 主键id
|
||||||
|
Name string `json:"name" comment:"策略名称"`
|
||||||
|
Direction int `json:"direction" comment:"涨跌方向 1-涨 2-跌"`
|
||||||
|
Percentag decimal.Decimal `json:"percentag" comment:"涨跌点数"`
|
||||||
|
CompareType int `json:"compareType" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
|
||||||
|
TimeSlotStart int `json:"timeSlotStart" comment:"时间段开始(分)"`
|
||||||
|
TimeSlotEnd int `json:"timeSlotEnd" comment:"时间断截至(分)"`
|
||||||
|
common.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
// 交易参数
|
||||||
|
func (s *LineStrategyTemplateInsertReq) Valid() error {
|
||||||
|
if s.Name == "" {
|
||||||
|
return errors.New("策略名称不能为空")
|
||||||
|
}
|
||||||
|
if len(s.Name) > 50 {
|
||||||
|
return errors.New("策略名称长度不能超过50")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Percentag.Cmp(decimal.Zero) <= 0 {
|
||||||
|
return errors.New("涨跌点数不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.CompareType < 1 || s.CompareType > 5 {
|
||||||
|
return errors.New("比较类型不合法")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.TimeSlotStart < 0 || s.TimeSlotEnd > 59 {
|
||||||
|
return errors.New("时间段不合法")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.TimeSlotEnd < s.TimeSlotStart {
|
||||||
|
return errors.New("时间段不合法")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineStrategyTemplateInsertReq) Generate(model *models.LineStrategyTemplate) {
|
||||||
|
if s.Id == 0 {
|
||||||
|
model.Model = common.Model{Id: s.Id}
|
||||||
|
}
|
||||||
|
model.Name = s.Name
|
||||||
|
model.Direction = s.Direction
|
||||||
|
model.Percentag = s.Percentag
|
||||||
|
model.CompareType = s.CompareType
|
||||||
|
model.TimeSlotStart = s.TimeSlotStart
|
||||||
|
model.TimeSlotEnd = s.TimeSlotEnd
|
||||||
|
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineStrategyTemplateInsertReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineStrategyTemplateUpdateReq struct {
|
||||||
|
Id int `uri:"id" comment:"主键id"` // 主键id
|
||||||
|
Name string `json:"name" comment:"策略名称"`
|
||||||
|
Direction int `json:"direction" comment:"涨跌方向 1-涨 2-跌"`
|
||||||
|
Percentag decimal.Decimal `json:"percentag" comment:"涨跌点数"`
|
||||||
|
CompareType int `json:"compareType" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
|
||||||
|
TimeSlotStart int `json:"timeSlotStart" comment:"时间段开始(分)"`
|
||||||
|
TimeSlotEnd int `json:"timeSlotEnd" comment:"时间断截至(分)"`
|
||||||
|
common.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
// 交易参数
|
||||||
|
func (s *LineStrategyTemplateUpdateReq) Valid() error {
|
||||||
|
if s.Name == "" {
|
||||||
|
return errors.New("策略名称不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.Name) > 50 {
|
||||||
|
return errors.New("策略名称长度不能超过50")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.Percentag.Cmp(decimal.Zero) <= 0 {
|
||||||
|
return errors.New("涨跌点数不能为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.CompareType < 1 || s.CompareType > 5 {
|
||||||
|
return errors.New("比较类型不合法")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.TimeSlotStart < 0 || s.TimeSlotEnd > 59 {
|
||||||
|
return errors.New("时间段不合法")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.TimeSlotEnd < s.TimeSlotStart {
|
||||||
|
return errors.New("时间段不合法")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (s *LineStrategyTemplateUpdateReq) Generate(model *models.LineStrategyTemplate) {
|
||||||
|
if s.Id == 0 {
|
||||||
|
model.Model = common.Model{Id: s.Id}
|
||||||
|
}
|
||||||
|
model.Name = s.Name
|
||||||
|
model.Direction = s.Direction
|
||||||
|
model.Percentag = s.Percentag
|
||||||
|
model.CompareType = s.CompareType
|
||||||
|
model.TimeSlotStart = s.TimeSlotStart
|
||||||
|
model.TimeSlotEnd = s.TimeSlotEnd
|
||||||
|
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineStrategyTemplateUpdateReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineStrategyTemplateGetReq 功能获取请求参数
|
||||||
|
type LineStrategyTemplateGetReq struct {
|
||||||
|
Id int `uri:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineStrategyTemplateGetReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineStrategyTemplateDeleteReq 功能删除请求参数
|
||||||
|
type LineStrategyTemplateDeleteReq struct {
|
||||||
|
Ids []int `json:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineStrategyTemplateDeleteReq) GetId() interface{} {
|
||||||
|
return s.Ids
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineStrategyTemplateRedis struct {
|
||||||
|
Direction int `json:"direction" comment:"涨跌方向 1-涨 2-跌"`
|
||||||
|
Percentag decimal.Decimal `json:"percentag" comment:"涨跌点数"`
|
||||||
|
CompareType int `json:"compareType" comment:"比较类型 1-大于 2-大于等于 3-小于 4-小于等于 5等于 "`
|
||||||
|
TimeSlotStart int `json:"timeSlotStart" comment:"时间段开始(分)"`
|
||||||
|
TimeSlotEnd int `json:"timeSlotEnd" comment:"时间断截至(分)"`
|
||||||
|
}
|
||||||
@ -14,6 +14,7 @@ type LineSymbolGetPageReq struct {
|
|||||||
BaseAsset string `form:"baseAsset" search:"type:contains;column:base_asset;table:line_symbol" comment:"基础货币"`
|
BaseAsset string `form:"baseAsset" search:"type:contains;column:base_asset;table:line_symbol" comment:"基础货币"`
|
||||||
QuoteAsset string `form:"quoteAsset" search:"type:exact;column:quote_asset;table:line_symbol" comment:"计价货币"`
|
QuoteAsset string `form:"quoteAsset" search:"type:exact;column:quote_asset;table:line_symbol" comment:"计价货币"`
|
||||||
Type string `form:"type" search:"type:exact;column:type;table:line_symbol" comment:"交易对类型"`
|
Type string `form:"type" search:"type:exact;column:type;table:line_symbol" comment:"交易对类型"`
|
||||||
|
// StrategyTemplateId int `form:"strategyTemplateId" search:"-" comment:"策略id"`
|
||||||
LineSymbolOrder
|
LineSymbolOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
88
app/admin/service/dto/line_symbol_price.go
Normal file
88
app/admin/service/dto/line_symbol_price.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/common/dto"
|
||||||
|
common "go-admin/common/models"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineSymbolPriceGetPageReq struct {
|
||||||
|
dto.Pagination `search:"-"`
|
||||||
|
Symbol string `form:"symbol" search:"type:exact;column:symbol;table:line_symbol_price" comment:"交易对"`
|
||||||
|
Status int `form:"status" search:"type:exact;column:status;table:line_symbol_price" comment:"状态 1-启用 2-禁用"`
|
||||||
|
LineSymbolPriceOrder
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineSymbolPriceOrder struct {
|
||||||
|
Id string `form:"idOrder" search:"type:order;column:id;table:line_symbol_price"`
|
||||||
|
Symbol string `form:"symbolOrder" search:"type:order;column:symbol;table:line_symbol_price"`
|
||||||
|
Status string `form:"statusOrder" search:"type:order;column:status;table:line_symbol_price"`
|
||||||
|
CreatedAt string `form:"createdAtOrder" search:"type:order;column:created_at;table:line_symbol_price"`
|
||||||
|
UpdatedAt string `form:"updatedAtOrder" search:"type:order;column:updated_at;table:line_symbol_price"`
|
||||||
|
DeletedAt string `form:"deletedAtOrder" search:"type:order;column:deleted_at;table:line_symbol_price"`
|
||||||
|
CreateBy string `form:"createByOrder" search:"type:order;column:create_by;table:line_symbol_price"`
|
||||||
|
UpdateBy string `form:"updateByOrder" search:"type:order;column:update_by;table:line_symbol_price"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *LineSymbolPriceGetPageReq) GetNeedSearch() interface{} {
|
||||||
|
return *m
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineSymbolPriceInsertReq struct {
|
||||||
|
Id int `json:"-" comment:"主键id"` // 主键id
|
||||||
|
Symbol string `json:"symbol" comment:"交易对"`
|
||||||
|
Status int `json:"status" comment:"状态 1-启用 2-禁用"`
|
||||||
|
common.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineSymbolPriceInsertReq) Generate(model *models.LineSymbolPrice) {
|
||||||
|
if s.Id == 0 {
|
||||||
|
model.Model = common.Model{Id: s.Id}
|
||||||
|
}
|
||||||
|
model.Symbol = strings.ToUpper(s.Symbol)
|
||||||
|
model.Status = s.Status
|
||||||
|
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineSymbolPriceInsertReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
type LineSymbolPriceUpdateReq struct {
|
||||||
|
Id int `uri:"id" comment:"主键id"` // 主键id
|
||||||
|
Symbol string `json:"symbol" comment:"交易对"`
|
||||||
|
Status int `json:"status" comment:"状态 1-启用 2-禁用"`
|
||||||
|
common.ControlBy
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineSymbolPriceUpdateReq) Generate(model *models.LineSymbolPrice) {
|
||||||
|
if s.Id == 0 {
|
||||||
|
model.Model = common.Model{Id: s.Id}
|
||||||
|
}
|
||||||
|
model.Symbol = strings.ToUpper(s.Symbol)
|
||||||
|
model.Status = s.Status
|
||||||
|
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineSymbolPriceUpdateReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineSymbolPriceGetReq 功能获取请求参数
|
||||||
|
type LineSymbolPriceGetReq struct {
|
||||||
|
Id int `uri:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineSymbolPriceGetReq) GetId() interface{} {
|
||||||
|
return s.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineSymbolPriceDeleteReq 功能删除请求参数
|
||||||
|
type LineSymbolPriceDeleteReq struct {
|
||||||
|
Ids []int `json:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LineSymbolPriceDeleteReq) GetId() interface{} {
|
||||||
|
return s.Ids
|
||||||
|
}
|
||||||
@ -57,14 +57,15 @@ func (s *LineSystemSettingInsertReq) GetId() interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type LineSystemSettingUpdateReq struct {
|
type LineSystemSettingUpdateReq struct {
|
||||||
Id int `uri:"id" comment:"id"` // id
|
Id int `uri:"id" comment:"id"` // id
|
||||||
Time int64 `json:"time" comment:"导入:挂单时长达到时间后失效"`
|
Time int64 `json:"time" comment:"导入:挂单时长达到时间后失效"`
|
||||||
BatchTime int64 `json:"batchTime" comment:"批量:挂单时长达到时间后失效"`
|
BatchTime int64 `json:"batchTime" comment:"批量:挂单时长达到时间后失效"`
|
||||||
ProfitRate string `json:"profitRate" comment:"平仓盈利比例"`
|
ProfitRate string `json:"profitRate" comment:"平仓盈利比例"`
|
||||||
CoverOrderTypeBRate string `json:"coverOrderTypeBRate" comment:"b账户限价补单的买入百分比"`
|
CoverOrderTypeBRate string `json:"coverOrderTypeBRate" comment:"b账户限价补单的买入百分比"`
|
||||||
StopLossPremium decimal.Decimal `json:"stopLossPremium" comment:"限价止损溢价"`
|
StopLossPremium decimal.Decimal `json:"stopLossPremium" comment:"限价止损溢价"`
|
||||||
AddPositionPremium decimal.Decimal `json:"addPositionPremium" comment:"限价加仓溢价"`
|
AddPositionPremium decimal.Decimal `json:"addPositionPremium" comment:"限价加仓溢价"`
|
||||||
ReducePremium decimal.Decimal `json:"reducePremium" comment:"限价减仓溢价"`
|
ReducePremium decimal.Decimal `json:"reducePremium" comment:"限价减仓溢价"`
|
||||||
|
ReduceEarlyTriggerPercent decimal.Decimal `json:"reduceEarlyTriggerPercent" comment:"减仓提前触发百分比"`
|
||||||
common.ControlBy
|
common.ControlBy
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,6 +80,7 @@ func (s *LineSystemSettingUpdateReq) Generate(model *models.LineSystemSetting) {
|
|||||||
model.StopLossPremium = s.StopLossPremium
|
model.StopLossPremium = s.StopLossPremium
|
||||||
model.AddPositionPremium = s.AddPositionPremium
|
model.AddPositionPremium = s.AddPositionPremium
|
||||||
model.ReducePremium = s.ReducePremium
|
model.ReducePremium = s.ReducePremium
|
||||||
|
model.ReduceEarlyTriggerPercent = s.ReduceEarlyTriggerPercent
|
||||||
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
|
model.UpdateBy = s.UpdateBy // 添加这而,需要记录是被谁更新的
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
"go-admin/pkg/utility/snowflakehelper"
|
"go-admin/pkg/utility/snowflakehelper"
|
||||||
"go-admin/services/binanceservice"
|
"go-admin/services/binanceservice"
|
||||||
|
"go-admin/services/cacheservice"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -263,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{}
|
||||||
@ -394,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, ",")
|
||||||
@ -403,6 +404,15 @@ func (e *LinePreOrder) AddPreOrderCheck(req *dto.LineAddPreOrderReq, p *actions.
|
|||||||
apiIds = append(apiIds, apiId)
|
apiIds = append(apiIds, apiId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req.StrategyTemplateType == 1 && req.StrategyTemplateId > 0 {
|
||||||
|
cachePriceSymbols, _ := helper.DefaultRedis.GetAllList(rediskey.CacheSymbolLastPrice)
|
||||||
|
|
||||||
|
if !utility.ContainsStr(cachePriceSymbols, req.Symbol) {
|
||||||
|
*errs = append(*errs, errors.New("交易对涨跌幅缓存不存在"))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
activeApiIds, _ := apiUserService.GetActiveApis(apiIds)
|
activeApiIds, _ := apiUserService.GetActiveApis(apiIds)
|
||||||
|
|
||||||
if len(activeApiIds) == 0 {
|
if len(activeApiIds) == 0 {
|
||||||
@ -413,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)
|
||||||
@ -438,6 +448,17 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
|
|||||||
saveTemplateParams.CreateBy = req.CreateBy
|
saveTemplateParams.CreateBy = req.CreateBy
|
||||||
e.Orm.Model(&models.LineOrderTemplateLogs{}).Create(&saveTemplateParams)
|
e.Orm.Model(&models.LineOrderTemplateLogs{}).Create(&saveTemplateParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
linestrategyTemplate := models.LineStrategyTemplate{}
|
||||||
|
|
||||||
|
if req.StrategyTemplateId > 0 {
|
||||||
|
e.Orm.Where("id =?", req.StrategyTemplateId).First(&linestrategyTemplate)
|
||||||
|
|
||||||
|
if linestrategyTemplate.Id == 0 {
|
||||||
|
*errs = append(*errs, fmt.Errorf("策略不存在:%v", req.StrategyTemplateId))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
if req.SaveTemplate == "2" {
|
if req.SaveTemplate == "2" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -461,13 +482,13 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
|
|||||||
}
|
}
|
||||||
var AddOrder models.LinePreOrder
|
var AddOrder models.LinePreOrder
|
||||||
var profitOrder models.LinePreOrder
|
var profitOrder models.LinePreOrder
|
||||||
var stopOrder models.LinePreOrder
|
var reduceOrder models.LinePreOrder
|
||||||
|
|
||||||
//获取交易对
|
//获取交易对
|
||||||
tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, key)
|
tradeSet, _ := helper.GetObjString[models2.TradeSet](helper.DefaultRedis, key)
|
||||||
tickerPrice := utility.StrToDecimal(tradeSet.LastPrice)
|
tickerPrice := utility.StrToDecimal(tradeSet.LastPrice)
|
||||||
if tickerPrice.Equal(decimal.Zero) { //redis 没有这个值
|
if tickerPrice.Equal(decimal.Zero) { //redis 没有这个值
|
||||||
*errs = append(*errs, fmt.Errorf("api_id:%s 获取交易对:%s 交易行情出错", id, req.Symbol))
|
*errs = append(*errs, fmt.Errorf("api_id:%d 获取交易对:%s 交易行情出错", id, req.Symbol))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -540,7 +561,7 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
|
|||||||
AddOrder.Num = utility.SafeDiv(buyPrice, fromString).Truncate(int32(tradeSet.AmountDigit)).String()
|
AddOrder.Num = utility.SafeDiv(buyPrice, fromString).Truncate(int32(tradeSet.AmountDigit)).String()
|
||||||
}
|
}
|
||||||
if utility.StringToFloat64(AddOrder.Num) < tradeSet.MinQty {
|
if utility.StringToFloat64(AddOrder.Num) < tradeSet.MinQty {
|
||||||
*errs = append(*errs, fmt.Errorf("api_id:%s 获取交易对:%s 小于最小下单数量", id, req.Symbol))
|
*errs = append(*errs, fmt.Errorf("api_id:%d 获取交易对:%s 小于最小下单数量", id, req.Symbol))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
AddOrder.OrderSn = strconv.FormatInt(snowflakehelper.GetOrderId(), 10)
|
AddOrder.OrderSn = strconv.FormatInt(snowflakehelper.GetOrderId(), 10)
|
||||||
@ -551,7 +572,7 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
|
|||||||
AddOrder.GroupId = "0"
|
AddOrder.GroupId = "0"
|
||||||
AddOrder.Status = 0
|
AddOrder.Status = 0
|
||||||
copier.Copy(&profitOrder, &AddOrder)
|
copier.Copy(&profitOrder, &AddOrder)
|
||||||
copier.Copy(&stopOrder, &AddOrder)
|
copier.Copy(&reduceOrder, &AddOrder)
|
||||||
|
|
||||||
preOrderStatus := models.LinePreOrderStatus{}
|
preOrderStatus := models.LinePreOrderStatus{}
|
||||||
preOrderStatus.OrderSn = AddOrder.OrderSn
|
preOrderStatus.OrderSn = AddOrder.OrderSn
|
||||||
@ -565,6 +586,7 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
|
|||||||
OrderType: req.PriceType,
|
OrderType: req.PriceType,
|
||||||
TakeProfitRatio: utility.StringToDecimal(req.Profit),
|
TakeProfitRatio: utility.StringToDecimal(req.Profit),
|
||||||
TakeProfitNumRatio: req.ProfitNumRatio,
|
TakeProfitNumRatio: req.ProfitNumRatio,
|
||||||
|
StopLossRatio: req.StopLoss,
|
||||||
TpTpPriceRatio: req.ProfitTpTpPriceRatio,
|
TpTpPriceRatio: req.ProfitTpTpPriceRatio,
|
||||||
TpSlPriceRatio: req.ProfitTpSlPriceRatio,
|
TpSlPriceRatio: req.ProfitTpSlPriceRatio,
|
||||||
}
|
}
|
||||||
@ -584,7 +606,6 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
|
|||||||
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
|
||||||
|
|
||||||
// if decimal.NewFromInt(100).Sub(req.ReduceNumRatio).Cmp(decimal.Zero) > 0 {
|
|
||||||
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)
|
defultExt2.ReTakeRatio = utility.SafeDiv(req.ReducePriceRatio, default2NumPercent).Truncate(2)
|
||||||
@ -614,7 +635,7 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
|
|||||||
mainParam.TotalLossAmountU = calculateResp.TotalLossAmountU //buyPrice.Mul(req.ReducePriceRatio.Div(decimal.NewFromInt(100)).Truncate(4)).Truncate(int32(tradeSet.PriceDigit))
|
mainParam.TotalLossAmountU = calculateResp.TotalLossAmountU //buyPrice.Mul(req.ReducePriceRatio.Div(decimal.NewFromInt(100)).Truncate(4)).Truncate(int32(tradeSet.PriceDigit))
|
||||||
req.ReduceReTakeProfitRatio = calculateResp.Ratio
|
req.ReduceReTakeProfitRatio = calculateResp.Ratio
|
||||||
mainParam.LossBeginPercent = req.ReducePriceRatio
|
mainParam.LossBeginPercent = req.ReducePriceRatio
|
||||||
defultExt.ReTakeRatio = calculateResp.Ratio
|
// defultExt.ReTakeRatio = calculateResp.Ratio
|
||||||
|
|
||||||
for index, addPosition := range req.Ext {
|
for index, addPosition := range req.Ext {
|
||||||
ext := models.LinePreOrderExt{
|
ext := models.LinePreOrderExt{
|
||||||
@ -651,11 +672,20 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
|
|||||||
preOrderExts = append(preOrderExts, ext)
|
preOrderExts = append(preOrderExts, ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//获取减仓策略
|
||||||
|
var reduceStrategy models.LineReduceStrategy
|
||||||
|
|
||||||
|
if req.ReduceStrategyId > 0 {
|
||||||
|
reduceStrategyService := LineReduceStrategy{Service: e.Service}
|
||||||
|
reduceStrategy, _ = reduceStrategyService.GetById(req.ReduceStrategyId)
|
||||||
|
}
|
||||||
|
|
||||||
//事务添加
|
//事务添加
|
||||||
e.Orm.Transaction(func(tx *gorm.DB) error {
|
e.Orm.Transaction(func(tx *gorm.DB) error {
|
||||||
|
reduceOrderStrategys := make([]models.LineOrderReduceStrategy, 0)
|
||||||
err := tx.Model(&models.LinePreOrder{}).Omit("id", "save_template", "template_name").Create(&AddOrder).Error
|
err := tx.Model(&models.LinePreOrder{}).Omit("id", "save_template", "template_name").Create(&AddOrder).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
*errs = append(*errs, fmt.Errorf("api_id:%s 获取交易对:%s 生成订单失败", id, req.Symbol))
|
*errs = append(*errs, fmt.Errorf("api_id:%d 获取交易对:%s 生成订单失败", id, req.Symbol))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -664,117 +694,72 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
|
|||||||
//加仓、减仓状态
|
//加仓、减仓状态
|
||||||
tx.Model(&models.LinePreOrderStatus{}).Create(&preOrderStatus)
|
tx.Model(&models.LinePreOrderStatus{}).Create(&preOrderStatus)
|
||||||
preOrderExts[0].OrderId = AddOrder.Id
|
preOrderExts[0].OrderId = AddOrder.Id
|
||||||
list := dto.PreOrderRedisList{
|
|
||||||
Id: AddOrder.Id,
|
|
||||||
Symbol: AddOrder.Symbol,
|
|
||||||
Price: AddOrder.Price,
|
|
||||||
Site: AddOrder.Site,
|
|
||||||
ApiId: AddOrder.ApiId,
|
|
||||||
OrderSn: AddOrder.OrderSn,
|
|
||||||
QuoteSymbol: AddOrder.QuoteSymbol,
|
|
||||||
}
|
|
||||||
marshal, _ := sonic.Marshal(&list)
|
|
||||||
var preKey string
|
|
||||||
if AddOrder.SymbolType == global.SYMBOL_SPOT {
|
|
||||||
preKey = fmt.Sprintf(rediskey.PreSpotOrderList, AddOrder.ExchangeType)
|
|
||||||
} else {
|
|
||||||
preKey = fmt.Sprintf(rediskey.PreFutOrderList, AddOrder.ExchangeType)
|
|
||||||
}
|
|
||||||
helper.DefaultRedis.LPushList(preKey, string(marshal))
|
|
||||||
|
|
||||||
//是否有止盈止损订单
|
childOrders, err := makeFuturesTakeAndReduce(&AddOrder, defultExt, tradeSet)
|
||||||
if req.Profit != "" {
|
|
||||||
if req.PricePattern == "mixture" {
|
|
||||||
mixturePrice := utility.StrToDecimal(req.Profit)
|
|
||||||
|
|
||||||
if mixturePrice.Cmp(decimal.Zero) <= 0 {
|
if err != nil {
|
||||||
return fmt.Errorf("止盈价不能小于等于0")
|
logger.Errorf("构建主单止盈止损失败,err:%v", err)
|
||||||
}
|
|
||||||
|
|
||||||
profitOrder.Price = mixturePrice.Truncate(int32(tradeSet.PriceDigit)).String()
|
|
||||||
profitOrder.Rate = "0"
|
|
||||||
} else {
|
|
||||||
if strings.ToUpper(req.Site) == "BUY" {
|
|
||||||
// profitOrder.Site = "SELL"
|
|
||||||
profitOrder.Price = decimal.NewFromFloat(utility.StringToFloat64(AddOrder.Price) * (1 + utility.StringToFloat64(req.Profit)/100)).Truncate(int32(tradeSet.PriceDigit)).String()
|
|
||||||
} else {
|
|
||||||
// profitOrder.Site = "BUY"
|
|
||||||
profitOrder.Price = decimal.NewFromFloat(utility.StringToFloat64(AddOrder.Price) * (1 - utility.StringToFloat64(req.Profit)/100)).Truncate(int32(tradeSet.PriceDigit)).String()
|
|
||||||
}
|
|
||||||
profitOrder.Rate = req.Profit
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.ToUpper(req.Site) == "BUY" {
|
|
||||||
profitOrder.Site = "SELL"
|
|
||||||
} else {
|
|
||||||
profitOrder.Site = "BUY"
|
|
||||||
}
|
|
||||||
profitOrder.OrderSn = strconv.FormatInt(snowflakehelper.GetOrderId(), 10)
|
|
||||||
profitOrder.Pid = AddOrder.Id
|
|
||||||
profitOrder.OrderType = 1
|
|
||||||
profitOrder.Status = 0
|
|
||||||
profitOrder.MainId = AddOrder.Id
|
|
||||||
|
|
||||||
if req.ProfitNumRatio.Cmp(decimal.Zero) > 0 {
|
|
||||||
numPercent := utility.SafeDiv(req.ProfitNumRatio, decimal.NewFromInt(100))
|
|
||||||
profitOrder.Num = utility.StrToDecimal(profitOrder.Num).Mul(numPercent).Truncate(int32(tradeSet.AmountDigit)).String()
|
|
||||||
}
|
|
||||||
tx.Model(&models.LinePreOrder{}).Omit("id", "save_template", "template_name").Create(&profitOrder)
|
|
||||||
|
|
||||||
//不全部止盈的时候
|
|
||||||
if req.ProfitNumRatio.Cmp(decimal.Zero) > 0 && req.ProfitNumRatio.Cmp(decimal.NewFromInt(100)) < 0 {
|
|
||||||
reminQuantity := utility.StrToDecimal(AddOrder.Num).Sub(utility.StrToDecimal(profitOrder.Num))
|
|
||||||
|
|
||||||
childrens, err := makeTpOrder(&profitOrder, reminQuantity, req.ProfitTpTpPriceRatio, req.ProfitTpSlPriceRatio, &tradeSet)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("生成止盈后子订单失败")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.Create(&childrens)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(childOrders) > 0 {
|
||||||
|
tx.Model(&models.LinePreOrder{}).Omit("id", "save_template", "template_name").Create(&childOrders)
|
||||||
|
|
||||||
|
for _, childOrder := range childOrders {
|
||||||
|
//不全部止盈的时候
|
||||||
|
if childOrder.OrderType == 1 && req.ProfitNumRatio.Cmp(decimal.Zero) > 0 && req.ProfitNumRatio.Cmp(decimal.NewFromInt(100)) < 0 {
|
||||||
|
reminQuantity := utility.StrToDecimal(AddOrder.Num).Sub(utility.StrToDecimal(childOrder.Num))
|
||||||
|
|
||||||
|
childrens, err := makeTpOrder(&childOrder, reminQuantity, req.ProfitTpTpPriceRatio, req.ProfitTpSlPriceRatio, &tradeSet)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("生成止盈后子订单失败")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.Create(&childrens)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//减仓单
|
||||||
if req.ReducePriceRatio.Cmp(decimal.Zero) > 0 {
|
if req.ReducePriceRatio.Cmp(decimal.Zero) > 0 {
|
||||||
if req.PricePattern == "mixture" {
|
if req.PricePattern == "mixture" {
|
||||||
if req.ReducePriceRatio.Cmp(decimal.Zero) <= 0 {
|
if req.ReducePriceRatio.Cmp(decimal.Zero) <= 0 {
|
||||||
return errors.New("检查价格不能小于等于0")
|
return errors.New("检查价格不能小于等于0")
|
||||||
}
|
}
|
||||||
|
|
||||||
stopOrder.Price = req.ReducePriceRatio.Truncate(int32(tradeSet.PriceDigit)).String()
|
reduceOrder.Price = req.ReducePriceRatio.Truncate(int32(tradeSet.PriceDigit)).String()
|
||||||
stopOrder.Rate = "0"
|
reduceOrder.Rate = "0"
|
||||||
} else {
|
} else {
|
||||||
if strings.ToUpper(req.Site) == "BUY" {
|
if strings.ToUpper(req.Site) == "BUY" {
|
||||||
// stopOrder.Site = "SELL"
|
// stopOrder.Site = "SELL"
|
||||||
stopOrder.Price = utility.StrToDecimal(AddOrder.Price).Mul(decimal.NewFromInt(1).Sub(utility.SafeDiv(req.ReducePriceRatio, decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)).String()
|
reduceOrder.Price = utility.StrToDecimal(AddOrder.Price).Mul(decimal.NewFromInt(1).Sub(utility.SafeDiv(req.ReducePriceRatio, decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)).String()
|
||||||
} else {
|
} else {
|
||||||
// stopOrder.Site = "BUY"
|
// stopOrder.Site = "BUY"
|
||||||
stopOrder.Price = utility.StrToDecimal(AddOrder.Price).Mul(decimal.NewFromInt(1).Add(utility.SafeDiv(req.ReducePriceRatio, decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)).String()
|
reduceOrder.Price = utility.StrToDecimal(AddOrder.Price).Mul(decimal.NewFromInt(1).Add(utility.SafeDiv(req.ReducePriceRatio, decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)).String()
|
||||||
}
|
}
|
||||||
|
|
||||||
stopOrder.Rate = req.ReducePriceRatio.String()
|
reduceOrder.Rate = req.ReducePriceRatio.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.ToUpper(req.Site) == "BUY" {
|
if strings.ToUpper(req.Site) == "BUY" {
|
||||||
stopOrder.Site = "SELL"
|
reduceOrder.Site = "SELL"
|
||||||
} else {
|
} else {
|
||||||
stopOrder.Site = "BUY"
|
reduceOrder.Site = "BUY"
|
||||||
}
|
}
|
||||||
stopOrder.OrderSn = strconv.FormatInt(snowflakehelper.GetOrderId(), 10)
|
reduceOrder.OrderSn = strconv.FormatInt(snowflakehelper.GetOrderId(), 10)
|
||||||
stopOrder.Pid = AddOrder.Id
|
reduceOrder.Pid = AddOrder.Id
|
||||||
stopOrder.MainId = AddOrder.Id
|
reduceOrder.MainId = AddOrder.Id
|
||||||
stopOrder.OrderType = 4
|
reduceOrder.OrderType = 4
|
||||||
stopOrder.Status = 0
|
reduceOrder.Status = 0
|
||||||
stopOrder.BuyPrice = "0"
|
reduceOrder.BuyPrice = "0"
|
||||||
stopNum := utility.StrToDecimal(AddOrder.Num).Mul(req.ReduceNumRatio.Div(decimal.NewFromInt(100)).Truncate(4))
|
stopNum := utility.StrToDecimal(AddOrder.Num).Mul(req.ReduceNumRatio.Div(decimal.NewFromInt(100)).Truncate(4))
|
||||||
stopOrder.Num = stopNum.Truncate(int32(tradeSet.AmountDigit)).String()
|
reduceOrder.Num = stopNum.Truncate(int32(tradeSet.AmountDigit)).String()
|
||||||
stopOrder.ExpireTime = time.Now().AddDate(10, 0, 0)
|
reduceOrder.ExpireTime = time.Now().AddDate(10, 0, 0)
|
||||||
|
|
||||||
tx.Model(&models.LinePreOrder{}).Omit("id", "save_template", "template_name").Create(&stopOrder)
|
tx.Model(&models.LinePreOrder{}).Omit("id", "save_template", "template_name").Create(&reduceOrder)
|
||||||
preOrderExts[1].OrderId = stopOrder.Id
|
preOrderExts[1].OrderId = reduceOrder.Id
|
||||||
if req.ReduceNumRatio.Cmp(decimal.Zero) > 0 && req.ReduceNumRatio.Cmp(decimal.NewFromInt(100)) < 0 {
|
if req.ReduceNumRatio.Cmp(decimal.Zero) > 0 && req.ReduceNumRatio.Cmp(decimal.NewFromInt(100)) < 0 {
|
||||||
if newOrders, err := makeReduceTakeAndStoploss(&stopOrder, defultExt2, tradeSet, false); err != nil {
|
if newOrders, err := makeReduceTakeAndStoploss(&reduceOrder, defultExt2, tradeSet, false); err != nil {
|
||||||
logger.Errorf("主单减仓生成止盈、减仓失败 err:%v", err)
|
logger.Errorf("主单减仓生成止盈、减仓失败 err:%v", err)
|
||||||
return err
|
return err
|
||||||
} else if len(newOrders) > 0 {
|
} else if len(newOrders) > 0 {
|
||||||
@ -784,9 +769,13 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if reduceStrategy.Id > 0 {
|
||||||
|
reduceOrderStrategys = append(reduceOrderStrategys, initOrderReduceStrategy(reduceStrategy, reduceOrder.Id))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//添加止盈单
|
//添加后续节点
|
||||||
for index, v := range preOrderExts {
|
for index, v := range preOrderExts {
|
||||||
preOrderExts[index].MainOrderId = AddOrder.Id
|
preOrderExts[index].MainOrderId = AddOrder.Id
|
||||||
if index < 2 {
|
if index < 2 {
|
||||||
@ -825,7 +814,7 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
|
|||||||
}
|
}
|
||||||
|
|
||||||
for index := range orders {
|
for index := range orders {
|
||||||
//减仓单且 减仓比例大于0 小于100 就冲下止盈止损
|
//止盈后止盈止损
|
||||||
if orders[index].OrderType == 1 && v.TakeProfitRatio.Cmp(decimal.Zero) > 0 && v.TakeProfitRatio.Cmp(decimal.NewFromInt(100)) < 0 {
|
if orders[index].OrderType == 1 && v.TakeProfitRatio.Cmp(decimal.Zero) > 0 && v.TakeProfitRatio.Cmp(decimal.NewFromInt(100)) < 0 {
|
||||||
reduceChildOrders, err := makeReduceTakeAndStoploss(&(orders[index]), v, tradeSet, true)
|
reduceChildOrders, err := makeReduceTakeAndStoploss(&(orders[index]), v, tradeSet, true)
|
||||||
|
|
||||||
@ -844,19 +833,113 @@ func (e *LinePreOrder) AddPreOrder(req *dto.LineAddPreOrderReq, apiUserIds []int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//减仓单绑定减仓未成交策略
|
||||||
|
if newOrder.OrderType == 4 && reduceStrategy.Id > 0 {
|
||||||
|
reduceOrderStrategys = append(reduceOrderStrategys, initOrderReduceStrategy(reduceStrategy, newOrder.Id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(reduceOrderStrategys) > 0 {
|
||||||
|
if err := tx.Create(reduceOrderStrategys).Error; err != nil {
|
||||||
|
logger.Error("保存减仓未成交策略失败")
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Model(&models.LinePreOrderExt{}).Create(&preOrderExts).Error
|
err = tx.Model(&models.LinePreOrderExt{}).Create(&preOrderExts).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
//保存下单缓存
|
||||||
|
return saveOrderCache(req, AddOrder, linestrategyTemplate)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 构建减仓单减仓策略
|
||||||
|
func initOrderReduceStrategy(reduceStrategy models.LineReduceStrategy, orderId int) models.LineOrderReduceStrategy {
|
||||||
|
result := models.LineOrderReduceStrategy{}
|
||||||
|
strategys := make([]dto.LineReduceStrategyItemResp, 0)
|
||||||
|
result.OrderId = orderId
|
||||||
|
result.ReduceStrategyId = reduceStrategy.Id
|
||||||
|
result.Actived = 2
|
||||||
|
|
||||||
|
for _, item := range reduceStrategy.Items {
|
||||||
|
strategys = append(strategys, dto.LineReduceStrategyItemResp{
|
||||||
|
LossPercent: item.LossPercent,
|
||||||
|
OrderType: item.OrderType,
|
||||||
|
Actived: 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
str, _ := sonic.MarshalString(strategys)
|
||||||
|
|
||||||
|
if str != "" {
|
||||||
|
result.ItemContent = str
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存下单缓存
|
||||||
|
func saveOrderCache(req *dto.LineAddPreOrderReq, AddOrder models.LinePreOrder, linestrategyTemplate models.LineStrategyTemplate) error {
|
||||||
|
var preKey string
|
||||||
|
var marshal []byte
|
||||||
|
|
||||||
|
switch {
|
||||||
|
//策略下单
|
||||||
|
case req.StrategyTemplateType == 1 && req.StrategyTemplateId > 0:
|
||||||
|
|
||||||
|
list := dto.StrategyOrderRedisList{
|
||||||
|
Id: AddOrder.Id,
|
||||||
|
OrderSn: AddOrder.OrderSn,
|
||||||
|
ApiId: AddOrder.ApiId,
|
||||||
|
Symbol: AddOrder.Symbol,
|
||||||
|
Price: AddOrder.Price,
|
||||||
|
Site: AddOrder.Site,
|
||||||
|
QuoteSymbol: AddOrder.QuoteSymbol,
|
||||||
|
}
|
||||||
|
list.Direction = linestrategyTemplate.Direction
|
||||||
|
list.Percentag = linestrategyTemplate.Percentag
|
||||||
|
list.CompareType = linestrategyTemplate.CompareType
|
||||||
|
list.TimeSlotStart = linestrategyTemplate.TimeSlotStart
|
||||||
|
list.TimeSlotEnd = linestrategyTemplate.TimeSlotEnd
|
||||||
|
|
||||||
|
marshal, _ = sonic.Marshal(&list)
|
||||||
|
if AddOrder.SymbolType == global.SYMBOL_SPOT {
|
||||||
|
preKey = fmt.Sprintf(rediskey.StrategySpotOrderList, AddOrder.ExchangeType)
|
||||||
|
} else {
|
||||||
|
preKey = fmt.Sprintf(rediskey.StrategyFutOrderList, AddOrder.ExchangeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
//直接下单
|
||||||
|
default:
|
||||||
|
list := dto.PreOrderRedisList{
|
||||||
|
Id: AddOrder.Id,
|
||||||
|
Symbol: AddOrder.Symbol,
|
||||||
|
Price: AddOrder.Price,
|
||||||
|
Site: AddOrder.Site,
|
||||||
|
ApiId: AddOrder.ApiId,
|
||||||
|
OrderSn: AddOrder.OrderSn,
|
||||||
|
QuoteSymbol: AddOrder.QuoteSymbol,
|
||||||
|
}
|
||||||
|
marshal, _ = sonic.Marshal(&list)
|
||||||
|
if AddOrder.SymbolType == global.SYMBOL_SPOT {
|
||||||
|
preKey = fmt.Sprintf(rediskey.PreSpotOrderList, AddOrder.ExchangeType)
|
||||||
|
} else {
|
||||||
|
preKey = fmt.Sprintf(rediskey.PreFutOrderList, AddOrder.ExchangeType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err := helper.DefaultRedis.LPushList(preKey, string(marshal))
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// 生成加仓单
|
// 生成加仓单
|
||||||
func createPreAddPosition(preOrder *models.LinePreOrder, v models.LinePreOrderExt, tradeSet models2.TradeSet) models.LinePreOrder {
|
func createPreAddPosition(preOrder *models.LinePreOrder, v models.LinePreOrderExt, tradeSet models2.TradeSet) models.LinePreOrder {
|
||||||
data := models.LinePreOrder{}
|
data := models.LinePreOrder{}
|
||||||
@ -959,6 +1042,7 @@ func createPreReduceOrder(preOrder *models.LinePreOrder, ext models.LinePreOrder
|
|||||||
// 构建止盈、止盈止损
|
// 构建止盈、止盈止损
|
||||||
func makeFuturesTakeAndReduce(preOrder *models.LinePreOrder, ext models.LinePreOrderExt, tradeSet models2.TradeSet) ([]models.LinePreOrder, error) {
|
func makeFuturesTakeAndReduce(preOrder *models.LinePreOrder, ext models.LinePreOrderExt, tradeSet models2.TradeSet) ([]models.LinePreOrder, error) {
|
||||||
orders := make([]models.LinePreOrder, 0)
|
orders := make([]models.LinePreOrder, 0)
|
||||||
|
mainId := preOrder.Id
|
||||||
var side string
|
var side string
|
||||||
|
|
||||||
if (preOrder.OrderType != 0 && strings.ToUpper(preOrder.Site) == "BUY") || (preOrder.OrderType == 0 && strings.ToUpper(preOrder.Site) == "SELL") {
|
if (preOrder.OrderType != 0 && strings.ToUpper(preOrder.Site) == "BUY") || (preOrder.OrderType == 0 && strings.ToUpper(preOrder.Site) == "SELL") {
|
||||||
@ -967,6 +1051,10 @@ func makeFuturesTakeAndReduce(preOrder *models.LinePreOrder, ext models.LinePreO
|
|||||||
side = "SELL"
|
side = "SELL"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if preOrder.MainId > 0 {
|
||||||
|
mainId = preOrder.MainId
|
||||||
|
}
|
||||||
|
|
||||||
if ext.TakeProfitRatio.Cmp(decimal.Zero) > 0 {
|
if ext.TakeProfitRatio.Cmp(decimal.Zero) > 0 {
|
||||||
// 止盈单
|
// 止盈单
|
||||||
profitOrder := models.LinePreOrder{}
|
profitOrder := models.LinePreOrder{}
|
||||||
@ -977,11 +1065,7 @@ func makeFuturesTakeAndReduce(preOrder *models.LinePreOrder, ext models.LinePreO
|
|||||||
profitOrder.Pid = preOrder.Id
|
profitOrder.Pid = preOrder.Id
|
||||||
profitOrder.OrderType = 1
|
profitOrder.OrderType = 1
|
||||||
profitOrder.Status = 0
|
profitOrder.Status = 0
|
||||||
profitOrder.MainId = preOrder.Id
|
profitOrder.MainId = mainId
|
||||||
|
|
||||||
if preOrder.MainId > 0 {
|
|
||||||
profitOrder.MainId = preOrder.MainId
|
|
||||||
}
|
|
||||||
profitOrder.BuyPrice = "0"
|
profitOrder.BuyPrice = "0"
|
||||||
profitOrder.Site = side
|
profitOrder.Site = side
|
||||||
|
|
||||||
@ -1007,7 +1091,7 @@ func makeFuturesTakeAndReduce(preOrder *models.LinePreOrder, ext models.LinePreO
|
|||||||
lossOrder.Pid = preOrder.Id
|
lossOrder.Pid = preOrder.Id
|
||||||
lossOrder.OrderType = 2
|
lossOrder.OrderType = 2
|
||||||
lossOrder.Status = 0
|
lossOrder.Status = 0
|
||||||
lossOrder.MainId = preOrder.MainId
|
lossOrder.MainId = mainId
|
||||||
lossOrder.BuyPrice = "0"
|
lossOrder.BuyPrice = "0"
|
||||||
lossOrder.Num = ext.TotalAfter.Truncate(int32(tradeSet.AmountDigit)).String()
|
lossOrder.Num = ext.TotalAfter.Truncate(int32(tradeSet.AmountDigit)).String()
|
||||||
lossOrder.Rate = ext.StopLossRatio.Truncate(2).String()
|
lossOrder.Rate = ext.StopLossRatio.Truncate(2).String()
|
||||||
@ -1134,31 +1218,6 @@ func (e *LinePreOrder) CheckRepeatOrder(symbolType int, apiUserId, site, baseCoi
|
|||||||
|
|
||||||
// AddBatchPreOrder 批量添加
|
// AddBatchPreOrder 批量添加
|
||||||
func (e *LinePreOrder) AddBatchPreOrder(batchReq *dto.LineBatchAddPreOrderReq, p *actions.DataPermission, errs *[]error) error {
|
func (e *LinePreOrder) AddBatchPreOrder(batchReq *dto.LineBatchAddPreOrderReq, p *actions.DataPermission, errs *[]error) error {
|
||||||
// apiIds := []int{}
|
|
||||||
// apiUserIds := strings.Split(batchReq.ApiUserId, ",")
|
|
||||||
// apiUserService := LineApiUser{Service: e.Service}
|
|
||||||
|
|
||||||
// for _, v := range apiUserIds {
|
|
||||||
// apiId, _ := strconv.Atoi(v)
|
|
||||||
// apiIds = append(apiIds, apiId)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// activeIds, err := apiUserService.GetActiveApis(apiIds)
|
|
||||||
|
|
||||||
// if err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if len(activeIds) == 0 {
|
|
||||||
// return errors.New("没有可用的api")
|
|
||||||
// }
|
|
||||||
|
|
||||||
// for _, v := range apiIds {
|
|
||||||
// if !utility.ContainsInt(activeIds, v) {
|
|
||||||
// *errs = append(*errs, errors.New("api_id:"+strconv.Itoa(v)+"不可用"))
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
if batchReq.SaveTemplate == "2" || batchReq.SaveTemplate == "1" { //2 = 只保存模板 1= 保存模板并下单
|
if batchReq.SaveTemplate == "2" || batchReq.SaveTemplate == "1" { //2 = 只保存模板 1= 保存模板并下单
|
||||||
var templateLog dto.LineBatchAddPreOrderReq
|
var templateLog dto.LineBatchAddPreOrderReq
|
||||||
copier.Copy(&templateLog, batchReq)
|
copier.Copy(&templateLog, batchReq)
|
||||||
@ -1191,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)
|
||||||
@ -1258,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 模板快速下单
|
||||||
@ -1279,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))
|
||||||
@ -1551,6 +1592,12 @@ func (e *LinePreOrder) ClearAll() error {
|
|||||||
"_PreFutOrderList_",
|
"_PreFutOrderList_",
|
||||||
"spot_reduce_list",
|
"spot_reduce_list",
|
||||||
"futures_reduce_list",
|
"futures_reduce_list",
|
||||||
|
"spot_reduce_strategy_list",
|
||||||
|
"fut_reduce_strategy_list",
|
||||||
|
"future_position",
|
||||||
|
"spot_position",
|
||||||
|
"strategy_spot_order_list",
|
||||||
|
"strategy_fut_order_list",
|
||||||
}
|
}
|
||||||
err = helper.DefaultRedis.DeleteKeysByPrefix(prefixs...)
|
err = helper.DefaultRedis.DeleteKeysByPrefix(prefixs...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1558,9 +1605,10 @@ func (e *LinePreOrder) ClearAll() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Orm.Model(&models.LinePreOrder{}).Exec("TRUNCATE TABLE line_pre_order") //订单表
|
e.Orm.Model(&models.LinePreOrder{}).Exec("TRUNCATE TABLE line_pre_order") //订单表
|
||||||
e.Orm.Model(&models.LinePreOrder{}).Exec("TRUNCATE TABLE line_pre_order_status") //订单拓展状态
|
e.Orm.Model(&models.LinePreOrder{}).Exec("TRUNCATE TABLE line_pre_order_status") //订单拓展状态
|
||||||
e.Orm.Model(&models.LinePreOrder{}).Exec("TRUNCATE TABLE line_pre_order_ext") //订单拓展配置
|
e.Orm.Model(&models.LinePreOrder{}).Exec("TRUNCATE TABLE line_pre_order_ext") //订单拓展配置
|
||||||
|
e.Orm.Model(&models.LinePreOrder{}).Exec("TRUNCATE TABLE line_order_reduce_strategy") //订单减仓策略配置
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1630,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
|
||||||
@ -2042,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, _ = binanceservice.GetTradeSet(req.Symbol, 0)
|
tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, req.Symbol, 1)
|
||||||
} else {
|
} else {
|
||||||
tradeSet, _ = binanceservice.GetTradeSet(req.Symbol, 1)
|
tradeSet, _ = cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, req.Symbol, 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
if tradeSet.LastPrice == "" {
|
if tradeSet.LastPrice == "" {
|
||||||
@ -2161,164 +2204,3 @@ func (e *LinePreOrder) CalculateBreakEvenRatio(req *dto.CalculateBreakEevenRatio
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// // 手动加仓
|
|
||||||
// func (e *LinePreOrder) AddPosition(req *dto.LinePreOrderAddPositionReq) error {
|
|
||||||
// lastPositionOrder := models.LinePreOrder{}
|
|
||||||
// var tradeSet models2.TradeSet
|
|
||||||
|
|
||||||
// if req.OrderType == 1 {
|
|
||||||
// tradeSet, _ = binanceservice.GetTradeSet(req.Symbol, 0)
|
|
||||||
// } else if req.OrderType == 2 {
|
|
||||||
// tradeSet, _ = binanceservice.GetTradeSet(req.Symbol, 1)
|
|
||||||
// } else {
|
|
||||||
// return fmt.Errorf("交易对:%s 订单类型错误", req.Symbol)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if tradeSet.LastPrice == "" {
|
|
||||||
// return fmt.Errorf("交易对:%s 交易对配置错误", req.Symbol)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if err := e.Orm.Model(&lastPositionOrder).Where("symbol =? AND status = 6 AND site =? AND api_id =? AND symbol_type =? AND exchange_type=?",
|
|
||||||
// req.Symbol, req.Site, req.ApiUserId, req.OrderType, req.ExchangeType).Error; err != nil {
|
|
||||||
// logger.Errorf("交易对:%s查询已开仓订单失败", req.Symbol)
|
|
||||||
// return fmt.Errorf("交易对:%s 没有已开仓的订单", req.Symbol)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// ext := models.LinePreOrderExt{
|
|
||||||
// MainOrderId: lastPositionOrder.Id,
|
|
||||||
// TakeProfitRatio: req.Profit,
|
|
||||||
// ReducePriceRatio: req.ReducePriceRatio,
|
|
||||||
// ReduceNumRatio: req.ReduceNumRatio,
|
|
||||||
// ReduceTakeProfitRatio: req.ReduceTakeProfitRatio,
|
|
||||||
// ReduceStopLossRatio: req.ReduceStopLossRatio,
|
|
||||||
// AddPositionOrderType: req.AddPositionOrderType,
|
|
||||||
// AddPositionType: 2,
|
|
||||||
// AddPositionVal: req.BuyPrice,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// addPosition := models.LinePreOrder{
|
|
||||||
// SignPrice: tradeSet.LastPrice,
|
|
||||||
// Pid: lastPositionOrder.Id,
|
|
||||||
// MainId: lastPositionOrder.Id,
|
|
||||||
// Symbol: req.Symbol,
|
|
||||||
// QuoteSymbol: tradeSet.Currency,
|
|
||||||
// SignPriceU: utility.StrToDecimal(tradeSet.LastPrice),
|
|
||||||
// ApiId: req.ApiUserId,
|
|
||||||
// Site: req.Site,
|
|
||||||
// ExchangeType: req.ExchangeType,
|
|
||||||
// OrderType: 0,
|
|
||||||
// OrderCategory: 3,
|
|
||||||
// BuyPrice: req.BuyPrice.String(),
|
|
||||||
// Status: 0,
|
|
||||||
// SymbolType: req.OrderType,
|
|
||||||
// MainOrderType: req.AddPositionOrderType,
|
|
||||||
// ExpireTime: time.Now().Add(4),
|
|
||||||
// OrderSn: utility.Int64ToString(snowflakehelper.GetOrderId()),
|
|
||||||
// }
|
|
||||||
|
|
||||||
// tickerPrice := utility.StrToDecimal(tradeSet.LastPrice)
|
|
||||||
// if req.PricePattern == "percentage" {
|
|
||||||
// addPosition.Rate = req.Price.String()
|
|
||||||
// priceRate := req.Price.Div(decimal.NewFromInt(100)) //下单价除100 =0.1
|
|
||||||
|
|
||||||
// if strings.ToUpper(req.Site) == "BUY" { //购买方向
|
|
||||||
// //实际下单价格
|
|
||||||
// truncate := tickerPrice.Mul(decimal.NewFromInt(1).Sub(priceRate)).Truncate(int32(tradeSet.PriceDigit))
|
|
||||||
// addPosition.Price = truncate.String()
|
|
||||||
// } else {
|
|
||||||
// truncate := tickerPrice.Mul(decimal.NewFromInt(1).Add(priceRate)).Truncate(int32(tradeSet.PriceDigit))
|
|
||||||
// addPosition.Price = truncate.String()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// } else { //实际价格下单
|
|
||||||
// addPosition.Price = req.Price.Truncate(int32(tradeSet.PriceDigit)).String()
|
|
||||||
// addPosition.SignPriceType = req.PricePattern
|
|
||||||
// addPosition.Rate = "0"
|
|
||||||
// }
|
|
||||||
// if tradeSet.Currency != "USDT" { //不是U本位
|
|
||||||
// //获取币本位兑换u的价格
|
|
||||||
// ticker2 := models2.TradeSet{}
|
|
||||||
// tickerVal, _ := helper.DefaultRedis.GetString(fmt.Sprintf(global.TICKER_SPOT, req.ExchangeType, strings.ToUpper(tradeSet.Coin+"USDT")))
|
|
||||||
|
|
||||||
// if tickerVal == "" {
|
|
||||||
// logger.Error("查询行情失败")
|
|
||||||
// return fmt.Errorf("交易对:%s 获取u本位行情失败", req.Symbol)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// err := sonic.Unmarshal([]byte(tickerVal), &ticker2)
|
|
||||||
|
|
||||||
// if ticker2.LastPrice == "" {
|
|
||||||
// logger.Errorf("查询行情失败 %s err:%v", strings.ToUpper(tradeSet.Coin+"USDT"), err)
|
|
||||||
// return fmt.Errorf("交易对:%s 获取u本位行情 反序列化失败", req.Symbol)
|
|
||||||
// }
|
|
||||||
// //LTCBTC --> LTCUSDT
|
|
||||||
// uTickerPrice := utility.StrToDecimal(ticker2.LastPrice) //94069
|
|
||||||
// //换算成U
|
|
||||||
// //div := decimal.NewFromInt(1).Div(uTickerPrice) //0.0000106365
|
|
||||||
// //在换算成对应交易对对应的价值
|
|
||||||
// //LTCBTC --> LTCUSDT == LTCUSDT -- 100.502
|
|
||||||
// div := tickerPrice.Div(decimal.NewFromInt(1).Div(uTickerPrice))
|
|
||||||
// //计算下单数量
|
|
||||||
// addPosition.Num = req.BuyPrice.Div(div).Truncate(int32(tradeSet.AmountDigit)).String()
|
|
||||||
// } else {
|
|
||||||
// fromString, _ := decimal.NewFromString(addPosition.Price)
|
|
||||||
// addPosition.Num = req.BuyPrice.Div(fromString).Truncate(int32(tradeSet.AmountDigit)).String()
|
|
||||||
// }
|
|
||||||
// //事务保存
|
|
||||||
// err := e.Orm.Transaction(func(tx *gorm.DB) error {
|
|
||||||
// //添加加仓单
|
|
||||||
// if err := tx.Create(&addPosition).Error; err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// //止盈、减仓
|
|
||||||
// orders, err := makeFuturesTakeAndReduce(&addPosition, ext, tradeSet)
|
|
||||||
|
|
||||||
// if err != nil {
|
|
||||||
// logger.Error("构造加仓单止盈、减仓失败")
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if err := e.Orm.Create(&orders).Error; err != nil {
|
|
||||||
// logger.Error("保存加仓单止盈、减仓失败")
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// //处理减仓单
|
|
||||||
// for index := range orders {
|
|
||||||
// //减仓单且 减仓比例大于0 小于100 就冲下止盈止损
|
|
||||||
// if orders[index].OrderType == 4 && ext.ReduceNumRatio.Cmp(decimal.Zero) > 0 && ext.ReduceNumRatio.Cmp(decimal.NewFromInt(100)) < 0 {
|
|
||||||
// reduceChildOrders, err := makeReduceTakeAndStoploss(&(orders[index]), ext, tradeSet)
|
|
||||||
|
|
||||||
// if err != nil {
|
|
||||||
// logger.Error("生产加仓单止盈、减仓失败")
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if len(reduceChildOrders) == 0 {
|
|
||||||
// continue
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if err := e.Orm.Create(&reduceChildOrders).Error; err != nil {
|
|
||||||
// logger.Error("报错减仓后止盈止损失败")
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// ext.OrderId = addPosition.Id
|
|
||||||
// if err := tx.Create(&ext).Error; err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return nil
|
|
||||||
// })
|
|
||||||
|
|
||||||
// if err != nil {
|
|
||||||
// logger.Errorf("交易对:%s 添加加仓订单失败", req.Symbol)
|
|
||||||
// return fmt.Errorf("交易对:%s 添加加仓订单失败", req.Symbol)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// return nil
|
|
||||||
// }
|
|
||||||
|
|||||||
1
app/admin/service/line_pre_order_strategy.go
Normal file
1
app/admin/service/line_pre_order_strategy.go
Normal file
@ -0,0 +1 @@
|
|||||||
|
package service
|
||||||
178
app/admin/service/line_reduce_strategy.go
Normal file
178
app/admin/service/line_reduce_strategy.go
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/app/admin/service/dto"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
"go-admin/common/const/rediskey"
|
||||||
|
cDto "go-admin/common/dto"
|
||||||
|
"go-admin/common/helper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReduceStrategy struct {
|
||||||
|
service.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPage 获取LineReduceStrategy列表
|
||||||
|
func (e *LineReduceStrategy) GetPage(c *dto.LineReduceStrategyGetPageReq, p *actions.DataPermission, list *[]models.LineReduceStrategy, count *int64) error {
|
||||||
|
var err error
|
||||||
|
var data models.LineReduceStrategy
|
||||||
|
|
||||||
|
err = e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
cDto.MakeCondition(c.GetNeedSearch()),
|
||||||
|
cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).
|
||||||
|
Find(list).Limit(-1).Offset(-1).
|
||||||
|
Count(count).Error
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("LineReduceStrategyService GetPage error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取LineReduceStrategy对象
|
||||||
|
func (e *LineReduceStrategy) Get(d *dto.LineReduceStrategyGetReq, p *actions.DataPermission, model *models.LineReduceStrategy) error {
|
||||||
|
var data models.LineReduceStrategy
|
||||||
|
|
||||||
|
err := e.Orm.Model(&data).
|
||||||
|
Preload("Items").
|
||||||
|
Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).
|
||||||
|
First(model, d.GetId()).Error
|
||||||
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
err = errors.New("查看对象不存在或无权查看")
|
||||||
|
e.Log.Errorf("Service GetLineReduceStrategy error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("db error:%s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据id获取对象
|
||||||
|
func (e *LineReduceStrategy) GetById(id int) (models.LineReduceStrategy, error) {
|
||||||
|
result := models.LineReduceStrategy{}
|
||||||
|
key := fmt.Sprintf(rediskey.ReduceStrategy, id)
|
||||||
|
str, _ := helper.DefaultRedis.GetString(key)
|
||||||
|
|
||||||
|
if str != "" {
|
||||||
|
sonic.Unmarshal([]byte(str), &result)
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.Id == 0 {
|
||||||
|
if err := e.Orm.Model(&models.LineReduceStrategy{}).First(&result, id).Error; err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 创建LineReduceStrategy对象
|
||||||
|
func (e *LineReduceStrategy) Insert(c *dto.LineReduceStrategyInsertReq) error {
|
||||||
|
var err error
|
||||||
|
var data models.LineReduceStrategy
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
e.Orm.Model(&models.LineReduceStrategy{}).Where("name = ?", c.Name).Count(&count)
|
||||||
|
if count > 0 {
|
||||||
|
err = errors.New("策略名称重复")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Generate(&data)
|
||||||
|
err = e.Orm.Create(&data).Error
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("LineReduceStrategyService Insert error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.saveCache(data)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 修改LineReduceStrategy对象
|
||||||
|
func (e *LineReduceStrategy) Update(c *dto.LineReduceStrategyUpdateReq, p *actions.DataPermission) error {
|
||||||
|
var err error
|
||||||
|
var data = models.LineReduceStrategy{}
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
e.Orm.Model(&models.LineReduceStrategy{}).Where("name = ? AND id !=?", c.Name, c.GetId()).Count(&count)
|
||||||
|
if count > 0 {
|
||||||
|
err = errors.New("策略名称重复")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Orm.Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).First(&data, c.GetId())
|
||||||
|
c.Generate(&data)
|
||||||
|
|
||||||
|
err = e.Orm.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if errr := tx.Delete(&models.LineReduceStrategyItem{}, "reduce_strategy_id =?", c.GetId()).Error; errr != nil {
|
||||||
|
return errr
|
||||||
|
}
|
||||||
|
|
||||||
|
db := tx.Save(&data)
|
||||||
|
if err = db.Error; err != nil {
|
||||||
|
e.Log.Errorf("LineReduceStrategyService Save error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if db.RowsAffected == 0 {
|
||||||
|
return errors.New("无权更新该数据")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
e.saveCache(data)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineReduceStrategy) saveCache(data models.LineReduceStrategy) {
|
||||||
|
str, _ := sonic.MarshalString(data)
|
||||||
|
|
||||||
|
if str != "" {
|
||||||
|
key := fmt.Sprintf(rediskey.ReduceStrategy, data.Id)
|
||||||
|
|
||||||
|
if errr := helper.DefaultRedis.SetString(key, str); errr != nil {
|
||||||
|
e.Log.Errorf("LineReduceStrategyService SetString error:%s \r\n", errr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove 删除LineReduceStrategy
|
||||||
|
func (e *LineReduceStrategy) Remove(d *dto.LineReduceStrategyDeleteReq, p *actions.DataPermission) error {
|
||||||
|
var data models.LineReduceStrategy
|
||||||
|
|
||||||
|
db := e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).Delete(&data, d.GetId())
|
||||||
|
if err := db.Error; err != nil {
|
||||||
|
e.Log.Errorf("Service RemoveLineReduceStrategy error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if db.RowsAffected == 0 {
|
||||||
|
return errors.New("无权删除该数据")
|
||||||
|
}
|
||||||
|
|
||||||
|
key := fmt.Sprintf(rediskey.ReduceStrategy, data.Id)
|
||||||
|
helper.DefaultRedis.DeleteString(key)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
109
app/admin/service/line_reduce_strategy_item.go
Normal file
109
app/admin/service/line_reduce_strategy_item.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/app/admin/service/dto"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
cDto "go-admin/common/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineReduceStrategyItem struct {
|
||||||
|
service.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPage 获取LineReduceStrategyItem列表
|
||||||
|
func (e *LineReduceStrategyItem) GetPage(c *dto.LineReduceStrategyItemGetPageReq, p *actions.DataPermission, list *[]models.LineReduceStrategyItem, count *int64) error {
|
||||||
|
var err error
|
||||||
|
var data models.LineReduceStrategyItem
|
||||||
|
|
||||||
|
err = e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
cDto.MakeCondition(c.GetNeedSearch()),
|
||||||
|
cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).
|
||||||
|
Find(list).Limit(-1).Offset(-1).
|
||||||
|
Count(count).Error
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("LineReduceStrategyItemService GetPage error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取LineReduceStrategyItem对象
|
||||||
|
func (e *LineReduceStrategyItem) Get(d *dto.LineReduceStrategyItemGetReq, p *actions.DataPermission, model *models.LineReduceStrategyItem) error {
|
||||||
|
var data models.LineReduceStrategyItem
|
||||||
|
|
||||||
|
err := e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).
|
||||||
|
First(model, d.GetId()).Error
|
||||||
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
err = errors.New("查看对象不存在或无权查看")
|
||||||
|
e.Log.Errorf("Service GetLineReduceStrategyItem error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("db error:%s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 创建LineReduceStrategyItem对象
|
||||||
|
func (e *LineReduceStrategyItem) Insert(c *dto.LineReduceStrategyItemInsertReq) error {
|
||||||
|
var err error
|
||||||
|
var data models.LineReduceStrategyItem
|
||||||
|
c.Generate(&data)
|
||||||
|
err = e.Orm.Create(&data).Error
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("LineReduceStrategyItemService Insert error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 修改LineReduceStrategyItem对象
|
||||||
|
func (e *LineReduceStrategyItem) Update(c *dto.LineReduceStrategyItemUpdateReq, p *actions.DataPermission) error {
|
||||||
|
var err error
|
||||||
|
var data = models.LineReduceStrategyItem{}
|
||||||
|
e.Orm.Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).First(&data, c.GetId())
|
||||||
|
c.Generate(&data)
|
||||||
|
|
||||||
|
db := e.Orm.Save(&data)
|
||||||
|
if err = db.Error; err != nil {
|
||||||
|
e.Log.Errorf("LineReduceStrategyItemService Save error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if db.RowsAffected == 0 {
|
||||||
|
return errors.New("无权更新该数据")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove 删除LineReduceStrategyItem
|
||||||
|
func (e *LineReduceStrategyItem) Remove(d *dto.LineReduceStrategyItemDeleteReq, p *actions.DataPermission) error {
|
||||||
|
var data models.LineReduceStrategyItem
|
||||||
|
|
||||||
|
db := e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).Delete(&data, d.GetId())
|
||||||
|
if err := db.Error; err != nil {
|
||||||
|
e.Log.Errorf("Service RemoveLineReduceStrategyItem error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if db.RowsAffected == 0 {
|
||||||
|
return errors.New("无权删除该数据")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
109
app/admin/service/line_strategy_template.go
Normal file
109
app/admin/service/line_strategy_template.go
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/app/admin/service/dto"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
cDto "go-admin/common/dto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineStrategyTemplate struct {
|
||||||
|
service.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPage 获取LineStrategyTemplate列表
|
||||||
|
func (e *LineStrategyTemplate) GetPage(c *dto.LineStrategyTemplateGetPageReq, p *actions.DataPermission, list *[]models.LineStrategyTemplate, count *int64) error {
|
||||||
|
var err error
|
||||||
|
var data models.LineStrategyTemplate
|
||||||
|
|
||||||
|
err = e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
cDto.MakeCondition(c.GetNeedSearch()),
|
||||||
|
cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).
|
||||||
|
Find(list).Limit(-1).Offset(-1).
|
||||||
|
Count(count).Error
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("LineStrategyTemplateService GetPage error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取LineStrategyTemplate对象
|
||||||
|
func (e *LineStrategyTemplate) Get(d *dto.LineStrategyTemplateGetReq, p *actions.DataPermission, model *models.LineStrategyTemplate) error {
|
||||||
|
var data models.LineStrategyTemplate
|
||||||
|
|
||||||
|
err := e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).
|
||||||
|
First(model, d.GetId()).Error
|
||||||
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
err = errors.New("查看对象不存在或无权查看")
|
||||||
|
e.Log.Errorf("Service GetLineStrategyTemplate error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("db error:%s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 创建LineStrategyTemplate对象
|
||||||
|
func (e *LineStrategyTemplate) Insert(c *dto.LineStrategyTemplateInsertReq) error {
|
||||||
|
var err error
|
||||||
|
var data models.LineStrategyTemplate
|
||||||
|
c.Generate(&data)
|
||||||
|
err = e.Orm.Create(&data).Error
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("LineStrategyTemplateService Insert error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 修改LineStrategyTemplate对象
|
||||||
|
func (e *LineStrategyTemplate) Update(c *dto.LineStrategyTemplateUpdateReq, p *actions.DataPermission) error {
|
||||||
|
var err error
|
||||||
|
var data = models.LineStrategyTemplate{}
|
||||||
|
e.Orm.Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).First(&data, c.GetId())
|
||||||
|
c.Generate(&data)
|
||||||
|
|
||||||
|
db := e.Orm.Save(&data)
|
||||||
|
if err = db.Error; err != nil {
|
||||||
|
e.Log.Errorf("LineStrategyTemplateService Save error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if db.RowsAffected == 0 {
|
||||||
|
return errors.New("无权更新该数据")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove 删除LineStrategyTemplate
|
||||||
|
func (e *LineStrategyTemplate) Remove(d *dto.LineStrategyTemplateDeleteReq, p *actions.DataPermission) error {
|
||||||
|
var data models.LineStrategyTemplate
|
||||||
|
|
||||||
|
db := e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).Delete(&data, d.GetId())
|
||||||
|
if err := db.Error; err != nil {
|
||||||
|
e.Log.Errorf("Service RemoveLineStrategyTemplate error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if db.RowsAffected == 0 {
|
||||||
|
return errors.New("无权删除该数据")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -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) {
|
||||||
|
|||||||
147
app/admin/service/line_symbol_price.go
Normal file
147
app/admin/service/line_symbol_price.go
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/app/admin/service/dto"
|
||||||
|
"go-admin/common/actions"
|
||||||
|
"go-admin/common/const/rediskey"
|
||||||
|
cDto "go-admin/common/dto"
|
||||||
|
"go-admin/common/helper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LineSymbolPrice struct {
|
||||||
|
service.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPage 获取LineSymbolPrice列表
|
||||||
|
func (e *LineSymbolPrice) GetPage(c *dto.LineSymbolPriceGetPageReq, p *actions.DataPermission, list *[]models.LineSymbolPrice, count *int64) error {
|
||||||
|
var err error
|
||||||
|
var data models.LineSymbolPrice
|
||||||
|
|
||||||
|
err = e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
cDto.MakeCondition(c.GetNeedSearch()),
|
||||||
|
cDto.Paginate(c.GetPageSize(), c.GetPageIndex()),
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).
|
||||||
|
Find(list).Limit(-1).Offset(-1).
|
||||||
|
Count(count).Error
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("LineSymbolPriceService GetPage error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取LineSymbolPrice对象
|
||||||
|
func (e *LineSymbolPrice) Get(d *dto.LineSymbolPriceGetReq, p *actions.DataPermission, model *models.LineSymbolPrice) error {
|
||||||
|
var data models.LineSymbolPrice
|
||||||
|
|
||||||
|
err := e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).
|
||||||
|
First(model, d.GetId()).Error
|
||||||
|
if err != nil && errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
err = errors.New("查看对象不存在或无权查看")
|
||||||
|
e.Log.Errorf("Service GetLineSymbolPrice error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("db error:%s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert 创建LineSymbolPrice对象
|
||||||
|
func (e *LineSymbolPrice) Insert(c *dto.LineSymbolPriceInsertReq) error {
|
||||||
|
var err error
|
||||||
|
var data models.LineSymbolPrice
|
||||||
|
c.Generate(&data)
|
||||||
|
var count int64
|
||||||
|
|
||||||
|
e.Orm.Model(&models.LineSymbolPrice{}).Where("symbol = ?", c.Symbol).Count(&count)
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
return errors.New("交易对已存在,请不要重复添加")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = e.Orm.Create(&data).Error
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("LineSymbolPriceService Insert error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
e.cacheList()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineSymbolPrice) cacheList() {
|
||||||
|
symbols := make([]string, 0)
|
||||||
|
if err := e.Orm.Model(&models.LineSymbolPrice{}).Where("status =1").Pluck("symbol", &symbols).Error; err != nil {
|
||||||
|
e.Log.Errorf("获取可用交易对失败,err:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(symbols) > 0 {
|
||||||
|
if err := helper.DefaultRedis.SetListCache(rediskey.CacheSymbolLastPrice, 0, symbols...); err != nil {
|
||||||
|
e.Log.Errorf("设置可缓存价格交易对失败")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update 修改LineSymbolPrice对象
|
||||||
|
func (e *LineSymbolPrice) Update(c *dto.LineSymbolPriceUpdateReq, p *actions.DataPermission) error {
|
||||||
|
var err error
|
||||||
|
var data = models.LineSymbolPrice{}
|
||||||
|
var count int64
|
||||||
|
e.Orm.Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).First(&data, c.GetId())
|
||||||
|
c.Generate(&data)
|
||||||
|
|
||||||
|
e.Orm.Model(&models.LineSymbolPrice{}).Where("symbol = ? and id !=?", c.Symbol, c.GetId()).Count(&count)
|
||||||
|
if count > 0 {
|
||||||
|
return errors.New("交易对已存在,请不要重复添加")
|
||||||
|
}
|
||||||
|
|
||||||
|
db := e.Orm.Save(&data)
|
||||||
|
if err = db.Error; err != nil {
|
||||||
|
e.Log.Errorf("LineSymbolPriceService Save error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if db.RowsAffected == 0 {
|
||||||
|
return errors.New("无权更新该数据")
|
||||||
|
}
|
||||||
|
e.cacheList()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove 删除LineSymbolPrice
|
||||||
|
func (e *LineSymbolPrice) Remove(d *dto.LineSymbolPriceDeleteReq, p *actions.DataPermission) error {
|
||||||
|
var data models.LineSymbolPrice
|
||||||
|
|
||||||
|
db := e.Orm.Model(&data).
|
||||||
|
Scopes(
|
||||||
|
actions.Permission(data.TableName(), p),
|
||||||
|
).Delete(&data, d.GetId())
|
||||||
|
if err := db.Error; err != nil {
|
||||||
|
e.Log.Errorf("Service RemoveLineSymbolPrice error:%s \r\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if db.RowsAffected == 0 {
|
||||||
|
return errors.New("无权删除该数据")
|
||||||
|
}
|
||||||
|
e.cacheList()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *LineSymbolPrice) InitCache() {
|
||||||
|
e.cacheList()
|
||||||
|
}
|
||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
@ -128,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{}
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import (
|
|||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
"go-admin/pkg/utility/snowflakehelper"
|
"go-admin/pkg/utility/snowflakehelper"
|
||||||
"go-admin/services/binanceservice"
|
"go-admin/services/binanceservice"
|
||||||
|
"go-admin/services/cacheservice"
|
||||||
"go-admin/services/fileservice"
|
"go-admin/services/fileservice"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -241,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, _ := binanceservice.GetTradeSet(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)
|
||||||
@ -359,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, _ := binanceservice.GetTradeSet(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)
|
||||||
|
|||||||
12
app/jobs/order_job.go
Normal file
12
app/jobs/order_job.go
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package jobs
|
||||||
|
|
||||||
|
//波段交易策略任务
|
||||||
|
type StrategyOrderJob struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t StrategyOrderJob) Exec(arg interface{}) error {
|
||||||
|
// strategyOrderService:=
|
||||||
|
//todo 策略订单任务
|
||||||
|
|
||||||
|
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)
|
||||||
|
|||||||
@ -86,6 +86,10 @@ func run() error {
|
|||||||
clearLogJob(db, ctx)
|
clearLogJob(db, ctx)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
//自动重启websocket
|
||||||
|
utility.SafeGo(func() {
|
||||||
|
reconnect(ctx)
|
||||||
|
})
|
||||||
// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)
|
// 等待中断信号以优雅地关闭服务器(设置 5 秒的超时时间)
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
signal.Notify(quit, os.Interrupt)
|
signal.Notify(quit, os.Interrupt)
|
||||||
@ -133,3 +137,18 @@ func clearLogJob(db *gorm.DB, ctx context.Context) {
|
|||||||
fileservice.ClearLogs(db)
|
fileservice.ClearLogs(db)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 定时重连websocket
|
||||||
|
func reconnect(ctx context.Context) error {
|
||||||
|
ticker := time.NewTicker(time.Hour * 1)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
|
case <-ticker.C:
|
||||||
|
serverinit.RestartConnect()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@ -19,11 +19,18 @@ 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}
|
||||||
|
StrategySpotOrderList = "strategy_spot_order_list:%s"
|
||||||
|
//策略合约订单集合 {交易所类型 exchange_type}
|
||||||
|
StrategyFutOrderList = "strategy_fut_order_list:%s"
|
||||||
|
|
||||||
API_USER = "api_user:%v" // api用户
|
API_USER = "api_user:%v" // api用户
|
||||||
SystemSetting = "system_setting" //系统设置
|
SystemSetting = "system_setting" //系统设置
|
||||||
@ -41,6 +48,16 @@ const (
|
|||||||
SpotTrigger = "spot_trigger_lock:%v_%s" //现货触发 {apiuserid|symbol}
|
SpotTrigger = "spot_trigger_lock:%v_%s" //现货触发 {apiuserid|symbol}
|
||||||
FutTrigger = "fut_trigger_lock:%v_%s" //合约触发 {apiuserid|symbol}
|
FutTrigger = "fut_trigger_lock:%v_%s" //合约触发 {apiuserid|symbol}
|
||||||
|
|
||||||
|
//波段现货触发{apiuserid|symbol}
|
||||||
|
StrategySpotTriggerLock = "strategy_spot_trigger_l:%v_%s"
|
||||||
|
//波段合约触发{apiuserid|symbol}
|
||||||
|
StrategyFutTriggerLock = "strategy_fut_trigger_l:%v_%s"
|
||||||
|
|
||||||
|
//减仓波段合约触发 {apiuserid|symbol}
|
||||||
|
ReduceStrategyFutTriggerLock = "reduce_strategy_fut_trigger_l:%v_%s"
|
||||||
|
//减仓波段现货触发 {apiuserid|symbol}
|
||||||
|
ReduceStrategySpotTriggerLock = "reduce_strategy_spot_trigger_l:%v_%s"
|
||||||
|
|
||||||
SpotCallBack = "spot_callback:%s" //现货回调 {ordersn}
|
SpotCallBack = "spot_callback:%s" //现货回调 {ordersn}
|
||||||
FutCallBack = "fut_callback:%s" //合约回调 {ordersn}
|
FutCallBack = "fut_callback:%s" //合约回调 {ordersn}
|
||||||
|
|
||||||
@ -61,6 +78,11 @@ const (
|
|||||||
SpotPosition = "spot_position:%s:%v:%s_%s"
|
SpotPosition = "spot_position:%s:%v:%s_%s"
|
||||||
//合约持仓 {exchangeType,apiuserid,symbol,side}
|
//合约持仓 {exchangeType,apiuserid,symbol,side}
|
||||||
FuturePosition = "future_position:%s:%v:%s_%s"
|
FuturePosition = "future_position:%s:%v:%s_%s"
|
||||||
|
|
||||||
|
//现货减仓单减仓策略 {exchangeType}
|
||||||
|
SpotOrderReduceStrategyList = "spot_reduce_strategy_list:%s"
|
||||||
|
//合约减仓单减仓策略 {exchangeType}
|
||||||
|
FutOrderReduceStrategyList = "fut_reduce_strategy_list:%s"
|
||||||
//需要清理键值---------END-----------------
|
//需要清理键值---------END-----------------
|
||||||
|
|
||||||
//定时取消限价并下市价锁
|
//定时取消限价并下市价锁
|
||||||
@ -74,6 +96,19 @@ const (
|
|||||||
AveRequestToken = "ave_request_token" // AVE请求token
|
AveRequestToken = "ave_request_token" // AVE请求token
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
//现货最后成交价 sort set {exchangeType,symbol}
|
||||||
|
SpotTickerLastPrice = "spot_ticker_last_price:%s:%s"
|
||||||
|
//合约最后成交价 sort set {exchangeType,symbol}
|
||||||
|
FutureTickerLastPrice = "fut_ticker_last_price:%s:%s"
|
||||||
|
|
||||||
|
//允许缓存交易对价格的交易对 list
|
||||||
|
CacheSymbolLastPrice = "cache_symbol_price"
|
||||||
|
|
||||||
|
//减仓策略缓存 {id}
|
||||||
|
ReduceStrategy = "reduce_stragy:%d"
|
||||||
|
)
|
||||||
|
|
||||||
// 用户下单
|
// 用户下单
|
||||||
const (
|
const (
|
||||||
MemberShipPre = "member_ship_pre:%v" //用户开通会员预下单 单价缓存{payable_amount}
|
MemberShipPre = "member_ship_pre:%v" //用户开通会员预下单 单价缓存{payable_amount}
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"math"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
@ -572,6 +573,33 @@ func (r *RedisHelper) HKeys(key string) ([]string, error) {
|
|||||||
return fields, nil
|
return fields, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *RedisHelper) HExists(key, field, value string) (bool, error) {
|
||||||
|
exists, err := r.client.HExists(r.ctx, key, field).Result()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("check existence failed: %v", err)
|
||||||
|
}
|
||||||
|
if !exists {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
storedValue, err := r.client.HGet(r.ctx, key, field).Result()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("get value failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果值是 JSON,比较前反序列化
|
||||||
|
var storedObj, inputObj interface{}
|
||||||
|
if err := sonic.UnmarshalString(storedValue, &storedObj); err != nil {
|
||||||
|
return false, fmt.Errorf("unmarshal stored value failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := sonic.UnmarshalString(value, &inputObj); err != nil {
|
||||||
|
return false, fmt.Errorf("unmarshal input value failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 比较两个对象(需要根据实际类型调整)
|
||||||
|
return fmt.Sprintf("%v", storedObj) == fmt.Sprintf("%v", inputObj), nil
|
||||||
|
}
|
||||||
|
|
||||||
// DelSet 从集合中删除元素
|
// DelSet 从集合中删除元素
|
||||||
func (r *RedisHelper) DelSet(key string, value string) error {
|
func (r *RedisHelper) DelSet(key string, value string) error {
|
||||||
_, err := r.client.SRem(r.ctx, key, value).Result()
|
_, err := r.client.SRem(r.ctx, key, value).Result()
|
||||||
@ -609,7 +637,7 @@ func (e *RedisHelper) SignelAdd(key string, score float64, member string) error
|
|||||||
_, err := e.client.ZRemRangeByScore(e.ctx, key, scoreStr, scoreStr).Result()
|
_, err := e.client.ZRemRangeByScore(e.ctx, key, scoreStr, scoreStr).Result()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("删除score失败", err.Error())
|
fmt.Printf("删除score失败,err:%s", err.Error())
|
||||||
}
|
}
|
||||||
_, err = e.client.ZAdd(e.ctx, key, &redis.Z{
|
_, err = e.client.ZAdd(e.ctx, key, &redis.Z{
|
||||||
Score: score,
|
Score: score,
|
||||||
@ -642,6 +670,46 @@ func (e *RedisHelper) DelSortSet(key, member string) error {
|
|||||||
return e.client.ZRem(e.ctx, key, member).Err()
|
return e.client.ZRem(e.ctx, key, member).Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveBeforeScore 移除 Sorted Set 中分数小于等于指定值的数据
|
||||||
|
// key: Sorted Set 的键
|
||||||
|
// score: 分数上限,所有小于等于此分数的元素将被移除
|
||||||
|
// 返回值: 移除的元素数量和可能的错误
|
||||||
|
func (e *RedisHelper) RemoveBeforeScore(key string, score float64) (int64, error) {
|
||||||
|
if key == "" {
|
||||||
|
return 0, errors.New("key 不能为空")
|
||||||
|
}
|
||||||
|
if math.IsNaN(score) || math.IsInf(score, 0) {
|
||||||
|
return 0, errors.New("score 必须是有效数字")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 ZRemRangeByScore 移除数据
|
||||||
|
count, err := e.client.ZRemRangeByScore(e.ctx, key, "-inf", strconv.FormatFloat(score, 'f', -1, 64)).Result()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("移除 Sorted Set 数据失败, key: %s, score: %f, err: %v", key, score, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNextAfterScore 获取指定分数及之后的第一条数据(包含指定分数)
|
||||||
|
func (e *RedisHelper) GetNextAfterScore(key string, score float64) (string, error) {
|
||||||
|
// 使用 ZRangeByScore 获取大于等于 score 的第一条数据
|
||||||
|
zs, err := e.client.ZRangeByScoreWithScores(e.ctx, key, &redis.ZRangeBy{
|
||||||
|
Min: fmt.Sprintf("%f", score), // 包含指定分数
|
||||||
|
Max: "+inf", // 上限为正无穷
|
||||||
|
Offset: 0, // 从第 0 条开始
|
||||||
|
Count: 1, // 只取 1 条
|
||||||
|
}).Result()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("获取数据失败: %v", err)
|
||||||
|
}
|
||||||
|
if len(zs) == 0 {
|
||||||
|
return "", nil // 没有符合条件的元素
|
||||||
|
}
|
||||||
|
return zs[0].Member.(string), nil
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
获取sort set 所有数据
|
获取sort set 所有数据
|
||||||
*/
|
*/
|
||||||
@ -656,6 +724,22 @@ func (e *RedisHelper) GetRevRangeScoresSortSet(key string) ([]redis.Z, error) {
|
|||||||
return e.client.ZRevRangeWithScores(e.ctx, key, 0, -1).Result()
|
return e.client.ZRevRangeWithScores(e.ctx, key, 0, -1).Result()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取最后一条数据
|
||||||
|
func (e *RedisHelper) GetLastSortSet(key string) ([]redis.Z, error) {
|
||||||
|
// 获取最后一个元素及其分数
|
||||||
|
results, err := e.client.ZRevRangeWithScores(e.ctx, key, 0, 0).Result()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get last member: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有数据,返回空
|
||||||
|
if len(results) == 0 {
|
||||||
|
return []redis.Z{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
// 获取指定区间数据
|
// 获取指定区间数据
|
||||||
func (e *RedisHelper) GetSortSetMembers(key string, start, stop int64) ([]string, error) {
|
func (e *RedisHelper) GetSortSetMembers(key string, start, stop int64) ([]string, error) {
|
||||||
return e.client.ZRange(e.ctx, key, start, stop).Result()
|
return e.client.ZRange(e.ctx, key, start, stop).Result()
|
||||||
|
|||||||
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,7 +8,6 @@ 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"
|
||||||
@ -31,9 +30,14 @@ func BusinessInit(db *gorm.DB) {
|
|||||||
|
|
||||||
//初始化参数配置
|
//初始化参数配置
|
||||||
cacheservice.InitConfigCache(db)
|
cacheservice.InitConfigCache(db)
|
||||||
|
//初始化可缓存价格交易对
|
||||||
|
symbolPriceService := service.LineSymbolPrice{}
|
||||||
|
symbolPriceService.Orm = db
|
||||||
|
symbolPriceService.Log = logger.NewHelper(sdk.Runtime.GetLogger()).WithFields(map[string]interface{}{})
|
||||||
|
symbolPriceService.InitCache()
|
||||||
|
|
||||||
//初始化订单配置
|
//初始化订单配置
|
||||||
binanceservice.ResetSystemSetting(db)
|
cacheservice.ResetSystemSetting(db)
|
||||||
lineApiUser := service.LineApiUser{}
|
lineApiUser := service.LineApiUser{}
|
||||||
lineApiUser.Orm = db
|
lineApiUser.Orm = db
|
||||||
if err := lineApiUser.InitCache(); err != nil {
|
if err := lineApiUser.InitCache(); err != nil {
|
||||||
@ -41,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{}
|
||||||
|
|||||||
@ -84,3 +84,44 @@ func UserSubscribeInit(orm *gorm.DB, ctx context.Context) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 重启连接
|
||||||
|
func RestartConnect() error {
|
||||||
|
spotSockets := excservice.SpotSockets
|
||||||
|
futuresSockets := excservice.FutureSockets
|
||||||
|
timeOut := 22 * time.Hour
|
||||||
|
|
||||||
|
for _, item := range spotSockets {
|
||||||
|
//超过22小时,重新连接
|
||||||
|
if time.Since(item.ConnectTime) > timeOut {
|
||||||
|
if err := item.ReplaceConnection(); err != nil {
|
||||||
|
log.Errorf("现货重启连接失败 key:%s,error:%s", item.GetKey(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range futuresSockets {
|
||||||
|
//超过22小时,重新连接
|
||||||
|
if time.Since(item.ConnectTime) > timeOut {
|
||||||
|
if err := item.ReplaceConnection(); err != nil {
|
||||||
|
log.Errorf("合约重启连接失败 key:%s,error:%s", item.GetKey(), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 假死重启
|
||||||
|
func DeadCheck() {
|
||||||
|
spotSockets := excservice.SpotSockets
|
||||||
|
futuresSockets := excservice.FutureSockets
|
||||||
|
|
||||||
|
for _, item := range spotSockets {
|
||||||
|
item.DeadCheck()
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range futuresSockets {
|
||||||
|
item.DeadCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
BIN
exchange-single
BIN
exchange-single
Binary file not shown.
20
go.mod
20
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
|
||||||
@ -47,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
|
||||||
@ -174,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
|
||||||
|
|||||||
@ -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"` //开盘价格
|
||||||
|
|||||||
@ -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"`
|
||||||
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -1,16 +1,15 @@
|
|||||||
package binanceservice
|
package binanceservice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"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/const/rediskey"
|
||||||
"go-admin/common/global"
|
"go-admin/common/global"
|
||||||
"go-admin/common/helper"
|
"go-admin/common/helper"
|
||||||
"go-admin/models"
|
|
||||||
"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"
|
||||||
@ -18,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"
|
||||||
)
|
)
|
||||||
@ -27,32 +25,6 @@ type AddPosition struct {
|
|||||||
Db *gorm.DB
|
Db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取缓存交易对
|
|
||||||
// symbolType 0-现货 1-合约
|
|
||||||
func GetTradeSet(symbol string, symbolType int) (models.TradeSet, error) {
|
|
||||||
result := models.TradeSet{}
|
|
||||||
val := ""
|
|
||||||
|
|
||||||
switch symbolType {
|
|
||||||
case 0:
|
|
||||||
key := fmt.Sprintf(global.TICKER_SPOT, global.EXCHANGE_BINANCE, symbol)
|
|
||||||
val, _ = helper.DefaultRedis.GetString(key)
|
|
||||||
case 1:
|
|
||||||
key := fmt.Sprintf(global.TICKER_FUTURES, global.EXCHANGE_BINANCE, symbol)
|
|
||||||
val, _ = helper.DefaultRedis.GetString(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
if val != "" {
|
|
||||||
if err := sonic.Unmarshal([]byte(val), &result); err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return result, errors.New("未找到交易对信息")
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetRisk(futApi *FutRestApi, apiInfo *DbModels.LineApiUser, symbol string) []PositionRisk {
|
func GetRisk(futApi *FutRestApi, apiInfo *DbModels.LineApiUser, symbol string) []PositionRisk {
|
||||||
for x := 0; x < 5; x++ {
|
for x := 0; x < 5; x++ {
|
||||||
risks, _ := futApi.GetPositionV3(apiInfo, symbol)
|
risks, _ := futApi.GetPositionV3(apiInfo, symbol)
|
||||||
@ -133,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 {
|
||||||
@ -237,44 +209,6 @@ 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合约
|
||||||
@ -284,7 +218,7 @@ func MainClosePositionClearCache(mainId int, symbolType int) {
|
|||||||
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)
|
||||||
var position AddPositionList
|
var position positiondto.AddPositionList
|
||||||
var stop dto.StopLossRedisList
|
var stop dto.StopLossRedisList
|
||||||
|
|
||||||
for _, item := range spotAddpositionArray {
|
for _, item := range spotAddpositionArray {
|
||||||
@ -312,7 +246,7 @@ func MainClosePositionClearCache(mainId int, symbolType int) {
|
|||||||
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 AddPositionList
|
var position positiondto.AddPositionList
|
||||||
var stop dto.StopLossRedisList
|
var stop dto.StopLossRedisList
|
||||||
|
|
||||||
for _, item := range futAddpositionArray {
|
for _, item := range futAddpositionArray {
|
||||||
@ -337,15 +271,6 @@ func MainClosePositionClearCache(mainId int, symbolType int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取数据库连接
|
|
||||||
func GetDBConnection() *gorm.DB {
|
|
||||||
dbs := sdk.Runtime.GetDb()
|
|
||||||
for _, db := range dbs {
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查询订单
|
// 查询订单
|
||||||
func getPreOrder(db *gorm.DB, orderSn interface{}) (*DbModels.LinePreOrder, error) {
|
func getPreOrder(db *gorm.DB, orderSn interface{}) (*DbModels.LinePreOrder, error) {
|
||||||
preOrder := &DbModels.LinePreOrder{}
|
preOrder := &DbModels.LinePreOrder{}
|
||||||
|
|||||||
65
services/binanceservice/futures_judge_service_test.go
Normal file
65
services/binanceservice/futures_judge_service_test.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package binanceservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go-admin/common/const/rediskey"
|
||||||
|
"go-admin/common/global"
|
||||||
|
"go-admin/common/helper"
|
||||||
|
"go-admin/models"
|
||||||
|
"go-admin/models/positiondto"
|
||||||
|
"go-admin/services/cacheservice"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFutureJudge(t *testing.T) {
|
||||||
|
dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||||
|
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||||
|
helper.InitDefaultRedis("127.0.0.1:6379", "", 2)
|
||||||
|
helper.InitLockRedisConn("127.0.0.1:6379", "", "2")
|
||||||
|
// tradeSet := models.TradeSet{
|
||||||
|
// Coin: "ADA",
|
||||||
|
// Currency: "USDT",
|
||||||
|
// LastPrice: "0.516",
|
||||||
|
// }
|
||||||
|
|
||||||
|
key := fmt.Sprintf(rediskey.FuturesReduceList, global.EXCHANGE_BINANCE)
|
||||||
|
item := `{"id":50,"apiId":49,"mainId":47,"pid":47,"symbol":"ADAUSDT","price":"0.5936","side":"SELL","num":"12","orderSn":"397913127842217984"}`
|
||||||
|
reduceOrder := positiondto.ReduceListItem{}
|
||||||
|
futApi := FutRestApi{}
|
||||||
|
setting, err := cacheservice.GetSystemSetting(db)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("获取系统设置失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := sonic.Unmarshal([]byte(item), &reduceOrder); err != nil {
|
||||||
|
logger.Error("反序列化失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// JudgeFuturesReduce(tradeSet)
|
||||||
|
FuturesReduceTrigger(db, reduceOrder, futApi, setting, key, item, false, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试减仓后减仓触发
|
||||||
|
func TestFutureReduceReduce(t *testing.T) {
|
||||||
|
dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||||
|
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||||
|
sdk.Runtime.SetDb("default", db)
|
||||||
|
helper.InitDefaultRedis("127.0.0.1:6379", "", 2)
|
||||||
|
helper.InitLockRedisConn("127.0.0.1:6379", "", "2")
|
||||||
|
tradeSet := models.TradeSet{
|
||||||
|
Coin: "ADA",
|
||||||
|
Currency: "USDT",
|
||||||
|
LastPrice: "0.5817",
|
||||||
|
PriceDigit: 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
// JudgeFuturesReduce(tradeSet)
|
||||||
|
JudgeFuturesReduce(tradeSet)
|
||||||
|
}
|
||||||
@ -5,7 +5,6 @@ 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"
|
||||||
@ -13,6 +12,7 @@ import (
|
|||||||
"go-admin/models/futuresdto"
|
"go-admin/models/futuresdto"
|
||||||
"go-admin/pkg/httputils"
|
"go-admin/pkg/httputils"
|
||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
|
"go-admin/services/cacheservice"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -261,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) {
|
||||||
@ -862,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)
|
||||||
@ -873,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,6 +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/services/cacheservice"
|
||||||
|
"go-admin/services/commonservice"
|
||||||
|
"go-admin/services/orderservice"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -24,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
|
||||||
@ -151,21 +156,94 @@ 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)
|
||||||
|
|
||||||
if len(reduceVal) == 0 {
|
db := commonservice.GetDBConnection()
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
db := GetDBConnection()
|
|
||||||
futApi := FutRestApi{}
|
futApi := FutRestApi{}
|
||||||
setting, err := GetSystemSetting(db)
|
setting, err := cacheservice.GetSystemSetting(db)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("获取系统设置失败")
|
log.Error("获取系统设置失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reduceOrder := positiondto.ReduceListItem{}
|
||||||
|
tradePrice, _ := decimal.NewFromString(trade.LastPrice)
|
||||||
|
//减仓单减仓策略
|
||||||
|
reduceReduceListKey := fmt.Sprintf(rediskey.FutOrderReduceStrategyList, global.EXCHANGE_BINANCE)
|
||||||
|
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 && !item2.Actived {
|
||||||
|
//买入
|
||||||
|
if item2.TriggerPrice.Cmp(decimal.Zero) > 0 &&
|
||||||
|
tradePrice.Cmp(decimal.Zero) > 0 &&
|
||||||
|
((strings.ToUpper(reduceOrderStrategy.Side) == "SELL" && item2.TriggerPrice.Cmp(tradePrice) >= 0) ||
|
||||||
|
(strings.ToUpper(reduceOrderStrategy.Side) == "BUY" && item2.TriggerPrice.Cmp(tradePrice) <= 0)) {
|
||||||
|
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.ReduceStrategyFutTriggerLock, reduceOrder.ApiId, reduceOrder.Symbol), 50, 15, 100*time.Millisecond)
|
||||||
|
|
||||||
|
if ok, err := lock.AcquireWait(context.Background()); err != nil {
|
||||||
|
log.Error("获取锁失败", err)
|
||||||
|
return
|
||||||
|
} else if ok {
|
||||||
|
defer lock.Release()
|
||||||
|
hasrecord, _ := helper.DefaultRedis.HExists(reduceReduceListKey, utility.IntToString(reduceOrderStrategy.OrderId), item)
|
||||||
|
|
||||||
|
if !hasrecord {
|
||||||
|
log.Debug("减仓缓存中不存在", item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
order, err := orderService.CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("%d 生成订单失败", reduceOrderStrategy.OrderId)
|
||||||
|
}
|
||||||
|
|
||||||
|
reduceOrder.ApiId = order.ApiId
|
||||||
|
reduceOrder.Id = order.Id
|
||||||
|
reduceOrder.Pid = order.Pid
|
||||||
|
reduceOrder.MainId = order.MainId
|
||||||
|
reduceOrder.Symbol = order.Symbol
|
||||||
|
reduceOrder.Side = reduceOrderStrategy.Side
|
||||||
|
reduceOrder.OrderSn = order.OrderSn
|
||||||
|
reduceOrder.Price = item2.Price
|
||||||
|
reduceOrder.Num = item2.Num
|
||||||
|
//下单成功修改策略节点状态
|
||||||
|
if FuturesReduceTrigger(db, reduceOrder, futApi, setting, reduceReduceListKey, item, true, reduceOrderStrategy.OrderId) {
|
||||||
|
reduceOrderStrategy.Items[index].Actived = true
|
||||||
|
allActive := true
|
||||||
|
orderId := utility.IntToString(reduceOrderStrategy.OrderId)
|
||||||
|
|
||||||
|
for _, item3 := range reduceOrderStrategy.Items {
|
||||||
|
if !item3.Actived {
|
||||||
|
allActive = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allActive {
|
||||||
|
if err := helper.DefaultRedis.HDelField(reduceReduceListKey, orderId); err != nil {
|
||||||
|
log.Errorf("删除redis reduceReduceListKey失败 %s", err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
str, _ := sonic.MarshalString(reduceOrderStrategy)
|
||||||
|
if err := helper.DefaultRedis.HSetField(reduceReduceListKey, orderId, str); err != nil {
|
||||||
|
log.Errorf("更新redis reduceReduceListKey失败 %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, item := range reduceVal {
|
for _, item := range reduceVal {
|
||||||
reduceOrder := ReduceListItem{}
|
|
||||||
if err := sonic.Unmarshal([]byte(item), &reduceOrder); err != nil {
|
if err := sonic.Unmarshal([]byte(item), &reduceOrder); err != nil {
|
||||||
log.Error("反序列化失败")
|
log.Error("反序列化失败")
|
||||||
continue
|
continue
|
||||||
@ -173,59 +251,67 @@ func JudgeFuturesReduce(trade models.TradeSet) {
|
|||||||
|
|
||||||
if reduceOrder.Symbol == trade.Coin+trade.Currency {
|
if reduceOrder.Symbol == trade.Coin+trade.Currency {
|
||||||
orderPrice := reduceOrder.Price
|
orderPrice := reduceOrder.Price
|
||||||
tradePrice, _ := decimal.NewFromString(trade.LastPrice)
|
|
||||||
//买入
|
//买入
|
||||||
if orderPrice.Cmp(decimal.Zero) > 0 &&
|
if orderPrice.Cmp(decimal.Zero) > 0 &&
|
||||||
tradePrice.Cmp(decimal.Zero) > 0 &&
|
tradePrice.Cmp(decimal.Zero) > 0 &&
|
||||||
((strings.ToUpper(reduceOrder.Side) == "SELL" && orderPrice.Cmp(tradePrice) >= 0) ||
|
((strings.ToUpper(reduceOrder.Side) == "SELL" && orderPrice.Cmp(tradePrice) >= 0) ||
|
||||||
(strings.ToUpper(reduceOrder.Side) == "BUY" && orderPrice.Cmp(tradePrice) <= 0)) {
|
(strings.ToUpper(reduceOrder.Side) == "BUY" && orderPrice.Cmp(tradePrice) <= 0)) {
|
||||||
|
|
||||||
FuturesReduceTrigger(db, reduceOrder, futApi, setting, key, item)
|
FuturesReduceTrigger(db, reduceOrder, futApi, setting, key, item, false, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 触发合约减仓
|
// 触发合约减仓
|
||||||
func FuturesReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, futApi FutRestApi, setting DbModels.LineSystemSetting, key, item string) {
|
// isStrategy 是否是策略减仓
|
||||||
tradeSet, _ := GetTradeSet(reduceOrder.Symbol, 1)
|
// reduceId 父减仓单id
|
||||||
|
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, 2)
|
||||||
|
result := true
|
||||||
|
|
||||||
if tradeSet.LastPrice == "" {
|
if tradeSet.LastPrice == "" {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.FutTrigger, reduceOrder.ApiId, reduceOrder.Symbol), 20, 5, 100*time.Millisecond)
|
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.FutTrigger, reduceOrder.ApiId, reduceOrder.Symbol), 20, 5, 100*time.Millisecond)
|
||||||
|
|
||||||
if ok, err := lock.AcquireWait(context.Background()); err != nil {
|
if ok, err := lock.AcquireWait(context.Background()); err != nil {
|
||||||
log.Error("获取锁失败", err)
|
log.Error("获取锁失败", err)
|
||||||
return
|
return false
|
||||||
} 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 =1 AND status IN (1,5)", reduceOrder.MainId).Find(&takeOrders).Error; err != nil {
|
if err := db.Model(&DbModels.LinePreOrder{}).Where("main_id =? AND order_type IN (1,2,4) AND status IN (1,5)", reduceOrder.MainId).Find(&takeOrders).Error; err != nil {
|
||||||
log.Error("查询止盈单失败")
|
log.Error("查询止盈单失败")
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
hasrecord, _ := helper.DefaultRedis.IsElementInList(key, item)
|
var hasrecord bool
|
||||||
|
|
||||||
|
if isStrategy {
|
||||||
|
hasrecord, _ = helper.DefaultRedis.HExists(key, utility.IntToString(reduceId), item)
|
||||||
|
} else {
|
||||||
|
hasrecord, _ = helper.DefaultRedis.IsElementInList(key, item)
|
||||||
|
}
|
||||||
|
|
||||||
if !hasrecord {
|
if !hasrecord {
|
||||||
log.Debug("减仓缓存中不存在", item)
|
log.Debug("减仓缓存中不存在", item)
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
apiInfo, _ := GetApiInfo(reduceOrder.ApiId)
|
apiInfo, _ := GetApiInfo(reduceOrder.ApiId)
|
||||||
|
|
||||||
if apiInfo.Id == 0 {
|
if apiInfo.Id == 0 {
|
||||||
log.Error("现货减仓 查询api用户不存在")
|
log.Error("现货减仓 查询api用户不存在")
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
for _, takeOrder := range takeOrders {
|
for _, takeOrder := range takeOrders {
|
||||||
err := CancelFutOrderByOrderSnLoop(apiInfo, takeOrder.Symbol, takeOrder.OrderSn)
|
err := CancelFutOrderByOrderSnLoop(apiInfo, takeOrder.Symbol, takeOrder.OrderSn)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("合约止盈撤单失败", err)
|
log.Error("撤单失败", err)
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,18 +325,8 @@ func FuturesReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, futApi FutRes
|
|||||||
positionSide = "LONG"
|
positionSide = "LONG"
|
||||||
}
|
}
|
||||||
|
|
||||||
// params := FutOrderPlace{
|
|
||||||
// ApiId: reduceOrder.ApiId,
|
|
||||||
// Side: reduceOrder.Side,
|
|
||||||
// OrderType: "STOP",
|
|
||||||
// Symbol: reduceOrder.Symbol,
|
|
||||||
// Price: price,
|
|
||||||
// StopPrice: price,
|
|
||||||
// Quantity: num,
|
|
||||||
// NewClientOrderId: reduceOrder.OrderSn,
|
|
||||||
// }
|
|
||||||
|
|
||||||
if err := futApi.ClosePositionLoop(reduceOrder.Symbol, reduceOrder.OrderSn, num, reduceOrder.Side, positionSide, apiInfo, "LIMIT", "0", price, 3); err != nil {
|
if err := futApi.ClosePositionLoop(reduceOrder.Symbol, reduceOrder.OrderSn, num, reduceOrder.Side, positionSide, apiInfo, "LIMIT", "0", price, 3); err != nil {
|
||||||
|
result = false
|
||||||
log.Errorf("合约减仓挂单失败 id:%s err:%v", reduceOrder.Id, err)
|
log.Errorf("合约减仓挂单失败 id:%s err:%v", reduceOrder.Id, err)
|
||||||
|
|
||||||
if err2 := db.Model(&DbModels.LinePreOrder{}).
|
if err2 := db.Model(&DbModels.LinePreOrder{}).
|
||||||
@ -267,21 +343,31 @@ func FuturesReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, futApi FutRes
|
|||||||
Where("id = ? AND status =0", reduceOrder.Id).Updates(map[string]interface{}{"status": 1}).Error; err != nil {
|
Where("id = ? AND status =0", reduceOrder.Id).Updates(map[string]interface{}{"status": 1}).Error; err != nil {
|
||||||
log.Errorf("合约减仓更新状态失败 id:%s err:%v", reduceOrder.Id, err)
|
log.Errorf("合约减仓更新状态失败 id:%s err:%v", reduceOrder.Id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//处理减仓单减仓策略
|
||||||
|
if err := CacheOrderStrategyAndReCreate(db, reduceOrder, 2, tradeSet, setting); err != nil {
|
||||||
|
log.Errorf("合约减仓策略处理失败 id:%s err:%v", reduceOrder.Id, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := helper.DefaultRedis.LRem(key, item); err != nil {
|
if !isStrategy {
|
||||||
log.Errorf("合约减仓 删除缓存失败 id:%v err:%v", reduceOrder.Id, err)
|
if _, err := helper.DefaultRedis.LRem(key, item); err != nil {
|
||||||
|
log.Errorf("合约减仓 删除缓存失败 id:%v err:%v", reduceOrder.Id, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Error("获取锁失败")
|
log.Error("获取锁失败")
|
||||||
|
result = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断合约加仓
|
// 判断合约加仓
|
||||||
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
|
||||||
@ -289,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
|
||||||
@ -313,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 {
|
||||||
@ -322,8 +408,8 @@ func FutAddPositionTrigger(db *gorm.DB, v *AddPositionList, item string, futApi
|
|||||||
} else if ok {
|
} else if ok {
|
||||||
defer lock.Release()
|
defer lock.Release()
|
||||||
|
|
||||||
setting, _ := GetSystemSetting(db)
|
setting, _ := cacheservice.GetSystemSetting(db)
|
||||||
tradeSet, _ := GetTradeSet(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)
|
||||||
|
|||||||
@ -13,6 +13,8 @@ import (
|
|||||||
models2 "go-admin/models"
|
models2 "go-admin/models"
|
||||||
"go-admin/models/positiondto"
|
"go-admin/models/positiondto"
|
||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
|
"go-admin/services/cacheservice"
|
||||||
|
"go-admin/services/commonservice"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -41,7 +43,7 @@ func ChangeFutureOrder(mapData map[string]interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取数据库连接
|
// 获取数据库连接
|
||||||
db := GetDBConnection()
|
db := commonservice.GetDBConnection()
|
||||||
if db == nil {
|
if db == nil {
|
||||||
logger.Error("合约订单回调失败,无法获取数据库连接")
|
logger.Error("合约订单回调失败,无法获取数据库连接")
|
||||||
return
|
return
|
||||||
@ -126,15 +128,22 @@ func handleReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tradeSet, err := GetTradeSet(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)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//修改减仓单减仓策略状态
|
||||||
|
ReduceCallBack(db, preOrder)
|
||||||
orderExt := models.LinePreOrderExt{}
|
orderExt := models.LinePreOrderExt{}
|
||||||
db.Model(&orderExt).Where("order_id =?", preOrder.Id).First(&orderExt)
|
//减仓策略单获取主减仓单的拓展信息
|
||||||
|
if preOrder.ReduceOrderId > 0 {
|
||||||
|
db.Model(&orderExt).Where("order_id =?", preOrder.ReduceOrderId).First(&orderExt)
|
||||||
|
} else {
|
||||||
|
db.Model(&orderExt).Where("order_id =?", preOrder.Id).First(&orderExt)
|
||||||
|
}
|
||||||
// rate := utility.StringAsFloat(orderExt.AddPositionVal)
|
// rate := utility.StringAsFloat(orderExt.AddPositionVal)
|
||||||
|
|
||||||
// 100%减仓 终止流程
|
// 100%减仓 终止流程
|
||||||
@ -160,9 +169,9 @@ func handleReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
|
|||||||
positionData := savePosition(db, preOrder)
|
positionData := savePosition(db, preOrder)
|
||||||
|
|
||||||
//市价单就跳出 市价减仓不设止盈止损
|
//市价单就跳出 市价减仓不设止盈止损
|
||||||
if preOrder.MainOrderType == "MARKET" {
|
// if preOrder.MainOrderType == "MARKET" {
|
||||||
return
|
// return
|
||||||
}
|
// }
|
||||||
|
|
||||||
//亏损大于0 重新计算比例
|
//亏损大于0 重新计算比例
|
||||||
FutTakeProfit(db, preOrder, apiUserInfo, tradeSet, positionData, orderExt, decimal.Zero, decimal.Zero)
|
FutTakeProfit(db, preOrder, apiUserInfo, tradeSet, positionData, orderExt, decimal.Zero, decimal.Zero)
|
||||||
@ -245,7 +254,7 @@ func nextFuturesReduceTrigger(db *gorm.DB, mainId int, totalNum decimal.Decimal,
|
|||||||
nextOrder := DbModels.LinePreOrder{}
|
nextOrder := DbModels.LinePreOrder{}
|
||||||
nextExt := DbModels.LinePreOrderExt{}
|
nextExt := DbModels.LinePreOrderExt{}
|
||||||
|
|
||||||
if err := db.Model(&models.LinePreOrder{}).Where("main_id =? AND order_type =4 AND status=0", mainId).Order("rate asc").First(&nextOrder).Error; err != nil {
|
if err := db.Model(&models.LinePreOrder{}).Where("main_id =? AND order_type =4 AND status=0 AND reduce_order_id=0", mainId).Order("rate asc").First(&nextOrder).Error; err != nil {
|
||||||
logger.Errorf("获取下一个单失败 err:%v", err)
|
logger.Errorf("获取下一个单失败 err:%v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -258,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)
|
||||||
@ -430,8 +439,10 @@ 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)
|
||||||
|
|
||||||
//止损缓存
|
//止损缓存
|
||||||
for _, v := range stoplossVal {
|
for _, v := range stoplossVal {
|
||||||
@ -473,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 := GetTradeSet(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)
|
||||||
@ -512,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,
|
||||||
@ -595,7 +606,7 @@ func handleFutMainOrderFilled(db *gorm.DB, preOrder *models.LinePreOrder, extOrd
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processFutStopLossOrder(db, order, price, num)
|
processFutStopLossOrder(db, order, utility.StrToDecimal(order.Price), num)
|
||||||
// case 4: // 减仓
|
// case 4: // 减仓
|
||||||
// processFutReduceOrder(order, price, num)
|
// processFutReduceOrder(order, price, num)
|
||||||
}
|
}
|
||||||
@ -697,9 +708,13 @@ func updateOrderQuantity(db *gorm.DB, order models.LinePreOrder, preOrder *model
|
|||||||
// order.Num = num.String()
|
// order.Num = num.String()
|
||||||
// } else
|
// } else
|
||||||
|
|
||||||
if first && (order.OrderCategory == 1 || order.OrderCategory == 3) && order.OrderType == 1 && ext.TakeProfitNumRatio.Cmp(decimal.Zero) > 0 && ext.TakeProfitNumRatio.Cmp(decimal.NewFromInt(100)) != 0 {
|
//止盈止损重算数量
|
||||||
|
if first && (order.OrderCategory == 1 || order.OrderCategory == 3) && ext.TakeProfitNumRatio.Cmp(decimal.Zero) > 0 && ext.TakeProfitNumRatio.Cmp(decimal.NewFromInt(100)) != 0 {
|
||||||
// 计算止盈数量
|
// 计算止盈数量
|
||||||
num = num.Mul(ext.TakeProfitNumRatio.Div(decimal.NewFromInt(100))).Truncate(int32(tradeSet.AmountDigit))
|
if order.OrderType == 1 {
|
||||||
|
num = num.Mul(ext.TakeProfitNumRatio.Div(decimal.NewFromInt(100))).Truncate(int32(tradeSet.AmountDigit))
|
||||||
|
}
|
||||||
|
|
||||||
order.Num = num.String()
|
order.Num = num.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -714,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,
|
||||||
@ -742,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, _ := GetTradeSet(order.Symbol, 1)
|
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 2)
|
||||||
|
|
||||||
params := FutOrderPlace{
|
params := FutOrderPlace{
|
||||||
ApiId: order.ApiId,
|
ApiId: order.ApiId,
|
||||||
@ -779,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, _ := GetTradeSet(order.Symbol, 1)
|
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 2)
|
||||||
|
|
||||||
params := FutOrderPlace{
|
params := FutOrderPlace{
|
||||||
ApiId: order.ApiId,
|
ApiId: order.ApiId,
|
||||||
|
|||||||
@ -186,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"`
|
||||||
@ -227,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"`
|
|
||||||
}
|
|
||||||
|
|||||||
179
services/binanceservice/reduce_order_strategy_service.go
Normal file
179
services/binanceservice/reduce_order_strategy_service.go
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
package binanceservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/app/admin/service/dto"
|
||||||
|
"go-admin/common/const/rediskey"
|
||||||
|
"go-admin/common/global"
|
||||||
|
"go-admin/common/helper"
|
||||||
|
models2 "go-admin/models"
|
||||||
|
"go-admin/models/positiondto"
|
||||||
|
"go-admin/pkg/utility"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 缓存减仓节点和重新生成
|
||||||
|
// reduceOrder:原始减仓单
|
||||||
|
// reduceOrderStrategy:减仓策略
|
||||||
|
func CacheOrderStrategyAndReCreate(db *gorm.DB, reduceOrder positiondto.ReduceListItem, symbolType int, tradeSet models2.TradeSet, setting models.LineSystemSetting) error {
|
||||||
|
reduceOrderStrategy := models.LineOrderReduceStrategy{}
|
||||||
|
var key string
|
||||||
|
|
||||||
|
if symbolType == 1 {
|
||||||
|
key = fmt.Sprintf(rediskey.SpotOrderReduceStrategyList, global.EXCHANGE_BINANCE)
|
||||||
|
} else {
|
||||||
|
key = fmt.Sprintf(rediskey.FutOrderReduceStrategyList, global.EXCHANGE_BINANCE)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := db.Model(&reduceOrderStrategy).Where("order_id =?", reduceOrder.Id).Find(&reduceOrderStrategy).Error; err != nil {
|
||||||
|
logger.Errorf("获取减仓策略失败,err:%v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if reduceOrderStrategy.Id > 0 {
|
||||||
|
items := make([]models.LineReduceStrategyItem, 0)
|
||||||
|
strategyCache := dto.LineOrderReduceStrategyResp{
|
||||||
|
OrderId: reduceOrder.Id,
|
||||||
|
Symbol: reduceOrder.Symbol,
|
||||||
|
MainId: reduceOrder.MainId,
|
||||||
|
Side: reduceOrder.Side,
|
||||||
|
}
|
||||||
|
|
||||||
|
sonic.Unmarshal([]byte(reduceOrderStrategy.ItemContent), &items)
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
var rate decimal.Decimal
|
||||||
|
var triggerRate decimal.Decimal
|
||||||
|
|
||||||
|
if reduceOrder.Side == "SELL" {
|
||||||
|
rate = (decimal.NewFromInt(100).Sub(item.LossPercent)).Div(decimal.NewFromInt(100))
|
||||||
|
|
||||||
|
if setting.ReduceEarlyTriggerPercent.Cmp(decimal.Zero) > 0 {
|
||||||
|
triggerRate = rate.Add(setting.ReduceEarlyTriggerPercent.Div(decimal.NewFromInt(100)))
|
||||||
|
} else {
|
||||||
|
triggerRate = rate
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rate = (decimal.NewFromInt(100).Add(item.LossPercent)).Div(decimal.NewFromInt(100))
|
||||||
|
|
||||||
|
if setting.ReduceEarlyTriggerPercent.Cmp(decimal.Zero) > 0 {
|
||||||
|
triggerRate = rate.Sub(setting.ReduceEarlyTriggerPercent.Div(decimal.NewFromInt(100)))
|
||||||
|
} else {
|
||||||
|
triggerRate = rate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
price := reduceOrder.Price.Mul(rate).Truncate(int32(tradeSet.PriceDigit))
|
||||||
|
triggerPrice := reduceOrder.Price.Mul(triggerRate).Truncate(int32(tradeSet.PriceDigit))
|
||||||
|
num := reduceOrder.Num
|
||||||
|
|
||||||
|
//百分比大于0就重新计算 否则就是主减仓单数量
|
||||||
|
if item.QuantityPercent.Cmp(decimal.Zero) > 0 {
|
||||||
|
num = reduceOrder.Num.Mul(item.QuantityPercent.Div(decimal.NewFromInt(100))).Truncate(int32(tradeSet.AmountDigit))
|
||||||
|
}
|
||||||
|
|
||||||
|
strategyCache.Items = append(strategyCache.Items, dto.LineOrderReduceStrategyRespItem{
|
||||||
|
LossPercent: item.LossPercent,
|
||||||
|
OrderType: item.OrderType,
|
||||||
|
Price: price,
|
||||||
|
TriggerPrice: triggerPrice,
|
||||||
|
Num: num,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
str, _ := sonic.MarshalString(strategyCache)
|
||||||
|
|
||||||
|
if str != "" {
|
||||||
|
if err := helper.DefaultRedis.HSetField(key, utility.IntToString(reduceOrder.Id), str); err != nil {
|
||||||
|
logger.Errorf("减仓单缓存减仓策略,err:%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据订单id获取订单减仓策略
|
||||||
|
func GetReduceStrategyById(db *gorm.DB, orderId int) (models.LineOrderReduceStrategy, error) {
|
||||||
|
result := models.LineOrderReduceStrategy{}
|
||||||
|
|
||||||
|
if err := db.Model(result).Where("order_id =?", orderId).Select("id", "order_id").First(&result).Error; err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 减仓单成功回调
|
||||||
|
func ReduceCallBack(db *gorm.DB, preOrder *models.LinePreOrder) error {
|
||||||
|
reduceOrderId := preOrder.Id
|
||||||
|
var key string
|
||||||
|
if preOrder.ReduceOrderId > 0 {
|
||||||
|
reduceOrderId = preOrder.ReduceOrderId
|
||||||
|
}
|
||||||
|
|
||||||
|
switch preOrder.SymbolType {
|
||||||
|
case 1:
|
||||||
|
key = fmt.Sprintf(rediskey.SpotOrderReduceStrategyList, global.EXCHANGE_BINANCE)
|
||||||
|
case 2:
|
||||||
|
key = fmt.Sprintf(rediskey.FutOrderReduceStrategyList, global.EXCHANGE_BINANCE)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("交易对类型错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
arrays, _ := helper.DefaultRedis.HGetAllFields(key)
|
||||||
|
cache := dto.LineOrderReduceStrategyResp{}
|
||||||
|
|
||||||
|
for _, v := range arrays {
|
||||||
|
sonic.Unmarshal([]byte(v), &cache)
|
||||||
|
|
||||||
|
if cache.OrderId == reduceOrderId {
|
||||||
|
if err := helper.DefaultRedis.HDelField(key, utility.IntToString(cache.OrderId)); err != nil {
|
||||||
|
logger.Errorf("移除减仓单减仓策略失败redis err:%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
orderReduceStrategy, _ := GetReduceStrategyById(db, reduceOrderId)
|
||||||
|
if orderReduceStrategy.Id > 0 {
|
||||||
|
if err := db.Model(&orderReduceStrategy).Update("actived", 1).Error; err != nil {
|
||||||
|
logger.Errorf("更新减仓单减仓策略状态失败 order_id:%d err:%v", orderReduceStrategy.OrderId, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除减仓后减仓策略
|
||||||
|
// mainId 主单id
|
||||||
|
// symbolType 交易对类型
|
||||||
|
func RemoveReduceReduceCacheByMainId(mainId int, symbolType int) error {
|
||||||
|
var key string
|
||||||
|
switch symbolType {
|
||||||
|
case 1:
|
||||||
|
key = fmt.Sprintf(rediskey.SpotOrderReduceStrategyList, global.EXCHANGE_BINANCE)
|
||||||
|
case 2:
|
||||||
|
key = fmt.Sprintf(rediskey.FutOrderReduceStrategyList, global.EXCHANGE_BINANCE)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("交易对类型错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
arrays, _ := helper.DefaultRedis.HGetAllFields(key)
|
||||||
|
cache := dto.LineOrderReduceStrategyResp{}
|
||||||
|
|
||||||
|
for _, v := range arrays {
|
||||||
|
sonic.Unmarshal([]byte(v), &cache)
|
||||||
|
|
||||||
|
if cache.MainId == mainId {
|
||||||
|
if err := helper.DefaultRedis.HDelField(key, utility.IntToString(cache.OrderId)); err != nil {
|
||||||
|
logger.Errorf("移除减仓单减仓策略失败redis err:%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
65
services/binanceservice/spot_judge_service_test.go
Normal file
65
services/binanceservice/spot_judge_service_test.go
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
package binanceservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go-admin/common/const/rediskey"
|
||||||
|
"go-admin/common/global"
|
||||||
|
"go-admin/common/helper"
|
||||||
|
"go-admin/models"
|
||||||
|
"go-admin/models/positiondto"
|
||||||
|
"go-admin/services/cacheservice"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSpotJudge(t *testing.T) {
|
||||||
|
dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||||
|
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||||
|
helper.InitDefaultRedis("127.0.0.1:6379", "", 2)
|
||||||
|
helper.InitLockRedisConn("127.0.0.1:6379", "", "2")
|
||||||
|
// tradeSet := models.TradeSet{
|
||||||
|
// Coin: "ADA",
|
||||||
|
// Currency: "USDT",
|
||||||
|
// LastPrice: "0.516",
|
||||||
|
// }
|
||||||
|
|
||||||
|
key := fmt.Sprintf(rediskey.SpotReduceList, global.EXCHANGE_BINANCE)
|
||||||
|
item := `{"id":66,"apiId":49,"mainId":63,"pid":63,"symbol":"ADAUSDT","price":"0.5912","side":"SELL","num":"12.6","orderSn":"397919643961917440"}`
|
||||||
|
reduceOrder := positiondto.ReduceListItem{}
|
||||||
|
spotApi := SpotRestApi{}
|
||||||
|
setting, err := cacheservice.GetSystemSetting(db)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("获取系统设置失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := sonic.Unmarshal([]byte(item), &reduceOrder); err != nil {
|
||||||
|
logger.Error("反序列化失败")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// JudgeFuturesReduce(tradeSet)
|
||||||
|
SpotReduceTrigger(db, reduceOrder, spotApi, setting, key, item, false, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 测试减仓后减仓触发
|
||||||
|
func TestSpotReduceReduce(t *testing.T) {
|
||||||
|
dsn := "root:123456@tcp(127.0.0.1:3306)/go_exchange_single?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||||
|
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||||
|
sdk.Runtime.SetDb("default", db)
|
||||||
|
helper.InitDefaultRedis("127.0.0.1:6379", "", 2)
|
||||||
|
helper.InitLockRedisConn("127.0.0.1:6379", "", "2")
|
||||||
|
tradeSet := models.TradeSet{
|
||||||
|
Coin: "ADA",
|
||||||
|
Currency: "USDT",
|
||||||
|
LastPrice: "0.5793",
|
||||||
|
PriceDigit: 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
// JudgeFuturesReduce(tradeSet)
|
||||||
|
JudgeSpotReduce(tradeSet)
|
||||||
|
}
|
||||||
@ -10,7 +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/commonservice"
|
||||||
|
"go-admin/services/orderservice"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -26,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 {
|
||||||
@ -45,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,16 +158,16 @@ func JudgeSpotStopLoss(trade models.TradeSet) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
db := GetDBConnection()
|
db := commonservice.GetDBConnection()
|
||||||
spotApi := SpotRestApi{}
|
spotApi := SpotRestApi{}
|
||||||
setting, err := GetSystemSetting(db)
|
setting, err := cacheservice.GetSystemSetting(db)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("获取系统设置失败")
|
log.Error("获取系统设置失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tradeSet, err := GetTradeSet(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("获取交易设置失败")
|
||||||
@ -256,23 +275,96 @@ 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)
|
||||||
reduceVal, _ := helper.DefaultRedis.GetAllList(key)
|
db := commonservice.GetDBConnection()
|
||||||
|
|
||||||
if len(reduceVal) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
db := GetDBConnection()
|
|
||||||
spotApi := SpotRestApi{}
|
spotApi := SpotRestApi{}
|
||||||
setting, err := GetSystemSetting(db)
|
setting, err := cacheservice.GetSystemSetting(db)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("获取系统设置失败")
|
log.Error("获取系统设置失败")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reduceOrder := positiondto.ReduceListItem{}
|
||||||
|
tradePrice, _ := decimal.NewFromString(trade.LastPrice)
|
||||||
|
//减仓单减仓策略
|
||||||
|
reduceReduceListKey := fmt.Sprintf(rediskey.SpotOrderReduceStrategyList, global.EXCHANGE_BINANCE)
|
||||||
|
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 {
|
||||||
|
log.Error("获取锁失败", err)
|
||||||
|
return
|
||||||
|
} else if ok {
|
||||||
|
defer lock.Release()
|
||||||
|
hasrecord, _ := helper.DefaultRedis.HExists(reduceReduceListKey, utility.IntTostring(reduceOrderStrategy.OrderId), item)
|
||||||
|
|
||||||
|
if !hasrecord {
|
||||||
|
log.Debug("减仓缓存中不存在", item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
order, err := orderService.CreateReduceReduceOrder(db, reduceOrderStrategy.OrderId, item2.Price, item2.Num, trade.PriceDigit)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("%d 生成订单失败", reduceOrderStrategy.OrderId)
|
||||||
|
}
|
||||||
|
|
||||||
|
reduceOrder.ApiId = order.ApiId
|
||||||
|
reduceOrder.Id = order.Id
|
||||||
|
reduceOrder.Pid = order.Pid
|
||||||
|
reduceOrder.MainId = order.MainId
|
||||||
|
reduceOrder.Symbol = order.Symbol
|
||||||
|
reduceOrder.Side = reduceOrderStrategy.Side
|
||||||
|
reduceOrder.OrderSn = order.OrderSn
|
||||||
|
reduceOrder.Price = item2.Price
|
||||||
|
reduceOrder.Num = item2.Num
|
||||||
|
if SpotReduceTrigger(db, reduceOrder, spotApi, setting, reduceReduceListKey, item, true, reduceOrderStrategy.OrderId) {
|
||||||
|
reduceOrderStrategy.Items[index].Actived = true
|
||||||
|
allActive := true
|
||||||
|
orderId := utility.IntToString(reduceOrderStrategy.OrderId)
|
||||||
|
|
||||||
|
for _, item3 := range reduceOrderStrategy.Items {
|
||||||
|
if !item3.Actived {
|
||||||
|
allActive = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if allActive {
|
||||||
|
if err := helper.DefaultRedis.HDelField(reduceReduceListKey, orderId); err != nil {
|
||||||
|
log.Errorf("删除redis reduceReduceListKey失败 %s", err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
str, _ := sonic.MarshalString(reduceOrderStrategy)
|
||||||
|
if err := helper.DefaultRedis.HSetField(reduceReduceListKey, orderId, str); err != nil {
|
||||||
|
log.Errorf("更新redis reduceReduceListKey失败 %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//减仓单
|
||||||
|
reduceVal, _ := helper.DefaultRedis.GetAllList(key)
|
||||||
|
|
||||||
for _, item := range reduceVal {
|
for _, item := range reduceVal {
|
||||||
reduceOrder := ReduceListItem{}
|
|
||||||
if err := sonic.Unmarshal([]byte(item), &reduceOrder); err != nil {
|
if err := sonic.Unmarshal([]byte(item), &reduceOrder); err != nil {
|
||||||
log.Error("反序列化失败")
|
log.Error("反序列化失败")
|
||||||
continue
|
continue
|
||||||
@ -280,59 +372,67 @@ func JudgeSpotReduce(trade models.TradeSet) {
|
|||||||
|
|
||||||
if reduceOrder.Symbol == trade.Coin+trade.Currency {
|
if reduceOrder.Symbol == trade.Coin+trade.Currency {
|
||||||
orderPrice := reduceOrder.Price
|
orderPrice := reduceOrder.Price
|
||||||
tradePrice, _ := decimal.NewFromString(trade.LastPrice)
|
|
||||||
//买入
|
//买入
|
||||||
if strings.ToUpper(reduceOrder.Side) == "SELL" &&
|
if strings.ToUpper(reduceOrder.Side) == "SELL" &&
|
||||||
orderPrice.Cmp(tradePrice) >= 0 &&
|
orderPrice.Cmp(tradePrice) >= 0 &&
|
||||||
orderPrice.Cmp(decimal.Zero) > 0 &&
|
orderPrice.Cmp(decimal.Zero) > 0 &&
|
||||||
tradePrice.Cmp(decimal.Zero) > 0 {
|
tradePrice.Cmp(decimal.Zero) > 0 {
|
||||||
|
|
||||||
SpotReduceTrigger(db, reduceOrder, spotApi, setting, key, item)
|
SpotReduceTrigger(db, reduceOrder, spotApi, setting, key, item, false, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 触发现货减仓
|
// 触发现货减仓
|
||||||
func SpotReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, spotApi SpotRestApi, setting DbModels.LineSystemSetting, key, item string) {
|
// isStrategy 是否是策略减仓单
|
||||||
tradeSet, err := GetTradeSet(reduceOrder.Symbol, 0)
|
// reduceId 策略主减仓单id
|
||||||
|
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, 1)
|
||||||
|
result := true
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("获取交易设置失败")
|
log.Error("获取交易设置失败")
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, reduceOrder.ApiId, reduceOrder.Symbol), 20, 5, 100*time.Millisecond)
|
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 {
|
if ok, err := lock.AcquireWait(context.Background()); err != nil {
|
||||||
log.Error("获取锁失败", err)
|
log.Error("获取锁失败", err)
|
||||||
return
|
return false
|
||||||
} 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 =1 AND status IN (1,5)", reduceOrder.MainId).Find(&takeOrders).Error; err != nil {
|
if err := db.Model(&DbModels.LinePreOrder{}).Where("main_id =? AND order_type IN (1,2,4) AND status IN (1,5)", reduceOrder.MainId).Find(&takeOrders).Error; err != nil {
|
||||||
log.Error("查询止盈单失败")
|
log.Error("查询止盈单失败")
|
||||||
return
|
return false
|
||||||
|
}
|
||||||
|
var hasrecord bool
|
||||||
|
|
||||||
|
if isStrategy {
|
||||||
|
hasrecord, _ = helper.DefaultRedis.HExists(key, utility.IntToString(reduceId), item)
|
||||||
|
} else {
|
||||||
|
hasrecord, _ = helper.DefaultRedis.IsElementInList(key, item)
|
||||||
}
|
}
|
||||||
hasrecord, _ := helper.DefaultRedis.IsElementInList(key, item)
|
|
||||||
|
|
||||||
if !hasrecord {
|
if !hasrecord {
|
||||||
log.Debug("减仓缓存中不存在", item)
|
log.Debug("减仓缓存中不存在", item)
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
apiInfo, _ := GetApiInfo(reduceOrder.ApiId)
|
apiInfo, _ := GetApiInfo(reduceOrder.ApiId)
|
||||||
|
|
||||||
if apiInfo.Id == 0 {
|
if apiInfo.Id == 0 {
|
||||||
log.Error("现货减仓 查询api用户不存在")
|
log.Error("现货减仓 查询api用户不存在")
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, takeOrder := range takeOrders {
|
for _, takeOrder := range takeOrders {
|
||||||
err := CancelOpenOrderByOrderSnLoop(apiInfo, takeOrder.Symbol, takeOrder.OrderSn)
|
err := CancelOpenOrderByOrderSnLoop(apiInfo, takeOrder.Symbol, takeOrder.OrderSn)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("现货止盈撤单失败", err)
|
log.Error("现货撤单失败", err)
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
price := reduceOrder.Price.Mul(decimal.NewFromInt(1).Sub(setting.ReducePremium.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit))
|
price := reduceOrder.Price.Mul(decimal.NewFromInt(1).Sub(setting.ReducePremium.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit))
|
||||||
@ -349,6 +449,7 @@ func SpotReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, spotApi SpotRest
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := spotApi.OrderPlaceLoop(db, params, 3); err != nil {
|
if err := spotApi.OrderPlaceLoop(db, params, 3); err != nil {
|
||||||
|
result = false
|
||||||
log.Errorf("现货减仓挂单失败 id:%s err:%v", reduceOrder.Id, err)
|
log.Errorf("现货减仓挂单失败 id:%s err:%v", reduceOrder.Id, err)
|
||||||
|
|
||||||
if err2 := db.Model(&DbModels.LinePreOrder{}).
|
if err2 := db.Model(&DbModels.LinePreOrder{}).
|
||||||
@ -366,21 +467,30 @@ func SpotReduceTrigger(db *gorm.DB, reduceOrder ReduceListItem, spotApi SpotRest
|
|||||||
Updates(map[string]interface{}{"status": 1}).Error; err != nil {
|
Updates(map[string]interface{}{"status": 1}).Error; err != nil {
|
||||||
log.Errorf("修改现货减仓状态失败 id:%s err:%v", reduceOrder.Id, err)
|
log.Errorf("修改现货减仓状态失败 id:%s err:%v", reduceOrder.Id, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//处理减仓单减仓策略
|
||||||
|
if err := CacheOrderStrategyAndReCreate(db, reduceOrder, 1, tradeSet, setting); err != nil {
|
||||||
|
log.Errorf("现货减仓 处理减仓策略失败 id:%v err:%v", reduceOrder.Id, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := helper.DefaultRedis.LRem(key, item); err != nil {
|
if _, err := helper.DefaultRedis.LRem(key, item); err != nil {
|
||||||
|
result = false
|
||||||
log.Errorf("现货减仓 删除缓存失败 id:%v err:%v", reduceOrder.Id, err)
|
log.Errorf("现货减仓 删除缓存失败 id:%v err:%v", reduceOrder.Id, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Error("获取锁失败")
|
log.Error("获取锁失败")
|
||||||
|
result = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断现货加仓
|
// 判断现货加仓
|
||||||
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
|
||||||
@ -388,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
|
||||||
@ -405,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 {
|
||||||
@ -414,8 +524,8 @@ func SpotAddPositionTrigger(db *gorm.DB, v *AddPositionList, item string, spotAp
|
|||||||
} else if ok {
|
} else if ok {
|
||||||
defer lock.Release()
|
defer lock.Release()
|
||||||
|
|
||||||
setting, _ := GetSystemSetting(db)
|
setting, _ := cacheservice.GetSystemSetting(db)
|
||||||
tradeSet, _ := GetTradeSet(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)
|
||||||
|
|||||||
@ -13,6 +13,8 @@ import (
|
|||||||
models2 "go-admin/models"
|
models2 "go-admin/models"
|
||||||
"go-admin/models/positiondto"
|
"go-admin/models/positiondto"
|
||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
|
"go-admin/services/cacheservice"
|
||||||
|
"go-admin/services/commonservice"
|
||||||
"go-admin/services/positionservice"
|
"go-admin/services/positionservice"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@ -43,7 +45,7 @@ func ChangeSpotOrder(mapData map[string]interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 获取数据库连接
|
// 获取数据库连接
|
||||||
db := GetDBConnection()
|
db := commonservice.GetDBConnection()
|
||||||
if db == nil {
|
if db == nil {
|
||||||
logger.Error("订单回调失败, 无法获取数据库连接")
|
logger.Error("订单回调失败, 无法获取数据库连接")
|
||||||
return
|
return
|
||||||
@ -165,20 +167,29 @@ func handleMainReduceFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tradeSet, err := GetTradeSet(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)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//修改减仓单减仓策略状态
|
||||||
|
ReduceCallBack(db, preOrder)
|
||||||
|
|
||||||
orderExt := models.LinePreOrderExt{}
|
orderExt := models.LinePreOrderExt{}
|
||||||
positionData := savePosition(db, preOrder)
|
positionData := savePosition(db, preOrder)
|
||||||
|
|
||||||
if err := db.Model(&DbModels.LinePreOrderStatus{}).Where("order_id =? ", preOrder.MainId).Update("reduce_status", 1).Error; err != nil {
|
if err := db.Model(&DbModels.LinePreOrderStatus{}).Where("order_id =? ", preOrder.MainId).Update("reduce_status", 1).Error; err != nil {
|
||||||
logger.Errorf("handleMainReduceFilled 更新主单减仓状态失败,订单号:%s", preOrder.OrderSn)
|
logger.Errorf("handleMainReduceFilled 更新主单减仓状态失败,订单号:%s", preOrder.OrderSn)
|
||||||
}
|
}
|
||||||
db.Model(&orderExt).Where("order_id =?", preOrder.Id).Find(&orderExt)
|
|
||||||
|
//策略减仓单 获取主减仓单拓展信息
|
||||||
|
if preOrder.ReduceOrderId > 0 {
|
||||||
|
db.Model(&orderExt).Where("order_id =?", preOrder.ReduceOrderId).Find(&orderExt)
|
||||||
|
} else {
|
||||||
|
db.Model(&orderExt).Where("order_id =?", preOrder.Id).Find(&orderExt)
|
||||||
|
}
|
||||||
|
|
||||||
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotReduceCallback, preOrder.ApiId, preOrder.Symbol), 120, 20, 100*time.Millisecond)
|
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotReduceCallback, preOrder.ApiId, preOrder.Symbol), 120, 20, 100*time.Millisecond)
|
||||||
|
|
||||||
@ -278,7 +289,7 @@ func nextSpotReduceTrigger(db *gorm.DB, mainId int, totalNum decimal.Decimal, tr
|
|||||||
nextExt := DbModels.LinePreOrderExt{}
|
nextExt := DbModels.LinePreOrderExt{}
|
||||||
// var percentag decimal.Decimal
|
// var percentag decimal.Decimal
|
||||||
|
|
||||||
if err := db.Model(&models.LinePreOrder{}).Where("main_id =? AND order_type =4 AND status=0", mainId).Order("rate asc").First(&nextOrder).Error; err != nil {
|
if err := db.Model(&models.LinePreOrder{}).Where("main_id =? AND order_type =4 AND status=0 AND reduce_order_id =0", mainId).Order("rate asc").First(&nextOrder).Error; err != nil {
|
||||||
logger.Errorf("获取下一个单失败 err:%v", err)
|
logger.Errorf("获取下一个单失败 err:%v", err)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -290,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)
|
||||||
@ -543,8 +554,11 @@ 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)
|
||||||
|
|
||||||
//止损缓存
|
//止损缓存
|
||||||
for _, v := range stoplossVal {
|
for _, v := range stoplossVal {
|
||||||
@ -633,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,
|
||||||
@ -731,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, _ := GetTradeSet(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 {
|
||||||
@ -842,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,
|
||||||
@ -869,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, _ := GetTradeSet(order.Symbol, 0)
|
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, 1)
|
||||||
|
|
||||||
if tradeSet.Coin == "" {
|
if tradeSet.Coin == "" {
|
||||||
logger.Error("获取交易对失败")
|
logger.Error("获取交易对失败")
|
||||||
@ -941,44 +955,6 @@ func processStopLossOrder(order models.LinePreOrder) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetSystemSetting(db *gorm.DB) (models.LineSystemSetting, error) {
|
|
||||||
key := fmt.Sprintf(rediskey.SystemSetting)
|
|
||||||
val, _ := helper.DefaultRedis.GetString(key)
|
|
||||||
setting := models.LineSystemSetting{}
|
|
||||||
|
|
||||||
if val != "" {
|
|
||||||
sonic.UnmarshalString(val, &setting)
|
|
||||||
}
|
|
||||||
|
|
||||||
if setting.Id > 0 {
|
|
||||||
return setting, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
setting, err = ResetSystemSetting(db)
|
|
||||||
if err != nil {
|
|
||||||
return setting, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return setting, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ResetSystemSetting(db *gorm.DB) (DbModels.LineSystemSetting, error) {
|
|
||||||
setting := DbModels.LineSystemSetting{}
|
|
||||||
if err := db.Model(&setting).First(&setting).Error; err != nil {
|
|
||||||
return setting, err
|
|
||||||
}
|
|
||||||
|
|
||||||
settVal, _ := sonic.MarshalString(&setting)
|
|
||||||
|
|
||||||
if settVal != "" {
|
|
||||||
if err := helper.DefaultRedis.SetString(rediskey.SystemSetting, settVal); err != nil {
|
|
||||||
logger.Error("redis添加系统设置失败", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return DbModels.LineSystemSetting{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 循环取消订单
|
// 循环取消订单
|
||||||
func CancelOpenOrderByOrderSnLoop(apiInfo DbModels.LineApiUser, symbol string, orderSn string) error {
|
func CancelOpenOrderByOrderSnLoop(apiInfo DbModels.LineApiUser, symbol string, orderSn string) error {
|
||||||
spotApi := SpotRestApi{}
|
spotApi := SpotRestApi{}
|
||||||
|
|||||||
232
services/binanceservice/strategy_order_service.go
Normal file
232
services/binanceservice/strategy_order_service.go
Normal file
@ -0,0 +1,232 @@
|
|||||||
|
package binanceservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/app/admin/service/dto"
|
||||||
|
"go-admin/common/const/rediskey"
|
||||||
|
"go-admin/common/global"
|
||||||
|
"go-admin/common/helper"
|
||||||
|
models2 "go-admin/models"
|
||||||
|
"go-admin/pkg/utility"
|
||||||
|
"go-admin/services/cacheservice"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BinanceStrategyOrderService struct {
|
||||||
|
service.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否触发波段订单
|
||||||
|
func (e *BinanceStrategyOrderService) TriggerStrategyOrder(exchangeType string) {
|
||||||
|
//现货
|
||||||
|
orderStrs, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.StrategyFutOrderList, exchangeType))
|
||||||
|
e.DoJudge(orderStrs, 1, exchangeType)
|
||||||
|
|
||||||
|
//合约
|
||||||
|
futOrdedrStrs, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.StrategyFutOrderList, exchangeType))
|
||||||
|
e.DoJudge(futOrdedrStrs, 2, exchangeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理订单
|
||||||
|
func (e *BinanceStrategyOrderService) DoJudge(orderStrs []string, symbolType int, exchangeType string) {
|
||||||
|
for _, orderStr := range orderStrs {
|
||||||
|
var lockKey string
|
||||||
|
orderItem := dto.StrategyOrderRedisList{}
|
||||||
|
err := sonic.Unmarshal([]byte(orderStr), &orderItem)
|
||||||
|
|
||||||
|
if err != nil || orderItem.Symbol == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if symbolType == 1 {
|
||||||
|
lockKey = rediskey.StrategySpotTriggerLock
|
||||||
|
} else {
|
||||||
|
lockKey = rediskey.StrategyFutTriggerLock
|
||||||
|
}
|
||||||
|
|
||||||
|
lock := helper.NewRedisLock(fmt.Sprintf(lockKey, orderItem.ApiId, orderItem.Symbol), 200, 50, 100*time.Millisecond)
|
||||||
|
|
||||||
|
if ok, err := lock.AcquireWait(context.Background()); err != nil {
|
||||||
|
e.Log.Debug("获取锁失败", err)
|
||||||
|
return
|
||||||
|
} else if ok {
|
||||||
|
defer lock.Release()
|
||||||
|
|
||||||
|
//判断是否符合条件
|
||||||
|
success, err := e.JudgeStrategy(orderItem, 1, exchangeType)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
e.Log.Errorf("order_id:%d err:%v", orderItem.Id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if success {
|
||||||
|
e.TriggerOrder(orderItem, symbolType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否符合触发条件
|
||||||
|
func (e *BinanceStrategyOrderService) JudgeStrategy(order dto.StrategyOrderRedisList, symbolType int, exchangeType string) (bool, error) {
|
||||||
|
var symbolKey string
|
||||||
|
result := false
|
||||||
|
nowUtc := time.Now().UnixMilli()
|
||||||
|
|
||||||
|
switch symbolType {
|
||||||
|
case 1:
|
||||||
|
symbolKey = fmt.Sprintf(rediskey.SpotTickerLastPrice, exchangeType, order.Symbol)
|
||||||
|
case 2:
|
||||||
|
symbolKey = fmt.Sprintf(rediskey.FutureTickerLastPrice, exchangeType, order.Symbol)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastUtc := nowUtc - (int64(order.TimeSlotStart) * 60 * 1000)
|
||||||
|
beforePrice, _ := helper.DefaultRedis.GetNextAfterScore(symbolKey, float64(lastUtc))
|
||||||
|
lastPrices, _ := helper.DefaultRedis.GetLastSortSet(symbolKey)
|
||||||
|
|
||||||
|
if beforePrice == "" || len(lastPrices) == 0 {
|
||||||
|
return result, errors.New("获取交易对起止价格失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
score := lastPrices[0].Score
|
||||||
|
startPrice := utility.StrToDecimal(beforePrice)
|
||||||
|
lastPrice := utility.StrToDecimal(lastPrices[0].Member.(string))
|
||||||
|
|
||||||
|
//时间差超过10s
|
||||||
|
if (nowUtc-int64(score))/1000 > 10 {
|
||||||
|
return result, fmt.Errorf("时间差超过 %ss", "10")
|
||||||
|
}
|
||||||
|
|
||||||
|
if startPrice.Cmp(decimal.Zero) == 0 || lastPrice.Cmp(decimal.Zero) == 0 {
|
||||||
|
return result, errors.New("获取交易对起止价格有一个为0")
|
||||||
|
}
|
||||||
|
|
||||||
|
percentag := lastPrice.Div(startPrice).Sub(decimal.NewFromInt(1)).Truncate(6)
|
||||||
|
|
||||||
|
//价格没有变动
|
||||||
|
if percentag.Cmp(decimal.Zero) == 0 {
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//满足条件
|
||||||
|
switch order.CompareType {
|
||||||
|
case 1:
|
||||||
|
result = percentag.Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) > 0
|
||||||
|
case 2:
|
||||||
|
result = percentag.Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) >= 0
|
||||||
|
case 5:
|
||||||
|
result = percentag.Mul(decimal.NewFromInt(100)).Cmp(order.Percentag) == 0
|
||||||
|
default:
|
||||||
|
return result, errors.New("没有对应的类型")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发委托单
|
||||||
|
func (e *BinanceStrategyOrderService) TriggerOrder(order dto.StrategyOrderRedisList, symbolType int) error {
|
||||||
|
orders := make([]models.LinePreOrder, 0)
|
||||||
|
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())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
setting, _ := cacheservice.GetSystemSetting(e.Orm)
|
||||||
|
|
||||||
|
if setting.Id == 0 {
|
||||||
|
return errors.New("获取系统设置失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
tradeSet, _ := cacheservice.GetTradeSet(global.EXCHANGE_BINANCE, order.Symbol, symbolType)
|
||||||
|
|
||||||
|
if tradeSet.Coin == "" {
|
||||||
|
return errors.New("获取交易对行情失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPrice := utility.StrToDecimal(tradeSet.LastPrice)
|
||||||
|
|
||||||
|
if lastPrice.Cmp(decimal.Zero) <= 0 {
|
||||||
|
return errors.New("最新成交价小于等于0")
|
||||||
|
}
|
||||||
|
|
||||||
|
var mainOrder models.LinePreOrder
|
||||||
|
|
||||||
|
for _, v := range orders {
|
||||||
|
if v.MainId == 0 {
|
||||||
|
mainOrder = v
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GetOrderByPid(&mainOrder, orders, mainOrder.Id)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新计算订单单价、数量
|
||||||
|
// tradeSet 交易对行情
|
||||||
|
// mainOrder 主订单
|
||||||
|
// setting 系统设置
|
||||||
|
func (e *BinanceStrategyOrderService) RecalculateOrder(tradeSet models2.TradeSet, mainOrder *models.LinePreOrder, setting models.LineSystemSetting) error {
|
||||||
|
exts := make([]models.LinePreOrderExt, 0)
|
||||||
|
if err := e.Orm.Model(models.LinePreOrderExt{}).Where("main_id =?", mainOrder.Id).Find(&exts).Error; err != nil {
|
||||||
|
return errors.New("获取拓展信息失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPrice := utility.StrToDecimal(tradeSet.LastPrice)
|
||||||
|
rate := utility.StrToDecimal(mainOrder.Rate)
|
||||||
|
newPrice := lastPrice.Mul(decimal.NewFromInt(1).Add(rate.Div(decimal.NewFromInt(100)).Truncate(4))).Truncate(int32(tradeSet.PriceDigit))
|
||||||
|
buyPrice := utility.StrToDecimal(mainOrder.BuyPrice)
|
||||||
|
totalNum := buyPrice.Div(newPrice).Truncate(int32(tradeSet.AmountDigit))
|
||||||
|
|
||||||
|
mainOrder.SignPrice = lastPrice.String()
|
||||||
|
mainOrder.Price = newPrice.String()
|
||||||
|
mainOrder.Num = totalNum.String()
|
||||||
|
|
||||||
|
for index := range mainOrder.Childs {
|
||||||
|
//主单止盈
|
||||||
|
if mainOrder.Child[index].OrderType == 1 {
|
||||||
|
var ext models.LinePreOrderExt
|
||||||
|
|
||||||
|
for _, v := range exts {
|
||||||
|
if v.OrderId == mainOrder.Child[index].Id {
|
||||||
|
ext = v
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ext.Id > 0 {
|
||||||
|
var percent decimal.Decimal
|
||||||
|
//多
|
||||||
|
if mainOrder.Site == "BUY" {
|
||||||
|
percent = decimal.NewFromInt(100).Add(ext.TakeProfitRatio)
|
||||||
|
} else {
|
||||||
|
percent = decimal.NewFromInt(100).Sub(ext.StopLossRatio)
|
||||||
|
}
|
||||||
|
|
||||||
|
childPrice := lastPrice.Mul(percent.Div(decimal.NewFromInt(100).Truncate(4))).Truncate(int32(tradeSet.PriceDigit))
|
||||||
|
mainOrder.Child[index].Price = childPrice.String()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//todo 重新计算
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 递归订单树
|
||||||
|
func GetOrderByPid(order *models.LinePreOrder, orders []models.LinePreOrder, pid int) {
|
||||||
|
for _, v := range orders {
|
||||||
|
if v.Pid == pid {
|
||||||
|
GetOrderByPid(&v, orders, v.Id)
|
||||||
|
|
||||||
|
order.Childs = append(order.Childs, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
41
services/bitgetservice/bitget_rest_test.go
Normal file
41
services/bitgetservice/bitget_rest_test.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package bitgetservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go-admin/config"
|
||||||
|
"go-admin/test"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetSpotSymbols(t *testing.T) {
|
||||||
|
test.InitSettings()
|
||||||
|
api := NewBitgetRestApi()
|
||||||
|
|
||||||
|
symbols, err := api.GetSpotSymbols()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log(symbols)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取交易所信息
|
||||||
|
func TestGetExchangeInfo(t *testing.T) {
|
||||||
|
test.InitSettings()
|
||||||
|
|
||||||
|
client, err := NewBitgetClient("", "", "", config.ExtConfig.ProxyUrl)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := client.GetExchangeInfo()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Log(data)
|
||||||
|
}
|
||||||
658
services/bitgetservice/bitget_socketmanager.go
Normal file
658
services/bitgetservice/bitget_socketmanager.go
Normal file
@ -0,0 +1,658 @@
|
|||||||
|
package bitgetservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"go-admin/pkg/utility"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
log "github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BitgetWebSocketManager Bitget WebSocket管理器
|
||||||
|
// 类似于BinanceWebSocketManager,支持外部传入API密钥
|
||||||
|
type BitgetWebSocketManager struct {
|
||||||
|
ws *websocket.Conn
|
||||||
|
stopChannel chan struct{}
|
||||||
|
url string
|
||||||
|
/* 0-现货 1-合约 */
|
||||||
|
wsType int
|
||||||
|
apiKey string
|
||||||
|
apiSecret string
|
||||||
|
passphrase string
|
||||||
|
proxyType string
|
||||||
|
proxyAddress string
|
||||||
|
reconnect chan struct{}
|
||||||
|
isStopped bool // 标记 WebSocket 是否已主动停止
|
||||||
|
mu sync.Mutex // 用于控制并发访问 isStopped
|
||||||
|
cancelFunc context.CancelFunc
|
||||||
|
reconnecting atomic.Bool // 防止重复重连
|
||||||
|
ConnectTime time.Time // 当前连接建立时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已有连接存储
|
||||||
|
var BitgetSpotSockets = map[string]*BitgetWebSocketManager{}
|
||||||
|
var BitgetFutureSockets = map[string]*BitgetWebSocketManager{}
|
||||||
|
|
||||||
|
// NewBitgetWebSocketManager 创建新的Bitget WebSocket管理器
|
||||||
|
func NewBitgetWebSocketManager(wsType int, apiKey, apiSecret, passphrase, proxyType, proxyAddress string) *BitgetWebSocketManager {
|
||||||
|
var wsUrl string
|
||||||
|
|
||||||
|
switch wsType {
|
||||||
|
case 0: // 现货
|
||||||
|
wsUrl = "wss://ws.bitget.com/spot/v1/stream"
|
||||||
|
case 1: // 合约
|
||||||
|
wsUrl = "wss://ws.bitget.com/mix/v1/stream"
|
||||||
|
default:
|
||||||
|
wsUrl = "wss://ws.bitget.com/spot/v1/stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &BitgetWebSocketManager{
|
||||||
|
stopChannel: make(chan struct{}, 10),
|
||||||
|
reconnect: make(chan struct{}, 10),
|
||||||
|
isStopped: false,
|
||||||
|
url: wsUrl,
|
||||||
|
wsType: wsType,
|
||||||
|
apiKey: apiKey,
|
||||||
|
apiSecret: apiSecret,
|
||||||
|
passphrase: passphrase,
|
||||||
|
proxyType: proxyType,
|
||||||
|
proxyAddress: proxyAddress,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetKey 获取API Key
|
||||||
|
func (wm *BitgetWebSocketManager) GetKey() string {
|
||||||
|
return wm.apiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start 启动WebSocket连接
|
||||||
|
func (wm *BitgetWebSocketManager) Start() {
|
||||||
|
utility.SafeGo(wm.run)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart 重启连接
|
||||||
|
func (wm *BitgetWebSocketManager) Restart(apiKey, apiSecret, passphrase, proxyType, proxyAddress string) *BitgetWebSocketManager {
|
||||||
|
wm.mu.Lock()
|
||||||
|
defer wm.mu.Unlock()
|
||||||
|
|
||||||
|
wm.apiKey = apiKey
|
||||||
|
wm.apiSecret = apiSecret
|
||||||
|
wm.passphrase = passphrase
|
||||||
|
wm.proxyType = proxyType
|
||||||
|
wm.proxyAddress = proxyAddress
|
||||||
|
|
||||||
|
if wm.isStopped {
|
||||||
|
wm.isStopped = false
|
||||||
|
utility.SafeGo(wm.run)
|
||||||
|
} else {
|
||||||
|
log.Warnf("调用Bitget WebSocket restart")
|
||||||
|
wm.triggerReconnect(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return wm
|
||||||
|
}
|
||||||
|
|
||||||
|
// triggerReconnect 触发重连
|
||||||
|
func (wm *BitgetWebSocketManager) triggerReconnect(force bool) {
|
||||||
|
if force {
|
||||||
|
wm.reconnecting.Store(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if wm.reconnecting.CompareAndSwap(false, true) {
|
||||||
|
log.Warnf("准备重连Bitget WebSocket key: %s wsType: %v", wm.apiKey, wm.wsType)
|
||||||
|
select {
|
||||||
|
case wm.reconnect <- struct{}{}:
|
||||||
|
default:
|
||||||
|
log.Debugf("Bitget reconnect 信号已存在,跳过 key:%s", wm.apiKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// run 主运行循环
|
||||||
|
func (wm *BitgetWebSocketManager) run() {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
wm.cancelFunc = cancel
|
||||||
|
|
||||||
|
utility.SafeGo(wm.handleSignal)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
if err := wm.connect(ctx); err != nil {
|
||||||
|
wm.handleConnectionError(err)
|
||||||
|
|
||||||
|
if wm.isErrorCountExceeded() {
|
||||||
|
log.Error("连接Bitget WebSocket时出错次数过多,停止WebSocket管理器: wsType=%v, key=%s", wm.wsType, wm.apiKey)
|
||||||
|
wm.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
<-wm.stopChannel
|
||||||
|
log.Info("停止Bitget WebSocket管理器... wsType=%s", getBitgetWsTypeName(wm.wsType))
|
||||||
|
wm.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleConnectionError 处理连接错误
|
||||||
|
func (wm *BitgetWebSocketManager) handleConnectionError(err error) {
|
||||||
|
// 这里可以添加错误记录到Redis的逻辑,类似binance的实现
|
||||||
|
log.Error("连接Bitget WebSocket时出错: wsType=%v, key=%v, 错误=%v", wm.wsType, wm.apiKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// isErrorCountExceeded 检查错误次数是否超过阈值
|
||||||
|
func (wm *BitgetWebSocketManager) isErrorCountExceeded() bool {
|
||||||
|
// 简化版本,可以根据需要实现更复杂的错误计数逻辑
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSignal 处理终止信号
|
||||||
|
func (wm *BitgetWebSocketManager) handleSignal() {
|
||||||
|
ch := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(ch, os.Interrupt)
|
||||||
|
<-ch
|
||||||
|
wm.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect 建立WebSocket连接
|
||||||
|
func (wm *BitgetWebSocketManager) connect(ctx context.Context) error {
|
||||||
|
dialer, err := wm.getDialer()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于私有频道,需要进行身份验证
|
||||||
|
headers := wm.getAuthHeaders()
|
||||||
|
|
||||||
|
wm.ws, _, err = dialer.Dial(wm.url, headers)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接成功,更新连接时间
|
||||||
|
wm.ConnectTime = time.Now()
|
||||||
|
log.Info("已连接到Bitget %s WebSocket key:%s", getBitgetWsTypeName(wm.wsType), wm.apiKey)
|
||||||
|
|
||||||
|
// 设置Ping处理器
|
||||||
|
wm.ws.SetPingHandler(func(appData string) error {
|
||||||
|
log.Info("收到Bitget Ping消息 wstype:%v key:%s data:%s", wm.wsType, wm.apiKey, appData)
|
||||||
|
|
||||||
|
for x := 0; x < 5; x++ {
|
||||||
|
if err := wm.ws.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(time.Second*10)); err != nil {
|
||||||
|
log.Error("Bitget回应Pong失败 次数:%d err:%v", x, err)
|
||||||
|
time.Sleep(time.Second * 1)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// 启动各种处理协程
|
||||||
|
utility.SafeGo(func() { wm.readMessages(ctx) })
|
||||||
|
utility.SafeGo(func() { wm.handleReconnect(ctx) })
|
||||||
|
utility.SafeGo(func() { wm.startPingLoop(ctx) })
|
||||||
|
|
||||||
|
// 发送登录认证消息
|
||||||
|
if err := wm.authenticate(); err != nil {
|
||||||
|
return fmt.Errorf("bitget WebSocket认证失败: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDialer 获取WebSocket拨号器
|
||||||
|
func (wm *BitgetWebSocketManager) getDialer() (*websocket.Dialer, error) {
|
||||||
|
if wm.proxyAddress == "" {
|
||||||
|
return &websocket.Dialer{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(wm.proxyAddress, "http://") &&
|
||||||
|
!strings.HasPrefix(wm.proxyAddress, "https://") &&
|
||||||
|
!strings.HasPrefix(wm.proxyAddress, "socks5://") {
|
||||||
|
wm.proxyAddress = wm.proxyType + "://" + wm.proxyAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyURL, err := url.Parse(wm.proxyAddress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse proxy URL: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch proxyURL.Scheme {
|
||||||
|
case "socks5":
|
||||||
|
return wm.createSocks5Dialer(proxyURL)
|
||||||
|
case "http", "https":
|
||||||
|
return &websocket.Dialer{
|
||||||
|
Proxy: func(req *http.Request) (*url.URL, error) {
|
||||||
|
return proxyURL, nil
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported proxy scheme: %s", proxyURL.Scheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createSocks5Dialer 创建SOCKS5代理拨号器
|
||||||
|
func (wm *BitgetWebSocketManager) createSocks5Dialer(proxyURL *url.URL) (*websocket.Dialer, error) {
|
||||||
|
auth := &proxy.Auth{}
|
||||||
|
if proxyURL.User != nil {
|
||||||
|
auth.User = proxyURL.User.Username()
|
||||||
|
auth.Password, _ = proxyURL.User.Password()
|
||||||
|
}
|
||||||
|
|
||||||
|
socksDialer, err := proxy.SOCKS5("tcp", proxyURL.Host, auth, proxy.Direct)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create SOCKS5 proxy dialer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &websocket.Dialer{
|
||||||
|
NetDialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return socksDialer.Dial(network, addr)
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getAuthHeaders 获取认证头信息
|
||||||
|
func (wm *BitgetWebSocketManager) getAuthHeaders() http.Header {
|
||||||
|
headers := http.Header{}
|
||||||
|
|
||||||
|
if wm.apiKey != "" {
|
||||||
|
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
||||||
|
sign := wm.generateSignature(timestamp)
|
||||||
|
|
||||||
|
headers.Set("ACCESS-KEY", wm.apiKey)
|
||||||
|
headers.Set("ACCESS-SIGN", sign)
|
||||||
|
headers.Set("ACCESS-TIMESTAMP", timestamp)
|
||||||
|
headers.Set("ACCESS-PASSPHRASE", wm.passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateSignature 生成签名
|
||||||
|
func (wm *BitgetWebSocketManager) generateSignature(timestamp string) string {
|
||||||
|
message := timestamp + "GET" + "/user/verify"
|
||||||
|
|
||||||
|
h := hmac.New(sha256.New, []byte(wm.apiSecret))
|
||||||
|
h.Write([]byte(message))
|
||||||
|
|
||||||
|
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// authenticate 发送认证消息
|
||||||
|
func (wm *BitgetWebSocketManager) authenticate() error {
|
||||||
|
if wm.apiKey == "" {
|
||||||
|
return nil // 公共频道不需要认证
|
||||||
|
}
|
||||||
|
|
||||||
|
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
|
||||||
|
sign := wm.generateSignature(timestamp)
|
||||||
|
|
||||||
|
authMsg := map[string]interface{}{
|
||||||
|
"op": "login",
|
||||||
|
"args": []map[string]string{
|
||||||
|
{
|
||||||
|
"apiKey": wm.apiKey,
|
||||||
|
"passphrase": wm.passphrase,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"sign": sign,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBytes, err := sonic.Marshal(authMsg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return wm.ws.WriteMessage(websocket.TextMessage, msgBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// readMessages 读取消息
|
||||||
|
func (wm *BitgetWebSocketManager) readMessages(ctx context.Context) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
log.Error("Bitget WebSocket消息处理器panic: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
wm.mu.Lock()
|
||||||
|
if wm.isStopped || wm.ws == nil {
|
||||||
|
wm.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, message, err := wm.ws.ReadMessage()
|
||||||
|
wm.mu.Unlock()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Bitget WebSocket读取消息错误: %v", err)
|
||||||
|
wm.triggerReconnect(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理消息
|
||||||
|
wm.handleMessage(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleMessage 处理收到的消息
|
||||||
|
func (wm *BitgetWebSocketManager) handleMessage(message []byte) {
|
||||||
|
// 解析消息并处理
|
||||||
|
var baseMsg map[string]interface{}
|
||||||
|
if err := sonic.Unmarshal(message, &baseMsg); err != nil {
|
||||||
|
log.Error("解析Bitget WebSocket消息失败: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是认证响应
|
||||||
|
if op, exists := baseMsg["op"]; exists && op == "login" {
|
||||||
|
wm.handleAuthResponse(baseMsg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是订阅数据
|
||||||
|
if arg, exists := baseMsg["arg"]; exists {
|
||||||
|
wm.handleSubscriptionData(baseMsg, arg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("收到Bitget WebSocket消息: %s", string(message))
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAuthResponse 处理认证响应
|
||||||
|
func (wm *BitgetWebSocketManager) handleAuthResponse(msg map[string]interface{}) {
|
||||||
|
if code, exists := msg["code"]; exists {
|
||||||
|
if code == "0" || code == 0 {
|
||||||
|
log.Info("Bitget WebSocket认证成功 key:%s", wm.apiKey)
|
||||||
|
// 认证成功后可以订阅私有频道
|
||||||
|
wm.subscribePrivateChannels()
|
||||||
|
} else {
|
||||||
|
log.Error("Bitget WebSocket认证失败 key:%s code:%v", wm.apiKey, code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleSubscriptionData 处理订阅数据
|
||||||
|
func (wm *BitgetWebSocketManager) handleSubscriptionData(msg map[string]interface{}, arg interface{}) {
|
||||||
|
// 根据频道类型处理不同的数据
|
||||||
|
// 这里可以根据具体需求实现不同频道的数据处理逻辑
|
||||||
|
log.Info("收到Bitget订阅数据: %v", arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// subscribePrivateChannels 订阅私有频道
|
||||||
|
func (wm *BitgetWebSocketManager) subscribePrivateChannels() {
|
||||||
|
// 订阅账户更新
|
||||||
|
wm.subscribeAccount()
|
||||||
|
// 订阅订单更新
|
||||||
|
wm.subscribeOrders()
|
||||||
|
// 订阅持仓更新
|
||||||
|
wm.subscribePositions()
|
||||||
|
}
|
||||||
|
|
||||||
|
// subscribeAccount 订阅账户更新
|
||||||
|
func (wm *BitgetWebSocketManager) subscribeAccount() {
|
||||||
|
var channel string
|
||||||
|
if wm.wsType == 0 {
|
||||||
|
channel = "account"
|
||||||
|
} else {
|
||||||
|
channel = "account"
|
||||||
|
}
|
||||||
|
|
||||||
|
subMsg := map[string]interface{}{
|
||||||
|
"op": "subscribe",
|
||||||
|
"args": []map[string]string{
|
||||||
|
{
|
||||||
|
"instType": wm.getInstType(),
|
||||||
|
"channel": channel,
|
||||||
|
"instId": "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
wm.sendMessage(subMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// subscribeOrders 订阅订单更新
|
||||||
|
func (wm *BitgetWebSocketManager) subscribeOrders() {
|
||||||
|
var channel string
|
||||||
|
if wm.wsType == 0 {
|
||||||
|
channel = "orders"
|
||||||
|
} else {
|
||||||
|
channel = "orders"
|
||||||
|
}
|
||||||
|
|
||||||
|
subMsg := map[string]interface{}{
|
||||||
|
"op": "subscribe",
|
||||||
|
"args": []map[string]string{
|
||||||
|
{
|
||||||
|
"instType": wm.getInstType(),
|
||||||
|
"channel": channel,
|
||||||
|
"instId": "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
wm.sendMessage(subMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// subscribePositions 订阅持仓更新(仅合约)
|
||||||
|
func (wm *BitgetWebSocketManager) subscribePositions() {
|
||||||
|
if wm.wsType != 1 {
|
||||||
|
return // 只有合约才有持仓
|
||||||
|
}
|
||||||
|
|
||||||
|
subMsg := map[string]interface{}{
|
||||||
|
"op": "subscribe",
|
||||||
|
"args": []map[string]string{
|
||||||
|
{
|
||||||
|
"instType": "umcbl",
|
||||||
|
"channel": "positions",
|
||||||
|
"instId": "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
wm.sendMessage(subMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getInstType 获取产品类型
|
||||||
|
func (wm *BitgetWebSocketManager) getInstType() string {
|
||||||
|
if wm.wsType == 0 {
|
||||||
|
return "sp" // 现货
|
||||||
|
}
|
||||||
|
return "umcbl" // U本位合约
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMessage 发送消息(公开方法)
|
||||||
|
func (wm *BitgetWebSocketManager) SendMessage(msg interface{}) error {
|
||||||
|
msgBytes, err := sonic.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
wm.mu.Lock()
|
||||||
|
defer wm.mu.Unlock()
|
||||||
|
|
||||||
|
if wm.ws == nil {
|
||||||
|
return errors.New("WebSocket连接不可用")
|
||||||
|
}
|
||||||
|
|
||||||
|
return wm.ws.WriteMessage(websocket.TextMessage, msgBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendMessage 发送消息(私有方法,保持向后兼容)
|
||||||
|
func (wm *BitgetWebSocketManager) sendMessage(msg interface{}) error {
|
||||||
|
return wm.SendMessage(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleReconnect 处理重连
|
||||||
|
func (wm *BitgetWebSocketManager) handleReconnect(ctx context.Context) {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-wm.reconnect:
|
||||||
|
wm.reconnecting.Store(false)
|
||||||
|
log.Info("开始重连Bitget WebSocket key:%s", wm.apiKey)
|
||||||
|
|
||||||
|
wm.mu.Lock()
|
||||||
|
if wm.ws != nil {
|
||||||
|
wm.ws.Close()
|
||||||
|
wm.ws = nil
|
||||||
|
}
|
||||||
|
wm.mu.Unlock()
|
||||||
|
|
||||||
|
// 触发主循环重新连接
|
||||||
|
select {
|
||||||
|
case wm.stopChannel <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// startPingLoop 启动心跳循环
|
||||||
|
func (wm *BitgetWebSocketManager) startPingLoop(ctx context.Context) {
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
wm.sendPing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendPing 发送心跳
|
||||||
|
func (wm *BitgetWebSocketManager) sendPing() {
|
||||||
|
pingMsg := "ping"
|
||||||
|
|
||||||
|
wm.mu.Lock()
|
||||||
|
defer wm.mu.Unlock()
|
||||||
|
|
||||||
|
if wm.ws == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := wm.ws.WriteMessage(websocket.TextMessage, []byte(pingMsg)); err != nil {
|
||||||
|
log.Error("Bitget WebSocket发送ping失败: %v", err)
|
||||||
|
wm.triggerReconnect(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop 停止WebSocket连接
|
||||||
|
func (wm *BitgetWebSocketManager) Stop() {
|
||||||
|
wm.mu.Lock()
|
||||||
|
defer wm.mu.Unlock()
|
||||||
|
|
||||||
|
if wm.isStopped {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
wm.isStopped = true
|
||||||
|
|
||||||
|
if wm.cancelFunc != nil {
|
||||||
|
wm.cancelFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
if wm.ws != nil {
|
||||||
|
wm.ws.Close()
|
||||||
|
wm.ws = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从全局map中移除
|
||||||
|
if wm.wsType == 0 {
|
||||||
|
delete(BitgetSpotSockets, wm.apiKey)
|
||||||
|
} else {
|
||||||
|
delete(BitgetFutureSockets, wm.apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Bitget WebSocket已停止 key:%s", wm.apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBitgetWsTypeName 获取WebSocket类型名称
|
||||||
|
func getBitgetWsTypeName(wsType int) string {
|
||||||
|
switch wsType {
|
||||||
|
case 0:
|
||||||
|
return "现货"
|
||||||
|
case 1:
|
||||||
|
return "合约"
|
||||||
|
default:
|
||||||
|
return "未知"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 工具函数:获取或创建Bitget WebSocket管理器
|
||||||
|
func GetOrCreateBitgetWebSocketManager(wsType int, apiKey, apiSecret, passphrase, proxyType, proxyAddress string) *BitgetWebSocketManager {
|
||||||
|
var socketsMap map[string]*BitgetWebSocketManager
|
||||||
|
|
||||||
|
if wsType == 0 {
|
||||||
|
socketsMap = BitgetSpotSockets
|
||||||
|
} else {
|
||||||
|
socketsMap = BitgetFutureSockets
|
||||||
|
}
|
||||||
|
|
||||||
|
if manager, exists := socketsMap[apiKey]; exists {
|
||||||
|
// 如果参数有变化,重启连接
|
||||||
|
if manager.apiSecret != apiSecret || manager.passphrase != passphrase ||
|
||||||
|
manager.proxyType != proxyType || manager.proxyAddress != proxyAddress {
|
||||||
|
manager.Restart(apiKey, apiSecret, passphrase, proxyType, proxyAddress)
|
||||||
|
}
|
||||||
|
return manager
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建新的管理器
|
||||||
|
manager := NewBitgetWebSocketManager(wsType, apiKey, apiSecret, passphrase, proxyType, proxyAddress)
|
||||||
|
socketsMap[apiKey] = manager
|
||||||
|
|
||||||
|
return manager
|
||||||
|
}
|
||||||
|
|
||||||
|
// StopBitgetWebSocketManager 停止指定的WebSocket管理器
|
||||||
|
func StopBitgetWebSocketManager(wsType int, apiKey string) {
|
||||||
|
var socketsMap map[string]*BitgetWebSocketManager
|
||||||
|
|
||||||
|
if wsType == 0 {
|
||||||
|
socketsMap = BitgetSpotSockets
|
||||||
|
} else {
|
||||||
|
socketsMap = BitgetFutureSockets
|
||||||
|
}
|
||||||
|
|
||||||
|
if manager, exists := socketsMap[apiKey]; exists {
|
||||||
|
manager.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,10 +1,13 @@
|
|||||||
package cacheservice
|
package cacheservice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go-admin/app/admin/models"
|
"go-admin/app/admin/models"
|
||||||
"go-admin/common/const/rediskey"
|
"go-admin/common/const/rediskey"
|
||||||
|
"go-admin/common/global"
|
||||||
"go-admin/common/helper"
|
"go-admin/common/helper"
|
||||||
|
models2 "go-admin/models"
|
||||||
|
|
||||||
"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"
|
||||||
@ -59,3 +62,96 @@ func GetConfigCacheByKey(db *gorm.DB, key string) models.SysConfig {
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取缓存交易对
|
||||||
|
// symbolType 1-现货 2-合约
|
||||||
|
func GetTradeSet(exchangeType string, symbol string, symbolType int) (models2.TradeSet, error) {
|
||||||
|
result := models2.TradeSet{}
|
||||||
|
val := ""
|
||||||
|
|
||||||
|
switch symbolType {
|
||||||
|
case 1:
|
||||||
|
key := fmt.Sprintf(global.TICKER_SPOT, exchangeType, symbol)
|
||||||
|
val, _ = helper.DefaultRedis.GetString(key)
|
||||||
|
case 2:
|
||||||
|
key := fmt.Sprintf(global.TICKER_FUTURES, exchangeType, symbol)
|
||||||
|
val, _ = helper.DefaultRedis.GetString(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
if val != "" {
|
||||||
|
if err := sonic.Unmarshal([]byte(val), &result); err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return result, errors.New("未找到交易对信息")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取交易所所有交易对
|
||||||
|
// symbolType 1-现货 2-合约
|
||||||
|
func GetExchangeTradeSets(exchange string, symbolType int) ([]models2.TradeSet, error) {
|
||||||
|
result := []models2.TradeSet{}
|
||||||
|
val := []string{}
|
||||||
|
|
||||||
|
switch symbolType {
|
||||||
|
case 1:
|
||||||
|
key := fmt.Sprintf(global.TICKER_SPOT, exchange, "*")
|
||||||
|
val, _ = helper.DefaultRedis.GetAllKeysAndValues(key)
|
||||||
|
case 2:
|
||||||
|
key := fmt.Sprintf(global.TICKER_FUTURES, exchange, "*")
|
||||||
|
val, _ = helper.DefaultRedis.GetAllKeysAndValues(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range val {
|
||||||
|
item := models2.TradeSet{}
|
||||||
|
if err := sonic.Unmarshal([]byte(v), &item); err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
result = append(result, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) == 0 {
|
||||||
|
return result, errors.New("未找到交易对信息")
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSystemSetting(db *gorm.DB) (models.LineSystemSetting, error) {
|
||||||
|
key := fmt.Sprintf(rediskey.SystemSetting)
|
||||||
|
val, _ := helper.DefaultRedis.GetString(key)
|
||||||
|
setting := models.LineSystemSetting{}
|
||||||
|
|
||||||
|
if val != "" {
|
||||||
|
sonic.UnmarshalString(val, &setting)
|
||||||
|
}
|
||||||
|
|
||||||
|
if setting.Id > 0 {
|
||||||
|
return setting, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
setting, err = ResetSystemSetting(db)
|
||||||
|
if err != nil {
|
||||||
|
return setting, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return setting, nil
|
||||||
|
}
|
||||||
|
func ResetSystemSetting(db *gorm.DB) (models.LineSystemSetting, error) {
|
||||||
|
setting := models.LineSystemSetting{}
|
||||||
|
if err := db.Model(&setting).First(&setting).Error; err != nil {
|
||||||
|
return setting, err
|
||||||
|
}
|
||||||
|
|
||||||
|
settVal, _ := sonic.MarshalString(&setting)
|
||||||
|
|
||||||
|
if settVal != "" {
|
||||||
|
if err := helper.DefaultRedis.SetString(rediskey.SystemSetting, settVal); err != nil {
|
||||||
|
logger.Error("redis添加系统设置失败", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return models.LineSystemSetting{}, nil
|
||||||
|
}
|
||||||
|
|||||||
15
services/commonservice/commonservice.go
Normal file
15
services/commonservice/commonservice.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package commonservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 获取数据库连接
|
||||||
|
func GetDBConnection() *gorm.DB {
|
||||||
|
dbs := sdk.Runtime.GetDb()
|
||||||
|
for _, db := range dbs {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
63
services/commonservice/judge.go
Normal file
63
services/commonservice/judge.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package commonservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"go-admin/common/global"
|
||||||
|
"go-admin/common/helper"
|
||||||
|
"go-admin/models/binancedto"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 判断api订阅是否超时
|
||||||
|
// wsType 1 现货 2 合约
|
||||||
|
func JudgeWebsocketTimeout(apiKey string, wsType int) error {
|
||||||
|
key := fmt.Sprintf(global.USER_SUBSCRIBE, apiKey)
|
||||||
|
cache := binancedto.UserSubscribeState{}
|
||||||
|
// 从redis中获取订阅信息
|
||||||
|
subscribeInfo, err := helper.DefaultRedis.GetString(key)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("获取订阅信息失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
sonic.Unmarshal([]byte(subscribeInfo), &cache)
|
||||||
|
|
||||||
|
if cache.FuturesLastTime == nil && cache.SpotLastTime == nil {
|
||||||
|
return errors.New("未订阅任何行情")
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeOutInterval time.Duration
|
||||||
|
|
||||||
|
if wsType == 1 {
|
||||||
|
timeOutInterval = time.Minute * 2
|
||||||
|
} else {
|
||||||
|
timeOutInterval = time.Minute * 4
|
||||||
|
}
|
||||||
|
|
||||||
|
switch wsType {
|
||||||
|
case 1:
|
||||||
|
// 现货
|
||||||
|
if cache.SpotLastTime == nil {
|
||||||
|
return errors.New("未订阅现货行情")
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Since(*cache.SpotLastTime) > timeOutInterval {
|
||||||
|
return errors.New("现货行情订阅超时")
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
// 合约
|
||||||
|
if cache.FuturesLastTime == nil {
|
||||||
|
return errors.New("未订阅合约行情")
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Since(*cache.FuturesLastTime) > timeOutInterval {
|
||||||
|
return errors.New("合约行情订阅超时")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return errors.New("无效的wsType")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
package excservice
|
package excservice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"go-admin/models/futuresdto"
|
"go-admin/models/futuresdto"
|
||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
"go-admin/services/binanceservice"
|
"go-admin/services/binanceservice"
|
||||||
@ -15,27 +16,43 @@ import (
|
|||||||
- @msg 消息内容
|
- @msg 消息内容
|
||||||
- @listenType 订阅类型 0-现货 1-合约
|
- @listenType 订阅类型 0-现货 1-合约
|
||||||
*/
|
*/
|
||||||
func ReceiveListen(msg []byte, listenType int) (reconnect bool, err error) {
|
// ReceiveListen 处理用户订单订阅
|
||||||
|
// 返回:
|
||||||
|
// - reconnect: bool, 是否需要立即重连 (通常用于 listenKey 过期)
|
||||||
|
// - fatal: bool, 是否是致命错误 (例如认证失败),需要停止整个服务
|
||||||
|
// - err: error, 解析或处理过程中的错误
|
||||||
|
func ReceiveListen(msg []byte, listenType int, apiKey string) (reconnect bool, fatal bool, err error) {
|
||||||
var dataMap map[string]interface{}
|
var dataMap map[string]interface{}
|
||||||
err = sonic.Unmarshal(msg, &dataMap)
|
err = sonic.Unmarshal(msg, &dataMap)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("接收ws 反序列化失败:", err)
|
log.Error("接收ws 反序列化失败:", err)
|
||||||
return
|
return false, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是币安的错误消息, e.g. {"code":-2015,"msg":"Invalid API-key, IP, or permissions for action."}
|
||||||
|
if code, ok := dataMap["code"].(float64); ok && code != 0 {
|
||||||
|
errMsg, _ := dataMap["msg"].(string)
|
||||||
|
log.Errorf("收到币安错误码: %d, 消息: %s, API Key: %s", int(code), errMsg, apiKey)
|
||||||
|
// -2015 是无效API Key的错误码,这是一个致命错误,不需要重连
|
||||||
|
if int(code) == -2015 {
|
||||||
|
return false, true, fmt.Errorf("币安认证失败 (code: %d): %s", int(code), errMsg)
|
||||||
|
}
|
||||||
|
// 其他错误码可能也需要特殊处理,但暂时我们只将-2015视为致命错误
|
||||||
|
return false, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
event, exits := dataMap["e"]
|
event, exits := dataMap["e"]
|
||||||
|
|
||||||
if !exits {
|
if !exits {
|
||||||
log.Error("不存在event")
|
// 如果没有 event 字段,但也不是一个错误消息,则可能是一个未知的消息格式
|
||||||
return
|
log.Warn("收到的消息中不存在event字段: %s", string(msg))
|
||||||
|
return false, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
switch event {
|
switch event {
|
||||||
//listenKey过期
|
//listenKey过期
|
||||||
case "listenKeyExpired":
|
case "listenKeyExpired":
|
||||||
log.Info("listenKey过期", string(msg))
|
log.Info("listenKey过期", string(msg))
|
||||||
return true, nil
|
return true, false, nil
|
||||||
|
|
||||||
//订单变更
|
//订单变更
|
||||||
case "ORDER_TRADE_UPDATE":
|
case "ORDER_TRADE_UPDATE":
|
||||||
@ -101,11 +118,12 @@ func ReceiveListen(msg []byte, listenType int) (reconnect bool, err error) {
|
|||||||
|
|
||||||
case "eventStreamTerminated":
|
case "eventStreamTerminated":
|
||||||
log.Info("账户数据流被终止 type:", getWsTypeName(listenType))
|
log.Info("账户数据流被终止 type:", getWsTypeName(listenType))
|
||||||
|
return true, false, nil
|
||||||
default:
|
default:
|
||||||
log.Info("未知事件 内容:", string(msg))
|
log.Info("未知事件 内容:", string(msg))
|
||||||
log.Info("未知事件", event)
|
log.Info("未知事件", event)
|
||||||
return false, nil
|
return false, false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return false, nil
|
return false, false, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package excservice
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"go-admin/common/global"
|
"go-admin/common/global"
|
||||||
@ -11,44 +10,146 @@ import (
|
|||||||
"go-admin/models/commondto"
|
"go-admin/models/commondto"
|
||||||
"go-admin/pkg/jsonhelper"
|
"go-admin/pkg/jsonhelper"
|
||||||
"go-admin/pkg/utility"
|
"go-admin/pkg/utility"
|
||||||
|
"go-admin/services/proxy"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"os/signal"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
log "github.com/go-admin-team/go-admin-core/logger"
|
log "github.com/go-admin-team/go-admin-core/logger"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"golang.org/x/net/proxy"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type BinanceWebSocketManager struct {
|
type BinanceWebSocketManager struct {
|
||||||
ws *websocket.Conn
|
mu sync.Mutex
|
||||||
stopChannel chan struct{}
|
stopOnce sync.Once // 新增,确保Stop只执行一次
|
||||||
url string
|
ws *websocket.Conn
|
||||||
/* 0-现货 1-合约 */
|
stopChannel chan struct{}
|
||||||
wsType int
|
url string
|
||||||
apiKey string
|
wsType int
|
||||||
apiSecret string
|
apiKey string
|
||||||
proxyType string
|
apiSecret string
|
||||||
proxyAddress string
|
proxyType string
|
||||||
reconnect chan struct{}
|
proxyAddress string
|
||||||
isStopped bool // 标记 WebSocket 是否已主动停止
|
reconnect chan struct{}
|
||||||
mu sync.Mutex // 用于控制并发访问 isStopped
|
isStopped bool
|
||||||
cancelFunc context.CancelFunc
|
cancelFunc context.CancelFunc
|
||||||
listenKey string // 新增字段
|
listenKey string
|
||||||
|
reconnecting atomic.Bool
|
||||||
|
ConnectTime time.Time
|
||||||
|
lastPingTime atomic.Value
|
||||||
|
forceReconnectTimer *time.Timer
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... (省略中间代码)
|
||||||
|
|
||||||
|
// sendPing 使用锁来确保发送ping时的线程安全
|
||||||
|
func (wm *BinanceWebSocketManager) sendPing() error {
|
||||||
|
wm.mu.Lock()
|
||||||
|
defer wm.mu.Unlock()
|
||||||
|
if wm.ws == nil {
|
||||||
|
return errors.New("websocket connection is nil")
|
||||||
|
}
|
||||||
|
wm.lastPingTime.Store(time.Now())
|
||||||
|
return wm.ws.WriteMessage(websocket.PingMessage, []byte("ping"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop 优雅地停止WebSocket管理器
|
||||||
|
func (wm *BinanceWebSocketManager) Stop() {
|
||||||
|
wm.stopOnce.Do(func() {
|
||||||
|
wm.mu.Lock()
|
||||||
|
if wm.isStopped {
|
||||||
|
wm.mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wm.isStopped = true
|
||||||
|
wm.mu.Unlock()
|
||||||
|
|
||||||
|
if wm.cancelFunc != nil {
|
||||||
|
wm.cancelFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
wm.closeConn() // 统一调用带锁的关闭方法
|
||||||
|
|
||||||
|
// 尝试关闭stopChannel,如果已经关闭则忽略
|
||||||
|
select {
|
||||||
|
case <-wm.stopChannel:
|
||||||
|
default:
|
||||||
|
close(wm.stopChannel)
|
||||||
|
}
|
||||||
|
log.Infof("WebSocket 已完全停止 key:%s", wm.apiKey)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleReconnect 处理重连逻辑
|
||||||
|
func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) {
|
||||||
|
const maxRetries = 10
|
||||||
|
backoff := time.Second
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-wm.reconnect:
|
||||||
|
if !wm.reconnecting.CompareAndSwap(false, true) {
|
||||||
|
continue // 如果已经在重连,则忽略
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在开始重连循环之前,先安全地关闭旧的连接
|
||||||
|
wm.closeConn()
|
||||||
|
|
||||||
|
retryCount := 0
|
||||||
|
for retryCount < maxRetries {
|
||||||
|
select {
|
||||||
|
case <-wm.stopChannel:
|
||||||
|
wm.reconnecting.Store(false)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("WebSocket (%s) 正在尝试重连,第 %d 次...", wm.wsType, retryCount+1)
|
||||||
|
if err := wm.connect(ctx); err == nil {
|
||||||
|
log.Infof("WebSocket (%s) 重连成功", wm.wsType)
|
||||||
|
wm.reconnecting.Store(false)
|
||||||
|
go wm.startDeadCheck(ctx) // 重连成功后,重新启动假死检测
|
||||||
|
break // 跳出重连循环
|
||||||
|
}
|
||||||
|
|
||||||
|
retryCount++
|
||||||
|
time.Sleep(backoff)
|
||||||
|
backoff *= 2
|
||||||
|
if backoff > time.Minute {
|
||||||
|
backoff = time.Minute
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if retryCount >= maxRetries {
|
||||||
|
log.Errorf("WebSocket (%s) 重连失败次数过多,停止重连", wm.wsType)
|
||||||
|
wm.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-ctx.Done():
|
||||||
|
wm.reconnecting.Store(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 已有连接
|
// 已有连接
|
||||||
var SpotSockets = map[string]*BinanceWebSocketManager{}
|
var SpotSockets = map[string]*BinanceWebSocketManager{}
|
||||||
var FutureSockets = map[string]*BinanceWebSocketManager{}
|
var FutureSockets = map[string]*BinanceWebSocketManager{}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建新的Binance WebSocket管理器
|
||||||
|
* @param wsType WebSocket类型:0-现货,1-合约
|
||||||
|
* @param apiKey API密钥
|
||||||
|
* @param apiSecret API密钥
|
||||||
|
* @param proxyType 代理类型
|
||||||
|
* @param proxyAddress 代理地址
|
||||||
|
* @return WebSocket管理器实例
|
||||||
|
*/
|
||||||
func NewBinanceWebSocketManager(wsType int, apiKey, apiSecret, proxyType, proxyAddress string) *BinanceWebSocketManager {
|
func NewBinanceWebSocketManager(wsType int, apiKey, apiSecret, proxyType, proxyAddress string) *BinanceWebSocketManager {
|
||||||
url := ""
|
url := ""
|
||||||
|
|
||||||
@ -59,7 +160,7 @@ func NewBinanceWebSocketManager(wsType int, apiKey, apiSecret, proxyType, proxyA
|
|||||||
url = "wss://fstream.binance.com/ws"
|
url = "wss://fstream.binance.com/ws"
|
||||||
}
|
}
|
||||||
|
|
||||||
return &BinanceWebSocketManager{
|
wm := &BinanceWebSocketManager{
|
||||||
stopChannel: make(chan struct{}, 10),
|
stopChannel: make(chan struct{}, 10),
|
||||||
reconnect: make(chan struct{}, 10),
|
reconnect: make(chan struct{}, 10),
|
||||||
isStopped: false,
|
isStopped: false,
|
||||||
@ -70,14 +171,36 @@ func NewBinanceWebSocketManager(wsType int, apiKey, apiSecret, proxyType, proxyA
|
|||||||
proxyType: proxyType,
|
proxyType: proxyType,
|
||||||
proxyAddress: proxyAddress,
|
proxyAddress: proxyAddress,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 初始化最后ping时间
|
||||||
|
wm.lastPingTime.Store(time.Now())
|
||||||
|
|
||||||
|
return wm
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取API密钥
|
||||||
|
* @return API密钥
|
||||||
|
*/
|
||||||
|
func (wm *BinanceWebSocketManager) GetKey() string {
|
||||||
|
return wm.apiKey
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动WebSocket连接
|
||||||
|
*/
|
||||||
func (wm *BinanceWebSocketManager) Start() {
|
func (wm *BinanceWebSocketManager) Start() {
|
||||||
utility.SafeGo(wm.run)
|
utility.SafeGo(wm.run)
|
||||||
// wm.run()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重启连接
|
/**
|
||||||
|
* 重启连接,更新配置参数
|
||||||
|
* @param apiKey 新的API密钥
|
||||||
|
* @param apiSecret 新的API密钥
|
||||||
|
* @param proxyType 新的代理类型
|
||||||
|
* @param proxyAddress 新的代理地址
|
||||||
|
* @return WebSocket管理器实例
|
||||||
|
*/
|
||||||
func (wm *BinanceWebSocketManager) Restart(apiKey, apiSecret, proxyType, proxyAddress string) *BinanceWebSocketManager {
|
func (wm *BinanceWebSocketManager) Restart(apiKey, apiSecret, proxyType, proxyAddress string) *BinanceWebSocketManager {
|
||||||
wm.mu.Lock()
|
wm.mu.Lock()
|
||||||
defer wm.mu.Unlock()
|
defer wm.mu.Unlock()
|
||||||
@ -91,20 +214,51 @@ func (wm *BinanceWebSocketManager) Restart(apiKey, apiSecret, proxyType, proxyAd
|
|||||||
wm.isStopped = false
|
wm.isStopped = false
|
||||||
utility.SafeGo(wm.run)
|
utility.SafeGo(wm.run)
|
||||||
} else {
|
} else {
|
||||||
wm.reconnect <- struct{}{}
|
log.Warnf("调用restart")
|
||||||
|
wm.triggerReconnect(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
return wm
|
return wm
|
||||||
}
|
}
|
||||||
|
|
||||||
func Restart(wm *BinanceWebSocketManager) {
|
/**
|
||||||
wm.reconnect <- struct{}{}
|
* 触发重连机制
|
||||||
|
* @param force 是否强制重连
|
||||||
|
*/
|
||||||
|
func (wm *BinanceWebSocketManager) triggerReconnect(force bool) {
|
||||||
|
if force {
|
||||||
|
wm.reconnecting.Store(false) // 强制重置标志位
|
||||||
|
}
|
||||||
|
|
||||||
|
if wm.reconnecting.CompareAndSwap(false, true) {
|
||||||
|
log.Warnf("准备重连 key: %s wsType: %v", wm.apiKey, wm.wsType)
|
||||||
|
// 发送信号触发重连协程
|
||||||
|
select {
|
||||||
|
case wm.reconnect <- struct{}{}:
|
||||||
|
default:
|
||||||
|
// 防止阻塞,如果通道满了就跳过
|
||||||
|
log.Debugf("reconnect 信号已存在,跳过 key:%s", wm.apiKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 外部重启函数
|
||||||
|
* @param wm WebSocket管理器实例
|
||||||
|
*/
|
||||||
|
func Restart(wm *BinanceWebSocketManager) {
|
||||||
|
log.Warnf("调用restart")
|
||||||
|
wm.triggerReconnect(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主运行循环
|
||||||
|
*/
|
||||||
func (wm *BinanceWebSocketManager) run() {
|
func (wm *BinanceWebSocketManager) run() {
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
wm.cancelFunc = cancel
|
wm.cancelFunc = cancel
|
||||||
|
|
||||||
utility.SafeGo(wm.handleSignal)
|
// utility.SafeGo(wm.handleSignal)
|
||||||
|
|
||||||
// 计算错误记录键
|
// 计算错误记录键
|
||||||
errKey := fmt.Sprintf(global.API_WEBSOCKET_ERR, wm.apiKey)
|
errKey := fmt.Sprintf(global.API_WEBSOCKET_ERR, wm.apiKey)
|
||||||
@ -137,7 +291,11 @@ func (wm *BinanceWebSocketManager) run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleConnectionError 处理 WebSocket 连接错误
|
/**
|
||||||
|
* 处理WebSocket连接错误
|
||||||
|
* @param errKey Redis错误记录键
|
||||||
|
* @param err 错误信息
|
||||||
|
*/
|
||||||
func (wm *BinanceWebSocketManager) handleConnectionError(errKey string, err error) {
|
func (wm *BinanceWebSocketManager) handleConnectionError(errKey string, err error) {
|
||||||
// 从 Redis 获取错误记录
|
// 从 Redis 获取错误记录
|
||||||
var errMessage commondto.WebSocketErr
|
var errMessage commondto.WebSocketErr
|
||||||
@ -160,7 +318,11 @@ func (wm *BinanceWebSocketManager) handleConnectionError(errKey string, err erro
|
|||||||
log.Error("连接 %s WebSocket 时出错: %v, 错误: %v", wm.wsType, wm.apiKey, err)
|
log.Error("连接 %s WebSocket 时出错: %v, 错误: %v", wm.wsType, wm.apiKey, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// isErrorCountExceeded 检查错误次数是否超过阈值
|
/**
|
||||||
|
* 检查错误次数是否超过阈值
|
||||||
|
* @param errKey Redis错误记录键
|
||||||
|
* @return 是否超过阈值
|
||||||
|
*/
|
||||||
func (wm *BinanceWebSocketManager) isErrorCountExceeded(errKey string) bool {
|
func (wm *BinanceWebSocketManager) isErrorCountExceeded(errKey string) bool {
|
||||||
val, _ := helper.DefaultRedis.GetString(errKey)
|
val, _ := helper.DefaultRedis.GetString(errKey)
|
||||||
if val == "" {
|
if val == "" {
|
||||||
@ -175,16 +337,13 @@ func (wm *BinanceWebSocketManager) isErrorCountExceeded(errKey string) bool {
|
|||||||
return errMessage.Count >= 5
|
return errMessage.Count >= 5
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理终止信号
|
/**
|
||||||
func (wm *BinanceWebSocketManager) handleSignal() {
|
* 建立WebSocket连接
|
||||||
ch := make(chan os.Signal)
|
* @param ctx 上下文
|
||||||
signal.Notify(ch, os.Interrupt)
|
* @return 错误信息
|
||||||
<-ch
|
*/
|
||||||
wm.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wm *BinanceWebSocketManager) connect(ctx context.Context) error {
|
func (wm *BinanceWebSocketManager) connect(ctx context.Context) error {
|
||||||
dialer, err := wm.getDialer()
|
dialer, err := proxy.GetDialer(wm.proxyType, wm.proxyAddress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -201,6 +360,9 @@ func (wm *BinanceWebSocketManager) connect(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 连接成功,更新连接时间和ping时间
|
||||||
|
wm.ConnectTime = time.Now()
|
||||||
|
wm.lastPingTime.Store(time.Now())
|
||||||
log.Info(fmt.Sprintf("已连接到 Binance %s WebSocket【%s】 key:%s", getWsTypeName(wm.wsType), wm.apiKey, listenKey))
|
log.Info(fmt.Sprintf("已连接到 Binance %s WebSocket【%s】 key:%s", getWsTypeName(wm.wsType), wm.apiKey, listenKey))
|
||||||
|
|
||||||
// Ping处理
|
// Ping处理
|
||||||
@ -210,22 +372,102 @@ func (wm *BinanceWebSocketManager) connect(ctx context.Context) error {
|
|||||||
for x := 0; x < 5; x++ {
|
for x := 0; x < 5; x++ {
|
||||||
if err := wm.ws.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(time.Second*10)); err != nil {
|
if err := wm.ws.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(time.Second*10)); err != nil {
|
||||||
log.Error("binance 回应pong失败 次数:", strconv.Itoa(x), " err:", err)
|
log.Error("binance 回应pong失败 次数:", strconv.Itoa(x), " err:", err)
|
||||||
|
|
||||||
time.Sleep(time.Second * 1)
|
time.Sleep(time.Second * 1)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新ping时间和Redis记录
|
||||||
|
wm.lastPingTime.Store(time.Now())
|
||||||
setLastTime(wm)
|
setLastTime(wm)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// utility.SafeGoParam(wm.restartConnect, ctx)
|
// 启动各种协程
|
||||||
utility.SafeGo(func() { wm.startListenKeyRenewal2(ctx) })
|
utility.SafeGo(func() { wm.startListenKeyRenewal2(ctx) })
|
||||||
utility.SafeGo(func() { wm.readMessages(ctx) })
|
utility.SafeGo(func() { wm.readMessages(ctx) })
|
||||||
utility.SafeGo(func() { wm.handleReconnect(ctx) })
|
utility.SafeGo(func() { wm.handleReconnect(ctx) })
|
||||||
|
utility.SafeGo(func() { wm.startPingLoop(ctx) })
|
||||||
|
utility.SafeGo(func() { wm.startDeadCheck(ctx) }) // 连接成功后立即启动假死检测
|
||||||
|
utility.SafeGo(func() { wm.start24HourReconnectTimer(ctx) }) // 启动24小时强制重连
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReplaceConnection 创建新连接并关闭旧连接,实现无缝连接替换
|
||||||
|
func (wm *BinanceWebSocketManager) ReplaceConnection() error {
|
||||||
|
wm.mu.Lock()
|
||||||
|
if wm.isStopped {
|
||||||
|
wm.mu.Unlock()
|
||||||
|
return errors.New("WebSocket 已停止")
|
||||||
|
}
|
||||||
|
oldCtxCancel := wm.cancelFunc
|
||||||
|
oldConn := wm.ws
|
||||||
|
wm.mu.Unlock()
|
||||||
|
|
||||||
|
log.Infof("🔄 正在替换连接: %s", wm.apiKey)
|
||||||
|
|
||||||
|
// 步骤 1:先获取新的 listenKey 和连接
|
||||||
|
newListenKey, err := wm.getListenKey()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("获取新 listenKey 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dialer, err := proxy.GetDialer(wm.proxyType, wm.proxyAddress)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newURL := fmt.Sprintf("%s/%s", wm.url, newListenKey)
|
||||||
|
newConn, _, err := dialer.Dial(newURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("新连接 Dial 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤 2:创建新的上下文并启动协程
|
||||||
|
newCtx, newCancel := context.WithCancel(context.Background())
|
||||||
|
|
||||||
|
// 设置 ping handler
|
||||||
|
newConn.SetPingHandler(func(appData string) error {
|
||||||
|
log.Infof("收到 Ping(新连接) key:%s msg:%s", wm.apiKey, appData)
|
||||||
|
for x := 0; x < 5; x++ {
|
||||||
|
if err := newConn.WriteControl(websocket.PongMessage, []byte(appData), time.Now().Add(10*time.Second)); err != nil {
|
||||||
|
log.Errorf("Pong 失败 %d 次 err:%v", x, err)
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
setLastTime(wm)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
// 步骤 3:安全切换连接
|
||||||
|
wm.mu.Lock()
|
||||||
|
wm.ws = newConn
|
||||||
|
wm.listenKey = newListenKey
|
||||||
|
wm.ConnectTime = time.Now()
|
||||||
|
wm.cancelFunc = newCancel
|
||||||
|
wm.mu.Unlock()
|
||||||
|
|
||||||
|
log.Infof("✅ 替换连接成功: %s listenKey: %s", wm.apiKey, newListenKey)
|
||||||
|
|
||||||
|
// 步骤 4:启动新连接协程
|
||||||
|
go wm.startListenKeyRenewal2(newCtx)
|
||||||
|
go wm.readMessages(newCtx)
|
||||||
|
go wm.handleReconnect(newCtx)
|
||||||
|
go wm.startPingLoop(newCtx)
|
||||||
|
// go wm.startDeadCheck(newCtx)
|
||||||
|
|
||||||
|
// 步骤 5:关闭旧连接、取消旧协程
|
||||||
|
if oldCtxCancel != nil {
|
||||||
|
oldCtxCancel()
|
||||||
|
}
|
||||||
|
if oldConn != nil {
|
||||||
|
_ = oldConn.Close()
|
||||||
|
log.Infof("🔒 旧连接已关闭: %s", wm.apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -251,54 +493,7 @@ func setLastTime(wm *BinanceWebSocketManager) {
|
|||||||
if val != "" {
|
if val != "" {
|
||||||
helper.DefaultRedis.SetString(subKey, val)
|
helper.DefaultRedis.SetString(subKey, val)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (wm *BinanceWebSocketManager) getDialer() (*websocket.Dialer, error) {
|
|
||||||
if wm.proxyAddress == "" {
|
|
||||||
return &websocket.Dialer{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasPrefix(wm.proxyAddress, "http://") && !strings.HasPrefix(wm.proxyAddress, "https://") && !strings.HasPrefix(wm.proxyAddress, "socks5://") {
|
|
||||||
wm.proxyAddress = wm.proxyType + "://" + wm.proxyAddress
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyURL, err := url.Parse(wm.proxyAddress)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse proxy URL: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
transport := &http.Transport{
|
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
|
|
||||||
}
|
|
||||||
|
|
||||||
switch proxyURL.Scheme {
|
|
||||||
case "socks5":
|
|
||||||
return wm.createSocks5Dialer(proxyURL)
|
|
||||||
case "http", "https":
|
|
||||||
transport.Proxy = http.ProxyURL(proxyURL)
|
|
||||||
return &websocket.Dialer{Proxy: transport.Proxy, TLSClientConfig: transport.TLSClientConfig}, nil
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported proxy scheme: %s", proxyURL.Scheme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wm *BinanceWebSocketManager) createSocks5Dialer(proxyURL *url.URL) (*websocket.Dialer, error) {
|
|
||||||
auth := &proxy.Auth{}
|
|
||||||
if proxyURL.User != nil {
|
|
||||||
auth.User = proxyURL.User.Username()
|
|
||||||
auth.Password, _ = proxyURL.User.Password()
|
|
||||||
}
|
|
||||||
|
|
||||||
socksDialer, err := proxy.SOCKS5("tcp", proxyURL.Host, auth, proxy.Direct)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create SOCKS5 proxy dialer: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &websocket.Dialer{
|
|
||||||
NetDialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
|
||||||
return socksDialer.Dial(network, address)
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复用创建HTTP客户端的逻辑
|
// 复用创建HTTP客户端的逻辑
|
||||||
@ -343,7 +538,6 @@ func (wm *BinanceWebSocketManager) getListenKey() (string, error) {
|
|||||||
|
|
||||||
// 接收消息
|
// 接收消息
|
||||||
func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) {
|
func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) {
|
||||||
defer wm.ws.Close()
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@ -355,19 +549,23 @@ func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_, msg, err := wm.ws.ReadMessage()
|
_, msg, err := wm.ws.ReadMessage()
|
||||||
if err != nil && strings.Contains(err.Error(), "websocket: close") {
|
if err != nil {
|
||||||
if !wm.isStopped {
|
// 检查是否是由于我们主动关闭连接导致的错误
|
||||||
wm.reconnect <- struct{}{}
|
if wm.isStopped {
|
||||||
|
log.Infof("WebSocket read loop gracefully stopped for key: %s", wm.apiKey)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Error("websocket 关闭")
|
// 如果不是主动关闭,再判断是否是连接关闭错误,并触发重连
|
||||||
return
|
if strings.Contains(err.Error(), "websocket: close") {
|
||||||
} else if err != nil {
|
log.Error("WebSocket connection closed unexpectedly, triggering reconnect for key: %s. Error: %v", wm.apiKey, err)
|
||||||
log.Error("读取消息时出错: %v", err)
|
wm.triggerReconnect(false)
|
||||||
return
|
} else {
|
||||||
|
log.Error("Error reading message for key: %s. Error: %v", wm.apiKey, err)
|
||||||
|
}
|
||||||
|
return // 任何错误发生后都应退出读取循环
|
||||||
}
|
}
|
||||||
wm.handleOrderUpdate(msg)
|
wm.handleOrderUpdate(msg)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -375,130 +573,73 @@ func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) {
|
|||||||
func (wm *BinanceWebSocketManager) handleOrderUpdate(msg []byte) {
|
func (wm *BinanceWebSocketManager) handleOrderUpdate(msg []byte) {
|
||||||
setLastTime(wm)
|
setLastTime(wm)
|
||||||
|
|
||||||
if reconnect, _ := ReceiveListen(msg, wm.wsType); reconnect {
|
if reconnect, fatal, err := ReceiveListen(msg, wm.wsType, wm.apiKey); err != nil {
|
||||||
wm.reconnect <- struct{}{}
|
log.Errorf("处理消息时出错: %v", err)
|
||||||
|
// 根据错误类型决定是否停止
|
||||||
|
if fatal {
|
||||||
|
log.Errorf("收到致命错误,将停止 WebSocket 管理器 for key: %s", wm.apiKey)
|
||||||
|
wm.Stop()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
} else if fatal {
|
||||||
|
log.Errorf("收到致命错误,将停止 WebSocket 管理器 for key: %s", wm.apiKey)
|
||||||
|
wm.Stop()
|
||||||
|
return
|
||||||
|
} else if reconnect {
|
||||||
|
log.Warnf("收到重连请求,将触发重连 for key: %s", wm.apiKey)
|
||||||
|
wm.triggerReconnect(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wm *BinanceWebSocketManager) Stop() {
|
// closeConn 安全地关闭WebSocket连接
|
||||||
|
func (wm *BinanceWebSocketManager) closeConn() {
|
||||||
wm.mu.Lock()
|
wm.mu.Lock()
|
||||||
defer wm.mu.Unlock()
|
defer wm.mu.Unlock()
|
||||||
|
|
||||||
if wm.isStopped {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
wm.isStopped = true
|
|
||||||
// 关闭 stopChannel(确保已经关闭,避免 panic)
|
|
||||||
select {
|
|
||||||
case <-wm.stopChannel:
|
|
||||||
default:
|
|
||||||
close(wm.stopChannel)
|
|
||||||
}
|
|
||||||
|
|
||||||
if wm.cancelFunc != nil {
|
|
||||||
wm.cancelFunc()
|
|
||||||
}
|
|
||||||
|
|
||||||
if wm.ws != nil {
|
if wm.ws != nil {
|
||||||
if err := wm.ws.Close(); err != nil {
|
_ = wm.ws.Close()
|
||||||
log.Error(fmt.Sprintf("key【%s】close失败", wm.apiKey), err)
|
wm.ws = nil
|
||||||
} else {
|
|
||||||
log.Info(fmt.Sprintf("key【%s】close", wm.apiKey))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// **重新创建 stopChannel,避免 Restart() 时无效**
|
|
||||||
wm.stopChannel = make(chan struct{})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重连机制
|
// 主动心跳发送机制
|
||||||
func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) {
|
func (wm *BinanceWebSocketManager) startPingLoop(ctx context.Context) {
|
||||||
maxRetries := 5 // 最大重试次数
|
ticker := time.NewTicker(1 * time.Minute)
|
||||||
retryCount := 0
|
defer ticker.Stop()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
case <-wm.reconnect:
|
case <-ticker.C:
|
||||||
if wm.isStopped {
|
if wm.isStopped {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if err := wm.sendPing(); err != nil {
|
||||||
log.Warn("WebSocket 连接断开,尝试重连...")
|
log.Error("主动 Ping Binance 失败:", err)
|
||||||
|
wm.triggerReconnect(false)
|
||||||
if wm.ws != nil {
|
return // 退出循环,等待重连
|
||||||
wm.ws.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 取消旧的上下文
|
|
||||||
if wm.cancelFunc != nil {
|
|
||||||
wm.cancelFunc()
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
|
||||||
newCtx, cancel := context.WithCancel(context.Background())
|
|
||||||
wm.cancelFunc = cancel // 更新 cancelFunc
|
|
||||||
|
|
||||||
if err := wm.connect(newCtx); err != nil {
|
|
||||||
log.Errorf("重连失败: %v", err)
|
|
||||||
cancel()
|
|
||||||
retryCount++
|
|
||||||
|
|
||||||
if retryCount >= maxRetries {
|
|
||||||
log.Error("重连失败次数过多,退出重连逻辑")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(5 * time.Second)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定期删除listenkey 并重启ws
|
// 定期删除listenkey 并重启ws
|
||||||
func (wm *BinanceWebSocketManager) startListenKeyRenewal(ctx context.Context, listenKey string) {
|
// func (wm *BinanceWebSocketManager) startListenKeyRenewal(ctx context.Context, listenKey string) {
|
||||||
time.Sleep(30 * time.Minute)
|
// time.Sleep(30 * time.Minute)
|
||||||
|
|
||||||
select {
|
// select {
|
||||||
case <-ctx.Done():
|
// case <-ctx.Done():
|
||||||
return
|
// return
|
||||||
default:
|
// default:
|
||||||
if err := wm.deleteListenKey(listenKey); err != nil {
|
// if err := wm.deleteListenKey(listenKey); err != nil {
|
||||||
log.Error("Failed to renew listenKey: ,type:%v key: %s", wm.wsType, wm.apiKey, err)
|
// log.Error("Failed to renew listenKey: ,type:%v key: %s", wm.wsType, wm.apiKey, err)
|
||||||
} else {
|
// } else {
|
||||||
log.Debug("Successfully delete listenKey")
|
// log.Debug("Successfully delete listenKey")
|
||||||
wm.reconnect <- struct{}{}
|
// wm.triggerReconnect()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// ticker := time.NewTicker(5 * time.Minute)
|
// }
|
||||||
// defer ticker.Stop()
|
|
||||||
|
|
||||||
// for {
|
|
||||||
// select {
|
|
||||||
// case <-ticker.C:
|
|
||||||
// if wm.isStopped {
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if err := wm.deleteListenKey(listenKey); err != nil {
|
|
||||||
// log.Error("Failed to renew listenKey: ,type:%v key: %s", wm.wsType, wm.apiKey, err)
|
|
||||||
// } else {
|
|
||||||
// log.Debug("Successfully delete listenKey")
|
|
||||||
// wm.reconnect <- struct{}{}
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// case <-ctx.Done():
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定时续期
|
// 定时续期
|
||||||
func (wm *BinanceWebSocketManager) startListenKeyRenewal2(ctx context.Context) {
|
func (wm *BinanceWebSocketManager) startListenKeyRenewal2(ctx context.Context) {
|
||||||
@ -527,49 +668,34 @@ func (wm *BinanceWebSocketManager) startListenKeyRenewal2(ctx context.Context) {
|
|||||||
/*
|
/*
|
||||||
删除listenkey
|
删除listenkey
|
||||||
*/
|
*/
|
||||||
func (wm *BinanceWebSocketManager) deleteListenKey(listenKey string) error {
|
// func (wm *BinanceWebSocketManager) deleteListenKey(listenKey string) error {
|
||||||
client, err := wm.createBinanceClient()
|
// client, err := wm.createBinanceClient()
|
||||||
if err != nil {
|
// if err != nil {
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
|
||||||
var resp []byte
|
// var resp []byte
|
||||||
|
|
||||||
switch wm.wsType {
|
// switch wm.wsType {
|
||||||
case 0:
|
// case 0:
|
||||||
path := fmt.Sprintf("/api/v3/userDataStream")
|
// path := fmt.Sprintf("/api/v3/userDataStream")
|
||||||
params := map[string]interface{}{
|
// params := map[string]interface{}{
|
||||||
"listenKey": listenKey,
|
// "listenKey": listenKey,
|
||||||
}
|
// }
|
||||||
resp, _, err = client.SendSpotRequestByKey(path, "DELETE", params)
|
// resp, _, err = client.SendSpotRequestByKey(path, "DELETE", params)
|
||||||
|
|
||||||
log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp)))
|
// log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp)))
|
||||||
case 1:
|
// case 1:
|
||||||
resp, _, err = client.SendFuturesRequestByKey("/fapi/v1/listenKey", "DELETE", nil)
|
// resp, _, err = client.SendFuturesRequestByKey("/fapi/v1/listenKey", "DELETE", nil)
|
||||||
log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp)))
|
// log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp)))
|
||||||
default:
|
// default:
|
||||||
return errors.New("unknown ws type")
|
// return errors.New("unknown ws type")
|
||||||
}
|
// }
|
||||||
|
|
||||||
return err
|
// return err
|
||||||
}
|
// }
|
||||||
|
|
||||||
func (wm *BinanceWebSocketManager) renewListenKey(listenKey string) error {
|
func (wm *BinanceWebSocketManager) renewListenKey(listenKey string) error {
|
||||||
// payloadParam := map[string]interface{}{
|
|
||||||
// "listenKey": listenKey,
|
|
||||||
// "apiKey": wm.apiKey,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// params := map[string]interface{}{
|
|
||||||
// "id": getUUID(),
|
|
||||||
// "method": "userDataStream.ping",
|
|
||||||
// "params": payloadParam,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if err := wm.ws.WriteJSON(params); err != nil {
|
|
||||||
// return err
|
|
||||||
// }
|
|
||||||
// wm.ws.WriteJSON()
|
|
||||||
client, err := wm.createBinanceClient()
|
client, err := wm.createBinanceClient()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -615,3 +741,112 @@ func randomHex(n int) string {
|
|||||||
}
|
}
|
||||||
return string(bytes)
|
return string(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动24小时强制重连定时器
|
||||||
|
* @param ctx 上下文
|
||||||
|
*/
|
||||||
|
func (wm *BinanceWebSocketManager) start24HourReconnectTimer(ctx context.Context) {
|
||||||
|
// Binance要求不到24小时就需要主动断开重连
|
||||||
|
// 设置为23小时50分钟,留出一些缓冲时间
|
||||||
|
duration := 23*time.Hour + 50*time.Minute
|
||||||
|
|
||||||
|
wm.forceReconnectTimer = time.NewTimer(duration)
|
||||||
|
defer func() {
|
||||||
|
if wm.forceReconnectTimer != nil {
|
||||||
|
wm.forceReconnectTimer.Stop()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-wm.forceReconnectTimer.C:
|
||||||
|
if !wm.isStopped {
|
||||||
|
log.Warnf("24小时强制重连触发 key:%s wsType:%v", wm.apiKey, wm.wsType)
|
||||||
|
wm.triggerReconnect(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 改进的假死检测机制
|
||||||
|
* @param ctx 上下文
|
||||||
|
*/
|
||||||
|
func (wm *BinanceWebSocketManager) startDeadCheck(ctx context.Context) {
|
||||||
|
// 缩短检测间隔,提高响应速度
|
||||||
|
ticker := time.NewTicker(30 * time.Second)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if wm.isStopped {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
wm.DeadCheck()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 改进的假死检测逻辑
|
||||||
|
*/
|
||||||
|
func (wm *BinanceWebSocketManager) DeadCheck() {
|
||||||
|
// 检查最后ping时间
|
||||||
|
lastPing := wm.lastPingTime.Load().(time.Time)
|
||||||
|
|
||||||
|
// 定义最大静默时间(根据WebSocket类型调整)
|
||||||
|
var timeout time.Duration
|
||||||
|
if wm.wsType == 0 {
|
||||||
|
timeout = 3 * time.Minute // 现货:3分钟无ping视为假死
|
||||||
|
} else {
|
||||||
|
timeout = 6 * time.Minute // 合约:6分钟无ping视为假死
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Since(lastPing) > timeout {
|
||||||
|
log.Warnf("检测到假死连接(基于ping时间) key:%s type:%v, 距离上次ping: %v, 触发重连",
|
||||||
|
wm.apiKey, wm.wsType, time.Since(lastPing))
|
||||||
|
wm.triggerReconnect(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同时检查Redis中的记录作为备用检测
|
||||||
|
subKey := fmt.Sprintf(global.USER_SUBSCRIBE, wm.apiKey)
|
||||||
|
val, _ := helper.DefaultRedis.GetString(subKey)
|
||||||
|
if val == "" {
|
||||||
|
log.Debugf("没有订阅信息,跳过Redis假死检测")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data binancedto.UserSubscribeState
|
||||||
|
_ = sonic.Unmarshal([]byte(val), &data)
|
||||||
|
|
||||||
|
var lastTime *time.Time
|
||||||
|
if wm.wsType == 0 {
|
||||||
|
lastTime = data.SpotLastTime
|
||||||
|
} else {
|
||||||
|
lastTime = data.FuturesLastTime
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redis记录的超时时间设置得更长一些
|
||||||
|
var redisTimeout time.Duration
|
||||||
|
if wm.wsType == 0 {
|
||||||
|
redisTimeout = 3 * time.Minute
|
||||||
|
} else {
|
||||||
|
redisTimeout = 6 * time.Minute
|
||||||
|
}
|
||||||
|
|
||||||
|
if lastTime != nil && time.Since(*lastTime) > redisTimeout {
|
||||||
|
log.Warnf("检测到假死连接(基于Redis记录) key:%s type:%v, 距离上次通信: %v, 触发重连",
|
||||||
|
wm.apiKey, wm.wsType, time.Since(*lastTime))
|
||||||
|
wm.triggerReconnect(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 优化的重连处理逻辑
|
||||||
|
* @param ctx 上下文
|
||||||
|
*/
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go-admin/models"
|
"go-admin/models"
|
||||||
@ -19,6 +20,7 @@ type BinanceWs struct {
|
|||||||
proxyUrl string
|
proxyUrl string
|
||||||
WorkType string
|
WorkType string
|
||||||
wsConns []*WsConn
|
wsConns []*WsConn
|
||||||
|
mu sync.Mutex // 新增互斥锁
|
||||||
tickerCallback func(models.Ticker24, string, string)
|
tickerCallback func(models.Ticker24, string, string)
|
||||||
forceCallback func(models.ForceOrder, string, string)
|
forceCallback func(models.ForceOrder, string, string)
|
||||||
depthCallback func(models.DepthBin, string, string)
|
depthCallback func(models.DepthBin, string, string)
|
||||||
@ -70,11 +72,15 @@ func (bnWs *BinanceWs) subscribe(endpoint string, handle func(msg []byte) error)
|
|||||||
if wsConn == nil {
|
if wsConn == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
bnWs.mu.Lock()
|
||||||
bnWs.wsConns = append(bnWs.wsConns, wsConn)
|
bnWs.wsConns = append(bnWs.wsConns, wsConn)
|
||||||
|
bnWs.mu.Unlock()
|
||||||
go bnWs.exitHandler(wsConn)
|
go bnWs.exitHandler(wsConn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (bnWs *BinanceWs) Close() {
|
func (bnWs *BinanceWs) Close() {
|
||||||
|
bnWs.mu.Lock()
|
||||||
|
defer bnWs.mu.Unlock()
|
||||||
for _, con := range bnWs.wsConns {
|
for _, con := range bnWs.wsConns {
|
||||||
con.CloseWs()
|
con.CloseWs()
|
||||||
}
|
}
|
||||||
|
|||||||
218
services/excservice/bitget_receive.go
Normal file
218
services/excservice/bitget_receive.go
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
package excservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go-admin/pkg/utility"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
log "github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Bitget用户订单订阅处理
|
||||||
|
- @msg 消息内容
|
||||||
|
- @listenType 订阅类型 0-现货 1-合约
|
||||||
|
*/
|
||||||
|
func ReceiveBitgetListen(msg []byte, listenType int, apiKey string) (reconnect bool, err error) {
|
||||||
|
var dataMap map[string]interface{}
|
||||||
|
err = sonic.Unmarshal(msg, &dataMap)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Error("接收Bitget ws 反序列化失败:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是操作响应
|
||||||
|
if op, exists := dataMap["op"]; exists {
|
||||||
|
switch op {
|
||||||
|
case "login":
|
||||||
|
return handleBitgetLogin(dataMap, apiKey)
|
||||||
|
case "subscribe":
|
||||||
|
return handleBitgetSubscribe(dataMap, apiKey)
|
||||||
|
case "unsubscribe":
|
||||||
|
return handleBitgetUnsubscribe(dataMap, apiKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是数据推送
|
||||||
|
if arg, exists := dataMap["arg"]; exists {
|
||||||
|
return handleBitgetDataPush(dataMap, arg, listenType, apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("未知Bitget事件 内容:", string(msg))
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleBitgetLogin 处理登录响应
|
||||||
|
func handleBitgetLogin(dataMap map[string]interface{}, apiKey string) (bool, error) {
|
||||||
|
if code, exists := dataMap["code"]; exists {
|
||||||
|
if code == "0" || code == 0 {
|
||||||
|
log.Info("Bitget WebSocket登录成功 key:%s", apiKey)
|
||||||
|
} else {
|
||||||
|
log.Error("Bitget WebSocket登录失败 key:%s code:%v", apiKey, code)
|
||||||
|
return true, nil // 需要重连
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleBitgetSubscribe 处理订阅响应
|
||||||
|
func handleBitgetSubscribe(dataMap map[string]interface{}, apiKey string) (bool, error) {
|
||||||
|
if code, exists := dataMap["code"]; exists {
|
||||||
|
if code == "0" || code == 0 {
|
||||||
|
log.Info("Bitget WebSocket订阅成功 key:%s", apiKey)
|
||||||
|
} else {
|
||||||
|
log.Error("Bitget WebSocket订阅失败 key:%s code:%v", apiKey, code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleBitgetUnsubscribe 处理取消订阅响应
|
||||||
|
func handleBitgetUnsubscribe(dataMap map[string]interface{}, apiKey string) (bool, error) {
|
||||||
|
if code, exists := dataMap["code"]; exists {
|
||||||
|
if code == "0" || code == 0 {
|
||||||
|
log.Info("Bitget WebSocket取消订阅成功 key:%s", apiKey)
|
||||||
|
} else {
|
||||||
|
log.Error("Bitget WebSocket取消订阅失败 key:%s code:%v", apiKey, code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleBitgetDataPush 处理数据推送
|
||||||
|
func handleBitgetDataPush(dataMap map[string]interface{}, arg interface{}, listenType int, apiKey string) (bool, error) {
|
||||||
|
argMap, ok := arg.(map[string]interface{})
|
||||||
|
if !ok {
|
||||||
|
log.Error("Bitget数据推送参数格式错误 key:%s", apiKey)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
channel, exists := argMap["channel"]
|
||||||
|
if !exists {
|
||||||
|
log.Error("Bitget数据推送缺少channel参数 key:%s", apiKey)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch channel {
|
||||||
|
case "account":
|
||||||
|
return handleBitgetAccount(dataMap, listenType, apiKey)
|
||||||
|
case "orders":
|
||||||
|
return handleBitgetOrders(dataMap, listenType, apiKey)
|
||||||
|
case "positions":
|
||||||
|
return handleBitgetPositions(dataMap, listenType, apiKey)
|
||||||
|
case "balance":
|
||||||
|
return handleBitgetBalance(dataMap, listenType, apiKey)
|
||||||
|
default:
|
||||||
|
log.Info("收到Bitget未处理的频道数据 channel:%v key:%s", channel, apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleBitgetAccount 处理账户更新
|
||||||
|
func handleBitgetAccount(dataMap map[string]interface{}, listenType int, apiKey string) (bool, error) {
|
||||||
|
log.Info("收到Bitget账户更新 listenType:%d key:%s", listenType, apiKey)
|
||||||
|
|
||||||
|
data, exists := dataMap["data"]
|
||||||
|
if !exists {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这里可以根据需要实现具体的账户更新处理逻辑
|
||||||
|
// 类似于binanceservice.ChangeSpotOrder或binanceservice.ChangeFutureOrder
|
||||||
|
|
||||||
|
utility.SafeGo(func() {
|
||||||
|
handleBitgetAccountUpdate(data, listenType, apiKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleBitgetOrders 处理订单更新
|
||||||
|
func handleBitgetOrders(dataMap map[string]interface{}, listenType int, apiKey string) (bool, error) {
|
||||||
|
log.Info("收到Bitget订单更新 listenType:%d key:%s", listenType, apiKey)
|
||||||
|
|
||||||
|
data, exists := dataMap["data"]
|
||||||
|
if !exists {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
utility.SafeGo(func() {
|
||||||
|
handleBitgetOrderUpdate(data, listenType, apiKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleBitgetPositions 处理持仓更新
|
||||||
|
func handleBitgetPositions(dataMap map[string]interface{}, listenType int, apiKey string) (bool, error) {
|
||||||
|
log.Info("收到Bitget持仓更新 listenType:%d key:%s", listenType, apiKey)
|
||||||
|
|
||||||
|
data, exists := dataMap["data"]
|
||||||
|
if !exists {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
utility.SafeGo(func() {
|
||||||
|
handleBitgetPositionUpdate(data, listenType, apiKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleBitgetBalance 处理余额更新
|
||||||
|
func handleBitgetBalance(dataMap map[string]interface{}, listenType int, apiKey string) (bool, error) {
|
||||||
|
log.Info("收到Bitget余额更新 listenType:%d key:%s", listenType, apiKey)
|
||||||
|
|
||||||
|
data, exists := dataMap["data"]
|
||||||
|
if !exists {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
utility.SafeGo(func() {
|
||||||
|
handleBitgetBalanceUpdate(data, listenType, apiKey)
|
||||||
|
})
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 具体的处理函数,可以根据业务需求实现
|
||||||
|
|
||||||
|
// handleBitgetAccountUpdate 处理账户更新的具体逻辑
|
||||||
|
func handleBitgetAccountUpdate(data interface{}, listenType int, apiKey string) {
|
||||||
|
// TODO: 实现具体的账户更新处理逻辑
|
||||||
|
log.Info("处理Bitget账户更新 listenType:%d key:%s data:%v", listenType, apiKey, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleBitgetOrderUpdate 处理订单更新的具体逻辑
|
||||||
|
func handleBitgetOrderUpdate(data interface{}, listenType int, apiKey string) {
|
||||||
|
// TODO: 实现具体的订单更新处理逻辑
|
||||||
|
log.Info("处理Bitget订单更新 listenType:%d key:%s data:%v", listenType, apiKey, data)
|
||||||
|
|
||||||
|
// 这里可以参考binanceservice中的订单处理逻辑
|
||||||
|
// 例如:更新数据库中的订单状态、触发回调等
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleBitgetPositionUpdate 处理持仓更新的具体逻辑
|
||||||
|
func handleBitgetPositionUpdate(data interface{}, listenType int, apiKey string) {
|
||||||
|
// TODO: 实现具体的持仓更新处理逻辑
|
||||||
|
log.Info("处理Bitget持仓更新 listenType:%d key:%s data:%v", listenType, apiKey, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleBitgetBalanceUpdate 处理余额更新的具体逻辑
|
||||||
|
func handleBitgetBalanceUpdate(data interface{}, listenType int, apiKey string) {
|
||||||
|
// TODO: 实现具体的余额更新处理逻辑
|
||||||
|
log.Info("处理Bitget余额更新 listenType:%d key:%s data:%v", listenType, apiKey, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getBitgetWsTypeName 获取WebSocket类型名称
|
||||||
|
func getBitgetWsTypeName(listenType int) string {
|
||||||
|
switch listenType {
|
||||||
|
case 0:
|
||||||
|
return "现货"
|
||||||
|
case 1:
|
||||||
|
return "合约"
|
||||||
|
default:
|
||||||
|
return "未知"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"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"
|
"go-admin/config"
|
||||||
@ -15,6 +16,7 @@ import (
|
|||||||
"go-admin/models"
|
"go-admin/models"
|
||||||
|
|
||||||
log "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/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@ -133,7 +135,19 @@ func handleTickerAllMessage(msg []byte) {
|
|||||||
// log.Debug(symbol, "ticker@all ws处理失败", err)
|
// log.Debug(symbol, "ticker@all ws处理失败", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tradeSet.LastPrice = utility.StringFloat64Cut(data["c"].(string), int32(tradeSet.PriceDigit))
|
var utcTime int64
|
||||||
|
var lastPrice decimal.Decimal
|
||||||
|
lastPriceKey := fmt.Sprintf(rediskey.FutureTickerLastPrice, global.EXCHANGE_BINANCE, symbol)
|
||||||
|
|
||||||
|
if e, ok := data["E"]; ok {
|
||||||
|
utcTime = utility.ToInt64(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, ok := data["c"]; ok {
|
||||||
|
lastPrice = utility.StrToDecimal(e.(string)).Truncate(int32(tradeSet.PriceDigit))
|
||||||
|
}
|
||||||
|
|
||||||
|
tradeSet.LastPrice = lastPrice.String()
|
||||||
tradeSet.OpenPrice = utility.StrToFloatCut(data["o"].(string), int32(tradeSet.PriceDigit))
|
tradeSet.OpenPrice = utility.StrToFloatCut(data["o"].(string), int32(tradeSet.PriceDigit))
|
||||||
tradeSet.HighPrice = utility.StringFloat64Cut(data["h"].(string), int32(tradeSet.PriceDigit))
|
tradeSet.HighPrice = utility.StringFloat64Cut(data["h"].(string), int32(tradeSet.PriceDigit))
|
||||||
tradeSet.LowPrice = utility.StringFloat64Cut(data["l"].(string), int32(tradeSet.PriceDigit))
|
tradeSet.LowPrice = utility.StringFloat64Cut(data["l"].(string), int32(tradeSet.PriceDigit))
|
||||||
@ -147,6 +161,16 @@ func handleTickerAllMessage(msg []byte) {
|
|||||||
log.Error(symbol, "ticker@all ws处理失败", err)
|
log.Error(symbol, "ticker@all ws处理失败", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//行情存储时间
|
||||||
|
lastUtc := utcTime - 1000*60*60
|
||||||
|
if _, err := helper.DefaultRedis.RemoveBeforeScore(lastPriceKey, float64(lastUtc)); err != nil {
|
||||||
|
log.Errorf("移除 合约交易对:%s %d之前的最新成交价失败,err:%v", symbol, lastUtc, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := helper.DefaultRedis.AddSortSet(lastPriceKey, float64(utcTime), lastPrice.String()); err != nil {
|
||||||
|
log.Errorf("添加 合约交易对:%s %d之前的最新成交价失败,err:%v", symbol, lastUtc, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(trades) > 0 {
|
if len(trades) > 0 {
|
||||||
|
|||||||
108
services/orderservice/order_service.go
Normal file
108
services/orderservice/order_service.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package orderservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go-admin/app/admin/models"
|
||||||
|
"go-admin/app/admin/service/dto"
|
||||||
|
"go-admin/pkg/utility"
|
||||||
|
"go-admin/pkg/utility/snowflakehelper"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk/service"
|
||||||
|
"github.com/jinzhu/copier"
|
||||||
|
"github.com/shopspring/decimal"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrderService struct {
|
||||||
|
service.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建减仓后减仓单
|
||||||
|
func (e *OrderService) 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 触发主单失败
|
||||||
|
func (e *OrderService) ErrorTrigger(preOrder *dto.PreOrderRedisList, errData string) error {
|
||||||
|
// var cacheOrderKey string
|
||||||
|
|
||||||
|
// if preOrder.OrderType == 1 {
|
||||||
|
// cacheOrderKey = fmt.Sprintf(rediskey.PreSpotOrderList, preOrder.ExchangeType)
|
||||||
|
// } else {
|
||||||
|
// cacheOrderKey = fmt.Sprintf(rediskey.PreFutOrderList, preOrder.ExchangeType)
|
||||||
|
// }
|
||||||
|
|
||||||
|
if err := e.Orm.Model(&models.LinePreOrder{}).
|
||||||
|
Where("id = ? and status =1", preOrder.Id).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"status": 2,
|
||||||
|
"desc": errData,
|
||||||
|
"trigger_time": time.Now(),
|
||||||
|
}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
63
services/proxy/proxy.go
Normal file
63
services/proxy/proxy.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package proxy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"golang.org/x/net/proxy"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetDialer 获取代理拨号器
|
||||||
|
func GetDialer(proxyType, proxyAddress string) (*websocket.Dialer, error) {
|
||||||
|
if proxyAddress == "" {
|
||||||
|
return &websocket.Dialer{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(proxyAddress, "http://") && !strings.HasPrefix(proxyAddress, "https://") && !strings.HasPrefix(proxyAddress, "socks5://") {
|
||||||
|
proxyAddress = proxyType + "://" + proxyAddress
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyURL, err := url.Parse(proxyAddress)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse proxy URL: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
transport := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
switch proxyURL.Scheme {
|
||||||
|
case "socks5":
|
||||||
|
return createSocks5Dialer(proxyURL)
|
||||||
|
case "http", "https":
|
||||||
|
transport.Proxy = http.ProxyURL(proxyURL)
|
||||||
|
return &websocket.Dialer{Proxy: transport.Proxy, TLSClientConfig: transport.TLSClientConfig}, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported proxy scheme: %s", proxyURL.Scheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func createSocks5Dialer(proxyURL *url.URL) (*websocket.Dialer, error) {
|
||||||
|
auth := &proxy.Auth{}
|
||||||
|
if proxyURL.User != nil {
|
||||||
|
auth.User = proxyURL.User.Username()
|
||||||
|
auth.Password, _ = proxyURL.User.Password()
|
||||||
|
}
|
||||||
|
|
||||||
|
socksDialer, err := proxy.SOCKS5("tcp", proxyURL.Host, auth, proxy.Direct)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create SOCKS5 proxy dialer: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &websocket.Dialer{
|
||||||
|
NetDialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
return socksDialer.Dial(network, address)
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@ -182,14 +182,7 @@ func handleTickerMessage(msg []byte) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pairs := make([]map[string]interface{}, 0)
|
|
||||||
trades := make([]models.TradeSet, 0)
|
trades := make([]models.TradeSet, 0)
|
||||||
pairsVal, _ := helper.DefaultRedis.GetString(rediskey.SpotSymbolTicker)
|
|
||||||
|
|
||||||
if err := sonic.UnmarshalString(pairsVal, &pairs); err != nil {
|
|
||||||
log.Error("解码ticker消息失败", zap.Error(err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, dataMap := range streamResp.Data {
|
for _, dataMap := range streamResp.Data {
|
||||||
symbolName, ok := dataMap["s"].(string)
|
symbolName, ok := dataMap["s"].(string)
|
||||||
@ -204,6 +197,7 @@ func handleTickerMessage(msg []byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
key := fmt.Sprintf(global.TICKER_SPOT, global.EXCHANGE_BINANCE, symbolName)
|
key := fmt.Sprintf(global.TICKER_SPOT, global.EXCHANGE_BINANCE, symbolName)
|
||||||
|
lastPriceKey := fmt.Sprintf(rediskey.SpotTickerLastPrice, global.EXCHANGE_BINANCE, symbolName)
|
||||||
tcVal, _ := helper.DefaultRedis.GetString(key)
|
tcVal, _ := helper.DefaultRedis.GetString(key)
|
||||||
tradeSet := models.TradeSet{}
|
tradeSet := models.TradeSet{}
|
||||||
if err := sonic.UnmarshalString(tcVal, &tradeSet); err != nil {
|
if err := sonic.UnmarshalString(tcVal, &tradeSet); err != nil {
|
||||||
@ -212,22 +206,25 @@ func handleTickerMessage(msg []byte) {
|
|||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tradeSet.LastPrice = utility.StringFloat64Cut(dataMap["c"].(string), int32(tradeSet.PriceDigit))
|
|
||||||
|
var utcTime int64
|
||||||
|
var lastPrice decimal.Decimal
|
||||||
|
|
||||||
|
if e, ok := dataMap["E"]; ok {
|
||||||
|
utcTime = utility.ToInt64(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if e, ok := dataMap["c"]; ok {
|
||||||
|
lastPrice = utility.StrToDecimal(e.(string)).Truncate(int32(tradeSet.PriceDigit))
|
||||||
|
}
|
||||||
|
|
||||||
|
tradeSet.LastPrice = lastPrice.String()
|
||||||
tradeSet.OpenPrice = utility.StrToFloatCut(dataMap["o"].(string), int32(tradeSet.PriceDigit))
|
tradeSet.OpenPrice = utility.StrToFloatCut(dataMap["o"].(string), int32(tradeSet.PriceDigit))
|
||||||
tradeSet.HighPrice = utility.StringFloat64Cut(dataMap["h"].(string), int32(tradeSet.PriceDigit))
|
tradeSet.HighPrice = utility.StringFloat64Cut(dataMap["h"].(string), int32(tradeSet.PriceDigit))
|
||||||
tradeSet.LowPrice = utility.StringFloat64Cut(dataMap["l"].(string), int32(tradeSet.PriceDigit))
|
tradeSet.LowPrice = utility.StringFloat64Cut(dataMap["l"].(string), int32(tradeSet.PriceDigit))
|
||||||
tradeSet.Volume = utility.StringFloat64Cut(dataMap["v"].(string), int32(tradeSet.AmountDigit))
|
tradeSet.Volume = utility.StringFloat64Cut(dataMap["v"].(string), int32(tradeSet.AmountDigit))
|
||||||
tradeSet.QuoteVolume = utility.StringFloat64Cut(dataMap["q"].(string), 5)
|
tradeSet.QuoteVolume = utility.StringFloat64Cut(dataMap["q"].(string), 5)
|
||||||
|
|
||||||
for index := range pairs {
|
|
||||||
if symbol, ok := pairs[index]["symbol"].(string); ok {
|
|
||||||
if symbol == symbolName {
|
|
||||||
pairs[index]["price"] = tradeSet.LastPrice
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trades = append(trades, tradeSet)
|
trades = append(trades, tradeSet)
|
||||||
tcVal, _ = sonic.MarshalString(&tradeSet)
|
tcVal, _ = sonic.MarshalString(&tradeSet)
|
||||||
|
|
||||||
@ -236,6 +233,17 @@ func handleTickerMessage(msg []byte) {
|
|||||||
log.Error("redis保存交易对失败", tradeSet.Coin, tradeSet.Currency)
|
log.Error("redis保存交易对失败", tradeSet.Coin, tradeSet.Currency)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//行情存储时间
|
||||||
|
lastUtc := utcTime - 1000*60*60
|
||||||
|
|
||||||
|
if _, err := helper.DefaultRedis.RemoveBeforeScore(lastPriceKey, float64(lastUtc)); err != nil {
|
||||||
|
log.Errorf("移除 现货交易对:%s %d之前的最新成交价失败,err:%v", symbolName, lastUtc, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := helper.DefaultRedis.AddSortSet(lastPriceKey, float64(utcTime), lastPrice.String()); err != nil {
|
||||||
|
log.Errorf("添加 现货交易对:%s %d之前的最新成交价失败,err:%v", symbolName, lastUtc, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//判断触发现货下单
|
//判断触发现货下单
|
||||||
|
|||||||
68
services/tradsetservice/trade_set.go
Normal file
68
services/tradsetservice/trade_set.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package tradsetservice
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go-admin/common/global"
|
||||||
|
"go-admin/common/helper"
|
||||||
|
"go-admin/models"
|
||||||
|
|
||||||
|
"github.com/bytedance/sonic"
|
||||||
|
"github.com/go-admin-team/go-admin-core/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 获取交易所交易对
|
||||||
|
// symbolType 1:spot 2:future
|
||||||
|
func GetExchangeTradeSets(exchange string, symbolType int) (*[]models.TradeSet, error) {
|
||||||
|
var tradeSets []models.TradeSet
|
||||||
|
var scanKey string
|
||||||
|
|
||||||
|
if symbolType == 1 {
|
||||||
|
scanKey = fmt.Sprintf(global.TICKER_SPOT, exchange, "*")
|
||||||
|
} else if symbolType == 2 {
|
||||||
|
scanKey = fmt.Sprintf(global.TICKER_FUTURES, exchange, "*")
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("symbolType error")
|
||||||
|
}
|
||||||
|
|
||||||
|
vals, err := helper.DefaultRedis.GetAllKeysAndValues(scanKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, val := range vals {
|
||||||
|
var tradeSet models.TradeSet
|
||||||
|
if err := sonic.Unmarshal([]byte(val), &tradeSet); err != nil {
|
||||||
|
logger.Errorf("unmarshal tradeSet err:%v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tradeSets = append(tradeSets, tradeSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tradeSets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取单个交易对
|
||||||
|
func GetExchangeTradeSet(exchange string, symbolType int, symbol string) (*models.TradeSet, error) {
|
||||||
|
var tradeSet models.TradeSet
|
||||||
|
var scanKey string
|
||||||
|
|
||||||
|
if symbolType == 1 {
|
||||||
|
scanKey = fmt.Sprintf(global.TICKER_SPOT, exchange, symbol)
|
||||||
|
} else if symbolType == 2 {
|
||||||
|
scanKey = fmt.Sprintf(global.TICKER_FUTURES, exchange, symbol)
|
||||||
|
} else {
|
||||||
|
return nil, fmt.Errorf("symbolType error")
|
||||||
|
}
|
||||||
|
|
||||||
|
val, err := helper.DefaultRedis.GetString(scanKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sonic.Unmarshal([]byte(val), &tradeSet); err != nil {
|
||||||
|
logger.Errorf("unmarshal tradeSet err:%v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tradeSet, nil
|
||||||
|
}
|
||||||
22
test/init_settings.go
Normal file
22
test/init_settings.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go-admin/common/helper"
|
||||||
|
"go-admin/config"
|
||||||
|
|
||||||
|
"github.com/go-admin-team/go-admin-core/sdk"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitSettings() *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")
|
||||||
|
|
||||||
|
config.ExtConfig.ProxyUrl = "http://127.0.0.1:7890"
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user