diff --git a/exchanges/bitget/bitget.go b/exchanges/bitget/bitget.go index 7455a52d98d..d1fdacbf53a 100644 --- a/exchanges/bitget/bitget.go +++ b/exchanges/bitget/bitget.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "strconv" + "strings" "time" "github.com/thrasher-corp/gocryptotrader/common" @@ -271,6 +272,7 @@ var ( errUnknownPairQuote = errors.New("unknown pair quote; pair can't be split due to lack of delimiter and unclear base length") errStrategyMutex = errors.New("only one of immediate or cancel, fill or kill, and post only can be set to true") errOrderNotFound = errors.New("order not found") + errReturnEmpty = errors.New("returned data unexpectedly empty") prodTypes = []string{"USDT-FUTURES", "COIN-FUTURES", "USDC-FUTURES"} planTypes = []string{"normal_plan", "track_plan", "profit_loss"} @@ -1999,9 +2001,6 @@ func (bi *Bitget) GetFundingCurrent(ctx context.Context, pair, productType strin // GetContractConfig returns details for a given contract func (bi *Bitget) GetContractConfig(ctx context.Context, pair, productType string) (*ContractConfigResp, error) { - if pair == "" { - return nil, errPairEmpty - } if productType == "" { return nil, errProductTypeEmpty } @@ -4494,8 +4493,13 @@ func (bi *Bitget) SendAuthenticatedHTTPRequest(ctx context.Context, ep exchange. return nil, err } } + // $ gets escaped in URLs, but the exchange reverses this before checking the signature; if we don't + // reverse it ourselves, they'll consider it invalid. This technically applies to other escape characters + // too, but $ is one we need to worry about, since it's included in some currencies supported by the + // exchange + unescapedPath := strings.ReplaceAll(path, "%24", "$") t := strconv.FormatInt(time.Now().UnixMilli(), 10) - message := t + method + "/api/v2/" + path + string(payload) + message := t + method + "/api/v2/" + unescapedPath + string(payload) // The exchange also supports user-generated RSA keys, but we haven't implemented that yet var hmac []byte hmac, err = crypto.GetHMAC(crypto.HashSHA256, []byte(message), []byte(creds.Secret)) diff --git a/exchanges/bitget/bitget_test.go b/exchanges/bitget/bitget_test.go index b995ab1a940..ad674e169e7 100644 --- a/exchanges/bitget/bitget_test.go +++ b/exchanges/bitget/bitget_test.go @@ -17,6 +17,8 @@ import ( "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "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/sharedtestvalues" @@ -1061,7 +1063,10 @@ func TestGetFundingCurrent(t *testing.T) { func TestGetContractConfig(t *testing.T) { t.Parallel() - testGetTwoArgs(t, bi.GetContractConfig) + _, err := bi.GetContractConfig(context.Background(), "", "") + assert.ErrorIs(t, err, errProductTypeEmpty) + _, err = bi.GetContractConfig(context.Background(), "", prodTypes[0]) + assert.NoError(t, err) } func TestGetOneFuturesAccount(t *testing.T) { @@ -2657,7 +2662,6 @@ func TestGetActiveOrders(t *testing.T) { assert.NoError(t, err) req.AssetType = asset.Spot _, err = bi.GetActiveOrders(context.Background(), req) - // This is failing since the String() method on these novel pairs returns them with a delimiter for some reason assert.NoError(t, err) req.Pairs = []currency.Pair{testPair} _, err = bi.GetActiveOrders(context.Background(), req) @@ -2727,20 +2731,12 @@ func TestGetHistoricCandles(t *testing.T) { t.Parallel() _, err := bi.GetHistoricCandles(context.Background(), currency.Pair{}, asset.Spot, kline.Raw, time.Time{}, time.Time{}) assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) - _, err = bi.GetHistoricCandles(context.Background(), testPair, asset.Spot, kline.OneDay, time.Time{}, time.Time{}) + _, err = bi.GetHistoricCandles(context.Background(), testPair, asset.Spot, kline.OneDay, + time.Now().Add(-time.Hour*24*20), time.Now()) assert.NoError(t, err) - _, err = bi.GetHistoricCandles(context.Background(), testPair, asset.Futures, kline.OneDay, time.Time{}, time.Time{}) + _, err = bi.GetHistoricCandles(context.Background(), testPair, asset.Futures, kline.OneDay, + time.Now().Add(-time.Hour*24*20), time.Now()) assert.NoError(t, err) - - // _, err = bi.GetHistoricCandles(context.Background(), testPair, asset.Binary, kline.OneMin, time.Now().Add(-time.Hour), - // time.Now()) - // assert.ErrorIs(t, err, asset.ErrNotSupported) - // _, err = bi.GetHistoricCandles(context.Background(), testPair, asset.Spot, kline.OneMin, time.Now().Add(-time.Hour), - // time.Now()) - // assert.NoError(t, err) - // _, err = bi.GetHistoricCandles(context.Background(), testPair, asset.Futures, kline.OneMin, time.Now().Add(-time.Hour), - // time.Now()) - // assert.NoError(t, err) } func TestGetHistoricCandlesExtended(t *testing.T) { @@ -2748,7 +2744,46 @@ func TestGetHistoricCandlesExtended(t *testing.T) { _, err := bi.GetHistoricCandlesExtended(context.Background(), currency.Pair{}, asset.Spot, kline.Raw, time.Time{}, time.Time{}) assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) - // Rest of this is being put on ice until the issue with the previous test has been figured out + _, err = bi.GetHistoricCandlesExtended(context.Background(), testPair, asset.Spot, kline.OneDay, + time.Now().Add(-time.Hour*24*20), time.Now()) + assert.NoError(t, err) + _, err = bi.GetHistoricCandlesExtended(context.Background(), testPair, asset.Futures, kline.OneDay, + time.Now().Add(-time.Hour*24*20), time.Now()) + assert.NoError(t, err) +} + +func TestGetFuturesContractDetails(t *testing.T) { + t.Parallel() + testGetOneArg(t, bi.GetFuturesContractDetails, asset.Empty, asset.Empty, nil, false, false, true) +} + +func TestGetLatestFundingRates(t *testing.T) { + t.Parallel() + req1 := new(fundingrate.LatestRateRequest) + req1.Pair = currency.Pair{} + req2 := new(fundingrate.LatestRateRequest) + req2.Pair = testPair + testGetOneArg(t, bi.GetLatestFundingRates, req1, req2, errPairEmpty, false, false, true) +} + +func TestUpdateOrderExecutionLimits(t *testing.T) { + t.Parallel() + err := bi.UpdateOrderExecutionLimits(context.Background(), asset.Empty) + assert.ErrorIs(t, err, asset.ErrNotSupported) + err = bi.UpdateOrderExecutionLimits(context.Background(), asset.Spot) + assert.NoError(t, err) + err = bi.UpdateOrderExecutionLimits(context.Background(), asset.Futures) + assert.NoError(t, err) + err = bi.UpdateOrderExecutionLimits(context.Background(), asset.Margin) + assert.NoError(t, err) +} + +func TestGetAvailableTransferChains(t *testing.T) { + t.Parallel() + testGetOneArg(t, bi.GetAvailableTransferChains, currency.EMPTYCODE, testCrypto, errCurrencyEmpty, false, false, true) + _, err := bi.GetAvailableTransferChains(context.Background(), currency.NewCode("fakecurrencynotrealmeowmeow")) + assert.NoError(t, err) + // See if there's an established fake currency you can use instead of reinventing this one } // The following 3 tests aren't parallel due to collisions with each other, and some other plan order-related tests @@ -3126,11 +3161,12 @@ type getOneArgResp interface { *SubOrderResp | *BatchOrderResp | *BoolData | *FutureTickerResp | *AllAccResp | *SubaccountFuturesResp | *CrossAssetResp | *MaxBorrowCross | *MaxTransferCross | *IntRateMaxBorrowCross | *TierConfigCross | *FlashRepayCross | *IsoAssetResp | *IntRateMaxBorrowIso | *MaxBorrowIso | *MaxTransferIso | *FlashRepayIso | - *EarnAssets | *LoanCurList | currency.Pairs | time.Time + *EarnAssets | *LoanCurList | currency.Pairs | time.Time | []futures.Contract | []fundingrate.LatestRateResponse | + []string } type getOneArgParam interface { - string | []string | bool | asset.Item + string | []string | bool | asset.Item | *fundingrate.LatestRateRequest | currency.Code } type getOneArgGen[R getOneArgResp, P getOneArgParam] func(context.Context, P) (R, error) diff --git a/exchanges/bitget/bitget_types.go b/exchanges/bitget/bitget_types.go index 3547099cadf..fffce9dd028 100644 --- a/exchanges/bitget/bitget_types.go +++ b/exchanges/bitget/bitget_types.go @@ -1186,11 +1186,11 @@ type ContractConfigResp struct { SymbolStatus string `json:"symbolStatus"` OffTime int64 `json:"offTime,string"` LimitOpenTime int64 `json:"limitOpenTime,string"` - DeliveryTime EmptyInt `json:"deliveryTime"` - DeliveryStartTime EmptyInt `json:"deliveryStartTime"` - DeliveryPeriod EmptyInt `json:"deliveryPeriod"` - LaunchTime EmptyInt `json:"launchTime"` - FundInterval uint16 `json:"fundInterval,string"` + DeliveryTime UnixTimestamp `json:"deliveryTime"` + DeliveryStartTime UnixTimestamp `json:"deliveryStartTime"` + DeliveryPeriod string `json:"deliveryPeriod"` + LaunchTime UnixTimestamp `json:"launchTime"` + FundInterval EmptyInt `json:"fundInterval"` MinLever float64 `json:"minLever,string"` MaxLever float64 `json:"maxLever,string"` PosLimit float64 `json:"posLimit,string"` diff --git a/exchanges/bitget/bitget_wrapper.go b/exchanges/bitget/bitget_wrapper.go index e0c746dfc9a..4458bc7e84d 100644 --- a/exchanges/bitget/bitget_wrapper.go +++ b/exchanges/bitget/bitget_wrapper.go @@ -8,6 +8,7 @@ import ( "time" "github.com/gofrs/uuid" + "github.com/shopspring/decimal" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" @@ -108,6 +109,7 @@ func (bi *Bitget) SetDefaults() { kline.IntervalCapacity{Interval: kline.OneWeek}, kline.IntervalCapacity{Interval: kline.OneMonth}, ), + GlobalResultLimit: 200, }, }, } @@ -1183,7 +1185,11 @@ func (bi *Bitget) GetActiveOrders(ctx context.Context, getOrdersRequest *order.M return nil, err } for y := range newPairs { - resp, err = bi.spotCurrentPlanOrdersHelper(ctx, newPairs[y].String(), newPairs[y], resp) + callStr, err := bi.FormatExchangeCurrency(newPairs[y], asset.Spot) + if err != nil { + return nil, err + } + resp, err = bi.spotCurrentPlanOrdersHelper(ctx, callStr.String(), newPairs[y], resp) if err != nil { return nil, err } @@ -1297,11 +1303,15 @@ func (bi *Bitget) GetOrderHistory(ctx context.Context, getOrdersRequest *order.M return nil, err } for y := range newPairs { - err = bi.spotFillsHelper(ctx, newPairs[y].String(), fillMap) + callStr, err := bi.FormatExchangeCurrency(newPairs[y], asset.Spot) if err != nil { return nil, err } - resp, err = bi.spotHistoricPlanOrdersHelper(ctx, newPairs[y].String(), newPairs[y], resp, + err = bi.spotFillsHelper(ctx, callStr.String(), fillMap) + if err != nil { + return nil, err + } + resp, err = bi.spotHistoricPlanOrdersHelper(ctx, callStr.String(), newPairs[y], resp, fillMap) if err != nil { return nil, err @@ -1592,20 +1602,154 @@ func (bi *Bitget) GetHistoricCandlesExtended(ctx context.Context, pair currency. } // GetFuturesContractDetails returns all contracts from the exchange by asset type -func (bi *Bitget) GetFuturesContractDetails(ctx context.Context, a asset.Item) ([]futures.Contract, error) { - return nil, common.ErrNotYetImplemented +func (bi *Bitget) GetFuturesContractDetails(ctx context.Context, _ asset.Item) ([]futures.Contract, error) { + var contracts []futures.Contract + for i := range prodTypes { + resp, err := bi.GetContractConfig(ctx, "", prodTypes[i]) + if err != nil { + return nil, err + } + temp := make([]futures.Contract, len(resp.Data)) + for x := range resp.Data { + temp[x] = futures.Contract{ + Exchange: bi.Name, + Name: currency.NewPair(currency.NewCode(resp.Data[x].BaseCoin), + currency.NewCode(resp.Data[x].QuoteCoin)), + Multiplier: resp.Data[x].SizeMultiplier, + Asset: itemDecoder(resp.Data[x].SymbolType), + Type: contractTypeDecoder(resp.Data[x].SymbolType), + Status: resp.Data[x].SymbolStatus, + StartDate: resp.Data[x].DeliveryStartTime.Time(), + EndDate: resp.Data[x].DeliveryTime.Time(), + MaxLeverage: resp.Data[x].MaxLever, + } + set := make(currency.Currencies, len(resp.Data[x].SupportMarginCoins)) + for y := range resp.Data[x].SupportMarginCoins { + set[y] = currency.NewCode(resp.Data[x].SupportMarginCoins[y]) + } + temp[x].SettlementCurrencies = set + if resp.Data[x].SymbolStatus == "listed" || resp.Data[x].SymbolStatus == "normal" { + temp[x].IsActive = true + } + } + contracts = append(contracts, temp...) + } + return contracts, nil } // GetLatestFundingRates returns the latest funding rates data -func (bi *Bitget) GetLatestFundingRates(_ context.Context, _ *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { - return nil, common.ErrNotYetImplemented +func (bi *Bitget) GetLatestFundingRates(ctx context.Context, req *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) { + curRate, err := bi.GetFundingCurrent(ctx, req.Pair.String(), getProductType(req.Pair)) + if err != nil { + return nil, err + } + nextTime, err := bi.GetNextFundingTime(ctx, req.Pair.String(), getProductType(req.Pair)) + if err != nil { + return nil, err + } + resp := []fundingrate.LatestRateResponse{ + { + Exchange: bi.Name, + Pair: req.Pair, + TimeOfNextRate: nextTime.Data[0].NextFundingTime.Time(), + TimeChecked: time.Now(), + }, + } + dec := decimal.NewFromFloat(curRate.Data[0].FundingRate) + resp[0].LatestRate.Rate = dec + return resp, nil } // UpdateOrderExecutionLimits updates order execution limits -func (bi *Bitget) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { +func (bi *Bitget) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error { + var limits []order.MinMaxLevel + switch a { + case asset.Spot: + resp, err := bi.GetSymbolInfo(ctx, "") + if err != nil { + return err + } + limits = make([]order.MinMaxLevel, len(resp.Data)) + for i := range resp.Data { + limits[i] = order.MinMaxLevel{ + Asset: a, + Pair: currency.NewPair(currency.NewCode(resp.Data[i].BaseCoin), + currency.NewCode(resp.Data[i].QuoteCoin)), + PriceStepIncrementSize: float64(resp.Data[i].PricePrecision), + AmountStepIncrementSize: float64(resp.Data[i].QuantityPrecision), + QuoteStepIncrementSize: float64(resp.Data[i].QuotePrecision), + MinNotional: resp.Data[i].MinTradeUSDT, + MarketMinQty: resp.Data[i].MinTradeAmount, + MarketMaxQty: resp.Data[i].MaxTradeAmount, + } + } + case asset.Futures: + for i := range prodTypes { + resp, err := bi.GetContractConfig(ctx, "", prodTypes[i]) + if err != nil { + return err + } + tempResp := make([]order.MinMaxLevel, len(resp.Data)) + for x := range resp.Data { + tempResp[x] = order.MinMaxLevel{ + Asset: a, + Pair: currency.NewPair(currency.NewCode(resp.Data[x].BaseCoin), + currency.NewCode(resp.Data[x].QuoteCoin)), + MinNotional: resp.Data[x].MinTradeUSDT, + MaxTotalOrders: resp.Data[x].MaxSymbolOpenOrderNum, + } + } + limits = append(limits, tempResp...) + } + case asset.Margin, asset.CrossMargin: + resp, err := bi.GetSupportedCurrencies(ctx) + if err != nil { + return err + } + limits = make([]order.MinMaxLevel, len(resp.Data)) + for i := range resp.Data { + limits[i] = order.MinMaxLevel{ + Asset: a, + Pair: currency.NewPair(currency.NewCode(resp.Data[i].BaseCoin), + currency.NewCode(resp.Data[i].QuoteCoin)), + MinNotional: resp.Data[i].MinTradeUSDT, + MarketMinQty: resp.Data[i].MinTradeAmount, + MarketMaxQty: resp.Data[i].MaxTradeAmount, + QuoteStepIncrementSize: float64(resp.Data[i].PricePrecision), + AmountStepIncrementSize: float64(resp.Data[i].QuantityPrecision), + } + } + default: + return asset.ErrNotSupported + } + return bi.LoadLimits(limits) +} + +// UpdateCurrencyStates updates currency states +func (bi *Bitget) UpdateCurrencyStates(ctx context.Context, a asset.Item) error { return common.ErrNotYetImplemented } +// GetAvailableTransferChains returns a list of supported transfer chains based +// on the supplied cryptocurrency +func (bi *Bitget) GetAvailableTransferChains(ctx context.Context, cur currency.Code) ([]string, error) { + if cur.IsEmpty() { + return nil, errCurrencyEmpty + } + resp, err := bi.GetCoinInfo(ctx, cur.String()) + if err != nil { + return nil, err + } + if len(resp.Data) == 0 { + return nil, errReturnEmpty + } + chains := make([]string, len(resp.Data[0].Chains)) + for i := range resp.Data[0].Chains { + chains[i] = resp.Data[0].Chains[i].Chain + } + return chains, nil +} + // GetProductType is a helper function that returns the appropriate product type for a given currency pair func getProductType(p currency.Pair) string { var prodType string @@ -2241,3 +2385,31 @@ func formatExchangeKlineIntervalFutures(interval kline.Interval) string { } return errIntervalNotSupported } + +// ItemDecoder is a helper function that returns the appropriate asset.Item for a given string +func itemDecoder(s string) asset.Item { + switch s { + case "spot": + return asset.Spot + case "margin": + return asset.Margin + case "futures": + return asset.Futures + case "perpetual": + return asset.PerpetualContract + case "delivery": + return asset.DeliveryFutures + } + return asset.Empty +} + +// contractTypeDecoder is a helper function that returns the appropriate contract type for a given string +func contractTypeDecoder(s string) futures.ContractType { + switch s { + case "delivery": + return futures.LongDated + case "perpetual": + return futures.Perpetual + } + return futures.Unknown +} diff --git a/testdata/configtest.json b/testdata/configtest.json index 17cb87681c3..d632963df26 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -2506,7 +2506,8 @@ "uppercase": true }, "configFormat": { - "uppercase": true + "uppercase": true, + "delimiter": "-" }, "useGlobalFormat": true, "pairs": { @@ -2516,7 +2517,9 @@ "available": "BTC-USDT" }, "futures": { - "assetEnabled": true + "assetEnabled": true, + "enabled": "BTC-USDT", + "available": "BTC-USDT" }, "margin": { "assetEnabled": true