Skip to content

Add momentum indicator ROC#277

Merged
cinar merged 6 commits intocinar:masterfrom
hwde:feature/roc
Aug 17, 2025
Merged

Add momentum indicator ROC#277
cinar merged 6 commits intocinar:masterfrom
hwde:feature/roc

Conversation

@hwde
Copy link
Contributor

@hwde hwde commented Aug 13, 2025

Describe Request

Add momentum indicator ROC

Fixed #57

Change Type

What is the type of this change.

Summary by CodeRabbit

  • New Features

    • Added a Rate of Change (ROC) indicator with a configurable period (default: 9), streaming computation and an automatic warm-up (idle) period before outputs appear.
    • ROC exposes a readable string representation showing the configured period.
  • Tests

    • Added unit tests validating ROC on simple sequences and CSV reference data.
    • Tests cover rounding/alignment, default-period fallback, and string representation.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 13, 2025

Walkthrough

Adds a new generic ROC indicator type with default period, constructors, Compute stream using a ring buffer and helper utilities, IdlePeriod/String methods, and unit tests (vector and CSV-driven) validating outputs after skipping the idle period and rounding.

Changes

Cohort / File(s) Summary
ROC Indicator Implementation
trend/roc.go
Adds Roc[T helper.Float] with Period and DefaultRocPeriod = 9, constructors NewRoc / NewRocWithPeriod, Compute(values <-chan T) <-chan T implementing ROC via a ring buffer (helper.NewRing) and helper.Map/helper.Skip, and methods IdlePeriod() and String().
ROC Tests
trend/roc_test.go
Adds tests TestRocSimple, TestRocTestdata, TestRocFallbackPeriod, and TestRocToStringAndIdlePeriod. Tests use testdata/roc.csv and helper utilities (SliceToChan, ReadFromCsvFile, Duplicate, Map, Skip, RoundDigits, CheckEquals) to validate numeric outputs, fallback behavior, and string formatting.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Roc
  participant Ring as helper.NewRing
  participant Map as helper.Map
  participant Skip as helper.Skip
  participant Out as Output Channel

  Caller->>Roc: NewRoc / NewRocWithPeriod
  Caller->>Roc: Compute(values <-chan)
  Roc->>Ring: create ring buffer (Period)
  Roc->>Map: for each value: when ring full -> compute (value - prev) / prev
  Roc->>Ring: push current value
  Roc->>Skip: skip first IdlePeriod outputs
  Skip-->>Out: emit ROC stream
  Out-->>Caller: consume ROC values
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Assessment against linked issues

