From a81ddead9e59e5f34132844d0c8c4880b7fb6700 Mon Sep 17 00:00:00 2001 From: Adam <31364354+MadCozBadd@users.noreply.github.com> Date: Fri, 23 Aug 2019 11:28:38 +1000 Subject: [PATCH] Add LBank exchange support (#327) * wip * Lbank support being added * Lbank exchange linter issues fixed * Removed the incomplete websocket stuff * PR Requests completed * PR request fixes * Lbank Update * Lbank Update * Wrapper functions fixed, linter issues fixed * Changed as per requested in PR * Changed as per requested in PR * Changed as per requested in PR * Changed as per requested in PR * PR Requests completed * FINALLY DONE * appveyor issues fixed * Skip functionality for new tests * Test functions fixed * PR Fixes * PR Fixes * PR Fixes * PR Fixes * Final Changes * Final Changes 2 * Final Changes 3 * Final Changes 4 --- CONTRIBUTORS | 14 +- README.md | 17 +- config/README.md | 1 - config/config_test.go | 4 +- config_example.json | 42 ++ exchange.go | 3 + exchanges/anx/anx_test.go | 13 + exchanges/exchange.go | 7 +- exchanges/lbank/README.md | 133 ++++ exchanges/lbank/lbank.go | 622 ++++++++++++++++++ exchanges/lbank/lbank_test.go | 387 +++++++++++ exchanges/lbank/lbank_types.go | 270 ++++++++ exchanges/lbank/lbank_wrapper.go | 545 +++++++++++++++ testdata/README.md | 1 + testdata/configtest.json | 43 ++ tools/documentation/documentation.go | 8 +- .../exchanges_templates/lbank.tmpl | 98 +++ .../root_templates/root_readme.tmpl | 1 + 18 files changed, 2183 insertions(+), 26 deletions(-) create mode 100644 exchanges/lbank/README.md create mode 100644 exchanges/lbank/lbank.go create mode 100644 exchanges/lbank/lbank_test.go create mode 100644 exchanges/lbank/lbank_types.go create mode 100644 exchanges/lbank/lbank_wrapper.go create mode 100644 tools/documentation/exchanges_templates/lbank.tmpl diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 0de25e74..f192ce33 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -3,12 +3,13 @@ Thanks to the following contributors: thrasher- | https://github.com/thrasher- shazbert | https://github.com/shazbert gloriousCode | https://github.com/gloriousCode -ermalguni | https://github.com/ermalguni xtda | https://github.com/xtda +ermalguni | https://github.com/ermalguni +vadimzhukck | https://github.com/vadimzhukck 140am | https://github.com/140am marcofranssen | https://github.com/marcofranssen -vadimzhukck | https://github.com/vadimzhukck cranktakular | https://github.com/cranktakular +leilaes | https://github.com/leilaes crackcomm | https://github.com/crackcomm MadCozBadd | https://github.com/MadCozBadd andreygrehov | https://github.com/andreygrehov @@ -24,10 +25,13 @@ CodeLingoBot | https://github.com/CodeLingoBot CodeLingoTeam | https://github.com/CodeLingoTeam Daanikus | https://github.com/Daanikus daniel-cohen | https://github.com/daniel-cohen +DirectX | https://github.com/DirectX frankzougc | https://github.com/frankzougc starit | https://github.com/starit Jimexist | https://github.com/Jimexist lookfirst | https://github.com/lookfirst -zeldrinn | https://github.com/zeldrinn -mattkanwisher | https://github.com/mattkanwisher - +| mattkanwisher | https://github.com/mattkanwisher +| mKurrels | https://github.com/mKurrels +| m1kola | https://github.com/m1kola +| cavapoo2 | https://github.com/cavapoo2 +| zeldrinn | https://github.com/zeldrinn diff --git a/README.md b/README.md index 145f6888..eb1507eb 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader | Huobi.Hadax | Yes | Yes | NA | | ItBit | Yes | NA | No | | Kraken | Yes | Yes | NA | +| Lbank | Yes | No | NA | | LakeBTC | Yes | No | NA | | LocalBitcoins | Yes | NA | NA | | OKCoin International | Yes | Yes | No | @@ -130,15 +131,17 @@ Binaries will be published once the codebase reaches a stable condition. |User|Github|Contribution Amount| |--|--|--| -| thrasher- | https://github.com/thrasher- | 526 | -| shazbert | https://github.com/shazbert | 166 | -| gloriousCode | https://github.com/gloriousCode | 146 | +| thrasher- | https://github.com/thrasher- | 543 | +| shazbert | https://github.com/shazbert | 174 | +| gloriousCode | https://github.com/gloriousCode | 154 | +| xtda | https://github.com/xtda | 18 | | ermalguni | https://github.com/ermalguni | 14 | | xtda | https://github.com/xtda | 11 | +| vadimzhukck | https://github.com/vadimzhukck | 10 | | 140am | https://github.com/140am | 8 | | marcofranssen | https://github.com/marcofranssen | 8 | -| vadimzhukck | https://github.com/vadimzhukck | 8 | | cranktakular | https://github.com/cranktakular | 5 | +| leilaes | https://github.com/leilaes | 3 | | crackcomm | https://github.com/crackcomm | 3 | | MadCozBadd | https://github.com/MadCozBadd | 2 | | andreygrehov | https://github.com/andreygrehov | 2 | @@ -154,15 +157,13 @@ Binaries will be published once the codebase reaches a stable condition. | CodeLingoTeam | https://github.com/CodeLingoTeam | 1 | | Daanikus | https://github.com/Daanikus | 1 | | daniel-cohen | https://github.com/daniel-cohen | 1 | +| DirectX | https://github.com/DirectX | 1 | | frankzougc | https://github.com/frankzougc | 1 | | starit | https://github.com/starit | 1 | | Jimexist | https://github.com/Jimexist | 1 | | lookfirst | https://github.com/lookfirst | 1 | -| zeldrinn | https://github.com/zeldrinn | 1 | | mattkanwisher | https://github.com/mattkanwisher | 1 | | mKurrels | https://github.com/mKurrels | 1 | | m1kola | https://github.com/m1kola | 1 | | cavapoo2 | https://github.com/cavapoo2 | 1 | - - - +| zeldrinn | https://github.com/zeldrinn | 1 | \ No newline at end of file diff --git a/config/README.md b/config/README.md index 240b3191..77ff7223 100644 --- a/config/README.md +++ b/config/README.md @@ -85,7 +85,6 @@ have multiple deposit accounts for different FIAT deposit currencies. "websocketResponseCheckTimeout": 30000000, "websocketResponseMaxLimit": 7000000000, "httpTimeout": 15000000000, - "AuthenticatedAPISupport": false, "APIKey": "Key", "APISecret": "Secret", "AvailablePairs": "ATENC_GBP,ATENC_NZD,BTC_AUD,BTC_SGD,LTC_BTC,START_GBP,...", diff --git a/config/config_test.go b/config/config_test.go index eed76da8..48f6b9b1 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -13,7 +13,7 @@ import ( const ( // Default number of enabled exchanges. Modify this whenever an exchange is // added or removed - defaultEnabledExchanges = 27 + defaultEnabledExchanges = 28 ) func TestGetCurrencyConfig(t *testing.T) { @@ -479,7 +479,7 @@ func TestCountEnabledExchanges(t *testing.T) { } enabledExch := GetConfigEnabledExchanges.CountEnabledExchanges() if enabledExch != defaultEnabledExchanges { - t.Error("Test failed. GetConfigEnabledExchanges is wrong") + t.Errorf("Test failed. Expected %v, Received %v", defaultEnabledExchanges, enabledExch) } } diff --git a/config_example.json b/config_example.json index 9a776f74..d38b1aff 100644 --- a/config_example.json +++ b/config_example.json @@ -1081,6 +1081,48 @@ } ] }, + { + "name": "LBank", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "fbc_usdt,hds_usdt,galt_usdt,dxn_usdt,iog_usdt,ioex_usdt,vollar_usdt,oath_usdt,bloc_usdt,btc_lbcn,eth_lbcn,usdt_lbcn,btc_usdt,eth_usdt,eth_btc,abbc_btc,bzky_eth,onot_eth,kisc_eth,bxa_usdt,atp_usdt,mat_usdt,sky_btc,sky_lbcn,rnt_usdt,vena_usdt,grin_usdt,ida_usdt,pnt_usdt,bsv_btc,bsv_usdt,opx_usdt,tena_eth,seer_lbcn,vet_lbcn,vtho_btc,vnx_lbcn,vnx_btc,amo_eth,ubex_btc,eos_btc,ubex_usdt,tns_lbcn,tns_btc,ali_eth,sdc_eth,sait_eth,artcn_usdt,dax_btc,dax_eth,dali_usdt,vet_usdt,ten_usdt,bch_usdt,neo_usdt,qtum_usdt,zec_usdt,vet_btc,pai_btc,pnt_btc,bch_btc,ltc_btc,neo_btc,dash_btc,etc_btc,qtum_btc,zec_btc,sc_btc,bts_btc,cpx_btc,xwc_btc,fil6_btc,fil12_btc,fil36_btc,eos_usdt,ut_eth,ela_eth,vet_eth,vtho_eth,pai_eth,bfdt_eth,her_eth,ptt_eth,tac_eth,idhub_eth,ssc_eth,skm_eth,iic_eth,ply_eth,ext_eth,eos_eth,yoyow_eth,trx_eth,qtum_eth,zec_eth,bts_eth,btm_eth,mith_eth,nas_eth,man_eth,dbc_eth,bto_eth,ddd_eth,cpx_eth,cs_eth,iht_eth,tky_eth,ocn_eth,dct_eth,zpt_eth,eko_eth,mda_eth,pst_eth,xwc_eth,put_eth,pnt_eth,aac_eth,fil6_eth,fil12_eth,fil36_eth,uip_eth,seer_eth,bsb_eth,cdc_eth,grams_eth,ddmx_eth,eai_eth,inc_eth,bnb_usdt,ht_usdt,bot_eth,kbc_btc,kbc_usdt,mai_usdt,phv_usdt,hnb_usdt,gt_usdt,b91_usdt,voken_usdt,cye_usdt,brc_usdt,btc_ausd", + "enabledPairs": "btc_usdt", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": false, + "delimiter": "_" + }, + "requestCurrencyPairFormat": { + "uppercase": false, + "delimiter": "_" + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, { "name": "LocalBitcoins", "enabled": true, diff --git a/exchange.go b/exchange.go index f6f4e042..4e6a0c39 100644 --- a/exchange.go +++ b/exchange.go @@ -28,6 +28,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/itbit" "github.com/thrasher-corp/gocryptotrader/exchanges/kraken" "github.com/thrasher-corp/gocryptotrader/exchanges/lakebtc" + "github.com/thrasher-corp/gocryptotrader/exchanges/lbank" "github.com/thrasher-corp/gocryptotrader/exchanges/localbitcoins" "github.com/thrasher-corp/gocryptotrader/exchanges/okcoin" "github.com/thrasher-corp/gocryptotrader/exchanges/okex" @@ -173,6 +174,8 @@ func LoadExchange(name string, useWG bool, wg *sync.WaitGroup) error { exch = new(kraken.Kraken) case "lakebtc": exch = new(lakebtc.LakeBTC) + case "lbank": + exch = new(lbank.Lbank) case "localbitcoins": exch = new(localbitcoins.LocalBitcoins) case "okcoin international": diff --git a/exchanges/anx/anx_test.go b/exchanges/anx/anx_test.go index 776208b8..31aae652 100644 --- a/exchanges/anx/anx_test.go +++ b/exchanges/anx/anx_test.go @@ -445,3 +445,16 @@ func TestGetDepositAddress(t *testing.T) { } } } + +func TestUpdateOrderbook(t *testing.T) { + a.SetDefaults() + q := currency.Pair{ + Delimiter: "_", + Base: currency.BTC, + Quote: currency.USD} + + _, err := a.UpdateOrderbook(q, "spot") + if err != nil { + t.Fatalf("Update for orderbook failed: %v", err) + } +} diff --git a/exchanges/exchange.go b/exchanges/exchange.go index 8162182e..84b01c6a 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -787,7 +787,6 @@ func (e *Base) UpdateCurrencies(exchangeProducts currency.Pairs, enabled, force log.Debugf("%s Updating pairs - Removed: %s.\n", e.Name, removedPairs) } } - if enabled { exch.EnabledPairs = products e.EnabledPairs = products @@ -829,7 +828,7 @@ type Format struct { OrderSide map[string]string } -// CancelAllOrdersResponse returns the status from attempting to cancel all orders on an exchagne +// CancelAllOrdersResponse returns the status from attempting to cancel all orders on an exchange type CancelAllOrdersResponse struct { OrderStatus map[string]string } @@ -853,7 +852,7 @@ const ( // ToString changes the ordertype to the exchange standard and returns a string func (o OrderType) ToString() string { - return fmt.Sprintf("%v", o) + return string(o) } // OrderSide enforces a standard for OrderSides across the code base @@ -870,7 +869,7 @@ const ( // ToString changes the ordertype to the exchange standard and returns a string func (o OrderSide) ToString() string { - return fmt.Sprintf("%v", o) + return string(o) } // SetAPIURL sets configuration API URL for an exchange diff --git a/exchanges/lbank/README.md b/exchanges/lbank/README.md new file mode 100644 index 00000000..5325ce2b --- /dev/null +++ b/exchanges/lbank/README.md @@ -0,0 +1,133 @@ +# GoCryptoTrader package Lbank + + + + +[![Build Status](https://travis-ci.org/thrasher-corp/gocryptotrader.svg?branch=master)](https://travis-ci.org/thrasher-corp/gocryptotrader) +[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) +[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/exchanges/lbank) +[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This lbank package is part of the GoCryptoTrader codebase. + +## This is still in active development + +You can track ideas, planned features and what's in progresss on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTQyYjIxNGVhMWU5MDZlOGYzMmE0NTJmM2MzYWY5NGMzMmM4MzUwNTBjZTEzNjIwODM5NDcxODQwZDljMGQyNGY) + +## Lbank Exchange + +### Current Features + ++ REST Support + +### How to enable + ++ [Enable via configuration](https://githul.com/thrasher-corp/gocryptotrader/tree/master/config#enable-exchange-via-config-example) + ++ Individual package example below: + +```go + // Exchanges will be abstracted out in further updates and examples will be + // supplied then +``` + +### How to do REST public/private calls + ++ If enabled via "configuration".json file the exchange will be added to the +IBotExchange array in the ```go var bot Bot``` and you will only be able to use +the wrapper interface functions for accessing exchange data. View routines.go +for an example of integration usage with GoCryptoTrader. Rudimentary example +below: + +main.go +```go +var l exchange.IBotExchange + +for i := range bot.exchanges { + if bot.exchanges[i].GetName() == "Lbank" { + l = bot.exchanges[i] + } +} + +// Public calls - wrapper functions + +// Fetches current ticker information +tick, err := l.GetTickerPrice() +if err != nil { + // Handle error +} + +// Fetches current orderbook information +ob, err := l.GetOrderbookEx() +if err != nil { + // Handle error +} + +// Private calls - wrapper functions - make sure your APIKEY and APISECRET are +// set and AuthenticatedAPISupport is set to true + +// Fetches current account information +accountInfo, err := l.GetAccountInfo() +if err != nil { + // Handle error +} +``` + ++ If enabled via individually importing package, rudimentary example below: + +```go +// Public calls + +// Fetches current ticker information +ticker, err := l.GetTicker() +if err != nil { + // Handle error +} + +// Fetches current orderbook information +ob, err := l.GetOrderBook() +if err != nil { + // Handle error +} + +// Private calls - make sure your APIKEY and APISECRET are set and +// AuthenticatedAPISupport is set to true + +// GetUserInfo returns account info +accountInfo, err := l.GetUserInfo(...) +if err != nil { + // Handle error +} + +// Submits an order and the exchange and returns its tradeID +tradeID, err := l.Trade(...) +if err != nil { + // Handle error +} +``` + +### Please click GoDocs chevron above to view current GoDoc information for this package + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + +***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** + diff --git a/exchanges/lbank/lbank.go b/exchanges/lbank/lbank.go new file mode 100644 index 00000000..afab0f2f --- /dev/null +++ b/exchanges/lbank/lbank.go @@ -0,0 +1,622 @@ +package lbank + +import ( + "bytes" + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" + log "github.com/thrasher-corp/gocryptotrader/logger" +) + +// Lbank is the overarching type across this package +type Lbank struct { + exchange.Base + privateKey *rsa.PrivateKey + WebsocketConn *wshandler.WebsocketConnection +} + +const ( + lbankAPIURL = "https://api.lbkex.com" + lbankAPIVersion = "1" + lbankAuthRateLimit = 0 + lbankUnAuthRateLimit = 0 + lbankFeeNotFound = 0.0 + + // Public endpoints + lbankTicker = "ticker.do" + lbankCurrencyPairs = "currencyPairs.do" + lbankMarketDepths = "depth.do" + lbankTrades = "trades.do" + lbankKlines = "kline.do" + lbankPairInfo = "accuracy.do" + lbankUSD2CNYRate = "usdToCny.do" + lbankWithdrawConfig = "withdrawConfigs.do" + + // Authenticated endpoints + lbankUserInfo = "user_info.do" + lbankPlaceOrder = "create_order.do" + lbankCancelOrder = "cancel_order.do" + lbankQueryOrder = "orders_info.do" + lbankQueryHistoryOrder = "orders_info_history.do" + lbankOrderTransactionDetails = "order_transaction_detail.do" + lbankPastTransactions = "transaction_history.do" + lbankOpeningOrders = "orders_info_no_deal.do" + lbankWithdrawalRecords = "withdraws.do" + lbankWithdraw = "withdraw.do" + lbankRevokeWithdraw = "withdrawCancel.do" +) + +// SetDefaults sets the basic defaults for Lbank +func (l *Lbank) SetDefaults() { + l.Name = "Lbank" + l.RESTPollingDelay = 10 + l.RequestCurrencyPairFormat.Delimiter = "_" + l.ConfigCurrencyPairFormat.Delimiter = "_" + l.AssetTypes = []string{ticker.Spot} + l.SupportsAutoPairUpdating = true + l.APIWithdrawPermissions = exchange.AutoWithdrawCryptoWithAPIPermission | exchange.NoFiatWithdrawals + l.Requester = request.New(l.Name, + request.NewRateLimit(time.Second, lbankAuthRateLimit), + request.NewRateLimit(time.Second, lbankUnAuthRateLimit), + common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout)) + l.APIUrlDefault = lbankAPIURL + l.APIUrl = l.APIUrlDefault + l.Websocket = wshandler.New() +} + +// Setup takes in the supplied exchange configuration details and sets params +func (l *Lbank) Setup(exch *config.ExchangeConfig) { + if !exch.Enabled { + l.SetEnabled(false) + } else { + l.Enabled = true + l.AuthenticatedAPISupport = exch.AuthenticatedAPISupport + l.AuthenticatedWebsocketAPISupport = exch.AuthenticatedWebsocketAPISupport + l.SetAPIKeys(exch.APIKey, exch.APISecret, "", false) + l.SetHTTPClientTimeout(exch.HTTPTimeout) + l.SetHTTPClientUserAgent(exch.HTTPUserAgent) + l.RESTPollingDelay = exch.RESTPollingDelay + l.Verbose = exch.Verbose + l.Websocket.SetWsStatusAndConnection(exch.Websocket) + l.BaseCurrencies = exch.BaseCurrencies + l.AvailablePairs = exch.AvailablePairs + l.EnabledPairs = exch.EnabledPairs + err := l.SetCurrencyPairFormat() + if err != nil { + log.Fatal(err) + } + err = l.SetAssetTypes() + if err != nil { + log.Fatal(err) + } + err = l.SetAutoPairDefaults() + if err != nil { + log.Fatal(err) + } + err = l.SetAPIURL(exch) + if err != nil { + log.Fatal(err) + } + err = l.SetClientProxyAddress(exch.ProxyAddress) + if err != nil { + log.Fatal(err) + } + if l.AuthenticatedAPISupport { + err = l.loadPrivKey() + if err != nil { + l.AuthenticatedAPISupport = false + log.Errorf("couldnt load private key, setting authenticated support to false") + } + } + } +} + +// GetTicker returns a ticker for the specified symbol +// symbol: eth_btc +func (l *Lbank) GetTicker(symbol string) (TickerResponse, error) { + var t TickerResponse + params := url.Values{} + params.Set("symbol", symbol) + path := fmt.Sprintf("%s/v%s/%s?%s", l.APIUrl, lbankAPIVersion, lbankTicker, params.Encode()) + return t, l.SendHTTPRequest(path, &t) +} + +// GetCurrencyPairs returns a list of supported currency pairs by the exchange +func (l *Lbank) GetCurrencyPairs() ([]string, error) { + path := fmt.Sprintf("%s/v%s/%s", l.APIUrl, lbankAPIVersion, + lbankCurrencyPairs) + var result []string + return result, l.SendHTTPRequest(path, &result) +} + +// GetMarketDepths returns arrays of asks, bids and timestamp +func (l *Lbank) GetMarketDepths(symbol, size, merge string) (MarketDepthResponse, error) { + var m MarketDepthResponse + params := url.Values{} + params.Set("symbol", symbol) + params.Set("size", size) + params.Set("merge", merge) + path := fmt.Sprintf("%s/v%s/%s?%s", l.APIUrl, lbankAPIVersion, lbankMarketDepths, params.Encode()) + return m, l.SendHTTPRequest(path, &m) +} + +// GetTrades returns an array of available trades regarding a particular exchange +func (l *Lbank) GetTrades(symbol, size, time string) ([]TradeResponse, error) { + var g []TradeResponse + params := url.Values{} + params.Set("symbol", symbol) + params.Set("size", size) + params.Set("time", time) + path := fmt.Sprintf("%s/v%s/%s?%s", l.APIUrl, lbankAPIVersion, lbankTrades, params.Encode()) + return g, l.SendHTTPRequest(path, &g) +} + +// GetKlines returns kline data +func (l *Lbank) GetKlines(symbol, size, klineType, time string) ([]KlineResponse, error) { + var klineTemp interface{} + var k []KlineResponse + params := url.Values{} + params.Set("symbol", symbol) + params.Set("size", size) + params.Set("type", klineType) + params.Set("time", time) + path := fmt.Sprintf("%s/v%s/%s?%s", l.APIUrl, lbankAPIVersion, lbankKlines, params.Encode()) + err := l.SendHTTPRequest(path, &klineTemp) + if err != nil { + return k, err + } + + resp, ok := klineTemp.([]interface{}) + if !ok { + return nil, errors.New("response received is invalid") + } + + for i := range resp { + resp2, ok := resp[i].([]interface{}) + if !ok { + return nil, errors.New("response received is invalid") + } + var tempResp KlineResponse + for x := range resp2 { + switch x { + case 0: + tempResp.TimeStamp = int64(resp2[x].(float64)) + case 1: + if val, ok := resp2[x].(int64); ok { + tempResp.OpenPrice = float64(val) + } else { + tempResp.OpenPrice = resp2[x].(float64) + } + case 2: + if val, ok := resp2[x].(int64); ok { + tempResp.HigestPrice = float64(val) + } else { + tempResp.HigestPrice = resp2[x].(float64) + } + case 3: + if val, ok := resp2[x].(int64); ok { + tempResp.LowestPrice = float64(val) + } else { + tempResp.LowestPrice = resp2[x].(float64) + } + + case 4: + if val, ok := resp2[x].(int64); ok { + tempResp.ClosePrice = float64(val) + } else { + tempResp.ClosePrice = resp2[x].(float64) + } + + case 5: + if val, ok := resp2[x].(int64); ok { + tempResp.TradingVolume = float64(val) + } else { + tempResp.TradingVolume = resp2[x].(float64) + } + + } + } + k = append(k, tempResp) + } + return k, nil +} + +// GetUserInfo gets users account info +func (l *Lbank) GetUserInfo() (InfoFinalResponse, error) { + var resp InfoFinalResponse + path := fmt.Sprintf("%s/v%s/%s?", l.APIUrl, lbankAPIVersion, lbankUserInfo) + err := l.SendAuthHTTPRequest(http.MethodPost, path, nil, &resp) + if err != nil { + return resp, err + } + + if resp.Error != 0 { + return resp, ErrorCapture(resp.Error) + } + + return resp, nil +} + +// CreateOrder creates an order +func (l *Lbank) CreateOrder(pair, side string, amount, price float64) (CreateOrderResponse, error) { + var resp CreateOrderResponse + if !strings.EqualFold(side, "buy") && !strings.EqualFold(side, "sell") { + return resp, errors.New("side type invalid can only be 'buy' or 'sell'") + } + if amount <= 0 { + return resp, errors.New("amount can't be smaller than or equal to 0") + } + if price <= 0 { + return resp, errors.New("price can't be smaller than or equal to 0") + } + params := url.Values{} + + params.Set("symbol", pair) + params.Set("type", common.StringToLower(side)) + params.Set("price", strconv.FormatFloat(price, 'f', -1, 64)) + params.Set("amount", strconv.FormatFloat(amount, 'f', -1, 64)) + path := fmt.Sprintf("%s/v%s/%s?", l.APIUrl, lbankAPIVersion, lbankPlaceOrder) + err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &resp) + if err != nil { + return resp, err + } + + if resp.Error != 0 { + return resp, ErrorCapture(resp.Error) + } + + return resp, nil +} + +// RemoveOrder cancels a given order +func (l *Lbank) RemoveOrder(pair, orderID string) (RemoveOrderResponse, error) { + var resp RemoveOrderResponse + params := url.Values{} + params.Set("symbol", pair) + params.Set("order_id", orderID) + path := fmt.Sprintf("%s/v%s/%s", l.APIUrl, lbankAPIVersion, lbankCancelOrder) + err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &resp) + if err != nil { + return resp, err + } + + if resp.Error != 0 { + return resp, ErrorCapture(resp.Error) + } + + return resp, nil +} + +// QueryOrder finds out information about orders (can pass up to 3 comma separated values to this) +// Lbank returns an empty string as their []OrderResponse instead of returning an empty array, so when len(tempResp.Orders) > 2 its not empty and should be unmarshalled separately +func (l *Lbank) QueryOrder(pair, orderIDs string) (QueryOrderFinalResponse, error) { + var resp QueryOrderFinalResponse + var tempResp QueryOrderResponse + params := url.Values{} + params.Set("symbol", pair) + params.Set("order_id", orderIDs) + path := fmt.Sprintf("%s/v%s/%s?", l.APIUrl, lbankAPIVersion, lbankQueryOrder) + err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &tempResp) + if err != nil { + return resp, err + } + + var totalOrders []OrderResponse + if len(tempResp.Orders) > 2 { + err = json.Unmarshal(tempResp.Orders, &totalOrders) + if err != nil { + return resp, err + } + } + resp.ErrCapture = tempResp.ErrCapture + resp.Orders = totalOrders + + if err != nil { + return resp, err + } + + if resp.Error != 0 { + return resp, ErrorCapture(resp.Error) + } + + return resp, nil +} + +// QueryOrderHistory finds order info in the past 2 days +// Lbank returns an empty string as their []OrderResponse instead of returning an empty array, so when len(tempResp.Orders) > 2 its not empty and should be unmarshalled separately +func (l *Lbank) QueryOrderHistory(pair, pageNumber, pageLength string) (OrderHistoryFinalResponse, error) { + var resp OrderHistoryFinalResponse + var tempResp OrderHistoryResponse + params := url.Values{} + params.Set("symbol", pair) + params.Set("current_page", pageNumber) + params.Set("page_length", pageLength) + path := fmt.Sprintf("%s/v%s/%s?", l.APIUrl, lbankAPIVersion, lbankQueryHistoryOrder) + err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &tempResp) + if err != nil { + return resp, err + } + + var totalOrders []OrderResponse + if len(tempResp.Orders) > 2 { + err = json.Unmarshal(tempResp.Orders, &totalOrders) + if err != nil { + return resp, err + } + } + resp.ErrCapture = tempResp.ErrCapture + resp.PageLength = tempResp.PageLength + resp.Orders = totalOrders + resp.CurrentPage = tempResp.CurrentPage + + if resp.Error != 0 { + return resp, ErrorCapture(resp.Error) + } + + return resp, nil +} + +// GetPairInfo finds information about all trading pairs +func (l *Lbank) GetPairInfo() ([]PairInfoResponse, error) { + var resp []PairInfoResponse + path := fmt.Sprintf("%s/v%s/%s?", l.APIUrl, lbankAPIVersion, lbankPairInfo) + return resp, l.SendHTTPRequest(path, &resp) +} + +// OrderTransactionDetails gets info about transactions +func (l *Lbank) OrderTransactionDetails(symbol, orderID string) (TransactionHistoryResp, error) { + var resp TransactionHistoryResp + params := url.Values{} + params.Set("symbol", symbol) + params.Set("order_id", orderID) + path := fmt.Sprintf("%s/v%s/%s?", l.APIUrl, lbankAPIVersion, lbankOrderTransactionDetails) + err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &resp) + if err != nil { + return resp, err + } + + if resp.Error != 0 { + return resp, ErrorCapture(resp.Error) + } + + return resp, nil +} + +// TransactionHistory stores info about transactions +func (l *Lbank) TransactionHistory(symbol, transactionType, startDate, endDate, from, direct, size string) (TransactionHistoryResp, error) { + var resp TransactionHistoryResp + params := url.Values{} + params.Set("symbol", symbol) + params.Set("type", transactionType) + params.Set("start_date", startDate) + params.Set("end_date", endDate) + params.Set("from", from) + params.Set("direct", direct) + params.Set("size", size) + path := fmt.Sprintf("%s/v%s/%s?", l.APIUrl, lbankAPIVersion, lbankPastTransactions) + err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &resp) + if err != nil { + return resp, err + } + + if resp.Error != 0 { + return resp, ErrorCapture(resp.Error) + } + + return resp, nil +} + +// GetOpenOrders gets opening orders +// Lbank returns an empty string as their []OrderResponse instead of returning an empty array, so when len(tempResp.Orders) > 2 its not empty and should be unmarshalled separately +func (l *Lbank) GetOpenOrders(pair, pageNumber, pageLength string) (OpenOrderFinalResponse, error) { + var resp OpenOrderFinalResponse + var tempResp OpenOrderResponse + params := url.Values{} + params.Set("symbol", pair) + params.Set("current_page", pageNumber) + params.Set("page_length", pageLength) + path := fmt.Sprintf("%s/v%s/%s", l.APIUrl, lbankAPIVersion, lbankOpeningOrders) + err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &tempResp) + if err != nil { + return resp, err + } + + var totalOrders []OrderResponse + if len(tempResp.Orders) > 2 { + err = json.Unmarshal(tempResp.Orders, &totalOrders) + if err != nil { + return resp, err + } + } + resp.ErrCapture = tempResp.ErrCapture + resp.PageLength = tempResp.PageLength + resp.PageNumber = tempResp.PageNumber + resp.Orders = totalOrders + + if resp.Error != 0 { + return resp, ErrorCapture(resp.Error) + } + + return resp, nil +} + +// USD2RMBRate finds USD-CNY Rate +func (l *Lbank) USD2RMBRate() (ExchangeRateResponse, error) { + var resp ExchangeRateResponse + path := fmt.Sprintf("%s/v%s/%s", l.APIUrl, lbankAPIVersion, lbankUSD2CNYRate) + return resp, l.SendHTTPRequest(path, &resp) +} + +// GetWithdrawConfig gets information about withdrawals +func (l *Lbank) GetWithdrawConfig(assetCode string) ([]WithdrawConfigResponse, error) { + var resp []WithdrawConfigResponse + params := url.Values{} + params.Set("assetCode", assetCode) + path := fmt.Sprintf("%s/v%s/%s?%s", l.APIUrl, lbankAPIVersion, lbankWithdrawConfig, params.Encode()) + return resp, l.SendHTTPRequest(path, &resp) +} + +// Withdraw sends a withdrawal request +func (l *Lbank) Withdraw(account, assetCode, amount, memo, mark string) (WithdrawResponse, error) { + var resp WithdrawResponse + params := url.Values{} + params.Set("account", account) + params.Set("assetCode", assetCode) + params.Set("amount", amount) + if memo != "" { + params.Set("memo", memo) + } + if mark != "" { + params.Set("mark", mark) + } + path := fmt.Sprintf("%s/v%s/%s", l.APIUrl, lbankAPIVersion, lbankWithdraw) + err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &resp) + if err != nil { + return resp, err + } + + if resp.Error != 0 { + return resp, ErrorCapture(resp.Error) + } + + return resp, nil +} + +// RevokeWithdraw cancels the withdrawal given the withdrawalID +func (l *Lbank) RevokeWithdraw(withdrawID string) (RevokeWithdrawResponse, error) { + var resp RevokeWithdrawResponse + params := url.Values{} + if withdrawID != "" { + params.Set("withdrawId", withdrawID) + } + path := fmt.Sprintf("%s/v%s/%s?", l.APIUrl, lbankAPIVersion, lbankRevokeWithdraw) + err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &resp) + if err != nil { + return resp, err + } + + if resp.Error != 0 { + return resp, ErrorCapture(resp.Error) + } + + return resp, nil +} + +// GetWithdrawalRecords gets withdrawal records +func (l *Lbank) GetWithdrawalRecords(assetCode, status, pageNo, pageSize string) (WithdrawalResponse, error) { + var resp WithdrawalResponse + params := url.Values{} + params.Set("assetCode", assetCode) + params.Set("status", status) + params.Set("pageNo", pageNo) + params.Set("pageSize", pageSize) + path := fmt.Sprintf("%s/v%s/%s", l.APIUrl, lbankAPIVersion, lbankWithdrawalRecords) + err := l.SendAuthHTTPRequest(http.MethodPost, path, params, &resp) + if err != nil { + return resp, err + } + + if resp.Error != 0 { + return resp, ErrorCapture(resp.Error) + } + + return resp, nil +} + +// ErrorCapture captures errors +func ErrorCapture(code int64) error { + msg, ok := errorCodes[code] + if !ok { + return fmt.Errorf("undefined code please check api docs for error code definition: %v", code) + } + return errors.New(msg) +} + +// SendHTTPRequest sends an unauthenticated HTTP request +func (l *Lbank) SendHTTPRequest(path string, result interface{}) error { + return l.SendPayload(http.MethodGet, path, nil, nil, &result, false, false, l.Verbose, l.HTTPDebugging) +} + +func (l *Lbank) loadPrivKey() error { + key := strings.Join([]string{ + "-----BEGIN RSA PRIVATE KEY-----", + l.APISecret, + "-----END RSA PRIVATE KEY-----", + }, "\n") + + block, _ := pem.Decode([]byte(key)) + if block == nil { + return errors.New("pem block is nil") + } + + p, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return fmt.Errorf("unable to decode priv key: %s", err) + } + + var ok bool + l.privateKey, ok = p.(*rsa.PrivateKey) + if !ok { + return errors.New("unable to parse RSA private key") + } + return nil +} + +func (l *Lbank) sign(data string) (string, error) { + if l.privateKey == nil { + return "", errors.New("private key not loaded") + } + md5hash := common.GetMD5([]byte(data)) + m := common.StringToUpper(common.HexEncodeToString(md5hash)) + s := common.GetSHA256([]byte(m)) + r, err := rsa.SignPKCS1v15(rand.Reader, l.privateKey, crypto.SHA256, s) + if err != nil { + return "", err + } + return common.Base64Encode(r), nil +} + +// SendAuthHTTPRequest sends an authenticated request +func (l *Lbank) SendAuthHTTPRequest(method, endpoint string, vals url.Values, result interface{}) error { + if vals == nil { + vals = url.Values{} + } + + vals.Set("api_key", l.APIKey) + sig, err := l.sign(vals.Encode()) + if err != nil { + return err + } + + vals.Set("sign", sig) + payload := vals.Encode() + headers := make(map[string]string) + headers["Content-Type"] = "application/x-www-form-urlencoded" + + return l.SendPayload(method, + endpoint, + headers, + bytes.NewBufferString(payload), + &result, + true, + false, + l.Verbose, + l.HTTPDebugging) +} diff --git a/exchanges/lbank/lbank_test.go b/exchanges/lbank/lbank_test.go new file mode 100644 index 00000000..25e143f1 --- /dev/null +++ b/exchanges/lbank/lbank_test.go @@ -0,0 +1,387 @@ +package lbank + +import ( + "fmt" + "sync" + "testing" + "time" + + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" +) + +// Please supply your own keys here for due diligence testing +const ( + testAPIKey = "" + testAPISecret = "" + canManipulateRealOrders = false +) + +var l Lbank +var setupRan bool +var m sync.Mutex + +func TestSetup(t *testing.T) { + t.Parallel() + m.Lock() + defer m.Unlock() + + if setupRan { + return + } + l.SetDefaults() + cfg := config.GetConfig() + err := cfg.LoadConfig("../../testdata/configtest.json") + if err != nil { + t.Errorf("Test Failed - Lbank Setup() init error:, %v", err) + } + lbankConfig, err := cfg.GetExchangeConfig("Lbank") + if err != nil { + t.Errorf("Test Failed - Lbank Setup() init error: %v", err) + } + lbankConfig.Websocket = true + lbankConfig.AuthenticatedAPISupport = true + lbankConfig.APISecret = testAPISecret + lbankConfig.APIKey = testAPIKey + l.Setup(&lbankConfig) + setupRan = true +} + +func areTestAPIKeysSet() bool { + if l.APIKey != "" && l.APIKey != "Key" && + l.APISecret != "" && l.APISecret != "Secret" { + return true + } + return false +} + +func TestGetTicker(t *testing.T) { + TestSetup(t) + _, err := l.GetTicker("btc_usdt") + if err != nil { + t.Errorf("test failed: %v", err) + } +} + +func TestGetCurrencyPairs(t *testing.T) { + TestSetup(t) + _, err := l.GetCurrencyPairs() + if err != nil { + t.Errorf("test failed: %v", err) + } +} + +func TestGetMarketDepths(t *testing.T) { + TestSetup(t) + _, err := l.GetMarketDepths("btc_usdt", "60", "1") + if err != nil { + t.Errorf("GetMarketDepth failed: %v", err) + } + a, _ := l.GetMarketDepths("btc_usdt", "60", "0") + if len(a.Asks) != 60 { + t.Errorf("length requested doesnt match the output") + } +} + +func TestGetTrades(t *testing.T) { + TestSetup(t) + _, err := l.GetTrades("btc_usdt", "600", fmt.Sprintf("%v", time.Now().Unix())) + if err != nil { + t.Errorf("test failed: %v", err) + } + a, err := l.GetTrades("btc_usdt", "600", "0") + if len(a) != 600 && err != nil { + t.Errorf("test failed: %v", err) + } +} + +func TestGetKlines(t *testing.T) { + TestSetup(t) + _, err := l.GetKlines("btc_usdt", "600", "minute1", fmt.Sprintf("%v", time.Now().Unix())) + if err != nil { + t.Errorf("test failed: %v", err) + } +} + +func TestUpdateOrderbook(t *testing.T) { + TestSetup(t) + p := currency.Pair{ + Delimiter: "_", + Base: currency.ETH, + Quote: currency.BTC} + + _, err := l.UpdateOrderbook(p.Lower(), "spot") + if err != nil { + t.Errorf("Update for orderbook failed: %v", err) + } +} + +func TestGetUserInfo(t *testing.T) { + TestSetup(t) + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := l.GetUserInfo() + if err != nil { + t.Errorf("invalid key or sign: %v", err) + } +} + +func TestCreateOrder(t *testing.T) { + TestSetup(t) + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") + } + cp := currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_") + _, err := l.CreateOrder(cp.Lower().String(), "what", 1231, 12314) + if err == nil { + t.Error("Test Failed - CreateOrder error cannot be nil") + } + _, err = l.CreateOrder(cp.Lower().String(), "buy", 0, 0) + if err == nil { + t.Error("Test Failed - CreateOrder error cannot be nil") + } + _, err = l.CreateOrder(cp.Lower().String(), "sell", 1231, 0) + if err == nil { + t.Error("Test Failed - CreateOrder error cannot be nil") + } + _, err = l.CreateOrder(cp.Lower().String(), "buy", 58, 681) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } +} + +func TestRemoveOrder(t *testing.T) { + TestSetup(t) + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") + } + cp := currency.NewPairWithDelimiter(currency.ETH.String(), currency.BTC.String(), "_") + _, err := l.RemoveOrder(cp.Lower().String(), "24f7ce27-af1d-4dca-a8c1-ef1cbeec1b23") + if err != nil { + t.Errorf("unable to remove order: %v", err) + } +} + +func TestQueryOrder(t *testing.T) { + TestSetup(t) + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + cp := currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_") + _, err := l.QueryOrder(cp.Lower().String(), "1") + if err != nil { + t.Errorf("unexpected error: %v", err) + } +} + +func TestQueryOrderHistory(t *testing.T) { + TestSetup(t) + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + cp := currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_") + _, err := l.QueryOrderHistory(cp.Lower().String(), "1", "100") + if err != nil { + t.Errorf("test failed: %v", err) + } +} + +func TestGetPairInfo(t *testing.T) { + TestSetup(t) + _, err := l.GetPairInfo() + if err != nil { + t.Errorf("couldnt get pair info: %v", err) + } +} + +func TestOrderTransactionDetails(t *testing.T) { + TestSetup(t) + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := l.OrderTransactionDetails("eth_btc", "24f7ce27-af1d-4dca-a8c1-ef1cbeec1b23") + if err != nil { + t.Errorf("couldnt get transaction details: %v", err) + } +} + +func TestTransactionHistory(t *testing.T) { + TestSetup(t) + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := l.TransactionHistory("btc_usdt", "", "", "", "", "", "") + if err != nil { + t.Errorf("couldnt get transaction history: %v", err) + } +} + +func TestGetOpenOrders(t *testing.T) { + TestSetup(t) + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + cp := currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_") + _, err := l.GetOpenOrders(cp.Lower().String(), "1", "50") + if err != nil { + t.Error("unexpected error", err) + } +} + +func TestUSD2RMBRate(t *testing.T) { + TestSetup(t) + _, err := l.USD2RMBRate() + if err != nil { + t.Error("unable to acquire the rate") + } +} + +func TestGetWithdrawConfig(t *testing.T) { + TestSetup(t) + _, err := l.GetWithdrawConfig("eth") + if err != nil { + t.Errorf("unable to get withdraw config: %v", err) + } +} + +func TestWithdraw(t *testing.T) { + TestSetup(t) + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") + } + _, err := l.Withdraw("", "", "", "", "") + if err != nil { + t.Errorf("unable to withdraw: %v", err) + } +} + +func TestGetWithdrawRecords(t *testing.T) { + TestSetup(t) + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := l.GetWithdrawalRecords("eth", "0", "1", "20") + if err != nil { + t.Errorf("unable to get withdrawal records: %v", err) + } +} + +func TestLoadPrivKey(t *testing.T) { + TestSetup(t) + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + err := l.loadPrivKey() + if err != nil { + t.Error(err) + } + l.APISecret = "errortest" + err = l.loadPrivKey() + if err == nil { + t.Errorf("expected error due to pemblock nil, got err: %v", err) + } +} + +func TestSign(t *testing.T) { + TestSetup(t) + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + l.APISecret = testAPISecret + l.loadPrivKey() + _, err := l.sign("hello123") + if err != nil { + t.Errorf("test failed: %v", err) + } +} + +func TestSubmitOrder(t *testing.T) { + TestSetup(t) + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + cp := currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_") + _, err := l.SubmitOrder(cp.Lower(), "BUY", "ANY", 2, 1312, "") + if err != nil { + t.Errorf("test failed: %v", err) + } +} + +func TestCancelOrder(t *testing.T) { + TestSetup(t) + if !areTestAPIKeysSet() || !canManipulateRealOrders { + t.Skip("skipping test, either api keys or manipulaterealorders isnt set correctly") + } + cp := currency.NewPairWithDelimiter(currency.ETH.String(), currency.BTC.String(), "_") + var a exchange.OrderCancellation + a.CurrencyPair = cp + a.OrderID = "24f7ce27-af1d-4dca-a8c1-ef1cbeec1b23" + err := l.CancelOrder(&a) + if err != nil { + t.Errorf("test failed: %v", err) + } +} + +func TestGetOrderInfo(t *testing.T) { + TestSetup(t) + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := l.GetOrderInfo("9ead39f5-701a-400b-b635-d7349eb0f6b") + if err != nil { + t.Errorf("test failed: %v", err) + } +} + +func TestGetAllOpenOrderID(t *testing.T) { + TestSetup(t) + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := l.getAllOpenOrderID() + if err != nil { + t.Errorf("test failed: %v", err) + } +} + +func TestGetFeeByType(t *testing.T) { + TestSetup(t) + cp := currency.NewPairWithDelimiter(currency.BTC.String(), currency.USDT.String(), "_") + var input exchange.FeeBuilder + input.Amount = 2 + input.FeeType = exchange.CryptocurrencyWithdrawalFee + input.Pair = cp + a, err := l.GetFeeByType(&input) + if err != nil { + t.Errorf("test failed. couldnt get fee: %v", err) + } + if a != 0.0005 { + t.Errorf("testGetFeeByType failed. Expected: 0.0005, Received: %v", a) + } +} + +func TestGetAccountInfo(t *testing.T) { + TestSetup(t) + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + _, err := l.GetAccountInfo() + if err != nil { + t.Error(err) + } +} + +func TestGetOrderHistory(t *testing.T) { + TestSetup(t) + if !areTestAPIKeysSet() { + t.Skip("API keys required but not set, skipping test") + } + var input exchange.GetOrdersRequest + input.OrderSide = exchange.BuyOrderSide + _, err := l.GetOrderHistory(&input) + if err != nil { + t.Error(err) + } +} diff --git a/exchanges/lbank/lbank_types.go b/exchanges/lbank/lbank_types.go new file mode 100644 index 00000000..cb6653ac --- /dev/null +++ b/exchanges/lbank/lbank_types.go @@ -0,0 +1,270 @@ +package lbank + +import ( + "encoding/json" +) + +// Ticker stores the ticker price data for a currency pair +type Ticker struct { + Change float64 `json:"change"` + High float64 `json:"high"` + Latest float64 `json:"latest"` + Low float64 `json:"low"` + Turnover float64 `json:"turnover"` + Volume float64 `json:"vol"` +} + +// TickerResponse stores the ticker price data and timestamp for a currency pair +type TickerResponse struct { + Symbol string `json:"symbol"` + Timestamp int64 `json:"timestamp"` + Ticker Ticker `json:"ticker"` +} + +// MarketDepthResponse stores arrays for asks, bids and a timestamp for a currecy pair +type MarketDepthResponse struct { + ErrCapture `json:",omitempty"` + Asks [][]float64 `json:"asks"` + Bids [][]float64 `json:"bids"` + Timestamp int64 `json:"timestamp"` +} + +// TradeResponse stores date_ms, amount, price, type, tid for a currency pair +type TradeResponse struct { + DateMS int64 `json:"date_ms"` + Amount float64 `json:"amount"` + Price float64 `json:"price"` + Type string `json:"type"` + TID string `json:"tid"` +} + +// KlineResponse stores kline info for given currency exchange +type KlineResponse struct { + TimeStamp int64 `json:"timestamp"` + OpenPrice float64 `json:"openprice"` + HigestPrice float64 `json:"highestprice"` + LowestPrice float64 `json:"lowestprice"` + ClosePrice float64 `json:"closeprice"` + TradingVolume float64 `json:"tradingvolume"` +} + +// InfoResponse stores info +type InfoResponse struct { + Freeze map[string]string `json:"freeze"` + Asset map[string]string `json:"asset"` + Free map[string]string `json:"Free"` +} + +// InfoFinalResponse stores info +type InfoFinalResponse struct { + ErrCapture `json:",omitempty"` + Info InfoResponse `json:"info"` +} + +// CreateOrderResponse stores the result of the Order and +type CreateOrderResponse struct { + ErrCapture `json:",omitempty"` + OrderID string `json:"order_id"` +} + +// RemoveOrderResponse stores the result when an order is cancelled +type RemoveOrderResponse struct { + ErrCapture `json:",omitempty"` + Err string `json:"error"` + OrderID string `json:"order_id"` + Success string `json:"success"` +} + +// OrderResponse stores the data related to the given OrderIDs +type OrderResponse struct { + Symbol string `json:"symbol"` + Amount float64 `json:"amount"` + CreateTime int64 `json:"created_time"` + Price float64 `json:"price"` + AvgPrice float64 `json:"avg_price"` + Type string `json:"type"` + OrderID string `json:"order_id"` + DealAmount float64 `json:"deal_amount"` + Status int64 `json:"status"` +} + +// QueryOrderResponse stores the data from queries +type QueryOrderResponse struct { + ErrCapture `json:",omitempty"` + Orders json.RawMessage `json:"orders"` +} + +// QueryOrderFinalResponse stores data from queries +type QueryOrderFinalResponse struct { + ErrCapture + Orders []OrderResponse +} + +// OrderHistory stores data for past orders +type OrderHistory struct { + Result bool `json:"result,string"` + Total string `json:"total"` + PageLength uint8 `json:"page_length"` + Orders json.RawMessage `json:"orders"` + CurrentPage uint8 `json:"current_page"` + ErrorCode int64 `json:"error_code"` +} + +// OrderHistoryResponse stores past orders +type OrderHistoryResponse struct { + ErrCapture `json:",omitempty"` + PageLength uint8 `json:"page_length"` + Orders json.RawMessage `json:"orders"` + CurrentPage uint8 `json:"current_page"` +} + +// OrderHistoryFinalResponse stores past orders +type OrderHistoryFinalResponse struct { + ErrCapture + PageLength uint8 + Orders []OrderResponse + CurrentPage uint8 +} + +// PairInfoResponse stores information about trading pairs +type PairInfoResponse struct { + MinimumQuantity string `json:"minTranQua"` + PriceAccuracy string `json:"priceAccuracy"` + QuantityAccuracy string `json:"quantityAccuracy"` + Symbol string `json:"symbol"` +} + +// TransactionTemp stores details about transactions +type TransactionTemp struct { + TxUUID string `json:"txUuid"` + OrderUUID string `json:"orderUuid"` + TradeType string `json:"tradeType"` + DealTime int64 `json:"dealTime"` + DealPrice float64 `json:"dealPrice"` + DealQuantity float64 `json:"dealQuantity"` + DealVolPrice float64 `json:"dealVolumePrice"` + TradeFee float64 `json:"tradeFee"` + TradeFeeRate float64 `json:"tradeFeeRate"` +} + +// TransactionHistoryResp stores details about past transactions +type TransactionHistoryResp struct { + ErrCapture `json:",omitempty"` + Transaction []TransactionTemp `json:"transaction"` +} + +// OpenOrderResponse stores information about the opening orders +type OpenOrderResponse struct { + ErrCapture `json:",omitempty"` + PageLength uint8 `json:"page_length"` + PageNumber uint8 `json:"page_number"` + Total string `json:"total"` + Orders json.RawMessage `json:"orders"` +} + +// OpenOrderFinalResponse stores the unmarshalled value of OpenOrderResponse +type OpenOrderFinalResponse struct { + ErrCapture + PageLength uint8 + PageNumber uint8 + Total string + Orders []OrderResponse +} + +// ExchangeRateResponse stores information about USD-RMB rate +type ExchangeRateResponse struct { + USD2CNY string `json:"USD2CNY"` +} + +// WithdrawConfigResponse stores info about withdrawal configurations +type WithdrawConfigResponse struct { + AssetCode string `json:"assetCode"` + Minimum string `json:"min"` + CanWithDraw bool `json:"canWithDraw"` + Fee string `json:"fee"` +} + +// WithdrawResponse stores info about the withdrawal +type WithdrawResponse struct { + ErrCapture `json:",omitempty"` + WithdrawID string `json:"withdrawId"` + Fee float64 `json:"fee"` +} + +// RevokeWithdrawResponse stores info about the revoked withdrawal +type RevokeWithdrawResponse struct { + ErrCapture `json:",omitempty"` + WithdrawID string `json:"string"` +} + +// ListDataResponse contains some of withdrawal data +type ListDataResponse struct { + ErrCapture `json:",omitempty"` + Amount float64 `json:"amount"` + AssetCode string `json:"assetCode"` + Address string `json:"address"` + Fee float64 `json:"fee"` + ID int64 `json:"id"` + Time int64 `json:"time"` + TXHash string `json:"txhash"` + Status string `json:"status"` +} + +// WithdrawalResponse stores data for withdrawals +type WithdrawalResponse struct { + ErrCapture `json:",omitempty"` + TotalPages int64 `json:"totalPages"` + PageSize int64 `json:"pageSize"` + PageNo int64 `json:"pageNo"` + List []ListDataResponse `json:"list"` +} + +// ErrCapture helps with error info +type ErrCapture struct { + Error int64 `json:"error_code"` + Result bool `json:"result,string"` +} + +// GetAllOpenIDResp stores orderIds and currency pairs for open orders +type GetAllOpenIDResp struct { + CurrencyPair string + OrderID string +} + +var errorCodes = map[int64]string{ + 10000: "Internal error", + 10001: "The required parameters can not be empty", + 10002: "Validation Failed", + 10003: "Invalid parameter", + 10004: "Request too frequent", + 10005: "Secret key does not exist", + 10006: "User does not exist", + 10007: "Invalid signature", + 10008: "Invalid Trading Pair", + 10009: "Price and/or Amount are required for limit order", + 10010: "Price and/or Amount must be more than 0", + 10013: "The amount is too small", + 10014: "Insufficient amount of money in account", + 10015: "Invalid order type", + 10016: "Insufficient account balance", + 10017: "Server Error", + 10018: "Page size should be between 1 and 50", + 10019: "Cancel NO more than 3 orders in one request", + 10020: "Volume < 0.001", + 10021: "Price < 0.01", + 10022: "Access denied", + 10023: "Market Order is not supported yet.", + 10024: "User cannot trade on this pair", + 10025: "Order has been filled", + 10026: "Order has been cancelld", + 10027: "Order is cancelling", + 10028: "Wrong query time", + 10029: "'from' is not in the query time", + 10030: "'from' does not match the transaction type of inqury", + 10100: "Has no privilege to withdraw", + 10101: "Invalid fee rate to withdraw", + 10102: "Too little to withdraw", + 10103: "Exceed daily limitation of withdraw", + 10104: "Cancel was rejected", + 10105: "Request has been cancelled", +} diff --git a/exchanges/lbank/lbank_wrapper.go b/exchanges/lbank/lbank_wrapper.go new file mode 100644 index 00000000..986ff542 --- /dev/null +++ b/exchanges/lbank/lbank_wrapper.go @@ -0,0 +1,545 @@ +package lbank + +import ( + "fmt" + "strconv" + "strings" + "sync" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/currency" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" + "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" + "github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler" + log "github.com/thrasher-corp/gocryptotrader/logger" +) + +// Start starts the Lbank go routine +func (l *Lbank) Start(wg *sync.WaitGroup) { + wg.Add(1) + go func() { + l.Run() + wg.Done() + }() +} + +// Run implements the Lbank wrapper +func (l *Lbank) Run() { + if l.Verbose { + log.Debugf("%s Websocket: %s. (url: %s).\n", l.GetName(), common.IsEnabled(l.Websocket.IsEnabled()), l.Websocket.GetWebsocketURL()) + log.Debugf("%s polling delay: %ds.\n", l.GetName(), l.RESTPollingDelay) + log.Debugf("%s %d currencies enabled: %s.\n", l.GetName(), len(l.EnabledPairs), l.EnabledPairs) + } + exchangeCurrencies, err := l.GetCurrencyPairs() + if err != nil { + log.Errorf("%s Failed to get available symbols.\n", l.GetName()) + } else { + var newExchangeCurrencies currency.Pairs + for _, p := range exchangeCurrencies { + newExchangeCurrencies = append(newExchangeCurrencies, + currency.NewPairFromString(p)) + } + err = l.UpdateCurrencies(newExchangeCurrencies, false, true) + if err != nil { + log.Errorf("%s Failed to update available currencies %s.\n", l.GetName(), err) + } + } +} + +// UpdateTicker updates and returns the ticker for a currency pair +func (l *Lbank) UpdateTicker(p currency.Pair, assetType string) (ticker.Price, error) { + var tickerPrice ticker.Price + tickerInfo, err := l.GetTicker(exchange.FormatExchangeCurrency(l.Name, p).String()) + if err != nil { + return tickerPrice, err + } + tickerPrice.Pair = p + tickerPrice.Last = tickerInfo.Ticker.Latest + tickerPrice.High = tickerInfo.Ticker.High + tickerPrice.Volume = tickerInfo.Ticker.Volume + tickerPrice.Low = tickerInfo.Ticker.Low + + err = ticker.ProcessTicker(l.GetName(), &tickerPrice, assetType) + if err != nil { + return tickerPrice, err + } + + return ticker.GetTicker(l.Name, p, assetType) +} + +// GetTickerPrice returns the ticker for a currency pair +func (l *Lbank) GetTickerPrice(p currency.Pair, assetType string) (ticker.Price, error) { + tickerNew, err := ticker.GetTicker(l.GetName(), exchange.FormatExchangeCurrency(l.Name, p), assetType) + if err != nil { + return l.UpdateTicker(p, assetType) + } + return tickerNew, nil +} + +// GetOrderbookEx returns orderbook base on the currency pair +func (l *Lbank) GetOrderbookEx(currency currency.Pair, assetType string) (orderbook.Base, error) { + ob, err := orderbook.Get(l.GetName(), currency, assetType) + if err != nil { + return l.UpdateOrderbook(currency, assetType) + } + return ob, nil +} + +// UpdateOrderbook updates and returns the orderbook for a currency pair +func (l *Lbank) UpdateOrderbook(p currency.Pair, assetType string) (orderbook.Base, error) { + var orderBook orderbook.Base + a, err := l.GetMarketDepths(exchange.FormatExchangeCurrency(l.Name, p).String(), "60", "1") + if err != nil { + return orderBook, err + } + for i := range a.Asks { + orderBook.Asks = append(orderBook.Asks, orderbook.Item{ + Price: a.Asks[i][0], + Amount: a.Asks[i][1]}) + } + for i := range a.Bids { + orderBook.Bids = append(orderBook.Bids, orderbook.Item{ + Price: a.Bids[i][0], + Amount: a.Bids[i][1]}) + } + orderBook.Pair = p + orderBook.ExchangeName = l.GetName() + orderBook.AssetType = assetType + err = orderBook.Process() + if err != nil { + return orderBook, err + } + + return orderbook.Get(l.Name, p, assetType) +} + +// GetAccountInfo retrieves balances for all enabled currencies for the +// Lbank exchange +func (l *Lbank) GetAccountInfo() (exchange.AccountInfo, error) { + var info exchange.AccountInfo + data, err := l.GetUserInfo() + if err != nil { + return info, err + } + var account exchange.Account + for key, val := range data.Info.Asset { + c := currency.NewCode(key) + hold, ok := data.Info.Freeze[key] + if !ok { + return info, fmt.Errorf("hold data not found with %s", key) + } + totalVal, err := strconv.ParseFloat(val, 64) + if err != nil { + return info, err + } + totalHold, err := strconv.ParseFloat(hold, 64) + if err != nil { + return info, err + } + account.Currencies = append(account.Currencies, + exchange.AccountCurrencyInfo{CurrencyName: c, + TotalValue: totalVal, + Hold: totalHold}) + } + + info.Accounts = append(info.Accounts, account) + info.Exchange = l.GetName() + return info, nil +} + +// GetFundingHistory returns funding history, deposits and +// withdrawals +func (l *Lbank) GetFundingHistory() ([]exchange.FundHistory, error) { + return nil, common.ErrFunctionNotSupported +} + +// GetExchangeHistory returns historic trade data since exchange opening. +func (l *Lbank) GetExchangeHistory(p currency.Pair, assetType string) ([]exchange.TradeHistory, error) { + return nil, common.ErrFunctionNotSupported +} + +// SubmitOrder submits a new order +func (l *Lbank) SubmitOrder(p currency.Pair, side exchange.OrderSide, _ exchange.OrderType, amount, price float64, clientID string) (exchange.SubmitOrderResponse, error) { + var resp exchange.SubmitOrderResponse + if side != exchange.BuyOrderSide && side != exchange.SellOrderSide { + return resp, fmt.Errorf("%s orderside is not supported by the exchange", side) + } + tempResp, err := l.CreateOrder(exchange.FormatExchangeCurrency(l.Name, p).String(), side.ToString(), amount, price) + if err != nil { + return resp, err + } + resp.IsOrderPlaced = true + resp.OrderID = tempResp.OrderID + return resp, nil +} + +// ModifyOrder will allow of changing orderbook placement and limit to +// market conversion +func (l *Lbank) ModifyOrder(action *exchange.ModifyOrder) (string, error) { + return "", common.ErrFunctionNotSupported +} + +// CancelOrder cancels an order by its corresponding ID number +func (l *Lbank) CancelOrder(order *exchange.OrderCancellation) error { + _, err := l.RemoveOrder(exchange.FormatExchangeCurrency(l.Name, order.CurrencyPair).String(), order.OrderID) + return err +} + +// CancelAllOrders cancels all orders associated with a currency pair +func (l *Lbank) CancelAllOrders(orders *exchange.OrderCancellation) (exchange.CancelAllOrdersResponse, error) { + var resp exchange.CancelAllOrdersResponse + orderIDs, err := l.getAllOpenOrderID() + if err != nil { + return resp, nil + } + + for key := range orderIDs { + if key != orders.CurrencyPair.String() { + continue + } + var x, y = 0, 0 + var input string + var tempSlice []string + for x <= len(orderIDs[key]) { + x++ + for y != x { + tempSlice = append(tempSlice, orderIDs[key][y]) + if y%3 == 0 { + input = strings.Join(tempSlice, ",") + CancelResponse, err2 := l.RemoveOrder(key, input) + if err2 != nil { + return resp, err2 + } + tempStringSuccess := strings.Split(CancelResponse.Success, ",") + for k := range tempStringSuccess { + resp.OrderStatus[tempStringSuccess[k]] = "Cancelled" + } + tempStringError := strings.Split(CancelResponse.Err, ",") + for l := range tempStringError { + resp.OrderStatus[tempStringError[l]] = "Failed" + } + tempSlice = tempSlice[:0] + y++ + } + y++ + } + input = strings.Join(tempSlice, ",") + CancelResponse, err2 := l.RemoveOrder(key, input) + if err2 != nil { + return resp, err2 + } + tempStringSuccess := strings.Split(CancelResponse.Success, ",") + for k := range tempStringSuccess { + resp.OrderStatus[tempStringSuccess[k]] = "Cancelled" + } + tempStringError := strings.Split(CancelResponse.Err, ",") + for l := range tempStringError { + resp.OrderStatus[tempStringError[l]] = "Failed" + } + tempSlice = tempSlice[:0] + } + } + return resp, nil +} + +// GetOrderInfo returns information on a current open order +func (l *Lbank) GetOrderInfo(orderID string) (exchange.OrderDetail, error) { + var resp exchange.OrderDetail + orderIDs, err := l.getAllOpenOrderID() + if err != nil { + return resp, err + } + + for key, val := range orderIDs { + for i := range val { + if val[i] != orderID { + continue + } + tempResp, err := l.QueryOrder(key, orderID) + if err != nil { + return resp, err + } + resp.Exchange = l.GetName() + resp.CurrencyPair = currency.NewPairFromString(key) + if strings.EqualFold(tempResp.Orders[0].Type, "buy") { + resp.OrderSide = exchange.BuyOrderSide + } else { + resp.OrderSide = exchange.SellOrderSide + } + z := tempResp.Orders[0].Status + switch { + case z == -1: + resp.Status = "cancelled" + case z == 0: + resp.Status = "on trading" + case z == 1: + resp.Status = "filled partially" + case z == 2: + resp.Status = "Filled totally" + case z == 4: + resp.Status = "Cancelling" + default: + resp.Status = "Invalid Order Status" + } + resp.Price = tempResp.Orders[0].Price + resp.Amount = tempResp.Orders[0].Amount + resp.ExecutedAmount = tempResp.Orders[0].DealAmount + resp.RemainingAmount = tempResp.Orders[0].Amount - tempResp.Orders[0].DealAmount + resp.Fee, err = l.GetFeeByType(&exchange.FeeBuilder{ + FeeType: exchange.CryptocurrencyTradeFee, + Amount: tempResp.Orders[0].Amount, + PurchasePrice: tempResp.Orders[0].Price}) + if err != nil { + resp.Fee = lbankFeeNotFound + } + } + } + return resp, nil +} + +// GetDepositAddress returns a deposit address for a specified currency +func (l *Lbank) GetDepositAddress(cryptocurrency currency.Code, accountID string) (string, error) { + return "", common.ErrFunctionNotSupported +} + +// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is +// submitted +func (l *Lbank) WithdrawCryptocurrencyFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { + resp, err := l.Withdraw(withdrawRequest.Address, withdrawRequest.Currency.String(), strconv.FormatFloat(withdrawRequest.Amount, 'f', -1, 64), "", withdrawRequest.Description) + return resp.WithdrawID, err +} + +// WithdrawFiatFunds returns a withdrawal ID when a withdrawal is +// submitted +func (l *Lbank) WithdrawFiatFunds(withdrawRequest *exchange.WithdrawRequest) (string, error) { + return "", common.ErrFunctionNotSupported +} + +// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is +// submitted +func (l *Lbank) WithdrawFiatFundsToInternationalBank(withdrawRequest *exchange.WithdrawRequest) (string, error) { + return "", common.ErrFunctionNotSupported +} + +// GetWebsocket returns a pointer to the exchange websocket +func (l *Lbank) GetWebsocket() (*wshandler.Websocket, error) { + return nil, common.ErrNotYetImplemented +} + +// GetActiveOrders retrieves any orders that are active/open +func (l *Lbank) GetActiveOrders(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { + var finalResp []exchange.OrderDetail + var resp exchange.OrderDetail + tempData, err := l.getAllOpenOrderID() + if err != nil { + return finalResp, err + } + + for key, val := range tempData { + for x := range val { + tempResp, err := l.QueryOrder(key, val[x]) + if err != nil { + return finalResp, err + } + resp.Exchange = l.GetName() + resp.CurrencyPair = currency.NewPairFromString(key) + if strings.EqualFold(tempResp.Orders[0].Type, "buy") { + resp.OrderSide = exchange.BuyOrderSide + } else { + resp.OrderSide = exchange.SellOrderSide + } + z := tempResp.Orders[0].Status + switch { + case z == -1: + resp.Status = "cancelled" + case z == 1: + resp.Status = "on trading" + case z == 2: + resp.Status = "filled partially" + case z == 3: + resp.Status = "Filled totally" + case z == 4: + resp.Status = "Cancelling" + default: + resp.Status = "Invalid Order Status" + } + resp.Price = tempResp.Orders[0].Price + resp.Amount = tempResp.Orders[0].Amount + resp.OrderDate = time.Unix(tempResp.Orders[0].CreateTime, 9) + resp.ExecutedAmount = tempResp.Orders[0].DealAmount + resp.RemainingAmount = tempResp.Orders[0].Amount - tempResp.Orders[0].DealAmount + resp.Fee, err = l.GetFeeByType(&exchange.FeeBuilder{ + FeeType: exchange.CryptocurrencyTradeFee, + Amount: tempResp.Orders[0].Amount, + PurchasePrice: tempResp.Orders[0].Price}) + if err != nil { + resp.Fee = lbankFeeNotFound + } + for y := int(0); y < len(getOrdersRequest.Currencies); y++ { + if getOrdersRequest.Currencies[y].String() != key { + continue + } + if getOrdersRequest.OrderSide == "ANY" { + finalResp = append(finalResp, resp) + continue + } + if strings.EqualFold(getOrdersRequest.OrderSide.ToString(), tempResp.Orders[0].Type) { + finalResp = append(finalResp, resp) + } + } + } + } + return finalResp, nil +} + +// GetOrderHistory retrieves account order information * +// Can Limit response to specific order status +func (l *Lbank) GetOrderHistory(getOrdersRequest *exchange.GetOrdersRequest) ([]exchange.OrderDetail, error) { + var finalResp []exchange.OrderDetail + var resp exchange.OrderDetail + var tempCurr currency.Pairs + var x int + if len(getOrdersRequest.Currencies) == 0 { + tempCurr = l.GetEnabledCurrencies() + } else { + for x < len(getOrdersRequest.Currencies) { + tempCurr = getOrdersRequest.Currencies + } + } + for a := range tempCurr { + p := exchange.FormatExchangeCurrency(l.Name, tempCurr[a]) + b := int64(1) + tempResp, err := l.QueryOrderHistory(exchange.FormatExchangeCurrency(l.Name, p).String(), strconv.FormatInt(b, 10), "200") + if err != nil { + return finalResp, err + } + for len(tempResp.Orders) != 0 { + tempResp, err = l.QueryOrderHistory(exchange.FormatExchangeCurrency(l.Name, p).String(), strconv.FormatInt(b, 10), "200") + if err != nil { + return finalResp, err + } + for x := 0; x < len(tempResp.Orders); x++ { + resp.Exchange = l.GetName() + resp.CurrencyPair = currency.NewPairFromString(tempResp.Orders[x].Symbol) + if strings.EqualFold(tempResp.Orders[x].Type, "buy") { + resp.OrderSide = exchange.BuyOrderSide + } else { + resp.OrderSide = exchange.SellOrderSide + } + z := tempResp.Orders[x].Status + switch { + case z == -1: + resp.Status = "cancelled" + case z == 1: + resp.Status = "on trading" + case z == 2: + resp.Status = "filled partially" + case z == 3: + resp.Status = "Filled totally" + case z == 4: + resp.Status = "Cancelling" + default: + resp.Status = "Invalid Order Status" + } + resp.Price = tempResp.Orders[x].Price + resp.Amount = tempResp.Orders[x].Amount + resp.OrderDate = time.Unix(tempResp.Orders[x].CreateTime, 9) + resp.ExecutedAmount = tempResp.Orders[x].DealAmount + resp.RemainingAmount = tempResp.Orders[x].Price - tempResp.Orders[x].DealAmount + resp.Fee, err = l.GetFeeByType(&exchange.FeeBuilder{ + FeeType: exchange.CryptocurrencyTradeFee, + Amount: tempResp.Orders[x].Amount, + PurchasePrice: tempResp.Orders[x].Price}) + if err != nil { + resp.Fee = lbankFeeNotFound + } + finalResp = append(finalResp, resp) + b++ + } + } + } + return finalResp, nil +} + +// GetFeeByType returns an estimate of fee based on the type of transaction * +func (l *Lbank) GetFeeByType(feeBuilder *exchange.FeeBuilder) (float64, error) { + var resp float64 + if feeBuilder.FeeType == exchange.CryptocurrencyTradeFee { + return feeBuilder.Amount * feeBuilder.PurchasePrice * l.Fee, nil + } + if feeBuilder.FeeType == exchange.CryptocurrencyWithdrawalFee { + withdrawalFee, err := l.GetWithdrawConfig(feeBuilder.Pair.Base.Lower().String()) + if err != nil { + return resp, err + } + var tempFee string + temp := strings.Split(withdrawalFee[0].Fee, ":\"") + if len(temp) > 1 { + tempFee = strings.TrimRight(temp[1], ",\"type") + } else { + tempFee = temp[0] + } + resp, err = strconv.ParseFloat(tempFee, 64) + if err != nil { + return resp, err + } + } + return resp, nil +} + +// GetAllOpenOrderID returns all open orders by currency pairs +func (l *Lbank) getAllOpenOrderID() (map[string][]string, error) { + allPairs := l.GetEnabledCurrencies() + resp := make(map[string][]string) + for a := range allPairs { + p := exchange.FormatExchangeCurrency(l.Name, allPairs[a]) + b := int64(1) + tempResp, err := l.GetOpenOrders(exchange.FormatExchangeCurrency(l.Name, p).String(), strconv.FormatInt(b, 10), "200") + if err != nil { + return resp, err + } + tempData := len(tempResp.Orders) + for tempData != 0 { + tempResp, err = l.GetOpenOrders(exchange.FormatExchangeCurrency(l.Name, p).String(), strconv.FormatInt(b, 10), "200") + if err != nil { + return resp, err + } + + if len(tempResp.Orders) == 0 { + return resp, nil + } + + for c := 0; c < tempData; c++ { + resp[exchange.FormatExchangeCurrency(l.Name, p).String()] = append(resp[exchange.FormatExchangeCurrency(l.Name, p).String()], tempResp.Orders[c].OrderID) + + } + tempData = len(tempResp.Orders) + b++ + } + } + return resp, nil +} + +// SubscribeToWebsocketChannels appends to ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle subscribing +func (l *Lbank) SubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + return common.ErrNotYetImplemented +} + +// UnsubscribeToWebsocketChannels removes from ChannelsToSubscribe +// which lets websocket.manageSubscriptions handle unsubscribing +func (l *Lbank) UnsubscribeToWebsocketChannels(channels []wshandler.WebsocketChannelSubscription) error { + return common.ErrNotYetImplemented +} + +// AuthenticateWebsocket authenticates it +func (l *Lbank) AuthenticateWebsocket() error { + return common.ErrNotYetImplemented +} + +// GetSubscriptions gets subscriptions +func (l *Lbank) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) { + return nil, common.ErrNotYetImplemented +} diff --git a/testdata/README.md b/testdata/README.md index 64ded58c..f178096f 100644 --- a/testdata/README.md +++ b/testdata/README.md @@ -42,3 +42,4 @@ When submitting a PR, please abide by our coding guidelines: If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: ***1F5zVDgNjorJ51oGebSvNCrSAHpwGkUdDB*** + diff --git a/testdata/configtest.json b/testdata/configtest.json index 62a9f87e..ed5eb119 100644 --- a/testdata/configtest.json +++ b/testdata/configtest.json @@ -443,6 +443,49 @@ } ] }, + { + "name": "LBank", + "enabled": true, + "verbose": false, + "websocket": false, + "useSandbox": false, + "restPollingDelay": 10, + "httpTimeout": 15000000000, + "httpUserAgent": "", + "httpDebugging": false, + "authenticatedApiSupport": false, + "authenticatedWebsocketApiSupport": false, + "apiKey": "Key", + "apiSecret": "Secret", + "apiUrl": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "apiUrlSecondary": "NON_DEFAULT_HTTP_LINK_TO_EXCHANGE_API", + "proxyAddress": "", + "websocketUrl": "NON_DEFAULT_HTTP_LINK_TO_WEBSOCKET_EXCHANGE_API", + "availablePairs": "fbc_usdt,hds_usdt,galt_usdt,dxn_usdt,iog_usdt,ioex_usdt,vollar_usdt,oath_usdt,bloc_usdt,btc_lbcn,eth_lbcn,usdt_lbcn,btc_usdt,eth_usdt,eth_btc,abbc_btc,bzky_eth,onot_eth,kisc_eth,bxa_usdt,atp_usdt,mat_usdt,sky_btc,sky_lbcn,rnt_usdt,vena_usdt,grin_usdt,ida_usdt,pnt_usdt,bsv_btc,bsv_usdt,opx_usdt,tena_eth,seer_lbcn,vet_lbcn,vtho_btc,vnx_lbcn,vnx_btc,amo_eth,ubex_btc,eos_btc,ubex_usdt,tns_lbcn,tns_btc,ali_eth,sdc_eth,sait_eth,artcn_usdt,dax_btc,dax_eth,dali_usdt,vet_usdt,ten_usdt,bch_usdt,neo_usdt,qtum_usdt,zec_usdt,vet_btc,pai_btc,pnt_btc,bch_btc,ltc_btc,neo_btc,dash_btc,etc_btc,qtum_btc,zec_btc,sc_btc,bts_btc,cpx_btc,xwc_btc,fil6_btc,fil12_btc,fil36_btc,eos_usdt,ut_eth,ela_eth,vet_eth,vtho_eth,pai_eth,bfdt_eth,her_eth,ptt_eth,tac_eth,idhub_eth,ssc_eth,skm_eth,iic_eth,ply_eth,ext_eth,eos_eth,yoyow_eth,trx_eth,qtum_eth,zec_eth,bts_eth,btm_eth,mith_eth,nas_eth,man_eth,dbc_eth,bto_eth,ddd_eth,cpx_eth,cs_eth,iht_eth,tky_eth,ocn_eth,dct_eth,zpt_eth,eko_eth,mda_eth,pst_eth,xwc_eth,put_eth,pnt_eth,aac_eth,fil6_eth,fil12_eth,fil36_eth,uip_eth,seer_eth,bsb_eth,cdc_eth,grams_eth,ddmx_eth,eai_eth,inc_eth,bnb_usdt,ht_usdt,bot_eth,kbc_btc,kbc_usdt,mai_usdt,phv_usdt,hnb_usdt,gt_usdt,b91_usdt,voken_usdt,cye_usdt,brc_usdt,btc_ausd", + "enabledPairs": "eth_btc", + "baseCurrencies": "USD", + "assetTypes": "SPOT", + "supportsAutoPairUpdates": true, + "configCurrencyPairFormat": { + "uppercase": false, + "delimiter": "_" + }, + "requestCurrencyPairFormat": { + "uppercase": false, + "delimiter": "_" + }, + "bankAccounts": [ + { + "bankName": "", + "bankAddress": "", + "accountName": "", + "accountNumber": "", + "swiftCode": "", + "iban": "", + "supportedCurrencies": "" + } + ] + }, { "name": "Bittrex", "enabled": true, diff --git a/tools/documentation/documentation.go b/tools/documentation/documentation.go index ad9ee552..458e8226 100644 --- a/tools/documentation/documentation.go +++ b/tools/documentation/documentation.go @@ -28,9 +28,6 @@ const ( currencyFXCurrencylayerPath = "..%s..%scurrency%sforexprovider%scurrencylayer%s" currencyFXFixerPath = "..%s..%scurrency%sforexprovider%sfixer.io%s" currencyFXOpenExchangeRatesPath = "..%s..%scurrency%sforexprovider%sopenexchangerates%s" - currencyPairPath = "..%s..%scurrency%spair%s" - currencySymbolPath = "..%s..%scurrency%ssymbol%s" - currencyTranslationPath = "..%s..%scurrency%stranslation%s" eventsPath = "..%s..%sevents%s" exchangesPath = "..%s..%sexchanges%s" exchangesNoncePath = "..%s..%sexchanges%snonce%s" @@ -67,6 +64,7 @@ const ( itbit = "..%s..%sexchanges%sitbit%s" kraken = "..%s..%sexchanges%skraken%s" lakebtc = "..%s..%sexchanges%slakebtc%s" + lbank = "..%s..%sexchanges%slbank%s" localbitcoins = "..%s..%sexchanges%slocalbitcoins%s" okcoin = "..%s..%sexchanges%sokcoin%s" okex = "..%s..%sexchanges%sokex%s" @@ -198,9 +196,6 @@ func addPaths() { codebasePaths["currency forexprovider currencylayer"] = fmt.Sprintf(currencyFXCurrencylayerPath, path, path, path, path, path) codebasePaths["currency forexprovider fixer"] = fmt.Sprintf(currencyFXFixerPath, path, path, path, path, path) codebasePaths["currency forexprovider openexchangerates"] = fmt.Sprintf(currencyFXOpenExchangeRatesPath, path, path, path, path, path) - codebasePaths["currency pair"] = fmt.Sprintf(currencyPairPath, path, path, path, path) - codebasePaths["currency symbol"] = fmt.Sprintf(currencySymbolPath, path, path, path, path) - codebasePaths["currency translation"] = fmt.Sprintf(currencyTranslationPath, path, path, path, path) codebasePaths["events"] = fmt.Sprintf(eventsPath, path, path, path) @@ -239,6 +234,7 @@ func addPaths() { codebasePaths["exchanges itbit"] = fmt.Sprintf(itbit, path, path, path, path) codebasePaths["exchanges kraken"] = fmt.Sprintf(kraken, path, path, path, path) codebasePaths["exchanges lakebtc"] = fmt.Sprintf(lakebtc, path, path, path, path) + codebasePaths["exchanges lbank"] = fmt.Sprintf(lbank, path, path, path, path) codebasePaths["exchanges localbitcoins"] = fmt.Sprintf(localbitcoins, path, path, path, path) codebasePaths["exchanges okcoin"] = fmt.Sprintf(okcoin, path, path, path, path) codebasePaths["exchanges okex"] = fmt.Sprintf(okex, path, path, path, path) diff --git a/tools/documentation/exchanges_templates/lbank.tmpl b/tools/documentation/exchanges_templates/lbank.tmpl new file mode 100644 index 00000000..45fb9441 --- /dev/null +++ b/tools/documentation/exchanges_templates/lbank.tmpl @@ -0,0 +1,98 @@ +{{define "exchanges lbank" -}} +{{template "header" .}} +## Lbank Exchange + +### Current Features + ++ REST Support + +### How to enable + ++ [Enable via configuration](https://githul.com/thrasher-corp/gocryptotrader/tree/master/config#enable-exchange-via-config-example) + ++ Individual package example below: + +```go + // Exchanges will be abstracted out in further updates and examples will be + // supplied then +``` + +### How to do REST public/private calls + ++ If enabled via "configuration".json file the exchange will be added to the +IBotExchange array in the ```go var bot Bot``` and you will only be able to use +the wrapper interface functions for accessing exchange data. View routines.go +for an example of integration usage with GoCryptoTrader. Rudimentary example +below: + +main.go +```go +var l exchange.IBotExchange + +for i := range bot.exchanges { + if bot.exchanges[i].GetName() == "Lbank" { + l = bot.exchanges[i] + } +} + +// Public calls - wrapper functions + +// Fetches current ticker information +tick, err := l.GetTickerPrice() +if err != nil { + // Handle error +} + +// Fetches current orderbook information +ob, err := l.GetOrderbookEx() +if err != nil { + // Handle error +} + +// Private calls - wrapper functions - make sure your APIKEY and APISECRET are +// set and AuthenticatedAPISupport is set to true + +// Fetches current account information +accountInfo, err := l.GetAccountInfo() +if err != nil { + // Handle error +} +``` + ++ If enabled via individually importing package, rudimentary example below: + +```go +// Public calls + +// Fetches current ticker information +ticker, err := l.GetTicker() +if err != nil { + // Handle error +} + +// Fetches current orderbook information +ob, err := l.GetOrderBook() +if err != nil { + // Handle error +} + +// Private calls - make sure your APIKEY and APISECRET are set and +// AuthenticatedAPISupport is set to true + +// GetUserInfo returns account info +accountInfo, err := l.GetUserInfo(...) +if err != nil { + // Handle error +} + +// Submits an order and the exchange and returns its tradeID +tradeID, err := l.Trade(...) +if err != nil { + // Handle error +} +``` + +### Please click GoDocs chevron above to view current GoDoc information for this package +{{template "contributions"}} +{{template "donations"}} +{{end}} diff --git a/tools/documentation/root_templates/root_readme.tmpl b/tools/documentation/root_templates/root_readme.tmpl index fe86c619..6c2d9edb 100644 --- a/tools/documentation/root_templates/root_readme.tmpl +++ b/tools/documentation/root_templates/root_readme.tmpl @@ -40,6 +40,7 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader | Huobi.Hadax | Yes | Yes | NA | | ItBit | Yes | NA | No | | Kraken | Yes | Yes | NA | +| Lbank | Yes | No | NA | | LakeBTC | Yes | No | NA | | LocalBitcoins | Yes | NA | NA | | OKCoin International | Yes | Yes | No |