diff --git a/doc/development/indicator-v1.md b/doc/development/indicator-v1.md new file mode 100644 index 000000000..beb10fdf4 --- /dev/null +++ b/doc/development/indicator-v1.md @@ -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) diff --git a/doc/development/indicator.md b/doc/development/indicator.md index d3469943e..08cb1430c 100644 --- a/doc/development/indicator.md +++ b/doc/development/indicator.md @@ -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 + + A *types.Float64Series + B *types.Float64Series -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) + // 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) diff --git a/doc/development/series.md b/doc/development/series.md index ec9597377..08c8db1b3 100644 --- a/doc/development/series.md +++ b/doc/development/series.md @@ -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):