fix cyclic imports

This commit is contained in:
c9s 2020-09-19 09:05:06 +08:00
parent c92ada2f34
commit 42a32924a7
13 changed files with 347 additions and 294 deletions

View File

@ -1,7 +1 @@
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
View 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
View 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: "",
}
}

View File

@ -3,6 +3,7 @@ package bbgo
import (
"sync"
"github.com/c9s/bbgo/pkg/bbgo/accounting"
"github.com/c9s/bbgo/pkg/bbgo/types"
)
@ -19,7 +20,7 @@ type Context struct {
Balances map[string]types.Balance
Quota map[string]types.Balance
ProfitAndLossCalculator *ProfitAndLossCalculator
ProfitAndLossCalculator *accounting.ProfitAndLossCalculator
StockManager *StockManager
}

View File

@ -6,6 +6,7 @@ import (
"github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/bbgo/accounting"
"github.com/c9s/bbgo/pkg/bbgo/types"
"github.com/c9s/bbgo/pkg/util"
)
@ -14,7 +15,7 @@ type KLineRegressionTrader struct {
// Context is trading Context
Context *Context
SourceKLines []types.KLine
ProfitAndLossCalculator *ProfitAndLossCalculator
ProfitAndLossCalculator *accounting.ProfitAndLossCalculator
doneOrders []*types.SubmitOrder
pendingOrders []*types.SubmitOrder

View 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")
}
}

View File

@ -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.Available < p.MinQuoteBalance {
return errors.Wrapf(ErrQuoteBalanceLevelTooLow, "quote balance level is too low: %s < %s",
USD.FormatMoneyFloat64(balance.Available),
USD.FormatMoneyFloat64(p.MinQuoteBalance))
types.USD.FormatMoneyFloat64(balance.Available),
types.USD.FormatMoneyFloat64(p.MinQuoteBalance))
}
if baseBalance, ok := tradingCtx.Balances[market.BaseCurrency]; ok {

View File

@ -1,185 +1,2 @@
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: "",
}
}

View File

@ -1,17 +1,5 @@
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 {
Notify(format string, args ...interface{})
}
@ -21,82 +9,3 @@ type NullNotifier struct{}
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")
}
}

View File

@ -14,6 +14,14 @@ type LogHook struct {
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 {
return []logrus.Level{
// log.InfoLevel,

View File

@ -10,7 +10,9 @@ import (
"github.com/jmoiron/sqlx"
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/notifier/slacknotifier"
"github.com/c9s/bbgo/pkg/bbgo/service"
"github.com/c9s/bbgo/pkg/bbgo/exchange/binance"
@ -27,7 +29,7 @@ type Trader struct {
TradeService *service.TradeService
TradeSync *service.TradeSync
Notifier *SlackNotifier
Notifier *slacknotifier.Notifier
// Context is trading Context
Context *Context
@ -36,7 +38,7 @@ type Trader struct {
reportTimer *time.Timer
ProfitAndLossCalculator *ProfitAndLossCalculator
ProfitAndLossCalculator *accounting.ProfitAndLossCalculator
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,
Symbol: trader.Symbol,
StartTime: startTime,
@ -247,7 +249,7 @@ func (trader *Trader) RunStrategy(ctx context.Context, strategy Strategy) (chan
log.WithError(err).Error("trade insert error")
}
trader.Notifier.ReportTrade(trade)
trader.Notifier.NotifyTrade(trade)
trader.ProfitAndLossCalculator.AddTrade(*trade)
_, err := trader.Context.StockManager.AddTrades([]types.Trade{*trade})
if err != nil {
@ -298,7 +300,7 @@ func (trader *Trader) RunStrategy(ctx context.Context, strategy Strategy) (chan
func (trader *Trader) reportPnL() {
report := trader.ProfitAndLossCalculator.Calculate()
report.Print()
trader.Notifier.ReportPnL(report)
trader.Notifier.NotifyPnL(report)
}
func (trader *Trader) SubmitOrder(ctx context.Context, order *types.SubmitOrder) {

8
bbgo/types/currencies.go Normal file
View 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}

View File

@ -1,15 +1,10 @@
package types
import (
"github.com/leekchan/accounting"
"math"
"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 {
Symbol string
PricePrecision int