2021-10-08 06:57:44 +00:00
|
|
|
package bbgo
|
|
|
|
|
|
|
|
import (
|
2021-10-09 05:24:28 +00:00
|
|
|
"fmt"
|
2021-10-13 17:03:57 +00:00
|
|
|
"github.com/slack-go/slack"
|
2021-10-09 05:24:28 +00:00
|
|
|
"time"
|
|
|
|
|
2021-10-08 06:57:44 +00:00
|
|
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
|
|
|
"github.com/c9s/bbgo/pkg/types"
|
|
|
|
"github.com/c9s/bbgo/pkg/util"
|
|
|
|
)
|
|
|
|
|
2021-10-08 07:09:55 +00:00
|
|
|
// Profit struct stores the PnL information
|
|
|
|
type Profit struct {
|
2021-10-09 05:24:28 +00:00
|
|
|
Symbol string `json:"symbol"`
|
|
|
|
|
2021-10-08 07:09:55 +00:00
|
|
|
// Profit is the profit of this trade made. negative profit means loss.
|
|
|
|
Profit fixedpoint.Value `json:"profit" db:"profit"`
|
|
|
|
|
|
|
|
// NetProfit is (profit - trading fee)
|
|
|
|
NetProfit fixedpoint.Value `json:"netProfit" db:"net_profit"`
|
|
|
|
AverageCost fixedpoint.Value `json:"averageCost" db:"average_ost"`
|
|
|
|
|
2021-10-13 17:03:57 +00:00
|
|
|
TradeAmount fixedpoint.Value `json:"tradeAmount" db:"trade_amount"`
|
2021-10-08 11:41:39 +00:00
|
|
|
|
2021-10-08 11:47:44 +00:00
|
|
|
// ProfitMargin is a percentage of the profit and the capital amount
|
2021-10-09 05:24:28 +00:00
|
|
|
ProfitMargin fixedpoint.Value `json:"profitMargin" db:"profit_margin"`
|
2021-10-08 11:47:44 +00:00
|
|
|
|
|
|
|
// NetProfitMargin is a percentage of the net profit and the capital amount
|
|
|
|
NetProfitMargin fixedpoint.Value `json:"netProfitMargin" db:"net_profit_margin"`
|
2021-10-08 11:41:39 +00:00
|
|
|
|
2021-10-14 02:14:11 +00:00
|
|
|
QuoteCurrency string `json:"quoteCurrency" db:"quote_currency"`
|
|
|
|
BaseCurrency string `json:"baseCurrency" db:"base_currency"`
|
2021-10-08 11:41:39 +00:00
|
|
|
|
2021-10-08 07:09:55 +00:00
|
|
|
// FeeInUSD is the summed fee of this profit,
|
|
|
|
// you will need to convert the trade fee into USD since the fee currencies can be different.
|
|
|
|
FeeInUSD fixedpoint.Value `json:"feeInUSD" db:"fee_in_usd"`
|
|
|
|
Time time.Time `json:"time" db:"time"`
|
|
|
|
Strategy string `json:"strategy" db:"strategy"`
|
|
|
|
StrategyInstanceID string `json:"strategyInstanceID" db:"strategy_instance_id"`
|
|
|
|
}
|
|
|
|
|
2021-10-13 23:33:15 +00:00
|
|
|
func (p *Profit) SlackAttachment() slack.Attachment {
|
2021-10-14 02:14:11 +00:00
|
|
|
var color = pnlColor(p.Profit)
|
|
|
|
var title = fmt.Sprintf("%s PnL ", p.Symbol)
|
2021-10-14 02:13:21 +00:00
|
|
|
title += pnlEmojiMargin(p.Profit, p.ProfitMargin, defaultPnlLevelResolution) + " "
|
2021-10-14 02:07:27 +00:00
|
|
|
title += pnlSignString(p.Profit) + " " + p.QuoteCurrency
|
2021-10-13 17:03:57 +00:00
|
|
|
|
|
|
|
var fields []slack.AttachmentField
|
|
|
|
|
2021-10-13 23:33:15 +00:00
|
|
|
if p.NetProfit != 0 {
|
2021-10-13 17:03:57 +00:00
|
|
|
fields = append(fields, slack.AttachmentField{
|
|
|
|
Title: "Net Profit",
|
2021-10-14 02:13:21 +00:00
|
|
|
Value: pnlSignString(p.NetProfit) + " " + p.QuoteCurrency,
|
2021-10-13 17:03:57 +00:00
|
|
|
Short: true,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-13 23:33:15 +00:00
|
|
|
if p.ProfitMargin != 0 {
|
2021-10-13 17:03:57 +00:00
|
|
|
fields = append(fields, slack.AttachmentField{
|
|
|
|
Title: "Profit Margin",
|
|
|
|
Value: p.ProfitMargin.Percentage(),
|
|
|
|
Short: true,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-13 23:33:15 +00:00
|
|
|
if p.NetProfitMargin != 0 {
|
2021-10-13 17:03:57 +00:00
|
|
|
fields = append(fields, slack.AttachmentField{
|
|
|
|
Title: "Net Profit Margin",
|
|
|
|
Value: p.NetProfitMargin.Percentage(),
|
|
|
|
Short: true,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-13 23:33:15 +00:00
|
|
|
if p.TradeAmount != 0.0 {
|
2021-10-13 17:03:57 +00:00
|
|
|
fields = append(fields, slack.AttachmentField{
|
|
|
|
Title: "Trade Amount",
|
|
|
|
Value: p.TradeAmount.String() + " " + p.QuoteCurrency,
|
|
|
|
Short: true,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-13 23:33:15 +00:00
|
|
|
if p.FeeInUSD != 0 {
|
2021-10-13 17:03:57 +00:00
|
|
|
fields = append(fields, slack.AttachmentField{
|
|
|
|
Title: "Fee In USD",
|
|
|
|
Value: p.FeeInUSD.String() + " USD",
|
|
|
|
Short: true,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-13 23:33:15 +00:00
|
|
|
if len(p.Strategy) != 0 {
|
2021-10-13 17:03:57 +00:00
|
|
|
fields = append(fields, slack.AttachmentField{
|
|
|
|
Title: "Strategy",
|
|
|
|
Value: p.Strategy,
|
|
|
|
Short: true,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return slack.Attachment{
|
|
|
|
Color: color,
|
|
|
|
Title: title,
|
|
|
|
Fields: fields,
|
|
|
|
// Footer: "",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-13 23:33:15 +00:00
|
|
|
func (p *Profit) PlainText() string {
|
2021-10-13 23:47:55 +00:00
|
|
|
var emoji string
|
|
|
|
if p.ProfitMargin != 0 {
|
|
|
|
emoji = pnlEmojiMargin(p.Profit, p.ProfitMargin, defaultPnlLevelResolution)
|
|
|
|
} else {
|
|
|
|
emoji = pnlEmojiSimple(p.Profit)
|
|
|
|
}
|
|
|
|
|
2021-10-09 05:24:28 +00:00
|
|
|
return fmt.Sprintf("%s trade profit %s %f %s (%.2f%%), net profit =~ %f %s (%.2f%%)",
|
|
|
|
p.Symbol,
|
2021-10-13 23:47:55 +00:00
|
|
|
emoji,
|
2021-10-09 05:24:28 +00:00
|
|
|
p.Profit.Float64(), p.QuoteCurrency,
|
|
|
|
p.ProfitMargin.Float64()*100.0,
|
|
|
|
p.NetProfit.Float64(), p.QuoteCurrency,
|
|
|
|
p.NetProfitMargin.Float64()*100.0,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
var lossEmoji = "🔥"
|
|
|
|
var profitEmoji = "💰"
|
2021-10-13 23:47:55 +00:00
|
|
|
var defaultPnlLevelResolution = fixedpoint.NewFromFloat(0.001)
|
2021-10-09 05:24:28 +00:00
|
|
|
|
2021-10-14 02:07:27 +00:00
|
|
|
func pnlColor(pnl fixedpoint.Value) string {
|
|
|
|
if pnl > 0 {
|
|
|
|
return types.GreenColor
|
|
|
|
}
|
|
|
|
return types.RedColor
|
|
|
|
}
|
|
|
|
|
|
|
|
func pnlSignString(pnl fixedpoint.Value) string {
|
|
|
|
if pnl > 0 {
|
|
|
|
return "+" + pnl.String()
|
|
|
|
}
|
|
|
|
return pnl.String()
|
|
|
|
}
|
|
|
|
|
2021-10-13 23:47:55 +00:00
|
|
|
func pnlEmojiSimple(pnl fixedpoint.Value) string {
|
2021-10-09 05:24:28 +00:00
|
|
|
if pnl < 0 {
|
|
|
|
return lossEmoji
|
|
|
|
}
|
|
|
|
|
|
|
|
if pnl == 0 {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
|
|
|
|
return profitEmoji
|
|
|
|
}
|
|
|
|
|
2021-10-13 23:47:55 +00:00
|
|
|
func pnlEmojiMargin(pnl, margin, resolution fixedpoint.Value) (out string) {
|
2021-10-14 02:13:21 +00:00
|
|
|
if margin == 0 {
|
|
|
|
return pnlEmojiSimple(pnl)
|
|
|
|
}
|
|
|
|
|
2021-10-13 23:47:55 +00:00
|
|
|
if pnl < 0 {
|
|
|
|
out = lossEmoji
|
|
|
|
level := (-margin).Div(resolution).Floor()
|
|
|
|
for i := 1; i < level.Int(); i++ {
|
|
|
|
out += lossEmoji
|
|
|
|
}
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
|
|
|
if pnl == 0 {
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
|
|
|
out = profitEmoji
|
|
|
|
level := margin.Div(resolution).Floor()
|
|
|
|
for i := 1; i < level.Int(); i++ {
|
|
|
|
out += profitEmoji
|
|
|
|
}
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
2021-10-08 06:57:44 +00:00
|
|
|
type ProfitStats struct {
|
2021-10-09 05:30:02 +00:00
|
|
|
Symbol string `json:"symbol"`
|
|
|
|
QuoteCurrency string `json:"quoteCurrency"`
|
|
|
|
BaseCurrency string `json:"baseCurrency"`
|
|
|
|
|
2021-10-08 07:09:55 +00:00
|
|
|
AccumulatedPnL fixedpoint.Value `json:"accumulatedPnL,omitempty"`
|
|
|
|
AccumulatedNetProfit fixedpoint.Value `json:"accumulatedNetProfit,omitempty"`
|
|
|
|
AccumulatedProfit fixedpoint.Value `json:"accumulatedProfit,omitempty"`
|
|
|
|
AccumulatedLoss fixedpoint.Value `json:"accumulatedLoss,omitempty"`
|
2021-10-08 11:43:53 +00:00
|
|
|
AccumulatedVolume fixedpoint.Value `json:"accumulatedVolume,omitempty"`
|
2021-10-08 07:09:55 +00:00
|
|
|
AccumulatedSince int64 `json:"accumulatedSince,omitempty"`
|
|
|
|
|
|
|
|
TodayPnL fixedpoint.Value `json:"todayPnL,omitempty"`
|
|
|
|
TodayNetProfit fixedpoint.Value `json:"todayNetProfit,omitempty"`
|
|
|
|
TodayProfit fixedpoint.Value `json:"todayProfit,omitempty"`
|
|
|
|
TodayLoss fixedpoint.Value `json:"todayLoss,omitempty"`
|
|
|
|
TodaySince int64 `json:"todaySince,omitempty"`
|
2021-10-08 06:57:44 +00:00
|
|
|
}
|
|
|
|
|
2021-11-04 16:22:44 +00:00
|
|
|
func (s *ProfitStats) Init(market types.Market) {
|
|
|
|
s.Symbol = market.Symbol
|
|
|
|
s.BaseCurrency = market.BaseCurrency
|
|
|
|
s.QuoteCurrency = market.QuoteCurrency
|
|
|
|
if s.AccumulatedSince == 0 {
|
|
|
|
s.AccumulatedSince = time.Now().Unix()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-08 11:16:40 +00:00
|
|
|
func (s *ProfitStats) AddProfit(profit Profit) {
|
|
|
|
s.AccumulatedPnL += profit.Profit
|
|
|
|
s.AccumulatedNetProfit += profit.NetProfit
|
|
|
|
s.TodayPnL += profit.Profit
|
|
|
|
s.TodayNetProfit += profit.NetProfit
|
2021-10-08 06:57:44 +00:00
|
|
|
|
2021-10-08 11:16:40 +00:00
|
|
|
if profit.Profit < 0 {
|
|
|
|
s.AccumulatedLoss += profit.Profit
|
|
|
|
s.TodayLoss += profit.Profit
|
|
|
|
} else if profit.Profit > 0 {
|
|
|
|
s.AccumulatedProfit += profit.Profit
|
|
|
|
s.TodayProfit += profit.Profit
|
2021-10-08 06:57:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *ProfitStats) AddTrade(trade types.Trade) {
|
|
|
|
if s.IsOver24Hours() {
|
|
|
|
s.ResetToday()
|
|
|
|
}
|
2021-10-08 11:43:53 +00:00
|
|
|
|
|
|
|
s.AccumulatedVolume += fixedpoint.NewFromFloat(trade.Quantity)
|
2021-10-08 06:57:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (s *ProfitStats) IsOver24Hours() bool {
|
|
|
|
return time.Since(time.Unix(s.TodaySince, 0)) > 24*time.Hour
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *ProfitStats) ResetToday() {
|
|
|
|
s.TodayPnL = 0
|
|
|
|
s.TodayNetProfit = 0
|
|
|
|
s.TodayProfit = 0
|
|
|
|
s.TodayLoss = 0
|
|
|
|
|
|
|
|
var beginningOfTheDay = util.BeginningOfTheDay(time.Now().Local())
|
|
|
|
s.TodaySince = beginningOfTheDay.Unix()
|
|
|
|
}
|
2021-10-09 05:30:02 +00:00
|
|
|
|
|
|
|
func (s *ProfitStats) PlainText() string {
|
|
|
|
since := time.Unix(s.AccumulatedSince, 0).Local()
|
2021-10-14 00:55:55 +00:00
|
|
|
return fmt.Sprintf("%s Profit Today\n"+
|
2021-10-14 02:16:11 +00:00
|
|
|
"Profit %f %s\n"+
|
|
|
|
"Net profit %f %s\n"+
|
|
|
|
"Trade Loss %f %s\n"+
|
2021-10-14 00:55:55 +00:00
|
|
|
"Summary:\n"+
|
2021-10-14 02:16:11 +00:00
|
|
|
"Accumulated Profit %f %s\n"+
|
|
|
|
"Accumulated Net Profit %f %s\n"+
|
|
|
|
"Accumulated Trade Loss %f %s\n"+
|
2021-10-14 00:55:55 +00:00
|
|
|
"Since %s",
|
|
|
|
s.Symbol,
|
|
|
|
s.TodayPnL.Float64(), s.QuoteCurrency,
|
|
|
|
s.TodayNetProfit.Float64(), s.QuoteCurrency,
|
|
|
|
s.TodayLoss.Float64(), s.QuoteCurrency,
|
2021-10-09 05:30:02 +00:00
|
|
|
s.AccumulatedPnL.Float64(), s.QuoteCurrency,
|
|
|
|
s.AccumulatedNetProfit.Float64(), s.QuoteCurrency,
|
|
|
|
s.AccumulatedLoss.Float64(), s.QuoteCurrency,
|
|
|
|
since.Format(time.RFC822),
|
|
|
|
)
|
|
|
|
}
|
2021-10-13 17:21:25 +00:00
|
|
|
|
2021-10-13 23:33:15 +00:00
|
|
|
func (s *ProfitStats) SlackAttachment() slack.Attachment {
|
2021-10-14 02:07:27 +00:00
|
|
|
var color = pnlColor(s.AccumulatedPnL)
|
|
|
|
var title = fmt.Sprintf("%s Accumulated PnL %s %s", s.Symbol, pnlSignString(s.AccumulatedPnL), s.QuoteCurrency)
|
2021-10-13 17:21:25 +00:00
|
|
|
|
|
|
|
since := time.Unix(s.AccumulatedSince, 0).Local()
|
|
|
|
title += " Since " + since.Format(time.RFC822)
|
|
|
|
|
|
|
|
var fields []slack.AttachmentField
|
|
|
|
|
2021-10-18 00:45:27 +00:00
|
|
|
if s.TodayPnL != 0 {
|
|
|
|
fields = append(fields, slack.AttachmentField{
|
|
|
|
Title: "P&L Today",
|
|
|
|
Value: pnlSignString(s.TodayPnL) + " " + s.QuoteCurrency,
|
|
|
|
Short: true,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-13 23:33:15 +00:00
|
|
|
if s.TodayProfit != 0 {
|
2021-10-13 17:21:25 +00:00
|
|
|
fields = append(fields, slack.AttachmentField{
|
|
|
|
Title: "Profit Today",
|
2021-10-14 02:13:21 +00:00
|
|
|
Value: pnlSignString(s.TodayProfit) + " " + s.QuoteCurrency,
|
2021-10-13 17:21:25 +00:00
|
|
|
Short: true,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-13 23:33:15 +00:00
|
|
|
if s.TodayNetProfit != 0 {
|
2021-10-13 17:21:25 +00:00
|
|
|
fields = append(fields, slack.AttachmentField{
|
|
|
|
Title: "Net Profit Today",
|
2021-10-14 02:13:21 +00:00
|
|
|
Value: pnlSignString(s.TodayNetProfit) + " " + s.QuoteCurrency,
|
2021-10-13 17:21:25 +00:00
|
|
|
Short: true,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-13 23:33:15 +00:00
|
|
|
if s.TodayLoss != 0 {
|
2021-10-13 17:21:25 +00:00
|
|
|
fields = append(fields, slack.AttachmentField{
|
|
|
|
Title: "Loss Today",
|
2021-10-14 02:13:21 +00:00
|
|
|
Value: pnlSignString(s.TodayLoss) + " " + s.QuoteCurrency,
|
2021-10-13 17:21:25 +00:00
|
|
|
Short: true,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-18 00:45:27 +00:00
|
|
|
if s.AccumulatedPnL != 0 {
|
|
|
|
fields = append(fields, slack.AttachmentField{
|
|
|
|
Title: "Accumulated P&L",
|
|
|
|
Value: pnlSignString(s.AccumulatedPnL) + " " + s.QuoteCurrency,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-13 23:33:15 +00:00
|
|
|
if s.AccumulatedProfit != 0 {
|
2021-10-13 17:21:25 +00:00
|
|
|
fields = append(fields, slack.AttachmentField{
|
|
|
|
Title: "Accumulated Profit",
|
2021-10-14 02:13:21 +00:00
|
|
|
Value: pnlSignString(s.AccumulatedProfit) + " " + s.QuoteCurrency,
|
2021-10-13 17:21:25 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-13 23:33:15 +00:00
|
|
|
if s.AccumulatedNetProfit != 0 {
|
2021-10-13 17:21:25 +00:00
|
|
|
fields = append(fields, slack.AttachmentField{
|
|
|
|
Title: "Accumulated Net Profit",
|
2021-10-14 02:13:21 +00:00
|
|
|
Value: pnlSignString(s.AccumulatedNetProfit) + " " + s.QuoteCurrency,
|
2021-10-13 17:21:25 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-10-13 23:33:15 +00:00
|
|
|
if s.AccumulatedLoss != 0 {
|
2021-10-13 17:21:25 +00:00
|
|
|
fields = append(fields, slack.AttachmentField{
|
|
|
|
Title: "Accumulated Loss",
|
2021-10-14 02:13:21 +00:00
|
|
|
Value: pnlSignString(s.AccumulatedLoss) + " " + s.QuoteCurrency,
|
2021-10-13 17:21:25 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
return slack.Attachment{
|
|
|
|
Color: color,
|
|
|
|
Title: title,
|
|
|
|
Fields: fields,
|
|
|
|
// Footer: "",
|
|
|
|
}
|
|
|
|
}
|