mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
Merge pull request #1076 from c9s/fix/grid2/fix-fee-reduction
fix: grid2: quantity fee reduction for quote currency
This commit is contained in:
commit
bfcb60d9d7
|
@ -143,6 +143,8 @@ type Strategy struct {
|
||||||
// StopIfLessThanMinimalQuoteInvestment stops the strategy if the quote investment does not match
|
// StopIfLessThanMinimalQuoteInvestment stops the strategy if the quote investment does not match
|
||||||
StopIfLessThanMinimalQuoteInvestment bool `json:"stopIfLessThanMinimalQuoteInvestment"`
|
StopIfLessThanMinimalQuoteInvestment bool `json:"stopIfLessThanMinimalQuoteInvestment"`
|
||||||
|
|
||||||
|
OrderFillDelay types.Duration `json:"orderFillDelay"`
|
||||||
|
|
||||||
// PrometheusLabels will be used as the base prometheus labels
|
// PrometheusLabels will be used as the base prometheus labels
|
||||||
PrometheusLabels prometheus.Labels `json:"prometheusLabels"`
|
PrometheusLabels prometheus.Labels `json:"prometheusLabels"`
|
||||||
|
|
||||||
|
@ -321,38 +323,43 @@ func (s *Strategy) verifyOrderTrades(o types.Order, trades []types.Trade) bool {
|
||||||
tq := aggregateTradesQuantity(trades)
|
tq := aggregateTradesQuantity(trades)
|
||||||
|
|
||||||
if tq.Compare(o.Quantity) != 0 {
|
if tq.Compare(o.Quantity) != 0 {
|
||||||
s.logger.Warnf("order trades missing. expected: %f actual: %f",
|
s.logger.Warnf("order trades missing. expected: %s got: %s",
|
||||||
o.Quantity.Float64(),
|
o.Quantity.String(),
|
||||||
tq.Float64())
|
tq.String())
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// aggregateOrderBaseFee collects the base fee quantity from the given order
|
// aggregateOrderFee collects the base fee quantity from the given order
|
||||||
// it falls back to query the trades via the RESTful API when the websocket trades are not all received.
|
// it falls back to query the trades via the RESTful API when the websocket trades are not all received.
|
||||||
func (s *Strategy) aggregateOrderBaseFee(o types.Order) fixedpoint.Value {
|
func (s *Strategy) aggregateOrderFee(o types.Order) (fixedpoint.Value, string) {
|
||||||
// try to get the received trades (websocket trades)
|
// try to get the received trades (websocket trades)
|
||||||
orderTrades := s.historicalTrades.GetOrderTrades(o)
|
orderTrades := s.historicalTrades.GetOrderTrades(o)
|
||||||
if len(orderTrades) > 0 {
|
if len(orderTrades) > 0 {
|
||||||
s.logger.Infof("found filled order trades: %+v", orderTrades)
|
s.logger.Infof("found filled order trades: %+v", orderTrades)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
feeCurrency := s.Market.BaseCurrency
|
||||||
|
if o.Side == types.SideTypeSell {
|
||||||
|
feeCurrency = s.Market.QuoteCurrency
|
||||||
|
}
|
||||||
|
|
||||||
for maxTries := maxNumberOfOrderTradesQueryTries; maxTries > 0; maxTries-- {
|
for maxTries := maxNumberOfOrderTradesQueryTries; maxTries > 0; maxTries-- {
|
||||||
// if one of the trades is missing, we need to query the trades from the RESTful API
|
// if one of the trades is missing, we need to query the trades from the RESTful API
|
||||||
if s.verifyOrderTrades(o, orderTrades) {
|
if s.verifyOrderTrades(o, orderTrades) {
|
||||||
// if trades are verified
|
// if trades are verified
|
||||||
fees := collectTradeFee(orderTrades)
|
fees := collectTradeFee(orderTrades)
|
||||||
if fee, ok := fees[s.Market.BaseCurrency]; ok {
|
if fee, ok := fees[feeCurrency]; ok {
|
||||||
return fee
|
return fee, feeCurrency
|
||||||
}
|
}
|
||||||
return fixedpoint.Zero
|
return fixedpoint.Zero, feeCurrency
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we don't support orderQueryService, then we should just skip
|
// if we don't support orderQueryService, then we should just skip
|
||||||
if s.orderQueryService == nil {
|
if s.orderQueryService == nil {
|
||||||
return fixedpoint.Zero
|
return fixedpoint.Zero, feeCurrency
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Warnf("missing order trades or missing trade fee, pulling order trades from API")
|
s.logger.Warnf("missing order trades or missing trade fee, pulling order trades from API")
|
||||||
|
@ -370,7 +377,7 @@ func (s *Strategy) aggregateOrderBaseFee(o types.Order) fixedpoint.Value {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return fixedpoint.Zero
|
return fixedpoint.Zero, feeCurrency
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Strategy) processFilledOrder(o types.Order) {
|
func (s *Strategy) processFilledOrder(o types.Order) {
|
||||||
|
@ -381,15 +388,35 @@ func (s *Strategy) processFilledOrder(o types.Order) {
|
||||||
newPrice := o.Price
|
newPrice := o.Price
|
||||||
newQuantity := o.Quantity
|
newQuantity := o.Quantity
|
||||||
executedPrice := o.Price
|
executedPrice := o.Price
|
||||||
|
|
||||||
|
/*
|
||||||
if o.AveragePrice.Sign() > 0 {
|
if o.AveragePrice.Sign() > 0 {
|
||||||
executedPrice = o.AveragePrice
|
executedPrice = o.AveragePrice
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// will be used for calculating quantity
|
// will be used for calculating quantity
|
||||||
orderExecutedQuoteAmount := o.Quantity.Mul(executedPrice)
|
orderExecutedQuoteAmount := o.Quantity.Mul(executedPrice)
|
||||||
|
|
||||||
// collect trades
|
// collect trades
|
||||||
baseSellQuantityReduction := fixedpoint.Zero
|
feeQuantityReduction := fixedpoint.Zero
|
||||||
|
feeCurrency := ""
|
||||||
|
feePrec := 2
|
||||||
|
|
||||||
|
// feeQuantityReduction calculation is used to reduce the order quantity
|
||||||
|
// because when 1.0 BTC buy order is filled without FEE token, then we will actually get 1.0 * (1 - feeRate) BTC
|
||||||
|
// if we don't reduce the sell quantity, than we might fail to place the sell order
|
||||||
|
feeQuantityReduction, feeCurrency = s.aggregateOrderFee(o)
|
||||||
|
s.logger.Infof("GRID ORDER #%d %s FEE: %s %s",
|
||||||
|
o.OrderID, o.Side,
|
||||||
|
feeQuantityReduction.String(), feeCurrency)
|
||||||
|
|
||||||
|
feeQuantityReduction, feePrec = roundUpMarketQuantity(s.Market, feeQuantityReduction, feeCurrency)
|
||||||
|
s.logger.Infof("GRID ORDER #%d %s FEE (rounding precision %d): %s %s",
|
||||||
|
o.OrderID, o.Side,
|
||||||
|
feePrec,
|
||||||
|
feeQuantityReduction.String(),
|
||||||
|
feeCurrency)
|
||||||
|
|
||||||
switch o.Side {
|
switch o.Side {
|
||||||
case types.SideTypeSell:
|
case types.SideTypeSell:
|
||||||
|
@ -405,6 +432,11 @@ func (s *Strategy) processFilledOrder(o types.Order) {
|
||||||
|
|
||||||
// use the profit to buy more inventory in the grid
|
// use the profit to buy more inventory in the grid
|
||||||
if s.Compound || s.EarnBase {
|
if s.Compound || s.EarnBase {
|
||||||
|
// if it's not using the platform fee currency, reduce the quote quantity for the buy order
|
||||||
|
if feeCurrency == s.Market.QuoteCurrency {
|
||||||
|
orderExecutedQuoteAmount = orderExecutedQuoteAmount.Sub(feeQuantityReduction)
|
||||||
|
}
|
||||||
|
|
||||||
newQuantity = fixedpoint.Max(orderExecutedQuoteAmount.Div(newPrice), s.Market.MinQuantity)
|
newQuantity = fixedpoint.Max(orderExecutedQuoteAmount.Div(newPrice), s.Market.MinQuantity)
|
||||||
} else if s.QuantityOrAmount.Quantity.Sign() > 0 {
|
} else if s.QuantityOrAmount.Quantity.Sign() > 0 {
|
||||||
newQuantity = s.QuantityOrAmount.Quantity
|
newQuantity = s.QuantityOrAmount.Quantity
|
||||||
|
@ -414,19 +446,9 @@ func (s *Strategy) processFilledOrder(o types.Order) {
|
||||||
profit = s.calculateProfit(o, newPrice, newQuantity)
|
profit = s.calculateProfit(o, newPrice, newQuantity)
|
||||||
|
|
||||||
case types.SideTypeBuy:
|
case types.SideTypeBuy:
|
||||||
// baseSellQuantityReduction calculation should be only for BUY order
|
if feeCurrency == s.Market.BaseCurrency {
|
||||||
// because when 1.0 BTC buy order is filled without FEE token, then we will actually get 1.0 * (1 - feeRate) BTC
|
newQuantity = newQuantity.Sub(feeQuantityReduction)
|
||||||
// if we don't reduce the sell quantity, than we might fail to place the sell order
|
}
|
||||||
baseSellQuantityReduction = s.aggregateOrderBaseFee(o)
|
|
||||||
s.logger.Infof("GRID BUY ORDER BASE FEE: %s %s", baseSellQuantityReduction.String(), s.Market.BaseCurrency)
|
|
||||||
|
|
||||||
baseSellQuantityReduction = roundUpMarketQuantity(s.Market, baseSellQuantityReduction)
|
|
||||||
s.logger.Infof("GRID BUY ORDER BASE FEE (Rounding with precision %d): %s %s",
|
|
||||||
s.Market.VolumePrecision,
|
|
||||||
baseSellQuantityReduction.String(),
|
|
||||||
s.Market.BaseCurrency)
|
|
||||||
|
|
||||||
newQuantity = newQuantity.Sub(baseSellQuantityReduction)
|
|
||||||
|
|
||||||
newSide = types.SideTypeSell
|
newSide = types.SideTypeSell
|
||||||
if !s.ProfitSpread.IsZero() {
|
if !s.ProfitSpread.IsZero() {
|
||||||
|
@ -438,7 +460,7 @@ func (s *Strategy) processFilledOrder(o types.Order) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.EarnBase {
|
if s.EarnBase {
|
||||||
newQuantity = fixedpoint.Max(orderExecutedQuoteAmount.Div(newPrice).Sub(baseSellQuantityReduction), s.Market.MinQuantity)
|
newQuantity = fixedpoint.Max(orderExecutedQuoteAmount.Div(newPrice).Sub(feeQuantityReduction), s.Market.MinQuantity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -765,6 +787,10 @@ func (s *Strategy) newTriggerPriceHandler(ctx context.Context, session *bbgo.Exc
|
||||||
|
|
||||||
func (s *Strategy) newOrderUpdateHandler(ctx context.Context, session *bbgo.ExchangeSession) func(o types.Order) {
|
func (s *Strategy) newOrderUpdateHandler(ctx context.Context, session *bbgo.ExchangeSession) func(o types.Order) {
|
||||||
return func(o types.Order) {
|
return func(o types.Order) {
|
||||||
|
if s.OrderFillDelay > 0 {
|
||||||
|
time.Sleep(s.OrderFillDelay.Duration())
|
||||||
|
}
|
||||||
|
|
||||||
s.handleOrderFilled(o)
|
s.handleOrderFilled(o)
|
||||||
|
|
||||||
// sync the profits to redis
|
// sync the profits to redis
|
||||||
|
@ -1789,6 +1815,11 @@ func (s *Strategy) openOrdersMismatches(ctx context.Context, session *bbgo.Excha
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func roundUpMarketQuantity(market types.Market, v fixedpoint.Value) fixedpoint.Value {
|
func roundUpMarketQuantity(market types.Market, v fixedpoint.Value, c string) (fixedpoint.Value, int) {
|
||||||
return v.Round(market.VolumePrecision, fixedpoint.Up)
|
prec := market.VolumePrecision
|
||||||
|
if c == market.QuoteCurrency {
|
||||||
|
prec = market.PricePrecision
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.Round(prec, fixedpoint.Up), prec
|
||||||
}
|
}
|
|
@ -478,7 +478,7 @@ func TestStrategy_aggregateOrderBaseFee(t *testing.T) {
|
||||||
},
|
},
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
baseFee := s.aggregateOrderBaseFee(types.Order{
|
baseFee, _ := s.aggregateOrderFee(types.Order{
|
||||||
SubmitOrder: types.SubmitOrder{
|
SubmitOrder: types.SubmitOrder{
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
Side: types.SideTypeBuy,
|
Side: types.SideTypeBuy,
|
||||||
|
@ -646,6 +646,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) {
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
|
|
||||||
mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl)
|
mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl)
|
||||||
|
|
||||||
mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{
|
mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
OrderID: "1",
|
OrderID: "1",
|
||||||
|
@ -655,14 +656,32 @@ func TestStrategy_handleOrderFilled(t *testing.T) {
|
||||||
OrderID: orderID,
|
OrderID: orderID,
|
||||||
Exchange: "binance",
|
Exchange: "binance",
|
||||||
Price: number(11000.0),
|
Price: number(11000.0),
|
||||||
Quantity: gridQuantity,
|
Quantity: number("0.1"),
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
Side: types.SideTypeBuy,
|
Side: types.SideTypeBuy,
|
||||||
IsBuyer: true,
|
IsBuyer: true,
|
||||||
FeeCurrency: "BTC",
|
FeeCurrency: "BTC",
|
||||||
Fee: fixedpoint.Zero,
|
Fee: fixedpoint.Zero,
|
||||||
},
|
},
|
||||||
}, nil)
|
}, nil).Times(1)
|
||||||
|
|
||||||
|
mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
OrderID: "2",
|
||||||
|
}).Return([]types.Trade{
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
OrderID: orderID,
|
||||||
|
Exchange: "binance",
|
||||||
|
Price: number(12000.0),
|
||||||
|
Quantity: number(0.09166666666),
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Side: types.SideTypeSell,
|
||||||
|
IsBuyer: true,
|
||||||
|
FeeCurrency: "BTC",
|
||||||
|
Fee: fixedpoint.Zero,
|
||||||
|
},
|
||||||
|
}, nil).Times(1)
|
||||||
|
|
||||||
s.orderQueryService = mockService
|
s.orderQueryService = mockService
|
||||||
|
|
||||||
|
@ -735,6 +754,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) {
|
||||||
defer mockCtrl.Finish()
|
defer mockCtrl.Finish()
|
||||||
|
|
||||||
mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl)
|
mockService := mocks.NewMockExchangeOrderQueryService(mockCtrl)
|
||||||
|
|
||||||
mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{
|
mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
OrderID: "1",
|
OrderID: "1",
|
||||||
|
@ -749,7 +769,25 @@ func TestStrategy_handleOrderFilled(t *testing.T) {
|
||||||
Side: types.SideTypeBuy,
|
Side: types.SideTypeBuy,
|
||||||
IsBuyer: true,
|
IsBuyer: true,
|
||||||
FeeCurrency: "BTC",
|
FeeCurrency: "BTC",
|
||||||
Fee: fixedpoint.Zero,
|
Fee: number("0.00001"),
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
|
||||||
|
mockService.EXPECT().QueryOrderTrades(ctx, types.OrderQuery{
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
OrderID: "2",
|
||||||
|
}).Return([]types.Trade{
|
||||||
|
{
|
||||||
|
ID: 2,
|
||||||
|
OrderID: orderID,
|
||||||
|
Exchange: "binance",
|
||||||
|
Price: number(12000.0),
|
||||||
|
Quantity: gridQuantity,
|
||||||
|
Symbol: "BTCUSDT",
|
||||||
|
Side: types.SideTypeSell,
|
||||||
|
IsBuyer: true,
|
||||||
|
FeeCurrency: "USDT",
|
||||||
|
Fee: number("0.01"),
|
||||||
},
|
},
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
|
@ -759,7 +797,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) {
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
Type: types.OrderTypeLimit,
|
Type: types.OrderTypeLimit,
|
||||||
Price: number(12_000.0),
|
Price: number(12_000.0),
|
||||||
Quantity: gridQuantity,
|
Quantity: number(0.09998999),
|
||||||
Side: types.SideTypeSell,
|
Side: types.SideTypeSell,
|
||||||
TimeInForce: types.TimeInForceGTC,
|
TimeInForce: types.TimeInForceGTC,
|
||||||
Market: s.Market,
|
Market: s.Market,
|
||||||
|
@ -775,7 +813,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) {
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
Type: types.OrderTypeLimit,
|
Type: types.OrderTypeLimit,
|
||||||
Price: number(11_000.0),
|
Price: number(11_000.0),
|
||||||
Quantity: number(0.1090909),
|
Quantity: number(0.10909),
|
||||||
Side: types.SideTypeBuy,
|
Side: types.SideTypeBuy,
|
||||||
TimeInForce: types.TimeInForceGTC,
|
TimeInForce: types.TimeInForceGTC,
|
||||||
Market: s.Market,
|
Market: s.Market,
|
||||||
|
@ -865,7 +903,7 @@ func TestStrategy_aggregateOrderBaseFeeRetry(t *testing.T) {
|
||||||
},
|
},
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
baseFee := s.aggregateOrderBaseFee(types.Order{
|
baseFee, _ := s.aggregateOrderFee(types.Order{
|
||||||
SubmitOrder: types.SubmitOrder{
|
SubmitOrder: types.SubmitOrder{
|
||||||
Symbol: "BTCUSDT",
|
Symbol: "BTCUSDT",
|
||||||
Side: types.SideTypeBuy,
|
Side: types.SideTypeBuy,
|
||||||
|
@ -937,8 +975,9 @@ func Test_roundUpMarketQuantity(t *testing.T) {
|
||||||
q := number("0.00000003")
|
q := number("0.00000003")
|
||||||
assert.Equal(t, "0.00000003", q.String())
|
assert.Equal(t, "0.00000003", q.String())
|
||||||
|
|
||||||
q3 := roundUpMarketQuantity(types.Market{
|
q3, prec := roundUpMarketQuantity(types.Market{
|
||||||
VolumePrecision: 8,
|
VolumePrecision: 8,
|
||||||
}, q)
|
}, q, "BTC")
|
||||||
assert.Equal(t, "0.00000003", q3.String(), "rounding prec 8")
|
assert.Equal(t, "0.00000003", q3.String(), "rounding prec 8")
|
||||||
|
assert.Equal(t, 8, prec)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user