1
This commit is contained in:
808
services/binanceservice/binancerest.go
Normal file
808
services/binanceservice/binancerest.go
Normal file
@ -0,0 +1,808 @@
|
||||
package binanceservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
DbModels "go-admin/app/admin/models"
|
||||
"go-admin/app/admin/service/dto"
|
||||
"go-admin/common/const/rediskey"
|
||||
commondto "go-admin/common/dto"
|
||||
"go-admin/common/global"
|
||||
"go-admin/common/helper"
|
||||
"go-admin/models"
|
||||
"go-admin/models/spot"
|
||||
"go-admin/pkg/httputils"
|
||||
"go-admin/pkg/utility"
|
||||
"go-admin/pkg/utility/snowflakehelper"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
binanceRestApi = "https://api.binance.com"
|
||||
)
|
||||
|
||||
var ErrorMaps = map[float64]string{
|
||||
-2021: "订单已拒绝。请调整触发价并重新下订单。 对于买入/做多,止盈订单触发价应低于市场价,止损订单的触发价应高于市场价。卖出/做空则与之相反",
|
||||
-4164: "下单失败。少于最小下单金额",
|
||||
-4061: "持仓方向需要设置为单向持仓。",
|
||||
-2019: "保证金不足",
|
||||
-1111: "金额设置错误。精度错误",
|
||||
-1021: "请求的时间戳在recvWindow之外",
|
||||
-2011: "该交易对没有订单",
|
||||
-2010: "账户余额不足",
|
||||
}
|
||||
|
||||
type SpotRestApi struct {
|
||||
}
|
||||
|
||||
/*
|
||||
获取 现货交易-规范信息
|
||||
|
||||
- return @info 规范信息
|
||||
- return @err 错误信息
|
||||
*/
|
||||
func (e SpotRestApi) GetExchangeInfo() (symbols []spot.Symbol, err error) {
|
||||
url := fmt.Sprintf("%s%s?permissions=SPOT", binanceRestApi, "/api/v3/exchangeInfo")
|
||||
mapData, err := httputils.NewHttpRequestWithFasthttp("GET", url, "", map[string]string{})
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(mapData) == 0 {
|
||||
err = errors.New("获取现货交易-规范信息数量为空")
|
||||
return
|
||||
}
|
||||
|
||||
var info spot.ExchangeInfo
|
||||
err = sonic.Unmarshal(mapData, &info)
|
||||
|
||||
if err == nil {
|
||||
return info.Symbols, nil
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*
|
||||
获取现货24h行情变更
|
||||
*/
|
||||
func (e SpotRestApi) GetSpotTicker24h(tradeSet *map[string]models.TradeSet) (deleteSymbols []string, err error) {
|
||||
tickerApi := fmt.Sprintf("%s%s", binanceRestApi, "/api/v3/ticker/24hr")
|
||||
mapData, err := httputils.NewHttpRequestWithFasthttp("GET", tickerApi, "", map[string]string{})
|
||||
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
deleteSymbols = make([]string, 0)
|
||||
if len(mapData) == 0 {
|
||||
return deleteSymbols, errors.New("获取交易对失败,或数量为空")
|
||||
}
|
||||
tickers := make([]spot.SpotTicker24h, 0)
|
||||
err = sonic.Unmarshal([]byte(mapData), &tickers)
|
||||
|
||||
if err != nil {
|
||||
log.Error("反序列化json失败", err)
|
||||
}
|
||||
|
||||
for _, item := range tickers {
|
||||
key := fmt.Sprintf("%s:%s", global.TICKER_SPOT, item.Symbol)
|
||||
symbol, exits := (*tradeSet)[item.Symbol]
|
||||
|
||||
if !exits {
|
||||
helper.DefaultRedis.DeleteString(key)
|
||||
continue
|
||||
}
|
||||
symbol.OpenPrice = utility.StringAsFloat(item.OpenPrice)
|
||||
symbol.PriceChange = utility.StringAsFloat(item.PriceChangePercent)
|
||||
symbol.LowPrice = item.LowPrice
|
||||
symbol.HighPrice = item.HighPrice
|
||||
symbol.Volume = item.Volume
|
||||
symbol.QuoteVolume = item.QuoteVolume
|
||||
symbol.LastPrice = item.LastPrice
|
||||
|
||||
val, err := sonic.Marshal(symbol)
|
||||
|
||||
if !strings.HasSuffix(item.Symbol, symbol.Currency) || item.Count <= 0 || utility.StringToFloat64(item.QuoteVolume) <= 0 {
|
||||
helper.DefaultRedis.DeleteString(key)
|
||||
deleteSymbols = append(deleteSymbols, item.Symbol)
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error("序列化失败", item.Symbol)
|
||||
continue
|
||||
}
|
||||
err = helper.DefaultRedis.SetString(key, string(val))
|
||||
|
||||
if err != nil {
|
||||
log.Error("缓存交易对失败|", item.Symbol, err)
|
||||
}
|
||||
|
||||
helper.DefaultRedis.AddSortSet(global.COIN_PRICE_CHANGE, symbol.PriceChange, symbol.Coin)
|
||||
}
|
||||
return deleteSymbols, nil
|
||||
}
|
||||
|
||||
/*
|
||||
获取单个交易对24h行情
|
||||
|
||||
- @symbol 交易对
|
||||
- @data 结果
|
||||
*/
|
||||
func (e SpotRestApi) GetSpotTicker24(symbol string, data *models.Ticker24, tradeSet *models.TradeSet) error {
|
||||
key := fmt.Sprintf("%s:%s", global.TICKER_SPOT, 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
|
||||
}
|
||||
|
||||
type Ticker struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Price string `json:"price"`
|
||||
}
|
||||
|
||||
func (e SpotRestApi) Ticker() {
|
||||
tickerApi := fmt.Sprintf("%s%s", binanceRestApi, "/api/v3/ticker/price")
|
||||
mapData, _ := httputils.NewHttpRequestWithFasthttp("GET", tickerApi, "", map[string]string{})
|
||||
//sonic.Unmarshal(mapData, &tickerData)
|
||||
helper.DefaultRedis.SetString(rediskey.SpotSymbolTicker, string(mapData))
|
||||
}
|
||||
|
||||
// OrderPlace 现货下单
|
||||
func (e SpotRestApi) OrderPlace(orm *gorm.DB, params OrderPlacementService) error {
|
||||
if orm == nil {
|
||||
return errors.New("数据库实例为空")
|
||||
}
|
||||
err2 := params.CheckParams()
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
paramsMaps := map[string]string{
|
||||
"symbol": params.Symbol,
|
||||
"side": params.Side,
|
||||
"quantity": params.Quantity.String(),
|
||||
"type": params.Type,
|
||||
"newClientOrderId": params.NewClientOrderId,
|
||||
}
|
||||
if strings.ToUpper(params.Type) != "MARKET" { //市价
|
||||
paramsMaps["price"] = params.Price.String()
|
||||
paramsMaps["timeInForce"] = "GTC"
|
||||
|
||||
if strings.ToUpper(params.Type) == "TAKE_PROFIT_LIMIT" || strings.ToUpper(params.Type) == "STOP_LOSS_LIMIT" {
|
||||
paramsMaps["stopPrice"] = params.StopPrice.String()
|
||||
}
|
||||
}
|
||||
var apiUserInfo DbModels.LineApiUser
|
||||
|
||||
err := orm.Model(&DbModels.LineApiUser{}).Where("id = ?", params.ApiId).Find(&apiUserInfo).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
log.Errorf("api用户出错 err: %+v", err)
|
||||
return err
|
||||
}
|
||||
client := GetClient(&apiUserInfo)
|
||||
resp, _, err := client.SendSpotAuth("/api/v3/order", "POST", paramsMaps)
|
||||
if err != nil {
|
||||
dataMap := make(map[string]interface{})
|
||||
|
||||
if err.Error() != "" {
|
||||
if err := sonic.Unmarshal([]byte(err.Error()), &dataMap); err != nil {
|
||||
return fmt.Errorf("api_id:%d 交易对:%s 下订单失败:%+v", apiUserInfo.Id, params.Symbol, err.Error())
|
||||
}
|
||||
}
|
||||
code, ok := dataMap["code"]
|
||||
if ok {
|
||||
errContent := ErrorMaps[code.(float64)]
|
||||
paramsVal, _ := sonic.MarshalString(paramsMaps)
|
||||
|
||||
log.Error("api_id:", utility.IntToString(apiUserInfo.Id), " 交易对:", params.Symbol, " 下单参数:", paramsVal)
|
||||
|
||||
if errContent == "" {
|
||||
errContent, _ = dataMap["msg"].(string)
|
||||
}
|
||||
|
||||
return fmt.Errorf("api_id:%d 交易对:%s 下订单失败:%s", apiUserInfo.Id, params.Symbol, errContent)
|
||||
}
|
||||
if strings.Contains(err.Error(), "Unknown order sent.") {
|
||||
return fmt.Errorf("api_id:%d 交易对:%s 下单失败:%+v", apiUserInfo.Id, params.Symbol, ErrorMaps[-2011])
|
||||
}
|
||||
|
||||
return fmt.Errorf("api_id:%d 交易对:%s 下单失败:%v", apiUserInfo.Id, params.Symbol, err)
|
||||
}
|
||||
var dataMap map[string]interface{}
|
||||
if err := sonic.Unmarshal(resp, &dataMap); err != nil {
|
||||
return fmt.Errorf("api_id:%d 交易对:%s 下单失败:%+v", apiUserInfo.Id, params.Symbol, err.Error())
|
||||
}
|
||||
//code, ok := dataMap["code"]
|
||||
//if !ok {
|
||||
// return fmt.Errorf("api_id:%d 交易对:%s 下单失败:%s", apiUserInfo.Id, params.Symbol, dataMap["message"])
|
||||
//
|
||||
//}
|
||||
//if code.(float64) != 200 {
|
||||
// return fmt.Errorf("api_id:%d 交易对:%s 下单失败:%s", apiUserInfo.Id, params.Symbol, dataMap["message"])
|
||||
//}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelOpenOrders 撤销单一交易对下所有挂单 包括了来自订单列表的挂单
|
||||
func (e SpotRestApi) CancelOpenOrders(orm *gorm.DB, req CancelOpenOrdersReq) error {
|
||||
if orm == nil {
|
||||
return errors.New("数据库实例为空")
|
||||
}
|
||||
err := req.CheckParams()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
params := map[string]string{
|
||||
"symbol": req.Symbol,
|
||||
}
|
||||
var apiUserInfo DbModels.LineApiUser
|
||||
|
||||
err = orm.Model(&DbModels.LineApiUser{}).Where("id = ?", req.ApiId).Find(&apiUserInfo).Error
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return fmt.Errorf("api_id:%d 交易对:%s api用户出错:%+v", apiUserInfo.Id, req.Symbol, err)
|
||||
}
|
||||
var client *helper.BinanceClient
|
||||
|
||||
if apiUserInfo.UserPass == "" {
|
||||
client, _ = helper.NewBinanceClient(apiUserInfo.ApiKey, apiUserInfo.ApiSecret, "", apiUserInfo.IpAddress)
|
||||
} else {
|
||||
client, _ = helper.NewBinanceClient(apiUserInfo.ApiKey, apiUserInfo.ApiSecret, "socks5", apiUserInfo.UserPass+"@"+apiUserInfo.IpAddress)
|
||||
}
|
||||
_, _, err = client.SendSpotAuth("/api/v3/openOrders", "DELETE", params)
|
||||
if err != nil {
|
||||
dataMap := make(map[string]interface{})
|
||||
|
||||
if err.Error() != "" {
|
||||
if err := sonic.Unmarshal([]byte(err.Error()), &dataMap); err != nil {
|
||||
return fmt.Errorf("api_id:%d 交易对:%s 撤销订单失败:%+v", apiUserInfo.Id, req.Symbol, err.Error())
|
||||
}
|
||||
}
|
||||
code, ok := dataMap["code"]
|
||||
if ok {
|
||||
return fmt.Errorf("api_id:%d 交易对:%s 撤销订单失败:%s", apiUserInfo.Id, req.Symbol, ErrorMaps[code.(float64)])
|
||||
}
|
||||
if strings.Contains(err.Error(), "Unknown order sent.") {
|
||||
return fmt.Errorf("api_id:%d 交易对:%s 撤销订单失败:%+v", apiUserInfo.Id, req.Symbol, ErrorMaps[-2011])
|
||||
}
|
||||
return fmt.Errorf("api_id:%d 交易对:%s 撤销订单失败:%+v", apiUserInfo.Id, req.Symbol, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelOpenOrderByOrderSn 通过单一订单号取消委托
|
||||
func (e SpotRestApi) CancelOpenOrderByOrderSn(apiUserInfo DbModels.LineApiUser, symbol string, newClientOrderId string) error {
|
||||
params := map[string]string{
|
||||
"symbol": symbol,
|
||||
"origClientOrderId": newClientOrderId,
|
||||
"recvWindow": "10000",
|
||||
}
|
||||
var client *helper.BinanceClient
|
||||
|
||||
if apiUserInfo.UserPass == "" {
|
||||
client, _ = helper.NewBinanceClient(apiUserInfo.ApiKey, apiUserInfo.ApiSecret, "", apiUserInfo.IpAddress)
|
||||
} else {
|
||||
client, _ = helper.NewBinanceClient(apiUserInfo.ApiKey, apiUserInfo.ApiSecret, "socks5", apiUserInfo.UserPass+"@"+apiUserInfo.IpAddress)
|
||||
}
|
||||
_, code, err := client.SendSpotAuth("/api/v3/order ", "DELETE", params)
|
||||
if err != nil || code != 200 {
|
||||
log.Error("取消现货委托失败 参数:", params)
|
||||
log.Error("取消现货委托失败 code:", code)
|
||||
log.Error("取消现货委托失败 err:", err)
|
||||
dataMap := make(map[string]interface{})
|
||||
if err.Error() != "" {
|
||||
if err := sonic.Unmarshal([]byte(err.Error()), &dataMap); err != nil {
|
||||
return fmt.Errorf("api_id:%d 交易对:%s 撤销订单失败:%+v", apiUserInfo.Id, symbol, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
code, ok := dataMap["code"]
|
||||
if ok {
|
||||
return fmt.Errorf("api_id:%d 交易对:%s 撤销订单失败:%s", apiUserInfo.Id, symbol, ErrorMaps[code.(float64)])
|
||||
}
|
||||
if strings.Contains(err.Error(), "Unknown order sent.") {
|
||||
return fmt.Errorf("api_id:%d 交易对:%s 撤销订单失败:%+v", apiUserInfo.Id, symbol, ErrorMaps[-2011])
|
||||
}
|
||||
return fmt.Errorf("api_id:%d 交易对:%s 撤销订单失败:%+v", apiUserInfo.Id, symbol, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CalcEntryCashPriceByOrder 计算现货主单均价
|
||||
func CalcEntryCashPriceByOrder(orderInfo *DbModels.LinePreOrder, orm *gorm.DB) (EntryPriceResult, error) {
|
||||
//找到主单成交的记录
|
||||
var id int
|
||||
if orderInfo.Pid > 0 {
|
||||
id = orderInfo.Pid
|
||||
} else {
|
||||
id = orderInfo.Id
|
||||
}
|
||||
|
||||
orderLists := make([]DbModels.LinePreOrder, 0)
|
||||
orm.Model(&DbModels.LinePreOrder{}).Where(" symbol = ? AND site = 'BUY' AND order_type in ('1','8') AND status ='9' AND (id = ? OR pid = ?)", orderInfo.Symbol, id, id).Find(&orderLists)
|
||||
|
||||
var (
|
||||
totalNum decimal.Decimal //总成交数量
|
||||
totalMoney decimal.Decimal //总金额
|
||||
entryPrice decimal.Decimal //均价
|
||||
initPrice decimal.Decimal //主单下单价格
|
||||
firstId int //主单id
|
||||
)
|
||||
for _, list := range orderLists {
|
||||
num, _ := decimal.NewFromString(list.Num)
|
||||
totalNum = totalNum.Add(num)
|
||||
price, _ := decimal.NewFromString(list.Price)
|
||||
totalMoney = totalMoney.Add(num.Mul(price))
|
||||
if list.OrderType == "1" {
|
||||
firstId = list.Id
|
||||
initPrice, _ = decimal.NewFromString(list.Price)
|
||||
}
|
||||
}
|
||||
if totalNum.GreaterThan(decimal.Zero) {
|
||||
entryPrice = totalMoney.Div(totalNum)
|
||||
}
|
||||
return EntryPriceResult{
|
||||
TotalNum: totalNum,
|
||||
EntryPrice: entryPrice,
|
||||
FirstPrice: initPrice,
|
||||
FirstId: firstId,
|
||||
TotalMoney: totalMoney,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ClosePosition 平仓
|
||||
// symbol 交易对
|
||||
// orderSn 平仓单号
|
||||
// quantity 平仓数量
|
||||
// side 原始仓位方向
|
||||
// apiUserInfo 用户信息
|
||||
// orderType 平仓类型 限价(LIMIT) 市价()
|
||||
func (e SpotRestApi) ClosePosition(symbol string, orderSn string, quantity decimal.Decimal, side string,
|
||||
apiUserInfo DbModels.LineApiUser, orderType string, rate string, price decimal.Decimal) error {
|
||||
endpoint := "/api/v3/order "
|
||||
params := map[string]string{
|
||||
"symbol": symbol,
|
||||
"type": orderType,
|
||||
"quantity": quantity.String(),
|
||||
"newClientOrderId": orderSn,
|
||||
}
|
||||
if side == "SELL" {
|
||||
params["side"] = "BUY"
|
||||
} else {
|
||||
params["side"] = "SELL"
|
||||
}
|
||||
|
||||
if orderType == "LIMIT" {
|
||||
key := fmt.Sprintf("%s:%s", global.TICKER_SPOT, symbol)
|
||||
tradeSet, _ := helper.GetObjString[models.TradeSet](helper.DefaultRedis, key)
|
||||
rateFloat, _ := decimal.NewFromString(rate)
|
||||
if rateFloat.GreaterThan(decimal.Zero) {
|
||||
if side == "SELL" { //仓位是空 平空的话
|
||||
price = price.Mul(decimal.NewFromInt(1).Add(rateFloat)).Truncate(int32(tradeSet.PriceDigit))
|
||||
} else {
|
||||
price = price.Mul(decimal.NewFromInt(1).Sub(rateFloat)).Truncate(int32(tradeSet.PriceDigit))
|
||||
}
|
||||
params["price"] = price.String()
|
||||
}
|
||||
}
|
||||
params["timeInForce"] = "GTC"
|
||||
client := GetClient(&apiUserInfo)
|
||||
|
||||
resp, _, err := client.SendFuturesRequestAuth(endpoint, "POST", params)
|
||||
if err != nil {
|
||||
var dataMap map[string]interface{}
|
||||
if err2 := sonic.Unmarshal([]byte(err.Error()), &dataMap); err2 != nil {
|
||||
return fmt.Errorf("api_id:%d 交易对:%s 平仓出错:%s", apiUserInfo.Id, symbol, err.Error())
|
||||
}
|
||||
code, ok := dataMap["code"]
|
||||
if ok {
|
||||
errContent := FutErrorMaps[code.(float64)]
|
||||
|
||||
if errContent == "" {
|
||||
errContent = err.Error()
|
||||
}
|
||||
|
||||
return fmt.Errorf("api_id:%d 交易对:%s 平仓出错:%s", apiUserInfo.Id, symbol, errContent)
|
||||
}
|
||||
}
|
||||
var orderResp FutOrderResp
|
||||
err = sonic.Unmarshal(resp, &orderResp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("api_id:%d 交易对:%s 平仓出错:%s", apiUserInfo.Id, symbol, err.Error())
|
||||
}
|
||||
if orderResp.Symbol == "" {
|
||||
return fmt.Errorf("api_id:%d 交易对:%s 平仓出错:未找到订单信息", apiUserInfo.Id, symbol)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetClient(apiUserInfo *DbModels.LineApiUser) *helper.BinanceClient {
|
||||
var client *helper.BinanceClient
|
||||
if apiUserInfo.UserPass == "" {
|
||||
client, _ = helper.NewBinanceClient(apiUserInfo.ApiKey, apiUserInfo.ApiSecret, "", apiUserInfo.IpAddress)
|
||||
} else {
|
||||
client, _ = helper.NewBinanceClient(apiUserInfo.ApiKey, apiUserInfo.ApiSecret, "socks5", apiUserInfo.UserPass+"@"+apiUserInfo.IpAddress)
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
/*
|
||||
重下止盈单
|
||||
*/
|
||||
func (e SpotRestApi) reTakeOrder(parentOrderInfo DbModels.LinePreOrder, orm *gorm.DB) {
|
||||
price, _ := decimal.NewFromString(parentOrderInfo.Price)
|
||||
num, _ := decimal.NewFromString(parentOrderInfo.Num)
|
||||
parentId := parentOrderInfo.Id
|
||||
|
||||
if parentOrderInfo.Pid > 0 {
|
||||
parentId = parentOrderInfo.Pid
|
||||
}
|
||||
|
||||
var takePrice decimal.Decimal
|
||||
holdeAKey := fmt.Sprintf(rediskey.HoldeA, parentId)
|
||||
holdeAVal, _ := helper.DefaultRedis.GetString(holdeAKey)
|
||||
holdeA := HoldeData{}
|
||||
|
||||
if holdeAVal != "" {
|
||||
sonic.Unmarshal([]byte(holdeAVal), &holdeA)
|
||||
}
|
||||
|
||||
//查询持仓失败
|
||||
if holdeA.Id == 0 {
|
||||
log.Error("查询A账号持仓失败")
|
||||
return
|
||||
}
|
||||
|
||||
//加仓次数大于0 就需要使用均价
|
||||
if holdeA.PositionIncrementCount > 0 {
|
||||
price = holdeA.AveragePrice
|
||||
num = holdeA.TotalQuantity
|
||||
}
|
||||
|
||||
if parentOrderInfo.Site == "BUY" {
|
||||
takePrice = price.Mul(decimal.NewFromInt(100).Add(parentOrderInfo.ProfitRate)).Div(decimal.NewFromInt(100))
|
||||
} else {
|
||||
takePrice = price.Mul(decimal.NewFromInt(100).Sub(parentOrderInfo.ProfitRate)).Div(decimal.NewFromInt(100))
|
||||
}
|
||||
var takeOrder, oldTakeOrder DbModels.LinePreOrder
|
||||
|
||||
if err := orm.Model(&oldTakeOrder).Where("pid= ? AND order_type ='5'", parentId).First(&oldTakeOrder).Error; err != nil {
|
||||
log.Error("查询止盈单失败")
|
||||
return
|
||||
}
|
||||
|
||||
tradeset, _ := GetTradeSet(oldTakeOrder.Symbol, 0)
|
||||
|
||||
if tradeset.Coin == "" {
|
||||
log.Error("查询交易对失败")
|
||||
return
|
||||
}
|
||||
copier.Copy(&takeOrder, &oldTakeOrder)
|
||||
|
||||
takeOrder.OrderSn = strconv.FormatInt(snowflakehelper.GetOrderId(), 10)
|
||||
takeOrder.Price = takePrice.Truncate(int32(tradeset.PriceDigit)).String()
|
||||
takeOrder.Num = num.Mul(decimal.NewFromFloat(0.995)).Truncate(int32(tradeset.AmountDigit)).String()
|
||||
takeOrder.Desc = ""
|
||||
takeOrder.Status = 0
|
||||
takeOrder.Id = 0
|
||||
|
||||
if err := orm.Create(&takeOrder).Error; err != nil {
|
||||
log.Error("创建新止盈单失败")
|
||||
return
|
||||
}
|
||||
|
||||
params := OrderPlacementService{
|
||||
ApiId: parentOrderInfo.ApiId,
|
||||
Symbol: parentOrderInfo.Symbol,
|
||||
Price: utility.StrToDecimal(takeOrder.Price),
|
||||
Quantity: utility.StringToDecimal(takeOrder.Num),
|
||||
Side: "SELL",
|
||||
Type: "TAKE_PROFIT_LIMIT",
|
||||
TimeInForce: "GTC",
|
||||
StopPrice: utility.StrToDecimal(takeOrder.Price),
|
||||
NewClientOrderId: takeOrder.OrderSn,
|
||||
}
|
||||
|
||||
apiUserInfo, _ := GetApiInfo(parentOrderInfo.ApiId)
|
||||
|
||||
if apiUserInfo.Id == 0 {
|
||||
log.Error("获取用户api失败")
|
||||
return
|
||||
}
|
||||
|
||||
if err := CancelSpotOrder(parentOrderInfo.Symbol, &apiUserInfo, "SELL"); err != nil {
|
||||
log.Error("取消旧止盈失败 err:", err)
|
||||
} else {
|
||||
if err := orm.Model(&DbModels.LinePreOrder{}).Where("pid = ? AND order_type =5 AND status in ('0','1','5') AND order_sn !=?", parentId, takeOrder.OrderSn).Update("status", "4").Error; err != nil {
|
||||
log.Error("更新旧止盈单取消状态失败 err:", err)
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
for x := 1; x <= 4; x++ {
|
||||
err = e.OrderPlace(orm, params)
|
||||
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
log.Error("下止盈单失败 第", utility.IntToString(x), "次", " err:", err)
|
||||
time.Sleep(2 * time.Second * time.Duration(x))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error("重新下单止盈失败 err:", err)
|
||||
|
||||
if err1 := orm.Model(&DbModels.LinePreOrder{}).Where("order_sn =?", takeOrder.OrderSn).
|
||||
Updates(map[string]interface{}{"status": "2", "desc": err.Error()}).Error; err1 != nil {
|
||||
log.Error("重新下单止盈 修改订单失败 pid:", parentId, " takePrice:", takePrice, " err:", err1)
|
||||
}
|
||||
}
|
||||
|
||||
//修改止盈单信息
|
||||
if err := orm.Model(&DbModels.LinePreOrder{}).Where("pid =? AND order_type ='5'", parentId).
|
||||
Updates(map[string]interface{}{"price": takePrice, "rate": parentOrderInfo.ProfitRate}).Error; err != nil {
|
||||
log.Error("重新下单止盈 修改订单失败 pid:", parentId, " takePrice:", takePrice, " rate:", parentOrderInfo.ProfitRate, " err:", err)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
判断是否触发
|
||||
*/
|
||||
func JudgeSpotPrice(trade models.TradeSet) {
|
||||
preOrderVal, _ := helper.DefaultRedis.GetAllList(rediskey.PreSpotOrderList)
|
||||
db := GetDBConnection()
|
||||
|
||||
if len(preOrderVal) == 0 {
|
||||
// log.Debug("没有现货预下单")
|
||||
return
|
||||
}
|
||||
|
||||
spotApi := SpotRestApi{}
|
||||
for _, item := range preOrderVal {
|
||||
preOrder := dto.PreOrderRedisList{}
|
||||
if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil {
|
||||
log.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 分布式锁下单
|
||||
// v 预下单信息
|
||||
// item 预下单源文本
|
||||
func SpotOrderLock(db *gorm.DB, v *dto.PreOrderRedisList, item string, spotApi SpotRestApi) {
|
||||
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 {
|
||||
log.Error("获取锁失败", err)
|
||||
return
|
||||
} else if ok {
|
||||
defer lock.Release()
|
||||
|
||||
key := fmt.Sprintf(rediskey.UserHolding, v.ApiId)
|
||||
symbols, err := helper.DefaultRedis.GetAllList(key)
|
||||
|
||||
if err != nil && err != redis.Nil {
|
||||
log.Error("获取用户持仓失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
preOrder := DbModels.LinePreOrder{}
|
||||
if err := db.Where("id = ?", v.Id).First(&preOrder).Error; err != nil {
|
||||
log.Error("获取预下单失败", err)
|
||||
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
log.Error("不存在待触发主单", item)
|
||||
helper.DefaultRedis.LRem(rediskey.PreSpotOrderList, item)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//获取代币 是否已有持仓
|
||||
coin := utility.ReplaceSuffix(preOrder.Symbol, preOrder.QuoteSymbol, "")
|
||||
|
||||
if utility.ContainsStr(symbols, coin) {
|
||||
log.Debug("已有持仓")
|
||||
return
|
||||
}
|
||||
|
||||
hasrecord, _ := helper.DefaultRedis.IsElementInList(rediskey.PreSpotOrderList, item)
|
||||
|
||||
if !hasrecord {
|
||||
log.Error("不存在待触发主单", item)
|
||||
return
|
||||
}
|
||||
|
||||
price, _ := decimal.NewFromString(v.Price)
|
||||
num, _ := decimal.NewFromString(preOrder.Num)
|
||||
params := OrderPlacementService{
|
||||
ApiId: v.ApiId,
|
||||
Symbol: v.Symbol,
|
||||
Side: v.Site,
|
||||
Type: preOrder.MainOrderType,
|
||||
TimeInForce: "GTC",
|
||||
Price: price,
|
||||
Quantity: num,
|
||||
NewClientOrderId: v.OrderSn,
|
||||
}
|
||||
preOrderVal, _ := sonic.MarshalString(&v)
|
||||
|
||||
if err := spotApi.OrderPlace(db, params); err != nil {
|
||||
log.Error("下单失败", v.Symbol, " err:", err)
|
||||
err := db.Model(&DbModels.LinePreOrder{}).Where("id =? AND status ='0'", preOrder.Id).Updates(map[string]interface{}{"status": "2", "desc": err.Error()}).Error
|
||||
|
||||
if err != nil {
|
||||
log.Error("下单失败后修改订单失败")
|
||||
}
|
||||
|
||||
if preOrderVal != "" {
|
||||
if _, err := helper.DefaultRedis.LRem(rediskey.PreSpotOrderList, preOrderVal); err != nil {
|
||||
log.Error("删除redis 预下单失败:", err)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if preOrderVal != "" {
|
||||
if _, err := helper.DefaultRedis.LRem(rediskey.PreSpotOrderList, preOrderVal); err != nil {
|
||||
log.Error("删除redis 预下单失败:", err)
|
||||
}
|
||||
|
||||
// spotPreOrders, _ := helper.DefaultRedis.GetAllList(rediskey.PreSpotOrderList)
|
||||
// futuresPreOrders, _ := helper.DefaultRedis.GetAllList(rediskey.PreFutOrderList)
|
||||
// var order dto.PreOrderRedisList
|
||||
|
||||
// for _, item := range spotPreOrders {
|
||||
// sonic.Unmarshal([]byte(item), &order)
|
||||
|
||||
// if order.QuoteSymbol == "" {
|
||||
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
if err := db.Model(&DbModels.LinePreOrder{}).Where("id =? AND status ='0'", preOrder.Id).Update("status", "1").Error; err != nil {
|
||||
log.Error("更新预下单状态失败 ordersn:", v.OrderSn, " status:1")
|
||||
}
|
||||
|
||||
if err := helper.DefaultRedis.RPushList(key, coin); err != nil {
|
||||
log.Error("写入用户持仓失败", v.Symbol)
|
||||
}
|
||||
|
||||
return
|
||||
} else {
|
||||
log.Error("获取锁失败")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
获取api用户信息
|
||||
*/
|
||||
func GetApiInfo(apiId int) (DbModels.LineApiUser, error) {
|
||||
api := DbModels.LineApiUser{}
|
||||
key := fmt.Sprintf(rediskey.API_USER, apiId)
|
||||
val, _ := helper.DefaultRedis.GetString(key)
|
||||
|
||||
if val != "" {
|
||||
if err := sonic.UnmarshalString(val, &api); err == nil {
|
||||
return api, nil
|
||||
}
|
||||
}
|
||||
|
||||
db := GetDBConnection()
|
||||
|
||||
if err := db.Model(&api).Where("id =?", apiId).First(&api).Error; err != nil {
|
||||
return api, err
|
||||
}
|
||||
|
||||
val, _ = sonic.MarshalString(&api)
|
||||
|
||||
if val != "" {
|
||||
helper.DefaultRedis.SetString(key, val)
|
||||
}
|
||||
|
||||
return api, nil
|
||||
}
|
||||
|
||||
/*
|
||||
根据A账户获取B账号信息
|
||||
*/
|
||||
func GetChildApiInfo(apiId int) (DbModels.LineApiUser, error) {
|
||||
var api DbModels.LineApiUser
|
||||
childApiId := 0
|
||||
groups := GetApiGroups()
|
||||
|
||||
for _, item := range groups {
|
||||
if item.ApiUserId == apiId {
|
||||
childApiId = item.ChildApiUserId
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if childApiId > 0 {
|
||||
return GetApiInfo(childApiId)
|
||||
}
|
||||
|
||||
return api, nil
|
||||
}
|
||||
|
||||
func GetApiGroups() []commondto.ApiGroupDto {
|
||||
apiGroups := make([]commondto.ApiGroupDto, 0)
|
||||
apiGroupStr, _ := helper.DefaultRedis.GetAllKeysAndValues(rediskey.ApiGroupAll)
|
||||
|
||||
if len(apiGroupStr) == 0 {
|
||||
return apiGroups
|
||||
}
|
||||
|
||||
for _, item := range apiGroupStr {
|
||||
apiGroup := commondto.ApiGroupDto{}
|
||||
if err := sonic.UnmarshalString(item, &apiGroup); err != nil {
|
||||
log.Error("groups 序列化失败", err)
|
||||
continue
|
||||
}
|
||||
apiGroups = append(apiGroups, apiGroup)
|
||||
}
|
||||
|
||||
return apiGroups
|
||||
}
|
||||
|
||||
// GetSpotSymbolLastPrice 获取现货交易对最新价格
|
||||
func (e SpotRestApi) GetSpotSymbolLastPrice(targetSymbol string) (lastPrice decimal.Decimal) {
|
||||
tickerSymbol := helper.DefaultRedis.Get(rediskey.SpotSymbolTicker).Val()
|
||||
tickerSymbolMaps := make([]dto.Ticker, 0)
|
||||
sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps)
|
||||
//key := fmt.Sprintf("%s:%s", global.TICKER_SPOT, targetSymbol)
|
||||
//tradeSet, _ := helper.GetObjString[models.TradeSet](helper.DefaultRedis, key)
|
||||
for _, symbolMap := range tickerSymbolMaps {
|
||||
if symbolMap.Symbol == strings.ToUpper(targetSymbol) {
|
||||
lastPrice = utility.StringToDecimal(symbolMap.Price)
|
||||
}
|
||||
}
|
||||
return lastPrice
|
||||
}
|
||||
205
services/binanceservice/binanceservice_test.go
Normal file
205
services/binanceservice/binanceservice_test.go
Normal file
@ -0,0 +1,205 @@
|
||||
package binanceservice
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/bytedance/sonic"
|
||||
"go-admin/app/admin/models"
|
||||
"go-admin/common/helper"
|
||||
"go-admin/pkg/utility"
|
||||
"go-admin/pkg/utility/snowflakehelper"
|
||||
"testing"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestCancelFutClosePosition(t *testing.T) {
|
||||
dsn := "root:root@tcp(192.168.123.216:3306)/gp-bian?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
var apiUserInfo models.LineApiUser
|
||||
db.Model(&models.LineApiUser{}).Where("id = 21").Find(&apiUserInfo)
|
||||
err := CancelFutClosePosition(apiUserInfo, "ADAUSDT", "BUY", "SHORT")
|
||||
if err != nil {
|
||||
t.Log("err:", err)
|
||||
}
|
||||
fmt.Println("成功")
|
||||
}
|
||||
|
||||
func TestDecimal(t *testing.T) {
|
||||
|
||||
fromString, err := decimal.NewFromString("")
|
||||
if err != nil {
|
||||
fmt.Println("err:", err)
|
||||
}
|
||||
fmt.Println(fromString)
|
||||
}
|
||||
|
||||
func TestPositionV3(t *testing.T) {
|
||||
api := FutRestApi{}
|
||||
dsn := "root:root@tcp(192.168.1.12:3306)/gp-bian?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
var apiUserInfo models.LineApiUser
|
||||
db.Model(&models.LineApiUser{}).Where("id = 21").Find(&apiUserInfo)
|
||||
err := CancelFutClosePosition(apiUserInfo, "ADAUSDT", "BUY", "SHORT")
|
||||
if err != nil {
|
||||
t.Log("err:", err)
|
||||
}
|
||||
|
||||
v3, err := api.GetPositionV3(&apiUserInfo, "DOGEUSDT")
|
||||
if err != nil {
|
||||
t.Log("err:", err)
|
||||
}
|
||||
fmt.Println(v3)
|
||||
}
|
||||
|
||||
func TestInsertLog(t *testing.T) {
|
||||
dsn := "root:root@tcp(192.168.1.12:3306)/gp-bian?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
//初始redis 链接
|
||||
helper.InitDefaultRedis("192.168.1.12:6379", "", 0)
|
||||
helper.InitLockRedisConn("192.168.1.12:6379", "", "0")
|
||||
|
||||
InsertProfitLogs(db, "367452130811838464", decimal.NewFromInt(20), decimal.NewFromFloat(0.34078000))
|
||||
}
|
||||
|
||||
func TestCancelSpotOrder(t *testing.T) {
|
||||
//dsn := "root:root@tcp(192.168.1.12:3306)/gp-bian?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||
//db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
//var apiUserInfo models.LineApiUser
|
||||
//db.Model(&models.LineApiUser{}).Where("id = 21").Find(&apiUserInfo)
|
||||
//api := SpotRestApi{}
|
||||
//dto.CancelOpenOrderReq{
|
||||
// ApiId: 21,
|
||||
// Symbol: "ADAUSDT",
|
||||
// OrderSn: utility.Int64ToString(snowflakehelper.GetOrderId()),
|
||||
// OrderType: 0,
|
||||
//}
|
||||
//api.CancelOpenOrders()
|
||||
}
|
||||
|
||||
func TestCancelAllFutOrder(t *testing.T) {
|
||||
dsn := "root:root@tcp(192.168.1.12:3306)/gp-bian?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
var apiUserInfo models.LineApiUser
|
||||
db.Model(&models.LineApiUser{}).Where("id = 21").Find(&apiUserInfo)
|
||||
api := FutRestApi{}
|
||||
api.CancelAllFutOrder(apiUserInfo, "TRUMPUSDT")
|
||||
}
|
||||
|
||||
//func TestName(t *testing.T) {
|
||||
// api := FutRestApi{}
|
||||
// dsn := "root:root@tcp(192.168.1.12:3306)/gp-bian?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||
// db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
// var apiUserInfo models.LineApiUser
|
||||
// db.Model(&models.LineApiUser{}).Where("id = 21").Find(&apiUserInfo)
|
||||
// api.CancelBatchFutOrder(apiUserInfo, "ADAUSDT",[]{""})
|
||||
//}
|
||||
|
||||
func TestFutOrderPalce(t *testing.T) {
|
||||
api := FutRestApi{}
|
||||
dsn := "root:root@tcp(192.168.1.12:3306)/gp-bian?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
api.OrderPlace(db, FutOrderPlace{
|
||||
ApiId: 21,
|
||||
Symbol: "ADAUSDT",
|
||||
Side: "SELL",
|
||||
Quantity: decimal.NewFromFloat(7),
|
||||
Price: decimal.NewFromFloat(0.9764),
|
||||
SideType: "LIMIT",
|
||||
OpenOrder: 0,
|
||||
Profit: decimal.Zero,
|
||||
StopPrice: decimal.Zero,
|
||||
OrderType: "LIMIT",
|
||||
NewClientOrderId: "367580922570080256",
|
||||
})
|
||||
}
|
||||
|
||||
func TestCancelOpenOrderByOrderSn(t *testing.T) {
|
||||
api := SpotRestApi{}
|
||||
dsn := "root:root@tcp(192.168.1.12:3306)/gp-bian?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
var apiUserInfo models.LineApiUser
|
||||
db.Model(&models.LineApiUser{}).Where("id = ?", 10).Find(&apiUserInfo)
|
||||
err := api.CancelOpenOrderByOrderSn(apiUserInfo, "DOGEUSDT", "367836524202426368")
|
||||
if err != nil {
|
||||
t.Log("err:", err)
|
||||
} else {
|
||||
fmt.Println("成功")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelOpenOrderBySymbol(t *testing.T) {
|
||||
// api := SpotRestApi{}
|
||||
dsn := "root:root@tcp(192.168.1.12:3306)/gp-bian?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
var apiUserInfo models.LineApiUser
|
||||
db.Model(&models.LineApiUser{}).Where("id = ?", 10).Find(&apiUserInfo)
|
||||
err := CancelSpotOrder("ADAUSDT", &apiUserInfo, "SELL")
|
||||
if err != nil {
|
||||
t.Log("err:", err)
|
||||
} else {
|
||||
fmt.Println("成功")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelRedisKeys(t *testing.T) {
|
||||
//初始redis 链接
|
||||
helper.InitDefaultRedis("192.168.1.12:6379", "", 0)
|
||||
helper.InitLockRedisConn("192.168.1.12:6379", "", "0")
|
||||
prefixs := []string{
|
||||
"api_user_hold",
|
||||
"spot_trigger_lock",
|
||||
"fut_trigger_lock",
|
||||
"fut_trigger_stop_lock",
|
||||
"spot_trigger_stop_lock",
|
||||
"spot_addposition_trigger",
|
||||
"fut_addposition_trigger",
|
||||
"spot_hedge_close_position",
|
||||
"futures_hedge_close_position",
|
||||
"spot_callback",
|
||||
"fut_callback",
|
||||
"holde_a",
|
||||
"holde_b",
|
||||
}
|
||||
helper.DefaultRedis.DeleteKeysByPrefix(prefixs...)
|
||||
}
|
||||
|
||||
func TestOpenOrders(t *testing.T) {
|
||||
dsn := "root:root@tcp(192.168.1.12:3306)/gp-bian?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
var apiUserInfo models.LineApiUser
|
||||
db.Model(&models.LineApiUser{}).Where("id = ?", 21).Find(&apiUserInfo)
|
||||
client := GetClient(&apiUserInfo)
|
||||
auth, _, err := client.SendSpotAuth("/api/v3/order", "GET", map[string]string{"symbol": "ADAUSDT", "orderId": "6001232151"})
|
||||
if err != nil {
|
||||
fmt.Println("err:", err)
|
||||
}
|
||||
m := make(map[string]interface{}, 0)
|
||||
sonic.Unmarshal(auth, &m)
|
||||
fmt.Println("m:", m)
|
||||
}
|
||||
|
||||
func TestClosePosition(t *testing.T) {
|
||||
endpoint := "/fapi/v1/order"
|
||||
params := map[string]string{
|
||||
"symbol": "ADAUSDT",
|
||||
"type": "LIMIT",
|
||||
"quantity": "5",
|
||||
"newClientOrderId": utility.Int64ToString(snowflakehelper.GetOrderId()),
|
||||
"positionSide": "SHORT",
|
||||
}
|
||||
params["side"] = "BUY"
|
||||
params["price"] = "0.98"
|
||||
params["timeInForce"] = "GTC"
|
||||
dsn := "root:root@tcp(192.168.1.12:3306)/gp-bian?charset=utf8mb4&parseTime=True&loc=Local&timeout=1000ms"
|
||||
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
var apiUserInfo models.LineApiUser
|
||||
db.Model(&models.LineApiUser{}).Where("id = ?", 21).Find(&apiUserInfo)
|
||||
|
||||
client := GetClient(&apiUserInfo)
|
||||
resp, _, err := client.SendFuturesRequestAuth(endpoint, "POST", params)
|
||||
fmt.Println("resp:", string(resp))
|
||||
fmt.Println("err:", err)
|
||||
|
||||
}
|
||||
331
services/binanceservice/commonservice.go
Normal file
331
services/binanceservice/commonservice.go
Normal file
@ -0,0 +1,331 @@
|
||||
package binanceservice
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
DbModels "go-admin/app/admin/models"
|
||||
"go-admin/app/admin/service/dto"
|
||||
"go-admin/common/const/rediskey"
|
||||
"go-admin/common/global"
|
||||
"go-admin/common/helper"
|
||||
"go-admin/models"
|
||||
"go-admin/pkg/utility"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/go-admin-team/go-admin-core/logger"
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type AddPosition struct {
|
||||
Db *gorm.DB
|
||||
}
|
||||
|
||||
// 获取缓存交易对
|
||||
// symbolType 0-现货 1-合约
|
||||
func GetTradeSet(symbol string, symbolType int) (models.TradeSet, error) {
|
||||
result := models.TradeSet{}
|
||||
val := ""
|
||||
|
||||
switch symbolType {
|
||||
case 0:
|
||||
key := fmt.Sprintf(global.TICKER_SPOT, global.EXCHANGE_BINANCE, symbol)
|
||||
val, _ = helper.DefaultRedis.GetString(key)
|
||||
case 1:
|
||||
key := fmt.Sprintf(global.TICKER_FUTURES, global.EXCHANGE_BINANCE, symbol)
|
||||
val, _ = helper.DefaultRedis.GetString(key)
|
||||
}
|
||||
|
||||
if val != "" {
|
||||
if err := sonic.Unmarshal([]byte(val), &result); err != nil {
|
||||
return result, err
|
||||
}
|
||||
} else {
|
||||
return result, errors.New("未找到交易对信息")
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func GetRisk(futApi *FutRestApi, apiInfo *DbModels.LineApiUser, symbol string) []PositionRisk {
|
||||
for x := 0; x < 5; x++ {
|
||||
risks, _ := futApi.GetPositionV3(apiInfo, symbol)
|
||||
if len(risks) > 0 {
|
||||
return risks
|
||||
}
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
}
|
||||
|
||||
return []PositionRisk{}
|
||||
}
|
||||
|
||||
// CancelFutClosePosition 撤销交易对指定方向的平仓单
|
||||
// apiUserInfo 账户信息
|
||||
// symbol 交易对
|
||||
// side 购买反向
|
||||
// positionSide 仓位方向
|
||||
// side=BUY positionSide=SHORT 就是撤销平空委托
|
||||
// side=BUY&positionSide=LONG是开多,
|
||||
// side=SELL&positionSide=LONG是平多,
|
||||
// side=SELL&positionSide=SHORT是开空,
|
||||
// side=BUY&positionSide=SHORT是平空。
|
||||
func CancelFutClosePosition(apiUserInfo DbModels.LineApiUser, symbol string, side string, positionSide string) error {
|
||||
//查询当前用户的委托订单
|
||||
client := GetClient(&apiUserInfo)
|
||||
resp, _, err := client.SendFuturesRequestAuth("/fapi/v1/openOrders", "GET", map[string]string{
|
||||
"symbol": symbol,
|
||||
"recvWindow": "5000",
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error("撤销平仓单时查询委托失败:", err)
|
||||
return err
|
||||
}
|
||||
var openOrders []OpenOrders
|
||||
sonic.Unmarshal(resp, &openOrders)
|
||||
orderIdList := make([]int, 0)
|
||||
for _, order := range openOrders {
|
||||
if order.Side == side && order.PositionSide == positionSide {
|
||||
orderIdList = append(orderIdList, order.OrderId)
|
||||
}
|
||||
}
|
||||
// 每次取 10 个元素
|
||||
batchSize := 10
|
||||
for i := 0; i < len(orderIdList); i += batchSize {
|
||||
end := i + batchSize
|
||||
if end > len(orderIdList) {
|
||||
end = len(orderIdList) // 避免越界
|
||||
}
|
||||
// 取出当前批次的元素
|
||||
batch := orderIdList[i:end]
|
||||
marshal, _ := sonic.Marshal(&batch)
|
||||
_, _, err = client.SendFuturesRequestAuth("/fapi/v1/batchOrders", "DELETE", map[string]string{
|
||||
"symbol": symbol,
|
||||
"orderIdList": string(marshal),
|
||||
"recvWindow": "5000",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// CancelSpotClosePosition 取消现货平仓单(等同于撤销卖单)
|
||||
// apiUserInfo: Api用户信息
|
||||
// symbol: 交易对
|
||||
func CancelSpotClosePosition(apiUserInfo *DbModels.LineApiUser, symbol string) error {
|
||||
|
||||
return CancelSpotOrder(symbol, apiUserInfo, "SELL")
|
||||
}
|
||||
|
||||
// 取消现货订单
|
||||
func CancelSpotOrder(symbol string, apiUserInfo *DbModels.LineApiUser, side string) error {
|
||||
searchEndpoint := "/api/v3/openOrders"
|
||||
cencelEndpoint := "/api/v3/order"
|
||||
searchParams := map[string]interface{}{
|
||||
"symbol": symbol,
|
||||
}
|
||||
client := GetClient(apiUserInfo)
|
||||
resp, _, err := client.SendSpotAuth(searchEndpoint, "GET", searchParams)
|
||||
db := GetDBConnection()
|
||||
|
||||
if err != nil {
|
||||
if len(resp) > 0 {
|
||||
|
||||
} else {
|
||||
logger.Error("查询现货当前下单失败:", err)
|
||||
}
|
||||
}
|
||||
|
||||
var openOrders []map[string]interface{}
|
||||
err = sonic.Unmarshal(resp, &openOrders)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, order := range openOrders {
|
||||
if orderSymbol, ok := order["symbol"].(string); ok && orderSymbol == symbol {
|
||||
if orderSide, ok := order["side"].(string); ok && orderSide == side {
|
||||
orderId, ok := order["orderId"].(float64)
|
||||
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
orderSn, _ := order["clientOrderId"].(string)
|
||||
params := map[string]string{
|
||||
"symbol": orderSymbol,
|
||||
"orderId": utility.Float64CutString(orderId, 0),
|
||||
// "cancelRestrictions": "ONLY_NEW",
|
||||
}
|
||||
_, _, err = client.SendSpotAuth(cencelEndpoint, "DELETE", params)
|
||||
|
||||
if err != nil {
|
||||
logger.Error("撤销指定现货平仓单失败 ordersn:", orderSn, " orderId:", orderId, " err:", err)
|
||||
} else {
|
||||
if err := db.Model(&DbModels.LinePreOrder{}).Where("order_sn = ? and status !='9'", orderSn).Update("status", "4").Error; err != nil {
|
||||
log.Error("修改止盈单撤销状态失败:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTargetSymbol 获取目标交易对信息
|
||||
func (e *AddPosition) GetTargetSymbol(symbol string, symbolType int) (string, bool, DbModels.LineSymbol, error) {
|
||||
var targetSymbol string
|
||||
var notUsdt bool
|
||||
var symbolInfo DbModels.LineSymbol
|
||||
|
||||
// 处理非 USDT 交易对
|
||||
if !strings.HasSuffix(symbol, "USDT") {
|
||||
notUsdt = true
|
||||
if err := e.Db.Model(&DbModels.LineSymbol{}).Where("symbol = ? AND type = ?", symbol, utility.IntToString(symbolType)).Find(&symbolInfo).Error; err != nil {
|
||||
return "", false, DbModels.LineSymbol{}, err
|
||||
}
|
||||
if symbolInfo.Id <= 0 {
|
||||
return "", false, DbModels.LineSymbol{}, fmt.Errorf("未找到交易对信息")
|
||||
}
|
||||
targetSymbol = symbolInfo.BaseAsset + "USDT"
|
||||
} else {
|
||||
targetSymbol = symbol
|
||||
}
|
||||
|
||||
return targetSymbol, notUsdt, symbolInfo, nil
|
||||
}
|
||||
|
||||
func (e *AddPosition) GetOrderInfo(req dto.ManuallyCover, symbol, orderType, site, status string) (DbModels.LinePreOrder, error) {
|
||||
var orderInfo DbModels.LinePreOrder
|
||||
if err := e.Db.Model(DbModels.LinePreOrder{}).Where("api_id = ? AND symbol = ? AND order_type = ? AND site = ? AND status = ?", req.ApiId, symbol, orderType, site, status).Find(&orderInfo).Error; err != nil {
|
||||
return DbModels.LinePreOrder{}, err
|
||||
}
|
||||
if orderInfo.Id <= 0 {
|
||||
return DbModels.LinePreOrder{}, fmt.Errorf("未找到主仓信息")
|
||||
}
|
||||
return orderInfo, nil
|
||||
}
|
||||
|
||||
func (e *AddPosition) GetFutOrderInfo(req dto.ManuallyCover, symbol, orderType, status string) (DbModels.LinePreOrder, error) {
|
||||
var orderInfo DbModels.LinePreOrder
|
||||
if err := e.Db.Model(DbModels.LinePreOrder{}).Where("api_id = ? AND symbol = ? AND order_type = ? AND status = ? AND cover_type = 2", req.ApiId, symbol, orderType, status).Find(&orderInfo).Error; err != nil {
|
||||
return DbModels.LinePreOrder{}, err
|
||||
}
|
||||
if orderInfo.Id <= 0 {
|
||||
return DbModels.LinePreOrder{}, fmt.Errorf("未找到主仓信息")
|
||||
}
|
||||
return orderInfo, nil
|
||||
}
|
||||
|
||||
// GetFutSpotOrderInfo 获取合约对现货的订单信息
|
||||
func (e *AddPosition) GetFutSpotOrderInfo(req dto.ManuallyCover, symbol, orderType, status string) (DbModels.LinePreOrder, error) {
|
||||
var orderInfo DbModels.LinePreOrder
|
||||
if err := e.Db.Model(DbModels.LinePreOrder{}).Where("api_id = ? AND symbol = ? AND order_type = ? AND status = ? AND cover_type = 3", req.ApiId, symbol, orderType, status).Find(&orderInfo).Error; err != nil {
|
||||
return DbModels.LinePreOrder{}, err
|
||||
}
|
||||
if orderInfo.Id <= 0 {
|
||||
return DbModels.LinePreOrder{}, fmt.Errorf("未找到主仓信息")
|
||||
}
|
||||
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
|
||||
// coverType 1现货->合约 2->合约->合约 3合约->现货
|
||||
func MainClosePositionClearCache(mainOrderId int, coverType int) {
|
||||
if coverType == 1 {
|
||||
spotStopArray, _ := helper.DefaultRedis.GetAllList(rediskey.SpotStopLossList)
|
||||
spotAddpositionArray, _ := helper.DefaultRedis.GetAllList(rediskey.SpotAddPositionList)
|
||||
var position AddPositionList
|
||||
var stop dto.StopLossRedisList
|
||||
|
||||
for _, item := range spotAddpositionArray {
|
||||
if err := sonic.Unmarshal([]byte(item), &position); err != nil {
|
||||
log.Error("MainClosePositionClearCache Unmarshal err:", err)
|
||||
}
|
||||
|
||||
if position.Pid == mainOrderId {
|
||||
helper.DefaultRedis.LRem(rediskey.SpotAddPositionList, item)
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range spotStopArray {
|
||||
if err := sonic.Unmarshal([]byte(item), &stop); err != nil {
|
||||
log.Error("MainClosePositionClearCache Unmarshal err:", err)
|
||||
}
|
||||
|
||||
if stop.PId == mainOrderId {
|
||||
helper.DefaultRedis.LRem(rediskey.SpotStopLossList, item)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
futAddpositionArray, _ := helper.DefaultRedis.GetAllList(rediskey.FuturesAddPositionList)
|
||||
futStopArray, _ := helper.DefaultRedis.GetAllList(rediskey.FuturesStopLossList)
|
||||
var position AddPositionList
|
||||
var stop dto.StopLossRedisList
|
||||
|
||||
for _, item := range futAddpositionArray {
|
||||
if err := sonic.Unmarshal([]byte(item), &position); err != nil {
|
||||
log.Error("MainClosePositionClearCache Unmarshal err:", err)
|
||||
}
|
||||
|
||||
if position.Pid == mainOrderId {
|
||||
helper.DefaultRedis.LRem(rediskey.FuturesAddPositionList, item)
|
||||
}
|
||||
}
|
||||
|
||||
for _, item := range futStopArray {
|
||||
if err := sonic.Unmarshal([]byte(item), &stop); err != nil {
|
||||
log.Error("MainClosePositionClearCache Unmarshal err:", err)
|
||||
}
|
||||
|
||||
if stop.PId == mainOrderId {
|
||||
helper.DefaultRedis.LRem(rediskey.FuturesStopLossList, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1134
services/binanceservice/futuresbinancerest.go
Normal file
1134
services/binanceservice/futuresbinancerest.go
Normal file
File diff suppressed because it is too large
Load Diff
62
services/binanceservice/futuresrest.go
Normal file
62
services/binanceservice/futuresrest.go
Normal file
@ -0,0 +1,62 @@
|
||||
package binanceservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"go-admin/common/const/rediskey"
|
||||
"go-admin/common/helper"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/go-admin-team/go-admin-core/logger"
|
||||
)
|
||||
|
||||
/*
|
||||
修改订单信息
|
||||
*/
|
||||
func ChangeFutureOrder(mapData map[string]interface{}) {
|
||||
// 检查订单号是否存在
|
||||
orderSn, ok := mapData["c"]
|
||||
if !ok {
|
||||
logger.Error("合约订单回调失败,没有订单号")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取数据库连接
|
||||
db := GetDBConnection()
|
||||
if db == nil {
|
||||
logger.Error("合约订单回调失败,无法获取数据库连接")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取 Redis 锁
|
||||
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotCallBack, orderSn), 10, 5, 500*time.Millisecond)
|
||||
acquired, err := lock.AcquireWait(context.Background())
|
||||
if err != nil {
|
||||
logger.Error("合约订单回调失败,获取锁失败:", orderSn, " err:", err)
|
||||
return
|
||||
}
|
||||
if !acquired {
|
||||
logger.Error("合约订单回调失败,获取锁失败:", orderSn)
|
||||
return
|
||||
}
|
||||
defer lock.Release()
|
||||
|
||||
// 查询订单
|
||||
preOrder, err := getPreOrder(db, orderSn)
|
||||
if err != nil {
|
||||
logger.Error("合约订单回调失败,查询订单失败:", orderSn, " err:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析订单状态
|
||||
status, ok := mapData["X"].(string)
|
||||
if !ok {
|
||||
mapStr, _ := sonic.Marshal(&mapData)
|
||||
logger.Error("订单回调失败,没有状态:", string(mapStr))
|
||||
return
|
||||
}
|
||||
|
||||
//todo
|
||||
|
||||
}
|
||||
228
services/binanceservice/models.go
Normal file
228
services/binanceservice/models.go
Normal file
@ -0,0 +1,228 @@
|
||||
package binanceservice
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// OrderPlacementService 币安现货下单
|
||||
type OrderPlacementService struct {
|
||||
ApiId int `json:"api_id"` //api_id
|
||||
Symbol string `json:"symbol"` //交易对
|
||||
Side string `json:"side"` //购买方向
|
||||
Type string `json:"type"` //下单类型 MARKET=市价 LIMIT=限价 TAKE_PROFIT_LIMIT=限价止盈 STOP_LOSS_LIMIT=限价止损
|
||||
TimeInForce string `json:"timeInForce"` // 订单有效期,默认为 GTC (Good Till Cancelled)
|
||||
Price decimal.Decimal `json:"price"` //限价单价
|
||||
Quantity decimal.Decimal `json:"quantity"` //下单数量
|
||||
NewClientOrderId string `json:"newClientOrderId"` //系统生成的订单号
|
||||
StopPrice decimal.Decimal `json:"stopprice"` //止盈止损时需要
|
||||
Rate string `json:"rate"` //下单百分比
|
||||
}
|
||||
|
||||
func (s *OrderPlacementService) CheckParams() error {
|
||||
|
||||
if s.ApiId == 0 || s.Symbol == "" || s.Type == "" || s.NewClientOrderId == "" || s.Side == "" || s.Quantity.LessThan(decimal.Zero) {
|
||||
return errors.New("缺失下单必要参数")
|
||||
}
|
||||
|
||||
if s.Type == "LIMIT" && s.Price.LessThanOrEqual(decimal.Zero) {
|
||||
return errors.New("缺失限价单参数price")
|
||||
}
|
||||
if s.Type == "TAKE_PROFIT_LIMIT" || s.Type == "STOP_LOSS_LIMIT" {
|
||||
if s.StopPrice.LessThanOrEqual(decimal.Zero) {
|
||||
return errors.New("缺失止盈止损订单参数stopprice")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CancelOpenOrdersReq 撤销单一交易对的所有挂单
|
||||
type CancelOpenOrdersReq struct {
|
||||
ApiId int `json:"api_id"` //api_id
|
||||
Symbol string `json:"symbol"` //交易对
|
||||
}
|
||||
|
||||
func (r *CancelOpenOrdersReq) CheckParams() error {
|
||||
if r.Symbol == "" {
|
||||
return errors.New("缺失下单必要参数")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EntryPriceResult 计算均价结果
|
||||
type EntryPriceResult struct {
|
||||
TotalNum decimal.Decimal `json:"total_num"` //总数量
|
||||
EntryPrice decimal.Decimal `json:"entry_price"` //均价
|
||||
FirstPrice decimal.Decimal `json:"first_price"` //主单的下单价格
|
||||
TotalMoney decimal.Decimal `json:"total_money"` //总金额(U)
|
||||
FirstId int `json:"first_id"` //主单id
|
||||
}
|
||||
|
||||
type FutOrderPlace struct {
|
||||
ApiId int `json:"api_id"` //api用户id
|
||||
Symbol string `json:"symbol"` //合约交易对
|
||||
Side string `json:"side"` //购买方向
|
||||
Quantity decimal.Decimal `json:"quantity"` //数量
|
||||
Price decimal.Decimal `json:"price"` //限价单价
|
||||
SideType string `json:"side_type"` //现价或者市价
|
||||
OpenOrder int `json:"open_order"` //是否开启限价单止盈止损
|
||||
Profit decimal.Decimal `json:"profit"` //止盈价格
|
||||
StopPrice decimal.Decimal `json:"stopprice"` //止损价格
|
||||
OrderType string `json:"order_type"` //订单类型,市价或限价MARKET(市价单) TAKE_PROFIT_MARKET(止盈) STOP_MARKET(止损)
|
||||
NewClientOrderId string `json:"newClientOrderId"`
|
||||
}
|
||||
|
||||
func (s FutOrderPlace) CheckParams() error {
|
||||
if s.ApiId == 0 || s.Symbol == "" || s.OrderType == "" || s.NewClientOrderId == "" || s.Side == "" || s.Quantity.LessThan(decimal.Zero) {
|
||||
return errors.New("缺失下单必要参数")
|
||||
}
|
||||
if s.OrderType == "LIMIT" && s.Price.LessThan(decimal.Zero) {
|
||||
return errors.New("缺失限价单参数price")
|
||||
}
|
||||
if s.OrderType == "TAKE_PROFIT_MARKET" || s.OrderType == "STOP_MARKET" {
|
||||
if s.StopPrice.LessThanOrEqual(decimal.Zero) || s.Profit.LessThanOrEqual(decimal.Zero) {
|
||||
return errors.New("缺失止盈止损订单参数stopprice")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PositionRisk 用户持仓风险
|
||||
type PositionRisk struct {
|
||||
Symbol string `json:"symbol"`
|
||||
PositionSide string `json:"positionSide"`
|
||||
PositionAmt string `json:"positionAmt"`
|
||||
EntryPrice string `json:"entryPrice"`
|
||||
BreakEvenPrice string `json:"breakEvenPrice"`
|
||||
MarkPrice string `json:"markPrice"`
|
||||
UnRealizedProfit string `json:"unRealizedProfit"`
|
||||
LiquidationPrice string `json:"liquidationPrice"`
|
||||
IsolatedMargin string `json:"isolatedMargin"`
|
||||
Notional string `json:"notional"`
|
||||
MarginAsset string `json:"marginAsset"`
|
||||
IsolatedWallet string `json:"isolatedWallet"`
|
||||
InitialMargin string `json:"initialMargin"`
|
||||
MaintMargin string `json:"maintMargin"`
|
||||
PositionInitialMargin string `json:"positionInitialMargin"`
|
||||
OpenOrderInitialMargin string `json:"openOrderInitialMargin"`
|
||||
Adl int `json:"adl"`
|
||||
BidNotional string `json:"bidNotional"`
|
||||
AskNotional string `json:"askNotional"`
|
||||
UpdateTime int64 `json:"updateTime"`
|
||||
}
|
||||
type FutOrderResp struct {
|
||||
ClientOrderId string `json:"clientOrderId"`
|
||||
CumQty string `json:"cumQty"`
|
||||
CumQuote string `json:"cumQuote"`
|
||||
ExecutedQty string `json:"executedQty"`
|
||||
OrderId int `json:"orderId"`
|
||||
AvgPrice string `json:"avgPrice"`
|
||||
OrigQty string `json:"origQty"`
|
||||
Price string `json:"price"`
|
||||
ReduceOnly bool `json:"reduceOnly"`
|
||||
Side string `json:"side"`
|
||||
PositionSide string `json:"positionSide"`
|
||||
Status string `json:"status"`
|
||||
StopPrice string `json:"stopPrice"`
|
||||
ClosePosition bool `json:"closePosition"`
|
||||
Symbol string `json:"symbol"`
|
||||
TimeInForce string `json:"timeInForce"`
|
||||
Type string `json:"type"`
|
||||
OrigType string `json:"origType"`
|
||||
ActivatePrice string `json:"activatePrice"`
|
||||
PriceRate string `json:"priceRate"`
|
||||
UpdateTime int64 `json:"updateTime"`
|
||||
WorkingType string `json:"workingType"`
|
||||
PriceProtect bool `json:"priceProtect"`
|
||||
PriceMatch string `json:"priceMatch"`
|
||||
SelfTradePreventionMode string `json:"selfTradePreventionMode"`
|
||||
GoodTillDate int64 `json:"goodTillDate"`
|
||||
}
|
||||
|
||||
type HoldeData struct {
|
||||
Id int `json:"id"` //主单id
|
||||
UpdateTime time.Time `json:"updateTime" comment:"最后更新时间"`
|
||||
Type string `json:"type" comment:"1-现货 2-合约"`
|
||||
Symbol string `json:"symbol"`
|
||||
AveragePrice decimal.Decimal `json:"averagePrice" comment:"均价"`
|
||||
Side string `json:"side" comment:"持仓方向"`
|
||||
TotalQuantity decimal.Decimal `json:"totalQuantity" comment:"持仓数量"`
|
||||
TotalBuyPrice decimal.Decimal `json:"totalBuyPrice" comment:"总购买金额"`
|
||||
PositionIncrementCount int `json:"positionIncrementCount"` //加仓次数
|
||||
HedgeCloseCount int `json:"hedgeCloseCount" comment:"对冲平仓数量"`
|
||||
TriggerStatus int `json:"triggerStatus" comment:"触发状态 平仓之后重置 0-未触发 1-触发中 2-触发完成"`
|
||||
PositionStatus int `json:"positionStatus" comment:"加仓状态 0-未开始 1-未完成 2-已完成 3-失败"`
|
||||
}
|
||||
|
||||
// OpenOrders 挂单信息
|
||||
type OpenOrders struct {
|
||||
AvgPrice string `json:"avgPrice"` // 平均成交价
|
||||
ClientOrderId string `json:"clientOrderId"` // 用户自定义的订单号
|
||||
CumQuote string `json:"cumQuote"` // 成交金额
|
||||
ExecutedQty string `json:"executedQty"` // 成交量
|
||||
OrderId int `json:"orderId"` // 系统订单号
|
||||
OrigQty string `json:"origQty"` // 原始委托数量
|
||||
OrigType string `json:"origType"` // 触发前订单类型
|
||||
Price string `json:"price"` // 委托价格
|
||||
ReduceOnly bool `json:"reduceOnly"` // 是否仅减仓
|
||||
Side string `json:"side"` // 买卖方向
|
||||
PositionSide string `json:"positionSide"` // 持仓方向
|
||||
Status string `json:"status"` // 订单状态
|
||||
StopPrice string `json:"stopPrice"` // 触发价,对`TRAILING_STOP_MARKET`无效
|
||||
ClosePosition bool `json:"closePosition"` // 是否条件全平仓
|
||||
Symbol string `json:"symbol"` // 交易对
|
||||
Time int64 `json:"time"` // 订单时间
|
||||
TimeInForce string `json:"timeInForce"` // 有效方法
|
||||
Type string `json:"type"` // 订单类型
|
||||
ActivatePrice string `json:"activatePrice"` // 跟踪止损激活价格, 仅`TRAILING_STOP_MARKET` 订单返回此字段
|
||||
PriceRate string `json:"priceRate"` // 跟踪止损回调比例, 仅`TRAILING_STOP_MARKET` 订单返回此字段
|
||||
UpdateTime int64 `json:"updateTime"` // 更新时间
|
||||
WorkingType string `json:"workingType"` // 条件价格触发类型
|
||||
PriceProtect bool `json:"priceProtect"` // 是否开启条件单触发保护
|
||||
PriceMatch string `json:"priceMatch"` //price match mode
|
||||
SelfTradePreventionMode string `json:"selfTradePreventionMode"` //self trading preventation mode
|
||||
GoodTillDate int `json:"goodTillDate"` //order pre-set auot cancel time for TIF GTD order
|
||||
}
|
||||
|
||||
// 待触发加仓单
|
||||
type AddPositionList struct {
|
||||
Pid int `json:"pid"` //主单id
|
||||
ApiId int `json:"apiId"` //触发账户id
|
||||
Symbol string `json:"symbol"` //交易对
|
||||
Price decimal.Decimal `json:"price"` //触发价
|
||||
Side string `json:"side"` //买卖方向
|
||||
AddPositionMainType string `json:"addPositionType"` //A账号加仓类型
|
||||
AddPositionHedgeType string `json:"addPositionHedgeType"` //B账号加仓类型
|
||||
SymbolType int `json:"type" comment:"交易对类别 1-现货 2-合约"`
|
||||
}
|
||||
|
||||
// SpotAccountInfo 现货账户信息
|
||||
type SpotAccountInfo struct {
|
||||
MakerCommission int `json:"makerCommission"`
|
||||
TakerCommission int `json:"takerCommission"`
|
||||
BuyerCommission int `json:"buyerCommission"`
|
||||
SellerCommission int `json:"sellerCommission"`
|
||||
CommissionRates struct {
|
||||
Maker string `json:"maker"`
|
||||
Taker string `json:"taker"`
|
||||
Buyer string `json:"buyer"`
|
||||
Seller string `json:"seller"`
|
||||
} `json:"commissionRates"`
|
||||
CanTrade bool `json:"canTrade"`
|
||||
CanWithdraw bool `json:"canWithdraw"`
|
||||
CanDeposit bool `json:"canDeposit"`
|
||||
Brokered bool `json:"brokered"`
|
||||
RequireSelfTradePrevention bool `json:"requireSelfTradePrevention"`
|
||||
PreventSor bool `json:"preventSor"`
|
||||
UpdateTime int `json:"updateTime"`
|
||||
AccountType string `json:"accountType"`
|
||||
Balances []struct {
|
||||
Asset string `json:"asset"`
|
||||
Free string `json:"free"`
|
||||
Locked string `json:"locked"`
|
||||
} `json:"balances"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Uid int `json:"uid"`
|
||||
}
|
||||
103
services/binanceservice/orderservice.go
Normal file
103
services/binanceservice/orderservice.go
Normal file
@ -0,0 +1,103 @@
|
||||
package binanceservice
|
||||
|
||||
import (
|
||||
"go-admin/app/admin/models"
|
||||
DbModels "go-admin/app/admin/models"
|
||||
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 获取订单明细
|
||||
func GetOrderById(db *gorm.DB, id int) (DbModels.LinePreOrder, error) {
|
||||
result := DbModels.LinePreOrder{}
|
||||
|
||||
if err := db.Model(&result).Where("id =?", id).First(&result).Error; err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 获取已开仓的对冲单、对冲加仓单id
|
||||
// pid:主单id
|
||||
// coverType:对冲类型 1-现货对合约 2-合约对合约 3-合约对现货
|
||||
func GetHedgeOpenOrderIds(db *gorm.DB, pid int, coverType int) ([]int, error) {
|
||||
result := make([]DbModels.LinePreOrder, 0)
|
||||
resultIds := make([]int, 0)
|
||||
query := db.Model(&result)
|
||||
|
||||
switch coverType {
|
||||
case 3:
|
||||
query = query.Where("pid =? AND order_type in ('7','8','10','11') AND operate_type =1 AND status in ('9','13')", pid)
|
||||
case 2, 1:
|
||||
query = query.Where("pid =? AND order_type in ('10','11') AND operate_type =1 AND status ='13'", pid)
|
||||
}
|
||||
|
||||
if err := query.Select("id").Find(&result).Error; err != nil {
|
||||
return resultIds, err
|
||||
}
|
||||
|
||||
for _, v := range result {
|
||||
resultIds = append(resultIds, v.Id)
|
||||
}
|
||||
|
||||
return resultIds, nil
|
||||
}
|
||||
|
||||
// 获得对冲单
|
||||
func GetHedgeOpenOrder(db *gorm.DB, pid int, coverType int) (DbModels.LinePreOrder, error) {
|
||||
result := DbModels.LinePreOrder{}
|
||||
orderType := ""
|
||||
|
||||
switch coverType {
|
||||
case 1:
|
||||
orderType = "7"
|
||||
case 2, 3:
|
||||
orderType = "10"
|
||||
}
|
||||
|
||||
if err := db.Model(&result).Where("pid =? AND order_type =? AND operate_type =1", pid, orderType).First(&result).Error; err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 获取止损单
|
||||
func GetStopOrder(db *gorm.DB, pid int) (DbModels.LinePreOrder, error) {
|
||||
result := DbModels.LinePreOrder{}
|
||||
|
||||
if err := db.Model(&result).Where("pid =? AND order_type in ('4','6')", pid).First(&result).Error; err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 获取最后一条对冲的下单百分比
|
||||
func GetStopOrderRate(db *gorm.DB, pid int) (decimal.Decimal, error) {
|
||||
var result decimal.Decimal
|
||||
|
||||
if err := db.Model(&DbModels.LinePreOrder{}).Where("pid =? AND order_type in ('7','10')", pid).
|
||||
Select("rate").
|
||||
Order("id DESC").
|
||||
First(&result).Error; err != nil {
|
||||
return result, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 获取最后一条对冲
|
||||
func GetLastStop(db *gorm.DB, pid int) (DbModels.LinePreOrder, error) {
|
||||
result := models.LinePreOrder{}
|
||||
|
||||
if err := db.Model(&result).
|
||||
Joins("JOIN line_pre_order as o ON o.id = line_pre_order.pid AND o.status in ('9','13')").
|
||||
Where("line_pre_order.pid =? AND line_pre_order.order_type in ('7','10')", pid).
|
||||
Order("line_pre_order.id DESC").Select("line_pre_order.*").First(&result).Error; err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
432
services/binanceservice/spotreset.go
Normal file
432
services/binanceservice/spotreset.go
Normal file
@ -0,0 +1,432 @@
|
||||
package binanceservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"go-admin/app/admin/models"
|
||||
DbModels "go-admin/app/admin/models"
|
||||
"go-admin/common/const/rediskey"
|
||||
"go-admin/common/helper"
|
||||
"go-admin/pkg/utility"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"github.com/go-admin-team/go-admin-core/logger"
|
||||
"github.com/shopspring/decimal"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
/*
|
||||
订单回调
|
||||
*/
|
||||
func ChangeSpotOrder(mapData map[string]interface{}) {
|
||||
// 检查订单号是否存在
|
||||
orderSn, ok := mapData["c"]
|
||||
if !ok {
|
||||
logger.Error("订单回调失败, 没有订单号", mapData)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取数据库连接
|
||||
db := GetDBConnection()
|
||||
if db == nil {
|
||||
logger.Error("订单回调失败, 无法获取数据库连接")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取 Redis 锁
|
||||
lockKey := fmt.Sprintf(rediskey.SpotCallBack, orderSn)
|
||||
lock := helper.NewRedisLock(lockKey, 200, 5, 100*time.Millisecond)
|
||||
if err := acquireLock(lock, orderSn); err != nil {
|
||||
return
|
||||
}
|
||||
defer lock.Release()
|
||||
|
||||
// 查询订单
|
||||
preOrder, err := getPreOrder(db, orderSn)
|
||||
if err != nil {
|
||||
logger.Error("订单回调失败, 查询订单失败:", orderSn, " err:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析订单状态
|
||||
status, ok := mapData["X"]
|
||||
if !ok {
|
||||
logMapData(mapData)
|
||||
return
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
orderStatus, reason := parseOrderStatus(preOrder, status, mapData)
|
||||
|
||||
if orderStatus == 0 {
|
||||
logger.Error("订单回调失败,状态错误:", orderSn, " status:", status, " reason:", reason)
|
||||
return
|
||||
}
|
||||
|
||||
if err := updateOrderStatus(db, preOrder, orderStatus, reason, true, mapData); err != nil {
|
||||
logger.Error("修改订单状态失败:", orderSn, " err:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 根据订单类型和状态处理逻辑
|
||||
handleOrderByType(db, preOrder, orderStatus)
|
||||
}
|
||||
|
||||
// 获取 Redis 锁
|
||||
func acquireLock(lock *helper.RedisLock, orderSn interface{}) error {
|
||||
acquired, err := lock.AcquireWait(context.Background())
|
||||
if err != nil {
|
||||
logger.Error("订单回调失败, 获取锁失败:", orderSn, " err:", err)
|
||||
return err
|
||||
}
|
||||
if !acquired {
|
||||
logger.Error("订单回调失败, 获取锁失败:", orderSn)
|
||||
return fmt.Errorf("failed to acquire lock")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 记录 mapData 数据
|
||||
func logMapData(mapData map[string]interface{}) {
|
||||
mapStr, _ := sonic.Marshal(&mapData)
|
||||
logger.Error("订单回调失败, 没有状态:", string(mapStr))
|
||||
}
|
||||
|
||||
// 根据订单类型和状态处理逻辑
|
||||
func handleOrderByType(db *gorm.DB, preOrder *DbModels.LinePreOrder, orderStatus int) {
|
||||
switch {
|
||||
// 主单成交
|
||||
case preOrder.OrderType == 0 && (orderStatus == 9 || orderStatus == 6):
|
||||
handleMainOrderFilled(db, preOrder)
|
||||
|
||||
//主单取消
|
||||
case preOrder.OrderType == 0 && preOrder.Pid == 0 && orderStatus == 4:
|
||||
coin := utility.ReplaceSuffix(preOrder.Symbol, preOrder.QuoteSymbol, "")
|
||||
removeHoldingCache(preOrder.ApiId, coin, preOrder.Id)
|
||||
|
||||
// 止盈成交
|
||||
case preOrder.OrderType == 1 && (orderStatus == 9 || orderStatus == 6):
|
||||
handleSpotTakeProfitFilled(db, preOrder)
|
||||
|
||||
//主单平仓
|
||||
case preOrder.OrderType == 3 && orderStatus == 9:
|
||||
handleMainOrderClosePosition(db, preOrder)
|
||||
}
|
||||
}
|
||||
|
||||
func handleMainOrderClosePosition(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func handleSpotTakeProfitFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func removeHoldingCache(i1 int, coin string, i2 int) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
func handleMainOrderFilled(db *gorm.DB, preOrder *DbModels.LinePreOrder) {
|
||||
panic("unimplemented")
|
||||
}
|
||||
|
||||
// 解析订单状态
|
||||
// 5:委托中 9:完全成交 4:取消
|
||||
func parseOrderStatus(preOrder *DbModels.LinePreOrder, status interface{}, mapData map[string]interface{}) (int, string) {
|
||||
reason, _ := mapData["r"].(string)
|
||||
|
||||
if strings.ToLower(reason) == "none" {
|
||||
reason = ""
|
||||
}
|
||||
|
||||
switch status {
|
||||
case "NEW": // 未成交
|
||||
return 5, reason
|
||||
case "FILLED": // 完全成交
|
||||
if preOrder.OrderType < 3 {
|
||||
return 6, reason
|
||||
}
|
||||
|
||||
return 9, reason
|
||||
case "CANCELED", "EXPIRED": // 取消
|
||||
return 4, reason
|
||||
default:
|
||||
return 0, reason
|
||||
}
|
||||
}
|
||||
|
||||
// 更新订单状态
|
||||
func updateOrderStatus(db *gorm.DB, preOrder *models.LinePreOrder, status int, reason string, isSpot bool, mapData map[string]interface{}) error {
|
||||
params := map[string]interface{}{"status": strconv.Itoa(status), "desc": reason}
|
||||
|
||||
switch isSpot {
|
||||
case true:
|
||||
total := decimal.Zero
|
||||
totalAmount := decimal.Zero
|
||||
if totalStr, ok := mapData["Z"].(string); ok {
|
||||
total, _ = decimal.NewFromString(totalStr)
|
||||
}
|
||||
if totalAmountStr, ok := mapData["z"].(string); ok {
|
||||
totalAmount, _ = decimal.NewFromString(totalAmountStr)
|
||||
}
|
||||
|
||||
//主单 修改单价 和成交数量
|
||||
if total.Cmp(decimal.Zero) > 0 && totalAmount.Cmp(decimal.Zero) > 0 {
|
||||
num := totalAmount.Div(decimal.NewFromFloat(100)).Mul(decimal.NewFromFloat(99.8))
|
||||
params["num"] = num
|
||||
params["price"] = total.Div(totalAmount)
|
||||
preOrder.Num = num.String()
|
||||
}
|
||||
case false:
|
||||
status, _ := mapData["X"].(string)
|
||||
|
||||
if status == "FILLED" {
|
||||
num, _ := decimal.NewFromString(mapData["z"].(string))
|
||||
params["num"] = num.Mul(decimal.NewFromFloat(0.998)).String()
|
||||
params["price"], _ = mapData["ap"].(string)
|
||||
|
||||
preOrder.Num = num.String()
|
||||
}
|
||||
}
|
||||
|
||||
return db.Model(&DbModels.LinePreOrder{}).Where("order_sn = ? AND status < 6", preOrder.OrderSn).
|
||||
Updates(params).Error
|
||||
}
|
||||
|
||||
// 主单成交 处理止盈止损订单
|
||||
func processTakeProfitAndStopLossOrders(db *gorm.DB, preOrder *models.LinePreOrder) {
|
||||
orders := []models.LinePreOrder{}
|
||||
if err := db.Model(&DbModels.LinePreOrder{}).Where("pid = ? AND order_type >0 AND status = '0' ", preOrder.Id).Find(&orders).Error; err != nil {
|
||||
logger.Error("订单回调查询止盈止损单失败:", err)
|
||||
return
|
||||
}
|
||||
|
||||
spotApi := SpotRestApi{}
|
||||
num, _ := decimal.NewFromString(preOrder.Num)
|
||||
|
||||
for i, order := range orders {
|
||||
if i >= 2 { // 最多处理 2 个订单
|
||||
break
|
||||
}
|
||||
|
||||
switch order.OrderType {
|
||||
case 1: // 止盈
|
||||
processTakeProfitOrder(db, spotApi, order, num)
|
||||
case 2: // 止损
|
||||
processStopLossOrder(db, order)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理止盈订单
|
||||
func processTakeProfitOrder(db *gorm.DB, spotApi SpotRestApi, order models.LinePreOrder, num decimal.Decimal) {
|
||||
tradeSet, _ := GetTradeSet(order.Symbol, 0)
|
||||
|
||||
if tradeSet.Coin == "" {
|
||||
logger.Error("获取交易对失败")
|
||||
return
|
||||
}
|
||||
|
||||
price, _ := decimal.NewFromString(order.Price)
|
||||
// num, _ := decimal.NewFromString(order.Num)
|
||||
|
||||
params := OrderPlacementService{
|
||||
ApiId: order.ApiId,
|
||||
Symbol: order.Symbol,
|
||||
Side: order.Site,
|
||||
Price: price.Truncate(int32(tradeSet.PriceDigit)),
|
||||
Quantity: num.Truncate(int32(tradeSet.AmountDigit)),
|
||||
Type: "TAKE_PROFIT_LIMIT",
|
||||
TimeInForce: "GTC",
|
||||
StopPrice: price.Truncate(int32(tradeSet.PriceDigit)),
|
||||
NewClientOrderId: order.OrderSn,
|
||||
}
|
||||
|
||||
err := spotApi.OrderPlace(db, params)
|
||||
|
||||
if err != nil {
|
||||
for x := 0; x < 5; x++ {
|
||||
if strings.Contains(err.Error(), "LOT_SIZE") {
|
||||
break
|
||||
}
|
||||
|
||||
err = spotApi.OrderPlace(db, params)
|
||||
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error("现货止盈下单失败:", order.OrderSn, " err:", err)
|
||||
if err := db.Model(&DbModels.LinePreOrder{}).Where("id = ?", order.Id).
|
||||
Updates(map[string]interface{}{"status": "2", "desc": err.Error()}).Error; err != nil {
|
||||
logger.Error("现货止盈下单失败,更新状态失败:", order.OrderSn, " err:", err)
|
||||
}
|
||||
} else {
|
||||
if err := db.Model(&DbModels.LinePreOrder{}).Where("id = ? and status ='0'", order.Id).
|
||||
Updates(map[string]interface{}{"status": "1", "num": num.String()}).Error; err != nil {
|
||||
logger.Error("现货止盈下单成功,更新状态失败:", order.OrderSn, " err:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理止损订单
|
||||
// order 止损单
|
||||
func processStopLossOrder(db *gorm.DB, order models.LinePreOrder) error {
|
||||
// var stopOrder models.LinePreOrder
|
||||
// orderTypes := []string{"4", "6", "9", "12"}
|
||||
// parentId := order.Id
|
||||
|
||||
// if order.Pid > 0 {
|
||||
// parentId = order.Pid
|
||||
// }
|
||||
|
||||
// if utility.ContainsStr(orderTypes, order.OrderType) {
|
||||
// var err error
|
||||
// stopOrder, err = GetStopOrder(db, order.Pid)
|
||||
|
||||
// if err != nil {
|
||||
// logger.Error("查询止损单失败:", err)
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
|
||||
// price, _ := decimal.NewFromString(stopOrder.Price)
|
||||
// stoploss, _ := decimal.NewFromString(order.Rate)
|
||||
|
||||
// if holdeB.Id > 0 {
|
||||
// _, holdeA := GetHoldeA(stopOrder.Pid)
|
||||
// var percent decimal.Decimal
|
||||
// lastPercent, _ := GetStopOrderRate(db, stopOrder.Pid)
|
||||
|
||||
// if stopOrder.Site == "BUY" {
|
||||
// //平仓次数>=最大次数 且余数为0 重新计算触发对冲百分比
|
||||
// // if holdeA.Id > 0 && holdeB.HedgeCloseCount >= stopOrder.HedgeCloseCount && (holdeB.HedgeCloseCount%stopOrder.HedgeCloseCount == 0) {
|
||||
// rand := getRand(stopOrder.HedgeTriggerPercent, stopOrder.HedgeTriggerPercentMax, lastPercent, 1)
|
||||
|
||||
// percent = decimal.NewFromInt(100).Add(rand).Div(decimal.NewFromInt(100))
|
||||
// // } else {
|
||||
// // rate, _ := decimal.NewFromString(stopOrder.Rate)
|
||||
// // percent = decimal.NewFromInt(100).Add(rate).Div(decimal.NewFromInt(100))
|
||||
// // }
|
||||
// } else {
|
||||
// // if holdeA.Id > 0 && holdeB.HedgeCloseCount >= stopOrder.HedgeCloseCount {
|
||||
// rand := getRand(stopOrder.HedgeTriggerPercent, stopOrder.HedgeTriggerPercentMax, lastPercent, 1)
|
||||
|
||||
// percent = decimal.NewFromInt(100).Sub(rand).Div(decimal.NewFromInt(100))
|
||||
// // } else {
|
||||
// // rate, _ := decimal.NewFromString(stopOrder.Rate)
|
||||
// // percent = decimal.NewFromInt(100).Sub(rate).Div(decimal.NewFromInt(100))
|
||||
// // }
|
||||
// }
|
||||
// stoploss = decimal.NewFromInt(100).Sub(percent.Mul(decimal.NewFromInt(100))).Truncate(2)
|
||||
// price = holdeA.AveragePrice.Mul(percent)
|
||||
// }
|
||||
|
||||
// tradeset, _ := GetTradeSet(stopOrder.Symbol, 1)
|
||||
// if tradeset.PriceDigit > 0 {
|
||||
// price = price.Truncate(int32(tradeset.PriceDigit))
|
||||
// }
|
||||
|
||||
// cache := dto.StopLossRedisList{
|
||||
// PId: stopOrder.Pid,
|
||||
// ApiId: stopOrder.ApiId,
|
||||
// Price: price,
|
||||
// OrderTye: stopOrder.OrderType,
|
||||
// Site: stopOrder.Site,
|
||||
// Symbol: stopOrder.Symbol,
|
||||
// Stoploss: stoploss,
|
||||
// }
|
||||
|
||||
// stoplossKey := fmt.Sprintf(rediskey.SpotStopLossList)
|
||||
// cacheVal, _ := sonic.MarshalString(&cache)
|
||||
|
||||
// if stopOrder.OrderType == "4" {
|
||||
// stoplossKey = rediskey.FuturesStopLossList
|
||||
// }
|
||||
|
||||
// stopLossVal, _ := helper.DefaultRedis.GetAllList(stoplossKey)
|
||||
// for _, itemVal := range stopLossVal {
|
||||
// if strings.Contains(itemVal, fmt.Sprintf("\"pid\":%v,", stopOrder.Pid)) {
|
||||
// helper.DefaultRedis.LRem(stoplossKey, itemVal)
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
|
||||
// //重新保存待触发对冲单
|
||||
// if err := helper.DefaultRedis.RPushList(stoplossKey, cacheVal); err != nil {
|
||||
// logger.Error("B单平仓回调,redis添加止损单失败", err)
|
||||
// }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 生成随机数 且不重复
|
||||
// lastPercent 上一次的百分比
|
||||
// floatNum 小数点后几位
|
||||
func getRand(start, end, lastPercent decimal.Decimal, floatNum int) decimal.Decimal {
|
||||
var rand decimal.Decimal
|
||||
|
||||
for x := 0; x < 10; x++ {
|
||||
rand = utility.DecimalRandom(start, end, floatNum)
|
||||
|
||||
if rand.Cmp(lastPercent) != 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return rand
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// NEW
|
||||
// PENDING_NEW
|
||||
// PARTIALLY_FILLED
|
||||
// FILLED
|
||||
// CANCELED
|
||||
// PENDING_CANCEL
|
||||
// REJECTED
|
||||
// EXPIRED
|
||||
// EXPIRED_IN_MATCH
|
||||
68
services/binanceservice/spotsymbolservice.go
Normal file
68
services/binanceservice/spotsymbolservice.go
Normal file
@ -0,0 +1,68 @@
|
||||
package binanceservice
|
||||
|
||||
import (
|
||||
"go-admin/models"
|
||||
"go-admin/models/spot"
|
||||
"go-admin/pkg/utility"
|
||||
"sync"
|
||||
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
)
|
||||
|
||||
var quoteAssetSymbols = []string{"USDT", "ETH", "BTC", "SOL", "BNB", "DOGE"}
|
||||
|
||||
func GetSpotSymbols() (map[string]models.TradeSet, []string, error) {
|
||||
spotApi := SpotRestApi{}
|
||||
symbols, err := spotApi.GetExchangeInfo()
|
||||
tradeSets := make(map[string]models.TradeSet, len(symbols))
|
||||
|
||||
if err != nil {
|
||||
log.Error("获取规范信息失败", err)
|
||||
return tradeSets, []string{}, err
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex // 用于保护 tradeSets 的并发写入
|
||||
|
||||
for _, item := range symbols {
|
||||
if utility.ContainsStr(quoteAssetSymbols, item.QuoteAsset) && item.Status == "TRADING" && item.IsSpotTradingAllowed {
|
||||
wg.Add(1)
|
||||
go func(item spot.Symbol) {
|
||||
defer wg.Done()
|
||||
tradeSet := models.TradeSet{
|
||||
Coin: item.BaseAsset,
|
||||
Currency: item.QuoteAsset,
|
||||
}
|
||||
|
||||
for _, filter := range item.Filters {
|
||||
switch filter.FilterType {
|
||||
case "PRICE_FILTER":
|
||||
tradeSet.PriceDigit = utility.GetPrecision(filter.TickSize)
|
||||
tradeSet.MinBuyVal = utility.StringAsFloat(filter.MinPrice)
|
||||
case "LOT_SIZE":
|
||||
tradeSet.AmountDigit = utility.GetPrecision(filter.StepSize)
|
||||
tradeSet.MinQty = utility.StringAsFloat(filter.MinQty)
|
||||
tradeSet.MaxQty = utility.StringAsFloat(filter.MaxQty)
|
||||
}
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
tradeSets[item.Symbol] = tradeSet
|
||||
mu.Unlock()
|
||||
}(item)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait() // 等待所有 goroutine 完成
|
||||
log.Info("初始化交易对")
|
||||
|
||||
deleteSymbols, err := spotApi.GetSpotTicker24h(&tradeSets)
|
||||
|
||||
if err != nil {
|
||||
log.Error("初始化币安现货交易对失败", err)
|
||||
return map[string]models.TradeSet{}, deleteSymbols, err
|
||||
} else {
|
||||
log.Info("初始化现货交易对完毕")
|
||||
return tradeSets, deleteSymbols, err
|
||||
}
|
||||
}
|
||||
94
services/excservice/apibuild.go
Normal file
94
services/excservice/apibuild.go
Normal file
@ -0,0 +1,94 @@
|
||||
package excservice
|
||||
|
||||
import (
|
||||
"go-admin/pkg/httputils"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"go.uber.org/zap"
|
||||
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultHttpClientConfig = &HttpClientConfig{
|
||||
// Proxy: nil,
|
||||
// HttpTimeout: 5 * time.Second,
|
||||
// MaxIdleConns: 10}
|
||||
)
|
||||
var (
|
||||
timeOffset int64 = 0
|
||||
)
|
||||
|
||||
//var INERNAL_KLINE_PERIOD_CONVERTER = map[int]string{
|
||||
// models.KLINE_1MIN: "1m",
|
||||
// models.KLINE_3MIN: "3m",
|
||||
// models.KLINE_5MIN: "5m",
|
||||
// models.KLINE_15MIN: "15m",
|
||||
// models.KLINE_30MIN: "30m",
|
||||
// models.KLINE_60MIN: "1h",
|
||||
// //models.KLINE_1H: "1h",
|
||||
// models.KLINE_2H: "2h",
|
||||
// models.KLINE_4H: "4h",
|
||||
// models.KLINE_6H: "6h",
|
||||
// models.KLINE_8H: "8h",
|
||||
// models.KLINE_12H: "12h",
|
||||
// models.KLINE_1DAY: "1d",
|
||||
// models.KLINE_3DAY: "3d",
|
||||
// models.KLINE_1WEEK: "1w",
|
||||
// models.KLINE_1MONTH: "1M",
|
||||
//}
|
||||
|
||||
type Filter struct {
|
||||
FilterType string `json:"filterType"`
|
||||
MaxPrice string `json:"maxPrice"`
|
||||
MinPrice string `json:"minPrice"`
|
||||
TickSize string `json:"tickSize"`
|
||||
MultiplierUp string `json:"multiplierUp,string"`
|
||||
MultiplierDown string `json:"multiplierDown,string"`
|
||||
MinQty string `json:"minQty"`
|
||||
MaxQty string `json:"maxQty"`
|
||||
StepSize string `json:"stepSize"`
|
||||
MinNotional string `json:"minNotional"`
|
||||
}
|
||||
|
||||
//
|
||||
//type RateLimit struct {
|
||||
// Interval string `json:"interval"`
|
||||
// IntervalNum int64 `json:"intervalNum"`
|
||||
// Limit int64 `json:"limit"`
|
||||
// RateLimitType string `json:"rateLimitType"`
|
||||
//}
|
||||
|
||||
type TradeSymbol struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Status string `json:"status"`
|
||||
BaseAsset string `json:"baseAsset"` //基础币种
|
||||
QuoteAsset string `json:"quoteAsset"` //计价币种
|
||||
BaseAssetPrecision int `json:"baseAssetPrecision"` //基础币种小数点位数
|
||||
QuotePrecision int `json:"quotePrecision"` //价格小数点位数 添加新字段 quoteAssetPrecision。此字段和 quotePrecision 重复。在未来的版本(v4)中 quotePrecision 会被移除
|
||||
QuoteAssetPrecision int `json:"quoteAssetPrecision"` //
|
||||
BaseCommissionPrecision int `json:"baseCommissionPrecision"`
|
||||
QuoteCommissionPrecision int `json:"quoteCommissionPrecision"`
|
||||
OrderTypes []string `json:"orderTypes"`
|
||||
Filters []Filter `json:"filters"`
|
||||
}
|
||||
type ExchangeInfo struct {
|
||||
Timezone string `json:"timezone"`
|
||||
ServerTime int `json:"serverTime"`
|
||||
//ExchangeFilters []interface{} `json:"exchangeFilters,omitempty"`
|
||||
//RateLimits []RateLimit `json:"rateLimits"`
|
||||
Symbols []TradeSymbol `json:"symbols"`
|
||||
}
|
||||
|
||||
// 获取exchangeInfo
|
||||
func GetExchangeInfoPro() ([]TradeSymbol, error) {
|
||||
respData, err := httputils.NewHttpRequestWithFasthttp("GET", apiUrl+"/api/v3/exchangeInfo", "", nil)
|
||||
if err != nil {
|
||||
log.Error("获取exchangeInfo", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
var info ExchangeInfo
|
||||
sonic.Unmarshal(respData, &info)
|
||||
|
||||
return info.Symbols, nil
|
||||
}
|
||||
299
services/excservice/biance.go
Normal file
299
services/excservice/biance.go
Normal file
@ -0,0 +1,299 @@
|
||||
package excservice
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"go-admin/common/global"
|
||||
"go-admin/common/helper"
|
||||
"go-admin/models"
|
||||
"go-admin/pkg/httputils"
|
||||
"go-admin/pkg/utility"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
TICKER_URI = "/api/v3/ticker/24hr?symbol=%s"
|
||||
TICKERS_URI = "ticker/allBookTickers"
|
||||
DEPTH_URI = "/api/v3/depth?symbol=%s&limit=%d"
|
||||
ACCOUNT_URI = "/api/v3/account?"
|
||||
ORDER_URI = "/api/v3/order"
|
||||
UNFINISHED_ORDERS_INFO = "openOrders?"
|
||||
KLINE_URI = "klines"
|
||||
SERVER_TIME_URL = "/api/v3/time"
|
||||
)
|
||||
|
||||
var (
|
||||
apiUrl = "https://api.binance.com"
|
||||
//如果上面的baseURL访问有性能问题,请访问下面的API集群:
|
||||
//https://api1.binance.com
|
||||
//https://api2.binance.com
|
||||
//https://api3.binance.com
|
||||
)
|
||||
|
||||
func init() {
|
||||
//err := setTimeOffsetPro()
|
||||
//if err != nil {
|
||||
// fmt.Println("setTimeOffsetPro,err:", err)
|
||||
//}
|
||||
}
|
||||
|
||||
func buildParamsSigned(postForm *url.Values, secretKey string) {
|
||||
postForm.Set("recvWindow", "60000")
|
||||
tonce := strconv.FormatInt(time.Now().UnixNano()+timeOffset, 10)[0:13]
|
||||
postForm.Set("timestamp", tonce)
|
||||
payload := postForm.Encode()
|
||||
sign, _ := GetHmacSHA256Sign(secretKey, payload)
|
||||
postForm.Set("signature", sign)
|
||||
}
|
||||
func GetHmacSHA256Sign(secret, params string) (string, error) {
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
_, err := mac.Write([]byte(params))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(mac.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// 获取服务器时间
|
||||
func setTimeOffsetPro() error {
|
||||
respData, err := httputils.NewHttpRequestWithFasthttp("GET", apiUrl+SERVER_TIME_URL, "", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var bodyDataMap map[string]interface{}
|
||||
err = json.Unmarshal(respData, &bodyDataMap)
|
||||
if err != nil {
|
||||
log.Error(string(respData))
|
||||
return err
|
||||
}
|
||||
stime := int64(utility.ToInt(bodyDataMap["serverTime"]))
|
||||
st := time.Unix(stime/1000, 1000000*(stime%1000))
|
||||
lt := time.Now()
|
||||
offset := st.Sub(lt).Nanoseconds()
|
||||
timeOffset = offset
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTicker 获取24小时行情
|
||||
func GetTicker(coin, currency string) (models.Ticker24, error) {
|
||||
par := strings.ToUpper(coin + currency)
|
||||
tickerUri := apiUrl + fmt.Sprintf(TICKER_URI, par)
|
||||
var ticker models.Ticker24
|
||||
respData, err := httputils.NewHttpRequestWithFasthttp("GET", tickerUri, "", nil)
|
||||
if err != nil {
|
||||
log.Error("GetTicker", zap.Error(err))
|
||||
return ticker, err
|
||||
}
|
||||
var tickerMap map[string]interface{}
|
||||
err = json.Unmarshal(respData, &tickerMap)
|
||||
if err != nil {
|
||||
log.Error("GetTicker", zap.ByteString("respData", respData), zap.Error(err))
|
||||
return ticker, err
|
||||
}
|
||||
|
||||
ticker.LastPrice = tickerMap["lastPrice"].(string)
|
||||
ticker.LowPrice = tickerMap["lowPrice"].(string)
|
||||
ticker.HighPrice = tickerMap["highPrice"].(string)
|
||||
ticker.Volume = tickerMap["volume"].(string)
|
||||
ticker.QuoteVolume = tickerMap["quoteVolume"].(string)
|
||||
ticker.ChangePercent = tickerMap["priceChangePercent"].(string)
|
||||
ticker.OpenPrice = tickerMap["openPrice"].(string)
|
||||
return ticker, nil
|
||||
}
|
||||
|
||||
// GetTickerBySymbols 获取24小时行情 symbols symbols参数可接受的格式: ["BTCUSDT","BNBUSDT"]
|
||||
func GetTickerBySymbols(symbols string) ([]models.Ticker24, error) {
|
||||
tickerUri := apiUrl + "/api/v3/ticker/24hr"
|
||||
|
||||
respData, err := httputils.NewHttpRequestWithFasthttp("GET", tickerUri, "", nil)
|
||||
if err != nil {
|
||||
log.Error("GetTicker", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
var tickerList []interface{}
|
||||
err = json.Unmarshal(respData, &tickerList)
|
||||
if err != nil {
|
||||
log.Error("GetTickerBySymbols", zap.ByteString("respData", respData), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
list := make([]models.Ticker24, 0, len(tickerList))
|
||||
for _, t := range tickerList {
|
||||
tickerMap := t.(map[string]interface{})
|
||||
if tickerMap == nil {
|
||||
continue
|
||||
}
|
||||
var ticker models.Ticker24
|
||||
ticker.LastPrice = tickerMap["lastPrice"].(string)
|
||||
ticker.LowPrice = tickerMap["lowPrice"].(string)
|
||||
ticker.HighPrice = tickerMap["highPrice"].(string)
|
||||
ticker.Volume = tickerMap["volume"].(string)
|
||||
ticker.QuoteVolume = tickerMap["quoteVolume"].(string)
|
||||
ticker.ChangePercent = tickerMap["priceChangePercent"].(string)
|
||||
ticker.OpenPrice = tickerMap["openPrice"].(string)
|
||||
ticker.Symbol = tickerMap["symbol"].(string)
|
||||
list = append(list, ticker)
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// GetKlinePro 获取k线--现货行情接口
|
||||
func GetKlinePro(coin, currency string, period string, size int) ([]models.Kline, error) {
|
||||
par := strings.ToUpper(coin + currency)
|
||||
periodS := period //, isOk := INERNAL_KLINE_PERIOD_CONVERTER[period]
|
||||
//if isOk != true {
|
||||
// periodS = "M1"
|
||||
//}
|
||||
key := fmt.Sprintf("%s:%s:%s", global.K_SPOT, par, period)
|
||||
|
||||
//获取缓存
|
||||
klineStrs, err := helper.DefaultRedis.GetAllSortSet(key)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(klineStrs) > 0 && len(klineStrs) >= 500 {
|
||||
klines := make([]models.Kline, 0)
|
||||
|
||||
for _, item := range klineStrs {
|
||||
var kline models.Kline
|
||||
err := sonic.Unmarshal([]byte(item), &kline)
|
||||
|
||||
if err == nil {
|
||||
klines = append(klines, kline)
|
||||
}
|
||||
}
|
||||
|
||||
return klines, nil
|
||||
}
|
||||
|
||||
//没有缓存 重新获取
|
||||
|
||||
urlKline := apiUrl + "/api/v3/klines?symbol=" + par + "&interval=" + periodS + "&limit=" + utility.IntTostring(size)
|
||||
respData, err := httputils.NewHttpRequestWithFasthttp("GET", urlKline, "", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var bodyDataMap []interface{}
|
||||
err = json.Unmarshal(respData, &bodyDataMap)
|
||||
if err != nil {
|
||||
log.Error("GetKlinePro", zap.ByteString("respData", respData), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
var klines []models.Kline
|
||||
for _, _record := range bodyDataMap {
|
||||
r := models.Kline{}
|
||||
record := _record.([]interface{})
|
||||
times := utility.ToFloat64(record[0]) //to unix timestramp
|
||||
|
||||
// 超出10位的 处理为
|
||||
if times > 9999999999 {
|
||||
r.Timestamp = int64(times) / 1000
|
||||
} else {
|
||||
r.Timestamp = int64(times)
|
||||
}
|
||||
|
||||
r.Open = record[1].(string)
|
||||
r.High = record[2].(string)
|
||||
r.Low = record[3].(string)
|
||||
r.Close = record[4].(string)
|
||||
r.Vol = record[5].(string)
|
||||
r.QuoteVolume = record[7].(string)
|
||||
|
||||
klines = append(klines, r)
|
||||
|
||||
member, err := sonic.Marshal(r)
|
||||
|
||||
if err == nil {
|
||||
err = helper.DefaultRedis.SignelAdd(key, float64(r.Timestamp), string(member))
|
||||
|
||||
if err != nil {
|
||||
log.Error("保存k线数据失败:", key, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return klines, nil
|
||||
}
|
||||
|
||||
// GetTrades 非个人,整个交易所的交易记录
|
||||
// 注意:since is fromId
|
||||
func GetTrades(coin, currency string) ([]models.NewDealPush, error) {
|
||||
param := url.Values{}
|
||||
param.Set("symbol", strings.ToUpper(coin+currency))
|
||||
param.Set("limit", "50")
|
||||
//if since > 0 {
|
||||
// param.Set("fromId", strconv.Itoa(int(since)))
|
||||
//}
|
||||
urlTrade := apiUrl + "/api/v3/trades?" + param.Encode()
|
||||
resp, err := httputils.NewHttpRequestWithFasthttp("GET", urlTrade, "", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var bodyDataMap []interface{}
|
||||
err = json.Unmarshal(resp, &bodyDataMap)
|
||||
if err != nil {
|
||||
log.Error("GetTrades", zap.ByteString("respData", resp), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
var trades []models.NewDealPush
|
||||
for _, v := range bodyDataMap {
|
||||
m := v.(map[string]interface{})
|
||||
ty := 2
|
||||
if m["isBuyerMaker"].(bool) {
|
||||
ty = 1
|
||||
}
|
||||
trades = append(trades, models.NewDealPush{
|
||||
DealId: utility.ToInt64(m["id"]),
|
||||
Type: ty,
|
||||
Num: utility.ToFloat64(m["qty"]),
|
||||
Price: utility.ToFloat64(m["price"]),
|
||||
CreateTime: utility.ToInt64(m["time"]),
|
||||
})
|
||||
}
|
||||
return trades, nil
|
||||
}
|
||||
|
||||
// GetDepth 获取深度
|
||||
func GetDepth(size int, coin, currency string) (models.DepthBin, error) {
|
||||
if size <= 5 {
|
||||
size = 5
|
||||
} else if size <= 10 {
|
||||
size = 10
|
||||
} else if size <= 20 {
|
||||
size = 20
|
||||
} else if size <= 50 {
|
||||
size = 50
|
||||
} else if size <= 100 {
|
||||
size = 100
|
||||
} else if size <= 500 {
|
||||
size = 500
|
||||
} else {
|
||||
size = 1000
|
||||
}
|
||||
urlDep := fmt.Sprintf(apiUrl+DEPTH_URI, strings.ToUpper(coin+currency), size)
|
||||
respFive, err := httputils.NewHttpRequestWithFasthttp("GET", urlDep, "", nil)
|
||||
if err != nil {
|
||||
return models.DepthBin{}, err
|
||||
}
|
||||
d := models.DepthBin{}
|
||||
err = sonic.Unmarshal(respFive, &d)
|
||||
if err != nil {
|
||||
fmt.Println("GetDepth json unmarshal error for ", string(respFive), zap.Error(err))
|
||||
return models.DepthBin{}, err
|
||||
}
|
||||
return d, nil
|
||||
|
||||
}
|
||||
111
services/excservice/binancereceive.go
Normal file
111
services/excservice/binancereceive.go
Normal file
@ -0,0 +1,111 @@
|
||||
package excservice
|
||||
|
||||
import (
|
||||
"go-admin/models/futuresdto"
|
||||
"go-admin/pkg/utility"
|
||||
"go-admin/services/binanceservice"
|
||||
"strconv"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
)
|
||||
|
||||
/*
|
||||
用户订单订阅处理
|
||||
- @msg 消息内容
|
||||
- @listenType 订阅类型 0-现货 1-合约
|
||||
*/
|
||||
func ReceiveListen(msg []byte, listenType int) (reconnect bool, err error) {
|
||||
var dataMap map[string]interface{}
|
||||
err = sonic.Unmarshal(msg, &dataMap)
|
||||
|
||||
if err != nil {
|
||||
log.Error("接收ws 反序列化失败:", err)
|
||||
return
|
||||
}
|
||||
|
||||
event, exits := dataMap["e"]
|
||||
|
||||
if !exits {
|
||||
log.Error("不存在event")
|
||||
return
|
||||
}
|
||||
|
||||
switch event {
|
||||
//listenKey过期
|
||||
case "listenKeyExpired":
|
||||
log.Info("listenKey过期", string(msg))
|
||||
return true, nil
|
||||
|
||||
//订单变更
|
||||
case "ORDER_TRADE_UPDATE":
|
||||
log.Info("ORDER_TRADE_UPDATE 推送:", string(msg))
|
||||
|
||||
//现货
|
||||
if listenType == 0 {
|
||||
var mapData map[string]interface{}
|
||||
err = sonic.Unmarshal(msg, &mapData)
|
||||
|
||||
if err != nil {
|
||||
log.Error("订单变更处理失败", err)
|
||||
break
|
||||
}
|
||||
|
||||
utility.SafeGo(func() {
|
||||
binanceservice.ChangeSpotOrder(mapData)
|
||||
})
|
||||
} else {
|
||||
var data futuresdto.OrderTradeUpdate
|
||||
err = sonic.Unmarshal(msg, &data)
|
||||
|
||||
if err != nil {
|
||||
log.Error("订单变更处理失败", err)
|
||||
break
|
||||
}
|
||||
|
||||
utility.SafeGo(func() {
|
||||
binanceservice.ChangeFutureOrder(data.OrderDetails)
|
||||
})
|
||||
}
|
||||
//订单更新
|
||||
case "executionReport":
|
||||
log.Info("executionReport 推送:", string(msg))
|
||||
|
||||
if listenType == 0 { //现货
|
||||
binanceservice.ChangeSpotOrder(dataMap)
|
||||
} else if listenType == 1 { //合约
|
||||
binanceservice.ChangeFutureOrder(dataMap)
|
||||
} else {
|
||||
log.Error("executionReport 不支持的订阅类型", strconv.Itoa(listenType))
|
||||
}
|
||||
//杠杆倍数等账户配置 更新推送
|
||||
case "ACCOUNT_CONFIG_UPDATE":
|
||||
log.Info(string(msg))
|
||||
//追加保证金
|
||||
case "MARGIN_CALL":
|
||||
log.Info(string(msg))
|
||||
|
||||
//条件订单(TP/SL)触发后拒绝更新推送
|
||||
case "CONDITIONAL_ORDER_TRIGGER_REJECT":
|
||||
or, exits := dataMap["or"].(string)
|
||||
|
||||
if exits {
|
||||
var data futuresdto.OrderTriggerReject
|
||||
|
||||
sonic.UnmarshalString(or, &data)
|
||||
|
||||
if data.OrderNo > 0 {
|
||||
log.Info("订单号【%v】止盈止损触发后被拒绝:%s", data.OrderNo, data.Reason)
|
||||
}
|
||||
}
|
||||
|
||||
case "eventStreamTerminated":
|
||||
log.Info("账户数据流被终止 type:", getWsTypeName(listenType))
|
||||
default:
|
||||
log.Info("未知事件 内容:", string(msg))
|
||||
log.Info("未知事件", event)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
600
services/excservice/binancesocketmanager.go
Normal file
600
services/excservice/binancesocketmanager.go
Normal file
@ -0,0 +1,600 @@
|
||||
package excservice
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go-admin/common/global"
|
||||
"go-admin/common/helper"
|
||||
"go-admin/models/binancedto"
|
||||
"go-admin/models/commondto"
|
||||
"go-admin/pkg/jsonhelper"
|
||||
"go-admin/pkg/utility"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
"github.com/gorilla/websocket"
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
type BinanceWebSocketManager struct {
|
||||
ws *websocket.Conn
|
||||
stopChannel chan struct{}
|
||||
url string
|
||||
/* 0-现货 1-合约 */
|
||||
wsType int
|
||||
apiKey string
|
||||
apiSecret string
|
||||
proxyType string
|
||||
proxyAddress string
|
||||
reconnect chan struct{}
|
||||
isStopped bool // 标记 WebSocket 是否已主动停止
|
||||
mu sync.Mutex // 用于控制并发访问 isStopped
|
||||
cancelFunc context.CancelFunc
|
||||
listenKey string // 新增字段
|
||||
}
|
||||
|
||||
// 已有连接
|
||||
var SpotSockets = map[string]*BinanceWebSocketManager{}
|
||||
var FutureSockets = map[string]*BinanceWebSocketManager{}
|
||||
|
||||
func NewBinanceWebSocketManager(wsType int, apiKey, apiSecret, proxyType, proxyAddress string) *BinanceWebSocketManager {
|
||||
url := ""
|
||||
|
||||
switch wsType {
|
||||
case 0:
|
||||
url = "wss://stream.binance.com:9443/ws"
|
||||
case 1:
|
||||
url = "wss://fstream.binance.com/ws"
|
||||
}
|
||||
|
||||
return &BinanceWebSocketManager{
|
||||
stopChannel: make(chan struct{}, 10),
|
||||
reconnect: make(chan struct{}, 10),
|
||||
isStopped: false,
|
||||
url: url,
|
||||
wsType: wsType,
|
||||
apiKey: apiKey,
|
||||
apiSecret: apiSecret,
|
||||
proxyType: proxyType,
|
||||
proxyAddress: proxyAddress,
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *BinanceWebSocketManager) Start() {
|
||||
utility.SafeGo(wm.run)
|
||||
// wm.run()
|
||||
}
|
||||
|
||||
// 重启连接
|
||||
func (wm *BinanceWebSocketManager) Restart(apiKey, apiSecret, proxyType, proxyAddress string) *BinanceWebSocketManager {
|
||||
wm.apiKey = apiKey
|
||||
wm.apiSecret = apiSecret
|
||||
wm.proxyType = proxyType
|
||||
wm.proxyAddress = proxyAddress
|
||||
|
||||
wm.reconnect <- struct{}{}
|
||||
return wm
|
||||
}
|
||||
|
||||
func Restart(wm *BinanceWebSocketManager) {
|
||||
wm.reconnect <- struct{}{}
|
||||
}
|
||||
func (wm *BinanceWebSocketManager) run() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
wm.cancelFunc = cancel
|
||||
|
||||
utility.SafeGo(wm.handleSignal)
|
||||
|
||||
// 计算错误记录键
|
||||
errKey := fmt.Sprintf(global.API_WEBSOCKET_ERR, wm.apiKey)
|
||||
errMessage := commondto.WebSocketErr{Time: time.Now()}
|
||||
helper.DefaultRedis.SetString(errKey, jsonhelper.ToJsonString(errMessage))
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
if err := wm.connect(ctx); err != nil {
|
||||
wm.handleConnectionError(errKey, err)
|
||||
|
||||
if wm.isErrorCountExceeded(errKey) {
|
||||
log.Error("连接 %s WebSocket 时出错次数过多,停止 WebSocket 管理器: %v", wm.wsType, wm.apiKey)
|
||||
wm.Stop()
|
||||
return
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
<-wm.stopChannel
|
||||
log.Info("停止 %s WebSocket 管理器...", getWsTypeName(wm.wsType))
|
||||
wm.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleConnectionError 处理 WebSocket 连接错误
|
||||
func (wm *BinanceWebSocketManager) handleConnectionError(errKey string, err error) {
|
||||
// 从 Redis 获取错误记录
|
||||
var errMessage commondto.WebSocketErr
|
||||
val, _ := helper.DefaultRedis.GetString(errKey)
|
||||
if val != "" {
|
||||
sonic.UnmarshalString(val, &errMessage)
|
||||
}
|
||||
|
||||
// 更新错误记录
|
||||
errMessage.Count++
|
||||
errMessage.Time = time.Now()
|
||||
errMessage.ErrorMessage = err.Error()
|
||||
|
||||
// 将错误记录保存到 Redis
|
||||
if data, err := sonic.MarshalString(errMessage); err == nil {
|
||||
helper.DefaultRedis.SetString(errKey, data)
|
||||
}
|
||||
|
||||
// 记录错误日志
|
||||
log.Error("连接 %s WebSocket 时出错: %v, 错误: %v", wm.wsType, wm.apiKey, err)
|
||||
}
|
||||
|
||||
// isErrorCountExceeded 检查错误次数是否超过阈值
|
||||
func (wm *BinanceWebSocketManager) isErrorCountExceeded(errKey string) bool {
|
||||
val, _ := helper.DefaultRedis.GetString(errKey)
|
||||
if val == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
var errMessage commondto.WebSocketErr
|
||||
if err := sonic.UnmarshalString(val, &errMessage); err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return errMessage.Count >= 5
|
||||
}
|
||||
|
||||
// 处理终止信号
|
||||
func (wm *BinanceWebSocketManager) handleSignal() {
|
||||
ch := make(chan os.Signal)
|
||||
signal.Notify(ch, os.Interrupt)
|
||||
<-ch
|
||||
wm.Stop()
|
||||
}
|
||||
|
||||
func (wm *BinanceWebSocketManager) connect(ctx context.Context) error {
|
||||
dialer, err := wm.getDialer()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
listenKey, err := wm.getListenKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wm.listenKey = listenKey
|
||||
url := fmt.Sprintf("%s/%s", wm.url, listenKey)
|
||||
wm.ws, _, err = dialer.Dial(url, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info(fmt.Sprintf("已连接到 Binance %s WebSocket【%s】 key:%s", getWsTypeName(wm.wsType), wm.apiKey, listenKey))
|
||||
|
||||
// Ping处理
|
||||
wm.ws.SetPingHandler(func(appData string) error {
|
||||
log.Info(fmt.Sprintf("收到 wstype: %v key:%s Ping 消息【%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("binance 回应pong失败 次数:", strconv.Itoa(x), " err:", err)
|
||||
|
||||
time.Sleep(time.Second * 1)
|
||||
continue
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
setLastTime(wm)
|
||||
return nil
|
||||
})
|
||||
|
||||
// utility.SafeGoParam(wm.restartConnect, ctx)
|
||||
utility.SafeGo(func() { wm.startListenKeyRenewal2(ctx) })
|
||||
utility.SafeGo(func() { wm.readMessages(ctx) })
|
||||
utility.SafeGo(func() { wm.handleReconnect(ctx) })
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 更新最后通信时间
|
||||
func setLastTime(wm *BinanceWebSocketManager) {
|
||||
subKey := fmt.Sprintf(global.USER_SUBSCRIBE, wm.apiKey)
|
||||
val, _ := helper.DefaultRedis.GetString(subKey)
|
||||
now := time.Now()
|
||||
var data binancedto.UserSubscribeState
|
||||
if val != "" {
|
||||
sonic.Unmarshal([]byte(val), &data)
|
||||
}
|
||||
|
||||
if wm.wsType == 0 {
|
||||
data.SpotLastTime = &now
|
||||
} else {
|
||||
data.FuturesLastTime = &now
|
||||
}
|
||||
|
||||
val, _ = sonic.MarshalString(&data)
|
||||
|
||||
if 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客户端的逻辑
|
||||
func (wm *BinanceWebSocketManager) createBinanceClient() (*helper.BinanceClient, error) {
|
||||
return helper.NewBinanceClient(wm.apiKey, wm.apiSecret, wm.proxyType, wm.proxyAddress)
|
||||
}
|
||||
|
||||
// 获取listenKey
|
||||
func (wm *BinanceWebSocketManager) getListenKey() (string, error) {
|
||||
client, err := wm.createBinanceClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var resp []byte
|
||||
switch wm.wsType {
|
||||
case 0:
|
||||
resp, _, err = client.SendSpotRequestByKey("/api/v3/userDataStream", "POST", nil)
|
||||
case 1:
|
||||
resp, _, err = client.SendFuturesRequestByKey("/fapi/v1/listenKey", "POST", nil)
|
||||
|
||||
default:
|
||||
log.Error("链接类型错误")
|
||||
return "", errors.New("链接类型错误")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var dataMap map[string]interface{}
|
||||
if err := sonic.Unmarshal(resp, &dataMap); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if listenKey, ok := dataMap["listenKey"]; ok {
|
||||
return listenKey.(string), nil
|
||||
}
|
||||
|
||||
return "", errors.New("listenKey 不存在")
|
||||
}
|
||||
|
||||
// 接收消息
|
||||
func (wm *BinanceWebSocketManager) readMessages(ctx context.Context) {
|
||||
defer wm.ws.Close()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
if wm.isStopped {
|
||||
return
|
||||
}
|
||||
|
||||
_, msg, err := wm.ws.ReadMessage()
|
||||
if err != nil && strings.Contains(err.Error(), "websocket: close") {
|
||||
if !wm.isStopped {
|
||||
wm.reconnect <- struct{}{}
|
||||
}
|
||||
|
||||
log.Error("websocket 关闭")
|
||||
return
|
||||
} else if err != nil {
|
||||
log.Error("读取消息时出错: %v", err)
|
||||
return
|
||||
}
|
||||
wm.handleOrderUpdate(msg)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *BinanceWebSocketManager) handleOrderUpdate(msg []byte) {
|
||||
setLastTime(wm)
|
||||
|
||||
if reconnect, _ := ReceiveListen(msg, wm.wsType); reconnect {
|
||||
wm.reconnect <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
func (wm *BinanceWebSocketManager) Stop() {
|
||||
wm.mu.Lock()
|
||||
defer wm.mu.Unlock()
|
||||
|
||||
if wm.isStopped {
|
||||
return
|
||||
}
|
||||
|
||||
wm.isStopped = true
|
||||
close(wm.stopChannel)
|
||||
|
||||
if wm.cancelFunc != nil {
|
||||
wm.cancelFunc()
|
||||
}
|
||||
|
||||
if wm.ws != nil {
|
||||
if err := wm.ws.Close(); err != nil {
|
||||
log.Error(fmt.Sprintf("key【%s】close失败", wm.apiKey), err)
|
||||
} else {
|
||||
log.Info(fmt.Sprintf("key【%s】close", wm.apiKey))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 重连机制
|
||||
func (wm *BinanceWebSocketManager) handleReconnect(ctx context.Context) {
|
||||
maxRetries := 5 // 最大重试次数
|
||||
retryCount := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-wm.reconnect:
|
||||
if wm.isStopped {
|
||||
return
|
||||
}
|
||||
|
||||
log.Warn("WebSocket 连接断开,尝试重连...")
|
||||
|
||||
if wm.ws != nil {
|
||||
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
|
||||
func (wm *BinanceWebSocketManager) startListenKeyRenewal(ctx context.Context, listenKey string) {
|
||||
time.Sleep(30 * time.Minute)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
if err := wm.deleteListenKey(listenKey); err != nil {
|
||||
log.Error("Failed to renew listenKey: ,type:%v key: %s", wm.wsType, wm.apiKey, err)
|
||||
} else {
|
||||
log.Debug("Successfully delete listenKey")
|
||||
wm.reconnect <- struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
ticker := time.NewTicker(30 * time.Minute)
|
||||
defer func() {
|
||||
log.Debug("定时续期任务退出 key:", wm.apiKey)
|
||||
ticker.Stop()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if wm.isStopped {
|
||||
return
|
||||
}
|
||||
|
||||
if err := wm.renewListenKey(wm.listenKey); err != nil {
|
||||
log.Error("Failed to renew listenKey: ,type:%v key: %s", wm.wsType, wm.apiKey, err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
删除listenkey
|
||||
*/
|
||||
func (wm *BinanceWebSocketManager) deleteListenKey(listenKey string) error {
|
||||
client, err := wm.createBinanceClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var resp []byte
|
||||
|
||||
switch wm.wsType {
|
||||
case 0:
|
||||
path := fmt.Sprintf("/api/v3/userDataStream")
|
||||
params := map[string]interface{}{
|
||||
"listenKey": listenKey,
|
||||
}
|
||||
resp, _, err = client.SendSpotRequestByKey(path, "DELETE", params)
|
||||
|
||||
log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp)))
|
||||
case 1:
|
||||
resp, _, err = client.SendFuturesRequestByKey("/fapi/v1/listenKey", "DELETE", nil)
|
||||
log.Debug(fmt.Sprintf("deleteListenKey resp: %s", string(resp)))
|
||||
default:
|
||||
return errors.New("unknown ws type")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (wm *BinanceWebSocketManager) renewListenKey(listenKey string) error {
|
||||
// payloadParam := map[string]interface{}{
|
||||
// "listenKey": listenKey,
|
||||
// "apiKey": wm.apiKey,
|
||||
// }
|
||||
|
||||
// params := map[string]interface{}{
|
||||
// "id": getUUID(),
|
||||
// "method": "userDataStream.ping",
|
||||
// "params": payloadParam,
|
||||
// }
|
||||
|
||||
// if err := wm.ws.WriteJSON(params); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// wm.ws.WriteJSON()
|
||||
client, err := wm.createBinanceClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var resp []byte
|
||||
|
||||
switch wm.wsType {
|
||||
case 0:
|
||||
path := fmt.Sprintf("/api/v3/userDataStream?listenKey=%s", listenKey)
|
||||
resp, _, err = client.SendSpotRequestByKey(path, "PUT", nil)
|
||||
log.Debug(fmt.Sprintf("renewListenKey resp: %s", string(resp)))
|
||||
case 1:
|
||||
// path := fmt.Sprintf("/fapi/v1/listenKey", listenKey)
|
||||
resp, _, err = client.SendFuturesRequestByKey("/fapi/v1/listenKey", "PUT", nil)
|
||||
log.Debug(fmt.Sprintf("renewListenKey resp: %s", string(resp)))
|
||||
default:
|
||||
return errors.New("unknown ws type")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getWsTypeName(wsType int) string {
|
||||
switch wsType {
|
||||
case 0:
|
||||
return "spot"
|
||||
case 1:
|
||||
return "futures"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
func getUUID() string {
|
||||
return fmt.Sprintf("%s-%s-%s-%s-%s", randomHex(8), randomHex(4), randomHex(4), randomHex(4), randomHex(12))
|
||||
}
|
||||
|
||||
func randomHex(n int) string {
|
||||
rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
hexChars := "0123456789abcdef"
|
||||
bytes := make([]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
bytes[i] = hexChars[rand.Intn(len(hexChars))]
|
||||
}
|
||||
return string(bytes)
|
||||
}
|
||||
175
services/excservice/binancews.go
Normal file
175
services/excservice/binancews.go
Normal file
@ -0,0 +1,175 @@
|
||||
package excservice
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"go-admin/models"
|
||||
"go-admin/pkg/timehelper"
|
||||
"go-admin/pkg/utility"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
)
|
||||
|
||||
type BinanceWs struct {
|
||||
baseURL string
|
||||
combinedBaseURL string
|
||||
proxyUrl string
|
||||
WorkType string
|
||||
wsConns []*WsConn
|
||||
tickerCallback func(models.Ticker24, string, string)
|
||||
forceCallback func(models.ForceOrder, string, string)
|
||||
depthCallback func(models.DepthBin, string, string)
|
||||
tradeCallback func(models.NewDealPush, string, string)
|
||||
klineCallback func(models.Kline, int, string, string)
|
||||
allBack func(msg []byte)
|
||||
allBackKline func(msg []byte, tradeSet models.TradeSet)
|
||||
}
|
||||
|
||||
func NewBinanceWs(wsbaseURL, proxyUrl string) *BinanceWs {
|
||||
return &BinanceWs{
|
||||
baseURL: wsbaseURL,
|
||||
combinedBaseURL: "wss://stream.binance.com:9443/stream?streams=",
|
||||
proxyUrl: proxyUrl,
|
||||
}
|
||||
}
|
||||
|
||||
func (bnWs *BinanceWs) SetProxyUrl(proxyUrl string) {
|
||||
bnWs.proxyUrl = proxyUrl
|
||||
}
|
||||
|
||||
func (bnWs *BinanceWs) SetBaseUrl(baseURL string) {
|
||||
bnWs.baseURL = baseURL
|
||||
}
|
||||
|
||||
func (bnWs *BinanceWs) SetCombinedBaseURL(combinedBaseURL string) {
|
||||
bnWs.combinedBaseURL = combinedBaseURL
|
||||
}
|
||||
|
||||
func (bnWs *BinanceWs) SetAllCallbacks(allBack func(msg []byte), allBackKline func(msg []byte, tradeSet models.TradeSet)) {
|
||||
if bnWs.allBack == nil {
|
||||
bnWs.allBack = allBack
|
||||
}
|
||||
|
||||
if bnWs.allBackKline == nil {
|
||||
bnWs.allBackKline = allBackKline
|
||||
}
|
||||
}
|
||||
|
||||
// 订阅通用函数
|
||||
func (bnWs *BinanceWs) subscribe(endpoint string, handle func(msg []byte) error) {
|
||||
wsConn := NewWsBuilder().
|
||||
WsUrl(endpoint).
|
||||
AutoReconnect().
|
||||
ProtoHandleFunc(handle).
|
||||
ProxyUrl(bnWs.proxyUrl).
|
||||
ReconnectInterval(time.Millisecond * 5).
|
||||
Build()
|
||||
if wsConn == nil {
|
||||
return
|
||||
}
|
||||
bnWs.wsConns = append(bnWs.wsConns, wsConn)
|
||||
go bnWs.exitHandler(wsConn)
|
||||
}
|
||||
|
||||
func (bnWs *BinanceWs) Close() {
|
||||
for _, con := range bnWs.wsConns {
|
||||
con.CloseWs()
|
||||
}
|
||||
}
|
||||
|
||||
func (bnWs *BinanceWs) Subscribe(streamName string, tradeSet models.TradeSet, callback func(msg []byte, tradeSet models.TradeSet)) error {
|
||||
endpoint := bnWs.baseURL + streamName
|
||||
handle := func(msg []byte) error {
|
||||
callback(msg, tradeSet)
|
||||
return nil
|
||||
}
|
||||
bnWs.subscribe(endpoint, handle)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bnWs *BinanceWs) exitHandler(c *WsConn) {
|
||||
pingTicker := time.NewTicker(1 * time.Minute)
|
||||
pongTicker := time.NewTicker(30 * time.Second)
|
||||
defer func() {
|
||||
pingTicker.Stop()
|
||||
pongTicker.Stop()
|
||||
c.CloseWs()
|
||||
|
||||
if err := recover(); err != nil {
|
||||
fmt.Printf("CloseWs, panic: %s\r\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case t := <-pingTicker.C:
|
||||
c.SendPingMessage([]byte(strconv.Itoa(int(t.UnixNano() / int64(time.Millisecond)))))
|
||||
case t := <-pongTicker.C:
|
||||
c.SendPongMessage([]byte(strconv.Itoa(int(t.UnixNano() / int64(time.Millisecond)))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseJsonToMap(msg []byte) (map[string]interface{}, error) {
|
||||
datamap := make(map[string]interface{})
|
||||
err := sonic.Unmarshal(msg, &datamap)
|
||||
return datamap, err
|
||||
}
|
||||
|
||||
func handleForceOrder(msg []byte, tradeSet models.TradeSet, callback func(models.ForceOrder, string, string)) error {
|
||||
datamap, err := parseJsonToMap(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("json unmarshal error: %v", err)
|
||||
}
|
||||
|
||||
msgType, ok := datamap["e"].(string)
|
||||
if !ok || msgType != "forceOrder" {
|
||||
return errors.New("unknown message type")
|
||||
}
|
||||
|
||||
datamapo := datamap["o"].(map[string]interface{})
|
||||
order := models.ForceOrder{
|
||||
Side: datamapo["S"].(string),
|
||||
Symbol: datamapo["s"].(string),
|
||||
Ordertype: datamapo["o"].(string),
|
||||
TimeInForce: datamapo["f"].(string),
|
||||
Num: utility.ToFloat64(datamapo["q"]),
|
||||
Price: utility.ToFloat64(datamapo["p"]),
|
||||
AvgPrice: utility.ToFloat64(datamapo["ap"]),
|
||||
State: datamapo["X"].(string),
|
||||
CreateTime: timehelper.IntToTime(utility.ToInt64(datamapo["T"])),
|
||||
}
|
||||
callback(order, tradeSet.Coin, tradeSet.Currency)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SubscribeAll 订阅 组合streams的URL格式为 /stream?streams=<streamName1>/<streamName2>/<streamName3>
|
||||
// 订阅组合streams时,事件payload会以这样的格式封装: {"stream":"<streamName>","data":<rawPayload>}
|
||||
// 单一原始 streams 格式为 /ws/<streamName>
|
||||
func (bnWs *BinanceWs) SubscribeAll(streamName string) error {
|
||||
endpoint := bnWs.baseURL + streamName
|
||||
|
||||
handle := func(msg []byte) error {
|
||||
bnWs.allBack(msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
bnWs.subscribe(endpoint, handle)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SubscribeAllKline 订阅kline推送 组合streams的URL格式为 /stream?streams=<streamName1>/<streamName2>/<streamName3>
|
||||
func (bnWs *BinanceWs) SubscribeAllKline(streamName string, tradeSet models.TradeSet) error {
|
||||
endpoint := bnWs.baseURL + streamName
|
||||
|
||||
handle := func(msg []byte) error {
|
||||
bnWs.allBackKline(msg, tradeSet)
|
||||
return nil
|
||||
}
|
||||
|
||||
bnWs.subscribe(endpoint, handle)
|
||||
return nil
|
||||
}
|
||||
442
services/excservice/websocket.go
Normal file
442
services/excservice/websocket.go
Normal file
@ -0,0 +1,442 @@
|
||||
package excservice
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"go-admin/pkg/utility"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type WsConfig struct {
|
||||
WsUrl string
|
||||
ProxyUrl string
|
||||
ReqHeaders map[string][]string //连接的时候加入的头部信息
|
||||
HeartbeatIntervalTime time.Duration //
|
||||
HeartbeatData func() []byte //心跳数据2
|
||||
IsAutoReconnect bool
|
||||
ProtoHandleFunc func([]byte) error //协议处理函数
|
||||
DecompressFunc func([]byte) ([]byte, error) //解压函数
|
||||
ErrorHandleFunc func(err error)
|
||||
ConnectSuccessAfterSendMessage func() []byte //for reconnect
|
||||
IsDump bool
|
||||
readDeadLineTime time.Duration
|
||||
reconnectInterval time.Duration
|
||||
}
|
||||
|
||||
var dialer = &websocket.Dialer{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
HandshakeTimeout: 30 * time.Second,
|
||||
EnableCompression: true,
|
||||
}
|
||||
|
||||
type WsConn struct {
|
||||
c *websocket.Conn
|
||||
WsConfig
|
||||
writeBufferChan chan []byte
|
||||
pingMessageBufferChan chan []byte
|
||||
pongMessageBufferChan chan []byte
|
||||
closeMessageBufferChan chan []byte
|
||||
subs [][]byte
|
||||
close chan bool
|
||||
reConnectLock *sync.Mutex
|
||||
}
|
||||
|
||||
type WsBuilder struct {
|
||||
wsConfig *WsConfig
|
||||
}
|
||||
|
||||
func NewWsBuilder() *WsBuilder {
|
||||
return &WsBuilder{&WsConfig{
|
||||
ReqHeaders: make(map[string][]string, 1),
|
||||
reconnectInterval: time.Second * 10,
|
||||
}}
|
||||
}
|
||||
|
||||
func (b *WsBuilder) WsUrl(wsUrl string) *WsBuilder {
|
||||
b.wsConfig.WsUrl = wsUrl
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *WsBuilder) ProxyUrl(proxyUrl string) *WsBuilder {
|
||||
b.wsConfig.ProxyUrl = proxyUrl
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *WsBuilder) ReqHeader(key, value string) *WsBuilder {
|
||||
b.wsConfig.ReqHeaders[key] = append(b.wsConfig.ReqHeaders[key], value)
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *WsBuilder) AutoReconnect() *WsBuilder {
|
||||
b.wsConfig.IsAutoReconnect = true
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *WsBuilder) Dump() *WsBuilder {
|
||||
b.wsConfig.IsDump = true
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *WsBuilder) Heartbeat(heartbeat func() []byte, t time.Duration) *WsBuilder {
|
||||
b.wsConfig.HeartbeatIntervalTime = t
|
||||
b.wsConfig.HeartbeatData = heartbeat
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *WsBuilder) ReconnectInterval(t time.Duration) *WsBuilder {
|
||||
b.wsConfig.reconnectInterval = t
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *WsBuilder) ProtoHandleFunc(f func([]byte) error) *WsBuilder {
|
||||
b.wsConfig.ProtoHandleFunc = f
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *WsBuilder) DecompressFunc(f func([]byte) ([]byte, error)) *WsBuilder {
|
||||
b.wsConfig.DecompressFunc = f
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *WsBuilder) ErrorHandleFunc(f func(err error)) *WsBuilder {
|
||||
b.wsConfig.ErrorHandleFunc = f
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *WsBuilder) ConnectSuccessAfterSendMessage(msg func() []byte) *WsBuilder {
|
||||
b.wsConfig.ConnectSuccessAfterSendMessage = msg
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *WsBuilder) Build() *WsConn {
|
||||
wsConn := &WsConn{WsConfig: *b.wsConfig}
|
||||
return wsConn.NewWs()
|
||||
}
|
||||
|
||||
func (ws *WsConn) NewWs() *WsConn {
|
||||
if ws.HeartbeatIntervalTime == 0 {
|
||||
ws.readDeadLineTime = time.Minute
|
||||
} else {
|
||||
ws.readDeadLineTime = ws.HeartbeatIntervalTime * 2
|
||||
}
|
||||
|
||||
if err := ws.connect(); err != nil {
|
||||
log.Error("[" + ws.WsUrl + "] " + err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
ws.close = make(chan bool, 1)
|
||||
ws.pingMessageBufferChan = make(chan []byte, 10)
|
||||
ws.pongMessageBufferChan = make(chan []byte, 10)
|
||||
ws.closeMessageBufferChan = make(chan []byte, 10)
|
||||
ws.writeBufferChan = make(chan []byte, 10)
|
||||
ws.reConnectLock = new(sync.Mutex)
|
||||
|
||||
go ws.writeRequest()
|
||||
go ws.receiveMessage()
|
||||
|
||||
//if ws.ConnectSuccessAfterSendMessage != nil {
|
||||
// msg := ws.ConnectSuccessAfterSendMessage()
|
||||
// if msg != nil{
|
||||
// ws.SendMessage(msg)
|
||||
// log.ErrorLogMsg("[ws] " + ws.WsUrl + " execute the connect success after send message=" + string(msg))
|
||||
// }else {
|
||||
// log.ErrorLogMsg("执行重新连接后执行的登入函数[ws] " + ws.WsUrl + " ,send message=" + string(msg))
|
||||
// }
|
||||
//}
|
||||
|
||||
return ws
|
||||
}
|
||||
|
||||
func (ws *WsConn) connect() error {
|
||||
const maxRetries = 5 // 最大重试次数
|
||||
const retryDelay = 2 * time.Second // 每次重试的延迟时间
|
||||
|
||||
var wsConn *websocket.Conn
|
||||
var resp *http.Response
|
||||
var err error
|
||||
|
||||
// 重试机制
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
if ws.ProxyUrl != "" {
|
||||
proxy, err := url.Parse(ws.ProxyUrl)
|
||||
if err == nil {
|
||||
// log.Info("[ws][%s] proxy url:%s", zap.String("ws.WsUrl", ws.WsUrl))
|
||||
dialer.Proxy = http.ProxyURL(proxy)
|
||||
} else {
|
||||
log.Error("[ws][" + ws.WsUrl + "] parse proxy url [" + ws.ProxyUrl + "] err: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试连接
|
||||
wsConn, resp, err = dialer.Dial(ws.WsUrl, http.Header(ws.ReqHeaders))
|
||||
if err != nil {
|
||||
log.Error(fmt.Sprintf("[ws][%s] Dial attempt %d failed: %s", ws.WsUrl, attempt, err.Error()))
|
||||
|
||||
// 如果开启了请求数据转储,打印响应信息
|
||||
if ws.IsDump && resp != nil {
|
||||
dumpData, _ := httputil.DumpResponse(resp, true)
|
||||
log.Info(fmt.Sprintf("[ws][%s] Response dump: %s", ws.WsUrl, string(dumpData)))
|
||||
}
|
||||
|
||||
// 达到最大重试次数,返回错误
|
||||
if attempt == maxRetries {
|
||||
return fmt.Errorf("达到最大重试次数 [ws][%s]: %v", ws.WsUrl, err)
|
||||
}
|
||||
|
||||
// 等待一段时间后重试
|
||||
time.Sleep(retryDelay)
|
||||
} else {
|
||||
// 连接成功,退出循环
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 设置读取超时时间
|
||||
wsConn.SetReadDeadline(time.Now().Add(ws.readDeadLineTime))
|
||||
|
||||
// 如果开启了请求数据转储,打印响应信息
|
||||
if ws.IsDump && resp != nil {
|
||||
dumpData, _ := httputil.DumpResponse(resp, true)
|
||||
log.Info(fmt.Sprintf("[ws][%s] Response dump: %s", ws.WsUrl, string(dumpData)))
|
||||
}
|
||||
|
||||
// 记录连接成功的日志
|
||||
log.Info("[ws][" + ws.WsUrl + "] connected")
|
||||
ws.c = wsConn
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws *WsConn) reconnect() {
|
||||
ws.reConnectLock.Lock()
|
||||
defer ws.reConnectLock.Unlock()
|
||||
|
||||
ws.c.Close() //主动关闭一次
|
||||
var err error
|
||||
for retry := 1; retry <= 100; retry++ {
|
||||
err = ws.connect()
|
||||
if err != nil {
|
||||
log.Error("[ws] [" + ws.WsUrl + "] websocket reconnect fail , " + err.Error())
|
||||
} else {
|
||||
break
|
||||
}
|
||||
time.Sleep(ws.WsConfig.reconnectInterval * time.Duration(retry))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Error("[ws] [" + ws.WsUrl + "] retry connect 100 count fail , begin exiting. ")
|
||||
ws.CloseWs()
|
||||
if ws.ErrorHandleFunc != nil {
|
||||
ws.ErrorHandleFunc(errors.New("retry reconnect fail"))
|
||||
}
|
||||
} else {
|
||||
//re subscribe
|
||||
if ws.ConnectSuccessAfterSendMessage != nil {
|
||||
msg := ws.ConnectSuccessAfterSendMessage()
|
||||
if msg != nil {
|
||||
ws.SendMessage(msg)
|
||||
//log.ErrorLogMsg("[ws] " + ws.WsUrl + " execute the connect success after send message=" + string(msg))
|
||||
} else {
|
||||
log.Error("执行重新连接后执行的登入函数[ws] " + ws.WsUrl + " ,send message=" + string(msg))
|
||||
}
|
||||
//ws.SendMessage(msg)
|
||||
//log.InfoLog("[ws] [" + ws.WsUrl + "] execute the connect success after send message=" + string(msg))
|
||||
time.Sleep(time.Second) //wait response
|
||||
}
|
||||
|
||||
for _, sub := range ws.subs {
|
||||
log.Info("[ws] re subscribe: " + string(sub))
|
||||
ws.SendMessage(sub)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WsConn) writeRequest() {
|
||||
var (
|
||||
heartTimer *time.Timer
|
||||
err error
|
||||
)
|
||||
|
||||
if ws.HeartbeatIntervalTime == 0 {
|
||||
heartTimer = time.NewTimer(time.Hour)
|
||||
} else {
|
||||
heartTimer = time.NewTimer(ws.HeartbeatIntervalTime)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ws.close:
|
||||
log.Info("[ws][" + ws.WsUrl + "] close websocket , exiting write message goroutine.")
|
||||
return
|
||||
case d := <-ws.writeBufferChan:
|
||||
err = ws.c.WriteMessage(websocket.TextMessage, d)
|
||||
case d := <-ws.pingMessageBufferChan:
|
||||
err = ws.c.WriteMessage(websocket.PingMessage, d)
|
||||
case d := <-ws.pongMessageBufferChan:
|
||||
err = ws.c.WriteMessage(websocket.PongMessage, d)
|
||||
case d := <-ws.closeMessageBufferChan:
|
||||
err = ws.c.WriteMessage(websocket.CloseMessage, d)
|
||||
case <-heartTimer.C:
|
||||
if ws.HeartbeatIntervalTime > 0 {
|
||||
err = ws.c.WriteMessage(websocket.TextMessage, ws.HeartbeatData())
|
||||
heartTimer.Reset(ws.HeartbeatIntervalTime)
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Info("[ws][" + ws.WsUrl + "] write message " + err.Error())
|
||||
//time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WsConn) Subscribe(subEvent interface{}) error {
|
||||
data, err := sonic.Marshal(subEvent)
|
||||
if err != nil {
|
||||
log.Error("[ws]["+ws.WsUrl+"] json encode error , ", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
//ws.writeBufferChan <- data
|
||||
ws.SendMessage(data)
|
||||
ws.subs = append(ws.subs, data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws *WsConn) SendMessage(msg []byte) {
|
||||
defer func() {
|
||||
//打印panic的错误信息
|
||||
if err := recover(); err != nil { //产生了panic异常
|
||||
fmt.Printf("SendMessage,panic: %s\r\n", err)
|
||||
}
|
||||
}()
|
||||
ws.writeBufferChan <- msg
|
||||
}
|
||||
|
||||
func (ws *WsConn) SendPingMessage(msg []byte) {
|
||||
ws.pingMessageBufferChan <- msg
|
||||
}
|
||||
|
||||
func (ws *WsConn) SendPongMessage(msg []byte) {
|
||||
ws.pongMessageBufferChan <- msg
|
||||
}
|
||||
|
||||
func (ws *WsConn) SendCloseMessage(msg []byte) {
|
||||
ws.closeMessageBufferChan <- msg
|
||||
}
|
||||
|
||||
func (ws *WsConn) SendJsonMessage(m interface{}) error {
|
||||
data, err := sonic.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//ws.writeBufferChan <- data
|
||||
ws.SendMessage(data)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws *WsConn) receiveMessage() {
|
||||
//exit
|
||||
ws.c.SetCloseHandler(func(code int, text string) error {
|
||||
log.Info("[ws][" + ws.WsUrl + "] websocket exiting [code=" + utility.IntTostring(code) + " , text=" + text + "]")
|
||||
//ws.CloseWs()
|
||||
return nil
|
||||
})
|
||||
|
||||
ws.c.SetPongHandler(func(pong string) error {
|
||||
// log.Info("[" + ws.WsUrl + "] received [pong] " + pong)
|
||||
ws.c.SetReadDeadline(time.Now().Add(ws.readDeadLineTime))
|
||||
return nil
|
||||
})
|
||||
|
||||
ws.c.SetPingHandler(func(ping string) error {
|
||||
// log.Info("[" + ws.WsUrl + "] received [ping] " + ping)
|
||||
ws.c.SetReadDeadline(time.Now().Add(ws.readDeadLineTime))
|
||||
return nil
|
||||
})
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ws.close:
|
||||
log.Info("[ws][" + ws.WsUrl + "] close websocket , exiting receive message goroutine.")
|
||||
return
|
||||
default:
|
||||
t, msg, err := ws.c.ReadMessage()
|
||||
if err != nil {
|
||||
log.Info("ws.c.ReadMessage[ws][" + ws.WsUrl + "] " + err.Error())
|
||||
if ws.IsAutoReconnect {
|
||||
log.Info("[ws][" + ws.WsUrl + "] Unexpected Closed , Begin Retry Connect.")
|
||||
ws.reconnect()
|
||||
continue
|
||||
}
|
||||
|
||||
if ws.ErrorHandleFunc != nil {
|
||||
ws.ErrorHandleFunc(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
// Log.Debug(string(msg))
|
||||
ws.c.SetReadDeadline(time.Now().Add(ws.readDeadLineTime))
|
||||
switch t {
|
||||
case websocket.TextMessage:
|
||||
ws.ProtoHandleFunc(msg)
|
||||
case websocket.BinaryMessage:
|
||||
if ws.DecompressFunc == nil {
|
||||
ws.ProtoHandleFunc(msg)
|
||||
} else {
|
||||
msg2, err := ws.DecompressFunc(msg)
|
||||
if err != nil {
|
||||
log.Error("[ws] decompress error " + ws.WsUrl + err.Error())
|
||||
} else {
|
||||
ws.ProtoHandleFunc(msg2)
|
||||
}
|
||||
}
|
||||
// case websocket.CloseMessage:
|
||||
// ws.CloseWs()
|
||||
default:
|
||||
log.Error("[ws][" + ws.WsUrl + "] error websocket message type , content is " + string(msg))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WsConn) CloseWs() {
|
||||
defer func() {
|
||||
//打印panic的错误信息
|
||||
if err := recover(); err != nil { //产生了panic异常
|
||||
fmt.Printf("CloseWs,panic: %s\r\n", err)
|
||||
}
|
||||
}()
|
||||
//ws.close <- true
|
||||
close(ws.close)
|
||||
close(ws.writeBufferChan)
|
||||
close(ws.closeMessageBufferChan)
|
||||
close(ws.pingMessageBufferChan)
|
||||
close(ws.pongMessageBufferChan)
|
||||
|
||||
err := ws.c.Close()
|
||||
if err != nil {
|
||||
log.Error("CloseWs[ws]["+ws.WsUrl+"] close websocket error ,", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func (ws *WsConn) clearChannel(c chan struct{}) {
|
||||
for {
|
||||
if len(c) > 0 {
|
||||
<-c
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
73
services/fileservice/clearlogs.go
Normal file
73
services/fileservice/clearlogs.go
Normal file
@ -0,0 +1,73 @@
|
||||
package fileservice
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go-admin/app/admin/models"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-admin-team/go-admin-core/sdk/config"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func ClearLogs(orm *gorm.DB) {
|
||||
dir := config.LoggerConfig.Path
|
||||
|
||||
if dir == "" {
|
||||
dir = "temp/logs"
|
||||
}
|
||||
|
||||
// 检查文件夹是否存在
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
fmt.Printf("Directory %s does not exist, skipping cleanup.\n", dir)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前时间
|
||||
now := time.Now()
|
||||
expirateDay := 7
|
||||
var sysConfig models.SysConfig
|
||||
|
||||
orm.Model(&sysConfig).Where("config_key = ?", "log_expirate_date").First(&sysConfig)
|
||||
|
||||
if sysConfig.ConfigValue != "" {
|
||||
day, _ := strconv.Atoi(sysConfig.ConfigValue)
|
||||
|
||||
if day > 0 {
|
||||
expirateDay = day
|
||||
}
|
||||
}
|
||||
|
||||
// 遍历指定文件夹中的所有文件
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 只处理普通文件
|
||||
if !info.IsDir() {
|
||||
// 获取文件的修改时间
|
||||
modTime := info.ModTime()
|
||||
|
||||
// 计算文件修改时间与当前时间的差值
|
||||
duration := now.Sub(modTime)
|
||||
|
||||
// 如果文件超过7天,则删除
|
||||
if duration > time.Duration(expirateDay)*24*time.Hour {
|
||||
fmt.Printf("Deleting file: %s (Last modified: %s)\n", path, modTime)
|
||||
err := os.Remove(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Error walking the path %v: %v\n", dir, err)
|
||||
}
|
||||
}
|
||||
234
services/futureservice/binancemarket.go
Normal file
234
services/futureservice/binancemarket.go
Normal file
@ -0,0 +1,234 @@
|
||||
package futureservice
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go-admin/common/const/rediskey"
|
||||
"go-admin/common/global"
|
||||
"go-admin/common/helper"
|
||||
"go-admin/config"
|
||||
"go-admin/pkg/utility"
|
||||
"go-admin/services/binanceservice"
|
||||
"go-admin/services/excservice"
|
||||
"sync"
|
||||
|
||||
"go-admin/models"
|
||||
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
baseBinanceWsUrlAll = "wss://fstream.binance.com/stream?streams="
|
||||
wsBin *excservice.BinanceWs
|
||||
binSetKey = make(map[string]bool)
|
||||
binSetKeyMu sync.RWMutex
|
||||
|
||||
quoteAssetSymbols = []string{"USDT"}
|
||||
)
|
||||
|
||||
type BaseWsDepthStream struct {
|
||||
Stream string `json:"stream"` //
|
||||
Data models.UFuturesDepthBin `json:"data"` //数据
|
||||
}
|
||||
|
||||
// StartBinanceProWs 启动币安现货市场推送
|
||||
// workType: normal-常规任务 trigger-主动触发任务
|
||||
func StartBinanceProWs(workType string) {
|
||||
if wsBin == nil {
|
||||
wsBin = excservice.NewBinanceWs(baseBinanceWsUrlAll, "")
|
||||
}
|
||||
|
||||
if wsBin == nil {
|
||||
log.Error("实例化wsBin失败")
|
||||
return
|
||||
}
|
||||
|
||||
if wsBin != nil && config.ExtConfig.ProxyUrl != "" {
|
||||
wsBin.SetProxyUrl(config.ExtConfig.ProxyUrl)
|
||||
}
|
||||
wsBin.WorkType = workType
|
||||
|
||||
wsBin.SetAllCallbacks(HandleWsAll, HandleWsAllKline)
|
||||
//订阅所有行情
|
||||
subscribeAll(wsBin, "!miniTicker@arr")
|
||||
|
||||
}
|
||||
|
||||
func subscribeAll(ws *excservice.BinanceWs, subscribe string) {
|
||||
err := ws.SubscribeAll(subscribe)
|
||||
|
||||
if err != nil {
|
||||
log.Error("订阅流失败", zap.String("streams", subscribe), zap.Error(err))
|
||||
} else {
|
||||
log.Info("发起订阅", subscribe)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleWsAll 处理从WebSocket接收到的消息
|
||||
func HandleWsAll(msg []byte) {
|
||||
|
||||
if bytes.Contains(msg, []byte("miniTicker@arr")) {
|
||||
handleTickerAllMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
根据ws 推送值获取 symbol 和交易对信息
|
||||
- @dataMap 数据源 dataMap和symbol二选一
|
||||
- @symbol 交易对 dataMap和symbol二选一
|
||||
*/
|
||||
func getWsRespTradeSet(dataMap map[string]interface{}, symbol string, tradeSet *models.TradeSet) (string, error) {
|
||||
if symbol == "" {
|
||||
symbol = dataMap["s"].(string)
|
||||
|
||||
if symbol == "" {
|
||||
return symbol, errors.New("交易对为空")
|
||||
}
|
||||
}
|
||||
cacheStr, err := helper.DefaultRedis.GetString(fmt.Sprintf(global.TICKER_FUTURES, global.EXCHANGE_BINANCE, symbol))
|
||||
|
||||
if err != nil {
|
||||
// log.Error("获取缓存失败", symbol, err)
|
||||
return symbol, errors.New("获取缓存失败 " + err.Error())
|
||||
}
|
||||
|
||||
err = sonic.Unmarshal([]byte(cacheStr), tradeSet)
|
||||
|
||||
if err != nil {
|
||||
return symbol, errors.New("对象转换失败 " + err.Error())
|
||||
}
|
||||
|
||||
return symbol, nil
|
||||
}
|
||||
|
||||
// handleTickerMessage 处理ticker@all消息
|
||||
func handleTickerAllMessage(msg []byte) {
|
||||
dataAll := tickerAllMessage{}
|
||||
if err := sonic.Unmarshal(msg, &dataAll); err != nil {
|
||||
log.Error("解码ticker@all消息失败", zap.Error(err))
|
||||
return
|
||||
}
|
||||
// dataMap, ok := dataAll["data"].([]map[string]interface{})
|
||||
if len(dataAll.Data) <= 0 {
|
||||
log.Error("ticker消息不包含有效数据字段")
|
||||
return
|
||||
}
|
||||
|
||||
pairs := make([]map[string]interface{}, 0)
|
||||
trades := make([]models.TradeSet, 0)
|
||||
pairVal, _ := helper.DefaultRedis.GetString(rediskey.FutSymbolTicker)
|
||||
|
||||
if pairVal != "" {
|
||||
err := sonic.Unmarshal([]byte(pairVal), &pairs)
|
||||
if err != nil {
|
||||
log.Error("获取redis数据失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
for _, data := range dataAll.Data {
|
||||
symbol := data["s"].(string)
|
||||
|
||||
if symbol == "" || !utility.HasSuffix(symbol, quoteAssetSymbols) {
|
||||
continue
|
||||
}
|
||||
|
||||
tradeSet := models.TradeSet{}
|
||||
symbol, err := getWsRespTradeSet(data, "", &tradeSet)
|
||||
|
||||
if err != nil {
|
||||
// log.Debug(symbol, "ticker@all ws处理失败", err)
|
||||
continue
|
||||
}
|
||||
tradeSet.LastPrice = utility.StringFloat64Cut(data["c"].(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.LowPrice = utility.StringFloat64Cut(data["l"].(string), int32(tradeSet.PriceDigit))
|
||||
tradeSet.Volume = utility.StringFloat64Cut(data["v"].(string), int32(tradeSet.AmountDigit))
|
||||
tradeSet.QuoteVolume = utility.StringFloat64Cut(data["q"].(string), 5)
|
||||
hasData := false
|
||||
trades = append(trades, tradeSet)
|
||||
|
||||
tradeSetVal, _ := sonic.MarshalString(&tradeSet)
|
||||
if tradeSetVal != "" {
|
||||
if err := helper.DefaultRedis.SetString(fmt.Sprintf(global.TICKER_FUTURES, global.EXCHANGE_BINANCE, symbol), tradeSetVal); err != nil {
|
||||
log.Error(symbol, "ticker@all ws处理失败", err)
|
||||
}
|
||||
}
|
||||
|
||||
for index := range pairs {
|
||||
if cacheSymbol, ok := pairs[index]["symbol"].(string); ok {
|
||||
if cacheSymbol == symbol {
|
||||
pairs[index]["price"] = tradeSet.LastPrice
|
||||
hasData = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hasData {
|
||||
pairs = append(pairs, map[string]interface{}{
|
||||
"symbol": symbol,
|
||||
"price": tradeSet.LastPrice,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(trades) > 0 {
|
||||
|
||||
for index := range trades {
|
||||
if wsBin.WorkType == "normal" {
|
||||
//主单触发
|
||||
utility.SafeGoParam(binanceservice.JudgeFuturesPrice, trades[index])
|
||||
//止损信息
|
||||
utility.SafeGoParam(binanceservice.JudgeFuturesStoplossPrice, trades[index])
|
||||
//加仓信息
|
||||
utility.SafeGoParam(binanceservice.JudgeFuturesAddPositionPrice, trades[index])
|
||||
//对冲平仓
|
||||
utility.SafeGoParam(binanceservice.JudgeFuturesHedgeClosePosition, trades[index])
|
||||
//保险对冲
|
||||
utility.SafeGoParam(binanceservice.JudgeFuturesProtectHedge, trades[index])
|
||||
} else {
|
||||
//合约对冲主动平仓
|
||||
// utility.SafeGoParam(binanceservice.FutureClosePositionTrigger, trades[index])
|
||||
binanceservice.FutureClosePositionTrigger(trades[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if wsBin.WorkType == "normal" {
|
||||
if len(pairs) > 0 {
|
||||
pairVal, _ = sonic.MarshalString(&pairs)
|
||||
|
||||
if pairVal != "" {
|
||||
if err := helper.DefaultRedis.SetString(rediskey.FutSymbolTicker, pairVal); err != nil {
|
||||
log.Error("pair@all ws处理合约价格失败", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HandleWsAllKline 处理kline推送结果
|
||||
func HandleWsAllKline(msg []byte, tradeSet models.TradeSet) {
|
||||
|
||||
}
|
||||
|
||||
type WskLineData struct {
|
||||
Line string `json:"i"` //"1m",K线间隔
|
||||
Timestamp int64 `json:"t"` // 这根K线的起始时间
|
||||
Open string `json:"o"` // 这根K线期间第一笔成交价
|
||||
Close string `json:"c"` // 这根K线期间末一笔成交价
|
||||
High string `json:"h"` // 这根K线期间最高成交价
|
||||
Low string `json:"l"` // 这根K线期间最低成交价
|
||||
Vol string `json:"v"` // 这根K线期间成交量
|
||||
QuoteVolume string `json:"q"` // 这根K线期间成交额
|
||||
}
|
||||
|
||||
type tickerAllMessage struct {
|
||||
Stream string `json:"stream"`
|
||||
Data []map[string]interface{} `json:"data"`
|
||||
}
|
||||
78
services/scriptservice/order.go
Normal file
78
services/scriptservice/order.go
Normal file
@ -0,0 +1,78 @@
|
||||
package scriptservice
|
||||
|
||||
import (
|
||||
"github.com/bytedance/sonic"
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
sysservice "github.com/go-admin-team/go-admin-core/sdk/service"
|
||||
"go-admin/app/admin/models"
|
||||
"go-admin/app/admin/service"
|
||||
"go-admin/app/admin/service/dto"
|
||||
"go-admin/common/const/rediskey"
|
||||
"go-admin/common/helper"
|
||||
"gorm.io/gorm"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type PreOrder struct {
|
||||
}
|
||||
|
||||
const GoroutineNum = 5
|
||||
|
||||
func (receiver *PreOrder) AddOrder(orm *gorm.DB) {
|
||||
var wg sync.WaitGroup
|
||||
for i := 1; i <= GoroutineNum; i++ {
|
||||
wg.Add(1)
|
||||
go workerWithLock(orm, &wg)
|
||||
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func workerWithLock(orm *gorm.DB, wg *sync.WaitGroup) {
|
||||
defer func() {
|
||||
wg.Done()
|
||||
}()
|
||||
scriptId, err := helper.DefaultRedis.LPopList(rediskey.PreOrderScriptList)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var scriptInfo models.LinePreScript
|
||||
err = orm.Model(&models.LinePreScript{}).Where("id = ? AND status = '0'", scriptId).Find(&scriptInfo).Error
|
||||
if err != nil {
|
||||
log.Error("获取脚本记录失败mysql err:", err)
|
||||
return
|
||||
}
|
||||
if scriptInfo.Id > 0 {
|
||||
orm.Model(&models.LinePreScript{}).Where("id = ?", scriptId).Update("status", "1")
|
||||
}
|
||||
|
||||
params := scriptInfo.ScriptParams
|
||||
var batchReq dto.LineBatchAddPreOrderReq
|
||||
sonic.Unmarshal([]byte(params), &batchReq)
|
||||
errs := make([]error, 0)
|
||||
errStr := make([]string, 0)
|
||||
order := service.LinePreOrder{
|
||||
Service: sysservice.Service{Orm: orm},
|
||||
}
|
||||
order.AddBatchPreOrder(&batchReq, nil, &errs)
|
||||
|
||||
if len(errs) > 0 {
|
||||
//e.Logger.Error(err)
|
||||
for _, err2 := range errs {
|
||||
errStr = append(errStr, err2.Error())
|
||||
}
|
||||
//e.Error(500, nil, strings.Join(errStr, ","))
|
||||
orm.Model(&models.LinePreScript{}).Where("id = ?", scriptId).Updates(map[string]interface{}{
|
||||
"status": "2",
|
||||
"desc": strings.Join(errStr, ","),
|
||||
})
|
||||
return
|
||||
} else {
|
||||
orm.Model(&models.LinePreScript{}).Where("id = ?", scriptId).Updates(map[string]interface{}{
|
||||
"status": "2",
|
||||
"desc": "执行成功",
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
316
services/spotservice/binancemarket.go
Normal file
316
services/spotservice/binancemarket.go
Normal file
@ -0,0 +1,316 @@
|
||||
package spotservice
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go-admin/common/const/rediskey"
|
||||
"go-admin/common/global"
|
||||
"go-admin/common/helper"
|
||||
"go-admin/config"
|
||||
"go-admin/pkg/utility"
|
||||
"go-admin/services/binanceservice"
|
||||
"go-admin/services/excservice"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"go-admin/models"
|
||||
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
"github.com/shopspring/decimal"
|
||||
|
||||
"github.com/bytedance/sonic"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
baseBinanceWsUrlAll = "wss://stream.binance.com:9443/stream?streams="
|
||||
wsBin *excservice.BinanceWs
|
||||
binSetKey = make(map[string]bool)
|
||||
binSetKeyMu sync.RWMutex
|
||||
quoteAssetSymbols = []string{"USDT", "ETH", "BTC", "SOL", "BNB", "DOGE"}
|
||||
)
|
||||
|
||||
type BaseWsDepthStream struct {
|
||||
Stream string `json:"stream"` //
|
||||
Data models.DepthBin `json:"data"` //数据
|
||||
}
|
||||
|
||||
// GetBinance24hr 获取币安24小时价格变动信息
|
||||
func GetBinance24hr(coin, curr string) (models.Ticker24, error) {
|
||||
ticker, err := excservice.GetTicker(coin, curr)
|
||||
if err != nil {
|
||||
log.Error("获取ticker失败", zap.String("coin", coin), zap.Error(err))
|
||||
return models.Ticker24{}, err
|
||||
}
|
||||
return ticker, nil
|
||||
}
|
||||
|
||||
// GetProLastDeal 获取最新交易记录
|
||||
func GetProLastDeal(coin, currency string) ([]models.NewDealPush, error) {
|
||||
resp, err := excservice.GetTrades(coin, currency)
|
||||
if err != nil {
|
||||
log.Error("获取交易记录失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetProDepth 获取交易对的市场深度
|
||||
func GetProDepth(coin, currency string) (models.FiveItem, error) {
|
||||
depth, err := excservice.GetDepth(50, coin, currency)
|
||||
if err != nil {
|
||||
log.Error("获取市场深度失败", zap.String("coin", coin), zap.String("currency", currency), zap.Error(err))
|
||||
return models.FiveItem{}, err
|
||||
}
|
||||
return createFive2(depth), nil
|
||||
}
|
||||
|
||||
// 组合买卖5结果
|
||||
func createFive2(result models.DepthBin) models.FiveItem {
|
||||
five := models.FiveItem{}
|
||||
//卖5单
|
||||
slice1 := make([][]string, 0, 20)
|
||||
var sum1 float64
|
||||
|
||||
for _, item := range result.Asks {
|
||||
if len(item) > 1 {
|
||||
num0 := utility.FloatToStringZero(item[0])
|
||||
num1 := utility.FloatToStringZero(item[1])
|
||||
slice1 = append(slice1, []string{num0, num1})
|
||||
}
|
||||
|
||||
if len(item) == 2 {
|
||||
sum1 = utility.FloatAdd(sum1, utility.StringAsFloat(item[1]))
|
||||
}
|
||||
}
|
||||
|
||||
tempNumAsk := 0.0
|
||||
for k := 0; k < len(slice1); k++ {
|
||||
//(前数量总值+当前数量值)/总数量值
|
||||
nowNum := utility.StringAsFloat(slice1[k][1])
|
||||
add := utility.FloatAdd(tempNumAsk, nowNum)
|
||||
sc1 := utility.FloatDiv(add, sum1)
|
||||
tempNumAsk = add
|
||||
|
||||
sc2 := decimal.NewFromFloat(sc1).Truncate(3).String() //.Float64()
|
||||
slice1[k] = append(slice1[k], sc2)
|
||||
}
|
||||
five.Sell = slice1
|
||||
five.SellNum = sum1
|
||||
|
||||
//买5
|
||||
slice2 := make([][]string, 0, 20)
|
||||
var sum2 float64
|
||||
for _, item := range result.Bids {
|
||||
if len(item) > 1 {
|
||||
num0 := utility.FloatToStringZero(item[0])
|
||||
num1 := utility.FloatToStringZero(item[1])
|
||||
slice2 = append(slice2, []string{num0, num1})
|
||||
}
|
||||
//slice2 = append(slice2, item) //item.Price, item.Amount})
|
||||
if len(item) == 2 {
|
||||
sum2 = utility.FloatAdd(sum2, utility.StringAsFloat(item[1])) //sum2 + item.Amount
|
||||
}
|
||||
}
|
||||
tempNumBid := 0.0
|
||||
for k := 0; k < len(slice2); k++ {
|
||||
//(前数量总值+当前数量值)/总数量值
|
||||
nowNum := utility.StringAsFloat(slice2[k][1])
|
||||
add := utility.FloatAdd(tempNumBid, nowNum)
|
||||
sc1 := utility.FloatDiv(add, sum2)
|
||||
tempNumBid = add
|
||||
//sc1 := utility.FloatDiv(utility.StringAsFloat(slice2[k][1]), sum2) //(slice2[k][1]) / sum2
|
||||
sc2 := decimal.NewFromFloat(sc1).Truncate(3).String()
|
||||
slice2[k] = append(slice2[k], sc2)
|
||||
}
|
||||
five.Buy = slice2
|
||||
five.BuyNum = sum2
|
||||
return five
|
||||
}
|
||||
|
||||
// StartBinanceProWs 启动币安现货市场推送
|
||||
// workType normal-正常任务 trigger-主动触发任务
|
||||
func StartBinanceProWs(workType string) {
|
||||
if wsBin == nil {
|
||||
wsBin = excservice.NewBinanceWs(baseBinanceWsUrlAll, "")
|
||||
}
|
||||
|
||||
if wsBin == nil {
|
||||
log.Error("实例化wsBin失败")
|
||||
return
|
||||
}
|
||||
|
||||
if wsBin != nil && config.ExtConfig.ProxyUrl != "" {
|
||||
wsBin.SetProxyUrl(config.ExtConfig.ProxyUrl)
|
||||
}
|
||||
|
||||
wsBin.WorkType = workType
|
||||
wsBin.SetAllCallbacks(HandleWsAll, HandleWsAllKline)
|
||||
|
||||
subscribeToTradeSet(wsBin)
|
||||
}
|
||||
|
||||
// subscribeToTradeSet 订阅给定交易集合的所有流
|
||||
func subscribeToTradeSet(ws *excservice.BinanceWs) {
|
||||
// 订阅ticker、深度和交易的所有流
|
||||
streamNames := []string{
|
||||
"!miniTicker@arr",
|
||||
}
|
||||
err := ws.SubscribeAll(strings.Join(streamNames, "/"))
|
||||
if err != nil {
|
||||
log.Error("订阅流失败", zap.String("streams", strings.Join(streamNames, ",")), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// HandleWsAll 处理从WebSocket接收到的消息
|
||||
func HandleWsAll(msg []byte) {
|
||||
if bytes.Contains(msg, []byte("miniTicker@arr")) {
|
||||
handleTickerMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// handleTickerMessage 处理ticker消息
|
||||
func handleTickerMessage(msg []byte) {
|
||||
var streamResp tickerAllMessage
|
||||
if err := sonic.Unmarshal(msg, &streamResp); err != nil {
|
||||
log.Error("解码ticker消息失败", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
if len(streamResp.Data) == 0 {
|
||||
log.Error("ticker消息为空")
|
||||
return
|
||||
}
|
||||
|
||||
pairs := make([]map[string]interface{}, 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 {
|
||||
symbolName, ok := dataMap["s"].(string)
|
||||
|
||||
if !ok {
|
||||
log.Error("ticker消息不包含有效symbol字段")
|
||||
return
|
||||
}
|
||||
|
||||
if !utility.HasSuffix(symbolName, quoteAssetSymbols) {
|
||||
continue
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%s:%s", global.TICKER_SPOT, symbolName)
|
||||
tcVal, _ := helper.DefaultRedis.GetString(key)
|
||||
tradeSet := models.TradeSet{}
|
||||
if err := sonic.UnmarshalString(tcVal, &tradeSet); err != nil {
|
||||
if tcVal != "" {
|
||||
log.Error("解析tradeSet失败", zap.Error(err))
|
||||
}
|
||||
continue
|
||||
}
|
||||
tradeSet.LastPrice = utility.StringFloat64Cut(dataMap["c"].(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.LowPrice = utility.StringFloat64Cut(dataMap["l"].(string), int32(tradeSet.PriceDigit))
|
||||
tradeSet.Volume = utility.StringFloat64Cut(dataMap["v"].(string), int32(tradeSet.AmountDigit))
|
||||
tradeSet.QuoteVolume = utility.StringFloat64Cut(dataMap["q"].(string), 5)
|
||||
hasData := false
|
||||
|
||||
for index := range pairs {
|
||||
if symbol, ok := pairs[index]["symbol"].(string); ok {
|
||||
if symbol == symbolName {
|
||||
hasData = true
|
||||
pairs[index]["price"] = tradeSet.LastPrice
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hasData {
|
||||
pairs = append(pairs, map[string]interface{}{
|
||||
"symbol": symbolName,
|
||||
"price": tradeSet.LastPrice,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
trades = append(trades, tradeSet)
|
||||
tcVal, _ = sonic.MarshalString(&tradeSet)
|
||||
|
||||
if tcVal != "" {
|
||||
if err := helper.DefaultRedis.SetString(key, tcVal); err != nil {
|
||||
log.Error("redis保存交易对失败", tradeSet.Coin, tradeSet.Currency)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//判断触发现货下单
|
||||
if len(trades) > 0 {
|
||||
for index := range trades {
|
||||
if wsBin.WorkType == "normal" {
|
||||
// 主单触发
|
||||
utility.SafeGoParam(binanceservice.JudgeSpotPrice, trades[index])
|
||||
|
||||
// 止损单
|
||||
utility.SafeGoParam(binanceservice.JudgeSpotStopLoss, trades[index])
|
||||
|
||||
// 触发加仓
|
||||
utility.SafeGoParam(binanceservice.JudgeSpotAddPosition, trades[index])
|
||||
|
||||
// 对冲平仓
|
||||
utility.SafeGoParam(binanceservice.JudgeSpotHedgeClosePosition, trades[index])
|
||||
// 保险对冲
|
||||
utility.SafeGoParam(binanceservice.JudgeSpotProtectHedge, trades[index])
|
||||
} else {
|
||||
//todo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if wsBin.WorkType == "normal" {
|
||||
if len(pairs) > 0 {
|
||||
pairsVal, _ = sonic.MarshalString(&pairs)
|
||||
|
||||
if pairsVal != "" {
|
||||
if err := helper.DefaultRedis.SetString(rediskey.SpotSymbolTicker, pairsVal); err != nil {
|
||||
log.Error("redis保存", rediskey.SpotSymbolTicker, "失败,", pairs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// // HandleWsAllKline 处理kline推送结果
|
||||
func HandleWsAllKline(msg []byte, tradeSet models.TradeSet) {
|
||||
}
|
||||
|
||||
type WskLineData struct {
|
||||
Line string `json:"i"` //"1m",K线间隔
|
||||
Timestamp int64 `json:"t"` // 这根K线的起始时间
|
||||
Open string `json:"o"` // 这根K线期间第一笔成交价
|
||||
Close string `json:"c"` // 这根K线期间末一笔成交价
|
||||
High string `json:"h"` // 这根K线期间最高成交价
|
||||
Low string `json:"l"` // 这根K线期间最低成交价
|
||||
Vol string `json:"v"` // 这根K线期间成交量
|
||||
QuoteVolume string `json:"q"` // 这根K线期间成交额
|
||||
}
|
||||
|
||||
type tickerAllMessage struct {
|
||||
Stream string `json:"stream"`
|
||||
Data []map[string]interface{} `json:"data"`
|
||||
}
|
||||
|
||||
var (
|
||||
depthDuration int64 = 1700 //深度推送时间间隔,单位毫秒
|
||||
tickerDuration int64 = 1300 //24小时推送时间间隔,单位毫秒
|
||||
//allPushDuration int64 = 2000 //allpus推送时间间隔,单位毫秒
|
||||
marketDuration int64 = 3000 //首页+行情页推送时间间隔,单位毫秒
|
||||
dealDuration int64 = 1300 //最新成交推送时间间隔,单位毫秒
|
||||
klineDuration int64 = 800 //kline推送时间间隔,单位毫秒
|
||||
|
||||
usdtCode = "USDT"
|
||||
)
|
||||
334
services/udunservice/udunservice.go
Normal file
334
services/udunservice/udunservice.go
Normal file
@ -0,0 +1,334 @@
|
||||
package udunservice
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
log "github.com/go-admin-team/go-admin-core/logger"
|
||||
"github.com/shopspring/decimal"
|
||||
"go-admin/app/admin/models"
|
||||
"go-admin/pkg/timehelper"
|
||||
"go-admin/pkg/udunhelper"
|
||||
"go-admin/pkg/utility"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TradeCallback 充值,提币回调
|
||||
func TradeCallback(orm *gorm.DB, timestamp, nonce, body, sign string) string {
|
||||
|
||||
log.Error("充值,提币回调返回值", zap.String("body", body), zap.String("timestamp", timestamp), zap.String("nonce", nonce),
|
||||
zap.String("sign", sign))
|
||||
|
||||
//验证签名
|
||||
sign1 := udunhelper.CheckCallBackSign(timestamp, nonce, body)
|
||||
//udunhelper.CheckCallBackSign()
|
||||
if sign != sign1 {
|
||||
log.Error("充值,提币回调返回值 签名不正确", zap.String("body", body), zap.String("timestamp", timestamp), zap.String("nonce", nonce),
|
||||
zap.String("sign", sign))
|
||||
return "error"
|
||||
}
|
||||
|
||||
// 反序列化
|
||||
var trade udunhelper.CallBackRes
|
||||
if err := json.Unmarshal([]byte(body), &trade); err != nil {
|
||||
return "error"
|
||||
}
|
||||
// 充值
|
||||
if trade.TradeType == 1 {
|
||||
//
|
||||
return handleRecharge(orm, trade, timestamp)
|
||||
} else if trade.TradeType == 2 {
|
||||
//提币
|
||||
//return handleWithdraw(trade)
|
||||
}
|
||||
return "success"
|
||||
}
|
||||
|
||||
// 充值回调处理
|
||||
func handleRecharge(orm *gorm.DB, trade udunhelper.CallBackRes, timestamp string) string {
|
||||
// 检测是否已经写入,根据交易Hash判断是否已经存在充值记录
|
||||
var charge models.LineRecharge
|
||||
err := orm.Model(&models.LineRecharge{}).Where("txid = ?", trade.TxId).Find(&charge).Error
|
||||
if err != nil {
|
||||
log.Error("GetVtsRechargeByOrderNo", zap.Error(err))
|
||||
return "error"
|
||||
}
|
||||
|
||||
if charge.Status == "2" {
|
||||
// 已经成功,则不继续写入
|
||||
log.Error("已经成功,则不继续写入", zap.String("txId", trade.TxId))
|
||||
return "error"
|
||||
}
|
||||
|
||||
// 充币通知
|
||||
amount := getBalance(trade.Amount, trade.Decimals)
|
||||
fee := getBalance(trade.Fee, trade.Decimals)
|
||||
// 查询钱包信息
|
||||
var wallet models.LineWallet
|
||||
err = orm.Model(&models.LineWallet{}).Where("address = ?", trade.Address).Find(&wallet).Error
|
||||
//wallet, err := walletdb.GetVtsWalletByAddress(trade.Address)
|
||||
if err != nil {
|
||||
log.Error("GetVtsWalletByAddress", zap.Error(err), zap.String("address", trade.Address))
|
||||
return "error"
|
||||
}
|
||||
// 加载币种信息
|
||||
var udCoin models.LineUduncoin
|
||||
err = orm.Model(&models.LineUduncoin{}).Where("main_coin_type = ? AND coin_type = ?", trade.MainCoinType, trade.CoinType).Find(&udCoin).Error
|
||||
//udCoin, err := uduncoindb.GetVtsUduncoinItemByCoinType(trade.MainCoinType, trade.CoinType)
|
||||
if err != nil {
|
||||
log.Error("GetVtsUduncoinItemByCoinType", zap.Error(err), zap.String("MainCoinType", trade.MainCoinType),
|
||||
zap.String("CoinType", trade.CoinType))
|
||||
return "error"
|
||||
}
|
||||
height := utility.StringAsInteger(trade.BlockHigh)
|
||||
//currTime := time.Now()
|
||||
status := 1
|
||||
switch trade.Status {
|
||||
case 0:
|
||||
status = 1
|
||||
case 1:
|
||||
status = 1
|
||||
case 2:
|
||||
status = 3 // 失败
|
||||
case 3:
|
||||
status = 2 // 成功
|
||||
case 4:
|
||||
status = 3 // 失败
|
||||
}
|
||||
coinCode := udCoin.Symbol
|
||||
if strings.EqualFold(coinCode, "TRCUSDT") {
|
||||
coinCode = "USDT"
|
||||
}
|
||||
//coin := coinservice.CoinCache.GetByCode(coinCode)
|
||||
//if coin.Id == 0 {
|
||||
// loghelper.Error("TradeCallback 充值,提币回调 未找到系统对应的币种Id", zap.String("udCoin.Symbol", udCoin.Symbol))
|
||||
// return "error"
|
||||
//}
|
||||
t1 := timehelper.IntToTime(utility.StringAsInt64(timestamp))
|
||||
//timehelper.IntToTime()
|
||||
// 充值
|
||||
recharge := models.LineRecharge{
|
||||
UserId: wallet.UserId,
|
||||
Confirms: strconv.Itoa(0),
|
||||
TranType: strconv.Itoa(1),
|
||||
BlockIndex: strconv.Itoa(height),
|
||||
Amount: utility.FloatToStr(amount),
|
||||
Fee: utility.FloatToStr(fee),
|
||||
Account: "",
|
||||
Address: trade.Address,
|
||||
Txid: trade.TxId,
|
||||
BlockTime: t1,
|
||||
TimeReceived: t1,
|
||||
MainCoin: udCoin.MainSymbol,
|
||||
OrderNo: trade.TradeId, // 流水号
|
||||
Status: strconv.Itoa(status),
|
||||
State: strconv.Itoa(trade.Status),
|
||||
AddressFrom: "",
|
||||
}
|
||||
// 加载账户信息
|
||||
//beforeAmount := float64(0)
|
||||
//afterAmount := float64(0)
|
||||
if trade.Status == 3 {
|
||||
tx := orm.Begin()
|
||||
err := tx.Model(&models.LineRecharge{}).Create(&recharge).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
log.Error("create LineRecharge err", zap.Error(err), zap.String("wallet.UserId", strconv.FormatInt(wallet.UserId, 10)),
|
||||
zap.String("money", utility.FloatToStr(amount)))
|
||||
}
|
||||
err = tx.Model(&models.LineUser{}).Where("id = ?", wallet.UserId).Update("money", gorm.Expr("money + ?", amount)).Error
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
log.Error("update user money err", zap.Error(err), zap.String("wallet.UserId", strconv.FormatInt(wallet.UserId, 10)),
|
||||
zap.String("money", utility.FloatToStr(amount)))
|
||||
return "error"
|
||||
}
|
||||
tx.Commit()
|
||||
}
|
||||
|
||||
//hold := holddb.GetUserHold(wallet.UserId, recharge.CoinId)
|
||||
//if hold.Id == 0 {
|
||||
// hold = dbmodel.VtsHold{
|
||||
// Id: 0,
|
||||
// CoinId: recharge.CoinId,
|
||||
// UserId: wallet.UserId,
|
||||
// Num: 0,
|
||||
// UseNum: amount,
|
||||
// FreezeNum: 0,
|
||||
// CreateTime: currTime,
|
||||
// UpdateTime: currTime,
|
||||
// }
|
||||
// beforeAmount = 0
|
||||
// afterAmount = amount
|
||||
//} else {
|
||||
// beforeAmount = hold.UseNum
|
||||
// afterAmount = utility.FloatAdd(hold.UseNum, amount)
|
||||
// hold.UseNum = amount
|
||||
// hold.UpdateTime = currTime
|
||||
//}
|
||||
//// 流水
|
||||
//log := dbmodel.VtsCurrentHoldLog{
|
||||
// ID: 0,
|
||||
// UserID: wallet.UserId,
|
||||
// CoinID: recharge.CoinId,
|
||||
// UseFree: 1,
|
||||
// DealType: 5,
|
||||
// Remarks: "优顿充值",
|
||||
// RelateOrderNo: trade.TradeId,
|
||||
// Amount: amount,
|
||||
// BeforeAmount: beforeAmount,
|
||||
// AfterAmount: afterAmount,
|
||||
// Poundage: fee,
|
||||
// CreateTime: currTime,
|
||||
//}
|
||||
//
|
||||
//// 开启事务
|
||||
//tx, err := dbhelper.MasterPgdb.Beginx()
|
||||
//if err != nil {
|
||||
// loghelper.Error("Begin", zap.Error(err))
|
||||
// return "error"
|
||||
//}
|
||||
//if err = walletdb.RechargeInsert(recharge, tx); err != nil {
|
||||
// _ = tx.Rollback()
|
||||
// return "error"
|
||||
//}
|
||||
//// 账户写入
|
||||
//if trade.Status == 3 {
|
||||
// if err = holddb.UpdateHoldUseNum(hold, tx); err != nil {
|
||||
// _ = tx.Rollback()
|
||||
// return "error"
|
||||
// }
|
||||
// // 流水
|
||||
// if err = holddb.AddCurrentHoldLog(log, tx); err != nil {
|
||||
// _ = tx.Rollback()
|
||||
// loghelper.Error("handleRecharge", zap.Error(err))
|
||||
// return "error"
|
||||
// }
|
||||
//}
|
||||
//// 提交
|
||||
//if err = tx.Commit(); err != nil {
|
||||
// loghelper.Error("Commit", zap.Error(err))
|
||||
// return "error"
|
||||
//}
|
||||
////保存消息日志
|
||||
//templates := cmsdb.GetTempContent(3, 3) //充币通知
|
||||
//var template adminmodel.CmsTemplateContentDb
|
||||
//if len(templates) > 0 {
|
||||
// template = templates[0]
|
||||
//}
|
||||
//if template.TemplateId > 0 {
|
||||
// // 写入站内信
|
||||
// store := dbmodel.CmsMessageUserDB{
|
||||
// UserId: recharge.UserId,
|
||||
// MessageId: template.TemplateId,
|
||||
// IsRead: 1,
|
||||
// Type: 1,
|
||||
// CreateTime: time.Now(),
|
||||
// CategoryId: template.CategoryId,
|
||||
// Content: strings.ReplaceAll(template.Content, "{num}", " "+utility.FloatToStr(amount)+" "+coinCode),
|
||||
// LittleTitle: template.LittleTitle,
|
||||
// }
|
||||
// err = cmsdb.AddMessageUserItem(store)
|
||||
// if err != nil {
|
||||
// loghelper.Error("AddCmsMessageUser Error:", zap.Error(err))
|
||||
// }
|
||||
//}
|
||||
return "success"
|
||||
}
|
||||
|
||||
// 提币回调处理
|
||||
//func handleWithdraw(trade udunhelper.CallBackRes) string {
|
||||
// // 提币通知
|
||||
// status := 7
|
||||
// switch trade.Status {
|
||||
// case 0:
|
||||
// status = 7
|
||||
// case 1:
|
||||
// status = 7
|
||||
// case 2:
|
||||
// status = 9
|
||||
// case 3:
|
||||
// status = 8
|
||||
// case 4:
|
||||
// status = 9
|
||||
// }
|
||||
// if status < 8 {
|
||||
// return "success"
|
||||
// }
|
||||
// // 加载
|
||||
// data, err := walletdb.GetWithdrawItemByOrderNo(trade.BusinessId)
|
||||
// if err != nil {
|
||||
// return "error"
|
||||
// }
|
||||
// data.Status = status
|
||||
// data.State = trade.Status
|
||||
// data.TxId = trade.TxId
|
||||
// if status == 9 {
|
||||
// data.Remark = "未知原因"
|
||||
// }
|
||||
// num := data.SumNum //utility.FloatAddCut(data.Num, data.NumFee, 8)
|
||||
// // 更新这个表的状态
|
||||
// tx, err := dbhelper.MasterPgdb.Beginx()
|
||||
// if err != nil {
|
||||
// loghelper.Error("Begin", zap.Error(err))
|
||||
// return "error"
|
||||
// }
|
||||
// err = walletdb.WithdrawStatusByUd(data, tx)
|
||||
// if err != nil {
|
||||
// _ = tx.Rollback()
|
||||
// return "error"
|
||||
// }
|
||||
// if status == 9 {
|
||||
// //如果失败则把资金重新返回给用户,减去相应的冻结资金
|
||||
// err = holddb.UpdateHoldWithdraw(num, -num, data.UserId, data.CoinId, tx)
|
||||
// if err != nil {
|
||||
// _ = tx.Rollback()
|
||||
// return "error"
|
||||
// }
|
||||
// } else {
|
||||
// //成功的话则减去相应的冻结资金
|
||||
// err = holddb.UpdateHoldWithdraw(0, -num, data.UserId, data.CoinId, tx)
|
||||
// if err != nil {
|
||||
// _ = tx.Rollback()
|
||||
// return "error"
|
||||
// }
|
||||
// }
|
||||
// _ = tx.Commit()
|
||||
//
|
||||
// //保存消息日志
|
||||
// templates := cmsdb.GetTempContent(4, 3) // 提币通知
|
||||
// var template adminmodel.CmsTemplateContentDb
|
||||
// if len(templates) > 0 {
|
||||
// template = templates[0]
|
||||
// }
|
||||
// if template.TemplateId > 0 {
|
||||
// coin := coinservice.CoinCache.GetById(data.CoinId)
|
||||
// // 写入站内信
|
||||
// store := dbmodel.CmsMessageUserDB{
|
||||
// UserId: data.UserId,
|
||||
// MessageId: template.TemplateId,
|
||||
// IsRead: 1,
|
||||
// Type: 1,
|
||||
// CreateTime: time.Now(),
|
||||
// CategoryId: template.CategoryId,
|
||||
// Content: strings.ReplaceAll(template.Content, "{num}", " "+utility.FloatToStr(data.Num)+" "+coin.CoinCode),
|
||||
// LittleTitle: template.LittleTitle,
|
||||
// }
|
||||
// err = cmsdb.AddMessageUserItem(store)
|
||||
// if err != nil {
|
||||
// loghelper.Error("AddCmsMessageUser Error:", zap.Error(err))
|
||||
// }
|
||||
// }
|
||||
// return "success"
|
||||
//}
|
||||
|
||||
// getBalance 获取金额
|
||||
func getBalance(balance, decimals string) float64 {
|
||||
am, _ := strconv.ParseFloat(balance, 64)
|
||||
dec, _ := strconv.ParseFloat(decimals, 64)
|
||||
res := decimal.NewFromFloat(am / math.Pow(10, dec))
|
||||
amount, _ := res.Truncate(8).Float64()
|
||||
return amount
|
||||
}
|
||||
Reference in New Issue
Block a user