Files
gocryptotrader/exchanges/orderbook/orderbook.go
Ryan O'Hara-Reid 22ff33cd54 Engine QA (#367)
* Improved error message when no config is set on startup

* Change inccorect error wording

* bump Bitfinex websocket orderbook return length to max

* temporary fix of incorrect orderbook updates, limit to bid and ask len of 100, will be extended later if needed

* Fixed issue in binance websocket that appended 0 volume bid/ask items

* Fix panic when unmarshalling an empty pair from config

* Add get pair asset method for exchange base
Fix Bitmex orderbook stream
Unbuffer Bitmex orderbook stream

* force syncer to update ticker instead of fetch, which allows a stream

* Fix websocket last price for coinbasepro

* fix websocket ticker for coinut

* Fix websocket orderbook stream Huobi

* increase orderbook depth REST for Huobi

* Fix websocket support and ensure data integrity

* Fix time parsing issue after error checks

* check error, only process enabled currency pairs, signal websocket data processing

* expanded websocket functionality for okgroup

* Add logic to not process zero length slice for orderbooks

* fix websocket ticker only updating enabled and individual book updates

* ZB fixes to order submission/retrieval/cancellation w/ general fixes

* Quiet unnecessary warning

* updated config entry values for REST and websocket (initial hack until I come up with a better solution for asset types)

* Ch GetName function to field access modifyer & rm useless code

* Add in error I missed

* Nits addressed

* some more fixes

* Turned kraken default websocket to true and some small changes

* fixes linter issues

* Ensured okgroup books and sent update through to datahandler. Zb update as well.

* Add test case to get asset type from pair

* Add test for pairs unmarshal

* Add testing and addressed nits

* FIX linter issue

* Addressed Gees nits

* Thanks glorious spotter

* more nitorinos

* Addres even more nits

* Add stringerino 4000

* Fix for panic cause by sort slice out of range, also nits addressed

* fix linter issues

* Changed from function to field access

* Changed from function to field access

* fix for orderbook update panic, removes quick fix - caused by sync item fetching through same protocol

* Add new test and update random generator

* pass in invalid string to future ob fetching, due to futures contract expire and a http 400 error is returned
2019-11-04 15:34:30 +11:00

272 lines
6.8 KiB
Go

