Skip to content

Commit

Permalink
Merge pull request c9s#505 from zenixls2/feature/series
Browse files Browse the repository at this point in the history
feature: add pinescript series interface
  • Loading branch information
zenixls2 authored Apr 13, 2022
2 parents 5d0a7f1 + c7c856e commit b57c94f
Show file tree
Hide file tree
Showing 18 changed files with 1,012 additions and 16 deletions.
100 changes: 100 additions & 0 deletions doc/development/indicator.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
How To Use Builtin Indicators and Create New Indicators
-------------------------------------------------------

### Built-in Indicators
In bbgo session, we already have several indicators defined inside.
We could refer to the live-data without the worriedness of handling market data subscription.
To use the builtin ones, we could refer the `StandardIndicatorSet` type:

```go
// defined in pkg/bbgo/session.go
(*StandardIndicatorSet) BOLL(iw types.IntervalWindow, bandwidth float64) *indicator.BOLL
(*StandardIndicatorSet) SMA(iw types.IntervalWindow) *indicator.SMA
(*StandardIndicatorSet) EWMA(iw types.IntervalWindow) *indicator.EWMA
(*StandardIndicatorSet) STOCH(iw types.IntervalWindow) *indicator.STOCH
(*StandardIndicatorSet) VOLATILITY(iw types.IntervalWindow) *indicator.VOLATILITY
```

and to get the `*StandardIndicatorSet` from `ExchangeSession`, just need to call:
```go
indicatorSet, ok := session.StandardIndicatorSet("BTCUSDT") // param: symbol
```
in your strategy's `Run` function.


And in `Subscribe` function in strategy, just subscribe the `KLineChannel` on the interval window of the indicator you want to query, you should be able to acquire the latest number on the indicators.

However, what if you want to use the indicators not defined in `StandardIndicatorSet`? For example, the `AD` indicator defined in `pkg/indicators/ad.go`?

Here's a simple example in what you should write in your strategy code:
```go
import (
"context"
"fmt"

"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/indicator"
)

type Strategy struct {}

func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
session.Subscribe(types.KLineChannel, s.Symbol. types.SubscribeOptions{Interval: "1m"})
}

func (s *Strategy) Run(ctx context.Context, oe bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
// first we need to get market data store(cached market data) from the exchange session
st, ok := session.MarketDataStore(s.Symbol)
if !ok {
...
return err
}
// setup the time frame size
window := types.IntervalWindow{Window: 10, Interval: types.Interval1m}
// construct AD indicator
AD := &indicator.AD{IntervalWindow: window}
// bind indicator to the data store, so that our callback could be triggered
AD.Bind(st)
AD.OnUpdate(func (ad float64) {
fmt.Printf("now we've got ad: %f, total length: %d\n", ad, AD.Length())
})
}
```

#### To Contribute

try to create new indicators in `pkg/indicator/` folder, and add compilation hint of go generator:
```go
// go:generate callbackgen -type StructName
type StructName struct {
...
UpdateCallbacks []func(value float64)
}

```
And implement required interface methods:
```go
// custom function
func (inc *StructName) calculateAndUpdate(kLines []types.KLine) {
// calculation...
// assign the result to calculatedValue
inc.EmitUpdate(calculatedValue) // produce data, broadcast to the subscribers
}

// custom function
func (inc *StructName) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
// filter on interval
inc.calculateAndUpdate(window)
}

// required
func (inc *StructName) Bind(updator KLineWindowUpdater) {
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
}
```

The `KLineWindowUpdater` interface is currently defined in `pkg/indicator/ewma.go` and may be moved out in the future.

Once the implementation is done, run `go generate` to generate the callback functions of the indicator.
You should be able to implement your strategy and use the new indicator in the same way as `AD`.
21 changes: 11 additions & 10 deletions pkg/bbgo/marketdatastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,26 @@ type MarketDataStore struct {
Symbol string

// KLineWindows stores all loaded klines per interval
KLineWindows map[types.Interval]types.KLineWindow `json:"-"`
KLineWindows map[types.Interval]*types.KLineWindow `json:"-"`

kLineWindowUpdateCallbacks []func(interval types.Interval, kline types.KLineWindow)
kLineWindowUpdateCallbacks []func(interval types.Interval, klines types.KLineWindow)
}

func NewMarketDataStore(symbol string) *MarketDataStore {
return &MarketDataStore{
Symbol: symbol,

// KLineWindows stores all loaded klines per interval
KLineWindows: make(map[types.Interval]types.KLineWindow, len(types.SupportedIntervals)), // 12 interval, 1m,5m,15m,30m,1h,2h,4h,6h,12h,1d,3d,1w
KLineWindows: make(map[types.Interval]*types.KLineWindow, len(types.SupportedIntervals)), // 12 interval, 1m,5m,15m,30m,1h,2h,4h,6h,12h,1d,3d,1w
}
}

