From 28bd0e52bcdf168dd95c030d1b67f6d0cf43171e Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Thu, 1 Feb 2018 16:39:26 +1100 Subject: [PATCH] Added support for Binance exchange --- README.md | 1 + config/config_test.go | 4 +- config_example.json | 21 ++ exchange.go | 3 + exchanges/binance/binance.go | 497 +++++++++++++++++++++++++++ exchanges/binance/binance_test.go | 130 +++++++ exchanges/binance/binance_types.go | 204 +++++++++++ exchanges/binance/binance_wrapper.go | 95 +++++ testdata/configtest.json | 21 ++ 9 files changed, 974 insertions(+), 2 deletions(-) create mode 100644 exchanges/binance/binance.go create mode 100644 exchanges/binance/binance_test.go create mode 100644 exchanges/binance/binance_types.go create mode 100644 exchanges/binance/binance_wrapper.go diff --git a/README.md b/README.md index 30bb4f31..40cfc988 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader |----------|------|-----------|-----| | Alphapoint | Yes | Yes | NA | | ANXPRO | Yes | No | NA | +| Bitfinex | Yes | No | NA | | Bitfinex | Yes | Yes | NA | | Bithumb | Yes | NA | NA | | Bitstamp | Yes | Yes | NA | diff --git a/config/config_test.go b/config/config_test.go index dbd596dc..a7f0f010 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -89,7 +89,7 @@ func TestGetEnabledExchanges(t *testing.T) { } exchanges := cfg.GetEnabledExchanges() - if len(exchanges) != 21 { + if len(exchanges) != 22 { t.Error( "Test failed. TestGetEnabledExchanges. Enabled exchanges value mismatch", ) @@ -141,7 +141,7 @@ func TestGetDisabledExchanges(t *testing.T) { } func TestCountEnabledExchanges(t *testing.T) { - defaultEnabledExchanges := 21 + defaultEnabledExchanges := 22 GetConfigEnabledExchanges := GetConfig() err := GetConfigEnabledExchanges.LoadConfig(ConfigTestFile) if err != nil { diff --git a/config_example.json b/config_example.json index 94ddf390..b8ba0313 100644 --- a/config_example.json +++ b/config_example.json @@ -80,6 +80,27 @@ "Index": "BTC" } }, + { + "Name": "Binance", + "Enabled": true, + "Verbose": false, + "Websocket": false, + "UseSandbox": false, + "RESTPollingDelay": 10, + "AuthenticatedAPISupport": false, + "APIKey": "Key", + "APISecret": "Secret", + "AvailablePairs": "ETHBTC,LTCBTC,BNBBTC,NEOBTC,123456,QTUMETH,EOSETH,SNTETH,BNTETH,BCCBTC,GASBTC,BNBETH,BTCUSDT,ETHUSDT,HSRBTC,OAXETH,DNTETH,MCOETH,ICNETH,MCOBTC,WTCBTC,WTCETH,LRCBTC,LRCETH,QTUMBTC,YOYOBTC,OMGBTC,OMGETH,ZRXBTC,ZRXETH,STRATBTC,STRATETH,SNGLSBTC,SNGLSETH,BQXBTC,BQXETH,KNCBTC,KNCETH,FUNBTC,FUNETH,SNMBTC,SNMETH,NEOETH,IOTABTC,IOTAETH,LINKBTC,LINKETH,XVGBTC,XVGETH,CTRBTC,CTRETH,SALTBTC,SALTETH,MDABTC,MDAETH,MTLBTC,MTLETH,SUBBTC,SUBETH,EOSBTC,SNTBTC,ETCETH,ETCBTC,MTHBTC,MTHETH,ENGBTC,ENGETH,DNTBTC,ZECBTC,ZECETH,BNTBTC,ASTBTC,ASTETH,DASHBTC,DASHETH,OAXBTC,ICNBTC,BTGBTC,BTGETH,EVXBTC,EVXETH,REQBTC,REQETH,VIBBTC,VIBETH,HSRETH,TRXBTC,TRXETH,POWRBTC,POWRETH,ARKBTC,ARKETH,YOYOETH,XRPBTC,XRPETH,MODBTC,MODETH,ENJBTC,ENJETH,STORJBTC,STORJETH,BNBUSDT,VENBNB,YOYOBNB,POWRBNB,VENBTC,VENETH,KMDBTC,KMDETH,NULSBNB,RCNBTC,RCNETH,RCNBNB,NULSBTC,NULSETH,RDNBTC,RDNETH,RDNBNB,XMRBTC,XMRETH,DLTBNB,WTCBNB,DLTBTC,DLTETH,AMBBTC,AMBETH,AMBBNB,BCCETH,BCCUSDT,BCCBNB,BATBTC,BATETH,BATBNB,BCPTBTC,BCPTETH,BCPTBNB,ARNBTC,ARNETH,GVTBTC,GVTETH,CDTBTC,CDTETH,GXSBTC,GXSETH,NEOUSDT,NEOBNB,POEBTC,POEETH,QSPBTC,QSPETH,QSPBNB,BTSBTC,BTSETH,BTSBNB,XZCBTC,XZCETH,XZCBNB,LSKBTC,LSKETH,LSKBNB,TNTBTC,TNTETH,FUELBTC,FUELETH,MANABTC,MANAETH,BCDBTC,BCDETH,DGDBTC,DGDETH,IOTABNB,ADXBTC,ADXETH,ADXBNB,ADABTC,ADAETH,PPTBTC,PPTETH,CMTBTC,CMTETH,CMTBNB,XLMBTC,XLMETH,XLMBNB,CNDBTC,CNDETH,CNDBNB,LENDBTC,LENDETH,WABIBTC,WABIETH,WABIBNB,LTCETH,LTCUSDT,LTCBNB,TNBBTC,TNBETH,WAVESBTC,WAVESETH,WAVESBNB,GTOBTC,GTOETH,GTOBNB,ICXBTC,ICXETH,ICXBNB,OSTBTC,OSTETH,OSTBNB,ELFBTC,ELFETH,AIONBTC,AIONETH,AIONBNB,NEBLBTC,NEBLETH,NEBLBNB,BRDBTC,BRDETH,BRDBNB,MCOBNB,EDOBTC,EDOETH,WINGSBTC,WINGSETH,NAVBTC,NAVETH,NAVBNB,LUNBTC,LUNETH,TRIGBTC,TRIGETH,TRIGBNB,APPCBTC,APPCETH,APPCBNB,VIBEBTC,VIBEETH,RLCBTC,RLCETH,RLCBNB,INSBTC,INSETH,PIVXBTC,PIVXETH,PIVXBNB,IOSTBTC,IOSTETH,CHATBTC,CHATETH", + "EnabledPairs": "BTCUSDT", + "BaseCurrencies": "USD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true + } + }, { "Name": "Bitfinex", "Enabled": true, diff --git a/exchange.go b/exchange.go index 392cfced..78c678d5 100644 --- a/exchange.go +++ b/exchange.go @@ -7,6 +7,7 @@ import ( "github.com/thrasher-/gocryptotrader/common" exchange "github.com/thrasher-/gocryptotrader/exchanges" "github.com/thrasher-/gocryptotrader/exchanges/anx" + "github.com/thrasher-/gocryptotrader/exchanges/binance" "github.com/thrasher-/gocryptotrader/exchanges/bitfinex" "github.com/thrasher-/gocryptotrader/exchanges/bithumb" "github.com/thrasher-/gocryptotrader/exchanges/bitstamp" @@ -128,6 +129,8 @@ func LoadExchange(name string) error { switch nameLower { case "anx": exch = new(anx.ANX) + case "binance": + exch = new(binance.Binance) case "bitfinex": exch = new(bitfinex.Bitfinex) case "bithumb": diff --git a/exchanges/binance/binance.go b/exchanges/binance/binance.go new file mode 100644 index 00000000..62830579 --- /dev/null +++ b/exchanges/binance/binance.go @@ -0,0 +1,497 @@ +package binance + +import ( + "bytes" + "errors" + "fmt" + "log" + "net/url" + "strconv" + "time" + + "github.com/thrasher-/gocryptotrader/common" + "github.com/thrasher-/gocryptotrader/config" + exchange "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" +) + +// Binance is the overarching type across the Bithumb package +type Binance struct { + exchange.Base + + // valid string list that a required by the exchange + validLimits []string + validIntervals []string +} + +const ( + apiURL = "https://api.binance.com" + + // Public endpoints + exchangeInfo = "/api/v1/exchangeInfo" + orderBookDepth = "/api/v1/depth" + recentTrades = "/api/v1/trades" + historicalTrades = "/api/v1/historicalTrades" + aggregatedTrades = "/api/v1/aggTrades" + candleStick = "/api/v1/klines" + priceChange = "/api/v1/ticker/24hr" + symbolPrice = "/api/v3/ticker/price" + bestPrice = "/api/v3/ticker/bookTicker" + + // Authenticated endpoints + + newOrderTest = "/api/v3/order/test" + newOrder = "/api/v3/order" + queryOrder = "/api/v3/order" +) + +// SetDefaults sets the basic defaults for Binance +func (b *Binance) SetDefaults() { + b.Name = "Binance" + b.Enabled = false + b.Verbose = false + b.Websocket = false + b.RESTPollingDelay = 10 + b.RequestCurrencyPairFormat.Delimiter = "" + b.RequestCurrencyPairFormat.Uppercase = true + b.ConfigCurrencyPairFormat.Delimiter = "" + b.ConfigCurrencyPairFormat.Uppercase = true + b.AssetTypes = []string{ticker.Spot} + b.SetValues() +} + +// Setup takes in the supplied exchange configuration details and sets params +func (b *Binance) Setup(exch config.ExchangeConfig) { + if !exch.Enabled { + b.SetEnabled(false) + } else { + b.Enabled = true + b.AuthenticatedAPISupport = exch.AuthenticatedAPISupport + b.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) + b.RESTPollingDelay = exch.RESTPollingDelay + b.Verbose = exch.Verbose + b.Websocket = exch.Websocket + b.BaseCurrencies = common.SplitStrings(exch.BaseCurrencies, ",") + b.AvailablePairs = common.SplitStrings(exch.AvailablePairs, ",") + b.EnabledPairs = common.SplitStrings(exch.EnabledPairs, ",") + err := b.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = b.SetAssetTypes() + if err != nil { + log.Fatal(err) + } + } +} + +// GetExchangeValidCurrencyPairs returns the full pair list from the exchange +// at the moment do not integrate with config currency pairs automatically +func (b *Binance) GetExchangeValidCurrencyPairs() (string, error) { + var validCurrencyPairs []string + + info, err := b.GetExchangeInfo() + if err != nil { + return "", err + } + + for _, symbol := range info.Symbols { + validCurrencyPairs = append(validCurrencyPairs, symbol.Symbol) + } + return common.JoinStrings(validCurrencyPairs, ","), nil +} + +// GetExchangeInfo returns exchange information. Check binance_types for more +// information +func (b *Binance) GetExchangeInfo() (ExchangeInfo, error) { + var resp ExchangeInfo + path := apiURL + exchangeInfo + + return resp, common.SendHTTPGetRequest(path, true, b.Verbose, &resp) +} + +// GetOrderBook returns full orderbook information +// +// symbol: string of currency pair +// limit: returned limit amount +func (b *Binance) GetOrderBook(symbol string, limit int64) (OrderBook, error) { + orderbook, resp := OrderBook{}, OrderBookData{} + + if err := b.CheckLimit(limit); err != nil { + return orderbook, err + } + if err := b.CheckSymbol(symbol); err != nil { + return orderbook, err + } + + params := url.Values{} + params.Set("symbol", common.StringToUpper(symbol)) + params.Set("limit", strconv.FormatInt(limit, 10)) + + path := fmt.Sprintf("%s%s?%s", apiURL, orderBookDepth, params.Encode()) + + if err := common.SendHTTPGetRequest(path, true, b.Verbose, &resp); err != nil { + return orderbook, err + } + + for _, asks := range resp.Asks { + var ASK struct { + Price float64 + Quantity float64 + } + for i, ask := range asks.([]interface{}) { + switch i { + case 0: + ASK.Price, _ = strconv.ParseFloat(ask.(string), 64) + case 1: + ASK.Quantity, _ = strconv.ParseFloat(ask.(string), 64) + } + orderbook.Asks = append(orderbook.Asks, ASK) + } + } + + for _, bids := range resp.Bids { + var BID struct { + Price float64 + Quantity float64 + } + for i, bid := range bids.([]interface{}) { + switch i { + case 0: + BID.Price, _ = strconv.ParseFloat(bid.(string), 64) + case 1: + BID.Quantity, _ = strconv.ParseFloat(bid.(string), 64) + } + orderbook.Bids = append(orderbook.Bids, BID) + } + } + return orderbook, nil +} + +// GetRecentTrades returns recent trade activity +// +// symbol: string of currency pair +// limit: returned limit amount WARNING: MAX 500! +func (b *Binance) GetRecentTrades(symbol string, limit int64) ([]RecentTrade, error) { + resp := []RecentTrade{} + + if err := b.CheckLimit(limit); err != nil { + return resp, err + } + if err := b.CheckSymbol(symbol); err != nil { + return resp, err + } + + params := url.Values{} + params.Set("symbol", common.StringToUpper(symbol)) + params.Set("limit", strconv.FormatInt(limit, 10)) + + path := fmt.Sprintf("%s%s?%s", apiURL, recentTrades, params.Encode()) + + return resp, common.SendHTTPGetRequest(path, true, b.Verbose, &resp) +} + +// GetHistoricalTrades returns historical trade activity +// +// symbol: string of currency pair +// limit: returned limit amount WARNING: MAX 500! (NOT REQUIRED) +// fromID: +func (b *Binance) GetHistoricalTrades(symbol string, limit, fromID int64) ([]HistoricalTrade, error) { + resp := []HistoricalTrade{} + + if err := b.CheckLimit(limit); err != nil { + return resp, err + } + if err := b.CheckSymbol(symbol); err != nil { + return resp, err + } + + params := url.Values{} + params.Set("symbol", common.StringToUpper(symbol)) + params.Set("limit", strconv.FormatInt(limit, 10)) + params.Set("fromid", strconv.FormatInt(fromID, 10)) + + path := fmt.Sprintf("%s%s?%s", apiURL, historicalTrades, params.Encode()) + + return resp, common.SendHTTPGetRequest(path, true, b.Verbose, &resp) +} + +// GetAggregatedTrades returns aggregated trade activity +// +// symbol: string of currency pair +// limit: returned limit amount WARNING: MAX 500! +func (b *Binance) GetAggregatedTrades(symbol string, limit int64) ([]AggregatedTrade, error) { + resp := []AggregatedTrade{} + + if err := b.CheckLimit(limit); err != nil { + return resp, err + } + if err := b.CheckSymbol(symbol); err != nil { + return resp, err + } + + params := url.Values{} + params.Set("symbol", common.StringToUpper(symbol)) + params.Set("limit", strconv.FormatInt(limit, 10)) + + path := fmt.Sprintf("%s%s?%s", apiURL, aggregatedTrades, params.Encode()) + + return resp, common.SendHTTPGetRequest(path, true, b.Verbose, &resp) +} + +// GetCandleStickData returns candle stick data +// +// symbol: +// limit: +// interval +func (b *Binance) GetCandleStickData(symbol, interval string, limit int64) ([]CandleStick, error) { + var resp interface{} + var kline []CandleStick + + if err := b.CheckLimit(limit); err != nil { + return kline, err + } + if err := b.CheckSymbol(symbol); err != nil { + return kline, err + } + if err := b.CheckIntervals(interval); err != nil { + return kline, err + } + + params := url.Values{} + params.Set("symbol", common.StringToUpper(symbol)) + params.Set("limit", strconv.FormatInt(limit, 10)) + params.Set("interval", interval) + + path := fmt.Sprintf("%s%s?%s", apiURL, candleStick, params.Encode()) + + if err := common.SendHTTPGetRequest(path, true, b.Verbose, &resp); err != nil { + return kline, err + } + + for _, responseData := range resp.([]interface{}) { + var candle CandleStick + for i, individualData := range responseData.([]interface{}) { + switch i { + case 0: + candle.OpenTime = individualData.(float64) + case 1: + candle.Open, _ = strconv.ParseFloat(individualData.(string), 64) + case 2: + candle.High, _ = strconv.ParseFloat(individualData.(string), 64) + case 3: + candle.Low, _ = strconv.ParseFloat(individualData.(string), 64) + case 4: + candle.Close, _ = strconv.ParseFloat(individualData.(string), 64) + case 5: + candle.Volume, _ = strconv.ParseFloat(individualData.(string), 64) + case 6: + candle.CloseTime = individualData.(float64) + case 7: + candle.QuoteAssetVolume, _ = strconv.ParseFloat(individualData.(string), 64) + case 8: + candle.TradeCount = individualData.(float64) + case 9: + candle.TakerBuyAssetVolume, _ = strconv.ParseFloat(individualData.(string), 64) + case 10: + candle.TakerBuyQuoteAssetVolume, _ = strconv.ParseFloat(individualData.(string), 64) + } + } + kline = append(kline, candle) + } + return kline, nil +} + +// GetPriceChangeStats returns price change statistics for the last 24 hours +// +// symbol: string of currency pair +func (b *Binance) GetPriceChangeStats(symbol string) (PriceChangeStats, error) { + resp := PriceChangeStats{} + + if err := b.CheckSymbol(symbol); err != nil { + return resp, err + } + + params := url.Values{} + params.Set("symbol", common.StringToUpper(symbol)) + + path := fmt.Sprintf("%s%s?%s", apiURL, priceChange, params.Encode()) + + return resp, common.SendHTTPGetRequest(path, true, b.Verbose, &resp) +} + +// GetLatestSpotPrice returns latest spot price of symbol +// +// symbol: string of currency pair +func (b *Binance) GetLatestSpotPrice(symbol string) (SymbolPrice, error) { + resp := SymbolPrice{} + + if err := b.CheckSymbol(symbol); err != nil { + return resp, err + } + + params := url.Values{} + params.Set("symbol", common.StringToUpper(symbol)) + + path := fmt.Sprintf("%s%s?%s", apiURL, symbolPrice, params.Encode()) + + return resp, common.SendHTTPGetRequest(path, true, b.Verbose, &resp) +} + +// GetBestPrice returns the latest best price for symbol +// +// symbol: string of currency pair +func (b *Binance) GetBestPrice(symbol string) (BestPrice, error) { + resp := BestPrice{} + + if err := b.CheckSymbol(symbol); err != nil { + return resp, err + } + + params := url.Values{} + params.Set("symbol", common.StringToUpper(symbol)) + + path := fmt.Sprintf("%s%s?%s", apiURL, bestPrice, params.Encode()) + + return resp, common.SendHTTPGetRequest(path, true, b.Verbose, &resp) +} + +// NewOrderTest sends a new order +func (b *Binance) NewOrderTest() (interface{}, error) { + var resp interface{} + + path := fmt.Sprintf("%s%s", apiURL, newOrderTest) + + params := url.Values{} + params.Set("symbol", "BTCUSDT") + params.Set("side", "BUY") + params.Set("type", "MARKET") + params.Set("quantity", "0.1") + + return resp, b.SendAuthHTTPRequest("POST", path, params, &resp) +} + +// NewOrder sends a new order to Binance +func (b *Binance) NewOrder(o NewOrderRequest) (NewOrderResponse, error) { + var resp NewOrderResponse + + path := fmt.Sprintf("%s%s", apiURL, newOrderTest) + + params := url.Values{} + params.Set("symbol", o.Symbol) + params.Set("side", o.Side) + params.Set("type", o.TradeType) + params.Set("timeInForce", o.TimeInForce) + params.Set("quantity", strconv.FormatFloat(o.Quantity, 'f', -1, 64)) + params.Set("price", strconv.FormatFloat(o.Price, 'f', -1, 64)) + params.Set("newClientOrderID", o.NewClientOrderID) + params.Set("stopPrice", strconv.FormatFloat(o.StopPrice, 'f', -1, 64)) + params.Set("icebergQty", strconv.FormatFloat(o.IcebergQty, 'f', -1, 64)) + params.Set("newOrderRespType", o.NewOrderRespType) + + if err := b.SendAuthHTTPRequest("POST", path, params, &resp); err != nil { + return resp, err + } + + if resp.Code != 0 { + return resp, errors.New(resp.Msg) + } + return resp, nil +} + +// QueryOrder returns information on a past order +func (b *Binance) QueryOrder(symbol, origClientOrderID string, orderID int64) (QueryOrderData, error) { + var resp QueryOrderData + + path := fmt.Sprintf("%s%s", apiURL, queryOrder) + + params := url.Values{} + params.Set("symbol", common.StringToUpper(symbol)) + params.Set("origClientOrderId", origClientOrderID) + params.Set("orderId", strconv.FormatInt(orderID, 10)) + + if err := b.SendAuthHTTPRequest("GET", path, params, &resp); err != nil { + return resp, err + } + + if resp.Code != 0 { + return resp, errors.New(resp.Msg) + } + return resp, nil +} + +// SendAuthHTTPRequest something +func (b *Binance) SendAuthHTTPRequest(method, path string, params url.Values, result interface{}) error { + if !b.AuthenticatedAPISupport { + return fmt.Errorf(exchange.WarningAuthenticatedRequestWithoutCredentialsSet, b.Name) + } + + if params == nil { + params = url.Values{} + } + params.Set("recvWindow", strconv.FormatInt(5000, 10)) + params.Set("timestamp", strconv.FormatInt(time.Now().Unix()*1000, 10)) + + signature := params.Encode() + hmacSigned := common.GetHMAC(common.HashSHA256, []byte(signature), []byte(b.APISecret)) + hmacSignedStr := common.HexEncodeToString(hmacSigned) + params.Set("signature", hmacSignedStr) + + if b.Nonce.Get() == 0 { + b.Nonce.Set(time.Now().UnixNano() / int64(time.Millisecond)) + } else { + b.Nonce.Inc() + } + + headers := make(map[string]string) + headers["X-MBX-APIKEY"] = b.APIKey + headers["Content-Type"] = "application/x-www-form-urlencoded" + + if b.Verbose { + log.Printf("sent path: \n%s\n", path) + } + + resp, err := common.SendHTTPRequest(method, path, headers, bytes.NewBufferString(params.Encode())) + if err != nil { + return err + } + + if b.Verbose { + log.Printf("Received raw: \n%s\n", resp) + } + + if err = common.JSONDecode([]byte(resp), &result); err != nil { + return errors.New("sendAuthenticatedHTTPRequest: Unable to JSON Unmarshal response." + err.Error()) + } + return nil +} + +// CheckLimit checks value against a variable list +func (b *Binance) CheckLimit(limit int64) error { + if !common.DataContains(b.validLimits, strconv.FormatInt(limit, 10)) { + return errors.New("Incorrect limit values - valid values are 5, 10, 20, 50, 100, 500, 1000") + } + return nil +} + +// CheckSymbol checks value against a variable list +func (b *Binance) CheckSymbol(symbol string) error { + if !common.DataContains(b.AvailablePairs, symbol) { + return errors.New("Incorrect symbol values - please check available pairs in configuration") + } + return nil +} + +// CheckIntervals checks value against a variable list +func (b *Binance) CheckIntervals(interval string) error { + if !common.DataContains(b.validIntervals, interval) { + return errors.New(`Incorrect interval values - valid values are "1m","3m","5m","15m","30m","1h","2h","4h","6h","8h","12h","1d","3d","1w","1M"`) + } + return nil +} + +// SetValues sets the default valid values +func (b *Binance) SetValues() { + b.validLimits = []string{"5", "10", "20", "50", "100", "500", "1000"} + b.validIntervals = []string{"1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "3d", "1w", "1M"} +} diff --git a/exchanges/binance/binance_test.go b/exchanges/binance/binance_test.go new file mode 100644 index 00000000..80ce4d2b --- /dev/null +++ b/exchanges/binance/binance_test.go @@ -0,0 +1,130 @@ +package binance + +import ( + "testing" + + "github.com/thrasher-/gocryptotrader/config" +) + +// Please supply your own keys here for due diligence testing +const ( + testAPIKey = "" + testAPISecret = "" +) + +var b Binance + +func TestSetDefaults(t *testing.T) { + b.SetDefaults() +} + +func TestSetup(t *testing.T) { + cfg := config.GetConfig() + cfg.LoadConfig("../../testdata/configtest.json") + binanceConfig, err := cfg.GetExchangeConfig("Binance") + if err != nil { + t.Error("Test Failed - Binance Setup() init error") + } + + binanceConfig.AuthenticatedAPISupport = true + binanceConfig.APIKey = testAPIKey + binanceConfig.APISecret = testAPISecret + + b.Setup(binanceConfig) +} + +func TestGetExchangeValidCurrencyPairs(t *testing.T) { + t.Parallel() + _, err := b.GetExchangeValidCurrencyPairs() + if err != nil { + t.Error("Test Failed - Binance GetExchangeValidCurrencyPairs() error", err) + } +} + +func TestGetOrderBook(t *testing.T) { + t.Parallel() + _, err := b.GetOrderBook("BTCUSDT", 5) + if err != nil { + t.Error("Test Failed - Binance GetOrderBook() error", err) + } +} + +func TestGetRecentTrades(t *testing.T) { + t.Parallel() + _, err := b.GetRecentTrades("BTCUSDT", 5) + if err != nil { + t.Error("Test Failed - Binance GetRecentTrades() error", err) + } +} + +func TestGetHistoricalTrades(t *testing.T) { + t.Parallel() + _, err := b.GetHistoricalTrades("BTCUSDT", 5, 1337) + if err == nil { + t.Error("Test Failed - Binance GetHistoricalTrades() error", err) + } +} + +func TestGetAggregatedTrades(t *testing.T) { + t.Parallel() + _, err := b.GetAggregatedTrades("BTCUSDT", 5) + if err != nil { + t.Error("Test Failed - Binance GetAggregatedTrades() error", err) + } +} + +func TestGetCandleStickData(t *testing.T) { + t.Parallel() + _, err := b.GetCandleStickData("BTCUSDT", "1d", 5) + if err != nil { + t.Error("Test Failed - Binance GetCandleStickData() error", err) + } +} + +func TestGetPriceChangeStats(t *testing.T) { + t.Parallel() + _, err := b.GetPriceChangeStats("BTCUSDT") + if err != nil { + t.Error("Test Failed - Binance GetPriceChangeStats() error", err) + } +} + +func TestGetLatestSpotPrice(t *testing.T) { + t.Parallel() + _, err := b.GetLatestSpotPrice("BTCUSDT") + if err != nil { + t.Error("Test Failed - Binance GetLatestSpotPrice() error", err) + } +} + +func TestGetBestPrice(t *testing.T) { + t.Parallel() + _, err := b.GetBestPrice("BTCUSDT") + if err != nil { + t.Error("Test Failed - Binance GetBestPrice() error", err) + } +} + +func TestNewOrderTest(t *testing.T) { + t.Parallel() + _, err := b.NewOrderTest() + if err != nil { + t.Error("Test Failed - Binance NewOrderTest() error", err) + } +} + +func TestNewOrder(t *testing.T) { + t.Parallel() + _, err := b.NewOrder(NewOrderRequest{}) + if err == nil { + t.Error("Test Failed - Binance NewOrder() error", err) + } +} + +func TestQueryOrder(t *testing.T) { + t.Parallel() + _, err := b.QueryOrder("", "", 1337) + if err == nil { + t.Error("Test Failed - Binance QueryOrder() error", err) + } +} diff --git a/exchanges/binance/binance_types.go b/exchanges/binance/binance_types.go new file mode 100644 index 00000000..d7cc9532 --- /dev/null +++ b/exchanges/binance/binance_types.go @@ -0,0 +1,204 @@ +package binance + +// ExchangeInfo holds the full exchange information type +type ExchangeInfo struct { + Code int `json:"code"` + Msg string `json:"msg"` + Timezone string `json:"timezone"` + Servertime int64 `json:"serverTime"` + RateLimits []struct { + RateLimitType string `json:"rateLimitType"` + Interval string `json:"interval"` + Limit int `json:"limit"` + } `json:"rateLimits"` + ExchangeFilters interface{} `json:"exchangeFilters"` + Symbols []struct { + Symbol string `json:"symbol"` + Status string `json:"status"` + BaseAsset string `json:"baseAsset"` + BaseAssetPrecision int `json:"baseAssetPrecision"` + QuoteAsset string `json:"quoteAsset"` + QuotePrecision int `json:"quotePrecision"` + OrderTypes []string `json:"orderTypes"` + IcebergAllowed bool `json:"icebergAllowed"` + Filters []struct { + FilterType string `json:"filterType"` + MinPrice float64 `json:"minPrice,string"` + MaxPrice float64 `json:"maxPrice,string"` + TickSize float64 `json:"tickSize,string"` + MinQty float64 `json:"minQty,string"` + MaxQty float64 `json:"maxQty,string"` + StepSize float64 `json:"stepSize,string"` + MinNotional float64 `json:"minNotional,string"` + } `json:"filters"` + } `json:"symbols"` +} + +// OrderBookData is resp data from orderbook endpoint +type OrderBookData struct { + Code int `json:"code"` + Msg string `json:"msg"` + LastUpdateID int64 `json:"lastUpdateId"` + Bids []interface{} `json:"bids"` + Asks []interface{} `json:"asks"` +} + +// OrderBook actual structured data that can be used for orderbook +type OrderBook struct { + Code int + Msg string + Bids []struct { + Price float64 + Quantity float64 + } + Asks []struct { + Price float64 + Quantity float64 + } +} + +// RecentTrade holds recent trade data +type RecentTrade struct { + Code int `json:"code"` + Msg string `json:"msg"` + ID int64 `json:"id"` + Price float64 `json:"price,string"` + Quantity float64 `json:"qty,string"` + Time int64 `json:"time"` + IsBuyerMaker bool `json:"isBuyerMaker"` + IsBestMatch bool `json:"isBestMatch"` +} + +// HistoricalTrade holds recent trade data +type HistoricalTrade struct { + Code int `json:"code"` + Msg string `json:"msg"` + ID int64 `json:"id"` + Price float64 `json:"price,string"` + Quantity float64 `json:"qty,string"` + Time int64 `json:"time"` + IsBuyerMaker bool `json:"isBuyerMaker"` + IsBestMatch bool `json:"isBestMatch"` +} + +// AggregatedTrade holds aggregated trade information +type AggregatedTrade struct { + ATradeID int64 `json:"a"` + Price float64 `json:"p,string"` + Quantity float64 `json:"q,string"` + FirstTradeID int64 `json:"f"` + LastTradeID int64 `json:"l"` + TimeStamp int64 `json:"T"` + Maker bool `json:"m"` + BestMatchPrice bool `json:"M"` +} + +// CandleStick holds kline data +type CandleStick struct { + OpenTime float64 + Open float64 + High float64 + Low float64 + Close float64 + Volume float64 + CloseTime float64 + QuoteAssetVolume float64 + TradeCount float64 + TakerBuyAssetVolume float64 + TakerBuyQuoteAssetVolume float64 +} + +// PriceChangeStats contains statistics for the last 24 hours trade +type PriceChangeStats struct { + Symbol string `json:"symbol"` + PriceChange float64 `json:"priceChange,string"` + PriceChangePercent float64 `json:"priceChangePercent,string"` + WeightedAvgPrice float64 `json:"weightedAvgPrice,string"` + PrevClosePrice float64 `json:"prevClosePrice,string"` + LastPrice float64 `json:"lastPrice,string"` + LastQty float64 `json:"lastQty,string"` + BidPrice float64 `json:"bidPrice,string"` + AskPrice float64 `json:"askPrice,string"` + OpenPrice float64 `json:"openPrice,string"` + HighPrice float64 `json:"highPrice,string"` + LowPrice float64 `json:"lowPrice,string"` + Volume float64 `json:"volume,string"` + QuoteVolume float64 `json:"quoteVolume,string"` + OpenTime int64 `json:"openTime"` + CloseTime int64 `json:"closeTime"` + FirstID int64 `json:"fristId"` + LastID int64 `json:"lastId"` + Count int64 `json:"count"` +} + +// SymbolPrice holds basic symbol price +type SymbolPrice struct { + Symbol string `json:"symbol"` + Price float64 `json:"price,string"` +} + +// BestPrice holds best price data +type BestPrice struct { + Symbol string `json:"symbol"` + BidPrice float64 `json:"bidPrice,string"` + BidQty float64 `json:"bidQty,string"` + AskPrice float64 `json:"askPrice,string"` + AskQty float64 `json:"askQty,string"` +} + +// NewOrderRequest request type +type NewOrderRequest struct { + Symbol string + Side string + TradeType string + TimeInForce string + Quantity float64 + Price float64 + NewClientOrderID string + StopPrice float64 + IcebergQty float64 + NewOrderRespType string +} + +// NewOrderResponse is the return structured response from the exchange +type NewOrderResponse struct { + Code int `json:"code"` + Msg string `json:"msg"` + Symbol string `json:"symbol"` + OrderID int64 `json:"orderId"` + ClientOrderID string `json:"clientOrderId"` + TransactionTime int64 `json:"transactTime"` + Price float64 `json:"price,string"` + OrigQty float64 `json:"origQty,string"` + ExecutedQty float64 `json:"executedQty,string"` + Status string `json:"status"` + TimeInForce string `json:"timeInForce"` + Type string `json:"type"` + Side string `json:"side"` + Fills []struct { + Price float64 `json:"price,string"` + Qty float64 `json:"qty,string"` + Commission float64 `json:"commission,string"` + CommissionAsset float64 `json:"commissionAsset,string"` + } `json:"fills"` +} + +// QueryOrderData holds query order data +type QueryOrderData struct { + Code int `json:"code"` + Msg string `json:"msg"` + Symbol string `json:"symbol"` + OrderID int64 `json:"orderId"` + ClientOrderID string `json:"clientOrderId"` + Price float64 `json:"price,string"` + OrigQty float64 `json:"origQty,string"` + ExecutedQty float64 `json:"executedQty,string"` + Status string `json:"status"` + TimeInForce string `json:"timeInForce"` + Type string `json:"type"` + Side string `json:"side"` + StopPrice float64 `json:"stopPrice,string"` + IcebergQty float64 `json:"icebergQty,string"` + Time int64 `json:"time"` + IsWorking bool `json:"isWorking"` +} diff --git a/exchanges/binance/binance_wrapper.go b/exchanges/binance/binance_wrapper.go new file mode 100644 index 00000000..fce25f1b --- /dev/null +++ b/exchanges/binance/binance_wrapper.go @@ -0,0 +1,95 @@ +package binance + +import ( + "errors" + "log" + + "github.com/thrasher-/gocryptotrader/common" + "github.com/thrasher-/gocryptotrader/currency/pair" + exchange "github.com/thrasher-/gocryptotrader/exchanges" + "github.com/thrasher-/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-/gocryptotrader/exchanges/ticker" +) + +// Start starts the OKEX go routine +func (b *Binance) Start() { + go b.Run() +} + +// Run implements the OKEX wrapper +func (b *Binance) Run() { + if b.Verbose { + log.Printf("%s Websocket: %s. (url: %s).\n", b.GetName(), common.IsEnabled(b.Websocket), b.WebsocketURL) + log.Printf("%s polling delay: %ds.\n", b.GetName(), b.RESTPollingDelay) + log.Printf("%s %d currencies enabled: %s.\n", b.GetName(), len(b.EnabledPairs), b.EnabledPairs) + } +} + +// UpdateTicker updates and returns the ticker for a currency pair +func (b *Binance) UpdateTicker(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + var tickerPrice ticker.Price + + tick, err := b.GetPriceChangeStats(p.Pair().String()) + if err != nil { + return tickerPrice, err + } + + tickerPrice.Pair = p + tickerPrice.Ask = tick.AskPrice + tickerPrice.Bid = tick.BidPrice + tickerPrice.High = tick.HighPrice + tickerPrice.Last = tick.LastPrice + tickerPrice.Low = tick.LowPrice + tickerPrice.Volume = tick.LastQty + + ticker.ProcessTicker(b.GetName(), p, tickerPrice, assetType) + + return ticker.GetTicker(b.Name, p, assetType) +} + +// GetTickerPrice returns the ticker for a currency pair +func (b *Binance) GetTickerPrice(p pair.CurrencyPair, assetType string) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(b.GetName(), p, assetType) + if err != nil { + return b.UpdateTicker(p, assetType) + } + return tickerNew, nil +} + +// GetOrderbookEx returns orderbook base on the currency pair +func (b *Binance) GetOrderbookEx(currency pair.CurrencyPair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.GetOrderbook(b.GetName(), currency, assetType) + if err != nil { + return b.UpdateOrderbook(currency, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (b *Binance) UpdateOrderbook(p pair.CurrencyPair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base + currency := p.GetFirstCurrency().String() + + orderbookNew, err := b.GetOrderBook(currency, 1000) + if err != nil { + return orderBook, err + } + + for _, bids := range orderbookNew.Bids { + orderBook.Bids = append(orderBook.Bids, orderbook.Item{Amount: bids.Quantity, Price: bids.Price}) + } + + for _, asks := range orderbookNew.Asks { + orderBook.Asks = append(orderBook.Asks, orderbook.Item{Amount: asks.Quantity, Price: asks.Price}) + } + + orderbook.ProcessOrderbook(b.GetName(), p, orderBook, assetType) + return orderbook.GetOrderbook(b.Name, p, assetType) +} + +// GetExchangeAccountInfo retrieves balances for all enabled currencies for the +// Bithumb exchange +func (b *Binance) GetExchangeAccountInfo() (exchange.AccountInfo, error) { + var response exchange.AccountInfo + return response, errors.New("not implemented") +} diff --git a/testdata/configtest.json b/testdata/configtest.json index 0ad63494..781f5304 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -80,6 +80,27 @@ "Index": "BTC" } }, + { + "Name": "Binance", + "Enabled": true, + "Verbose": false, + "Websocket": false, + "UseSandbox": false, + "RESTPollingDelay": 10, + "AuthenticatedAPISupport": false, + "APIKey": "Key", + "APISecret": "Secret", + "AvailablePairs": "ETHBTC,LTCBTC,BNBBTC,NEOBTC,123456,QTUMETH,EOSETH,SNTETH,BNTETH,BCCBTC,GASBTC,BNBETH,BTCUSDT,ETHUSDT,HSRBTC,OAXETH,DNTETH,MCOETH,ICNETH,MCOBTC,WTCBTC,WTCETH,LRCBTC,LRCETH,QTUMBTC,YOYOBTC,OMGBTC,OMGETH,ZRXBTC,ZRXETH,STRATBTC,STRATETH,SNGLSBTC,SNGLSETH,BQXBTC,BQXETH,KNCBTC,KNCETH,FUNBTC,FUNETH,SNMBTC,SNMETH,NEOETH,IOTABTC,IOTAETH,LINKBTC,LINKETH,XVGBTC,XVGETH,CTRBTC,CTRETH,SALTBTC,SALTETH,MDABTC,MDAETH,MTLBTC,MTLETH,SUBBTC,SUBETH,EOSBTC,SNTBTC,ETCETH,ETCBTC,MTHBTC,MTHETH,ENGBTC,ENGETH,DNTBTC,ZECBTC,ZECETH,BNTBTC,ASTBTC,ASTETH,DASHBTC,DASHETH,OAXBTC,ICNBTC,BTGBTC,BTGETH,EVXBTC,EVXETH,REQBTC,REQETH,VIBBTC,VIBETH,HSRETH,TRXBTC,TRXETH,POWRBTC,POWRETH,ARKBTC,ARKETH,YOYOETH,XRPBTC,XRPETH,MODBTC,MODETH,ENJBTC,ENJETH,STORJBTC,STORJETH,BNBUSDT,VENBNB,YOYOBNB,POWRBNB,VENBTC,VENETH,KMDBTC,KMDETH,NULSBNB,RCNBTC,RCNETH,RCNBNB,NULSBTC,NULSETH,RDNBTC,RDNETH,RDNBNB,XMRBTC,XMRETH,DLTBNB,WTCBNB,DLTBTC,DLTETH,AMBBTC,AMBETH,AMBBNB,BCCETH,BCCUSDT,BCCBNB,BATBTC,BATETH,BATBNB,BCPTBTC,BCPTETH,BCPTBNB,ARNBTC,ARNETH,GVTBTC,GVTETH,CDTBTC,CDTETH,GXSBTC,GXSETH,NEOUSDT,NEOBNB,POEBTC,POEETH,QSPBTC,QSPETH,QSPBNB,BTSBTC,BTSETH,BTSBNB,XZCBTC,XZCETH,XZCBNB,LSKBTC,LSKETH,LSKBNB,TNTBTC,TNTETH,FUELBTC,FUELETH,MANABTC,MANAETH,BCDBTC,BCDETH,DGDBTC,DGDETH,IOTABNB,ADXBTC,ADXETH,ADXBNB,ADABTC,ADAETH,PPTBTC,PPTETH,CMTBTC,CMTETH,CMTBNB,XLMBTC,XLMETH,XLMBNB,CNDBTC,CNDETH,CNDBNB,LENDBTC,LENDETH,WABIBTC,WABIETH,WABIBNB,LTCETH,LTCUSDT,LTCBNB,TNBBTC,TNBETH,WAVESBTC,WAVESETH,WAVESBNB,GTOBTC,GTOETH,GTOBNB,ICXBTC,ICXETH,ICXBNB,OSTBTC,OSTETH,OSTBNB,ELFBTC,ELFETH,AIONBTC,AIONETH,AIONBNB,NEBLBTC,NEBLETH,NEBLBNB,BRDBTC,BRDETH,BRDBNB,MCOBNB,EDOBTC,EDOETH,WINGSBTC,WINGSETH,NAVBTC,NAVETH,NAVBNB,LUNBTC,LUNETH,TRIGBTC,TRIGETH,TRIGBNB,APPCBTC,APPCETH,APPCBNB,VIBEBTC,VIBEETH,RLCBTC,RLCETH,RLCBNB,INSBTC,INSETH,PIVXBTC,PIVXETH,PIVXBNB,IOSTBTC,IOSTETH,CHATBTC,CHATETH", + "EnabledPairs": "BTCUSDT", + "BaseCurrencies": "USD", + "AssetTypes": "SPOT", + "ConfigCurrencyPairFormat": { + "Uppercase": true + }, + "RequestCurrencyPairFormat": { + "Uppercase": true + } + }, { "Name": "Bitfinex", "Enabled": true,