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
This commit is contained in:
Adam
2021-06-29 15:47:03 +10:00
committed by GitHub
parent 0090400eae
commit 81249646da
23 changed files with 564 additions and 136 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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))
}
}
}

View File

@@ -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"}

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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

View File

@@ -66,7 +66,7 @@ func (b *Bitfinex) SetDefaults() {
}
fmt2 := currency.PairStore{
RequestFormat: &currency.PairFormat{Uppercase: true},
RequestFormat: &currency.PairFormat{Uppercase: true, Delimiter: ":"},
ConfigFormat: &currency.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'
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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) {

View File

@@ -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))
}
}
}

View File

@@ -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))
}
}
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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"`

View File

@@ -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
}

View File

@@ -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")
}
}

View File

@@ -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{

View File

@@ -255890,6 +255890,18 @@
}
]
},
"/fapi/v1/time": {
"GET": [
{
"data": {
"serverTime": 1620099477465
},
"queryString": "",
"bodyParams": "",
"headers": {}
}
]
},
"/fapi/v1/trades": {
"GET": [
{