2025-02-06 11:14:33 +08:00
|
|
|
|
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"
|
2025-02-20 12:00:04 +08:00
|
|
|
|
"go-admin/models/binancedto"
|
2025-02-06 11:14:33 +08:00
|
|
|
|
"go-admin/models/futuresdto"
|
|
|
|
|
|
"go-admin/pkg/httputils"
|
2025-07-26 09:09:09 +08:00
|
|
|
|
"go-admin/pkg/retryhelper"
|
2025-02-06 11:14:33 +08:00
|
|
|
|
"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)
|
2025-03-27 16:18:32 +08:00
|
|
|
|
tradeSet.MaxQty = utility.StringAsFloat(*filter.MaxQty)
|
|
|
|
|
|
tradeSet.MinQty = utility.StringAsFloat(*filter.MinQty)
|
|
|
|
|
|
case "MARKET_LOT_SIZE":
|
|
|
|
|
|
tradeSet.MarketMaxQty = utility.StringAsFloat(*filter.MaxQty)
|
|
|
|
|
|
tradeSet.MarketMinQty = utility.StringAsFloat(*filter.MinQty)
|
2025-02-06 11:14:33 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2025-02-08 14:05:57 +08:00
|
|
|
|
key := fmt.Sprintf(global.TICKER_FUTURES, global.EXCHANGE_BINANCE, item.Symbol)
|
2025-02-06 11:14:33 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-08 14:05:57 +08:00
|
|
|
|
tcKey := fmt.Sprintf(global.TICKER_FUTURES, global.EXCHANGE_BINANCE, item.Symbol)
|
2025-02-06 11:14:33 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-14 09:43:49 +08:00
|
|
|
|
func (e FutRestApi) OrderPlaceLoop(db *gorm.DB, params FutOrderPlace, retryCount int) error {
|
|
|
|
|
|
var err error
|
|
|
|
|
|
err = e.OrderPlace(db, params)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
//数量不正确
|
|
|
|
|
|
if strings.Contains(err.Error(), "LOT_SIZE") {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for x := 1; x <= retryCount; x++ {
|
|
|
|
|
|
err = e.OrderPlace(db, params)
|
|
|
|
|
|
if err == nil || strings.Contains(err.Error(), "LOT_SIZE") {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 循环平仓
|
|
|
|
|
|
func (e FutRestApi) ClosePositionLoop(symbol string, orderSn string, quantity decimal.Decimal, side string, positionSide string,
|
|
|
|
|
|
apiUserInfo DbModels.LineApiUser, orderType string, rate string, price decimal.Decimal, retryCount int) error {
|
|
|
|
|
|
var err error
|
|
|
|
|
|
err = e.ClosePosition(symbol, orderSn, quantity, side, positionSide, apiUserInfo, orderType, rate, price)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
//数量不正确
|
|
|
|
|
|
if strings.Contains(err.Error(), "LOT_SIZE") {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for x := 1; x <= retryCount; x++ {
|
|
|
|
|
|
err = e.ClosePosition(symbol, orderSn, quantity, side, positionSide, apiUserInfo, orderType, rate, price)
|
|
|
|
|
|
if err == nil || strings.Contains(err.Error(), "LOT_SIZE") {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
time.Sleep(200 * time.Millisecond)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-06 11:14:33 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
2025-02-28 18:27:52 +08:00
|
|
|
|
paramsMaps := map[string]string{
|
2025-02-06 11:14:33 +08:00
|
|
|
|
"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"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-11 18:03:30 +08:00
|
|
|
|
if strings.ToUpper(params.OrderType) == "TAKE_PROFIT" {
|
|
|
|
|
|
paramsMaps["timeInForce"] = "GTC"
|
|
|
|
|
|
paramsMaps["price"] = params.Price.String()
|
|
|
|
|
|
paramsMaps["stopprice"] = params.Profit.String()
|
2025-02-14 09:43:49 +08:00
|
|
|
|
// paramsMaps["workingType"] = "MARK_PRICE"
|
2025-02-11 18:03:30 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-06 11:14:33 +08:00
|
|
|
|
if strings.ToUpper(params.OrderType) == "STOP_MARKET" {
|
|
|
|
|
|
paramsMaps["stopprice"] = params.StopPrice.String()
|
|
|
|
|
|
paramsMaps["workingType"] = "MARK_PRICE"
|
|
|
|
|
|
paramsMaps["timeInForce"] = "GTC"
|
|
|
|
|
|
}
|
2025-02-10 17:55:34 +08:00
|
|
|
|
|
|
|
|
|
|
if strings.ToUpper(params.OrderType) == "STOP" {
|
|
|
|
|
|
paramsMaps["price"] = params.Price.String()
|
|
|
|
|
|
paramsMaps["stopprice"] = params.StopPrice.String()
|
|
|
|
|
|
paramsMaps["workingType"] = "MARK_PRICE"
|
|
|
|
|
|
paramsMaps["timeInForce"] = "GTC"
|
|
|
|
|
|
}
|
2025-02-06 11:14:33 +08:00
|
|
|
|
}
|
2025-02-10 17:55:34 +08:00
|
|
|
|
if strings.ToUpper(params.OrderType) != "STOP_MARKET" && strings.ToUpper(params.OrderType) != "TAKE_PROFIT_MARKET" && strings.ToUpper(params.OrderType) != "STOP" && strings.ToUpper(params.OrderType) != "TAKE_PROFIT" {
|
2025-02-06 11:14:33 +08:00
|
|
|
|
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"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-02-28 18:27:52 +08:00
|
|
|
|
apiUserInfo, err := GetApiInfo(params.ApiId)
|
2025-02-06 11:14:33 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
2025-02-28 18:27:52 +08:00
|
|
|
|
client := GetClient(&apiUserInfo)
|
2025-02-06 11:14:33 +08:00
|
|
|
|
_, 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取合约 持仓价格、数量
|
|
|
|
|
|
// 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-11 09:27:32 +08:00
|
|
|
|
// 获取合约 持仓价格、数量
|
|
|
|
|
|
// symbol:交易对
|
|
|
|
|
|
// positionSide:持仓方向
|
|
|
|
|
|
// holdeData:持仓数据
|
|
|
|
|
|
func (e FutRestApi) GetPositionData(apiInfo *DbModels.LineApiUser, symbol, positionSide string, holdeData *HoldeData) error {
|
|
|
|
|
|
opts := retryhelper.DefaultRetryOptions()
|
|
|
|
|
|
opts.RetryableErrFn = func(err error) bool {
|
|
|
|
|
|
if strings.Contains(err.Error(), "LOT_SIZE") {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
//重试
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
holdes, err := retryhelper.RetryWithResult(func() ([]PositionRisk, error) {
|
|
|
|
|
|
return e.GetPositionV3(apiInfo, symbol)
|
|
|
|
|
|
}, opts)
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for _, item := range holdes {
|
|
|
|
|
|
positionAmount, _ := decimal.NewFromString(item.PositionAmt)
|
|
|
|
|
|
if (positionSide == "LONG" && item.PositionSide == "BOTH" && positionAmount.Cmp(decimal.Zero) > 0) || item.PositionSide == positionSide { //多
|
|
|
|
|
|
holdeData.AveragePrice, _ = decimal.NewFromString(item.EntryPrice)
|
|
|
|
|
|
holdeData.TotalQuantity = positionAmount.Abs()
|
|
|
|
|
|
continue
|
|
|
|
|
|
} else if (positionSide == "SHORT" && item.PositionSide == "BOTH" && positionAmount.Cmp(decimal.Zero) < 0) || item.PositionSide == positionSide { //空
|
|
|
|
|
|
holdeData.AveragePrice, _ = decimal.NewFromString(item.EntryPrice)
|
|
|
|
|
|
holdeData.TotalQuantity = positionAmount.Abs()
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if holdeData.AveragePrice.Cmp(decimal.Zero) == 0 {
|
|
|
|
|
|
holdesVal, _ := sonic.MarshalString(&holdes)
|
|
|
|
|
|
log.Error("均价错误 symbol:", symbol, " 数据:", holdesVal)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-06 11:14:33 +08:00
|
|
|
|
// 获取代币持仓信息
|
|
|
|
|
|
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) {
|
2025-02-28 18:27:52 +08:00
|
|
|
|
client := GetClient(apiUserInfo)
|
2025-02-06 11:14:33 +08:00
|
|
|
|
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"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-28 18:27:52 +08:00
|
|
|
|
client := GetClient(&apiUserInfo)
|
2025-02-06 11:14:33 +08:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-07-26 09:09:09 +08:00
|
|
|
|
// 带重试机制的取消订单
|
|
|
|
|
|
func (e FutRestApi) CancelFutOrderRetry(apiUserInfo DbModels.LineApiUser, symbol string, newClientOrderId string) error {
|
|
|
|
|
|
opts := retryhelper.DefaultRetryOptions()
|
|
|
|
|
|
|
|
|
|
|
|
return retryhelper.Retry(func() error {
|
|
|
|
|
|
return e.CancelFutOrder(apiUserInfo, symbol, newClientOrderId)
|
|
|
|
|
|
}, opts)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-06 11:14:33 +08:00
|
|
|
|
// 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, //用户自定义订单号
|
|
|
|
|
|
}
|
2025-02-28 18:27:52 +08:00
|
|
|
|
client := GetClient(&apiUserInfo)
|
2025-02-06 11:14:33 +08:00
|
|
|
|
_, _, err := client.SendFuturesRequestAuth(endpoint, "DELETE", params)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
var dataMap map[string]interface{}
|
|
|
|
|
|
if err2 := sonic.Unmarshal([]byte(err.Error()), &dataMap); err2 != nil {
|
2025-02-11 14:49:16 +08:00
|
|
|
|
return fmt.Errorf("api_id:%d 交易对:%s 撤销合约委托出错:%s", apiUserInfo.Id, symbol, err.Error())
|
2025-02-06 11:14:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
code, ok := dataMap["code"]
|
|
|
|
|
|
if ok {
|
|
|
|
|
|
errContent := FutErrorMaps[code.(float64)]
|
|
|
|
|
|
|
|
|
|
|
|
if errContent == "" {
|
|
|
|
|
|
errContent = err.Error()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-11 14:49:16 +08:00
|
|
|
|
return fmt.Errorf("api_id:%d 交易对:%s 撤销合约委托出错:%s", apiUserInfo.Id, symbol, errContent)
|
2025-02-06 11:14:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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",
|
|
|
|
|
|
}
|
2025-02-28 18:27:52 +08:00
|
|
|
|
client := GetClient(&apiUserInfo)
|
2025-02-06 11:14:33 +08:00
|
|
|
|
_, _, 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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-01 10:30:43 +08:00
|
|
|
|
// 带重试的批量撤销订单
|
|
|
|
|
|
func (e FutRestApi) CancelBatchFutOrderLoop(apiUserInfo DbModels.LineApiUser, symbol string, newClientOrderIdList []string) error {
|
|
|
|
|
|
opts := retryhelper.DefaultRetryOptions()
|
|
|
|
|
|
opts.RetryableErrFn = func(err error) bool {
|
|
|
|
|
|
if strings.Contains(err.Error(), "LOT_SIZE") {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
//重试
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
err := retryhelper.Retry(func() error {
|
|
|
|
|
|
return e.CancelBatchFutOrder(apiUserInfo, symbol, newClientOrderIdList)
|
|
|
|
|
|
}, opts)
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-06 11:14:33 +08:00
|
|
|
|
// CancelBatchFutOrder 批量撤销订单
|
|
|
|
|
|
// symbol 交易对
|
|
|
|
|
|
// newClientOrderIdList 系统自定义的订单号, 最多支持10个订单
|
|
|
|
|
|
func (e FutRestApi) CancelBatchFutOrder(apiUserInfo DbModels.LineApiUser, symbol string, newClientOrderIdList []string) error {
|
|
|
|
|
|
if len(newClientOrderIdList) > 10 {
|
2025-02-11 14:49:16 +08:00
|
|
|
|
return fmt.Errorf("api_id:%d 交易对:%s 撤销批量合约委托出错:%s", apiUserInfo.Id, symbol, "最多支持10个订单")
|
2025-02-06 11:14:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
endpoint := "/fapi/v1/batchOrders"
|
|
|
|
|
|
marshal, _ := sonic.Marshal(newClientOrderIdList)
|
|
|
|
|
|
params := map[string]string{
|
|
|
|
|
|
"symbol": symbol, //交易对
|
|
|
|
|
|
"origClientOrderIdList": string(marshal),
|
|
|
|
|
|
}
|
2025-02-28 18:27:52 +08:00
|
|
|
|
client := GetClient(&apiUserInfo)
|
2025-02-06 11:14:33 +08:00
|
|
|
|
_, 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 {
|
2025-02-11 14:49:16 +08:00
|
|
|
|
return fmt.Errorf("api_id:%d 交易对:%s 撤销批量合约委托出错:%s", apiUserInfo.Id, symbol, err.Error())
|
2025-02-06 11:14:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
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)
|
2025-02-28 18:27:52 +08:00
|
|
|
|
|
|
|
|
|
|
targetSymbol := strings.Replace(symbol, quoteSymbol, "USDT", 1) //ETHBTC -》 ETHUSDT
|
2025-02-06 11:14:33 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetFutSymbolLastPrice 获取现货交易对最新价格
|
|
|
|
|
|
func (e FutRestApi) GetFutSymbolLastPrice(targetSymbol string) (lastPrice decimal.Decimal) {
|
2025-02-14 09:43:49 +08:00
|
|
|
|
key := fmt.Sprintf(global.TICKER_FUTURES, global.EXCHANGE_BINANCE, targetSymbol)
|
|
|
|
|
|
tickerSymbol, _ := helper.DefaultRedis.GetString(key)
|
|
|
|
|
|
|
|
|
|
|
|
if tickerSymbol != "" {
|
|
|
|
|
|
tradeSet := models.TradeSet{}
|
|
|
|
|
|
|
|
|
|
|
|
sonic.Unmarshal([]byte(tickerSymbol), &tradeSet)
|
|
|
|
|
|
|
|
|
|
|
|
if tradeSet.LastPrice != "" {
|
|
|
|
|
|
lastPrice = utility.StrToDecimal(tradeSet.LastPrice).Truncate(int32(tradeSet.PriceDigit))
|
2025-02-06 11:14:33 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-02-14 09:43:49 +08:00
|
|
|
|
// 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)
|
|
|
|
|
|
// }
|
|
|
|
|
|
// }
|
2025-02-06 11:14:33 +08:00
|
|
|
|
return lastPrice
|
|
|
|
|
|
}
|
2025-02-20 12:00:04 +08:00
|
|
|
|
|
|
|
|
|
|
func (e FutRestApi) GetOrderByOrderSn(symbol, orderSn string, apiUserInfo DbModels.LineApiUser) (order binancedto.BinanceFutureOrder, err error) {
|
|
|
|
|
|
result := binancedto.BinanceFutureOrder{}
|
|
|
|
|
|
params := map[string]string{
|
|
|
|
|
|
"symbol": symbol,
|
|
|
|
|
|
"origClientOrderId": orderSn,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
client := GetClient(&apiUserInfo)
|
|
|
|
|
|
|
|
|
|
|
|
body, code, err := client.SendFuturesAuth("/fapi/v1/order", "GET", 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 result, fmt.Errorf("api_id:%d 交易对:%s 查询订单失败:%+v", apiUserInfo.Id, symbol, err.Error())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
code, ok := dataMap["code"]
|
|
|
|
|
|
if ok {
|
|
|
|
|
|
return result, fmt.Errorf("api_id:%d 交易对:%s 查询订单失败:%s", apiUserInfo.Id, symbol, ErrorMaps[code.(float64)])
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.Contains(err.Error(), "Unknown order sent.") {
|
|
|
|
|
|
return result, fmt.Errorf("api_id:%d 交易对:%s 查询订单失败:%+v", apiUserInfo.Id, symbol, ErrorMaps[-2011])
|
|
|
|
|
|
}
|
|
|
|
|
|
return result, fmt.Errorf("api_id:%d 交易对:%s 查询订单失败:%+v", apiUserInfo.Id, symbol, err.Error())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sonic.Unmarshal(body, &result)
|
|
|
|
|
|
|
|
|
|
|
|
if result.OrderID == 0 {
|
|
|
|
|
|
return result, fmt.Errorf("api_id:%d 交易对:%s 查询订单失败:%+v", apiUserInfo.Id, symbol, "订单不存在")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/*
|
2025-02-28 18:27:52 +08:00
|
|
|
|
查询合约委托
|
2025-02-20 12:00:04 +08:00
|
|
|
|
*/
|
|
|
|
|
|
// 根据订单号获取订单信息,如果获取失败,则进行重试
|
|
|
|
|
|
func (e FutRestApi) GetOrderByOrderSnLoop(symbol, ordersn string, apiUserInfo DbModels.LineApiUser, retryCount int) (order binancedto.BinanceFutureOrder, err error) {
|
|
|
|
|
|
result, err := e.GetOrderByOrderSn(symbol, ordersn, apiUserInfo)
|
|
|
|
|
|
// 如果获取失败,则进行重试
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
// 调用GetOrderByOrderSn方法获取订单信息
|
|
|
|
|
|
for x := 1; x < retryCount; x++ {
|
|
|
|
|
|
// 如果获取成功,则跳出循环
|
|
|
|
|
|
result, err = e.GetOrderByOrderSn(symbol, ordersn, apiUserInfo)
|
|
|
|
|
|
if err == nil {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 返回订单信息和错误信息
|
|
|
|
|
|
|
|
|
|
|
|
return result, err
|
|
|
|
|
|
}
|
2025-02-25 18:34:20 +08:00
|
|
|
|
|
|
|
|
|
|
// 获取合约U资产
|
|
|
|
|
|
func GetFuturesUProperty(apiUserInfo DbModels.LineApiUser, data *dto.LineUserPropertyResp) error {
|
|
|
|
|
|
endpoint := "/fapi/v3/balance"
|
|
|
|
|
|
params := map[string]string{
|
|
|
|
|
|
"recvWindow": "5000",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
balanceResp := make([]binancedto.BinanceFutureBalance, 0)
|
|
|
|
|
|
client := GetClient(&apiUserInfo)
|
|
|
|
|
|
body, code, err := client.SendFuturesAuth(endpoint, "GET", params)
|
|
|
|
|
|
|
|
|
|
|
|
if err != nil || code != 200 {
|
|
|
|
|
|
log.Error("查询合约资产 参数:", params)
|
|
|
|
|
|
log.Error("查询合约资产 code:", code)
|
|
|
|
|
|
log.Error("查询合约资产 err:", err)
|
|
|
|
|
|
dataMap := make(map[string]interface{})
|
|
|
|
|
|
if err.Error() != "" {
|
|
|
|
|
|
if err := sonic.Unmarshal([]byte(err.Error()), &dataMap); err != nil {
|
|
|
|
|
|
return fmt.Errorf("api_id:%d 查询资产失败:%+v", apiUserInfo.Id, err.Error())
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
code, ok := dataMap["code"]
|
|
|
|
|
|
if ok {
|
|
|
|
|
|
return fmt.Errorf("api_id:%d 查询资产失败:%s", apiUserInfo.Id, ErrorMaps[code.(float64)])
|
|
|
|
|
|
}
|
|
|
|
|
|
if strings.Contains(err.Error(), "Unknown order sent.") {
|
|
|
|
|
|
return fmt.Errorf("api_id:%d 查询资产失败:%+v", apiUserInfo.Id, ErrorMaps[-2011])
|
|
|
|
|
|
}
|
|
|
|
|
|
return fmt.Errorf("api_id:%d 查询资产失败:%+v", apiUserInfo.Id, err.Error())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
sonic.Unmarshal(body, &balanceResp)
|
|
|
|
|
|
|
|
|
|
|
|
for _, v := range balanceResp {
|
|
|
|
|
|
if v.Asset == "USDT" {
|
|
|
|
|
|
free := utility.StrToDecimal(v.AvailableBalance)
|
|
|
|
|
|
|
|
|
|
|
|
data.FuturesFreeAmount = free
|
|
|
|
|
|
data.FuturesTotalAmount = utility.StrToDecimal(v.Balance)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|