Merge pull request #505 from zenixls2/feature/series

feature: add pinescript series interface
This commit is contained in:
Zenix 2022-04-13 11:13:56 +09:00 committed by GitHub
commit b57c94fe12
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1012 additions and 16 deletions

View 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`.

View File

@ -11,9 +11,9 @@ type MarketDataStore struct {
Symbol string Symbol string
// KLineWindows stores all loaded klines per interval // 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 { func NewMarketDataStore(symbol string) *MarketDataStore {
@ -21,16 +21,16 @@ func NewMarketDataStore(symbol string) *MarketDataStore {
Symbol: symbol, Symbol: symbol,
// KLineWindows stores all loaded klines per interval // 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 store.KLineWindows = windows
} }
// KLinesOfInterval returns the kline window of the given interval // 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] kLines, ok = store.KLineWindows[interval]
return kLines, ok return kLines, ok
} }
@ -50,14 +50,15 @@ func (store *MarketDataStore) handleKLineClosed(kline types.KLine) {
func (store *MarketDataStore) AddKLine(kline types.KLine) { func (store *MarketDataStore) AddKLine(kline types.KLine) {
window, ok := store.KLineWindows[kline.Interval] window, ok := store.KLineWindows[kline.Interval]
if !ok { 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) window.Add(kline)
if len(window) > MaxNumOfKLines { if len(*window) > MaxNumOfKLines {
window = window[MaxNumOfKLinesTruncate-1:] *window = (*window)[MaxNumOfKLinesTruncate-1:]
} }
store.KLineWindows[kline.Interval] = window store.EmitKLineWindowUpdate(kline.Interval, *window)
store.EmitKLineWindowUpdate(kline.Interval, window)
} }

View File

@ -6,12 +6,12 @@ import (
"github.com/c9s/bbgo/pkg/types" "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) 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 { for _, cb := range store.kLineWindowUpdateCallbacks {
cb(interval, kline) cb(interval, klines)
} }
} }

View File

@ -365,7 +365,7 @@ var BacktestCmd = &cobra.Command{
startPrice, ok := session.StartPrice(symbol) startPrice, ok := session.StartPrice(symbol)
if !ok { 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) lastPrice, ok := session.LastPrice(symbol)

View File

@ -23,12 +23,17 @@ type AD struct {
} }
func (inc *AD) Update(kLine types.KLine) { func (inc *AD) Update(kLine types.KLine) {
close := kLine.Close.Float64() cloze := kLine.Close.Float64()
high := kLine.High.Float64() high := kLine.High.Float64()
low := kLine.Low.Float64() low := kLine.Low.Float64()
volume := kLine.Volume.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 ad := inc.Last() + moneyFlowVolume
inc.Values.Push(ad) inc.Values.Push(ad)
@ -41,6 +46,20 @@ func (inc *AD) Last() float64 {
return inc.Values[len(inc.Values)-1] 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) { func (inc *AD) calculateAndUpdate(kLines []types.KLine) {
for _, k := range kLines { for _, k := range kLines {
if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) {

View File

@ -39,6 +39,24 @@ type BOLL struct {
updateCallbacks []func(sma, upBand, downBand float64) 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 { func (inc *BOLL) LastUpBand() float64 {
if len(inc.UpBand) == 0 { if len(inc.UpBand) == 0 {
return 0.0 return 0.0

View File

@ -44,6 +44,18 @@ func (inc *EWMA) Last() float64 {
return inc.Values[len(inc.Values)-1] 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) { func (inc *EWMA) calculateAndUpdate(allKLines []types.KLine) {
if len(allKLines) < inc.Window { if len(allKLines) < inc.Window {
// we can't calculate // we can't calculate
@ -149,3 +161,5 @@ func (inc *EWMA) handleKLineWindowUpdate(interval types.Interval, window types.K
func (inc *EWMA) Bind(updater KLineWindowUpdater) { func (inc *EWMA) Bind(updater KLineWindowUpdater) {
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
} }
var _ types.Series = &EWMA{}

76
pkg/indicator/line.go Normal file
View 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{}

View File

@ -89,3 +89,34 @@ func (inc *MACD) handleKLineWindowUpdate(interval types.Interval, window types.K
func (inc *MACD) Bind(updater KLineWindowUpdater) { func (inc *MACD) Bind(updater KLineWindowUpdater) {
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) 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
}

View File

@ -63,6 +63,20 @@ func (inc *RSI) Last() float64 {
return inc.Values[len(inc.Values)-1] 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) { func (inc *RSI) calculateAndUpdate(kLines []types.KLine) {
var priceF = KLineClosePriceMapper var priceF = KLineClosePriceMapper

View File

@ -30,6 +30,31 @@ func (inc *SMA) Last() float64 {
return inc.Values[len(inc.Values)-1] 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) { func (inc *SMA) calculateAndUpdate(kLines []types.KLine) {
if len(kLines) < inc.Window { if len(kLines) < inc.Window {
return return

View File

@ -82,3 +82,11 @@ func (inc *STOCH) handleKLineWindowUpdate(interval types.Interval, window types.
func (inc *STOCH) Bind(updater KLineWindowUpdater) { func (inc *STOCH) Bind(updater KLineWindowUpdater) {
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
} }
func (inc *STOCH) GetD() types.Series {
return &inc.D
}
func (inc *STOCH) GetK() types.Series {
return &inc.K
}

View File

@ -35,6 +35,21 @@ func (inc *VWAP) Last() float64 {
return inc.Values[len(inc.Values)-1] 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) { func (inc *VWAP) Update(kLine types.KLine, priceF KLinePriceMapper) {
price := priceF(kLine) price := priceF(kLine)
volume := kLine.Volume.Float64() volume := kLine.Volume.Float64()

View File

@ -34,6 +34,20 @@ func (inc *VWMA) Last() float64 {
return inc.Values[len(inc.Values)-1] 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 { func KLinePriceVolumeMapper(k types.KLine) float64 {
return k.Close.Mul(k.Volume).Float64() return k.Close.Mul(k.Volume).Float64()
} }

View File

@ -116,3 +116,29 @@ func (s Float64Slice) Dot(other Float64Slice) float64 {
func (s Float64Slice) Normalize() Float64Slice { func (s Float64Slice) Normalize() Float64Slice {
return s.DivScalar(s.Sum()) 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()

View File

@ -1,6 +1,512 @@
package types 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. // Float64Indicator is the indicators (SMA and EWMA) that we want to use are returning float64 data.
type Float64Indicator interface { type Float64Indicator interface {
Last() float64 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

View 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.)
}

View File

@ -508,3 +508,97 @@ func (k KLineWindow) SlackAttachment() slack.Attachment {
} }
type KLineCallback func(kline KLine) 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{}