func (store *MarketDataStore) SetKLineWindows(windows map[types.Interval]types.KLineWindow) {
func (store *MarketDataStore) SetKLineWindows(windows map[types.Interval]*types.KLineWindow) {
store.KLineWindows = windows
}

// KLinesOfInterval returns the kline window of the given interval
func (store *MarketDataStore) KLinesOfInterval(interval types.Interval) (kLines types.KLineWindow, ok bool) {
func (store *MarketDataStore) KLinesOfInterval(interval types.Interval) (kLines *types.KLineWindow, ok bool) {
kLines, ok = store.KLineWindows[interval]
return kLines, ok
}
Expand All @@ -50,14 +50,15 @@ func (store *MarketDataStore) handleKLineClosed(kline types.KLine) {
func (store *MarketDataStore) AddKLine(kline types.KLine) {
window, ok := store.KLineWindows[kline.Interval]
if !ok {
window = make(types.KLineWindow, 0, 1000)
var tmp = make(types.KLineWindow, 0, 1000)
store.KLineWindows[kline.Interval] = &tmp
window = &tmp
}
window.Add(kline)

if len(window) > MaxNumOfKLines {
window = window[MaxNumOfKLinesTruncate-1:]
if len(*window) > MaxNumOfKLines {
*window = (*window)[MaxNumOfKLinesTruncate-1:]
}

store.KLineWindows[kline.Interval] = window
store.EmitKLineWindowUpdate(kline.Interval, window)
store.EmitKLineWindowUpdate(kline.Interval, *window)
}
6 changes: 3 additions & 3 deletions pkg/bbgo/marketdatastore_callbacks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pkg/cmd/backtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
23 changes: 21 additions & 2 deletions pkg/indicator/ad.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,17 @@ type AD struct {
}

func (inc *AD) Update(kLine types.KLine) {
close := kLine.Close.Float64()
cloze := kLine.Close.Float64()
high := kLine.High.Float64()
low := kLine.Low.Float64()
volume := kLine.Volume.Float64()

moneyFlowVolume := ((2*close - high - low) / (high - low)) * volume
var moneyFlowVolume float64
if high == low {
moneyFlowVolume = 0
} else {
moneyFlowVolume = ((2*cloze - high - low) / (high - low)) * volume
}

ad := inc.Last() + moneyFlowVolume
inc.Values.Push(ad)
Expand All @@ -41,6 +46,20 @@ func (inc *AD) Last() float64 {
return inc.Values[len(inc.Values)-1]
}

func (inc *AD) Index(i int) float64 {
length := len(inc.Values)
if length == 0 || length-i-1 < 0 {
return 0
}
return inc.Values[length-i-1]
}

func (inc *AD) Length() int {
return len(inc.Values)
}

var _ types.Series = &AD{}

func (inc *AD) calculateAndUpdate(kLines []types.KLine) {
for _, k := range kLines {
if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) {
Expand Down
18 changes: 18 additions & 0 deletions pkg/indicator/boll.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ type BOLL struct {
updateCallbacks []func(sma, upBand, downBand float64)
}

type BandType int

func (inc *BOLL) GetUpBand() types.Series {
return &inc.UpBand
}

func (inc *BOLL) GetDownBand() types.Series {
return &inc.DownBand
}

func (inc *BOLL) GetSMA() types.Series {
return &inc.SMA
}

func (inc *BOLL) GetStdDev() types.Series {
return &inc.StdDev
}

func (inc *BOLL) LastUpBand() float64 {
if len(inc.UpBand) == 0 {
return 0.0
Expand Down
14 changes: 14 additions & 0 deletions pkg/indicator/ewma.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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{}
76 changes: 76 additions & 0 deletions pkg/indicator/line.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package indicator

import (
"time"

"github.com/c9s/bbgo/pkg/types"
)

// Line indicator is a utility that helps to simulate either the
// 1. trend
// 2. support
// 3. resistance
// of the market data, defined with series interface
type Line struct {
types.IntervalWindow
start float64
end float64
startIndex int
endIndex int
currentTime time.Time
Interval types.Interval
}

func (l *Line) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
if interval != l.Interval {
return
}
newTime := window.Last().EndTime.Time()
delta := int(newTime.Sub(l.currentTime).Minutes()) / l.Interval.Minutes()
l.startIndex += delta
l.endIndex += delta
l.currentTime = newTime
}

func (l *Line) Bind(updater KLineWindowUpdater) {
updater.OnKLineWindowUpdate(l.handleKLineWindowUpdate)
}

func (l *Line) Last() float64 {
return (l.end-l.start) / float64(l.startIndex - l.endIndex) * float64(l.endIndex) + l.end
}

func (l *Line) Index(i int) float64 {
return (l.end-l.start) / float64(l.startIndex - l.endIndex) * float64(l.endIndex - i) + l.end
}

func (l *Line) Length() int {
if l.startIndex > l.endIndex {
return l.startIndex - l.endIndex
} else {
return l.endIndex - l.startIndex
}
}

func (l *Line) SetXY1(index int, value float64) {
l.startIndex = index
l.start = value
}

func (l *Line) SetXY2(index int, value float64) {
l.endIndex = index
l.end = value
}

func NewLine(startIndex int, startValue float64, endIndex int, endValue float64, interval types.Interval) *Line {
return &Line{
start: startValue,
end: endValue,
startIndex: startIndex,
endIndex: endIndex,
currentTime: time.Time{},
Interval: interval,
}
}

var _ types.Series = &Line{}
31 changes: 31 additions & 0 deletions pkg/indicator/macd.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,34 @@ func (inc *MACD) handleKLineWindowUpdate(interval types.Interval, window types.K
func (inc *MACD) Bind(updater KLineWindowUpdater) {
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
}

type MACDValues struct {
*MACD
}

func (inc *MACDValues) Last() float64 {
if len(inc.Values) == 0 {
return 0.0
}
return inc.Values[len(inc.Values)-1]
}

func (inc *MACDValues) Index(i int) float64 {
length := len(inc.Values)
if length == 0 || length-1-i < 0 {
return 0.0
}
return inc.Values[length-1+i]
}

func (inc *MACDValues) Length() int {
return len(inc.Values)
}

func (inc *MACD) MACD() types.Series {
return &MACDValues{inc}
}

func (inc *MACD) Singals() types.Series {
return &inc.SignalLine
}
Loading

0 comments on commit b57c94f

Please sign in to comment.