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/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 { diff --git a/pkg/types/position.go b/pkg/types/position.go index 2d71ee740..52cdc8617 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" @@ -43,16 +44,16 @@ 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"` // 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"` @@ -277,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"` @@ -306,7 +303,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), } } @@ -315,7 +315,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), } } @@ -345,6 +348,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 } @@ -489,6 +496,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 @@ -502,6 +537,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) } @@ -513,27 +549,15 @@ 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) } } 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() @@ -551,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 @@ -564,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 } @@ -578,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 @@ -593,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 @@ -605,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 } @@ -619,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) @@ -635,3 +655,18 @@ func (p *Position) AddTrade(td Trade) (profit fixedpoint.Value, netProfit fixedp return fixedpoint.Zero, fixedpoint.Zero, false } + +func (p *Position) updateMetrics() { + // update the position metrics only if the position defines the strategy ID + 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..e3523f895 --- /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_quote_qty", + Help: "bbgo position quote quantity metrics", + }, []string{"strategy_id", "strategy_type", "symbol"}) + +func init() { + prometheus.MustRegister( + positionAverageCostMetrics, + positionBaseQuantityMetrics, + positionQuoteQuantityMetrics, + ) +}