Merge pull request #1593 from c9s/c9s/xalign-add-test-cases

FIX: [xalign] add more complex test case for xalign strategy
This commit is contained in:
c9s 2024-03-19 16:07:57 +08:00 committed by GitHub
commit d58461d1cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 245 additions and 6 deletions

View File

@ -659,6 +659,10 @@ func (session *ExchangeSession) Markets() types.MarketMap {
return session.markets
}
func (session *ExchangeSession) SetMarkets(markets types.MarketMap) {
session.markets = markets
}
func (session *ExchangeSession) OrderStore(symbol string) (store *core.OrderStore, ok bool) {
store, ok = session.orderStores[symbol]
return store, ok

View File

@ -138,15 +138,16 @@ func (s *Strategy) selectSessionForCurrency(
}
// check both fromQuoteCurrency/currency and currency/fromQuoteCurrency
reversed := false
baseCurrency := currency
quoteCurrency := fromQuoteCurrency
symbol := currency + fromQuoteCurrency
symbol := currency + quoteCurrency
market, ok := session.Market(symbol)
if !ok {
// for TWD in USDT/TWD market, buy TWD means sell USDT
baseCurrency = fromQuoteCurrency
quoteCurrency = currency
symbol = fromQuoteCurrency + currency
symbol = baseCurrency + currency
market, ok = session.Market(symbol)
if !ok {
continue
@ -154,6 +155,7 @@ func (s *Strategy) selectSessionForCurrency(
// reverse side
side = side.Reverse()
reversed = true
}
ticker, err := session.Exchange.QueryTicker(ctx, symbol)
@ -169,10 +171,17 @@ func (s *Strategy) selectSessionForCurrency(
q := changeQuantity.Abs()
// a fast filtering
if reversed {
if q.Compare(market.MinNotional) < 0 {
log.Debugf("skip dust notional: %f", q.Float64())
continue
}
} else {
if q.Compare(market.MinQuantity) < 0 {
log.Debugf("skip dust quantity: %f", q.Float64())
continue
}
}
log.Infof("%s changeQuantity: %f ticker: %+v market: %+v", symbol, changeQuantity.Float64(), ticker, market)
@ -193,7 +202,13 @@ func (s *Strategy) selectSessionForCurrency(
continue
}
requiredQuoteAmount := q.Mul(price)
requiredQuoteAmount := fixedpoint.Zero
if reversed {
requiredQuoteAmount = q
} else {
requiredQuoteAmount = q.Mul(price)
}
requiredQuoteAmount = requiredQuoteAmount.Round(market.PricePrecision, fixedpoint.Up)
if requiredQuoteAmount.Compare(quoteBalance.Available) > 0 {
log.Warnf("required quote amount %f > quote balance %v, skip", requiredQuoteAmount.Float64(), quoteBalance)
@ -243,6 +258,10 @@ func (s *Strategy) selectSessionForCurrency(
price = ticker.Sell
}
if reversed {
q = q.Div(price)
}
baseBalance, ok := session.Account.Balance(baseCurrency)
if !ok {
continue

View File

@ -0,0 +1,216 @@
//go:build !dnum
package xalign
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
. "github.com/c9s/bbgo/pkg/testing/testhelper"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/types/mocks"
)
// cat ~/.bbgo/cache/max-markets.json | jq '.[] | select(.symbol == "USDTTWD")'
func getTestMarkets() types.MarketMap {
return map[string]types.Market{
"ETHBTC": {
Exchange: types.ExchangeMax,
Symbol: "ETHBTC",
LocalSymbol: "ETHBTC",
PricePrecision: 6,
VolumePrecision: 4,
BaseCurrency: "ETH",
QuoteCurrency: "BTC",
MinNotional: Number(0.00030000),
MinAmount: Number(0.00030000),
MinQuantity: Number(0.00460000),
StepSize: Number(0.00010000),
TickSize: Number(0.00000100),
},
"BTCUSDT": {
Exchange: types.ExchangeMax,
Symbol: "BTCUSDT",
LocalSymbol: "BTCUSDT",
PricePrecision: 2,
VolumePrecision: 6,
BaseCurrency: "BTC",
QuoteCurrency: "USDT",
MinNotional: Number(8.00000000),
MinAmount: Number(8.00000000),
MinQuantity: Number(0.00030000),
StepSize: Number(0.00000100),
TickSize: Number(0.01000000),
},
"BTCTWD": {
Exchange: types.ExchangeMax,
Symbol: "BTCTWD",
LocalSymbol: "BTCTWD",
PricePrecision: 1,
VolumePrecision: 8,
BaseCurrency: "BTC",
QuoteCurrency: "TWD",
MinNotional: Number(250.00000000),
MinAmount: Number(250.00000000),
MinQuantity: Number(0.00030000),
StepSize: Number(0.00000001),
TickSize: Number(0.01000000),
},
"ETHUSDT": {
Exchange: types.ExchangeMax,
Symbol: "ETHUSDT",
LocalSymbol: "ETHUSDT",
PricePrecision: 2,
VolumePrecision: 6,
BaseCurrency: "ETH",
QuoteCurrency: "USDT",
MinNotional: Number(8.00000000),
MinAmount: Number(8.00000000),
MinQuantity: Number(0.00460000),
StepSize: Number(0.00001000),
TickSize: Number(0.01000000),
},
"ETHTWD": {
Exchange: types.ExchangeMax,
Symbol: "ETHTWD",
LocalSymbol: "ETHTWD",
PricePrecision: 1,
VolumePrecision: 6,
BaseCurrency: "ETH",
QuoteCurrency: "TWD",
MinNotional: Number(250.00000000),
MinAmount: Number(250.00000000),
MinQuantity: Number(0.00460000),
StepSize: Number(0.00000100),
TickSize: Number(0.10000000),
},
"USDTTWD": {
Exchange: types.ExchangeMax,
Symbol: "USDTTWD",
LocalSymbol: "USDTTWD",
PricePrecision: 3,
VolumePrecision: 2,
BaseCurrency: "USDT",
QuoteCurrency: "TWD",
MinNotional: Number(250.00000000),
MinAmount: Number(250.00000000),
MinQuantity: Number(8.00000000),
StepSize: Number(0.01000000),
TickSize: Number(0.00100000),
},
}
}
func TestStrategy(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
ctx := context.Background()
s := &Strategy{
ExpectedBalances: map[string]fixedpoint.Value{
"TWD": Number(10_000),
},
PreferredQuoteCurrencies: &QuoteCurrencyPreference{
Buy: []string{"TWD", "USDT"},
Sell: []string{"USDT"},
},
PreferredSessions: []string{"max"},
UseTakerOrder: true,
}
testMarkets := getTestMarkets()
t.Run("buy TWD", func(t *testing.T) {
mockEx := mocks.NewMockExchange(mockCtrl)
mockEx.EXPECT().QueryTicker(ctx, "USDTTWD").Return(&types.Ticker{
Buy: Number(32.0),
Sell: Number(33.0),
}, nil)
account := types.NewAccount()
account.AddBalance("TWD", Number(20_000))
account.AddBalance("USDT", Number(80_000))
session := &bbgo.ExchangeSession{
Exchange: mockEx,
Account: account,
}
session.SetMarkets(testMarkets)
sessions := map[string]*bbgo.ExchangeSession{}
sessions["max"] = session
_, submitOrder := s.selectSessionForCurrency(ctx, sessions, "TWD", Number(70_000))
assert.NotNil(t, submitOrder)
assert.Equal(t, types.SideTypeSell, submitOrder.Side)
assert.Equal(t, Number(32).String(), submitOrder.Price.String())
assert.Equal(t, "2187.5", submitOrder.Quantity.String(), "70_000 / 32 best bid = 2187.5")
})
t.Run("sell TWD", func(t *testing.T) {
mockEx := mocks.NewMockExchange(mockCtrl)
mockEx.EXPECT().QueryTicker(ctx, "USDTTWD").Return(&types.Ticker{
Buy: Number(32.0),
Sell: Number(33.0),
}, nil)
account := types.NewAccount()
account.AddBalance("TWD", Number(20_000))
account.AddBalance("USDT", Number(80_000))
session := &bbgo.ExchangeSession{
Exchange: mockEx,
Account: account,
}
session.SetMarkets(testMarkets)
sessions := map[string]*bbgo.ExchangeSession{}
sessions["max"] = session
_, submitOrder := s.selectSessionForCurrency(ctx, sessions, "TWD", Number(-10_000))
assert.NotNil(t, submitOrder)
assert.Equal(t, types.SideTypeBuy, submitOrder.Side)
assert.Equal(t, Number(33).String(), submitOrder.Price.String())
assert.Equal(t, "303.03", submitOrder.Quantity.String(), "10_000 / 33 best ask = 303.0303030303")
})
t.Run("buy BTC with USDT instead of TWD", func(t *testing.T) {
mockEx := mocks.NewMockExchange(mockCtrl)
mockEx.EXPECT().QueryTicker(ctx, "BTCTWD").Return(&types.Ticker{
Sell: Number(36000.0 * 32),
Buy: Number(35000.0 * 31),
}, nil)
mockEx.EXPECT().QueryTicker(ctx, "BTCUSDT").Return(&types.Ticker{
Sell: Number(36000.0),
Buy: Number(35000.0),
}, nil)
account := types.NewAccount()
account.AddBalance("BTC", Number(0.955))
account.AddBalance("TWD", Number(60_000))
account.AddBalance("USDT", Number(80_000))
// 36000.0 * 32 * 0.045
session := &bbgo.ExchangeSession{
Exchange: mockEx,
Account: account,
}
session.SetMarkets(testMarkets)
sessions := map[string]*bbgo.ExchangeSession{}
sessions["max"] = session
_, submitOrder := s.selectSessionForCurrency(ctx, sessions, "BTC", Number(0.045))
assert.NotNil(t, submitOrder)
assert.Equal(t, types.SideTypeBuy, submitOrder.Side)
assert.Equal(t, "36000", submitOrder.Price.String())
assert.Equal(t, "0.045", submitOrder.Quantity.String())
})
}