Files
gocryptotrader/exchanges/coinbase/coinbase_wrapper.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

1207 lines
42 KiB
Go

package coinbase
import (
"context"
"fmt"
"maps"
"slices"
"time"
"github.com/shopspring/decimal"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/key"
"github.com/thrasher-corp/gocryptotrader/config"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchange/order/limits"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket/buffer"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/deposit"
"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
"github.com/thrasher-corp/gocryptotrader/exchanges/futures"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
"github.com/thrasher-corp/gocryptotrader/log"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
// SetDefaults sets default values for the exchange
func (e *Exchange) SetDefaults() {
e.Name = "Coinbase"
e.Enabled = true
e.API.CredentialsValidator.RequiresKey = true
e.API.CredentialsValidator.RequiresSecret = true
requestFmt := &currency.PairFormat{Delimiter: currency.DashDelimiter, Uppercase: true}
configFmt := &currency.PairFormat{Delimiter: currency.DashDelimiter, Uppercase: true}
err := e.SetGlobalPairsManager(requestFmt, configFmt, asset.Spot, asset.Futures)
if err != nil {
log.Errorln(log.ExchangeSys, err)
}
e.Features = exchange.Features{
Supports: exchange.FeaturesSupported{
REST: true,
Websocket: true,
RESTCapabilities: protocol.Features{
AutoPairUpdates: true,
AccountBalance: true,
CryptoDeposit: true,
CryptoWithdrawal: true,
FiatWithdraw: true,
GetOrder: true,
GetOrders: true,
CancelOrders: true,
CancelOrder: true,
SubmitOrder: true,
ModifyOrder: true,
DepositHistory: true,
WithdrawalHistory: true,
FiatWithdrawalFee: true,
CryptoWithdrawalFee: true,
TickerFetching: true,
KlineFetching: true,
OrderbookFetching: true,
AccountInfo: true,
FiatDeposit: true,
FundingRateFetching: true,
HasAssetTypeAccountSegregation: true,
},
WebsocketCapabilities: protocol.Features{
TickerFetching: true,
OrderbookFetching: true,
Subscribe: true,
Unsubscribe: true,
AuthenticatedEndpoints: true,
MessageSequenceNumbers: true,
GetOrders: true,
GetOrder: true,
},
WithdrawPermissions: exchange.AutoWithdrawCryptoWithAPIPermission |
exchange.AutoWithdrawFiatWithAPIPermission,
Kline: kline.ExchangeCapabilitiesSupported{
DateRanges: true,
Intervals: true,
},
},
Enabled: exchange.FeaturesEnabled{
AutoPairUpdates: true,
Kline: kline.ExchangeCapabilitiesEnabled{
Intervals: kline.DeployExchangeIntervals(
kline.IntervalCapacity{Interval: kline.OneMin},
kline.IntervalCapacity{Interval: kline.FiveMin},
kline.IntervalCapacity{Interval: kline.FifteenMin},
kline.IntervalCapacity{Interval: kline.ThirtyMin},
kline.IntervalCapacity{Interval: kline.OneHour},
kline.IntervalCapacity{Interval: kline.TwoHour},
kline.IntervalCapacity{Interval: kline.SixHour},
kline.IntervalCapacity{Interval: kline.OneDay},
),
GlobalResultLimit: 300,
},
},
Subscriptions: defaultSubscriptions.Clone(),
TradingRequirements: protocol.TradingRequirements{},
}
if e.Requester, err = request.New(e.Name, common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout), request.WithLimiter(rateLimits)); err != nil {
log.Errorln(log.ExchangeSys, err)
}
e.API.Endpoints = e.NewEndpoints()
if err = e.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{
exchange.RestSpot: apiURL,
exchange.RestSandbox: sandboxAPIURL,
exchange.WebsocketSpot: coinbaseWebsocketURL,
exchange.RestSpotSupplementary: v1APIURL,
}); err != nil {
log.Errorln(log.ExchangeSys, err)
}
e.Websocket = websocket.NewManager()
e.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
e.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
e.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
}
// Setup initialises the exchange parameters with the current configuration
func (e *Exchange) Setup(exch *config.Exchange) error {
if err := exch.Validate(); err != nil {
return err
}
if !exch.Enabled {
e.SetEnabled(false)
return nil
}
if err := e.SetupDefaults(exch); err != nil {
return err
}
e.checkSubscriptions()
wsRunningURL, err := e.API.Endpoints.GetURL(exchange.WebsocketSpot)
if err != nil {
return err
}
if err := e.Websocket.Setup(&websocket.ManagerSetup{
ExchangeConfig: exch,
DefaultURL: coinbaseWebsocketURL,
RunningURL: wsRunningURL,
Connector: e.WsConnect,
Subscriber: e.Subscribe,
Unsubscriber: e.Unsubscribe,
GenerateSubscriptions: e.generateSubscriptions,
Features: &e.Features.Supports.WebsocketCapabilities,
OrderbookBufferConfig: buffer.Config{
SortBuffer: true,
},
}); err != nil {
return err
}
return e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
ResponseMaxLimit: exch.WebsocketResponseMaxLimit,
})
}
// FetchTradablePairs returns a list of the exchanges tradable pairs
func (e *Exchange) FetchTradablePairs(ctx context.Context, a asset.Item) (currency.Pairs, error) {
aString := FormatAssetOutbound(a)
products, err := e.GetAllProducts(ctx, 0, 0, aString, "", "", "", nil, false, true, false)
if err != nil {
return nil, err
}
pairs := make([]currency.Pair, 0, len(products.Products))
aliases := make(map[currency.Pair]currency.Pairs)
for x := range products.Products {
if products.Products[x].TradingDisabled {
continue
}
if products.Products[x].Price == 0 {
continue
}
pairs = append(pairs, products.Products[x].ID)
if !products.Products[x].Alias.IsEmpty() {
aliases[products.Products[x].Alias] = aliases[products.Products[x].Alias].Add(products.Products[x].ID)
}
if len(products.Products[x].AliasTo) > 0 {
aliases[products.Products[x].ID] = aliases[products.Products[x].ID].Add(products.Products[x].AliasTo...)
}
// Products need to be considered aliases of themselves for some code in websocket, and it seems better to add that here
aliases[products.Products[x].ID] = aliases[products.Products[x].ID].Add(products.Products[x].ID)
}
e.pairAliases.Load(aliases)
return pairs, nil
}
// UpdateTradablePairs updates the exchanges available pairs and stores them in the exchanges config
func (e *Exchange) UpdateTradablePairs(ctx context.Context, forceUpdate bool) error {
assets := e.GetAssetTypes(false)
for i := range assets {
pairs, err := e.FetchTradablePairs(ctx, assets[i])
if err != nil {
return err
}
if err := e.UpdatePairs(pairs, assets[i], false, forceUpdate); err != nil {
return err
}
}
return e.EnsureOnePairEnabled()
}
// UpdateAccountInfo retrieves balances for all enabled currencies for the coinbase exchange
func (e *Exchange) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (account.Holdings, error) {
var (
response account.Holdings
accountBalance []Account
done bool
err error
cursor int64
accountResp *AllAccountsResponse
)
response.Exchange = e.Name
for !done {
if accountResp, err = e.ListAccounts(ctx, 250, cursor); err != nil {
return response, err
}
accountBalance = append(accountBalance, accountResp.Accounts...)
done = !accountResp.HasNext
cursor = int64(accountResp.Cursor)
}
accountCurrencies := make(map[string][]account.Balance)
for i := range accountBalance {
profileID := accountBalance[i].UUID
currencies := accountCurrencies[profileID]
accountCurrencies[profileID] = append(currencies, account.Balance{
Currency: currency.NewCode(accountBalance[i].Currency),
Total: accountBalance[i].AvailableBalance.Value.Float64(),
Hold: accountBalance[i].Hold.Value.Float64(),
Free: accountBalance[i].AvailableBalance.Value.Float64() - accountBalance[i].Hold.Value.Float64(),
AvailableWithoutBorrow: accountBalance[i].AvailableBalance.Value.Float64(),
})
}
if response.Accounts, err = account.CollectBalances(accountCurrencies, assetType); err != nil {
return account.Holdings{}, err
}
creds, err := e.GetCredentials(ctx)
if err != nil {
return account.Holdings{}, err
}
if err := account.Process(&response, creds); err != nil {
return account.Holdings{}, err
}
return response, nil
}
// UpdateTickers updates all currency pairs of a given asset type
func (e *Exchange) UpdateTickers(context.Context, asset.Item) error {
return common.ErrFunctionNotSupported
}
// UpdateTicker updates and returns the ticker for a currency pair
func (e *Exchange) UpdateTicker(ctx context.Context, p currency.Pair, a asset.Item) (*ticker.Price, error) {
fPair, err := e.FormatExchangeCurrency(p, a)
if err != nil {
return nil, err
}
if err := e.tickerHelper(ctx, fPair, a); err != nil {
return nil, err
}
return ticker.GetTicker(e.Name, p, a)
}
// UpdateOrderbook updates and returns the orderbook for a currency pair
func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetType asset.Item) (*orderbook.Book, error) {
if p.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
p, err := e.FormatExchangeCurrency(p, assetType)
if err != nil {
return nil, err
}
if err := e.CurrencyPairs.IsAssetEnabled(assetType); err != nil {
return nil, err
}
book := &orderbook.Book{
Exchange: e.Name,
Pair: p,
Asset: assetType,
ValidateOrderbook: e.ValidateOrderbook,
}
var orderbookNew *ProductBookResp
if orderbookNew, err = e.GetProductBookV3(ctx, p, 1000, 0, false); err != nil {
return book, err
}
book.Bids = make(orderbook.Levels, len(orderbookNew.Pricebook.Bids))
for x := range orderbookNew.Pricebook.Bids {
book.Bids[x] = orderbook.Level{
Amount: orderbookNew.Pricebook.Bids[x].Size.Float64(),
Price: orderbookNew.Pricebook.Bids[x].Price.Float64(),
}
}
book.Asks = make(orderbook.Levels, len(orderbookNew.Pricebook.Asks))
for x := range orderbookNew.Pricebook.Asks {
book.Asks[x] = orderbook.Level{
Amount: orderbookNew.Pricebook.Asks[x].Size.Float64(),
Price: orderbookNew.Pricebook.Asks[x].Price.Float64(),
}
}
aliases := e.pairAliases.GetAlias(p)
var errs error
var validPairs currency.Pairs
for i := range aliases {
isEnabled, err := e.CurrencyPairs.IsPairEnabled(aliases[i], assetType)
if err != nil {
errs = common.AppendError(errs, err)
continue
}
if isEnabled {
book.Pair = aliases[i]
if err := book.Process(); err != nil {
errs = common.AppendError(errs, err)
continue
}
validPairs = append(validPairs, book.Pair)
}
}
if errs != nil {
return book, errs
}
if len(validPairs) == 0 {
return book, errPairsDisabledOrErrored
}
return orderbook.Get(e.Name, validPairs[0], assetType)
}
// GetAccountFundingHistory returns funding history, deposits and withdrawals
func (e *Exchange) GetAccountFundingHistory(ctx context.Context) ([]exchange.FundingHistory, error) {
wallIDs, err := e.GetAllWallets(ctx, PaginationInp{})
if err != nil {
return nil, err
}
if len(wallIDs.Data) == 0 {
return nil, errNoWalletsReturned
}
var accHistory []DeposWithdrData
for i := range wallIDs.Data {
tempAccHist, err := e.GetAllFiatTransfers(ctx, wallIDs.Data[i].ID, PaginationInp{}, FiatDeposit)
if err != nil {
return nil, err
}
accHistory = append(accHistory, tempAccHist.Data...)
if tempAccHist, err = e.GetAllFiatTransfers(ctx, wallIDs.Data[i].ID, PaginationInp{}, FiatWithdrawal); err != nil {
return nil, err
}
accHistory = append(accHistory, tempAccHist.Data...)
}
var cryptoHistory []TransactionData
for i := range wallIDs.Data {
tempCryptoHist, err := e.GetAllTransactions(ctx, wallIDs.Data[i].ID, PaginationInp{})
if err != nil {
return nil, err
}
for j := range tempCryptoHist.Data {
if tempCryptoHist.Data[j].Type == "receive" || tempCryptoHist.Data[j].Type == "send" {
cryptoHistory = append(cryptoHistory, tempCryptoHist.Data[j])
}
}
}
return e.processFundingData(accHistory, cryptoHistory)
}
// GetWithdrawalsHistory returns previous withdrawals data
func (e *Exchange) GetWithdrawalsHistory(ctx context.Context, cur currency.Code, _ asset.Item) ([]exchange.WithdrawalHistory, error) {
tempWallIDs, err := e.GetAllWallets(ctx, PaginationInp{})
if err != nil {
return nil, err
}
if len(tempWallIDs.Data) == 0 {
return nil, errNoWalletsReturned
}
var wallIDs []string
for i := range tempWallIDs.Data {
if tempWallIDs.Data[i].Currency.Code == cur.String() {
wallIDs = append(wallIDs, tempWallIDs.Data[i].ID)
}
}
if len(wallIDs) == 0 {
return nil, errNoMatchingWallets
}
var accHistory []DeposWithdrData
for i := range wallIDs {
tempAccHist, err := e.GetAllFiatTransfers(ctx, wallIDs[i], PaginationInp{}, FiatWithdrawal)
if err != nil {
return nil, err
}
accHistory = append(accHistory, tempAccHist.Data...)
}
var cryptoHistory []TransactionData
for i := range wallIDs {
tempCryptoHist, err := e.GetAllTransactions(ctx, wallIDs[i], PaginationInp{})
if err != nil {
return nil, err
}
for j := range tempCryptoHist.Data {
if tempCryptoHist.Data[j].Type == "send" {
cryptoHistory = append(cryptoHistory, tempCryptoHist.Data[j])
}
}
}
tempFundingData, err := e.processFundingData(accHistory, cryptoHistory)
if err != nil {
return nil, err
}
fundingData := make([]exchange.WithdrawalHistory, len(tempFundingData))
for i := range tempFundingData {
fundingData[i] = exchange.WithdrawalHistory{
Status: tempFundingData[i].Status,
TransferID: tempFundingData[i].TransferID,
Description: tempFundingData[i].Description,
Timestamp: tempFundingData[i].Timestamp,
Currency: tempFundingData[i].Currency,
Amount: tempFundingData[i].Amount,
Fee: tempFundingData[i].Fee,
TransferType: tempFundingData[i].TransferType,
CryptoToAddress: tempFundingData[i].CryptoToAddress,
CryptoTxID: tempFundingData[i].CryptoTxID,
CryptoChain: tempFundingData[i].CryptoChain,
BankTo: tempFundingData[i].BankTo,
}
}
return fundingData, nil
}
// GetRecentTrades returns the most recent trades for a currency and asset
func (e *Exchange) GetRecentTrades(context.Context, currency.Pair, asset.Item) ([]trade.Data, error) {
return nil, common.ErrFunctionNotSupported
}
// GetHistoricTrades returns historic trade data within the timeframe provided
func (e *Exchange) GetHistoricTrades(_ context.Context, _ currency.Pair, _ asset.Item, _, _ time.Time) ([]trade.Data, error) {
return nil, common.ErrFunctionNotSupported
}
// SubmitOrder submits a new order
func (e *Exchange) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) {
if err := s.Validate(e.GetTradingRequirements()); err != nil {
return nil, err
}
fPair, err := e.FormatExchangeCurrency(s.Pair, s.AssetType)
if err != nil {
return nil, err
}
var stopDir string
if s.Type == order.StopLimit {
switch s.StopDirection {
case order.StopUp:
stopDir = "STOP_DIRECTION_STOP_UP"
case order.StopDown:
stopDir = "STOP_DIRECTION_STOP_DOWN"
}
}
resp, err := e.PlaceOrder(ctx, &PlaceOrderInfo{
ClientOID: s.ClientOrderID,
ProductID: fPair.String(),
Side: s.Side.String(),
MarginType: s.MarginType.Upper(),
Leverage: s.Leverage,
OrderInfo: OrderInfo{
StopDirection: stopDir,
OrderType: s.Type,
TimeInForce: s.TimeInForce,
BaseAmount: s.Amount,
QuoteAmount: s.QuoteAmount,
LimitPrice: s.Price,
StopPrice: s.TriggerPrice,
PostOnly: s.TimeInForce.Is(order.PostOnly),
RFQDisabled: s.RFQDisabled,
EndTime: s.EndTime,
},
})
if err != nil {
return nil, err
}
subResp, err := s.DeriveSubmitResponse(resp.SuccessResponse.OrderID)
if err != nil {
return nil, err
}
if s.RetrieveFees {
time.Sleep(s.RetrieveFeeDelay)
feeResp, err := e.GetOrderByID(ctx, resp.SuccessResponse.OrderID, s.ClientOrderID, currency.Code{})
if err != nil {
return nil, err
}
subResp.Fee = feeResp.TotalFees.Float64()
}
return subResp, nil
}
// ModifyOrder will allow of changing orderbook placement and limit to market conversion
func (e *Exchange) ModifyOrder(ctx context.Context, m *order.Modify) (*order.ModifyResponse, error) {
if m == nil {
return nil, common.ErrNilPointer
}
if err := m.Validate(); err != nil {
return nil, err
}
success, err := e.EditOrder(ctx, m.OrderID, m.Amount, m.Price)
if err != nil {
return nil, err
}
if !success {
return nil, errOrderModFailNoRet
}
return m.DeriveModifyResponse()
}
// CancelOrder cancels an order by its corresponding ID number
func (e *Exchange) CancelOrder(ctx context.Context, o *order.Cancel) error {
if o == nil {
return common.ErrNilPointer
}
if err := o.Validate(o.StandardCancel()); err != nil {
return err
}
canSlice := []order.Cancel{*o}
resp, err := e.CancelBatchOrders(ctx, canSlice)
if err != nil {
return err
}
if resp.Status[o.OrderID] != order.Cancelled.String() {
return fmt.Errorf("%w %v", errOrderFailedToCancel, o.OrderID)
}
return nil
}
// CancelBatchOrders cancels orders by their corresponding ID numbers
func (e *Exchange) CancelBatchOrders(ctx context.Context, o []order.Cancel) (*order.CancelBatchResponse, error) {
var status order.CancelBatchResponse
ordToCancel := len(o)
if ordToCancel == 0 {
return nil, order.ErrOrderIDNotSet
}
status.Status = make(map[string]string)
ordIDSlice := make([]string, ordToCancel)
for i := range o {
if err := o[i].Validate(o[i].StandardCancel()); err != nil {
return nil, err
}
ordIDSlice[i] = o[i].OrderID
status.Status[o[i].OrderID] = "Failed to cancel"
}
resp := struct {
Results []OrderCancelDetail `json:"results"`
}{}
for i := 0; i < ordToCancel; i += 100 {
var tempOrdIDSlice []string
if ordToCancel-i < 100 {
tempOrdIDSlice = ordIDSlice[i:]
} else {
tempOrdIDSlice = ordIDSlice[i : i+100]
}
tempResp, err := e.CancelOrders(ctx, tempOrdIDSlice)
if err != nil {
return nil, err
}
resp.Results = append(resp.Results, tempResp...)
}
for i := range resp.Results {
if resp.Results[i].Success {
status.Status[resp.Results[i].OrderID] = order.Cancelled.String()
}
}
return &status, nil
}
// CancelAllOrders cancels all orders associated with a currency pair
func (e *Exchange) CancelAllOrders(context.Context, *order.Cancel) (order.CancelAllResponse, error) {
return order.CancelAllResponse{}, common.ErrFunctionNotSupported
}
// GetOrderInfo returns order information based on order ID
func (e *Exchange) GetOrderInfo(ctx context.Context, orderID string, pair currency.Pair, assetItem asset.Item) (*order.Detail, error) {
genOrderDetail, err := e.GetOrderByID(ctx, orderID, "", currency.Code{})
if err != nil {
return nil, err
}
response := e.getOrderRespToOrderDetail(genOrderDetail, pair, assetItem)
fillData, err := e.ListFills(ctx, []string{orderID}, nil, nil, 0, "", time.Time{}, time.Now(), defaultOrderFillCount)
if err != nil {
return nil, err
}
cursor := fillData.Cursor
for cursor != 0 {
tempFillData, err := e.ListFills(ctx, []string{orderID}, nil, nil, int64(cursor), "", time.Time{}, time.Now(), defaultOrderFillCount)
if err != nil {
return nil, err
}
fillData.Fills = append(fillData.Fills, tempFillData.Fills...)
cursor = tempFillData.Cursor
}
response.Trades = make([]order.TradeHistory, len(fillData.Fills))
var orderSide order.Side
switch response.Side {
case order.Buy:
orderSide = order.Sell
case order.Sell:
orderSide = order.Buy
}
for i := range fillData.Fills {
response.Trades[i] = order.TradeHistory{
Price: fillData.Fills[i].Price.Float64(),
Amount: fillData.Fills[i].Size.Float64(),
Fee: fillData.Fills[i].Commission.Float64(),
Exchange: e.GetName(),
TID: fillData.Fills[i].TradeID,
Side: orderSide,
Timestamp: fillData.Fills[i].TradeTime,
Total: fillData.Fills[i].Price.Float64() * fillData.Fills[i].Size.Float64(),
}
}
return response, nil
}
// GetDepositAddress returns a deposit address for a specified currency
func (e *Exchange) GetDepositAddress(ctx context.Context, cryptocurrency currency.Code, _, _ string) (*deposit.Address, error) {
allWalResp, err := e.GetAllWallets(ctx, PaginationInp{})
if err != nil {
return nil, err
}
var targetWalletID string
for i := range allWalResp.Data {
if allWalResp.Data[i].Currency.Code == cryptocurrency.String() {
targetWalletID = allWalResp.Data[i].ID
break
}
}
if targetWalletID == "" {
return nil, errNoWalletForCurrency
}
resp, err := e.GetAllAddresses(ctx, targetWalletID, PaginationInp{})
if err != nil || len(resp.Data) == 0 {
resp2, err2 := e.CreateAddress(ctx, targetWalletID, "")
if err2 != nil {
return nil, common.AppendError(err, err2)
}
return &deposit.Address{
Address: resp2.Address,
Tag: resp2.Name,
Chain: resp2.Network,
}, nil
}
return &deposit.Address{
Address: resp.Data[0].Address,
Tag: resp.Data[0].Name,
Chain: resp.Data[0].Network,
}, nil
}
// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is submitted
func (e *Exchange) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) {
if err := withdrawRequest.Validate(); err != nil {
return nil, err
}
if withdrawRequest.WalletID == "" {
return nil, errWalletIDEmpty
}
travel := &TravelRule{
BeneficiaryWalletType: withdrawRequest.Travel.BeneficiaryWalletType,
BeneficiaryName: withdrawRequest.Travel.BeneficiaryName,
BeneficiaryAddress: FullAddress{
Address1: withdrawRequest.Travel.BeneficiaryAddress.Address1,
Address2: withdrawRequest.Travel.BeneficiaryAddress.Address2,
Address3: withdrawRequest.Travel.BeneficiaryAddress.Address3,
City: withdrawRequest.Travel.BeneficiaryAddress.City,
State: withdrawRequest.Travel.BeneficiaryAddress.State,
Country: withdrawRequest.Travel.BeneficiaryAddress.Country,
PostalCode: withdrawRequest.Travel.BeneficiaryAddress.PostalCode,
},
BeneficiaryFinancialInstitution: withdrawRequest.Travel.BeneficiaryFinancialInstitution,
TransferPurpose: withdrawRequest.Travel.TransferPurpose,
}
if withdrawRequest.Travel.IsSelf {
travel.IsSelf = "IS_SELF_TRUE"
} else {
travel.IsSelf = "IS_SELF_FALSE"
}
resp, err := e.SendMoney(ctx, "send", withdrawRequest.WalletID, withdrawRequest.Crypto.Address, withdrawRequest.Description, withdrawRequest.IdempotencyToken, withdrawRequest.Crypto.AddressTag, "", withdrawRequest.Currency, withdrawRequest.Amount, false, travel)
if err != nil {
return nil, err
}
return &withdraw.ExchangeResponse{ID: resp.ID, Status: resp.Status}, nil
}
// WithdrawFiatFunds returns a withdrawal ID when a withdrawal is submitted
func (e *Exchange) WithdrawFiatFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) {
if err := withdrawRequest.Validate(); err != nil {
return nil, err
}
if withdrawRequest.WalletID == "" {
return nil, errWalletIDEmpty
}
paymentMethods, err := e.ListPaymentMethods(ctx)
if err != nil {
return nil, err
}
selectedWithdrawalMethod := PaymentMethodData{}
for i := range paymentMethods {
if withdrawRequest.Fiat.Bank.BankName == paymentMethods[i].Name {
selectedWithdrawalMethod = paymentMethods[i]
break
}
}
if selectedWithdrawalMethod.ID == "" {
return nil, fmt.Errorf("%w %v", errPayMethodNotFound, withdrawRequest.Fiat.Bank.BankName)
}
resp, err := e.FiatTransfer(ctx, withdrawRequest.WalletID, withdrawRequest.Currency.String(), selectedWithdrawalMethod.ID, withdrawRequest.Amount, true, FiatWithdrawal)
if err != nil {
return nil, err
}
return &withdraw.ExchangeResponse{
Name: selectedWithdrawalMethod.Name,
ID: resp.ID,
Status: resp.Status,
}, nil
}
// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is submitted
func (e *Exchange) WithdrawFiatFundsToInternationalBank(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) {
return e.WithdrawFiatFunds(ctx, withdrawRequest)
}
// GetFeeByType returns an estimate of fee based on type of transaction
func (e *Exchange) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
if feeBuilder == nil {
return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer)
}
if !e.AreCredentialsValid(ctx) && feeBuilder.FeeType == exchange.CryptocurrencyTradeFee {
feeBuilder.FeeType = exchange.OfflineTradeFee
}
return e.GetFee(ctx, feeBuilder)
}
// GetActiveOrders retrieves any orders that are active/open
func (e *Exchange) GetActiveOrders(ctx context.Context, req *order.MultiOrderRequest) (order.FilteredOrders, error) {
if req == nil {
return nil, common.ErrNilPointer
}
err := req.Validate()
if err != nil {
return nil, err
}
var respOrders []GetOrderResponse
if respOrders, err = e.iterativeGetAllOrders(ctx, req.Pairs, req.Type.String(), req.Side.String(), req.AssetType.Upper(), openStatus, 1000, req.StartTime, req.EndTime); err != nil {
return nil, err
}
orders := make([]order.Detail, len(respOrders))
for i := range respOrders {
orderRec := e.getOrderRespToOrderDetail(&respOrders[i], respOrders[i].ProductID, req.AssetType)
orders[i] = *orderRec
}
if len(req.Pairs) > 1 {
order.FilterOrdersByPairs(&orders, req.Pairs)
}
return req.Filter(e.Name, orders), nil
}
// GetOrderHistory retrieves account order information. Can Limit response to specific order status
func (e *Exchange) GetOrderHistory(ctx context.Context, req *order.MultiOrderRequest) (order.FilteredOrders, error) {
err := req.Validate()
if err != nil {
return nil, err
}
for i := range req.Pairs {
req.Pairs[i], err = e.FormatExchangeCurrency(req.Pairs[i], req.AssetType)
if err != nil {
return nil, err
}
}
var ord []GetOrderResponse
interOrd, err := e.iterativeGetAllOrders(ctx, req.Pairs, req.Type.String(), req.Side.String(), req.AssetType.Upper(), closedStatuses, defaultOrderCount, req.StartTime, req.EndTime)
if err != nil {
return nil, err
}
ord = append(ord, interOrd...)
if interOrd, err = e.iterativeGetAllOrders(ctx, req.Pairs, req.Type.String(), req.Side.String(), req.AssetType.Upper(), openStatus, defaultOrderCount, req.StartTime, req.EndTime); err != nil {
return nil, err
}
ord = append(ord, interOrd...)
orders := make([]order.Detail, len(ord))
for i := range ord {
singleOrder := e.getOrderRespToOrderDetail(&ord[i], ord[i].ProductID, req.AssetType)
orders[i] = *singleOrder
}
if len(req.Pairs) > 1 {
order.FilterOrdersByPairs(&orders, req.Pairs)
}
return req.Filter(e.Name, orders), nil
}
// GetHistoricCandles returns a set of candle between two time periods for a designated time period
func (e *Exchange) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
req, err := e.GetKlineRequest(pair, a, interval, start, end, false)
if err != nil {
return nil, err
}
timeSeries, err := e.GetHistoricKlines(ctx, req.RequestFormatted.String(), interval, start, end, false)
if err != nil {
return nil, err
}
return req.ProcessResponse(timeSeries)
}
// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (e *Exchange) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
req, err := e.GetKlineExtendedRequest(pair, a, interval, start, end)
if err != nil {
return nil, err
}
var timeSeries []kline.Candle
for x := range req.RangeHolder.Ranges {
hist, err := e.GetHistoricKlines(ctx, req.RequestFormatted.String(), interval, req.RangeHolder.Ranges[x].Start.Time.Add(-time.Nanosecond), req.RangeHolder.Ranges[x].End.Time.Add(-time.Nanosecond), false)
if err != nil {
return nil, err
}
timeSeries = append(timeSeries, hist...)
}
return req.ProcessResponse(timeSeries)
}
// ValidateAPICredentials validates current credentials used for wrapper functionality
func (e *Exchange) ValidateAPICredentials(ctx context.Context, assetType asset.Item) error {
_, err := e.UpdateAccountInfo(ctx, assetType)
return e.CheckTransientError(err)
}
// GetServerTime returns the current exchange server time.
func (e *Exchange) GetServerTime(ctx context.Context, _ asset.Item) (time.Time, error) {
st, err := e.GetV3Time(ctx)
if err != nil {
return time.Time{}, err
}
return st.Iso, nil
}
// GetLatestFundingRates returns the latest funding rates data
func (e *Exchange) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) {
if r == nil {
return nil, common.ErrNilPointer
}
if !e.SupportsAsset(r.Asset) {
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, r.Asset)
}
products, perpStart, err := e.fetchFutures(ctx)
if err != nil {
return nil, err
}
funding := make([]fundingrate.LatestRateResponse, len(products.Products))
for i := perpStart; i < len(products.Products); i++ {
funRate := fundingrate.Rate{
Time: products.Products[i].FutureProductDetails.PerpetualDetails.FundingTime,
Rate: decimal.NewFromFloat(products.Products[i].FutureProductDetails.PerpetualDetails.FundingRate.Float64()),
}
funding[i] = fundingrate.LatestRateResponse{
Exchange: e.Name,
Asset: r.Asset,
Pair: products.Products[i].ID,
LatestRate: funRate,
TimeChecked: time.Now(),
}
}
return funding, nil
}
// GetFuturesContractDetails returns all contracts from the exchange by asset type
func (e *Exchange) GetFuturesContractDetails(ctx context.Context, item asset.Item) ([]futures.Contract, error) {
if !item.IsFutures() {
return nil, futures.ErrNotFuturesAsset
}
if !e.SupportsAsset(item) {
return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, item)
}
products, perpStart, err := e.fetchFutures(ctx)
if err != nil {
return nil, err
}
contracts := make([]futures.Contract, len(products.Products))
for i := range products.Products {
funRate := fundingrate.Rate{
Time: products.Products[i].FutureProductDetails.PerpetualDetails.FundingTime,
Rate: decimal.NewFromFloat(products.Products[i].FutureProductDetails.PerpetualDetails.FundingRate.Float64()),
}
contracts[i] = futures.Contract{
Exchange: e.Name,
Name: products.Products[i].ID,
Asset: item,
EndDate: products.Products[i].FutureProductDetails.ContractExpiry,
IsActive: !products.Products[i].IsDisabled,
Status: products.Products[i].Status,
SettlementCurrencies: currency.Currencies{products.Products[i].QuoteCurrencyID},
Multiplier: products.Products[i].BaseIncrement.Float64(),
LatestRate: funRate,
}
if i < perpStart {
contracts[i].Type = futures.LongDated
} else {
contracts[i].Type = futures.Perpetual
}
}
return contracts, nil
}
// UpdateOrderExecutionLimits updates order execution limits
func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error {
if !e.SupportsAsset(a) {
return fmt.Errorf("%w %q", asset.ErrNotSupported, a)
}
aString := FormatAssetOutbound(a)
data, err := e.GetAllProducts(ctx, 0, 0, aString, "", "", "", nil, false, true, false)
if err != nil {
return err
}
lim := make([]limits.MinMaxLevel, len(data.Products))
for i := range data.Products {
lim[i] = limits.MinMaxLevel{
Key: key.NewExchangeAssetPair(e.Name, a, data.Products[i].ID),
MinPrice: data.Products[i].QuoteMinSize.Float64(),
MaxPrice: data.Products[i].QuoteMaxSize.Float64(),
PriceStepIncrementSize: data.Products[i].PriceIncrement.Float64(),
MinimumBaseAmount: data.Products[i].BaseMinSize.Float64(),
MaximumBaseAmount: data.Products[i].BaseMaxSize.Float64(),
MinimumQuoteAmount: data.Products[i].QuoteMinSize.Float64(),
MaximumQuoteAmount: data.Products[i].QuoteMaxSize.Float64(),
AmountStepIncrementSize: data.Products[i].BaseIncrement.Float64(),
QuoteStepIncrementSize: data.Products[i].QuoteIncrement.Float64(),
MaxTotalOrders: 1000,
}
}
return limits.Load(lim)
}
// GetCurrencyTradeURL returns the URL to the exchange's trade page for the given asset and currency pair
func (e *Exchange) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp currency.Pair) (string, error) {
if _, err := e.CurrencyPairs.IsPairEnabled(cp, a); err != nil {
return "", err
}
cp.Delimiter = currency.DashDelimiter
return tradeBaseURL + cp.Upper().String(), nil
}
// fetchFutures is a helper function for GetLatestFundingRates and GetFuturesContractDetails that calls the List Products endpoint twice, to get both expiring futures and perpetual futures
func (e *Exchange) fetchFutures(ctx context.Context) (*AllProducts, int, error) {
products, err := e.GetAllProducts(ctx, 0, 0, "FUTURE", "", "", "", nil, false, false, false)
if err != nil {
return nil, 0, err
}
products2, err := e.GetAllProducts(ctx, 0, 0, "FUTURE", "PERPETUAL", "", "", nil, false, false, false)
if err != nil {
return nil, 0, err
}
perpStart := len(products.Products)
products.Products = append(products.Products, products2.Products...)
return products, perpStart, nil
}
// processFundingData is a helper function for GetAccountFundingHistory and GetWithdrawalsHistory, transforming the data returned by the Coinbase API into a format suitable for the exchange package
func (e *Exchange) processFundingData(accHistory []DeposWithdrData, cryptoHistory []TransactionData) ([]exchange.FundingHistory, error) {
fundingData := make([]exchange.FundingHistory, len(accHistory)+len(cryptoHistory))
for i := range accHistory {
fundingData[i] = exchange.FundingHistory{
ExchangeName: e.Name,
Status: accHistory[i].Status,
TransferID: accHistory[i].ID,
Timestamp: accHistory[i].PayoutAt,
Currency: accHistory[i].Amount.Currency.String(),
Amount: accHistory[i].Amount.Value.Float64(),
Fee: accHistory[i].TotalFee.Amount.Value.Float64(),
}
switch accHistory[i].Type {
case "TRANSFER_TYPE_DEPOSIT":
fundingData[i].TransferType = "deposit"
case "TRANSFER_TYPE_WITHDRAWAL":
fundingData[i].TransferType = "withdrawal"
default:
return nil, fmt.Errorf("%w %v", errUnknownTransferType, accHistory[i].Type)
}
}
for i := range cryptoHistory {
fundingData[i+len(accHistory)] = exchange.FundingHistory{
ExchangeName: e.Name,
Status: cryptoHistory[i].Status,
TransferID: cryptoHistory[i].ID,
Timestamp: cryptoHistory[i].CreatedAt,
Currency: cryptoHistory[i].Amount.Currency,
Amount: cryptoHistory[i].Amount.Amount.Float64(),
}
if cryptoHistory[i].Type == "receive" {
fundingData[i+len(accHistory)].TransferType = "deposit"
}
if cryptoHistory[i].Type == "send" {
fundingData[i+len(accHistory)].TransferType = "withdrawal"
}
}
return fundingData, nil
}
// iterativeGetAllOrders is a helper function used in GetActiveOrders and GetOrderHistory to repeatedly call GetAllOrders until all orders have been retrieved
func (e *Exchange) iterativeGetAllOrders(ctx context.Context, productIDs currency.Pairs, orderType, orderSide, productType string, orderStatus []string, limit int32, startDate, endDate time.Time) ([]GetOrderResponse, error) {
hasNext := true
var resp []GetOrderResponse
var cursor int64
if orderSide == "ANY" {
orderSide = ""
}
if orderType == "ANY" {
orderType = ""
}
if productType == "FUTURES" {
productType = "FUTURE"
}
orderTypeSlice := []string{orderType}
if orderType == "" {
orderTypeSlice = nil
}
for hasNext {
interResp, err := e.ListOrders(ctx, &ListOrdersReq{
OrderStatus: orderStatus,
OrderTypes: orderTypeSlice,
ProductIDs: productIDs,
ProductType: productType,
OrderSide: orderSide,
Cursor: cursor,
Limit: limit,
StartDate: startDate,
EndDate: endDate,
})
if err != nil {
return nil, err
}
resp = append(resp, interResp.Orders...)
hasNext = interResp.HasNext
cursor = int64(interResp.Cursor)
}
return resp, nil
}
// getOrderRespToOrderDetail is a helper function used in GetOrderInfo, GetActiveOrders, and GetOrderHistory to convert data returned by the Coinbase API into a format suitable for the exchange package
func (e *Exchange) getOrderRespToOrderDetail(genOrderDetail *GetOrderResponse, pair currency.Pair, assetItem asset.Item) *order.Detail {
var amount float64
var quoteAmount float64
var orderType order.Type
if genOrderDetail.OrderConfiguration.MarketMarketIOC != nil {
quoteAmount = genOrderDetail.OrderConfiguration.MarketMarketIOC.QuoteSize.Float64()
amount = genOrderDetail.OrderConfiguration.MarketMarketIOC.BaseSize.Float64()
orderType = order.Market
}
var price float64
var postOnly bool
if genOrderDetail.OrderConfiguration.LimitLimitGTC != nil {
amount = genOrderDetail.OrderConfiguration.LimitLimitGTC.BaseSize.Float64()
price = genOrderDetail.OrderConfiguration.LimitLimitGTC.LimitPrice.Float64()
postOnly = genOrderDetail.OrderConfiguration.LimitLimitGTC.PostOnly
orderType = order.Limit
}
if genOrderDetail.OrderConfiguration.LimitLimitGTD != nil {
amount = genOrderDetail.OrderConfiguration.LimitLimitGTD.BaseSize.Float64()
price = genOrderDetail.OrderConfiguration.LimitLimitGTD.LimitPrice.Float64()
postOnly = genOrderDetail.OrderConfiguration.LimitLimitGTD.PostOnly
orderType = order.Limit
}
var triggerPrice float64
if genOrderDetail.OrderConfiguration.StopLimitStopLimitGTC != nil {
amount = genOrderDetail.OrderConfiguration.StopLimitStopLimitGTC.BaseSize.Float64()
price = genOrderDetail.OrderConfiguration.StopLimitStopLimitGTC.LimitPrice.Float64()
triggerPrice = genOrderDetail.OrderConfiguration.StopLimitStopLimitGTC.StopPrice.Float64()
orderType = order.StopLimit
}
if genOrderDetail.OrderConfiguration.StopLimitStopLimitGTD != nil {
amount = genOrderDetail.OrderConfiguration.StopLimitStopLimitGTD.BaseSize.Float64()
price = genOrderDetail.OrderConfiguration.StopLimitStopLimitGTD.LimitPrice.Float64()
triggerPrice = genOrderDetail.OrderConfiguration.StopLimitStopLimitGTD.StopPrice.Float64()
orderType = order.StopLimit
}
var remainingAmount float64
if !genOrderDetail.SizeInQuote {
remainingAmount = amount - genOrderDetail.FilledSize.Float64()
}
var orderSide order.Side
switch genOrderDetail.Side {
case order.Buy.String():
orderSide = order.Buy
case order.Sell.String():
orderSide = order.Sell
}
var orderStatus order.Status
switch genOrderDetail.Status {
case order.Open.String():
orderStatus = order.Open
case order.Filled.String():
orderStatus = order.Filled
case order.Cancelled.String():
orderStatus = order.Cancelled
case order.Expired.String():
orderStatus = order.Expired
case "FAILED":
orderStatus = order.Rejected
case "UNKNOWN_ORDER_STATUS":
orderStatus = order.UnknownStatus
}
var closeTime time.Time
if genOrderDetail.Settled {
closeTime = genOrderDetail.LastFillTime
}
var lastUpdateTime time.Time
if len(genOrderDetail.EditHistory) > 0 {
lastUpdateTime = genOrderDetail.EditHistory[len(genOrderDetail.EditHistory)-1].ReplaceAcceptTimestamp
}
var tif order.TimeInForce
if postOnly {
tif = order.PostOnly
}
if genOrderDetail.OrderConfiguration.MarketMarketIOC != nil {
tif |= order.ImmediateOrCancel
}
response := order.Detail{
TimeInForce: tif,
Price: price,
Amount: amount,
TriggerPrice: triggerPrice,
AverageExecutedPrice: genOrderDetail.AverageFilledPrice.Float64(),
QuoteAmount: quoteAmount,
ExecutedAmount: genOrderDetail.FilledSize.Float64(),
RemainingAmount: remainingAmount,
Cost: genOrderDetail.TotalValueAfterFees.Float64(),
Fee: genOrderDetail.TotalFees.Float64(),
Exchange: e.GetName(),
OrderID: genOrderDetail.OrderID,
ClientOrderID: genOrderDetail.ClientOID,
ClientID: genOrderDetail.UserID,
Type: orderType,
Side: orderSide,
Status: orderStatus,
AssetType: assetItem,
Date: genOrderDetail.CreatedTime,
CloseTime: closeTime,
LastUpdated: lastUpdateTime,
Pair: pair,
}
return &response
}
// tickerHelper fetches the ticker for a given currency pair, used by UpdateTicker
func (e *Exchange) tickerHelper(ctx context.Context, name currency.Pair, assetType asset.Item) error {
newTick := &ticker.Price{
Pair: name,
ExchangeName: e.Name,
AssetType: assetType,
}
ticks, err := e.GetTicker(ctx, name, 1, time.Time{}, time.Time{}, false)
if err != nil {
return err
}
var last float64
if len(ticks.Trades) != 0 {
last = ticks.Trades[0].Price.Float64()
}
newTick.Last = last
newTick.Bid = ticks.BestBid.Float64()
newTick.Ask = ticks.BestAsk.Float64()
return ticker.ProcessTicker(newTick)
}
// FormatAssetOutbound formats asset items for outbound requests
func FormatAssetOutbound(a asset.Item) string {
if a == asset.Futures {
return "FUTURE"
}
return a.Upper()
}
// GetAlias returns the aliases for a currency pair
func (a *pairAliases) GetAlias(p currency.Pair) currency.Pairs {
a.m.RLock()
defer a.m.RUnlock()
return slices.Clone(a.associatedAliases[p])
}
// GetAliases returns a map of all aliases associated with all pairs
func (a *pairAliases) GetAliases() map[currency.Pair]currency.Pairs {
a.m.RLock()
defer a.m.RUnlock()
return maps.Clone(a.associatedAliases)
}
// Load adds a batch of aliases to the alias map
func (a *pairAliases) Load(aliases map[currency.Pair]currency.Pairs) {
a.m.Lock()
defer a.m.Unlock()
if a.associatedAliases == nil {
a.associatedAliases = make(map[currency.Pair]currency.Pairs)
}
for k, v := range aliases {
a.associatedAliases[k] = a.associatedAliases[k].Add(v...)
}
}