Merge pull request #813 from zenixls2/feature/drift_study

feature: drift study
This commit is contained in:
Yo-An Lin 2022-07-27 11:29:48 +08:00 committed by GitHub
commit 4fd571d712
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1470 additions and 85 deletions

80
config/drift.yaml Normal file
View File

@ -0,0 +1,80 @@
---
persistence:
redis:
host: 127.0.0.1
port: 6379
db: 0
sessions:
binance:
exchange: binance
futures: false
envVarPrefix: binance
heikinAshi: false
exchangeStrategies:
- on: binance
drift:
canvasPath: "./output.png"
symbol: ETHUSDT
# kline interval for indicators
interval: 15m
window: 2
stoploss: 0.3%
source: close
predictOffset: 2
# position avg +- takeProfitFactor * atr as take profit price
takeProfitFactor: 1.4
noTrailingStopLoss: true
# stddev on high/low-source
hlVarianceMultiplier: 0.22
generateGraph: true
graphPNLDeductFee: false
graphPNLPath: "./pnl.png"
graphCumPNLPath: "./cumpnl.png"
#exits:
#- roiStopLoss:
# percentage: 0.8%
#- roiTakeProfit:
# percentage: 35%
#- protectiveStopLoss:
# activationRatio: 0.6%
# stopLossRatio: 0.1%
# placeStopOrder: false
#- protectiveStopLoss:
# activationRatio: 5%
# stopLossRatio: 1%
# placeStopOrder: false
#- cumulatedVolumeTakeProfit:
# interval: 5m
# window: 2
# minQuoteVolume: 200_000_000
#- protectiveStopLoss:
# activationRatio: 2%
# stopLossRatio: 1%
# placeStopOrder: false
sync:
userDataStream:
trades: true
filledOrders: true
sessions:
- binance
symbols:
- ETHUSDT
backtest:
startTime: "2022-01-01"
endTime: "2022-06-18"
symbols:
- ETHUSDT
sessions: [binance]
accounts:
binance:
#makerFeeRate: 0.00001
#takerFeeRate: 0.00001
balances:
ETH: 10
USDT: 5000.0

91
config/driftBTC.yaml Normal file
View File

@ -0,0 +1,91 @@
---
persistence:
redis:
host: 127.0.0.1
port: 6379
db: 0
sessions:
binance:
exchange: binance
futures: false
envVarPrefix: binance
heikinAshi: false
exchangeStrategies:
- on: binance
drift:
canvasPath: "./output.png"
symbol: BTCBUSD
# kline interval for indicators
interval: 15m
window: 2
stoploss: 0.3%
source: close
predictOffset: 2
# position avg +- takeProfitFactor * atr as take profit price
takeProfitFactor: 1.2
noTrailingStopLoss: true
# stddev on high/low-source
hlVarianceMultiplier: 0.27
generateGraph: true
graphPNLDeductFee: true
graphPNLPath: "./pnl.png"
graphCumPNLPath: "./cumpnl.png"
exits:
#- roiStopLoss:
# percentage: 0.8%
#- roiTakeProfit:
# percentage: 3%
#- protectiveStopLoss:
# activationRatio: 0.5%
# stopLossRatio: 0.1%
# placeStopOrder: false
- trailingStop:
callbackRate: 1%
# activationRatio is relative to the average cost,
# when side is buy, 1% means lower 1% than the average cost.
# when side is sell, 1% means higher 1% than the average cost.
activationRatio: 3%
# minProfit uses the position ROI to calculate the profit ratio
minProfit: 1%
interval: 1m
side: buy
closePosition: 100%
#- protectiveStopLoss:
# activationRatio: 5%
# stopLossRatio: 1%
# placeStopOrder: false
#- cumulatedVolumeTakeProfit:
# interval: 5m
# window: 2
# minQuoteVolume: 200_000_000
#- protectiveStopLoss:
# activationRatio: 2%
# stopLossRatio: 1%
# placeStopOrder: false
sync:
userDataStream:
trades: true
filledOrders: true
sessions:
- binance
symbols:
- BTCBUSD
backtest:
startTime: "2022-01-01"
endTime: "2022-06-18"
symbols:
- BTCBUSD
sessions: [binance]
accounts:
binance:
makerFeeRate: 0.000
takerFeeRate: 0.00075
balances:
BTC: 10
BUSD: 5000.0

5
go.mod
View File

