mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-25 16:25:16 +00:00
Merge pull request #505 from zenixls2/feature/series
feature: add pinescript series interface
This commit is contained in:
commit
b57c94fe12
100
doc/development/indicator.md
Normal file
100
doc/development/indicator.md
Normal file
|
@ -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`.
|
|
@ -11,9 +11,9 @@ 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 {
|
||||
|
@ -21,16 +21,16 @@ func NewMarketDataStore(symbol string) *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
|
||||
}
|
||||
|
@ -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,12 +6,12 @@ import (
|
|||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
func (store *MarketDataStore) OnKLineWindowUpdate(cb func(interval types.Interval, kline types.KLineWindow)) {
|
||||
func (store *MarketDataStore) OnKLineWindowUpdate(cb func(interval types.Interval, klines types.KLineWindow)) {
|
||||
store.kLineWindowUpdateCallbacks = append(store.kLineWindowUpdateCallbacks, cb)
|
||||
}
|
||||
|
||||
func (store *MarketDataStore) EmitKLineWindowUpdate(interval types.Interval, kline types.KLineWindow) {
|
||||
func (store *MarketDataStore) EmitKLineWindowUpdate(interval types.Interval, klines types.KLineWindow) {
|
||||
for _, cb := range store.kLineWindowUpdateCallbacks {
|
||||
cb(interval, kline)
|
||||
cb(interval, klines)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
@ -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
pkg/indicator/line.go
Normal file
76
pkg/indicator/line.go
Normal file
|
@ -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{}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -63,6 +63,20 @@ func (inc *RSI) Last() float64 {
|
|||
return inc.Values[len(inc.Values)-1]
|
||||
}
|
||||
|
||||
func (inc *RSI) Index(i int) float64 {
|
||||
length := len(inc.Values)
|
||||
if length <= 0 || length-i-1 < 0 {
|
||||
return 0.0
|
||||
}
|
||||
return inc.Values[length-i-1]
|
||||
}
|
||||
|
||||
func (inc *RSI) Length() int {
|
||||
return len(inc.Values)
|
||||
}
|
||||
|
||||
var _ types.Series = &RSI{}
|
||||
|
||||
func (inc *RSI) calculateAndUpdate(kLines []types.KLine) {
|
||||
var priceF = KLineClosePriceMapper
|
||||
|
||||
|
|
|
@ -30,6 +30,31 @@ func (inc *SMA) Last() float64 {
|
|||
return inc.Values[len(inc.Values)-1]
|
||||
}
|
||||
|
||||
func (inc *SMA) Index(i int) float64 {
|
||||
length := len(inc.Values)
|
||||
if length == 0 || length-i-1 < 0 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
return inc.Values[length-i-1]
|
||||
}
|
||||
|
||||
func (inc *SMA) Length() int {
|
||||
return len(inc.Values)
|
||||
}
|
||||
|
||||
var _ types.Series = &SMA{}
|
||||
|
||||
func (inc *SMA) Update(value float64) {
|
||||
length := len(inc.Values)
|
||||
if length == 0 {
|
||||
inc.Values = append(inc.Values, value)
|
||||
return
|
||||
}
|
||||
newVal := (inc.Values[length-1]*float64(inc.Window-1) + value) / float64(inc.Window)
|
||||
inc.Values = append(inc.Values, newVal)
|
||||
}
|
||||
|
||||
func (inc *SMA) calculateAndUpdate(kLines []types.KLine) {
|
||||
if len(kLines) < inc.Window {
|
||||
return
|
||||
|
|
|
@ -82,3 +82,11 @@ func (inc *STOCH) handleKLineWindowUpdate(interval types.Interval, window types.
|
|||
func (inc *STOCH) Bind(updater KLineWindowUpdater) {
|
||||
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
|
||||
}
|
||||
|
||||
func (inc *STOCH) GetD() types.Series {
|
||||
return &inc.D
|
||||
}
|
||||
|
||||
func (inc *STOCH) GetK() types.Series {
|
||||
return &inc.K
|
||||
}
|
||||
|
|
|
@ -35,6 +35,21 @@ func (inc *VWAP) Last() float64 {
|
|||
return inc.Values[len(inc.Values)-1]
|
||||
}
|
||||
|
||||
func (inc *VWAP) 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 *VWAP) Length() int {
|
||||
return len(inc.Values)
|
||||
}
|
||||
|
||||
var _ types.Series = &VWAP{}
|
||||
|
||||
func (inc *VWAP) Update(kLine types.KLine, priceF KLinePriceMapper) {
|
||||
price := priceF(kLine)
|
||||
volume := kLine.Volume.Float64()
|
||||
|
|
|
@ -34,6 +34,20 @@ func (inc *VWMA) Last() float64 {
|
|||
return inc.Values[len(inc.Values)-1]
|
||||
}
|
||||
|
||||
func (inc *VWMA) 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 *VWMA) Length() int {
|
||||
return len(inc.Values)
|
||||
}
|
||||
|
||||
var _ types.Series = &VWMA{}
|
||||
|
||||
func KLinePriceVolumeMapper(k types.KLine) float64 {
|
||||
return k.Close.Mul(k.Volume).Float64()
|
||||
}
|
||||
|
|
|
@ -116,3 +116,29 @@ func (s Float64Slice) Dot(other Float64Slice) float64 {
|
|||
func (s Float64Slice) Normalize() Float64Slice {
|
||||
return s.DivScalar(s.Sum())
|
||||
}
|
||||
|
||||
func (a *Float64Slice) Last() float64 {
|
||||
length := len(*a)
|
||||
if length > 0 {
|
||||
return (*a)[length-1]
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
func (a *Float64Slice) Index(i int) float64 {
|
||||
length := len(*a)
|
||||
if length-i < 0 || i < 0 {
|
||||
return 0.0
|
||||
}
|
||||
return (*a)[length-i-1]
|
||||
}
|
||||
|
||||
func (a *Float64Slice) Length() int {
|
||||
return len(*a)
|
||||
}
|
||||
|
||||
func (a Float64Slice) Addr() *Float64Slice {
|
||||
return &a
|
||||
}
|
||||
|
||||
var _ Series = Float64Slice([]float64{}).Addr()
|
||||
|
|
|
@ -1,6 +1,512 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"reflect"
|
||||
|
||||
"gonum.org/v1/gonum/stat"
|
||||
)
|
||||
|
||||
// Float64Indicator is the indicators (SMA and EWMA) that we want to use are returning float64 data.
|
||||
type Float64Indicator interface {
|
||||
Last() float64
|
||||
}
|
||||
|
||||
// The interface maps to pinescript basic type `series`
|
||||
// Access the internal historical data from the latest to the oldest
|
||||
// Index(0) always maps to Last()
|
||||
type Series interface {
|
||||
Last() float64
|
||||
Index(int) float64
|
||||
Length() int
|
||||
}
|
||||
|
||||
// The interface maps to pinescript basic type `series` for bool type
|
||||
// Access the internal historical data from the latest to the oldest
|
||||
// Index(0) always maps to Last()
|
||||
type BoolSeries interface {
|
||||
Last() bool
|
||||
Index(int) bool
|
||||
Length() int
|
||||
}
|
||||
|
||||
// Calculate sum of the series
|
||||
// if limit is given, will only sum first limit numbers (a.Index[0..limit])
|
||||
// otherwise will sum all elements
|
||||
func Sum(a Series, limit ...int) (sum float64) {
|
||||
l := -1
|
||||
if len(limit) > 0 {
|
||||
l = limit[0]
|
||||
}
|
||||
if l < a.Length() {
|
||||
l = a.Length()
|
||||
}
|
||||
for i := 0; i < l; i++ {
|
||||
sum += a.Index(i)
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
// Calculate the average value of the series
|
||||
// if limit is given, will only calculate the average of first limit numbers (a.Index[0..limit])
|
||||
// otherwise will operate on all elements
|
||||
func Mean(a Series, limit ...int) (mean float64) {
|
||||
l := -1
|
||||
if len(limit) > 0 {
|
||||
l = limit[0]
|
||||
}
|
||||
if l < a.Length() {
|
||||
l = a.Length()
|
||||
}
|
||||
return Sum(a, l) / float64(l)
|
||||
}
|
||||
|
||||
type AbsResult struct {
|
||||
a Series
|
||||
}
|
||||
|
||||
func (a *AbsResult) Last() float64 {
|
||||
return math.Abs(a.a.Last())
|
||||
}
|
||||
|
||||
func (a *AbsResult) Index(i int) float64 {
|
||||
return math.Abs(a.a.Index(i))
|
||||
}
|
||||
|
||||
func (a *AbsResult) Length() int {
|
||||
return a.a.Length()
|
||||
}
|
||||
|
||||
// Return series that having all the elements positive
|
||||
func Abs(a Series) Series {
|
||||
return &AbsResult{a}
|
||||
}
|
||||
|
||||
var _ Series = &AbsResult{}
|
||||
|
||||
func Predict(a Series, lookback int, offset ...int) float64 {
|
||||
if a.Length() < lookback {
|
||||
lookback = a.Length()
|
||||
}
|
||||
x := make([]float64, lookback, lookback)
|
||||
y := make([]float64, lookback, lookback)
|
||||
var weights []float64
|
||||
for i := 0; i < lookback; i++ {
|
||||
x[i] = float64(i)
|
||||
y[i] = a.Index(i)
|
||||
}
|
||||
alpha, beta := stat.LinearRegression(x, y, weights, false)
|
||||
o := -1.0
|
||||
if len(offset) > 0 {
|
||||
o = -float64(offset[0])
|
||||
}
|
||||
return alpha + beta*o
|
||||
}
|
||||
|
||||
// This will make prediction using Linear Regression to get the next cross point
|
||||
// Return (offset from latest, crossed value, could cross)
|
||||
// offset from latest should always be positive
|
||||
// lookback param is to use at most `lookback` points to determine linear regression functions
|
||||
//
|
||||
// You may also refer to excel's FORECAST function
|
||||
func NextCross(a Series, b Series, lookback int) (int, float64, bool) {
|
||||
if a.Length() < lookback {
|
||||
lookback = a.Length()
|
||||
}
|
||||
if b.Length() < lookback {
|
||||
lookback = b.Length()
|
||||
}
|
||||
x := make([]float64, lookback, lookback)
|
||||
y1 := make([]float64, lookback, lookback)
|
||||
y2 := make([]float64, lookback, lookback)
|
||||
var weights []float64
|
||||
for i := 0; i < lookback; i++ {
|
||||
x[i] = float64(i)
|
||||
y1[i] = a.Index(i)
|
||||
y2[i] = b.Index(i)
|
||||
}
|
||||
alpha1, beta1 := stat.LinearRegression(x, y1, weights, false)
|
||||
alpha2, beta2 := stat.LinearRegression(x, y2, weights, false)
|
||||
if beta2 == beta1 {
|
||||
return 0, 0, false
|
||||
}
|
||||
indexf := (alpha1 - alpha2) / (beta2 - beta1)
|
||||
|
||||
// crossed in different direction
|
||||
if indexf >= 0 {
|
||||
return 0, 0, false
|
||||
}
|
||||
return int(math.Ceil(-indexf)), alpha1 + beta1*indexf, true
|
||||
}
|
||||
|
||||
// The result structure that maps to the crossing result of `CrossOver` and `CrossUnder`
|
||||
// Accessible through BoolSeries interface
|
||||
type CrossResult struct {
|
||||
a Series
|
||||
b Series
|
||||
isOver bool
|
||||
}
|
||||
|
||||
func (c *CrossResult) Last() bool {
|
||||
if c.Length() == 0 {
|
||||
return false
|
||||
}
|
||||
if c.isOver {
|
||||
return c.a.Last()-c.b.Last() > 0 && c.a.Index(1)-c.b.Index(1) < 0
|
||||
} else {
|
||||
return c.a.Last()-c.b.Last() < 0 && c.a.Index(1)-c.b.Index(1) > 0
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CrossResult) Index(i int) bool {
|
||||
if i >= c.Length() {
|
||||
return false
|
||||
}
|
||||
if c.isOver {
|
||||
return c.a.Index(i)-c.b.Index(i) > 0 && c.a.Index(i+1)-c.b.Index(i+1) < 0
|
||||
} else {
|
||||
return c.a.Index(i)-c.b.Index(i) < 0 && c.a.Index(i+1)-c.b.Index(i+1) > 0
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CrossResult) Length() int {
|
||||
la := c.a.Length()
|
||||
lb := c.b.Length()
|
||||
if la > lb {
|
||||
return lb
|
||||
}
|
||||
return la
|
||||
}
|
||||
|
||||
// a series cross above b series.
|
||||
// If in current KLine, a is higher than b, and in previous KLine, a is lower than b, then return true.
|
||||
// Otherwise return false.
|
||||
// If accessing index <= length, will always return false
|
||||
func CrossOver(a Series, b Series) BoolSeries {
|
||||
return &CrossResult{a, b, true}
|
||||
}
|
||||
|
||||
// a series cross under b series.
|
||||
// If in current KLine, a is lower than b, and in previous KLine, a is higher than b, then return true.
|
||||
// Otherwise return false.
|
||||
// If accessing index <= length, will always return false
|
||||
func CrossUnder(a Series, b Series) BoolSeries {
|
||||
return &CrossResult{a, b, false}
|
||||
}
|
||||
|
||||
func Highest(a Series, lookback int) float64 {
|
||||
if lookback > a.Length() {
|
||||
lookback = a.Length()
|
||||
}
|
||||
highest := a.Last()
|
||||
for i := 1; i < lookback; i++ {
|
||||
current := a.Index(i)
|
||||
if highest < current {
|
||||
highest = current
|
||||
}
|
||||
}
|
||||
return highest
|
||||
}
|
||||
|
||||
func Lowest(a Series, lookback int) float64 {
|
||||
if lookback > a.Length() {
|
||||
lookback = a.Length()
|
||||
}
|
||||
lowest := a.Last()
|
||||
for i := 1; i < lookback; i++ {
|
||||
current := a.Index(i)
|
||||
if lowest > current {
|
||||
lowest = current
|
||||
}
|
||||
}
|
||||
return lowest
|
||||
}
|
||||
|
||||
type NumberSeries float64
|
||||
|
||||
func (a NumberSeries) Last() float64 {
|
||||
return float64(a)
|
||||
}
|
||||
|
||||
func (a NumberSeries) Index(_ int) float64 {
|
||||
return float64(a)
|
||||
}
|
||||
|
||||
func (a NumberSeries) Length() int {
|
||||
return math.MaxInt32
|
||||
}
|
||||
|
||||
var _ Series = NumberSeries(0)
|
||||
|
||||
type AddSeriesResult struct {
|
||||
a Series
|
||||
b Series
|
||||
}
|
||||
|
||||
// Add two series, result[i] = a[i] + b[i]
|
||||
func Add(a interface{}, b interface{}) Series {
|
||||
var aa Series
|
||||
var bb Series
|
||||
|
||||
switch a.(type) {
|
||||
case float64:
|
||||
aa = NumberSeries(a.(float64))
|
||||
case Series:
|
||||
aa = a.(Series)
|
||||
default:
|
||||
panic("input should be either *Series or float64")
|
||||
|
||||
}
|
||||
switch b.(type) {
|
||||
case float64:
|
||||
bb = NumberSeries(b.(float64))
|
||||
case Series:
|
||||
bb = b.(Series)
|
||||
default:
|
||||
panic("input should be either *Series or float64")
|
||||
|
||||
}
|
||||
return &AddSeriesResult{aa, bb}
|
||||
}
|
||||
|
||||
func (a *AddSeriesResult) Last() float64 {
|
||||
return a.a.Last() + a.b.Last()
|
||||
}
|
||||
|
||||
func (a *AddSeriesResult) Index(i int) float64 {
|
||||
return a.a.Index(i) + a.b.Index(i)
|
||||
}
|
||||
|
||||
func (a *AddSeriesResult) Length() int {
|
||||
lengtha := a.a.Length()
|
||||
lengthb := a.b.Length()
|
||||
if lengtha < lengthb {
|
||||
return lengtha
|
||||
}
|
||||
return lengthb
|
||||
}
|
||||
|
||||
var _ Series = &AddSeriesResult{}
|
||||
|
||||
type MinusSeriesResult struct {
|
||||
a Series
|
||||
b Series
|
||||
}
|
||||
|
||||
// Minus two series, result[i] = a[i] - b[i]
|
||||
func Minus(a interface{}, b interface{}) Series {
|
||||
aa := switchIface(a)
|
||||
bb := switchIface(b)
|
||||
return &MinusSeriesResult{aa, bb}
|
||||
}
|
||||
|
||||
func (a *MinusSeriesResult) Last() float64 {
|
||||
return a.a.Last() - a.b.Last()
|
||||
}
|
||||
|
||||
func (a *MinusSeriesResult) Index(i int) float64 {
|
||||
return a.a.Index(i) - a.b.Index(i)
|
||||
}
|
||||
|
||||
func (a *MinusSeriesResult) Length() int {
|
||||
lengtha := a.a.Length()
|
||||
lengthb := a.b.Length()
|
||||
if lengtha < lengthb {
|
||||
return lengtha
|
||||
}
|
||||
return lengthb
|
||||
}
|
||||
|
||||
var _ Series = &MinusSeriesResult{}
|
||||
|
||||
func switchIface(b interface{}) Series {
|
||||
switch b.(type) {
|
||||
case float64:
|
||||
return NumberSeries(b.(float64))
|
||||
case int32:
|
||||
return NumberSeries(float64(b.(int32)))
|
||||
case int64:
|
||||
return NumberSeries(float64(b.(int64)))
|
||||
case float32:
|
||||
return NumberSeries(float64(b.(float32)))
|
||||
case int:
|
||||
return NumberSeries(float64(b.(int)))
|
||||
case Series:
|
||||
return b.(Series)
|
||||
default:
|
||||
fmt.Println(reflect.TypeOf(b))
|
||||
panic("input should be either *Series or float64")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Divid two series, result[i] = a[i] / b[i]
|
||||
func Div(a interface{}, b interface{}) Series {
|
||||
aa := switchIface(a)
|
||||
if 0 == b {
|
||||
panic("Divid by zero exception")
|
||||
}
|
||||
bb := switchIface(b)
|
||||
return &DivSeriesResult{aa, bb}
|
||||
|
||||
}
|
||||
|
||||
type DivSeriesResult struct {
|
||||
a Series
|
||||
b Series
|
||||
}
|
||||
|
||||
func (a *DivSeriesResult) Last() float64 {
|
||||
return a.a.Last() / a.b.Last()
|
||||
}
|
||||
|
||||
func (a *DivSeriesResult) Index(i int) float64 {
|
||||
return a.a.Index(i) / a.b.Index(i)
|
||||
}
|
||||
|
||||
func (a *DivSeriesResult) Length() int {
|
||||
lengtha := a.a.Length()
|
||||
lengthb := a.b.Length()
|
||||
if lengtha < lengthb {
|
||||
return lengtha
|
||||
}
|
||||
return lengthb
|
||||
}
|
||||
|
||||
var _ Series = &DivSeriesResult{}
|
||||
|
||||
// Multiple two series, result[i] = a[i] * b[i]
|
||||
func Mul(a interface{}, b interface{}) Series {
|
||||
var aa Series
|
||||
var bb Series
|
||||
|
||||
switch a.(type) {
|
||||
case float64:
|
||||
aa = NumberSeries(a.(float64))
|
||||
case Series:
|
||||
aa = a.(Series)
|
||||
default:
|
||||
panic("input should be either Series or float64")
|
||||
}
|
||||
switch b.(type) {
|
||||
case float64:
|
||||
bb = NumberSeries(b.(float64))
|
||||
case Series:
|
||||
bb = b.(Series)
|
||||
default:
|
||||
panic("input should be either Series or float64")
|
||||
|
||||
}
|
||||
return &MulSeriesResult{aa, bb}
|
||||
|
||||
}
|
||||
|
||||
type MulSeriesResult struct {
|
||||
a Series
|
||||
b Series
|
||||
}
|
||||
|
||||
func (a *MulSeriesResult) Last() float64 {
|
||||
return a.a.Last() * a.b.Last()
|
||||
}
|
||||
|
||||
func (a *MulSeriesResult) Index(i int) float64 {
|
||||
return a.a.Index(i) * a.b.Index(i)
|
||||
}
|
||||
|
||||
func (a *MulSeriesResult) Length() int {
|
||||
lengtha := a.a.Length()
|
||||
lengthb := a.b.Length()
|
||||
if lengtha < lengthb {
|
||||
return lengtha
|
||||
}
|
||||
return lengthb
|
||||
}
|
||||
|
||||
var _ Series = &MulSeriesResult{}
|
||||
|
||||
// Calculate (a dot b).
|
||||
// if limit is given, will only calculate the first limit numbers (a.Index[0..limit])
|
||||
// otherwise will operate on all elements
|
||||
func Dot(a interface{}, b interface{}, limit ...int) float64 {
|
||||
return Sum(Mul(a, b), limit...)
|
||||
}
|
||||
|
||||
// Extract elements from the Series to a float64 array, following the order of Index(0..limit)
|
||||
// if limit is given, will only take the first limit numbers (a.Index[0..limit])
|
||||
// otherwise will operate on all elements
|
||||
func ToArray(a Series, limit ...int) (result []float64) {
|
||||
l := -1
|
||||
if len(limit) > 0 {
|
||||
l = limit[0]
|
||||
}
|
||||
if l < a.Length() {
|
||||
l = a.Length()
|
||||
}
|
||||
result = make([]float64, l, l)
|
||||
for i := 0; i < l; i++ {
|
||||
result[i] = a.Index(i)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Similar to ToArray but in reverse order.
|
||||
// Useful when you want to cache series' calculated result as float64 array
|
||||
// the then reuse the result in multiple places (so that no recalculation will be triggered)
|
||||
//
|
||||
// notice that the return type is a Float64Slice, which implements the Series interface
|
||||
func ToReverseArray(a Series, limit ...int) (result Float64Slice) {
|
||||
l := -1
|
||||
if len(limit) > 0 {
|
||||
l = limit[0]
|
||||
}
|
||||
if l < a.Length() {
|
||||
l = a.Length()
|
||||
}
|
||||
result = make([]float64, l, l)
|
||||
for i := 0; i < l; i++ {
|
||||
result[l-i-1] = a.Index(i)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ChangeResult struct {
|
||||
a Series
|
||||
offset int
|
||||
}
|
||||
|
||||
func (c *ChangeResult) Last() float64 {
|
||||
if c.offset >= c.a.Length() {
|
||||
return 0
|
||||
}
|
||||
return c.a.Last() - c.a.Index(c.offset)
|
||||
}
|
||||
|
||||
func (c *ChangeResult) Index(i int) float64 {
|
||||
if i+c.offset >= c.a.Length() {
|
||||
return 0
|
||||
}
|
||||
return c.a.Index(i) - c.a.Index(i+c.offset)
|
||||
}
|
||||
|
||||
func (c *ChangeResult) Length() int {
|
||||
length := c.a.Length()
|
||||
if length >= c.offset {
|
||||
return length - c.offset
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Difference between current value and previous, a - a[offset]
|
||||
// offset: if not given, offset is 1.
|
||||
func Change(a Series, offset ...int) Series {
|
||||
o := 1
|
||||
if len(offset) == 0 {
|
||||
o = offset[0]
|
||||
}
|
||||
|
||||
return &ChangeResult{a, o}
|
||||
}
|
||||
|
||||
// TODO: ta.linreg
|
||||
|
|
35
pkg/types/indicator_test.go
Normal file
35
pkg/types/indicator_test.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFloat(t *testing.T) {
|
||||
var a Series = Minus(3., 2.)
|
||||
assert.Equal(t, a.Last(), 1.)
|
||||
assert.Equal(t, a.Index(100), 1.)
|
||||
}
|
||||
|
||||
func TestNextCross(t *testing.T) {
|
||||
var a Series = NumberSeries(1.2)
|
||||
|
||||
var b Series = &Float64Slice{100., 80., 60.}
|
||||
// index 2 1 0
|
||||
// predicted 40 20 0
|
||||
// offset 1 2 3
|
||||
|
||||
index, value, ok := NextCross(a, b, 3)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, value, 1.2)
|
||||
assert.Equal(t, index, 3) // 2.94, ceil
|
||||
}
|
||||
|
||||
func TestFloat64Slice(t *testing.T) {
|
||||
var a = Float64Slice{1.0, 2.0, 3.0}
|
||||
var b = Float64Slice{1.0, 2.0, 3.0}
|
||||
var c Series = Minus(&a, &b)
|
||||
a = append(a, 4.0)
|
||||
b = append(b, 3.0)
|
||||
assert.Equal(t, c.Last(), 1.)
|
||||
}
|
|
@ -508,3 +508,97 @@ func (k KLineWindow) SlackAttachment() slack.Attachment {
|
|||
}
|
||||
|
||||
type KLineCallback func(kline KLine)
|
||||
|
||||
type KValueType int
|
||||
|
||||
const (
|
||||
kOpUnknown KValueType = iota
|
||||
kOpenValue
|
||||
kCloseValue
|
||||
kHighValue
|
||||
kLowValue
|
||||
kVolumeValue
|
||||
)
|
||||
|
||||
func (k *KLineWindow) High() Series {
|
||||
return &KLineSeries{
|
||||
lines: k,
|
||||
kv: kHighValue,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KLineWindow) Low() Series {
|
||||
return &KLineSeries{
|
||||
lines: k,
|
||||
kv: kLowValue,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KLineWindow) Open() Series {
|
||||
return &KLineSeries{
|
||||
lines: k,
|
||||
kv: kOpenValue,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KLineWindow) Close() Series {
|
||||
return &KLineSeries{
|
||||
lines: k,
|
||||
kv: kCloseValue,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *KLineWindow) Volume() Series {
|
||||
return &KLineSeries{
|
||||
lines: k,
|
||||
kv: kVolumeValue,
|
||||
}
|
||||
}
|
||||
|
||||
type KLineSeries struct {
|
||||
lines *KLineWindow
|
||||
kv KValueType
|
||||
}
|
||||
|
||||
func (k *KLineSeries) Last() float64 {
|
||||
length := len(*k.lines)
|
||||
switch k.kv {
|
||||
case kOpenValue:
|
||||
return (*k.lines)[length-1].GetOpen().Float64()
|
||||
case kCloseValue:
|
||||
return (*k.lines)[length-1].GetClose().Float64()
|
||||
case kLowValue:
|
||||
return (*k.lines)[length-1].GetLow().Float64()
|
||||
case kHighValue:
|
||||
return (*k.lines)[length-1].GetHigh().Float64()
|
||||
case kVolumeValue:
|
||||
return (*k.lines)[length-1].Volume.Float64()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (k *KLineSeries) Index(i int) float64 {
|
||||
length := len(*k.lines)
|
||||
if length == 0 || length-i-1 < 0 {
|
||||
return 0
|
||||
}
|
||||
switch k.kv {
|
||||
case kOpenValue:
|
||||
return (*k.lines)[length-i-1].GetOpen().Float64()
|
||||
case kCloseValue:
|
||||
return (*k.lines)[length-i-1].GetClose().Float64()
|
||||
case kLowValue:
|
||||
return (*k.lines)[length-i-1].GetLow().Float64()
|
||||
case kHighValue:
|
||||
return (*k.lines)[length-i-1].GetHigh().Float64()
|
||||
case kVolumeValue:
|
||||
return (*k.lines)[length-i-1].Volume.Float64()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (k *KLineSeries) Length() int {
|
||||
return len(*k.lines)
|
||||
}
|
||||
|
||||
var _ Series = &KLineSeries{}
|
||||
|
|
Loading…
Reference in New Issue
Block a user