use position to calculate the pnl

This commit is contained in:
c9s 2021-12-05 02:16:48 +08:00
parent 9d38dc2c87
commit df683bdf56
7 changed files with 87 additions and 90 deletions

View File

@ -25,12 +25,14 @@ riskControls:
minBaseAssetBalance: 0.0
maxOrderAmount: 1000.0
# example command:
# godotenv -f .env.local -- go run ./cmd/bbgo backtest --exchange max --sync-from 2020-11-01 --config config/grid.yaml --base-asset-baseline
backtest:
# for testing max draw down (MDD) at 03-12
# see here for more details
# https://www.investopedia.com/terms/m/maximum-drawdown-mdd.asp
startTime: "2020-10-01"
endTime: "2021-10-01"
startTime: "2021-01-10"
endTime: "2021-01-21"
symbols:
- BTCUSDT
account:
@ -43,15 +45,15 @@ exchangeStrategies:
- on: max
grid:
symbol: BTCUSDT
# quantity: 0.001
scaleQuantity:
byPrice:
exp:
domain: [20_000, 30_000]
range: [0.2, 0.001]
quantity: 0.001
# scaleQuantity:
# byPrice:
# exp:
# domain: [20_000, 30_000]
# range: [0.2, 0.001]
gridNumber: 30
profitSpread: 50.0
upperPrice: 20_000.0
lowerPrice: 30_000.0
upperPrice: 50_000.0
lowerPrice: 20_000.0
long: true

View File

