Merge pull request #1433 from c9s/kbearXD/dca2/open-maker-orders

FEATURE: prepare open maker orders function
This commit is contained in:
kbearXD 2023-12-07 13:46:23 +08:00 committed by GitHub
commit 9101d7ad4a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 205 additions and 0 deletions

View File

@ -0,0 +1,17 @@
package retry
import (
"context"
"github.com/c9s/bbgo/pkg/types"
)
func QueryTickerUntilSuccessful(ctx context.Context, ex types.Exchange, symbol string) (ticker *types.Ticker, err error) {
var op = func() (err2 error) {
ticker, err2 = ex.QueryTicker(ctx, symbol)
return err2
}
err = GeneralBackoff(ctx, op)
return ticker, err
}

View File

@ -0,0 +1,113 @@
package dca2
import (
"context"
"fmt"
"github.com/c9s/bbgo/pkg/exchange/retry"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
func (s *Strategy) placeOpenPositionOrders(ctx context.Context) error {
s.logger.Infof("[DCA] start placing open position orders")
price, err := getBestPriceUntilSuccess(ctx, s.Session.Exchange, s.Symbol, s.Short)
if err != nil {
return err
}
orders, err := s.generateOpenPositionOrders(s.Short, s.Budget, price, s.PriceDeviation, s.MaxOrderNum)
if err != nil {
return err
}
createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, orders...)
if err != nil {
return err
}
s.debugOrders(createdOrders)
return nil
}
func getBestPriceUntilSuccess(ctx context.Context, ex types.Exchange, symbol string, short bool) (fixedpoint.Value, error) {
ticker, err := retry.QueryTickerUntilSuccessful(ctx, ex, symbol)
if err != nil {
return fixedpoint.Zero, err
}
if short {
return ticker.Buy, nil
} else {
return ticker.Sell, nil
}
}
func (s *Strategy) generateOpenPositionOrders(short bool, budget, price, priceDeviation fixedpoint.Value, maxOrderNum int64) ([]types.SubmitOrder, error) {
factor := fixedpoint.One.Sub(priceDeviation)
if short {
factor = fixedpoint.One.Add(priceDeviation)
}
// calculate all valid prices
var prices []fixedpoint.Value
for i := 0; i < int(maxOrderNum); i++ {
if i > 0 {
price = price.Mul(factor)
}
price = s.Market.TruncatePrice(price)
if price.Compare(s.Market.MinPrice) < 0 {
break
}
prices = append(prices, price)
}
notional, orderNum := calculateNotionalAndNum(s.Market, short, budget, prices)
if orderNum == 0 {
return nil, fmt.Errorf("failed to calculate notional and num of open position orders, price: %s, budget: %s", price, budget)
}
var submitOrders []types.SubmitOrder
for i := 0; i < orderNum; i++ {
quantity := s.Market.TruncateQuantity(notional.Div(prices[i]))
submitOrders = append(submitOrders, types.SubmitOrder{
Symbol: s.Symbol,
Market: s.Market,
Type: types.OrderTypeLimit,
Price: prices[i],
Side: s.makerSide,
TimeInForce: types.TimeInForceGTC,
Quantity: quantity,
Tag: orderTag,
GroupID: s.OrderGroupID,
})
}
return submitOrders, nil
}
// calculateNotionalAndNum calculates the notional and num of open position orders
// DCA2 is notional-based, every order has the same notional
func calculateNotionalAndNum(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

@ -0,0 +1,75 @@
package dca2
import (
"testing"
"github.com/sirupsen/logrus"
. "github.com/c9s/bbgo/pkg/testing/testhelper"
"github.com/c9s/bbgo/pkg/types"
"github.com/stretchr/testify/assert"
)
func newTestMarket() types.Market {
return types.Market{
BaseCurrency: "BTC",
QuoteCurrency: "USDT",
TickSize: Number(0.01),
StepSize: Number(0.000001),
PricePrecision: 2,
VolumePrecision: 8,
MinNotional: Number(8.0),
MinQuantity: Number(0.0003),
}
}
func newTestStrategy(va ...string) *Strategy {
symbol := "BTCUSDT"
if len(va) > 0 {
symbol = va[0]
}
market := newTestMarket()
s := &Strategy{
logger: logrus.NewEntry(logrus.New()),
Symbol: symbol,
Market: market,
}
return s
}
func TestGenerateOpenPositionOrders(t *testing.T) {
assert := assert.New(t)
strategy := newTestStrategy()
t.Run("case 1: all config is valid and we can place enough orders", func(t *testing.T) {
budget := Number("10500")
askPrice := Number("30000")
margin := Number("0.05")
submitOrders, err := strategy.generateOpenPositionOrders(false, budget, askPrice, margin, 4)
if !assert.NoError(err) {
return
}
assert.Len(submitOrders, 4)
assert.Equal(Number("30000"), submitOrders[0].Price)
assert.Equal(Number("0.0875"), submitOrders[0].Quantity)
assert.Equal(Number("28500"), submitOrders[1].Price)
assert.Equal(Number("0.092105"), submitOrders[1].Quantity)
assert.Equal(Number("27075"), submitOrders[2].Price)
assert.Equal(Number("0.096952"), submitOrders[2].Quantity)
assert.Equal(Number("25721.25"), submitOrders[3].Price)
assert.Equal(Number("0.102055"), submitOrders[3].Quantity)
})
t.Run("case 2: some orders' price will below 0, so 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) {
})
t.Run("case 4: quantity is too small, so we should decrease num of orders", func(t *testing.T) {
})
}