Merge pull request #1533 from c9s/doc/indicator-v2

DOC: update indicator documents for v2
This commit is contained in:
c9s 2024-02-18 21:30:49 +08:00 committed by GitHub
commit 97cdf42643
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 293 additions and 94 deletions

View File

@ -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)

View File

@ -1,124 +1,191 @@
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.
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.
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.
To get the closed prices from the kLines stream, simply pass the kLines stream instance to a ClosedPrice indicator:
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"
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)
```
To create an EMA indicator instance, again, simply pass the closePrice indicator to the SMA stream constructor:
```go
ema := indicatorv2.EMA(closePrices, 17)
```
If you want to listen to the EMA value events, just add a callback on the indicator instance:
```go
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.
```
[input float64] -> [Calculate] -> [output float64]
```
Since it will be a float64 value indicator, we will use `*types.Float64Series` here to store our values,
`types.Float64Series` is a struct that contains a slice to store the float64 values, it is implemented as follows:
```go
type Float64Series struct {
SeriesBase
Float64Updater
Slice floats.Slice
}
```
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
The `Slice` field is a []float64 slice,
which provides some helper methods that helps you do some calculation on the float64 slice.
And Float64Updater provides a way to let indicators subscribe to the updated value:
```go
u := Float64Updater{}
u.OnUpdate(func(v float64) { ... })
u.OnUpdate(func(v float64) { ... })
// to emit the callbacks
u.EmitUpdate(10.0)
```
Now you have a basic concept about the Float64Series,
we can now start to implement our first indicator structure:
```go
package indicatorv2
type EWMAStream struct {
// embedded struct to inherit Float64Series methods
*types.Float64Series
// parameters we need
window int
multiplier float64
}
```
Again, since it is a float64 value indicator, we use `*types.Float64Series` here to store our values.
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),
}
// 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())
})
s.Bind(source, s)
return s
}
```
#### To Contribute
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.
Next, write your algorithm within the Calculate method:
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)
}
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
}
```
And implement required interface methods:
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
func (inc *StructName) Update(value float64) {
// indicator calculation here...
// push value...
}
A *types.Float64Series
B *types.Float64Series
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)
// 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)

View File

@ -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):