From 9136877207d1cf1b72d3a5448bcce37ce79adb7c Mon Sep 17 00:00:00 2001 From: c9s Date: Wed, 21 Aug 2024 16:35:57 +0800 Subject: [PATCH 1/5] types: update position metrics after adding trades --- pkg/types/position.go | 17 +++++++++++++++++ pkg/types/position_metrics.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 pkg/types/position_metrics.go diff --git a/pkg/types/position.go b/pkg/types/position.go index 2d71ee740..60b938eab 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -5,6 +5,7 @@ import ( "sync" "time" + "github.com/prometheus/client_golang/prometheus" "github.com/slack-go/slack" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -534,6 +535,8 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp p.Lock() defer p.Unlock() + defer p.updateMetrics() + // update changedAt field before we unlock in the defer func defer func() { p.ChangedAt = td.Time.Time() @@ -635,3 +638,17 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp return fixedpoint.Zero, fixedpoint.Zero, false } + +func (p *Position) updateMetrics() { + if p.StrategyInstanceID == "" || p.Strategy == "" { + return + } + + labels := prometheus.Labels{ + "strategy_id": p.StrategyInstanceID, + "strategy_type": p.Strategy, + } + positionAverageCostMetrics.With(labels).Set(p.AverageCost.Float64()) + positionBaseQuantityMetrics.With(labels).Set(p.Base.Float64()) + positionQuoteQuantityMetrics.With(labels).Set(p.Quote.Float64()) +} diff --git a/pkg/types/position_metrics.go b/pkg/types/position_metrics.go new file mode 100644 index 000000000..7384ae0fe --- /dev/null +++ b/pkg/types/position_metrics.go @@ -0,0 +1,29 @@ +package types + +import "github.com/prometheus/client_golang/prometheus" + +var positionAverageCostMetrics = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "bbgo_position_avg_cost", + Help: "bbgo position average cost metrics", + }, []string{"strategy_id", "strategy_type", "symbol"}) + +var positionBaseQuantityMetrics = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "bbgo_position_base_qty", + Help: "bbgo position base quantity metrics", + }, []string{"strategy_id", "strategy_type", "symbol"}) + +var positionQuoteQuantityMetrics = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "bbgo_position_base_qty", + Help: "bbgo position base quantity metrics", + }, []string{"strategy_id", "strategy_type", "symbol"}) + +func init() { + prometheus.MustRegister( + positionAverageCostMetrics, + positionBaseQuantityMetrics, + positionQuoteQuantityMetrics, + ) +} From e2d68f2a861da85f54f408424c0d2f435dc44480 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 22 Aug 2024 10:59:38 +0800 Subject: [PATCH 2/5] types: add fee cost settter to the position --- pkg/types/position.go | 19 +++++++++++++++++-- pkg/types/position_metrics.go | 4 ++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/pkg/types/position.go b/pkg/types/position.go index 60b938eab..10ff98e7e 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -54,6 +54,10 @@ type Position struct { // TotalFee stores the fee currency -> total fee quantity TotalFee map[string]fixedpoint.Value `json:"totalFee" db:"-"` + // FeeAverageCosts stores the fee currency -> average cost of the fee + // e.g. BNB -> 341.0 + FeeAverageCosts map[string]fixedpoint.Value `json:"feeAverageCosts" db:"-"` + OpenedAt time.Time `json:"openedAt,omitempty" db:"-"` ChangedAt time.Time `json:"changedAt,omitempty" db:"changed_at"` @@ -307,7 +311,10 @@ func NewPositionFromMarket(market Market) *Position { BaseCurrency: market.BaseCurrency, QuoteCurrency: market.QuoteCurrency, Market: market, - TotalFee: make(map[string]fixedpoint.Value), + + FeeAverageCosts: make(map[string]fixedpoint.Value), + TotalFee: make(map[string]fixedpoint.Value), + ExchangeFeeRates: make(map[ExchangeName]ExchangeFee), } } @@ -316,7 +323,10 @@ func NewPosition(symbol, base, quote string) *Position { Symbol: symbol, BaseCurrency: base, QuoteCurrency: quote, - TotalFee: make(map[string]fixedpoint.Value), + + TotalFee: make(map[string]fixedpoint.Value), + FeeAverageCosts: make(map[string]fixedpoint.Value), + ExchangeFeeRates: make(map[ExchangeName]ExchangeFee), } } @@ -346,6 +356,10 @@ func (p *Position) SetExchangeFeeRate(ex ExchangeName, exchangeFee ExchangeFee) p.ExchangeFeeRates[ex] = exchangeFee } +func (p *Position) SetFeeAverageCost(currency string, cost fixedpoint.Value) { + p.FeeAverageCosts[currency] = cost +} + func (p *Position) IsShort() bool { return p.Base.Sign() < 0 } @@ -640,6 +654,7 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp } func (p *Position) updateMetrics() { + // update the position metrics only if the position defines the strategy ID if p.StrategyInstanceID == "" || p.Strategy == "" { return } diff --git a/pkg/types/position_metrics.go b/pkg/types/position_metrics.go index 7384ae0fe..e3523f895 100644 --- a/pkg/types/position_metrics.go +++ b/pkg/types/position_metrics.go @@ -16,8 +16,8 @@ var positionBaseQuantityMetrics = prometheus.NewGaugeVec( var positionQuoteQuantityMetrics = prometheus.NewGaugeVec( prometheus.GaugeOpts{ - Name: "bbgo_position_base_qty", - Help: "bbgo position base quantity metrics", + Name: "bbgo_position_quote_qty", + Help: "bbgo position quote quantity metrics", }, []string{"strategy_id", "strategy_type", "symbol"}) func init() { From 5635e3148768d482ab58ca78fe3befa283bfd155 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 22 Aug 2024 11:07:45 +0800 Subject: [PATCH 3/5] types: pull out calculateFeeInQuote method --- pkg/types/position.go | 45 ++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/pkg/types/position.go b/pkg/types/position.go index 10ff98e7e..a573732e5 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -504,6 +504,34 @@ func (p *Position) AddTrades(trades []Trade) (fixedpoint.Value, fixedpoint.Value return totalProfitAmount, totalNetProfit, !totalProfitAmount.IsZero() } +func (p *Position) calculateFeeInQuote(td Trade) fixedpoint.Value { + var quoteQuantity = td.QuoteQuantity + + if cost, ok := p.FeeAverageCosts[td.FeeCurrency]; ok { + return td.Fee.Mul(cost) + } + + if p.ExchangeFeeRates != nil { + if exchangeFee, ok := p.ExchangeFeeRates[td.Exchange]; ok { + if td.IsMaker { + return exchangeFee.MakerFeeRate.Mul(quoteQuantity) + } else { + return exchangeFee.TakerFeeRate.Mul(quoteQuantity) + } + } + } + + if p.FeeRate != nil { + if td.IsMaker { + return p.FeeRate.MakerFeeRate.Mul(quoteQuantity) + } else { + return p.FeeRate.TakerFeeRate.Mul(quoteQuantity) + } + } + + return fixedpoint.Zero +} + func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedpoint.Value, madeProfit bool) { price := td.Price quantity := td.Quantity @@ -517,6 +545,7 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp switch td.FeeCurrency { case p.BaseCurrency: + // USD-M futures use the quote currency as the fee currency. if !td.IsFutures { quantity = quantity.Sub(fee) } @@ -528,21 +557,7 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp default: if !td.Fee.IsZero() { - if p.ExchangeFeeRates != nil { - if exchangeFee, ok := p.ExchangeFeeRates[td.Exchange]; ok { - if td.IsMaker { - feeInQuote = feeInQuote.Add(exchangeFee.MakerFeeRate.Mul(quoteQuantity)) - } else { - feeInQuote = feeInQuote.Add(exchangeFee.TakerFeeRate.Mul(quoteQuantity)) - } - } - } else if p.FeeRate != nil { - if td.IsMaker { - feeInQuote = feeInQuote.Add(p.FeeRate.MakerFeeRate.Mul(quoteQuantity)) - } else { - feeInQuote = feeInQuote.Add(p.FeeRate.TakerFeeRate.Mul(quoteQuantity)) - } - } + feeInQuote = p.calculateFeeInQuote(td) } } From a900c72032de784385ad49e883d390ab856e51ea Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 22 Aug 2024 11:15:42 +0800 Subject: [PATCH 4/5] types/position: drop approximateAverageCost --- pkg/exchange/binance/convert_futures.go | 12 +++++------ pkg/strategy/drift/strategy.go | 4 ++-- pkg/types/position.go | 28 +++++++------------------ 3 files changed, 16 insertions(+), 28 deletions(-) diff --git a/pkg/exchange/binance/convert_futures.go b/pkg/exchange/binance/convert_futures.go index 9811e62d2..f40b53255 100644 --- a/pkg/exchange/binance/convert_futures.go +++ b/pkg/exchange/binance/convert_futures.go @@ -2,9 +2,10 @@ package binance import ( "fmt" - "github.com/c9s/bbgo/pkg/exchange/binance/binanceapi" "time" + "github.com/c9s/bbgo/pkg/exchange/binance/binanceapi" + "github.com/adshao/go-binance/v2/futures" "github.com/pkg/errors" @@ -42,11 +43,10 @@ func toGlobalFuturesPositions(futuresPositions []*binanceapi.FuturesAccountPosit retFuturesPositions := make(types.FuturesPositionMap) for _, futuresPosition := range futuresPositions { retFuturesPositions[futuresPosition.Symbol] = types.FuturesPosition{ // TODO: types.FuturesPosition - Isolated: futuresPosition.Isolated, - AverageCost: fixedpoint.MustNewFromString(futuresPosition.EntryPrice), - ApproximateAverageCost: fixedpoint.MustNewFromString(futuresPosition.EntryPrice), - Base: fixedpoint.MustNewFromString(futuresPosition.PositionAmt), - Quote: fixedpoint.MustNewFromString(futuresPosition.Notional), + Isolated: futuresPosition.Isolated, + AverageCost: fixedpoint.MustNewFromString(futuresPosition.EntryPrice), + Base: fixedpoint.MustNewFromString(futuresPosition.PositionAmt), + Quote: fixedpoint.MustNewFromString(futuresPosition.Notional), PositionRisk: &types.PositionRisk{ Leverage: fixedpoint.MustNewFromString(futuresPosition.Leverage), diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index cf9c53e8e..fe8e9a19d 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -854,12 +854,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.highestPrice = 0 s.lowestPrice = 0 } else if s.Position.IsLong() { - s.buyPrice = s.Position.ApproximateAverageCost.Float64() + s.buyPrice = s.Position.AverageCost.Float64() s.sellPrice = 0 s.highestPrice = math.Max(s.buyPrice, s.highestPrice) s.lowestPrice = s.buyPrice } else if s.Position.IsShort() { - s.sellPrice = s.Position.ApproximateAverageCost.Float64() + s.sellPrice = s.Position.AverageCost.Float64() s.buyPrice = 0 s.highestPrice = s.sellPrice if s.lowestPrice == 0 { diff --git a/pkg/types/position.go b/pkg/types/position.go index a573732e5..52cdc8617 100644 --- a/pkg/types/position.go +++ b/pkg/types/position.go @@ -44,10 +44,6 @@ type Position struct { Quote fixedpoint.Value `json:"quote" db:"quote"` AverageCost fixedpoint.Value `json:"averageCost" db:"average_cost"` - // ApproximateAverageCost adds the computed fee in quote in the average cost - // This is used for calculating net profit - ApproximateAverageCost fixedpoint.Value `json:"approximateAverageCost"` - FeeRate *ExchangeFee `json:"feeRate,omitempty"` ExchangeFeeRates map[ExchangeName]ExchangeFee `json:"exchangeFeeRates"` @@ -282,10 +278,6 @@ type FuturesPosition struct { Quote fixedpoint.Value `json:"quote"` AverageCost fixedpoint.Value `json:"averageCost"` - // ApproximateAverageCost adds the computed fee in quote in the average cost - // This is used for calculating net profit - ApproximateAverageCost fixedpoint.Value `json:"approximateAverageCost"` - FeeRate *ExchangeFee `json:"feeRate,omitempty"` ExchangeFeeRates map[ExchangeName]ExchangeFee `json:"exchangeFeeRates"` @@ -583,11 +575,10 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp // convert short position to long position if p.Base.Add(quantity).Sign() > 0 { profit = p.AverageCost.Sub(price).Mul(p.Base.Neg()) - netProfit = p.ApproximateAverageCost.Sub(price).Mul(p.Base.Neg()).Sub(feeInQuote) + netProfit = p.AverageCost.Sub(price).Mul(p.Base.Neg()).Sub(feeInQuote) p.Base = p.Base.Add(quantity) p.Quote = p.Quote.Sub(quoteQuantity) p.AverageCost = price - p.ApproximateAverageCost = price p.AccumulatedProfit = p.AccumulatedProfit.Add(profit) p.OpenedAt = td.Time.Time() return profit, netProfit, true @@ -596,7 +587,7 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp p.Base = p.Base.Add(quantity) p.Quote = p.Quote.Sub(quoteQuantity) profit = p.AverageCost.Sub(price).Mul(quantity) - netProfit = p.ApproximateAverageCost.Sub(price).Mul(quantity).Sub(feeInQuote) + netProfit = p.AverageCost.Sub(price).Mul(quantity).Sub(feeInQuote) p.AccumulatedProfit = p.AccumulatedProfit.Add(profit) return profit, netProfit, true } @@ -610,11 +601,12 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp // here the case is: base == 0 or base > 0 divisor := p.Base.Add(quantity) - p.ApproximateAverageCost = p.ApproximateAverageCost.Mul(p.Base). + + p.AverageCost = p.AverageCost.Mul(p.Base). Add(quoteQuantity). Add(feeInQuote). Div(divisor) - p.AverageCost = p.AverageCost.Mul(p.Base).Add(quoteQuantity).Div(divisor) + p.Base = p.Base.Add(quantity) p.Quote = p.Quote.Sub(quoteQuantity) return fixedpoint.Zero, fixedpoint.Zero, false @@ -625,11 +617,10 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp // convert long position to short position if p.Base.Compare(quantity) < 0 { profit = price.Sub(p.AverageCost).Mul(p.Base) - netProfit = price.Sub(p.ApproximateAverageCost).Mul(p.Base).Sub(feeInQuote) + netProfit = price.Sub(p.AverageCost).Mul(p.Base).Sub(feeInQuote) p.Base = p.Base.Sub(quantity) p.Quote = p.Quote.Add(quoteQuantity) p.AverageCost = price - p.ApproximateAverageCost = price p.AccumulatedProfit = p.AccumulatedProfit.Add(profit) p.OpenedAt = td.Time.Time() return profit, netProfit, true @@ -637,7 +628,7 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp p.Base = p.Base.Sub(quantity) p.Quote = p.Quote.Add(quoteQuantity) profit = price.Sub(p.AverageCost).Mul(quantity) - netProfit = price.Sub(p.ApproximateAverageCost).Mul(quantity).Sub(feeInQuote) + netProfit = price.Sub(p.AverageCost).Mul(quantity).Sub(feeInQuote) p.AccumulatedProfit = p.AccumulatedProfit.Add(profit) return profit, netProfit, true } @@ -651,13 +642,10 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp // handling short position, since Base here is negative we need to reverse the sign divisor := quantity.Sub(p.Base) - p.ApproximateAverageCost = p.ApproximateAverageCost.Mul(p.Base.Neg()). - Add(quoteQuantity). - Sub(feeInQuote). - Div(divisor) p.AverageCost = p.AverageCost.Mul(p.Base.Neg()). Add(quoteQuantity). + Sub(feeInQuote). Div(divisor) p.Base = p.Base.Sub(quantity) p.Quote = p.Quote.Add(quoteQuantity) From 72575e3cd8459515cd548e1a52793d0c34eff485 Mon Sep 17 00:00:00 2001 From: c9s Date: Thu, 22 Aug 2024 11:26:46 +0800 Subject: [PATCH 5/5] elliottwave: use AverageCost instead --- pkg/strategy/elliottwave/strategy.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/strategy/elliottwave/strategy.go b/pkg/strategy/elliottwave/strategy.go index c307f054c..b4dc7761f 100644 --- a/pkg/strategy/elliottwave/strategy.go +++ b/pkg/strategy/elliottwave/strategy.go @@ -354,12 +354,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.highestPrice = 0 s.lowestPrice = 0 } else if s.Position.IsLong() { - s.buyPrice = s.Position.ApproximateAverageCost.Float64() + s.buyPrice = s.Position.AverageCost.Float64() s.sellPrice = 0 s.highestPrice = math.Max(s.buyPrice, s.highestPrice) s.lowestPrice = 0 } else { - s.sellPrice = s.Position.ApproximateAverageCost.Float64() + s.sellPrice = s.Position.AverageCost.Float64() s.buyPrice = 0 s.highestPrice = 0 if s.lowestPrice == 0 {