Files
gocryptotrader/exchanges/bitfinex/bitfinex.go
Ryan O'Hara-Reid e823f9edd8 request/nonce: Refactor to simplify package and prevent consecutive mutex lock calls when accessing/setting nonce values (#1506)
* improv. timed mutex

* Add all protection back in and jankyness because races. :'(

* Add intial benchmarkeroos

* Add master benchmarks

* goodness me

* what?

* what again?

* glorious: nits

* just a swaperino instead

* clean up package nonce so that we only need to aquire mutex once

* unlock before checking master

* commentary

* wha

* more comment

* ch comment

* nonce: Allow for broad customisation externally with a ~2ns overhead

* glorious: nits maybe works?

---------

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
2024-04-12 16:54:21 +10:00

2328 lines
71 KiB
Go

package bitfinex
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"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"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/nonce"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)
const (
bitfinexAPIURLBase = "https://api.bitfinex.com"
// Version 1 API endpoints
bitfinexAPIVersion = "/v1/"
bitfinexStats = "stats/"
bitfinexAccountInfo = "account_infos"
bitfinexAccountFees = "account_fees"
bitfinexAccountSummary = "summary"
bitfinexBalances = "balances"
bitfinexTransfer = "transfer"
bitfinexWithdrawal = "withdraw"
bitfinexOrderNew = "order/new"
bitfinexOrderNewMulti = "order/new/multi"
bitfinexOrderCancel = "order/cancel"
bitfinexOrderCancelMulti = "order/cancel/multi"
bitfinexOrderCancelAll = "order/cancel/all"
bitfinexOrderCancelReplace = "order/cancel/replace"
bitfinexOrderStatus = "order/status"
bitfinexInactiveOrders = "hist"
bitfinexOrders = "orders"
bitfinexPositions = "positions"
bitfinexClaimPosition = "position/claim"
bitfinexHistory = "history"
bitfinexHistoryMovements = "movements"
bitfinexTradeHistory = "mytrades"
bitfinexOfferNew = "offer/new"
bitfinexOfferCancel = "offer/cancel"
bitfinexActiveCredits = "credits"
bitfinexOffers = "offers"
bitfinexMarginActiveFunds = "taken_funds"
bitfinexMarginUnusedFunds = "unused_taken_funds"
bitfinexMarginTotalFunds = "total_taken_funds"
bitfinexMarginClose = "funding/close"
bitfinexLendbook = "lendbook/"
bitfinexLends = "lends/"
bitfinexLeaderboard = "rankings"
// Version 2 API endpoints
bitfinexAPIVersion2 = "/v2/"
bitfinexV2MarginFunding = "calc/trade/avg?"
bitfinexV2Balances = "auth/r/wallets"
bitfinexV2AccountInfo = "auth/r/info/user"
bitfinexV2MarginInfo = "auth/r/info/margin/"
bitfinexV2FundingInfo = "auth/r/info/funding/%s"
bitfinexV2Auth = "auth/"
bitfinexDerivativeData = "status/deriv?"
bitfinexPlatformStatus = "platform/status"
bitfinexTickerBatch = "tickers"
bitfinexTicker = "ticker/"
bitfinexTrades = "trades/"
bitfinexOrderbook = "book/"
bitfinexHistoryShort = "hist"
bitfinexCandles = "candles/trade"
bitfinexKeyPermissions = "key_info"
bitfinexMarginInfo = "margin_infos"
bitfinexDepositMethod = "conf/pub:map:tx:method"
bitfinexDepositAddress = "auth/w/deposit/address"
bitfinexOrderUpdate = "auth/w/order/update"
bitfinexMarginPairs = "conf/pub:list:pair:margin"
bitfinexSpotPairs = "conf/pub:list:pair:exchange"
bitfinexMarginFundingPairs = "conf/pub:list:currency"
bitfinexFuturesPairs = "conf/pub:list:pair:futures" // TODO: Implement
bitfinexSecuritiesPairs = "conf/pub:list:pair:securities" // TODO: Implement
bitfinexInfoPairs = "conf/pub:info:pair"
bitfinexInfoFuturePairs = "conf/pub:info:pair:futures"
// Bitfinex platform status values
// When the platform is marked in maintenance mode bots should stop trading
// activity. Cancelling orders will be possible.
bitfinexMaintenanceMode = 0
bitfinexOperativeMode = 1
bitfinexChecksumFlag = 131072
bitfinexWsSequenceFlag = 65536
// CandlesTimeframeKey configures the timeframe in subscription.Subscription.Params
CandlesTimeframeKey = "_timeframe"
// CandlesPeriodKey configures the aggregated period in subscription.Subscription.Params
CandlesPeriodKey = "_period"
)
// Bitfinex is the overarching type across the bitfinex package
type Bitfinex struct {
exchange.Base
}
// GetPlatformStatus returns the Bifinex platform status
func (b *Bitfinex) GetPlatformStatus(ctx context.Context) (int, error) {
var response []int
err := b.SendHTTPRequest(ctx, exchange.RestSpot,
bitfinexAPIVersion2+
bitfinexPlatformStatus,
&response,
platformStatus)
if err != nil {
return -1, err
}
switch response[0] {
case bitfinexOperativeMode:
return bitfinexOperativeMode, nil
case bitfinexMaintenanceMode:
return bitfinexMaintenanceMode, nil
}
return -1, fmt.Errorf("unexpected platform status value %d", response[0])
}
func baseMarginInfo(data []interface{}) (MarginInfoV2, error) {
var resp MarginInfoV2
marginInfo, ok := data[1].([]any)
if !ok {
return resp, common.GetTypeAssertError("[]any", data[1], "MarginInfo")
}
if resp.UserPNL, ok = marginInfo[0].(float64); !ok {
return resp, common.GetTypeAssertError("float64", marginInfo[0], "UserPNL")
}
if resp.UserSwaps, ok = marginInfo[1].(float64); !ok {
return resp, common.GetTypeAssertError("float64", marginInfo[1], "UserSwaps")
}
if resp.MarginBalance, ok = marginInfo[2].(float64); !ok {
return resp, common.GetTypeAssertError("float64", marginInfo[2], "MarginBalance")
}
if resp.MarginNet, ok = marginInfo[3].(float64); !ok {
return resp, common.GetTypeAssertError("float64", marginInfo[3], "MarginNet")
}
if resp.MarginMin, ok = marginInfo[4].(float64); !ok {
return resp, common.GetTypeAssertError("float64", marginInfo[4], "MarginMin")
}
return resp, nil
}
func symbolMarginInfo(data []interface{}) ([]MarginInfoV2, error) {
resp := make([]MarginInfoV2, len(data))
for x := range data {
var tempResp MarginInfoV2
marginInfo, ok := data[x].([]any)
if !ok {
return nil, common.GetTypeAssertError("[]any", data[x], "MarginInfo")
}
var check bool
if tempResp.Symbol, check = marginInfo[1].(string); !check {
return nil, common.GetTypeAssertError("string", marginInfo[1], "Symbol")
}
pairMarginInfo, check := marginInfo[2].([]any)
if !check {
return nil, common.GetTypeAssertError("[]any", marginInfo[2], "MarginInfo.Data")
}
if len(pairMarginInfo) < 4 {
return nil, errors.New("invalid data received")
}
if tempResp.TradableBalance, ok = pairMarginInfo[0].(float64); !ok {
return nil, common.GetTypeAssertError("float64", pairMarginInfo[0], "MarginInfo.Data.TradableBalance")
}
if tempResp.GrossBalance, ok = pairMarginInfo[1].(float64); !ok {
return nil, common.GetTypeAssertError("float64", pairMarginInfo[1], "MarginInfo.Data.GlossBalance")
}
if tempResp.BestAskAmount, ok = pairMarginInfo[2].(float64); !ok {
return nil, common.GetTypeAssertError("float64", pairMarginInfo[2], "MarginInfo.Data.BestAskAmount")
}
if tempResp.BestBidAmount, ok = pairMarginInfo[3].(float64); !ok {
return nil, common.GetTypeAssertError("float64", pairMarginInfo[3], "MarginInfo.Data.BestBidAmount")
}
resp[x] = tempResp
}
return resp, nil
}
func defaultMarginV2Info(data []interface{}) (MarginInfoV2, error) {
var resp MarginInfoV2
var ok bool
if resp.Symbol, ok = data[1].(string); !ok {
return resp, common.GetTypeAssertError("string", data[1], "Symbol")
}
marginInfo, check := data[2].([]any)
if !check {
return resp, common.GetTypeAssertError("[]any", data[2], "MarginInfo.Data")
}
if len(marginInfo) < 4 {
return resp, errors.New("invalid data received")
}
if resp.TradableBalance, ok = marginInfo[0].(float64); !ok {
return resp, common.GetTypeAssertError("float64", marginInfo[0], "MarginInfo.Data.TradableBalance")
}
if resp.GrossBalance, ok = marginInfo[1].(float64); !ok {
return resp, common.GetTypeAssertError("float64", marginInfo[1], "MarginInfo.Data.GrossBalance")
}
if resp.BestAskAmount, ok = marginInfo[2].(float64); !ok {
return resp, common.GetTypeAssertError("float64", marginInfo[2], "MarginInfo.Data.BestAskAmount")
}
if resp.BestBidAmount, ok = marginInfo[3].(float64); !ok {
return resp, common.GetTypeAssertError("float64", marginInfo[3], "MarginInfo.Data.BestBidAmount")
}
return resp, nil
}
// GetV2MarginInfo gets v2 margin info for a symbol provided
// symbol: base, sym_all, any other trading symbol example tBTCUSD
func (b *Bitfinex) GetV2MarginInfo(ctx context.Context, symbol string) ([]MarginInfoV2, error) {
var data []interface{}
err := b.SendAuthenticatedHTTPRequestV2(ctx,
exchange.RestSpot, http.MethodPost,
bitfinexV2MarginInfo+symbol,
nil,
&data,
getMarginInfoRate)
if err != nil {
return nil, err
}
var tempResp MarginInfoV2
switch symbol {
case "base":
tempResp, err = baseMarginInfo(data)
if err != nil {
return nil, fmt.Errorf("%v - %s: %w", b.Name, symbol, err)
}
case "sym_all":
var resp []MarginInfoV2
resp, err = symbolMarginInfo(data)
return resp, err
default:
tempResp, err = defaultMarginV2Info(data)
if err != nil {
return nil, fmt.Errorf("%v - %s: %w", b.Name, symbol, err)
}
}
return []MarginInfoV2{tempResp}, nil
}
// GetV2MarginFunding gets borrowing rates for margin trading
func (b *Bitfinex) GetV2MarginFunding(ctx context.Context, symbol, amount string, period int32) (MarginV2FundingData, error) {
var resp []interface{}
var response MarginV2FundingData
params := make(map[string]interface{})
params["symbol"] = symbol
params["period"] = period
params["amount"] = amount
err := b.SendAuthenticatedHTTPRequestV2(ctx, exchange.RestSpot, http.MethodPost,
bitfinexV2MarginFunding,
params,
&resp,
getMarginInfoRate)
if err != nil {
return response, err
}
if len(resp) != 2 {
return response, errors.New("invalid data received")
}
avgRate, ok := resp[0].(float64)
if !ok {
return response, common.GetTypeAssertError("float64", resp[0], "MarketAveragePrice.PriceOrRate")
}
avgAmount, ok := resp[1].(float64)
if !ok {
return response, common.GetTypeAssertError("float64", resp[1], "MarketAveragePrice.Amount")
}
response.Symbol = symbol
response.RateAverage = avgRate
response.AmountAverage = avgAmount
return response, nil
}
// GetV2FundingInfo gets funding info for margin pairs
func (b *Bitfinex) GetV2FundingInfo(ctx context.Context, key string) (MarginFundingDataV2, error) {
var resp []interface{}
var response MarginFundingDataV2
err := b.SendAuthenticatedHTTPRequestV2(ctx, exchange.RestSpot, http.MethodPost,
fmt.Sprintf(bitfinexV2FundingInfo, key),
nil,
&resp,
getAccountFees)
if err != nil {
return response, err
}
if len(resp) != 3 {
return response, errors.New("invalid data received")
}
sym, ok := resp[0].(string)
if !ok {
return response, common.GetTypeAssertError("string", resp[0], "FundingInfo.sym")
}
symbol, ok := resp[1].(string)
if !ok {
return response, common.GetTypeAssertError("string", resp[1], "FundingInfo.Symbol")
}
fundingData, ok := resp[2].([]any)
if !ok {
return response, common.GetTypeAssertError("[]any", resp[2], "FundingInfo.FundingRateOrDuration")
}
response.Sym = sym
response.Symbol = symbol
if len(fundingData) < 4 {
return response, fmt.Errorf("%v GetV2FundingInfo: invalid length of fundingData", b.Name)
}
if response.Data.YieldLoan, ok = fundingData[0].(float64); !ok {
return response, errors.New("type conversion failed for YieldLoan")
}
if response.Data.YieldLend, ok = fundingData[1].(float64); !ok {
return response, errors.New("type conversion failed for YieldLend")
}
if response.Data.DurationLoan, ok = fundingData[2].(float64); !ok {
return response, errors.New("type conversion failed for DurationLoan")
}
if response.Data.DurationLend, ok = fundingData[3].(float64); !ok {
return response, errors.New("type conversion failed for DurationLend")
}
return response, nil
}
// GetAccountInfoV2 gets V2 account data
func (b *Bitfinex) GetAccountInfoV2(ctx context.Context) (AccountV2Data, error) {
var resp AccountV2Data
var data []interface{}
err := b.SendAuthenticatedHTTPRequestV2(ctx, exchange.RestSpot, http.MethodPost,
bitfinexV2AccountInfo,
nil,
&data,
getAccountFees)
if err != nil {
return resp, err
}
if len(data) < 8 {
return resp, fmt.Errorf("%v GetAccountInfoV2: invalid length of data", b.Name)
}
var ok bool
var tempString string
var tempFloat float64
if tempFloat, ok = data[0].(float64); !ok {
return resp, common.GetTypeAssertError("float64", data[0], "AccountInfo.AccountID")
}
resp.ID = int64(tempFloat)
if tempString, ok = data[1].(string); !ok {
return resp, common.GetTypeAssertError("string", data[1], "AccountInfo.AccountEmail")
}
resp.Email = tempString
if tempString, ok = data[2].(string); !ok {
return resp, common.GetTypeAssertError("string", data[2], "AccountInfo.AccountUsername")
}
resp.Username = tempString
if tempFloat, ok = data[3].(float64); !ok {
return resp, common.GetTypeAssertError("float64", data[3], "AccountInfo.Account.MTSAccountCreate")
}
resp.MTSAccountCreate = int64(tempFloat)
if tempFloat, ok = data[4].(float64); !ok {
return resp, common.GetTypeAssertError("float64", data[4], "AccountInfo.AccountVerified")
}
resp.Verified = int64(tempFloat)
if tempString, ok = data[7].(string); !ok {
return resp, common.GetTypeAssertError("string", data[7], "AccountInfo.AccountTimezone")
}
resp.Timezone = tempString
return resp, nil
}
// GetV2Balances gets v2 balances
func (b *Bitfinex) GetV2Balances(ctx context.Context) ([]WalletDataV2, error) {
var data [][4]interface{}
err := b.SendAuthenticatedHTTPRequestV2(ctx,
exchange.RestSpot, http.MethodPost,
bitfinexV2Balances,
nil,
&data,
getAccountFees)
if err != nil {
return nil, err
}
resp := make([]WalletDataV2, len(data))
for x := range data {
walletType, ok := data[x][0].(string)
if !ok {
return resp, common.GetTypeAssertError("string", data[x][0], "Wallets.WalletType")
}
currency, ok := data[x][1].(string)
if !ok {
return resp, common.GetTypeAssertError("string", data[x][1], "Wallets.Currency")
}
balance, ok := data[x][2].(float64)
if !ok {
return resp, common.GetTypeAssertError("float64", data[x][2], "Wallets.WalletBalance")
}
unsettledInterest, ok := data[x][3].(float64)
if !ok {
return resp, common.GetTypeAssertError("float64", data[x][3], "Wallets.UnsettledInterest")
}
resp[x] = WalletDataV2{
WalletType: walletType,
Currency: currency,
Balance: balance,
UnsettledInterest: unsettledInterest,
}
}
return resp, nil
}
// GetPairs gets pairs for different assets
func (b *Bitfinex) GetPairs(ctx context.Context, a asset.Item) ([]string, error) {
switch a {
case asset.Spot:
list, err := b.GetSiteListConfigData(ctx, bitfinexSpotPairs)
if err != nil {
return nil, err
}
filter, err := b.GetSiteListConfigData(ctx, bitfinexSecuritiesPairs)
if err != nil {
return nil, err
}
filtered := make([]string, 0, len(list))
for x := range list {
if common.StringDataCompare(filter, list[x]) {
continue
}
filtered = append(filtered, list[x])
}
return filtered, nil
case asset.Margin:
return b.GetSiteListConfigData(ctx, bitfinexMarginPairs)
case asset.Futures:
return b.GetSiteListConfigData(ctx, bitfinexFuturesPairs)
case asset.MarginFunding:
funding, err := b.GetTickerBatch(ctx)
if err != nil {
return nil, err
}
var pairs []string
for key := range funding {
symbol := key[1:]
if key[0] != 'f' || strings.Contains(symbol, ":") || len(symbol) > 6 {
continue
}
pairs = append(pairs, symbol)
}
return pairs, nil
default:
return nil, fmt.Errorf("%v GetPairs: %v %w", b.Name, a, asset.ErrNotSupported)
}
}
// GetSiteListConfigData returns site configuration data by pub:list:{Object}:{Detail}
// string sets.
// NOTE: See https://docs.bitfinex.com/reference/rest-public-conf
func (b *Bitfinex) GetSiteListConfigData(ctx context.Context, set string) ([]string, error) {
if set == "" {
return nil, errSetCannotBeEmpty
}
var resp [][]string
path := bitfinexAPIVersion2 + set
err := b.SendHTTPRequest(ctx, exchange.RestSpot, path, &resp, status)
if err != nil {
return nil, err
}
if len(resp) != 1 {
return nil, errors.New("invalid response")
}
return resp[0], nil
}
// GetSiteInfoConfigData returns site configuration data by pub:info:{AssetType} as a map
// path should be bitfinexInfoPairs or bitfinexInfoPairsFuture???
// NOTE: See https://docs.bitfinex.com/reference/rest-public-conf
func (b *Bitfinex) GetSiteInfoConfigData(ctx context.Context, assetType asset.Item) ([]order.MinMaxLevel, error) {
var path string
switch assetType {
case asset.Spot:
path = bitfinexInfoPairs
case asset.Futures:
path = bitfinexInfoFuturePairs
default:
return nil, fmt.Errorf("invalid asset type for GetSiteInfoConfigData: %s", assetType)
}
url := bitfinexAPIVersion2 + path
var resp [][][]any
err := b.SendHTTPRequest(ctx, exchange.RestSpot, url, &resp, status)
if err != nil {
return nil, err
}
if len(resp) != 1 {
return nil, errors.New("response did not contain only one item")
}
data := resp[0]
pairs := make([]order.MinMaxLevel, 0, len(data))
for i := range data {
if len(data[i]) != 2 {
return nil, errors.New("response contained a tuple without exactly 2 items")
}
pairSymbol, ok := data[i][0].(string)
if !ok {
return nil, fmt.Errorf("could not convert first item in SiteInfoConfigData to string: Type is %T", data[i][0])
}
if strings.Contains(pairSymbol, "TEST") {
continue
}
// SIC: Array type really is any. It contains nils and strings
info, ok := data[i][1].([]any)
if !ok {
return nil, fmt.Errorf("could not convert second item in SiteInfoConfigData to []any; Type is %T", data[i][1])
}
if len(info) < 5 {
return nil, errors.New("response contained order info with less than 5 elements")
}
minOrder, err := convert.FloatFromString(info[3])
if err != nil {
return nil, fmt.Errorf("could not convert MinOrderAmount: %s", err)
}
maxOrder, err := convert.FloatFromString(info[4])
if err != nil {
return nil, fmt.Errorf("could not convert MaxOrderAmount: %s", err)
}
pair, err := currency.NewPairFromString(pairSymbol)
if err != nil {
return nil, err
}
pairs = append(pairs, order.MinMaxLevel{
Asset: assetType,
Pair: pair,
MinimumBaseAmount: minOrder,
MaximumBaseAmount: maxOrder,
})
}
return pairs, nil
}
// GetDerivativeStatusInfo gets status data for the queried derivative
func (b *Bitfinex) GetDerivativeStatusInfo(ctx context.Context, keys, startTime, endTime string, sort, limit int64) ([]DerivativeDataResponse, error) {
params := url.Values{}
params.Set("keys", keys)
if startTime != "" {
params.Set("start", startTime)
}
if endTime != "" {
params.Set("end", endTime)
}
if sort != 0 {
params.Set("sort", strconv.FormatInt(sort, 10))
}
if limit != 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
var result [][]interface{}
path := bitfinexAPIVersion2 + bitfinexDerivativeData +
params.Encode()
err := b.SendHTTPRequest(ctx, exchange.RestSpot, path, &result, status)
if err != nil {
return nil, err
}
finalResp := make([]DerivativeDataResponse, len(result))
for z := range result {
if len(result[z]) < 19 {
return finalResp, fmt.Errorf("%v GetDerivativeStatusInfo: invalid response, array length too small, check api docs for updates", b.Name)
}
var response DerivativeDataResponse
var ok bool
if response.Key, ok = result[z][0].(string); !ok {
return finalResp, common.GetTypeAssertError("string", result[z][0], "DerivativesStatus.Key")
}
if response.MTS, ok = result[z][1].(float64); !ok {
return finalResp, common.GetTypeAssertError("float64", result[z][1], "DerivativesStatus.MTS")
}
if response.DerivPrice, ok = result[z][3].(float64); !ok {
return finalResp, common.GetTypeAssertError("float64", result[z][3], "DerivativesStatus.DerivPrice")
}
if response.SpotPrice, ok = result[z][4].(float64); !ok {
return finalResp, common.GetTypeAssertError("float64", result[z][4], "DerivativesStatus.SpotPrice")
}
if response.InsuranceFundBalance, ok = result[z][6].(float64); !ok {
return finalResp, common.GetTypeAssertError("float64", result[z][6], "DerivativesStatus.InsuranceFundBalance")
}
if response.NextFundingEventTS, ok = result[z][8].(float64); !ok {
return finalResp, common.GetTypeAssertError("float64", result[z][8], "DerivativesStatus.NextFundingEventMTS")
}
if response.NextFundingAccrued, ok = result[z][9].(float64); !ok {
return finalResp, common.GetTypeAssertError("float64", result[z][9], "DerivativesStatus.NextFundingAccrued")
}
if response.NextFundingStep, ok = result[z][10].(float64); !ok {
return finalResp, common.GetTypeAssertError("float64", result[z][10], "DerivativesStatus.NextFundingStep")
}
if response.CurrentFunding, ok = result[z][12].(float64); !ok {
return finalResp, common.GetTypeAssertError("float64", result[z][12], "DerivativesStatus.CurrentFunding")
}
if response.MarkPrice, ok = result[z][15].(float64); !ok {
return finalResp, common.GetTypeAssertError("float64", result[z][15], "DerivativesStatus.MarkPrice")
}
switch t := result[z][18].(type) {
case float64:
response.OpenInterest = t
case nil:
break // SupportedCapability will default to 0
default:
return finalResp, common.GetTypeAssertError(" float64|nil", t, "DerivativesStatus.SupportedCapability")
}
finalResp[z] = response
}
return finalResp, nil
}
// GetTickerBatch returns all supported ticker information
func (b *Bitfinex) GetTickerBatch(ctx context.Context) (map[string]*Ticker, error) {
var response [][]any
path := bitfinexAPIVersion2 + bitfinexTickerBatch +
"?symbols=ALL"
err := b.SendHTTPRequest(ctx, exchange.RestSpot, path, &response, tickerBatch)
if err != nil {
return nil, err
}
var tickErrs error
var tickers = make(map[string]*Ticker)
for _, tickResp := range response {
symbol, ok := tickResp[0].(string)
if !ok {
tickErrs = common.AppendError(tickErrs, fmt.Errorf("%w: %v", errTickerInvalidSymbol, symbol))
continue
}
if t, err := tickerFromResp(symbol, tickResp[1:]); err != nil {
// We get too frequent intermittent formatting errors from tALT2612:USD to treat them as errors
if !errors.Is(err, errTickerInvalidResp) {
tickErrs = common.AppendError(tickErrs, err)
}
} else {
tickers[symbol] = t
}
}
return tickers, tickErrs
}
// GetTicker returns ticker information for one symbol
func (b *Bitfinex) GetTicker(ctx context.Context, symbol string) (*Ticker, error) {
var response []any
path := bitfinexAPIVersion2 + bitfinexTicker + symbol
err := b.SendHTTPRequest(ctx, exchange.RestSpot, path, &response, tickerFunction)
if err != nil {
return nil, err
}
t, err := tickerFromResp(symbol, response)
if err != nil {
return nil, err
}
return t, nil
}
var tickerFields = []string{"Bid", "BidSize", "Ask", "AskSize", "DailyChange", "DailyChangePercentage", "LastPrice", "DailyVolume", "DailyHigh", "DailyLow"}
func tickerFromResp(symbol string, respAny []any) (*Ticker, error) {
if strings.HasPrefix(symbol, "f") {
return tickerFromFundingResp(symbol, respAny)
}
if len(respAny) != 10 {
return nil, fmt.Errorf("%w for %s: %v", errTickerInvalidFieldCount, symbol, respAny)
}
resp := make([]float64, 10)
for i := range respAny {
f, ok := respAny[i].(float64)
if !ok {
return nil, fmt.Errorf("%w for %s field %s from %v", errTickerInvalidResp, symbol, tickerFields[i], respAny)
}
resp[i] = f
}
return &Ticker{
Bid: resp[0],
BidSize: resp[1],
Ask: resp[2],
AskSize: resp[3],
DailyChange: resp[4],
DailyChangePerc: resp[5],
Last: resp[6],
Volume: resp[7],
High: resp[8],
Low: resp[9],
}, nil
}
var fundingTickerFields = []string{"FlashReturnRate", "Bid", "BidPeriod", "BidSize", "Ask", "AskPeriod", "AskSize", "DailyChange", "DailyChangePercentage", "LastPrice", "DailyVolume", "DailyHigh", "DailyLow", "", "", "FFRAmountAvailable"}
func tickerFromFundingResp(symbol string, respAny []any) (*Ticker, error) {
if len(respAny) != 16 {
return nil, fmt.Errorf("%w for %s: %v", errTickerInvalidFieldCount, symbol, respAny)
}
resp := make([]float64, 16)
for i := range respAny {
if fundingTickerFields[i] == "" { // Unused nil fields
continue
}
f, ok := respAny[i].(float64)
if !ok {
return nil, fmt.Errorf("%w for %s field %s from %v", errTickerInvalidResp, symbol, fundingTickerFields[i], respAny)
}
resp[i] = f
}
return &Ticker{
FlashReturnRate: resp[0],
Bid: resp[1],
BidPeriod: int64(resp[2]),
BidSize: resp[3],
Ask: resp[4],
AskPeriod: int64(resp[5]),
AskSize: resp[6],
DailyChange: resp[7],
DailyChangePerc: resp[8],
Last: resp[9],
Volume: resp[10],
High: resp[11],
Low: resp[12],
FFRAmountAvailable: resp[15],
}, nil
}
// GetTrades gets historic trades that occurred on the exchange
//
// currencyPair e.g. "tBTCUSD"
// 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) {
v := url.Values{}
if limit > 0 {
v.Set("limit", strconv.FormatInt(limit, 10))
}
if timestampStart > 0 {
v.Set("start", strconv.FormatInt(timestampStart, 10))
}
if timestampEnd > 0 {
v.Set("end", strconv.FormatInt(timestampEnd, 10))
}
sortVal := "0"
if reOrderResp {
sortVal = "1"
}
v.Set("sort", sortVal)
path := bitfinexAPIVersion2 + bitfinexTrades + currencyPair + "/hist" + "?" + v.Encode()
var resp [][]interface{}
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
}
// GetOrderbook retrieves the orderbook bid and ask price points for a currency
// pair - By default the response will return 25 bid and 25 ask price points.
// symbol - Example "tBTCUSD"
// precision - P0,P1,P2,P3,R0
// Values can contain limit amounts for both the asks and bids - Example
// "len" = 100
func (b *Bitfinex) GetOrderbook(ctx context.Context, symbol, precision string, limit int64) (Orderbook, error) {
var u = url.Values{}
if limit > 0 {
u.Set("len", strconv.FormatInt(limit, 10))
}
path := bitfinexAPIVersion2 + bitfinexOrderbook + symbol + "/" + precision + "?" + u.Encode()
var response [][]interface{}
err := b.SendHTTPRequest(ctx, exchange.RestSpot, path, &response, orderbookFunction)
if err != nil {
return Orderbook{}, err
}
var o Orderbook
if precision == "R0" {
// Raw book changes the return
for x := range response {
var b Book
if len(response[x]) > 3 {
// Funding currency
var ok bool
if b.Amount, ok = response[x][3].(float64); !ok {
return Orderbook{}, errors.New("unable to type assert amount")
}
if b.Rate, ok = response[x][2].(float64); !ok {
return Orderbook{}, errors.New("unable to type assert rate")
}
if b.Period, ok = response[x][1].(float64); !ok {
return Orderbook{}, errors.New("unable to type assert period")
}
orderID, ok := response[x][0].(float64)
if !ok {
return Orderbook{}, errors.New("unable to type assert orderID")
}
b.OrderID = int64(orderID)
if b.Amount > 0 {
o.Asks = append(o.Asks, b)
} else {
b.Amount *= -1
o.Bids = append(o.Bids, b)
}
} else {
// Trading currency
var ok bool
if b.Amount, ok = response[x][2].(float64); !ok {
return Orderbook{}, errors.New("unable to type assert amount")
}
if b.Price, ok = response[x][1].(float64); !ok {
return Orderbook{}, errors.New("unable to type assert price")
}
orderID, ok := response[x][0].(float64)
if !ok {
return Orderbook{}, errors.New("unable to type assert order ID")
}
b.OrderID = int64(orderID)
if b.Amount > 0 {
o.Bids = append(o.Bids, b)
} else {
b.Amount *= -1
o.Asks = append(o.Asks, b)
}
}
}
} else {
for x := range response {
var b Book
if len(response[x]) > 3 {
// Funding currency
var ok bool
if b.Amount, ok = response[x][3].(float64); !ok {
return Orderbook{}, errors.New("unable to type assert amount")
}
count, ok := response[x][2].(float64)
if !ok {
return Orderbook{}, errors.New("unable to type assert count")
}
b.Count = int64(count)
if b.Period, ok = response[x][1].(float64); !ok {
return Orderbook{}, errors.New("unable to type assert period")
}
if b.Rate, ok = response[x][0].(float64); !ok {
return Orderbook{}, errors.New("unable to type assert rate")
}
if b.Amount > 0 {
o.Asks = append(o.Asks, b)
} else {
b.Amount *= -1
o.Bids = append(o.Bids, b)
}
} else {
// Trading currency
var ok bool
if b.Amount, ok = response[x][2].(float64); !ok {
return Orderbook{}, errors.New("unable to type assert amount")
}
count, ok := response[x][1].(float64)
if !ok {
return Orderbook{}, errors.New("unable to type assert count")
}
b.Count = int64(count)
if b.Price, ok = response[x][0].(float64); !ok {
return Orderbook{}, errors.New("unable to type assert price")
}
if b.Amount > 0 {
o.Bids = append(o.Bids, b)
} else {
b.Amount *= -1
o.Asks = append(o.Asks, b)
}
}
}
}
return o, nil
}
// GetStats returns various statistics about the requested pair
func (b *Bitfinex) GetStats(ctx context.Context, symbol string) ([]Stat, error) {
var response []Stat
path := bitfinexAPIVersion + bitfinexStats + symbol
return response, b.SendHTTPRequest(ctx, exchange.RestSpot, path, &response, statsV1)
}
// GetFundingBook the entire margin funding book for both bids and asks sides
// per currency string
// symbol - example "USD"
// WARNING: Orderbook now has this support, will be deprecated once a full
// conversion to full V2 API update is done.
func (b *Bitfinex) GetFundingBook(ctx context.Context, symbol string) (FundingBook, error) {
response := FundingBook{}
path := bitfinexAPIVersion + bitfinexLendbook + symbol
if err := b.SendHTTPRequest(ctx, exchange.RestSpot, path, &response, fundingbook); err != nil {
return response, err
}
return response, nil
}
// GetLends returns a list of the most recent funding data for the given
// currency: total amount provided and Flash Return Rate (in % by 365 days)
// over time
// Symbol - example "USD"
func (b *Bitfinex) GetLends(ctx context.Context, symbol string, values url.Values) ([]Lends, error) {
var response []Lends
path := common.EncodeURLValues(bitfinexAPIVersion+
bitfinexLends+
symbol,
values)
return response, b.SendHTTPRequest(ctx, exchange.RestSpot, path, &response, lends)
}
// 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 uint32, historic bool) ([]Candle, error) {
var fundingPeriod string
if symbol[0] == 'f' {
fundingPeriod = ":p30"
}
var path = bitfinexAPIVersion2 +
bitfinexCandles +
":" +
timeFrame +
":" +
symbol +
fundingPeriod
if historic {
v := url.Values{}
if start > 0 {
v.Set("start", strconv.FormatInt(start, 10))
}
if end > 0 {
v.Set("end", strconv.FormatInt(end, 10))
}
if limit > 0 {
v.Set("limit", strconv.FormatInt(int64(limit), 10))
}
path += "/hist"
if len(v) > 0 {
path += "?" + v.Encode()
}
var response [][]interface{}
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
}
path += "/last"
var response []interface{}
err := b.SendHTTPRequest(ctx, exchange.RestSpot, path, &response, 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
}
// GetConfigurations fetches currency and symbol site configuration data.
func (b *Bitfinex) GetConfigurations() error {
return common.ErrNotYetImplemented
}
// GetStatus returns different types of platform information - currently
// supports derivatives pair status only.
func (b *Bitfinex) GetStatus() error {
return common.ErrNotYetImplemented
}
// GetLiquidationFeed returns liquidations. By default it will retrieve the most
// recent liquidations, but time-specific data can be retrieved using
// timestamps.
func (b *Bitfinex) GetLiquidationFeed() error {
return common.ErrNotYetImplemented
}
// GetLeaderboard returns leaderboard standings for unrealized profit (period
// delta), unrealized profit (inception), volume, and realized profit.
// Allowed key values: "plu_diff" for unrealized profit (period delta), "plu"
// for unrealized profit (inception); "vol" for volume; "plr" for realized
// profit
// Allowed time frames are 3h, 1w and 1M
// Allowed symbols are trading pairs (e.g. tBTCUSD, tETHUSD and tGLOBAL:USD)
func (b *Bitfinex) GetLeaderboard(ctx context.Context, key, timeframe, symbol string, sort, limit int, start, end string) ([]LeaderboardEntry, error) {
validLeaderboardKey := func(input string) bool {
switch input {
case LeaderboardUnrealisedProfitPeriodDelta,
LeaderboardUnrealisedProfitInception,
LeaderboardVolume,
LeaderbookRealisedProfit:
return true
default:
return false
}
}
if !validLeaderboardKey(key) {
return nil, errors.New("invalid leaderboard key")
}
path := fmt.Sprintf("%s/%s:%s:%s/hist", bitfinexAPIVersion2+bitfinexLeaderboard,
key,
timeframe,
symbol)
vals := url.Values{}
if sort != 0 {
vals.Set("sort", strconv.Itoa(sort))
}
if limit != 0 {
vals.Set("limit", strconv.Itoa(limit))
}
if start != "" {
vals.Set("start", start)
}
if end != "" {
vals.Set("end", end)
}
path = common.EncodeURLValues(path, vals)
var resp []interface{}
if err := b.SendHTTPRequest(ctx, exchange.RestSpot, path, &resp, leaderBoardReqRate); err != nil {
return nil, err
}
parseTwitterHandle := func(i interface{}) string {
r, ok := i.(string)
if !ok {
return ""
}
return r
}
result := make([]LeaderboardEntry, len(resp))
for x := range resp {
r, ok := resp[x].([]interface{})
if !ok {
return nil, errors.New("unable to type assert leaderboard")
}
if len(r) < 10 {
return nil, errors.New("unexpected leaderboard data length")
}
tm, ok := r[0].(float64)
if !ok {
return nil, errors.New("unable to type assert time")
}
username, ok := r[2].(string)
if !ok {
return nil, errors.New("unable to type assert username")
}
ranking, ok := r[3].(float64)
if !ok {
return nil, errors.New("unable to type assert ranking")
}
value, ok := r[6].(float64)
if !ok {
return nil, errors.New("unable to type assert value")
}
result[x] = LeaderboardEntry{
Timestamp: time.UnixMilli(int64(tm)),
Username: username,
Ranking: int(ranking),
Value: value,
TwitterHandle: parseTwitterHandle(r[9]),
}
}
return result, nil
}
// GetMarketAveragePrice calculates the average execution price for Trading or
// rate for Margin funding
func (b *Bitfinex) GetMarketAveragePrice() error {
return common.ErrNotYetImplemented
}
// GetForeignExchangeRate calculates the exchange rate between two currencies
func (b *Bitfinex) GetForeignExchangeRate() error {
return common.ErrNotYetImplemented
}
// GetAccountFees returns information about your account trading fees
func (b *Bitfinex) GetAccountFees(ctx context.Context) ([]AccountInfo, error) {
var responses []AccountInfo
return responses, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexAccountInfo,
nil,
&responses,
getAccountFees)
}
// GetWithdrawalFees - Gets all fee rates for withdrawals
func (b *Bitfinex) GetWithdrawalFees(ctx context.Context) (AccountFees, error) {
response := AccountFees{}
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexAccountFees,
nil,
&response,
getWithdrawalFees)
}
// GetAccountSummary returns a 30-day summary of your trading volume and return
// on margin funding
func (b *Bitfinex) GetAccountSummary(ctx context.Context) (AccountSummary, error) {
response := AccountSummary{}
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexAccountSummary,
nil,
&response,
getAccountSummary)
}
// NewDeposit returns a new deposit address
// Method - Example methods accepted: “bitcoin”, “litecoin”, “ethereum”,
// “tethers", "ethereumc", "zcash", "monero", "iota", "bcash"
// WalletName - accepted: "exchange", "margin", "funding" (can also use the old labels
// which are "exchange", "trading" and "deposit" respectively). If none is set,
// "funding" will be used by default
// renew - Default is 0. If set to 1, will return a new unused deposit address
func (b *Bitfinex) NewDeposit(ctx context.Context, method, walletName string, renew uint8) (*Deposit, error) {
if walletName == "" {
walletName = "funding"
} else if !common.StringDataCompare(AcceptedWalletNames, walletName) {
return nil,
fmt.Errorf("walletname: [%s] is not allowed, supported: %s",
walletName,
AcceptedWalletNames)
}
req := make(map[string]interface{}, 3)
req["wallet"] = walletName
req["method"] = strings.ToLower(method)
req["op_renew"] = renew
var result []interface{}
err := b.SendAuthenticatedHTTPRequestV2(ctx,
exchange.RestSpot,
http.MethodPost,
bitfinexDepositAddress,
req,
&result,
newDepositAddress)
if err != nil {
return nil, err
}
if len(result) != 8 {
return nil, errors.New("expected result to have a len of 8")
}
depositInfo, ok := result[4].([]interface{})
if !ok || len(depositInfo) != 6 {
return nil, errors.New("unable to get deposit data")
}
depositMethod, ok := depositInfo[1].(string)
if !ok {
return nil, errors.New("unable to type assert depositMethod to string")
}
coin, ok := depositInfo[2].(string)
if !ok {
return nil, errors.New("unable to type assert coin to string")
}
var address, poolAddress string
if depositInfo[5] == nil {
address, ok = depositInfo[4].(string)
if !ok {
return nil, errors.New("unable to type assert address to string")
}
} else {
poolAddress, ok = depositInfo[4].(string)
if !ok {
return nil, errors.New("unable to type assert poolAddress to string")
}
address, ok = depositInfo[5].(string)
if !ok {
return nil, errors.New("unable to type assert address to string")
}
}
return &Deposit{
Method: depositMethod,
CurrencyCode: coin,
Address: address,
PoolAddress: poolAddress,
}, nil
}
// GetKeyPermissions checks the permissions of the key being used to generate
// this request.
func (b *Bitfinex) GetKeyPermissions(ctx context.Context) (KeyPermissions, error) {
response := KeyPermissions{}
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexKeyPermissions,
nil,
&response,
getAccountFees)
}
// GetMarginInfo shows your trading wallet information for margin trading
func (b *Bitfinex) GetMarginInfo(ctx context.Context) ([]MarginInfo, error) {
var response []MarginInfo
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexMarginInfo,
nil,
&response,
getMarginInfo)
}
// GetAccountBalance returns full wallet balance information
func (b *Bitfinex) GetAccountBalance(ctx context.Context) ([]Balance, error) {
var response []Balance
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexBalances,
nil,
&response,
getAccountBalance)
}
// WalletTransfer move available balances between your wallets
// Amount - Amount to move
// Currency - example "BTC"
// WalletFrom - example "exchange"
// WalletTo - example "deposit"
func (b *Bitfinex) WalletTransfer(ctx context.Context, amount float64, currency, walletFrom, walletTo string) (WalletTransfer, error) {
var response []WalletTransfer
req := make(map[string]interface{})
req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64)
req["currency"] = currency
req["walletfrom"] = walletFrom
req["walletto"] = walletTo
err := b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexTransfer,
req,
&response,
walletTransfer)
if err != nil {
return WalletTransfer{}, err
}
if response[0].Status == "error" {
return WalletTransfer{}, errors.New(response[0].Message)
}
return response[0], nil
}
// WithdrawCryptocurrency requests a withdrawal from one of your wallets.
// For FIAT, use WithdrawFIAT
func (b *Bitfinex) WithdrawCryptocurrency(ctx context.Context, wallet, address, paymentID, curr string, amount float64) (Withdrawal, error) {
var response []Withdrawal
req := make(map[string]interface{})
req["withdraw_type"] = strings.ToLower(curr)
req["walletselected"] = wallet
req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64)
req["address"] = address
if paymentID != "" {
req["payment_id"] = paymentID
}
err := b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexWithdrawal,
req,
&response,
withdrawV1)
if err != nil {
return Withdrawal{}, err
}
if response[0].Status == "error" {
return Withdrawal{}, errors.New(response[0].Message)
}
return response[0], nil
}
// WithdrawFIAT Sends an authenticated request to withdraw FIAT currency
func (b *Bitfinex) WithdrawFIAT(ctx context.Context, withdrawalType, walletType string, withdrawRequest *withdraw.Request) (Withdrawal, error) {
var response []Withdrawal
req := make(map[string]interface{})
req["withdraw_type"] = withdrawalType
req["walletselected"] = walletType
req["amount"] = strconv.FormatFloat(withdrawRequest.Amount, 'f', -1, 64)
req["account_name"] = withdrawRequest.Fiat.Bank.AccountName
req["account_number"] = withdrawRequest.Fiat.Bank.AccountNumber
req["bank_name"] = withdrawRequest.Fiat.Bank.BankName
req["bank_address"] = withdrawRequest.Fiat.Bank.BankAddress
req["bank_city"] = withdrawRequest.Fiat.Bank.BankPostalCity
req["bank_country"] = withdrawRequest.Fiat.Bank.BankCountry
req["expressWire"] = withdrawRequest.Fiat.IsExpressWire
req["swift"] = withdrawRequest.Fiat.Bank.SWIFTCode
req["detail_payment"] = withdrawRequest.Description
req["currency"] = withdrawRequest.Currency
req["account_address"] = withdrawRequest.Fiat.Bank.BankAddress
if withdrawRequest.Fiat.RequiresIntermediaryBank {
req["intermediary_bank_name"] = withdrawRequest.Fiat.IntermediaryBankName
req["intermediary_bank_address"] = withdrawRequest.Fiat.IntermediaryBankAddress
req["intermediary_bank_city"] = withdrawRequest.Fiat.IntermediaryBankCity
req["intermediary_bank_country"] = withdrawRequest.Fiat.IntermediaryBankCountry
req["intermediary_bank_account"] = strconv.FormatFloat(withdrawRequest.Fiat.IntermediaryBankAccountNumber, 'f', -1, 64)
req["intermediary_bank_swift"] = withdrawRequest.Fiat.IntermediarySwiftCode
}
err := b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexWithdrawal,
req,
&response,
withdrawV1)
if err != nil {
return Withdrawal{}, err
}
if response[0].Status == "error" {
return Withdrawal{}, errors.New(response[0].Message)
}
return response[0], nil
}
// NewOrder submits a new order and returns a order information
// Major Upgrade needed on this function to include all query params
func (b *Bitfinex) NewOrder(ctx context.Context, currencyPair, orderType string, amount, price float64, buy, hidden bool) (Order, error) {
if !common.StringDataCompare(AcceptedOrderType, orderType) {
return Order{}, fmt.Errorf("order type %s not accepted", orderType)
}
response := Order{}
req := make(map[string]interface{})
req["symbol"] = currencyPair
req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64)
req["price"] = strconv.FormatFloat(price, 'f', -1, 64)
req["type"] = orderType
req["is_hidden"] = hidden
req["side"] = order.Sell.Lower()
if buy {
req["side"] = order.Buy.Lower()
}
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexOrderNew,
req,
&response,
orderV1)
}
// OrderUpdate will send an update signal for an existing order
// and attempt to modify it
func (b *Bitfinex) OrderUpdate(ctx context.Context, orderID, groupID, clientOrderID string, amount, price, leverage float64) (*Order, error) {
req := make(map[string]interface{})
if orderID != "" {
req["id"] = orderID
}
if groupID != "" {
req["gid"] = groupID
}
if clientOrderID != "" {
req["cid"] = clientOrderID
}
req["price"] = strconv.FormatFloat(price, 'f', -1, 64)
req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64)
if leverage > 1 {
req["lev"] = strconv.FormatFloat(leverage, 'f', -1, 64)
}
response := Order{}
return &response, b.SendAuthenticatedHTTPRequestV2(ctx, exchange.RestSpot, http.MethodPost,
bitfinexOrderUpdate,
req,
&response,
orderV1)
}
// NewOrderMulti allows several new orders at once
func (b *Bitfinex) NewOrderMulti(ctx context.Context, orders []PlaceOrder) (OrderMultiResponse, error) {
response := OrderMultiResponse{}
req := make(map[string]interface{})
req["orders"] = orders
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexOrderNewMulti,
req,
&response,
orderMulti)
}
// CancelExistingOrder cancels a single order by OrderID
func (b *Bitfinex) CancelExistingOrder(ctx context.Context, orderID int64) (Order, error) {
response := Order{}
req := make(map[string]interface{})
req["order_id"] = orderID
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexOrderCancel,
req,
&response,
orderMulti)
}
// CancelMultipleOrders cancels multiple orders
func (b *Bitfinex) CancelMultipleOrders(ctx context.Context, orderIDs []int64) (string, error) {
response := GenericResponse{}
req := make(map[string]interface{})
req["order_ids"] = orderIDs
return response.Result, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexOrderCancelMulti,
req,
nil,
orderMulti)
}
// CancelMultipleOrdersV2 cancels multiple orders
func (b *Bitfinex) CancelMultipleOrdersV2(ctx context.Context, orderID, clientOrderID, groupOrderID int64, clientOrderIDDate time.Time, allOrders bool) ([]CancelMultiOrderResponse, error) {
var response []interface{}
req := make(map[string]interface{})
if orderID > 0 {
req["id"] = orderID
}
if clientOrderID > 0 {
req["cid"] = clientOrderID
}
if !clientOrderIDDate.IsZero() {
req["cid_date"] = clientOrderIDDate.Format("2006-01-02")
}
if groupOrderID > 0 {
req["gid"] = groupOrderID
}
if allOrders {
req["all"] = 1
}
err := b.SendAuthenticatedHTTPRequestV2(ctx, exchange.RestSpot, http.MethodPost,
bitfinexOrderCancelMulti,
req,
&response,
orderMulti)
if err != nil {
return nil, err
}
var cancelledOrders []CancelMultiOrderResponse
for x := range response {
cancelledOrdersSlice, ok := response[x].([]interface{})
if !ok {
continue
}
for y := range cancelledOrdersSlice {
cancelledOrderFields, ok := cancelledOrdersSlice[y].([]interface{})
if !ok {
continue
}
var cancelledOrder CancelMultiOrderResponse
for z := range cancelledOrderFields {
switch z {
case 0:
f, ok := cancelledOrderFields[z].(float64)
if !ok {
return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.OrderID")
}
cancelledOrder.OrderID = strconv.FormatFloat(f, 'f', -1, 64)
case 1:
f, ok := cancelledOrderFields[z].(float64)
if !ok {
return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.GroupOrderID")
}
cancelledOrder.GroupOrderID = strconv.FormatFloat(f, 'f', -1, 64)
case 2:
f, ok := cancelledOrderFields[z].(float64)
if !ok {
return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.ClientOrderID")
}
cancelledOrder.ClientOrderID = strconv.FormatFloat(f, 'f', -1, 64)
case 3:
f, ok := cancelledOrderFields[z].(string)
if !ok {
return nil, common.GetTypeAssertError("string", cancelledOrderFields[z], "CancelOrders.Symbol")
}
cancelledOrder.Symbol = f
case 4:
f, ok := cancelledOrderFields[z].(float64)
if !ok {
return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.MTSOfCreation")
}
cancelledOrder.CreatedTime = time.UnixMilli(int64(f))
case 5:
f, ok := cancelledOrderFields[z].(float64)
if !ok {
return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.MTSOfLastUpdate")
}
cancelledOrder.UpdatedTime = time.UnixMilli(int64(f))
case 6:
f, ok := cancelledOrderFields[z].(float64)
if !ok {
return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.Amount")
}
cancelledOrder.Amount = f
case 7:
f, ok := cancelledOrderFields[z].(float64)
if !ok {
return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.OriginalAmount")
}
cancelledOrder.OriginalAmount = f
case 8:
f, ok := cancelledOrderFields[z].(string)
if !ok {
return nil, common.GetTypeAssertError("string", cancelledOrderFields[z], "CancelOrders.OrderType")
}
cancelledOrder.OrderType = f
case 9:
f, ok := cancelledOrderFields[z].(string)
if !ok {
return nil, common.GetTypeAssertError("string", cancelledOrderFields[z], "CancelOrders.PreviousOrderType")
}
cancelledOrder.OriginalOrderType = f
case 12:
f, ok := cancelledOrderFields[z].(string)
if !ok {
return nil, common.GetTypeAssertError("string", cancelledOrderFields[z], "CancelOrders.SumOfOrderFlags")
}
cancelledOrder.OrderFlags = f
case 13:
f, ok := cancelledOrderFields[z].(string)
if !ok {
return nil, common.GetTypeAssertError("string", cancelledOrderFields[z], "CancelOrders.OrderStatuses")
}
cancelledOrder.OrderStatus = f
case 16:
f, ok := cancelledOrderFields[z].(float64)
if !ok {
return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.Price")
}
cancelledOrder.Price = f
case 17:
f, ok := cancelledOrderFields[z].(float64)
if !ok {
return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.AveragePrice")
}
cancelledOrder.AveragePrice = f
case 18:
f, ok := cancelledOrderFields[z].(float64)
if !ok {
return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.TrailingPrice")
}
cancelledOrder.TrailingPrice = f
case 19:
f, ok := cancelledOrderFields[z].(float64)
if !ok {
return nil, common.GetTypeAssertError("float64", cancelledOrderFields[z], "CancelOrders.AuxiliaryLimitPrice")
}
cancelledOrder.AuxLimitPrice = f
}
}
cancelledOrders[y] = cancelledOrder
}
}
return cancelledOrders, nil
}
// CancelAllExistingOrders cancels all active and open orders
func (b *Bitfinex) CancelAllExistingOrders(ctx context.Context) (string, error) {
response := GenericResponse{}
return response.Result, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexOrderCancelAll,
nil,
nil,
orderMulti)
}
// ReplaceOrder replaces an older order with a new order
func (b *Bitfinex) ReplaceOrder(ctx context.Context, orderID int64, symbol string, amount, price float64, buy bool, orderType string, hidden bool) (Order, error) {
response := Order{}
req := make(map[string]interface{})
req["order_id"] = orderID
req["symbol"] = symbol
req["amount"] = strconv.FormatFloat(amount, 'f', -1, 64)
req["price"] = strconv.FormatFloat(price, 'f', -1, 64)
req["exchange"] = "bitfinex"
req["type"] = orderType
req["is_hidden"] = hidden
if buy {
req["side"] = order.Buy.Lower()
} else {
req["side"] = order.Sell.Lower()
}
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexOrderCancelReplace,
req,
&response,
orderMulti)
}
// GetOrderStatus returns order status information
func (b *Bitfinex) GetOrderStatus(ctx context.Context, orderID int64) (Order, error) {
orderStatus := Order{}
req := make(map[string]interface{})
req["order_id"] = orderID
return orderStatus, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexOrderStatus,
req,
&orderStatus,
orderMulti)
}
// GetInactiveOrders returns order status information
func (b *Bitfinex) GetInactiveOrders(ctx context.Context, symbol string, ids ...int64) ([]Order, error) {
var response []Order
req := make(map[string]interface{})
req["limit"] = 2500
if len(ids) > 0 {
req["ids"] = ids
}
return response, b.SendAuthenticatedHTTPRequestV2(
ctx,
exchange.RestSpot,
http.MethodPost,
bitfinexV2Auth+"r/"+bitfinexOrders+"/"+symbol+"/"+bitfinexInactiveOrders,
req,
&response,
orderMulti)
}
// GetOpenOrders returns all active orders and statuses
func (b *Bitfinex) GetOpenOrders(ctx context.Context, ids ...int64) ([]Order, error) {
var response []Order
req := make(map[string]interface{})
if len(ids) > 0 {
req["ids"] = ids
}
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexOrders,
req,
&response,
orderMulti)
}
// GetActivePositions returns an array of active positions
func (b *Bitfinex) GetActivePositions(ctx context.Context) ([]Position, error) {
var response []Position
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexPositions,
nil,
&response,
orderMulti)
}
// ClaimPosition allows positions to be claimed
func (b *Bitfinex) ClaimPosition(ctx context.Context, positionID int) (Position, error) {
response := Position{}
req := make(map[string]interface{})
req["position_id"] = positionID
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexClaimPosition,
nil,
nil,
orderMulti)
}
// GetBalanceHistory returns balance history for the account
func (b *Bitfinex) GetBalanceHistory(ctx context.Context, symbol string, timeSince, timeUntil time.Time, limit int, wallet string) ([]BalanceHistory, error) {
var response []BalanceHistory
req := make(map[string]interface{})
req["currency"] = symbol
if !timeSince.IsZero() {
req["since"] = timeSince
}
if !timeUntil.IsZero() {
req["until"] = timeUntil
}
if limit > 0 {
req["limit"] = limit
}
if wallet != "" {
req["wallet"] = wallet
}
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexHistory,
req,
&response,
orderMulti)
}
// 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 [][]interface{}
req := make(map[string]interface{})
req["currency"] = symbol
if method != "" {
req["method"] = method
}
if !timeSince.IsZero() {
req["since"] = timeSince
}
if !timeUntil.IsZero() {
req["until"] = timeUntil
}
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
}
// GetTradeHistory returns past executed trades
func (b *Bitfinex) GetTradeHistory(ctx context.Context, currencyPair string, timestamp, until time.Time, limit, reverse int) ([]TradeHistory, error) {
var response []TradeHistory
req := make(map[string]interface{})
req["currency"] = currencyPair
req["timestamp"] = timestamp
if !until.IsZero() {
req["until"] = until
}
if limit > 0 {
req["limit"] = limit
}
if reverse > 0 {
req["reverse"] = reverse
}
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexTradeHistory,
req,
&response,
orderMulti)
}
// NewOffer submits a new offer
func (b *Bitfinex) NewOffer(ctx context.Context, symbol string, amount, rate float64, period int64, direction string) (Offer, error) {
response := Offer{}
req := make(map[string]interface{})
req["currency"] = symbol
req["amount"] = amount
req["rate"] = rate
req["period"] = period
req["direction"] = direction
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexOfferNew,
req,
&response,
orderMulti)
}
// CancelOffer cancels offer by offerID
func (b *Bitfinex) CancelOffer(ctx context.Context, offerID int64) (Offer, error) {
response := Offer{}
req := make(map[string]interface{})
req["offer_id"] = offerID
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexOfferCancel,
req,
&response,
orderMulti)
}
// GetOfferStatus checks offer status whether it has been cancelled, execute or
// is still active
func (b *Bitfinex) GetOfferStatus(ctx context.Context, offerID int64) (Offer, error) {
response := Offer{}
req := make(map[string]interface{})
req["offer_id"] = offerID
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexOrderStatus,
req,
&response,
orderMulti)
}
// GetActiveCredits returns all available credits
func (b *Bitfinex) GetActiveCredits(ctx context.Context) ([]Offer, error) {
var response []Offer
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexActiveCredits,
nil,
&response,
orderMulti)
}
// GetActiveOffers returns all current active offers
func (b *Bitfinex) GetActiveOffers(ctx context.Context) ([]Offer, error) {
var response []Offer
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexOffers,
nil,
&response,
orderMulti)
}
// GetActiveMarginFunding returns an array of active margin funds
func (b *Bitfinex) GetActiveMarginFunding(ctx context.Context) ([]MarginFunds, error) {
var response []MarginFunds
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexMarginActiveFunds,
nil,
&response,
orderMulti)
}
// GetUnusedMarginFunds returns an array of funding borrowed but not currently
// used
func (b *Bitfinex) GetUnusedMarginFunds(ctx context.Context) ([]MarginFunds, error) {
var response []MarginFunds
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexMarginUnusedFunds,
nil,
&response,
orderMulti)
}
// GetMarginTotalTakenFunds returns an array of active funding used in a
// position
func (b *Bitfinex) GetMarginTotalTakenFunds(ctx context.Context) ([]MarginTotalTakenFunds, error) {
var response []MarginTotalTakenFunds
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexMarginTotalFunds,
nil,
&response,
orderMulti)
}
// CloseMarginFunding closes an unused or used taken fund
func (b *Bitfinex) CloseMarginFunding(ctx context.Context, swapID int64) (Offer, error) {
response := Offer{}
req := make(map[string]interface{})
req["swap_id"] = swapID
return response, b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, http.MethodPost,
bitfinexMarginClose,
req,
&response,
closeFunding)
}
// SendHTTPRequest sends an unauthenticated request
func (b *Bitfinex) SendHTTPRequest(ctx context.Context, ep exchange.URL, path string, result interface{}, e request.EndpointLimit) error {
endpoint, err := b.API.Endpoints.GetURL(ep)
if err != nil {
return err
}
item := &request.Item{
Method: http.MethodGet,
Path: endpoint + path,
Result: result,
Verbose: b.Verbose,
HTTPDebugging: b.HTTPDebugging,
HTTPRecording: b.HTTPRecording}
return b.SendPayload(ctx, e, func() (*request.Item, error) {
return item, nil
}, request.UnauthenticatedRequest)
}
// SendAuthenticatedHTTPRequest sends an authenticated http request and json
// unmarshals result to a supplied variable
func (b *Bitfinex) SendAuthenticatedHTTPRequest(ctx context.Context, ep exchange.URL, method, path string, params map[string]interface{}, result interface{}, endpoint request.EndpointLimit) error {
creds, err := b.GetCredentials(ctx)
if err != nil {
return err
}
ePoint, err := b.API.Endpoints.GetURL(ep)
if err != nil {
return err
}
fullPath := ePoint + bitfinexAPIVersion + path
return b.SendPayload(ctx, endpoint, func() (*request.Item, error) {
req := make(map[string]interface{})
req["request"] = bitfinexAPIVersion + path
req["nonce"] = b.Requester.GetNonce(nonce.UnixNano).String()
for key, value := range params {
req[key] = value
}
PayloadJSON, err := json.Marshal(req)
if err != nil {
return nil, err
}
PayloadBase64 := crypto.Base64Encode(PayloadJSON)
hmac, err := crypto.GetHMAC(crypto.HashSHA512_384,
[]byte(PayloadBase64),
[]byte(creds.Secret))
if err != nil {
return nil, err
}
headers := make(map[string]string)
headers["X-BFX-APIKEY"] = creds.Key
headers["X-BFX-PAYLOAD"] = PayloadBase64
headers["X-BFX-SIGNATURE"] = crypto.HexEncodeToString(hmac)
return &request.Item{
Method: method,
Path: fullPath,
Headers: headers,
Result: result,
NonceEnabled: true,
Verbose: b.Verbose,
HTTPDebugging: b.HTTPDebugging,
HTTPRecording: b.HTTPRecording}, nil
}, request.AuthenticatedRequest)
}
// SendAuthenticatedHTTPRequestV2 sends an authenticated http request and json
// unmarshals result to a supplied variable
func (b *Bitfinex) SendAuthenticatedHTTPRequestV2(ctx context.Context, ep exchange.URL, method, path string, params map[string]interface{}, result interface{}, endpoint request.EndpointLimit) error {
creds, err := b.GetCredentials(ctx)
if err != nil {
return err
}
ePoint, err := b.API.Endpoints.GetURL(ep)
if err != nil {
return err
}
return b.SendPayload(ctx, endpoint, func() (*request.Item, error) {
var body io.Reader
var payload []byte
if len(params) != 0 {
payload, err = json.Marshal(params)
if err != nil {
return nil, err
}
body = bytes.NewBuffer(payload)
}
n := strconv.FormatInt(time.Now().Unix()*1e9, 10)
headers := make(map[string]string)
headers["Content-Type"] = "application/json"
headers["Accept"] = "application/json"
headers["bfx-apikey"] = creds.Key
headers["bfx-nonce"] = n
sig := "/api" + bitfinexAPIVersion2 + path + n + string(payload)
hmac, err := crypto.GetHMAC(
crypto.HashSHA512_384,
[]byte(sig),
[]byte(creds.Secret),
)
if err != nil {
return nil, err
}
headers["bfx-signature"] = crypto.HexEncodeToString(hmac)
return &request.Item{
Method: method,
Path: ePoint + bitfinexAPIVersion2 + path,
Headers: headers,
Body: body,
Result: result,
NonceEnabled: true,
Verbose: b.Verbose,
HTTPDebugging: b.HTTPDebugging,
HTTPRecording: b.HTTPRecording,
}, nil
}, request.AuthenticatedRequest)
}
// GetFee returns an estimate of fee based on type of transaction
func (b *Bitfinex) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
var fee float64
switch feeBuilder.FeeType {
case exchange.CryptocurrencyTradeFee:
accountInfos, err := b.GetAccountFees(ctx)
if err != nil {
return 0, err
}
fee, err = b.CalculateTradingFee(accountInfos,
feeBuilder.PurchasePrice,
feeBuilder.Amount,
feeBuilder.Pair.Base,
feeBuilder.IsMaker)
if err != nil {
return 0, err
}
case exchange.CryptocurrencyDepositFee:
//TODO: fee is charged when < $1000USD is transferred, need to infer value in some way
fee = 0
case exchange.CryptocurrencyWithdrawalFee:
acc, err := b.GetWithdrawalFees(ctx)
if err != nil {
return 0, err
}
fee, err = b.GetCryptocurrencyWithdrawalFee(feeBuilder.Pair.Base, acc)
if err != nil {
return 0, err
}
case exchange.InternationalBankDepositFee:
fee = getInternationalBankDepositFee(feeBuilder.Amount)
case exchange.InternationalBankWithdrawalFee:
fee = getInternationalBankWithdrawalFee(feeBuilder.Amount)
case exchange.OfflineTradeFee:
fee = getOfflineTradeFee(feeBuilder.PurchasePrice, feeBuilder.Amount)
}
if fee < 0 {
fee = 0
}
return fee, nil
}
// getOfflineTradeFee calculates the worst case-scenario trading fee
// does not require an API request, requires manual updating
func getOfflineTradeFee(price, amount float64) float64 {
return 0.001 * price * amount
}
// 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
}
return fee, nil
}
func getInternationalBankDepositFee(amount float64) float64 {
return 0.001 * amount
}
func getInternationalBankWithdrawalFee(amount float64) float64 {
return 0.001 * amount
}
// CalculateTradingFee returns an estimate of fee based on type of whether is maker or taker fee
func (b *Bitfinex) CalculateTradingFee(i []AccountInfo, purchasePrice, amount float64, c currency.Code, isMaker bool) (fee float64, err error) {
for x := range i {
for y := range i[x].Fees {
if c.String() == i[x].Fees[y].Pairs {
if isMaker {
fee = i[x].Fees[y].MakerFees
} else {
fee = i[x].Fees[y].TakerFees
}
break
}
}
if fee > 0 {
break
}
}
return (fee / 100) * purchasePrice * amount, err
}
// PopulateAcceptableMethods retrieves all accepted currency strings and
// populates a map to check
func (b *Bitfinex) PopulateAcceptableMethods(ctx context.Context) error {
if acceptableMethods.loaded() {
return nil
}
var response [][][]interface{}
err := b.SendHTTPRequest(ctx,
exchange.RestSpot,
bitfinexAPIVersion2+bitfinexDepositMethod,
&response,
configs)
if err != nil {
return err
}
if len(response) == 0 {
return errors.New("response contains no data cannot populate acceptable method map")
}
data := response[0]
storeData := make(map[string][]string)
for x := range data {
if len(data[x]) == 0 {
return errors.New("data should not be empty")
}
name, ok := data[x][0].(string)
if !ok {
return errors.New("unable to type assert name")
}
var availOptions []string
options, ok := data[x][1].([]interface{})
if !ok {
return errors.New("unable to type assert options")
}
for x := range options {
o, ok := options[x].(string)
if !ok {
return errors.New("unable to type assert option to string")
}
availOptions = append(availOptions, o)
}
storeData[name] = availOptions
}
acceptableMethods.load(storeData)
return nil
}