diff --git a/pkg/accounting/pnl/avg_cost.go b/pkg/accounting/pnl/avg_cost.go index 7db141398..b31eed2ea 100644 --- a/pkg/accounting/pnl/avg_cost.go +++ b/pkg/accounting/pnl/avg_cost.go @@ -81,6 +81,7 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c unrealizedProfit := currentPrice.Sub(position.AverageCost). Mul(position.GetBase()) + return &AverageCostPnlReport{ Symbol: symbol, Market: c.Market, diff --git a/pkg/accounting/pnl/report.go b/pkg/accounting/pnl/report.go index 8ddffecb6..a72333c2b 100644 --- a/pkg/accounting/pnl/report.go +++ b/pkg/accounting/pnl/report.go @@ -5,7 +5,7 @@ import ( "strconv" "time" - log "github.com/sirupsen/logrus" + "github.com/fatih/color" "github.com/slack-go/slack" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -15,15 +15,15 @@ import ( ) type AverageCostPnlReport struct { - LastPrice fixedpoint.Value `json:"lastPrice"` - StartTime time.Time `json:"startTime"` - Symbol string `json:"symbol"` - Market types.Market `json:"market"` + LastPrice fixedpoint.Value `json:"lastPrice"` + StartTime time.Time `json:"startTime"` + Symbol string `json:"symbol"` + Market types.Market `json:"market"` - NumTrades int `json:"numTrades"` - Profit fixedpoint.Value `json:"profit"` - NetProfit fixedpoint.Value `json:"netProfit"` - UnrealizedProfit fixedpoint.Value `json:"unrealizedProfit"` + NumTrades int `json:"numTrades"` + Profit fixedpoint.Value `json:"profit"` + NetProfit fixedpoint.Value `json:"netProfit"` + UnrealizedProfit fixedpoint.Value `json:"unrealizedProfit"` AverageCost fixedpoint.Value `json:"averageCost"` BuyVolume fixedpoint.Value `json:"buyVolume,omitempty"` SellVolume fixedpoint.Value `json:"sellVolume,omitempty"` @@ -37,19 +37,29 @@ func (report *AverageCostPnlReport) JSON() ([]byte, error) { } func (report AverageCostPnlReport) Print() { - log.Infof("TRADES SINCE: %v", report.StartTime) - log.Infof("NUMBER OF TRADES: %d", report.NumTrades) - log.Infof("AVERAGE COST: %s", types.USD.FormatMoney(report.AverageCost)) - log.Infof("TOTAL BUY VOLUME: %v", report.BuyVolume) - log.Infof("TOTAL SELL VOLUME: %v", report.SellVolume) + color.Green("TRADES SINCE: %v", report.StartTime) + color.Green("NUMBER OF TRADES: %d", report.NumTrades) + color.Green("AVERAGE COST: %s", types.USD.FormatMoney(report.AverageCost)) + color.Green("TOTAL BUY VOLUME: %v", report.BuyVolume) + color.Green("TOTAL SELL VOLUME: %v", report.SellVolume) - log.Infof("CURRENT PRICE: %s", types.USD.FormatMoney(report.LastPrice)) - log.Infof("CURRENCY FEES:") + color.Green("CURRENT PRICE: %s", types.USD.FormatMoney(report.LastPrice)) + color.Green("CURRENCY FEES:") for currency, fee := range report.CurrencyFees { - log.Infof(" - %s: %s", currency, fee.String()) + color.Green(" - %s: %s", currency, fee.String()) + } + + if report.Profit.Sign() > 0 { + color.Green("PROFIT: %s", types.USD.FormatMoney(report.Profit)) + } else { + color.Red("PROFIT: %s", types.USD.FormatMoney(report.Profit)) + } + + if report.UnrealizedProfit.Sign() > 0 { + color.Green("UNREALIZED PROFIT: %s", types.USD.FormatMoney(report.UnrealizedProfit)) + } else { + color.Red("UNREALIZED PROFIT: %s", types.USD.FormatMoney(report.UnrealizedProfit)) } - log.Infof("PROFIT: %s", types.USD.FormatMoney(report.Profit)) - log.Infof("UNREALIZED PROFIT: %s", types.USD.FormatMoney(report.UnrealizedProfit)) } func (report AverageCostPnlReport) SlackAttachment() slack.Attachment { diff --git a/pkg/backtest/report.go b/pkg/backtest/report.go index cb65ca5c2..ae3316503 100644 --- a/pkg/backtest/report.go +++ b/pkg/backtest/report.go @@ -10,7 +10,18 @@ import ( "github.com/c9s/bbgo/pkg/types" ) -type Report struct { +// SummaryReport is the summary of the back-test session +type SummaryReport struct { + StartTime time.Time `json:"startTime"` + EndTime time.Time `json:"endTime"` + Sessions []string `json:"sessions"` + InitialTotalBalances types.BalanceMap `json:"initialTotalBalances"` + FinalTotalBalances types.BalanceMap `json:"finalTotalBalances"` +} + +// SessionSymbolReport is the report per exchange session +// trades are merged, collected and re-calculated +type SessionSymbolReport struct { StartTime time.Time `json:"startTime"` EndTime time.Time `json:"endTime"` Symbol string `json:"symbol,omitempty"` diff --git a/pkg/bbgo/config.go b/pkg/bbgo/config.go index 9e66365c7..0e5a36da9 100644 --- a/pkg/bbgo/config.go +++ b/pkg/bbgo/config.go @@ -104,13 +104,22 @@ type Backtest struct { EndTime *types.LooseFormatTime `json:"endTime,omitempty" yaml:"endTime,omitempty"` // RecordTrades is an option, if set to true, back-testing should record the trades into database - RecordTrades bool `json:"recordTrades,omitempty" yaml:"recordTrades,omitempty"` + RecordTrades bool `json:"recordTrades,omitempty" yaml:"recordTrades,omitempty"` // Account is deprecated, use Accounts instead - Account map[string]BacktestAccount `json:"account" yaml:"account"` - Accounts map[string]BacktestAccount `json:"accounts" yaml:"accounts"` - Symbols []string `json:"symbols" yaml:"symbols"` - Sessions []string `json:"sessions" yaml:"sessions"` + Account map[string]BacktestAccount `json:"account" yaml:"account"` + Accounts map[string]BacktestAccount `json:"accounts" yaml:"accounts"` + Symbols []string `json:"symbols" yaml:"symbols"` + Sessions []string `json:"sessions" yaml:"sessions"` +} + +func (b *Backtest) GetAccount(n string) BacktestAccount { + accountConfig, ok := b.Accounts[n] + if ok { + return accountConfig + } + + return b.Account[n] } type BacktestAccount struct { diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index 9ea020d54..84b4ea24f 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -432,12 +432,43 @@ var BacktestCmd = &cobra.Command{ // put the logger back to print the pnl log.SetLevel(log.InfoLevel) - log.Infof("BACK-TEST REPORT") - log.Infof("===============================================") - log.Infof("START TIME: %s", startTime.Format(time.RFC1123)) - log.Infof("END TIME: %s", endTime.Format(time.RFC1123)) + color.Green("BACK-TEST REPORT") + color.Green("===============================================\n") + color.Green("START TIME: %s\n", startTime.Format(time.RFC1123)) + color.Green("END TIME: %s\n", endTime.Format(time.RFC1123)) + + // aggregate total balances + initTotalBalances := types.BalanceMap{} + finalTotalBalances := types.BalanceMap{} + sessionNames := []string{} for _, session := range environ.Sessions() { - backtestExchange := session.Exchange.(*backtest.Exchange) + sessionNames = append(sessionNames, session.Name) + accountConfig := userConfig.Backtest.GetAccount(session.Name) + initBalances := accountConfig.Balances.BalanceMap() + initTotalBalances = initTotalBalances.Add(initBalances) + + finalBalances := session.GetAccount().Balances() + finalTotalBalances = finalTotalBalances.Add(finalBalances) + } + color.Green("INITIAL TOTAL BALANCE: %v\n", initTotalBalances) + color.Green("FINAL TOTAL BALANCE: %v\n", finalTotalBalances) + + summaryReport := &backtest.SummaryReport{ + StartTime: startTime, + EndTime: endTime, + Sessions: sessionNames, + InitialTotalBalances: initTotalBalances, + FinalTotalBalances: finalTotalBalances, + } + _ = summaryReport + + for _, session := range environ.Sessions() { + backtestExchange, ok := session.Exchange.(*backtest.Exchange) + if !ok { + return fmt.Errorf("unexpected error, exchange instance is not a backtest exchange") + } + + // per symbol report exchangeName := session.Exchange.Name().String() for symbol, trades := range session.Trades { market, ok := session.Market(symbol) @@ -466,22 +497,12 @@ var BacktestCmd = &cobra.Command{ report := calculator.Calculate(symbol, trades.Trades, lastPrice) report.Print() - accountConfig, ok := userConfig.Backtest.Accounts[exchangeName] - if !ok { - accountConfig = userConfig.Backtest.Account[exchangeName] - } - + accountConfig := userConfig.Backtest.GetAccount(exchangeName) initBalances := accountConfig.Balances.BalanceMap() finalBalances := session.GetAccount().Balances() - log.Infof("INITIAL BALANCES:") - initBalances.Print() - - log.Infof("FINAL BALANCES:") - finalBalances.Print() - if generatingReport { - result := backtest.Report{ + result := backtest.SessionSymbolReport{ StartTime: startTime, EndTime: endTime, Symbol: symbol, @@ -493,20 +514,15 @@ var BacktestCmd = &cobra.Command{ Manifests: manifests, } - jsonOutput, err := json.MarshalIndent(&result, "", " ") - if err != nil { - return err - } - - if err := ioutil.WriteFile(filepath.Join(outputDirectory, symbol+".json"), jsonOutput, 0644); err != nil { + if err := writeJsonFile(filepath.Join(outputDirectory, symbol+".json"), &result) ; err != nil { return err } } initQuoteAsset := inQuoteAsset(initBalances, market, startPrice) finalQuoteAsset := inQuoteAsset(finalBalances, market, lastPrice) - log.Infof("INITIAL ASSET IN %s ~= %s %s (1 %s = %v)", market.QuoteCurrency, market.FormatQuantity(initQuoteAsset), market.QuoteCurrency, market.BaseCurrency, startPrice) - log.Infof("FINAL ASSET IN %s ~= %s %s (1 %s = %v)", market.QuoteCurrency, market.FormatQuantity(finalQuoteAsset), market.QuoteCurrency, market.BaseCurrency, lastPrice) + color.Green("INITIAL ASSET IN %s ~= %s %s (1 %s = %v)", market.QuoteCurrency, market.FormatQuantity(initQuoteAsset), market.QuoteCurrency, market.BaseCurrency, startPrice) + color.Green("FINAL ASSET IN %s ~= %s %s (1 %s = %v)", market.QuoteCurrency, market.FormatQuantity(finalQuoteAsset), market.QuoteCurrency, market.BaseCurrency, lastPrice) if report.Profit.Sign() > 0 { color.Green("REALIZED PROFIT: +%v %s", report.Profit, market.QuoteCurrency) @@ -577,6 +593,15 @@ func confirmation(s string) bool { } } +func writeJsonFile(p string, obj interface{}) error { + out, err := json.MarshalIndent(obj, "", " ") + if err != nil { + return err + } + + return ioutil.WriteFile(p, out, 0644) +} + func safeMkdirAll(p string) error { st, err := os.Stat(p) if err == nil { diff --git a/pkg/types/balance.go b/pkg/types/balance.go index e9529661f..87d5d6537 100644 --- a/pkg/types/balance.go +++ b/pkg/types/balance.go @@ -6,8 +6,6 @@ import ( "strings" "time" - "github.com/sirupsen/logrus" - "github.com/c9s/bbgo/pkg/fixedpoint" ) @@ -193,11 +191,15 @@ func (m BalanceMap) Print() { continue } + fmt.Printf(" %s: %v", balance.Currency, balance.Available) if balance.Locked.Sign() > 0 { - logrus.Infof(" %s: %v (locked %v)", balance.Currency, balance.Available, balance.Locked) - } else { - logrus.Infof(" %s: %v", balance.Currency, balance.Available) + fmt.Printf(" (locked %v)", balance.Locked) } + + if balance.Borrowed.Sign() > 0 { + fmt.Printf(" (borrowed %v)", balance.Borrowed) + } + fmt.Println() } }