mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
Merge pull request #1271 from c9s/c9s/strategy-convert
REFACTOR: apply market.GreaterThanMinimalOrderQuantity on both convert and xalign
This commit is contained in:
commit
85201d0b57
|
@ -327,27 +327,16 @@ func (s *Strategy) convertBalance(ctx context.Context, fromAsset string, availab
|
||||||
switch fromAsset {
|
switch fromAsset {
|
||||||
|
|
||||||
case market.BaseCurrency:
|
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
|
price := ticker.Sell
|
||||||
if s.UseTakerOrder {
|
if s.UseTakerOrder {
|
||||||
price = ticker.Buy
|
price = ticker.Buy
|
||||||
}
|
}
|
||||||
|
|
||||||
quoteAmount := price.Mul(available)
|
log.Infof("converting %s %s to %s...", available, fromAsset, market.QuoteCurrency)
|
||||||
if quoteAmount.Compare(market.MinNotional) < 0 {
|
|
||||||
log.Debugf("asset %s %s (%s %s) is less than minNotional %s, skip convert",
|
quantity, ok := market.GreaterThanMinimalOrderQuantity(types.SideTypeSell, price, available)
|
||||||
available, fromAsset,
|
if !ok {
|
||||||
quoteAmount, market.QuoteCurrency,
|
log.Debugf("asset %s %s is less than MoQ, skip convert", available, fromAsset)
|
||||||
market.MinNotional)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -355,7 +344,7 @@ func (s *Strategy) convertBalance(ctx context.Context, fromAsset string, availab
|
||||||
Symbol: market.Symbol,
|
Symbol: market.Symbol,
|
||||||
Side: types.SideTypeSell,
|
Side: types.SideTypeSell,
|
||||||
Type: types.OrderTypeLimit,
|
Type: types.OrderTypeLimit,
|
||||||
Quantity: available,
|
Quantity: quantity,
|
||||||
Price: price,
|
Price: price,
|
||||||
Market: market,
|
Market: market,
|
||||||
TimeInForce: types.TimeInForceGTC,
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
@ -365,36 +354,16 @@ func (s *Strategy) convertBalance(ctx context.Context, fromAsset string, availab
|
||||||
}
|
}
|
||||||
|
|
||||||
case market.QuoteCurrency:
|
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
|
price := ticker.Buy
|
||||||
if s.UseTakerOrder {
|
if s.UseTakerOrder {
|
||||||
price = ticker.Sell
|
price = ticker.Sell
|
||||||
}
|
}
|
||||||
|
|
||||||
quantity := available.Div(price)
|
log.Infof("converting %s %s to %s...", available, fromAsset, market.BaseCurrency)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
notional := quantity.Mul(price)
|
quantity, ok := market.GreaterThanMinimalOrderQuantity(types.SideTypeBuy, price, available)
|
||||||
if notional.Compare(market.MinNotional) < 0 {
|
if !ok {
|
||||||
log.Debugf("asset %s %s (%s %s) is less than minNotional %s, skip convert",
|
log.Debugf("asset %s %s is less than MoQ, skip convert", available, fromAsset)
|
||||||
quantity, fromAsset,
|
|
||||||
notional, market.QuoteCurrency,
|
|
||||||
market.MinNotional)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -145,8 +145,9 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st
|
||||||
// changeQuantity < 0 = sell
|
// changeQuantity < 0 = sell
|
||||||
q := changeQuantity.Abs()
|
q := changeQuantity.Abs()
|
||||||
|
|
||||||
|
// a fast filtering
|
||||||
if q.Compare(market.MinQuantity) < 0 {
|
if q.Compare(market.MinQuantity) < 0 {
|
||||||
log.Infof("skip dust quantity: %f", q.Float64())
|
log.Debugf("skip dust quantity: %f", q.Float64())
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,11 +156,6 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st
|
||||||
switch side {
|
switch side {
|
||||||
|
|
||||||
case types.SideTypeBuy:
|
case types.SideTypeBuy:
|
||||||
quoteBalance, ok := session.Account.Balance(quoteCurrency)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
price := ticker.Sell
|
price := ticker.Sell
|
||||||
if taker {
|
if taker {
|
||||||
price = ticker.Sell
|
price = ticker.Sell
|
||||||
|
@ -169,6 +165,11 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st
|
||||||
price = ticker.Buy
|
price = ticker.Buy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
quoteBalance, ok := session.Account.Balance(quoteCurrency)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
requiredQuoteAmount := q.Mul(price)
|
requiredQuoteAmount := q.Mul(price)
|
||||||
requiredQuoteAmount = requiredQuoteAmount.Round(market.PricePrecision, fixedpoint.Up)
|
requiredQuoteAmount = requiredQuoteAmount.Round(market.PricePrecision, fixedpoint.Up)
|
||||||
if requiredQuoteAmount.Compare(quoteBalance.Available) > 0 {
|
if requiredQuoteAmount.Compare(quoteBalance.Available) > 0 {
|
||||||
|
@ -176,24 +177,28 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if market.IsDustQuantity(q, price) {
|
if quantity, ok := market.GreaterThanMinimalOrderQuantity(side, price, requiredQuoteAmount); ok {
|
||||||
log.Infof("%s ignore dust quantity: %f", currency, q.Float64())
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
q = market.AdjustQuantityByMinNotional(q, price)
|
|
||||||
|
|
||||||
return session, &types.SubmitOrder{
|
return session, &types.SubmitOrder{
|
||||||
Symbol: symbol,
|
Symbol: symbol,
|
||||||
Side: side,
|
Side: side,
|
||||||
Type: types.OrderTypeLimit,
|
Type: types.OrderTypeLimit,
|
||||||
Quantity: q,
|
Quantity: quantity,
|
||||||
Price: price,
|
Price: price,
|
||||||
Market: market,
|
Market: market,
|
||||||
TimeInForce: "GTC",
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
case types.SideTypeSell:
|
case types.SideTypeSell:
|
||||||
|
price := ticker.Buy
|
||||||
|
if taker {
|
||||||
|
price = ticker.Buy
|
||||||
|
} else if spread.Compare(market.TickSize) > 0 {
|
||||||
|
price = ticker.Buy.Add(market.TickSize)
|
||||||
|
} else {
|
||||||
|
price = ticker.Sell
|
||||||
|
}
|
||||||
|
|
||||||
baseBalance, ok := session.Account.Balance(currency)
|
baseBalance, ok := session.Account.Balance(currency)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
|
@ -204,28 +209,16 @@ func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[st
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
price := ticker.Buy
|
if quantity, ok := market.GreaterThanMinimalOrderQuantity(side, price, q); ok {
|
||||||
if taker {
|
|
||||||
price = ticker.Buy
|
|
||||||
} else if spread.Compare(market.TickSize) > 0 {
|
|
||||||
price = ticker.Buy.Add(market.TickSize)
|
|
||||||
} else {
|
|
||||||
price = ticker.Sell
|
|
||||||
}
|
|
||||||
|
|
||||||
if market.IsDustQuantity(q, price) {
|
|
||||||
log.Infof("%s ignore dust quantity: %f", currency, q.Float64())
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return session, &types.SubmitOrder{
|
return session, &types.SubmitOrder{
|
||||||
Symbol: symbol,
|
Symbol: symbol,
|
||||||
Side: side,
|
Side: side,
|
||||||
Type: types.OrderTypeLimit,
|
Type: types.OrderTypeLimit,
|
||||||
Quantity: q,
|
Quantity: quantity,
|
||||||
Price: price,
|
Price: price,
|
||||||
Market: market,
|
Market: market,
|
||||||
TimeInForce: "GTC",
|
TimeInForce: types.TimeInForceGTC,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
// 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 {
|
func (m Market) TruncateQuoteQuantity(quantity fixedpoint.Value) fixedpoint.Value {
|
||||||
var ts = m.TickSize.Float64()
|
var ts = m.TickSize.Float64()
|
||||||
var prec = int(math.Round(math.Log10(ts) * -1.0))
|
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)
|
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
|
// 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)
|
// This is different from the TruncateQuantity, which uses StepSize (it uses fewer fractions to truncate)
|
||||||
func (m Market) RoundDownQuantityByPrecision(quantity fixedpoint.Value) fixedpoint.Value {
|
func (m Market) RoundDownQuantityByPrecision(quantity fixedpoint.Value) fixedpoint.Value {
|
||||||
|
|
|
@ -13,6 +13,31 @@ import (
|
||||||
|
|
||||||
var s func(string) fixedpoint.Value = fixedpoint.MustNewFromString
|
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) {
|
func TestFormatQuantity(t *testing.T) {
|
||||||
quantity := formatQuantity(
|
quantity := formatQuantity(
|
||||||
s("0.12511"),
|
s("0.12511"),
|
||||||
|
|
41
pkg/util/tradingutil/trades_test.go
Normal file
41
pkg/util/tradingutil/trades_test.go
Normal file
|
@ -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"])
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user