Files
gocryptotrader/exchanges/ticker/ticker.go
Gareth Kirwan 37b1121bbd BTSE: Fix duplicate pair errors on Million pairs (M_*) (#1401)
* BTSE: Fix duplicate error on Million pairs (M_*)

BTSE has listed Pitbull token with two symbols:
PIT-USD and M_PIT-USD for millons of PIT / USD.
The native token is not tradable, so we ignore them and
get a base of M_PIT because that's what later APIs will accept

* BTSE: Fix test errors on locked market

* Common: Improve AppendError and ExcludeError

This change switches from a stateful multiError to caring more about the
Unwrap() []error interface, the same as [go standard
lib](https://github.com/golang/go/blob/go1.21.4/src/errors/wrap.go#L54-L68)

Notably, if we implement Unwrap() []error and do NOT implement Is() then
we get free compatibility with the core functions.

The only distateful thing here is needing to deeply unwrap fmt.Errorf
errors, since they don't flatten. I can't see any way around that

* Pairs: Fix exchange config Pairs loading

When a pair string contained two punctuation runes, the first one is used,
and the configFormat is ignored.

This fix checks the list and corrects any with the wrong delimiter, or
errors if the format is inconsistent.

* BTSE: Fix all tickers retrieved by GetTicker

PR #764 introduced GetTickers, but it wasn't rolled out to BTSE.
This fix ensures that when one ticker is a locked market, the rest continue to
function. Particularly important if the locked market wasn't even
enabled anyway.

* Kucoin: Fix test config future pairs

* BTSE: Remove PIT tests; Token removed

BTSE have removed the PIT token pairs

All these changes stand, and this just removes the test

* ITBit: Fix fatal error on second run

This fix removes incorrect config pair delimiter, because it would be
re-inserted into config the first run, and then error the second time.

This delimiter doesn't match the config we have.
There's no implementation of fetching pairs, so what's in config files
now is all that matters

* Engine: Fix TestConfigAllJsonResponse

* Clarity of non-matching json improved
* Handling for fixing pair delimiters
2023-12-19 14:40:13 +11:00

227 lines
5.4 KiB
Go

package ticker
import (
"errors"
"fmt"
"strings"
"time"
"github.com/gofrs/uuid"
"github.com/thrasher-corp/gocryptotrader/common/key"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/dispatch"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
)
var (
// ErrBidEqualsAsk error for locked markets
ErrBidEqualsAsk = errors.New("bid equals ask this is a crossed or locked market")
errInvalidTicker = errors.New("invalid ticker")
errTickerNotFound = errors.New("ticker not found")
errExchangeNameIsEmpty = errors.New("exchange name is empty")
errBidGreaterThanAsk = errors.New("bid greater than ask this is a crossed or locked market")
)
func init() {
service = new(Service)
service.Tickers = make(map[key.ExchangePairAsset]*Ticker)
service.Exchange = make(map[string]uuid.UUID)
service.mux = dispatch.GetNewMux(nil)
}
// SubscribeTicker subscribes to a ticker and returns a communication channel to
// stream new ticker updates
func SubscribeTicker(exchange string, p currency.Pair, a asset.Item) (dispatch.Pipe, error) {
exchange = strings.ToLower(exchange)
service.mu.Lock()
defer service.mu.Unlock()
tick, ok := service.Tickers[key.ExchangePairAsset{
Exchange: exchange,
Base: p.Base.Item,
Quote: p.Quote.Item,
Asset: a,
}]
if !ok {
return dispatch.Pipe{}, fmt.Errorf("ticker item not found for %s %s %s",
exchange,
p,
a)
}
return service.mux.Subscribe(tick.Main)
}
// SubscribeToExchangeTickers subscribes to all tickers on an exchange
func SubscribeToExchangeTickers(exchange string) (dispatch.Pipe, error) {
exchange = strings.ToLower(exchange)
service.mu.Lock()
defer service.mu.Unlock()
id, ok := service.Exchange[exchange]
if !ok {
return dispatch.Pipe{}, fmt.Errorf("%s exchange tickers not found",
exchange)
}
return service.mux.Subscribe(id)
}
// GetTicker checks and returns a requested ticker if it exists
func GetTicker(exchange string, p currency.Pair, a asset.Item) (*Price, error) {
if exchange == "" {
return nil, errExchangeNameIsEmpty
}
if p.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
if !a.IsValid() {
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, a)
}
exchange = strings.ToLower(exchange)
service.mu.Lock()
defer service.mu.Unlock()
tick, ok := service.Tickers[key.ExchangePairAsset{
Exchange: exchange,
Base: p.Base.Item,
Quote: p.Quote.Item,
Asset: a,
}]
if !ok {
return nil, fmt.Errorf("no tickers associated with asset type %s %s %s",
exchange, p, a)
}
cpy := tick.Price // Don't let external functions have access to underlying
return &cpy, nil
}
// FindLast searches for a currency pair and returns the first available
func FindLast(p currency.Pair, a asset.Item) (float64, error) {
service.mu.Lock()
defer service.mu.Unlock()
for mapKey, t := range service.Tickers {
if !mapKey.MatchesPairAsset(p, a) {
continue
}
if t.Last == 0 {
return 0, errInvalidTicker
}
return t.Last, nil
}
return 0, fmt.Errorf("%w %s %s", errTickerNotFound, p, a)
}
// ProcessTicker processes incoming tickers, creating or updating the Tickers
// list
func ProcessTicker(p *Price) error {
if p == nil {
return errors.New(errTickerPriceIsNil)
}
if p.ExchangeName == "" {
return fmt.Errorf(ErrExchangeNameUnset)
}
if p.Pair.IsEmpty() {
return fmt.Errorf("%s %s", p.ExchangeName, errPairNotSet)
}
if p.Bid != 0 && p.Ask != 0 {
switch {
case p.ExchangeName == "Bitfinex" && p.AssetType == asset.MarginFunding:
// Margin funding books can be crossed see Bitfinex.
default:
if p.Bid == p.Ask {
return fmt.Errorf("%s %s %w",
p.ExchangeName,
p.Pair,
ErrBidEqualsAsk)
}
if p.Bid > p.Ask {
return fmt.Errorf("%s %s %w",
p.ExchangeName,
p.Pair,
errBidGreaterThanAsk)
}
}
}
if p.AssetType == asset.Empty {
return fmt.Errorf("%s %s %s",
p.ExchangeName,
p.Pair,
errAssetTypeNotSet)
}
if p.LastUpdated.IsZero() {
p.LastUpdated = time.Now()
}
return service.update(p)
}
// update updates ticker price
func (s *Service) update(p *Price) error {
name := strings.ToLower(p.ExchangeName)
mapKey := key.ExchangePairAsset{
Exchange: name,
Base: p.Pair.Base.Item,
Quote: p.Pair.Quote.Item,
Asset: p.AssetType,
}
s.mu.Lock()
t, ok := service.Tickers[mapKey]
if !ok || t == nil {
newTicker := &Ticker{}
err := s.setItemID(newTicker, p, name)
if err != nil {
s.mu.Unlock()
return err
}
service.Tickers[mapKey] = newTicker
s.mu.Unlock()
return nil
}
t.Price = *p
//nolint: gocritic
ids := append(t.Assoc, t.Main)
s.mu.Unlock()
return s.mux.Publish(p, ids...)
}
// setItemID retrieves and sets dispatch mux publish IDs
func (s *Service) setItemID(t *Ticker, p *Price, exch string) error {
ids, err := s.getAssociations(exch)
if err != nil {
return err
}
singleID, err := s.mux.GetID()
if err != nil {
return err
}
t.Price = *p
t.Main = singleID
t.Assoc = ids
return nil
}
// getAssociations links a singular book with it's dispatch associations
func (s *Service) getAssociations(exch string) ([]uuid.UUID, error) {
if exch == "" {
return nil, errExchangeNameIsEmpty
}
var ids []uuid.UUID
exchangeID, ok := s.Exchange[exch]
if !ok {
var err error
exchangeID, err = s.mux.GetID()
if err != nil {
return nil, err
}
s.Exchange[exch] = exchangeID
}
ids = append(ids, exchangeID)
return ids, nil
}