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" "strings" "time" "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(global.TICKER_SPOT, global.EXCHANGE_BINANCE, 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(global.TICKER_SPOT, global.EXCHANGE_BINANCE, 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()) } 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 } // 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(global.TICKER_SPOT, global.EXCHANGE_BINANCE, 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 JudgeSpotPrice(trade models.TradeSet) { key := fmt.Sprintf(rediskey.PreSpotOrderList, global.EXCHANGE_BINANCE) preOrderVal, _ := helper.DefaultRedis.GetAllList(key) db := GetDBConnection() if len(preOrderVal) == 0 { 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.PreSpotOrderList, 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.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(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 } } // 判断是否触发止损 func JudgeSpotStopLoss(trade models.TradeSet) { key := fmt.Sprintf(rediskey.SpotStopLossList, global.EXCHANGE_BINANCE) stopLossVal, _ := helper.DefaultRedis.GetAllList(key) if len(stopLossVal) == 0 { return } db := GetDBConnection() spotApi := SpotRestApi{} setting, err := GetSystemSetting(db) if err != nil { log.Error("获取系统设置失败") return } tradeSet, err := GetTradeSet(trade.Coin+trade.Currency, 0) if err != nil { log.Error("获取交易设置失败") return } for _, item := range stopLossVal { stopOrder := dto.StopLossRedisList{} if err := sonic.Unmarshal([]byte(item), &stopOrder); err != nil { log.Error("反序列化失败") continue } if stopOrder.Symbol == trade.Coin+trade.Currency { orderPrice := stopOrder.Price tradePrice, _ := decimal.NewFromString(trade.LastPrice) //买入 if strings.ToUpper(stopOrder.Site) == "SELL" && orderPrice.Cmp(tradePrice) >= 0 && orderPrice.Cmp(decimal.Zero) > 0 && tradePrice.Cmp(decimal.Zero) > 0 { SpotStopLossTrigger(db, stopOrder, spotApi, setting, tradeSet, key, item) } } } } // 触发现货止损 func SpotStopLossTrigger(db *gorm.DB, stopOrder dto.StopLossRedisList, spotApi SpotRestApi, setting DbModels.LineSystemSetting, tradeSet models.TradeSet, key string, item string) { lock := helper.NewRedisLock(fmt.Sprintf(rediskey.SpotTrigger, stopOrder.ApiId, stopOrder.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() takeOrder := DbModels.LinePreOrder{} if err := db.Model(&DbModels.LinePreOrder{}).Where("pid =? AND order_type =1", stopOrder.PId).Find(&takeOrder).Error; err != nil { log.Error("查询止盈单失败") return } apiInfo, _ := GetApiInfo(takeOrder.ApiId) if apiInfo.Id == 0 { log.Error("现货止损 查询api用户不存在") return } var err error for x := 1; x <= 4; x++ { err = spotApi.CancelOpenOrderByOrderSn(apiInfo, takeOrder.Symbol, takeOrder.OrderSn) if err == nil { break } } if err != nil { log.Error("现货止损撤单失败", err) return } stopPreOrder, _ := GetOrderById(db, stopOrder.Id) price := stopOrder.Price.Mul(decimal.NewFromInt(1).Sub(setting.StopLossPremium.Div(decimal.NewFromInt(100)))).Truncate(int32(tradeSet.PriceDigit)) num := utility.StrToDecimal(takeOrder.Num).Truncate(int32(tradeSet.AmountDigit)) params := OrderPlacementService{ ApiId: takeOrder.ApiId, Side: takeOrder.Site, Type: "LIMIT", TimeInForce: "GTC", Symbol: takeOrder.Symbol, Price: price, Quantity: num, NewClientOrderId: stopPreOrder.OrderSn, } if err := spotApi.OrderPlace(db, params); err != nil { log.Errorf("现货止损挂单失败 id:%s err:%v", stopOrder.Id, err) } if _, err := helper.DefaultRedis.LRem(key, item); err != nil { log.Errorf("现货止损 删除缓存失败 id:%v err:%v", stopOrder.Id, err) } } else { log.Error("获取锁失败") } } /* 获取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 }