Skip to content
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
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,20 @@ atrIndicator := indicator.NewAverageVolume(series, period)
fmt.Println(atrIndicator.Calculate(1)) // 4368750
```

#### Filter Option

You can filter candles using `WithAverageVolumeFilter` option. Filtered candles are skipped during calculation, and the indicator looks back further to collect the required number of candles.

```go
// Skip candles with zero volume
filter := func(i int, candle *timeseries.Candle) bool {
return candle.Volume > 0
}
period := 2
avgVolume := indicator.NewAverageVolume(series, period, indicator.WithAverageVolumeFilter(filter))
fmt.Println(avgVolume.Calculate(1))
```

### Exponential Moving Average

Indicator calculates Exponential Moving Average
Expand All @@ -74,6 +88,20 @@ atrIndicator := indicator.NewExponentialMovingAverage(series, smoothInterval)
fmt.Println(atrIndicator.Calculate(1)) // 22.84552
```

#### Filter Option

You can filter candles using `WithExponentialMovingAverageFilter` option. When a candle is filtered out, the indicator uses the previous EMA value instead of recalculating.

```go
// Skip candles with low volume
filter := func(i int, candle *timeseries.Candle) bool {
return candle.Volume > 1000000
}
smoothInterval := 2
ema := indicator.NewExponentialMovingAverage(series, smoothInterval, indicator.WithExponentialMovingAverageFilter(filter))
fmt.Println(ema.Calculate(1))
```

### Trend

The indicator returns a trend direction. It bases on fast (with shorter period) and slow EMA. The third parameter (flatMaxDiff) of NewTrend allows setting max difference between fast and slow EMA when Calculate returns the flat. Option TrendWithFlatMaxDiffInPercent allows to pass flatMaxDiff in percent
Expand Down
9 changes: 8 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
module github.com/evsamsonov/trading-indicators/v2

go 1.15
go 1.25.4

require (
github.com/evsamsonov/trading-timeseries v1.3.0
github.com/stretchr/testify v1.11.1
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
Expand Down
79 changes: 60 additions & 19 deletions indicator/exponential_moving_average.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,38 @@ package indicator

import (
"errors"
"sync"

"github.com/evsamsonov/trading-timeseries/timeseries"
)

type ExponentialMovingAverageFilterFunc func(i int, candle *timeseries.Candle) bool

type ExponentialMovingAverageOption func(e *ExponentialMovingAverage)

func WithExponentialMovingAverageFilter(filter ExponentialMovingAverageFilterFunc) ExponentialMovingAverageOption {
return func(e *ExponentialMovingAverage) {
e.filter = filter
}
}

// ExponentialMovingAverage represents indicator to calculate Exponential Moving Average (EMA)
type ExponentialMovingAverage struct {
series *timeseries.TimeSeries
smoothInterval int
maxIndex int
cache []float64
smooth float64
filter ExponentialMovingAverageFilterFunc

smooth float64
mu sync.Mutex
maxIndex int
cache []float64
}

func NewExponentialMovingAverage(series *timeseries.TimeSeries, smoothInterval int) (*ExponentialMovingAverage, error) {
func NewExponentialMovingAverage(
series *timeseries.TimeSeries,
smoothInterval int,
opts ...ExponentialMovingAverageOption,
) (*ExponentialMovingAverage, error) {
if series.Length() == 0 {
return nil, errors.New("series is empty")
}
Expand All @@ -24,30 +42,53 @@ func NewExponentialMovingAverage(series *timeseries.TimeSeries, smoothInterval i
}
smooth := 2 / (float64(smoothInterval) + 1)

return &ExponentialMovingAverage{
ema := &ExponentialMovingAverage{
series: series,
smoothInterval: smoothInterval,
cache: make([]float64, series.Length()),
smooth: smooth,
}, nil
maxIndex: -1,
}
for _, opt := range opts {
opt(ema)
}
return ema, nil
}

func (a *ExponentialMovingAverage) Calculate(index int) float64 {
if index >= len(a.cache) {
a.cache = append(a.cache, 0)
a.cache = a.cache[:cap(a.cache)]
}
if a.cache[index] != 0 {
return a.cache[index]
func (e *ExponentialMovingAverage) Calculate(index int) float64 {
e.mu.Lock()
defer e.mu.Unlock()

if index >= len(e.cache) {
for range len(e.cache) - index + 1 {
e.cache = append(e.cache, 0)
}
}
if a.maxIndex == 0 {
a.cache[0] = a.series.Candle(0).Close
if index <= e.maxIndex && e.cache[index] != 0 {
return e.cache[index]
}

for i := a.maxIndex + 1; i <= index; i++ {
a.cache[i] = a.smooth*a.series.Candle(i).Close + (1-a.smooth)*a.cache[i-1]
for i := e.maxIndex + 1; i <= index; i++ {
candle := e.series.Candle(i)

prevEMA := float64(0)
if i > 0 {
prevEMA = e.cache[i-1]
}

// If filtered, use previous value
if e.filter != nil && !e.filter(i, candle) {
e.cache[i] = prevEMA
continue
}

if prevEMA == 0 {
e.cache[i] = candle.Close
} else {
e.cache[i] = e.smooth*candle.Close + (1-e.smooth)*prevEMA
}
}

a.maxIndex = index
return a.cache[index]
e.maxIndex = index
return e.cache[index]
}
66 changes: 66 additions & 0 deletions indicator/exponential_moving_average_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,72 @@ func TestExponentialMovingAverage_Calculate(t *testing.T) {
}
}

func TestExponentialMovingAverage_Calculate_WithFilter(t *testing.T) {
tests := []struct {
name string
closes []float64
smoothInterval int
filter ExponentialMovingAverageFilterFunc
expected []float64
}{
{
name: "filter middle candle",
closes: []float64{10, 20, 30, 40},
smoothInterval: 3,
filter: func(i int, candle *timeseries.Candle) bool {
return i != 1
},
// smooth = 2 / (3 + 1) = 0.5
// 0: 10
// 1: filtered, use previous value 10
// 2: 0.5 * 30 + 0.5 * 10 = 20
// 3: 0.5 * 40 + 0.5 * 20 = 30
expected: []float64{10, 10, 20, 30},
},
{
name: "filter first candle",
closes: []float64{10, 20, 30},
smoothInterval: 1,
filter: func(i int, candle *timeseries.Candle) bool {
return i != 0
},
// smooth = 2 / (1 + 1) = 1
// 0: filtered, use 0
// 1: 1 * 20 + 0 * 10 = 20
// 2: 1 * 30 + 0 * 20 = 30
expected: []float64{0, 20, 30},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
series := timeseries.New()
baseTime := time.Unix(1, 0)
for i, closePrice := range tt.closes {
candle := timeseries.NewCandle(baseTime.Add(time.Duration(i) * time.Hour))
candle.Close = closePrice
assert.NoError(t, series.AddCandle(candle))
}

ema, err := NewExponentialMovingAverage(
series,
tt.smoothInterval,
WithExponentialMovingAverageFilter(tt.filter),
)
assert.Nil(t, err)

for i := 0; i < series.Length(); i++ {
val := ema.Calculate(i)
if tt.expected[i] == 0 {
assert.Equal(t, tt.expected[i], val, "Index %d", i)
} else {
assert.InEpsilon(t, tt.expected[i], val, float64EqualityThreshold, "Index %d", i)
}
}
})
}
}

func TestExponentialMovingAverage_CalculateAfterAddCandle(t *testing.T) {
defer func() {
if r := recover(); r != nil {
Expand Down