Skip to content

Commit 8ff5e12

Browse files
hwdeheikocinar
authored
Feature/fix aroon (#276)
# Describe Request Introduce Window, MaxSince and MinSince on channels to hopefully fix #124 as suggested. Fixed # (issue) # Change Type What is the type of this change. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Streaming sliding-window utilities: Lowest/Highest per-window extrema, MaxSince/MinSince persistence counters, a reverse-slice iterator, and a generic Window transformer. * **Bug Fixes** * More accurate "since last high" and "since last low" calculations in trend indicators via specialized helpers. * **Tests** * Added unit tests covering sliding-window behavior, extrema persistence logic, reverse-slice utility, and Aroon/trend validation. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: heiko <[email protected]> Co-authored-by: Onur Cinar <[email protected]>
1 parent ac8ac2b commit 8ff5e12

File tree

18 files changed

+747
-303
lines changed

18 files changed

+747
-303
lines changed

helper/Lowest.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) 2021-2024 Onur Cinar.
2+
// The source code is provided under GNU AGPLv3 License.
3+
// https://github.com/cinar/indicator
4+
5+
package helper
6+
7+
import "slices"
8+
9+
// Lowest returns a channel that emits the lowest value
10+
// within a sliding window of size w from the input channel c.
11+
func Lowest[T Number](c <-chan T, w int) <-chan T {
12+
return Window(c, func(s []T, i int) T {
13+
return slices.Min(s)
14+
}, w)
15+
}

helper/highest.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) 2021-2024 Onur Cinar.
2+
// The source code is provided under GNU AGPLv3 License.
3+
// https://github.com/cinar/indicator
4+
5+
package helper
6+
7+
import "slices"
8+
9+
// Highest returns a channel that emits the highest value
10+
// within a sliding window of size w from the input channel c.
11+
func Highest[T Number](c <-chan T, w int) <-chan T {
12+
return Window(c, func(s []T, i int) T {
13+
return slices.Max(s)
14+
}, w)
15+
}

helper/highest_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) 2021-2024 Onur Cinar.
2+
// The source code is provided under GNU AGPLv3 License.
3+
// https://github.com/cinar/indicator
4+
5+
package helper
6+
7+
import (
8+
"testing"
9+
)
10+
11+
func TestHighest(t *testing.T) {
12+
input := SliceToChan([]int{48, 52, 50, 49, 10})
13+
expected := SliceToChan([]int{48, 52, 52, 52, 50})
14+
window := 3
15+
actual := Highest(input, window)
16+
17+
err := CheckEquals(actual, expected)
18+
if err != nil {
19+
t.Fatal(err)
20+
}
21+
}

helper/lowest_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) 2021-2024 Onur Cinar.
2+
// The source code is provided under GNU AGPLv3 License.
3+
// https://github.com/cinar/indicator
4+
5+
package helper
6+
7+
import (
8+
"testing"
9+
)
10+
11+
func TestLowest(t *testing.T) {
12+
input := SliceToChan([]int{48, 52, 50, 49, 10})
13+
expected := SliceToChan([]int{48, 48, 48, 49, 10})
14+
window := 3
15+
actual := Lowest(input, window)
16+
17+
err := CheckEquals(actual, expected)
18+
if err != nil {
19+
t.Fatal(err)
20+
}
21+
}

helper/max_since.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) 2021-2024 Onur Cinar.
2+
// The source code is provided under GNU AGPLv3 License.
3+
// https://github.com/cinar/indicator
4+
5+
package helper
6+
7+
import (
8+
"slices"
9+
)
10+
11+
// MaxSince returns a channel of T indicating since when
12+
// (number of previous values) the respective value was the maximum
13+
// within the window of size w.
14+
func MaxSince[T Number](c <-chan T, w int) <-chan T {
15+
return Window(c, func(w []T, i int) T {
16+
since := 0
17+
found := false
18+
m := slices.Max(w)
19+
SlicesReverse(w, i, func(n T) bool {
20+
if found && n < m {
21+
return false
22+
}
23+
since++
24+
if n == m {
25+
found = true
26+
}
27+
return true
28+
})
29+
return T(since - 1)
30+
}, w)
31+
}

