From f7014f222831591136249a0b1f3b905bb214b9d7 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 14 Dec 2024 17:53:45 +0800 Subject: [PATCH 01/20] add NewAssetMapFromBalanceMap with priceSolver --- pkg/bbgo/session.go | 50 +++++++++++++---------------------- pkg/bbgo/session_test.go | 43 ------------------------------ pkg/strategy/xnav/strategy.go | 2 +- pkg/types/balance.go | 42 +++++++++++++++++++++++++++++ pkg/types/ticker.go | 5 ++++ 5 files changed, 67 insertions(+), 75 deletions(-) diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index edf49332ad..b80dcb3ba1 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -202,6 +202,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 +772,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 +789,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) + session.lastPrices[k] = validPrice.Div(fixedpoint.One) } else { - session.lastPrices[k] = v.Last + session.lastPrices[k] = validPrice } } else { session.lastPrices[k] = v.Last @@ -1115,24 +1124,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/strategy/xnav/strategy.go b/pkg/strategy/xnav/strategy.go index ee47e1fc52..580f827af9 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -109,7 +109,7 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] account := session.GetAccount() balances := account.Balances() - if err := session.UpdatePrices(ctx, balances.Currencies(), quoteCurrency); err != nil { + if err := session.UpdatePrices(ctx, balances.NotZero().Currencies(), quoteCurrency); err != nil { log.WithError(err).Error("price update failed") return } diff --git a/pkg/types/balance.go b/pkg/types/balance.go index 454ca813ca..aa910b730e 100644 --- a/pkg/types/balance.go +++ b/pkg/types/balance.go @@ -10,6 +10,7 @@ import ( "github.com/slack-go/slack" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/pricesolver" ) type PriceMap map[string]fixedpoint.Value @@ -196,6 +197,47 @@ func (m BalanceMap) Copy() (d BalanceMap) { return d } +func NewAssetMapFromBalanceMap(priceSolver *pricesolver.SimplePriceSolver, m BalanceMap, fiat string) AssetMap { + assets := make(AssetMap) + + btcInUSD, hasBtcPrice := priceSolver.ResolvePrice("BTC", fiat, "USDT") + + now := time.Now() + for currency, b := range m { + + total := b.Total() + netAsset := b.Net() + debt := b.Debt() + + if total.IsZero() && netAsset.IsZero() && debt.IsZero() { + continue + } + + asset := Asset{ + Currency: currency, + Total: total, + Time: now, + Locked: b.Locked, + Available: b.Available, + Borrowed: b.Borrowed, + Interest: b.Interest, + NetAsset: netAsset, + } + + if assetPrice, ok := priceSolver.ResolvePrice(currency, fiat, "USDT"); ok { + asset.PriceInUSD = assetPrice + asset.InUSD = netAsset.Mul(assetPrice) + if hasBtcPrice { + asset.InBTC = asset.InUSD.Div(btcInUSD) + } + } + + assets[currency] = asset + } + + return assets +} + // Assets converts balances into assets with the given prices func (m BalanceMap) Assets(prices PriceMap, priceTime time.Time) AssetMap { assets := make(AssetMap) 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 From b6e3292c0f462f89ede076568944c7131274c575 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 14 Dec 2024 21:34:20 +0800 Subject: [PATCH 02/20] xnav: improve and simplify price solving --- pkg/notifier/slacknotifier/slack.go | 3 ++ pkg/strategy/xnav/strategy.go | 54 +++++++++++++-------------- pkg/types/asset.go | 37 +++++++++++++++++++ pkg/types/asset/asset.go | 57 +++++++++++++++++++++++++++++ pkg/types/balance.go | 42 --------------------- 5 files changed, 124 insertions(+), 69 deletions(-) create mode 100644 pkg/types/asset/asset.go diff --git a/pkg/notifier/slacknotifier/slack.go b/pkg/notifier/slacknotifier/slack.go index 7d62a7ee85..ab96633fa6 100644 --- a/pkg/notifier/slacknotifier/slack.go +++ b/pkg/notifier/slacknotifier/slack.go @@ -468,6 +468,9 @@ func (n *Notifier) NotifyTo(channel string, obj interface{}, args ...interface{} case slack.Attachment: opts = append(opts, slack.MsgOptionAttachments(append([]slack.Attachment{a}, slackAttachments...)...)) + case *slack.Attachment: + opts = append(opts, slack.MsgOptionAttachments(append([]slack.Attachment{*a}, slackAttachments...)...)) + case types.SlackAttachmentCreator: // convert object to slack attachment (if supported) opts = append(opts, slack.MsgOptionAttachments(append([]slack.Attachment{a.SlackAttachment()}, slackAttachments...)...)) diff --git a/pkg/strategy/xnav/strategy.go b/pkg/strategy/xnav/strategy.go index 580f827af9..26dd57b7a9 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -9,6 +9,7 @@ import ( "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/util/templateutil" "github.com/c9s/bbgo/pkg/util/timejitter" @@ -58,10 +59,11 @@ func (s *State) Reset() { type Strategy struct { *bbgo.Environment - Interval types.Interval `json:"interval"` - Schedule string `json:"schedule"` - ReportOnStart bool `json:"reportOnStart"` - IgnoreDusts bool `json:"ignoreDusts"` + Interval types.Interval `json:"interval"` + Schedule string `json:"schedule"` + ReportOnStart bool `json:"reportOnStart"` + IgnoreDusts bool `json:"ignoreDusts"` + DisplayBreakdown bool `json:"displayBreakdown"` State *State `persistence:"state"` @@ -88,10 +90,8 @@ 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{} priceTime := time.Now() + allAssets := map[string]types.AssetMap{} // iterate the sessions and record them quoteCurrency := "USDT" @@ -108,38 +108,38 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] } account := session.GetAccount() - balances := account.Balances() - if err := session.UpdatePrices(ctx, balances.NotZero().Currencies(), quoteCurrency); err != nil { + 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) + assets := asset.NewMapFromBalanceMap(session.GetPriceSolver(), priceTime, balances, quoteCurrency) + s.Environment.RecordAsset(priceTime, session, assets) - prices := session.LastPrices() - assets := balances.Assets(prices, priceTime) + allAssets[sessionName] = assets - // merge prices - for m, p := range prices { - allPrices[m] = p + if s.DisplayBreakdown { + slackAttachment := assets.SlackAttachment() + slackAttachment.Title = "Session " + sessionName + " " + slackAttachment.Title + bbgo.Notify(slackAttachment) } - - s.Environment.RecordAsset(priceTime, session, assets) } - displayAssets := types.AssetMap{} - totalAssets := totalBalances.Assets(allPrices, priceTime) - s.Environment.RecordAsset(priceTime, &bbgo.ExchangeSession{Name: "ALL"}, totalAssets) + totalAssets := types.AssetMap{} + for _, assets := range allAssets { + totalAssets = totalAssets.Merge(assets) + } - 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 *types.Asset) bool { + if s.IgnoreDusts && !asset.InUSD.IsZero() && asset.InUSD.Abs().Compare(ten) < 0 { + return false } - displayAssets[currency] = asset - } + return true + }) + + s.Environment.RecordAsset(priceTime, &bbgo.ExchangeSession{Name: "ALL"}, totalAssets) bbgo.Notify(displayAssets) diff --git a/pkg/types/asset.go b/pkg/types/asset.go index c4865838be..204e5cded3 100644 --- a/pkg/types/asset.go +++ b/pkg/types/asset.go @@ -34,6 +34,38 @@ type Asset struct { type AssetMap map[string]Asset +func (m AssetMap) Merge(other AssetMap) AssetMap { + newMap := make(AssetMap) + for currency, asset := range other { + if existing, ok := m[currency]; ok { + asset.Total = asset.Total.Add(existing.Total) + asset.NetAsset = asset.NetAsset.Add(existing.NetAsset) + 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.InUSD = asset.InUSD.Add(existing.InUSD) + asset.InBTC = asset.InBTC.Add(existing.InBTC) + } + + m[currency] = asset + } + + return newMap +} + +func (m AssetMap) Filter(f func(asset *Asset) bool) AssetMap { + newMap := make(AssetMap) + + for currency, asset := range m { + if f(&asset) { + newMap[currency] = asset + } + } + + return newMap +} + func (m AssetMap) InUSD() (total fixedpoint.Value) { for _, a := range m { if a.InUSD.IsZero() { @@ -42,6 +74,7 @@ func (m AssetMap) InUSD() (total fixedpoint.Value) { total = total.Add(a.InUSD) } + return total } @@ -118,6 +151,10 @@ func (m AssetMap) SlackAttachment() slack.Attachment { 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, diff --git a/pkg/types/asset/asset.go b/pkg/types/asset/asset.go new file mode 100644 index 0000000000..243e2d196b --- /dev/null +++ b/pkg/types/asset/asset.go @@ -0,0 +1,57 @@ +package asset + +import ( + "time" + + "github.com/sirupsen/logrus" + + "github.com/c9s/bbgo/pkg/pricesolver" + "github.com/c9s/bbgo/pkg/types" +) + +func NewMapFromBalanceMap( + priceSolver *pricesolver.SimplePriceSolver, priceTime time.Time, m types.BalanceMap, fiat string, +) types.AssetMap { + assets := make(types.AssetMap) + + btcInUSD, hasBtcPrice := priceSolver.ResolvePrice("BTC", fiat, "USDT") + if !hasBtcPrice { + logrus.Warnf("AssetMap: unable to resolve price for BTC") + } + + for currency, b := range m { + + total := b.Total() + netAsset := b.Net() + debt := b.Debt() + + if total.IsZero() && netAsset.IsZero() && debt.IsZero() { + continue + } + + asset := types.Asset{ + Currency: currency, + Total: total, + Time: priceTime, + Locked: b.Locked, + Available: b.Available, + Borrowed: b.Borrowed, + Interest: b.Interest, + NetAsset: netAsset, + } + + if assetPrice, ok := priceSolver.ResolvePrice(currency, fiat, "USDT"); ok { + asset.PriceInUSD = assetPrice + asset.InUSD = netAsset.Mul(assetPrice) + if hasBtcPrice { + asset.InBTC = asset.InUSD.Div(btcInUSD) + } + } else { + logrus.Warnf("AssetMap: unable to resolve price for %s", currency) + } + + assets[currency] = asset + } + + return assets +} diff --git a/pkg/types/balance.go b/pkg/types/balance.go index aa910b730e..454ca813ca 100644 --- a/pkg/types/balance.go +++ b/pkg/types/balance.go @@ -10,7 +10,6 @@ import ( "github.com/slack-go/slack" "github.com/c9s/bbgo/pkg/fixedpoint" - "github.com/c9s/bbgo/pkg/pricesolver" ) type PriceMap map[string]fixedpoint.Value @@ -197,47 +196,6 @@ func (m BalanceMap) Copy() (d BalanceMap) { return d } -func NewAssetMapFromBalanceMap(priceSolver *pricesolver.SimplePriceSolver, m BalanceMap, fiat string) AssetMap { - assets := make(AssetMap) - - btcInUSD, hasBtcPrice := priceSolver.ResolvePrice("BTC", fiat, "USDT") - - now := time.Now() - for currency, b := range m { - - total := b.Total() - netAsset := b.Net() - debt := b.Debt() - - if total.IsZero() && netAsset.IsZero() && debt.IsZero() { - continue - } - - asset := Asset{ - Currency: currency, - Total: total, - Time: now, - Locked: b.Locked, - Available: b.Available, - Borrowed: b.Borrowed, - Interest: b.Interest, - NetAsset: netAsset, - } - - if assetPrice, ok := priceSolver.ResolvePrice(currency, fiat, "USDT"); ok { - asset.PriceInUSD = assetPrice - asset.InUSD = netAsset.Mul(assetPrice) - if hasBtcPrice { - asset.InBTC = asset.InUSD.Div(btcInUSD) - } - } - - assets[currency] = asset - } - - return assets -} - // Assets converts balances into assets with the given prices func (m BalanceMap) Assets(prices PriceMap, priceTime time.Time) AssetMap { assets := make(AssetMap) From 4efbd647a638858548eb1354e89ead8d1b984c2f Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 14 Dec 2024 21:37:24 +0800 Subject: [PATCH 03/20] xnav: log asset balance --- pkg/strategy/xnav/strategy.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/strategy/xnav/strategy.go b/pkg/strategy/xnav/strategy.go index 26dd57b7a9..a4c8b8f73b 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -117,6 +117,14 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] assets := asset.NewMapFromBalanceMap(session.GetPriceSolver(), priceTime, balances, quoteCurrency) s.Environment.RecordAsset(priceTime, session, assets) + for _, as := range assets { + log.Infof("session %s %s asset = net:%s available:%s", + sessionName, + as.Currency, + as.NetAsset.String(), + as.Available.String()) + } + allAssets[sessionName] = assets if s.DisplayBreakdown { From ec28f4bef63a3c1dc6e293ff936ff1d51001e372 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 14 Dec 2024 21:38:02 +0800 Subject: [PATCH 04/20] xnav: add log with fields --- pkg/strategy/xnav/strategy.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/xnav/strategy.go b/pkg/strategy/xnav/strategy.go index a4c8b8f73b..cd2de37e3d 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -118,7 +118,10 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] s.Environment.RecordAsset(priceTime, session, assets) for _, as := range assets { - log.Infof("session %s %s asset = net:%s available:%s", + log.WithFields(logrus.Fields{ + "session": sessionName, + "exchange": session.ExchangeName, + }).Infof("session %s %s asset = net:%s available:%s", sessionName, as.Currency, as.NetAsset.String(), From 1c202e6dd07cee2ce7523358ab8fcf8bde1c29ce Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 14 Dec 2024 21:41:26 +0800 Subject: [PATCH 05/20] update AverageDepthPriceByQuote implementation and its test cases fix #1861 --- pkg/strategy/xdepthmaker/strategy_test.go | 4 ++-- pkg/types/price_volume_slice.go | 25 +++++++++++++++-------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/pkg/strategy/xdepthmaker/strategy_test.go b/pkg/strategy/xdepthmaker/strategy_test.go index 9880d75c4a..3c89566169 100644 --- a/pkg/strategy/xdepthmaker/strategy_test.go +++ b/pkg/strategy/xdepthmaker/strategy_test.go @@ -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/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) } From 218be5ab1f33cf5c33ae596281c2599a07734b24 Mon Sep 17 00:00:00 2001 From: c9s Date: Sat, 14 Dec 2024 21:57:33 +0800 Subject: [PATCH 06/20] xnav: early return when in backtest --- pkg/strategy/xnav/strategy.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/strategy/xnav/strategy.go b/pkg/strategy/xnav/strategy.go index cd2de37e3d..232400dcae 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -180,6 +180,7 @@ 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 != "" { From 26d7e17dff2d45baa617a248ebb14961e361f383 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 15 Dec 2024 01:02:47 +0800 Subject: [PATCH 07/20] xnav: add more logs to xnav --- pkg/strategy/xnav/strategy.go | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/pkg/strategy/xnav/strategy.go b/pkg/strategy/xnav/strategy.go index 232400dcae..d07f5daa04 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -90,12 +90,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) { + log.Infof("recording net asset value...") + priceTime := time.Now() allAssets := map[string]types.AssetMap{} // 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 @@ -162,6 +166,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{} @@ -184,21 +203,7 @@ func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, se } 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() { From 76513f7f2d641eda51e1b184e930060fd8d07715 Mon Sep 17 00:00:00 2001 From: c9s Date: Sun, 15 Dec 2024 13:17:42 +0800 Subject: [PATCH 08/20] all: move asset map to a single package --- pkg/accounting/pnl/report.go | 19 +- pkg/asset/asset.go | 186 ++++++++++++++++++ pkg/bbgo/account_value_calc.go | 7 +- pkg/bbgo/environment.go | 3 +- pkg/bbgo/order_execution.go | 5 +- pkg/bbgo/session.go | 5 +- pkg/server/routes.go | 7 +- pkg/service/account.go | 12 +- pkg/service/account_test.go | 5 +- pkg/strategy/autoborrow/strategy.go | 3 +- pkg/strategy/tri/position.go | 7 +- pkg/strategy/xmaker/strategy_test.go | 5 +- pkg/strategy/xnav/strategy.go | 7 +- pkg/types/asset.go | 184 ----------------- pkg/types/asset/asset.go | 7 +- pkg/types/balance.go | 10 +- pkg/types/balance_test.go | 5 +- .../{currencies.go => currency/formatters.go} | 8 +- pkg/types/market.go | 7 +- pkg/types/order.go | 5 +- 20 files changed, 260 insertions(+), 237 deletions(-) create mode 100644 pkg/asset/asset.go rename pkg/types/{currencies.go => currency/formatters.go} (91%) 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/asset/asset.go b/pkg/asset/asset.go new file mode 100644 index 0000000000..add50fdb4d --- /dev/null +++ b/pkg/asset/asset.go @@ -0,0 +1,186 @@ +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"` + + // 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 Map map[string]Asset + +func (m Map) Merge(other Map) Map { + newMap := make(Map) + for currency, asset := range other { + if existing, ok := m[currency]; ok { + asset.Total = asset.Total.Add(existing.Total) + asset.NetAsset = asset.NetAsset.Add(existing.NetAsset) + 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.InUSD = asset.InUSD.Add(existing.InUSD) + asset.InBTC = asset.InBTC.Add(existing.InBTC) + } + + m[currency] = asset + } + + return newMap +} + +func (m Map) Filter(f func(asset *Asset) bool) Map { + newMap := make(Map) + + for currency, asset := range m { + if f(&asset) { + newMap[currency] = asset + } + } + + return newMap +} + +func (m Map) InUSD() (total fixedpoint.Value) { + for _, a := range m { + if a.InUSD.IsZero() { + continue + } + + total = total.Add(a.InUSD) + } + + 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].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(), + 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].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(), + currency.USD.FormatMoney(a.InUSD), + currency.BTC.FormatMoney(a.InBTC), + a.InUSD.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/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..b791281eed 100644 --- a/pkg/bbgo/environment.go +++ b/pkg/bbgo/environment.go @@ -19,6 +19,7 @@ import ( "github.com/spf13/viper" "gopkg.in/tucnak/telebot.v2" + "github.com/c9s/bbgo/pkg/asset" "github.com/c9s/bbgo/pkg/envvar" "github.com/c9s/bbgo/pkg/exchange" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -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 b80dcb3ba1..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" @@ -799,7 +800,7 @@ func (session *ExchangeSession) UpdatePrices(ctx context.Context, currencies []s // for {Crypto}/USDT markets // map things like BTCUSDT = {price} if market, ok := markets[k]; ok { - if types.IsFiatCurrency(market.BaseCurrency) { + if currency2.IsFiatCurrency(market.BaseCurrency) { session.lastPrices[k] = validPrice.Div(fixedpoint.One) } else { session.lastPrices[k] = validPrice @@ -828,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) } diff --git a/pkg/server/routes.go b/pkg/server/routes.go index ef43eee712..1ba565a15b 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -16,6 +16,7 @@ import ( "github.com/joho/godotenv" "github.com/sirupsen/logrus" + "github.com/c9s/bbgo/pkg/asset" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/service" @@ -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..9117c33bde 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/asset" + "github.com/c9s/bbgo/pkg/types" ) 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 diff --git a/pkg/service/account_test.go b/pkg/service/account_test.go index 89c0fa98cf..089c029e07 100644 --- a/pkg/service/account_test.go +++ b/pkg/service/account_test.go @@ -7,6 +7,7 @@ import ( "github.com/jmoiron/sqlx" "github.com/stretchr/testify/assert" + "github.com/c9s/bbgo/pkg/asset" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -23,8 +24,8 @@ 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{ + err = service.InsertAsset(t1, "binance", types.ExchangeBinance, "main", false, false, "", asset.Map{ + "BTC": asset.Asset{ Currency: "BTC", Total: fixedpoint.MustNewFromString("1.0"), InUSD: fixedpoint.MustNewFromString("10.0"), 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/tri/position.go b/pkg/strategy/tri/position.go index 5bbbe815e8..d4f842dbf4 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) } @@ -81,7 +82,7 @@ func (p *MultiCurrencyPosition) CollectProfits() []Profit { if price, ok := p.TradePrices[currency]; ok && !price.IsZero() { profit.ProfitInUSD = base.Mul(price) - } else if types.IsUSDFiatCurrency(currency) { + } else if currency.IsUSDFiatCurrency(currency) { profit.ProfitInUSD = base } 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/strategy.go b/pkg/strategy/xnav/strategy.go index d07f5daa04..b078dbeff2 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -6,6 +6,7 @@ import ( "sync" "time" + asset2 "github.com/c9s/bbgo/pkg/asset" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" @@ -93,7 +94,7 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] log.Infof("recording net asset value...") priceTime := time.Now() - allAssets := map[string]types.AssetMap{} + allAssets := map[string]asset2.Map{} // iterate the sessions and record them quoteCurrency := "USDT" @@ -141,12 +142,12 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] } } - totalAssets := types.AssetMap{} + totalAssets := asset2.Map{} for _, assets := range allAssets { totalAssets = totalAssets.Merge(assets) } - displayAssets := totalAssets.Filter(func(asset *types.Asset) bool { + displayAssets := totalAssets.Filter(func(asset *asset2.Asset) bool { if s.IgnoreDusts && !asset.InUSD.IsZero() && asset.InUSD.Abs().Compare(ten) < 0 { return false } diff --git a/pkg/types/asset.go b/pkg/types/asset.go index 204e5cded3..ab1254f4c2 100644 --- a/pkg/types/asset.go +++ b/pkg/types/asset.go @@ -1,185 +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) Merge(other AssetMap) AssetMap { - newMap := make(AssetMap) - for currency, asset := range other { - if existing, ok := m[currency]; ok { - asset.Total = asset.Total.Add(existing.Total) - asset.NetAsset = asset.NetAsset.Add(existing.NetAsset) - 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.InUSD = asset.InUSD.Add(existing.InUSD) - asset.InBTC = asset.InBTC.Add(existing.InBTC) - } - - m[currency] = asset - } - - return newMap -} - -func (m AssetMap) Filter(f func(asset *Asset) bool) AssetMap { - newMap := make(AssetMap) - - for currency, asset := range m { - if f(&asset) { - newMap[currency] = asset - } - } - - return newMap -} - -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()) - } - - 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)", - USD.FormatMoney(netAssetInUSD), - BTC.FormatMoney(netAssetInBTC), - ), - Fields: fields, - } -} diff --git a/pkg/types/asset/asset.go b/pkg/types/asset/asset.go index 243e2d196b..d878962ae9 100644 --- a/pkg/types/asset/asset.go +++ b/pkg/types/asset/asset.go @@ -5,14 +5,15 @@ import ( "github.com/sirupsen/logrus" + asset2 "github.com/c9s/bbgo/pkg/asset" "github.com/c9s/bbgo/pkg/pricesolver" "github.com/c9s/bbgo/pkg/types" ) func NewMapFromBalanceMap( priceSolver *pricesolver.SimplePriceSolver, priceTime time.Time, m types.BalanceMap, fiat string, -) types.AssetMap { - assets := make(types.AssetMap) +) asset2.Map { + assets := make(asset2.Map) btcInUSD, hasBtcPrice := priceSolver.ResolvePrice("BTC", fiat, "USDT") if !hasBtcPrice { @@ -29,7 +30,7 @@ func NewMapFromBalanceMap( continue } - asset := types.Asset{ + asset := asset2.Asset{ Currency: currency, Total: total, Time: priceTime, diff --git a/pkg/types/balance.go b/pkg/types/balance.go index 454ca813ca..a4a6161d56 100644 --- a/pkg/types/balance.go +++ b/pkg/types/balance.go @@ -9,7 +9,9 @@ import ( log "github.com/sirupsen/logrus" "github.com/slack-go/slack" + "github.com/c9s/bbgo/pkg/asset" "github.com/c9s/bbgo/pkg/fixedpoint" + currency2 "github.com/c9s/bbgo/pkg/types/currency" ) type PriceMap map[string]fixedpoint.Value @@ -197,8 +199,8 @@ 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) @@ -211,7 +213,7 @@ func (m BalanceMap) Assets(prices PriceMap, priceTime time.Time) AssetMap { continue } - asset := Asset{ + asset := asset.Asset{ Currency: currency, Total: total, Time: priceTime, @@ -222,7 +224,7 @@ func (m BalanceMap) Assets(prices PriceMap, priceTime time.Time) AssetMap { NetAsset: netAsset, } - if IsUSDFiatCurrency(currency) { // for usd + if currency2.IsUSDFiatCurrency(currency) { // for usd asset.InUSD = netAsset asset.PriceInUSD = fixedpoint.One if hasBtcPrice && !asset.InUSD.IsZero() { diff --git a/pkg/types/balance_test.go b/pkg/types/balance_test.go index d3b69aeddf..1f45416643 100644 --- a/pkg/types/balance_test.go +++ b/pkg/types/balance_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/c9s/bbgo/pkg/asset" "github.com/c9s/bbgo/pkg/fixedpoint" ) @@ -48,7 +49,7 @@ func TestBalanceMap_Assets(t *testing.T) { name string m BalanceMap args args - want AssetMap + want asset.Map }{ { m: BalanceMap{ @@ -60,7 +61,7 @@ func TestBalanceMap_Assets(t *testing.T) { "BTCUSDT": number(19000.0), }, }, - want: AssetMap{ + want: asset.Map{ "USDT": { Currency: "USDT", Total: number(100), 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 { From d7ccbd43c818c8d9ddc907b0d99f379272e31ec2 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 16 Dec 2024 09:26:19 +0800 Subject: [PATCH 09/20] xdepthmaker: fix test cases --- pkg/strategy/xdepthmaker/strategy_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/xdepthmaker/strategy_test.go b/pkg/strategy/xdepthmaker/strategy_test.go index 3c89566169..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) } From 0fea07be267a3ce901bfe3cd4cbdf91d73756d89 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 16 Dec 2024 09:29:26 +0800 Subject: [PATCH 10/20] xnav: rename displayBreakdown to showBreakdown --- pkg/strategy/xnav/strategy.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/xnav/strategy.go b/pkg/strategy/xnav/strategy.go index b078dbeff2..19bfdf172c 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -60,11 +60,11 @@ func (s *State) Reset() { type Strategy struct { *bbgo.Environment - Interval types.Interval `json:"interval"` - Schedule string `json:"schedule"` - ReportOnStart bool `json:"reportOnStart"` - IgnoreDusts bool `json:"ignoreDusts"` - DisplayBreakdown bool `json:"displayBreakdown"` + Interval types.Interval `json:"interval"` + Schedule string `json:"schedule"` + ReportOnStart bool `json:"reportOnStart"` + IgnoreDusts bool `json:"ignoreDusts"` + ShowBreakdown bool `json:"showBreakdown"` State *State `persistence:"state"` @@ -135,7 +135,7 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] allAssets[sessionName] = assets - if s.DisplayBreakdown { + if s.ShowBreakdown { slackAttachment := assets.SlackAttachment() slackAttachment.Title = "Session " + sessionName + " " + slackAttachment.Title bbgo.Notify(slackAttachment) From 4f1e4077727edb6d13be65078fab61d7461a2859 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 16 Dec 2024 10:10:08 +0800 Subject: [PATCH 11/20] xnav: add showDebtDetails --- pkg/strategy/xnav/strategy.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/strategy/xnav/strategy.go b/pkg/strategy/xnav/strategy.go index 19bfdf172c..5875ee1df0 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -66,6 +66,8 @@ type Strategy struct { IgnoreDusts bool `json:"ignoreDusts"` ShowBreakdown bool `json:"showBreakdown"` + ShowDebtDetails bool `json:"showDebtDetails"` + State *State `persistence:"state"` cron *cron.Cron From 1ab2af7838b67ecb74f7fdfe9475bc8bd3160fcf Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 16 Dec 2024 11:20:09 +0800 Subject: [PATCH 12/20] types: fix asset package and calculate debt, interest in usd --- pkg/asset/asset.go | 186 ------------------------------- pkg/bbgo/environment.go | 2 +- pkg/server/routes.go | 2 +- pkg/service/account.go | 6 +- pkg/service/account_test.go | 22 ++-- pkg/strategy/tri/_doc.go | 4 + pkg/strategy/tri/position.go | 30 ++--- pkg/strategy/xnav/assetmap.go | 60 ++++++++++ pkg/strategy/xnav/strategy.go | 2 +- pkg/types/asset/asset.go | 202 ++++++++++++++++++++++++++++------ pkg/types/balance.go | 38 +++---- pkg/types/balance_test.go | 46 ++++---- 12 files changed, 306 insertions(+), 294 deletions(-) delete mode 100644 pkg/asset/asset.go create mode 100644 pkg/strategy/tri/_doc.go create mode 100644 pkg/strategy/xnav/assetmap.go diff --git a/pkg/asset/asset.go b/pkg/asset/asset.go deleted file mode 100644 index add50fdb4d..0000000000 --- a/pkg/asset/asset.go +++ /dev/null @@ -1,186 +0,0 @@ -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"` - - // 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 Map map[string]Asset - -func (m Map) Merge(other Map) Map { - newMap := make(Map) - for currency, asset := range other { - if existing, ok := m[currency]; ok { - asset.Total = asset.Total.Add(existing.Total) - asset.NetAsset = asset.NetAsset.Add(existing.NetAsset) - 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.InUSD = asset.InUSD.Add(existing.InUSD) - asset.InBTC = asset.InBTC.Add(existing.InBTC) - } - - m[currency] = asset - } - - return newMap -} - -func (m Map) Filter(f func(asset *Asset) bool) Map { - newMap := make(Map) - - for currency, asset := range m { - if f(&asset) { - newMap[currency] = asset - } - } - - return newMap -} - -func (m Map) InUSD() (total fixedpoint.Value) { - for _, a := range m { - if a.InUSD.IsZero() { - continue - } - - total = total.Add(a.InUSD) - } - - 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].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(), - 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].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(), - currency.USD.FormatMoney(a.InUSD), - currency.BTC.FormatMoney(a.InBTC), - a.InUSD.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/bbgo/environment.go b/pkg/bbgo/environment.go index b791281eed..c495860547 100644 --- a/pkg/bbgo/environment.go +++ b/pkg/bbgo/environment.go @@ -19,7 +19,6 @@ import ( "github.com/spf13/viper" "gopkg.in/tucnak/telebot.v2" - "github.com/c9s/bbgo/pkg/asset" "github.com/c9s/bbgo/pkg/envvar" "github.com/c9s/bbgo/pkg/exchange" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -30,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" ) diff --git a/pkg/server/routes.go b/pkg/server/routes.go index 1ba565a15b..3f9a10fff8 100644 --- a/pkg/server/routes.go +++ b/pkg/server/routes.go @@ -16,11 +16,11 @@ import ( "github.com/joho/godotenv" "github.com/sirupsen/logrus" - "github.com/c9s/bbgo/pkg/asset" "github.com/c9s/bbgo/pkg/bbgo" "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" diff --git a/pkg/service/account.go b/pkg/service/account.go index 9117c33bde..917d13bea0 100644 --- a/pkg/service/account.go +++ b/pkg/service/account.go @@ -6,8 +6,8 @@ import ( "github.com/jmoiron/sqlx" "go.uber.org/multierr" - "github.com/c9s/bbgo/pkg/asset" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/types/asset" ) type AccountService struct { @@ -52,8 +52,8 @@ func (s *AccountService) InsertAsset( 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 089c029e07..8c8f4e99ac 100644 --- a/pkg/service/account_test.go +++ b/pkg/service/account_test.go @@ -7,9 +7,9 @@ import ( "github.com/jmoiron/sqlx" "github.com/stretchr/testify/assert" - "github.com/c9s/bbgo/pkg/asset" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/types/asset" ) func TestAccountService(t *testing.T) { @@ -26,16 +26,16 @@ func TestAccountService(t *testing.T) { t1 := time.Now() err = service.InsertAsset(t1, "binance", types.ExchangeBinance, "main", false, false, "", asset.Map{ "BTC": asset.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"), + 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/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 d4f842dbf4..5efa49b6c9 100644 --- a/pkg/strategy/tri/position.go +++ b/pkg/strategy/tri/position.go @@ -69,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 currency.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 } } @@ -100,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/xnav/assetmap.go b/pkg/strategy/xnav/assetmap.go new file mode 100644 index 0000000000..b62ce28867 --- /dev/null +++ b/pkg/strategy/xnav/assetmap.go @@ -0,0 +1,60 @@ +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, + } + + 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 5875ee1df0..e16e5ca594 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -121,7 +121,7 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] return } - assets := asset.NewMapFromBalanceMap(session.GetPriceSolver(), priceTime, balances, quoteCurrency) + assets := NewAssetMapFromBalanceMap(session.GetPriceSolver(), priceTime, balances, quoteCurrency) s.Environment.RecordAsset(priceTime, session, assets) for _, as := range assets { diff --git a/pkg/types/asset/asset.go b/pkg/types/asset/asset.go index d878962ae9..5b9f80efc3 100644 --- a/pkg/types/asset/asset.go +++ b/pkg/types/asset/asset.go @@ -1,58 +1,192 @@ package asset import ( + "fmt" + "sort" "time" - "github.com/sirupsen/logrus" + "github.com/slack-go/slack" - asset2 "github.com/c9s/bbgo/pkg/asset" - "github.com/c9s/bbgo/pkg/pricesolver" - "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types/currency" ) -func NewMapFromBalanceMap( - priceSolver *pricesolver.SimplePriceSolver, priceTime time.Time, m types.BalanceMap, fiat string, -) asset2.Map { - assets := make(asset2.Map) +type Asset struct { + Currency string `json:"currency" db:"currency"` - btcInUSD, hasBtcPrice := priceSolver.ResolvePrice("BTC", fiat, "USDT") - if !hasBtcPrice { - logrus.Warnf("AssetMap: unable to resolve price for BTC") + 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"` + + 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.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) + } + + m[cu] = asset } - for currency, b := range m { + 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 + } + } - total := b.Total() - netAsset := b.Net() - debt := b.Debt() + return newMap +} - if total.IsZero() && netAsset.IsZero() && debt.IsZero() { +func (m Map) InUSD() (total fixedpoint.Value) { + for _, a := range m { + if a.NetAssetInUSD.IsZero() { continue } - asset := asset2.Asset{ - Currency: currency, - Total: total, - Time: priceTime, - Locked: b.Locked, - Available: b.Available, - Borrowed: b.Borrowed, - Interest: b.Interest, - NetAsset: netAsset, + 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 - if assetPrice, ok := priceSolver.ResolvePrice(currency, fiat, "USDT"); ok { - asset.PriceInUSD = assetPrice - asset.InUSD = netAsset.Mul(assetPrice) - if hasBtcPrice { - asset.InBTC = asset.InUSD.Div(btcInUSD) + 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 { - logrus.Warnf("AssetMap: unable to resolve price for %s", currency) - } + text := a.NetAsset.String() - assets[currency] = asset + 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 assets + 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 a4a6161d56..054eef2bd0 100644 --- a/pkg/types/balance.go +++ b/pkg/types/balance.go @@ -9,8 +9,8 @@ import ( log "github.com/sirupsen/logrus" "github.com/slack-go/slack" - "github.com/c9s/bbgo/pkg/asset" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types/asset" currency2 "github.com/c9s/bbgo/pkg/types/currency" ) @@ -204,7 +204,7 @@ func (m BalanceMap) Assets(prices PriceMap, priceTime time.Time) 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() @@ -213,8 +213,8 @@ func (m BalanceMap) Assets(prices PriceMap, priceTime time.Time) asset.Map { continue } - asset := asset.Asset{ - Currency: currency, + as := asset.Asset{ + Currency: cu, Total: total, Time: priceTime, Locked: b.Locked, @@ -224,34 +224,34 @@ func (m BalanceMap) Assets(prices PriceMap, priceTime time.Time) asset.Map { NetAsset: netAsset, } - if currency2.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 1f45416643..66f3b8d9d9 100644 --- a/pkg/types/balance_test.go +++ b/pkg/types/balance_test.go @@ -6,8 +6,8 @@ import ( "github.com/stretchr/testify/assert" - "github.com/c9s/bbgo/pkg/asset" "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types/asset" ) func TestBalanceMap_Add(t *testing.T) { @@ -63,30 +63,30 @@ func TestBalanceMap_Assets(t *testing.T) { }, 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), }, }, }, From 9f81e1abac1691889f4de50b3f28c327508a9907 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 16 Dec 2024 11:29:15 +0800 Subject: [PATCH 13/20] xnav: only ignore asset that both debt and nav are small --- pkg/strategy/xnav/strategy.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/strategy/xnav/strategy.go b/pkg/strategy/xnav/strategy.go index e16e5ca594..bbdd55ff66 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -6,7 +6,6 @@ import ( "sync" "time" - asset2 "github.com/c9s/bbgo/pkg/asset" "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" @@ -96,7 +95,7 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] log.Infof("recording net asset value...") priceTime := time.Now() - allAssets := map[string]asset2.Map{} + allAssets := map[string]asset.Map{} // iterate the sessions and record them quoteCurrency := "USDT" @@ -144,13 +143,13 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] } } - totalAssets := asset2.Map{} + totalAssets := asset.Map{} for _, assets := range allAssets { totalAssets = totalAssets.Merge(assets) } - displayAssets := totalAssets.Filter(func(asset *asset2.Asset) bool { - if s.IgnoreDusts && !asset.InUSD.IsZero() && asset.InUSD.Abs().Compare(ten) < 0 { + 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 } From a67c92f138a7e06ca3243f2a56c9a32069afc949 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 16 Dec 2024 12:06:49 +0800 Subject: [PATCH 14/20] xnav: store asset snapshot --- pkg/strategy/xnav/strategy.go | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/pkg/strategy/xnav/strategy.go b/pkg/strategy/xnav/strategy.go index bbdd55ff66..bdf66dbcef 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -26,6 +26,12 @@ func init() { bbgo.RegisterStrategy(ID, &Strategy{}) } +type AllAssetSnapshot struct { + SessionAssets map[string]asset.Map + TotalAssets asset.Map + Time time.Time +} + type State struct { Since int64 `json:"since"` } @@ -67,6 +73,8 @@ type Strategy struct { ShowDebtDetails bool `json:"showDebtDetails"` + lastAllAssetSnapshot *AllAssetSnapshot + State *State `persistence:"state"` cron *cron.Cron @@ -95,7 +103,7 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] log.Infof("recording net asset value...") priceTime := time.Now() - allAssets := map[string]asset.Map{} + sessionAssets := map[string]asset.Map{} // iterate the sessions and record them quoteCurrency := "USDT" @@ -134,7 +142,7 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] as.Available.String()) } - allAssets[sessionName] = assets + sessionAssets[sessionName] = assets if s.ShowBreakdown { slackAttachment := assets.SlackAttachment() @@ -144,7 +152,7 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] } totalAssets := asset.Map{} - for _, assets := range allAssets { + for _, assets := range sessionAssets { totalAssets = totalAssets.Merge(assets) } @@ -160,6 +168,12 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] bbgo.Notify(displayAssets) + s.lastAllAssetSnapshot = &AllAssetSnapshot{ + SessionAssets: sessionAssets, + TotalAssets: totalAssets, + Time: priceTime, + } + if s.State != nil { if s.State.IsOver24Hours() { s.State.Reset() From bf386c6234c2de9dcc694b132554fe3ac0e4b05d Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 16 Dec 2024 12:44:16 +0800 Subject: [PATCH 15/20] move slack attachment createor interface --- pkg/notifier/slacknotifier/slack.go | 14 +++++++++++--- pkg/types/slack.go | 6 ------ 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/pkg/notifier/slacknotifier/slack.go b/pkg/notifier/slacknotifier/slack.go index ab96633fa6..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 } @@ -471,7 +479,7 @@ 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 SlackAttachmentCreator: // convert object to slack attachment (if supported) opts = append(opts, slack.MsgOptionAttachments(append([]slack.Attachment{a.SlackAttachment()}, slackAttachments...)...)) 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 -} From b1f0fd6cafc9388bb31bfc7cd03b079229172e73 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 16 Dec 2024 14:44:46 +0800 Subject: [PATCH 16/20] xnav: refine debt view and breakdown --- pkg/strategy/xnav/assetmap.go | 1 + pkg/strategy/xnav/strategy.go | 89 +++++++++++++++++++++++++++++++---- pkg/types/asset/asset.go | 4 +- 3 files changed, 84 insertions(+), 10 deletions(-) diff --git a/pkg/strategy/xnav/assetmap.go b/pkg/strategy/xnav/assetmap.go index b62ce28867..fa84c647ad 100644 --- a/pkg/strategy/xnav/assetmap.go +++ b/pkg/strategy/xnav/assetmap.go @@ -38,6 +38,7 @@ func NewAssetMapFromBalanceMap( Borrowed: b.Borrowed, Interest: b.Interest, NetAsset: netAsset, + Debt: debt, } if assetPrice, ok := priceSolver.ResolvePrice(cu, fiat, "USDT"); ok { diff --git a/pkg/strategy/xnav/strategy.go b/pkg/strategy/xnav/strategy.go index bdf66dbcef..2c4695de9d 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -3,6 +3,7 @@ package xnav import ( "context" "fmt" + "sort" "sync" "time" @@ -10,6 +11,7 @@ import ( "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" @@ -32,6 +34,57 @@ type AllAssetSnapshot struct { 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"` } @@ -69,8 +122,8 @@ type Strategy struct { Schedule string `json:"schedule"` ReportOnStart bool `json:"reportOnStart"` IgnoreDusts bool `json:"ignoreDusts"` - ShowBreakdown bool `json:"showBreakdown"` + ShowBreakdown bool `json:"showBreakdown"` ShowDebtDetails bool `json:"showDebtDetails"` lastAllAssetSnapshot *AllAssetSnapshot @@ -144,11 +197,6 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] sessionAssets[sessionName] = assets - if s.ShowBreakdown { - slackAttachment := assets.SlackAttachment() - slackAttachment.Title = "Session " + sessionName + " " + slackAttachment.Title - bbgo.Notify(slackAttachment) - } } totalAssets := asset.Map{} @@ -156,6 +204,8 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] totalAssets = totalAssets.Merge(assets) } + s.Environment.RecordAsset(priceTime, &bbgo.ExchangeSession{Name: "ALL"}, totalAssets) + 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 @@ -164,16 +214,37 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] return true }) - s.Environment.RecordAsset(priceTime, &bbgo.ExchangeSession{Name: "ALL"}, totalAssets) - bbgo.Notify(displayAssets) - s.lastAllAssetSnapshot = &AllAssetSnapshot{ + if s.ShowBreakdown { + 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() diff --git a/pkg/types/asset/asset.go b/pkg/types/asset/asset.go index 5b9f80efc3..778bb7408e 100644 --- a/pkg/types/asset/asset.go +++ b/pkg/types/asset/asset.go @@ -23,6 +23,7 @@ type Asset struct { // 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"` @@ -45,6 +46,7 @@ func (m Map) Merge(other Map) Map { 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) @@ -55,7 +57,7 @@ func (m Map) Merge(other Map) Map { asset.InterestInUSD = asset.InterestInUSD.Add(existing.InterestInUSD) } - m[cu] = asset + newMap[cu] = asset } return newMap From d5bc9d6869cfd6f44ac9d8688f3c1de72d40cbc3 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 16 Dec 2024 14:45:53 +0800 Subject: [PATCH 17/20] xnav: show breakdown only when multiple sessions are set --- pkg/strategy/xnav/strategy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/strategy/xnav/strategy.go b/pkg/strategy/xnav/strategy.go index 2c4695de9d..ae82b8a2fe 100644 --- a/pkg/strategy/xnav/strategy.go +++ b/pkg/strategy/xnav/strategy.go @@ -216,7 +216,7 @@ func (s *Strategy) recordNetAssetValue(ctx context.Context, sessions map[string] bbgo.Notify(displayAssets) - if s.ShowBreakdown { + if s.ShowBreakdown && len(sessionAssets) > 1 { for sessionName, assets := range sessionAssets { slackAttachment := assets.SlackAttachment() slackAttachment.Title = "Session " + sessionName + " " + slackAttachment.Title From f259f0b67f82e9b3fa77463bf09149d290c2745e Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 16 Dec 2024 14:48:53 +0800 Subject: [PATCH 18/20] autobuy: fix balance not found issue --- pkg/strategy/autobuy/strategy.go | 4 ++-- pkg/types/account.go | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/autobuy/strategy.go b/pkg/strategy/autobuy/strategy.go index 9fa59c33dd..207f14fb3c 100644 --- a/pkg/strategy/autobuy/strategy.go +++ b/pkg/strategy/autobuy/strategy.go @@ -148,9 +148,9 @@ func (s *Strategy) autobuy(ctx context.Context) { baseBalance, ok := s.Session.GetAccount().Balance(s.Market.BaseCurrency) if !ok { - log.Errorf("%s balance not found", s.Market.BaseCurrency) - return + baseBalance = types.NewZeroBalance(s.Market.BaseCurrency) } + log.Infof("balance: %s", baseBalance.String()) quoteBalance, ok := s.Session.GetAccount().Balance(s.Market.QuoteCurrency) 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 } From 93bb7a3437327035a817cb3b7b49f10ebbfe6cc3 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 16 Dec 2024 14:50:13 +0800 Subject: [PATCH 19/20] autobuy: improve balance checking --- pkg/strategy/autobuy/strategy.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pkg/strategy/autobuy/strategy.go b/pkg/strategy/autobuy/strategy.go index 207f14fb3c..2a6d09bf62 100644 --- a/pkg/strategy/autobuy/strategy.go +++ b/pkg/strategy/autobuy/strategy.go @@ -146,16 +146,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 { - baseBalance = types.NewZeroBalance(s.Market.BaseCurrency) - } + 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 } From f1161b25a7a7c68fcd385f32eb96698e8bb551c7 Mon Sep 17 00:00:00 2001 From: c9s Date: Mon, 16 Dec 2024 14:50:46 +0800 Subject: [PATCH 20/20] autobuy: handle cron error --- pkg/strategy/autobuy/strategy.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/strategy/autobuy/strategy.go b/pkg/strategy/autobuy/strategy.go index 2a6d09bf62..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