qbtrade/pkg/strategy/dca2/open_position.go
2024-06-27 22:42:38 +08:00

132 lines
3.9 KiB
Go

package dca2
import (
"context"
"fmt"
"git.qtrade.icu/lychiyu/qbtrade/pkg/exchange/retry"
"git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint"
"git.qtrade.icu/lychiyu/qbtrade/pkg/qbtrade"
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
)
type cancelOrdersByGroupIDApi interface {
CancelOrdersByGroupID(ctx context.Context, groupID int64) ([]types.Order, error)
}
func (s *Strategy) placeOpenPositionOrders(ctx context.Context) error {
s.logger.Infof("start placing open position orders")
price, err := getBestPriceUntilSuccess(ctx, s.ExchangeSession.Exchange, s.Symbol)
if err != nil {
return err
}
orders, err := generateOpenPositionOrders(s.Market, s.EnableQuoteInvestmentReallocate, s.QuoteInvestment, s.ProfitStats.TotalProfit, price, s.PriceDeviation, s.MaxOrderCount, s.OrderGroupID)
if err != nil {
return err
}
createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, orders...)
if err != nil {
return err
}
s.debugOrders(createdOrders)
// store price quantity pairs into persistence
var pvs []types.PriceVolume
for _, createdOrder := range createdOrders {
pvs = append(pvs, types.PriceVolume{Price: createdOrder.Price, Volume: createdOrder.Quantity})
}
s.ProfitStats.OpenPositionPVs = pvs
qbtrade.Sync(ctx, s)
return nil
}
func getBestPriceUntilSuccess(ctx context.Context, ex types.Exchange, symbol string) (fixedpoint.Value, error) {
ticker, err := retry.QueryTickerUntilSuccessful(ctx, ex, symbol)
if err != nil {
return fixedpoint.Zero, err
}
return ticker.Sell, nil
}
func generateOpenPositionOrders(market types.Market, enableQuoteInvestmentReallocate bool, quoteInvestment, profit, price, priceDeviation fixedpoint.Value, maxOrderCount int64, orderGroupID uint32) ([]types.SubmitOrder, error) {
factor := fixedpoint.One.Sub(priceDeviation)
profit = market.TruncatePrice(profit)
// calculate all valid prices
var prices []fixedpoint.Value
for i := 0; i < int(maxOrderCount); i++ {
if i > 0 {
price = price.Mul(factor)
}
price = market.TruncatePrice(price)
if price.Compare(market.MinPrice) < 0 {
break
}
prices = append(prices, price)
}
notional, orderNum := calculateNotionalAndNumOrders(market, quoteInvestment, prices)
if orderNum == 0 {
return nil, fmt.Errorf("failed to calculate notional and num of open position orders, price: %s, quote investment: %s", price, quoteInvestment)
}
if !enableQuoteInvestmentReallocate && orderNum != int(maxOrderCount) {
return nil, fmt.Errorf("failed to generate open-position orders due to the orders may be under min notional or quantity")
}
side := types.SideTypeBuy
var submitOrders []types.SubmitOrder
for i := 0; i < orderNum; i++ {
var quantity fixedpoint.Value
// all the profit will use in the first order
if i == 0 {
quantity = market.TruncateQuantity(notional.Add(profit).Div(prices[i]))
} else {
quantity = market.TruncateQuantity(notional.Div(prices[i]))
}
submitOrders = append(submitOrders, types.SubmitOrder{
Symbol: market.Symbol,
Market: market,
Type: types.OrderTypeLimit,
Price: prices[i],
Side: side,
TimeInForce: types.TimeInForceGTC,
Quantity: quantity,
Tag: orderTag,
GroupID: orderGroupID,
})
}
return submitOrders, nil
}
// calculateNotionalAndNumOrders calculates the notional and num of open position orders
// DCA2 is notional-based, every order has the same notional
func calculateNotionalAndNumOrders(market types.Market, quoteInvestment fixedpoint.Value, prices []fixedpoint.Value) (fixedpoint.Value, int) {
for num := len(prices); num > 0; num-- {
notional := quoteInvestment.Div(fixedpoint.NewFromInt(int64(num)))
if notional.Compare(market.MinNotional) < 0 {
continue
}
maxPriceIdx := 0
quantity := market.TruncateQuantity(notional.Div(prices[maxPriceIdx]))
if quantity.Compare(market.MinQuantity) < 0 {
continue
}
return market.TruncatePrice(notional), num
}
return fixedpoint.Zero, 0
}