Skip to content

Conversation

hwde
Copy link
Contributor

@hwde hwde commented Aug 10, 2025

Describe Request

Introduce Window, MaxSince and MinSince on channels to hopefully fix #124 as suggested.

Fixed # (issue)

Change Type

What is the type of this change.

Summary by CodeRabbit

  • New Features

    • Streaming sliding-window utilities: per-window minima/maxima, persistence counters since extrema, a reverse-slice iterator, and a generic sliding-window transformer.
  • Bug Fixes

    • More accurate "since last high" and "since last low" calculations in trend indicators via specialized helpers.
  • Tests

    • Added unit tests for sliding-window behavior, extrema persistence logic, reverse-slice utility, and Aroon/trend validation.

Copy link

coderabbitai bot commented Aug 10, 2025

Walkthrough

Adds generic sliding-window utilities and derived helpers: Window and SlicesReverse, Highest/Lowest, MaxSince/MinSince, unit tests for these helpers, and updates Aroon to use MaxSince/MinSince for "since last high/low" calculations.

Changes

Cohort / File(s) Change Summary
Core sliding-window helpers
helper/window.go, helper/slices_reverse.go
Added Window[T any] to apply a function over a sliding window of a stream and SlicesReverse[T any] to iterate a slice in reverse from a split index.
Windowed extrema
helper/highest.go, helper/Lowest.go
Added Highest[T Number] and Lowest[T Number] that emit per-window maxima/minima via Window using slices.Max / slices.Min.
"Since" computations
helper/max_since.go, helper/min_since.go
Added MaxSince[T Number] and MinSince[T Number] to compute how many positions have elapsed since the current window's max/min began (per-window counts).
Unit tests for helpers
helper/window_test.go, helper/slices_reverse_test.go, helper/highest_test.go, helper/lowest_test.go, helper/max_since_test.go, helper/min_since_test.go
Added tests validating Window, SlicesReverse, Highest, Lowest, MaxSince, MinSince behavior and edge cases with representative sequences.
Aroon indicator changes & tests
trend/aroon.go, trend/aroon_test.go
Replaced previous generic Since usage with MaxSince / MinSince in Aroon computation and added tests comparing Aroon outputs to expected/reference sequences.

Sequence Diagram(s)

sequenceDiagram
    participant In as Input stream
    participant Win as helper.Window
    participant Ext as helper.Highest/Lowest
    participant Since as helper.MaxSince/MinSince
    participant Aroon as trend.Aroon
    participant Out as Aroon Up/Down

    In->>Win: stream elements
    Win->>Ext: apply per-window extrema func
    Ext-->>Win: window extrema stream
    Win->>Since: compute per-window "since" counts
    Since-->>Aroon: emit days-since-last-extrema
    Aroon-->>Out: compute and emit Aroon Up/Down
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Assessment against linked issues

