diff --git a/config/dca2.yaml b/config/dca2.yaml new file mode 100644 index 000000000..8c10453ca --- /dev/null +++ b/config/dca2.yaml @@ -0,0 +1,30 @@ +--- +backtest: + startTime: "2023-06-01" + endTime: "2023-07-01" + sessions: + - max + symbols: + - ETHUSDT + accounts: + binance: + balances: + USDT: 20_000.0 + +persistence: + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +exchangeStrategies: + +- on: max + dca2: + symbol: ETHUSDT + short: false + budget: 5000 + maxOrderNum: 10 + priceDeviation: 1% + takeProfitRatio: 1% + coolDownInterval: 5m diff --git a/pkg/exchange/max/stream.go b/pkg/exchange/max/stream.go index 72bece012..12a9ae32f 100644 --- a/pkg/exchange/max/stream.go +++ b/pkg/exchange/max/stream.go @@ -96,6 +96,9 @@ func (s *Stream) handleConnect() { case types.DepthLevelMedium: depth = 20 + case types.DepthLevel1: + depth = 1 + case types.DepthLevel5: depth = 5 diff --git a/pkg/strategy/common/strategy.go b/pkg/strategy/common/strategy.go index 991b6b8c5..cb040be2c 100644 --- a/pkg/strategy/common/strategy.go +++ b/pkg/strategy/common/strategy.go @@ -69,9 +69,11 @@ func (s *Strategy) Initialize(ctx context.Context, environ *bbgo.Environment, se s.OrderExecutor.BindEnvironment(environ) s.OrderExecutor.BindProfitStats(s.ProfitStats) s.OrderExecutor.Bind() - s.OrderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { - // bbgo.Sync(ctx, s) - }) + /* + s.OrderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(ctx, s) + }) + */ if !s.PositionHardLimit.IsZero() && !s.MaxPositionQuantity.IsZero() { log.Infof("positionHardLimit and maxPositionQuantity are configured, setting up PositionRiskControl...") diff --git a/pkg/strategy/dca2/callbacks.go b/pkg/strategy/dca2/callbacks.go deleted file mode 100644 index 718d76e88..000000000 --- a/pkg/strategy/dca2/callbacks.go +++ /dev/null @@ -1 +0,0 @@ -package dca2 diff --git a/pkg/strategy/dca2/debug.go b/pkg/strategy/dca2/debug.go new file mode 100644 index 000000000..8e44bf916 --- /dev/null +++ b/pkg/strategy/dca2/debug.go @@ -0,0 +1,19 @@ +package dca2 + +import ( + "fmt" + "strings" + + "github.com/c9s/bbgo/pkg/types" +) + +func (s *Strategy) debugOrders(submitOrders []types.Order) { + var sb strings.Builder + sb.WriteString("DCA ORDERS[\n") + for i, order := range submitOrders { + sb.WriteString(fmt.Sprintf("%3d) ", i+1) + order.String() + "\n") + } + sb.WriteString("] END OF DCA ORDERS") + + s.logger.Info(sb.String()) +} diff --git a/pkg/strategy/dca2/strategy.go b/pkg/strategy/dca2/strategy.go index 23c852480..55e13433b 100644 --- a/pkg/strategy/dca2/strategy.go +++ b/pkg/strategy/dca2/strategy.go @@ -3,10 +3,15 @@ package dca2 import ( "context" "fmt" + "math" + "sync" + "time" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/strategy/common" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/util" "github.com/sirupsen/logrus" ) @@ -21,17 +26,20 @@ func init() { } type Strategy struct { + *common.Strategy + Environment *bbgo.Environment Market types.Market Symbol string `json:"symbol"` // setting + Short bool `json:"short"` Budget fixedpoint.Value `json:"budget"` - OrderNum int64 `json:"orderNum"` - Margin fixedpoint.Value `json:"margin"` - TakeProfitSpread fixedpoint.Value `json:"takeProfitSpread"` - RoundInterval types.Duration `json:"roundInterval"` + MaxOrderNum int64 `json:"maxOrderNum"` + PriceDeviation fixedpoint.Value `json:"priceDeviation"` + TakeProfitRatio fixedpoint.Value `json:"takeProfitRatio"` + CoolDownInterval types.Duration `json:"coolDownInterval"` // OrderGroupID is the group ID used for the strategy instance for canceling orders OrderGroupID uint32 `json:"orderGroupID"` @@ -40,14 +48,12 @@ type Strategy struct { logger *logrus.Entry LogFields logrus.Fields `json:"logFields"` - // persistence fields: position and profit - Position *types.Position `persistence:"position"` - ProfitStats *types.ProfitStats `persistence:"profit_stats"` - // private field - session *bbgo.ExchangeSession - orderExecutor *bbgo.GeneralOrderExecutor - book *types.StreamOrderBook + mu sync.Mutex + makerSide types.SideType + takeProfitSide types.SideType + takeProfitPrice fixedpoint.Value + startTimeOfNextRound time.Time } func (s *Strategy) ID() string { @@ -55,15 +61,15 @@ func (s *Strategy) ID() string { } func (s *Strategy) Validate() error { - if s.OrderNum < 1 { + if s.MaxOrderNum < 1 { return fmt.Errorf("maxOrderNum can not be < 1") } - if s.TakeProfitSpread.Compare(fixedpoint.Zero) <= 0 { + if s.TakeProfitRatio.Sign() <= 0 { return fmt.Errorf("takeProfitSpread can not be <= 0") } - if s.Margin.Compare(fixedpoint.Zero) <= 0 { + if s.PriceDeviation.Sign() <= 0 { return fmt.Errorf("margin can not be <= 0") } @@ -91,95 +97,52 @@ func (s *Strategy) InstanceID() string { } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { - session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{Depth: types.DepthLevel1}) + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) } func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { - if s.Position == nil { - s.Position = types.NewPositionFromMarket(s.Market) - } - - if s.ProfitStats == nil { - s.ProfitStats = types.NewProfitStats(s.Market) - } - + s.Strategy = &common.Strategy{} + s.Strategy.Initialize(ctx, s.Environment, session, s.Market, ID, s.InstanceID()) instanceID := s.InstanceID() - s.session = session - s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) - s.orderExecutor.BindEnvironment(s.Environment) - s.orderExecutor.BindProfitStats(s.ProfitStats) - s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { - bbgo.Sync(ctx, s) - }) - s.orderExecutor.Bind() - s.book = types.NewStreamBook(s.Symbol) - s.book.BindStream(s.session.MarketDataStream) - balances := session.GetAccount().Balances() + if s.Short { + s.makerSide = types.SideTypeSell + s.takeProfitSide = types.SideTypeBuy + } else { + s.makerSide = types.SideTypeBuy + s.takeProfitSide = types.SideTypeSell + } + + if s.OrderGroupID == 0 { + s.OrderGroupID = util.FNV32(instanceID) % math.MaxInt32 + } + + // order executor + s.OrderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + s.logger.Infof("position: %s", s.Position.String()) + bbgo.Sync(ctx, s) + + // update take profit price here + }) + + session.MarketDataStream.OnKLine(func(kline types.KLine) { + // check price here + }) + + session.UserDataStream.OnAuth(func() { + s.logger.Info("user data stream authenticated, start the process") + // decide state here + }) + + balances, err := session.Exchange.QueryAccountBalances(ctx) + if err != nil { + return err + } + 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) } - session.MarketDataStream.OnBookUpdate(func(book types.SliceOrderBook) { - bid, ok := book.BestBid() - if !ok { - return - } - - takeProfitPrice := s.Market.TruncatePrice(s.Position.AverageCost.Mul(fixedpoint.One.Add(s.TakeProfitSpread))) - if bid.Price.Compare(takeProfitPrice) >= 0 { - } - }) - return nil } - -func (s *Strategy) generateMakerOrder(budget, askPrice, margin fixedpoint.Value, orderNum int64) ([]types.SubmitOrder, error) { - marginPrice := askPrice.Mul(margin) - price := askPrice - var prices []fixedpoint.Value - var total fixedpoint.Value - for i := 0; i < int(orderNum); i++ { - price = price.Sub(marginPrice) - truncatePrice := s.Market.TruncatePrice(price) - prices = append(prices, truncatePrice) - total = total.Add(truncatePrice) - } - - quantity := budget.Div(total) - quantity = s.Market.TruncateQuantity(quantity) - - var submitOrders []types.SubmitOrder - - for _, price := range prices { - submitOrders = append(submitOrders, types.SubmitOrder{ - Symbol: s.Symbol, - Market: s.Market, - Type: types.OrderTypeLimit, - Price: price, - Side: types.SideTypeBuy, - TimeInForce: types.TimeInForceGTC, - Quantity: quantity, - Tag: orderTag, - GroupID: s.OrderGroupID, - }) - } - - return submitOrders, nil -} - -func (s *Strategy) generateTakeProfitOrder(position *types.Position, takeProfitSpread fixedpoint.Value) types.SubmitOrder { - takeProfitPrice := s.Market.TruncatePrice(position.AverageCost.Mul(fixedpoint.One.Add(takeProfitSpread))) - return types.SubmitOrder{ - Symbol: s.Symbol, - Market: s.Market, - Type: types.OrderTypeLimit, - Price: takeProfitPrice, - Side: types.SideTypeSell, - TimeInForce: types.TimeInForceGTC, - Quantity: position.GetBase().Abs(), - Tag: orderTag, - GroupID: s.OrderGroupID, - } -} diff --git a/pkg/strategy/dca2/strategy_test.go b/pkg/strategy/dca2/strategy_test.go deleted file mode 100644 index 5278819fe..000000000 --- a/pkg/strategy/dca2/strategy_test.go +++ /dev/null @@ -1,141 +0,0 @@ -package dca2 - -import ( - "testing" - - "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/types" - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" -) - -func number(a interface{}) fixedpoint.Value { - switch v := a.(type) { - case string: - return fixedpoint.MustNewFromString(v) - case int: - return fixedpoint.NewFromInt(int64(v)) - case int64: - return fixedpoint.NewFromInt(int64(v)) - case float64: - return fixedpoint.NewFromFloat(v) - } - - return fixedpoint.Zero -} - -func newTestMarket(symbol string) types.Market { - switch symbol { - case "BTCUSDT": - return types.Market{ - BaseCurrency: "BTC", - QuoteCurrency: "USDT", - TickSize: number(0.01), - StepSize: number(0.000001), - PricePrecision: 2, - VolumePrecision: 8, - MinNotional: number(8.0), - MinQuantity: number(0.0003), - } - case "ETHUSDT": - return types.Market{ - BaseCurrency: "ETH", - QuoteCurrency: "USDT", - TickSize: number(0.01), - StepSize: number(0.00001), - PricePrecision: 2, - VolumePrecision: 6, - MinNotional: number(8.000), - MinQuantity: number(0.0046), - } - } - - // default - return types.Market{ - BaseCurrency: "BTC", - QuoteCurrency: "USDT", - TickSize: number(0.01), - StepSize: number(0.00001), - PricePrecision: 2, - VolumePrecision: 8, - MinNotional: number(10.0), - MinQuantity: number(0.001), - } -} - -func newTestStrategy(va ...string) *Strategy { - symbol := "BTCUSDT" - - if len(va) > 0 { - symbol = va[0] - } - - market := newTestMarket(symbol) - s := &Strategy{ - logger: logrus.NewEntry(logrus.New()), - Symbol: symbol, - Market: market, - } - return s -} - -func TestGenerateMakerOrder(t *testing.T) { - assert := assert.New(t) - - strategy := newTestStrategy() - - budget := number("105000") - askPrice := number("30000") - margin := number("0.05") - submitOrders, err := strategy.generateMakerOrder(budget, askPrice, margin, 4) - if !assert.NoError(err) { - return - } - - assert.Len(submitOrders, 4) - assert.Equal(submitOrders[0].Price, number("28500")) - assert.Equal(submitOrders[0].Quantity, number("1")) - assert.Equal(submitOrders[1].Price, number("27000")) - assert.Equal(submitOrders[1].Quantity, number("1")) - assert.Equal(submitOrders[2].Price, number("25500")) - assert.Equal(submitOrders[2].Quantity, number("1")) - assert.Equal(submitOrders[3].Price, number("24000")) - assert.Equal(submitOrders[3].Quantity, number("1")) -} - -func TestGenerateTakeProfitOrder(t *testing.T) { - assert := assert.New(t) - - strategy := newTestStrategy() - - position := types.NewPositionFromMarket(strategy.Market) - position.AddTrade(types.Trade{ - Side: types.SideTypeBuy, - Price: number("28500"), - Quantity: number("1"), - QuoteQuantity: number("28500"), - Fee: number("0.0015"), - FeeCurrency: strategy.Market.BaseCurrency, - }) - - o := strategy.generateTakeProfitOrder(position, number("10%")) - assert.Equal(number("31397.09"), o.Price) - assert.Equal(number("0.9985"), o.Quantity) - assert.Equal(types.SideTypeSell, o.Side) - assert.Equal(strategy.Symbol, o.Symbol) - - position.AddTrade(types.Trade{ - Side: types.SideTypeBuy, - Price: number("27000"), - Quantity: number("0.5"), - QuoteQuantity: number("13500"), - Fee: number("0.00075"), - FeeCurrency: strategy.Market.BaseCurrency, - }) - o = strategy.generateTakeProfitOrder(position, number("10%")) - assert.Equal(number("30846.26"), o.Price) - assert.Equal(number("1.49775"), o.Quantity) - assert.Equal(types.SideTypeSell, o.Side) - assert.Equal(strategy.Symbol, o.Symbol) - -}