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

@@ -29,6 +29,11 @@ implementation
+ A guide on implementing API support for a new exchange can be found [here](../docs/ADD_NEW_EXCHANGE.md)
## websocket notes
+ If contributing websocket improvements, please make sure order reports
follow [these rules](../docs/WS_ORDER_EVENTS.md).
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution
@@ -42,9 +47,6 @@ When submitting a PR, please abide by our coding 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.
If contributing websocket improvements, please make sure order reports
follow [these rules](../docs/WS_ORDER_EVENTS.md).
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">

83
exchanges/alert/alert.go Normal file
View File

@@ -0,0 +1,83 @@
package alert
import (
"sync"
"sync/atomic"
)
// Notice defines fields required to alert sub-systems of a change of state so a
// routine can re-check in memory data
type Notice struct {
// Channel to wait for an alert on.
forAlert chan struct{}
// Lets the updater functions know if there are any routines waiting for an
// alert.
sema uint32
// After closing the forAlert channel this will notify when all the routines
// that have waited, have completed their checks.
wg sync.WaitGroup
// Segregated lock only for waiting routines, so as this does not interfere
// with the main calling lock, this acts as a rolling gate.
m sync.Mutex
}
// Alert establishes a state change on the required struct.
func (n *Notice) Alert() {
// CompareAndSwap is used to swap from 1 -> 2 so we don't keep actuating
// the opposing compare and swap in method wait. This function can return
// freely when an alert operation is in process.
if !atomic.CompareAndSwapUint32(&n.sema, 1, 2) {
// Return if no waiting routines or currently alerting.
return
}
go n.actuate()
}
// Actuate lock in a different routine, as alerting is a second order priority
// compared to updating and releasing calling routine.
func (n *Notice) actuate() {
n.m.Lock()
// Closing; alerts many waiting routines.
close(n.forAlert)
// Wait for waiting routines to receive alert and return.
n.wg.Wait()
atomic.SwapUint32(&n.sema, 0) // Swap back to neutral state.
n.m.Unlock()
}
// Wait pauses calling routine until change of state has been established via
// notice method Alert. Kick allows for cancellation of waiting or when the
// caller has been shut down, if this is not needed it can be set to nil. This
// returns a channel so strategies can cleanly wait on a select statement case.
func (n *Notice) Wait(kick <-chan struct{}) <-chan bool {
reply := make(chan bool)
n.m.Lock()
n.wg.Add(1)
if atomic.CompareAndSwapUint32(&n.sema, 0, 1) {
n.forAlert = make(chan struct{})
}
go n.hold(reply, kick)
n.m.Unlock()
return reply
}
// hold waits on either channel in the event that the routine has
// finished/cancelled or an alert from an update has occurred.
func (n *Notice) hold(ch chan<- bool, kick <-chan struct{}) {
select {
// In a select statement, if by chance there is no receiver or its late,
// we can still close and return, limiting dead-lock potential.
case <-n.forAlert: // Main waiting channel from alert
select {
case ch <- false:
default:
}
case <-kick: // This can be nil.
select {
case ch <- true:
default:
}
}
n.wg.Done()
close(ch)
}

View File

