Skip to content

Commit 7eaa98e

Browse files
committed
Add momentum indicator Martin Prings Special K
1 parent a3e7cbd commit 7eaa98e

File tree

2 files changed

+220
-0
lines changed

2 files changed

+220
-0
lines changed

momentum/prings_special_k.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package momentum
2+
3+
import (
4+
"github.com/cinar/indicator/v2/helper"
5+
"github.com/cinar/indicator/v2/trend"
6+
)
7+
8+
type PringsSpecialK[T helper.Float] struct {
9+
Roc10 *trend.Roc[T]
10+
Roc15 *trend.Roc[T]
11+
Roc50 *trend.Roc[T]
12+
Roc65 *trend.Roc[T]
13+
Roc75 *trend.Roc[T]
14+
Roc100 *trend.Roc[T]
15+
Roc130 *trend.Roc[T]
16+
Roc195 *trend.Roc[T]
17+
18+
Sma10 *trend.Sma[T]
19+
Sma15 *trend.Sma[T]
20+
Sma20 *trend.Sma[T]
21+
Sma30 *trend.Sma[T]
22+
Sma40 *trend.Sma[T]
23+
Sma65 *trend.Sma[T]
24+
Sma75 *trend.Sma[T]
25+
Sma100 *trend.Sma[T]
26+
Sma195 *trend.Sma[T]
27+
Sma265 *trend.Sma[T]
28+
Sma390 *trend.Sma[T]
29+
Sma530 *trend.Sma[T]
30+
}
31+
32+
// NewPringsSpecialK function initializes a new Martin Pring's Special K instance.
33+
func NewPringsSpecialK[T helper.Float]() *PringsSpecialK[T] {
34+
return &PringsSpecialK[T]{
35+
Roc10: trend.NewRocWithPeriod[T](10),
36+
Roc15: trend.NewRocWithPeriod[T](15),
37+
Roc50: trend.NewRocWithPeriod[T](50),
38+
Roc65: trend.NewRocWithPeriod[T](65),
39+
Roc75: trend.NewRocWithPeriod[T](75),
40+
Roc100: trend.NewRocWithPeriod[T](100),
41+
Roc130: trend.NewRocWithPeriod[T](130),
42+
Roc195: trend.NewRocWithPeriod[T](195),
43+
44+
Sma10: trend.NewSmaWithPeriod[T](10),
45+
Sma15: trend.NewSmaWithPeriod[T](15),
46+
Sma20: trend.NewSmaWithPeriod[T](20),
47+
Sma30: trend.NewSmaWithPeriod[T](30),
48+
Sma40: trend.NewSmaWithPeriod[T](40),
49+
Sma65: trend.NewSmaWithPeriod[T](65),
50+
Sma75: trend.NewSmaWithPeriod[T](75),
51+
Sma100: trend.NewSmaWithPeriod[T](100),
52+
Sma195: trend.NewSmaWithPeriod[T](195),
53+
Sma265: trend.NewSmaWithPeriod[T](265),
54+
Sma390: trend.NewSmaWithPeriod[T](390),
55+
Sma530: trend.NewSmaWithPeriod[T](530),
56+
}
57+
}
58+
59+
func (p *PringsSpecialK[T]) Compute(closings <-chan T) <-chan T {
60+
c := helper.Duplicate(closings, 8)
61+
62+
roc10 := p.Roc10.Compute(c[0])
63+
roc15 := p.Roc15.Compute(c[1])
64+
roc50 := p.Roc50.Compute(c[2])
65+
roc65 := p.Roc65.Compute(c[3])
66+
roc75 := p.Roc75.Compute(c[4])
67+
roc100 := p.Roc100.Compute(c[5])
68+
roc130 := p.Roc130.Compute(c[6])
69+
roc195 := p.Roc195.Compute(c[7])
70+
71+
roc10s := helper.Duplicate(roc10, 3)
72+
sma10 := p.Sma10.Compute(roc10s[0])
73+
sma15 := p.Sma15.Compute(roc10s[1])
74+
sma20 := p.Sma20.Compute(roc10s[2])
75+
sma30 := p.Sma30.Compute(roc15)
76+
sma40 := p.Sma40.Compute(roc50)
77+
sma65 := p.Sma65.Compute(roc65)
78+
sma75 := p.Sma75.Compute(roc75)
79+
sma100 := p.Sma100.Compute(roc100)
80+
roc130s := helper.Duplicate(roc130, 3)
81+
sma195 := p.Sma195.Compute(roc130s[0])
82+
sma265 := p.Sma265.Compute(roc130s[1])
83+
sma390 := p.Sma390.Compute(roc130s[2])
84+
sma530 := p.Sma530.Compute(roc195)
85+
86+
maxIdle := p.Sma530.IdlePeriod() + p.Roc195.IdlePeriod()
87+
88+
sma10 = helper.Skip(sma10, maxIdle-p.Sma10.IdlePeriod()-p.Roc10.IdlePeriod())
89+
sma15 = helper.Skip(sma15, maxIdle-p.Sma15.IdlePeriod()-p.Roc10.IdlePeriod())
90+
sma20 = helper.Skip(sma20, maxIdle-p.Sma20.IdlePeriod()-p.Roc10.IdlePeriod())
91+
sma30 = helper.Skip(sma30, maxIdle-p.Sma30.IdlePeriod()-p.Roc15.IdlePeriod())
92+
sma40 = helper.Skip(sma40, maxIdle-p.Sma40.IdlePeriod()-p.Roc50.IdlePeriod())
93+
sma65 = helper.Skip(sma65, maxIdle-p.Sma65.IdlePeriod()-p.Roc65.IdlePeriod())
94+
sma75 = helper.Skip(sma75, maxIdle-p.Sma75.IdlePeriod()-p.Roc75.IdlePeriod())
95+
sma100 = helper.Skip(sma100, maxIdle-p.Sma100.IdlePeriod()-p.Roc100.IdlePeriod())
96+
sma195 = helper.Skip(sma195, maxIdle-p.Sma195.IdlePeriod()-p.Roc130.IdlePeriod())
97+
sma265 = helper.Skip(sma265, maxIdle-p.Sma265.IdlePeriod()-p.Roc130.IdlePeriod())
98+
sma390 = helper.Skip(sma390, maxIdle-p.Sma390.IdlePeriod()-p.Roc130.IdlePeriod())
99+
//sma530 = helper.Skip(sma530, p.Sma530.IdlePeriod()-p.Sma530.IdlePeriod())
100+
101+
p0 := helper.MultiplyBy(sma10, 1)
102+
p1 := helper.Add(p0, helper.MultiplyBy(sma15, 2))
103+
p2 := helper.Add(p1, helper.MultiplyBy(sma20, 3))
104+
p3 := helper.Add(p2, helper.MultiplyBy(sma30, 4))
105+
p4 := helper.Add(p3, helper.MultiplyBy(sma40, 1))
106+
p5 := helper.Add(p4, helper.MultiplyBy(sma65, 2))
107+
p6 := helper.Add(p5, helper.MultiplyBy(sma75, 3))
108+
p7 := helper.Add(p6, helper.MultiplyBy(sma100, 4))
109+
p8 := helper.Add(p7, helper.MultiplyBy(sma195, 1))
110+
p9 := helper.Add(p8, helper.MultiplyBy(sma265, 2))
111+
p10 := helper.Add(p9, helper.MultiplyBy(sma390, 3))
112+
p11 := helper.Add(p10, helper.MultiplyBy(sma530, 4))
113+
114+
return p11
115+
}

