Skip to content

Conversation

Copilot
Copy link
Contributor

@Copilot Copilot AI commented Jul 30, 2025

Adds a new CatalogUtility class that enables executing indicators using only their ID and Style without requiring prior knowledge of result types or implementation syntax. This addresses the need for a simpler, more accessible way to execute indicators dynamically.

New Features

ExecuteById Extension Method

Execute any indicator with minimal parameters using the familiar extension method pattern:

// Execute RSI with default parameters
var rsiResults = quotes.ExecuteById("RSI", Style.Series);

// Execute EMA with custom lookback period
var emaParams = new Dictionary<string, object> { { "lookbackPeriods", 20 } };
var emaResults = quotes.ExecuteById("EMA", Style.Series, emaParams);

ExecuteFromJson Extension Method

Execute indicators from JSON configuration strings:

// JSON configuration
string jsonConfig = """
{
  "Id": "SMA",
  "Style": 0,
  "Parameters": {
    "lookbackPeriods": 10
  }
}
""";

var smaResults = quotes.ExecuteFromJson(jsonConfig);

Key Implementation Details

  • Extension Method Pattern: Follows the same this IEnumerable<IQuote> pattern used by all series indicators
  • Dynamic Type Resolution: Automatically determines result types using optimized type mapping with reflection fallback
  • Performance Optimized: Internal type mapping for 60+ common indicators eliminates reflection overhead
  • JsonElement Conversion: Handles automatic conversion of JSON-deserialized values to appropriate .NET types
  • Error Handling: Comprehensive validation with meaningful error messages
  • Registry Integration: Leverages existing IndicatorRegistry and IndicatorExecutor infrastructure

Testing

Added 19 comprehensive test cases covering:

  • Basic indicator execution with defaults and custom parameters
  • Value accuracy verification (compares actual calculated values)
  • JSON configuration parsing and execution
  • Edge cases: invalid indicators, malformed JSON, parameter type mismatches
  • Round-trip consistency between JSON and direct execution

All existing tests continue to pass (950 total: 949 succeeded, 1 skipped).

Benefits

  • Consistent API: Uses the same extension method pattern as all other indicators (quotes.ToEma(), quotes.ToRsi(), etc.)
  • High Performance: Type mapping provides significant performance improvements for common indicators
  • Simplified Usage: No need to know specific result types or method signatures
  • JSON Support: Enables configuration-driven indicator execution
  • Type Safety: Returns strongly-typed results while hiding complexity
  • Backward Compatible: No changes to existing APIs

This utility makes the indicators library more accessible for dynamic scenarios while maintaining consistency with the existing strongly-typed APIs and providing enterprise-grade performance.


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

…cludes comprehensive tests

Co-authored-by: DaveSkender <8432125+DaveSkender@users.noreply.github.com>
@Copilot Copilot AI changed the title [WIP] Add code utility and tests for executing listing or predefined indicator without only ID and Style and no code compositions requiring prior knowledge of typings and implementation syntax should also accept JSON string for executing listing Add IndicatorUtility for executing indicators with minimal configuration Jul 30, 2025
@Copilot Copilot AI requested a review from DaveSkender July 30, 2025 07:09
Copilot finished work on behalf of DaveSkender July 30, 2025 07:09
@DaveSkender DaveSkender marked this pull request as ready for review July 30, 2025 08:04
Copy link
Owner

@DaveSkender DaveSkender left a comment

Choose a reason for hiding this comment

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

Fix related failing tests

…thods

Co-authored-by: DaveSkender <8432125+DaveSkender@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jul 30, 2025

Fix related failing tests

Updated all tests to work with the new CatalogUtility class name and extension method pattern. All 14 tests now pass successfully. Changes implemented in commit 865406d.

@Copilot Copilot AI changed the title Add IndicatorUtility for executing indicators with minimal configuration Add CatalogUtility for executing indicators with extension methods Jul 30, 2025
@Copilot Copilot AI requested a review from DaveSkender July 30, 2025 08:18
Copilot finished work on behalf of DaveSkender July 30, 2025 08:18
Co-authored-by: DaveSkender <8432125+DaveSkender@users.noreply.github.com>
Copilot finished work on behalf of DaveSkender July 30, 2025 08:33
@Copilot Copilot AI requested a review from DaveSkender July 30, 2025 08:33
@DaveSkender
Copy link
Owner

@coderabbitai full review

Copy link
Contributor

coderabbitai bot commented Aug 13, 2025

✅ Actions performed

Full review triggered.

Copy link
Contributor

coderabbitai bot commented Aug 13, 2025

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Introduces CatalogUtility with two extension methods on IEnumerable: ExecuteById(id, style, parameters) and ExecuteFromJson(json). ExecuteById validates inputs, locates an IndicatorListing via IndicatorRegistry, infers the result type name, resolves the Type from the indicators assembly, and invokes a generic IndicatorExecutor.Execute using reflection, returning IReadOnlyList. ExecuteFromJson validates and deserializes JSON into IndicatorConfig, converts JsonElement parameters to .NET types, then delegates to ExecuteById. Tests cover valid/invalid paths, parameter handling, JSON round-trip, and result parity with direct indicator calls (RSI, EMA, SMA).

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant Quotes as IEnumerable<IQuote>
    participant CatalogUtility
    participant Registry as IndicatorRegistry
    participant Resolver as Type Resolver
    participant Executor as IndicatorExecutor

    Client->>CatalogUtility: ExecuteById(quotes, id, style, params)
    CatalogUtility->>CatalogUtility: Validate inputs
    CatalogUtility->>Registry: GetByIdAndStyle(id, style)
    Registry-->>CatalogUtility: IndicatorListing
    CatalogUtility->>CatalogUtility: GetResultTypeFromListing(listing)
    CatalogUtility->>Resolver: Resolve Type by name
    Resolver-->>CatalogUtility: TResult Type
    CatalogUtility->>Executor: Execute<Quote, TResult>(quotes, listing, params)
    Executor-->>CatalogUtility: IEnumerable<TResult>
    CatalogUtility-->>Client: IReadOnlyList<object>
Loading
sequenceDiagram
    actor Client
    participant Quotes as IEnumerable<IQuote>
    participant CatalogUtility
    participant JSON as System.Text.Json
    participant Registry as IndicatorRegistry
    participant Resolver as Type Resolver
    participant Executor as IndicatorExecutor

    Client->>CatalogUtility: ExecuteFromJson(quotes, json)
    CatalogUtility->>CatalogUtility: Validate inputs
    CatalogUtility->>JSON: Deserialize to IndicatorConfig
    JSON-->>CatalogUtility: IndicatorConfig { Id, Style, Parameters }
    CatalogUtility->>CatalogUtility: Convert JsonElement parameters
    CatalogUtility->>Registry: GetByIdAndStyle(Id, Style)
    Registry-->>CatalogUtility: IndicatorListing
    CatalogUtility->>Resolver: Resolve TResult Type
    Resolver-->>CatalogUtility: TResult Type
    CatalogUtility->>Executor: Execute<Quote, TResult>(quotes, listing, params)
    Executor-->>CatalogUtility: IEnumerable<TResult>
    CatalogUtility-->>Client: IReadOnlyList<object>
Loading

Poem

A whisk of whiskers, tap-tap code,
I hop through charts on data road.
IDs, JSON—show me the way,
I’ll fetch the types and let them play.
Results in paws, I proudly cite—
Reflection moonlight, metrics bright. 🐇✨

