Skip to content

Commit

Permalink
doc: update indicator documents
Browse files Browse the repository at this point in the history
  • Loading branch information
c9s committed Feb 18, 2024
1 parent 4bd5c62 commit 925590c
Show file tree
Hide file tree
Showing 3 changed files with 261 additions and 90 deletions.
128 changes: 128 additions & 0 deletions doc/development/indicator-v1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
How To Use Builtin Indicators and Create New Indicators
-------------------------------------------------------

**NOTE THAT V1 INDICATOR WILL BE DEPRECATED, USE V2 INDICATOR INSTEAD**

### 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

func (inc *StructName) Update(value float64) {
// indicator calculation here...
// push value...
}

func (inc *StructName) PushK(k types.KLine) {
inc.Update(k.Close.Float64())
}

// custom function
func (inc *StructName) CalculateAndUpdate(kLines []types.KLine) {
if len(inc.Values) == 0 {
// preload or initialization
for _, k := range allKLines {
inc.PushK(k)
}

inc.EmitUpdate(inc.Last())
} else {
// update new value only
k := allKLines[len(allKLines)-1]
inc.PushK(k)
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`.

#### Generalize

In order to provide indicator users a lower learning curve, we've designed the `types.Series` interface. We recommend indicator developers to also implement the `types.Series` interface to provide richer functionality on the computed result. To have deeper understanding how `types.Series` works, please refer to [doc/development/series.md](./series.md)
213 changes: 126 additions & 87 deletions doc/development/indicator.md
Original file line number Diff line number Diff line change
@@ -1,124 +1,163 @@
How To Use Builtin Indicators and Create New Indicators
-------------------------------------------------------
How To Use Builtin Indicators and Add New Indicators (V2)
=========================================================

## Using Built-in Indicators

In bbgo session, we already have several built-in indicators defined.

### 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:
To use the builtin indicators, call `Indicators(symbol)` method to get the indicator set of a symbol,

```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
session.Indicators(symbol string) *IndicatorSet
```

and to get the `*StandardIndicatorSet` from `ExchangeSession`, just need to call:
IndicatorSet is a helper that helps you construct the indicator instance.
Each indicator is a stream that subscribes to
an upstream through the callback.

We will explain how the indicator works from scratch in the following section.

The following code will create a kLines stream that subscribes to specific klines from a websocket stream instance:

```go
indicatorSet, ok := session.StandardIndicatorSet("BTCUSDT") // param: symbol
kLines := indicatorv2.KLines(stream, "BTCUSDT", types.Interval1m)
```
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.
The kLines stream is a special indicator stream that subscribes to the kLine event from the websocket stream. It
registers a callback on `OnKLineClosed`, and when there is a kLine event triggered, it pushes the kLines into its
series, and then it triggers all the subscribers that subscribe to it.

However, what if you want to use the indicators not defined in `StandardIndicatorSet`? For example, the `AD` indicator defined in `pkg/indicators/ad.go`?
To get the closed prices from the kLines stream, simply pass the kLines stream instance to a ClosedPrice indicator:

Here's a simple example in what you should write in your strategy code:
```go
import (
"context"
"fmt"
closePrices := indicatorv2.ClosePrices(kLines)
```

"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/types"
"github.com/c9s/bbgo/pkg/indicator"
)
When the kLine indicator pushes a new kline, the ClosePrices stream receives a kLine object then gets the closed price
from the kLine object.

type Strategy struct {}
to get the latest value of an indicator (closePrices), use Last(n) method, where n starts from 0:

func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
session.Subscribe(types.KLineChannel, s.Symbol. types.SubscribeOptions{Interval: "1m"})
}
```go
lastClosedPrice := closePrices.Last(0)
secondClosedPrice := closePrices.Last(1)
```

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 create a EMA indicator instance, again, simply pass the closePrice indicator to the SMA stream constructor:

```go
ema := indicatorv2.EMA(closePrices, 17)
```

#### To Contribute
If you want to listen to the EMA value events, just add a callback on the indicator instance:

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)
}
ema.OnUpdate(func(v float64) { .... })
```

## Adding New Indicator

Adding a new indicator is pretty straightforward. Simply create a new struct and insert the necessary parameters as
struct fields.

The indicator algorithm will be implemented in the `Calculate(v float64) float64` method.

You can think of it as a simple input-output model: it takes a float64 number as input, calculates the value, and
returns a float64 number as output.

```
And implement required interface methods:
[input float64] -> [Calculate] -> [output float64]
```

Since it is a float64 value indicator, we will use `*types.Float64Series` here to store our values:

```go
package indicatorv2

func (inc *StructName) Update(value float64) {
// indicator calculation here...
// push value...
}
type EWMAStream struct {
// embedded struct to inherit Float64Series methods
*types.Float64Series

func (inc *StructName) PushK(k types.KLine) {
inc.Update(k.Close.Float64())
// parameters we need
window int
multiplier float64
}
```

// custom function
func (inc *StructName) CalculateAndUpdate(kLines []types.KLine) {
if len(inc.Values) == 0 {
// preload or initialization
for _, k := range allKLines {
inc.PushK(k)
}

inc.EmitUpdate(inc.Last())
} else {
// update new value only
k := allKLines[len(allKLines)-1]
inc.PushK(k)
inc.EmitUpdate(calculatedValue) // produce data, broadcast to the subscribers
}
And then, add the constructor of the indicator stream:

```go
// the "source" here is your value source
func EWMA(source types.Float64Source, window int) *EWMAStream {
s := &EWMAStream{
Float64Series: types.NewFloat64Series(),
window: window,
multiplier: 2.0 / float64(1+window),
}
s.Bind(source, s)
return s
}
```

Where the source refers to your upstream value, such as closedPrices, openedPrices, or any type of float64 series. For
example, Volume could also serve as the source.

The Bind method invokes the `Calculate()` method to obtain the updated value from a callback of the upstream source.
Subsequently, it calls EmitUpdate to activate the callbacks of its subscribers,
thereby passing the updated value to all of them.

// custom function
func (inc *StructName) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
// filter on interval
inc.CalculateAndUpdate(window)
Next, write your algorithm within the Calculate method:

```go
func (s *EWMAStream) Calculate(v float64) float64 {
// if you need the last number to calculate the next value
// call s.Slice.Last(0)
//
last := s.Slice.Last(0)
if last == 0.0 {
return v
}

m := s.multiplier
return (1.0-m)*last + m*v
}
```

// required
func (inc *StructName) Bind(updator KLineWindowUpdater) {
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
Sometimes you might need to store the intermediate values inside your indicator, you can add the extra field with type Float64Series like this:

```go
type EWMAStream struct {
// embedded struct to inherit Float64Series methods
*types.Float64Series

A *types.Float64Series
B *types.Float64Series

// parameters we need
window int
multiplier float64
}
```

The `KLineWindowUpdater` interface is currently defined in `pkg/indicator/ewma.go` and may be moved out in the future.
In your `Calculate()` method, you can push the values into these float64 series, for example:

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`.
```go
func (s *EWMAStream) Calculate(v float64) float64 {
// if you need the last number to calculate the next value
// call s.Slice.Last(0)
last := s.Slice.Last(0)
if last == 0.0 {
return v
}

// If you need to trigger callbacks, use PushAndEmit
s.A.Push(last / 2)
s.B.Push(last / 3)

m := s.multiplier
return (1.0-m)*last + m*v
}
```

#### Generalize

In order to provide indicator users a lower learning curve, we've designed the `types.Series` interface. We recommend indicator developers to also implement the `types.Series` interface to provide richer functionality on the computed result. To have deeper understanding how `types.Series` works, please refer to [doc/development/series.md](./series.md)
10 changes: 7 additions & 3 deletions doc/development/series.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
Indicator Interface
Series Interface
-----------------------------------

In bbgo, we've added several interfaces to standardize the indicator protocol.
The new interfaces will allow strategy developers switching similar indicators without checking the code.
Series defines the data structure of the indicator data.

indicators use this series interface to manage these time-series data.

The interface allow strategy developers to switch similar indicators without checking the code.

Signal contributors or indicator developers were also able to be benefit from the existing interface functions, such as `Add`, `Mul`, `Minus`, and `Div`, without rebuilding the wheels.

The series interface in bbgo borrows the concept of `series` type in pinescript that allow us to query data in time-based reverse order (data that created later will be the former object in series). Right now, based on the return type, we have two interfaces been defined in [pkg/types/indicator.go](../../pkg/types/indicator.go):
Expand Down

0 comments on commit 925590c

Please sign in to comment.