Skip to content

Commit

Permalink
Merge pull request #1866 from c9s/c9s/xnav/breakdown
Browse files Browse the repository at this point in the history
FEATURE: [xnav] support breakdown and improve price solving logics
  • Loading branch information
c9s authored Dec 16, 2024
2 parents 49d1188 + f1161b2 commit c842609
Show file tree
Hide file tree
Showing 29 changed files with 605 additions and 400 deletions.
19 changes: 10 additions & 9 deletions pkg/accounting/pnl/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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))
}
}

Expand All @@ -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},

Expand Down
7 changes: 4 additions & 3 deletions pkg/bbgo/account_value_calc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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())
}
}
Expand All @@ -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
Expand Down Expand Up @@ -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())
Expand Down
3 changes: 2 additions & 1 deletion pkg/bbgo/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
googleservice "github.com/c9s/bbgo/pkg/service/google"
"github.com/c9s/bbgo/pkg/slack/slacklog"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/types/asset"
"github.com/c9s/bbgo/pkg/util"
)

Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions pkg/bbgo/order_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand Down
55 changes: 22 additions & 33 deletions pkg/bbgo/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -202,6 +203,14 @@ func NewExchangeSession(name string, exchange types.Exchange) *ExchangeSession {
return session
}

func (session *ExchangeSession) GetPriceSolver() *pricesolver.SimplePriceSolver {
if session.priceSolver == nil {
session.priceSolver = pricesolver.NewSimplePriceResolver(session.markets)
}

return session.priceSolver
}

func (session *ExchangeSession) GetAccountLabel() string {
var label string

Expand Down Expand Up @@ -764,16 +773,12 @@ func (session *ExchangeSession) FormatOrder(order types.SubmitOrder) (types.Subm
}

func (session *ExchangeSession) UpdatePrices(ctx context.Context, currencies []string, fiat string) (err error) {
// TODO: move this cache check to the http routes
// if session.lastPriceUpdatedAt.After(time.Now().Add(-time.Hour)) {
// return nil
// }

markets := session.Markets()
var symbols []string
symbols := make([]string, 0, 50)
for _, c := range currencies {
possibleSymbols := findPossibleMarketSymbols(markets, c, fiat)
symbols = append(symbols, possibleSymbols...)
for symbol := range markets.FindAssetMarkets(c) {
symbols = append(symbols, symbol)
}
}

if len(symbols) == 0 {
Expand All @@ -785,15 +790,20 @@ func (session *ExchangeSession) UpdatePrices(ctx context.Context, currencies []s
return err
}

priceSolver := session.GetPriceSolver()

var lastTime time.Time
for k, v := range tickers {
validPrice := v.GetValidPrice()
priceSolver.Update(k, validPrice)

// for {Crypto}/USDT markets
// map things like BTCUSDT = {price}
if market, ok := markets[k]; ok {
if types.IsFiatCurrency(market.BaseCurrency) {
session.lastPrices[k] = v.Last.Div(fixedpoint.One)
if currency2.IsFiatCurrency(market.BaseCurrency) {
session.lastPrices[k] = validPrice.Div(fixedpoint.One)
} else {
session.lastPrices[k] = v.Last
session.lastPrices[k] = validPrice
}
} else {
session.lastPrices[k] = v.Last
Expand All @@ -819,7 +829,7 @@ func (session *ExchangeSession) FindPossibleAssetSymbols() (symbols []string, er
var balances = session.GetAccount().Balances()
var fiatAssets []string

for _, currency := range types.FiatCurrencies {
for _, currency := range currency2.FiatCurrencies {
if balance, ok := balances[currency]; ok && balance.Total().Sign() > 0 {
fiatAssets = append(fiatAssets, currency)
}
Expand Down Expand Up @@ -1115,24 +1125,3 @@ func (session *ExchangeSession) FormatOrders(orders []types.SubmitOrder) (format

return formattedOrders, err
}

func findPossibleMarketSymbols(markets types.MarketMap, c, fiat string) (symbols []string) {
var tries []string
// expand USD stable coin currencies
if types.IsUSDFiatCurrency(fiat) {
for _, usdFiat := range types.USDFiatCurrencies {
tries = append(tries, c+usdFiat, usdFiat+c)
}
} else {
tries = []string{c + fiat, fiat + c}
}

for _, try := range tries {
if markets.Has(try) {
symbols = append(symbols, try)
break
}
}

return symbols
}
43 changes: 0 additions & 43 deletions pkg/bbgo/session_test.go
Original file line number Diff line number Diff line change
@@ -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])
}
})
}
17 changes: 14 additions & 3 deletions pkg/notifier/slacknotifier/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ var emailRegExp = regexp.MustCompile("`^(?P<name>[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

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -468,7 +476,10 @@ func (n *Notifier) NotifyTo(channel string, obj interface{}, args ...interface{}
case slack.Attachment:
opts = append(opts, slack.MsgOptionAttachments(append([]slack.Attachment{a}, slackAttachments...)...))

case types.SlackAttachmentCreator:
case *slack.Attachment:
opts = append(opts, slack.MsgOptionAttachments(append([]slack.Attachment{*a}, slackAttachments...)...))

case SlackAttachmentCreator:
// convert object to slack attachment (if supported)
opts = append(opts, slack.MsgOptionAttachments(append([]slack.Attachment{a.SlackAttachment()}, slackAttachments...)...))

Expand Down
7 changes: 4 additions & 3 deletions pkg/server/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/service"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/types/asset"
)

const DefaultBindAddress = "localhost:8080"
Expand Down Expand Up @@ -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())},
Expand Down Expand Up @@ -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()

Expand Down
16 changes: 11 additions & 5 deletions pkg/service/account.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package service

import (
"github.com/c9s/bbgo/pkg/types"
"time"

"github.com/jmoiron/sqlx"
"go.uber.org/multierr"
"time"

"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/types/asset"
)

type AccountService struct {
Expand All @@ -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
Expand Down Expand Up @@ -46,8 +52,8 @@ func (s *AccountService) InsertAsset(time time.Time, session string, name types.
account,
time,
v.Currency,
v.InUSD,
v.InBTC,
v.NetAssetInUSD,
v.NetAssetInBTC,
v.Total,
v.Available,
v.Locked,
Expand Down
Loading

0 comments on commit c842609

Please sign in to comment.