pkg/exchange: implement trade event

This commit is contained in:
Edwin 2023-08-10 15:13:09 +08:00
parent affff32599
commit 54e7065d8a
6 changed files with 600 additions and 14 deletions

View File

@ -36,19 +36,19 @@ func (m *MockMarketInfoProvider) EXPECT() *MockMarketInfoProviderMockRecorder {
return m.recorder return m.recorder
} }
// GetFeeRates mocks base method. // GetAllFeeRates mocks base method.
func (m *MockMarketInfoProvider) GetFeeRates(arg0 context.Context) (bybitapi.FeeRates, error) { func (m *MockMarketInfoProvider) GetAllFeeRates(arg0 context.Context) (bybitapi.FeeRates, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetFeeRates", arg0) ret := m.ctrl.Call(m, "GetAllFeeRates", arg0)
ret0, _ := ret[0].(bybitapi.FeeRates) ret0, _ := ret[0].(bybitapi.FeeRates)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// GetFeeRates indicates an expected call of GetFeeRates. // GetAllFeeRates indicates an expected call of GetAllFeeRates.
func (mr *MockMarketInfoProviderMockRecorder) GetFeeRates(arg0 interface{}) *gomock.Call { func (mr *MockMarketInfoProviderMockRecorder) GetAllFeeRates(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFeeRates", reflect.TypeOf((*MockMarketInfoProvider)(nil).GetFeeRates), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllFeeRates", reflect.TypeOf((*MockMarketInfoProvider)(nil).GetAllFeeRates), arg0)
} }
// QueryMarkets mocks base method. // QueryMarkets mocks base method.

View File

@ -29,7 +29,7 @@ var (
//go:generate mockgen -destination=mocks/stream.go -package=mocks . MarketInfoProvider //go:generate mockgen -destination=mocks/stream.go -package=mocks . MarketInfoProvider
type MarketInfoProvider interface { type MarketInfoProvider interface {
GetFeeRates(ctx context.Context) (bybitapi.FeeRates, error) GetAllFeeRates(ctx context.Context) (bybitapi.FeeRates, error)
QueryMarkets(ctx context.Context) (types.MarketMap, error) QueryMarkets(ctx context.Context) (types.MarketMap, error)
} }
@ -46,6 +46,7 @@ type Stream struct {
walletEventCallbacks []func(e []bybitapi.WalletBalances) walletEventCallbacks []func(e []bybitapi.WalletBalances)
kLineEventCallbacks []func(e KLineEvent) kLineEventCallbacks []func(e KLineEvent)
orderEventCallbacks []func(e []OrderEvent) orderEventCallbacks []func(e []OrderEvent)
tradeEventCallbacks []func(e []TradeEvent)
} }
func NewStream(key, secret string, marketProvider MarketInfoProvider) *Stream { func NewStream(key, secret string, marketProvider MarketInfoProvider) *Stream {
@ -68,6 +69,7 @@ func NewStream(key, secret string, marketProvider MarketInfoProvider) *Stream {
stream.OnKLineEvent(stream.handleKLineEvent) stream.OnKLineEvent(stream.handleKLineEvent)
stream.OnWalletEvent(stream.handleWalletEvent) stream.OnWalletEvent(stream.handleWalletEvent)
stream.OnOrderEvent(stream.handleOrderEvent) stream.OnOrderEvent(stream.handleOrderEvent)
stream.OnTradeEvent(stream.handleTradeEvent)
return stream return stream
} }
@ -99,6 +101,10 @@ func (s *Stream) dispatchEvent(event interface{}) {
case []OrderEvent: case []OrderEvent:
s.EmitOrderEvent(e) s.EmitOrderEvent(e)
case []TradeEvent:
s.EmitTradeEvent(e)
} }
} }
@ -149,6 +155,10 @@ func (s *Stream) parseWebSocketEvent(in []byte) (interface{}, error) {
var orders []OrderEvent var orders []OrderEvent
return orders, json.Unmarshal(e.WebSocketTopicEvent.Data, &orders) return orders, json.Unmarshal(e.WebSocketTopicEvent.Data, &orders)
case TopicTypeTrade:
var trades []TradeEvent
return trades, json.Unmarshal(e.WebSocketTopicEvent.Data, &trades)
} }
} }
@ -237,6 +247,7 @@ func (s *Stream) handlerConnect() {
Args: []string{ Args: []string{
string(TopicTypeWallet), string(TopicTypeWallet),
string(TopicTypeOrder), string(TopicTypeOrder),
string(TopicTypeTrade),
}, },
}); err != nil { }); err != nil {
log.WithError(err).Error("failed to send subscription request") log.WithError(err).Error("failed to send subscription request")
@ -320,6 +331,23 @@ func (s *Stream) handleKLineEvent(klineEvent KLineEvent) {
} }
} }
func (s *Stream) handleTradeEvent(events []TradeEvent) {
for _, event := range events {
feeRate, found := s.symbolFeeDetails[event.Symbol]
if !found {
log.Warnf("unexpected symbol found, fee rate not supported, symbol: %s", event.Symbol)
continue
}
gTrade, err := event.toGlobalTrade(*feeRate)
if err != nil {
log.WithError(err).Errorf("unable to convert: %+v", event)
continue
}
s.StandardStream.EmitTradeUpdate(*gTrade)
}
}
type symbolFeeDetail struct { type symbolFeeDetail struct {
bybitapi.FeeRate bybitapi.FeeRate
@ -330,7 +358,7 @@ type symbolFeeDetail struct {
// getAllFeeRates retrieves all fee rates from the Bybit API and then fetches markets to ensure the base coin and quote coin // getAllFeeRates retrieves all fee rates from the Bybit API and then fetches markets to ensure the base coin and quote coin
// are correct. // are correct.
func (e *Stream) getAllFeeRates(ctx context.Context) error { func (e *Stream) getAllFeeRates(ctx context.Context) error {
feeRates, err := e.marketProvider.GetFeeRates(ctx) feeRates, err := e.marketProvider.GetAllFeeRates(ctx)
if err != nil { if err != nil {
return fmt.Errorf("failed to call get fee rates: %w", err) return fmt.Errorf("failed to call get fee rates: %w", err)
} }

View File

@ -45,3 +45,13 @@ func (s *Stream) EmitOrderEvent(e []OrderEvent) {
cb(e) cb(e)
} }
} }
func (s *Stream) OnTradeEvent(cb func(e []TradeEvent)) {
s.tradeEventCallbacks = append(s.tradeEventCallbacks, cb)
}
func (s *Stream) EmitTradeEvent(e []TradeEvent) {
for _, cb := range s.tradeEventCallbacks {
cb(e)
}
}

View File

@ -99,6 +99,17 @@ func TestStream(t *testing.T) {
c := make(chan struct{}) c := make(chan struct{})
<-c <-c
}) })
t.Run("trade test", func(t *testing.T) {
err := s.Connect(context.Background())
assert.NoError(t, err)
s.OnTradeUpdate(func(trade types.Trade) {
t.Log("got update", trade)
})
c := make(chan struct{})
<-c
})
} }
func TestStream_parseWebSocketEvent(t *testing.T) { func TestStream_parseWebSocketEvent(t *testing.T) {
@ -364,7 +375,7 @@ func TestStream_getFeeRate(t *testing.T) {
}, },
} }
mockMarketProvider.EXPECT().GetFeeRates(ctx).Return(feeRates, nil).Times(1) mockMarketProvider.EXPECT().GetAllFeeRates(ctx).Return(feeRates, nil).Times(1)
mockMarketProvider.EXPECT().QueryMarkets(ctx).Return(mkts, nil).Times(1) mockMarketProvider.EXPECT().QueryMarkets(ctx).Return(mkts, nil).Times(1)
expFeeRates := map[string]*symbolFeeDetail{ expFeeRates := map[string]*symbolFeeDetail{
@ -379,7 +390,7 @@ func TestStream_getFeeRate(t *testing.T) {
QuoteCoin: "USDT", QuoteCoin: "USDT",
}, },
} }
err := s.getFeeRate(ctx) err := s.getAllFeeRates(ctx)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, expFeeRates, s.symbolFeeDetails) assert.Equal(t, expFeeRates, s.symbolFeeDetails)
}) })
@ -411,10 +422,10 @@ func TestStream_getFeeRate(t *testing.T) {
}, },
} }
mockMarketProvider.EXPECT().GetFeeRates(ctx).Return(feeRates, nil).Times(1) mockMarketProvider.EXPECT().GetAllFeeRates(ctx).Return(feeRates, nil).Times(1)
mockMarketProvider.EXPECT().QueryMarkets(ctx).Return(nil, unknownErr).Times(1) mockMarketProvider.EXPECT().QueryMarkets(ctx).Return(nil, unknownErr).Times(1)
err := s.getFeeRate(ctx) err := s.getAllFeeRates(ctx)
assert.Equal(t, fmt.Errorf("failed to get markets: %w", unknownErr), err) assert.Equal(t, fmt.Errorf("failed to get markets: %w", unknownErr), err)
assert.Equal(t, map[string]*symbolFeeDetail(nil), s.symbolFeeDetails) assert.Equal(t, map[string]*symbolFeeDetail(nil), s.symbolFeeDetails)
}) })
@ -427,9 +438,9 @@ func TestStream_getFeeRate(t *testing.T) {
ctx := context.Background() ctx := context.Background()
mockMarketProvider.EXPECT().GetFeeRates(ctx).Return(bybitapi.FeeRates{}, unknownErr).Times(1) mockMarketProvider.EXPECT().GetAllFeeRates(ctx).Return(bybitapi.FeeRates{}, unknownErr).Times(1)
err := s.getFeeRate(ctx) err := s.getAllFeeRates(ctx)
assert.Equal(t, fmt.Errorf("failed to call get fee rates: %w", unknownErr), err) assert.Equal(t, fmt.Errorf("failed to call get fee rates: %w", unknownErr), err)
assert.Equal(t, map[string]*symbolFeeDetail(nil), s.symbolFeeDetails) assert.Equal(t, map[string]*symbolFeeDetail(nil), s.symbolFeeDetails)
}) })

