diff --git a/pkg/accounting/pnl/report.go b/pkg/accounting/pnl/report.go index 1c81524230..a2bb9aa0d0 100644 --- a/pkg/accounting/pnl/report.go +++ b/pkg/accounting/pnl/report.go @@ -9,6 +9,7 @@ import ( "github.com/slack-go/slack" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types/currency" "github.com/c9s/bbgo/pkg/slack/slackstyle" "github.com/c9s/bbgo/pkg/types" @@ -45,28 +46,28 @@ func (report AverageCostPnLReport) Print() { color.Green("TRADES SINCE: %v", report.StartTime) color.Green("NUMBER OF TRADES: %d", report.NumTrades) color.Green(report.Position.String()) - color.Green("AVERAGE COST: %s", types.USD.FormatMoney(report.AverageCost)) + color.Green("AVERAGE COST: %s", currency.USD.FormatMoney(report.AverageCost)) color.Green("BASE ASSET POSITION: %s", report.BaseAssetPosition.String()) color.Green("TOTAL BUY VOLUME: %v", report.BuyVolume) color.Green("TOTAL SELL VOLUME: %v", report.SellVolume) - color.Green("CURRENT PRICE: %s", types.USD.FormatMoney(report.LastPrice)) + color.Green("CURRENT PRICE: %s", currency.USD.FormatMoney(report.LastPrice)) color.Green("CURRENCY FEES:") for currency, fee := range report.CurrencyFees { color.Green(" - %s: %s", currency, fee.String()) } if report.Profit.Sign() > 0 { - color.Green("PROFIT: %s", types.USD.FormatMoney(report.Profit)) + color.Green("PROFIT: %s", currency.USD.FormatMoney(report.Profit)) } else { - color.Red("PROFIT: %s", types.USD.FormatMoney(report.Profit)) + color.Red("PROFIT: %s", currency.USD.FormatMoney(report.Profit)) } if report.UnrealizedProfit.Sign() > 0 { - color.Green("UNREALIZED PROFIT: %s", types.USD.FormatMoney(report.UnrealizedProfit)) + color.Green("UNREALIZED PROFIT: %s", currency.USD.FormatMoney(report.UnrealizedProfit)) } else { - color.Red("UNREALIZED PROFIT: %s", types.USD.FormatMoney(report.UnrealizedProfit)) + color.Red("UNREALIZED PROFIT: %s", currency.USD.FormatMoney(report.UnrealizedProfit)) } } @@ -79,13 +80,13 @@ func (report AverageCostPnLReport) SlackAttachment() slack.Attachment { return slack.Attachment{ Title: report.Symbol + " Profit and Loss report", - Text: "Profit " + types.USD.FormatMoney(report.Profit), + Text: "Profit " + currency.USD.FormatMoney(report.Profit), Color: color, // Pretext: "", // Text: "", Fields: []slack.AttachmentField{ - {Title: "Profit", Value: types.USD.FormatMoney(report.Profit)}, - {Title: "Unrealized Profit", Value: types.USD.FormatMoney(report.UnrealizedProfit)}, + {Title: "Profit", Value: currency.USD.FormatMoney(report.Profit)}, + {Title: "Unrealized Profit", Value: currency.USD.FormatMoney(report.UnrealizedProfit)}, {Title: "Current Price", Value: report.Market.FormatPrice(report.LastPrice), Short: true}, {Title: "Average Cost", Value: report.Market.FormatPrice(report.AverageCost), Short: true}, diff --git a/pkg/bbgo/account_value_calc.go b/pkg/bbgo/account_value_calc.go index 1fc4ee971e..2c4657fd50 100644 --- a/pkg/bbgo/account_value_calc.go +++ b/pkg/bbgo/account_value_calc.go @@ -11,6 +11,7 @@ import ( "github.com/c9s/bbgo/pkg/pricesolver" "github.com/c9s/bbgo/pkg/risk" "github.com/c9s/bbgo/pkg/types" + currency2 "github.com/c9s/bbgo/pkg/types/currency" ) var defaultLeverage = fixedpoint.NewFromInt(3) @@ -159,7 +160,7 @@ func aggregateUsdNetValue(balances types.BalanceMap) fixedpoint.Value { totalUsdValue := fixedpoint.Zero // get all usd value if any for currency, balance := range balances { - if types.IsUSDFiatCurrency(currency) { + if currency2.IsUSDFiatCurrency(currency) { totalUsdValue = totalUsdValue.Add(balance.Net()) } } @@ -171,7 +172,7 @@ func usdFiatBalances(balances types.BalanceMap) (fiats types.BalanceMap, rest ty rest = make(types.BalanceMap) fiats = make(types.BalanceMap) for currency, balance := range balances { - if types.IsUSDFiatCurrency(currency) { + if currency2.IsUSDFiatCurrency(currency) { fiats[currency] = balance } else { rest[currency] = balance @@ -214,7 +215,7 @@ func CalculateBaseQuantity( // for isolated margin, we can calculate from these two pair totalUsdValue := fixedpoint.Zero - if len(restBalances) == 1 && types.IsUSDFiatCurrency(market.QuoteCurrency) { + if len(restBalances) == 1 && currency2.IsUSDFiatCurrency(market.QuoteCurrency) { totalUsdValue = aggregateUsdNetValue(balances) } else if len(restBalances) > 1 { priceSolver := pricesolver.NewSimplePriceResolver(session.Markets()) diff --git a/pkg/bbgo/environment.go b/pkg/bbgo/environment.go index 4d0eef08d3..c495860547 100644 --- a/pkg/bbgo/environment.go +++ b/pkg/bbgo/environment.go @@ -29,6 +29,7 @@ import ( googleservice "github.com/c9s/bbgo/pkg/service/google" "github.com/c9s/bbgo/pkg/slack/slacklog" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/types/asset" "github.com/c9s/bbgo/pkg/util" ) @@ -542,7 +543,7 @@ func (environ *Environment) Sync(ctx context.Context, userConfig ...*Config) err return nil } -func (environ *Environment) RecordAsset(t time.Time, session *ExchangeSession, assets types.AssetMap) { +func (environ *Environment) RecordAsset(t time.Time, session *ExchangeSession, assets asset.Map) { // skip for back-test if environ.BacktestService != nil { return diff --git a/pkg/bbgo/order_execution.go b/pkg/bbgo/order_execution.go index 2d52457d75..b9df070545 100644 --- a/pkg/bbgo/order_execution.go +++ b/pkg/bbgo/order_execution.go @@ -14,6 +14,7 @@ import ( "github.com/c9s/bbgo/pkg/envvar" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/types/currency" ) var DefaultSubmitOrderRetryTimeout = 5 * time.Minute @@ -176,8 +177,8 @@ func (c *BasicRiskController) ProcessOrders( if quoteBalance.Available.Compare(c.MinQuoteBalance) < 0 { addError(errors.Wrapf(ErrQuoteBalanceLevelTooLow, "can not place buy order, quote balance level is too low: %s < %s, order: %s", - types.USD.FormatMoney(quoteBalance.Available), - types.USD.FormatMoney(c.MinQuoteBalance), order.String())) + currency.USD.FormatMoney(quoteBalance.Available), + currency.USD.FormatMoney(c.MinQuoteBalance), order.String())) continue } diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index edf49332ad..ce4d6a8f4a 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -19,6 +19,7 @@ import ( "github.com/c9s/bbgo/pkg/exchange/retry" "github.com/c9s/bbgo/pkg/metrics" "github.com/c9s/bbgo/pkg/pricesolver" + currency2 "github.com/c9s/bbgo/pkg/types/currency" "github.com/c9s/bbgo/pkg/util/templateutil" exchange2 "github.com/c9s/bbgo/pkg/exchange" @@ -202,6 +203,14 @@ func NewExchangeSession(name string, exchange types.Exchange) *ExchangeSession { return session } +func (session *ExchangeSession) GetPriceSolver() *pricesolver.SimplePriceSolver { + if session.priceSolver == nil { + session.priceSolver = pricesolver.NewSimplePriceResolver(session.markets) + } + + return session.priceSolver +} + func (session *ExchangeSession) GetAccountLabel() string { var label string @@ -764,16 +773,12 @@ func (session *ExchangeSession) FormatOrder(order types.SubmitOrder) (types.Subm } func (session *ExchangeSession) UpdatePrices(ctx context.Context, currencies []string, fiat string) (err error) { - // TODO: move this cache check to the http routes - // if session.lastPriceUpdatedAt.After(time.Now().Add(-time.Hour)) { - // return nil - // } - markets := session.Markets() - var symbols []string + symbols := make([]string, 0, 50) for _, c := range currencies { - possibleSymbols := findPossibleMarketSymbols(markets, c, fiat) - symbols = append(symbols, possibleSymbols...) + for symbol := range markets.FindAssetMarkets(c) { + symbols = append(symbols, symbol) + } } if len(symbols) == 0 { @@ -785,15 +790,20 @@ func (session *ExchangeSession) UpdatePrices(ctx context.Context, currencies []s return err } + priceSolver := session.GetPriceSolver() + var lastTime time.Time for k, v := range tickers { + validPrice := v.GetValidPrice() + priceSolver.Update(k, validPrice) + // for {Crypto}/USDT markets // map things like BTCUSDT = {price} if market, ok := markets[k]; ok { - if types.IsFiatCurrency(market.BaseCurrency) { - session.lastPrices[k] = v.Last.Div(fixedpoint.One) + if currency2.IsFiatCurrency(market.BaseCurrency) { + session.lastPrices[k] = validPrice.Div(fixedpoint.One) } else { - session.lastPrices[k] = v.Last + session.lastPrices[k] = validPrice } } else { session.lastPrices[k] = v.Last @@ -819,7 +829,7 @@ func (session *ExchangeSession) FindPossibleAssetSymbols() (symbols []string, er var balances = session.GetAccount().Balances() var fiatAssets []string - for _, currency := range types.FiatCurrencies { + for _, currency := range currency2.FiatCurrencies { if balance, ok := balances[currency]; ok && balance.Total().Sign() > 0 { fiatAssets = append(fiatAssets, currency) } @@ -1115,24 +1125,3 @@ func (session *ExchangeSession) FormatOrders(orders []types.SubmitOrder) (format return formattedOrders, err } - -func findPossibleMarketSymbols(markets types.MarketMap, c, fiat string) (symbols []string) { - var tries []string - // expand USD stable coin currencies - if types.IsUSDFiatCurrency(fiat) { - for _, usdFiat := range types.USDFiatCurrencies { - tries = append(tries, c+usdFiat, usdFiat+c) - } - } else { - tries = []string{c + fiat, fiat + c} - } - - for _, try := range tries { - if markets.Has(try) { - symbols = append(symbols, try) - break - } - } - - return symbols -} diff --git a/pkg/bbgo/session_test.go b/pkg/bbgo/session_test.go index f4ab11f479..f30d11b655 100644 --- a/pkg/bbgo/session_test.go +++ b/pkg/bbgo/session_test.go @@ -1,44 +1 @@ package bbgo - -import ( - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/c9s/bbgo/pkg/types" -) - -func Test_findPossibleMarketSymbols(t *testing.T) { - t.Run("btcusdt", func(t *testing.T) { - markets := types.MarketMap{ - "BTCUSDT": types.Market{}, - "BTCUSDC": types.Market{}, - "BTCUSD": types.Market{}, - "BTCBUSD": types.Market{}, - } - symbols := findPossibleMarketSymbols(markets, "BTC", "USDT") - if assert.Len(t, symbols, 1) { - assert.Equal(t, "BTCUSDT", symbols[0]) - } - }) - - t.Run("btcusd only", func(t *testing.T) { - markets := types.MarketMap{ - "BTCUSD": types.Market{}, - } - symbols := findPossibleMarketSymbols(markets, "BTC", "USDT") - if assert.Len(t, symbols, 1) { - assert.Equal(t, "BTCUSD", symbols[0]) - } - }) - - t.Run("usd to stable coin", func(t *testing.T) { - markets := types.MarketMap{ - "BTCUSDT": types.Market{}, - } - symbols := findPossibleMarketSymbols(markets, "BTC", "USD") - if assert.Len(t, symbols, 1) { - assert.Equal(t, "BTCUSDT", symbols[0]) - } - }) -} diff --git a/pkg/notifier/slacknotifier/slack.go b/pkg/notifier/slacknotifier/slack.go index 7d62a7ee85..e836685164 100644 --- a/pkg/notifier/slacknotifier/slack.go +++ b/pkg/notifier/slacknotifier/slack.go @@ -31,6 +31,14 @@ var emailRegExp = regexp.MustCompile("`^(?P[a-zA-Z0-9.!#$%&'*+/=?^_ \\x60{ var typeNamePrefixRE = regexp.MustCompile(`^\*?([a-zA-Z0-9_]+\.)?`) +type SlackAttachmentCreator interface { + SlackAttachment() slack.Attachment +} + +type SlackBlocksCreator interface { + SlackBlocks() slack.Blocks +} + type notifyTask struct { channel string @@ -297,7 +305,7 @@ func (n *Notifier) PostLiveNote(obj livenote.Object, opts ...livenote.Option) er } var attachment slack.Attachment - if creator, ok := note.Object.(types.SlackAttachmentCreator); ok { + if creator, ok := note.Object.(SlackAttachmentCreator); ok { attachment = creator.SlackAttachment() } else { return fmt.Errorf("livenote object does not support types.SlackAttachmentCreator interface") @@ -422,7 +430,7 @@ func filterSlackAttachments(args []interface{}) (slackAttachments []slack.Attach slackAttachments = append(slackAttachments, *a) - case types.SlackAttachmentCreator: + case SlackAttachmentCreator: if firstAttachmentOffset == -1 { firstAttachmentOffset = idx } @@ -468,7 +476,10 @@ func (n *Notifier) NotifyTo(channel string, obj interface{}, args ...interface{} case slack.Attachment: opts = append(opts, slack.MsgOptionAttachments(append([]slack.Attachment{a}, slackAttachments...)...)) - case types.SlackAttachmentCreator: + case *slack.Attachment: + opts = append(opts, slack.MsgOptionAttachments(append([]slack.Attachment{*a}, slackAttachments...)...)) + + case SlackAttachmentCreator: // convert object to slack attachment (if supported) opts = append(opts, slack.MsgOptionAttachments(append([]slack.Attachment{a.SlackAttachment()}, slackAttachments...)...)) diff --git a/pkg/server/routes.go b/pkg/server/routes.go index ef43eee712..3f9a10fff8 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -20,6 +20,7 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/types/asset" ) const DefaultBindAddress = "localhost:8080" @@ -408,9 +409,9 @@ func (s *Server) listSessionOpenOrders(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"orders": nil}) } -func genFakeAssets() types.AssetMap { +func genFakeAssets() asset.Map { - totalAssets := types.AssetMap{} + totalAssets := asset.Map{} balances := types.BalanceMap{ "BTC": types.Balance{Currency: "BTC", Available: fixedpoint.NewFromFloat(10.0 * rand.Float64())}, "BCH": types.Balance{Currency: "BCH", Available: fixedpoint.NewFromFloat(0.01 * rand.Float64())}, @@ -447,7 +448,7 @@ func (s *Server) listAssets(c *gin.Context) { return } - totalAssets := types.AssetMap{} + totalAssets := asset.Map{} for _, session := range s.Environ.Sessions() { balances := session.GetAccount().Balances() diff --git a/pkg/service/account.go b/pkg/service/account.go index d924932660..917d13bea0 100644 --- a/pkg/service/account.go +++ b/pkg/service/account.go @@ -1,10 +1,13 @@ package service import ( - "github.com/c9s/bbgo/pkg/types" + "time" + "github.com/jmoiron/sqlx" "go.uber.org/multierr" - "time" + + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/types/asset" ) type AccountService struct { @@ -16,7 +19,10 @@ func NewAccountService(db *sqlx.DB) *AccountService { } // TODO: should pass bbgo.ExchangeSession to this function, but that might cause cyclic import -func (s *AccountService) InsertAsset(time time.Time, session string, name types.ExchangeName, account string, isMargin bool, isIsolatedMargin bool, isolatedMarginSymbol string, assets types.AssetMap) error { +func (s *AccountService) InsertAsset( + time time.Time, session string, name types.ExchangeName, account string, isMargin bool, isIsolatedMargin bool, + isolatedMarginSymbol string, assets asset.Map, +) error { if s.DB == nil { // skip db insert when no db connection setting. return nil @@ -46,8 +52,8 @@ func (s *AccountService) InsertAsset(time time.Time, session string, name types. account, time, v.Currency, - v.InUSD, - v.InBTC, + v.NetAssetInUSD, + v.NetAssetInBTC, v.Total, v.Available, v.Locked, diff --git a/pkg/service/account_test.go b/pkg/service/account_test.go index 89c0fa98cf..8c8f4e99ac 100644 --- a/pkg/service/account_test.go +++ b/pkg/service/account_test.go @@ -9,6 +9,7 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/types/asset" ) func TestAccountService(t *testing.T) { @@ -23,18 +24,18 @@ func TestAccountService(t *testing.T) { service := &AccountService{DB: xdb} t1 := time.Now() - err = service.InsertAsset(t1, "binance", types.ExchangeBinance, "main", false, false, "", types.AssetMap{ - "BTC": types.Asset{ - Currency: "BTC", - Total: fixedpoint.MustNewFromString("1.0"), - InUSD: fixedpoint.MustNewFromString("10.0"), - InBTC: fixedpoint.MustNewFromString("0.0001"), - Time: t1, - Locked: fixedpoint.MustNewFromString("0"), - Available: fixedpoint.MustNewFromString("1.0"), - Borrowed: fixedpoint.MustNewFromString("0"), - NetAsset: fixedpoint.MustNewFromString("1"), - PriceInUSD: fixedpoint.MustNewFromString("44870"), + err = service.InsertAsset(t1, "binance", types.ExchangeBinance, "main", false, false, "", asset.Map{ + "BTC": asset.Asset{ + Currency: "BTC", + Total: fixedpoint.MustNewFromString("1.0"), + NetAssetInUSD: fixedpoint.MustNewFromString("10.0"), + NetAssetInBTC: fixedpoint.MustNewFromString("0.0001"), + Time: t1, + Locked: fixedpoint.MustNewFromString("0"), + Available: fixedpoint.MustNewFromString("1.0"), + Borrowed: fixedpoint.MustNewFromString("0"), + NetAsset: fixedpoint.MustNewFromString("1"), + PriceInUSD: fixedpoint.MustNewFromString("44870"), }, }) assert.NoError(t, err) diff --git a/pkg/strategy/autoborrow/strategy.go b/pkg/strategy/autoborrow/strategy.go index 19a54f4939..f610d84d54 100644 --- a/pkg/strategy/autoborrow/strategy.go +++ b/pkg/strategy/autoborrow/strategy.go @@ -16,6 +16,7 @@ import ( "github.com/c9s/bbgo/pkg/pricesolver" "github.com/c9s/bbgo/pkg/slack/slackalert" "github.com/c9s/bbgo/pkg/types" + currency2 "github.com/c9s/bbgo/pkg/types/currency" ) const ID = "autoborrow" @@ -629,7 +630,7 @@ func (s *Strategy) marginAlertWorker(ctx context.Context, alertInterval time.Dur totalDebtValueInUSDT := fixedpoint.Zero debts := account.Balances().Debts() for currency, bal := range debts { - price, ok := s.priceSolver.ResolvePrice(currency, types.USDT) + price, ok := s.priceSolver.ResolvePrice(currency, currency2.USDT) if !ok { log.Warnf("unable to resolve price for %s", currency) continue diff --git a/pkg/strategy/autobuy/strategy.go b/pkg/strategy/autobuy/strategy.go index 9fa59c33dd..bf76083c5b 100644 --- a/pkg/strategy/autobuy/strategy.go +++ b/pkg/strategy/autobuy/strategy.go @@ -129,9 +129,13 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. }) s.cron = cron.New() - s.cron.AddFunc(s.Schedule, func() { + _, err := s.cron.AddFunc(s.Schedule, func() { s.autobuy(ctx) }) + if err != nil { + return err + } + s.cron.Start() return nil @@ -146,16 +150,14 @@ func (s *Strategy) cancelOrders(ctx context.Context) { func (s *Strategy) autobuy(ctx context.Context) { s.cancelOrders(ctx) - baseBalance, ok := s.Session.GetAccount().Balance(s.Market.BaseCurrency) - if !ok { - log.Errorf("%s balance not found", s.Market.BaseCurrency) - return - } + account := s.Session.GetAccount() + baseBalance, _ := account.Balance(s.Market.BaseCurrency) + log.Infof("balance: %s", baseBalance.String()) - quoteBalance, ok := s.Session.GetAccount().Balance(s.Market.QuoteCurrency) + quoteBalance, ok := account.Balance(s.Market.QuoteCurrency) if !ok { - log.Errorf("%s balance not found", s.Market.QuoteCurrency) + log.Errorf("quote balance is zero") return } diff --git a/pkg/strategy/tri/_doc.go b/pkg/strategy/tri/_doc.go new file mode 100644 index 0000000000..16741181c0 --- /dev/null +++ b/pkg/strategy/tri/_doc.go @@ -0,0 +1,4 @@ +/* +tri is an example strategy that demonstrates how to implement an arbitrage strategy. +*/ +package tri diff --git a/pkg/strategy/tri/position.go b/pkg/strategy/tri/position.go index 5bbbe815e8..5efa49b6c9 100644 --- a/pkg/strategy/tri/position.go +++ b/pkg/strategy/tri/position.go @@ -5,6 +5,7 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/types/currency" ) type MultiCurrencyPosition struct { @@ -51,9 +52,9 @@ func (p *MultiCurrencyPosition) handleTrade(trade types.Trade) { p.Currencies[market.QuoteCurrency] = p.Currencies[market.QuoteCurrency].Add(trade.QuoteQuantity) } - if types.IsUSDFiatCurrency(market.QuoteCurrency) { + if currency.IsUSDFiatCurrency(market.QuoteCurrency) { p.TradePrices[market.BaseCurrency] = trade.Price - } else if types.IsUSDFiatCurrency(market.BaseCurrency) { // For USDT/TWD pair, convert USDT/TWD price to TWD/USDT + } else if currency.IsUSDFiatCurrency(market.BaseCurrency) { // For USDT/TWD pair, convert USDT/TWD price to TWD/USDT p.TradePrices[market.QuoteCurrency] = one.Div(trade.Price) } @@ -68,29 +69,29 @@ func (p *MultiCurrencyPosition) handleTrade(trade types.Trade) { func (p *MultiCurrencyPosition) CollectProfits() []Profit { var profits []Profit - for currency, base := range p.Currencies { + for cu, base := range p.Currencies { if base.IsZero() { continue } profit := Profit{ - Asset: currency, + Asset: cu, Profit: base, ProfitInUSD: fixedpoint.Zero, } - if price, ok := p.TradePrices[currency]; ok && !price.IsZero() { + if price, ok := p.TradePrices[cu]; ok && !price.IsZero() { profit.ProfitInUSD = base.Mul(price) - } else if types.IsUSDFiatCurrency(currency) { + } else if currency.IsUSDFiatCurrency(cu) { profit.ProfitInUSD = base } profits = append(profits, profit) - if total, ok := p.TotalProfits[currency]; ok { - p.TotalProfits[currency] = total.Add(base) + if total, ok := p.TotalProfits[cu]; ok { + p.TotalProfits[cu] = total.Add(base) } else { - p.TotalProfits[currency] = base + p.TotalProfits[cu] = base } } @@ -99,40 +100,40 @@ func (p *MultiCurrencyPosition) CollectProfits() []Profit { } func (p *MultiCurrencyPosition) Reset() { - for currency := range p.Currencies { - p.Currencies[currency] = fixedpoint.Zero + for cu := range p.Currencies { + p.Currencies[cu] = fixedpoint.Zero } } func (p *MultiCurrencyPosition) String() (o string) { o += "position: \n" - for currency, base := range p.Currencies { + for cu, base := range p.Currencies { if base.IsZero() { continue } - o += fmt.Sprintf("- %s: %f\n", currency, base.Float64()) + o += fmt.Sprintf("- %s: %f\n", cu, base.Float64()) } o += "totalProfits: \n" - for currency, total := range p.TotalProfits { + for cu, total := range p.TotalProfits { if total.IsZero() { continue } - o += fmt.Sprintf("- %s: %f\n", currency, total.Float64()) + o += fmt.Sprintf("- %s: %f\n", cu, total.Float64()) } o += "fees: \n" - for currency, fee := range p.Fees { + for cu, fee := range p.Fees { if fee.IsZero() { continue } - o += fmt.Sprintf("- %s: %f\n", currency, fee.Float64()) + o += fmt.Sprintf("- %s: %f\n", cu, fee.Float64()) } return o diff --git a/pkg/strategy/xdepthmaker/strategy_test.go b/pkg/strategy/xdepthmaker/strategy_test.go index 9880d75c4a..875c70b592 100644 --- a/pkg/strategy/xdepthmaker/strategy_test.go +++ b/pkg/strategy/xdepthmaker/strategy_test.go @@ -68,11 +68,11 @@ func TestStrategy_generateMakerOrders(t *testing.T) { assert.NoError(t, err) AssertOrdersPriceSideQuantity(t, []PriceSideQuantityAssert{ {Side: types.SideTypeBuy, Price: Number("25000"), Quantity: Number("0.04")}, // =~ $1000.00 - {Side: types.SideTypeBuy, Price: Number("24866.66"), Quantity: Number("0.281715")}, // =~ $7005.3111219, accumulated amount =~ $1000.00 + $7005.3111219 = $8005.3111219 - {Side: types.SideTypeBuy, Price: Number("24800"), Quantity: Number("0.283123")}, // =~ $7021.4504, accumulated amount =~ $1000.00 + $7005.3111219 + $7021.4504 = $8005.3111219 + $7021.4504 =~ $15026.7615219 - {Side: types.SideTypeSell, Price: Number("25100"), Quantity: Number("0.03984")}, - {Side: types.SideTypeSell, Price: Number("25233.34"), Quantity: Number("0.2772")}, - {Side: types.SideTypeSell, Price: Number("25300"), Quantity: Number("0.275845")}, + {Side: types.SideTypeBuy, Price: Number("24910.71"), Quantity: Number("0.281147")}, // =~ $7005.3111219, accumulated amount =~ $1000.00 + $7005.3111219 = $8005.3111219 + {Side: types.SideTypeBuy, Price: Number("24803.34"), Quantity: Number("0.28361")}, // =~ $7021.4504, accumulated amount =~ $1000.00 + $7005.3111219 + $7021.4504 = $8005.3111219 + $7021.4504 =~ $15026.7615219 + {Side: types.SideTypeSell, Price: Number("25100.01"), Quantity: Number("0.03984")}, + {Side: types.SideTypeSell, Price: Number("25188.06"), Quantity: Number("0.27777")}, + {Side: types.SideTypeSell, Price: Number("25294.61"), Quantity: Number("0.275401")}, }, orders) } @@ -112,11 +112,11 @@ func TestPriceVolumeSlice_AverageDepthPrice(t *testing.T) { t.Run("test average price by quote quantity", func(t *testing.T) { // Test buying with ~119637.9398 quote buyPrice := book.Asks.AverageDepthPriceByQuote(fixedpoint.NewFromFloat(119637.9398), 0) - assert.InDelta(t, 59899.6009, buyPrice.Float64(), 0.001) + assert.InDelta(t, 59818.9699, buyPrice.Float64(), 0.001) // Test selling with ~118238.219281 quote sellPrice := book.Bids.AverageDepthPriceByQuote(fixedpoint.NewFromFloat(118238.219281), 0) - assert.InDelta(t, 59066.2024, sellPrice.Float64(), 0.001) + assert.InDelta(t, 59119.10993609, sellPrice.Float64(), 0.001) assert.Less(t, sellPrice.Float64(), buyPrice.Float64(), "the sell price should be lower than the buy price") }) diff --git a/pkg/strategy/xmaker/strategy_test.go b/pkg/strategy/xmaker/strategy_test.go index 02e9624486..0a067debf8 100644 --- a/pkg/strategy/xmaker/strategy_test.go +++ b/pkg/strategy/xmaker/strategy_test.go @@ -14,6 +14,7 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/pricesolver" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/types/currency" . "github.com/c9s/bbgo/pkg/testing/testhelper" ) @@ -51,7 +52,7 @@ func TestStrategy_allowMarginHedge(t *testing.T) { Account: account, } - accountValueCalc := bbgo.NewAccountValueCalculator(session, priceSolver, types.USDT) + accountValueCalc := bbgo.NewAccountValueCalculator(session, priceSolver, currency.USDT) assert.Equal(t, "98000", accountValueCalc.DebtValue().String()) assert.Equal(t, "298000", accountValueCalc.NetValue().String()) @@ -98,7 +99,7 @@ func TestStrategy_allowMarginHedge(t *testing.T) { Account: account, } - accountValueCalc := bbgo.NewAccountValueCalculator(session, priceSolver, types.USDT) + accountValueCalc := bbgo.NewAccountValueCalculator(session, priceSolver, currency.USDT) assert.Equal(t, "392000", accountValueCalc.DebtValue().String()) assert.Equal(t, "4000", accountValueCalc.NetValue().String()) diff --git a/pkg/strategy/xnav/assetmap.go b/pkg/strategy/xnav/assetmap.go new file mode 100644 index 0000000000..fa84c647ad --- /dev/null +++ b/pkg/strategy/xnav/assetmap.go @@ -0,0 +1,61 @@ +package xnav + +import ( + "time" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/pricesolver" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/types/asset" +) + +func NewAssetMapFromBalanceMap( + priceSolver *pricesolver.SimplePriceSolver, priceTime time.Time, m types.BalanceMap, fiat string, +) asset.Map { + assets := make(asset.Map) + + btcInUSD, hasBtcPrice := priceSolver.ResolvePrice("BTC", fiat, "USDT") + if !hasBtcPrice { + logrus.Warnf("AssetMap: unable to resolve price for BTC") + } + + for cu, b := range m { + total := b.Total() + netAsset := b.Net() + debt := b.Debt() + + if total.IsZero() && netAsset.IsZero() && debt.IsZero() { + continue + } + + asset := asset.Asset{ + Currency: cu, + Total: total, + Time: priceTime, + Locked: b.Locked, + Available: b.Available, + Borrowed: b.Borrowed, + Interest: b.Interest, + NetAsset: netAsset, + Debt: debt, + } + + if assetPrice, ok := priceSolver.ResolvePrice(cu, fiat, "USDT"); ok { + asset.PriceInUSD = assetPrice + asset.NetAssetInUSD = netAsset.Mul(assetPrice) + if hasBtcPrice { + asset.NetAssetInBTC = asset.NetAssetInUSD.Div(btcInUSD) + } + + asset.DebtInUSD = debt.Mul(assetPrice) + asset.InterestInUSD = b.Interest.Mul(assetPrice) + } else { + logrus.Warnf("AssetMap: unable to resolve price for %s", cu) + } + + assets[cu] = asset + } + + return assets +} diff --git a/pkg/strategy/xnav/strategy.go b/pkg/strategy/xnav/strategy.go index ee47e1fc52..ae82b8a2fe 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -3,12 +3,15 @@ package xnav import ( "context" "fmt" + "sort" "sync" "time" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/types/asset" + "github.com/c9s/bbgo/pkg/types/currency" "github.com/c9s/bbgo/pkg/util/templateutil" "github.com/c9s/bbgo/pkg/util/timejitter" @@ -25,6 +28,63 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } +type AllAssetSnapshot struct { + SessionAssets map[string]asset.Map + TotalAssets asset.Map + Time time.Time +} + +type DebtAssetMap asset.Map + +func (m DebtAssetMap) SlackAttachment() slack.Attachment { + var fields []slack.AttachmentField + var netAssetInUSD, debtInUSD fixedpoint.Value + + var assets = asset.Map(m).Slice() + + // sort assets + sort.Slice(assets, func(i, j int) bool { + return assets[i].DebtInUSD.Compare(assets[j].DebtInUSD) > 0 + }) + + for _, a := range assets { + debtInUSD = debtInUSD.Add(a.DebtInUSD) + netAssetInUSD = netAssetInUSD.Add(a.NetAssetInUSD) + } + + for _, a := range assets { + if a.DebtInUSD.IsZero() { + continue + } + + text := fmt.Sprintf("%s (≈ %s) (≈ %s)", + a.Debt.String(), + currency.USD.FormatMoney(a.DebtInUSD), + a.DebtInUSD.Div(netAssetInUSD).FormatPercentage(2), + ) + + if !a.Borrowed.IsZero() { + text += fmt.Sprintf(" Principle: %s (≈ %s)", a.Borrowed.String(), currency.USD.FormatMoney(a.Borrowed.Mul(a.PriceInUSD))) + } + + if !a.Interest.IsZero() { + text += fmt.Sprintf(" Interest: %s (≈ %s)", a.Interest.String(), currency.USD.FormatMoney(a.InterestInUSD)) + } + + fields = append(fields, slack.AttachmentField{ + Title: a.Currency, + Value: text, + }) + } + + return slack.Attachment{ + Title: fmt.Sprintf("Debt Overview %s", + currency.USD.FormatMoney(debtInUSD), + ), + Fields: fields, + } +} + type State struct { Since int64 `json:"since"` } @@ -63,6 +123,11 @@ type Strategy struct { ReportOnStart bool `json:"reportOnStart"` IgnoreDusts bool `json:"ignoreDusts"` + ShowBreakdown bool `json:"showBreakdown"` + ShowDebtDetails bool `json:"showDebtDetails"` + + lastAllAssetSnapshot *AllAssetSnapshot + State *State `persistence:"state"` cron *cron.Cron @@ -88,14 +153,16 @@ var ten = fixedpoint.NewFromInt(10) func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) {} func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string]*bbgo.ExchangeSession) { - totalBalances := types.BalanceMap{} - allPrices := map[string]fixedpoint.Value{} - sessionBalances := map[string]types.BalanceMap{} + log.Infof("recording net asset value...") + priceTime := time.Now() + sessionAssets := map[string]asset.Map{} // iterate the sessions and record them quoteCurrency := "USDT" for sessionName, session := range sessions { + log.Infof("recording net asset value for session %s...", sessionName) + if session.PublicOnly { log.Infof("session %s is public only, skip", sessionName) continue @@ -108,41 +175,76 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] } account := session.GetAccount() - balances := account.Balances() + balances := account.Balances().NotZero() if err := session.UpdatePrices(ctx, balances.Currencies(), quoteCurrency); err != nil { log.WithError(err).Error("price update failed") return } - sessionBalances[sessionName] = balances - totalBalances = totalBalances.Add(balances) - - prices := session.LastPrices() - assets := balances.Assets(prices, priceTime) + assets := NewAssetMapFromBalanceMap(session.GetPriceSolver(), priceTime, balances, quoteCurrency) + s.Environment.RecordAsset(priceTime, session, assets) - // merge prices - for m, p := range prices { - allPrices[m] = p + for _, as := range assets { + log.WithFields(logrus.Fields{ + "session": sessionName, + "exchange": session.ExchangeName, + }).Infof("session %s %s asset = net:%s available:%s", + sessionName, + as.Currency, + as.NetAsset.String(), + as.Available.String()) } - s.Environment.RecordAsset(priceTime, session, assets) + sessionAssets[sessionName] = assets + + } + + totalAssets := asset.Map{} + for _, assets := range sessionAssets { + totalAssets = totalAssets.Merge(assets) } - displayAssets := types.AssetMap{} - totalAssets := totalBalances.Assets(allPrices, priceTime) s.Environment.RecordAsset(priceTime, &bbgo.ExchangeSession{Name: "ALL"}, totalAssets) - for currency, asset := range totalAssets { - // calculated if it's dust only when InUSD (usd value) is defined. - if s.IgnoreDusts && !asset.InUSD.IsZero() && asset.InUSD.Compare(ten) < 0 && asset.InUSD.Compare(ten.Neg()) > 0 { - continue + displayAssets := totalAssets.Filter(func(asset *asset.Asset) bool { + if s.IgnoreDusts && asset.NetAssetInUSD.Abs().Compare(ten) < 0 && asset.DebtInUSD.Abs().Compare(ten) < 0 { + return false } - displayAssets[currency] = asset - } + return true + }) bbgo.Notify(displayAssets) + if s.ShowBreakdown && len(sessionAssets) > 1 { + for sessionName, assets := range sessionAssets { + slackAttachment := assets.SlackAttachment() + slackAttachment.Title = "Session " + sessionName + " " + slackAttachment.Title + bbgo.Notify(slackAttachment) + } + } + + if s.ShowDebtDetails { + debtAssets := DebtAssetMap(displayAssets.Filter(func(asset *asset.Asset) bool { + return asset.DebtInUSD.Compare(ten) > 0 + })) + + if len(debtAssets) > 0 { + bbgo.Notify(debtAssets) + } + } + + allAssetSnapshot := &AllAssetSnapshot{ + SessionAssets: sessionAssets, + TotalAssets: totalAssets, + Time: priceTime, + } + + if s.lastAllAssetSnapshot != nil { + // TODO: compare the last snapshot with the current snapshot + } + s.lastAllAssetSnapshot = allAssetSnapshot + if s.State != nil { if s.State.IsOver24Hours() { s.State.Reset() @@ -151,6 +253,21 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] } } +func (s *Strategy) worker(ctx context.Context, sessions map[string]*bbgo.ExchangeSession, interval time.Duration) { + ticker := time.NewTicker(timejitter.Milliseconds(interval, 1000)) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + + case <-ticker.C: + s.recordNetAssetValue(ctx, sessions) + } + } +} + func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error { if s.State == nil { s.State = &State{} @@ -169,24 +286,11 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se if s.Environment.BacktestService != nil { log.Warnf("xnav does not support backtesting") + return nil } if s.Interval != "" { - go func() { - ticker := time.NewTicker(timejitter.Milliseconds(s.Interval.Duration(), 1000)) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - - case <-ticker.C: - s.recordNetAssetValue(ctx, sessions) - } - } - }() - + go s.worker(ctx, sessions, s.Interval.Duration()) } else if s.Schedule != "" { s.cron = cron.New() _, err := s.cron.AddFunc(s.Schedule, func() { diff --git a/pkg/types/account.go b/pkg/types/account.go index c681924ca4..26b938407e 100644 --- a/pkg/types/account.go +++ b/pkg/types/account.go @@ -137,6 +137,10 @@ func (a *Account) Balance(currency string) (balance Balance, ok bool) { a.Lock() balance, ok = a.balances[currency] a.Unlock() + if !ok { + balance = NewZeroBalance(currency) + } + return balance, ok } diff --git a/pkg/types/asset.go b/pkg/types/asset.go index c4865838be..ab1254f4c2 100644 --- a/pkg/types/asset.go +++ b/pkg/types/asset.go @@ -1,148 +1 @@ package types - -import ( - "fmt" - "sort" - "time" - - "github.com/slack-go/slack" - - "github.com/c9s/bbgo/pkg/fixedpoint" -) - -type Asset struct { - Currency string `json:"currency" db:"currency"` - - Total fixedpoint.Value `json:"total" db:"total"` - - NetAsset fixedpoint.Value `json:"netAsset" db:"net_asset"` - - Interest fixedpoint.Value `json:"interest" db:"interest"` - - // InUSD is net asset in USD - InUSD fixedpoint.Value `json:"inUSD" db:"net_asset_in_usd"` - - // InBTC is net asset in BTC - InBTC fixedpoint.Value `json:"inBTC" db:"net_asset_in_btc"` - - Time time.Time `json:"time" db:"time"` - Locked fixedpoint.Value `json:"lock" db:"lock" ` - Available fixedpoint.Value `json:"available" db:"available"` - Borrowed fixedpoint.Value `json:"borrowed" db:"borrowed"` - PriceInUSD fixedpoint.Value `json:"priceInUSD" db:"price_in_usd"` -} - -type AssetMap map[string]Asset - -func (m AssetMap) InUSD() (total fixedpoint.Value) { - for _, a := range m { - if a.InUSD.IsZero() { - continue - } - - total = total.Add(a.InUSD) - } - return total -} - -func (m AssetMap) PlainText() (o string) { - var assets = m.Slice() - - // sort assets - sort.Slice(assets, func(i, j int) bool { - return assets[i].InUSD.Compare(assets[j].InUSD) > 0 - }) - - sumUsd := fixedpoint.Zero - sumBTC := fixedpoint.Zero - for _, a := range assets { - usd := a.InUSD - btc := a.InBTC - if !a.InUSD.IsZero() { - o += fmt.Sprintf(" %s: %s (≈ %s) (≈ %s)", - a.Currency, - a.NetAsset.String(), - USD.FormatMoney(usd), - BTC.FormatMoney(btc), - ) + "\n" - sumUsd = sumUsd.Add(usd) - sumBTC = sumBTC.Add(btc) - } else { - o += fmt.Sprintf(" %s: %s", - a.Currency, - a.NetAsset.String(), - ) + "\n" - } - } - - o += fmt.Sprintf("Net Asset Value: (≈ %s) (≈ %s)", - USD.FormatMoney(sumUsd), - BTC.FormatMoney(sumBTC), - ) - return o -} - -func (m AssetMap) Slice() (assets []Asset) { - for _, a := range m { - assets = append(assets, a) - } - return assets -} - -func (m AssetMap) SlackAttachment() slack.Attachment { - var fields []slack.AttachmentField - var netAssetInBTC, netAssetInUSD fixedpoint.Value - - var assets = m.Slice() - - // sort assets - sort.Slice(assets, func(i, j int) bool { - return assets[i].InUSD.Compare(assets[j].InUSD) > 0 - }) - - for _, a := range assets { - netAssetInUSD = netAssetInUSD.Add(a.InUSD) - netAssetInBTC = netAssetInBTC.Add(a.InBTC) - } - - for _, a := range assets { - if !a.InUSD.IsZero() { - text := fmt.Sprintf("%s (≈ %s) (≈ %s) (%s)", - a.NetAsset.String(), - USD.FormatMoney(a.InUSD), - BTC.FormatMoney(a.InBTC), - a.InUSD.Div(netAssetInUSD).FormatPercentage(2), - ) - - if !a.Borrowed.IsZero() { - text += fmt.Sprintf(" Borrowed: %s", a.Borrowed.String()) - } - - fields = append(fields, slack.AttachmentField{ - Title: a.Currency, - Value: text, - Short: false, - }) - } else { - text := a.NetAsset.String() - - if !a.Borrowed.IsZero() { - text += fmt.Sprintf(" Borrowed: %s", a.Borrowed.String()) - } - - fields = append(fields, slack.AttachmentField{ - Title: a.Currency, - Value: text, - Short: false, - }) - } - } - - return slack.Attachment{ - Title: fmt.Sprintf("Net Asset Value %s (≈ %s)", - USD.FormatMoney(netAssetInUSD), - BTC.FormatMoney(netAssetInBTC), - ), - Fields: fields, - } -} diff --git a/pkg/types/asset/asset.go b/pkg/types/asset/asset.go new file mode 100644 index 0000000000..778bb7408e --- /dev/null +++ b/pkg/types/asset/asset.go @@ -0,0 +1,194 @@ +package asset + +import ( + "fmt" + "sort" + "time" + + "github.com/slack-go/slack" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types/currency" +) + +type Asset struct { + Currency string `json:"currency" db:"currency"` + + Total fixedpoint.Value `json:"total" db:"total"` + + NetAsset fixedpoint.Value `json:"netAsset" db:"net_asset"` + + Interest fixedpoint.Value `json:"interest" db:"interest"` + + // NetAssetInUSD is net asset in USD + NetAssetInUSD fixedpoint.Value `json:"netAssetInUSD" db:"net_asset_in_usd"` + + Debt fixedpoint.Value `json:"debt" db:"debt"` + DebtInUSD fixedpoint.Value `json:"debtInUSD" db:"debt_in_usd"` + + InterestInUSD fixedpoint.Value `json:"interestInUSD" db:"interest_in_usd"` + + // NetAssetInBTC is net asset in BTC + NetAssetInBTC fixedpoint.Value `json:"netAssetInBTC" db:"net_asset_in_btc"` + + Time time.Time `json:"time" db:"time"` + Locked fixedpoint.Value `json:"lock" db:"lock" ` + Available fixedpoint.Value `json:"available" db:"available"` + Borrowed fixedpoint.Value `json:"borrowed" db:"borrowed"` + PriceInUSD fixedpoint.Value `json:"priceInUSD" db:"price_in_usd"` +} + +type Map map[string]Asset + +func (m Map) Merge(other Map) Map { + newMap := make(Map) + for cu, asset := range other { + if existing, ok := m[cu]; ok { + asset.Total = asset.Total.Add(existing.Total) + asset.NetAsset = asset.NetAsset.Add(existing.NetAsset) + asset.Debt = asset.Debt.Add(existing.Debt) + asset.Interest = asset.Interest.Add(existing.Interest) + asset.Locked = asset.Locked.Add(existing.Locked) + asset.Available = asset.Available.Add(existing.Available) + asset.Borrowed = asset.Borrowed.Add(existing.Borrowed) + asset.NetAssetInUSD = asset.NetAssetInUSD.Add(existing.NetAssetInUSD) + asset.NetAssetInBTC = asset.NetAssetInBTC.Add(existing.NetAssetInBTC) + asset.DebtInUSD = asset.DebtInUSD.Add(existing.DebtInUSD) + asset.InterestInUSD = asset.InterestInUSD.Add(existing.InterestInUSD) + } + + newMap[cu] = asset + } + + return newMap +} + +func (m Map) Filter(f func(asset *Asset) bool) Map { + newMap := make(Map) + + for cu, asset := range m { + if f(&asset) { + newMap[cu] = asset + } + } + + return newMap +} + +func (m Map) InUSD() (total fixedpoint.Value) { + for _, a := range m { + if a.NetAssetInUSD.IsZero() { + continue + } + + total = total.Add(a.NetAssetInUSD) + } + + return total +} + +func (m Map) PlainText() (o string) { + var assets = m.Slice() + + // sort assets + sort.Slice(assets, func(i, j int) bool { + return assets[i].NetAssetInUSD.Compare(assets[j].NetAssetInUSD) > 0 + }) + + sumUsd := fixedpoint.Zero + sumBTC := fixedpoint.Zero + for _, a := range assets { + usd := a.NetAssetInUSD + btc := a.NetAssetInBTC + if !a.NetAssetInUSD.IsZero() { + o += fmt.Sprintf(" %s: %s (≈ %s) (≈ %s)", + a.Currency, + a.NetAsset.String(), + currency.USD.FormatMoney(usd), + currency.BTC.FormatMoney(btc), + ) + "\n" + sumUsd = sumUsd.Add(usd) + sumBTC = sumBTC.Add(btc) + } else { + o += fmt.Sprintf(" %s: %s", + a.Currency, + a.NetAsset.String(), + ) + "\n" + } + } + + o += fmt.Sprintf("Net Asset Value: (≈ %s) (≈ %s)", + currency.USD.FormatMoney(sumUsd), + currency.BTC.FormatMoney(sumBTC), + ) + return o +} + +func (m Map) Slice() (assets []Asset) { + for _, a := range m { + assets = append(assets, a) + } + return assets +} + +func (m Map) SlackAttachment() slack.Attachment { + var fields []slack.AttachmentField + var netAssetInBTC, netAssetInUSD fixedpoint.Value + + var assets = m.Slice() + + // sort assets + sort.Slice(assets, func(i, j int) bool { + return assets[i].NetAssetInUSD.Compare(assets[j].NetAssetInUSD) > 0 + }) + + for _, a := range assets { + netAssetInUSD = netAssetInUSD.Add(a.NetAssetInUSD) + netAssetInBTC = netAssetInBTC.Add(a.NetAssetInBTC) + } + + for _, a := range assets { + if !a.NetAssetInUSD.IsZero() { + text := fmt.Sprintf("%s (≈ %s) (≈ %s) (%s)", + a.NetAsset.String(), + currency.USD.FormatMoney(a.NetAssetInUSD), + currency.BTC.FormatMoney(a.NetAssetInBTC), + a.NetAssetInUSD.Div(netAssetInUSD).FormatPercentage(2), + ) + + if !a.Borrowed.IsZero() { + text += fmt.Sprintf(" Borrowed: %s", a.Borrowed.String()) + } + + if !a.Interest.IsZero() { + text += fmt.Sprintf(" Interest: %s", a.Interest.String()) + } + + fields = append(fields, slack.AttachmentField{ + Title: a.Currency, + Value: text, + Short: false, + }) + } else { + text := a.NetAsset.String() + + if !a.Borrowed.IsZero() { + text += fmt.Sprintf(" Borrowed: %s", a.Borrowed.String()) + } + + fields = append(fields, slack.AttachmentField{ + Title: a.Currency, + Value: text, + Short: false, + }) + } + } + + return slack.Attachment{ + Title: fmt.Sprintf("Net Asset Value %s (≈ %s)", + currency.USD.FormatMoney(netAssetInUSD), + currency.BTC.FormatMoney(netAssetInBTC), + ), + Fields: fields, + } +} diff --git a/pkg/types/balance.go b/pkg/types/balance.go index 454ca813ca..054eef2bd0 100644 --- a/pkg/types/balance.go +++ b/pkg/types/balance.go @@ -10,6 +10,8 @@ import ( "github.com/slack-go/slack" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types/asset" + currency2 "github.com/c9s/bbgo/pkg/types/currency" ) type PriceMap map[string]fixedpoint.Value @@ -197,12 +199,12 @@ func (m BalanceMap) Copy() (d BalanceMap) { } // Assets converts balances into assets with the given prices -func (m BalanceMap) Assets(prices PriceMap, priceTime time.Time) AssetMap { - assets := make(AssetMap) +func (m BalanceMap) Assets(prices PriceMap, priceTime time.Time) asset.Map { + assets := make(asset.Map) _, btcInUSD, hasBtcPrice := findUSDMarketPrice("BTC", prices) - for currency, b := range m { + for cu, b := range m { total := b.Total() netAsset := b.Net() debt := b.Debt() @@ -211,8 +213,8 @@ func (m BalanceMap) Assets(prices PriceMap, priceTime time.Time) AssetMap { continue } - asset := Asset{ - Currency: currency, + as := asset.Asset{ + Currency: cu, Total: total, Time: priceTime, Locked: b.Locked, @@ -222,34 +224,34 @@ func (m BalanceMap) Assets(prices PriceMap, priceTime time.Time) AssetMap { NetAsset: netAsset, } - if IsUSDFiatCurrency(currency) { // for usd - asset.InUSD = netAsset - asset.PriceInUSD = fixedpoint.One - if hasBtcPrice && !asset.InUSD.IsZero() { - asset.InBTC = asset.InUSD.Div(btcInUSD) + if currency2.IsUSDFiatCurrency(cu) { // for usd + as.NetAssetInUSD = netAsset + as.PriceInUSD = fixedpoint.One + if hasBtcPrice && !as.NetAssetInUSD.IsZero() { + as.NetAssetInBTC = as.NetAssetInUSD.Div(btcInUSD) } } else { // for crypto - if market, usdPrice, ok := findUSDMarketPrice(currency, prices); ok { + if market, usdPrice, ok := findUSDMarketPrice(cu, prices); ok { // this includes USDT, USD, USDC and so on if strings.HasPrefix(market, "USD") || strings.HasPrefix(market, "BUSD") { // for prices like USDT/TWD, BUSD/USDT - if !asset.NetAsset.IsZero() { - asset.InUSD = asset.NetAsset.Div(usdPrice) + if !as.NetAsset.IsZero() { + as.NetAssetInUSD = as.NetAsset.Div(usdPrice) } - asset.PriceInUSD = fixedpoint.One.Div(usdPrice) + as.PriceInUSD = fixedpoint.One.Div(usdPrice) } else { // for prices like BTC/USDT - if !asset.NetAsset.IsZero() { - asset.InUSD = asset.NetAsset.Mul(usdPrice) + if !as.NetAsset.IsZero() { + as.NetAssetInUSD = as.NetAsset.Mul(usdPrice) } - asset.PriceInUSD = usdPrice + as.PriceInUSD = usdPrice } - if hasBtcPrice && !asset.InUSD.IsZero() { - asset.InBTC = asset.InUSD.Div(btcInUSD) + if hasBtcPrice && !as.NetAssetInUSD.IsZero() { + as.NetAssetInBTC = as.NetAssetInUSD.Div(btcInUSD) } } } - assets[currency] = asset + assets[cu] = as } return assets diff --git a/pkg/types/balance_test.go b/pkg/types/balance_test.go index d3b69aeddf..66f3b8d9d9 100644 --- a/pkg/types/balance_test.go +++ b/pkg/types/balance_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types/asset" ) func TestBalanceMap_Add(t *testing.T) { @@ -48,7 +49,7 @@ func TestBalanceMap_Assets(t *testing.T) { name string m BalanceMap args args - want AssetMap + want asset.Map }{ { m: BalanceMap{ @@ -60,32 +61,32 @@ func TestBalanceMap_Assets(t *testing.T) { "BTCUSDT": number(19000.0), }, }, - want: AssetMap{ + want: asset.Map{ "USDT": { - Currency: "USDT", - Total: number(100), - NetAsset: number(100.0), - Interest: number(0), - InUSD: number(100.0), - InBTC: number(100.0 / 19000.0), - Time: time.Time{}, - Locked: number(0), - Available: number(100.0), - Borrowed: number(0), - PriceInUSD: number(1.0), + Currency: "USDT", + Total: number(100), + NetAsset: number(100.0), + Interest: number(0), + NetAssetInUSD: number(100.0), + NetAssetInBTC: number(100.0 / 19000.0), + Time: time.Time{}, + Locked: number(0), + Available: number(100.0), + Borrowed: number(0), + PriceInUSD: number(1.0), }, "BTC": { - Currency: "BTC", - Total: number(0), - NetAsset: number(-2), - Interest: number(0), - InUSD: number(-2 * 19000.0), - InBTC: number(-2), - Time: time.Time{}, - Locked: number(0), - Available: number(0), - Borrowed: number(2), - PriceInUSD: number(19000.0), + Currency: "BTC", + Total: number(0), + NetAsset: number(-2), + Interest: number(0), + NetAssetInUSD: number(-2 * 19000.0), + NetAssetInBTC: number(-2), + Time: time.Time{}, + Locked: number(0), + Available: number(0), + Borrowed: number(2), + PriceInUSD: number(19000.0), }, }, }, diff --git a/pkg/types/currencies.go b/pkg/types/currency/formatters.go similarity index 91% rename from pkg/types/currencies.go rename to pkg/types/currency/formatters.go index 3c5b539e21..dd05a90e2c 100644 --- a/pkg/types/currencies.go +++ b/pkg/types/currency/formatters.go @@ -1,4 +1,4 @@ -package types +package currency import ( "math/big" @@ -8,16 +8,14 @@ import ( "github.com/c9s/bbgo/pkg/fixedpoint" ) -type Acc = accounting.Accounting - type wrapper struct { - Acc + accounting.Accounting } func (w *wrapper) FormatMoney(v fixedpoint.Value) string { f := new(big.Float) f.SetString(v.String()) - return w.Acc.FormatMoneyBigFloat(f) + return w.Accounting.FormatMoneyBigFloat(f) } var USD = wrapper{accounting.Accounting{Symbol: "$ ", Precision: 2}} diff --git a/pkg/types/market.go b/pkg/types/market.go index 1574b785e5..9d731de8c2 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -7,6 +7,7 @@ import ( "github.com/leekchan/accounting" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types/currency" ) type Market struct { @@ -181,13 +182,13 @@ func (m Market) FormatPriceCurrency(val fixedpoint.Value) string { switch m.QuoteCurrency { case "USD", "USDT": - return USD.FormatMoney(val) + return currency.USD.FormatMoney(val) case "BTC": - return BTC.FormatMoney(val) + return currency.BTC.FormatMoney(val) case "BNB": - return BNB.FormatMoney(val) + return currency.BNB.FormatMoney(val) } diff --git a/pkg/types/order.go b/pkg/types/order.go index 79e54e8ac1..1cc722e473 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -11,6 +11,7 @@ import ( "github.com/slack-go/slack" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types/currency" "github.com/c9s/bbgo/pkg/util/templateutil" ) @@ -216,10 +217,10 @@ func (o *SubmitOrder) SlackAttachment() slack.Attachment { } if o.Price.Sign() > 0 && o.Quantity.Sign() > 0 && len(o.Market.QuoteCurrency) > 0 { - if IsFiatCurrency(o.Market.QuoteCurrency) { + if currency.IsFiatCurrency(o.Market.QuoteCurrency) { fields = append(fields, slack.AttachmentField{ Title: "Amount", - Value: USD.FormatMoney(o.Price.Mul(o.Quantity)), + Value: currency.USD.FormatMoney(o.Price.Mul(o.Quantity)), Short: true, }) } else { diff --git a/pkg/types/price_volume_slice.go b/pkg/types/price_volume_slice.go index 47ebcebacc..73b9a18eaf 100644 --- a/pkg/types/price_volume_slice.go +++ b/pkg/types/price_volume_slice.go @@ -261,6 +261,7 @@ func (slice PriceVolumeSlice) AverageDepthPriceByQuote(requiredDepthInQuote fixe return fixedpoint.Zero } + remainingQuote := requiredDepthInQuote totalQuoteAmount := fixedpoint.Zero totalQuantity := fixedpoint.Zero @@ -272,17 +273,25 @@ func (slice PriceVolumeSlice) AverageDepthPriceByQuote(requiredDepthInQuote fixe for i := 0; i < l; i++ { pv := slice[i] - // quoteAmount is the total quote amount of the current price level - quoteAmount := pv.Volume.Mul(pv.Price) - - totalQuoteAmount = totalQuoteAmount.Add(quoteAmount) - totalQuantity = totalQuantity.Add(pv.Volume) - - if requiredDepthInQuote.Sign() > 0 && totalQuoteAmount.Compare(requiredDepthInQuote) > 0 { - return totalQuoteAmount.Div(totalQuantity) + quoteVolume := pv.Price.Mul(pv.Volume) + + if remainingQuote.Compare(quoteVolume) >= 0 { + totalQuantity = totalQuantity.Add(pv.Volume) + totalQuoteAmount = totalQuoteAmount.Add(quoteVolume) + remainingQuote = remainingQuote.Sub(quoteVolume) + } else { + baseAmount := remainingQuote.Div(pv.Price) + totalQuantity = totalQuantity.Add(baseAmount) + totalQuoteAmount = totalQuoteAmount.Add(remainingQuote) + remainingQuote = fixedpoint.Zero + break } } + if totalQuantity.IsZero() { + return fixedpoint.Zero + } + return totalQuoteAmount.Div(totalQuantity) } diff --git a/pkg/types/slack.go b/pkg/types/slack.go index 322220ada3..ab1254f4c2 100644 --- a/pkg/types/slack.go +++ b/pkg/types/slack.go @@ -1,7 +1 @@ package types - -import "github.com/slack-go/slack" - -type SlackAttachmentCreator interface { - SlackAttachment() slack.Attachment -} diff --git a/pkg/types/ticker.go b/pkg/types/ticker.go index 903c638135..4e9fb5072a 100644 --- a/pkg/types/ticker.go +++ b/pkg/types/ticker.go @@ -18,6 +18,11 @@ type Ticker struct { Sell fixedpoint.Value // `sell` from Max, `askPrice` from binance } +// GetValidPrice returns the valid price from the ticker +// if the last price is not zero, return the last price +// if the buy price is not zero, return the buy price +// if the sell price is not zero, return the sell price +// otherwise return the open price func (t *Ticker) GetValidPrice() fixedpoint.Value { if !t.Last.IsZero() { return t.Last