diff --git a/go.mod b/go.mod index 47e219dbf..e459de352 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,7 @@ require ( github.com/x-cray/logrus-prefixed-formatter v0.5.2 golang.org/x/net v0.0.0-20201002202402-0a1ea396d57c // indirect golang.org/x/time v0.0.0-20191024005414-555d28b269f0 + gonum.org/v1/gonum v0.8.1 // indirect gopkg.in/yaml.v2 v2.3.0 // indirect gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c ) diff --git a/go.sum b/go.sum index 6335a56dd..5e03dd20f 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,7 @@ github.com/adshao/go-binance v0.0.0-20200604145522-bf563a35f17f h1:lVxx5HSt/impr github.com/adshao/go-binance v0.0.0-20200604145522-bf563a35f17f/go.mod h1:XlIpE7brbCEQxp6VRouG/ZgjLjygQWE1xnc1DtQNp6I= github.com/adshao/go-binance v0.0.0-20201015231210-37cee298310e h1:+fOwsQnvjCIVXuiVbyfuzNbubHvUrx1saeRa9pd7Df8= github.com/adshao/go-binance v0.0.0-20201015231210-37cee298310e/go.mod h1:XlIpE7brbCEQxp6VRouG/ZgjLjygQWE1xnc1DtQNp6I= +github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY= @@ -60,6 +61,7 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 h1:Ghm4eQYC0nEPnSJdVkTrXpu9KtoVCSo1hg7mtI7G9KU= github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -79,6 +81,7 @@ github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= +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/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -147,6 +150,7 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -301,11 +305,15 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +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-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -375,8 +383,10 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= @@ -393,6 +403,11 @@ golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8= golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= +gonum.org/v1/gonum v0.8.1 h1:wGtP3yGpc5mCLOLeTeBdjeui9oZSz5De0eOjMLC/QuQ= +gonum.org/v1/gonum v0.8.1/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -444,3 +459,4 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/pkg/bbgo/session.go b/pkg/bbgo/session.go index 018f5839c..84c5634cf 100644 --- a/pkg/bbgo/session.go +++ b/pkg/bbgo/session.go @@ -11,6 +11,7 @@ type StandardIndicatorSet struct { // interval -> window SMA map[types.IntervalWindow]*indicator.SMA EWMA map[types.IntervalWindow]*indicator.EWMA + BOLL map[types.IntervalWindow]*indicator.BOLL store *MarketDataStore } @@ -33,6 +34,12 @@ func NewStandardIndicatorSet(symbol string, store *MarketDataStore) *StandardInd set.EWMA[iw] = &indicator.EWMA{IntervalWindow: iw} set.EWMA[iw].Bind(store) } + + // setup BOLL indicator, we may refactor BOLL indicator by subscribing SMA indicator, + // however, since general used BOLLINGER band use window 21, which is not in the existing SMA indicator sets. + iw := types.IntervalWindow{Interval: interval, Window: 21} + set.BOLL[iw] = &indicator.BOLL{IntervalWindow: iw, K: 2.0} + set.BOLL[iw].Bind(store) } return set diff --git a/pkg/bbgo/trader.go b/pkg/bbgo/trader.go index 44cf88c30..a4dcae2fb 100644 --- a/pkg/bbgo/trader.go +++ b/pkg/bbgo/trader.go @@ -46,8 +46,6 @@ type Trader struct { exchangeStrategies map[string][]SingleExchangeStrategy tradeReporter *TradeReporter - // reportTimer *time.Timer - // ProfitAndLossCalculator *accounting.ProfitAndLossCalculator } func NewTrader(environ *Environment) *Trader { diff --git a/pkg/indicator/boll.go b/pkg/indicator/boll.go new file mode 100644 index 000000000..5301a97d1 --- /dev/null +++ b/pkg/indicator/boll.go @@ -0,0 +1,89 @@ +package indicator + +import ( + "time" + + "gonum.org/v1/gonum/stat" + + "github.com/c9s/bbgo/pkg/types" +) + +/* +BOLL implements the bollinger indicator: + +The Basics of Bollinger Bands +- https://www.investopedia.com/articles/technical/102201.asp + +Bollinger Bands +- https://www.investopedia.com/terms/b/bollingerbands.asp + +Bollinger Bands Technical indicator guide: +- https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/bollinger-bands +*/ +type BOLL struct { + types.IntervalWindow + + // times of Std, generally it's 2 + K float64 + + SMA Float64Slice + StdDev Float64Slice + UpBand Float64Slice + DownBand Float64Slice + + EndTime time.Time +} + +func (inc *BOLL) LastSMA() float64 { + return inc.SMA[len(inc.SMA)-1] +} + +func (inc *BOLL) calculateAndUpdate(kLines []types.KLine) { + if len(kLines) < inc.Window { + return + } + + var index = len(kLines) - 1 + var kline = kLines[index] + + if inc.EndTime != zeroTime && kline.EndTime.Before(inc.EndTime) { + return + } + + var recentK = kLines[index-(inc.Window-1) : index+1] + var sma = calculateSMA(recentK) + inc.SMA.Push(sma) + + var prices []float64 + for _, k := range recentK { + prices = append(prices, k.Close) + } + + var std = stat.StdDev(prices, nil) + inc.StdDev.Push(std) + + var upBand = sma + inc.K*std + inc.UpBand.Push(upBand) + + var downBand = sma - inc.K*std + inc.DownBand.Push(downBand) + + // update end time + inc.EndTime = kLines[index].EndTime +} + +func (inc *BOLL) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + if inc.EndTime != zeroTime && inc.EndTime.Before(inc.EndTime) { + return + } + + inc.calculateAndUpdate(window) +} + +func (inc *BOLL) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} diff --git a/pkg/indicator/sma.go b/pkg/indicator/sma.go index a74f3ae06..10901a075 100644 --- a/pkg/indicator/sma.go +++ b/pkg/indicator/sma.go @@ -16,8 +16,8 @@ var zeroTime time.Time type SMA struct { types.IntervalWindow - Values Float64Slice - EndTime time.Time + Values Float64Slice + EndTime time.Time } func (inc *SMA) Last() float64 { diff --git a/pkg/strategy/skeleton/strategy.go b/pkg/strategy/skeleton/strategy.go index c9cbf697c..07b3636de 100644 --- a/pkg/strategy/skeleton/strategy.go +++ b/pkg/strategy/skeleton/strategy.go @@ -15,17 +15,17 @@ func init() { type Strategy struct { Symbol string `json:"symbol"` + + types.Market +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"}) } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { - session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"}) session.Stream.OnKLineClosed(func(kline types.KLine) { - market, ok := session.Market(s.Symbol) - if !ok { - return - } - - quoteBalance, ok := session.Account.Balance(market.QuoteCurrency) + quoteBalance, ok := session.Account.Balance(s.Market.QuoteCurrency) if !ok { return } diff --git a/pkg/strategy/swing/strategy.go b/pkg/strategy/swing/strategy.go index b6c4a466f..9c8cf1782 100644 --- a/pkg/strategy/swing/strategy.go +++ b/pkg/strategy/swing/strategy.go @@ -121,7 +121,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se trend := kline.GetTrend() switch trend { - case 1: + case types.TrendUp: // if it goes up and it's above the moving average price, then we sell if closePrice > movingAveragePrice { s.notify(":chart_with_upwards_trend: closePrice %f is above movingAveragePrice %f, submitting SELL order", closePrice, movingAveragePrice) @@ -137,7 +137,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se log.WithError(err).Error("submit order error") } } - case -1: + case types.TrendDown: // if it goes down and it's below the moving average price, then we buy if closePrice < movingAveragePrice { s.notify(":chart_with_downwards_trend: closePrice %f is below movingAveragePrice %f, submitting BUY order", closePrice, movingAveragePrice) diff --git a/pkg/types/kline.go b/pkg/types/kline.go index 6061cd43e..d4c1f4ebb 100644 --- a/pkg/types/kline.go +++ b/pkg/types/kline.go @@ -10,6 +10,12 @@ import ( "github.com/c9s/bbgo/pkg/util" ) +type Trend int + +const TrendUp = 1 +const TrendFlat = 0 +const TrendDown = -1 + type KLineOrWindow interface { GetInterval() string GetTrend() int @@ -87,16 +93,16 @@ func (k KLine) BounceDown() bool { return trend > 0 && k.Open < mid && k.Close < mid } -func (k KLine) GetTrend() int { +func (k KLine) GetTrend() Trend { o := k.GetOpen() c := k.GetClose() if c > o { - return 1 + return TrendUp } else if c < o { - return -1 + return TrendDown } - return 0 + return TrendFlat } func (k KLine) GetHigh() float64 { @@ -442,4 +448,3 @@ func (k KLineWindow) SlackAttachment() slack.Attachment { } type KLineCallback func(kline KLine) -