package orderbook
import (
"errors"
"fmt"
"sort"
"strings"
"time"
"github.com/gofrs/uuid"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/dispatch"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
// Get checks and returns the orderbook given an exchange name and currency pair
// if it exists
func Get(exchange string, p currency.Pair, a asset.Item) (Base, error) {
o, err := service.Retrieve(exchange, p, a)
if err != nil {
return Base{}, err
}
return *o, nil
}
// 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.RLock()
defer service.RUnlock()
book, ok := service.Books[exchange][p.Base.Item][p.Quote.Item][a]
if !ok {
return dispatch.Pipe{}, fmt.Errorf("orderbook item not found for %s %s %s",
exchange,
p,
a)
}
return service.mux.Subscribe(book.Main)
}
// SubscribeToExchangeOrderbooks subcribes to all orderbooks on an exchange
func SubscribeToExchangeOrderbooks(exchange string) (dispatch.Pipe, error) {
exchange = strings.ToLower(exchange)
service.RLock()
defer service.RUnlock()
id, ok := service.Exchange[exchange]
if !ok {
return dispatch.Pipe{}, fmt.Errorf("%s exchange orderbooks not found",
exchange)
}
return service.mux.Subscribe(id)
}
// Update stores orderbook data
func (s *Service) Update(b *Base) error {
var ids []uuid.UUID
s.Lock()
switch {
case s.Books[b.ExchangeName] == nil:
s.Books[b.ExchangeName] = make(map[*currency.Item]map[*currency.Item]map[asset.Item]*Book)
s.Books[b.ExchangeName][b.Pair.Base.Item] = make(map[*currency.Item]map[asset.Item]*Book)
s.Books[b.ExchangeName][b.Pair.Base.Item][b.Pair.Quote.Item] = make(map[asset.Item]*Book)
err := s.SetNewData(b)
if err != nil {
s.Unlock()
return err
}
case s.Books[b.ExchangeName][b.Pair.Base.Item] == nil:
s.Books[b.ExchangeName][b.Pair.Base.Item] = make(map[*currency.Item]map[asset.Item]*Book)
s.Books[b.ExchangeName][b.Pair.Base.Item][b.Pair.Quote.Item] = make(map[asset.Item]*Book)
err := s.SetNewData(b)
if err != nil {
s.Unlock()
return err
}
case s.Books[b.ExchangeName][b.Pair.Base.Item][b.Pair.Quote.Item] == nil:
s.Books[b.ExchangeName][b.Pair.Base.Item][b.Pair.Quote.Item] = make(map[asset.Item]*Book)
err := s.SetNewData(b)
if err != nil {
s.Unlock()
return err
}
case s.Books[b.ExchangeName][b.Pair.Base.Item][b.Pair.Quote.Item][b.AssetType] == nil:
err := s.SetNewData(b)
if err != nil {
s.Unlock()
return err
}
default:
book := s.Books[b.ExchangeName][b.Pair.Base.Item][b.Pair.Quote.Item][b.AssetType]
book.b.Bids = b.Bids
book.b.Asks = b.Asks
book.b.LastUpdated = b.LastUpdated
ids = book.Assoc
ids = append(ids, book.Main)
}
s.Unlock()
return s.mux.Publish(ids, b)
}
// SetNewData sets new data
func (s *Service) SetNewData(b *Base) error {
ids, err := s.GetAssociations(b)
if err != nil {
return err
}
singleID, err := s.mux.GetID()
if err != nil {
return err
}
// 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
cpyBook := *b
cpyBook.Bids = make([]Item, len(b.Bids))
copy(cpyBook.Bids, b.Bids)
cpyBook.Asks = make([]Item, len(b.Asks))
copy(cpyBook.Asks, b.Asks)
s.Books[b.ExchangeName][b.Pair.Base.Item][b.Pair.Quote.Item][b.AssetType] = &Book{
b: &cpyBook,
Main: singleID,
Assoc: ids}
return nil
}
// GetAssociations links a singular book with it's dispatch associations
func (s *Service) GetAssociations(b *Base) ([]uuid.UUID, error) {
if b == nil {
return nil, errors.New("orderbook is nil")
}
var ids []uuid.UUID
exchangeID, ok := s.Exchange[b.ExchangeName]
if !ok {
var err error
exchangeID, err = s.mux.GetID()
if err != nil {
return nil, err
}
s.Exchange[b.ExchangeName] = exchangeID
}
ids = append(ids, exchangeID)
return ids, nil
}
// Retrieve gets orderbook data from the slice
func (s *Service) Retrieve(exchange string, p currency.Pair, a asset.Item) (*Base, error) {
exchange = strings.ToLower(exchange)
s.RLock()
defer s.RUnlock()
if s.Books[exchange] == nil {
return nil, fmt.Errorf("no orderbooks for %s exchange", exchange)
}
if s.Books[exchange][p.Base.Item] == nil {
return nil, fmt.Errorf("no orderbooks associated with base currency %s",
p.Base)
}
if s.Books[exchange][p.Base.Item][p.Quote.Item] == nil {
return nil, fmt.Errorf("no orderbooks associated with quote currency %s",
p.Quote)
}
if s.Books[exchange][p.Base.Item][p.Quote.Item][a] == nil {
return nil, fmt.Errorf("no orderbooks associated with asset type %s",
a)
}
return s.Books[exchange][p.Base.Item][p.Quote.Item][a].b, nil
}
// TotalBidsAmount returns the total amount of bids and the total orderbook
// bids value
func (b *Base) TotalBidsAmount() (amountCollated, total float64) {
for x := range b.Bids {
amountCollated += b.Bids[x].Amount
total += b.Bids[x].Amount * b.Bids[x].Price
}
return amountCollated, total
}
// TotalAsksAmount returns the total amount of asks and the total orderbook
// asks value
func (b *Base) TotalAsksAmount() (amountCollated, total float64) {
for y := range b.Asks {
amountCollated += b.Asks[y].Amount
total += b.Asks[y].Amount * b.Asks[y].Price
}
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
// 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() {
var lastPrice float64
var sortBids, sortAsks bool
for x := range b.Bids {
if lastPrice != 0 && b.Bids[x].Price >= lastPrice {
sortBids = true
break
}
lastPrice = b.Bids[x].Price
}
lastPrice = 0
for x := range b.Asks {
if lastPrice != 0 && b.Asks[x].Price <= lastPrice {
sortAsks = true
break
}
lastPrice = b.Asks[x].Price
}
if sortBids {
sort.Sort(sort.Reverse(byOBPrice(b.Bids)))
}
if sortAsks {
sort.Sort((byOBPrice(b.Asks)))
}
}
// Process processes incoming orderbooks, creating or updating the orderbook
// list
func (b *Base) Process() error {
if b.ExchangeName == "" {
return errors.New(errExchangeNameUnset)
}
b.ExchangeName = strings.ToLower(b.ExchangeName)
if b.Pair.IsEmpty() {
return errors.New(errPairNotSet)
}
if b.AssetType.String() == "" {
return errors.New(errAssetTypeNotSet)
}
if len(b.Asks) == 0 && len(b.Bids) == 0 {
return errors.New(errNoOrderbook)
}
if b.LastUpdated.IsZero() {
b.LastUpdated = time.Now()
}
b.Verify()
return service.Update(b)
}