mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 14:55:16 +00:00
move profit struct into the types package
This commit is contained in:
parent
2bcd3fce45
commit
9e0df77a36
|
@ -1,343 +1,2 @@
|
||||||
package bbgo
|
package bbgo
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/slack-go/slack"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
|
||||||
"github.com/c9s/bbgo/pkg/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Profit struct stores the PnL information
|
|
||||||
type Profit struct {
|
|
||||||
Symbol string `json:"symbol"`
|
|
||||||
|
|
||||||
// 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"`
|
|
||||||
|
|
||||||
TradeAmount fixedpoint.Value `json:"tradeAmount" db:"trade_amount"`
|
|
||||||
|
|
||||||
// ProfitMargin is a percentage of the profit and the capital amount
|
|
||||||
ProfitMargin fixedpoint.Value `json:"profitMargin" db:"profit_margin"`
|
|
||||||
|
|
||||||
// NetProfitMargin is a percentage of the net profit and the capital amount
|
|
||||||
NetProfitMargin fixedpoint.Value `json:"netProfitMargin" db:"net_profit_margin"`
|
|
||||||
|
|
||||||
QuoteCurrency string `json:"quoteCurrency" db:"quote_currency"`
|
|
||||||
BaseCurrency string `json:"baseCurrency" db:"base_currency"`
|
|
||||||
|
|
||||||
// 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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Profit) SlackAttachment() slack.Attachment {
|
|
||||||
var color = pnlColor(p.Profit)
|
|
||||||
var title = fmt.Sprintf("%s PnL ", p.Symbol)
|
|
||||||
title += pnlEmojiMargin(p.Profit, p.ProfitMargin, defaultPnlLevelResolution) + " "
|
|
||||||
title += pnlSignString(p.Profit) + " " + p.QuoteCurrency
|
|
||||||
|
|
||||||
var fields []slack.AttachmentField
|
|
||||||
|
|
||||||
if !p.NetProfit.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Net Profit",
|
|
||||||
Value: pnlSignString(p.NetProfit) + " " + p.QuoteCurrency,
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.ProfitMargin.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Profit Margin",
|
|
||||||
Value: p.ProfitMargin.Percentage(),
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.NetProfitMargin.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Net Profit Margin",
|
|
||||||
Value: p.NetProfitMargin.Percentage(),
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.TradeAmount.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Trade Amount",
|
|
||||||
Value: p.TradeAmount.String() + " " + p.QuoteCurrency,
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.FeeInUSD.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Fee In USD",
|
|
||||||
Value: p.FeeInUSD.String() + " USD",
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(p.Strategy) != 0 {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Strategy",
|
|
||||||
Value: p.Strategy,
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return slack.Attachment{
|
|
||||||
Color: color,
|
|
||||||
Title: title,
|
|
||||||
Fields: fields,
|
|
||||||
// Footer: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Profit) PlainText() string {
|
|
||||||
var emoji string
|
|
||||||
if !p.ProfitMargin.IsZero() {
|
|
||||||
emoji = pnlEmojiMargin(p.Profit, p.ProfitMargin, defaultPnlLevelResolution)
|
|
||||||
} else {
|
|
||||||
emoji = pnlEmojiSimple(p.Profit)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s trade profit %s %s %s (%s), net profit =~ %s %s (%s)",
|
|
||||||
p.Symbol,
|
|
||||||
emoji,
|
|
||||||
p.Profit.String(), p.QuoteCurrency,
|
|
||||||
p.ProfitMargin.Percentage(),
|
|
||||||
p.NetProfit.String(), p.QuoteCurrency,
|
|
||||||
p.NetProfitMargin.Percentage(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var lossEmoji = "🔥"
|
|
||||||
var profitEmoji = "💰"
|
|
||||||
var defaultPnlLevelResolution = fixedpoint.NewFromFloat(0.001)
|
|
||||||
|
|
||||||
func pnlColor(pnl fixedpoint.Value) string {
|
|
||||||
if pnl.Sign() > 0 {
|
|
||||||
return types.GreenColor
|
|
||||||
}
|
|
||||||
return types.RedColor
|
|
||||||
}
|
|
||||||
|
|
||||||
func pnlSignString(pnl fixedpoint.Value) string {
|
|
||||||
if pnl.Sign() > 0 {
|
|
||||||
return "+" + pnl.String()
|
|
||||||
}
|
|
||||||
return pnl.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func pnlEmojiSimple(pnl fixedpoint.Value) string {
|
|
||||||
if pnl.Sign() < 0 {
|
|
||||||
return lossEmoji
|
|
||||||
}
|
|
||||||
|
|
||||||
if pnl.IsZero() {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return profitEmoji
|
|
||||||
}
|
|
||||||
|
|
||||||
func pnlEmojiMargin(pnl, margin, resolution fixedpoint.Value) (out string) {
|
|
||||||
if margin.IsZero() {
|
|
||||||
return pnlEmojiSimple(pnl)
|
|
||||||
}
|
|
||||||
|
|
||||||
if pnl.Sign() < 0 {
|
|
||||||
out = lossEmoji
|
|
||||||
level := (margin.Neg()).Div(resolution).Int()
|
|
||||||
for i := 1; i < level; i++ {
|
|
||||||
out += lossEmoji
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
if pnl.IsZero() {
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
out = profitEmoji
|
|
||||||
level := margin.Div(resolution).Int()
|
|
||||||
for i := 1; i < level; i++ {
|
|
||||||
out += profitEmoji
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProfitStats struct {
|
|
||||||
Symbol string `json:"symbol"`
|
|
||||||
QuoteCurrency string `json:"quoteCurrency"`
|
|
||||||
BaseCurrency string `json:"baseCurrency"`
|
|
||||||
|
|
||||||
AccumulatedPnL fixedpoint.Value `json:"accumulatedPnL,omitempty"`
|
|
||||||
AccumulatedNetProfit fixedpoint.Value `json:"accumulatedNetProfit,omitempty"`
|
|
||||||
AccumulatedProfit fixedpoint.Value `json:"accumulatedProfit,omitempty"`
|
|
||||||
AccumulatedLoss fixedpoint.Value `json:"accumulatedLoss,omitempty"`
|
|
||||||
AccumulatedVolume fixedpoint.Value `json:"accumulatedVolume,omitempty"`
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProfitStats) AddProfit(profit Profit) {
|
|
||||||
s.AccumulatedPnL = s.AccumulatedPnL.Add(profit.Profit)
|
|
||||||
s.AccumulatedNetProfit = s.AccumulatedNetProfit.Add(profit.NetProfit)
|
|
||||||
s.TodayPnL = s.TodayPnL.Add(profit.Profit)
|
|
||||||
s.TodayNetProfit = s.TodayNetProfit.Add(profit.NetProfit)
|
|
||||||
|
|
||||||
if profit.Profit.Sign() < 0 {
|
|
||||||
s.AccumulatedLoss = s.AccumulatedLoss.Add(profit.Profit)
|
|
||||||
s.TodayLoss = s.TodayLoss.Add(profit.Profit)
|
|
||||||
} else if profit.Profit.Sign() > 0 {
|
|
||||||
s.AccumulatedProfit = s.AccumulatedLoss.Add(profit.Profit)
|
|
||||||
s.TodayProfit = s.TodayProfit.Add(profit.Profit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProfitStats) AddTrade(trade types.Trade) {
|
|
||||||
if s.IsOver24Hours() {
|
|
||||||
s.ResetToday()
|
|
||||||
}
|
|
||||||
|
|
||||||
s.AccumulatedVolume = s.AccumulatedVolume.Add(trade.Quantity)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProfitStats) IsOver24Hours() bool {
|
|
||||||
return time.Since(time.Unix(s.TodaySince, 0)) > 24*time.Hour
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProfitStats) ResetToday() {
|
|
||||||
s.TodayPnL = fixedpoint.Zero
|
|
||||||
s.TodayNetProfit = fixedpoint.Zero
|
|
||||||
s.TodayProfit = fixedpoint.Zero
|
|
||||||
s.TodayLoss = fixedpoint.Zero
|
|
||||||
|
|
||||||
var beginningOfTheDay = util.BeginningOfTheDay(time.Now().Local())
|
|
||||||
s.TodaySince = beginningOfTheDay.Unix()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProfitStats) PlainText() string {
|
|
||||||
since := time.Unix(s.AccumulatedSince, 0).Local()
|
|
||||||
return fmt.Sprintf("%s Profit Today\n"+
|
|
||||||
"Profit %s %s\n"+
|
|
||||||
"Net profit %s %s\n"+
|
|
||||||
"Trade Loss %s %s\n"+
|
|
||||||
"Summary:\n"+
|
|
||||||
"Accumulated Profit %s %s\n"+
|
|
||||||
"Accumulated Net Profit %s %s\n"+
|
|
||||||
"Accumulated Trade Loss %s %s\n"+
|
|
||||||
"Since %s",
|
|
||||||
s.Symbol,
|
|
||||||
s.TodayPnL.String(), s.QuoteCurrency,
|
|
||||||
s.TodayNetProfit.String(), s.QuoteCurrency,
|
|
||||||
s.TodayLoss.String(), s.QuoteCurrency,
|
|
||||||
s.AccumulatedPnL.String(), s.QuoteCurrency,
|
|
||||||
s.AccumulatedNetProfit.String(), s.QuoteCurrency,
|
|
||||||
s.AccumulatedLoss.String(), s.QuoteCurrency,
|
|
||||||
since.Format(time.RFC822),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *ProfitStats) SlackAttachment() slack.Attachment {
|
|
||||||
var color = pnlColor(s.AccumulatedPnL)
|
|
||||||
var title = fmt.Sprintf("%s Accumulated PnL %s %s", s.Symbol, pnlSignString(s.AccumulatedPnL), s.QuoteCurrency)
|
|
||||||
|
|
||||||
since := time.Unix(s.AccumulatedSince, 0).Local()
|
|
||||||
title += " Since " + since.Format(time.RFC822)
|
|
||||||
|
|
||||||
var fields []slack.AttachmentField
|
|
||||||
|
|
||||||
if !s.TodayPnL.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "P&L Today",
|
|
||||||
Value: pnlSignString(s.TodayPnL) + " " + s.QuoteCurrency,
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.TodayProfit.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Profit Today",
|
|
||||||
Value: pnlSignString(s.TodayProfit) + " " + s.QuoteCurrency,
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.TodayNetProfit.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Net Profit Today",
|
|
||||||
Value: pnlSignString(s.TodayNetProfit) + " " + s.QuoteCurrency,
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.TodayLoss.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Loss Today",
|
|
||||||
Value: pnlSignString(s.TodayLoss) + " " + s.QuoteCurrency,
|
|
||||||
Short: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.AccumulatedPnL.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Accumulated P&L",
|
|
||||||
Value: pnlSignString(s.AccumulatedPnL) + " " + s.QuoteCurrency,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.AccumulatedProfit.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Accumulated Profit",
|
|
||||||
Value: pnlSignString(s.AccumulatedProfit) + " " + s.QuoteCurrency,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.AccumulatedNetProfit.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Accumulated Net Profit",
|
|
||||||
Value: pnlSignString(s.AccumulatedNetProfit) + " " + s.QuoteCurrency,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if !s.AccumulatedLoss.IsZero() {
|
|
||||||
fields = append(fields, slack.AttachmentField{
|
|
||||||
Title: "Accumulated Loss",
|
|
||||||
Value: pnlSignString(s.AccumulatedLoss) + " " + s.QuoteCurrency,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return slack.Attachment{
|
|
||||||
Color: color,
|
|
||||||
Title: title,
|
|
||||||
Fields: fields,
|
|
||||||
// Footer: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
59
pkg/service/profit.go
Normal file
59
pkg/service/profit.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProfitService struct {
|
||||||
|
DB *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProfitService(db *sqlx.DB) *ProfitService {
|
||||||
|
return &ProfitService{db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitService) Load(ctx context.Context, id int64) (*types.Trade, error) {
|
||||||
|
var trade types.Trade
|
||||||
|
|
||||||
|
rows, err := s.DB.NamedQuery("SELECT * FROM trades WHERE id = :id", map[string]interface{}{
|
||||||
|
"id": id,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
if rows.Next() {
|
||||||
|
err = rows.StructScan(&trade)
|
||||||
|
return &trade, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, errors.Wrapf(ErrTradeNotFound, "trade id:%d not found", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitService) scanRows(rows *sqlx.Rows) (trades []types.Trade, err error) {
|
||||||
|
for rows.Next() {
|
||||||
|
var trade types.Trade
|
||||||
|
if err := rows.StructScan(&trade); err != nil {
|
||||||
|
return trades, err
|
||||||
|
}
|
||||||
|
|
||||||
|
trades = append(trades, trade)
|
||||||
|
}
|
||||||
|
|
||||||
|
return trades, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitService) Insert(trade types.Trade) error {
|
||||||
|
_, err := s.DB.NamedExec(`
|
||||||
|
INSERT INTO profits (id, exchange, symbol, trade_id, average_cost, profit, price, quantity, quote_quantity, side, traded_at, is_margin, is_futures, is_isolated)
|
||||||
|
VALUES (:id, :exchange, :order_id, :symbol, :price, :quantity, :quote_quantity, :side, :is_buyer, :is_maker, :fee, :fee_currency, :traded_at, :is_margin, :is_futures, :is_isolated)`,
|
||||||
|
trade)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -38,8 +38,8 @@ func init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
type State struct {
|
type State struct {
|
||||||
Position *types.Position `json:"position,omitempty"`
|
Position *types.Position `json:"position,omitempty"`
|
||||||
ProfitStats bbgo.ProfitStats `json:"profitStats,omitempty"`
|
ProfitStats types.ProfitStats `json:"profitStats,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BollingerSetting struct {
|
type BollingerSetting struct {
|
||||||
|
@ -571,7 +571,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
||||||
s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.state.Position, s.orderStore)
|
s.tradeCollector = bbgo.NewTradeCollector(s.Symbol, s.state.Position, s.orderStore)
|
||||||
s.tradeCollector.OnProfit(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) {
|
s.tradeCollector.OnProfit(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) {
|
||||||
log.Infof("generated profit: %v", profit)
|
log.Infof("generated profit: %v", profit)
|
||||||
p := bbgo.Profit{
|
p := types.Profit{
|
||||||
Symbol: s.Symbol,
|
Symbol: s.Symbol,
|
||||||
Profit: profit,
|
Profit: profit,
|
||||||
NetProfit: netProfit,
|
NetProfit: netProfit,
|
||||||
|
|
|
@ -41,7 +41,7 @@ type State struct {
|
||||||
// [source Order ID] -> arbitrage order
|
// [source Order ID] -> arbitrage order
|
||||||
ArbitrageOrders map[uint64]types.Order `json:"arbitrageOrders"`
|
ArbitrageOrders map[uint64]types.Order `json:"arbitrageOrders"`
|
||||||
|
|
||||||
ProfitStats bbgo.ProfitStats `json:"profitStats,omitempty"`
|
ProfitStats types.ProfitStats `json:"profitStats,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Strategy struct {
|
type Strategy struct {
|
||||||
|
|
|
@ -3,7 +3,6 @@ package xmaker
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/bbgo"
|
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
"github.com/c9s/bbgo/pkg/types"
|
"github.com/c9s/bbgo/pkg/types"
|
||||||
)
|
)
|
||||||
|
@ -15,7 +14,7 @@ type State struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProfitStats struct {
|
type ProfitStats struct {
|
||||||
bbgo.ProfitStats
|
types.ProfitStats
|
||||||
lock sync.Mutex
|
lock sync.Mutex
|
||||||
|
|
||||||
MakerExchange types.ExchangeName `json:"makerExchange"`
|
MakerExchange types.ExchangeName `json:"makerExchange"`
|
||||||
|
|
|
@ -763,7 +763,7 @@ func (s *Strategy) CrossRun(ctx context.Context, orderExecutionRouter bbgo.Order
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
s.tradeCollector.OnProfit(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) {
|
s.tradeCollector.OnProfit(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) {
|
||||||
p := bbgo.Profit{
|
p := types.Profit{
|
||||||
Symbol: s.Symbol,
|
Symbol: s.Symbol,
|
||||||
Profit: profit,
|
Profit: profit,
|
||||||
NetProfit: netProfit,
|
NetProfit: netProfit,
|
||||||
|
|
343
pkg/types/profit.go
Normal file
343
pkg/types/profit.go
Normal file
|
@ -0,0 +1,343 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/slack-go/slack"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
|
"github.com/c9s/bbgo/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Profit struct stores the PnL information
|
||||||
|
type Profit struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
|
||||||
|
TradeAmount fixedpoint.Value `json:"tradeAmount" db:"trade_amount"`
|
||||||
|
|
||||||
|
// ProfitMargin is a percentage of the profit and the capital amount
|
||||||
|
ProfitMargin fixedpoint.Value `json:"profitMargin" db:"profit_margin"`
|
||||||
|
|
||||||
|
// NetProfitMargin is a percentage of the net profit and the capital amount
|
||||||
|
NetProfitMargin fixedpoint.Value `json:"netProfitMargin" db:"net_profit_margin"`
|
||||||
|
|
||||||
|
QuoteCurrency string `json:"quoteCurrency" db:"quote_currency"`
|
||||||
|
BaseCurrency string `json:"baseCurrency" db:"base_currency"`
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Profit) SlackAttachment() slack.Attachment {
|
||||||
|
var color = pnlColor(p.Profit)
|
||||||
|
var title = fmt.Sprintf("%s PnL ", p.Symbol)
|
||||||
|
title += pnlEmojiMargin(p.Profit, p.ProfitMargin, defaultPnlLevelResolution) + " "
|
||||||
|
title += pnlSignString(p.Profit) + " " + p.QuoteCurrency
|
||||||
|
|
||||||
|
var fields []slack.AttachmentField
|
||||||
|
|
||||||
|
if !p.NetProfit.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Net Profit",
|
||||||
|
Value: pnlSignString(p.NetProfit) + " " + p.QuoteCurrency,
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.ProfitMargin.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Profit Margin",
|
||||||
|
Value: p.ProfitMargin.Percentage(),
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.NetProfitMargin.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Net Profit Margin",
|
||||||
|
Value: p.NetProfitMargin.Percentage(),
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.TradeAmount.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Trade Amount",
|
||||||
|
Value: p.TradeAmount.String() + " " + p.QuoteCurrency,
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !p.FeeInUSD.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Fee In USD",
|
||||||
|
Value: p.FeeInUSD.String() + " USD",
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(p.Strategy) != 0 {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Strategy",
|
||||||
|
Value: p.Strategy,
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return slack.Attachment{
|
||||||
|
Color: color,
|
||||||
|
Title: title,
|
||||||
|
Fields: fields,
|
||||||
|
// Footer: "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Profit) PlainText() string {
|
||||||
|
var emoji string
|
||||||
|
if !p.ProfitMargin.IsZero() {
|
||||||
|
emoji = pnlEmojiMargin(p.Profit, p.ProfitMargin, defaultPnlLevelResolution)
|
||||||
|
} else {
|
||||||
|
emoji = pnlEmojiSimple(p.Profit)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s trade profit %s %s %s (%s), net profit =~ %s %s (%s)",
|
||||||
|
p.Symbol,
|
||||||
|
emoji,
|
||||||
|
p.Profit.String(), p.QuoteCurrency,
|
||||||
|
p.ProfitMargin.Percentage(),
|
||||||
|
p.NetProfit.String(), p.QuoteCurrency,
|
||||||
|
p.NetProfitMargin.Percentage(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var lossEmoji = "🔥"
|
||||||
|
var profitEmoji = "💰"
|
||||||
|
var defaultPnlLevelResolution = fixedpoint.NewFromFloat(0.001)
|
||||||
|
|
||||||
|
func pnlColor(pnl fixedpoint.Value) string {
|
||||||
|
if pnl.Sign() > 0 {
|
||||||
|
return GreenColor
|
||||||
|
}
|
||||||
|
return RedColor
|
||||||
|
}
|
||||||
|
|
||||||
|
func pnlSignString(pnl fixedpoint.Value) string {
|
||||||
|
if pnl.Sign() > 0 {
|
||||||
|
return "+" + pnl.String()
|
||||||
|
}
|
||||||
|
return pnl.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func pnlEmojiSimple(pnl fixedpoint.Value) string {
|
||||||
|
if pnl.Sign() < 0 {
|
||||||
|
return lossEmoji
|
||||||
|
}
|
||||||
|
|
||||||
|
if pnl.IsZero() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return profitEmoji
|
||||||
|
}
|
||||||
|
|
||||||
|
func pnlEmojiMargin(pnl, margin, resolution fixedpoint.Value) (out string) {
|
||||||
|
if margin.IsZero() {
|
||||||
|
return pnlEmojiSimple(pnl)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pnl.Sign() < 0 {
|
||||||
|
out = lossEmoji
|
||||||
|
level := (margin.Neg()).Div(resolution).Int()
|
||||||
|
for i := 1; i < level; i++ {
|
||||||
|
out += lossEmoji
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
if pnl.IsZero() {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
out = profitEmoji
|
||||||
|
level := margin.Div(resolution).Int()
|
||||||
|
for i := 1; i < level; i++ {
|
||||||
|
out += profitEmoji
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfitStats struct {
|
||||||
|
Symbol string `json:"symbol"`
|
||||||
|
QuoteCurrency string `json:"quoteCurrency"`
|
||||||
|
BaseCurrency string `json:"baseCurrency"`
|
||||||
|
|
||||||
|
AccumulatedPnL fixedpoint.Value `json:"accumulatedPnL,omitempty"`
|
||||||
|
AccumulatedNetProfit fixedpoint.Value `json:"accumulatedNetProfit,omitempty"`
|
||||||
|
AccumulatedProfit fixedpoint.Value `json:"accumulatedProfit,omitempty"`
|
||||||
|
AccumulatedLoss fixedpoint.Value `json:"accumulatedLoss,omitempty"`
|
||||||
|
AccumulatedVolume fixedpoint.Value `json:"accumulatedVolume,omitempty"`
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitStats) Init(market Market) {
|
||||||
|
s.Symbol = market.Symbol
|
||||||
|
s.BaseCurrency = market.BaseCurrency
|
||||||
|
s.QuoteCurrency = market.QuoteCurrency
|
||||||
|
if s.AccumulatedSince == 0 {
|
||||||
|
s.AccumulatedSince = time.Now().Unix()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitStats) AddProfit(profit Profit) {
|
||||||
|
s.AccumulatedPnL = s.AccumulatedPnL.Add(profit.Profit)
|
||||||
|
s.AccumulatedNetProfit = s.AccumulatedNetProfit.Add(profit.NetProfit)
|
||||||
|
s.TodayPnL = s.TodayPnL.Add(profit.Profit)
|
||||||
|
s.TodayNetProfit = s.TodayNetProfit.Add(profit.NetProfit)
|
||||||
|
|
||||||
|
if profit.Profit.Sign() < 0 {
|
||||||
|
s.AccumulatedLoss = s.AccumulatedLoss.Add(profit.Profit)
|
||||||
|
s.TodayLoss = s.TodayLoss.Add(profit.Profit)
|
||||||
|
} else if profit.Profit.Sign() > 0 {
|
||||||
|
s.AccumulatedProfit = s.AccumulatedLoss.Add(profit.Profit)
|
||||||
|
s.TodayProfit = s.TodayProfit.Add(profit.Profit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitStats) AddTrade(trade Trade) {
|
||||||
|
if s.IsOver24Hours() {
|
||||||
|
s.ResetToday()
|
||||||
|
}
|
||||||
|
|
||||||
|
s.AccumulatedVolume = s.AccumulatedVolume.Add(trade.Quantity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitStats) IsOver24Hours() bool {
|
||||||
|
return time.Since(time.Unix(s.TodaySince, 0)) > 24*time.Hour
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitStats) ResetToday() {
|
||||||
|
s.TodayPnL = fixedpoint.Zero
|
||||||
|
s.TodayNetProfit = fixedpoint.Zero
|
||||||
|
s.TodayProfit = fixedpoint.Zero
|
||||||
|
s.TodayLoss = fixedpoint.Zero
|
||||||
|
|
||||||
|
var beginningOfTheDay = util.BeginningOfTheDay(time.Now().Local())
|
||||||
|
s.TodaySince = beginningOfTheDay.Unix()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitStats) PlainText() string {
|
||||||
|
since := time.Unix(s.AccumulatedSince, 0).Local()
|
||||||
|
return fmt.Sprintf("%s Profit Today\n"+
|
||||||
|
"Profit %s %s\n"+
|
||||||
|
"Net profit %s %s\n"+
|
||||||
|
"Trade Loss %s %s\n"+
|
||||||
|
"Summary:\n"+
|
||||||
|
"Accumulated Profit %s %s\n"+
|
||||||
|
"Accumulated Net Profit %s %s\n"+
|
||||||
|
"Accumulated Trade Loss %s %s\n"+
|
||||||
|
"Since %s",
|
||||||
|
s.Symbol,
|
||||||
|
s.TodayPnL.String(), s.QuoteCurrency,
|
||||||
|
s.TodayNetProfit.String(), s.QuoteCurrency,
|
||||||
|
s.TodayLoss.String(), s.QuoteCurrency,
|
||||||
|
s.AccumulatedPnL.String(), s.QuoteCurrency,
|
||||||
|
s.AccumulatedNetProfit.String(), s.QuoteCurrency,
|
||||||
|
s.AccumulatedLoss.String(), s.QuoteCurrency,
|
||||||
|
since.Format(time.RFC822),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ProfitStats) SlackAttachment() slack.Attachment {
|
||||||
|
var color = pnlColor(s.AccumulatedPnL)
|
||||||
|
var title = fmt.Sprintf("%s Accumulated PnL %s %s", s.Symbol, pnlSignString(s.AccumulatedPnL), s.QuoteCurrency)
|
||||||
|
|
||||||
|
since := time.Unix(s.AccumulatedSince, 0).Local()
|
||||||
|
title += " Since " + since.Format(time.RFC822)
|
||||||
|
|
||||||
|
var fields []slack.AttachmentField
|
||||||
|
|
||||||
|
if !s.TodayPnL.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "P&L Today",
|
||||||
|
Value: pnlSignString(s.TodayPnL) + " " + s.QuoteCurrency,
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.TodayProfit.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Profit Today",
|
||||||
|
Value: pnlSignString(s.TodayProfit) + " " + s.QuoteCurrency,
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.TodayNetProfit.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Net Profit Today",
|
||||||
|
Value: pnlSignString(s.TodayNetProfit) + " " + s.QuoteCurrency,
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.TodayLoss.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Loss Today",
|
||||||
|
Value: pnlSignString(s.TodayLoss) + " " + s.QuoteCurrency,
|
||||||
|
Short: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.AccumulatedPnL.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Accumulated P&L",
|
||||||
|
Value: pnlSignString(s.AccumulatedPnL) + " " + s.QuoteCurrency,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.AccumulatedProfit.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Accumulated Profit",
|
||||||
|
Value: pnlSignString(s.AccumulatedProfit) + " " + s.QuoteCurrency,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.AccumulatedNetProfit.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Accumulated Net Profit",
|
||||||
|
Value: pnlSignString(s.AccumulatedNetProfit) + " " + s.QuoteCurrency,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.AccumulatedLoss.IsZero() {
|
||||||
|
fields = append(fields, slack.AttachmentField{
|
||||||
|
Title: "Accumulated Loss",
|
||||||
|
Value: pnlSignString(s.AccumulatedLoss) + " " + s.QuoteCurrency,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return slack.Attachment{
|
||||||
|
Color: color,
|
||||||
|
Title: title,
|
||||||
|
Fields: fields,
|
||||||
|
// Footer: "",
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user