Skip to content

Klinger Volume Oscillator (KVO) is added.#327

Merged
cinar merged 1 commit intomasterfrom
implement-variadic-operate
Feb 25, 2026
Merged

Klinger Volume Oscillator (KVO) is added.#327
cinar merged 1 commit intomasterfrom
implement-variadic-operate

Conversation

@cinar
Copy link
Owner

@cinar cinar commented Feb 25, 2026

Describe Request

Klinger Volume Oscillator (KVO) is added.

Fixed #309

Change Type

New indicator.

Summary by CodeRabbit

  • New Features

    • Added Klinger Volume Oscillator (KVO) indicator for volume analysis.
    • Added Operate5 helper function for concurrent multi-channel operations.
  • Improvements

    • Enhanced RVI and KST stream alignment for better output synchronization.
  • Documentation

    • Updated README files with new indicator references and anchor links.
  • Chores

    • Added .geminiignore configuration file and updated .gitignore patterns.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Walkthrough

This pull request implements the Klinger Volume Oscillator (KVO), a volume-based technical indicator that tracks money flow trends through EMA differences. It introduces a new helper function Operate5 for five-channel operations, refactors RVI and KST stream handling for signal alignment, and adds configuration files along with comprehensive tests and documentation updates.

Changes

Cohort / File(s) Summary
Configuration & Ignore Files
.geminiignore, .gitignore
Adds ignore patterns for Gemini operations (.gemini/ directory, gha-creds-*.json files) and common artifacts.
Documentation & Deletion
AGENTS.md, README.md, helper/README.md, momentum/README.md, trend/README.md, volume/README.md
Removes AGENTS.md entirely; updates documentation links for Rvi/Kst anchor positions; adds KVO type, constructor, and method signatures with constants to volume package docs; adds README entry for KVO indicator.
Helper Package - Operate5
helper/operate5.go, helper/operate5_test.go
Adds new generic function Operate5 for concurrent operations across five input channels, with comprehensive edge-case tests covering differing stream lengths.
Momentum Package - RVI Refactoring
momentum/rvi.go, momentum/rvi_test.go
Introduces stream duplication (rviSplice) to align RVI output with signal line via Skip operation; refactors test to synchronously consume RVI and signal channels instead of independent slice conversion.
Trend Package - KST Refactoring
trend/kst.go, trend/kst_test.go
Implements stream duplication (kstSplice) for signal line alignment and delayed KST return; refactors tests to explicitly iterate channels synchronously instead of using ChanToSlice helper.
Volume Package - KVO Implementation
volume/kvo.go, volume/kvo_test.go
Implements Klinger Volume Oscillator with Kvo[T] generic type; computes Volume Force via trend evaluation, applies dual EMAs (short/long periods), derives oscillator difference, and generates signal line; includes tests for computation and idle period behavior.

Sequence Diagram

sequenceDiagram
    participant Input as Input Streams<br/>(highs, lows, volumes)
    participant VF as VF Computation<br/>(Volume Force)
    participant ShortEMA as Short EMA<br/>(Period 34)
    participant LongEMA as Long EMA<br/>(Period 55)
    participant KVO as KVO Difference<br/>(Short - Long)
    participant SignalEMA as Signal EMA<br/>(Period 13)
    participant Output as Output<br/>(KVO + Signal)

    Input->>VF: Duplicate streams with shift
    VF->>VF: Compute Trend (high/low comparison)<br/>VF = Volume × Trend
    VF->>ShortEMA: Stream to short period EMA
    VF->>LongEMA: Stream to long period EMA
    ShortEMA->>ShortEMA: Apply EMA(34)
    LongEMA->>LongEMA: Apply EMA(55)
    ShortEMA->>KVO: Align via idle period
    LongEMA->>KVO: Align via idle period
    KVO->>KVO: Compute difference
    KVO->>SignalEMA: Stream to signal EMA
    SignalEMA->>SignalEMA: Apply EMA(13)
    KVO->>Output: Emit KVO with skip
    SignalEMA->>Output: Emit Signal result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • The Know Sure Thing (KST) indicator. #299: Implements similar stream duplication and signal alignment patterns in KST computation, directly paralleling the channel-splitting approach introduced in trend/kst.go.

Poem

