2023-11-13 08:20:25 +00:00
package dca2
import (
"context"
"fmt"
2023-11-23 08:45:28 +00:00
"math"
2024-01-09 08:01:10 +00:00
"strconv"
2023-11-23 08:45:28 +00:00
"sync"
"time"
2023-11-13 08:20:25 +00:00
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
2024-01-10 06:37:07 +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-12-27 08:13:34 +00:00
"github.com/prometheus/client_golang/prometheus"
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 { } )
}
2024-01-02 06:09:38 +00:00
//go:generate callbackgen -type Strateg
2023-11-13 08:20:25 +00:00
type Strategy struct {
2024-01-02 09:16:47 +00:00
Position * types . Position ` json:"position,omitempty" persistence:"position" `
ProfitStats * ProfitStats ` json:"profitStats,omitempty" persistence:"profit_stats" `
2023-11-23 08:45:28 +00:00
2024-01-02 09:16:47 +00:00
Environment * bbgo . Environment
Session * bbgo . ExchangeSession
OrderExecutor * bbgo . GeneralOrderExecutor
Market types . Market
2023-11-13 08:20:25 +00:00
Symbol string ` json:"symbol" `
// setting
2024-01-02 09:16:47 +00:00
QuoteInvestment fixedpoint . Value ` json:"quoteInvestment" `
MaxOrderCount int64 ` json:"maxOrderCount" `
2023-11-23 08:45:28 +00:00
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" `
2023-12-27 08:13:34 +00:00
// RecoverWhenStart option is used for recovering dca states
RecoverWhenStart bool ` json:"recoverWhenStart" `
// KeepOrdersWhenShutdown option is used for keeping the grid orders when shutting down bbgo
KeepOrdersWhenShutdown bool ` json:"keepOrdersWhenShutdown" `
2023-11-13 08:20:25 +00:00
// log
logger * logrus . Entry
LogFields logrus . Fields ` json:"logFields" `
2023-12-27 08:13:34 +00:00
// PrometheusLabels will be used as the base prometheus labels
PrometheusLabels prometheus . Labels ` json:"prometheusLabels" `
2023-11-13 08:20:25 +00:00
// 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-12-27 08:13:34 +00:00
// callbacks
2024-01-10 06:37:07 +00:00
common . StatusCallbacks
2023-12-27 08:13:34 +00:00
positionCallbacks [ ] func ( * types . Position )
2024-01-02 09:16:47 +00:00
profitCallbacks [ ] func ( * ProfitStats )
2023-11-13 08:20:25 +00:00
}
func ( s * Strategy ) ID ( ) string {
return ID
}
func ( s * Strategy ) Validate ( ) error {
2024-01-02 09:16:47 +00:00
if s . MaxOrderCount < 1 {
return fmt . Errorf ( "maxOrderCount can not be < 1" )
2023-11-13 08:20:25 +00:00
}
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 )
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
instanceID := s . InstanceID ( )
2024-01-02 09:16:47 +00:00
s . Session = session
if s . ProfitStats == nil {
s . ProfitStats = newProfitStats ( s . Market , s . QuoteInvestment )
}
if s . Position == nil {
s . Position = types . NewPositionFromMarket ( s . Market )
}
s . Position . Strategy = ID
s . Position . StrategyInstanceID = instanceID
if session . MakerFeeRate . Sign ( ) > 0 || session . TakerFeeRate . Sign ( ) > 0 {
s . Position . SetExchangeFeeRate ( session . ExchangeName , types . ExchangeFee {
MakerFeeRate : session . MakerFeeRate ,
TakerFeeRate : session . TakerFeeRate ,
} )
}
s . OrderExecutor = bbgo . NewGeneralOrderExecutor ( session , s . Symbol , ID , instanceID , s . Position )
s . OrderExecutor . BindEnvironment ( s . Environment )
s . OrderExecutor . Bind ( )
2023-11-23 08:45:28 +00:00
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 {
2023-12-27 08:13:34 +00:00
if s . RecoverWhenStart {
// recover
if err := s . recover ( ctx ) ; err != nil {
s . logger . WithError ( err ) . Error ( "[DCA] something wrong when state recovering" )
return
}
2024-01-08 10:24:11 +00:00
s . logger . Infof ( "[DCA] state: %d" , s . state )
s . logger . Infof ( "[DCA] position %s" , s . Position . String ( ) )
s . logger . Infof ( "[DCA] profit stats %s" , s . ProfitStats . String ( ) )
s . logger . Infof ( "[DCA] startTimeOfNextRound %s" , s . startTimeOfNextRound )
2023-12-27 08:13:34 +00:00
} else {
s . state = WaitToOpenPosition
2023-11-27 07:55:02 +00:00
}
2023-12-22 07:27:31 +00:00
s . updateTakeProfitPrice ( )
2023-11-27 07:55:02 +00:00
// store persistence
bbgo . Sync ( ctx , s )
2024-01-02 06:09:38 +00:00
// ready
s . EmitReady ( )
2023-11-27 07:55:02 +00:00
// 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 ]
2024-01-08 10:24:11 +00:00
if balance . Available . Compare ( s . ProfitStats . QuoteInvestment ) < 0 {
return fmt . Errorf ( "the available balance of %s is %s which is less than quote investment setting %s, please check it" , s . Market . QuoteCurrency , balance . Available , s . ProfitStats . QuoteInvestment )
2023-11-13 08:20:25 +00:00
}
2023-12-27 08:13:34 +00:00
bbgo . OnShutdown ( ctx , func ( ctx context . Context , wg * sync . WaitGroup ) {
defer wg . Done ( )
if s . KeepOrdersWhenShutdown {
s . logger . Infof ( "keepOrdersWhenShutdown is set, will keep the orders on the exchange" )
return
}
if err := s . Close ( ctx ) ; err != nil {
s . logger . WithError ( err ) . Errorf ( "dca2 graceful order cancel error" )
}
} )
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 )
}
2023-12-27 08:13:34 +00:00
func ( s * Strategy ) Close ( ctx context . Context ) error {
s . logger . Infof ( "[DCA] closing %s dca2" , s . Symbol )
defer s . EmitClosed ( )
2024-01-03 09:02:03 +00:00
err := s . OrderExecutor . GracefulCancel ( ctx )
2024-01-03 08:41:59 +00:00
if err != nil {
s . logger . WithError ( err ) . Errorf ( "[DCA] there are errors when cancelling orders at close" )
}
2023-12-27 08:13:34 +00:00
2024-01-03 08:41:59 +00:00
bbgo . Sync ( ctx , s )
return err
2023-12-27 08:13:34 +00:00
}
func ( s * Strategy ) CleanUp ( ctx context . Context ) error {
_ = s . Initialize ( )
defer s . EmitClosed ( )
2024-01-03 08:41:59 +00:00
2024-01-03 09:02:03 +00:00
err := s . OrderExecutor . GracefulCancel ( ctx )
2024-01-03 08:41:59 +00:00
if err != nil {
s . logger . WithError ( err ) . Errorf ( "[DCA] there are errors when cancelling orders at clean up" )
}
bbgo . Sync ( ctx , s )
return err
2023-12-27 08:13:34 +00:00
}
2024-01-09 08:01:10 +00:00
func ( s * Strategy ) CalculateProfitOfCurrentRound ( ctx context . Context ) error {
historyService , ok := s . Session . Exchange . ( types . ExchangeTradeHistoryService )
if ! ok {
return fmt . Errorf ( "exchange %s doesn't support ExchangeTradeHistoryService" , s . Session . Exchange . Name ( ) )
}
queryService , ok := s . Session . Exchange . ( types . ExchangeOrderQueryService )
if ! ok {
return fmt . Errorf ( "exchange %s doesn't support ExchangeOrderQueryService" , s . Session . Exchange . Name ( ) )
}
// query the orders of this round
orders , err := historyService . QueryClosedOrders ( ctx , s . Symbol , time . Time { } , time . Time { } , s . ProfitStats . FromOrderID )
if err != nil {
return err
}
// query the trades of this round
for _ , order := range orders {
2024-01-10 06:37:07 +00:00
if order . OrderID > s . ProfitStats . FromOrderID {
s . ProfitStats . FromOrderID = order . OrderID
}
// skip not this strategy order
if order . GroupID != s . OrderGroupID {
continue
}
2024-01-09 08:01:10 +00:00
if order . ExecutedQuantity . Sign ( ) == 0 {
// skip no trade orders
continue
}
2024-01-10 06:37:07 +00:00
s . logger . Infof ( "[DCA] calculate profit stats from order: %s" , order . String ( ) )
2024-01-09 08:01:10 +00:00
trades , err := queryService . QueryOrderTrades ( ctx , types . OrderQuery {
Symbol : order . Symbol ,
OrderID : strconv . FormatUint ( order . OrderID , 10 ) ,
} )
if err != nil {
return err
}
for _ , trade := range trades {
2024-01-10 06:37:07 +00:00
s . logger . Infof ( "[DCA] calculate profit stats from trade: %s" , trade . String ( ) )
2024-01-09 08:01:10 +00:00
s . ProfitStats . AddTrade ( trade )
}
}
s . ProfitStats . FromOrderID = s . ProfitStats . FromOrderID + 1
s . ProfitStats . QuoteInvestment = s . ProfitStats . QuoteInvestment . Add ( s . ProfitStats . CurrentRoundProfit )
return nil
}