View File

@ -3,6 +3,7 @@ package bybit
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"strconv"
"strings" "strings"
"github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi" "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi"
@ -83,6 +84,7 @@ const (
TopicTypeWallet TopicType = "wallet" TopicTypeWallet TopicType = "wallet"
TopicTypeOrder TopicType = "order" TopicTypeOrder TopicType = "order"
TopicTypeKLine TopicType = "kline" TopicTypeKLine TopicType = "kline"
TopicTypeTrade TopicType = "execution"
) )
type DataType string type DataType string
@ -210,3 +212,146 @@ func (k *KLine) toGlobalKLine(symbol string) (types.KLine, error) {
Closed: k.Confirm, Closed: k.Confirm,
}, nil }, nil
} }
type TradeEvent struct {
// linear and inverse order id format: 42f4f364-82e1-49d3-ad1d-cd8cf9aa308d (UUID format)
// spot: 1468264727470772736 (only numbers)
// we only use spot trading.
OrderId string `json:"orderId"`
OrderLinkId string `json:"orderLinkId"`
Category bybitapi.Category `json:"category"`
Symbol string `json:"symbol"`
ExecId string `json:"execId"`
ExecPrice fixedpoint.Value `json:"execPrice"`
ExecQty fixedpoint.Value `json:"execQty"`
// Is maker order. true: maker, false: taker
IsMaker bool `json:"isMaker"`
// Paradigm block trade ID
BlockTradeId string `json:"blockTradeId"`
// Order type. Market,Limit
OrderType bybitapi.OrderType `json:"orderType"`
// Side. Buy,Sell
Side bybitapi.Side `json:"side"`
// Executed timestampms
ExecTime types.MillisecondTimestamp `json:"execTime"`
// Closed position size
ClosedSize fixedpoint.Value `json:"closedSize"`
/* The following parameters do not support SPOT trading. */
// Executed trading fee. You can get spot fee currency instruction here. Normal spot is not supported
ExecFee fixedpoint.Value `json:"execFee"`
// Executed type. Normal spot is not supported
ExecType string `json:"execType"`
// Executed order value. Normal spot is not supported
ExecValue fixedpoint.Value `json:"execValue"`
// Trading fee rate. Normal spot is not supported
FeeRate fixedpoint.Value `json:"feeRate"`
// The remaining qty not executed. Normal spot is not supported
LeavesQty fixedpoint.Value `json:"leavesQty"`
// Order price. Normal spot is not supported
OrderPrice fixedpoint.Value `json:"orderPrice"`
// Order qty. Normal spot is not supported
OrderQty fixedpoint.Value `json:"orderQty"`
// Stop order type. If the order is not stop order, any type is not returned. Normal spot is not supported
StopOrderType string `json:"stopOrderType"`
// Whether to borrow. Unified spot only. 0: false, 1: true. . Normal spot is not supported, always 0
IsLeverage string `json:"isLeverage"`
// Implied volatility of mark price. Valid for option
MarkIv string `json:"markIv"`
// The mark price of the symbol when executing. Valid for option
MarkPrice fixedpoint.Value `json:"markPrice"`
// The index price of the symbol when executing. Valid for option
IndexPrice fixedpoint.Value `json:"indexPrice"`
// The underlying price of the symbol when executing. Valid for option
UnderlyingPrice fixedpoint.Value `json:"underlyingPrice"`
// Implied volatility. Valid for option
TradeIv string `json:"tradeIv"`
}
func (t *TradeEvent) toGlobalTrade(symbolFee symbolFeeDetail) (*types.Trade, error) {
if t.Category != bybitapi.CategorySpot {
return nil, fmt.Errorf("unexected category: %s", t.Category)
}
side, err := toGlobalSideType(t.Side)
if err != nil {
return nil, err
}
orderIdNum, err := strconv.ParseUint(t.OrderId, 10, 64)
if err != nil {
return nil, fmt.Errorf("unexpected order id: %s, err: %w", t.OrderId, err)
}
execIdNum, err := strconv.ParseUint(t.ExecId, 10, 64)
if err != nil {
return nil, fmt.Errorf("unexpected exec id: %s, err: %w", t.ExecId, err)
}
trade := &types.Trade{
ID: execIdNum,
OrderID: orderIdNum,
Exchange: types.ExchangeBybit,
Price: t.ExecPrice,
Quantity: t.ExecQty,
QuoteQuantity: t.ExecPrice.Mul(t.ExecQty),
Symbol: t.Symbol,
Side: side,
IsBuyer: side == types.SideTypeBuy,
IsMaker: t.IsMaker,
Time: types.Time(t.ExecTime),
Fee: fixedpoint.Zero,
FeeCurrency: "",
}
trade.FeeCurrency, trade.Fee = calculateFee(*t, symbolFee)
return trade, nil
}
// CalculateFee given isMaker to get the fee currency and fee.
// https://bybit-exchange.github.io/docs/v5/enum#spot-fee-currency-instruction
//
// with the example of BTCUSDT:
//
// Is makerFeeRate positive?
//
// - TRUE
// Side = Buy -> base currency (BTC)
// Side = Sell -> quote currency (USDT)
//
// - FALSE
// IsMakerOrder = TRUE
// -> Side = Buy -> quote currency (USDT)
// -> Side = Sell -> base currency (BTC)
//
// IsMakerOrder = FALSE
// -> Side = Buy -> base currency (BTC)
// -> Side = Sell -> quote currency (USDT)
func calculateFee(t TradeEvent, feeDetail symbolFeeDetail) (string, fixedpoint.Value) {
if feeDetail.MakerFeeRate.Sign() > 0 || !t.IsMaker {
if t.Side == bybitapi.SideBuy {
return feeDetail.BaseCoin, baseCoinAsFee(t, feeDetail)
}
return feeDetail.QuoteCoin, quoteCoinAsFee(t, feeDetail)
}
if t.Side == bybitapi.SideBuy {
return feeDetail.QuoteCoin, quoteCoinAsFee(t, feeDetail)
}
return feeDetail.BaseCoin, baseCoinAsFee(t, feeDetail)
}
func baseCoinAsFee(t TradeEvent, feeDetail symbolFeeDetail) fixedpoint.Value {
if t.IsMaker {
return feeDetail.MakerFeeRate.Mul(t.ExecQty)
}
return feeDetail.TakerFeeRate.Mul(t.ExecQty)
}
func quoteCoinAsFee(t TradeEvent, feeDetail symbolFeeDetail) fixedpoint.Value {
baseFee := t.ExecPrice.Mul(t.ExecQty)
if t.IsMaker {
return feeDetail.MakerFeeRate.Mul(baseFee)
}
return feeDetail.TakerFeeRate.Mul(baseFee)
}

