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,312 @@
package currencystate
import (
"errors"
"fmt"
"sync"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/alert"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
var (
errEmptyCurrency = errors.New("empty currency")
errUpdatesAreNil = errors.New("updates are nil")
errNilStates = errors.New("states is not started or set up")
// Specific operational errors
errDepositNotAllowed = errors.New("depositing not allowed")
errWithdrawalsNotAllowed = errors.New("withdrawals not allowed")
errTradingNotAllowed = errors.New("trading not allowed")
// ErrCurrencyStateNotFound is an error when the currency state has not been
// found
ErrCurrencyStateNotFound = errors.New("currency state not found")
)
// NewCurrencyStates gets a new type for tracking exchange currency states
func NewCurrencyStates() *States {
return &States{m: make(map[asset.Item]map[*currency.Item]*Currency)}
}
// States defines all currency states for an exchange
type States struct {
m map[asset.Item]map[*currency.Item]*Currency
mtx sync.RWMutex
}
// GetCurrencyStateSnapshot returns the exchange currency state snapshot
func (s *States) GetCurrencyStateSnapshot() ([]Snapshot, error) {
if s == nil {
return nil, errNilStates
}
s.mtx.RLock()
defer s.mtx.RUnlock()
var sh []Snapshot
for a, m1 := range s.m {
for c, val := range m1 {
sh = append(sh, Snapshot{
Code: currency.Code{Item: c},
Asset: a,
Options: val.GetState(),
})
}
}
return sh, nil
}
// CanTradePair returns if the currency pair is currently tradeable for this
// exchange. If there are no states loaded for a specific currency, this will
// assume the currency pair is operational. NOTE: Future exchanges will have
// functionality specific to a currency.Pair, can upgrade this when needed.
func (s *States) CanTradePair(pair currency.Pair, a asset.Item) error {
err := s.CanTrade(pair.Base, a)
if err != nil && err != ErrCurrencyStateNotFound {
return fmt.Errorf("cannot trade base currency %s %s: %w",
pair.Base, a, err)
}
err = s.CanTrade(pair.Quote, a)
if err != nil && err != ErrCurrencyStateNotFound {
return fmt.Errorf("cannot trade quote currency %s %s: %w",
pair.Base, a, err)
}
return nil
}
// CanTrade returns if the currency is currently tradeable for this exchange
func (s *States) CanTrade(c currency.Code, a asset.Item) error {
if s == nil {
return errNilStates
}
p, err := s.Get(c, a)
if err != nil {
return err
}
if !p.CanTrade() {
return errTradingNotAllowed
}
return nil
}
// CanWithdraw returns if the currency can be withdrawn from this exchange
func (s *States) CanWithdraw(c currency.Code, a asset.Item) error {
if s == nil {
return errNilStates
}
p, err := s.Get(c, a)
if err != nil {
return err
}
if !p.CanWithdraw() {
return errWithdrawalsNotAllowed
}
return nil
}
// CanDeposit returns if the currency can be deposited onto this exchange
func (s *States) CanDeposit(c currency.Code, a asset.Item) error {
if s == nil {
return errNilStates
}
p, err := s.Get(c, a)
if err != nil {
return err
}
if !p.CanDeposit() {
return errDepositNotAllowed
}
return nil
}
// UpdateAll updates the full currency state, used for REST calls
func (s *States) UpdateAll(a asset.Item, updates map[currency.Code]Options) error {
if s == nil {
return errNilStates
}
if !a.IsValid() {
return fmt.Errorf("%s %w", a, asset.ErrNotSupported)
}
if updates == nil {
return errUpdatesAreNil
}
s.mtx.Lock()
for code, option := range updates {
s.update(code, a, option)
}
s.mtx.Unlock()
return nil
}
// Update updates a singular currency state, primarily used for singular
// websocket updates or alerts.
func (s *States) Update(c currency.Code, a asset.Item, o Options) error {
if s == nil {
return errNilStates
}
if c.String() == "" {
return errEmptyCurrency
}
if !a.IsValid() {
return fmt.Errorf("%s, %w", a, asset.ErrNotSupported)
}
s.mtx.Lock()
s.update(c, a, o)
s.mtx.Unlock()
return nil
}
// update updates a singular currency state without protection
func (s *States) update(c currency.Code, a asset.Item, o Options) {
m1, ok := s.m[a]
if !ok {
m1 = make(map[*currency.Item]*Currency)
s.m[a] = m1
}
p, ok := m1[c.Item]
if !ok {
p = &Currency{}
m1[c.Item] = p
}
p.update(o)
}
// Get returns the currency state by currency code
func (s *States) Get(c currency.Code, a asset.Item) (*Currency, error) {
if s == nil {
return nil, errNilStates
}
if c.String() == "" {
return nil, errEmptyCurrency
}
if !a.IsValid() {
return nil, fmt.Errorf("%s %w", a, asset.ErrNotSupported)
}
s.mtx.RLock()
defer s.mtx.RUnlock()
cs, ok := s.m[a][c.Item]
if !ok {
return nil, ErrCurrencyStateNotFound
}
return cs, nil
}
// Currency defines the state of currency operations
type Currency struct {
withdrawals bool
withdrawAlerts alert.Notice
deposits bool
depositAlerts alert.Notice
trading bool
tradingAlerts alert.Notice
mtx sync.RWMutex
}
// update updates the underlying values
func (c *Currency) update(o Options) {
c.mtx.Lock()
if o.Withdraw == nil {
c.withdrawals = true
c.withdrawAlerts.Alert()
} else if c.withdrawals != *o.Withdraw {
c.withdrawals = *o.Withdraw
c.withdrawAlerts.Alert()
}
if o.Deposit == nil {
c.deposits = true
c.depositAlerts.Alert()
} else if c.deposits != *o.Deposit {
c.deposits = *o.Deposit
c.depositAlerts.Alert()
}
if o.Trade == nil {
c.trading = true
c.tradingAlerts.Alert()
} else if c.trading != *o.Trade {
c.trading = *o.Trade
c.tradingAlerts.Alert()
}
c.mtx.Unlock()
}
// CanTrade returns if the currency is currently tradeable
func (c *Currency) CanTrade() bool {
c.mtx.RLock()
defer c.mtx.RUnlock()
return c.trading
}
// CanWithdraw returns if the currency can be withdrawn from the exchange
func (c *Currency) CanWithdraw() bool {
c.mtx.RLock()
defer c.mtx.RUnlock()
return c.withdrawals
}
// CanDeposit returns if the currency can be deposited onto an exchange
func (c *Currency) CanDeposit() bool {
c.mtx.RLock()
defer c.mtx.RUnlock()
return c.deposits
}
// WaitTrading allows a routine to wait until a trading change of state occurs
func (c *Currency) WaitTrading(kick <-chan struct{}) <-chan bool {
c.mtx.RLock()
defer c.mtx.RUnlock()
return c.tradingAlerts.Wait(kick)
}
// WaitDeposit allows a routine to wait until a deposit change of state occurs
func (c *Currency) WaitDeposit(kick <-chan struct{}) <-chan bool {
c.mtx.RLock()
defer c.mtx.RUnlock()
return c.depositAlerts.Wait(kick)
}
// WaitWithdraw allows a routine to wait until a withdraw change of state occurs
func (c *Currency) WaitWithdraw(kick <-chan struct{}) <-chan bool {
c.mtx.RLock()
defer c.mtx.RUnlock()
return c.withdrawAlerts.Wait(kick)
}
// GetState returns the internal state of the currency
func (c *Currency) GetState() Options {
c.mtx.RLock()
defer c.mtx.RUnlock()
return Options{
Withdraw: convert.BoolPtr(c.withdrawals),
Deposit: convert.BoolPtr(c.deposits),
Trade: convert.BoolPtr(c.trading),
}
}
// Options defines the current allowable options for a currency, using a bool
// pointer for optional setting for incomplete data, so we can default to true
// on nil values.
type Options struct {
Withdraw *bool
Deposit *bool
Trade *bool
}
// Snapshot defines a snapshot of the internal asset for exportation
type Snapshot struct {
Code currency.Code
Asset asset.Item
Options
}

View File

@@ -0,0 +1,324 @@
package currencystate
import (
"errors"
"sync"
"testing"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
func TestNewCurrencyStates(t *testing.T) {
if NewCurrencyStates() == nil {
t.Fatal("unexpected value")
}
}
func TestGetSnapshot(t *testing.T) {
t.Parallel()
_, err := (*States)(nil).GetCurrencyStateSnapshot()
if !errors.Is(err, errNilStates) {
t.Fatalf("received: %v, but expected: %v", err, errNilStates)
}
o, err := (&States{
m: map[asset.Item]map[*currency.Item]*Currency{
asset.Spot: {currency.BTC.Item: {
withdrawals: true,
deposits: true,
trading: true,
}},
},
}).GetCurrencyStateSnapshot()
if !errors.Is(err, nil) {
t.Fatalf("received: %v, but expected: %v", err, nil)
}
if o == nil {
t.Fatal("unexpected value")
}
}
func TestCanTradePair(t *testing.T) {
t.Parallel()
err := (*States)(nil).CanTradePair(currency.Pair{}, "")
if !errors.Is(err, errNilStates) {
t.Fatalf("received: %v, but expected: %v", err, errNilStates)
}
err = (&States{}).CanTradePair(currency.Pair{}, "")
if !errors.Is(err, errEmptyCurrency) {
t.Fatalf("received: %v, but expected: %v", err, errEmptyCurrency)
}
cp := currency.NewPair(currency.BTC, currency.USD)
err = (&States{}).CanTradePair(cp, "")
if !errors.Is(err, asset.ErrNotSupported) {
t.Fatalf("received: %v, but expected: %v", err, asset.ErrNotSupported)
}
err = (&States{}).CanTradePair(cp, asset.Spot)
if !errors.Is(err, nil) { // not found but default to operational
t.Fatalf("received: %v, but expected: %v", err, nil)
}
err = (&States{
m: map[asset.Item]map[*currency.Item]*Currency{
asset.Spot: {
currency.BTC.Item: {trading: true},
currency.USD.Item: {trading: true},
},
},
}).CanTradePair(cp, asset.Spot)
if !errors.Is(err, nil) {
t.Fatalf("received: %v, but expected: %v", err, nil)
}
err = (&States{
m: map[asset.Item]map[*currency.Item]*Currency{
asset.Spot: {
currency.BTC.Item: {trading: false},
currency.USD.Item: {trading: true},
},
},
}).CanTradePair(cp, asset.Spot)
if !errors.Is(err, errTradingNotAllowed) {
t.Fatalf("received: %v, but expected: %v", err, errTradingNotAllowed)
}
err = (&States{
m: map[asset.Item]map[*currency.Item]*Currency{
asset.Spot: {
currency.BTC.Item: {trading: true},
currency.USD.Item: {trading: false},
},
},
}).CanTradePair(cp, asset.Spot)
if !errors.Is(err, errTradingNotAllowed) {
t.Fatalf("received: %v, but expected: %v", err, errTradingNotAllowed)
}
err = (&States{
m: map[asset.Item]map[*currency.Item]*Currency{
asset.Spot: {
currency.BTC.Item: {trading: false},
currency.USD.Item: {trading: false},
},
},
}).CanTradePair(cp, asset.Spot)
if !errors.Is(err, errTradingNotAllowed) {
t.Fatalf("received: %v, but expected: %v", err, errTradingNotAllowed)
}
}
func TestStatesCanTrade(t *testing.T) {
t.Parallel()
err := (*States)(nil).CanTrade(currency.Code{}, "")
if !errors.Is(err, errNilStates) {
t.Fatalf("received: %v, but expected: %v", err, errNilStates)
}
err = (&States{}).CanTrade(currency.Code{}, "")
if !errors.Is(err, errEmptyCurrency) {
t.Fatalf("received: %v, but expected: %v", err, errEmptyCurrency)
}
}
func TestStatesCanWithdraw(t *testing.T) {
t.Parallel()
err := (*States)(nil).CanWithdraw(currency.Code{}, "")
if !errors.Is(err, errNilStates) {
t.Fatalf("received: %v, but expected: %v", err, errNilStates)
}
err = (&States{}).CanWithdraw(currency.Code{}, "")
if !errors.Is(err, errEmptyCurrency) {
t.Fatalf("received: %v, but expected: %v", err, errEmptyCurrency)
}
err = (&States{
m: map[asset.Item]map[*currency.Item]*Currency{
asset.Spot: {
currency.BTC.Item: {withdrawals: true},
},
},
}).CanWithdraw(currency.BTC, asset.Spot)
if !errors.Is(err, nil) {
t.Fatalf("received: %v, but expected: %v", err, nil)
}
err = (&States{
m: map[asset.Item]map[*currency.Item]*Currency{
asset.Spot: {
currency.BTC.Item: {},
},
},
}).CanWithdraw(currency.BTC, asset.Spot)
if !errors.Is(err, errWithdrawalsNotAllowed) {
t.Fatalf("received: %v, but expected: %v", err, errWithdrawalsNotAllowed)
}
}
func TestStatesCanDeposit(t *testing.T) {
t.Parallel()
err := (*States)(nil).CanDeposit(currency.Code{}, "")
if !errors.Is(err, errNilStates) {
t.Fatalf("received: %v, but expected: %v", err, errNilStates)
}
err = (&States{}).CanDeposit(currency.Code{}, "")
if !errors.Is(err, errEmptyCurrency) {
t.Fatalf("received: %v, but expected: %v", err, errEmptyCurrency)
}
err = (&States{
m: map[asset.Item]map[*currency.Item]*Currency{
asset.Spot: {
currency.BTC.Item: {deposits: true},
},
},
}).CanDeposit(currency.BTC, asset.Spot)
if !errors.Is(err, nil) {
t.Fatalf("received: %v, but expected: %v", err, nil)
}
err = (&States{
m: map[asset.Item]map[*currency.Item]*Currency{
asset.Spot: {
currency.BTC.Item: {},
},
},
}).CanDeposit(currency.BTC, asset.Spot)
if !errors.Is(err, errDepositNotAllowed) {
t.Fatalf("received: %v, but expected: %v", err, errDepositNotAllowed)
}
}
func TestStatesUpdateAll(t *testing.T) {
t.Parallel()
err := (*States)(nil).UpdateAll("", nil)
if !errors.Is(err, errNilStates) {
t.Fatalf("received: %v, but expected: %v", err, errNilStates)
}
err = (&States{}).UpdateAll("", nil)
if !errors.Is(err, asset.ErrNotSupported) {
t.Fatalf("received: %v, but expected: %v", err, asset.ErrNotSupported)
}
err = (&States{}).UpdateAll(asset.Spot, nil)
if !errors.Is(err, errUpdatesAreNil) {
t.Fatalf("received: %v, but expected: %v", err, errUpdatesAreNil)
}
s := &States{
m: map[asset.Item]map[*currency.Item]*Currency{},
}
err = s.UpdateAll(asset.Spot, map[currency.Code]Options{
currency.BTC: {
Withdraw: convert.BoolPtr(true),
Trade: convert.BoolPtr(true),
Deposit: convert.BoolPtr(true)},
})
if !errors.Is(err, nil) {
t.Fatalf("received: %v, but expected: %v", err, nil)
}
err = s.UpdateAll(asset.Spot, map[currency.Code]Options{currency.BTC: {
Withdraw: convert.BoolPtr(false),
Deposit: convert.BoolPtr(false),
Trade: convert.BoolPtr(false),
}})
if !errors.Is(err, nil) {
t.Fatalf("received: %v, but expected: %v", err, nil)
}
c, err := s.Get(currency.BTC, asset.Spot)
if !errors.Is(err, nil) {
t.Fatalf("received: %v, but expected: %v", err, nil)
}
if c.CanDeposit() || c.CanTrade() || c.CanWithdraw() {
t.Fatal()
}
}
func TestStatesUpdate(t *testing.T) {
t.Parallel()
err := (*States)(nil).Update(currency.Code{}, "", Options{})
if !errors.Is(err, errNilStates) {
t.Fatalf("received: %v, but expected: %v", err, errNilStates)
}
err = (&States{}).Update(currency.Code{}, "", Options{})
if !errors.Is(err, errEmptyCurrency) {
t.Fatalf("received: %v, but expected: %v", err, errEmptyCurrency)
}
err = (&States{}).Update(currency.BTC, "", Options{})
if !errors.Is(err, asset.ErrNotSupported) {
t.Fatalf("received: %v, but expected: %v", err, asset.ErrNotSupported)
}
err = (&States{
m: map[asset.Item]map[*currency.Item]*Currency{
asset.Spot: {currency.BTC.Item: &Currency{}},
},
}).Update(currency.BTC, asset.Spot, Options{})
if !errors.Is(err, nil) {
t.Fatalf("received: %v, but expected: %v", err, nil)
}
}
func TestStatesGet(t *testing.T) {
t.Parallel()
_, err := (*States)(nil).Get(currency.Code{}, "")
if !errors.Is(err, errNilStates) {
t.Fatalf("received: %v, but expected: %v", err, errNilStates)
}
_, err = (&States{}).Get(currency.Code{}, "")
if !errors.Is(err, errEmptyCurrency) {
t.Fatalf("received: %v, but expected: %v", err, errEmptyCurrency)
}
_, err = (&States{}).Get(currency.BTC, "")
if !errors.Is(err, asset.ErrNotSupported) {
t.Fatalf("received: %v, but expected: %v", err, asset.ErrNotSupported)
}
_, err = (&States{}).Get(currency.BTC, asset.Spot)
if !errors.Is(err, ErrCurrencyStateNotFound) {
t.Fatalf("received: %v, but expected: %v", err, ErrCurrencyStateNotFound)
}
}
func TestCurrencyGetState(t *testing.T) {
o := (&Currency{}).GetState()
if *o.Deposit || *o.Trade || *o.Withdraw {
t.Fatal("unexpected values")
}
}
func TestAlerting(t *testing.T) {
c := Currency{}
var start, finish sync.WaitGroup
start.Add(3)
finish.Add(3)
go waitForAlert(c.WaitTrading(nil), &start, &finish)
go waitForAlert(c.WaitDeposit(nil), &start, &finish)
go waitForAlert(c.WaitWithdraw(nil), &start, &finish)
start.Wait()
c.update(Options{
Trade: convert.BoolPtr(true),
Withdraw: convert.BoolPtr(true),
Deposit: convert.BoolPtr(true)})
finish.Wait()
}
func waitForAlert(ch <-chan bool, start, finish *sync.WaitGroup) {
defer finish.Done()
start.Done()
<-ch
}