Fix account value tests with price solver

Signed-off-by: c9s <yoanlin93@gmail.com>
This commit is contained in:
c9s 2024-10-04 19:45:07 +08:00
parent c3bf0ed7e7
commit 14fa561f6e
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
4 changed files with 140 additions and 95 deletions

View File

@ -9,6 +9,7 @@ import (
log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/pricesolver"
"github.com/c9s/bbgo/pkg/risk"
"github.com/c9s/bbgo/pkg/types"
)
@ -20,6 +21,8 @@ var maxIsolatedMarginLeverage = fixedpoint.NewFromInt(10)
var maxCrossMarginLeverage = fixedpoint.NewFromInt(3)
type AccountValueCalculator struct {
priceSolver *pricesolver.SimplePriceSolver
session *ExchangeSession
quoteCurrency string
prices map[string]fixedpoint.Value
@ -27,8 +30,13 @@ type AccountValueCalculator struct {
updateTime time.Time
}
func NewAccountValueCalculator(session *ExchangeSession, quoteCurrency string) *AccountValueCalculator {
func NewAccountValueCalculator(
session *ExchangeSession,
priceSolver *pricesolver.SimplePriceSolver,
quoteCurrency string,
) *AccountValueCalculator {
return &AccountValueCalculator{
priceSolver: priceSolver,
session: session,
quoteCurrency: quoteCurrency,
prices: make(map[string]fixedpoint.Value),
@ -39,6 +47,8 @@ func NewAccountValueCalculator(session *ExchangeSession, quoteCurrency string) *
func (c *AccountValueCalculator) UpdatePrices(ctx context.Context) error {
balances := c.session.Account.Balances()
currencies := balances.Currencies()
// TODO: improve this part
var symbols []string
for _, currency := range currencies {
if currency == c.quoteCurrency {
@ -49,19 +59,7 @@ func (c *AccountValueCalculator) UpdatePrices(ctx context.Context) error {
symbols = append(symbols, symbol)
}
tickers, err := c.session.Exchange.QueryTickers(ctx, symbols...)
if err != nil {
return err
}
c.tickers = tickers
for symbol, ticker := range tickers {
c.prices[symbol] = ticker.Last
if ticker.Time.After(c.updateTime) {
c.updateTime = ticker.Time
}
}
return nil
return c.priceSolver.UpdateFromTickers(ctx, c.session.Exchange, symbols...)
}
func (c *AccountValueCalculator) DebtValue(ctx context.Context) (fixedpoint.Value, error) {
@ -103,14 +101,17 @@ func (c *AccountValueCalculator) MarketValue(ctx context.Context) (fixedpoint.Va
continue
}
symbol := b.Currency + c.quoteCurrency
price, ok := c.prices[symbol]
if !ok {
continue
}
if c.priceSolver != nil {
if price, ok := c.priceSolver.ResolvePrice(b.Currency, c.quoteCurrency); ok {
marketValue = marketValue.Add(b.Total().Mul(price))
}
} else {
symbol := b.Currency + c.quoteCurrency
if price, ok := c.prices[symbol]; ok {
marketValue = marketValue.Add(b.Total().Mul(price))
}
}
}
return marketValue, nil
}
@ -123,25 +124,22 @@ func (c *AccountValueCalculator) NetValue(ctx context.Context) (fixedpoint.Value
}
balances := c.session.Account.Balances()
accountValue := calculateNetValueInQuote(balances, c.prices, c.quoteCurrency)
accountValue := calculateNetValueInQuote(balances, c.priceSolver, c.quoteCurrency)
return accountValue, nil
}
func calculateNetValueInQuote(balances types.BalanceMap, prices types.PriceMap, quoteCurrency string) (accountValue fixedpoint.Value) {
func calculateNetValueInQuote(
balances types.BalanceMap, priceSolver *pricesolver.SimplePriceSolver, quoteCurrency string,
) (accountValue fixedpoint.Value) {
accountValue = fixedpoint.Zero
for _, b := range balances {
if b.Currency == quoteCurrency {
accountValue = accountValue.Add(b.Net())
continue
}
symbol := b.Currency + quoteCurrency // for BTC/USDT, ETH/USDT pairs
symbolReverse := quoteCurrency + b.Currency // for USDT/USDC or USDT/TWD pairs
if price, ok := prices[symbol]; ok {
if price, ok := priceSolver.ResolvePrice(b.Currency, quoteCurrency); ok {
accountValue = accountValue.Add(b.Net().Mul(price))
} else if priceReverse, ok2 := prices[symbolReverse]; ok2 {
accountValue = accountValue.Add(b.Net().Div(priceReverse))
}
}
@ -220,7 +218,9 @@ func usdFiatBalances(balances types.BalanceMap) (fiats types.BalanceMap, rest ty
return fiats, rest
}
func CalculateBaseQuantity(session *ExchangeSession, market types.Market, price, quantity, leverage fixedpoint.Value) (fixedpoint.Value, error) {
func CalculateBaseQuantity(
session *ExchangeSession, market types.Market, price, quantity, leverage fixedpoint.Value,
) (fixedpoint.Value, error) {
// default leverage guard
if leverage.IsZero() {
leverage = defaultLeverage
@ -254,7 +254,7 @@ func CalculateBaseQuantity(session *ExchangeSession, market types.Market, price,
if len(restBalances) == 1 && types.IsUSDFiatCurrency(market.QuoteCurrency) {
totalUsdValue = aggregateUsdNetValue(balances)
} else if len(restBalances) > 1 {
accountValue := NewAccountValueCalculator(session, "USDT")
accountValue := NewAccountValueCalculator(session, nil, "USDT")
netValue, err := accountValue.NetValue(context.Background())
if err != nil {
return quantity, err
@ -329,7 +329,9 @@ func CalculateBaseQuantity(session *ExchangeSession, market types.Market, price,
errors.New("quantity is zero, can not submit sell order, please check your settings"))
}
func CalculateQuoteQuantity(ctx context.Context, session *ExchangeSession, quoteCurrency string, leverage fixedpoint.Value) (fixedpoint.Value, error) {
func CalculateQuoteQuantity(
ctx context.Context, session *ExchangeSession, quoteCurrency string, leverage fixedpoint.Value,
) (fixedpoint.Value, error) {
// default leverage guard
if leverage.IsZero() {
leverage = defaultLeverage
@ -359,7 +361,7 @@ func CalculateQuoteQuantity(ctx context.Context, session *ExchangeSession, quote
}
// using leverage -- starts from here
accountValue := NewAccountValueCalculator(session, quoteCurrency)
accountValue := NewAccountValueCalculator(session, nil, quoteCurrency)
availableQuote, err := accountValue.AvailableQuote(ctx)
if err != nil {
log.WithError(err).Errorf("can not update available quote")

View File

@ -9,6 +9,8 @@ import (
"go.uber.org/mock/gomock"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/pricesolver"
. "github.com/c9s/bbgo/pkg/testing/testhelper"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/types/mocks"
)
@ -27,6 +29,8 @@ func newTestTicker() types.Ticker {
}
func TestAccountValueCalculator_NetValue(t *testing.T) {
symbol := "BTCUSDT"
markets := AllMarkets()
t.Run("borrow and available", func(t *testing.T) {
mockCtrl := gomock.NewController(t)
@ -35,8 +39,8 @@ func TestAccountValueCalculator_NetValue(t *testing.T) {
mockEx := mocks.NewMockExchange(mockCtrl)
// for market data stream and user data stream
mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).Times(2)
mockEx.EXPECT().QueryTickers(gomock.Any(), []string{"BTCUSDT"}).Return(map[string]types.Ticker{
"BTCUSDT": newTestTicker(),
mockEx.EXPECT().QueryTickers(gomock.Any(), []string{symbol}).Return(map[string]types.Ticker{
"BTCUSDT": Ticker(symbol),
}, nil)
session := NewExchangeSession("test", mockEx)
@ -60,10 +64,12 @@ func TestAccountValueCalculator_NetValue(t *testing.T) {
})
assert.NotNil(t, session)
cal := NewAccountValueCalculator(session, "USDT")
ctx := context.Background()
priceSolver := pricesolver.NewSimplePriceResolver(markets)
cal := NewAccountValueCalculator(session, priceSolver, "USDT")
assert.NotNil(t, cal)
ctx := context.Background()
netValue, err := cal.NetValue(ctx)
assert.NoError(t, err)
assert.Equal(t, "20000", netValue.String())
@ -76,8 +82,8 @@ func TestAccountValueCalculator_NetValue(t *testing.T) {
mockEx := mocks.NewMockExchange(mockCtrl)
// for market data stream and user data stream
mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).Times(2)
mockEx.EXPECT().QueryTickers(gomock.Any(), []string{"BTCUSDT"}).Return(map[string]types.Ticker{
"BTCUSDT": newTestTicker(),
mockEx.EXPECT().QueryTickers(gomock.Any(), []string{symbol}).Return(map[string]types.Ticker{
symbol: Ticker(symbol),
}, nil)
session := NewExchangeSession("test", mockEx)
@ -101,10 +107,12 @@ func TestAccountValueCalculator_NetValue(t *testing.T) {
})
assert.NotNil(t, session)
cal := NewAccountValueCalculator(session, "USDT")
ctx := context.Background()
priceSolver := pricesolver.NewSimplePriceResolver(markets)
cal := NewAccountValueCalculator(session, priceSolver, "USDT")
assert.NotNil(t, cal)
ctx := context.Background()
netValue, err := cal.NetValue(ctx)
assert.NoError(t, err)
assert.Equal(t, "2000", netValue.String()) // 21000-19000
@ -115,11 +123,13 @@ func TestNewAccountValueCalculator_MarginLevel(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
ticker := newTestTicker()
mockEx := mocks.NewMockExchange(mockCtrl)
// for market data stream and user data stream
mockEx.EXPECT().NewStream().Return(&types.StandardStream{}).Times(2)
mockEx.EXPECT().QueryTickers(gomock.Any(), []string{"BTCUSDT"}).Return(map[string]types.Ticker{
"BTCUSDT": newTestTicker(),
"BTCUSDT": ticker,
}, nil)
session := NewExchangeSession("test", mockEx)
@ -143,10 +153,15 @@ func TestNewAccountValueCalculator_MarginLevel(t *testing.T) {
})
assert.NotNil(t, session)
cal := NewAccountValueCalculator(session, "USDT")
ctx := context.Background()
markets := AllMarkets()
priceSolver := pricesolver.NewSimplePriceResolver(markets)
err := priceSolver.UpdateFromTickers(ctx, mockEx, "BTCUSDT")
assert.NoError(t, err)
cal := NewAccountValueCalculator(session, priceSolver, "USDT")
assert.NotNil(t, cal)
ctx := context.Background()
marginLevel, err := cal.MarginLevel(ctx)
assert.NoError(t, err)
@ -173,10 +188,9 @@ func Test_aggregateUsdValue(t *testing.T) {
name: "mixed",
args: args{
balances: types.BalanceMap{
"USDC": types.Balance{Currency: "USDC", Available: number(70.0)},
"USDT": types.Balance{Currency: "USDT", Available: number(100.0)},
"BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)},
"BTC": types.Balance{Currency: "BTC", Available: number(0.01)},
"USDC": types.Balance{Currency: "USDC", Available: Number(70.0 + 80.0)},
"USDT": types.Balance{Currency: "USDT", Available: Number(100.0)},
"BTC": types.Balance{Currency: "BTC", Available: Number(0.01)},
},
},
want: number(250.0),
@ -202,19 +216,17 @@ func Test_usdFiatBalances(t *testing.T) {
{
args: args{
balances: types.BalanceMap{
"USDC": types.Balance{Currency: "USDC", Available: number(70.0)},
"USDT": types.Balance{Currency: "USDT", Available: number(100.0)},
"BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)},
"BTC": types.Balance{Currency: "BTC", Available: number(0.01)},
"USDC": types.Balance{Currency: "USDC", Available: Number(70.0 + 80.0)},
"USDT": types.Balance{Currency: "USDT", Available: Number(100.0)},
"BTC": types.Balance{Currency: "BTC", Available: Number(0.01)},
},
},
wantFiats: types.BalanceMap{
"USDC": types.Balance{Currency: "USDC", Available: number(70.0)},
"USDT": types.Balance{Currency: "USDT", Available: number(100.0)},
"BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)},
"USDC": types.Balance{Currency: "USDC", Available: Number(70.0 + 80.0)},
"USDT": types.Balance{Currency: "USDT", Available: Number(100.0)},
},
wantRest: types.BalanceMap{
"BTC": types.Balance{Currency: "BTC", Available: number(0.01)},
"BTC": types.Balance{Currency: "BTC", Available: Number(0.01)},
},
},
}
@ -242,52 +254,46 @@ func Test_calculateNetValueInQuote(t *testing.T) {
name: "positive asset",
args: args{
balances: types.BalanceMap{
"USDC": types.Balance{Currency: "USDC", Available: number(70.0)},
"USDC": types.Balance{Currency: "USDC", Available: number(70.0 + 80.0)},
"USDT": types.Balance{Currency: "USDT", Available: number(100.0)},
"BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)},
"BTC": types.Balance{Currency: "BTC", Available: number(0.01)},
},
prices: types.PriceMap{
"USDCUSDT": number(1.0),
"BUSDUSDT": number(1.0),
"BTCUSDT": number(19000.0),
"USDCUSDT": Number(1.0),
"BTCUSDT": Number(19000.0),
},
quoteCurrency: "USDT",
},
wantAccountValue: number(19000.0*0.01 + 100.0 + 80.0 + 70.0),
wantAccountValue: Number(19000.0*0.01 + 100.0 + 80.0 + 70.0),
},
{
name: "reversed usdt price",
args: args{
balances: types.BalanceMap{
"USDC": types.Balance{Currency: "USDC", Available: number(70.0)},
"TWD": types.Balance{Currency: "TWD", Available: number(3000.0)},
"USDT": types.Balance{Currency: "USDT", Available: number(100.0)},
"BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)},
"BTC": types.Balance{Currency: "BTC", Available: number(0.01)},
"USDC": types.Balance{Currency: "USDC", Available: Number(70.0 + 80.0)},
"TWD": types.Balance{Currency: "TWD", Available: Number(3000.0)},
"USDT": types.Balance{Currency: "USDT", Available: Number(100.0)},
"BTC": types.Balance{Currency: "BTC", Available: Number(0.01)},
},
prices: types.PriceMap{
"USDTTWD": number(30.0),
"USDCUSDT": number(1.0),
"BUSDUSDT": number(1.0),
"BTCUSDT": number(19000.0),
"USDTTWD": Number(30.0),
"USDCUSDT": Number(1.0),
"BTCUSDT": Number(19000.0),
},
quoteCurrency: "USDT",
},
wantAccountValue: number(19000.0*0.01 + 100.0 + 80.0 + 70.0 + (3000.0 / 30.0)),
wantAccountValue: Number(19000.0*0.01 + 100.0 + 80.0 + 70.0 + (3000.0 / 30.0)),
},
{
name: "borrow base asset",
args: args{
balances: types.BalanceMap{
"USDT": types.Balance{Currency: "USDT", Available: number(20000.0 * 2)},
"USDC": types.Balance{Currency: "USDC", Available: number(70.0)},
"BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)},
"BTC": types.Balance{Currency: "BTC", Available: number(0), Borrowed: number(2.0)},
"USDT": types.Balance{Currency: "USDT", Available: Number(20000.0*2 + 80.0)},
"USDC": types.Balance{Currency: "USDC", Available: Number(70.0)},
"BTC": types.Balance{Currency: "BTC", Available: Number(0), Borrowed: Number(2.0)},
},
prices: types.PriceMap{
"USDCUSDT": number(1.0),
"BUSDUSDT": number(1.0),
"BTCUSDT": number(19000.0),
},
quoteCurrency: "USDT",
@ -298,26 +304,35 @@ func Test_calculateNetValueInQuote(t *testing.T) {
name: "multi base asset",
args: args{
balances: types.BalanceMap{
"USDT": types.Balance{Currency: "USDT", Available: number(20000.0 * 2)},
"USDC": types.Balance{Currency: "USDC", Available: number(70.0)},
"BUSD": types.Balance{Currency: "BUSD", Available: number(80.0)},
"ETH": types.Balance{Currency: "ETH", Available: number(10.0)},
"BTC": types.Balance{Currency: "BTC", Available: number(0), Borrowed: number(2.0)},
"USDT": types.Balance{Currency: "USDT", Available: Number(20000.0*2 + 80.0)},
"USDC": types.Balance{Currency: "USDC", Available: Number(70.0)},
"ETH": types.Balance{Currency: "ETH", Available: Number(10.0)},
"BTC": types.Balance{Currency: "BTC", Available: Number(0), Borrowed: Number(2.0)},
},
prices: types.PriceMap{
"USDCUSDT": number(1.0),
"BUSDUSDT": number(1.0),
"ETHUSDT": number(1700.0),
"BTCUSDT": number(19000.0),
"USDCUSDT": Number(1.0),
"BTCUSDT": Number(19000.0),
"ETHUSDT": Number(1700.0),
},
quoteCurrency: "USDT",
},
wantAccountValue: number(19000.0*-2.0 + 1700.0*10.0 + 20000.0*2 + 80.0 + 70.0),
wantAccountValue: Number(19000.0*-2.0 + 1700.0*10.0 + 20000.0*2 + 80.0 + 70.0),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equalf(t, tt.wantAccountValue, calculateNetValueInQuote(tt.args.balances, tt.args.prices, tt.args.quoteCurrency), "calculateNetValueInQuote(%v, %v, %v)", tt.args.balances, tt.args.prices, tt.args.quoteCurrency)
markets := AllMarkets()
priceSolver := pricesolver.NewSimplePriceResolver(markets)
for symbol, price := range tt.args.prices {
priceSolver.Update(symbol, price)
}
assert.InDeltaf(t,
tt.wantAccountValue.Float64(),
calculateNetValueInQuote(tt.args.balances, priceSolver, tt.args.quoteCurrency).Float64(),
0.01,
"calculateNetValueInQuote(%v, %v, %v)", tt.args.balances, tt.args.prices, tt.args.quoteCurrency)
})
}
}

View File

@ -7,36 +7,64 @@ import (
"github.com/c9s/bbgo/pkg/types"
)
var markets = map[string]types.Market{
var _markets = types.MarketMap{
"BTCUSDT": {
Symbol: "BTCUSDT",
PricePrecision: 2,
VolumePrecision: 8,
QuoteCurrency: "USDT",
BaseCurrency: "BTC",
MinNotional: fixedpoint.MustNewFromString("0.001"),
MinNotional: fixedpoint.MustNewFromString("10.0"),
MinAmount: fixedpoint.MustNewFromString("10.0"),
MinQuantity: fixedpoint.MustNewFromString("0.001"),
TickSize: fixedpoint.MustNewFromString("0.01"),
},
"ETHUSDT": {
Symbol: "ETH",
Symbol: "ETHUSDT",
PricePrecision: 2,
VolumePrecision: 8,
QuoteCurrency: "USDT",
BaseCurrency: "ETH",
MinNotional: fixedpoint.MustNewFromString("0.005"),
MinNotional: fixedpoint.MustNewFromString("10.0"),
MinAmount: fixedpoint.MustNewFromString("10.0"),
MinQuantity: fixedpoint.MustNewFromString("0.001"),
TickSize: fixedpoint.MustNewFromString("0.01"),
},
"USDCUSDT": {
Symbol: "USDCUSDT",
PricePrecision: 5,
VolumePrecision: 2,
QuoteCurrency: "USDT",
BaseCurrency: "USDC",
MinNotional: fixedpoint.MustNewFromString("10.0"),
MinAmount: fixedpoint.MustNewFromString("10.0"),
MinQuantity: fixedpoint.MustNewFromString("10.0"),
TickSize: fixedpoint.MustNewFromString("0.0001"),
},
"USDTTWD": {
Symbol: "USDTTWD",
PricePrecision: 2,
VolumePrecision: 1,
QuoteCurrency: "TWD",
BaseCurrency: "USDT",
MinNotional: fixedpoint.MustNewFromString("10.0"),
MinAmount: fixedpoint.MustNewFromString("10.0"),
MinQuantity: fixedpoint.MustNewFromString("10.0"),
TickSize: fixedpoint.MustNewFromString("0.01"),
},
}
func AllMarkets() types.MarketMap {
return _markets
}
func Market(symbol string) types.Market {
market, ok := markets[symbol]
market, ok := _markets[symbol]
if !ok {
panic(fmt.Errorf("%s market not found, valid markets: %+v", symbol, markets))
panic(fmt.Errorf("%s test market not found, valid markets: %+v", symbol, _markets))
}
return market

View File

@ -8,7 +8,7 @@ import (
"github.com/c9s/bbgo/pkg/types"
)
var tickers = map[string]types.Ticker{
var _tickers = map[string]types.Ticker{
"BTCUSDT": {
Time: time.Now(),
Volume: fixedpoint.Zero,
@ -33,7 +33,7 @@ var tickers = map[string]types.Ticker{
}
func Ticker(symbol string) types.Ticker {
ticker, ok := tickers[symbol]
ticker, ok := _tickers[symbol]
if !ok {
panic(fmt.Errorf("%s test ticker not found, valid tickers: %+v", symbol, []string{"BTCUSDT", "ETHUSDT"}))
}