Files
gocryptotrader/exchanges/ticker/ticker.go
cranktakular fd9aaf00a2 Coinbase: Update exchange implementation (#1480)
* Slight enhance of Coinbase tests

Continual enhance of Coinbase tests

The revamp continues

Oh jeez the Orderbook part's unfinished don't look

Coinbase revamp, Orderbook still unfinished

* Coinbase revamp; CreateReport is still WIP

* More coinbase improvements; onto sandbox testing

* Coinbase revamp continues

* Coinbase revamp continues

* Coinbasepro revamp is ceaseless

* Coinbase revamp, starting on advanced trade API

* Coinbase Advanced Trade Starts in Ernest

V3 done, onto V2

Coinbase revamp nears completion

Coinbase revamp nears completion

Test commit should fail

Coinbase revamp nears completion

* Coinbase revamp stage wrapper

* Coinbase wrapper coherence continues

* Coinbase wrapper continues writhing

* Coinbase wrapper & codebase cleanup

* Coinbase updates & wrap progress

* More Coinbase wrapper progress

* Wrapper is wrapped, kinda

* Test & type checking

* Coinbase REST revamp finished

* Post-merge fix

* WS revamp begins

* WS Main Revamp Done?

* CB websocket tidying up

* Coinbase WS wrapperupperer

* Coinbase revamp done??

* Linter progress

* Continued lint cleanup

* Further lint cleanup

* Increased lint coverage

* Does this fix all sloppy reassigns & shadowing?

* Undoing retry policy change

* Documentation regeneration

* Coinbase code improvements

* Providing warning about known issue

* Updating an error to new format

* Making gocritic happy

* Review adherence

* Endpoints moved to V3 & nil pointer fixes

* Removing seemingly superfluous constant

* Glorious improvements

* Removing unused error

* Partial public endpoint addition

* Slight improvements

* Wrapper improvements; still a few errors left in other packages

* A lil Coinbase progress

* Json cleaning

* Lint appeasement

* Config repair

* Config fix (real)

* Little fix

* New public endpoint incorporation

* Additional fixes

* Improvements & Appeasements

* LineSaver

* Additional fixes

* Another fix

* Fixing picked nits

* Quick fixies

* Lil fixes

* Subscriptions: Add List.Enabled

* CoinbasePro: Add subscription templating

* fixup! CoinbasePro: Add subscription templating

* fixup! CoinbasePro: Add subscription templating

* Comment fix

* Subsequent fixes

* Issues hopefully fixed

* Lint fix

* Glorious fixes

* Json formatting

* ShazNits

* (L/N)i(n/)t

* Adding a test

* Tiny test improvement

* Template patch testing

* Fixes

* Further shaznits

* Lint nit

* JWT move and other fixes

* Small nits

* Shaznit, singular

* Post-merge fix

* Post-merge fixes

* Typo fix

* Some glorious nits

* Required changes

* Stop going

* Alias attempt

* Alias fix & test cleanup

* Test fix

* GetDepositAddress logic improvement

* Status update: Fixed

* Lint fix

* Happy birthday to PR 1480

* Cleanups

* Necessary nit corrections

* Fixing sillybug

* As per request

* Programming progress

* Order fixes

* Further fixies

* Test fix

* Pre-merge fixes

* More shaznits

* Context

* Sonic error handling

* Import fix

* Better Sonic error handling

* Perfect Sonic error handling?

* F purge

* Coinbase improvements

* API Update Conformity

* Coinbase continuation

* Coinbase order improvements

* Coinbase order improvements

* CreateOrderConfig improvements

* Managing API updates

* Coinbase API update progression

* jwt rename

* Comment link fix

* Coinbase v2 cleanup

* Post-merge fixes

* Review fixes

* GK's suggestions

* Linter fix

* Minor gbjk fixes

* Nit fixes

* Merge fix

* Lint fixes

* Coinbase rename stage 1

* Coinbase rename stage 2

* Coinbase rename stage 3

* Coinbase rename stage 4

* Coinbase rename final fix

* Coinbase: PoC on converting to request structs

* Applying requested changes

* Many review fixes, handled

* Thrashed by nits

* More minor modifications

* The last nit!?

---------

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
2025-09-16 13:37:00 +10:00

241 lines
5.9 KiB
Go

package ticker
import (
"errors"
"fmt"
"strings"
"time"
"github.com/gofrs/uuid"
"github.com/thrasher-corp/gocryptotrader/common"
"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"
)
// Public errors
var (
ErrTickerNotFound = errors.New("no ticker found")
ErrBidEqualsAsk = errors.New("bid equals ask this is a crossed or locked market")
)
var (
errInvalidTicker = errors.New("invalid ticker")
errBidGreaterThanAsk = errors.New("bid greater than ask this is a crossed or locked market")
errExchangeNotFound = errors.New("exchange not found")
)
func init() {
service = new(Service)
service.Tickers = make(map[key.ExchangeAssetPair]*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.NewExchangeAssetPair(exchange, a, p)]
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, common.ErrExchangeNameNotSet
}
if p.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
if !a.IsValid() {
return nil, fmt.Errorf("%w %q", asset.ErrNotSupported, a)
}
exchange = strings.ToLower(exchange)
service.mu.Lock()
defer service.mu.Unlock()
tick, ok := service.Tickers[key.NewExchangeAssetPair(exchange, a, p)]
if !ok {
return nil, fmt.Errorf("%w %s %s %s", ErrTickerNotFound, exchange, p, a)
}
cpy := tick.Price // Don't let external functions have access to underlying
return &cpy, nil
}
// GetExchangeTickers returns all tickers for a given exchange
func GetExchangeTickers(exchange string) ([]*Price, error) {
return service.getExchangeTickers(exchange)
}
func (s *Service) getExchangeTickers(exchange string) ([]*Price, error) {
if exchange == "" {
return nil, common.ErrExchangeNameNotSet
}
exchange = strings.ToLower(exchange)
s.mu.Lock()
defer s.mu.Unlock()
_, ok := s.Exchange[exchange]
if !ok {
return nil, fmt.Errorf("%w %v", errExchangeNotFound, exchange)
}
tickers := make([]*Price, 0, len(s.Tickers))
for k, v := range s.Tickers {
if k.Exchange != exchange {
continue
}
cpy := v.Price // Don't let external functions have access to underlying
tickers = append(tickers, &cpy)
}
return tickers, 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 common.ErrExchangeNameNotSet
}
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.NewExchangeAssetPair(name, p.AssetType, p.Pair)
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 its dispatch associations
func (s *Service) getAssociations(exch string) ([]uuid.UUID, error) {
if exch == "" {
return nil, common.ErrExchangeNameNotSet
}
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
}