Merge pull request #1436 from c9s/kbearXD/dca2/open-takeprofit-orders

FEATURE: cancel maker orders and open take profit order
This commit is contained in:
kbearXD 2023-12-13 14:21:45 +08:00 committed by GitHub
commit 7e61fe40eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 142 additions and 17 deletions

View File

@ -9,6 +9,10 @@ import (
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
type cancelOrdersByGroupIDApi interface {
CancelOrdersByGroupID(ctx context.Context, groupID int64) ([]types.Order, error)
}
func (s *Strategy) placeOpenPositionOrders(ctx context.Context) error { func (s *Strategy) placeOpenPositionOrders(ctx context.Context) error {
s.logger.Infof("[DCA] start placing open position orders") s.logger.Infof("[DCA] start placing open position orders")
price, err := getBestPriceUntilSuccess(ctx, s.Session.Exchange, s.Symbol, s.Short) price, err := getBestPriceUntilSuccess(ctx, s.Session.Exchange, s.Symbol, s.Short)
@ -16,7 +20,7 @@ func (s *Strategy) placeOpenPositionOrders(ctx context.Context) error {
return err return err
} }
orders, err := s.generateOpenPositionOrders(s.Short, s.Budget, price, s.PriceDeviation, s.MaxOrderNum) orders, err := generateOpenPositionOrders(s.Market, s.Short, s.Budget, price, s.PriceDeviation, s.MaxOrderNum, s.OrderGroupID)
if err != nil { if err != nil {
return err return err
} }
@ -44,7 +48,7 @@ func getBestPriceUntilSuccess(ctx context.Context, ex types.Exchange, symbol str
} }
} }
func (s *Strategy) generateOpenPositionOrders(short bool, budget, price, priceDeviation fixedpoint.Value, maxOrderNum int64) ([]types.SubmitOrder, error) { func generateOpenPositionOrders(market types.Market, short bool, budget, price, priceDeviation fixedpoint.Value, maxOrderNum int64, orderGroupID uint32) ([]types.SubmitOrder, error) {
factor := fixedpoint.One.Sub(priceDeviation) factor := fixedpoint.One.Sub(priceDeviation)
if short { if short {
factor = fixedpoint.One.Add(priceDeviation) factor = fixedpoint.One.Add(priceDeviation)
@ -56,32 +60,37 @@ func (s *Strategy) generateOpenPositionOrders(short bool, budget, price, priceDe
if i > 0 { if i > 0 {
price = price.Mul(factor) price = price.Mul(factor)
} }
price = s.Market.TruncatePrice(price) price = market.TruncatePrice(price)
if price.Compare(s.Market.MinPrice) < 0 { if price.Compare(market.MinPrice) < 0 {
break break
} }
prices = append(prices, price) prices = append(prices, price)
} }
notional, orderNum := calculateNotionalAndNum(s.Market, short, budget, prices) notional, orderNum := calculateNotionalAndNum(market, short, budget, prices)
if orderNum == 0 { if orderNum == 0 {
return nil, fmt.Errorf("failed to calculate notional and num of open position orders, price: %s, budget: %s", price, budget) return nil, fmt.Errorf("failed to calculate notional and num of open position orders, price: %s, budget: %s", price, budget)
} }
side := types.SideTypeBuy
if short {
side = types.SideTypeSell
}
var submitOrders []types.SubmitOrder var submitOrders []types.SubmitOrder
for i := 0; i < orderNum; i++ { for i := 0; i < orderNum; i++ {
quantity := s.Market.TruncateQuantity(notional.Div(prices[i])) quantity := market.TruncateQuantity(notional.Div(prices[i]))
submitOrders = append(submitOrders, types.SubmitOrder{ submitOrders = append(submitOrders, types.SubmitOrder{
Symbol: s.Symbol, Symbol: market.Symbol,
Market: s.Market, Market: market,
Type: types.OrderTypeLimit, Type: types.OrderTypeLimit,
Price: prices[i], Price: prices[i],
Side: s.makerSide, Side: side,
TimeInForce: types.TimeInForceGTC, TimeInForce: types.TimeInForceGTC,
Quantity: quantity, Quantity: quantity,
Tag: orderTag, Tag: orderTag,
GroupID: s.OrderGroupID, GroupID: orderGroupID,
}) })
} }
@ -111,3 +120,24 @@ func calculateNotionalAndNum(market types.Market, short bool, budget fixedpoint.
return fixedpoint.Zero, 0 return fixedpoint.Zero, 0
} }
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
}

View File

@ -12,6 +12,7 @@ import (
func newTestMarket() types.Market { func newTestMarket() types.Market {
return types.Market{ return types.Market{
Symbol: "BTCUSDT",
BaseCurrency: "BTC", BaseCurrency: "BTC",
QuoteCurrency: "USDT", QuoteCurrency: "USDT",
TickSize: Number(0.01), TickSize: Number(0.01),
@ -32,9 +33,13 @@ func newTestStrategy(va ...string) *Strategy {
market := newTestMarket() market := newTestMarket()
s := &Strategy{ s := &Strategy{
logger: logrus.NewEntry(logrus.New()), logger: logrus.NewEntry(logrus.New()),
Symbol: symbol, Symbol: symbol,
Market: market, Market: market,
Short: false,
TakeProfitRatio: Number("10%"),
openPositionSide: types.SideTypeBuy,
takeProfitSide: types.SideTypeSell,
} }
return s return s
} }
@ -48,7 +53,7 @@ func TestGenerateOpenPositionOrders(t *testing.T) {
budget := Number("10500") budget := Number("10500")
askPrice := Number("30000") askPrice := Number("30000")
margin := Number("0.05") margin := Number("0.05")
submitOrders, err := strategy.generateOpenPositionOrders(false, budget, askPrice, margin, 4) submitOrders, err := generateOpenPositionOrders(strategy.Market, false, budget, askPrice, margin, 4, strategy.OrderGroupID)
if !assert.NoError(err) { if !assert.NoError(err) {
return return
} }

View File

@ -50,7 +50,7 @@ type Strategy struct {
// private field // private field
mu sync.Mutex mu sync.Mutex
makerSide types.SideType openPositionSide types.SideType
takeProfitSide types.SideType takeProfitSide types.SideType
takeProfitPrice fixedpoint.Value takeProfitPrice fixedpoint.Value
startTimeOfNextRound time.Time startTimeOfNextRound time.Time
@ -106,10 +106,10 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.
instanceID := s.InstanceID() instanceID := s.InstanceID()
if s.Short { if s.Short {
s.makerSide = types.SideTypeSell s.openPositionSide = types.SideTypeSell
s.takeProfitSide = types.SideTypeBuy s.takeProfitSide = types.SideTypeBuy
} else { } else {
s.makerSide = types.SideTypeBuy s.openPositionSide = types.SideTypeBuy
s.takeProfitSide = types.SideTypeSell s.takeProfitSide = types.SideTypeSell
} }

View File

@ -0,0 +1,43 @@
package dca2
import (
"context"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
func (s *Strategy) placeTakeProfitOrders(ctx context.Context) error {
s.logger.Info("[DCA] start placing take profit orders")
order := generateTakeProfitOrder(s.Market, s.Short, s.TakeProfitRatio, s.Position, s.OrderGroupID)
createdOrders, err := s.OrderExecutor.SubmitOrders(ctx, order)
if err != nil {
return err
}
for _, createdOrder := range createdOrders {
s.logger.Info("SUBMIT TAKE PROFIT ORDER ", createdOrder.String())
}
return nil
}
func generateTakeProfitOrder(market types.Market, short bool, takeProfitRatio fixedpoint.Value, position *types.Position, orderGroupID uint32) types.SubmitOrder {
side := types.SideTypeSell
if short {
takeProfitRatio = takeProfitRatio.Neg()
side = types.SideTypeBuy
}
takeProfitPrice := market.TruncatePrice(position.AverageCost.Mul(fixedpoint.One.Add(takeProfitRatio)))
return types.SubmitOrder{
Symbol: market.Symbol,
Market: market,
Type: types.OrderTypeLimit,
Price: takeProfitPrice,
Side: side,
TimeInForce: types.TimeInForceGTC,
Quantity: position.GetBase().Abs(),
Tag: orderTag,
GroupID: orderGroupID,
}
}

View File

@ -0,0 +1,47 @@
package dca2
import (
"testing"
. "github.com/c9s/bbgo/pkg/testing/testhelper"
"github.com/c9s/bbgo/pkg/types"
"github.com/stretchr/testify/assert"
)
func TestGenerateTakeProfitOrder(t *testing.T) {
assert := assert.New(t)
strategy := newTestStrategy()
position := types.NewPositionFromMarket(strategy.Market)
position.AddTrade(types.Trade{
Symbol: "BTCUSDT",
Side: types.SideTypeBuy,
Price: Number("28500"),
Quantity: Number("1"),
QuoteQuantity: Number("28500"),
Fee: Number("0.0015"),
FeeCurrency: strategy.Market.BaseCurrency,
})
o := generateTakeProfitOrder(strategy.Market, false, strategy.TakeProfitRatio, position, strategy.OrderGroupID)
assert.Equal(Number("31397.09"), o.Price)
assert.Equal(Number("0.9985"), o.Quantity)
assert.Equal(types.SideTypeSell, o.Side)
assert.Equal(strategy.Symbol, o.Symbol)
position.AddTrade(types.Trade{
Side: types.SideTypeBuy,
Price: Number("27000"),
Quantity: Number("0.5"),
QuoteQuantity: Number("13500"),
Fee: Number("0.00075"),
FeeCurrency: strategy.Market.BaseCurrency,
})
o = generateTakeProfitOrder(strategy.Market, false, strategy.TakeProfitRatio, position, strategy.OrderGroupID)
assert.Equal(Number("30846.26"), o.Price)
assert.Equal(Number("1.49775"), o.Quantity)
assert.Equal(types.SideTypeSell, o.Side)
assert.Equal(strategy.Symbol, o.Symbol)
}