2023-11-24 06:46:26 +00:00
|
|
|
package dca2
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
|
2023-12-06 08:16:17 +00:00
|
|
|
"github.com/c9s/bbgo/pkg/exchange/retry"
|
2023-11-24 06:46:26 +00:00
|
|
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
|
|
|
"github.com/c9s/bbgo/pkg/types"
|
|
|
|
)
|
|
|
|
|
2023-12-06 03:22:56 +00:00
|
|
|
func (s *Strategy) placeDCAOrders(ctx context.Context) error {
|
2023-11-30 08:36:21 +00:00
|
|
|
s.logger.Infof("[DCA] start placing maker orders")
|
2023-12-06 08:16:17 +00:00
|
|
|
price, err := getBestPriceUntilSuccess(ctx, s.Session.Exchange, s.Symbol, s.Short)
|
2023-11-24 06:46:26 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2023-12-06 03:22:56 +00:00
|
|
|
orders, err := s.generateDCAOrders(s.Short, s.Budget, price, s.PriceDeviation, s.MaxOrderNum)
|
2023-11-24 06:46:26 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, orders...)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
s.debugOrders(createdOrders)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-12-06 08:16:17 +00:00
|
|
|
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
|
2023-11-24 06:46:26 +00:00
|
|
|
}
|
|
|
|
|
2023-12-06 08:16:17 +00:00
|
|
|
if short {
|
|
|
|
return ticker.Buy, nil
|
|
|
|
} else {
|
|
|
|
return ticker.Sell, nil
|
|
|
|
}
|
2023-11-24 06:46:26 +00:00
|
|
|
}
|
|
|
|
|
2023-12-06 03:22:56 +00:00
|
|
|
func (s *Strategy) generateDCAOrders(short bool, budget, price, priceDeviation fixedpoint.Value, maxOrderNum int64) ([]types.SubmitOrder, error) {
|
|
|
|
// TODO: not implement short part yet
|
|
|
|
factor := fixedpoint.One.Sub(priceDeviation)
|
|
|
|
if short {
|
|
|
|
factor = fixedpoint.One.Add(priceDeviation)
|
2023-11-24 06:46:26 +00:00
|
|
|
}
|
|
|
|
|
2023-12-06 03:22:56 +00:00
|
|
|
// calculate all valid prices
|
2023-11-24 06:46:26 +00:00
|
|
|
var prices []fixedpoint.Value
|
2023-12-06 03:22:56 +00:00
|
|
|
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 {
|
2023-11-24 06:46:26 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2023-12-06 03:22:56 +00:00
|
|
|
prices = append(prices, price)
|
2023-11-24 06:46:26 +00:00
|
|
|
}
|
|
|
|
|
2023-12-06 03:22:56 +00:00
|
|
|
notional, orderNum := calculateDCAMakerOrderNotionalAndNum(s.Market, short, budget, prices)
|
|
|
|
if orderNum == 0 {
|
|
|
|
return nil, fmt.Errorf("failed to calculate DCA maker order notional and num, price: %s, budget: %s", price, budget)
|
2023-11-24 06:46:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var submitOrders []types.SubmitOrder
|
2023-12-06 03:22:56 +00:00
|
|
|
for i := 0; i < orderNum; i++ {
|
|
|
|
quantity := s.Market.TruncateQuantity(notional.Div(prices[i]))
|
2023-11-24 06:46:26 +00:00
|
|
|
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
|
|
|
|
}
|
2023-12-06 03:22:56 +00:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|