From 1eb03c3dbaedf2e0e4cb1d552905991d09e353d6 Mon Sep 17 00:00:00 2001 From: zenix Date: Mon, 29 Aug 2022 14:11:02 +0900 Subject: [PATCH] fix: taker price, matching engine kline emit order and process order, nan in sortino and sharpe --- pkg/backtest/matching.go | 17 +++++++++++++---- pkg/backtest/report.go | 16 ++++++++-------- pkg/cmd/backtest.go | 20 ++++++-------------- pkg/service/backtest.go | 6 ++++-- pkg/strategy/output.go | 2 -- pkg/types/indicator.go | 3 +++ pkg/types/trade_stats.go | 3 ++- 7 files changed, 36 insertions(+), 31 deletions(-) delete mode 100644 pkg/strategy/output.go diff --git a/pkg/backtest/matching.go b/pkg/backtest/matching.go index 64bcdef23..7b321ee08 100644 --- a/pkg/backtest/matching.go +++ b/pkg/backtest/matching.go @@ -180,10 +180,20 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *ty order := m.newOrder(o, orderID) if isTaker { + var price fixedpoint.Value if order.Type == types.OrderTypeMarket { order.Price = m.Market.TruncatePrice(m.LastPrice) + price = order.Price } else if order.Type == types.OrderTypeLimit { - order.AveragePrice = m.Market.TruncatePrice(m.LastPrice) + if m.LastKLine.High.Compare(order.Price) > 0 && order.Side == types.SideTypeBuy { + order.AveragePrice = order.Price + } else if m.LastKLine.Low.Compare(order.Price) < 0 && order.Side == types.SideTypeSell { + order.AveragePrice = order.Price + } else { + + order.AveragePrice = m.Market.TruncatePrice(m.LastPrice) + } + price = order.AveragePrice } // emit the order update for Status:New @@ -193,7 +203,7 @@ func (m *SimplePriceMatching) PlaceOrder(o types.SubmitOrder) (*types.Order, *ty var order2 = order // emit trade before we publish order - trade := m.newTradeFromOrder(&order2, false, m.Market.TruncatePrice(m.LastPrice)) + trade := m.newTradeFromOrder(&order2, false, price) m.executeTrade(trade) // unlock the rest balances for limit taker @@ -627,6 +637,7 @@ func (m *SimplePriceMatching) processKLine(kline types.KLine) { m.buyToPrice(kline.Open) } } + m.LastKLine = kline switch kline.Direction() { case types.DirectionDown: @@ -658,8 +669,6 @@ func (m *SimplePriceMatching) processKLine(kline types.KLine) { m.buyToPrice(kline.Close) } } - - m.LastKLine = kline } func (m *SimplePriceMatching) newOrder(o types.SubmitOrder, orderID uint64) types.Order { diff --git a/pkg/backtest/report.go b/pkg/backtest/report.go index fffbdd040..0f5de6a5f 100644 --- a/pkg/backtest/report.go +++ b/pkg/backtest/report.go @@ -79,8 +79,8 @@ type SessionSymbolReport struct { InitialBalances types.BalanceMap `json:"initialBalances,omitempty"` FinalBalances types.BalanceMap `json:"finalBalances,omitempty"` Manifests Manifests `json:"manifests,omitempty"` - Sharpe float64 `json:"sharpeRatio"` - Sortino float64 `json:"sortinoRatio"` + Sharpe fixedpoint.Value `json:"sharpeRatio"` + Sortino fixedpoint.Value `json:"sortinoRatio"` } func (r *SessionSymbolReport) InitialEquityValue() fixedpoint.Value { @@ -119,16 +119,16 @@ func (r *SessionSymbolReport) Print(wantBaseAssetBaseline bool) { color.Red("ASSET DECREASED: %v %s (%s)", finalQuoteAsset.Sub(initQuoteAsset), r.Market.QuoteCurrency, finalQuoteAsset.Sub(initQuoteAsset).Div(initQuoteAsset).FormatPercentage(2)) } - if r.Sharpe > 0.0 { - color.Green("REALIZED SHARPE RATIO: %.4f", r.Sharpe) + if r.Sharpe.Sign() > 0 { + color.Green("REALIZED SHARPE RATIO: %s", r.Sharpe.FormatString(4)) } else { - color.Red("REALIZED SHARPE RATIO: %.4f", r.Sharpe) + color.Red("REALIZED SHARPE RATIO: %s", r.Sharpe.FormatString(4)) } - if r.Sortino > 0.0 { - color.Green("REALIZED SORTINO RATIO: %.4f", r.Sortino) + if r.Sortino.Sign() > 0 { + color.Green("REALIZED SORTINO RATIO: %s", r.Sortino.FormatString(4)) } else { - color.Red("REALIZED SORTINO RATIO: %.4f", r.Sortino) + color.Red("REALIZED SORTINO RATIO: %s", r.Sortino.FormatString(4)) } if wantBaseAssetBaseline { diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index 4ad019106..26b9ab64c 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -4,7 +4,6 @@ import ( "bufio" "context" "fmt" - "math" "os" "path/filepath" "sort" @@ -25,6 +24,7 @@ import ( "github.com/c9s/bbgo/pkg/cmd/cmdutil" "github.com/c9s/bbgo/pkg/data/tsv" "github.com/c9s/bbgo/pkg/exchange" + "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/service" "github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/util" @@ -135,15 +135,15 @@ var BacktestCmd = &cobra.Command{ ctx, cancel := context.WithCancel(context.Background()) defer cancel() - var now = time.Now() + var now = time.Now().Local() var startTime, endTime time.Time - startTime = userConfig.Backtest.StartTime.Time() + startTime = userConfig.Backtest.StartTime.Time().Local() // set default start time to the past 6 months // userConfig.Backtest.StartTime = now.AddDate(0, -6, 0).Format("2006-01-02") if userConfig.Backtest.EndTime != nil { - endTime = userConfig.Backtest.EndTime.Time() + endTime = userConfig.Backtest.EndTime.Time().Local() } else { endTime = now } @@ -622,16 +622,8 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession, Market: market, } - finiteRatio := func(ratio float64) float64 { - if math.IsInf(ratio, 1) { - return 99999.99 - } else if math.IsInf(ratio, -1) { - return -99999.99 - } - return ratio - } - sharpeRatio := finiteRatio(intervalProfit.GetSharpe()) - sortinoRatio := finiteRatio(intervalProfit.GetSortino()) + sharpeRatio := fixedpoint.NewFromFloat(intervalProfit.GetSharpe()) + sortinoRatio := fixedpoint.NewFromFloat(intervalProfit.GetSortino()) report := calculator.Calculate(symbol, trades, lastPrice) accountConfig := userConfig.Backtest.GetAccount(session.Exchange.Name().String()) diff --git a/pkg/service/backtest.go b/pkg/service/backtest.go index 3a73726f3..5a609ff8f 100644 --- a/pkg/service/backtest.go +++ b/pkg/service/backtest.go @@ -210,10 +210,12 @@ func (s *BacktestService) QueryKLinesCh(since, until time.Time, exchange types.E tableName := targetKlineTable(exchange.Name()) var query string + // need to sort by start_time desc in order to let matching engine process 1m first + // otherwise any other close event could peek on the final close price if len(symbols) == 1 { - query = "SELECT * FROM `binance_klines` WHERE `end_time` BETWEEN :since AND :until AND `symbol` = :symbols AND `interval` IN (:intervals) ORDER BY end_time ASC" + query = "SELECT * FROM `binance_klines` WHERE `end_time` BETWEEN :since AND :until AND `symbol` = :symbols AND `interval` IN (:intervals) ORDER BY end_time ASC, start_time DESC" } else { - query = "SELECT * FROM `binance_klines` WHERE `end_time` BETWEEN :since AND :until AND `symbol` IN (:symbols) AND `interval` IN (:intervals) ORDER BY end_time ASC" + query = "SELECT * FROM `binance_klines` WHERE `end_time` BETWEEN :since AND :until AND `symbol` IN (:symbols) AND `interval` IN (:intervals) ORDER BY end_time ASC, start_time DESC" } query = strings.ReplaceAll(query, "binance_klines", tableName) diff --git a/pkg/strategy/output.go b/pkg/strategy/output.go deleted file mode 100644 index 485f0fd29..000000000 --- a/pkg/strategy/output.go +++ /dev/null @@ -1,2 +0,0 @@ -package strategy - diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index f7d3e1f63..c9b8caf7b 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -754,6 +754,9 @@ func Stdev(a Series, params ...int) float64 { diff := a.Index(i) - avg s += diff * diff } + if length-ddof == 0 { + return 0 + } return math.Sqrt(s / float64(length-ddof)) } diff --git a/pkg/types/trade_stats.go b/pkg/types/trade_stats.go index 7d8e1f356..886624ce2 100644 --- a/pkg/types/trade_stats.go +++ b/pkg/types/trade_stats.go @@ -2,10 +2,11 @@ package types import ( "encoding/json" - "log" "math" "time" + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v3" "github.com/c9s/bbgo/pkg/datatype/floats"