Merge pull request #1076 from c9s/fix/grid2/fix-fee-reduction

fix: grid2: quantity fee reduction for quote currency
This commit is contained in:
Yo-An Lin 2023-03-03 14:36:23 +08:00 committed by GitHub
commit bfcb60d9d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 110 additions and 40 deletions

View File

@ -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
} }

View File

@ -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)
} }