mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 09:11:55 +00:00
reset main
This commit is contained in:
parent
f7d54291f2
commit
0d7990fc18
|
@ -68,49 +68,21 @@ func ReadSummaryReport(filename string) (*SummaryReport, error) {
|
||||||
// SessionSymbolReport is the report per exchange session
|
// SessionSymbolReport is the report per exchange session
|
||||||
// trades are merged, collected and re-calculated
|
// trades are merged, collected and re-calculated
|
||||||
type SessionSymbolReport struct {
|
type SessionSymbolReport struct {
|
||||||
Exchange types.ExchangeName `json:"exchange"`
|
Exchange types.ExchangeName `json:"exchange"`
|
||||||
Symbol string `json:"symbol,omitempty"`
|
Symbol string `json:"symbol,omitempty"`
|
||||||
Intervals []types.Interval `json:"intervals,omitempty"`
|
Intervals []types.Interval `json:"intervals,omitempty"`
|
||||||
Subscriptions []types.Subscription `json:"subscriptions"`
|
Subscriptions []types.Subscription `json:"subscriptions"`
|
||||||
Market types.Market `json:"market"`
|
Market types.Market `json:"market"`
|
||||||
LastPrice fixedpoint.Value `json:"lastPrice,omitempty"`
|
LastPrice fixedpoint.Value `json:"lastPrice,omitempty"`
|
||||||
StartPrice fixedpoint.Value `json:"startPrice,omitempty"`
|
StartPrice fixedpoint.Value `json:"startPrice,omitempty"`
|
||||||
PnL *pnl.AverageCostPnLReport `json:"pnl,omitempty"`
|
PnL *pnl.AverageCostPnLReport `json:"pnl,omitempty"`
|
||||||
InitialBalances types.BalanceMap `json:"initialBalances,omitempty"`
|
InitialBalances types.BalanceMap `json:"initialBalances,omitempty"`
|
||||||
FinalBalances types.BalanceMap `json:"finalBalances,omitempty"`
|
FinalBalances types.BalanceMap `json:"finalBalances,omitempty"`
|
||||||
Manifests Manifests `json:"manifests,omitempty"`
|
Manifests Manifests `json:"manifests,omitempty"`
|
||||||
TradeCount fixedpoint.Value `json:"tradeCount,omitempty"`
|
Sharpe fixedpoint.Value `json:"sharpeRatio"`
|
||||||
RoundTurnCount fixedpoint.Value `json:"roundTurnCount,omitempty"`
|
Sortino fixedpoint.Value `json:"sortinoRatio"`
|
||||||
TotalNetProfit fixedpoint.Value `json:"totalNetProfit,omitempty"`
|
ProfitFactor fixedpoint.Value `json:"profitFactor"`
|
||||||
AvgNetProfit fixedpoint.Value `json:"avgNetProfit,omitempty"`
|
WinningRatio fixedpoint.Value `json:"winningRatio"`
|
||||||
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 {
|
func (r *SessionSymbolReport) InitialEquityValue() fixedpoint.Value {
|
||||||
|
|
|
@ -535,7 +535,8 @@ var BacktestCmd = &cobra.Command{
|
||||||
profitFactor := tradeState.ProfitFactor
|
profitFactor := tradeState.ProfitFactor
|
||||||
winningRatio := tradeState.WinningRatio
|
winningRatio := tradeState.WinningRatio
|
||||||
intervalProfits := tradeState.IntervalProfits[types.Interval1d]
|
intervalProfits := tradeState.IntervalProfits[types.Interval1d]
|
||||||
symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Copy(), tradeStats)
|
|
||||||
|
symbolReport, err := createSymbolReport(userConfig, session, symbol, trades.Copy(), intervalProfits, profitFactor, winningRatio)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -600,12 +601,14 @@ var BacktestCmd = &cobra.Command{
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession, symbol string, trades []types.Trade, tradeStats *types.TradeStats) (
|
func createSymbolReport(
|
||||||
|
userConfig *bbgo.Config, session *bbgo.ExchangeSession, symbol string, trades []types.Trade,
|
||||||
|
intervalProfit *types.IntervalProfitCollector,
|
||||||
|
profitFactor, winningRatio fixedpoint.Value,
|
||||||
|
) (
|
||||||
*backtest.SessionSymbolReport,
|
*backtest.SessionSymbolReport,
|
||||||
error,
|
error,
|
||||||
) {
|
) {
|
||||||
intervalProfit := tradeStats.IntervalProfits[types.Interval1d]
|
|
||||||
|
|
||||||
backtestExchange, ok := session.Exchange.(*backtest.Exchange)
|
backtestExchange, ok := session.Exchange.(*backtest.Exchange)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("unexpected error, exchange instance is not a backtest exchange")
|
return nil, fmt.Errorf("unexpected error, exchange instance is not a backtest exchange")
|
||||||
|
@ -615,11 +618,6 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession,
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("market not found: %s, %s", symbol, session.Exchange.Name())
|
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)
|
startPrice, ok := session.StartPrice(symbol)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -636,81 +634,29 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession,
|
||||||
Market: market,
|
Market: market,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sharpeRatio := fixedpoint.NewFromFloat(intervalProfit.GetSharpe())
|
||||||
|
sortinoRatio := fixedpoint.NewFromFloat(intervalProfit.GetSortino())
|
||||||
|
|
||||||
report := calculator.Calculate(symbol, trades, lastPrice)
|
report := calculator.Calculate(symbol, trades, lastPrice)
|
||||||
accountConfig := userConfig.Backtest.GetAccount(session.Exchange.Name().String())
|
accountConfig := userConfig.Backtest.GetAccount(session.Exchange.Name().String())
|
||||||
initBalances := accountConfig.Balances.BalanceMap()
|
initBalances := accountConfig.Balances.BalanceMap()
|
||||||
finalBalances := session.GetAccount().Balances()
|
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{
|
symbolReport := backtest.SessionSymbolReport{
|
||||||
Exchange: session.Exchange.Name(),
|
Exchange: session.Exchange.Name(),
|
||||||
Symbol: symbol,
|
Symbol: symbol,
|
||||||
Market: market,
|
Market: market,
|
||||||
LastPrice: lastPrice,
|
LastPrice: lastPrice,
|
||||||
StartPrice: startPrice,
|
StartPrice: startPrice,
|
||||||
InitialBalances: initBalances,
|
PnL: report,
|
||||||
FinalBalances: finalBalances,
|
InitialBalances: initBalances,
|
||||||
TradeCount: fixedpoint.NewFromInt(int64(len(trades))),
|
FinalBalances: finalBalances,
|
||||||
GrossLoss: tradeStats.GrossLoss,
|
// Manifests: manifests,
|
||||||
GrossProfit: tradeStats.GrossProfit,
|
Sharpe: sharpeRatio,
|
||||||
WinningCount: tradeStats.NumOfProfitTrade,
|
Sortino: sortinoRatio,
|
||||||
LosingCount: tradeStats.NumOfLossTrade,
|
ProfitFactor: profitFactor,
|
||||||
RoundTurnCount: roundTurnCount,
|
WinningRatio: winningRatio,
|
||||||
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 {
|
for _, s := range session.Subscriptions {
|
||||||
symbolReport.Subscriptions = append(symbolReport.Subscriptions, s)
|
symbolReport.Subscriptions = append(symbolReport.Subscriptions, s)
|
||||||
}
|
}
|
||||||
|
@ -729,10 +675,6 @@ func createSymbolReport(userConfig *bbgo.Config, session *bbgo.ExchangeSession,
|
||||||
return &symbolReport, nil
|
return &symbolReport, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func n(v float64) fixedpoint.Value {
|
|
||||||
return fixedpoint.NewFromFloat(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func verify(
|
func verify(
|
||||||
userConfig *bbgo.Config, backtestService *service.BacktestService,
|
userConfig *bbgo.Config, backtestService *service.BacktestService,
|
||||||
sourceExchanges map[types.ExchangeName]types.Exchange, startTime, endTime time.Time,
|
sourceExchanges map[types.ExchangeName]types.Exchange, startTime, endTime time.Time,
|
||||||
|
|
|
@ -112,18 +112,6 @@ func (s Slice) Average() float64 {
|
||||||
return total / float64(len(s))
|
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) {
|
func (s Slice) Diff() (values Slice) {
|
||||||
for i, v := range s {
|
for i, v := range s {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
|
|
|
@ -1,151 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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])
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -14,10 +14,38 @@ import (
|
||||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
type IntervalProfitCollector struct {
|
||||||
ErrStartTimeNotValid = "No valid start time. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?"
|
Interval Interval `json:"interval"`
|
||||||
ErrProfitArrEmpty = "profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?"
|
Profits *floats.Slice `json:"profits"`
|
||||||
)
|
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())}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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?")
|
||||||
|
} else {
|
||||||
|
duration := s.Interval.Duration()
|
||||||
|
if profit.TradedAt.Before(s.tmpTime.Add(duration)) {
|
||||||
|
(*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()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type ProfitReport struct {
|
type ProfitReport struct {
|
||||||
StartTime time.Time `json:"startTime"`
|
StartTime time.Time `json:"startTime"`
|
||||||
|
@ -33,55 +61,6 @@ func (s ProfitReport) String() string {
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewIntervalProfitCollector(i Interval, startTime time.Time) *IntervalProfitCollector {
|
|
||||||
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(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()
|
|
||||||
} 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()
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
// Get all none-profitable intervals
|
||||||
func (s *IntervalProfitCollector) GetNonProfitableIntervals() (result []ProfitReport) {
|
func (s *IntervalProfitCollector) GetNonProfitableIntervals() (result []ProfitReport) {
|
||||||
if s.Profits == nil {
|
if s.Profits == nil {
|
||||||
|
@ -113,9 +92,9 @@ func (s *IntervalProfitCollector) GetProfitableIntervals() (result []ProfitRepor
|
||||||
// Get number of profitable traded intervals
|
// Get number of profitable traded intervals
|
||||||
func (s *IntervalProfitCollector) GetNumOfProfitableIntervals() (profit int) {
|
func (s *IntervalProfitCollector) GetNumOfProfitableIntervals() (profit int) {
|
||||||
if s.Profits == nil {
|
if s.Profits == nil {
|
||||||
panic(ErrProfitArrEmpty)
|
panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?")
|
||||||
}
|
}
|
||||||
for _, v := range s.Profits {
|
for _, v := range *s.Profits {
|
||||||
if v > 1. {
|
if v > 1. {
|
||||||
profit += 1
|
profit += 1
|
||||||
}
|
}
|
||||||
|
@ -127,9 +106,9 @@ func (s *IntervalProfitCollector) GetNumOfProfitableIntervals() (profit int) {
|
||||||
// (no trade within the interval or pnl = 0 will be also included here)
|
// (no trade within the interval or pnl = 0 will be also included here)
|
||||||
func (s *IntervalProfitCollector) GetNumOfNonProfitableIntervals() (nonprofit int) {
|
func (s *IntervalProfitCollector) GetNumOfNonProfitableIntervals() (nonprofit int) {
|
||||||
if s.Profits == nil {
|
if s.Profits == nil {
|
||||||
panic(ErrProfitArrEmpty)
|
panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?")
|
||||||
}
|
}
|
||||||
for _, v := range s.Profits {
|
for _, v := range *s.Profits {
|
||||||
if v <= 1. {
|
if v <= 1. {
|
||||||
nonprofit += 1
|
nonprofit += 1
|
||||||
}
|
}
|
||||||
|
@ -141,11 +120,10 @@ func (s *IntervalProfitCollector) GetNumOfNonProfitableIntervals() (nonprofit in
|
||||||
// no smart sharpe ON for the calculated result
|
// no smart sharpe ON for the calculated result
|
||||||
func (s *IntervalProfitCollector) GetSharpe() float64 {
|
func (s *IntervalProfitCollector) GetSharpe() float64 {
|
||||||
if s.tmpTime.IsZero() {
|
if s.tmpTime.IsZero() {
|
||||||
panic(ErrStartTimeNotValid)
|
panic("No valid start time. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?")
|
||||||
}
|
}
|
||||||
if s.Profits == nil {
|
if s.Profits == nil {
|
||||||
panic(ErrStartTimeNotValid)
|
panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?")
|
||||||
|
|
||||||
}
|
}
|
||||||
return Sharpe(Sub(s.Profits, 1.), s.Profits.Length(), true, false)
|
return Sharpe(Sub(s.Profits, 1.), s.Profits.Length(), true, false)
|
||||||
}
|
}
|
||||||
|
@ -154,10 +132,10 @@ func (s *IntervalProfitCollector) GetSharpe() float64 {
|
||||||
// No risk-free return rate and smart sortino OFF for the calculated result.
|
// No risk-free return rate and smart sortino OFF for the calculated result.
|
||||||
func (s *IntervalProfitCollector) GetSortino() float64 {
|
func (s *IntervalProfitCollector) GetSortino() float64 {
|
||||||
if s.tmpTime.IsZero() {
|
if s.tmpTime.IsZero() {
|
||||||
panic(ErrStartTimeNotValid)
|
panic("No valid start time. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?")
|
||||||
}
|
}
|
||||||
if s.Profits == nil {
|
if s.Profits == nil {
|
||||||
panic(ErrProfitArrEmpty)
|
panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?")
|
||||||
}
|
}
|
||||||
return Sortino(Sub(s.Profits, 1.), 0., s.Profits.Length(), true, false)
|
return Sortino(Sub(s.Profits, 1.), 0., s.Profits.Length(), true, false)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user