Skip to content

Consolidate zero-denominator guards into helper.SafeDivide - #505

Open
cinar wants to merge 1 commit into
masterfrom
worktree-agent-acf7e30cab909e262
Open

Consolidate zero-denominator guards into helper.SafeDivide#505
cinar wants to merge 1 commit into
masterfrom
worktree-agent-acf7e30cab909e262

Conversation

@cinar

@cinar cinar commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Summary

Earlier work independently added zero-denominator/NaN guards across ~17 indicators in trend, momentum, volatility, and volume, each shaped like:

if denom == 0 {
    return fallback // 0, 50, or 0.5
}
return numerator / denom

This PR adds a single generic helper, helper.SafeDivide[T Number](numerator, denominator, fallback T) T (in helper/safe_divide.go, modeled on the existing scalar helper helper.RoundDigit), and refactors every matching call site to use it. Behavior is unchanged β€” same fallback values, same floating-point evaluation order β€” verified by the full existing test suite passing unmodified.

Call sites refactored

Direct pattern (SafeDivide(num, denom, fallback) replaces the guard 1:1):

  • trend/kama.go β€” Efficiency Ratio, fallback 0
  • trend/bop.go β€” BOP, fallback 0
  • trend/vwma.go β€” VWMA, fallback 0
  • momentum/ibs.go β€” Internal Bar Strength, fallback 0
  • momentum/ultimate_oscillator.go β€” per-period average, fallback 0.5
  • volatility/bollinger_band_width.go β€” fallback 0
  • volatility/z_score.go β€” fallback 0
  • volatility/acceleration_bands.go β€” fallback 0
  • volatility/percent_b.go β€” %B, fallback 0.5
  • volume/mfm.go β€” Money Flow Multiplier, fallback 0
  • volume/cmf.go β€” CMF, fallback 0

Scaled pattern (original code applied a * 100 or pre-multiplied the numerator by 100 around the guarded division; to keep the arithmetic byte-for-byte identical, the fallback is expressed in the same pre-scale units so the surrounding multiply reproduces the exact original float value):

  • trend/kdj.go β€” RSV: SafeDivide(num, denom, 0.5) * 100 (0.5 Γ— 100 = 50, exact)
  • trend/slow_stochastic.go β€” fastK: same transform
  • trend/stochastic.go β€” %K: same transform
  • momentum/stochastic_oscillator.go β€” fastK: same transform
  • trend/tsi.go β€” SafeDivide(pcd, apcd, 0) * 100 (fallback 0, scaling is a no-op)
  • trend/cfo.go β€” SafeDivide(num, price, 0) * 100 (same)
  • volatility/po.go β€” original pre-multiplied the numerator (100 * (closing - pl) / denom), so here it's SafeDivide(100*(closing-pl), denom, 50) β€” fallback needs no rescaling since the original guard already returned the unscaled 50 literal

helper.SafeDivide is constrained to helper.Number (not helper.Float) specifically so that trend/tsi.go, whose Tsi[T helper.Number] is generic over integers too, can call it directly without widening or narrowing any existing type's constraint.

Explicitly skipped (do not fit the pattern)

  • volume/vwap.go β€” carries forward the last valid computed value instead of a constant fallback (per task instructions, left untouched).
  • trend/mcginley_dynamic.go β€” same shape as Vwap: on a zero previous value it reseeds state and takes a completely different code path (not a single guarded return of a quotient), so it isn't a SafeDivide candidate.
  • momentum/rsi.go β€” the guard condition is averageGain == 0 && averageLoss == 0 (compound, two variables), not a single denominator check, so it doesn't fit SafeDivide(numerator, denominator, fallback)'s shape.
  • volatility/chop.go β€” the guarded division feeds into math.Log10(sum/diff) rather than being returned directly as a quotient; the guard and the formula shape don't reduce to a plain numerator/denominator.

Test plan

  • go build ./...
  • go vet ./...
  • gofmt -l . (no new files flagged; pre-existing unrelated formatting debt in unrelated examples//strategy//backtest test files, untouched by this PR)
  • go test ./... -timeout 300s β€” all packages pass unmodified, confirming no behavior change

πŸ€– Generated with Claude Code

https://claude.ai/code/session_01Xv4stuAb6WuQ8rPZ4cupLp

Replace 17 ad hoc "if denom == 0 { return fallback }" guards across
trend, momentum, volatility, and volume with a single generic
helper.SafeDivide[T Number](numerator, denominator, fallback T) T.
Behavior and fallback values (0, 50, or 0.5) are unchanged at every
call site; volume.Vwap (carries forward the last valid value) and
trend.McginleyDynamic (same shape) are left as-is since they don't
fit the constant-fallback pattern, and momentum.Rsi and
volatility.Chop are left as-is since their guards check a compound
or non-quotient condition rather than a plain zero denominator.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xv4stuAb6WuQ8rPZ4cupLp
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

βœ… All modified and coverable lines are covered by tests.
βœ… Project coverage is 91.99%. Comparing base (74b9529) to head (f998834).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #505      +/-   ##
==========================================
+ Coverage   91.96%   91.99%   +0.02%     
==========================================
  Files         233      234       +1     
  Lines        7719     7703      -16     
==========================================
- Hits         7099     7086      -13     
+ Misses        531      528       -3     
  Partials       89       89              

β˜” 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants