all: refactor NewAccountValueCalculator

This commit is contained in:
c9s 2024-10-05 13:09:31 +08:00
parent a718e30bb4
commit 6079e7b06a
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
8 changed files with 144 additions and 192 deletions

View File

@ -39,7 +39,6 @@ func NewAccountValueCalculator(
priceSolver: priceSolver,
session: session,
quoteCurrency: quoteCurrency,
prices: make(map[string]fixedpoint.Value),
tickers: make(map[string]types.Ticker),
}
}
@ -62,99 +61,69 @@ func (c *AccountValueCalculator) UpdatePrices(ctx context.Context) error {
return c.priceSolver.UpdateFromTickers(ctx, c.session.Exchange, symbols...)
}
func (c *AccountValueCalculator) DebtValue(ctx context.Context) (fixedpoint.Value, error) {
debtValue := fixedpoint.Zero
if len(c.prices) == 0 {
if err := c.UpdatePrices(ctx); err != nil {
return debtValue, err
}
}
func (c *AccountValueCalculator) DebtValue() fixedpoint.Value {
balances := c.session.Account.Balances()
for _, b := range balances {
symbol := b.Currency + c.quoteCurrency
price, ok := c.prices[symbol]
if !ok {
continue
}
debtValue = debtValue.Add(b.Debt().Mul(price))
}
return debtValue, nil
return totalValueInQuote(balances, c.priceSolver, c.quoteCurrency, func(
prev fixedpoint.Value, b types.Balance, price fixedpoint.Value,
) fixedpoint.Value {
return prev.Add(b.Debt().Mul(price))
})
}
func (c *AccountValueCalculator) MarketValue(ctx context.Context) (fixedpoint.Value, error) {
marketValue := fixedpoint.Zero
if len(c.prices) == 0 {
if err := c.UpdatePrices(ctx); err != nil {
return marketValue, err
}
}
func (c *AccountValueCalculator) MarketValue() fixedpoint.Value {
balances := c.session.Account.Balances()
for _, b := range balances {
if b.Currency == c.quoteCurrency {
marketValue = marketValue.Add(b.Total())
continue
}
return totalValueInQuote(balances, c.priceSolver, c.quoteCurrency, func(
prev fixedpoint.Value, b types.Balance, price fixedpoint.Value,
) fixedpoint.Value {
return prev.Add(b.Total().Mul(price))
})
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
}
func (c *AccountValueCalculator) NetValue(ctx context.Context) (fixedpoint.Value, error) {
if len(c.prices) == 0 {
if err := c.UpdatePrices(ctx); err != nil {
return fixedpoint.Zero, err
func (c *AccountValueCalculator) NetValue() fixedpoint.Value {
balances := c.session.Account.Balances()
return totalValueInQuote(balances, c.priceSolver, c.quoteCurrency, func(
prev fixedpoint.Value, b types.Balance, price fixedpoint.Value,
) fixedpoint.Value {
return prev.Add(b.Net().Mul(price))
})
}
func totalValueInQuote(
balances types.BalanceMap,
priceSolver *pricesolver.SimplePriceSolver,
quoteCurrency string,
algo func(prev fixedpoint.Value, b types.Balance, price fixedpoint.Value) fixedpoint.Value,
) (totalValue fixedpoint.Value) {
totalValue = fixedpoint.Zero
for _, b := range balances {
if b.Currency == quoteCurrency {
totalValue = algo(totalValue, b, fixedpoint.One)
continue
} else if price, ok := priceSolver.ResolvePrice(b.Currency, quoteCurrency); ok {
totalValue = algo(totalValue, b, price)
}
}
balances := c.session.Account.Balances()
accountValue := calculateNetValueInQuote(balances, c.priceSolver, c.quoteCurrency)
return accountValue, nil
return totalValue
}
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
}
if price, ok := priceSolver.ResolvePrice(b.Currency, quoteCurrency); ok {
accountValue = accountValue.Add(b.Net().Mul(price))
}
}
return accountValue
balances types.BalanceMap,
priceSolver *pricesolver.SimplePriceSolver,
quoteCurrency string,
) fixedpoint.Value {
return totalValueInQuote(balances, priceSolver, quoteCurrency, func(
prev fixedpoint.Value, b types.Balance, price fixedpoint.Value,
) fixedpoint.Value {
return prev.Add(b.Net().Mul(price))
})
}
func (c *AccountValueCalculator) AvailableQuote(ctx context.Context) (fixedpoint.Value, error) {
func (c *AccountValueCalculator) AvailableQuote() (fixedpoint.Value, error) {
accountValue := fixedpoint.Zero
if len(c.prices) == 0 {
if err := c.UpdatePrices(ctx); err != nil {
return accountValue, err
}
}
balances := c.session.Account.Balances()
for _, b := range balances {
if b.Currency == c.quoteCurrency {
@ -162,13 +131,9 @@ func (c *AccountValueCalculator) AvailableQuote(ctx context.Context) (fixedpoint
continue
}
symbol := b.Currency + c.quoteCurrency
price, ok := c.prices[symbol]
if !ok {
continue
if price, ok := c.priceSolver.ResolvePrice(b.Currency, c.quoteCurrency); ok {
accountValue = accountValue.Add(b.Net().Mul(price))
}
accountValue = accountValue.Add(b.Net().Mul(price))
}
return accountValue, nil
@ -176,20 +141,15 @@ func (c *AccountValueCalculator) AvailableQuote(ctx context.Context) (fixedpoint
// MarginLevel calculates the margin level from the asset market value and the debt value
// See https://www.binance.com/en/support/faq/360030493931
func (c *AccountValueCalculator) MarginLevel(ctx context.Context) (fixedpoint.Value, error) {
marginLevel := fixedpoint.Zero
marketValue, err := c.MarketValue(ctx)
if err != nil {
return marginLevel, err
func (c *AccountValueCalculator) MarginLevel() (fixedpoint.Value, error) {
marketValue := c.MarketValue()
debtValue := c.DebtValue()
if marketValue.IsZero() || debtValue.IsZero() {
return fixedpoint.NewFromFloat(999.0), nil
}
debtValue, err := c.DebtValue(ctx)
if err != nil {
return marginLevel, err
}
marginLevel = marketValue.Div(debtValue)
return marginLevel, nil
return marketValue.Div(debtValue), nil
}
func aggregateUsdNetValue(balances types.BalanceMap) fixedpoint.Value {
@ -255,11 +215,7 @@ func CalculateBaseQuantity(
totalUsdValue = aggregateUsdNetValue(balances)
} else if len(restBalances) > 1 {
accountValue := NewAccountValueCalculator(session, nil, "USDT")
netValue, err := accountValue.NetValue(context.Background())
if err != nil {
return quantity, err
}
netValue := accountValue.NetValue()
totalUsdValue = netValue
} else {
// TODO: translate quote currency like BTC of ETH/BTC to usd value
@ -362,7 +318,7 @@ func CalculateQuoteQuantity(
// using leverage -- starts from here
accountValue := NewAccountValueCalculator(session, nil, quoteCurrency)
availableQuote, err := accountValue.AvailableQuote(ctx)
availableQuote, err := accountValue.AvailableQuote()
if err != nil {
log.WithError(err).Errorf("can not update available quote")
return fixedpoint.Zero, err

View File

@ -3,7 +3,6 @@ package bbgo
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
@ -15,19 +14,6 @@ import (
"github.com/c9s/bbgo/pkg/types/mocks"
)
func newTestTicker() types.Ticker {
return types.Ticker{
Time: time.Now(),
Volume: fixedpoint.Zero,
Last: fixedpoint.NewFromFloat(19000.0),
Open: fixedpoint.NewFromFloat(19500.0),
High: fixedpoint.NewFromFloat(19900.0),
Low: fixedpoint.NewFromFloat(18800.0),
Buy: fixedpoint.NewFromFloat(19500.0),
Sell: fixedpoint.NewFromFloat(18900.0),
}
}
func TestAccountValueCalculator_NetValue(t *testing.T) {
symbol := "BTCUSDT"
markets := AllMarkets()
@ -36,12 +22,11 @@ func TestAccountValueCalculator_NetValue(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
ticker := Ticker(symbol)
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{symbol}).Return(map[string]types.Ticker{
"BTCUSDT": Ticker(symbol),
}, nil)
mockEx.EXPECT().QueryTicker(gomock.Any(), symbol).Return(&ticker, nil).AnyTimes()
session := NewExchangeSession("test", mockEx)
session.Account.UpdateBalances(types.BalanceMap{
@ -64,14 +49,12 @@ func TestAccountValueCalculator_NetValue(t *testing.T) {
})
assert.NotNil(t, session)
ctx := context.Background()
priceSolver := pricesolver.NewSimplePriceResolver(markets)
priceSolver.Update(symbol, ticker.GetValidPrice())
cal := NewAccountValueCalculator(session, priceSolver, "USDT")
assert.NotNil(t, cal)
netValue, err := cal.NetValue(ctx)
assert.NoError(t, err)
netValue := cal.NetValue()
assert.Equal(t, "20000", netValue.String())
})
@ -79,12 +62,12 @@ func TestAccountValueCalculator_NetValue(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
ticker := Ticker(symbol)
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{symbol}).Return(map[string]types.Ticker{
symbol: Ticker(symbol),
}, nil)
mockEx.EXPECT().QueryTicker(gomock.Any(), symbol).Return(&ticker, nil).AnyTimes()
session := NewExchangeSession("test", mockEx)
session.Account.UpdateBalances(types.BalanceMap{
@ -105,16 +88,12 @@ func TestAccountValueCalculator_NetValue(t *testing.T) {
NetAsset: fixedpoint.Zero,
},
})
assert.NotNil(t, session)
ctx := context.Background()
priceSolver := pricesolver.NewSimplePriceResolver(markets)
priceSolver.Update(symbol, ticker.GetValidPrice())
cal := NewAccountValueCalculator(session, priceSolver, "USDT")
assert.NotNil(t, cal)
netValue, err := cal.NetValue(ctx)
assert.NoError(t, err)
netValue := cal.NetValue()
assert.Equal(t, "2000", netValue.String()) // 21000-19000
})
}
@ -123,14 +102,13 @@ func TestNewAccountValueCalculator_MarginLevel(t *testing.T) {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()
ticker := newTestTicker()
symbol := "BTCUSDT"
ticker := Ticker(symbol)
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": ticker,
}, nil)
mockEx.EXPECT().QueryTicker(gomock.Any(), symbol).Return(&ticker, nil).AnyTimes()
session := NewExchangeSession("test", mockEx)
session.Account.UpdateBalances(types.BalanceMap{
@ -142,27 +120,20 @@ func TestNewAccountValueCalculator_MarginLevel(t *testing.T) {
Interest: fixedpoint.NewFromFloat(0.003),
NetAsset: fixedpoint.Zero,
},
"USDT": {
Currency: "USDT",
Available: fixedpoint.NewFromFloat(21000.0),
Locked: fixedpoint.Zero,
Borrowed: fixedpoint.Zero,
Interest: fixedpoint.Zero,
NetAsset: fixedpoint.Zero,
},
"USDT": Balance("USDT", Number(21000.0)),
})
assert.NotNil(t, session)
ctx := context.Background()
markets := AllMarkets()
priceSolver := pricesolver.NewSimplePriceResolver(markets)
err := priceSolver.UpdateFromTickers(ctx, mockEx, "BTCUSDT")
err := priceSolver.UpdateFromTickers(ctx, mockEx, symbol)
assert.NoError(t, err)
cal := NewAccountValueCalculator(session, priceSolver, "USDT")
assert.NotNil(t, cal)
marginLevel, err := cal.MarginLevel(ctx)
marginLevel, err := cal.MarginLevel()
assert.NoError(t, err)
// expected (21000 / 19000 * 1.003)

View File

@ -99,7 +99,6 @@ func (m *SimplePriceSolver) UpdateFromTickers(ctx context.Context, ex types.Exch
}
func (m *SimplePriceSolver) inferencePrice(asset string, assetPrice fixedpoint.Value, preferredFiats ...string) (fixedpoint.Value, bool) {
// log.Infof("inferencePrice %s = %f", asset, assetPrice.Float64())
quotePrices, ok := m.pricesByBase[asset]
if ok {
for quote, price := range quotePrices {
@ -122,10 +121,8 @@ func (m *SimplePriceSolver) inferencePrice(asset string, assetPrice fixedpoint.V
basePrices, ok := m.pricesByQuote[asset]
if ok {
for base, basePrice := range basePrices {
// log.Infof("base %s @ %s", base, basePrice.String())
for _, fiat := range preferredFiats {
if base == fiat {
// log.Infof("ret %f / %f = %f", assetPrice.Float64(), basePrice.Float64(), assetPrice.Div(basePrice).Float64())
return assetPrice.Div(basePrice), true
}
}
@ -142,6 +139,12 @@ func (m *SimplePriceSolver) inferencePrice(asset string, assetPrice fixedpoint.V
}
func (m *SimplePriceSolver) ResolvePrice(asset string, preferredFiats ...string) (fixedpoint.Value, bool) {
if len(preferredFiats) == 0 {
return fixedpoint.Zero, false
} else if asset == preferredFiats[0] {
return fixedpoint.One, true
}
m.mu.Lock()
defer m.mu.Unlock()
return m.inferencePrice(asset, fixedpoint.One, preferredFiats...)

View File

@ -6,13 +6,14 @@ import (
"os"
"sync"
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/data/tsv"
"github.com/c9s/bbgo/pkg/datatype/floats"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
"github.com/sirupsen/logrus"
)
const ID = "harmonic"
@ -29,7 +30,7 @@ type Strategy struct {
Market types.Market
types.IntervalWindow
//bbgo.OpenPositionOptions
// bbgo.OpenPositionOptions
// persistence fields
Position *types.Position `persistence:"position"`
@ -239,7 +240,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
// Cancel active orders
_ = s.orderExecutor.GracefulCancel(ctx)
// Close 100% position
//_ = s.ClosePosition(ctx, fixedpoint.One)
// _ = s.ClosePosition(ctx, fixedpoint.One)
})
s.session = session
@ -258,7 +259,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.orderExecutor.BindTradeStats(s.TradeStats)
// AccountValueCalculator
s.AccountValueCalculator = bbgo.NewAccountValueCalculator(s.session, s.Market.QuoteCurrency)
s.AccountValueCalculator = bbgo.NewAccountValueCalculator(s.session, nil, s.Market.QuoteCurrency)
// Accumulated profit report
if bbgo.IsBackTesting {

View File

@ -255,7 +255,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.orderExecutor.BindTradeStats(s.TradeStats)
// AccountValueCalculator
s.AccountValueCalculator = bbgo.NewAccountValueCalculator(s.session, s.Market.QuoteCurrency)
s.AccountValueCalculator = bbgo.NewAccountValueCalculator(s.session, nil, s.Market.QuoteCurrency)
// Accumulated profit report
if bbgo.IsBackTesting {

View File

@ -255,7 +255,9 @@ func (s *Strategy) getSide(stSignal types.Direction, demaSignal types.Direction,
return side
}
func (s *Strategy) generateOrderForm(side types.SideType, quantity fixedpoint.Value, marginOrderSideEffect types.MarginOrderSideEffectType) types.SubmitOrder {
func (s *Strategy) generateOrderForm(
side types.SideType, quantity fixedpoint.Value, marginOrderSideEffect types.MarginOrderSideEffectType,
) types.SubmitOrder {
orderForm := types.SubmitOrder{
Symbol: s.Symbol,
Market: s.Market,
@ -382,7 +384,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}
// AccountValueCalculator
s.AccountValueCalculator = bbgo.NewAccountValueCalculator(s.session, s.Market.QuoteCurrency)
s.AccountValueCalculator = bbgo.NewAccountValueCalculator(s.session, nil, s.Market.QuoteCurrency)
// For drawing
profitSlice := floats.Slice{1., 1.}

View File

@ -187,6 +187,8 @@ type Strategy struct {
logger logrus.FieldLogger
metricsLabels prometheus.Labels
connectivityGroup *types.ConnectivityGroup
}
func (s *Strategy) ID() string {
@ -654,40 +656,37 @@ func (s *Strategy) updateQuote(ctx context.Context) error {
hedgeAccount.MarginLevel.String(),
s.MinMarginLevel.String())
netValueInUsd, calcErr := s.accountValueCalculator.NetValue(ctx)
if calcErr != nil {
s.logger.WithError(calcErr).Errorf("unable to calculate the net value")
} else {
// calculate credit buffer
s.logger.Infof("hedge account net value in usd: %f", netValueInUsd.Float64())
netValueInUsd := s.accountValueCalculator.NetValue()
maximumValueInUsd := netValueInUsd.Mul(s.MaxHedgeAccountLeverage)
// calculate credit buffer
s.logger.Infof("hedge account net value in usd: %f", netValueInUsd.Float64())
s.logger.Infof("hedge account maximum leveraged value in usd: %f (%f x)", maximumValueInUsd.Float64(), s.MaxHedgeAccountLeverage.Float64())
maximumValueInUsd := netValueInUsd.Mul(s.MaxHedgeAccountLeverage)
if quote, ok := hedgeAccount.Balance(s.sourceMarket.QuoteCurrency); ok {
debt := quote.Debt()
quota := maximumValueInUsd.Sub(debt)
s.logger.Infof("hedge account maximum leveraged value in usd: %f (%f x)", maximumValueInUsd.Float64(), s.MaxHedgeAccountLeverage.Float64())
s.logger.Infof("hedge account quote balance: %s, debt: %s, quota: %s",
quote.String(),
debt.String(),
quota.String())
if quote, ok := hedgeAccount.Balance(s.sourceMarket.QuoteCurrency); ok {
debt := quote.Debt()
quota := maximumValueInUsd.Sub(debt)
hedgeQuota.QuoteAsset.Add(quota)
}
s.logger.Infof("hedge account quote balance: %s, debt: %s, quota: %s",
quote.String(),
debt.String(),
quota.String())
if base, ok := hedgeAccount.Balance(s.sourceMarket.BaseCurrency); ok {
debt := base.Debt()
quota := maximumValueInUsd.Div(bestAsk.Price).Sub(debt)
hedgeQuota.QuoteAsset.Add(quota)
}
s.logger.Infof("hedge account base balance: %s, debt: %s, quota: %s",
base.String(),
debt.String(),
quota.String())
if base, ok := hedgeAccount.Balance(s.sourceMarket.BaseCurrency); ok {
debt := base.Debt()
quota := maximumValueInUsd.Div(bestAsk.Price).Sub(debt)
hedgeQuota.BaseAsset.Add(quota)
}
s.logger.Infof("hedge account base balance: %s, debt: %s, quota: %s",
base.String(),
debt.String(),
quota.String())
hedgeQuota.BaseAsset.Add(quota)
}
}
} else {
@ -1322,12 +1321,7 @@ func (s *Strategy) accountUpdater(ctx context.Context) {
return
}
netValue, err := s.accountValueCalculator.NetValue(ctx)
if err != nil {
log.WithError(err).Errorf("unable to update account")
return
}
netValue := s.accountValueCalculator.NetValue()
s.logger.Infof("hedge session net value ~= %f USD", netValue.Float64())
}
}
@ -1419,7 +1413,7 @@ func (s *Strategy) CrossRun(
return fmt.Errorf("maker session market %s is not defined", s.Symbol)
}
s.accountValueCalculator = bbgo.NewAccountValueCalculator(s.sourceSession, s.sourceMarket.QuoteCurrency)
s.accountValueCalculator = bbgo.NewAccountValueCalculator(s.sourceSession, nil, s.sourceMarket.QuoteCurrency)
indicators := s.sourceSession.Indicators(s.Symbol)
@ -1622,13 +1616,27 @@ func (s *Strategy) CrossRun(
s.stopC = make(chan struct{})
sourceConnectivity := types.NewConnectivity()
sourceConnectivity.Bind(s.sourceSession.UserDataStream)
s.connectivityGroup = types.NewConnectivityGroup(sourceConnectivity)
if s.RecoverTrade {
go s.tradeRecover(ctx)
}
go s.accountUpdater(ctx)
go s.hedgeWorker(ctx)
go s.quoteWorker(ctx)
go func() {
select {
case <-ctx.Done():
case <-s.connectivityGroup.AllAuthedC(ctx, 15*time.Second):
}
s.logger.Infof("all user data streams are connected, starting workers...")
go s.accountUpdater(ctx)
go s.hedgeWorker(ctx)
go s.quoteWorker(ctx)
}()
bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) {
// the ctx here is the shutdown context (not the strategy context)

View File

@ -30,12 +30,23 @@ var _tickers = map[string]types.Ticker{
Buy: fixedpoint.NewFromFloat(2519.0),
Sell: fixedpoint.NewFromFloat(2521.0),
},
"USDTTWD": {
Time: time.Now(),
Volume: fixedpoint.Zero,
Open: fixedpoint.NewFromFloat(32.1),
High: fixedpoint.NewFromFloat(32.31),
Low: fixedpoint.NewFromFloat(32.01),
Last: fixedpoint.NewFromFloat(32.0),
Buy: fixedpoint.NewFromFloat(32.0),
Sell: fixedpoint.NewFromFloat(32.01),
},
}
func Ticker(symbol string) types.Ticker {
ticker, ok := _tickers[symbol]
if !ok {
panic(fmt.Errorf("%s test ticker not found, valid tickers: %+v", symbol, []string{"BTCUSDT", "ETHUSDT"}))
panic(fmt.Errorf("%s test ticker not found, valid tickers: %+v", symbol, []string{"BTCUSDT", "ETHUSDT", "USDTTWD"}))
}
return ticker