1package gota
2
3// CMO - Chande Momentum Oscillator (https://www.fidelity.com/learning-center/trading-investing/technical-analysis/technical-indicator-guide/cmo)
4type CMO struct {
5	points  []cmoPoint
6	sumUp   float64
7	sumDown float64
8	count   int
9	idx     int // index of newest point
10}
11
12type cmoPoint struct {
13	price float64
14	diff  float64
15}
16
17// NewCMO constructs a new CMO.
18func NewCMO(inTimePeriod int) *CMO {
19	return &CMO{
20		points: make([]cmoPoint, inTimePeriod-1),
21	}
22}
23
24// WarmCount returns the number of samples that must be provided for the algorithm to be fully "warmed".
25func (cmo *CMO) WarmCount() int {
26	return len(cmo.points)
27}
28
29// Add adds a new sample value to the algorithm and returns the computed value.
30func (cmo *CMO) Add(v float64) float64 {
31	idxOldest := cmo.idx + 1
32	if idxOldest == len(cmo.points) {
33		idxOldest = 0
34	}
35
36	var diff float64
37	if cmo.count != 0 {
38		prev := cmo.points[cmo.idx]
39		diff = v - prev.price
40		if diff > 0 {
41			cmo.sumUp += diff
42		} else if diff < 0 {
43			cmo.sumDown -= diff
44		}
45	}
46
47	var outV float64
48	if cmo.sumUp != 0 || cmo.sumDown != 0 {
49		outV = 100.0 * ((cmo.sumUp - cmo.sumDown) / (cmo.sumUp + cmo.sumDown))
50	}
51
52	oldest := cmo.points[idxOldest]
53	//NOTE: because we're just adding and subtracting the difference, and not recalculating sumUp/sumDown using cmo.points[].price, it's possible for imprecision to creep in over time. Not sure how significant this is going to be, but if we want to fix it, we could recalculate it from scratch every N points.
54	if oldest.diff > 0 {
55		cmo.sumUp -= oldest.diff
56	} else if oldest.diff < 0 {
57		cmo.sumDown += oldest.diff
58	}
59
60	p := cmoPoint{
61		price: v,
62		diff:  diff,
63	}
64	cmo.points[idxOldest] = p
65	cmo.idx = idxOldest
66
67	if !cmo.Warmed() {
68		cmo.count++
69	}
70
71	return outV
72}
73
74// Warmed indicates whether the algorithm has enough data to generate accurate results.
75func (cmo *CMO) Warmed() bool {
76	return cmo.count == len(cmo.points)+2
77}
78
79// CMOS is a smoothed version of the Chande Momentum Oscillator.
80// This is the version of CMO utilized by ta-lib.
81type CMOS struct {
82	emaUp   EMA
83	emaDown EMA
84	lastV   float64
85}
86
87// NewCMOS constructs a new CMOS.
88func NewCMOS(inTimePeriod int, warmType WarmupType) *CMOS {
89	ema := NewEMA(inTimePeriod+1, warmType)
90	ema.alpha = float64(1) / float64(inTimePeriod)
91	return &CMOS{
92		emaUp:   *ema,
93		emaDown: *ema,
94	}
95}
96
97// WarmCount returns the number of samples that must be provided for the algorithm to be fully "warmed".
98func (cmos CMOS) WarmCount() int {
99	return cmos.emaUp.WarmCount()
100}
101
102// Warmed indicates whether the algorithm has enough data to generate accurate results.
103func (cmos CMOS) Warmed() bool {
104	return cmos.emaUp.Warmed()
105}
106
107// Last returns the last output value.
108func (cmos CMOS) Last() float64 {
109	up := cmos.emaUp.Last()
110	down := cmos.emaDown.Last()
111	return 100.0 * ((up - down) / (up + down))
112}
113
114// Add adds a new sample value to the algorithm and returns the computed value.
115func (cmos *CMOS) Add(v float64) float64 {
116	var up float64
117	var down float64
118	if v > cmos.lastV {
119		up = v - cmos.lastV
120	} else if v < cmos.lastV {
121		down = cmos.lastV - v
122	}
123	cmos.emaUp.Add(up)
124	cmos.emaDown.Add(down)
125	cmos.lastV = v
126	return cmos.Last()
127}
128