mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-18 15:10:03 +00:00
orderbook: Implement initial linked list (#643)
* Exchanges: Initial implementation after rebase of depth (WIP) * orderbook/buffer: convert and couple orderbook interaction functionality from buffer to orderbook linked list - Use single point reference for orderbook depth * buffer/orderbook: conversion continued (WIP) * exchange: buffer/linkedlist handover (WIP) * Added some tests for yesterday * linkedList: added more testing and trying to figure out broken things * Started tying everything in * continuous integration and testing * orderbook: expanded tests * go mod tidy * Add in different synchornisation levels for protocols Add in timer for the streaming system to reduce updates to datahandler Add in more test code as I integrate more exchanges * Depth: Add tests, add length check to call linked list updating, add in constructor. Linked List: Improve tests, add in checks for zero liquidity on books. Node: Added in cleaner POC, add in contructor. Buffer: Fixed tests, checked benchmarks. * orderbook: reinstate dispatch calls * Addr glorious & madcozbad nits * fix functionality and add tests * Address linterinos * remove label * expanded comment * fix races and and bitmex test * reinstate go routine for alerting changes * rm line :D * fix more tests * Addr glorious nits * rm glorious field * depth: defer unlock to stop deadlock * orderbook: remove unused vars * buffer: fix test to what it should be * nits: madcosbad addr * nits: glorious nits * linkedlist: remove unused params * orderbook: shift time call to outside of push to inline, add in case for update inster price for zero liquidity, nits * orderbook: nits addressed * engine: change stream -> websocket convention and remove unused function * nits: glorious nits * Websocket Buffer: Add verbosity switch * linked list: Add comment * linked list: fix spelling * nits: glorious nits * orderbook: Adds in test and explicit time type with constructor, fix nits * linter * spelling: removed the dere fence * depth: Update alerting mechanism to a more battle tested state * depth: spelling * nits: glorious nits * linked list: match cases * buffer: fix linter issue * golangci: increase timeout by 30 seconds * nodes: update atomic checks * spelling: fix * node: add in commentary * exchanges/syncer: add function to switch over to REST when websocket functionality is not available for a specific asset type * linter: exchange linter issues * syncer: Add in warning * nits: glorious nits * AssetWebsocketSupport: unexport map * Nits: Adrr * rm letter * exchanges: Orderbook verification change for naming, deprecate checksum bypass as it has the potential to obfuscate errors that are at the tail end of the book, add in verification for websocket stream updates * general: fix spelling remove breakpoint * nits: fix more glorious nits until more are found * orderbook: fix tests * orderbook: fix wait tests and add in more checks * nits: addr * orderbook: remove dispatch reference * linkedlist: consolidate bid/ask functions * linked lisdt: remove words * fix spelling
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
run:
|
||||
timeout: 2m0s
|
||||
timeout: 2m30s
|
||||
issues-exit-code: 1
|
||||
tests: true
|
||||
skip-dirs:
|
||||
|
||||
@@ -283,10 +283,10 @@ func ({{.Variable}} *{{.CapitalName}}) FetchOrderbook(currency currency.Pair, as
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func ({{.Variable}} *{{.CapitalName}}) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: {{.Variable}}.Name,
|
||||
Exchange: {{.Variable}}.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: {{.Variable}}.OrderbookVerificationBypass,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: {{.Variable}}.CanVerifyOrderbook,
|
||||
}
|
||||
|
||||
// NOTE: UPDATE ORDERBOOK EXAMPLE
|
||||
|
||||
@@ -208,7 +208,8 @@ func validateSettings(b *Engine, s *Settings, flagSet map[string]bool) {
|
||||
b.Settings.EnableOrderbookSyncing = s.EnableOrderbookSyncing
|
||||
b.Settings.EnableTradeSyncing = s.EnableTradeSyncing
|
||||
b.Settings.SyncWorkers = s.SyncWorkers
|
||||
b.Settings.SyncTimeout = s.SyncTimeout
|
||||
b.Settings.SyncTimeoutREST = s.SyncTimeoutREST
|
||||
b.Settings.SyncTimeoutWebsocket = s.SyncTimeoutWebsocket
|
||||
b.Settings.SyncContinuously = s.SyncContinuously
|
||||
b.Settings.EnableDepositAddressManager = s.EnableDepositAddressManager
|
||||
b.Settings.EnableExchangeAutoPairUpdates = s.EnableExchangeAutoPairUpdates
|
||||
@@ -305,7 +306,8 @@ func PrintSettings(s *Settings) {
|
||||
gctlog.Debugf(gctlog.Global, "\t Enable ticker syncing: %v\n", s.EnableTickerSyncing)
|
||||
gctlog.Debugf(gctlog.Global, "\t Enable orderbook syncing: %v\n", s.EnableOrderbookSyncing)
|
||||
gctlog.Debugf(gctlog.Global, "\t Enable trade syncing: %v\n", s.EnableTradeSyncing)
|
||||
gctlog.Debugf(gctlog.Global, "\t Exchange sync timeout: %v\n", s.SyncTimeout)
|
||||
gctlog.Debugf(gctlog.Global, "\t Exchange REST sync timeout: %v\n", s.SyncTimeoutREST)
|
||||
gctlog.Debugf(gctlog.Global, "\t Exchange Websocket sync timeout: %v\n", s.SyncTimeoutWebsocket)
|
||||
gctlog.Debugf(gctlog.Global, "- FOREX SETTINGS:")
|
||||
gctlog.Debugf(gctlog.Global, "\t Enable currency conveter: %v", s.EnableCurrencyConverter)
|
||||
gctlog.Debugf(gctlog.Global, "\t Enable currency layer: %v", s.EnableCurrencyLayer)
|
||||
@@ -464,13 +466,14 @@ func (bot *Engine) Start() error {
|
||||
|
||||
if bot.Settings.EnableExchangeSyncManager {
|
||||
exchangeSyncCfg := CurrencyPairSyncerConfig{
|
||||
SyncTicker: bot.Settings.EnableTickerSyncing,
|
||||
SyncOrderbook: bot.Settings.EnableOrderbookSyncing,
|
||||
SyncTrades: bot.Settings.EnableTradeSyncing,
|
||||
SyncContinuously: bot.Settings.SyncContinuously,
|
||||
NumWorkers: bot.Settings.SyncWorkers,
|
||||
Verbose: bot.Settings.Verbose,
|
||||
SyncTimeout: bot.Settings.SyncTimeout,
|
||||
SyncTicker: bot.Settings.EnableTickerSyncing,
|
||||
SyncOrderbook: bot.Settings.EnableOrderbookSyncing,
|
||||
SyncTrades: bot.Settings.EnableTradeSyncing,
|
||||
SyncContinuously: bot.Settings.SyncContinuously,
|
||||
NumWorkers: bot.Settings.SyncWorkers,
|
||||
Verbose: bot.Settings.Verbose,
|
||||
SyncTimeoutREST: bot.Settings.SyncTimeoutREST,
|
||||
SyncTimeoutWebsocket: bot.Settings.SyncTimeoutWebsocket,
|
||||
}
|
||||
|
||||
bot.ExchangeCurrencyPairManager, err = NewCurrencyPairSyncer(exchangeSyncCfg)
|
||||
|
||||
@@ -44,7 +44,8 @@ type Settings struct {
|
||||
EnableTradeSyncing bool
|
||||
SyncWorkers int
|
||||
SyncContinuously bool
|
||||
SyncTimeout time.Duration
|
||||
SyncTimeoutREST time.Duration
|
||||
SyncTimeoutWebsocket time.Duration
|
||||
|
||||
// Forex settings
|
||||
EnableCurrencyConverter bool
|
||||
|
||||
@@ -210,11 +210,11 @@ func TestProcessOrderbook(t *testing.T) {
|
||||
|
||||
// now populate it with a 0 entry
|
||||
o := orderbook.Base{
|
||||
Pair: currency.NewPair(currency.BTC, currency.USD),
|
||||
Bids: []orderbook.Item{{Amount: 24, Price: 23}},
|
||||
Asks: []orderbook.Item{{Amount: 24, Price: 23}},
|
||||
ExchangeName: e.Exchange,
|
||||
AssetType: e.Asset,
|
||||
Pair: currency.NewPair(currency.BTC, currency.USD),
|
||||
Bids: []orderbook.Item{{Amount: 24, Price: 23}},
|
||||
Asks: []orderbook.Item{{Amount: 24, Price: 23}},
|
||||
Exchange: e.Exchange,
|
||||
Asset: e.Asset,
|
||||
}
|
||||
if err := o.Process(); err != nil {
|
||||
t.Fatal("unexpected result:", err)
|
||||
|
||||
@@ -535,10 +535,10 @@ func TestGetSpecificOrderbook(t *testing.T) {
|
||||
bids = append(bids, orderbook.Item{Price: 1000, Amount: 1})
|
||||
|
||||
base := orderbook.Base{
|
||||
Pair: currency.NewPair(currency.BTC, currency.USD),
|
||||
Bids: bids,
|
||||
ExchangeName: "Bitstamp",
|
||||
AssetType: asset.Spot,
|
||||
Pair: currency.NewPair(currency.BTC, currency.USD),
|
||||
Bids: bids,
|
||||
Exchange: "Bitstamp",
|
||||
Asset: asset.Spot,
|
||||
}
|
||||
|
||||
err := base.Process()
|
||||
|
||||
@@ -132,17 +132,17 @@ func printOrderbookSummary(result *orderbook.Base, protocol string, bot *Engine,
|
||||
if err == common.ErrNotYetImplemented {
|
||||
log.Warnf(log.OrderBook, "Failed to get %s orderbook for %s %s %s. Error: %s\n",
|
||||
protocol,
|
||||
result.ExchangeName,
|
||||
result.Exchange,
|
||||
result.Pair,
|
||||
result.AssetType,
|
||||
result.Asset,
|
||||
err)
|
||||
return
|
||||
}
|
||||
log.Errorf(log.OrderBook, "Failed to get %s orderbook for %s %s %s. Error: %s\n",
|
||||
protocol,
|
||||
result.ExchangeName,
|
||||
result.Exchange,
|
||||
result.Pair,
|
||||
result.AssetType,
|
||||
result.Asset,
|
||||
err)
|
||||
return
|
||||
}
|
||||
@@ -165,10 +165,10 @@ func printOrderbookSummary(result *orderbook.Base, protocol string, bot *Engine,
|
||||
}
|
||||
|
||||
log.Infof(log.OrderBook, book,
|
||||
result.ExchangeName,
|
||||
result.Exchange,
|
||||
protocol,
|
||||
bot.FormatCurrency(result.Pair),
|
||||
strings.ToUpper(result.AssetType.String()),
|
||||
strings.ToUpper(result.Asset.String()),
|
||||
len(result.Bids),
|
||||
bidsAmount,
|
||||
result.Pair.Base,
|
||||
@@ -317,7 +317,7 @@ func (bot *Engine) WebsocketDataHandler(exchName string, data interface{}) error
|
||||
if bot.Settings.EnableExchangeSyncManager && bot.ExchangeCurrencyPairManager != nil {
|
||||
bot.ExchangeCurrencyPairManager.update(exchName,
|
||||
d.Pair,
|
||||
d.AssetType,
|
||||
d.Asset,
|
||||
SyncItemOrderbook,
|
||||
nil)
|
||||
}
|
||||
|
||||
@@ -118,8 +118,8 @@ func TestHandleData(t *testing.T) {
|
||||
}
|
||||
|
||||
err = b.WebsocketDataHandler(exchName, &orderbook.Base{
|
||||
ExchangeName: fakePassExchange,
|
||||
Pair: currency.NewPair(currency.BTC, currency.USD),
|
||||
Exchange: fakePassExchange,
|
||||
Pair: currency.NewPair(currency.BTC, currency.USD),
|
||||
})
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
|
||||
@@ -1672,45 +1672,37 @@ func (s *RPCServer) GetOrderbookStream(r *gctrpc.GetOrderbookStreamRequest, stre
|
||||
return err
|
||||
}
|
||||
|
||||
pipe, err := orderbook.SubscribeOrderbook(r.Exchange, p, a)
|
||||
depth, err := orderbook.GetDepth(r.Exchange, p, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer pipe.Release()
|
||||
|
||||
for {
|
||||
data, ok := <-pipe.C
|
||||
if !ok {
|
||||
return errDispatchSystem
|
||||
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}
|
||||
}
|
||||
|
||||
ob := (*data.(*interface{})).(orderbook.Base)
|
||||
var bids, asks []*gctrpc.OrderbookItem
|
||||
for i := range ob.Bids {
|
||||
bids = append(bids, &gctrpc.OrderbookItem{
|
||||
Amount: ob.Bids[i].Amount,
|
||||
Price: ob.Bids[i].Price,
|
||||
Id: ob.Bids[i].ID,
|
||||
})
|
||||
}
|
||||
for i := range ob.Asks {
|
||||
asks = append(asks, &gctrpc.OrderbookItem{
|
||||
Amount: ob.Asks[i].Amount,
|
||||
Price: ob.Asks[i].Price,
|
||||
Id: ob.Asks[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{
|
||||
Pair: &gctrpc.CurrencyPair{Base: ob.Pair.Base.String(),
|
||||
Quote: ob.Pair.Quote.String()},
|
||||
Pair: &gctrpc.CurrencyPair{Base: r.Pair.Base, Quote: r.Pair.Quote},
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
AssetType: ob.AssetType.String(),
|
||||
AssetType: r.AssetType,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
<-depth.Wait(nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1734,27 +1726,26 @@ func (s *RPCServer) GetExchangeOrderbookStream(r *gctrpc.GetExchangeOrderbookStr
|
||||
}
|
||||
|
||||
ob := (*data.(*interface{})).(orderbook.Base)
|
||||
var bids, asks []*gctrpc.OrderbookItem
|
||||
bids := make([]*gctrpc.OrderbookItem, len(ob.Bids))
|
||||
for i := range ob.Bids {
|
||||
bids = append(bids, &gctrpc.OrderbookItem{
|
||||
bids[i] = &gctrpc.OrderbookItem{
|
||||
Amount: ob.Bids[i].Amount,
|
||||
Price: ob.Bids[i].Price,
|
||||
Id: ob.Bids[i].ID,
|
||||
})
|
||||
Id: ob.Bids[i].ID}
|
||||
}
|
||||
asks := make([]*gctrpc.OrderbookItem, len(ob.Asks))
|
||||
for i := range ob.Asks {
|
||||
asks = append(asks, &gctrpc.OrderbookItem{
|
||||
asks[i] = &gctrpc.OrderbookItem{
|
||||
Amount: ob.Asks[i].Amount,
|
||||
Price: ob.Asks[i].Price,
|
||||
Id: ob.Asks[i].ID,
|
||||
})
|
||||
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.AssetType.String(),
|
||||
AssetType: ob.Asset.String(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
111
engine/syncer.go
111
engine/syncer.go
@@ -18,8 +18,9 @@ const (
|
||||
SyncItemOrderbook
|
||||
SyncItemTrade
|
||||
|
||||
DefaultSyncerWorkers = 15
|
||||
DefaultSyncerTimeout = time.Second * 15
|
||||
DefaultSyncerWorkers = 15
|
||||
DefaultSyncerTimeoutREST = time.Second * 15
|
||||
DefaultSyncerTimeoutWebsocket = time.Minute
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -37,28 +38,25 @@ func NewCurrencyPairSyncer(c CurrencyPairSyncerConfig) (*ExchangeCurrencyPairSyn
|
||||
c.NumWorkers = DefaultSyncerWorkers
|
||||
}
|
||||
|
||||
if c.SyncTimeout <= time.Duration(0) {
|
||||
c.SyncTimeout = DefaultSyncerTimeout
|
||||
if c.SyncTimeoutREST <= time.Duration(0) {
|
||||
c.SyncTimeoutREST = DefaultSyncerTimeoutREST
|
||||
}
|
||||
|
||||
s := ExchangeCurrencyPairSyncer{
|
||||
Cfg: CurrencyPairSyncerConfig{
|
||||
SyncTicker: c.SyncTicker,
|
||||
SyncOrderbook: c.SyncOrderbook,
|
||||
SyncTrades: c.SyncTrades,
|
||||
SyncContinuously: c.SyncContinuously,
|
||||
SyncTimeout: c.SyncTimeout,
|
||||
NumWorkers: c.NumWorkers,
|
||||
},
|
||||
if c.SyncTimeoutWebsocket <= time.Duration(0) {
|
||||
c.SyncTimeoutWebsocket = DefaultSyncerTimeoutWebsocket
|
||||
}
|
||||
|
||||
s := ExchangeCurrencyPairSyncer{Cfg: c}
|
||||
|
||||
s.tickerBatchLastRequested = make(map[string]time.Time)
|
||||
|
||||
log.Debugf(log.SyncMgr,
|
||||
"Exchange currency pair syncer config: continuous: %v ticker: %v"+
|
||||
" orderbook: %v trades: %v workers: %v verbose: %v timeout: %v\n",
|
||||
" orderbook: %v trades: %v workers: %v verbose: %v timeout REST: %v"+
|
||||
" timeout Websocket: %v\n",
|
||||
s.Cfg.SyncContinuously, s.Cfg.SyncTicker, s.Cfg.SyncOrderbook,
|
||||
s.Cfg.SyncTrades, s.Cfg.NumWorkers, s.Cfg.Verbose, s.Cfg.SyncTimeout)
|
||||
s.Cfg.SyncTrades, s.Cfg.NumWorkers, s.Cfg.Verbose, s.Cfg.SyncTimeoutREST,
|
||||
s.Cfg.SyncTimeoutWebsocket)
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
@@ -138,20 +136,6 @@ func (e *ExchangeCurrencyPairSyncer) add(c *CurrencyPairSyncAgent) {
|
||||
e.CurrencyPairs = append(e.CurrencyPairs, *c)
|
||||
}
|
||||
|
||||
func (e *ExchangeCurrencyPairSyncer) remove(c *CurrencyPairSyncAgent) {
|
||||
e.mux.Lock()
|
||||
defer e.mux.Unlock()
|
||||
|
||||
for x := range e.CurrencyPairs {
|
||||
if e.CurrencyPairs[x].Exchange == c.Exchange &&
|
||||
e.CurrencyPairs[x].Pair.Equal(c.Pair) &&
|
||||
e.CurrencyPairs[x].AssetType == c.AssetType {
|
||||
e.CurrencyPairs = append(e.CurrencyPairs[:x], e.CurrencyPairs[x+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ExchangeCurrencyPairSyncer) isProcessing(exchangeName string, p currency.Pair, a asset.Item, syncType int) bool {
|
||||
e.mux.Lock()
|
||||
defer e.mux.Unlock()
|
||||
@@ -325,6 +309,8 @@ func (e *ExchangeCurrencyPairSyncer) worker() {
|
||||
if exchanges[x].GetBase().CurrencyPairs.IsAssetEnabled(assetTypes[y]) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
wsAssetSupported := exchanges[x].IsAssetWebsocketSupported(assetTypes[y])
|
||||
enabledPairs, err := exchanges[x].GetEnabledPairs(assetTypes[y])
|
||||
if err != nil {
|
||||
log.Errorf(log.SyncMgr,
|
||||
@@ -345,25 +331,21 @@ func (e *ExchangeCurrencyPairSyncer) worker() {
|
||||
Pair: enabledPairs[i],
|
||||
}
|
||||
|
||||
sBase := SyncBase{
|
||||
IsUsingREST: usingREST || !wsAssetSupported,
|
||||
IsUsingWebsocket: usingWebsocket && wsAssetSupported,
|
||||
}
|
||||
|
||||
if e.Cfg.SyncTicker {
|
||||
c.Ticker = SyncBase{
|
||||
IsUsingREST: usingREST,
|
||||
IsUsingWebsocket: usingWebsocket,
|
||||
}
|
||||
c.Ticker = sBase
|
||||
}
|
||||
|
||||
if e.Cfg.SyncOrderbook {
|
||||
c.Orderbook = SyncBase{
|
||||
IsUsingREST: usingREST,
|
||||
IsUsingWebsocket: usingWebsocket,
|
||||
}
|
||||
c.Orderbook = sBase
|
||||
}
|
||||
|
||||
if e.Cfg.SyncTrades {
|
||||
c.Trade = SyncBase{
|
||||
IsUsingREST: usingREST,
|
||||
IsUsingWebsocket: usingWebsocket,
|
||||
}
|
||||
c.Trade = sBase
|
||||
}
|
||||
|
||||
e.add(&c)
|
||||
@@ -382,9 +364,11 @@ func (e *ExchangeCurrencyPairSyncer) worker() {
|
||||
}
|
||||
if e.Cfg.SyncTicker {
|
||||
if !e.isProcessing(exchangeName, c.Pair, c.AssetType, SyncItemTicker) {
|
||||
if c.Ticker.LastUpdated.IsZero() || time.Since(c.Ticker.LastUpdated) > e.Cfg.SyncTimeout {
|
||||
if c.Ticker.LastUpdated.IsZero() ||
|
||||
(time.Since(c.Ticker.LastUpdated) > e.Cfg.SyncTimeoutREST && c.Ticker.IsUsingREST) ||
|
||||
(time.Since(c.Ticker.LastUpdated) > e.Cfg.SyncTimeoutWebsocket && c.Ticker.IsUsingWebsocket) {
|
||||
if c.Ticker.IsUsingWebsocket {
|
||||
if time.Since(c.Created) < e.Cfg.SyncTimeout {
|
||||
if time.Since(c.Created) < e.Cfg.SyncTimeoutWebsocket {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -397,7 +381,7 @@ func (e *ExchangeCurrencyPairSyncer) worker() {
|
||||
c.Exchange,
|
||||
Bot.FormatCurrency(enabledPairs[i]).String(),
|
||||
strings.ToUpper(c.AssetType.String()),
|
||||
e.Cfg.SyncTimeout,
|
||||
e.Cfg.SyncTimeoutWebsocket,
|
||||
)
|
||||
switchedToRest = true
|
||||
e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTicker, false)
|
||||
@@ -417,7 +401,7 @@ func (e *ExchangeCurrencyPairSyncer) worker() {
|
||||
}
|
||||
e.mux.Unlock()
|
||||
|
||||
if batchLastDone.IsZero() || time.Since(batchLastDone) > e.Cfg.SyncTimeout {
|
||||
if batchLastDone.IsZero() || time.Since(batchLastDone) > e.Cfg.SyncTimeoutREST {
|
||||
e.mux.Lock()
|
||||
if e.Cfg.Verbose {
|
||||
log.Debugf(log.SyncMgr, "%s Init'ing REST ticker batching\n", exchangeName)
|
||||
@@ -450,9 +434,11 @@ func (e *ExchangeCurrencyPairSyncer) worker() {
|
||||
|
||||
if e.Cfg.SyncOrderbook {
|
||||
if !e.isProcessing(exchangeName, c.Pair, c.AssetType, SyncItemOrderbook) {
|
||||
if c.Orderbook.LastUpdated.IsZero() || time.Since(c.Orderbook.LastUpdated) > e.Cfg.SyncTimeout {
|
||||
if c.Orderbook.LastUpdated.IsZero() ||
|
||||
(time.Since(c.Orderbook.LastUpdated) > e.Cfg.SyncTimeoutREST && c.Orderbook.IsUsingREST) ||
|
||||
(time.Since(c.Orderbook.LastUpdated) > e.Cfg.SyncTimeoutWebsocket && c.Orderbook.IsUsingWebsocket) {
|
||||
if c.Orderbook.IsUsingWebsocket {
|
||||
if time.Since(c.Created) < e.Cfg.SyncTimeout {
|
||||
if time.Since(c.Created) < e.Cfg.SyncTimeoutWebsocket {
|
||||
continue
|
||||
}
|
||||
if supportsREST {
|
||||
@@ -464,7 +450,7 @@ func (e *ExchangeCurrencyPairSyncer) worker() {
|
||||
c.Exchange,
|
||||
Bot.FormatCurrency(c.Pair).String(),
|
||||
strings.ToUpper(c.AssetType.String()),
|
||||
e.Cfg.SyncTimeout,
|
||||
e.Cfg.SyncTimeoutWebsocket,
|
||||
)
|
||||
switchedToRest = true
|
||||
e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemOrderbook, false)
|
||||
@@ -486,7 +472,7 @@ func (e *ExchangeCurrencyPairSyncer) worker() {
|
||||
}
|
||||
if e.Cfg.SyncTrades {
|
||||
if !e.isProcessing(exchangeName, c.Pair, c.AssetType, SyncItemTrade) {
|
||||
if c.Trade.LastUpdated.IsZero() || time.Since(c.Trade.LastUpdated) > e.Cfg.SyncTimeout {
|
||||
if c.Trade.LastUpdated.IsZero() || time.Since(c.Trade.LastUpdated) > e.Cfg.SyncTimeoutREST {
|
||||
e.setProcessing(c.Exchange, c.Pair, c.AssetType, SyncItemTrade, true)
|
||||
e.update(c.Exchange, c.Pair, c.AssetType, SyncItemTrade, nil)
|
||||
}
|
||||
@@ -560,6 +546,13 @@ func (e *ExchangeCurrencyPairSyncer) Start() {
|
||||
continue
|
||||
}
|
||||
|
||||
wsAssetSupported := exchanges[x].IsAssetWebsocketSupported(assetTypes[y])
|
||||
if !wsAssetSupported {
|
||||
log.Warnf(log.SyncMgr,
|
||||
"%s asset type %s websocket functionality is unsupported, REST fetching only.",
|
||||
exchangeName,
|
||||
assetTypes[y])
|
||||
}
|
||||
enabledPairs, err := exchanges[x].GetEnabledPairs(assetTypes[y])
|
||||
if err != nil {
|
||||
log.Errorf(log.SyncMgr,
|
||||
@@ -579,25 +572,21 @@ func (e *ExchangeCurrencyPairSyncer) Start() {
|
||||
Pair: enabledPairs[i],
|
||||
}
|
||||
|
||||
sBase := SyncBase{
|
||||
IsUsingREST: usingREST || !wsAssetSupported,
|
||||
IsUsingWebsocket: usingWebsocket && wsAssetSupported,
|
||||
}
|
||||
|
||||
if e.Cfg.SyncTicker {
|
||||
c.Ticker = SyncBase{
|
||||
IsUsingREST: usingREST,
|
||||
IsUsingWebsocket: usingWebsocket,
|
||||
}
|
||||
c.Ticker = sBase
|
||||
}
|
||||
|
||||
if e.Cfg.SyncOrderbook {
|
||||
c.Orderbook = SyncBase{
|
||||
IsUsingREST: usingREST,
|
||||
IsUsingWebsocket: usingWebsocket,
|
||||
}
|
||||
c.Orderbook = sBase
|
||||
}
|
||||
|
||||
if e.Cfg.SyncTrades {
|
||||
c.Trade = SyncBase{
|
||||
IsUsingREST: usingREST,
|
||||
IsUsingWebsocket: usingWebsocket,
|
||||
}
|
||||
c.Trade = sBase
|
||||
}
|
||||
|
||||
e.add(&c)
|
||||
|
||||
@@ -10,13 +10,14 @@ import (
|
||||
|
||||
// CurrencyPairSyncerConfig stores the currency pair config
|
||||
type CurrencyPairSyncerConfig struct {
|
||||
SyncTicker bool
|
||||
SyncOrderbook bool
|
||||
SyncTrades bool
|
||||
SyncContinuously bool
|
||||
SyncTimeout time.Duration
|
||||
NumWorkers int
|
||||
Verbose bool
|
||||
SyncTicker bool
|
||||
SyncOrderbook bool
|
||||
SyncTrades bool
|
||||
SyncContinuously bool
|
||||
SyncTimeoutREST time.Duration
|
||||
SyncTimeoutWebsocket time.Duration
|
||||
NumWorkers int
|
||||
Verbose bool
|
||||
}
|
||||
|
||||
// ExchangeSyncerConfig stores the exchange syncer config
|
||||
|
||||
@@ -187,8 +187,8 @@ func (a *Alphapoint) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*or
|
||||
}
|
||||
|
||||
orderBook.Pair = p
|
||||
orderBook.ExchangeName = a.Name
|
||||
orderBook.AssetType = assetType
|
||||
orderBook.Exchange = a.Name
|
||||
orderBook.Asset = assetType
|
||||
|
||||
err = orderBook.Process()
|
||||
if err != nil {
|
||||
|
||||
@@ -2128,7 +2128,10 @@ func TestWsDepthUpdate(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
ob := b.Websocket.Orderbook.GetOrderbook(p, asset.Spot)
|
||||
ob, err := b.Websocket.Orderbook.GetOrderbook(p, asset.Spot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if exp, got := seedLastUpdateID, ob.LastUpdateID; got != exp {
|
||||
t.Fatalf("Unexpected Last update id of orderbook for old update. Exp: %d, got: %d", exp, got)
|
||||
}
|
||||
@@ -2154,11 +2157,14 @@ func TestWsDepthUpdate(t *testing.T) {
|
||||
]
|
||||
}}`)
|
||||
|
||||
if err := b.wsHandleData(update2); err != nil {
|
||||
if err = b.wsHandleData(update2); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
ob = b.Websocket.Orderbook.GetOrderbook(p, asset.Spot)
|
||||
ob, err = b.Websocket.Orderbook.GetOrderbook(p, asset.Spot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if exp, got := int64(165), ob.LastUpdateID; got != exp {
|
||||
t.Fatalf("Unexpected Last update id of orderbook for new update. Exp: %d, got: %d", exp, got)
|
||||
}
|
||||
|
||||
@@ -463,10 +463,10 @@ func (b *Binance) SeedLocalCacheWithBook(p currency.Pair, orderbookNew *OrderBoo
|
||||
}
|
||||
|
||||
newOrderBook.Pair = p
|
||||
newOrderBook.AssetType = asset.Spot
|
||||
newOrderBook.ExchangeName = b.Name
|
||||
newOrderBook.Asset = asset.Spot
|
||||
newOrderBook.Exchange = b.Name
|
||||
newOrderBook.LastUpdateID = orderbookNew.LastUpdateID
|
||||
newOrderBook.VerificationBypass = b.OrderbookVerificationBypass
|
||||
newOrderBook.VerifyOrderbook = b.CanVerifyOrderbook
|
||||
|
||||
return b.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
|
||||
}
|
||||
@@ -632,8 +632,8 @@ func (b *Binance) applyBufferUpdate(pair currency.Pair) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
recent := b.Websocket.Orderbook.GetOrderbook(pair, asset.Spot)
|
||||
if recent == nil || (recent.Asks == nil && recent.Bids == nil) {
|
||||
recent, err := b.Websocket.Orderbook.GetOrderbook(pair, asset.Spot)
|
||||
if err != nil || (recent.Asks == nil && recent.Bids == nil) {
|
||||
return b.obm.fetchBookViaREST(pair)
|
||||
}
|
||||
|
||||
|
||||
@@ -92,14 +92,26 @@ func (b *Binance) SetDefaults() {
|
||||
if err != nil {
|
||||
log.Errorln(log.ExchangeSys, err)
|
||||
}
|
||||
err = b.DisableAssetWebsocketSupport(asset.Margin)
|
||||
if err != nil {
|
||||
log.Errorln(log.ExchangeSys, err)
|
||||
}
|
||||
err = b.StoreAssetPairFormat(asset.CoinMarginedFutures, coinFutures)
|
||||
if err != nil {
|
||||
log.Errorln(log.ExchangeSys, err)
|
||||
}
|
||||
err = b.DisableAssetWebsocketSupport(asset.CoinMarginedFutures)
|
||||
if err != nil {
|
||||
log.Errorln(log.ExchangeSys, err)
|
||||
}
|
||||
err = b.StoreAssetPairFormat(asset.USDTMarginedFutures, usdtFutures)
|
||||
if err != nil {
|
||||
log.Errorln(log.ExchangeSys, err)
|
||||
}
|
||||
err = b.DisableAssetWebsocketSupport(asset.USDTMarginedFutures)
|
||||
if err != nil {
|
||||
log.Errorln(log.ExchangeSys, err)
|
||||
}
|
||||
b.Features = exchange.Features{
|
||||
Supports: exchange.FeaturesSupported{
|
||||
REST: true,
|
||||
@@ -520,10 +532,10 @@ func (b *Binance) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderb
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (b *Binance) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: b.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: b.OrderbookVerificationBypass,
|
||||
Exchange: b.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: b.CanVerifyOrderbook,
|
||||
}
|
||||
var orderbookNew OrderBook
|
||||
var err error
|
||||
|
||||
@@ -962,13 +962,12 @@ func (b *Bitfinex) WsInsertSnapshot(p currency.Pair, assetType asset.Item, books
|
||||
}
|
||||
}
|
||||
|
||||
book.AssetType = assetType
|
||||
book.Asset = assetType
|
||||
book.Pair = p
|
||||
book.ExchangeName = b.Name
|
||||
book.NotAggregated = true
|
||||
book.HasChecksumValidation = true
|
||||
book.Exchange = b.Name
|
||||
book.PriceDuplication = true
|
||||
book.IsFundingRate = fundingRate
|
||||
book.VerificationBypass = b.OrderbookVerificationBypass
|
||||
book.VerifyOrderbook = b.CanVerifyOrderbook
|
||||
return b.Websocket.Orderbook.LoadSnapshot(&book)
|
||||
}
|
||||
|
||||
@@ -1036,14 +1035,15 @@ func (b *Bitfinex) WsUpdateOrderbook(p currency.Pair, assetType asset.Item, book
|
||||
if checkme.Sequence+1 == sequenceNo {
|
||||
// Sequence numbers get dropped, if checksum is not in line with
|
||||
// sequence, do not check.
|
||||
ob := b.Websocket.Orderbook.GetOrderbook(p, assetType)
|
||||
if ob == nil {
|
||||
return fmt.Errorf("cannot calculate websocket checksum: book not found for %s %s",
|
||||
ob, err := b.Websocket.Orderbook.GetOrderbook(p, assetType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot calculate websocket checksum: book not found for %s %s %w",
|
||||
p,
|
||||
assetType)
|
||||
assetType,
|
||||
err)
|
||||
}
|
||||
|
||||
err := validateCRC32(ob, checkme.Token)
|
||||
err = validateCRC32(ob, checkme.Token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1398,7 +1398,7 @@ func validateCRC32(book *orderbook.Base, token int) error {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid checksum for %s %s: calculated [%d] does not match [%d]",
|
||||
book.AssetType,
|
||||
book.Asset,
|
||||
book.Pair,
|
||||
checksum,
|
||||
uint32(token))
|
||||
|
||||
@@ -395,11 +395,11 @@ func (b *Bitfinex) FetchOrderbook(p currency.Pair, assetType asset.Item) (*order
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (b *Bitfinex) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
o := &orderbook.Base{
|
||||
ExchangeName: b.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
NotAggregated: true,
|
||||
VerificationBypass: b.OrderbookVerificationBypass,
|
||||
Exchange: b.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
PriceDuplication: true,
|
||||
VerifyOrderbook: b.CanVerifyOrderbook,
|
||||
}
|
||||
|
||||
fPair, err := b.FormatExchangeCurrency(p, assetType)
|
||||
|
||||
@@ -254,10 +254,10 @@ func (b *Bitflyer) FetchOrderbook(p currency.Pair, assetType asset.Item) (*order
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (b *Bitflyer) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: b.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: b.OrderbookVerificationBypass,
|
||||
Exchange: b.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: b.CanVerifyOrderbook,
|
||||
}
|
||||
|
||||
fPair, err := b.FormatExchangeCurrency(p, assetType)
|
||||
|
||||
@@ -246,10 +246,10 @@ func (b *Bithumb) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderb
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (b *Bithumb) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: b.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: b.OrderbookVerificationBypass,
|
||||
Exchange: b.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: b.CanVerifyOrderbook,
|
||||
}
|
||||
curr := p.Base.String()
|
||||
|
||||
|
||||
@@ -955,7 +955,7 @@ func TestWSOrderbookHandling(t *testing.T) {
|
||||
]
|
||||
}`)
|
||||
err = b.wsHandleData(pressXToJSON)
|
||||
if err != nil && err.Error() != "perpetualcontract ETHUSD update cannot be deleted id: 17999995000 not found" {
|
||||
if err != nil && err.Error() != "delete error: cannot match ID on linked list 17999995000 not found" {
|
||||
t.Error(err)
|
||||
}
|
||||
if err == nil {
|
||||
|
||||
@@ -508,11 +508,11 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, p currency.
|
||||
data[i].Side)
|
||||
}
|
||||
}
|
||||
orderbook.Reverse(book.Asks) // Reverse asks for correct alignment
|
||||
book.AssetType = a
|
||||
book.Asks.Reverse() // Reverse asks for correct alignment
|
||||
book.Asset = a
|
||||
book.Pair = p
|
||||
book.ExchangeName = b.Name
|
||||
book.VerificationBypass = b.OrderbookVerificationBypass
|
||||
book.Exchange = b.Name
|
||||
book.VerifyOrderbook = b.CanVerifyOrderbook
|
||||
|
||||
err := b.Websocket.Orderbook.LoadSnapshot(&book)
|
||||
if err != nil {
|
||||
|
||||
@@ -69,6 +69,11 @@ func (b *Bitmex) SetDefaults() {
|
||||
log.Errorln(log.ExchangeSys, err)
|
||||
}
|
||||
|
||||
err = b.DisableAssetWebsocketSupport(asset.Index)
|
||||
if err != nil {
|
||||
log.Errorln(log.ExchangeSys, err)
|
||||
}
|
||||
|
||||
b.Features = exchange.Features{
|
||||
Supports: exchange.FeaturesSupported{
|
||||
REST: true,
|
||||
@@ -346,10 +351,10 @@ func (b *Bitmex) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbo
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (b *Bitmex) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: b.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: b.OrderbookVerificationBypass,
|
||||
Exchange: b.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: b.CanVerifyOrderbook,
|
||||
}
|
||||
|
||||
if assetType == asset.Index {
|
||||
@@ -384,7 +389,7 @@ func (b *Bitmex) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderb
|
||||
orderbookNew[i].Side)
|
||||
}
|
||||
}
|
||||
orderbook.Reverse(book.Asks)
|
||||
book.Asks.Reverse() // Reverse order of asks to ascending
|
||||
|
||||
err = book.Process()
|
||||
if err != nil {
|
||||
|
||||
@@ -239,13 +239,13 @@ func (b *Bitstamp) wsUpdateOrderbook(update websocketOrderBook, p currency.Pair,
|
||||
bids = append(bids, orderbook.Item{Price: target, Amount: amount})
|
||||
}
|
||||
return b.Websocket.Orderbook.LoadSnapshot(&orderbook.Base{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
Pair: p,
|
||||
LastUpdated: time.Unix(update.Timestamp, 0),
|
||||
AssetType: assetType,
|
||||
ExchangeName: b.Name,
|
||||
VerificationBypass: b.OrderbookVerificationBypass,
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
Pair: p,
|
||||
LastUpdated: time.Unix(update.Timestamp, 0),
|
||||
Asset: assetType,
|
||||
Exchange: b.Name,
|
||||
VerifyOrderbook: b.CanVerifyOrderbook,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -275,9 +275,9 @@ func (b *Bitstamp) seedOrderBook() error {
|
||||
})
|
||||
}
|
||||
newOrderBook.Pair = p[x]
|
||||
newOrderBook.AssetType = asset.Spot
|
||||
newOrderBook.ExchangeName = b.Name
|
||||
newOrderBook.VerificationBypass = b.OrderbookVerificationBypass
|
||||
newOrderBook.Asset = asset.Spot
|
||||
newOrderBook.Exchange = b.Name
|
||||
newOrderBook.VerifyOrderbook = b.CanVerifyOrderbook
|
||||
|
||||
err = b.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
|
||||
if err != nil {
|
||||
|
||||
@@ -324,10 +324,10 @@ func (b *Bitstamp) FetchOrderbook(p currency.Pair, assetType asset.Item) (*order
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (b *Bitstamp) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: b.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: b.OrderbookVerificationBypass,
|
||||
Exchange: b.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: b.CanVerifyOrderbook,
|
||||
}
|
||||
fPair, err := b.FormatExchangeCurrency(p, assetType)
|
||||
if err != nil {
|
||||
|
||||
@@ -333,10 +333,10 @@ func (b *Bittrex) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderb
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (b *Bittrex) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: b.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: b.OrderbookVerificationBypass,
|
||||
Exchange: b.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: b.CanVerifyOrderbook,
|
||||
}
|
||||
fpair, err := b.FormatExchangeCurrency(p, assetType)
|
||||
if err != nil {
|
||||
|
||||
@@ -83,7 +83,7 @@ func (b *BTCMarkets) wsHandleData(respRaw []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var bids, asks []orderbook.Item
|
||||
var bids, asks orderbook.Items
|
||||
for x := range ob.Bids {
|
||||
var price, amount float64
|
||||
price, err = strconv.ParseFloat(ob.Bids[x][0].(string), 64)
|
||||
@@ -117,14 +117,15 @@ func (b *BTCMarkets) wsHandleData(respRaw []byte) error {
|
||||
})
|
||||
}
|
||||
if ob.Snapshot {
|
||||
bids.SortBids() // Alignment completely out, sort is needed.
|
||||
err = b.Websocket.Orderbook.LoadSnapshot(&orderbook.Base{
|
||||
Pair: p,
|
||||
Bids: orderbook.SortBids(bids), // Alignment completely out sort is needed
|
||||
Asks: asks,
|
||||
LastUpdated: ob.Timestamp,
|
||||
AssetType: asset.Spot,
|
||||
ExchangeName: b.Name,
|
||||
VerificationBypass: b.OrderbookVerificationBypass,
|
||||
Pair: p,
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
LastUpdated: ob.Timestamp,
|
||||
Asset: asset.Spot,
|
||||
Exchange: b.Name,
|
||||
VerifyOrderbook: b.CanVerifyOrderbook,
|
||||
})
|
||||
} else {
|
||||
err = b.Websocket.Orderbook.Update(&buffer.Update{
|
||||
|
||||
@@ -360,11 +360,11 @@ func (b *BTCMarkets) FetchOrderbook(p currency.Pair, assetType asset.Item) (*ord
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (b *BTCMarkets) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: b.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
NotAggregated: true,
|
||||
VerificationBypass: b.OrderbookVerificationBypass,
|
||||
Exchange: b.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
PriceDuplication: true,
|
||||
VerifyOrderbook: b.CanVerifyOrderbook,
|
||||
}
|
||||
|
||||
fpair, err := b.FormatExchangeCurrency(p, assetType)
|
||||
|
||||
@@ -311,10 +311,10 @@ func (b *BTSE) wsHandleData(respRaw []byte) error {
|
||||
return err
|
||||
}
|
||||
newOB.Pair = p
|
||||
newOB.AssetType = a
|
||||
newOB.ExchangeName = b.Name
|
||||
orderbook.Reverse(newOB.Asks) // Reverse asks for correct alignment
|
||||
newOB.VerificationBypass = b.OrderbookVerificationBypass
|
||||
newOB.Asset = a
|
||||
newOB.Exchange = b.Name
|
||||
newOB.Asks.Reverse() // Reverse asks for correct alignment
|
||||
newOB.VerifyOrderbook = b.CanVerifyOrderbook
|
||||
err = b.Websocket.Orderbook.LoadSnapshot(&newOB)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -336,10 +336,10 @@ func (b *BTSE) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbook
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (b *BTSE) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: b.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: b.OrderbookVerificationBypass,
|
||||
Exchange: b.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: b.CanVerifyOrderbook,
|
||||
}
|
||||
fPair, err := b.FormatExchangeCurrency(p, assetType)
|
||||
if err != nil {
|
||||
@@ -366,10 +366,10 @@ func (b *BTSE) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderboo
|
||||
Price: a.SellQuote[x].Price,
|
||||
Amount: a.SellQuote[x].Size})
|
||||
}
|
||||
orderbook.Reverse(book.Asks) // Reverse asks for correct alignment
|
||||
book.Asks.Reverse() // Reverse asks for correct alignment
|
||||
book.Pair = p
|
||||
book.ExchangeName = b.Name
|
||||
book.AssetType = assetType
|
||||
book.Exchange = b.Name
|
||||
book.Asset = assetType
|
||||
err = book.Process()
|
||||
if err != nil {
|
||||
return book, err
|
||||
|
||||
@@ -309,10 +309,10 @@ func (c *CoinbasePro) ProcessSnapshot(snapshot *WebsocketOrderbookSnapshot) erro
|
||||
return err
|
||||
}
|
||||
|
||||
base.AssetType = asset.Spot
|
||||
base.Asset = asset.Spot
|
||||
base.Pair = pair
|
||||
base.ExchangeName = c.Name
|
||||
base.VerificationBypass = c.OrderbookVerificationBypass
|
||||
base.Exchange = c.Name
|
||||
base.VerifyOrderbook = c.CanVerifyOrderbook
|
||||
|
||||
return c.Websocket.Orderbook.LoadSnapshot(&base)
|
||||
}
|
||||
|
||||
@@ -411,10 +411,10 @@ func (c *CoinbasePro) FetchOrderbook(p currency.Pair, assetType asset.Item) (*or
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (c *CoinbasePro) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: c.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: c.OrderbookVerificationBypass,
|
||||
Exchange: c.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: c.CanVerifyOrderbook,
|
||||
}
|
||||
fpair, err := c.FormatExchangeCurrency(p, assetType)
|
||||
if err != nil {
|
||||
|
||||
@@ -285,11 +285,11 @@ func (c *Coinbene) wsHandleData(respRaw []byte) error {
|
||||
var newOB orderbook.Base
|
||||
newOB.Asks = asks
|
||||
newOB.Bids = bids
|
||||
newOB.AssetType = assetType
|
||||
newOB.Asset = assetType
|
||||
newOB.Pair = newPair
|
||||
newOB.ExchangeName = c.Name
|
||||
newOB.Exchange = c.Name
|
||||
newOB.LastUpdated = time.Unix(orderBook.Data[0].Timestamp, 0)
|
||||
newOB.VerificationBypass = c.OrderbookVerificationBypass
|
||||
newOB.VerifyOrderbook = c.CanVerifyOrderbook
|
||||
err = c.Websocket.Orderbook.LoadSnapshot(&newOB)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -411,10 +411,10 @@ func (c *Coinbene) FetchOrderbook(p currency.Pair, assetType asset.Item) (*order
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (c *Coinbene) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: c.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: c.OrderbookVerificationBypass,
|
||||
Exchange: c.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: c.CanVerifyOrderbook,
|
||||
}
|
||||
if !c.SupportsAsset(assetType) {
|
||||
return book,
|
||||
|
||||
@@ -530,7 +530,7 @@ func (c *COINUT) WsProcessOrderbookSnapshot(ob *WsOrderbookSnapshot) error {
|
||||
var newOrderBook orderbook.Base
|
||||
newOrderBook.Asks = asks
|
||||
newOrderBook.Bids = bids
|
||||
newOrderBook.VerificationBypass = c.OrderbookVerificationBypass
|
||||
newOrderBook.VerifyOrderbook = c.CanVerifyOrderbook
|
||||
|
||||
pairs, err := c.GetEnabledPairs(asset.Spot)
|
||||
if err != nil {
|
||||
@@ -550,8 +550,8 @@ func (c *COINUT) WsProcessOrderbookSnapshot(ob *WsOrderbookSnapshot) error {
|
||||
return err
|
||||
}
|
||||
|
||||
newOrderBook.AssetType = asset.Spot
|
||||
newOrderBook.ExchangeName = c.Name
|
||||
newOrderBook.Asset = asset.Spot
|
||||
newOrderBook.Exchange = c.Name
|
||||
|
||||
return c.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
|
||||
}
|
||||
|
||||
@@ -463,10 +463,10 @@ func (c *COINUT) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbo
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (c *COINUT) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: c.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: c.OrderbookVerificationBypass,
|
||||
Exchange: c.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: c.CanVerifyOrderbook,
|
||||
}
|
||||
err := c.loadInstrumentsIfNotLoaded()
|
||||
if err != nil {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2323,3 +2323,49 @@ func TestSetRunning(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssetWebsocketFunctionality(t *testing.T) {
|
||||
b := Base{}
|
||||
if !b.IsAssetWebsocketSupported(asset.Spot) {
|
||||
t.Fatal("error asset is not turned off, unexpected response")
|
||||
}
|
||||
|
||||
err := b.DisableAssetWebsocketSupport(asset.Spot)
|
||||
if !errors.Is(err, asset.ErrNotSupported) {
|
||||
t.Fatalf("expected error: %v but received: %v", asset.ErrNotSupported, err)
|
||||
}
|
||||
|
||||
err = b.StoreAssetPairFormat(asset.Spot, currency.PairStore{
|
||||
RequestFormat: ¤cy.PairFormat{
|
||||
Uppercase: true,
|
||||
},
|
||||
ConfigFormat: ¤cy.PairFormat{
|
||||
Uppercase: true,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Errorln(log.ExchangeSys, err)
|
||||
}
|
||||
|
||||
err = b.DisableAssetWebsocketSupport(asset.Spot)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("expected error: %v but received: %v", nil, err)
|
||||
}
|
||||
|
||||
if b.IsAssetWebsocketSupported(asset.Spot) {
|
||||
t.Fatal("error asset is not turned off, unexpected response")
|
||||
}
|
||||
|
||||
// Edge case
|
||||
b.AssetWebsocketSupport.unsupported = make(map[asset.Item]bool)
|
||||
b.AssetWebsocketSupport.unsupported[asset.Spot] = true
|
||||
b.AssetWebsocketSupport.unsupported[asset.Futures] = false
|
||||
|
||||
if b.IsAssetWebsocketSupported(asset.Spot) {
|
||||
t.Fatal("error asset is turned off, unexpected response")
|
||||
}
|
||||
|
||||
if !b.IsAssetWebsocketSupported(asset.Futures) {
|
||||
t.Fatal("error asset is not turned off, unexpected response")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/thrasher-corp/gocryptotrader/config"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
|
||||
@@ -220,10 +221,15 @@ type Base struct {
|
||||
WebsocketOrderbookBufferLimit int64
|
||||
Websocket *stream.Websocket
|
||||
*request.Requester
|
||||
Config *config.ExchangeConfig
|
||||
settingsMutex sync.RWMutex
|
||||
OrderbookVerificationBypass bool
|
||||
Config *config.ExchangeConfig
|
||||
settingsMutex sync.RWMutex
|
||||
// CanVerifyOrderbook determines if the orderbook verification can be bypassed,
|
||||
// increasing potential update speed but decreasing confidence in orderbook
|
||||
// integrity.
|
||||
CanVerifyOrderbook bool
|
||||
order.ExecutionLimits
|
||||
|
||||
AssetWebsocketSupport
|
||||
}
|
||||
|
||||
// url lookup consts
|
||||
@@ -259,3 +265,11 @@ var keyURLs = []URL{RestSpot,
|
||||
|
||||
// URL stores uint conversions
|
||||
type URL uint16
|
||||
|
||||
// AssetWebsocketSupport defines the availability of websocket functionality to
|
||||
// the specific asset type. TODO: Deprecate as this is a temp item to address
|
||||
// certain limitations quickly.
|
||||
type AssetWebsocketSupport struct {
|
||||
unsupported map[asset.Item]bool
|
||||
m sync.RWMutex
|
||||
}
|
||||
|
||||
@@ -240,10 +240,10 @@ func (e *EXMO) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbook
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (e *EXMO) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
callingBook := &orderbook.Base{
|
||||
ExchangeName: e.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: e.OrderbookVerificationBypass,
|
||||
Exchange: e.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: e.CanVerifyOrderbook,
|
||||
}
|
||||
enabledPairs, err := e.GetEnabledPairs(assetType)
|
||||
if err != nil {
|
||||
@@ -262,10 +262,10 @@ func (e *EXMO) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderboo
|
||||
|
||||
for i := range enabledPairs {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: e.Name,
|
||||
Pair: enabledPairs[i],
|
||||
AssetType: assetType,
|
||||
VerificationBypass: e.OrderbookVerificationBypass,
|
||||
Exchange: e.Name,
|
||||
Pair: enabledPairs[i],
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: e.CanVerifyOrderbook,
|
||||
}
|
||||
|
||||
curr, err := e.FormatExchangeCurrency(enabledPairs[i], assetType)
|
||||
|
||||
@@ -452,7 +452,10 @@ func (f *FTX) WsProcessUpdateOB(data *WsOrderbookData, p currency.Pair, a asset.
|
||||
return err
|
||||
}
|
||||
|
||||
updatedOb := f.Websocket.Orderbook.GetOrderbook(p, a)
|
||||
updatedOb, err := f.Websocket.Orderbook.GetOrderbook(p, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
checksum := f.CalcUpdateOBChecksum(updatedOb)
|
||||
|
||||
if checksum != data.Checksum {
|
||||
@@ -506,14 +509,13 @@ func (f *FTX) WsProcessPartialOB(data *WsOrderbookData, p currency.Pair, a asset
|
||||
}
|
||||
|
||||
newOrderBook := orderbook.Base{
|
||||
Asks: asks,
|
||||
Bids: bids,
|
||||
AssetType: a,
|
||||
LastUpdated: timestampFromFloat64(data.Time),
|
||||
Pair: p,
|
||||
ExchangeName: f.Name,
|
||||
HasChecksumValidation: true,
|
||||
VerificationBypass: f.OrderbookVerificationBypass,
|
||||
Asks: asks,
|
||||
Bids: bids,
|
||||
Asset: a,
|
||||
LastUpdated: timestampFromFloat64(data.Time),
|
||||
Pair: p,
|
||||
Exchange: f.Name,
|
||||
VerifyOrderbook: f.CanVerifyOrderbook,
|
||||
}
|
||||
return f.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
|
||||
}
|
||||
|
||||
@@ -344,10 +344,10 @@ func (f *FTX) FetchOrderbook(currency currency.Pair, assetType asset.Item) (*ord
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (f *FTX) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: f.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: f.OrderbookVerificationBypass,
|
||||
Exchange: f.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: f.CanVerifyOrderbook,
|
||||
}
|
||||
formattedPair, err := f.FormatExchangeCurrency(p, assetType)
|
||||
if err != nil {
|
||||
|
||||
@@ -350,10 +350,10 @@ func (g *Gateio) wsHandleData(respRaw []byte) error {
|
||||
var newOrderBook orderbook.Base
|
||||
newOrderBook.Asks = asks
|
||||
newOrderBook.Bids = bids
|
||||
newOrderBook.AssetType = asset.Spot
|
||||
newOrderBook.Asset = asset.Spot
|
||||
newOrderBook.Pair = p
|
||||
newOrderBook.ExchangeName = g.Name
|
||||
newOrderBook.VerificationBypass = g.OrderbookVerificationBypass
|
||||
newOrderBook.Exchange = g.Name
|
||||
newOrderBook.VerifyOrderbook = g.CanVerifyOrderbook
|
||||
|
||||
err = g.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
|
||||
if err != nil {
|
||||
|
||||
@@ -287,10 +287,10 @@ func (g *Gateio) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbo
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (g *Gateio) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: g.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: g.OrderbookVerificationBypass,
|
||||
Exchange: g.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: g.CanVerifyOrderbook,
|
||||
}
|
||||
curr, err := g.FormatExchangeCurrency(p, assetType)
|
||||
if err != nil {
|
||||
|
||||
@@ -526,10 +526,10 @@ func (g *Gemini) wsProcessUpdate(result *wsL2MarketData) error {
|
||||
var newOrderBook orderbook.Base
|
||||
newOrderBook.Asks = asks
|
||||
newOrderBook.Bids = bids
|
||||
newOrderBook.AssetType = asset.Spot
|
||||
newOrderBook.Asset = asset.Spot
|
||||
newOrderBook.Pair = pair
|
||||
newOrderBook.ExchangeName = g.Name
|
||||
newOrderBook.VerificationBypass = g.OrderbookVerificationBypass
|
||||
newOrderBook.Exchange = g.Name
|
||||
newOrderBook.VerifyOrderbook = g.CanVerifyOrderbook
|
||||
err := g.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -403,10 +403,10 @@ func (g *Gemini) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbo
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (g *Gemini) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: g.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: g.OrderbookVerificationBypass,
|
||||
Exchange: g.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: g.CanVerifyOrderbook,
|
||||
}
|
||||
fPair, err := g.FormatExchangeCurrency(p, assetType)
|
||||
if err != nil {
|
||||
|
||||
@@ -327,10 +327,10 @@ func (h *HitBTC) WsProcessOrderbookSnapshot(ob WsOrderbook) error {
|
||||
h.Websocket.DataHandler <- err
|
||||
return err
|
||||
}
|
||||
newOrderBook.AssetType = asset.Spot
|
||||
newOrderBook.Asset = asset.Spot
|
||||
newOrderBook.Pair = p
|
||||
newOrderBook.ExchangeName = h.Name
|
||||
newOrderBook.VerificationBypass = h.OrderbookVerificationBypass
|
||||
newOrderBook.Exchange = h.Name
|
||||
newOrderBook.VerifyOrderbook = h.CanVerifyOrderbook
|
||||
|
||||
return h.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
|
||||
}
|
||||
|
||||
@@ -376,10 +376,10 @@ func (h *HitBTC) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbo
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (h *HitBTC) UpdateOrderbook(c currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: h.Name,
|
||||
Pair: c,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: h.OrderbookVerificationBypass,
|
||||
Exchange: h.Name,
|
||||
Pair: c,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: h.CanVerifyOrderbook,
|
||||
}
|
||||
fpair, err := h.FormatExchangeCurrency(c, assetType)
|
||||
if err != nil {
|
||||
|
||||
@@ -464,9 +464,9 @@ func (h *HUOBI) WsProcessOrderbook(update *WsDepth, symbol string) error {
|
||||
newOrderBook.Asks = asks
|
||||
newOrderBook.Bids = bids
|
||||
newOrderBook.Pair = p
|
||||
newOrderBook.AssetType = asset.Spot
|
||||
newOrderBook.ExchangeName = h.Name
|
||||
newOrderBook.VerificationBypass = h.OrderbookVerificationBypass
|
||||
newOrderBook.Asset = asset.Spot
|
||||
newOrderBook.Exchange = h.Name
|
||||
newOrderBook.VerifyOrderbook = h.CanVerifyOrderbook
|
||||
|
||||
return h.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
|
||||
}
|
||||
|
||||
@@ -522,10 +522,10 @@ func (h *HUOBI) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderboo
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (h *HUOBI) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: h.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: h.OrderbookVerificationBypass,
|
||||
Exchange: h.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: h.CanVerifyOrderbook,
|
||||
}
|
||||
var err error
|
||||
switch assetType {
|
||||
|
||||
@@ -82,6 +82,7 @@ type IBotExchange interface {
|
||||
SupportsWebsocket() bool
|
||||
SubscribeToWebsocketChannels(channels []stream.ChannelSubscription) error
|
||||
UnsubscribeToWebsocketChannels(channels []stream.ChannelSubscription) error
|
||||
IsAssetWebsocketSupported(aType asset.Item) bool
|
||||
// FlushWebsocketChannels checks and flushes subscriptions if there is a
|
||||
// pair,asset, url/proxy or subscription change
|
||||
FlushWebsocketChannels() error
|
||||
|
||||
@@ -191,11 +191,11 @@ func (i *ItBit) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderboo
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (i *ItBit) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: i.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
NotAggregated: true,
|
||||
VerificationBypass: i.OrderbookVerificationBypass,
|
||||
Exchange: i.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
PriceDuplication: true,
|
||||
VerifyOrderbook: i.CanVerifyOrderbook,
|
||||
}
|
||||
fpair, err := i.FormatExchangeCurrency(p, assetType)
|
||||
if err != nil {
|
||||
|
||||
@@ -795,9 +795,9 @@ func (k *Kraken) wsProcessOrderBook(channelData *WebsocketChannelData, data map[
|
||||
// wsProcessOrderBookPartial creates a new orderbook entry for a given currency pair
|
||||
func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, askData, bidData []interface{}) error {
|
||||
base := orderbook.Base{
|
||||
Pair: channelData.Pair,
|
||||
AssetType: asset.Spot,
|
||||
VerificationBypass: k.OrderbookVerificationBypass,
|
||||
Pair: channelData.Pair,
|
||||
Asset: asset.Spot,
|
||||
VerifyOrderbook: k.CanVerifyOrderbook,
|
||||
}
|
||||
// Kraken ob data is timestamped per price, GCT orderbook data is
|
||||
// timestamped per entry using the highest last update time, we can attempt
|
||||
@@ -851,8 +851,7 @@ func (k *Kraken) wsProcessOrderBookPartial(channelData *WebsocketChannelData, as
|
||||
}
|
||||
}
|
||||
base.LastUpdated = highestLastUpdate
|
||||
base.ExchangeName = k.Name
|
||||
base.HasChecksumValidation = true
|
||||
base.Exchange = k.Name
|
||||
return k.Websocket.Orderbook.LoadSnapshot(&base)
|
||||
}
|
||||
|
||||
@@ -995,11 +994,12 @@ func (k *Kraken) wsProcessOrderBookUpdate(channelData *WebsocketChannelData, ask
|
||||
return err
|
||||
}
|
||||
|
||||
book := k.Websocket.Orderbook.GetOrderbook(channelData.Pair, asset.Spot)
|
||||
if book == nil {
|
||||
return fmt.Errorf("cannot calculate websocket checksum: book not found for %s %s",
|
||||
book, err := k.Websocket.Orderbook.GetOrderbook(channelData.Pair, asset.Spot)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot calculate websocket checksum: book not found for %s %s %w",
|
||||
channelData.Pair,
|
||||
asset.Spot)
|
||||
asset.Spot,
|
||||
err)
|
||||
}
|
||||
|
||||
token, err := strconv.ParseInt(checksum, 10, 64)
|
||||
@@ -1014,12 +1014,13 @@ func validateCRC32(b *orderbook.Base, token uint32, decPrice, decAmount int) err
|
||||
if len(b.Asks) < 10 || len(b.Bids) < 10 {
|
||||
return fmt.Errorf("%s %s insufficient bid and asks to calculate checksum",
|
||||
b.Pair,
|
||||
b.AssetType)
|
||||
b.Asset)
|
||||
}
|
||||
|
||||
if decPrice == 0 || decAmount == 0 {
|
||||
return fmt.Errorf("%s %s trailing decimal count not calculated", b.Pair,
|
||||
b.AssetType)
|
||||
return fmt.Errorf("%s %s trailing decimal count not calculated",
|
||||
b.Pair,
|
||||
b.Asset)
|
||||
}
|
||||
|
||||
var checkStr strings.Builder
|
||||
@@ -1040,7 +1041,7 @@ func validateCRC32(b *orderbook.Base, token uint32, decPrice, decAmount int) err
|
||||
if check := crc32.ChecksumIEEE([]byte(checkStr.String())); check != token {
|
||||
return fmt.Errorf("%s %s invalid checksum %d, expected %d",
|
||||
b.Pair,
|
||||
b.AssetType,
|
||||
b.Asset,
|
||||
check,
|
||||
token)
|
||||
}
|
||||
|
||||
@@ -93,6 +93,11 @@ func (k *Kraken) SetDefaults() {
|
||||
log.Errorln(log.ExchangeSys, err)
|
||||
}
|
||||
|
||||
err = k.DisableAssetWebsocketSupport(asset.Futures)
|
||||
if err != nil {
|
||||
log.Errorln(log.ExchangeSys, err)
|
||||
}
|
||||
|
||||
k.Features = exchange.Features{
|
||||
Supports: exchange.FeaturesSupported{
|
||||
REST: true,
|
||||
@@ -497,10 +502,10 @@ func (k *Kraken) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbo
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (k *Kraken) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: k.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: k.OrderbookVerificationBypass,
|
||||
Exchange: k.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: k.CanVerifyOrderbook,
|
||||
}
|
||||
var err error
|
||||
switch assetType {
|
||||
|
||||
@@ -174,11 +174,11 @@ func (l *LakeBTC) processOrderbook(obUpdate, channel string) error {
|
||||
}
|
||||
|
||||
book := orderbook.Base{
|
||||
Pair: p,
|
||||
LastUpdated: time.Now(),
|
||||
AssetType: asset.Spot,
|
||||
ExchangeName: l.Name,
|
||||
VerificationBypass: l.OrderbookVerificationBypass,
|
||||
Pair: p,
|
||||
LastUpdated: time.Now(),
|
||||
Asset: asset.Spot,
|
||||
Exchange: l.Name,
|
||||
VerifyOrderbook: l.CanVerifyOrderbook,
|
||||
}
|
||||
|
||||
for i := range update.Asks {
|
||||
|
||||
@@ -279,10 +279,10 @@ func (l *LakeBTC) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderb
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (l *LakeBTC) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: l.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: l.OrderbookVerificationBypass,
|
||||
Exchange: l.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: l.CanVerifyOrderbook,
|
||||
}
|
||||
fPair, err := l.FormatExchangeCurrency(p, assetType)
|
||||
if err != nil {
|
||||
|
||||
@@ -247,10 +247,10 @@ func (l *Lbank) FetchOrderbook(currency currency.Pair, assetType asset.Item) (*o
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (l *Lbank) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: l.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: l.OrderbookVerificationBypass,
|
||||
Exchange: l.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: l.CanVerifyOrderbook,
|
||||
}
|
||||
fpair, err := l.FormatExchangeCurrency(p, assetType)
|
||||
if err != nil {
|
||||
|
||||
@@ -218,11 +218,12 @@ func (l *LocalBitcoins) FetchOrderbook(p currency.Pair, assetType asset.Item) (*
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (l *LocalBitcoins) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: l.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: l.OrderbookVerificationBypass,
|
||||
Exchange: l.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: l.CanVerifyOrderbook,
|
||||
}
|
||||
|
||||
orderbookNew, err := l.GetOrderbook(p.Quote.String())
|
||||
if err != nil {
|
||||
return book, err
|
||||
@@ -242,7 +243,7 @@ func (l *LocalBitcoins) UpdateOrderbook(p currency.Pair, assetType asset.Item) (
|
||||
})
|
||||
}
|
||||
|
||||
book.NotAggregated = true
|
||||
book.PriceDuplication = true
|
||||
err = book.Process()
|
||||
if err != nil {
|
||||
return book, err
|
||||
|
||||
@@ -678,14 +678,13 @@ func (o *OKGroup) WsProcessPartialOrderBook(wsEventData *WebsocketOrderBook, ins
|
||||
}
|
||||
|
||||
newOrderBook := orderbook.Base{
|
||||
Asks: asks,
|
||||
Bids: bids,
|
||||
AssetType: a,
|
||||
LastUpdated: wsEventData.Timestamp,
|
||||
Pair: instrument,
|
||||
ExchangeName: o.Name,
|
||||
HasChecksumValidation: true,
|
||||
VerificationBypass: o.OrderbookVerificationBypass,
|
||||
Asks: asks,
|
||||
Bids: bids,
|
||||
Asset: a,
|
||||
LastUpdated: wsEventData.Timestamp,
|
||||
Pair: instrument,
|
||||
Exchange: o.Name,
|
||||
VerifyOrderbook: o.CanVerifyOrderbook,
|
||||
}
|
||||
return o.Websocket.Orderbook.LoadSnapshot(&newOrderBook)
|
||||
}
|
||||
@@ -715,7 +714,10 @@ func (o *OKGroup) WsProcessUpdateOrderbook(wsEventData *WebsocketOrderBook, inst
|
||||
return err
|
||||
}
|
||||
|
||||
updatedOb := o.Websocket.Orderbook.GetOrderbook(instrument, a)
|
||||
updatedOb, err := o.Websocket.Orderbook.GetOrderbook(instrument, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
checksum := o.CalculateUpdateOrderbookChecksum(updatedOb)
|
||||
|
||||
if checksum != wsEventData.Checksum {
|
||||
|
||||
@@ -86,10 +86,10 @@ func (o *OKGroup) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderb
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (o *OKGroup) UpdateOrderbook(p currency.Pair, a asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: o.Name,
|
||||
Pair: p,
|
||||
AssetType: a,
|
||||
VerificationBypass: o.OrderbookVerificationBypass,
|
||||
Exchange: o.Name,
|
||||
Pair: p,
|
||||
Asset: a,
|
||||
VerifyOrderbook: o.CanVerifyOrderbook,
|
||||
}
|
||||
|
||||
if a == asset.Index {
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
|
||||
func testSetup() Base {
|
||||
return Base{
|
||||
ExchangeName: "a",
|
||||
Pair: currency.NewPair(currency.BTC, currency.USD),
|
||||
Exchange: "a",
|
||||
Pair: currency.NewPair(currency.BTC, currency.USD),
|
||||
Asks: []Item{
|
||||
{Price: 7000, Amount: 1},
|
||||
{Price: 7001, Amount: 2},
|
||||
|
||||
353
exchanges/orderbook/depth.go
Normal file
353
exchanges/orderbook/depth.go
Normal file
@@ -0,0 +1,353 @@
|
||||
package orderbook
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/thrasher-corp/gocryptotrader/dispatch"
|
||||
"github.com/thrasher-corp/gocryptotrader/log"
|
||||
)
|
||||
|
||||
// Depth defines a linked list of orderbook items
|
||||
type Depth struct {
|
||||
asks
|
||||
bids
|
||||
|
||||
// unexported stack of nodes
|
||||
stack *stack
|
||||
|
||||
Alert
|
||||
|
||||
mux *dispatch.Mux
|
||||
id uuid.UUID
|
||||
|
||||
options
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
// NewDepth returns a new depth item
|
||||
func newDepth(id uuid.UUID) *Depth {
|
||||
return &Depth{
|
||||
stack: newStack(),
|
||||
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 {
|
||||
log.Errorf(log.ExchangeSys, "Cannot publish orderbook update to mux %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// GetAskLength returns length of asks
|
||||
func (d *Depth) GetAskLength() int {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
return d.asks.length
|
||||
}
|
||||
|
||||
// GetBidLength returns length of bids
|
||||
func (d *Depth) GetBidLength() int {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
return d.bids.length
|
||||
}
|
||||
|
||||
// Retrieve returns the orderbook base a copy of the underlying linked list
|
||||
// spread
|
||||
func (d *Depth) Retrieve() *Base {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
return &Base{
|
||||
Bids: d.bids.retrieve(),
|
||||
Asks: d.asks.retrieve(),
|
||||
Exchange: d.exchange,
|
||||
Asset: d.asset,
|
||||
Pair: d.pair,
|
||||
LastUpdated: d.lastUpdated,
|
||||
LastUpdateID: d.lastUpdateID,
|
||||
PriceDuplication: d.priceDuplication,
|
||||
IsFundingRate: d.isFundingRate,
|
||||
VerifyOrderbook: d.VerifyOrderbook,
|
||||
}
|
||||
}
|
||||
|
||||
// TotalBidAmounts returns the total amount of bids and the total orderbook
|
||||
// bids value
|
||||
func (d *Depth) TotalBidAmounts() (liquidity, value float64) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
return d.bids.amount()
|
||||
}
|
||||
|
||||
// TotalAskAmounts returns the total amount of asks and the total orderbook
|
||||
// asks value
|
||||
func (d *Depth) TotalAskAmounts() (liquidity, value float64) {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
return d.asks.amount()
|
||||
}
|
||||
|
||||
// LoadSnapshot flushes the bids and asks with a snapshot
|
||||
func (d *Depth) LoadSnapshot(bids, asks []Item) {
|
||||
d.m.Lock()
|
||||
d.bids.load(bids, d.stack)
|
||||
d.asks.load(asks, d.stack)
|
||||
d.alert()
|
||||
d.m.Unlock()
|
||||
}
|
||||
|
||||
// Flush flushes the bid and ask depths
|
||||
func (d *Depth) Flush() {
|
||||
d.m.Lock()
|
||||
d.bids.load(nil, d.stack)
|
||||
d.asks.load(nil, d.stack)
|
||||
d.alert()
|
||||
d.m.Unlock()
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if len(bidUpdts) == 0 && len(askUpdts) == 0 {
|
||||
return
|
||||
}
|
||||
d.m.Lock()
|
||||
tn := getNow()
|
||||
if len(bidUpdts) != 0 {
|
||||
d.bids.updateInsertByPrice(bidUpdts, d.stack, maxDepth, tn)
|
||||
}
|
||||
if len(askUpdts) != 0 {
|
||||
d.asks.updateInsertByPrice(askUpdts, d.stack, maxDepth, tn)
|
||||
}
|
||||
d.alert()
|
||||
d.m.Unlock()
|
||||
}
|
||||
|
||||
// UpdateBidAskByID amends details by ID
|
||||
func (d *Depth) UpdateBidAskByID(bidUpdts, askUpdts Items) error {
|
||||
if len(bidUpdts) == 0 && len(askUpdts) == 0 {
|
||||
return nil
|
||||
}
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if len(bidUpdts) != 0 {
|
||||
err := d.bids.updateByID(bidUpdts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(askUpdts) != 0 {
|
||||
err := d.asks.updateByID(askUpdts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
d.alert()
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteBidAskByID deletes a price level by ID
|
||||
func (d *Depth) DeleteBidAskByID(bidUpdts, askUpdts Items, bypassErr bool) error {
|
||||
if len(bidUpdts) == 0 && len(askUpdts) == 0 {
|
||||
return nil
|
||||
}
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if len(bidUpdts) != 0 {
|
||||
err := d.bids.deleteByID(bidUpdts, d.stack, bypassErr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(askUpdts) != 0 {
|
||||
err := d.asks.deleteByID(askUpdts, d.stack, bypassErr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
d.alert()
|
||||
return nil
|
||||
}
|
||||
|
||||
// InsertBidAskByID inserts new updates
|
||||
func (d *Depth) InsertBidAskByID(bidUpdts, askUpdts Items) error {
|
||||
if len(bidUpdts) == 0 && len(askUpdts) == 0 {
|
||||
return nil
|
||||
}
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if len(bidUpdts) != 0 {
|
||||
err := d.bids.insertUpdates(bidUpdts, d.stack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(askUpdts) != 0 {
|
||||
err := d.asks.insertUpdates(askUpdts, d.stack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
d.alert()
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateInsertByID updates or inserts by ID at current price level.
|
||||
func (d *Depth) UpdateInsertByID(bidUpdts, askUpdts Items) error {
|
||||
if len(bidUpdts) == 0 && len(askUpdts) == 0 {
|
||||
return nil
|
||||
}
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
if len(bidUpdts) != 0 {
|
||||
err := d.bids.updateInsertByID(bidUpdts, d.stack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(askUpdts) != 0 {
|
||||
err := d.asks.updateInsertByID(askUpdts, d.stack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
d.alert()
|
||||
return nil
|
||||
}
|
||||
|
||||
// AssignOptions assigns the initial options for the depth instance
|
||||
func (d *Depth) AssignOptions(b *Base) {
|
||||
d.m.Lock()
|
||||
d.options = options{
|
||||
exchange: b.Exchange,
|
||||
pair: b.Pair,
|
||||
asset: b.Asset,
|
||||
lastUpdated: b.LastUpdated,
|
||||
lastUpdateID: b.LastUpdateID,
|
||||
priceDuplication: b.PriceDuplication,
|
||||
isFundingRate: b.IsFundingRate,
|
||||
VerifyOrderbook: b.VerifyOrderbook,
|
||||
restSnapshot: b.RestSnapshot,
|
||||
idAligned: b.IDAlignment,
|
||||
}
|
||||
d.m.Unlock()
|
||||
}
|
||||
|
||||
// SetLastUpdate sets details of last update information
|
||||
func (d *Depth) SetLastUpdate(lastUpdate time.Time, lastUpdateID int64, updateByREST bool) {
|
||||
d.m.Lock()
|
||||
d.lastUpdated = lastUpdate
|
||||
d.lastUpdateID = lastUpdateID
|
||||
d.restSnapshot = updateByREST
|
||||
d.m.Unlock()
|
||||
}
|
||||
|
||||
// GetName returns name of exchange
|
||||
func (d *Depth) GetName() string {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
return d.exchange
|
||||
}
|
||||
|
||||
// IsRestSnapshot returns if the depth item was updated via REST
|
||||
func (d *Depth) IsRestSnapshot() bool {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
return d.restSnapshot
|
||||
}
|
||||
|
||||
// LastUpdateID returns the last Update ID
|
||||
func (d *Depth) LastUpdateID() int64 {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
return d.lastUpdateID
|
||||
}
|
||||
|
||||
// IsFundingRate returns if the depth is a funding rate
|
||||
func (d *Depth) IsFundingRate() bool {
|
||||
d.m.Lock()
|
||||
defer d.m.Unlock()
|
||||
return d.isFundingRate
|
||||
}
|
||||
|
||||
// Alert defines fields required to alert sub-systems of a change of state to
|
||||
// re-check depth list
|
||||
type Alert struct {
|
||||
// Channel to wait for an alert on.
|
||||
forAlert chan struct{}
|
||||
// Lets the updater functions know if there are any routines waiting for an
|
||||
// alert.
|
||||
sema uint32
|
||||
// After closing the forAlert channel this will notify when all the routines
|
||||
// that have waited, have either checked the orderbook depth or finished.
|
||||
wg sync.WaitGroup
|
||||
// Segregated lock only for waiting routines, so as this does not interfere
|
||||
// with the main depth lock, acts as a rolling gate.
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
// alert establishes a state change on the orderbook depth.
|
||||
func (a *Alert) alert() {
|
||||
// CompareAndSwap is used to swap from 1 -> 2 so we don't keep actuating
|
||||
// the opposing compare and swap in method wait. This function can return
|
||||
// freely when an alert operation is in process.
|
||||
if !atomic.CompareAndSwapUint32(&a.sema, 1, 2) {
|
||||
// Return if no waiting routines or currently alerting.
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
// Actuate lock in a different routine, as alerting is a second order
|
||||
// priority compared to updating and releasing calling routine.
|
||||
a.m.Lock()
|
||||
// Closing; alerts many waiting routines.
|
||||
close(a.forAlert)
|
||||
// Wait for waiting routines to receive alert and return.
|
||||
a.wg.Wait()
|
||||
atomic.SwapUint32(&a.sema, 0) // Swap back to neutral state.
|
||||
a.m.Unlock()
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait pauses calling routine until depth change has been established via depth
|
||||
// method alert. Kick allows for cancellation of waiting or when the caller has
|
||||
// has been shut down, if this is not needed it can be set to nil. This
|
||||
// returns a channel so strategies can cleanly wait on a select statement case.
|
||||
func (a *Alert) Wait(kick <-chan struct{}) <-chan bool {
|
||||
reply := make(chan bool)
|
||||
a.m.Lock()
|
||||
a.wg.Add(1)
|
||||
if atomic.CompareAndSwapUint32(&a.sema, 0, 1) {
|
||||
a.forAlert = make(chan struct{})
|
||||
}
|
||||
go a.hold(reply, kick)
|
||||
a.m.Unlock()
|
||||
return reply
|
||||
}
|
||||
|
||||
// hold waits on either channel in the event that the routine has finished or an
|
||||
// alert from a depth update has occurred.
|
||||
func (a *Alert) hold(ch chan<- bool, kick <-chan struct{}) {
|
||||
select {
|
||||
// In a select statement, if by chance there is no receiver or its late,
|
||||
// we can still close and return, limiting dead-lock potential.
|
||||
case <-a.forAlert: // Main waiting channel from alert
|
||||
select {
|
||||
case ch <- false:
|
||||
default:
|
||||
}
|
||||
case <-kick: // This can be nil.
|
||||
select {
|
||||
case ch <- true:
|
||||
default:
|
||||
}
|
||||
}
|
||||
a.wg.Done()
|
||||
close(ch)
|
||||
}
|
||||
404
exchanges/orderbook/depth_test.go
Normal file
404
exchanges/orderbook/depth_test.go
Normal file
@@ -0,0 +1,404 @@
|
||||
package orderbook
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/thrasher-corp/gocryptotrader/currency"
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
|
||||
)
|
||||
|
||||
var id, _ = uuid.NewV4()
|
||||
|
||||
func TestGetLength(t *testing.T) {
|
||||
d := newDepth(id)
|
||||
if d.GetAskLength() != 0 {
|
||||
t.Errorf("expected len %v, but received %v", 0, d.GetAskLength())
|
||||
}
|
||||
|
||||
d.asks.load([]Item{{Price: 1337}}, d.stack)
|
||||
|
||||
if d.GetAskLength() != 1 {
|
||||
t.Errorf("expected len %v, but received %v", 1, d.GetAskLength())
|
||||
}
|
||||
|
||||
d = newDepth(id)
|
||||
if d.GetBidLength() != 0 {
|
||||
t.Errorf("expected len %v, but received %v", 0, d.GetBidLength())
|
||||
}
|
||||
|
||||
d.bids.load([]Item{{Price: 1337}}, d.stack)
|
||||
|
||||
if d.GetBidLength() != 1 {
|
||||
t.Errorf("expected len %v, but received %v", 1, d.GetBidLength())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetrieve(t *testing.T) {
|
||||
d := newDepth(id)
|
||||
d.asks.load([]Item{{Price: 1337}}, d.stack)
|
||||
d.bids.load([]Item{{Price: 1337}}, d.stack)
|
||||
d.options = options{
|
||||
exchange: "THE BIG ONE!!!!!!",
|
||||
pair: currency.NewPair(currency.THETA, currency.USD),
|
||||
asset: "Silly asset",
|
||||
lastUpdated: time.Now(),
|
||||
lastUpdateID: 007,
|
||||
priceDuplication: true,
|
||||
isFundingRate: true,
|
||||
VerifyOrderbook: true,
|
||||
restSnapshot: true,
|
||||
idAligned: true,
|
||||
}
|
||||
|
||||
// If we add anymore options to the options struct later this will complain
|
||||
// generally want to return a full carbon copy
|
||||
mirrored := reflect.Indirect(reflect.ValueOf(d.options))
|
||||
for n := 0; n < mirrored.NumField(); n++ {
|
||||
structVal := mirrored.Field(n)
|
||||
if structVal.IsZero() {
|
||||
t.Fatalf("struct value options not set for field %v",
|
||||
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))
|
||||
}
|
||||
|
||||
if len(theBigD.Bids) != 1 {
|
||||
t.Errorf("expected len %v, but received %v", 1, len(theBigD.Bids))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTotalAmounts(t *testing.T) {
|
||||
d := newDepth(id)
|
||||
|
||||
liquidity, value := d.TotalBidAmounts()
|
||||
if liquidity != 0 || value != 0 {
|
||||
t.Fatalf("liquidity expected %f received %f value expected %f received %f",
|
||||
0.,
|
||||
liquidity,
|
||||
0.,
|
||||
value)
|
||||
}
|
||||
|
||||
liquidity, value = d.TotalAskAmounts()
|
||||
if liquidity != 0 || value != 0 {
|
||||
t.Fatalf("liquidity expected %f received %f value expected %f received %f",
|
||||
0.,
|
||||
liquidity,
|
||||
0.,
|
||||
value)
|
||||
}
|
||||
|
||||
d.asks.load([]Item{{Price: 1337, Amount: 1}}, d.stack)
|
||||
d.bids.load([]Item{{Price: 1337, Amount: 10}}, d.stack)
|
||||
|
||||
liquidity, value = d.TotalBidAmounts()
|
||||
if liquidity != 10 || value != 13370 {
|
||||
t.Fatalf("liquidity expected %f received %f value expected %f received %f",
|
||||
10.,
|
||||
liquidity,
|
||||
13370.,
|
||||
value)
|
||||
}
|
||||
|
||||
liquidity, value = d.TotalAskAmounts()
|
||||
if liquidity != 1 || value != 1337 {
|
||||
t.Fatalf("liquidity expected %f received %f value expected %f received %f",
|
||||
1.,
|
||||
liquidity,
|
||||
1337.,
|
||||
value)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadSnapshot(t *testing.T) {
|
||||
d := newDepth(id)
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1}}, Items{{Price: 1337, Amount: 10}})
|
||||
if d.Retrieve().Asks[0].Price != 1337 || d.Retrieve().Bids[0].Price != 1337 {
|
||||
t.Fatal("not set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlush(t *testing.T) {
|
||||
d := newDepth(id)
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1}}, Items{{Price: 1337, Amount: 10}})
|
||||
d.Flush()
|
||||
if len(d.Retrieve().Asks) != 0 || len(d.Retrieve().Bids) != 0 {
|
||||
t.Fatal("not flushed")
|
||||
}
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1}}, Items{{Price: 1337, Amount: 10}})
|
||||
d.Flush()
|
||||
if len(d.Retrieve().Asks) != 0 || len(d.Retrieve().Bids) != 0 {
|
||||
t.Fatal("not flushed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateBidAskByPrice(t *testing.T) {
|
||||
d := newDepth(id)
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}})
|
||||
d.UpdateBidAskByPrice(Items{{Price: 1337, Amount: 2, ID: 1}}, Items{{Price: 1337, Amount: 2, ID: 2}}, 0)
|
||||
if d.Retrieve().Asks[0].Amount != 2 || d.Retrieve().Bids[0].Amount != 2 {
|
||||
t.Fatal("orderbook amounts not updated correctly")
|
||||
}
|
||||
d.UpdateBidAskByPrice(Items{{Price: 1337, Amount: 0, ID: 1}}, Items{{Price: 1337, Amount: 0, ID: 2}}, 0)
|
||||
if d.GetAskLength() != 0 || d.GetBidLength() != 0 {
|
||||
t.Fatal("orderbook amounts not updated correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteBidAskByID(t *testing.T) {
|
||||
d := newDepth(id)
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}})
|
||||
err := d.DeleteBidAskByID(Items{{Price: 1337, Amount: 2, ID: 1}}, Items{{Price: 1337, Amount: 2, ID: 2}}, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(d.Retrieve().Asks) != 0 || len(d.Retrieve().Bids) != 0 {
|
||||
t.Fatal("items not deleted")
|
||||
}
|
||||
|
||||
err = d.DeleteBidAskByID(Items{{Price: 1337, Amount: 2, ID: 1}}, nil, false)
|
||||
if !errors.Is(err, errIDCannotBeMatched) {
|
||||
t.Fatalf("error expected %v received %v", errIDCannotBeMatched, err)
|
||||
}
|
||||
|
||||
err = d.DeleteBidAskByID(nil, Items{{Price: 1337, Amount: 2, ID: 2}}, false)
|
||||
if !errors.Is(err, errIDCannotBeMatched) {
|
||||
t.Fatalf("error expected %v received %v", errIDCannotBeMatched, err)
|
||||
}
|
||||
|
||||
err = d.DeleteBidAskByID(nil, Items{{Price: 1337, Amount: 2, ID: 2}}, true)
|
||||
if !errors.Is(err, nil) {
|
||||
t.Fatalf("error expected %v received %v", nil, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateBidAskByID(t *testing.T) {
|
||||
d := newDepth(id)
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}})
|
||||
err := d.UpdateBidAskByID(Items{{Price: 1337, Amount: 2, ID: 1}}, Items{{Price: 1337, Amount: 2, ID: 2}})
|
||||
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")
|
||||
}
|
||||
|
||||
// random unmatching IDs
|
||||
err = d.UpdateBidAskByID(Items{{Price: 1337, Amount: 2, ID: 666}}, nil)
|
||||
if !errors.Is(err, errIDCannotBeMatched) {
|
||||
t.Fatalf("error expected %v received %v", errIDCannotBeMatched, err)
|
||||
}
|
||||
|
||||
err = d.UpdateBidAskByID(nil, Items{{Price: 1337, Amount: 2, ID: 69}})
|
||||
if !errors.Is(err, errIDCannotBeMatched) {
|
||||
t.Fatalf("error expected %v received %v", errIDCannotBeMatched, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertBidAskByID(t *testing.T) {
|
||||
d := newDepth(id)
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}})
|
||||
err := d.InsertBidAskByID(Items{{Price: 1338, Amount: 2, ID: 3}}, Items{{Price: 1336, Amount: 2, ID: 4}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(d.Retrieve().Asks) != 2 || len(d.Retrieve().Bids) != 2 {
|
||||
t.Fatal("items not added correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateInsertByID(t *testing.T) {
|
||||
d := newDepth(id)
|
||||
d.LoadSnapshot(Items{{Price: 1337, Amount: 1, ID: 1}}, Items{{Price: 1337, Amount: 10, ID: 2}})
|
||||
|
||||
err := d.UpdateInsertByID(Items{{Price: 1338, Amount: 0, ID: 3}}, Items{{Price: 1336, Amount: 2, ID: 4}})
|
||||
if !errors.Is(err, errAmountCannotBeLessOrEqualToZero) {
|
||||
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}})
|
||||
if !errors.Is(err, errAmountCannotBeLessOrEqualToZero) {
|
||||
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}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(d.Retrieve().Asks) != 2 || len(d.Retrieve().Bids) != 2 {
|
||||
t.Fatal("items not added correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAssignOptions(t *testing.T) {
|
||||
d := Depth{}
|
||||
cp := currency.NewPair(currency.LINK, currency.BTC)
|
||||
tn := time.Now()
|
||||
d.AssignOptions(&Base{
|
||||
Exchange: "test",
|
||||
Pair: cp,
|
||||
Asset: asset.Spot,
|
||||
LastUpdated: tn,
|
||||
LastUpdateID: 1337,
|
||||
PriceDuplication: true,
|
||||
IsFundingRate: true,
|
||||
VerifyOrderbook: true,
|
||||
RestSnapshot: true,
|
||||
IDAlignment: true,
|
||||
})
|
||||
|
||||
if d.exchange != "test" ||
|
||||
d.pair != cp ||
|
||||
d.asset != asset.Spot ||
|
||||
d.lastUpdated != tn ||
|
||||
d.lastUpdateID != 1337 ||
|
||||
!d.priceDuplication ||
|
||||
!d.isFundingRate ||
|
||||
!d.VerifyOrderbook ||
|
||||
!d.restSnapshot ||
|
||||
!d.idAligned {
|
||||
t.Fatal("failed to set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetLastUpdate(t *testing.T) {
|
||||
d := Depth{}
|
||||
tn := time.Now()
|
||||
d.SetLastUpdate(tn, 1337, true)
|
||||
if d.lastUpdated != tn ||
|
||||
d.lastUpdateID != 1337 ||
|
||||
!d.restSnapshot {
|
||||
t.Fatal("failed to set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetName(t *testing.T) {
|
||||
d := Depth{}
|
||||
d.exchange = "test"
|
||||
if d.GetName() != "test" {
|
||||
t.Fatal("failed to get correct value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsRestSnapshot(t *testing.T) {
|
||||
d := Depth{}
|
||||
d.restSnapshot = true
|
||||
if !d.IsRestSnapshot() {
|
||||
t.Fatal("failed to set correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastUpdateID(t *testing.T) {
|
||||
d := Depth{}
|
||||
d.lastUpdateID = 1337
|
||||
if d.LastUpdateID() != 1337 {
|
||||
t.Fatal("failed to get correct value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsFundingRate(t *testing.T) {
|
||||
d := Depth{}
|
||||
d.isFundingRate = true
|
||||
if !d.IsFundingRate() {
|
||||
t.Fatal("failed to get correct value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPublish(t *testing.T) {
|
||||
d := Depth{}
|
||||
d.Publish()
|
||||
}
|
||||
|
||||
func TestWait(t *testing.T) {
|
||||
wait := Alert{}
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// standard alert
|
||||
wg.Add(100)
|
||||
for x := 0; x < 100; x++ {
|
||||
go func() {
|
||||
w := wait.Wait(nil)
|
||||
wg.Done()
|
||||
if <-w {
|
||||
log.Fatal("incorrect routine wait response for alert expecting false")
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
wg.Add(100)
|
||||
isLeaky(&wait, nil, t)
|
||||
wait.alert()
|
||||
wg.Wait()
|
||||
isLeaky(&wait, nil, t)
|
||||
|
||||
// use kick
|
||||
ch := make(chan struct{})
|
||||
wg.Add(100)
|
||||
for x := 0; x < 100; x++ {
|
||||
go func() {
|
||||
w := wait.Wait(ch)
|
||||
wg.Done()
|
||||
if !<-w {
|
||||
log.Fatal("incorrect routine wait response for kick expecting true")
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
wg.Add(100)
|
||||
isLeaky(&wait, ch, t)
|
||||
close(ch)
|
||||
wg.Wait()
|
||||
ch = make(chan struct{})
|
||||
isLeaky(&wait, ch, t)
|
||||
|
||||
// late receivers
|
||||
wg.Add(100)
|
||||
for x := 0; x < 100; x++ {
|
||||
go func(x int) {
|
||||
bb := wait.Wait(ch)
|
||||
wg.Done()
|
||||
if x%2 == 0 {
|
||||
time.Sleep(time.Millisecond * 5)
|
||||
}
|
||||
b := <-bb
|
||||
if b {
|
||||
log.Fatal("incorrect routine wait response since we call alert below; expecting false")
|
||||
}
|
||||
wg.Done()
|
||||
}(x)
|
||||
}
|
||||
wg.Wait()
|
||||
wg.Add(100)
|
||||
isLeaky(&wait, ch, t)
|
||||
wait.alert()
|
||||
wg.Wait()
|
||||
isLeaky(&wait, ch, t)
|
||||
}
|
||||
|
||||
// isLeaky tests to see if the wait functionality is returning an abnormal
|
||||
// channel that is operational when it shouldn't be.
|
||||
func isLeaky(a *Alert, ch chan struct{}, t *testing.T) {
|
||||
t.Helper()
|
||||
check := a.Wait(ch)
|
||||
time.Sleep(time.Millisecond * 5) // When we call wait a routine for hold is
|
||||
// spawned, so for a test we need to add in a time for goschedular to allow
|
||||
// routine to actually wait on the forAlert and kick channels
|
||||
select {
|
||||
case <-check:
|
||||
t.Fatal("leaky waiter")
|
||||
default:
|
||||
}
|
||||
}
|
||||
530
exchanges/orderbook/linked_list.go
Normal file
530
exchanges/orderbook/linked_list.go
Normal file
@@ -0,0 +1,530 @@
|
||||
package orderbook
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var errIDCannotBeMatched = errors.New("cannot match ID on linked list")
|
||||
var errCollisionDetected = errors.New("cannot insert update collision detected")
|
||||
var errAmountCannotBeLessOrEqualToZero = errors.New("amount cannot be less or equal to zero")
|
||||
|
||||
// linkedList defines a linked list for a depth level, reutilisation of nodes
|
||||
// to and from a stack.
|
||||
type linkedList struct {
|
||||
length int
|
||||
head *node
|
||||
}
|
||||
|
||||
// comparison defines expected functionality to compare between two reference
|
||||
// price levels
|
||||
type comparison func(float64, float64) bool
|
||||
|
||||
// load iterates across new items and refreshes linked list. It creates a linked
|
||||
// list exactly the same as the item slice that is supplied, if items is of nil
|
||||
// value it will flush entire list.
|
||||
func (ll *linkedList) load(items Items, stack *stack) {
|
||||
// Tip sets up a pointer to a struct field variable pointer. This is used
|
||||
// so when a node is popped from the stack we can reference that current
|
||||
// nodes' struct 'next' field and set on next iteration without utilising
|
||||
// assignment for example `prev.next = *node`.
|
||||
var tip = &ll.head
|
||||
// Prev denotes a place holder to node and all of its next references need
|
||||
// to be pushed back onto stack.
|
||||
var prev *node
|
||||
for i := range items {
|
||||
if *tip == nil {
|
||||
// Extend node chain
|
||||
*tip = stack.Pop()
|
||||
// Set current node prev to last node
|
||||
(*tip).prev = prev
|
||||
ll.length++
|
||||
}
|
||||
// Set item value
|
||||
(*tip).value = items[i]
|
||||
// Set previous to current node
|
||||
prev = *tip
|
||||
// Set tip to next node
|
||||
tip = &(*tip).next
|
||||
}
|
||||
|
||||
// Push has references to dangling nodes that need to be removed and pushed
|
||||
// back onto stack for re-use
|
||||
var push *node
|
||||
// Cleave unused reference chain from main chain
|
||||
if prev == nil {
|
||||
// The entire chain will need to be pushed back on to stack
|
||||
push = *tip
|
||||
ll.head = nil
|
||||
} else {
|
||||
push = prev.next
|
||||
prev.next = nil
|
||||
}
|
||||
|
||||
// Push unused pointers back on stack
|
||||
for push != nil {
|
||||
pending := push.next
|
||||
stack.Push(push, getNow())
|
||||
ll.length--
|
||||
push = pending
|
||||
}
|
||||
}
|
||||
|
||||
// updateByID amends price by corresponding ID and returns an error if not found
|
||||
func (ll *linkedList) updateByID(updts []Item) error {
|
||||
updates:
|
||||
for x := range updts {
|
||||
for tip := ll.head; tip != nil; tip = tip.next {
|
||||
if updts[x].ID != tip.value.ID { // Filter IDs that don't match
|
||||
continue
|
||||
}
|
||||
if updts[x].Price > 0 {
|
||||
// Only apply changes when zero values are not present, Bitmex
|
||||
// for example sends 0 price values.
|
||||
tip.value.Price = updts[x].Price
|
||||
}
|
||||
tip.value.Amount = updts[x].Amount
|
||||
continue updates
|
||||
}
|
||||
return fmt.Errorf("update error: %w %d not found",
|
||||
errIDCannotBeMatched,
|
||||
updts[x].ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteByID deletes reference by ID
|
||||
func (ll *linkedList) deleteByID(updts Items, stack *stack, bypassErr bool) error {
|
||||
updates:
|
||||
for x := range updts {
|
||||
for tip := &ll.head; *tip != nil; tip = &(*tip).next {
|
||||
if updts[x].ID != (*tip).value.ID {
|
||||
continue
|
||||
}
|
||||
stack.Push(deleteAtTip(ll, tip), getNow())
|
||||
continue updates
|
||||
}
|
||||
if !bypassErr {
|
||||
return fmt.Errorf("delete error: %w %d not found",
|
||||
errIDCannotBeMatched,
|
||||
updts[x].ID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanup reduces the max size of the depth length if exceeded. Is used after
|
||||
// updates have been applied instead of adhoc, reason being its easier to prune
|
||||
// at the end. (cant inline)
|
||||
func (ll *linkedList) cleanup(maxChainLength int, stack *stack) {
|
||||
// Reduces the max length of total linked list chain, occurs after updates
|
||||
// have been implemented as updates can push length out of bounds, if
|
||||
// cleaved after that update, new update might not applied correctly.
|
||||
n := ll.head
|
||||
for i := 0; i < maxChainLength; i++ {
|
||||
if n.next == nil {
|
||||
return
|
||||
}
|
||||
n = n.next
|
||||
}
|
||||
|
||||
// cleave reference to current node
|
||||
if n.prev != nil {
|
||||
n.prev.next = nil
|
||||
} else {
|
||||
ll.head = nil
|
||||
}
|
||||
|
||||
var pruned int
|
||||
for n != nil {
|
||||
pruned++
|
||||
pending := n.next
|
||||
stack.Push(n, getNow())
|
||||
n = pending
|
||||
}
|
||||
ll.length -= pruned
|
||||
}
|
||||
|
||||
// amount returns total depth liquidity and value
|
||||
func (ll *linkedList) amount() (liquidity, value float64) {
|
||||
for tip := ll.head; tip != nil; tip = tip.next {
|
||||
liquidity += tip.value.Amount
|
||||
value += tip.value.Amount * tip.value.Price
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// retrieve returns a full slice of contents from the linked list
|
||||
func (ll *linkedList) retrieve() Items {
|
||||
depth := make(Items, ll.length)
|
||||
iterator := 0
|
||||
for tip := ll.head; tip != nil; tip = tip.next {
|
||||
depth[iterator] = tip.value
|
||||
iterator++
|
||||
}
|
||||
return depth
|
||||
}
|
||||
|
||||
// updateInsertByPrice amends, inserts, moves and cleaves length of depth by
|
||||
// updates
|
||||
func (ll *linkedList) updateInsertByPrice(updts Items, stack *stack, maxChainLength int, compare func(float64, float64) bool, tn now) {
|
||||
for x := range updts {
|
||||
for tip := &ll.head; ; tip = &(*tip).next {
|
||||
if *tip == nil {
|
||||
insertHeadSpecific(ll, updts[x], stack)
|
||||
break
|
||||
}
|
||||
if (*tip).value.Price == updts[x].Price { // Match check
|
||||
if updts[x].Amount <= 0 { // Capture delete update
|
||||
stack.Push(deleteAtTip(ll, tip), tn)
|
||||
} else { // Amend current amount value
|
||||
(*tip).value.Amount = updts[x].Amount
|
||||
}
|
||||
break // Continue updates
|
||||
}
|
||||
|
||||
if compare((*tip).value.Price, updts[x].Price) { // Insert
|
||||
// This check below filters zero values and provides an
|
||||
// optimisation for when select exchanges send a delete update
|
||||
// to a non-existent price level (OTC/Hidden order) so we can
|
||||
// break instantly and reduce the traversal of the entire chain.
|
||||
if updts[x].Amount > 0 {
|
||||
insertAtTip(ll, tip, updts[x], stack)
|
||||
}
|
||||
break // Continue updates
|
||||
}
|
||||
|
||||
if (*tip).next == nil { // Tip is at tail
|
||||
// This check below is just a catch all in the event the above
|
||||
// zero value check fails
|
||||
if updts[x].Amount > 0 {
|
||||
insertAtTail(ll, tip, updts[x], stack)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reduces length of total linked list chain to a maxChainLength value
|
||||
if maxChainLength != 0 && ll.length > maxChainLength {
|
||||
ll.cleanup(maxChainLength, stack)
|
||||
}
|
||||
}
|
||||
|
||||
// updateInsertByID updates or inserts if not found for a bid or ask depth
|
||||
// 1) node ID found amount amended (best case)
|
||||
// 2) node ID found amount and price amended and node moved to correct position
|
||||
// (medium case)
|
||||
// 3) Update price exceeds traversal node price before ID found, save node
|
||||
// address for either; node ID matches then re-address node or end of depth pop
|
||||
// a node from the stack (worst case)
|
||||
func (ll *linkedList) updateInsertByID(updts Items, stack *stack, compare comparison) error {
|
||||
updates:
|
||||
for x := range updts {
|
||||
if updts[x].Amount <= 0 {
|
||||
return errAmountCannotBeLessOrEqualToZero
|
||||
}
|
||||
// bookmark allows for saving of a position of a node in the event that
|
||||
// an update price exceeds the current node price. We can then match an
|
||||
// ID and re-assign that ID's node to that positioning without popping
|
||||
// from the stack and then pushing to the stack later for cleanup.
|
||||
// If the ID is not found we can pop from stack then insert into that
|
||||
// price level
|
||||
var bookmark *node
|
||||
for tip := ll.head; tip != nil; tip = tip.next {
|
||||
if tip.value.ID == updts[x].ID {
|
||||
if tip.value.Price != updts[x].Price { // Price level change
|
||||
if tip.next == nil {
|
||||
// no movement needed just a re-adjustment
|
||||
tip.value.Price = updts[x].Price
|
||||
tip.value.Amount = updts[x].Amount
|
||||
continue updates
|
||||
}
|
||||
// bookmark tip to move this node to correct price level
|
||||
bookmark = tip
|
||||
continue // continue through node depth
|
||||
}
|
||||
// no price change, amend amount and continue update
|
||||
tip.value.Amount = updts[x].Amount
|
||||
continue updates // continue to next update
|
||||
}
|
||||
|
||||
if compare(tip.value.Price, updts[x].Price) {
|
||||
if bookmark != nil { // shift bookmarked node to current tip
|
||||
bookmark.value = updts[x]
|
||||
move(&ll.head, bookmark, tip)
|
||||
continue updates
|
||||
}
|
||||
|
||||
// search for ID
|
||||
for n := tip.next; n != nil; n = n.next {
|
||||
if n.value.ID == updts[x].ID {
|
||||
n.value = updts[x]
|
||||
// inserting before the tip
|
||||
move(&ll.head, n, tip)
|
||||
continue updates
|
||||
}
|
||||
}
|
||||
// ID not matched in depth so add correct level for insert
|
||||
if tip.next == nil {
|
||||
n := stack.Pop()
|
||||
n.value = updts[x]
|
||||
ll.length++
|
||||
if tip.prev == nil {
|
||||
tip.prev = n
|
||||
n.next = tip
|
||||
ll.head = n
|
||||
continue updates
|
||||
}
|
||||
tip.prev.next = n
|
||||
n.prev = tip.prev
|
||||
tip.prev = n
|
||||
n.next = tip
|
||||
continue updates
|
||||
}
|
||||
bookmark = tip
|
||||
break
|
||||
}
|
||||
|
||||
if tip.next == nil {
|
||||
if shiftBookmark(tip, &bookmark, &ll.head, updts[x]) {
|
||||
continue updates
|
||||
}
|
||||
}
|
||||
}
|
||||
n := stack.Pop()
|
||||
n.value = updts[x]
|
||||
insertNodeAtBookmark(ll, bookmark, n) // Won't inline with stack
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// insertUpdates inserts new updates for bids or asks based on price level
|
||||
func (ll *linkedList) insertUpdates(updts Items, stack *stack, comp comparison) error {
|
||||
for x := range updts {
|
||||
var prev *node
|
||||
for tip := &ll.head; ; tip = &(*tip).next {
|
||||
if *tip == nil { // Head
|
||||
n := stack.Pop()
|
||||
n.value = updts[x]
|
||||
n.prev = prev
|
||||
ll.length++
|
||||
*tip = n
|
||||
break // Continue updates
|
||||
}
|
||||
|
||||
if (*tip).value.Price == updts[x].Price { // Price already found
|
||||
return fmt.Errorf("%w for price %f",
|
||||
errCollisionDetected,
|
||||
updts[x].Price)
|
||||
}
|
||||
|
||||
if comp((*tip).value.Price, updts[x].Price) { // Alignment
|
||||
n := stack.Pop()
|
||||
n.value = updts[x]
|
||||
n.prev = prev
|
||||
ll.length++
|
||||
// Reference current with new node
|
||||
(*tip).prev = n
|
||||
// Push tip to the right
|
||||
n.next = *tip
|
||||
// This is the same as prev.next = n
|
||||
*tip = n
|
||||
break // Continue updates
|
||||
}
|
||||
|
||||
if (*tip).next == nil { // Tail
|
||||
insertAtTail(ll, tip, updts[x], stack)
|
||||
break // Continue updates
|
||||
}
|
||||
prev = *tip
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// bids embed a linked list to attach methods for bid depth specific
|
||||
// functionality
|
||||
type bids struct {
|
||||
linkedList
|
||||
}
|
||||
|
||||
// bidCompare ensures price is in correct descending alignment (can inline)
|
||||
func bidCompare(left, right float64) bool {
|
||||
return left < right
|
||||
}
|
||||
|
||||
// updateInsertByPrice amends, inserts, moves and cleaves length of depth by
|
||||
// updates
|
||||
func (ll *bids) updateInsertByPrice(updts Items, stack *stack, maxChainLength int, tn now) {
|
||||
ll.linkedList.updateInsertByPrice(updts, stack, maxChainLength, bidCompare, tn)
|
||||
}
|
||||
|
||||
// updateInsertByID updates or inserts if not found
|
||||
func (ll *bids) updateInsertByID(updts Items, stack *stack) error {
|
||||
return ll.linkedList.updateInsertByID(updts, stack, bidCompare)
|
||||
}
|
||||
|
||||
// insertUpdates inserts new updates for bids based on price level
|
||||
func (ll *bids) insertUpdates(updts Items, stack *stack) error {
|
||||
return ll.linkedList.insertUpdates(updts, stack, bidCompare)
|
||||
}
|
||||
|
||||
// asks embed a linked list to attach methods for ask depth specific
|
||||
// functionality
|
||||
type asks struct {
|
||||
linkedList
|
||||
}
|
||||
|
||||
// askCompare ensures price is in correct ascending alignment (can inline)
|
||||
func askCompare(left, right float64) bool {
|
||||
return left > right
|
||||
}
|
||||
|
||||
// updateInsertByPrice amends, inserts, moves and cleaves length of depth by
|
||||
// updates
|
||||
func (ll *asks) updateInsertByPrice(updts Items, stack *stack, maxChainLength int, tn now) {
|
||||
ll.linkedList.updateInsertByPrice(updts, stack, maxChainLength, askCompare, tn)
|
||||
}
|
||||
|
||||
// updateInsertByID updates or inserts if not found
|
||||
func (ll *asks) updateInsertByID(updts Items, stack *stack) error {
|
||||
return ll.linkedList.updateInsertByID(updts, stack, askCompare)
|
||||
}
|
||||
|
||||
// insertUpdates inserts new updates for asks based on price level
|
||||
func (ll *asks) insertUpdates(updts Items, stack *stack) error {
|
||||
return ll.linkedList.insertUpdates(updts, stack, askCompare)
|
||||
}
|
||||
|
||||
// move moves a node from a point in a node chain to another node position,
|
||||
// this left justified towards head as element zero is the top of the depth
|
||||
// side. (can inline)
|
||||
func move(head **node, from, to *node) {
|
||||
if from.next != nil { // From is at tail
|
||||
from.next.prev = from.prev
|
||||
}
|
||||
if from.prev == nil { // From is at head
|
||||
(*head).next.prev = nil
|
||||
*head = (*head).next
|
||||
} else {
|
||||
from.prev.next = from.next
|
||||
}
|
||||
// insert from node next to 'to' node
|
||||
if to.prev == nil { // Destination is at head position
|
||||
*head = from
|
||||
} else {
|
||||
to.prev.next = from
|
||||
}
|
||||
from.prev = to.prev
|
||||
to.prev = from
|
||||
from.next = to
|
||||
}
|
||||
|
||||
// deleteAtTip removes a node from tip target returns old node (can inline)
|
||||
func deleteAtTip(ll *linkedList, tip **node) *node {
|
||||
// Old is a placeholder for current tips node value to push
|
||||
// back on to the stack.
|
||||
old := *tip
|
||||
switch {
|
||||
case old.prev == nil: // At head position
|
||||
// shift current tip head to the right
|
||||
*tip = old.next
|
||||
// Remove reference to node from chain
|
||||
if old.next != nil { // This is when liquidity hits zero
|
||||
old.next.prev = nil
|
||||
}
|
||||
case old.next == nil: // At tail position
|
||||
// Remove reference to node from chain
|
||||
old.prev.next = nil
|
||||
default:
|
||||
// Reference prior node in chain to next node in chain
|
||||
// bypassing current node
|
||||
old.prev.next = old.next
|
||||
old.next.prev = old.prev
|
||||
}
|
||||
ll.length--
|
||||
return old
|
||||
}
|
||||
|
||||
// insertAtTip inserts at a tip target (can inline)
|
||||
func insertAtTip(ll *linkedList, tip **node, updt Item, stack *stack) {
|
||||
n := stack.Pop()
|
||||
n.value = updt
|
||||
n.next = *tip
|
||||
n.prev = (*tip).prev
|
||||
if (*tip).prev == nil { // Tip is at head
|
||||
// Replace head which will push everything to the right
|
||||
// when this node will reference new node below
|
||||
*tip = n
|
||||
} else {
|
||||
// Reference new node to previous node
|
||||
(*tip).prev.next = n
|
||||
}
|
||||
// Reference next node to new node
|
||||
n.next.prev = n
|
||||
ll.length++
|
||||
}
|
||||
|
||||
// insertAtTail inserts at tail end of node chain (can inline)
|
||||
func insertAtTail(ll *linkedList, tip **node, updt Item, stack *stack) {
|
||||
n := stack.Pop()
|
||||
n.value = updt
|
||||
// Reference tip to new node
|
||||
(*tip).next = n
|
||||
// Reference new node with current tip
|
||||
n.prev = *tip
|
||||
ll.length++
|
||||
}
|
||||
|
||||
// insertHeadSpecific inserts at head specifically there might be an instance
|
||||
// where the liquidity on an exchange does fall to zero through a streaming
|
||||
// endpoint then it comes back online. (can inline)
|
||||
func insertHeadSpecific(ll *linkedList, updt Item, stack *stack) {
|
||||
n := stack.Pop()
|
||||
n.value = updt
|
||||
ll.head = n
|
||||
ll.length++
|
||||
}
|
||||
|
||||
// insertNodeAtBookmark inserts a new node at a bookmarked node position
|
||||
// returns if a node needs to replace head (can inline)
|
||||
func insertNodeAtBookmark(ll *linkedList, bookmark, n *node) {
|
||||
switch {
|
||||
case bookmark == nil: // Zero liquidity and we are rebuilding from scratch
|
||||
ll.head = n
|
||||
case bookmark.prev == nil:
|
||||
n.prev = bookmark.prev
|
||||
bookmark.prev = n
|
||||
n.next = bookmark
|
||||
ll.head = n
|
||||
case bookmark.next == nil:
|
||||
n.prev = bookmark
|
||||
bookmark.next = n
|
||||
default:
|
||||
bookmark.prev.next = n
|
||||
n.prev = bookmark.prev
|
||||
bookmark.prev = n
|
||||
n.next = bookmark
|
||||
}
|
||||
ll.length++
|
||||
}
|
||||
|
||||
// shiftBookmark moves a bookmarked node to the tip's next position or if nil,
|
||||
// sets tip as bookmark (can inline)
|
||||
func shiftBookmark(tip *node, bookmark, head **node, updt Item) bool {
|
||||
if *bookmark == nil { // End of the chain and no bookmark set
|
||||
*bookmark = tip // Set tip to bookmark so we can set a new node there
|
||||
return false
|
||||
}
|
||||
(*bookmark).value = updt
|
||||
(*bookmark).next.prev = (*bookmark).prev
|
||||
if (*bookmark).prev == nil { // Bookmark is at head
|
||||
*head = (*bookmark).next
|
||||
} else {
|
||||
(*bookmark).prev.next = (*bookmark).next
|
||||
}
|
||||
tip.next = *bookmark
|
||||
(*bookmark).prev = tip
|
||||
(*bookmark).next = nil
|
||||
return true
|
||||
}
|
||||
1449
exchanges/orderbook/linked_list_test.go
Normal file
1449
exchanges/orderbook/linked_list_test.go
Normal file
File diff suppressed because it is too large
Load Diff
135
exchanges/orderbook/node.go
Normal file
135
exchanges/orderbook/node.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package orderbook
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
neutral uint32 = iota
|
||||
active
|
||||
)
|
||||
|
||||
var (
|
||||
defaultInterval = time.Minute
|
||||
defaultAllowance = time.Second * 30
|
||||
)
|
||||
|
||||
// node defines a linked list node for an orderbook item
|
||||
type node struct {
|
||||
value Item
|
||||
next *node
|
||||
prev *node
|
||||
// Denotes time pushed to stack, this will influence cleanup routine when
|
||||
// there is a pause or minimal actions during period
|
||||
shelved time.Time
|
||||
}
|
||||
|
||||
// stack defines a FILO list of reusable nodes
|
||||
type stack struct {
|
||||
nodes []*node
|
||||
sema uint32
|
||||
count int32
|
||||
}
|
||||
|
||||
// newStack returns a ptr to a new stack instance, also starts the cleaning
|
||||
// service
|
||||
func newStack() *stack {
|
||||
s := &stack{}
|
||||
go s.cleaner()
|
||||
return s
|
||||
}
|
||||
|
||||
// now defines a time which is now to ensure no other values get passed in
|
||||
type now time.Time
|
||||
|
||||
// getNow returns the time at which it is called
|
||||
func getNow() now {
|
||||
return now(time.Now())
|
||||
}
|
||||
|
||||
// Push pushes a node pointer into the stack to be reused the time is passed in
|
||||
// to allow for inlining which sets the time at which the node is theoretically
|
||||
// pushed to a stack.
|
||||
func (s *stack) Push(n *node, tn now) {
|
||||
if !atomic.CompareAndSwapUint32(&s.sema, neutral, active) {
|
||||
// Stack is in use, for now we can dereference pointer
|
||||
n = nil
|
||||
return
|
||||
}
|
||||
// Adds a time when its placed back on to stack.
|
||||
n.shelved = time.Time(tn)
|
||||
n.next = nil
|
||||
n.prev = nil
|
||||
n.value = Item{}
|
||||
|
||||
// Allows for resize when overflow TODO: rethink this
|
||||
s.nodes = append(s.nodes[:s.count], n)
|
||||
s.count++
|
||||
atomic.StoreUint32(&s.sema, neutral)
|
||||
}
|
||||
|
||||
// Pop returns the last pointer off the stack and reduces the count and if empty
|
||||
// will produce a lovely fresh node
|
||||
func (s *stack) Pop() *node {
|
||||
if !atomic.CompareAndSwapUint32(&s.sema, neutral, active) {
|
||||
// Stack is in use, for now we can allocate a new node pointer
|
||||
return &node{}
|
||||
}
|
||||
|
||||
if s.count == 0 {
|
||||
// Create an empty node when no nodes are in slice or when cleaning
|
||||
// service is running
|
||||
atomic.StoreUint32(&s.sema, neutral)
|
||||
return &node{}
|
||||
}
|
||||
s.count--
|
||||
n := s.nodes[s.count]
|
||||
atomic.StoreUint32(&s.sema, neutral)
|
||||
return n
|
||||
}
|
||||
|
||||
// cleaner (POC) runs to the defaultTimer to clean excess nodes (nodes not being
|
||||
// utilised) TODO: Couple time parameters to check for a reduction in activity.
|
||||
// Add in counter per second function (?) so if there is a lot of activity don't
|
||||
// inhibit stack performance.
|
||||
func (s *stack) cleaner() {
|
||||
tt := time.NewTimer(defaultInterval)
|
||||
sleeperino:
|
||||
for range tt.C {
|
||||
if !atomic.CompareAndSwapUint32(&s.sema, neutral, active) {
|
||||
// Stack is in use, reset timer to zero to recheck for neutral state.
|
||||
tt.Reset(0)
|
||||
continue
|
||||
}
|
||||
// As the old nodes are going to be left justified on this slice we
|
||||
// should just be able to shift the nodes that are still within time
|
||||
// allowance all the way to the left. Not going to resize capacity
|
||||
// because if it can get this big, it might as well stay this big.
|
||||
// TODO: Test and rethink if sizing is an issue
|
||||
for x := int32(0); x < s.count; x++ {
|
||||
if time.Since(s.nodes[x].shelved) > defaultAllowance {
|
||||
// Old node found continue
|
||||
continue
|
||||
}
|
||||
// First good node found, everything to the left of this on the
|
||||
// slice can be reassigned
|
||||
var counter int32
|
||||
for y := int32(0); y+x < s.count; y++ { // Go through good nodes
|
||||
// Reassign
|
||||
s.nodes[y] = s.nodes[y+x]
|
||||
// Add to the changed counter to remove from main
|
||||
// counter
|
||||
counter++
|
||||
}
|
||||
s.count -= counter
|
||||
atomic.StoreUint32(&s.sema, neutral)
|
||||
tt.Reset(defaultInterval)
|
||||
continue sleeperino
|
||||
}
|
||||
// Nodes are old, flush entirety.
|
||||
s.count = 0
|
||||
atomic.StoreUint32(&s.sema, neutral)
|
||||
tt.Reset(defaultInterval)
|
||||
}
|
||||
}
|
||||
101
exchanges/orderbook/node_test.go
Normal file
101
exchanges/orderbook/node_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package orderbook
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPushPop(t *testing.T) {
|
||||
s := newStack()
|
||||
var nSlice []*node
|
||||
for i := 0; i < 100; i++ {
|
||||
nSlice = append(nSlice, s.Pop())
|
||||
}
|
||||
|
||||
if s.getCount() != 0 {
|
||||
t.Fatalf("incorrect stack count expected %v but received %v", 0, s.getCount())
|
||||
}
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
s.Push(nSlice[i], getNow())
|
||||
}
|
||||
|
||||
if s.getCount() != 100 {
|
||||
t.Fatalf("incorrect stack count expected %v but received %v", 100, s.getCount())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleaner(t *testing.T) {
|
||||
s := newStack()
|
||||
var nSlice []*node
|
||||
for i := 0; i < 100; i++ {
|
||||
nSlice = append(nSlice, s.Pop())
|
||||
}
|
||||
|
||||
tn := getNow()
|
||||
for i := 0; i < 50; i++ {
|
||||
s.Push(nSlice[i], tn)
|
||||
}
|
||||
// Makes all the 50 pushed nodes invalid
|
||||
time.Sleep(time.Millisecond * 550)
|
||||
tn = getNow()
|
||||
for i := 50; i < 100; i++ {
|
||||
s.Push(nSlice[i], tn)
|
||||
}
|
||||
time.Sleep(time.Millisecond * 550)
|
||||
if s.getCount() != 50 {
|
||||
t.Fatalf("incorrect stack count expected %v but received %v", 50, s.getCount())
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
if s.getCount() != 0 {
|
||||
t.Fatalf("incorrect stack count expected %v but received %v", 0, s.getCount())
|
||||
}
|
||||
}
|
||||
|
||||
// Display nodes for testing purposes
|
||||
func (s *stack) Display() {
|
||||
for i := int32(0); i < s.getCount(); i++ {
|
||||
fmt.Printf("NODE IN STACK: %+v %p \n", s.nodes[i], s.nodes[i])
|
||||
}
|
||||
fmt.Println("TOTAL COUNT:", s.getCount())
|
||||
}
|
||||
|
||||
// 158 9,521,717 ns/op 9600104 B/op 100001 allocs/op
|
||||
func BenchmarkWithoutStack(b *testing.B) {
|
||||
var n *node
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j := 0; j < 100000; j++ {
|
||||
n = new(node)
|
||||
n.value.Price = 1337
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 316 3,485,211 ns/op 1 B/op 0 allocs/op
|
||||
func BenchmarkWithStack(b *testing.B) {
|
||||
var n *node
|
||||
stack := newStack()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
tn := getNow()
|
||||
for i := 0; i < b.N; i++ {
|
||||
for j := 0; j < 100000; j++ {
|
||||
n = stack.Pop()
|
||||
n.value.Price = 1337
|
||||
stack.Push(n, tn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getCount is a test helper function to derive the count that does not race.
|
||||
func (s *stack) getCount() int32 {
|
||||
if !atomic.CompareAndSwapUint32(&s.sema, neutral, active) {
|
||||
return -1
|
||||
}
|
||||
defer atomic.StoreUint32(&s.sema, neutral)
|
||||
return s.count
|
||||
}
|
||||
@@ -18,144 +18,181 @@ func Get(exchange string, p currency.Pair, a asset.Item) (*Base, error) {
|
||||
return service.Retrieve(exchange, p, a)
|
||||
}
|
||||
|
||||
// SubscribeOrderbook subcribes to an orderbook and returns a communication
|
||||
// channel to stream orderbook data updates
|
||||
func SubscribeOrderbook(exchange string, p currency.Pair, a asset.Item) (dispatch.Pipe, error) {
|
||||
exchange = strings.ToLower(exchange)
|
||||
service.Lock()
|
||||
defer service.Unlock()
|
||||
book, ok := service.Books[exchange][a][p.Base.Item][p.Quote.Item]
|
||||
if !ok {
|
||||
return dispatch.Pipe{},
|
||||
fmt.Errorf("orderbook item not found for %s %s %s",
|
||||
exchange,
|
||||
p,
|
||||
a)
|
||||
}
|
||||
return service.mux.Subscribe(book.Main)
|
||||
// GetDepth returns a Depth pointer allowing the caller to stream orderbook
|
||||
// changes
|
||||
func GetDepth(exchange string, p currency.Pair, a asset.Item) (*Depth, error) {
|
||||
return service.GetDepth(exchange, p, a)
|
||||
}
|
||||
|
||||
// SubscribeToExchangeOrderbooks subcribes to all orderbooks on an exchange
|
||||
// DeployDepth sets a depth struct and returns a depth pointer. This allows for
|
||||
// the loading of a new orderbook snapshot and incremental updates via the
|
||||
// streaming package.
|
||||
func DeployDepth(exchange string, p currency.Pair, a asset.Item) (*Depth, error) {
|
||||
return service.DeployDepth(exchange, p, a)
|
||||
}
|
||||
|
||||
// SubscribeToExchangeOrderbooks returns a pipe to an exchange feed
|
||||
func SubscribeToExchangeOrderbooks(exchange string) (dispatch.Pipe, error) {
|
||||
service.Lock()
|
||||
defer service.Unlock()
|
||||
id, ok := service.Exchange[strings.ToLower(exchange)]
|
||||
exch, ok := service.books[strings.ToLower(exchange)]
|
||||
if !ok {
|
||||
return dispatch.Pipe{}, fmt.Errorf("%s exchange orderbooks not found",
|
||||
exchange)
|
||||
return dispatch.Pipe{}, fmt.Errorf("%w for %s exchange",
|
||||
errCannotFindOrderbook, exchange)
|
||||
}
|
||||
return service.mux.Subscribe(id)
|
||||
return service.Mux.Subscribe(exch.ID)
|
||||
}
|
||||
|
||||
// Update stores orderbook data
|
||||
func (s *Service) Update(b *Base) error {
|
||||
name := strings.ToLower(b.ExchangeName)
|
||||
name := strings.ToLower(b.Exchange)
|
||||
s.Lock()
|
||||
m1, ok := s.Books[name]
|
||||
m1, ok := s.books[name]
|
||||
if !ok {
|
||||
m1 = make(map[asset.Item]map[*currency.Item]map[*currency.Item]*Book)
|
||||
s.Books[name] = m1
|
||||
id, err := s.Mux.GetID()
|
||||
if err != nil {
|
||||
s.Unlock()
|
||||
return err
|
||||
}
|
||||
m1 = Exchange{
|
||||
m: make(map[asset.Item]map[*currency.Item]map[*currency.Item]*Depth),
|
||||
ID: id,
|
||||
}
|
||||
s.books[name] = m1
|
||||
}
|
||||
|
||||
m2, ok := m1[b.AssetType]
|
||||
m2, ok := m1.m[b.Asset]
|
||||
if !ok {
|
||||
m2 = make(map[*currency.Item]map[*currency.Item]*Book)
|
||||
m1[b.AssetType] = m2
|
||||
m2 = make(map[*currency.Item]map[*currency.Item]*Depth)
|
||||
m1.m[b.Asset] = m2
|
||||
}
|
||||
|
||||
m3, ok := m2[b.Pair.Base.Item]
|
||||
if !ok {
|
||||
m3 = make(map[*currency.Item]*Book)
|
||||
m3 = make(map[*currency.Item]*Depth)
|
||||
m2[b.Pair.Base.Item] = m3
|
||||
}
|
||||
|
||||
book, ok := m3[b.Pair.Quote.Item]
|
||||
if !ok {
|
||||
book = new(Book)
|
||||
book = newDepth(m1.ID)
|
||||
book.AssignOptions(b)
|
||||
m3[b.Pair.Quote.Item] = book
|
||||
err := s.SetNewData(b, book, name)
|
||||
s.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
book.b.Bids = append(b.Bids[:0:0], b.Bids...) // nolint:gocritic // Short hand to not use make and copy
|
||||
book.b.Asks = append(b.Asks[:0:0], b.Asks...) // nolint:gocritic // Short hand to not use make and copy
|
||||
book.b.LastUpdated = b.LastUpdated
|
||||
ids := append(book.Assoc, book.Main)
|
||||
book.SetLastUpdate(b.LastUpdated, b.LastUpdateID, true)
|
||||
book.LoadSnapshot(b.Bids, b.Asks)
|
||||
s.Unlock()
|
||||
return s.mux.Publish(ids, b)
|
||||
return s.Mux.Publish([]uuid.UUID{m1.ID}, book.Retrieve())
|
||||
}
|
||||
|
||||
// SetNewData sets new data
|
||||
func (s *Service) SetNewData(ob *Base, book *Book, exch string) error {
|
||||
var err error
|
||||
book.Assoc, err = s.getAssociations(strings.ToLower(exch))
|
||||
if err != nil {
|
||||
return err
|
||||
// DeployDepth used for subsystem deployment creates a depth item in the struct
|
||||
// then returns a ptr to that Depth item
|
||||
func (s *Service) DeployDepth(exchange string, p currency.Pair, a asset.Item) (*Depth, error) {
|
||||
if exchange == "" {
|
||||
return nil, errExchangeNameUnset
|
||||
}
|
||||
book.Main, err = s.mux.GetID()
|
||||
if err != nil {
|
||||
return err
|
||||
if p.IsEmpty() {
|
||||
return nil, errPairNotSet
|
||||
}
|
||||
|
||||
// Below instigates orderbook item separation so we can ensure, in the event
|
||||
// of a simultaneous update via websocket/rest/fix, we don't affect package
|
||||
// scoped orderbook data which could result in a potential panic
|
||||
cpy := *ob
|
||||
cpy.Bids = append(cpy.Bids[:0:0], cpy.Bids...)
|
||||
cpy.Asks = append(cpy.Asks[:0:0], cpy.Asks...)
|
||||
book.b = &cpy
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAssociations links a singular book with it's dispatch associations
|
||||
func (s *Service) getAssociations(exch string) ([]uuid.UUID, error) {
|
||||
var ids []uuid.UUID
|
||||
exchangeID, ok := s.Exchange[exch]
|
||||
if !a.IsValid() {
|
||||
return nil, errAssetTypeNotSet
|
||||
}
|
||||
s.Lock()
|
||||
m1, ok := s.books[strings.ToLower(exchange)]
|
||||
if !ok {
|
||||
var err error
|
||||
exchangeID, err = s.mux.GetID()
|
||||
id, err := s.Mux.GetID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.Exchange[exch] = exchangeID
|
||||
m1 = Exchange{
|
||||
m: make(map[asset.Item]map[*currency.Item]map[*currency.Item]*Depth),
|
||||
ID: id,
|
||||
}
|
||||
s.books[strings.ToLower(exchange)] = m1
|
||||
}
|
||||
|
||||
ids = append(ids, exchangeID)
|
||||
return ids, nil
|
||||
m2, ok := m1.m[a]
|
||||
if !ok {
|
||||
m2 = make(map[*currency.Item]map[*currency.Item]*Depth)
|
||||
m1.m[a] = m2
|
||||
}
|
||||
m3, ok := m2[p.Base.Item]
|
||||
if !ok {
|
||||
m3 = make(map[*currency.Item]*Depth)
|
||||
m2[p.Base.Item] = m3
|
||||
}
|
||||
book, ok := m3[p.Quote.Item]
|
||||
if !ok {
|
||||
book = newDepth(m1.ID)
|
||||
m3[p.Quote.Item] = book
|
||||
}
|
||||
s.Unlock()
|
||||
return book, nil
|
||||
}
|
||||
|
||||
// Retrieve gets orderbook data from the slice
|
||||
func (s *Service) Retrieve(exchange string, p currency.Pair, a asset.Item) (*Base, error) {
|
||||
// GetDepth returns the actual depth struct for potential subsystems and
|
||||
// strategies to interact with
|
||||
func (s *Service) GetDepth(exchange string, p currency.Pair, a asset.Item) (*Depth, error) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
m1, ok := s.Books[strings.ToLower(exchange)]
|
||||
m1, ok := s.books[strings.ToLower(exchange)]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no orderbooks for %s exchange", exchange)
|
||||
return nil, fmt.Errorf("%w for %s exchange",
|
||||
errCannotFindOrderbook, exchange)
|
||||
}
|
||||
|
||||
m2, ok := m1[a]
|
||||
m2, ok := m1.m[a]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no orderbooks associated with asset type %s",
|
||||
return nil, fmt.Errorf("%w associated with asset type %s",
|
||||
errCannotFindOrderbook,
|
||||
a)
|
||||
}
|
||||
|
||||
m3, ok := m2[p.Base.Item]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no orderbooks associated with base currency %s",
|
||||
return nil, fmt.Errorf("%w associated with base currency %s",
|
||||
errCannotFindOrderbook,
|
||||
p.Base)
|
||||
}
|
||||
|
||||
book, ok := m3[p.Quote.Item]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no orderbooks associated with base currency %s",
|
||||
return nil, fmt.Errorf("%w associated with base currency %s",
|
||||
errCannotFindOrderbook,
|
||||
p.Quote)
|
||||
}
|
||||
return book, nil
|
||||
}
|
||||
|
||||
ob := *book.b
|
||||
ob.Bids = append(ob.Bids[:0:0], ob.Bids...)
|
||||
ob.Asks = append(ob.Asks[:0:0], ob.Asks...)
|
||||
return &ob, nil
|
||||
// Retrieve gets orderbook depth data from the associated linked list and
|
||||
// returns the base equivalent copy
|
||||
func (s *Service) Retrieve(exchange string, p currency.Pair, a asset.Item) (*Base, error) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
m1, ok := s.books[strings.ToLower(exchange)]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w for %s exchange",
|
||||
errCannotFindOrderbook,
|
||||
exchange)
|
||||
}
|
||||
m2, ok := m1.m[a]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w associated with asset type %s",
|
||||
errCannotFindOrderbook,
|
||||
a)
|
||||
}
|
||||
m3, ok := m2[p.Base.Item]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w associated with base currency %s",
|
||||
errCannotFindOrderbook,
|
||||
p.Base)
|
||||
}
|
||||
book, ok := m3[p.Quote.Item]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w associated with base currency %s",
|
||||
errCannotFindOrderbook,
|
||||
p.Quote)
|
||||
}
|
||||
return book.Retrieve(), nil
|
||||
}
|
||||
|
||||
// TotalBidsAmount returns the total amount of bids and the total orderbook
|
||||
@@ -178,18 +215,15 @@ func (b *Base) TotalAsksAmount() (amountCollated, total float64) {
|
||||
return amountCollated, total
|
||||
}
|
||||
|
||||
// Update updates the bids and asks
|
||||
func (b *Base) Update(bids, asks []Item) {
|
||||
b.Bids = bids
|
||||
b.Asks = asks
|
||||
b.LastUpdated = time.Now()
|
||||
}
|
||||
|
||||
// Verify ensures that the orderbook items are correctly sorted prior to being
|
||||
// set and will reject any book with incorrect values.
|
||||
// Bids should always go from a high price to a low price and
|
||||
// Asks should always go from a low price to a higher price
|
||||
func (b *Base) Verify() error {
|
||||
if !b.VerifyOrderbook {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Checking for both ask and bid lengths being zero has been removed and
|
||||
// a warning has been put in place some exchanges e.g. LakeBTC return zero
|
||||
// level books. In the event that there is a massive liquidity change where
|
||||
@@ -198,58 +232,68 @@ func (b *Base) Verify() error {
|
||||
if len(b.Asks) == 0 || len(b.Bids) == 0 {
|
||||
log.Warnf(log.OrderBook,
|
||||
bookLengthIssue,
|
||||
b.ExchangeName,
|
||||
b.Exchange,
|
||||
b.Pair,
|
||||
b.AssetType,
|
||||
b.Asset,
|
||||
len(b.Bids),
|
||||
len(b.Asks))
|
||||
}
|
||||
for i := range b.Bids {
|
||||
if b.Bids[i].Price == 0 {
|
||||
return fmt.Errorf(bidLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errPriceNotSet)
|
||||
}
|
||||
if b.Bids[i].Amount <= 0 {
|
||||
return fmt.Errorf(bidLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errAmountInvalid)
|
||||
}
|
||||
if b.IsFundingRate && b.Bids[i].Period == 0 {
|
||||
return fmt.Errorf(bidLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errPeriodUnset)
|
||||
}
|
||||
if i != 0 {
|
||||
if b.Bids[i].Price > b.Bids[i-1].Price {
|
||||
return fmt.Errorf(bidLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errOutOfOrder)
|
||||
}
|
||||
|
||||
if !b.NotAggregated && b.Bids[i].Price == b.Bids[i-1].Price {
|
||||
return fmt.Errorf(bidLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errDuplication)
|
||||
}
|
||||
|
||||
if b.Bids[i].ID != 0 && b.Bids[i].ID == b.Bids[i-1].ID {
|
||||
return fmt.Errorf(bidLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errIDDuplication)
|
||||
}
|
||||
}
|
||||
err := checkAlignment(b.Bids, b.IsFundingRate, b.PriceDuplication, b.IDAlignment, dsc)
|
||||
if err != nil {
|
||||
return fmt.Errorf(bidLoadBookFailure, b.Exchange, b.Pair, b.Asset, err)
|
||||
}
|
||||
err = checkAlignment(b.Asks, b.IsFundingRate, b.PriceDuplication, b.IDAlignment, asc)
|
||||
if err != nil {
|
||||
return fmt.Errorf(askLoadBookFailure, b.Exchange, b.Pair, b.Asset, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
for i := range b.Asks {
|
||||
if b.Asks[i].Price == 0 {
|
||||
return fmt.Errorf(askLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errPriceNotSet)
|
||||
// checker defines specific functionality to determine ascending/descending
|
||||
// validation
|
||||
type checker func(current Item, previous Item) error
|
||||
|
||||
// asc specifically defines ascending price check
|
||||
var asc = func(current Item, previous Item) error {
|
||||
if current.Price < previous.Price {
|
||||
return errPriceOutOfOrder
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dsc specifically defines descending price check
|
||||
var dsc = func(current Item, previous Item) error {
|
||||
if current.Price > previous.Price {
|
||||
return errPriceOutOfOrder
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkAlignment validates full orderbook
|
||||
func checkAlignment(depth Items, fundingRate, priceDuplication, isIDAligned bool, c checker) error {
|
||||
for i := range depth {
|
||||
if depth[i].Price == 0 {
|
||||
return errPriceNotSet
|
||||
}
|
||||
if b.Asks[i].Amount <= 0 {
|
||||
return fmt.Errorf(askLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errAmountInvalid)
|
||||
if depth[i].Amount <= 0 {
|
||||
return errAmountInvalid
|
||||
}
|
||||
if b.IsFundingRate && b.Asks[i].Period == 0 {
|
||||
return fmt.Errorf(askLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errPeriodUnset)
|
||||
if fundingRate && depth[i].Period == 0 {
|
||||
return errPeriodUnset
|
||||
}
|
||||
if i != 0 {
|
||||
if b.Asks[i].Price < b.Asks[i-1].Price {
|
||||
return fmt.Errorf(askLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errOutOfOrder)
|
||||
prev := i - 1
|
||||
if err := c(depth[i], depth[prev]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !b.NotAggregated && b.Asks[i].Price == b.Asks[i-1].Price {
|
||||
return fmt.Errorf(askLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errDuplication)
|
||||
if isIDAligned && depth[i].ID < depth[prev].ID {
|
||||
return errIDOutOfOrder
|
||||
}
|
||||
|
||||
if b.Asks[i].ID != 0 && b.Asks[i].ID == b.Asks[i-1].ID {
|
||||
return fmt.Errorf(askLoadBookFailure, b.ExchangeName, b.Pair, b.AssetType, errIDDuplication)
|
||||
if !priceDuplication && depth[i].Price == depth[prev].Price {
|
||||
return errDuplication
|
||||
}
|
||||
if depth[i].ID != 0 && depth[i].ID == depth[prev].ID {
|
||||
return errIDDuplication
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -259,7 +303,7 @@ func (b *Base) Verify() error {
|
||||
// Process processes incoming orderbooks, creating or updating the orderbook
|
||||
// list
|
||||
func (b *Base) Process() error {
|
||||
if b.ExchangeName == "" {
|
||||
if b.Exchange == "" {
|
||||
return errExchangeNameUnset
|
||||
}
|
||||
|
||||
@@ -267,7 +311,7 @@ func (b *Base) Process() error {
|
||||
return errPairNotSet
|
||||
}
|
||||
|
||||
if b.AssetType.String() == "" {
|
||||
if b.Asset.String() == "" {
|
||||
return errAssetTypeNotSet
|
||||
}
|
||||
|
||||
@@ -275,11 +319,9 @@ func (b *Base) Process() error {
|
||||
b.LastUpdated = time.Now()
|
||||
}
|
||||
|
||||
if !b.VerificationBypass && !b.HasChecksumValidation {
|
||||
err := b.Verify()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err := b.Verify()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return service.Update(b)
|
||||
}
|
||||
@@ -290,27 +332,25 @@ func (b *Base) Process() error {
|
||||
// using a sort algorithm as the algorithm could be impeded by a worst case time
|
||||
// complexity when elements are shifted as opposed to just swapping element
|
||||
// values.
|
||||
func Reverse(elem []Item) {
|
||||
eLen := len(elem)
|
||||
func (elem *Items) Reverse() {
|
||||
eLen := len(*elem)
|
||||
var target int
|
||||
for i := eLen/2 - 1; i >= 0; i-- {
|
||||
target = eLen - 1 - i
|
||||
elem[i], elem[target] = elem[target], elem[i]
|
||||
(*elem)[i], (*elem)[target] = (*elem)[target], (*elem)[i]
|
||||
}
|
||||
}
|
||||
|
||||
// SortAsks sorts ask items to the correct ascending order if pricing values are
|
||||
// scattered. If order from exchange is descending consider using the Reverse
|
||||
// function.
|
||||
func SortAsks(d []Item) []Item {
|
||||
sort.Sort(byOBPrice(d))
|
||||
return d
|
||||
func (elem *Items) SortAsks() {
|
||||
sort.Sort(byOBPrice(*elem))
|
||||
}
|
||||
|
||||
// SortBids sorts bid items to the correct descending order if pricing values
|
||||
// are scattered. If order from exchange is ascending consider using the Reverse
|
||||
// function.
|
||||
func SortBids(d []Item) []Item {
|
||||
sort.Sort(sort.Reverse(byOBPrice(d)))
|
||||
return d
|
||||
func (elem *Items) SortBids() {
|
||||
sort.Sort(sort.Reverse(byOBPrice(*elem)))
|
||||
}
|
||||
|
||||
@@ -16,105 +16,29 @@ import (
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
// Sets up lower values for test environment
|
||||
defaultInterval = time.Second
|
||||
defaultAllowance = time.Millisecond * 500
|
||||
err := dispatch.Start(1, dispatch.DefaultJobsLimit)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
cpyMux = service.mux
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
var cpyMux *dispatch.Mux
|
||||
|
||||
func TestSubscribeOrderbook(t *testing.T) {
|
||||
_, err := SubscribeOrderbook("", currency.Pair{}, asset.Item(""))
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
}
|
||||
|
||||
p := currency.NewPair(currency.BTC, currency.USD)
|
||||
|
||||
b := Base{
|
||||
Pair: p,
|
||||
AssetType: asset.Spot,
|
||||
}
|
||||
|
||||
err = b.Process()
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
}
|
||||
|
||||
b.ExchangeName = "SubscribeOBTest"
|
||||
b.Bids = []Item{{Price: 100, Amount: 1}, {Price: 99, Amount: 1}}
|
||||
err = b.Process()
|
||||
if err != nil {
|
||||
t.Error("process error", err)
|
||||
}
|
||||
|
||||
_, err = SubscribeOrderbook("SubscribeOBTest", p, asset.Spot)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
// process redundant update
|
||||
err = b.Process()
|
||||
if err != nil {
|
||||
t.Error("process error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateBooks(t *testing.T) {
|
||||
p := currency.NewPair(currency.BTC, currency.USD)
|
||||
|
||||
b := Base{
|
||||
Pair: p,
|
||||
AssetType: asset.Spot,
|
||||
ExchangeName: "UpdateTest",
|
||||
}
|
||||
|
||||
service.mux = nil
|
||||
|
||||
err := service.Update(&b)
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
}
|
||||
|
||||
b.Pair.Base = currency.CYC
|
||||
err = service.Update(&b)
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
}
|
||||
|
||||
b.Pair.Quote = currency.ENAU
|
||||
err = service.Update(&b)
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
}
|
||||
|
||||
b.AssetType = "unicorns"
|
||||
err = service.Update(&b)
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
}
|
||||
|
||||
service.mux = cpyMux
|
||||
}
|
||||
|
||||
func TestSubscribeToExchangeOrderbooks(t *testing.T) {
|
||||
_, err := SubscribeToExchangeOrderbooks("")
|
||||
if err == nil {
|
||||
t.Error("error cannot be nil")
|
||||
if !errors.Is(err, errCannotFindOrderbook) {
|
||||
t.Fatalf("expected: %v but received: %v", errCannotFindOrderbook, err)
|
||||
}
|
||||
|
||||
p := currency.NewPair(currency.BTC, currency.USD)
|
||||
|
||||
b := Base{
|
||||
Pair: p,
|
||||
AssetType: asset.Spot,
|
||||
ExchangeName: "SubscribeToExchangeOrderbooks",
|
||||
Bids: []Item{{Price: 100, Amount: 1}, {Price: 99, Amount: 1}},
|
||||
Pair: p,
|
||||
Asset: asset.Spot,
|
||||
Exchange: "SubscribeToExchangeOrderbooks",
|
||||
Bids: []Item{{Price: 100, Amount: 1}, {Price: 99, Amount: 1}},
|
||||
}
|
||||
|
||||
err = b.Process()
|
||||
@@ -131,9 +55,10 @@ func TestSubscribeToExchangeOrderbooks(t *testing.T) {
|
||||
func TestVerify(t *testing.T) {
|
||||
t.Parallel()
|
||||
b := Base{
|
||||
ExchangeName: "TestExchange",
|
||||
AssetType: asset.Spot,
|
||||
Pair: currency.NewPair(currency.BTC, currency.USD),
|
||||
Exchange: "TestExchange",
|
||||
Asset: asset.Spot,
|
||||
Pair: currency.NewPair(currency.BTC, currency.USD),
|
||||
VerifyOrderbook: true,
|
||||
}
|
||||
|
||||
err := b.Verify()
|
||||
@@ -143,75 +68,75 @@ func TestVerify(t *testing.T) {
|
||||
|
||||
b.Asks = []Item{{ID: 1337, Price: 99, Amount: 1}, {ID: 1337, Price: 100, Amount: 1}}
|
||||
err = b.Verify()
|
||||
if err == nil || !errors.Is(err, errIDDuplication) {
|
||||
if !errors.Is(err, errIDDuplication) {
|
||||
t.Fatalf("expecting %s error but received %v", errIDDuplication, err)
|
||||
}
|
||||
|
||||
b.Asks = []Item{{Price: 100, Amount: 1}, {Price: 100, Amount: 1}}
|
||||
err = b.Verify()
|
||||
if err == nil || !errors.Is(err, errDuplication) {
|
||||
if !errors.Is(err, errDuplication) {
|
||||
t.Fatalf("expecting %s error but received %v", errDuplication, err)
|
||||
}
|
||||
|
||||
b.Asks = []Item{{Price: 100, Amount: 1}, {Price: 99, Amount: 1}}
|
||||
b.IsFundingRate = true
|
||||
err = b.Verify()
|
||||
if err == nil || !errors.Is(err, errPeriodUnset) {
|
||||
if !errors.Is(err, errPeriodUnset) {
|
||||
t.Fatalf("expecting %s error but received %v", errPeriodUnset, err)
|
||||
}
|
||||
b.IsFundingRate = false
|
||||
|
||||
err = b.Verify()
|
||||
if err == nil || !errors.Is(err, errOutOfOrder) {
|
||||
t.Fatalf("expecting %s error but received %v", errOutOfOrder, err)
|
||||
if !errors.Is(err, errPriceOutOfOrder) {
|
||||
t.Fatalf("expecting %s error but received %v", errPriceOutOfOrder, err)
|
||||
}
|
||||
|
||||
b.Asks = []Item{{Price: 100, Amount: 1}, {Price: 100, Amount: 0}}
|
||||
err = b.Verify()
|
||||
if err == nil || !errors.Is(err, errAmountInvalid) {
|
||||
if !errors.Is(err, errAmountInvalid) {
|
||||
t.Fatalf("expecting %s error but received %v", errAmountInvalid, err)
|
||||
}
|
||||
|
||||
b.Asks = []Item{{Price: 100, Amount: 1}, {Price: 0, Amount: 100}}
|
||||
err = b.Verify()
|
||||
if err == nil || !errors.Is(err, errPriceNotSet) {
|
||||
if !errors.Is(err, errPriceNotSet) {
|
||||
t.Fatalf("expecting %s error but received %v", errPriceNotSet, err)
|
||||
}
|
||||
|
||||
b.Bids = []Item{{ID: 1337, Price: 100, Amount: 1}, {ID: 1337, Price: 99, Amount: 1}}
|
||||
err = b.Verify()
|
||||
if err == nil || !errors.Is(err, errIDDuplication) {
|
||||
if !errors.Is(err, errIDDuplication) {
|
||||
t.Fatalf("expecting %s error but received %v", errIDDuplication, err)
|
||||
}
|
||||
|
||||
b.Bids = []Item{{Price: 100, Amount: 1}, {Price: 100, Amount: 1}}
|
||||
err = b.Verify()
|
||||
if err == nil || !errors.Is(err, errDuplication) {
|
||||
if !errors.Is(err, errDuplication) {
|
||||
t.Fatalf("expecting %s error but received %v", errDuplication, err)
|
||||
}
|
||||
|
||||
b.Bids = []Item{{Price: 99, Amount: 1}, {Price: 100, Amount: 1}}
|
||||
b.IsFundingRate = true
|
||||
err = b.Verify()
|
||||
if err == nil || !errors.Is(err, errPeriodUnset) {
|
||||
if !errors.Is(err, errPeriodUnset) {
|
||||
t.Fatalf("expecting %s error but received %v", errPeriodUnset, err)
|
||||
}
|
||||
b.IsFundingRate = false
|
||||
|
||||
err = b.Verify()
|
||||
if err == nil || !errors.Is(err, errOutOfOrder) {
|
||||
t.Fatalf("expecting %s error but received %v", errOutOfOrder, err)
|
||||
if !errors.Is(err, errPriceOutOfOrder) {
|
||||
t.Fatalf("expecting %s error but received %v", errPriceOutOfOrder, err)
|
||||
}
|
||||
|
||||
b.Bids = []Item{{Price: 100, Amount: 1}, {Price: 100, Amount: 0}}
|
||||
err = b.Verify()
|
||||
if err == nil || !errors.Is(err, errAmountInvalid) {
|
||||
if !errors.Is(err, errAmountInvalid) {
|
||||
t.Fatalf("expecting %s error but received %v", errAmountInvalid, err)
|
||||
}
|
||||
|
||||
b.Bids = []Item{{Price: 100, Amount: 1}, {Price: 0, Amount: 100}}
|
||||
err = b.Verify()
|
||||
if err == nil || !errors.Is(err, errPriceNotSet) {
|
||||
if !errors.Is(err, errPriceNotSet) {
|
||||
t.Fatalf("expecting %s error but received %v", errPriceNotSet, err)
|
||||
}
|
||||
}
|
||||
@@ -251,51 +176,17 @@ func TestCalculateTotaAsks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
t.Parallel()
|
||||
curr, err := currency.NewPairFromStrings("BTC", "USD")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
timeNow := time.Now()
|
||||
base := Base{
|
||||
Pair: curr,
|
||||
Asks: []Item{{Price: 100, Amount: 10}},
|
||||
Bids: []Item{{Price: 200, Amount: 10}},
|
||||
LastUpdated: timeNow,
|
||||
}
|
||||
|
||||
asks := []Item{{Price: 200, Amount: 101}}
|
||||
bids := []Item{{Price: 201, Amount: 100}}
|
||||
time.Sleep(time.Millisecond * 50)
|
||||
base.Update(bids, asks)
|
||||
|
||||
if !base.LastUpdated.After(timeNow) {
|
||||
t.Fatal("TestUpdate expected LastUpdated to be greater then original time")
|
||||
}
|
||||
|
||||
a, b := base.TotalAsksAmount()
|
||||
if a != 100 && b != 20200 {
|
||||
t.Fatal("TestUpdate expected a = 100 and b = 20100")
|
||||
}
|
||||
|
||||
a, b = base.TotalBidsAmount()
|
||||
if a != 100 && b != 20100 {
|
||||
t.Fatal("TestUpdate expected a = 100 and b = 20100")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrderbook(t *testing.T) {
|
||||
c, err := currency.NewPairFromStrings("BTC", "USD")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
base := &Base{
|
||||
Pair: c,
|
||||
Asks: []Item{{Price: 100, Amount: 10}},
|
||||
Bids: []Item{{Price: 200, Amount: 10}},
|
||||
ExchangeName: "Exchange",
|
||||
AssetType: asset.Spot,
|
||||
Pair: c,
|
||||
Asks: []Item{{Price: 100, Amount: 10}},
|
||||
Bids: []Item{{Price: 200, Amount: 10}},
|
||||
Exchange: "Exchange",
|
||||
Asset: asset.Spot,
|
||||
}
|
||||
|
||||
err = base.Process()
|
||||
@@ -344,17 +235,102 @@ func TestGetOrderbook(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDepth(t *testing.T) {
|
||||
c, err := currency.NewPairFromStrings("BTC", "USD")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
base := &Base{
|
||||
Pair: c,
|
||||
Asks: []Item{{Price: 100, Amount: 10}},
|
||||
Bids: []Item{{Price: 200, Amount: 10}},
|
||||
Exchange: "Exchange",
|
||||
Asset: asset.Spot,
|
||||
}
|
||||
|
||||
err = base.Process()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
result, err := GetDepth("Exchange", c, asset.Spot)
|
||||
if err != nil {
|
||||
t.Fatalf("TestGetOrderbook failed to get orderbook. Error %s",
|
||||
err)
|
||||
}
|
||||
if !result.pair.Equal(c) {
|
||||
t.Fatal("TestGetOrderbook failed. Mismatched pairs")
|
||||
}
|
||||
|
||||
_, err = GetDepth("nonexistent", c, asset.Spot)
|
||||
if !errors.Is(err, errCannotFindOrderbook) {
|
||||
t.Fatalf("expecting %s error but received %v", errCannotFindOrderbook, err)
|
||||
}
|
||||
|
||||
c.Base = currency.NewCode("blah")
|
||||
_, err = GetDepth("Exchange", c, asset.Spot)
|
||||
if !errors.Is(err, errCannotFindOrderbook) {
|
||||
t.Fatalf("expecting %s error but received %v", errCannotFindOrderbook, err)
|
||||
}
|
||||
|
||||
newCurrency, err := currency.NewPairFromStrings("BTC", "AUD")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = GetDepth("Exchange", newCurrency, asset.Futures)
|
||||
if !errors.Is(err, errCannotFindOrderbook) {
|
||||
t.Fatalf("expecting %s error but received %v", errCannotFindOrderbook, err)
|
||||
}
|
||||
|
||||
base.Pair = newCurrency
|
||||
err = base.Process()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
_, err = GetDepth("Exchange", newCurrency, "meowCats")
|
||||
if !errors.Is(err, errCannotFindOrderbook) {
|
||||
t.Fatalf("expecting %s error but received %v", errCannotFindOrderbook, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployDepth(t *testing.T) {
|
||||
c, err := currency.NewPairFromStrings("BTC", "USD")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = DeployDepth("", c, asset.Spot)
|
||||
if !errors.Is(err, errExchangeNameUnset) {
|
||||
t.Fatalf("expecting %s error but received %v", errExchangeNameUnset, err)
|
||||
}
|
||||
_, err = DeployDepth("test", currency.Pair{}, asset.Spot)
|
||||
if !errors.Is(err, errPairNotSet) {
|
||||
t.Fatalf("expecting %s error but received %v", errPairNotSet, err)
|
||||
}
|
||||
_, err = DeployDepth("test", c, "")
|
||||
if !errors.Is(err, errAssetTypeNotSet) {
|
||||
t.Fatalf("expecting %s error but received %v", errAssetTypeNotSet, err)
|
||||
}
|
||||
d, err := DeployDepth("test", c, asset.Spot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if d == nil {
|
||||
t.Fatal("depth ptr shall not be nill")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateNewOrderbook(t *testing.T) {
|
||||
c, err := currency.NewPairFromStrings("BTC", "USD")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
base := &Base{
|
||||
Pair: c,
|
||||
Asks: []Item{{Price: 100, Amount: 10}},
|
||||
Bids: []Item{{Price: 200, Amount: 10}},
|
||||
ExchangeName: "testCreateNewOrderbook",
|
||||
AssetType: asset.Spot,
|
||||
Pair: c,
|
||||
Asks: []Item{{Price: 100, Amount: 10}},
|
||||
Bids: []Item{{Price: 200, Amount: 10}},
|
||||
Exchange: "testCreateNewOrderbook",
|
||||
Asset: asset.Spot,
|
||||
}
|
||||
|
||||
err = base.Process()
|
||||
@@ -388,9 +364,9 @@ func TestProcessOrderbook(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
base := Base{
|
||||
Asks: []Item{{Price: 100, Amount: 10}},
|
||||
Bids: []Item{{Price: 200, Amount: 10}},
|
||||
ExchangeName: "ProcessOrderbook",
|
||||
Asks: []Item{{Price: 100, Amount: 10}},
|
||||
Bids: []Item{{Price: 200, Amount: 10}},
|
||||
Exchange: "ProcessOrderbook",
|
||||
}
|
||||
|
||||
// test for empty pair
|
||||
@@ -408,7 +384,7 @@ func TestProcessOrderbook(t *testing.T) {
|
||||
}
|
||||
|
||||
// now process a valid orderbook
|
||||
base.AssetType = asset.Spot
|
||||
base.Asset = asset.Spot
|
||||
err = base.Process()
|
||||
if err != nil {
|
||||
t.Error("unexpcted result: ", err)
|
||||
@@ -458,7 +434,7 @@ func TestProcessOrderbook(t *testing.T) {
|
||||
}
|
||||
|
||||
base.Asks = []Item{{Price: 200, Amount: 200}}
|
||||
base.AssetType = "monthly"
|
||||
base.Asset = "monthly"
|
||||
err = base.Process()
|
||||
if err != nil {
|
||||
t.Error("Process() error", err)
|
||||
@@ -475,8 +451,8 @@ func TestProcessOrderbook(t *testing.T) {
|
||||
}
|
||||
|
||||
base.Bids = []Item{{Price: 420, Amount: 200}}
|
||||
base.ExchangeName = "Blah"
|
||||
base.AssetType = "quarterly"
|
||||
base.Exchange = "Blah"
|
||||
base.Asset = "quarterly"
|
||||
err = base.Process()
|
||||
if err != nil {
|
||||
t.Error("Process() error", err)
|
||||
@@ -521,11 +497,11 @@ func TestProcessOrderbook(t *testing.T) {
|
||||
asks := []Item{{Price: rand.Float64(), Amount: rand.Float64()}} // nolint:gosec // no need to import crypo/rand for testing
|
||||
bids := []Item{{Price: rand.Float64(), Amount: rand.Float64()}} // nolint:gosec // no need to import crypo/rand for testing
|
||||
base := &Base{
|
||||
Pair: newPairs,
|
||||
Asks: asks,
|
||||
Bids: bids,
|
||||
ExchangeName: newName,
|
||||
AssetType: asset.Spot,
|
||||
Pair: newPairs,
|
||||
Asks: asks,
|
||||
Bids: bids,
|
||||
Exchange: newName,
|
||||
Asset: asset.Spot,
|
||||
}
|
||||
|
||||
m.Lock()
|
||||
@@ -577,7 +553,7 @@ func TestProcessOrderbook(t *testing.T) {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func deployUnorderedSlice() []Item {
|
||||
func deployUnorderedSlice() Items {
|
||||
var items []Item
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
for i := 0; i < 1000; i++ {
|
||||
@@ -588,16 +564,15 @@ func deployUnorderedSlice() []Item {
|
||||
|
||||
func TestSorting(t *testing.T) {
|
||||
var b Base
|
||||
b.VerifyOrderbook = true
|
||||
|
||||
b.Asks = deployUnorderedSlice()
|
||||
err := b.Verify()
|
||||
if err == nil {
|
||||
t.Fatal("error cannot be nil")
|
||||
if !errors.Is(err, errPriceOutOfOrder) {
|
||||
t.Fatalf("error expected %v received %v", errPriceOutOfOrder, err)
|
||||
}
|
||||
|
||||
SortAsks(nil)
|
||||
|
||||
SortAsks(b.Asks)
|
||||
b.Asks.SortAsks()
|
||||
err = b.Verify()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -605,20 +580,18 @@ func TestSorting(t *testing.T) {
|
||||
|
||||
b.Bids = deployUnorderedSlice()
|
||||
err = b.Verify()
|
||||
if err == nil {
|
||||
t.Fatal("error cannot be nil")
|
||||
if !errors.Is(err, errPriceOutOfOrder) {
|
||||
t.Fatalf("error expected %v received %v", errPriceOutOfOrder, err)
|
||||
}
|
||||
|
||||
SortBids(nil)
|
||||
|
||||
SortBids(b.Bids)
|
||||
b.Bids.SortBids()
|
||||
err = b.Verify()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func deploySliceOrdered() []Item {
|
||||
func deploySliceOrdered() Items {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
var items []Item
|
||||
for i := 0; i < 1000; i++ {
|
||||
@@ -629,6 +602,7 @@ func deploySliceOrdered() []Item {
|
||||
|
||||
func TestReverse(t *testing.T) {
|
||||
var b Base
|
||||
b.VerifyOrderbook = true
|
||||
|
||||
length := 1000
|
||||
b.Bids = deploySliceOrdered()
|
||||
@@ -637,12 +611,11 @@ func TestReverse(t *testing.T) {
|
||||
}
|
||||
|
||||
err := b.Verify()
|
||||
if err == nil {
|
||||
t.Fatal("error cannot be nil")
|
||||
if !errors.Is(err, errPriceOutOfOrder) {
|
||||
t.Fatalf("error expected %v received %v", errPriceOutOfOrder, err)
|
||||
}
|
||||
|
||||
Reverse(nil)
|
||||
Reverse(b.Bids)
|
||||
b.Bids.Reverse()
|
||||
err = b.Verify()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -650,11 +623,11 @@ func TestReverse(t *testing.T) {
|
||||
|
||||
b.Asks = append(b.Bids[:0:0], b.Bids...) // nolint:gocritic // Short hand
|
||||
err = b.Verify()
|
||||
if err == nil {
|
||||
t.Fatal("error cannot be nil")
|
||||
if !errors.Is(err, errPriceOutOfOrder) {
|
||||
t.Fatalf("error expected %v received %v", errPriceOutOfOrder, err)
|
||||
}
|
||||
|
||||
Reverse(b.Asks)
|
||||
b.Asks.Reverse()
|
||||
err = b.Verify()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -670,7 +643,7 @@ func BenchmarkReverse(b *testing.B) {
|
||||
}
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
Reverse(s)
|
||||
s.Reverse()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -679,17 +652,17 @@ func BenchmarkSortAsksDecending(b *testing.B) {
|
||||
s := deploySliceOrdered()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ts := append(s[:0:0], s...)
|
||||
SortAsks(ts)
|
||||
ts.SortAsks()
|
||||
}
|
||||
}
|
||||
|
||||
// 14924 79199 ns/op 49206 B/op 3 allocs/op
|
||||
func BenchmarkSortBidsAscending(b *testing.B) {
|
||||
s := deploySliceOrdered()
|
||||
Reverse(s)
|
||||
s.Reverse()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ts := append(s[:0:0], s...)
|
||||
SortBids(ts)
|
||||
ts.SortBids()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -698,7 +671,7 @@ func BenchmarkSortAsksStandard(b *testing.B) {
|
||||
s := deployUnorderedSlice()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ts := append(s[:0:0], s...)
|
||||
SortAsks(ts)
|
||||
ts.SortAsks()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -707,7 +680,7 @@ func BenchmarkSortBidsStandard(b *testing.B) {
|
||||
s := deployUnorderedSlice()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ts := append(s[:0:0], s...)
|
||||
SortBids(ts)
|
||||
ts.SortBids()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -716,21 +689,21 @@ func BenchmarkSortAsksAscending(b *testing.B) {
|
||||
s := deploySliceOrdered()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ts := append(s[:0:0], s...)
|
||||
SortAsks(ts)
|
||||
ts.SortAsks()
|
||||
}
|
||||
}
|
||||
|
||||
// 12565 97257 ns/op 49208 B/op 3 allocs/op
|
||||
func BenchmarkSortBidsDescending(b *testing.B) {
|
||||
s := deploySliceOrdered()
|
||||
Reverse(s)
|
||||
s.Reverse()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ts := append(s[:0:0], s...)
|
||||
SortBids(ts)
|
||||
ts.SortBids()
|
||||
}
|
||||
}
|
||||
|
||||
// 923154 1169 ns/op 4096 B/op 1 allocs/op
|
||||
// 124867 8480 ns/op 49152 B/op 1 allocs/op
|
||||
func BenchmarkDuplicatingSlice(b *testing.B) {
|
||||
s := deploySliceOrdered()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -738,7 +711,7 @@ func BenchmarkDuplicatingSlice(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
// 705922 1546 ns/op 4096 B/op 1 allocs/op
|
||||
// 122998 8441 ns/op 49152 B/op 1 allocs/op
|
||||
func BenchmarkCopySlice(b *testing.B) {
|
||||
s := deploySliceOrdered()
|
||||
for i := 0; i < b.N; i++ {
|
||||
|
||||
@@ -20,42 +20,38 @@ const (
|
||||
|
||||
// Vars for the orderbook package
|
||||
var (
|
||||
service *Service
|
||||
|
||||
errExchangeNameUnset = errors.New("orderbook exchange name not set")
|
||||
errPairNotSet = errors.New("orderbook currency pair not set")
|
||||
errAssetTypeNotSet = errors.New("orderbook asset type not set")
|
||||
errNoOrderbook = errors.New("orderbook bids and asks are empty")
|
||||
errPriceNotSet = errors.New("price cannot be zero")
|
||||
errAmountInvalid = errors.New("amount cannot be less or equal to zero")
|
||||
errOutOfOrder = errors.New("pricing out of order")
|
||||
errDuplication = errors.New("price duplication")
|
||||
errIDDuplication = errors.New("id duplication")
|
||||
errPeriodUnset = errors.New("funding rate period is unset")
|
||||
errExchangeNameUnset = errors.New("orderbook exchange name not set")
|
||||
errPairNotSet = errors.New("orderbook currency pair not set")
|
||||
errAssetTypeNotSet = errors.New("orderbook asset type not set")
|
||||
errCannotFindOrderbook = errors.New("cannot find orderbook(s)")
|
||||
errPriceNotSet = errors.New("price cannot be zero")
|
||||
errAmountInvalid = errors.New("amount cannot be less or equal to zero")
|
||||
errPriceOutOfOrder = errors.New("pricing out of order")
|
||||
errIDOutOfOrder = errors.New("ID out of order")
|
||||
errDuplication = errors.New("price duplication")
|
||||
errIDDuplication = errors.New("id duplication")
|
||||
errPeriodUnset = errors.New("funding rate period is unset")
|
||||
)
|
||||
|
||||
func init() {
|
||||
service = new(Service)
|
||||
service.mux = dispatch.GetNewMux()
|
||||
service.Books = make(map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*Book)
|
||||
service.Exchange = make(map[string]uuid.UUID)
|
||||
var service = Service{
|
||||
books: make(map[string]Exchange),
|
||||
Mux: dispatch.GetNewMux(),
|
||||
}
|
||||
|
||||
// Book defines an orderbook with its links to different dispatch outputs
|
||||
type Book struct {
|
||||
b *Base
|
||||
Main uuid.UUID
|
||||
Assoc []uuid.UUID
|
||||
}
|
||||
|
||||
// Service holds orderbook information for each individual exchange
|
||||
// Service provides a store for difference exchange orderbooks
|
||||
type Service struct {
|
||||
Books map[string]map[asset.Item]map[*currency.Item]map[*currency.Item]*Book
|
||||
Exchange map[string]uuid.UUID
|
||||
mux *dispatch.Mux
|
||||
books map[string]Exchange
|
||||
*dispatch.Mux
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// Exchange defines a holder for the exchange specific depth items with a
|
||||
// specific ID associated with that exchange
|
||||
type Exchange struct {
|
||||
m map[asset.Item]map[*currency.Item]map[*currency.Item]*Depth
|
||||
ID uuid.UUID
|
||||
}
|
||||
|
||||
// Item stores the amount and price values
|
||||
type Item struct {
|
||||
Amount float64
|
||||
@@ -70,27 +66,34 @@ type Item struct {
|
||||
OrderCount int64
|
||||
}
|
||||
|
||||
// Items defines a slice of orderbook items
|
||||
type Items []Item
|
||||
|
||||
// Base holds the fields for the orderbook base
|
||||
type Base struct {
|
||||
Pair currency.Pair `json:"pair"`
|
||||
Bids []Item `json:"bids"`
|
||||
Asks []Item `json:"asks"`
|
||||
LastUpdated time.Time `json:"lastUpdated"`
|
||||
LastUpdateID int64 `json:"lastUpdateId"`
|
||||
AssetType asset.Item `json:"assetType"`
|
||||
ExchangeName string `json:"exchangeName"`
|
||||
Bids Items
|
||||
Asks Items
|
||||
|
||||
// NotAggregated defines whether an orderbook can contain duplicate prices
|
||||
// in a payload
|
||||
NotAggregated bool `json:"-"`
|
||||
IsFundingRate bool `json:"fundingRate"`
|
||||
Exchange string
|
||||
Pair currency.Pair
|
||||
Asset asset.Item
|
||||
|
||||
// VerificationBypass is a complete orderbook verification bypass set by
|
||||
// user configuration
|
||||
VerificationBypass bool `json:"-"`
|
||||
// HasChecksumValidation defines an allowance to bypass internal
|
||||
// verification if the book has been verified by checksum.
|
||||
HasChecksumValidation bool `json:"-"`
|
||||
LastUpdated time.Time
|
||||
LastUpdateID int64
|
||||
// PriceDuplication defines whether an orderbook can contain duplicate
|
||||
// prices in a payload
|
||||
PriceDuplication bool
|
||||
IsFundingRate bool
|
||||
// VerifyOrderbook allows for a toggle between orderbook verification set by
|
||||
// user configuration, this allows for a potential processing boost but
|
||||
// a potential for orderbook integrity being deminished.
|
||||
VerifyOrderbook bool `json:"-"`
|
||||
// RestSnapshot defines if the depth was applied via the REST protocol thus
|
||||
// an update cannot be applied via websocket mechanics and a resubscription
|
||||
// would need to take place to maintain book integrity
|
||||
RestSnapshot bool
|
||||
// Checks if the orderbook needs ID alignment as well as price alignment
|
||||
IDAlignment bool
|
||||
}
|
||||
|
||||
type byOBPrice []Item
|
||||
@@ -98,3 +101,16 @@ type byOBPrice []Item
|
||||
func (a byOBPrice) Len() int { return len(a) }
|
||||
func (a byOBPrice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a byOBPrice) Less(i, j int) bool { return a[i].Price < a[j].Price }
|
||||
|
||||
type options struct {
|
||||
exchange string
|
||||
pair currency.Pair
|
||||
asset asset.Item
|
||||
lastUpdated time.Time
|
||||
lastUpdateID int64
|
||||
priceDuplication bool
|
||||
isFundingRate bool
|
||||
VerifyOrderbook bool
|
||||
restSnapshot bool
|
||||
idAligned bool
|
||||
}
|
||||
|
||||
@@ -503,17 +503,17 @@ func (p *Poloniex) WsProcessOrderbookSnapshot(ob []interface{}, symbol string) e
|
||||
}
|
||||
|
||||
// Both sides are completely out of order - sort needs to be used
|
||||
book.Asks = orderbook.SortAsks(book.Asks)
|
||||
book.Bids = orderbook.SortBids(book.Bids)
|
||||
book.AssetType = asset.Spot
|
||||
book.VerificationBypass = p.OrderbookVerificationBypass
|
||||
book.Asks.SortAsks()
|
||||
book.Bids.SortBids()
|
||||
book.Asset = asset.Spot
|
||||
book.VerifyOrderbook = p.CanVerifyOrderbook
|
||||
|
||||
var err error
|
||||
book.Pair, err = currency.NewPairFromString(symbol)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
book.ExchangeName = p.Name
|
||||
book.Exchange = p.Name
|
||||
|
||||
return p.Websocket.Orderbook.LoadSnapshot(&book)
|
||||
}
|
||||
|
||||
@@ -330,10 +330,10 @@ func (p *Poloniex) FetchOrderbook(currencyPair currency.Pair, assetType asset.It
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (p *Poloniex) UpdateOrderbook(c currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
callingBook := &orderbook.Base{
|
||||
ExchangeName: p.Name,
|
||||
Pair: c,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: p.OrderbookVerificationBypass,
|
||||
Exchange: p.Name,
|
||||
Pair: c,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: p.CanVerifyOrderbook,
|
||||
}
|
||||
orderbookNew, err := p.GetOrderbook("", poloniexMaxOrderbookDepth)
|
||||
if err != nil {
|
||||
@@ -346,10 +346,10 @@ func (p *Poloniex) UpdateOrderbook(c currency.Pair, assetType asset.Item) (*orde
|
||||
}
|
||||
for i := range enabledPairs {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: p.Name,
|
||||
Pair: enabledPairs[i],
|
||||
AssetType: assetType,
|
||||
VerificationBypass: p.OrderbookVerificationBypass,
|
||||
Exchange: p.Name,
|
||||
Pair: enabledPairs[i],
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: p.CanVerifyOrderbook,
|
||||
}
|
||||
|
||||
fpair, err := p.FormatExchangeCurrency(enabledPairs[i], assetType)
|
||||
|
||||
@@ -4,10 +4,12 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"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/log"
|
||||
)
|
||||
|
||||
const packageError = "websocket orderbook buffer error: %w"
|
||||
@@ -18,6 +20,8 @@ var (
|
||||
errIssueBufferEnabledButNoLimit = errors.New("buffer enabled but no limit set")
|
||||
errUpdateIsNil = errors.New("update is nil")
|
||||
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")
|
||||
)
|
||||
|
||||
// Setup sets private variables
|
||||
@@ -25,7 +29,10 @@ func (w *Orderbook) Setup(obBufferLimit int,
|
||||
bufferEnabled,
|
||||
sortBuffer,
|
||||
sortBufferByUpdateIDs,
|
||||
updateEntriesByID bool, exchangeName string, dataHandler chan interface{}) error {
|
||||
updateEntriesByID,
|
||||
verbose bool,
|
||||
exchangeName string,
|
||||
dataHandler chan interface{}) error {
|
||||
if exchangeName == "" {
|
||||
return fmt.Errorf(packageError, errUnsetExchangeName)
|
||||
}
|
||||
@@ -43,6 +50,7 @@ func (w *Orderbook) Setup(obBufferLimit int,
|
||||
w.exchangeName = exchangeName
|
||||
w.dataHandler = dataHandler
|
||||
w.ob = make(map[currency.Code]map[currency.Code]map[asset.Item]*orderbookHolder)
|
||||
w.verbose = verbose
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -57,27 +65,48 @@ func (w *Orderbook) validate(u *Update) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update updates a local buffer using bid targets and ask targets then updates
|
||||
// main orderbook
|
||||
// Volume == 0; deletion at price target
|
||||
// Price target not found; append of price target
|
||||
// Price target found; amend volume of price target
|
||||
// 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 {
|
||||
if err := w.validate(u); err != nil {
|
||||
return err
|
||||
}
|
||||
w.m.Lock()
|
||||
defer w.m.Unlock()
|
||||
obLookup, ok := w.ob[u.Pair.Base][u.Pair.Quote][u.Asset]
|
||||
book, ok := w.ob[u.Pair.Base][u.Pair.Quote][u.Asset]
|
||||
if !ok {
|
||||
return fmt.Errorf("ob.Base could not be found for Exchange %s CurrencyPair: %s AssetType: %s",
|
||||
return fmt.Errorf("%w for Exchange %s CurrencyPair: %s AssetType: %s",
|
||||
errDepthNotFound,
|
||||
w.exchangeName,
|
||||
u.Pair,
|
||||
u.Asset)
|
||||
}
|
||||
|
||||
// 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() {
|
||||
if w.verbose {
|
||||
log.Warnf(log.WebsocketMgr,
|
||||
"%s for Exchange %s CurrencyPair: %s AssetType: %s consider extending synctimeoutwebsocket",
|
||||
errRESTOverwrite,
|
||||
w.exchangeName,
|
||||
u.Pair,
|
||||
u.Asset)
|
||||
}
|
||||
return fmt.Errorf("%w for Exchange %s CurrencyPair: %s AssetType: %s",
|
||||
errRESTOverwrite,
|
||||
w.exchangeName,
|
||||
u.Pair,
|
||||
u.Asset)
|
||||
}
|
||||
|
||||
// Apply new update information
|
||||
book.ob.SetLastUpdate(u.UpdateTime, u.UpdateID, false)
|
||||
|
||||
if w.bufferEnabled {
|
||||
processed, err := w.processBufferUpdate(obLookup, u)
|
||||
processed, err := w.processBufferUpdate(book, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -86,21 +115,36 @@ func (w *Orderbook) Update(u *Update) error {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
err := w.processObUpdate(obLookup, u)
|
||||
err := w.processObUpdate(book, u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err := obLookup.ob.Process()
|
||||
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()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Process in data handler
|
||||
select {
|
||||
case w.dataHandler <- obLookup.ob:
|
||||
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()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -139,230 +183,42 @@ 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 {
|
||||
o.ob.LastUpdateID = u.UpdateID
|
||||
if w.updateEntriesByID {
|
||||
return o.updateByIDAndAction(u)
|
||||
}
|
||||
return o.updateByPrice(u)
|
||||
o.updateByPrice(u)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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) error {
|
||||
askUpdates:
|
||||
for j := range updts.Asks {
|
||||
for target := range o.ob.Asks {
|
||||
if o.ob.Asks[target].Price == updts.Asks[j].Price {
|
||||
if updts.Asks[j].Amount == 0 {
|
||||
o.ob.Asks = append(o.ob.Asks[:target], o.ob.Asks[target+1:]...)
|
||||
continue askUpdates
|
||||
}
|
||||
o.ob.Asks[target].Amount = updts.Asks[j].Amount
|
||||
continue askUpdates
|
||||
}
|
||||
}
|
||||
if updts.Asks[j].Amount <= 0 {
|
||||
continue
|
||||
}
|
||||
insertAsk(updts.Asks[j], &o.ob.Asks)
|
||||
if updts.MaxDepth != 0 && len(o.ob.Asks) > updts.MaxDepth {
|
||||
o.ob.Asks = o.ob.Asks[:updts.MaxDepth]
|
||||
}
|
||||
}
|
||||
bidUpdates:
|
||||
for j := range updts.Bids {
|
||||
for target := range o.ob.Bids {
|
||||
if o.ob.Bids[target].Price == updts.Bids[j].Price {
|
||||
if updts.Bids[j].Amount == 0 {
|
||||
o.ob.Bids = append(o.ob.Bids[:target], o.ob.Bids[target+1:]...)
|
||||
continue bidUpdates
|
||||
}
|
||||
o.ob.Bids[target].Amount = updts.Bids[j].Amount
|
||||
continue bidUpdates
|
||||
}
|
||||
}
|
||||
if updts.Bids[j].Amount <= 0 {
|
||||
continue
|
||||
}
|
||||
insertBid(updts.Bids[j], &o.ob.Bids)
|
||||
if updts.MaxDepth != 0 && len(o.ob.Bids) > updts.MaxDepth {
|
||||
o.ob.Bids = o.ob.Bids[:updts.MaxDepth]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
func (o *orderbookHolder) updateByPrice(updts *Update) {
|
||||
o.ob.UpdateBidAskByPrice(updts.Bids, updts.Asks, updts.MaxDepth)
|
||||
}
|
||||
|
||||
// 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) (err error) {
|
||||
func (o *orderbookHolder) updateByIDAndAction(updts *Update) error {
|
||||
switch updts.Action {
|
||||
case Amend:
|
||||
err = applyUpdates(updts.Bids, o.ob.Bids)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = applyUpdates(updts.Asks, o.ob.Asks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return o.ob.UpdateBidAskByID(updts.Bids, updts.Asks)
|
||||
case Delete:
|
||||
// edge case for Bitfinex as their streaming endpoint duplicates deletes
|
||||
bypassErr := o.ob.ExchangeName == "Bitfinex" && o.ob.IsFundingRate
|
||||
err = deleteUpdates(updts.Bids, &o.ob.Bids, bypassErr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %s %v", o.ob.AssetType, o.ob.Pair, err)
|
||||
}
|
||||
err = deleteUpdates(updts.Asks, &o.ob.Asks, bypassErr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s %s %v", o.ob.AssetType, o.ob.Pair, err)
|
||||
}
|
||||
bypassErr := o.ob.GetName() == "Bitfinex" && o.ob.IsFundingRate()
|
||||
return o.ob.DeleteBidAskByID(updts.Bids, updts.Asks, bypassErr)
|
||||
case Insert:
|
||||
insertUpdatesBid(updts.Bids, &o.ob.Bids)
|
||||
insertUpdatesAsk(updts.Asks, &o.ob.Asks)
|
||||
return o.ob.InsertBidAskByID(updts.Bids, updts.Asks)
|
||||
case UpdateInsert:
|
||||
updateBids:
|
||||
for x := range updts.Bids {
|
||||
for target := range o.ob.Bids { // First iteration finds ID matches
|
||||
if o.ob.Bids[target].ID == updts.Bids[x].ID {
|
||||
if o.ob.Bids[target].Price != updts.Bids[x].Price {
|
||||
// Price change occurred so correct bid alignment is
|
||||
// needed - delete instance and insert into correct
|
||||
// price level
|
||||
o.ob.Bids = append(o.ob.Bids[:target], o.ob.Bids[target+1:]...)
|
||||
break
|
||||
}
|
||||
o.ob.Bids[target].Amount = updts.Bids[x].Amount
|
||||
continue updateBids
|
||||
}
|
||||
}
|
||||
insertBid(updts.Bids[x], &o.ob.Bids)
|
||||
}
|
||||
updateAsks:
|
||||
for x := range updts.Asks {
|
||||
for target := range o.ob.Asks {
|
||||
if o.ob.Asks[target].ID == updts.Asks[x].ID {
|
||||
if o.ob.Asks[target].Price != updts.Asks[x].Price {
|
||||
// Price change occurred so correct ask alignment is
|
||||
// needed - delete instance and insert into correct
|
||||
// price level
|
||||
o.ob.Asks = append(o.ob.Asks[:target], o.ob.Asks[target+1:]...)
|
||||
break
|
||||
}
|
||||
o.ob.Asks[target].Amount = updts.Asks[x].Amount
|
||||
continue updateAsks
|
||||
}
|
||||
}
|
||||
insertAsk(updts.Asks[x], &o.ob.Asks)
|
||||
}
|
||||
return o.ob.UpdateInsertByID(updts.Bids, updts.Asks)
|
||||
default:
|
||||
return fmt.Errorf("invalid action [%s]", updts.Action)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyUpdates amends amount by ID and returns an error if not found
|
||||
func applyUpdates(updts, book []orderbook.Item) error {
|
||||
updates:
|
||||
for x := range updts {
|
||||
for y := range book {
|
||||
if book[y].ID == updts[x].ID {
|
||||
book[y].Amount = updts[x].Amount
|
||||
continue updates
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("update cannot be applied id: %d not found",
|
||||
updts[x].ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteUpdates removes updates from orderbook and returns an error if not
|
||||
// found
|
||||
func deleteUpdates(updt []orderbook.Item, book *[]orderbook.Item, bypassErr bool) error {
|
||||
updates:
|
||||
for x := range updt {
|
||||
for y := range *book {
|
||||
if (*book)[y].ID == updt[x].ID {
|
||||
*book = append((*book)[:y], (*book)[y+1:]...) // nolint:gocritic
|
||||
continue updates
|
||||
}
|
||||
}
|
||||
// bypassErr is for expected duplication from endpoint.
|
||||
if !bypassErr {
|
||||
return fmt.Errorf("update cannot be deleted id: %d not found",
|
||||
updt[x].ID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func insertAsk(updt orderbook.Item, book *[]orderbook.Item) {
|
||||
for target := range *book {
|
||||
if updt.Price < (*book)[target].Price {
|
||||
insertItem(updt, book, target)
|
||||
return
|
||||
}
|
||||
}
|
||||
*book = append(*book, updt)
|
||||
}
|
||||
|
||||
func insertBid(updt orderbook.Item, book *[]orderbook.Item) {
|
||||
for target := range *book {
|
||||
if updt.Price > (*book)[target].Price {
|
||||
insertItem(updt, book, target)
|
||||
return
|
||||
}
|
||||
}
|
||||
*book = append(*book, updt)
|
||||
}
|
||||
|
||||
// insertUpdatesBid inserts on **correctly aligned** book at price level
|
||||
func insertUpdatesBid(updt []orderbook.Item, book *[]orderbook.Item) {
|
||||
updates:
|
||||
for x := range updt {
|
||||
for target := range *book {
|
||||
if updt[x].Price > (*book)[target].Price {
|
||||
insertItem(updt[x], book, target)
|
||||
continue updates
|
||||
}
|
||||
}
|
||||
*book = append(*book, updt[x])
|
||||
}
|
||||
}
|
||||
|
||||
// insertUpdatesBid inserts on **correctly aligned** book at price level
|
||||
func insertUpdatesAsk(updt []orderbook.Item, book *[]orderbook.Item) {
|
||||
updates:
|
||||
for x := range updt {
|
||||
for target := range *book {
|
||||
if updt[x].Price < (*book)[target].Price {
|
||||
insertItem(updt[x], book, target)
|
||||
continue updates
|
||||
}
|
||||
}
|
||||
*book = append(*book, updt[x])
|
||||
}
|
||||
}
|
||||
|
||||
// insertItem inserts item in slice by target element this is an optimization
|
||||
// to reduce the need for sorting algorithms
|
||||
func insertItem(update orderbook.Item, book *[]orderbook.Item, target int) {
|
||||
// TODO: extend slice by incoming update length before this gets hit
|
||||
*book = append(*book, orderbook.Item{})
|
||||
copy((*book)[target+1:], (*book)[target:])
|
||||
(*book)[target] = update
|
||||
}
|
||||
|
||||
// LoadSnapshot loads initial snapshot of ob data from websocket
|
||||
// LoadSnapshot loads initial snapshot of orderbook data from websocket
|
||||
func (w *Orderbook) LoadSnapshot(book *orderbook.Base) error {
|
||||
w.m.Lock()
|
||||
defer w.m.Unlock()
|
||||
|
||||
err := book.Process()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m1, ok := w.ob[book.Pair.Base]
|
||||
if !ok {
|
||||
m1 = make(map[currency.Code]map[asset.Item]*orderbookHolder)
|
||||
@@ -373,31 +229,60 @@ func (w *Orderbook) LoadSnapshot(book *orderbook.Base) error {
|
||||
m2 = make(map[asset.Item]*orderbookHolder)
|
||||
m1[book.Pair.Quote] = m2
|
||||
}
|
||||
m3, ok := m2[book.AssetType]
|
||||
holder, ok := m2[book.Asset]
|
||||
if !ok {
|
||||
m3 = &orderbookHolder{ob: book, buffer: &[]Update{}}
|
||||
m2[book.AssetType] = m3
|
||||
} else {
|
||||
m3.ob.LastUpdateID = book.LastUpdateID
|
||||
m3.ob.Bids = book.Bids
|
||||
m3.ob.Asks = book.Asks
|
||||
// Associate orderbook pointer with local exchange depth map
|
||||
depth, err := orderbook.DeployDepth(book.Exchange, book.Pair, book.Asset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
depth.AssignOptions(book)
|
||||
buffer := make([]Update, w.obBufferLimit)
|
||||
ticker := time.NewTicker(timerDefault)
|
||||
holder = &orderbookHolder{
|
||||
ob: depth,
|
||||
buffer: &buffer,
|
||||
ticker: ticker,
|
||||
}
|
||||
m2[book.Asset] = holder
|
||||
}
|
||||
w.dataHandler <- book
|
||||
|
||||
// Checks if book can deploy to linked list
|
||||
err := book.Verify()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
holder.ob.LoadSnapshot(book.Bids, book.Asks)
|
||||
|
||||
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()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
w.dataHandler <- holder.ob.Retrieve()
|
||||
holder.ob.Publish()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetOrderbook returns orderbook stored in current buffer
|
||||
func (w *Orderbook) GetOrderbook(p currency.Pair, a asset.Item) *orderbook.Base {
|
||||
// GetOrderbook returns an orderbook copy as orderbook.Base
|
||||
func (w *Orderbook) GetOrderbook(p currency.Pair, a asset.Item) (*orderbook.Base, error) {
|
||||
w.m.Lock()
|
||||
defer w.m.Unlock()
|
||||
ptr, ok := w.ob[p.Base][p.Quote][a]
|
||||
book, ok := w.ob[p.Base][p.Quote][a]
|
||||
if !ok {
|
||||
return nil
|
||||
return nil, fmt.Errorf("%s %s %s %w",
|
||||
w.exchangeName,
|
||||
p,
|
||||
a,
|
||||
errDepthNotFound)
|
||||
}
|
||||
cpy := *ptr.ob
|
||||
cpy.Asks = append(cpy.Asks[:0:0], cpy.Asks...)
|
||||
cpy.Bids = append(cpy.Bids[:0:0], cpy.Bids...)
|
||||
return &cpy
|
||||
return book.ob.Retrieve(), nil
|
||||
}
|
||||
|
||||
// FlushBuffer flushes w.ob data to be garbage collected and refreshed when a
|
||||
@@ -414,9 +299,12 @@ func (w *Orderbook) FlushOrderbook(p currency.Pair, a asset.Item) error {
|
||||
defer w.m.Unlock()
|
||||
book, ok := w.ob[p.Base][p.Quote][a]
|
||||
if !ok {
|
||||
return fmt.Errorf("orderbook not associated with pair: [%s] and asset [%s]", p, a)
|
||||
return fmt.Errorf("cannot flush orderbook %s %s %s %w",
|
||||
w.exchangeName,
|
||||
p,
|
||||
a,
|
||||
errDepthNotFound)
|
||||
}
|
||||
book.ob.Bids = nil
|
||||
book.ob.Asks = nil
|
||||
book.ob.Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -26,26 +26,27 @@ const (
|
||||
exchangeName = "exchangeTest"
|
||||
)
|
||||
|
||||
func createSnapshot() (obl *Orderbook, asks, bids []orderbook.Item, err error) {
|
||||
var snapShot1 orderbook.Base
|
||||
snapShot1.ExchangeName = exchangeName
|
||||
asks = []orderbook.Item{
|
||||
{Price: 4000, Amount: 1, ID: 6},
|
||||
func createSnapshot() (holder *Orderbook, asks, bids orderbook.Items, err error) {
|
||||
asks = orderbook.Items{{Price: 4000, Amount: 1, ID: 6}}
|
||||
bids = orderbook.Items{{Price: 4000, Amount: 1, ID: 6}}
|
||||
|
||||
book := &orderbook.Base{
|
||||
Exchange: exchangeName,
|
||||
Asks: asks,
|
||||
Bids: bids,
|
||||
Asset: asset.Spot,
|
||||
Pair: cp,
|
||||
PriceDuplication: true,
|
||||
}
|
||||
bids = []orderbook.Item{
|
||||
{Price: 4000, Amount: 1, ID: 6},
|
||||
}
|
||||
snapShot1.Asks = asks
|
||||
snapShot1.Bids = bids
|
||||
snapShot1.AssetType = asset.Spot
|
||||
snapShot1.Pair = cp
|
||||
snapShot1.NotAggregated = true
|
||||
obl = &Orderbook{
|
||||
|
||||
newBook := make(map[currency.Code]map[currency.Code]map[asset.Item]*orderbookHolder)
|
||||
|
||||
holder = &Orderbook{
|
||||
exchangeName: exchangeName,
|
||||
dataHandler: make(chan interface{}, 100),
|
||||
ob: make(map[currency.Code]map[currency.Code]map[asset.Item]*orderbookHolder),
|
||||
ob: newBook,
|
||||
}
|
||||
err = obl.LoadSnapshot(&snapShot1)
|
||||
err = holder.LoadSnapshot(book)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -108,20 +109,13 @@ func BenchmarkUpdateAsksByPrice(b *testing.B) {
|
||||
|
||||
// BenchmarkBufferPerformance demonstrates buffer more performant than multi
|
||||
// process calls
|
||||
// 4219518 287 ns/op 176 B/op 1 allocs/op
|
||||
func BenchmarkBufferPerformance(b *testing.B) {
|
||||
obl, asks, bids, err := createSnapshot()
|
||||
holder, asks, bids, err := createSnapshot()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
obl.bufferEnabled = true
|
||||
// This is to ensure we do not send in zero orderbook info to our main book
|
||||
// in orderbook.go, orderbooks should not be zero even after an update.
|
||||
dummyItem := orderbook.Item{
|
||||
Amount: 1333337,
|
||||
Price: 1337.1337,
|
||||
ID: 1337,
|
||||
}
|
||||
obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids = append(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids, dummyItem)
|
||||
holder.bufferEnabled = true
|
||||
update := &Update{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
@@ -134,7 +128,7 @@ func BenchmarkBufferPerformance(b *testing.B) {
|
||||
randomIndex := rand.Intn(4) // nolint:gosec // no need to import crypo/rand for testing
|
||||
update.Asks = itemArray[randomIndex]
|
||||
update.Bids = itemArray[randomIndex]
|
||||
err = obl.Update(update)
|
||||
err = holder.Update(update)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -142,21 +136,14 @@ func BenchmarkBufferPerformance(b *testing.B) {
|
||||
}
|
||||
|
||||
// BenchmarkBufferSortingPerformance benchmark
|
||||
// 2693391 467 ns/op 208 B/op 2 allocs/op
|
||||
func BenchmarkBufferSortingPerformance(b *testing.B) {
|
||||
obl, asks, bids, err := createSnapshot()
|
||||
holder, asks, bids, err := createSnapshot()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
obl.bufferEnabled = true
|
||||
obl.sortBuffer = true
|
||||
// This is to ensure we do not send in zero orderbook info to our main book
|
||||
// in orderbook.go, orderbooks should not be zero even after an update.
|
||||
dummyItem := orderbook.Item{
|
||||
Amount: 1333337,
|
||||
Price: 1337.1337,
|
||||
ID: 1337,
|
||||
}
|
||||
obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids = append(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids, dummyItem)
|
||||
holder.bufferEnabled = true
|
||||
holder.sortBuffer = true
|
||||
update := &Update{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
@@ -169,7 +156,7 @@ func BenchmarkBufferSortingPerformance(b *testing.B) {
|
||||
randomIndex := rand.Intn(4) // nolint:gosec // no need to import crypo/rand for testing
|
||||
update.Asks = itemArray[randomIndex]
|
||||
update.Bids = itemArray[randomIndex]
|
||||
err = obl.Update(update)
|
||||
err = holder.Update(update)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
@@ -177,22 +164,15 @@ func BenchmarkBufferSortingPerformance(b *testing.B) {
|
||||
}
|
||||
|
||||
// BenchmarkBufferSortingPerformance benchmark
|
||||
// 1000000 1019 ns/op 208 B/op 2 allocs/op
|
||||
func BenchmarkBufferSortingByIDPerformance(b *testing.B) {
|
||||
obl, asks, bids, err := createSnapshot()
|
||||
holder, asks, bids, err := createSnapshot()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
obl.bufferEnabled = true
|
||||
obl.sortBuffer = true
|
||||
obl.sortBufferByUpdateIDs = true
|
||||
// This is to ensure we do not send in zero orderbook info to our main book
|
||||
// in orderbook.go, orderbooks should not be zero even after an update.
|
||||
dummyItem := orderbook.Item{
|
||||
Amount: 1333337,
|
||||
Price: 1337.1337,
|
||||
ID: 1337,
|
||||
}
|
||||
obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids = append(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids, dummyItem)
|
||||
holder.bufferEnabled = true
|
||||
holder.sortBuffer = true
|
||||
holder.sortBufferByUpdateIDs = true
|
||||
update := &Update{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
@@ -205,28 +185,21 @@ func BenchmarkBufferSortingByIDPerformance(b *testing.B) {
|
||||
randomIndex := rand.Intn(4) // nolint:gosec // no need to import crypo/rand for testing
|
||||
update.Asks = itemArray[randomIndex]
|
||||
update.Bids = itemArray[randomIndex]
|
||||
err = obl.Update(update)
|
||||
err = holder.Update(update)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkNoBufferPerformance demonstrates orderbook process less performant
|
||||
// BenchmarkNoBufferPerformance demonstrates orderbook process more performant
|
||||
// than buffer
|
||||
// 9516966 141 ns/op 0 B/op 0 allocs/op
|
||||
func BenchmarkNoBufferPerformance(b *testing.B) {
|
||||
obl, asks, bids, err := createSnapshot()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
// This is to ensure we do not send in zero orderbook info to our main book
|
||||
// in orderbook.go, orderbooks should not be zero even after an update.
|
||||
dummyItem := orderbook.Item{
|
||||
Amount: 1333337,
|
||||
Price: 1337.1337,
|
||||
ID: 1337,
|
||||
}
|
||||
obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids = append(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids, dummyItem)
|
||||
update := &Update{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
@@ -247,13 +220,13 @@ func BenchmarkNoBufferPerformance(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestUpdates(t *testing.T) {
|
||||
obl, _, _, err := createSnapshot()
|
||||
holder, _, _, err := createSnapshot()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
holder := obl.ob[cp.Base][cp.Quote][asset.Spot]
|
||||
holder.updateByPrice(&Update{
|
||||
book := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
||||
book.updateByPrice(&Update{
|
||||
Bids: itemArray[5],
|
||||
Asks: itemArray[5],
|
||||
Pair: cp,
|
||||
@@ -264,7 +237,7 @@ func TestUpdates(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
holder.updateByPrice(&Update{
|
||||
book.updateByPrice(&Update{
|
||||
Bids: itemArray[0],
|
||||
Asks: itemArray[0],
|
||||
Pair: cp,
|
||||
@@ -275,23 +248,23 @@ func TestUpdates(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if len(holder.ob.Asks) != 3 {
|
||||
if book.ob.GetAskLength() != 3 {
|
||||
t.Error("Did not update")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHittingTheBuffer logic test
|
||||
func TestHittingTheBuffer(t *testing.T) {
|
||||
obl, _, _, err := createSnapshot()
|
||||
holder, _, _, err := createSnapshot()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
obl.bufferEnabled = true
|
||||
obl.obBufferLimit = 5
|
||||
holder.bufferEnabled = true
|
||||
holder.obBufferLimit = 5
|
||||
for i := range itemArray {
|
||||
asks := itemArray[i]
|
||||
bids := itemArray[i]
|
||||
err = obl.Update(&Update{
|
||||
err = holder.Update(&Update{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
Pair: cp,
|
||||
@@ -302,67 +275,67 @@ func TestHittingTheBuffer(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Asks) != 3 {
|
||||
t.Errorf("expected 3 entries, received: %v",
|
||||
len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Asks))
|
||||
|
||||
book := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
||||
if book.ob.GetAskLength() != 3 {
|
||||
t.Errorf("expected 3 entries, received: %v", book.ob.GetAskLength())
|
||||
}
|
||||
if len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids) != 3 {
|
||||
t.Errorf("expected 3 entries, received: %v",
|
||||
len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids))
|
||||
if book.ob.GetBidLength() != 3 {
|
||||
t.Errorf("expected 3 entries, received: %v", book.ob.GetBidLength())
|
||||
}
|
||||
}
|
||||
|
||||
// TestInsertWithIDs logic test
|
||||
func TestInsertWithIDs(t *testing.T) {
|
||||
obl, _, _, err := createSnapshot()
|
||||
holder, _, _, err := createSnapshot()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
obl.bufferEnabled = true
|
||||
obl.updateEntriesByID = true
|
||||
obl.obBufferLimit = 5
|
||||
holder.bufferEnabled = true
|
||||
holder.updateEntriesByID = true
|
||||
holder.obBufferLimit = 5
|
||||
for i := range itemArray {
|
||||
asks := itemArray[i]
|
||||
if asks[0].Amount <= 0 {
|
||||
continue
|
||||
}
|
||||
bids := itemArray[i]
|
||||
err = obl.Update(&Update{
|
||||
err = holder.Update(&Update{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
Pair: cp,
|
||||
UpdateTime: time.Now(),
|
||||
Asset: asset.Spot,
|
||||
Action: "insert",
|
||||
Action: UpdateInsert,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Asks) != 6 {
|
||||
t.Errorf("expected 6 entries, received: %v",
|
||||
len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Asks))
|
||||
|
||||
book := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
||||
if book.ob.GetAskLength() != 6 {
|
||||
t.Errorf("expected 5 entries, received: %v", book.ob.GetAskLength())
|
||||
}
|
||||
if len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids) != 6 {
|
||||
t.Errorf("expected 6 entries, received: %v",
|
||||
len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids))
|
||||
if book.ob.GetBidLength() != 6 {
|
||||
t.Errorf("expected 5 entries, received: %v", book.ob.GetBidLength())
|
||||
}
|
||||
}
|
||||
|
||||
// TestSortIDs logic test
|
||||
func TestSortIDs(t *testing.T) {
|
||||
obl, _, _, err := createSnapshot()
|
||||
holder, _, _, err := createSnapshot()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
obl.bufferEnabled = true
|
||||
obl.sortBufferByUpdateIDs = true
|
||||
obl.sortBuffer = true
|
||||
obl.obBufferLimit = 5
|
||||
holder.bufferEnabled = true
|
||||
holder.sortBufferByUpdateIDs = true
|
||||
holder.sortBuffer = true
|
||||
holder.obBufferLimit = 5
|
||||
for i := range itemArray {
|
||||
asks := itemArray[i]
|
||||
bids := itemArray[i]
|
||||
err = obl.Update(&Update{
|
||||
err = holder.Update(&Update{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
Pair: cp,
|
||||
@@ -373,19 +346,18 @@ func TestSortIDs(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Asks) != 3 {
|
||||
t.Errorf("expected 3 entries, received: %v",
|
||||
len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Asks))
|
||||
book := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
||||
if book.ob.GetAskLength() != 3 {
|
||||
t.Errorf("expected 3 entries, received: %v", book.ob.GetAskLength())
|
||||
}
|
||||
if len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids) != 3 {
|
||||
t.Errorf("expected 3 entries, received: %v",
|
||||
len(obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Bids))
|
||||
if book.ob.GetAskLength() != 3 {
|
||||
t.Errorf("expected 3 entries, received: %v", book.ob.GetAskLength())
|
||||
}
|
||||
}
|
||||
|
||||
// TestOutOfOrderIDs logic test
|
||||
func TestOutOfOrderIDs(t *testing.T) {
|
||||
obl, _, _, err := createSnapshot()
|
||||
holder, _, _, err := createSnapshot()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -394,12 +366,12 @@ func TestOutOfOrderIDs(t *testing.T) {
|
||||
t.Errorf("expected sorted price to be 3000, received: %v",
|
||||
itemArray[1][0].Price)
|
||||
}
|
||||
obl.bufferEnabled = true
|
||||
obl.sortBuffer = true
|
||||
obl.obBufferLimit = 5
|
||||
holder.bufferEnabled = true
|
||||
holder.sortBuffer = true
|
||||
holder.obBufferLimit = 5
|
||||
for i := range itemArray {
|
||||
asks := itemArray[i]
|
||||
err = obl.Update(&Update{
|
||||
err = holder.Update(&Update{
|
||||
Asks: asks,
|
||||
Pair: cp,
|
||||
UpdateID: outOFOrderIDs[i],
|
||||
@@ -409,15 +381,16 @@ func TestOutOfOrderIDs(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
book := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
||||
cpy := book.ob.Retrieve()
|
||||
// Index 1 since index 0 is price 7000
|
||||
if obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Asks[1].Price != 2000 {
|
||||
t.Errorf("expected sorted price to be 3000, received: %v",
|
||||
obl.ob[cp.Base][cp.Quote][asset.Spot].ob.Asks[1].Price)
|
||||
if cpy.Asks[1].Price != 2000 {
|
||||
t.Errorf("expected sorted price to be 2000, received: %v", cpy.Asks[1].Price)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOrderbookLastUpdateID(t *testing.T) {
|
||||
obl, _, _, err := createSnapshot()
|
||||
holder, _, _, err := createSnapshot()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -428,7 +401,7 @@ func TestOrderbookLastUpdateID(t *testing.T) {
|
||||
|
||||
for i := range itemArray {
|
||||
asks := itemArray[i]
|
||||
err = obl.Update(&Update{
|
||||
err = holder.Update(&Update{
|
||||
Asks: asks,
|
||||
Pair: cp,
|
||||
UpdateID: int64(i) + 1,
|
||||
@@ -439,7 +412,10 @@ func TestOrderbookLastUpdateID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
ob := obl.GetOrderbook(cp, asset.Spot)
|
||||
ob, err := holder.GetOrderbook(cp, asset.Spot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if exp := len(itemArray); ob.LastUpdateID != int64(exp) {
|
||||
t.Errorf("expected last update id to be %d, received: %v", exp, ob.LastUpdateID)
|
||||
}
|
||||
@@ -447,7 +423,7 @@ func TestOrderbookLastUpdateID(t *testing.T) {
|
||||
|
||||
// TestRunUpdateWithoutSnapshot logic test
|
||||
func TestRunUpdateWithoutSnapshot(t *testing.T) {
|
||||
var obl Orderbook
|
||||
var holder Orderbook
|
||||
var snapShot1 orderbook.Base
|
||||
asks := []orderbook.Item{
|
||||
{Price: 4000, Amount: 1, ID: 8},
|
||||
@@ -458,21 +434,18 @@ func TestRunUpdateWithoutSnapshot(t *testing.T) {
|
||||
}
|
||||
snapShot1.Asks = asks
|
||||
snapShot1.Bids = bids
|
||||
snapShot1.AssetType = asset.Spot
|
||||
snapShot1.Asset = asset.Spot
|
||||
snapShot1.Pair = cp
|
||||
obl.exchangeName = exchangeName
|
||||
err := obl.Update(&Update{
|
||||
holder.exchangeName = exchangeName
|
||||
err := holder.Update(&Update{
|
||||
Bids: bids,
|
||||
Asks: asks,
|
||||
Pair: cp,
|
||||
UpdateTime: time.Now(),
|
||||
Asset: asset.Spot,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected an error running update with no snapshot loaded")
|
||||
}
|
||||
if err.Error() != "ob.Base could not be found for Exchange exchangeTest CurrencyPair: BTCUSD AssetType: spot" {
|
||||
t.Fatal(err)
|
||||
if !errors.Is(err, errDepthNotFound) {
|
||||
t.Fatalf("expected %v but received %v", errDepthNotFound, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,7 +455,7 @@ func TestRunUpdateWithoutAnyUpdates(t *testing.T) {
|
||||
var snapShot1 orderbook.Base
|
||||
snapShot1.Asks = []orderbook.Item{}
|
||||
snapShot1.Bids = []orderbook.Item{}
|
||||
snapShot1.AssetType = asset.Spot
|
||||
snapShot1.Asset = asset.Spot
|
||||
snapShot1.Pair = cp
|
||||
obl.exchangeName = exchangeName
|
||||
err := obl.Update(&Update{
|
||||
@@ -492,11 +465,8 @@ func TestRunUpdateWithoutAnyUpdates(t *testing.T) {
|
||||
UpdateTime: time.Now(),
|
||||
Asset: asset.Spot,
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected an error running update with no snapshot loaded")
|
||||
}
|
||||
if err.Error() != "websocket orderbook buffer error: update bid/ask targets cannot be nil" {
|
||||
t.Fatal("expected nil asks and bids error")
|
||||
if !errors.Is(err, errUpdateNoTargets) {
|
||||
t.Fatalf("expected %v but received %v", errUpdateNoTargets, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -506,9 +476,9 @@ func TestRunSnapshotWithNoData(t *testing.T) {
|
||||
obl.ob = make(map[currency.Code]map[currency.Code]map[asset.Item]*orderbookHolder)
|
||||
obl.dataHandler = make(chan interface{}, 1)
|
||||
var snapShot1 orderbook.Base
|
||||
snapShot1.AssetType = asset.Spot
|
||||
snapShot1.Asset = asset.Spot
|
||||
snapShot1.Pair = cp
|
||||
snapShot1.ExchangeName = "test"
|
||||
snapShot1.Exchange = "test"
|
||||
obl.exchangeName = "test"
|
||||
err := obl.LoadSnapshot(&snapShot1)
|
||||
if err != nil {
|
||||
@@ -522,7 +492,7 @@ func TestLoadSnapshot(t *testing.T) {
|
||||
obl.dataHandler = make(chan interface{}, 100)
|
||||
obl.ob = make(map[currency.Code]map[currency.Code]map[asset.Item]*orderbookHolder)
|
||||
var snapShot1 orderbook.Base
|
||||
snapShot1.ExchangeName = "SnapshotWithOverride"
|
||||
snapShot1.Exchange = "SnapshotWithOverride"
|
||||
asks := []orderbook.Item{
|
||||
{Price: 4000, Amount: 1, ID: 8},
|
||||
}
|
||||
@@ -531,7 +501,7 @@ func TestLoadSnapshot(t *testing.T) {
|
||||
}
|
||||
snapShot1.Asks = asks
|
||||
snapShot1.Bids = bids
|
||||
snapShot1.AssetType = asset.Spot
|
||||
snapShot1.Asset = asset.Spot
|
||||
snapShot1.Pair = cp
|
||||
err := obl.LoadSnapshot(&snapShot1)
|
||||
if err != nil {
|
||||
@@ -556,11 +526,11 @@ func TestFlushbuffer(t *testing.T) {
|
||||
|
||||
// TestInsertingSnapShots logic test
|
||||
func TestInsertingSnapShots(t *testing.T) {
|
||||
var obl Orderbook
|
||||
obl.dataHandler = make(chan interface{}, 100)
|
||||
obl.ob = make(map[currency.Code]map[currency.Code]map[asset.Item]*orderbookHolder)
|
||||
var holder Orderbook
|
||||
holder.dataHandler = make(chan interface{}, 100)
|
||||
holder.ob = make(map[currency.Code]map[currency.Code]map[asset.Item]*orderbookHolder)
|
||||
var snapShot1 orderbook.Base
|
||||
snapShot1.ExchangeName = "WSORDERBOOKTEST1"
|
||||
snapShot1.Exchange = "WSORDERBOOKTEST1"
|
||||
asks := []orderbook.Item{
|
||||
{Price: 6000, Amount: 1, ID: 1},
|
||||
{Price: 6001, Amount: 0.5, ID: 2},
|
||||
@@ -591,14 +561,14 @@ func TestInsertingSnapShots(t *testing.T) {
|
||||
|
||||
snapShot1.Asks = asks
|
||||
snapShot1.Bids = bids
|
||||
snapShot1.AssetType = asset.Spot
|
||||
snapShot1.Asset = asset.Spot
|
||||
snapShot1.Pair = cp
|
||||
err := obl.LoadSnapshot(&snapShot1)
|
||||
err := holder.LoadSnapshot(&snapShot1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var snapShot2 orderbook.Base
|
||||
snapShot2.ExchangeName = "WSORDERBOOKTEST2"
|
||||
snapShot2.Exchange = "WSORDERBOOKTEST2"
|
||||
asks = []orderbook.Item{
|
||||
{Price: 51, Amount: 1, ID: 1},
|
||||
{Price: 52, Amount: 0.5, ID: 2},
|
||||
@@ -627,19 +597,21 @@ func TestInsertingSnapShots(t *testing.T) {
|
||||
{Price: 39, Amount: 7, ID: 22},
|
||||
}
|
||||
|
||||
snapShot2.Asks = orderbook.SortAsks(asks)
|
||||
snapShot2.Bids = orderbook.SortBids(bids)
|
||||
snapShot2.AssetType = asset.Spot
|
||||
snapShot2.Asks = asks
|
||||
snapShot2.Asks.SortAsks()
|
||||
snapShot2.Bids = bids
|
||||
snapShot2.Bids.SortBids()
|
||||
snapShot2.Asset = asset.Spot
|
||||
snapShot2.Pair, err = currency.NewPairFromString("LTCUSD")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = obl.LoadSnapshot(&snapShot2)
|
||||
err = holder.LoadSnapshot(&snapShot2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var snapShot3 orderbook.Base
|
||||
snapShot3.ExchangeName = "WSORDERBOOKTEST3"
|
||||
snapShot3.Exchange = "WSORDERBOOKTEST3"
|
||||
asks = []orderbook.Item{
|
||||
{Price: 511, Amount: 1, ID: 1},
|
||||
{Price: 52, Amount: 0.5, ID: 2},
|
||||
@@ -668,74 +640,88 @@ func TestInsertingSnapShots(t *testing.T) {
|
||||
{Price: 39, Amount: 7, ID: 22},
|
||||
}
|
||||
|
||||
snapShot3.Asks = orderbook.SortAsks(asks)
|
||||
snapShot3.Bids = orderbook.SortBids(bids)
|
||||
snapShot3.AssetType = "FUTURES"
|
||||
snapShot3.Asks = asks
|
||||
snapShot3.Asks.SortAsks()
|
||||
snapShot3.Bids = bids
|
||||
snapShot3.Bids.SortBids()
|
||||
snapShot3.Asset = asset.Futures
|
||||
snapShot3.Pair, err = currency.NewPairFromString("LTCUSD")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = obl.LoadSnapshot(&snapShot3)
|
||||
err = holder.LoadSnapshot(&snapShot3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if obl.ob[snapShot1.Pair.Base][snapShot1.Pair.Quote][snapShot1.AssetType].ob.Asks[0] != snapShot1.Asks[0] {
|
||||
ob, err := holder.GetOrderbook(snapShot1.Pair, snapShot1.Asset)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ob.Asks[0] != snapShot1.Asks[0] {
|
||||
t.Errorf("loaded data mismatch. Expected %v, received %v",
|
||||
snapShot1.Asks[0],
|
||||
obl.ob[snapShot1.Pair.Base][snapShot1.Pair.Quote][snapShot1.AssetType].ob.Asks[0])
|
||||
ob.Asks[0])
|
||||
}
|
||||
if obl.ob[snapShot2.Pair.Base][snapShot1.Pair.Quote][snapShot2.AssetType].ob.Asks[0] != snapShot2.Asks[0] {
|
||||
ob, err = holder.GetOrderbook(snapShot2.Pair, snapShot2.Asset)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ob.Asks[0] != snapShot2.Asks[0] {
|
||||
t.Errorf("loaded data mismatch. Expected %v, received %v",
|
||||
snapShot2.Asks[0],
|
||||
obl.ob[snapShot2.Pair.Base][snapShot1.Pair.Quote][snapShot2.AssetType].ob.Asks[0])
|
||||
ob.Asks[0])
|
||||
}
|
||||
if obl.ob[snapShot3.Pair.Base][snapShot1.Pair.Quote][snapShot3.AssetType].ob.Asks[0] != snapShot3.Asks[0] {
|
||||
ob, err = holder.GetOrderbook(snapShot3.Pair, snapShot3.Asset)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ob.Asks[0] != snapShot3.Asks[0] {
|
||||
t.Errorf("loaded data mismatch. Expected %v, received %v",
|
||||
snapShot3.Asks[0],
|
||||
obl.ob[snapShot3.Pair.Base][snapShot1.Pair.Quote][snapShot3.AssetType].ob.Asks[0])
|
||||
ob.Asks[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOrderbook(t *testing.T) {
|
||||
obl, _, _, err := createSnapshot()
|
||||
holder, _, _, err := createSnapshot()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ob := obl.GetOrderbook(cp, asset.Spot)
|
||||
bufferOb := obl.ob[cp.Base][cp.Quote][asset.Spot]
|
||||
if bufferOb.ob == ob {
|
||||
t.Error("orderbooks should be separate in pointer value and not linked to orderbook package")
|
||||
ob, err := holder.GetOrderbook(cp, asset.Spot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(bufferOb.ob.Asks) != len(ob.Asks) ||
|
||||
len(bufferOb.ob.Bids) != len(ob.Bids) ||
|
||||
bufferOb.ob.AssetType != ob.AssetType ||
|
||||
bufferOb.ob.ExchangeName != ob.ExchangeName ||
|
||||
bufferOb.ob.LastUpdateID != ob.LastUpdateID ||
|
||||
bufferOb.ob.NotAggregated != ob.NotAggregated ||
|
||||
bufferOb.ob.Pair != ob.Pair {
|
||||
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.Asset != ob.Asset ||
|
||||
b.Exchange != ob.Exchange ||
|
||||
b.LastUpdateID != ob.LastUpdateID ||
|
||||
b.PriceDuplication != ob.PriceDuplication ||
|
||||
b.Pair != ob.Pair {
|
||||
t.Fatal("data on both books should be the same")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetup(t *testing.T) {
|
||||
w := Orderbook{}
|
||||
err := w.Setup(0, false, false, false, false, "", nil)
|
||||
if err == nil || !errors.Is(err, errUnsetExchangeName) {
|
||||
err := w.Setup(0, false, false, false, false, true, "", nil)
|
||||
if !errors.Is(err, errUnsetExchangeName) {
|
||||
t.Fatalf("expected error %v but received %v", errUnsetExchangeName, err)
|
||||
}
|
||||
|
||||
err = w.Setup(0, false, false, false, false, "test", nil)
|
||||
if err == nil || !errors.Is(err, errUnsetDataHandler) {
|
||||
err = w.Setup(0, false, false, false, false, false, "test", nil)
|
||||
if !errors.Is(err, errUnsetDataHandler) {
|
||||
t.Fatalf("expected error %v but received %v", errUnsetDataHandler, err)
|
||||
}
|
||||
|
||||
err = w.Setup(0, true, false, false, false, "test", make(chan interface{}))
|
||||
if err == nil || !errors.Is(err, errIssueBufferEnabledButNoLimit) {
|
||||
err = w.Setup(0, true, false, false, false, true, "test", make(chan interface{}))
|
||||
if !errors.Is(err, errIssueBufferEnabledButNoLimit) {
|
||||
t.Fatalf("expected error %v but received %v", errIssueBufferEnabledButNoLimit, err)
|
||||
}
|
||||
|
||||
err = w.Setup(1337, true, true, true, true, "test", make(chan interface{}))
|
||||
err = w.Setup(1337, true, true, true, true, false, "test", make(chan interface{}))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -752,25 +738,25 @@ func TestSetup(t *testing.T) {
|
||||
func TestValidate(t *testing.T) {
|
||||
w := Orderbook{}
|
||||
err := w.validate(nil)
|
||||
if err == nil || !errors.Is(err, errUpdateIsNil) {
|
||||
if !errors.Is(err, errUpdateIsNil) {
|
||||
t.Fatalf("expected error %v but received %v", errUpdateIsNil, err)
|
||||
}
|
||||
|
||||
err = w.validate(&Update{})
|
||||
if err == nil || !errors.Is(err, errUpdateNoTargets) {
|
||||
if !errors.Is(err, errUpdateNoTargets) {
|
||||
t.Fatalf("expected error %v but received %v", errUpdateNoTargets, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureMultipleUpdatesViaPrice(t *testing.T) {
|
||||
obl, _, _, err := createSnapshot()
|
||||
holder, _, _, err := createSnapshot()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
asks := bidAskGenerator()
|
||||
holder := obl.ob[cp.Base][cp.Quote][asset.Spot]
|
||||
holder.updateByPrice(&Update{
|
||||
book := holder.ob[cp.Base][cp.Quote][asset.Spot]
|
||||
book.updateByPrice(&Update{
|
||||
Bids: asks,
|
||||
Asks: asks,
|
||||
Pair: cp,
|
||||
@@ -781,69 +767,12 @@ func TestEnsureMultipleUpdatesViaPrice(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if len(holder.ob.Asks) <= 3 {
|
||||
if book.ob.GetAskLength() <= 3 {
|
||||
t.Errorf("Insufficient updates")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertItem(t *testing.T) {
|
||||
update := []orderbook.Item{{Price: 4}}
|
||||
|
||||
// Correctly aligned
|
||||
asks := []orderbook.Item{
|
||||
{
|
||||
Price: 1,
|
||||
},
|
||||
{
|
||||
Price: 2,
|
||||
},
|
||||
{
|
||||
Price: 3,
|
||||
},
|
||||
{
|
||||
Price: 5,
|
||||
},
|
||||
{
|
||||
Price: 6,
|
||||
},
|
||||
{
|
||||
Price: 7,
|
||||
},
|
||||
}
|
||||
|
||||
insertUpdatesAsk(update, &asks)
|
||||
if asks[3].Price != 4 {
|
||||
t.Fatal("incorrect insertion")
|
||||
}
|
||||
|
||||
bids := []orderbook.Item{
|
||||
{
|
||||
Price: 7,
|
||||
},
|
||||
{
|
||||
Price: 6,
|
||||
},
|
||||
{
|
||||
Price: 5,
|
||||
},
|
||||
{
|
||||
Price: 3,
|
||||
},
|
||||
{
|
||||
Price: 2,
|
||||
},
|
||||
{
|
||||
Price: 1,
|
||||
},
|
||||
}
|
||||
|
||||
insertUpdatesBid(update, &bids)
|
||||
if asks[3].Price != 4 {
|
||||
t.Fatal("incorrect insertion")
|
||||
}
|
||||
}
|
||||
|
||||
func deploySliceOrdered(size int) []orderbook.Item {
|
||||
func deploySliceOrdered(size int) orderbook.Items {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
var items []orderbook.Item
|
||||
for i := 0; i < size; i++ {
|
||||
@@ -857,14 +786,16 @@ func TestUpdateByIDAndAction(t *testing.T) {
|
||||
|
||||
asks := deploySliceOrdered(100)
|
||||
bids := append(asks[:0:0], asks...)
|
||||
orderbook.Reverse(bids)
|
||||
bids.Reverse()
|
||||
|
||||
book := &orderbook.Base{
|
||||
Bids: append(bids[:0:0], bids...),
|
||||
Asks: append(asks[:0:0], asks...),
|
||||
book, err := orderbook.DeployDepth("test", cp, asset.Spot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err := book.Verify()
|
||||
book.LoadSnapshot(append(bids[:0:0], bids...), append(asks[:0:0], asks...))
|
||||
|
||||
err = book.Retrieve().Verify()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -894,14 +825,16 @@ func TestUpdateByIDAndAction(t *testing.T) {
|
||||
Action: UpdateInsert,
|
||||
Bids: []orderbook.Item{
|
||||
{
|
||||
Price: 0,
|
||||
ID: 1337,
|
||||
Price: 0,
|
||||
ID: 1337,
|
||||
Amount: 1,
|
||||
},
|
||||
},
|
||||
Asks: []orderbook.Item{
|
||||
{
|
||||
Price: 100,
|
||||
ID: 1337,
|
||||
Price: 100,
|
||||
ID: 1337,
|
||||
Amount: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -909,10 +842,12 @@ func TestUpdateByIDAndAction(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if book.Bids[len(book.Bids)-1].Price != 0 {
|
||||
cpy := book.Retrieve()
|
||||
|
||||
if cpy.Bids[len(cpy.Bids)-1].Price != 0 {
|
||||
t.Fatal("did not append bid item")
|
||||
}
|
||||
if book.Asks[len(book.Asks)-1].Price != 100 {
|
||||
if cpy.Asks[len(cpy.Asks)-1].Price != 100 {
|
||||
t.Fatal("did not append ask item")
|
||||
}
|
||||
|
||||
@@ -938,11 +873,13 @@ func TestUpdateByIDAndAction(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if book.Bids[len(book.Bids)-1].Amount != 100 {
|
||||
t.Fatal("did not update bid amount")
|
||||
cpy = book.Retrieve()
|
||||
|
||||
if cpy.Bids[len(cpy.Bids)-1].Amount != 100 {
|
||||
t.Fatal("did not update bid amount", cpy.Bids[len(cpy.Bids)-1].Amount)
|
||||
}
|
||||
|
||||
if book.Asks[len(book.Asks)-1].Amount != 100 {
|
||||
if cpy.Asks[len(cpy.Asks)-1].Amount != 100 {
|
||||
t.Fatal("did not update ask amount")
|
||||
}
|
||||
|
||||
@@ -968,16 +905,17 @@ func TestUpdateByIDAndAction(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if book.Bids[0].Amount != 99 && book.Bids[0].Price != 100 {
|
||||
cpy = book.Retrieve()
|
||||
|
||||
if cpy.Bids[0].Amount != 99 && cpy.Bids[0].Price != 100 {
|
||||
t.Fatal("did not adjust bid item placement and details")
|
||||
}
|
||||
|
||||
if book.Asks[0].Amount != 99 && book.Asks[0].Amount != 0 {
|
||||
if cpy.Asks[0].Amount != 99 && cpy.Asks[0].Amount != 0 {
|
||||
t.Fatal("did not adjust ask item placement and details")
|
||||
}
|
||||
|
||||
book.Bids = append(bids[:0:0], bids...) // nolint:gocritic
|
||||
book.Asks = append(asks[:0:0], asks...) // nolint:gocritic
|
||||
book.LoadSnapshot(append(bids[:0:0], bids...), append(bids[:0:0], bids...)) // nolint:gocritic
|
||||
|
||||
// Delete - not found
|
||||
err = holder.updateByIDAndAction(&Update{
|
||||
@@ -1018,7 +956,7 @@ func TestUpdateByIDAndAction(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(book.Asks) != 99 {
|
||||
if book.GetAskLength() != 99 {
|
||||
t.Fatal("element not deleted")
|
||||
}
|
||||
|
||||
@@ -1033,7 +971,7 @@ func TestUpdateByIDAndAction(t *testing.T) {
|
||||
t.Fatal("error cannot be nil")
|
||||
}
|
||||
|
||||
update := book.Asks[0]
|
||||
update := book.Retrieve().Asks[0]
|
||||
update.Amount = 1337
|
||||
|
||||
err = holder.updateByIDAndAction(&Update{
|
||||
@@ -1046,20 +984,20 @@ func TestUpdateByIDAndAction(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if book.Asks[0].Amount != 1337 {
|
||||
if book.Retrieve().Asks[0].Amount != 1337 {
|
||||
t.Fatal("element not updated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlushOrderbook(t *testing.T) {
|
||||
w := &Orderbook{}
|
||||
err := w.Setup(5, false, false, false, false, "test", make(chan interface{}, 2))
|
||||
err := w.Setup(5, false, false, false, false, false, "test", make(chan interface{}, 2))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var snapShot1 orderbook.Base
|
||||
snapShot1.ExchangeName = "Snapshooooot"
|
||||
snapShot1.Exchange = "Snapshooooot"
|
||||
asks := []orderbook.Item{
|
||||
{Price: 4000, Amount: 1, ID: 8},
|
||||
}
|
||||
@@ -1068,7 +1006,7 @@ func TestFlushOrderbook(t *testing.T) {
|
||||
}
|
||||
snapShot1.Asks = asks
|
||||
snapShot1.Bids = bids
|
||||
snapShot1.AssetType = asset.Spot
|
||||
snapShot1.Asset = asset.Spot
|
||||
snapShot1.Pair = cp
|
||||
|
||||
err = w.FlushOrderbook(cp, asset.Spot)
|
||||
@@ -1076,14 +1014,9 @@ func TestFlushOrderbook(t *testing.T) {
|
||||
t.Fatal("book not loaded error cannot be nil")
|
||||
}
|
||||
|
||||
o := w.GetOrderbook(cp, asset.Spot)
|
||||
if o != nil {
|
||||
t.Fatal("book not loaded, this should not happen")
|
||||
}
|
||||
|
||||
err = w.LoadSnapshot(&snapShot1)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
_, err = w.GetOrderbook(cp, asset.Spot)
|
||||
if !errors.Is(err, errDepthNotFound) {
|
||||
t.Fatalf("expected: %v but received: %v", errDepthNotFound, err)
|
||||
}
|
||||
|
||||
err = w.LoadSnapshot(&snapShot1)
|
||||
@@ -1096,12 +1029,12 @@ func TestFlushOrderbook(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
o = w.GetOrderbook(cp, asset.Spot)
|
||||
if o == nil {
|
||||
t.Fatal("cannot get book")
|
||||
o, err := w.GetOrderbook(cp, asset.Spot)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if o.Bids != nil && o.Asks != nil {
|
||||
if len(o.Bids) != 0 || len(o.Asks) != 0 {
|
||||
t.Fatal("orderbook items not flushed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ import (
|
||||
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
|
||||
)
|
||||
|
||||
// timerDefault defines the amount of time between alerting the sync manager of
|
||||
// an update.
|
||||
var timerDefault = time.Second * 10
|
||||
|
||||
// Orderbook defines a local cache of orderbooks for amending, appending
|
||||
// and deleting changes and updates the main store for a stream
|
||||
type Orderbook struct {
|
||||
@@ -20,12 +24,20 @@ type Orderbook struct {
|
||||
updateEntriesByID bool // Use the update IDs to match ob entries
|
||||
exchangeName string
|
||||
dataHandler chan interface{}
|
||||
verbose bool
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
// orderbookHolder defines a store of pending updates and a pointer to the
|
||||
// orderbook depth
|
||||
type orderbookHolder struct {
|
||||
ob *orderbook.Base
|
||||
ob *orderbook.Depth
|
||||
buffer *[]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
|
||||
// currency.
|
||||
ticker *time.Ticker
|
||||
}
|
||||
|
||||
// Update stores orderbook updates and dictates what features to use when processing
|
||||
|
||||
@@ -113,6 +113,7 @@ func (w *Websocket) Setup(s *WebsocketSetup) error {
|
||||
s.SortBuffer,
|
||||
s.SortBufferByUpdateIDs,
|
||||
s.UpdateEntriesByID,
|
||||
s.Verbose,
|
||||
w.exchangeName,
|
||||
w.DataHandler)
|
||||
}
|
||||
|
||||
@@ -240,10 +240,10 @@ func (y *Yobit) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderboo
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (y *Yobit) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: y.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: y.OrderbookVerificationBypass,
|
||||
Exchange: y.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: y.CanVerifyOrderbook,
|
||||
}
|
||||
fpair, err := y.FormatExchangeCurrency(p, assetType)
|
||||
if err != nil {
|
||||
|
||||
@@ -143,11 +143,11 @@ func (z *ZB) wsHandleData(respRaw []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
orderbook.Reverse(book.Asks) // Reverse asks for correct alignment
|
||||
book.AssetType = asset.Spot
|
||||
book.Asks.Reverse() // Reverse asks for correct alignment
|
||||
book.Asset = asset.Spot
|
||||
book.Pair = cPair
|
||||
book.ExchangeName = z.Name
|
||||
book.VerificationBypass = z.OrderbookVerificationBypass
|
||||
book.Exchange = z.Name
|
||||
book.VerifyOrderbook = z.CanVerifyOrderbook
|
||||
|
||||
err = z.Websocket.Orderbook.LoadSnapshot(&book)
|
||||
if err != nil {
|
||||
|
||||
@@ -301,10 +301,10 @@ func (z *ZB) FetchOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.B
|
||||
// UpdateOrderbook updates and returns the orderbook for a currency pair
|
||||
func (z *ZB) UpdateOrderbook(p currency.Pair, assetType asset.Item) (*orderbook.Base, error) {
|
||||
book := &orderbook.Base{
|
||||
ExchangeName: z.Name,
|
||||
Pair: p,
|
||||
AssetType: assetType,
|
||||
VerificationBypass: z.OrderbookVerificationBypass,
|
||||
Exchange: z.Name,
|
||||
Pair: p,
|
||||
Asset: assetType,
|
||||
VerifyOrderbook: z.CanVerifyOrderbook,
|
||||
}
|
||||
currFormat, err := z.FormatExchangeCurrency(p, assetType)
|
||||
if err != nil {
|
||||
|
||||
@@ -85,11 +85,11 @@ func ExchangeOrderbook(args ...objects.Object) (objects.Object, error) {
|
||||
}
|
||||
|
||||
data := make(map[string]objects.Object, 5)
|
||||
data["exchange"] = &objects.String{Value: ob.ExchangeName}
|
||||
data["exchange"] = &objects.String{Value: ob.Exchange}
|
||||
data["pair"] = &objects.String{Value: ob.Pair.String()}
|
||||
data["asks"] = &asks
|
||||
data["bids"] = &bids
|
||||
data["asset"] = &objects.String{Value: ob.AssetType.String()}
|
||||
data["asset"] = &objects.String{Value: ob.Asset.String()}
|
||||
|
||||
return &objects.Map{
|
||||
Value: data,
|
||||
|
||||
@@ -49,9 +49,9 @@ func (w Wrapper) Orderbook(exch string, pair currency.Pair, item asset.Item) (*o
|
||||
}
|
||||
|
||||
return &orderbook.Base{
|
||||
ExchangeName: exch,
|
||||
AssetType: item,
|
||||
Pair: pair,
|
||||
Exchange: exch,
|
||||
Asset: item,
|
||||
Pair: pair,
|
||||
Bids: []orderbook.Item{
|
||||
{
|
||||
Amount: 1,
|
||||
|
||||
6
main.go
6
main.go
@@ -63,8 +63,10 @@ func main() {
|
||||
flag.BoolVar(&settings.EnableTradeSyncing, "tradesync", false, "enables trade syncing for all enabled exchanges")
|
||||
flag.IntVar(&settings.SyncWorkers, "syncworkers", engine.DefaultSyncerWorkers, "the amount of workers (goroutines) to use for syncing exchange data")
|
||||
flag.BoolVar(&settings.SyncContinuously, "synccontinuously", true, "whether to sync exchange data continuously (ticker, orderbook and trade history info")
|
||||
flag.DurationVar(&settings.SyncTimeout, "synctimeout", engine.DefaultSyncerTimeout,
|
||||
"the amount of time before the syncer will switch from one protocol to the other (e.g. from REST to websocket)")
|
||||
flag.DurationVar(&settings.SyncTimeoutREST, "synctimeoutrest", engine.DefaultSyncerTimeoutREST,
|
||||
"the amount of time before the syncer will switch from rest protocol to the streaming protocol (e.g. from REST to websocket)")
|
||||
flag.DurationVar(&settings.SyncTimeoutWebsocket, "synctimeoutwebsocket", engine.DefaultSyncerTimeoutWebsocket,
|
||||
"the amount of time before the syncer will switch from the websocket protocol to REST protocol (e.g. from websocket to REST)")
|
||||
|
||||
// Forex provider settings
|
||||
flag.BoolVar(&settings.EnableCurrencyConverter, "currencyconverter", false, "overrides config and sets up foreign exchange Currency Converter")
|
||||
|
||||
Reference in New Issue
Block a user