@@ -0,0 +1,92 @@
package alert
import (
"log"
"sync"
"testing"
"time"
)
func TestWait(t *testing.T) {
wait := Notice{}
var wg sync.WaitGroup
// standard alert
wg.Add(100)
for x := 0; x < 100; x++ {
go func() {
w := wait.Wait(nil)
wg.Done()
if <-w {
log.Fatal("incorrect routine wait response for alert expecting false")
}
wg.Done()
}()
}
wg.Wait()
wg.Add(100)
isLeaky(&wait, nil, t)
wait.Alert()
wg.Wait()
isLeaky(&wait, nil, t)
// use kick
ch := make(chan struct{})
wg.Add(100)
for x := 0; x < 100; x++ {
go func() {
w := wait.Wait(ch)
wg.Done()
if !<-w {
log.Fatal("incorrect routine wait response for kick expecting true")
}
wg.Done()
}()
}
wg.Wait()
wg.Add(100)
isLeaky(&wait, ch, t)
close(ch)
wg.Wait()
ch = make(chan struct{})
isLeaky(&wait, ch, t)
// late receivers
wg.Add(100)
for x := 0; x < 100; x++ {
go func(x int) {
bb := wait.Wait(ch)
wg.Done()
if x%2 == 0 {
time.Sleep(time.Millisecond * 5)
}
b := <-bb
if b {
log.Fatal("incorrect routine wait response since we call alert below; expecting false")
}
wg.Done()
}(x)
}
wg.Wait()
wg.Add(100)
isLeaky(&wait, ch, t)
wait.Alert()
wg.Wait()
isLeaky(&wait, ch, t)
}
// isLeaky tests to see if the wait functionality is returning an abnormal
// channel that is operational when it shouldn't be.
func isLeaky(a *Notice, ch chan struct{}, t *testing.T) {
t.Helper()
check := a.Wait(ch)
time.Sleep(time.Millisecond * 5) // When we call wait a routine for hold is
// spawned, so for a test we need to add in a time for goschedular to allow
// routine to actually wait on the forAlert and kick channels
select {
case <-check:
t.Fatal("leaky waiter")
default:
}
}

View File

@@ -480,7 +480,7 @@ func (b *Bitfinex) GetDerivativeStatusInfo(ctx context.Context, keys, startTime,
if response.NextFundingEventTS, ok = result[z][8].(float64); !ok {
return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for NextFundingEventTS", b.Name, errTypeAssert)
}
if response.NextFundingAccured, ok = result[z][9].(float64); !ok {
if response.NextFundingAccrued, ok = result[z][9].(float64); !ok {
return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for NextFundingAccrued", b.Name, errTypeAssert)
}
if response.NextFundingStep, ok = result[z][10].(float64); !ok {
@@ -493,8 +493,17 @@ func (b *Bitfinex) GetDerivativeStatusInfo(ctx context.Context, keys, startTime,
return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for MarkPrice", b.Name, errTypeAssert)
}
if response.OpenInterest, ok = result[z][18].(float64); !ok {
return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for OpenInterest", b.Name, errTypeAssert)
switch t := result[z][18].(type) {
case float64:
response.OpenInterest = t
case nil:
break // OpenInterest will default to 0
default:
return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: %w for OpenInterest. Type received: %v",
b.Name,
errTypeAssert,
t,
)
}
finalResp = append(finalResp, response)
}

View File

@@ -116,7 +116,7 @@ type DerivativeDataResponse struct {
MarkPrice float64
InsuranceFundBalance float64
NextFundingEventTS float64
NextFundingAccured float64
NextFundingAccrued float64
NextFundingStep float64
CurrentFunding float64
OpenInterest float64

View File

@@ -150,6 +150,21 @@ func (b *Bithumb) GetAssetStatus(ctx context.Context, symbol string) (*Status, e
return &response, nil
}
// GetAssetStatusAll returns the withdrawal and deposit status for all symbols
func (b *Bithumb) GetAssetStatusAll(ctx context.Context) (*StatusAll, error) {
var response StatusAll
err := b.SendHTTPRequest(ctx, exchange.RestSpot, publicAssetStatus+"ALL", &response)
if err != nil {
return nil, err
}
if response.Status != noError {
return nil, errors.New(response.Message)
}
return &response, nil
}
// GetTransactionHistory returns recent transactions
//
// symbol e.g. "btc"

View File

@@ -741,3 +741,19 @@ func TestGetAssetStatus(t *testing.T) {
t.Fatalf("received: %v but expected: %v", err, nil)
}
}
func TestGetAssetStatusAll(t *testing.T) {
t.Parallel()
_, err := b.GetAssetStatusAll(context.Background())
if !errors.Is(err, nil) {
t.Fatalf("received: %v but expected: %v", err, nil)
}
}
func TestUpdateCurrencyStates(t *testing.T) {
t.Parallel()
err := b.UpdateCurrencyStates(context.Background(), asset.Spot)
if !errors.Is(err, nil) {
t.Fatalf("received: %v but expected: %v", err, nil)
}
}

View File

@@ -301,3 +301,14 @@ type Status struct {
} `json:"data"`
Message string `json:"message"`
}
// StatusAll defines the current exchange allowance to deposit or withdraw a
// currency
type StatusAll struct {
Status string `json:"status"`
Data map[string]struct {
DepositStatus int64 `json:"deposit_status"`
WithdrawalStatus int64 `json:"withdrawal_status"`
} `json:"data"`
Message string `json:"message"`
}

View File

@@ -11,11 +11,13 @@ import (
"time"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"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/currencystate"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
@@ -891,3 +893,20 @@ func (b *Bithumb) UpdateOrderExecutionLimits(ctx context.Context, _ asset.Item)
}
return b.LoadLimits(limits)
}
// UpdateCurrencyStates updates currency states for exchange
func (b *Bithumb) UpdateCurrencyStates(ctx context.Context, a asset.Item) error {
status, err := b.GetAssetStatusAll(ctx)
if err != nil {
return err
}
payload := make(map[currency.Code]currencystate.Options)
for coin, options := range status.Data {
payload[currency.NewCode(coin)] = currencystate.Options{
Withdraw: convert.BoolPtr(options.WithdrawalStatus == 1),
Deposit: convert.BoolPtr(options.DepositStatus == 1),
}
}
return b.States.UpdateAll(a, payload)
}

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
}

