@@ -33,10 +33,17 @@ const (
3333// EMA2 = EMA(values, slowPeriod)
3434// MACD = EMA1 - EMA2
3535//
36- // %K = Stochastic %K of MACD with kPeriod
37- // %D = Stochastic %D of MACD with dPeriod
36+ // %K1, %D1 = Stochastic( MACD, kPeriod, dPeriod)
37+ // %K2, %D2 = Stochastic(%D1, kPeriod, dPeriod)
3838//
39- // STC = 100 * (MACD - %K) / (%D - %K)
39+ // STC = %D2
40+ //
41+ // The Stochastic pass (rolling-min/max normalization to a 0-100 range,
42+ // then smoothed by an SMA) is applied twice, once to MACD and again to
43+ // the first pass's %D, the way the standard Schaff Trend Cycle algorithm
44+ // double-smooths MACD -- not once to MACD with a second division against
45+ // its own %K/%D, which isn't bounded to 0-100 and can divide by a
46+ // near-zero denominator.
4047//
4148// Example:
4249//
@@ -96,47 +103,22 @@ func (s *Stc[T]) ComputeWithContext(ctx context.Context, c <-chan T) <-chan T {
96103 c = helper .BufferedWithContext (ctx , c , s .SlowPeriod )
97104 macd := s .Apo .ComputeWithContext (ctx , c )
98105
99- macd = helper .BufferedWithContext (ctx , macd , s .Stochastic .Period )
100- inputs := helper .DuplicateWithContext (ctx , macd , 4 )
101-
102- movingMin := NewMovingMinWithPeriod [T ](s .Stochastic .Period )
103- movingMax := NewMovingMaxWithPeriod [T ](s .Stochastic .Period )
104-
105- lowestSplice := helper .DuplicateWithContext (ctx , movingMin .ComputeWithContext (ctx , inputs [0 ]),
106- 2 ,
107- )
106+ // %K1 and %K2 are unused (STC only needs the doubly-smoothed %D), but
107+ // each comes from the same internal duplicate fan-out as %D1/%D2, so
108+ // it still has to be drained -- an unread duplicate branch blocks the
109+ // shared producer, stalling %D1/%D2 too.
110+ k1 , d1 := s .Stochastic .ComputeWithContext (ctx , macd )
111+ go helper .DrainWithContext (ctx , k1 )
108112
109- highest := movingMax .ComputeWithContext (ctx , inputs [1 ])
110-
111- skipped := helper .SkipWithContext (ctx , inputs [2 ], movingMin .IdlePeriod ())
112-
113- kValues := helper .MultiplyByWithContext (ctx , helper .DivideWithContext (ctx , helper .SubtractWithContext (ctx , skipped , lowestSplice [0 ]),
114- helper .SubtractWithContext (ctx , highest , lowestSplice [1 ]),
115- ),
116- 100 ,
117- )
113+ k2 , d2 := s .Stochastic .ComputeWithContext (ctx , d1 )
114+ go helper .DrainWithContext (ctx , k2 )
118115
119- kDuplicate := helper .DuplicateWithContext (ctx , kValues , 2 )
120-
121- d := s .Stochastic .Sma .ComputeWithContext (ctx , kDuplicate [0 ])
122-
123- kForStcSplice := helper .DuplicateWithContext (ctx ,
124- helper .SkipWithContext (ctx , kDuplicate [1 ], s .Stochastic .Sma .IdlePeriod ()),
125- 2 ,
126- )
127-
128- macdForStc := helper .SkipWithContext (ctx , inputs [3 ], s .Stochastic .IdlePeriod ())
129-
130- return helper .MultiplyByWithContext (ctx , helper .DivideWithContext (ctx , helper .SubtractWithContext (ctx , macdForStc , kForStcSplice [0 ]),
131- helper .SubtractWithContext (ctx , d , kForStcSplice [1 ]),
132- ),
133- 100 ,
134- )
116+ return d2
135117}
136118
137119// IdlePeriod is the initial period that STC won't yield any results.
138120func (s * Stc [T ]) IdlePeriod () int {
139- return s .Apo .IdlePeriod () + s .Stochastic .IdlePeriod ()
121+ return s .Apo .IdlePeriod () + 2 * s .Stochastic .IdlePeriod ()
140122}
141123
142124// Compute wraps ComputeWithContext for backwards compatibility.
0 commit comments