mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 08:45:16 +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
|
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.
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 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{})
|
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