mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-26 08:45:16 +00:00
fix cyclic imports
This commit is contained in:
parent
c92ada2f34
commit
42a32924a7
|
@ -1,7 +1 @@
|
||||||
package bbgo
|
package bbgo
|
||||||
|
|
||||||
import "github.com/leekchan/accounting"
|
|
||||||
|
|
||||||
var USD = accounting.Accounting{Symbol: "$ ", Precision: 2}
|
|
||||||
var BTC = accounting.Accounting{Symbol: "BTC ", Precision: 8}
|
|
||||||
|
|
||||||
|
|
117
bbgo/accounting/pnl.go
Normal file
117
bbgo/accounting/pnl.go
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
package accounting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/bbgo/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProfitAndLossCalculator struct {
|
||||||
|
Symbol string
|
||||||
|
StartTime time.Time
|
||||||
|
CurrentPrice float64
|
||||||
|
Trades []types.Trade
|
||||||
|
TradingFeeCurrency string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ProfitAndLossCalculator) AddTrade(trade types.Trade) {
|
||||||
|
c.Trades = append(c.Trades, trade)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ProfitAndLossCalculator) SetCurrentPrice(price float64) {
|
||||||
|
c.CurrentPrice = price
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ProfitAndLossCalculator) Calculate() *ProfitAndLossReport {
|
||||||
|
// copy trades, so that we can truncate it.
|
||||||
|
var trades = c.Trades
|
||||||
|
var bidVolume = 0.0
|
||||||
|
var bidAmount = 0.0
|
||||||
|
|
||||||
|
var askVolume = 0.0
|
||||||
|
|
||||||
|
var feeUSD = 0.0
|
||||||
|
var bidFeeUSD = 0.0
|
||||||
|
var feeRate = 0.0015
|
||||||
|
|
||||||
|
var currencyFees = map[string]float64{}
|
||||||
|
|
||||||
|
for _, trade := range trades {
|
||||||
|
if trade.Symbol == c.Symbol {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := currencyFees[trade.FeeCurrency]; !ok {
|
||||||
|
currencyFees[trade.FeeCurrency] = 0.0
|
||||||
|
}
|
||||||
|
currencyFees[trade.FeeCurrency] += trade.Fee
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof("average bid price = (total amount %f + total feeUSD %f) / volume %f", bidAmount, bidFeeUSD, bidVolume)
|
||||||
|
profit := 0.0
|
||||||
|
averageCost := (bidAmount + bidFeeUSD) / bidVolume
|
||||||
|
|
||||||
|
for _, t := range trades {
|
||||||
|
if t.Symbol != c.Symbol {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.IsBuyer {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
profit += (t.Price - averageCost) * t.Quantity
|
||||||
|
askVolume += t.Quantity
|
||||||
|
}
|
||||||
|
|
||||||
|
profit -= feeUSD
|
||||||
|
unrealizedProfit := profit
|
||||||
|
|
||||||
|
stock := bidVolume - askVolume
|
||||||
|
if stock > 0 {
|
||||||
|
stockFee := c.CurrentPrice * stock * feeRate
|
||||||
|
unrealizedProfit += (c.CurrentPrice-averageCost)*stock - stockFee
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ProfitAndLossReport{
|
||||||
|
Symbol: c.Symbol,
|
||||||
|
StartTime: c.StartTime,
|
||||||
|
CurrentPrice: c.CurrentPrice,
|
||||||
|
NumTrades: len(trades),
|
||||||
|
|
||||||
|
BidVolume: bidVolume,
|
||||||
|
AskVolume: askVolume,
|
||||||
|
|
||||||
|
Stock: stock,
|
||||||
|
Profit: profit,
|
||||||
|
UnrealizedProfit: unrealizedProfit,
|
||||||
|
AverageBidCost: averageCost,
|
||||||
|
FeeUSD: feeUSD,
|
||||||
|
CurrencyFees: currencyFees,
|
||||||
|
}
|
||||||
|
}
|
77
bbgo/accounting/report.go
Normal file
77
bbgo/accounting/report.go
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
package accounting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/slack-go/slack"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/bbgo/slack/slackstyle"
|
||||||
|
"github.com/c9s/bbgo/pkg/bbgo/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProfitAndLossReport struct {
|
||||||
|
CurrentPrice float64
|
||||||
|
StartTime time.Time
|
||||||
|
Symbol string
|
||||||
|
|
||||||
|
NumTrades int
|
||||||
|
Profit float64
|
||||||
|
UnrealizedProfit float64
|
||||||
|
AverageBidCost float64
|
||||||
|
BidVolume float64
|
||||||
|
AskVolume float64
|
||||||
|
FeeUSD float64
|
||||||
|
Stock float64
|
||||||
|
CurrencyFees map[string]float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (report ProfitAndLossReport) Print() {
|
||||||
|
logrus.Infof("trades since: %v", report.StartTime)
|
||||||
|
logrus.Infof("average bid cost: %s", types.USD.FormatMoneyFloat64(report.AverageBidCost))
|
||||||
|
logrus.Infof("total bid volume: %f", report.BidVolume)
|
||||||
|
logrus.Infof("total ask volume: %f", report.AskVolume)
|
||||||
|
logrus.Infof("stock: %f", report.Stock)
|
||||||
|
logrus.Infof("fee (USD): %f", report.FeeUSD)
|
||||||
|
logrus.Infof("current price: %s", types.USD.FormatMoneyFloat64(report.CurrentPrice))
|
||||||
|
logrus.Infof("profit: %s", types.USD.FormatMoneyFloat64(report.Profit))
|
||||||
|
logrus.Infof("unrealized profit: %s", types.USD.FormatMoneyFloat64(report.UnrealizedProfit))
|
||||||
|
logrus.Infof("currency fees:")
|
||||||
|
for currency, fee := range report.CurrencyFees {
|
||||||
|
logrus.Infof(" - %s: %f", currency, fee)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (report ProfitAndLossReport) SlackAttachment() slack.Attachment {
|
||||||
|
var color = ""
|
||||||
|
if report.UnrealizedProfit > 0 {
|
||||||
|
color = slackstyle.Green
|
||||||
|
} else {
|
||||||
|
color = slackstyle.Red
|
||||||
|
}
|
||||||
|
|
||||||
|
market, ok := types.FindMarket(report.Symbol)
|
||||||
|
if !ok {
|
||||||
|
return slack.Attachment{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return slack.Attachment{
|
||||||
|
Title: report.Symbol + " Profit and Loss report",
|
||||||
|
Text: "Profit " + types.USD.FormatMoney(report.Profit),
|
||||||
|
Color: color,
|
||||||
|
// Pretext: "",
|
||||||
|
// Text: "",
|
||||||
|
Fields: []slack.AttachmentField{
|
||||||
|
{Title: "Profit", Value: types.USD.FormatMoney(report.Profit)},
|
||||||
|
{Title: "Unrealized Profit", Value: types.USD.FormatMoney(report.UnrealizedProfit)},
|
||||||
|
{Title: "Current Price", Value: market.FormatPrice(report.CurrentPrice), Short: true},
|
||||||
|
{Title: "Average Cost", Value: market.FormatPrice(report.AverageBidCost), Short: true},
|
||||||
|
{Title: "Fee (USD)", Value: types.USD.FormatMoney(report.FeeUSD), 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},
|
||||||
|
},
|
||||||
|
Footer: report.StartTime.Format(time.RFC822),
|
||||||
|
FooterIcon: "",
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ package bbgo
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/bbgo/accounting"
|
||||||
"github.com/c9s/bbgo/pkg/bbgo/types"
|
"github.com/c9s/bbgo/pkg/bbgo/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,7 +20,7 @@ type Context struct {
|
||||||
|
|
||||||
Balances map[string]types.Balance
|
Balances map[string]types.Balance
|
||||||
Quota map[string]types.Balance
|
Quota map[string]types.Balance
|
||||||
ProfitAndLossCalculator *ProfitAndLossCalculator
|
ProfitAndLossCalculator *accounting.ProfitAndLossCalculator
|
||||||
StockManager *StockManager
|
StockManager *StockManager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/bbgo/accounting"
|
||||||
"github.com/c9s/bbgo/pkg/bbgo/types"
|
"github.com/c9s/bbgo/pkg/bbgo/types"
|
||||||
"github.com/c9s/bbgo/pkg/util"
|
"github.com/c9s/bbgo/pkg/util"
|
||||||
)
|
)
|
||||||
|
@ -14,7 +15,7 @@ type KLineRegressionTrader struct {
|
||||||
// Context is trading Context
|
// Context is trading Context
|
||||||
Context *Context
|
Context *Context
|
||||||
SourceKLines []types.KLine
|
SourceKLines []types.KLine
|
||||||
ProfitAndLossCalculator *ProfitAndLossCalculator
|
ProfitAndLossCalculator *accounting.ProfitAndLossCalculator
|
||||||
|
|
||||||
doneOrders []*types.SubmitOrder
|
doneOrders []*types.SubmitOrder
|
||||||
pendingOrders []*types.SubmitOrder
|
pendingOrders []*types.SubmitOrder
|
||||||
|
|
124
bbgo/notifier/slacknotifier/slack.go
Normal file
124
bbgo/notifier/slacknotifier/slack.go
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
package slacknotifier
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/slack-go/slack"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/bbgo/accounting"
|
||||||
|
"github.com/c9s/bbgo/pkg/bbgo/types"
|
||||||
|
"github.com/c9s/bbgo/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SlackAttachmentCreator interface {
|
||||||
|
SlackAttachment() slack.Attachment
|
||||||
|
}
|
||||||
|
|
||||||
|
type Notifier struct {
|
||||||
|
client *slack.Client
|
||||||
|
|
||||||
|
Channel string
|
||||||
|
|
||||||
|
TradeChannel string
|
||||||
|
PnlChannel string
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotifyOption func(notifier *Notifier)
|
||||||
|
|
||||||
|
func TradeChannel(channel string) NotifyOption {
|
||||||
|
return func(notifier *Notifier) {
|
||||||
|
notifier.TradeChannel = channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func PnlChannel(channel string) NotifyOption {
|
||||||
|
return func(notifier *Notifier) {
|
||||||
|
notifier.PnlChannel = channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(token string, channel string, options ...NotifyOption) *Notifier {
|
||||||
|
var client = slack.New(token, slack.OptionDebug(true))
|
||||||
|
notifier := &Notifier{
|
||||||
|
client: client,
|
||||||
|
Channel: channel,
|
||||||
|
TradeChannel: channel,
|
||||||
|
PnlChannel: channel,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range options {
|
||||||
|
o(notifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
return notifier
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Notifier) Notify(format string, args ...interface{}) {
|
||||||
|
var slackAttachments []slack.Attachment
|
||||||
|
var slackArgsOffset = -1
|
||||||
|
|
||||||
|
for idx, arg := range args {
|
||||||
|
switch a := arg.(type) {
|
||||||
|
|
||||||
|
// concrete type assert first
|
||||||
|
case slack.Attachment:
|
||||||
|
if slackArgsOffset == -1 {
|
||||||
|
slackArgsOffset = idx
|
||||||
|
}
|
||||||
|
|
||||||
|
slackAttachments = append(slackAttachments, a)
|
||||||
|
|
||||||
|
case SlackAttachmentCreator:
|
||||||
|
if slackArgsOffset == -1 {
|
||||||
|
slackArgsOffset = idx
|
||||||
|
}
|
||||||
|
|
||||||
|
slackAttachments = append(slackAttachments, a.SlackAttachment())
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var nonSlackArgs = args
|
||||||
|
if slackArgsOffset > -1 {
|
||||||
|
nonSlackArgs = args[:slackArgsOffset]
|
||||||
|
}
|
||||||
|
|
||||||
|
logrus.Infof(format, nonSlackArgs...)
|
||||||
|
|
||||||
|
_, _, err := n.client.PostMessageContext(context.Background(), n.Channel,
|
||||||
|
slack.MsgOptionText(fmt.Sprintf(format, nonSlackArgs...), true),
|
||||||
|
slack.MsgOptionAttachments(slackAttachments...))
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Errorf("slack error: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Notifier) NotifyTrade(trade *types.Trade) {
|
||||||
|
_, _, err := n.client.PostMessageContext(context.Background(), n.TradeChannel,
|
||||||
|
slack.MsgOptionText(util.Render(`:handshake: {{ .Symbol }} {{ .Side }} Trade Execution @ {{ .Price }}`, trade), true),
|
||||||
|
slack.MsgOptionAttachments(trade.SlackAttachment()))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Error("slack send error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Notifier) NotifyPnL(report *accounting.ProfitAndLossReport) {
|
||||||
|
attachment := report.SlackAttachment()
|
||||||
|
|
||||||
|
_, _, err := n.client.PostMessageContext(context.Background(), n.PnlChannel,
|
||||||
|
slack.MsgOptionText(util.Render(
|
||||||
|
`:heavy_dollar_sign: Here is your *{{ .symbol }}* PnL report collected since *{{ .startTime }}*`,
|
||||||
|
map[string]interface{}{
|
||||||
|
"symbol": report.Symbol,
|
||||||
|
"startTime": report.StartTime.Format(time.RFC822),
|
||||||
|
}), true),
|
||||||
|
slack.MsgOptionAttachments(attachment))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Errorf("slack send error")
|
||||||
|
}
|
||||||
|
}
|
|
@ -55,8 +55,8 @@ func (p *OrderProcessor) Submit(ctx context.Context, order *types.SubmitOrder) e
|
||||||
if balance, ok := tradingCtx.Balances[market.QuoteCurrency]; ok {
|
if balance, ok := tradingCtx.Balances[market.QuoteCurrency]; ok {
|
||||||
if balance.Available < p.MinQuoteBalance {
|
if balance.Available < p.MinQuoteBalance {
|
||||||
return errors.Wrapf(ErrQuoteBalanceLevelTooLow, "quote balance level is too low: %s < %s",
|
return errors.Wrapf(ErrQuoteBalanceLevelTooLow, "quote balance level is too low: %s < %s",
|
||||||
USD.FormatMoneyFloat64(balance.Available),
|
types.USD.FormatMoneyFloat64(balance.Available),
|
||||||
USD.FormatMoneyFloat64(p.MinQuoteBalance))
|
types.USD.FormatMoneyFloat64(p.MinQuoteBalance))
|
||||||
}
|
}
|
||||||
|
|
||||||
if baseBalance, ok := tradingCtx.Balances[market.BaseCurrency]; ok {
|
if baseBalance, ok := tradingCtx.Balances[market.BaseCurrency]; ok {
|
||||||
|
|
183
bbgo/pnl.go
183
bbgo/pnl.go
|
@ -1,185 +1,2 @@
|
||||||
package bbgo
|
package bbgo
|
||||||
|
|
||||||
import (
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"github.com/slack-go/slack"
|
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/bbgo/slack/slackstyle"
|
|
||||||
"github.com/c9s/bbgo/pkg/bbgo/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProfitAndLossCalculator struct {
|
|
||||||
Symbol string
|
|
||||||
StartTime time.Time
|
|
||||||
CurrentPrice float64
|
|
||||||
Trades []types.Trade
|
|
||||||
TradingFeeCurrency string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ProfitAndLossCalculator) AddTrade(trade types.Trade) {
|
|
||||||
c.Trades = append(c.Trades, trade)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ProfitAndLossCalculator) SetCurrentPrice(price float64) {
|
|
||||||
c.CurrentPrice = price
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ProfitAndLossCalculator) Calculate() *ProfitAndLossReport {
|
|
||||||
// copy trades, so that we can truncate it.
|
|
||||||
var trades = c.Trades
|
|
||||||
var bidVolume = 0.0
|
|
||||||
var bidAmount = 0.0
|
|
||||||
|
|
||||||
var askVolume = 0.0
|
|
||||||
|
|
||||||
var feeUSD = 0.0
|
|
||||||
var bidFeeUSD = 0.0
|
|
||||||
var feeRate = 0.0015
|
|
||||||
|
|
||||||
var currencyFees = map[string]float64{}
|
|
||||||
|
|
||||||
for _, trade := range trades {
|
|
||||||
if trade.Symbol == c.Symbol {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := currencyFees[trade.FeeCurrency]; !ok {
|
|
||||||
currencyFees[trade.FeeCurrency] = 0.0
|
|
||||||
}
|
|
||||||
currencyFees[trade.FeeCurrency] += trade.Fee
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("average bid price = (total amount %f + total feeUSD %f) / volume %f", bidAmount, bidFeeUSD, bidVolume)
|
|
||||||
profit := 0.0
|
|
||||||
averageCost := (bidAmount + bidFeeUSD) / bidVolume
|
|
||||||
|
|
||||||
for _, t := range trades {
|
|
||||||
if t.Symbol != c.Symbol {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if t.IsBuyer {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
profit += (t.Price - averageCost) * t.Quantity
|
|
||||||
askVolume += t.Quantity
|
|
||||||
}
|
|
||||||
|
|
||||||
profit -= feeUSD
|
|
||||||
unrealizedProfit := profit
|
|
||||||
|
|
||||||
stock := bidVolume - askVolume
|
|
||||||
if stock > 0 {
|
|
||||||
stockFee := c.CurrentPrice * stock * feeRate
|
|
||||||
unrealizedProfit += (c.CurrentPrice-averageCost)*stock - stockFee
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ProfitAndLossReport{
|
|
||||||
Symbol: c.Symbol,
|
|
||||||
StartTime: c.StartTime,
|
|
||||||
CurrentPrice: c.CurrentPrice,
|
|
||||||
NumTrades: len(trades),
|
|
||||||
|
|
||||||
BidVolume: bidVolume,
|
|
||||||
AskVolume: askVolume,
|
|
||||||
|
|
||||||
Stock: stock,
|
|
||||||
Profit: profit,
|
|
||||||
UnrealizedProfit: unrealizedProfit,
|
|
||||||
AverageBidCost: averageCost,
|
|
||||||
FeeUSD: feeUSD,
|
|
||||||
CurrencyFees: currencyFees,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProfitAndLossReport struct {
|
|
||||||
CurrentPrice float64
|
|
||||||
StartTime time.Time
|
|
||||||
Symbol string
|
|
||||||
|
|
||||||
NumTrades int
|
|
||||||
Profit float64
|
|
||||||
UnrealizedProfit float64
|
|
||||||
AverageBidCost float64
|
|
||||||
BidVolume float64
|
|
||||||
AskVolume float64
|
|
||||||
FeeUSD float64
|
|
||||||
Stock float64
|
|
||||||
CurrencyFees map[string]float64
|
|
||||||
}
|
|
||||||
|
|
||||||
func (report ProfitAndLossReport) Print() {
|
|
||||||
log.Infof("trades since: %v", report.StartTime)
|
|
||||||
log.Infof("average bid cost: %s", USD.FormatMoneyFloat64(report.AverageBidCost))
|
|
||||||
log.Infof("total bid volume: %f", report.BidVolume)
|
|
||||||
log.Infof("total ask volume: %f", report.AskVolume)
|
|
||||||
log.Infof("stock: %f", report.Stock)
|
|
||||||
log.Infof("fee (USD): %f", report.FeeUSD)
|
|
||||||
log.Infof("current price: %s", USD.FormatMoneyFloat64(report.CurrentPrice))
|
|
||||||
log.Infof("profit: %s", USD.FormatMoneyFloat64(report.Profit))
|
|
||||||
log.Infof("unrealized profit: %s", USD.FormatMoneyFloat64(report.UnrealizedProfit))
|
|
||||||
log.Infof("currency fees:")
|
|
||||||
for currency, fee := range report.CurrencyFees {
|
|
||||||
log.Infof(" - %s: %f", currency, fee)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (report ProfitAndLossReport) SlackAttachment() slack.Attachment {
|
|
||||||
var color = ""
|
|
||||||
if report.UnrealizedProfit > 0 {
|
|
||||||
color = slackstyle.Green
|
|
||||||
} else {
|
|
||||||
color = slackstyle.Red
|
|
||||||
}
|
|
||||||
|
|
||||||
market, ok := types.FindMarket(report.Symbol)
|
|
||||||
if !ok {
|
|
||||||
return slack.Attachment{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return slack.Attachment{
|
|
||||||
Title: report.Symbol + " Profit and Loss report",
|
|
||||||
Text: "Profit " + USD.FormatMoney(report.Profit),
|
|
||||||
Color: color,
|
|
||||||
// Pretext: "",
|
|
||||||
// Text: "",
|
|
||||||
Fields: []slack.AttachmentField{
|
|
||||||
{Title: "Profit", Value: USD.FormatMoney(report.Profit)},
|
|
||||||
{Title: "Unrealized Profit", Value: USD.FormatMoney(report.UnrealizedProfit)},
|
|
||||||
{Title: "Current Price", Value: market.FormatPrice(report.CurrentPrice), Short: true},
|
|
||||||
{Title: "Average Cost", Value: market.FormatPrice(report.AverageBidCost), Short: true},
|
|
||||||
{Title: "Fee (USD)", Value: USD.FormatMoney(report.FeeUSD), 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},
|
|
||||||
},
|
|
||||||
Footer: report.StartTime.Format(time.RFC822),
|
|
||||||
FooterIcon: "",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,17 +1,5 @@
|
||||||
package bbgo
|
package bbgo
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/slack-go/slack"
|
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/bbgo/types"
|
|
||||||
"github.com/c9s/bbgo/pkg/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Notifier interface {
|
type Notifier interface {
|
||||||
Notify(format string, args ...interface{})
|
Notify(format string, args ...interface{})
|
||||||
}
|
}
|
||||||
|
@ -21,82 +9,3 @@ type NullNotifier struct{}
|
||||||
func (n *NullNotifier) Notify(format string, args ...interface{}) {
|
func (n *NullNotifier) Notify(format string, args ...interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SlackAttachmentCreator interface {
|
|
||||||
SlackAttachment() slack.Attachment
|
|
||||||
}
|
|
||||||
|
|
||||||
type SlackNotifier struct {
|
|
||||||
Slack *slack.Client
|
|
||||||
|
|
||||||
TradeChannel string
|
|
||||||
ErrorChannel string
|
|
||||||
InfoChannel string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *SlackNotifier) Notify(format string, args ...interface{}) {
|
|
||||||
var slackAttachments []slack.Attachment
|
|
||||||
var slackArgsOffset = -1
|
|
||||||
|
|
||||||
for idx, arg := range args {
|
|
||||||
switch a := arg.(type) {
|
|
||||||
|
|
||||||
// concrete type assert first
|
|
||||||
case slack.Attachment:
|
|
||||||
if slackArgsOffset == -1 {
|
|
||||||
slackArgsOffset = idx
|
|
||||||
}
|
|
||||||
|
|
||||||
slackAttachments = append(slackAttachments, a)
|
|
||||||
|
|
||||||
case SlackAttachmentCreator:
|
|
||||||
if slackArgsOffset == -1 {
|
|
||||||
slackArgsOffset = idx
|
|
||||||
}
|
|
||||||
|
|
||||||
slackAttachments = append(slackAttachments, a.SlackAttachment())
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var nonSlackArgs = args
|
|
||||||
if slackArgsOffset > -1 {
|
|
||||||
nonSlackArgs = args[:slackArgsOffset]
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Infof(format, nonSlackArgs...)
|
|
||||||
|
|
||||||
_, _, err := n.Slack.PostMessageContext(context.Background(), n.InfoChannel,
|
|
||||||
slack.MsgOptionText(fmt.Sprintf(format, nonSlackArgs...), true),
|
|
||||||
slack.MsgOptionAttachments(slackAttachments...))
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Errorf("slack error: %s", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *SlackNotifier) ReportTrade(trade *types.Trade) {
|
|
||||||
_, _, err := n.Slack.PostMessageContext(context.Background(), n.TradeChannel,
|
|
||||||
slack.MsgOptionText(util.Render(`:handshake: {{ .Symbol }} {{ .Side }} Trade Execution @ {{ .Price }}`, trade), true),
|
|
||||||
slack.MsgOptionAttachments(trade.SlackAttachment()))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Error("slack send error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *SlackNotifier) ReportPnL(report *ProfitAndLossReport) {
|
|
||||||
attachment := report.SlackAttachment()
|
|
||||||
|
|
||||||
_, _, err := n.Slack.PostMessageContext(context.Background(), n.TradeChannel,
|
|
||||||
slack.MsgOptionText(util.Render(
|
|
||||||
`:heavy_dollar_sign: Here is your *{{ .symbol }}* PnL report collected since *{{ .startTime }}*`,
|
|
||||||
map[string]interface{}{
|
|
||||||
"symbol": report.Symbol,
|
|
||||||
"startTime": report.StartTime.Format(time.RFC822),
|
|
||||||
}), true),
|
|
||||||
slack.MsgOptionAttachments(attachment))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Errorf("slack send error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,14 @@ type LogHook struct {
|
||||||
ErrorChannel string
|
ErrorChannel string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewLogHook(token string, channel string) *LogHook {
|
||||||
|
var client = slack.New(token)
|
||||||
|
return &LogHook{
|
||||||
|
Slack: client,
|
||||||
|
ErrorChannel: channel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (t *LogHook) Levels() []logrus.Level {
|
func (t *LogHook) Levels() []logrus.Level {
|
||||||
return []logrus.Level{
|
return []logrus.Level{
|
||||||
// log.InfoLevel,
|
// log.InfoLevel,
|
||||||
|
|
|
@ -10,7 +10,9 @@ import (
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
"github.com/c9s/bbgo/pkg/bbgo/accounting"
|
||||||
"github.com/c9s/bbgo/pkg/bbgo/config"
|
"github.com/c9s/bbgo/pkg/bbgo/config"
|
||||||
|
"github.com/c9s/bbgo/pkg/bbgo/notifier/slacknotifier"
|
||||||
"github.com/c9s/bbgo/pkg/bbgo/service"
|
"github.com/c9s/bbgo/pkg/bbgo/service"
|
||||||
|
|
||||||
"github.com/c9s/bbgo/pkg/bbgo/exchange/binance"
|
"github.com/c9s/bbgo/pkg/bbgo/exchange/binance"
|
||||||
|
@ -27,7 +29,7 @@ type Trader struct {
|
||||||
TradeService *service.TradeService
|
TradeService *service.TradeService
|
||||||
TradeSync *service.TradeSync
|
TradeSync *service.TradeSync
|
||||||
|
|
||||||
Notifier *SlackNotifier
|
Notifier *slacknotifier.Notifier
|
||||||
|
|
||||||
// Context is trading Context
|
// Context is trading Context
|
||||||
Context *Context
|
Context *Context
|
||||||
|
@ -36,7 +38,7 @@ type Trader struct {
|
||||||
|
|
||||||
reportTimer *time.Timer
|
reportTimer *time.Timer
|
||||||
|
|
||||||
ProfitAndLossCalculator *ProfitAndLossCalculator
|
ProfitAndLossCalculator *accounting.ProfitAndLossCalculator
|
||||||
|
|
||||||
Account *Account
|
Account *Account
|
||||||
}
|
}
|
||||||
|
@ -118,7 +120,7 @@ func (trader *Trader) Initialize(ctx context.Context, startTime time.Time) error
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
trader.ProfitAndLossCalculator = &ProfitAndLossCalculator{
|
trader.ProfitAndLossCalculator = &accounting.ProfitAndLossCalculator{
|
||||||
TradingFeeCurrency: tradingFeeCurrency,
|
TradingFeeCurrency: tradingFeeCurrency,
|
||||||
Symbol: trader.Symbol,
|
Symbol: trader.Symbol,
|
||||||
StartTime: startTime,
|
StartTime: startTime,
|
||||||
|
@ -247,7 +249,7 @@ func (trader *Trader) RunStrategy(ctx context.Context, strategy Strategy) (chan
|
||||||
log.WithError(err).Error("trade insert error")
|
log.WithError(err).Error("trade insert error")
|
||||||
}
|
}
|
||||||
|
|
||||||
trader.Notifier.ReportTrade(trade)
|
trader.Notifier.NotifyTrade(trade)
|
||||||
trader.ProfitAndLossCalculator.AddTrade(*trade)
|
trader.ProfitAndLossCalculator.AddTrade(*trade)
|
||||||
_, err := trader.Context.StockManager.AddTrades([]types.Trade{*trade})
|
_, err := trader.Context.StockManager.AddTrades([]types.Trade{*trade})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -298,7 +300,7 @@ func (trader *Trader) RunStrategy(ctx context.Context, strategy Strategy) (chan
|
||||||
func (trader *Trader) reportPnL() {
|
func (trader *Trader) reportPnL() {
|
||||||
report := trader.ProfitAndLossCalculator.Calculate()
|
report := trader.ProfitAndLossCalculator.Calculate()
|
||||||
report.Print()
|
report.Print()
|
||||||
trader.Notifier.ReportPnL(report)
|
trader.Notifier.NotifyPnL(report)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (trader *Trader) SubmitOrder(ctx context.Context, order *types.SubmitOrder) {
|
func (trader *Trader) SubmitOrder(ctx context.Context, order *types.SubmitOrder) {
|
||||||
|
|
8
bbgo/types/currencies.go
Normal file
8
bbgo/types/currencies.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
import "github.com/leekchan/accounting"
|
||||||
|
|
||||||
|
var USD = accounting.Accounting{Symbol: "$ ", Precision: 2}
|
||||||
|
var BTC = accounting.Accounting{Symbol: "BTC ", Precision: 2}
|
||||||
|
var BNB = accounting.Accounting{Symbol: "BNB ", Precision: 4}
|
||||||
|
|
|
@ -1,15 +1,10 @@
|
||||||
package types
|
package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/leekchan/accounting"
|
|
||||||
"math"
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var USD = accounting.Accounting{Symbol: "$ ", Precision: 2}
|
|
||||||
var BTC = accounting.Accounting{Symbol: "BTC ", Precision: 2}
|
|
||||||
var BNB = accounting.Accounting{Symbol: "BNB ", Precision: 4}
|
|
||||||
|
|
||||||
type Market struct {
|
type Market struct {
|
||||||
Symbol string
|
Symbol string
|
||||||
PricePrecision int
|
PricePrecision int
|
||||||
|
|
Loading…
Reference in New Issue
Block a user