Five channels dance in harmony, 🌊
KVO flows through volume's sea,
EMA whispers, trends align,
Signal bells and numbers shine, ✨
Let the data flow and intertwine! 🐰

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Out of Scope Changes check ❓ Inconclusive Multiple changes beyond KVO implementation detected: Operate5 addition, AGENTS.md deletion, .geminiignore addition, and RVI/KST stream alignment changes require justification. Clarify whether Operate5, AGENTS.md deletion, .geminiignore, and RVI/KST alignment changes are necessary dependencies for KVO or separate enhancements that should be in separate PRs.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding a new Klinger Volume Oscillator indicator.
Description check ✅ Passed The PR description follows the template with both required sections filled: 'Describe Request' and 'Change Type', accurately describing the addition of KVO.
Linked Issues check ✅ Passed The implementation fully satisfies issue #309 requirements: KVO type and constructor added, VF computation logic implemented, short/long EMAs applied concurrently, and KVO difference returned.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch implement-variadic-operate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter
Copy link

Codecov Report

❌ Patch coverage is 93.93939% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.99%. Comparing base (d4f7163) to head (4fbc1af).

Files with missing lines Patch % Lines
volume/kvo.go 87.50% 3 Missing and 1 partial ⚠️

❌ Your project status has failed because the head coverage (88.99%) is below the target coverage (90.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #327      +/-   ##
==========================================
- Coverage   88.99%   88.99%   -0.01%     
==========================================
  Files         205      207       +2     
  Lines        5761     5822      +61     
==========================================
+ Hits         5127     5181      +54     
- Misses        563      568       +5     
- Partials       71       73       +2     

☔ View full report in Codecov by Sentry.
📢 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@helper/README.md`:
- Around line 840-855: The README's Operate5 documentation uses fenced code
blocks without language labels (triggering markdownlint MD040); update the three
fenced blocks around the Operate5 section — the function signature block and the
example block(s) that contain the code sample for Operate5 — to use the "go"
language tag (replace ``` with ```go) so the blocks specify language and satisfy
the linter while preserving the existing code and formatting.

In `@momentum/rvi_test.go`:
- Around line 41-44: The loop that collects from rviResult and signalResult
should guard the paired receive from signalResult with the comma-ok form to
detect closed channels: in the loop that iterates over rviResult (referencing
rviResult, rviSlice, signalResult, signalSlice) replace the direct
<-signalResult receive with a two-value receive (val, ok := <-signalResult) and,
if !ok, stop the loop or handle the unexpected early close (e.g., break and fail
the test), otherwise append val to signalSlice; ensure rvi values are still
appended to rviSlice only when the paired signal value was successfully
received.

In `@trend/kst_test.go`:
- Around line 30-33: The test currently pairs values from actualKst and
actualSignal using single-value receives which masks premature channel closure;
update both loops that iterate over actualKst (the block using variables
kstSlice and signalSlice and the similar block at lines ~99-102) to perform
two-value receives from actualSignal (s, ok := <-actualSignal), check ok, and
fail the test (e.g., t.Fatalf or equivalent) if ok is false before appending, so
a closed signal channel is detected rather than silently using zero-values.

In `@volume/kvo_test.go`:
- Around line 37-53: The current test reads from kvoResult and signalResult
together (looping over kvoResult and consuming signalResult inside), which can
hang on mismatched channel lengths and uses manual checks; change to drain each
channel independently (e.g., range kvoResult to append to kvoSlice until closed,
then range signalResult to append to signalSlice) so length mismatches fail
cleanly, then replace manual NaN/Inf and length checks with the project test
helpers: use helper.RoundDigits(...) when comparing float64 values and assert
equality with helper.CheckEquals(...) for expected counts/values; update the
assertions around kvoSlice and signalSlice and any float comparisons to call
helper.RoundDigits before passing values to helper.CheckEquals.

In `@volume/README.md`:
- Around line 395-409: The fenced code blocks for the KVO formula and the Go
example are missing language identifiers; update the formula block to use a
neutral identifier (e.g., ```text) and the example block to use the Go
identifier (```go) so markdownlint MD040 is satisfied; specifically edit the
README's KVO section containing the formula block and the snippet that creates
NewKvo[float64]() and calls Compute(highs, lows, volumes) to add the appropriate
fenced-language tags.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d4f7163 and 4fbc1af.

📒 Files selected for processing (16)
  • .geminiignore
  • .gitignore
  • AGENTS.md
  • README.md
  • helper/README.md
  • helper/operate5.go
  • helper/operate5_test.go
  • momentum/README.md
  • momentum/rvi.go
  • momentum/rvi_test.go
  • trend/README.md
  • trend/kst.go
  • trend/kst_test.go
  • volume/README.md
  • volume/kvo.go
  • volume/kvo_test.go
💤 Files with no reviewable changes (1)
  • AGENTS.md

Comment on lines +840 to +855
<a name="Operate5"></a>
## func [Operate5](<https://github.com/cinar/indicator/blob/master/helper/operate5.go#L15-L22>)

```go
func Operate5[A any, B any, C any, D any, E any, R any](ac <-chan A, bc <-chan B, cc <-chan C, dc <-chan D, ec <-chan E, o func(A, B, C, D, E) R) <-chan R
```

Operate5 applies the provided operate function to corresponding values from five numeric input channels and sends the resulting values to an output channel.

Example:

```
result := helper.Operate5(ac, bc, cc, dc, ec, func(a, b, c, d, e int) int {
return a + b + c + d + e
})
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Specify language on the new fenced code blocks.

The newly added Operate5 section includes fenced blocks without explicit language, which triggers markdownlint MD040.

📝 Suggested fix
-```
+```go
 result := helper.Operate5(ac, bc, cc, dc, ec, func(a, b, c, d, e int) int {
   return a + b + c + d + e
 })
-```
+```
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 840-840: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


[warning] 847-847: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@helper/README.md` around lines 840 - 855, The README's Operate5 documentation
uses fenced code blocks without language labels (triggering markdownlint MD040);
update the three fenced blocks around the Operate5 section — the function
signature block and the example block(s) that contain the code sample for
Operate5 — to use the "go" language tag (replace ``` with ```go) so the blocks
specify language and satisfy the linter while preserving the existing code and
formatting.

Comment on lines +41 to +44
for v := range rviResult {
rviSlice = append(rviSlice, v)
signalSlice = append(signalSlice, <-signalResult)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard paired channel reads with ok checks.

Using <-signalResult directly can hide early channel closure by appending zero values, which may let regressions slip through.

✅ Suggested fix
-	for v := range rviResult {
-		rviSlice = append(rviSlice, v)
-		signalSlice = append(signalSlice, <-signalResult)
-	}
+	for v := range rviResult {
+		signal, ok := <-signalResult
+		if !ok {
+			t.Fatalf("signal result closed before rvi at index %d", len(rviSlice))
+		}
+
+		rviSlice = append(rviSlice, v)
+		signalSlice = append(signalSlice, signal)
+	}
+
+	if _, ok := <-signalResult; ok {
+		t.Fatal("signal produced extra values beyond rvi")
+	}
#!/bin/bash
# Verify one-value receive usage in the paired RVI collection loop.
rg -nP --type=go 'signalSlice\s*=\s*append\(\s*signalSlice,\s*<-\s*signalResult\s*\)' momentum/rvi_test.go
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@momentum/rvi_test.go` around lines 41 - 44, The loop that collects from
rviResult and signalResult should guard the paired receive from signalResult
with the comma-ok form to detect closed channels: in the loop that iterates over
rviResult (referencing rviResult, rviSlice, signalResult, signalSlice) replace
the direct <-signalResult receive with a two-value receive (val, ok :=
<-signalResult) and, if !ok, stop the loop or handle the unexpected early close
(e.g., break and fail the test), otherwise append val to signalSlice; ensure rvi
values are still appended to rviSlice only when the paired signal value was
successfully received.

Comment on lines +30 to +33
for v := range actualKst {
kstSlice = append(kstSlice, v)
signalSlice = append(signalSlice, <-actualSignal)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n trend/kst_test.go | head -50

Repository: cinar/indicator

Length of output: 1608


🏁 Script executed:

sed -n '25,110p' trend/kst_test.go | cat -n

Repository: cinar/indicator

Length of output: 2852


🏁 Script executed:

# Also check for the pattern mentioned in the review
rg -nP --type=go 'signalSlice\s*=\s*append\(\s*signalSlice,\s*<-\s*actualSignal\s*\)' trend/kst_test.go

Repository: cinar/indicator

Length of output: 169


🏁 Script executed:

cat trend/kst.go | head -100

Repository: cinar/indicator

Length of output: 2798


🏁 Script executed:

# Find the Compute method for KST
ast-grep --pattern $'func ($_ *Kst[$_]) Compute($_ <-chan $_) ($_ <-chan $_, $_ <-chan $_) {
  $$$
}'

Repository: cinar/indicator

Length of output: 2317


🏁 Script executed:

# Alternative: search for Compute in kst.go
rg -nA 30 'func.*Compute' trend/kst.go | head -80

Repository: cinar/indicator

Length of output: 969


🏁 Script executed:

# Check the Skip helper implementation
rg -nA 10 'func Skip' helper/ | head -30

Repository: cinar/indicator

Length of output: 1535


🏁 Script executed:

# Also check how SMA Compute works to understand channel length contracts
fd 'sma' trend/ --type f -x basename {}

Repository: cinar/indicator

Length of output: 94


🏁 Script executed:

# Check SMA Compute to verify channel length contract
rg -nA 20 'func.*Sma.*Compute' trend/sma.go | head -50

Repository: cinar/indicator

Length of output: 556


🏁 Script executed:

# Check the Duplicate helper function
rg -nA 20 'func Duplicate' helper/ | head -50

Repository: cinar/indicator

Length of output: 3420


🏁 Script executed:

# Check the Apply helper function used in SMA
rg -nA 15 'func Apply' helper/ | head -40

Repository: cinar/indicator

Length of output: 2654


🏁 Script executed:

# Let's verify the exact lines referenced in the review
sed -n '30,33p' trend/kst_test.go | cat -n
sed -n '99,102p' trend/kst_test.go | cat -n

Repository: cinar/indicator

Length of output: 346


🏁 Script executed:

# Check if there are additional test functions that also consume these channels
grep -n "for.*range actual" trend/kst_test.go

Repository: cinar/indicator

Length of output: 120


🏁 Script executed:

# Let's trace through the channel operations more carefully
# Check if there are other test files using paired channels to see patterns
fd '_test.go' trend/ -x basename {} | head -10

Repository: cinar/indicator

Length of output: 210


🏁 Script executed:

# Check if there's a test pattern used in similar paired-channel indicators
grep -l "actualSignal" trend/*_test.go

Repository: cinar/indicator

Length of output: 95


🏁 Script executed:

# Verify: does Skip maintain length parity with the original?
# Check what Skip does with count parameter
cat helper/skip.go | head -50

Repository: cinar/indicator

Length of output: 659


🏁 Script executed:

# Let's check if MACD has the same pattern and see how it handles the channels
sed -n '1,150p' trend/macd_test.go | grep -A 5 "for.*range"

Repository: cinar/indicator

Length of output: 245


🏁 Script executed:

# Let's examine the actual channel flow and understand the length contract
# Check the MovingSum implementation to understand how many values it outputs
rg -nA 25 'func.*MovingSum.*Compute' trend/

Repository: cinar/indicator

Length of output: 4751


🏁 Script executed:

# Let me trace through the channel length logic more carefully
# Check what values MovingSum actually outputs
sed -n '34,46p' trend/moving_sum.go

Repository: cinar/indicator

Length of output: 332


🏁 Script executed:

# Check the Operate helper function
rg -nA 20 'func Operate' helper/ | head -40

Repository: cinar/indicator

Length of output: 1500


🏁 Script executed:

# Verify the channel flow in KST one more time by checking line 164-169
sed -n '164,169p' trend/kst.go

Repository: cinar/indicator

Length of output: 257


🏁 Script executed:

# Check the Shift helper to understand channel length preservation
rg -nA 20 'func Shift' helper/ | head -40

Repository: cinar/indicator

Length of output: 2698


🏁 Script executed:

# Verify SMA output length matches input length by checking Apply
# Apply should preserve length since it just transforms values
sed -n '44,51p' trend/sma.go

Repository: cinar/indicator

Length of output: 242


🏁 Script executed:

# Now I need to verify the exact channel output lengths
# Let's trace through: what is maxIdle in KST?
sed -n '130,151p' trend/kst.go

Repository: cinar/indicator

Length of output: 542


🏁 Script executed:

# And check the default periods
sed -n '10,45p' trend/kst.go | grep 'Default'

Repository: cinar/indicator

Length of output: 888


🏁 Script executed:

# Let's understand ROC output length
rg -nA 20 'func.*Roc.*Compute' trend/roc.go | head -40

Repository: cinar/indicator

Length of output: 483


🏁 Script executed:

# Also need to understand how many values the test data has and what the expected output is
sed -n '15,45p' trend/kst_test.go

Repository: cinar/indicator

Length of output: 851


🏁 Script executed:

# Check the Add helper to confirm it maintains channel length parity
rg -nA 20 'func Add' helper/ | head -50

Repository: cinar/indicator

Length of output: 3488


🏁 Script executed:

# Let me also check if there's a test that validates the actual output lengths
tail -30 trend/kst_test.go

Repository: cinar/indicator

Length of output: 781


Use two-value receives to detect premature channel closure when pairing KST and signal streams.

Both loops receive from actualSignal without checking the ok flag. If the signal channel closes early (before the KST channel), the receive silently yields zero-values instead of erroring. Use two-value receives with explicit length validation:

Suggested fix pattern (apply to both loops)
-	for v := range actualKst {
-		kstSlice = append(kstSlice, v)
-		signalSlice = append(signalSlice, <-actualSignal)
-	}
+	for v := range actualKst {
+		signal, ok := <-actualSignal
+		if !ok {
+			t.Fatalf("signal closed before kst at index %d", len(kstSlice))
+		}
+
+		kstSlice = append(kstSlice, v)
+		signalSlice = append(signalSlice, signal)
+	}
+
+	if _, ok := <-actualSignal; ok {
+		t.Fatal("signal produced extra values beyond kst")
+	}

Applies to lines 30-33 and 99-102.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@trend/kst_test.go` around lines 30 - 33, The test currently pairs values from
actualKst and actualSignal using single-value receives which masks premature
channel closure; update both loops that iterate over actualKst (the block using
variables kstSlice and signalSlice and the similar block at lines ~99-102) to
perform two-value receives from actualSignal (s, ok := <-actualSignal), check
ok, and fail the test (e.g., t.Fatalf or equivalent) if ok is false before
appending, so a closed signal channel is detected rather than silently using
zero-values.

Comment on lines +37 to +53
for v := range kvoResult {
kvoSlice = append(kvoSlice, v)
signalSlice = append(signalSlice, <-signalResult)
}

if len(kvoSlice) == 0 {
t.Fatal("KVO produced no output")
}

for i, v := range kvoSlice {
if math.IsNaN(v) || math.IsInf(v, 0) {
t.Fatalf("KVO at index %d is NaN or Inf: %v", i, v)
}
}

t.Logf("KVO: %d values, Signal: %d values", len(kvoSlice), len(signalSlice))
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, locate the kvo_test.go file and verify helper usage
fd -type f "kvo_test.go" 2>/dev/null | head -5

Repository: cinar/indicator

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Check for helper function usage in kvo_test.go
rg -n 'helper\.(CheckEquals|RoundDigits|ReadFromCsvFile)' --type go 2>/dev/null || echo "No helper functions found in pattern search"

Repository: cinar/indicator

Length of output: 46374


🏁 Script executed:

#!/bin/bash
# Get the full content of the test file to understand its structure
fd -type f -name "kvo_test.go" -exec wc -l {} \;

Repository: cinar/indicator

Length of output: 229


🏁 Script executed:

#!/bin/bash
# Read the kvo_test.go file to see the actual implementation
fd -type f -name "kvo_test.go" -exec cat -n {} \;

Repository: cinar/indicator

Length of output: 229


🏁 Script executed:

# Read the kvo_test.go file to see the actual implementation
find . -type f -name "kvo_test.go" -o -name "kvo.go" | head -10

Repository: cinar/indicator

Length of output: 95


🏁 Script executed:

# Check for kvo files more directly
ls -la volume/ | grep -i kvo

Repository: cinar/indicator

Length of output: 179


🏁 Script executed:

# Read kvo_test.go with line numbers
find . -name "kvo_test.go" -exec cat -n {} \;

Repository: cinar/indicator

Length of output: 1903


Use standard test helpers and fix channel consumption to detect length mismatches.

This test bypasses the project's standard testing pattern used in all other volume indicators. It lacks deterministic assertions and uses manual checks instead of helper.CheckEquals. Additionally, consuming signalResult inside range kvoResult will hang if the channels diverge instead of failing with a clear error.

✅ Suggested fix

Replace the loop at lines 37-40 with explicit channel closing checks:

-	for v := range kvoResult {
-		kvoSlice = append(kvoSlice, v)
-		signalSlice = append(signalSlice, <-signalResult)
-	}
+	for {
+		kvoValue, kvoOk := <-kvoResult
+		signalValue, signalOk := <-signalResult
+
+		if kvoOk != signalOk {
+			t.Fatalf("kvo/signal length mismatch")
+		}
+		if !kvoOk {
+			break
+		}
+
+		kvoSlice = append(kvoSlice, kvoValue)
+		signalSlice = append(signalSlice, signalValue)
+	}

Then add proper assertions per guidelines: use helper.RoundDigits() before comparing float64 values, and use helper.CheckEquals() for equality checks instead of manual validation.

Per coding guidelines: Use helper.CheckEquals for assertions in tests and Use helper.RoundDigits for floating-point number comparison in tests.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for v := range kvoResult {
kvoSlice = append(kvoSlice, v)
signalSlice = append(signalSlice, <-signalResult)
}
if len(kvoSlice) == 0 {
t.Fatal("KVO produced no output")
}
for i, v := range kvoSlice {
if math.IsNaN(v) || math.IsInf(v, 0) {
t.Fatalf("KVO at index %d is NaN or Inf: %v", i, v)
}
}
t.Logf("KVO: %d values, Signal: %d values", len(kvoSlice), len(signalSlice))
}
for {
kvoValue, kvoOk := <-kvoResult
signalValue, signalOk := <-signalResult
if kvoOk != signalOk {
t.Fatalf("kvo/signal length mismatch")
}
if !kvoOk {
break
}
kvoSlice = append(kvoSlice, kvoValue)
signalSlice = append(signalSlice, signalValue)
}
if len(kvoSlice) == 0 {
t.Fatal("KVO produced no output")
}
for i, v := range kvoSlice {
if math.IsNaN(v) || math.IsInf(v, 0) {
t.Fatalf("KVO at index %d is NaN or Inf: %v", i, v)
}
}
t.Logf("KVO: %d values, Signal: %d values", len(kvoSlice), len(signalSlice))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@volume/kvo_test.go` around lines 37 - 53, The current test reads from
kvoResult and signalResult together (looping over kvoResult and consuming
signalResult inside), which can hang on mismatched channel lengths and uses
manual checks; change to drain each channel independently (e.g., range kvoResult
to append to kvoSlice until closed, then range signalResult to append to
signalSlice) so length mismatches fail cleanly, then replace manual NaN/Inf and
length checks with the project test helpers: use helper.RoundDigits(...) when
comparing float64 values and assert equality with helper.CheckEquals(...) for
expected counts/values; update the assertions around kvoSlice and signalSlice
and any float comparisons to call helper.RoundDigits before passing values to
helper.CheckEquals.

Comment on lines +395 to +409
```
Trend = +1 if High > High[1] and Low >= Low[1]
Trend = -1 if High <= High[1] and Low < Low[1]
Trend = 0 otherwise
VF = Volume * Trend
KVO = EMA(VF, shortPeriod) - EMA(VF, longPeriod)
Signal = EMA(KVO, signalPeriod)
```

Example:

```
kvo := volume.NewKvo[float64]()
kvoResult, signalResult := kvo.Compute(highs, lows, volumes)
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language identifiers to the new KVO fenced blocks.

The added KVO formula/example fences are unlabeled and trigger markdownlint MD040.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@volume/README.md` around lines 395 - 409, The fenced code blocks for the KVO
formula and the Go example are missing language identifiers; update the formula
block to use a neutral identifier (e.g., ```text) and the example block to use
the Go identifier (```go) so markdownlint MD040 is satisfied; specifically edit
the README's KVO section containing the formula block and the snippet that
creates NewKvo[float64]() and calls Compute(highs, lows, volumes) to add the
appropriate fenced-language tags.

@cinar cinar merged commit bf76642 into master Feb 25, 2026
6 checks passed
@cinar cinar deleted the implement-variadic-operate branch February 25, 2026 05:42
@github-actions github-actions bot locked and limited conversation to collaborators Feb 25, 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.

Implement Klinger Volume Oscillator (KVO)

2 participants