2023-11-13 08:20:25 +00:00
package dca2
import (
"context"
"fmt"
2023-11-23 08:45:28 +00:00
"math"
"sync"
"time"
2023-11-13 08:20:25 +00:00
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
2023-11-23 08:45:28 +00:00
"github.com/c9s/bbgo/pkg/strategy/common"
2023-11-13 08:20:25 +00:00
"github.com/c9s/bbgo/pkg/types"
2023-11-23 08:45:28 +00:00
"github.com/c9s/bbgo/pkg/util"
2023-11-13 08:20:25 +00:00
"github.com/sirupsen/logrus"
)
const ID = "dca2"
const orderTag = "dca2"
var log = logrus . WithField ( "strategy" , ID )
func init ( ) {
bbgo . RegisterStrategy ( ID , & Strategy { } )
}
type Strategy struct {
2023-12-18 06:48:13 +00:00
* common . Strategy
2023-11-23 08:45:28 +00:00
2023-11-13 08:20:25 +00:00
Environment * bbgo . Environment
Market types . Market
Symbol string ` json:"symbol" `
// setting
Budget fixedpoint . Value ` json:"budget" `
2023-11-23 08:45:28 +00:00
MaxOrderNum int64 ` json:"maxOrderNum" `
PriceDeviation fixedpoint . Value ` json:"priceDeviation" `
TakeProfitRatio fixedpoint . Value ` json:"takeProfitRatio" `
CoolDownInterval types . Duration ` json:"coolDownInterval" `
2023-11-13 08:20:25 +00:00
// OrderGroupID is the group ID used for the strategy instance for canceling orders
OrderGroupID uint32 ` json:"orderGroupID" `
// log
logger * logrus . Entry
LogFields logrus . Fields ` json:"logFields" `
// private field
2023-11-23 08:45:28 +00:00
mu sync . Mutex
takeProfitPrice fixedpoint . Value
startTimeOfNextRound time . Time
2023-11-27 07:55:02 +00:00
nextStateC chan State
state State
2023-11-13 08:20:25 +00:00
}
func ( s * Strategy ) ID ( ) string {
return ID
}
func ( s * Strategy ) Validate ( ) error {
2023-11-23 08:45:28 +00:00
if s . MaxOrderNum < 1 {
2023-11-13 08:20:25 +00:00
return fmt . Errorf ( "maxOrderNum can not be < 1" )
}
2023-11-23 08:45:28 +00:00
if s . TakeProfitRatio . Sign ( ) <= 0 {
2023-11-13 08:20:25 +00:00
return fmt . Errorf ( "takeProfitSpread can not be <= 0" )
}
2023-11-23 08:45:28 +00:00
if s . PriceDeviation . Sign ( ) <= 0 {
2023-11-13 08:20:25 +00:00
return fmt . Errorf ( "margin can not be <= 0" )
}
// TODO: validate balance is enough
return nil
}
func ( s * Strategy ) Defaults ( ) error {
if s . LogFields == nil {
s . LogFields = logrus . Fields { }
}
s . LogFields [ "symbol" ] = s . Symbol
s . LogFields [ "strategy" ] = ID
return nil
}
func ( s * Strategy ) Initialize ( ) error {
s . logger = log . WithFields ( s . LogFields )
2023-12-18 06:48:13 +00:00
s . Strategy = & common . Strategy { }
2023-11-13 08:20:25 +00:00
return nil
}
func ( s * Strategy ) InstanceID ( ) string {
return fmt . Sprintf ( "%s-%s" , ID , s . Symbol )
}
func ( s * Strategy ) Subscribe ( session * bbgo . ExchangeSession ) {
2023-11-23 08:45:28 +00:00
session . Subscribe ( types . KLineChannel , s . Symbol , types . SubscribeOptions { Interval : types . Interval1m } )
2023-11-13 08:20:25 +00:00
}
func ( s * Strategy ) Run ( ctx context . Context , _ bbgo . OrderExecutor , session * bbgo . ExchangeSession ) error {
2023-11-23 08:45:28 +00:00
s . Strategy . Initialize ( ctx , s . Environment , session , s . Market , ID , s . InstanceID ( ) )
instanceID := s . InstanceID ( )
if s . OrderGroupID == 0 {
s . OrderGroupID = util . FNV32 ( instanceID ) % math . MaxInt32
2023-11-13 08:20:25 +00:00
}
2023-11-23 08:45:28 +00:00
// order executor
s . OrderExecutor . TradeCollector ( ) . OnPositionUpdate ( func ( position * types . Position ) {
2023-11-27 07:55:02 +00:00
s . logger . Infof ( "[DCA] POSITION UPDATE: %s" , s . Position . String ( ) )
2023-11-13 08:20:25 +00:00
bbgo . Sync ( ctx , s )
2023-11-23 08:45:28 +00:00
// update take profit price here
2023-11-27 07:55:02 +00:00
s . updateTakeProfitPrice ( )
} )
s . OrderExecutor . ActiveMakerOrders ( ) . OnFilled ( func ( o types . Order ) {
s . logger . Infof ( "[DCA] FILLED ORDER: %s" , o . String ( ) )
openPositionSide := types . SideTypeBuy
takeProfitSide := types . SideTypeSell
switch o . Side {
case openPositionSide :
2023-12-22 07:27:31 +00:00
s . emitNextState ( OpenPositionOrderFilled )
2023-11-27 07:55:02 +00:00
case takeProfitSide :
2023-12-22 07:27:31 +00:00
s . emitNextState ( WaitToOpenPosition )
2023-11-27 07:55:02 +00:00
default :
s . logger . Infof ( "[DCA] unsupported side (%s) of order: %s" , o . Side , o )
}
2023-11-23 08:45:28 +00:00
} )
2023-11-13 08:20:25 +00:00
2023-11-23 08:45:28 +00:00
session . MarketDataStream . OnKLine ( func ( kline types . KLine ) {
// check price here
2023-11-27 07:55:02 +00:00
if s . state != OpenPositionOrderFilled {
return
}
compRes := kline . Close . Compare ( s . takeProfitPrice )
// price doesn't hit the take profit price
2023-12-22 07:50:48 +00:00
if compRes < 0 {
2023-11-27 07:55:02 +00:00
return
}
2023-12-22 07:27:31 +00:00
s . emitNextState ( OpenPositionOrdersCancelling )
2023-11-13 08:20:25 +00:00
} )
2023-11-23 08:45:28 +00:00
session . UserDataStream . OnAuth ( func ( ) {
2023-11-27 07:55:02 +00:00
s . logger . Info ( "[DCA] user data stream authenticated" )
time . AfterFunc ( 3 * time . Second , func ( ) {
if isInitialize := s . initializeNextStateC ( ) ; ! isInitialize {
// recover
if err := s . recover ( ctx ) ; err != nil {
s . logger . WithError ( err ) . Error ( "[DCA] something wrong when state recovering" )
return
}
s . logger . Infof ( "[DCA] recovered state: %d" , s . state )
s . logger . Infof ( "[DCA] recovered position %s" , s . Position . String ( ) )
s . logger . Infof ( "[DCA] recovered budget %s" , s . Budget )
s . logger . Infof ( "[DCA] recovered startTimeOfNextRound %s" , s . startTimeOfNextRound )
2023-12-22 07:27:31 +00:00
s . updateTakeProfitPrice ( )
2023-11-27 07:55:02 +00:00
// store persistence
bbgo . Sync ( ctx , s )
// start running state machine
s . runState ( ctx )
}
} )
2023-11-23 08:45:28 +00:00
} )
2023-11-13 08:20:25 +00:00
2023-11-23 08:45:28 +00:00
balances , err := session . Exchange . QueryAccountBalances ( ctx )
if err != nil {
return err
2023-11-13 08:20:25 +00:00
}
2023-11-23 08:45:28 +00:00
balance := balances [ s . Market . QuoteCurrency ]
if balance . Available . Compare ( s . Budget ) < 0 {
return fmt . Errorf ( "the available balance of %s is %s which is less than budget setting %s, please check it" , s . Market . QuoteCurrency , balance . Available , s . Budget )
2023-11-13 08:20:25 +00:00
}
2023-11-23 08:45:28 +00:00
return nil
2023-11-13 08:20:25 +00:00
}
2023-11-27 07:55:02 +00:00
func ( s * Strategy ) updateTakeProfitPrice ( ) {
takeProfitRatio := s . TakeProfitRatio
s . takeProfitPrice = s . Market . TruncatePrice ( s . Position . AverageCost . Mul ( fixedpoint . One . Add ( takeProfitRatio ) ) )
s . logger . Infof ( "[DCA] cost: %s, ratio: %s, price: %s" , s . Position . AverageCost , takeProfitRatio , s . takeProfitPrice )
}