Objective Addressed Explanation
Replace incorrect "since" logic in Aroon with correct "since last high/low" calculation (#124)
Implement sliding-window logic for max/min and since calculations used by Aroon (#124)
Provide generic helper functions usable across indicators (#124)

Poem

"I hop through windows, counting highs and lows,
I scan back softly where the last peak grows.
Max and Min I trace, then days since they shone,
Aroon sings truer as I nibble on the known.
Hop, compute, repeat — a rabbit's tiny code. 🐇"

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.

✨ 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

github-actions bot commented Aug 10, 2025

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@hwde
Copy link
Contributor Author

hwde commented Aug 10, 2025

I have read the CLA Document and I hereby sign the CLA

Copy link

@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)
helper/slices_reverse_test.go (3)

16-18: Improve failure messages to aid debugging

Include expected vs actual in the failure for quicker triage.

- if !slices.Equal(actual, expected) {
-   t.Fatal("not equal")
- }
+ if !slices.Equal(actual, expected) {
+   t.Fatalf("not equal: expected=%v actual=%v", expected, actual)
+ }

29-31: Likewise here: print expected vs actual on failure

Matching the first test improves consistency and debuggability.

- if !slices.Equal(actual, expected) {
-   t.Fatal("not equal")
- }
+ if !slices.Equal(actual, expected) {
+   t.Fatalf("not equal: expected=%v actual=%v", expected, actual)
+ }

8-19: Add coverage for early termination and pivot edge cases

Current tests don’t verify callback early-stop or boundary pivots (0/len). Adding these catches regressions in traversal order and stopping logic.

Add these tests:

func TestSlicesReverseEarlyStop(t *testing.T) {
  input := []int{1,2,3,4,5}
  // Expect only last two elements when stopping early
  expected := []int{5,4}
  actual := make([]int, 0, len(expected))
  count := 0
  SlicesReverse(input, 0, func(i int) bool {
    actual = append(actual, i)
    count++
    return count < 2 // stop after two elements
  })
  if !slices.Equal(actual, expected) {
    t.Fatalf("not equal: expected=%v actual=%v", expected, actual)
  }
}

func TestSlicesReversePivotAtLen(t *testing.T) {
  input := []int{1,2,3,4}
  expected := []int{4,3,2,1} // pivot=len behaves like pivot=0
  actual := make([]int, 0, len(input))
  SlicesReverse(input, len(input), func(i int) bool {
    actual = append(actual, i)
    return true
  })
  if !slices.Equal(actual, expected) {
    t.Fatalf("not equal: expected=%v actual=%v", expected, actual)
  }
}

Also applies to: 21-32

helper/highest.go (1)

12-14: Mark unused callback parameter to clarify intent

Use underscore for the unused index parameter to avoid linter warnings and improve readability.

-return Window(c, func(s []T, i int) T {
-  return slices.Max(s)
+return Window(c, func(s []T, _ int) T {
+  return slices.Max(s)
 }, w)
helper/window_test.go (1)

7-20: Optional: use t.Run and descriptive messages for readability

Table-driven subtests with descriptive names ease maintenance and failure triage. Current checks rely on CheckEquals error text; adding context can help.

📜 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 33d212d.

⛔ Files ignored due to path filters (1)
  • trend/testdata/aroon.csv is excluded by !**/*.csv
📒 Files selected for processing (13)
  • helper/Lowest.go (1 hunks)
  • helper/highest.go (1 hunks)
  • helper/highest_test.go (1 hunks)
  • helper/lowest_test.go (1 hunks)
  • helper/max_since.go (1 hunks)
  • helper/max_since_test.go (1 hunks)
  • helper/min_since.go (1 hunks)
  • helper/min_since_test.go (1 hunks)
  • helper/slices_reverse.go (1 hunks)
  • helper/slices_reverse_test.go (1 hunks)
  • helper/window.go (1 hunks)
  • helper/window_test.go (1 hunks)
  • trend/aroon.go (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
helper/max_since_test.go (3)
helper/max_since.go (1)
  • MaxSince (14-31)
helper/bst_test.go (2)
  • TestMinAndMax (59-79)
  • TestEmptyMax (90-97)
helper/sync_test.go (1)
  • TestSync (13-33)
helper/window.go (2)
helper/shift.go (1)
  • Shift (15-27)
helper/skip.go (1)
  • Skip (15-30)
🔇 Additional comments (7)
helper/window.go (1)

20-30: The sliding window logic is correctly implemented.

The circular buffer approach efficiently manages memory while maintaining the sliding window. The offset parameter correctly indicates the start of the current window within the circular buffer, and the initial phase properly handles partial windows.

helper/max_since_test.go (1)

7-16: LGTM! Test data and logic are correct.

The test correctly validates the MaxSince function with appropriate input data and expected outputs. The expected values match the correct "since last maximum" calculations for a sliding window of size 3.

helper/min_since_test.go (1)

5-14: LGTM! Test data and logic are correct.

The test correctly validates the MinSince function with appropriate input data and expected outputs. The expected values match the correct "since last minimum" calculations for a sliding window of size 3.

helper/lowest_test.go (1)

11-21: LGTM! Test data and logic are correct.

The test correctly validates the Lowest function with appropriate input data and expected outputs. The expected values match the correct minimum calculations for a sliding window of size 3.

helper/Lowest.go (1)

11-15: LGTM! Clean and efficient implementation.

The Lowest function correctly leverages the generic Window function and slices.Min to compute sliding window minimums. The implementation is concise and follows the established pattern.

trend/aroon.go (1)

49-50: Excellent fix for the Aroon indicator calculation issue.

This change directly addresses issue #124 by replacing the generic helper.Since function with specialized helper.MaxSince and helper.MinSince functions. The new functions correctly calculate the number of days since the last maximum/minimum within the specified period, rather than when the value last changed. This ensures accurate Aroon Up and Aroon Down calculations.

helper/highest_test.go (1)

11-21: LGTM! Test data and logic are correct.

The test correctly validates the Highest function with appropriate input data and expected outputs. The expected values match the correct maximum calculations for a sliding window of size 3.

Comment on lines +15 to +30
return Window(c, func(w []T, i int) T {
since := 0
found := false
m := slices.Max(w)
SlicesReverse(w, i, func(n T) bool {
if found && n < m {
return false
}
since++
if n == m {
found = true
}
return true
})
return T(since - 1)
}, w)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Correctness: off-by-run when max repeats at the end; returns wrong “since last high”

Current logic counts the entire trailing run of maxima and subtracts 1, which breaks Aroon when the maximum repeats at the window end. Example window [3, 5, 5] should yield 0 (max occurred now), but this returns 1.

Fix: count steps until the first encounter of the maximum from the end, then stop.

 func MaxSince[T Number](c <-chan T, w int) <-chan T {
-  return Window(c, func(w []T, i int) T {
-    since := 0
-    found := false
-    m := slices.Max(w)
-    SlicesReverse(w, i, func(n T) bool {
-      if found && n < m {
-        return false
-      }
-      since++
-      if n == m {
-        found = true
-      }
-      return true
-    })
-    return T(since - 1)
-  }, w)
+  return Window(c, func(win []T, i int) T {
+    m := slices.Max(win)
+    steps := 0
+    SlicesReverse(win, i, func(n T) bool {
+      if n == m {
+        return false // stop at the most recent occurrence of max
+      }
+      steps++
+      return true
+    })
+    return T(steps)
+  }, w)
 }

Consider adding unit tests covering:

  • Repeated max at the end: expect 0.
  • Max one step back: expect 1.
  • Max at farthest position in the window: expect w-1.
🤖 Prompt for AI Agents
In helper/max_since.go around lines 15 to 30, the current logic incorrectly
counts the entire trailing run of maximum values and subtracts one, causing an
off-by-one error when the maximum repeats at the end of the window. To fix this,
modify the counting logic to increment steps only until the first encounter of
the maximum from the end, then stop counting immediately. Additionally, add unit
tests to cover cases where the max repeats at the end (expect 0), max is one
step back (expect 1), and max is at the farthest position in the window (expect
w-1).

Comment on lines +14 to +29
return Window(c, func(w []T, i int) T {
since := 0
found := false
m := slices.Min(w)
SlicesReverse(w, i, func(n T) bool {
if found && n > m {
return false
}
since++
if n == m {
found = true
}
return true
})
return T(since - 1)
}, w)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Correctness: off-by-run when min repeats at the end; returns wrong “since last low”

Same issue as MaxSince. For window [1, 1, 3], the correct “since last low” is 0 (min occurred now), but current code returns 1.

Fix by stopping at the first encounter of the minimum from the end.

 func MinSince[T Number](c <-chan T, w int) <-chan T {
-  return Window(c, func(w []T, i int) T {
-    since := 0
-    found := false
-    m := slices.Min(w)
-    SlicesReverse(w, i, func(n T) bool {
-      if found && n > m {
-        return false
-      }
-      since++
-      if n == m {
-        found = true
-      }
-      return true
-    })
-    return T(since - 1)
-  }, w)
+  return Window(c, func(win []T, i int) T {
+    m := slices.Min(win)
+    steps := 0
+    SlicesReverse(win, i, func(n T) bool {
+      if n == m {
+        return false // stop at the most recent occurrence of min
+      }
+      steps++
+      return true
+    })
+    return T(steps)
+  }, w)
 }

Please add complementary tests mirroring the MaxSince cases for repeated minima.

📝 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
return Window(c, func(w []T, i int) T {
since := 0
found := false
m := slices.Min(w)
SlicesReverse(w, i, func(n T) bool {
if found && n > m {
return false
}
since++
if n == m {
found = true
}
return true
})
return T(since - 1)
}, w)
func MinSince[T Number](c <-chan T, w int) <-chan T {
return Window(c, func(win []T, i int) T {
m := slices.Min(win)
steps := 0
SlicesReverse(win, i, func(n T) bool {
if n == m {
return false // stop at the most recent occurrence of min
}
steps++
return true
})
return T(steps)
}, w)
}
🤖 Prompt for AI Agents
In helper/min_since.go around lines 14 to 29, the function incorrectly
calculates the "since last low" value when the minimum value repeats at the end
of the window, causing an off-by-one error. To fix this, modify the loop to stop
immediately upon encountering the first minimum value from the end, ensuring the
count reflects the correct distance. Additionally, add test cases similar to
those for MaxSince that cover scenarios with repeated minimum values to verify
correctness.

Copy link

@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 (1)
trend/aroon_test.go (1)

49-54: Consider reusing the existing AroonData struct.

The test defines a local AroonData struct that's identical to the one in the existing TestAroon function (lines 15-20). This creates code duplication.

Consider moving the AroonData struct to package level or a test helper to avoid duplication:

+type AroonData struct {
+	High float64
+	Low  float64
+	Up   float64
+	Down float64
+}
+
 func TestAroon(t *testing.T) {
-	type AroonData struct {
-		High float64
-		Low  float64
-		Up   float64
-		Down float64
-	}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 33d212d and e82397e.

📒 Files selected for processing (1)
  • trend/aroon_test.go (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
trend/aroon_test.go (2)
trend/aroon.go (1)
  • NewAroon (37-41)
helper/skip.go (1)
  • Skip (15-30)
🔇 Additional comments (5)
trend/aroon_test.go (5)

46-48: Good documentation with external reference.

The comment provides clear context by referencing the Rust implementation being compared against, which helps with test maintainability and verification.


55-71: Test data looks comprehensive and well-structured.

The test data covers the expected patterns:

  • Initial "None" cases (lines 56-60) where no meaningful Aroon values can be calculated
  • Transition period with valid Up/Down calculations
  • Good coverage of different Aroon value ranges (0, 20, 40, 60, 80, 100)

The data structure matches the referenced Rust implementation, ensuring accurate cross-validation.


73-84: Test setup follows established patterns correctly.

The test properly:

  • Uses helper.SliceToChan to convert test data to channels
  • Creates a new Aroon instance with default configuration
  • Uses helper.Duplicate to create multiple input streams
  • Maps the channels to extract high/low values
  • Skips the initial period-1 values for expected results alignment

This matches the pattern used in the existing TestAroon function.


85-91: Test execution and validation implemented correctly.

The test properly:

  • Calls the aroon.Compute method with high and low channels
  • Uses helper.CheckEquals to compare actual vs expected results for both Up and Down channels
  • Handles errors appropriately with t.Fatal

This follows the same validation pattern as the existing test.


46-91: Verified Test Data Matches Rust Reference

The hard-coded expected values in TestAroonCompareRust have been confirmed to exactly match the assertions in sabinchitrakar/aroon-rs’s docs/aroon.md. No updates needed.

Copy link

@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

♻️ Duplicate comments (1)
helper/window.go (1)

15-18: Input validation properly addresses the previous concern.

The addition of window size validation correctly handles the edge case identified in the past review comment. Returning a closed channel for invalid window sizes is a clean approach that prevents panics and provides predictable behavior.

🧹 Nitpick comments (1)
helper/max_since_test.go (1)

7-27: Consider additional test coverage for robustness.

While the current tests effectively validate the core MaxSince functionality, consider adding tests for:

  • Different window sizes (1, 2, larger values)
  • Edge cases like empty input or single elements
  • Window size larger than input sequence

This would enhance robustness, though the existing coverage is adequate for the primary use cases.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between e82397e and 3ab1515.

📒 Files selected for processing (4)
  • helper/max_since_test.go (1 hunks)
  • helper/min_since_test.go (1 hunks)
  • helper/slices_reverse.go (1 hunks)
  • helper/window.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • helper/min_since_test.go
  • helper/slices_reverse.go
🧰 Additional context used
🧬 Code Graph Analysis (1)
helper/max_since_test.go (1)
helper/max_since.go (1)
  • MaxSince (14-31)
🔇 Additional comments (5)
helper/window.go (3)

20-36: Solid circular buffer implementation with correct offset calculation.

The sliding window implementation correctly handles both the initial filling phase and the full window phase. The offset calculation (n+1)%w properly indicates the logical start of the window within the circular buffer, enabling the callback function to reconstruct the correct element order when needed.


20-21: Excellent goroutine lifecycle management.

The use of defer close(r) ensures proper cleanup, and the channel reading pattern correctly detects input channel closure for clean termination. This follows Go best practices and prevents goroutine leaks.

Also applies to: 25-26


7-11: Clear and helpful documentation.

The function documentation effectively explains the sliding window behavior and includes the important note about slice ordering and offset usage, which is crucial for users to understand the circular buffer implementation.

helper/max_since_test.go (2)

7-16: Well-designed test cases with correct expected values.

Both test functions cover important scenarios:

  • TestMaxSince tests a comprehensive sequence with various patterns (increasing, decreasing, duplicates)
  • TestMaxSinceAtEnd specifically validates the edge case where multiple equal maximums appear consecutively

The expected results correctly reflect the MaxSince algorithm behavior, which counts positions back to the first occurrence of the maximum within each sliding window.

Also applies to: 18-27


8-15: Clean testing methodology using appropriate helper functions.

The tests properly utilize SliceToChan and CheckEquals helper functions to work with the channel-based API. The use of t.Fatal provides clear failure reporting, and the testing pattern is consistent and follows Go best practices.

Also applies to: 19-26

@hwde hwde force-pushed the feature/fix-aroon branch from 3ab1515 to 92c40ae Compare August 12, 2025 04:28
Copy link

@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

♻️ Duplicate comments (1)
helper/max_since.go (1)

15-30: Fix off-by-run: duplicates of the maximum at the end should yield 0, not 1

Current logic counts the entire trailing run of maxima and subtracts 1. This breaks Aroon when the max repeats at the window end (e.g., [3,5,5] yields 1 instead of 0).

-func MaxSince[T Number](c <-chan T, w int) <-chan T {
-	return Window(c, func(w []T, i int) T {
-		since := 0
-		found := false
-		m := slices.Max(w)
-		SlicesReverse(w, i, func(n T) bool {
-			if found && n < m {
-				return false
-			}
-			since++
-			if n == m {
-				found = true
-			}
-			return true
-		})
-		return T(since - 1)
-	}, w)
-}
+func MaxSince[T Number](c <-chan T, w int) <-chan T {
+	return Window(c, func(win []T, i int) T {
+		m := slices.Max(win)
+		steps := 0
+		SlicesReverse(win, i, func(n T) bool {
+			if n == m {
+				return false // stop at the most recent occurrence of max
+			}
+			steps++
+			return true
+		})
+		return T(steps)
+	}, w)
+}

Consider adding unit tests covering:

  • Repeated max at the end: expect 0.
  • Max one step back: expect 1.
  • Max at farthest position (full window): expect w-1.
🧹 Nitpick comments (5)
helper/window.go (3)

7-11: Clarify and tighten the Window doc comment

The current phrasing is grammatically off and ambiguous about the meaning of index i. Recommend rewording for clarity.

-// Window returns a channel that emits the passed function result
-// within a sliding window of size w from the input channel c.
-// Note: the slice is in the same order than in source channel
-// but the 1st element may not be 0, use modulo window size if
-// order is important.
+// Window returns a channel that emits f applied to a sliding window of size w over c.
+// For the initial (partial) windows, f receives a slice in source order and i=0.
+// Once the window is full, f receives the circular buffer backing array and i points
+// to the logical start (use modulo indexing with i for order-sensitive logic).

13-13: Buffer the output channel to match input channel capacity to reduce blocking

Other helpers (e.g., Shift, Skip) preserve capacity using cap(c). Doing the same here avoids unnecessary blocking and aligns with existing patterns.

-	r := make(chan T)
+	r := make(chan T, cap(c))

25-35: Simplify the loop; avoid incrementing n after input channel closes

More idiomatic and avoids the extra n increment on a closed channel path.

-		for ok := true; ok; {
-			if h[n], ok = <-c; ok {
-				if cnt < w {
-					cnt++
-					r <- f(h[:cnt], 0)
-				} else {
-					r <- f(h, (n+1)%w)
-				}
-			}
-			n = (n + 1) % w
-		}
+		for v := range c {
+			h[n] = v
+			if cnt < w {
+				cnt++
+				r <- f(h[:cnt], 0)
+			} else {
+				r <- f(h, (n+1)%w)
+			}
+			n = (n + 1) % w
+		}
helper/max_since.go (1)

11-13: Clarify MaxSince semantics in the doc comment

Make it explicit we count since the most recent maximum (Aroon-compatible).

-// MaxSince returns a channel of T indicating since when
-// (number of previous values) the respective value was the maximum
-// within the window of size w.
+// MaxSince returns a channel with the number of periods since the most recent
+// maximum within the last w observations (0 if the current observation is the max).
helper/window_test.go (1)

1-64: Add tests for invalid window sizes and for windows larger than the input

Edge cases strengthen confidence and prevent regressions for w<=0 and w>len(input).

@@
 func TestWindowIndexElement(t *testing.T) {
@@
 }
+
+func TestWindowInvalidSizes(t *testing.T) {
+	input := SliceToChan([]int{1, 2, 3})
+	// w = 0
+	out0 := Window(input, func(v []int, i int) int { return 42 }, 0)
+	if err := CheckEquals(out0, SliceToChan([]int{})); err != nil {
+		t.Fatal(err)
+	}
+	// w < 0
+	outNeg := Window(SliceToChan([]int{1, 2, 3}), func(v []int, i int) int { return 42 }, -1)
+	if err := CheckEquals(outNeg, SliceToChan([]int{})); err != nil {
+		t.Fatal(err)
+	}
+}
+
+func TestWindowLargerThanInput(t *testing.T) {
+	input := SliceToChan([]int{1, 2})
+	// With w > len(input), only partial windows exist.
+	expected := SliceToChan([]int{1, 3})
+	out := Window(input, func(v []int, i int) int {
+		sum := 0
+		for _, n := range v {
+			sum += n
+		}
+		return sum
+	}, 5)
+	if err := CheckEquals(out, expected); err != nil {
+		t.Fatal(err)
+	}
+}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 3ab1515 and 92c40ae.

⛔ Files ignored due to path filters (1)
  • trend/testdata/aroon.csv is excluded by !**/*.csv
📒 Files selected for processing (14)
  • helper/Lowest.go (1 hunks)
  • helper/highest.go (1 hunks)
  • helper/highest_test.go (1 hunks)
  • helper/lowest_test.go (1 hunks)
  • helper/max_since.go (1 hunks)
  • helper/max_since_test.go (1 hunks)
  • helper/min_since.go (1 hunks)
  • helper/min_since_test.go (1 hunks)
  • helper/slices_reverse.go (1 hunks)
  • helper/slices_reverse_test.go (1 hunks)
  • helper/window.go (1 hunks)
  • helper/window_test.go (1 hunks)
  • trend/aroon.go (1 hunks)
  • trend/aroon_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (10)
  • trend/aroon.go
  • helper/highest_test.go
  • helper/Lowest.go
  • helper/lowest_test.go
  • helper/max_since_test.go
  • helper/highest.go
  • trend/aroon_test.go
  • helper/slices_reverse.go
  • helper/slices_reverse_test.go
  • helper/min_since.go
🧰 Additional context used
🧬 Code Graph Analysis (3)
helper/min_since_test.go (1)
helper/min_since.go (1)
  • MinSince (13-30)
helper/window_test.go (1)
helper/window.go (1)
  • Window (12-39)
helper/max_since.go (5)
helper/window.go (1)
  • Window (12-39)
helper/slices_reverse.go (1)
  • SlicesReverse (6-21)
helper/sync.go (1)
  • CommonPeriod (24-26)
helper/shift.go (1)
  • Shift (15-27)
helper/skip.go (1)
  • Skip (15-30)
🔇 Additional comments (5)
helper/min_since_test.go (2)

16-25: Ignore incorrect expectation update for trailing minima

The current MinSince implementation scans the entire window when the minimum value repeats at the end, so it returns windowSize–1 for a run of equal minima. For input [48, 50, 50, 50, 49, 49, 49] with w=3, you get:

• Window 1–3 ([48], [48,50], [48,50,50]) → 0, 1, 2
• Window 2–4 ([50,50,50]) → 2 (since all three are equal minima)
• Windows ending in 49 ([50,50,49], [50,49,49], [49,49,49]) → 0, 1, 2

The existing expected output

SliceToChan([]int{0, 1, 2, 2, 0, 1, 2})

matches this behavior, so no test change is needed.

Likely an incorrect or invalid review comment.


27-36: No changes needed for MinSinceFromStart test

The existing test and implementation are correct and consistent with the intended semantics:
– MinSince counts how many values have passed since the last time the window’s minimum occurred, including repeated minima.
– In the partial window [1, 1], the second “1” indeed has its previous minimum one step back, yielding 1.
– Changing the expectation to [0, 0, 1] or altering the scanning logic would break the other MinSince tests (e.g., repeated‐min sequences in TestMinSince and TestMinSinceAtEnd).

Please disregard the proposed diff and implementation change.

Likely an incorrect or invalid review comment.

helper/window_test.go (3)

7-20: LGTM: Validates 1-sized sliding windows correctly

Good coverage of the partial-window behavior and using i=0 for initial windows.


22-39: LGTM: Confirms aggregation over partial and full windows

Sum behavior across window growth and steady-state is exercised well.


41-63: LGTM: Index semantics of the circular buffer are well tested

The i parameter and element selection are validated thoroughly.

Comment on lines +5 to +14
func TestMinSince(t *testing.T) {
input := SliceToChan([]int{48, 50, 50, 50, 49, 49, 51})
expected := SliceToChan([]int{0, 1, 2, 2, 0, 1, 2})
actual := MinSince(input, 3)

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

@coderabbitai coderabbitai bot Aug 12, 2025

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Tests encode “first occurrence of min” (w-1 when all equal), not “most recent min” required by Aroon

Per issue #124 and Aroon’s definition, “since” should be counted from the most recent minimum within the window (0 if current point is the min). The current expected values reflect counting from the earliest occurrence. Update expectations accordingly.

-	expected := SliceToChan([]int{0, 1, 2, 2, 0, 1, 2})
+	// Most recent-min semantics: count steps back to the latest min in the window.
+	expected := SliceToChan([]int{0, 1, 2, 0, 0, 0, 1})

If desired, I can open a follow-up to adjust helper/MinSince to match these semantics (mirroring the MaxSince fix) and extend tests.


🏁 Script executed:

#!/bin/bash
set -e

# Locate the MinSince implementation
rg -n -C5 "func MinSince" --color=never

Length of output: 625


🏁 Script executed:

#!/bin/bash
set -e

# Show the full Window function
rg -n -C20 "func Window" --color=never

# Show the full SlicesReverse implementation
rg -n -C20 "func SlicesReverse" --color=never

Length of output: 2548


Update MinSince test to use most-recent-min semantics

The TestMinSince currently counts from the first window minimum (yielding 2 when all values are equal). Per Aroon’s definition, “since” must reset to 0 whenever the current value is the window minimum and otherwise count bars since the most recent minimum.

• helper/min_since_test.go: in TestMinSince, update the expected channel to reflect most-recent‐min semantics.

-	expected := SliceToChan([]int{0, 1, 2, 2, 0, 1, 2})
+	// most-recent-min semantics: number of bars since the latest window minimum (0 if current is the min)
+	expected := SliceToChan([]int{0, 1, 2, 0, 0, 0, 1})

You may also open a follow-up to adjust helper/MinSince (mirroring the MaxSince fix) and extend tests accordingly.

📝 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
func TestMinSince(t *testing.T) {
input := SliceToChan([]int{48, 50, 50, 50, 49, 49, 51})
expected := SliceToChan([]int{0, 1, 2, 2, 0, 1, 2})
actual := MinSince(input, 3)
err := CheckEquals(actual, expected)
if err != nil {
t.Fatal(err)
}
}
func TestMinSince(t *testing.T) {
input := SliceToChan([]int{48, 50, 50, 50, 49, 49, 51})
// most-recent-min semantics: number of bars since the latest window minimum (0 if current is the min)
expected := SliceToChan([]int{0, 1, 2, 0, 0, 0, 1})
actual := MinSince(input, 3)
err := CheckEquals(actual, expected)
if err != nil {
t.Fatal(err)
}
}
🤖 Prompt for AI Agents
In helper/min_since_test.go around lines 5 to 14, the TestMinSince expected
values use first-window-min semantics; update the expected channel to follow
most-recent-min semantics so results are [0,1,2,0,0,0,1] for input
[]int{48,50,50,50,49,49,51} with window 3 (i.e., reset to 0 when the current
value is the window minimum and otherwise count bars since the most recent
minimum).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is hard to find really an exact definition how to calculate MinSince and MaxSince. A explanation of on old R implementation (https://github.com/sabinchitrakar/aroon-rs/blob/master/docs/aroon.md) notes:

Aroon up (down) is the elapsed time, expressed as a percentage, between today
and the highest (lowest) price in the last \code{n} periods. If today's
price is a new high (low) Aroon up (down) will be 100. Each subsequent period
without another new high (low) causes Aroon up (down) to decrease by (1 /
\code{n}) x 100.

So another test function of Aroon() might follow that:

func TestAroonValidateDefinition(t *testing.T) {
	// compares to: https://github.com/sabinchitrakar/aroon-rs/blob/master/docs/aroon.md
	type AroonData struct {
		High float64
		Low  float64
		Up   float64
		Down float64
	}
	var testData = []AroonData{
		{High: 11, Low: 9, Up: 0, Down: 0},    // None
		{High: 10, Low: 9, Up: 0, Down: 0},    // None
		{High: 9, Low: 9, Up: 100, Down: 100}, // None
		{High: 9, Low: 9, Up: 67, Down: 67},   // None
		{High: 9, Low: 9, Up: 33, Down: 33},   // None
	}

	input := helper.SliceToChan[AroonData](testData)
	aroon := &trend.Aroon[float64]{
		Period: 3,
	}

	inputs := helper.Duplicate(input, 4)
	high := helper.Map(inputs[0], func(row AroonData) float64 { return row.High })
	low := helper.Map(inputs[1], func(row AroonData) float64 { return row.Low })
	expectedUp := helper.Map(inputs[2], func(row AroonData) float64 { return row.Up })
	expectedDown := helper.Map(inputs[3], func(row AroonData) float64 { return row.Down })

	expectedUp = helper.Skip(expectedUp, aroon.Period-1)
	expectedDown = helper.Skip(expectedDown, aroon.Period-1)

	actualUp, actualDown := aroon.Compute(high, low)

	err := helper.CheckEquals(actualUp, expectedUp, actualDown, expectedDown)
	if err != nil {
		t.Fatal(err)
	}
}

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

@hwde Thank you for that excellent clarification! The R implementation reference you found provides exactly the definition we needed to resolve the MinSince/MaxSince semantics question.

Your quote is particularly illuminating:

"If today's price is a new high (low) Aroon up (down) will be 100. Each subsequent period without another new high (low) causes Aroon up (down) to decrease by (1/n) x 100."

This definitively confirms that Aroon should use "most recent" semantics - when the current value is the window high/low, it should return 0 periods since (resulting in 100% Aroon value), and otherwise count from the most recent occurrence.

Your proposed TestAroonValidateDefinition test case would be an excellent addition to validate the implementation against this authoritative definition. It provides concrete expected values that can serve as a reference benchmark.


🏁 Script executed:

#!/bin/bash
# Let's verify if the current implementation already handles this correctly by checking the existing Aroon test
cat trend/aroon_test.go

Length of output: 3098


Perfect! I can see that there's already a TestAroonCompareRust function that validates against the exact same Rust implementation reference you mentioned. This is excellent - it shows the current implementation is already being tested against that authoritative source.

Looking at the existing test data in TestAroonCompareRust, I can see the correct behavior patterns:

  • When a new high occurs (like 88.00 in the data), AroonUp jumps to 100
  • When a new low occurs, AroonDown becomes 100
  • Values properly decrease over time when no new extremes are found

Your proposed TestAroonValidateDefinition would still be a valuable addition because:

  1. It uses simpler, more predictable data (repeated 9s) making it easier to manually verify
  2. It serves as an educational example of the core Aroon logic
  3. It provides additional test coverage with different edge cases

The fact that TestAroonCompareRust is already passing suggests that the current MaxSince/MinSince implementation is likely working correctly with the "most recent" semantics. Your test case would be a nice complement to validate this behavior with clearer, more obvious data patterns.

Would you like to add your TestAroonValidateDefinition test case to provide additional validation coverage?


✏️ Learnings added
Learnt from: hwde
PR: cinar/indicator#276
File: helper/min_since_test.go:5-14
Timestamp: 2025-08-12T05:34:50.485Z
Learning: The Aroon indicator measures elapsed time as a percentage between today and the highest/lowest price in the last n periods. When today's price is a new high/low, Aroon should be 100. Each subsequent period without another new high/low causes Aroon to decrease by (1/n) x 100. This confirms that MinSince/MaxSince should use "most recent" semantics, returning 0 when current value is the window extremum.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@hwde hwde force-pushed the feature/fix-aroon branch from 87195f1 to 3c11e79 Compare August 12, 2025 12:50
add highest and lowest

add window, use it from highest and lowest

add Max/MinSince with helper Window and SlicesReverse

change aroon indicator as suggested by cinar#124

add a another test for Aroon

applied some ai suggesttions

update aroon strategy test data to match new actions

add another Aroon Test inspired by an R implementation

Update testdata to fix depending test of no_loss_strategy

fix depending testdata
@hwde hwde force-pushed the feature/fix-aroon branch from 50d7785 to 705d549 Compare August 14, 2025 16:12
@codecov-commenter
Copy link

codecov-commenter commented Aug 14, 2025

Codecov Report

❌ Patch coverage is 90.66667% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 91.73%. Comparing base (8252ac7) to head (2564e6d).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
helper/window.go 81.81% 3 Missing and 1 partial ⚠️
helper/slices_reverse.go 76.92% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #276      +/-   ##
==========================================
+ Coverage   91.66%   91.73%   +0.06%     
==========================================
  Files         181      188       +7     
  Lines        6540     6643     +103     
==========================================
+ Hits         5995     6094      +99     
- Misses        484      487       +3     
- Partials       61       62       +1     

☔ 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

@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: 3

♻️ Duplicate comments (1)
helper/slices_reverse.go (1)

8-10: Bounds checks addressed — good defensive programming

Early return on empty slice and out-of-range i prevents panics and undefined traversal.

🧹 Nitpick comments (7)
helper/highest_test.go (2)

12-15: Broaden coverage with table-driven tests (multiple windows, edge patterns).

Add more cases to harden the contract: window=1, window>len(input), strictly increasing/decreasing sequences, negatives, and duplicate plateaus. This will guard against regressions in Window/Highest.

Here’s a compact table-driven test you can drop alongside the current one:

func TestHighest_Table(t *testing.T) {
	t.Parallel()
	type tc struct {
		name   string
		in     []int
		w      int
		expect []int
	}
	for _, c := range []tc{
		{
			name:   "w=1 returns input",
			in:     []int{3, 2, 5, 1},
			w:      1,
			expect: []int{3, 2, 5, 1},
		},
		{
			name:   "w>len(input) behaves like growing window",
			in:     []int{1, 4, 2},
			w:      10,
			expect: []int{1, 4, 4},
		},
		{
			name:   "strictly increasing sequence",
			in:     []int{1, 2, 3, 4},
			w:      3,
			expect: []int{1, 2, 3, 4},
		},
		{
			name:   "strictly decreasing sequence",
			in:     []int{9, 7, 5, 3},
			w:      3,
			expect: []int{9, 9, 9, 7},
		},
		{
			name:   "duplicates within window",
			in:     []int{2, 5, 5, 1, 5},
			w:      3,
			expect: []int{2, 5, 5, 5, 5},
		},
		{
			name:   "negative values",
			in:     []int{-5, -2, -3, -1},
			w:      2,
			expect: []int{-5, -2, -2, -1},
		},
	} {
		t.Run(c.name, func(t *testing.T) {
			inCh := SliceToChan(c.in)
			wantCh := SliceToChan(c.expect)
			if err := CheckEquals(Highest(inCh, c.w), wantCh); err != nil {
				t.Fatal(err)
			}
		})
	}
}

17-20: Nit: inline the error check for brevity.

Minor readability tweak; avoid the extra var.

-	err := CheckEquals(actual, expected)
-	if err != nil {
-		t.Fatal(err)
-	}
+	if err := CheckEquals(actual, expected); err != nil {
+		t.Fatal(err)
+	}
helper/Lowest.go (2)

9-14: Clarify docstring about edge cases (w <= 0, warm-up period)

Document that Window closes immediately when w <= 0 and that during warm-up (before w items), the minimum of seen items is emitted. This helps users reason about initial outputs.

Apply:

-// Lowest returns a channel that emits the lowest value
-// within a sliding window of size w from the input channel c.
+// Lowest returns a channel that emits the lowest value within a sliding window of size w
+// from the input channel c. During warm-up (before w items are received), it emits the
+// minimum of the items seen so far. If w <= 0, the returned channel is closed immediately.

1-6: File naming consistency

Go files are typically lowercase. Consider renaming Lowest.go to lowest.go to align with common Go conventions and the rest of the helper package.

helper/max_since_test.go (1)

1-27: Broaden test coverage and edge cases

Consider adding:

  • A case where w = 1 (always 0).
  • A case where w > len(series) (warm-up behavior).
  • A case where the last value equals but does not exceed the prior max (e.g., [1, 3, 3] with w=3 → 0).
  • Negative/zero window sizes return closed channel (no emissions) to match Window behavior.

A table-driven test will keep these concise.

helper/slices_reverse.go (2)

3-6: Clarify the semantics of index i in the doc

This function iterates the slice in reverse circular order starting from i-1, then wraps: i-1 … 0, then len(r)-1 … i. Explicitly documenting the “circular wrap” and that each element is visited at most once will make callers (e.g., Window users) less error-prone.

Apply:

-// SlicesReverse loops through a slice in reverse order starting from the given index.
-// The given function is called for each element in the slice. If the function returns false,
-// the loop is terminated.
+// SlicesReverse traverses r in reverse circular order relative to index i:
+// it visits i-1 down to 0, then wraps and visits len(r)-1 down to i. For each element, f is
+// called; if f returns false, traversal stops. Each element is visited at most once.

11-20: Consider exposing the element index to the callback

Some callers may need the logical or physical index during traversal. A variant like SlicesReverseIndex[T any](r []T, i int, f func(idx int, v T) bool) would avoid capturing external counters.

📜 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 3c11e79 and 705d549.

⛔ Files ignored due to path filters (4)
  • strategy/decorator/testdata/no_loss_strategy.csv is excluded by !**/*.csv
  • strategy/decorator/testdata/stop_loss_strategy.csv is excluded by !**/*.csv
  • strategy/trend/testdata/aroon_strategy.csv is excluded by !**/*.csv
  • trend/testdata/aroon.csv is excluded by !**/*.csv
📒 Files selected for processing (14)
  • helper/Lowest.go (1 hunks)
  • helper/highest.go (1 hunks)
  • helper/highest_test.go (1 hunks)
  • helper/lowest_test.go (1 hunks)
  • helper/max_since.go (1 hunks)
  • helper/max_since_test.go (1 hunks)
  • helper/min_since.go (1 hunks)
  • helper/min_since_test.go (1 hunks)
  • helper/slices_reverse.go (1 hunks)
  • helper/slices_reverse_test.go (1 hunks)
  • helper/window.go (1 hunks)
  • helper/window_test.go (1 hunks)
  • trend/aroon.go (1 hunks)
  • trend/aroon_test.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (10)
  • helper/min_since_test.go
  • trend/aroon_test.go
  • helper/highest.go
  • helper/min_since.go
  • helper/slices_reverse_test.go
  • trend/aroon.go
  • helper/lowest_test.go
  • helper/window_test.go
  • helper/window.go
  • helper/max_since.go
🧰 Additional context used
🧬 Code Graph Analysis (3)
helper/Lowest.go (1)
helper/window.go (1)
  • Window (12-39)
helper/max_since_test.go (1)
helper/max_since.go (1)
  • MaxSince (14-31)
helper/highest_test.go (1)
helper/highest.go (1)
  • Highest (11-15)
🔇 Additional comments (5)
helper/highest_test.go (3)

11-16: Test case correctly validates sliding-window maximum behavior (w=3).

The expected sequence matches the semantics of a windowed max with partial warm-up windows. Good coverage of the rolling behavior and boundaries within a short series.


11-21: Confirmed: go.mod sets go 1.22 — slices.Max is supported

Found go.mod: "go 1.22". slices.Max is used in:

  • helper/highest.go
  • helper/max_since.go
  • helper/sync.go

No changes required.


11-21: No extra tests needed — MaxSince/MinSince already validate “most recent” semantics.

Existing code/tests show the requested behavior:

  • helper/max_since.go / helper/min_since.go — implementation uses slices.Max/Min with SlicesReverse+found logic that selects the most recent match.
  • helper/max_since_test.go — test with consecutive equal maxima (…52,52,52…) expecting [0,0,1,0,1,2,0,1,0] (w=3).
  • helper/min_since_test.go — tests with duplicate minima (…49,49…) expecting [0,1,2,2,0,1,2] (w=3).

Ignore the original suggestion to add the complementary tests — the repository already covers the “most recent high/low” cases.

Likely an incorrect or invalid review comment.

helper/Lowest.go (2)

11-15: Implementation aligns with Window/Highest pattern; logic is correct

Using Window with slices.Min correctly emits the minimum for each sliding window and ignores the ring start index as expected for min/max operations. The function is safe for partial windows (cnt < w) because Window never passes an empty slice.


7-7: No action needed — repository declares Go ≥1.21

Verified the go versions from go.mod files:

  • /go.mod: go 1.22
  • /mcp/go.mod: go 1.23

slices.Min (Go 1.21+) is supported by these toolchain versions, so the import in helper/Lowest.go is fine.

Comment on lines +11 to +21
func TestHighest(t *testing.T) {
input := SliceToChan([]int{48, 52, 50, 49, 10})
expected := SliceToChan([]int{48, 52, 52, 52, 50})
window := 3
actual := Highest(input, window)

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

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Define and test behavior for invalid window sizes (w <= 0).

If Highest/Window assumes w > 0, add a test that verifies a panic (or an explicit error contract). This prevents silent misconfiguration and future regressions.

Example panic test if w<=0 should not be allowed:

func TestHighest_InvalidWindowPanics(t *testing.T) {
	defer func() {
		if r := recover(); r == nil {
			t.Fatal("expected panic for invalid window size")
		}
	}()
	_ = Highest(SliceToChan([]int{1, 2, 3}), 0) // or -1
	// Drain to ensure evaluation if needed:
	// for range Highest(...) {}
}

If the intended behavior is not to panic, please clarify the contract and I can adjust the test accordingly.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for Highest definitions/usages..."
rg -n --hidden --no-ignore -S "func Highest" || true
rg -n --hidden --no-ignore -S "Highest(" || true

echo
echo "Searching for helper/highest.go and related files..."
rg -n --hidden --no-ignore -S "highest" || true

echo
echo "Showing candidate Go files containing 'Highest' for inspection..."
files=$(rg -n --hidden --no-ignore -S "Highest" -l || true)
for f in $files; do
  echo "----- FILE: $f -----"
  sed -n '1,240p' "$f"
  echo
done

Length of output: 19062


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for Window definitions/usages..."
rg -n --hidden --no-ignore -S "func Window" || true
rg -n --hidden --no-ignore -S " Window(" || true
rg -n --hidden --no-ignore -S "Window(" || true

echo
echo "Showing candidate files that define or mention Window..."
files=$(rg -n --hidden --no-ignore -S "func Window|Window(" -l || true)
for f in $files; do
  echo "----- FILE: $f -----"
  sed -n '1,240p' "$f"
  echo
done

Length of output: 775


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "----- helper/window.go -----"
sed -n '1,240p' helper/window.go || true

echo
echo "----- Search for uses of Window (fixed string) -----"
rg -n --hidden --no-ignore -F "Window(" || true

echo
echo "----- Search for tests referencing invalid window sizes -----"
rg -n --hidden --no-ignore -S "window <= 0|w <= 0|w<=0|invalid window|invalid.*window" || true

Length of output: 1924


Window returns a closed channel for w <= 0 — test for "no output", don't expect a panic.

Window already guards w <= 0 by closing and returning the result channel (helper/window.go — the if w <= 0 { close(r); return r } block). Highest delegates to Window (helper/highest.go), so calling Highest(..., 0) or Highest(..., -1) yields a closed channel rather than panicking.

Files to update:

  • helper/window.go — current behavior: close+return when w <= 0.
  • helper/highest.go — uses Window, inherits that behavior.
  • Tests to add/update: helper/highest_test.go (and/or helper/window_test.go).

Suggested replacement test (assert no values emitted):

func TestHighest_InvalidWindowReturnsClosedChannel(t *testing.T) {
	input := SliceToChan([]int{1, 2, 3})
	actual := Highest(input, 0) // also test -1 if desired

	// immediate receive should indicate closed channel (no values)
	if _, ok := <-actual; ok {
		t.Fatal("expected closed channel (no values) for w <= 0")
	}
}

If you prefer a panic-based contract instead, change Window to panic on w <= 0 and update callers/tests accordingly. Otherwise, add the above test to lock in the current behavior.

🤖 Prompt for AI Agents
In helper/highest_test.go around lines 11 to 21, add a new test that asserts
Highest returns a closed channel for w <= 0 instead of panicking: create an
input channel (e.g., SliceToChan([]int{1,2,3})), call Highest(input, 0) (and
optionally Highest(input, -1)), then perform an immediate receive and fail the
test if the receive returns ok==true; keep the existing TestHighest intact. This
locks in the current Window behavior (close+return for non-positive window)
without changing Highest or Window implementation.

Comment on lines +7 to +16
func TestMaxSince(t *testing.T) {
input := SliceToChan([]int{48, 49, 47, 52, 52, 52, 53, 50, 55})
expected := SliceToChan([]int{0, 0, 1, 0, 1, 2, 0, 1, 0})
actual := MaxSince(input, 3)

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

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Expected values reflect “earliest occurrence” of the max, not “last (most recent) high”

Per the PR objective and Aroon definition, “periods since the last high” should measure distance to the most recent occurrence of the window’s maximum. The current expected values treat ties by picking the earliest occurrence in the window, which yields non-zero distances even when the latest value equals the maximum (e.g., windows like [52, 52, 52] should produce 0).

Update expected to measure distance to the most recent max:

Please confirm the intended tie-break for duplicates (most recent vs earliest). If “most recent” is correct, apply:

-	expected := SliceToChan([]int{0, 0, 1, 0, 1, 2, 0, 1, 0})
+	expected := SliceToChan([]int{0, 0, 1, 0, 0, 0, 0, 1, 0})

If the implementation also needs adjustment to use “most recent,” update helper/MaxSince accordingly:

 func MaxSince[T Number](c <-chan T, w int) <-chan T {
-	return Window(c, func(w []T, i int) T {
-		since := 0
-		found := false
-		m := slices.Max(w)
-		SlicesReverse(w, i, func(n T) bool {
-			if found && n < m {
-				return false
-			}
-			since++
-			if n == m {
-				found = true
-			}
-			return true
-		})
-		return T(since - 1)
-	}, w)
+	return Window(c, func(w []T, i int) T {
+		m := slices.Max(w)
+		var since int
+		SlicesReverse(w, i, func(n T) bool {
+			if n == m { // stop at first (most recent) occurrence of the max
+				return false
+			}
+			since++
+			return true
+		})
+		return T(since)
+	}, w)
 }

To align on definition, please run a quick web check:


🌐 Web query:

For the Aroon indicator, when multiple bars share the highest high within the lookback window, should "periods since highest high" be measured from the most recent occurrence or the earliest? Cite authoritative sources (e.g., TA-Lib, TradingView, StockCharts).

💡 Result:

When multiple bars share the highest high within the Aroon indicator's lookback window, "periods since highest high" should be measured from the most recent occurrence (i.e., the most recent bar with the highest high).

Authoritative sources support this as follows:

  • StockCharts states: “Aroon-Up...measures the number of periods since price recorded an x-day high” and provides the formula:
    [
    \text{Aroon-Up} = \left(\frac{N - \text{Days since } N\text{-day High}}{N}\right) \times 100
    ]
    Their context, examples, and charts show that if the current bar equals the highest high of the window, "days since" is 0, regardless of earlier occurrences of the same high. Thus, "days since high" always refers to the most recent occurrence within the period[1][4].

  • TradingView (official Help Center):
    “If the highest value occurs in the current candlestick, the up value is set to 100, indicating a new peak. If not, it returns a percentage value indicating the time elapsed since the last peak.” Here, “last peak” means the most recent occurrence within the window[3][7].

  • LightningChart provides a table and calculation:
    "Periods since High within n = How many time periods since the highest High value within the specified time period count (n)" and in their example, when the highest value appears multiple times, the calculation uses the distance from the latest occurrence[9].

  • TA-Lib follows the same convention. Its public formulas do not distinguish between first or last occurrence, only "periods since," which is universally interpreted as periods since the most recent occurrence up to and including the current bar[10].

Summary Table: "Periods Since Highest High"

Scenario Value Used in Calculation
Highest high is on current bar 0
Highest high occurred n bars ago n (counted from current bar to most recent high)
Multiple bars with same highest high value Distance from current bar to most recent high bar

Key sources:

  • StockCharts[1][4]
  • TradingView[3][7]
  • LightningChart[9]
  • TA-Lib (documentation and industry convention)[10]

No authoritative source (including StockCharts, TradingView, and TA-Lib) suggests using the earliest (oldest) occurrence within the lookback window. The industry standard and all major implementations measure from the most recent occurrence of the highest high.

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching repo for MaxSince symbols..."
rg -n --hidden "MaxSince" || true

echo
echo "Searching for files named *max*since*..."
fd -HI -e go max | sed -n '1,200p' || true

echo
echo "Showing helper/max_since_test.go (if exists):"
if [ -f helper/max_since_test.go ]; then
  sed -n '1,200p' helper/max_since_test.go
else
  echo "helper/max_since_test.go not found"
fi

echo
echo "Showing helper/max_since.go (if exists):"
if [ -f helper/max_since.go ]; then
  sed -n '1,240p' helper/max_since.go
else
  echo "helper/max_since.go not found"
fi

echo
echo "Searching for MaxSince implementation (func MaxSince):"
rg -n --hidden "func\s+MaxSince" -S || true

echo
echo "Searching for SlicesReverse or similar helpers used in original review suggestion..."
rg -n --hidden "SlicesReverse|slices.Max|Window(" -S || true

Length of output: 3024


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for SlicesReverse and Window definitions..."
rg -n --hidden "func\s+SlicesReverse|func\s+Window" -S || true

echo
echo "Searching for SlicesReverse usages to locate its file..."
rg -n --hidden "SlicesReverse" -S || true

echo
echo "Showing files that likely contain these helpers (helper/*.go):"
ls -1 helper/*.go || true

echo
echo "Printing helper/slices_reverse.go and helper/window.go if present:"
for f in helper/*; do
  case "$(basename "$f")" in
    *slice*|*reverse*|*window*|*helpers*|*util*|*slices*)
      echo "---- $f ----"
      sed -n '1,240p' "$f" || true
      echo
      ;;
  esac
done

echo
echo "Printing all helper/*.go for manual scan (first 400 lines each):"
for f in helper/*.go; do
  echo "---- $f ----"
  sed -n '1,400p' "$f" || true
  echo
done

Length of output: 156076


MaxSince/MinSince use the earliest tie-break today — change to the most-recent occurrence (Aroon convention)

Confirmed: helper/max_since.go and helper/min_since.go currently pick the earliest occurrence among tied extrema; authoritative sources (StockCharts / TradingView / TA‑Lib) define "periods since high/low" as distance to the most recent occurrence. Update both implementations and their tests.

Files to change:

  • helper/max_since.go
  • helper/max_since_test.go
  • helper/min_since.go
  • helper/min_since_test.go

Suggested diffs:

  • helper/max_since_test.go
-	expected := SliceToChan([]int{0, 0, 1, 0, 1, 2, 0, 1, 0})
+	expected := SliceToChan([]int{0, 0, 1, 0, 0, 0, 0, 1, 0})
  • helper/max_since_test.go (TestMaxSinceAtEnd)
-	expected := SliceToChan([]int{0, 0, 1, 0, 1, 2, 2})
+	expected := SliceToChan([]int{0, 0, 1, 0, 0, 0, 0})
  • helper/max_since.go
 func MaxSince[T Number](c <-chan T, w int) <-chan T {
-	return Window(c, func(w []T, i int) T {
-		since := 0
-		found := false
-		m := slices.Max(w)
-		SlicesReverse(w, i, func(n T) bool {
-			if found && n < m {
-				return false
-			}
-			since++
-			if n == m {
-				found = true
-			}
-			return true
-		})
-		return T(since - 1)
-	}, w)
+	return Window(c, func(w []T, i int) T {
+		m := slices.Max(w)
+		var since int
+		// stop at the first (most recent) occurrence of the max
+		SlicesReverse(w, i, func(n T) bool {
+			if n == m {
+				return false
+			}
+			since++
+			return true
+		})
+		return T(since)
+	}, w)
 }
  • helper/min_since_test.go
-	expected := SliceToChan([]int{0, 1, 2, 2, 0, 1, 2})
+	expected := SliceToChan([]int{0, 1, 2, 0, 0, 0, 1})
  • helper/min_since_test.go (TestMinSinceAtEnd)
-	expected := SliceToChan([]int{0, 1, 2, 2, 0, 1, 2})
+	expected := SliceToChan([]int{0, 1, 2, 0, 0, 0, 0})
  • helper/min_since_test.go (TestMinSinceFromStart)
-	expected := SliceToChan([]int{0, 1, 2})
+	expected := SliceToChan([]int{0, 0, 1})
  • helper/min_since.go
 func MinSince[T Number](c <-chan T, w int) <-chan T {
-	return Window(c, func(w []T, i int) T {
-		since := 0
-		found := false
-		m := slices.Min(w)
-		SlicesReverse(w, i, func(n T) bool {
-			if found && n > m {
-				return false
-			}
-			since++
-			if n == m {
-				found = true
-			}
-			return true
-		})
-		return T(since - 1)
-	}, w)
+	return Window(c, func(w []T, i int) T {
+		m := slices.Min(w)
+		var since int
+		// stop at the first (most recent) occurrence of the min
+		SlicesReverse(w, i, func(n T) bool {
+			if n == m {
+				return false
+			}
+			since++
+			return true
+		})
+		return T(since)
+	}, w)
 }

Please apply these changes (or confirm if your intended tie-break is different). After updating, run the test suite — Aroon-related code (trend/aroon.go) is consistent with the most-recent semantics and should produce correct Aroon values thereafter.

📝 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
func TestMaxSince(t *testing.T) {
input := SliceToChan([]int{48, 49, 47, 52, 52, 52, 53, 50, 55})
expected := SliceToChan([]int{0, 0, 1, 0, 1, 2, 0, 1, 0})
actual := MaxSince(input, 3)
err := CheckEquals(actual, expected)
if err != nil {
t.Fatal(err)
}
}
func TestMaxSince(t *testing.T) {
input := SliceToChan([]int{48, 49, 47, 52, 52, 52, 53, 50, 55})
expected := SliceToChan([]int{0, 0, 1, 0, 0, 0, 0, 1, 0})
actual := MaxSince(input, 3)
err := CheckEquals(actual, expected)
if err != nil {
t.Fatal(err)
}
}
🤖 Prompt for AI Agents
In helper/max_since_test.go around lines 7-16, the test and implementation
currently assume earliest-occurrence tie-breaking; change both
helper/max_since.go and helper/min_since.go to break ties by choosing the
most-recent occurrence (largest index) instead of the earliest, and update all
related tests (helper/max_since_test.go, helper/max_since_test.go
TestMaxSinceAtEnd, helper/min_since_test.go, helper/min_since_test.go
TestMinSinceAtEnd, helper/min_since_test.go TestMinSinceFromStart) so expected
outputs reflect distance to the most recent high/low; specifically, where the
code currently selects the first equal max/min index, change logic to update the
chosen index when a tied value is encountered (prefer later index), and adjust
test expected SliceToChan arrays accordingly, then run the test suite to verify
Aroon-related outputs remain correct.

Comment on lines +18 to +27
func TestMaxSinceAtEnd(t *testing.T) {
input := SliceToChan([]int{48, 49, 47, 52, 52, 52, 52})
expected := SliceToChan([]int{0, 0, 1, 0, 1, 2, 2})
actual := MaxSince(input, 3)

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

Choose a reason for hiding this comment

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

⚠️ Potential issue

Same issue in TestMaxSinceAtEnd: duplicates at the end should produce 0 if ties resolve to “most recent”

In a window [52, 52, 52], the distance to the last (most recent) high is 0, not 2. Adjust expected accordingly if the intended tie-break is “most recent.”

Apply:

-	expected := SliceToChan([]int{0, 0, 1, 0, 1, 2, 2})
+	expected := SliceToChan([]int{0, 0, 1, 0, 0, 0, 0})
📝 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
func TestMaxSinceAtEnd(t *testing.T) {
input := SliceToChan([]int{48, 49, 47, 52, 52, 52, 52})
expected := SliceToChan([]int{0, 0, 1, 0, 1, 2, 2})
actual := MaxSince(input, 3)
err := CheckEquals(actual, expected)
if err != nil {
t.Fatal(err)
}
}
func TestMaxSinceAtEnd(t *testing.T) {
input := SliceToChan([]int{48, 49, 47, 52, 52, 52, 52})
expected := SliceToChan([]int{0, 0, 1, 0, 0, 0, 0})
actual := MaxSince(input, 3)
err := CheckEquals(actual, expected)
if err != nil {
t.Fatal(err)
}
}
🤖 Prompt for AI Agents
In helper/max_since_test.go around lines 18 to 27, the test's expected output
assumes ties pick the oldest max but the intended tie-break is “most recent”;
update the expected channel to reflect that duplicates at the end produce 0
(change the expected SliceToChan from []int{0,0,1,0,1,2,2} to
[]int{0,0,1,0,1,0,0}) so the test verifies ties resolve to the most recent
occurrence.

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.

Aroon Indicator
3 participants