mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 06:53:52 +00:00
Merge pull request #812 from c9s/refactor/backtest-report
refactor: improve backtest report and fix issues
This commit is contained in:
commit
36857bd142
|
@ -113,6 +113,19 @@ const fetchPositionHistory = (basePath: string, runID: string, filename: string)
|
|||
});
|
||||
};
|
||||
|
||||
const selectPositionHistory = (data: PositionHistoryEntry[], since: Date, until: Date): PositionHistoryEntry[] => {
|
||||
const entries: PositionHistoryEntry[] = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const d = data[i];
|
||||
if (d.time < since || d.time > until) {
|
||||
continue
|
||||
}
|
||||
|
||||
entries.push(d)
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
const fetchOrders = (basePath: string, runID: string) => {
|
||||
return fetch(
|
||||
`${basePath}/${runID}/orders.tsv`,
|
||||
|
@ -124,6 +137,19 @@ const fetchOrders = (basePath: string, runID: string) => {
|
|||
});
|
||||
}
|
||||
|
||||
const selectOrders = (data: Order[], since: Date, until: Date): Order[] => {
|
||||
const entries: Order[] = [];
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const d = data[i];
|
||||
if (d.time && (d.time < since || d.time > until)) {
|
||||
continue
|
||||
}
|
||||
|
||||
entries.push(d);
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
const parseInterval = (s: string) => {
|
||||
switch (s) {
|
||||
case "1m":
|
||||
|
@ -390,7 +416,7 @@ const TradingViewChart = (props: TradingViewChartProps) => {
|
|||
const resizeObserver = useRef<any>();
|
||||
const intervals = props.reportSummary.intervals || [];
|
||||
|
||||
intervals.sort((a,b) => {
|
||||
intervals.sort((a, b) => {
|
||||
const as = parseInterval(a)
|
||||
const bs = parseInterval(b)
|
||||
if (as < bs) {
|
||||
|
@ -403,7 +429,6 @@ const TradingViewChart = (props: TradingViewChartProps) => {
|
|||
|
||||
const [currentInterval, setCurrentInterval] = useState(intervals.length > 0 ? intervals[intervals.length - 1] : '1m');
|
||||
const [showPositionBase, setShowPositionBase] = useState(false);
|
||||
const [showCanceledOrders, setShowCanceledOrders] = useState(false);
|
||||
const [showPositionAverageCost, setShowPositionAverageCost] = useState(false);
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
|
||||
|
@ -412,7 +437,6 @@ const TradingViewChart = (props: TradingViewChartProps) => {
|
|||
new Date(props.reportSummary.endTime),
|
||||
]
|
||||
const [selectedTimeRange, setSelectedTimeRange] = useState(reportTimeRange)
|
||||
const [timeRange, setTimeRange] = useState(reportTimeRange);
|
||||
|
||||
useEffect(() => {
|
||||
if (!chartContainerRef.current || chartContainerRef.current.children.length > 0) {
|
||||
|
@ -423,7 +447,7 @@ const TradingViewChart = (props: TradingViewChartProps) => {
|
|||
const fetchers = [];
|
||||
const ordersFetcher = fetchOrders(props.basePath, props.runID).then((orders: Order[] | void) => {
|
||||
if (orders) {
|
||||
const markers = ordersToMarkers(currentInterval, orders);
|
||||
const markers = ordersToMarkers(currentInterval, selectOrders(orders, selectedTimeRange[0], selectedTimeRange[1]));
|
||||
chartData.orders = orders;
|
||||
chartData.markers = markers;
|
||||
setOrders(orders);
|
||||
|
@ -436,7 +460,8 @@ const TradingViewChart = (props: TradingViewChartProps) => {
|
|||
const manifest = props.reportSummary?.manifests[0];
|
||||
if (manifest && manifest.type === "strategyProperty" && manifest.strategyProperty === "position") {
|
||||
const positionHistoryFetcher = fetchPositionHistory(props.basePath, props.runID, manifest.filename).then((data) => {
|
||||
chartData.positionHistory = data;
|
||||
chartData.positionHistory = selectPositionHistory(data as PositionHistoryEntry[], selectedTimeRange[0], selectedTimeRange[1]);
|
||||
// chartData.positionHistory = data;
|
||||
});
|
||||
fetchers.push(positionHistoryFetcher);
|
||||
}
|
||||
|
@ -594,7 +619,7 @@ const TradingViewChart = (props: TradingViewChartProps) => {
|
|||
|
||||
<TimeRangeSlider
|
||||
selectedInterval={selectedTimeRange}
|
||||
timelineInterval={timeRange}
|
||||
timelineInterval={reportTimeRange}
|
||||
formatTick={(ms: Date) => format(new Date(ms), 'M d HH')}
|
||||
step={1000 * parseInterval(currentInterval)}
|
||||
onChange={(tr: any) => {
|
||||
|
@ -661,12 +686,12 @@ const createLegendUpdater = (legend: HTMLDivElement, prefix: string) => {
|
|||
}
|
||||
}
|
||||
|
||||
const formatDate = (d : Date) : string => {
|
||||
const formatDate = (d: Date): string => {
|
||||
return moment(d).format("MMM Do YY hh:mm:ss A Z");
|
||||
}
|
||||
|
||||
const createOHLCLegendUpdater = (legend: HTMLDivElement, prefix: string) => {
|
||||
return (param: any, time : any) => {
|
||||
return (param: any, time: any) => {
|
||||
if (param) {
|
||||
const change = Math.round((param.close - param.open) * 100.0) / 100.0
|
||||
const changePercentage = Math.round((param.close - param.open) / param.close * 10000.0) / 100.0;
|
||||
|
|
|
@ -19,6 +19,8 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c
|
|||
var bidVolume = fixedpoint.Zero
|
||||
var askVolume = fixedpoint.Zero
|
||||
var feeUSD = fixedpoint.Zero
|
||||
var grossProfit = fixedpoint.Zero
|
||||
var grossLoss = fixedpoint.Zero
|
||||
|
||||
if len(trades) == 0 {
|
||||
return &AverageCostPnlReport{
|
||||
|
@ -64,6 +66,12 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c
|
|||
totalNetProfit = totalNetProfit.Add(netProfit)
|
||||
}
|
||||
|
||||
if profit.Sign() > 0 {
|
||||
grossProfit = grossProfit.Add(profit)
|
||||
} else if profit.Sign() < 0 {
|
||||
grossLoss = grossLoss.Add(profit)
|
||||
}
|
||||
|
||||
if trade.IsBuyer {
|
||||
bidVolume = bidVolume.Add(trade.Quantity)
|
||||
} else {
|
||||
|
@ -96,8 +104,12 @@ func (c *AverageCostCalculator) Calculate(symbol string, trades []types.Trade, c
|
|||
Profit: totalProfit,
|
||||
NetProfit: totalNetProfit,
|
||||
UnrealizedProfit: unrealizedProfit,
|
||||
AverageCost: position.AverageCost,
|
||||
FeeInUSD: totalProfit.Sub(totalNetProfit),
|
||||
CurrencyFees: currencyFees,
|
||||
|
||||
GrossProfit: grossProfit,
|
||||
GrossLoss: grossLoss,
|
||||
|
||||
AverageCost: position.AverageCost,
|
||||
FeeInUSD: totalProfit.Sub(totalNetProfit),
|
||||
CurrencyFees: currencyFees,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,10 +20,14 @@ type AverageCostPnlReport struct {
|
|||
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"`
|
||||
UnrealizedProfit fixedpoint.Value `json:"unrealizedProfit"`
|
||||
|
||||
NetProfit fixedpoint.Value `json:"netProfit"`
|
||||
GrossProfit fixedpoint.Value `json:"grossProfit"`
|
||||
GrossLoss fixedpoint.Value `json:"grossLoss"`
|
||||
|
||||
AverageCost fixedpoint.Value `json:"averageCost"`
|
||||
BuyVolume fixedpoint.Value `json:"buyVolume,omitempty"`
|
||||
SellVolume fixedpoint.Value `json:"sellVolume,omitempty"`
|
||||
|
|
|
@ -27,6 +27,7 @@ type StateRecorder struct {
|
|||
outputDirectory string
|
||||
strategies []Instance
|
||||
writers map[types.CsvFormatter]*tsv.Writer
|
||||
lastLines map[types.CsvFormatter][]string
|
||||
manifests Manifests
|
||||
}
|
||||
|
||||
|
@ -34,6 +35,7 @@ func NewStateRecorder(outputDir string) *StateRecorder {
|
|||
return &StateRecorder{
|
||||
outputDirectory: outputDir,
|
||||
writers: make(map[types.CsvFormatter]*tsv.Writer),
|
||||
lastLines: make(map[types.CsvFormatter][]string),
|
||||
manifests: make(Manifests),
|
||||
}
|
||||
}
|
||||
|
@ -42,11 +44,18 @@ func (r *StateRecorder) Snapshot() (int, error) {
|
|||
var c int
|
||||
for obj, writer := range r.writers {
|
||||
records := obj.CsvRecords()
|
||||
lastLine, hasLastLine := r.lastLines[obj]
|
||||
|
||||
for _, record := range records {
|
||||
if hasLastLine && equalStringSlice(lastLine, record) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := writer.Write(record); err != nil {
|
||||
return c, err
|
||||
}
|
||||
c++
|
||||
r.lastLines[obj] = record
|
||||
}
|
||||
|
||||
writer.Flush()
|
||||
|
@ -129,3 +138,19 @@ func (r *StateRecorder) Close() error {
|
|||
|
||||
return err
|
||||
}
|
||||
|
||||
func equalStringSlice(a, b []string) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := 0; i < len(a); i++ {
|
||||
ad := a[i]
|
||||
bd := b[i]
|
||||
if ad != bd {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -39,10 +39,16 @@ type SummaryReport struct {
|
|||
InitialTotalBalances types.BalanceMap `json:"initialTotalBalances"`
|
||||
FinalTotalBalances types.BalanceMap `json:"finalTotalBalances"`
|
||||
|
||||
InitialEquityValue fixedpoint.Value `json:"initialEquityValue"`
|
||||
FinalEquityValue fixedpoint.Value `json:"finalEquityValue"`
|
||||
|
||||
// TotalProfit is the profit aggregated from the symbol reports
|
||||
TotalProfit fixedpoint.Value `json:"totalProfit,omitempty"`
|
||||
TotalUnrealizedProfit fixedpoint.Value `json:"totalUnrealizedProfit,omitempty"`
|
||||
|
||||
TotalGrossProfit fixedpoint.Value `json:"totalGrossProfit,omitempty"`
|
||||
TotalGrossLoss fixedpoint.Value `json:"totalGrossLoss,omitempty"`
|
||||
|
||||
SymbolReports []SessionSymbolReport `json:"symbolReports,omitempty"`
|
||||
|
||||
Manifests Manifests `json:"manifests,omitempty"`
|
||||
|
@ -75,13 +81,21 @@ type SessionSymbolReport struct {
|
|||
Manifests Manifests `json:"manifests,omitempty"`
|
||||
}
|
||||
|
||||
func (r *SessionSymbolReport) InitialEquityValue() fixedpoint.Value {
|
||||
return InQuoteAsset(r.InitialBalances, r.Market, r.StartPrice)
|
||||
}
|
||||
|
||||
func (r *SessionSymbolReport) FinalEquityValue() fixedpoint.Value {
|
||||
return InQuoteAsset(r.FinalBalances, r.Market, r.StartPrice)
|
||||
}
|
||||
|
||||
func (r *SessionSymbolReport) Print(wantBaseAssetBaseline bool) {
|
||||
color.Green("%s %s PROFIT AND LOSS REPORT", r.Exchange, r.Symbol)
|
||||
color.Green("===============================================")
|
||||
r.PnL.Print()
|
||||
|
||||
initQuoteAsset := inQuoteAsset(r.InitialBalances, r.Market, r.StartPrice)
|
||||
finalQuoteAsset := inQuoteAsset(r.FinalBalances, r.Market, r.LastPrice)
|
||||
initQuoteAsset := r.InitialEquityValue()
|
||||
finalQuoteAsset := r.FinalEquityValue()
|
||||
color.Green("INITIAL ASSET IN %s ~= %s %s (1 %s = %v)", r.Market.QuoteCurrency, r.Market.FormatQuantity(initQuoteAsset), r.Market.QuoteCurrency, r.Market.BaseCurrency, r.StartPrice)
|
||||
color.Green("FINAL ASSET IN %s ~= %s %s (1 %s = %v)", r.Market.QuoteCurrency, r.Market.FormatQuantity(finalQuoteAsset), r.Market.QuoteCurrency, r.Market.BaseCurrency, r.LastPrice)
|
||||
|
||||
|
@ -186,8 +200,8 @@ func AddReportIndexRun(outputDirectory string, run Run) error {
|
|||
return WriteReportIndex(outputDirectory, reportIndex)
|
||||
}
|
||||
|
||||
// inQuoteAsset converts all balances in quote asset
|
||||
func inQuoteAsset(balances types.BalanceMap, market types.Market, price fixedpoint.Value) fixedpoint.Value {
|
||||
// InQuoteAsset converts all balances in quote asset
|
||||
func InQuoteAsset(balances types.BalanceMap, market types.Market, price fixedpoint.Value) fixedpoint.Value {
|
||||
quote := balances[market.QuoteCurrency]
|
||||
base := balances[market.BaseCurrency]
|
||||
return base.Total().Mul(price).Add(quote.Total())
|
||||
|
|
|
@ -340,9 +340,6 @@ var BacktestCmd = &cobra.Command{
|
|||
})
|
||||
|
||||
dumper := backtest.NewKLineDumper(kLineDataDir)
|
||||
defer func() {
|
||||
_ = dumper.Close()
|
||||
}()
|
||||
defer func() {
|
||||
if err := dumper.Close(); err != nil {
|
||||
log.WithError(err).Errorf("kline dumper can not close files")
|
||||
|
@ -496,7 +493,6 @@ var BacktestCmd = &cobra.Command{
|
|||
}
|
||||
|
||||
for _, session := range environ.Sessions() {
|
||||
|
||||
for symbol, trades := range session.Trades {
|
||||
symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Trades)
|
||||
if err != nil {
|
||||
|
@ -507,6 +503,10 @@ var BacktestCmd = &cobra.Command{
|
|||
summaryReport.SymbolReports = append(summaryReport.SymbolReports, *symbolReport)
|
||||
summaryReport.TotalProfit = symbolReport.PnL.Profit
|
||||
summaryReport.TotalUnrealizedProfit = symbolReport.PnL.UnrealizedProfit
|
||||
summaryReport.InitialEquityValue = summaryReport.InitialEquityValue.Add(symbolReport.InitialEquityValue())
|
||||
summaryReport.FinalEquityValue = summaryReport.FinalEquityValue.Add(symbolReport.FinalEquityValue())
|
||||
summaryReport.TotalGrossProfit.Add(symbolReport.PnL.GrossProfit)
|
||||
summaryReport.TotalGrossLoss.Add(symbolReport.PnL.GrossLoss)
|
||||
|
||||
// write report to a file
|
||||
if generatingReport {
|
||||
|
|
|
@ -4,12 +4,12 @@ package indicator
|
|||
|
||||
import ()
|
||||
|
||||
func (A *ATR) OnUpdate(cb func(value float64)) {
|
||||
A.UpdateCallbacks = append(A.UpdateCallbacks, cb)
|
||||
func (inc *ATR) OnUpdate(cb func(value float64)) {
|
||||
inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb)
|
||||
}
|
||||
|
||||
func (A *ATR) EmitUpdate(value float64) {
|
||||
for _, cb := range A.UpdateCallbacks {
|
||||
func (inc *ATR) EmitUpdate(value float64) {
|
||||
for _, cb := range inc.UpdateCallbacks {
|
||||
cb(value)
|
||||
}
|
||||
}
|
||||
|
|
112
pkg/indicator/atrp.go
Normal file
112
pkg/indicator/atrp.go
Normal file
|
@ -0,0 +1,112 @@
|
|||
package indicator
|
||||
|
||||
import (
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
// ATRP is the average true range percentage
|
||||
// See also https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/atrp
|
||||
//
|
||||
// Calculation:
|
||||
//
|
||||
// ATRP = (Average True Range / Close) * 100
|
||||
//
|
||||
//go:generate callbackgen -type ATRP
|
||||
type ATRP struct {
|
||||
types.SeriesBase
|
||||
types.IntervalWindow
|
||||
PercentageVolatility types.Float64Slice
|
||||
|
||||
PreviousClose float64
|
||||
RMA *RMA
|
||||
|
||||
EndTime time.Time
|
||||
UpdateCallbacks []func(value float64)
|
||||
}
|
||||
|
||||
func (inc *ATRP) Update(high, low, cloze float64) {
|
||||
if inc.Window <= 0 {
|
||||
panic("window must be greater than 0")
|
||||
}
|
||||
|
||||
if inc.RMA == nil {
|
||||
inc.SeriesBase.Series = inc
|
||||
inc.RMA = &RMA{
|
||||
IntervalWindow: types.IntervalWindow{Window: inc.Window},
|
||||
Adjust: true,
|
||||
}
|
||||
inc.PreviousClose = cloze
|
||||
return
|
||||
}
|
||||
|
||||
// calculate true range
|
||||
trueRange := high - low
|
||||
hc := math.Abs(high - inc.PreviousClose)
|
||||
lc := math.Abs(low - inc.PreviousClose)
|
||||
if trueRange < hc {
|
||||
trueRange = hc
|
||||
}
|
||||
if trueRange < lc {
|
||||
trueRange = lc
|
||||
}
|
||||
|
||||
// Note: this is the difference from ATR
|
||||
trueRange = trueRange / inc.PreviousClose * 100.0
|
||||
|
||||
inc.PreviousClose = cloze
|
||||
|
||||
// apply rolling moving average
|
||||
inc.RMA.Update(trueRange)
|
||||
atr := inc.RMA.Last()
|
||||
inc.PercentageVolatility.Push(atr / cloze)
|
||||
}
|
||||
|
||||
func (inc *ATRP) Last() float64 {
|
||||
if inc.RMA == nil {
|
||||
return 0
|
||||
}
|
||||
return inc.RMA.Last()
|
||||
}
|
||||
|
||||
func (inc *ATRP) Index(i int) float64 {
|
||||
if inc.RMA == nil {
|
||||
return 0
|
||||
}
|
||||
return inc.RMA.Index(i)
|
||||
}
|
||||
|
||||
func (inc *ATRP) Length() int {
|
||||
if inc.RMA == nil {
|
||||
return 0
|
||||
}
|
||||
return inc.RMA.Length()
|
||||
}
|
||||
|
||||
var _ types.SeriesExtend = &ATRP{}
|
||||
|
||||
func (inc *ATRP) CalculateAndUpdate(kLines []types.KLine) {
|
||||
for _, k := range kLines {
|
||||
if inc.EndTime != zeroTime && !k.EndTime.After(inc.EndTime) {
|
||||
continue
|
||||
}
|
||||
inc.Update(k.High.Float64(), k.Low.Float64(), k.Close.Float64())
|
||||
}
|
||||
|
||||
inc.EmitUpdate(inc.Last())
|
||||
inc.EndTime = kLines[len(kLines)-1].EndTime.Time()
|
||||
}
|
||||
|
||||
func (inc *ATRP) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
|
||||
if inc.Interval != interval {
|
||||
return
|
||||
}
|
||||
|
||||
inc.CalculateAndUpdate(window)
|
||||
}
|
||||
|
||||
func (inc *ATRP) Bind(updater KLineWindowUpdater) {
|
||||
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
|
||||
}
|
15
pkg/indicator/atrp_callbacks.go
Normal file
15
pkg/indicator/atrp_callbacks.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
// Code generated by "callbackgen -type ATRP"; DO NOT EDIT.
|
||||
|
||||
package indicator
|
||||
|
||||
import ()
|
||||
|
||||
func (inc *ATRP) OnUpdate(cb func(value float64)) {
|
||||
inc.UpdateCallbacks = append(inc.UpdateCallbacks, cb)
|
||||
}
|
||||
|
||||
func (inc *ATRP) EmitUpdate(value float64) {
|
||||
for _, cb := range inc.UpdateCallbacks {
|
||||
cb(value)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user