From fac61f27dc51ba778f8c5ab22a0b51ec90cb4f0e Mon Sep 17 00:00:00 2001 From: zenix Date: Thu, 31 Mar 2022 19:03:26 +0900 Subject: [PATCH] feature: add pinescript series interface --- pkg/cmd/backtest.go | 2 +- pkg/indicator/boll.go | 97 +++++++++++++++++++++++++++++++++++++++ pkg/indicator/ewma.go | 14 ++++++ pkg/indicator/line.go | 42 +++++++++++++++++ pkg/types/indicator.go | 101 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 pkg/indicator/line.go diff --git a/pkg/cmd/backtest.go b/pkg/cmd/backtest.go index 382857292..5661466e6 100644 --- a/pkg/cmd/backtest.go +++ b/pkg/cmd/backtest.go @@ -365,7 +365,7 @@ var BacktestCmd = &cobra.Command{ startPrice, ok := session.StartPrice(symbol) if !ok { - return fmt.Errorf("start price not found: %s, %s", symbol, exchangeName) + return fmt.Errorf("start price not found: %s, %s. Run --sync first", symbol, exchangeName) } lastPrice, ok := session.LastPrice(symbol) diff --git a/pkg/indicator/boll.go b/pkg/indicator/boll.go index dd68abb2e..31e7e6a1f 100644 --- a/pkg/indicator/boll.go +++ b/pkg/indicator/boll.go @@ -39,6 +39,39 @@ type BOLL struct { updateCallbacks []func(sma, upBand, downBand float64) } +type BandType int + +const ( + _SMA BandType = iota + _StdDev + _UpBand + _DownBand +) + +func (inc *BOLL) GetUpBand() types.Series { + return &BollSeries{ + inc, _UpBand, + } +} + +func (inc *BOLL) GetDownBand() types.Series { + return &BollSeries{ + inc, _DownBand, + } +} + +func (inc *BOLL) GetSMA() types.Series { + return &BollSeries{ + inc, _SMA, + } +} + +func (inc *BOLL) GetStdDev() types.Series { + return &BollSeries{ + inc, _StdDev, + } +} + func (inc *BOLL) LastUpBand() float64 { if len(inc.UpBand) == 0 { return 0.0 @@ -130,3 +163,67 @@ func (inc *BOLL) handleKLineWindowUpdate(interval types.Interval, window types.K func (inc *BOLL) Bind(updater KLineWindowUpdater) { updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) } + +type BollSeries struct { + *BOLL + bandType BandType +} + +func (b *BollSeries) Last() float64 { + switch b.bandType { + case _SMA: + return b.LastSMA() + case _StdDev: + return b.LastStdDev() + case _UpBand: + return b.LastUpBand() + case _DownBand: + return b.LastDownBand() + default: + panic("bandType wrong") + } +} + +func (b *BollSeries) Index(i int) float64 { + switch b.bandType { + case _SMA: + if len(b.SMA) <= i { + return 0 + } + return b.SMA[len(b.SMA)-i-1] + case _StdDev: + if len(b.StdDev) <= i { + return 0 + } + return b.StdDev[len(b.StdDev)-i-1] + case _UpBand: + if len(b.UpBand) <= i { + return 0 + } + return b.UpBand[len(b.UpBand)-i-1] + case _DownBand: + if len(b.DownBand) <= i { + return 0 + } + return b.DownBand[len(b.DownBand)-i-1] + default: + panic("bandType wrong") + } +} + +func (b *BollSeries) Length() int { + switch b.bandType { + case _SMA: + return len(b.SMA) + case _StdDev: + return len(b.StdDev) + case _UpBand: + return len(b.UpBand) + case _DownBand: + return len(b.DownBand) + default: + panic("bandType wrong") + } +} + +var _ types.Series = &BollSeries{} diff --git a/pkg/indicator/ewma.go b/pkg/indicator/ewma.go index bd100fb89..cf5023451 100644 --- a/pkg/indicator/ewma.go +++ b/pkg/indicator/ewma.go @@ -44,6 +44,18 @@ func (inc *EWMA) Last() float64 { return inc.Values[len(inc.Values)-1] } +func (inc *EWMA) Index(i int) float64 { + if i >= len(inc.Values) { + return 0 + } + + return inc.Values[len(inc.Values)-1-i] +} + +func (inc *EWMA) Length() int { + return len(inc.Values) +} + func (inc *EWMA) calculateAndUpdate(allKLines []types.KLine) { if len(allKLines) < inc.Window { // we can't calculate @@ -149,3 +161,5 @@ func (inc *EWMA) handleKLineWindowUpdate(interval types.Interval, window types.K func (inc *EWMA) Bind(updater KLineWindowUpdater) { updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) } + +var _ types.Series = &EWMA{} diff --git a/pkg/indicator/line.go b/pkg/indicator/line.go new file mode 100644 index 000000000..8518a499e --- /dev/null +++ b/pkg/indicator/line.go @@ -0,0 +1,42 @@ +package indicator + +import ( + "time" + + "github.com/c9s/bbgo/pkg/types" +) + +type Line struct { + types.IntervalWindow + start float64 + end float64 + startTime time.Time + endTime time.Time + currentTime time.Time + Interval types.Interval +} + +func (l *Line) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if interval != l.Interval { + return + } + l.currentTime = window.Last().EndTime.Time() +} + +func (l *Line) Bind(updater KLineWindowUpdater) { + updater.OnKLineWindowUpdate(l.handleKLineWindowUpdate) +} + +func (l *Line) Last() float64 { + return l.currentTime.Sub(l.endTime).Minutes()*(l.end-l.start)/l.endTime.Sub(l.startTime).Minutes() + l.end +} + +func (l *Line) Index(i int) float64 { + return (l.currentTime.Sub(l.endTime).Minutes()-float64(i*l.Interval.Minutes()))*(l.end-l.start)/l.endTime.Sub(l.startTime).Minutes() + l.end +} + +func (l *Line) Length() int { + return int(l.startTime.Sub(l.currentTime).Minutes()) / l.Interval.Minutes() +} + +var _ types.Series = &Line{} diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index dd37c228d..f4e296b81 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -4,3 +4,104 @@ package types type Float64Indicator interface { Last() float64 } + +// The interface maps to pinescript basic type `series` +// Access the internal historical data from the latest to the oldest +// Index(0) always maps to Last() +type Series interface { + Last() float64 + Index(int) float64 + Length() int +} + +// The interface maps to pinescript basic type `series` for bool type +// Access the internal historical data from the latest to the oldest +// Index(0) always maps to Last() +type BoolSeries interface { + Last() bool + Index(int) bool + Length() int +} + +// The result structure that maps to the crossing result of `CrossOver` and `CrossUnder` +// Accessible through BoolSeries interface +type CrossResult struct { + a Series + b Series + isOver bool +} + +func (c *CrossResult) Last() bool { + if c.Length() == 0 { + return false + } + if c.isOver { + return c.a.Last()-c.b.Last() > 0 && c.a.Index(1)-c.b.Index(1) < 0 + } else { + return c.a.Last()-c.b.Last() < 0 && c.a.Index(1)-c.b.Index(1) > 0 + } +} + +func (c *CrossResult) Index(i int) bool { + if i >= c.Length() { + return false + } + if c.isOver { + return c.a.Index(i)-c.b.Index(i) > 0 && c.a.Index(i-1)-c.b.Index(i-1) < 0 + } else { + return c.a.Index(i)-c.b.Index(i) < 0 && c.a.Index(i-1)-c.b.Index(i-1) > 0 + } +} + +func (c *CrossResult) Length() int { + la := c.a.Length() + lb := c.b.Length() + if la > lb { + return lb + } + return la +} + +// a series cross above b series. +// If in current KLine, a is higher than b, and in previous KLine, a is lower than b, then return true. +// Otherwise return false. +// If accessing index <= length, will always return false +func CrossOver(a Series, b Series) BoolSeries { + return &CrossResult{a, b, true} +} + +// a series cross under b series. +// If in current KLine, a is lower than b, and in previous KLine, a is higher than b, then return true. +// Otherwise return false. +// If accessing index <= length, will always return false +func CrossUnder(a Series, b Series) BoolSeries { + return &CrossResult{a, b, false} +} + +func Highest(a Series, lookback int) float64 { + if lookback > a.Length() { + lookback = a.Length() + } + highest := a.Last() + for i := 1; i < lookback; i++ { + current := a.Index(i) + if highest < current { + highest = current + } + } + return highest +} + +func Lowest(a Series, lookback int) float64 { + if lookback > a.Length() { + lookback = a.Length() + } + lowest := a.Last() + for i := 1; i < lookback; i++ { + current := a.Index(i) + if lowest > current { + lowest = current + } + } + return lowest +}