exchanges: Refactor time handling and other minor improvements (#1948)

* exchanges: Refactor time handling and other minor improvements

- Updated Kraken wrapper to utilise new time handling methods.
- Simplified Kucoin types by removing unnecessary structures and using direct JSON unmarshalling.
- Improved websocket handling in Kucoin to directly parse candlestick data.
- Modified Lbank types to use the new time representation.
- Adjusted Poloniex wrapper and types to utilise the new time handling.
- Updated Yobit types and wrapper to reflect changes in time representation.
- Introduced DateTime type for better handling of specific time formats.
- Added tests for DateTime unmarshalling to ensure correctness.
- Rid UTC().Unix and UTC().UnixMilli as it's not needed
- Correct Huobi timestamp usage for some endpoints.
- Rid RFC3339 time parsing since Go does that automatically.

* exchanges: Refactor JSON unmarshalling for various types and improve test coverage

* linter: Update error message in TestGetKlines

* refactor: Simplify JSON unmarshalling in MovementHistory and improve test assertions in GetKlines

* refactor: Improve JSON unmarshalling for channel name and clarify comment in wsProcessOpenOrders

* refactor: Update time handling in Huobi types to use types.Time for createdAt fields and relax GetLiquidationOrders test

* refactor: Move wsTicker, wsSpread, wsTrades, and wsCandle types to kraken_types.go for better organistion

* refactor: Add validation for underlying parameter in GetExpirationTime and update tests
This commit is contained in:
Adrian Gallagher
2025-07-01 09:11:55 +10:00
committed by GitHub
parent 48a66c9faa
commit 3cc9a2b9e0
92 changed files with 2488 additions and 3276 deletions

View File

@@ -750,18 +750,16 @@ func tickerFromFundingResp(symbol string, respAny []any) (*Ticker, error) {
// timestampStart is a millisecond timestamp
// timestampEnd is a millisecond timestamp
// reOrderResp reorders the returned data.
func (b *Bitfinex) GetTrades(ctx context.Context, currencyPair string, limit, timestampStart, timestampEnd int64, reOrderResp bool) ([]Trade, error) {
func (b *Bitfinex) GetTrades(ctx context.Context, currencyPair string, limit uint64, start, end time.Time, reOrderResp bool) ([]Trade, error) {
v := url.Values{}
if limit > 0 {
v.Set("limit", strconv.FormatInt(limit, 10))
v.Set("limit", strconv.FormatUint(limit, 10))
}
if timestampStart > 0 {
v.Set("start", strconv.FormatInt(timestampStart, 10))
if !start.IsZero() {
v.Set("start", strconv.FormatInt(start.UnixMilli(), 10))
}
if timestampEnd > 0 {
v.Set("end", strconv.FormatInt(timestampEnd, 10))
if !end.IsZero() {
v.Set("end", strconv.FormatInt(end.UnixMilli(), 10))
}
sortVal := "0"
if reOrderResp {
@@ -769,72 +767,9 @@ func (b *Bitfinex) GetTrades(ctx context.Context, currencyPair string, limit, ti
}
v.Set("sort", sortVal)
path := bitfinexAPIVersion2 + bitfinexTrades + currencyPair + "/hist" + "?" + v.Encode()
var resp [][]any
err := b.SendHTTPRequest(ctx, exchange.RestSpot, path, &resp, tradeRateLimit)
if err != nil {
return nil, err
}
history := make([]Trade, len(resp))
for i := range resp {
amount, ok := resp[i][2].(float64)
if !ok {
return nil, errors.New("unable to type assert amount")
}
side := order.Buy.String()
if amount < 0 {
side = order.Sell.String()
amount *= -1
}
tid, ok := resp[i][0].(float64)
if !ok {
return nil, errors.New("unable to type assert trade ID")
}
timestamp, ok := resp[i][1].(float64)
if !ok {
return nil, errors.New("unable to type assert timestamp")
}
if len(resp[i]) > 4 {
var rate float64
rate, ok = resp[i][3].(float64)
if !ok {
return nil, errors.New("unable to type assert rate")
}
var period float64
period, ok = resp[i][4].(float64)
if !ok {
return nil, errors.New("unable to type assert period")
}
history[i] = Trade{
TID: int64(tid),
Timestamp: int64(timestamp),
Amount: amount,
Rate: rate,
Period: int64(period),
Type: side,
}
continue
}
price, ok := resp[i][3].(float64)
if !ok {
return nil, errors.New("unable to type assert price")
}
history[i] = Trade{
TID: int64(tid),
Timestamp: int64(timestamp),
Amount: amount,
Price: price,
Type: side,
}
}
return history, nil
path := common.EncodeURLValues(bitfinexAPIVersion2+bitfinexTrades+currencyPair+"/hist", v)
var resp []Trade
return resp, b.SendHTTPRequest(ctx, exchange.RestSpot, path, &resp, tradeRateLimit)
}
// GetOrderbook retrieves the orderbook bid and ask price points for a currency
@@ -997,108 +932,39 @@ func (b *Bitfinex) GetLends(ctx context.Context, symbol string, values url.Value
// GetCandles returns candle chart data
// timeFrame values: '1m', '5m', '15m', '30m', '1h', '3h', '6h', '12h', '1D', '1W', '14D', '1M'
// section values: last or hist
func (b *Bitfinex) GetCandles(ctx context.Context, symbol, timeFrame string, start, end int64, limit uint64, historic bool) ([]Candle, error) {
func (b *Bitfinex) GetCandles(ctx context.Context, symbol, timeFrame string, start, end time.Time, limit uint64, historic bool) ([]Candle, error) {
var fundingPeriod string
if symbol[0] == 'f' {
fundingPeriod = ":p30"
}
path := bitfinexAPIVersion2 +
bitfinexCandles +
":" +
timeFrame +
":" +
symbol +
fundingPeriod
path := bitfinexAPIVersion2 + bitfinexCandles + ":" + timeFrame + ":" + symbol + fundingPeriod
if historic {
v := url.Values{}
if start > 0 {
v.Set("start", strconv.FormatInt(start, 10))
if !start.IsZero() {
v.Set("start", strconv.FormatInt(start.UnixMilli(), 10))
}
if end > 0 {
v.Set("end", strconv.FormatInt(end, 10))
if !end.IsZero() {
v.Set("end", strconv.FormatInt(end.UnixMilli(), 10))
}
if limit > 0 {
v.Set("limit", strconv.FormatUint(limit, 10))
}
path += "/hist"
if len(v) > 0 {
path += "?" + v.Encode()
}
var response [][]any
err := b.SendHTTPRequest(ctx, exchange.RestSpot, path, &response, candle)
if err != nil {
return nil, err
}
candles := make([]Candle, len(response))
for i := range response {
var c Candle
timestamp, ok := response[i][0].(float64)
if !ok {
return nil, errors.New("unable to type assert timestamp")
}
c.Timestamp = time.UnixMilli(int64(timestamp))
if c.Open, ok = response[i][1].(float64); !ok {
return nil, errors.New("unable to type assert open")
}
if c.Close, ok = response[i][2].(float64); !ok {
return nil, errors.New("unable to type assert close")
}
if c.High, ok = response[i][3].(float64); !ok {
return nil, errors.New("unable to type assert high")
}
if c.Low, ok = response[i][4].(float64); !ok {
return nil, errors.New("unable to type assert low")
}
if c.Volume, ok = response[i][5].(float64); !ok {
return nil, errors.New("unable to type assert volume")
}
candles[i] = c
}
return candles, nil
var response []Candle
return response, b.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues(path+"/hist", v), &response, candle)
}
path += "/last"
var response []any
err := b.SendHTTPRequest(ctx, exchange.RestSpot, path, &response, candle)
var c Candle
err := b.SendHTTPRequest(ctx, exchange.RestSpot, path, &c, candle)
if err != nil {
return nil, err
}
if len(response) == 0 {
return nil, errors.New("no data returned")
}
var c Candle
timestamp, ok := response[0].(float64)
if !ok {
return nil, errors.New("unable to type assert timestamp")
}
c.Timestamp = time.UnixMilli(int64(timestamp))
if c.Open, ok = response[1].(float64); !ok {
return nil, errors.New("unable to type assert open")
}
if c.Close, ok = response[2].(float64); !ok {
return nil, errors.New("unable to type assert close")
}
if c.High, ok = response[3].(float64); !ok {
return nil, errors.New("unable to type assert high")
}
if c.Low, ok = response[4].(float64); !ok {
return nil, errors.New("unable to type assert low")
}
if c.Volume, ok = response[5].(float64); !ok {
return nil, errors.New("unable to type assert volume")
}
return []Candle{c}, nil
}
@@ -1831,7 +1697,6 @@ func (b *Bitfinex) GetBalanceHistory(ctx context.Context, symbol string, timeSin
// GetMovementHistory returns an array of past deposits and withdrawals
func (b *Bitfinex) GetMovementHistory(ctx context.Context, symbol, method string, timeSince, timeUntil time.Time, limit int) ([]MovementHistory, error) {
var response [][]any
req := make(map[string]any)
req["currency"] = symbol
@@ -1839,89 +1704,18 @@ func (b *Bitfinex) GetMovementHistory(ctx context.Context, symbol, method string
req["method"] = method
}
if !timeSince.IsZero() {
req["since"] = timeSince
req["since"] = timeSince.UnixMilli()
}
if !timeUntil.IsZero() {
req["until"] = timeUntil
req["until"] = timeUntil.UnixMilli()
}
if limit > 0 {
req["limit"] = limit
}
err := b.SendAuthenticatedHTTPRequestV2(ctx, exchange.RestSpot, http.MethodPost,
"auth/r/"+bitfinexHistoryMovements+"/"+symbol+"/"+bitfinexHistoryShort,
req,
&response,
orderMulti)
if err != nil {
return nil, err
}
var resp []MovementHistory //nolint:prealloc // its an array in an array
var ok bool
for i := range response {
var move MovementHistory
for j := range response[i] {
if response[i][j] == nil {
continue
}
switch j {
case 0:
var id float64
id, ok = response[i][j].(float64)
if !ok {
return nil, common.GetTypeAssertError("float64", response[i][j], "Movements.Id")
}
move.ID = int64(id)
case 1:
move.Currency, ok = response[i][j].(string)
if !ok {
return nil, common.GetTypeAssertError("string", response[i][j], "Movements.Currency")
}
case 5:
move.TimestampCreated, ok = response[i][j].(float64)
if !ok {
return nil, common.GetTypeAssertError("float64", response[i][j], "Movements.MovementStartedAt")
}
case 6:
move.Timestamp, ok = response[i][j].(float64)
if !ok {
return nil, common.GetTypeAssertError("float64", response[i][j], "Movements.MovementLastUpdated")
}
case 9:
move.Status, ok = response[i][j].(string)
if !ok {
return nil, common.GetTypeAssertError("string", response[i][j], "Movements.CurrentStatus")
}
case 12:
move.Amount, ok = response[i][j].(float64)
if !ok {
return nil, common.GetTypeAssertError("float64", response[i][j], "Movements.AmountOfFundsMoved")
}
case 13:
move.Fee, ok = response[i][j].(float64)
if !ok {
return nil, common.GetTypeAssertError("float64", response[i][j], "Movements.FeesApplied")
}
case 16:
move.Address, ok = response[i][j].(string)
if !ok {
return nil, common.GetTypeAssertError("string", response[i][j], "Movements.DestinationAddress")
}
case 20:
move.TxID, ok = response[i][j].(string)
if !ok {
return nil, common.GetTypeAssertError("string", response[i][j], "Movements.TransactionId")
}
case 21:
move.Description, ok = response[i][j].(string)
if !ok {
return nil, common.GetTypeAssertError("string", response[i][j], "Movements.WithdrawTransactionNote")
}
}
}
resp = append(resp, move)
}
return resp, nil
var resp []MovementHistory
path := bitfinexV2Auth + "r/" + bitfinexHistoryMovements + "/" + symbol + "/" + bitfinexHistoryShort
return resp, b.SendAuthenticatedHTTPRequestV2(ctx, exchange.RestSpot, http.MethodPost, path, req, &resp, orderMulti)
}
// GetTradeHistory returns past executed trades
@@ -2232,18 +2026,12 @@ func getOfflineTradeFee(price, amount float64) float64 {
}
// GetCryptocurrencyWithdrawalFee returns an estimate of fee based on type of transaction
func (b *Bitfinex) GetCryptocurrencyWithdrawalFee(c currency.Code, accountFees AccountFees) (fee float64, err error) {
switch result := accountFees.Withdraw[c.String()].(type) {
case string:
fee, err = strconv.ParseFloat(result, 64)
if err != nil {
return 0, err
}
case float64:
fee = result
func (b *Bitfinex) GetCryptocurrencyWithdrawalFee(c currency.Code, accountFees AccountFees) (float64, error) {
fee, ok := accountFees.Withdraw[c.String()]
if !ok {
return 0, fmt.Errorf("withdrawal fee for %s not found", c.String())
}
return fee, nil
return fee.Float64(), nil
}
func getInternationalBankDepositFee(amount float64) float64 {

View File

@@ -27,6 +27,7 @@ import (
testexch "github.com/thrasher-corp/gocryptotrader/internal/testing/exchange"
testsubs "github.com/thrasher-corp/gocryptotrader/internal/testing/subscriptions"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
"github.com/thrasher-corp/gocryptotrader/types"
)
// Please supply API keys here or in config/testdata.json to test authenticated endpoints
@@ -308,10 +309,9 @@ func checkFundingTick(tb testing.TB, tick *Ticker) {
func TestGetTrades(t *testing.T) {
t.Parallel()
_, err := b.GetTrades(t.Context(), "tBTCUSD", 5, 0, 0, false)
if err != nil {
t.Error(err)
}
r, err := b.GetTrades(t.Context(), "tBTCUSD", 5, time.Time{}, time.Time{}, false)
require.NoError(t, err, "GetTrades must not error")
assert.NotEmpty(t, r, "GetTrades should return some trades")
}
func TestGetOrderbook(t *testing.T) {
@@ -368,12 +368,9 @@ func TestGetLends(t *testing.T) {
func TestGetCandles(t *testing.T) {
t.Parallel()
e := time.Now().Add(-time.Hour * 2).Truncate(time.Hour)
s := e.Add(-time.Hour * 4)
_, err := b.GetCandles(t.Context(), "fUST", "1D", s.UnixMilli(), e.UnixMilli(), 10000, true)
if err != nil {
t.Fatal(err)
}
c, err := b.GetCandles(t.Context(), "fUST", "1D", time.Now().AddDate(0, 0, -1), time.Now(), 10000, true)
require.NoError(t, err, "GetCandles must not error")
assert.NotEmpty(t, c, "GetCandles should return some candles")
}
func TestGetLeaderboard(t *testing.T) {
@@ -670,14 +667,45 @@ func TestGetMovementHistory(t *testing.T) {
}
}
func TestGetTradeHistory(t *testing.T) {
func TestMovementHistoryUnmarshalJSON(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, b)
_, err := b.GetTradeHistory(t.Context(),
"BTCUSD", time.Time{}, time.Time{}, 1, 0)
if err != nil {
t.Error(err)
deposit := []byte(`[13105603,"ETH","ETHEREUM",null,null,1569348774000,1569348774000,null,null,"COMPLETED",null,null,0.26300954,-0.00135,null,null,"DESTINATION_ADDRESS",null,null,null,"TRANSACTION_ID",null]`)
var result MovementHistory
require.NoError(t, json.Unmarshal(deposit, &result))
stringPtr := func(s string) *string {
return &s
}
exp := MovementHistory{
ID: 13105603,
Currency: "ETH",
CurrencyName: "ETHEREUM",
MTSStarted: types.Time(time.Unix(1569348774, 0)),
MTSUpdated: types.Time(time.Unix(1569348774, 0)),
Status: "COMPLETED",
Amount: 0.26300954,
Fees: -0.00135,
DestinationAddress: "DESTINATION_ADDRESS",
TransactionID: stringPtr("TRANSACTION_ID"),
TransactionType: "deposit",
}
assert.Equal(t, exp, result, "MovementHistory should unmarshal correctly")
withdrawal := []byte(`[13293039,"ETH","ETHEREUM",null,null,1574175052000,1574181326000,null,null,"CANCELED",null,null,-0.24,-0.00135,null,null,"DESTINATION_ADDRESS",null,null,null,"TRANSACTION_ID","Purchase of 100 pizzas"]`)
require.NoError(t, json.Unmarshal(withdrawal, &result))
exp = MovementHistory{
ID: 13293039,
Currency: "ETH",
CurrencyName: "ETHEREUM",
MTSStarted: types.Time(time.Unix(1574175052, 0)),
MTSUpdated: types.Time(time.Unix(1574181326, 0)),
Status: "CANCELED",
Amount: -0.24,
Fees: -0.00135,
DestinationAddress: "DESTINATION_ADDRESS",
TransactionID: stringPtr("TRANSACTION_ID"),
TransactionNote: stringPtr("Purchase of 100 pizzas"),
TransactionType: "withdrawal",
}
assert.Equal(t, exp, result, "MovementHistory should unmarshal correctly")
}
func TestNewOffer(t *testing.T) {

View File

@@ -2,6 +2,7 @@ package bitfinex
import (
"errors"
"math"
"sync"
"time"
@@ -197,17 +198,31 @@ type Orderbook struct {
// Trade holds resp information
type Trade struct {
Timestamp int64
TID int64
Price float64
Timestamp types.Time
Amount float64
Exchange string
Price float64
Rate float64
Period int64
Type string
Period int64 // Funding offer period in days
Side order.Side
}
// UnmarshalJSON unmarshals JSON data into a Trade struct
func (t *Trade) UnmarshalJSON(data []byte) error {
if err := json.Unmarshal(data, &[5]any{&t.TID, &t.Timestamp, &t.Amount, &t.Rate, &t.Period}); err != nil {
return err
}
if t.Period == 0 {
t.Price, t.Rate = t.Rate, 0
}
t.Side = order.Buy
if t.Amount < 0 {
t.Amount = math.Abs(t.Amount)
t.Side = order.Sell
}
return nil
}
// Lendbook holds most recent funding data for a relevant currency
type Lendbook struct {
Bids []Book `json:"bids"`
@@ -216,19 +231,19 @@ type Lendbook struct {
// FundingBookItem is a generalised sub-type to hold book information
type FundingBookItem struct {
Rate float64 `json:"rate,string"`
Amount float64 `json:"amount,string"`
Period int `json:"period"`
Timestamp string `json:"timestamp"`
FlashReturnRate string `json:"frr"`
Rate float64 `json:"rate,string"`
Amount float64 `json:"amount,string"`
Period int `json:"period"`
Timestamp types.Time `json:"timestamp"`
FlashReturnRate string `json:"frr"`
}
// Lends holds the lent information by currency
type Lends struct {
Rate float64 `json:"rate,string"`
AmountLent float64 `json:"amount_lent,string"`
AmountUsed float64 `json:"amount_used,string"`
Timestamp int64 `json:"timestamp"`
Rate float64 `json:"rate,string"`
AmountLent float64 `json:"amount_lent,string"`
AmountUsed float64 `json:"amount_used,string"`
Timestamp types.Time `json:"timestamp"`
}
// AccountInfoFull adds the error message to Account info
@@ -254,7 +269,7 @@ type AccountInfoFees struct {
// AccountFees stores withdrawal account fee data from Bitfinex
type AccountFees struct {
Withdraw map[string]any `json:"withdraw"`
Withdraw map[string]types.Number `json:"withdraw"`
}
// AccountSummary holds account summary data
@@ -395,80 +410,120 @@ type GenericResponse struct {
// Position holds position information
type Position struct {
ID int64 `json:"id"`
Symbol string `json:"string"`
Status string `json:"active"`
Base float64 `json:"base,string"`
Amount float64 `json:"amount,string"`
Timestamp string `json:"timestamp"`
Swap float64 `json:"swap,string"`
PL float64 `json:"pl,string"`
ID int64 `json:"id"`
Symbol string `json:"string"`
Status string `json:"active"`
Base float64 `json:"base,string"`
Amount float64 `json:"amount,string"`
Timestamp types.Time `json:"timestamp"`
Swap float64 `json:"swap,string"`
PL float64 `json:"pl,string"`
}
// BalanceHistory holds balance history information
type BalanceHistory struct {
Currency string `json:"currency"`
Amount float64 `json:"amount,string"`
Balance float64 `json:"balance,string"`
Description string `json:"description"`
Timestamp string `json:"timestamp"`
Currency string `json:"currency"`
Amount float64 `json:"amount,string"`
Balance float64 `json:"balance,string"`
Description string `json:"description"`
Timestamp types.Time `json:"timestamp"`
}
// MovementHistory holds deposit and withdrawal history data
type MovementHistory struct {
ID int64 `json:"id"`
TxID string `json:"txid"`
Currency string `json:"currency"`
Method string `json:"method"`
Type string `json:"withdrawal"`
Amount float64 `json:"amount,string"`
Description string `json:"description"`
Address string `json:"address"`
Status string `json:"status"`
Timestamp float64 `json:"timestamp"`
TimestampCreated float64 `json:"timestamp_created"`
Fee float64 `json:"fee"`
ID int64
Currency string
CurrencyName string // AKA Method
TXID string
MTSStarted types.Time
MTSUpdated types.Time
Status string
Amount types.Number // Positive for deposits, negative for withdrawals
Fees types.Number
DestinationAddress string
PaymentID *string
TransactionID *string
TransactionNote *string
TransactionType string // "deposit" or "withdrawal"
}
// UnmarshalJSON unmarshals JSON data into a MovementHistory struct
func (m *MovementHistory) UnmarshalJSON(data []byte) error {
var unusedField any
if err := json.Unmarshal(data, &[22]any{
&m.ID,
&m.Currency,
&m.CurrencyName,
&unusedField,
&unusedField,
&m.MTSStarted,
&m.MTSUpdated,
&unusedField,
&unusedField,
&m.Status,
&unusedField,
&unusedField,
&m.Amount,
&m.Fees,
&unusedField,
&unusedField,
&m.DestinationAddress,
&m.PaymentID,
&unusedField,
&unusedField,
&m.TransactionID,
&m.TransactionNote,
}); err != nil {
return err
}
if m.Amount < 0 {
m.TransactionType = "withdrawal"
} else {
m.TransactionType = "deposit"
}
return nil
}
// TradeHistory holds trade history data
type TradeHistory struct {
Price float64 `json:"price,string"`
Amount float64 `json:"amount,string"`
Timestamp int64 `json:"timestamp"`
Exchange string `json:"exchange"`
Type string `json:"type"`
FeeCurrency string `json:"fee_currency"`
FeeAmount float64 `json:"fee_amount,string"`
TID int64 `json:"tid"`
OrderID int64 `json:"order_id"`
Price float64 `json:"price,string"`
Amount float64 `json:"amount,string"`
Timestamp types.Time `json:"timestamp"`
Exchange string `json:"exchange"`
Type string `json:"type"`
FeeCurrency string `json:"fee_currency"`
FeeAmount float64 `json:"fee_amount,string"`
TID int64 `json:"tid"`
OrderID int64 `json:"order_id"`
}
// Offer holds offer information
type Offer struct {
ID int64 `json:"id"`
Currency string `json:"currency"`
Rate float64 `json:"rate,string"`
Period int64 `json:"period"`
Direction string `json:"direction"`
Timestamp string `json:"timestamp"`
Type string `json:"type"`
IsLive bool `json:"is_live"`
IsCancelled bool `json:"is_cancelled"`
OriginalAmount float64 `json:"original_amount,string"`
RemainingAmount float64 `json:"remaining_amount,string"`
ExecutedAmount float64 `json:"executed_amount,string"`
ID int64 `json:"id"`
Currency string `json:"currency"`
Rate float64 `json:"rate,string"`
Period int64 `json:"period"`
Direction string `json:"direction"`
Timestamp types.Time `json:"timestamp"`
Type string `json:"type"`
IsLive bool `json:"is_live"`
IsCancelled bool `json:"is_cancelled"`
OriginalAmount float64 `json:"original_amount,string"`
RemainingAmount float64 `json:"remaining_amount,string"`
ExecutedAmount float64 `json:"executed_amount,string"`
}
// MarginFunds holds active funding information used in a margin position
type MarginFunds struct {
ID int64 `json:"id"`
PositionID int64 `json:"position_id"`
Currency string `json:"currency"`
Rate float64 `json:"rate,string"`
Period int `json:"period"`
Amount float64 `json:"amount,string"`
Timestamp string `json:"timestamp"`
AutoClose bool `json:"auto_close"`
ID int64 `json:"id"`
PositionID int64 `json:"position_id"`
Currency string `json:"currency"`
Rate float64 `json:"rate,string"`
Period int `json:"period"`
Amount float64 `json:"amount,string"`
Timestamp types.Time `json:"timestamp"`
AutoClose bool `json:"auto_close"`
}
// MarginTotalTakenFunds holds position funding including sum of active backing
@@ -493,28 +548,19 @@ type WebsocketBook struct {
Period int64
}
// wsTrade holds trade information
type wsTrade struct {
ID int64
Timestamp types.Time
Amount float64
Price float64
Period int64 // Funding offer period in days
}
// UnmarshalJSON unmarshals json bytes into a wsTrade
func (t *wsTrade) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &[5]any{&t.ID, &t.Timestamp, &t.Amount, &t.Price, &t.Period})
}
// Candle holds OHLC data
// Candle holds OHLCV data
type Candle struct {
Timestamp time.Time
Open float64
Close float64
High float64
Low float64
Volume float64
Timestamp types.Time
Open types.Number
Close types.Number
High types.Number
Low types.Number
Volume types.Number
}
// UnmarshalJSON unmarshals JSON data into a Candle struct
func (c *Candle) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &[6]any{&c.Timestamp, &c.Open, &c.Close, &c.High, &c.Low, &c.Volume})
}
// Leaderboard keys
@@ -578,7 +624,7 @@ type WebsocketOrder struct {
Status string
Price float64
PriceAvg float64
Timestamp int64
Timestamp types.Time
Notify int
}
@@ -586,7 +632,7 @@ type WebsocketOrder struct {
type WebsocketTradeExecuted struct {
TradeID int64
Pair string
Timestamp int64
Timestamp types.Time
OrderID int64
AmountExecuted float64
PriceExecuted float64
@@ -596,7 +642,7 @@ type WebsocketTradeExecuted struct {
type WebsocketTradeData struct {
TradeID int64
Pair string
Timestamp int64
Timestamp types.Time
OrderID int64
AmountExecuted float64
PriceExecuted float64

View File

@@ -1,12 +1,12 @@
package bitfinex
import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"hash/crc32"
"math"
"net/http"
"sort"
"strconv"
@@ -19,7 +19,6 @@ import (
"github.com/buger/jsonparser"
gws "github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/convert"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/encoding/json"
@@ -33,6 +32,7 @@ import (
"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/types"
)
const (
@@ -628,7 +628,7 @@ func (b *Bitfinex) handleWSChannelUpdate(s *subscription.Subscription, respRaw [
case subscription.OrderbookChannel:
return b.handleWSBookUpdate(s, d)
case subscription.CandlesChannel:
return b.handleWSCandleUpdate(s, d)
return b.handleWSAllCandleUpdates(s, respRaw)
case subscription.TickerChannel:
return b.handleWSTickerUpdate(s, d)
case subscription.AllTradesChannel:
@@ -777,83 +777,48 @@ func (b *Bitfinex) handleWSBookUpdate(c *subscription.Subscription, d []any) err
return nil
}
func (b *Bitfinex) handleWSCandleUpdate(c *subscription.Subscription, d []any) error {
func (b *Bitfinex) handleWSAllCandleUpdates(c *subscription.Subscription, respRaw []byte) error {
if c == nil {
return fmt.Errorf("%w: Subscription param", common.ErrNilPointer)
}
if len(c.Pairs) != 1 {
return subscription.ErrNotSinglePair
}
candleBundle, ok := d[1].([]any)
if !ok || len(candleBundle) == 0 {
return nil
v, valueType, _, err := jsonparser.Get(respRaw, "[1]")
if err != nil {
return fmt.Errorf("%w `candlesUpdate[1]`: %w", common.ErrParsingWSField, err)
}
if valueType != jsonparser.Array {
return fmt.Errorf("%w `candlesUpdate[1]`: %w %q", common.ErrParsingWSField, jsonparser.UnknownValueTypeError, valueType)
}
var wsCandles []Candle
if bytes.HasPrefix(v, []byte("[[")) {
if err := json.Unmarshal(v, &wsCandles); err != nil {
return fmt.Errorf("error unmarshalling candle snapshot: %w", err)
}
} else {
var wsCandle Candle
if err := json.Unmarshal(v, &wsCandle); err != nil {
return fmt.Errorf("error unmarshalling candle update: %w", err)
}
wsCandles = []Candle{wsCandle}
}
switch candleData := candleBundle[0].(type) {
case []any:
for i := range candleBundle {
var element []any
element, ok = candleBundle[i].([]any)
if !ok {
return errors.New("candle type assertion for element data")
}
if len(element) < 6 {
return errors.New("invalid candleBundle length")
}
var err error
var klineData websocket.KlineData
if klineData.Timestamp, err = convert.TimeFromUnixTimestampFloat(element[0]); err != nil {
return fmt.Errorf("unable to convert candle timestamp: %w", err)
}
if klineData.OpenPrice, ok = element[1].(float64); !ok {
return errors.New("unable to type assert candle open price")
}
if klineData.ClosePrice, ok = element[2].(float64); !ok {
return errors.New("unable to type assert candle close price")
}
if klineData.HighPrice, ok = element[3].(float64); !ok {
return errors.New("unable to type assert candle high price")
}
if klineData.LowPrice, ok = element[4].(float64); !ok {
return errors.New("unable to type assert candle low price")
}
if klineData.Volume, ok = element[5].(float64); !ok {
return errors.New("unable to type assert candle volume")
}
klineData.Exchange = b.Name
klineData.AssetType = c.Asset
klineData.Pair = c.Pairs[0]
b.Websocket.DataHandler <- klineData
klines := make([]websocket.KlineData, len(wsCandles))
for i := range wsCandles {
klines[i] = websocket.KlineData{
Exchange: b.Name,
AssetType: c.Asset,
Pair: c.Pairs[0],
Timestamp: wsCandles[i].Timestamp.Time(),
OpenPrice: wsCandles[i].Open.Float64(),
ClosePrice: wsCandles[i].Close.Float64(),
HighPrice: wsCandles[i].High.Float64(),
LowPrice: wsCandles[i].Low.Float64(),
Volume: wsCandles[i].Volume.Float64(),
}
case float64:
if len(candleBundle) < 6 {
return errors.New("invalid candleBundle length")
}
var err error
var klineData websocket.KlineData
if klineData.Timestamp, err = convert.TimeFromUnixTimestampFloat(candleData); err != nil {
return fmt.Errorf("unable to convert candle timestamp: %w", err)
}
if klineData.OpenPrice, ok = candleBundle[1].(float64); !ok {
return errors.New("unable to type assert candle open price")
}
if klineData.ClosePrice, ok = candleBundle[2].(float64); !ok {
return errors.New("unable to type assert candle close price")
}
if klineData.HighPrice, ok = candleBundle[3].(float64); !ok {
return errors.New("unable to type assert candle high price")
}
if klineData.LowPrice, ok = candleBundle[4].(float64); !ok {
return errors.New("unable to type assert candle low price")
}
if klineData.Volume, ok = candleBundle[5].(float64); !ok {
return errors.New("unable to type assert candle volume")
}
klineData.Exchange = b.Name
klineData.AssetType = c.Asset
klineData.Pair = c.Pairs[0]
b.Websocket.DataHandler <- klineData
}
b.Websocket.DataHandler <- klines
return nil
}
@@ -951,14 +916,14 @@ func (b *Bitfinex) handleWSAllTrades(s *subscription.Subscription, respRaw []byt
if err != nil {
return fmt.Errorf("%w `tradesUpdate[1]`: %w", common.ErrParsingWSField, err)
}
var wsTrades []*wsTrade
var wsTrades []*Trade
switch valueType {
case jsonparser.String:
t, err := b.handleWSPublicTradeUpdate(respRaw)
if err != nil {
return fmt.Errorf("%w `tradesUpdate[2]`: %w", common.ErrParsingWSField, err)
}
wsTrades = []*wsTrade{t}
wsTrades = []*Trade{t}
case jsonparser.Array:
if wsTrades, err = b.handleWSPublicTradesSnapshot(v); err != nil {
return fmt.Errorf("%w `tradesSnapshot`: %w", common.ErrParsingWSField, err)
@@ -972,18 +937,15 @@ func (b *Bitfinex) handleWSAllTrades(s *subscription.Subscription, respRaw []byt
Exchange: b.Name,
AssetType: s.Asset,
CurrencyPair: s.Pairs[0],
TID: strconv.FormatInt(w.ID, 10),
TID: strconv.FormatInt(w.TID, 10),
Timestamp: w.Timestamp.Time().UTC(),
Side: order.Buy,
Side: w.Side,
Amount: w.Amount,
Price: w.Price,
}
if w.Period != 0 {
t.AssetType = asset.MarginFunding
}
if t.Amount < 0 {
t.Side = order.Sell
t.Amount = math.Abs(t.Amount)
t.Price = w.Rate
}
if feedEnabled {
b.Websocket.DataHandler <- t
@@ -995,17 +957,17 @@ func (b *Bitfinex) handleWSAllTrades(s *subscription.Subscription, respRaw []byt
return err
}
func (b *Bitfinex) handleWSPublicTradesSnapshot(v []byte) ([]*wsTrade, error) {
var trades []*wsTrade
func (b *Bitfinex) handleWSPublicTradesSnapshot(v []byte) ([]*Trade, error) {
var trades []*Trade
return trades, json.Unmarshal(v, &trades)
}
func (b *Bitfinex) handleWSPublicTradeUpdate(respRaw []byte) (*wsTrade, error) {
func (b *Bitfinex) handleWSPublicTradeUpdate(respRaw []byte) (*Trade, error) {
v, _, _, err := jsonparser.Get(respRaw, "[2]")
if err != nil {
return nil, err
}
t := &wsTrade{}
t := &Trade{}
return t, json.Unmarshal(v, t)
}
@@ -1199,7 +1161,7 @@ func (b *Bitfinex) handleWSMyTradeUpdate(d []any, eventType string) error {
if timestamp, ok = tradeData[2].(float64); !ok {
return errors.New("unable to type assert trade timestamp")
}
tData.Timestamp = int64(timestamp)
tData.Timestamp = types.Time(time.UnixMilli(int64(timestamp)))
var orderID float64
if orderID, ok = tradeData[3].(float64); !ok {
return errors.New("unable to type assert trade order ID")

View File

@@ -468,14 +468,14 @@ func (b *Bitfinex) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _
resp[i] = exchange.WithdrawalHistory{
Status: history[i].Status,
TransferID: strconv.FormatInt(history[i].ID, 10),
Description: history[i].Description,
Timestamp: time.UnixMilli(int64(history[i].Timestamp)),
Description: *history[i].TransactionID,
Timestamp: history[i].MTSStarted.Time(),
Currency: history[i].Currency,
Amount: history[i].Amount,
Fee: history[i].Fee,
TransferType: history[i].Type,
CryptoToAddress: history[i].Address,
CryptoTxID: history[i].TxID,
Amount: history[i].Amount.Float64(),
Fee: history[i].Fees.Float64(),
TransferType: history[i].TransactionType,
CryptoToAddress: history[i].DestinationAddress,
CryptoTxID: history[i].TXID,
}
}
return resp, nil
@@ -494,41 +494,38 @@ func (b *Bitfinex) GetHistoricTrades(ctx context.Context, p currency.Pair, a ass
if err := common.StartEndTimeCheck(timestampStart, timestampEnd); err != nil {
return nil, fmt.Errorf("invalid time range supplied. Start: %v End %v %w", timestampStart, timestampEnd, err)
}
var err error
p, err = b.FormatExchangeCurrency(p, a)
p, err := b.FormatExchangeCurrency(p, a)
if err != nil {
return nil, err
}
var currString string
currString, err = b.fixCasing(p, a)
currString, err := b.fixCasing(p, a)
if err != nil {
return nil, err
}
var resp []trade.Data
ts := timestampEnd
limit := 10000
const limit = 10000
allTrades:
for {
var tradeData []Trade
tradeData, err = b.GetTrades(ctx,
currString, int64(limit), 0, ts.Unix()*1000, false)
tradeData, err := b.GetTrades(ctx, currString, limit, time.Time{}, ts, false)
if err != nil {
return nil, err
}
for i := range tradeData {
tradeTS := time.UnixMilli(tradeData[i].Timestamp)
tradeTS := tradeData[i].Timestamp.Time()
if tradeTS.Before(timestampStart) && !timestampStart.IsZero() {
break allTrades
}
tID := strconv.FormatInt(tradeData[i].TID, 10)
resp = append(resp, trade.Data{
TID: tID,
TID: strconv.FormatInt(tradeData[i].TID, 10),
Exchange: b.Name,
CurrencyPair: p,
AssetType: a,
Price: tradeData[i].Price,
Amount: tradeData[i].Amount,
Timestamp: time.UnixMilli(tradeData[i].Timestamp),
Timestamp: tradeData[i].Timestamp.Time(),
})
if i == len(tradeData)-1 {
if ts.Equal(tradeTS) {
@@ -543,8 +540,7 @@ allTrades:
}
}
err = b.AddTradesToBuffer(resp...)
if err != nil {
if err := b.AddTradesToBuffer(resp...); err != nil {
return nil, err
}
@@ -1052,7 +1048,7 @@ func (b *Bitfinex) GetHistoricCandles(ctx context.Context, pair currency.Pair, a
if err != nil {
return nil, err
}
candles, err := b.GetCandles(ctx, cf, fInterval, req.Start.UnixMilli(), req.End.UnixMilli(), req.RequestLimit, true)
candles, err := b.GetCandles(ctx, cf, fInterval, req.Start, req.End, req.RequestLimit, true)
if err != nil {
return nil, err
}
@@ -1060,12 +1056,12 @@ func (b *Bitfinex) GetHistoricCandles(ctx context.Context, pair currency.Pair, a
timeSeries := make([]kline.Candle, len(candles))
for x := range candles {
timeSeries[x] = kline.Candle{
Time: candles[x].Timestamp,
Open: candles[x].Open,
High: candles[x].High,
Low: candles[x].Low,
Close: candles[x].Close,
Volume: candles[x].Volume,
Time: candles[x].Timestamp.Time(),
Open: candles[x].Open.Float64(),
High: candles[x].High.Float64(),
Low: candles[x].Low.Float64(),
Close: candles[x].Close.Float64(),
Volume: candles[x].Volume.Float64(),
}
}
return req.ProcessResponse(timeSeries)
@@ -1089,19 +1085,19 @@ func (b *Bitfinex) GetHistoricCandlesExtended(ctx context.Context, pair currency
timeSeries := make([]kline.Candle, 0, req.Size())
for x := range req.RangeHolder.Ranges {
var candles []Candle
candles, err = b.GetCandles(ctx, cf, fInterval, req.RangeHolder.Ranges[x].Start.Time.UnixMilli(), req.RangeHolder.Ranges[x].End.Time.UnixMilli(), req.RequestLimit, true)
candles, err = b.GetCandles(ctx, cf, fInterval, req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time, req.RequestLimit, true)
if err != nil {
return nil, err
}
for i := range candles {
timeSeries = append(timeSeries, kline.Candle{
Time: candles[i].Timestamp,
Open: candles[i].Open,
High: candles[i].High,
Low: candles[i].Low,
Close: candles[i].Close,
Volume: candles[i].Volume,
Time: candles[i].Timestamp.Time(),
Open: candles[i].Open.Float64(),
High: candles[i].High.Float64(),
Low: candles[i].Low.Float64(),
Close: candles[i].Close.Float64(),
Volume: candles[i].Volume.Float64(),
})
}
}