Skip to content

Commit

Permalink
Bitstamp: Add new trading fees API endpoint; refine fee calcuations a…
Browse files Browse the repository at this point in the history
…nd test coverage (thrasher-corp#1289)

* Bitstamp: WIP fixing trading fees

* Bitstamp/amended the TestGetFee test and currected the getTradingFee function

* Bitstamp: TestGetFee implemented for maker and taker

* Bitfinex: added a wrapped error

* Bitstamp: GetTradingFee and test updated, fetched from the API

Progresses thrasher-corp#1271

* Bitstamp: minor changes- whitespace removal and comment added

* Bitstamp: fixed lint issues in TestGetBalance and TestGetTransactions

* Bitstamp:Typo in types TradingFee comment

* Bitstamp: GetAccountTradingFees at view the fees on all pairs

	modified:   exchanges/bitstamp/bitstamp.go

* Bitstamp: returning the TradingFee info instead of just MakerTakerFees

* Bitstamp:TestGetAccountTradingFees + TestGetAccountTradingFees added,data structure amended to match the outcome

* Bitstamp:error and a test for empty pair added in GetAccountTradingFee

* Bitstamp: RM the whitespace linter error
  • Loading branch information
Beadko authored and shazbert committed Nov 9, 2023
1 parent 0834b63 commit 03cd1b2
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 68 deletions.
77 changes: 39 additions & 38 deletions exchanges/bitstamp/bitstamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const (
bitstampAPIOrderbook = "order_book"
bitstampAPITransactions = "transactions"
bitstampAPIEURUSD = "eur_usd"
bitstampAPITradingFees = "fees/trading"
bitstampAPIBalance = "balance"
bitstampAPIUserTransactions = "user_transactions"
bitstampAPIOHLC = "ohlc"
Expand Down Expand Up @@ -67,15 +68,11 @@ func (b *Bitstamp) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder)

switch feeBuilder.FeeType {
case exchange.CryptocurrencyTradeFee:
balance, err := b.GetBalance(ctx)
tradingFee, err := b.getTradingFee(ctx, feeBuilder)
if err != nil {
return 0, err
return 0, fmt.Errorf("error getting trading fee: %w", err)
}
fee = b.CalculateTradingFee(feeBuilder.Pair.Base,
feeBuilder.Pair.Quote,
feeBuilder.PurchasePrice,
feeBuilder.Amount,
balance)
fee = tradingFee
case exchange.CryptocurrencyDepositFee:
fee = 0
case exchange.InternationalBankDepositFee:
Expand All @@ -91,6 +88,41 @@ func (b *Bitstamp) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder)
return fee, nil
}

// GetTradingFee returns a trading fee based on a currency
func (b *Bitstamp) getTradingFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
tradingFees, err := b.GetAccountTradingFee(ctx, feeBuilder.Pair)
if err != nil {
return 0, err
}
fees := tradingFees.Fees
fee := fees.Taker
if feeBuilder.IsMaker {
fee = fees.Maker
}
return fee / 100 * feeBuilder.PurchasePrice * feeBuilder.Amount, nil
}

// GetAccountTradingFee returns a TradingFee for a pair
func (b *Bitstamp) GetAccountTradingFee(ctx context.Context, pair currency.Pair) (TradingFees, error) {
path := bitstampAPITradingFees + "/" + strings.ToLower(pair.String())

var resp TradingFees
if pair.IsEmpty() {
return resp, currency.ErrCurrencyPairEmpty
}
err := b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, path, true, nil, &resp)

return resp, err
}

// GetAccountTradingFees returns a slice of TradingFee
func (b *Bitstamp) GetAccountTradingFees(ctx context.Context) ([]TradingFees, error) {
path := bitstampAPITradingFees
var resp []TradingFees
err := b.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, path, true, nil, &resp)
return resp, err
}

