Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support ta.mfi #51

Merged
merged 5 commits into from
Apr 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion pine/ohlcv_series_base.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@ import (
"math"

"github.com/pkg/errors"
"github.com/twinj/uuid"
)

// OHLCVBaseSeries represents a series of OHLCV type (i.e. open, high, low, close, volume)
type OHLCVBaseSeries interface {
ID() string

Push(OHLCV)

Shift() bool
Expand Down Expand Up @@ -36,7 +39,9 @@ type OHLCVBaseSeries interface {
}

func NewOHLCVBaseSeries() OHLCVBaseSeries {
u := uuid.NewV4()
s := &ohlcvBaseSeries{
id: u.String(),
max: 1000, // default maximum items
vals: make(map[int64]OHLCV),
}
Expand All @@ -49,6 +54,8 @@ type ohlcvBaseSeries struct {
// current ohlcv
cur *OHLCV

id string

first *OHLCV

last *OHLCV
Expand Down Expand Up @@ -94,6 +101,10 @@ func (s *ohlcvBaseSeries) GoToFirst() *OHLCV {
return s.cur
}

func (s *ohlcvBaseSeries) ID() string {
return s.id
}

func (s *ohlcvBaseSeries) fetchAndAppend() (bool, error) {
more, err := s.ds.Populate(s.cur.S)
if err != nil {
Expand Down Expand Up @@ -165,7 +176,8 @@ func (s *ohlcvBaseSeries) GetSeries(p OHLCProp) ValueSeries {
} else {
propVal = math.Abs(v.H - v.L)
}

case OHLCPropHLC3:
propVal = (v.H + v.L + v.C) / 3
default:
continue
}
Expand Down
8 changes: 4 additions & 4 deletions pine/ohlcv_series_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,16 @@ func TestNewOHLCVSeries(t *testing.T) {
vals []float64
}{
{
prop: []OHLCProp{OHLCPropClose, OHLCPropHigh, OHLCPropLow, OHLCPropOpen, OHLCPropTR},
vals: []float64{data[0].C, data[0].H, data[0].L, data[0].O, tr1},
prop: []OHLCProp{OHLCPropClose, OHLCPropHigh, OHLCPropLow, OHLCPropOpen, OHLCPropTR, OHLCPropHLC3},
vals: []float64{data[0].C, data[0].H, data[0].L, data[0].O, tr1, (data[0].H + data[0].L + data[0].C) / 3},
},
{
prop: []OHLCProp{OHLCPropClose, OHLCPropHigh, OHLCPropLow, OHLCPropOpen, OHLCPropTR},
vals: []float64{data[1].C, data[1].H, data[1].L, data[1].O, tr2},
vals: []float64{data[1].C, data[1].H, data[1].L, data[1].O, tr2, (data[1].H + data[1].L + data[1].C) / 3},
},
{
prop: []OHLCProp{OHLCPropClose, OHLCPropHigh, OHLCPropLow, OHLCPropOpen, OHLCPropTR},
vals: []float64{data[2].C, data[2].H, data[2].L, data[2].O, tr3},
vals: []float64{data[2].C, data[2].H, data[2].L, data[2].O, tr3, (data[2].H + data[2].L + data[2].C) / 3},
},
}

Expand Down
5 changes: 5 additions & 0 deletions pine/series_change.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,8 @@ func change(stop Value, src, chg ValueSeries, l int) (ValueSeries, error) {

return chg, nil
}

func NewFloat64(v float64) *float64 {
v2 := v
return &v2
}
5 changes: 0 additions & 5 deletions pine/series_macd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,11 +171,6 @@ func TestSeriesMACDIteration(t *testing.T) {
}
}

func NewFloat64(v float64) *float64 {
v2 := v
return &v2
}

func BenchmarkMACD(b *testing.B) {
// run the Fib function b.N times
start := time.Now()
Expand Down
80 changes: 80 additions & 0 deletions pine/series_mfi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package pine

import (
"fmt"

"github.com/pkg/errors"
)

// MFI generates a ValueSeries of exponential moving average.
func MFI(o OHLCVSeries, l int64) (ValueSeries, error) {
key := fmt.Sprintf("mfi:%s:%d", o.ID(), l)
mfi := getCache(key)
if mfi == nil {
mfi = NewValueSeries()
}

hlc3 := o.GetSeries(OHLCPropHLC3)
vol := o.GetSeries(OHLCPropVolume)

hlc3c := hlc3.GetCurrent()
if hlc3c == nil {
return mfi, nil
}

chg, err := Change(hlc3, 1)
if err != nil {
return mfi, errors.Wrap(err, "error getting change")
}

u := hlc3.OperateWithNil(chg, func(a, b *float64) *float64 {
var v float64
// treat nil value as HLC3
if b == nil {
return a
} else {
v = *b
}
if v <= 0.0 {
return NewFloat64(0.0)
}
return a
})
lo := hlc3.OperateWithNil(chg, func(a, b *float64) *float64 {
var v float64
// treat nil value as HLC3
if b == nil {
return a
} else {
v = *b
}
if v >= 0.0 {
return NewFloat64(0.0)
}
return a
})

uv := vol.Mul(u)
lv := vol.Mul(lo)

upper, err := Sum(uv, int(l))
if err != nil {
return mfi, errors.Wrap(err, "error getting sum for higher")
}

lower, err := Sum(lv, int(l))
if err != nil {
return mfi, errors.Wrap(err, "error getting sum for lower")
}

hundo := hlc3.Copy()
hundo.SetAll(100)

mfi = hundo.Sub(hundo.Div(upper.Div(lower).AddConst(1)))

setCache(key, mfi)

mfi.SetCurrent(hlc3c.t)

return mfi, nil
}
128 changes: 128 additions & 0 deletions pine/series_mfi_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package pine

import (
"fmt"
"log"
"testing"
"time"

"github.com/pkg/errors"
)

// TestSeriesMFI tests no data scenario
//
// t=time.Time (no iteration) | |
// p=ValueSeries | |
// mfi=ValueSeries | |
func TestSeriesMFI(t *testing.T) {

start := time.Now()
data := OHLCVTestData(start, 4, 5*60*1000)

series, err := NewOHLCVSeries(data)
if err != nil {
t.Fatal(err)
}

mfi, err := MFI(series, 3)
if err != nil {
t.Fatal(errors.Wrap(err, "error MFI"))
}
if mfi == nil {
t.Error("Expected mfi to be non nil but got nil")
}
}

// TestSeriesMFINoIteration tests this sceneario where there's no iteration yet
//
// t=time.Time (no iteration) | 1 | 2 | 3 | 4 |
// p=ValueSeries | 14 | 15 | 17 | 18 |
// mfi=ValueSeries | | | | |
func TestSeriesMFINoIteration(t *testing.T) {

start := time.Now()
data := OHLCVTestData(start, 4, 5*60*1000)
data[0].C = 14
data[1].C = 15
data[2].C = 17
data[3].C = 18

series, err := NewOHLCVSeries(data)
if err != nil {
t.Fatal(err)
}

mfi, err := MFI(series, 3)
if err != nil {
t.Fatal(errors.Wrap(err, "error MFI"))
}
if mfi == nil {
t.Error("Expected mfi to be non nil but got nil")
}
}

// TestSeriesMFIIteration tests the output against TradingView's expected values
func TestSeriesMFIIteration(t *testing.T) {
data := OHLCVStaticTestData()
series, err := NewOHLCVSeries(data)
if err != nil {
t.Fatal(err)
}

tests := []*float64{
nil,
nil,
nil,
NewFloat64(38.856),
NewFloat64(52.679),
NewFloat64(27.212),
NewFloat64(26.905),
NewFloat64(28.794),
NewFloat64(27.858),
NewFloat64(31.572),
}

for i, v := range tests {
series.Next()
mfi, err := MFI(series, 4)
if err != nil {
t.Fatal(errors.Wrap(err, "error mfi"))
}

// mfi line
if (mfi.Val() == nil) != (v == nil) {
if mfi.Val() != nil {
t.Errorf("Expected mfi to be nil: %t but got %+v for iteration: %d", v == nil, *mfi.Val(), i)
} else {
t.Errorf("Expected mfi to be: %+v but got %+v for iteration: %d", *v, mfi.Val(), i)
}
continue
}
if v != nil && fmt.Sprintf("%.03f", *v) != fmt.Sprintf("%.03f", *mfi.Val()) {
t.Errorf("Expected mfi to be %+v but got %+v for iteration: %d", *v, *mfi.Val(), i)
}
}
}

func BenchmarkMFI(b *testing.B) {
// run the Fib function b.N times
start := time.Now()
data := OHLCVTestData(start, 10000, 5*60*1000)
series, _ := NewOHLCVSeries(data)

for n := 0; n < b.N; n++ {
series.Next()
MFI(series, 12)
}
}

func ExampleMFI() {
start := time.Now()
data := OHLCVTestData(start, 10000, 5*60*1000)
series, _ := NewOHLCVSeries(data)
mfi, err := MFI(series, 12)
if err != nil {
log.Fatal(errors.Wrap(err, "error MFI"))
}
log.Printf("MFI line: %+v", mfi.Val())
}
24 changes: 24 additions & 0 deletions pine/testdata_ohlcv_no_gap.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,39 @@ func OHLCVTestData(start time.Time, period, intervalms int64) []OHLCV {
return v
}

func OHLCVStaticTestData() []OHLCV {
start := time.Now()
data := []OHLCV{
OHLCV{O: 11.3, H: 19.7, L: 11.1, C: 16.5, V: 11.6},
OHLCV{O: 12.9, H: 19.1, L: 12.3, C: 18.7, V: 13.0},
OHLCV{O: 11.0, H: 18.8, L: 10.3, C: 18.2, V: 13.8},
OHLCV{O: 19.2, H: 19.6, L: 11.7, C: 11.9, V: 15.9},
OHLCV{O: 18.1, H: 19.5, L: 11.2, C: 19.3, V: 16.8},
OHLCV{O: 19.4, H: 19.8, L: 13.5, C: 14.2, V: 19.1},
OHLCV{O: 19.1, H: 19.5, L: 12.9, C: 14.4, V: 14.7},
OHLCV{O: 10.6, H: 19.9, L: 10.3, C: 11.0, V: 11.7},
OHLCV{O: 18.8, H: 19.0, L: 12.4, C: 14.7, V: 17.4},
OHLCV{O: 17.1, H: 17.6, L: 10.0, C: 10.3, V: 15.0},
}

for i := range data {
fivemin := 5 * time.Minute
data[i].S = start.Add(time.Duration(i) * fivemin)
}
return data
}

func generateOHLCV(t time.Time) OHLCV {
max := 20.0
min := 10.0

o := randVal(min, max)
c := randVal(min, max)
vol := randVal(min, max)
v := OHLCV{
O: o,
C: c,
V: vol,
}

h2 := math.Max(c, o)
Expand Down
Loading