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
+
+
+
+
+[](https://travis-ci.org/thrasher-corp/gocryptotrader)
+[](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
+[](https://godoc.org/github.com/thrasher-corp/gocryptotrader/exchanges/lbank)
+[](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
+[](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 |