engine/exchanges: Add exchange currency state subsystem (#774)

* state: Add management system (init)

* linter: fix

* engine: gofmt

* gct: after merge fixup

* documentation: add

* rpc: implement services for testing

* gctcli: gofmt state_management.go

* documentation: reinstate lost information

* state: Add pair check to determine trading operation

* exchanges: add interface for specific state scoped subsystem functionality

* engine/order_man: reduce code footprint using new method

* RPC: implement pair trading request and change exported name to something specific to state

* engine: add tests

* engine: Add to withdraw manager

* documentation: reinstate soxipy in contrib. list

* engine: const fake name

* Glorious: NITERINOS

* merge: fix issues

* engine: csm incorporate service name into log output

* engine: fix linter issues

* gct: fix tests

* currencystate: remove management type

* rpc: fix tests

* backtester: fix tests

* Update engine/currency_state_manager.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* Update engine/currency_state_manager.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* Update exchanges/currencystate/currency_state.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* Update exchanges/alert/alert.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* Update exchanges/alert/alert.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits

* config: integrate with config and remove flag delay adjustment

* gctcli: fix issues after name changes

* engine: gofmt manager file

* Update engine/rpcserver.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* engine: Add enable/disable manager functions, add default popoulation for potential assets

* linter: fix

* engine/test: bump subsystem count

* Update engine/currency_state_manager.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* Update exchanges/bithumb/bithumb.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits addressed

* alert: fix commenting for its generalized purpose

* glorious: nits

* engine: use standard string in log output

* bitfinex: apply patch, thanks @thrasher-

* bitfinex: fix spelling

* engine/currencystate: Add logs/fix logs

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>
This commit is contained in:
Ryan O'Hara-Reid
2021-09-27 13:33:49 +10:00
committed by GitHub
parent 1d7c656665
commit 5dfbbf84de
48 changed files with 4685 additions and 1110 deletions

View File

@@ -0,0 +1,276 @@
package engine
import (
"context"
"errors"
"fmt"
"sync"
"sync/atomic"
"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/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/currencystate"
"github.com/thrasher-corp/gocryptotrader/gctrpc"
"github.com/thrasher-corp/gocryptotrader/log"
)
const (
// CurrencyStateManagementName defines the manager name string
CurrencyStateManagementName = "currency_state_manager"
// DefaultStateManagerDelay defines the default duration when the manager
// fetches and updates each exchange for its currency state
DefaultStateManagerDelay = time.Minute
)
var enabled = &gctrpc.GenericResponse{Status: "enabled"}
// CurrencyStateManager manages currency states
type CurrencyStateManager struct {
started int32
shutdown chan struct{}
wg sync.WaitGroup
iExchangeManager
sleep time.Duration
}
// SetupCurrencyStateManager applies configuration parameters before running
func SetupCurrencyStateManager(interval time.Duration, em iExchangeManager) (*CurrencyStateManager, error) {
if em == nil {
return nil, errNilExchangeManager
}
var c CurrencyStateManager
if interval <= 0 {
log.Warnf(log.ExchangeSys,
"Currency state manager interval is invalid, defaulting to: %s",
DefaultStateManagerDelay)
interval = DefaultStateManagerDelay
}
c.sleep = interval
c.iExchangeManager = em
c.shutdown = make(chan struct{})
return &c, nil
}
// Start runs the subsystem
func (c *CurrencyStateManager) Start() error {
log.Debugln(log.ExchangeSys, "Currency state manager starting...")
if c == nil {
return fmt.Errorf("%s %w", CurrencyStateManagementName, ErrNilSubsystem)
}
if !atomic.CompareAndSwapInt32(&c.started, 0, 1) {
return fmt.Errorf("%s %w", CurrencyStateManagementName, ErrSubSystemAlreadyStarted)
}
c.wg.Add(1)
go c.monitor()
log.Debugln(log.ExchangeSys, "Currency state manager started.")
return nil
}
// Stop stops the subsystem
func (c *CurrencyStateManager) Stop() error {
if c == nil {
return fmt.Errorf("%s %w", CurrencyStateManagementName, ErrNilSubsystem)
}
if atomic.LoadInt32(&c.started) == 0 {
return fmt.Errorf("%s %w", CurrencyStateManagementName, ErrSubSystemNotStarted)
}
log.Debugf(log.ExchangeSys, "Currency state manager %s", MsgSubSystemShuttingDown)
close(c.shutdown)
c.wg.Wait()
c.shutdown = make(chan struct{})
log.Debugf(log.ExchangeSys, "Currency state manager %s", MsgSubSystemShutdown)
atomic.StoreInt32(&c.started, 0)
return nil
}
// IsRunning safely checks whether the subsystem is running
func (c *CurrencyStateManager) IsRunning() bool {
if c == nil {
return false
}
return atomic.LoadInt32(&c.started) == 1
}
func (c *CurrencyStateManager) monitor() {
defer c.wg.Done()
timer := time.NewTimer(0) // Prime firing of channel for initial sync.
for {
select {
case <-c.shutdown:
return
case <-timer.C:
var wg sync.WaitGroup
exchs, err := c.GetExchanges()
if err != nil {
log.Errorf(log.Global,
"Currency state manager failed to get exchanges error: %v",
err)
}
for x := range exchs {
wg.Add(1)
go c.update(exchs[x], &wg, exchs[x].GetAssetTypes(true))
}
wg.Wait() // This causes some variability in the timer due to
// longest length of request time. Can do time.Ticker but don't
// want routines to stack behind, this is more uniform.
timer.Reset(c.sleep)
}
}
}
func (c *CurrencyStateManager) update(exch exchange.IBotExchange, wg *sync.WaitGroup, enabledAssets asset.Items) {
defer wg.Done()
for y := range enabledAssets {
err := exch.UpdateCurrencyStates(context.TODO(), enabledAssets[y])
if err != nil {
if errors.Is(err, common.ErrNotYetImplemented) {
// Deploy default values for outbound gRPC aspects.
var pairs currency.Pairs
pairs, err = exch.GetAvailablePairs(enabledAssets[y])
if err != nil {
log.Errorf(log.ExchangeSys, "Currency state manager %s %s: %v",
exch.GetName(),
enabledAssets[y],
err)
return
}
// Deploys a full spectrum supported list for the currency states
update := map[currency.Code]currencystate.Options{}
for x := range pairs {
update[pairs[x].Base] = currencystate.Options{}
update[pairs[x].Quote] = currencystate.Options{}
}
b := exch.GetBase()
if b == nil {
log.Errorf(log.ExchangeSys, "Currency state manager %s %s: %v",
exch.GetName(),
enabledAssets[y],
"cannot update because base is nil")
return
}
err = b.States.UpdateAll(enabledAssets[y], update)
if err != nil {
log.Errorf(log.ExchangeSys, "Currency state manager %s %s: %v",
exch.GetName(),
enabledAssets[y],
err)
}
return
}
log.Errorf(log.ExchangeSys, "Currency state manager %s %s: %v",
exch.GetName(),
enabledAssets[y],
err)
}
}
}
// GetAllRPC returns a full snapshot of currency states, whether they are able
// to be withdrawn, deposited or traded on an exchange for RPC.
func (c *CurrencyStateManager) GetAllRPC(exchName string) (*gctrpc.CurrencyStateResponse, error) {
if !c.IsRunning() {
return nil, fmt.Errorf("%s %w", CurrencyStateManagementName, ErrSubSystemNotStarted)
}
exch, err := c.GetExchangeByName(exchName)
if err != nil {
return nil, err
}
sh, err := exch.GetCurrencyStateSnapshot()
if err != nil {
return nil, err
}
var resp = &gctrpc.CurrencyStateResponse{}
for x := range sh {
resp.CurrencyStates = append(resp.CurrencyStates, &gctrpc.CurrencyState{
Currency: sh[x].Code.String(),
Asset: sh[x].Asset.String(),
WithdrawEnabled: sh[x].Withdraw == nil || *sh[x].Withdraw,
DepositEnabled: sh[x].Deposit == nil || *sh[x].Deposit,
TradingEnabled: sh[x].Trade == nil || *sh[x].Trade,
})
}
return resp, nil
}
// CanWithdrawRPC determines if the currency code is operational for withdrawal
// from an exchange for RPC
func (c *CurrencyStateManager) CanWithdrawRPC(exchName string, cc currency.Code, a asset.Item) (*gctrpc.GenericResponse, error) {
if !c.IsRunning() {
return nil, fmt.Errorf("%s %w", CurrencyStateManagementName, ErrSubSystemNotStarted)
}
exch, err := c.GetExchangeByName(exchName)
if err != nil {
return nil, err
}
err = exch.CanWithdraw(cc, a)
if err != nil {
return nil, err
}
return enabled, nil
}
// CanDepositRPC determines if the currency code is operational for depositing
// to an exchange for RPC
func (c *CurrencyStateManager) CanDepositRPC(exchName string, cc currency.Code, a asset.Item) (*gctrpc.GenericResponse, error) {
if !c.IsRunning() {
return nil, fmt.Errorf("%s %w", CurrencyStateManagementName, ErrSubSystemNotStarted)
}
exch, err := c.GetExchangeByName(exchName)
if err != nil {
return nil, err
}
err = exch.CanDeposit(cc, a)
if err != nil {
return nil, err
}
return enabled, nil
}
// CanTradeRPC determines if the currency code is operational for trading for
// RPC
func (c *CurrencyStateManager) CanTradeRPC(exchName string, cc currency.Code, a asset.Item) (*gctrpc.GenericResponse, error) {
if !c.IsRunning() {
return nil, fmt.Errorf("%s %w", CurrencyStateManagementName, ErrSubSystemNotStarted)
}
exch, err := c.GetExchangeByName(exchName)
if err != nil {
return nil, err
}
err = exch.CanTrade(cc, a)
if err != nil {
return nil, err
}
return enabled, nil
}
// CanTradePairRPC determines if the pair is operational for trading for RPC
func (c *CurrencyStateManager) CanTradePairRPC(exchName string, pair currency.Pair, a asset.Item) (*gctrpc.GenericResponse, error) {
if !c.IsRunning() {
return nil, fmt.Errorf("%s %w", CurrencyStateManagementName, ErrSubSystemNotStarted)
}
exch, err := c.GetExchangeByName(exchName)
if err != nil {
return nil, err
}
err = exch.CanTradePair(pair, a)
if err != nil {
return nil, err
}
return enabled, nil
}

View File

@@ -0,0 +1,48 @@
# GoCryptoTrader package State manager
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml)
[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE)
[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/state_manager)
[![Coverage Status](http://codecov.io/github/thrasher-corp/gocryptotrader/coverage.svg?branch=master)](http://codecov.io/github/thrasher-corp/gocryptotrader?branch=master)
[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader)
This state_manager package is part of the GoCryptoTrader codebase.
## 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).
Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk)
## Current Features for State manager
+ The state manager keeps currency states up to date, which include:
* Withdrawal - Determines if the currency is allowed to be withdrawn from the exchange.
* Deposit - Determines if the currency is allowed to be deposited to an exchange.
* Trading - Determines if the currency is allowed to be traded on the exchange.
+ This allows for an internal state check to compliment internal and external
strategies.
## 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
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

View File

@@ -0,0 +1,376 @@
package engine
import (
"context"
"errors"
"sync"
"testing"
"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/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/currencystate"
)
func TestSetupCurrencyStateManager(t *testing.T) {
t.Parallel()
_, err := SetupCurrencyStateManager(0, nil)
if !errors.Is(err, errNilExchangeManager) {
t.Fatalf("received: '%v' but expected: '%v'", err, errNilExchangeManager)
}
cm, err := SetupCurrencyStateManager(0, &ExchangeManager{})
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if cm.sleep != DefaultStateManagerDelay {
t.Fatal("unexpected value")
}
}
var (
errManager = errors.New("manager level error")
errExchange = errors.New("exchange level error")
)
type fakeExchangeManagerino struct {
ErrorMeOne bool
ErrorMeTwo bool
}
func (f *fakeExchangeManagerino) GetExchanges() ([]exchange.IBotExchange, error) {
if f.ErrorMeOne {
return nil, errManager
}
return []exchange.IBotExchange{&fakerino{errorMe: f.ErrorMeTwo}}, nil
}
func (f *fakeExchangeManagerino) GetExchangeByName(_ string) (exchange.IBotExchange, error) {
if f.ErrorMeOne {
return nil, errManager
}
return &fakerino{errorMe: f.ErrorMeTwo}, nil
}
type fakerino struct {
exchange.IBotExchange
errorMe bool
GetAvailablePairsError bool
GetBaseError bool
}
func (f *fakerino) UpdateCurrencyStates(_ context.Context, _ asset.Item) error {
if f.errorMe {
return common.ErrNotYetImplemented
}
return nil
}
func (f *fakerino) GetAssetTypes(_ bool) asset.Items {
return asset.Items{asset.Spot}
}
func (f *fakerino) GetName() string {
return "testssssssssssssss"
}
func (f *fakerino) GetCurrencyStateSnapshot() ([]currencystate.Snapshot, error) {
if f.errorMe {
return nil, errExchange
}
return []currencystate.Snapshot{
{Code: currency.SHORTY, Asset: asset.Spot},
}, nil
}
func (f *fakerino) CanWithdraw(c currency.Code, a asset.Item) error {
if f.errorMe {
return errExchange
}
return nil
}
func (f *fakerino) CanDeposit(c currency.Code, a asset.Item) error {
if f.errorMe {
return errExchange
}
return nil
}
func (f *fakerino) CanTrade(c currency.Code, a asset.Item) error {
if f.errorMe {
return errExchange
}
return nil
}
func (f *fakerino) CanTradePair(p currency.Pair, a asset.Item) error {
if f.errorMe {
return errExchange
}
return nil
}
func (f *fakerino) GetAvailablePairs(a asset.Item) (currency.Pairs, error) {
if f.GetAvailablePairsError {
return nil, errExchange
}
return currency.Pairs{currency.NewPair(currency.BTC, currency.USD)}, nil
}
func (f *fakerino) GetBase() *exchange.Base {
if f.GetBaseError {
return nil
}
return &exchange.Base{States: currencystate.NewCurrencyStates()}
}
func TestCurrencyStateManagerIsRunning(t *testing.T) {
t.Parallel()
err := (*CurrencyStateManager)(nil).Stop()
if !errors.Is(err, ErrNilSubsystem) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrNilSubsystem)
}
err = (&CurrencyStateManager{}).Stop()
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrSubSystemNotStarted)
}
err = (&CurrencyStateManager{started: 1, shutdown: make(chan struct{})}).Stop()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
err = (*CurrencyStateManager)(nil).Start()
if !errors.Is(err, ErrNilSubsystem) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrNilSubsystem)
}
err = (&CurrencyStateManager{started: 1}).Start()
if !errors.Is(err, ErrSubSystemAlreadyStarted) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrSubSystemAlreadyStarted)
}
man := &CurrencyStateManager{
shutdown: make(chan struct{}),
iExchangeManager: &fakeExchangeManagerino{ErrorMeOne: true},
sleep: time.Minute}
err = man.Start()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
time.Sleep(time.Millisecond)
err = man.Stop()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
man.iExchangeManager = &fakeExchangeManagerino{ErrorMeOne: true}
err = man.Start()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
time.Sleep(time.Millisecond)
err = man.Stop()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
man.iExchangeManager = &fakeExchangeManagerino{ErrorMeOne: true}
err = man.Start()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
time.Sleep(time.Millisecond)
if !man.IsRunning() {
t.Fatal("this should be running")
}
err = man.Stop()
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
if man.IsRunning() {
t.Fatal("this should be stopped")
}
}
func TestGetAllRPC(t *testing.T) {
t.Parallel()
_, err := (*CurrencyStateManager)(nil).GetAllRPC("")
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrSubSystemNotStarted)
}
_, err = (&CurrencyStateManager{
started: 1,
iExchangeManager: &fakeExchangeManagerino{ErrorMeOne: true},
}).GetAllRPC("")
if !errors.Is(err, errManager) {
t.Fatalf("received: '%v' but expected: '%v'", err, errManager)
}
_, err = (&CurrencyStateManager{
started: 1,
iExchangeManager: &fakeExchangeManagerino{ErrorMeTwo: true},
}).GetAllRPC("")
if !errors.Is(err, errExchange) {
t.Fatalf("received: '%v' but expected: '%v'", err, errExchange)
}
_, err = (&CurrencyStateManager{
started: 1,
iExchangeManager: &fakeExchangeManagerino{},
}).GetAllRPC("")
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
}
func TestCanWithdrawRPC(t *testing.T) {
t.Parallel()
_, err := (*CurrencyStateManager)(nil).CanWithdrawRPC("", currency.Code{}, "")
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrSubSystemNotStarted)
}
_, err = (&CurrencyStateManager{
started: 1,
iExchangeManager: &fakeExchangeManagerino{ErrorMeOne: true},
}).CanWithdrawRPC("", currency.Code{}, "")
if !errors.Is(err, errManager) {
t.Fatalf("received: '%v' but expected: '%v'", err, errManager)
}
_, err = (&CurrencyStateManager{
started: 1,
iExchangeManager: &fakeExchangeManagerino{ErrorMeTwo: true},
}).CanWithdrawRPC("", currency.Code{}, "")
if !errors.Is(err, errExchange) {
t.Fatalf("received: '%v' but expected: '%v'", err, errExchange)
}
_, err = (&CurrencyStateManager{
started: 1,
iExchangeManager: &fakeExchangeManagerino{},
}).CanWithdrawRPC("", currency.Code{}, "")
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
}
func TestCanDepositRPC(t *testing.T) {
t.Parallel()
_, err := (*CurrencyStateManager)(nil).CanDepositRPC("", currency.Code{}, "")
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrSubSystemNotStarted)
}
_, err = (&CurrencyStateManager{
started: 1,
iExchangeManager: &fakeExchangeManagerino{ErrorMeOne: true},
}).CanDepositRPC("", currency.Code{}, "")
if !errors.Is(err, errManager) {
t.Fatalf("received: '%v' but expected: '%v'", err, errManager)
}
_, err = (&CurrencyStateManager{
started: 1,
iExchangeManager: &fakeExchangeManagerino{ErrorMeTwo: true},
}).CanDepositRPC("", currency.Code{}, "")
if !errors.Is(err, errExchange) {
t.Fatalf("received: '%v' but expected: '%v'", err, errExchange)
}
_, err = (&CurrencyStateManager{
started: 1,
iExchangeManager: &fakeExchangeManagerino{},
}).CanDepositRPC("", currency.Code{}, "")
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
}
func TestCanTradeRPC(t *testing.T) {
t.Parallel()
_, err := (*CurrencyStateManager)(nil).CanTradeRPC("", currency.Code{}, "")
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrSubSystemNotStarted)
}
_, err = (&CurrencyStateManager{
started: 1,
iExchangeManager: &fakeExchangeManagerino{ErrorMeOne: true},
}).CanTradeRPC("", currency.Code{}, "")
if !errors.Is(err, errManager) {
t.Fatalf("received: '%v' but expected: '%v'", err, errManager)
}
_, err = (&CurrencyStateManager{
started: 1,
iExchangeManager: &fakeExchangeManagerino{ErrorMeTwo: true},
}).CanTradeRPC("", currency.Code{}, "")
if !errors.Is(err, errExchange) {
t.Fatalf("received: '%v' but expected: '%v'", err, errExchange)
}
_, err = (&CurrencyStateManager{
started: 1,
iExchangeManager: &fakeExchangeManagerino{},
}).CanTradeRPC("", currency.Code{}, "")
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
}
func TestCanTradePairRPC(t *testing.T) {
t.Parallel()
_, err := (*CurrencyStateManager)(nil).CanTradePairRPC("", currency.Pair{}, "")
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Fatalf("received: '%v' but expected: '%v'", err, ErrSubSystemNotStarted)
}
_, err = (&CurrencyStateManager{
started: 1,
iExchangeManager: &fakeExchangeManagerino{ErrorMeOne: true},
}).CanTradePairRPC("", currency.Pair{}, "")
if !errors.Is(err, errManager) {
t.Fatalf("received: '%v' but expected: '%v'", err, errManager)
}
_, err = (&CurrencyStateManager{
started: 1,
iExchangeManager: &fakeExchangeManagerino{ErrorMeTwo: true},
}).CanTradePairRPC("", currency.Pair{}, "")
if !errors.Is(err, errExchange) {
t.Fatalf("received: '%v' but expected: '%v'", err, errExchange)
}
_, err = (&CurrencyStateManager{
started: 1,
iExchangeManager: &fakeExchangeManagerino{},
}).CanTradePairRPC("", currency.Pair{}, "")
if !errors.Is(err, nil) {
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
}
}
func TestUpdate(t *testing.T) {
man := &CurrencyStateManager{}
var wg sync.WaitGroup
wg.Add(3)
man.update(&fakerino{errorMe: true, GetAvailablePairsError: true}, &wg, asset.Items{asset.Spot})
man.update(&fakerino{errorMe: true, GetBaseError: true}, &wg, asset.Items{asset.Spot})
man.update(&fakerino{errorMe: true}, &wg, asset.Items{asset.Spot})
}