Objective Addressed Explanation
Implement ROC that measures percentage change between current price and price N periods ago [#57] Implementation computes fractional change (v - prev) / prev; issue wording asks for "percentage change" — it's unclear whether outputs should be scaled by 100.

Poem

I nibble numbers, hop and see,
Comparing now to what used to be.
Rings hold past, I compute with glee,
A tiny rate, a carrot for me,
Streaming changes, swift and free. 🥕

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 62c920f and 5b293a1.

📒 Files selected for processing (1)
  • trend/roc_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • trend/roc_test.go
✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

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: 1

🧹 Nitpick comments (1)
trend/roc.go (1)

66-69: Fix the string representation comment.

The comment incorrectly references "WMA" instead of "ROC".

-// String is the string representation of the WMA.
+// String is the string representation of the ROC.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8252ac7 and 9c266b8.

⛔ Files ignored due to path filters (1)
  • trend/testdata/roc.csv is excluded by !**/*.csv
📒 Files selected for processing (2)
  • trend/roc.go (1 hunks)
  • trend/roc_test.go (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
trend/roc_test.go (3)
trend/roc.go (2)
  • NewRoc (27-29)
  • Roc (21-24)
helper/skip.go (1)
  • Skip (15-30)
helper/csv.go (1)
  • ReadFromCsvFile (278-285)
trend/roc.go (1)
helper/skip.go (1)
  • Skip (15-30)
🔇 Additional comments (7)
trend/roc_test.go (2)

9-23: LGTM! Simple test correctly validates ROC calculation.

The test correctly sets up a simple scenario with a closing price stream and validates the ROC calculation with a period of 3. The expected values are correctly calculated and the test properly handles the idle period alignment.


25-51: LGTM! Data-driven test provides comprehensive coverage.

The test effectively validates the ROC indicator using CSV test data, properly handles channel duplication, and includes rounding to handle floating-point precision. The test structure follows established patterns in the codebase.

trend/roc.go (5)

13-16: LGTM! Default period aligns with common ROC usage.

The default period of 9 is a reasonable choice for the ROC indicator and is commonly used in technical analysis.


26-29: LGTM! Constructor follows established patterns.

The constructor correctly delegates to the parameterized version with the default period.


31-36: LGTM! Parameterized constructor is well-structured.

The constructor properly initializes the ROC struct with the given period parameter.


38-59: LGTM! ROC computation is correctly implemented.

The implementation correctly:

  • Uses a ring buffer to maintain the sliding window of values
  • Calculates ROC as (current - previous) / previous when sufficient data is available
  • Skips the idle period to align output properly
  • Handles the case when the window is not yet full

The logic matches the ROC indicator specification from the linked issue.


61-64: LGTM! Idle period correctly returns the configured period.

The implementation correctly returns the period as the idle period, which aligns with the ROC calculation needing that many periods of historical data.

@codecov-commenter
Copy link

codecov-commenter commented Aug 14, 2025

Codecov Report

❌ Patch coverage is 83.33333% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.67%. Comparing base (8252ac7) to head (2bac27c).

Files with missing lines Patch % Lines
trend/roc.go 83.33% 4 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master     #277   +/-   ##
=======================================
  Coverage   91.66%   91.67%           
=======================================
  Files         181      182    +1     
  Lines        6540     6570   +30     
=======================================
+ Hits         5995     6023   +28     
- Misses        484      486    +2     
  Partials       61       61           

☔ 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: 4

🧹 Nitpick comments (5)
trend/roc_test.go (2)

9-23: Add assertion for String() to cover and validate representation.

You can cheaply bump patch coverage and validate the public API by asserting the string format after setting Period.

Apply this diff:

  roc := NewRoc[float64]()
  roc.Period = 3
+ if s := roc.String(); s != "ROC(3)" {
+ 	t.Fatalf("unexpected String(): %s", s)
+ }

40-41: Assert IdlePeriod matches the default to improve coverage and guard API.

This will exercise IdlePeriod() and the default constant, addressing part of the Codecov gap.

Apply this diff:

  roc := NewRoc[float64]()
+ if roc.IdlePeriod() != DefaultRocPeriod {
+ 	t.Fatalf("unexpected IdlePeriod: %d", roc.IdlePeriod())
+ }
trend/roc.go (3)

18-21: Clarify ROC documentation and fractional vs. percentage form.

Issue #57 describes ROC as a percentage change. The implementation returns a fractional change (matching tests). Make this explicit to avoid confusion.

Apply this diff:

 // Roc represents the configuration parameters for calculating the Rate Of Change (ROC) indicator.
 //
-//	ROC = (Current Price - Price n periods ago) / Price n periods ago
+//	ROC = (Current Price - Price n periods ago) / Price n periods ago
+//
+// Note: This implementation returns a fractional change (e.g., 0.05 == 5%).
+// Multiply by 100 if you need the percentage form.

If you prefer percentage output by default (per the issue wording), we can update Compute to multiply by 100 and adjust tests/CSV accordingly. Confirm the intended convention for this repo.


38-39: Fix comment: no “signal line” is computed.

The method returns only ROC values.

Apply this diff:

-// Compute function takes a channel of numbers and computes the ROC and the signal line.
+// Compute calculates the Rate of Change (ROC) over the configured period.

66-69: Correct docstring to reference ROC (not WMA).

Copy/paste typo.

Apply this diff:

-// String is the string representation of the WMA.
+// String is the string representation of the ROC.
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between feb76e7 and 341ffc0.

⛔ Files ignored due to path filters (1)
  • trend/testdata/roc.csv is excluded by !**/*.csv
📒 Files selected for processing (2)
  • trend/roc.go (1 hunks)
  • trend/roc_test.go (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
trend/roc.go (2)
helper/skip.go (1)
  • Skip (15-30)
trend/aroon.go (2)
  • NewAroon (37-41)
  • Compute (45-67)
trend/roc_test.go (3)
trend/roc.go (2)
  • NewRoc (27-29)
  • Roc (21-24)
helper/skip.go (1)
  • Skip (15-30)
helper/csv.go (1)
  • ReadFromCsvFile (278-285)

Comment on lines +25 to +51
func TestRocTestdata(t *testing.T) {
type Data struct {
Close float64
Roc float64
}

input, err := helper.ReadFromCsvFile[Data]("testdata/roc.csv", true)
if err != nil {
t.Fatal(err)
}

inputs := helper.Duplicate(input, 2)
closing := helper.Map(inputs[0], func(d *Data) float64 { return d.Close })
expected := helper.Map(inputs[1], func(d *Data) float64 { return d.Roc })

roc := NewRoc[float64]()

actual := roc.Compute(closing)
actual = helper.RoundDigits(actual, 2)

expected = helper.Skip(expected, roc.IdlePeriod())

err = helper.CheckEquals(actual, expected)
if err != nil {
t.Fatal(err)
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding a targeted test for zero-division and NewRocWithPeriod constructor.

  • Division by zero is currently not guarded in roc.go; if p == 0, this test will catch the edge-case once the guard is added.
  • Explicitly tests NewRocWithPeriod to cover constructor and String().

Add this new test function:

func TestRocWithZeroDivisionAndConstructor(t *testing.T) {
	// previous value (n periods ago) is zero -> ROC should be 0 (no panic/inf)
	closing := helper.SliceToChan([]float64{0, 1, 2, 3})
	roc := NewRocWithPeriod[float64](3)

	if s := roc.String(); s != "ROC(3)" {
		t.Fatalf("unexpected String(): %s", s)
	}
	if roc.IdlePeriod() != 3 {
		t.Fatalf("unexpected IdlePeriod: %d", roc.IdlePeriod())
	}

	actual := roc.Compute(closing)
	expected := helper.SliceToChan([]float64{0})

	err := helper.CheckEquals(actual, expected)
	if err != nil {
		t.Fatal(err)
	}
}

This should close the remaining coverage gap Codecov reported (String() and NewRocWithPeriod()) and guard a critical numeric edge case.

🤖 Prompt for AI Agents
In trend/roc_test.go around lines 25 to 51, add a new unit test that exercises
NewRocWithPeriod and the zero-division edge-case: create a closing series where
the value from n periods ago is zero, instantiate roc with
NewRocWithPeriod[float64](3), assert roc.String() == "ROC(3)" and
roc.IdlePeriod() == 3, compute ROC over the input (use helper.SliceToChan) and
assert the result yields 0 (no panic/Inf) using helper.CheckEquals;
additionally, update roc.go to guard the division so when the previous value is
zero the function returns 0 for that ROC element instead of dividing by zero.

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: 0

🧹 Nitpick comments (4)
trend/roc.go (4)

18-21: Clarify “percentage vs. fraction” in the ROC formula comment.

Implementation returns a fractional ROC (no ×100). The PR/issue text says “percentage,” which could confuse users.

Consider updating the doc to explicitly state the fractional form and how to get percent:

-//	ROC = (Current Price - Price n periods ago) / Price n periods ago
+//	ROC = (Current Price - Price n periods ago) / Price n periods ago
+//
+// Note: Returns fractional ROC (e.g., 0.05 == 5%). Multiply by 100 if percentage is desired.

Please confirm this matches conventions used in other indicators in this repo. I can align related docs/tests if needed.


41-42: Fix copy/paste in Compute’s doc (“signal line” is not present).

Apply:

-// Compute function takes a channel of numbers and computes the ROC and the signal line.
+// Compute computes the ROC over the input stream.

48-53: Drop redundant ok check after IsFull().

If the ring is full, Get should consistently succeed; the extra ok check adds noise.

-		if window.IsFull() {
-			p, ok := window.Get()
-			if ok && p != 0 {
-				result = (value - p) / p
-			}
-		}
+		if window.IsFull() {
+			p, _ := window.Get()
+			if p != 0 {
+				result = (value - p) / p
+			}
+		}

If Get can still fail in a “full” ring per helper semantics, ignore this suggestion.


48-53: Add a test for the p == 0 branch to improve patch coverage and guard behavior.

Codecov reports a few uncovered lines here. A targeted test with period=1 and an initial zero will exercise the division-by-zero guard:

// In trend/roc_test.go
func TestRocDivisionByZeroGuard(t *testing.T) {
	roc := NewRocWithPeriod[float64](1)
	values := []float64{0, 1, 2}
	in := make(chan float64, len(values))
	for _, v := range values {
		in <- v
	}
	close(in)

	out := roc.Compute(in)

	var got []float64
	for v := range out {
		got = append(got, v)
	}
	// period=1 -> skip first element; expect two outputs
	// First computed step divides by previous=0 -> guarded to 0.0
	// Second step: (2-1)/1 = 1.0
	want := []float64{0.0, 1.0}
	if len(got) != len(want) {
		t.Fatalf("length mismatch: got %d, want %d", len(got), len(want))
	}
	for i := range want {
		if math.Abs(got[i]-want[i]) > 1e-9 {
			t.Fatalf("got[%d]=%v, want=%v", i, got[i], want[i])
		}
	}
}

I can add a companion test to assert warm-up skipping for a small period if you’d like.

Also applies to: 59-61

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4f7b97d and 62c920f.

📒 Files selected for processing (2)
  • trend/roc.go (1 hunks)
  • trend/roc_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • trend/roc_test.go
🧰 Additional context used
🧬 Code Graph Analysis (1)
trend/roc.go (1)
helper/skip.go (1)
  • Skip (15-30)
🔇 Additional comments (4)
trend/roc.go (4)

31-39: Good input validation for period.

Clamping period <= 0 to DefaultRocPeriod avoids undefined ring behavior. Matches prior review feedback.


45-57: Core ROC computation and warm-up handling look correct.

  • Using a ring to access the value n periods ago is appropriate.
  • Off-by-one is handled correctly with Skip(r.Period), so the first emitted value corresponds to the first valid ROC.
  • Guarding division by zero avoids Inf/NaN leakage during zero prices.

59-61: Idle period skipping is correct.

Skip(r.Period) cleanly removes the initial warm-up window from the output stream.


69-72: String() format is consistent and useful.

"ROC(%d)" aligns with other indicators and is test-friendly.

@cinar
Copy link
Owner

cinar commented Aug 17, 2025

Thank you very much!

@cinar cinar merged commit a3e7cbd into cinar:master Aug 17, 2025
3 checks passed
@github-actions github-actions bot locked and limited conversation to collaborators Aug 17, 2025
@hwde hwde deleted the feature/roc branch September 11, 2025 10:50
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.

Momentum: Price Rate Of Change Indicator (ROC)

3 participants