bbgo_origin/doc/development/indicator.md

5.7 KiB

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.

We could refer to the live-data without the worriedness of handling market data subscription. To use the builtin indicators, call Indicators(symbol) method to get the indicator set of a symbol,

session.Indicators(symbol string) *IndicatorSet

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:

kLines := indicatorv2.KLines(stream, "BTCUSDT", types.Interval1m)

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.

To get the closed prices from the kLines stream, simply pass the kLines stream instance to a ClosedPrice indicator:

closePrices := indicatorv2.ClosePrices(kLines)

When the kLine indicator pushes a new kline, the ClosePrices stream receives a kLine object then gets the closed price from the kLine object.

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

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:

ema := indicatorv2.EMA(closePrices, 17)

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

ema.OnUpdate(func(v float64) { .... })

To combine these techniques together:


// use dot import to use the constructors as helpers
import . "github.com/c9s/bbgo/pkg/indicator/v2"

func main() {
    // you should get the correct stream instance from the *bbgo.ExchangeSession instance
    stream := &types.Stream{}
    
    ema := EMA(
        ClosePrices(
            KLines(stream, types.Interval1m)), 14)
    _ = ema
}

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:

type Float64Series struct {
	SeriesBase
	Float64Updater
	Slice floats.Slice
}

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:

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:

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:

// 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.

Next, write your algorithm within the Calculate method:

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
}

Sometimes you might need to store the intermediate values inside your indicator, you can add the extra field with type Float64Series like this:

type EWMAStream struct {
    // embedded struct to inherit Float64Series methods
	*types.Float64Series
	
	A *types.Float64Series
	B *types.Float64Series

    // parameters we need
	window     int
	multiplier float64
}

In your Calculate() method, you can push the values into these float64 series, for example:

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
}