mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 06:53:52 +00:00
pkg/exchange: implement trade event
This commit is contained in:
parent
affff32599
commit
54e7065d8a
|
@ -36,19 +36,19 @@ func (m *MockMarketInfoProvider) EXPECT() *MockMarketInfoProviderMockRecorder {
|
|||
return m.recorder
|
||||
}
|
||||
|
||||
// GetFeeRates mocks base method.
|
||||
func (m *MockMarketInfoProvider) GetFeeRates(arg0 context.Context) (bybitapi.FeeRates, error) {
|
||||
// GetAllFeeRates mocks base method.
|
||||
func (m *MockMarketInfoProvider) GetAllFeeRates(arg0 context.Context) (bybitapi.FeeRates, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetFeeRates", arg0)
|
||||
ret := m.ctrl.Call(m, "GetAllFeeRates", arg0)
|
||||
ret0, _ := ret[0].(bybitapi.FeeRates)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetFeeRates indicates an expected call of GetFeeRates.
|
||||
func (mr *MockMarketInfoProviderMockRecorder) GetFeeRates(arg0 interface{}) *gomock.Call {
|
||||
// GetAllFeeRates indicates an expected call of GetAllFeeRates.
|
||||
func (mr *MockMarketInfoProviderMockRecorder) GetAllFeeRates(arg0 interface{}) *gomock.Call {
|
||||
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.
|
||||
|
|
|
@ -29,7 +29,7 @@ var (
|
|||
|
||||
//go:generate mockgen -destination=mocks/stream.go -package=mocks . MarketInfoProvider
|
||||
type MarketInfoProvider interface {
|
||||
GetFeeRates(ctx context.Context) (bybitapi.FeeRates, error)
|
||||
GetAllFeeRates(ctx context.Context) (bybitapi.FeeRates, error)
|
||||
QueryMarkets(ctx context.Context) (types.MarketMap, error)
|
||||
}
|
||||
|
||||
|
@ -46,6 +46,7 @@ type Stream struct {
|
|||
walletEventCallbacks []func(e []bybitapi.WalletBalances)
|
||||
kLineEventCallbacks []func(e KLineEvent)
|
||||
orderEventCallbacks []func(e []OrderEvent)
|
||||
tradeEventCallbacks []func(e []TradeEvent)
|
||||
}
|
||||
|
||||
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.OnWalletEvent(stream.handleWalletEvent)
|
||||
stream.OnOrderEvent(stream.handleOrderEvent)
|
||||
stream.OnTradeEvent(stream.handleTradeEvent)
|
||||
return stream
|
||||
}
|
||||
|
||||
|
@ -99,6 +101,10 @@ func (s *Stream) dispatchEvent(event interface{}) {
|
|||
|
||||
case []OrderEvent:
|
||||
s.EmitOrderEvent(e)
|
||||
|
||||
case []TradeEvent:
|
||||
s.EmitTradeEvent(e)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -149,6 +155,10 @@ func (s *Stream) parseWebSocketEvent(in []byte) (interface{}, error) {
|
|||
var orders []OrderEvent
|
||||
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{
|
||||
string(TopicTypeWallet),
|
||||
string(TopicTypeOrder),
|
||||
string(TopicTypeTrade),
|
||||
},
|
||||
}); err != nil {
|
||||
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 {
|
||||
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
|
||||
// are correct.
|
||||
func (e *Stream) getAllFeeRates(ctx context.Context) error {
|
||||
feeRates, err := e.marketProvider.GetFeeRates(ctx)
|
||||
feeRates, err := e.marketProvider.GetAllFeeRates(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to call get fee rates: %w", err)
|
||||
}
|
||||
|
|
|
@ -45,3 +45,13 @@ func (s *Stream) EmitOrderEvent(e []OrderEvent) {
|
|||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,6 +99,17 @@ func TestStream(t *testing.T) {
|
|||
c := make(chan struct{})
|
||||
<-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) {
|
||||
|
@ -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)
|
||||
|
||||
expFeeRates := map[string]*symbolFeeDetail{
|
||||
|
@ -379,7 +390,7 @@ func TestStream_getFeeRate(t *testing.T) {
|
|||
QuoteCoin: "USDT",
|
||||
},
|
||||
}
|
||||
err := s.getFeeRate(ctx)
|
||||
err := s.getAllFeeRates(ctx)
|
||||
assert.NoError(t, err)
|
||||
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)
|
||||
|
||||
err := s.getFeeRate(ctx)
|
||||
err := s.getAllFeeRates(ctx)
|
||||
assert.Equal(t, fmt.Errorf("failed to get markets: %w", unknownErr), err)
|
||||
assert.Equal(t, map[string]*symbolFeeDetail(nil), s.symbolFeeDetails)
|
||||
})
|
||||
|
@ -427,9 +438,9 @@ func TestStream_getFeeRate(t *testing.T) {
|
|||
|
||||
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, map[string]*symbolFeeDetail(nil), s.symbolFeeDetails)
|
||||
})
|
||||
|
|
|
@ -3,6 +3,7 @@ package bybit
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi"
|
||||
|
@ -83,6 +84,7 @@ const (
|
|||
TopicTypeWallet TopicType = "wallet"
|
||||
TopicTypeOrder TopicType = "order"
|
||||
TopicTypeKLine TopicType = "kline"
|
||||
TopicTypeTrade TopicType = "execution"
|
||||
)
|
||||
|
||||
type DataType string
|
||||
|
@ -210,3 +212,146 @@ func (k *KLine) toGlobalKLine(symbol string) (types.KLine, error) {
|
|||
Closed: k.Confirm,
|
||||
}, 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 timestamp(ms)
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -460,3 +460,395 @@ func TestKLine_toGlobalKLine(t *testing.T) {
|
|||
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))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user