diff --git a/pkg/accounting/pnl/avg_cost.go b/pkg/accounting/pnl/avg_cost.go index 3d16e997e..6e0f558a7 100644 --- a/pkg/accounting/pnl/avg_cost.go +++ b/pkg/accounting/pnl/avg_cost.go @@ -21,12 +21,13 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c if len(trades) == 0 { return &AverageCostPnlReport{ - Symbol: symbol, - CurrentPrice: currentPrice, - NumTrades: 0, - BuyVolume: bidVolume, - SellVolume: askVolume, - FeeInUSD: feeUSD, + Symbol: symbol, + Market: c.Market, + LastPrice: currentPrice, + NumTrades: 0, + BuyVolume: bidVolume, + SellVolume: askVolume, + FeeInUSD: feeUSD, } } @@ -68,10 +69,11 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c unrealizedProfit := (fixedpoint.NewFromFloat(currentPrice) - position.AverageCost).Mul(position.Base) return &AverageCostPnlReport{ - Symbol: symbol, - CurrentPrice: currentPrice, - NumTrades: len(trades), - StartTime: time.Time(trades[0].Time), + Symbol: symbol, + Market: c.Market, + LastPrice: currentPrice, + NumTrades: len(trades), + StartTime: time.Time(trades[0].Time), BuyVolume: bidVolume, SellVolume: askVolume, @@ -80,7 +82,7 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c Profit: totalProfit, NetProfit: totalNetProfit, UnrealizedProfit: unrealizedProfit, - AverageBidCost: position.AverageCost.Float64(), + AverageCost: position.AverageCost.Float64(), FeeInUSD: (totalProfit - totalNetProfit).Float64(), CurrencyFees: currencyFees, } diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index 697a9024a..1ef28736e 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -3,8 +3,11 @@ package cmd import ( "bufio" "context" + "encoding/json" "fmt" + "io/ioutil" "os" + "path/filepath" "strings" "time" @@ -28,6 +31,7 @@ func init() { BacktestCmd.Flags().CountP("verbose", "v", "verbose level") BacktestCmd.Flags().String("config", "config/bbgo.yaml", "strategy config file") BacktestCmd.Flags().Bool("force", false, "force execution without confirm") + BacktestCmd.Flags().String("output", "", "the report output directory") RootCmd.AddCommand(BacktestCmd) } @@ -65,6 +69,12 @@ var BacktestCmd = &cobra.Command{ return err } + outputDirectory, err := cmd.Flags().GetString("output") + if err != nil { + return err + } + jsonOutputEnabled := len(outputDirectory) > 0 + syncOnly, err := cmd.Flags().GetBool("sync-only") if err != nil { return err @@ -270,7 +280,7 @@ var BacktestCmd = &cobra.Command{ calculator := &pnl.AverageCostCalculator{ TradingFeeCurrency: backtestExchange.PlatformFeeCurrency(), - Market: market, + Market: market, } startPrice, ok := session.StartPrice(symbol) @@ -278,14 +288,14 @@ var BacktestCmd = &cobra.Command{ return fmt.Errorf("start price not found: %s", symbol) } - log.Infof("%s PROFIT AND LOSS REPORT", symbol) - log.Infof("===============================================") - lastPrice, ok := session.LastPrice(symbol) if !ok { return fmt.Errorf("last price not found: %s", symbol) } + log.Infof("%s PROFIT AND LOSS REPORT", symbol) + log.Infof("===============================================") + report := calculator.Calculate(symbol, trades.Trades, lastPrice) report.Print() @@ -298,6 +308,29 @@ var BacktestCmd = &cobra.Command{ log.Infof("FINAL BALANCES:") finalBalances.Print() + if jsonOutputEnabled { + result := struct { + Symbol string `json:"symbol,omitempty"` + PnLReport *pnl.AverageCostPnlReport `json:"pnlReport,omitempty"` + InitialBalances types.BalanceMap `json:"initialBalances,omitempty"` + FinalBalances types.BalanceMap `json:"finalBalances,omitempty"` + }{ + Symbol: symbol, + PnLReport: report, + InitialBalances: initBalances, + FinalBalances: finalBalances, + } + + jsonOutput, err := json.MarshalIndent(&result,"", " ") + if err != nil { + return err + } + + if err := ioutil.WriteFile(filepath.Join(outputDirectory, symbol + ".json"), jsonOutput, 0644) ; err != nil { + return err + } + } + if wantBaseAssetBaseline { initBaseAsset := inBaseAsset(initBalances, market, startPrice) finalBaseAsset := inBaseAsset(finalBalances, market, lastPrice) diff --git a/pkg/types/market.go b/pkg/types/market.go index 446867a71..78308f2a4 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -3,10 +3,11 @@ package types import ( "encoding/json" "fmt" - "github.com/leekchan/accounting" "math" "strconv" "time" + + "github.com/leekchan/accounting" ) type Duration time.Duration @@ -48,27 +49,27 @@ func (d *Duration) UnmarshalJSON(data []byte) error { } type Market struct { - Symbol string - LocalSymbol string // LocalSymbol is used for exchange's API + Symbol string `json:"symbol"` + LocalSymbol string `json:"localSymbol,omitempty" `// LocalSymbol is used for exchange's API - PricePrecision int - VolumePrecision int - QuoteCurrency string - BaseCurrency string + PricePrecision int `json:"pricePrecision,omitempty"` + VolumePrecision int `json:"volumePrecision,omitempty"` + QuoteCurrency string `json:"quoteCurrency,omitempty"` + BaseCurrency string `json:"baseCurrency,omitempty"` // The MIN_NOTIONAL filter defines the minimum notional value allowed for an order on a symbol. // An order's notional value is the price * quantity - MinNotional float64 - MinAmount float64 + MinNotional float64 `json:"minNotional,omitempty"` + MinAmount float64 `json:"minAmount,omitempty"` // The LOT_SIZE filter defines the quantity - MinQuantity float64 - MaxQuantity float64 - StepSize float64 + MinQuantity float64 `json:"minQuantity,omitempty"` + MaxQuantity float64 `json:"maxQuantity,omitempty"` + StepSize float64 `json:"stepSize,omitempty"` - MinPrice float64 - MaxPrice float64 - TickSize float64 + MinPrice float64 `json:"minPrice,omitempty"` + MaxPrice float64 `json:"maxPrice,omitempty"` + TickSize float64 `json:"tickSize,omitempty"` } func (m Market) BaseCurrencyFormatter() *accounting.Accounting {