mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
* Shift wrapper function GetDefaultConfig to exchange.Base method definition, to ensure set defaults doesn't get called twice and to reduce code * rm alphapoint bootstrap method as is defined as exchange.Base method * add tests * glorious: make it a function and make it IBOTEXCHANGE --------- Co-authored-by: shazbert <ryan.oharareid@thrasher.io>
2476 lines
73 KiB
Go
2476 lines
73 KiB
Go
package huobi
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"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"
|
|
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
|
|
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
|
|
"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/stream"
|
|
"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 (h *HUOBI) SetDefaults() {
|
|
h.Name = "Huobi"
|
|
h.Enabled = true
|
|
h.Verbose = true
|
|
h.API.CredentialsValidator.RequiresKey = true
|
|
h.API.CredentialsValidator.RequiresSecret = true
|
|
|
|
fmt1 := currency.PairStore{
|
|
RequestFormat: ¤cy.PairFormat{Uppercase: false},
|
|
ConfigFormat: ¤cy.PairFormat{
|
|
Delimiter: currency.DashDelimiter,
|
|
Uppercase: true,
|
|
},
|
|
}
|
|
coinFutures := currency.PairStore{
|
|
RequestFormat: ¤cy.PairFormat{
|
|
Uppercase: true,
|
|
Delimiter: currency.DashDelimiter,
|
|
},
|
|
ConfigFormat: ¤cy.PairFormat{
|
|
Uppercase: true,
|
|
Delimiter: currency.DashDelimiter,
|
|
},
|
|
}
|
|
futuresFormatting := currency.PairStore{
|
|
RequestFormat: ¤cy.PairFormat{
|
|
Uppercase: true,
|
|
},
|
|
ConfigFormat: ¤cy.PairFormat{
|
|
Uppercase: true,
|
|
Delimiter: currency.DashDelimiter,
|
|
},
|
|
}
|
|
err := h.StoreAssetPairFormat(asset.Spot, fmt1)
|
|
if err != nil {
|
|
log.Errorln(log.ExchangeSys, err)
|
|
}
|
|
err = h.StoreAssetPairFormat(asset.CoinMarginedFutures, coinFutures)
|
|
if err != nil {
|
|
log.Errorln(log.ExchangeSys, err)
|
|
}
|
|
err = h.StoreAssetPairFormat(asset.Futures, futuresFormatting)
|
|
if err != nil {
|
|
log.Errorln(log.ExchangeSys, err)
|
|
}
|
|
|
|
h.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,
|
|
},
|
|
},
|
|
}
|
|
|
|
h.Requester, err = request.New(h.Name,
|
|
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),
|
|
request.WithLimiter(SetRateLimit()))
|
|
if err != nil {
|
|
log.Errorln(log.ExchangeSys, err)
|
|
}
|
|
h.API.Endpoints = h.NewEndpoints()
|
|
err = h.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{
|
|
exchange.RestSpot: huobiAPIURL,
|
|
exchange.RestFutures: huobiFuturesURL,
|
|
exchange.RestCoinMargined: huobiFuturesURL,
|
|
exchange.WebsocketSpot: wsMarketURL,
|
|
})
|
|
if err != nil {
|
|
log.Errorln(log.ExchangeSys, err)
|
|
}
|
|
h.Websocket = stream.NewWebsocket()
|
|
h.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
|
|
h.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
|
|
h.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
|
|
}
|
|
|
|
// Setup sets user configuration
|
|
func (h *HUOBI) Setup(exch *config.Exchange) error {
|
|
err := exch.Validate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !exch.Enabled {
|
|
h.SetEnabled(false)
|
|
return nil
|
|
}
|
|
err = h.SetupDefaults(exch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
wsRunningURL, err := h.API.Endpoints.GetURL(exchange.WebsocketSpot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = h.Websocket.Setup(&stream.WebsocketSetup{
|
|
ExchangeConfig: exch,
|
|
DefaultURL: wsMarketURL,
|
|
RunningURL: wsRunningURL,
|
|
Connector: h.WsConnect,
|
|
Subscriber: h.Subscribe,
|
|
Unsubscriber: h.Unsubscribe,
|
|
GenerateSubscriptions: h.GenerateDefaultSubscriptions,
|
|
Features: &h.Features.Supports.WebsocketCapabilities,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = h.Websocket.SetupNewConnection(stream.ConnectionSetup{
|
|
RateLimit: rateLimit,
|
|
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
|
|
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return h.Websocket.SetupNewConnection(stream.ConnectionSetup{
|
|
RateLimit: rateLimit,
|
|
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
|
|
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
|
|
URL: wsAccountsOrdersURL,
|
|
Authenticated: true,
|
|
})
|
|
}
|
|
|
|
// FetchTradablePairs returns a list of the exchanges tradable pairs
|
|
func (h *HUOBI) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.Pairs, error) {
|
|
if !h.SupportsAsset(a) {
|
|
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a)
|
|
}
|
|
|
|
var pairs []currency.Pair
|
|
var pair currency.Pair
|
|
switch a {
|
|
case asset.Spot:
|
|
symbols, err := h.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 := h.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 := h.FGetContractInfo(ctx, "", "", currency.EMPTYPAIR)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pairs = make([]currency.Pair, 0, len(symbols.Data))
|
|
for c := range symbols.Data {
|
|
if symbols.Data[c].ContractStatus != 1 {
|
|
continue
|
|
}
|
|
pair, err := currency.NewPairFromString(symbols.Data[c].ContractCode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pairs = append(pairs, pair)
|
|
}
|
|
}
|
|
return pairs, nil
|
|
}
|
|
|
|
// UpdateTradablePairs updates the exchanges available pairs and stores
|
|
// them in the exchanges config
|
|
func (h *HUOBI) UpdateTradablePairs(ctx context.Context, forceUpdate bool) error {
|
|
assets := h.GetAssetTypes(false)
|
|
for x := range assets {
|
|
pairs, err := h.FetchTradablePairs(ctx, assets[x])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = h.UpdatePairs(pairs, assets[x], false, forceUpdate)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return h.EnsureOnePairEnabled()
|
|
}
|
|
|
|
// UpdateTickers updates the ticker for all currency pairs of a given asset type
|
|
func (h *HUOBI) UpdateTickers(ctx context.Context, a asset.Item) error {
|
|
switch a {
|
|
case asset.Spot:
|
|
ticks, err := h.GetTickers(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range ticks.Data {
|
|
var cp currency.Pair
|
|
cp, _, err = h.MatchSymbolCheckEnabled(ticks.Data[i].Symbol, a, false)
|
|
if err != nil {
|
|
if errors.Is(err, currency.ErrPairNotFound) {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
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].Volume,
|
|
QuoteVolume: ticks.Data[i].Amount,
|
|
Open: ticks.Data[i].Open,
|
|
Close: ticks.Data[i].Close,
|
|
BidSize: ticks.Data[i].BidSize,
|
|
AskSize: ticks.Data[i].AskSize,
|
|
Pair: cp,
|
|
ExchangeName: h.Name,
|
|
AssetType: a,
|
|
LastUpdated: time.Now(),
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case asset.CoinMarginedFutures:
|
|
ticks, err := h.GetBatchCoinMarginSwapContracts(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i := range ticks {
|
|
var cp currency.Pair
|
|
cp, _, err = h.MatchSymbolCheckEnabled(ticks[i].ContractCode, a, true)
|
|
if err != nil {
|
|
if errors.Is(err, currency.ErrPairNotFound) {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
tt := time.UnixMilli(ticks[i].Timestamp)
|
|
err = ticker.ProcessTicker(&ticker.Price{
|
|
High: ticks[i].High.Float64(),
|
|
Low: ticks[i].Low.Float64(),
|
|
Volume: ticks[i].Volume.Float64(),
|
|
QuoteVolume: ticks[i].Amount.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: h.Name,
|
|
AssetType: a,
|
|
LastUpdated: tt,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case asset.Futures:
|
|
linearTicks, err := h.GetBatchLinearSwapContracts(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ticks, err := h.GetBatchFuturesContracts(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
allTicks := make([]FuturesBatchTicker, 0, len(linearTicks)+len(ticks))
|
|
allTicks = append(allTicks, linearTicks...)
|
|
allTicks = append(allTicks, ticks...)
|
|
for i := range allTicks {
|
|
var cp currency.Pair
|
|
if allTicks[i].Symbol != "" {
|
|
cp, err = currency.NewPairFromString(allTicks[i].Symbol)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cp, err = h.convertContractShortHandToExpiry(cp, time.Now())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cp, _, err = h.MatchSymbolCheckEnabled(cp.String(), a, true)
|
|
if err != nil {
|
|
if errors.Is(err, currency.ErrPairNotFound) {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
} else {
|
|
cp, _, err = h.MatchSymbolCheckEnabled(allTicks[i].ContractCode, a, true)
|
|
if err != nil {
|
|
if errors.Is(err, currency.ErrPairNotFound) {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
tt := time.UnixMilli(allTicks[i].Timestamp)
|
|
err = ticker.ProcessTicker(&ticker.Price{
|
|
High: allTicks[i].High.Float64(),
|
|
Low: allTicks[i].Low.Float64(),
|
|
Volume: allTicks[i].Volume.Float64(),
|
|
QuoteVolume: allTicks[i].Amount.Float64(),
|
|
Open: allTicks[i].Open.Float64(),
|
|
Close: allTicks[i].Close.Float64(),
|
|
Bid: allTicks[i].Bid[0],
|
|
BidSize: allTicks[i].Bid[1],
|
|
Ask: allTicks[i].Ask[0],
|
|
AskSize: allTicks[i].Ask[1],
|
|
Pair: cp,
|
|
ExchangeName: h.Name,
|
|
AssetType: a,
|
|
LastUpdated: tt,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
default:
|
|
return fmt.Errorf("%w %v", asset.ErrNotSupported, a)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateTicker updates and returns the ticker for a currency pair
|
|
func (h *HUOBI) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error) {
|
|
if p.IsEmpty() {
|
|
return nil, currency.ErrCurrencyPairEmpty
|
|
}
|
|
if !h.SupportsAsset(a) {
|
|
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a)
|
|
}
|
|
switch a {
|
|
case asset.Spot:
|
|
tickerData, err := h.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.Volume,
|
|
Open: tickerData.Tick.Open,
|
|
Close: tickerData.Tick.Close,
|
|
Pair: p,
|
|
ExchangeName: h.Name,
|
|
AssetType: asset.Spot,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
case asset.CoinMarginedFutures:
|
|
marketData, err := h.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.Vol,
|
|
Open: marketData.Tick.Open,
|
|
Close: marketData.Tick.Close,
|
|
Pair: p,
|
|
Bid: marketData.Tick.Bid[0],
|
|
Ask: marketData.Tick.Ask[0],
|
|
ExchangeName: h.Name,
|
|
AssetType: a,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
case asset.Futures:
|
|
marketData, err := h.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.Vol,
|
|
Open: marketData.Tick.Open,
|
|
Close: marketData.Tick.Close,
|
|
Pair: p,
|
|
Bid: marketData.Tick.Bid[0],
|
|
Ask: marketData.Tick.Ask[0],
|
|
ExchangeName: h.Name,
|
|
AssetType: a,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return ticker.GetTicker(h.Name, p, a)
|
|
}
|
|
|
|
// FetchTicker returns the ticker for a currency pair
|
|
func (h *HUOBI) FetchTicker(ctx context.Context, p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
|
|
tickerNew, err := ticker.GetTicker(h.Name, p, assetType)
|
|
if err != nil {
|
|
return h.UpdateTicker(ctx, p, assetType)
|
|
}
|
|
return tickerNew, nil
|
|
}
|
|
|
|
// FetchOrderbook returns orderbook base on the currency pair
|
|
func (h *HUOBI) FetchOrderbook(ctx context.Context, p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
|
ob, err := orderbook.Get(h.Name, p, assetType)
|
|
if err != nil {
|
|
return h.UpdateOrderbook(ctx, p, assetType)
|
|
}
|
|
return ob, nil
|
|
}
|
|
|
|
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
|
func (h *HUOBI) UpdateOrderbook(ctx context.Context, p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
|
if p.IsEmpty() {
|
|
return nil, currency.ErrCurrencyPairEmpty
|
|
}
|
|
if !assetType.IsValid() {
|
|
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType)
|
|
}
|
|
book := &orderbook.Base{
|
|
Exchange: h.Name,
|
|
Pair: p,
|
|
Asset: assetType,
|
|
VerifyOrderbook: h.CanVerifyOrderbook,
|
|
}
|
|
var err error
|
|
switch assetType {
|
|
case asset.Spot:
|
|
var orderbookNew *Orderbook
|
|
orderbookNew, err = h.GetDepth(ctx,
|
|
&OrderBookDataRequestParams{
|
|
Symbol: p,
|
|
Type: OrderBookDataRequestParamsTypeStep0,
|
|
})
|
|
if err != nil {
|
|
return book, err
|
|
}
|
|
|
|
book.Bids = make(orderbook.Items, len(orderbookNew.Bids))
|
|
for x := range orderbookNew.Bids {
|
|
book.Bids[x] = orderbook.Item{
|
|
Amount: orderbookNew.Bids[x][1],
|
|
Price: orderbookNew.Bids[x][0],
|
|
}
|
|
}
|
|
book.Asks = make(orderbook.Items, len(orderbookNew.Asks))
|
|
for x := range orderbookNew.Asks {
|
|
book.Asks[x] = orderbook.Item{
|
|
Amount: orderbookNew.Asks[x][1],
|
|
Price: orderbookNew.Asks[x][0],
|
|
}
|
|
}
|
|
|
|
case asset.Futures:
|
|
var orderbookNew *OBData
|
|
orderbookNew, err = h.FGetMarketDepth(ctx, p, "step0")
|
|
if err != nil {
|
|
return book, err
|
|
}
|
|
|
|
book.Asks = make(orderbook.Items, len(orderbookNew.Asks))
|
|
for x := range orderbookNew.Asks {
|
|
book.Asks[x] = orderbook.Item{
|
|
Amount: orderbookNew.Asks[x].Quantity,
|
|
Price: orderbookNew.Asks[x].Price,
|
|
}
|
|
}
|
|
book.Bids = make(orderbook.Items, len(orderbookNew.Bids))
|
|
for y := range orderbookNew.Bids {
|
|
book.Bids[y] = orderbook.Item{
|
|
Amount: orderbookNew.Bids[y].Quantity,
|
|
Price: orderbookNew.Bids[y].Price,
|
|
}
|
|
}
|
|
|
|
case asset.CoinMarginedFutures:
|
|
var orderbookNew SwapMarketDepthData
|
|
orderbookNew, err = h.GetSwapMarketDepth(ctx, p, "step0")
|
|
if err != nil {
|
|
return book, err
|
|
}
|
|
|
|
book.Asks = make(orderbook.Items, len(orderbookNew.Tick.Asks))
|
|
for x := range orderbookNew.Tick.Asks {
|
|
book.Asks[x] = orderbook.Item{
|
|
Amount: orderbookNew.Tick.Asks[x][1],
|
|
Price: orderbookNew.Tick.Asks[x][0],
|
|
}
|
|
}
|
|
|
|
book.Bids = make(orderbook.Items, len(orderbookNew.Tick.Bids))
|
|
for y := range orderbookNew.Tick.Bids {
|
|
book.Bids[y] = orderbook.Item{
|
|
Amount: orderbookNew.Tick.Bids[y][1],
|
|
Price: orderbookNew.Tick.Bids[y][0],
|
|
}
|
|
}
|
|
}
|
|
err = book.Process()
|
|
if err != nil {
|
|
return book, err
|
|
}
|
|
return orderbook.Get(h.Name, p, assetType)
|
|
}
|
|
|
|
// GetAccountID returns the account ID for trades
|
|
func (h *HUOBI) GetAccountID(ctx context.Context) ([]Account, error) {
|
|
acc, err := h.GetAccounts(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(acc) < 1 {
|
|
return nil, errors.New("no account returned")
|
|
}
|
|
|
|
return acc, nil
|
|
}
|
|
|
|
// UpdateAccountInfo retrieves balances for all enabled currencies for the
|
|
// HUOBI exchange - to-do
|
|
func (h *HUOBI) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
|
|
var info account.Holdings
|
|
var acc account.SubAccount
|
|
info.Exchange = h.Name
|
|
switch assetType {
|
|
case asset.Spot:
|
|
if h.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
|
|
resp, err := h.wsGetAccountsList(ctx)
|
|
if err != nil {
|
|
return info, err
|
|
}
|
|
var currencyDetails []account.Balance
|
|
for i := range resp.Data {
|
|
if len(resp.Data[i].List) == 0 {
|
|
continue
|
|
}
|
|
currData := account.Balance{
|
|
Currency: currency.NewCode(resp.Data[i].List[0].Currency),
|
|
Total: resp.Data[i].List[0].Balance,
|
|
}
|
|
if len(resp.Data[i].List) > 1 && resp.Data[i].List[1].Type == "frozen" {
|
|
currData.Hold = resp.Data[i].List[1].Balance
|
|
}
|
|
currencyDetails = append(currencyDetails, currData)
|
|
}
|
|
acc.Currencies = currencyDetails
|
|
} else {
|
|
accounts, err := h.GetAccountID(ctx)
|
|
if err != nil {
|
|
return info, err
|
|
}
|
|
for i := range accounts {
|
|
if accounts[i].Type != "spot" {
|
|
continue
|
|
}
|
|
acc.ID = strconv.FormatInt(accounts[i].ID, 10)
|
|
balances, err := h.GetAccountBalance(ctx, acc.ID)
|
|
if err != nil {
|
|
return info, err
|
|
}
|
|
|
|
var currencyDetails []account.Balance
|
|
balance:
|
|
for j := range balances {
|
|
frozen := balances[j].Type == "frozen"
|
|
for i := range currencyDetails {
|
|
if currencyDetails[i].Currency.String() == balances[j].Currency {
|
|
if frozen {
|
|
currencyDetails[i].Hold = balances[j].Balance
|
|
} else {
|
|
currencyDetails[i].Total = balances[j].Balance
|
|
}
|
|
continue balance
|
|
}
|
|
}
|
|
|
|
if frozen {
|
|
currencyDetails = append(currencyDetails,
|
|
account.Balance{
|
|
Currency: currency.NewCode(balances[j].Currency),
|
|
Hold: balances[j].Balance,
|
|
})
|
|
} else {
|
|
currencyDetails = append(currencyDetails,
|
|
account.Balance{
|
|
Currency: currency.NewCode(balances[j].Currency),
|
|
Total: balances[j].Balance,
|
|
})
|
|
}
|
|
}
|
|
acc.Currencies = currencyDetails
|
|
}
|
|
}
|
|
|
|
case asset.CoinMarginedFutures:
|
|
// fetch swap account info
|
|
acctInfo, err := h.GetSwapAccountInfo(ctx, currency.EMPTYPAIR)
|
|
if err != nil {
|
|
return info, err
|
|
}
|
|
|
|
var mainAcctBalances []account.Balance
|
|
for x := range acctInfo.Data {
|
|
mainAcctBalances = append(mainAcctBalances, account.Balance{
|
|
Currency: currency.NewCode(acctInfo.Data[x].Symbol),
|
|
Total: acctInfo.Data[x].MarginBalance,
|
|
Hold: acctInfo.Data[x].MarginFrozen,
|
|
Free: acctInfo.Data[x].MarginAvailable,
|
|
})
|
|
}
|
|
|
|
info.Accounts = append(info.Accounts, account.SubAccount{
|
|
Currencies: mainAcctBalances,
|
|
AssetType: assetType,
|
|
})
|
|
|
|
// fetch subaccounts data
|
|
subAccsData, err := h.GetSwapAllSubAccAssets(ctx, currency.EMPTYPAIR)
|
|
if err != nil {
|
|
return info, err
|
|
}
|
|
var currencyDetails []account.Balance
|
|
for x := range subAccsData.Data {
|
|
a, err := h.SwapSingleSubAccAssets(ctx,
|
|
currency.EMPTYPAIR,
|
|
subAccsData.Data[x].SubUID)
|
|
if err != nil {
|
|
return info, err
|
|
}
|
|
for y := range a.Data {
|
|
currencyDetails = append(currencyDetails, account.Balance{
|
|
Currency: currency.NewCode(a.Data[y].Symbol),
|
|
Total: a.Data[y].MarginBalance,
|
|
Hold: a.Data[y].MarginFrozen,
|
|
Free: a.Data[y].MarginAvailable,
|
|
})
|
|
}
|
|
}
|
|
acc.Currencies = currencyDetails
|
|
case asset.Futures:
|
|
// fetch main account data
|
|
mainAcctData, err := h.FGetAccountInfo(ctx, currency.EMPTYCODE)
|
|
if err != nil {
|
|
return info, err
|
|
}
|
|
|
|
var mainAcctBalances []account.Balance
|
|
for x := range mainAcctData.AccData {
|
|
mainAcctBalances = append(mainAcctBalances, account.Balance{
|
|
Currency: currency.NewCode(mainAcctData.AccData[x].Symbol),
|
|
Total: mainAcctData.AccData[x].MarginBalance,
|
|
Hold: mainAcctData.AccData[x].MarginFrozen,
|
|
Free: mainAcctData.AccData[x].MarginAvailable,
|
|
})
|
|
}
|
|
|
|
info.Accounts = append(info.Accounts, account.SubAccount{
|
|
Currencies: mainAcctBalances,
|
|
AssetType: assetType,
|
|
})
|
|
|
|
// fetch subaccounts data
|
|
subAccsData, err := h.FGetAllSubAccountAssets(ctx, currency.EMPTYCODE)
|
|
if err != nil {
|
|
return info, err
|
|
}
|
|
var currencyDetails []account.Balance
|
|
for x := range subAccsData.Data {
|
|
a, err := h.FGetSingleSubAccountInfo(ctx,
|
|
"",
|
|
strconv.FormatInt(subAccsData.Data[x].SubUID, 10))
|
|
if err != nil {
|
|
return info, err
|
|
}
|
|
for y := range a.AssetsData {
|
|
currencyDetails = append(currencyDetails, account.Balance{
|
|
Currency: currency.NewCode(a.AssetsData[y].Symbol),
|
|
Total: a.AssetsData[y].MarginBalance,
|
|
Hold: a.AssetsData[y].MarginFrozen,
|
|
Free: a.AssetsData[y].MarginAvailable,
|
|
})
|
|
}
|
|
}
|
|
acc.Currencies = currencyDetails
|
|
}
|
|
acc.AssetType = assetType
|
|
info.Accounts = append(info.Accounts, acc)
|
|
creds, err := h.GetCredentials(ctx)
|
|
if err != nil {
|
|
return account.Holdings{}, err
|
|
}
|
|
if err := account.Process(&info, creds); err != nil {
|
|
return info, err
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
// FetchAccountInfo retrieves balances for all enabled currencies
|
|
func (h *HUOBI) FetchAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
|
|
creds, err := h.GetCredentials(ctx)
|
|
if err != nil {
|
|
return account.Holdings{}, err
|
|
}
|
|
acc, err := account.GetHoldings(h.Name, creds, assetType)
|
|
if err != nil {
|
|
return h.UpdateAccountInfo(ctx, assetType)
|
|
}
|
|
return acc, nil
|
|
}
|
|
|
|
// GetAccountFundingHistory returns funding history, deposits and
|
|
// withdrawals
|
|
func (h *HUOBI) GetAccountFundingHistory(_ context.Context) ([]exchange.FundingHistory, error) {
|
|
return nil, common.ErrFunctionNotSupported
|
|
}
|
|
|
|
// GetWithdrawalsHistory returns previous withdrawals data
|
|
func (h *HUOBI) GetWithdrawalsHistory(ctx context.Context, c currency.Code, a asset.Item) ([]exchange.WithdrawalHistory, error) {
|
|
if a != asset.Spot {
|
|
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a)
|
|
}
|
|
withdrawals, err := h.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: time.UnixMilli(withdrawals.Data[i].CreatedAt),
|
|
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 (h *HUOBI) GetRecentTrades(ctx context.Context, p currency.Pair, a asset.Item) ([]trade.Data, error) {
|
|
var resp []trade.Data
|
|
pFmt, err := h.GetPairFormat(a, true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
p = p.Format(pFmt)
|
|
switch a {
|
|
case asset.Spot:
|
|
var sTrades []TradeHistory
|
|
sTrades, err = h.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: h.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: time.UnixMilli(sTrades[i].Timestamp),
|
|
})
|
|
}
|
|
}
|
|
case asset.Futures:
|
|
var fTrades FBatchTradesForContractData
|
|
fTrades, err = h.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: h.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: time.UnixMilli(fTrades.Data[i].Data[j].Timestamp),
|
|
})
|
|
}
|
|
}
|
|
case asset.CoinMarginedFutures:
|
|
var cTrades BatchTradesData
|
|
cTrades, err = h.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: h.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: time.UnixMilli(cTrades.Data[i].Timestamp),
|
|
})
|
|
}
|
|
}
|
|
|
|
err = h.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 (h *HUOBI) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.Item, _, _ time.Time) ([]trade.Data, error) {
|
|
return nil, common.ErrFunctionNotSupported
|
|
}
|
|
|
|
// SubmitOrder submits a new order
|
|
func (h *HUOBI) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) {
|
|
if err := s.Validate(); 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
|
|
var 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 := h.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.Limit:
|
|
oType = "limit"
|
|
case order.PostOnly:
|
|
oType = "post_only"
|
|
}
|
|
offset := "open"
|
|
if s.ReduceOnly {
|
|
offset = "close"
|
|
}
|
|
orderResp, err := h.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 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 filled in 100%.
|
|
// The system will obtain the optimal N price at that moment and place the order.
|
|
oType = "optimal_20"
|
|
if s.ImmediateOrCancel {
|
|
oType = "optimal_20_ioc"
|
|
}
|
|
case order.Limit:
|
|
oType = "limit"
|
|
case order.PostOnly:
|
|
oType = "post_only"
|
|
}
|
|
offset := "open"
|
|
if s.ReduceOnly {
|
|
offset = "close"
|
|
}
|
|
o, err := h.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 will allow of changing orderbook placement and limit to
|
|
// market conversion
|
|
func (h *HUOBI) ModifyOrder(_ context.Context, _ *order.Modify) (*order.ModifyResponse, error) {
|
|
return nil, common.ErrFunctionNotSupported
|
|
}
|
|
|
|
// CancelOrder cancels an order by its corresponding ID number
|
|
func (h *HUOBI) 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 = h.CancelExistingOrder(ctx, orderIDInt)
|
|
case asset.CoinMarginedFutures:
|
|
_, err = h.CancelSwapOrder(ctx, o.OrderID, o.ClientID, o.Pair)
|
|
case asset.Futures:
|
|
_, err = h.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 (h *HUOBI) 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 := h.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 (h *HUOBI) 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 := h.GetEnabledPairs(asset.Spot)
|
|
if err != nil {
|
|
return cancelAllOrdersResponse, err
|
|
}
|
|
for i := range enabledPairs {
|
|
resp, err := h.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 := h.GetEnabledPairs(asset.CoinMarginedFutures)
|
|
if err != nil {
|
|
return cancelAllOrdersResponse, err
|
|
}
|
|
for i := range enabledPairs {
|
|
a, err := h.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 := h.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 := h.GetEnabledPairs(asset.Futures)
|
|
if err != nil {
|
|
return cancelAllOrdersResponse, err
|
|
}
|
|
for i := range enabledPairs {
|
|
a, err := h.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 := h.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 (h *HUOBI) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, assetType asset.Item) (*order.Detail, error) {
|
|
if pair.IsEmpty() {
|
|
return nil, currency.ErrCurrencyPairEmpty
|
|
}
|
|
if err := h.CurrencyPairs.IsAssetEnabled(assetType); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var orderDetail order.Detail
|
|
switch assetType {
|
|
case asset.Spot:
|
|
var respData *OrderInfo
|
|
if h.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
|
|
resp, err := h.wsGetOrderDetails(ctx, orderID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
respData = &resp.Data
|
|
} else {
|
|
oID, err := strconv.ParseInt(orderID, 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := h.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", h.Name, orderID)
|
|
}
|
|
var responseID = strconv.FormatInt(respData.ID, 10)
|
|
if responseID != orderID {
|
|
return nil, errors.New(h.Name + " - GetOrderInfo orderID mismatch. Expected: " +
|
|
orderID + " Received: " + responseID)
|
|
}
|
|
typeDetails := strings.Split(respData.Type, "-")
|
|
orderSide, err := order.StringToOrderSide(typeDetails[0])
|
|
if err != nil {
|
|
if h.Websocket.IsConnected() {
|
|
h.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: h.Name,
|
|
OrderID: orderID,
|
|
Err: err,
|
|
}
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
orderType, err := order.StringToOrderType(typeDetails[1])
|
|
if err != nil {
|
|
if h.Websocket.IsConnected() {
|
|
h.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: h.Name,
|
|
OrderID: orderID,
|
|
Err: err,
|
|
}
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
orderStatus, err := order.StringToOrderStatus(respData.State)
|
|
if err != nil {
|
|
if h.Websocket.IsConnected() {
|
|
h.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: h.Name,
|
|
OrderID: orderID,
|
|
Err: err,
|
|
}
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
var p currency.Pair
|
|
var a asset.Item
|
|
p, a, err = h.GetRequestFormattedPairAndAssetType(respData.Symbol)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
orderDetail = order.Detail{
|
|
Exchange: h.Name,
|
|
OrderID: orderID,
|
|
AccountID: strconv.FormatInt(respData.AccountID, 10),
|
|
Pair: p,
|
|
Type: orderType,
|
|
Side: orderSide,
|
|
Date: time.UnixMilli(respData.CreatedAt),
|
|
Status: orderStatus,
|
|
Price: respData.Price,
|
|
Amount: respData.Amount,
|
|
ExecutedAmount: respData.FilledAmount,
|
|
Fee: respData.FilledFees,
|
|
AssetType: a,
|
|
}
|
|
case asset.CoinMarginedFutures:
|
|
orderInfo, err := h.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 := true
|
|
if orderVars.OrderType == order.Limit || orderVars.OrderType == order.PostOnly {
|
|
maker = false
|
|
}
|
|
orderDetail.Trades = append(orderDetail.Trades, order.TradeHistory{
|
|
Price: orderInfo.Data[x].Price,
|
|
Amount: orderInfo.Data[x].Volume,
|
|
Fee: orderInfo.Data[x].Fee,
|
|
Exchange: h.Name,
|
|
TID: orderInfo.Data[x].OrderIDString,
|
|
Type: orderVars.OrderType,
|
|
Side: orderVars.Side,
|
|
IsMaker: maker,
|
|
})
|
|
}
|
|
case asset.Futures:
|
|
fPair, err := h.FormatSymbol(pair, asset.Futures)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
orderInfo, err := h.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: h.Name,
|
|
TID: orderInfo.Data[x].OrderIDString,
|
|
Type: orderVars.OrderType,
|
|
Side: orderVars.Side,
|
|
IsMaker: orderVars.OrderType == order.Limit || orderVars.OrderType == 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 (h *HUOBI) GetDepositAddress(ctx context.Context, cryptocurrency currency.Code, _, chain string) (*deposit.Address, error) {
|
|
resp, err := h.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 (h *HUOBI) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) {
|
|
if err := withdrawRequest.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := h.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 (h *HUOBI) WithdrawFiatFunds(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) {
|
|
return nil, common.ErrFunctionNotSupported
|
|
}
|
|
|
|
// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a
|
|
// withdrawal is submitted
|
|
func (h *HUOBI) WithdrawFiatFundsToInternationalBank(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) {
|
|
return nil, common.ErrFunctionNotSupported
|
|
}
|
|
|
|
// GetFeeByType returns an estimate of fee based on type of transaction
|
|
func (h *HUOBI) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
|
|
if feeBuilder == nil {
|
|
return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer)
|
|
}
|
|
if !h.AreCredentialsValid(ctx) && // Todo check connection status
|
|
feeBuilder.FeeType == exchange.CryptocurrencyTradeFee {
|
|
feeBuilder.FeeType = exchange.OfflineTradeFee
|
|
}
|
|
return h.GetFee(feeBuilder)
|
|
}
|
|
|
|
// GetActiveOrders retrieves any orders that are active/open
|
|
func (h *HUOBI) 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()
|
|
}
|
|
if h.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
|
|
for i := range req.Pairs {
|
|
resp, err := h.wsGetOrdersList(ctx, -1, req.Pairs[i])
|
|
if err != nil {
|
|
return orders, err
|
|
}
|
|
for j := range resp.Data {
|
|
sideData := strings.Split(resp.Data[j].OrderState, "-")
|
|
side = sideData[0]
|
|
var orderID = strconv.FormatInt(resp.Data[j].OrderID, 10)
|
|
orderSide, err := order.StringToOrderSide(side)
|
|
if err != nil {
|
|
h.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: h.Name,
|
|
OrderID: orderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
orderType, err := order.StringToOrderType(sideData[1])
|
|
if err != nil {
|
|
h.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: h.Name,
|
|
OrderID: orderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
orderStatus, err := order.StringToOrderStatus(resp.Data[j].OrderState)
|
|
if err != nil {
|
|
h.Websocket.DataHandler <- order.ClassificationError{
|
|
Exchange: h.Name,
|
|
OrderID: orderID,
|
|
Err: err,
|
|
}
|
|
}
|
|
orders = append(orders, order.Detail{
|
|
Exchange: h.Name,
|
|
AccountID: strconv.FormatInt(resp.Data[j].AccountID, 10),
|
|
OrderID: orderID,
|
|
Pair: req.Pairs[i],
|
|
Type: orderType,
|
|
Side: orderSide,
|
|
Date: time.UnixMilli(resp.Data[j].CreatedAt),
|
|
Status: orderStatus,
|
|
Price: resp.Data[j].Price,
|
|
Amount: resp.Data[j].OrderAmount,
|
|
ExecutedAmount: resp.Data[j].FilledAmount,
|
|
RemainingAmount: resp.Data[j].UnfilledAmount,
|
|
Fee: resp.Data[j].FilledFees,
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
creds, err := h.GetCredentials(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range req.Pairs {
|
|
resp, err := h.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: h.Name,
|
|
Date: time.UnixMilli(resp[x].CreatedAt),
|
|
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 := h.GetSwapOpenOrders(ctx,
|
|
req.Pairs[x], 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{
|
|
PostOnly: orderVars.OrderType == order.PostOnly,
|
|
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: h.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 := h.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{
|
|
PostOnly: orderVars.OrderType == order.PostOnly,
|
|
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: h.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(h.Name, orders), nil
|
|
}
|
|
|
|
// GetOrderHistory retrieves account order information
|
|
// Can Limit response to specific order status
|
|
func (h *HUOBI) 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 := h.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: h.Name,
|
|
Date: time.UnixMilli(resp[x].CreatedAt),
|
|
CloseTime: time.UnixMilli(resp[x].FinishedAt),
|
|
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 := h.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{
|
|
PostOnly: orderVars.OrderType == order.PostOnly,
|
|
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: h.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 := h.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
|
|
}
|
|
orderCreateTime := time.Unix(openOrders.Data.Orders[x].CreateDate, 0)
|
|
|
|
p, err := currency.NewPairFromString(openOrders.Data.Orders[x].ContractCode)
|
|
if err != nil {
|
|
return orders, err
|
|
}
|
|
orders = append(orders, order.Detail{
|
|
PostOnly: orderVars.OrderType == order.PostOnly,
|
|
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: h.Name,
|
|
AssetType: req.AssetType,
|
|
OrderID: openOrders.Data.Orders[x].OrderIDString,
|
|
Side: orderVars.Side,
|
|
Type: orderVars.OrderType,
|
|
Status: orderVars.Status,
|
|
Pair: p,
|
|
Date: orderCreateTime,
|
|
})
|
|
}
|
|
currentPage++
|
|
done = currentPage == openOrders.Data.TotalPage
|
|
}
|
|
}
|
|
}
|
|
return req.Filter(h.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 (h *HUOBI) AuthenticateWebsocket(ctx context.Context) error {
|
|
return h.wsLogin(ctx)
|
|
}
|
|
|
|
// ValidateAPICredentials validates current credentials used for wrapper
|
|
// functionality
|
|
func (h *HUOBI) ValidateAPICredentials(ctx context.Context, assetType asset.Item) error {
|
|
_, err := h.UpdateAccountInfo(ctx, assetType)
|
|
return h.CheckTransientError(err)
|
|
}
|
|
|
|
// FormatExchangeKlineInterval returns Interval to exchange formatted string
|
|
func (h *HUOBI) 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 (h *HUOBI) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
|
|
req, err := h.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 := h.GetSpotKline(ctx, KlinesRequestParams{
|
|
Period: h.FormatExchangeKlineInterval(req.ExchangeInterval),
|
|
Symbol: req.Pair,
|
|
Size: int(req.RequestLimit),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for x := range candles {
|
|
timestamp := time.Unix(candles[x].IDTimestamp, 0)
|
|
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 := h.FGetKlineData(ctx, req.Pair, h.FormatExchangeKlineInterval(req.ExchangeInterval), size, req.Start, req.End)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for x := range candles.Data {
|
|
timestamp := time.Unix(candles.Data[x].IDTimestamp, 0)
|
|
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 := h.GetSwapKlineData(ctx, req.Pair, h.FormatExchangeKlineInterval(req.ExchangeInterval), size, req.Start, req.End)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for x := range candles.Data {
|
|
timestamp := time.Unix(candles.Data[x].IDTimestamp, 0)
|
|
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 (h *HUOBI) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
|
|
req, err := h.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 = h.FGetKlineData(ctx, req.Pair, h.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 := time.Unix(candles.Data[x].IDTimestamp, 0).UTC()
|
|
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 = h.GetSwapKlineData(ctx, req.Pair, h.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 := time.Unix(candles.Data[x].IDTimestamp, 0)
|
|
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.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 (h *HUOBI) GetAvailableTransferChains(ctx context.Context, cryptocurrency currency.Code) ([]string, error) {
|
|
chains, err := h.GetCurrenciesIncludingChains(ctx, cryptocurrency)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(chains) == 0 {
|
|
return nil, errors.New("chain data isn't populated")
|
|
}
|
|
|
|
availableChains := make([]string, len(chains[0].ChainData))
|
|
for x := range chains[0].ChainData {
|
|
availableChains[x] = chains[0].ChainData[x].Chain
|
|
}
|
|
return availableChains, nil
|
|
}
|
|
|
|
// GetServerTime returns the current exchange server time.
|
|
func (h *HUOBI) GetServerTime(ctx context.Context, _ asset.Item) (time.Time, error) {
|
|
return h.GetCurrentServerTime(ctx)
|
|
}
|
|
|
|
// GetFuturesContractDetails returns details about futures contracts
|
|
func (h *HUOBI) GetFuturesContractDetails(ctx context.Context, item asset.Item) ([]futures.Contract, error) {
|
|
if !item.IsFutures() {
|
|
return nil, futures.ErrNotFuturesAsset
|
|
}
|
|
if !h.SupportsAsset(item) {
|
|
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, item)
|
|
}
|
|
|
|
switch item {
|
|
case asset.CoinMarginedFutures:
|
|
result, err := h.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: h.Name,
|
|
Name: cp,
|
|
Underlying: underlying,
|
|
Asset: item,
|
|
StartDate: s,
|
|
SettlementType: futures.Inverse,
|
|
IsActive: result[x].ContractStatus == 1,
|
|
Type: futures.Perpetual,
|
|
SettlementCurrencies: currency.Currencies{currency.USD},
|
|
Multiplier: result[x].ContractSize,
|
|
})
|
|
}
|
|
return resp, nil
|
|
case asset.Futures:
|
|
result, err := h.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 s, e time.Time
|
|
s, err = time.Parse("20060102", result.Data[x].CreateDate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if result.Data[x].DeliveryTime > 0 {
|
|
e = time.UnixMilli(result.Data[x].DeliveryTime)
|
|
} else {
|
|
e = time.UnixMilli(result.Data[x].SettlementTime)
|
|
}
|
|
contractLength := e.Sub(s)
|
|
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: h.Name,
|
|
Name: cp,
|
|
Underlying: underlying,
|
|
Asset: item,
|
|
StartDate: s,
|
|
EndDate: e,
|
|
SettlementType: futures.Linear,
|
|
IsActive: result.Data[x].ContractStatus == 1,
|
|
Type: ct,
|
|
SettlementCurrencies: currency.Currencies{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 (h *HUOBI) 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 := h.GetSwapFundingRates(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rates = batchRates.Data
|
|
} else {
|
|
rateResp, err := h.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 := h.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 = h.IsPerpetualFutureCurrency(r.Asset, cp)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !isPerp {
|
|
continue
|
|
}
|
|
var ft, nft time.Time
|
|
nft = time.UnixMilli(rates[i].NextFundingTime)
|
|
ft = time.UnixMilli(rates[i].FundingTime)
|
|
var fri time.Duration
|
|
if len(h.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies) == 1 {
|
|
// can infer funding rate interval from the only funding rate frequency defined
|
|
for k := range h.Features.Supports.FuturesCapabilities.SupportedFundingRateFrequencies {
|
|
fri = k.Duration()
|
|
}
|
|
}
|
|
if rates[i].FundingTime == 0 {
|
|
ft = nft.Add(-fri)
|
|
}
|
|
if ft.After(time.Now()) {
|
|
ft = ft.Add(-fri)
|
|
nft = nft.Add(-fri)
|
|
}
|
|
rate := fundingrate.LatestRateResponse{
|
|
Exchange: h.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 (h *HUOBI) IsPerpetualFutureCurrency(a asset.Item, _ currency.Pair) (bool, error) {
|
|
return a == asset.CoinMarginedFutures, nil
|
|
}
|
|
|
|
// UpdateOrderExecutionLimits updates order execution limits
|
|
func (h *HUOBI) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error {
|
|
return common.ErrNotYetImplemented
|
|
}
|
|
|
|
// GetOpenInterest returns the open interest rate for a given asset pair
|
|
func (h *HUOBI) 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:
|
|
_, err := strconv.ParseInt(k[0].Quote.Symbol, 10, 64)
|
|
if err == nil {
|
|
// 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 := h.FContractOpenInterest(ctx, "", "", k[0].Pair())
|
|
if err != nil {
|
|
data2, err2 := h.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 = h.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.ExchangePairAsset{
|
|
Exchange: h.Name,
|
|
Base: p.Base.Item,
|
|
Quote: p.Quote.Item,
|
|
Asset: k[0].Asset,
|
|
},
|
|
OpenInterest: data.Data[i].Amount,
|
|
},
|
|
}, nil
|
|
}
|
|
case asset.CoinMarginedFutures:
|
|
data, err := h.SwapOpenInterestInformation(ctx, k[0].Pair())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range data.Data {
|
|
var p currency.Pair
|
|
p, err = h.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.ExchangePairAsset{
|
|
Exchange: h.Name,
|
|
Base: p.Base.Item,
|
|
Quote: p.Quote.Item,
|
|
Asset: k[0].Asset,
|
|
},
|
|
OpenInterest: data.Data[i].Amount,
|
|
},
|
|
}, nil
|
|
}
|
|
}
|
|
}
|
|
var resp []futures.OpenInterest
|
|
for _, a := range h.GetAssetTypes(true) {
|
|
switch a {
|
|
case asset.Futures:
|
|
data, err := h.FContractOpenInterest(ctx, "", "", currency.EMPTYPAIR)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
uData, err := h.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 = h.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.ExchangePairAsset{
|
|
Exchange: h.Name,
|
|
Base: p.Base.Item,
|
|
Quote: p.Quote.Item,
|
|
Asset: a,
|
|
},
|
|
OpenInterest: allData[i].Amount,
|
|
})
|
|
}
|
|
case asset.CoinMarginedFutures:
|
|
data, err := h.SwapOpenInterestInformation(ctx, currency.EMPTYPAIR)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for i := range data.Data {
|
|
p, isEnabled, err := h.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.ExchangePairAsset{
|
|
Exchange: h.Name,
|
|
Base: p.Base.Item,
|
|
Quote: p.Quote.Item,
|
|
Asset: a,
|
|
},
|
|
OpenInterest: data.Data[i].Amount,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return resp, nil
|
|
}
|