View File

@@ -46,6 +46,7 @@ type Engine struct {
websocketRoutineManager *websocketRoutineManager
WithdrawManager *WithdrawManager
dataHistoryManager *DataHistoryManager
currencyStateManager *CurrencyStateManager
Settings Settings
uptime time.Time
ServicesWG sync.WaitGroup
@@ -145,13 +146,17 @@ func validateSettings(b *Engine, s *Settings, flagSet map[string]bool) {
b.Settings.EnableDataHistoryManager = (flagSet["datahistorymanager"] && b.Settings.EnableDatabaseManager) || b.Config.DataHistoryManager.Enabled
b.Settings.EnableCurrencyStateManager = (flagSet["currencystatemanager"] &&
b.Settings.EnableCurrencyStateManager) ||
b.Config.CurrencyStateManager.Enabled != nil &&
*b.Config.CurrencyStateManager.Enabled
b.Settings.EnableGCTScriptManager = b.Settings.EnableGCTScriptManager &&
(flagSet["gctscriptmanager"] || b.Config.GCTScript.Enabled)
if b.Settings.EnablePortfolioManager {
if b.Settings.PortfolioManagerDelay <= 0 {
b.Settings.PortfolioManagerDelay = PortfolioSleepDelay
}
if b.Settings.EnablePortfolioManager &&
b.Settings.PortfolioManagerDelay <= 0 {
b.Settings.PortfolioManagerDelay = PortfolioSleepDelay
}
if !flagSet["grpc"] {
@@ -242,6 +247,7 @@ func PrintSettings(s *Settings) {
gctlog.Debugf(gctlog.Global, "\t Enable coinmarketcap analaysis: %v", s.EnableCoinmarketcapAnalysis)
gctlog.Debugf(gctlog.Global, "\t Enable portfolio manager: %v", s.EnablePortfolioManager)
gctlog.Debugf(gctlog.Global, "\t Enable data history manager: %v", s.EnableDataHistoryManager)
gctlog.Debugf(gctlog.Global, "\t Enable currency state manager: %v", s.EnableCurrencyStateManager)
gctlog.Debugf(gctlog.Global, "\t Portfolio manager sleep delay: %v\n", s.PortfolioManagerDelay)
gctlog.Debugf(gctlog.Global, "\t Enable gPRC: %v", s.EnableGRPC)
gctlog.Debugf(gctlog.Global, "\t Enable gRPC Proxy: %v", s.EnableGRPCProxy)
@@ -460,8 +466,7 @@ func (bot *Engine) Start() error {
return err
}
if bot.Settings.EnableDeprecatedRPC ||
bot.Settings.EnableWebsocketRPC {
if bot.Settings.EnableDeprecatedRPC || bot.Settings.EnableWebsocketRPC {
var filePath string
filePath, err = config.GetAndMigrateDefaultPath(bot.Settings.ConfigFile)
if err != nil {
@@ -570,11 +575,30 @@ func (bot *Engine) Start() error {
if err != nil {
gctlog.Errorf(gctlog.Global, "failed to create script manager. Err: %s", err)
}
if err := bot.gctScriptManager.Start(&bot.ServicesWG); err != nil {
if err = bot.gctScriptManager.Start(&bot.ServicesWG); err != nil {
gctlog.Errorf(gctlog.Global, "GCTScript manager unable to start: %s", err)
}
}
if bot.Settings.EnableCurrencyStateManager {
bot.currencyStateManager, err = SetupCurrencyStateManager(
bot.Config.CurrencyStateManager.Delay,
bot.ExchangeManager)
if err != nil {
gctlog.Errorf(gctlog.Global,
"%s unable to setup: %s",
CurrencyStateManagementName,
err)
} else {
err = bot.currencyStateManager.Start()
if err != nil {
gctlog.Errorf(gctlog.Global,
"%s unable to start: %s",
CurrencyStateManagementName,
err)
}
}
}
return nil
}
@@ -599,61 +623,51 @@ func (bot *Engine) Stop() {
gctlog.Errorf(gctlog.Global, "Order manager unable to stop. Error: %v", err)
}
}
if bot.eventManager.IsRunning() {
if err := bot.eventManager.Stop(); err != nil {
gctlog.Errorf(gctlog.Global, "event manager unable to stop. Error: %v", err)
}
}
if bot.ntpManager.IsRunning() {
if err := bot.ntpManager.Stop(); err != nil {
gctlog.Errorf(gctlog.Global, "NTP manager unable to stop. Error: %v", err)
}
}
if bot.CommunicationsManager.IsRunning() {
if err := bot.CommunicationsManager.Stop(); err != nil {
gctlog.Errorf(gctlog.Global, "Communication manager unable to stop. Error: %v", err)
}
}
if bot.portfolioManager.IsRunning() {
if err := bot.portfolioManager.Stop(); err != nil {
gctlog.Errorf(gctlog.Global, "Fund manager unable to stop. Error: %v", err)
}
}
if bot.connectionManager.IsRunning() {
if err := bot.connectionManager.Stop(); err != nil {
gctlog.Errorf(gctlog.Global, "Connection manager unable to stop. Error: %v", err)
}
}
if bot.apiServer.IsRESTServerRunning() {
if err := bot.apiServer.StopRESTServer(); err != nil {
gctlog.Errorf(gctlog.Global, "API Server unable to stop REST server. Error: %s", err)
}
}
if bot.apiServer.IsWebsocketServerRunning() {
if err := bot.apiServer.StopWebsocketServer(); err != nil {
gctlog.Errorf(gctlog.Global, "API Server unable to stop websocket server. Error: %s", err)
}
}
if bot.dataHistoryManager.IsRunning() {
if err := bot.dataHistoryManager.Stop(); err != nil {
gctlog.Errorf(gctlog.DataHistory, "data history manager unable to stop. Error: %v", err)
}
}
if bot.DatabaseManager.IsRunning() {
if err := bot.DatabaseManager.Stop(); err != nil {
gctlog.Errorf(gctlog.Global, "Database manager unable to stop. Error: %v", err)
}
}
if dispatch.IsRunning() {
if err := dispatch.Stop(); err != nil {
gctlog.Errorf(gctlog.DispatchMgr, "Dispatch system unable to stop. Error: %v", err)
@@ -664,6 +678,13 @@ func (bot *Engine) Stop() {
gctlog.Errorf(gctlog.Global, "websocket routine manager unable to stop. Error: %v", err)
}
}
if bot.currencyStateManager.IsRunning() {
if err := bot.currencyStateManager.Stop(); err != nil {
gctlog.Errorf(gctlog.Global,
"currency state manager unable to stop. Error: %v",
err)
}
}
if bot.Settings.EnableCoinmarketcapAnalysis ||
bot.Settings.EnableCurrencyConverter ||

View File

@@ -36,6 +36,7 @@ type Settings struct {
EnableGCTScriptManager bool
EnableNTPClient bool
EnableWebsocketRoutine bool
EnableCurrencyStateManager bool
EventManagerDelay time.Duration
Verbose bool

View File

@@ -59,6 +59,7 @@ func (bot *Engine) GetSubsystemsStatus() map[string]bool {
WebsocketName: bot.Settings.EnableWebsocketRPC,
dispatch.Name: dispatch.IsRunning(),
dataHistoryManagerName: bot.dataHistoryManager.IsRunning(),
CurrencyStateManagementName: bot.currencyStateManager.IsRunning(),
}
}
@@ -264,6 +265,19 @@ func (bot *Engine) SetSubsystem(subSystemName string, enable bool) error {
return bot.gctScriptManager.Start(&bot.ServicesWG)
}
return bot.gctScriptManager.Stop()
case strings.ToLower(CurrencyStateManagementName):
if enable {
if bot.currencyStateManager == nil {
bot.currencyStateManager, err = SetupCurrencyStateManager(
bot.Config.CurrencyStateManager.Delay,
bot.ExchangeManager)
if err != nil {
return err
}
}
return bot.currencyStateManager.Start()
}
return bot.currencyStateManager.Stop()
}
return fmt.Errorf("%s: %w", subSystemName, errSubsystemNotFound)
}

View File

@@ -96,7 +96,7 @@ func CreateTestBot(t *testing.T) *Engine {
func TestGetSubsystemsStatus(t *testing.T) {
m := (&Engine{}).GetSubsystemsStatus()
if len(m) != 14 {
if len(m) != 15 {
t.Fatalf("subsystem count is wrong expecting: %d but received: %d", 14, len(m))
}
}

View File

@@ -393,6 +393,17 @@ func (m *OrderManager) Submit(ctx context.Context, newOrder *order.Submit) (*Ord
err)
}
// Determines if current trading activity is turned off by the exchange for
// the currency pair
err = exch.CanTradePair(newOrder.Pair, newOrder.AssetType)
if err != nil {
return nil, fmt.Errorf("order manager: exchange %s cannot trade pair %s %s: %w",
newOrder.Exchange,
newOrder.Pair,
newOrder.AssetType,
err)
}
result, err := exch.SubmitOrder(ctx, newOrder)
if err != nil {
return nil, err

View File

@@ -190,6 +190,16 @@ func OrdersSetup(t *testing.T) *OrderManager {
}
exch.SetDefaults()
cfg, err := exch.GetDefaultConfig()
if err != nil {
t.Fatal(err)
}
err = exch.Setup(cfg)
if err != nil {
t.Fatal(err)
}
fakeExchange := omfExchange{
IBotExchange: exch,
}

View File

@@ -3889,3 +3889,59 @@ func (s *RPCServer) UpdateDataHistoryJobPrerequisite(_ context.Context, r *gctrp
}
return &gctrpc.GenericResponse{Status: status, Data: fmt.Sprintf("Set job '%v' prerequisite job to '%v' and set status to paused", r.Nickname, r.PrerequisiteJobNickname)}, nil
}
// CurrencyStateGetAll returns a full snapshot of currency states, whether they
// are able to be withdrawn, deposited or traded on an exchange.
func (s *RPCServer) CurrencyStateGetAll(_ context.Context, r *gctrpc.CurrencyStateGetAllRequest) (*gctrpc.CurrencyStateResponse, error) {
return s.currencyStateManager.GetAllRPC(r.Exchange)
}
// CurrencyStateWithdraw determines via RPC if the currency code is operational for
// withdrawal from an exchange
func (s *RPCServer) CurrencyStateWithdraw(_ context.Context, r *gctrpc.CurrencyStateWithdrawRequest) (*gctrpc.GenericResponse, error) {
return s.currencyStateManager.CanWithdrawRPC(r.Exchange,
currency.NewCode(r.Code),
asset.Item(r.Asset))
}
// CurrencyStateDeposit determines via RPC if the currency code is operational for
// depositing to an exchange
func (s *RPCServer) CurrencyStateDeposit(_ context.Context, r *gctrpc.CurrencyStateDepositRequest) (*gctrpc.GenericResponse, error) {
return s.currencyStateManager.CanDepositRPC(r.Exchange,
currency.NewCode(r.Code),
asset.Item(r.Asset))
}
// CurrencyStateTrading determines via RPC if the currency code is operational for trading
func (s *RPCServer) CurrencyStateTrading(_ context.Context, r *gctrpc.CurrencyStateTradingRequest) (*gctrpc.GenericResponse, error) {
return s.currencyStateManager.CanTradeRPC(r.Exchange,
currency.NewCode(r.Code),
asset.Item(r.Asset))
}
// CurrencyStateTradingPair determines via RPC if the pair is operational for trading
func (s *RPCServer) CurrencyStateTradingPair(_ context.Context, r *gctrpc.CurrencyStateTradingPairRequest) (*gctrpc.GenericResponse, error) {
exch, err := s.GetExchangeByName(r.Exchange)
if err != nil {
return nil, err
}
cp, err := currency.NewPairFromString(r.Pair)
if err != nil {
return nil, err
}
a := asset.Item(r.Asset)
err = checkParams(r.Exchange, exch, a, cp)
if err != nil {
return nil, err
}
err = exch.CanTradePair(cp, a)
if err != nil {
return nil, err
}
return s.currencyStateManager.CanTradePairRPC(r.Exchange,
cp,
asset.Item(r.Asset))
}

View File

@@ -28,6 +28,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/binance"
"github.com/thrasher-corp/gocryptotrader/exchanges/currencystate"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
@@ -43,6 +44,7 @@ const (
unexpectedLackOfError = "unexpected lack of error"
migrationsFolder = "migrations"
databaseFolder = "database"
fakeExchangeName = "fake"
)
// fExchange is a fake exchange with function overrides
@@ -53,7 +55,7 @@ type fExchange struct {
func (f fExchange) GetHistoricCandles(ctx context.Context, p currency.Pair, a asset.Item, timeStart, _ time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{
Exchange: "fake",
Exchange: fakeExchangeName,
Pair: p,
Asset: a,
Interval: interval,
@@ -72,7 +74,7 @@ func (f fExchange) GetHistoricCandles(ctx context.Context, p currency.Pair, a as
func (f fExchange) GetHistoricCandlesExtended(ctx context.Context, p currency.Pair, a asset.Item, timeStart, _ time.Time, interval kline.Interval) (kline.Item, error) {
return kline.Item{
Exchange: "fake",
Exchange: fakeExchangeName,
Pair: p,
Asset: a,
Interval: interval,
@@ -122,6 +124,36 @@ func (f fExchange) UpdateAccountInfo(ctx context.Context, a asset.Item) (account
}, nil
}
// GetCurrencyStateSnapshot overrides interface function
func (f fExchange) GetCurrencyStateSnapshot() ([]currencystate.Snapshot, error) {
return []currencystate.Snapshot{
{
Code: currency.BTC,
Asset: asset.Spot,
},
}, nil
}
// CanTradePair overrides interface function
func (f fExchange) CanTradePair(p currency.Pair, a asset.Item) error {
return nil
}
// CanTrade overrides interface function
func (f fExchange) CanTrade(c currency.Code, a asset.Item) error {
return nil
}
// CanWithdraw overrides interface function
func (f fExchange) CanWithdraw(c currency.Code, a asset.Item) error {
return nil
}
// CanDeposit overrides interface function
func (f fExchange) CanDeposit(c currency.Code, a asset.Item) error {
return nil
}
// Sets up everything required to run any function inside rpcserver
// Only use if you require a database, this makes tests slow
func RPCTestSetup(t *testing.T) *Engine {
@@ -238,7 +270,7 @@ func TestGetSavedTrades(t *testing.T) {
t.Error(err)
}
_, err = s.GetSavedTrades(context.Background(), &gctrpc.GetSavedTradesRequest{
Exchange: "fake",
Exchange: fakeExchangeName,
Pair: &gctrpc.CurrencyPair{
Delimiter: currency.DashDelimiter,
Base: currency.BTC.String(),
@@ -849,7 +881,7 @@ func TestGetRecentTrades(t *testing.T) {
t.Error(err)
}
_, err = s.GetRecentTrades(context.Background(), &gctrpc.GetSavedTradesRequest{
Exchange: "fake",
Exchange: fakeExchangeName,
Pair: &gctrpc.CurrencyPair{
Delimiter: currency.DashDelimiter,
Base: currency.BTC.String(),
@@ -897,7 +929,7 @@ func TestGetHistoricTrades(t *testing.T) {
t.Error(err)
}
err = s.GetHistoricTrades(&gctrpc.GetSavedTradesRequest{
Exchange: "fake",
Exchange: fakeExchangeName,
Pair: &gctrpc.CurrencyPair{
Delimiter: currency.DashDelimiter,
Base: currency.BTC.String(),
@@ -934,7 +966,7 @@ func TestGetAccountInfo(t *testing.T) {
t.Fatal(err)
}
b := exch.GetBase()
b.Name = "fake"
b.Name = fakeExchangeName
b.Enabled = true
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
@@ -945,7 +977,7 @@ func TestGetAccountInfo(t *testing.T) {
}
em.Add(fakeExchange)
s := RPCServer{Engine: &Engine{ExchangeManager: em}}
_, err = s.GetAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{Exchange: "fake", AssetType: asset.Spot.String()})
_, err = s.GetAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{Exchange: fakeExchangeName, AssetType: asset.Spot.String()})
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
@@ -959,7 +991,7 @@ func TestUpdateAccountInfo(t *testing.T) {
t.Fatal(err)
}
b := exch.GetBase()
b.Name = "fake"
b.Name = fakeExchangeName
b.Enabled = true
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
@@ -971,18 +1003,18 @@ func TestUpdateAccountInfo(t *testing.T) {
em.Add(fakeExchange)
s := RPCServer{Engine: &Engine{ExchangeManager: em}}
_, err = s.GetAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{Exchange: "fake", AssetType: asset.Spot.String()})
_, err = s.GetAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{Exchange: fakeExchangeName, AssetType: asset.Spot.String()})
if !errors.Is(err, nil) {
t.Errorf("received '%v', expected '%v'", err, nil)
}
_, err = s.UpdateAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{Exchange: "fake", AssetType: asset.Futures.String()})
_, err = s.UpdateAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{Exchange: fakeExchangeName, AssetType: asset.Futures.String()})
if !errors.Is(err, errAssetTypeDisabled) {
t.Errorf("received '%v', expected '%v'", err, errAssetTypeDisabled)
}
_, err = s.UpdateAccountInfo(context.Background(), &gctrpc.GetAccountInfoRequest{
Exchange: "fake",
Exchange: fakeExchangeName,
AssetType: asset.Spot.String(),
})
if !errors.Is(err, nil) {
@@ -1862,3 +1894,87 @@ func TestUpdateDataHistoryJobPrerequisite(t *testing.T) {
t.Errorf("received %v, expected %v", err, nil)
}
}
func TestCurrencyStateGetAll(t *testing.T) {
t.Parallel()
_, err := (&RPCServer{Engine: &Engine{}}).CurrencyStateGetAll(context.Background(),
&gctrpc.CurrencyStateGetAllRequest{Exchange: fakeExchangeName})
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Errorf("received %v, expected %v", err, ErrSubSystemNotStarted)
}
}
func TestCurrencyStateWithdraw(t *testing.T) {
t.Parallel()
_, err := (&RPCServer{
Engine: &Engine{},
}).CurrencyStateWithdraw(context.Background(),
&gctrpc.CurrencyStateWithdrawRequest{
Exchange: "wow"})
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Fatalf("received: %v, but expected: %v", err, ErrSubSystemNotStarted)
}
}
func TestCurrencyStateDeposit(t *testing.T) {
t.Parallel()
_, err := (&RPCServer{
Engine: &Engine{},
}).CurrencyStateDeposit(context.Background(),
&gctrpc.CurrencyStateDepositRequest{Exchange: "wow"})
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Fatalf("received: %v, but expected: %v", err, ErrSubSystemNotStarted)
}
}
func TestCurrencyStateTrading(t *testing.T) {
t.Parallel()
_, err := (&RPCServer{
Engine: &Engine{},
}).CurrencyStateTrading(context.Background(),
&gctrpc.CurrencyStateTradingRequest{Exchange: "wow"})
if !errors.Is(err, ErrSubSystemNotStarted) {
t.Fatalf("received: %v, but expected: %v", err, ErrSubSystemNotStarted)
}
}
func TestCurrencyStateTradingPair(t *testing.T) {
t.Parallel()
em := SetupExchangeManager()
exch, err := em.NewExchangeByName(testExchange)
if err != nil {
t.Fatal(err)
}
b := exch.GetBase()
b.Name = fakeExchangeName
b.Enabled = true
cp, err := currency.NewPairFromString("btc-usd")
if err != nil {
t.Fatal(err)
}
b.CurrencyPairs.Pairs = make(map[asset.Item]*currency.PairStore)
b.CurrencyPairs.Pairs[asset.Spot] = &currency.PairStore{
AssetEnabled: convert.BoolPtr(true),
ConfigFormat: &currency.PairFormat{},
Available: currency.Pairs{cp},
Enabled: currency.Pairs{cp},
}
fakeExchange := fExchange{
IBotExchange: exch,
}
em.Add(fakeExchange)
s := RPCServer{Engine: &Engine{ExchangeManager: em,
currencyStateManager: &CurrencyStateManager{started: 1, iExchangeManager: em}}}
_, err = s.CurrencyStateTradingPair(context.Background(),
&gctrpc.CurrencyStateTradingPairRequest{
Exchange: fakeExchangeName,
Pair: "btc-usd",
Asset: "spot",
})
if !errors.Is(err, nil) {
t.Fatalf("received: %v, but expected: %v", err, nil)
}
}

View File

@@ -7,6 +7,8 @@ import (
"time"
dbwithdraw "github.com/thrasher-corp/gocryptotrader/database/repository/withdraw"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/currencystate"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
@@ -45,6 +47,12 @@ func (m *WithdrawManager) SubmitWithdrawal(ctx context.Context, req *withdraw.Re
RequestDetails: *req,
}
// Determines if the currency can be withdrawn from the exchange
errF := exch.CanWithdraw(req.Currency, asset.Spot)
if errF != nil && !errors.Is(errF, currencystate.ErrCurrencyStateNotFound) { // Suppress not found error
return nil, errF
}
if m.isDryRun {
log.Warnln(log.Global, "Dry run enabled, no withdrawal request will be submitted or have an event created")
resp.ID = withdraw.DryRunID

View File

@@ -25,6 +25,14 @@ func withdrawManagerTestHelper(t *testing.T) (*ExchangeManager, *portfolioManage
em := SetupExchangeManager()
b := new(binance.Binance)
b.SetDefaults()
cfg, err := b.GetDefaultConfig()
if err != nil {
t.Fatal(err)
}
err = b.Setup(cfg)
if err != nil {
t.Fatal(err)
}
em.Add(b)
pm, err := setupPortfolioManager(em, 0, &portfolio.Base{Addresses: []portfolio.Address{}})
if err != nil {