From 9e0df77a3643143af041088911653293f1b937ca Mon Sep 17 00:00:00 2001 From: c9s Date: Fri, 4 Mar 2022 16:39:48 +0800 Subject: [PATCH] move profit struct into the types package --- pkg/bbgo/profitstats.go | 341 ---------------------------- pkg/service/profit.go | 59 +++++ pkg/strategy/bollmaker/strategy.go | 6 +- pkg/strategy/grid/strategy.go | 2 +- pkg/strategy/xmaker/state.go | 3 +- pkg/strategy/xmaker/strategy.go | 2 +- pkg/types/profit.go | 343 +++++++++++++++++++++++++++++ 7 files changed, 408 insertions(+), 348 deletions(-) create mode 100644 pkg/service/profit.go create mode 100644 pkg/types/profit.go diff --git a/pkg/bbgo/profitstats.go b/pkg/bbgo/profitstats.go index 0b46307a1..920078f66 100644 --- a/pkg/bbgo/profitstats.go +++ b/pkg/bbgo/profitstats.go @@ -1,343 +1,2 @@ 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: "", - } -} diff --git a/pkg/service/profit.go b/pkg/service/profit.go new file mode 100644 index 000000000..7920d3aca --- /dev/null +++ b/pkg/service/profit.go @@ -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 +} diff --git a/pkg/strategy/bollmaker/strategy.go b/pkg/strategy/bollmaker/strategy.go index cbf313d94..bdf4f9a1c 100644 --- a/pkg/strategy/bollmaker/strategy.go +++ b/pkg/strategy/bollmaker/strategy.go @@ -38,8 +38,8 @@ func init() { } type State struct { - Position *types.Position `json:"position,omitempty"` - ProfitStats bbgo.ProfitStats `json:"profitStats,omitempty"` + Position *types.Position `json:"position,omitempty"` + ProfitStats types.ProfitStats `json:"profitStats,omitempty"` } 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.OnProfit(func(trade types.Trade, profit fixedpoint.Value, netProfit fixedpoint.Value) { log.Infof("generated profit: %v", profit) - p := bbgo.Profit{ + p := types.Profit{ Symbol: s.Symbol, Profit: profit, NetProfit: netProfit, diff --git a/pkg/strategy/grid/strategy.go b/pkg/strategy/grid/strategy.go index 572e0833a..f03d2df7b 100644 --- a/pkg/strategy/grid/strategy.go +++ b/pkg/strategy/grid/strategy.go @@ -41,7 +41,7 @@ type State struct { // [source Order ID] -> arbitrage order ArbitrageOrders map[uint64]types.Order `json:"arbitrageOrders"` - ProfitStats bbgo.ProfitStats `json:"profitStats,omitempty"` + ProfitStats types.ProfitStats `json:"profitStats,omitempty"` } type Strategy struct { diff --git a/pkg/strategy/xmaker/state.go b/pkg/strategy/xmaker/state.go index f6a57c285..ea82bc96f 100644 --- a/pkg/strategy/xmaker/state.go +++ b/pkg/strategy/xmaker/state.go @@ -3,7 +3,6 @@ package xmaker import ( "sync" - "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/types" ) @@ -15,7 +14,7 @@ type State struct { } type ProfitStats struct { - bbgo.ProfitStats + types.ProfitStats lock sync.Mutex MakerExchange types.ExchangeName `json:"makerExchange"` diff --git a/pkg/strategy/xmaker/strategy.go b/pkg/strategy/xmaker/strategy.go index bbfc162b8..cf85e0d9d 100644 --- a/pkg/strategy/xmaker/strategy.go +++ b/pkg/strategy/xmaker/strategy.go @@ -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) { - p := bbgo.Profit{ + p := types.Profit{ Symbol: s.Symbol, Profit: profit, NetProfit: netProfit, diff --git a/pkg/types/profit.go b/pkg/types/profit.go new file mode 100644 index 000000000..90d06134d --- /dev/null +++ b/pkg/types/profit.go @@ -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: "", + } +}