View File

@ -460,3 +460,395 @@ func TestKLine_toGlobalKLine(t *testing.T) {
assert.Equal(t, gKline, types.KLine{}) assert.Equal(t, gKline, types.KLine{})
}) })
} }
func TestTradeEvent_toGlobalTrade(t *testing.T) {
/*
{
"category":"spot",
"symbol":"BTCUSDT",
"execFee":"",
"execId":"2100000000032905730",
"execPrice":"28829.7600",
"execQty":"0.002289",
"execType":"",
"execValue":"",
"isMaker":false,
"feeRate":"",
"tradeIv":"",
"markIv":"",
"blockTradeId":"",
"markPrice":"",
"indexPrice":"",
"underlyingPrice":"",
"leavesQty":"",
"orderId":"1482125285219500288",
"orderLinkId":"1691419101980",
"orderPrice":"",
"orderQty":"",
"orderType":"",
"stopOrderType":"",
"side":"Buy",
"execTime":"1691419102282",
"isLeverage":"0"
}
*/
t.Run("succeeds", func(t *testing.T) {
symbolFee := symbolFeeDetail{
FeeRate: bybitapi.FeeRate{
Symbol: "BTCUSDT",
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
MakerFeeRate: fixedpoint.NewFromFloat(0.002),
},
BaseCoin: "BTC",
QuoteCoin: "USDT",
}
qty := fixedpoint.NewFromFloat(0.002289)
price := fixedpoint.NewFromFloat(28829.7600)
timeNow := time.Now().Truncate(time.Second)
expTrade := &types.Trade{
ID: 2100000000032905730,
OrderID: 1482125285219500288,
Exchange: types.ExchangeBybit,
Price: price,
Quantity: qty,
QuoteQuantity: qty.Mul(price),
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
IsBuyer: true,
IsMaker: false,
Time: types.Time(timeNow),
Fee: symbolFee.FeeRate.TakerFeeRate.Mul(qty),
FeeCurrency: "BTC",
}
tradeEvent := TradeEvent{
OrderId: fmt.Sprintf("%d", expTrade.OrderID),
OrderLinkId: "1691419101980",
Category: "spot",
Symbol: fmt.Sprintf("%s", expTrade.Symbol),
ExecId: fmt.Sprintf("%d", expTrade.ID),
ExecPrice: expTrade.Price,
ExecQty: expTrade.Quantity,
IsMaker: false,
BlockTradeId: "",
OrderType: "",
Side: bybitapi.SideBuy,
ExecTime: types.MillisecondTimestamp(timeNow),
ClosedSize: fixedpoint.NewFromInt(0),
ExecFee: fixedpoint.NewFromInt(0),
ExecType: "",
ExecValue: fixedpoint.NewFromInt(0),
FeeRate: fixedpoint.NewFromInt(0),
LeavesQty: fixedpoint.NewFromInt(0),
OrderPrice: fixedpoint.NewFromInt(0),
OrderQty: fixedpoint.NewFromInt(0),
StopOrderType: "",
IsLeverage: "0",
MarkIv: "",
MarkPrice: fixedpoint.NewFromInt(0),
IndexPrice: fixedpoint.NewFromInt(0),
UnderlyingPrice: fixedpoint.NewFromInt(0),
TradeIv: "",
}
actualTrade, err := tradeEvent.toGlobalTrade(symbolFee)
assert.NoError(t, err)
assert.Equal(t, expTrade, actualTrade)
})
t.Run("unexpected category", func(t *testing.T) {
tradeEvent := TradeEvent{
Category: "test-spot",
}
actualTrade, err := tradeEvent.toGlobalTrade(symbolFeeDetail{})
assert.Equal(t, fmt.Errorf("unexected category: %s", tradeEvent.Category), err)
assert.Nil(t, actualTrade)
})
t.Run("unexpected side", func(t *testing.T) {
tradeEvent := TradeEvent{
Category: "spot",
Side: bybitapi.Side("BOTH"),
}
actualTrade, err := tradeEvent.toGlobalTrade(symbolFeeDetail{})
assert.Equal(t, fmt.Errorf("unexpected side: BOTH"), err)
assert.Nil(t, actualTrade)
})
t.Run("unexpected order id", func(t *testing.T) {
tradeEvent := TradeEvent{
Category: "spot",
Side: bybitapi.SideBuy,
OrderId: "ABCD3123",
}
_, nerr := strconv.ParseUint(tradeEvent.OrderId, 10, 64)
actualTrade, err := tradeEvent.toGlobalTrade(symbolFeeDetail{})
assert.Equal(t, fmt.Errorf("unexpected order id: %s, err: %w", tradeEvent.OrderId, nerr), err)
assert.Nil(t, actualTrade)
})
t.Run("unexpected exec id", func(t *testing.T) {
tradeEvent := TradeEvent{
Category: "spot",
Side: bybitapi.SideBuy,
OrderId: "3123",
ExecId: "ABC3123",
}
_, nerr := strconv.ParseUint(tradeEvent.ExecId, 10, 64)
actualTrade, err := tradeEvent.toGlobalTrade(symbolFeeDetail{})
assert.Equal(t, fmt.Errorf("unexpected exec id: %s, err: %w", tradeEvent.ExecId, nerr), err)
assert.Nil(t, actualTrade)
})
}
func TestTradeEvent_CalculateFee(t *testing.T) {
t.Run("maker fee positive, maker, buyer", func(t *testing.T) {
symbolFee := symbolFeeDetail{
FeeRate: bybitapi.FeeRate{
Symbol: "BTCUSDT",
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
MakerFeeRate: fixedpoint.NewFromFloat(0.002),
},
BaseCoin: "BTC",
QuoteCoin: "USDT",
}
qty := fixedpoint.NewFromFloat(0.010000)
price := fixedpoint.NewFromFloat(28830.8100)
trade := &TradeEvent{
ExecPrice: price,
ExecQty: qty,
IsMaker: true,
Side: bybitapi.SideBuy,
}
feeCurrency, fee := calculateFee(*trade, symbolFee)
assert.Equal(t, feeCurrency, "BTC")
assert.Equal(t, fee, qty.Mul(symbolFee.FeeRate.MakerFeeRate))
})
t.Run("maker fee positive, maker, seller", func(t *testing.T) {
symbolFee := symbolFeeDetail{
FeeRate: bybitapi.FeeRate{
Symbol: "BTCUSDT",
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
MakerFeeRate: fixedpoint.NewFromFloat(0.002),
},
BaseCoin: "BTC",
QuoteCoin: "USDT",
}
qty := fixedpoint.NewFromFloat(0.010000)
price := fixedpoint.NewFromFloat(28830.8099)
trade := &TradeEvent{
ExecPrice: price,
ExecQty: qty,
IsMaker: true,
Side: bybitapi.SideSell,
}
feeCurrency, fee := calculateFee(*trade, symbolFee)
assert.Equal(t, feeCurrency, "USDT")
assert.Equal(t, fee, qty.Mul(price).Mul(symbolFee.FeeRate.MakerFeeRate))
})
t.Run("maker fee positive, taker, buyer", func(t *testing.T) {
symbolFee := symbolFeeDetail{
FeeRate: bybitapi.FeeRate{
Symbol: "BTCUSDT",
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
MakerFeeRate: fixedpoint.NewFromFloat(0.002),
},
BaseCoin: "BTC",
QuoteCoin: "USDT",
}
qty := fixedpoint.NewFromFloat(0.010000)
price := fixedpoint.NewFromFloat(28830.8100)
trade := &TradeEvent{
ExecPrice: price,
ExecQty: qty,
IsMaker: false,
Side: bybitapi.SideBuy,
}
feeCurrency, fee := calculateFee(*trade, symbolFee)
assert.Equal(t, feeCurrency, "BTC")
assert.Equal(t, fee, qty.Mul(symbolFee.FeeRate.TakerFeeRate))
})
t.Run("maker fee positive, taker, seller", func(t *testing.T) {
symbolFee := symbolFeeDetail{
FeeRate: bybitapi.FeeRate{
Symbol: "BTCUSDT",
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
MakerFeeRate: fixedpoint.NewFromFloat(0.002),
},
BaseCoin: "BTC",
QuoteCoin: "USDT",
}
qty := fixedpoint.NewFromFloat(0.010000)
price := fixedpoint.NewFromFloat(28830.8099)
trade := &TradeEvent{
ExecPrice: price,
ExecQty: qty,
IsMaker: false,
Side: bybitapi.SideSell,
}
feeCurrency, fee := calculateFee(*trade, symbolFee)
assert.Equal(t, feeCurrency, "USDT")
assert.Equal(t, fee, qty.Mul(price).Mul(symbolFee.FeeRate.TakerFeeRate))
})
t.Run("maker fee negative, maker, buyer", func(t *testing.T) {
symbolFee := symbolFeeDetail{
FeeRate: bybitapi.FeeRate{
Symbol: "BTCUSDT",
TakerFeeRate: fixedpoint.NewFromFloat(-0.001),
MakerFeeRate: fixedpoint.NewFromFloat(-0.002),
},
BaseCoin: "BTC",
QuoteCoin: "USDT",
}
qty := fixedpoint.NewFromFloat(0.002289)
price := fixedpoint.NewFromFloat(28829.7600)
trade := &TradeEvent{
ExecPrice: price,
ExecQty: qty,
IsMaker: true,
Side: bybitapi.SideBuy,
}
feeCurrency, fee := calculateFee(*trade, symbolFee)
assert.Equal(t, feeCurrency, "USDT")
assert.Equal(t, fee, qty.Mul(price).Mul(symbolFee.FeeRate.MakerFeeRate))
})
t.Run("maker fee negative, maker, seller", func(t *testing.T) {
symbolFee := symbolFeeDetail{
FeeRate: bybitapi.FeeRate{
Symbol: "BTCUSDT",
TakerFeeRate: fixedpoint.NewFromFloat(-0.001),
MakerFeeRate: fixedpoint.NewFromFloat(-0.002),
},
BaseCoin: "BTC",
QuoteCoin: "USDT",
}
qty := fixedpoint.NewFromFloat(0.002289)
price := fixedpoint.NewFromFloat(28829.7600)
trade := &TradeEvent{
ExecPrice: price,
ExecQty: qty,
IsMaker: true,
Side: bybitapi.SideSell,
}
feeCurrency, fee := calculateFee(*trade, symbolFee)
assert.Equal(t, feeCurrency, "BTC")
assert.Equal(t, fee, qty.Mul(symbolFee.FeeRate.MakerFeeRate))
})
t.Run("maker fee negative, taker, buyer", func(t *testing.T) {
symbolFee := symbolFeeDetail{
FeeRate: bybitapi.FeeRate{
Symbol: "BTCUSDT",
TakerFeeRate: fixedpoint.NewFromFloat(-0.001),
MakerFeeRate: fixedpoint.NewFromFloat(-0.002),
},
BaseCoin: "BTC",
QuoteCoin: "USDT",
}
qty := fixedpoint.NewFromFloat(0.002289)
price := fixedpoint.NewFromFloat(28829.7600)
trade := &TradeEvent{
ExecPrice: price,
ExecQty: qty,
IsMaker: false,
Side: bybitapi.SideBuy,
}
feeCurrency, fee := calculateFee(*trade, symbolFee)
assert.Equal(t, feeCurrency, "BTC")
assert.Equal(t, fee, qty.Mul(symbolFee.FeeRate.TakerFeeRate))
})
t.Run("maker fee negative, taker, seller", func(t *testing.T) {
symbolFee := symbolFeeDetail{
FeeRate: bybitapi.FeeRate{
Symbol: "BTCUSDT",
TakerFeeRate: fixedpoint.NewFromFloat(-0.001),
MakerFeeRate: fixedpoint.NewFromFloat(-0.002),
},
BaseCoin: "BTC",
QuoteCoin: "USDT",
}
qty := fixedpoint.NewFromFloat(0.002289)
price := fixedpoint.NewFromFloat(28829.7600)
trade := &TradeEvent{
ExecPrice: price,
ExecQty: qty,
IsMaker: false,
Side: bybitapi.SideSell,
}
feeCurrency, fee := calculateFee(*trade, symbolFee)
assert.Equal(t, feeCurrency, "USDT")
assert.Equal(t, fee, qty.Mul(price).Mul(symbolFee.FeeRate.TakerFeeRate))
})
}
func TestTradeEvent_baseCoinAsFee(t *testing.T) {
symbolFee := symbolFeeDetail{
FeeRate: bybitapi.FeeRate{
Symbol: "BTCUSDT",
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
MakerFeeRate: fixedpoint.NewFromFloat(0.002),
},
BaseCoin: "BTC",
QuoteCoin: "USDT",
}
qty := fixedpoint.NewFromFloat(0.002289)
price := fixedpoint.NewFromFloat(28829.7600)
trade := &TradeEvent{
ExecPrice: price,
ExecQty: qty,
IsMaker: false,
}
assert.Equal(t, symbolFee.FeeRate.TakerFeeRate.Mul(qty), baseCoinAsFee(*trade, symbolFee))
trade.IsMaker = true
assert.Equal(t, symbolFee.FeeRate.MakerFeeRate.Mul(qty), baseCoinAsFee(*trade, symbolFee))
}
func TestTradeEvent_quoteCoinAsFee(t *testing.T) {
symbolFee := symbolFeeDetail{
FeeRate: bybitapi.FeeRate{
Symbol: "BTCUSDT",
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
MakerFeeRate: fixedpoint.NewFromFloat(0.002),
},
BaseCoin: "BTC",
QuoteCoin: "USDT",
}
qty := fixedpoint.NewFromFloat(0.002289)
price := fixedpoint.NewFromFloat(28829.7600)
trade := &TradeEvent{
ExecPrice: price,
ExecQty: qty,
IsMaker: false,
}
assert.Equal(t, symbolFee.FeeRate.TakerFeeRate.Mul(qty.Mul(price)), quoteCoinAsFee(*trade, symbolFee))
trade.IsMaker = true
assert.Equal(t, symbolFee.FeeRate.MakerFeeRate.Mul(qty.Mul(price)), quoteCoinAsFee(*trade, symbolFee))
}