From c9ee4e52ccd1d88f649e71b40d7e7212d5ab4ec2 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 8 Jun 2023 15:54:32 +0800 Subject: [PATCH 1/4] xalign: add xalign strategy --- config/xalign.yaml | 41 +++++ pkg/cmd/strategy/builtin.go | 1 + pkg/strategy/xalign/strategy.go | 268 ++++++++++++++++++++++++++++++++ pkg/strategy/xnav/strategy.go | 5 +- pkg/types/balance.go | 6 +- 5 files changed, 315 insertions(+), 6 deletions(-) create mode 100644 config/xalign.yaml create mode 100644 pkg/strategy/xalign/strategy.go diff --git a/config/xalign.yaml b/config/xalign.yaml new file mode 100644 index 000000000..2be28cf82 --- /dev/null +++ b/config/xalign.yaml @@ -0,0 +1,41 @@ +--- +notifications: + slack: + defaultChannel: "dev-bbgo" + errorChannel: "bbgo-error" + + switches: + trade: true + orderUpdate: true + submitOrder: true + +sessions: + max: + exchange: max + envVarPrefix: max + + binance: + exchange: binance + envVarPrefix: binance + +persistence: + json: + directory: var/data + redis: + host: 127.0.0.1 + port: 6379 + db: 0 + +crossExchangeStrategies: + +- xalign: + interval: 1m + sessions: + - max + - binance + quoteCurrencies: + buy: [USDC, TWD] + sell: [USDT] + expectedBalances: + BTC: 0.0440 + diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index 50e1ec8bb..0a76cfb4e 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -36,6 +36,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/techsignal" _ "github.com/c9s/bbgo/pkg/strategy/trendtrader" _ "github.com/c9s/bbgo/pkg/strategy/wall" + _ "github.com/c9s/bbgo/pkg/strategy/xalign" _ "github.com/c9s/bbgo/pkg/strategy/xbalance" _ "github.com/c9s/bbgo/pkg/strategy/xfunding" _ "github.com/c9s/bbgo/pkg/strategy/xgap" diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go new file mode 100644 index 000000000..5c25d1db2 --- /dev/null +++ b/pkg/strategy/xalign/strategy.go @@ -0,0 +1,268 @@ +package xalign + +import ( + "context" + "errors" + "fmt" + "time" + + log "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +const ID = "xalign" + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type QuoteCurrencyPreference struct { + Buy []string `json:"buy"` + Sell []string `json:"sell"` +} + +type Strategy struct { + *bbgo.Environment + Interval types.Interval `json:"interval"` + PreferredSessions []string `json:"sessions"` + PreferredQuoteCurrencies *QuoteCurrencyPreference `json:"quoteCurrencies"` + ExpectedBalances map[string]fixedpoint.Value `json:"expectedBalances"` + UseTakerOrder bool `json:"useTakerOrder"` + + orderBook map[string]*bbgo.ActiveOrderBook +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s", ID) +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + // session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) +} + +func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) { + +} + +func (s *Strategy) Validate() error { + if s.PreferredQuoteCurrencies == nil { + return errors.New("quoteCurrencies is not defined") + } + + return nil +} + +func (s *Strategy) aggregateBalances(ctx context.Context, sessions map[string]*bbgo.ExchangeSession) (totalBalances types.BalanceMap, sessionBalances map[string]types.BalanceMap) { + totalBalances = make(types.BalanceMap) + sessionBalances = make(map[string]types.BalanceMap) + + // iterate the sessions and record them + for sessionName, session := range sessions { + // update the account balances and the margin information + if _, err := session.UpdateAccount(ctx); err != nil { + log.WithError(err).Errorf("can not update account") + return + } + + account := session.GetAccount() + balances := account.Balances() + + sessionBalances[sessionName] = balances + totalBalances = totalBalances.Add(balances) + } + + return totalBalances, sessionBalances +} + +func (s *Strategy) selectSessionForCurrency(ctx context.Context, sessions map[string]*bbgo.ExchangeSession, currency string, changeQuantity fixedpoint.Value) (*bbgo.ExchangeSession, *types.SubmitOrder) { + for _, sessionName := range s.PreferredSessions { + session := sessions[sessionName] + + var taker bool = s.UseTakerOrder + var side types.SideType + var quoteCurrencies []string + if changeQuantity.Sign() > 0 { + quoteCurrencies = s.PreferredQuoteCurrencies.Buy + side = types.SideTypeBuy + } else { + quoteCurrencies = s.PreferredQuoteCurrencies.Sell + side = types.SideTypeSell + } + + for _, quoteCurrency := range quoteCurrencies { + symbol := currency + quoteCurrency + market, ok := session.Market(symbol) + if !ok { + continue + } + + ticker, err := session.Exchange.QueryTicker(ctx, symbol) + if err != nil { + log.WithError(err).Errorf("unable to query ticker on %s", symbol) + continue + } + + // changeQuantity > 0 = buy + // changeQuantity < 0 = sell + q := changeQuantity.Abs() + + switch side { + + case types.SideTypeBuy: + quoteBalance, ok := session.Account.Balance(quoteCurrency) + if !ok { + continue + } + + price := ticker.Sell + if taker { + price = ticker.Sell + } else if ticker.Buy.Add(market.TickSize).Compare(ticker.Sell) < 0 { + price = ticker.Buy.Add(market.TickSize) + } else { + price = ticker.Buy + } + + requiredQuoteAmount := q.Div(price) + if requiredQuoteAmount.Compare(quoteBalance.Available) < 0 { + log.Warnf("required quote amount %f < quote balance %v", requiredQuoteAmount.Float64(), quoteBalance) + continue + } + + q = market.AdjustQuantityByMinNotional(q, price) + + return session, &types.SubmitOrder{ + Symbol: symbol, + Side: side, + Type: types.OrderTypeLimit, + Quantity: q, + Price: price, + Market: market, + TimeInForce: "GTC", + } + + case types.SideTypeSell: + baseBalance, ok := session.Account.Balance(currency) + if !ok { + continue + } + + if q.Compare(baseBalance.Available) > 0 { + log.Warnf("required base amount %f < available base balance %v", q.Float64(), baseBalance) + continue + } + + price := ticker.Buy + if taker { + price = ticker.Buy + } else if ticker.Sell.Add(market.TickSize.Neg()).Compare(ticker.Buy) < 0 { + price = ticker.Sell.Add(market.TickSize.Neg()) + } else { + price = ticker.Sell + } + + if market.IsDustQuantity(q, price) { + log.Infof("%s dust quantity: %f", currency, q.Float64()) + return nil, nil + } + + return session, &types.SubmitOrder{ + Symbol: symbol, + Side: side, + Type: types.OrderTypeLimit, + Quantity: q, + Price: price, + Market: market, + TimeInForce: "GTC", + } + } + + } + } + + return nil, nil +} + +func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error { + instanceID := s.InstanceID() + _ = instanceID + + s.orderBook = make(map[string]*bbgo.ActiveOrderBook) + + for _, sessionName := range s.PreferredSessions { + session, ok := sessions[sessionName] + if !ok { + return fmt.Errorf("incorrect preferred session name: %s is not defined", sessionName) + } + + orderBook := bbgo.NewActiveOrderBook("") + orderBook.BindStream(session.UserDataStream) + s.orderBook[sessionName] = orderBook + } + + go func() { + s.align(ctx, sessions) + + ticker := time.NewTicker(time.Minute) + defer ticker.Stop() + + for { + select { + + case <-ctx.Done(): + return + + case <-ticker.C: + s.align(ctx, sessions) + } + } + }() + + return nil +} + +func (s *Strategy) align(ctx context.Context, sessions map[string]*bbgo.ExchangeSession) { + totalBalances, sessionBalances := s.aggregateBalances(ctx, sessions) + _ = sessionBalances + + for sessionName, session := range sessions { + if err := s.orderBook[sessionName].GracefulCancel(ctx, session.Exchange); err != nil { + log.WithError(err).Errorf("can not cancel order") + } + } + + for currency, expectedBalance := range s.ExpectedBalances { + q := s.calculateRefillQuantity(totalBalances, currency, expectedBalance) + + selectedSession, submitOrder := s.selectSessionForCurrency(ctx, sessions, currency, q) + if selectedSession != nil && submitOrder != nil { + + log.Infof("placing order on %s: %#v", selectedSession.Name, submitOrder) + + createdOrder, err := selectedSession.Exchange.SubmitOrder(ctx, *submitOrder) + if err != nil { + log.WithError(err).Errorf("can not place order") + return + } + + if createdOrder != nil { + s.orderBook[selectedSession.Name].Add(*createdOrder) + } + } + } +} + +func (s *Strategy) calculateRefillQuantity(totalBalances types.BalanceMap, currency string, expectedBalance fixedpoint.Value) fixedpoint.Value { + if b, ok := totalBalances[currency]; ok { + netBalance := b.Net() + return expectedBalance.Sub(netBalance) + } + return expectedBalance +} diff --git a/pkg/strategy/xnav/strategy.go b/pkg/strategy/xnav/strategy.go index b0514d946..b4b33cab7 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -19,8 +19,6 @@ import ( const ID = "xnav" -const stateKey = "state-v1" - var log = logrus.WithField("strategy", ID) func init() { @@ -82,6 +80,7 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] priceTime := time.Now() // iterate the sessions and record them + quoteCurrency := "USDT" for sessionName, session := range sessions { // update the account balances and the margin information if _, err := session.UpdateAccount(ctx); err != nil { @@ -91,7 +90,7 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] account := session.GetAccount() balances := account.Balances() - if err := session.UpdatePrices(ctx, balances.Currencies(), "USDT"); err != nil { + if err := session.UpdatePrices(ctx, balances.Currencies(), quoteCurrency); err != nil { log.WithError(err).Error("price update failed") return } diff --git a/pkg/types/balance.go b/pkg/types/balance.go index e0c0255f7..dc83f65b0 100644 --- a/pkg/types/balance.go +++ b/pkg/types/balance.go @@ -71,15 +71,15 @@ func (b Balance) String() (o string) { o = fmt.Sprintf("%s: %s", b.Currency, b.Net().String()) if b.Locked.Sign() > 0 { - o += fmt.Sprintf(" (locked %v)", b.Locked) + o += fmt.Sprintf(" (locked %f)", b.Locked.Float64()) } if b.Borrowed.Sign() > 0 { - o += fmt.Sprintf(" (borrowed: %v)", b.Borrowed) + o += fmt.Sprintf(" (borrowed: %f)", b.Borrowed.Float64()) } if b.Interest.Sign() > 0 { - o += fmt.Sprintf(" (interest: %v)", b.Interest) + o += fmt.Sprintf(" (interest: %f)", b.Interest.Float64()) } return o From db43c872276a04b9cf107febc5c1a8a5ab2f2045 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 8 Jun 2023 16:00:43 +0800 Subject: [PATCH 2/4] xalign: load interval from config --- pkg/strategy/xalign/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index 5c25d1db2..11511dd98 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -210,7 +210,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se go func() { s.align(ctx, sessions) - ticker := time.NewTicker(time.Minute) + ticker := time.NewTicker(s.Interval.Duration()) defer ticker.Stop() for { From cbb9cc7722cc426cfe54b0734052d8bdc855c436 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 8 Jun 2023 16:04:43 +0800 Subject: [PATCH 3/4] xalign: add doc comment --- config/xalign.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/xalign.yaml b/config/xalign.yaml index 2be28cf82..ce68ccb20 100644 --- a/config/xalign.yaml +++ b/config/xalign.yaml @@ -33,6 +33,9 @@ crossExchangeStrategies: sessions: - max - binance + + ## quoteCurrencies config specifies which quote currency should be used for BUY order or SELL order. + ## when specifying [USDC,TWD] for "BUY", then it will consider BTCUSDT first then BTCTWD second. quoteCurrencies: buy: [USDC, TWD] sell: [USDT] From 7a6000a3163fb25cc0e4f2054b0bae62ab810042 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 8 Jun 2023 18:05:58 +0800 Subject: [PATCH 4/4] xalign: fix instanceID --- pkg/strategy/xalign/strategy.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/xalign/strategy.go b/pkg/strategy/xalign/strategy.go index 11511dd98..5c4457203 100644 --- a/pkg/strategy/xalign/strategy.go +++ b/pkg/strategy/xalign/strategy.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strings" "time" log "github.com/sirupsen/logrus" @@ -40,7 +41,13 @@ func (s *Strategy) ID() string { } func (s *Strategy) InstanceID() string { - return fmt.Sprintf("%s", ID) + var cs []string + + for cur := range s.ExpectedBalances { + cs = append(cs, cur) + } + + return ID + strings.Join(s.PreferredSessions, "-") + strings.Join(cs, "-") } func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {