exchanges/qa: Add exchange wrapper testing suite (#1159)

* initial concept of a nice validation tester for exchanges

* adds some datahandler design

* expand testing

* more tests and fixes

* minor end of day fix for bithumb

* fixes implementation issues

* more test coverage and improvements, but not sure if i should continue

* fix more wrapper implementations

* adds error type, more fixes

* changes signature, fixes implementations

* fixes more wrapper implementations

* one more bit

* more cleanup

* WOW things work?

* lintle 1/1337

* mini bump

* fixes all linting

* neaten

* GetOrderInfo+ asset pair fixes+improvements

* adds new websocket test

* expand ws testing

* fix bug, expand tests, improve implementation

* code coverage of a lot of new codes

* fixes everything

* reverts accidental changes

* minor fixes from reviewing code

* removes Bitfinex cancelBatchOrder implementation

* fixes dumb baby typo for babies

* mini nit fixes

* so many nits to address

* addresses all the nits

* Titlecase

* switcheroo

* removes websocket testing for now

* fix appveyor, minor test fix

* fixes typo, re-kindles killed kode

* skip binance wrapper tests when running CI

* expired context, huobi okx fixes

* kodespull

* fix ordering

* time fix because why not

* fix exmo, others

* hopefully this fixes all of my life's problems

* last thing today

* huobi, more like hypotrophy

* golangci-lint, more like mypooroldknee-splint

* fix huobi times by removing them

* should fix okx currency issues

* blocks the application

* adds last little contingency for pairs

* addresses most nits and new problems

* lovely fixed before seeing why okx sucks

* fixes issues with okx websocket

* the classic receieieivaier

* lintle

* adds test and fixes existing tests

* expands error handling messages during setup

* fixes dumb okx bugs introduced

* quick fix for lint and exmo

* fixes nixes

* fix exmo deposit issue

* lint

* fixes issue with extra asset runs missing

* fix surprise race

* all the lint and merge fixes

* fixes surprise bugs in OKx

* fixes issues with times and chains

* fixing all the merge stuff

* merge fix

* rm logs and a panic potential

* lovely lint lament

* an easy demonstration of scenario, but not of initial purpose

* put it in the bin

* Revert "put it in the bin"

This reverts commit 15c6490f713233d43f10957367fcbf18e3818bdd.

* re-add after immediate error popup

* fix mini poor test design

* okx okay

* merge fixes

* fixes issues discovered in lovely test

* I FORGOT TO COMMIT THIS

* nit fixaroonaboo

* forgoetten test fix

* revert old okx asset intrument work

* fixes

* revert problems I didnt understand. update bybit

* fix merge bugs

* test cleanup

* further improvements

* reshuffle and lint

* rm redundant CI_TEST by rm the CI_TEST field that is redundant

* path fix

* move to its own section, dont run on 32 bit + appveyor

* lint

* fix lbank

* address nits

* let it rip

* fix failing test time range

* niteroo boogaloo

* mod tidy, use common.SimpleTimeFormat
This commit is contained in:
Scott
2023-07-03 11:09:43 +10:00
committed by GitHub
parent ef605a3c19
commit fcc5ad4551
210 changed files with 38548 additions and 6519 deletions

View File

@@ -19,7 +19,6 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/log"
)
// Binance is the overarching type across the Binance package
@@ -220,11 +219,7 @@ func (b *Binance) GetHistoricalTrades(ctx context.Context, symbol string, limit
// https://binance-docs.github.io/apidocs/spot/en/#compressed-aggregate-trades-list
func (b *Binance) GetAggregatedTrades(ctx context.Context, arg *AggregatedTradeRequestParams) ([]AggregatedTrade, error) {
params := url.Values{}
symbol, err := b.FormatSymbol(arg.Symbol, asset.Spot)
if err != nil {
return nil, err
}
params.Set("symbol", symbol)
params.Set("symbol", arg.Symbol.String())
// if the user request is directly not supported by the exchange, we might be able to fulfill it
// by merging results from multiple API requests
needBatch := false
@@ -296,8 +291,7 @@ func (b *Binance) batchAggregateTrades(ctx context.Context, arg *AggregatedTrade
err := b.SendHTTPRequest(ctx,
exchange.RestSpotSupplementary, path, spotDefaultRate, &resp)
if err != nil {
log.Warnln(log.ExchangeSys, err.Error())
return resp, err
return resp, fmt.Errorf("%w %v", err, arg.Symbol)
}
}
fromID = resp[len(resp)-1].ATradeID
@@ -318,7 +312,7 @@ func (b *Binance) batchAggregateTrades(ctx context.Context, arg *AggregatedTrade
spotDefaultRate,
&additionalTrades)
if err != nil {
return resp, err
return resp, fmt.Errorf("%w %v", err, arg.Symbol)
}
lastIndex := len(additionalTrades)
if !arg.EndTime.IsZero() {
@@ -382,14 +376,14 @@ func (b *Binance) GetSpotKline(ctx context.Context, arg *KlinesRequestParams) ([
}
responseData, ok := resp.([]interface{})
if !ok {
return nil, errors.New("unable to type assert responseData")
return nil, common.GetTypeAssertError("[]interface{}", resp)
}
klineData := make([]CandleStick, len(responseData))
for x := range responseData {
individualData, ok := responseData[x].([]interface{})
if !ok {
return nil, errors.New("unable to type assert individualData")
return nil, common.GetTypeAssertError("[]interface{}", responseData[x])
}
if len(individualData) != 12 {
return nil, errors.New("unexpected kline data length")
@@ -420,7 +414,7 @@ func (b *Binance) GetSpotKline(ctx context.Context, arg *KlinesRequestParams) ([
return nil, err
}
if candle.TradeCount, ok = individualData[8].(float64); !ok {
return nil, errors.New("unable to type assert trade count")
return nil, common.GetTypeAssertError("float64", individualData[8])
}
if candle.TakerBuyAssetVolume, err = convert.FloatFromString(individualData[9]); err != nil {
return nil, err
@@ -751,7 +745,7 @@ func (b *Binance) SendHTTPRequest(ctx context.Context, ePath exchange.URL, path
return b.SendPayload(ctx, f, func() (*request.Item, error) {
return item, nil
})
}, request.UnauthenticatedRequest)
}
// SendAPIKeyHTTPRequest is a special API request where the api key is
@@ -780,7 +774,7 @@ func (b *Binance) SendAPIKeyHTTPRequest(ctx context.Context, ePath exchange.URL,
return b.SendPayload(ctx, f, func() (*request.Item, error) {
return item, nil
})
}, request.AuthenticatedRequest)
}
// SendAuthHTTPRequest sends an authenticated HTTP request
@@ -825,11 +819,10 @@ func (b *Binance) SendAuthHTTPRequest(ctx context.Context, ePath exchange.URL, m
Path: fullPath,
Headers: headers,
Result: &interim,
AuthRequest: true,
Verbose: b.Verbose,
HTTPDebugging: b.HTTPDebugging,
HTTPRecording: b.HTTPRecording}, nil
})
}, request.AuthenticatedRequest)
if err != nil {
return err
}
@@ -1115,7 +1108,6 @@ func (b *Binance) GetWsAuthStreamKey(ctx context.Context) (string, error) {
Path: endpointPath + userAccountStream,
Headers: headers,
Result: &resp,
AuthRequest: true,
Verbose: b.Verbose,
HTTPDebugging: b.HTTPDebugging,
HTTPRecording: b.HTTPRecording,
@@ -1123,7 +1115,7 @@ func (b *Binance) GetWsAuthStreamKey(ctx context.Context) (string, error) {
err = b.SendPayload(ctx, request.Unset, func() (*request.Item, error) {
return item, nil
})
}, request.AuthenticatedRequest)
if err != nil {
return "", err
}
@@ -1156,7 +1148,6 @@ func (b *Binance) MaintainWsAuthStreamKey(ctx context.Context) error {
Method: http.MethodPut,
Path: path,
Headers: headers,
AuthRequest: true,
Verbose: b.Verbose,
HTTPDebugging: b.HTTPDebugging,
HTTPRecording: b.HTTPRecording,
@@ -1164,7 +1155,7 @@ func (b *Binance) MaintainWsAuthStreamKey(ctx context.Context) error {
return b.SendPayload(ctx, request.Unset, func() (*request.Item, error) {
return item, nil
})
}, request.AuthenticatedRequest)
}
// FetchSpotExchangeLimits fetches spot order execution limits

