2023-11-27 07:55:02 +00:00
|
|
|
package dca2
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"time"
|
2024-01-03 05:54:38 +00:00
|
|
|
|
|
|
|
"github.com/c9s/bbgo/pkg/bbgo"
|
2023-11-27 07:55:02 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type State int64
|
|
|
|
|
|
|
|
const (
|
|
|
|
None State = iota
|
|
|
|
WaitToOpenPosition
|
|
|
|
PositionOpening
|
|
|
|
OpenPositionReady
|
|
|
|
OpenPositionOrderFilled
|
|
|
|
OpenPositionOrdersCancelling
|
|
|
|
OpenPositionOrdersCancelled
|
|
|
|
TakeProfitReady
|
|
|
|
)
|
|
|
|
|
2023-12-27 03:41:29 +00:00
|
|
|
var stateTransition map[State]State = map[State]State{
|
|
|
|
WaitToOpenPosition: PositionOpening,
|
|
|
|
PositionOpening: OpenPositionReady,
|
|
|
|
OpenPositionReady: OpenPositionOrderFilled,
|
|
|
|
OpenPositionOrderFilled: OpenPositionOrdersCancelling,
|
|
|
|
OpenPositionOrdersCancelling: OpenPositionOrdersCancelled,
|
|
|
|
OpenPositionOrdersCancelled: TakeProfitReady,
|
|
|
|
TakeProfitReady: WaitToOpenPosition,
|
|
|
|
}
|
|
|
|
|
2023-11-27 07:55:02 +00:00
|
|
|
func (s *Strategy) initializeNextStateC() bool {
|
|
|
|
s.mu.Lock()
|
|
|
|
defer s.mu.Unlock()
|
|
|
|
|
|
|
|
isInitialize := false
|
|
|
|
if s.nextStateC == nil {
|
|
|
|
s.logger.Info("[DCA] initializing next state channel")
|
|
|
|
s.nextStateC = make(chan State, 1)
|
|
|
|
} else {
|
|
|
|
s.logger.Info("[DCA] nextStateC is already initialized")
|
|
|
|
isInitialize = true
|
|
|
|
}
|
|
|
|
|
|
|
|
return isInitialize
|
|
|
|
}
|
|
|
|
|
2024-02-22 08:15:54 +00:00
|
|
|
func (s *Strategy) updateState(state State) {
|
|
|
|
s.state = state
|
|
|
|
|
|
|
|
s.logger.Infof("[state] update state to %d", state)
|
|
|
|
metricsState.With(baseLabels).Set(float64(s.state))
|
|
|
|
}
|
|
|
|
|
2023-12-22 07:27:31 +00:00
|
|
|
func (s *Strategy) emitNextState(nextState State) {
|
|
|
|
select {
|
|
|
|
case s.nextStateC <- nextState:
|
|
|
|
default:
|
|
|
|
s.logger.Info("[DCA] nextStateC is full or not initialized")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-27 07:55:02 +00:00
|
|
|
// runState
|
|
|
|
// WaitToOpenPosition -> after startTimeOfNextRound, place dca orders ->
|
|
|
|
// PositionOpening
|
|
|
|
// OpenPositionReady -> any dca maker order filled ->
|
|
|
|
// OpenPositionOrderFilled -> price hit the take profit ration, start cancelling ->
|
|
|
|
// OpenPositionOrdersCancelled -> place the takeProfit order ->
|
|
|
|
// TakeProfitReady -> the takeProfit order filled ->
|
|
|
|
func (s *Strategy) runState(ctx context.Context) {
|
|
|
|
s.logger.Info("[DCA] runState")
|
2024-02-22 08:15:54 +00:00
|
|
|
stateTriggerTicker := time.NewTicker(5 * time.Second)
|
|
|
|
defer stateTriggerTicker.Stop()
|
|
|
|
|
|
|
|
monitorTicker := time.NewTicker(10 * time.Minute)
|
|
|
|
defer monitorTicker.Stop()
|
2023-12-22 07:27:31 +00:00
|
|
|
|
2023-11-27 07:55:02 +00:00
|
|
|
for {
|
|
|
|
select {
|
|
|
|
case <-ctx.Done():
|
|
|
|
s.logger.Info("[DCA] runState DONE")
|
|
|
|
return
|
2024-02-22 08:15:54 +00:00
|
|
|
case <-stateTriggerTicker.C:
|
|
|
|
// s.logger.Infof("[DCA] triggerNextState current state: %d", s.state)
|
2023-12-22 07:27:31 +00:00
|
|
|
s.triggerNextState()
|
2024-02-22 08:15:54 +00:00
|
|
|
case <-monitorTicker.C:
|
|
|
|
s.updateNumOfOrdersMetrics(ctx)
|
2023-11-27 07:55:02 +00:00
|
|
|
case nextState := <-s.nextStateC:
|
|
|
|
s.logger.Infof("[DCA] currenct state: %d, next state: %d", s.state, nextState)
|
2023-12-27 03:41:29 +00:00
|
|
|
|
|
|
|
// check the next state is valid
|
|
|
|
validNextState, exist := stateTransition[s.state]
|
|
|
|
if !exist {
|
|
|
|
s.logger.Warnf("[DCA] %d not in stateTransition", s.state)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if nextState != validNextState {
|
|
|
|
s.logger.Warnf("[DCA] %d is not valid next state of curreny state %d", nextState, s.state)
|
2024-01-18 07:39:56 +00:00
|
|
|
continue
|
2023-12-27 03:41:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// move to next state
|
2023-11-27 07:55:02 +00:00
|
|
|
switch s.state {
|
|
|
|
case WaitToOpenPosition:
|
|
|
|
s.runWaitToOpenPositionState(ctx, nextState)
|
|
|
|
case PositionOpening:
|
|
|
|
s.runPositionOpening(ctx, nextState)
|
|
|
|
case OpenPositionReady:
|
|
|
|
s.runOpenPositionReady(ctx, nextState)
|
|
|
|
case OpenPositionOrderFilled:
|
|
|
|
s.runOpenPositionOrderFilled(ctx, nextState)
|
|
|
|
case OpenPositionOrdersCancelling:
|
|
|
|
s.runOpenPositionOrdersCancelling(ctx, nextState)
|
|
|
|
case OpenPositionOrdersCancelled:
|
|
|
|
s.runOpenPositionOrdersCancelled(ctx, nextState)
|
|
|
|
case TakeProfitReady:
|
|
|
|
s.runTakeProfitReady(ctx, nextState)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-22 07:27:31 +00:00
|
|
|
func (s *Strategy) triggerNextState() {
|
|
|
|
switch s.state {
|
|
|
|
case OpenPositionReady:
|
2023-12-27 03:41:29 +00:00
|
|
|
// only trigger from order filled event
|
2023-12-22 07:27:31 +00:00
|
|
|
case OpenPositionOrderFilled:
|
2023-12-27 03:41:29 +00:00
|
|
|
// only trigger from kline event
|
2023-12-22 07:27:31 +00:00
|
|
|
case TakeProfitReady:
|
2023-12-27 03:41:29 +00:00
|
|
|
// only trigger from order filled event
|
|
|
|
default:
|
|
|
|
if nextState, ok := stateTransition[s.state]; ok {
|
2024-01-12 16:24:47 +00:00
|
|
|
s.emitNextState(nextState)
|
2023-12-27 03:41:29 +00:00
|
|
|
}
|
2023-12-22 07:27:31 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-08 10:24:11 +00:00
|
|
|
func (s *Strategy) runWaitToOpenPositionState(ctx context.Context, next State) {
|
2023-12-22 07:27:31 +00:00
|
|
|
s.logger.Info("[State] WaitToOpenPosition - check startTimeOfNextRound")
|
2023-11-27 07:55:02 +00:00
|
|
|
if time.Now().Before(s.startTimeOfNextRound) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2024-02-22 08:15:54 +00:00
|
|
|
s.updateState(PositionOpening)
|
2023-12-22 07:27:31 +00:00
|
|
|
s.logger.Info("[State] WaitToOpenPosition -> PositionOpening")
|
2023-11-27 07:55:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Strategy) runPositionOpening(ctx context.Context, next State) {
|
2023-12-22 07:27:31 +00:00
|
|
|
s.logger.Info("[State] PositionOpening - start placing open-position orders")
|
2023-11-27 07:55:02 +00:00
|
|
|
if err := s.placeOpenPositionOrders(ctx); err != nil {
|
|
|
|
s.logger.WithError(err).Error("failed to place dca orders, please check it.")
|
|
|
|
return
|
|
|
|
}
|
2024-02-22 08:15:54 +00:00
|
|
|
s.updateState(OpenPositionReady)
|
2023-12-22 07:27:31 +00:00
|
|
|
s.logger.Info("[State] PositionOpening -> OpenPositionReady")
|
2023-11-27 07:55:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Strategy) runOpenPositionReady(_ context.Context, next State) {
|
2024-02-22 08:15:54 +00:00
|
|
|
s.updateState(OpenPositionOrderFilled)
|
2023-12-22 07:27:31 +00:00
|
|
|
s.logger.Info("[State] OpenPositionReady -> OpenPositionOrderFilled")
|
2023-11-27 07:55:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Strategy) runOpenPositionOrderFilled(_ context.Context, next State) {
|
2024-02-22 08:15:54 +00:00
|
|
|
s.updateState(OpenPositionOrdersCancelling)
|
2023-12-22 07:27:31 +00:00
|
|
|
s.logger.Info("[State] OpenPositionOrderFilled -> OpenPositionOrdersCancelling")
|
|
|
|
|
|
|
|
// after open position cancelling, immediately trigger open position cancelled to cancel the other orders
|
2024-01-12 16:24:47 +00:00
|
|
|
s.emitNextState(OpenPositionOrdersCancelled)
|
2023-11-27 07:55:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Strategy) runOpenPositionOrdersCancelling(ctx context.Context, next State) {
|
2023-12-22 07:27:31 +00:00
|
|
|
s.logger.Info("[State] OpenPositionOrdersCancelling - start cancelling open-position orders")
|
2024-01-03 09:02:03 +00:00
|
|
|
if err := s.OrderExecutor.GracefulCancel(ctx); err != nil {
|
2023-11-27 07:55:02 +00:00
|
|
|
s.logger.WithError(err).Error("failed to cancel maker orders")
|
|
|
|
return
|
|
|
|
}
|
2024-02-22 08:15:54 +00:00
|
|
|
s.updateState(OpenPositionOrdersCancelled)
|
2023-12-22 07:27:31 +00:00
|
|
|
s.logger.Info("[State] OpenPositionOrdersCancelling -> OpenPositionOrdersCancelled")
|
|
|
|
|
|
|
|
// after open position cancelled, immediately trigger take profit ready to open take-profit order
|
2024-01-12 16:24:47 +00:00
|
|
|
s.emitNextState(TakeProfitReady)
|
2023-11-27 07:55:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *Strategy) runOpenPositionOrdersCancelled(ctx context.Context, next State) {
|
2023-12-22 07:27:31 +00:00
|
|
|
s.logger.Info("[State] OpenPositionOrdersCancelled - start placing take-profit orders")
|
2023-11-27 07:55:02 +00:00
|
|
|
if err := s.placeTakeProfitOrders(ctx); err != nil {
|
|
|
|
s.logger.WithError(err).Error("failed to open take profit orders")
|
|
|
|
return
|
|
|
|
}
|
2024-02-22 08:15:54 +00:00
|
|
|
s.updateState(TakeProfitReady)
|
2023-12-22 07:27:31 +00:00
|
|
|
s.logger.Info("[State] OpenPositionOrdersCancelled -> TakeProfitReady")
|
2023-11-27 07:55:02 +00:00
|
|
|
}
|
|
|
|
|
2024-01-03 05:54:38 +00:00
|
|
|
func (s *Strategy) runTakeProfitReady(ctx context.Context, next State) {
|
2023-12-22 07:50:48 +00:00
|
|
|
// wait 3 seconds to avoid position not update
|
|
|
|
time.Sleep(3 * time.Second)
|
|
|
|
|
2024-01-02 09:16:47 +00:00
|
|
|
s.logger.Info("[State] TakeProfitReady - start reseting position and calculate quote investment for next round")
|
2023-11-27 07:55:02 +00:00
|
|
|
|
2024-01-12 16:24:47 +00:00
|
|
|
// reset position
|
|
|
|
|
2024-01-08 10:24:11 +00:00
|
|
|
// calculate profit stats
|
2024-01-12 16:24:47 +00:00
|
|
|
s.CalculateAndEmitProfit(ctx)
|
|
|
|
|
|
|
|
// reset position and open new round for profit stats before position opening
|
|
|
|
s.Position.Reset()
|
2023-11-27 07:55:02 +00:00
|
|
|
|
2024-01-12 16:24:47 +00:00
|
|
|
// store into redis
|
|
|
|
bbgo.Sync(ctx, s)
|
2024-01-03 05:54:38 +00:00
|
|
|
|
2023-11-27 07:55:02 +00:00
|
|
|
// set the start time of the next round
|
|
|
|
s.startTimeOfNextRound = time.Now().Add(s.CoolDownInterval.Duration())
|
2024-02-22 08:15:54 +00:00
|
|
|
s.updateState(WaitToOpenPosition)
|
2023-12-22 07:27:31 +00:00
|
|
|
s.logger.Info("[State] TakeProfitReady -> WaitToOpenPosition")
|
2023-11-27 07:55:02 +00:00
|
|
|
}
|