Skip to content

Fix STC dividing by a near-zero denominator, producing -Inf/huge out-of-range swings - #428

Merged
cinar merged 1 commit into
masterfrom
fix/stc-near-zero-denominator
Aug 22, 2026
Merged

Fix STC dividing by a near-zero denominator, producing -Inf/huge out-of-range swings#428
cinar merged 1 commit into
masterfrom
fix/stc-near-zero-denominator

Conversation

@cinar

@cinar cinar commented Aug 22, 2026

Copy link
Copy Markdown
Owner

Closes #425.

Summary

  • trend.Stc.ComputeWithContext divided (MACD-%K)/(%D-%K) for its final line, per the doc comment's STC = 100 * (MACD - %K) / (%D - %K). %D-%K is frequently near zero (both are stochastic-smoothed values of the same MACD series and track each other closely), so real-world input yields -Inf and swings like 1435/-710 for an indicator documented to oscillate 0-100.
  • Replaced with the standard Schaff Trend Cycle algorithm: apply the existing Stochastic (rolling-min/max normalization + SMA) twice -- once to MACD to get %K1/%D1, then again to %D1 to get %K2/%D2 -- with STC = %D2. Both stages stay bounded in [0, 100] by construction.
  • %K1/%K2 are discarded (STC only needs the doubly-smoothed %D), but each comes from the same internal duplicate fan-out as %D1/%D2 inside Stochastic.ComputeWithContext, so each is drained in its own goroutine (helper.DrainWithContext) -- an unread duplicate branch otherwise blocks the shared producer and stalls %D1/%D2 too (this reintroduces the class of deadlock Fix Stc.ComputeWithContext output channel never closing #421 fixed if skipped).
  • IdlePeriod() updated to Apo.IdlePeriod() + 2*Stochastic.IdlePeriod() to match the now-doubled warm-up.
  • TestStcFull's fixture (testdata/stochastic.csv, 19 rows, shared with two other tests) is too short for the new warm-up, so it now generates its own longer synthetic series instead.

Note on remaining NaN

STC can still legitimately output NaN when a stochastic stage's rolling window is exactly flat (a genuine 0/0). Separately, I found that trend.MovingSum (which the Sma inside Stochastic uses) doesn't recover from a single NaN even after it leaves the window -- filed and fixed separately as #427 / PR (fix/movingsum-nan-poisoning), since it's a shared primitive bug, not specific to STC. With that fix applied too, STC on a 251-day BRK-B series drops from 157/180 NaN outputs down to 36/180, with the rest being either the initial warm-up or a genuinely flat window at the very tail (no future data to recover into). Without it, STC's math is still correct here, just noisier in practice on real data until #427 lands.

Test plan

  • go build ./...
  • go test -race ./... (full repo, no regressions)
  • go vet, gosec, staticcheck, revive clean on trend/
  • Manual check against asset/testdata/repository/brk-b.csv: output now bounded in [0, 100] (previously -Inf, 1435, -710, etc.)
  • New TestStcFull (longer synthetic fixture) and existing TestStcDeadlock/TestStcSlowStochastic all pass

🤖 Generated with Claude Code

STC = 100*(MACD-%K)/(%D-%K) divided by a denominator that's frequently
near zero (%K and %D are both stochastic-smoothed values of the same
MACD series and track each other closely), so real-world input could
yield -Inf or swings like 1435/-710 for an indicator documented to
oscillate 0-100.

The standard Schaff Trend Cycle algorithm instead applies the
rolling-min/max stochastic normalization *twice* -- once to MACD to
get %K1/%D1, then again to %D1 to get %K2/%D2 -- with STC = %D2. Both
stages stay bounded in [0, 100] by construction, since they're each a
range-normalized percentage smoothed by an SMA, not a division against
another stochastic-smoothed value.

TestStcFull's fixture was too short for the new, larger warm-up (two
stochastic passes need more lead-in than one), so it now generates its
own longer synthetic series instead of testdata/stochastic.csv.

Fixes #425

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cinar

cinar commented Aug 22, 2026

Copy link
Copy Markdown
Owner Author

Depends on #430 (MovingSum NaN-poisoning fix) for STC to be fully usable on real data -- STC's math is correct here regardless, but a single flat MACD window anywhere in the series will otherwise cause an extended NaN run downstream until #430 also lands. See #430's description for the before/after numbers.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.29%. Comparing base (6653727) to head (9022386).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #428      +/-   ##
==========================================
- Coverage   92.32%   92.29%   -0.03%     
==========================================
  Files         229      229              
  Lines        7280     7259      -21     
==========================================
- Hits         6721     6700      -21     
  Misses        472      472              
  Partials       87       87              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

cinar added a commit that referenced this pull request Aug 22, 2026
Cross-reference #425/PR #428 and #426/PR #429 now that both formula
bugs have fixes proposed, not just issues filed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cinar
cinar merged commit 0dbec11 into master Aug 22, 2026
5 checks passed
@cinar
cinar deleted the fix/stc-near-zero-denominator branch August 22, 2026 03:54
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 22, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

trend.Stc: STC formula divides by a near-zero denominator, producing -Inf and huge out-of-range swings

2 participants