Merge pull request #1515 from c9s/kbearXD/dca2/dev-mode

[dca2] add dev mode field for dev
This commit is contained in:
c9s 2024-02-05 19:04:58 +08:00 committed by GitHub
commit 7cc3d1b193
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 159 additions and 147 deletions

View File

@ -0,0 +1,7 @@
package dca2
// DevMode, if Enabled is true. it means it will check the running field
type DevMode struct {
Enabled bool `json:"enabled"`
IsNewAccount bool `json:"isNewAccount"`
}

View File

@ -4,6 +4,7 @@ import (
"context"
"fmt"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/exchange/retry"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
@ -32,6 +33,21 @@ func (s *Strategy) placeOpenPositionOrders(ctx context.Context) error {
s.debugOrders(createdOrders)
if s.DevMode != nil && s.DevMode.Enabled && s.DevMode.IsNewAccount {
if len(createdOrders) > 0 {
s.ProfitStats.FromOrderID = createdOrders[0].OrderID
}
for _, createdOrder := range createdOrders {
if s.ProfitStats.FromOrderID > createdOrder.OrderID {
s.ProfitStats.FromOrderID = createdOrder.OrderID
}
}
s.DevMode.IsNewAccount = false
bbgo.Sync(ctx, s)
}
return nil
}

View File

@ -28,7 +28,7 @@ func newProfitStats(market types.Market, quoteInvestment fixedpoint.Value) *Prof
return &ProfitStats{
Symbol: market.Symbol,
Market: market,
Round: 0,
Round: 1,
QuoteInvestment: quoteInvestment,
CurrentRoundFee: make(map[string]fixedpoint.Value),
TotalFee: make(map[string]fixedpoint.Value),

View File

@ -7,11 +7,12 @@ import (
"time"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/core"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
var recoverSinceLimit = time.Date(2024, time.January, 29, 12, 0, 0, 0, time.Local)
type descendingClosedOrderQueryService interface {
QueryClosedOrdersDesc(ctx context.Context, symbol string, since, until time.Time, lastOrderID uint64) ([]types.Order, error)
}
@ -34,7 +35,7 @@ func (s *Strategy) recover(ctx context.Context) error {
return err
}
closedOrders, err := queryService.QueryClosedOrdersDesc(ctx, s.Symbol, time.Date(2024, time.January, 12, 14, 0, 0, 0, time.Local), time.Now(), 0)
closedOrders, err := queryService.QueryClosedOrdersDesc(ctx, s.Symbol, recoverSinceLimit, time.Now(), 0)
if err != nil {
return err
}
@ -45,12 +46,11 @@ func (s *Strategy) recover(ctx context.Context) error {
}
debugRoundOrders(s.logger, "current", currentRound)
// recover state
state, err := recoverState(ctx, s.Symbol, int(s.MaxOrderCount), openOrders, currentRound, s.OrderExecutor.ActiveMakerOrders(), s.OrderExecutor.OrderStore(), s.OrderGroupID)
if err != nil {
// recover profit stats
if err := recoverProfitStats(ctx, s); err != nil {
return err
}
s.logger.Info("recover stats DONE")
s.logger.Info("recover profit stats DONE")
// recover position
if err := recoverPosition(ctx, s.Position, queryService, currentRound); err != nil {
@ -58,73 +58,65 @@ func (s *Strategy) recover(ctx context.Context) error {
}
s.logger.Info("recover position DONE")
// recover profit stats
recoverProfitStats(ctx, s)
s.logger.Info("recover profit stats DONE")
// recover startTimeOfNextRound
startTimeOfNextRound := recoverStartTimeOfNextRound(ctx, currentRound, s.CoolDownInterval)
s.state = state
s.startTimeOfNextRound = startTimeOfNextRound
// recover state
state, err := recoverState(ctx, s.ProfitStats.QuoteInvestment, int(s.MaxOrderCount), currentRound, s.OrderExecutor)
if err != nil {
return err
}
s.state = state
s.logger.Info("recover stats DONE")
return nil
}
// recover state
func recoverState(ctx context.Context, symbol string, maxOrderCount int, openOrders []types.Order, currentRound Round, activeOrderBook *bbgo.ActiveOrderBook, orderStore *core.OrderStore, groupID uint32) (State, error) {
if len(currentRound.OpenPositionOrders) == 0 {
// new strategy
func recoverState(ctx context.Context, quoteInvestment fixedpoint.Value, maxOrderCount int, currentRound Round, orderExecutor *bbgo.GeneralOrderExecutor) (State, error) {
activeOrderBook := orderExecutor.ActiveMakerOrders()
orderStore := orderExecutor.OrderStore()
// dca stop at take-profit order stage
if currentRound.TakeProfitOrder.OrderID != 0 {
if len(currentRound.OpenPositionOrders) != maxOrderCount {
return None, fmt.Errorf("there is take-profit order but the number of open-position orders (%d) is not the same as maxOrderCount(%d). Please check it", len(currentRound.OpenPositionOrders), maxOrderCount)
}
takeProfitOrder := currentRound.TakeProfitOrder
if takeProfitOrder.Status == types.OrderStatusFilled {
return WaitToOpenPosition, nil
} else if types.IsActiveOrder(takeProfitOrder) {
activeOrderBook.Add(takeProfitOrder)
orderStore.Add(takeProfitOrder)
return TakeProfitReady, nil
} else {
return None, fmt.Errorf("the status of take-profit order is %s. Please check it", takeProfitOrder.Status)
}
}
// dca stop at no take-profit order stage
openPositionOrders := currentRound.OpenPositionOrders
numOpenPositionOrders := len(openPositionOrders)
// new strategy
if len(openPositionOrders) == 0 {
return WaitToOpenPosition, nil
}
numOpenOrders := len(openOrders)
// dca stop at take profit order stage
if currentRound.TakeProfitOrder.OrderID != 0 {
if numOpenOrders == 0 {
// current round's take-profit order filled, wait to open next round
return WaitToOpenPosition, nil
}
// check the open orders is take profit order or not
if numOpenOrders == 1 {
if openOrders[0].OrderID == currentRound.TakeProfitOrder.OrderID {
activeOrderBook.Add(openOrders[0])
// current round's take-profit order still opened, wait to fill
return TakeProfitReady, nil
} else {
return None, fmt.Errorf("stop at taking profit stage, but the open order's OrderID is not the take-profit order's OrderID")
}
}
return None, fmt.Errorf("stop at taking profit stage, but the number of open orders is > 1")
}
numOpenPositionOrders := len(currentRound.OpenPositionOrders)
// should not happen
if numOpenPositionOrders > maxOrderCount {
return None, fmt.Errorf("the number of open-position orders is > max order number")
} else if numOpenPositionOrders < maxOrderCount {
// The number of open-position orders should be the same as maxOrderCount
// If not, it may be the following possible cause
// 1. This strategy at position opening, so it may not place all orders we want successfully
// 2. There are some errors when placing open-position orders. e.g. cannot lock fund.....
return None, fmt.Errorf("the number of open-position orders is < max order number")
}
if numOpenOrders > numOpenPositionOrders {
return None, fmt.Errorf("the number of open orders is > the number of open-position orders")
}
if numOpenOrders == numOpenPositionOrders {
activeOrderBook.Add(openOrders...)
orderStore.Add(openOrders...)
return OpenPositionReady, nil
return None, fmt.Errorf("the number of open-position orders (%d) is > max order number", numOpenPositionOrders)
}
// collect open-position orders' status
var openedCnt, filledCnt, cancelledCnt int64
for _, order := range currentRound.OpenPositionOrders {
switch order.Status {
case types.OrderStatusNew, types.OrderStatusPartiallyFilled:
activeOrderBook.Add(order)
orderStore.Add(order)
openedCnt++
case types.OrderStatusFilled:
filledCnt++
@ -135,21 +127,40 @@ func recoverState(ctx context.Context, symbol string, maxOrderCount int, openOrd
}
}
// the number of open-position orders is the same as maxOrderCount -> place open-position orders successfully
if numOpenPositionOrders == maxOrderCount {
// all open-position orders are still not filled -> OpenPositionReady
if filledCnt == 0 && cancelledCnt == 0 {
return OpenPositionReady, nil
}
// there are at least one open-position orders filled -> OpenPositionOrderFilled
if filledCnt > 0 && cancelledCnt == 0 {
return OpenPositionOrderFilled, nil
}
// there are at last one open-position orders cancelled ->
if cancelledCnt > 0 {
return OpenPositionOrdersCancelling, nil
}
return None, fmt.Errorf("unexpected order status combination when numOpenPositionOrders(%d) == maxOrderCount(%d) (opened, filled, cancelled) = (%d, %d, %d)", numOpenPositionOrders, maxOrderCount, openedCnt, filledCnt, cancelledCnt)
}
// the number of open-position orders is less than maxOrderCount -> failed to place open-position orders
// 1. This strategy is at position opening, so it may not place all orders we want successfully
// 2. There are some errors when placing open-position orders. e.g. cannot lock fund.....
if filledCnt == 0 && cancelledCnt == 0 {
// TODO: place the remaining open-position orders
return OpenPositionReady, nil
}
if filledCnt > 0 && cancelledCnt == 0 {
activeOrderBook.Add(openOrders...)
orderStore.Add(openOrders...)
// TODO: place the remaing open-position orders and change state to OpenPositionOrderFilled
return OpenPositionOrderFilled, nil
}
if openedCnt > 0 && filledCnt > 0 && cancelledCnt > 0 {
return OpenPositionOrdersCancelling, nil
}
if openedCnt == 0 && filledCnt > 0 && cancelledCnt > 0 {
return OpenPositionOrdersCancelled, nil
}
return None, fmt.Errorf("unexpected order status combination")
return None, fmt.Errorf("unexpected order status combination when numOpenPositionOrders(%d) < maxOrderCount(%d) (opened, filled, cancelled) = (%d, %d, %d)", numOpenPositionOrders, maxOrderCount, openedCnt, filledCnt, cancelledCnt)
}
func recoverPosition(ctx context.Context, position *types.Position, queryService RecoverApiQueryService, currentRound Round) error {
@ -197,29 +208,7 @@ func recoverProfitStats(ctx context.Context, strategy *Strategy) error {
return fmt.Errorf("profit stats is nil, please check it")
}
strategy.CalculateAndEmitProfit(ctx)
return nil
}
func recoverQuoteInvestment(currentRound Round) fixedpoint.Value {
if len(currentRound.OpenPositionOrders) == 0 {
return fixedpoint.Zero
}
total := fixedpoint.Zero
for _, order := range currentRound.OpenPositionOrders {
total = total.Add(order.Quantity.Mul(order.Price))
}
if currentRound.TakeProfitOrder.OrderID != 0 && currentRound.TakeProfitOrder.Status == types.OrderStatusFilled {
total = total.Add(currentRound.TakeProfitOrder.Quantity.Mul(currentRound.TakeProfitOrder.Price))
for _, order := range currentRound.OpenPositionOrders {
total = total.Sub(order.ExecutedQuantity.Mul(order.Price))
}
}
return total
return strategy.CalculateAndEmitProfit(ctx)
}
func recoverStartTimeOfNextRound(ctx context.Context, currentRound Round, coolDownInterval types.Duration) time.Time {

View File

@ -7,7 +7,7 @@ import (
"time"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/core"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
"github.com/stretchr/testify/assert"
)
@ -24,7 +24,7 @@ func generateTestOrder(side types.SideType, status types.OrderStatus, createdAt
}
func Test_GetCurrenctAndLastRoundOrders(t *testing.T) {
func Test_GetCurrenctRoundOrders(t *testing.T) {
t.Run("case 1", func(t *testing.T) {
now := time.Now()
openOrders := []types.Order{
@ -94,85 +94,74 @@ func (m *MockQueryOrders) QueryClosedOrdersDesc(ctx context.Context, symbol stri
}
func Test_RecoverState(t *testing.T) {
symbol := "BTCUSDT"
strategy := newTestStrategy()
quoteInvestment := fixedpoint.MustNewFromString("1000")
t.Run("new strategy", func(t *testing.T) {
openOrders := []types.Order{}
currentRound := Round{}
activeOrderBook := bbgo.NewActiveOrderBook(symbol)
orderStore := core.NewOrderStore(symbol)
state, err := recoverState(context.Background(), symbol, 5, openOrders, currentRound, activeOrderBook, orderStore, 0)
position := types.NewPositionFromMarket(strategy.Market)
orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position)
state, err := recoverState(context.Background(), quoteInvestment, 5, currentRound, orderExecutor)
assert.NoError(t, err)
assert.Equal(t, WaitToOpenPosition, state)
})
t.Run("at open position stage and no filled order", func(t *testing.T) {
now := time.Now()
openOrders := []types.Order{
generateTestOrder(types.SideTypeBuy, types.OrderStatusPartiallyFilled, now.Add(-1*time.Second)),
generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-2*time.Second)),
generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-3*time.Second)),
generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-4*time.Second)),
generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-5*time.Second)),
}
currentRound := Round{
OpenPositionOrders: openOrders,
OpenPositionOrders: []types.Order{
generateTestOrder(types.SideTypeBuy, types.OrderStatusPartiallyFilled, now.Add(-1*time.Second)),
generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-2*time.Second)),
generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-3*time.Second)),
generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-4*time.Second)),
generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-5*time.Second)),
},
}
orderStore := core.NewOrderStore(symbol)
activeOrderBook := bbgo.NewActiveOrderBook(symbol)
state, err := recoverState(context.Background(), symbol, 5, openOrders, currentRound, activeOrderBook, orderStore, 0)
position := types.NewPositionFromMarket(strategy.Market)
orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position)
state, err := recoverState(context.Background(), quoteInvestment, 5, currentRound, orderExecutor)
assert.NoError(t, err)
assert.Equal(t, OpenPositionReady, state)
})
t.Run("at open position stage and there at least one filled order", func(t *testing.T) {
now := time.Now()
openOrders := []types.Order{
generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-2*time.Second)),
generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-3*time.Second)),
generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-4*time.Second)),
generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-5*time.Second)),
}
currentRound := Round{
OpenPositionOrders: []types.Order{
generateTestOrder(types.SideTypeBuy, types.OrderStatusFilled, now.Add(-1*time.Second)),
openOrders[0],
openOrders[1],
openOrders[2],
openOrders[3],
generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-2*time.Second)),
generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-3*time.Second)),
generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-4*time.Second)),
generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-5*time.Second)),
},
}
orderStore := core.NewOrderStore(symbol)
activeOrderBook := bbgo.NewActiveOrderBook(symbol)
state, err := recoverState(context.Background(), symbol, 5, openOrders, currentRound, activeOrderBook, orderStore, 0)
position := types.NewPositionFromMarket(strategy.Market)
orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position)
state, err := recoverState(context.Background(), quoteInvestment, 5, currentRound, orderExecutor)
assert.NoError(t, err)
assert.Equal(t, OpenPositionOrderFilled, state)
})
t.Run("open position stage finish, but stop at cancelling", func(t *testing.T) {
now := time.Now()
openOrders := []types.Order{
generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-5*time.Second)),
}
currentRound := Round{
OpenPositionOrders: []types.Order{
generateTestOrder(types.SideTypeBuy, types.OrderStatusFilled, now.Add(-1*time.Second)),
generateTestOrder(types.SideTypeBuy, types.OrderStatusCanceled, now.Add(-2*time.Second)),
generateTestOrder(types.SideTypeBuy, types.OrderStatusCanceled, now.Add(-3*time.Second)),
generateTestOrder(types.SideTypeBuy, types.OrderStatusCanceled, now.Add(-4*time.Second)),
openOrders[0],
generateTestOrder(types.SideTypeBuy, types.OrderStatusNew, now.Add(-5*time.Second)),
},
}
orderStore := core.NewOrderStore(symbol)
activeOrderBook := bbgo.NewActiveOrderBook(symbol)
state, err := recoverState(context.Background(), symbol, 5, openOrders, currentRound, activeOrderBook, orderStore, 0)
position := types.NewPositionFromMarket(strategy.Market)
orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position)
state, err := recoverState(context.Background(), quoteInvestment, 5, currentRound, orderExecutor)
assert.NoError(t, err)
assert.Equal(t, OpenPositionOrdersCancelling, state)
})
t.Run("open-position orders are cancelled", func(t *testing.T) {
now := time.Now()
openOrders := []types.Order{}
currentRound := Round{
OpenPositionOrders: []types.Order{
generateTestOrder(types.SideTypeBuy, types.OrderStatusFilled, now.Add(-1*time.Second)),
@ -182,20 +171,17 @@ func Test_RecoverState(t *testing.T) {
generateTestOrder(types.SideTypeBuy, types.OrderStatusCanceled, now.Add(-5*time.Second)),
},
}
orderStore := core.NewOrderStore(symbol)
activeOrderBook := bbgo.NewActiveOrderBook(symbol)
state, err := recoverState(context.Background(), symbol, 5, openOrders, currentRound, activeOrderBook, orderStore, 0)
position := types.NewPositionFromMarket(strategy.Market)
orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position)
state, err := recoverState(context.Background(), quoteInvestment, 5, currentRound, orderExecutor)
assert.NoError(t, err)
assert.Equal(t, OpenPositionOrdersCancelled, state)
assert.Equal(t, OpenPositionOrdersCancelling, state)
})
t.Run("at take profit stage, and not filled yet", func(t *testing.T) {
now := time.Now()
openOrders := []types.Order{
generateTestOrder(types.SideTypeSell, types.OrderStatusNew, now),
}
currentRound := Round{
TakeProfitOrder: openOrders[0],
TakeProfitOrder: generateTestOrder(types.SideTypeSell, types.OrderStatusNew, now),
OpenPositionOrders: []types.Order{
generateTestOrder(types.SideTypeBuy, types.OrderStatusFilled, now.Add(-1*time.Second)),
generateTestOrder(types.SideTypeBuy, types.OrderStatusCanceled, now.Add(-2*time.Second)),
@ -204,16 +190,15 @@ func Test_RecoverState(t *testing.T) {
generateTestOrder(types.SideTypeBuy, types.OrderStatusCanceled, now.Add(-5*time.Second)),
},
}
orderStore := core.NewOrderStore(symbol)
activeOrderBook := bbgo.NewActiveOrderBook(symbol)
state, err := recoverState(context.Background(), symbol, 5, openOrders, currentRound, activeOrderBook, orderStore, 0)
position := types.NewPositionFromMarket(strategy.Market)
orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position)
state, err := recoverState(context.Background(), quoteInvestment, 5, currentRound, orderExecutor)
assert.NoError(t, err)
assert.Equal(t, TakeProfitReady, state)
})
t.Run("at take profit stage, take-profit order filled", func(t *testing.T) {
now := time.Now()
openOrders := []types.Order{}
currentRound := Round{
TakeProfitOrder: generateTestOrder(types.SideTypeSell, types.OrderStatusFilled, now),
OpenPositionOrders: []types.Order{
@ -224,9 +209,9 @@ func Test_RecoverState(t *testing.T) {
generateTestOrder(types.SideTypeBuy, types.OrderStatusCanceled, now.Add(-5*time.Second)),
},
}
orderStore := core.NewOrderStore(symbol)
activeOrderBook := bbgo.NewActiveOrderBook(symbol)
state, err := recoverState(context.Background(), symbol, 5, openOrders, currentRound, activeOrderBook, orderStore, 0)
position := types.NewPositionFromMarket(strategy.Market)
orderExecutor := bbgo.NewGeneralOrderExecutor(nil, strategy.Symbol, ID, "", position)
state, err := recoverState(context.Background(), quoteInvestment, 5, currentRound, orderExecutor)
assert.NoError(t, err)
assert.Equal(t, WaitToOpenPosition, state)
})

View File

@ -66,6 +66,9 @@ type Strategy struct {
// UseCancelAllOrdersApiWhenClose uses a different API to cancel all the orders on the market when closing a grid
UseCancelAllOrdersApiWhenClose bool `json:"useCancelAllOrdersApiWhenClose"`
// dev mode
DevMode *DevMode `json:"devMode"`
// log
logger *logrus.Entry
LogFields logrus.Fields `json:"logFields"`
@ -141,6 +144,12 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.
s.Position = types.NewPositionFromMarket(s.Market)
}
// if dev mode is on and it's not a new strategy
if s.DevMode != nil && s.DevMode.Enabled && !s.DevMode.IsNewAccount {
s.ProfitStats = newProfitStats(s.Market, s.QuoteInvestment)
s.Position = types.NewPositionFromMarket(s.Market)
}
s.Position.Strategy = ID
s.Position.StrategyInstanceID = instanceID
@ -202,14 +211,18 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.
s.logger.Info("[DCA] user data stream authenticated")
time.AfterFunc(3*time.Second, func() {
if isInitialize := s.initializeNextStateC(); !isInitialize {
if s.RecoverWhenStart {
// no need to recover when two situation
// 1. recoverWhenStart is false
// 2. dev mode is on and it's not new strategy
if !s.RecoverWhenStart || (s.DevMode != nil && s.DevMode.Enabled && !s.DevMode.IsNewAccount) {
s.state = WaitToOpenPosition
} else {
// recover
if err := s.recover(ctx); err != nil {
s.logger.WithError(err).Error("[DCA] something wrong when state recovering")
return
}
} else {
s.state = WaitToOpenPosition
}
s.logger.Infof("[DCA] state: %d", s.state)
@ -382,6 +395,8 @@ func (s *Strategy) CalculateAndEmitProfit(ctx context.Context) error {
// store into persistence
bbgo.Sync(ctx, s)
s.logger.Infof("[DCA] profit stats:\n%s", s.ProfitStats.String())
// emit profit
s.EmitProfit(s.ProfitStats)