@ -2,7 +2,7 @@
module github.com/c9s/bbgo module github.com/c9s/bbgo
go 1.17 go 1.18
require ( require (
github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/DATA-DOG/go-sqlmock v1.5.0
@ -43,6 +43,7 @@ require (
github.com/spf13/viper v1.7.1 github.com/spf13/viper v1.7.1
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
github.com/valyala/fastjson v1.5.1 github.com/valyala/fastjson v1.5.1
github.com/wcharczuk/go-chart/v2 v2.1.0
github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90 github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90
github.com/x-cray/logrus-prefixed-formatter v0.5.2 github.com/x-cray/logrus-prefixed-formatter v0.5.2
github.com/zserge/lorca v0.1.9 github.com/zserge/lorca v0.1.9
@ -75,6 +76,7 @@ require (
github.com/go-test/deep v1.0.6 // indirect github.com/go-test/deep v1.0.6 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/mock v1.6.0 // indirect github.com/golang/mock v1.6.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
@ -117,6 +119,7 @@ require (
go.opentelemetry.io/otel/trace v0.19.0 // indirect go.opentelemetry.io/otel/trace v0.19.0 // indirect
go.uber.org/atomic v1.9.0 // indirect go.uber.org/atomic v1.9.0 // indirect
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 // indirect
golang.org/x/mod v0.5.1 // indirect golang.org/x/mod v0.5.1 // indirect
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect

3
go.sum
View File

@ -182,6 +182,7 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -516,6 +517,7 @@ github.com/ugorji/go/codec v1.2.3 h1:/mVYEV+Jo3IZKeA5gBngN0AvNnQltEDkR+eQikkWQu0
github.com/ugorji/go/codec v1.2.3/go.mod h1:5FxzDJIgeiWJZslYHPj+LS1dq1ZBQVelZFnjsFGI/Uc= github.com/ugorji/go/codec v1.2.3/go.mod h1:5FxzDJIgeiWJZslYHPj+LS1dq1ZBQVelZFnjsFGI/Uc=
github.com/valyala/fastjson v1.5.1 h1:SXaQZVSwLjZOVhDEhjiCcDtnX0Feu7Z7A1+C5atpoHM= github.com/valyala/fastjson v1.5.1 h1:SXaQZVSwLjZOVhDEhjiCcDtnX0Feu7Z7A1+C5atpoHM=
github.com/valyala/fastjson v1.5.1/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= github.com/valyala/fastjson v1.5.1/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I=
github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA=
github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90 h1:G/O1RFjhc9hgVYjaPQ0Oceqxf3GwRQl/5XEAWYetjmg= github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90 h1:G/O1RFjhc9hgVYjaPQ0Oceqxf3GwRQl/5XEAWYetjmg=
github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90/go.mod h1:rpXAuuHgyEJb6kXcXldlkOjU6y4x+YcASKKXJNUhh0Y= github.com/webview/webview v0.0.0-20210216142346-e0bfdf0e5d90/go.mod h1:rpXAuuHgyEJb6kXcXldlkOjU6y4x+YcASKKXJNUhh0Y=
@ -584,6 +586,7 @@ golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMk
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 h1:QelT11PB4FXiDEXucrfNckHoFxwt8USGY1ajP1ZF5lM=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=

View File

@ -453,6 +453,10 @@ func (environ *Environment) SetStartTime(t time.Time) *Environment {
return environ return environ
} }
func (environ *Environment) StartTime() time.Time {
return environ.startTime
}
// SetSyncStartTime overrides the default trade scan time (-7 days) // SetSyncStartTime overrides the default trade scan time (-7 days)
func (environ *Environment) SetSyncStartTime(t time.Time) *Environment { func (environ *Environment) SetSyncStartTime(t time.Time) *Environment {
environ.syncStartTime = t environ.syncStartTime = t

View File

@ -2,6 +2,7 @@ package bbgo
import ( import (
"context" "context"
"fmt"
"strings" "strings"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -102,7 +103,7 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ..
createdOrders, err := e.session.Exchange.SubmitOrders(ctx, formattedOrders...) createdOrders, err := e.session.Exchange.SubmitOrders(ctx, formattedOrders...)
if err != nil { if err != nil {
log.WithError(err).Errorf("can not place orders") err = fmt.Errorf("can not place orders: %w", err)
} }
e.orderStore.Add(createdOrders...) e.orderStore.Add(createdOrders...)
@ -113,9 +114,11 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ..
// GracefulCancelActiveOrderBook cancels the orders from the active orderbook. // GracefulCancelActiveOrderBook cancels the orders from the active orderbook.
func (e *GeneralOrderExecutor) GracefulCancelActiveOrderBook(ctx context.Context, activeOrders *ActiveOrderBook) error { func (e *GeneralOrderExecutor) GracefulCancelActiveOrderBook(ctx context.Context, activeOrders *ActiveOrderBook) error {
if activeOrders.NumOfOrders() == 0 {
return nil
}
if err := activeOrders.GracefulCancel(ctx, e.session.Exchange); err != nil { if err := activeOrders.GracefulCancel(ctx, e.session.Exchange); err != nil {
log.WithError(err).Errorf("graceful cancel order error") return fmt.Errorf("graceful cancel order error: %w", err)
return err
} }
e.tradeCollector.Process() e.tradeCollector.Process()

View File

@ -33,4 +33,5 @@ import (
_ "github.com/c9s/bbgo/pkg/strategy/xmaker" _ "github.com/c9s/bbgo/pkg/strategy/xmaker"
_ "github.com/c9s/bbgo/pkg/strategy/xnav" _ "github.com/c9s/bbgo/pkg/strategy/xnav"
_ "github.com/c9s/bbgo/pkg/strategy/xpuremaker" _ "github.com/c9s/bbgo/pkg/strategy/xpuremaker"
_ "github.com/c9s/bbgo/pkg/strategy/drift"
) )

View File

@ -15,8 +15,8 @@ import (
type ALMA struct { type ALMA struct {
types.SeriesBase types.SeriesBase
types.IntervalWindow // required types.IntervalWindow // required
Offset float64 // required: recommend to be 5 Offset float64 // required: recommend to be 0.5
Sigma int // required: recommend to be 0.5 Sigma int // required: recommend to be 5
weight []float64 weight []float64
sum float64 sum float64
input []float64 input []float64

View File

@ -22,6 +22,24 @@ type ATR struct {
var _ types.SeriesExtend = &ATR{} var _ types.SeriesExtend = &ATR{}
func (inc *ATR) Clone() *ATR {
out := &ATR{
IntervalWindow: inc.IntervalWindow,
PercentageVolatility: inc.PercentageVolatility[:],
PreviousClose: inc.PreviousClose,
RMA: inc.RMA.Clone().(*RMA),
EndTime: inc.EndTime,
}
out.SeriesBase.Series = out
return out
}
func (inc *ATR) TestUpdate(high, low, cloze float64) *ATR {
c := inc.Clone()
c.Update(high, low, cloze)
return c
}
func (inc *ATR) Update(high, low, cloze float64) { func (inc *ATR) Update(high, low, cloze float64) {
if inc.Window <= 0 { if inc.Window <= 0 {
panic("window must be greater than 0") panic("window must be greater than 0")

View File

@ -21,7 +21,6 @@ Bollinger Bands Technical indicator guide:
//go:generate callbackgen -type BOLL //go:generate callbackgen -type BOLL
type BOLL struct { type BOLL struct {
types.SeriesBase
types.IntervalWindow types.IntervalWindow
// K is the multiplier of Std, generally it's 2 // K is the multiplier of Std, generally it's 2
@ -74,7 +73,6 @@ func (inc *BOLL) LastDownBand() float64 {
func (inc *BOLL) Update(value float64) { func (inc *BOLL) Update(value float64) {
if inc.SMA == nil { if inc.SMA == nil {
inc.SeriesBase.Series = inc
inc.SMA = &SMA{IntervalWindow: inc.IntervalWindow} inc.SMA = &SMA{IntervalWindow: inc.IntervalWindow}
} }

View File

@ -9,4 +9,3 @@ import (
var three = fixedpoint.NewFromInt(3) var three = fixedpoint.NewFromInt(3)
var zeroTime = time.Time{} var zeroTime = time.Time{}

View File

@ -18,6 +18,23 @@ type DEMA struct {
UpdateCallbacks []func(value float64) UpdateCallbacks []func(value float64)
} }
func (inc *DEMA) Clone() *DEMA {
out := &DEMA{
IntervalWindow: inc.IntervalWindow,
Values: inc.Values[:],
a1: inc.a1.Clone(),
a2: inc.a2.Clone(),
}
out.SeriesBase.Series = out
return out
}
func (inc *DEMA) TestUpdate(value float64) *DEMA {
out := inc.Clone()
out.Update(value)
return out
}
func (inc *DEMA) Update(value float64) { func (inc *DEMA) Update(value float64) {
if len(inc.Values) == 0 { if len(inc.Values) == 0 {
inc.SeriesBase.Series = inc inc.SeriesBase.Series = inc

View File

@ -15,7 +15,7 @@ type Drift struct {
types.IntervalWindow types.IntervalWindow
chng *types.Queue chng *types.Queue
Values types.Float64Slice Values types.Float64Slice
SMA *SMA MA types.UpdatableSeriesExtend
LastValue float64 LastValue float64
UpdateCallbacks []func(value float64) UpdateCallbacks []func(value float64)
@ -24,7 +24,9 @@ type Drift struct {
func (inc *Drift) Update(value float64) { func (inc *Drift) Update(value float64) {
if inc.chng == nil { if inc.chng == nil {
inc.SeriesBase.Series = inc inc.SeriesBase.Series = inc
inc.SMA = &SMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: inc.Window}} if inc.MA == nil {
inc.MA = &SMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: inc.Window}}
}
inc.chng = types.NewQueue(inc.Window) inc.chng = types.NewQueue(inc.Window)
inc.LastValue = value inc.LastValue = value
return return
@ -36,15 +38,54 @@ func (inc *Drift) Update(value float64) {
chng = math.Log(value / inc.LastValue) chng = math.Log(value / inc.LastValue)
inc.LastValue = value inc.LastValue = value
} }
inc.SMA.Update(chng) inc.MA.Update(chng)
inc.chng.Update(chng) inc.chng.Update(chng)
if inc.chng.Length() >= inc.Window { if inc.chng.Length() >= inc.Window {
stdev := types.Stdev(inc.chng, inc.Window) stdev := types.Stdev(inc.chng, inc.Window)
drift := inc.SMA.Last() - stdev*stdev*0.5 drift := inc.MA.Last() - stdev*stdev*0.5
inc.Values.Push(drift) inc.Values.Push(drift)
} }
} }
// Assume that MA is SMA
func (inc *Drift) ZeroPoint() float64 {
window := float64(inc.Window)
stdev := types.Stdev(inc.chng, inc.Window)
chng := inc.chng.Index(inc.Window - 1)
/*b := -2 * inc.MA.Last() - 2
c := window * stdev * stdev - chng * chng + 2 * chng * (inc.MA.Last() + 1) - 2 * inc.MA.Last() * window
root := math.Sqrt(b*b - 4*c)
K1 := (-b + root)/2
K2 := (-b - root)/2
N1 := math.Exp(K1) * inc.LastValue
N2 := math.Exp(K2) * inc.LastValue
if math.Abs(inc.LastValue-N1) < math.Abs(inc.LastValue-N2) {
return N1
} else {
return N2
}*/
return inc.LastValue * math.Exp(window*(0.5*stdev*stdev)+chng-inc.MA.Last()*window)
}
func (inc *Drift) Clone() (out *Drift) {
out = &Drift{
IntervalWindow: inc.IntervalWindow,
chng: inc.chng.Clone(),
Values: inc.Values[:],
MA: types.Clone(inc.MA),
LastValue: inc.LastValue,
}
out.SeriesBase.Series = out
return out
}
func (inc *Drift) TestUpdate(value float64) *Drift {
out := inc.Clone()
out.Update(value)
return out
}
func (inc *Drift) Index(i int) float64 { func (inc *Drift) Index(i int) float64 {
if inc.Values == nil { if inc.Values == nil {
return 0 return 0

View File

@ -23,6 +23,21 @@ type EWMA struct {
var _ types.SeriesExtend = &EWMA{} var _ types.SeriesExtend = &EWMA{}
func (inc *EWMA) Clone() *EWMA {
out := &EWMA{
IntervalWindow: inc.IntervalWindow,
Values: inc.Values[:],
}
out.SeriesBase.Series = out
return out
}
func (inc *EWMA) TestUpdate(value float64) *EWMA {
out := inc.Clone()
out.Update(value)
return out
}
func (inc *EWMA) Update(value float64) { func (inc *EWMA) Update(value float64) {
var multiplier = 2.0 / float64(1+inc.Window) var multiplier = 2.0 / float64(1+inc.Window)

58
pkg/indicator/fisher.go Normal file
View File

@ -0,0 +1,58 @@
package indicator
import (
"math"
"github.com/c9s/bbgo/pkg/types"
)
//go:generate callbackgen -type FisherTransform
type FisherTransform struct {
types.SeriesBase
types.IntervalWindow
prices *types.Queue
Values types.Float64Slice
UpdateCallbacks []func(value float64)
}
func (inc *FisherTransform) Update(value float64) {
if inc.prices == nil {
inc.prices = types.NewQueue(inc.Window)
inc.SeriesBase.Series = inc
}
inc.prices.Update(value)
highest := inc.prices.Highest(inc.Window)
lowest := inc.prices.Lowest(inc.Window)
x := 2*((value-lowest)/(highest-lowest)) - 1
if x == 1 {
x = 0.9999
} else if x == -1 {
x = -0.9999
}
inc.Values.Update(0.5 * math.Log((1+x)/(1-x)))
if len(inc.Values) > MaxNumOfEWMA {
inc.Values = inc.Values[MaxNumOfEWMATruncateSize-1:]
}
}
func (inc *FisherTransform) Last() float64 {
if inc.Values == nil {
return 0.0
}
return inc.Values.Last()
}
func (inc *FisherTransform) Index(i int) float64 {
if inc.Values == nil {
return 0.0
}
return inc.Values.Index(i)
}
func (inc *FisherTransform) Length() int {
if inc.Values == nil {
return 0
}
return inc.Values.Length()
}

View File

@ -25,6 +25,20 @@ type RMA struct {
updateCallbacks []func(value float64) updateCallbacks []func(value float64)
} }
func (inc *RMA) Clone() types.UpdatableSeriesExtend {
out := &RMA{
IntervalWindow: inc.IntervalWindow,
Values: inc.Values[:],
counter: inc.counter,
Adjust: inc.Adjust,
tmp: inc.tmp,
sum: inc.sum,
EndTime: inc.EndTime,
}
out.SeriesBase.Series = out
return out
}
func (inc *RMA) Update(x float64) { func (inc *RMA) Update(x float64) {
lambda := 1 / float64(inc.Window) lambda := 1 / float64(inc.Window)
if inc.counter == 0 { if inc.counter == 0 {

View File

@ -40,6 +40,16 @@ func (inc *SMA) Length() int {
return inc.Values.Length() return inc.Values.Length()
} }
func (inc *SMA) Clone() types.UpdatableSeriesExtend {
out := &SMA{
Values: inc.Values[:],
rawValues: inc.rawValues.Clone(),
EndTime: inc.EndTime,
}
out.SeriesBase.Series = out
return out
}
var _ types.SeriesExtend = &SMA{} var _ types.SeriesExtend = &SMA{}
func (inc *SMA) Update(value float64) { func (inc *SMA) Update(value float64) {

View File

@ -1,2 +1 @@
package indicator package indicator

View File

@ -1 +0,0 @@
package statistics

View File

@ -0,0 +1,737 @@
package drift
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"math"
"os"
"strings"
"sync"
"github.com/fatih/color"
"github.com/sirupsen/logrus"
"github.com/wcharczuk/go-chart/v2"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
)
const ID = "drift"
var log = logrus.WithField("strategy", ID)
var Four fixedpoint.Value = fixedpoint.NewFromInt(4)
var Three fixedpoint.Value = fixedpoint.NewFromInt(3)
var Two fixedpoint.Value = fixedpoint.NewFromInt(2)
var Delta fixedpoint.Value = fixedpoint.NewFromFloat(0.01)
func init() {
bbgo.RegisterStrategy(ID, &Strategy{})
}
type SourceFunc func(*types.KLine) fixedpoint.Value
type Strategy struct {
Symbol string `json:"symbol"`
bbgo.StrategyController
types.Market
types.IntervalWindow
*bbgo.Environment
*types.Position `persistence:"position"`
*types.ProfitStats `persistence:"profit_stats"`
*types.TradeStats `persistence:"trade_stats"`
ma types.UpdatableSeriesExtend
stdevHigh *indicator.StdDev
stdevLow *indicator.StdDev
drift *DriftMA
atr *indicator.ATR
midPrice fixedpoint.Value
lock sync.RWMutex
Source string `json:"source,omitempty"`
TakeProfitFactor float64 `json:"takeProfitFactor"`
StopLoss fixedpoint.Value `json:"stoploss"`
CanvasPath string `json:"canvasPath"`
PredictOffset int `json:"predictOffset"`
HighLowVarianceMultiplier float64 `json:"hlVarianceMultiplier"`
NoTrailingStopLoss bool `json:"noTrailingStopLoss"`
buyPrice float64
sellPrice float64
highestPrice float64
lowestPrice float64
// This is not related to trade but for statistics graph generation
// Will deduct fee in percentage from every trade
GraphPNLDeductFee bool `json:"graphPNLDeductFee"`
GraphPNLPath string `json:"graphPNLPath"`
GraphCumPNLPath string `json:"graphCumPNLPath"`
// Whether to generate graph when shutdown
GenerateGraph bool `json:"generateGraph"`
ExitMethods bbgo.ExitMethodSet `json:"exits"`
Session *bbgo.ExchangeSession
*bbgo.GeneralOrderExecutor
getLastPrice func() fixedpoint.Value
getSource SourceFunc
}
func (s *Strategy) Print(o *os.File) {
f := bufio.NewWriter(o)
defer f.Flush()
b, _ := json.MarshalIndent(s.ExitMethods, " ", " ")
hiyellow := color.New(color.FgHiYellow).FprintfFunc()
hiyellow(f, "------ %s Settings ------\n", s.InstanceID())
hiyellow(f, "canvasPath: %s\n", s.CanvasPath)
hiyellow(f, "source: %s\n", s.Source)
hiyellow(f, "stoploss: %v\n", s.StopLoss)
hiyellow(f, "takeProfitFactor: %f\n", s.TakeProfitFactor)
hiyellow(f, "predictOffset: %d\n", s.PredictOffset)
hiyellow(f, "exits:\n %s\n", string(b))
hiyellow(f, "symbol: %s\n", s.Symbol)
hiyellow(f, "interval: %s\n", s.Interval)
hiyellow(f, "window: %d\n", s.Window)
hiyellow(f, "noTrailingStopLoss: %v\n", s.NoTrailingStopLoss)
hiyellow(f, "hlVarianceMutiplier: %f\n", s.HighLowVarianceMultiplier)
hiyellow(f, "\n")
}
func (s *Strategy) ID() string {
return ID
}
func (s *Strategy) InstanceID() string {
return fmt.Sprintf("%s-%s", ID, s.Symbol)
}
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{
Interval: s.Interval,
})
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{
Interval: types.Interval1m,
})
if !bbgo.IsBackTesting {
session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{})
}
s.ExitMethods.SetAndSubscribe(session, s)
}
func (s *Strategy) CurrentPosition() *types.Position {
return s.Position
}
func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error {
order := s.Position.NewMarketCloseOrder(percentage)
if order == nil {
return nil
}
order.Tag = "close"
order.TimeInForce = ""
balances := s.Session.GetAccount().Balances()
baseBalance := balances[s.Market.BaseCurrency].Available
price := s.getLastPrice()
if order.Side == types.SideTypeBuy {
quoteAmount := balances[s.Market.QuoteCurrency].Available.Div(price)
if order.Quantity.Compare(quoteAmount) > 0 {
order.Quantity = quoteAmount
}
} else if order.Side == types.SideTypeSell && order.Quantity.Compare(baseBalance) > 0 {
order.Quantity = baseBalance
}
for {
if s.Market.IsDustQuantity(order.Quantity, price) {
return nil
}
_, err := s.GeneralOrderExecutor.SubmitOrders(ctx, *order)
if err != nil {
order.Quantity = order.Quantity.Mul(fixedpoint.One.Sub(Delta))
continue
}
return nil
}
}
func (s *Strategy) SourceFuncGenerator() SourceFunc {
switch strings.ToLower(s.Source) {
case "close":
return func(kline *types.KLine) fixedpoint.Value { return kline.Close }
case "high":
return func(kline *types.KLine) fixedpoint.Value { return kline.High }
case "low":
return func(kline *types.KLine) fixedpoint.Value { return kline.Low }
case "hl2":
return func(kline *types.KLine) fixedpoint.Value {
return kline.High.Add(kline.Low).Div(Two)
}
case "hlc3":
return func(kline *types.KLine) fixedpoint.Value {
return kline.High.Add(kline.Low).Add(kline.Close).Div(Three)
}
case "ohlc4":
return func(kline *types.KLine) fixedpoint.Value {
return kline.Open.Add(kline.High).Add(kline.Low).Add(kline.Close).Div(Four)
}
case "open":
return func(kline *types.KLine) fixedpoint.Value { return kline.Open }
case "":
log.Infof("source not set, use hl2 by default")
return func(kline *types.KLine) fixedpoint.Value {
return kline.High.Add(kline.Low).Div(Two)
}
default:
panic(fmt.Sprintf("Unable to parse: %s", s.Source))
}
}
type DriftMA struct {
types.SeriesBase
ma1 types.UpdatableSeries
drift *indicator.Drift
ma2 types.UpdatableSeries
}
func (s *DriftMA) Update(value float64) {
s.ma1.Update(value)
s.drift.Update(s.ma1.Last())
s.ma2.Update(s.drift.Last())
}
func (s *DriftMA) Last() float64 {
return s.ma2.Last()
}
func (s *DriftMA) Index(i int) float64 {
return s.ma2.Index(i)
}
func (s *DriftMA) Length() int {
return s.ma2.Length()
}
func (s *DriftMA) ZeroPoint() float64 {
return s.drift.ZeroPoint()
}
func (s *Strategy) initIndicators() error {
s.ma = &indicator.SMA{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 = &DriftMA{
drift: &indicator.Drift{
MA: &indicator.SMA{IntervalWindow: s.IntervalWindow},
IntervalWindow: s.IntervalWindow,
},
ma1: &indicator.EWMA{
IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: 2},
},
ma2: &indicator.FisherTransform{
IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: 9},
},
}
s.drift.SeriesBase.Series = s.drift
s.atr = &indicator.ATR{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: 14}}
store, _ := s.Session.MarketDataStore(s.Symbol)
klines, ok := store.KLinesOfInterval(s.Interval)
if !ok {
return errors.New("klines not exists")
}
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)
}
return nil
}
func (s *Strategy) initTickerFunctions(ctx context.Context) {
if s.IsBackTesting() {
s.getLastPrice = func() fixedpoint.Value {
lastPrice, ok := s.Session.LastPrice(s.Symbol)
if !ok {
log.Error("cannot get lastprice")
}
return lastPrice
}
} else {
s.Session.MarketDataStream.OnBookTickerUpdate(func(ticker types.BookTicker) {
bestBid := ticker.Buy
bestAsk := ticker.Sell
var pricef, stoploss, atr, avg float64
var price fixedpoint.Value
if util.TryLock(&s.lock) {
if !bestAsk.IsZero() && !bestBid.IsZero() {
s.midPrice = bestAsk.Add(bestBid).Div(Two)
} else if !bestAsk.IsZero() {
s.midPrice = bestAsk
} else {
s.midPrice = bestBid
}
price = s.midPrice
pricef = s.midPrice.Float64()
} else {
return
}
if s.highestPrice > 0 && s.highestPrice < pricef {
s.highestPrice = pricef
}
if s.lowestPrice > 0 && s.lowestPrice > pricef {
s.lowestPrice = pricef
}
// for trailing stoploss during the realtime
if s.NoTrailingStopLoss {
s.lock.Unlock()
return
}
atr = s.atr.Last()
avg = s.buyPrice + s.sellPrice
stoploss = s.StopLoss.Float64()
exitShortCondition := (avg+atr/2 <= pricef || avg*(1.+stoploss) <= pricef || avg-atr*s.TakeProfitFactor >= pricef ||
((pricef-s.lowestPrice)/pricef > stoploss && (s.sellPrice-s.lowestPrice)/s.sellPrice > 0.01)) &&
(s.Position.IsShort() && !s.Position.IsDust(price))
exitLongCondition := (avg-atr/2 >= pricef || avg*(1.-stoploss) >= pricef || avg+atr*s.TakeProfitFactor <= pricef ||
((s.highestPrice-pricef)/pricef > stoploss && (s.highestPrice-s.buyPrice)/s.buyPrice > 0.01)) &&
(!s.Position.IsLong() && !s.Position.IsDust(price))
if exitShortCondition || exitLongCondition {
if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil {
log.WithError(err).Errorf("cannot cancel orders")
return
}
_ = s.ClosePosition(ctx, fixedpoint.One)
}
s.lock.Unlock()
})
s.getLastPrice = func() (lastPrice fixedpoint.Value) {
var ok bool
s.lock.RLock()
if s.midPrice.IsZero() {
lastPrice, ok = s.Session.LastPrice(s.Symbol)
if !ok {
log.Error("cannot get lastprice")
return lastPrice
}
} else {
lastPrice = s.midPrice
}
s.lock.RUnlock()
return lastPrice
}
}
}
func (s *Strategy) Draw(time types.Time, priceLine types.SeriesExtend, profit types.Series, cumProfit types.Series, zeroPoints types.Series) {
canvas := types.NewCanvas(s.InstanceID(), s.Interval)
Length := priceLine.Length()
if Length > 300 {
Length = 300
}
mean := priceLine.Mean(Length)
highestPrice := priceLine.Minus(mean).Abs().Highest(Length)
highestDrift := s.drift.Abs().Highest(Length)
hi := s.drift.drift.Abs().Highest(Length)
ratio := highestPrice / highestDrift
canvas.Plot("upband", s.ma.Add(s.stdevHigh), time, Length)
canvas.Plot("ma", s.ma, time, Length)
canvas.Plot("downband", s.ma.Minus(s.stdevLow), time, Length)
canvas.Plot("drift", s.drift.Mul(ratio).Add(mean), time, Length)
canvas.Plot("driftOrig", s.drift.drift.Mul(highestPrice/hi).Add(mean), time, Length)
canvas.Plot("zero", types.NumberSeries(mean), time, Length)
canvas.Plot("price", priceLine, time, Length)
canvas.Plot("zeroPoint", zeroPoints, time, Length)
f, err := os.Create(s.CanvasPath)
if err != nil {
log.WithError(err).Errorf("cannot create on %s", s.CanvasPath)
return
}
defer f.Close()
if err := canvas.Render(chart.PNG, f); err != nil {
log.WithError(err).Errorf("cannot render in drift")
}
canvas = types.NewCanvas(s.InstanceID())
if s.GraphPNLDeductFee {
canvas.PlotRaw("pnl % (with Fee Deducted)", profit, profit.Length())
} else {
canvas.PlotRaw("pnl %", profit, profit.Length())
}
f, err = os.Create(s.GraphPNLPath)
if err != nil {
log.WithError(err).Errorf("open pnl")
return
}
defer f.Close()
if err := canvas.Render(chart.PNG, f); err != nil {
log.WithError(err).Errorf("render pnl")
}
canvas = types.NewCanvas(s.InstanceID())
if s.GraphPNLDeductFee {
canvas.PlotRaw("cummulative pnl % (with Fee Deducted)", cumProfit, cumProfit.Length())
} else {
canvas.PlotRaw("cummulative pnl %", cumProfit, cumProfit.Length())
}
f, err = os.Create(s.GraphCumPNLPath)
if err != nil {
log.WithError(err).Errorf("open cumpnl")
return
}
defer f.Close()
if err := canvas.Render(chart.PNG, f); err != nil {
log.WithError(err).Errorf("render cumpnl")
}
}
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
instanceID := s.InstanceID()
// Will be set by persistence if there's any from DB
if s.Position == nil {
s.Position = types.NewPositionFromMarket(s.Market)
}
if s.ProfitStats == nil {
s.ProfitStats = types.NewProfitStats(s.Market)
}
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
// Get source function from config input
s.getSource = s.SourceFuncGenerator()
s.OnSuspend(func() {
_ = s.GeneralOrderExecutor.GracefulCancel(ctx)
})
s.OnEmergencyStop(func() {
_ = s.GeneralOrderExecutor.GracefulCancel(ctx)
_ = s.ClosePosition(ctx, fixedpoint.One)
})
s.GeneralOrderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position)
s.GeneralOrderExecutor.BindEnvironment(s.Environment)
s.GeneralOrderExecutor.BindProfitStats(s.ProfitStats)
s.GeneralOrderExecutor.BindTradeStats(s.TradeStats)
s.GeneralOrderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
bbgo.Sync(s)
})
s.GeneralOrderExecutor.Bind()
// Exit methods from config
for _, method := range s.ExitMethods {
method.Bind(session, s.GeneralOrderExecutor)
}
buyPrice := fixedpoint.Zero
sellPrice := fixedpoint.Zero
Volume := fixedpoint.Zero
profit := types.Float64Slice{}
cumProfit := types.Float64Slice{1.}
orderTagHistory := make(map[uint64]string)
s.buyPrice = 0
s.sellPrice = 0
s.Session.UserDataStream.OnOrderUpdate(func(order types.Order) {
orderTagHistory[order.OrderID] = order.Tag
})
modify := func(p fixedpoint.Value) fixedpoint.Value {
return p
}
if s.GraphPNLDeductFee {
fee := fixedpoint.NewFromFloat(0.0004) // taker fee % * 2, for upper bound
modify = func(p fixedpoint.Value) fixedpoint.Value {
return p.Mul(fixedpoint.One.Sub(fee))
}
}
s.Session.UserDataStream.OnTradeUpdate(func(trade types.Trade) {
tag, ok := orderTagHistory[trade.OrderID]
if !ok {
panic(fmt.Sprintf("cannot find order: %v", trade))
}
if tag == "close" {
if !buyPrice.IsZero() {
profit.Update(modify(trade.Price.Div(buyPrice)).
Sub(fixedpoint.One).
Mul(trade.Quantity).
Div(Volume).
Add(fixedpoint.One).
Float64())
cumProfit.Update(cumProfit.Last() * profit.Last())
Volume = Volume.Sub(trade.Quantity)
if Volume.IsZero() {
buyPrice = fixedpoint.Zero
}
if !sellPrice.IsZero() {
panic("sellprice shouldn't be zero")
}
} else if !sellPrice.IsZero() {
profit.Update(modify(sellPrice.Div(trade.Price)).
Sub(fixedpoint.One).
Mul(trade.Quantity).
Div(Volume).
Neg().
Add(fixedpoint.One).
Float64())
cumProfit.Update(cumProfit.Last() * profit.Last())
Volume = Volume.Add(trade.Quantity)
if Volume.IsZero() {
sellPrice = fixedpoint.Zero
}
if !buyPrice.IsZero() {
panic("buyprice shouldn't be zero")
}
} else {
panic("no price available")
}
} else if tag == "short" {
if buyPrice.IsZero() {
if !sellPrice.IsZero() {
sellPrice = sellPrice.Mul(Volume).Sub(trade.Price.Mul(trade.Quantity)).Div(Volume.Sub(trade.Quantity))
} else {
sellPrice = trade.Price
}
} else {
profit.Update(modify(trade.Price.Div(buyPrice)).Float64())
cumProfit.Update(cumProfit.Last() * profit.Last())
buyPrice = fixedpoint.Zero
Volume = fixedpoint.Zero
sellPrice = trade.Price
}
Volume = Volume.Sub(trade.Quantity)
} else if tag == "long" {
if sellPrice.IsZero() {
if !buyPrice.IsZero() {
buyPrice = buyPrice.Mul(Volume).Add(trade.Price.Mul(trade.Quantity)).Div(Volume.Add(trade.Quantity))
} else {
buyPrice = trade.Price
}
} else {
profit.Update(modify(sellPrice.Div(trade.Price)).Float64())
cumProfit.Update(cumProfit.Last() * profit.Last())
sellPrice = fixedpoint.Zero
buyPrice = trade.Price
Volume = fixedpoint.Zero
}
Volume = Volume.Add(trade.Quantity)
}
s.buyPrice = buyPrice.Float64()
s.highestPrice = s.buyPrice
s.sellPrice = sellPrice.Float64()
s.lowestPrice = s.sellPrice
})
if err := s.initIndicators(); err != nil {
log.WithError(err).Errorf("initIndicator failed")
return nil
}
s.initTickerFunctions(ctx)
dynamicKLine := &types.KLine{}
priceLine := types.NewQueue(300)
zeroPoints := types.NewQueue(300)
stoploss := s.StopLoss.Float64()
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
if s.Status != types.StrategyStatusRunning {
return
}
if kline.Symbol != s.Symbol {
return
}
var driftPred, atr float64
var drift []float64
if !kline.Closed {
return
}
if kline.Interval == types.Interval1m {
if s.NoTrailingStopLoss || !s.IsBackTesting() {
return
}
// for doing the trailing stoploss during backtesting
atr = s.atr.Last()
price := s.getLastPrice()
pricef := price.Float64()
lowf := math.Min(kline.Low.Float64(), pricef)
highf := math.Max(kline.High.Float64(), pricef)
if s.lowestPrice > 0 && lowf < s.lowestPrice {
s.lowestPrice = lowf
}
if s.highestPrice > 0 && highf > s.highestPrice {
s.highestPrice = highf
}
avg := s.buyPrice + s.sellPrice
exitShortCondition := (avg+atr/2 <= highf || avg*(1.+stoploss) <= highf || avg-atr*s.TakeProfitFactor >= lowf ||
((highf-s.lowestPrice)/pricef > stoploss && (s.sellPrice-s.lowestPrice)/s.sellPrice > 0.01)) &&
(s.Position.IsShort() && !s.Position.IsDust(price))
exitLongCondition := (avg-atr/2 >= lowf || avg*(1.-stoploss) >= lowf || avg+atr*s.TakeProfitFactor <= highf ||
((s.highestPrice-pricef)/pricef > stoploss && (s.highestPrice-s.buyPrice)/s.buyPrice > 0.01)) &&
(s.Position.IsLong() && !s.Position.IsDust(price))
if exitShortCondition || exitLongCondition {
if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil {
log.WithError(err).Errorf("cannot cancel orders")
return
}
_ = s.ClosePosition(ctx, fixedpoint.One)
}
return
}
dynamicKLine.Set(&kline)
source := s.getSource(dynamicKLine)
sourcef := source.Float64()
priceLine.Update(sourcef)
s.ma.Update(sourcef)
s.drift.Update(sourcef)
zeroPoint := s.drift.ZeroPoint()
zeroPoints.Update(zeroPoint)
s.atr.PushK(kline)
drift = s.drift.Array(2)
ddrift := s.drift.drift.Array(2)
driftPred = s.drift.Predict(s.PredictOffset)
atr = s.atr.Last()
price := s.getLastPrice()
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.buyPrice + s.sellPrice
if !s.IsBackTesting() {
balances := s.Session.GetAccount().Balances()
bbgo.Notify("zeroPoint: %.4f, source: %.4f, price: %.4f, driftPred: %.4f, drift: %.4f, drift[1]: %.4f, atr: %.4f, avg: %.4f",
zeroPoint, sourcef, pricef, driftPred, drift[0], drift[1], atr, avg)
// Notify will parse args to strings and process separately
bbgo.Notify("balances: [Base] %s [Quote] %s", balances[s.Market.BaseCurrency].String(), balances[s.Market.QuoteCurrency].String())
}
//shortCondition := (sourcef <= zeroPoint && driftPred <= drift[0] && drift[0] <= 0 && drift[1] > 0 && drift[2] > drift[1])
//longCondition := (sourcef >= zeroPoint && driftPred >= drift[0] && drift[0] >= 0 && drift[1] < 0 && drift[2] < drift[1])
//bothUp := ddrift[1] < ddrift[0] && drift[1] < drift[0]
//bothDown := ddrift[1] > ddrift[0] && drift[1] > drift[0]
shortCondition := (ddrift[0] <= 0 || drift[0] <= 0) && driftPred < 0.
longCondition := (ddrift[0] >= 0 || drift[0] >= 0) && driftPred > 0
exitShortCondition := (avg+atr <= highf || avg*(1.+stoploss) <= highf || avg-atr*s.TakeProfitFactor >= lowf) &&
(s.Position.IsShort() && !s.Position.IsDust(fixedpoint.Max(price, source))) && !longCondition && !shortCondition
exitLongCondition := (avg-atr >= lowf || avg*(1.-stoploss) >= lowf || avg+atr*s.TakeProfitFactor <= highf) &&
(s.Position.IsLong() && !s.Position.IsDust(fixedpoint.Min(price, source))) && !shortCondition && !longCondition
if exitShortCondition || exitLongCondition {
if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil {
log.WithError(err).Errorf("cannot cancel orders")
return
}
_ = s.ClosePosition(ctx, fixedpoint.One)
return
}
if shortCondition {
if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil {
log.WithError(err).Errorf("cannot cancel orders")
return
}
baseBalance, ok := s.Session.GetAccount().Balance(s.Market.BaseCurrency)
if !ok {
log.Errorf("unable to get baseBalance")
return
}
source = source.Add(fixedpoint.NewFromFloat(s.stdevHigh.Last() * s.HighLowVarianceMultiplier))
if source.Compare(price) < 0 {
source = price
}
sourcef = source.Float64()
if s.Market.IsDustQuantity(baseBalance.Available, source) {
return
}
// Cleanup pending StopOrders
quantity := baseBalance.Available
createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeLimit,
Price: source,
Quantity: quantity,
Tag: "short",
})
if err != nil {
log.WithError(err).Errorf("cannot place sell order")
return
}
orderTagHistory[createdOrders[0].OrderID] = "short"
}
if longCondition {
if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil {
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
}
sourcef = source.Float64()
quoteBalance, ok := s.Session.GetAccount().Balance(s.Market.QuoteCurrency)
if !ok {
log.Errorf("unable to get quoteCurrency")
return
}
if s.Market.IsDustQuantity(
quoteBalance.Available.Div(source), source) {
return
}
quantity := quoteBalance.Available.Div(source)
createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeLimit,
Price: source,
Quantity: quantity,
Tag: "long",
})
if err != nil {
log.WithError(err).Errorf("cannot place buy order")
return
}
orderTagHistory[createdOrders[0].OrderID] = "long"
}
})
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
defer s.Print(os.Stdout)
defer fmt.Fprintln(os.Stdout, s.TradeStats.BriefString())
if s.GenerateGraph {
s.Draw(dynamicKLine.StartTime, priceLine, &profit, &cumProfit, zeroPoints)
}
wg.Done()
})
return nil
}

View File

@ -15,6 +15,7 @@ import (
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator" "github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/util"
) )
const ID = "ewo_dgtrd" const ID = "ewo_dgtrd"
@ -114,11 +115,6 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
} }
} }
type UpdatableSeries interface {
types.Series
Update(value float64)
}
// Refer: https://tw.tradingview.com/script/XZyG5SOx-CCI-Stochastic-and-a-quick-lesson-on-Scalping-Trading-Systems/ // Refer: https://tw.tradingview.com/script/XZyG5SOx-CCI-Stochastic-and-a-quick-lesson-on-Scalping-Trading-Systems/
type CCISTOCH struct { type CCISTOCH struct {
cci *indicator.CCI cci *indicator.CCI
@ -180,8 +176,8 @@ func (inc *CCISTOCH) SellSignal() bool {
} }
type VWEMA struct { type VWEMA struct {
PV UpdatableSeries PV types.UpdatableSeries
V UpdatableSeries V types.UpdatableSeries
} }
func (inc *VWEMA) Last() float64 { func (inc *VWEMA) Last() float64 {
@ -1010,7 +1006,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
bestAsk := ticker.Sell bestAsk := ticker.Sell
var midPrice fixedpoint.Value var midPrice fixedpoint.Value
if tryLock(&s.lock) { if util.TryLock(&s.lock) {
if !bestAsk.IsZero() && !bestBid.IsZero() { if !bestAsk.IsZero() && !bestBid.IsZero() {
s.midPrice = bestAsk.Add(bestBid).Div(types.Two) s.midPrice = bestAsk.Add(bestBid).Div(types.Two)
} else if !bestAsk.IsZero() { } else if !bestAsk.IsZero() {

View File

@ -12,6 +12,10 @@ func (s *Float64Slice) Push(v float64) {
*s = append(*s, v) *s = append(*s, v)
} }
func (s *Float64Slice) Update(v float64) {
*s = append(*s, v)
}
func (s *Float64Slice) Pop(i int64) (v float64) { func (s *Float64Slice) Pop(i int64) (v float64) {
v = (*s)[i] v = (*s)[i]
*s = append((*s)[:i], (*s)[i+1:]...) *s = append((*s)[:i], (*s)[i+1:]...)

View File

@ -4,7 +4,9 @@ import (
"fmt" "fmt"
"math" "math"
"reflect" "reflect"
"time"
"github.com/wcharczuk/go-chart/v2"
"gonum.org/v1/gonum/stat" "gonum.org/v1/gonum/stat"
) )
@ -43,6 +45,15 @@ func (inc *Queue) Length() int {
return len(inc.arr) return len(inc.arr)
} }
func (inc *Queue) Clone() *Queue {
out := &Queue{
arr: inc.arr[:],
size: inc.size,
}
out.SeriesBase.Series = out
return out
}
func (inc *Queue) Update(v float64) { func (inc *Queue) Update(v float64) {
inc.arr = append(inc.arr, v) inc.arr = append(inc.arr, v)
if len(inc.arr) > inc.size { if len(inc.arr) > inc.size {
@ -50,7 +61,7 @@ func (inc *Queue) Update(v float64) {
} }
} }
var _ SeriesExtend = &Queue{} var _ UpdatableSeriesExtend = &Queue{}
// Float64Indicator is the indicators (SMA and EWMA) that we want to use are returning float64 data. // Float64Indicator is the indicators (SMA and EWMA) that we want to use are returning float64 data.
type Float64Indicator interface { type Float64Indicator interface {
@ -93,6 +104,7 @@ type SeriesExtend interface {
Variance(length int) float64 Variance(length int) float64
Covariance(b Series, length int) float64 Covariance(b Series, length int) float64
Correlation(b Series, length int, method ...CorrFunc) float64 Correlation(b Series, length int, method ...CorrFunc) float64
AutoCorrelation(length int, lag ...int) float64
Rank(length int) SeriesExtend Rank(length int) SeriesExtend
Sigmoid() SeriesExtend Sigmoid() SeriesExtend
Softmax(window int) SeriesExtend Softmax(window int) SeriesExtend
@ -120,6 +132,24 @@ type UpdatableSeriesExtend interface {
Update(float64) Update(float64)
} }
func Clone(u UpdatableSeriesExtend) UpdatableSeriesExtend {
method, ok := reflect.TypeOf(u).MethodByName("Clone")
if ok {
out := method.Func.Call([]reflect.Value{reflect.ValueOf(u)})
return out[0].Interface().(UpdatableSeriesExtend)
}
panic("method Clone not exist")
}
func TestUpdate(u UpdatableSeriesExtend, input float64) UpdatableSeriesExtend {
method, ok := reflect.TypeOf(u).MethodByName("TestUpdate")
if ok {
out := method.Func.Call([]reflect.Value{reflect.ValueOf(u), reflect.ValueOf(input)})
return out[0].Interface().(UpdatableSeriesExtend)
}
panic("method TestUpdate not exist")
}
// The interface maps to pinescript basic type `series` for bool type // The interface maps to pinescript basic type `series` for bool type
// Access the internal historical data from the latest to the oldest // Access the internal historical data from the latest to the oldest
// Index(0) always maps to Last() // Index(0) always maps to Last()
@ -133,13 +163,10 @@ type BoolSeries interface {
// if limit is given, will only sum first limit numbers (a.Index[0..limit]) // if limit is given, will only sum first limit numbers (a.Index[0..limit])
// otherwise will sum all elements // otherwise will sum all elements
func Sum(a Series, limit ...int) (sum float64) { func Sum(a Series, limit ...int) (sum float64) {
l := -1 l := a.Length()
if len(limit) > 0 { if len(limit) > 0 && limit[0] < l {
l = limit[0] l = limit[0]
} }
if l < a.Length() {
l = a.Length()
}
for i := 0; i < l; i++ { for i := 0; i < l; i++ {
sum += a.Index(i) sum += a.Index(i)
} }
@ -150,13 +177,10 @@ func Sum(a Series, limit ...int) (sum float64) {
// if limit is given, will only calculate the average of first limit numbers (a.Index[0..limit]) // if limit is given, will only calculate the average of first limit numbers (a.Index[0..limit])
// otherwise will operate on all elements // otherwise will operate on all elements
func Mean(a Series, limit ...int) (mean float64) { func Mean(a Series, limit ...int) (mean float64) {
l := -1 l := a.Length()
if len(limit) > 0 { if len(limit) > 0 && limit[0] < l {
l = limit[0] l = limit[0]
} }
if l < a.Length() {
l = a.Length()
}
return Sum(a, l) / float64(l) return Sum(a, l) / float64(l)
} }
@ -183,7 +207,7 @@ func Abs(a Series) SeriesExtend {
var _ Series = &AbsResult{} var _ Series = &AbsResult{}
func Predict(a Series, lookback int, offset ...int) float64 { func LinearRegression(a Series, lookback int) (alpha float64, beta float64) {
if a.Length() < lookback { if a.Length() < lookback {
lookback = a.Length() lookback = a.Length()
} }
@ -194,7 +218,12 @@ func Predict(a Series, lookback int, offset ...int) float64 {
x[i] = float64(i) x[i] = float64(i)
y[i] = a.Index(i) y[i] = a.Index(i)
} }
alpha, beta := stat.LinearRegression(x, y, weights, false) alpha, beta = stat.LinearRegression(x, y, weights, false)
return
}
func Predict(a Series, lookback int, offset ...int) float64 {
alpha, beta := LinearRegression(a, lookback)
o := -1.0 o := -1.0
if len(offset) > 0 { if len(offset) > 0 {
o = -float64(offset[0]) o = -float64(offset[0])
@ -335,6 +364,10 @@ func (a NumberSeries) Length() int {
return math.MaxInt32 return math.MaxInt32
} }
func (a NumberSeries) Clone() NumberSeries {
return a
}
var _ Series = NumberSeries(0) var _ Series = NumberSeries(0)
type AddSeriesResult struct { type AddSeriesResult struct {
@ -597,11 +630,11 @@ func Dot(a interface{}, b interface{}, limit ...int) float64 {
// if limit is given, will only take the first limit numbers (a.Index[0..limit]) // if limit is given, will only take the first limit numbers (a.Index[0..limit])
// otherwise will operate on all elements // otherwise will operate on all elements
func Array(a Series, limit ...int) (result []float64) { func Array(a Series, limit ...int) (result []float64) {
l := -1 l := a.Length()
if len(limit) > 0 { if len(limit) > 0 && l > limit[0] {
l = limit[0] l = limit[0]
} }
if l < a.Length() { if l > a.Length() {
l = a.Length() l = a.Length()
} }
result = make([]float64, l) result = make([]float64, l)
@ -617,13 +650,10 @@ func Array(a Series, limit ...int) (result []float64) {
// //
// notice that the return type is a Float64Slice, which implements the Series interface // notice that the return type is a Float64Slice, which implements the Series interface
func Reverse(a Series, limit ...int) (result Float64Slice) { func Reverse(a Series, limit ...int) (result Float64Slice) {
l := -1 l := a.Length()
if len(limit) > 0 { if len(limit) > 0 && l > limit[0] {
l = limit[0] l = limit[0]
} }
if l < a.Length() {
l = a.Length()
}
result = make([]float64, l) result = make([]float64, l)
for i := 0; i < l; i++ { for i := 0; i < l; i++ {
result[l-i-1] = a.Index(i) result[l-i-1] = a.Index(i)
@ -709,10 +739,8 @@ func PercentageChange(a Series, offset ...int) SeriesExtend {
func Stdev(a Series, params ...int) float64 { func Stdev(a Series, params ...int) float64 {
length := a.Length() length := a.Length()
if len(params) > 0 { if len(params) > 0 && params[0] < length {
if params[0] < length { length = params[0]
length = params[0]
}
} }
ddof := 0 ddof := 0
if len(params) > 1 { if len(params) > 1 {
@ -817,6 +845,17 @@ func Correlation(a Series, b Series, length int, method ...CorrFunc) float64 {
return runner(a, b, length) return runner(a, b, length)
} }
// similar to pandas.Series.autocorr() function.
//
// The method computes the Pearson correlation between Series and shifted itself
func AutoCorrelation(a Series, length int, lags ...int) float64 {
lag := 1
if len(lags) > 0 {
lag = lags[0]
}
return Pearson(a, Shift(a, lag), length)
}
// similar to pandas.Series.cov() function with ddof=0 // similar to pandas.Series.cov() function with ddof=0
// //
// Compute covariance with Series // Compute covariance with Series
@ -1118,4 +1157,65 @@ func (l *LogisticRegressionModel) Predict(x []float64) float64 {
return sigmoid(z + l.Gradient) return sigmoid(z + l.Gradient)
} }
type Canvas struct {
chart.Chart
Interval Interval
}
func NewCanvas(title string, intervals ...Interval) *Canvas {
valueFormatter := chart.TimeValueFormatter
interval := Interval1m
if len(intervals) > 0 {
interval = intervals[0]
if interval.Minutes() > 24*60 {
valueFormatter = chart.TimeDateValueFormatter
} else if interval.Minutes() > 60 {
valueFormatter = chart.TimeHourValueFormatter
} else {
valueFormatter = chart.TimeMinuteValueFormatter
}
} else {
valueFormatter = chart.IntValueFormatter
}
out := &Canvas{
Chart: chart.Chart{
Title: title,
XAxis: chart.XAxis{
ValueFormatter: valueFormatter,
},
},
Interval: interval,
}
out.Chart.Elements = []chart.Renderable{
chart.LegendLeft(&out.Chart),
}
return out
}
func (canvas *Canvas) Plot(tag string, a Series, endTime Time, length int) {
var timeline []time.Time
e := endTime.Time()
for i := length - 1; i >= 0; i-- {
shiftedT := e.Add(-time.Duration(i*canvas.Interval.Minutes()) * time.Minute)
timeline = append(timeline, shiftedT)
}
canvas.Series = append(canvas.Series, chart.TimeSeries{
Name: tag,
YValues: Reverse(a, length),
XValues: timeline,
})
}
func (canvas *Canvas) PlotRaw(tag string, a Series, length int) {
var x []float64
for i := 0; i < length; i++ {
x = append(x, float64(i))
}
canvas.Series = append(canvas.Series, chart.ContinuousSeries{
Name: tag,
XValues: x,
YValues: Reverse(a, length),
})
}
// TODO: ta.linreg // TODO: ta.linreg

View File

@ -1,9 +1,13 @@
package types package types
import ( import (
"github.com/stretchr/testify/assert" //"os"
"gonum.org/v1/gonum/stat"
"testing" "testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/wcharczuk/go-chart/v2"
"gonum.org/v1/gonum/stat"
) )
func TestFloat(t *testing.T) { func TestFloat(t *testing.T) {
@ -119,18 +123,18 @@ func TestSigmoid(t *testing.T) {
// from https://en.wikipedia.org/wiki/Logistic_regression // from https://en.wikipedia.org/wiki/Logistic_regression
func TestLogisticRegression(t *testing.T) { func TestLogisticRegression(t *testing.T) {
a := []Float64Slice{{0.5, 0.75, 1., 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3., 3.25, 3.5, 4., 4.25, 4.5, 4.75, 5., 5.5}} a := []Float64Slice{{0.5, 0.75, 1., 1.25, 1.5, 1.75, 1.75, 2.0, 2.25, 2.5, 2.75, 3., 3.25, 3.5, 4., 4.25, 4.5, 4.75, 5., 5.5}}
b := Float64Slice{0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1} b := Float64Slice{0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1}
var x []Series var x []Series
x = append(x, &a[0]) x = append(x, &a[0])
model := LogisticRegression(x, &b, a[0].Length(), 8000, 0.0009) model := LogisticRegression(x, &b, a[0].Length(), 90000, 0.0018)
inputs := []float64{1., 2., 2.7, 3., 4., 5.} inputs := []float64{1., 2., 2.7, 3., 4., 5.}
results := []bool{false, false, true, true, true, true} results := []bool{false, false, true, true, true, true}
for i, x := range inputs { for i, x := range inputs {
input := []float64{x} input := []float64{x}
pred := model.Predict(input) pred := model.Predict(input)
assert.Equal(t, pred > 0.5, results[i]) assert.Equal(t, pred >= 0.5, results[i])
} }
} }
@ -144,3 +148,23 @@ func TestDot(t *testing.T) {
out3 := Dot(3., &a, 2) out3 := Dot(3., &a, 2)
assert.InDelta(t, out2, out3, 0.001) assert.InDelta(t, out2, out3, 0.001)
} }
func TestClone(t *testing.T) {
a := NewQueue(3)
a.Update(3.)
b := Clone(a)
b.Update(4.)
assert.Equal(t, a.Last(), 3.)
assert.Equal(t, b.Last(), 4.)
}
func TestPlot(t *testing.T) {
ct := NewCanvas("test", Interval5m)
a := Float64Slice{200., 205., 230., 236}
ct.Plot("test", &a, Time(time.Now()), 4)
assert.Equal(t, ct.Interval, Interval5m)
assert.Equal(t, ct.Series[0].(chart.TimeSeries).Len(), 4)
//f, _ := os.Create("output.png")
//defer f.Close()
//ct.Render(chart.PNG, f)
}

View File

@ -71,6 +71,26 @@ type KLine struct {
Closed bool `json:"closed" db:"closed"` Closed bool `json:"closed" db:"closed"`
} }
func (k *KLine) Set(o *KLine) {
k.GID = o.GID
k.Exchange = o.Exchange
k.Symbol = o.Symbol
k.StartTime = o.StartTime
k.EndTime = o.EndTime
k.Interval = o.Interval
k.Open = o.Open
k.Close = o.Close
k.High = o.High
k.Low = o.Low
k.Volume = o.Volume
k.QuoteVolume = o.QuoteVolume
k.TakerBuyBaseAssetVolume = o.TakerBuyBaseAssetVolume
k.TakerBuyQuoteAssetVolume = o.TakerBuyQuoteAssetVolume
k.LastTradeID = o.LastTradeID
k.NumberOfTrades = o.NumberOfTrades
k.Closed = o.Closed
}
func (k KLine) GetStartTime() Time { func (k KLine) GetStartTime() Time {
return k.StartTime return k.StartTime
} }

28
pkg/types/omega.go Normal file
View File

@ -0,0 +1,28 @@
package types
// 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
View 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)
}

View File

@ -121,6 +121,10 @@ func (s *SeriesBase) Correlation(b Series, length int, method ...CorrFunc) float
return Correlation(s, b, length, method...) return Correlation(s, b, length, method...)
} }
func (s *SeriesBase) AutoCorrelation(length int, lag ...int) float64 {
return AutoCorrelation(s, length, lag...)
}
func (s *SeriesBase) Rank(length int) SeriesExtend { func (s *SeriesBase) Rank(length int) SeriesExtend {
return Rank(s, length) return Rank(s, length)
} }

View File

@ -1,32 +1,28 @@
package statistics package types
import ( import (
"math" "math"
"github.com/c9s/bbgo/pkg/types"
) )
// Sharpe: Calcluates the sharpe ratio of access returns // 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 periods (int): Freq. of returns (252/365 for daily, 12 for monthy)
// @param annualize (bool): return annualize sharpe? // @param annualize (bool): return annualize sharpe?
// @param smart (bool): return smart sharpe ratio // @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 data := returns
num := data.Length() num := data.Length()
if types.Lowest(data, num) >= 0 && types.Highest(data, num) > 1 { divisor := Stdev(data, data.Length(), 1)
data = types.PercentageChange(returns)
}
divisor := types.Stdev(data, data.Length(), 1)
if smart { if smart {
sum := 0. 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++ { for i := 1; i < num; i++ {
sum += float64(num-i) / float64(num) * math.Pow(coef, float64(i)) sum += float64(num-i) / float64(num) * math.Pow(coef, float64(i))
} }
divisor = divisor * math.Sqrt(1.+2.*sum) divisor = divisor * math.Sqrt(1.+2.*sum)
} }
result := types.Mean(data) / divisor result := Mean(data) / divisor
if annualize { if annualize {
return result * math.Sqrt(float64(periods)) return result * math.Sqrt(float64(periods))
} }

View File

@ -1,7 +1,6 @@
package statistics package types
import ( import (
"github.com/c9s/bbgo/pkg/types"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"testing" "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)) print(qx.stats.sharpe(pd.Series([0.01, 0.1, 0.001]), 0, 252, True, False))
*/ */
func TestSharpe(t *testing.T) { 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) output := Sharpe(a, 0, false, false)
assert.InDelta(t, output, 0.67586, 0.0001) assert.InDelta(t, output, 0.67586, 0.0001)
output = Sharpe(a, 252, false, false) output = Sharpe(a, 252, false, false)

View File

@ -1,30 +1,121 @@
package types package types
import ( import (
"time"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/fixedpoint"
) )
type IntervalProfitCollector struct {
Interval Interval `json:"interval"`
Profits *Float64Slice `json:"profits"`
tmpTime time.Time `json:"tmpTime"`
}
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
}
// TODO: Add more stats from the reference: // TODO: Add more stats from the reference:
// See https://www.metatrader5.com/en/terminal/help/algotrading/testing_report // See https://www.metatrader5.com/en/terminal/help/algotrading/testing_report
type TradeStats struct { type TradeStats struct {
Symbol string `json:"symbol"` Symbol string `json:"symbol"`
WinningRatio fixedpoint.Value `json:"winningRatio" yaml:"winningRatio"` WinningRatio fixedpoint.Value `json:"winningRatio" yaml:"winningRatio"`
NumOfLossTrade int `json:"numOfLossTrade" yaml:"numOfLossTrade"` NumOfLossTrade int `json:"numOfLossTrade" yaml:"numOfLossTrade"`
NumOfProfitTrade int `json:"numOfProfitTrade" yaml:"numOfProfitTrade"` NumOfProfitTrade int `json:"numOfProfitTrade" yaml:"numOfProfitTrade"`
GrossProfit fixedpoint.Value `json:"grossProfit" yaml:"grossProfit"` GrossProfit fixedpoint.Value `json:"grossProfit" yaml:"grossProfit"`
GrossLoss fixedpoint.Value `json:"grossLoss" yaml:"grossLoss"` GrossLoss fixedpoint.Value `json:"grossLoss" yaml:"grossLoss"`
Profits []fixedpoint.Value `json:"profits" yaml:"profits"` Profits []fixedpoint.Value `json:"profits" yaml:"profits"`
Losses []fixedpoint.Value `json:"losses" yaml:"losses"` Losses []fixedpoint.Value `json:"losses" yaml:"losses"`
MostProfitableTrade fixedpoint.Value `json:"mostProfitableTrade" yaml:"mostProfitableTrade"` MostProfitableTrade fixedpoint.Value `json:"mostProfitableTrade" yaml:"mostProfitableTrade"`
MostLossTrade fixedpoint.Value `json:"mostLossTrade" yaml:"mostLossTrade"` MostLossTrade fixedpoint.Value `json:"mostLossTrade" yaml:"mostLossTrade"`
ProfitFactor fixedpoint.Value `json:"profitFactor" yaml:"profitFactor"` ProfitFactor fixedpoint.Value `json:"profitFactor" yaml:"profitFactor"`
TotalNetProfit fixedpoint.Value `json:"totalNetProfit" yaml:"totalNetProfit"` TotalNetProfit fixedpoint.Value `json:"totalNetProfit" yaml:"totalNetProfit"`
IntervalProfits map[Interval]*IntervalProfitCollector `jons:"intervalProfits,omitempty" yaml: "intervalProfits,omitempty"`
} }
func NewTradeStats(symbol string) *TradeStats { 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) { func (s *TradeStats) Add(profit *Profit) {
@ -33,6 +124,9 @@ func (s *TradeStats) Add(profit *Profit) {
} }
s.add(profit.Profit) s.add(profit.Profit)
for _, v := range s.IntervalProfits {
v.Update(profit)
}
} }
func (s *TradeStats) add(pnl fixedpoint.Value) { func (s *TradeStats) add(pnl fixedpoint.Value) {
@ -61,6 +155,24 @@ func (s *TradeStats) add(pnl fixedpoint.Value) {
s.ProfitFactor = s.GrossProfit.Div(s.GrossLoss.Abs()) 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 { func (s *TradeStats) String() string {
out, _ := yaml.Marshal(s) out, _ := yaml.Marshal(s)
return string(out) return string(out)

View File

@ -1,16 +1,16 @@
//go:build !go1.18 //go:build !go1.18
// +build !go1.18 // +build !go1.18
package ewoDgtrd package util
import "sync" import "sync"
func tryLock(lock *sync.RWMutex) bool { func TryLock(lock *sync.RWMutex) bool {
lock.Lock() lock.Lock()
return true return true
} }
func tryRLock(lock *sync.RWMutex) bool { func TryRLock(lock *sync.RWMutex) bool {
lock.RLock() lock.RLock()
return true return true
} }

View File

@ -1,14 +1,14 @@
//go:build go1.18 //go:build go1.18
// +build go1.18 // +build go1.18
package ewoDgtrd package util
import "sync" import "sync"
func tryLock(lock *sync.RWMutex) bool { func TryLock(lock *sync.RWMutex) bool {
return lock.TryLock() return lock.TryLock()
} }
func tryRLock(lock *sync.RWMutex) bool { func TryRLock(lock *sync.RWMutex) bool {
return lock.TryRLock() return lock.TryRLock()
} }