View File

@@ -6,6 +6,7 @@
package binance
import (
"context"
"log"
"os"
"testing"
@@ -40,5 +41,9 @@ func TestMain(m *testing.M) {
request.MaxRequestJobs = 100
b.Websocket.DataHandler = sharedtestvalues.GetWebsocketInterfaceChannelOverride()
log.Printf(sharedtestvalues.LiveTesting, b.Name)
err = b.UpdateTradablePairs(context.Background(), true)
if err != nil {
log.Fatal("Binance setup error", err)
}
os.Exit(m.Run())
}

View File

@@ -6,6 +6,7 @@
package binance
import (
"context"
"log"
"os"
"testing"
@@ -60,5 +61,9 @@ func TestMain(m *testing.M) {
}
request.MaxRequestJobs = 100
log.Printf(sharedtestvalues.MockTesting, b.Name)
err = b.UpdateTradablePairs(context.Background(), true)
if err != nil {
log.Fatal(err)
}
os.Exit(m.Run())
}

View File

@@ -1411,7 +1411,7 @@ func TestGetActiveOrders(t *testing.T) {
if err != nil {
t.Error(err)
}
var getOrdersRequest = order.GetOrdersRequest{
var getOrdersRequest = order.MultiOrderRequest{
Type: order.AnyType,
Pairs: currency.Pairs{pair},
AssetType: asset.Spot,
@@ -1432,7 +1432,7 @@ func TestGetActiveOrders(t *testing.T) {
func TestGetOrderHistory(t *testing.T) {
t.Parallel()
var getOrdersRequest = order.GetOrdersRequest{
var getOrdersRequest = order.MultiOrderRequest{
Type: order.AnyType,
AssetType: asset.Spot,
Side: order.AnySide,
@@ -1605,7 +1605,7 @@ func TestGetAggregatedTradesBatched(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if tt.mock != mockTests {
t.Skip()
t.Skip("mock mismatch, skipping")
}
result, err := b.GetAggregatedTrades(context.Background(), tt.args)
if err != nil {
@@ -1783,7 +1783,7 @@ func TestWrapperGetActiveOrders(t *testing.T) {
if err != nil {
t.Error(err)
}
_, err = b.GetActiveOrders(context.Background(), &order.GetOrdersRequest{
_, err = b.GetActiveOrders(context.Background(), &order.MultiOrderRequest{
Type: order.AnyType,
Side: order.AnySide,
Pairs: currency.Pairs{p},
@@ -1797,7 +1797,7 @@ func TestWrapperGetActiveOrders(t *testing.T) {
if err != nil {
t.Error(err)
}
_, err = b.GetActiveOrders(context.Background(), &order.GetOrdersRequest{
_, err = b.GetActiveOrders(context.Background(), &order.MultiOrderRequest{
Type: order.AnyType,
Side: order.AnySide,
Pairs: currency.Pairs{p2},
@@ -1815,12 +1815,12 @@ func TestWrapperGetOrderHistory(t *testing.T) {
if err != nil {
t.Error(err)
}
_, err = b.GetOrderHistory(context.Background(), &order.GetOrdersRequest{
Type: order.AnyType,
Side: order.AnySide,
OrderID: "123",
Pairs: currency.Pairs{p},
AssetType: asset.CoinMarginedFutures,
_, err = b.GetOrderHistory(context.Background(), &order.MultiOrderRequest{
Type: order.AnyType,
Side: order.AnySide,
FromOrderID: "123",
Pairs: currency.Pairs{p},
AssetType: asset.CoinMarginedFutures,
})
if err != nil {
t.Error(err)
@@ -1830,18 +1830,18 @@ func TestWrapperGetOrderHistory(t *testing.T) {
if err != nil {
t.Error(err)
}
_, err = b.GetOrderHistory(context.Background(), &order.GetOrdersRequest{
Type: order.AnyType,
Side: order.AnySide,
OrderID: "123",
Pairs: currency.Pairs{p2},
AssetType: asset.USDTMarginedFutures,
_, err = b.GetOrderHistory(context.Background(), &order.MultiOrderRequest{
Type: order.AnyType,
Side: order.AnySide,
FromOrderID: "123",
Pairs: currency.Pairs{p2},
AssetType: asset.USDTMarginedFutures,
})
if err != nil {
t.Error(err)
}
_, err = b.GetOrderHistory(context.Background(), &order.GetOrdersRequest{
_, err = b.GetOrderHistory(context.Background(), &order.MultiOrderRequest{
AssetType: asset.USDTMarginedFutures,
})
if err == nil {
@@ -1856,13 +1856,13 @@ func TestCancelOrder(t *testing.T) {
if err != nil {
t.Error(err)
}
fpair, err := b.FormatExchangeCurrency(p, asset.CoinMarginedFutures)
fPair, err := b.FormatExchangeCurrency(p, asset.CoinMarginedFutures)
if err != nil {
t.Error(err)
}
err = b.CancelOrder(context.Background(), &order.Cancel{
AssetType: asset.CoinMarginedFutures,
Pair: fpair,
Pair: fPair,
OrderID: "1234",
})
if err != nil {
@@ -2313,18 +2313,29 @@ func TestExecutionTypeToOrderStatus(t *testing.T) {
func TestGetHistoricCandles(t *testing.T) {
t.Parallel()
startTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
end := startTime.Add(time.Hour * 24 * 7)
bAssets := b.GetAssetTypes(false)
for i := range bAssets {
cps, err := b.GetAvailablePairs(bAssets[i])
if err != nil {
t.Error(err)
}
err = b.CurrencyPairs.EnablePair(bAssets[i], cps[0])
if err != nil && !errors.Is(err, currency.ErrPairAlreadyEnabled) {
t.Fatal(err)
}
_, err = b.GetHistoricCandles(context.Background(), cps[0], bAssets[i], kline.OneDay, startTime, end)
if err != nil {
t.Error(err)
}
}
pair, err := currency.NewPairFromString("BTC-USDT")
if err != nil {
t.Fatal(err)
}
startTime := time.Unix(1546300800, 0)
end := time.Unix(1577836799, 0)
_, err = b.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.OneDay, startTime, end)
if err != nil {
t.Error(err)
}
startTime = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)
_, err = b.GetHistoricCandles(context.Background(), pair, asset.Spot, kline.Interval(time.Hour*7), startTime, end)
if !errors.Is(err, kline.ErrRequestExceedsExchangeLimits) {
t.Fatalf("received: '%v', but expected: '%v'", err, kline.ErrRequestExceedsExchangeLimits)
@@ -2333,17 +2344,22 @@ func TestGetHistoricCandles(t *testing.T) {
func TestGetHistoricCandlesExtended(t *testing.T) {
t.Parallel()
pair, err := currency.NewPairFromString("BTC-USDT")
if err != nil {
t.Fatal(err)
}
startTime := time.Date(2020, 9, 1, 0, 0, 0, 0, time.UTC)
end := time.Date(2021, 2, 15, 0, 0, 0, 0, time.UTC)
_, err = b.GetHistoricCandlesExtended(context.Background(), pair, asset.Spot, kline.OneDay, startTime, end)
if err != nil {
t.Error(err)
startTime := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC)
end := startTime.Add(time.Hour * 24 * 7)
bAssets := b.GetAssetTypes(false)
for i := range bAssets {
cps, err := b.GetAvailablePairs(bAssets[i])
if err != nil {
t.Error(err)
}
err = b.CurrencyPairs.EnablePair(bAssets[i], cps[0])
if err != nil && !errors.Is(err, currency.ErrPairAlreadyEnabled) {
t.Fatal(err)
}
_, err = b.GetHistoricCandlesExtended(context.Background(), cps[0], bAssets[i], kline.OneDay, startTime, end)
if err != nil {
t.Error(err)
}
}
}
@@ -2390,14 +2406,17 @@ func TestBinance_FormatExchangeKlineInterval(t *testing.T) {
func TestGetRecentTrades(t *testing.T) {
t.Parallel()
currencyPair, err := currency.NewPairFromString("BTCUSDT")
if err != nil {
t.Fatal(err)
}
_, err = b.GetRecentTrades(context.Background(),
currencyPair, asset.Spot)
if err != nil {
t.Error(err)
bAssets := b.GetAssetTypes(false)
for i := range bAssets {
cps, err := b.GetAvailablePairs(bAssets[i])
if err != nil {
t.Error(err)
}
_, err = b.GetRecentTrades(context.Background(),
cps[0], bAssets[i])
if err != nil {
t.Error(err)
}
}
}
@@ -2428,7 +2447,7 @@ func TestGenerateSubscriptions(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if len(subs) != 8 {
if len(subs) == 0 {
t.Fatal("unexpected subscription length")
}
}

View File

@@ -723,18 +723,18 @@ type WithdrawResponse struct {
// WithdrawStatusResponse defines a withdrawal status response
type WithdrawStatusResponse struct {
Address string `json:"address"`
Amount float64 `json:"amount,string"`
ApplyTime string `json:"applyTime"`
Coin string `json:"coin"`
ID string `json:"id"`
WithdrawOrderID string `json:"withdrawOrderId"`
Network string `json:"network"`
TransferType uint8 `json:"transferType"`
Status int64 `json:"status"`
TransactionFee float64 `json:"transactionFee,string"`
TransactionID string `json:"txId"`
ConfirmNumber int64 `json:"confirmNo"`
Address string `json:"address"`
Amount float64 `json:"amount,string"`
ApplyTime binanceTime `json:"applyTime"`
Coin string `json:"coin"`
ID string `json:"id"`
WithdrawOrderID string `json:"withdrawOrderId"`
Network string `json:"network"`
TransferType uint8 `json:"transferType"`
Status int64 `json:"status"`
TransactionFee float64 `json:"transactionFee,string"`
TransactionID string `json:"txId"`
ConfirmNumber int64 `json:"confirmNo"`
}
// DepositAddress stores the deposit address info

View File

@@ -354,7 +354,7 @@ func (b *Binance) Run(ctx context.Context) {
// FetchTradablePairs returns a list of the exchanges tradable pairs
func (b *Binance) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.Pairs, error) {
if !b.SupportsAsset(a) {
return nil, fmt.Errorf("asset type of %s is not supported by %s", a, b.Name)
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a)
}
tradingStatus := "TRADING"
var pairs []currency.Pair
@@ -438,7 +438,7 @@ func (b *Binance) UpdateTradablePairs(ctx context.Context, forceUpdate bool) err
return err
}
}
return nil
return b.EnsureOnePairEnabled()
}
// UpdateTickers updates the ticker for all currency pairs of a given asset type
@@ -540,13 +540,16 @@ func (b *Binance) UpdateTickers(ctx context.Context, a asset.Item) error {
}
}
default:
return fmt.Errorf("assetType not supported: %v", a)
return fmt.Errorf("%w %v", asset.ErrNotSupported, a)
}
return nil
}
// UpdateTicker updates and returns the ticker for a currency pair
func (b *Binance) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error) {
if p.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
switch a {
case asset.Spot, asset.Margin:
tick, err := b.GetPriceChangeStats(ctx, p)
@@ -612,7 +615,7 @@ func (b *Binance) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Ite
}
default:
return nil, fmt.Errorf("assetType not supported: %v", a)
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a)
}
return ticker.GetTicker(b.Name, p, a)
}
@@ -644,6 +647,12 @@ func (b *Binance) FetchOrderbook(ctx context.Context, p currency.Pair, assetType
// UpdateOrderbook updates and returns the orderbook for a currency pair
func (b *Binance) UpdateOrderbook(ctx context.Context, p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
if p.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
if err := b.CurrencyPairs.IsAssetEnabled(assetType); err != nil {
return nil, err
}
book := &orderbook.Base{
Exchange: b.Name,
Pair: p,
@@ -652,6 +661,7 @@ func (b *Binance) UpdateOrderbook(ctx context.Context, p currency.Pair, assetTyp
}
var orderbookNew *OrderBook
var err error
switch assetType {
case asset.Spot, asset.Margin:
orderbookNew, err = b.GetOrderBook(ctx,
@@ -778,7 +788,7 @@ func (b *Binance) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (
acc.Currencies = currencyDetails
default:
return info, fmt.Errorf("%v assetType not supported", assetType)
return info, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType)
}
acc.AssetType = assetType
info.Accounts = append(info.Accounts, acc)
@@ -805,61 +815,101 @@ func (b *Binance) FetchAccountInfo(ctx context.Context, assetType asset.Item) (a
return acc, nil
}
// GetFundingHistory returns funding history, deposits and
// GetAccountFundingHistory returns funding history, deposits and
// withdrawals
func (b *Binance) GetFundingHistory(_ context.Context) ([]exchange.FundHistory, error) {
func (b *Binance) GetAccountFundingHistory(_ context.Context) ([]exchange.FundingHistory, error) {
return nil, common.ErrFunctionNotSupported
}
// GetWithdrawalsHistory returns previous withdrawals data
func (b *Binance) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _ asset.Item) (resp []exchange.WithdrawalHistory, err error) {
w, err := b.WithdrawHistory(ctx, c, "", time.Time{}, time.Time{}, 0, 10000)
func (b *Binance) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _ asset.Item) ([]exchange.WithdrawalHistory, error) {
withdrawals, err := b.WithdrawHistory(ctx, c, "", time.Time{}, time.Time{}, 0, 10000)
if err != nil {
return nil, err
}
for i := range w {
tm, err := time.Parse(binanceSAPITimeLayout, w[i].ApplyTime)
if err != nil {
return nil, err
resp := make([]exchange.WithdrawalHistory, len(withdrawals))
for i := range withdrawals {
resp[i] = exchange.WithdrawalHistory{
Status: strconv.FormatInt(withdrawals[i].Status, 10),
TransferID: withdrawals[i].ID,
Currency: withdrawals[i].Coin,
Amount: withdrawals[i].Amount,
Fee: withdrawals[i].TransactionFee,
CryptoToAddress: withdrawals[i].Address,
CryptoTxID: withdrawals[i].TransactionID,
CryptoChain: withdrawals[i].Network,
Timestamp: withdrawals[i].ApplyTime.Time(),
}
resp = append(resp, exchange.WithdrawalHistory{
Status: strconv.FormatInt(w[i].Status, 10),
TransferID: w[i].ID,
Currency: w[i].Coin,
Amount: w[i].Amount,
Fee: w[i].TransactionFee,
CryptoToAddress: w[i].Address,
CryptoTxID: w[i].TransactionID,
CryptoChain: w[i].Network,
Timestamp: tm,
})
}
return resp, nil
}
// GetRecentTrades returns the most recent trades for a currency and asset
func (b *Binance) GetRecentTrades(ctx context.Context, p currency.Pair, assetType asset.Item) ([]trade.Data, error) {
func (b *Binance) GetRecentTrades(ctx context.Context, p currency.Pair, a asset.Item) ([]trade.Data, error) {
const limit = 1000
tradeData, err := b.GetMostRecentTrades(ctx,
RecentTradeRequestParams{p, limit})
rFmt, err := b.GetPairFormat(a, true)
if err != nil {
return nil, err
}
pFmt := p.Format(rFmt)
resp := make([]trade.Data, 0, limit)
switch a {
case asset.Spot:
tradeData, err := b.GetMostRecentTrades(ctx,
RecentTradeRequestParams{pFmt, limit})
if err != nil {
return nil, err
}
resp := make([]trade.Data, len(tradeData))
for i := range tradeData {
resp[i] = trade.Data{
TID: strconv.FormatInt(tradeData[i].ID, 10),
Exchange: b.Name,
CurrencyPair: p,
AssetType: assetType,
Price: tradeData[i].Price,
Amount: tradeData[i].Quantity,
Timestamp: tradeData[i].Time,
for i := range tradeData {
resp = append(resp, trade.Data{
TID: strconv.FormatInt(tradeData[i].ID, 10),
Exchange: b.Name,
CurrencyPair: p,
AssetType: a,
Price: tradeData[i].Price,
Amount: tradeData[i].Quantity,
Timestamp: tradeData[i].Time,
})
}
case asset.USDTMarginedFutures:
tradeData, err := b.URecentTrades(ctx, pFmt, "", limit)
if err != nil {
return nil, err
}
for i := range tradeData {
resp = append(resp, trade.Data{
TID: strconv.FormatInt(tradeData[i].ID, 10),
Exchange: b.Name,
CurrencyPair: p,
AssetType: a,
Price: tradeData[i].Price,
Amount: tradeData[i].Qty,
Timestamp: tradeData[i].Time.Time(),
})
}
case asset.CoinMarginedFutures:
tradeData, err := b.GetFuturesPublicTrades(ctx, pFmt, limit)
if err != nil {
return nil, err
}
for i := range tradeData {
resp = append(resp, trade.Data{
TID: strconv.FormatInt(tradeData[i].ID, 10),
Exchange: b.Name,
CurrencyPair: p,
AssetType: a,
Price: tradeData[i].Price,
Amount: tradeData[i].Qty,
Timestamp: tradeData[i].Time.Time(),
})
}
}
if b.IsSaveTradeDataEnabled() {
err := trade.AddTradesToBuffer(b.Name, resp...)
if err != nil {
@@ -873,37 +923,42 @@ func (b *Binance) GetRecentTrades(ctx context.Context, p currency.Pair, assetTyp
// GetHistoricTrades returns historic trade data within the timeframe provided
func (b *Binance) GetHistoricTrades(ctx context.Context, p currency.Pair, a asset.Item, from, to time.Time) ([]trade.Data, error) {
if err := b.CurrencyPairs.IsAssetEnabled(a); err != nil {
return nil, err
}
if a != asset.Spot {
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a)
}
rFmt, err := b.GetPairFormat(a, true)
if err != nil {
return nil, err
}
pFmt := p.Format(rFmt)
req := AggregatedTradeRequestParams{
Symbol: p,
Symbol: pFmt,
StartTime: from,
EndTime: to,
}
trades, err := b.GetAggregatedTrades(ctx, &req)
if err != nil {
return nil, err
return nil, fmt.Errorf("%w %v", err, pFmt)
}
result := make([]trade.Data, len(trades))
exName := b.GetName()
for i := range trades {
t := trades[i].toTradeData(p, exName, a)
result[i] = *t
result[i] = trade.Data{
CurrencyPair: p,
TID: strconv.FormatInt(trades[i].ATradeID, 10),
Amount: trades[i].Quantity,
Exchange: b.Name,
Price: trades[i].Price,
Timestamp: trades[i].TimeStamp,
AssetType: a,
Side: order.AnySide,
}
}
return result, nil
}
func (a *AggregatedTrade) toTradeData(p currency.Pair, exchange string, aType asset.Item) *trade.Data {
return &trade.Data{
CurrencyPair: p,
TID: strconv.FormatInt(a.ATradeID, 10),
Amount: a.Quantity,
Exchange: exchange,
Price: a.Price,
Timestamp: a.TimeStamp,
AssetType: aType,
Side: order.AnySide,
}
}
// SubmitOrder submits a new order
func (b *Binance) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) {
if err := s.Validate(); err != nil {
@@ -933,7 +988,7 @@ func (b *Binance) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Subm
}
requestParamsOrderType = BinanceRequestParamsOrderLimit
default:
return nil, errors.New("unsupported order type")
return nil, fmt.Errorf("%w %v", order.ErrUnsupportedOrderType, s.Type)
}
var orderRequest = NewOrderRequest{
@@ -1046,7 +1101,7 @@ func (b *Binance) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Subm
default:
return nil, errors.New("invalid type, check api docs for updates")
}
order, err := b.UFuturesNewOrder(ctx,
o, err := b.UFuturesNewOrder(ctx,
&UFuturesNewOrderRequest{
Symbol: s.Pair,
Side: reqSide,
@@ -1061,9 +1116,9 @@ func (b *Binance) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Subm
if err != nil {
return nil, err
}
orderID = strconv.FormatInt(order.OrderID, 10)
orderID = strconv.FormatInt(o.OrderID, 10)
default:
return nil, fmt.Errorf("assetType not supported")
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, s.AssetType)
}
resp, err := s.DeriveSubmitResponse(orderID)
@@ -1114,8 +1169,8 @@ func (b *Binance) CancelOrder(ctx context.Context, o *order.Cancel) error {
}
// CancelBatchOrders cancels an orders by their corresponding ID numbers
func (b *Binance) CancelBatchOrders(_ context.Context, _ []order.Cancel) (order.CancelBatchResponse, error) {
return order.CancelBatchResponse{}, common.ErrNotYetImplemented
func (b *Binance) CancelBatchOrders(_ context.Context, _ []order.Cancel) (*order.CancelBatchResponse, error) {
return nil, common.ErrFunctionNotSupported
}
// CancelAllOrders cancels all orders associated with a currency pair
@@ -1177,28 +1232,35 @@ func (b *Binance) CancelAllOrders(ctx context.Context, req *order.Cancel) (order
}
}
default:
return cancelAllOrdersResponse, fmt.Errorf("assetType not supported: %v", req.AssetType)
return cancelAllOrdersResponse, fmt.Errorf("%w %v", asset.ErrNotSupported, req.AssetType)
}
return cancelAllOrdersResponse, nil
}
// GetOrderInfo returns information on a current open order
func (b *Binance) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, assetType asset.Item) (order.Detail, error) {
func (b *Binance) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, assetType asset.Item) (*order.Detail, error) {
if pair.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
if err := b.CurrencyPairs.IsAssetEnabled(assetType); err != nil {
return nil, err
}
var respData order.Detail
orderIDInt, err := strconv.ParseInt(orderID, 10, 64)
if err != nil {
return respData, err
return nil, err
}
switch assetType {
case asset.Spot:
resp, err := b.QueryOrder(ctx, pair, "", orderIDInt)
if err != nil {
return respData, err
return nil, err
}
var side order.Side
side, err = order.StringToOrderSide(resp.Side)
if err != nil {
return respData, err
return nil, err
}
status, err := order.StringToOrderStatus(resp.Status)
if err != nil {
@@ -1209,7 +1271,7 @@ func (b *Binance) GetOrderInfo(ctx context.Context, orderID string, pair currenc
orderType = order.Market
}
return order.Detail{
return &order.Detail{
Amount: resp.OrigQty,
Exchange: b.Name,
OrderID: strconv.FormatInt(resp.OrderID, 10),
@@ -1228,7 +1290,7 @@ func (b *Binance) GetOrderInfo(ctx context.Context, orderID string, pair currenc
case asset.CoinMarginedFutures:
orderData, err := b.FuturesOpenOrderData(ctx, pair, orderID, "")
if err != nil {
return respData, err
return nil, err
}
var feeBuilder exchange.FeeBuilder
feeBuilder.Amount = orderData.ExecutedQuantity
@@ -1236,7 +1298,7 @@ func (b *Binance) GetOrderInfo(ctx context.Context, orderID string, pair currenc
feeBuilder.Pair = pair
fee, err := b.GetFee(ctx, &feeBuilder)
if err != nil {
return respData, err
return nil, err
}
orderVars := compatibleOrderVars(orderData.Side, orderData.Status, orderData.OrderType)
respData.Amount = orderData.OriginalQuantity
@@ -1257,7 +1319,7 @@ func (b *Binance) GetOrderInfo(ctx context.Context, orderID string, pair currenc
case asset.USDTMarginedFutures:
orderData, err := b.UGetOrderData(ctx, pair, orderID, "")
if err != nil {
return respData, err
return nil, err
}
var feeBuilder exchange.FeeBuilder
feeBuilder.Amount = orderData.ExecutedQuantity
@@ -1265,7 +1327,7 @@ func (b *Binance) GetOrderInfo(ctx context.Context, orderID string, pair currenc
feeBuilder.Pair = pair
fee, err := b.GetFee(ctx, &feeBuilder)
if err != nil {
return respData, err
return nil, err
}
orderVars := compatibleOrderVars(orderData.Side, orderData.Status, orderData.OrderType)
respData.Amount = orderData.OriginalQuantity
@@ -1284,9 +1346,9 @@ func (b *Binance) GetOrderInfo(ctx context.Context, orderID string, pair currenc
respData.Date = orderData.Time
respData.LastUpdated = orderData.UpdateTime
default:
return respData, fmt.Errorf("assetType %s not supported", assetType)
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, assetType)
}
return respData, nil
return &respData, nil
}
// GetDepositAddress returns a deposit address for a specified currency
@@ -1351,7 +1413,7 @@ func (b *Binance) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuil
}
// GetActiveOrders retrieves any orders that are active/open
func (b *Binance) GetActiveOrders(ctx context.Context, req *order.GetOrdersRequest) (order.FilteredOrders, error) {
func (b *Binance) GetActiveOrders(ctx context.Context, req *order.MultiOrderRequest) (order.FilteredOrders, error) {
err := req.Validate()
if err != nil {
return nil, err
@@ -1465,7 +1527,7 @@ func (b *Binance) GetActiveOrders(ctx context.Context, req *order.GetOrdersReque
})
}
default:
return orders, fmt.Errorf("assetType not supported")
return orders, fmt.Errorf("%w %v", asset.ErrNotSupported, req.AssetType)
}
}
return req.Filter(b.Name, orders), nil
@@ -1473,7 +1535,7 @@ func (b *Binance) GetActiveOrders(ctx context.Context, req *order.GetOrdersReque
// GetOrderHistory retrieves account order information
// Can Limit response to specific order status
func (b *Binance) GetOrderHistory(ctx context.Context, req *order.GetOrdersRequest) (order.FilteredOrders, error) {
func (b *Binance) GetOrderHistory(ctx context.Context, req *order.MultiOrderRequest) (order.FilteredOrders, error) {
err := req.Validate()
if err != nil {
return nil, err
@@ -1544,7 +1606,7 @@ func (b *Binance) GetOrderHistory(ctx context.Context, req *order.GetOrdersReque
var orderHistory []FuturesOrderData
var err error
switch {
case !req.StartTime.IsZero() && !req.EndTime.IsZero() && req.OrderID == "":
case !req.StartTime.IsZero() && !req.EndTime.IsZero() && req.FromOrderID == "":
if req.EndTime.Before(req.StartTime) {
return nil, errors.New("endTime cannot be before startTime")
}
@@ -1556,8 +1618,8 @@ func (b *Binance) GetOrderHistory(ctx context.Context, req *order.GetOrdersReque
if err != nil {
return nil, err
}
case req.OrderID != "" && req.StartTime.IsZero() && req.EndTime.IsZero():
fromID, err := strconv.ParseInt(req.OrderID, 10, 64)
case req.FromOrderID != "" && req.StartTime.IsZero() && req.EndTime.IsZero():
fromID, err := strconv.ParseInt(req.FromOrderID, 10, 64)
if err != nil {
return nil, err
}
@@ -1602,7 +1664,7 @@ func (b *Binance) GetOrderHistory(ctx context.Context, req *order.GetOrdersReque
var orderHistory []UFuturesOrderData
var err error
switch {
case !req.StartTime.IsZero() && !req.EndTime.IsZero() && req.OrderID == "":
case !req.StartTime.IsZero() && !req.EndTime.IsZero() && req.FromOrderID == "":
if req.EndTime.Before(req.StartTime) {
return nil, errors.New("endTime cannot be before startTime")
}
@@ -1614,8 +1676,8 @@ func (b *Binance) GetOrderHistory(ctx context.Context, req *order.GetOrdersReque
if err != nil {
return nil, err
}
case req.OrderID != "" && req.StartTime.IsZero() && req.EndTime.IsZero():
fromID, err := strconv.ParseInt(req.OrderID, 10, 64)
case req.FromOrderID != "" && req.StartTime.IsZero() && req.EndTime.IsZero():
fromID, err := strconv.ParseInt(req.FromOrderID, 10, 64)
if err != nil {
return nil, err
}
@@ -1656,7 +1718,7 @@ func (b *Binance) GetOrderHistory(ctx context.Context, req *order.GetOrdersReque
}
}
default:
return orders, fmt.Errorf("assetType not supported")
return orders, fmt.Errorf("%w %v", asset.ErrNotSupported, req.AssetType)
}
return req.Filter(b.Name, orders), nil
}
@@ -1691,31 +1753,74 @@ func (b *Binance) GetHistoricCandles(ctx context.Context, pair currency.Pair, a
return nil, err
}
if a != asset.Spot {
// TODO: Add support for other asset types.
return nil, common.ErrNotYetImplemented
}
candles, err := b.GetSpotKline(ctx, &KlinesRequestParams{
Interval: b.FormatExchangeKlineInterval(req.ExchangeInterval),
Symbol: req.Pair,
StartTime: req.Start,
EndTime: req.End,
Limit: int(req.RequestLimit),
})
if err != nil {
return nil, err
}
timeSeries := make([]kline.Candle, len(candles))
for x := range candles {
timeSeries[x] = kline.Candle{
Time: candles[x].OpenTime,
Open: candles[x].Open,
High: candles[x].High,
Low: candles[x].Low,
Close: candles[x].Close,
Volume: candles[x].Volume,
timeSeries := make([]kline.Candle, 0, req.Size())
switch a {
case asset.Spot, asset.Margin:
var candles []CandleStick
candles, err = b.GetSpotKline(ctx, &KlinesRequestParams{
Interval: b.FormatExchangeKlineInterval(req.ExchangeInterval),
Symbol: req.Pair,
StartTime: req.Start,
EndTime: req.End,
Limit: int(req.RequestLimit),
})
if err != nil {
return nil, err
}
for i := range candles {
timeSeries = append(timeSeries, kline.Candle{
Time: candles[i].OpenTime,
Open: candles[i].Open,
High: candles[i].High,
Low: candles[i].Low,
Close: candles[i].Close,
Volume: candles[i].Volume,
})
}
case asset.USDTMarginedFutures:
var candles []FuturesCandleStick
candles, err = b.UKlineData(ctx,
req.RequestFormatted,
b.FormatExchangeKlineInterval(interval),
req.RequestLimit,
req.Start,
req.End)
if err != nil {
return nil, err
}
for i := range candles {
timeSeries = append(timeSeries, kline.Candle{
Time: candles[i].OpenTime,
Open: candles[i].Open,
High: candles[i].High,
Low: candles[i].Low,
Close: candles[i].Close,
Volume: candles[i].Volume,
})
}
case asset.CoinMarginedFutures:
var candles []FuturesCandleStick
candles, err = b.GetFuturesKlineData(ctx,
req.RequestFormatted,
b.FormatExchangeKlineInterval(interval),
req.RequestLimit,
req.Start,
req.End)
if err != nil {
return nil, err
}
for i := range candles {
timeSeries = append(timeSeries, kline.Candle{
Time: candles[i].OpenTime,
Open: candles[i].Open,
High: candles[i].High,
Low: candles[i].Low,
Close: candles[i].Close,
Volume: candles[i].Volume,
})
}
default:
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a)
}
return req.ProcessResponse(timeSeries)
}
@@ -1728,34 +1833,75 @@ func (b *Binance) GetHistoricCandlesExtended(ctx context.Context, pair currency.
return nil, err
}
if a != asset.Spot {
// TODO: Add support for other asset types.
return nil, common.ErrNotYetImplemented
}
timeSeries := make([]kline.Candle, 0, req.Size())
for x := range req.RangeHolder.Ranges {
var candles []CandleStick
candles, err = b.GetSpotKline(ctx, &KlinesRequestParams{
Interval: b.FormatExchangeKlineInterval(req.ExchangeInterval),
Symbol: req.Pair,
StartTime: req.RangeHolder.Ranges[x].Start.Time,
EndTime: req.RangeHolder.Ranges[x].End.Time,
Limit: int(req.RequestLimit),
})
if err != nil {
return nil, err
}
for i := range candles {
timeSeries = append(timeSeries, kline.Candle{
Time: candles[i].OpenTime,
Open: candles[i].Open,
High: candles[i].High,
Low: candles[i].Low,
Close: candles[i].Close,
Volume: candles[i].Volume,
switch a {
case asset.Spot, asset.Margin:
var candles []CandleStick
candles, err = b.GetSpotKline(ctx, &KlinesRequestParams{
Interval: b.FormatExchangeKlineInterval(req.ExchangeInterval),
Symbol: req.Pair,
StartTime: req.RangeHolder.Ranges[x].Start.Time,
EndTime: req.RangeHolder.Ranges[x].End.Time,
Limit: int(req.RequestLimit),
})
if err != nil {
return nil, err
}
for i := range candles {
timeSeries = append(timeSeries, kline.Candle{
Time: candles[i].OpenTime,
Open: candles[i].Open,
High: candles[i].High,
Low: candles[i].Low,
Close: candles[i].Close,
Volume: candles[i].Volume,
})
}
case asset.USDTMarginedFutures:
var candles []FuturesCandleStick
candles, err = b.UKlineData(ctx,
req.RequestFormatted,
b.FormatExchangeKlineInterval(interval),
int64(req.RangeHolder.Limit),
req.RangeHolder.Ranges[x].Start.Time,
req.RangeHolder.Ranges[x].End.Time)
if err != nil {
return nil, err
}
for i := range candles {
timeSeries = append(timeSeries, kline.Candle{
Time: candles[i].OpenTime,
Open: candles[i].Open,
High: candles[i].High,
Low: candles[i].Low,
Close: candles[i].Close,
Volume: candles[i].Volume,
})
}
case asset.CoinMarginedFutures:
var candles []FuturesCandleStick
candles, err = b.GetFuturesKlineData(ctx,
req.RequestFormatted,
b.FormatExchangeKlineInterval(interval),
int64(req.RangeHolder.Limit),
req.RangeHolder.Ranges[x].Start.Time,
req.RangeHolder.Ranges[x].End.Time)
if err != nil {
return nil, err
}
for i := range candles {
timeSeries = append(timeSeries, kline.Candle{
Time: candles[i].OpenTime,
Open: candles[i].Open,
High: candles[i].High,
Low: candles[i].Low,
Close: candles[i].Close,
Volume: candles[i].Volume,
})
}
default:
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a)
}
}
return req.ProcessResponse(timeSeries)
@@ -1822,7 +1968,7 @@ func (b *Binance) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item)
return nil
}
default:
err = fmt.Errorf("unhandled asset type %s", a)
err = fmt.Errorf("%w %v", asset.ErrNotSupported, a)
}
if err != nil {
return fmt.Errorf("cannot update exchange execution limits: %v", err)

View File

@@ -15,32 +15,32 @@ type Response struct {
// FuturesPublicTradesData stores recent public trades for futures
type FuturesPublicTradesData struct {
ID int64 `json:"id"`
Price float64 `json:"price,string"`
Qty float64 `json:"qty,string"`
QuoteQty float64 `json:"quoteQty,string"`
Time int64 `json:"time"`
IsBuyerMaker bool `json:"isBuyerMaker"`
ID int64 `json:"id"`
Price float64 `json:"price,string"`
Qty float64 `json:"qty,string"`
QuoteQty float64 `json:"quoteQty,string"`
Time binanceTime `json:"time"`
IsBuyerMaker bool `json:"isBuyerMaker"`
}
// CompressedTradesData stores futures trades data in a compressed format
type CompressedTradesData struct {
TradeID int64 `json:"a"`
Price float64 `json:"p"`
Quantity float64 `json:"q"`
FirstTradeID int64 `json:"f"`
LastTradeID int64 `json:"l"`
Timestamp int64 `json:"t"`
BuyerMaker bool `json:"b"`
TradeID int64 `json:"a"`
Price float64 `json:"p"`
Quantity float64 `json:"q"`
FirstTradeID int64 `json:"f"`
LastTradeID int64 `json:"l"`
Timestamp binanceTime `json:"t"`
BuyerMaker bool `json:"b"`
}
// MarkPriceData stores mark price data for futures
type MarkPriceData struct {
Symbol string `json:"symbol"`
MarkPrice float64 `json:"markPrice"`
LastFundingRate float64 `json:"lastFundingRate"`
NextFundingTime int64 `json:"nextFundingTime"`
Time int64 `json:"time"`
Symbol string `json:"symbol"`
MarkPrice float64 `json:"markPrice"`
LastFundingRate float64 `json:"lastFundingRate"`
NextFundingTime int64 `json:"nextFundingTime"`
Time binanceTime `json:"time"`
}
// SymbolPriceTicker stores ticker price stats

View File

@@ -51,12 +51,12 @@ type OrderbookData struct {
// UPublicTradesData stores trade data
type UPublicTradesData struct {
ID int64 `json:"id"`
Price float64 `json:"price,string"`
Qty float64 `json:"qty,string"`
QuoteQty float64 `json:"quoteQty,string"`
Time int64 `json:"time"`
IsBuyerMaker bool `json:"isBuyerMaker"`
ID int64 `json:"id"`
Price float64 `json:"price,string"`
Qty float64 `json:"qty,string"`
QuoteQty float64 `json:"quoteQty,string"`
Time binanceTime `json:"time"`
IsBuyerMaker bool `json:"isBuyerMaker"`
}
// UCompressedTradeData stores compressed trade data