mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-25 08:15:15 +00:00
Merge pull request #1759 from c9s/edwin/bybit/add-v5-execution-request
FEATURE: [bybit] integrate the v5 trade history
This commit is contained in:
commit
7e908c3ff7
|
@ -325,6 +325,42 @@ func v3ToGlobalTrade(trade v3.Trade) (*types.Trade, error) {
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toGlobalTrade(trade bybitapi.Trade, feeDetail SymbolFeeDetail) (*types.Trade, error) {
|
||||||
|
side, err := toGlobalSideType(trade.Side)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unexpected side: %s, err: %w", trade.Side, err)
|
||||||
|
}
|
||||||
|
orderIdNum, err := strconv.ParseUint(trade.OrderId, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unexpected order id: %s, err: %w", trade.OrderId, err)
|
||||||
|
}
|
||||||
|
tradeIdNum, err := strconv.ParseUint(trade.ExecId, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unexpected trade id: %s, err: %w", trade.ExecId, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fc, _ := calculateFee(trade, feeDetail)
|
||||||
|
|
||||||
|
return &types.Trade{
|
||||||
|
ID: tradeIdNum,
|
||||||
|
OrderID: orderIdNum,
|
||||||
|
Exchange: types.ExchangeBybit,
|
||||||
|
Price: trade.ExecPrice,
|
||||||
|
Quantity: trade.ExecQty,
|
||||||
|
QuoteQuantity: trade.ExecPrice.Mul(trade.ExecQty),
|
||||||
|
Symbol: trade.Symbol,
|
||||||
|
Side: side,
|
||||||
|
IsBuyer: side == types.SideTypeBuy,
|
||||||
|
IsMaker: trade.IsMaker,
|
||||||
|
Time: types.Time(trade.ExecTime),
|
||||||
|
Fee: trade.ExecFee,
|
||||||
|
FeeCurrency: fc,
|
||||||
|
IsMargin: false,
|
||||||
|
IsFutures: false,
|
||||||
|
IsIsolated: false,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func toGlobalBalanceMap(events []bybitapi.WalletBalances) types.BalanceMap {
|
func toGlobalBalanceMap(events []bybitapi.WalletBalances) types.BalanceMap {
|
||||||
bm := types.BalanceMap{}
|
bm := types.BalanceMap{}
|
||||||
for _, event := range events {
|
for _, event := range events {
|
||||||
|
|
|
@ -12,17 +12,17 @@ import (
|
||||||
"golang.org/x/time/rate"
|
"golang.org/x/time/rate"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi"
|
"github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi"
|
||||||
v3 "github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi/v3"
|
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
maxOrderIdLen = 36
|
maxOrderIdLen = 36
|
||||||
defaultQueryLimit = 50
|
defaultQueryLimit = 50
|
||||||
defaultKLineLimit = 1000
|
defaultQueryTradeLimit = 100
|
||||||
|
defaultKLineLimit = 1000
|
||||||
|
|
||||||
halfYearDuration = 6 * 30 * 24 * time.Hour
|
queryTradeDurationLimit = 7 * 24 * time.Hour
|
||||||
)
|
)
|
||||||
|
|
||||||
// https://bybit-exchange.github.io/docs/zh-TW/v5/rate-limit
|
// https://bybit-exchange.github.io/docs/zh-TW/v5/rate-limit
|
||||||
|
@ -52,7 +52,13 @@ var (
|
||||||
type Exchange struct {
|
type Exchange struct {
|
||||||
key, secret string
|
key, secret string
|
||||||
client *bybitapi.RestClient
|
client *bybitapi.RestClient
|
||||||
v3client *v3.Client
|
marketsInfo types.MarketMap
|
||||||
|
|
||||||
|
// feeRateProvider provides the fee rate and fee currency for each symbol.
|
||||||
|
// Because the bybit exchange does not provide a fee currency on traditional SPOT accounts, we need to query the marker
|
||||||
|
// fee rate to get the fee currency.
|
||||||
|
// https://bybit-exchange.github.io/docs/v5/enum#spot-fee-currency-instruction
|
||||||
|
FeeRatePoller
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(key, secret string) (*Exchange, error) {
|
func New(key, secret string) (*Exchange, error) {
|
||||||
|
@ -60,18 +66,25 @@ func New(key, secret string) (*Exchange, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
ex := &Exchange{
|
||||||
if len(key) > 0 && len(secret) > 0 {
|
|
||||||
client.Auth(key, secret)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Exchange{
|
|
||||||
key: key,
|
key: key,
|
||||||
// pragma: allowlist nextline secret
|
// pragma: allowlist nextline secret
|
||||||
secret: secret,
|
secret: secret,
|
||||||
client: client,
|
client: client,
|
||||||
v3client: v3.NewClient(client),
|
}
|
||||||
}, nil
|
if len(key) > 0 && len(secret) > 0 {
|
||||||
|
client.Auth(key, secret)
|
||||||
|
ex.FeeRatePoller = newFeeRatePoller(ex)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeoutCause(context.Background(), 5*time.Second, errors.New("query markets timeout"))
|
||||||
|
defer cancel()
|
||||||
|
ex.marketsInfo, err = ex.QueryMarkets(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to query markets, err: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ex, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exchange) Name() types.ExchangeName {
|
func (e *Exchange) Name() types.ExchangeName {
|
||||||
|
@ -226,35 +239,14 @@ func (e *Exchange) QueryOrderTrades(ctx context.Context, q types.OrderQuery) (tr
|
||||||
if len(q.OrderID) == 0 {
|
if len(q.OrderID) == 0 {
|
||||||
return nil, errors.New("orderID is required parameter")
|
return nil, errors.New("orderID is required parameter")
|
||||||
}
|
}
|
||||||
req := e.v3client.NewGetTradesRequest().OrderId(q.OrderID)
|
req := e.client.NewGetExecutionListRequest().OrderId(q.OrderID)
|
||||||
|
|
||||||
if len(q.Symbol) != 0 {
|
if len(q.Symbol) != 0 {
|
||||||
req.Symbol(q.Symbol)
|
req.Symbol(q.Symbol)
|
||||||
}
|
}
|
||||||
|
req.Limit(defaultQueryTradeLimit)
|
||||||
|
|
||||||
if err := queryOrderTradeRateLimiter.Wait(ctx); err != nil {
|
return e.queryTrades(ctx, req)
|
||||||
return nil, fmt.Errorf("trade rate limiter wait error: %w", err)
|
|
||||||
}
|
|
||||||
response, err := req.Do(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to query order trades, err: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var errs error
|
|
||||||
for _, trade := range response.List {
|
|
||||||
res, err := v3ToGlobalTrade(trade)
|
|
||||||
if err != nil {
|
|
||||||
errs = multierr.Append(errs, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
trades = append(trades, *res)
|
|
||||||
}
|
|
||||||
|
|
||||||
if errs != nil {
|
|
||||||
return nil, errs
|
|
||||||
}
|
|
||||||
|
|
||||||
return trades, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) {
|
func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (*types.Order, error) {
|
||||||
|
@ -432,32 +424,65 @@ func (e *Exchange) QueryClosedOrders(
|
||||||
return types.SortOrdersAscending(orders), nil
|
return types.SortOrdersAscending(orders), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
func (e *Exchange) queryTrades(ctx context.Context, req *bybitapi.GetExecutionListRequest) (trades []types.Trade, err error) {
|
||||||
QueryTrades queries trades by time range or trade id range.
|
cursor := ""
|
||||||
If options.StartTime is not specified, you can only query for records in the last 7 days.
|
for {
|
||||||
If you want to query for records older than 7 days, options.StartTime is required.
|
if len(cursor) != 0 {
|
||||||
It supports to query records up to 180 days.
|
req = req.Cursor(cursor)
|
||||||
|
}
|
||||||
|
|
||||||
** Here includes MakerRebate. If needed, let's discuss how to modify it to return in trade. **
|
res, err := req.Do(ctx)
|
||||||
** StartTime and EndTime are inclusive. **
|
if err != nil {
|
||||||
** StartTime and EndTime cannot exceed 180 days. **
|
return nil, fmt.Errorf("failed to query trades, err: %w", err)
|
||||||
** StartTime, EndTime, FromTradeId can be used together. **
|
}
|
||||||
** If the `FromTradeId` is passed, and `ToTradeId` is null, then the result is sorted by tradeId in `ascend`.
|
|
||||||
Otherwise, the result is sorted by tradeId in `descend`. **
|
for _, trade := range res.List {
|
||||||
|
feeRate, err := pollAndGetFeeRate(ctx, trade.Symbol, e.FeeRatePoller, e.marketsInfo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get fee rate, err: %v", err)
|
||||||
|
}
|
||||||
|
trade, err := toGlobalTrade(trade, feeRate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert trade, err: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
trades = append(trades, *trade)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(res.NextPageCursor) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cursor = res.NextPageCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
return trades, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
QueryTrades queries trades by time range.
|
||||||
|
** startTime and endTime are not passed, return 7 days by default **
|
||||||
|
** Only startTime is passed, return range between startTime and startTime+7 days **
|
||||||
|
** Only endTime is passed, return range between endTime-7 days and endTime **
|
||||||
|
** If both are passed, the rule is endTime - startTime <= 7 days **
|
||||||
*/
|
*/
|
||||||
func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) {
|
func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *types.TradeQueryOptions) (trades []types.Trade, err error) {
|
||||||
// using v3 client, since the v5 API does not support feeCurrency.
|
req := e.client.NewGetExecutionListRequest()
|
||||||
req := e.v3client.NewGetTradesRequest()
|
|
||||||
req.Symbol(symbol)
|
req.Symbol(symbol)
|
||||||
|
|
||||||
// If `lastTradeId` is given and greater than 0, the query will use it as a condition and the retrieved result will be
|
if options.StartTime != nil && options.EndTime != nil {
|
||||||
// in `ascending` order. We can use `lastTradeId` to retrieve all the data. So we hack it to '1' if `lastTradeID` is '0'.
|
if options.EndTime.Before(*options.StartTime) {
|
||||||
// If 0 is given, it will not be used as a condition and the result will be in `descending` order. The FromTradeId
|
return nil, fmt.Errorf("end time is before start time, start time: %s, end time: %s", options.StartTime.String(), options.EndTime.String())
|
||||||
// option cannot be used to retrieve more data.
|
}
|
||||||
req.FromTradeId(strconv.FormatUint(options.LastTradeID, 10))
|
|
||||||
if options.LastTradeID == 0 {
|
if options.EndTime.Sub(*options.StartTime) > queryTradeDurationLimit {
|
||||||
req.FromTradeId("1")
|
newStartTime := options.EndTime.Add(-queryTradeDurationLimit)
|
||||||
|
|
||||||
|
log.Warnf("!!!BYBIT EXCHANGE API NOTICE!!! The time range exceeds the server boundary: %s, start time: %s, end time: %s, updated start time %s -> %s", queryTradeDurationLimit, options.StartTime.String(), options.EndTime.String(), options.StartTime.String(), newStartTime.String())
|
||||||
|
options.StartTime = &newStartTime
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.StartTime != nil {
|
if options.StartTime != nil {
|
||||||
req.StartTime(options.StartTime.UTC())
|
req.StartTime(options.StartTime.UTC())
|
||||||
}
|
}
|
||||||
|
@ -466,35 +491,13 @@ func (e *Exchange) QueryTrades(ctx context.Context, symbol string, options *type
|
||||||
}
|
}
|
||||||
|
|
||||||
limit := uint64(options.Limit)
|
limit := uint64(options.Limit)
|
||||||
if limit > defaultQueryLimit || limit <= 0 {
|
if limit > defaultQueryTradeLimit || limit <= 0 {
|
||||||
log.Debugf("the parameter limit exceeds the server boundary or is set to zero. changed to %d, original value: %d", defaultQueryLimit, options.Limit)
|
log.Debugf("the parameter limit exceeds the server boundary or is set to zero. changed to %d, original value: %d", defaultQueryTradeLimit, options.Limit)
|
||||||
limit = defaultQueryLimit
|
limit = defaultQueryTradeLimit
|
||||||
}
|
}
|
||||||
req.Limit(limit)
|
req.Limit(limit)
|
||||||
|
|
||||||
if err := queryOrderTradeRateLimiter.Wait(ctx); err != nil {
|
return e.queryTrades(ctx, req)
|
||||||
return nil, fmt.Errorf("trade rate limiter wait error: %w", err)
|
|
||||||
}
|
|
||||||
response, err := req.Do(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to query trades, err: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var errs error
|
|
||||||
for _, trade := range response.List {
|
|
||||||
res, err := v3ToGlobalTrade(trade)
|
|
||||||
if err != nil {
|
|
||||||
errs = multierr.Append(errs, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
trades = append(trades, *res)
|
|
||||||
}
|
|
||||||
|
|
||||||
if errs != nil {
|
|
||||||
return nil, errs
|
|
||||||
}
|
|
||||||
|
|
||||||
return trades, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
|
func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) {
|
||||||
|
|
|
@ -21,7 +21,13 @@ var (
|
||||||
pollFeeRateRateLimiter = rate.NewLimiter(rate.Every(10*time.Minute), 1)
|
pollFeeRateRateLimiter = rate.NewLimiter(rate.Every(10*time.Minute), 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
type symbolFeeDetail struct {
|
type FeeRatePoller interface {
|
||||||
|
StartFeeRatePoller(ctx context.Context)
|
||||||
|
GetFeeRate(symbol string) (SymbolFeeDetail, bool)
|
||||||
|
PollFeeRate(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type SymbolFeeDetail struct {
|
||||||
bybitapi.FeeRate
|
bybitapi.FeeRate
|
||||||
|
|
||||||
BaseCoin string
|
BaseCoin string
|
||||||
|
@ -34,24 +40,27 @@ type feeRatePoller struct {
|
||||||
once sync.Once
|
once sync.Once
|
||||||
client MarketInfoProvider
|
client MarketInfoProvider
|
||||||
|
|
||||||
symbolFeeDetail map[string]symbolFeeDetail
|
// lastSyncTime is the last time the fee rate was updated.
|
||||||
|
lastSyncTime time.Time
|
||||||
|
|
||||||
|
symbolFeeDetail map[string]SymbolFeeDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
func newFeeRatePoller(marketInfoProvider MarketInfoProvider) *feeRatePoller {
|
func newFeeRatePoller(marketInfoProvider MarketInfoProvider) *feeRatePoller {
|
||||||
return &feeRatePoller{
|
return &feeRatePoller{
|
||||||
client: marketInfoProvider,
|
client: marketInfoProvider,
|
||||||
symbolFeeDetail: map[string]symbolFeeDetail{},
|
symbolFeeDetail: map[string]SymbolFeeDetail{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *feeRatePoller) Start(ctx context.Context) {
|
func (p *feeRatePoller) StartFeeRatePoller(ctx context.Context) {
|
||||||
p.once.Do(func() {
|
p.once.Do(func() {
|
||||||
p.startLoop(ctx)
|
p.startLoop(ctx)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *feeRatePoller) startLoop(ctx context.Context) {
|
func (p *feeRatePoller) startLoop(ctx context.Context) {
|
||||||
err := p.poll(ctx)
|
err := p.PollFeeRate(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Warn("failed to initialize the fee rate, the ticker is scheduled to update it subsequently")
|
log.WithError(err).Warn("failed to initialize the fee rate, the ticker is scheduled to update it subsequently")
|
||||||
}
|
}
|
||||||
|
@ -67,22 +76,27 @@ func (p *feeRatePoller) startLoop(ctx context.Context) {
|
||||||
|
|
||||||
return
|
return
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
if err := p.poll(ctx); err != nil {
|
if err := p.PollFeeRate(ctx); err != nil {
|
||||||
log.WithError(err).Warn("failed to update fee rate")
|
log.WithError(err).Warn("failed to update fee rate")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *feeRatePoller) poll(ctx context.Context) error {
|
func (p *feeRatePoller) PollFeeRate(ctx context.Context) error {
|
||||||
|
p.mu.Lock()
|
||||||
|
defer p.mu.Unlock()
|
||||||
|
// the poll will be called frequently, so we need to check the last sync time.
|
||||||
|
if time.Since(p.lastSyncTime) < feeRatePollingPeriod {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
symbolFeeRate, err := p.getAllFeeRates(ctx)
|
symbolFeeRate, err := p.getAllFeeRates(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
p.mu.Lock()
|
|
||||||
p.symbolFeeDetail = symbolFeeRate
|
p.symbolFeeDetail = symbolFeeRate
|
||||||
p.mu.Unlock()
|
p.lastSyncTime = time.Now()
|
||||||
|
|
||||||
if pollFeeRateRateLimiter.Allow() {
|
if pollFeeRateRateLimiter.Allow() {
|
||||||
log.Infof("updated fee rate: %+v", p.symbolFeeDetail)
|
log.Infof("updated fee rate: %+v", p.symbolFeeDetail)
|
||||||
|
@ -91,7 +105,7 @@ func (p *feeRatePoller) poll(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *feeRatePoller) Get(symbol string) (symbolFeeDetail, bool) {
|
func (p *feeRatePoller) GetFeeRate(symbol string) (SymbolFeeDetail, bool) {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
|
||||||
|
@ -99,16 +113,16 @@ func (p *feeRatePoller) Get(symbol string) (symbolFeeDetail, bool) {
|
||||||
return fee, found
|
return fee, found
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *feeRatePoller) getAllFeeRates(ctx context.Context) (map[string]symbolFeeDetail, error) {
|
func (e *feeRatePoller) getAllFeeRates(ctx context.Context) (map[string]SymbolFeeDetail, error) {
|
||||||
feeRates, err := e.client.GetAllFeeRates(ctx)
|
feeRates, err := e.client.GetAllFeeRates(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to call get fee rates: %w", err)
|
return nil, fmt.Errorf("failed to call get fee rates: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
symbolMap := map[string]symbolFeeDetail{}
|
symbolMap := map[string]SymbolFeeDetail{}
|
||||||
for _, f := range feeRates.List {
|
for _, f := range feeRates.List {
|
||||||
if _, found := symbolMap[f.Symbol]; !found {
|
if _, found := symbolMap[f.Symbol]; !found {
|
||||||
symbolMap[f.Symbol] = symbolFeeDetail{FeeRate: f}
|
symbolMap[f.Symbol] = SymbolFeeDetail{FeeRate: f}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -117,7 +131,7 @@ func (e *feeRatePoller) getAllFeeRates(ctx context.Context) (map[string]symbolFe
|
||||||
return nil, fmt.Errorf("failed to get markets: %w", err)
|
return nil, fmt.Errorf("failed to get markets: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// update base coin, quote coin into symbolFeeDetail
|
// update base coin, quote coin into SymbolFeeDetail
|
||||||
for _, mkt := range mkts {
|
for _, mkt := range mkts {
|
||||||
feeRate, found := symbolMap[mkt.Symbol]
|
feeRate, found := symbolMap[mkt.Symbol]
|
||||||
if !found {
|
if !found {
|
||||||
|
|
|
@ -3,16 +3,14 @@ package bybit
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"go.uber.org/mock/gomock"
|
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi"
|
"github.com/c9s/bbgo/pkg/exchange/bybit/bybitapi"
|
||||||
"github.com/c9s/bbgo/pkg/exchange/bybit/mocks"
|
"github.com/c9s/bbgo/pkg/exchange/bybit/mocks"
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"go.uber.org/mock/gomock"
|
||||||
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFeeRatePoller_getAllFeeRates(t *testing.T) {
|
func TestFeeRatePoller_getAllFeeRates(t *testing.T) {
|
||||||
|
@ -64,7 +62,7 @@ func TestFeeRatePoller_getAllFeeRates(t *testing.T) {
|
||||||
mockMarketProvider.EXPECT().GetAllFeeRates(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{
|
||||||
"BTCUSDT": {
|
"BTCUSDT": {
|
||||||
FeeRate: feeRates.List[0],
|
FeeRate: feeRates.List[0],
|
||||||
BaseCoin: "BTC",
|
BaseCoin: "BTC",
|
||||||
|
@ -113,7 +111,7 @@ func TestFeeRatePoller_getAllFeeRates(t *testing.T) {
|
||||||
|
|
||||||
symbolFeeDetails, err := s.getAllFeeRates(ctx)
|
symbolFeeDetails, 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), symbolFeeDetails)
|
assert.Equal(t, map[string]SymbolFeeDetail(nil), symbolFeeDetails)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("failed to get fee rates", func(t *testing.T) {
|
t.Run("failed to get fee rates", func(t *testing.T) {
|
||||||
|
@ -128,7 +126,7 @@ func TestFeeRatePoller_getAllFeeRates(t *testing.T) {
|
||||||
|
|
||||||
symbolFeeDetails, err := s.getAllFeeRates(ctx)
|
symbolFeeDetails, 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), symbolFeeDetails)
|
assert.Equal(t, map[string]SymbolFeeDetail(nil), symbolFeeDetails)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +137,7 @@ func Test_feeRatePoller_Get(t *testing.T) {
|
||||||
mockMarketProvider := mocks.NewMockStreamDataProvider(mockCtrl)
|
mockMarketProvider := mocks.NewMockStreamDataProvider(mockCtrl)
|
||||||
t.Run("found", func(t *testing.T) {
|
t.Run("found", func(t *testing.T) {
|
||||||
symbol := "BTCUSDT"
|
symbol := "BTCUSDT"
|
||||||
expFeeDetail := symbolFeeDetail{
|
expFeeDetail := SymbolFeeDetail{
|
||||||
FeeRate: bybitapi.FeeRate{
|
FeeRate: bybitapi.FeeRate{
|
||||||
Symbol: symbol,
|
Symbol: symbol,
|
||||||
TakerFeeRate: fixedpoint.NewFromFloat(0.1),
|
TakerFeeRate: fixedpoint.NewFromFloat(0.1),
|
||||||
|
@ -151,12 +149,12 @@ func Test_feeRatePoller_Get(t *testing.T) {
|
||||||
|
|
||||||
s := &feeRatePoller{
|
s := &feeRatePoller{
|
||||||
client: mockMarketProvider,
|
client: mockMarketProvider,
|
||||||
symbolFeeDetail: map[string]symbolFeeDetail{
|
symbolFeeDetail: map[string]SymbolFeeDetail{
|
||||||
symbol: expFeeDetail,
|
symbol: expFeeDetail,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
res, found := s.Get(symbol)
|
res, found := s.GetFeeRate(symbol)
|
||||||
assert.True(t, found)
|
assert.True(t, found)
|
||||||
assert.Equal(t, expFeeDetail, res)
|
assert.Equal(t, expFeeDetail, res)
|
||||||
})
|
})
|
||||||
|
@ -164,10 +162,10 @@ func Test_feeRatePoller_Get(t *testing.T) {
|
||||||
symbol := "BTCUSDT"
|
symbol := "BTCUSDT"
|
||||||
s := &feeRatePoller{
|
s := &feeRatePoller{
|
||||||
client: mockMarketProvider,
|
client: mockMarketProvider,
|
||||||
symbolFeeDetail: map[string]symbolFeeDetail{},
|
symbolFeeDetail: map[string]SymbolFeeDetail{},
|
||||||
}
|
}
|
||||||
|
|
||||||
_, found := s.Get(symbol)
|
_, found := s.GetFeeRate(symbol)
|
||||||
assert.False(t, found)
|
assert.False(t, found)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,7 @@ type AccountBalanceProvider interface {
|
||||||
type StreamDataProvider interface {
|
type StreamDataProvider interface {
|
||||||
MarketInfoProvider
|
MarketInfoProvider
|
||||||
AccountBalanceProvider
|
AccountBalanceProvider
|
||||||
|
FeeRatePoller
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:generate callbackgen -type Stream
|
//go:generate callbackgen -type Stream
|
||||||
|
@ -59,7 +60,7 @@ type Stream struct {
|
||||||
|
|
||||||
key, secret string
|
key, secret string
|
||||||
streamDataProvider StreamDataProvider
|
streamDataProvider StreamDataProvider
|
||||||
feeRateProvider *feeRatePoller
|
feeRateProvider FeeRatePoller
|
||||||
marketsInfo types.MarketMap
|
marketsInfo types.MarketMap
|
||||||
|
|
||||||
bookEventCallbacks []func(e BookEvent)
|
bookEventCallbacks []func(e BookEvent)
|
||||||
|
@ -77,7 +78,6 @@ func NewStream(key, secret string, userDataProvider StreamDataProvider) *Stream
|
||||||
key: key,
|
key: key,
|
||||||
secret: secret,
|
secret: secret,
|
||||||
streamDataProvider: userDataProvider,
|
streamDataProvider: userDataProvider,
|
||||||
feeRateProvider: newFeeRatePoller(userDataProvider),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stream.SetEndpointCreator(stream.createEndpoint)
|
stream.SetEndpointCreator(stream.createEndpoint)
|
||||||
|
@ -91,7 +91,7 @@ func NewStream(key, secret string, userDataProvider StreamDataProvider) *Stream
|
||||||
}
|
}
|
||||||
|
|
||||||
// get account fee rate
|
// get account fee rate
|
||||||
go stream.feeRateProvider.Start(ctx)
|
go stream.streamDataProvider.StartFeeRatePoller(ctx)
|
||||||
|
|
||||||
stream.marketsInfo, err = stream.streamDataProvider.QueryMarkets(ctx)
|
stream.marketsInfo, err = stream.streamDataProvider.QueryMarkets(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -439,37 +439,49 @@ func (s *Stream) handleKLineEvent(klineEvent KLineEvent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Stream) handleTradeEvent(events []TradeEvent) {
|
func pollAndGetFeeRate(ctx context.Context, symbol string, poller FeeRatePoller, marketsInfo types.MarketMap) (SymbolFeeDetail, error) {
|
||||||
for _, event := range events {
|
err := poller.PollFeeRate(ctx)
|
||||||
feeRate, found := s.feeRateProvider.Get(event.Symbol)
|
if err != nil {
|
||||||
if !found {
|
return SymbolFeeDetail{}, err
|
||||||
feeRate = symbolFeeDetail{
|
}
|
||||||
FeeRate: bybitapi.FeeRate{
|
return getFeeRate(symbol, poller, marketsInfo), nil
|
||||||
Symbol: event.Symbol,
|
}
|
||||||
TakerFeeRate: defaultTakerFee,
|
|
||||||
MakerFeeRate: defaultMakerFee,
|
|
||||||
},
|
|
||||||
BaseCoin: "",
|
|
||||||
QuoteCoin: "",
|
|
||||||
}
|
|
||||||
|
|
||||||
if market, ok := s.marketsInfo[event.Symbol]; ok {
|
func getFeeRate(symbol string, poller FeeRatePoller, marketsInfo types.MarketMap) SymbolFeeDetail {
|
||||||
feeRate.BaseCoin = market.BaseCurrency
|
feeRate, found := poller.GetFeeRate(symbol)
|
||||||
feeRate.QuoteCoin = market.QuoteCurrency
|
if !found {
|
||||||
}
|
feeRate = SymbolFeeDetail{
|
||||||
|
FeeRate: bybitapi.FeeRate{
|
||||||
if tradeLogLimiter.Allow() {
|
Symbol: symbol,
|
||||||
// The error log level was utilized due to a detected discrepancy in the fee calculations.
|
TakerFeeRate: defaultTakerFee,
|
||||||
log.Errorf("failed to get %s fee rate, use default taker fee %f, maker fee %f, base coin: %s, quote coin: %s",
|
MakerFeeRate: defaultMakerFee,
|
||||||
event.Symbol,
|
},
|
||||||
feeRate.TakerFeeRate.Float64(),
|
BaseCoin: "",
|
||||||
feeRate.MakerFeeRate.Float64(),
|
QuoteCoin: "",
|
||||||
feeRate.BaseCoin,
|
|
||||||
feeRate.QuoteCoin,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if market, ok := marketsInfo[symbol]; ok {
|
||||||
|
feeRate.BaseCoin = market.BaseCurrency
|
||||||
|
feeRate.QuoteCoin = market.QuoteCurrency
|
||||||
|
}
|
||||||
|
|
||||||
|
if tradeLogLimiter.Allow() {
|
||||||
|
// The error log level was utilized due to a detected discrepancy in the fee calculations.
|
||||||
|
log.Errorf("failed to get %s fee rate, use default taker fee %f, maker fee %f, base coin: %s, quote coin: %s",
|
||||||
|
symbol,
|
||||||
|
feeRate.TakerFeeRate.Float64(),
|
||||||
|
feeRate.MakerFeeRate.Float64(),
|
||||||
|
feeRate.BaseCoin,
|
||||||
|
feeRate.QuoteCoin,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return feeRate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stream) handleTradeEvent(events []TradeEvent) {
|
||||||
|
for _, event := range events {
|
||||||
|
feeRate := getFeeRate(event.Symbol, s.feeRateProvider, s.marketsInfo)
|
||||||
gTrade, err := event.toGlobalTrade(feeRate)
|
gTrade, err := event.toGlobalTrade(feeRate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if tradeLogLimiter.Allow() {
|
if tradeLogLimiter.Allow() {
|
||||||
|
|
|
@ -327,7 +327,7 @@ type TradeEvent struct {
|
||||||
TradeIv string `json:"tradeIv"`
|
TradeIv string `json:"tradeIv"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *TradeEvent) toGlobalTrade(symbolFee symbolFeeDetail) (*types.Trade, error) {
|
func (t *TradeEvent) toGlobalTrade(symbolFee SymbolFeeDetail) (*types.Trade, error) {
|
||||||
if t.Category != bybitapi.CategorySpot {
|
if t.Category != bybitapi.CategorySpot {
|
||||||
return nil, fmt.Errorf("unexected category: %s", t.Category)
|
return nil, fmt.Errorf("unexected category: %s", t.Category)
|
||||||
}
|
}
|
||||||
|
@ -385,7 +385,7 @@ func (t *TradeEvent) toGlobalTrade(symbolFee symbolFeeDetail) (*types.Trade, err
|
||||||
// IsMakerOrder = FALSE
|
// IsMakerOrder = FALSE
|
||||||
// -> Side = Buy -> base currency (BTC)
|
// -> Side = Buy -> base currency (BTC)
|
||||||
// -> Side = Sell -> quote currency (USDT)
|
// -> Side = Sell -> quote currency (USDT)
|
||||||
func calculateFee(t bybitapi.Trade, feeDetail symbolFeeDetail) (string, fixedpoint.Value) {
|
func calculateFee(t bybitapi.Trade, feeDetail SymbolFeeDetail) (string, fixedpoint.Value) {
|
||||||
if feeDetail.MakerFeeRate.Sign() > 0 || !t.IsMaker {
|
if feeDetail.MakerFeeRate.Sign() > 0 || !t.IsMaker {
|
||||||
if t.Side == bybitapi.SideBuy {
|
if t.Side == bybitapi.SideBuy {
|
||||||
return feeDetail.BaseCoin, baseCoinAsFee(t, feeDetail)
|
return feeDetail.BaseCoin, baseCoinAsFee(t, feeDetail)
|
||||||
|
@ -399,14 +399,14 @@ func calculateFee(t bybitapi.Trade, feeDetail symbolFeeDetail) (string, fixedpoi
|
||||||
return feeDetail.BaseCoin, baseCoinAsFee(t, feeDetail)
|
return feeDetail.BaseCoin, baseCoinAsFee(t, feeDetail)
|
||||||
}
|
}
|
||||||
|
|
||||||
func baseCoinAsFee(t bybitapi.Trade, feeDetail symbolFeeDetail) fixedpoint.Value {
|
func baseCoinAsFee(t bybitapi.Trade, feeDetail SymbolFeeDetail) fixedpoint.Value {
|
||||||
if t.IsMaker {
|
if t.IsMaker {
|
||||||
return feeDetail.MakerFeeRate.Mul(t.ExecQty)
|
return feeDetail.MakerFeeRate.Mul(t.ExecQty)
|
||||||
}
|
}
|
||||||
return feeDetail.TakerFeeRate.Mul(t.ExecQty)
|
return feeDetail.TakerFeeRate.Mul(t.ExecQty)
|
||||||
}
|
}
|
||||||
|
|
||||||
func quoteCoinAsFee(t bybitapi.Trade, feeDetail symbolFeeDetail) fixedpoint.Value {
|
func quoteCoinAsFee(t bybitapi.Trade, feeDetail SymbolFeeDetail) fixedpoint.Value {
|
||||||
baseFee := t.ExecPrice.Mul(t.ExecQty)
|
baseFee := t.ExecPrice.Mul(t.ExecQty)
|
||||||
if t.IsMaker {
|
if t.IsMaker {
|
||||||
return feeDetail.MakerFeeRate.Mul(baseFee)
|
return feeDetail.MakerFeeRate.Mul(baseFee)
|
||||||
|
|
|
@ -528,7 +528,7 @@ func TestTradeEvent_toGlobalTrade(t *testing.T) {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
t.Run("succeeds", func(t *testing.T) {
|
t.Run("succeeds", func(t *testing.T) {
|
||||||
symbolFee := symbolFeeDetail{
|
symbolFee := SymbolFeeDetail{
|
||||||
FeeRate: bybitapi.FeeRate{
|
FeeRate: bybitapi.FeeRate{
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
||||||
|
@ -597,7 +597,7 @@ func TestTradeEvent_toGlobalTrade(t *testing.T) {
|
||||||
Category: "test-spot",
|
Category: "test-spot",
|
||||||
}
|
}
|
||||||
|
|
||||||
actualTrade, err := tradeEvent.toGlobalTrade(symbolFeeDetail{})
|
actualTrade, err := tradeEvent.toGlobalTrade(SymbolFeeDetail{})
|
||||||
assert.Equal(t, fmt.Errorf("unexected category: %s", tradeEvent.Category), err)
|
assert.Equal(t, fmt.Errorf("unexected category: %s", tradeEvent.Category), err)
|
||||||
assert.Nil(t, actualTrade)
|
assert.Nil(t, actualTrade)
|
||||||
})
|
})
|
||||||
|
@ -610,7 +610,7 @@ func TestTradeEvent_toGlobalTrade(t *testing.T) {
|
||||||
Category: "spot",
|
Category: "spot",
|
||||||
}
|
}
|
||||||
|
|
||||||
actualTrade, err := tradeEvent.toGlobalTrade(symbolFeeDetail{})
|
actualTrade, err := tradeEvent.toGlobalTrade(SymbolFeeDetail{})
|
||||||
assert.Equal(t, fmt.Errorf("unexpected side: BOTH"), err)
|
assert.Equal(t, fmt.Errorf("unexpected side: BOTH"), err)
|
||||||
assert.Nil(t, actualTrade)
|
assert.Nil(t, actualTrade)
|
||||||
})
|
})
|
||||||
|
@ -625,7 +625,7 @@ func TestTradeEvent_toGlobalTrade(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
_, nerr := strconv.ParseUint(tradeEvent.OrderId, 10, 64)
|
_, nerr := strconv.ParseUint(tradeEvent.OrderId, 10, 64)
|
||||||
actualTrade, err := tradeEvent.toGlobalTrade(symbolFeeDetail{})
|
actualTrade, err := tradeEvent.toGlobalTrade(SymbolFeeDetail{})
|
||||||
assert.Equal(t, fmt.Errorf("unexpected order id: %s, err: %w", tradeEvent.OrderId, nerr), err)
|
assert.Equal(t, fmt.Errorf("unexpected order id: %s, err: %w", tradeEvent.OrderId, nerr), err)
|
||||||
assert.Nil(t, actualTrade)
|
assert.Nil(t, actualTrade)
|
||||||
})
|
})
|
||||||
|
@ -641,7 +641,7 @@ func TestTradeEvent_toGlobalTrade(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
_, nerr := strconv.ParseUint(tradeEvent.ExecId, 10, 64)
|
_, nerr := strconv.ParseUint(tradeEvent.ExecId, 10, 64)
|
||||||
actualTrade, err := tradeEvent.toGlobalTrade(symbolFeeDetail{})
|
actualTrade, err := tradeEvent.toGlobalTrade(SymbolFeeDetail{})
|
||||||
assert.Equal(t, fmt.Errorf("unexpected exec id: %s, err: %w", tradeEvent.ExecId, nerr), err)
|
assert.Equal(t, fmt.Errorf("unexpected exec id: %s, err: %w", tradeEvent.ExecId, nerr), err)
|
||||||
assert.Nil(t, actualTrade)
|
assert.Nil(t, actualTrade)
|
||||||
})
|
})
|
||||||
|
@ -649,7 +649,7 @@ func TestTradeEvent_toGlobalTrade(t *testing.T) {
|
||||||
|
|
||||||
func TestTradeEvent_CalculateFee(t *testing.T) {
|
func TestTradeEvent_CalculateFee(t *testing.T) {
|
||||||
t.Run("maker fee positive, maker, buyer", func(t *testing.T) {
|
t.Run("maker fee positive, maker, buyer", func(t *testing.T) {
|
||||||
symbolFee := symbolFeeDetail{
|
symbolFee := SymbolFeeDetail{
|
||||||
FeeRate: bybitapi.FeeRate{
|
FeeRate: bybitapi.FeeRate{
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
||||||
|
@ -676,7 +676,7 @@ func TestTradeEvent_CalculateFee(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("maker fee positive, maker, seller", func(t *testing.T) {
|
t.Run("maker fee positive, maker, seller", func(t *testing.T) {
|
||||||
symbolFee := symbolFeeDetail{
|
symbolFee := SymbolFeeDetail{
|
||||||
FeeRate: bybitapi.FeeRate{
|
FeeRate: bybitapi.FeeRate{
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
||||||
|
@ -703,7 +703,7 @@ func TestTradeEvent_CalculateFee(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("maker fee positive, taker, buyer", func(t *testing.T) {
|
t.Run("maker fee positive, taker, buyer", func(t *testing.T) {
|
||||||
symbolFee := symbolFeeDetail{
|
symbolFee := SymbolFeeDetail{
|
||||||
FeeRate: bybitapi.FeeRate{
|
FeeRate: bybitapi.FeeRate{
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
||||||
|
@ -730,7 +730,7 @@ func TestTradeEvent_CalculateFee(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("maker fee positive, taker, seller", func(t *testing.T) {
|
t.Run("maker fee positive, taker, seller", func(t *testing.T) {
|
||||||
symbolFee := symbolFeeDetail{
|
symbolFee := SymbolFeeDetail{
|
||||||
FeeRate: bybitapi.FeeRate{
|
FeeRate: bybitapi.FeeRate{
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
||||||
|
@ -757,7 +757,7 @@ func TestTradeEvent_CalculateFee(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("maker fee negative, maker, buyer", func(t *testing.T) {
|
t.Run("maker fee negative, maker, buyer", func(t *testing.T) {
|
||||||
symbolFee := symbolFeeDetail{
|
symbolFee := SymbolFeeDetail{
|
||||||
FeeRate: bybitapi.FeeRate{
|
FeeRate: bybitapi.FeeRate{
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
TakerFeeRate: fixedpoint.NewFromFloat(-0.001),
|
TakerFeeRate: fixedpoint.NewFromFloat(-0.001),
|
||||||
|
@ -784,7 +784,7 @@ func TestTradeEvent_CalculateFee(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("maker fee negative, maker, seller", func(t *testing.T) {
|
t.Run("maker fee negative, maker, seller", func(t *testing.T) {
|
||||||
symbolFee := symbolFeeDetail{
|
symbolFee := SymbolFeeDetail{
|
||||||
FeeRate: bybitapi.FeeRate{
|
FeeRate: bybitapi.FeeRate{
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
TakerFeeRate: fixedpoint.NewFromFloat(-0.001),
|
TakerFeeRate: fixedpoint.NewFromFloat(-0.001),
|
||||||
|
@ -811,7 +811,7 @@ func TestTradeEvent_CalculateFee(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("maker fee negative, taker, buyer", func(t *testing.T) {
|
t.Run("maker fee negative, taker, buyer", func(t *testing.T) {
|
||||||
symbolFee := symbolFeeDetail{
|
symbolFee := SymbolFeeDetail{
|
||||||
FeeRate: bybitapi.FeeRate{
|
FeeRate: bybitapi.FeeRate{
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
TakerFeeRate: fixedpoint.NewFromFloat(-0.001),
|
TakerFeeRate: fixedpoint.NewFromFloat(-0.001),
|
||||||
|
@ -838,7 +838,7 @@ func TestTradeEvent_CalculateFee(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("maker fee negative, taker, seller", func(t *testing.T) {
|
t.Run("maker fee negative, taker, seller", func(t *testing.T) {
|
||||||
symbolFee := symbolFeeDetail{
|
symbolFee := SymbolFeeDetail{
|
||||||
FeeRate: bybitapi.FeeRate{
|
FeeRate: bybitapi.FeeRate{
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
TakerFeeRate: fixedpoint.NewFromFloat(-0.001),
|
TakerFeeRate: fixedpoint.NewFromFloat(-0.001),
|
||||||
|
@ -867,7 +867,7 @@ func TestTradeEvent_CalculateFee(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTradeEvent_baseCoinAsFee(t *testing.T) {
|
func TestTradeEvent_baseCoinAsFee(t *testing.T) {
|
||||||
symbolFee := symbolFeeDetail{
|
symbolFee := SymbolFeeDetail{
|
||||||
FeeRate: bybitapi.FeeRate{
|
FeeRate: bybitapi.FeeRate{
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
||||||
|
@ -892,7 +892,7 @@ func TestTradeEvent_baseCoinAsFee(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTradeEvent_quoteCoinAsFee(t *testing.T) {
|
func TestTradeEvent_quoteCoinAsFee(t *testing.T) {
|
||||||
symbolFee := symbolFeeDetail{
|
symbolFee := SymbolFeeDetail{
|
||||||
FeeRate: bybitapi.FeeRate{
|
FeeRate: bybitapi.FeeRate{
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
TakerFeeRate: fixedpoint.NewFromFloat(0.001),
|
||||||
|
|
Loading…
Reference in New Issue
Block a user