995 lines
30 KiB
Go
995 lines
30 KiB
Go
package binanceservice
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
DbModels "go-admin/app/admin/models"
|
||
"go-admin/app/admin/service/dto"
|
||
"go-admin/common/const/rediskey"
|
||
"go-admin/common/global"
|
||
"go-admin/common/helper"
|
||
"go-admin/models"
|
||
"go-admin/models/futuresdto"
|
||
"go-admin/pkg/httputils"
|
||
"go-admin/pkg/utility"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/shopspring/decimal"
|
||
"gorm.io/gorm"
|
||
|
||
"github.com/bytedance/sonic"
|
||
"github.com/go-redis/redis/v8"
|
||
|
||
"github.com/go-admin-team/go-admin-core/logger"
|
||
log "github.com/go-admin-team/go-admin-core/logger"
|
||
)
|
||
|
||
const (
|
||
futureApi = "https://fapi.binance.com"
|
||
)
|
||
|
||
type FutRestApi struct {
|
||
}
|
||
|
||
var FutErrorMaps = map[float64]string{
|
||
-2021: "订单已拒绝。请调整触发价并重新下订单。 对于买入/做多,止盈订单触发价应低于市场价,止损订单的触发价应高于市场价。卖出/做空则与之相反",
|
||
-4164: "少于最小下单金额",
|
||
-4061: "持仓方向需要设置为单向持仓。",
|
||
-2019: "保证金不足",
|
||
-1111: "金额设置错误。精度错误",
|
||
-1021: "请求的时间戳在recvWindow之外",
|
||
-1106: "发送了不需要的参数。",
|
||
-1109: "非有效账户",
|
||
-1110: "交易对不正确",
|
||
-1112: "交易对没有挂单",
|
||
-1114: "发送的TimeInForce参数不需要。",
|
||
-1115: "无效的timeInForce",
|
||
-1116: "无效订单类型。",
|
||
-1117: "无效买卖方向。",
|
||
-1121: "无效的交易对。",
|
||
-2010: "新订单被拒绝",
|
||
-2011: "取消订单被拒绝",
|
||
-2012: "批量取消失败",
|
||
-2013: "订单不存在。",
|
||
-2014: "API-key 格式无效。",
|
||
-2015: "无效的API密钥,IP或操作权限。",
|
||
-2018: "余额不足",
|
||
-2020: "无法成交",
|
||
-2022: "ReduceOnly订单被拒绝",
|
||
-2023: "用户正处于被强平模式",
|
||
-2024: "持仓不足",
|
||
-2025: "挂单量达到上限",
|
||
-4001: "价格小于0",
|
||
-4002: "价格超过最大值",
|
||
-4003: "数量小于0",
|
||
-4004: "数量小于最小值",
|
||
-4005: "数量大于最大值",
|
||
-4008: "价格精度小于0",
|
||
-4009: "最大价格小于最小价格",
|
||
-4029: "价格精度小数点位数不正确。",
|
||
-4031: "不正确的参数类型。",
|
||
-4047: "如果有挂单,仓位模式不能切换。",
|
||
-4048: "如果有仓位,仓位模式不能切换。",
|
||
-4059: "无需变更仓位方向",
|
||
-4117: "stop订单在触发中",
|
||
}
|
||
|
||
var reduceOnlyRate = 0.95
|
||
|
||
/*
|
||
获取资金费率
|
||
*/
|
||
func GetPremiumIndex() ([]futuresdto.FundingInfo, error) {
|
||
url := fmt.Sprintf("%s%s", futureApi, "/fapi/v1/premiumIndex")
|
||
mapData, err := httputils.NewHttpRequestWithFasthttp("GET", url, "", map[string]string{})
|
||
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if len(mapData) == 0 {
|
||
return nil, errors.New("获取交易对失败,或数量为空")
|
||
}
|
||
|
||
var res []futuresdto.FundingInfo
|
||
err = sonic.Unmarshal([]byte(mapData), &res)
|
||
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
for _, item := range res {
|
||
key := fmt.Sprintf("%s:%s", global.FUNDING_INFO_FUTURES, item.Symbol)
|
||
|
||
if item.NextFundingTime == 0 {
|
||
helper.DefaultRedis.DeleteString(key)
|
||
continue
|
||
}
|
||
|
||
val, err := sonic.Marshal(item)
|
||
|
||
if err != nil {
|
||
log.Error("序列化失败:", item.Symbol, err)
|
||
continue
|
||
}
|
||
|
||
err = helper.DefaultRedis.SetString(key, string(val))
|
||
|
||
if err != nil {
|
||
log.Error("保存资金费率失败:", item.Symbol, err)
|
||
continue
|
||
}
|
||
}
|
||
|
||
return nil, nil
|
||
}
|
||
|
||
/*
|
||
获取指定交易对资金费率
|
||
|
||
- @symbol 交易对名称
|
||
- @data 结果
|
||
*/
|
||
func GetFundingInfoBySymbol(symbol string, data *futuresdto.FundingInfo) error {
|
||
key := fmt.Sprintf("%s:%s", global.FUNDING_INFO_FUTURES, strings.ToUpper(symbol))
|
||
resData, err := helper.DefaultRedis.GetString(key)
|
||
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if resData == "" {
|
||
shouldReturn, err := GetAndReloadSymbol(symbol, data)
|
||
if shouldReturn {
|
||
return err
|
||
}
|
||
}
|
||
|
||
err = sonic.Unmarshal([]byte(resData), data)
|
||
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if data.NextFundingTime <= time.Now().Unix() {
|
||
shouldReturn, err := GetAndReloadSymbol(symbol, data)
|
||
if shouldReturn {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// 获取并更新费率
|
||
func GetAndReloadSymbol(symbol string, data *futuresdto.FundingInfo) (bool, error) {
|
||
datas, err := GetPremiumIndex()
|
||
|
||
if err != nil {
|
||
return true, err
|
||
}
|
||
|
||
for _, item := range datas {
|
||
if item.Symbol == strings.ToUpper(symbol) {
|
||
*data = item
|
||
|
||
return true, nil
|
||
}
|
||
}
|
||
return false, nil
|
||
}
|
||
|
||
// 获取合约交易对信息
|
||
func GetSymbols(data *futuresdto.FutExchangeInfo) error {
|
||
url := fmt.Sprintf("%s%s", futureApi, "/fapi/v1/exchangeInfo")
|
||
mapData, err := httputils.NewHttpRequestWithFasthttp("GET", url, "", map[string]string{})
|
||
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if len(mapData) == 0 {
|
||
return errors.New("获取合约交易对失败,或数量为空")
|
||
}
|
||
|
||
err = sonic.Unmarshal(mapData, data)
|
||
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// 获取并更新交易对
|
||
func GetAndReloadSymbols(data *map[string]models.TradeSet) error {
|
||
var info futuresdto.FutExchangeInfo
|
||
err := GetSymbols(&info)
|
||
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
maps := make(map[string]string, 0)
|
||
if len(info.Symbols) > 0 {
|
||
for _, item := range info.Symbols {
|
||
if item.Status == "TRADING" && strings.HasSuffix(item.Symbol, "USDT") {
|
||
tradeSet := models.TradeSet{}
|
||
tradeSet.Coin = item.BaseAsset
|
||
tradeSet.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)
|
||
case "MIN_NOTIONAL":
|
||
tradeSet.MinNotional = *filter.Notional
|
||
case "MAX_NOTIONAL":
|
||
tradeSet.MaxNotional = *filter.Notional
|
||
}
|
||
}
|
||
|
||
(*data)[item.Symbol] = tradeSet
|
||
|
||
key := fmt.Sprintf("%s:%s", global.TICKER_FUTURES, item.Symbol)
|
||
val, err := sonic.Marshal(tradeSet)
|
||
|
||
if err != nil {
|
||
log.Error("合约交易对序列化失败", err)
|
||
} else {
|
||
maps[key] = string(val)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if len(maps) > 0 {
|
||
if err = helper.DefaultRedis.BatchSet(&maps); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
func (e FutRestApi) Ticker() {
|
||
url := fmt.Sprintf("%s%s", futureApi, "/fapi/v1/ticker/price")
|
||
mapData, err := httputils.NewHttpRequestWithFasthttp("GET", url, "", map[string]string{})
|
||
if err != nil {
|
||
log.Error("获取合约交易对行情失败:err:", err)
|
||
}
|
||
|
||
if len(mapData) == 0 {
|
||
log.Error("获取合约交易对行情失败,或数量为空")
|
||
}
|
||
helper.DefaultRedis.SetString(rediskey.FutSymbolTicker, string(mapData))
|
||
}
|
||
|
||
// 获取合约行情
|
||
func GetTicker24h() ([]futuresdto.FutureTicker24h, error) {
|
||
url := fmt.Sprintf("%s%s", futureApi, "/fapi/v1/ticker/24hr")
|
||
|
||
mapData, err := httputils.NewHttpRequestWithFasthttp("GET", url, "", map[string]string{})
|
||
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if len(mapData) == 0 {
|
||
return nil, errors.New("获取合约交易对24h行情失败,或数量为空")
|
||
}
|
||
|
||
var data []futuresdto.FutureTicker24h
|
||
err = sonic.Unmarshal(mapData, &data)
|
||
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return data, err
|
||
}
|
||
|
||
// 初始化交易对行情
|
||
func InitSymbolsTicker24h(maps *map[string]models.TradeSet) (deletes []string, err error) {
|
||
tickers, err := GetTicker24h()
|
||
|
||
if err != nil {
|
||
return []string{}, err
|
||
}
|
||
deleteSymbol := make([]string, 0)
|
||
caches := make(map[string]string, 0)
|
||
priceChange := make([]*redis.Z, 0)
|
||
|
||
for _, item := range tickers {
|
||
symbol, exits := (*maps)[item.Symbol]
|
||
|
||
if !exits {
|
||
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
|
||
|
||
key := fmt.Sprintf("%s:%s", global.TICKER_FUTURES, item.Symbol)
|
||
if !strings.HasSuffix(item.Symbol, symbol.Currency) || item.Count <= 0 || utility.StringToFloat64(item.QuoteVolume) <= 0 {
|
||
helper.DefaultRedis.DeleteString(key)
|
||
deleteSymbol = append(deleteSymbol, item.Symbol)
|
||
continue
|
||
}
|
||
|
||
val, err := sonic.Marshal(symbol)
|
||
|
||
if err != nil {
|
||
log.Error("设置行情序列化报错", err)
|
||
}
|
||
|
||
tcKey := fmt.Sprintf("%s:%s", global.TICKER_FUTURES, item.Symbol)
|
||
caches[tcKey] = string(val)
|
||
priceChange = append(priceChange, &redis.Z{
|
||
Score: symbol.PriceChange,
|
||
Member: symbol.Coin + symbol.Currency,
|
||
})
|
||
}
|
||
|
||
if len(caches) > 0 {
|
||
err = helper.DefaultRedis.BatchSet(&caches)
|
||
|
||
if err != nil {
|
||
log.Error("批量行情保存失败", err)
|
||
|
||
return deleteSymbol, err
|
||
}
|
||
|
||
err = helper.DefaultRedis.BatchSortSet(global.UFUTURES_PRICE_CHANGE, priceChange)
|
||
|
||
if err != nil {
|
||
log.Error("批量涨跌幅保存失败", err)
|
||
|
||
return deleteSymbol, err
|
||
}
|
||
}
|
||
|
||
return deleteSymbol, nil
|
||
}
|
||
|
||
// 获取合约 24h行情
|
||
func GetTicker(coin string, data *futuresdto.MarketResp) error {
|
||
key := fmt.Sprintf("%s:%sUSDT", global.TICKER_FUTURES, coin)
|
||
cache, _ := helper.DefaultRedis.GetString(key)
|
||
|
||
if cache == "" {
|
||
|
||
} else {
|
||
var trade models.TradeSet
|
||
sonic.Unmarshal([]byte(cache), &trade)
|
||
|
||
if trade.Coin != "" {
|
||
data.Coin = trade.Coin
|
||
data.HighPrice = trade.HighPrice
|
||
data.LowPrice = trade.LowPrice
|
||
data.NewPrice = trade.LastPrice
|
||
data.Ratio = utility.Float64ToString(trade.PriceChange, -1)
|
||
data.AmountDigit = trade.AmountDigit
|
||
data.DealCoin = trade.Volume
|
||
data.DealAmt = trade.QuoteVolume
|
||
}
|
||
|
||
fiKey := fmt.Sprintf("%s:%sUSDT", global.FUNDING_INFO_FUTURES, coin)
|
||
fi, _ := helper.DefaultRedis.GetString(fiKey)
|
||
|
||
if fi != "" {
|
||
var fundingInfo futuresdto.FundingInfo
|
||
sonic.Unmarshal([]byte(fi), &fundingInfo)
|
||
|
||
if fundingInfo.NextFundingTime > 0 {
|
||
data.FundRate = strconv.FormatFloat(utility.ToFloat64(fundingInfo.LastFundingRate)*100, 'f', -1, 64)
|
||
data.MarkPrice = utility.StringFloat64Cut(fundingInfo.MarkPrice, int32(trade.PriceDigit))
|
||
data.IndexPrice = utility.StringFloat64Cut(fundingInfo.IndexPrice, int32(trade.PriceDigit))
|
||
|
||
if fundingInfo.NextFundingTime > 999999999999 {
|
||
data.NextFundingSec = int(fundingInfo.NextFundingTime / 1000)
|
||
} else {
|
||
data.NextFundingSec = int(fundingInfo.NextFundingTime)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// CheckKlineType 检查k线合法性
|
||
func CheckKlineType(kLineType string) bool {
|
||
for _, v := range []string{"1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "3d", "1w", "1M"} {
|
||
if v == kLineType {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// OrderPlace 合约下单
|
||
func (e FutRestApi) OrderPlace(orm *gorm.DB, params FutOrderPlace) error {
|
||
if orm == nil {
|
||
return errors.New("数据库实例为空")
|
||
}
|
||
err2 := params.CheckParams()
|
||
if err2 != nil {
|
||
return err2
|
||
}
|
||
var paramsMaps map[string]string
|
||
paramsMaps = map[string]string{
|
||
"symbol": params.Symbol,
|
||
"side": params.Side,
|
||
"quantity": params.Quantity.String(),
|
||
"type": strings.ToUpper(params.OrderType),
|
||
"newClientOrderId": params.NewClientOrderId,
|
||
}
|
||
if strings.ToUpper(params.OrderType) != "MARKET" { //不是市价
|
||
if strings.ToUpper(params.OrderType) == "LIMIT" {
|
||
paramsMaps["price"] = params.Price.String()
|
||
paramsMaps["timeInForce"] = "GTC"
|
||
}
|
||
if strings.ToUpper(params.OrderType) == "TAKE_PROFIT_MARKET" {
|
||
paramsMaps["timeInForce"] = "GTC"
|
||
paramsMaps["stopprice"] = params.Profit.String()
|
||
paramsMaps["workingType"] = "MARK_PRICE"
|
||
}
|
||
|
||
if strings.ToUpper(params.OrderType) == "STOP_MARKET" {
|
||
paramsMaps["stopprice"] = params.StopPrice.String()
|
||
paramsMaps["workingType"] = "MARK_PRICE"
|
||
paramsMaps["timeInForce"] = "GTC"
|
||
}
|
||
}
|
||
if strings.ToUpper(params.OrderType) != "STOP_MARKET" && strings.ToUpper(params.OrderType) != "TAKE_PROFIT_MARKET" {
|
||
if strings.ToUpper(params.Side) == "BUY" {
|
||
paramsMaps["positionSide"] = "LONG"
|
||
} else {
|
||
paramsMaps["positionSide"] = "SHORT"
|
||
}
|
||
} else {
|
||
if strings.ToUpper(params.Side) == "BUY" {
|
||
paramsMaps["positionSide"] = "SHORT"
|
||
} else {
|
||
paramsMaps["positionSide"] = "LONG"
|
||
}
|
||
}
|
||
var apiUserInfo DbModels.LineApiUser
|
||
err := orm.Model(&DbModels.LineApiUser{}).Where("id = ?", params.ApiId).Find(&apiUserInfo).Error
|
||
if err != nil {
|
||
return 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)
|
||
}
|
||
_, statusCode, err := client.SendFuturesRequestAuth("/fapi/v1/order", "POST", paramsMaps)
|
||
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 statusCode:%v 下单失败:%s", apiUserInfo.Id, params.Symbol, statusCode, err.Error())
|
||
}
|
||
code, ok := dataMap["code"]
|
||
if ok {
|
||
paramsVal, _ := sonic.MarshalString(¶msMaps)
|
||
log.Error("下单失败 参数:", paramsVal)
|
||
errContent := FutErrorMaps[code.(float64)]
|
||
|
||
if errContent == "" {
|
||
errContent = err.Error()
|
||
}
|
||
|
||
return fmt.Errorf("api_id:%d 交易对:%s 下单失败:%s", apiUserInfo.Id, params.Symbol, errContent)
|
||
}
|
||
if strings.Contains(err.Error(), "Margin is insufficient.") {
|
||
return fmt.Errorf("api_id:%d 交易对:%s 下单失败:%s", apiUserInfo.Id, params.Symbol, FutErrorMaps[-2019])
|
||
}
|
||
return fmt.Errorf("api_id:%d 交易对:%s 下单失败:%s", apiUserInfo.Id, params.Symbol, err.Error())
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ClosePositionB 平仓B对应的交易对
|
||
// bApiUserInfo B 账户api-user信息
|
||
// symbol 需要平仓的交易对
|
||
// closeType 平仓模式 ALL = 全平 reduceOnly = 只减仓
|
||
// func (e FutRestApi) ClosePositionB(orm *gorm.DB, bApiUserInfo *DbModels.LineApiUser, symbol string, closeType string) error {
|
||
// if bApiUserInfo == nil {
|
||
// return errors.New("缺失平仓账户信息")
|
||
// }
|
||
// if symbol == "" {
|
||
// return errors.New("缺失平仓交易对信息")
|
||
// }
|
||
// risks, err := e.GetPositionV3(bApiUserInfo, symbol)
|
||
// if err != nil {
|
||
// return err
|
||
// }
|
||
|
||
// for _, risk := range risks {
|
||
// if risk.Symbol == strings.ToUpper(symbol) {
|
||
// //持仓数量
|
||
// positionAmt, _ := decimal.NewFromString(risk.PositionAmt)
|
||
// side := "BUY"
|
||
// if positionAmt.GreaterThan(decimal.Zero) {
|
||
// side = "SELL"
|
||
// }
|
||
// var closeAmt decimal.Decimal
|
||
// if strings.ToUpper(closeType) == "ALL" { //全部平仓
|
||
// closeAmt = positionAmt
|
||
// } else {
|
||
// //如果是只减仓的话 数量=仓位数量*比例
|
||
// closeAmt = positionAmt.Mul(decimal.NewFromFloat(reduceOnlyRate))
|
||
// }
|
||
|
||
// err = e.ClosePosition(symbol, closeAmt, side, *bApiUserInfo, "MARKET", "", decimal.Zero)
|
||
// if err != nil {
|
||
// return err
|
||
// }
|
||
// orm.Model(&DbModels.LinePreOrder{}).Where("api_id = ? AND symbol = ?", bApiUserInfo.Id, symbol).Updates(map[string]interface{}{
|
||
// "status": "6",
|
||
// })
|
||
// }
|
||
// }
|
||
// return nil
|
||
// }
|
||
|
||
// 获取合约 持仓价格、数量
|
||
// symbol:交易对
|
||
// side:方向
|
||
// holdeData:持仓数据
|
||
func (e FutRestApi) GetHoldeData(apiInfo *DbModels.LineApiUser, symbol, side string, holdeData *HoldeData) error {
|
||
var holdes []PositionRisk
|
||
var err error
|
||
|
||
for x := 0; x < 3; x++ {
|
||
holdes, err = getSymbolHolde(e, apiInfo, symbol, side, holdeData)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if len(holdes) == 0 {
|
||
logger.Error("获取持仓信息,未查询到持仓信息")
|
||
time.Sleep(time.Second * 1)
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
|
||
//side=SELL&positionSide=LONG是平多,
|
||
//side=SELL&positionSide=SHORT是开空,
|
||
for _, item := range holdes {
|
||
positionAmount, _ := decimal.NewFromString(item.PositionAmt)
|
||
if side == "BUY" && (item.PositionSide == "BOTH" || item.PositionSide == "LONG") { //多
|
||
holdeData.AveragePrice, _ = decimal.NewFromString(item.EntryPrice)
|
||
holdeData.TotalQuantity = positionAmount.Abs()
|
||
} else if side == "SELL" && (item.PositionSide == "BOTH" || item.PositionSide == "SHORT") { //空
|
||
holdeData.AveragePrice, _ = decimal.NewFromString(item.EntryPrice)
|
||
holdeData.TotalQuantity = positionAmount.Abs()
|
||
}
|
||
}
|
||
|
||
if holdeData.AveragePrice.Cmp(decimal.Zero) == 0 {
|
||
holdesVal, _ := sonic.MarshalString(&holdes)
|
||
log.Error("均价错误 symbol:", symbol, " 数据:", holdesVal)
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// 获取代币持仓信息
|
||
func getSymbolHolde(e FutRestApi, apiInfo *DbModels.LineApiUser, symbol string, side string, holdeData *HoldeData) ([]PositionRisk, error) {
|
||
holdes, err := e.GetPositionV3(apiInfo, symbol)
|
||
|
||
if err != nil {
|
||
log.Error("订单回调-获取合约持仓信息失败:", err)
|
||
|
||
if strings.Contains(err.Error(), " EOF") || strings.Contains(err.Error(), "无效的API密钥,IP或操作权限") {
|
||
return nil, e.GetHoldeData(apiInfo, symbol, side, holdeData)
|
||
} else {
|
||
return nil, err
|
||
}
|
||
}
|
||
return holdes, nil
|
||
}
|
||
|
||
func (e FutRestApi) GetPositionV3(apiUserInfo *DbModels.LineApiUser, symbol string) ([]PositionRisk, error) {
|
||
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)
|
||
}
|
||
params := map[string]string{
|
||
"symbol": symbol,
|
||
"recvWindow": "5000",
|
||
}
|
||
|
||
resp, _, err := client.SendFuturesRequestAuth("/fapi/v3/positionRisk", "GET", params)
|
||
if err != nil {
|
||
var dataMap map[string]interface{}
|
||
if err2 := sonic.Unmarshal([]byte(err.Error()), &dataMap); err2 != nil {
|
||
return []PositionRisk{}, 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 []PositionRisk{}, fmt.Errorf("api_id:%d 交易对:%s 获取仓位:%s", apiUserInfo.Id, symbol, errContent)
|
||
}
|
||
}
|
||
risks := make([]PositionRisk, 0)
|
||
err = sonic.Unmarshal(resp, &risks)
|
||
if err != nil {
|
||
return []PositionRisk{}, fmt.Errorf("api_id:%d 交易对:%s 解析用户持仓信息失败:%s", apiUserInfo.Id, symbol, err.Error())
|
||
}
|
||
return risks, nil
|
||
}
|
||
|
||
// ClosePosition 合约平仓
|
||
// symbol 交易对
|
||
// orderSn 系统订单号
|
||
// quantity 平仓数量
|
||
// side 仓位方向 做多==BUY 做空 == SELL
|
||
// apiUserInfo 用户信息
|
||
// orderType 平仓是限价平 还是市价平 MARKET = 市价 LIMIT = 限价
|
||
// rate 限价平的价格比例 没有除100 的百分比
|
||
func (e FutRestApi) ClosePosition(symbol string, orderSn string, quantity decimal.Decimal, side string, positionSide string,
|
||
apiUserInfo DbModels.LineApiUser, orderType string, rate string, price decimal.Decimal) error {
|
||
endpoint := "/fapi/v1/order"
|
||
params := map[string]string{
|
||
"symbol": symbol,
|
||
"side": side,
|
||
"positionSide": positionSide,
|
||
"type": orderType,
|
||
"quantity": quantity.String(),
|
||
"newClientOrderId": orderSn,
|
||
}
|
||
|
||
if orderType == "LIMIT" {
|
||
key := fmt.Sprintf("%s:%s", global.TICKER_FUTURES, symbol)
|
||
tradeSet, _ := helper.GetObjString[models.TradeSet](helper.DefaultRedis, key)
|
||
rateFloat, _ := decimal.NewFromString(rate)
|
||
if rateFloat.GreaterThanOrEqual(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()
|
||
}
|
||
}
|
||
|
||
if orderType == "LIMIT" {
|
||
params["timeInForce"] = "GTC"
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
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 JudgeFuturesPrice(tradeSet models.TradeSet) {
|
||
preOrderVal, _ := helper.DefaultRedis.GetAllList(fmt.Sprintf(rediskey.PreFutOrderList, global.EXCHANGE_BINANCE))
|
||
db := GetDBConnection()
|
||
|
||
if len(preOrderVal) == 0 {
|
||
return
|
||
}
|
||
futApi := FutRestApi{}
|
||
|
||
for _, item := range preOrderVal {
|
||
preOrder := dto.PreOrderRedisList{}
|
||
if err := sonic.Unmarshal([]byte(item), &preOrder); err != nil {
|
||
log.Error("反序列化失败")
|
||
continue
|
||
}
|
||
|
||
if preOrder.Symbol != tradeSet.Coin+tradeSet.Currency {
|
||
continue
|
||
}
|
||
|
||
orderPrice, _ := decimal.NewFromString(preOrder.Price)
|
||
tradePrice, _ := decimal.NewFromString(tradeSet.LastPrice)
|
||
|
||
if orderPrice.Cmp(decimal.Zero) == 0 || tradePrice.Cmp(decimal.Zero) == 0 {
|
||
continue
|
||
}
|
||
|
||
//多
|
||
if (strings.ToUpper(preOrder.Site) == "BUY" && orderPrice.Cmp(tradePrice) >= 0) ||
|
||
(strings.ToUpper(preOrder.Site) == "SELL" && orderPrice.Cmp(tradePrice) <= 0) {
|
||
futTriggerOrder(db, &preOrder, item, futApi)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 分布式锁下单
|
||
func futTriggerOrder(db *gorm.DB, v *dto.PreOrderRedisList, item string, futApi FutRestApi) {
|
||
lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, v.ApiId, v.Symbol), 200, 5, 100*time.Millisecond)
|
||
|
||
if ok, err := lock.AcquireWait(context.Background()); err != nil {
|
||
log.Debug("获取锁失败", err)
|
||
return
|
||
} else if ok {
|
||
defer lock.Release()
|
||
|
||
key := fmt.Sprintf(rediskey.PreFutOrderList, global.EXCHANGE_BINANCE)
|
||
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(key, item)
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
hasrecord, _ := helper.DefaultRedis.IsElementInList(key, item)
|
||
|
||
if !hasrecord {
|
||
log.Debug("预下单缓存中不存在", item)
|
||
return
|
||
}
|
||
|
||
price, _ := decimal.NewFromString(v.Price)
|
||
num, _ := decimal.NewFromString(preOrder.Num)
|
||
|
||
if price.Cmp(decimal.Zero) == 0 {
|
||
log.Error("价格不能为0")
|
||
return
|
||
}
|
||
|
||
params := FutOrderPlace{
|
||
ApiId: v.ApiId,
|
||
Symbol: v.Symbol,
|
||
Side: v.Site,
|
||
OrderType: preOrder.MainOrderType,
|
||
SideType: preOrder.MainOrderType,
|
||
Price: price,
|
||
Quantity: num,
|
||
NewClientOrderId: v.OrderSn,
|
||
}
|
||
preOrderVal, _ := sonic.MarshalString(&v)
|
||
|
||
if err := futApi.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(key, preOrderVal); err != nil {
|
||
log.Error("删除redis 预下单失败:", err)
|
||
}
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
if preOrderVal != "" {
|
||
if _, err := helper.DefaultRedis.LRem(key, preOrderVal); err != nil {
|
||
log.Error("删除redis 预下单失败:", err)
|
||
}
|
||
}
|
||
|
||
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")
|
||
}
|
||
|
||
return
|
||
} else {
|
||
log.Error("获取锁失败")
|
||
|
||
return
|
||
}
|
||
}
|
||
|
||
// CancelFutOrder 通过单个订单号取消合约委托
|
||
// symbol 交易对
|
||
// newClientOrderId 系统自定义订单号
|
||
func (e FutRestApi) CancelFutOrder(apiUserInfo DbModels.LineApiUser, symbol string, newClientOrderId string) error {
|
||
endpoint := "/fapi/v1/order"
|
||
params := map[string]string{
|
||
"symbol": symbol, //交易对
|
||
"origClientOrderId": newClientOrderId, //用户自定义订单号
|
||
}
|
||
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.SendFuturesRequestAuth(endpoint, "DELETE", params)
|
||
if err != nil {
|
||
var dataMap map[string]interface{}
|
||
if err2 := sonic.Unmarshal([]byte(err.Error()), &dataMap); err2 != nil {
|
||
return errors.New(fmt.Sprintf("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 errors.New(fmt.Sprintf("api_id:%d 交易对:%s 撤销合约委托出错:%s", apiUserInfo.Id, symbol, errContent))
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// CancelAllFutOrder 通过交易对取消合约委托
|
||
// symbol 交易对
|
||
func (e FutRestApi) CancelAllFutOrder(apiUserInfo DbModels.LineApiUser, symbol string) error {
|
||
endpoint := "/fapi/v1/allOpenOrders"
|
||
params := map[string]string{
|
||
"symbol": symbol, //交易对
|
||
"recvWindow": "5000",
|
||
}
|
||
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.SendFuturesRequestAuth(endpoint, "DELETE", 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)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// CancelBatchFutOrder 批量撤销订单
|
||
// symbol 交易对
|
||
// newClientOrderIdList 系统自定义的订单号, 最多支持10个订单
|
||
func (e FutRestApi) CancelBatchFutOrder(apiUserInfo DbModels.LineApiUser, symbol string, newClientOrderIdList []string) error {
|
||
if len(newClientOrderIdList) > 10 {
|
||
return errors.New(fmt.Sprintf("api_id:%d 交易对:%s 撤销批量合约委托出错:%s", apiUserInfo.Id, symbol, "最多支持10个订单"))
|
||
}
|
||
endpoint := "/fapi/v1/batchOrders"
|
||
marshal, _ := sonic.Marshal(newClientOrderIdList)
|
||
params := map[string]string{
|
||
"symbol": symbol, //交易对
|
||
"origClientOrderIdList": string(marshal),
|
||
}
|
||
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.SendFuturesRequestAuth(endpoint, "DELETE", params)
|
||
if err != nil {
|
||
log.Error("取消合约委托失败 参数:", params)
|
||
log.Error("取消合约委托失败 code:", code)
|
||
log.Error("取消合约委托失败 err:", err)
|
||
var dataMap map[string]interface{}
|
||
if err.Error() != "" {
|
||
if err2 := sonic.Unmarshal([]byte(err.Error()), &dataMap); err2 != nil {
|
||
return errors.New(fmt.Sprintf("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)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// CalcSymbolExchangeAmt 计算兑换成USDT后的交易对数量
|
||
// symbol 需要兑换的交易对 例如 ETHBTC
|
||
// quoteSymbol 计价货币 ETHBTC -> 计价货币就是BTC
|
||
// totalMoney 兑换的总金额
|
||
func (e FutRestApi) CalcSymbolExchangeAmt(symbol string, quoteSymbol string, totalMoney decimal.Decimal) decimal.Decimal {
|
||
tickerSymbol := helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val()
|
||
tickerSymbolMaps := make([]dto.Ticker, 0)
|
||
sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps)
|
||
var targetSymbol string
|
||
targetSymbol = strings.Replace(symbol, quoteSymbol, "USDT", 1) //ETHBTC -》 ETHUSDT
|
||
key := fmt.Sprintf("%s:%s", global.TICKER_FUTURES, targetSymbol)
|
||
tradeSet, _ := helper.GetObjString[models.TradeSet](helper.DefaultRedis, key)
|
||
var targetPrice decimal.Decimal
|
||
quantity := decimal.Zero
|
||
for _, symbolMap := range tickerSymbolMaps {
|
||
if symbolMap.Symbol == strings.ToUpper(targetSymbol) {
|
||
targetPrice, _ = decimal.NewFromString(symbolMap.Price)
|
||
quantity = totalMoney.Div(targetPrice).Truncate(int32(tradeSet.AmountDigit))
|
||
}
|
||
}
|
||
return quantity
|
||
}
|
||
|
||
// 加仓主账号
|
||
func (e FutRestApi) CoverAccountA(apiUserInfo DbModels.LineApiUser, symbol string) {
|
||
|
||
}
|
||
|
||
// GetFutSymbolLastPrice 获取现货交易对最新价格
|
||
func (e FutRestApi) GetFutSymbolLastPrice(targetSymbol string) (lastPrice decimal.Decimal) {
|
||
tickerSymbol := helper.DefaultRedis.Get(rediskey.FutSymbolTicker).Val()
|
||
tickerSymbolMaps := make([]dto.Ticker, 0)
|
||
sonic.Unmarshal([]byte(tickerSymbol), &tickerSymbolMaps)
|
||
for _, symbolMap := range tickerSymbolMaps {
|
||
if symbolMap.Symbol == strings.ToUpper(targetSymbol) {
|
||
lastPrice = utility.StringToDecimal(symbolMap.Price)
|
||
}
|
||
}
|
||
return lastPrice
|
||
}
|