View File

@@ -17,6 +17,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/currencystate"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
@@ -115,8 +116,7 @@ func (b *Base) SetClientProxyAddress(addr string) error {
return nil
}
// SetFeatureDefaults sets the exchanges default feature
// support set
// SetFeatureDefaults sets the exchanges default feature support set
func (b *Base) SetFeatureDefaults() {
if b.Config.Features == nil {
s := &config.FeaturesConfig{
@@ -620,7 +620,8 @@ func (b *Base) SetupDefaults(exch *config.ExchangeConfig) error {
b.Name)
}
b.CanVerifyOrderbook = !exch.OrderbookConfig.VerificationBypass
return nil
b.States = currencystate.NewCurrencyStates()
return err
}
// AllowAuthenticatedRequest checks to see if the required fields have been set
@@ -1385,3 +1386,8 @@ func (a *AssetWebsocketSupport) IsAssetWebsocketSupported(aType asset.Item) bool
defer a.m.RUnlock()
return a.unsupported == nil || !a.unsupported[aType]
}
// UpdateCurrencyStates updates currency states
func (b *Base) UpdateCurrencyStates(ctx context.Context, a asset.Item) error {
return common.ErrNotYetImplemented
}

View File

@@ -1267,7 +1267,7 @@ func TestSetAPIKeys(t *testing.T) {
func TestSetupDefaults(t *testing.T) {
t.Parallel()
var b Base
var b = Base{Name: "awesomeTest"}
cfg := config.ExchangeConfig{
HTTPTimeout: time.Duration(-1),
API: config.APIConfig{

View File

@@ -7,6 +7,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"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/protocol"
@@ -231,6 +232,7 @@ type Base struct {
order.ExecutionLimits
AssetWebsocketSupport
*currencystate.States
}
// url lookup consts

View File

@@ -9,6 +9,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"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/orderbook"
@@ -90,4 +91,16 @@ type IBotExchange interface {
GetOrderExecutionLimits(a asset.Item, cp currency.Pair) (*order.Limits, error)
CheckOrderExecutionLimits(a asset.Item, cp currency.Pair, price, amount float64, orderType order.Type) error
UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error
CurrencyStateManagement
}
// CurrencyStateManagement defines functionality for currency state management
type CurrencyStateManagement interface {
GetCurrencyStateSnapshot() ([]currencystate.Snapshot, error)
UpdateCurrencyStates(ctx context.Context, a asset.Item) error
CanTradePair(p currency.Pair, a asset.Item) error
CanTrade(c currency.Code, a asset.Item) error
CanWithdraw(c currency.Code, a asset.Item) error
CanDeposit(c currency.Code, a asset.Item) error
}

View File

@@ -2,11 +2,11 @@ package orderbook
import (
"sync"
"sync/atomic"
"time"
"github.com/gofrs/uuid"
"github.com/thrasher-corp/gocryptotrader/dispatch"
"github.com/thrasher-corp/gocryptotrader/exchanges/alert"
"github.com/thrasher-corp/gocryptotrader/log"
)
@@ -18,7 +18,7 @@ type Depth struct {
// unexported stack of nodes
stack *stack
Alert
alert.Notice
mux *dispatch.Mux
id uuid.UUID
@@ -101,7 +101,7 @@ func (d *Depth) LoadSnapshot(bids, asks []Item, lastUpdateID int64, lastUpdated
d.restSnapshot = updateByREST
d.bids.load(bids, d.stack)
d.asks.load(asks, d.stack)
d.alert()
d.Alert()
d.m.Unlock()
}
@@ -112,7 +112,7 @@ func (d *Depth) Flush() {
d.lastUpdated = time.Time{}
d.bids.load(nil, d.stack)
d.asks.load(nil, d.stack)
d.alert()
d.Alert()
d.m.Unlock()
}
@@ -132,7 +132,7 @@ func (d *Depth) UpdateBidAskByPrice(bidUpdts, askUpdts Items, maxDepth int, last
if len(askUpdts) != 0 {
d.asks.updateInsertByPrice(askUpdts, d.stack, maxDepth, tn)
}
d.alert()
d.Alert()
d.m.Unlock()
}
@@ -157,7 +157,7 @@ func (d *Depth) UpdateBidAskByID(bidUpdts, askUpdts Items, lastUpdateID int64, l
}
d.lastUpdateID = lastUpdateID
d.lastUpdated = lastUpdated
d.alert()
d.Alert()
return nil
}
@@ -182,7 +182,7 @@ func (d *Depth) DeleteBidAskByID(bidUpdts, askUpdts Items, bypassErr bool, lastU
}
d.lastUpdateID = lastUpdateID
d.lastUpdated = lastUpdated
d.alert()
d.Alert()
return nil
}
@@ -207,7 +207,7 @@ func (d *Depth) InsertBidAskByID(bidUpdts, askUpdts Items, lastUpdateID int64, l
}
d.lastUpdateID = lastUpdateID
d.lastUpdated = lastUpdated
d.alert()
d.Alert()
return nil
}
@@ -230,7 +230,7 @@ func (d *Depth) UpdateInsertByID(bidUpdts, askUpdts Items, lastUpdateID int64, l
return err
}
}
d.alert()
d.Alert()
d.lastUpdateID = lastUpdateID
d.lastUpdated = lastUpdated
return nil
@@ -281,79 +281,3 @@ func (d *Depth) IsFundingRate() bool {
defer d.m.Unlock()
return d.isFundingRate
}
// Alert defines fields required to alert sub-systems of a change of state to
// re-check depth list
type Alert struct {
// Channel to wait for an alert on.
forAlert chan struct{}
// Lets the updater functions know if there are any routines waiting for an
// alert.
sema uint32
// After closing the forAlert channel this will notify when all the routines
// that have waited, have either checked the orderbook depth or finished.
wg sync.WaitGroup
// Segregated lock only for waiting routines, so as this does not interfere
// with the main depth lock, acts as a rolling gate.
m sync.Mutex
}
// alert establishes a state change on the orderbook depth.
func (a *Alert) alert() {
// CompareAndSwap is used to swap from 1 -> 2 so we don't keep actuating
// the opposing compare and swap in method wait. This function can return
// freely when an alert operation is in process.
if !atomic.CompareAndSwapUint32(&a.sema, 1, 2) {
// Return if no waiting routines or currently alerting.
return
}
go func() {
// Actuate lock in a different routine, as alerting is a second order
// priority compared to updating and releasing calling routine.
a.m.Lock()
// Closing; alerts many waiting routines.
close(a.forAlert)
// Wait for waiting routines to receive alert and return.
a.wg.Wait()
atomic.SwapUint32(&a.sema, 0) // Swap back to neutral state.
a.m.Unlock()
}()
}
// Wait pauses calling routine until depth change has been established via depth
// method alert. Kick allows for cancellation of waiting or when the caller has
// has been shut down, if this is not needed it can be set to nil. This
// returns a channel so strategies can cleanly wait on a select statement case.
func (a *Alert) Wait(kick <-chan struct{}) <-chan bool {
reply := make(chan bool)
a.m.Lock()
a.wg.Add(1)
if atomic.CompareAndSwapUint32(&a.sema, 0, 1) {
a.forAlert = make(chan struct{})
}
go a.hold(reply, kick)
a.m.Unlock()
return reply
}
// hold waits on either channel in the event that the routine has finished or an
// alert from a depth update has occurred.
func (a *Alert) hold(ch chan<- bool, kick <-chan struct{}) {
select {
// In a select statement, if by chance there is no receiver or its late,
// we can still close and return, limiting dead-lock potential.
case <-a.forAlert: // Main waiting channel from alert
select {
case ch <- false:
default:
}
case <-kick: // This can be nil.
select {
case ch <- true:
default:
}
}
a.wg.Done()
close(ch)
}

View File

@@ -2,9 +2,7 @@ package orderbook
import (
"errors"
"log"
"reflect"
"sync"
"testing"
"time"
@@ -307,87 +305,3 @@ func TestPublish(t *testing.T) {
d := Depth{}
d.Publish()
}
func TestWait(t *testing.T) {
wait := Alert{}
var wg sync.WaitGroup
// standard alert
wg.Add(100)
for x := 0; x < 100; x++ {
go func() {
w := wait.Wait(nil)
wg.Done()
if <-w {
log.Fatal("incorrect routine wait response for alert expecting false")
}
wg.Done()
}()
}
wg.Wait()
wg.Add(100)
isLeaky(&wait, nil, t)
wait.alert()
wg.Wait()
isLeaky(&wait, nil, t)
// use kick
ch := make(chan struct{})
wg.Add(100)
for x := 0; x < 100; x++ {
go func() {
w := wait.Wait(ch)
wg.Done()
if !<-w {
log.Fatal("incorrect routine wait response for kick expecting true")
}
wg.Done()
}()
}
wg.Wait()
wg.Add(100)
isLeaky(&wait, ch, t)
close(ch)
wg.Wait()
ch = make(chan struct{})
isLeaky(&wait, ch, t)
// late receivers
wg.Add(100)
for x := 0; x < 100; x++ {
go func(x int) {
bb := wait.Wait(ch)
wg.Done()
if x%2 == 0 {
time.Sleep(time.Millisecond * 5)
}
b := <-bb
if b {
log.Fatal("incorrect routine wait response since we call alert below; expecting false")
}
wg.Done()
}(x)
}
wg.Wait()
wg.Add(100)
isLeaky(&wait, ch, t)
wait.alert()
wg.Wait()
isLeaky(&wait, ch, t)
}
// isLeaky tests to see if the wait functionality is returning an abnormal
// channel that is operational when it shouldn't be.
func isLeaky(a *Alert, ch chan struct{}, t *testing.T) {
t.Helper()
check := a.Wait(ch)
time.Sleep(time.Millisecond * 5) // When we call wait a routine for hold is
// spawned, so for a test we need to add in a time for goschedular to allow
// routine to actually wait on the forAlert and kick channels
select {
case <-check:
t.Fatal("leaky waiter")
default:
}
}

View File

@@ -3,6 +3,8 @@ package orderbook
import (
"sync"
"time"
"github.com/thrasher-corp/gocryptotrader/exchanges/alert"
)
// Unsafe is an exported linked list reference to the current bid/ask heads and
@@ -20,7 +22,7 @@ type Unsafe struct {
// protocol then this book is not considered live and cannot be trusted.
UpdatedViaREST *bool
LastUpdated *time.Time
*Alert
*alert.Notice
}
// Lock locks down the underlying linked list which inhibits all pending updates
@@ -55,7 +57,7 @@ func (d *Depth) GetUnsafe() Unsafe {
BidHead: &d.bids.linkedList.head,
AskHead: &d.asks.linkedList.head,
m: &d.m,
Alert: &d.Alert,
Notice: &d.Notice,
UpdatedViaREST: &d.options.restSnapshot,
LastUpdated: &d.options.lastUpdated,
}