support json output for backtesting

This commit is contained in:
c9s 2021-12-06 01:05:33 +08:00
parent 1e151a170a
commit 474be4e815
3 changed files with 66 additions and 30 deletions

View File

@ -21,12 +21,13 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c
if len(trades) == 0 { if len(trades) == 0 {
return &AverageCostPnlReport{ return &AverageCostPnlReport{
Symbol: symbol, Symbol: symbol,
CurrentPrice: currentPrice, Market: c.Market,
NumTrades: 0, LastPrice: currentPrice,
BuyVolume: bidVolume, NumTrades: 0,
SellVolume: askVolume, BuyVolume: bidVolume,
FeeInUSD: feeUSD, 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) unrealizedProfit := (fixedpoint.NewFromFloat(currentPrice) - position.AverageCost).Mul(position.Base)
return &AverageCostPnlReport{ return &AverageCostPnlReport{
Symbol: symbol, Symbol: symbol,
CurrentPrice: currentPrice, Market: c.Market,
NumTrades: len(trades), LastPrice: currentPrice,
StartTime: time.Time(trades[0].Time), NumTrades: len(trades),
StartTime: time.Time(trades[0].Time),
BuyVolume: bidVolume, BuyVolume: bidVolume,
SellVolume: askVolume, SellVolume: askVolume,
@ -80,7 +82,7 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c
Profit: totalProfit, Profit: totalProfit,
NetProfit: totalNetProfit, NetProfit: totalNetProfit,
UnrealizedProfit: unrealizedProfit, UnrealizedProfit: unrealizedProfit,
AverageBidCost: position.AverageCost.Float64(), AverageCost: position.AverageCost.Float64(),
FeeInUSD: (totalProfit - totalNetProfit).Float64(), FeeInUSD: (totalProfit - totalNetProfit).Float64(),
CurrencyFees: currencyFees, CurrencyFees: currencyFees,
} }

View File

@ -3,8 +3,11 @@ package cmd
import ( import (
"bufio" "bufio"
"context" "context"
"encoding/json"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath"
"strings" "strings"
"time" "time"
@ -28,6 +31,7 @@ func init() {
BacktestCmd.Flags().CountP("verbose", "v", "verbose level") BacktestCmd.Flags().CountP("verbose", "v", "verbose level")
BacktestCmd.Flags().String("config", "config/bbgo.yaml", "strategy config file") BacktestCmd.Flags().String("config", "config/bbgo.yaml", "strategy config file")
BacktestCmd.Flags().Bool("force", false, "force execution without confirm") BacktestCmd.Flags().Bool("force", false, "force execution without confirm")
BacktestCmd.Flags().String("output", "", "the report output directory")
RootCmd.AddCommand(BacktestCmd) RootCmd.AddCommand(BacktestCmd)
} }
@ -65,6 +69,12 @@ var BacktestCmd = &cobra.Command{
return err return err
} }
outputDirectory, err := cmd.Flags().GetString("output")
if err != nil {
return err
}
jsonOutputEnabled := len(outputDirectory) > 0
syncOnly, err := cmd.Flags().GetBool("sync-only") syncOnly, err := cmd.Flags().GetBool("sync-only")
if err != nil { if err != nil {
return err return err
@ -270,7 +280,7 @@ var BacktestCmd = &cobra.Command{
calculator := &pnl.AverageCostCalculator{ calculator := &pnl.AverageCostCalculator{
TradingFeeCurrency: backtestExchange.PlatformFeeCurrency(), TradingFeeCurrency: backtestExchange.PlatformFeeCurrency(),
Market: market, Market: market,
} }
startPrice, ok := session.StartPrice(symbol) startPrice, ok := session.StartPrice(symbol)
@ -278,14 +288,14 @@ var BacktestCmd = &cobra.Command{
return fmt.Errorf("start price not found: %s", symbol) return fmt.Errorf("start price not found: %s", symbol)
} }
log.Infof("%s PROFIT AND LOSS REPORT", symbol)
log.Infof("===============================================")
lastPrice, ok := session.LastPrice(symbol) lastPrice, ok := session.LastPrice(symbol)
if !ok { if !ok {
return fmt.Errorf("last price not found: %s", symbol) 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 := calculator.Calculate(symbol, trades.Trades, lastPrice)
report.Print() report.Print()
@ -298,6 +308,29 @@ var BacktestCmd = &cobra.Command{
log.Infof("FINAL BALANCES:") log.Infof("FINAL BALANCES:")
finalBalances.Print() 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 { if wantBaseAssetBaseline {
initBaseAsset := inBaseAsset(initBalances, market, startPrice) initBaseAsset := inBaseAsset(initBalances, market, startPrice)
finalBaseAsset := inBaseAsset(finalBalances, market, lastPrice) finalBaseAsset := inBaseAsset(finalBalances, market, lastPrice)

View File

@ -3,10 +3,11 @@ package types
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/leekchan/accounting"
"math" "math"
"strconv" "strconv"
"time" "time"
"github.com/leekchan/accounting"
) )
type Duration time.Duration type Duration time.Duration
@ -48,27 +49,27 @@ func (d *Duration) UnmarshalJSON(data []byte) error {
} }
type Market struct { type Market struct {
Symbol string Symbol string `json:"symbol"`
LocalSymbol string // LocalSymbol is used for exchange's API LocalSymbol string `json:"localSymbol,omitempty" `// LocalSymbol is used for exchange's API
PricePrecision int PricePrecision int `json:"pricePrecision,omitempty"`
VolumePrecision int VolumePrecision int `json:"volumePrecision,omitempty"`
QuoteCurrency string QuoteCurrency string `json:"quoteCurrency,omitempty"`
BaseCurrency string BaseCurrency string `json:"baseCurrency,omitempty"`
// The MIN_NOTIONAL filter defines the minimum notional value allowed for an order on a symbol. // 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 // An order's notional value is the price * quantity
MinNotional float64 MinNotional float64 `json:"minNotional,omitempty"`
MinAmount float64 MinAmount float64 `json:"minAmount,omitempty"`
// The LOT_SIZE filter defines the quantity // The LOT_SIZE filter defines the quantity
MinQuantity float64 MinQuantity float64 `json:"minQuantity,omitempty"`
MaxQuantity float64 MaxQuantity float64 `json:"maxQuantity,omitempty"`
StepSize float64 StepSize float64 `json:"stepSize,omitempty"`
MinPrice float64 MinPrice float64 `json:"minPrice,omitempty"`
MaxPrice float64 MaxPrice float64 `json:"maxPrice,omitempty"`
TickSize float64 TickSize float64 `json:"tickSize,omitempty"`
} }
func (m Market) BaseCurrencyFormatter() *accounting.Accounting { func (m Market) BaseCurrencyFormatter() *accounting.Accounting {