diff --git a/pkg/backtest/report.go b/pkg/backtest/report.go index bce827699d..3772e2786d 100644 --- a/pkg/backtest/report.go +++ b/pkg/backtest/report.go @@ -68,21 +68,49 @@ func ReadSummaryReport(filename string) (*SummaryReport, error) { // SessionSymbolReport is the report per exchange session // trades are merged, collected and re-calculated type SessionSymbolReport struct { - Exchange types.ExchangeName `json:"exchange"` - Symbol string `json:"symbol,omitempty"` - Intervals []types.Interval `json:"intervals,omitempty"` - Subscriptions []types.Subscription `json:"subscriptions"` - Market types.Market `json:"market"` - LastPrice fixedpoint.Value `json:"lastPrice,omitempty"` - StartPrice fixedpoint.Value `json:"startPrice,omitempty"` - PnL *pnl.AverageCostPnLReport `json:"pnl,omitempty"` - InitialBalances types.BalanceMap `json:"initialBalances,omitempty"` - FinalBalances types.BalanceMap `json:"finalBalances,omitempty"` - Manifests Manifests `json:"manifests,omitempty"` - Sharpe fixedpoint.Value `json:"sharpeRatio"` - Sortino fixedpoint.Value `json:"sortinoRatio"` - ProfitFactor fixedpoint.Value `json:"profitFactor"` - WinningRatio fixedpoint.Value `json:"winningRatio"` + Exchange types.ExchangeName `json:"exchange"` + Symbol string `json:"symbol,omitempty"` + Intervals []types.Interval `json:"intervals,omitempty"` + Subscriptions []types.Subscription `json:"subscriptions"` + Market types.Market `json:"market"` + LastPrice fixedpoint.Value `json:"lastPrice,omitempty"` + StartPrice fixedpoint.Value `json:"startPrice,omitempty"` + PnL *pnl.AverageCostPnLReport `json:"pnl,omitempty"` + InitialBalances types.BalanceMap `json:"initialBalances,omitempty"` + FinalBalances types.BalanceMap `json:"finalBalances,omitempty"` + Manifests Manifests `json:"manifests,omitempty"` + TradeCount fixedpoint.Value `json:"tradeCount,omitempty"` + RoundTurnCount fixedpoint.Value `json:"roundTurnCount,omitempty"` + TotalNetProfit fixedpoint.Value `json:"totalNetProfit,omitempty"` + AvgNetProfit fixedpoint.Value `json:"avgNetProfit,omitempty"` + GrossProfit fixedpoint.Value `json:"grossProfit,omitempty"` + GrossLoss fixedpoint.Value `json:"grossLoss,omitempty"` + PRR fixedpoint.Value `json:"prr,omitempty"` + PercentProfitable fixedpoint.Value `json:"percentProfitable,omitempty"` + MaxDrawdown fixedpoint.Value `json:"maxDrawdown,omitempty"` + AverageDrawdown fixedpoint.Value `json:"avgDrawdown,omitempty"` + MaxProfit fixedpoint.Value `json:"maxProfit,omitempty"` + MaxLoss fixedpoint.Value `json:"maxLoss,omitempty"` + AvgProfit fixedpoint.Value `json:"avgProfit,omitempty"` + AvgLoss fixedpoint.Value `json:"avgLoss,omitempty"` + TotalTimeInMarketSec int64 `json:"totalTimeInMarketSec,omitempty"` + AvgHoldSec int64 `json:"avgHoldSec,omitempty"` + WinningCount int `json:"winningCount,omitempty"` + LosingCount int `json:"losingCount,omitempty"` + MaxLossStreak int `json:"maxLossStreak,omitempty"` + Sharpe fixedpoint.Value `json:"sharpeRatio"` + AnnualHistoricVolatility fixedpoint.Value `json:"annualHistoricVolatility,omitempty"` + CAGR fixedpoint.Value `json:"cagr,omitempty"` + Calmar fixedpoint.Value `json:"calmar,omitempty"` + Sterling fixedpoint.Value `json:"sterling,omitempty"` + Burke fixedpoint.Value `json:"burke,omitempty"` + Kelly fixedpoint.Value `json:"kelly,omitempty"` + OptimalF fixedpoint.Value `json:"optimalF,omitempty"` + StatN fixedpoint.Value `json:"statN,omitempty"` + StdErr fixedpoint.Value `json:"statNStdErr,omitempty"` + Sortino fixedpoint.Value `json:"sortinoRatio"` + ProfitFactor fixedpoint.Value `json:"profitFactor"` + WinningRatio fixedpoint.Value `json:"winningRatio"` } func (r *SessionSymbolReport) InitialEquityValue() fixedpoint.Value { diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index 979412e9bc..8b9822c436 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -12,12 +12,6 @@ import ( "github.com/fatih/color" "github.com/google/uuid" - - "github.com/c9s/bbgo/pkg/cmd/cmdutil" - "github.com/c9s/bbgo/pkg/core" - "github.com/c9s/bbgo/pkg/data/tsv" - "github.com/c9s/bbgo/pkg/util" - "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" @@ -26,10 +20,14 @@ import ( "github.com/c9s/bbgo/pkg/accounting/pnl" "github.com/c9s/bbgo/pkg/backtest" "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/cmd/cmdutil" + "github.com/c9s/bbgo/pkg/core" + "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" ) func init() { @@ -533,12 +531,9 @@ var BacktestCmd = &cobra.Command{ continue } - tradeState := sessionTradeStats[session.Name][symbol] - profitFactor := tradeState.ProfitFactor - winningRatio := tradeState.WinningRatio - intervalProfits := tradeState.IntervalProfits[types.Interval1d] + tradeStats := sessionTradeStats[session.Name][symbol] - symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Copy(), intervalProfits, profitFactor, winningRatio) + symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Copy(), tradeStats) if err != nil { return err } @@ -549,8 +544,8 @@ var BacktestCmd = &cobra.Command{ 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) + summaryReport.TotalGrossProfit = summaryReport.TotalGrossProfit.Add(symbolReport.PnL.GrossProfit) + summaryReport.TotalGrossLoss = summaryReport.TotalGrossLoss.Add(symbolReport.PnL.GrossLoss) // write report to a file if generatingReport { @@ -603,14 +598,12 @@ var BacktestCmd = &cobra.Command{ }, } -func createSymbolReport( - userConfig *bbgo.Config, session *bbgo.ExchangeSession, symbol string, trades []types.Trade, - intervalProfit *types.IntervalProfitCollector, - profitFactor, winningRatio fixedpoint.Value, -) ( +func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession, symbol string, trades []types.Trade, tradeStats *types.TradeStats) ( *backtest.SessionSymbolReport, error, ) { + intervalProfit := tradeStats.IntervalProfits[types.Interval1d] + backtestExchange, ok := session.Exchange.(*backtest.Exchange) if !ok { return nil, fmt.Errorf("unexpected error, exchange instance is not a backtest exchange") @@ -620,6 +613,11 @@ func createSymbolReport( if !ok { return nil, fmt.Errorf("market not found: %s, %s", symbol, session.Exchange.Name()) } + tStart, tEnd := trades[0].Time, trades[len(trades)-1].Time + + periodStart := tStart.Time() + periodEnd := tEnd.Time() + period := periodEnd.Sub(periodStart) startPrice, ok := session.StartPrice(symbol) if !ok { @@ -636,29 +634,81 @@ func createSymbolReport( Market: market, } - sharpeRatio := fixedpoint.NewFromFloat(intervalProfit.GetSharpe()) - sortinoRatio := fixedpoint.NewFromFloat(intervalProfit.GetSortino()) - report := calculator.Calculate(symbol, trades, lastPrice) accountConfig := userConfig.Backtest.GetAccount(session.Exchange.Name().String()) initBalances := accountConfig.Balances.BalanceMap() finalBalances := session.GetAccount().Balances() + maxProfit := n(intervalProfit.Profits.Max()) + maxLoss := n(intervalProfit.Profits.Min()) + drawdown := types.Drawdown(intervalProfit.Profits) + maxDrawdown := drawdown.Max() + avgDrawdown := drawdown.Average() + roundTurnCount := n(float64(tradeStats.NumOfProfitTrade + tradeStats.NumOfLossTrade)) + roundTurnLength := n(float64(intervalProfit.Profits.Length())) + winningCount := n(float64(tradeStats.NumOfProfitTrade)) + loosingCount := n(float64(tradeStats.NumOfLossTrade)) + avgProfit := tradeStats.GrossProfit.Div(n(types.NNZ(float64(tradeStats.NumOfProfitTrade), 1))) + avgLoss := tradeStats.GrossLoss.Div(n(types.NNZ(float64(tradeStats.NumOfLossTrade), 1))) + + winningPct := winningCount.Div(roundTurnCount) + // losingPct := fixedpoint.One.Sub(winningPct) + + sharpeRatio := n(intervalProfit.GetSharpe()) + sortinoRatio := n(intervalProfit.GetSortino()) + annVolHis := n(types.AnnualHistoricVolatility(intervalProfit.Profits)) + totalTimeInMarketSec, avgHoldSec := intervalProfit.GetTimeInMarket() + statn, stdErr := types.StatN(intervalProfit.Profits) symbolReport := backtest.SessionSymbolReport{ - Exchange: session.Exchange.Name(), - Symbol: symbol, - Market: market, - LastPrice: lastPrice, - StartPrice: startPrice, - PnL: report, - InitialBalances: initBalances, - FinalBalances: finalBalances, - // Manifests: manifests, - Sharpe: sharpeRatio, - Sortino: sortinoRatio, - ProfitFactor: profitFactor, - WinningRatio: winningRatio, + Exchange: session.Exchange.Name(), + Symbol: symbol, + Market: market, + LastPrice: lastPrice, + StartPrice: startPrice, + InitialBalances: initBalances, + FinalBalances: finalBalances, + TradeCount: fixedpoint.NewFromInt(int64(len(trades))), + GrossLoss: tradeStats.GrossLoss, + GrossProfit: tradeStats.GrossProfit, + WinningCount: tradeStats.NumOfProfitTrade, + LosingCount: tradeStats.NumOfLossTrade, + RoundTurnCount: roundTurnCount, + WinningRatio: tradeStats.WinningRatio, + PercentProfitable: winningPct, + ProfitFactor: tradeStats.ProfitFactor, + MaxDrawdown: n(maxDrawdown), + AverageDrawdown: n(avgDrawdown), + MaxProfit: maxProfit, + MaxLoss: maxLoss, + MaxLossStreak: tradeStats.MaximumConsecutiveLosses, + TotalTimeInMarketSec: totalTimeInMarketSec, + AvgHoldSec: avgHoldSec, + AvgProfit: avgProfit, + AvgLoss: avgLoss, + AvgNetProfit: tradeStats.TotalNetProfit.Div(roundTurnLength), + TotalNetProfit: tradeStats.TotalNetProfit, + AnnualHistoricVolatility: annVolHis, + PnL: report, + PRR: types.PRR(tradeStats.GrossProfit, tradeStats.GrossLoss, winningCount, loosingCount), + Kelly: types.KellyCriterion(tradeStats.ProfitFactor, winningPct), + OptimalF: types.OptimalF(intervalProfit.Profits), + StatN: statn, + StdErr: stdErr, + Sharpe: sharpeRatio, + Sortino: sortinoRatio, } + cagr := types.NN( + types.CAGR( + symbolReport.InitialEquityValue().Float64(), + symbolReport.FinalEquityValue().Float64(), + int(period.Hours())/24, + ), 0) + + symbolReport.CAGR = n(cagr) + symbolReport.Calmar = n(types.CalmarRatio(cagr, maxDrawdown)) + symbolReport.Sterling = n(types.SterlingRatio(cagr, avgDrawdown)) + symbolReport.Burke = n(types.BurkeRatio(cagr, drawdown.AverageSquared())) + for _, s := range session.Subscriptions { symbolReport.Subscriptions = append(symbolReport.Subscriptions, s) } @@ -677,6 +727,10 @@ func createSymbolReport( return &symbolReport, nil } +func n(v float64) fixedpoint.Value { + return fixedpoint.NewFromFloat(v) +} + func verify( userConfig *bbgo.Config, backtestService *service.BacktestService, sourceExchanges map[types.ExchangeName]types.Exchange, startTime, endTime time.Time, diff --git a/pkg/datatype/floats/slice.go b/pkg/datatype/floats/slice.go index 1d610a4f53..dd7b14aa19 100644 --- a/pkg/datatype/floats/slice.go +++ b/pkg/datatype/floats/slice.go @@ -112,6 +112,18 @@ func (s Slice) Average() float64 { return total / float64(len(s)) } +func (s Slice) AverageSquared() float64 { + if len(s) == 0 { + return 0.0 + } + + total := 0.0 + for _, value := range s { + total += math.Pow(value, 2) + } + return total / float64(len(s)) +} + func (s Slice) Diff() (values Slice) { for i, v := range s { if i == 0 { diff --git a/pkg/types/trade_stat.go b/pkg/types/trade_stat.go new file mode 100644 index 0000000000..d89c59484e --- /dev/null +++ b/pkg/types/trade_stat.go @@ -0,0 +1,151 @@ +package types + +import ( + "math" + + "gonum.org/v1/gonum/stat" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +const ( + // DailyToAnnualFactor is the factor to scale daily observations to annual. + // Commonly defined as the number of public market trading days in a year. + DailyToAnnualFactor = 252 // todo does this apply to crypto at all? +) + +// AnnualHistoricVolatility is the historic volatility of the equity curve as annualized std dev. +func AnnualHistoricVolatility(data Series) float64 { + var sd = Stdev(data, data.Length(), 1) + return sd * math.Sqrt(DailyToAnnualFactor) +} + +// CAGR is the Compound Annual Growth Rate of the equity curve. +func CAGR(initial, final float64, days int) float64 { + var ( + growthRate = (final - initial) / initial + x = 1 + growthRate + y = 365.0 / float64(days) + ) + return math.Pow(x, y) - 1 +} + +// measures of risk-adjusted return based on drawdown risk + +// calmar ratio - discounts expected excess return of a portfolio by the +// worst expected maximum draw down for that portfolio +// CR = E(re)/MD1 = (E(r) - rf) / MD1 +func CalmarRatio(cagr, maxDrawdown float64) float64 { + return cagr / maxDrawdown +} + +// Sterling ratio +// discounts the expected excess return of a portfolio by the average of the N worst +// expected maximum drawdowns for that portfolio +// CR = E(re) / (1/N)(sum MDi) +func SterlingRatio(cagr, avgDrawdown float64) float64 { + return cagr / avgDrawdown +} + +// Burke Ratio +// similar to sterling, but less sensitive to outliers +// discounts the expected excess return of a portfolio by the square root of the average +// of the N worst expected maximum drawdowns for that portfolio +// BR = E(re) / ((1/N)(sum MD^2))^0.5 ---> smoothing, can take roots, logs etc +func BurkeRatio(cagr, avgDrawdownSquared float64) float64 { + return cagr / math.Sqrt(avgDrawdownSquared) +} + +// KellyCriterion the famous method for trade sizing. +func KellyCriterion(profitFactor, winP fixedpoint.Value) fixedpoint.Value { + return profitFactor.Mul(winP).Sub(fixedpoint.One.Sub(winP)).Div(profitFactor) +} + +// PRR (Pessimistic Return Ratio) is the profit factor with a penalty for a lower number of roundturns. +func PRR(profit, loss, winningN, losingN fixedpoint.Value) fixedpoint.Value { + var ( + winF = 1 / math.Sqrt(1+winningN.Float64()) + loseF = 1 / math.Sqrt(1+losingN.Float64()) + ) + return fixedpoint.NewFromFloat((1 - winF) / (1 + loseF) * (1 + profit.Float64()) / (1 + loss.Float64())) +} + +// StatN returns the statistically significant number of samples required based on the distribution of a series. +// From: https://www.elitetrader.com/et/threads/minimum-number-of-roundturns-required-for-backtesting-results-to-be-trusted.356588/page-2 +func StatN(xs floats.Slice) (sn, se fixedpoint.Value) { + var ( + sd = Stdev(xs, xs.Length(), 1) + m = Mean(xs) + statn = math.Pow(4*(sd/m), 2) + stdErr = stat.StdErr(sd, float64(xs.Length())) + ) + return fixedpoint.NewFromFloat(statn), fixedpoint.NewFromFloat(stdErr) +} + +// OptimalF is a function that returns the 'OptimalF' for a series of trade returns as defined by Ralph Vince. +// It is a method for sizing positions to maximize geometric return whilst accounting for biggest trading loss. +// See: https://www.investopedia.com/terms/o/optimalf.asp +// Param roundturns is the series of profits (-ve amount for losses) for each trade +func OptimalF(roundturns floats.Slice) fixedpoint.Value { + var ( + maxTWR, optimalF float64 + maxLoss = roundturns.Min() + ) + for i := 1.0; i <= 100.0; i++ { + twr := 1.0 + f := i / 100 + for j := range roundturns { + if roundturns[j] == 0 { + continue + } + hpr := 1 + f*(-roundturns[j]/maxLoss) + twr *= hpr + } + if twr > maxTWR { + maxTWR = twr + optimalF = f + } + } + + return fixedpoint.NewFromFloat(optimalF) +} + +// NN (Not Number) returns y if x is NaN or Inf. +func NN(x, y float64) float64 { + if math.IsNaN(x) || math.IsInf(x, 0) { + return y + } + return x +} + +// NNZ (Not Number or Zero) returns y if x is NaN or Inf or Zero. +func NNZ(x, y float64) float64 { + if NN(x, y) == y || x == 0 { + return y + } + return x +} + +// Compute the drawdown function associated to a portfolio equity curve, +// also called the portfolio underwater equity curve. +// Portfolio Optimization with Drawdown Constraints, Chekhlov et al., 2000 +// http://papers.ssrn.com/sol3/papers.cfm?abstract_id=223323 +func Drawdown(equityCurve floats.Slice) floats.Slice { + // Initialize highWaterMark + highWaterMark := math.Inf(-1) + + // Create ddVector with the same length as equityCurve + ddVector := make([]float64, len(equityCurve)) + + // Loop over all the values to compute the drawdown vector + for i := 0; i < len(equityCurve); i++ { + if equityCurve[i] > highWaterMark { + highWaterMark = equityCurve[i] + } + + ddVector[i] = (highWaterMark - equityCurve[i]) / highWaterMark + } + + return ddVector +} diff --git a/pkg/types/trade_stat_test.go b/pkg/types/trade_stat_test.go new file mode 100644 index 0000000000..e1d25800a1 --- /dev/null +++ b/pkg/types/trade_stat_test.go @@ -0,0 +1,56 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +func TestCAGR(t *testing.T) { + giveInitial := 1000.0 + giveFinal := 2500.0 + giveDays := 190 + want := 4.81 + act := CAGR(giveInitial, giveFinal, giveDays) + assert.InDelta(t, want, act, 0.01) +} + +func TestKellyCriterion(t *testing.T) { + var ( + giveProfitFactor = fixedpoint.NewFromFloat(1.6) + giveWinP = fixedpoint.NewFromFloat(0.7) + want = 0.51 + act = KellyCriterion(giveProfitFactor, giveWinP) + ) + assert.InDelta(t, want, act.Float64(), 0.01) +} + +func TestAnnualHistoricVolatility(t *testing.T) { + var ( + give = floats.Slice{0.1, 0.2, -0.15, 0.1, 0.8, -0.3, 0.2} + want = 5.51 + act = AnnualHistoricVolatility(give) + ) + assert.InDelta(t, want, act, 0.01) +} + +func TestOptimalF(t *testing.T) { + roundturns := floats.Slice{10, 20, 50, -10, 40, -40} + f := OptimalF(roundturns) + assert.EqualValues(t, 0.45, f.Float64()) +} + +func TestDrawdown(t *testing.T) { + roundturns := floats.Slice{100, 50, 100} + expected := []float64{.0, .5, .0} + drawdown := Drawdown(roundturns) + assert.EqualValues(t, 0.5, drawdown.Max()) + assert.EqualValues(t, 0.16666666666666666, drawdown.Average()) + assert.EqualValues(t, 0.08333333333333333, drawdown.AverageSquared()) + for i, v := range expected { + assert.EqualValues(t, v, drawdown[i]) + } +} diff --git a/pkg/types/trade_stats.go b/pkg/types/trade_stats.go index b20d3791c0..0cc3e0f486 100644 --- a/pkg/types/trade_stats.go +++ b/pkg/types/trade_stats.go @@ -8,39 +8,59 @@ import ( "time" log "github.com/sirupsen/logrus" - "gopkg.in/yaml.v3" "github.com/c9s/bbgo/pkg/datatype/floats" "github.com/c9s/bbgo/pkg/fixedpoint" ) +const ( + ErrStartTimeNotValid = "No valid start time. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?" + ErrProfitArrEmpty = "profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?" +) + +type ProfitReport struct { + StartTime time.Time `json:"startTime"` + Profit float64 `json:"profit"` + Interval Interval `json:"interval"` +} + +func (s ProfitReport) String() string { + b, err := json.MarshalIndent(s, "", "\t") + if err != nil { + log.Fatal(err) + } + return string(b) +} + type IntervalProfitCollector struct { - Interval Interval `json:"interval"` - Profits *floats.Slice `json:"profits"` - Timestamp *floats.Slice `json:"timestamp"` - tmpTime time.Time `json:"tmpTime"` + Interval Interval `json:"interval"` + Profits floats.Slice `json:"profits"` + TimeInMarket []time.Duration `json:"timeInMarket"` + Timestamp floats.Slice `json:"timestamp"` + tmpTime time.Time `json:"tmpTime"` } func NewIntervalProfitCollector(i Interval, startTime time.Time) *IntervalProfitCollector { - return &IntervalProfitCollector{Interval: i, tmpTime: startTime, Profits: &floats.Slice{1.}, Timestamp: &floats.Slice{float64(startTime.Unix())}} + return &IntervalProfitCollector{Interval: i, tmpTime: startTime, Profits: floats.Slice{1.}, Timestamp: floats.Slice{float64(startTime.Unix())}} } // Update the collector by every traded profit func (s *IntervalProfitCollector) Update(profit *Profit) { if s.tmpTime.IsZero() { - panic("No valid start time. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") + panic(ErrStartTimeNotValid) } else { + s.TimeInMarket = append(s.TimeInMarket, profit.TradedAt.Sub(profit.PositionOpenedAt)) duration := s.Interval.Duration() if profit.TradedAt.Before(s.tmpTime.Add(duration)) { - (*s.Profits)[len(*s.Profits)-1] *= 1. + profit.NetProfitMargin.Float64() + (s.Profits)[len(s.Profits)-1] *= 1. + profit.NetProfitMargin.Float64() } else { for { s.Profits.Update(1.) s.tmpTime = s.tmpTime.Add(duration) s.Timestamp.Update(float64(s.tmpTime.Unix())) if profit.TradedAt.Before(s.tmpTime.Add(duration)) { - (*s.Profits)[len(*s.Profits)-1] *= 1. + profit.NetProfitMargin.Float64() + (s.Profits)[len(s.Profits)-1] *= 1. + profit.NetProfitMargin.Float64() break } } @@ -48,18 +68,18 @@ func (s *IntervalProfitCollector) Update(profit *Profit) { } } -type ProfitReport struct { - StartTime time.Time `json:"startTime"` - Profit float64 `json:"profit"` - Interval Interval `json:"interval"` -} - -func (s ProfitReport) String() string { - b, err := json.MarshalIndent(s, "", "\t") - if err != nil { - log.Fatal(err) +// Determine average and total time spend in market +func (s *IntervalProfitCollector) GetTimeInMarket() (avgHoldSec, totalTimeInMarketSec int64) { + if s.Profits == nil { + return 0, 0 } - return string(b) + l := len(s.TimeInMarket) + for i := 0; i < l; i++ { + d := s.TimeInMarket[i] + totalTimeInMarketSec += int64(d / time.Millisecond) + } + avgHoldSec = totalTimeInMarketSec / int64(l) + return } // Get all none-profitable intervals @@ -93,9 +113,9 @@ func (s *IntervalProfitCollector) GetProfitableIntervals() (result []ProfitRepor // Get number of profitable traded intervals func (s *IntervalProfitCollector) GetNumOfProfitableIntervals() (profit int) { if s.Profits == nil { - panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") + panic(ErrProfitArrEmpty) } - for _, v := range *s.Profits { + for _, v := range s.Profits { if v > 1. { profit += 1 } @@ -107,9 +127,9 @@ func (s *IntervalProfitCollector) GetNumOfProfitableIntervals() (profit int) { // (no trade within the interval or pnl = 0 will be also included here) func (s *IntervalProfitCollector) GetNumOfNonProfitableIntervals() (nonprofit int) { if s.Profits == nil { - panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") + panic(ErrProfitArrEmpty) } - for _, v := range *s.Profits { + for _, v := range s.Profits { if v <= 1. { nonprofit += 1 } @@ -121,10 +141,11 @@ func (s *IntervalProfitCollector) GetNumOfNonProfitableIntervals() (nonprofit in // no smart sharpe ON for the calculated result func (s *IntervalProfitCollector) GetSharpe() float64 { if s.tmpTime.IsZero() { - panic("No valid start time. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") + panic(ErrStartTimeNotValid) } if s.Profits == nil { - panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") + panic(ErrStartTimeNotValid) + } return Sharpe(Sub(s.Profits, 1.), s.Profits.Length(), true, false) } @@ -133,10 +154,10 @@ func (s *IntervalProfitCollector) GetSharpe() float64 { // No risk-free return rate and smart sortino OFF for the calculated result. func (s *IntervalProfitCollector) GetSortino() float64 { if s.tmpTime.IsZero() { - panic("No valid start time. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") + panic(ErrStartTimeNotValid) } if s.Profits == nil { - panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?") + panic(ErrProfitArrEmpty) } return Sortino(Sub(s.Profits, 1.), 0., s.Profits.Length(), true, false) }