From 81249646da2673f879187baae6e5af87ce9d39b4 Mon Sep 17 00:00:00 2001 From: Adam <31364354+MadCozBadd@users.noreply.github.com> Date: Tue, 29 Jun 2021 15:47:03 +1000 Subject: [PATCH] exchanges: API coverage improvements and helper functions (#681) * improved functions and new helper functions * bitfinex margin info func * small rate change * rate changes * adding some currencies for margin funding translation * adding index candles * added test * slight improvement in params * time func * orderbook helper avgprice func * broken test + removing some tlogs and prints * adding test cases * error fix * remove unused * another unused * shazbert changes * wip * bitfinex func and more nits * final shazzy nits * most shazzy nits * few prior requested changes * shazbert nits final WIP * shazbert changes * minor linter issue * unused val * glorious changes * more verbositiy improvements * quick changes * unused remaining amount oops * thrasher changes * reverting changes that were only for testing purposes and bymistake pushed up * bfx shadow dec + huobi fetch tradable pairs formatted so as to return config format for ease of comparison and requests * more linters * glorious final nits wip * formatting tradable pairs for different asset types + remove println * glorious changes --- currency/code_types.go | 2 + exchanges/binance/binance_test.go | 8 + exchanges/binance/binance_ufutures.go | 13 ++ exchanges/binance/binance_wrapper.go | 20 +- exchanges/binance/ufutures_types.go | 5 - exchanges/bitfinex/bitfinex.go | 278 +++++++++++++++++------- exchanges/bitfinex/bitfinex_test.go | 45 +++- exchanges/bitfinex/bitfinex_types.go | 17 ++ exchanges/bitfinex/bitfinex_wrapper.go | 6 +- exchanges/bitfinex/ratelimit.go | 1 + exchanges/ftx/ftx.go | 51 ++++- exchanges/ftx/ftx_test.go | 22 +- exchanges/ftx/ftx_wrapper.go | 24 +- exchanges/huobi/huobi_test.go | 17 +- exchanges/huobi/huobi_wrapper.go | 23 +- exchanges/kraken/kraken_wrapper.go | 15 +- exchanges/okex/okex.go | 12 +- exchanges/okex/okex_test.go | 8 + exchanges/okgroup/okgroup_types.go | 19 ++ exchanges/orderbook/calculator.go | 37 ++++ exchanges/orderbook/calculator_test.go | 64 ++++++ exchanges/orderbook/orderbook_types.go | 1 + testdata/http_mock/binance/binance.json | 12 + 23 files changed, 564 insertions(+), 136 deletions(-) diff --git a/currency/code_types.go b/currency/code_types.go index 2ce4f1b7..8f988261 100644 --- a/currency/code_types.go +++ b/currency/code_types.go @@ -1540,7 +1540,9 @@ var ( PDX = NewCode("PDX") SLT = NewCode("SLT") HPY = NewCode("HPY") + XXRP = NewCode("XXRP") // XRP XXBT = NewCode("XXBT") // BTC, but XXBT instead + XXDG = NewCode("XXDG") // DOGE XDG = NewCode("XDG") // DOGE HKD = NewCode("HKD") // Hong Kong Dollar AUD = NewCode("AUD") // Australian Dollar diff --git a/exchanges/binance/binance_test.go b/exchanges/binance/binance_test.go index 7efbf55a..d4239621 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -43,6 +43,14 @@ func setFeeBuilder() *exchange.FeeBuilder { } } +func TestUServerTime(t *testing.T) { + t.Parallel() + _, err := b.UServerTime() + if err != nil { + t.Error(err) + } +} + func TestUpdateTicker(t *testing.T) { t.Parallel() spotPairs, err := b.FetchTradablePairs(asset.Spot) diff --git a/exchanges/binance/binance_ufutures.go b/exchanges/binance/binance_ufutures.go index 52382f97..35fd2201 100644 --- a/exchanges/binance/binance_ufutures.go +++ b/exchanges/binance/binance_ufutures.go @@ -19,6 +19,7 @@ import ( const ( // Unauth + ufuturesServerTime = "/fapi/v1/time" ufuturesExchangeInfo = "/fapi/v1/exchangeInfo?" ufuturesOrderbook = "/fapi/v1/depth?" ufuturesRecentTrades = "/fapi/v1/trades?" @@ -62,6 +63,18 @@ const ( ufuturesADLQuantile = "/fapi/v1/adlQuantile" ) +// UServerTime gets the server time +func (b *Binance) UServerTime() (time.Time, error) { + var data struct { + ServerTime int64 `json:"serverTime"` + } + err := b.SendHTTPRequest(exchange.RestUSDTMargined, ufuturesServerTime, uFuturesDefaultRate, &data) + if err != nil { + return time.Time{}, err + } + return time.Unix(0, data.ServerTime*1000000), nil +} + // UExchangeInfo stores usdt margined futures data func (b *Binance) UExchangeInfo() (UFuturesExchangeInfo, error) { var resp UFuturesExchangeInfo diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go index 20f2433e..3cd27455 100644 --- a/exchanges/binance/binance_wrapper.go +++ b/exchanges/binance/binance_wrapper.go @@ -345,6 +345,10 @@ func (b *Binance) FetchTradablePairs(a asset.Item) ([]string, error) { if !b.SupportsAsset(a) { return nil, fmt.Errorf("asset type of %s is not supported by %s", a, b.Name) } + format, err := b.GetPairFormat(a, false) + if err != nil { + return nil, err + } var pairs []string switch a { case asset.Spot, asset.Margin: @@ -352,10 +356,6 @@ func (b *Binance) FetchTradablePairs(a asset.Item) ([]string, error) { if err != nil { return nil, err } - format, err := b.GetPairFormat(a, false) - if err != nil { - return nil, err - } for x := range info.Symbols { if info.Symbols[x].Status == "TRADING" { pair := info.Symbols[x].BaseAsset + @@ -376,7 +376,11 @@ func (b *Binance) FetchTradablePairs(a asset.Item) ([]string, error) { } for z := range cInfo.Symbols { if cInfo.Symbols[z].ContractStatus == "TRADING" { - pairs = append(pairs, cInfo.Symbols[z].Symbol) + curr, err := currency.NewPairFromString(cInfo.Symbols[z].Symbol) + if err != nil { + return nil, err + } + pairs = append(pairs, format.Format(curr)) } } case asset.USDTMarginedFutures: @@ -386,7 +390,11 @@ func (b *Binance) FetchTradablePairs(a asset.Item) ([]string, error) { } for u := range uInfo.Symbols { if uInfo.Symbols[u].Status == "TRADING" { - pairs = append(pairs, uInfo.Symbols[u].Symbol) + curr, err := currency.NewPairFromString(uInfo.Symbols[u].Symbol) + if err != nil { + return nil, err + } + pairs = append(pairs, format.Format(curr)) } } } diff --git a/exchanges/binance/ufutures_types.go b/exchanges/binance/ufutures_types.go index d3c423ce..a2a6a953 100644 --- a/exchanges/binance/ufutures_types.go +++ b/exchanges/binance/ufutures_types.go @@ -13,11 +13,6 @@ var ( "ALL", "CURRENT_QUARTER", "NEXT_QUARTER", } - validOrderType = []string{ - "LIMIT", "MARKET", "STOP", "TAKE_PROFIT", - "STOP_MARKET", "TAKE_PROFIT_MARKET", "TRAILING_STOP_MARKET", - } - validNewOrderRespType = []string{"ACK", "RESULT"} validWorkingType = []string{"MARK_PRICE", "CONTRACT_TYPE"} diff --git a/exchanges/bitfinex/bitfinex.go b/exchanges/bitfinex/bitfinex.go index 05c39848..02728bcb 100644 --- a/exchanges/bitfinex/bitfinex.go +++ b/exchanges/bitfinex/bitfinex.go @@ -66,6 +66,7 @@ const ( bitfinexV2MarginFunding = "calc/trade/avg?" bitfinexV2Balances = "auth/r/wallets" bitfinexV2AccountInfo = "auth/r/info/user" + bitfinexV2MarginInfo = "auth/r/info/margin/" bitfinexV2FundingInfo = "auth/r/info/funding/%s" bitfinexDerivativeData = "status/deriv?" bitfinexPlatformStatus = "platform/status" @@ -118,6 +119,141 @@ func (b *Bitfinex) GetPlatformStatus() (int, error) { return -1, fmt.Errorf("unexpected platform status value %d", response[0]) } +func baseMarginInfo(data []interface{}) (MarginInfoV2, error) { + var resp MarginInfoV2 + tempData, ok := data[1].([]interface{}) + if !ok { + return resp, fmt.Errorf("%w", errTypeAssert) + } + resp.UserPNL, ok = tempData[0].(float64) + if !ok { + return resp, fmt.Errorf("%w for UserPNL", errTypeAssert) + } + resp.UserSwaps, ok = tempData[1].(float64) + if !ok { + return resp, fmt.Errorf("%w for UserSwaps", errTypeAssert) + } + resp.MarginBalance, ok = tempData[2].(float64) + if !ok { + return resp, fmt.Errorf("%w for MarginBalance", errTypeAssert) + } + resp.MarginNet, ok = tempData[3].(float64) + if !ok { + return resp, fmt.Errorf("%w for MarginNet", errTypeAssert) + } + resp.MarginMin, ok = tempData[4].(float64) + if !ok { + return resp, fmt.Errorf("%w for MarginMin", errTypeAssert) + } + return resp, nil +} + +func symbolMarginInfo(data []interface{}) ([]MarginInfoV2, error) { + var resp []MarginInfoV2 + for x := range data { + var tempResp MarginInfoV2 + tempData, ok := data[x].([]interface{}) + if !ok { + return nil, fmt.Errorf("%w for all sym", errTypeAssert) + } + var check bool + tempResp.Symbol, check = tempData[1].(string) + if !check { + return nil, fmt.Errorf("%w for symbol data", errTypeAssert) + } + tempFloatData, check := tempData[2].([]interface{}) + if !check { + return nil, fmt.Errorf("%w for symbol data", errTypeAssert) + } + if len(tempFloatData) < 4 { + return nil, errors.New("invalid data received") + } + tempResp.TradableBalance, ok = tempFloatData[0].(float64) + if !ok { + return nil, fmt.Errorf("%w for TradableBalance", errTypeAssert) + } + tempResp.GrossBalance, ok = tempFloatData[1].(float64) + if !ok { + return nil, fmt.Errorf("%w for GrossBalance", errTypeAssert) + } + tempResp.BestAskAmount, ok = tempFloatData[2].(float64) + if !ok { + return nil, fmt.Errorf("%w for BestAskAmount", errTypeAssert) + } + tempResp.BestBidAmount, ok = tempFloatData[3].(float64) + if !ok { + return nil, fmt.Errorf("%w for BestBidAmount", errTypeAssert) + } + resp = append(resp, tempResp) + } + return resp, nil +} + +func defaultMarginV2Info(data []interface{}) (MarginInfoV2, error) { + var resp MarginInfoV2 + var ok bool + resp.Symbol, ok = data[1].(string) + if !ok { + return resp, fmt.Errorf("%w for symbol", errTypeAssert) + } + tempData, check := data[2].([]interface{}) + if !check { + return resp, fmt.Errorf("%w for symbol data", errTypeAssert) + } + if len(tempData) < 4 { + return resp, errors.New("invalid data received") + } + resp.TradableBalance, ok = tempData[0].(float64) + if !ok { + return resp, fmt.Errorf("%w for TradableBalance", errTypeAssert) + } + resp.GrossBalance, ok = tempData[1].(float64) + if !ok { + return resp, fmt.Errorf("%w for GrossBalance", errTypeAssert) + } + resp.BestAskAmount, ok = tempData[2].(float64) + if !ok { + return resp, fmt.Errorf("%w for BestAskAmount", errTypeAssert) + } + resp.BestBidAmount, ok = tempData[3].(float64) + if !ok { + return resp, fmt.Errorf("%w for BestBidAmount", errTypeAssert) + } + return resp, nil +} + +// GetV2MarginInfo gets v2 margin info for a symbol provided +// symbol: base, sym_all, any other trading symbol example tBTCUSD +func (b *Bitfinex) GetV2MarginInfo(symbol string) ([]MarginInfoV2, error) { + var data []interface{} + err := b.SendAuthenticatedHTTPRequestV2(exchange.RestSpot, http.MethodPost, + bitfinexV2MarginInfo+symbol, + nil, + &data, + getMarginInfoRate) + if err != nil { + return nil, err + } + var tempResp MarginInfoV2 + switch symbol { + case "base": + tempResp, err = baseMarginInfo(data) + if err != nil { + return nil, fmt.Errorf("%v - %s: %w", b.Name, symbol, err) + } + case "sym_all": + var resp []MarginInfoV2 + resp, err = symbolMarginInfo(data) + return resp, err + default: + tempResp, err = defaultMarginV2Info(data) + if err != nil { + return nil, fmt.Errorf("%v - %s: %w", b.Name, symbol, err) + } + } + return []MarginInfoV2{tempResp}, nil +} + // GetV2MarginFunding gets borrowing rates for margin trading func (b *Bitfinex) GetV2MarginFunding(symbol, amount string, period int32) (MarginV2FundingData, error) { var resp []interface{} @@ -130,7 +266,7 @@ func (b *Bitfinex) GetV2MarginFunding(symbol, amount string, period int32) (Marg bitfinexV2MarginFunding, params, &resp, - getAccountFees) + getMarginInfoRate) if err != nil { return response, err } @@ -139,11 +275,11 @@ func (b *Bitfinex) GetV2MarginFunding(symbol, amount string, period int32) (Marg } avgRate, ok := resp[0].(float64) if !ok { - return response, errors.New("failed type assertion for rate") + return response, fmt.Errorf("%v - %v: %w for rate", b.Name, symbol, errTypeAssert) } avgAmount, ok := resp[1].(float64) if !ok { - return response, errors.New("failed type assertion for amount") + return response, fmt.Errorf("%v - %v: %w for amount", b.Name, symbol, errTypeAssert) } response.Symbol = symbol response.RateAverage = avgRate @@ -168,20 +304,20 @@ func (b *Bitfinex) GetV2FundingInfo(key string) (MarginFundingDataV2, error) { } sym, ok := resp[0].(string) if !ok { - return response, errors.New("failed type assertion for sym") + return response, fmt.Errorf("%v GetV2FundingInfo: %w for sym", b.Name, errTypeAssert) } symbol, ok := resp[1].(string) if !ok { - return response, errors.New("failed type assertion for symbol") + return response, fmt.Errorf("%v GetV2FundingInfo: %w for symbol", b.Name, errTypeAssert) } fundingData, ok := resp[2].([]interface{}) if !ok { - return response, errors.New("failed type assertion for fundingData") + return response, fmt.Errorf("%v GetV2FundingInfo: %w for fundingData", b.Name, errTypeAssert) } response.Sym = sym response.Symbol = symbol if len(fundingData) < 4 { - return response, errors.New("invalid length of fundingData") + return response, fmt.Errorf("%v GetV2FundingInfo: invalid length of fundingData", b.Name) } for x := 0; x < 3; x++ { _, ok := fundingData[x].(float64) @@ -209,33 +345,33 @@ func (b *Bitfinex) GetAccountInfoV2() (AccountV2Data, error) { return resp, err } if len(data) < 8 { - return resp, errors.New("invalid length of data") + return resp, fmt.Errorf("%v GetAccountInfoV2: invalid length of data", b.Name) } var ok bool var tempString string var tempFloat float64 if tempFloat, ok = data[0].(float64); !ok { - return resp, errors.New("type assertion failed for id, check for api updates") + return resp, fmt.Errorf("%v GetAccountInfoV2: %w for id", b.Name, errTypeAssert) } resp.ID = int64(tempFloat) if tempString, ok = data[1].(string); !ok { - return resp, errors.New("type assertion failed for email, check for api updates") + return resp, fmt.Errorf("%v GetAccountInfoV2: %w for email", b.Name, errTypeAssert) } resp.Email = tempString if tempString, ok = data[2].(string); !ok { - return resp, errors.New("type assertion failed for username, check for api updates") + return resp, fmt.Errorf("%v GetAccountInfoV2: %w for username", b.Name, errTypeAssert) } resp.Username = tempString if tempFloat, ok = data[3].(float64); !ok { - return resp, errors.New("type assertion failed for accountcreate, check for api updates") + return resp, fmt.Errorf("%v GetAccountInfoV2: %w for accountcreate", b.Name, errTypeAssert) } resp.MTSAccountCreate = int64(tempFloat) if tempFloat, ok = data[4].(float64); !ok { - return resp, errors.New("type assertion failed for verified, check for api updates") + return resp, fmt.Errorf("%v GetAccountInfoV2: %w failed for verified", b.Name, errTypeAssert) } resp.Verified = int64(tempFloat) if tempString, ok = data[7].(string); !ok { - return resp, errors.New("type assertion failed for timezone, check for api updates") + return resp, fmt.Errorf("%v GetAccountInfoV2: %w for timezone", b.Name, errTypeAssert) } resp.Timezone = tempString return resp, nil @@ -256,19 +392,19 @@ func (b *Bitfinex) GetV2Balances() ([]WalletDataV2, error) { for x := range data { wType, ok := data[x][0].(string) if !ok { - return resp, errors.New("type assertion failed for walletType, check for api updates") + return resp, fmt.Errorf("%v GetV2Balances: %w for walletType", b.Name, errTypeAssert) } curr, ok := data[x][1].(string) if !ok { - return resp, errors.New("type assertion failed for currency, check for api updates") + return resp, fmt.Errorf("%v GetV2Balances: %w for currency", b.Name, errTypeAssert) } bal, ok := data[x][2].(float64) if !ok { - return resp, errors.New("type assertion failed for balance, check for api updates") + return resp, fmt.Errorf("%v GetV2Balances: %w for balance", b.Name, errTypeAssert) } unsettledInterest, ok := data[x][3].(float64) if !ok { - return resp, errors.New("type assertion failed for unsettledInterest, check for api updates") + return resp, fmt.Errorf("%v GetV2Balances: %w for unsettledInterest", b.Name, errTypeAssert) } resp = append(resp, WalletDataV2{ WalletType: wType, @@ -294,10 +430,10 @@ func (b *Bitfinex) GetMarginPairs() ([]string, error) { return resp[0], nil } -// GetDerivativeData gets data for the queried derivative -func (b *Bitfinex) GetDerivativeData(keys, startTime, endTime string, sort, limit int64) (DerivativeDataResponse, error) { - var result [][19]interface{} - var response DerivativeDataResponse +// GetDerivativeStatusInfo gets status data for the queried derivative +func (b *Bitfinex) GetDerivativeStatusInfo(keys, startTime, endTime string, sort, limit int64) ([]DerivativeDataResponse, error) { + var result [][]interface{} + var finalResp []DerivativeDataResponse params := url.Values{} params.Set("keys", keys) @@ -317,62 +453,51 @@ func (b *Bitfinex) GetDerivativeData(keys, startTime, endTime string, sort, limi params.Encode() err := b.SendHTTPRequest(exchange.RestSpot, path, &result, status) if err != nil { - return response, err + return finalResp, err } - if len(result) < 1 { - return response, errors.New("invalid response, array length too small, check api docs for updates") + for z := range result { + if len(result[z]) < 19 { + return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: invalid response, array length too small, check api docs for updates", b.Name) + } + var response DerivativeDataResponse + var ok bool + if response.Key, ok = result[z][0].(string); !ok { + return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for Key", b.Name, errTypeAssert) + } + if response.MTS, ok = result[z][1].(float64); !ok { + return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for MTS", b.Name, errTypeAssert) + } + if response.DerivPrice, ok = result[z][3].(float64); !ok { + return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for DerivPrice", b.Name, errTypeAssert) + } + if response.SpotPrice, ok = result[z][4].(float64); !ok { + return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for SpotPrice", b.Name, errTypeAssert) + } + if response.InsuranceFundBalance, ok = result[z][6].(float64); !ok { + return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for Insurance fund balance", b.Name, errTypeAssert) + } + if response.NextFundingEventTS, ok = result[z][8].(float64); !ok { + return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for NextFundingEventTS", b.Name, errTypeAssert) + } + if response.NextFundingAccured, ok = result[z][9].(float64); !ok { + return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for NextFundingAccrued", b.Name, errTypeAssert) + } + if response.NextFundingStep, ok = result[z][10].(float64); !ok { + return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for NextFundingStep", b.Name, errTypeAssert) + } + if response.CurrentFunding, ok = result[z][12].(float64); !ok { + return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for CurrentFunding", b.Name, errTypeAssert) + } + if response.MarkPrice, ok = result[z][15].(float64); !ok { + return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for MarkPrice", b.Name, errTypeAssert) + } + + if response.OpenInterest, ok = result[z][18].(float64); !ok { + return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for OpenInterest", b.Name, errTypeAssert) + } + finalResp = append(finalResp, response) } - if len(result[0]) < 19 { - return response, errors.New("invalid response, array length too small, check api docs for updates") - } - var floatData float64 - var stringData string - var ok bool - if stringData, ok = result[0][0].(string); !ok { - return response, errors.New("type assertion failed, check for api updates") - } - response.Key = stringData - if floatData, ok = result[0][1].(float64); !ok { - return response, errors.New("type assertion failed, check for api updates") - } - response.MTS = floatData - if floatData, ok = result[0][3].(float64); !ok { - return response, errors.New("type assertion failed, check for api updates") - } - response.DerivPrice = floatData - if floatData, ok = result[0][4].(float64); !ok { - return response, errors.New("type assertion failed, check for api updates") - } - response.SpotPrice = floatData - if floatData, ok = result[0][6].(float64); !ok { - return response, errors.New("type assertion failed, check for api updates") - } - response.InsuranceFundBalance = floatData - if floatData, ok = result[0][8].(float64); !ok { - return response, errors.New("type assertion failed, check for api updates") - } - response.NextFundingEventTS = floatData - if floatData, ok = result[0][9].(float64); !ok { - return response, errors.New("type assertion failed, check for api updates") - } - response.NextFundingAccured = floatData - if floatData, ok = result[0][10].(float64); !ok { - return response, errors.New("type assertion failed, check for api updates") - } - response.NextFundingStep = floatData - if floatData, ok = result[0][12].(float64); !ok { - return response, errors.New("type assertion failed, check for api updates") - } - response.CurrentFunding = floatData - if floatData, ok = result[0][15].(float64); !ok { - return response, errors.New("type assertion failed, check for api updates") - } - response.MarkPrice = floatData - if floatData, ok = result[0][18].(float64); !ok { - return response, errors.New("type assertion failed, check for api updates") - } - response.OpenInterest = floatData - return response, nil + return finalResp, nil } // GetTickerBatch returns all supported ticker information @@ -545,7 +670,6 @@ func (b *Bitfinex) GetOrderbook(symbol, precision string, limit int64) (Orderboo u.Set("len", strconv.FormatInt(limit, 10)) } path := bitfinexAPIVersion2 + bitfinexOrderbook + symbol + "/" + precision + "?" + u.Encode() - var response [][]interface{} err := b.SendHTTPRequest(exchange.RestSpot, path, &response, orderbookFunction) if err != nil { diff --git a/exchanges/bitfinex/bitfinex_test.go b/exchanges/bitfinex/bitfinex_test.go index be3614f3..7dab4241 100644 --- a/exchanges/bitfinex/bitfinex_test.go +++ b/exchanges/bitfinex/bitfinex_test.go @@ -63,7 +63,7 @@ func TestMain(m *testing.M) { func TestGetV2MarginFunding(t *testing.T) { if !areTestAPIKeysSet() { - t.SkipNow() + t.Skip("api keys are not set or invalid") } _, err := b.GetV2MarginFunding("fUSD", "2", 2) if err != nil { @@ -71,10 +71,28 @@ func TestGetV2MarginFunding(t *testing.T) { } } +func TestGetV2MarginInfo(t *testing.T) { + if !areTestAPIKeysSet() { + t.Skip("api keys are not set or invalid") + } + _, err := b.GetV2MarginInfo("base") + if err != nil { + t.Error(err) + } + _, err = b.GetV2MarginInfo("tBTCUSD") + if err != nil { + t.Error(err) + } + _, err = b.GetV2MarginInfo("sym_all") + if err != nil { + t.Error(err) + } +} + func TestGetAccountInfoV2(t *testing.T) { t.Parallel() if !areTestAPIKeysSet() { - t.SkipNow() + t.Skip("api keys are not set or invalid") } _, err := b.GetAccountInfoV2() if err != nil { @@ -84,9 +102,9 @@ func TestGetAccountInfoV2(t *testing.T) { func TestGetV2FundingInfo(t *testing.T) { if !areTestAPIKeysSet() { - t.SkipNow() + t.Skip("api keys are not set or invalid") } - _, err := b.GetV2FundingInfo("fUSD") + _, err := b.GetV2FundingInfo("fUST") if err != nil { t.Error(err) } @@ -95,7 +113,7 @@ func TestGetV2FundingInfo(t *testing.T) { func TestGetV2Balances(t *testing.T) { t.Parallel() if !areTestAPIKeysSet() { - t.SkipNow() + t.Skip("api keys are not set or invalid") } _, err := b.GetV2Balances() if err != nil { @@ -103,9 +121,9 @@ func TestGetV2Balances(t *testing.T) { } } -func TestGetDerivativeData(t *testing.T) { +func TestGetDerivativeStatusInfo(t *testing.T) { t.Parallel() - _, err := b.GetDerivativeData("tBTCF0:USTF0", "", "", 0, 0) + _, err := b.GetDerivativeStatusInfo("ALL", "", "", 0, 0) if err != nil { t.Error(err) } @@ -205,6 +223,11 @@ func TestGetOrderbook(t *testing.T) { if err != nil { t.Error(err) } + + _, err = b.GetOrderbook("tLINK:UST", "P0", 1) + if err != nil { + t.Error(err) + } } func TestGetStats(t *testing.T) { @@ -1363,7 +1386,7 @@ func TestFixCasing(t *testing.T) { if err != nil { t.Fatal(err) } - if ret != "fBTCUSD" { + if ret != "tBTC:USD" { t.Errorf("unexpected result: %v", ret) } pair, err = currency.NewPairFromString("BTCUSD") @@ -1415,7 +1438,7 @@ func TestFixCasing(t *testing.T) { if err != nil { t.Fatal(err) } - ret, err = b.fixCasing(pair, asset.Margin) + ret, err = b.fixCasing(pair, asset.MarginFunding) if err != nil { t.Fatal(err) } @@ -1426,7 +1449,7 @@ func TestFixCasing(t *testing.T) { if err != nil { t.Fatal(err) } - ret, err = b.fixCasing(pair, asset.Margin) + ret, err = b.fixCasing(pair, asset.MarginFunding) if err != nil { t.Fatal(err) } @@ -1438,7 +1461,7 @@ func TestFixCasing(t *testing.T) { if err != nil { t.Fatal(err) } - ret, err = b.fixCasing(pair, asset.Margin) + ret, err = b.fixCasing(pair, asset.MarginFunding) if err != nil { t.Fatal(err) } diff --git a/exchanges/bitfinex/bitfinex_types.go b/exchanges/bitfinex/bitfinex_types.go index 115a5b1d..a1f0e980 100644 --- a/exchanges/bitfinex/bitfinex_types.go +++ b/exchanges/bitfinex/bitfinex_types.go @@ -1,11 +1,14 @@ package bitfinex import ( + "errors" "time" "github.com/thrasher-corp/gocryptotrader/exchanges/order" ) +var errTypeAssert = errors.New("type assertion failed") + // AccountV2Data stores account v2 data type AccountV2Data struct { ID int64 @@ -16,6 +19,20 @@ type AccountV2Data struct { Timezone string } +// MarginInfoV2 stores V2 margin data +type MarginInfoV2 struct { + Symbol string + UserPNL float64 + UserSwaps float64 + MarginBalance float64 + MarginNet float64 + MarginMin float64 + TradableBalance float64 + GrossBalance float64 + BestAskAmount float64 + BestBidAmount float64 +} + // WalletDataV2 stores wallet data for v2 type WalletDataV2 struct { WalletType string diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index f5663d01..5bbf5f29 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -66,7 +66,7 @@ func (b *Bitfinex) SetDefaults() { } fmt2 := currency.PairStore{ - RequestFormat: ¤cy.PairFormat{Uppercase: true}, + RequestFormat: ¤cy.PairFormat{Uppercase: true, Delimiter: ":"}, ConfigFormat: ¤cy.PairFormat{Uppercase: true, Delimiter: ":"}, } @@ -1063,10 +1063,10 @@ func (b *Bitfinex) GetHistoricCandlesExtended(pair currency.Pair, a asset.Item, func (b *Bitfinex) fixCasing(in currency.Pair, a asset.Item) (string, error) { var checkString [2]byte - if a == asset.Spot { + if a == asset.Spot || a == asset.Margin { checkString[0] = 't' checkString[1] = 'T' - } else if a == asset.Margin { + } else if a == asset.MarginFunding { checkString[0] = 'f' checkString[1] = 'F' } diff --git a/exchanges/bitfinex/ratelimit.go b/exchanges/bitfinex/ratelimit.go index 839d664c..86b1cd2d 100644 --- a/exchanges/bitfinex/ratelimit.go +++ b/exchanges/bitfinex/ratelimit.go @@ -48,6 +48,7 @@ const ( getPositionAuditReqRate = 45 updateCollateralOnPositionReqRate = 45 // This is not specified just inputed above // Margin funding - + getMarginInfoRate = 90 getActiveFundingOffersReqRate = 45 submitFundingOfferReqRate = 45 // This is not specified just inputed above cancelFundingOfferReqRate = 45 diff --git a/exchanges/ftx/ftx.go b/exchanges/ftx/ftx.go index a3d25477..06ab7c88 100644 --- a/exchanges/ftx/ftx.go +++ b/exchanges/ftx/ftx.go @@ -41,6 +41,7 @@ const ( getFundingRates = "/funding_rates" getIndexWeights = "/indexes/%s/weights" getAllWalletBalances = "/wallet/all_balances" + getIndexCandles = "/indexes/%s/candles" // Authenticated endpoints getAccountInfo = "/account" @@ -133,8 +134,45 @@ var ( errCoinMustBeSpecified = errors.New("a coin must be specified") errSubaccountTransferSizeGreaterThanZero = errors.New("transfer size must be greater than 0") errSubaccountTransferSourceDestinationMustNotBeEqual = errors.New("subaccount transfer source and destination must not be the same value") + + validResolutionData = []int64{15, 60, 300, 900, 3600, 14400, 86400} ) +// GetHistoricalIndex gets historical index data +func (f *FTX) GetHistoricalIndex(indexName string, resolution int64, startTime, endTime time.Time) ([]OHLCVData, error) { + params := url.Values{} + if indexName == "" { + return nil, errors.New("indexName is a mandatory field") + } + params.Set("index_name", indexName) + err := checkResolution(resolution) + if err != nil { + return nil, err + } + params.Set("resolution", strconv.FormatInt(resolution, 10)) + if !startTime.IsZero() && !endTime.IsZero() { + if startTime.After(endTime) { + return nil, errStartTimeCannotBeAfterEndTime + } + params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10)) + params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10)) + } + resp := struct { + Data []OHLCVData `json:"result"` + }{} + endpoint := common.EncodeURLValues(fmt.Sprintf(getIndexCandles, indexName), params) + return resp.Data, f.SendHTTPRequest(exchange.RestSpot, endpoint, &resp) +} + +func checkResolution(res int64) error { + for x := range validResolutionData { + if validResolutionData[x] == res { + return nil + } + } + return errors.New("resolution data is a mandatory field and the data provided is invalid") +} + // GetMarkets gets market data func (f *FTX) GetMarkets() ([]MarketData, error) { resp := struct { @@ -210,19 +248,20 @@ func (f *FTX) GetTrades(marketName string, startTime, endTime, limit int64) ([]T } // GetHistoricalData gets historical OHLCV data for a given market pair -func (f *FTX) GetHistoricalData(marketName, timeInterval, limit string, startTime, endTime time.Time) ([]OHLCVData, error) { +func (f *FTX) GetHistoricalData(marketName string, timeInterval, limit int64, startTime, endTime time.Time) ([]OHLCVData, error) { if marketName == "" { return nil, errors.New("a market pair must be specified") } - if timeInterval == "" { - return nil, errors.New("a time interval must be specified") + err := checkResolution(timeInterval) + if err != nil { + return nil, err } params := url.Values{} - params.Set("resolution", timeInterval) - if limit != "" { - params.Set("limit", limit) + params.Set("resolution", strconv.FormatInt(timeInterval, 10)) + if limit != 0 { + params.Set("limit", strconv.FormatInt(limit, 10)) } if !startTime.IsZero() && !endTime.IsZero() { if startTime.After(endTime) { diff --git a/exchanges/ftx/ftx_test.go b/exchanges/ftx/ftx_test.go index ca53f2d1..af8488db 100644 --- a/exchanges/ftx/ftx_test.go +++ b/exchanges/ftx/ftx_test.go @@ -81,6 +81,18 @@ func TestGetMarkets(t *testing.T) { } } +func TestGetHistoricalIndex(t *testing.T) { + t.Parallel() + _, err := f.GetHistoricalIndex("BTC", 3600, time.Now().Add(-time.Hour*2), time.Now().Add(-time.Hour*1)) + if err != nil { + t.Error(err) + } + _, err = f.GetHistoricalIndex("BTC", 3600, time.Time{}, time.Time{}) + if err != nil { + t.Error(err) + } +} + func TestGetMarket(t *testing.T) { t.Parallel() _, err := f.GetMarket(spotPair) @@ -136,28 +148,28 @@ func TestGetTrades(t *testing.T) { func TestGetHistoricalData(t *testing.T) { t.Parallel() // test empty market - _, err := f.GetHistoricalData("", "86400", "5", time.Time{}, time.Time{}) + _, err := f.GetHistoricalData("", 86400, 5, time.Time{}, time.Time{}) if err == nil { t.Error("empty market should return an error") } // test empty resolution - _, err = f.GetHistoricalData(spotPair, "", "5", time.Time{}, time.Time{}) + _, err = f.GetHistoricalData(spotPair, 0, 5, time.Time{}, time.Time{}) if err == nil { t.Error("empty resolution should return an error") } - _, err = f.GetHistoricalData(spotPair, "86400", "5", time.Unix(validFTTBTCEndTime, 0), time.Unix(validFTTBTCStartTime, 0)) + _, err = f.GetHistoricalData(spotPair, 86400, 5, time.Unix(validFTTBTCEndTime, 0), time.Unix(validFTTBTCStartTime, 0)) if err != errStartTimeCannotBeAfterEndTime { t.Errorf("should have thrown errStartTimeCannotBeAfterEndTime, got %v", err) } var o []OHLCVData - o, err = f.GetHistoricalData(spotPair, "86400", "5", time.Time{}, time.Time{}) + o, err = f.GetHistoricalData(spotPair, 86400, 5, time.Time{}, time.Time{}) if err != nil { t.Error(err) } if len(o) != 5 { t.Error("limit of 5 should return 5 items") } - o, err = f.GetHistoricalData(spotPair, "86400", "5", time.Unix(invalidFTTBTCStartTime, 0), time.Unix(invalidFTTBTCEndTime, 0)) + o, err = f.GetHistoricalData(spotPair, 86400, 5, time.Unix(invalidFTTBTCStartTime, 0), time.Unix(invalidFTTBTCEndTime, 0)) if err != nil { t.Error(err) } diff --git a/exchanges/ftx/ftx_wrapper.go b/exchanges/ftx/ftx_wrapper.go index cbffd1b8..68291bbf 100644 --- a/exchanges/ftx/ftx_wrapper.go +++ b/exchanges/ftx/ftx_wrapper.go @@ -239,18 +239,30 @@ func (f *FTX) FetchTradablePairs(a asset.Item) ([]string, error) { if err != nil { return nil, err } + format, err := f.GetPairFormat(a, false) + if err != nil { + return nil, err + } var pairs []string switch a { case asset.Spot: for x := range markets { if markets[x].MarketType == spotString { - pairs = append(pairs, markets[x].Name) + curr, err := currency.NewPairFromString(markets[x].Name) + if err != nil { + return nil, err + } + pairs = append(pairs, format.Format(curr)) } } case asset.Futures: for x := range markets { if markets[x].MarketType == futuresString { - pairs = append(pairs, markets[x].Name) + curr, err := currency.NewPairFromString(markets[x].Name) + if err != nil { + return nil, err + } + pairs = append(pairs, format.Format(curr)) } } } @@ -1021,8 +1033,8 @@ func (f *FTX) GetHistoricCandles(p currency.Pair, a asset.Item, start, end time. } ohlcData, err := f.GetHistoricalData(formattedPair.String(), - f.FormatExchangeKlineInterval(interval), - strconv.FormatInt(int64(f.Features.Enabled.Kline.ResultLimit), 10), + int64(interval.Duration().Seconds()), + int64(f.Features.Enabled.Kline.ResultLimit), start, end) if err != nil { return kline.Item{}, err @@ -1071,8 +1083,8 @@ func (f *FTX) GetHistoricCandlesExtended(p currency.Pair, a asset.Item, start, e for x := range dates.Ranges { var ohlcData []OHLCVData ohlcData, err = f.GetHistoricalData(formattedPair.String(), - f.FormatExchangeKlineInterval(interval), - strconv.FormatInt(int64(f.Features.Enabled.Kline.ResultLimit), 10), + int64(interval.Duration().Seconds()), + int64(f.Features.Enabled.Kline.ResultLimit), dates.Ranges[x].Start.Time, dates.Ranges[x].End.Time) if err != nil { return kline.Item{}, err diff --git a/exchanges/huobi/huobi_test.go b/exchanges/huobi/huobi_test.go index 62c22f19..13fa286d 100644 --- a/exchanges/huobi/huobi_test.go +++ b/exchanges/huobi/huobi_test.go @@ -698,7 +698,7 @@ func TestUpdateOrderbook(t *testing.T) { if err != nil { t.Error(err) } - cp1, err := currency.NewPairFromString("BTC-USD") + cp1, err := currency.NewPairFromString("BTC_USD") if err != nil { t.Error(err) } @@ -721,6 +721,21 @@ func TestUpdateOrderbook(t *testing.T) { if err != nil { t.Error(err) } + tradablePairs, err = h.FetchTradablePairs(asset.CoinMarginedFutures) + if err != nil { + t.Error(err) + } + if len(tradablePairs) == 0 { + t.Fatal("no tradable pairs") + } + cp2, err = currency.NewPairFromString(tradablePairs[0]) + if err != nil { + t.Error(err) + } + _, err = h.UpdateOrderbook(cp2, asset.Futures) + if err != nil { + t.Error(err) + } } func TestUpdateAccountInfo(t *testing.T) { diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index 646e7723..f84d5590 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -336,6 +336,11 @@ func (h *HUOBI) FetchTradablePairs(a asset.Item) ([]string, error) { var pairs []string + format, err := h.GetPairFormat(a, false) + if err != nil { + return nil, err + } + switch a { case asset.Spot: symbols, err := h.GetSymbols() @@ -343,11 +348,6 @@ func (h *HUOBI) FetchTradablePairs(a asset.Item) ([]string, error) { return nil, err } - format, err := h.GetPairFormat(a, false) - if err != nil { - return nil, err - } - for x := range symbols { if symbols[x].State != "online" { continue @@ -365,10 +365,13 @@ func (h *HUOBI) FetchTradablePairs(a asset.Item) ([]string, error) { for z := range symbols { if symbols[z].ContractStatus == 1 { - pairs = append(pairs, symbols[z].ContractCode) + curr, err := currency.NewPairFromString(symbols[z].ContractCode) + if err != nil { + return nil, err + } + pairs = append(pairs, format.Format(curr)) } } - case asset.Futures: symbols, err := h.FGetContractInfo("", "", currency.Pair{}) if err != nil { @@ -377,7 +380,11 @@ func (h *HUOBI) FetchTradablePairs(a asset.Item) ([]string, error) { for c := range symbols.Data { if symbols.Data[c].ContractStatus == 1 { - pairs = append(pairs, symbols.Data[c].ContractCode) + curr, err := currency.NewPairFromString(symbols.Data[c].ContractCode) + if err != nil { + return nil, err + } + pairs = append(pairs, format.Format(curr)) } } } diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index 96db312c..b8fa2259 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -330,7 +330,10 @@ func (k *Kraken) Run() { // FetchTradablePairs returns a list of the exchanges tradable pairs func (k *Kraken) FetchTradablePairs(assetType asset.Item) ([]string, error) { var products []string - + format, err := k.GetPairFormat(assetType, false) + if err != nil { + return nil, err + } switch assetType { case asset.Spot: if !assetTranslator.Seeded() { @@ -342,10 +345,6 @@ func (k *Kraken) FetchTradablePairs(assetType asset.Item) ([]string, error) { if err != nil { return nil, err } - format, err := k.GetPairFormat(assetType, false) - if err != nil { - return nil, err - } for i := range pairs { if strings.Contains(pairs[i].Altname, ".d") { continue @@ -375,7 +374,11 @@ func (k *Kraken) FetchTradablePairs(assetType asset.Item) ([]string, error) { } for x := range pairs.Instruments { if pairs.Instruments[x].Tradable { - products = append(products, pairs.Instruments[x].Symbol) + curr, err := currency.NewPairFromString(pairs.Instruments[x].Symbol) + if err != nil { + return nil, err + } + products = append(products, format.Format(curr)) } } } diff --git a/exchanges/okex/okex.go b/exchanges/okex/okex.go index dcc00cb8..d04a7f6f 100644 --- a/exchanges/okex/okex.go +++ b/exchanges/okex/okex.go @@ -52,7 +52,7 @@ const ( okGroupPerpTickers = "instruments/ticker" okGroupMarginPairData = "accounts/%s/availability" okGroupMarginPairsData = "accounts/availability" - okGroupSpotPairs = "instruments" + okGroupInstruments = "instruments" ) // OKEX bases all account, spot and margin methods off okgroup implementation @@ -68,6 +68,14 @@ func (o *OKEX) GetSwapMarkets() ([]okgroup.SwapInstrumentsData, error) { nil, &resp, false) } +// GetSwapInstruments gets perpetual swap instruments data +func (o *OKEX) GetSwapInstruments() ([]okgroup.PerpSwapInstrumentData, error) { + var resp []okgroup.PerpSwapInstrumentData + return resp, o.SendHTTPRequest(exchange.RestSpot, http.MethodGet, okGroupSwapSubsection, + okGroupInstruments, + nil, &resp, false) +} + // GetAllMarginRates gets interest rates for all margin currencies on OKEX func (o *OKEX) GetAllMarginRates() ([]okgroup.MarginCurrencyData, error) { var resp []okgroup.MarginCurrencyData @@ -172,7 +180,7 @@ func (o *OKEX) GetMarginRates(instrumentID currency.Pair) (okgroup.MarginCurrenc // GetSpotMarkets gets perpetual swap markets' data func (o *OKEX) GetSpotMarkets() ([]okgroup.TradingPairData, error) { var resp []okgroup.TradingPairData - return resp, o.SendHTTPRequest(exchange.RestSpot, http.MethodGet, okGroupSpotSubsection, okGroupSpotPairs, nil, &resp, false) + return resp, o.SendHTTPRequest(exchange.RestSpot, http.MethodGet, okGroupSpotSubsection, okGroupInstruments, nil, &resp, false) } // GetFundingRate gets funding rate of a given currency diff --git a/exchanges/okex/okex_test.go b/exchanges/okex/okex_test.go index 501faf78..e8e06fc7 100644 --- a/exchanges/okex/okex_test.go +++ b/exchanges/okex/okex_test.go @@ -154,6 +154,14 @@ func TestGetSpotMarkets(t *testing.T) { } } +func TestGetSwapInstruments(t *testing.T) { + t.Parallel() + _, err := o.GetSwapInstruments() + if err != nil { + t.Error(err) + } +} + func TestGetSwapMarkets(t *testing.T) { t.Parallel() _, err := o.GetSwapMarkets() diff --git a/exchanges/okgroup/okgroup_types.go b/exchanges/okgroup/okgroup_types.go index 1614ebd6..f3c2560b 100644 --- a/exchanges/okgroup/okgroup_types.go +++ b/exchanges/okgroup/okgroup_types.go @@ -15,6 +15,25 @@ const ( ImmediateOrCancelOrder ) +// PerpSwapInstrumentData stores instrument data for perpetual swap contracts +type PerpSwapInstrumentData struct { + InstrumentID string `json:"instrument_id"` + UnderlyingIndex string `json:"underlying_index"` + QuoteCurrency string `json:"quote_currency"` + Coin string `json:"coin"` + ContractValue float64 `json:"contract_val,string"` + Listing string `json:"listing"` + Delivery string `json:"delivery"` + SizeIncrement float64 `json:"size_increment,string"` + TickSize float64 `json:"tick_size,string"` + BaseCurrency string `json:"base_currency"` + Underlying string `json:"underlying"` + SettlementCurrency string `json:"settlement_currency"` + IsInverse bool `json:"is_inverse,string"` + Category float64 `json:"category,string"` + ContractValCurrency string `json:"contract_val_currency"` +} + // TradingPairData stores data about a trading pair type TradingPairData struct { BaseCurrency string `json:"base_currency"` diff --git a/exchanges/orderbook/calculator.go b/exchanges/orderbook/calculator.go index 8150ea2e..e763d31b 100644 --- a/exchanges/orderbook/calculator.go +++ b/exchanges/orderbook/calculator.go @@ -224,3 +224,40 @@ func (b *Base) sell(amount float64) (orders orderSummary, quoteAmount float64) { } return } + +// GetAveragePrice finds the average buy or sell price of a specified amount. +// It finds the nominal amount spent on the total purchase or sell and uses it +// to find the average price for an individual unit bought or sold +func (b *Base) GetAveragePrice(buy bool, amount float64) (float64, error) { + if amount <= 0 { + return 0, errAmountInvalid + } + var aggNominalAmount, remainingAmount float64 + if buy { + aggNominalAmount, remainingAmount = b.Asks.FindNominalAmount(amount) + } else { + aggNominalAmount, remainingAmount = b.Bids.FindNominalAmount(amount) + } + if remainingAmount != 0 { + return 0, fmt.Errorf("%w for %v on exchange %v to support a buy amount of %v", errNotEnoughLiquidity, b.Pair, b.Exchange, amount) + } + return aggNominalAmount / amount, nil +} + +// FindNominalAmount finds the nominal amount spent in terms of the quote +// If the orderbook doesn't have enough liquidity it returns a non zero +// remaining amount value +func (elem Items) FindNominalAmount(amount float64) (aggNominalAmount, remainingAmount float64) { + remainingAmount = amount + for x := range elem { + if remainingAmount <= elem[x].Amount { + aggNominalAmount += elem[x].Price * remainingAmount + remainingAmount = 0 + break + } else { + aggNominalAmount += elem[x].Price * elem[x].Amount + remainingAmount -= elem[x].Amount + } + } + return aggNominalAmount, remainingAmount +} diff --git a/exchanges/orderbook/calculator_test.go b/exchanges/orderbook/calculator_test.go index ef5d4b88..c64c2ca2 100644 --- a/exchanges/orderbook/calculator_test.go +++ b/exchanges/orderbook/calculator_test.go @@ -2,6 +2,7 @@ package orderbook import ( "errors" + "math" "testing" "github.com/thrasher-corp/gocryptotrader/currency" @@ -90,3 +91,66 @@ func TestOrderSummary(t *testing.T) { o.Print() } + +func TestGetAveragePrice(t *testing.T) { + var b Base + b.Exchange = "Binance" + cp, err := currency.NewPairFromString("ETH-USDT") + if err != nil { + t.Error(err) + } + b.Pair = cp + b.Bids = []Item{} + _, err = b.GetAveragePrice(false, 5) + if errors.Is(errNotEnoughLiquidity, err) { + t.Error("expected: %w, received %w", errNotEnoughLiquidity, err) + } + b = Base{} + b.Pair = cp + b.Asks = []Item{ + {Amount: 5, Price: 1}, + {Amount: 5, Price: 2}, + {Amount: 5, Price: 3}, + {Amount: 5, Price: 4}, + } + _, err = b.GetAveragePrice(true, -2) + if !errors.Is(err, errAmountInvalid) { + t.Errorf("expected: %v, received %v", errAmountInvalid, err) + } + avgPrice, err := b.GetAveragePrice(true, 15) + if err != nil { + t.Error(err) + } + if avgPrice != 2 { + t.Errorf("avg price calculation failed: expected 2, received %f", avgPrice) + } + avgPrice, err = b.GetAveragePrice(true, 18) + if err != nil { + t.Error(err) + } + if math.Round(avgPrice*1000)/1000 != 2.333 { + t.Errorf("avg price calculation failed: expected 2.333, received %f", math.Round(avgPrice*1000)/1000) + } + _, err = b.GetAveragePrice(true, 25) + if !errors.Is(err, errNotEnoughLiquidity) { + t.Errorf("expected: %v, received %v", errNotEnoughLiquidity, err) + } +} + +func TestFindNominalAmount(t *testing.T) { + b := Items{ + {Amount: 5, Price: 1}, + {Amount: 5, Price: 2}, + {Amount: 5, Price: 3}, + {Amount: 5, Price: 4}, + } + nomAmt, remainingAmt := b.FindNominalAmount(15) + if nomAmt != 30 && remainingAmt != 0 { + t.Errorf("invalid return") + } + b = Items{} + nomAmt, remainingAmt = b.FindNominalAmount(15) + if nomAmt != 0 && remainingAmt != 30 { + t.Errorf("invalid return") + } +} diff --git a/exchanges/orderbook/orderbook_types.go b/exchanges/orderbook/orderbook_types.go index 2e21a6b6..ee80a28c 100644 --- a/exchanges/orderbook/orderbook_types.go +++ b/exchanges/orderbook/orderbook_types.go @@ -31,6 +31,7 @@ var ( errDuplication = errors.New("price duplication") errIDDuplication = errors.New("id duplication") errPeriodUnset = errors.New("funding rate period is unset") + errNotEnoughLiquidity = errors.New("not enough liquidity") ) var service = Service{ diff --git a/testdata/http_mock/binance/binance.json b/testdata/http_mock/binance/binance.json index 65eda425..c6ae7e48 100644 --- a/testdata/http_mock/binance/binance.json +++ b/testdata/http_mock/binance/binance.json @@ -255890,6 +255890,18 @@ } ] }, + "/fapi/v1/time": { + "GET": [ + { + "data": { + "serverTime": 1620099477465 + }, + "queryString": "", + "bodyParams": "", + "headers": {} + } + ] + }, "/fapi/v1/trades": { "GET": [ {