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-11-24 08:04:48 +00:00
|
|
|
type cancelOrdersByGroupIDApi interface {
|
|
|
|
CancelOrdersByGroupID(ctx context.Context, groupID int64) ([]types.Order, error)
|
|
|
|
}
|
|
|
|
|
2023-12-07 03:27:28 +00:00
|
|
|
func (s *Strategy) placeOpenPositionOrders(ctx context.Context) error {
|
|
|
|
s.logger.Infof("[DCA] start placing open position 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-07 06:38:13 +00:00
|
|
|
orders, err := generateOpenPositionOrders(s.Market, s.Short, s.Budget, price, s.PriceDeviation, s.MaxOrderNum, s.OrderGroupID)
|
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-07 06:38:13 +00:00
|
|
|
func generateOpenPositionOrders(market types.Market, short bool, budget, price, priceDeviation fixedpoint.Value, maxOrderNum int64, orderGroupID uint32) ([]types.SubmitOrder, error) {
|
2023-12-06 03:22:56 +00:00
|
|
|
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)
|
|
|
|
}
|
2023-12-07 06:38:13 +00:00
|
|
|
price = market.TruncatePrice(price)
|
|
|
|
if price.Compare(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-07 06:38:13 +00:00
|
|
|
notional, orderNum := calculateNotionalAndNum(market, short, budget, prices)
|
2023-12-06 03:22:56 +00:00
|
|
|
if orderNum == 0 {
|
2023-12-07 03:29:42 +00:00
|
|
|
return nil, fmt.Errorf("failed to calculate notional and num of open position orders, price: %s, budget: %s", price, budget)
|
2023-11-24 06:46:26 +00:00
|
|
|
}
|
|
|
|
|
2023-12-07 06:38:13 +00:00
|
|
|
side := types.SideTypeBuy
|
|
|
|
if short {
|
|
|
|
side = types.SideTypeSell
|
|
|
|
}
|
|
|
|
|
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++ {
|
2023-12-07 06:38:13 +00:00
|
|
|
quantity := market.TruncateQuantity(notional.Div(prices[i]))
|
2023-11-24 06:46:26 +00:00
|
|
|
submitOrders = append(submitOrders, types.SubmitOrder{
|
2023-12-07 06:38:13 +00:00
|
|
|
Symbol: market.Symbol,
|
|
|
|
Market: market,
|
2023-11-24 06:46:26 +00:00
|
|
|
Type: types.OrderTypeLimit,
|
|
|
|
Price: prices[i],
|
2023-12-07 06:38:13 +00:00
|
|
|
Side: side,
|
2023-11-24 06:46:26 +00:00
|
|
|
TimeInForce: types.TimeInForceGTC,
|
|
|
|
Quantity: quantity,
|
|
|
|
Tag: orderTag,
|
2023-12-07 06:38:13 +00:00
|
|
|
GroupID: orderGroupID,
|
2023-11-24 06:46:26 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return submitOrders, nil
|
|
|
|
}
|
2023-12-06 03:22:56 +00:00
|
|
|
|
2023-12-07 03:27:28 +00:00
|
|
|
// calculateNotionalAndNum calculates the notional and num of open position orders
|
2023-12-06 03:22:56 +00:00
|
|
|
// DCA2 is notional-based, every order has the same notional
|
2023-12-07 03:27:28 +00:00
|
|
|
func calculateNotionalAndNum(market types.Market, short bool, budget fixedpoint.Value, prices []fixedpoint.Value) (fixedpoint.Value, int) {
|
2023-12-06 03:22:56 +00:00
|
|
|
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
|
|
|
|
}
|
2023-11-24 08:04:48 +00:00
|
|
|
|
|
|
|
func (s *Strategy) cancelOpenPositionOrders(ctx context.Context) error {
|
|
|
|
s.logger.Info("[DCA] cancel open position orders")
|
|
|
|
e, ok := s.Session.Exchange.(cancelOrdersByGroupIDApi)
|
|
|
|
if ok {
|
|
|
|
cancelledOrders, err := e.CancelOrdersByGroupID(ctx, int64(s.OrderGroupID))
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, cancelledOrder := range cancelledOrders {
|
|
|
|
s.logger.Info("CANCEL ", cancelledOrder.String())
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if err := s.OrderExecutor.ActiveMakerOrders().GracefulCancel(ctx, s.Session.Exchange); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|