diff --git a/pkg/risk/account_value.go b/pkg/risk/account_value.go new file mode 100644 index 000000000..0a051f6e7 --- /dev/null +++ b/pkg/risk/account_value.go @@ -0,0 +1,193 @@ +package risk + +import ( + "context" + "fmt" + "time" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +var one = fixedpoint.One + +var maxLeverage = fixedpoint.NewFromInt(10) + +type AccountValueCalculator struct { + session *bbgo.ExchangeSession + quoteCurrency string + prices map[string]fixedpoint.Value + tickers map[string]types.Ticker + updateTime time.Time +} + +func NewAccountValueCalculator(session *bbgo.ExchangeSession, quoteCurrency string) *AccountValueCalculator { + return &AccountValueCalculator{ + session: session, + quoteCurrency: quoteCurrency, + prices: make(map[string]fixedpoint.Value), + tickers: make(map[string]types.Ticker), + } +} + +func (c *AccountValueCalculator) UpdatePrices(ctx context.Context) error { + balances := c.session.Account.Balances() + currencies := balances.Currencies() + var symbols []string + for _, currency := range currencies { + symbol := currency + c.quoteCurrency + 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 +} + +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 + } + } + + 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 +} + +func (c *AccountValueCalculator) NetValue(ctx context.Context) (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 { + symbol := b.Currency + c.quoteCurrency + price, ok := c.prices[symbol] + if !ok { + continue + } + + accountValue = accountValue.Add(b.Net().Mul(price)) + } + + return accountValue, nil +} + +func calculateAccountNetValue(session *bbgo.ExchangeSession) (fixedpoint.Value, error) { + accountValue := fixedpoint.Zero + ctx := context.Background() + c := NewAccountValueCalculator(session, "USDT") + if err := c.UpdatePrices(ctx); err != nil { + return accountValue, err + } + + return c.NetValue(ctx) +} + +func CalculateBaseQuantity(session *bbgo.ExchangeSession, market types.Market, price, quantity, leverage fixedpoint.Value) (fixedpoint.Value, error) { + if leverage.IsZero() { + leverage = fixedpoint.NewFromInt(3) + } + + usingLeverage := session.Margin || session.IsolatedMargin || session.Futures || session.IsolatedFutures + if usingLeverage { + if !quantity.IsZero() { + return quantity, nil + } + + // quantity is zero, we need to calculate the quantity + baseBalance, _ := session.Account.Balance(market.BaseCurrency) + quoteBalance, _ := session.Account.Balance(market.QuoteCurrency) + + logrus.Infof("calculating leveraged quantity: base balance = %+v, quote balance = %+v", baseBalance, quoteBalance) + + // calculate the quantity automatically + if session.Margin || session.IsolatedMargin { + baseBalanceValue := baseBalance.Net().Mul(price) + accountValue := baseBalanceValue.Add(quoteBalance.Net()) + + // avoid using all account value since there will be some trade loss for interests and the fee + accountValue = accountValue.Mul(one.Sub(fixedpoint.NewFromFloat(0.01))) + + logrus.Infof("calculated account value %f %s", accountValue.Float64(), market.QuoteCurrency) + + if session.IsolatedMargin { + originLeverage := leverage + leverage = fixedpoint.Min(leverage, maxLeverage) + logrus.Infof("using isolated margin, maxLeverage=10 originalLeverage=%f currentLeverage=%f", + originLeverage.Float64(), + leverage.Float64()) + } + + // spot margin use the equity value, so we use the total quote balance here + maxPosition := CalculateMaxPosition(price, accountValue, leverage) + debt := baseBalance.Debt() + + logrus.Infof("margin leverage: calculated maxPosition=%f debt=%f price=%f accountValue=%f %s leverage=%f", + maxPosition.Float64(), + debt.Float64(), + price.Float64(), + accountValue.Float64(), + market.QuoteCurrency, + leverage.Float64()) + + return maxPosition.Sub(debt), nil + } + + if session.Futures || session.IsolatedFutures { + // TODO: get mark price here + maxPositionQuantity := CalculateMaxPosition(price, quoteBalance.Available, leverage) + requiredPositionCost := CalculatePositionCost(price, price, maxPositionQuantity, leverage, types.SideTypeSell) + if quoteBalance.Available.Compare(requiredPositionCost) < 0 { + return maxPositionQuantity, fmt.Errorf("available margin %f %s is not enough, can not submit order", quoteBalance.Available.Float64(), market.QuoteCurrency) + } + + return maxPositionQuantity, nil + } + } + + // For spot, we simply sell the base quoteCurrency + balance, hasBalance := session.Account.Balance(market.BaseCurrency) + if hasBalance { + if quantity.IsZero() { + logrus.Warnf("sell quantity is not set, submitting sell with all base balance: %s", balance.Available.String()) + if !balance.Available.IsZero() { + return balance.Available, nil + } + } else { + return fixedpoint.Min(quantity, balance.Available), nil + } + } + + return quantity, fmt.Errorf("quantity is zero, can not submit sell order, please check your settings") +} diff --git a/pkg/strategy/pivotshort/breaklow.go b/pkg/strategy/pivotshort/breaklow.go index 9871718a1..d240cc330 100644 --- a/pkg/strategy/pivotshort/breaklow.go +++ b/pkg/strategy/pivotshort/breaklow.go @@ -2,8 +2,6 @@ package pivotshort import ( "context" - "fmt" - "time" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -108,7 +106,7 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener s.Quantity.Float64(), s.Leverage.Float64()) - quantity, err := useQuantityOrBaseBalance(s.session, s.Market, s.lastLow, s.Quantity, s.Leverage) + quantity, err := risk.CalculateBaseQuantity(s.session, s.Market, s.lastLow, s.Quantity, s.Leverage) if err != nil { log.WithError(err).Errorf("quantity calculation error") } @@ -203,7 +201,7 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener // graceful cancel all active orders _ = orderExecutor.GracefulCancel(ctx) - quantity, err := useQuantityOrBaseBalance(s.session, s.Market, closePrice, s.Quantity, s.Leverage) + quantity, err := risk.CalculateBaseQuantity(s.session, s.Market, closePrice, s.Quantity, s.Leverage) if err != nil { log.WithError(err).Errorf("quantity calculation error") } @@ -240,179 +238,3 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener })) } -type AccountValueCalculator struct { - session *bbgo.ExchangeSession - quoteCurrency string - prices map[string]fixedpoint.Value - tickers map[string]types.Ticker - updateTime time.Time -} - -func NewAccountValueCalculator(session *bbgo.ExchangeSession, quoteCurrency string) *AccountValueCalculator { - return &AccountValueCalculator{ - session: session, - quoteCurrency: quoteCurrency, - prices: make(map[string]fixedpoint.Value), - tickers: make(map[string]types.Ticker), - } -} - -func (c *AccountValueCalculator) UpdatePrices(ctx context.Context) error { - balances := c.session.Account.Balances() - currencies := balances.Currencies() - var symbols []string - for _, currency := range currencies { - symbol := currency + c.quoteCurrency - 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 -} - -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 - } - } - - 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 -} - -func (c *AccountValueCalculator) NetValue(ctx context.Context) (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 { - symbol := b.Currency + c.quoteCurrency - price, ok := c.prices[symbol] - if !ok { - continue - } - - accountValue = accountValue.Add(b.Net().Mul(price)) - } - - return accountValue, nil -} - -func calculateAccountNetValue(session *bbgo.ExchangeSession) (fixedpoint.Value, error) { - accountValue := fixedpoint.Zero - ctx := context.Background() - c := NewAccountValueCalculator(session, "USDT") - if err := c.UpdatePrices(ctx); err != nil { - return accountValue, err - } - - return c.NetValue(ctx) -} - -func useQuantityOrBaseBalance(session *bbgo.ExchangeSession, market types.Market, price, quantity, leverage fixedpoint.Value) (fixedpoint.Value, error) { - if leverage.IsZero() { - leverage = fixedpoint.NewFromInt(3) - } - - usingLeverage := session.Margin || session.IsolatedMargin || session.Futures || session.IsolatedFutures - if usingLeverage { - if !quantity.IsZero() { - return quantity, nil - } - - // quantity is zero, we need to calculate the quantity - baseBalance, _ := session.Account.Balance(market.BaseCurrency) - quoteBalance, _ := session.Account.Balance(market.QuoteCurrency) - - log.Infof("calculating quantity: base balance = %+v, quote balance = %+v", baseBalance, quoteBalance) - - // calculate the quantity automatically - if session.Margin || session.IsolatedMargin { - baseBalanceValue := baseBalance.Net().Mul(price) - accountValue := baseBalanceValue.Add(quoteBalance.Net()) - - // avoid using all account value since there will be some trade loss for interests and the fee - accountValue = accountValue.Mul(one.Sub(fixedpoint.NewFromFloat(0.01))) - - log.Infof("calculated account value %f %s", accountValue.Float64(), market.QuoteCurrency) - - if session.IsolatedMargin { - originLeverage := leverage - leverage = fixedpoint.Min(leverage, fixedpoint.NewFromInt(10)) // max leverage is 10 - log.Infof("using isolated margin, maxLeverage=10 originalLeverage=%f currentLeverage=%f", - originLeverage.Float64(), - leverage.Float64()) - } - - // spot margin use the equity value, so we use the total quote balance here - maxPosition := risk.CalculateMaxPosition(price, accountValue, leverage) - debt := baseBalance.Debt() - - log.Infof("margin leverage: calculated maxPosition=%f debt=%f price=%f accountValue=%f %s leverage=%f", - maxPosition.Float64(), - debt.Float64(), - price.Float64(), - accountValue.Float64(), - market.QuoteCurrency, - leverage.Float64()) - - return maxPosition.Sub(debt), nil - } - - if session.Futures || session.IsolatedFutures { - // TODO: get mark price here - maxPositionQuantity := risk.CalculateMaxPosition(price, quoteBalance.Available, leverage) - requiredPositionCost := risk.CalculatePositionCost(price, price, maxPositionQuantity, leverage, types.SideTypeSell) - if quoteBalance.Available.Compare(requiredPositionCost) < 0 { - return maxPositionQuantity, fmt.Errorf("available margin %f %s is not enough, can not submit order", quoteBalance.Available.Float64(), market.QuoteCurrency) - } - - return maxPositionQuantity, nil - } - - } - - // For spot, we simply sell the base quoteCurrency - balance, hasBalance := session.Account.Balance(market.BaseCurrency) - if hasBalance { - if quantity.IsZero() { - log.Warnf("sell quantity is not set, submitting sell with all base balance: %s", balance.Available.String()) - if !balance.Available.IsZero() { - return balance.Available, nil - } - } else { - return fixedpoint.Min(quantity, balance.Available), nil - } - } - - return quantity, fmt.Errorf("quantity is zero, can not submit sell order, please check your settings") -}