Files
gocryptotrader/engine/engine_test.go
Samuael A. 3f534a15f1 cmd/exchange_template, exchanges: Update templates and propogate to exchanges (#1777)
* Added TimeInForce type and updated related files

* Linter issue fix and minor coinbasepro type update

* Bitrex consts update

* added unit test and minor changes in bittrex

* Unit tests update

* Fix minor linter issues

* Update TestStringToTimeInForce unit test

* Exchange test template change

* A different approach

* fix conflict with gateio timeInForce

* minor exchange template update

* Minor fix to test_files template

* Update order tests

* Complete updating the order unit tests

* Updating exchange wrapper and test template files

* update kucoin and deribit wrapper to match the time in force change

* minor comment update

* fix time-in-force related test errors

* linter issue fix

* ADD_NEW_EXCHANGE documentation update

* time in force constants, functions and unit tests update

* shift tif policies to TimeInForce

* Update time-in-force, related functions, and unit tests

* fix linter issue and time-in-force processing

* added a good till crossing tif value

* order type fix and fix related tim-in-force entries

* update time-in-force unmarshaling and unit test

* consistency guideline added

* fix time-in-force error in gateio

* linter issue fix

* update based on review comments

* add unit test and fix missing issues

* minor fix and added benchmark unit test

* change GTT to GTC for limit

* fix linter issue

* added time-in-force value to place order param

* fix minor issues based on review comment and move tif code to separate files

* update on exchanges linked to time-in-force

* resolve missing review comments

* minor linter issues fix

* added time-in-force handler and update timeInForce parametered endpoint

* minor fixes based on review

* nits fix

* update based on review

* linter fix

* rm getTimeInForce func and minor change to time-in-force

* minor change

* update based on review comments

* wrappers and time-in-force calling approach

* minor change

* update gateio string to timeInForce conversion and unit test

* update exchange template

* update wrapper template file

* policy comments, and template files update

* rename all exchange types name to Exchange

* update on template files and template generation

* templates and generation code and other updates

* linter issue fix

* added subscriptions and websocket templates

* update ADD_NEW_EXCHANGE.md with recent binance functions and implementations

* rename template files and update unit tests

* minor template and unit test fix

* rename templates and fix on unit tests

* update on template files and documentation

* removed unnecessary tag fix and update templates

* fix Add_NEW_EXCHANGE.md doc file

* formatting, comments, and error checks update on template files

* rename exchange receivers to e and ex for consistency

* rename unit test exchange receiver and minor updates

* linter issues fix

* fix deribit issue and minor style update

* fix test issues caused by receiver change

* raname local variables exchange declaration variables

* update templates comments

* update templates and related comments

* renamed ex to e

* update template comments

* toggle WS to false to improve coverage

* template comments update

* added test coverage to Ws enabled and minor changes

---------

Co-authored-by: Samuel Reid <43227667+cranktakular@users.noreply.github.com>
2025-07-17 10:46:36 +10:00

491 lines
14 KiB
Go

package engine
import (
"os"
"slices"
"strings"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/config"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/bitfinex"
"github.com/thrasher-corp/gocryptotrader/exchanges/bitstamp"
)
// blockedCIExchanges are exchanges that are not able to be tested on CI
var blockedCIExchanges = []string{
"binance", // binance API is banned from executing within the US where github Actions is ran
"bybit", // bybit API is banned from executing within the US where github Actions is ran
}
func isCITest() bool {
return os.Getenv("CI") == "true"
}
func TestLoadConfigWithSettings(t *testing.T) {
empty := ""
somePath := "somePath"
// Clean up after the tests
defer os.RemoveAll(somePath)
tests := []struct {
name string
flags []string
settings *Settings
want *string
wantErr bool
}{
{
name: "invalid file",
settings: &Settings{
ConfigFile: "nonExistent.json",
},
wantErr: true,
},
{
name: "test file",
settings: &Settings{
ConfigFile: config.TestFile,
CoreSettings: CoreSettings{EnableDryRun: true},
},
want: &empty,
wantErr: false,
},
{
name: "data dir in settings overrides config data dir",
flags: []string{"datadir"},
settings: &Settings{
ConfigFile: config.TestFile,
DataDir: somePath,
CoreSettings: CoreSettings{EnableDryRun: true},
},
want: &somePath,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// prepare the 'flags'
flagSet := make(map[string]bool)
for _, v := range tt.flags {
flagSet[v] = true
}
// Run the test
got, err := loadConfigWithSettings(tt.settings, flagSet)
if (err != nil) != tt.wantErr {
t.Errorf("loadConfigWithSettings() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != nil || tt.want != nil {
if (got == nil && tt.want != nil) || (got != nil && tt.want == nil) {
t.Errorf("loadConfigWithSettings() = is nil %v, want nil %v", got == nil, tt.want == nil)
} else if got.DataDirectory != *tt.want {
t.Errorf("loadConfigWithSettings() = %v, want %v", got.DataDirectory, *tt.want)
}
}
})
}
}
func TestStartStopDoesNotCausePanic(t *testing.T) {
t.Parallel()
tempDir := t.TempDir()
botOne, err := NewFromSettings(&Settings{
ConfigFile: config.TestFile,
CoreSettings: CoreSettings{EnableDryRun: true},
DataDir: tempDir,
}, nil)
if err != nil {
t.Fatal(err)
}
botOne.Settings.EnableGRPCProxy = false
for i := range botOne.Config.Exchanges {
if botOne.Config.Exchanges[i].Name != testExchange {
// there is no need to load all exchanges for this test
botOne.Config.Exchanges[i].Enabled = false
}
}
if err = botOne.Start(); err != nil {
t.Error(err)
}
botOne.Stop()
}
var enableExperimentalTest = false
func TestStartStopTwoDoesNotCausePanic(t *testing.T) {
t.Parallel()
if !enableExperimentalTest {
t.Skip("test is functional, however does not need to be included in go test runs")
}
tempDir := t.TempDir()
tempDir2 := t.TempDir()
botOne, err := NewFromSettings(&Settings{
ConfigFile: config.TestFile,
CoreSettings: CoreSettings{EnableDryRun: true},
DataDir: tempDir,
}, nil)
if err != nil {
t.Error(err)
}
botOne.Settings.EnableGRPCProxy = false
botTwo, err := NewFromSettings(&Settings{
ConfigFile: config.TestFile,
CoreSettings: CoreSettings{EnableDryRun: true},
DataDir: tempDir2,
}, nil)
if err != nil {
t.Error(err)
}
botTwo.Settings.EnableGRPCProxy = false
if err = botOne.Start(); err != nil {
t.Error(err)
}
if err = botTwo.Start(); err != nil {
t.Error(err)
}
botOne.Stop()
botTwo.Stop()
}
func TestGetExchangeByName(t *testing.T) {
t.Parallel()
_, err := (*ExchangeManager)(nil).GetExchangeByName("tehehe")
assert.ErrorIs(t, err, ErrNilSubsystem)
em := NewExchangeManager()
exch, err := em.NewExchangeByName(testExchange)
require.NoError(t, err)
exch.SetDefaults()
exch.SetEnabled(true)
err = em.Add(exch)
require.NoError(t, err)
e := &Engine{ExchangeManager: em}
if !exch.IsEnabled() {
t.Errorf("TestGetExchangeByName: Unexpected result")
}
exch.SetEnabled(false)
bfx, err := e.GetExchangeByName(testExchange)
if err != nil {
t.Fatal(err)
}
if bfx.IsEnabled() {
t.Errorf("TestGetExchangeByName: Unexpected result")
}
if exch.GetName() != testExchange {
t.Errorf("TestGetExchangeByName: Unexpected result")
}
_, err = e.GetExchangeByName("Asdasd")
assert.ErrorIs(t, err, ErrExchangeNotFound)
}
func TestUnloadExchange(t *testing.T) {
t.Parallel()
em := NewExchangeManager()
exch, err := em.NewExchangeByName(testExchange)
require.NoError(t, err)
exch.SetDefaults()
exch.SetEnabled(true)
err = em.Add(exch)
require.NoError(t, err)
e := &Engine{
ExchangeManager: em,
Config: &config.Config{Exchanges: []config.Exchange{{Name: testExchange}}},
}
err = e.UnloadExchange("asdf")
assert.ErrorIs(t, err, config.ErrExchangeNotFound)
err = e.UnloadExchange(testExchange)
if err != nil {
t.Errorf("TestUnloadExchange: Failed to get exchange. %s",
err)
}
err = e.UnloadExchange(testExchange)
assert.ErrorIs(t, err, ErrExchangeNotFound)
}
func TestDryRunParamInteraction(t *testing.T) {
t.Parallel()
bot := &Engine{
ExchangeManager: NewExchangeManager(),
Settings: Settings{},
Config: &config.Config{
Exchanges: []config.Exchange{
{
Name: testExchange,
WebsocketTrafficTimeout: time.Second,
},
},
},
}
err := bot.LoadExchange(testExchange)
assert.NoError(t, err, "LoadExchange should not error")
exchCfg, err := bot.Config.GetExchangeConfig(testExchange)
if err != nil {
t.Error(err)
}
if exchCfg.Verbose {
t.Error("verbose should have been disabled")
}
if err = bot.UnloadExchange(testExchange); err != nil {
t.Error(err)
}
// Now set dryrun mode to true,
// enable exchange verbose mode and verify that verbose mode
// will be set on Bitfinex
bot.Settings.EnableDryRun = true
bot.Settings.CheckParamInteraction = true
bot.Settings.EnableExchangeVerbose = true
err = bot.LoadExchange(testExchange)
assert.NoError(t, err, "LoadExchange should not error")
exchCfg, err = bot.Config.GetExchangeConfig(testExchange)
if err != nil {
t.Error(err)
}
if !bot.Settings.EnableDryRun ||
!exchCfg.Verbose {
t.Error("dryrun should be true and verbose should be true")
}
}
func TestFlagSetWith(t *testing.T) {
var isRunning bool
flags := make(FlagSet)
// Flag not set default to config
flags.WithBool("NOT SET", &isRunning, true)
if !isRunning {
t.Fatalf("received: '%v' but expected: '%v'", isRunning, true)
}
flags.WithBool("NOT SET", &isRunning, false)
if isRunning {
t.Fatalf("received: '%v' but expected: '%v'", isRunning, false)
}
flags["IS SET"] = true
isRunning = true
// Flag set true which will override config
flags.WithBool("IS SET", &isRunning, true)
if !isRunning {
t.Fatalf("received: '%v' but expected: '%v'", isRunning, true)
}
flags.WithBool("IS SET", &isRunning, false)
if !isRunning {
t.Fatalf("received: '%v' but expected: '%v'", isRunning, true)
}
flags["IS SET"] = true
isRunning = false
// Flag set false which will override config
flags.WithBool("IS SET", &isRunning, true)
if isRunning {
t.Fatalf("received: '%v' but expected: '%v'", isRunning, false)
}
flags.WithBool("IS SET", &isRunning, false)
if isRunning {
t.Fatalf("received: '%v' but expected: '%v'", isRunning, false)
}
}
func TestRegisterWebsocketDataHandler(t *testing.T) {
t.Parallel()
var e *Engine
err := e.RegisterWebsocketDataHandler(nil, false)
require.ErrorIs(t, err, errNilBot)
e = &Engine{WebsocketRoutineManager: &WebsocketRoutineManager{}}
err = e.RegisterWebsocketDataHandler(func(_ string, _ any) error { return nil }, false)
require.NoError(t, err)
}
func TestSetDefaultWebsocketDataHandler(t *testing.T) {
t.Parallel()
var e *Engine
err := e.SetDefaultWebsocketDataHandler()
require.ErrorIs(t, err, errNilBot)
e = &Engine{WebsocketRoutineManager: &WebsocketRoutineManager{}}
err = e.SetDefaultWebsocketDataHandler()
require.NoError(t, err)
}
func TestSettingsPrint(t *testing.T) {
t.Parallel()
var s *Settings
s.PrintLoadedSettings()
s = &Settings{}
s.PrintLoadedSettings()
}
var unsupportedDefaultConfigExchanges = []string{
"poloniex", // poloniex has dropped support for the API GCT has implemented //TODO: drop this when supported
"coinbasepro", // deprecated API. TODO: Remove this when the Coinbase update is merged
}
func TestGetDefaultConfigurations(t *testing.T) {
t.Parallel()
em := NewExchangeManager()
for i := range exchange.Exchanges {
name := strings.ToLower(exchange.Exchanges[i])
t.Run(name, func(t *testing.T) {
t.Parallel()
exch, err := em.NewExchangeByName(name)
require.NoError(t, err, "NewExchangeByName must not error")
if isCITest() && slices.Contains(blockedCIExchanges, name) {
t.Skipf("skipping %s due to CI test restrictions", name)
}
if slices.Contains(unsupportedDefaultConfigExchanges, name) {
t.Skipf("skipping %s unsupported", name)
}
defaultCfg, err := exchange.GetDefaultConfig(t.Context(), exch)
require.NoError(t, err, "GetDefaultConfig must not error")
require.NotNil(t, defaultCfg)
assert.NotEmpty(t, defaultCfg.Name, "Name should not be empty")
assert.True(t, defaultCfg.Enabled, "Enabled should have the correct value")
if exch.SupportsWebsocket() {
assert.Positive(t, defaultCfg.WebsocketResponseCheckTimeout, "WebsocketResponseCheckTimeout should be positive")
assert.Positive(t, defaultCfg.WebsocketResponseMaxLimit, "WebsocketResponseMaxLimit should be positive")
assert.Positive(t, defaultCfg.WebsocketTrafficTimeout, "WebsocketTrafficTimeout should be positive")
}
require.NoError(t, exch.Setup(defaultCfg), "Setup must not error")
})
}
}
func TestSetupExchanges(t *testing.T) {
t.Parallel()
t.Run("No enabled exchanges", func(t *testing.T) {
t.Parallel()
e := &Engine{
Config: &config.Config{Exchanges: []config.Exchange{{Name: testExchange}}},
}
assert.ErrorIs(t, e.SetupExchanges(), ErrNoExchangesLoaded)
})
t.Run("EnableAllExchanges with specific exchanges set", func(t *testing.T) {
t.Parallel()
e := &Engine{
Config: &config.Config{},
Settings: Settings{
CoreSettings: CoreSettings{
EnableAllExchanges: true,
Exchanges: "Bitstamp,Bitfinex",
},
},
}
assert.EqualError(t, e.SetupExchanges(), "cannot enable all exchanges and specific exchanges concurrently")
})
t.Run("Settings dry run toggling", func(t *testing.T) {
t.Parallel()
e := &Engine{
Config: &config.Config{},
Settings: Settings{
CoreSettings: CoreSettings{
EnableAllPairs: true,
EnableAllExchanges: true,
},
ExchangeTuningSettings: ExchangeTuningSettings{
EnableExchangeVerbose: true,
EnableExchangeWebsocketSupport: true,
EnableExchangeAutoPairUpdates: true,
DisableExchangeAutoPairUpdates: true,
HTTPUserAgent: "test",
HTTPProxy: "test",
HTTPTimeout: 1,
EnableExchangeHTTPDebugging: true,
},
},
}
assert.ErrorIs(t, e.SetupExchanges(), ErrNoExchangesLoaded)
assert.False(t, e.Settings.EnableDryRun)
e.Settings.CheckParamInteraction = true
assert.ErrorIs(t, e.SetupExchanges(), ErrNoExchangesLoaded)
assert.True(t, e.Settings.EnableDryRun)
})
// Test that overridden exchange inputs are handled correctly
testCases := []struct {
name string
exchangeString string
expectedError string
}{
{"Invalid exchange pair", "bob|jill", "exchange bob|jill not found"},
{"Single invalid exchange", "bob", "exchange bob not found"},
{"Mixed valid and invalid exchanges", "bob,bitstamp", "exchange bob not found"},
{"Valid exchange", "BiTSTaMp", "no exchanges have been loaded"}, // Proper exchange name, but not loaded
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
e := &Engine{
Config: &config.Config{},
Settings: Settings{CoreSettings: CoreSettings{Exchanges: tc.exchangeString}},
}
assert.ErrorContains(t, e.SetupExchanges(), tc.expectedError)
})
}
t.Run("Two valid exchanges with exchanges flag toggled", func(t *testing.T) {
t.Parallel()
e := &Engine{Config: &config.Config{}}
exchLoader := func(exch exchange.IBotExchange) {
exch.SetDefaults()
exch.GetBase().Features.Supports.RESTCapabilities.AutoPairUpdates = false
cfg, err := exchange.GetDefaultConfig(t.Context(), exch)
require.NoError(t, err)
e.Config.Exchanges = append(e.Config.Exchanges, *cfg)
}
e.ExchangeManager = NewExchangeManager()
exchLoader(new(bitstamp.Exchange))
exchLoader(new(bitfinex.Exchange))
assert.ElementsMatch(t, []string{"Bitstamp", "Bitfinex"}, e.Config.GetEnabledExchanges())
t.Run("Load specific exchange", func(t *testing.T) {
e.Settings.Exchanges = "BiTfInEx"
assert.NoError(t, e.SetupExchanges(), "SetupExchanges with a valid exchange should not error")
exchanges, err := e.ExchangeManager.GetExchanges()
require.NoError(t, err)
require.Len(t, exchanges, 1)
assert.Equal(t, "Bitfinex", exchanges[0].GetName())
})
t.Run("Load all enabled exchanges", func(t *testing.T) {
e.Settings.Exchanges = ""
assert.NoError(t, e.SetupExchanges(), "SetupExchanges with all enabled exchanges should not error")
exchanges, err := e.ExchangeManager.GetExchanges()
require.NoError(t, err)
require.Len(t, exchanges, 2)
exchangeNames := []string{exchanges[0].GetName(), exchanges[1].GetName()}
assert.ElementsMatch(t, []string{"Bitstamp", "Bitfinex"}, exchangeNames)
})
})
}