package huobi import ( "context" "errors" "fmt" "slices" "sort" "strconv" "strings" "time" "github.com/shopspring/decimal" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchange/accounts" "github.com/thrasher-corp/gocryptotrader/exchange/websocket" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/deposit" "github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate" "github.com/thrasher-corp/gocryptotrader/exchanges/futures" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/protocol" "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/trade" "github.com/thrasher-corp/gocryptotrader/log" "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" ) // SetDefaults sets default values for the exchange func (e *Exchange) SetDefaults() { e.Name = "Huobi" e.Enabled = true e.Verbose = true e.API.CredentialsValidator.RequiresKey = true e.API.CredentialsValidator.RequiresSecret = true for _, a := range []asset.Item{asset.Spot, asset.CoinMarginedFutures, asset.Futures} { ps := currency.PairStore{ AssetEnabled: true, RequestFormat: ¤cy.PairFormat{Uppercase: true}, ConfigFormat: ¤cy.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}, } switch a { case asset.Spot: ps.RequestFormat.Uppercase = false case asset.CoinMarginedFutures: ps.RequestFormat.Delimiter = currency.DashDelimiter } if err := e.SetAssetPairStore(a, ps); err != nil { log.Errorf(log.ExchangeSys, "%s error storing %q default asset formats: %s", e.Name, a, err) } } for _, a := range []asset.Item{asset.Futures, asset.CoinMarginedFutures} { if err := e.DisableAssetWebsocketSupport(a); err != nil { log.Errorf(log.ExchangeSys, "%s error disabling %q asset type websocket support: %s", e.Name, a, err) } } e.Features = exchange.Features{ Supports: exchange.FeaturesSupported{ REST: true, Websocket: true, RESTCapabilities: protocol.Features{ TickerFetching: true, TickerBatching: true, KlineFetching: true, TradeFetching: true, OrderbookFetching: true, AutoPairUpdates: true, AccountInfo: true, GetOrder: true, GetOrders: true, CancelOrders: true, CancelOrder: true, SubmitOrder: true, CryptoDeposit: true, CryptoWithdrawal: true, TradeFee: true, MultiChainDeposits: true, MultiChainWithdrawals: true, HasAssetTypeAccountSegregation: true, FundingRateFetching: true, PredictedFundingRate: true, }, WebsocketCapabilities: protocol.Features{ KlineFetching: true, OrderbookFetching: true, TradeFetching: true, Subscribe: true, Unsubscribe: true, AuthenticatedEndpoints: true, AccountInfo: true, MessageCorrelation: true, GetOrder: true, GetOrders: true, TickerFetching: true, FundingRateFetching: false, // supported but not implemented // TODO when multi-websocket support added }, WithdrawPermissions: exchange.AutoWithdrawCryptoWithSetup | exchange.NoFiatWithdrawals, Kline: kline.ExchangeCapabilitiesSupported{ Intervals: true, }, FuturesCapabilities: exchange.FuturesCapabilities{ FundingRates: true, SupportedFundingRateFrequencies: map[kline.Interval]bool{ kline.EightHour: true, }, FundingRateBatching: map[asset.Item]bool{ asset.CoinMarginedFutures: true, }, OpenInterest: exchange.OpenInterestSupport{ Supported: true, SupportsRestBatch: true, }, }, }, Enabled: exchange.FeaturesEnabled{ AutoPairUpdates: true, Kline: kline.ExchangeCapabilitiesEnabled{ Intervals: kline.DeployExchangeIntervals( kline.IntervalCapacity{Interval: kline.OneMin}, kline.IntervalCapacity{Interval: kline.FiveMin}, kline.IntervalCapacity{Interval: kline.FifteenMin}, kline.IntervalCapacity{Interval: kline.ThirtyMin}, kline.IntervalCapacity{Interval: kline.OneHour}, kline.IntervalCapacity{Interval: kline.FourHour}, kline.IntervalCapacity{Interval: kline.OneYear}, // NOTE: The supported time intervals below are returned // offset to the Asia/Shanghai time zone. This may lead to // issues with candle quality and conversion as the // intervals may be broken up. The below intervals // are constructed from hourly candles. // kline.IntervalCapacity{Interval: kline.OneDay}, // kline.IntervalCapacity{Interval: kline.OneWeek}, // kline.IntervalCapacity{Interval: kline.OneMonth}, ), GlobalResultLimit: 2000, }, }, Subscriptions: defaultSubscriptions.Clone(), } var err error e.Requester, err = request.New(e.Name, common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout), request.WithLimiter(GetRateLimit())) if err != nil { log.Errorln(log.ExchangeSys, err) } e.API.Endpoints = e.NewEndpoints() err = e.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{ exchange.RestSpot: huobiAPIURL, exchange.RestFutures: huobiFuturesURL, exchange.RestCoinMargined: huobiFuturesURL, exchange.WebsocketSpot: wsSpotURL + wsPublicPath, }) if err != nil { log.Errorln(log.ExchangeSys, err) } e.Websocket = websocket.NewManager() e.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit e.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout e.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit } // Bootstrap ensures that future contract expiry codes are loaded if AutoPairUpdates is not enabled func (e *Exchange) Bootstrap(ctx context.Context) (continueBootstrap bool, err error) { continueBootstrap = true if !e.GetEnabledFeatures().AutoPairUpdates && e.SupportsAsset(asset.Futures) { _, err = e.FetchTradablePairs(ctx, asset.Futures) } return } // Setup sets user configuration func (e *Exchange) Setup(exch *config.Exchange) error { err := exch.Validate() if err != nil { return err } if !exch.Enabled { e.SetEnabled(false) return nil } err = e.SetupDefaults(exch) if err != nil { return err } wsRunningURL, err := e.API.Endpoints.GetURL(exchange.WebsocketSpot) if err != nil { return err } err = e.Websocket.Setup(&websocket.ManagerSetup{ ExchangeConfig: exch, DefaultURL: wsSpotURL + wsPublicPath, RunningURL: wsRunningURL, Connector: e.WsConnect, Subscriber: e.Subscribe, Unsubscriber: e.Unsubscribe, GenerateSubscriptions: e.generateSubscriptions, Features: &e.Features.Supports.WebsocketCapabilities, }) if err != nil { return err } err = e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{ RateLimit: request.NewWeightedRateLimitByDuration(20 * time.Millisecond), ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, ResponseMaxLimit: exch.WebsocketResponseMaxLimit, }) if err != nil { return err } return e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{ RateLimit: request.NewWeightedRateLimitByDuration(20 * time.Millisecond), ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, ResponseMaxLimit: exch.WebsocketResponseMaxLimit, URL: wsSpotURL + wsPrivatePath, Authenticated: true, }) } // FetchTradablePairs returns a list of the exchanges tradable pairs func (e *Exchange) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.Pairs, error) { if !e.SupportsAsset(a) { return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } var pairs []currency.Pair switch a { case asset.Spot: symbols, err := e.GetSymbols(ctx) if err != nil { return nil, err } pairs = make([]currency.Pair, 0, len(symbols)) for x := range symbols { if symbols[x].State != "online" { continue } pair, err := currency.NewPairFromStrings(symbols[x].BaseCurrency, symbols[x].QuoteCurrency) if err != nil { return nil, err } pairs = append(pairs, pair) } case asset.CoinMarginedFutures: symbols, err := e.GetSwapMarkets(ctx, currency.EMPTYPAIR) if err != nil { return nil, err } pairs = make([]currency.Pair, 0, len(symbols)) for z := range symbols { if symbols[z].ContractStatus != 1 { continue } pair, err := currency.NewPairFromString(symbols[z].ContractCode) if err != nil { return nil, err } pairs = append(pairs, pair) } case asset.Futures: symbols, err := e.FGetContractInfo(ctx, "", "", currency.EMPTYPAIR) if err != nil { return nil, err } pairs = make([]currency.Pair, 0, len(symbols.Data)) expiryCodeDates := map[string]currency.Code{} for i := range symbols.Data { c := symbols.Data[i] if c.ContractStatus != 1 { continue } pair, err := currency.NewPairFromString(c.ContractCode) if err != nil { return nil, err } pairs = append(pairs, pair) if cType, ok := contractExpiryNames[c.ContractType]; ok { if v, ok := expiryCodeDates[cType]; !ok { expiryCodeDates[cType] = currency.NewCode(pair.Quote.String()) } else if v.String() != pair.Quote.String() { return nil, fmt.Errorf("%w: %s (%s vs %s)", errInconsistentContractExpiry, cType, v.String(), pair.Quote.String()) } } } // We cache contract expiries on the exchange locally right now because there's no exchange base holder for them // It's not as dangerous as it seems, because when contracts change, so would tradeable pairs, // so by caching them in FetchTradablePairs we're not adding any extra-layer of out-of-date data e.futureContractCodesMutex.Lock() e.futureContractCodes = expiryCodeDates e.futureContractCodesMutex.Unlock() } return pairs, nil } // UpdateTradablePairs updates the exchanges available pairs and stores // them in the exchanges config func (e *Exchange) UpdateTradablePairs(ctx context.Context) error { assets := e.GetAssetTypes(false) for x := range assets { pairs, err := e.FetchTradablePairs(ctx, assets[x]) if err != nil { return err } if err := e.UpdatePairs(pairs, assets[x], false); err != nil { return err } } return e.EnsureOnePairEnabled() } // UpdateTickers updates the ticker for all currency pairs of a given asset type func (e *Exchange) UpdateTickers(ctx context.Context, a asset.Item) error { var errs error switch a { case asset.Spot: ticks, err := e.GetTickers(ctx) if err != nil { return err } for i := range ticks.Data { var cp currency.Pair cp, _, err = e.MatchSymbolCheckEnabled(ticks.Data[i].Symbol, a, false) if err != nil { if !errors.Is(err, currency.ErrPairNotFound) { errs = common.AppendError(errs, err) } continue } err = ticker.ProcessTicker(&ticker.Price{ High: ticks.Data[i].High, Low: ticks.Data[i].Low, Bid: ticks.Data[i].Bid, Ask: ticks.Data[i].Ask, Volume: ticks.Data[i].Amount, QuoteVolume: ticks.Data[i].Volume, Open: ticks.Data[i].Open, Close: ticks.Data[i].Close, BidSize: ticks.Data[i].BidSize, AskSize: ticks.Data[i].AskSize, Pair: cp, ExchangeName: e.Name, AssetType: a, LastUpdated: time.Now(), }) if err != nil { errs = common.AppendError(errs, err) } } case asset.CoinMarginedFutures: ticks, err := e.GetBatchCoinMarginSwapContracts(ctx) if err != nil { return err } for i := range ticks { var cp currency.Pair cp, _, err = e.MatchSymbolCheckEnabled(ticks[i].ContractCode, a, true) if err != nil { if !errors.Is(err, currency.ErrPairNotFound) { errs = common.AppendError(errs, err) } continue } tt := ticks[i].Timestamp.Time() err = ticker.ProcessTicker(&ticker.Price{ High: ticks[i].High.Float64(), Low: ticks[i].Low.Float64(), Volume: ticks[i].Amount.Float64(), QuoteVolume: ticks[i].Volume.Float64(), Open: ticks[i].Open.Float64(), Close: ticks[i].Close.Float64(), Bid: ticks[i].Bid[0], BidSize: ticks[i].Bid[1], Ask: ticks[i].Ask[0], AskSize: ticks[i].Ask[1], Pair: cp, ExchangeName: e.Name, AssetType: a, LastUpdated: tt, }) if err != nil { errs = common.AppendError(errs, err) } } case asset.Futures: ticks := []FuturesBatchTicker{} // TODO: Linear swap contracts are coin-m assets if coinMTicks, err := e.GetBatchLinearSwapContracts(ctx); err != nil { errs = common.AppendError(errs, err) } else { ticks = append(ticks, coinMTicks...) } if futureTicks, err := e.GetBatchFuturesContracts(ctx); err != nil { errs = common.AppendError(errs, err) } else { ticks = append(ticks, futureTicks...) } for i := range ticks { var cp currency.Pair var err error if ticks[i].Symbol != "" { cp, err = currency.NewPairFromString(ticks[i].Symbol) if err == nil { cp, err = e.pairFromContractExpiryCode(cp) } if err == nil { cp, _, err = e.MatchSymbolCheckEnabled(cp.String(), a, true) } } else { cp, _, err = e.MatchSymbolCheckEnabled(ticks[i].ContractCode, a, true) } if err != nil { if !errors.Is(err, currency.ErrPairNotFound) { errs = common.AppendError(errs, err) } continue } err = ticker.ProcessTicker(&ticker.Price{ High: ticks[i].High.Float64(), Low: ticks[i].Low.Float64(), Volume: ticks[i].Amount.Float64(), QuoteVolume: ticks[i].Volume.Float64(), Open: ticks[i].Open.Float64(), Close: ticks[i].Close.Float64(), Bid: ticks[i].Bid[0], BidSize: ticks[i].Bid[1], Ask: ticks[i].Ask[0], AskSize: ticks[i].Ask[1], Pair: cp, ExchangeName: e.Name, AssetType: a, LastUpdated: ticks[i].Timestamp.Time(), }) if err != nil { errs = common.AppendError(errs, err) } } default: return fmt.Errorf("%w %q", asset.ErrNotSupported, a) } return errs } // UpdateTicker updates and returns the ticker for a currency pair func (e *Exchange) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error) { if p.IsEmpty() { return nil, currency.ErrCurrencyPairEmpty } if !e.SupportsAsset(a) { return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } switch a { case asset.Spot: tickerData, err := e.Get24HrMarketSummary(ctx, p) if err != nil { return nil, err } err = ticker.ProcessTicker(&ticker.Price{ High: tickerData.Tick.High, Low: tickerData.Tick.Low, Volume: tickerData.Tick.Amount, QuoteVolume: tickerData.Tick.Volume, Open: tickerData.Tick.Open, Close: tickerData.Tick.Close, Pair: p, ExchangeName: e.Name, AssetType: asset.Spot, }) if err != nil { return nil, err } case asset.CoinMarginedFutures: marketData, err := e.GetSwapMarketOverview(ctx, p) if err != nil { return nil, err } if len(marketData.Tick.Bid) == 0 { return nil, errors.New("invalid data for bid") } if len(marketData.Tick.Ask) == 0 { return nil, errors.New("invalid data for Ask") } err = ticker.ProcessTicker(&ticker.Price{ High: marketData.Tick.High, Low: marketData.Tick.Low, Volume: marketData.Tick.Amount, QuoteVolume: marketData.Tick.Vol, Open: marketData.Tick.Open, Close: marketData.Tick.Close, Pair: p, Bid: marketData.Tick.Bid[0], Ask: marketData.Tick.Ask[0], ExchangeName: e.Name, AssetType: a, }) if err != nil { return nil, err } case asset.Futures: marketData, err := e.FGetMarketOverviewData(ctx, p) if err != nil { return nil, err } err = ticker.ProcessTicker(&ticker.Price{ High: marketData.Tick.High, Low: marketData.Tick.Low, Volume: marketData.Tick.Amount, QuoteVolume: marketData.Tick.Vol, Open: marketData.Tick.Open, Close: marketData.Tick.Close, Pair: p, Bid: marketData.Tick.Bid[0], Ask: marketData.Tick.Ask[0], ExchangeName: e.Name, AssetType: a, }) if err != nil { return nil, err } } return ticker.GetTicker(e.Name, p, a) } // UpdateOrderbook updates and returns the orderbook for a currency pair func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetType asset.Item) (*orderbook.Book, error) { if p.IsEmpty() { return nil, currency.ErrCurrencyPairEmpty } if !assetType.IsValid() { return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) } book := &orderbook.Book{ Exchange: e.Name, Pair: p, Asset: assetType, ValidateOrderbook: e.ValidateOrderbook, } var err error switch assetType { case asset.Spot: var orderbookNew *Orderbook orderbookNew, err = e.GetDepth(ctx, &OrderBookDataRequestParams{ Symbol: p, Type: OrderBookDataRequestParamsTypeStep0, }) if err != nil { return book, err } book.Bids = make(orderbook.Levels, len(orderbookNew.Bids)) for x := range orderbookNew.Bids { book.Bids[x] = orderbook.Level{ Amount: orderbookNew.Bids[x][1], Price: orderbookNew.Bids[x][0], } } book.Asks = make(orderbook.Levels, len(orderbookNew.Asks)) for x := range orderbookNew.Asks { book.Asks[x] = orderbook.Level{ Amount: orderbookNew.Asks[x][1], Price: orderbookNew.Asks[x][0], } } case asset.Futures: var orderbookNew *OBData orderbookNew, err = e.FGetMarketDepth(ctx, p, "step0") if err != nil { return book, err } book.Asks = make(orderbook.Levels, len(orderbookNew.Asks)) for x := range orderbookNew.Asks { book.Asks[x] = orderbook.Level{ Amount: orderbookNew.Asks[x].Quantity, Price: orderbookNew.Asks[x].Price, } } book.Bids = make(orderbook.Levels, len(orderbookNew.Bids)) for y := range orderbookNew.Bids { book.Bids[y] = orderbook.Level{ Amount: orderbookNew.Bids[y].Quantity, Price: orderbookNew.Bids[y].Price, } } case asset.CoinMarginedFutures: var orderbookNew SwapMarketDepthData orderbookNew, err = e.GetSwapMarketDepth(ctx, p, "step0") if err != nil { return book, err } book.Asks = make(orderbook.Levels, len(orderbookNew.Tick.Asks)) for x := range orderbookNew.Tick.Asks { book.Asks[x] = orderbook.Level{ Amount: orderbookNew.Tick.Asks[x][1], Price: orderbookNew.Tick.Asks[x][0], } } book.Bids = make(orderbook.Levels, len(orderbookNew.Tick.Bids)) for y := range orderbookNew.Tick.Bids { book.Bids[y] = orderbook.Level{ Amount: orderbookNew.Tick.Bids[y][1], Price: orderbookNew.Tick.Bids[y][0], } } } err = book.Process() if err != nil { return book, err } return orderbook.Get(e.Name, p, assetType) } // GetAccountID returns the account ID for trades func (e *Exchange) GetAccountID(ctx context.Context) ([]Account, error) { acc, err := e.GetAccounts(ctx) if err != nil { return nil, err } if len(acc) < 1 { return nil, errors.New("no account returned") } return acc, nil } // UpdateAccountBalances retrieves currency balances func (e *Exchange) UpdateAccountBalances(ctx context.Context, assetType asset.Item) (subAccts accounts.SubAccounts, err error) { switch assetType { case asset.Spot: resp, err := e.GetAccountID(ctx) if err != nil { return nil, err } subAccts = make(accounts.SubAccounts, 0, len(resp)) for i := range resp { if resp[i].Type != "spot" { continue } a := accounts.NewSubAccount(assetType, strconv.FormatInt(resp[i].ID, 10)) balances, err := e.GetAccountBalance(ctx, a.ID) if err != nil { return nil, err } for j := range balances { if balances[j].Type == "frozen" { err = a.Balances.Add(balances[j].Currency, accounts.Balance{Hold: balances[j].Balance}) } else { err = a.Balances.Add(balances[j].Currency, accounts.Balance{Total: balances[j].Balance}) } if err != nil { return nil, err } } subAccts = subAccts.Merge(a) } case asset.CoinMarginedFutures: mainResp, err := e.GetSwapAccountInfo(ctx, currency.EMPTYPAIR) if err != nil { return nil, err } subAccts = accounts.SubAccounts{accounts.NewSubAccount(assetType, "")} for i := range mainResp.Data { subAccts[0].Balances.Set(mainResp.Data[i].Symbol, accounts.Balance{ Total: mainResp.Data[i].MarginBalance, Hold: mainResp.Data[i].MarginFrozen, Free: mainResp.Data[i].MarginAvailable, }) } subResp, err := e.GetSwapAllSubAccAssets(ctx, currency.EMPTYPAIR) if err != nil { return nil, err } for i := range subResp.Data { resp, err := e.SwapSingleSubAccAssets(ctx, currency.EMPTYPAIR, subResp.Data[i].SubUID) if err != nil { return nil, err } a := accounts.NewSubAccount(assetType, strconv.FormatInt(subResp.Data[i].SubUID, 10)) for j := range resp.Data { a.Balances.Set(resp.Data[j].Symbol, accounts.Balance{ Total: resp.Data[j].MarginBalance, Hold: resp.Data[j].MarginFrozen, Free: resp.Data[j].MarginAvailable, }) } subAccts = subAccts.Merge(a) } case asset.Futures: mainResp, err := e.FGetAccountInfo(ctx, currency.EMPTYCODE) if err != nil { return nil, err } subAccts = accounts.SubAccounts{accounts.NewSubAccount(assetType, "")} for i := range mainResp.AccData { subAccts[0].Balances.Set(mainResp.AccData[i].Symbol, accounts.Balance{ Total: mainResp.AccData[i].MarginBalance, Hold: mainResp.AccData[i].MarginFrozen, Free: mainResp.AccData[i].MarginAvailable, }) } subResp, err := e.FGetAllSubAccountAssets(ctx, currency.EMPTYCODE) if err != nil { return nil, err } for i := range subResp.Data { a := accounts.NewSubAccount(assetType, strconv.FormatInt(subResp.Data[i].SubUID, 10)) resp, err := e.FGetSingleSubAccountInfo(ctx, "", a.ID) if err != nil { return nil, err } for j := range resp.AssetsData { a.Balances.Set(resp.AssetsData[j].Symbol, accounts.Balance{ Total: resp.AssetsData[j].MarginBalance, Hold: resp.AssetsData[j].MarginFrozen, Free: resp.AssetsData[j].MarginAvailable, }) } subAccts = subAccts.Merge(a) } } return subAccts, e.Accounts.Save(ctx, subAccts, true) } // GetAccountFundingHistory returns funding history, deposits and // withdrawals func (e *Exchange) GetAccountFundingHistory(_ context.Context) ([]exchange.FundingHistory, error) { return nil, common.ErrFunctionNotSupported } // GetWithdrawalsHistory returns previous withdrawals data func (e *Exchange) GetWithdrawalsHistory(ctx context.Context, c currency.Code, a asset.Item) ([]exchange.WithdrawalHistory, error) { if a != asset.Spot { return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a) } withdrawals, err := e.SearchForExistedWithdrawsAndDeposits(ctx, c, "withdraw", "", 0, 500) if err != nil { return nil, err } resp := make([]exchange.WithdrawalHistory, len(withdrawals.Data)) for i := range withdrawals.Data { resp[i] = exchange.WithdrawalHistory{ Status: withdrawals.Data[i].State, TransferID: withdrawals.Data[i].TransactionHash, Timestamp: withdrawals.Data[i].CreatedAt.Time(), Currency: withdrawals.Data[i].Currency.String(), Amount: withdrawals.Data[i].Amount, Fee: withdrawals.Data[i].Fee, TransferType: withdrawals.Data[i].Type, CryptoToAddress: withdrawals.Data[i].Address, CryptoTxID: withdrawals.Data[i].TransactionHash, CryptoChain: withdrawals.Data[i].Chain, } } return resp, nil } // GetRecentTrades returns the most recent trades for a currency and asset func (e *Exchange) GetRecentTrades(ctx context.Context, p currency.Pair, a asset.Item) ([]trade.Data, error) { var resp []trade.Data pFmt, err := e.GetPairFormat(a, true) if err != nil { return nil, err } p = p.Format(pFmt) switch a { case asset.Spot: var sTrades []TradeHistory sTrades, err = e.GetTradeHistory(ctx, p, 2000) if err != nil { return nil, err } for i := range sTrades { for j := range sTrades[i].Trades { var side order.Side side, err = order.StringToOrderSide(sTrades[i].Trades[j].Direction) if err != nil { return nil, err } resp = append(resp, trade.Data{ Exchange: e.Name, TID: strconv.FormatFloat(sTrades[i].Trades[j].TradeID, 'f', -1, 64), CurrencyPair: p, AssetType: a, Side: side, Price: sTrades[i].Trades[j].Price, Amount: sTrades[i].Trades[j].Amount, Timestamp: sTrades[i].Timestamp.Time(), }) } } case asset.Futures: var fTrades FBatchTradesForContractData fTrades, err = e.FRequestPublicBatchTrades(ctx, p, 2000) if err != nil { return nil, err } for i := range fTrades.Data { for j := range fTrades.Data[i].Data { var side order.Side if fTrades.Data[i].Data[j].Direction != "" { side, err = order.StringToOrderSide(fTrades.Data[i].Data[j].Direction) if err != nil { return nil, err } } resp = append(resp, trade.Data{ Exchange: e.Name, TID: strconv.FormatInt(fTrades.Data[i].Data[j].ID, 10), CurrencyPair: p, AssetType: a, Side: side, Price: fTrades.Data[i].Data[j].Price, Amount: fTrades.Data[i].Data[j].Amount, Timestamp: fTrades.Data[i].Data[j].Timestamp.Time(), }) } } case asset.CoinMarginedFutures: var cTrades BatchTradesData cTrades, err = e.GetBatchTrades(ctx, p, 2000) if err != nil { return nil, err } for i := range cTrades.Data { var side order.Side if cTrades.Data[i].Direction != "" { side, err = order.StringToOrderSide(cTrades.Data[i].Direction) if err != nil { return nil, err } } resp = append(resp, trade.Data{ Exchange: e.Name, TID: strconv.FormatInt(cTrades.Data[i].ID, 10), CurrencyPair: p, AssetType: a, Side: side, Price: cTrades.Data[i].Price, Amount: cTrades.Data[i].Amount, Timestamp: cTrades.Data[i].Timestamp.Time(), }) } } err = e.AddTradesToBuffer(resp...) if err != nil { return nil, err } sort.Sort(trade.ByDate(resp)) return resp, nil } // GetHistoricTrades returns historic trade data within the timeframe provided func (e *Exchange) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.Item, _, _ time.Time) ([]trade.Data, error) { return nil, common.ErrFunctionNotSupported } // SubmitOrder submits a new order func (e *Exchange) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) { if err := s.Validate(e.GetTradingRequirements()); err != nil { return nil, err } var orderID string status := order.New switch s.AssetType { case asset.Spot: accountID, err := strconv.ParseInt(s.ClientID, 10, 64) if err != nil { return nil, err } var formattedType SpotNewOrderRequestParamsType params := SpotNewOrderRequestParams{ Amount: s.Amount, Source: "api", Symbol: s.Pair, AccountID: int(accountID), } switch { case s.Side.IsLong() && s.Type == order.Market: formattedType = SpotNewOrderRequestTypeBuyMarket case s.Side.IsShort() && s.Type == order.Market: formattedType = SpotNewOrderRequestTypeSellMarket case s.Side.IsLong() && s.Type == order.Limit: formattedType = SpotNewOrderRequestTypeBuyLimit params.Price = s.Price case s.Side.IsShort() && s.Type == order.Limit: formattedType = SpotNewOrderRequestTypeSellLimit params.Price = s.Price } params.Type = formattedType response, err := e.SpotNewOrder(ctx, ¶ms) if err != nil { return nil, err } orderID = strconv.FormatInt(response, 10) if s.Type == order.Market { status = order.Filled } case asset.CoinMarginedFutures: var oDirection string switch { case s.Side.IsLong(): oDirection = "BUY" case s.Side.IsShort(): oDirection = "SELL" } var oType string switch s.Type { case order.Market: // https://huobiapi.github.io/docs/dm/v1/en/#order-and-trade // At present, Huobi Futures does not support unlimited slippage market price when placing an order. // To increase the probability of a transaction, users can choose to place an order based on BBO price (opponent), // optimal 5 (optimal_5), optimal 10 (optimal_10), optimal 20 (optimal_20), among which the success probability of // optimal 20 is the largest, while the slippage always is the largest as well. // // It is important to note that the above methods will not guarantee the order to be fully-filled // The exchange will obtain the optimal N price when the order is placed oType = "optimal_20" switch { case s.TimeInForce.Is(order.ImmediateOrCancel): oType = "optimal_20_ioc" case s.TimeInForce.Is(order.FillOrKill): oType = "optimal_20_fok" } case order.Limit: oType = "limit" if s.TimeInForce.Is(order.PostOnly) { oType = "post_only" } default: oType = "opponent" } offset := "open" if s.ReduceOnly { offset = "close" } orderResp, err := e.PlaceSwapOrders(ctx, s.Pair, s.ClientOrderID, oDirection, offset, oType, s.Price, s.Amount, s.Leverage) if err != nil { return nil, err } orderID = orderResp.Data.OrderIDString case asset.Futures: var oDirection string switch { case s.Side.IsLong(): oDirection = "BUY" case s.Side.IsShort(): oDirection = "SELL" } var oType string switch s.Type { case order.Market: // https://huobiapi.github.io/docs/dm/v1/en/#order-and-trade // At present, Huobi Futures does not support unlimited slippage market price when placing an order. // To increase the probability of a transaction, users can choose to place an order based on BBO price (opponent), // optimal 5 (optimal_5), optimal 10 (optimal_10), optimal 20 (optimal_20), among which the success probability of // optimal 20 is the largest, while the slippage always is the largest as well. // // It is important to note that the above methods will not guarantee the order to be fully-filled // The exchange will obtain the optimal N price when the order is placed oType = "optimal_20" switch { case s.TimeInForce.Is(order.ImmediateOrCancel): oType = "optimal_20_ioc" case s.TimeInForce.Is(order.FillOrKill): oType = "optimal_20_fok" } case order.Limit: oType = "limit" if s.TimeInForce.Is(order.PostOnly) { oType = "post_only" } default: oType = "opponent" } offset := "open" if s.ReduceOnly { offset = "close" } o, err := e.FOrder(ctx, s.Pair, "", "", s.ClientOrderID, oDirection, offset, oType, s.Price, s.Amount, s.Leverage) if err != nil { return nil, err } orderID = o.Data.OrderIDStr } resp, err := s.DeriveSubmitResponse(orderID) if err != nil { return nil, err } resp.Status = status return resp, nil } // ModifyOrder modifies an existing order func (e *Exchange) ModifyOrder(context.Context, *order.Modify) (*order.ModifyResponse, error) { return nil, common.ErrFunctionNotSupported } // CancelOrder cancels an order by its corresponding ID number func (e *Exchange) CancelOrder(ctx context.Context, o *order.Cancel) error { if err := o.Validate(o.StandardCancel()); err != nil { return err } var err error switch o.AssetType { case asset.Spot: var orderIDInt int64 orderIDInt, err = strconv.ParseInt(o.OrderID, 10, 64) if err != nil { return err } _, err = e.CancelExistingOrder(ctx, orderIDInt) case asset.CoinMarginedFutures: _, err = e.CancelSwapOrder(ctx, o.OrderID, o.ClientID, o.Pair) case asset.Futures: _, err = e.FCancelOrder(ctx, o.Pair.Base, o.ClientID, o.ClientOrderID) default: return fmt.Errorf("%w %v", asset.ErrNotSupported, o.AssetType) } return err } // CancelBatchOrders cancels an orders by their corresponding ID numbers func (e *Exchange) CancelBatchOrders(ctx context.Context, o []order.Cancel) (*order.CancelBatchResponse, error) { if len(o) == 0 { return nil, order.ErrCancelOrderIsNil } ids := make([]string, 0, len(o)) cIDs := make([]string, 0, len(o)) for i := range o { switch { case o[i].ClientOrderID != "": cIDs = append(cIDs, o[i].ClientID) case o[i].OrderID != "": ids = append(ids, o[i].OrderID) default: return nil, order.ErrOrderIDNotSet } } cancelledOrders, err := e.CancelOrderBatch(ctx, ids, cIDs) if err != nil { return nil, err } resp := &order.CancelBatchResponse{Status: make(map[string]string)} for i := range cancelledOrders.Success { resp.Status[cancelledOrders.Success[i]] = "true" } for i := range cancelledOrders.Failed { resp.Status[cancelledOrders.Failed[i].OrderID] = cancelledOrders.Failed[i].ErrorMessage } return resp, nil } // CancelAllOrders cancels all orders associated with a currency pair func (e *Exchange) CancelAllOrders(ctx context.Context, orderCancellation *order.Cancel) (order.CancelAllResponse, error) { if err := orderCancellation.Validate(); err != nil { return order.CancelAllResponse{}, err } var cancelAllOrdersResponse order.CancelAllResponse cancelAllOrdersResponse.Status = make(map[string]string) switch orderCancellation.AssetType { case asset.Spot: enabledPairs, err := e.GetEnabledPairs(asset.Spot) if err != nil { return cancelAllOrdersResponse, err } for i := range enabledPairs { resp, err := e.CancelOpenOrdersBatch(ctx, orderCancellation.AccountID, enabledPairs[i]) if err != nil { return cancelAllOrdersResponse, err } if resp.Data.FailedCount > 0 { return cancelAllOrdersResponse, fmt.Errorf("%v orders failed to cancel", resp.Data.FailedCount) } if resp.Status == "error" { return cancelAllOrdersResponse, errors.New(resp.ErrorMessage) } } case asset.CoinMarginedFutures: if orderCancellation.Pair.IsEmpty() { enabledPairs, err := e.GetEnabledPairs(asset.CoinMarginedFutures) if err != nil { return cancelAllOrdersResponse, err } for i := range enabledPairs { a, err := e.CancelAllSwapOrders(ctx, enabledPairs[i]) if err != nil { return cancelAllOrdersResponse, err } split := strings.Split(a.Successes, ",") for x := range split { cancelAllOrdersResponse.Status[split[x]] = "success" } for y := range a.Errors { cancelAllOrdersResponse.Status[a.Errors[y].OrderID] = "fail: " + a.Errors[y].ErrMsg } } } else { a, err := e.CancelAllSwapOrders(ctx, orderCancellation.Pair) if err != nil { return cancelAllOrdersResponse, err } split := strings.Split(a.Successes, ",") for x := range split { cancelAllOrdersResponse.Status[split[x]] = "success" } for y := range a.Errors { cancelAllOrdersResponse.Status[a.Errors[y].OrderID] = "fail: " + a.Errors[y].ErrMsg } } case asset.Futures: if orderCancellation.Pair.IsEmpty() { enabledPairs, err := e.GetEnabledPairs(asset.Futures) if err != nil { return cancelAllOrdersResponse, err } for i := range enabledPairs { a, err := e.FCancelAllOrders(ctx, enabledPairs[i], "", "") if err != nil { return cancelAllOrdersResponse, err } split := strings.Split(a.Data.Successes, ",") for x := range split { cancelAllOrdersResponse.Status[split[x]] = "success" } for y := range a.Data.Errors { cancelAllOrdersResponse.Status[strconv.FormatInt(a.Data.Errors[y].OrderID, 10)] = "fail: " + a.Data.Errors[y].ErrMsg } } } else { a, err := e.FCancelAllOrders(ctx, orderCancellation.Pair, "", "") if err != nil { return cancelAllOrdersResponse, err } split := strings.Split(a.Data.Successes, ",") for x := range split { cancelAllOrdersResponse.Status[split[x]] = "success" } for y := range a.Data.Errors { cancelAllOrdersResponse.Status[strconv.FormatInt(a.Data.Errors[y].OrderID, 10)] = "fail: " + a.Data.Errors[y].ErrMsg } } } return cancelAllOrdersResponse, nil } // GetOrderInfo returns order information based on order ID func (e *Exchange) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, assetType asset.Item) (*order.Detail, error) { if pair.IsEmpty() { return nil, currency.ErrCurrencyPairEmpty } if err := e.CurrencyPairs.IsAssetEnabled(assetType); err != nil { return nil, err } var orderDetail order.Detail switch assetType { case asset.Spot: oID, err := strconv.ParseInt(orderID, 10, 64) if err != nil { return nil, err } resp, err := e.GetOrder(ctx, oID) if err != nil { return nil, err } respData := &resp if respData.ID == 0 { return nil, fmt.Errorf("%s - order not found for orderid %s", e.Name, orderID) } responseID := strconv.FormatInt(respData.ID, 10) if responseID != orderID { return nil, errors.New(e.Name + " - GetOrderInfo orderID mismatch. Expected: " + orderID + " Received: " + responseID) } typeDetails := strings.Split(respData.Type, "-") orderSide, err := order.StringToOrderSide(typeDetails[0]) if err != nil { if e.Websocket.IsConnected() { e.Websocket.DataHandler <- order.ClassificationError{ Exchange: e.Name, OrderID: orderID, Err: err, } } else { return nil, err } } orderType, err := order.StringToOrderType(typeDetails[1]) if err != nil { if e.Websocket.IsConnected() { e.Websocket.DataHandler <- order.ClassificationError{ Exchange: e.Name, OrderID: orderID, Err: err, } } else { return nil, err } } orderStatus, err := order.StringToOrderStatus(respData.State) if err != nil { if e.Websocket.IsConnected() { e.Websocket.DataHandler <- order.ClassificationError{ Exchange: e.Name, OrderID: orderID, Err: err, } } else { return nil, err } } var p currency.Pair var a asset.Item p, a, err = e.GetRequestFormattedPairAndAssetType(respData.Symbol) if err != nil { return nil, err } orderDetail = order.Detail{ Exchange: e.Name, OrderID: orderID, AccountID: strconv.FormatInt(respData.AccountID, 10), Pair: p, Type: orderType, Side: orderSide, Date: respData.CreatedAt.Time(), Status: orderStatus, Price: respData.Price, Amount: respData.Amount, ExecutedAmount: respData.FilledAmount, Fee: respData.FilledFees, AssetType: a, } case asset.CoinMarginedFutures: orderInfo, err := e.GetSwapOrderInfo(ctx, pair, orderID, "") if err != nil { return nil, err } var orderVars OrderVars for x := range orderInfo.Data { orderVars, err = compatibleVars(orderInfo.Data[x].Direction, orderInfo.Data[x].OrderPriceType, orderInfo.Data[x].Status) if err != nil { return nil, err } maker := false if orderVars.OrderType == order.Limit || orderVars.TimeInForce.Is(order.PostOnly) { maker = true } orderDetail.Trades = append(orderDetail.Trades, order.TradeHistory{ Price: orderInfo.Data[x].Price, Amount: orderInfo.Data[x].Volume, Fee: orderInfo.Data[x].Fee, Exchange: e.Name, TID: orderInfo.Data[x].OrderIDString, Type: orderVars.OrderType, Side: orderVars.Side, IsMaker: maker, }) } case asset.Futures: fPair, err := e.FormatSymbol(pair, asset.Futures) if err != nil { return nil, err } orderInfo, err := e.FGetOrderInfo(ctx, fPair, orderID, "") if err != nil { return nil, err } var orderVars OrderVars for x := range orderInfo.Data { orderVars, err = compatibleVars(orderInfo.Data[x].Direction, orderInfo.Data[x].OrderPriceType, orderInfo.Data[x].Status) if err != nil { return nil, err } orderDetail.Trades = append(orderDetail.Trades, order.TradeHistory{ Price: orderInfo.Data[x].Price, Amount: orderInfo.Data[x].Volume, Fee: orderInfo.Data[x].Fee, Exchange: e.Name, TID: orderInfo.Data[x].OrderIDString, Type: orderVars.OrderType, Side: orderVars.Side, IsMaker: orderVars.OrderType == order.Limit || orderVars.TimeInForce.Is(order.PostOnly), }) } default: return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType) } return &orderDetail, nil } // GetDepositAddress returns a deposit address for a specified currency func (e *Exchange) GetDepositAddress(ctx context.Context, cryptocurrency currency.Code, _, chain string) (*deposit.Address, error) { resp, err := e.QueryDepositAddress(ctx, cryptocurrency) if err != nil { return nil, err } for x := range resp { if chain != "" && strings.EqualFold(resp[x].Chain, chain) { return &deposit.Address{ Address: resp[x].Address, Tag: resp[x].AddressTag, }, nil } else if chain == "" && strings.EqualFold(resp[x].Currency, cryptocurrency.String()) { return &deposit.Address{ Address: resp[x].Address, Tag: resp[x].AddressTag, }, nil } } return nil, errors.New("unable to match deposit address currency or chain") } // WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is // submitted func (e *Exchange) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) { if err := withdrawRequest.Validate(); err != nil { return nil, err } resp, err := e.Withdraw(ctx, withdrawRequest.Currency, withdrawRequest.Crypto.Address, withdrawRequest.Crypto.AddressTag, withdrawRequest.Crypto.Chain, withdrawRequest.Amount, withdrawRequest.Crypto.FeeAmount) if err != nil { return nil, err } return &withdraw.ExchangeResponse{ ID: strconv.FormatInt(resp, 10), }, err } // WithdrawFiatFunds returns a withdrawal ID when a // withdrawal is submitted func (e *Exchange) WithdrawFiatFunds(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a // withdrawal is submitted func (e *Exchange) WithdrawFiatFundsToInternationalBank(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) { return nil, common.ErrFunctionNotSupported } // GetFeeByType returns an estimate of fee based on type of transaction func (e *Exchange) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) { if feeBuilder == nil { return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer) } if !e.AreCredentialsValid(ctx) && // Todo check connection status feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { feeBuilder.FeeType = exchange.OfflineTradeFee } return e.GetFee(feeBuilder) } // GetActiveOrders retrieves any orders that are active/open func (e *Exchange) GetActiveOrders(ctx context.Context, req *order.MultiOrderRequest) (order.FilteredOrders, error) { err := req.Validate() if err != nil { return nil, err } var orders []order.Detail switch req.AssetType { case asset.Spot: if len(req.Pairs) == 0 { return nil, errors.New("currency must be supplied") } side := "" if req.Side == order.Sell { side = req.Side.Lower() } creds, err := e.GetCredentials(ctx) if err != nil { return nil, err } for i := range req.Pairs { resp, err := e.GetOpenOrders(ctx, req.Pairs[i], creds.ClientID, side, 500) if err != nil { return nil, err } for x := range resp { orderDetail := order.Detail{ OrderID: strconv.FormatInt(resp[x].ID, 10), Price: resp[x].Price, Amount: resp[x].Amount, ExecutedAmount: resp[x].FilledAmount, RemainingAmount: resp[x].Amount - resp[x].FilledAmount, Pair: req.Pairs[i], Exchange: e.Name, Date: resp[x].CreatedAt.Time(), AccountID: strconv.FormatInt(resp[x].AccountID, 10), Fee: resp[x].FilledFees, } setOrderSideStatusAndType(resp[x].State, resp[x].Type, &orderDetail) orders = append(orders, orderDetail) } } case asset.CoinMarginedFutures: for x := range req.Pairs { var currentPage int64 for done := false; !done; { openOrders, err := e.GetSwapOpenOrders(ctx, req.Pairs[x], currentPage, 50) if err != nil { return orders, err } for x := range openOrders.Data.Orders { orderVars, err := compatibleVars(openOrders.Data.Orders[x].Direction, openOrders.Data.Orders[x].OrderPriceType, openOrders.Data.Orders[x].Status) if err != nil { return orders, err } p, err := currency.NewPairFromString(openOrders.Data.Orders[x].ContractCode) if err != nil { return orders, err } orders = append(orders, order.Detail{ TimeInForce: orderVars.TimeInForce, Leverage: openOrders.Data.Orders[x].LeverageRate, Price: openOrders.Data.Orders[x].Price, Amount: openOrders.Data.Orders[x].Volume, ExecutedAmount: openOrders.Data.Orders[x].TradeVolume, RemainingAmount: openOrders.Data.Orders[x].Volume - openOrders.Data.Orders[x].TradeVolume, Fee: openOrders.Data.Orders[x].Fee, Exchange: e.Name, AssetType: req.AssetType, OrderID: openOrders.Data.Orders[x].OrderIDString, Side: orderVars.Side, Type: orderVars.OrderType, Status: orderVars.Status, Pair: p, }) } currentPage++ done = currentPage == openOrders.Data.TotalPage } } case asset.Futures: for x := range req.Pairs { var currentPage int64 for done := false; !done; { openOrders, err := e.FGetOpenOrders(ctx, req.Pairs[x].Base, currentPage, 50) if err != nil { return orders, err } var orderVars OrderVars for x := range openOrders.Data.Orders { orderVars, err = compatibleVars(openOrders.Data.Orders[x].Direction, openOrders.Data.Orders[x].OrderPriceType, openOrders.Data.Orders[x].Status) if err != nil { return orders, err } p, err := currency.NewPairFromString(openOrders.Data.Orders[x].ContractCode) if err != nil { return orders, err } orders = append(orders, order.Detail{ TimeInForce: orderVars.TimeInForce, Leverage: openOrders.Data.Orders[x].LeverageRate, Price: openOrders.Data.Orders[x].Price, Amount: openOrders.Data.Orders[x].Volume, ExecutedAmount: openOrders.Data.Orders[x].TradeVolume, RemainingAmount: openOrders.Data.Orders[x].Volume - openOrders.Data.Orders[x].TradeVolume, Fee: openOrders.Data.Orders[x].Fee, Exchange: e.Name, AssetType: req.AssetType, OrderID: openOrders.Data.Orders[x].OrderIDString, Side: orderVars.Side, Type: orderVars.OrderType, Status: orderVars.Status, Pair: p, }) } currentPage++ done = currentPage == openOrders.Data.TotalPage } } } return req.Filter(e.Name, orders), nil } // GetOrderHistory retrieves account order information // Can Limit response to specific order status func (e *Exchange) GetOrderHistory(ctx context.Context, req *order.MultiOrderRequest) (order.FilteredOrders, error) { err := req.Validate() if err != nil { return nil, err } var orders []order.Detail switch req.AssetType { case asset.Spot: if len(req.Pairs) == 0 { return nil, errors.New("currency must be supplied") } states := "partial-canceled,filled,canceled" for i := range req.Pairs { resp, err := e.GetOrders(ctx, req.Pairs[i], "", "", "", states, "", "", "") if err != nil { return nil, err } for x := range resp { orderDetail := order.Detail{ OrderID: strconv.FormatInt(resp[x].ID, 10), Price: resp[x].Price, Amount: resp[x].Amount, ExecutedAmount: resp[x].FilledAmount, RemainingAmount: resp[x].Amount - resp[x].FilledAmount, Cost: resp[x].FilledCashAmount, CostAsset: req.Pairs[i].Quote, Pair: req.Pairs[i], Exchange: e.Name, Date: resp[x].CreatedAt.Time(), CloseTime: resp[x].FinishedAt.Time(), AccountID: strconv.FormatInt(resp[x].AccountID, 10), Fee: resp[x].FilledFees, } setOrderSideStatusAndType(resp[x].State, resp[x].Type, &orderDetail) orderDetail.InferCostsAndTimes() orders = append(orders, orderDetail) } } case asset.CoinMarginedFutures: for x := range req.Pairs { var currentPage int64 for done := false; !done; { orderHistory, err := e.GetSwapOrderHistory(ctx, req.Pairs[x], "all", "all", []order.Status{order.AnyStatus}, int64(req.EndTime.Sub(req.StartTime).Hours()/24), currentPage, 50) if err != nil { return orders, err } var orderVars OrderVars for x := range orderHistory.Data.Orders { p, err := currency.NewPairFromString(orderHistory.Data.Orders[x].ContractCode) if err != nil { return orders, err } orderVars, err = compatibleVars(orderHistory.Data.Orders[x].Direction, orderHistory.Data.Orders[x].OrderPriceType, orderHistory.Data.Orders[x].Status) if err != nil { return orders, err } orders = append(orders, order.Detail{ TimeInForce: orderVars.TimeInForce, Leverage: orderHistory.Data.Orders[x].LeverageRate, Price: orderHistory.Data.Orders[x].Price, Amount: orderHistory.Data.Orders[x].Volume, ExecutedAmount: orderHistory.Data.Orders[x].TradeVolume, RemainingAmount: orderHistory.Data.Orders[x].Volume - orderHistory.Data.Orders[x].TradeVolume, Fee: orderHistory.Data.Orders[x].Fee, Exchange: e.Name, AssetType: req.AssetType, OrderID: orderHistory.Data.Orders[x].OrderIDString, Side: orderVars.Side, Type: orderVars.OrderType, Status: orderVars.Status, Pair: p, }) } currentPage++ done = currentPage == orderHistory.Data.TotalPage } } case asset.Futures: for x := range req.Pairs { var currentPage int64 for done := false; !done; { openOrders, err := e.FGetOrderHistory(ctx, req.Pairs[x], "", "all", "all", "limit", []order.Status{order.AnyStatus}, int64(req.EndTime.Sub(req.StartTime).Hours()/24), currentPage, 50) if err != nil { return orders, err } var orderVars OrderVars for x := range openOrders.Data.Orders { orderVars, err = compatibleVars(openOrders.Data.Orders[x].Direction, openOrders.Data.Orders[x].OrderPriceType, openOrders.Data.Orders[x].Status) if err != nil { return orders, err } if req.Side != orderVars.Side { continue } if req.Type != orderVars.OrderType { continue } p, err := currency.NewPairFromString(openOrders.Data.Orders[x].ContractCode) if err != nil { return orders, err } orders = append(orders, order.Detail{ TimeInForce: orderVars.TimeInForce, Leverage: openOrders.Data.Orders[x].LeverageRate, Price: openOrders.Data.Orders[x].Price, Amount: openOrders.Data.Orders[x].Volume, ExecutedAmount: openOrders.Data.Orders[x].TradeVolume, RemainingAmount: openOrders.Data.Orders[x].Volume - openOrders.Data.Orders[x].TradeVolume, Fee: openOrders.Data.Orders[x].Fee, Exchange: e.Name, AssetType: req.AssetType, OrderID: openOrders.Data.Orders[x].OrderIDString, Side: orderVars.Side, Type: orderVars.OrderType, Status: orderVars.Status, Pair: p, Date: openOrders.Data.Orders[x].CreateDate.Time(), }) } currentPage++ done = currentPage == openOrders.Data.TotalPage } } } return req.Filter(e.Name, orders), nil } func setOrderSideStatusAndType(orderState, requestType string, orderDetail *order.Detail) { var err error if orderDetail.Status, err = order.StringToOrderStatus(orderState); err != nil { log.Errorf(log.ExchangeSys, "%s %v", orderDetail.Exchange, err) } switch SpotNewOrderRequestParamsType(requestType) { case SpotNewOrderRequestTypeBuyMarket: orderDetail.Side = order.Buy orderDetail.Type = order.Market case SpotNewOrderRequestTypeSellMarket: orderDetail.Side = order.Sell orderDetail.Type = order.Market case SpotNewOrderRequestTypeBuyLimit: orderDetail.Side = order.Buy orderDetail.Type = order.Limit case SpotNewOrderRequestTypeSellLimit: orderDetail.Side = order.Sell orderDetail.Type = order.Limit } } // AuthenticateWebsocket sends an authentication message to the websocket func (e *Exchange) AuthenticateWebsocket(ctx context.Context) error { return e.wsLogin(ctx) } // ValidateAPICredentials validates current credentials used for wrapper functionality func (e *Exchange) ValidateAPICredentials(ctx context.Context, assetType asset.Item) error { _, err := e.UpdateAccountBalances(ctx, assetType) return e.CheckTransientError(err) } // FormatExchangeKlineInterval returns Interval to exchange formatted string func (e *Exchange) FormatExchangeKlineInterval(in kline.Interval) string { switch in { case kline.OneMin, kline.FiveMin, kline.FifteenMin, kline.ThirtyMin: return in.Short() + "in" case kline.OneHour: return "60min" case kline.FourHour: return "4hour" case kline.OneDay: return "1day" case kline.OneMonth: return "1mon" case kline.OneWeek: return "1week" case kline.OneYear: return "1year" } return "" } // GetHistoricCandles returns candles between a time period for a set time interval func (e *Exchange) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { req, err := e.GetKlineRequest(pair, a, interval, start, end, true) if err != nil { return nil, err } timeSeries := make([]kline.Candle, 0, req.Size()) switch a { case asset.Spot: candles, err := e.GetSpotKline(ctx, KlinesRequestParams{ Period: e.FormatExchangeKlineInterval(req.ExchangeInterval), Symbol: req.Pair, Size: req.RequestLimit, }) if err != nil { return nil, err } for x := range candles { timestamp := candles[x].IDTimestamp.Time() if timestamp.Before(req.Start) || timestamp.After(req.End) { continue } timeSeries = append(timeSeries, kline.Candle{ Time: timestamp, Open: candles[x].Open, High: candles[x].High, Low: candles[x].Low, Close: candles[x].Close, Volume: candles[x].Volume, }) } case asset.Futures: // if size, from, to are all populated, only size is considered size := int64(-1) candles, err := e.FGetKlineData(ctx, req.Pair, e.FormatExchangeKlineInterval(req.ExchangeInterval), size, req.Start, req.End) if err != nil { return nil, err } for x := range candles.Data { timestamp := candles.Data[x].IDTimestamp.Time() if timestamp.Before(req.Start) || timestamp.After(req.End) { continue } timeSeries = append(timeSeries, kline.Candle{ Time: timestamp, Open: candles.Data[x].Open, High: candles.Data[x].High, Low: candles.Data[x].Low, Close: candles.Data[x].Close, Volume: candles.Data[x].Volume, }) } case asset.CoinMarginedFutures: // if size, from, to are all populated, only size is considered size := int64(-1) candles, err := e.GetSwapKlineData(ctx, req.Pair, e.FormatExchangeKlineInterval(req.ExchangeInterval), size, req.Start, req.End) if err != nil { return nil, err } for x := range candles.Data { timestamp := candles.Data[x].IDTimestamp.Time() if timestamp.Before(req.Start) || timestamp.After(req.End) { continue } timeSeries = append(timeSeries, kline.Candle{ Time: timestamp, Open: candles.Data[x].Open, High: candles.Data[x].High, Low: candles.Data[x].Low, Close: candles.Data[x].Close, Volume: candles.Data[x].Volume, }) } } return req.ProcessResponse(timeSeries) } // GetHistoricCandlesExtended returns candles between a time period for a set time interval func (e *Exchange) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) { req, err := e.GetKlineExtendedRequest(pair, a, interval, start, end) if err != nil { return nil, err } timeSeries := make([]kline.Candle, 0, req.Size()) switch a { case asset.Spot: return nil, common.ErrFunctionNotSupported case asset.Futures: for i := range req.RangeHolder.Ranges { // if size, from, to are all populated, only size is considered size := int64(-1) var candles FKlineData candles, err = e.FGetKlineData(ctx, req.Pair, e.FormatExchangeKlineInterval(req.ExchangeInterval), size, req.RangeHolder.Ranges[i].Start.Time, req.RangeHolder.Ranges[i].End.Time) if err != nil { return nil, err } for x := range candles.Data { // align response data timestamp := candles.Data[x].IDTimestamp.Time() if timestamp.Before(req.Start) || timestamp.After(req.End) { continue } timeSeries = append(timeSeries, kline.Candle{ Time: timestamp, Open: candles.Data[x].Open, High: candles.Data[x].High, Low: candles.Data[x].Low, Close: candles.Data[x].Close, Volume: candles.Data[x].Volume, }) } } case asset.CoinMarginedFutures: for i := range req.RangeHolder.Ranges { // if size, from, to are all populated, only size is considered size := int64(-1) var candles SwapKlineData candles, err = e.GetSwapKlineData(ctx, req.Pair, e.FormatExchangeKlineInterval(req.ExchangeInterval), size, req.RangeHolder.Ranges[i].Start.Time, req.RangeHolder.Ranges[i].End.Time) if err != nil { return nil, err } for x := range candles.Data { // align response data timestamp := candles.Data[x].IDTimestamp.Time() if timestamp.Before(req.Start) || timestamp.After(req.End) { continue } timeSeries = append(timeSeries, kline.Candle{ Time: timestamp, Open: candles.Data[x].Open, High: candles.Data[x].High, Low: candles.Data[x].Low, Close: candles.Data[x].Close, Volume: candles.Data[x].Volume, }) } } } return req.ProcessResponse(timeSeries) } // compatibleVars gets compatible variables for order vars func compatibleVars(side, orderPriceType string, status int64) (OrderVars, error) { var resp OrderVars switch side { case "buy": resp.Side = order.Buy case "sell": resp.Side = order.Sell default: return resp, errors.New("invalid orderSide") } switch orderPriceType { case "limit": resp.OrderType = order.Limit case "opponent": resp.OrderType = order.Market case "post_only": resp.OrderType = order.Limit resp.TimeInForce = order.PostOnly default: return resp, errors.New("invalid orderPriceType") } switch status { case 1, 2, 11: resp.Status = order.UnknownStatus case 3: resp.Status = order.Active case 4: resp.Status = order.PartiallyFilled case 5: resp.Status = order.PartiallyCancelled case 6: resp.Status = order.Filled case 7: resp.Status = order.Cancelled default: return resp, errors.New("invalid orderStatus") } return resp, nil } // GetAvailableTransferChains returns the available transfer blockchains for the specific cryptocurrency func (e *Exchange) GetAvailableTransferChains(ctx context.Context, cryptocurrency currency.Code) ([]string, error) { resp, err := e.GetCurrenciesIncludingChains(ctx, cryptocurrency) if err != nil { return nil, err } if len(resp) == 0 { return nil, errors.New("no chains returned from currencies API") } chains := resp[0].ChainData availableChains := make([]string, 0, len(chains)) for _, c := range chains { if c.DepositStatus == "allowed" || c.WithdrawStatus == "allowed" { availableChains = append(availableChains, c.Chain) } } return availableChains, nil } // GetServerTime returns the current exchange server time. func (e *Exchange) GetServerTime(ctx context.Context, _ asset.Item) (time.Time, error) { return e.GetCurrentServerTime(ctx) } // GetFuturesContractDetails returns details about futures contracts func (e *Exchange) GetFuturesContractDetails(ctx context.Context, item asset.Item) ([]futures.Contract, error) { if !item.IsFutures() { return nil, futures.ErrNotFuturesAsset } if !e.SupportsAsset(item) { return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, item) } switch item { case asset.CoinMarginedFutures: result, err := e.GetSwapMarkets(ctx, currency.EMPTYPAIR) if err != nil { return nil, err } resp := make([]futures.Contract, 0, len(result)) for x := range result { contractSplitIndex := strings.Split(result[x].ContractCode, currency.DashDelimiter) var cp, underlying currency.Pair cp, err = currency.NewPairFromStrings(contractSplitIndex[0], contractSplitIndex[1]) if err != nil { return nil, err } underlying, err = currency.NewPairFromStrings(result[x].Symbol, "USD") if err != nil { return nil, err } var s time.Time s, err = time.Parse("20060102", result[x].CreateDate) if err != nil { return nil, err } resp = append(resp, futures.Contract{ Exchange: e.Name, Name: cp, Underlying: underlying, Asset: item, StartDate: s, SettlementType: futures.Inverse, IsActive: result[x].ContractStatus == 1, Type: futures.Perpetual, SettlementCurrency: currency.USD, Multiplier: result[x].ContractSize, }) } return resp, nil case asset.Futures: result, err := e.FGetContractInfo(ctx, "", "", currency.EMPTYPAIR) if err != nil { return nil, err } resp := make([]futures.Contract, 0, len(result.Data)) for x := range result.Data { contractSplitIndex := strings.Split(result.Data[x].ContractCode, result.Data[x].Symbol) var cp, underlying currency.Pair cp, err = currency.NewPairFromStrings(result.Data[x].Symbol, contractSplitIndex[1]) if err != nil { return nil, err } underlying, err = currency.NewPairFromStrings(result.Data[x].Symbol, "USD") if err != nil { return nil, err } var startTime, endTime time.Time startTime, err = time.Parse("20060102", result.Data[x].CreateDate) if err != nil { return nil, err } if result.Data[x].DeliveryTime.Time().IsZero() { endTime = result.Data[x].DeliveryTime.Time() } else { endTime = result.Data[x].SettlementTime.Time() } contractLength := endTime.Sub(startTime) var ct futures.ContractType switch { case contractLength <= kline.OneWeek.Duration()+kline.ThreeDay.Duration(): ct = futures.Weekly case contractLength <= kline.TwoWeek.Duration()+kline.ThreeDay.Duration(): ct = futures.Fortnightly case contractLength <= kline.ThreeMonth.Duration()+kline.ThreeWeek.Duration(): ct = futures.Quarterly case contractLength <= kline.SixMonth.Duration()+kline.ThreeWeek.Duration(): ct = futures.HalfYearly default: ct = futures.Perpetual } resp = append(resp, futures.Contract{ Exchange: e.Name, Name: cp, Underlying: underlying, Asset: item, StartDate: startTime, EndDate: endTime, SettlementType: futures.Linear, IsActive: result.Data[x].ContractStatus == 1, Type: ct, SettlementCurrency: currency.USD, Multiplier: result.Data[x].ContractSize, }) } return resp, nil } return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, item) } // GetLatestFundingRates returns the latest funding rates data func (e *Exchange) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { if r == nil { return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer) } if r.Asset != asset.CoinMarginedFutures { return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, r.Asset) } var rates []FundingRatesData if r.Pair.IsEmpty() { batchRates, err := e.GetSwapFundingRates(ctx) if err != nil { return nil, err } rates = batchRates.Data } else { rateResp, err := e.GetSwapFundingRate(ctx, r.Pair) if err != nil { return nil, err } rates = append(rates, rateResp) } resp := make([]fundingrate.LatestRateResponse, 0, len(rates)) for i := range rates { if rates[i].ContractCode == "" { // formatting to match documentation rates[i].ContractCode = rates[i].Symbol + "-USD" } cp, isEnabled, err := e.MatchSymbolCheckEnabled(rates[i].ContractCode, r.Asset, true) if err != nil && !errors.Is(err, currency.ErrPairNotFound) { return nil, err } if !isEnabled { continue } var isPerp bool isPerp, err = e.IsPerpetualFutureCurrency(r.Asset, cp) if err != nil { return nil, err } if !isPerp { continue } ft, nft := rates[i].FundingTime.Time(), rates[i].NextFundingTime.Time() var fri time.Duration if len(e.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 { // can infer funding rate interval from the only funding rate frequency defined for k := range e.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies { fri = k.Duration() } } if rates[i].FundingTime.Time().IsZero() { ft = nft.Add(-fri) } if ft.After(time.Now()) { ft = ft.Add(-fri) nft = nft.Add(-fri) } rate := fundingrate.LatestRateResponse{ Exchange: e.Name, Asset: r.Asset, Pair: cp, LatestRate: fundingrate.Rate{ Time: ft, Rate: decimal.NewFromFloat(rates[i].FundingRate), }, TimeOfNextRate: nft, TimeChecked: time.Now(), } if r.IncludePredictedRate { rate.PredictedUpcomingRate = fundingrate.Rate{ Time: rate.TimeOfNextRate, Rate: decimal.NewFromFloat(rates[i].EstimatedRate), } } resp = append(resp, rate) } return resp, nil } // IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future func (e *Exchange) IsPerpetualFutureCurrency(a asset.Item, _ currency.Pair) (bool, error) { return a == asset.CoinMarginedFutures, nil } // UpdateOrderExecutionLimits updates order execution limits func (e *Exchange) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { return common.ErrNotYetImplemented } // GetOpenInterest returns the open interest rate for a given asset pair func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]futures.OpenInterest, error) { for i := range k { if k[i].Asset != asset.Futures && k[i].Asset != asset.CoinMarginedFutures { // avoid API calls or returning errors after a successful retrieval return nil, fmt.Errorf("%w %v %v", asset.ErrNotSupported, k[i].Asset, k[i].Pair()) } } if len(k) == 1 { switch k[0].Asset { case asset.Futures: if !slices.Contains(validContractExpiryCodes, strings.ToUpper(k[0].Pair().Quote.String())) { // Huobi does not like requests being made with contract expiry in them (eg BTC240109) return nil, fmt.Errorf("%w %v, must use shorthand such as CW (current week)", currency.ErrCurrencyNotSupported, k[0].Pair()) } data, err := e.FContractOpenInterest(ctx, "", "", k[0].Pair()) if err != nil { data2, err2 := e.ContractOpenInterestUSDT(ctx, k[0].Pair(), currency.EMPTYPAIR, "", "") if err2 != nil { return nil, fmt.Errorf("%w %w", err, err2) } data.Data = data2 } for i := range data.Data { var p currency.Pair p, err = e.MatchSymbolWithAvailablePairs(data.Data[i].ContractCode, k[0].Asset, true) if err != nil { if errors.Is(err, currency.ErrPairNotFound) { continue } return nil, err } return []futures.OpenInterest{ { Key: key.NewExchangeAssetPair(e.Name, k[0].Asset, p), OpenInterest: data.Data[i].Amount, }, }, nil } case asset.CoinMarginedFutures: data, err := e.SwapOpenInterestInformation(ctx, k[0].Pair()) if err != nil { return nil, err } for i := range data.Data { var p currency.Pair p, err = e.MatchSymbolWithAvailablePairs(data.Data[i].ContractCode, k[0].Asset, true) if err != nil { if errors.Is(err, currency.ErrPairNotFound) { continue } return nil, err } return []futures.OpenInterest{ { Key: key.NewExchangeAssetPair(e.Name, k[0].Asset, p), OpenInterest: data.Data[i].Amount, }, }, nil } } } var resp []futures.OpenInterest for _, a := range e.GetAssetTypes(true) { switch a { case asset.Futures: data, err := e.FContractOpenInterest(ctx, "", "", currency.EMPTYPAIR) if err != nil { return nil, err } uData, err := e.ContractOpenInterestUSDT(ctx, currency.EMPTYPAIR, currency.EMPTYPAIR, "", "") if err != nil { return nil, err } allData := make([]UContractOpenInterest, 0, len(data.Data)+len(uData)) allData = append(allData, data.Data...) allData = append(allData, uData...) for i := range allData { var p currency.Pair var isEnabled, appendData bool p, isEnabled, err = e.MatchSymbolCheckEnabled(allData[i].ContractCode, a, true) if err != nil && !errors.Is(err, currency.ErrPairNotFound) { return nil, err } if !isEnabled { continue } for j := range k { if k[j].Pair().Equal(p) { appendData = true break } } if len(k) > 0 && !appendData { continue } resp = append(resp, futures.OpenInterest{ Key: key.NewExchangeAssetPair(e.Name, a, p), OpenInterest: allData[i].Amount, }) } case asset.CoinMarginedFutures: data, err := e.SwapOpenInterestInformation(ctx, currency.EMPTYPAIR) if err != nil { return nil, err } for i := range data.Data { p, isEnabled, err := e.MatchSymbolCheckEnabled(data.Data[i].ContractCode, a, true) if err != nil && !errors.Is(err, currency.ErrPairNotFound) { return nil, err } if !isEnabled { continue } var appendData bool for j := range k { if k[j].Pair().Equal(p) { appendData = true break } } if len(k) > 0 && !appendData { continue } resp = append(resp, futures.OpenInterest{ Key: key.NewExchangeAssetPair(e.Name, a, p), OpenInterest: data.Data[i].Amount, }) } } } return resp, nil } // GetCurrencyTradeURL returns the UR˜L to the exchange's trade page for the given asset and currency pair func (e *Exchange) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp currency.Pair) (string, error) { _, err := e.CurrencyPairs.IsPairEnabled(cp, a) if err != nil { return "", err } switch a { case asset.Spot: cp.Delimiter = currency.UnderscoreDelimiter return tradeBaseURL + tradeSpot + cp.Lower().String(), nil case asset.Futures: if !cp.Quote.Equal(currency.USD) && !cp.Quote.Equal(currency.USDT) { // todo: support long dated currencies return "", fmt.Errorf("%w %v requires translating currency into static contracts eg 'weekly'", common.ErrNotYetImplemented, a) } cp.Delimiter = currency.DashDelimiter return tradeBaseURL + tradeFutures + cp.Upper().String(), nil case asset.CoinMarginedFutures: if !cp.Quote.Equal(currency.USD) && !cp.Quote.Equal(currency.USDT) { // todo: support long dated currencies return "", fmt.Errorf("%w %v requires translating currency into static contracts eg 'weekly'", common.ErrNotYetImplemented, a) } return tradeBaseURL + tradeCoinMargined + cp.Base.Upper().String(), nil default: return "", fmt.Errorf("%w %q", asset.ErrNotSupported, a) } }