mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
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:
@@ -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
83
exchanges/alert/alert.go
Normal 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)
|
||||
}
|
||||
92
exchanges/alert/alert_test.go
Normal file
92
exchanges/alert/alert_test.go
Normal 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:
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ type DerivativeDataResponse struct {
|
||||
MarkPrice float64
|
||||
InsuranceFundBalance float64
|
||||
NextFundingEventTS float64
|
||||
NextFundingAccured float64
|
||||
NextFundingAccrued float64
|
||||
NextFundingStep float64
|
||||
CurrentFunding float64
|
||||
OpenInterest float64
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
312
exchanges/currencystate/currency_state.go
Normal file
312
exchanges/currencystate/currency_state.go
Normal 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
|
||||
}
|
||||
324
exchanges/currencystate/currency_state_test.go
Normal file
324
exchanges/currencystate/currency_state_test.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user