@ -1,26 +1,23 @@
package pnl
import (
"strings"
"time"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/types"
)
type AverageCostCalculator struct {
TradingFeeCurrency string
Market types.Market
}
func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, currentPrice float64) *AverageCostPnlReport {
// copy trades, so that we can truncate it.
var bidVolume = 0.0
var bidAmount = 0.0
var askVolume = 0.0
var feeUSD = 0.0
var bidFeeUSD = 0.0
var feeRate = 0.0015
if len(trades) == 0 {
return &AverageCostPnlReport{
@ -35,64 +32,41 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c
var currencyFees = map[string]float64{}
var position = bbgo.NewPositionFromMarket(c.Market)
position.SetFeeRate(bbgo.ExchangeFee{
// binance vip 0 uses 0.075%
MakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01),
TakerFeeRate: fixedpoint.NewFromFloat(0.075 * 0.01),
})
// TODO: configure the exchange fee rate here later
// position.SetExchangeFeeRate()
var totalProfit fixedpoint.Value
var totalNetProfit fixedpoint.Value
for _, trade := range trades {
if trade.Symbol == symbol {
if trade.IsBuyer && trade.Side != types.SideTypeSelf {
profit, netProfit, madeProfit := position.AddTrade(trade)
if madeProfit {
totalProfit += profit
totalNetProfit += netProfit
}
if trade.IsBuyer {
bidVolume += trade.Quantity
bidAmount += trade.Price * trade.Quantity
}
// since we use USDT as the quote currency, we simply check if it matches the currency symbol
if strings.HasPrefix(trade.Symbol, trade.FeeCurrency) {
bidVolume -= trade.Fee
feeUSD += trade.Price * trade.Fee
if trade.IsBuyer {
bidFeeUSD += trade.Price * trade.Fee
}
} else if trade.FeeCurrency == "USDT" {
feeUSD += trade.Fee
if trade.IsBuyer {
bidFeeUSD += trade.Fee
}
}
} else {
if trade.FeeCurrency == c.TradingFeeCurrency {
bidVolume -= trade.Fee
} else {
askVolume += trade.Quantity
}
}
if _, ok := currencyFees[trade.FeeCurrency]; !ok {
currencyFees[trade.FeeCurrency] = 0.0
currencyFees[trade.FeeCurrency] = trade.Fee
} else {
currencyFees[trade.FeeCurrency] += trade.Fee
}
currencyFees[trade.FeeCurrency] += trade.Fee
}
profit := 0.0
averageCost := (bidAmount + bidFeeUSD) / bidVolume
for _, t := range trades {
if t.Symbol != symbol {
continue
}
if t.IsBuyer || t.Side == types.SideTypeSelf {
continue
}
profit += (t.Price - averageCost) * t.Quantity
askVolume += t.Quantity
}
profit -= feeUSD
unrealizedProfit := profit
stock := bidVolume - askVolume
if stock > 0 {
stockFee := currentPrice * stock * feeRate
unrealizedProfit += (currentPrice-averageCost)*stock - stockFee
}
unrealizedProfit := (fixedpoint.NewFromFloat(currentPrice) - position.AverageCost).Mul(position.Base)
return &AverageCostPnlReport{
Symbol: symbol,
CurrentPrice: currentPrice,
@ -102,11 +76,12 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c
BuyVolume: bidVolume,
SellVolume: askVolume,
Stock: stock,
Profit: profit,
Stock: position.Base.Float64(),
Profit: totalProfit,
NetProfit: totalNetProfit,
UnrealizedProfit: unrealizedProfit,
AverageBidCost: averageCost,
FeeInUSD: feeUSD,
AverageBidCost: position.AverageCost.Float64(),
FeeInUSD: (totalProfit - totalNetProfit).Float64(),
CurrencyFees: currencyFees,
}
}

View File

@ -4,6 +4,7 @@ import (
"strconv"
"time"
"github.com/c9s/bbgo/pkg/fixedpoint"
log "github.com/sirupsen/logrus"
"github.com/slack-go/slack"
@ -18,8 +19,7 @@ type AverageCostPnlReport struct {
Market types.Market
NumTrades int
Profit float64
UnrealizedProfit float64
Profit, NetProfit, UnrealizedProfit fixedpoint.Value
AverageBidCost float64
BuyVolume float64
SellVolume float64
@ -35,14 +35,16 @@ func (report AverageCostPnlReport) Print() {
log.Infof("TOTAL BUY VOLUME: %f", report.BuyVolume)
log.Infof("TOTAL SELL VOLUME: %f", report.SellVolume)
log.Infof("STOCK: %f", report.Stock)
log.Infof("FEE (USD): %f", report.FeeInUSD)
// FIXME:
// log.Infof("FEE (USD): %f", report.FeeInUSD)
log.Infof("CURRENT PRICE: %s", types.USD.FormatMoneyFloat64(report.CurrentPrice))
log.Infof("CURRENCY FEES:")
for currency, fee := range report.CurrencyFees {
log.Infof(" - %s: %f", currency, fee)
}
log.Infof("PROFIT: %s", types.USD.FormatMoneyFloat64(report.Profit))
log.Infof("UNREALIZED PROFIT: %s", types.USD.FormatMoneyFloat64(report.UnrealizedProfit))
log.Infof("PROFIT: %s", types.USD.FormatMoneyFloat64(report.Profit.Float64()))
log.Infof("UNREALIZED PROFIT: %s", types.USD.FormatMoneyFloat64(report.UnrealizedProfit.Float64()))
}
func (report AverageCostPnlReport) SlackAttachment() slack.Attachment {
@ -63,7 +65,9 @@ func (report AverageCostPnlReport) SlackAttachment() slack.Attachment {
{Title: "Unrealized Profit", Value: types.USD.FormatMoney(report.UnrealizedProfit)},
{Title: "Current Price", Value: report.Market.FormatPrice(report.CurrentPrice), Short: true},
{Title: "Average Cost", Value: report.Market.FormatPrice(report.AverageBidCost), Short: true},
{Title: "Fee (USD)", Value: types.USD.FormatMoney(report.FeeInUSD), Short: true},
// FIXME:
// {Title: "Fee (USD)", Value: types.USD.FormatMoney(report.FeeInUSD), Short: true},
{Title: "Stock", Value: strconv.FormatFloat(report.Stock, 'f', 8, 64), Short: true},
{Title: "Number of Trades", Value: strconv.Itoa(report.NumTrades), Short: true},
},

View File

@ -18,7 +18,6 @@ import (
"github.com/spf13/viper"
"gopkg.in/tucnak/telebot.v2"
"github.com/c9s/bbgo/pkg/accounting/pnl"
"github.com/c9s/bbgo/pkg/cmd/cmdutil"
"github.com/c9s/bbgo/pkg/notifier/slacknotifier"
"github.com/c9s/bbgo/pkg/notifier/telegramnotifier"
@ -398,7 +397,9 @@ func (environ *Environment) ConfigureNotificationRouting(conf *NotificationConfi
}
// currently not used
// currently, not used
// FIXME: this is causing cyclic import
/*
switch conf.Routing.PnL {
case "$symbol":
environ.ObjectChannelRouter.Route(func(obj interface{}) (channel string, ok bool) {
@ -410,6 +411,7 @@ func (environ *Environment) ConfigureNotificationRouting(conf *NotificationConfi
return
})
}
*/
}
return nil

View File

@ -31,6 +31,7 @@ type Position struct {
// This is used for calculating net profit
ApproximateAverageCost fixedpoint.Value `json:"approximateAverageCost"`
FeeRate *ExchangeFee `json:"feeRate,omitempty"`
ExchangeFeeRates map[types.ExchangeName]ExchangeFee `json:"exchangeFeeRates"`
sync.Mutex
@ -59,6 +60,10 @@ func (p *Position) Reset() {
p.AverageCost = 0
}
func (p *Position) SetFeeRate(exchangeFee ExchangeFee) {
p.FeeRate = &exchangeFee
}
func (p *Position) SetExchangeFeeRate(ex types.ExchangeName, exchangeFee ExchangeFee) {
if p.ExchangeFeeRates == nil {
p.ExchangeFeeRates = make(map[types.ExchangeName]ExchangeFee)
@ -168,6 +173,12 @@ func (p *Position) AddTrade(t types.Trade) (profit fixedpoint.Value, netProfit f
feeInQuote += exchangeFee.TakerFeeRate.Mul(quoteQuantity)
}
}
} else if p.FeeRate != nil {
if t.IsMaker {
feeInQuote += p.FeeRate.MakerFeeRate.Mul(quoteQuantity)
} else {
feeInQuote += p.FeeRate.TakerFeeRate.Mul(quoteQuantity)
}
}
}

View File

@ -4,8 +4,6 @@ import (
"regexp"
"github.com/robfig/cron/v3"
"github.com/c9s/bbgo/pkg/accounting/pnl"
)
type PnLReporter interface {
@ -67,17 +65,20 @@ func (reporter *AverageCostPnLReporter) When(specs ...string) *AverageCostPnLRep
}
func (reporter *AverageCostPnLReporter) Run() {
for _, sessionName := range reporter.Sessions {
session := reporter.environment.sessions[sessionName]
calculator := &pnl.AverageCostCalculator{
TradingFeeCurrency: session.Exchange.PlatformFeeCurrency(),
}
// FIXME: this is causing cyclic import
/*
for _, sessionName := range reporter.Sessions {
session := reporter.environment.sessions[sessionName]
calculator := &pnl.AverageCostCalculator{
TradingFeeCurrency: session.Exchange.PlatformFeeCurrency(),
}
for _, symbol := range reporter.Symbols {
report := calculator.Calculate(symbol, session.Trades[symbol].Copy(), session.lastPrices[symbol])
report.Print()
for _, symbol := range reporter.Symbols {
report := calculator.Calculate(symbol, session.Trades[symbol].Copy(), session.lastPrices[symbol])
report.Print()
}
}
}
*/
}
type PatternChannelRouter struct {

View File

@ -262,15 +262,17 @@ var BacktestCmd = &cobra.Command{
// put the logger back to print the pnl
log.SetLevel(log.InfoLevel)
for _, session := range environ.Sessions() {
calculator := &pnl.AverageCostCalculator{
TradingFeeCurrency: backtestExchange.PlatformFeeCurrency(),
}
for symbol, trades := range session.Trades {
market, ok := session.Market(symbol)
if !ok {
return fmt.Errorf("market not found: %s", symbol)
}
calculator := &pnl.AverageCostCalculator{
TradingFeeCurrency: backtestExchange.PlatformFeeCurrency(),
Market: market,
}
startPrice, ok := session.StartPrice(symbol)
if !ok {
return fmt.Errorf("start price not found: %s", symbol)