mirror of
https://github.com/c9s/bbgo.git
synced 2024-09-20 08:11:08 +00:00
feature: add omega ratio, print sharpe/omega/interval profit from trade_stats, use stdev for high/low diff for drift to estimate the variance and improve profit, add yaml marshal for dnum fixedpoint
This commit is contained in:
parent
b6fb5e958d
commit
a5039de6aa
|
@ -20,6 +20,8 @@ exchangeStrategies:
|
|||
predictOffset: 14
|
||||
noStopPrice: true
|
||||
noTrailingStopLoss: false
|
||||
# stddev on high/low-source
|
||||
hlVarianceMultiplier: 0.35
|
||||
|
||||
generateGraph: true
|
||||
graphPNLDeductFee: false
|
||||
|
|
|
@ -453,6 +453,10 @@ func (environ *Environment) SetStartTime(t time.Time) *Environment {
|
|||
return environ
|
||||
}
|
||||
|
||||
func (environ *Environment) StartTime() time.Time {
|
||||
return environ.startTime
|
||||
}
|
||||
|
||||
// SetSyncStartTime overrides the default trade scan time (-7 days)
|
||||
func (environ *Environment) SetSyncStartTime(t time.Time) *Environment {
|
||||
environ.syncStartTime = t
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
package statistics
|
|
@ -48,17 +48,21 @@ type Strategy struct {
|
|||
*types.ProfitStats `persistence:"profit_stats"`
|
||||
*types.TradeStats `persistence:"trade_stats"`
|
||||
|
||||
drift *indicator.Drift
|
||||
atr *indicator.ATR
|
||||
midPrice fixedpoint.Value
|
||||
lock sync.RWMutex
|
||||
ma types.UpdatableSeriesExtend
|
||||
stdevHigh *indicator.StdDev
|
||||
stdevLow *indicator.StdDev
|
||||
drift *indicator.Drift
|
||||
atr *indicator.ATR
|
||||
midPrice fixedpoint.Value
|
||||
lock sync.RWMutex
|
||||
|
||||
Source string `json:"source"`
|
||||
StopLoss fixedpoint.Value `json:"stoploss"`
|
||||
CanvasPath string `json:"canvasPath"`
|
||||
PredictOffset int `json:"predictOffset"`
|
||||
NoStopPrice bool `json:"noStopPrice"`
|
||||
NoTrailingStopLoss bool `json:"noTrailingStopLoss"`
|
||||
Source string `json:"source"`
|
||||
StopLoss fixedpoint.Value `json:"stoploss"`
|
||||
CanvasPath string `json:"canvasPath"`
|
||||
PredictOffset int `json:"predictOffset"`
|
||||
HighLowVarianceMultiplier float64 `json:"hlVarianceMultiplier"`
|
||||
NoStopPrice bool `json:"noStopPrice"`
|
||||
NoTrailingStopLoss bool `json:"noTrailingStopLoss"`
|
||||
|
||||
// This is not related to trade but for statistics graph generation
|
||||
// Will deduct fee in percentage from every trade
|
||||
|
@ -94,6 +98,7 @@ func (s *Strategy) Print(o *os.File) {
|
|||
hiyellow(f, "window: %d\n", s.Window)
|
||||
hiyellow(f, "noStopPrice: %v\n", s.NoStopPrice)
|
||||
hiyellow(f, "noTrailingStopLoss: %v\n", s.NoTrailingStopLoss)
|
||||
hiyellow(f, "hlVarianceMutiplier: %f\n", s.HighLowVarianceMultiplier)
|
||||
hiyellow(f, "\n")
|
||||
}
|
||||
|
||||
|
@ -225,6 +230,9 @@ func (s *Strategy) BindStopLoss(ctx context.Context) {
|
|||
}
|
||||
|
||||
func (s *Strategy) InitIndicators() error {
|
||||
s.ma = &indicator.EWMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: 5}}
|
||||
s.stdevHigh = &indicator.StdDev{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: 6}}
|
||||
s.stdevLow = &indicator.StdDev{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: 6}}
|
||||
s.drift = &indicator.Drift{
|
||||
MA: &indicator.SMA{IntervalWindow: s.IntervalWindow},
|
||||
IntervalWindow: s.IntervalWindow,
|
||||
|
@ -238,6 +246,11 @@ func (s *Strategy) InitIndicators() error {
|
|||
|
||||
for _, kline := range *klines {
|
||||
source := s.getSource(&kline).Float64()
|
||||
high := kline.High.Float64()
|
||||
low := kline.Low.Float64()
|
||||
s.ma.Update(source)
|
||||
s.stdevHigh.Update(high - s.ma.Last())
|
||||
s.stdevLow.Update(s.ma.Last() - low)
|
||||
s.drift.Update(source)
|
||||
s.atr.PushK(kline)
|
||||
}
|
||||
|
@ -382,6 +395,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
if s.TradeStats == nil {
|
||||
s.TradeStats = types.NewTradeStats(s.Symbol)
|
||||
}
|
||||
startTime := s.Environment.StartTime()
|
||||
s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, startTime))
|
||||
s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, startTime))
|
||||
|
||||
// StrategyController
|
||||
s.Status = types.StrategyStatusRunning
|
||||
|
@ -546,6 +562,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
source := s.getSource(dynamicKLine)
|
||||
sourcef := source.Float64()
|
||||
priceLine.Update(sourcef)
|
||||
s.ma.Update(sourcef)
|
||||
s.drift.Update(sourcef)
|
||||
s.atr.PushK(kline)
|
||||
drift = s.drift.Array(2)
|
||||
|
@ -555,6 +572,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
pricef := price.Float64()
|
||||
lowf := math.Min(kline.Low.Float64(), pricef)
|
||||
highf := math.Max(kline.High.Float64(), pricef)
|
||||
lowdiff := s.ma.Last() - lowf
|
||||
s.stdevLow.Update(lowdiff)
|
||||
highdiff := highf - s.ma.Last()
|
||||
s.stdevHigh.Update(highdiff)
|
||||
avg := s.Position.AverageCost.Float64()
|
||||
|
||||
shortCondition := (driftPred <= 0 && drift[0] <= 0)
|
||||
|
@ -581,10 +602,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
log.Errorf("unable to get baseBalance")
|
||||
return
|
||||
}
|
||||
source = source.Add(fixedpoint.NewFromFloat(s.stdevHigh.Last() * s.HighLowVarianceMultiplier))
|
||||
if source.Compare(price) < 0 {
|
||||
source = price
|
||||
}
|
||||
source = source.Mul(fixedpoint.NewFromFloat(1.0002))
|
||||
|
||||
if s.Market.IsDustQuantity(baseBalance.Available, source) {
|
||||
return
|
||||
|
@ -628,10 +649,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
log.WithError(err).Errorf("cannot cancel orders")
|
||||
return
|
||||
}
|
||||
source = source.Sub(fixedpoint.NewFromFloat(s.stdevLow.Last() * s.HighLowVarianceMultiplier))
|
||||
if source.Compare(price) > 0 {
|
||||
source = price
|
||||
}
|
||||
source = source.Mul(fixedpoint.NewFromFloat(0.9998))
|
||||
quoteBalance, ok := s.Session.GetAccount().Balance(s.Market.QuoteCurrency)
|
||||
if !ok {
|
||||
log.Errorf("unable to get quoteCurrency")
|
||||
|
@ -682,7 +703,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
|
||||
defer s.Print(os.Stdout)
|
||||
|
||||
defer fmt.Fprintln(os.Stdout, s.TradeStats.String())
|
||||
defer fmt.Fprintln(os.Stdout, s.TradeStats.BriefString())
|
||||
|
||||
if s.GenerateGraph {
|
||||
s.Draw(dynamicKLine.StartTime, priceLine, &profit, &cumProfit)
|
||||
|
|
30
pkg/types/omega.go
Normal file
30
pkg/types/omega.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package types
|
||||
|
||||
import ()
|
||||
|
||||
// Determines the Omega ratio of a strategy
|
||||
// See https://en.wikipedia.org/wiki/Omega_ratio for more details
|
||||
//
|
||||
// @param returns (Series): Series of profit/loss percentage every specific interval
|
||||
// @param returnThresholds(float64): threshold for returns filtering
|
||||
// @return Omega ratio for give return series and threshold
|
||||
func Omega(returns Series, returnThresholds ...float64) float64 {
|
||||
threshold := 0.0
|
||||
if len(returnThresholds) > 0 {
|
||||
threshold = returnThresholds[0]
|
||||
} else {
|
||||
threshold = Mean(returns)
|
||||
}
|
||||
length := returns.Length()
|
||||
win := 0.0
|
||||
loss := 0.0
|
||||
for i := 0; i < length; i++ {
|
||||
out := threshold - returns.Index(i)
|
||||
if out > 0 {
|
||||
win += out
|
||||
} else {
|
||||
loss -= out
|
||||
}
|
||||
}
|
||||
return win / loss
|
||||
}
|
12
pkg/types/omega_test.go
Normal file
12
pkg/types/omega_test.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOmega(t *testing.T) {
|
||||
var a Series = &Float64Slice{0.08, 0.09, 0.07, 0.15, 0.02, 0.03, 0.04, 0.05, 0.06, 0.01}
|
||||
output := Omega(a)
|
||||
assert.InDelta(t, output, 1, 0.0001)
|
||||
}
|
|
@ -1,32 +1,28 @@
|
|||
package statistics
|
||||
package types
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
// Sharpe: Calcluates the sharpe ratio of access returns
|
||||
//
|
||||
// @param returns (Series): Series of profit/loss percentage every specific interval
|
||||
// @param periods (int): Freq. of returns (252/365 for daily, 12 for monthy)
|
||||
// @param annualize (bool): return annualize sharpe?
|
||||
// @param smart (bool): return smart sharpe ratio
|
||||
func Sharpe(returns types.Series, periods int, annualize bool, smart bool) float64 {
|
||||
func Sharpe(returns Series, periods int, annualize bool, smart bool) float64 {
|
||||
data := returns
|
||||
num := data.Length()
|
||||
if types.Lowest(data, num) >= 0 && types.Highest(data, num) > 1 {
|
||||
data = types.PercentageChange(returns)
|
||||
}
|
||||
divisor := types.Stdev(data, data.Length(), 1)
|
||||
divisor := Stdev(data, data.Length(), 1)
|
||||
if smart {
|
||||
sum := 0.
|
||||
coef := math.Abs(types.Correlation(data, types.Shift(data, 1), num-1))
|
||||
coef := math.Abs(Correlation(data, Shift(data, 1), num-1))
|
||||
for i := 1; i < num; i++ {
|
||||
sum += float64(num-i) / float64(num) * math.Pow(coef, float64(i))
|
||||
}
|
||||
divisor = divisor * math.Sqrt(1.+2.*sum)
|
||||
}
|
||||
result := types.Mean(data) / divisor
|
||||
result := Mean(data) / divisor
|
||||
if annualize {
|
||||
return result * math.Sqrt(float64(periods))
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package statistics
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
@ -17,7 +16,7 @@ print(qx.stats.sharpe(pd.Series([0.01, 0.1, 0.001]), 0, 252, False, False))
|
|||
print(qx.stats.sharpe(pd.Series([0.01, 0.1, 0.001]), 0, 252, True, False))
|
||||
*/
|
||||
func TestSharpe(t *testing.T) {
|
||||
var a types.Series = &types.Float64Slice{0.01, 0.1, 0.001}
|
||||
var a Series = &Float64Slice{0.01, 0.1, 0.001}
|
||||
output := Sharpe(a, 0, false, false)
|
||||
assert.InDelta(t, output, 0.67586, 0.0001)
|
||||
output = Sharpe(a, 252, false, false)
|
|
@ -1,30 +1,132 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
)
|
||||
|
||||
type IntervalProfitCollector struct {
|
||||
Interval Interval
|
||||
Profits *Float64Slice
|
||||
tmpTime time.Time
|
||||
}
|
||||
|
||||
func NewIntervalProfitCollector(i Interval, startTime time.Time) *IntervalProfitCollector {
|
||||
return &IntervalProfitCollector{Interval: i, tmpTime: startTime, Profits: &Float64Slice{1.}}
|
||||
}
|
||||
|
||||
// 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)
|
||||
if profit.TradedAt.Before(s.tmpTime.Add(duration)) {
|
||||
(*s.Profits)[len(*s.Profits)-1] *= 1. + profit.NetProfitMargin.Float64()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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?")
|
||||
}
|
||||
for _, v := range *s.Profits {
|
||||
if v > 1. {
|
||||
profit += 1
|
||||
}
|
||||
}
|
||||
return profit
|
||||
}
|
||||
|
||||
// Get number of non-profitable traded intervals
|
||||
// (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?")
|
||||
}
|
||||
for _, v := range *s.Profits {
|
||||
if v <= 1. {
|
||||
nonprofit += 1
|
||||
}
|
||||
}
|
||||
return nonprofit
|
||||
}
|
||||
|
||||
// Get sharpe value with the interval of profit collected.
|
||||
// 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?")
|
||||
}
|
||||
if s.Profits == nil {
|
||||
panic("profits array empty. Did you create IntervalProfitCollector instance using NewIntervalProfitCollector?")
|
||||
}
|
||||
return Sharpe(Minus(s.Profits, 1.), s.Profits.Length(), true, false)
|
||||
}
|
||||
|
||||
func (s *IntervalProfitCollector) GetOmega() float64 {
|
||||
return Omega(Minus(s.Profits, 1.))
|
||||
}
|
||||
|
||||
func (s IntervalProfitCollector) MarshalYAML() (interface{}, error) {
|
||||
result := make(map[string]interface{})
|
||||
result["Sharpe Ratio"] = s.GetSharpe()
|
||||
result["Omega Ratio"] = s.GetOmega()
|
||||
result["Profitable Count"] = s.GetNumOfProfitableIntervals()
|
||||
result["NonProfitable Count"] = s.GetNumOfNonProfitableIntervals()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *IntervalProfitCollector) MarshalJSON() ([]byte, error) {
|
||||
result := make(map[string]interface{})
|
||||
result["Sharpe Ratio"] = s.GetSharpe()
|
||||
result["Omega Ratio"] = s.GetOmega()
|
||||
result["Profitable Count"] = s.GetNumOfProfitableIntervals()
|
||||
result["NonProfitable Count"] = s.GetNumOfNonProfitableIntervals()
|
||||
return json.Marshal(result)
|
||||
|
||||
}
|
||||
|
||||
// TODO: Add more stats from the reference:
|
||||
// See https://www.metatrader5.com/en/terminal/help/algotrading/testing_report
|
||||
type TradeStats struct {
|
||||
Symbol string `json:"symbol"`
|
||||
WinningRatio fixedpoint.Value `json:"winningRatio" yaml:"winningRatio"`
|
||||
NumOfLossTrade int `json:"numOfLossTrade" yaml:"numOfLossTrade"`
|
||||
NumOfProfitTrade int `json:"numOfProfitTrade" yaml:"numOfProfitTrade"`
|
||||
GrossProfit fixedpoint.Value `json:"grossProfit" yaml:"grossProfit"`
|
||||
GrossLoss fixedpoint.Value `json:"grossLoss" yaml:"grossLoss"`
|
||||
Profits []fixedpoint.Value `json:"profits" yaml:"profits"`
|
||||
Losses []fixedpoint.Value `json:"losses" yaml:"losses"`
|
||||
MostProfitableTrade fixedpoint.Value `json:"mostProfitableTrade" yaml:"mostProfitableTrade"`
|
||||
MostLossTrade fixedpoint.Value `json:"mostLossTrade" yaml:"mostLossTrade"`
|
||||
ProfitFactor fixedpoint.Value `json:"profitFactor" yaml:"profitFactor"`
|
||||
TotalNetProfit fixedpoint.Value `json:"totalNetProfit" yaml:"totalNetProfit"`
|
||||
Symbol string `json:"symbol"`
|
||||
WinningRatio fixedpoint.Value `json:"winningRatio" yaml:"winningRatio"`
|
||||
NumOfLossTrade int `json:"numOfLossTrade" yaml:"numOfLossTrade"`
|
||||
NumOfProfitTrade int `json:"numOfProfitTrade" yaml:"numOfProfitTrade"`
|
||||
GrossProfit fixedpoint.Value `json:"grossProfit" yaml:"grossProfit"`
|
||||
GrossLoss fixedpoint.Value `json:"grossLoss" yaml:"grossLoss"`
|
||||
Profits []fixedpoint.Value `json:"profits" yaml:"profits"`
|
||||
Losses []fixedpoint.Value `json:"losses" yaml:"losses"`
|
||||
MostProfitableTrade fixedpoint.Value `json:"mostProfitableTrade" yaml:"mostProfitableTrade"`
|
||||
MostLossTrade fixedpoint.Value `json:"mostLossTrade" yaml:"mostLossTrade"`
|
||||
ProfitFactor fixedpoint.Value `json:"profitFactor" yaml:"profitFactor"`
|
||||
TotalNetProfit fixedpoint.Value `json:"totalNetProfit" yaml:"totalNetProfit"`
|
||||
IntervalProfits map[Interval]*IntervalProfitCollector `json:"intervalProfits,omitempty" yaml: "intervalProfits,omitempty"`
|
||||
}
|
||||
|
||||
func NewTradeStats(symbol string) *TradeStats {
|
||||
return &TradeStats{Symbol: symbol}
|
||||
return &TradeStats{Symbol: symbol, IntervalProfits: make(map[Interval]*IntervalProfitCollector)}
|
||||
}
|
||||
|
||||
// Set IntervalProfitCollector explicitly to enable the sharpe ratio calculation
|
||||
func (s *TradeStats) SetIntervalProfitCollector(c *IntervalProfitCollector) {
|
||||
s.IntervalProfits[c.Interval] = c
|
||||
}
|
||||
|
||||
func (s *TradeStats) Add(profit *Profit) {
|
||||
|
@ -33,6 +135,9 @@ func (s *TradeStats) Add(profit *Profit) {
|
|||
}
|
||||
|
||||
s.add(profit.Profit)
|
||||
for _, v := range s.IntervalProfits {
|
||||
v.Update(profit)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TradeStats) add(pnl fixedpoint.Value) {
|
||||
|
@ -61,6 +166,24 @@ func (s *TradeStats) add(pnl fixedpoint.Value) {
|
|||
s.ProfitFactor = s.GrossProfit.Div(s.GrossLoss.Abs())
|
||||
}
|
||||
|
||||
// Output TradeStats without Profits and Losses
|
||||
func (s *TradeStats) BriefString() string {
|
||||
out, _ := yaml.Marshal(&TradeStats{
|
||||
Symbol: s.Symbol,
|
||||
WinningRatio: s.WinningRatio,
|
||||
NumOfLossTrade: s.NumOfLossTrade,
|
||||
NumOfProfitTrade: s.NumOfProfitTrade,
|
||||
GrossProfit: s.GrossProfit,
|
||||
GrossLoss: s.GrossLoss,
|
||||
MostProfitableTrade: s.MostProfitableTrade,
|
||||
MostLossTrade: s.MostLossTrade,
|
||||
ProfitFactor: s.ProfitFactor,
|
||||
TotalNetProfit: s.TotalNetProfit,
|
||||
IntervalProfits: s.IntervalProfits,
|
||||
})
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func (s *TradeStats) String() string {
|
||||
out, _ := yaml.Marshal(s)
|
||||
return string(out)
|
||||
|
|
Loading…
Reference in New Issue
Block a user