Merge pull request #1195 from c9s/strategy/xalign

This commit is contained in:
c9s 2023-06-08 18:23:27 +08:00 committed by GitHub
commit fb078c9ede
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 325 additions and 6 deletions

44
config/xalign.yaml Normal file
View File

@ -0,0 +1,44 @@
---
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 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]
expectedBalances:
BTC: 0.0440

View File

@ -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"

View File

@ -0,0 +1,275 @@
package xalign
import (
"context"
"errors"
"fmt"
"strings"
"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 {
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) {
// 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(s.Interval.Duration())
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
}

View File

@ -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
}

View File

@ -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