Skip to content

Commit

Permalink
tool/exchange_wrapper_coverage: fix regression and implement reflecti…
Browse files Browse the repository at this point in the history
…on (thrasher-corp#837)

* cmd/tools/exchange: fix regression and implement reflection so as this can dynamically scale to our interface

* exchanges: add comment and fix whoopsie

* exchanges: fix linter issues

* wrapper_cov_tool: add actual full interface method count to get a better perceived deployment

* exchanges/tool: addr glorious nits

* kraken: remove string in test

* exchange_template_tool: fix tmpl issue
  • Loading branch information
shazbert authored Nov 17, 2021
1 parent 7c7aebe commit da34024
Show file tree
Hide file tree
Showing 67 changed files with 878 additions and 239 deletions.
19 changes: 15 additions & 4 deletions cmd/exchange_template/wrapper_file.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
package {{.Name}}

import (
"sync"
"context"
"fmt"
"sync"
"time"

"github.com/thrasher-corp/gocryptotrader/common"
Expand Down Expand Up @@ -129,12 +130,18 @@ func ({{.Variable}} *{{.CapitalName}}) SetDefaults() {

// Setup takes in the supplied exchange configuration details and sets params
func ({{.Variable}} *{{.CapitalName}}) Setup(exch *config.Exchange) error {
err := exch.Validate()
if err != nil {
return err
}
if !exch.Enabled {
{{.Variable}}.SetEnabled(false)
return nil
}

{{.Variable}}.SetupDefaults(exch)
err = {{.Variable}}.SetupDefaults(exch)
if err != nil {
return err
}

/*
wsRunningEndpoint, err := {{.Variable}}.API.Endpoints.GetURL(exchange.WebsocketSpot)
Expand Down Expand Up @@ -171,12 +178,16 @@ func ({{.Variable}} *{{.CapitalName}}) Setup(exch *config.Exchange) error {
}

// Start starts the {{.CapitalName}} go routine
func ({{.Variable}} *{{.CapitalName}}) Start(wg *sync.WaitGroup) {
func ({{.Variable}} *{{.CapitalName}}) Start(wg *sync.WaitGroup) error {
if wg == nil {
return fmt.Errorf("%T %w", wg, common.ErrNilPointer)
}
wg.Add(1)
go func() {
{{.Variable}}.Run()
wg.Done()
}()
return nil
}

// Run implements the {{.CapitalName}} wrapper
Expand Down
205 changes: 51 additions & 154 deletions cmd/exchange_wrapper_coverage/main.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
package main

import (
"context"
"errors"
"fmt"
"log"
"math/rand"
"reflect"
"sync"
"time"

"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/engine"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
)

const (
totalWrappers = 25
)

func main() {
Expand All @@ -29,12 +21,16 @@ func main() {

engine.Bot.Settings = engine.Settings{
DisableExchangeAutoPairUpdates: true,
EnableDryRun: true,
}

engine.Bot.Config.PurgeExchangeAPICredentials()
engine.Bot.ExchangeManager = engine.SetupExchangeManager()

log.Printf("Loading exchanges..")
var wg sync.WaitGroup
for x := range exchange.Exchanges {
err := engine.Bot.LoadExchange(exchange.Exchanges[x], &wg)
err = engine.Bot.LoadExchange(exchange.Exchanges[x], &wg)
if err != nil {
log.Printf("Failed to load exchange %s. Err: %s",
exchange.Exchanges[x],
Expand All @@ -53,17 +49,28 @@ func main() {
exch := exchanges[x]
wg.Add(1)
go func(e exchange.IBotExchange) {
results[e.GetName()] = testWrappers(e)
results[e.GetName()], err = testWrappers(e)
if err != nil {
fmt.Printf("failed to test wrappers for %s %s", e.GetName(), err)
}
wg.Done()
}(exch)
}
wg.Wait()
log.Println("Done.")

var dummyInterface exchange.IBotExchange
totalWrappers := reflect.TypeOf(&dummyInterface).Elem().NumMethod()

log.Println()
for name, funcs := range results {
pct := float64(totalWrappers-len(funcs)) / float64(totalWrappers) * 100
log.Printf("Exchange %s wrapper coverage [%d/%d - %.2f%%] | Total missing: %d", name, totalWrappers-len(funcs), totalWrappers, pct, len(funcs))
log.Printf("Exchange %s wrapper coverage [%d/%d - %.2f%%] | Total missing: %d",
name,
totalWrappers-len(funcs),
totalWrappers,
pct,
len(funcs))
log.Printf("\t Wrappers not implemented:")

for x := range funcs {
Expand All @@ -73,149 +80,39 @@ func main() {
}
}

func testWrappers(e exchange.IBotExchange) []string {
p := currency.NewPair(currency.BTC, currency.USD)
assetType := asset.Spot
if !e.SupportsAsset(assetType) {
assets := e.GetAssetTypes(false)
rand.Seed(time.Now().Unix())
assetType = assets[rand.Intn(len(assets))] // nolint:gosec // basic number generation required, no need for crypo/rand
}
// testWrappers executes and checks each IBotExchange's function return for the
// error common.ErrNotYetImplemented to verify whether the wrapper function has
// been implemented yet.
func testWrappers(e exchange.IBotExchange) ([]string, error) {
iExchange := reflect.TypeOf(&e).Elem()
actualExchange := reflect.ValueOf(e)
errType := reflect.TypeOf(common.ErrNotYetImplemented)

var funcs []string
for x := 0; x < iExchange.NumMethod(); x++ {
name := iExchange.Method(x).Name
method := actualExchange.MethodByName(name)
inputs := make([]reflect.Value, method.Type().NumIn())
for y := 0; y < method.Type().NumIn(); y++ {
input := method.Type().In(y)
inputs[y] = reflect.Zero(input)
}

_, err := e.FetchTicker(context.TODO(), p, assetType)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "FetchTicker")
}

_, err = e.UpdateTicker(context.TODO(), p, assetType)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "UpdateTicker")
}

_, err = e.FetchOrderbook(context.TODO(), p, assetType)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "FetchOrderbook")
}

_, err = e.UpdateOrderbook(context.TODO(), p, assetType)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "UpdateOrderbook")
}

_, err = e.FetchTradablePairs(context.TODO(), asset.Spot)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "FetchTradablePairs")
}

err = e.UpdateTradablePairs(context.TODO(), false)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "UpdateTradablePairs")
}

_, err = e.FetchAccountInfo(context.TODO(), assetType)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "GetAccountInfo")
}

_, err = e.GetRecentTrades(context.TODO(), p, assetType)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "GetRecentTrades")
}

_, err = e.GetHistoricTrades(context.TODO(), p, assetType, time.Time{}, time.Time{})
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "GetHistoricTrades")
}

_, err = e.GetFundingHistory(context.TODO())
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "GetFundingHistory")
}

_, err = e.SubmitOrder(context.TODO(), nil)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "SubmitOrder")
}

_, err = e.ModifyOrder(context.TODO(), nil)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "ModifyOrder")
}

err = e.CancelOrder(context.TODO(), nil)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "CancelOrder")
}

_, err = e.CancelBatchOrders(context.TODO(), nil)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "CancelBatchOrders")
}

_, err = e.CancelAllOrders(context.TODO(), nil)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "CancelAllOrders")
}

_, err = e.GetOrderInfo(context.TODO(), "1", p, assetType)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "GetOrderInfo")
}

_, err = e.GetOrderHistory(context.TODO(), nil)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "GetOrderHistory")
}

_, err = e.GetActiveOrders(context.TODO(), nil)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "GetActiveOrders")
}

_, err = e.GetDepositAddress(context.TODO(), currency.BTC, "", "")
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "GetDepositAddress")
}

_, err = e.WithdrawCryptocurrencyFunds(context.TODO(), nil)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "WithdrawCryptocurrencyFunds")
}

_, err = e.WithdrawFiatFunds(context.TODO(), nil)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "WithdrawFiatFunds")
}
_, err = e.WithdrawFiatFundsToInternationalBank(context.TODO(), nil)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "WithdrawFiatFundsToInternationalBank")
}

_, err = e.GetHistoricCandles(context.TODO(), currency.Pair{}, asset.Spot, time.Unix(0, 0), time.Unix(0, 0), kline.OneDay)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "GetHistoricCandles")
}

_, err = e.GetHistoricCandlesExtended(context.TODO(), currency.Pair{}, asset.Spot, time.Unix(0, 0), time.Unix(0, 0), kline.OneDay)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "GetHistoricCandlesExtended")
}

_, err = e.UpdateAccountInfo(context.TODO(), assetType)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "UpdateAccountInfo")
}

_, err = e.GetFeeByType(context.TODO(), &exchange.FeeBuilder{})
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "GetFeeByType")
}

err = e.UpdateOrderExecutionLimits(context.TODO(), asset.DownsideProfitContract)
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, "UpdateOrderExecutionLimits")
outputs := method.Call(inputs)
for y := range outputs {
incoming := outputs[y].Interface()
if reflect.TypeOf(incoming) == errType {
err, ok := incoming.(error)
if !ok {
return nil, fmt.Errorf("%s type assertion failure for %v", name, incoming)
}
if errors.Is(err, common.ErrNotYetImplemented) {
funcs = append(funcs, name)
}
// found error; there should not be another error in this slice.
break
}
}
}
return funcs
return funcs, nil
}
4 changes: 3 additions & 1 deletion common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ var (
// ErrStartEqualsEnd is an error for start end check calculations
ErrStartEqualsEnd = errors.New("start date equals end date")
// ErrStartAfterTimeNow is an error for start end check calculations
ErrStartAfterTimeNow = errors.New("start date is after current time")
ErrStartAfterTimeNow = errors.New("start date is after current time")
// ErrNilPointer defines an error for a nil pointer
ErrNilPointer = errors.New("nil pointer")
errCannotSetInvalidTimeout = errors.New("cannot set new HTTP client with timeout that is equal or less than 0")
errUserAgentInvalid = errors.New("cannot set invalid user agent")
errHTTPClientInvalid = errors.New("custom http client cannot be nil")
Expand Down
11 changes: 11 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import (
"github.com/thrasher-corp/gocryptotrader/portfolio/banking"
)

// errExchangeConfigIsNil defines an error when the config is nil
var errExchangeConfigIsNil = errors.New("exchange config is nil")

// GetCurrencyConfig returns currency configurations
func (c *Config) GetCurrencyConfig() CurrencyConfig {
return c.Currency
Expand Down Expand Up @@ -1893,3 +1896,11 @@ func (c *Config) GetDataPath(elem ...string) string {
}
return filepath.Join(append([]string{baseDir}, elem...)...)
}

// Validate checks if exchange config is valid
func (c *Exchange) Validate() error {
if c == nil {
return errExchangeConfigIsNil
}
return nil
}
12 changes: 12 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2358,3 +2358,15 @@ func TestMigrateConfig(t *testing.T) {
})
}
}

func TestExchangeConfigValidate(t *testing.T) {
err := (*Exchange)(nil).Validate()
if !errors.Is(err, errExchangeConfigIsNil) {
t.Fatalf("received: '%v' but expected: '%v'", err, errExchangeConfigIsNil)
}

err = (&Exchange{}).Validate()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
}
7 changes: 5 additions & 2 deletions docs/ADD_NEW_EXCHANGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -1096,12 +1096,15 @@ Add websocket functionality if supported to Setup:
```go
// Setup takes in the supplied exchange configuration details and sets params
func (f *FTX) Setup(exch *config.Exchange) error {
err := exch.Validate()
if err != nil {
return err
}
if !exch.Enabled {
f.SetEnabled(false)
return nil
}

err := f.SetupDefaults(exch)
err = f.SetupDefaults(exch)
if err != nil {
return err
}
Expand Down
4 changes: 4 additions & 0 deletions engine/database_connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"sync/atomic"
"time"

"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/database"
dbpsql "github.com/thrasher-corp/gocryptotrader/database/drivers/postgres"
dbsqlite3 "github.com/thrasher-corp/gocryptotrader/database/drivers/sqlite3"
Expand Down Expand Up @@ -68,6 +69,9 @@ func (m *DatabaseConnectionManager) IsConnected() bool {

// Start sets up the database connection manager to maintain a SQL connection
func (m *DatabaseConnectionManager) Start(wg *sync.WaitGroup) (err error) {
if wg == nil {
return fmt.Errorf("%T %w", wg, common.ErrNilPointer)
}
if m == nil {
return fmt.Errorf("%s %w", DatabaseConnectionManagerName, ErrNilSubsystem)
}
Expand Down
Loading

0 comments on commit da34024

Please sign in to comment.