FEATURE: use notional based to crease dca maker orders

This commit is contained in:
chiahung.lin 2023-12-06 11:22:56 +08:00
parent 60003fc472
commit 4aa6ea3a46
2 changed files with 59 additions and 74 deletions

View File

@ -9,14 +9,14 @@ import (
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
func (s *Strategy) placeMakerOrders(ctx context.Context) error { func (s *Strategy) placeDCAOrders(ctx context.Context) error {
s.logger.Infof("[DCA] start placing maker orders") s.logger.Infof("[DCA] start placing maker orders")
price, err := s.getBestPriceUntilSuccess(ctx, s.Short) price, err := s.getBestPriceUntilSuccess(ctx, s.Short)
if err != nil { if err != nil {
return err return err
} }
orders, err := s.generateMakerOrder(s.Short, s.Budget, price, s.PriceDeviation, s.MaxOrderNum) orders, err := s.generateDCAOrders(s.Short, s.Budget, price, s.PriceDeviation, s.MaxOrderNum)
if err != nil { if err != nil {
return err return err
} }
@ -50,47 +50,35 @@ func (s *Strategy) getBestPriceUntilSuccess(ctx context.Context, short bool) (fi
return fixedpoint.Zero, err return fixedpoint.Zero, err
} }
func (s *Strategy) generateMakerOrder(short bool, budget, price, margin fixedpoint.Value, orderNum int64) ([]types.SubmitOrder, error) { func (s *Strategy) generateDCAOrders(short bool, budget, price, priceDeviation fixedpoint.Value, maxOrderNum int64) ([]types.SubmitOrder, error) {
marginPrice := price.Mul(margin)
if !short {
marginPrice = marginPrice.Neg()
}
// TODO: not implement short part yet // TODO: not implement short part yet
factor := fixedpoint.One.Sub(priceDeviation)
if short {
factor = fixedpoint.One.Add(priceDeviation)
}
// calculate all valid prices
var prices []fixedpoint.Value var prices []fixedpoint.Value
var total fixedpoint.Value for i := 0; i < int(maxOrderNum); i++ {
for i := 0; i < int(orderNum); i++ { if i > 0 {
price = price.Add(marginPrice) price = price.Mul(factor)
truncatePrice := s.Market.TruncatePrice(price) }
price = s.Market.TruncatePrice(price)
// need to avoid the price is below 0 if price.Compare(s.Market.MinPrice) < 0 {
if truncatePrice.Compare(fixedpoint.Zero) <= 0 {
break break
} }
prices = append(prices, truncatePrice) prices = append(prices, price)
total = total.Add(truncatePrice)
} }
quantity := fixedpoint.Zero notional, orderNum := calculateDCAMakerOrderNotionalAndNum(s.Market, short, budget, prices)
l := len(prices) - 1 if orderNum == 0 {
for ; l >= 0; l-- { return nil, fmt.Errorf("failed to calculate DCA maker order notional and num, price: %s, budget: %s", price, budget)
if total.IsZero() {
return nil, fmt.Errorf("total is zero, please check it")
}
quantity = budget.Div(total)
quantity = s.Market.TruncateQuantity(quantity)
if prices[l].Mul(quantity).Compare(s.Market.MinNotional) > 0 {
break
}
total = total.Sub(prices[l])
} }
var submitOrders []types.SubmitOrder var submitOrders []types.SubmitOrder
for i := 0; i < orderNum; i++ {
for i := 0; i <= l; i++ { quantity := s.Market.TruncateQuantity(notional.Div(prices[i]))
submitOrders = append(submitOrders, types.SubmitOrder{ submitOrders = append(submitOrders, types.SubmitOrder{
Symbol: s.Symbol, Symbol: s.Symbol,
Market: s.Market, Market: s.Market,
@ -106,3 +94,27 @@ func (s *Strategy) generateMakerOrder(short bool, budget, price, margin fixedpoi
return submitOrders, nil return submitOrders, nil
} }
// calculateDCAMakerNotionalAndNum will calculate the notional and num of DCA orders
// DCA2 is notional-based, every order has the same notional
func calculateDCAMakerOrderNotionalAndNum(market types.Market, short bool, budget fixedpoint.Value, prices []fixedpoint.Value) (fixedpoint.Value, int) {
for num := len(prices); num > 0; num-- {
notional := budget.Div(fixedpoint.NewFromInt(int64(num)))
if notional.Compare(market.MinNotional) < 0 {
continue
}
maxPriceIdx := 0
if short {
maxPriceIdx = num - 1
}
quantity := market.TruncateQuantity(notional.Div(prices[maxPriceIdx]))
if quantity.Compare(market.MinQuantity) < 0 {
continue
}
return notional, num
}
return fixedpoint.Zero, 0
}

View File

@ -45,58 +45,31 @@ func TestGenerateMakerOrder(t *testing.T) {
strategy := newTestStrategy() strategy := newTestStrategy()
t.Run("case 1: all config is valid and we can place enough orders", func(t *testing.T) { t.Run("case 1: all config is valid and we can place enough orders", func(t *testing.T) {
budget := Number("105000") budget := Number("10500")
askPrice := Number("30000") askPrice := Number("30000")
margin := Number("0.05") margin := Number("0.05")
submitOrders, err := strategy.generateMakerOrder(false, budget, askPrice, margin, 4) submitOrders, err := strategy.generateDCAOrders(false, budget, askPrice, margin, 4)
if !assert.NoError(err) { if !assert.NoError(err) {
return return
} }
assert.Len(submitOrders, 4) assert.Len(submitOrders, 4)
assert.Equal(submitOrders[0].Price, Number("28500")) assert.Equal(Number("30000"), submitOrders[0].Price)
assert.Equal(submitOrders[1].Price, Number("27000")) assert.Equal(Number("0.0875"), submitOrders[0].Quantity)
assert.Equal(submitOrders[2].Price, Number("25500")) assert.Equal(Number("28500"), submitOrders[1].Price)
assert.Equal(submitOrders[3].Price, Number("24000")) assert.Equal(Number("0.092105"), submitOrders[1].Quantity)
assert.Equal(submitOrders[0].Quantity, Number("1")) assert.Equal(Number("27075"), submitOrders[2].Price)
assert.Equal(submitOrders[1].Quantity, Number("1")) assert.Equal(Number("0.096952"), submitOrders[2].Quantity)
assert.Equal(submitOrders[2].Quantity, Number("1")) assert.Equal(Number("25721.25"), submitOrders[3].Price)
assert.Equal(submitOrders[3].Quantity, Number("1")) assert.Equal(Number("0.102055"), submitOrders[3].Quantity)
}) })
t.Run("case 2: some orders' price will below 0 and we should not create such order", func(t *testing.T) { t.Run("case 2: some orders' price will below 0, so we should not create such order", func(t *testing.T) {
budget := Number("100000")
askPrice := Number("30000")
margin := Number("0.2")
submitOrders, err := strategy.generateMakerOrder(false, budget, askPrice, margin, 5)
if !assert.NoError(err) {
return
}
assert.Len(submitOrders, 4)
assert.Equal(submitOrders[0].Price, Number("24000"))
assert.Equal(submitOrders[1].Price, Number("18000"))
assert.Equal(submitOrders[2].Price, Number("12000"))
assert.Equal(submitOrders[3].Price, Number("6000"))
assert.Equal(submitOrders[0].Quantity, Number("1.666666"))
assert.Equal(submitOrders[1].Quantity, Number("1.666666"))
assert.Equal(submitOrders[2].Quantity, Number("1.666666"))
assert.Equal(submitOrders[3].Quantity, Number("1.666666"))
}) })
t.Run("case 3: some orders' notional is too small and we should not create such order", func(t *testing.T) { t.Run("case 3: notional is too small, so we should decrease num of orders", func(t *testing.T) {
budget := Number("30") })
askPrice := Number("30000")
margin := Number("0.2")
submitOrders, err := strategy.generateMakerOrder(false, budget, askPrice, margin, 5)
if !assert.NoError(err) {
return
}
assert.Len(submitOrders, 2) t.Run("case 4: quantity is too small, so we should decrease num of orders", func(t *testing.T) {
assert.Equal(submitOrders[0].Price, Number("24000"))
assert.Equal(submitOrders[1].Price, Number("18000"))
assert.Equal(submitOrders[0].Quantity, Number("0.000714"))
assert.Equal(submitOrders[1].Quantity, Number("0.000714"))
}) })
} }