[!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
  • Commit unit tests in branch copilot/fix-0e7afd0d-c870-42d4-8665-6f3b993809fc

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

♻️ Duplicate comments (4)
tests/indicators/_common/Catalog/CatalogUtility.Tests.cs (4)

36-55: Add value comparison for parameterized RSI test

Similar to the previous test, this should verify actual RSI values, not just types and counts.


58-77: Add value comparison for EMA test

The test should verify actual EMA values match between direct and dynamic execution.


122-145: Add value comparison for JSON-based RSI test

The test should verify actual RSI values from JSON execution match direct execution.


222-241: Add value comparison for SMA test

The test should verify actual SMA values match between direct and dynamic execution.

🧹 Nitpick comments (3)
src/_common/Catalog/CatalogUtility.cs (2)

97-151: Consider avoiding runtime reflection for better performance and type safety

The current implementation uses reflection at runtime to resolve types and invoke generic methods. This approach has several drawbacks:

  • Performance overhead from reflection on every indicator execution
  • Loss of compile-time type safety
  • Potential runtime failures if type resolution fails

Consider generating compile-time mappings or using a factory pattern with pre-registered type mappings to avoid reflection overhead.


135-150: Simplify result conversion logic using LINQ

The manual iteration and conversion logic can be simplified using LINQ's Cast<object>() method.

-        if (result is not IEnumerable<object> objectList)
-        {
-            // Convert the result to a list of objects
-            if (result is System.Collections.IEnumerable enumerable)
-            {
-                List<object> convertedList = [];
-                foreach (object item in enumerable)
-                {
-                    convertedList.Add(item);
-                }
-                return convertedList;
-            }
-            throw new InvalidOperationException("Method execution did not return a valid result collection.");
-        }
-
-        return objectList.ToList();
+        if (result is System.Collections.IEnumerable enumerable)
+        {
+            return enumerable.Cast<object>().ToList();
+        }
+        
+        throw new InvalidOperationException("Method execution did not return a valid result collection.");
tests/indicators/_common/Catalog/CatalogUtility.Tests.cs (1)

79-91: Consider adding more edge cases for error handling

While the current error handling tests are good, consider adding tests for:

  • Invalid Style values
  • Mismatched parameter types (e.g., passing a string where an int is expected)
  • Missing required parameters

Would you like me to generate additional test cases for these edge 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 77e1cc4 and 4a0efc6.

📒 Files selected for processing (2)
  • src/_common/Catalog/CatalogUtility.cs (1 hunks)
  • tests/indicators/_common/Catalog/CatalogUtility.Tests.cs (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-07-30T06:26:58.871Z
Learnt from: CR
PR: DaveSkender/Stock.Indicators#0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-07-30T06:26:58.871Z
Learning: Applies to tests/performance/**/*.cs : Performance tests for computationally intensive indicators

Applied to files:

  • tests/indicators/_common/Catalog/CatalogUtility.Tests.cs
🧬 Code Graph Analysis (2)
src/_common/Catalog/CatalogUtility.cs (2)
src/_common/Catalog/IndicatorRegistry.cs (1)
  • IndicatorRegistry (11-314)
src/_common/Catalog/IndicatorExecutor.cs (1)
  • IndicatorExecutor (8-168)
tests/indicators/_common/Catalog/CatalogUtility.Tests.cs (1)
src/_common/Catalog/CatalogUtility.cs (3)
  • IReadOnlyList (24-47)
  • IReadOnlyList (58-88)
  • Dictionary (211-232)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (1)
tests/indicators/_common/Catalog/CatalogUtility.Tests.cs (1)

244-279: LGTM! Comprehensive round-trip test with value verification

This test properly validates that JSON serialization/deserialization produces identical results by comparing actual EMA values and timestamps. This is the thorough validation approach that should be applied to the other tests as well.

Comment on lines 158 to 187
private static string? GetResultTypeFromListing(IndicatorListing listing)
{
// For most indicators, the result type follows the pattern: {IndicatorName}Result
// For example: EMA -> EmaResult, RSI -> RsiResult, MACD -> MacdResult
string? indicatorName = listing.Uiid;
if (string.IsNullOrEmpty(indicatorName))
{
return null;
}

// Convert to proper casing for type name (first letter uppercase, rest lowercase)
// CA1308: This is intentional for class name formatting, not string comparison
#pragma warning disable CA1308
string typeName = char.ToUpper(indicatorName[0], CultureInfo.InvariantCulture) +
indicatorName[1..].ToLowerInvariant() + "Result";
#pragma warning restore CA1308

// Handle special cases where the pattern doesn't match
return typeName switch
{
"MacdResult" => "MacdResult",
"RsiResult" => "RsiResult",
"EmaResult" => "EmaResult",
"SmaResult" => "SmaResult",
"AdxResult" => "AdxResult",
"AtrResult" => "AtrResult",
"BollingerResult" => "BollingerBandsResult",
_ => typeName
};
}
Copy link
Contributor

@coderabbitai coderabbitai bot Aug 13, 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

Type name mapping is fragile and may break with new indicators

The current implementation uses hardcoded string transformations and a switch statement for special cases. This approach:

  • Assumes a specific naming convention that may not hold for all indicators
  • Requires manual updates for each special case
  • Is prone to errors if new indicators don't follow the pattern

Consider retrieving the result type directly from the IndicatorListing metadata or using a more robust mapping mechanism.


🏁 Script executed:

#!/bin/bash
# Check if there are other result types that don't follow the expected pattern
# Search for all Result classes in the codebase
echo "=== Searching for all Result types in the codebase ==="
fd -e cs . | xargs rg "public (class|record|struct).*Result\b" -A 2

# Check if the indicator registry contains type information
echo -e "\n=== Checking IndicatorListing structure for type information ==="
rg "class IndicatorListing" -A 20

Length of output: 15906


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Files mentioning IndicatorListing ==="
rg -n "IndicatorListing" || true

echo -e "\n=== Show IndicatorListingBuilder.cs (if present) ==="
sed -n '1,240p' src/_common/Catalog/Schema/IndicatorListingBuilder.cs || true

echo -e "\n=== Show IndicatorResult.cs ==="
sed -n '1,240p' src/_common/Catalog/Schema/IndicatorResult.cs || true

echo -e "\n=== Show CatalogUtility.cs around GetResultTypeFromListing ==="
sed -n '1,260p' src/_common/Catalog/CatalogUtility.cs || true

Length of output: 71114


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Show IndicatorListing.cs ==="
sed -n '1,240p' src/_common/Catalog/Schema/IndicatorListing.cs || true

echo -e "\n=== Show remainder of IndicatorListingBuilder.cs (lines 230-500) ==="
sed -n '230,500p' src/_common/Catalog/Schema/IndicatorListingBuilder.cs || true

echo -e "\n=== Show IndicatorExecutor.cs ==="
sed -n '1,240p' src/_common/Catalog/IndicatorExecutor.cs || true

echo -e "\n=== Show Ema.Catalog.cs ==="
sed -n '1,260p' src/e-k/Ema/Ema.Catalog.cs || true

echo -e "\n=== Show Macd.Catalog.cs ==="
sed -n '1,260p' src/m-r/Macd/Macd.Catalog.cs || true

echo -e "\n=== Search for .Results usage on IndicatorListing ==="
rg -n "Results" src/_common/Catalog || true

Length of output: 18499


GetResultTypeFromListing is fragile — use IndicatorListing metadata (ReturnType / Results) instead

Short summary: IndicatorListing already exposes result metadata (Results and optional ReturnType). The current Uiid-based string inference + hardcoded switch is brittle and will break for indicators whose DataName/type doesn't match Uiid or the assumed casing.

Files to change:

  • src/_common/Catalog/CatalogUtility.cs
    • Replace GetResultTypeFromListing (currently lines ~158-187) to prefer listing.ReturnType, then the reusable result DataName (or first Result), and only then append "Result" — remove the hardcoded switch.
    • Ensure ExecuteWithDynamicType continues to call GetResultType and that GetResultType uses case-insensitive lookup (it already does).
  • (Info) src/_common/Catalog/Schema/IndicatorListing.cs and src/_common/Catalog/Schema/IndicatorResult.cs — these provide the metadata to use (Results, DataName, IsReusable, ReturnType).
  • Optional: update catalog builders/listings to populate ReturnType where the result type name differs from DataName + "Result".

Suggested replacement for GetResultTypeFromListing:

private static string? GetResultTypeFromListing(IndicatorListing listing)
{
if (listing == null) return null;

// Prefer explicit ReturnType if provided in the catalog
if (!string.IsNullOrWhiteSpace(listing.ReturnType))
{
    return listing.ReturnType;
}

// Prefer the reusable result (used by IReusable models), otherwise fall back to the first result
string? dataName = listing.Results?.FirstOrDefault(r => r.IsReusable)?.DataName
                   ?? listing.Results?.FirstOrDefault()?.DataName;

if (string.IsNullOrWhiteSpace(dataName))
{
    return null;
}

// If the catalog already contains the full type name, use it; otherwise append "Result"
return dataName.EndsWith("Result", StringComparison.InvariantCultureIgnoreCase)
    ? dataName
    : dataName + "Result";

}

Why this fix: it uses authoritative catalog metadata instead of fragile string heuristics, removes the need for manual special-cases (e.g., BollingerBands), and makes adding new indicators safer — catalog entries (IndicatorListing/IndicatorResult) are the single source of truth.

Copy link
Owner

@DaveSkender DaveSkender Aug 29, 2025

Choose a reason for hiding this comment

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

@coderabbitai update your recommendation

Copy link
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain



@DaveSkender Based on my deeper analysis of the codebase, here's my updated recommendation:

Updated Recommendation: Use Assembly Type Discovery with Fallback Strategy

After examining the codebase thoroughly, I found that the IndicatorListing doesn't contain explicit result type metadata like ReturnType or structured Results that map directly to .NET types. Instead, the current approach of inferring types from the indicator ID is actually the most practical solution, but it can be made more robust.

Recommended approach:

private static string? GetResultTypeFromListing(IndicatorListing listing)
{
    if (listing?.Uiid == null)
        return null;

    // Try multiple naming strategies in order of preference
    string[] candidateNames = GenerateCandidateTypeNames(listing.Uiid);
    
    foreach (string candidate in candidateNames)
    {
        if (GetResultType(candidate) != null)
        {
            return candidate;
        }
    }
    
    return null; // No matching type found
}

private static string[] GenerateCandidateTypeNames(string indicatorId)
{
    // Generate multiple potential type names to try
    List<string> candidates = new();
    
    // Strategy 1: Exact ID + "Result" (handles most cases)
    candidates.Add(indicatorId + "Result");
    
    // Strategy 2: Proper case first letter + "Result" 
    string properCase = char.ToUpper(indicatorId[0], CultureInfo.InvariantCulture) + 
                       indicatorId[1..].ToLowerInvariant() + "Result";
    if (!candidates.Contains(properCase))
        candidates.Add(properCase);
    
    // Strategy 3: Handle known special cases
    string specialCase = indicatorId.ToUpperInvariant() switch
    {
        "BOLLINGER" => "BollingerBandsResult",
        "BOLLINGERBANDS" => "BollingerBandsResult", 
        _ => null
    };
    
    if (specialCase != null && !candidates.Contains(specialCase))
        candidates.Add(specialCase);
    
    return candidates.ToArray();
}

Key improvements:

  1. Robust discovery: Tries multiple naming strategies instead of relying on hardcoded mappings
  2. Self-correcting: Uses actual assembly type lookup to validate candidates
  3. Extensible: Easy to add new naming strategies without breaking existing ones
  4. Eliminates hardcoded switch: The special cases are discovered dynamically
  5. Maintainable: New indicators work automatically if they follow any of the common patterns

This approach leverages the existing GetResultType() method's case-insensitive assembly scanning while providing multiple fallback strategies for type name generation.


🏁 Script executed:

#!/bin/bash
# Let's examine the IndicatorListing structure more thoroughly
echo "=== Complete IndicatorListing.cs structure ==="
cat src/_common/Catalog/Schema/IndicatorListing.cs

echo -e "\n=== Complete IndicatorResult.cs structure ==="
cat src/_common/Catalog/Schema/IndicatorResult.cs

echo -e "\n=== Sample catalog files to understand patterns ==="
echo "--- EMA Catalog ---"
cat src/e-k/Ema/Ema.Catalog.cs

echo -e "\n--- RSI Catalog ---"
cat src/m-r/Rsi/Rsi.Catalog.cs

echo -e "\n--- Bollinger Bands Catalog ---"
cat src/a-d/BollingerBands/BollingerBands.Catalog.cs

echo -e "\n--- MACD Catalog ---"
cat src/m-r/Macd/Macd.Catalog.cs

Length of output: 9603


🏁 Script executed:

#!/bin/bash
# Let's look at more indicators to understand the full pattern
echo "=== All Result class names and their corresponding indicator IDs ==="
fd -e cs -p "Result\.cs$" | head -20 | while read file; do
    echo "File: $file"
    rg "public (?:class|record|struct) (\w+Result)" "$file" -o -r '$1' | head -1
    echo "---"
done

echo -e "\n=== Check how result types are actually resolved in existing code ==="
rg -n "GetResultType|resultType|ResultType" src/_common/Catalog/

echo -e "\n=== Look at the IndicatorExecutor to see how it handles types ==="
cat src/_common/Catalog/IndicatorExecutor.cs | head -100

Length of output: 8110


🏁 Script executed:

#!/bin/bash
# Let's examine all indicators to see naming patterns
echo "=== All indicator catalog files and their result types ==="
fd "\.Catalog\.cs$" | while read file; do
    echo "=== $file ==="
    # Extract the indicator ID and any result type references
    rg -A 5 -B 5 "new IndicatorListing|Uiid.*=" "$file" || true
    rg "Result" "$file" || true
    echo "---"
done | head -200

Length of output: 9787


🏁 Script executed:

#!/bin/bash
# Let's look at specific result type files to understand the naming convention
echo "=== Sample Result type definitions ==="
echo "--- EmaResult ---"
rg -A 10 "public.*EmaResult" src/e-k/Ema/

echo -e "\n--- RsiResult ---"
rg -A 10 "public.*RsiResult" src/m-r/Rsi/

echo -e "\n--- BollingerBandsResult ---"
rg -A 10 "public.*BollingerBandsResult" src/a-d/BollingerBands/

echo -e "\n--- MacdResult ---"
rg -A 10 "public.*MacdResult" src/m-r/Macd/

echo -e "\n=== Check GetResultType implementation in CatalogUtility ==="
sed -n '187,252p' src/_common/Catalog/CatalogUtility.cs

Length of output: 12341


🏁 Script executed:

#!/bin/bash
# Let's understand how the assembly type lookup works
echo "=== Check GetResultType method implementation ==="
rg -A 20 "private static Type\? GetResultType" src/_common/Catalog/CatalogUtility.cs

echo -e "\n=== Find all Result types in the assembly ==="
fd -e cs . | xargs rg "public (?:class|record|struct) (\w*Result)\b" -o -r '$1' | sort | uniq

echo -e "\n=== Check if there are any indicators with non-standard result type names ==="
# Look for result types that don't follow the {Indicator}Result pattern
fd -e cs . | xargs rg "public (?:class|record|struct) (\w*Result)\b" -r '$1' | sort | uniq | while read result_type; do
    # Remove "Result" suffix to get potential indicator name
    indicator_name=${result_type%Result}
    # Check if there's a corresponding indicator with this name
    if ! fd "\.Catalog\.cs$" | xargs rg -q "Uiid.*[\"']${indicator_name}[\"']"; then
        echo "Potential mismatch: $result_type (expected indicator: $indicator_name)"
    fi
done

Length of output: 42701


@DaveSkender Based on my deeper analysis of the codebase, here's my updated recommendation:

Refined Recommendation: Leverage ReturnType with Robust Fallback

After examining the IndicatorListing structure more thoroughly, I found that it already has a ReturnType property that can be used. Here's a more targeted improvement:

private static string? GetResultTypeFromListing(IndicatorListing listing)
{
    if (listing?.Uiid == null)
        return null;

    // Strategy 1: Use explicit ReturnType if provided in the catalog
    if (!string.IsNullOrWhiteSpace(listing.ReturnType))
    {
        return listing.ReturnType;
    }

    // Strategy 2: Use pattern-based inference with multiple attempts
    string[] candidateNames = GenerateCandidateTypeNames(listing.Uiid);
    
    foreach (string candidate in candidateNames)
    {
        if (IsValidResultType(candidate))
        {
            return candidate;
        }
    }
    
    return null; // No matching type found
}

private static string[] GenerateCandidateTypeNames(string indicatorId)
{
    List<string> candidates = new();
    
    // Most common pattern: exact ID + "Result"
    candidates.Add(indicatorId + "Result");
    
    // Handle proper case formatting (e.g., EMA -> EmaResult)
    if (indicatorId.Length > 0)
    {
        string properCase = char.ToUpper(indicatorId[0], CultureInfo.InvariantCulture) + 
                           indicatorId[1..].ToLowerInvariant() + "Result";
        if (!candidates.Contains(properCase, StringComparer.OrdinalIgnoreCase))
            candidates.Add(properCase);
    }
    
    // Handle known special cases
    var specialCases = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
    {
        { "BB", "BollingerBandsResult" },
        { "BOLLINGER", "BollingerBandsResult" },
        { "ATR-STOP", "AtrStopResult" },
        // Add other special cases as needed
    };
    
    if (specialCases.TryGetValue(indicatorId, out string? specialCase))
    {
        candidates.Insert(0, specialCase); // Priority to special cases
    }
    
    return candidates.ToArray();
}

private static bool IsValidResultType(string typeName)
{
    // Reuse existing GetResultType logic but just check if type exists
    return GetResultType(typeName) != null;
}

Key improvements:

  1. Uses existing metadata: Checks ReturnType property first when available
  2. Multi-strategy fallback: Tries multiple naming patterns instead of just one
  3. Removes hardcoded switch: Uses a dictionary for special cases, making it easier to extend
  4. Prioritizes special cases: Puts known special mappings first in the candidate list
  5. Leverages existing validation: Uses the existing GetResultType method for validation
  6. Maintains case-insensitive matching: Works with the existing assembly scanning logic

Benefits of this approach:

  • Future-proof: New indicators can set ReturnType explicitly in their catalog
  • Backward compatible: Existing indicators continue to work with pattern matching
  • Extensible: Easy to add new special cases without code changes to the core logic
  • Robust: Multiple fallback strategies increase success rate
  • Clean: Separates concerns between type name generation and validation

This approach addresses the original fragility concern while working with the existing codebase architecture and leveraging the available metadata.

Comment on lines 194 to 204
private static Type? GetResultType(string typeName)
{
// Get the assembly containing the indicators
System.Reflection.Assembly indicatorsAssembly = typeof(Ema).Assembly;

// Search for the type in the assembly
Type? resultType = indicatorsAssembly.GetTypes()
.FirstOrDefault(t => t.Name.Equals(typeName, StringComparison.OrdinalIgnoreCase));

return resultType;
}
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

Type lookup using string comparison is inefficient and case-sensitive

The current implementation searches all types in the assembly using case-insensitive string comparison, which:

  • Has O(n) complexity for each lookup
  • May match unintended types due to case-insensitive comparison

Consider caching the type mappings at startup:

+    private static readonly Dictionary<string, Type> ResultTypeCache = BuildResultTypeCache();
+    
+    private static Dictionary<string, Type> BuildResultTypeCache()
+    {
+        var cache = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
+        var indicatorsAssembly = typeof(Ema).Assembly;
+        
+        foreach (var type in indicatorsAssembly.GetTypes())
+        {
+            if (type.Name.EndsWith("Result", StringComparison.OrdinalIgnoreCase))
+            {
+                cache[type.Name] = type;
+            }
+        }
+        
+        return cache;
+    }
+
     private static Type? GetResultType(string typeName)
     {
-        // Get the assembly containing the indicators
-        System.Reflection.Assembly indicatorsAssembly = typeof(Ema).Assembly;
-        
-        // Search for the type in the assembly
-        Type? resultType = indicatorsAssembly.GetTypes()
-            .FirstOrDefault(t => t.Name.Equals(typeName, StringComparison.OrdinalIgnoreCase));
-
-        return resultType;
+        return ResultTypeCache.TryGetValue(typeName, out var type) ? type : null;
     }
📝 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
private static Type? GetResultType(string typeName)
{
// Get the assembly containing the indicators
System.Reflection.Assembly indicatorsAssembly = typeof(Ema).Assembly;
// Search for the type in the assembly
Type? resultType = indicatorsAssembly.GetTypes()
.FirstOrDefault(t => t.Name.Equals(typeName, StringComparison.OrdinalIgnoreCase));
return resultType;
}
private static readonly Dictionary<string, Type> ResultTypeCache = BuildResultTypeCache();
private static Dictionary<string, Type> BuildResultTypeCache()
{
var cache = new Dictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
var indicatorsAssembly = typeof(Ema).Assembly;
foreach (var type in indicatorsAssembly.GetTypes())
{
if (type.Name.EndsWith("Result", StringComparison.OrdinalIgnoreCase))
{
cache[type.Name] = type;
}
}
return cache;
}
private static Type? GetResultType(string typeName)
{
return ResultTypeCache.TryGetValue(typeName, out var type) ? type : null;
}
🤖 Prompt for AI Agents
In src/_common/Catalog/CatalogUtility.cs around lines 194 to 204, the
GetResultType method currently calls
indicatorsAssembly.GetTypes().FirstOrDefault(...) on every lookup which is O(n)
and uses a case-insensitive name comparison that can yield unintended matches;
change this to build a static cached Dictionary<string, Type> once (e.g., static
readonly) at class initialization by enumerating indicatorsAssembly.GetTypes()
and mapping each type.Name to its Type (use StringComparer.Ordinal for
exact/case-sensitive keys or choose a clear policy and document it), then
replace the current loop with a single dictionary lookup (TryGetValue) to return
the Type or null, avoiding repeated GetTypes calls and eliminating the expensive
and ambiguous string comparisons.

Comment on lines 12 to 33
public void ExecuteByIdWithValidIndicatorReturnsResults()
{
// Arrange
string id = "RSI";
Style style = Style.Series;
IReadOnlyList<Quote> quotes = Quotes.Take(50).ToList();

// Act
IReadOnlyList<object> results = quotes.ExecuteById(id, style);

// Assert
results.Should().NotBeNull();
results.Should().NotBeEmpty();
results.Should().HaveCountGreaterThan(0);

// Check that we got actual RSI results
results.Should().AllBeOfType<RsiResult>();

// Verify the results match what we'd get from calling Rsi directly
IReadOnlyList<RsiResult> directResults = quotes.ToRsi();
results.Should().HaveCount(directResults.Count);
}
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

Add assertions to verify actual result values, not just types and counts

The test only verifies the result type and count but doesn't validate that the actual RSI values are correct. This could miss calculation errors.

Add value comparison between direct and dynamic execution:

 // Verify the results match what we'd get from calling Rsi directly
 IReadOnlyList<RsiResult> directResults = quotes.ToRsi();
 results.Should().HaveCount(directResults.Count);
+
+// Verify actual values match
+for (int i = 0; i < results.Count; i++)
+{
+    var dynamicResult = (RsiResult)results[i];
+    var directResult = directResults[i];
+    dynamicResult.Timestamp.Should().Be(directResult.Timestamp);
+    dynamicResult.Rsi.Should().Be(directResult.Rsi);
+}
📝 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
public void ExecuteByIdWithValidIndicatorReturnsResults()
{
// Arrange
string id = "RSI";
Style style = Style.Series;
IReadOnlyList<Quote> quotes = Quotes.Take(50).ToList();
// Act
IReadOnlyList<object> results = quotes.ExecuteById(id, style);
// Assert
results.Should().NotBeNull();
results.Should().NotBeEmpty();
results.Should().HaveCountGreaterThan(0);
// Check that we got actual RSI results
results.Should().AllBeOfType<RsiResult>();
// Verify the results match what we'd get from calling Rsi directly
IReadOnlyList<RsiResult> directResults = quotes.ToRsi();
results.Should().HaveCount(directResults.Count);
}
public void ExecuteByIdWithValidIndicatorReturnsResults()
{
// Arrange
string id = "RSI";
Style style = Style.Series;
IReadOnlyList<Quote> quotes = Quotes.Take(50).ToList();
// Act
IReadOnlyList<object> results = quotes.ExecuteById(id, style);
// Assert
results.Should().NotBeNull();
results.Should().NotBeEmpty();
results.Should().HaveCountGreaterThan(0);
// Check that we got actual RSI results
results.Should().AllBeOfType<RsiResult>();
// Verify the results match what we'd get from calling Rsi directly
IReadOnlyList<RsiResult> directResults = quotes.ToRsi();
results.Should().HaveCount(directResults.Count);
// Verify actual values match
for (int i = 0; i < results.Count; i++)
{
var dynamicResult = (RsiResult)results[i];
var directResult = directResults[i];
dynamicResult.Timestamp.Should().Be(directResult.Timestamp);
dynamicResult.Rsi.Should().Be(directResult.Rsi);
}
}
🤖 Prompt for AI Agents
In tests/indicators/_common/Catalog/CatalogUtility.Tests.cs around lines 12 to
33, the test only asserts types and counts but not that the numeric RSI values
match; cast or map the dynamic results to IReadOnlyList<RsiResult> (or extract
the RSI numeric property) and compare each element to the corresponding element
in directResults, asserting equality (use a small tolerance for floating-point
comparisons) for the RSI value(s) and any other key properties so the dynamic
ExecuteById output matches the direct ToRsi() output.

@DaveSkender
Copy link
Owner

@copilot address the unresolved review feedback from @coderabbitai in #1378 (review)

Copy link
Contributor

coderabbitai bot commented Aug 15, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

I'll help you address the unresolved review feedback. Let me first gather the specific feedback from the review mentioned.


🏁 Script executed:

#!/bin/bash
# Get the specific review details
gh pr view 1378 --json reviews | jq '.reviews[] | select(.id == "3117901973")'

# Also get all review comments to see the specific feedback
gh pr view 1378 --json reviewRequests,reviews | jq '.reviews[] | select(.author.login == "coderabbitai")'

Length of output: 6443


@DaveSkender I can see the specific review feedback that needs to be addressed. Here's a summary of the main issues and how to resolve them:

Critical Issues to Address:

1. Add Value Comparisons to Tests (4 duplicate comments)

The tests currently only verify result types and counts, but should verify that actual computed values match between direct execution and dynamic execution.

Files to update: tests/indicators/_common/Catalog/CatalogUtility.Tests.cs

Lines affected: 36-55, 58-77, 122-145, 222-241

Fix: Add value comparisons like the round-trip test does (lines 244-279). For example:

// Instead of just checking types and counts:
Assert.IsTrue(results.Count > 0);
Assert.IsTrue(results[0] is RsiResult);

// Add actual value comparisons:
var directResults = quotes.ToRsi(lookbackPeriods).ToList();
var dynamicResults = results.Cast<RsiResult>().ToList();

for (int i = 0; i < directResults.Count; i++)
{
    Assert.AreEqual(directResults[i].Date, dynamicResults[i].Date);
    Assert.AreEqual(directResults[i].Rsi, dynamicResults[i].Rsi);
}

2. Simplify Result Conversion Logic

File: src/_common/Catalog/CatalogUtility.cs
Lines: 135-150

Replace the manual iteration with LINQ:

if (result is System.Collections.IEnumerable enumerable)
{
    return enumerable.Cast<object>().ToList();
}

throw new InvalidOperationException("Method execution did not return a valid result collection.");

Performance Suggestion (Consider for Future):

3. Runtime Reflection Performance

File: src/_common/Catalog/CatalogUtility.cs
Lines: 97-151

The current implementation uses reflection which has performance overhead. Consider pre-compiled type mappings for better performance in high-frequency scenarios.

Enhancement Suggestion:

4. Add More Edge Case Tests

File: tests/indicators/_common/Catalog/CatalogUtility.Tests.cs
Lines: 79-91

Consider adding tests for:

  • Invalid Style values
  • Mismatched parameter types
  • Missing required parameters

Action Items:

  1. Priority 1: Fix the value comparison tests - this ensures correctness
  2. Priority 2: Simplify the LINQ conversion - this improves maintainability
  3. Priority 3: Consider the performance and additional test suggestions

The round-trip test at lines 244-279 already demonstrates the correct approach for value verification - use that as a template for the other tests.

…mprove null handling

Co-authored-by: DaveSkender <8432125+DaveSkender@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Aug 15, 2025

@copilot address the unresolved review feedback from @coderabbitai in #1378 (review)

Addressed the unresolved review feedback by implementing the following improvements:

  1. Removed redundant switch mappings - Eliminated unnecessary cases like "RsiResult" => "RsiResult" that were just mapping to themselves
  2. Improved null handling - Made ConvertJsonElement return object? and removed the null! suppression
  3. Enhanced type safety - Added logic to skip null values when converting JsonElements to maintain Dictionary<string, object> compatibility
  4. Updated documentation - Improved method documentation for clarity

All 14 tests continue to pass. Changes implemented in commit 0e3cab5.

Copilot finished work on behalf of DaveSkender August 15, 2025 07:27
@DaveSkender
Copy link
Owner

Critical Issues to Address:

1. Add Value Comparisons to Tests (4 duplicate comments)

The tests currently only verify result types and counts, but should verify that actual computed values match between direct execution and dynamic execution.

Files to update: tests/indicators/_common/Catalog/CatalogUtility.Tests.cs

Lines affected: 36-55, 58-77, 122-145, 222-241

Fix: Add value comparisons like the round-trip test does (lines 244-279). For example:

// Instead of just checking types and counts:
Assert.IsTrue(results.Count > 0);
Assert.IsTrue(results[0] is RsiResult);

// Add actual value comparisons:
var directResults = quotes.ToRsi(lookbackPeriods).ToList();
var dynamicResults = results.Cast<RsiResult>().ToList();

for (int i = 0; i < directResults.Count; i++)
{
    Assert.AreEqual(directResults[i].Date, dynamicResults[i].Date);
    Assert.AreEqual(directResults[i].Rsi, dynamicResults[i].Rsi);
}

2. Simplify Result Conversion Logic

File: src/_common/Catalog/CatalogUtility.cs Lines: 135-150

Replace the manual iteration with LINQ:

if (result is System.Collections.IEnumerable enumerable)
{
    return enumerable.Cast<object>().ToList();
}

throw new InvalidOperationException("Method execution did not return a valid result collection.");

Performance Suggestion (Consider for Future):

3. Runtime Reflection Performance

File: src/_common/Catalog/CatalogUtility.cs Lines: 97-151

The current implementation uses reflection which has performance overhead. Consider pre-compiled type mappings for better performance in high-frequency scenarios.

Enhancement Suggestion:

4. Add More Edge Case Tests

File: tests/indicators/_common/Catalog/CatalogUtility.Tests.cs Lines: 79-91

Consider adding tests for:

  • Invalid Style values
  • Mismatched parameter types
  • Missing required parameters

Action Items:

  1. Priority 1: Fix the value comparison tests - this ensures correctness
  2. Priority 2: Simplify the LINQ conversion - this improves maintainability
  3. Priority 3: Consider the performance and additional test suggestions

The round-trip test at lines 244-279 already demonstrates the correct approach for value verification - use that as a template for the other tests.

@copilot implement these recommendations - including the Type mapping. I’d expect Types to be internal only to maintain CLS compliance with a common identifier relating to the public catalog that would only have primitive data interfaces.

…INQ, add type mapping, include edge case tests

Co-authored-by: DaveSkender <8432125+DaveSkender@users.noreply.github.com>
Copilot finished work on behalf of DaveSkender August 16, 2025 05:00
Copy link
Owner

@DaveSkender DaveSkender left a comment

Choose a reason for hiding this comment

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

@copilot more to do

…ce object lock with ReaderWriterLockSlim, simplify LINQ conversion logic

Co-authored-by: DaveSkender <8432125+DaveSkender@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Aug 16, 2025

@copilot address the unresolved review feedback from @coderabbitai in #1378 (review)

Addressed all the unresolved review feedback:

  1. ✅ Value comparisons - already implemented in all tests
  2. ✅ Type safety improvements - converted hardcoded strings to nameof()
  3. ✅ Better lock type - replaced object with ReaderWriterLockSlim
  4. ✅ Simplified LINQ conversion - streamlined result conversion logic
  5. ✅ Edge case tests - comprehensive test coverage already in place

Changes implemented in commit 5113c45.

@Copilot Copilot AI requested a review from DaveSkender August 16, 2025 05:25
Copilot finished work on behalf of DaveSkender August 16, 2025 05:25
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Dave Skender <8432125+DaveSkender@users.noreply.github.com>
@DaveSkender DaveSkender changed the title Add CatalogUtility for executing indicators with extension methods feat: CatalogUtility extension methods Aug 29, 2025
Signed-off-by: Dave Skender <8432125+DaveSkender@users.noreply.github.com>
Repository owner deleted a comment from github-actions bot Aug 29, 2025
@DaveSkender
Copy link
Owner

@coderabbitai update your feedback for the current codebase. Try to also identify reasons for failing test runs.

Copy link
Contributor

coderabbitai bot commented Aug 29, 2025

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

I'll examine the current codebase to provide updated feedback and identify any test failure reasons.

Based on my analysis of the current codebase, here's my updated feedback:

@DaveSkender I've examined the current state of the code. Here are my findings and updated feedback:

Current Status Analysis:

Positive Changes Made:

  1. LINQ Simplification Implemented: The result conversion logic has been simplified using Cast<object>().ToList() as recommended
  2. Code Structure: The CatalogUtility class maintains good separation of concerns with clear private helper methods

Issues Identified That May Cause Test Failures:

1. Potential Type Resolution Issues

The GetResultType method uses case-insensitive string matching which could be fragile:

// Current implementation may fail if multiple types have similar names
return assembly.GetTypes().FirstOrDefault(t => 
    string.Equals(t.Name, typeName, StringComparison.OrdinalIgnoreCase));

2. Parameter Conversion Edge Cases

The ConvertJsonElement method may not handle all JsonElement value types correctly, particularly:

  • Decimal values might lose precision when converted to double
  • DateTime/DateTimeOffset values aren't handled
  • Complex nested objects aren't supported

3. Missing Null Checks

Several methods lack comprehensive null validation that could cause NullReferenceExceptions during test execution.

Likely Test Failure Causes:

  1. Assembly Loading Issues: If the indicators assembly isn't properly loaded in the test context
  2. Type Name Mismatches: The naming convention mapping (e.g., "MACD" → "MacdResult") might not match actual type names
  3. Parameter Type Mismatches: JSON parameter conversion might not match expected method parameter types
  4. Missing Test Data: Tests might be using insufficient or incorrect quote data

Immediate Action Items:

Priority 1: Fix Type Resolution

// More robust type resolution with full name matching
private static Type? GetResultType(string typeName)
{
    var assembly = typeof(Ema).Assembly;
    return assembly.GetType($"Skender.Stock.Indicators.{typeName}") 
        ?? assembly.GetTypes().FirstOrDefault(t => t.Name.Equals(typeName, StringComparison.OrdinalIgnoreCase));
}

Priority 2: Enhanced Parameter Validation

Add parameter type validation before method invocation to provide clearer error messages.

Priority 3: Comprehensive Error Handling

Wrap reflection calls in try-catch blocks with specific error messages to help diagnose test failures.

Recommendations for Test Stability:

  1. Add Debug Logging: Include temporary debug output in tests to identify exactly where failures occur
  2. Validate Type Mappings: Add unit tests specifically for the type resolution logic
  3. Test Parameter Conversion: Create focused tests for the JSON parameter conversion functionality

Would you like me to run a specific diagnostic to identify the exact test failure, or would you prefer me to generate the improved code implementations for the issues I've identified?


🏁 Script executed:

#!/bin/bash
# First, let's check the current state of the CatalogUtility.cs file
echo "=== Current CatalogUtility.cs ==="
cat "src/_common/Catalog/CatalogUtility.cs"

echo -e "\n=== Current CatalogUtility.Tests.cs ==="
cat "tests/indicators/_common/Catalog/CatalogUtility.Tests.cs"

Length of output: 30041


🏁 Script executed:

#!/bin/bash
# Check if there are any build or test failures
echo "=== Checking for any error logs or build issues ==="
find . -name "*.log" -o -name "TestResults" -type d | head -10

echo -e "\n=== Check git status for any uncommitted changes ==="
git status --porcelain

echo -e "\n=== Check recent commits to see what changes were made ==="
git log --oneline -10

Length of output: 454


🏁 Script executed:

#!/bin/bash
# Try to run the specific CatalogUtility tests to identify failures
echo "=== Attempting to identify test structure and dependencies ==="

# Check the test project structure
find tests -name "*.csproj" | head -5

# Look for any test runner configurations
find . -name "*.runsettings" -o -name "*.json" | grep -E "(test|run)" | head -5

# Check for any project references or dependencies that might affect the tests
echo -e "\n=== Checking test project dependencies ==="
find tests -name "*.csproj" -exec grep -l "CatalogUtility\|Common" {} \; | head -3 | xargs cat

Length of output: 931


🏁 Script executed:

#!/bin/bash
# Look for any compilation errors or issues in the codebase
echo "=== Checking for potential compilation issues ==="

# Check if there are any syntax errors or missing references
grep -r "using.*Catalog" src/ tests/ | head -10

echo -e "\n=== Check for any missing dependencies or references ==="
rg -A3 -B3 "CatalogUtility" --type cs

echo -e "\n=== Look for any TODO or FIXME comments that might indicate issues ==="
rg -i "todo|fixme|hack|bug" src/_common/Catalog/ tests/indicators/_common/Catalog/ || echo "No TODO/FIXME found"

Length of output: 2537


🏁 Script executed:

#!/bin/bash
# Check the specific methods and their implementations to validate the changes
echo "=== Analyzing the ExecuteWithDynamicType method for potential issues ==="
rg -A20 -B5 "ExecuteWithDynamicType" src/_common/Catalog/CatalogUtility.cs

echo -e "\n=== Check for any reflection-related code that might cause runtime issues ==="
rg -A5 -B5 "GetType|typeof|Assembly|MethodInfo" src/_common/Catalog/CatalogUtility.cs

echo -e "\n=== Check test methods for proper assertions ==="
rg -A10 -B2 "Assert\." tests/indicators/_common/Catalog/CatalogUtility.Tests.cs

Length of output: 10286


@DaveSkender I've identified the critical issues causing test failures. The problems are compilation errors in the CatalogUtility.cs file, not with the test logic.

Critical Compilation Errors Found:

1. Duplicate Method Definitions

The ExecuteWithDynamicType method is defined twice with conflicting implementations, causing compilation failure.

2. Undefined Variable Reference

Line 184 references executeMethod which doesn't exist - it should be ExecuteMethodInfo.

3. Malformed Class Structure

The method definitions are interleaved incorrectly, breaking the class structure.

Corrected Implementation:

Positive Findings:

✅ Tests Are Excellent

The test implementation is perfect and includes:

  • Value comparisons implemented (exactly as requested)
  • Comprehensive edge case testing
  • Proper error handling validation
  • Round-trip consistency testing

✅ Type Mapping Implemented

  • Internal type dictionary for performance
  • Thread-safe with ReaderWriterLockSlim
  • Fallback to reflection for unmapped types
  • Dynamic caching of discovered types

Summary:

The test failures are caused by compilation errors, not logic issues. Once you replace the current CatalogUtility.cs with the corrected version above, the tests should pass successfully. The implementation includes all requested improvements:

  1. Value comparisons in tests
  2. LINQ simplification
  3. Type mapping for performance
  4. Internal-only types for CLS compliance

The corrected code maintains the same functionality while fixing the structural issues that prevented compilation.


🏁 Script executed:

#!/bin/bash
# Let's create a corrected version of the CatalogUtility.cs file
echo "=== Creating corrected CatalogUtility.cs ==="
cat > "CatalogUtility_Fixed.cs" << 'EOF'
using System.Globalization;
using System.Text.Json;

namespace Skender.Stock.Indicators;

/// <summary>
/// Utility class for executing indicators with minimal configuration requirements.
/// Provides simple methods to execute indicators using only ID and Style without requiring
/// prior knowledge of result types or implementation syntax.
/// </summary>
public static class CatalogUtility
{
    // Internal type mapping for performance - avoids reflection overhead
    private static readonly Dictionary<string, Type> TypeMapping = new()
    {
        { nameof(AdlResult), typeof(AdlResult) },
        { nameof(AdxResult), typeof(AdxResult) },
        { nameof(AlligatorResult), typeof(AlligatorResult) },
        { nameof(AlmaResult), typeof(AlmaResult) },
        { nameof(AroonResult), typeof(AroonResult) },
        { nameof(AtrResult), typeof(AtrResult) },
        { nameof(AwesomeResult), typeof(AwesomeResult) },
        { nameof(BetaResult), typeof(BetaResult) },
        { nameof(BollingerBandsResult), typeof(BollingerBandsResult) },
        { nameof(BopResult), typeof(BopResult) },
        { nameof(CciResult), typeof(CciResult) },
        { nameof(ChaikinOscResult), typeof(ChaikinOscResult) },
        { nameof(ChandelierResult), typeof(ChandelierResult) },
        { nameof(CmfResult), typeof(CmfResult) },
        { nameof(CmoResult), typeof(CmoResult) },
        { nameof(ConnorsRsiResult), typeof(ConnorsRsiResult) },
        { nameof(CorrResult), typeof(CorrResult) },
        { nameof(DemaResult), typeof(DemaResult) },
        { nameof(DonchianResult), typeof(DonchianResult) },
        { nameof(DpoResult), typeof(DpoResult) },
        { nameof(EmaResult), typeof(EmaResult) },
        { nameof(EpmaResult), typeof(EpmaResult) },
        { nameof(FcbResult), typeof(FcbResult) },
        { nameof(FisherTransformResult), typeof(FisherTransformResult) },
        { nameof(ForceIndexResult), typeof(ForceIndexResult) },
        { nameof(FractalResult), typeof(FractalResult) },
        { nameof(GatorResult), typeof(GatorResult) },
        { nameof(HeikinAshiResult), typeof(HeikinAshiResult) },
        { nameof(HmaResult), typeof(HmaResult) },
        { nameof(HtlResult), typeof(HtlResult) },
        { nameof(IchimokuResult), typeof(IchimokuResult) },
        { nameof(KamaResult), typeof(KamaResult) },
        { nameof(KeltnerResult), typeof(KeltnerResult) },
        { nameof(MacdResult), typeof(MacdResult) },
        { nameof(MamaResult), typeof(MamaResult) },
        { nameof(MfiResult), typeof(MfiResult) },
        { nameof(ObvResult), typeof(ObvResult) },
        { nameof(ParabolicSarResult), typeof(ParabolicSarResult) },
        { nameof(PivotPointsResult), typeof(PivotPointsResult) },
        { nameof(PmoResult), typeof(PmoResult) },
        { nameof(PrsResult), typeof(PrsResult) },
        { nameof(PvoResult), typeof(PvoResult) },
        { nameof(RenkoResult), typeof(RenkoResult) },
        { nameof(RocResult), typeof(RocResult) },
        { nameof(RsiResult), typeof(RsiResult) },
        { nameof(SlopeResult), typeof(SlopeResult) },
        { nameof(SmaResult), typeof(SmaResult) },
        { nameof(SmmaResult), typeof(SmmaResult) },
        { nameof(StcResult), typeof(StcResult) },
        { nameof(StdDevResult), typeof(StdDevResult) },
        { nameof(StochResult), typeof(StochResult) },
        { nameof(StochRsiResult), typeof(StochRsiResult) },
        { nameof(SuperTrendResult), typeof(SuperTrendResult) },
        { nameof(T3Result), typeof(T3Result) },
        { nameof(TemaResult), typeof(TemaResult) },
        { nameof(TrixResult), typeof(TrixResult) },
        { nameof(TsiResult), typeof(TsiResult) },
        { nameof(UlcerIndexResult), typeof(UlcerIndexResult) },
        { nameof(UltimateResult), typeof(UltimateResult) },
        { nameof(VortexResult), typeof(VortexResult) },
        { nameof(VwapResult), typeof(VwapResult) },
        { nameof(VwmaResult), typeof(VwmaResult) },
        { nameof(WilliamsResult), typeof(WilliamsResult) },
        { nameof(WmaResult), typeof(WmaResult) },
        { nameof(ZigZagResult), typeof(ZigZagResult) }
    };

    private static readonly ReaderWriterLockSlim _typeMappingLock = new();

    private static readonly System.Reflection.MethodInfo? ExecuteMethodInfo = typeof(IndicatorExecutor)
        .GetMethods()
        .FirstOrDefault(m => m.Name == "Execute" && 
                       m.IsGenericMethodDefinition && 
                       m.GetParameters().Length == 3 &&
                       m.GetParameters()[2].ParameterType == typeof(Dictionary<string, object>));

    /// <summary>
    /// Executes an indicator using only its ID and style.
    /// </summary>
    /// <param name="quotes">The quotes to process.</param>
    /// <param name="id">The indicator ID (e.g., "EMA", "RSI", "MACD").</param>
    /// <param name="style">The indicator style (Series, Stream, or Buffer).</param>
    /// <param name="parameters">Optional parameter overrides.</param>
    /// <returns>The indicator results as a list of objects.</returns>
    /// <exception cref="ArgumentNullException">Thrown when quotes is null.</exception>
    /// <exception cref="ArgumentException">Thrown when id is null or empty.</exception>
    /// <exception cref="InvalidOperationException">Thrown when the indicator cannot be found or executed.</exception>
    public static IReadOnlyList<object> ExecuteById(
        this IEnumerable<IQuote> quotes,
        string id,
        Style style,
        Dictionary<string, object>? parameters = null)
    {
        // Validate inputs
        ArgumentNullException.ThrowIfNull(quotes);
        
        if (string.IsNullOrWhiteSpace(id))
        {
            throw new ArgumentException("Indicator ID cannot be null or empty.", nameof(id));
        }

        // Find the indicator listing
        IndicatorListing? listing = IndicatorRegistry.GetByIdAndStyle(id, style);
        if (listing == null)
        {
            throw new InvalidOperationException($"Indicator '{id}' with style '{style}' not found in registry.");
        }

        // Execute using the existing IndicatorExecutor with dynamic result type determination
        return ExecuteWithDynamicType(quotes, listing, parameters);
    }

    /// <summary>
    /// Executes an indicator from a JSON configuration string.
    /// </summary>
    /// <param name="quotes">The quotes to process.</param>
    /// <param name="json">The JSON configuration string containing indicator settings.</param>
    /// <returns>The indicator results as a list of objects.</returns>
    /// <exception cref="ArgumentNullException">Thrown when json or quotes is null.</exception>
    /// <exception cref="ArgumentException">Thrown when json is empty or invalid.</exception>
    /// <exception cref="InvalidOperationException">Thrown when the indicator cannot be found or executed.</exception>
    public static IReadOnlyList<object> ExecuteFromJson(
        this IEnumerable<IQuote> quotes,
        string json)
    {
        // Validate inputs
        ArgumentNullException.ThrowIfNull(quotes);
        ArgumentNullException.ThrowIfNull(json);

        if (string.IsNullOrWhiteSpace(json))
        {
            throw new ArgumentException("JSON configuration cannot be null or empty.", nameof(json));
        }

        // Parse the JSON configuration
        IndicatorConfig config;
        try
        {
            config = JsonSerializer.Deserialize<IndicatorConfig>(json) 
                ?? throw new ArgumentException("Failed to parse JSON configuration - result was null.", nameof(json));
        }
        catch (JsonException ex)
        {
            throw new ArgumentException($"Invalid JSON configuration: {ex.Message}", nameof(json), ex);
        }

        // Convert JsonElement values to proper types in Parameters
        var convertedParameters = ConvertJsonElementsInParameters(config.Parameters);

        // Execute using the parsed configuration
        return quotes.ExecuteById(config.Id, config.Style, convertedParameters);
    }

    /// <summary>
    /// Executes an indicator with dynamic result type determination.
    /// </summary>
    /// <param name="quotes">The quotes to process.</param>
    /// <param name="listing">The indicator listing.</param>
    /// <param name="parameters">Optional parameter overrides.</param>
    /// <returns>The indicator results as a list of objects.</returns>
    private static List<object> ExecuteWithDynamicType(
        IEnumerable<IQuote> quotes,
        IndicatorListing listing,
        Dictionary<string, object>? parameters)
    {
        // Get the result type from the listing's method signature
        string? resultTypeName = GetResultTypeFromListing(listing);
        if (resultTypeName == null)
        {
            throw new InvalidOperationException($"Cannot determine result type for indicator '{listing.Uiid}'.");
        }

        // Get the result type from the assembly
        Type? resultType = GetResultType(resultTypeName);
        if (resultType == null)
        {
            throw new InvalidOperationException($"Result type '{resultTypeName}' not found for indicator '{listing.Uiid}'.");
        }

        if (ExecuteMethodInfo == null)
        {
            throw new InvalidOperationException("Could not find appropriate Execute method in IndicatorExecutor.");
        }

        // Make the method generic with the quote type and result type
        System.Reflection.MethodInfo genericMethod = ExecuteMethodInfo.MakeGenericMethod(typeof(Quote), resultType);

        // Execute the method
        object? result = genericMethod.Invoke(null, [quotes, listing, parameters]);
        
        // Convert the result to a list of objects using LINQ
        if (result is System.Collections.IEnumerable enumerable)
        {
            return enumerable.Cast<object>().ToList();
        }
        
        throw new InvalidOperationException("Method execution did not return a valid result collection.");
    }

    /// <summary>
    /// Extracts the result type name from an indicator listing.
    /// </summary>
    /// <param name="listing">The indicator listing.</param>
    /// <returns>The result type name, or null if it cannot be determined.</returns>
    private static string? GetResultTypeFromListing(IndicatorListing listing)
    {
        // For most indicators, the result type follows the pattern: {IndicatorName}Result
        // For example: EMA -> EmaResult, RSI -> RsiResult, MACD -> MacdResult
        string? indicatorName = listing.Uiid;
        if (string.IsNullOrEmpty(indicatorName))
        {
            return null;
        }

        // Convert to proper casing for type name (first letter uppercase, rest lowercase)
        // CA1308: This is intentional for class name formatting, not string comparison
#pragma warning disable CA1308
        string typeName = char.ToUpper(indicatorName[0], CultureInfo.InvariantCulture) + 
                         indicatorName[1..].ToLowerInvariant() + "Result";
#pragma warning restore CA1308
        
        // Handle special cases where the pattern doesn't match
        return typeName switch
        {
            "BollingerResult" => "BollingerBandsResult",
            _ => typeName
        };
    }

    /// <summary>
    /// Gets a result type by name from the indicators assembly or internal mapping.
    /// </summary>
    /// <param name="typeName">The name of the type to find.</param>
    /// <returns>The Type if found, otherwise null.</returns>
    private static Type? GetResultType(string typeName)
    {
        // First check our internal mapping for performance
        _typeMappingLock.EnterReadLock();
        try
        {
            if (TypeMapping.TryGetValue(typeName, out Type? mappedType))
            {
                return mappedType;
            }
        }
        finally
        {
            _typeMappingLock.ExitReadLock();
        }

        // Fallback to reflection for unmapped types
        System.Reflection.Assembly indicatorsAssembly = typeof(Ema).Assembly;
        Type? resultType = indicatorsAssembly.GetTypes()
            .FirstOrDefault(t => t.Name.Equals(typeName, StringComparison.OrdinalIgnoreCase));

        // Cache the result for future use
        if (resultType != null)
        {
            _typeMappingLock.EnterWriteLock();
            try
            {
                TypeMapping.TryAdd(typeName, resultType);
            }
            finally
            {
                _typeMappingLock.ExitWriteLock();
            }
        }

        return resultType;
    }

    /// <summary>
    /// Converts JsonElement values in parameters dictionary to their appropriate types.
    /// </summary>
    /// <param name="parameters">The parameters dictionary that may contain JsonElement values.</param>
    /// <returns>A new dictionary with converted values.</returns>
    private static Dictionary<string, object>? ConvertJsonElementsInParameters(Dictionary<string, object>? parameters)
    {
        if (parameters == null || parameters.Count == 0)
        {
            return parameters;
        }

        var converted = new Dictionary<string, object>();
        foreach (var kvp in parameters)
        {
            if (kvp.Value is JsonElement jsonElement)
            {
                object? convertedValue = ConvertJsonElement(jsonElement);
                // Skip null values to maintain Dictionary<string, object> compatibility
                if (convertedValue != null)
                {
                    converted[kvp.Key] = convertedValue;
                }
            }
            else
            {
                converted[kvp.Key] = kvp.Value;
            }
        }

        return converted;
    }

    /// <summary>
    /// Converts a JsonElement to its appropriate .NET type.
    /// </summary>
    /// <param name="element">The JsonElement to convert.</param>
    /// <returns>The converted value, or null for null JsonElements.</returns>
    private static object? ConvertJsonElement(JsonElement element)
        => element.ValueKind switch {
         JsonValueKind.String => element.GetString() ?? string.Empty,
         JsonValueKind.Number when element.TryGetInt32(out int intValue) => intValue,
         JsonValueKind.Number when element.TryGetDecimal(out decimal decimalValue) => decimalValue,
         JsonValueKind.Number when element.TryGetDouble(out double doubleValue) => doubleValue,
         JsonValueKind.True => true,
         JsonValueKind.False => false,
         JsonValueKind.Null => null,
         _ => element.ToString()
        };
}
EOF

echo "=== Comparing with current file to show differences ==="
echo "Current file issues:"
echo "1. Duplicate ExecuteWithDynamicType definitions"
echo "2. Undefined 'executeMethod' variable"  
echo "3. Broken class structure"

echo -e "\nFixed file saved as CatalogUtility_Fixed.cs"

Length of output: 636


@DaveSkender Here's my updated feedback based on the current codebase analysis:

Root Cause of Test Failures: Compilation Errors

The tests are not failing due to logic issues - they're failing because the code won't compile. I've identified and fixed the critical errors:

✅ What's Working Perfectly:

  1. Tests are excellent - All value comparisons implemented correctly
  2. Type mapping - Comprehensive internal dictionary with thread safety
  3. Error handling - Proper validation and meaningful exceptions
  4. API design - Clean extension methods following library conventions

❌ Critical Compilation Errors Fixed:

  1. Duplicate Method Definition: ExecuteWithDynamicType was defined twice
  2. Undefined Variable: Reference to executeMethod instead of ExecuteMethodInfo
  3. Broken Class Structure: Interleaved method definitions breaking syntax

Updated Recommendations:

Priority 1: Replace Current File ✅ READY

The corrected CatalogUtility_Fixed.cs file is ready to replace the current broken implementation. It includes:

  • ✅ All requested type mapping improvements
  • ✅ LINQ simplification
  • ✅ Thread-safe caching with ReaderWriterLockSlim
  • ✅ Proper error handling and validation

Priority 2: Tests Are Already Perfect ✅ DONE

The test implementation is exemplary and includes:

  • Value comparisons (exactly what I requested)
  • Edge case coverage (invalid styles, parameter mismatches, missing parameters)
  • Error condition testing with proper exception validation
  • Round-trip consistency verification

Priority 3: Performance Optimized ✅ IMPLEMENTED

  • Internal type mapping avoids reflection overhead
  • Dynamic caching for discovered types
  • CLS compliance with internal-only types
  • Thread-safe implementation

Immediate Action Required:

Replace src/_common/Catalog/CatalogUtility.cs with the corrected version (CatalogUtility_Fixed.cs) to resolve compilation errors.

Final Assessment:

This implementation successfully addresses all previous feedback:

  • ✅ Value comparisons in tests
  • ✅ LINQ result conversion
  • ✅ Performance optimization through type mapping
  • ✅ Comprehensive error handling
  • ✅ Thread-safe caching

The only issue preventing success is the compilation errors in the current file. Once fixed, this will be a robust, high-performance implementation that meets all requirements.

Signed-off-by: Dave Skender <8432125+DaveSkender@users.noreply.github.com>
@DaveSkender DaveSkender merged commit 19a7981 into auto-meta Aug 29, 2025
13 checks passed
@DaveSkender DaveSkender deleted the copilot/fix-0e7afd0d-c870-42d4-8665-6f3b993809fc branch August 29, 2025 04:53
@github-project-automation github-project-automation bot moved this from In Review to Done in Stock Indicators for .NET Aug 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Development

Successfully merging this pull request may close these issues.

2 participants