Skip to content

Commit

Permalink
Merge pull request #1271 from c9s/c9s/strategy-convert
Browse files Browse the repository at this point in the history
REFACTOR: apply market.GreaterThanMinimalOrderQuantity on both convert and xalign
  • Loading branch information
c9s authored Aug 8, 2023
2 parents adcd4d7 + c3cce05 commit 85201d0
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 84 deletions.
53 changes: 11 additions & 42 deletions pkg/strategy/convert/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})

Expand Down Expand Up @@ -327,35 +327,24 @@ 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
}

orderForm := types.SubmitOrder{
Symbol: market.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeLimit,
Quantity: available,
Quantity: quantity,
Price: price,
Market: market,
TimeInForce: types.TimeInForceGTC,
Expand All @@ -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
}

Expand Down
77 changes: 35 additions & 42 deletions pkg/strategy/xalign/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,9 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st
// changeQuantity < 0 = sell
q := changeQuantity.Abs()

// a fast filtering
if q.Compare(market.MinQuantity) < 0 {
log.Infof("skip dust quantity: %f", q.Float64())
log.Debugf("skip dust quantity: %f", q.Float64())
continue
}

Expand All @@ -155,11 +156,6 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st
switch side {

case types.SideTypeBuy:
quoteBalance, ok := session.Account.Balance(quoteCurrency)
if !ok {
continue
}

price := ticker.Sell
if taker {
price = ticker.Sell
Expand All @@ -169,41 +165,31 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st
price = ticker.Buy
}

quoteBalance, ok := session.Account.Balance(quoteCurrency)
if !ok {
continue
}

requiredQuoteAmount := q.Mul(price)
requiredQuoteAmount = requiredQuoteAmount.Round(market.PricePrecision, fixedpoint.Up)
if requiredQuoteAmount.Compare(quoteBalance.Available) > 0 {
log.Warnf("required quote amount %f > quote balance %v, skip", requiredQuoteAmount.Float64(), quoteBalance)
continue
}

if market.IsDustQuantity(q, price) {
log.Infof("%s ignore dust quantity: %f", currency, q.Float64())
return nil, nil
}

q = market.AdjustQuantityByMinNotional(q, price)

return session, &types.SubmitOrder{
Symbol: symbol,
Side: side,
Type: types.OrderTypeLimit,
Quantity: q,
Price: price,
Market: market,
TimeInForce: "GTC",
if quantity, ok := market.GreaterThanMinimalOrderQuantity(side, price, requiredQuoteAmount); ok {
return session, &types.SubmitOrder{
Symbol: symbol,
Side: side,
Type: types.OrderTypeLimit,
Quantity: quantity,
Price: price,
Market: market,
TimeInForce: types.TimeInForceGTC,
}
}

case types.SideTypeSell:
baseBalance, ok := session.Account.Balance(currency)
if !ok {
continue
}

if q.Compare(baseBalance.Available) > 0 {
log.Warnf("required base amount %f < available base balance %v, skip", q.Float64(), baseBalance)
continue
}

price := ticker.Buy
if taker {
price = ticker.Buy
Expand All @@ -213,19 +199,26 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st
price = ticker.Sell
}

if market.IsDustQuantity(q, price) {
log.Infof("%s ignore dust quantity: %f", currency, q.Float64())
return nil, nil
baseBalance, ok := session.Account.Balance(currency)
if !ok {
continue
}

if q.Compare(baseBalance.Available) > 0 {
log.Warnf("required base amount %f < available base balance %v, skip", q.Float64(), baseBalance)
continue
}

return session, &types.SubmitOrder{
Symbol: symbol,
Side: side,
Type: types.OrderTypeLimit,
Quantity: q,
Price: price,
Market: market,
TimeInForce: "GTC",
if quantity, ok := market.GreaterThanMinimalOrderQuantity(side, price, q); ok {
return session, &types.SubmitOrder{
Symbol: symbol,
Side: side,
Type: types.OrderTypeLimit,
Quantity: quantity,
Price: price,
Market: market,
TimeInForce: types.TimeInForceGTC,
}
}
}

Expand Down
46 changes: 46 additions & 0 deletions pkg/types/market.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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 {
Expand Down
25 changes: 25 additions & 0 deletions pkg/types/market_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
41 changes: 41 additions & 0 deletions pkg/util/tradingutil/trades_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package tradingutil

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)

var number = fixedpoint.MustNewFromString

func Test_CollectTradeFee(t *testing.T) {
trades := []types.Trade{
{
ID: 1,
Price: number("21000"),
Quantity: number("0.001"),
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
Fee: number("0.00001"),
FeeCurrency: "BTC",
FeeDiscounted: false,
},
{
ID: 2,
Price: number("21200"),
Quantity: number("0.001"),
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
Fee: number("0.00002"),
FeeCurrency: "BTC",
FeeDiscounted: false,
},
}

fees := CollectTradeFee(trades)
assert.NotNil(t, fees)
assert.Equal(t, number("0.00003"), fees["BTC"])
}

0 comments on commit 85201d0

Please sign in to comment.