Merge pull request #1302 from c9s/feature/grid2/use-quote-quantity

FEATURE: use quote quantity if there is QuoteQuantity in trade
This commit is contained in:
kbearXD 2023-09-19 10:41:34 +08:00 committed by GitHub
commit 6d0c266513
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 56 additions and 41 deletions

View File

@ -302,7 +302,7 @@ func convertWebSocketTrade(t max.TradeUpdate) (*types.Trade, error) {
Fee: t.Fee, Fee: t.Fee,
FeeCurrency: toGlobalCurrency(t.FeeCurrency), FeeCurrency: toGlobalCurrency(t.FeeCurrency),
FeeDiscounted: t.FeeDiscounted, FeeDiscounted: t.FeeDiscounted,
QuoteQuantity: t.Price.Mul(t.Volume), QuoteQuantity: t.Funds,
Time: types.Time(t.Timestamp.Time()), Time: types.Time(t.Timestamp.Time()),
}, nil }, nil
} }

View File

@ -98,6 +98,7 @@ type TradeUpdate struct {
Side string `json:"sd"` Side string `json:"sd"`
Price fixedpoint.Value `json:"p"` Price fixedpoint.Value `json:"p"`
Volume fixedpoint.Value `json:"v"` Volume fixedpoint.Value `json:"v"`
Funds fixedpoint.Value `json:"fn"`
Market string `json:"M"` Market string `json:"M"`
Fee fixedpoint.Value `json:"f"` Fee fixedpoint.Value `json:"f"`

View File

@ -378,9 +378,9 @@ func (s *Strategy) verifyOrderTrades(o types.Order, trades []types.Trade) bool {
return true return true
} }
// aggregateOrderFee collects the base fee quantity from the given order // aggregateOrderQuoteAmountAndBaseFee 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) aggregateOrderFee(o types.Order) (fixedpoint.Value, string) { func (s *Strategy) aggregateOrderQuoteAmountAndFee(o types.Order) (fixedpoint.Value, 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 {
@ -396,16 +396,17 @@ func (s *Strategy) aggregateOrderFee(o types.Order) (fixedpoint.Value, string) {
// 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
quoteAmount := aggregateTradesQuoteQuantity(orderTrades)
fees := collectTradeFee(orderTrades) fees := collectTradeFee(orderTrades)
if fee, ok := fees[feeCurrency]; ok { if fee, ok := fees[feeCurrency]; ok {
return fee, feeCurrency return quoteAmount, fee, feeCurrency
} }
return fixedpoint.Zero, feeCurrency return quoteAmount, 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, feeCurrency return fixedpoint.Zero, fixedpoint.Zero, feeCurrency
} }
s.logger.Warnf("GRID: missing #%d order trades or missing trade fee, pulling order trades from API", o.OrderID) s.logger.Warnf("GRID: missing #%d order trades or missing trade fee, pulling order trades from API", o.OrderID)
@ -423,13 +424,14 @@ func (s *Strategy) aggregateOrderFee(o types.Order) (fixedpoint.Value, string) {
} }
} }
quoteAmount := aggregateTradesQuoteQuantity(orderTrades)
// still try to aggregate the trades quantity if we can: // still try to aggregate the trades quantity if we can:
fees := collectTradeFee(orderTrades) fees := collectTradeFee(orderTrades)
if fee, ok := fees[feeCurrency]; ok { if fee, ok := fees[feeCurrency]; ok {
return fee, feeCurrency return quoteAmount, fee, feeCurrency
} }
return fixedpoint.Zero, feeCurrency return quoteAmount, fixedpoint.Zero, feeCurrency
} }
func (s *Strategy) processFilledOrder(o types.Order) { func (s *Strategy) processFilledOrder(o types.Order) {
@ -446,7 +448,6 @@ func (s *Strategy) processFilledOrder(o types.Order) {
} }
newQuantity := executedQuantity newQuantity := executedQuantity
executedPrice := o.Price
if o.ExecutedQuantity.Compare(o.Quantity) != 0 { if o.ExecutedQuantity.Compare(o.Quantity) != 0 {
s.logger.Warnf("order #%d is filled, but order executed quantity %s != order quantity %s, something is wrong", o.OrderID, o.ExecutedQuantity, o.Quantity) s.logger.Warnf("order #%d is filled, but order executed quantity %s != order quantity %s, something is wrong", o.OrderID, o.ExecutedQuantity, o.Quantity)
@ -458,16 +459,11 @@ func (s *Strategy) processFilledOrder(o types.Order) {
} }
*/ */
// will be used for calculating quantity
orderExecutedQuoteAmount := executedQuantity.Mul(executedPrice)
// round down order executed quote amount to avoid insufficient balance
orderExecutedQuoteAmount = orderExecutedQuoteAmount.Round(s.Market.PricePrecision, fixedpoint.Down)
// collect trades for fee // collect trades for fee
// fee calculation is used to reduce the order quantity // fee 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 // 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 // if we don't reduce the sell quantity, than we might fail to place the sell order
fee, feeCurrency := s.aggregateOrderFee(o) orderExecutedQuoteAmount, fee, feeCurrency := s.aggregateOrderQuoteAmountAndFee(o)
s.logger.Infof("GRID ORDER #%d %s FEE: %s %s", s.logger.Infof("GRID ORDER #%d %s FEE: %s %s",
o.OrderID, o.Side, o.OrderID, o.Side,
fee.String(), feeCurrency) fee.String(), feeCurrency)

View File

@ -651,7 +651,7 @@ func TestStrategy_calculateProfit(t *testing.T) {
}) })
} }
func TestStrategy_aggregateOrderBaseFee(t *testing.T) { func TestStrategy_aggregateOrderQuoteAmountAndFee(t *testing.T) {
s := newTestStrategy() s := newTestStrategy()
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
@ -666,32 +666,34 @@ func TestStrategy_aggregateOrderBaseFee(t *testing.T) {
OrderID: "3", OrderID: "3",
}).Return([]types.Trade{ }).Return([]types.Trade{
{ {
ID: 1, ID: 1,
OrderID: 3, OrderID: 3,
Exchange: "binance", Exchange: "binance",
Price: number(20000.0), Price: number(20000.0),
Quantity: number(0.2), Quantity: number(0.2),
Symbol: "BTCUSDT", QuoteQuantity: number(4000),
Side: types.SideTypeBuy, Symbol: "BTCUSDT",
IsBuyer: true, Side: types.SideTypeBuy,
FeeCurrency: "BTC", IsBuyer: true,
Fee: number(0.2 * 0.01), FeeCurrency: "BTC",
Fee: number(0.2 * 0.01),
}, },
{ {
ID: 1, ID: 1,
OrderID: 3, OrderID: 3,
Exchange: "binance", Exchange: "binance",
Price: number(20000.0), Price: number(20000.0),
Quantity: number(0.8), Quantity: number(0.8),
Symbol: "BTCUSDT", QuoteQuantity: number(16000),
Side: types.SideTypeBuy, Symbol: "BTCUSDT",
IsBuyer: true, Side: types.SideTypeBuy,
FeeCurrency: "BTC", IsBuyer: true,
Fee: number(0.8 * 0.01), FeeCurrency: "BTC",
Fee: number(0.8 * 0.01),
}, },
}, nil) }, nil)
baseFee, _ := s.aggregateOrderFee(types.Order{ quoteAmount, fee, _ := s.aggregateOrderQuoteAmountAndFee(types.Order{
SubmitOrder: types.SubmitOrder{ SubmitOrder: types.SubmitOrder{
Symbol: "BTCUSDT", Symbol: "BTCUSDT",
Side: types.SideTypeBuy, Side: types.SideTypeBuy,
@ -710,7 +712,8 @@ func TestStrategy_aggregateOrderBaseFee(t *testing.T) {
ExecutedQuantity: number(1.0), ExecutedQuantity: number(1.0),
IsWorking: false, IsWorking: false,
}) })
assert.Equal(t, "0.01", baseFee.String()) assert.Equal(t, "0.01", fee.String())
assert.Equal(t, "20000", quoteAmount.String())
} }
func TestStrategy_findDuplicatedPriceOpenOrders(t *testing.T) { func TestStrategy_findDuplicatedPriceOpenOrders(t *testing.T) {
@ -1116,7 +1119,7 @@ func TestStrategy_handleOrderFilled(t *testing.T) {
}) })
} }
func TestStrategy_aggregateOrderBaseFeeRetry(t *testing.T) { func TestStrategy_aggregateOrderQuoteAmountAndFeeRetry(t *testing.T) {
s := newTestStrategy() s := newTestStrategy()
mockCtrl := gomock.NewController(t) mockCtrl := gomock.NewController(t)
@ -1161,7 +1164,7 @@ func TestStrategy_aggregateOrderBaseFeeRetry(t *testing.T) {
}, },
}, nil) }, nil)
baseFee, _ := s.aggregateOrderFee(types.Order{ quoteAmount, fee, _ := s.aggregateOrderQuoteAmountAndFee(types.Order{
SubmitOrder: types.SubmitOrder{ SubmitOrder: types.SubmitOrder{
Symbol: "BTCUSDT", Symbol: "BTCUSDT",
Side: types.SideTypeBuy, Side: types.SideTypeBuy,
@ -1180,7 +1183,8 @@ func TestStrategy_aggregateOrderBaseFeeRetry(t *testing.T) {
ExecutedQuantity: number(1.0), ExecutedQuantity: number(1.0),
IsWorking: false, IsWorking: false,
}) })
assert.Equal(t, "0.01", baseFee.String()) assert.Equal(t, "0.01", fee.String())
assert.Equal(t, "20000", quoteAmount.String())
} }
func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) { func TestStrategy_checkMinimalQuoteInvestment(t *testing.T) {

View File

@ -29,3 +29,17 @@ func aggregateTradesQuantity(trades []types.Trade) fixedpoint.Value {
} }
return tq return tq
} }
// aggregateTradesQuoteQuantity aggregates the quote quantity from the given trade slice
func aggregateTradesQuoteQuantity(trades []types.Trade) fixedpoint.Value {
quoteQuantity := fixedpoint.Zero
for _, t := range trades {
if t.QuoteQuantity.IsZero() {
quoteQuantity = quoteQuantity.Add(t.Price.Mul(t.Quantity))
} else {
quoteQuantity = quoteQuantity.Add(t.QuoteQuantity)
}
}
return quoteQuantity
}