mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-14 07:26:47 +00:00
* moved order and ticker fetching to return a pointer * return nil instead of empty struct * fixed incorrect nil * general cleanup
272 lines
6.8 KiB
Go
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 nil, 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)
|
|
}
|