245 lines
7.5 KiB
Go
245 lines
7.5 KiB
Go
|
package dca2
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"time"
|
||
|
|
||
|
"git.qtrade.icu/lychiyu/bbgo/pkg/bbgo"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
openPositionRetryInterval = 10 * time.Minute
|
||
|
)
|
||
|
|
||
|
type State int64
|
||
|
|
||
|
const (
|
||
|
None State = iota
|
||
|
WaitToOpenPosition
|
||
|
PositionOpening
|
||
|
OpenPositionReady
|
||
|
OpenPositionOrderFilled
|
||
|
OpenPositionOrdersCancelling
|
||
|
OpenPositionOrdersCancelled
|
||
|
TakeProfitReady
|
||
|
)
|
||
|
|
||
|
var stateTransition map[State]State = map[State]State{
|
||
|
WaitToOpenPosition: PositionOpening,
|
||
|
PositionOpening: OpenPositionReady,
|
||
|
OpenPositionReady: OpenPositionOrderFilled,
|
||
|
OpenPositionOrderFilled: OpenPositionOrdersCancelling,
|
||
|
OpenPositionOrdersCancelling: OpenPositionOrdersCancelled,
|
||
|
OpenPositionOrdersCancelled: TakeProfitReady,
|
||
|
TakeProfitReady: WaitToOpenPosition,
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|
||
|
|
||
|
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))
|
||
|
}
|
||
|
|
||
|
func (s *Strategy) emitNextState(nextState State) {
|
||
|
select {
|
||
|
case s.nextStateC <- nextState:
|
||
|
default:
|
||
|
s.logger.Info("[DCA] nextStateC is full or not initialized")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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")
|
||
|
stateTriggerTicker := time.NewTicker(1 * time.Minute)
|
||
|
defer stateTriggerTicker.Stop()
|
||
|
|
||
|
for {
|
||
|
select {
|
||
|
case <-ctx.Done():
|
||
|
s.logger.Info("[DCA] runState DONE")
|
||
|
return
|
||
|
case <-stateTriggerTicker.C:
|
||
|
// move triggerNextState to the end of next state handler, this ticker is used to avoid the state is stopped unexpectedly
|
||
|
s.triggerNextState()
|
||
|
case nextState := <-s.nextStateC:
|
||
|
// next state == current state -> skip
|
||
|
if nextState == s.state {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// 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)
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// move to next state
|
||
|
if triggerImmediately := s.moveToNextState(ctx, nextState); triggerImmediately {
|
||
|
s.triggerNextState()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (s *Strategy) triggerNextState() {
|
||
|
switch s.state {
|
||
|
case OpenPositionReady:
|
||
|
// only trigger from order filled event
|
||
|
case OpenPositionOrderFilled:
|
||
|
// only trigger from kline event
|
||
|
case TakeProfitReady:
|
||
|
// only trigger from order filled event
|
||
|
default:
|
||
|
if nextState, ok := stateTransition[s.state]; ok {
|
||
|
s.emitNextState(nextState)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// moveToNextState will run the process when moving current state to next state
|
||
|
// it will return true if we want it trigger the next state immediately
|
||
|
func (s *Strategy) moveToNextState(ctx context.Context, nextState State) bool {
|
||
|
switch s.state {
|
||
|
case WaitToOpenPosition:
|
||
|
return s.runWaitToOpenPositionState(ctx, nextState)
|
||
|
case PositionOpening:
|
||
|
return s.runPositionOpening(ctx, nextState)
|
||
|
case OpenPositionReady:
|
||
|
return s.runOpenPositionReady(ctx, nextState)
|
||
|
case OpenPositionOrderFilled:
|
||
|
return s.runOpenPositionOrderFilled(ctx, nextState)
|
||
|
case OpenPositionOrdersCancelling:
|
||
|
return s.runOpenPositionOrdersCancelling(ctx, nextState)
|
||
|
case OpenPositionOrdersCancelled:
|
||
|
return s.runOpenPositionOrdersCancelled(ctx, nextState)
|
||
|
case TakeProfitReady:
|
||
|
return s.runTakeProfitReady(ctx, nextState)
|
||
|
}
|
||
|
|
||
|
s.logger.Errorf("unexpected state: %d, please check it", s.state)
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func (s *Strategy) runWaitToOpenPositionState(ctx context.Context, next State) bool {
|
||
|
if s.nextRoundPaused {
|
||
|
s.logger.Info("[State] WaitToOpenPosition - nextRoundPaused is set")
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
if time.Now().Before(s.startTimeOfNextRound) {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
s.updateState(PositionOpening)
|
||
|
s.logger.Info("[State] WaitToOpenPosition -> PositionOpening")
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func (s *Strategy) runPositionOpening(ctx context.Context, next State) bool {
|
||
|
s.logger.Info("[State] PositionOpening - start placing open-position orders")
|
||
|
|
||
|
if err := s.placeOpenPositionOrders(ctx); err != nil {
|
||
|
s.logger.WithError(err).Error("failed to place open-position orders, please check it.")
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
s.updateState(OpenPositionReady)
|
||
|
s.logger.Info("[State] PositionOpening -> OpenPositionReady")
|
||
|
// do not trigger next state immediately, because OpenPositionReady state only trigger by kline to move to the next state
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func (s *Strategy) runOpenPositionReady(_ context.Context, next State) bool {
|
||
|
s.updateState(OpenPositionOrderFilled)
|
||
|
s.logger.Info("[State] OpenPositionReady -> OpenPositionOrderFilled")
|
||
|
// do not trigger next state immediately, because OpenPositionOrderFilled state only trigger by kline to move to the next state
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func (s *Strategy) runOpenPositionOrderFilled(_ context.Context, next State) bool {
|
||
|
s.updateState(OpenPositionOrdersCancelling)
|
||
|
s.logger.Info("[State] OpenPositionOrderFilled -> OpenPositionOrdersCancelling")
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func (s *Strategy) runOpenPositionOrdersCancelling(ctx context.Context, next State) bool {
|
||
|
s.logger.Info("[State] OpenPositionOrdersCancelling - start cancelling open-position orders")
|
||
|
if err := s.OrderExecutor.GracefulCancel(ctx); err != nil {
|
||
|
s.logger.WithError(err).Error("failed to cancel maker orders")
|
||
|
return false
|
||
|
}
|
||
|
s.updateState(OpenPositionOrdersCancelled)
|
||
|
s.logger.Info("[State] OpenPositionOrdersCancelling -> OpenPositionOrdersCancelled")
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
func (s *Strategy) runOpenPositionOrdersCancelled(ctx context.Context, next State) bool {
|
||
|
s.logger.Info("[State] OpenPositionOrdersCancelled - start placing take-profit orders")
|
||
|
if err := s.placeTakeProfitOrders(ctx); err != nil {
|
||
|
s.logger.WithError(err).Error("failed to open take profit orders")
|
||
|
return false
|
||
|
}
|
||
|
s.updateState(TakeProfitReady)
|
||
|
s.logger.Info("[State] OpenPositionOrdersCancelled -> TakeProfitReady")
|
||
|
// do not trigger next state immediately, because TakeProfitReady state only trigger by kline to move to the next state
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
func (s *Strategy) runTakeProfitReady(ctx context.Context, next State) bool {
|
||
|
// wait 3 seconds to avoid position not update
|
||
|
time.Sleep(3 * time.Second)
|
||
|
|
||
|
s.logger.Info("[State] TakeProfitReady - start reseting position and calculate quote investment for next round")
|
||
|
|
||
|
// update profit stats
|
||
|
if err := s.UpdateProfitStatsUntilSuccessful(ctx); err != nil {
|
||
|
s.logger.WithError(err).Warn("failed to calculate and emit profit")
|
||
|
}
|
||
|
|
||
|
// reset position and open new round for profit stats before position opening
|
||
|
s.Position.Reset()
|
||
|
|
||
|
// emit position
|
||
|
s.OrderExecutor.TradeCollector().EmitPositionUpdate(s.Position)
|
||
|
|
||
|
// store into redis
|
||
|
bbgo.Sync(ctx, s)
|
||
|
|
||
|
// set the start time of the next round
|
||
|
s.startTimeOfNextRound = time.Now().Add(s.CoolDownInterval.Duration())
|
||
|
s.updateState(WaitToOpenPosition)
|
||
|
s.logger.Infof("[State] TakeProfitReady -> WaitToOpenPosition (startTimeOfNextRound: %s)", s.startTimeOfNextRound.String())
|
||
|
|
||
|
return false
|
||
|
}
|