Skip to content

Commit f20ef11

Browse files
hwdecinar
andauthored
Add momentum indicator Martin Prings Special K (#278)
# Describe Request Please describe your request. Fixed #260 # 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** * Added Pring’s Special K momentum indicator — a weighted combination of multiple rate-of-change and moving-average horizons producing a single momentum signal; accepts generic numeric inputs. * **Tests** * Added unit tests for initialization, readiness/output thresholds, monotonic behavior on rising inputs, and zero output for constant inputs. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Onur Cinar <onur.cinar@gmail.com>
1 parent b32f418 commit f20ef11

File tree

2 files changed

+235
-0
lines changed

2 files changed

+235
-0
lines changed

momentum/prings_special_k.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package momentum
2+
3+
import (
4+
"github.com/cinar/indicator/v2/helper"
5+
"github.com/cinar/indicator/v2/trend"
6+
)
7+
8+
// PringsSpecialK implements Martin Pring's Special K momentum indicator.
9+
// It composes multiple Rate-of-Change (ROC) series smoothed by Simple Moving Averages (SMA)
10+
// and outputs a weighted sum aligned to the slowest path so all terms are time-synchronized.
11+
// See Compute for the exact composition and weights.
12+
type PringsSpecialK[T helper.Float] struct {
13+
Roc10 *trend.Roc[T]
14+
Roc15 *trend.Roc[T]
15+
Roc20 *trend.Roc[T]
16+
Roc30 *trend.Roc[T]
17+
Roc40 *trend.Roc[T]
18+
Roc65 *trend.Roc[T]
19+
Roc75 *trend.Roc[T]
20+
Roc100 *trend.Roc[T]
21+
Roc195 *trend.Roc[T]
22+
Roc265 *trend.Roc[T]
23+
Roc390 *trend.Roc[T]
24+
Roc530 *trend.Roc[T]
25+
26+
Sma10Roc10 *trend.Sma[T]
27+
Sma10Roc15 *trend.Sma[T]
28+
Sma10Roc20 *trend.Sma[T]
29+
Sma15Roc30 *trend.Sma[T]
30+
Sma50Roc40 *trend.Sma[T]
31+
Sma65Roc65 *trend.Sma[T]
32+
Sma75Roc75 *trend.Sma[T]
33+
Sma100Roc100 *trend.Sma[T]
34+
Sma130Roc195 *trend.Sma[T]
35+
Sma130Roc265 *trend.Sma[T]
36+
Sma130Roc390 *trend.Sma[T]
37+
Sma195Roc530 *trend.Sma[T]
38+
}
39+
40+
// NewPringsSpecialK function initializes a new Martin Pring's Special K instance.
41+
func NewPringsSpecialK[T helper.Float]() *PringsSpecialK[T] {
42+
return &PringsSpecialK[T]{
43+
Roc10: trend.NewRocWithPeriod[T](10),
44+
Roc15: trend.NewRocWithPeriod[T](15),
45+
Roc20: trend.NewRocWithPeriod[T](20),
46+
Roc30: trend.NewRocWithPeriod[T](30),
47+
Roc40: trend.NewRocWithPeriod[T](40),
48+
Roc65: trend.NewRocWithPeriod[T](65),
49+
Roc75: trend.NewRocWithPeriod[T](75),
50+
Roc100: trend.NewRocWithPeriod[T](100),
51+
Roc195: trend.NewRocWithPeriod[T](195),
52+
Roc265: trend.NewRocWithPeriod[T](265),
53+
Roc390: trend.NewRocWithPeriod[T](390),
54+
Roc530: trend.NewRocWithPeriod[T](530),
55+
56+
Sma10Roc10: trend.NewSmaWithPeriod[T](10),
57+
Sma10Roc15: trend.NewSmaWithPeriod[T](10),
58+
Sma10Roc20: trend.NewSmaWithPeriod[T](10),
59+
Sma15Roc30: trend.NewSmaWithPeriod[T](15),
60+
Sma50Roc40: trend.NewSmaWithPeriod[T](50),
61+
Sma65Roc65: trend.NewSmaWithPeriod[T](65),
62+
Sma75Roc75: trend.NewSmaWithPeriod[T](75),
63+
Sma100Roc100: trend.NewSmaWithPeriod[T](100),
64+
Sma130Roc195: trend.NewSmaWithPeriod[T](130),
65+
Sma130Roc265: trend.NewSmaWithPeriod[T](130),
66+
Sma130Roc390: trend.NewSmaWithPeriod[T](130),
67+
Sma195Roc530: trend.NewSmaWithPeriod[T](195),
68+
}
69+
}
70+
71+
func (p *PringsSpecialK[T]) Compute(closings <-chan T) <-chan T {
72+
c := helper.Duplicate(closings, 12)
73+
74+
roc10 := p.Roc10.Compute(c[0])
75+
roc15 := p.Roc15.Compute(c[1])
76+
roc20 := p.Roc20.Compute(c[2])
77+
roc30 := p.Roc30.Compute(c[3])
78+
roc40 := p.Roc40.Compute(c[4])
79+
roc65 := p.Roc65.Compute(c[5])
80+
roc75 := p.Roc75.Compute(c[6])
81+
roc100 := p.Roc100.Compute(c[7])
82+
roc195 := p.Roc195.Compute(c[8])
83+
roc265 := p.Roc265.Compute(c[9])
84+
roc390 := p.Roc390.Compute(c[10])
85+
roc530 := p.Roc530.Compute(c[11])
86+
87+
sma10Roc10 := p.Sma10Roc10.Compute(roc10)
88+
sma10Roc15 := p.Sma10Roc15.Compute(roc15)
89+
sma10Roc20 := p.Sma10Roc20.Compute(roc20)
90+
sma15Roc30 := p.Sma15Roc30.Compute(roc30)
91+
sma50Roc40 := p.Sma50Roc40.Compute(roc40)
92+
sma65Roc65 := p.Sma65Roc65.Compute(roc65)
93+
sma75Roc75 := p.Sma75Roc75.Compute(roc75)
94+
sma100Roc100 := p.Sma100Roc100.Compute(roc100)
95+
sma130Roc195 := p.Sma130Roc195.Compute(roc195)
96+
sma130Roc265 := p.Sma130Roc265.Compute(roc265)
97+
sma130Roc390 := p.Sma130Roc390.Compute(roc390)
98+
sma195Roc530 := p.Sma195Roc530.Compute(roc530)
99+
100+
maxIdle := p.Sma195Roc530.IdlePeriod() + p.Roc530.IdlePeriod()
101+
102+
sma10Roc10 = helper.Skip(sma10Roc10, maxIdle-p.Sma10Roc10.IdlePeriod()-p.Roc10.IdlePeriod())
103+
sma10Roc15 = helper.Skip(sma10Roc15, maxIdle-p.Sma10Roc15.IdlePeriod()-p.Roc15.IdlePeriod())
104+
sma10Roc20 = helper.Skip(sma10Roc20, maxIdle-p.Sma10Roc20.IdlePeriod()-p.Roc20.IdlePeriod())
105+
sma15Roc30 = helper.Skip(sma15Roc30, maxIdle-p.Sma15Roc30.IdlePeriod()-p.Roc30.IdlePeriod())
106+
sma50Roc40 = helper.Skip(sma50Roc40, maxIdle-p.Sma50Roc40.IdlePeriod()-p.Roc40.IdlePeriod())
107+
sma65Roc65 = helper.Skip(sma65Roc65, maxIdle-p.Sma65Roc65.IdlePeriod()-p.Roc65.IdlePeriod())
108+
sma75Roc75 = helper.Skip(sma75Roc75, maxIdle-p.Sma75Roc75.IdlePeriod()-p.Roc75.IdlePeriod())
109+
sma100Roc100 = helper.Skip(sma100Roc100, maxIdle-p.Sma100Roc100.IdlePeriod()-p.Roc100.IdlePeriod())
110+
sma130Roc195 = helper.Skip(sma130Roc195, maxIdle-p.Sma130Roc195.IdlePeriod()-p.Roc195.IdlePeriod())
111+
sma130Roc265 = helper.Skip(sma130Roc265, maxIdle-p.Sma130Roc265.IdlePeriod()-p.Roc265.IdlePeriod())
112+
sma130Roc390 = helper.Skip(sma130Roc390, maxIdle-p.Sma130Roc390.IdlePeriod()-p.Roc390.IdlePeriod())
113+
114+
p0 := helper.MultiplyBy(sma10Roc10, 1)
115+
p1 := helper.Add(p0, helper.MultiplyBy(sma10Roc15, 2))
116+
p2 := helper.Add(p1, helper.MultiplyBy(sma10Roc20, 3))
117+
p3 := helper.Add(p2, helper.MultiplyBy(sma15Roc30, 4))
118+
p4 := helper.Add(p3, helper.MultiplyBy(sma50Roc40, 1))
119+
p5 := helper.Add(p4, helper.MultiplyBy(sma65Roc65, 2))
120+
p6 := helper.Add(p5, helper.MultiplyBy(sma75Roc75, 3))
121+
p7 := helper.Add(p6, helper.MultiplyBy(sma100Roc100, 4))
122+
p8 := helper.Add(p7, helper.MultiplyBy(sma130Roc195, 1))
123+
p9 := helper.Add(p8, helper.MultiplyBy(sma130Roc265, 2))
124+
p10 := helper.Add(p9, helper.MultiplyBy(sma130Roc390, 3))
125+
p11 := helper.Add(p10, helper.MultiplyBy(sma195Roc530, 4))
126+
127+
return p11
128+
}

momentum/prings_special_k_test.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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.Roc20 == nil || psk.Roc30 == nil ||
15+
psk.Roc40 == nil || psk.Roc65 == nil || psk.Roc75 == nil || psk.Roc100 == nil ||
16+
psk.Roc195 == nil || psk.Roc265 == nil || psk.Roc390 == nil || psk.Roc530 == nil {
17+
t.Error("ROC pointers should be initialized")
18+
}
19+
if psk.Sma10Roc10 == nil || psk.Sma10Roc15 == nil || psk.Sma10Roc20 == nil ||
20+
psk.Sma15Roc30 == nil || psk.Sma50Roc40 == nil || psk.Sma65Roc65 == nil ||
21+
psk.Sma75Roc75 == nil || psk.Sma100Roc100 == nil || psk.Sma130Roc195 == nil ||
22+
psk.Sma130Roc265 == nil || psk.Sma130Roc390 == nil || psk.Sma195Roc530 == nil {
23+
t.Error("SMA pointers should be initialized")
24+
}
25+
}
26+
27+
func TestPringsSpecialKIsDecreasing(t *testing.T) {
28+
f := 0.1
29+
r := make([]float64, 0)
30+
for i := 0; i < 800; i++ {
31+
r = append(r, f)
32+
f += 0.01
33+
}
34+
cl := helper.SliceToChan(r)
35+
sp := NewPringsSpecialK[float64]()
36+
rs := sp.Compute(cl)
37+
p := math.NaN()
38+
upwardCount := 0
39+
for v := range rs {
40+
if !math.IsNaN(p) && p <= v {
41+
upwardCount++
42+
}
43+
p = v
44+
}
45+
if upwardCount != 0 {
46+
t.Error("Prings Special K should be decreasing on increasing values")
47+
}
48+
}
49+
50+
func pringsSpecialKComputeOutputLengthOnInputLength(inputLen int) int {
51+
input := make(chan float64, inputLen)
52+
53+
go func() {
54+
for i := 1; i <= inputLen; i++ {
55+
input <- float64(i)
56+
}
57+
close(input)
58+
}()
59+
60+
psk := NewPringsSpecialK[float64]()
61+
out := psk.Compute(input)
62+
63+
var got []float64
64+
for v := range out {
65+
got = append(got, v)
66+
}
67+
68+
return len(got)
69+
}
70+
71+
// Test sufficient number of samples for output
72+
func TestPringsSpecialKComputeBasicOutput(t *testing.T) {
73+
sma530 := trend.NewSmaWithPeriod[float64](530)
74+
roc195 := trend.NewRocWithPeriod[float64](195)
75+
minimumRequiredInputLength := sma530.IdlePeriod() + roc195.IdlePeriod() + 1
76+
77+
expectedOutputLen := 0
78+
actualOutputLen := pringsSpecialKComputeOutputLengthOnInputLength(minimumRequiredInputLength - 1)
79+
if actualOutputLen != expectedOutputLen {
80+
t.Errorf("Expected %d output values, got %d", expectedOutputLen, actualOutputLen)
81+
}
82+
expectedOutputLen = 1
83+
actualOutputLen = pringsSpecialKComputeOutputLengthOnInputLength(minimumRequiredInputLength)
84+
if actualOutputLen != expectedOutputLen {
85+
t.Errorf("Expected %d output values, got %d", expectedOutputLen, actualOutputLen)
86+
}
87+
}
88+
89+
func TestPringsSpecialKComputeConstantInput(t *testing.T) {
90+
inputLen := 800
91+
val := 100.0
92+
input := make(chan float64, inputLen)
93+
go func() {
94+
for i := 0; i < inputLen; i++ {
95+
input <- val
96+
}
97+
close(input)
98+
}()
99+
psk := NewPringsSpecialK[float64]()
100+
out := psk.Compute(input)
101+
for v := range out {
102+
if v != 0.0 {
103+
t.Errorf("Expected output to be 0 for constant input, got %v", v)
104+
break
105+
}
106+
}
107+
}

0 commit comments

Comments
 (0)