mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
add more trade stats
This commit is contained in:
parent
09e2b84d36
commit
f19bbbc5aa
|
@ -79,7 +79,31 @@ type SessionSymbolReport struct {
|
|||
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"`
|
||||
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"`
|
||||
Kelly fixedpoint.Value `json:"kelly,omitempty"`
|
||||
OptimalF fixedpoint.Value `json:"optimalF,omitempty"`
|
||||
StatN fixedpoint.Value `json:"statN,omitempty"`
|
||||
StatNStdErr fixedpoint.Value `json:"statNStdErr,omitempty"`
|
||||
Sortino fixedpoint.Value `json:"sortinoRatio"`
|
||||
ProfitFactor fixedpoint.Value `json:"profitFactor"`
|
||||
WinningRatio fixedpoint.Value `json:"winningRatio"`
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -12,12 +13,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 +21,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() {
|
||||
|
@ -528,12 +527,9 @@ var BacktestCmd = &cobra.Command{
|
|||
|
||||
for _, session := range environ.Sessions() {
|
||||
for symbol, trades := range session.Trades {
|
||||
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
|
||||
}
|
||||
|
@ -598,11 +594,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")
|
||||
|
@ -612,6 +609,11 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession,
|
|||
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 {
|
||||
|
@ -628,29 +630,74 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession,
|
|||
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(math.Abs(intervalProfit.Profits.Min()))
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
StatNStdErr: stdErr,
|
||||
Sharpe: sharpeRatio,
|
||||
Sortino: sortinoRatio,
|
||||
ProfitFactor: profitFactor,
|
||||
WinningRatio: winningRatio,
|
||||
}
|
||||
|
||||
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, tradeStats.MaxDrawdown))
|
||||
|
||||
for _, s := range session.Subscriptions {
|
||||
symbolReport.Subscriptions = append(symbolReport.Subscriptions, s)
|
||||
}
|
||||
|
@ -669,6 +716,9 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession,
|
|||
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) error {
|
||||
for _, sourceExchange := range sourceExchanges {
|
||||
err := backtestService.Verify(sourceExchange, userConfig.Backtest.Symbols, startTime, endTime)
|
||||
|
|
107
pkg/types/trade_stat.go
Normal file
107
pkg/types/trade_stat.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
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?
|
||||
)
|
||||
|
||||
// HistVolAnn is the annualized historic volatility of daily returns.
|
||||
func AnnualHistoricVolatility(data Series) float64 {
|
||||
var sd = Stdev(data, data.Length(), 1)
|
||||
return sd * math.Sqrt(DailyToAnnualFactor)
|
||||
}
|
||||
|
||||
// CAGR Compound Annual Growth Rate
|
||||
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
|
||||
}
|
||||
|
||||
// CalmarRatio relates the capaital growth rate to the maximum drawdown.
|
||||
func CalmarRatio(cagr, maxDrawdown float64) float64 {
|
||||
return cagr / maxDrawdown
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
44
pkg/types/trade_stat_test.go
Normal file
44
pkg/types/trade_stat_test.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
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)
|
||||
}
|
|
@ -8,16 +8,21 @@ 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 IntervalProfitCollector struct {
|
||||
Interval Interval `json:"interval"`
|
||||
Profits *floats.Slice `json:"profits"`
|
||||
TimeInMarket []time.Duration `json:"timeInMarket"`
|
||||
Timestamp *floats.Slice `json:"timestamp"`
|
||||
tmpTime time.Time `json:"tmpTime"`
|
||||
}
|
||||
|
@ -28,8 +33,10 @@ func NewIntervalProfitCollector(i Interval, startTime time.Time) *IntervalProfit
|
|||
|
||||
// Update the collector by every traded profit
|
||||
func (s *IntervalProfitCollector) Update(profit *Profit) {
|
||||
s.TimeInMarket = append(s.TimeInMarket, profit.TradedAt.Sub(profit.PositionOpenedAt))
|
||||
|
||||
if s.tmpTime.IsZero() {
|
||||
panic("No valid start time. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?")
|
||||
panic(ErrStartTimeNotValid)
|
||||
} else {
|
||||
duration := s.Interval.Duration()
|
||||
if profit.TradedAt.Before(s.tmpTime.Add(duration)) {
|
||||
|
@ -62,6 +69,20 @@ func (s ProfitReport) String() string {
|
|||
return string(b)
|
||||
}
|
||||
|
||||
// Determine average and total time spend in market
|
||||
func (s *IntervalProfitCollector) GetTimeInMarket() (avgHoldSec, totalTimeInMarketSec int64) {
|
||||
if s.Profits == nil {
|
||||
return 0, 0
|
||||
}
|
||||
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
|
||||
func (s *IntervalProfitCollector) GetNonProfitableIntervals() (result []ProfitReport) {
|
||||
if s.Profits == nil {
|
||||
|
@ -93,7 +114,7 @@ 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 {
|
||||
if v > 1. {
|
||||
|
@ -107,7 +128,7 @@ 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 {
|
||||
if v <= 1. {
|
||||
|
@ -121,10 +142,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 +155,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)
|
||||
}
|
||||
|
@ -197,6 +219,24 @@ type TradeStats struct {
|
|||
consecutiveSide int
|
||||
consecutiveCounter int
|
||||
consecutiveAmount fixedpoint.Value
|
||||
|
||||
// CAGR is the Compound Annual Growth Rate of the equity curve.
|
||||
CAGR float64
|
||||
|
||||
// MaxDrawdown is the maximum percentage drawdown of the equity curve
|
||||
MaxDrawdown float64
|
||||
|
||||
// MDDRecovery is the recovery time of the maximum drawdown of the equity curve.
|
||||
MDDRecovery time.Duration
|
||||
|
||||
// HistVolAnn is the historic volatility of the equity curve as annualized std dev.
|
||||
HistVolAnn float64
|
||||
|
||||
// Sharpe is the Sharpe ratio of the equity curve.
|
||||
Sharpe float64
|
||||
|
||||
// Calmar is the Calmar ratio of the equity curve.
|
||||
Calmar float64
|
||||
}
|
||||
|
||||
func NewTradeStats(symbol string) *TradeStats {
|
||||
|
|
Loading…
Reference in New Issue
Block a user