Miscellaneous bug fixes (#513)

* Various bug fixes

* Deadlink, cleanup plus bug fixes

* Various Kraken fixes

* Add convert func for decimal unix timestamps

* Convert all test times to UTC

* Kraken: Make assets a pointer to prevent excessive copying

* Docker slash fix

* Address nits plus bump ITBit last checked pairs timestamp

* Set pairs to enabled pairs when getting active orders

* Use asset translator for UpdateAccountInfo and more checks for the exchange template tool

* Address MadCozBadd's nits

* Make exchange var 2 chars

* Make program more user friendly

* Project wide comment checks and exchName check

* Fix Huobi indexing bug and use correct pair formatting

* Address nits + readme change
This commit is contained in:
Adrian Gallagher
2020-06-03 12:48:36 +10:00
committed by GitHub
parent c437b408ca
commit 8afee0b4b2
41 changed files with 879 additions and 678 deletions

View File

@@ -226,6 +226,11 @@ func main() {
URL: "https://github.com/lookfirst",
Contributions: 1,
},
{
Login: "merkeld",
URL: "https://github.com/merkeld",
Contributions: 1,
},
}...)
if verbose {

View File

@@ -48,9 +48,10 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader
| Yobit | Yes | NA | NA |
| ZB.COM | Yes | Yes | NA |
We are aiming to support the top 20 highest volume exchanges based off the [CoinMarketCap exchange data](https://coinmarketcap.com/exchanges/volume/24-hour/).
We are aiming to support the top 30 exchanges sorted by average liquidity as [ranked by CoinMarketCap](https://coinmarketcap.com/rankings/exchanges/).
However, we welcome pull requests for any exchange which does not match this criterion. If you need help with this, please join us on [Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk).
** NA means not applicable as the Exchange does not support the feature.
** NA means not applicable as the exchange does not support the feature.
## Current Features

View File

@@ -1,6 +1,7 @@
package main
import (
"errors"
"flag"
"fmt"
"html/template"
@@ -12,28 +13,14 @@ import (
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/core"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
const (
packageTests = "%s_test.go"
packageTypes = "%s_types.go"
packageWrapper = "%s_wrapper.go"
packageMain = "%s.go"
packageReadme = "README.md"
exchangePackageLocation = "../../exchanges"
exchangeConfigPath = "../../testdata/configtest.json"
)
var (
exchangeDirectory string
exchangeTest string
exchangeTypes string
exchangeWrapper string
exchangeMain string
exchangeReadme string
exchangeConfigPath = "../../testdata/configtest.json"
targetPath = "../../exchanges"
)
type exchange struct {
@@ -45,25 +32,40 @@ type exchange struct {
FIX bool
}
var (
errInvalidExchangeName = errors.New("invalid exchange name")
)
func main() {
var newExchangeName string
var websocketSupport, restSupport, fixSupport bool
flag.StringVar(&newExchangeName, "name", "", "-name [string] adds a new exchange")
flag.BoolVar(&websocketSupport, "ws", false, "-websocket adds websocket support")
flag.BoolVar(&restSupport, "rest", false, "-rest adds REST support")
flag.BoolVar(&fixSupport, "fix", false, "-fix adds FIX support?")
flag.StringVar(&newExchangeName, "name", "", "the exchange name")
flag.BoolVar(&websocketSupport, "ws", false, "whether the exchange supports websocket")
flag.BoolVar(&restSupport, "rest", false, "whether the exchange supports REST")
flag.BoolVar(&fixSupport, "fix", false, "whether the exchange supports FIX")
flag.Parse()
fmt.Println("GoCryptoTrader: Exchange templating tool.")
fmt.Println(core.Copyright)
fmt.Println()
if newExchangeName == "" || newExchangeName == " " {
log.Fatal(`GoCryptoTrader: Exchange templating tool exchange name not set e.g. "exchange_template -name [newExchangeNameString]"`)
if len(os.Args) == 1 {
log.Println("Invalid arguments supplied, please see application usage below:")
flag.Usage()
return
}
if err := checkExchangeName(newExchangeName); err != nil {
log.Fatal(err)
}
newExchangeName = strings.ToLower(newExchangeName)
if !websocketSupport && !restSupport && !fixSupport {
log.Fatal(`GoCryptoTrader: Exchange templating tool support not set e.g. "exchange_template -name [newExchangeNameString] [-fix -ws -rest]"`)
log.Println("At least one protocol must be specified (rest/ws or fix)")
flag.Usage()
return
}
fmt.Println("Exchange Name: ", newExchangeName)
@@ -83,37 +85,64 @@ func main() {
log.Fatal("GoCryptoTrader: Exchange templating tool stopped...")
}
newExchangeName = strings.ToLower(newExchangeName)
v := newExchangeName[:1]
capName := strings.ToUpper(v) + newExchangeName[1:]
exch := exchange{
Name: newExchangeName,
CapitalName: capName,
Variable: v,
REST: restSupport,
WS: websocketSupport,
FIX: fixSupport,
Name: newExchangeName,
REST: restSupport,
WS: websocketSupport,
FIX: fixSupport,
}
if err = makeExchange(&exch); err != nil {
log.Fatal(err)
}
fmt.Println("GoCryptoTrader: Exchange templating tool service complete")
fmt.Println("When the exchange code implementation has been completed (REST/Websocket/wrappers and tests), please add the exchange to engine/exchange.go")
fmt.Println("Add the exchange config settings to config_example.json (it will automatically be added to testdata/configtest.json)")
fmt.Println("Increment the available exchanges counter in config/config_test.go")
fmt.Println("Add the exchange name to exchanges/support.go")
fmt.Println("Ensure go test ./... -race passes")
fmt.Println("Open a pull request")
fmt.Println("If help is needed, please post a message in Slack.")
}
func checkExchangeName(exchName string) error {
if strings.Contains(exchName, " ") ||
len(exchName) <= 2 {
return errInvalidExchangeName
}
return nil
}
func makeExchange(exch *exchange) error {
configTestFile := config.GetConfig()
err = configTestFile.LoadConfig(exchangeConfigPath, true)
err := configTestFile.LoadConfig(exchangeConfigPath, true)
if err != nil {
log.Fatal("GoCryptoTrader: Exchange templating configuration retrieval error ", err)
return err
}
// NOTE need to nullify encrypt configuration
var configTestExchanges []string
for x := range configTestFile.Exchanges {
configTestExchanges = append(configTestExchanges, configTestFile.Exchanges[x].Name)
_, err = configTestFile.GetExchangeConfig(exch.Name)
if err == nil {
return errors.New("exchange already exists")
}
if common.StringDataContainsInsensitive(configTestExchanges, capName) {
log.Fatal("GoCryptoTrader: Exchange templating configuration error - exchange already exists")
exchangeDirectory := filepath.Join(targetPath, exch.Name)
_, err = os.Stat(exchangeDirectory)
if !os.IsNotExist(err) {
return errors.New("directory already exists")
}
err = os.MkdirAll(exchangeDirectory, 0770)
if err != nil {
return err
}
fmt.Printf("Output directory: %s\n", exchangeDirectory)
exch.CapitalName = strings.Title(exch.Name)
exch.Variable = exch.Name[0:2]
newExchConfig := config.ExchangeConfig{}
newExchConfig.Name = capName
newExchConfig.Name = exch.CapitalName
newExchConfig.Enabled = true
newExchConfig.API.Credentials.Key = "Key"
newExchConfig.API.Credentials.Secret = "Secret"
@@ -130,112 +159,99 @@ func main() {
},
}
configTestFile.Exchanges = append(configTestFile.Exchanges, newExchConfig)
outputFiles := []struct {
Name string
Filename string
FilePostfix string
TemplateFile string
}{
{
Name: "readme",
Filename: "README.md",
TemplateFile: "readme_file.tmpl",
},
{
Name: "main",
Filename: "main_file.tmpl",
FilePostfix: ".go",
TemplateFile: "main_file.tmpl",
},
{
Name: "test",
Filename: "test_file.tmpl",
FilePostfix: "_test.go",
TemplateFile: "test_file.tmpl",
},
{
Name: "type",
Filename: "type_file.tmpl",
FilePostfix: "_types.go",
TemplateFile: "type_file.tmpl",
},
{
Name: "wrapper",
Filename: "wrapper_file.tmpl",
FilePostfix: "_wrapper.go",
TemplateFile: "wrapper_file.tmpl",
},
}
for x := range outputFiles {
var tmpl *template.Template
tmpl, err = template.New(outputFiles[x].Name).ParseFiles(outputFiles[x].TemplateFile)
if err != nil {
return fmt.Errorf("%s template error: %s", outputFiles[x].Name, err)
}
filename := outputFiles[x].Filename
if outputFiles[x].FilePostfix != "" {
filename = exch.Name + outputFiles[x].FilePostfix
}
outputFile := filepath.Join(exchangeDirectory, filename)
newFile(outputFile)
var f *os.File
f, err = os.OpenFile(outputFile, os.O_WRONLY, 0770)
if err != nil {
return err
}
if err = tmpl.Execute(f, exch); err != nil {
f.Close()
return err
}
f.Close()
}
cmd := exec.Command("go", "fmt")
cmd.Dir = exchangeDirectory
out, err := cmd.Output()
if err != nil {
return fmt.Errorf("unable to go fmt. output: %s err: %s", out, err)
}
configTestFile.Exchanges = append(configTestFile.Exchanges, newExchConfig)
err = configTestFile.SaveConfig(exchangeConfigPath, false)
if err != nil {
log.Fatal("GoCryptoTrader: Exchange templating configuration error - cannot save")
return err
}
exchangeDirectory = filepath.Join(exchangePackageLocation, newExchangeName)
exchangeTest = filepath.Join(exchangeDirectory, fmt.Sprintf(packageTests, newExchangeName))
exchangeTypes = filepath.Join(exchangeDirectory, fmt.Sprintf(packageTypes, newExchangeName))
exchangeWrapper = filepath.Join(exchangeDirectory, fmt.Sprintf(packageWrapper, newExchangeName))
exchangeMain = filepath.Join(exchangeDirectory, fmt.Sprintf(packageMain, newExchangeName))
exchangeReadme = filepath.Join(exchangeDirectory, packageReadme)
err = os.Mkdir(exchangeDirectory, 0700)
cmd = exec.Command("go", "test")
cmd.Dir = exchangeDirectory
out, err = cmd.Output()
if err != nil {
log.Fatal("GoCryptoTrader: Exchange templating tool cannot make directory ", err)
return fmt.Errorf("unable to go test. output: %s err: %s", out, err)
}
tReadme, err := template.New("readme").ParseFiles("readme_file.tmpl")
if err != nil {
log.Fatal("GoCryptoTrader: Exchange templating tool error ", err)
}
newFile(exchangeReadme)
r1, err := os.OpenFile(exchangeReadme, os.O_WRONLY, 0700)
if err != nil {
log.Fatal("GoCryptoTrader: Exchange templating tool cannot open file ", err)
}
tReadme.Execute(r1, exch)
r1.Close()
tMain, err := template.New("main").ParseFiles("main_file.tmpl")
if err != nil {
log.Fatal("GoCryptoTrader: Exchange templating tool error ", err)
}
newFile(exchangeMain)
m1, err := os.OpenFile(exchangeMain, os.O_WRONLY, 0700)
if err != nil {
log.Fatal("GoCryptoTrader: Exchange templating tool cannot open file ", err)
}
tMain.Execute(m1, exch)
m1.Close()
tTest, err := template.New("test").ParseFiles("test_file.tmpl")
if err != nil {
log.Fatal("GoCryptoTrader: Exchange templating tool error ", err)
}
newFile(exchangeTest)
t1, err := os.OpenFile(exchangeTest, os.O_WRONLY, 0700)
if err != nil {
log.Fatal("GoCryptoTrader: Exchange templating tool cannot open file ", err)
}
tTest.Execute(t1, exch)
t1.Close()
tType, err := template.New("type").ParseFiles("type_file.tmpl")
if err != nil {
log.Fatal("GoCryptoTrader: Exchange templating tool error ", err)
}
newFile(exchangeTypes)
ty1, err := os.OpenFile(exchangeTypes, os.O_WRONLY, 0700)
if err != nil {
log.Fatal("GoCryptoTrader: Exchange templating tool cannot open file ", err)
}
tType.Execute(ty1, exch)
ty1.Close()
tWrapper, err := template.New("wrapper").ParseFiles("wrapper_file.tmpl")
if err != nil {
log.Fatal("GoCryptoTrader: Exchange templating tool error ", err)
}
newFile(exchangeWrapper)
w1, err := os.OpenFile(exchangeWrapper, os.O_WRONLY, 0700)
if err != nil {
log.Fatal("GoCryptoTrader: Exchange templating tool cannot open file ", err)
}
tWrapper.Execute(w1, exch)
w1.Close()
err = exec.Command("go", "fmt", exchangeDirectory).Run()
if err != nil {
log.Fatal("GoCryptoTrader: Exchange templating tool go fmt error ", err)
}
err = exec.Command("go", "test", exchangeDirectory).Run()
if err != nil {
log.Fatal("GoCryptoTrader: Exchange templating tool testing failed ", err)
}
fmt.Println("GoCryptoTrader: Exchange templating tool service complete")
fmt.Println("When the exchange code implementation has been completed (REST/Websocket/wrappers and tests), please add the exchange to engine/exchange.go")
fmt.Println("Add the exchange config settings to config_example.json (it will automatically be added to testdata/configtest.json)")
fmt.Println("Increment the available exchanges counter in config/config_test.go")
fmt.Println("Add the exchange name to exchanges/support.go")
fmt.Println("Ensure go test ./... -race passes")
fmt.Println("Open a pull request")
fmt.Println("If help is needed, please post a message in Slack.")
return nil
}
func newFile(path string) {
_, err := os.Stat(path)
if os.IsNotExist(err) {
var file, err = os.Create(path)
if err != nil {
log.Fatal("GoCryptoTrader: Exchange templating tool file creation error ", err)
log.Fatal(err)
}
file.Close()
}

View File

@@ -0,0 +1,74 @@
package main
import (
"os"
"path/filepath"
"testing"
"github.com/thrasher-corp/gocryptotrader/config"
)
func TestCheckExchangeName(t *testing.T) {
tester := []struct {
Name string
ErrExpected error
}{
{
Name: "test exch",
ErrExpected: errInvalidExchangeName,
},
{
ErrExpected: errInvalidExchangeName,
},
{
Name: " ",
ErrExpected: errInvalidExchangeName,
},
{
Name: "m",
ErrExpected: errInvalidExchangeName,
},
{
Name: "mu",
ErrExpected: errInvalidExchangeName,
},
{
Name: "testexch",
},
}
for x := range tester {
if r := checkExchangeName(tester[x].Name); r != tester[x].ErrExpected {
t.Errorf("test: %d unexpected result", x)
}
}
}
func TestNewExchange(t *testing.T) {
testExchangeName := "testexch"
testExchangeDir := filepath.Join(targetPath, testExchangeName)
if err := makeExchange(&exchange{
Name: testExchangeName,
REST: true,
WS: true,
}); err != nil {
t.Error(err)
}
if err := os.RemoveAll(testExchangeDir); err != nil {
t.Errorf("unable to remove dir: %s, manual removal required", err)
}
cfg := config.GetConfig()
if err := cfg.LoadConfig(exchangeConfigPath, true); err != nil {
t.Fatal(err)
}
if success := cfg.RemoveExchange(testExchangeName); !success {
t.Fatalf("unable to remove exchange config for %s, manual removal required\n",
testExchangeName)
}
if err := cfg.SaveConfig(exchangeConfigPath, false); err != nil {
t.Fatal(err)
}
}

View File

@@ -7,7 +7,7 @@ An exchange interface wrapper for the GoCryptoTrader application.
## This is still in active development
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
You can track ideas, planned features and what's in progress on this Trello board: [https://trello.com/b/ZAhMhpOy/gocryptotrader](https://trello.com/b/ZAhMhpOy/gocryptotrader).
## Current {{.CapitalName}} Exchange Features

View File

@@ -7,6 +7,7 @@ import (
"testing"
"github.com/thrasher-corp/gocryptotrader/config"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
)
// Please supply your own keys here to do authenticated endpoint testing
@@ -44,6 +45,14 @@ func TestMain(m *testing.M) {
os.Exit(m.Run())
}
// Ensures that this exchange package is compatible with IBotExchange
func TestInterface(t *testing.T) {
var e exchange.IBotExchange
if e = new({{.CapitalName}}); e == nil {
t.Fatal("unable to allocate exchange")
}
}
func areTestAPIKeysSet() bool {
return {{.Variable}}.ValidateAPICredentials()
}

View File

@@ -9,15 +9,17 @@ import (
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/websocket/wshandler"
"github.com/thrasher-corp/gocryptotrader/withdraw"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
// GetDefaultConfig returns a default exchange config
@@ -83,9 +85,10 @@ func ({{.Variable}} *{{.CapitalName}}) SetDefaults() {
AutoPairUpdates: true,
},
}
// NOTE: SET THE EXCHANGES RATE LIMIT HERE
{{.Variable}}.Requester = request.New({{.Variable}}.Name,
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),
request.WithLimiter(request.NewRateLimit(time.Second, 0)))
common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout))
{{.Variable}}.API.Endpoints.URLDefault = {{.Name}}APIURL
{{.Variable}}.API.Endpoints.URL = {{.Variable}}.API.Endpoints.URLDefault
{{.Variable}}.Websocket = wshandler.New()
@@ -279,10 +282,14 @@ func ({{.Variable}} *{{.CapitalName}}) UpdateOrderbook(p currency.Pair, assetTyp
return orderbook.Get({{.Variable}}.Name, p, assetType)
}
// GetAccountInfo retrieves balances for all enabled currencies for the
// {{.CapitalName}} exchange
func ({{.Variable}} *{{.CapitalName}}) GetAccountInfo() (exchange.AccountInfo, error) {
return exchange.AccountInfo{}, common.ErrNotYetImplemented
// UpdateAccountInfo retrieves balances for all enabled currencies
func ({{.Variable}} *{{.CapitalName}}) UpdateAccountInfo() (account.Holdings, error) {
return account.Holdings{}, common.ErrNotYetImplemented
}
// FetchAccountInfo retrieves balances for all enabled currencies
func ({{.Variable}} *{{.CapitalName}}) FetchAccountInfo() (account.Holdings, error) {
return account.Holdings{}, common.ErrNotYetImplemented
}
// GetFundingHistory returns funding history, deposits and
@@ -334,19 +341,19 @@ func ({{.Variable}} *{{.CapitalName}}) GetDepositAddress(cryptocurrency currency
// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is
// submitted
func ({{.Variable}} *{{.CapitalName}}) WithdrawCryptocurrencyFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return "", common.ErrNotYetImplemented
return nil, common.ErrNotYetImplemented
}
// WithdrawFiatFunds returns a withdrawal ID when a withdrawal is
// submitted
func ({{.Variable}} *{{.CapitalName}}) WithdrawFiatFunds(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return "", common.ErrNotYetImplemented
return nil, common.ErrNotYetImplemented
}
// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is
// submitted
func ({{.Variable}} *{{.CapitalName}}) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (string, error) {
return "", common.ErrNotYetImplemented
func ({{.Variable}} *{{.CapitalName}}) WithdrawFiatFundsToInternationalBank(withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return nil, common.ErrNotYetImplemented
}
// GetWebsocket returns a pointer to the exchange websocket
@@ -385,13 +392,25 @@ func ({{.Variable}} *{{.CapitalName}}) UnsubscribeToWebsocketChannels(channels [
}
// GetSubscriptions returns a copied list of subscriptions
func ({{.Variable}} *{{.CapitalName}}) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
func ({{.Variable}} *{{.CapitalName}}) GetSubscriptions() ([]wshandler.WebsocketChannelSubscription, error) {
return nil, common.ErrNotYetImplemented
}
// AuthenticateWebsocket sends an authentication message to the websocket
func ({{.Variable}} *{{.CapitalName}}) AuthenticateWebsocket() error {
func ({{.Variable}} *{{.CapitalName}}) AuthenticateWebsocket() error {
return common.ErrNotYetImplemented
}
// ValidateCredentials validates current credentials used for wrapper
// functionality
func ({{.Variable}} *{{.CapitalName}}) ValidateCredentials() error {
_, err := {{.Variable}}.UpdateAccountInfo()
return {{.Variable}}.CheckTransientError(err)
}
// GetHistoricCandles returns candles between a time period for a set time interval
func ({{.Variable}} *{{.CapitalName}}) GetHistoricCandles(pair currency.Pair, a asset.Item, start, end time.Time, interval time.Duration) (kline.Item, error) {
return kline.Item{}, common.ErrNotYetImplemented
}
{{end}}