mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-20 07:26:46 +00:00
orderbook/buffer: data integrity and resubscription pass (#910)
* orderbook/buffer: data integrity and resubscription pass * btcmarkets: REMOVE THAT LIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIINE!!!!!!!!!!!!!!!!! * buffer: reinstate publish, refaactor, invalidate more and comments * buffer/orderbook: improve update and snapshot performance. Move Update type to orderbook package to util. pointer through entire function calls. (cleanup). Change action string to uint8 for easier comparison. Add parsing helper. Update current test benchmark comments. * dispatch: change publish func to variadic id param * dispatch: remove sender receiver wait time as this adds overhead and complexity. update tests. * dispatch: don't create pointers for every job container * rpcserver: fix assertion issues with data publishing change * linter: fixes * glorious: nits addr * depth: change validation handling to incorporate and store err * linter: fix more issues * dispatch: fix race * travis: update before fetching * depth: wrap and return wrapped error in invalidate call and fix tests * btcmarkets: fix commenting * workflow: check * workflow: check * orderbook: check error * buffer/depth: return invalidation error and fix tests * gctcli: display errors on orderbook streams * buffer: remove unused types * orderbook/bitmex: shift function to bitmex * orderbook: Add specific comments to unexported functions that don't have locking require locking. * orderbook: restrict published data functionality to orderbook.Outbound interface * common: add assertion failure helper for error * dispatch: remove atomics, add mutex protection, remove add/remove worker, redo main tests * dispatch: export function * engine: revert and change sub logger to manager * engine: remove old test * dispatch: add common variable ;) * btcmarket: don't overflow int in tests on 32bit systems * ci: force 1.17.7 usage for go * Revert "ci: force 1.17.7 usage for go" This reverts commit af2f95563bf218cf2b9f36a9fcf3258e2c6a2d91. * golangci: bump version add and remove linter items * Revert "golangci: bump version add and remove linter items" This reverts commit 3c98bffc9d030e39faca0387ea40c151df2ab06b. * dispatch: remove unsused mutex from mux * order: slight optimizations * nits: glorious * dispatch: fix regression on uuid generation and input inline with master * linter: fix * linter: fix * glorious: nit - rm slice segration * account: fix test after merge * coinbasepro: revert change * account: close channel instead of needing a receiver, push alert in routine to prepare for waiter. Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
This commit is contained in:
3
.github/workflows/tests.yml
vendored
3
.github/workflows/tests.yml
vendored
@@ -97,6 +97,9 @@ jobs:
|
||||
key: ${{ runner.os }}-go-386-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: ${{ runner.os }}-go-386-
|
||||
|
||||
- name: Update apt-get
|
||||
run: sudo apt-get update
|
||||
|
||||
- name: Install gcc-multilib
|
||||
run: sudo apt-get install gcc-multilib
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ matrix:
|
||||
script:
|
||||
- export GOARCH=386
|
||||
- export CGO_ENABLED=1
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install gcc-multilib
|
||||
- make test
|
||||
after_success:
|
||||
|
||||
@@ -3437,8 +3437,12 @@ func getOrderbookStream(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Orderbook stream for %s %s:\n\n", exchangeName,
|
||||
resp.Pair.String())
|
||||
fmt.Printf("Orderbook stream for %s %s:\n\n", exchangeName, resp.Pair)
|
||||
if resp.Error != "" {
|
||||
fmt.Printf("%s\n", resp.Error)
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Println("\t\tBids\t\t\t\tAsks")
|
||||
fmt.Println()
|
||||
|
||||
@@ -3535,9 +3539,10 @@ func getExchangeOrderbookStream(c *cli.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Orderbook streamed for %s %s",
|
||||
exchangeName,
|
||||
resp.Pair.String())
|
||||
fmt.Printf("Orderbook streamed for %s %s", exchangeName, resp.Pair)
|
||||
if resp.Error != "" {
|
||||
fmt.Printf("%s\n", resp.Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,9 @@ var (
|
||||
errCannotSetInvalidTimeout = errors.New("cannot set new HTTP client with timeout that is equal or less than 0")
|
||||
errUserAgentInvalid = errors.New("cannot set invalid user agent")
|
||||
errHTTPClientInvalid = errors.New("custom http client cannot be nil")
|
||||
|
||||
// ErrTypeAssertFailure defines an error when type assertion fails
|
||||
ErrTypeAssertFailure = errors.New("type assert failure")
|
||||
)
|
||||
|
||||
// SetHTTPClientWithTimeout sets a new *http.Client with different timeout
|
||||
@@ -444,3 +447,9 @@ func StartEndTimeCheck(start, end time.Time) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAssertError returns additional information for when an assertion failure
|
||||
// occurs.
|
||||
func GetAssertError(required string, received interface{}) error {
|
||||
return fmt.Errorf("%w from %T to %s", ErrTypeAssertFailure, received, required)
|
||||
}
|
||||
|
||||
@@ -675,3 +675,20 @@ func TestParseStartEndDate(t *testing.T) {
|
||||
t.Errorf("received %v, expected %v", err, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAssertError(t *testing.T) {
|
||||
err := GetAssertError("*[]string", float64(0))
|
||||
if err.Error() != "type assert failure from float64 to *[]string" {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = GetAssertError("<nil>", nil)
|
||||
if err.Error() != "type assert failure from <nil> to <nil>" {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = GetAssertError("bruh", struct{}{})
|
||||
if !errors.Is(err, ErrTypeAssertFailure) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrTypeAssertFailure)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,156 +4,149 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
|
||||
// ErrNotRunning defines an error when the dispatcher is not running
|
||||
var ErrNotRunning = errors.New("dispatcher not running")
|
||||
var (
|
||||
// ErrNotRunning defines an error when the dispatcher is not running
|
||||
ErrNotRunning = errors.New("dispatcher not running")
|
||||
|
||||
errDispatcherNotInitialized = errors.New("dispatcher not initialised")
|
||||
errDispatcherAlreadyRunning = errors.New("dispatcher already running")
|
||||
errDispatchShutdown = errors.New("dispatcher did not shutdown properly, routines failed to close")
|
||||
errDispatcherUUIDNotFoundInRouteList = errors.New("dispatcher uuid not found in route list")
|
||||
errTypeAssertionFailure = errors.New("type assertion failure")
|
||||
errChannelNotFoundInUUIDRef = errors.New("dispatcher channel not found in uuid reference slice")
|
||||
errUUIDCollision = errors.New("dispatcher collision detected, uuid already exists")
|
||||
errDispatcherJobsAtLimit = errors.New("dispatcher jobs at limit")
|
||||
errChannelIsNil = errors.New("channel is nil")
|
||||
errUUIDGeneratorFunctionIsNil = errors.New("UUID generator function is nil")
|
||||
|
||||
limitMessage = "%w [%d] current worker count [%d]. Spawn more workers via --dispatchworkers=x, or increase the jobs limit via --dispatchjobslimit=x"
|
||||
)
|
||||
|
||||
// Name is an exported subsystem name
|
||||
const Name = "dispatch"
|
||||
|
||||
func init() {
|
||||
dispatcher = &Dispatcher{
|
||||
dispatcher = NewDispatcher()
|
||||
}
|
||||
|
||||
// NewDispatcher creates a new Dispatcher for relaying data.
|
||||
func NewDispatcher() *Dispatcher {
|
||||
return &Dispatcher{
|
||||
routes: make(map[uuid.UUID][]chan interface{}),
|
||||
outbound: sync.Pool{
|
||||
New: func() interface{} {
|
||||
// Create unbuffered channel for data pass
|
||||
return make(chan interface{})
|
||||
},
|
||||
New: getChan,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getChan() interface{} {
|
||||
// Create unbuffered channel for data pass
|
||||
return make(chan interface{})
|
||||
}
|
||||
|
||||
// Start starts the dispatch system by spawning workers and allocating memory
|
||||
func Start(workers, jobsLimit int) error {
|
||||
if dispatcher == nil {
|
||||
return errors.New(errNotInitialised)
|
||||
}
|
||||
|
||||
mtx.Lock()
|
||||
defer mtx.Unlock()
|
||||
return dispatcher.start(workers, jobsLimit)
|
||||
}
|
||||
|
||||
// Stop attempts to stop the dispatch service, this will close all pipe channels
|
||||
// flush job list and drop all workers
|
||||
func Stop() error {
|
||||
if dispatcher == nil {
|
||||
return errors.New(errNotInitialised)
|
||||
}
|
||||
|
||||
log.Debugln(log.DispatchMgr, "Dispatch manager shutting down...")
|
||||
|
||||
mtx.Lock()
|
||||
defer mtx.Unlock()
|
||||
return dispatcher.stop()
|
||||
}
|
||||
|
||||
// IsRunning checks to see if the dispatch service is running
|
||||
func IsRunning() bool {
|
||||
if dispatcher == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return dispatcher.isRunning()
|
||||
}
|
||||
|
||||
// DropWorker drops a worker routine
|
||||
func DropWorker() error {
|
||||
if dispatcher == nil {
|
||||
return errors.New(errNotInitialised)
|
||||
}
|
||||
|
||||
dispatcher.dropWorker()
|
||||
return nil
|
||||
}
|
||||
|
||||
// SpawnWorker starts a new worker routine
|
||||
func SpawnWorker() error {
|
||||
if dispatcher == nil {
|
||||
return errors.New(errNotInitialised)
|
||||
}
|
||||
return dispatcher.spawnWorker()
|
||||
}
|
||||
|
||||
// start compares atomic running value, sets defaults, overides with
|
||||
// configuration, then spawns workers
|
||||
func (d *Dispatcher) start(workers, channelCapacity int) error {
|
||||
if atomic.LoadUint32(&d.running) == 1 {
|
||||
return errors.New("dispatcher already running")
|
||||
if d == nil {
|
||||
return errDispatcherNotInitialized
|
||||
}
|
||||
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
|
||||
if d.running {
|
||||
return errDispatcherAlreadyRunning
|
||||
}
|
||||
|
||||
d.running = true
|
||||
|
||||
if workers < 1 {
|
||||
log.Warn(log.DispatchMgr,
|
||||
"Dispatcher: workers cannot be zero, using default values")
|
||||
log.Warnf(log.DispatchMgr,
|
||||
"workers cannot be zero, using default value %d\n",
|
||||
DefaultMaxWorkers)
|
||||
workers = DefaultMaxWorkers
|
||||
}
|
||||
if channelCapacity < 1 {
|
||||
log.Warn(log.DispatchMgr,
|
||||
"Dispatcher: jobs limit cannot be zero, using default values")
|
||||
log.Warnf(log.DispatchMgr,
|
||||
"jobs limit cannot be zero, using default values %d\n",
|
||||
DefaultJobsLimit)
|
||||
channelCapacity = DefaultJobsLimit
|
||||
}
|
||||
d.jobs = make(chan *job, channelCapacity)
|
||||
d.maxWorkers = int32(workers)
|
||||
d.shutdown = make(chan *sync.WaitGroup)
|
||||
d.jobs = make(chan job, channelCapacity)
|
||||
d.maxWorkers = workers
|
||||
d.shutdown = make(chan struct{})
|
||||
|
||||
if atomic.LoadInt32(&d.count) != 0 {
|
||||
return errors.New("dispatcher leaked workers found")
|
||||
for i := 0; i < d.maxWorkers; i++ {
|
||||
d.wg.Add(1)
|
||||
go d.relayer()
|
||||
}
|
||||
|
||||
for i := int32(0); i < d.maxWorkers; i++ {
|
||||
err := d.spawnWorker()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
atomic.SwapUint32(&d.running, 1)
|
||||
return nil
|
||||
}
|
||||
|
||||
// stop stops the service and shuts down all worker routines
|
||||
func (d *Dispatcher) stop() error {
|
||||
if !atomic.CompareAndSwapUint32(&d.running, 1, 0) {
|
||||
if d == nil {
|
||||
return errDispatcherNotInitialized
|
||||
}
|
||||
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
|
||||
if !d.running {
|
||||
return ErrNotRunning
|
||||
}
|
||||
|
||||
d.running = false
|
||||
|
||||
// Stop all jobs
|
||||
close(d.jobs)
|
||||
|
||||
// Release finished workers
|
||||
close(d.shutdown)
|
||||
ch := make(chan struct{})
|
||||
timer := time.NewTimer(1 * time.Second)
|
||||
defer func() {
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
}
|
||||
|
||||
d.rMtx.Lock()
|
||||
for key, pipes := range d.routes {
|
||||
for i := range pipes {
|
||||
// Boot off receivers waiting on pipes.
|
||||
close(pipes[i])
|
||||
}
|
||||
}()
|
||||
go func(ch chan struct{}) { d.wg.Wait(); ch <- struct{}{} }(ch)
|
||||
// Flush all pipes, re-subscription will need to occur.
|
||||
d.routes[key] = nil
|
||||
}
|
||||
d.rMtx.Unlock()
|
||||
|
||||
ch := make(chan struct{})
|
||||
timer := time.NewTimer(time.Second)
|
||||
go func(ch chan<- struct{}) { d.wg.Wait(); ch <- struct{}{} }(ch)
|
||||
select {
|
||||
case <-ch:
|
||||
// close all routes
|
||||
for key := range d.routes {
|
||||
for i := range d.routes[key] {
|
||||
close(d.routes[key][i])
|
||||
}
|
||||
|
||||
d.routes[key] = nil
|
||||
}
|
||||
|
||||
for len(d.jobs) != 0 { // drain jobs channel for old data
|
||||
<-d.jobs
|
||||
}
|
||||
|
||||
log.Debugln(log.DispatchMgr, "Dispatch manager shutdown.")
|
||||
|
||||
return nil
|
||||
case <-timer.C:
|
||||
return errors.New(errShutdownRoutines)
|
||||
return errDispatchShutdown
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,80 +155,29 @@ func (d *Dispatcher) isRunning() bool {
|
||||
if d == nil {
|
||||
return false
|
||||
}
|
||||
return atomic.LoadUint32(&d.running) == 1
|
||||
}
|
||||
|
||||
// dropWorker deallocates a worker routine
|
||||
func (d *Dispatcher) dropWorker() {
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(1)
|
||||
d.shutdown <- &wg
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// spawnWorker allocates a new worker for job processing
|
||||
func (d *Dispatcher) spawnWorker() error {
|
||||
if atomic.LoadInt32(&d.count) >= d.maxWorkers {
|
||||
return errors.New("dispatcher cannot spawn more workers; ceiling reached")
|
||||
}
|
||||
var spawnWg sync.WaitGroup
|
||||
spawnWg.Add(1)
|
||||
go d.relayer(&spawnWg)
|
||||
spawnWg.Wait()
|
||||
return nil
|
||||
d.m.RLock()
|
||||
defer d.m.RUnlock()
|
||||
return d.running
|
||||
}
|
||||
|
||||
// relayer routine relays communications across the defined routes
|
||||
func (d *Dispatcher) relayer(i *sync.WaitGroup) {
|
||||
atomic.AddInt32(&d.count, 1)
|
||||
d.wg.Add(1)
|
||||
timeout := time.NewTimer(0)
|
||||
i.Done()
|
||||
func (d *Dispatcher) relayer() {
|
||||
for {
|
||||
select {
|
||||
case j := <-d.jobs:
|
||||
d.rMtx.RLock()
|
||||
if _, ok := d.routes[j.ID]; !ok {
|
||||
d.rMtx.RUnlock()
|
||||
continue
|
||||
}
|
||||
// Channel handshake timeout feature if a channel is blocked for any
|
||||
// period of time due to an issue with the receiving routine.
|
||||
// This will wait on channel then fall over to the next route when
|
||||
// the timer actuates and continue over the route list. Have to
|
||||
// iterate across full length of routes so every routine can get
|
||||
// their new info, cannot be buffered as we dont want to have an old
|
||||
// orderbook etc contained in a buffered channel when a routine
|
||||
// actually is ready for a receive.
|
||||
// TODO: Need to consider optimal timer length
|
||||
for i := range d.routes[j.ID] {
|
||||
if !timeout.Stop() { // Stop timer before reset
|
||||
// Drain channel if timer has already actuated
|
||||
if pipes, ok := d.routes[j.ID]; ok {
|
||||
for i := range pipes {
|
||||
select {
|
||||
case <-timeout.C:
|
||||
case pipes[i] <- j.Data:
|
||||
default:
|
||||
// no receiver; don't wait. This limits complexity.
|
||||
}
|
||||
}
|
||||
|
||||
timeout.Reset(DefaultHandshakeTimeout)
|
||||
select {
|
||||
case d.routes[j.ID][i] <- j.Data:
|
||||
case <-timeout.C:
|
||||
}
|
||||
}
|
||||
d.rMtx.RUnlock()
|
||||
|
||||
case v := <-d.shutdown:
|
||||
if !timeout.Stop() {
|
||||
select {
|
||||
case <-timeout.C:
|
||||
default:
|
||||
}
|
||||
}
|
||||
atomic.AddInt32(&d.count, -1)
|
||||
if v != nil {
|
||||
v.Done()
|
||||
}
|
||||
case <-d.shutdown:
|
||||
d.wg.Done()
|
||||
return
|
||||
}
|
||||
@@ -244,131 +186,151 @@ func (d *Dispatcher) relayer(i *sync.WaitGroup) {
|
||||
|
||||
// publish relays data to the subscribed subsystems
|
||||
func (d *Dispatcher) publish(id uuid.UUID, data interface{}) error {
|
||||
if d == nil {
|
||||
return errDispatcherNotInitialized
|
||||
}
|
||||
|
||||
if id.IsNil() {
|
||||
return errIDNotSet
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
return errors.New("dispatcher data cannot be nil")
|
||||
return errNoData
|
||||
}
|
||||
|
||||
if id == (uuid.UUID{}) {
|
||||
return errors.New("dispatcher uuid not set")
|
||||
}
|
||||
d.m.RLock()
|
||||
defer d.m.RUnlock()
|
||||
|
||||
if atomic.LoadUint32(&d.running) == 0 {
|
||||
if !d.running {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a new job to publish
|
||||
newJob := &job{
|
||||
Data: data,
|
||||
ID: id,
|
||||
}
|
||||
|
||||
// Push job on stack here
|
||||
select {
|
||||
case d.jobs <- newJob:
|
||||
case d.jobs <- job{data, id}: // Push job into job channel.
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("dispatcher jobs at limit [%d] current worker count [%d]. Spawn more workers via --dispatchworkers=x"+
|
||||
", or increase the jobs limit via --dispatchjobslimit=x",
|
||||
return fmt.Errorf(limitMessage,
|
||||
errDispatcherJobsAtLimit,
|
||||
len(d.jobs),
|
||||
atomic.LoadInt32(&d.count))
|
||||
d.maxWorkers)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Subscribe subscribes a system and returns a communication chan, this does not
|
||||
// ensure initial push. If your routine is out of sync with heartbeat and the
|
||||
// system does not get a change, its up to you to in turn get initial state.
|
||||
func (d *Dispatcher) subscribe(id uuid.UUID) (chan interface{}, error) {
|
||||
if atomic.LoadUint32(&d.running) == 0 {
|
||||
return nil, errors.New(errNotInitialised)
|
||||
// ensure initial push.
|
||||
func (d *Dispatcher) subscribe(id uuid.UUID) (<-chan interface{}, error) {
|
||||
if d == nil {
|
||||
return nil, errDispatcherNotInitialized
|
||||
}
|
||||
|
||||
// Read lock to read route list
|
||||
d.rMtx.RLock()
|
||||
if _, ok := d.routes[id]; !ok {
|
||||
d.rMtx.RUnlock()
|
||||
return nil, errors.New("dispatcher uuid not found in route list")
|
||||
if id.IsNil() {
|
||||
return nil, errIDNotSet
|
||||
}
|
||||
|
||||
d.m.RLock()
|
||||
defer d.m.RUnlock()
|
||||
|
||||
if !d.running {
|
||||
return nil, ErrNotRunning
|
||||
}
|
||||
|
||||
d.rMtx.Lock()
|
||||
defer d.rMtx.Unlock()
|
||||
if _, ok := d.routes[id]; !ok {
|
||||
return nil, errDispatcherUUIDNotFoundInRouteList
|
||||
}
|
||||
d.rMtx.RUnlock()
|
||||
|
||||
// Get an unused channel from the channel pool
|
||||
unusedChan, ok := d.outbound.Get().(chan interface{})
|
||||
ch, ok := d.outbound.Get().(chan interface{})
|
||||
if !ok {
|
||||
return nil, errors.New("unable to type assert unusedChan")
|
||||
return nil, errTypeAssertionFailure
|
||||
}
|
||||
|
||||
// Lock for writing to the route list
|
||||
d.rMtx.Lock()
|
||||
d.routes[id] = append(d.routes[id], unusedChan)
|
||||
d.rMtx.Unlock()
|
||||
|
||||
return unusedChan, nil
|
||||
d.routes[id] = append(d.routes[id], ch)
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
// Unsubscribe unsubs a routine from the dispatcher
|
||||
func (d *Dispatcher) unsubscribe(id uuid.UUID, usedChan chan interface{}) error {
|
||||
if atomic.LoadUint32(&d.running) == 0 {
|
||||
func (d *Dispatcher) unsubscribe(id uuid.UUID, usedChan <-chan interface{}) error {
|
||||
if d == nil {
|
||||
return errDispatcherNotInitialized
|
||||
}
|
||||
|
||||
if id.IsNil() {
|
||||
return errIDNotSet
|
||||
}
|
||||
|
||||
if usedChan == nil {
|
||||
return errChannelIsNil
|
||||
}
|
||||
|
||||
d.m.RLock()
|
||||
defer d.m.RUnlock()
|
||||
|
||||
if !d.running {
|
||||
// reference will already be released in the stop function
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read lock to read route list
|
||||
d.rMtx.RLock()
|
||||
if _, ok := d.routes[id]; !ok {
|
||||
d.rMtx.RUnlock()
|
||||
return errors.New("dispatcher uuid does not reference any channels")
|
||||
}
|
||||
d.rMtx.RUnlock()
|
||||
|
||||
// Lock for write to delete references
|
||||
d.rMtx.Lock()
|
||||
for i := range d.routes[id] {
|
||||
if d.routes[id][i] != usedChan {
|
||||
defer d.rMtx.Unlock()
|
||||
pipes, ok := d.routes[id]
|
||||
if !ok {
|
||||
return errDispatcherUUIDNotFoundInRouteList
|
||||
}
|
||||
|
||||
for i := range pipes {
|
||||
if pipes[i] != usedChan {
|
||||
continue
|
||||
}
|
||||
// Delete individual reference
|
||||
d.routes[id][i] = d.routes[id][len(d.routes[id])-1]
|
||||
d.routes[id][len(d.routes[id])-1] = nil
|
||||
d.routes[id] = d.routes[id][:len(d.routes[id])-1]
|
||||
|
||||
d.rMtx.Unlock()
|
||||
pipes[i] = pipes[len(pipes)-1]
|
||||
pipes[len(pipes)-1] = nil
|
||||
d.routes[id] = pipes[:len(pipes)-1]
|
||||
|
||||
// Drain and put the used chan back in pool; only if it is not closed.
|
||||
select {
|
||||
case _, ok := <-usedChan:
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
case _, ok = <-usedChan:
|
||||
default:
|
||||
}
|
||||
|
||||
d.outbound.Put(usedChan)
|
||||
if ok {
|
||||
d.outbound.Put(usedChan)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
d.rMtx.Unlock()
|
||||
return errors.New("dispatcher channel not found in uuid reference slice")
|
||||
return errChannelNotFoundInUUIDRef
|
||||
}
|
||||
|
||||
// GetNewID returns a new ID
|
||||
func (d *Dispatcher) getNewID() (uuid.UUID, error) {
|
||||
func (d *Dispatcher) getNewID(genFn func() (uuid.UUID, error)) (uuid.UUID, error) {
|
||||
if d == nil {
|
||||
return uuid.Nil, errDispatcherNotInitialized
|
||||
}
|
||||
|
||||
if genFn == nil {
|
||||
return uuid.Nil, errUUIDGeneratorFunctionIsNil
|
||||
}
|
||||
|
||||
// Continue to allow the generation, input and return of UUIDs even if
|
||||
// service is not currently enabled.
|
||||
|
||||
d.m.RLock()
|
||||
defer d.m.RUnlock()
|
||||
|
||||
// Generate new uuid
|
||||
newID, err := uuid.NewV4()
|
||||
newID, err := genFn()
|
||||
if err != nil {
|
||||
return uuid.UUID{}, err
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
// Check to see if it already exists
|
||||
d.rMtx.RLock()
|
||||
if _, ok := d.routes[newID]; ok {
|
||||
d.rMtx.RUnlock()
|
||||
return newID, errors.New("dispatcher collision detected, uuid already exists")
|
||||
}
|
||||
d.rMtx.RUnlock()
|
||||
|
||||
// Write the key into system
|
||||
d.rMtx.Lock()
|
||||
defer d.rMtx.Unlock()
|
||||
// Check to see if it already exists
|
||||
if _, ok := d.routes[newID]; ok {
|
||||
return uuid.Nil, errUUIDCollision
|
||||
}
|
||||
// Write the key into system
|
||||
d.routes[newID] = nil
|
||||
d.rMtx.Unlock()
|
||||
|
||||
return newID, nil
|
||||
}
|
||||
|
||||
@@ -1,233 +1,452 @@
|
||||
package dispatch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
var mux *Mux
|
||||
var (
|
||||
errTest = errors.New("test error")
|
||||
nonEmptyUUID = [uuid.Size]byte{108, 105, 99, 107, 77, 121, 72, 97, 105, 114, 121, 66, 97, 108, 108, 115}
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
err := Start(DefaultMaxWorkers, 0)
|
||||
func TestGlobalDispatcher(t *testing.T) {
|
||||
err := Start(0, 0)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
cpyDispatch = dispatcher
|
||||
mux = GetNewMux()
|
||||
cpyMux = mux
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
var cpyDispatch *Dispatcher
|
||||
var cpyMux *Mux
|
||||
|
||||
func TestDispatcher(t *testing.T) {
|
||||
dispatcher = nil
|
||||
err := Stop()
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
}
|
||||
|
||||
err = Start(10, 0)
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
}
|
||||
if IsRunning() {
|
||||
t.Error("should be false")
|
||||
}
|
||||
|
||||
err = DropWorker()
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
}
|
||||
|
||||
err = SpawnWorker()
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
}
|
||||
|
||||
dispatcher = cpyDispatch
|
||||
|
||||
if !IsRunning() {
|
||||
t.Error("should be true")
|
||||
}
|
||||
|
||||
err = Start(10, 0)
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
}
|
||||
|
||||
err = DropWorker()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = DropWorker()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = SpawnWorker()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = SpawnWorker()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = SpawnWorker()
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
running := IsRunning()
|
||||
if !running {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", IsRunning(), true)
|
||||
}
|
||||
|
||||
err = Stop()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
err = Stop()
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
}
|
||||
|
||||
err = Start(0, 20)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if cap(dispatcher.jobs) != 20 {
|
||||
t.Errorf("Expected jobs limit to be %v, is %v", 20, cap(dispatcher.jobs))
|
||||
}
|
||||
payload := "something"
|
||||
|
||||
err = dispatcher.publish(uuid.UUID{}, &payload)
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
}
|
||||
|
||||
err = dispatcher.publish(uuid.UUID{}, nil)
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
}
|
||||
|
||||
id, err := dispatcher.getNewID()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = dispatcher.publish(id, &payload)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = dispatcher.stop()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
err = dispatcher.publish(id, &payload)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = dispatcher.subscribe(id)
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
}
|
||||
|
||||
err = dispatcher.start(10, -1)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if cap(dispatcher.jobs) != DefaultJobsLimit {
|
||||
t.Errorf("Expected jobs limit to be %v, is %v", DefaultJobsLimit, cap(dispatcher.jobs))
|
||||
}
|
||||
someID, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = dispatcher.subscribe(someID)
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
}
|
||||
|
||||
randomChan := make(chan interface{})
|
||||
err = dispatcher.unsubscribe(someID, randomChan)
|
||||
if err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
|
||||
err = dispatcher.unsubscribe(id, randomChan)
|
||||
if err == nil {
|
||||
t.Error("Expected error")
|
||||
}
|
||||
|
||||
close(randomChan)
|
||||
err = dispatcher.unsubscribe(id, randomChan)
|
||||
if err == nil {
|
||||
t.Error("Expected error")
|
||||
running = IsRunning()
|
||||
if running {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", IsRunning(), false)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMux(t *testing.T) {
|
||||
mux = nil
|
||||
_, err := mux.Subscribe(uuid.UUID{})
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
func TestStartStop(t *testing.T) {
|
||||
t.Parallel()
|
||||
var d *Dispatcher
|
||||
|
||||
if d.isRunning() {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", d.isRunning(), false)
|
||||
}
|
||||
|
||||
err = mux.Unsubscribe(uuid.UUID{}, nil)
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
err := d.stop()
|
||||
if !errors.Is(err, errDispatcherNotInitialized) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errDispatcherNotInitialized)
|
||||
}
|
||||
|
||||
err = mux.Publish(nil, nil)
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
err = d.start(10, 0)
|
||||
if !errors.Is(err, errDispatcherNotInitialized) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errDispatcherNotInitialized)
|
||||
}
|
||||
|
||||
_, err = mux.GetID()
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
}
|
||||
mux = cpyMux
|
||||
d = NewDispatcher()
|
||||
|
||||
err = mux.Publish(nil, nil)
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
err = d.stop()
|
||||
if !errors.Is(err, ErrNotRunning) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrNotRunning)
|
||||
}
|
||||
|
||||
payload := "string"
|
||||
id, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
if d.isRunning() {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", d.isRunning(), false)
|
||||
}
|
||||
|
||||
err = mux.Publish([]uuid.UUID{id}, &payload)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
err = d.start(1, 100)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
_, err = mux.Subscribe(uuid.UUID{})
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
if !d.isRunning() {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", d.isRunning(), true)
|
||||
}
|
||||
|
||||
_, err = mux.Subscribe(id)
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
err = d.start(0, 0)
|
||||
if !errors.Is(err, errDispatcherAlreadyRunning) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errDispatcherAlreadyRunning)
|
||||
}
|
||||
|
||||
// Add route option
|
||||
id, err := d.getNewID(uuid.NewV4)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
// Add pipe
|
||||
_, err = d.subscribe(id)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
// Max out jobs channel
|
||||
for x := 0; x < 99; x++ {
|
||||
err = d.publish(id, "woah-nelly")
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
}
|
||||
|
||||
err = d.stop()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if d.isRunning() {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", d.isRunning(), false)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubscribe(t *testing.T) {
|
||||
t.Parallel()
|
||||
var d *Dispatcher
|
||||
_, err := d.subscribe(uuid.Nil)
|
||||
if !errors.Is(err, errDispatcherNotInitialized) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errDispatcherNotInitialized)
|
||||
}
|
||||
|
||||
d = NewDispatcher()
|
||||
|
||||
_, err = d.subscribe(uuid.Nil)
|
||||
if !errors.Is(err, errIDNotSet) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errIDNotSet)
|
||||
}
|
||||
|
||||
_, err = d.subscribe(nonEmptyUUID)
|
||||
if !errors.Is(err, ErrNotRunning) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrNotRunning)
|
||||
}
|
||||
|
||||
err = d.start(0, 0)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
id, err := d.getNewID(uuid.NewV4)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
_, err = d.subscribe(nonEmptyUUID)
|
||||
if !errors.Is(err, errDispatcherUUIDNotFoundInRouteList) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errDispatcherUUIDNotFoundInRouteList)
|
||||
}
|
||||
|
||||
d.outbound.New = func() interface{} { return "omg" }
|
||||
_, err = d.subscribe(id)
|
||||
if !errors.Is(err, errTypeAssertionFailure) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errTypeAssertionFailure)
|
||||
}
|
||||
|
||||
d.outbound.New = getChan
|
||||
ch, err := d.subscribe(id)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if ch == nil {
|
||||
t.Fatal("expected channel value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsubscribe(t *testing.T) {
|
||||
t.Parallel()
|
||||
var d *Dispatcher
|
||||
|
||||
err := d.unsubscribe(uuid.Nil, nil)
|
||||
if !errors.Is(err, errDispatcherNotInitialized) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errDispatcherNotInitialized)
|
||||
}
|
||||
|
||||
d = NewDispatcher()
|
||||
|
||||
err = d.unsubscribe(uuid.Nil, nil)
|
||||
if !errors.Is(err, errIDNotSet) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errIDNotSet)
|
||||
}
|
||||
|
||||
err = d.unsubscribe(nonEmptyUUID, nil)
|
||||
if !errors.Is(err, errChannelIsNil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errChannelIsNil)
|
||||
}
|
||||
|
||||
// will return nil if not running
|
||||
err = d.unsubscribe(nonEmptyUUID, make(<-chan interface{}))
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
err = d.start(0, 0)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
err = d.unsubscribe(nonEmptyUUID, make(<-chan interface{}))
|
||||
if !errors.Is(err, errDispatcherUUIDNotFoundInRouteList) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errDispatcherUUIDNotFoundInRouteList)
|
||||
}
|
||||
|
||||
id, err := d.getNewID(uuid.NewV4)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
err = d.unsubscribe(id, make(<-chan interface{}))
|
||||
if !errors.Is(err, errChannelNotFoundInUUIDRef) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errChannelNotFoundInUUIDRef)
|
||||
}
|
||||
|
||||
// Skip over this when matching pipes
|
||||
_, err = d.subscribe(id)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
ch, err := d.subscribe(id)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
err = d.unsubscribe(id, ch)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublish(t *testing.T) {
|
||||
t.Parallel()
|
||||
var d *Dispatcher
|
||||
|
||||
err := d.publish(uuid.Nil, nil)
|
||||
if !errors.Is(err, errDispatcherNotInitialized) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errDispatcherNotInitialized)
|
||||
}
|
||||
|
||||
d = NewDispatcher()
|
||||
|
||||
err = d.publish(nonEmptyUUID, "lol")
|
||||
if !errors.Is(err, nil) { // If not running, don't send back an error.
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
err = d.start(2, 10)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
err = d.publish(uuid.Nil, nil)
|
||||
if !errors.Is(err, errIDNotSet) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errIDNotSet)
|
||||
}
|
||||
|
||||
err = d.publish(nonEmptyUUID, nil)
|
||||
if !errors.Is(err, errNoData) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errNoData)
|
||||
}
|
||||
|
||||
// max out worker processing
|
||||
for x := 0; x < 100; x++ {
|
||||
err2 := d.publish(nonEmptyUUID, "lol")
|
||||
if !errors.Is(err2, nil) {
|
||||
err = err2
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !errors.Is(err, errDispatcherJobsAtLimit) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errDispatcherJobsAtLimit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublishReceive(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := NewDispatcher()
|
||||
if err := d.start(0, 0); !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
id, err := d.getNewID(uuid.NewV4)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
incoming, err := d.subscribe(id)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
go func(d *Dispatcher, id uuid.UUID) {
|
||||
for x := 0; x < 10; x++ {
|
||||
err2 := d.publish(id, "WOW")
|
||||
if !errors.Is(err2, nil) {
|
||||
panic(err2)
|
||||
}
|
||||
}
|
||||
}(d, id)
|
||||
|
||||
data, ok := (<-incoming).(string)
|
||||
if !ok {
|
||||
t.Fatal("type assertion failure expected string")
|
||||
}
|
||||
|
||||
if data != "WOW" {
|
||||
t.Fatal("unexpected value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNewID(t *testing.T) {
|
||||
t.Parallel()
|
||||
var d *Dispatcher
|
||||
|
||||
_, err := d.getNewID(uuid.NewV4)
|
||||
if !errors.Is(err, errDispatcherNotInitialized) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errDispatcherNotInitialized)
|
||||
}
|
||||
|
||||
d = NewDispatcher()
|
||||
|
||||
err = d.start(0, 0)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
_, err = d.getNewID(nil)
|
||||
if !errors.Is(err, errUUIDGeneratorFunctionIsNil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errUUIDGeneratorFunctionIsNil)
|
||||
}
|
||||
|
||||
_, err = d.getNewID(func() (uuid.UUID, error) { return uuid.Nil, errTest })
|
||||
if !errors.Is(err, errTest) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errTest)
|
||||
}
|
||||
|
||||
_, err = d.getNewID(func() (uuid.UUID, error) { return [uuid.Size]byte{254}, nil })
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
_, err = d.getNewID(func() (uuid.UUID, error) { return [uuid.Size]byte{254}, nil })
|
||||
if !errors.Is(err, errUUIDCollision) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errUUIDCollision)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMux(t *testing.T) {
|
||||
t.Parallel()
|
||||
var mux *Mux
|
||||
_, err := mux.Subscribe(uuid.Nil)
|
||||
if !errors.Is(err, errMuxIsNil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errMuxIsNil)
|
||||
}
|
||||
|
||||
err = mux.Unsubscribe(uuid.Nil, nil)
|
||||
if !errors.Is(err, errMuxIsNil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errMuxIsNil)
|
||||
}
|
||||
|
||||
err = mux.Publish(nil)
|
||||
if !errors.Is(err, errMuxIsNil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errMuxIsNil)
|
||||
}
|
||||
|
||||
_, err = mux.GetID()
|
||||
if !errors.Is(err, errMuxIsNil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errMuxIsNil)
|
||||
}
|
||||
|
||||
d := NewDispatcher()
|
||||
err = d.start(0, 0)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
mux = GetNewMux(d)
|
||||
|
||||
err = mux.Publish(nil)
|
||||
if !errors.Is(err, errNoData) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errNoData)
|
||||
}
|
||||
|
||||
err = mux.Publish("lol")
|
||||
if !errors.Is(err, errNoIDs) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errNoIDs)
|
||||
}
|
||||
|
||||
id, err := mux.GetID()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
_, err = mux.Subscribe(uuid.Nil)
|
||||
if !errors.Is(err, errIDNotSet) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errIDNotSet)
|
||||
}
|
||||
|
||||
pipe, err := mux.Subscribe(id)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
var errChan = make(chan error)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
// Makes sure receiver is waiting for update
|
||||
go func(ch <-chan interface{}, errChan chan error, wg *sync.WaitGroup) {
|
||||
wg.Done()
|
||||
response, ok := (<-ch).(string)
|
||||
if !ok {
|
||||
errChan <- errors.New("type assertion failure")
|
||||
return
|
||||
}
|
||||
|
||||
if response != "string" {
|
||||
errChan <- errors.New("unexpected return")
|
||||
return
|
||||
}
|
||||
errChan <- nil
|
||||
}(pipe.C, errChan, &wg)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
payload := "string"
|
||||
go func(payload string) {
|
||||
err2 := mux.Publish(payload, id)
|
||||
if err2 != nil {
|
||||
fmt.Println(err2)
|
||||
}
|
||||
}(payload)
|
||||
|
||||
err = <-errChan
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = pipe.Release()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMuxSubscribe(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := NewDispatcher()
|
||||
err := d.start(0, 0)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
mux := GetNewMux(d)
|
||||
itemID, err := mux.GetID()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -250,7 +469,14 @@ func TestSubscribe(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublish(t *testing.T) {
|
||||
func TestMuxPublish(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := NewDispatcher()
|
||||
err := d.start(0, 0)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
mux := GetNewMux(d)
|
||||
itemID, err := mux.GetID()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -261,41 +487,32 @@ func TestPublish(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func(wg *sync.WaitGroup) {
|
||||
wg.Done()
|
||||
for {
|
||||
_, ok := <-pipe.C
|
||||
if !ok {
|
||||
pErr := pipe.Release()
|
||||
if pErr != nil {
|
||||
t.Error(pErr)
|
||||
}
|
||||
wg.Done()
|
||||
return
|
||||
go func(mux *Mux) {
|
||||
for i := 0; i < 100; i++ {
|
||||
errMux := mux.Publish(i, itemID)
|
||||
if errMux != nil {
|
||||
t.Error(errMux)
|
||||
}
|
||||
}
|
||||
}(&wg)
|
||||
wg.Wait()
|
||||
wg.Add(1)
|
||||
mainPayload := "PAYLOAD"
|
||||
for i := 0; i < 100; i++ {
|
||||
errMux := mux.Publish([]uuid.UUID{itemID}, &mainPayload)
|
||||
if errMux != nil {
|
||||
t.Error(errMux)
|
||||
}
|
||||
}
|
||||
}(mux)
|
||||
|
||||
<-pipe.C
|
||||
|
||||
// Shut down dispatch system
|
||||
err = Stop()
|
||||
err = d.stop()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// 2363419 468.7 ns/op 142 B/op 1 allocs/op
|
||||
func BenchmarkSubscribe(b *testing.B) {
|
||||
d := NewDispatcher()
|
||||
err := d.start(0, 0)
|
||||
if !errors.Is(err, nil) {
|
||||
b.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
mux := GetNewMux(d)
|
||||
newID, err := mux.GetID()
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
|
||||
@@ -17,14 +17,10 @@ const (
|
||||
// DefaultHandshakeTimeout defines a workers max length of time to wait on a
|
||||
// an unbuffered channel for a receiver before moving on to next route
|
||||
DefaultHandshakeTimeout = 200 * time.Nanosecond
|
||||
|
||||
errNotInitialised = "dispatcher not initialised"
|
||||
errShutdownRoutines = "dispatcher did not shutdown properly, routines failed to close"
|
||||
)
|
||||
|
||||
// dispatcher is our main in memory instance with a stop/start mtx below
|
||||
var dispatcher *Dispatcher
|
||||
var mtx sync.Mutex
|
||||
|
||||
// Dispatcher defines an internal subsystem communication/change state publisher
|
||||
type Dispatcher struct {
|
||||
@@ -33,30 +29,30 @@ type Dispatcher struct {
|
||||
// then publish the data across the full registered channels for that uuid.
|
||||
// See relayer() method below.
|
||||
routes map[uuid.UUID][]chan interface{}
|
||||
|
||||
// rMtx protects the routes variable ensuring acceptable read/write access
|
||||
rMtx sync.RWMutex
|
||||
|
||||
// Persistent buffered job queue for relayers
|
||||
jobs chan *job
|
||||
jobs chan job
|
||||
|
||||
// Dynamic channel pool; returns an unbuffered channel for routes map
|
||||
outbound sync.Pool
|
||||
|
||||
// MaxWorkers defines max worker ceiling
|
||||
maxWorkers int32
|
||||
// Atomic values -----------------------
|
||||
// Worker counter
|
||||
count int32
|
||||
maxWorkers int
|
||||
|
||||
// Dispatch status
|
||||
running uint32
|
||||
running bool
|
||||
|
||||
// Unbufferd shutdown chan, sync wg for ensuring concurrency when only
|
||||
// dropping a single relayer routine
|
||||
shutdown chan *sync.WaitGroup
|
||||
shutdown chan struct{}
|
||||
|
||||
// Relayer shutdown tracking
|
||||
wg sync.WaitGroup
|
||||
|
||||
// dispatcher write protection
|
||||
m sync.RWMutex
|
||||
}
|
||||
|
||||
// job defines a relaying job associated with a ticket which allows routing to
|
||||
@@ -76,7 +72,7 @@ type Mux struct {
|
||||
// Pipe defines an outbound object to the desired routine
|
||||
type Pipe struct {
|
||||
// Channel to get all our lovely informations
|
||||
C chan interface{}
|
||||
C <-chan interface{}
|
||||
// ID to tracked system
|
||||
id uuid.UUID
|
||||
// Reference to multiplexer
|
||||
|
||||
@@ -2,25 +2,35 @@ package dispatch
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
)
|
||||
|
||||
// GetNewMux returns a new multiplexer to track subsystem updates
|
||||
func GetNewMux() *Mux {
|
||||
return &Mux{d: dispatcher}
|
||||
var (
|
||||
errMuxIsNil = errors.New("mux is nil")
|
||||
errIDNotSet = errors.New("id not set")
|
||||
errNoData = errors.New("data payload is nil")
|
||||
errNoIDs = errors.New("no IDs to publish data to")
|
||||
)
|
||||
|
||||
// GetNewMux returns a new multiplexer to track subsystem updates, if nil
|
||||
// dispatcher provided it will default to the global Dispatcher.
|
||||
func GetNewMux(d *Dispatcher) *Mux {
|
||||
if d == nil {
|
||||
d = dispatcher
|
||||
}
|
||||
return &Mux{d: d}
|
||||
}
|
||||
|
||||
// Subscribe takes in a package defined signature element pointing to an ID set
|
||||
// and returns the associated pipe
|
||||
func (m *Mux) Subscribe(id uuid.UUID) (Pipe, error) {
|
||||
if m == nil {
|
||||
return Pipe{}, errors.New("mux is nil")
|
||||
return Pipe{}, errMuxIsNil
|
||||
}
|
||||
|
||||
if id == (uuid.UUID{}) {
|
||||
return Pipe{}, errors.New("id not set")
|
||||
if id.IsNil() {
|
||||
return Pipe{}, errIDNotSet
|
||||
}
|
||||
|
||||
ch, err := m.d.subscribe(id)
|
||||
@@ -32,29 +42,30 @@ func (m *Mux) Subscribe(id uuid.UUID) (Pipe, error) {
|
||||
}
|
||||
|
||||
// Unsubscribe returns channel to the pool for the full signature set
|
||||
func (m *Mux) Unsubscribe(id uuid.UUID, ch chan interface{}) error {
|
||||
func (m *Mux) Unsubscribe(id uuid.UUID, ch <-chan interface{}) error {
|
||||
if m == nil {
|
||||
return errors.New("mux is nil")
|
||||
return errMuxIsNil
|
||||
}
|
||||
return m.d.unsubscribe(id, ch)
|
||||
}
|
||||
|
||||
// Publish takes in a persistent memory address and dispatches changes to
|
||||
// required pipes. Data should be of *type.
|
||||
func (m *Mux) Publish(ids []uuid.UUID, data interface{}) error {
|
||||
// required pipes.
|
||||
func (m *Mux) Publish(data interface{}, ids ...uuid.UUID) error {
|
||||
if m == nil {
|
||||
return errors.New("mux is nil")
|
||||
return errMuxIsNil
|
||||
}
|
||||
|
||||
if data == nil {
|
||||
return errors.New("data payload is nil")
|
||||
return errNoData
|
||||
}
|
||||
|
||||
cpy := reflect.ValueOf(data).Elem().Interface()
|
||||
if len(ids) == 0 {
|
||||
return errNoIDs
|
||||
}
|
||||
|
||||
for i := range ids {
|
||||
// Create copy to not interfere with stored value
|
||||
err := m.d.publish(ids[i], &cpy)
|
||||
err := m.d.publish(ids[i], data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -65,9 +76,9 @@ func (m *Mux) Publish(ids []uuid.UUID, data interface{}) error {
|
||||
// GetID a new unique ID to track routing information in the dispatch system
|
||||
func (m *Mux) GetID() (uuid.UUID, error) {
|
||||
if m == nil {
|
||||
return uuid.UUID{}, errors.New("mux is nil")
|
||||
return uuid.UUID{}, errMuxIsNil
|
||||
}
|
||||
return m.d.getNewID()
|
||||
return m.d.getNewID(uuid.NewV4)
|
||||
}
|
||||
|
||||
// Release returns the channel to the communications pool to be reused
|
||||
|
||||
@@ -692,35 +692,29 @@ func (s *RPCServer) GetAccountInfoStream(r *gctrpc.GetAccountInfoRequest, stream
|
||||
return errDispatchSystem
|
||||
}
|
||||
|
||||
d, ok := data.(*interface{})
|
||||
holdings, ok := data.(*account.Holdings)
|
||||
if !ok {
|
||||
return errors.New("unable to type assert data")
|
||||
return common.GetAssertError("*account.Holdings", data)
|
||||
}
|
||||
|
||||
dd := *d
|
||||
acc, ok := dd.(account.Holdings)
|
||||
if !ok {
|
||||
return errors.New("unable to type assert account holdings data")
|
||||
}
|
||||
|
||||
accounts := make([]*gctrpc.Account, len(acc.Accounts))
|
||||
for x := range acc.Accounts {
|
||||
subAccounts := make([]*gctrpc.AccountCurrencyInfo, len(acc.Accounts[x].Currencies))
|
||||
for y := range acc.Accounts[x].Currencies {
|
||||
accounts := make([]*gctrpc.Account, len(holdings.Accounts))
|
||||
for x := range holdings.Accounts {
|
||||
subAccounts := make([]*gctrpc.AccountCurrencyInfo, len(holdings.Accounts[x].Currencies))
|
||||
for y := range holdings.Accounts[x].Currencies {
|
||||
subAccounts[y] = &gctrpc.AccountCurrencyInfo{
|
||||
Currency: acc.Accounts[x].Currencies[y].CurrencyName.String(),
|
||||
TotalValue: acc.Accounts[x].Currencies[y].Total,
|
||||
Hold: acc.Accounts[x].Currencies[y].Hold,
|
||||
Currency: holdings.Accounts[x].Currencies[y].CurrencyName.String(),
|
||||
TotalValue: holdings.Accounts[x].Currencies[y].Total,
|
||||
Hold: holdings.Accounts[x].Currencies[y].Hold,
|
||||
}
|
||||
}
|
||||
accounts[x] = &gctrpc.Account{
|
||||
Id: acc.Accounts[x].ID,
|
||||
Id: holdings.Accounts[x].ID,
|
||||
Currencies: subAccounts,
|
||||
}
|
||||
}
|
||||
|
||||
if err := stream.Send(&gctrpc.GetAccountInfoResponse{
|
||||
Exchange: acc.Exchange,
|
||||
Exchange: holdings.Exchange,
|
||||
Accounts: accounts,
|
||||
}); err != nil {
|
||||
return err
|
||||
@@ -2064,27 +2058,32 @@ func (s *RPCServer) GetOrderbookStream(r *gctrpc.GetOrderbookStreamRequest, stre
|
||||
}
|
||||
|
||||
for {
|
||||
base := depth.Retrieve()
|
||||
bids := make([]*gctrpc.OrderbookItem, len(base.Bids))
|
||||
for i := range base.Bids {
|
||||
bids[i] = &gctrpc.OrderbookItem{
|
||||
Amount: base.Bids[i].Amount,
|
||||
Price: base.Bids[i].Price,
|
||||
Id: base.Bids[i].ID}
|
||||
}
|
||||
asks := make([]*gctrpc.OrderbookItem, len(base.Asks))
|
||||
for i := range base.Asks {
|
||||
asks[i] = &gctrpc.OrderbookItem{
|
||||
Amount: base.Asks[i].Amount,
|
||||
Price: base.Asks[i].Price,
|
||||
Id: base.Asks[i].ID}
|
||||
}
|
||||
err := stream.Send(&gctrpc.OrderbookResponse{
|
||||
resp := &gctrpc.OrderbookResponse{
|
||||
Pair: &gctrpc.CurrencyPair{Base: r.Pair.Base, Quote: r.Pair.Quote},
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
AssetType: r.AssetType,
|
||||
})
|
||||
}
|
||||
base, err := depth.Retrieve()
|
||||
if err != nil {
|
||||
resp.Error = err.Error()
|
||||
resp.LastUpdated = time.Now().Unix()
|
||||
} else {
|
||||
resp.Bids = make([]*gctrpc.OrderbookItem, len(base.Bids))
|
||||
for i := range base.Bids {
|
||||
resp.Bids[i] = &gctrpc.OrderbookItem{
|
||||
Amount: base.Bids[i].Amount,
|
||||
Price: base.Bids[i].Price,
|
||||
Id: base.Bids[i].ID}
|
||||
}
|
||||
resp.Asks = make([]*gctrpc.OrderbookItem, len(base.Asks))
|
||||
for i := range base.Asks {
|
||||
resp.Asks[i] = &gctrpc.OrderbookItem{
|
||||
Amount: base.Asks[i].Amount,
|
||||
Price: base.Asks[i].Price,
|
||||
Id: base.Asks[i].ID}
|
||||
}
|
||||
}
|
||||
|
||||
err = stream.Send(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2120,38 +2119,39 @@ func (s *RPCServer) GetExchangeOrderbookStream(r *gctrpc.GetExchangeOrderbookStr
|
||||
return errDispatchSystem
|
||||
}
|
||||
|
||||
d, ok := data.(*interface{})
|
||||
d, ok := data.(orderbook.Outbound)
|
||||
if !ok {
|
||||
return errors.New("unable to type assert data")
|
||||
return common.GetAssertError("orderbook.Outbound", data)
|
||||
}
|
||||
|
||||
dd := *d
|
||||
ob, ok := dd.(orderbook.Base)
|
||||
if !ok {
|
||||
return errors.New("unable to type assert orderbook data")
|
||||
resp := &gctrpc.OrderbookResponse{}
|
||||
ob, err := d.Retrieve()
|
||||
if err != nil {
|
||||
resp.Error = err.Error()
|
||||
resp.LastUpdated = time.Now().Unix()
|
||||
} else {
|
||||
resp.Pair = &gctrpc.CurrencyPair{
|
||||
Base: ob.Pair.Base.String(),
|
||||
Quote: ob.Pair.Quote.String(),
|
||||
}
|
||||
resp.AssetType = ob.Asset.String()
|
||||
resp.Bids = make([]*gctrpc.OrderbookItem, len(ob.Bids))
|
||||
for i := range ob.Bids {
|
||||
resp.Bids[i] = &gctrpc.OrderbookItem{
|
||||
Amount: ob.Bids[i].Amount,
|
||||
Price: ob.Bids[i].Price,
|
||||
Id: ob.Bids[i].ID}
|
||||
}
|
||||
resp.Asks = make([]*gctrpc.OrderbookItem, len(ob.Asks))
|
||||
for i := range ob.Asks {
|
||||
resp.Asks[i] = &gctrpc.OrderbookItem{
|
||||
Amount: ob.Asks[i].Amount,
|
||||
Price: ob.Asks[i].Price,
|
||||
Id: ob.Asks[i].ID}
|
||||
}
|
||||
}
|
||||
|
||||
bids := make([]*gctrpc.OrderbookItem, len(ob.Bids))
|
||||
for i := range ob.Bids {
|
||||
bids[i] = &gctrpc.OrderbookItem{
|
||||
Amount: ob.Bids[i].Amount,
|
||||
Price: ob.Bids[i].Price,
|
||||
Id: ob.Bids[i].ID}
|
||||
}
|
||||
asks := make([]*gctrpc.OrderbookItem, len(ob.Asks))
|
||||
for i := range ob.Asks {
|
||||
asks[i] = &gctrpc.OrderbookItem{
|
||||
Amount: ob.Asks[i].Amount,
|
||||
Price: ob.Asks[i].Price,
|
||||
Id: ob.Asks[i].ID}
|
||||
}
|
||||
err := stream.Send(&gctrpc.OrderbookResponse{
|
||||
Pair: &gctrpc.CurrencyPair{Base: ob.Pair.Base.String(),
|
||||
Quote: ob.Pair.Quote.String()},
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
AssetType: ob.Asset.String(),
|
||||
})
|
||||
err = stream.Send(resp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -2204,15 +2204,9 @@ func (s *RPCServer) GetTickerStream(r *gctrpc.GetTickerStreamRequest, stream gct
|
||||
return errDispatchSystem
|
||||
}
|
||||
|
||||
d, ok := data.(*interface{})
|
||||
t, ok := data.(*ticker.Price)
|
||||
if !ok {
|
||||
return errors.New("unable to type assert data")
|
||||
}
|
||||
|
||||
dd := *d
|
||||
t, ok := dd.(ticker.Price)
|
||||
if !ok {
|
||||
return errors.New("unable to type assert ticker data")
|
||||
return common.GetAssertError("*ticker.Price", data)
|
||||
}
|
||||
|
||||
err := stream.Send(&gctrpc.TickerResponse{
|
||||
@@ -2263,15 +2257,9 @@ func (s *RPCServer) GetExchangeTickerStream(r *gctrpc.GetExchangeTickerStreamReq
|
||||
return errDispatchSystem
|
||||
}
|
||||
|
||||
d, ok := data.(*interface{})
|
||||
t, ok := data.(*ticker.Price)
|
||||
if !ok {
|
||||
return errors.New("unable to type assert data")
|
||||
}
|
||||
|
||||
dd := *d
|
||||
t, ok := dd.(ticker.Price)
|
||||
if !ok {
|
||||
return errors.New("unable to type assert ticker data")
|
||||
return common.GetAssertError("*ticker.Price", data)
|
||||
}
|
||||
|
||||
err := stream.Send(&gctrpc.TickerResponse{
|
||||
@@ -2530,7 +2518,7 @@ func (s *RPCServer) GCTScriptStatus(_ context.Context, _ *gctrpc.GCTScriptStatus
|
||||
gctscript.AllVMSync.Range(func(k, v interface{}) bool {
|
||||
vm, ok := v.(*gctscript.VM)
|
||||
if !ok {
|
||||
log.Errorf(log.GRPCSys, "Unable to type assert gctscript.VM")
|
||||
log.Errorf(log.GRPCSys, "%v", common.GetAssertError("*gctscript.VM", v))
|
||||
return false
|
||||
}
|
||||
resp.Scripts = append(resp.Scripts, &gctrpc.GCTScript{
|
||||
|
||||
@@ -765,7 +765,7 @@ func (m *syncManager) PrintTickerSummary(result *ticker.Price, protocol string,
|
||||
!result.Pair.Quote.Equal(m.fiatDisplayCurrency) &&
|
||||
!m.fiatDisplayCurrency.IsEmpty() {
|
||||
origCurrency := result.Pair.Quote.Upper()
|
||||
log.Infof(log.Ticker, "%s %s %s %s: TICKER: Last %s Ask %s Bid %s High %s Low %s Volume %.8f",
|
||||
log.Infof(log.SyncMgr, "%s %s %s %s TICKER: Last %s Ask %s Bid %s High %s Low %s Volume %.8f",
|
||||
result.ExchangeName,
|
||||
protocol,
|
||||
m.FormatCurrency(result.Pair),
|
||||
@@ -780,7 +780,7 @@ func (m *syncManager) PrintTickerSummary(result *ticker.Price, protocol string,
|
||||
if result.Pair.Quote.IsFiatCurrency() &&
|
||||
result.Pair.Quote.Equal(m.fiatDisplayCurrency) &&
|
||||
!m.fiatDisplayCurrency.IsEmpty() {
|
||||
log.Infof(log.Ticker, "%s %s %s %s: TICKER: Last %s Ask %s Bid %s High %s Low %s Volume %.8f",
|
||||
log.Infof(log.SyncMgr, "%s %s %s %s TICKER: Last %s Ask %s Bid %s High %s Low %s Volume %.8f",
|
||||
result.ExchangeName,
|
||||
protocol,
|
||||
m.FormatCurrency(result.Pair),
|
||||
@@ -792,7 +792,7 @@ func (m *syncManager) PrintTickerSummary(result *ticker.Price, protocol string,
|
||||
printCurrencyFormat(result.Low, m.fiatDisplayCurrency),
|
||||
result.Volume)
|
||||
} else {
|
||||
log.Infof(log.Ticker, "%s %s %s %s: TICKER: Last %.8f Ask %.8f Bid %.8f High %.8f Low %.8f Volume %.8f",
|
||||
log.Infof(log.SyncMgr, "%s %s %s %s TICKER: Last %.8f Ask %.8f Bid %.8f High %.8f Low %.8f Volume %.8f",
|
||||
result.ExchangeName,
|
||||
protocol,
|
||||
m.FormatCurrency(result.Pair),
|
||||
@@ -817,7 +817,7 @@ func (m *syncManager) FormatCurrency(p currency.Pair) currency.Pair {
|
||||
}
|
||||
|
||||
const (
|
||||
book = "%s %s %s %s: ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %s Asks len: %d Amount: %f %s. Total value: %s"
|
||||
book = "%s %s %s %s ORDERBOOK: Bids len: %d Amount: %f %s. Total value: %s Asks len: %d Amount: %f %s. Total value: %s"
|
||||
)
|
||||
|
||||
// PrintOrderbookSummary outputs orderbook results
|
||||
@@ -871,7 +871,7 @@ func (m *syncManager) PrintOrderbookSummary(result *orderbook.Base, protocol str
|
||||
askValueResult = strconv.FormatFloat(asksValue, 'f', -1, 64)
|
||||
}
|
||||
|
||||
log.Infof(log.OrderBook, book,
|
||||
log.Infof(log.SyncMgr, book,
|
||||
result.Exchange,
|
||||
protocol,
|
||||
m.FormatCurrency(result.Pair),
|
||||
|
||||
@@ -196,18 +196,22 @@ func (m *websocketRoutineManager) WebsocketDataHandler(exchName string, data int
|
||||
d.AssetType,
|
||||
d)
|
||||
}
|
||||
case *orderbook.Base:
|
||||
case *orderbook.Depth:
|
||||
base, err := d.Retrieve()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if m.syncer.IsRunning() {
|
||||
err := m.syncer.Update(exchName,
|
||||
d.Pair,
|
||||
d.Asset,
|
||||
base.Pair,
|
||||
base.Asset,
|
||||
SyncItemOrderbook,
|
||||
nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
m.syncer.PrintOrderbookSummary(d, "websocket", nil)
|
||||
m.syncer.PrintOrderbookSummary(base, "websocket", nil)
|
||||
case *order.Detail:
|
||||
m.printOrderSummary(d)
|
||||
if !m.orderManager.Exists(d) {
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/thrasher-corp/gocryptotrader/common"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/dispatch"
|
||||
@@ -15,7 +14,7 @@ import (
|
||||
|
||||
func init() {
|
||||
service.exchangeAccounts = make(map[string]*Accounts)
|
||||
service.mux = dispatch.GetNewMux()
|
||||
service.mux = dispatch.GetNewMux(nil)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -237,7 +236,7 @@ func (s *Service) Update(a *Holdings) error {
|
||||
bal.load(a.Accounts[x].Currencies[y])
|
||||
}
|
||||
}
|
||||
err := s.mux.Publish([]uuid.UUID{accounts.ID}, a)
|
||||
err := s.mux.Publish(a, accounts.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -284,10 +283,7 @@ func (b *ProtectedBalance) Wait(maxWait time.Duration) (wait <-chan bool, cancel
|
||||
ch := make(chan struct{})
|
||||
go func(ch chan<- struct{}, until time.Duration) {
|
||||
time.Sleep(until)
|
||||
select {
|
||||
case ch <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
close(ch)
|
||||
}(ch, maxWait)
|
||||
|
||||
return b.notice.Wait(ch), ch, nil
|
||||
|
||||
@@ -330,7 +330,7 @@ func TestBalanceInternalWait(t *testing.T) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
bi.notice.Alert()
|
||||
go bi.notice.Alert()
|
||||
if <-waiter {
|
||||
t.Fatal("should have been alerted by change notice")
|
||||
}
|
||||
@@ -378,7 +378,7 @@ func TestGetFree(t *testing.T) {
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := &Service{exchangeAccounts: make(map[string]*Accounts), mux: dispatch.GetNewMux()}
|
||||
s := &Service{exchangeAccounts: make(map[string]*Accounts), mux: dispatch.GetNewMux(nil)}
|
||||
err := s.Update(nil)
|
||||
if !errors.Is(err, errHoldingsIsNil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errHoldingsIsNil)
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
@@ -669,7 +668,7 @@ func (b *Binance) ProcessUpdate(cp currency.Pair, a asset.Item, ws *WebsocketDep
|
||||
updateAsk[i] = orderbook.Item{Price: p, Amount: a}
|
||||
}
|
||||
|
||||
return b.Websocket.Orderbook.Update(&buffer.Update{
|
||||
return b.Websocket.Orderbook.Update(&orderbook.Update{
|
||||
Bids: updateBid,
|
||||
Asks: updateAsk,
|
||||
Pair: cp,
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
@@ -1409,7 +1408,7 @@ func (b *Bitfinex) WsInsertSnapshot(p currency.Pair, assetType asset.Item, books
|
||||
// WsUpdateOrderbook updates the orderbook list, removing and adding to the
|
||||
// orderbook sides
|
||||
func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType asset.Item, book []WebsocketBook, channelID int, sequenceNo int64, fundingRate bool) error {
|
||||
orderbookUpdate := buffer.Update{
|
||||
orderbookUpdate := orderbook.Update{
|
||||
Asset: assetType,
|
||||
Pair: p,
|
||||
Bids: make([]orderbook.Item, 0, len(book)),
|
||||
@@ -1425,7 +1424,7 @@ func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType asset.Item, book
|
||||
}
|
||||
|
||||
if book[i].Price > 0 {
|
||||
orderbookUpdate.Action = buffer.UpdateInsert
|
||||
orderbookUpdate.Action = orderbook.UpdateInsert
|
||||
if fundingRate {
|
||||
if book[i].Amount < 0 {
|
||||
item.Amount *= -1
|
||||
@@ -1442,7 +1441,7 @@ func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType asset.Item, book
|
||||
}
|
||||
}
|
||||
} else {
|
||||
orderbookUpdate.Action = buffer.Delete
|
||||
orderbookUpdate.Action = orderbook.Delete
|
||||
if fundingRate {
|
||||
if book[i].Amount == 1 {
|
||||
// delete bid
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
|
||||
@@ -36,7 +35,7 @@ func (b *Bithumb) processBooks(updates *WsOrderbooks) error {
|
||||
}
|
||||
asks = append(asks, i)
|
||||
}
|
||||
return b.Websocket.Orderbook.Update(&buffer.Update{
|
||||
return b.Websocket.Orderbook.Update(&orderbook.Update{
|
||||
Pair: updates.List[0].Symbol,
|
||||
Asset: asset.Spot,
|
||||
Bids: bids,
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
|
||||
@@ -1041,12 +1042,9 @@ func TestWSOrderbookHandling(t *testing.T) {
|
||||
]
|
||||
}`)
|
||||
err = b.wsHandleData(pressXToJSON)
|
||||
if err != nil && err.Error() != "delete error: cannot match ID on linked list 17999995000 not found" {
|
||||
if !errors.Is(err, orderbook.ErrOrderbookInvalid) {
|
||||
t.Error(err)
|
||||
}
|
||||
if err == nil {
|
||||
t.Error("expecting error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWSDeleveragePositionUpdateHandling(t *testing.T) {
|
||||
@@ -1175,3 +1173,47 @@ func TestCurrencyNormalization(t *testing.T) {
|
||||
t.Errorf("amount mismatch, expected 1.0, got %f", w.Amount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetActionFromString(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := b.GetActionFromString("meow")
|
||||
if !errors.Is(err, orderbook.ErrInvalidAction) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, orderbook.ErrInvalidAction)
|
||||
}
|
||||
|
||||
action, err := b.GetActionFromString("update")
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if action != orderbook.Amend {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", action, orderbook.Amend)
|
||||
}
|
||||
|
||||
action, err = b.GetActionFromString("delete")
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if action != orderbook.Delete {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", action, orderbook.Delete)
|
||||
}
|
||||
|
||||
action, err = b.GetActionFromString("insert")
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if action != orderbook.Insert {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", action, orderbook.Insert)
|
||||
}
|
||||
|
||||
action, err = b.GetActionFromString("update/insert")
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if action != orderbook.UpdateInsert {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", action, orderbook.UpdateInsert)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
@@ -526,6 +525,11 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, p currency.
|
||||
err)
|
||||
}
|
||||
default:
|
||||
updateAction, err := b.GetActionFromString(action)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
asks := make([]orderbook.Item, 0, len(data))
|
||||
bids := make([]orderbook.Item, 0, len(data))
|
||||
for i := range data {
|
||||
@@ -541,12 +545,12 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, p currency.
|
||||
bids = append(bids, nItem)
|
||||
}
|
||||
|
||||
err := b.Websocket.Orderbook.Update(&buffer.Update{
|
||||
err = b.Websocket.Orderbook.Update(&orderbook.Update{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
Pair: p,
|
||||
Asset: a,
|
||||
Action: buffer.Action(action),
|
||||
Action: updateAction,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -693,3 +697,18 @@ func (b *Bitmex) websocketSendAuth(ctx context.Context) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActionFromString matches a string action to an internal action.
|
||||
func (b *Bitmex) GetActionFromString(s string) (orderbook.Action, error) {
|
||||
switch s {
|
||||
case "update":
|
||||
return orderbook.Amend, nil
|
||||
case "delete":
|
||||
return orderbook.Delete, nil
|
||||
case "insert":
|
||||
return orderbook.Insert, nil
|
||||
case "update/insert":
|
||||
return orderbook.UpdateInsert, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s %w", s, orderbook.ErrInvalidAction)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
|
||||
@@ -64,7 +63,7 @@ func (b *Bittrex) ProcessUpdateOB(pair currency.Pair, message *OrderbookUpdateMe
|
||||
}
|
||||
}
|
||||
|
||||
return b.Websocket.Orderbook.Update(&buffer.Update{
|
||||
return b.Websocket.Orderbook.Update(&orderbook.Update{
|
||||
Asset: asset.Spot,
|
||||
Pair: pair,
|
||||
UpdateID: message.Sequence,
|
||||
|
||||
@@ -83,6 +83,11 @@ const (
|
||||
tick = "tick"
|
||||
wsOB = "orderbookUpdate"
|
||||
tradeEndPoint = "trade"
|
||||
|
||||
// Subscription management when connection and subscription established
|
||||
addSubscription = "addSubscription"
|
||||
removeSubscription = "removeSubscription"
|
||||
clientType = "api"
|
||||
)
|
||||
|
||||
// BTCMarkets is the overarching type across the BTCMarkets package
|
||||
|
||||
@@ -348,6 +348,7 @@ type WsSubscribe struct {
|
||||
Signature string `json:"signature,omitempty"`
|
||||
Timestamp string `json:"timestamp,omitempty"`
|
||||
MessageType string `json:"messageType,omitempty"`
|
||||
ClientType string `json:"clientType,omitempty"`
|
||||
}
|
||||
|
||||
// WsMessageType message sent via ws to determine type
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
@@ -32,6 +31,8 @@ const (
|
||||
var (
|
||||
errTypeAssertionFailure = errors.New("type assertion failure")
|
||||
errChecksumFailure = errors.New("crc32 checksum failure")
|
||||
|
||||
authChannels = []string{fundChange, heartbeat, orderChange}
|
||||
)
|
||||
|
||||
// WsConnect connects to a websocket feed
|
||||
@@ -144,7 +145,7 @@ func (b *BTCMarkets) wsHandleData(respRaw []byte) error {
|
||||
VerifyOrderbook: b.CanVerifyOrderbook,
|
||||
})
|
||||
} else {
|
||||
err = b.Websocket.Orderbook.Update(&buffer.Update{
|
||||
err = b.Websocket.Orderbook.Update(&orderbook.Update{
|
||||
UpdateTime: ob.Timestamp,
|
||||
UpdateID: ob.SnapshotID,
|
||||
Asset: asset.Spot,
|
||||
@@ -155,8 +156,15 @@ func (b *BTCMarkets) wsHandleData(respRaw []byte) error {
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
if errors.Is(err, orderbook.ErrOrderbookInvalid) {
|
||||
err2 := b.ReSubscribeSpecificOrderbook(ob.Currency)
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
case tradeEndPoint:
|
||||
if !b.IsSaveTradeDataEnabled() {
|
||||
return nil
|
||||
@@ -334,7 +342,6 @@ func (b *BTCMarkets) generateDefaultSubscriptions() ([]stream.ChannelSubscriptio
|
||||
}
|
||||
|
||||
if b.Websocket.CanUseAuthenticatedEndpoints() {
|
||||
var authChannels = []string{fundChange, heartbeat, orderChange}
|
||||
for i := range authChannels {
|
||||
subscriptions = append(subscriptions, stream.ChannelSubscription{
|
||||
Channel: authChannels[i],
|
||||
@@ -345,57 +352,101 @@ func (b *BTCMarkets) generateDefaultSubscriptions() ([]stream.ChannelSubscriptio
|
||||
}
|
||||
|
||||
// Subscribe sends a websocket message to receive data from the channel
|
||||
func (b *BTCMarkets) Subscribe(channelsToSubscribe []stream.ChannelSubscription) error {
|
||||
func (b *BTCMarkets) Subscribe(subs []stream.ChannelSubscription) error {
|
||||
var payload WsSubscribe
|
||||
payload.MessageType = subscribe
|
||||
|
||||
for i := range channelsToSubscribe {
|
||||
payload.Channels = append(payload.Channels,
|
||||
channelsToSubscribe[i].Channel)
|
||||
|
||||
if channelsToSubscribe[i].Currency.String() != "" {
|
||||
if !common.StringDataCompare(payload.MarketIDs,
|
||||
channelsToSubscribe[i].Currency.String()) {
|
||||
payload.MarketIDs = append(payload.MarketIDs,
|
||||
channelsToSubscribe[i].Currency.String())
|
||||
}
|
||||
}
|
||||
if len(subs) > 1 {
|
||||
// TODO: Expand this to stream package as this assumes that we are doing
|
||||
// an initial sync.
|
||||
payload.MessageType = subscribe
|
||||
} else {
|
||||
payload.MessageType = addSubscription
|
||||
payload.ClientType = clientType
|
||||
}
|
||||
|
||||
if b.Websocket.CanUseAuthenticatedEndpoints() {
|
||||
var authChannels = []string{fundChange, heartbeat, orderChange}
|
||||
var authenticate bool
|
||||
for i := range subs {
|
||||
if !authenticate && common.StringDataContains(authChannels, subs[i].Channel) {
|
||||
authenticate = true
|
||||
}
|
||||
payload.Channels = append(payload.Channels, subs[i].Channel)
|
||||
if subs[i].Currency.IsEmpty() {
|
||||
continue
|
||||
}
|
||||
pair := subs[i].Currency.String()
|
||||
if common.StringDataCompare(payload.MarketIDs, pair) {
|
||||
continue
|
||||
}
|
||||
payload.MarketIDs = append(payload.MarketIDs, pair)
|
||||
}
|
||||
|
||||
if authenticate {
|
||||
creds, err := b.GetCredentials(context.TODO())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range authChannels {
|
||||
if !common.StringDataCompare(payload.Channels, authChannels[i]) {
|
||||
continue
|
||||
}
|
||||
signTime := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
strToSign := "/users/self/subscribe" + "\n" + signTime
|
||||
tempSign, err := crypto.GetHMAC(crypto.HashSHA512,
|
||||
[]byte(strToSign),
|
||||
[]byte(creds.Secret))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sign := crypto.Base64Encode(tempSign)
|
||||
payload.Key = creds.Key
|
||||
payload.Signature = sign
|
||||
payload.Timestamp = signTime
|
||||
break
|
||||
signTime := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
strToSign := "/users/self/subscribe" + "\n" + signTime
|
||||
var tempSign []byte
|
||||
tempSign, err = crypto.GetHMAC(crypto.HashSHA512,
|
||||
[]byte(strToSign),
|
||||
[]byte(creds.Secret))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sign := crypto.Base64Encode(tempSign)
|
||||
payload.Key = creds.Key
|
||||
payload.Signature = sign
|
||||
payload.Timestamp = signTime
|
||||
}
|
||||
|
||||
if err := b.Websocket.Conn.SendJSONMessage(payload); err != nil {
|
||||
return err
|
||||
}
|
||||
b.Websocket.AddSuccessfulSubscriptions(channelsToSubscribe...)
|
||||
b.Websocket.AddSuccessfulSubscriptions(subs...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unsubscribe sends a websocket message to manage and remove a subscription.
|
||||
func (b *BTCMarkets) Unsubscribe(subs []stream.ChannelSubscription) error {
|
||||
payload := WsSubscribe{
|
||||
MessageType: removeSubscription,
|
||||
ClientType: clientType,
|
||||
}
|
||||
for i := range subs {
|
||||
payload.Channels = append(payload.Channels, subs[i].Channel)
|
||||
if subs[i].Currency.IsEmpty() {
|
||||
continue
|
||||
}
|
||||
|
||||
pair := subs[i].Currency.String()
|
||||
if common.StringDataCompare(payload.MarketIDs, pair) {
|
||||
continue
|
||||
}
|
||||
payload.MarketIDs = append(payload.MarketIDs, pair)
|
||||
}
|
||||
|
||||
err := b.Websocket.Conn.SendJSONMessage(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.Websocket.RemoveSuccessfulUnsubscriptions(subs...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReSubscribeSpecificOrderbook removes the subscription and the subscribes
|
||||
// again to fetch a new snapshot in the event of a de-sync event.
|
||||
func (b *BTCMarkets) ReSubscribeSpecificOrderbook(pair currency.Pair) error {
|
||||
sub := []stream.ChannelSubscription{{
|
||||
Channel: wsOB,
|
||||
Currency: pair,
|
||||
Asset: asset.Spot,
|
||||
}}
|
||||
if err := b.Unsubscribe(sub); err != nil {
|
||||
return err
|
||||
}
|
||||
return b.Subscribe(sub)
|
||||
}
|
||||
|
||||
// checksum provides assurance on current in memory liquidity
|
||||
func checksum(ob *orderbook.Base, checksum uint32) error {
|
||||
check := crc32.ChecksumIEEE([]byte(concat(ob.Bids) + concat(ob.Asks)))
|
||||
|
||||
@@ -96,6 +96,7 @@ func (b *BTCMarkets) SetDefaults() {
|
||||
OrderbookFetching: true,
|
||||
AccountInfo: true,
|
||||
Subscribe: true,
|
||||
Unsubscribe: true,
|
||||
AuthenticatedEndpoints: true,
|
||||
GetOrders: true,
|
||||
GetOrder: true,
|
||||
@@ -166,6 +167,7 @@ func (b *BTCMarkets) Setup(exch *config.Exchange) error {
|
||||
RunningURL: wsURL,
|
||||
Connector: b.WsConnect,
|
||||
Subscriber: b.Subscribe,
|
||||
Unsubscriber: b.Unsubscribe,
|
||||
GenerateSubscriptions: b.generateDefaultSubscriptions,
|
||||
Features: &b.Features.Supports.WebsocketCapabilities,
|
||||
OrderbookBufferConfig: buffer.Config{
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
||||
)
|
||||
@@ -362,7 +361,7 @@ func (c *CoinbasePro) ProcessUpdate(update *WebsocketL2Update) error {
|
||||
}
|
||||
}
|
||||
|
||||
return c.Websocket.Orderbook.Update(&buffer.Update{
|
||||
return c.Websocket.Orderbook.Update(&orderbook.Update{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
Pair: p,
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
@@ -582,7 +581,7 @@ func (c *COINUT) WsProcessOrderbookUpdate(update *WsOrderbookUpdate) error {
|
||||
return err
|
||||
}
|
||||
|
||||
bufferUpdate := &buffer.Update{
|
||||
bufferUpdate := &orderbook.Update{
|
||||
Pair: p,
|
||||
UpdateID: update.TransID,
|
||||
Asset: asset.Spot,
|
||||
|
||||
@@ -1003,8 +1003,8 @@ func TestGetPublicOptionsTrades(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(result) != 5 {
|
||||
t.Error("limit of 5 should return 5 items")
|
||||
if len(result) > 5 {
|
||||
t.Error("limit of 5 should not exceed 5 items")
|
||||
}
|
||||
_, err = f.GetPublicOptionsTrades(context.Background(),
|
||||
time.Unix(validFTTBTCEndTime, 0), time.Unix(validFTTBTCStartTime, 0), "5")
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
@@ -477,7 +476,7 @@ func (f *FTX) wsHandleData(respRaw []byte) error {
|
||||
|
||||
// WsProcessUpdateOB processes an update on the orderbook
|
||||
func (f *FTX) WsProcessUpdateOB(data *WsOrderbookData, p currency.Pair, a asset.Item) error {
|
||||
update := buffer.Update{
|
||||
update := orderbook.Update{
|
||||
Asset: a,
|
||||
Pair: p,
|
||||
Bids: make([]orderbook.Item, len(data.Bids)),
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
||||
)
|
||||
@@ -412,7 +411,7 @@ func (g *Gateio) wsHandleData(respRaw []byte) error {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err = g.Websocket.Orderbook.Update(&buffer.Update{
|
||||
err = g.Websocket.Orderbook.Update(&orderbook.Update{
|
||||
Asks: asks,
|
||||
Bids: bids,
|
||||
Pair: p,
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
@@ -569,7 +568,7 @@ func (g *Gemini) wsProcessUpdate(result *wsL2MarketData) error {
|
||||
if len(asks) == 0 && len(bids) == 0 {
|
||||
return nil
|
||||
}
|
||||
err := g.Websocket.Orderbook.Update(&buffer.Update{
|
||||
err := g.Websocket.Orderbook.Update(&orderbook.Update{
|
||||
Asks: asks,
|
||||
Bids: bids,
|
||||
Pair: pair,
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
@@ -454,7 +453,7 @@ func (h *HitBTC) WsProcessOrderbookUpdate(update *WsOrderbook) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.Websocket.Orderbook.Update(&buffer.Update{
|
||||
return h.Websocket.Orderbook.Update(&orderbook.Update{
|
||||
Asks: asks,
|
||||
Bids: bids,
|
||||
Pair: p,
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
@@ -934,7 +933,7 @@ func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, as
|
||||
|
||||
// wsProcessOrderBookUpdate updates an orderbook entry for a given currency pair
|
||||
func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, askData, bidData []interface{}, checksum string) error {
|
||||
update := buffer.Update{
|
||||
update := orderbook.Update{
|
||||
Asset: asset.Spot,
|
||||
Pair: channelData.Pair,
|
||||
MaxDepth: krakenWsOrderbookDepth,
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
@@ -708,7 +707,7 @@ func (o *OKGroup) WsProcessPartialOrderBook(wsEventData *WebsocketOrderBook, ins
|
||||
// After merging WS data, it will sort, validate and finally update the existing
|
||||
// orderbook
|
||||
func (o *OKGroup) WsProcessUpdateOrderbook(wsEventData *WebsocketOrderBook, instrument currency.Pair, a asset.Item) error {
|
||||
update := buffer.Update{
|
||||
update := orderbook.Update{
|
||||
Asset: a,
|
||||
Pair: instrument,
|
||||
UpdateTime: wsEventData.Timestamp,
|
||||
|
||||
@@ -563,10 +563,12 @@ var stringsToOrderSide = []struct {
|
||||
{"ask", Ask, nil},
|
||||
{"ASK", Ask, nil},
|
||||
{"aSk", Ask, nil},
|
||||
{"lOnG", Long, nil},
|
||||
{"ShoRt", Short, nil},
|
||||
{"any", AnySide, nil},
|
||||
{"ANY", AnySide, nil},
|
||||
{"aNy", AnySide, nil},
|
||||
{"woahMan", Buy, errors.New("woahMan not recognised as order side")},
|
||||
{"woahMan", Buy, errors.New("WOAHMAN not recognised as order side")},
|
||||
}
|
||||
|
||||
func TestStringToOrderSide(t *testing.T) {
|
||||
@@ -585,6 +587,16 @@ func TestStringToOrderSide(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
var sideBenchmark Side
|
||||
|
||||
// 9756914 126.7 ns/op 0 B/op 0 allocs/op // PREV
|
||||
// 25200660 57.63 ns/op 3 B/op 1 allocs/op // CURRENT
|
||||
func BenchmarkStringToOrderSide(b *testing.B) {
|
||||
for x := 0; x < b.N; x++ {
|
||||
sideBenchmark, _ = StringToOrderSide("any")
|
||||
}
|
||||
}
|
||||
|
||||
var stringsToOrderType = []struct {
|
||||
in string
|
||||
out Type
|
||||
@@ -619,7 +631,7 @@ var stringsToOrderType = []struct {
|
||||
{"trigger", Trigger, nil},
|
||||
{"TRIGGER", Trigger, nil},
|
||||
{"tRiGgEr", Trigger, nil},
|
||||
{"woahMan", UnknownType, errors.New("woahMan not recognised as order type")},
|
||||
{"woahMan", UnknownType, errors.New("WOAHMAN not recognised as order type")},
|
||||
}
|
||||
|
||||
func TestStringToOrderType(t *testing.T) {
|
||||
@@ -638,6 +650,16 @@ func TestStringToOrderType(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
var typeBenchmark Type
|
||||
|
||||
// 5703705 299.9 ns/op 0 B/op 0 allocs/op // PREV
|
||||
// 16353608 81.23 ns/op 8 B/op 1 allocs/op // CURRENT
|
||||
func BenchmarkStringToOrderType(b *testing.B) {
|
||||
for x := 0; x < b.N; x++ {
|
||||
typeBenchmark, _ = StringToOrderType("trigger")
|
||||
}
|
||||
}
|
||||
|
||||
var stringsToOrderStatus = []struct {
|
||||
in string
|
||||
out Status
|
||||
@@ -682,7 +704,8 @@ var stringsToOrderStatus = []struct {
|
||||
{"PARTIALLY_CANCELLEd", PartiallyCancelled, nil},
|
||||
{"partially canceLLed", PartiallyCancelled, nil},
|
||||
{"opeN", Open, nil},
|
||||
{"woahMan", UnknownStatus, errors.New("woahMan not recognised as order status")},
|
||||
{"cLosEd", Closed, nil},
|
||||
{"woahMan", UnknownStatus, errors.New("WOAHMAN not recognised as order status")},
|
||||
}
|
||||
|
||||
func TestStringToOrderStatus(t *testing.T) {
|
||||
@@ -701,6 +724,16 @@ func TestStringToOrderStatus(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
var statusBenchmark Status
|
||||
|
||||
// 3569052 351.8 ns/op 0 B/op 0 allocs/op // PREV
|
||||
// 11126791 101.9 ns/op 24 B/op 1 allocs/op // CURRENT
|
||||
func BenchmarkStringToOrderStatus(b *testing.B) {
|
||||
for x := 0; x < b.N; x++ {
|
||||
statusBenchmark, _ = StringToOrderStatus("market_unavailable")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateOrderFromModify(t *testing.T) {
|
||||
var leet = "1337"
|
||||
od := Detail{
|
||||
|
||||
@@ -734,20 +734,21 @@ func SortOrdersBySide(orders *[]Detail, reverse bool) {
|
||||
// StringToOrderSide for converting case insensitive order side
|
||||
// and returning a real Side
|
||||
func StringToOrderSide(side string) (Side, error) {
|
||||
switch {
|
||||
case strings.EqualFold(side, Buy.String()):
|
||||
side = strings.ToUpper(side)
|
||||
switch Side(side) {
|
||||
case Buy:
|
||||
return Buy, nil
|
||||
case strings.EqualFold(side, Sell.String()):
|
||||
case Sell:
|
||||
return Sell, nil
|
||||
case strings.EqualFold(side, Bid.String()):
|
||||
case Bid:
|
||||
return Bid, nil
|
||||
case strings.EqualFold(side, Ask.String()):
|
||||
case Ask:
|
||||
return Ask, nil
|
||||
case strings.EqualFold(side, Long.String()):
|
||||
case Long:
|
||||
return Long, nil
|
||||
case strings.EqualFold(side, Short.String()):
|
||||
case Short:
|
||||
return Short, nil
|
||||
case strings.EqualFold(side, AnySide.String()):
|
||||
case AnySide:
|
||||
return AnySide, nil
|
||||
default:
|
||||
return UnknownSide, errors.New(side + " not recognised as order side")
|
||||
@@ -757,40 +758,29 @@ func StringToOrderSide(side string) (Side, error) {
|
||||
// StringToOrderType for converting case insensitive order type
|
||||
// and returning a real Type
|
||||
func StringToOrderType(oType string) (Type, error) {
|
||||
switch {
|
||||
case strings.EqualFold(oType, Limit.String()),
|
||||
strings.EqualFold(oType, "EXCHANGE LIMIT"):
|
||||
oType = strings.ToUpper(oType)
|
||||
switch oType {
|
||||
case Limit.String(), "EXCHANGE LIMIT":
|
||||
return Limit, nil
|
||||
case strings.EqualFold(oType, Market.String()),
|
||||
strings.EqualFold(oType, "EXCHANGE MARKET"):
|
||||
case Market.String(), "EXCHANGE MARKET":
|
||||
return Market, nil
|
||||
case strings.EqualFold(oType, ImmediateOrCancel.String()),
|
||||
strings.EqualFold(oType, "immediate or cancel"),
|
||||
strings.EqualFold(oType, "IOC"),
|
||||
strings.EqualFold(oType, "EXCHANGE IOC"):
|
||||
case ImmediateOrCancel.String(), "IMMEDIATE OR CANCEL", "IOC", "EXCHANGE IOC":
|
||||
return ImmediateOrCancel, nil
|
||||
case strings.EqualFold(oType, Stop.String()),
|
||||
strings.EqualFold(oType, "stop loss"),
|
||||
strings.EqualFold(oType, "stop_loss"),
|
||||
strings.EqualFold(oType, "EXCHANGE STOP"):
|
||||
case Stop.String(), "STOP LOSS", "STOP_LOSS", "EXCHANGE STOP":
|
||||
return Stop, nil
|
||||
case strings.EqualFold(oType, StopLimit.String()),
|
||||
strings.EqualFold(oType, "EXCHANGE STOP LIMIT"):
|
||||
case StopLimit.String(), "EXCHANGE STOP LIMIT":
|
||||
return StopLimit, nil
|
||||
case strings.EqualFold(oType, TrailingStop.String()),
|
||||
strings.EqualFold(oType, "trailing stop"),
|
||||
strings.EqualFold(oType, "EXCHANGE TRAILING STOP"):
|
||||
case TrailingStop.String(), "TRAILING STOP", "EXCHANGE TRAILING STOP":
|
||||
return TrailingStop, nil
|
||||
case strings.EqualFold(oType, FillOrKill.String()),
|
||||
strings.EqualFold(oType, "EXCHANGE FOK"):
|
||||
case FillOrKill.String(), "EXCHANGE FOK":
|
||||
return FillOrKill, nil
|
||||
case strings.EqualFold(oType, IOS.String()):
|
||||
case IOS.String():
|
||||
return IOS, nil
|
||||
case strings.EqualFold(oType, PostOnly.String()):
|
||||
case PostOnly.String():
|
||||
return PostOnly, nil
|
||||
case strings.EqualFold(oType, AnyType.String()):
|
||||
case AnyType.String():
|
||||
return AnyType, nil
|
||||
case strings.EqualFold(oType, Trigger.String()):
|
||||
case Trigger.String():
|
||||
return Trigger, nil
|
||||
default:
|
||||
return UnknownType, errors.New(oType + " not recognised as order type")
|
||||
@@ -800,49 +790,37 @@ func StringToOrderType(oType string) (Type, error) {
|
||||
// StringToOrderStatus for converting case insensitive order status
|
||||
// and returning a real Status
|
||||
func StringToOrderStatus(status string) (Status, error) {
|
||||
switch {
|
||||
case strings.EqualFold(status, AnyStatus.String()):
|
||||
status = strings.ToUpper(status)
|
||||
switch status {
|
||||
case AnyStatus.String():
|
||||
return AnyStatus, nil
|
||||
case strings.EqualFold(status, New.String()),
|
||||
strings.EqualFold(status, "placed"):
|
||||
case New.String(), "PLACED":
|
||||
return New, nil
|
||||
case strings.EqualFold(status, Active.String()),
|
||||
strings.EqualFold(status, "STATUS_ACTIVE"): // BTSE case
|
||||
case Active.String(), "STATUS_ACTIVE":
|
||||
return Active, nil
|
||||
case strings.EqualFold(status, PartiallyFilled.String()),
|
||||
strings.EqualFold(status, "partially matched"),
|
||||
strings.EqualFold(status, "partially filled"):
|
||||
case PartiallyFilled.String(), "PARTIALLY MATCHED", "PARTIALLY FILLED":
|
||||
return PartiallyFilled, nil
|
||||
case strings.EqualFold(status, Filled.String()),
|
||||
strings.EqualFold(status, "fully matched"),
|
||||
strings.EqualFold(status, "fully filled"),
|
||||
strings.EqualFold(status, "ORDER_FULLY_TRANSACTED"): // BTSE case
|
||||
case Filled.String(), "FULLY MATCHED", "FULLY FILLED", "ORDER_FULLY_TRANSACTED":
|
||||
return Filled, nil
|
||||
case strings.EqualFold(status, PartiallyCancelled.String()),
|
||||
strings.EqualFold(status, "partially cancelled"),
|
||||
strings.EqualFold(status, "ORDER_PARTIALLY_TRANSACTED"): // BTSE case
|
||||
case PartiallyCancelled.String(), "PARTIALLY CANCELLED", "ORDER_PARTIALLY_TRANSACTED":
|
||||
return PartiallyCancelled, nil
|
||||
case strings.EqualFold(status, Open.String()):
|
||||
case Open.String():
|
||||
return Open, nil
|
||||
case strings.EqualFold(status, Closed.String()):
|
||||
case Closed.String():
|
||||
return Closed, nil
|
||||
case strings.EqualFold(status, Cancelled.String()),
|
||||
strings.EqualFold(status, "CANCELED"), // Binance and Kraken case
|
||||
strings.EqualFold(status, "ORDER_CANCELLED"): // BTSE case
|
||||
case Cancelled.String(), "CANCELED", "ORDER_CANCELLED":
|
||||
return Cancelled, nil
|
||||
case strings.EqualFold(status, PendingCancel.String()),
|
||||
strings.EqualFold(status, "pending cancel"),
|
||||
strings.EqualFold(status, "pending cancellation"):
|
||||
case PendingCancel.String(), "PENDING CANCEL", "PENDING CANCELLATION":
|
||||
return PendingCancel, nil
|
||||
case strings.EqualFold(status, Rejected.String()):
|
||||
case Rejected.String():
|
||||
return Rejected, nil
|
||||
case strings.EqualFold(status, Expired.String()):
|
||||
case Expired.String():
|
||||
return Expired, nil
|
||||
case strings.EqualFold(status, Hidden.String()):
|
||||
case Hidden.String():
|
||||
return Hidden, nil
|
||||
case strings.EqualFold(status, InsufficientBalance.String()):
|
||||
case InsufficientBalance.String():
|
||||
return InsufficientBalance, nil
|
||||
case strings.EqualFold(status, MarketUnavailable.String()):
|
||||
case MarketUnavailable.String():
|
||||
return MarketUnavailable, nil
|
||||
default:
|
||||
return UnknownStatus, errors.New(status + " not recognised as order status")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package orderbook
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -10,6 +12,21 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrOrderbookInvalid defines an error for when the orderbook is invalid and
|
||||
// should not be trusted
|
||||
ErrOrderbookInvalid = errors.New("orderbook data integrity compromised")
|
||||
// ErrInvalidAction defines and error when an action is invalid
|
||||
ErrInvalidAction = errors.New("invalid action")
|
||||
)
|
||||
|
||||
// Outbound restricts outbound usage of depth. NOTE: Type assert to
|
||||
// *orderbook.Depth or alternatively retrieve orderbook.Unsafe type to access
|
||||
// underlying linked list.
|
||||
type Outbound interface {
|
||||
Retrieve() (*Base, error)
|
||||
}
|
||||
|
||||
// Depth defines a linked list of orderbook items
|
||||
type Depth struct {
|
||||
asks
|
||||
@@ -21,9 +38,13 @@ type Depth struct {
|
||||
alert.Notice
|
||||
|
||||
mux *dispatch.Mux
|
||||
id uuid.UUID
|
||||
_ID uuid.UUID
|
||||
|
||||
options
|
||||
|
||||
// validationError defines current book state and why it was invalidated.
|
||||
validationError error
|
||||
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
@@ -31,38 +52,46 @@ type Depth struct {
|
||||
func NewDepth(id uuid.UUID) *Depth {
|
||||
return &Depth{
|
||||
stack: newStack(),
|
||||
id: id,
|
||||
_ID: id,
|
||||
mux: service.Mux,
|
||||
}
|
||||
}
|
||||
|
||||
// Publish alerts any subscribed routines using a dispatch mux
|
||||
func (d *Depth) Publish() {
|
||||
err := d.mux.Publish([]uuid.UUID{d.id}, d.Retrieve())
|
||||
if err != nil {
|
||||
if err := d.mux.Publish(Outbound(d), d._ID); err != nil {
|
||||
log.Errorf(log.ExchangeSys, "Cannot publish orderbook update to mux %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetAskLength returns length of asks
|
||||
func (d *Depth) GetAskLength() int {
|
||||
func (d *Depth) GetAskLength() (int, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
return d.asks.length
|
||||
if d.validationError != nil {
|
||||
return 0, d.validationError
|
||||
}
|
||||
return d.asks.length, nil
|
||||
}
|
||||
|
||||
// GetBidLength returns length of bids
|
||||
func (d *Depth) GetBidLength() int {
|
||||
func (d *Depth) GetBidLength() (int, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
return d.bids.length
|
||||
if d.validationError != nil {
|
||||
return 0, d.validationError
|
||||
}
|
||||
return d.bids.length, nil
|
||||
}
|
||||
|
||||
// Retrieve returns the orderbook base a copy of the underlying linked list
|
||||
// spread
|
||||
func (d *Depth) Retrieve() *Base {
|
||||
func (d *Depth) Retrieve() (*Base, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if d.validationError != nil {
|
||||
return nil, d.validationError
|
||||
}
|
||||
return &Base{
|
||||
Bids: d.bids.retrieve(),
|
||||
Asks: d.asks.retrieve(),
|
||||
@@ -74,23 +103,31 @@ func (d *Depth) Retrieve() *Base {
|
||||
PriceDuplication: d.priceDuplication,
|
||||
IsFundingRate: d.isFundingRate,
|
||||
VerifyOrderbook: d.VerifyOrderbook,
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TotalBidAmounts returns the total amount of bids and the total orderbook
|
||||
// bids value
|
||||
func (d *Depth) TotalBidAmounts() (liquidity, value float64) {
|
||||
func (d *Depth) TotalBidAmounts() (liquidity, value float64, err error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
return d.bids.amount()
|
||||
if d.validationError != nil {
|
||||
return 0, 0, d.validationError
|
||||
}
|
||||
liquidity, value = d.bids.amount()
|
||||
return liquidity, value, nil
|
||||
}
|
||||
|
||||
// TotalAskAmounts returns the total amount of asks and the total orderbook
|
||||
// asks value
|
||||
func (d *Depth) TotalAskAmounts() (liquidity, value float64) {
|
||||
func (d *Depth) TotalAskAmounts() (liquidity, value float64, err error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
return d.asks.amount()
|
||||
if d.validationError != nil {
|
||||
return 0, 0, d.validationError
|
||||
}
|
||||
liquidity, value = d.asks.amount()
|
||||
return liquidity, value, nil
|
||||
}
|
||||
|
||||
// LoadSnapshot flushes the bids and asks with a snapshot
|
||||
@@ -101,138 +138,136 @@ 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.validationError = nil
|
||||
d.Alert()
|
||||
d.m.Unlock()
|
||||
}
|
||||
|
||||
// Flush flushes the bid and ask depths
|
||||
func (d *Depth) Flush() {
|
||||
d.m.Lock()
|
||||
// invalidate flushes all values back to zero so as to not allow strategy
|
||||
// traversal on compromised data. NOTE: This requires locking.
|
||||
func (d *Depth) invalidate(withReason error) error {
|
||||
d.lastUpdateID = 0
|
||||
d.lastUpdated = time.Time{}
|
||||
d.bids.load(nil, d.stack)
|
||||
d.asks.load(nil, d.stack)
|
||||
d.validationError = fmt.Errorf("%s %s %s %w Reason: [%v]",
|
||||
d.exchange,
|
||||
d.pair,
|
||||
d.asset,
|
||||
ErrOrderbookInvalid,
|
||||
withReason)
|
||||
d.Alert()
|
||||
return d.validationError
|
||||
}
|
||||
|
||||
// Invalidate flushes all values back to zero so as to not allow strategy
|
||||
// traversal on compromised data.
|
||||
func (d *Depth) Invalidate(withReason error) error {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
return d.invalidate(withReason)
|
||||
}
|
||||
|
||||
// IsValid returns if the underlying book is valid.
|
||||
func (d *Depth) IsValid() bool {
|
||||
d.m.Lock()
|
||||
valid := d.validationError == nil
|
||||
d.m.Unlock()
|
||||
return valid
|
||||
}
|
||||
|
||||
// UpdateBidAskByPrice updates the bid and ask spread by supplied updates, this
|
||||
// will trim total length of depth level to a specified supplied number
|
||||
func (d *Depth) UpdateBidAskByPrice(bidUpdts, askUpdts Items, maxDepth int, lastUpdateID int64, lastUpdated time.Time) {
|
||||
if len(bidUpdts) == 0 && len(askUpdts) == 0 {
|
||||
return
|
||||
}
|
||||
d.m.Lock()
|
||||
d.lastUpdateID = lastUpdateID
|
||||
d.lastUpdated = lastUpdated
|
||||
func (d *Depth) UpdateBidAskByPrice(update *Update) {
|
||||
tn := getNow()
|
||||
if len(bidUpdts) != 0 {
|
||||
d.bids.updateInsertByPrice(bidUpdts, d.stack, maxDepth, tn)
|
||||
d.m.Lock()
|
||||
if len(update.Bids) != 0 {
|
||||
d.bids.updateInsertByPrice(update.Bids, d.stack, update.MaxDepth, tn)
|
||||
}
|
||||
if len(askUpdts) != 0 {
|
||||
d.asks.updateInsertByPrice(askUpdts, d.stack, maxDepth, tn)
|
||||
if len(update.Asks) != 0 {
|
||||
d.asks.updateInsertByPrice(update.Asks, d.stack, update.MaxDepth, tn)
|
||||
}
|
||||
d.Alert()
|
||||
d.updateAndAlert(update)
|
||||
d.m.Unlock()
|
||||
}
|
||||
|
||||
// UpdateBidAskByID amends details by ID
|
||||
func (d *Depth) UpdateBidAskByID(bidUpdts, askUpdts Items, lastUpdateID int64, lastUpdated time.Time) error {
|
||||
if len(bidUpdts) == 0 && len(askUpdts) == 0 {
|
||||
return nil
|
||||
}
|
||||
func (d *Depth) UpdateBidAskByID(update *Update) error {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if len(bidUpdts) != 0 {
|
||||
err := d.bids.updateByID(bidUpdts)
|
||||
if len(update.Bids) != 0 {
|
||||
err := d.bids.updateByID(update.Bids)
|
||||
if err != nil {
|
||||
return err
|
||||
return d.invalidate(err)
|
||||
}
|
||||
}
|
||||
if len(askUpdts) != 0 {
|
||||
err := d.asks.updateByID(askUpdts)
|
||||
if len(update.Asks) != 0 {
|
||||
err := d.asks.updateByID(update.Asks)
|
||||
if err != nil {
|
||||
return err
|
||||
return d.invalidate(err)
|
||||
}
|
||||
}
|
||||
d.lastUpdateID = lastUpdateID
|
||||
d.lastUpdated = lastUpdated
|
||||
d.Alert()
|
||||
d.updateAndAlert(update)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteBidAskByID deletes a price level by ID
|
||||
func (d *Depth) DeleteBidAskByID(bidUpdts, askUpdts Items, bypassErr bool, lastUpdateID int64, lastUpdated time.Time) error {
|
||||
if len(bidUpdts) == 0 && len(askUpdts) == 0 {
|
||||
return nil
|
||||
}
|
||||
func (d *Depth) DeleteBidAskByID(update *Update, bypassErr bool) error {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if len(bidUpdts) != 0 {
|
||||
err := d.bids.deleteByID(bidUpdts, d.stack, bypassErr)
|
||||
if len(update.Bids) != 0 {
|
||||
err := d.bids.deleteByID(update.Bids, d.stack, bypassErr)
|
||||
if err != nil {
|
||||
return err
|
||||
return d.invalidate(err)
|
||||
}
|
||||
}
|
||||
if len(askUpdts) != 0 {
|
||||
err := d.asks.deleteByID(askUpdts, d.stack, bypassErr)
|
||||
if len(update.Asks) != 0 {
|
||||
err := d.asks.deleteByID(update.Asks, d.stack, bypassErr)
|
||||
if err != nil {
|
||||
return err
|
||||
return d.invalidate(err)
|
||||
}
|
||||
}
|
||||
d.lastUpdateID = lastUpdateID
|
||||
d.lastUpdated = lastUpdated
|
||||
d.Alert()
|
||||
d.updateAndAlert(update)
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertBidAskByID inserts new updates
|
||||
func (d *Depth) InsertBidAskByID(bidUpdts, askUpdts Items, lastUpdateID int64, lastUpdated time.Time) error {
|
||||
if len(bidUpdts) == 0 && len(askUpdts) == 0 {
|
||||
return nil
|
||||
}
|
||||
func (d *Depth) InsertBidAskByID(update *Update) error {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if len(bidUpdts) != 0 {
|
||||
err := d.bids.insertUpdates(bidUpdts, d.stack)
|
||||
if len(update.Bids) != 0 {
|
||||
err := d.bids.insertUpdates(update.Bids, d.stack)
|
||||
if err != nil {
|
||||
return err
|
||||
return d.invalidate(err)
|
||||
}
|
||||
}
|
||||
if len(askUpdts) != 0 {
|
||||
err := d.asks.insertUpdates(askUpdts, d.stack)
|
||||
if len(update.Asks) != 0 {
|
||||
err := d.asks.insertUpdates(update.Asks, d.stack)
|
||||
if err != nil {
|
||||
return err
|
||||
return d.invalidate(err)
|
||||
}
|
||||
}
|
||||
d.lastUpdateID = lastUpdateID
|
||||
d.lastUpdated = lastUpdated
|
||||
d.Alert()
|
||||
d.updateAndAlert(update)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateInsertByID updates or inserts by ID at current price level.
|
||||
func (d *Depth) UpdateInsertByID(bidUpdts, askUpdts Items, lastUpdateID int64, lastUpdated time.Time) error {
|
||||
if len(bidUpdts) == 0 && len(askUpdts) == 0 {
|
||||
return nil
|
||||
}
|
||||
func (d *Depth) UpdateInsertByID(update *Update) error {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if len(bidUpdts) != 0 {
|
||||
err := d.bids.updateInsertByID(bidUpdts, d.stack)
|
||||
if len(update.Bids) != 0 {
|
||||
err := d.bids.updateInsertByID(update.Bids, d.stack)
|
||||
if err != nil {
|
||||
return err
|
||||
return d.invalidate(err)
|
||||
}
|
||||
}
|
||||
if len(askUpdts) != 0 {
|
||||
err := d.asks.updateInsertByID(askUpdts, d.stack)
|
||||
if len(update.Asks) != 0 {
|
||||
err := d.asks.updateInsertByID(update.Asks, d.stack)
|
||||
if err != nil {
|
||||
return err
|
||||
return d.invalidate(err)
|
||||
}
|
||||
}
|
||||
d.Alert()
|
||||
d.lastUpdateID = lastUpdateID
|
||||
d.lastUpdated = lastUpdated
|
||||
d.updateAndAlert(update)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -261,18 +296,24 @@ func (d *Depth) GetName() string {
|
||||
return d.exchange
|
||||
}
|
||||
|
||||
// IsRestSnapshot returns if the depth item was updated via REST
|
||||
func (d *Depth) IsRestSnapshot() bool {
|
||||
// IsRESTSnapshot returns if the depth item was updated via REST
|
||||
func (d *Depth) IsRESTSnapshot() (bool, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
return d.restSnapshot
|
||||
if d.validationError != nil {
|
||||
return false, d.validationError
|
||||
}
|
||||
return d.restSnapshot, nil
|
||||
}
|
||||
|
||||
// LastUpdateID returns the last Update ID
|
||||
func (d *Depth) LastUpdateID() int64 {
|
||||
func (d *Depth) LastUpdateID() (int64, error) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
return d.lastUpdateID
|
||||
if d.validationError != nil {
|
||||
return 0, d.validationError
|
||||
}
|
||||
return d.lastUpdateID, nil
|
||||
}
|
||||
|
||||
// IsFundingRate returns if the depth is a funding rate
|
||||
@@ -281,3 +322,11 @@ func (d *Depth) IsFundingRate() bool {
|
||||
defer d.m.Unlock()
|
||||
return d.isFundingRate
|
||||
}
|
||||
|
||||
// updateAndAlert updates the last updated ID and when it was updated to the
|
||||
// recent update. Then alerts all pending routines. NOTE: This requires locking.
|
||||
func (d *Depth) updateAndAlert(update *Update) {
|
||||
d.lastUpdateID = update.UpdateID
|
||||
d.lastUpdated = update.UpdateTime
|
||||
d.Alert()
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package orderbook
|
||||
import (
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -11,33 +12,77 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
)
|
||||
|
||||
var id, _ = uuid.NewV4()
|
||||
var id = uuid.Must(uuid.NewV4())
|
||||
|
||||
func TestGetLength(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := NewDepth(id)
|
||||
if d.GetAskLength() != 0 {
|
||||
t.Errorf("expected len %v, but received %v", 0, d.GetAskLength())
|
||||
err := d.Invalidate(nil)
|
||||
if !errors.Is(err, ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderbookInvalid)
|
||||
}
|
||||
_, err = d.GetAskLength()
|
||||
if !errors.Is(err, ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderbookInvalid)
|
||||
}
|
||||
|
||||
d.LoadSnapshot([]Item{{Price: 1337}}, nil, 0, time.Time{}, true)
|
||||
|
||||
askLen, err := d.GetAskLength()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if askLen != 0 {
|
||||
t.Errorf("expected len %v, but received %v", 0, askLen)
|
||||
}
|
||||
|
||||
d.asks.load([]Item{{Price: 1337}}, d.stack)
|
||||
|
||||
if d.GetAskLength() != 1 {
|
||||
t.Errorf("expected len %v, but received %v", 1, d.GetAskLength())
|
||||
askLen, err = d.GetAskLength()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if askLen != 1 {
|
||||
t.Errorf("expected len %v, but received %v", 1, askLen)
|
||||
}
|
||||
|
||||
d = NewDepth(id)
|
||||
if d.GetBidLength() != 0 {
|
||||
t.Errorf("expected len %v, but received %v", 0, d.GetBidLength())
|
||||
err = d.Invalidate(nil)
|
||||
if !errors.Is(err, ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderbookInvalid)
|
||||
}
|
||||
_, err = d.GetBidLength()
|
||||
if !errors.Is(err, ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderbookInvalid)
|
||||
}
|
||||
|
||||
d.LoadSnapshot(nil, []Item{{Price: 1337}}, 0, time.Time{}, true)
|
||||
|
||||
bidLen, err := d.GetBidLength()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if bidLen != 0 {
|
||||
t.Errorf("expected len %v, but received %v", 0, bidLen)
|
||||
}
|
||||
|
||||
d.bids.load([]Item{{Price: 1337}}, d.stack)
|
||||
|
||||
if d.GetBidLength() != 1 {
|
||||
t.Errorf("expected len %v, but received %v", 1, d.GetBidLength())
|
||||
bidLen, err = d.GetBidLength()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if bidLen != 1 {
|
||||
t.Errorf("expected len %v, but received %v", 1, bidLen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieve(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := NewDepth(id)
|
||||
d.asks.load([]Item{{Price: 1337}}, d.stack)
|
||||
d.bids.load([]Item{{Price: 1337}}, d.stack)
|
||||
@@ -64,20 +109,40 @@ func TestRetrieve(t *testing.T) {
|
||||
mirrored.Type().Field(n).Name)
|
||||
}
|
||||
}
|
||||
theBigD := d.Retrieve()
|
||||
if len(theBigD.Asks) != 1 {
|
||||
t.Errorf("expected len %v, but received %v", 1, len(theBigD.Bids))
|
||||
|
||||
ob, err := d.Retrieve()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if len(theBigD.Bids) != 1 {
|
||||
t.Errorf("expected len %v, but received %v", 1, len(theBigD.Bids))
|
||||
if len(ob.Asks) != 1 {
|
||||
t.Errorf("expected len %v, but received %v", 1, len(ob.Bids))
|
||||
}
|
||||
|
||||
if len(ob.Bids) != 1 {
|
||||
t.Errorf("expected len %v, but received %v", 1, len(ob.Bids))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTotalAmounts(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := NewDepth(id)
|
||||
|
||||
liquidity, value := d.TotalBidAmounts()
|
||||
err := d.Invalidate(nil)
|
||||
if !errors.Is(err, ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderbookInvalid)
|
||||
}
|
||||
_, _, err = d.TotalBidAmounts()
|
||||
if !errors.Is(err, ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderbookInvalid)
|
||||
}
|
||||
|
||||
d.validationError = nil
|
||||
liquidity, value, err := d.TotalBidAmounts()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if liquidity != 0 || value != 0 {
|
||||
t.Fatalf("liquidity expected %f received %f value expected %f received %f",
|
||||
0.,
|
||||
@@ -86,7 +151,23 @@ func TestTotalAmounts(t *testing.T) {
|
||||
value)
|
||||
}
|
||||
|
||||
liquidity, value = d.TotalAskAmounts()
|
||||
err = d.Invalidate(nil)
|
||||
if !errors.Is(err, ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderbookInvalid)
|
||||
}
|
||||
|
||||
_, _, err = d.TotalAskAmounts()
|
||||
if !errors.Is(err, ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderbookInvalid)
|
||||
}
|
||||
|
||||
d.validationError = nil
|
||||
|
||||
liquidity, value, err = d.TotalAskAmounts()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if liquidity != 0 || value != 0 {
|
||||
t.Fatalf("liquidity expected %f received %f value expected %f received %f",
|
||||
0.,
|
||||
@@ -98,7 +179,11 @@ func TestTotalAmounts(t *testing.T) {
|
||||
d.asks.load([]Item{{Price: 1337, Amount: 1}}, d.stack)
|
||||
d.bids.load([]Item{{Price: 1337, Amount: 10}}, d.stack)
|
||||
|
||||
liquidity, value = d.TotalBidAmounts()
|
||||
liquidity, value, err = d.TotalBidAmounts()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if liquidity != 10 || value != 13370 {
|
||||
t.Fatalf("liquidity expected %f received %f value expected %f received %f",
|
||||
10.,
|
||||
@@ -107,7 +192,11 @@ func TestTotalAmounts(t *testing.T) {
|
||||
value)
|
||||
}
|
||||
|
||||
liquidity, value = d.TotalAskAmounts()
|
||||
liquidity, value, err = d.TotalAskAmounts()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if liquidity != 1 || value != 1337 {
|
||||
t.Fatalf("liquidity expected %f received %f value expected %f received %f",
|
||||
1.,
|
||||
@@ -118,131 +207,303 @@ func TestTotalAmounts(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestLoadSnapshot(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := NewDepth(id)
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1}}, Items{{Price: 1337, Amount: 10}}, 0, time.Time{}, false)
|
||||
if d.Retrieve().Asks[0].Price != 1337 || d.Retrieve().Bids[0].Price != 1337 {
|
||||
t.Fatal("not set")
|
||||
|
||||
ob, err := d.Retrieve()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if ob.Asks[0].Price != 1337 || ob.Bids[0].Price != 1337 {
|
||||
t.Fatalf("not set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlush(t *testing.T) {
|
||||
func TestInvalidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := NewDepth(id)
|
||||
d.exchange = "testexchange"
|
||||
d.pair = currency.NewPair(currency.BTC, currency.WABI)
|
||||
d.asset = asset.Spot
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1}}, Items{{Price: 1337, Amount: 10}}, 0, time.Time{}, false)
|
||||
d.Flush()
|
||||
if len(d.Retrieve().Asks) != 0 || len(d.Retrieve().Bids) != 0 {
|
||||
t.Fatal("not flushed")
|
||||
|
||||
ob, err := d.Retrieve()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1}}, Items{{Price: 1337, Amount: 10}}, 0, time.Time{}, false)
|
||||
d.Flush()
|
||||
if len(d.Retrieve().Asks) != 0 || len(d.Retrieve().Bids) != 0 {
|
||||
t.Fatal("not flushed")
|
||||
|
||||
if ob == nil {
|
||||
t.Fatalf("unexpected value")
|
||||
}
|
||||
|
||||
err = d.Invalidate(errors.New("random reason"))
|
||||
if !errors.Is(err, ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderbookInvalid)
|
||||
}
|
||||
|
||||
_, err = d.Retrieve()
|
||||
if !errors.Is(err, ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderbookInvalid)
|
||||
}
|
||||
|
||||
if err.Error() != "testexchange BTCWABI spot orderbook data integrity compromised Reason: [random reason]" {
|
||||
t.Fatal("unexpected string return")
|
||||
}
|
||||
|
||||
d.validationError = nil
|
||||
|
||||
ob, err = d.Retrieve()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if len(ob.Asks) != 0 || len(ob.Bids) != 0 {
|
||||
t.Fatalf("not flushed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateBidAskByPrice(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := NewDepth(id)
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Time{}, false)
|
||||
|
||||
// empty
|
||||
d.UpdateBidAskByPrice(nil, nil, 0, 1, time.Time{})
|
||||
d.UpdateBidAskByPrice(&Update{})
|
||||
|
||||
d.UpdateBidAskByPrice(Items{{Price: 1337, Amount: 2, ID: 1}}, Items{{Price: 1337, Amount: 2, ID: 2}}, 0, 1, time.Time{})
|
||||
if d.Retrieve().Asks[0].Amount != 2 || d.Retrieve().Bids[0].Amount != 2 {
|
||||
t.Fatal("orderbook amounts not updated correctly")
|
||||
updates := &Update{
|
||||
Bids: Items{{Price: 1337, Amount: 2, ID: 1}},
|
||||
Asks: Items{{Price: 1337, Amount: 2, ID: 2}},
|
||||
UpdateID: 1,
|
||||
}
|
||||
d.UpdateBidAskByPrice(Items{{Price: 1337, Amount: 0, ID: 1}}, Items{{Price: 1337, Amount: 0, ID: 2}}, 0, 2, time.Time{})
|
||||
if d.GetAskLength() != 0 || d.GetBidLength() != 0 {
|
||||
t.Fatal("orderbook amounts not updated correctly")
|
||||
d.UpdateBidAskByPrice(updates)
|
||||
|
||||
ob, err := d.Retrieve()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if ob.Asks[0].Amount != 2 || ob.Bids[0].Amount != 2 {
|
||||
t.Fatalf("orderbook amounts not updated correctly")
|
||||
}
|
||||
|
||||
updates = &Update{
|
||||
Bids: Items{{Price: 1337, Amount: 0, ID: 1}},
|
||||
Asks: Items{{Price: 1337, Amount: 0, ID: 2}},
|
||||
UpdateID: 2,
|
||||
}
|
||||
d.UpdateBidAskByPrice(updates)
|
||||
|
||||
askLen, err := d.GetAskLength()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
bidLen, err := d.GetBidLength()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if askLen != 0 || bidLen != 0 {
|
||||
t.Fatalf("orderbook amounts not updated correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteBidAskByID(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := NewDepth(id)
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Time{}, false)
|
||||
err := d.DeleteBidAskByID(Items{{Price: 1337, Amount: 2, ID: 1}}, Items{{Price: 1337, Amount: 2, ID: 2}}, false, 0, time.Time{})
|
||||
|
||||
updates := &Update{
|
||||
Bids: Items{{Price: 1337, Amount: 2, ID: 1}},
|
||||
Asks: Items{{Price: 1337, Amount: 2, ID: 2}},
|
||||
}
|
||||
err := d.DeleteBidAskByID(updates, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(d.Retrieve().Asks) != 0 || len(d.Retrieve().Bids) != 0 {
|
||||
t.Fatal("items not deleted")
|
||||
|
||||
ob, err := d.Retrieve()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
err = d.DeleteBidAskByID(Items{{Price: 1337, Amount: 2, ID: 1}}, nil, false, 0, time.Time{})
|
||||
if !errors.Is(err, errIDCannotBeMatched) {
|
||||
if len(ob.Asks) != 0 || len(ob.Bids) != 0 {
|
||||
t.Fatalf("items not deleted")
|
||||
}
|
||||
|
||||
updates = &Update{
|
||||
Bids: Items{{Price: 1337, Amount: 2, ID: 1}},
|
||||
}
|
||||
err = d.DeleteBidAskByID(updates, false)
|
||||
if !strings.Contains(err.Error(), errIDCannotBeMatched.Error()) {
|
||||
t.Fatalf("error expected %v received %v", errIDCannotBeMatched, err)
|
||||
}
|
||||
|
||||
err = d.DeleteBidAskByID(nil, Items{{Price: 1337, Amount: 2, ID: 2}}, false, 0, time.Time{})
|
||||
if !errors.Is(err, errIDCannotBeMatched) {
|
||||
updates = &Update{
|
||||
Asks: Items{{Price: 1337, Amount: 2, ID: 2}},
|
||||
}
|
||||
err = d.DeleteBidAskByID(updates, false)
|
||||
if !strings.Contains(err.Error(), errIDCannotBeMatched.Error()) {
|
||||
t.Fatalf("error expected %v received %v", errIDCannotBeMatched, err)
|
||||
}
|
||||
|
||||
err = d.DeleteBidAskByID(nil, Items{{Price: 1337, Amount: 2, ID: 2}}, true, 0, time.Time{})
|
||||
updates = &Update{
|
||||
Asks: Items{{Price: 1337, Amount: 2, ID: 2}},
|
||||
}
|
||||
err = d.DeleteBidAskByID(updates, true)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("error expected %v received %v", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateBidAskByID(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := NewDepth(id)
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Time{}, false)
|
||||
err := d.UpdateBidAskByID(Items{{Price: 1337, Amount: 2, ID: 1}}, Items{{Price: 1337, Amount: 2, ID: 2}}, 0, time.Time{})
|
||||
|
||||
updates := &Update{
|
||||
Bids: Items{{Price: 1337, Amount: 2, ID: 1}},
|
||||
Asks: Items{{Price: 1337, Amount: 2, ID: 2}},
|
||||
}
|
||||
err := d.UpdateBidAskByID(updates)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if d.Retrieve().Asks[0].Amount != 2 || d.Retrieve().Bids[0].Amount != 2 {
|
||||
t.Fatal("orderbook amounts not updated correctly")
|
||||
|
||||
ob, err := d.Retrieve()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if ob.Asks[0].Amount != 2 || ob.Bids[0].Amount != 2 {
|
||||
t.Fatalf("orderbook amounts not updated correctly")
|
||||
}
|
||||
|
||||
updates = &Update{
|
||||
Bids: Items{{Price: 1337, Amount: 2, ID: 666}},
|
||||
}
|
||||
// random unmatching IDs
|
||||
err = d.UpdateBidAskByID(Items{{Price: 1337, Amount: 2, ID: 666}}, nil, 0, time.Time{})
|
||||
if !errors.Is(err, errIDCannotBeMatched) {
|
||||
err = d.UpdateBidAskByID(updates)
|
||||
if !strings.Contains(err.Error(), errIDCannotBeMatched.Error()) {
|
||||
t.Fatalf("error expected %v received %v", errIDCannotBeMatched, err)
|
||||
}
|
||||
|
||||
err = d.UpdateBidAskByID(nil, Items{{Price: 1337, Amount: 2, ID: 69}}, 0, time.Time{})
|
||||
if !errors.Is(err, errIDCannotBeMatched) {
|
||||
updates = &Update{
|
||||
Asks: Items{{Price: 1337, Amount: 2, ID: 69}},
|
||||
}
|
||||
err = d.UpdateBidAskByID(updates)
|
||||
if !strings.Contains(err.Error(), errIDCannotBeMatched.Error()) {
|
||||
t.Fatalf("error expected %v received %v", errIDCannotBeMatched, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertBidAskByID(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := NewDepth(id)
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Time{}, false)
|
||||
err := d.InsertBidAskByID(Items{{Price: 1338, Amount: 2, ID: 3}}, Items{{Price: 1336, Amount: 2, ID: 4}}, 0, time.Time{})
|
||||
|
||||
updates := &Update{
|
||||
Asks: Items{{Price: 1337, Amount: 2, ID: 3}},
|
||||
}
|
||||
|
||||
err := d.InsertBidAskByID(updates)
|
||||
if !strings.Contains(err.Error(), errCollisionDetected.Error()) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errCollisionDetected)
|
||||
}
|
||||
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Time{}, false)
|
||||
|
||||
updates = &Update{
|
||||
Bids: Items{{Price: 1337, Amount: 2, ID: 3}},
|
||||
}
|
||||
|
||||
err = d.InsertBidAskByID(updates)
|
||||
if !strings.Contains(err.Error(), errCollisionDetected.Error()) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errCollisionDetected)
|
||||
}
|
||||
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Time{}, false)
|
||||
updates = &Update{
|
||||
Bids: Items{{Price: 1338, Amount: 2, ID: 3}},
|
||||
Asks: Items{{Price: 1336, Amount: 2, ID: 4}},
|
||||
}
|
||||
err = d.InsertBidAskByID(updates)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(d.Retrieve().Asks) != 2 || len(d.Retrieve().Bids) != 2 {
|
||||
t.Fatal("items not added correctly")
|
||||
|
||||
ob, err := d.Retrieve()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if len(ob.Asks) != 2 || len(ob.Bids) != 2 {
|
||||
t.Fatalf("items not added correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateInsertByID(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := NewDepth(id)
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Time{}, false)
|
||||
|
||||
err := d.UpdateInsertByID(Items{{Price: 1338, Amount: 0, ID: 3}}, Items{{Price: 1336, Amount: 2, ID: 4}}, 0, time.Time{})
|
||||
if !errors.Is(err, errAmountCannotBeLessOrEqualToZero) {
|
||||
updates := &Update{
|
||||
Bids: Items{{Price: 1338, Amount: 0, ID: 3}},
|
||||
Asks: Items{{Price: 1336, Amount: 2, ID: 4}},
|
||||
}
|
||||
err := d.UpdateInsertByID(updates)
|
||||
if !strings.Contains(err.Error(), errAmountCannotBeLessOrEqualToZero.Error()) {
|
||||
t.Fatalf("expected: %v but received: %v", errAmountCannotBeLessOrEqualToZero, err)
|
||||
}
|
||||
|
||||
err = d.UpdateInsertByID(Items{{Price: 1338, Amount: 2, ID: 3}}, Items{{Price: 1336, Amount: 0, ID: 4}}, 0, time.Time{})
|
||||
if !errors.Is(err, errAmountCannotBeLessOrEqualToZero) {
|
||||
// Above will invalidate the book
|
||||
_, err = d.Retrieve()
|
||||
if !errors.Is(err, ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderbookInvalid)
|
||||
}
|
||||
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Time{}, false)
|
||||
|
||||
updates = &Update{
|
||||
Bids: Items{{Price: 1338, Amount: 2, ID: 3}},
|
||||
Asks: Items{{Price: 1336, Amount: 0, ID: 4}},
|
||||
}
|
||||
err = d.UpdateInsertByID(updates)
|
||||
if !strings.Contains(err.Error(), errAmountCannotBeLessOrEqualToZero.Error()) {
|
||||
t.Fatalf("expected: %v but received: %v", errAmountCannotBeLessOrEqualToZero, err)
|
||||
}
|
||||
|
||||
err = d.UpdateInsertByID(Items{{Price: 1338, Amount: 2, ID: 3}}, Items{{Price: 1336, Amount: 2, ID: 4}}, 0, time.Time{})
|
||||
// Above will invalidate the book
|
||||
_, err = d.Retrieve()
|
||||
if !errors.Is(err, ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderbookInvalid)
|
||||
}
|
||||
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}}, 0, time.Time{}, false)
|
||||
|
||||
updates = &Update{
|
||||
Bids: Items{{Price: 1338, Amount: 2, ID: 3}},
|
||||
Asks: Items{{Price: 1336, Amount: 2, ID: 4}},
|
||||
}
|
||||
err = d.UpdateInsertByID(updates)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(d.Retrieve().Asks) != 2 || len(d.Retrieve().Bids) != 2 {
|
||||
t.Fatal("items not added correctly")
|
||||
ob, err := d.Retrieve()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if len(ob.Asks) != 2 || len(ob.Bids) != 2 {
|
||||
t.Fatalf("items not added correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssignOptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := Depth{}
|
||||
cp := currency.NewPair(currency.LINK, currency.BTC)
|
||||
tn := time.Now()
|
||||
@@ -269,43 +530,97 @@ func TestAssignOptions(t *testing.T) {
|
||||
!d.VerifyOrderbook ||
|
||||
!d.restSnapshot ||
|
||||
!d.idAligned {
|
||||
t.Fatal("failed to set correctly")
|
||||
t.Fatalf("failed to set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetName(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := Depth{}
|
||||
d.exchange = "test"
|
||||
if d.GetName() != "test" {
|
||||
t.Fatal("failed to get correct value")
|
||||
t.Fatalf("failed to get correct value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsRestSnapshot(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := Depth{}
|
||||
d.restSnapshot = true
|
||||
if !d.IsRestSnapshot() {
|
||||
t.Fatal("failed to set correctly")
|
||||
err := d.Invalidate(nil)
|
||||
if !errors.Is(err, ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderbookInvalid)
|
||||
}
|
||||
_, err = d.IsRESTSnapshot()
|
||||
if !errors.Is(err, ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderbookInvalid)
|
||||
}
|
||||
|
||||
d.validationError = nil
|
||||
b, err := d.IsRESTSnapshot()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if !b {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", b, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastUpdateID(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := Depth{}
|
||||
err := d.Invalidate(nil)
|
||||
if !errors.Is(err, ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderbookInvalid)
|
||||
}
|
||||
_, err = d.LastUpdateID()
|
||||
if !errors.Is(err, ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderbookInvalid)
|
||||
}
|
||||
|
||||
d.validationError = nil
|
||||
d.lastUpdateID = 1337
|
||||
if d.LastUpdateID() != 1337 {
|
||||
t.Fatal("failed to get correct value")
|
||||
id, err := d.LastUpdateID()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if id != 1337 {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", id, 1337)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsFundingRate(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := Depth{}
|
||||
d.isFundingRate = true
|
||||
if !d.IsFundingRate() {
|
||||
t.Fatal("failed to get correct value")
|
||||
t.Fatalf("failed to get correct value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublish(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := Depth{}
|
||||
if err := d.Invalidate(nil); !errors.Is(err, ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderbookInvalid)
|
||||
}
|
||||
d.Publish()
|
||||
d.validationError = nil
|
||||
d.Publish()
|
||||
}
|
||||
|
||||
func TestIsValid(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := Depth{}
|
||||
if !d.IsValid() {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", d.IsValid(), true)
|
||||
}
|
||||
if err := d.Invalidate(nil); !errors.Is(err, ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, ErrOrderbookInvalid)
|
||||
}
|
||||
if d.IsValid() {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", d.IsValid(), false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ updates:
|
||||
tip.Value.Amount = updts[x].Amount
|
||||
continue updates
|
||||
}
|
||||
return fmt.Errorf("update error: %w %d not found",
|
||||
return fmt.Errorf("update error: %w ID: %d not found",
|
||||
errIDCannotBeMatched,
|
||||
updts[x].ID)
|
||||
}
|
||||
@@ -156,11 +156,9 @@ func (ll *linkedList) amount() (liquidity, value float64) {
|
||||
|
||||
// retrieve returns a full slice of contents from the linked list
|
||||
func (ll *linkedList) retrieve() Items {
|
||||
depth := make(Items, ll.length)
|
||||
iterator := 0
|
||||
depth := make(Items, 0, ll.length)
|
||||
for tip := ll.head; tip != nil; tip = tip.Next {
|
||||
depth[iterator] = tip.Value
|
||||
iterator++
|
||||
depth = append(depth, tip.Value)
|
||||
}
|
||||
return depth
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/dispatch"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
@@ -81,7 +80,7 @@ func (s *Service) Update(b *Base) error {
|
||||
}
|
||||
book.LoadSnapshot(b.Bids, b.Asks, b.LastUpdateID, b.LastUpdated, true)
|
||||
s.mu.Unlock()
|
||||
return s.Mux.Publish([]uuid.UUID{m1.ID}, book.Retrieve())
|
||||
return s.Mux.Publish(book, m1.ID)
|
||||
}
|
||||
|
||||
// DeployDepth used for subsystem deployment creates a depth item in the struct
|
||||
@@ -194,7 +193,7 @@ func (s *Service) Retrieve(exchange string, p currency.Pair, a asset.Item) (*Bas
|
||||
errCannotFindOrderbook,
|
||||
p.Quote)
|
||||
}
|
||||
return book.Retrieve(), nil
|
||||
return book.Retrieve()
|
||||
}
|
||||
|
||||
// TotalBidsAmount returns the total amount of bids and the total orderbook
|
||||
|
||||
@@ -36,7 +36,7 @@ var (
|
||||
|
||||
var service = Service{
|
||||
books: make(map[string]Exchange),
|
||||
Mux: dispatch.GetNewMux(),
|
||||
Mux: dispatch.GetNewMux(nil),
|
||||
}
|
||||
|
||||
// Service provides a store for difference exchange orderbooks
|
||||
@@ -115,3 +115,36 @@ type options struct {
|
||||
restSnapshot bool
|
||||
idAligned bool
|
||||
}
|
||||
|
||||
// Action defines a set of differing states required to implement an incoming
|
||||
// orderbook update used in conjunction with UpdateEntriesByID
|
||||
type Action uint8
|
||||
|
||||
const (
|
||||
// Amend applies amount adjustment by ID
|
||||
Amend Action = iota + 1
|
||||
// Delete removes price level from book by ID
|
||||
Delete
|
||||
// Insert adds price level to book
|
||||
Insert
|
||||
// UpdateInsert on conflict applies amount adjustment or appends new amount
|
||||
// to book
|
||||
UpdateInsert
|
||||
)
|
||||
|
||||
// Update and things and stuff
|
||||
type Update struct {
|
||||
UpdateID int64 // Used when no time is provided
|
||||
UpdateTime time.Time
|
||||
Asset asset.Item
|
||||
Action
|
||||
Bids []Item
|
||||
Asks []Item
|
||||
Pair currency.Pair
|
||||
// Checksum defines the expected value when the books have been verified
|
||||
Checksum uint32
|
||||
// Determines if there is a max depth of orderbooks and after an append we
|
||||
// should remove any items that are outside of this scope. Kraken is the
|
||||
// only exchange utilising this field.
|
||||
MaxDepth int
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
@@ -499,7 +498,7 @@ func (p *Poloniex) WsProcessOrderbookUpdate(sequenceNumber float64, data []inter
|
||||
if !ok {
|
||||
return fmt.Errorf("%w buysell not float64", errTypeAssertionFailure)
|
||||
}
|
||||
update := &buffer.Update{
|
||||
update := &orderbook.Update{
|
||||
Pair: pair,
|
||||
Asset: asset.Spot,
|
||||
UpdateID: int64(sequenceNumber),
|
||||
|
||||
@@ -24,6 +24,13 @@ var (
|
||||
errUpdateNoTargets = errors.New("update bid/ask targets cannot be nil")
|
||||
errDepthNotFound = errors.New("orderbook depth not found")
|
||||
errRESTOverwrite = errors.New("orderbook has been overwritten by REST protocol")
|
||||
errInvalidAction = errors.New("invalid action")
|
||||
errAmendFailure = errors.New("orderbook amend update failure")
|
||||
errDeleteFailure = errors.New("orderbook delete update failure")
|
||||
errInsertFailure = errors.New("orderbook insert update failure")
|
||||
errUpdateInsertFailure = errors.New("orderbook update/insert update failure")
|
||||
errRESTTimerLapse = errors.New("rest sync timer lapse with active websocket connection")
|
||||
errOrderbookFlushed = errors.New("orderbook flushed")
|
||||
)
|
||||
|
||||
// Setup sets private variables
|
||||
@@ -68,7 +75,7 @@ func (w *Orderbook) Setup(exchangeConfig *config.Exchange, c *Config, dataHandle
|
||||
}
|
||||
|
||||
// validate validates update against setup values
|
||||
func (w *Orderbook) validate(u *Update) error {
|
||||
func (w *Orderbook) validate(u *orderbook.Update) error {
|
||||
if u == nil {
|
||||
return fmt.Errorf(packageError, errUpdateIsNil)
|
||||
}
|
||||
@@ -80,7 +87,7 @@ func (w *Orderbook) validate(u *Update) error {
|
||||
|
||||
// Update updates a stored pointer to an orderbook.Depth struct containing a
|
||||
// linked list, this switches between the usage of a buffered update
|
||||
func (w *Orderbook) Update(u *Update) error {
|
||||
func (w *Orderbook) Update(u *orderbook.Update) error {
|
||||
if err := w.validate(u); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -110,8 +117,26 @@ func (w *Orderbook) Update(u *Update) error {
|
||||
// Checks for when the rest protocol overwrites a streaming dominated book
|
||||
// will stop updating book via incremental updates. This occurs because our
|
||||
// sync manager (engine/sync.go) timer has elapsed for streaming. Usually
|
||||
// because the book is highly illiquid. TODO: Book resubscribe on websocket.
|
||||
if book.ob.IsRestSnapshot() {
|
||||
// because the book is highly illiquid.
|
||||
isREST, err := book.ob.IsRESTSnapshot()
|
||||
if err != nil {
|
||||
if !errors.Is(err, orderbook.ErrOrderbookInvalid) {
|
||||
return err
|
||||
}
|
||||
// In the event a checksum or processing error invalidates the book, all
|
||||
// updates that could be stored in the websocket buffer, skip applying
|
||||
// until a new snapshot comes through.
|
||||
if w.verbose {
|
||||
log.Warnf(log.WebsocketMgr,
|
||||
"Exchange %s CurrencyPair: %s AssetType: %s underlying book is invalid, cannot apply update.",
|
||||
w.exchangeName,
|
||||
u.Pair,
|
||||
u.Asset)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if isREST {
|
||||
if w.verbose {
|
||||
log.Warnf(log.WebsocketMgr,
|
||||
"%s for Exchange %s CurrencyPair: %s AssetType: %s consider extending synctimeoutwebsocket",
|
||||
@@ -120,15 +145,16 @@ func (w *Orderbook) Update(u *Update) error {
|
||||
u.Pair,
|
||||
u.Asset)
|
||||
}
|
||||
return fmt.Errorf("%w for Exchange %s CurrencyPair: %s AssetType: %s",
|
||||
errRESTOverwrite,
|
||||
w.exchangeName,
|
||||
u.Pair,
|
||||
u.Asset)
|
||||
// Instance of illiquidity, this signal notifies that there is websocket
|
||||
// activity. We can invalidate the book and request a new snapshot. All
|
||||
// further updates through the websocket should be caught above in the
|
||||
// IsRestSnapshot() call.
|
||||
return book.ob.Invalidate(errRESTTimerLapse)
|
||||
}
|
||||
|
||||
if w.bufferEnabled {
|
||||
processed, err := w.processBufferUpdate(book, u)
|
||||
var processed bool
|
||||
processed, err = w.processBufferUpdate(book, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -137,55 +163,54 @@ func (w *Orderbook) Update(u *Update) error {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
err := w.processObUpdate(book, u)
|
||||
err = w.processObUpdate(book, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if book.ob.VerifyOrderbook { // This is used here so as to not retrieve
|
||||
// book if verification is off.
|
||||
// On every update, this will retrieve and verify orderbook depths
|
||||
err := book.ob.Retrieve().Verify()
|
||||
var ret *orderbook.Base
|
||||
if book.ob.VerifyOrderbook {
|
||||
// This is used here so as to not retrieve book if verification is off.
|
||||
// On every update, this will retrieve and verify orderbook depth.
|
||||
ret, err = book.ob.Retrieve()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// a nil ticker means that a zero publish period has been requested,
|
||||
// this means publish now whatever was received with no throttling
|
||||
if book.ticker == nil {
|
||||
go func() {
|
||||
w.dataHandler <- book.ob.Retrieve()
|
||||
book.ob.Publish()
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
select {
|
||||
case <-book.ticker.C:
|
||||
// Opted to wait for receiver because we are limiting here and the sync
|
||||
// manager requires update
|
||||
go func() {
|
||||
w.dataHandler <- book.ob.Retrieve()
|
||||
book.ob.Publish()
|
||||
}()
|
||||
default:
|
||||
// We do not need to send an update to the sync manager within this time
|
||||
// window unless verbose is turned on
|
||||
if w.verbose {
|
||||
w.dataHandler <- book.ob.Retrieve()
|
||||
book.ob.Publish()
|
||||
err = ret.Verify()
|
||||
if err != nil {
|
||||
return book.ob.Invalidate(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Publish all state changes, disregarding verbosity or sync requirements.
|
||||
book.ob.Publish()
|
||||
|
||||
if book.ticker != nil {
|
||||
select {
|
||||
case <-book.ticker.C:
|
||||
// Send update to engine websocket manager to update engine
|
||||
// sync manager to reset websocket orderbook sync timeout. This will
|
||||
// stop the fall over to REST protocol fetching of orderbook data.
|
||||
default:
|
||||
if !w.verbose {
|
||||
// We do not need to send an update to the sync manager within
|
||||
// this time window unless verbose is turned on.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A nil ticker means that a zero publish period has been set and the entire
|
||||
// websocket updates will be sent to the engine websocket manager for
|
||||
// display purposes. Same as being verbose.
|
||||
w.dataHandler <- book.ob
|
||||
return nil
|
||||
}
|
||||
|
||||
// processBufferUpdate stores update into buffer, when buffer at capacity as
|
||||
// defined by w.obBufferLimit it well then sort and apply updates.
|
||||
func (w *Orderbook) processBufferUpdate(o *orderbookHolder, u *Update) (bool, error) {
|
||||
func (w *Orderbook) processBufferUpdate(o *orderbookHolder, u *orderbook.Update) (bool, error) {
|
||||
*o.buffer = append(*o.buffer, *u)
|
||||
if len(*o.buffer) < w.obBufferLimit {
|
||||
return false, nil
|
||||
@@ -216,16 +241,20 @@ func (w *Orderbook) processBufferUpdate(o *orderbookHolder, u *Update) (bool, er
|
||||
|
||||
// processObUpdate processes updates either by its corresponding id or by
|
||||
// price level
|
||||
func (w *Orderbook) processObUpdate(o *orderbookHolder, u *Update) error {
|
||||
func (w *Orderbook) processObUpdate(o *orderbookHolder, u *orderbook.Update) error {
|
||||
if w.updateEntriesByID {
|
||||
return o.updateByIDAndAction(u)
|
||||
}
|
||||
o.updateByPrice(u)
|
||||
if w.checksum != nil {
|
||||
err := w.checksum(o.ob.Retrieve(), u.Checksum)
|
||||
compare, err := o.ob.Retrieve()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = w.checksum(compare, u.Checksum)
|
||||
if err != nil {
|
||||
return o.ob.Invalidate(err)
|
||||
}
|
||||
o.updateID = u.UpdateID
|
||||
}
|
||||
return nil
|
||||
@@ -233,48 +262,50 @@ func (w *Orderbook) processObUpdate(o *orderbookHolder, u *Update) error {
|
||||
|
||||
// updateByPrice ammends amount if match occurs by price, deletes if amount is
|
||||
// zero or less and inserts if not found.
|
||||
func (o *orderbookHolder) updateByPrice(updts *Update) {
|
||||
o.ob.UpdateBidAskByPrice(updts.Bids,
|
||||
updts.Asks,
|
||||
updts.MaxDepth,
|
||||
updts.UpdateID,
|
||||
updts.UpdateTime)
|
||||
func (o *orderbookHolder) updateByPrice(updts *orderbook.Update) {
|
||||
o.ob.UpdateBidAskByPrice(updts)
|
||||
}
|
||||
|
||||
// updateByIDAndAction will receive an action to execute against the orderbook
|
||||
// it will then match by IDs instead of price to perform the action
|
||||
func (o *orderbookHolder) updateByIDAndAction(updts *Update) error {
|
||||
func (o *orderbookHolder) updateByIDAndAction(updts *orderbook.Update) error {
|
||||
switch updts.Action {
|
||||
case Amend:
|
||||
return o.ob.UpdateBidAskByID(updts.Bids,
|
||||
updts.Asks,
|
||||
updts.UpdateID,
|
||||
updts.UpdateTime)
|
||||
case Delete:
|
||||
case orderbook.Amend:
|
||||
err := o.ob.UpdateBidAskByID(updts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v %w", errAmendFailure, err)
|
||||
}
|
||||
case orderbook.Delete:
|
||||
// edge case for Bitfinex as their streaming endpoint duplicates deletes
|
||||
bypassErr := o.ob.GetName() == "Bitfinex" && o.ob.IsFundingRate()
|
||||
return o.ob.DeleteBidAskByID(updts.Bids,
|
||||
updts.Asks,
|
||||
bypassErr,
|
||||
updts.UpdateID,
|
||||
updts.UpdateTime)
|
||||
case Insert:
|
||||
return o.ob.InsertBidAskByID(updts.Bids,
|
||||
updts.Asks,
|
||||
updts.UpdateID,
|
||||
updts.UpdateTime)
|
||||
case UpdateInsert:
|
||||
return o.ob.UpdateInsertByID(updts.Bids,
|
||||
updts.Asks,
|
||||
updts.UpdateID,
|
||||
updts.UpdateTime)
|
||||
err := o.ob.DeleteBidAskByID(updts, bypassErr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v %w", errDeleteFailure, err)
|
||||
}
|
||||
case orderbook.Insert:
|
||||
err := o.ob.InsertBidAskByID(updts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v %w", errInsertFailure, err)
|
||||
}
|
||||
case orderbook.UpdateInsert:
|
||||
err := o.ob.UpdateInsertByID(updts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v %w", errUpdateInsertFailure, err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid action [%s]", updts.Action)
|
||||
return fmt.Errorf("%w [%d]", errInvalidAction, updts.Action)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadSnapshot loads initial snapshot of orderbook data from websocket
|
||||
func (w *Orderbook) LoadSnapshot(book *orderbook.Base) error {
|
||||
// Checks if book can deploy to linked list
|
||||
err := book.Verify()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.m.Lock()
|
||||
defer w.m.Unlock()
|
||||
m1, ok := w.ob[book.Pair.Base]
|
||||
@@ -290,12 +321,13 @@ func (w *Orderbook) LoadSnapshot(book *orderbook.Base) error {
|
||||
holder, ok := m2[book.Asset]
|
||||
if !ok {
|
||||
// Associate orderbook pointer with local exchange depth map
|
||||
depth, err := orderbook.DeployDepth(book.Exchange, book.Pair, book.Asset)
|
||||
var depth *orderbook.Depth
|
||||
depth, err = orderbook.DeployDepth(book.Exchange, book.Pair, book.Asset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
depth.AssignOptions(book)
|
||||
buffer := make([]Update, w.obBufferLimit)
|
||||
buffer := make([]orderbook.Update, w.obBufferLimit)
|
||||
|
||||
var ticker *time.Ticker
|
||||
if w.publishPeriod != 0 {
|
||||
@@ -311,31 +343,28 @@ func (w *Orderbook) LoadSnapshot(book *orderbook.Base) error {
|
||||
|
||||
holder.updateID = book.LastUpdateID
|
||||
|
||||
// Checks if book can deploy to linked list
|
||||
err := book.Verify()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
holder.ob.LoadSnapshot(book.Bids,
|
||||
book.Asks,
|
||||
book.LastUpdateID,
|
||||
book.LastUpdated,
|
||||
false,
|
||||
)
|
||||
false)
|
||||
|
||||
if holder.ob.VerifyOrderbook { // This is used here so as to not retrieve
|
||||
// book if verification is off.
|
||||
if holder.ob.VerifyOrderbook {
|
||||
// This is used here so as to not retrieve book if verification is off.
|
||||
// Checks to see if orderbook snapshot that was deployed has not been
|
||||
// altered in any way
|
||||
err = holder.ob.Retrieve().Verify()
|
||||
book, err = holder.ob.Retrieve()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = book.Verify()
|
||||
if err != nil {
|
||||
return holder.ob.Invalidate(err)
|
||||
}
|
||||
}
|
||||
|
||||
w.dataHandler <- holder.ob.Retrieve()
|
||||
holder.ob.Publish()
|
||||
w.dataHandler <- holder.ob
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -351,7 +380,7 @@ func (w *Orderbook) GetOrderbook(p currency.Pair, a asset.Item) (*orderbook.Base
|
||||
a,
|
||||
errDepthNotFound)
|
||||
}
|
||||
return book.ob.Retrieve(), nil
|
||||
return book.ob.Retrieve()
|
||||
}
|
||||
|
||||
// FlushBuffer flushes w.ob data to be garbage collected and refreshed when a
|
||||
@@ -374,6 +403,7 @@ func (w *Orderbook) FlushOrderbook(p currency.Pair, a asset.Item) error {
|
||||
a,
|
||||
errDepthNotFound)
|
||||
}
|
||||
book.ob.Flush()
|
||||
// error not needed in this return
|
||||
_ = book.ob.Invalidate(errOrderbookFlushed)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package buffer
|
||||
import (
|
||||
"errors"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -44,9 +45,14 @@ func createSnapshot() (holder *Orderbook, asks, bids orderbook.Items, err error)
|
||||
|
||||
newBook := make(map[currency.Code]map[currency.Code]map[asset.Item]*orderbookHolder)
|
||||
|
||||
ch := make(chan interface{})
|
||||
go func(<-chan interface{}) { // reader
|
||||
for range ch {
|
||||
}
|
||||
}(ch)
|
||||
holder = &Orderbook{
|
||||
exchangeName: exchangeName,
|
||||
dataHandler: make(chan interface{}, 100),
|
||||
dataHandler: ch,
|
||||
ob: newBook,
|
||||
}
|
||||
err = holder.LoadSnapshot(book)
|
||||
@@ -78,7 +84,7 @@ func BenchmarkUpdateBidsByPrice(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
bidAsks := bidAskGenerator()
|
||||
update := &Update{
|
||||
update := &orderbook.Update{
|
||||
Bids: bidAsks,
|
||||
Asks: bidAsks,
|
||||
Pair: cp,
|
||||
@@ -98,7 +104,7 @@ func BenchmarkUpdateAsksByPrice(b *testing.B) {
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
bidAsks := bidAskGenerator()
|
||||
update := &Update{
|
||||
update := &orderbook.Update{
|
||||
Bids: bidAsks,
|
||||
Asks: bidAsks,
|
||||
Pair: cp,
|
||||
@@ -112,14 +118,14 @@ func BenchmarkUpdateAsksByPrice(b *testing.B) {
|
||||
|
||||
// BenchmarkBufferPerformance demonstrates buffer more performant than multi
|
||||
// process calls
|
||||
// 4219518 287 ns/op 176 B/op 1 allocs/op
|
||||
// 890016 1688 ns/op 416 B/op 3 allocs/op
|
||||
func BenchmarkBufferPerformance(b *testing.B) {
|
||||
holder, asks, bids, err := createSnapshot()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
holder.bufferEnabled = true
|
||||
update := &Update{
|
||||
update := &orderbook.Update{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
Pair: cp,
|
||||
@@ -139,7 +145,7 @@ func BenchmarkBufferPerformance(b *testing.B) {
|
||||
}
|
||||
|
||||
// BenchmarkBufferSortingPerformance benchmark
|
||||
// 2693391 467 ns/op 208 B/op 2 allocs/op
|
||||
// 613964 2093 ns/op 440 B/op 4 allocs/op
|
||||
func BenchmarkBufferSortingPerformance(b *testing.B) {
|
||||
holder, asks, bids, err := createSnapshot()
|
||||
if err != nil {
|
||||
@@ -147,7 +153,7 @@ func BenchmarkBufferSortingPerformance(b *testing.B) {
|
||||
}
|
||||
holder.bufferEnabled = true
|
||||
holder.sortBuffer = true
|
||||
update := &Update{
|
||||
update := &orderbook.Update{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
Pair: cp,
|
||||
@@ -167,7 +173,7 @@ func BenchmarkBufferSortingPerformance(b *testing.B) {
|
||||
}
|
||||
|
||||
// BenchmarkBufferSortingPerformance benchmark
|
||||
// 1000000 1019 ns/op 208 B/op 2 allocs/op
|
||||
// 914500 1599 ns/op 440 B/op 4 allocs/op
|
||||
func BenchmarkBufferSortingByIDPerformance(b *testing.B) {
|
||||
holder, asks, bids, err := createSnapshot()
|
||||
if err != nil {
|
||||
@@ -176,7 +182,7 @@ func BenchmarkBufferSortingByIDPerformance(b *testing.B) {
|
||||
holder.bufferEnabled = true
|
||||
holder.sortBuffer = true
|
||||
holder.sortBufferByUpdateIDs = true
|
||||
update := &Update{
|
||||
update := &orderbook.Update{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
Pair: cp,
|
||||
@@ -197,13 +203,15 @@ func BenchmarkBufferSortingByIDPerformance(b *testing.B) {
|
||||
|
||||
// BenchmarkNoBufferPerformance demonstrates orderbook process more performant
|
||||
// than buffer
|
||||
// 9516966 141 ns/op 0 B/op 0 allocs/op
|
||||
// 122659 12792 ns/op 972 B/op 7 allocs/op PRIOR
|
||||
// 1225924 1028 ns/op 240 B/op 2 allocs/op CURRENT
|
||||
|
||||
func BenchmarkNoBufferPerformance(b *testing.B) {
|
||||
obl, asks, bids, err := createSnapshot()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
update := &Update{
|
||||
update := &orderbook.Update{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
Pair: cp,
|
||||
@@ -211,6 +219,7 @@ func BenchmarkNoBufferPerformance(b *testing.B) {
|
||||
Asset: asset.Spot,
|
||||
}
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
randomIndex := rand.Intn(4) // nolint:gosec // no need to import crypo/rand for testing
|
||||
update.Asks = itemArray[randomIndex]
|
||||
@@ -229,7 +238,7 @@ func TestUpdates(t *testing.T) {
|
||||
}
|
||||
|
||||
book := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
||||
book.updateByPrice(&Update{
|
||||
book.updateByPrice(&orderbook.Update{
|
||||
Bids: itemArray[5],
|
||||
Asks: itemArray[5],
|
||||
Pair: cp,
|
||||
@@ -240,7 +249,7 @@ func TestUpdates(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
book.updateByPrice(&Update{
|
||||
book.updateByPrice(&orderbook.Update{
|
||||
Bids: itemArray[0],
|
||||
Asks: itemArray[0],
|
||||
Pair: cp,
|
||||
@@ -251,7 +260,12 @@ func TestUpdates(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if book.ob.GetAskLength() != 3 {
|
||||
askLen, err := book.ob.GetAskLength()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if askLen != 3 {
|
||||
t.Error("Did not update")
|
||||
}
|
||||
}
|
||||
@@ -267,7 +281,7 @@ func TestHittingTheBuffer(t *testing.T) {
|
||||
for i := range itemArray {
|
||||
asks := itemArray[i]
|
||||
bids := itemArray[i]
|
||||
err = holder.Update(&Update{
|
||||
err = holder.Update(&orderbook.Update{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
Pair: cp,
|
||||
@@ -280,11 +294,22 @@ func TestHittingTheBuffer(t *testing.T) {
|
||||
}
|
||||
|
||||
book := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
||||
if book.ob.GetAskLength() != 3 {
|
||||
t.Errorf("expected 3 entries, received: %v", book.ob.GetAskLength())
|
||||
askLen, err := book.ob.GetAskLength()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
if book.ob.GetBidLength() != 3 {
|
||||
t.Errorf("expected 3 entries, received: %v", book.ob.GetBidLength())
|
||||
|
||||
if askLen != 3 {
|
||||
t.Errorf("expected 3 entries, received: %v", askLen)
|
||||
}
|
||||
|
||||
bidLen, err := book.ob.GetBidLength()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if bidLen != 3 {
|
||||
t.Errorf("expected 3 entries, received: %v", bidLen)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,13 +328,13 @@ func TestInsertWithIDs(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
bids := itemArray[i]
|
||||
err = holder.Update(&Update{
|
||||
err = holder.Update(&orderbook.Update{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
Pair: cp,
|
||||
UpdateTime: time.Now(),
|
||||
Asset: asset.Spot,
|
||||
Action: UpdateInsert,
|
||||
Action: orderbook.UpdateInsert,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -317,11 +342,20 @@ func TestInsertWithIDs(t *testing.T) {
|
||||
}
|
||||
|
||||
book := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
||||
if book.ob.GetAskLength() != 6 {
|
||||
t.Errorf("expected 5 entries, received: %v", book.ob.GetAskLength())
|
||||
askLen, err := book.ob.GetAskLength()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
if book.ob.GetBidLength() != 6 {
|
||||
t.Errorf("expected 5 entries, received: %v", book.ob.GetBidLength())
|
||||
if askLen != 6 {
|
||||
t.Errorf("expected 6 entries, received: %v", askLen)
|
||||
}
|
||||
|
||||
bidLen, err := book.ob.GetBidLength()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
if bidLen != 6 {
|
||||
t.Errorf("expected 6 entries, received: %v", bidLen)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,7 +372,7 @@ func TestSortIDs(t *testing.T) {
|
||||
for i := range itemArray {
|
||||
asks := itemArray[i]
|
||||
bids := itemArray[i]
|
||||
err = holder.Update(&Update{
|
||||
err = holder.Update(&orderbook.Update{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
Pair: cp,
|
||||
@@ -350,11 +384,20 @@ func TestSortIDs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
book := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
||||
if book.ob.GetAskLength() != 3 {
|
||||
t.Errorf("expected 3 entries, received: %v", book.ob.GetAskLength())
|
||||
askLen, err := book.ob.GetAskLength()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
if book.ob.GetAskLength() != 3 {
|
||||
t.Errorf("expected 3 entries, received: %v", book.ob.GetAskLength())
|
||||
if askLen != 3 {
|
||||
t.Errorf("expected 3 entries, received: %v", askLen)
|
||||
}
|
||||
|
||||
bidLen, err := book.ob.GetBidLength()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
if bidLen != 3 {
|
||||
t.Errorf("expected 3 entries, received: %v", bidLen)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,7 +417,7 @@ func TestOutOfOrderIDs(t *testing.T) {
|
||||
holder.obBufferLimit = 5
|
||||
for i := range itemArray {
|
||||
asks := itemArray[i]
|
||||
err = holder.Update(&Update{
|
||||
err = holder.Update(&orderbook.Update{
|
||||
Asks: asks,
|
||||
Pair: cp,
|
||||
UpdateID: outOFOrderIDs[i],
|
||||
@@ -385,15 +428,16 @@ func TestOutOfOrderIDs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
book := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
||||
cpy := book.ob.Retrieve()
|
||||
cpy, err := book.ob.Retrieve()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
// Index 1 since index 0 is price 7000
|
||||
if cpy.Asks[1].Price != 2000 {
|
||||
t.Errorf("expected sorted price to be 2000, received: %v", cpy.Asks[1].Price)
|
||||
}
|
||||
}
|
||||
|
||||
var errTest = errors.New("test error")
|
||||
|
||||
func TestOrderbookLastUpdateID(t *testing.T) {
|
||||
holder, _, _, err := createSnapshot()
|
||||
if err != nil {
|
||||
@@ -404,16 +448,22 @@ func TestOrderbookLastUpdateID(t *testing.T) {
|
||||
exp, itemArray[1][0].Price)
|
||||
}
|
||||
|
||||
holder.checksum = func(state *orderbook.Base, checksum uint32) error { return errTest }
|
||||
holder.checksum = func(state *orderbook.Base, checksum uint32) error { return errors.New("testerino") }
|
||||
|
||||
err = holder.Update(&Update{
|
||||
// this update invalidates the book
|
||||
err = holder.Update(&orderbook.Update{
|
||||
Asks: []orderbook.Item{{Price: 999999}},
|
||||
Pair: cp,
|
||||
UpdateID: -1,
|
||||
Asset: asset.Spot,
|
||||
})
|
||||
if !errors.Is(err, errTest) {
|
||||
t.Fatalf("received: %v but expected: %v", err, errTest)
|
||||
if !errors.Is(err, orderbook.ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: %v but expected: %v", err, orderbook.ErrOrderbookInvalid)
|
||||
}
|
||||
|
||||
holder, _, _, err = createSnapshot()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
holder.checksum = func(state *orderbook.Base, checksum uint32) error { return nil }
|
||||
@@ -421,7 +471,7 @@ func TestOrderbookLastUpdateID(t *testing.T) {
|
||||
|
||||
for i := range itemArray {
|
||||
asks := itemArray[i]
|
||||
err = holder.Update(&Update{
|
||||
err = holder.Update(&orderbook.Update{
|
||||
Asks: asks,
|
||||
Pair: cp,
|
||||
UpdateID: int64(i) + 1,
|
||||
@@ -434,7 +484,7 @@ func TestOrderbookLastUpdateID(t *testing.T) {
|
||||
|
||||
// out of order
|
||||
holder.verbose = true
|
||||
err = holder.Update(&Update{
|
||||
err = holder.Update(&orderbook.Update{
|
||||
Asks: []orderbook.Item{{Price: 999999}},
|
||||
Pair: cp,
|
||||
UpdateID: 1,
|
||||
@@ -470,7 +520,7 @@ func TestRunUpdateWithoutSnapshot(t *testing.T) {
|
||||
snapShot1.Asset = asset.Spot
|
||||
snapShot1.Pair = cp
|
||||
holder.exchangeName = exchangeName
|
||||
err := holder.Update(&Update{
|
||||
err := holder.Update(&orderbook.Update{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
Pair: cp,
|
||||
@@ -492,7 +542,7 @@ func TestRunUpdateWithoutAnyUpdates(t *testing.T) {
|
||||
snapShot1.Asset = asset.Spot
|
||||
snapShot1.Pair = cp
|
||||
obl.exchangeName = exchangeName
|
||||
err := obl.Update(&Update{
|
||||
err := obl.Update(&orderbook.Update{
|
||||
Bids: snapShot1.Asks,
|
||||
Asks: snapShot1.Bids,
|
||||
Pair: cp,
|
||||
@@ -729,9 +779,21 @@ func TestGetOrderbook(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bufferOb := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
||||
b := bufferOb.ob.Retrieve()
|
||||
if bufferOb.ob.GetAskLength() != len(ob.Asks) ||
|
||||
bufferOb.ob.GetBidLength() != len(ob.Bids) ||
|
||||
b, err := bufferOb.ob.Retrieve()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
askLen, err := bufferOb.ob.GetAskLength()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
bidLen, err := bufferOb.ob.GetBidLength()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
if askLen != len(ob.Asks) ||
|
||||
bidLen != len(ob.Bids) ||
|
||||
b.Asset != ob.Asset ||
|
||||
b.Exchange != ob.Exchange ||
|
||||
b.LastUpdateID != ob.LastUpdateID ||
|
||||
@@ -795,7 +857,7 @@ func TestValidate(t *testing.T) {
|
||||
t.Fatalf("expected error %v but received %v", errUpdateIsNil, err)
|
||||
}
|
||||
|
||||
err = w.validate(&Update{})
|
||||
err = w.validate(&orderbook.Update{})
|
||||
if !errors.Is(err, errUpdateNoTargets) {
|
||||
t.Fatalf("expected error %v but received %v", errUpdateNoTargets, err)
|
||||
}
|
||||
@@ -810,7 +872,7 @@ func TestEnsureMultipleUpdatesViaPrice(t *testing.T) {
|
||||
|
||||
asks := bidAskGenerator()
|
||||
book := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
||||
book.updateByPrice(&Update{
|
||||
book.updateByPrice(&orderbook.Update{
|
||||
Bids: asks,
|
||||
Asks: asks,
|
||||
Pair: cp,
|
||||
@@ -821,7 +883,12 @@ func TestEnsureMultipleUpdatesViaPrice(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if book.ob.GetAskLength() <= 3 {
|
||||
askLen, err := book.ob.GetAskLength()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if askLen <= 3 {
|
||||
t.Errorf("Insufficient updates")
|
||||
}
|
||||
}
|
||||
@@ -851,20 +918,25 @@ func TestUpdateByIDAndAction(t *testing.T) {
|
||||
|
||||
book.LoadSnapshot(append(bids[:0:0], bids...), append(asks[:0:0], asks...), 0, time.Time{}, true)
|
||||
|
||||
err = book.Retrieve().Verify()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
ob, err := book.Retrieve()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
err = ob.Verify()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
holder.ob = book
|
||||
|
||||
err = holder.updateByIDAndAction(&Update{})
|
||||
if err == nil {
|
||||
t.Fatal("error cannot be nil")
|
||||
err = holder.updateByIDAndAction(&orderbook.Update{})
|
||||
if !errors.Is(err, errInvalidAction) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errInvalidAction)
|
||||
}
|
||||
|
||||
err = holder.updateByIDAndAction(&Update{
|
||||
Action: Amend,
|
||||
err = holder.updateByIDAndAction(&orderbook.Update{
|
||||
Action: orderbook.Amend,
|
||||
Bids: []orderbook.Item{
|
||||
{
|
||||
Price: 100,
|
||||
@@ -872,13 +944,14 @@ func TestUpdateByIDAndAction(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("error cannot be nil")
|
||||
if !strings.Contains(err.Error(), errAmendFailure.Error()) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errAmendFailure)
|
||||
}
|
||||
|
||||
book.LoadSnapshot(append(bids[:0:0], bids...), append(asks[:0:0], asks...), 0, time.Time{}, true)
|
||||
// append to slice
|
||||
err = holder.updateByIDAndAction(&Update{
|
||||
Action: UpdateInsert,
|
||||
err = holder.updateByIDAndAction(&orderbook.Update{
|
||||
Action: orderbook.UpdateInsert,
|
||||
Bids: []orderbook.Item{
|
||||
{
|
||||
Price: 0,
|
||||
@@ -894,11 +967,14 @@ func TestUpdateByIDAndAction(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
cpy := book.Retrieve()
|
||||
cpy, err := book.Retrieve()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if cpy.Bids[len(cpy.Bids)-1].Price != 0 {
|
||||
t.Fatal("did not append bid item")
|
||||
@@ -908,8 +984,8 @@ func TestUpdateByIDAndAction(t *testing.T) {
|
||||
}
|
||||
|
||||
// Change amount
|
||||
err = holder.updateByIDAndAction(&Update{
|
||||
Action: UpdateInsert,
|
||||
err = holder.updateByIDAndAction(&orderbook.Update{
|
||||
Action: orderbook.UpdateInsert,
|
||||
Bids: []orderbook.Item{
|
||||
{
|
||||
Price: 0,
|
||||
@@ -925,11 +1001,14 @@ func TestUpdateByIDAndAction(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
cpy = book.Retrieve()
|
||||
cpy, err = book.Retrieve()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if cpy.Bids[len(cpy.Bids)-1].Amount != 100 {
|
||||
t.Fatal("did not update bid amount", cpy.Bids[len(cpy.Bids)-1].Amount)
|
||||
@@ -940,8 +1019,8 @@ func TestUpdateByIDAndAction(t *testing.T) {
|
||||
}
|
||||
|
||||
// Change price level
|
||||
err = holder.updateByIDAndAction(&Update{
|
||||
Action: UpdateInsert,
|
||||
err = holder.updateByIDAndAction(&orderbook.Update{
|
||||
Action: orderbook.UpdateInsert,
|
||||
Bids: []orderbook.Item{
|
||||
{
|
||||
Price: 100,
|
||||
@@ -957,11 +1036,14 @@ func TestUpdateByIDAndAction(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
cpy = book.Retrieve()
|
||||
cpy, err = book.Retrieve()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if cpy.Bids[0].Amount != 99 && cpy.Bids[0].Price != 100 {
|
||||
t.Fatal("did not adjust bid item placement and details")
|
||||
@@ -972,10 +1054,9 @@ func TestUpdateByIDAndAction(t *testing.T) {
|
||||
}
|
||||
|
||||
book.LoadSnapshot(append(bids[:0:0], bids...), append(bids[:0:0], bids...), 0, time.Time{}, true) // nolint:gocritic
|
||||
|
||||
// Delete - not found
|
||||
err = holder.updateByIDAndAction(&Update{
|
||||
Action: Delete,
|
||||
err = holder.updateByIDAndAction(&orderbook.Update{
|
||||
Action: orderbook.Delete,
|
||||
Asks: []orderbook.Item{
|
||||
{
|
||||
Price: 0,
|
||||
@@ -984,54 +1065,54 @@ func TestUpdateByIDAndAction(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("error cannot be nil")
|
||||
}
|
||||
err = holder.updateByIDAndAction(&Update{
|
||||
Action: Delete,
|
||||
Bids: []orderbook.Item{
|
||||
{
|
||||
Price: 0,
|
||||
ID: 1337,
|
||||
Amount: 99,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("error cannot be nil")
|
||||
if !strings.Contains(err.Error(), errDeleteFailure.Error()) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errDeleteFailure)
|
||||
}
|
||||
|
||||
book.LoadSnapshot(append(bids[:0:0], bids...), append(bids[:0:0], bids...), 0, time.Time{}, true) // nolint:gocritic
|
||||
// Delete - found
|
||||
err = holder.updateByIDAndAction(&Update{
|
||||
Action: Delete,
|
||||
err = holder.updateByIDAndAction(&orderbook.Update{
|
||||
Action: orderbook.Delete,
|
||||
Asks: []orderbook.Item{
|
||||
asks[0],
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if book.GetAskLength() != 99 {
|
||||
askLen, err := book.GetAskLength()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if askLen != 99 {
|
||||
t.Fatal("element not deleted")
|
||||
}
|
||||
|
||||
// Apply update
|
||||
err = holder.updateByIDAndAction(&Update{
|
||||
Action: Amend,
|
||||
err = holder.updateByIDAndAction(&orderbook.Update{
|
||||
Action: orderbook.Amend,
|
||||
Asks: []orderbook.Item{
|
||||
{ID: 123456},
|
||||
},
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("error cannot be nil")
|
||||
if !strings.Contains(err.Error(), errAmendFailure.Error()) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, errAmendFailure)
|
||||
}
|
||||
|
||||
update := book.Retrieve().Asks[0]
|
||||
book.LoadSnapshot(bids, bids, 0, time.Time{}, true)
|
||||
|
||||
ob, err = book.Retrieve()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
update := ob.Asks[0]
|
||||
update.Amount = 1337
|
||||
|
||||
err = holder.updateByIDAndAction(&Update{
|
||||
Action: Amend,
|
||||
err = holder.updateByIDAndAction(&orderbook.Update{
|
||||
Action: orderbook.Amend,
|
||||
Asks: []orderbook.Item{
|
||||
update,
|
||||
},
|
||||
@@ -1040,7 +1121,12 @@ func TestUpdateByIDAndAction(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if book.Retrieve().Asks[0].Amount != 1337 {
|
||||
ob, err = book.Retrieve()
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, nil)
|
||||
}
|
||||
|
||||
if ob.Asks[0].Amount != 1337 {
|
||||
t.Fatal("element not updated")
|
||||
}
|
||||
}
|
||||
@@ -1086,12 +1172,8 @@ func TestFlushOrderbook(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
o, err := w.GetOrderbook(cp, asset.Spot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(o.Bids) != 0 || len(o.Asks) != 0 {
|
||||
t.Fatal("orderbook items not flushed")
|
||||
_, err = w.GetOrderbook(cp, asset.Spot)
|
||||
if !errors.Is(err, orderbook.ErrOrderbookInvalid) {
|
||||
t.Fatalf("received: '%v' but expected: '%v'", err, orderbook.ErrOrderbookInvalid)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ type Orderbook struct {
|
||||
// orderbook depth
|
||||
type orderbookHolder struct {
|
||||
ob *orderbook.Depth
|
||||
buffer *[]Update
|
||||
buffer *[]orderbook.Update
|
||||
// Reduces the amount of outbound alerts to the data handler for example
|
||||
// coinbasepro can have up too 100 updates per second introducing overhead.
|
||||
// The sync agent only requires an alert every 15 seconds for a specific
|
||||
@@ -62,36 +62,3 @@ type orderbookHolder struct {
|
||||
ticker *time.Ticker
|
||||
updateID int64
|
||||
}
|
||||
|
||||
// Update stores orderbook updates and dictates what features to use when processing
|
||||
type Update struct {
|
||||
UpdateID int64 // Used when no time is provided
|
||||
UpdateTime time.Time
|
||||
Asset asset.Item
|
||||
Action
|
||||
Bids []orderbook.Item
|
||||
Asks []orderbook.Item
|
||||
Pair currency.Pair
|
||||
// Checksum defines the expected value when the books have been verified
|
||||
Checksum uint32
|
||||
// Determines if there is a max depth of orderbooks and after an append we
|
||||
// should remove any items that are outside of this scope. Kraken is the
|
||||
// only exchange utilising this field.
|
||||
MaxDepth int
|
||||
}
|
||||
|
||||
// Action defines a set of differing states required to implement an incoming
|
||||
// orderbook update used in conjunction with UpdateEntriesByID
|
||||
type Action string
|
||||
|
||||
const (
|
||||
// Amend applies amount adjustment by ID
|
||||
Amend Action = "update"
|
||||
// Delete removes price level from book by ID
|
||||
Delete Action = "delete"
|
||||
// Insert adds price level to book
|
||||
Insert Action = "insert"
|
||||
// UpdateInsert on conflict applies amount adjustment or appends new amount
|
||||
// to book
|
||||
UpdateInsert Action = "update/insert"
|
||||
)
|
||||
|
||||
@@ -22,7 +22,7 @@ func init() {
|
||||
service = new(Service)
|
||||
service.Tickers = make(map[string]map[*currency.Item]map[*currency.Item]map[asset.Item]*Ticker)
|
||||
service.Exchange = make(map[string]uuid.UUID)
|
||||
service.mux = dispatch.GetNewMux()
|
||||
service.mux = dispatch.GetNewMux(nil)
|
||||
}
|
||||
|
||||
// SubscribeTicker subscribes to a ticker and returns a communication channel to
|
||||
@@ -183,7 +183,7 @@ func (s *Service) update(p *Price) error {
|
||||
// nolint: gocritic
|
||||
ids := append(t.Assoc, t.Main)
|
||||
s.mu.Unlock()
|
||||
return s.mux.Publish(ids, p)
|
||||
return s.mux.Publish(p, ids...)
|
||||
}
|
||||
|
||||
// setItemID retrieves and sets dispatch mux publish IDs
|
||||
|
||||
4156
gctrpc/rpc.pb.go
4156
gctrpc/rpc.pb.go
File diff suppressed because it is too large
Load Diff
@@ -148,6 +148,7 @@ message OrderbookResponse {
|
||||
repeated OrderbookItem asks = 4;
|
||||
int64 last_updated = 5;
|
||||
string asset_type = 6;
|
||||
string error = 7;
|
||||
}
|
||||
|
||||
message GetOrderbooksRequest {}
|
||||
|
||||
@@ -4949,6 +4949,9 @@
|
||||
},
|
||||
"assetType": {
|
||||
"type": "string"
|
||||
},
|
||||
"error": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user