momentum/prings_special_k_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package momentum
2+
3+
import (
4+
"math"
5+
"testing"
6+
7+
"github.com/cinar/indicator/v2/helper"
8+
"github.com/cinar/indicator/v2/trend"
9+
)
10+
11+
func TestNewPringsSpecialKInitialization(t *testing.T) {
12+
psk := NewPringsSpecialK[float64]()
13+
14+
if psk.Roc10 == nil || psk.Roc15 == nil || psk.Roc50 == nil || psk.Roc65 == nil ||
15+
psk.Roc75 == nil || psk.Roc100 == nil || psk.Roc130 == nil || psk.Roc195 == nil {
16+
t.Error("ROC pointers should be initialized")
17+
}
18+
if psk.Sma10 == nil || psk.Sma15 == nil || psk.Sma20 == nil || psk.Sma30 == nil ||
19+
psk.Sma40 == nil || psk.Sma65 == nil || psk.Sma75 == nil || psk.Sma100 == nil ||
20+
psk.Sma195 == nil || psk.Sma265 == nil || psk.Sma390 == nil || psk.Sma530 == nil {
21+
t.Error("SMA pointers should be initialized")
22+
}
23+
}
24+
25+
func TestPringsSpecialKIsDecreasing(t *testing.T) {
26+
f := 0.1
27+
r := make([]float64, 0)
28+
for i := 0; i < 800; i++ {
29+
r = append(r, f)
30+
f += 0.01
31+
}
32+
cl := helper.SliceToChan(r)
33+
sp := NewPringsSpecialK[float64]()
34+
rs := sp.Compute(cl)
35+
p := math.NaN()
36+
upwardCount := 0
37+
for v := range rs {
38+
if !math.IsNaN(p) && p <= v {
39+
upwardCount++
40+
}
41+
p = v
42+
}
43+
if upwardCount != 0 {
44+
t.Error("Prings Special K should be decreasing on increasing values")
45+
}
46+
}
47+
48+
func pringsSpecialKComputeOutputLengthOnInputLength(inputLen int) int {
49+
input := make(chan float64, inputLen)
50+
51+
go func() {
52+
for i := 1; i <= inputLen; i++ {
53+
input <- float64(i)
54+
}
55+
close(input)
56+
}()
57+
58+
psk := NewPringsSpecialK[float64]()
59+
out := psk.Compute(input)
60+
61+
var got []float64
62+
for v := range out {
63+
got = append(got, v)
64+
}
65+
66+
return len(got)
67+
}
68+
69+
// Test sufficient number of samples for output
70+
func TestPringsSpecialKComputeBasicOutput(t *testing.T) {
71+
sma530 := trend.NewSmaWithPeriod[float64](530)
72+
roc195 := trend.NewRocWithPeriod[float64](195)
73+
minimumRequiredInputLength := sma530.IdlePeriod() + roc195.IdlePeriod() + 1
74+
75+
expectedOutputLen := 0
76+
actualOutputLen := pringsSpecialKComputeOutputLengthOnInputLength(minimumRequiredInputLength - 1)
77+
if actualOutputLen != expectedOutputLen {
78+
t.Errorf("Expected %d output values, got %d", expectedOutputLen, actualOutputLen)
79+
}
80+
expectedOutputLen = 1
81+
actualOutputLen = pringsSpecialKComputeOutputLengthOnInputLength(minimumRequiredInputLength)
82+
if actualOutputLen != expectedOutputLen {
83+
t.Errorf("Expected %d output values, got %d", expectedOutputLen, actualOutputLen)
84+
}
85+
}
86+
87+
func TestPringsSpecialKComputeConstantInput(t *testing.T) {
88+
inputLen := 800
89+
val := 100.0
90+
input := make(chan float64, inputLen)
91+
go func() {
92+
for i := 0; i < inputLen; i++ {
93+
input <- val
94+
}
95+
close(input)
96+
}()
97+
psk := NewPringsSpecialK[float64]()
98+
out := psk.Compute(input)
99+
for v := range out {
100+
if v != 0.0 {
101+
t.Errorf("Expected output to be 0 for constant input, got %v", v)
102+
break
103+
}
104+
}
105+
}

0 commit comments

Comments
 (0)