// getOfflineTradeFee calculates the worst case-scenario trading fee
func getOfflineTradeFee(price, amount float64) float64 {
return 0.0025 * price * amount
Expand Down Expand Up @@ -119,22 +151,6 @@ func getInternationalBankDepositFee(amount float64) float64 {
return fee
}

// CalculateTradingFee returns fee on a currency pair
func (b *Bitstamp) CalculateTradingFee(base, quote currency.Code, purchasePrice, amount float64, balances Balances) float64 {
var fee float64
if v, ok := balances[base.String()]; ok {
switch quote {
case currency.BTC:
fee = v.BTCFee
case currency.USD:
fee = v.USDFee
case currency.EUR:
fee = v.EURFee
}
}
return fee * purchasePrice * amount
}

// GetTicker returns ticker information
func (b *Bitstamp) GetTicker(ctx context.Context, currency string, hourly bool) (*Ticker, error) {
response := Ticker{}
Expand Down Expand Up @@ -251,21 +267,6 @@ func (b *Bitstamp) GetBalance(ctx context.Context) (Balances, error) {
Reserved: reserved,
WithdrawalFee: withdrawalFee,
}
switch strings.ToUpper(curr) {
case currency.USD.String():
eurFee, _ := strconv.ParseFloat(balance[curr+"eur_fee"], 64)
currBalance.EURFee = eurFee
case currency.EUR.String():
usdFee, _ := strconv.ParseFloat(balance[curr+"usd_fee"], 64)
currBalance.USDFee = usdFee
default:
btcFee, _ := strconv.ParseFloat(balance[curr+"btc_fee"], 64)
currBalance.BTCFee = btcFee
eurFee, _ := strconv.ParseFloat(balance[curr+"eur_fee"], 64)
currBalance.EURFee = eurFee
usdFee, _ := strconv.ParseFloat(balance[curr+"usd_fee"], 64)
currBalance.USDFee = usdFee
}
balances[strings.ToUpper(curr)] = currBalance
}
return balances, nil
Expand Down
12 changes: 9 additions & 3 deletions exchanges/bitstamp/bitstamp_live_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,15 @@ func TestMain(m *testing.M) {
log.Fatal("Bitstamp Setup() init error", err)
}
bitstampConfig.API.AuthenticatedSupport = true
bitstampConfig.API.Credentials.Key = apiKey
bitstampConfig.API.Credentials.Secret = apiSecret
bitstampConfig.API.Credentials.ClientID = customerID
if apiKey != "" {
bitstampConfig.API.Credentials.Key = apiKey
}
if apiSecret != "" {
bitstampConfig.API.Credentials.Secret = apiSecret
}
if customerID != "" {
bitstampConfig.API.Credentials.ClientID = customerID
}
b.SetDefaults()
b.Websocket = sharedtestvalues.NewTestWebsocket()
err = b.Setup(bitstampConfig)
Expand Down
72 changes: 48 additions & 24 deletions exchanges/bitstamp/bitstamp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ var b = &Bitstamp{}

func setFeeBuilder() *exchange.FeeBuilder {
return &exchange.FeeBuilder{
Amount: 1,
Amount: 5,
FeeType: exchange.CryptocurrencyTradeFee,
Pair: currency.NewPair(currency.BTC, currency.LTC),
PurchasePrice: 1,
Pair: currency.NewPair(currency.LTC, currency.BTC),
PurchasePrice: 1800,
}
}

Expand Down Expand Up @@ -99,8 +99,21 @@ func TestGetFee(t *testing.T) {
// CryptocurrencyTradeFee IsMaker
feeBuilder = setFeeBuilder()
feeBuilder.IsMaker = true
if _, err := b.GetFee(context.Background(), feeBuilder); err != nil {
fee, err := b.GetFee(context.Background(), feeBuilder)
if err != nil {
t.Error(err)
} else if expected := 0.003 * feeBuilder.PurchasePrice * feeBuilder.Amount; fee != expected {
t.Errorf("Bitstamp GetFee wrong Maker fee; Pair: %s Expected: %v Got: %v", feeBuilder.Pair, expected, fee)
}

// CryptocurrencyTradeFee IsTaker
feeBuilder = setFeeBuilder()
feeBuilder.IsMaker = false
fee, err = b.GetFee(context.Background(), feeBuilder)
if err != nil {
t.Error(err)
} else if expected := 0.002 * feeBuilder.PurchasePrice * feeBuilder.Amount; fee != expected {
t.Errorf("Bitstamp GetFee wrong Taker fee; Pair: %s Expected: %v Got: %v", feeBuilder.Pair, expected, fee)
}

// CryptocurrencyTradeFee Negative purchase price
Expand Down Expand Up @@ -141,28 +154,44 @@ func TestGetFee(t *testing.T) {
}
}

func TestCalculateTradingFee(t *testing.T) {
func TestGetAccountTradingFee(t *testing.T) {
t.Parallel()

newBalance := make(Balances)
newBalance["BTC"] = Balance{
USDFee: 1,
EURFee: 0,
if !mockTests {
sharedtestvalues.SkipTestIfCredentialsUnset(t, b)
}

if resp := b.CalculateTradingFee(currency.BTC, currency.USD, 0, 0, newBalance); resp != 0 {
t.Error("GetFee() error")
}
if resp := b.CalculateTradingFee(currency.BTC, currency.USD, 2, 2, newBalance); resp != float64(4) {
t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(4), resp)
fee, err := b.GetAccountTradingFee(context.Background(), currency.NewPair(currency.LTC, currency.BTC))
if assert.NoError(t, err, "GetAccountTradingFee should not error") {
if mockTests {
assert.Positive(t, fee.Fees.Maker, "Maker should be positive")
assert.Positive(t, fee.Fees.Taker, "Taker should be positive")
}
assert.NotEmpty(t, fee.Symbol, "Symbol should not be empty")
assert.Equal(t, fee.Symbol, "ltcbtc", "Symbol should be correct")
}
if resp := b.CalculateTradingFee(currency.BTC, currency.EUR, 2, 2, newBalance); resp != float64(0) {
t.Errorf("GetFee() error. Expected: %f, Received: %f", float64(0), resp)

_, err = b.GetAccountTradingFee(context.Background(), currency.EMPTYPAIR)
assert.ErrorIs(t, err, currency.ErrCurrencyPairEmpty, "Should get back the right error")
}

func TestGetAccountTradingFees(t *testing.T) {
t.Parallel()

if !mockTests {
sharedtestvalues.SkipTestIfCredentialsUnset(t, b)
}

dummy1, dummy2 := currency.NewCode(""), currency.NewCode("")
if resp := b.CalculateTradingFee(dummy1, dummy2, 0, 0, newBalance); resp != 0 {
t.Error("GetFee() error")
fees, err := b.GetAccountTradingFees(context.Background())
if assert.NoError(t, err, "GetAccountTradingFee should not error") {
if assert.Positive(t, len(fees), "Should get back multiple fees") {
fee := fees[0]
assert.NotEmpty(t, fee.Symbol, "Should get back a symbol")
if mockTests {
assert.Positive(t, fee.Fees.Maker, "Maker should be positive")
assert.Positive(t, fee.Fees.Taker, "Taker should be positive")
}
}
}
}

Expand Down Expand Up @@ -287,14 +316,12 @@ func TestGetBalance(t *testing.T) {
Balance: 1337.42,
Reserved: 1295.00,
WithdrawalFee: 5.0,
USDFee: 0,
},
"BTC": {
Available: 9.1,
Balance: 11.2,
Reserved: 2.1,
WithdrawalFee: 0.00050000,
USDFee: 0.25,
},
} {
if got, ok := bal[k]; !ok {
Expand All @@ -312,9 +339,6 @@ func TestGetBalance(t *testing.T) {
if got.WithdrawalFee != e.WithdrawalFee {
t.Errorf("Incorrect WithdrawalFee for %s; Expected: %v Got: %v", k, e.WithdrawalFee, got.WithdrawalFee)
}
if got.USDFee != e.USDFee {
t.Errorf("Incorrect USDFee for %s; Expected: %v Got: %v", k, e.USDFee, got.USDFee)
}
}
}
}
Expand Down
15 changes: 12 additions & 3 deletions exchanges/bitstamp/bitstamp_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,15 +75,24 @@ type EURUSDConversionRate struct {
Sell float64 `json:"sell,string"`
}

// TradingFees holds trading fee information
type TradingFees struct {
Symbol string `json:"currency_pair"`
Fees MakerTakerFees `json:"fees"`
}

// MakerTakerFees holds maker and taker fee information
type MakerTakerFees struct {
Maker float64 `json:"maker,string"`
Taker float64 `json:"taker,string"`
}

// Balance stores the balance info
type Balance struct {
Available float64
Balance float64
Reserved float64
WithdrawalFee float64
BTCFee float64 // for cryptocurrency pairs
USDFee float64
EURFee float64
}

// Balances holds full balance information with the supplied APIKEYS
Expand Down
50 changes: 50 additions & 0 deletions testdata/http_mock/bitstamp/bitstamp.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,56 @@
}
]
},
"/api/v2/fees/trading/ltcbtc/": {
"POST": [
{
"data": {
"currency_pair": "ltcbtc",
"fees":
{
"maker": "0.3",
"taker": "0.2"
}
},
"queryString": "",
"bodyParams": "key=\u0026nonce=1690610275327495000\u0026signature=B619E1AEF317B95D3BB28025F36EEF94CABCDE425CE3E13E8A1861C0A9777F2A",
"headers": {
"Content-Type": [
"application/x-www-form-urlencoded"
]
}
}
]
},
"/api/v2/fees/trading/": {
"POST": [
{
"data": [
{
"currency_pair":"ltcbtc",
"fees": {
"maker": "0.3",
"taker": "0.2"
}
},
{
"currency_pair":"btcusd",
"fees": {
"maker": "0.3",
"taker": "0.2"
}
}
],
"queryString": "",
"bodyParams": "key=&nonce=1696325404980771000&signature=85B2B7E60567E352241BD98BDD08C0A435C2A77CB8F386739DD471BFFFA6C660",
"headers": {
"Content-Type": [
"application/x-www-form-urlencoded"
]
}
}
]
},
"/api/v2/buy/market/btcusd/": {
"POST": [
{
Expand Down

0 comments on commit 03cd1b2

Please sign in to comment.