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:
Ryan O'Hara-Reid
2022-05-03 12:37:08 +10:00
committed by GitHub
parent 8adbcd0b2c
commit c6ad429827
48 changed files with 3998 additions and 3184 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -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)
}
}
}

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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{

View File

@@ -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),

View File

@@ -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) {

View File

@@ -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

View File

@@ -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)

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -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)
}
}

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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)))

View File

@@ -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{

View File

@@ -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,

View File

@@ -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,

View File

@@ -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")

View File

@@ -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)),

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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{

View File

@@ -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")

View File

@@ -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()
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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),

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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"
)

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -148,6 +148,7 @@ message OrderbookResponse {
repeated OrderbookItem asks = 4;
int64 last_updated = 5;
string asset_type = 6;
string error = 7;
}
message GetOrderbooksRequest {}

View File

@@ -4949,6 +4949,9 @@
},
"assetType": {
"type": "string"
},
"error": {
"type": "string"
}
}
},