helper/max_since_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package helper
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestMaxSince(t *testing.T) {
8+
input := SliceToChan([]int{48, 49, 47, 52, 52, 52, 53, 50, 55})
9+
expected := SliceToChan([]int{0, 0, 1, 0, 1, 2, 0, 1, 0})
10+
actual := MaxSince(input, 3)
11+
12+
err := CheckEquals(actual, expected)
13+
if err != nil {
14+
t.Fatal(err)
15+
}
16+
}
17+
18+
func TestMaxSinceAtEnd(t *testing.T) {
19+
input := SliceToChan([]int{48, 49, 47, 52, 52, 52, 52})
20+
expected := SliceToChan([]int{0, 0, 1, 0, 1, 2, 2})
21+
actual := MaxSince(input, 3)
22+
23+
err := CheckEquals(actual, expected)
24+
if err != nil {
25+
t.Fatal(err)
26+
}
27+
}

helper/min_since.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright (c) 2021-2024 Onur Cinar.
2+
// The source code is provided under GNU AGPLv3 License.
3+
// https://github.com/cinar/indicator
4+
5+
package helper
6+
7+
import (
8+
"slices"
9+
)
10+
11+
// MinSince returns a channel of T indicating since when
12+
// (number of previous values) the respective value was the minimum.
13+
func MinSince[T Number](c <-chan T, w int) <-chan T {
14+
return Window(c, func(w []T, i int) T {
15+
since := 0
16+
found := false
17+
m := slices.Min(w)
18+
SlicesReverse(w, i, func(n T) bool {
19+
if found && n > m {
20+
return false
21+
}
22+
since++
23+
if n == m {
24+
found = true
25+
}
26+
return true
27+
})
28+
return T(since - 1)
29+
}, w)
30+
}

helper/min_since_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package helper
2+
3+
import "testing"
4+
5+
func TestMinSince(t *testing.T) {
6+
input := SliceToChan([]int{48, 50, 50, 50, 49, 49, 51})
7+
expected := SliceToChan([]int{0, 1, 2, 2, 0, 1, 2})
8+
actual := MinSince(input, 3)
9+
10+
err := CheckEquals(actual, expected)
11+
if err != nil {
12+
t.Fatal(err)
13+
}
14+
}
15+
16+
func TestMinSinceAtEnd(t *testing.T) {
17+
input := SliceToChan([]int{48, 50, 50, 50, 49, 49, 49})
18+
expected := SliceToChan([]int{0, 1, 2, 2, 0, 1, 2})
19+
actual := MinSince(input, 3)
20+
21+
err := CheckEquals(actual, expected)
22+
if err != nil {
23+
t.Fatal(err)
24+
}
25+
}
26+
27+
func TestMinSinceFromStart(t *testing.T) {
28+
input := SliceToChan([]int{1, 1, 3})
29+
expected := SliceToChan([]int{0, 1, 2})
30+
actual := MinSince(input, 3)
31+
32+
err := CheckEquals(actual, expected)
33+
if err != nil {
34+
t.Fatal(err)
35+
}
36+
}

helper/slices_reverse.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package helper
2+
3+
// SlicesReverse loops through a slice in reverse order starting from the given index.
4+
// The given function is called for each element in the slice. If the function returns false,
5+
// the loop is terminated.
6+
func SlicesReverse[T any](r []T, i int, f func(T) bool) {
7+
l := len(r)
8+
if l == 0 || i < 0 || i >= l {
9+
return
10+
}
11+
for m := i - 1; m >= 0; m-- {
12+
if !f(r[m]) {
13+
return
14+
}
15+
}
16+
for m := l - 1; m >= i; m-- {
17+
if !f(r[m]) {
18+
return
19+
}
20+
}
21+
}

helper/slices_reverse_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package helper
2+
3+
import (
4+
"slices"
5+
"testing"
6+
)
7+
8+
func TestSlicesReverseSimple(t *testing.T) {
9+
input := []int{1, 2, 3, 4}
10+
expected := []int{4, 3, 2, 1}
11+
actual := make([]int, 0, len(input))
12+
SlicesReverse(input, 0, func(i int) bool {
13+
actual = append(actual, i)
14+
return true
15+
})
16+
if !slices.Equal(actual, expected) {
17+
t.Fatal("not equal")
18+
}
19+
}
20+
21+
func TestSlicesReverseMiddle(t *testing.T) {
22+
input := []int{1, 2, 3, 4}
23+
expected := []int{2, 1, 4, 3}
24+
actual := make([]int, 0, len(input))
25+
SlicesReverse(input, 2, func(i int) bool {
26+
actual = append(actual, i)
27+
return true
28+
})
29+
if !slices.Equal(actual, expected) {
30+
t.Fatal("not equal")
31+
}
32+
}

0 commit comments

Comments
 (0)