From 8b6a8aeb7b773fd7a8f094837c9153fae0cee200 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 5 Aug 2023 16:39:03 +0800 Subject: [PATCH] convert: move moq check/adjustment to types.Market --- pkg/strategy/convert/strategy.go | 53 +++++++------------------------- pkg/types/market.go | 46 +++++++++++++++++++++++++++ pkg/types/market_test.go | 25 +++++++++++++++ 3 files changed, 82 insertions(+), 42 deletions(-) diff --git a/pkg/strategy/convert/strategy.go b/pkg/strategy/convert/strategy.go index 5f8618864..419ea658d 100644 --- a/pkg/strategy/convert/strategy.go +++ b/pkg/strategy/convert/strategy.go @@ -162,7 +162,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) { s.collectPendingQuantity(ctx) - + _ = s.orderExecutor.GracefulCancel(ctx) }) @@ -327,27 +327,16 @@ func (s *Strategy) convertBalance(ctx context.Context, fromAsset string, availab switch fromAsset { case market.BaseCurrency: - log.Infof("converting %s %s to %s...", available, fromAsset, market.QuoteCurrency) - - available = market.TruncateQuantity(available) - - // from = Base -> action = sell - if available.Compare(market.MinQuantity) < 0 { - log.Debugf("asset %s %s is less than minQuantity %s, skip convert", available, fromAsset, market.MinQuantity) - return nil - } - price := ticker.Sell if s.UseTakerOrder { price = ticker.Buy } - quoteAmount := price.Mul(available) - if quoteAmount.Compare(market.MinNotional) < 0 { - log.Debugf("asset %s %s (%s %s) is less than minNotional %s, skip convert", - available, fromAsset, - quoteAmount, market.QuoteCurrency, - market.MinNotional) + log.Infof("converting %s %s to %s...", available, fromAsset, market.QuoteCurrency) + + quantity, ok := market.GreaterThanMinimalOrderQuantity(types.SideTypeSell, price, available) + if !ok { + log.Debugf("asset %s %s is less than MoQ, skip convert", available, fromAsset) return nil } @@ -355,7 +344,7 @@ func (s *Strategy) convertBalance(ctx context.Context, fromAsset string, availab Symbol: market.Symbol, Side: types.SideTypeSell, Type: types.OrderTypeLimit, - Quantity: available, + Quantity: quantity, Price: price, Market: market, TimeInForce: types.TimeInForceGTC, @@ -365,36 +354,16 @@ func (s *Strategy) convertBalance(ctx context.Context, fromAsset string, availab } case market.QuoteCurrency: - log.Infof("converting %s %s to %s...", available, fromAsset, market.BaseCurrency) - - available = market.TruncateQuoteQuantity(available) - - // from = Quote -> action = buy - if available.Compare(market.MinNotional) < 0 { - log.Debugf("asset %s %s is less than minNotional %s, skip convert", available, fromAsset, market.MinNotional) - return nil - } - price := ticker.Buy if s.UseTakerOrder { price = ticker.Sell } - quantity := available.Div(price) - quantity = market.TruncateQuantity(quantity) - if quantity.Compare(market.MinQuantity) < 0 { - log.Debugf("asset %s %s is less than minQuantity %s, skip convert", - quantity, fromAsset, - market.MinQuantity) - return nil - } + log.Infof("converting %s %s to %s...", available, fromAsset, market.BaseCurrency) - notional := quantity.Mul(price) - if notional.Compare(market.MinNotional) < 0 { - log.Debugf("asset %s %s (%s %s) is less than minNotional %s, skip convert", - quantity, fromAsset, - notional, market.QuoteCurrency, - market.MinNotional) + quantity, ok := market.GreaterThanMinimalOrderQuantity(types.SideTypeBuy, price, available) + if !ok { + log.Debugf("asset %s %s is less than MoQ, skip convert", available, fromAsset) return nil } diff --git a/pkg/types/market.go b/pkg/types/market.go index 0c598799f..07f2cc0da 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -72,6 +72,7 @@ func (m Market) TruncateQuantity(quantity fixedpoint.Value) fixedpoint.Value { } // TruncateQuoteQuantity uses the tick size to truncate floating number, in order to avoid the rounding issue +// this is usually used for calculating the order size from the quote quantity. func (m Market) TruncateQuoteQuantity(quantity fixedpoint.Value) fixedpoint.Value { var ts = m.TickSize.Float64() var prec = int(math.Round(math.Log10(ts) * -1.0)) @@ -84,6 +85,51 @@ func (m Market) TruncateQuoteQuantity(quantity fixedpoint.Value) fixedpoint.Valu return fixedpoint.MustNewFromString(qs) } +// GreaterThanMinimalOrderQuantity ensures that your given balance could fit the minimal order quantity +// when side = sell, then available = base balance +// when side = buy, then available = quote balance +// The balance will be truncated first in order to calculate the minimal notional and minimal quantity +// The adjusted (truncated) order quantity will be returned +func (m Market) GreaterThanMinimalOrderQuantity(side SideType, price, available fixedpoint.Value) (fixedpoint.Value, bool) { + switch side { + case SideTypeSell: + available = m.TruncateQuantity(available) + + if available.Compare(m.MinQuantity) < 0 { + return fixedpoint.Zero, false + } + + quoteAmount := price.Mul(available) + if quoteAmount.Compare(m.MinNotional) < 0 { + return fixedpoint.Zero, false + } + + return available, true + + case SideTypeBuy: + available = m.TruncateQuoteQuantity(available) + + if available.Compare(m.MinNotional) < 0 { + return fixedpoint.Zero, false + } + + quantity := available.Div(price) + quantity = m.TruncateQuantity(quantity) + if quantity.Compare(m.MinQuantity) < 0 { + return fixedpoint.Zero, false + } + + notional := quantity.Mul(price) + if notional.Compare(m.MinNotional) < 0 { + return fixedpoint.Zero, false + } + + return quantity, true + } + + return available, true +} + // RoundDownQuantityByPrecision uses the volume precision to round down the quantity // This is different from the TruncateQuantity, which uses StepSize (it uses fewer fractions to truncate) func (m Market) RoundDownQuantityByPrecision(quantity fixedpoint.Value) fixedpoint.Value { diff --git a/pkg/types/market_test.go b/pkg/types/market_test.go index 7966a6d5a..a20121750 100644 --- a/pkg/types/market_test.go +++ b/pkg/types/market_test.go @@ -13,6 +13,31 @@ import ( var s func(string) fixedpoint.Value = fixedpoint.MustNewFromString +func TestMarket_GreaterThanMinimalOrderQuantity(t *testing.T) { + market := Market{ + Symbol: "BTCUSDT", + LocalSymbol: "BTCUSDT", + PricePrecision: 8, + VolumePrecision: 8, + QuoteCurrency: "USDT", + BaseCurrency: "BTC", + MinNotional: number(10.0), + MinAmount: number(10.0), + MinQuantity: number(0.0001), + StepSize: number(0.00001), + TickSize: number(0.01), + } + + _, ok := market.GreaterThanMinimalOrderQuantity(SideTypeSell, number(20000.0), number(0.00051)) + assert.True(t, ok) + + _, ok = market.GreaterThanMinimalOrderQuantity(SideTypeBuy, number(20000.0), number(10.0)) + assert.True(t, ok) + + _, ok = market.GreaterThanMinimalOrderQuantity(SideTypeBuy, number(20000.0), number(0.99999)) + assert.False(t, ok) +} + func TestFormatQuantity(t *testing.T) { quantity := formatQuantity( s("0.12511"),