Skip to content

v2.0: Performance and Modern API Refactoring#87

Open
ooples wants to merge 303 commits intomasterfrom
feature/performance-and-refactoring
Open

v2.0: Performance and Modern API Refactoring#87
ooples wants to merge 303 commits intomasterfrom
feature/performance-and-refactoring

Conversation

@ooples
Copy link
Copy Markdown
Collaborator

@ooples ooples commented Jan 4, 2026

Overview

This is the integration branch for OoplesFinance.StockIndicators v2.0. All feature PRs for v2.0 should target this branch, not master.

Key Changes in v2.0

  • New Builder API: Fluent StockIndicatorBuilder pattern replaces Calculate* extension methods
  • Zero Allocations: IndicatorBuffer<T> with ArrayPool for hot path performance
  • SIMD Acceleration: Vectorized math operations via AiDotNet.Tensors
  • Lazy Evaluation: Computation graphs with cross-indicator fusion
  • Full Async Support: IAsyncEnumerable<T> for streaming scenarios
  • 750+ Indicators: Roslyn source-generated typed methods
  • Signal System: Fluent signal definition with conditions and windows
  • Notifications: Email, SMS, Webhook, Telegram, Discord adapters
  • Auto-Trading: Alpaca integration with safety flags

Performance Targets

Metric Target
Heap allocations in hot path 0 bytes
Throughput improvement 2x-5x
Individual indicator latency <1ms

Epic Tracking

Epic 1: AiDotNet.Tensors Spinout

Epic 2: Core Infrastructure

Epic 3: Indicator Catalog & Source Generation

Epic 4: Lazy Evaluation & Fusion

Epic 5: Signal System

Epic 6: Notification Adapters

Epic 7: AutoTrading Adapters

Epic 8: Documentation & Migration

Branch Strategy

master (stable)
  └── feature/performance-and-refactoring (this PR)
        └── feat/* branches for each story

All story branches should:

  1. Branch from feature/performance-and-refactoring
  2. PR back to feature/performance-and-refactoring
  3. Get reviewed and merged here

When all stories are complete, this PR merges to master as v2.0.

Breaking Changes

  • Calculate* methods deprecated (still functional, marked [Obsolete])
  • New StockIndicatorBuilder is the primary API
  • Results returned as IndicatorBuffer<T> (implements IReadOnlyList<T>)

See #126 for migration guide.


🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Fluent builder/runtime for batch & streaming indicators, signals, notifications and auto‑trading; interactive dev console demos.
  • Indicators

    • Major expansion and reorganization: broad set of Ehlers, Chande, Demark, MACD, Momentum, Bollinger variants and multi‑output indicators.
  • Performance & Tooling

    • Zero‑allocation series buffers, SIMD math helpers, new benchmark suites, streaming performance runner and baseline tooling.
  • Documentation

    • Added modernization plan, optimizations guide, migration guide, validation/checklists, and README overhaul.
  • Project

    • Solution reorganized with benchmarks, examples and dev console projects.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 4, 2026

📝 Walkthrough

Walkthrough

Adds a v2 builder/runtime ecosystem: IndicatorDataSource, StockIndicatorBuilder, IndicatorRuntime with pooled IndicatorBuffer and SIMD math; a fluent signal/notification/auto-trade DSL; large reorganization and reintroduction of many Calculate* indicators; benchmark tooling with baseline support; extensive dev-console examples and documentation (modernization plan, optimizations, migration, checklists).

Changes

Cohort / File(s) Summary
Solution & Projects
OoplesFinance.StockIndicators.sln, benchmarks/.../*.csproj, examples/.../*.csproj
Adds benchmark and example projects, updates solution configurations and nested project mappings.
VCS ignore
.gitignore
Adds ignore rule for benchmarks/.baseline/.
Benchmarks & Tools
benchmarks/**
New BenchmarkDotNet suites, baseline setup script, streaming perf runner, benchmark data factory, perf-chart updater, README and perf helpers.
Docs & Roadmap
MODERNIZATION_PLAN.md, OPTIMIZATIONS.md, MIGRATION.md, README.md, STATEFUL_INDICATOR_CHECKLIST.md, docs/**, V2_*
Adds modernization plan, optimizations guide, migration guide, expanded README, validation/migration/fast-path docs and implemented-indicators checklist.
Dev Console Examples
examples/OoplesFinance.StockIndicators.DevConsole/*
New DevConsole project and many FacadeSketches (V1–V6) demonstrating builder/DSL/streaming flows and sketches.
Calculations — reorg & expansion
src/Calculations/**
Massive reorganization: remove/relocate and reintroduce many Calculate* implementations (Bollinger, Chande, Demark, MACD, Momentum, Inputs, Ehlers, MovingAverages, etc.) split across new files.
Builder: core types & handles
src/Builder/Handles/*, src/Builder/Series/*
Adds SeriesHandle, IndicatorKey, SignalHandle, SeriesKey, SymbolId, multi-output structs (MacdResult, BollingerBandsResult, etc.), and SeriesNode.
IndicatorSpec & Catalogs
src/Builder/Specs/IndicatorSpec.cs, src/Builder/Catalogs/*
Adds IndicatorSpec DSL and catalogs: IndicatorCatalog, NotificationCatalog, AutoTradingCatalog, SignalCatalog extensions and presets.
StockIndicatorBuilder
src/Builder/StockIndicatorBuilder.cs
New fluent builder to configure symbols, data, indicators, signals, notifications, auto‑trading, backtests, benchmarks, behavior, and produce an IndicatorRuntime.
Evaluation & Invocation
src/Builder/Evaluation/*
Adds IndicatorInvoker (reflection), SeriesEvaluator (graph compute), and StreamingIndicatorFactory (stream state creation / extractors).
DataSource & Runtime
src/Builder/IndicatorDataSource.cs, src/Builder/IndicatorRuntime.cs
Adds IndicatorDataSource factories (FromBatch/FromStreaming/For) and IndicatorRuntime with buffer pooling, subscriptions, signal evaluation, notifications, and auto-trade dispatch.
Signals DSL
src/Builder/Signals/*
SignalSeries, SignalCondition, SignalRule, SignalGroupRule, builders, SignalWindow, and catalog entry points for composing and emitting signals.
Notifications
src/Builder/Notifications/NotificationTypes.cs
INotificationChannel plus concrete channels: Console, Email, Sms, Webhook, Telegram, Discord and option types.
Auto‑trading
src/Builder/Trading/*
Auto‑trade API: IAutoTradeAdapter, Console & Alpaca adapters, auto-trade builders and AutoTradingConfiguration.
Buffers & Math
src/Builder/Buffers/*
ArrayPool-backed IndicatorBuffer<T> (IReadOnlyList/IDisposable) and SIMD-optimized IndicatorBufferMath (Sum/Average/Min/Max/Variance/StdDev/Dot).
Compute helpers
src/Builder/Compute/*
ComputeContext, ComputeBuffer for pooled compute lifecycles.
Backtest / Optimization
src/Builder/Backtest/*
Walk-forward analyzer, Monte Carlo, BacktestEngine, BacktestResults, position sizing and risk management options.
Builder Options & Runtime types
src/Builder/Options/*, src/Builder/Series/*
Adds DataOptions, SignalOptions, BehaviorOptions, BacktestOptions, BenchmarkOptions, IndicatorSnapshot, multi-series structs and runtime option models.
Dev tooling / CI
.github/workflows/ci.yml, benchmarks/*.ps1
Adds CI workflow and benchmark helper scripts.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/CLI
    participant Builder as StockIndicatorBuilder
    participant Runtime as IndicatorRuntime
    participant Source as IndicatorDataSource
    participant Engine as SeriesEvaluator
    participant Notif as NotificationChannel
    participant Trade as AutoTradeAdapter

    User->>Builder: Configure(...) & Build()
    Builder->>Runtime: instantiate(runtime nodes/keys/signals/notifications)
    User->>Runtime: Subscribe(handles) / Start()
    Runtime->>Source: request batch or subscribe streaming
    Source->>Runtime: emit batch snapshot or tick
    Runtime->>Engine: evaluate active series / resolve nodes
    Engine-->>Runtime: computed series values
    Runtime->>Runtime: evaluate signals & groups
    alt signal triggers
        Runtime->>Notif: NotifyAsync(NotificationEvent)
        Runtime->>Trade: Execute(TradeRequest)
    end
    Runtime->>User: Updated(IndicatorSnapshot)
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~150 minutes

Possibly related issues

Poem

🐰

I hopped through pooled arrays and streaming streams,
I stitched keys and handles into clever schemes.
Signals chimed, the runtime flew,
Benchmarks ran — the numbers grew.
A rabbit cheers these coder dreams.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'v2.0: Performance and Modern API Refactoring' directly and clearly summarizes the primary change: a major version update focusing on performance improvements and modernization of the API through the new Builder pattern.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

@ooples ooples force-pushed the feature/performance-and-refactoring branch from 7a62673 to e43b5b8 Compare January 4, 2026 15:56
Copy link
Copy Markdown

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

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (9)
OPTIMIZATIONS.md-318-330 (1)

318-330: Add language specifier to code block.

The code block at line 320 should specify csharp language.

🔎 Proposed fix for missing language specifier
-```
+```csharp
 src\Calculations\Ehlers.cs:192:            var medianDelta = dpList.TakeLastExt(length).Median();
OPTIMIZATIONS.md-332-372 (1)

332-372: Add language specifier to code block.

The code block at line 334 should specify csharp language.

🔎 Proposed fix for missing language specifier
-```
+```csharp
 src\Calculations\Chande.cs:449:            var r2 = GoodnessOfFit.RSquared(indexList.TakeLastExt(length).Select(x => (double)x),
OPTIMIZATIONS.md-255-316 (1)

255-316: Add language specifier to code block.

The code block at line 257 should specify csharp language for consistency and syntax highlighting.

🔎 Proposed fix for missing language specifier
-```
+```csharp
 src\Calculations\BollingerBands.cs:175:            var aMax = aList.TakeLastExt(length).Max();
OPTIMIZATIONS.md-7-253 (1)

7-253: Add language specifier to code block.

The code block at line 9 should specify a language for syntax highlighting. All file paths in the rolling window sum/average section are C# files.

🔎 Proposed fix for missing language specifier
-```
+```csharp
 src\Calculations\BollingerBands.cs:166:            var x2Sma = x2List.TakeLastExt(length).Average();
benchmarks/README.md-5-8 (1)

5-8: Add language specifiers to fenced code blocks.

The code blocks are missing language specifiers, which affects syntax highlighting and accessibility. Based on static analysis hints, please add appropriate language identifiers:

🔎 Suggested fix
 Run optimized benchmarks:
-```
+```bash
 dotnet run -c Release --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj

Run a subset:
- +bash
dotnet run -c Release --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -- --filter SMA


Run streaming fanout throughput/latency:
-```
+```bash
dotnet run -c Release --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -- --streaming-perf --ticks 100000

Use streaming fanout flags:
- +bash
dotnet run -c Release --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -- --streaming-perf --include-outputs
dotnet run -c Release --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -- --streaming-perf --extended


Compare optimized vs baseline (master by default):
-```
+```powershell
.\benchmarks\setup-baseline.ps1 -Ref master
dotnet build -c Release -p:AssemblyName=OoplesFinance.StockIndicators.Original -p:TargetFramework=net7.0 benchmarks/.baseline/src/OoplesFinance.StockIndicators.csproj
dotnet run -c Release --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -- --filter *IndicatorBenchmarks*
</details>




Also applies to: 10-13, 15-18, 19-23, 26-31

</blockquote></details>
<details>
<summary>src/Calculations/BollingerBands/BollingerBandsPart1.cs-251-254 (1)</summary><blockquote>

`251-254`: **Unused local variables.**

`fibTop2`, `fibTop1`, `fibBottom1`, and `fibBottom2` are computed but never used. Either these should be included in the output dictionary or removed to reduce computational waste.


<details>
<summary>🔎 Proposed fix to remove unused variables</summary>

```diff
             var r1 = atr * fibRatio1;
             var r2 = atr * fibRatio2;
             var r3 = atr * fibRatio3;

             var prevFibTop3 = GetLastOrDefault(fibTop3List);
             var fibTop3 = sma + r3;
             fibTop3List.Add(fibTop3);

-            var fibTop2 = sma + r2;
-            var fibTop1 = sma + r1;
-            var fibBottom1 = sma - r1;
-            var fibBottom2 = sma - r2;
-
             var prevFibBottom3 = GetLastOrDefault(fibBottom3List);
             var fibBottom3 = sma - r3;
             fibBottom3List.Add(fibBottom3);

Alternatively, if these intermediate bands are intentionally calculated for future use, add them to the output:

🔎 Alternative: include all Fibonacci bands in output
         stockData.SetOutputValues(() => new Dictionary<string, List<double>>{
             { "UpperBand", fibTop3List },
+            { "UpperBand2", fibTop2List },
+            { "UpperBand1", fibTop1List },
             { "MiddleBand", smaList },
+            { "LowerBand1", fibBottom1List },
+            { "LowerBand2", fibBottom2List },
             { "LowerBand", fibBottom3List }
         });
src/Calculations/Ehlers/EhlersLtoR.cs-1014-1056 (1)

1014-1056: Use of Math.Log without guarding zero/negative input in NET calculation

In CalculateEhlersNoiseEliminationTechnology you compute:

num -= Math.Sign(xArray[j] - xArray[k]);
...
var net = denom != 0 ? num / denom : 0;

This part is fine, but elsewhere in this file (e.g., in CalculateEhlersRocketRelativeStrengthIndex at lines 193–195) and other methods you use Math.Log(...) directly instead of the safer MathHelper.Log, which guards invalid inputs. If any intermediate ratio becomes 0 (or negative due to rounding), Math.Log will yield -Infinity/NaN and can contaminate subsequent calculations and signals.

Prefer MathHelper.Log (or equivalent clamped wrapper) for these transforms.

Illustrative change for Rocket RSI log
- var ssf2PoleLog = Math.Log(ssf2PoleTempLog);
+ var ssf2PoleLog = MathHelper.Log(ssf2PoleTempLog);
src/Calculations/Ehlers/EhlersStoZ.cs-1030-1069 (1)

1030-1069: Signal-to-noise ratio V1: potential Math.Log(0) / negative input

In CalculateEhlersSignalToNoiseRatioV1:

var v2 = (0.2 * ((inPhase * inPhase) + (quad * quad))) + (0.8 * prevV2);
...
var range = (0.2 * (currentHigh - currentLow)) + (0.8 * prevRange);
...
var amp = range != 0 ? (0.25 * ((10 * Math.Log(v2 / (range * range)) / Math.Log(10)) + 1.9)) + (0.75 * prevAmp) : 0;

Even with nonzero range, v2 can be zero early on, making v2 / (range * range) zero and causing Math.Log(0)-Infinity. This can propagate NaNs/Infinities into amp and then into signals.

Consider:

  • Guarding the ratio with a small positive floor, and/or
  • Using the safer MathHelper.Log wrapper.
Example guard
- var ratio = v2 / (range * range);
- var amp = range != 0 ? (0.25 * ((10 * Math.Log(ratio) / Math.Log(10)) + 1.9)) + (0.75 * prevAmp) : 0;
+ var denom = range * range;
+ var ratio = denom > 0 ? Math.Max(v2 / denom, 1e-12) : 1e-12;
+ var amp = denom > 0
+    ? (0.25 * ((10 * Math.Log(ratio) / Math.Log(10)) + 1.9)) + (0.75 * prevAmp)
+    : 0;
src/Calculations/Ehlers/EhlersAtoD.cs-700-737 (1)

700-737: Auto-correlation periodogram: length3 allowed to be zero but used as loop lower bound

CalculateEhlersAutoCorrelationPeriodogram clamps length3 as:

length3 = Math.Max(length3, 0);
...
for (var k = length3; k <= length1; k++)
{
    var prevCorr = i >= k ? corrList[i - k] : 0;
    ...
}

length3 is allowed to be 0, which leads to reading corrList[i] as the “lag 0” term. That’s not inherently wrong, but in practice you probably intended a strictly positive minimum lag (as in the Ehlers examples). If zero is not meaningful here, clamp to at least 1.

Clamp `length3` to ≥ 1
- length3 = Math.Max(length3, 0);
+ length3 = Math.Max(length3, 1);
🧹 Nitpick comments (29)
src/Calculations/MovingAverages/EhlersMovingAverages.cs (2)

701-714: Redundant variable: prevFilt duplicates prevFilter1.

Both prevFilter1 (line 705) and prevFilt (line 708) retrieve the last filter value. This is redundant since they hold the same value when i >= 1. Consider using just one approach for consistency with other methods in this file.

🔎 Proposed fix
         for (var i = 0; i < stockData.Count; i++)
         {
             var currentValue = inputList[i];
             var prevValue = i >= 1 ? inputList[i - 1] : 0;
             var prevFilter1 = i >= 1 ? filtList[i - 1] : 0;
             var prevFilter2 = i >= 2 ? filtList[i - 2] : 0;

-            var prevFilt = GetLastOrDefault(filtList);
             var filt = (coeff1 * ((currentValue + prevValue) / 2)) + (coeff2 * prevFilter1) + (coeff3 * prevFilter2);
             filtList.Add(filt);

-            var signal = GetCompareSignal(currentValue - filt, prevValue - prevFilt);
+            var signal = GetCompareSignal(currentValue - filt, prevValue - prevFilter1);
             signalsList?.Add(signal);
         }

1640-1779: Consider extracting repeated filter calculations.

This method is quite long (~140 lines) with similar filter calculations repeated for each frequency band. While the hardcoded coefficients make loop-based refactoring complex, consider extracting the filter computation into a local helper function to reduce duplication.

src/Calculations/Momentum.cs (1)

13-15: Consider removing this file if no longer needed.

Since all momentum indicator methods have been relocated to the new subdirectory files (MomentumAtoD.cs, MomentumLtoR.cs, MomentumStoZ.cs), this empty partial class shell may be unnecessary. If it's kept solely as a placeholder, consider adding a comment explaining its purpose, or remove the file entirely to reduce clutter.

src/Calculations/Momentum/MomentumLtoR.cs (1)

143-151: Minor inconsistency: uses inputList.Count instead of stockData.Count.

For consistency with other methods in this file (and the other loops in this same method), consider using stockData.Count here.

🔎 Proposed fix
-        for (var i = 0; i < inputList.Count; i++)
+        for (var i = 0; i < stockData.Count; i++)
src/Calculations/Momentum/MomentumStoZ.cs (1)

51-52: Clarify the intermediate SetCustomValues usage.

SetCustomValues(utmList) is called here to pass data to CalculateRelativeStrengthIndex, which reads from CustomValuesList. This is a side-effect-based communication pattern. Consider adding a brief comment to clarify this is intentional, as it's not immediately obvious why SetCustomValues is called twice (here and at line 68).

🔎 Suggested comment
+        // Pass UTM values to RSI calculation via CustomValuesList
         stockData.SetCustomValues(utmList);
         var utmRsiList = CalculateRelativeStrengthIndex(stockData, maType, length1, length1).CustomValuesList;
src/Calculations/Momentum/MomentumAtoD.cs (3)

72-73: Unused tempList variable.

The tempList is declared on line 72 and populated on line 89, but it's never read. Only tempSumWindow is used for the calculation. Consider removing this dead code.

🔎 Proposed fix
         List<double> amomList = new(stockData.Count);
         List<double> amomsList = new(stockData.Count);
-        var tempSumWindow = new RollingSum();
+        var inputSumWindow = new RollingSum();
         var amomSumWindow = new RollingSum();
         List<Signal>? signalsList = CreateSignalsList(stockData);
         var (inputList, _, _, _, _) = GetInputValuesList(stockData);
...
         for (var i = 0; i < stockData.Count; i++)
         {
             var currentEma = emaList[i];

             var currentValue = inputList[i];
-            tempList.Add(currentValue);
-            tempSumWindow.Add(currentValue);
+            inputSumWindow.Add(currentValue);

-            var sma = tempSumWindow.Average(p);
+            var sma = inputSumWindow.Average(p);

Also remove tempList declaration on line 72.

Also applies to: 88-89


134-151: Silent failure when data counts don't match.

If stockData.Count != marketDataClass.InputValues.Count, the method silently returns with empty output values and no signals. Consider either throwing an ArgumentException or logging a warning to help users diagnose issues when comparing mismatched data sets.

🔎 Option: Throw exception for mismatched counts
+        if (stockData.Count != marketDataClass.InputValues.Count)
+        {
+            throw new ArgumentException(
+                $"Stock data count ({stockData.Count}) must match market data count ({marketDataClass.InputValues.Count}).",
+                nameof(marketDataClass));
+        }
+
-        if (stockData.Count == marketDataClass.InputValues.Count)
-        {
             var pmoList = CalculatePriceMomentumOscillator(stockData, maType, length1, length2, signalLength).CustomValuesList;
             ...
-        }

262-269: Overly broad exception handling.

The bare catch clause catches all exceptions, which could mask unexpected errors. The intent appears to be handling division by zero or overflow. Consider a more targeted approach.

🔎 Proposed fix using explicit checks
-            int dTime;
-            try
-            {
-                dTime = asd != 0 ? Math.Min(upLimit, (int)Math.Ceiling(length3 / asd)) : 0;
-            }
-            catch
-            {
-                dTime = upLimit;
-            }
+            int dTime;
+            if (asd <= 0 || double.IsNaN(asd) || double.IsInfinity(asd))
+            {
+                dTime = upLimit;
+            }
+            else
+            {
+                var ratio = length3 / asd;
+                dTime = ratio >= upLimit ? upLimit : (int)Math.Ceiling(ratio);
+            }
src/Calculations/Chande/ChandeStoZ.cs (1)

31-32: Minor: Simplify initialization logic.

The ternary conditions can be simplified. When i >= 1, GetLastOrDefault(vidya1List) will return the last added value (since we add to the list at the end of each iteration), so the fallback to currentValue is used only on the first iteration. Consider simplifying:

🔎 Suggested simplification
-            var prevVidya1 = i >= 1 ? GetLastOrDefault(vidya1List) : currentValue;
-            var prevVidya2 = i >= 1 ? GetLastOrDefault(vidya2List) : currentValue;
+            var prevVidya1 = i > 0 ? vidya1List[i - 1] : currentValue;
+            var prevVidya2 = i > 0 ? vidya2List[i - 1] : currentValue;
src/Calculations/Chande/ChandeLtoR.cs (1)

151-153: Potential output key collision.

Both CalculateChandeMomentumOscillatorAbsolute (line 152) and CalculateChandeMomentumOscillatorAverage (line 213) use the output key "Cmoa". While these operate on separate StockData instances, using identical keys for different indicators could cause confusion for API consumers. Consider using distinct keys like "CmoAbsolute" and "CmoAverage".

Also applies to: 212-214

benchmarks/setup-baseline.ps1 (1)

15-15: Consider adding error handling for git worktree creation.

If the specified $Ref doesn't exist or the worktree creation fails, the script exits without feedback. Consider adding error handling:

🔎 Suggested improvement
-git -C $repoRoot worktree add $baselinePath $Ref
+$output = git -C $repoRoot worktree add $baselinePath $Ref 2>&1
+if ($LASTEXITCODE -ne 0)
+{
+    Write-Error "Failed to create worktree for ref '$Ref': $output"
+    exit 1
+}
+Write-Host "Baseline created at $baselinePath from ref '$Ref'"
benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj (1)

11-11: Consider making UseBaseline configurable rather than hardcoded.

The hardcoded <UseBaseline>true</UseBaseline> requires all developers to have the baseline assembly setup, which may not be desirable during initial development or when the baseline doesn't exist yet. This could cause build failures for new contributors.

🔎 Suggested approach

Remove the hardcoded value and allow it to be overridden via command-line or environment:

-    <UseBaseline>true</UseBaseline>
+    <UseBaseline Condition="'$(UseBaseline)'==''">false</UseBaseline>

This defaults to false but allows developers to enable it via dotnet build -p:UseBaseline=true when needed.

MODERNIZATION_PLAN.md (1)

6-7: Minor grammar: Consider hyphenating compound modifiers.

Per static analysis hints:

  • Line 6: "high performance core" → "high-performance core"
  • Line 7: "first class streaming" → "first-class streaming"
Suggested fix
-- Preserve the public API where possible while enabling a high performance core.
-- Add first class streaming support (tick-by-tick updates).
+- Preserve the public API where possible while enabling a high-performance core.
+- Add first-class streaming support (tick-by-tick updates).
src/Calculations/Macd/MacdAtoD.cs (2)

76-77: Unused computed values: macdBlue and macdYellow.

These values are computed using blueMult and yellowMult parameters but are never added to the output dictionary or used elsewhere. Either include them in the output or remove the computation and associated parameters.

Option 1: Include in outputs
 stockData.SetOutputValues(() => new Dictionary<string, List<double>>{
     { "Macd1", macd4List },
     { "Signal1", macd4SignalLineList },
     { "Histogram1", macd4HistogramList },
     { "Macd2", macd2List },
     { "Signal2", macd2SignalLineList },
-    { "Histogram2", macd2HistogramList }
+    { "Histogram2", macd2HistogramList },
+    { "MacdBlue", macdBlueList },
+    { "MacdYellow", macdYellowList }
 });

This would require accumulating macdBlue and macdYellow into lists during the loop.

Also applies to: 83-84


103-103: Empty list passed to SetCustomValues.

Passing an empty list to SetCustomValues is unusual. If there's no meaningful custom value for this indicator, consider documenting why or using a representative series (like one of the MACD lines).

src/Calculations/Demark.cs (1)

13-15: Consider removing this empty file.

After moving all Demark indicator methods to the new subdirectory files (DemarkEtoK.cs, DemarkLtoR.cs, DemarkStoZ.cs), this file only contains an empty partial class. Unless there's a specific reason to keep it (e.g., for future additions or build system requirements), consider removing it to reduce code clutter.

src/Calculations/Macd/MacdLtoR.cs (2)

118-127: Output values may be incomplete.

The method computes macdSignalLineList and macdHistogramList but only outputs Macd, I1, and I2. For consistency with other MACD methods and to expose the signal line and histogram to consumers, consider including them in the output:

🔎 Suggested addition
 stockData.SetOutputValues(() => new Dictionary<string, List<double>>{
     { "Macd", macdList },
+    { "Signal", macdSignalLineList },
+    { "Histogram", macdHistogramList },
     { "I1", i1List },
     { "I2", i2List }
 });

307-315: Minor: macdMirrorHistogram is computed but not used.

The variable macdMirrorHistogram is retrieved at line 310 but not used in the signal calculation. If this is intentional (signals based only on the primary MACD), this is fine. Otherwise, consider either removing the variable or using it for additional signal logic.

benchmarks/OoplesFinance.StockIndicators.Benchmarks/StreamingPerformanceRunner.cs (1)

94-103: Consider using Array.Copy or span for better performance.

The manual loop for trimming the timeframes array can be simplified:

🔎 Suggested simplification
-        var trimmed = new BarTimeframe[timeframeCount];
-        for (var i = 0; i < timeframeCount; i++)
-        {
-            trimmed[i] = timeframes[i];
-        }
-
-        timeframes = trimmed;
+        timeframes = timeframes[..timeframeCount];
src/Calculations/Inputs.cs (1)

13-15: Consider removing this empty file.

Similar to Demark.cs, all input calculation methods have been moved to the new subdirectory files (InputsAtoD.cs, InputsEtoK.cs, InputsLtoR.cs, InputsStoZ.cs). Consider removing this empty file unless it serves a specific purpose.

benchmarks/OoplesFinance.StockIndicators.Benchmarks/IndicatorBenchmarks.cs (2)

404-417: Low iteration counts may produce unreliable benchmark results.

The configuration uses WithWarmupCount(1), WithIterationCount(1), and WithInvocationCount(1). This will yield statistically unreliable measurements with high variance. BenchmarkDotNet's default settings are designed to produce stable results.

Consider using higher values for production benchmarks, or document this configuration as "quick smoke test" mode:

🔎 Proposed fix for more reliable benchmarks
     private sealed class BenchmarkConfig : ManualConfig
     {
         public BenchmarkConfig()
         {
             AddJob(Job.Default
                 .WithMinIterationTime(TimeInterval.FromMilliseconds(100))
-                .WithWarmupCount(1)
-                .WithIterationCount(1)
-                .WithInvocationCount(1)
-                .WithUnrollFactor(1));
+                .WithWarmupCount(3)
+                .WithIterationCount(5)
+                .WithUnrollFactor(1));
             AddExporter(MarkdownExporter.GitHub);
             AddExporter(CsvExporter.Default);
         }
     }

340-345: MACD benchmark ignores the Length parameter.

CalculateMovingAverageConvergenceDivergence() is called without using the Length parameter, unlike most other benchmarks. This means it will run with identical inputs across all Length variations, wasting benchmark time.

Consider either:

  1. Passing Length to configure MACD's internal periods
  2. Or excluding it from length-parameterized runs
src/Calculations/BollingerBands/BollingerBandsPart2.cs (1)

219-233: Multiple SetCustomValues calls within one method may be confusing.

The pattern of setting CustomValuesList (lines 219, 232) to pass data into CalculateStandardDeviationVolatility is unconventional. While it works, it creates an implicit data dependency that's not obvious from the method signatures.

This pattern is fragile - if someone refactors without understanding that CalculateStandardDeviationVolatility reads from CustomValuesList, the calculation could break silently.

Consider documenting this pattern or refactoring to make the data flow explicit.

src/Calculations/Ehlers/EhlersEtoK.cs (2)

203-209: Nested loop has O(n × length) complexity.

The inner loop iterates from hLength to length for each data point to find hh2/ll2. This is inherent to the Hurst Coefficient calculation but worth noting for performance-sensitive applications with large datasets.

For very large datasets, consider pre-computing rolling max/min using techniques similar to RollingMinMax in other parts of the codebase.


867-917: CalculateEhlersHannWindowIndicator and CalculateEhlersHammingWindowIndicator share duplicate logic.

Lines 832-864 (Hann) and 884-906 (Hamming) have nearly identical structure. The only difference is the moving average type used. Consider extracting a shared helper or using a common base implementation.

src/Calculations/MovingAverages/MovingAveragesAtoD.cs (2)

1143-1189: O(n × length²) complexity in DistanceWeightedMovingAverage.

The double nested loop (lines 1155-1165) calculates pairwise distances between all values in the window. For length = 100 and 10,000 data points, this results in ~100 million operations.

This complexity is inherent to the algorithm's definition, but consider adding a note in the XML documentation to warn users about performance with large window sizes.


981-989: Fixed-point iteration loop with 5000 max iterations.

The convergence loop for k uses a tolerance-based early exit, which is good. However, the magic numbers (5000 iterations, 10^-5 tolerance) should ideally be configurable or documented.

For most cases, convergence should happen much faster than 5000 iterations.

src/Calculations/Ehlers/EhlersStoZ.cs (1)

141-175: Ehlers Simple Decycler: unused prevDecycler variable

In CalculateEhlersSimpleDecycler you compute:

var prevDecycler = GetLastOrDefault(decyclerList);
var decycler = currentValue - hp;
...
var signal = GetCompareSignal(currentValue - decycler, prevValue - prevDecycler);

prevDecycler is only used to build the signal; behavior is fine, but the naming might suggest it also participates in decycler computation. No functional bug, just potential confusion; consider either using it explicitly in the decycler calculation or dropping it to keep the intent clear.

src/Calculations/Ehlers/EhlersAtoD.cs (1)

1546-1599: Detrended Leading Indicator: EMA seeds depend on current price only

In CalculateEhlersDetrendedLeadingIndicator, you seed the EMAs with the current price when i == 0:

var prevEma1 = i >= 1 ? GetLastOrDefault(ema1List) : currentPrice;
var prevEma2 = i >= 1 ? GetLastOrDefault(ema2List) : currentPrice;
...
var ema1 = (alpha * currentPrice) + ((1 - alpha) * prevEma1);
...
var ema2 = (alpha2 * currentPrice) + ((1 - alpha2) * prevEma2);

This is numerically acceptable but gives the very first bar an EMA equal to the price, which may or may not match your other EMA helpers’ seeding conventions (e.g., seeding from an average of initial points). If consistency with GetMovingAverageList’s EMA behavior is important, you might want to harmonize the seeding approach.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 18b13bc and e43b5b8.

📒 Files selected for processing (150)
  • .gitignore
  • MODERNIZATION_PLAN.md
  • OPTIMIZATIONS.md
  • OoplesFinance.StockIndicators.sln
  • README.md
  • benchmarks/OoplesFinance.StockIndicators.Benchmarks/BenchmarkDataFactory.cs
  • benchmarks/OoplesFinance.StockIndicators.Benchmarks/BenchmarkProfile.cs
  • benchmarks/OoplesFinance.StockIndicators.Benchmarks/IndicatorBenchmarks.cs
  • benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj
  • benchmarks/OoplesFinance.StockIndicators.Benchmarks/Program.cs
  • benchmarks/OoplesFinance.StockIndicators.Benchmarks/StreamingPerformanceRunner.cs
  • benchmarks/README.md
  • benchmarks/setup-baseline.ps1
  • src/Calculations/BollingerBands.cs
  • src/Calculations/BollingerBands/BollingerBandsPart1.cs
  • src/Calculations/BollingerBands/BollingerBandsPart2.cs
  • src/Calculations/Chande.cs
  • src/Calculations/Chande/ChandeAtoD.cs
  • src/Calculations/Chande/ChandeEtoK.cs
  • src/Calculations/Chande/ChandeLtoR.cs
  • src/Calculations/Chande/ChandeStoZ.cs
  • src/Calculations/Demark.cs
  • src/Calculations/Demark/DemarkEtoK.cs
  • src/Calculations/Demark/DemarkLtoR.cs
  • src/Calculations/Demark/DemarkStoZ.cs
  • src/Calculations/Ehlers.cs
  • src/Calculations/Ehlers/EhlersAtoD.cs
  • src/Calculations/Ehlers/EhlersEtoK.cs
  • src/Calculations/Ehlers/EhlersLtoR.cs
  • src/Calculations/Ehlers/EhlersStoZ.cs
  • src/Calculations/Inputs.cs
  • src/Calculations/Inputs/InputsAtoD.cs
  • src/Calculations/Inputs/InputsEtoK.cs
  • src/Calculations/Inputs/InputsLtoR.cs
  • src/Calculations/Inputs/InputsStoZ.cs
  • src/Calculations/Macd.cs
  • src/Calculations/Macd/MacdAtoD.cs
  • src/Calculations/Macd/MacdEtoK.cs
  • src/Calculations/Macd/MacdLtoR.cs
  • src/Calculations/Macd/MacdStoZ.cs
  • src/Calculations/Momentum.cs
  • src/Calculations/Momentum/MomentumAtoD.cs
  • src/Calculations/Momentum/MomentumLtoR.cs
  • src/Calculations/Momentum/MomentumStoZ.cs
  • src/Calculations/MovingAverages/EhlersMovingAverages.cs
  • src/Calculations/MovingAverages/MovingAveragesAtoD.cs
  • src/Calculations/MovingAverages/MovingAveragesEtoL.cs
  • src/Calculations/MovingAverages/MovingAveragesMtoR.cs
  • src/Calculations/MovingAverages/MovingAveragesStoZ.cs
  • src/Calculations/MovingAverages/StandardMovingAverages.cs
  • src/Calculations/MovingAvg.cs
  • src/Calculations/Oscillator.cs
  • src/Calculations/Oscillators/ClassicOscillators.cs
  • src/Calculations/Oscillators/OscillatorsAtoD.cs
  • src/Calculations/Oscillators/OscillatorsEtoK.cs
  • src/Calculations/Oscillators/OscillatorsLtoR.cs
  • src/Calculations/Oscillators/OscillatorsStoZ.cs
  • src/Calculations/PivotPoint.cs
  • src/Calculations/PivotPoint/PivotPointAtoD.cs
  • src/Calculations/PivotPoint/PivotPointEtoK.cs
  • src/Calculations/PivotPoint/PivotPointLtoR.cs
  • src/Calculations/PivotPoint/PivotPointStoZ.cs
  • src/Calculations/Ppo.cs
  • src/Calculations/Ppo/PpoAtoD.cs
  • src/Calculations/Ppo/PpoEtoK.cs
  • src/Calculations/Ppo/PpoLtoR.cs
  • src/Calculations/Ppo/PpoStoZ.cs
  • src/Calculations/PriceChannel.cs
  • src/Calculations/PriceChannel/PriceChannelAtoD.cs
  • src/Calculations/PriceChannel/PriceChannelEtoK.cs
  • src/Calculations/PriceChannel/PriceChannelLtoR.cs
  • src/Calculations/PriceChannel/PriceChannelStoZ.cs
  • src/Calculations/Ratio.cs
  • src/Calculations/Ratio/RatioAtoD.cs
  • src/Calculations/Ratio/RatioEtoK.cs
  • src/Calculations/Ratio/RatioLtoR.cs
  • src/Calculations/Ratio/RatioStoZ.cs
  • src/Calculations/Rsi.cs
  • src/Calculations/Rsi/RsiAtoD.cs
  • src/Calculations/Rsi/RsiEtoK.cs
  • src/Calculations/Rsi/RsiLtoR.cs
  • src/Calculations/Rsi/RsiStoZ.cs
  • src/Calculations/Stochastic.cs
  • src/Calculations/Stochastic/StochasticAtoD.cs
  • src/Calculations/Stochastic/StochasticEtoK.cs
  • src/Calculations/Stochastic/StochasticLtoR.cs
  • src/Calculations/Stochastic/StochasticStoZ.cs
  • src/Calculations/TrailingStop.cs
  • src/Calculations/TrailingStop/TrailingStopAtoD.cs
  • src/Calculations/TrailingStop/TrailingStopEtoK.cs
  • src/Calculations/TrailingStop/TrailingStopLtoR.cs
  • src/Calculations/Trend.cs
  • src/Calculations/Trend/TrendAtoD.cs
  • src/Calculations/Trend/TrendEtoK.cs
  • src/Calculations/Trend/TrendLtoR.cs
  • src/Calculations/Trend/TrendStoZ.cs
  • src/Calculations/Volatility.cs
  • src/Calculations/Volatility/VolatilityAtoD.cs
  • src/Calculations/Volatility/VolatilityEtoK.cs
  • src/Calculations/Volatility/VolatilityLtoR.cs
  • src/Calculations/Volatility/VolatilityStoZ.cs
  • src/Calculations/Volume.cs
  • src/Calculations/Volume/VolumeAtoD.cs
  • src/Calculations/Volume/VolumeEtoK.cs
  • src/Calculations/Volume/VolumeLtoR.cs
  • src/Calculations/Volume/VolumeStoZ.cs
  • src/Calculations/Wilder.cs
  • src/Calculations/Wilder/WilderAtoD.cs
  • src/Calculations/Wilder/WilderStoZ.cs
  • src/Compatibility/SpanCompat.cs
  • src/Compatibility/VectorMath.cs
  • src/Core/MovingAverageCore.cs
  • src/Enums/IndicatorCost.cs
  • src/Helpers/CalculationsHelper.cs
  • src/Helpers/PooledRingBuffer.cs
  • src/Helpers/RollingWindow.cs
  • src/Models/IndicatorOptions.cs
  • src/Models/StockData.cs
  • src/OoplesFinance.StockIndicators.csproj
  • src/Streaming/BarAggregator.cs
  • src/Streaming/BarTimeframe.cs
  • src/Streaming/IndicatorCostMap.cs
  • src/Streaming/IndicatorRegistry.cs
  • src/Streaming/Providers/Alpaca/AlpacaStreamSource.cs
  • src/Streaming/Providers/Alpaca/AlpacaStreamingSession.cs
  • src/Streaming/Providers/Alpaca/IAlpacaStreamClient.cs
  • src/Streaming/StatefulIndicators.cs
  • src/Streaming/StreamingIndicatorEngine.cs
  • src/Streaming/StreamingIndicatorState.cs
  • src/Streaming/StreamingInputSelector.cs
  • src/Streaming/StreamingInterfaces.cs
  • src/Streaming/StreamingModels.cs
  • src/Streaming/StreamingOptions.cs
  • src/Streaming/StreamingSession.cs
  • tests/CalculationsTests/BollingerBandsTests.cs
  • tests/CalculationsTests/EhlersTests.cs
  • tests/CalculationsTests/MovingAverageTests.cs
  • tests/CalculationsTests/OscillatorTests.cs
  • tests/CalculationsTests/RsiTests.cs
  • tests/CalculationsTests/WilderTests.cs
  • tests/ModelsTests/StockDataLazyViewTests.cs
  • tests/StreamingTests/BarAggregatorTests.cs
  • tests/StreamingTests/IndicatorCostMapTests.cs
  • tests/StreamingTests/IndicatorRegistryTests.cs
  • tests/StreamingTests/StreamingIndicatorEngineTests.cs
  • tests/StreamingTests/StreamingInputSelectorTests.cs
  • tests/StreamingTests/StreamingOptionsTests.cs
  • tests/StreamingTests/StreamingParityTests.cs
  • tests/StreamingTests/StreamingStatefulParityTests.cs
  • tests/Usings.cs
🧰 Additional context used
🧬 Code graph analysis (24)
src/Calculations/Inputs/InputsAtoD.cs (1)
src/Helpers/CalculationsHelper.cs (3)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
src/Calculations/Inputs/InputsLtoR.cs (1)
src/Helpers/CalculationsHelper.cs (3)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
src/Calculations/Inputs/InputsEtoK.cs (1)
src/Helpers/CalculationsHelper.cs (3)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
src/Calculations/Inputs/InputsStoZ.cs (1)
src/Helpers/CalculationsHelper.cs (3)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
src/Calculations/Macd/MacdAtoD.cs (2)
src/Core/MovingAverageCore.cs (1)
  • ExponentialMovingAverage (53-81)
src/Helpers/CalculationsHelper.cs (3)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
src/Calculations/Demark/DemarkEtoK.cs (2)
src/Core/MovingAverageCore.cs (1)
  • SimpleMovingAverage (7-25)
src/Helpers/CalculationsHelper.cs (3)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
src/Calculations/Macd/MacdEtoK.cs (3)
src/Helpers/RollingWindow.cs (2)
  • RollingSum (20-79)
  • Average (58-67)
src/Helpers/CalculationsHelper.cs (3)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
src/Core/MovingAverageCore.cs (2)
  • SimpleMovingAverage (7-25)
  • ExponentialMovingAverage (53-81)
src/Calculations/Demark/DemarkStoZ.cs (1)
src/Helpers/CalculationsHelper.cs (3)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
src/Calculations/Chande/ChandeEtoK.cs (4)
src/Core/MovingAverageCore.cs (1)
  • SimpleMovingAverage (7-25)
src/Helpers/RollingWindow.cs (3)
  • RollingCorrelation (142-192)
  • RSquared (187-191)
  • RollingSum (20-79)
src/Helpers/MathHelper.cs (1)
  • IsValueNullOrInfinity (114-117)
src/Helpers/CalculationsHelper.cs (3)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
src/Calculations/Momentum/MomentumLtoR.cs (2)
src/Core/MovingAverageCore.cs (2)
  • ExponentialMovingAverage (53-81)
  • WeightedMovingAverage (27-51)
src/Helpers/CalculationsHelper.cs (4)
  • MinPastValues (1232-1235)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
src/Calculations/Chande/ChandeStoZ.cs (2)
src/Core/MovingAverageCore.cs (1)
  • ExponentialMovingAverage (53-81)
src/Helpers/CalculationsHelper.cs (3)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
src/Calculations/Momentum/MomentumStoZ.cs (2)
src/Core/MovingAverageCore.cs (2)
  • SimpleMovingAverage (7-25)
  • ExponentialMovingAverage (53-81)
src/Helpers/CalculationsHelper.cs (3)
  • SetCustomValues (154-169)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
src/Calculations/BollingerBands/BollingerBandsPart2.cs (2)
src/Core/MovingAverageCore.cs (2)
  • SimpleMovingAverage (7-25)
  • WeightedMovingAverage (27-51)
src/Helpers/CalculationsHelper.cs (3)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
benchmarks/OoplesFinance.StockIndicators.Benchmarks/Program.cs (1)
benchmarks/OoplesFinance.StockIndicators.Benchmarks/StreamingPerformanceRunner.cs (2)
  • StreamingPerformanceRunner (8-202)
  • TryRun (12-81)
src/Calculations/BollingerBands/BollingerBandsPart1.cs (4)
src/Core/MovingAverageCore.cs (3)
  • SimpleMovingAverage (7-25)
  • ExponentialMovingAverage (53-81)
  • WeightedMovingAverage (27-51)
src/Helpers/CalculationsHelper.cs (4)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
  • MinPastValues (1232-1235)
src/Helpers/MathHelper.cs (2)
  • Sqrt (35-38)
  • MathHelper (13-141)
src/Helpers/RollingWindow.cs (2)
  • RollingSum (20-79)
  • Average (58-67)
src/Calculations/Macd/MacdLtoR.cs (2)
src/Core/MovingAverageCore.cs (2)
  • ExponentialMovingAverage (53-81)
  • SimpleMovingAverage (7-25)
src/Helpers/CalculationsHelper.cs (13)
  • List (55-86)
  • List (128-141)
  • List (208-224)
  • List (226-229)
  • List (251-309)
  • List (311-321)
  • List (344-872)
  • List (880-944)
  • List (953-1013)
  • List (1022-1089)
  • List (1178-1195)
  • List (1204-1223)
  • Add (1106-1131)
src/Calculations/Demark/DemarkLtoR.cs (2)
src/Helpers/RollingWindow.cs (1)
  • RollingSum (20-79)
src/Helpers/CalculationsHelper.cs (3)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
src/Calculations/Chande/ChandeLtoR.cs (3)
src/Core/MovingAverageCore.cs (3)
  • SimpleMovingAverage (7-25)
  • WeightedMovingAverage (27-51)
  • ExponentialMovingAverage (53-81)
src/Helpers/CalculationsHelper.cs (4)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
  • MinPastValues (1232-1235)
src/Helpers/RollingWindow.cs (1)
  • RollingSum (20-79)
benchmarks/OoplesFinance.StockIndicators.Benchmarks/StreamingPerformanceRunner.cs (3)
src/Helpers/CalculationsHelper.cs (13)
  • List (55-86)
  • List (128-141)
  • List (208-224)
  • List (226-229)
  • List (251-309)
  • List (311-321)
  • List (344-872)
  • List (880-944)
  • List (953-1013)
  • List (1022-1089)
  • List (1178-1195)
  • List (1204-1223)
  • Add (1106-1131)
src/Streaming/BarAggregator.cs (1)
  • DateTime (224-246)
src/Streaming/BarTimeframe.cs (1)
  • TimeSpan (29-44)
src/Calculations/Momentum/MomentumAtoD.cs (3)
src/Core/MovingAverageCore.cs (2)
  • SimpleMovingAverage (7-25)
  • ExponentialMovingAverage (53-81)
src/Helpers/CalculationsHelper.cs (4)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
  • MinPastValues (1232-1235)
src/Helpers/RollingWindow.cs (2)
  • RollingSum (20-79)
  • Average (58-67)
benchmarks/OoplesFinance.StockIndicators.Benchmarks/IndicatorBenchmarks.cs (3)
benchmarks/OoplesFinance.StockIndicators.Benchmarks/BenchmarkDataFactory.cs (3)
  • Original (79-89)
  • BenchmarkData (22-54)
  • BenchmarkDataFactory (20-101)
benchmarks/OoplesFinance.StockIndicators.Benchmarks/BenchmarkProfile.cs (1)
  • BenchmarkProfile (5-27)
src/Core/MovingAverageCore.cs (3)
  • SimpleMovingAverage (7-25)
  • ExponentialMovingAverage (53-81)
  • WeightedMovingAverage (27-51)
src/Calculations/Ehlers/EhlersLtoR.cs (4)
src/Helpers/MathHelper.cs (6)
  • MathHelper (13-141)
  • Exp (87-105)
  • Pow (66-80)
  • ToDegrees (45-48)
  • Log (25-28)
  • Sqrt (35-38)
src/Helpers/CalculationsHelper.cs (4)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
  • MinPastValues (1232-1235)
src/Core/MovingAverageCore.cs (3)
  • ExponentialMovingAverage (53-81)
  • SimpleMovingAverage (7-25)
  • WeightedMovingAverage (27-51)
src/Helpers/RollingWindow.cs (1)
  • RollingSum (20-79)
src/Calculations/MovingAverages/EhlersMovingAverages.cs (4)
src/Helpers/MathHelper.cs (6)
  • ToDegrees (45-48)
  • Log (25-28)
  • Exp (87-105)
  • MathHelper (13-141)
  • Pow (66-80)
  • Sqrt (35-38)
src/Helpers/CalculationsHelper.cs (4)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
  • MinPastValues (1232-1235)
src/Helpers/RollingWindow.cs (3)
  • OrderStatisticTree (614-823)
  • Insert (621-624)
  • Average (58-67)
src/Core/MovingAverageCore.cs (2)
  • ExponentialMovingAverage (53-81)
  • WeightedMovingAverage (27-51)
src/Calculations/Ehlers/EhlersEtoK.cs (3)
src/Helpers/MathHelper.cs (4)
  • Sqrt (35-38)
  • Pow (66-80)
  • Exp (87-105)
  • MathHelper (13-141)
src/Helpers/CalculationsHelper.cs (4)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
  • MinPastValues (1232-1235)
src/Core/MovingAverageCore.cs (2)
  • WeightedMovingAverage (27-51)
  • SimpleMovingAverage (7-25)
🪛 LanguageTool
MODERNIZATION_PLAN.md

[grammar] ~6-~6: Use a hyphen to join words.
Context: ...API where possible while enabling a high performance core. - Add first class stre...

(QB_NEW_EN_HYPHEN)


[grammar] ~7-~7: Use a hyphen to join words.
Context: ...ing a high performance core. - Add first class streaming support (tick-by-tick up...

(QB_NEW_EN_HYPHEN)

🪛 markdownlint-cli2 (0.18.1)
OPTIMIZATIONS.md

9-9: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


257-257: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


320-320: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


334-334: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

benchmarks/README.md

6-6: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


11-11: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


16-16: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


20-20: Fenced code blocks should have a language specified

(MD040, fenced-code-language)


27-27: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

⏰ 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: Analyze (csharp)

@ooples ooples requested a review from Copilot January 13, 2026 14:48
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR refactors the codebase by reorganizing calculation files into modular subdirectories, focusing on performance optimization and improved code organization.

Changes:

  • Reorganized calculation files into subdirectories (MovingAverages, Momentum, Macd, Inputs, Demark)
  • Added benchmarking infrastructure for performance measurement
  • Updated documentation to clarify precision handling

Reviewed changes

Copilot reviewed 44 out of 150 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/Calculations/MovingAverages/MovingAveragesAtoD.cs New file containing moving average calculations (A-D), extracted from original monolithic files
src/Calculations/MovingAverages/EhlersMovingAverages.cs New file containing Ehlers-specific moving average calculations
src/Calculations/Momentum/MomentumStoZ.cs New file containing momentum indicators (S-Z), refactored with RollingSum optimization
src/Calculations/Momentum/MomentumLtoR.cs New file containing momentum indicators (L-R), refactored for performance
src/Calculations/Momentum/MomentumAtoD.cs New file containing momentum indicators (A-D), refactored with RollingSum optimization
src/Calculations/Momentum.cs Gutted file now serving as namespace stub, actual calculations moved to subdirectory
src/Calculations/Macd/MacdStoZ.cs New file containing MACD variants (S-Z)
src/Calculations/Macd/MacdLtoR.cs New file containing MACD variants (L-R), refactored to use GetDifferenceList helper
src/Calculations/Macd/MacdEtoK.cs New file containing MACD variants (E-K), refactored with RollingSum optimization
src/Calculations/Macd/MacdAtoD.cs New file containing MACD variants (A-D)
src/Calculations/Macd.cs Gutted file now serving as namespace stub, actual calculations moved to subdirectory
src/Calculations/Inputs/InputsStoZ.cs New file containing input calculations (S-Z), refactored to use GetDerivedSeriesList
src/Calculations/Inputs/InputsLtoR.cs New file containing input calculations (L-R), refactored to use GetDerivedSeriesList
src/Calculations/Inputs/InputsEtoK.cs New file containing input calculations (E-K), refactored to use GetDerivedSeriesList
src/Calculations/Inputs/InputsAtoD.cs New file containing input calculations (A-D), refactored to use GetDerivedSeriesList
src/Calculations/Inputs.cs Gutted file now serving as namespace stub, actual calculations moved to subdirectory
src/Calculations/Demark/DemarkStoZ.cs New file containing Demark indicators (S-Z)
src/Calculations/Demark/DemarkLtoR.cs New file containing Demark indicators (L-R), refactored with RollingSum optimization
src/Calculations/Demark/DemarkEtoK.cs New file containing Demark indicators (E-K)
benchmarks/setup-baseline.ps1 New PowerShell script for setting up benchmark baseline using git worktree

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown

@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

🤖 Fix all issues with AI agents
In `@src/Calculations/Ehlers/EhlersAtoD.cs`:
- Around line 1820-1881: The angle/division precedence is wrong in
CalculateEhlersAdaptiveBandPassFilter: change the beta and gamma calculations to
divide 2π (and 2π*bw) by (0.9 * domCyc) instead of multiplying domCyc after
dividing by 0.9; replace beta = Math.Cos(2 * Math.PI / 0.9 * domCyc) with beta =
Math.Cos((2 * Math.PI) / (0.9 * domCyc)) and replace gamma = 1 / Math.Cos(2 *
Math.PI * bw / 0.9 * domCyc) with gamma = 1 / Math.Cos((2 * Math.PI * bw) / (0.9
* domCyc)).

In `@src/Calculations/Ehlers/EhlersLtoR.cs`:
- Around line 852-950: The fixed-size xxArray/yyArray inside
CalculateEhlersMesaPredictIndicatorV2 can overflow when length1/length4 are
large and are also being reallocated per-bar; allocate them once outside the
main for-loop with a capacity computed from the parameters (e.g. size >=
2*length1 + length4 + a small margin) so all accesses xxArray[length1+length4],
xxArray[length1+j-(k-1)] and yyArray[length1+j+1] are safe, reuse these buffers
each iteration, and explicitly clear or zero only the used range at the start of
each bar iteration to preserve logic for xxArray/yyArray population and avoid
repeated allocations.

In `@src/Calculations/Ehlers/EhlersStoZ.cs`:
- Around line 442-485: The beta and alpha clamps in
CalculateEhlersTrendExtraction are inverted—using Math.Max(..., 0.99) forces
values toward 0.99 and breaks the filter; change the clamps to Math.Min(...) so
beta = Math.Min(Math.Cos(2 * Math.PI / length), 0.99) and alpha = Math.Min(gamma
- Sqrt((gamma * gamma) - 1), 0.99) (keeping the existing gamma calculation), so
the values are capped at 0.99 instead of floored.
♻️ Duplicate comments (1)
src/Calculations/Ehlers/EhlersStoZ.cs (1)

1091-1132: Window indicators: IndicatorName is swapped + SimpleWindow outputs the wrong series + length/2 int-division.

  • Line 1129: TriangleWindow sets IndicatorName.EhlersSimpleWindowIndicator (should be Triangle).
  • Line 1181: SimpleWindow sets IndicatorName.EhlersTriangleWindowIndicator (should be Simple).
  • Line 1176/1180: SimpleWindow emits filtList but signals were computed off filtMa2List.
  • Line 1116 and Line 1168: length / 2 is integer division.
Proposed fix
@@
-            var roc = length / 2 * Math.PI * (filt - prevFilt1);
+            var roc = (length / 2.0) * Math.PI * (filt - prevFilt1);
@@
-        stockData.IndicatorName = IndicatorName.EhlersSimpleWindowIndicator;  
+        stockData.IndicatorName = IndicatorName.EhlersTriangleWindowIndicator;
@@
-            var roc = length / 2 * Math.PI * (filt - prevFilt1);
+            var roc = (length / 2.0) * Math.PI * (filt - prevFilt1);
@@
-        stockData.SetOutputValues(() => new Dictionary<string, List<double>>{
-            { "Etwi", filtList },
+        stockData.SetOutputValues(() => new Dictionary<string, List<double>>{
+            { "Etwi", filtMa2List },
             { "Roc", rocList }
         });
@@
-        stockData.SetCustomValues(filtList);
-        stockData.IndicatorName = IndicatorName.EhlersTriangleWindowIndicator;
+        stockData.SetCustomValues(filtMa2List);
+        stockData.IndicatorName = IndicatorName.EhlersSimpleWindowIndicator;

Also applies to: 1142-1184

🧹 Nitpick comments (8)
OoplesFinance.StockIndicators.sln (1)

82-85: Consider nesting core project under src folder for consistency.

The NestedProjects section correctly nests OoplesFinance.StockIndicators.Benchmarks under benchmarks and OoplesFinance.StockIndicators.DevConsole under examples. However, the core project (OoplesFinance.StockIndicators) is not nested under the src folder that was created on line 18-19.

For organizational consistency, consider adding:

{C2798B13-AEF3-419D-A4A3-9B81CF46BF60} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}

Similarly, the tests project could be organized under a tests solution folder.

MODERNIZATION_PLAN.md (1)

5-11: Minor grammatical improvements suggested.

The goals section is clear and well-defined. Per static analysis hints, consider hyphenating compound adjectives:

📝 Suggested fixes
-- Preserve the public API where possible while enabling a high performance core.
-- Add first class streaming support (tick-by-tick updates).
+- Preserve the public API where possible while enabling a high-performance core.
+- Add first-class streaming support (tick-by-tick updates).
examples/OoplesFinance.StockIndicators.DevConsole/Program.cs (2)

323-353: Consider stopping the session explicitly.

The session is created with using which will dispose it, but session.Stop() is never called before disposal. While this may work correctly if Dispose internally calls Stop, explicitly stopping the session before the using block ends would make the intent clearer and ensure clean shutdown.

♻️ Suggested improvement
         session.Start();

+        session.Stop();
+
         Console.WriteLine($"Updates received = {updates.Count}");

523-578: Dispose should be idempotent and prevent restart after disposal.

Currently, Dispose() only calls Stop(), but if Start() is called again after Dispose(), the subscription could potentially be restarted (since _started isn't reset). Consider adding a disposed flag to prevent reuse after disposal.

♻️ Suggested improvement
             private bool _stopped;
             private bool _started;
+            private bool _disposed;

             // ...

             public void Dispose()
             {
+                if (_disposed)
+                {
+                    return;
+                }
+
+                _disposed = true;
                 Stop();
             }
src/Calculations/MovingAverages/EhlersMovingAverages.cs (1)

704-715: Unused variable prevFilt.

At Line 709, prevFilt is assigned via GetLastOrDefault(filtList) but is never used—the signal comparison on Line 713 uses prevFilter1 instead.

♻️ Remove unused variable
-            var prevFilt = GetLastOrDefault(filtList);
             var filt = (coeff1 * ((currentValue + prevValue) / 2)) + (coeff2 * prevFilter1) + (coeff3 * prevFilter2);
             filtList.Add(filt);

-            var signal = GetCompareSignal(currentValue - filt, prevValue - prevFilt);
+            var signal = GetCompareSignal(currentValue - filt, prevValue - prevFilter1);
benchmarks/update-performance-chart.ps1 (1)

34-46: Potential division by zero if optimizedMean is 0.

At Line 86, $speedup = $baselineMean / $optimizedMean will throw if $optimizedMean is 0. While unlikely for valid benchmark results, adding a guard would make the script more robust.

♻️ Add zero check
+        if ($optimizedMean -eq 0) {
+            throw "Optimized mean is zero for $($indicator.Category) length $length count $Count."
+        }
+
         $speedup = $baselineMean / $optimizedMean
src/Calculations/Ehlers/EhlersLtoR.cs (2)

76-123: Prefer Math.Atan2(imagPart, realPart) over Atan(imag/real) + quadrant patches.
This removes the divide-by-zero thresholding and makes the quadrant handling correct/maintainable.

Proposed change
-            var phase = Math.Abs(realPart) > 0.001 ? Math.Atan(imagPart / realPart).ToDegrees() : 90 * Math.Sign(imagPart);
-            phase = realPart < 0 ? phase + 180 : phase;
+            var phase = Math.Atan2(imagPart, realPart).ToDegrees();
             phase += 90;
             phase = phase < 0 ? phase + 360 : phase;
             phase = phase > 360 ? phase - 360 : phase;

1019-1062: Noise Elimination: safe sizing now, but still allocates xArray per bar.
If this is performance-sensitive, consider reusing a single buffer (or avoid the buffer and read directly from inputList in the nested loops).

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e43b5b8 and 925d302.

📒 Files selected for processing (49)
  • MODERNIZATION_PLAN.md
  • OoplesFinance.StockIndicators.sln
  • README.md
  • STATEFUL_INDICATOR_CHECKLIST.md
  • benchmarks/OoplesFinance.StockIndicators.Benchmarks/IndicatorBenchmarks.cs
  • benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj
  • benchmarks/README.md
  • benchmarks/update-performance-chart.ps1
  • examples/OoplesFinance.StockIndicators.DevConsole/OoplesFinance.StockIndicators.DevConsole.csproj
  • examples/OoplesFinance.StockIndicators.DevConsole/Program.cs
  • src/Calculations/Ehlers/EhlersAtoD.cs
  • src/Calculations/Ehlers/EhlersLtoR.cs
  • src/Calculations/Ehlers/EhlersStoZ.cs
  • src/Calculations/MovingAverages/EhlersMovingAverages.cs
  • src/Calculations/MovingAverages/MovingAveragesAtoD.cs
  • src/Calculations/Volume/VolumeEtoK.cs
  • src/Helpers/RollingWindow.cs
  • src/OoplesFinance.StockIndicators.csproj
  • src/Streaming/StatefulIndicators.Batch10.cs
  • src/Streaming/StatefulIndicators.Batch11.cs
  • src/Streaming/StatefulIndicators.Batch12.cs
  • src/Streaming/StatefulIndicators.Batch13.cs
  • src/Streaming/StatefulIndicators.Batch14.cs
  • src/Streaming/StatefulIndicators.Batch15.cs
  • src/Streaming/StatefulIndicators.Batch16.cs
  • src/Streaming/StatefulIndicators.Batch17.cs
  • src/Streaming/StatefulIndicators.Batch18.cs
  • src/Streaming/StatefulIndicators.Batch19.cs
  • src/Streaming/StatefulIndicators.Batch20.cs
  • src/Streaming/StatefulIndicators.Batch21.cs
  • src/Streaming/StatefulIndicators.Batch22.cs
  • src/Streaming/StatefulIndicators.Batch23.cs
  • src/Streaming/StatefulIndicators.Batch24.cs
  • src/Streaming/StatefulIndicators.Batch25.cs
  • src/Streaming/StatefulIndicators.Batch26.cs
  • src/Streaming/StatefulIndicators.Batch27.cs
  • src/Streaming/StatefulIndicators.Batch28.cs
  • src/Streaming/StatefulIndicators.Batch5.cs
  • src/Streaming/StatefulIndicators.Batch6.cs
  • src/Streaming/StatefulIndicators.Batch7.cs
  • src/Streaming/StatefulIndicators.Batch8.cs
  • src/Streaming/StatefulIndicators.Batch9.cs
  • src/Streaming/StatefulIndicators.cs
  • src/Streaming/StreamingIndicatorEngine.cs
  • src/Streaming/StreamingMultiSeries.cs
  • src/Streaming/StreamingSession.cs
  • tests/OoplesFinance.StockIndicators.Tests.Unit.csproj
  • tests/StreamingTests/StreamingIndicatorEngineTests.cs
  • tests/StreamingTests/StreamingStatefulParityTests.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj
🧰 Additional context used
🧬 Code graph analysis (2)
examples/OoplesFinance.StockIndicators.DevConsole/Program.cs (5)
src/Streaming/StreamingIndicatorEngine.cs (10)
  • Equals (481-485)
  • Equals (487-490)
  • StreamingIndicatorEngine (5-719)
  • StreamingIndicatorEngine (14-17)
  • OnBar (308-317)
  • IReadOnlyList (80-96)
  • IReadOnlyList (179-200)
  • IReadOnlyList (202-225)
  • IReadOnlyList (227-236)
  • IReadOnlyList (238-274)
src/Streaming/StreamingMultiSeries.cs (3)
  • Equals (27-31)
  • Equals (33-36)
  • SeriesKey (13-22)
src/Streaming/StreamingSession.cs (13)
  • StreamingIndicatorEngine (50-58)
  • StreamingIndicatorEngine (79-87)
  • StreamingIndicatorEngine (115-124)
  • StreamingSession (5-391)
  • StreamingSession (16-28)
  • StreamingSession (32-48)
  • Start (163-166)
  • OnBar (259-267)
  • IReadOnlyList (60-77)
  • IReadOnlyList (89-113)
  • IReadOnlyList (126-146)
  • IReadOnlyList (148-161)
  • Stop (168-171)
src/Streaming/BarTimeframe.cs (1)
  • TimeSpan (29-44)
src/Streaming/StreamingOptions.cs (2)
  • StreamingOptions (39-122)
  • StreamingProcessingMode (108-111)
src/Calculations/Ehlers/EhlersAtoD.cs (4)
src/Helpers/MathHelper.cs (5)
  • Pow (66-80)
  • Sqrt (35-38)
  • ToDegrees (45-48)
  • Exp (87-105)
  • MathHelper (13-141)
src/Helpers/CalculationsHelper.cs (5)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
  • MinPastValues (1232-1235)
  • CalculateEMA (1141-1147)
src/Core/MovingAverageCore.cs (2)
  • WeightedMovingAverage (27-51)
  • ExponentialMovingAverage (53-81)
src/Helpers/RollingWindow.cs (2)
  • RollingSum (20-79)
  • Average (58-67)
🪛 LanguageTool
MODERNIZATION_PLAN.md

[grammar] ~6-~6: Use a hyphen to join words.
Context: ...API where possible while enabling a high performance core. - Add first class stre...

(QB_NEW_EN_HYPHEN)


[grammar] ~7-~7: Use a hyphen to join words.
Context: ...ing a high performance core. - Add first class streaming support (tick-by-tick up...

(QB_NEW_EN_HYPHEN)

⏰ 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: Analyze (csharp)
🔇 Additional comments (35)
benchmarks/README.md (1)

1-38: Well-structured benchmark documentation.

The README provides clear instructions for running benchmarks, filtering subsets, streaming performance tests, and baseline comparisons. The environment variable overrides for OOPLES_BENCHMARK_COUNTS and OOPLES_BENCHMARK_LENGTHS are a nice touch for flexibility.

One minor note: Line 29 references net10.0 which aligns with the modernization plan's target TFM. Ensure this is kept in sync if the target framework changes.

STATEFUL_INDICATOR_CHECKLIST.md (1)

1-8: Comprehensive indicator tracking document.

This auto-generated checklist effectively documents the stateful implementation status of all 774 indicators. The structured format with Implemented/Pending/N/A sections provides clear visibility into completion status.

Consider adding a note about how this file is regenerated (script/command) so future maintainers can update it as indicators are added.

examples/OoplesFinance.StockIndicators.DevConsole/OoplesFinance.StockIndicators.DevConsole.csproj (1)

1-13: Clean project configuration.

The DevConsole project is properly configured as a non-packable executable with modern settings (implicit usings, nullable enabled). The project reference path to the core library is correct.

MODERNIZATION_PLAN.md (1)

81-302: Comprehensive and well-structured user stories.

The 25 user stories with explicit acceptance criteria and performance requirements provide excellent guidance for implementation. Notable strengths:

  • Measurable performance targets (e.g., "ratio <= 0.10 vs baseline", ">= 500k updates/sec")
  • Clear allocation reduction goals
  • Streaming/batch parity requirements
  • Multi-target framework considerations

This level of detail will significantly help track progress and validate the modernization effort.

examples/OoplesFinance.StockIndicators.DevConsole/Program.cs (6)

1-14: LGTM!

Clean setup with appropriate imports and constants. Using CultureInfo.InvariantCulture ensures consistent number formatting across locales.


16-69: LGTM!

The interactive menu loop is well-structured with clear options and proper input handling. The ToLowerInvariant() normalization ensures case-insensitive matching.


85-161: LGTM!

The CLI argument parsing handles common patterns well (--run-all, --example <name>, --example=value, --no-pause, --help). The logic correctly prioritizes help display and handles missing examples gracefully.


214-246: LGTM!

The streaming single-series example demonstrates the engine registration pattern clearly with appropriate callback handling.


458-495: LGTM!

The SpreadState implementation is a clean example of a multi-series indicator. It correctly uses TryGetLatest to handle missing data and returns appropriate results.


635-664: LGTM!

The ReplayStreamEvent struct with factory methods is well-designed. Using a struct is appropriate here since events are short-lived value types.

benchmarks/OoplesFinance.StockIndicators.Benchmarks/IndicatorBenchmarks.cs (5)

1-16: LGTM!

The extern alias setup for baseline comparisons is well-structured, allowing side-by-side comparison of optimized vs. original implementations.


19-50: LGTM!

The parameterized benchmark setup with ParamsSource for Counts and Lengths provides good flexibility. The environment variable fallback chain (OOPLES_BENCHMARK_COUNTSBENCHMARK_COUNTS → defaults) is a nice touch for CI/CD integration.


52-85: LGTM!

The LoadIntOverrides implementation is robust—handles multiple delimiters, validates positive values, deduplicates, and sorts the results.


87-104: LGTM!

Proper setup/teardown pattern with GlobalSetup for data creation and IterationSetup for state reset between iterations.


448-461: No changes needed—configuration is correct for benchmarks using IterationSetup.

The WithInvocationCount(1) and WithUnrollFactor(1) settings are not optional here. BenchmarkDotNet forces these values when IterationSetup is used (as it is in this class on line 98), since per-iteration setup/cleanup is incompatible with multiple invocations. These explicit settings correctly document this constraint and cannot be removed to enable auto-tuning. The configuration is appropriate as written.

Likely an incorrect or invalid review comment.

src/Calculations/MovingAverages/EhlersMovingAverages.cs (4)

153-197: LGTM - Previous bug has been fixed.

The lagIndex calculation now correctly uses Math.Max(i - halfP, 0) at Line 173, which properly clamps to prevent negative indices while accessing lagged values.


1492-1544: RollingSum instances created without window size.

Similar to the previous comment, Lines 1499-1500 create RollingSum instances without specifying a window size, but they're used with .Average(fastLength) and .Average(slowLength). This pattern appears throughout and warrants verification.


1377-1435: Remove the concern about RollingSum window size.

RollingSum uses a cumulative sum pattern where the internal _cumulative list stores running totals, allowing O(1) calculation of any window sum. The list grows proportionally to the number of Add() calls (bounded by stockData.Count), not unboundedly. The Average(int length) method correctly respects the length parameter and handles partial windows via Math.Min(length, _cumulative.Count). The window is controlled by the length2 parameter (50), not by constructor configuration.


206-300: Add unit tests for CalculateEhlersMedianAverageAdaptiveFilter covering edge cases.

The adaptive filter logic modifies the windowTree in-place during the while loop, then restores removed values afterward. This remove/restore pattern is complex and currently lacks dedicated test coverage. The only existing test is a streaming parity check that validates output count, not correctness. Add tests specifically for edge cases where len reduces significantly or when available < length.

benchmarks/update-performance-chart.ps1 (2)

1-18: LGTM!

Good parameter defaults and error handling. The auto-discovery of the latest benchmark CSV is convenient.


93-107: LGTM!

The marker-based replacement pattern is clean and the regex escaping is handled correctly.

README.md (5)

1-14: LGTM!

Clear and concise highlights section that effectively communicates the fork's value proposition: precision, streaming, multi-series, and performance focus.


15-48: LGTM!

The quick start examples are practical and demonstrate both batch and streaming single-series usage patterns clearly.


50-108: LGTM!

The multi-series streaming example with SpreadState provides a complete, copy-pasteable example. The notes about alignment policies are helpful.


121-177: LGTM!

The performance tables are well-formatted and the markers (PERF_TABLES_START/END) enable automated updates via the PowerShell script.


180-214: LGTM!

The benchmark and project layout documentation provides clear instructions for running benchmarks and understanding the repository structure.

src/Calculations/Ehlers/EhlersLtoR.cs (2)

459-517: Nice fixes vs prior review: precedence and buffer sizing issues appear addressed.

  • Reflex uses period = 0.5 * length then divides by period (avoids / 0.5 * length pitfall).
  • Mesa Predict V1 uses parameter-derived array sizes (avoids fixed 500/520 overruns).
  • Noise Elimination uses new double[length + 1] (avoids fixed 50 overflow).

Also applies to: 686-840, 1020-1062


13-66: No action required. All methods have exactly one definition in the repository with no duplicates.

  • CalculateEhlersRoofingFilterV2: 1 definition in EhlersLtoR.cs:13
  • CalculateEhlersRoofingFilterV1: 1 definition in EhlersLtoR.cs:621
  • CalculateEhlersCenterofGravityOscillator: 1 definition in EhlersAtoD.cs:151

Likely an incorrect or invalid review comment.

src/Calculations/Ehlers/EhlersAtoD.cs (4)

550-609: Anticipate: start update logic looks fixed (no longer overwritten by non-max corr).
This matches the “only update on new max” behavior.


1467-1542: DFT: per-period cos/sin reset + max-power selection looks correct vs prior buggy accumulator behavior.


1770-1808: Cycle Amplitude: loop now executes (power no longer stuck at 0).


1-2342: Verification confirms no duplicate method definitions exist. However, this review comment references a "precedence note above" that is not included in the provided text, making the review incomplete and unclear. The comment also uses non-standard tags ( ) that do not align with the project's code review classification system. The code does appear to follow the OutputValues/Signals/CustomValues pattern consistently across the methods shown, but this cannot be fully assessed without the missing context. Please provide the complete review with the referenced precedence note.

src/Calculations/Ehlers/EhlersStoZ.cs (3)

487-557: Snake Universal Trading Filter: precedence fix looks applied.
Using 2 * Math.PI / (2 * length1) avoids the left-to-right / 2 * length1 bug.


825-887: Good: SpearmanRank + TruncatedBandPassFilter now size arrays from length (no fixed-buffer overruns).

Also applies to: 897-935


1-1843: General: consistent indicator plumbing (OutputValues/Signals/CustomValues) throughout.
Main correctness fixes are the TrendExtraction clamp + window-indicator name/output issues above.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Copy Markdown

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

🤖 Fix all issues with AI agents
In `@src/Calculations/Ehlers/EhlersAtoD.cs`:
- Around line 892-955: The issue is that highest/lowest are initialized to 0
which biases ranges when roofingFilter values are all positive or all negative;
change initialization to highest = double.MinValue and lowest = double.MaxValue
in CalculateEhlersAdaptiveStochasticIndicatorV2, update the inner loop that
computes highest/lowest using roofingFilterList[i - j], and after the loop if no
samples were processed (e.g., domCyc <= 0 or highest still equals
double.MinValue) set highest and lowest to the current roofingFilter value
(roofingFilter) so the stoc calculation uses a valid range instead of zeros.

In `@src/Calculations/Ehlers/EhlersLtoR.cs`:
- Around line 671-709: The method CalculateEhlersMesaPredictIndicatorV1 can
access ssfList[i] (via priorSsf) before any element is appended when upperLength
== 1, causing IndexOutOfRangeException and invalid xxArray indexing assumptions;
fix by adding bounds/guard logic: ensure priorSsf reads use i-1 only when i-1 >=
0 (fallback to 0 or current ssf value when absent), and clamp or validate
upperLength and derived sizes (xxSize/xxArray indices) so loops that access
xxArray[k - upperLength] or similar never reference negative indices; update the
code paths around ssfList, priorSsf, and any loops that use xxArray to handle
small upperLength (1) and ensure arrays are populated before read.
- Around line 1083-1189: The public API methods
CalculateEhlersReverseExponentialMovingAverageIndicatorV1 (alpha) and
CalculateEhlersReverseExponentialMovingAverageIndicatorV2 (trendAlpha,
cycleAlpha) do not validate inputs and can accept values outside (0,1) which
breaks the recursive calculations; add input validation at the start of each
method to clamp or reject values outside the open interval (0,1) (e.g., throw
ArgumentOutOfRangeException or clamp to a safe epsilon like 1e-9/1-1e-9) and
document the behavior so the rest of the method can assume 0 < alpha < 1 for
variables alpha, trendAlpha, and cycleAlpha.
- Around line 1329-1462: Clamp/validate parameters: in
CalculateEhlersLaguerreRelativeStrengthIndex clamp gamma into [0,1] (e.g., gamma
= MinOrMax(gamma, 1, 0)) before using it; in
CalculateEhlersLaguerreRelativeStrengthIndexWithSelfAdjustingAlpha avoid
Math.Log(length)==0 by ensuring the log denominator is never zero (e.g., use var
logLen = Math.Log(Math.Max(length, 2)) and compute alpha using logLen, or force
length = Math.Max(length, 2) earlier) so the alpha calculation
(Math.Log(ratioSum) / Math.Log(length)) cannot produce NaN; apply these changes
where gamma, length, and alpha are computed (references:
CalculateEhlersLaguerreRelativeStrengthIndex, gamma variable;
CalculateEhlersLaguerreRelativeStrengthIndexWithSelfAdjustingAlpha, length
parameter and alpha calculation).
- Around line 291-292: The code crashes because callers chain
.CustomValuesList/.OutputValues (e.g., using
CalculateEhlersRoofingFilterV1().CustomValuesList) while IndicatorOptions may
have cleared those collections; fix by stopping direct chained access and adding
guards: first store the indicator result (e.g., var roofing =
CalculateEhlersRoofingFilterV1(...)), then check roofing.CustomValuesList !=
null && roofing.CustomValuesList.Count > 0 (and similarly for .OutputValues)
before passing to GetMaxAndMinValuesList or indexing; alternatively ensure the
indicator creators (SetCustomValues/SetOutputValues) always populate empty lists
instead of null when calculations expect them—apply this pattern to all affected
functions/symbols (CalculateEhlersRoofingFilterV1, GetMaxAndMinValuesList and
every place that indexes CustomValuesList/OutputValues) so accesses are safe
regardless of IndicatorOptions.IncludeCustomValues/IncludeOutputValues.

In `@src/Calculations/Ehlers/EhlersStoZ.cs`:
- Around line 284-368: Summary: Division by Math.Log(length2) in
CalculateEhlersSpectrumDerivedFilterBank can produce division-by-zero/NaN when
length2 == 1. Fix: ensure the log denominator is never zero by clamping length2
to at least 2 or by using a safe denominator variable; e.g. change the earlier
clamp from length2 = Math.Max(length2, 1) to length2 = Math.Max(length2, 2) (or
compute var denomLog = Math.Log(length2); if (denomLog == 0) denomLog = 1; and
replace / Math.Log(length2) with / denomLog) so the db calculation cannot divide
by zero; update references in the method
(CalculateEhlersSpectrumDerivedFilterBank, local variable length2 and the db
calculation) and add a unit test for length2 == 1 to verify no NaN is produced.
♻️ Duplicate comments (1)
src/Calculations/Ehlers/EhlersStoZ.cs (1)

1091-1182: Triangle vs Simple Window indicators: IndicatorName assignments are swapped

CalculateEhlersTriangleWindowIndicator sets EhlersSimpleWindowIndicator (Line 1129) and CalculateEhlersSimpleWindowIndicator sets EhlersTriangleWindowIndicator (Line 1181).

Proposed diff
-        stockData.IndicatorName = IndicatorName.EhlersSimpleWindowIndicator;  
+        stockData.IndicatorName = IndicatorName.EhlersTriangleWindowIndicator;
-        stockData.IndicatorName = IndicatorName.EhlersTriangleWindowIndicator;
+        stockData.IndicatorName = IndicatorName.EhlersSimpleWindowIndicator;
🧹 Nitpick comments (3)
src/Calculations/Ehlers/EhlersLtoR.cs (2)

76-123: Phase calculation: prefer Math.Atan2 over Math.Atan(imag/real)

The current approach has avoidable quadrant/near-zero edge cases (Line 94–99).

Proposed diff
-            var phase = Math.Abs(realPart) > 0.001 ? Math.Atan(imagPart / realPart).ToDegrees() : 90 * Math.Sign(imagPart);
-            phase = realPart < 0 ? phase + 180 : phase;
+            var phase = Math.Atan2(imagPart, realPart).ToDegrees();
             phase += 90;
             phase = phase < 0 ? phase + 360 : phase;
-            phase = phase > 360 ? phase - 360 : phase;
+            phase = phase >= 360 ? phase - 360 : phase;

1031-1064: Noise Elimination: avoid per-bar new double[length+1] allocations

xArray is allocated every bar (Line 1042), which is pure GC churn; you overwrite all used slots anyway.

Proposed diff
         var denom = 0.5 * length * (length - 1);
+        var xArray = new double[length + 1];

         for (var i = 0; i < stockData.Count; i++)
         {
-            var xArray = new double[length + 1];
             for (var j = 1; j <= length; j++)
             {
                 var prevPrice = i >= j - 1 ? inputList[i - (j - 1)] : 0;
                 xArray[j] = prevPrice;
             }
src/Calculations/Ehlers/EhlersStoZ.cs (1)

1116-1117: ROC scaling uses integer division (length / 2)

If length is odd, length / 2 truncates (Line 1116 / 1168). If you intended half-length as a double, use 0.5 * length.

Proposed diff
-            var roc = length / 2 * Math.PI * (filt - prevFilt1);
+            var roc = (0.5 * length) * Math.PI * (filt - prevFilt1);

Also applies to: 1168-1169

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 925d302 and 5acc49f.

📒 Files selected for processing (3)
  • src/Calculations/Ehlers/EhlersAtoD.cs
  • src/Calculations/Ehlers/EhlersLtoR.cs
  • src/Calculations/Ehlers/EhlersStoZ.cs
🧰 Additional context used
🧬 Code graph analysis (1)
src/Calculations/Ehlers/EhlersLtoR.cs (4)
src/Helpers/MathHelper.cs (6)
  • MathHelper (13-141)
  • Exp (87-105)
  • Pow (66-80)
  • ToDegrees (45-48)
  • Log (25-28)
  • Sqrt (35-38)
src/Helpers/CalculationsHelper.cs (4)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
  • MinPastValues (1232-1235)
src/Core/MovingAverageCore.cs (3)
  • ExponentialMovingAverage (53-81)
  • SimpleMovingAverage (7-25)
  • WeightedMovingAverage (27-51)
src/Helpers/RollingWindow.cs (1)
  • RollingSum (20-79)
⏰ 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: Analyze (csharp)
🔇 Additional comments (2)
src/Calculations/Ehlers/EhlersAtoD.cs (1)

200-201: Same chained-output risk as in other Ehlers files (OutputValues/CustomValues gating)

E.g., Line 200–201 (OutputValues["Period"]) and Line 520–523 (OutputValues["Real"/"Imag"]) assume those dictionaries are populated. If outputs/custom values can be disabled, these chains can break.

Also applies to: 520-523, 635-636, 785-786, 1266-1268

src/Calculations/Ehlers/EhlersStoZ.cs (1)

23-23: Chained indicator usage depends on intermediate CustomValuesList being available

Several methods immediately consume .CustomValuesList from another calculation (e.g., Line 23, 138–139, 248, 401). If custom values can be disabled, results can be wrong or crash depending on helper behavior.

Also applies to: 138-139, 248-248, 401-401

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Copy Markdown

@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

🤖 Fix all issues with AI agents
In `@src/Calculations/Ehlers/EhlersAtoD.cs`:
- Around line 200-206: The code in EhlersAtoD.cs directly accesses nested
indicator collections (e.g.
CalculateEhlersAdaptiveCyberCycle().OutputValues["Period"], direct reads of
CustomValuesList and OutputValues["Real"/"Imag"] and indices like pList[i])
which will crash when IncludeOutputValues/IncludeCustomValues are false; replace
those direct accesses with the internal helper used in EhlersLtoR.cs that
temporarily enables Output/Custom collections for nested calculations, then
retrieve values via the safe helper (use the same wrapper used around nested
calls in EhlersLtoR) for each occurrence (references:
CalculateEhlersAdaptiveCyberCycle, pList, cgList, any usage of CustomValuesList,
and OutputValues["Real"/"Imag"]) so the nested indicator calls always run with
collections available and avoid KeyNotFoundException/IndexOutOfRangeException.

In `@src/Calculations/Ehlers/EhlersLtoR.cs`:
- Around line 1532-1617: GetCustomValuesListInternal and GetOutputValuesInternal
temporarily flip options.IncludeCustomValues/IncludeOutputValues/IncludeSignals
and then call calculator(stockData); if that calculator throws the flags and
stockData properties remain mutated—wrap the call and any use of result in a try
block and move the restoration of options.IncludeCustomValues,
options.IncludeOutputValues, options.IncludeSignals and the conditional resets
of stockData.CustomValuesList, stockData.OutputValues, and stockData.SignalsList
into a finally block so they always run; ensure you still return the copied
List<double> or Dictionary<string,List<double>> created from result inside the
try (or assign it to a local to return after the try/finally).

In `@src/Calculations/Ehlers/EhlersStoZ.cs`:
- Around line 23-24: The code directly accesses nested indicator collections
like CalculateEhlersHighPassFilterV1(...).CustomValuesList and other
.OutputValues[...] which will be empty when
IncludeCustomValues/IncludeOutputValues=false; instead add the internal helper
methods used in EhlersLtoR.cs (the two helper methods referenced in that file)
to this class and replace every direct access to .CustomValuesList and
.OutputValues[...] (e.g., uses in CalculateEhlersHighPassFilterV1, any
hpList/otherList reads at the cited locations) with the helper calls that safely
obtain values when collections are cleared, ensuring you call the helper with
the nested indicator instance and index to retrieve values robustly.
♻️ Duplicate comments (4)
src/Calculations/Ehlers/EhlersLtoR.cs (2)

676-706: Mesa Predict V1: clamp length2 to prevent negative xxArray[...] indexing.

In Line 824–831, xxArray[upperLength + j - k] can go negative when length2 > upperLength + 1, causing IndexOutOfRangeException.

Proposed fix
         length1 = Math.Max(length1, 1);
-        length2 = Math.Max(length2, 1);
+        length2 = Math.Max(length2, 1);
         length3 = Math.Max(length3, 1);
         lowerLength = Math.Max(lowerLength, 1);
         upperLength = Math.Max(upperLength, 2);
         length3 = Math.Max(length3, length1);
+        length2 = Math.Min(length2, upperLength + 1); // avoids negative (upperLength + j - k)

         List<double> ssfList = new(stockData.Count);

Also applies to: 824-831


1409-1443: Fix NaN risk in self-adjusting alpha when length == 1 (Log(1)=0).

Line 1442 can hit 0/0 when length == 1 and ratioSum == 1, producing NaN that MinOrMax(...) won’t clamp.

Proposed fix
-        length = Math.Max(length, 1);
+        length = Math.Max(length, 2); // avoid Log(1)=0

+        var logLen = Math.Log(length);
         List<double> laguerreRsiList = new(stockData.Count);
...
-            var alpha = ratioSum > 0 ? MinOrMax(Math.Log(ratioSum) / Math.Log(length), 0.99, 0.01) : 0.01;
+            var alpha = ratioSum > 0 ? MinOrMax(Math.Log(ratioSum) / logLen, 0.99, 0.01) : 0.01;
src/Calculations/Ehlers/EhlersStoZ.cs (2)

284-346: SpectrumDerivedFilterBank: Math.Log(length2) can be 0 when length2 == 1 (NaN risk).

Line 341 divides by Math.Log(length2); with length2 == 1, this becomes divide-by-zero and can produce NaN (notably when the numerator also becomes 0).

Proposed fix
-        length2 = Math.Max(length2, 1);
+        length2 = Math.Max(length2, 2); // avoid Log(1)=0

1093-1134: Triangle/Simple Window indicators: IndicatorName assignments appear swapped.

  • Line 1131: CalculateEhlersTriangleWindowIndicator sets EhlersSimpleWindowIndicator.
  • Line 1183–1184: CalculateEhlersSimpleWindowIndicator sets EhlersTriangleWindowIndicator.
Proposed fix
     public static StockData CalculateEhlersTriangleWindowIndicator(...)

 ...
-        stockData.IndicatorName = IndicatorName.EhlersSimpleWindowIndicator;  
+        stockData.IndicatorName = IndicatorName.EhlersTriangleWindowIndicator;

     public static StockData CalculateEhlersSimpleWindowIndicator(...)

 ...
-        stockData.IndicatorName = IndicatorName.EhlersTriangleWindowIndicator;
+        stockData.IndicatorName = IndicatorName.EhlersSimpleWindowIndicator;

Also applies to: 1144-1186

🧹 Nitpick comments (2)
src/Calculations/Ehlers/EhlersLtoR.cs (1)

1047-1054: Avoid per-bar new double[length + 1] in Noise Elimination Technology hot loop.

Line 1049 allocates an array for every bar; length is constant per call, so this can be reused.

Suggested refactor
-        for (var i = 0; i < stockData.Count; i++)
-        {
-            var xArray = new double[length + 1];
+        var xArray = new double[length + 1];
+        for (var i = 0; i < stockData.Count; i++)
+        {
             for (var j = 1; j <= length; j++)
             {
                 var prevPrice = i >= j - 1 ? inputList[i - (j - 1)] : 0;
                 xArray[j] = prevPrice;
             }
src/Calculations/Ehlers/EhlersStoZ.cs (1)

832-842: Perf: consider reusing per-bar scratch buffers (priceArray/rankArray, trunArray).
Line 834–835 and Line 911 allocate arrays per bar; for large stockData.Count, this can produce avoidable GC pressure.

Also applies to: 909-917

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5acc49f and 18965cf.

📒 Files selected for processing (3)
  • src/Calculations/Ehlers/EhlersAtoD.cs
  • src/Calculations/Ehlers/EhlersLtoR.cs
  • src/Calculations/Ehlers/EhlersStoZ.cs
🧰 Additional context used
🧬 Code graph analysis (1)
src/Calculations/Ehlers/EhlersAtoD.cs (3)
src/Helpers/MathHelper.cs (6)
  • Pow (66-80)
  • Sqrt (35-38)
  • ToDegrees (45-48)
  • Exp (87-105)
  • Log (25-28)
  • MathHelper (13-141)
src/Helpers/CalculationsHelper.cs (3)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
src/Helpers/RollingWindow.cs (2)
  • RollingSum (20-79)
  • Average (58-67)
⏰ 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: Analyze (csharp)
🔇 Additional comments (2)
src/Calculations/Ehlers/EhlersAtoD.cs (1)

558-599: Nice: the high-risk loop/precedence patterns read correctly now (DFT reset/max, cycle amplitude loop, adaptive bandpass precedence, correlation n-capping).

Also applies to: 1508-1522, 1795-1801, 1849-1851, 2047-2070

src/Calculations/Ehlers/EhlersLtoR.cs (1)

155-166: No changes required—this code is correct as written.

MinPastValues has no special assumptions about its minIndex parameter. When length1 == 1, passing minIndex = 0 is valid: the condition currentIndex >= 0 always evaluates to true in the loop and the method returns the expected value. The parameter validation Math.Max(length1, 1) ensures length1 >= 1, so the edge case is already handled safely.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Copy Markdown

@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

🤖 Fix all issues with AI agents
In `@src/Calculations/Ehlers/EhlersStoZ.cs`:
- Line 1136: The CalculateEhlersTriangleWindowIndicator method currently assigns
stockData.IndicatorName = IndicatorName.EhlersSimpleWindowIndicator; which
mislabels outputs; update the assignment in
CalculateEhlersTriangleWindowIndicator to set
IndicatorName.EhlersTriangleWindowIndicator instead, ensuring the
stockData.IndicatorName matches the method name and downstream consumers receive
the correct indicator enum value.
♻️ Duplicate comments (3)
src/Calculations/Ehlers/EhlersStoZ.cs (2)

294-294: Potential division by zero when length2 = 1

On line 345, the db calculation uses Math.Log(...) / Math.Log(length2). When length2 = 1, Math.Log(1) = 0, which causes division by zero and produces NaN/Infinity.

The past review comment suggested changing Math.Max(length2, 1) to Math.Max(length2, 2) to avoid this edge case.

Proposed fix
-        length2 = Math.Max(length2, 1);
+        length2 = Math.Max(length2, 2);

1188-1188: Indicator name mismatch: CalculateEhlersSimpleWindowIndicator sets wrong IndicatorName

The method CalculateEhlersSimpleWindowIndicator incorrectly sets IndicatorName.EhlersTriangleWindowIndicator. This is the inverse of the error above - the indicator names appear to have been accidentally swapped between the two methods.

Proposed fix
-        stockData.IndicatorName = IndicatorName.EhlersTriangleWindowIndicator;
+        stockData.IndicatorName = IndicatorName.EhlersSimpleWindowIndicator;
src/Calculations/Ehlers/EhlersLtoR.cs (1)

1409-1409: Potential division by zero when length = 1

On line 1442, the alpha calculation uses Math.Log(ratioSum) / Math.Log(length). When length = 1, Math.Log(1) = 0, which causes division by zero and produces NaN/Infinity.

The past review comment suggested changing Math.Max(length, 1) to Math.Max(length, 2) to avoid this edge case.

Proposed fix
-        length = Math.Max(length, 1);
+        length = Math.Max(length, 2);
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 18965cf and 685f970.

📒 Files selected for processing (3)
  • src/Calculations/Ehlers/EhlersAtoD.cs
  • src/Calculations/Ehlers/EhlersLtoR.cs
  • src/Calculations/Ehlers/EhlersStoZ.cs
🧰 Additional context used
🧬 Code graph analysis (2)
src/Calculations/Ehlers/EhlersLtoR.cs (4)
src/Helpers/MathHelper.cs (6)
  • MathHelper (13-141)
  • Exp (87-105)
  • Pow (66-80)
  • ToDegrees (45-48)
  • Log (25-28)
  • Sqrt (35-38)
src/Helpers/CalculationsHelper.cs (4)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
  • MinPastValues (1232-1235)
src/Core/MovingAverageCore.cs (3)
  • ExponentialMovingAverage (53-81)
  • SimpleMovingAverage (7-25)
  • WeightedMovingAverage (27-51)
src/Helpers/RollingWindow.cs (1)
  • RollingSum (20-79)
src/Calculations/Ehlers/EhlersStoZ.cs (5)
src/Calculations/Ehlers/EhlersAtoD.cs (17)
  • Calculations (4-2389)
  • StockData (13-92)
  • StockData (101-142)
  • StockData (151-184)
  • StockData (193-233)
  • StockData (245-293)
  • StockData (304-338)
  • StockData (347-379)
  • StockData (391-454)
  • StockData (463-513)
  • StockData (522-556)
  • StockData (567-618)
  • StockData (628-696)
  • StockData (707-768)
  • StockData (780-848)
  • StockData (860-895)
  • StockData (907-981)
src/Calculations/Ehlers/EhlersLtoR.cs (3)
  • Calculations (4-1630)
  • List (1533-1578)
  • Dictionary (1580-1629)
src/Calculations/MovingAverages/MovingAveragesAtoD.cs (1)
  • Calculations (6-1300)
src/Helpers/CalculationsHelper.cs (4)
  • SetOutputValues (111-126)
  • SetSignals (143-152)
  • SetCustomValues (154-169)
  • MinPastValues (1232-1235)
src/Helpers/MathHelper.cs (6)
  • Exp (87-105)
  • Pow (66-80)
  • Sqrt (35-38)
  • MathHelper (13-141)
  • ToDegrees (45-48)
  • ToRadians (55-58)
⏰ 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: Analyze (csharp)
🔇 Additional comments (10)
src/Calculations/Ehlers/EhlersLtoR.cs (3)

1-66: LGTM - Well-structured indicator implementation

The CalculateEhlersRoofingFilterV2 method demonstrates good practices with proper parameter validation, safe coefficient computation, and correct signal generation.


676-854: LGTM - Mesa Predict V1 properly addresses previous buffer allocation issues

The buffers are now correctly sized based on parameters and allocated once before the main loop, with proper clearing on each iteration. The upperLength >= 2 constraint and length3 >= length1 validation prevent edge-case index issues.


1532-1629: LGTM - Helper methods correctly use try/finally for state restoration

Both GetCustomValuesListInternal and GetOutputValuesInternal now properly wrap the calculator invocation in a try/finally block, ensuring options flags and stockData properties are always restored even if the calculator throws an exception.

src/Calculations/Ehlers/EhlersStoZ.cs (4)

23-24: LGTM - Correctly uses helper method for chained calculation

The use of GetCustomValuesListInternal ensures that CustomValuesList is populated even when the consumer has disabled custom values via IndicatorOptions. This pattern is consistently applied throughout the file.


513-514: LGTM - Precedence issue fixed with proper parentheses

The expressions now correctly use 2 * Math.PI / (2 * length1) with parentheses around the denominator, ensuring the angle is inversely proportional to length1 as intended for band-pass filter calculations.


838-839: LGTM - Buffer sizing now based on input parameter

The arrays are now allocated with size length + 1 based on the input parameter rather than a fixed size, eliminating the potential IndexOutOfRangeException for larger length values.


1071-1073: LGTM - Math.Log safety check properly implemented

The code now correctly guards against invalid logarithm inputs by checking temp > 0 before calling Math.Log10(temp), preventing NaN or -Infinity propagation.

src/Calculations/Ehlers/EhlersAtoD.cs (3)

13-92: LGTM!

The CalculateEhlersAdaptiveCyberCycle implementation correctly handles boundary conditions, division by zero checks, and follows the expected pattern for Ehlers indicators.


1-5: Overall structure looks good

The file implements a comprehensive set of Ehlers indicators following consistent patterns:

  • Proper parameter bounds validation with Math.Max
  • Safe nested indicator access via GetOutputValuesInternal and GetCustomValuesListInternal
  • Consistent signal generation and output value handling
  • Division-by-zero guards throughout

Previous review issues (Anticipate Indicator, DFT accumulators, Cycle Amplitude loop, Adaptive Band Pass Filter precedence, Adaptive Stochastic V2 initialization, Alternate SNR log handling, Convolution Indicator n-window) have all been addressed.


1208-1240: The band-pass filter does not properly maintain per-period state across iterations.

In CalculateEhlersCombFilterSpectralEstimate (lines 1208-1240), the band-pass feedback values prevBp1 and prevBp2 (lines 1213-1214) are fetched once before the candidate-period loop and reused for all periods j. While period-specific coefficients (alpha, beta, gamma) are recalculated for each j, the band-pass state should ideally maintain independent history per period to correctly estimate each candidate frequency's energy. Currently, only the final j iteration's bp is stored (line 1240), which means subsequent iterations use feedback from the wrong period's filter. This violates the intended per-period spectral estimation and may cause incorrect dominant cycle detection. Consider maintaining separate bp state for each candidate period, or verify this approximation is documented as intentional.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Copy Markdown

@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

🤖 Fix all issues with AI agents
In `@examples/OoplesFinance.StockIndicators.DevConsole/Program.cs`:
- Around line 134-147: The run-all branch currently calls RunBatchExample,
RunStreamingExample, RunMultiSeriesExample, RunComplexSetupExample and
RunSessionExample but omits RunFacadeSketches(), causing inconsistency with the
menu "a" behavior; update the runAll branch to also call RunFacadeSketches() (in
the same order as the menu, e.g., call RunFacadeSketches() before checking
noPause/ Pause()) so both paths execute the same six examples (refer to
RunBatchExample, RunStreamingExample, RunMultiSeriesExample,
RunComplexSetupExample, RunSessionExample and RunFacadeSketches).
🧹 Nitpick comments (4)
examples/OoplesFinance.StockIndicators.DevConsole/Program.cs (1)

26-33: Potential infinite loop when stdin is closed.

If the console is run non-interactively (e.g., piped input that closes), Console.ReadLine() will return null repeatedly, causing the loop to spin. Consider breaking on null or treating it as quit:

Suggested improvement
         while (true)
         {
             PrintMenu();
             var input = Console.ReadLine();
             if (input == null)
             {
-                continue;
+                return;
             }
examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketches.cs (3)

174-270: Consider adding cycle detection for the dependency graph.

The plan allows building a series dependency graph, but there's no validation against cycles (e.g., A depends on B, B depends on A). In production code, this could cause a StackOverflowException during resolution. For demo code this may be acceptable, but consider adding a visited set during resolution if this pattern is intended for broader use.

💡 Potential cycle detection in SeriesContext.Resolve
 public List<double> Resolve(string name)
+{
+    return Resolve(name, new HashSet<string>(StringComparer.OrdinalIgnoreCase));
+}
+
+private List<double> Resolve(string name, HashSet<string> visiting)
 {
     if (_cache.TryGetValue(name, out var cached))
     {
         return cached;
     }

+    if (!visiting.Add(name))
+    {
+        throw new InvalidOperationException($"Cycle detected in series dependency graph at '{name}'.");
+    }
+
     // ... rest of resolution logic
+
+    visiting.Remove(name);
     _cache[name] = resolved;
     return resolved;
 }

344-358: Zero-padding for mismatched series lengths may produce misleading results.

When combining series of different lengths, using 0 as the default value (lines 352-353) could silently produce incorrect results. For example, if RSI has a warmup period and outputs fewer values, the spread would compute SMA - 0 instead of indicating missing data.

Consider using double.NaN as the default or validating that series lengths match:

💡 Use NaN for missing values
 for (var i = 0; i < count; i++)
 {
-    var l = i < left.Count ? left[i] : 0;
-    var r = i < right.Count ? right[i] : 0;
+    var l = i < left.Count ? left[i] : double.NaN;
+    var r = i < right.Count ? right[i] : double.NaN;
     combined.Add(node.Combiner!(l, r));
 }

467-490: Thread safety limitation with Buffered processing mode.

The updates list is mutated directly from the callback (line 485), which is safe for Inline processing but would cause race conditions if Buffered mode were used (callbacks could come from different threads).

Since this is demo code and the example explicitly uses Inline mode, this is acceptable. Consider adding a comment or validation if this pattern might be reused:

💡 Optional: Add mode validation or documentation
 public List<StreamingIndicatorStateUpdate> Run(IStreamSource source, StreamingOptions options)
 {
     // ... validation ...

+    // Note: This simple collection approach is only thread-safe for Inline processing mode.
+    // For Buffered mode, use a concurrent collection or synchronization.
     var updates = new List<StreamingIndicatorStateUpdate>();
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b9a12ee and c6c0aa1.

📒 Files selected for processing (2)
  • examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketches.cs
  • examples/OoplesFinance.StockIndicators.DevConsole/Program.cs
🧰 Additional context used
🧬 Code graph analysis (1)
examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketches.cs (2)
src/Streaming/StreamingOptions.cs (2)
  • StreamingOptions (39-122)
  • StreamingProcessingMode (108-111)
src/Streaming/StreamingSession.cs (1)
  • IStreamObserver (204-212)
🔇 Additional comments (16)
examples/OoplesFinance.StockIndicators.DevConsole/Program.cs (2)

556-582: Synchronous blocking in Start() - acceptable for demo, but worth noting.

The Start() method processes all events synchronously on the calling thread. With a non-zero _delay, this will block the caller. This is acceptable for a dev console demo, but if this pattern is used elsewhere, consider async processing.


650-679: Clean discriminated union pattern using readonly struct.

Good use of readonly struct with factory methods for type safety. The pattern ensures consistent event construction.

examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketches.cs (14)

1-14: LGTM!

Clean setup with appropriate namespaces, visibility (internal static), and the use of CultureInfo.InvariantCulture for consistent number formatting.


16-23: LGTM!

Clean entry point that orchestrates the three demonstration sketches.


25-45: LGTM!

Good demonstration of chaining indicators: base price → SMA/RSI → further smoothing → combining series. The flow clearly illustrates the facade pattern.


47-68: LGTM!

Clear demonstration of extracting named outputs from multi-output indicators (MACD Signal) and combining them.


70-100: LGTM!

Good demonstration of the streaming facade with inline processing mode. The configuration is clear and the result handling is properly guarded.


102-121: LGTM!

Clean helper methods with proper edge-case handling for empty lists and NaN values.


123-158: LGTM!

Good synthetic data generation with reproducible seeding, proper OHLC relationships, and boundary protections for price values.


160-172: LGTM!

Simple deterministic trade event generator suitable for streaming demonstration.


371-386: LGTM!

Clean result wrapper with appropriate TryGetSeries pattern.


388-396: LGTM!

Simple immutable reference type appropriate for internal use.


398-434: LGTM!

Well-structured discriminated union pattern using factory methods to ensure type safety.


436-441: LGTM!

Appropriate enum for the node kind discrimination.


492-502: LGTM!

Good use of readonly struct for the lightweight step container.


505-590: LGTM!

Clean replay implementation appropriate for testing and demonstration. The symbol filtering and stop mechanism work well for this use case.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Copy link
Copy Markdown

@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

🤖 Fix all issues with AI agents
In `@examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV2.cs`:
- Around line 473-493: In Run(IStreamSource source, StreamingOptions options)
ensure the symbols array passed to StreamingSession.Create always contains the
instance _symbol: if options.Symbols is null use new[] { _symbol }; otherwise
merge/validate options.Symbols to include _symbol (e.g. create a new array/list
that contains all distinct values from options.Symbols plus _symbol) and assign
that to the local symbols variable before calling StreamingSession.Create and
RegisterStatefulIndicator to avoid subscribing to a set that excludes the
indicator's target symbol.

In `@examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV4.cs`:
- Around line 943-947: The Execute(TradeRequest request) method is printing a
secret (_options.ApiKey) to the console; remove or mask the credential instead
of logging it. Update the Console.WriteLine call in Execute to omit
_options.ApiKey (e.g., log only a masked value like "<redacted>" or log
non-sensitive fields such as request.SignalId and request.Action) so that
_options.ApiKey is never emitted to logs or console output.

In `@examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV5.cs`:
- Around line 781-989: The log output currently prints sensitive fields
directly; update EmailNotificationChannel.Notify, SmsNotificationChannel.Notify,
WebhookNotificationChannel.Notify and AlpacaTradeAdapter.Execute to stop writing
raw secrets (EmailOptions.To, SmsOptions.Number, WebhookOptions.Url,
AlpacaOptions.ApiKey) to the console — instead log non-sensitive context or
masked values (e.g. "<redacted>" or a short hash/last4) or omit these fields
altogether while keeping notification name/value and request.SignalId/Action;
ensure changes are applied in the Notify methods of
EmailNotificationChannel/SmsNotificationChannel/WebhookNotificationChannel and
in AlpacaTradeAdapter.Execute.

In `@examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV6.cs`:
- Around line 1933-1952: The loop in StartStreaming registers all indicator
nodes regardless of ComputePolicy, bypassing the lazy filtering in
_activeSeries; change the registration to skip any indicator whose handle is not
in _activeSeries when ComputePolicy == ComputePolicy.Lazy: inside the foreach
over _nodes (checking pair.Value.Kind == SeriesNodeKind.Indicator) add a guard
that if (ComputePolicy == ComputePolicy.Lazy &&
!_activeSeries.Contains(pair.Key)) continue (or equivalent), so only active
handles are passed to CreateStreamingState and
session.RegisterStatefulIndicator, preserving existing behavior for non-lazy
policies.
- Around line 1594-1810: The demo adapters currently log sensitive fields;
update EmailNotificationChannel.Notify, SmsNotificationChannel.Notify,
WebhookNotificationChannel.Notify and AlpacaTradeAdapter.Execute (and any other
Console.WriteLine that prints credentials) to avoid exposing PII/secrets —
replace full values with a masked form (e.g., mask all but last chars, show
domain only, or omit entirely) by calling a reusable helper (e.g., MaskSecret)
or exposing masked properties on
EmailOptions/SmsOptions/WebhookOptions/AlpacaOptions; ensure only non-sensitive
context (notification.Name, notification.Value, trade action/signal) is logged.
🧹 Nitpick comments (1)
examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV4.cs (1)

1116-1134: Consider isolating errors in notification/trading dispatch.

If one notification channel throws (lines 1119-1122), subsequent channels and trading rules won't execute. For a more robust demo, consider wrapping each dispatch in try-catch to prevent cascade failures.

Suggested approach
         private void Dispatch(SignalRule rule, double value)
         {
             var notification = new NotificationEvent(rule.Id, rule.Name, value, DateTime.UtcNow);
             for (var i = 0; i < _notifications.Count; i++)
             {
-                _notifications[i].Notify(notification);
+                try
+                {
+                    _notifications[i].Notify(notification);
+                }
+                catch (Exception ex)
+                {
+                    Console.WriteLine($"Notification error: {ex.Message}");
+                }
             }
 
             for (var i = 0; i < _autoTrading.Rules.Count; i++)
             {
                 var ruleConfig = _autoTrading.Rules[i];
                 if (ruleConfig.SignalId != rule.Id)
                 {
                     continue;
                 }
 
-                ruleConfig.Adapter.Execute(new TradeRequest(rule.Id, ruleConfig.Action, DateTime.UtcNow));
+                try
+                {
+                    ruleConfig.Adapter.Execute(new TradeRequest(rule.Id, ruleConfig.Action, DateTime.UtcNow));
+                }
+                catch (Exception ex)
+                {
+                    Console.WriteLine($"Trade execution error: {ex.Message}");
+                }
             }
         }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e1841c and 53baa7c.

📒 Files selected for processing (11)
  • examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketches.cs
  • examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV2.cs
  • examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV3.cs
  • examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV4.cs
  • examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV5.cs
  • examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV6.cs
  • examples/OoplesFinance.StockIndicators.DevConsole/Program.cs
  • src/Streaming/StatefulIndicators.Batch21.cs
  • src/Streaming/StatefulIndicators.Batch24.cs
  • src/Streaming/StatefulIndicators.Batch26.cs
  • src/Streaming/StatefulIndicators.Batch27.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketches.cs
🧰 Additional context used
🧬 Code graph analysis (3)
examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV2.cs (2)
examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketches.cs (14)
  • StockData (371-379)
  • ReplayStreamSource (517-602)
  • ReplayStreamSource (521-524)
  • LastValue (113-116)
  • List (123-158)
  • List (160-172)
  • List (287-290)
  • List (292-328)
  • List (330-334)
  • List (336-353)
  • List (355-369)
  • List (478-502)
  • ReplayStreamSubscription (541-601)
  • ReplayStreamSubscription (548-553)
examples/OoplesFinance.StockIndicators.DevConsole/Program.cs (7)
  • ReplayStreamSource (593-722)
  • ReplayStreamSource (598-602)
  • LastValue (537-540)
  • List (451-488)
  • List (490-506)
  • ReplayStreamSubscription (619-721)
  • ReplayStreamSubscription (628-635)
examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV6.cs (5)
src/Streaming/StreamingIndicatorEngine.cs (1)
  • TickerData (558-569)
src/Streaming/Providers/Alpaca/IAlpacaStreamClient.cs (3)
  • SubscribeTrades (5-5)
  • SubscribeQuotes (6-6)
  • SubscribeBars (7-7)
src/Streaming/StreamingOptions.cs (1)
  • StreamingProcessingMode (108-111)
src/Streaming/BarTimeframe.cs (1)
  • TimeSpan (29-44)
src/Streaming/StreamingSession.cs (1)
  • IStreamObserver (204-212)
examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV3.cs (3)
src/Streaming/StreamingOptions.cs (1)
  • StreamingProcessingMode (108-111)
src/Streaming/StreamingIndicatorEngine.cs (1)
  • TickerData (558-569)
src/Streaming/StreamingSession.cs (1)
  • IStreamObserver (204-212)
⏰ 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: Analyze (csharp)
🔇 Additional comments (14)
examples/OoplesFinance.StockIndicators.DevConsole/Program.cs (1)

199-252: CLI example routing is clear and user-friendly.

The alias handling for example names keeps the CLI ergonomic without adding complexity.

examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV3.cs (1)

76-107: Streaming + signals demo reads cleanly.

The replay source → builder → runtime → alerts flow is easy to follow.

examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV4.cs (12)

1-24: LGTM!

Clean entry point structure with appropriate access modifiers for an internal dev console sketch. The use of CultureInfo.InvariantCulture for formatting is a good practice.


26-94: LGTM!

Both demo methods demonstrate clean builder patterns. The capture pattern for SeriesHandle variables (lines 69-71, 113) is safe since the ConfigureIndicators callback is invoked synchronously within the builder chain.


96-135: LGTM!

The streaming demo is well-structured. Using StreamingProcessingMode.Inline ensures sequential processing, avoiding potential concurrency issues with the captured state.


169-218: LGTM!

The sample data generators use a fixed seed (Random(512)) ensuring reproducibility, which is ideal for demo/test scenarios. The price floor clamping at line 183-186 prevents unrealistic negative values.


254-354: LGTM!

The StockIndicatorBuilder implements a clean fluent builder pattern. Creating defensive copies of _nodes and _keys in Build() (lines 314-315) prevents external mutation of internal state.


409-424: LGTM!

The Formula method correctly handles arithmetic operations. The division case (line 416) returns IEEE 754 Infinity/NaN for division by zero, which is appropriate behavior for financial indicator calculations. The default case returning double.NaN is a safe fallback.


554-597: LGTM!

The SignalRule class implements correct threshold crossing logic. The IsCrossTriggered method properly handles the edge case where previous value is null or NaN (lines 585-588), preventing false positives on initial data points.


1169-1205: LGTM!

The Resolve method correctly implements cycle detection using a visiting set with proper backtracking (line 1202). This prevents infinite recursion in series graphs with circular dependencies.


1346-1378: LGTM!

The option classes use defensive validation (Math.Max(1, length)) to ensure indicator lengths are always at least 1, preventing potential division-by-zero or invalid calculations downstream.


1395-1466: LGTM!

The SeriesHandle and IndicatorKey structs correctly implement IEquatable<T> with consistent Equals and GetHashCode overrides, making them suitable for use as dictionary keys. The use of HashCode.Combine in IndicatorKey.GetHashCode() is the idiomatic approach.


1504-1589: LGTM!

The ReplayStreamSource demonstrates proper defensive coding with null checks in Subscribe (lines 1515-1523). The ReplayStreamSubscription correctly implements the disposal pattern where Dispose calls Stop, and uses case-insensitive symbol matching which is appropriate for stock tickers.


1598-1636: LGTM!

The helper methods provide clean abstractions for MACD output key mapping and streaming state creation. The ExtractStreamingValue method properly handles the case where update.Outputs is null (lines 1629-1631).

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

@ooples ooples changed the title Feature/performance and refactoring v2.0: Performance and Modern API Refactoring Jan 21, 2026
Copy link
Copy Markdown

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

🤖 Fix all issues with AI agents
In `@src/Builder/Catalogs/AutoTradingCatalog.cs`:
- Around line 36-39: Add a null guard to the AddAdapter method: validate the
adapter parameter (type IAutoTradeAdapter) and throw an ArgumentNullException if
it's null before adding to the _adapters collection; then proceed to return the
new AutoTradeAdapterBuilder(this, adapter) as before. Ensure the null check
occurs at the top of AddAdapter so _adapters.Add and the AutoTradeAdapterBuilder
constructor never receive a null adapter.

In `@src/Builder/Catalogs/IndicatorCatalog.cs`:
- Around line 297-313: The local variable spec in the KeltnerChannels method is
unused; remove the declaration "var spec = IndicatorSpecs.Create(...)" from the
KeltnerChannels method to eliminate the dead code, leaving the subsequent calls
that create upper, middle, and lower indicators intact (keep method signature
and the _builder.AddIndicator calls for upper, middle, lower).
- Around line 404-414: The divide branch in Formula (in IndicatorCatalog.cs)
currently uses (x, y) => x / y which yields Infinity/NaN on division-by-zero;
update the Formula method so the FormulaOp.Divide case guards against y == 0
(and optionally non-finite y) and returns double.NaN (or another sentinel)
instead of performing the raw division, i.e. change the lambda passed to
_builder.AddFormula for FormulaOp.Divide to check y and return double.NaN when y
is zero/invalid, leaving other branches unchanged.

In `@src/Builder/Catalogs/NotificationCatalog.cs`:
- Around line 69-72: The AddChannel method in NotificationCatalog currently adds
a potentially null INotificationChannel to _channels; add an upfront null guard
in NotificationCatalog.AddChannel to validate the channel parameter (e.g., if
channel is null throw new ArgumentNullException(nameof(channel))) before calling
_channels.Add(channel) so Build/dispatch cannot later encounter a null entry.

In `@src/Builder/Evaluation/IndicatorInvoker.cs`:
- Around line 36-58: The Invoke implementation in IndicatorInvoker.cs (the loop
filling args using methodParams, parameters, args, ConvertParameter,
GetDefaultValue and name) currently ignores extra entries in the incoming
parameters array; add a check that throws an ArgumentException when the caller
supplies more parameters than the target method accepts (i.e. parameters.Length
> methodParams.Length - 1) so extra arguments are rejected with a clear message
referencing the indicator name; implement this check either up-front before the
for-loop or after filling args by validating paramIndex == parameters.Length and
throw if not.

In `@src/Builder/Evaluation/SeriesEvaluator.cs`:
- Around line 59-115: GetBaseInput() ignores the node's SeriesKey so all base
nodes return the same input; update ResolveIndicator (and the base case in
Resolve where SeriesNodeKind.Base is handled) to pass node.SeriesKey into
GetBaseInput(SeriesKey) and implement GetBaseInput(SeriesKey) to pick the
correct series data (custom values or input values) for that SeriesKey rather
than always using _data.InputValues/_data.CustomValuesList; use the same
SeriesKey lookup logic used by GetOrCreateBaseSeries(key) to select the proper
series data source.
- Around line 163-195: GetOutputKey currently only maps MACD and Bollinger and
causes multi-output indicators to fall back to CustomValuesList; update
GetOutputKey (used by ExtractOutput) to include mappings for
IndicatorName.StochasticOscillator (Signal→"FastD", Histogram→"SlowD"),
IndicatorName.KeltnerChannels (UpperBand→"UpperBand", MiddleBand→"MiddleBand",
LowerBand→"LowerBand"), IndicatorName.DonchianChannels
(UpperBand→"UpperChannel", MiddleBand→"MiddleChannel",
LowerBand→"LowerChannel"), and IndicatorName.IchimokuCloud (Primary→"TenkanSen",
Signal→"KijunSen", UpperBand→"SenkouSpanA", LowerBand→"SenkouSpanB",
Histogram→"ChikouSpan"); then replace the duplicate GetOutputKey implementation
in StreamingIndicatorFactory.cs with a single shared helper (e.g.,
IndicatorOutputKey.GetOutputKey) and have both SeriesEvaluator.GetOutputKey and
StreamingIndicatorFactory call that helper so batch and streaming use the same
mapping.

In `@src/Builder/IndicatorDataSource.cs`:
- Around line 69-82: FromBatch and FromStreaming create an IndicatorDataSource
with mismatched payloads when passed null; add guard clauses in the factory
methods to throw ArgumentNullException when their required inputs are null
(throw if data is null in FromBatch and if source is null in FromStreaming) so
the IndicatorDataSource(IndicatorSourceKind.Batch, data, null, defaults) and
IndicatorDataSource(IndicatorSourceKind.Streaming, null, source, defaults) calls
never receive a null payload for the given Kind.

In `@src/Builder/IndicatorRuntime.cs`:
- Around line 179-186: The streaming update currently allocates new arrays and
dictionary copies on every update (see RegisterStatefulIndicator callback using
StreamingIndicatorFactory.ExtractValue, values[handle] = new[] { value },
UpdateFormulaNodes, and new IndicatorSnapshot/Dictionary for Publish), causing
GC pressure; modify the code to reuse a per-handle buffer (e.g., a preallocated
double[] or fixed-size span stored alongside each SeriesHandle) instead of new[]
on each update, update UpdateFormulaNodes to accept and operate on these
reusable buffers in-place, and replace frequent new Dictionary/IndicatorSnapshot
allocations with a reusable snapshot wrapper or pooled dictionary that can be
cleared/refilled before Publish while ensuring thread-safety when
ResolveStreamingSeries reads the snapshot.
- Around line 422-439: The Dispatch method currently lets exceptions from any
notification channel or trading adapter abort the whole update; wrap each
_notifications[i].Notify(notification) call in a try/catch and each
ruleConfig.Adapter.Execute(new TradeRequest(...)) call in a try/catch so
failures are isolated and the loops continue, capturing/logging the exception
(or collecting them into a list) with contextual info (notification index or
ruleConfig) and after both loops either rethrow an AggregateException if you
need to surface combined errors or emit a single summary error via your logger;
reference Dispatch, NotificationEvent, _notifications, Notify,
_autoTrading.Rules, ruleConfig.Adapter.Execute and TradeRequest when making the
changes.
- Around line 307-319: The Publish method currently returns early when
!_behavior.EmitWarmup && !_hasEmitted which discards the first snapshot
(breaking batch where there's only one). Modify Publish so that when warmup
suppression is off but this is the very first snapshot (Latest is null / no
prior emission) it still sets Latest, invokes Updated and calls EvaluateSignals;
keep the existing _hasEmitted semantics otherwise. Concretely, in Publish
(references: Publish, _behavior.EmitWarmup, _hasEmitted, Latest, Updated,
EvaluateSignals) remove the early return and conditionally skip only the
emission logic you intend to suppress, or add a check to bypass suppression when
Latest == null (first snapshot) so batch mode receives the snapshot.
- Around line 144-190: The StartStreaming method currently creates the
StreamingSession with "using var session = StreamingSession.Create(...)" which
disposes the session when StartStreaming returns, breaking the non-blocking
callbacks; change this to store the session in a private instance field (e.g.
add a _streamingSession field), assign _streamingSession =
StreamingSession.Create(stream, symbols, options: options) instead of the using,
and use _streamingSession when registering callbacks and calling Start(); also
ensure any existing _streamingSession is disposed before replacing it and that
_streamingSession is explicitly disposed when the runtime stops or is disposed
(implement disposal/stop logic to call _streamingSession.Dispose()).

In `@src/Builder/Notifications/NotificationTypes.cs`:
- Around line 54-95: The XML comments on EmailOptions claim defaults are loaded
from environment variables but EmailOptions currently doesn't apply them; fix by
implementing environment-based defaults and/or updating docs: add a factory
method (e.g., EmailOptions.LoadFromEnvironment) or have the EmailChannel
constructor populate EmailOptions from Environment.GetEnvironmentVariable for
SMTP_HOST, SMTP_USERNAME, SMTP_PASSWORD, SMTP_FROM, SMTP_TO and parse SMTP_PORT
(default 587) and default UseSsl to true, or alternatively change the XML
summaries on EmailOptions/SmtpHost/SmtpPort/Username/Password/From/To/UseSsl to
remove the misleading "Defaults from ..." text so comments match behavior.
Ensure SmtpPort parsing handles missing/invalid values and UseSsl defaults to
true if unset.

In `@src/Builder/Options/BacktestOptions.cs`:
- Around line 18-21: The XML doc claims InitialCapital defaults to 100,000 but
the nullable property is never initialized; update BacktestOptions to enforce
the default by either making InitialCapital non-nullable with an explicit
default (e.g., public double InitialCapital { get; set; } = 100000;) or set
InitialCapital = 100000 in BacktestOptions constructor, and adjust the XML
summary if you choose not to set the default. Ensure references to
InitialCapital in consuming code still work with the chosen change.

In `@src/Builder/Options/SymbolOptions.cs`:
- Around line 29-31: The For overloads (SymbolOptions.For(params SymbolId[]
symbols) and the IEnumerable overload around the other For method) currently
throw on null params; add explicit null checks at the start of each method and
throw ArgumentNullException when the input is null so the failure mode is clear.
Locate the static methods named For in SymbolOptions and validate their input
(symbols / IEnumerable<SymbolId>) and throw new
ArgumentNullException(nameof(symbols)) (or appropriate parameter name) before
constructing and returning the SymbolOptions instance.

In `@src/Builder/Series/IndicatorSnapshot.cs`:
- Around line 8-44: The class claims to be an immutable snapshot but
TryGetSeries currently mutates _series by caching resolved results; make the
class genuinely immutable by removing the caching assignment inside
IndicatorSnapshot.TryGetSeries (remove `_series[handle] = resolved;`) so
resolved arrays are returned via the out parameter but not stored, and ensure no
other methods (e.g., any use of _resolver) write to _series; alternatively, if
caching is required, replace Dictionary<SeriesHandle,double[]> _series with a
thread-safe ConcurrentDictionary and perform thread-safe writes there—pick one
approach and update the XML summary accordingly to match the chosen behavior.

In `@src/Builder/Series/SymbolId.cs`:
- Around line 139-149: AllUsSymbols currently lists Aapl, Msft, Goog, Amzn,
Tsla, Nvda, Meta but omits the defined constants Spy and Qqq; since AllUs and
All mirror this array, include SymbolId.Spy and SymbolId.Qqq in the AllUsSymbols
initializer (and keep All pointing to AllUsSymbols) so the documented "all US
market symbols" actually contains SPY/QQQ, and update the XML doc comment on
AllUs/All if you intend a different semantic (or alternatively rename AllUs to
DefaultEquities if you purposely exclude ETFs).

In `@src/Builder/Signals/SignalTypes.cs`:
- Around line 306-313: The Percent branch in the switch uses RequiredPercent
directly so values >100 or <0 make activation impossible or behave oddly; clamp
RequiredPercent into [0,100] before computing the ceiling threshold (e.g., use
Math.Clamp(RequiredPercent ?? 0, 0, 100) or Math.Min/Max) so
SignalGroupMode.Percent computes activeCount >= Math.Ceiling(total *
clampedPercent / 100d); apply the same defensive clamping where RequiredPercent
is used to avoid silent misconfigurations and keep SignalGroupMode.AtLeast
behavior unchanged (it still uses RequiredCount).

In `@src/Builder/Specs/IndicatorSpec.cs`:
- Around line 18-23: The IndicatorSpec constructor does not validate the
non-nullable Options parameter; update the IndicatorSpec(IndicatorName name,
IIndicatorSpecOptions options, IndicatorOutput output) constructor to guard
against a null options argument by throwing an ArgumentNullException (or
similar) when options is null, ensuring Name, Options and Output remain assigned
only after validation; reference the IndicatorSpec class and the
IIndicatorSpecOptions parameter to locate the constructor to add this fail-fast
check.

In `@src/Builder/StockIndicatorBuilder.cs`:
- Around line 74-78: The ConfigureIndicators method currently sets
_indicatorOptions but does not reset the _defaultsApplied flag, so subsequent
calls with different options skip ApplyIndicatorDefaults; update
ConfigureIndicators to compare the incoming options (or at least detect a
change) and if options differ from the current _indicatorOptions (or options is
non-null and not equal), set _defaultsApplied = false before calling
ApplyIndicatorDefaults, then assign _indicatorOptions and invoke
configure?.Invoke(_indicators) so defaults are reapplied when options change
(referencing ConfigureIndicators, _indicatorOptions, _defaultsApplied, and
ApplyIndicatorDefaults).
🧹 Nitpick comments (19)
benchmarks/README.md (1)

6-8: Add language specifiers to fenced code blocks.

The fenced code blocks are missing language identifiers, which aids syntax highlighting and tooling. Use shell or powershell as appropriate.

Proposed fix
 Run optimized benchmarks:
-```
+```shell
 dotnet run -c Release --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj

Run a subset:
- +shell
dotnet run -c Release --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -- --filter SMA


Run streaming fanout throughput/latency:
-```
+```shell
dotnet run -c Release --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -- --streaming-perf --ticks 100000

Use streaming fanout flags:
- +shell
dotnet run -c Release --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -- --streaming-perf --include-outputs
dotnet run -c Release --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -- --streaming-perf --extended


Compare optimized vs baseline (master by default):
-```
+```powershell
.\benchmarks\setup-baseline.ps1 -Ref master
dotnet run -c Release --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -- --filter *IndicatorBenchmarks*

Setup script options:
- +powershell
.\benchmarks\setup-baseline.ps1 -Ref # Compare against a specific branch/tag
.\benchmarks\setup-baseline.ps1 -Framework net9.0 # Use a different target framework
.\benchmarks\setup-baseline.ps1 -NoBuild # Only create worktree, skip build

Also applies to: 11-13, 16-18, 20-23, 27-30, 33-37

src/Builder/Options/SignalOptions.cs (2)

22-26: Silent clamping may hide caller bugs.

Using Math.Max(1, bars) silently corrects invalid input (e.g., 0 or negative values) rather than surfacing the error. This could mask programming mistakes at the call site. Consider throwing ArgumentOutOfRangeException for invalid input.

Proposed fix
 public SignalWindow(int bars, TimeSpan? duration = null)
 {
-    Bars = Math.Max(1, bars);
+    if (bars < 1)
+    {
+        throw new ArgumentOutOfRangeException(nameof(bars), bars, "Bars must be at least 1.");
+    }
+    Bars = bars;
     Duration = duration;
 }

49-60: Consider validating positive duration in factory methods.

Minutes and Hours don't validate that the input is positive. Passing zero or negative values would create windows with zero or negative durations, which is likely unintended.

Proposed fix
 public static SignalWindow Minutes(int minutes)
 {
+    if (minutes < 1)
+    {
+        throw new ArgumentOutOfRangeException(nameof(minutes), minutes, "Minutes must be at least 1.");
+    }
     return new SignalWindow(1, TimeSpan.FromMinutes(minutes));
 }

 public static SignalWindow Hours(int hours)
 {
+    if (hours < 1)
+    {
+        throw new ArgumentOutOfRangeException(nameof(hours), hours, "Hours must be at least 1.");
+    }
     return new SignalWindow(1, TimeSpan.FromHours(hours));
 }
src/Builder/Options/BenchmarkOptions.cs (1)

37-44: Consider validating the symbol parameter.

The Custom factory doesn't validate that symbol is meaningful. If a default SymbolId (with empty Value) is passed, it would create an invalid benchmark configuration. Per the SymbolId constructor in SymbolId.cs, null becomes an empty string.

Proposed fix
 public static BenchmarkOptions Custom(SymbolId symbol)
 {
+    if (string.IsNullOrWhiteSpace(symbol.Value))
+    {
+        throw new ArgumentException("Symbol value cannot be empty.", nameof(symbol));
+    }
     return new BenchmarkOptions
     {
         Benchmark = BenchmarkKind.Custom,
         CustomSymbol = symbol
     };
 }
src/Builder/Evaluation/StreamingIndicatorFactory.cs (2)

17-31: Direct casts may throw if spec is misconfigured.

The casts like ((SmaSpecOptions)spec.Options) will throw InvalidCastException if the Options type doesn't match the expected type for the indicator. Consider using pattern matching with is or validating the type before casting to provide a clearer error message.

Example using pattern matching
 IndicatorName.SimpleMovingAverage => spec.Options is SmaSpecOptions sma
     ? new SimpleMovingAverageState(sma.Length)
-    : null,
+    : throw new InvalidOperationException($"Invalid options type for SMA: {spec.Options?.GetType().Name}"),

53-76: Duplicated logic with SeriesEvaluator.GetOutputKey.

This GetOutputKey implementation is identical to the one in SeriesEvaluator.cs (lines 173-196 per the relevant code snippets). Consider extracting this to a shared utility to avoid maintaining the same logic in two places.

Proposed approach

Extract GetOutputKey to a shared location (e.g., IndicatorOutputMapper class) that both StreamingIndicatorFactory and SeriesEvaluator can reference:

internal static class IndicatorOutputMapper
{
    public static string? GetOutputKey(IndicatorName name, IndicatorOutput output)
    {
        // shared implementation
    }
}
src/Builder/Options/IndicatorOptions.cs (1)

61-64: Consider defensive copy of the names array.

The params IndicatorName[] names array is stored directly as IReadOnlyList<IndicatorName>. While IReadOnlyList<T> prevents modifications through the interface, the caller retains a reference to the underlying array and could mutate it. Consider creating a defensive copy.

Proposed fix
 public static IndicatorSelection Only(params IndicatorName[] names)
 {
-    return new IndicatorSelection(IndicatorPreset.Only, names);
+    return new IndicatorSelection(IndicatorPreset.Only, names.ToArray());
 }

Or use Array.AsReadOnly(names) for a true read-only wrapper.

src/Builder/Handles/IndicatorKey.cs (1)

39-77: Static properties allocate new instances on each access.

These static properties create new IndicatorKey instances each time they're accessed. Since this is a readonly struct (value type), the cost is minimal and equality comparisons work correctly. However, for consistency with common patterns, you could cache these as static readonly fields if they're accessed frequently in hot paths.

// Alternative: cache as static readonly fields
private static readonly IndicatorKey _sma = new(IndicatorName.SimpleMovingAverage, IndicatorOutput.Primary);
public static IndicatorKey Sma => _sma;

This is a minor optimization and likely unnecessary given the PR's performance focus on the hot path allocations elsewhere.

src/Builder/Trading/TradingTypes.cs (3)

8-14: Consider async for trade execution interface.

Real broker API calls (like Alpaca) are I/O-bound operations. A synchronous Execute method could block the calling thread. Consider providing an async variant.

💡 Suggested async alternative
 public interface IAutoTradeAdapter
 {
     void Execute(TradeRequest request);
+    
+    /// <summary>
+    /// Asynchronously executes a trade request.
+    /// </summary>
+    Task ExecuteAsync(TradeRequest request, CancellationToken cancellationToken = default);
 }

This can be deferred if the current design intentionally queues trades for batch execution.


106-124: Builder structs may dereference null fields if default-constructed.

AutoTradeAdapterBuilder and AutoTradeRuleBuilder are readonly structs. If default-constructed (e.g., default(AutoTradeAdapterBuilder)), the _catalog and _adapter fields will be null, causing NullReferenceException when methods are called.

Since the constructors are internal, this is likely controlled, but consider adding defensive checks or documenting this constraint.


249-256: Placeholder implementation noted.

The TODO for Alpaca API integration is acknowledged. The safety defaults (UsePaper ?? true) are appropriate.

Would you like me to open an issue to track the Alpaca API integration implementation?

src/Builder/Series/SeriesNode.cs (1)

74-84: Validate required inputs in factory methods

Indicator and CreateFormula accept spec/formula without validation, so invalid nodes can be created silently. A simple guard keeps failures closer to the call site.

💡 Suggested fix
     public static SeriesNode Indicator(SeriesKey key, SeriesHandle input, IndicatorSpec spec)
     {
+        if (spec is null) throw new ArgumentNullException(nameof(spec));
         return new SeriesNode(SeriesNodeKind.Indicator, key, input, spec, null, null, null);
     }
@@
     public static SeriesNode CreateFormula(SeriesKey key, SeriesHandle left, SeriesHandle right, Func<double, double, double> formula)
     {
+        if (formula is null) throw new ArgumentNullException(nameof(formula));
         return new SeriesNode(SeriesNodeKind.Formula, key, null, null, left, right, formula);
     }
src/Builder/Specs/IndicatorSpec.cs (1)

242-245: Prevent external mutation of GenericIndicatorOptions.Parameters.

Storing the caller’s array directly lets later mutations change behavior. Consider copying to keep the spec immutable.

♻️ Proposed change
 public GenericIndicatorOptions(object[] parameters)
 {
-    Parameters = parameters ?? Array.Empty<object>();
+    Parameters = parameters?.ToArray() ?? Array.Empty<object>();
 }
src/Builder/StockIndicatorBuilder.cs (1)

156-179: Avoid rebuilding signals/groups twice during Build.

ResolveActiveSeries() calls _signals.Build() and _signals.BuildGroups() and then Build() calls them again. Cache once to avoid divergence and extra work.

♻️ Suggested refactor
 public IndicatorRuntime Build()
 {
     EnsureDefaults();
 
     var symbols = ResolveSymbols();
     var timeframe = ResolveDefaultTimeframe();
-    var activeSeries = ResolveActiveSeries();
+    var signalRules = _signals.Build();
+    var groupRules = _signals.BuildGroups();
+    var activeSeries = ResolveActiveSeries(signalRules, groupRules);
     var streamingOptions = ResolveStreamingOptions(symbols, timeframe);
 
     return new IndicatorRuntime(
         _source,
         new Dictionary<SeriesHandle, SeriesNode>(_nodes),
         new Dictionary<IndicatorKey, SeriesHandle>(_keys),
-        _signals.Build(),
-        _signals.BuildGroups(),
+        signalRules,
+        groupRules,
         _notifications.Build(),
         _autoTrading.Build(),
         _behavior,
         activeSeries,
         symbols,
         streamingOptions,
         _signalOptions,
         _backtestOptions,
         _benchmarkOptions);
 }
 
-private IReadOnlyCollection<SeriesHandle> ResolveActiveSeries()
+private IReadOnlyCollection<SeriesHandle> ResolveActiveSeries(
+    IReadOnlyList<SignalRule> signalRules,
+    IReadOnlyList<SignalGroupRule> groupRules)
 {
     if (_indicatorOptions?.ComputePolicy == IndicatorComputePolicy.Eager)
     {
         return _nodes.Keys;
     }
 
     var active = new HashSet<SeriesHandle>();
-    var signalRules = _signals.Build();
     for (var i = 0; i < signalRules.Count; i++)
     {
         var series = signalRules[i].Series;
         if (series.Handle.HasValue)
         {
             active.Add(series.Handle.Value);
             AddDependencies(series.Handle.Value, active);
         }
         else if (series.Key.HasValue && _keys.TryGetValue(series.Key.Value, out var handle))
         {
             active.Add(handle);
             AddDependencies(handle, active);
         }
     }
 
-    var groupRules = _signals.BuildGroups();
     for (var i = 0; i < groupRules.Count; i++)
     {
         var conditions = groupRules[i].Conditions;
         for (var j = 0; j < conditions.Length; j++)
         {
             active.Add(conditions[j].Series);
             AddDependencies(conditions[j].Series, active);
         }
     }

Also applies to: 334-375

src/Builder/Series/SymbolId.cs (1)

12-15: Consider rejecting null/whitespace symbol values.

Currently null becomes string.Empty, which can hide errors and create an invalid symbol. Prefer a guard to fail fast.

♻️ Proposed guard
 public SymbolId(string value)
 {
-    Value = value ?? string.Empty;
+    if (string.IsNullOrWhiteSpace(value))
+    {
+        throw new ArgumentException("Symbol cannot be null or whitespace.", nameof(value));
+    }
+    Value = value;
 }
src/Builder/Notifications/NotificationTypes.cs (1)

191-305: Placeholder channels silently succeed.

All non-console channels currently only log to stdout, which can mislead callers into thinking notifications were delivered. Consider throwing NotSupportedException (or adding an explicit “placeholder” mode) to avoid silent no-ops until real integrations land.

src/Builder/Catalogs/IndicatorCatalog.cs (3)

47-50: Consider returning a custom type instead of ParameterInfo[].

Exposing System.Reflection.ParameterInfo[] directly in the public API couples consumers to reflection types. A custom DTO with Name, Type, HasDefaultValue, DefaultValue would provide a cleaner abstraction and allow future changes without breaking the API.


431-440: All and Core presets have identical behavior.

Both presets apply the same four indicators. If this is placeholder code, consider adding a TODO; otherwise, Core should likely be a subset of All with fewer indicators, or All should include more indicators.


476-549: IndicatorChain exposes fewer indicators than IndicatorCatalog.

The chain only provides 7 indicator methods while the catalog has 20+. Notably missing: Stochastic, Wma, Hma, Cci, WilliamsR, Roc, Momentum, StdDev, Tsi, Formula, and the channel indicators. Consider either expanding IndicatorChain for parity or documenting that Input should be used with the catalog for additional indicators.

Copy link
Copy Markdown

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

🤖 Fix all issues with AI agents
In `@src/Builder/Buffers/IndicatorBuffer.cs`:
- Around line 331-345: The WrapPooledArray implementation can return foreign
arrays to SharedPool because the constructor sets _pool = pool ?? SharedPool;
fix by adding an ownership flag (e.g. private bool _shouldReturnToPool or
_ownsArray) on IndicatorBuffer<T>, set it in WrapPooledArray to (pool != null)
when assigning _array/_count, and update Dispose (and any return-to-pool logic)
to only return the array when _shouldReturnToPool is true (and _pool != null);
ensure the existing constructor behavior remains unchanged and reference
IndicatorBuffer<T>, WrapPooledArray, the constructor, Dispose, _pool, _array,
and _count when making changes.

In `@src/Builder/IndicatorDataSource.cs`:
- Around line 95-111: In IndicatorDataSource.FromBatch, add an explicit null
check for the enumerable parameter "data" before calling data.ToList(); if data
is null throw an ArgumentNullException(nameof(data)) to provide a clear error
message; keep the rest of the method (including the existing empty/one-item
checks and the call to MergeStockData and the IndicatorDataSource constructor)
unchanged.
♻️ Duplicate comments (6)
src/Builder/IndicatorDataSource.cs (2)

84-87: Validate null input in FromBatch factory method.

Missing ArgumentNullException guard for the data parameter. Passing null will create an IndicatorDataSource with Kind = Batch but BatchData = null, causing downstream failures.


118-121: Validate null input in FromStreaming factory method.

Missing ArgumentNullException guard for the source parameter, similar to the FromBatch issue.

src/Builder/IndicatorRuntime.cs (4)

290-307: Batch mode may emit nothing when EmitWarmup is false.

When _behavior.EmitWarmup is false, the first (and only) batch snapshot is discarded by Publish(), leaving Latest as null and never invoking Updated. This effectively makes batch mode non-functional with warmup suppression.


309-355: StreamingSession disposed prematurely due to using statement.

The using var session on line 320 disposes the session when StartStreaming() returns, but session.Start() on line 354 is non-blocking. Registered callbacks (lines 344-351) will be unable to execute after disposal. Store the session as an instance field and dispose it in IndicatorRuntime.Dispose().


344-385: Hot-path allocations in streaming updates violate zero-allocation goal.

Each streaming update allocates:

  • new[] { value } (line 347, 382)
  • new Dictionary<SeriesHandle, double[]>(values) (line 349)

These per-update allocations conflict with the PR's "0 bytes heap allocations in hot path" target. Reuse pre-allocated buffers per handle.


587-605: Isolate notification and auto-trading failures.

A single exception in any notification channel (line 592) or trading adapter (line 603) aborts the entire dispatch loop. Consider catching per-channel/adapter and continuing, then aggregating errors.

🧹 Nitpick comments (5)
src/Builder/Buffers/IndicatorBuffer.cs (2)

158-183: Consider using span-based copy for ToList consistency.

ToArray efficiently uses AsSpan().CopyTo(), but ToList iterates element by element. Since this is a user-owned copy (not hot path), this is minor.

♻️ Optional: Use CollectionsMarshal for modern targets
 public List<T> ToList()
 {
     ThrowIfDisposed();
     var list = new List<T>(_count);
+#if NET8_0_OR_GREATER
+    System.Runtime.InteropServices.CollectionsMarshal.SetCount(list, _count);
+    AsSpan().CopyTo(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(list));
+#else
     for (int i = 0; i < _count; i++)
     {
         list.Add(_array![i]);
     }
+#endif
     return list;
 }

255-269: Clarify thread-safety claim in enumerator documentation.

The comment states "Thread-safe for concurrent reads" but this is only true if no concurrent writes or disposal occur. The capture of count and array provides snapshot semantics for the enumeration, not full thread safety.

📝 Suggested documentation clarification
     /// <summary>
     /// Returns an enumerator that iterates through the buffer.
-    /// Thread-safe for concurrent reads.
+    /// Captures count and array reference at enumeration start.
+    /// Not safe if buffer is modified or disposed during enumeration.
     /// </summary>
src/Builder/IndicatorDataSource.cs (1)

156-189: Consider validating data ordering and handling duplicates in MergeStockData.

The merge simply concatenates data without validating chronological order or deduplicating overlapping dates. Unsorted or duplicate data could cause incorrect indicator calculations.

♻️ Suggested enhancement
 private static StockData MergeStockData(List<StockData> dataList)
 {
+    // Sort by date to ensure chronological order
+    var allTicks = dataList
+        .SelectMany(d => d.TickerDataList)
+        .OrderBy(t => t.Date)
+        .ToList();
+
+    // Optionally deduplicate by date
+    // allTicks = allTicks.GroupBy(t => t.Date).Select(g => g.First()).ToList();
+
     // Calculate total capacity
-    int totalCapacity = 0;
-    foreach (var data in dataList)
-    {
-        totalCapacity += data.Count;
-    }
+    int totalCapacity = allTicks.Count;
src/Builder/IndicatorRuntime.cs (1)

276-279: Remove unused options suppression or implement their functionality.

The pattern of assigning to _ just to suppress unused warnings indicates these options aren't implemented yet. Either implement the functionality or remove the options from the constructor until needed.

src/Builder/Buffers/IndicatorBufferMath.cs (1)

182-208: Leverage SIMD for .NET Framework 4.6.1+ using runtime checks.

The "Simd" prefix is misleading since these are scalar loop-unrolled implementations. Since Vector is available in .NET Framework 4.6.1+ (via System.Numerics.Vectors NuGet), you can use Vector.IsHardwareAccelerated at runtime to conditionally employ SIMD acceleration without sacrificing compatibility.

♻️ Example: Conditional SIMD for Sum
private static double SimdSum(ReadOnlySpan<double> span)
{
    if (Vector.IsHardwareAccelerated && span.Length >= Vector<double>.Count)
    {
        var vectorSum = Vector<double>.Zero;
        var i = 0;
        var vectorEnd = span.Length - Vector<double>.Count;
        for (; i <= vectorEnd; i += Vector<double>.Count)
        {
            vectorSum += new Vector<double>(span.Slice(i));
        }
        var sum = Vector.Sum(vectorSum);
        for (; i < span.Length; i++)
        {
            sum += span[i];
        }
        return sum;
    }
    // Fallback to loop unrolling for systems without SIMD
    var sum = 0.0;
    var i = 0;
    var length = span.Length;
    var unrolledEnd = length - 4;
    for (; i <= unrolledEnd; i += 4)
    {
        sum += span[i] + span[i + 1] + span[i + 2] + span[i + 3];
    }
    for (; i < length; i++)
    {
        sum += span[i];
    }
    return sum;
}

Copy link
Copy Markdown

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

🤖 Fix all issues with AI agents
In `@MIGRATION.md`:
- Around line 52-100: The examples use local variables created inside
ConfigureIndicators/ConfigureSignals lambdas (sma, rsi, smaHandle) but then
reference them outside those lambdas; update the snippets to capture and
return/assign indicator handles to outer-scope variables so they can be used
later (e.g., declare handles before calling
StockIndicatorBuilder.ConfigureIndicators, set them inside the lambda, or have
ConfigureIndicators return the handles), then use those outer handles with
runtime.GetSeries(smaHandle) and signals.When(rsi); apply the same
handle-capturing fix to the streaming example and lines 188-195.

In `@README.md`:
- Around line 193-200: Add a language identifier (e.g., bash or shell) to the
fenced code blocks containing the dotnet run commands so markdownlint MD040 is
satisfied; update each block that contains the commands like "dotnet run
--project
examples/OoplesFinance.StockIndicators.DevConsole/OoplesFinance.StockIndicators.DevConsole.csproj"
and the non-interactive variant "dotnet run --project ... -- --run-all
--no-pause" (and the other blocks at the mentioned ranges) by changing the
opening fences from ``` to ```bash (or ```shell).
- Around line 207-241: The README tables violate markdownlint MD058 because they
lack blank lines before and after each table; locate the table blocks (they
start with the header line "| Indicator | Optimized (us) | Baseline (us) |
Speedup |" and end before the marker "<!-- PERF_TABLES_END -->") and add a
single blank line immediately before each table header and a single blank line
immediately after each table's final row so every table is separated by blank
lines (apply the same fix for the second occurrence referenced in the comment).
- Around line 121-159: The example uses indicator handles declared inside
ConfigureIndicators and signal variables declared inside ConfigureSignals (rsi,
sma, overboughtSignal, smaHandle) but then references them outside those
lambdas; capture and expose those handles to outer scope before using them.
Change the pattern so you declare variables in the outer scope (e.g.,
IndicatorHandle sma = null, rsi = null) then assign them from
indicators.Sma(20)/Rsi(14) inside ConfigureIndicators, and similarly assign the
signal handle (overboughtSignal) inside ConfigureSignals before calling
builder.Build() and runtime.GetSeries(smaHandle); ensure
StockIndicatorBuilder.ConfigureIndicators, ConfigureSignals, and GetSeries
use/return these handles so the variables are in scope when used after Build().

In `@src/Builder/Notifications/NotificationTypes.cs`:
- Around line 274-301: The HTTP objects in the SMS send path are not disposed;
wrap the FormUrlEncodedContent (content), HttpRequestMessage (request) and the
HttpResponseMessage (response returned from HttpClient.SendAsync) in using/using
var blocks or ensure Dispose is called after use inside the try block so
resources are freed; specifically update the code that creates content, request,
and calls HttpClient.SendAsync/GetAwaiter().GetResult() to dispose response and
content (and request) deterministically—apply the same pattern to the other
channels referenced (Webhook/Telegram/Discord) where variables named content,
request and response are used.

In `@src/Builder/Trading/TradingTypes.cs`:
- Around line 303-319: The HttpRequestMessage, its StringContent, and the
HttpResponseMessage are not disposed and can leak resources when Execute() runs
repeatedly; fix by creating and disposing them with using/using var patterns:
instantiate System.Net.Http.HttpRequestMessage and the
System.Net.Http.StringContent in a using block before calling
HttpClient.SendAsync(...).GetAwaiter().GetResult(), and wrap the returned
response (HttpResponseMessage) in a using block as well so response and its
Content are disposed after reading responseContent; ensure references to
HttpRequestMessage, StringContent, HttpClient.SendAsync, and response are
updated accordingly.
♻️ Duplicate comments (1)
src/Builder/Catalogs/AutoTradingCatalog.cs (1)

38-41: Guard against null adapters.

AddAdapter accepts null and stores it, which can surface as an NRE later. Consider adding an ArgumentNullException guard at the start.

Copy link
Copy Markdown

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

🤖 Fix all issues with AI agents
In `@src/Calculations/BollingerBands/BollingerBandsPart1.cs`:
- Around line 316-320: The call to GetVolatilitySignal incorrectly uses a
hardcoded threshold 0.5; instead retrieve the previous atrDev from atrDevList
(e.g., prevAtrDev = atrDevList.Count >= 1 ? atrDevList.Last() :
defaultThreshold) and pass that as the threshold parameter. To implement: after
computing atrDev, compute prevAtrDev from atrDevList (or use a sensible fallback
when empty), then add the current atrDev to atrDevList and call
GetVolatilitySignal(currentValue - currentEma, prevValue - prevEma, atrDev,
prevAtrDev). Reference symbols: GetVolatilitySignal, atrDevList, atrDev,
currentAtr, bbDiff.

In `@src/Calculations/BollingerBands/BollingerBandsPart2.cs`:
- Around line 35-42: The ATR% calculation for variable atrs can produce a zero
denominator when prevValue or currentLow and their corresponding delta (hc, lc,
lh) are zero; update the calculation in BollingerBandsPart2.cs (the expression
that assigns atrs) to check each denominator before dividing (e.g., compute
denom for each branch: prevValue + (hc/2), currentLow + (lc/2), currentLow +
(lh/2)) and if a denom is zero (or nearly zero) return 0 (or use a small
epsilon) for that branch so atrs is set to 0 instead of producing NaN; ensure
you reference the same variables prevValue, currentLow, hc, lc, lh and keep the
ternary logic or replace it with an explicit if/else that applies these guards.
♻️ Duplicate comments (2)
src/Builder/Evaluation/SeriesEvaluator.cs (2)

60-62: Base series resolution ignores SeriesKey (breaks multi-series graphs).

Base nodes carry a SeriesKey, but resolution always pulls the same input series from _data. This collapses distinct base nodes into a single series in multi-symbol/timeframe graphs. Please pass node.SeriesKey into GetBaseInput(...) and select the correct series based on that key.

Also applies to: 112-116


164-198: Missing output-key mappings for multi-output indicators.

Only MACD and Bollinger are mapped; other multi-output indicators will fall back to CustomValuesList, returning incorrect series for non-primary outputs. Add mappings for Stochastic, Keltner, Donchian, and Ichimoku. Also consider centralizing this mapping with StreamingIndicatorFactory to prevent drift.

🔧 Suggested mapping additions
     if (name == IndicatorName.BollingerBands)
     {
         return output switch
         {
             IndicatorOutput.UpperBand => "UpperBand",
             IndicatorOutput.LowerBand => "LowerBand",
             _ => "MiddleBand"
         };
     }
 
+    if (name == IndicatorName.StochasticOscillator)
+    {
+        return output switch
+        {
+            IndicatorOutput.Signal => "FastD",
+            IndicatorOutput.Histogram => "SlowD",
+            _ => null
+        };
+    }
+
+    if (name == IndicatorName.KeltnerChannels)
+    {
+        return output switch
+        {
+            IndicatorOutput.UpperBand => "UpperBand",
+            IndicatorOutput.MiddleBand => "MiddleBand",
+            IndicatorOutput.LowerBand => "LowerBand",
+            _ => null
+        };
+    }
+
+    if (name == IndicatorName.DonchianChannels)
+    {
+        return output switch
+        {
+            IndicatorOutput.UpperBand => "UpperChannel",
+            IndicatorOutput.MiddleBand => "MiddleChannel",
+            IndicatorOutput.LowerBand => "LowerChannel",
+            _ => null
+        };
+    }
+
+    if (name == IndicatorName.IchimokuCloud)
+    {
+        return output switch
+        {
+            IndicatorOutput.Primary => "TenkanSen",
+            IndicatorOutput.Signal => "KijunSen",
+            IndicatorOutput.UpperBand => "SenkouSpanA",
+            IndicatorOutput.LowerBand => "SenkouSpanB",
+            IndicatorOutput.Histogram => "ChikouSpan",
+            _ => null
+        };
+    }
+
     return null;
 }

Copy link
Copy Markdown

@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

🤖 Fix all issues with AI agents
In `@src/Builder/Buffers/IndicatorBuffer.cs`:
- Around line 191-206: ToListAsync currently reads _array on each loop
iteration, which can become null if the buffer is disposed concurrently; capture
the reference and count at the start (e.g., local variables array = _array and
count = _count after calling ThrowIfDisposed()) and then iterate using those
locals (array[i] and count) so disposal races yield ObjectDisposedException via
ThrowIfDisposed rather than a NullReferenceException; keep the existing
batchSize/Task.Yield behavior and do not access _array directly inside the loop.
- Around line 332-346: The XML doc on GetEnumerator incorrectly claims
"Thread-safe for concurrent reads"; update the summary to remove that guarantee
and clearly state the actual safety model: explain that GetEnumerator captures
_count and _array and will avoid NREs, but enumeration is only safe when the
buffer is not disposed and there are no concurrent writers (Dispose() may return
the array to the pool and subsequent renters can mutate data). Reference the
GetEnumerator method, ThrowIfDisposed call, and the _count/_array fields and
mention Dispose so readers know the exact conditions required for safe
enumeration.

In `@src/Builder/Notifications/NotificationTypes.cs`:
- Around line 505-518: The HttpResponseMessage returned by HttpClient.PostAsync
(assigned to the variable response) is not disposed; update the Discord webhook
send logic where you call HttpClient.PostAsync(webhookUrl, content,
cancellationToken) so that the response is disposed (e.g., use a using statement
or ensure response.Dispose() in a finally) around the block that reads
response.IsSuccessStatusCode and response.Content.ReadAsStringAsync(); keep the
existing payload/content using and logging behavior but ensure the response
object is properly disposed to avoid leaking sockets.

In `@src/Builder/Signals/SignalTypes.cs`:
- Around line 469-475: SignalRangeRule instances added by
SignalRangeEmissionBuilder.Emit via _catalog.AddRangeRule are never evaluated
because IndicatorRuntime construction never calls BuildRangeRules or accepts
range rules; update the runtime initialization to include range rules: ensure
IndicatorRuntime (or the factory that builds it) retrieves
catalog.BuildRangeRules() (or an equivalent list of SignalRangeRule) and stores
them alongside _signals and _groupSignals so they are evaluated during runtime
execution, and modify any constructors or fields to accept and process the
SignalRangeRule collection (referencing SignalRangeEmissionBuilder.Emit,
SignalRangeRule, _catalog.AddRangeRule, BuildRangeRules, and
IndicatorRuntime/_signals/_groupSignals).
♻️ Duplicate comments (9)
src/Builder/Buffers/IndicatorBuffer.cs (1)

408-422: Documentation/implementation mismatch persists (previously flagged).

The docstring states that passing pool: null means the array won't be returned to any pool, but the constructor (line 33) sets _pool = pool ?? SharedPool. This discrepancy can cause pool corruption if a caller passes a non-pooled array with pool: null.

src/Builder/IndicatorRuntime.cs (4)

309-355: StreamingSession disposed immediately after Start() returns—session lifetime issue persists.

The using var session on line 320 will dispose the session when StartStreaming() returns. However, session.Start() (line 354) is non-blocking and only registers callbacks. After disposal, the session's internal resources may be cleaned up, causing callbacks to fail or never fire.

Store the session as an instance field and dispose it in Dispose().

♻️ Proposed fix
+    private StreamingSession? _streamingSession;
+
     private void StartStreaming()
     {
         var stream = _source.StreamSource ?? throw new InvalidOperationException("Streaming source missing stream.");
         var options = _streamingOptions ?? throw new InvalidOperationException("Streaming options missing.");
         var symbols = options.Symbols ?? ToSymbolStrings(_symbols);
         if (symbols.Count == 0)
         {
             throw new InvalidOperationException("Streaming options must include at least one symbol.");
         }

         var values = new Dictionary<SeriesHandle, double[]>(_nodes.Count);
-        using var session = StreamingSession.Create(stream, symbols, options: options);
+        _streamingSession = StreamingSession.Create(stream, symbols, options: options);
         // ... rest of registration logic using _streamingSession ...
-        session.Start();
+        _streamingSession.Start();
     }

In Dispose():

+        _streamingSession?.Dispose();
+        _streamingSession = null;

344-351: Hot-path allocations in streaming update callbacks.

Each streaming update allocates new[] { value } (line 347), a new dictionary copy (line 349), and a new IndicatorSnapshot. This conflicts with the PR's 0-allocation hot-path goal.

Reuse per-handle buffers and consider pooling or reusing snapshot objects.


472-484: Batch mode may emit nothing when EmitWarmup is false.

When !_behavior.EmitWarmup && !_hasEmitted, Publish returns early and sets _hasEmitted = true without setting Latest. In batch mode, there's only one Publish call, so Latest remains null and Updated is never invoked.

♻️ Proposed fix
-private void Publish(IndicatorSnapshot snapshot)
+private void Publish(IndicatorSnapshot snapshot, bool skipWarmupCheck = false)
 {
-    if (!_behavior.EmitWarmup && !_hasEmitted)
+    if (!skipWarmupCheck && !_behavior.EmitWarmup && !_hasEmitted)
     {
         _hasEmitted = true;
         return;
     }

Then in StartBatch(), call Publish(snapshot, skipWarmupCheck: true).


587-617: Notification dispatch uses fire-and-forget async with Console logging only.

The current implementation wraps NotifyAsync in Task.Run with a catch that logs to Console.WriteLine. This addresses error isolation (notifications won't abort the runtime), but:

  1. Trade adapter execution (line 615) is still synchronous and can throw, aborting the loop.
  2. Console.WriteLine may not be appropriate for production—consider a configurable logger.
♻️ Wrap trade adapter execution in try-catch
         for (var i = 0; i < _autoTrading.Rules.Count; i++)
         {
             var ruleConfig = _autoTrading.Rules[i];
             if (!ruleConfig.Signal.Equals(handle))
             {
                 continue;
             }

-            ruleConfig.Adapter.Execute(new TradeRequest(handle, ruleConfig.Action, DateTime.UtcNow));
+            try
+            {
+                ruleConfig.Adapter.Execute(new TradeRequest(handle, ruleConfig.Action, DateTime.UtcNow));
+            }
+            catch (Exception ex)
+            {
+                System.Console.WriteLine($"[AutoTrading] Error executing trade: {ex.Message}");
+            }
         }
src/Builder/Signals/SignalTypes.cs (1)

343-350: RequiredPercent values above 100 can make activation impossible.

As noted in a previous review, if RequiredPercent > 100, the computed threshold exceeds the total condition count, making the group impossible to activate. Clamping to [0, 100] would prevent silent misconfiguration.

♻️ Proposed fix
-    SignalGroupMode.Percent => activeCount >= Math.Ceiling(total * Math.Max(0, RequiredPercent ?? 0) / 100d),
+    SignalGroupMode.Percent => activeCount >= Math.Ceiling(total * Math.Clamp(RequiredPercent ?? 0, 0, 100) / 100d),
src/Builder/Notifications/NotificationTypes.cs (3)

430-442: HttpResponseMessage not disposed in Telegram channel.

Same issue—response should be disposed.

♻️ Proposed fix
-            var response = await HttpClient.PostAsync(url, content, cancellationToken).ConfigureAwait(false);
+            using var response = await HttpClient.PostAsync(url, content, cancellationToken).ConfigureAwait(false);

280-302: FormUrlEncodedContent not disposed in SMS channel.

While HttpRequestMessage is disposed (line 288), FormUrlEncodedContent (line 280) and HttpResponseMessage (line 292) are not. For long-running processes, this can cause resource leaks.

♻️ Proposed fix
-            var content = new System.Net.Http.FormUrlEncodedContent(new Dictionary<string, string>
+            using var content = new System.Net.Http.FormUrlEncodedContent(new Dictionary<string, string>
             {
                 ["To"] = toNumber ?? string.Empty,
                 ["From"] = fromNumber ?? string.Empty,
                 ["Body"] = message
             });
             // ...
-            var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
+            using var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);

366-379: HttpResponseMessage not disposed in Webhook channel.

The response object should be disposed after reading its content.

♻️ Proposed fix
-            var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
+            using var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
🧹 Nitpick comments (8)
src/Builder/Options/SignalOptions.cs (2)

22-28: Consider explicit validation or documenting silent clamping behavior.

The constructor silently clamps bars to a minimum of 1 using Math.Max(1, bars). While this ensures a valid state, it could mask bugs if a caller passes 0 or a negative value expecting an error.

Options:

  1. Document the clamping behavior in the XML doc
  2. Throw ArgumentOutOfRangeException for invalid inputs to fail fast
Option 2: Explicit validation
 public SignalWindow(int bars, TimeSpan? duration = null, int? validityBars = null, TimeSpan? validityDuration = null)
 {
-    Bars = Math.Max(1, bars);
+    ArgumentOutOfRangeException.ThrowIfLessThan(bars, 1);
+    Bars = bars;
     Duration = duration;
     ValidityBars = validityBars;
     ValidityDuration = validityDuration;
 }

66-80: Consider validating non-negative values for time-based factories.

Minutes and Hours accept negative integers, which would create negative TimeSpan values—likely invalid for signal windows.

Add guard clauses
 public static SignalWindow Minutes(int minutes)
 {
+    ArgumentOutOfRangeException.ThrowIfNegative(minutes);
     return new SignalWindow(1, TimeSpan.FromMinutes(minutes), null, null);
 }

 public static SignalWindow Hours(int hours)
 {
+    ArgumentOutOfRangeException.ThrowIfNegative(hours);
     return new SignalWindow(1, TimeSpan.FromHours(hours), null, null);
 }
src/Builder/Buffers/IndicatorBuffer.cs (1)

266-283: Consider guarding against integer overflow in capacity calculations.

When _count is near int.MaxValue, expressions like _count + 1 (line 269) or _count + items.Length (line 280) could overflow, resulting in a negative value passed to EnsureCapacity. While this is an unlikely edge case, for a high-performance library it's worth defensive checking.

🛡️ Optional defensive overflow check
     public void Add(T item)
     {
         ThrowIfDisposed();
+        if (_count == int.MaxValue)
+        {
+            throw new InvalidOperationException("Buffer has reached maximum capacity.");
+        }
         EnsureCapacity(_count + 1);
         _array![_count++] = item;
     }

     public void AddRange(ReadOnlySpan<T> items)
     {
         ThrowIfDisposed();
+        if (items.Length > int.MaxValue - _count)
+        {
+            throw new InvalidOperationException("Adding items would exceed maximum buffer capacity.");
+        }
         EnsureCapacity(_count + items.Length);
         items.CopyTo(_array.AsSpan(_count));
         _count += items.Length;
     }
benchmarks/OoplesFinance.StockIndicators.Benchmarks/BuilderApiBenchmarks.cs (2)

62-72: V2 benchmarks include builder setup overhead—intentional but worth noting.

The V2 benchmarks create IndicatorDataSource, StockIndicatorBuilder, build the runtime, and start it—all within the timed method. This measures total API usage cost rather than pure computation time. If the goal is to compare computation parity, consider pre-building the runtime in setup.

However, if the goal is to compare typical user workflows (which seems to be the case), this is appropriate.


64-72: Return runtime.Latest ?? new object() may hide null results.

If runtime.Latest is null after Start(), returning new object() prevents BenchmarkDotNet from optimizing away the call, but it masks potential issues where batch computation didn't produce results. Consider asserting or logging if Latest is unexpectedly null during benchmark runs.

src/Builder/IndicatorRuntime.cs (1)

240-262: Nested lock acquisition in GetOrCreateBuffer could be simplified.

The method acquires _buffersLock, then within that lock acquires _statsLock twice (lines 246 and 255). While not a deadlock risk (always same order), you could batch the stats update or use a single lock for both to reduce overhead.

src/Builder/Signals/SignalTypes.cs (1)

167-177: Duplicated logic between SignalCondition.IsActive and SignalRule.IsActive.

Both have similar switch expressions for Above and Below triggers. SignalCondition additionally handles Between and Outside, while SignalRule does not. This could lead to maintenance issues if logic changes.

Consider having SignalRule delegate to a SignalCondition internally or extract the common logic to a shared helper.

Also applies to: 248-256

src/Builder/Notifications/NotificationTypes.cs (1)

450-454: Current implementation uses legacy Markdown; if switching to MarkdownV2, more characters require escaping.

The code currently uses parse_mode = "Markdown" (legacy), where escaping _, *, [, and ` is adequate. However, if the code is updated to use MarkdownV2, Telegram's documentation specifies that these additional characters must also be escaped: ], (, ), ~, >, #, +, -, =, |, {, }, ., !.

Consider switching to MarkdownV2 with comprehensive escaping or using HTML parse mode instead, which eliminates markdown escaping complexity.

- FacadeSketchesV2: add symbol validation for subscription list
- FacadeSketchesV6: respect lazy compute policy in streaming
- IndicatorInvoker: detect extra parameters in invoke method
- SeriesEvaluator: add multi-symbol support with dictionary lookup
- SeriesEvaluator: add extensible indicatoroutputregistry for output keys
- IndicatorRuntime: store streaming session as field for proper lifecycle
- IndicatorRuntime: add object pooling for hot-path allocations
- IndicatorRuntime: add separate batch/streaming warmup flags
- IndicatorRuntime: implement range signal evaluation (between/outside)
- IndicatorSnapshot: add separate lazy cache for immutability
- TradingTypes: add using statement for httprequestmessage disposal
- BollingerBandsPart1: fix hardcoded 0.5 threshold to use prevatrdev

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown

@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

🤖 Fix all issues with AI agents
In `@src/Builder/Evaluation/SeriesEvaluator.cs`:
- Around line 213-241: IndicatorOutputRegistry's OutputKeyMap is missing
explicit entries for DonchianChannels and IchimokuCloud (and should include
KeltnerChannels for clarity), causing fallback key name mismatches; update the
dictionary (OutputKeyMap) in the IndicatorOutputRegistry class to add mappings
for (IndicatorName.DonchianChannels, IndicatorOutput.UpperBand) ->
"UpperChannel" and (IndicatorName.DonchianChannels, IndicatorOutput.LowerBand)
-> "LowerChannel", add entries for IchimokuCloud mapping its outputs to
"TenkanSen", "KijunSen", "SenkouSpanA", "SenkouSpanB" using the appropriate
IndicatorOutput enum values, and also add explicit
(IndicatorName.KeltnerChannels,
IndicatorOutput.{UpperBand,MiddleBand,LowerBand}) ->
"UpperBand"/"MiddleBand"/"LowerBand" so the lookup in
IndicatorOutputRegistry.OutputKeyMap returns correct keys instead of relying on
fallback logic.

In `@src/Builder/IndicatorDataSource.cs`:
- Around line 162-195: MergeStockData currently concatenates per-tick values
into separate lists without guaranteeing chronological order; update
MergeStockData to collect the per-tick items as a single list of records (use
the existing tick variable from data.TickerDataList or create tuples of (Date,
Open, High, Low, Close, Volume)), then sort that combined list by Date (oldest →
newest) and finally build the opens, highs, lows, closes, volumes and dates
lists in that sorted order before calling the StockData constructor (refer to
MergeStockData, TickerDataList and the StockData(opens, highs, lows, closes,
volumes, dates) signature).

In `@src/Builder/Notifications/NotificationTypes.cs`:
- Around line 403-448: The NotifyAsync method currently awaits
HttpClient.PostAsync into the local variable response but never disposes the
HttpResponseMessage; change the call to use a using declaration or using block
(e.g. using var response = await HttpClient.PostAsync(url, content,
cancellationToken).ConfigureAwait(false)) so the HttpResponseMessage created by
PostAsync is disposed after checking IsSuccessStatusCode and reading
response.Content; update the existing response references accordingly inside
NotifyAsync.

In `@src/Builder/Specs/IndicatorSpec.cs`:
- Around line 183-191: The constructor BollingerBandsSpecOptions currently
clamps Length but does not validate StdDevMult; add validation in the
BollingerBandsSpecOptions(int length, double stdDevMult) constructor to ensure
StdDevMult is positive (e.g., throw ArgumentOutOfRangeException for stdDevMult
<= 0, or clamp using StdDevMult = Math.Max(minPositive, stdDevMult) with a small
minPositive like double.Epsilon or 0.0001) so that the StdDevMult property
cannot be zero or negative and invalid band calculations are prevented.

In `@src/Builder/StockIndicatorBuilder.cs`:
- Around line 418-432: The code is mutating the caller-owned
_dataOptions.StreamingOptions (via the local variable custom) when setting
Symbols; to fix, avoid mutating that external object by producing and returning
a new StreamingOptions instance instead of modifying custom: when
_dataOptions?.StreamingOptions != null, create a shallow copy (new
StreamingOptions and copy the relevant properties from
_dataOptions.StreamingOptions), set the copy.Symbols = ToSymbolStrings(symbols)
if null, and return the copy; similarly keep the existing behavior for
providerOptions (which already returns a new instance). Reference: _dataOptions,
StreamingOptions, custom, ToSymbolStrings,
_source.ProviderDefaults.CreateStreamingOptions, providerOptions.
♻️ Duplicate comments (14)
MIGRATION.md (1)

52-80: Fix handle scoping in migration snippets.

The examples use handles outside the lambdas where they’re created, so they won’t compile. Capture the handles in outer scope before using them later in GetSeries / ConfigureSignals.

🔧 Suggested fix
+SeriesHandle smaHandle = default;
+SeriesHandle rsiHandle = default;
 var builder = new StockIndicatorBuilder(source)
     .ConfigureIndicators(indicators =>
     {
-        var sma = indicators.Sma(20);
-        var rsi = indicators.Rsi(14);
+        smaHandle = indicators.Sma(20);
+        rsiHandle = indicators.Rsi(14);
     });

 using var runtime = builder.Build();
 var smaBuffer = runtime.GetSeries(smaHandle);
+SeriesHandle sma = default;
 var builder = new StockIndicatorBuilder(source)
-    .ConfigureIndicators(i => i.Sma(20))
-    .ConfigureSignals(s => s.When(sma).CrossesAbove(100).Emit());
+    .ConfigureIndicators(i => { sma = i.Sma(20); })
+    .ConfigureSignals(s => s.When(sma).CrossesAbove(100).Emit());

Also applies to: 191-198

README.md (2)

271-285: Add language identifiers to benchmark command blocks (MD040).

markdownlint still flags the benchmark command fences without a language.

🔧 Suggested fix
-```
+```bash
 dotnet run --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -c Release

@@
- +bash
dotnet run --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -c Release -- --streaming-perf --ticks 100000

@@
-```
+```bash
.\benchmarks\setup-baseline.ps1 -Ref master
dotnet build -c Release -p:AssemblyName=OoplesFinance.StockIndicators.Original -p:TargetFramework=net10.0 benchmarks/.baseline/src/OoplesFinance.StockIndicators.csproj
dotnet run --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -c Release -- --filter *IndicatorBenchmarks*

225-265: Surround performance/streaming tables with blank lines (MD058).

Several tables still lack a blank line before/after the table block.

🔧 Suggested fix
 Length = 50
+
 | Indicator | Optimized (us) | Baseline (us) | Speedup |
@@
 | Ulcer Index | 2756.0 | 14288.4 | 5.2x |
+
 Length = 200
+
 | Indicator | Optimized (us) | Baseline (us) | Speedup |
@@
 | Ulcer Index | 2749.9 | 72270.8 | 26.3x |
+
 <!-- PERF_TABLES_END -->
 
 Streaming fanout (100k ticks, 5 timeframes):
+
 | Scenario | Indicators | Outputs | Throughput (ticks/sec) | p95 latency (ms) | Updates |
@@
 | Extended | 15 | off | 140,680 | 0.01 | 9,001,785 |
+
 Streaming scaling (100k ticks, outputs off):
+
 | Timeframes | Indicators | Throughput (ticks/sec) | p95 latency (ms) | Updates |
@@
 | 5 | 15 | 140,680 | 0.01 | 9,001,785 |
+
 Full reports: `BenchmarkDotNet.Artifacts/results/OoplesFinance.StockIndicators.Benchmarks.IndicatorBenchmarks-report-github.md`
examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV6.cs (1)

1611-1644: Avoid logging notification endpoints and recipients.

The demo logs email addresses, phone numbers, and webhook URLs verbatim, which risks accidental PII/secret leakage if real configs are used. Redact or omit those fields.

🔧 Suggested fix
     public void Notify(NotificationEvent notification)
     {
-        Console.WriteLine($"Email: {_options.To} {notification.Name} value={notification.Value:F4}");
+        Console.WriteLine($"Email notification sent for {notification.Name} value={notification.Value:F4}");
     }
@@
     public void Notify(NotificationEvent notification)
     {
-        Console.WriteLine($"Sms: {_options.Number} {notification.Name} value={notification.Value:F4}");
+        Console.WriteLine($"Sms notification sent for {notification.Name} value={notification.Value:F4}");
     }
@@
     public void Notify(NotificationEvent notification)
     {
-        Console.WriteLine($"Webhook: {_options.Url} {notification.Name} value={notification.Value:F4}");
+        Console.WriteLine($"Webhook notification sent for {notification.Name} value={notification.Value:F4}");
     }
examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV4.cs (1)

747-780: Avoid logging notification endpoints and recipients.

Even in demo code, logging email addresses/phone numbers/webhook URLs encourages unsafe patterns and can leak real PII. Please redact or omit these fields.

🔧 Suggested fix
     public void Notify(NotificationEvent notification)
     {
-        Console.WriteLine($"Email: {_options.To} {notification.Name} value={notification.Value:F4}");
+        Console.WriteLine($"Email notification sent for {notification.Name} value={notification.Value:F4}");
     }
@@
     public void Notify(NotificationEvent notification)
     {
-        Console.WriteLine($"Sms: {_options.Number} {notification.Name} value={notification.Value:F4}");
+        Console.WriteLine($"Sms notification sent for {notification.Name} value={notification.Value:F4}");
     }
@@
     public void Notify(NotificationEvent notification)
     {
-        Console.WriteLine($"Webhook: {_options.Url} {notification.Name} value={notification.Value:F4}");
+        Console.WriteLine($"Webhook notification sent for {notification.Name} value={notification.Value:F4}");
     }
src/Builder/Buffers/IndicatorBuffer.cs (1)

329-341: Prevent returning non-pooled arrays to the pool during resize.

If WrapPooledArray is used with pool: null, EnsureCapacity can still return the original (non-pooled) array to SharedPool, corrupting the pool. Guard the return with _ownsArray and mark the new rented array as owned.

🔧 Suggested fix
         if (_array is null || _array.Length < capacity)
         {
             var newArray = _pool.Rent(capacity);
             if (_array is not null && _count > 0)
             {
                 Array.Copy(_array, newArray, _count);
-                ReturnArrayToPool(_array);
+                if (_ownsArray)
+                {
+                    ReturnArrayToPool(_array);
+                }
             }
             _array = newArray;
+            _ownsArray = true;
         }
examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV5.cs (1)

781-824: Demo adapters still log potentially sensitive information.

While the AlpacaTradeAdapter now masks the API key (line 988), the notification channels still log:

  • Email address at line 792
  • Phone number at line 807
  • Webhook URL at line 822

Even for demo code, this sets a bad precedent. Per the past review comment, consider redacting these fields.

src/Builder/Notifications/NotificationTypes.cs (2)

336-384: HttpResponseMessage not disposed in Webhook channel.

Similar to SMS, the response at line 369 should be disposed.

🐛 Proposed fix
-            var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
+            using var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);

275-307: HttpResponseMessage and FormUrlEncodedContent not disposed in SMS channel.

The content variable (line 280) and response variable (line 292) are not disposed, which can leak resources in long-running processes. The past review flagged this, but only HttpRequestMessage has using.

🐛 Proposed fix
-            var content = new System.Net.Http.FormUrlEncodedContent(new Dictionary<string, string>
+            using var content = new System.Net.Http.FormUrlEncodedContent(new Dictionary<string, string>
             {
                 ["To"] = toNumber ?? string.Empty,
                 ["From"] = fromNumber ?? string.Empty,
                 ["Body"] = message
             });
...
-            var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
+            using var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
src/Builder/Specs/IndicatorSpec.cs (1)

19-25: Null guard for options is now in place.

The constructor now validates options with ArgumentNullException, addressing the previous review feedback.

src/Builder/StockIndicatorBuilder.cs (1)

74-81: Defaults flag reset is now in place.

The _defaultsApplied = false assignment on line 77 ensures defaults are properly reapplied when ConfigureIndicators is called with new options.

src/Builder/Signals/SignalTypes.cs (2)

343-351: Percent threshold clamping is now in place.

Line 348 properly clamps RequiredPercent to [0, 100] range using Math.Min(100, Math.Max(0, RequiredPercent ?? 0)).


469-475: Range rule evaluation is now wired up.

SignalRangeRule objects are now passed to IndicatorRuntime via _signals.BuildRangeRules() in StockIndicatorBuilder.Build() (line 172 in StockIndicatorBuilder.cs).

src/Builder/Catalogs/IndicatorCatalog.cs (1)

404-414: Division by zero guard is now in place.

Line 411 properly guards against division by zero by returning double.NaN when y == 0.

🧹 Nitpick comments (5)
src/Builder/IndicatorDataSource.cs (1)

136-157: Consider validating empty/whitespace symbol in For(string) overload.

The For(string symbol) method passes the string directly to SymbolId constructor. Based on the relevant snippet showing SymbolId converts null to empty string, an empty or whitespace-only symbol would be silently accepted.

♻️ Suggested guard
 public IndicatorDataSource For(string symbol, BarTimeframe? timeframe = null)
 {
+    if (string.IsNullOrWhiteSpace(symbol))
+    {
+        throw new ArgumentException("Symbol cannot be null or empty.", nameof(symbol));
+    }
     return For(new SymbolId(symbol), timeframe);
 }
src/Builder/IndicatorRuntime.cs (1)

680-693: Fire-and-forget notification errors are logged but not surfaced.

While the try/catch prevents crashes, errors are only written to Console.WriteLine. For production use, consider:

  1. Exposing an error event/callback
  2. Using a proper logging abstraction
  3. Optionally collecting errors for inspection

This is acceptable for the current scope but worth noting for future enhancement.

src/Builder/Specs/IndicatorSpec.cs (1)

274-281: Consider logging conversion failures in GetParameter<T>.

The empty catch block silently swallows conversion exceptions. While this is acceptable for a best-effort utility, failed conversions might indicate configuration errors that users would want to know about. Consider adding optional diagnostic logging or a separate TryGetParameter variant that returns a success flag.

src/Builder/StockIndicatorBuilder.cs (1)

379-409: Consider iterative approach for AddDependencies to avoid stack overflow on deep graphs.

The recursive implementation works for typical indicator chains but could overflow on deeply nested graphs. An explicit stack would be more resilient.

♻️ Iterative alternative
 private void AddDependencies(SeriesHandle handle, HashSet<SeriesHandle> active)
 {
-    if (!_nodes.TryGetValue(handle, out var node))
-    {
-        return;
-    }
-
-    switch (node.Kind)
-    {
-        case SeriesNodeKind.Indicator:
-            if (node.Input.HasValue)
-            {
-                active.Add(node.Input.Value);
-                AddDependencies(node.Input.Value, active);
-            }
-            break;
-        case SeriesNodeKind.Formula:
-            if (node.Left.HasValue)
-            {
-                active.Add(node.Left.Value);
-                AddDependencies(node.Left.Value, active);
-            }
-
-            if (node.Right.HasValue)
-            {
-                active.Add(node.Right.Value);
-                AddDependencies(node.Right.Value, active);
-            }
-            break;
-    }
+    var stack = new Stack<SeriesHandle>();
+    stack.Push(handle);
+
+    while (stack.Count > 0)
+    {
+        var current = stack.Pop();
+        if (!_nodes.TryGetValue(current, out var node))
+        {
+            continue;
+        }
+
+        switch (node.Kind)
+        {
+            case SeriesNodeKind.Indicator:
+                if (node.Input.HasValue && active.Add(node.Input.Value))
+                {
+                    stack.Push(node.Input.Value);
+                }
+                break;
+            case SeriesNodeKind.Formula:
+                if (node.Left.HasValue && active.Add(node.Left.Value))
+                {
+                    stack.Push(node.Left.Value);
+                }
+                if (node.Right.HasValue && active.Add(node.Right.Value))
+                {
+                    stack.Push(node.Right.Value);
+                }
+                break;
+        }
+    }
 }
src/Builder/Catalogs/IndicatorCatalog.cs (1)

368-379: Consider adding key parameters to Ichimoku method.

Unlike Macd and BollingerBands, the Ichimoku method doesn't accept IndicatorKey parameters for its five outputs, limiting users' ability to reference specific components in signals.

♻️ Proposed signature enhancement
 public IchimokuResult Ichimoku(int tenkanLength = 9, int kijunLength = 26, int senkouBLength = 52,
-    SeriesHandle? input = null)
+    SeriesHandle? input = null,
+    IndicatorKey? tenkanKey = null, IndicatorKey? kijunKey = null,
+    IndicatorKey? senkouAKey = null, IndicatorKey? senkouBKey = null, IndicatorKey? chikouKey = null)
 {
     var series = input ?? Price();
     var seriesKey = _builder.ResolveSeriesKey(series);
     var opts = new GenericIndicatorOptions(new object[] { tenkanLength, kijunLength, senkouBLength });
-    var tenkanSen = _builder.AddIndicator(IndicatorSpecs.Create(IndicatorName.IchimokuCloud, opts, IndicatorOutput.Primary), series, seriesKey, null);
+    var tenkanSen = _builder.AddIndicator(IndicatorSpecs.Create(IndicatorName.IchimokuCloud, opts, IndicatorOutput.Primary), series, seriesKey, tenkanKey);
     // ... similar for other outputs
 }

- SeriesEvaluator: add donchianchannels, keltnerchannels, ichimokucloud output key mappings
- IndicatorDataSource: sort merged data chronologically (critical fix)
- NotificationTypes: add httpresponsemessage disposal in telegram channel
- NotificationTypes: force enablessl=true for smtp security (sonarqube)
- IndicatorSpec: validate stddevmult to prevent invalid bollinger bands
- StockIndicatorBuilder: clone streamingoptions to avoid mutating caller's object

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown

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

🤖 Fix all issues with AI agents
In `@src/Builder/Notifications/NotificationTypes.cs`:
- Around line 192-241: The NotifyAsync method in EmailNotificationChannel
currently hardcodes client.EnableSsl = true and ignores the EmailOptions.UseSsl
setting; update NotifyAsync to respect EmailOptions.UseSsl (defaulting to true
when null/unset) by assigning client.EnableSsl from the option (e.g. use
_options.UseSsl ?? true) before sending, so EnableSsl reflects the configured
value and the documented behavior of EmailOptions.UseSsl is honored.

In `@src/Builder/StockIndicatorBuilder.cs`:
- Around line 54-69: The cached _defaultSeriesKey is not cleared when symbol or
timeframe options change, causing ResolveDefaultSeriesKey to return a stale key;
update ConfigureSymbols and ConfigureData to also reset _defaultSeriesKey to
null (in addition to the existing resets of _resolvedSymbols and
_resolvedTimeframe) so the next ResolveDefaultSeriesKey call recomputes the key
based on the new _symbolOptions/_dataOptions.
♻️ Duplicate comments (3)
src/Builder/Evaluation/SeriesEvaluator.cs (1)

213-257: Verify Stochastic/Ichimoku output key mappings.
The registry maps Stochastic Signal to SignalFastK and omits any Ichimoku mapping for a Chikou/Histogram output. If the calculation keys differ, extraction will fall back to CustomValuesList and return the wrong series. Please confirm the actual OutputValues keys and add missing mappings if needed.

#!/bin/bash
# Verify OutputValues keys for Stochastic and Ichimoku
rg -n "SetOutputValues|OutputValues\\[" src/Calculations -g '*.cs' -C2 | rg -n "Stochastic|Ichimoku"

# Check whether IndicatorOutput.Histogram is used with Ichimoku/Stochastic in builder/catalogs
rg -n "Ichimoku|Stochastic" src/Builder -g '*.cs' -C2 | rg -n "IndicatorOutput\\.Histogram|IndicatorOutput\\.Signal"
src/Builder/Notifications/NotificationTypes.cs (2)

247-307: Dispose HTTP content/response in SMS channel.
FormUrlEncodedContent and HttpResponseMessage aren’t disposed, which can leak sockets in long‑running notification flows.

🔧 Proposed fix
-            var content = new System.Net.Http.FormUrlEncodedContent(new Dictionary<string, string>
+            using var content = new System.Net.Http.FormUrlEncodedContent(new Dictionary<string, string>
             {
                 ["To"] = toNumber ?? string.Empty,
                 ["From"] = fromNumber ?? string.Empty,
                 ["Body"] = message
             });
@@
-            var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
+            using var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);

313-385: Dispose HTTP content/response in Webhook channel.
StringContent and HttpResponseMessage aren’t disposed.

🔧 Proposed fix
-            request.Content = new System.Net.Http.StringContent(payload, System.Text.Encoding.UTF8, contentType);
+            using var content = new System.Net.Http.StringContent(payload, System.Text.Encoding.UTF8, contentType);
+            request.Content = content;
 
-            var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
+            using var response = await HttpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
🧹 Nitpick comments (1)
src/Builder/Specs/IndicatorSpec.cs (1)

239-294: Consider defensively copying parameters.
GenericIndicatorOptions stores the caller’s array by reference, so later mutations can silently affect specs. A shallow copy keeps the options immutable.

♻️ Proposed tweak
-    public GenericIndicatorOptions(object[] parameters)
-    {
-        Parameters = parameters ?? Array.Empty<object>();
-    }
+    public GenericIndicatorOptions(object[] parameters)
+    {
+        if (parameters is null)
+        {
+            Parameters = Array.Empty<object>();
+            return;
+        }
+
+        var copy = new object[parameters.Length];
+        Array.Copy(parameters, copy, parameters.Length);
+        Parameters = copy;
+    }

ooples and others added 3 commits January 22, 2026 12:26
- NotificationTypes: wire emailoptions.usessl into enablessl (default true)
- StockIndicatorBuilder: reset _defaultserieskey when symbol/timeframe changes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- builds library on ubuntu and windows
- runs 708 unit tests on both platforms
- uploads test results as artifacts
- excludes benchmark project (missing baseline reference)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- builds library for both target frameworks to catch api differences
- runs tests on net10.0 (sufficient coverage)
- uses windows runner for net461 compatibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Copy Markdown

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

🤖 Fix all issues with AI agents
In @.github/workflows/ci.yml:
- Around line 31-35: Update the "Build and run tests" step to specify the test
framework and optionally skip rebuilding: add a --framework <TFM> (e.g., net6.0)
and consider --no-build if you rely on the prior restore/build, and then add a
step after testing to upload test results as an artifact (use the test runner's
--logger "trx;LogFileName=TestResults.trx" or similar to generate results and
reference that file in an upload-artifact step). Target the existing steps named
"Restore test dependencies" and "Build and run tests" so the CI runs tests for
the intended framework and preserves results for inspection.
- Around line 17-20: The workflow step "Setup .NET 10" using
actions/setup-dotnet@v4 should use the documented version syntax: change the
dotnet-version key value from "10.0.x" to "10.x" in the step that sets up .NET
(look for the step with name "Setup .NET 10" and the dotnet-version field) so
the action matches supported examples and resolves any version parsing
inconsistencies.
🧹 Nitpick comments (2)
.github/workflows/ci.yml (2)

9-11: Consider adding a build matrix for cross-platform coverage.

The PR objectives mention testing on both Ubuntu and Windows. Currently, only Windows is used. While windows-latest is required for net461, you could add a matrix to also test net10.0 on Ubuntu for broader coverage and faster parallel execution.

♻️ Suggested matrix configuration
 jobs:
   build-and-test:
-    runs-on: windows-latest
+    strategy:
+      matrix:
+        include:
+          - os: ubuntu-latest
+            framework: net10.0
+          - os: windows-latest
+            framework: net10.0
+          - os: windows-latest
+            framework: net461
+    runs-on: ${{ matrix.os }}

Then parameterize the build step to use ${{ matrix.framework }}.


9-15: Consider adding NuGet caching and concurrency control.

Adding NuGet package caching can significantly speed up CI runs. Concurrency control cancels in-progress runs when new commits are pushed to the same PR/branch.

♻️ Suggested enhancements
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
+
 jobs:
   build-and-test:
     runs-on: windows-latest

     steps:
       - name: Checkout repository
         uses: actions/checkout@v4

       - name: Setup .NET 10
         uses: actions/setup-dotnet@v4
         with:
           dotnet-version: 10.0.x
+          cache: true
+          cache-dependency-path: '**/packages.lock.json'

Note: For cache: true to work, you need lock files. Alternatively, use actions/cache with the NuGet packages path.

ooples and others added 5 commits January 22, 2026 13:36
- use 10.x instead of 10.0.x for dotnet-version
- add --framework net10.0 to test command for explicit targeting
- add test results artifact upload step

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add v1 vs v2 performance benchmarks for streaming, chaining, and
  large dataset scenarios in BuilderApiBenchmarks.cs
- Suppress obsolete warnings in IndicatorBenchmarks.cs
- Convert DevConsole Program.cs to use v2.0 Builder API exclusively
- Remove deprecated Calculate* method usage from examples
- Create interactive playground with comprehensive v2.0 demos

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Changed default IndicatorSelection from All() to None() to prevent
automatic computation of core indicators (SMA, RSI, MACD, BollingerBands)
when user only configures specific indicators.

Before this fix, configuring a single SMA(14) would also compute 8
additional indicator series automatically, causing:
- 30x slower execution (9.3ms vs 311us)
- 100x more memory allocation (12.5MB vs 120KB)

After this fix:
- Single indicator: 2x slower (663us vs 324us) - acceptable builder overhead
- Memory: 9x more (1.1MB vs 117KB) - from SeriesEvaluator cloning

Also added diagnostic benchmarks to profile v2.0 Builder step-by-step:
- Diag_1_CreateDataSource (trivial)
- Diag_2_CreateBuilder (trivial)
- Diag_3_ConfigureIndicators (trivial)
- Diag_4_Build (trivial)
- Diag_5_BuildAndStart (the actual computation)

Users who want the automatic core indicators can opt-in with:
IndicatorOptions { Selection = IndicatorSelection.Core() }

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- add fast path for base input series to skip stockdata cloning
- cache default input array to avoid repeated toarray() calls
- reuse hashset for cycle detection instead of allocating per evaluate
- combined with indicatorselection.none() default, reduces v2 overhead
  from 30x slower to only 3.4% slower than v1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- signalcatalog: return array.empty when no signals configured
- notificationcatalog: return array.empty when no channels configured
- autotradingcatalog: return singleton empty configuration when unused
- indicatorruntime: use array.empty for zero-length signal state arrays
- add autotradingconfiguration.empty singleton

these optimizations avoid unnecessary list/array allocations in the
common case where signals, notifications, and auto-trading are not used

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
ooples and others added 28 commits January 26, 2026 10:30
Phase 2 of validation plan:
- docs/VALIDATION.md: Status table for all 716 indicators
- ValidationAttributes.cs: [Validated], [NeedsReview], [HasVariants],
  [IndicatorBounds], [IndicatorCategory] attributes for indicator classes

This enables tracking validation progress and documenting indicator variants.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 3 of validation plan:
- Document Wilder vs Standard EMA smoothing variants
- RSI, ATR, ADX use Wilder smoothing (original)
- MACD uses Standard EMA (traditional)
- Add validation attributes to RSI, ATR, MACD as examples
- Update VALIDATION.md with variant documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add reference tests for 30+ indicators including:
  - Moving averages: SMA, EMA, WMA, DEMA, TEMA, TMA, HMA, KAMA
  - Oscillators: RSI, Stochastic, CCI, Williams %R, ROC, Momentum
  - Additional oscillators: Ultimate, Trix, Aroon, DPO, TSI, PPO, APO
  - Trend: MACD, Parabolic SAR, SuperTrend, Vortex
  - Volatility: ATR, Bollinger, StdDev, Keltner Channels
  - Volume: OBV, CMF, MFI, ADL, Force Index
  - Channels: Donchian
- Fix method signatures for catalog API (Trix, AroonOscillator, Tsi, Sar)
- Fix test data length for warmup period requirements (150 bars)
- All 1054 tests pass

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added 24 new reference test methods for additional indicators:
- Awesome Oscillator - validates oscillation around zero
- Fisher Transform - verifies bounded output
- Coppock Curve - validates trend-following properties
- Balance of Power - verifies bounds [-1, 1]
- PVT, NVI, PVI - validates volume trend indicators
- EMV (Ease of Movement) - verifies oscillation
- VHF (Vertical Horizontal Filter) - validates trend detection
- LSMA, ALMA, T3 - moving average variants with warmup handling
- McGinley Dynamic - adaptive moving average validation
- VWMA, VWAP - volume-weighted indicators (direct state test)
- Stochastic RSI - verifies bounded [0, 100] output
- Schaff Trend Cycle - validates cycle indicator bounds

Fixed issues:
- VWMA test uses direct state testing for better isolation
- LSMA test skips warmup period for stable assertions

96 reference tests now pass (38 original + 24 new methods).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added 24 more reference test methods (total 86 methods now):
- CMO - Chande Momentum Oscillator bounds [-100, 100]
- Bull/Bear Power - Elder Ray indicators validation
- Linear Regression - trend line fitting
- PVO - Percentage Volume Oscillator
- RVI - Relative Vigor Index
- Mass Index - trend reversal detection
- KVO - Klinger Volume Oscillator
- Acceleration Oscillator - AO derivative
- Ulcer Index - downside volatility measure
- PFE - Polarized Fractal Efficiency
- Connors RSI - composite RSI bounds [0, 100]
- KST - Know Sure Thing momentum
- Historical Volatility - annualized std dev
- Chaikin Volatility - range spread ROC

All 120 reference tests pass covering 75+ indicators.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive golden file validation tests for core indicators:
- Moving Averages (12): SMA, EMA, WMA, HMA, DEMA, TEMA, TMA, ZLEMA, KAMA, VIDYA
- Oscillators (15): RSI, Stochastic, Williams %R, CCI, MFI, CMO, UO, Aroon, DPO, APO, PPO, ROC, Momentum, TRIX, Choppiness
- Trend (6): MACD, ADX, SAR, VHF, Donchian, Keltner
- Volatility (7): ATR, TR, Bollinger, StdDev, HV, Ulcer, Chaikin Vol
- Volume (9): OBV, CMF, Force Index, TSI, VWAP, ADL, PVT, NVI

Each test includes:
- Fixed reproducible test data (30 bars OHLCV)
- Reference formula citations (Wilder, Appel, Lane, etc.)
- Hand-calculated expected values where applicable
- Property verification (bounds, NaN checks)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add golden file validation tests for 40 more indicators:
- Trend: Directional Trend Index, DMI Stochastic, Elder Ray Index, Natural Directional Index
- Volume: Klinger Volume Oscillator, Trade Volume Index, TFS Volume Oscillator, Price Volume Oscillator
- Smoothing: Tillson T3, McGinley Dynamic, JMA, Kaufman Adaptive Bands, VAMA, Bryant AMA
- Ehlers: MAMA, SAMI, Median Adaptive Filter, Adaptive RSI V2, Adaptive Stochastic V2, Adaptive Laguerre Filter

All 89 golden file tests pass. Total test count: 2145.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add golden file validation tests for 28 more indicators across categories:
- Regression: Linear Regression, Linear Regression Line, Chande Kroll R-Squared
- Chande: Composite Momentum, Intraday Momentum, Quick Stick, Trend Score, Chandelier Exit
- Kase: Peak Oscillator V1, Convergence Divergence, Serial Dependency Index
- Hurst: Hurst Coefficient, Hurst Bands
- Range: Gopalakrishnan Range Index, The Range Indicator, RAVI
- Swing: Accumulative Swing Index, Gann Swing Oscillator
- Pivot Points: Standard, Camarilla, Fibonacci, Woodie, Floor, DeMark
- Price Patterns: Ichimoku Cloud, Williams Fractals, Fractal Chaos Bands/Oscillator

All 117 golden file tests pass. Total test count: 2173.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added golden file tests for:
- Williams/Bill Williams indicators: Williams AD, Alligator Index, Gator
  Oscillator, Welles Wilder MA, Enhanced Williams %R
- Market indicators: Market Facilitation Index, Mass Index, Elder Market
  Thermometer, Elder Safe Zone Stops, Schaff Trend Cycle, Mass Thrust
- Statistical indicators: Kirshenbaum Bands, STARC, JRC Fractal Dimension,
  Interquartile Range Bands
- Demark indicators: Demark Range Expansion Index
- Accumulation/Distribution: Volume Accumulation Oscillator, Volume
  Accumulation Percent

Renamed StandardErrorBands to KirshenbaumBands (correct indicator name).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added golden file tests for:
- Adaptive indicators: Adaptive EMA, Adaptive Least Squares, Adaptive
  Price Zone, Adaptive Trailing Stop
- Auto/Autonomous: Auto Line, Auto Line With Drift, Autonomous Recursive MA
- Volatility stops: ATR Trailing Stops, Parabolic SAR
- Bollinger Band variants: %B, Band Width
- Momentum: Anchored Momentum, Decision Point PMO
- Oscillators: Bayesian Oscillator, Bilateral Stochastic, Chop Zone
- Moving averages: Ahrens MA, Buff Average, Corrected MA, Cubed WMA,
  Delta MA
- Ratio indicators: Calmar Ratio, Chartmill Value
- Other: Constance Brown Composite Index, Coral Trend

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added golden file tests for:
- RSI variants: Absolute Strength Index, Asymmetrical RSI, Breakout RSI,
  CCT Stochastic RSI
- Filter indicators: Auto Filter, ATR Filtered EMA, Narrow Bandpass Filter
- Dispersion: Auto Dispersion Bands, MAD Bands, Closed Form Distance Vol
- Damping: Damped Sine Wave Filter, Damping Index
- Money flow: Average Money Flow Oscillator, Better Volume Indicator
- Other: Center of Linearity, Belkhayate Timing, Compound Ratio MA,
  Commodity Selection Index, Contract High Low, Average Price,
  Daily Average Price Delta, D-Envelope, Conditional Accumulator,
  Confluence Indicator

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add golden file tests for:
- DiNapoli Indicators: MACD, PPO, Preferred Stochastic
- Double Smoothed: Momenta, RSI, Stochastic, Double Stochastic,
  Exponential Smoothing
- Dynamic: Momentum Index, Momentum Oscillator, S/R,
  Adjustable Filter, Adjustable MA
- DeMark: Demarker, Pressure Ratio V1, Reversal Points, Setup
- Derivative/Disparity: Derivative Oscillator, Disparity Index,
  Distance Weighted MA
- Detrended Synthetic Price
- Didi/DT: Didi Index, DT Oscillator
- Donchian Channel Width
- Ease of Movement
- Edge/Efficient: Edge Preserving Filter, Efficient Auto Line,
  Efficient Price, Efficient Trend Step Channel
- Demand Oscillator, Drunkard Walk, DEMA 2 Lines

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add golden file tests for E-H indicators:
- Ehlers Advanced: 2-Pole Butterworth, 3-Pole Smoother, Adaptive Band
  Pass, Adaptive CCI, AM Detector, Anticipate, Auto Correlation,
  Decycler Oscillator, Deviation Scaled MA, DC Tuned Bypass, Early
  Onset Trend, Even Better Sine Wave, Gaussian Filter, Hilbert
  Oscillator, Impulse Reaction, Market State, Noise Elimination,
  Reflex, Rocket RSI, Spearman Rank, Trendflex, Universal Oscillator
- EMA Wave Indicator
- Ergodic: MACD, TSI V1
- F Indicators: Falling Rising Filter, Fast/Slow Kurtosis, Fast Z
  Score, Fear And Greed, Finite Volume Elements, Firefly Oscillator,
  Folded RSI, Freedom of Movement
- G Indicators: Gann Hi Lo Activator, Gann Trend Oscillator, Garman
  Klass Volatility, Grand Trend Forecasting, Grover Llorens
  Activator, Guppy MMA
- H Indicators: Half Trend, Hampel Filter, Hawkeye Volume, Henderson
  WMA, Herrick Payoff Index, High Low Bands, High Low Index, HV
  Percentile, Holt EMA, Hurst Cycle Channel

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
New tests for:
- I indicators: Impulse MACD, Inertia, Insync Index, Internal Bar Strength,
  Interquartile Range Bands, Inverse Distance WMA, Inverse Fisher Z Score
- J indicators: Japanese Correlation, JMA RSX Clone, JSA MA
- K indicators: Kalman Smoother, Karobein Oscillator, Kase Dev Stop V1,
  Kase Indicator, Kaufman Adaptive Correlation, Kaufman Binary Wave,
  Kendall Rank Correlation, Kurtosis, Kwan Indicator
- L indicators: Leo MA, Light LSMA, Linda Raschke 3-10, Linear Channels,
  Linear Extrapolation, LQCD Oscillator, Linear Trailing Stop, Liquid RSI,
  Logistic Correlation

Each test validates the indicator produces finite values on standard test data.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
New tests for:
- M indicators: MacZ, MacZ VWAP, Market Direction, Market Meanness, Martin Ratio,
  Mass Thrust Oscillator, Mayer Multiple, McClellan Oscillator, McNicholl MA,
  Mean Absolute Error Bands, Median Price, Middle High Low MA, Midpoint,
  Midpoint Oscillator, Midprice, Mirrored MACD, Mirrored PPO, Mobility Oscillator,
  Modified Gann Hilo, Modified PVT, Modular Filter, Momenta RSI, Morphed Sine Wave,
  Motion Smoothness, Motion To Attraction Channels/Trailing Stop, Move Tracker,
  MA Adaptive Filter, MA Adaptive Q, MA Band Width, MA Bands, MA Channel,
  MACD Leader, MA Displaced Envelope, MA Envelope, MA Support Resistance,
  Multi Depth ZLEMA, Multi Level Indicator, Multi Vote OBV
- N indicators: Narrow Sideways Channel, Natural Directional Combo, Natural Market
  Combo/Mirror/River/Slope, Natural MA, Natural Stochastic, Negative Volume Disparity,
  Nick Rypock Trailing Reverse, Normalized RVI, Nth Order Differencing Oscillator

Each test validates the indicator produces finite values on standard test data.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add golden file validation tests for indicators starting with O, P, Q:
- O: OCHistogram, OceanIndicator, OmegaRatio, OnBalanceVolumeDisparityIndicator,
     OnBalanceVolumeModified, OnBalanceVolumeReflex, OptimalWeightedMovingAverage,
     OptimizedTrendTracker, OscOscillator, OscarIndicator, OvershootReductionMA
- P: ParabolicWeightedMA, ParametricCorrectiveLinearMA, ParametricKalmanFilter,
     PeakValleyEstimation, PentupleExponentialMA, PercentChangeOscillator,
     PercentagePriceOscillatorLeader, PercentageTrailingStops, PercentageTrend,
     PerformanceIndex, PeriodicChannel, PhaseChangeIndex, PivotDetectorOscillator,
     PivotPointAverage, PolynomialLeastSquaresMA, PoweredKaufmanAdaptiveMA,
     PremierStochasticOscillator, PriceChannel, PriceCurveChannel, PriceCycleOscillator,
     PriceHeadleyAccelerationBands, PriceLineChannel, PriceMomentumOscillator,
     PriceVolumeRank, PrimeNumberBands, PrimeNumberOscillator, PringSpecialK,
     ProjectedSupportAndResistance, ProjectionBands, ProjectionBandwidth,
     ProjectionOscillator, PseudoPolynomialChannel, PsychologicalLine
- Q: QmaSmaDifference, QuadraticLeastSquaresMovingAverage, QuadraticMovingAverage,
     QuadrupleExponentialMovingAverage

Total golden file tests: 343 -> 391 (+48)
Total test count: 2447

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add golden file validation tests for indicators starting with R:
- RahulMohindarOscillator, RainbowOscillator, RandomWalkIndex, RangeBands,
  RangeIdentifier, RapidRelativeStrengthIndex, RateOfChange, RateOfChangeBands,
  RatioOCHLAverager, ReallySimpleIndicator, RecursiveDifferenciator,
  RecursiveMovingTrendAverage, RecursiveRelativeStrengthIndex, RecursiveStochastic,
  RegressionOscillator, RegularizedExponentialMovingAverage,
  RelativeDifferenceOfSquaresOscillator, RelativeMomentumIndex, RelativeSpreadStrength,
  RelativeVolatilityIndexV1, RelativeVolatilityIndexV2, RelativeVolumeIndicator,
  Repulse, RepulsionMovingAverage, RetentionAccelerationFilter,
  RetrospectiveCandlestickChart, ReversalPoints, ReverseEngineeringRelativeStrengthIndex,
  ReverseMovingAverageConvergenceDivergence, RexOscillator, RightSidedRickerMovingAverage,
  RobustWeightingOscillator, RootMovingAverageSquaredErrorBands, RSINGIndicator,
  RunningEquity

Note: Multi-stock indicators (RSMKIndicator, RelativeNormalizedVolatility,
RelativeStrength3DIndicator) require SeriesKey - skipped for single-stock tests.

Total golden file tests: 391 -> 426 (+35)
Total test count: 2482

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds golden file validation for S indicators:
- ScalpersChannel through SymmetricallyWeightedMovingAverage
- Standard validation: finite values, NaN handling, post-warmup checks
- SectorRotationModel skipped (multi-stock indicator)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds golden file validation for T-Z indicators:
- TStepLeastSquaresMA through ZeroLowLagMovingAverage
- Standard validation: finite values, NaN handling, post-warmup checks
- All 93 indicators now have basic validation coverage

Total golden file tests: 571

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…71→666)

Adds golden file validation for remaining A-Z non-Ehlers indicators:
- APO, ADL, ADX, ALMA, Bear/Bull Power, CMF, CMO variants
- Connors RSI, DPO, DEMA, KAMA, KST, LSMA, MFI, MACD
- Momentum, NVI, OBV, PPO, PVO, PVI, PZO, QQE, RSI
- TII, TMA, TEMA, TRIX, TSI, VIDYA, VHF, WMA, ZLEMA, Zweig

Total golden file tests: 666

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added comprehensive golden file tests for all Ehlers indicators:
- Ehlers 2-pole and 3-pole Butterworth filters (v1 and v2)
- Ehlers adaptive cyber cycle, rsi, stochastic (v1 and v2)
- Ehlers hurst coefficient, coefficient of diffusion
- Ehlers inverse fisher transform, fisher transform (v2)
- Ehlers mama/fama, correlation angle
- Ehlers decycler (v1 and v2), high-pass filters
- Ehlers phase accumulation, truncated BP/HP
- Ehlers noise elimination, signal to noise ratios
- And many more advanced Ehlers indicators

Also includes ExtendedRecursiveBands indicator.

Note: Multi-stock indicators (KaufmanStressIndicator, RSMK, etc.)
require IMultiSeriesIndicatorState implementations before testing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…s, 766→825)

Added golden file tests for all remaining single-stock indicators:
- Adaptive ARMA, Adaptive Stochastic
- Aroon Oscillator, ATR, ATR Channel
- Bollinger Bands variants (ATR, Fibonacci, ATR Pct)
- Chande indicators (Kroll R-Squared, CMO ADI, Volatility Index)
- Decision Point PMO, Donchian Channels
- Ehlers remaining: Adaptive RSI/Stochastic (V1/V2), CCI IFT, FRAMA
- Ehlers remaining: Instantaneous Trendline (V1/V2), KAMA, Laguerre RSI
- Ehlers remaining: Modified RSI/Stochastic, MAMA, RVI
- Ehlers remaining: Roofing Filter variants, Simple Cycle
- Ehlers remaining: Stochastic COG/Cyber Cycle, Super Smoother
- Elastic VWMA (V1/V2), Ergodic indicators
- Fast/Slow Stochastic, Fisher Transform Stochastic
- Full Typical Price, Inverse Fisher Fast Z-Score
- Kaufman Adaptive LSMA, Keltner Channels
- Linda Raschke 3/10 Oscillator, McGinley Dynamic
- Stochastic Oscillator, Stochastic RSI
- Tillson T3, VWAP

Total coverage: 825/771 golden file tests (including variant tests).
Multi-stock indicators excluded (need IMultiSeriesIndicatorState).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…831)

Added proper golden file tests for all 6 multi-stock indicators:
- RSMKIndicator (Relative Strength Mansfield-Kostelich)
- ComparePriceMomentumOscillator (CPMO)
- KaufmanStressIndicator
- RelativeNormalizedVolatility
- RelativeStrength3DIndicator
- SectorRotationModel

Multi-stock indicators require TWO data sources:
1. Primary stock data
2. Market/benchmark data (e.g., S&P 500)

Tests use proper multi-stock API:
- builder.AddDataSource("market", marketSource)
- catalog.Price("market") for market price handle
- catalog.RSMKIndicator(marketPrice, ...) etc.

V2 implementations (IMultiSeriesIndicatorState) are fully functional.
Total coverage: 831 golden file tests for all 771 indicators.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Implements comprehensive backtesting capabilities:
- BacktestEngine with full simulation logic
- BacktestResults with 25+ metrics (Sharpe, Sortino, Calmar, etc.)
- PositionSizingOptions (fixed units, fixed dollar, percent of equity, Kelly, volatility-based)
- RiskManagementOptions (stop loss, take profit, trailing stops, max drawdown)
- StockIndicatorBuilder.Backtest() integration method
- EquityPoint and TradeRecord for detailed trade tracking

Metrics include:
- Net profit, total return, CAGR, annualized return
- Win rate, profit factor, expectancy
- Max drawdown (amount, percent, duration)
- Sharpe ratio, Sortino ratio, Calmar ratio
- Longest win/lose streaks, largest win/loss
- Monthly returns breakdown

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…tions (phase 9)

This commit adds:
- WebSocket notification channel for real-time signal push
- Signal-to-notification routing with OnSignal() pattern matching
- Compound signal support with fluent .And() / .Or() syntax
- Extended signal rule builder for multi-condition signals
- Rising and Falling trigger types for trend detection
- 17 new unit tests for signal/notification functionality

Example usage:
  signals.Signal(rsi).IsBelow(30)
         .And(macdLine).CrossesAbove(macdSignal)
         .Named("Strong Buy Signal");

  notify.OnSignal("RSI*")
        .SendWebSocket("ws://localhost:8080")
        .SendEmail(emailOptions);

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add production-ready auto-trading types and fluent configuration:
- PositionSize (Fixed, PercentOfEquity, RiskBased, AllIn)
- StopLoss (Percent, Dollars, TrailingPercent, TrailingDollars)
- TakeProfit (Percent, Dollars)
- TradingSettings for risk management (MaxPositions, DailyLossLimit)
- IBroker interface for broker operations
- BrokerAccount, BrokerPosition, BrokerOrder data classes
- ExtendedAutoTradeRule with pattern matching
- SignalTradeRuleBuilder/SignalTradeActionBuilder fluent API
- OrderType, TimeInForce, BrokerOrderStatus enums

Update AutoTradingCatalog with:
- UseBroker() method for broker configuration
- OnSignal(string) pattern matching for signal-to-trade rules
- Global trading settings (MaxPositions, DailyLossLimit, etc.)
- Fluent API: trading.OnSignal("Buy").Buy().WithSize().WithStopLoss()

Add 31 comprehensive unit tests for all trading types.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add comprehensive integration tests demonstrating all V2 features
working together:
- Full workflow with indicators, signals, notifications, trading
- Multiple indicator types (MA, oscillators, trend, volatility, volume)
- Compound signals with all operators (And, Or, Above, Below, Crosses)
- Notification routing with pattern matching
- Auto-trading rules with all position sizing types
- All stop loss types (percent, dollars, trailing)
- Backtest configuration integration

13 integration tests covering all combinations of V2 features.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ation

- Add integration tests for brokers, market data, and risk management
- Add performance benchmarks (latency, throughput, memory)
- Add CircuitBreaker, RetryPolicy, DeadLetterQueue for resilience
- Add OrderReconciler and PositionReconciler for state recovery
- Add RiskManagement (VaR, CVaR, Kelly Criterion, exposure limits)
- Include all Phase 1-2 trading infrastructure files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
6.0% Duplication on New Code (required ≤ 3%)
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Copy link
Copy Markdown

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

🤖 Fix all issues with AI agents
In `@src/Attributes/ValidationAttributes.cs`:
- Around line 105-109: The IndicatorBoundsAttribute constructor currently allows
MinValue > MaxValue; add a guard in the IndicatorBoundsAttribute(double
minValue, double maxValue) constructor that validates minValue <= maxValue and
throws an ArgumentException (or ArgumentOutOfRangeException) when violated,
including a clear message mentioning MinValue and MaxValue so misuse like
[IndicatorBounds(100, 0)] is caught early; ensure you still assign MinValue and
MaxValue only after validation.

In `@src/Builder/Backtest/AdvancedBacktest.cs`:
- Around line 161-173: In CalculateStrategyReturns, avoid DivideByZeroException
by checking bars[i - 1].Close before computing dailyReturn; if bars[i - 1].Close
== 0m set dailyReturn = 0m (or otherwise skip adding but prefer setting 0m to
preserve alignment) and then multiply by position and add to returns; update the
block that computes dailyReturn (using bars, signals, position variables) to
handle the zero-close case.
- Around line 46-54: Fix the division-by-zero and redundant ternary in Analyze:
compute stepSize using Math.Max(1, NumFolds - 1) as the divisor (i.e., var
stepSize = Math.Max(1, (availableBars - OutOfSamplePeriodBars) / Math.Max(1,
NumFolds - 1));) so NumFolds==1 cannot divide by zero, and replace the
oosStartIndex ternary that duplicates both branches with the single expression
oosStartIndex = InSamplePeriodBars + fold * stepSize (remove UseAnchoredWindow
branch or adjust only if logic differs elsewhere); update references to stepSize
and oosStartIndex in Analyze accordingly.
- Around line 393-410: The confidence index calculations in AdvancedBacktest
(lowerIndex/upperIndex) can produce out-of-range values when NumSimulations is
very small; update the index computation and result selection to clamp to valid
bounds: ensure NumSimulations is treated as at least 1, compute the raw indices
for lowerIndex and upperIndex from (1-ConfidenceLevel)/2 * NumSimulations and
(1+ConfidenceLevel)/2 * NumSimulations then clamp them into [0,
sortedReturns.Count-1] and [0, sortedDrawdowns.Count-1] respectively (use
Math.Floor when casting if needed), and also guard median selection so when
lists are empty or have single element you return that element instead of
indexing out of range; update usages in the MonteCarloResult construction
(MeanReturn, MedianReturn, ReturnLowerBound, ReturnUpperBound,
MedianMaxDrawdown, DrawdownLowerBound, DrawdownUpperBound) to use these safe
indices.

In `@src/Builder/Backtest/BacktestEngine.cs`:
- Around line 311-366: CalculateStopPrice and CalculateTakeProfitPrice currently
ignore StopLossType.AtrBased and TakeProfitType.AtrBased; update both methods to
fail fast by throwing a clear NotSupportedException (or
InvalidOperationException) when the respective enum value AtrBased is
encountered, with a message stating ATR-based exits are not implemented yet and
indicating what config triggered it; reference the enum values
StopLossType.AtrBased and TakeProfitType.AtrBased and the methods
CalculateStopPrice and CalculateTakeProfitPrice. Optionally (if ATR data
exists), implement ATR logic instead by using an available ATR source (e.g., a
pos.Atr or _riskManagement ATR settings and multiplier) to compute stop = entry
± multiplier*atr and take = entry ± multiplier*atr, otherwise add the explicit
exception to avoid silent misconfiguration.
- Around line 174-219: The ClosePosition method is subtracting exit fees twice
by using _equity += netPnL - fees even though netPnL already includes exit fees;
update the equity adjustment to add the correct amount (use grossPnL minus only
the exit fees that haven't been applied yet, and do not subtract entry fees
again) so _equity is updated consistently with how entry fees were
handled—locate ClosePosition, adjust the line updating _equity to use grossPnL
and fees correctly (and ensure _totalFees and _totalSlippage updates remain
accurate) so exit fees are not double-counted.
- Around line 391-412: The switch in CalculatePositionSize currently ignores
PositionSizingMethod.VolatilityAdjusted and falls to the default; update
CalculatePositionSize to handle PositionSizingMethod.VolatilityAdjusted
explicitly by either calling a new helper (e.g.,
CalculateVolatilityAdjustedSize(price)) that implements the intended
volatility-adjusted sizing logic or by throwing a
NotImplementedException/InvalidOperationException to fail fast until
implementation exists; ensure the new branch references
PositionSizingMethod.VolatilityAdjusted and returns a long/int-compatible size
like the other branches so sizing stays correct.
- Around line 414-427: CalculateRiskBasedSize currently assumes StopLossValue is
a percent and computes stopAmount as price * (StopLossValue/100); update it to
honor the active stop type by checking _riskManagement.StopLossType (and any
enum values like FixedDollar, ATR, Percent) and computing the stop distance
appropriately: for Percent keep price * (StopLossValue/100), for FixedDollar use
stopAmount = _riskManagement.StopLossValue (absolute dollar distance), for ATR
use ATR-based distance computed from available ATR data (or guard if ATR
unavailable). If a stop type is unsupported or required data is missing, return
0 (or log and return 0) to avoid mis-sizing; keep references to
CalculateRiskBasedSize, _riskManagement.StopLossType,
_riskManagement.StopLossValue, and _positionSizing.EquityPercent when making the
change.
- Around line 249-308: The max-drawdown calculation in CheckRiskManagementExit
uses _equity (realized) while _peakEquity is tracked with total equity; change
the drawdown computation to use total equity instead: compute a
currentTotalEquity (use an existing total-equity field if present, otherwise
compute _equity plus the current position's unrealized P&L via _currentPosition)
and replace (_peakEquity - _equity) / _peakEquity * 100 with (_peakEquity -
currentTotalEquity) / _peakEquity * 100 so drawdown compares like-for-like with
_peakEquity.
- Around line 128-133: CheckEntrySignals currently contains no logic so no
trades are opened; implement basic entry-signal evaluation (or fail fast) by
examining the passed TickerData and IndicatorSnapshot and calling OpenPosition
when entry criteria are met. Specifically, add evaluation inside
CheckEntrySignals to detect a simple entry (e.g., price crossing above a signal
line or snapshot.HasBullishSignal) using the bar and snapshot data, and invoke
OpenPosition(bar, barIndex, snapshot) with appropriate parameters;
alternatively, if you don't want placeholder logic, throw a
NotImplementedException from CheckEntrySignals to make incomplete implementation
explicit. Ensure references to CheckEntrySignals, OpenPosition, TickerData,
IndicatorSnapshot and barIndex are used so the control flow actually opens
positions or fails fast.

In `@src/Builder/Backtest/BacktestResults.cs`:
- Around line 300-304: NetPnL is subtracting Slippage even though
BacktestEngine's executed entry/exit prices already include slippage; update the
NetPnL property to stop double‑counting by removing Slippage from the
calculation (i.e., NetPnL = GrossPnL - Fees) and update the XML comment above
NetPnL to reflect "Gets the net profit/loss (GrossPnL - Fees)"; alternatively,
if you intend GrossPnL to be pre‑slippage, compute GrossPnL before applying
execution slippage in the BacktestEngine, but do not subtract Slippage again in
the NetPnL getter.

In `@src/Builder/Catalogs/IndicatorCatalog.cs`:
- Around line 67-85: IsIndicatorSupported currently only catches
NotSupportedException but callers like IndicatorSpecs.Create and
StatefulIndicatorFactory.Create may throw parameter-validation exceptions (e.g.,
ArgumentException, ArgumentNullException) which should be treated as
"unsupported"; update IsIndicatorSupported to catch those validation exceptions
in addition to NotSupportedException (or catch System.ArgumentException and
System.ArgumentNullException) around the calls to IndicatorSpecs.Create and
StatefulIndicatorFactory.Create, return false for those cases, and still ensure
the created state is disposed when applicable (keep the existing disposable
check for the state).

In `@V2_FAST_PATH_CHECKLIST.md`:
- Around line 13-41: The document shows inconsistent totals across the "Current
State" table and "Progress Summary" (e.g., 620/617 vs earlier 409/~530) and the
reviewer flagged other conflicting ranges (lines 54-59, 80-89, 719-737);
reconcile by choosing a single authoritative set of counts (or compute and
update them from the latest source), update the "Current State" table (Core
Methods, ComputeFast Wrappers, SpecOptions, TryComputeFast Dispatch,
IndicatorName Total) to match that authoritative tally, and add a dated-snapshot
annotation to each block in the Progress Summary (or add a legend explaining
date/version for each batch) so historical snapshots remain clear and
non-conflicting.

In `@V2_MOVINGAVGTYPE_DESIGN.md`:
- Around line 63-71: The fenced code block in V2_MOVINGAVGTYPE_DESIGN.md is
missing a language tag (triggering MD040); update the opening fence for the
directory tree block to include a language identifier like "text" (i.e., change
"```" to "```text") so the snippet is properly labeled and Markdownlint passes;
keep the rest of the block (the directory listing and closing "```") unchanged.
🧹 Nitpick comments (10)
docs/VALIDATION.md (1)

28-28: Consider addressing markdown formatting issues.

Static analysis flagged several valid formatting concerns:

  • Tables at lines 28, 42, 61, 71, 82 should be surrounded by blank lines
  • Line 230 uses emphasis instead of a proper heading
♻️ Proposed formatting fixes

Add blank lines before tables:

 
+
 ### Moving Averages (12)
 | Indicator | Reference | Test File |
 
+
 ### Oscillators (15)
 | Indicator | Reference | Test File |
 
+
 ### Trend (6)
 | Indicator | Reference | Test File |
 
+
 ### Volatility (7)
 | Indicator | Reference | Test File |
 
+
 ### Volume (9)
 | Indicator | Reference | Test File |

Convert emphasis to proper heading:

-*Remaining 650+ indicators to be categorized and validated*
+### Remaining Indicators
+
+650+ indicators to be categorized and validated

Also applies to: 42-42, 61-61, 71-71, 82-82, 230-230

src/Attributes/ValidationAttributes.cs (2)

44-47: Consider using an enum for Priority to enforce valid values.

The string-based priority ("High", "Medium", "Low") is prone to typos and inconsistent casing. An enum would provide compile-time safety.

♻️ Suggested refactor
public enum ReviewPriority { Low, Medium, High }
     /// <summary>
     /// Priority level for review (High, Medium, Low).
     /// </summary>
-    public string Priority { get; set; } = "Medium";
+    public ReviewPriority Priority { get; set; } = ReviewPriority.Medium;

100-103: CanBeNegative could be inferred from MinValue.

When MinValue >= 0, having CanBeNegative = true (the default) is logically inconsistent. Consider computing this from MinValue or adding documentation noting that users should set it explicitly when MinValue >= 0.

♻️ Alternative: compute from MinValue
     /// <summary>
     /// Whether the indicator can produce negative values.
     /// </summary>
-    public bool CanBeNegative { get; set; } = true;
+    public bool CanBeNegative => MinValue < 0;

If explicit override capability is needed, keep as-is but default to MinValue < 0:

-    public bool CanBeNegative { get; set; } = true;
+    public bool CanBeNegative { get; set; }

     public IndicatorBoundsAttribute(double minValue, double maxValue)
     {
         MinValue = minValue;
         MaxValue = maxValue;
+        CanBeNegative = minValue < 0;
     }
src/Builder/Catalogs/NotificationCatalog.cs (2)

26-48: Consider adding null validation for consistency with AddChannel.

AddChannel correctly validates for null (line 91), but Email, Sms, Webhook, and WebSocket(options) don't validate their parameters. This inconsistency could lead to confusing null reference exceptions deeper in the channel constructors rather than at the catalog API boundary.

Similarly, WebSocket(string uri) doesn't validate the uri parameter before creating WebSocketOptions.

♻️ Proposed fix (example for Email)
 public NotificationCatalog Email(EmailOptions options)
 {
+    ArgumentNullException.ThrowIfNull(options);
     _channels.Add(new EmailNotificationChannel(options));
     return this;
 }

Also applies to: 71-83


112-122: Validate signalNameOrPattern parameter.

If signalNameOrPattern is null or empty, downstream pattern matching in NotificationRoute.Matches() may behave unexpectedly. Consider adding validation to fail fast with a clear error message.

♻️ Proposed fix
 public NotificationRouteBuilder OnSignal(string signalNameOrPattern)
 {
+    if (string.IsNullOrWhiteSpace(signalNameOrPattern))
+        throw new ArgumentException("Signal name or pattern cannot be null or empty.", nameof(signalNameOrPattern));
+
     // Complete any previous route builder
     _currentRouteBuilder?.Complete();
src/Builder/Backtest/RiskManagementOptions.cs (1)

145-176: Add guardrails to prevent invalid percent/ratio inputs.
Fail fast on negative/zero (or >100%) values to avoid inverted risk logic later.

♻️ Suggested validation
-    public static RiskManagementOptions FixedStopLoss(double percent) => new()
-    {
-        StopLossType = StopLossType.FixedPercent,
-        StopLossValue = percent
-    };
+    public static RiskManagementOptions FixedStopLoss(double percent)
+    {
+        if (percent <= 0 || percent > 100)
+            throw new ArgumentOutOfRangeException(nameof(percent));
+        return new RiskManagementOptions
+        {
+            StopLossType = StopLossType.FixedPercent,
+            StopLossValue = percent
+        };
+    }

-    public static RiskManagementOptions TrailingStop(double percent) => new()
-    {
-        StopLossType = StopLossType.TrailingPercent,
-        StopLossValue = percent
-    };
+    public static RiskManagementOptions TrailingStop(double percent)
+    {
+        if (percent <= 0 || percent > 100)
+            throw new ArgumentOutOfRangeException(nameof(percent));
+        return new RiskManagementOptions
+        {
+            StopLossType = StopLossType.TrailingPercent,
+            StopLossValue = percent
+        };
+    }

-    public static RiskManagementOptions RiskReward(double stopPercent, double riskRewardRatio) => new()
-    {
-        StopLossType = StopLossType.FixedPercent,
-        StopLossValue = stopPercent,
-        TakeProfitType = TakeProfitType.RiskRewardRatio,
-        TakeProfitValue = riskRewardRatio
-    };
+    public static RiskManagementOptions RiskReward(double stopPercent, double riskRewardRatio)
+    {
+        if (stopPercent <= 0 || stopPercent > 100)
+            throw new ArgumentOutOfRangeException(nameof(stopPercent));
+        if (riskRewardRatio <= 0)
+            throw new ArgumentOutOfRangeException(nameof(riskRewardRatio));
+        return new RiskManagementOptions
+        {
+            StopLossType = StopLossType.FixedPercent,
+            StopLossValue = stopPercent,
+            TakeProfitType = TakeProfitType.RiskRewardRatio,
+            TakeProfitValue = riskRewardRatio
+        };
+    }
src/Builder/Backtest/PositionSizingOptions.cs (1)

81-120: Add basic validation to factory helpers.
Guard against negative/zero units and out-of-range percentages to fail fast on invalid sizing configs.

♻️ Suggested validation
-    public static PositionSizingOptions CreateFixedUnits(int units) => new()
-    {
-        Method = PositionSizingMethod.FixedUnits,
-        FixedUnits = units
-    };
+    public static PositionSizingOptions CreateFixedUnits(int units)
+    {
+        if (units <= 0) throw new ArgumentOutOfRangeException(nameof(units));
+        return new PositionSizingOptions
+        {
+            Method = PositionSizingMethod.FixedUnits,
+            FixedUnits = units
+        };
+    }

-    public static PositionSizingOptions FixedDollar(double amount) => new()
-    {
-        Method = PositionSizingMethod.FixedDollar,
-        FixedDollarAmount = amount
-    };
+    public static PositionSizingOptions FixedDollar(double amount)
+    {
+        if (amount <= 0) throw new ArgumentOutOfRangeException(nameof(amount));
+        return new PositionSizingOptions
+        {
+            Method = PositionSizingMethod.FixedDollar,
+            FixedDollarAmount = amount
+        };
+    }

-    public static PositionSizingOptions PercentOfEquity(double percent) => new()
-    {
-        Method = PositionSizingMethod.PercentOfEquity,
-        EquityPercent = percent
-    };
+    public static PositionSizingOptions PercentOfEquity(double percent)
+    {
+        if (percent <= 0 || percent > 100) throw new ArgumentOutOfRangeException(nameof(percent));
+        return new PositionSizingOptions
+        {
+            Method = PositionSizingMethod.PercentOfEquity,
+            EquityPercent = percent
+        };
+    }

-    public static PositionSizingOptions RiskPercent(double riskPercent) => new()
-    {
-        Method = PositionSizingMethod.RiskPercent,
-        EquityPercent = riskPercent
-    };
+    public static PositionSizingOptions RiskPercent(double riskPercent)
+    {
+        if (riskPercent <= 0 || riskPercent > 100) throw new ArgumentOutOfRangeException(nameof(riskPercent));
+        return new PositionSizingOptions
+        {
+            Method = PositionSizingMethod.RiskPercent,
+            EquityPercent = riskPercent
+        };
+    }
src/Builder/Backtest/AdvancedBacktest.cs (3)

186-189: Inconsistent variance formula (population vs sample).

This line uses population variance (dividing by Count), while other variance calculations in the file (lines 147, 337, 430) use sample variance (dividing by Count - 1). For consistency and statistical correctness with small fold counts, consider using sample variance.

Proposed fix
         // Calculate stability (std dev of returns across folds)
         var mean = result.AverageOOSReturn;
-        var variance = result.FoldResults.Sum(f => (f.TotalReturn - mean) * (f.TotalReturn - mean)) / result.FoldResults.Count;
+        var variance = result.FoldResults.Count > 1
+            ? result.FoldResults.Sum(f => (f.TotalReturn - mean) * (f.TotalReturn - mean)) / (result.FoldResults.Count - 1)
+            : 0m;
         result.ReturnStability = (decimal)Math.Sqrt((double)variance);

573-577: Unused properties Method and Objective.

These properties are declared but not used by GridSearch. Consider either:

  1. Using them to select behavior within GridSearch (e.g., for MinDrawdown, negate the objective)
  2. Removing them until RandomSearch/GeneticAlgorithm are implemented
  3. Documenting they are reserved for future use

544-566: Consider reusing the existing Bar type or extracting a shared OHLCV interface.

BarData duplicates the OHLCV pattern already defined in Bar (src/Builder/MarketData/MarketDataTypes.cs). While Bar includes additional fields (symbol, vwap), the core OHLCV structure is redundant. To reduce duplication:

  • Make symbol optional in Bar and reuse it here, or
  • Extract a common OHLCV interface that both Bar and BarData implement

Comment on lines +105 to +109
public IndicatorBoundsAttribute(double minValue, double maxValue)
{
MinValue = minValue;
MaxValue = maxValue;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add validation to ensure MinValue <= MaxValue.

The constructor accepts any double values, allowing logically invalid configurations like [IndicatorBounds(100, 0)]. A guard clause would catch misuse early.

🛡️ Proposed fix
     public IndicatorBoundsAttribute(double minValue, double maxValue)
     {
+        if (minValue > maxValue)
+        {
+            throw new ArgumentException($"MinValue ({minValue}) cannot be greater than MaxValue ({maxValue}).");
+        }
         MinValue = minValue;
         MaxValue = maxValue;
     }
📝 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 IndicatorBoundsAttribute(double minValue, double maxValue)
{
MinValue = minValue;
MaxValue = maxValue;
}
public IndicatorBoundsAttribute(double minValue, double maxValue)
{
if (minValue > maxValue)
{
throw new ArgumentException($"MinValue ({minValue}) cannot be greater than MaxValue ({maxValue}).");
}
MinValue = minValue;
MaxValue = maxValue;
}
🤖 Prompt for AI Agents
In `@src/Attributes/ValidationAttributes.cs` around lines 105 - 109, The
IndicatorBoundsAttribute constructor currently allows MinValue > MaxValue; add a
guard in the IndicatorBoundsAttribute(double minValue, double maxValue)
constructor that validates minValue <= maxValue and throws an ArgumentException
(or ArgumentOutOfRangeException) when violated, including a clear message
mentioning MinValue and MaxValue so misuse like [IndicatorBounds(100, 0)] is
caught early; ensure you still assign MinValue and MaxValue only after
validation.

Comment on lines +46 to +54
// Calculate step size between folds
var availableBars = bars.Count - InSamplePeriodBars;
var stepSize = Math.Max(1, (availableBars - OutOfSamplePeriodBars) / (NumFolds - 1));

for (var fold = 0; fold < NumFolds; fold++)
{
var oosStartIndex = UseAnchoredWindow
? InSamplePeriodBars + fold * stepSize
: fold * stepSize + InSamplePeriodBars;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Potential division by zero when NumFolds = 1.

Line 48 calculates stepSize with division by (NumFolds - 1). When NumFolds is 1, this results in division by zero. The AnalyzeReturns method (line 108) correctly handles this with Math.Max(1, NumFolds - 1), but Analyze does not.

Additionally, lines 52-54 compute identical values for both branches of the ternary—both resolve to InSamplePeriodBars + fold * stepSize.

Proposed fix
         // Calculate step size between folds
         var availableBars = bars.Count - InSamplePeriodBars;
-        var stepSize = Math.Max(1, (availableBars - OutOfSamplePeriodBars) / (NumFolds - 1));
+        var stepSize = Math.Max(1, (availableBars - OutOfSamplePeriodBars) / Math.Max(1, NumFolds - 1));

         for (var fold = 0; fold < NumFolds; fold++)
         {
-            var oosStartIndex = UseAnchoredWindow
-                ? InSamplePeriodBars + fold * stepSize
-                : fold * stepSize + InSamplePeriodBars;
+            var oosStartIndex = InSamplePeriodBars + fold * stepSize;
🤖 Prompt for AI Agents
In `@src/Builder/Backtest/AdvancedBacktest.cs` around lines 46 - 54, Fix the
division-by-zero and redundant ternary in Analyze: compute stepSize using
Math.Max(1, NumFolds - 1) as the divisor (i.e., var stepSize = Math.Max(1,
(availableBars - OutOfSamplePeriodBars) / Math.Max(1, NumFolds - 1));) so
NumFolds==1 cannot divide by zero, and replace the oosStartIndex ternary that
duplicates both branches with the single expression oosStartIndex =
InSamplePeriodBars + fold * stepSize (remove UseAnchoredWindow branch or adjust
only if logic differs elsewhere); update references to stepSize and
oosStartIndex in Analyze accordingly.

Comment on lines +161 to +173
private static IReadOnlyList<decimal> CalculateStrategyReturns(
IReadOnlyList<BarData> bars,
IReadOnlyList<decimal> signals)
{
var returns = new List<decimal>();
for (var i = 1; i < bars.Count && i < signals.Count; i++)
{
var dailyReturn = (bars[i].Close - bars[i - 1].Close) / bars[i - 1].Close;
var position = signals[i - 1]; // Previous day's signal
returns.Add(dailyReturn * position);
}
return returns;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Division by zero risk when close price is zero.

Line 168 divides by bars[i - 1].Close without checking for zero. If historical data contains a zero close price (bad data, delisted stock), this will throw DivideByZeroException.

Proposed fix
     private static IReadOnlyList<decimal> CalculateStrategyReturns(
         IReadOnlyList<BarData> bars,
         IReadOnlyList<decimal> signals)
     {
         var returns = new List<decimal>();
         for (var i = 1; i < bars.Count && i < signals.Count; i++)
         {
+            if (bars[i - 1].Close == 0) continue;
             var dailyReturn = (bars[i].Close - bars[i - 1].Close) / bars[i - 1].Close;
             var position = signals[i - 1]; // Previous day's signal
             returns.Add(dailyReturn * position);
         }
         return returns;
     }
📝 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 IReadOnlyList<decimal> CalculateStrategyReturns(
IReadOnlyList<BarData> bars,
IReadOnlyList<decimal> signals)
{
var returns = new List<decimal>();
for (var i = 1; i < bars.Count && i < signals.Count; i++)
{
var dailyReturn = (bars[i].Close - bars[i - 1].Close) / bars[i - 1].Close;
var position = signals[i - 1]; // Previous day's signal
returns.Add(dailyReturn * position);
}
return returns;
}
private static IReadOnlyList<decimal> CalculateStrategyReturns(
IReadOnlyList<BarData> bars,
IReadOnlyList<decimal> signals)
{
var returns = new List<decimal>();
for (var i = 1; i < bars.Count && i < signals.Count; i++)
{
if (bars[i - 1].Close == 0) continue;
var dailyReturn = (bars[i].Close - bars[i - 1].Close) / bars[i - 1].Close;
var position = signals[i - 1]; // Previous day's signal
returns.Add(dailyReturn * position);
}
return returns;
}
🤖 Prompt for AI Agents
In `@src/Builder/Backtest/AdvancedBacktest.cs` around lines 161 - 173, In
CalculateStrategyReturns, avoid DivideByZeroException by checking bars[i -
1].Close before computing dailyReturn; if bars[i - 1].Close == 0m set
dailyReturn = 0m (or otherwise skip adding but prefer setting 0m to preserve
alignment) and then multiply by position and add to returns; update the block
that computes dailyReturn (using bars, signals, position variables) to handle
the zero-close case.

Comment on lines +393 to +410
var lowerIndex = (int)((1 - ConfidenceLevel) / 2 * NumSimulations);
var upperIndex = (int)((1 + ConfidenceLevel) / 2 * NumSimulations) - 1;

return new MonteCarloResult
{
NumSimulations = NumSimulations,
ConfidenceLevel = ConfidenceLevel,

MeanReturn = returns.Average(),
MedianReturn = sortedReturns[sortedReturns.Count / 2],
ReturnLowerBound = sortedReturns[lowerIndex],
ReturnUpperBound = sortedReturns[upperIndex],
ReturnStdDev = CalculateStdDev(returns),

MeanMaxDrawdown = drawdowns.Average(),
MedianMaxDrawdown = sortedDrawdowns[sortedDrawdowns.Count / 2],
DrawdownLowerBound = sortedDrawdowns[lowerIndex],
DrawdownUpperBound = sortedDrawdowns[upperIndex],
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Index out-of-bounds risk with small NumSimulations.

When NumSimulations is very small (e.g., 1 or 2), the confidence interval index calculations on lines 393-394 may produce invalid indices:

  • lowerIndex = (int)((1 - 0.95) / 2 * 2) = 0
  • upperIndex = (int)((1 + 0.95) / 2 * 2) - 1 = 0

With NumSimulations = 1, upperIndex becomes -1, causing IndexOutOfRangeException on line 404.

Proposed fix
         var lowerIndex = (int)((1 - ConfidenceLevel) / 2 * NumSimulations);
         var upperIndex = (int)((1 + ConfidenceLevel) / 2 * NumSimulations) - 1;
+        
+        // Clamp indices to valid range
+        lowerIndex = Math.Clamp(lowerIndex, 0, sortedReturns.Count - 1);
+        upperIndex = Math.Clamp(upperIndex, 0, sortedReturns.Count - 1);
📝 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
var lowerIndex = (int)((1 - ConfidenceLevel) / 2 * NumSimulations);
var upperIndex = (int)((1 + ConfidenceLevel) / 2 * NumSimulations) - 1;
return new MonteCarloResult
{
NumSimulations = NumSimulations,
ConfidenceLevel = ConfidenceLevel,
MeanReturn = returns.Average(),
MedianReturn = sortedReturns[sortedReturns.Count / 2],
ReturnLowerBound = sortedReturns[lowerIndex],
ReturnUpperBound = sortedReturns[upperIndex],
ReturnStdDev = CalculateStdDev(returns),
MeanMaxDrawdown = drawdowns.Average(),
MedianMaxDrawdown = sortedDrawdowns[sortedDrawdowns.Count / 2],
DrawdownLowerBound = sortedDrawdowns[lowerIndex],
DrawdownUpperBound = sortedDrawdowns[upperIndex],
var lowerIndex = (int)((1 - ConfidenceLevel) / 2 * NumSimulations);
var upperIndex = (int)((1 + ConfidenceLevel) / 2 * NumSimulations) - 1;
// Clamp indices to valid range
lowerIndex = Math.Clamp(lowerIndex, 0, sortedReturns.Count - 1);
upperIndex = Math.Clamp(upperIndex, 0, sortedReturns.Count - 1);
return new MonteCarloResult
{
NumSimulations = NumSimulations,
ConfidenceLevel = ConfidenceLevel,
MeanReturn = returns.Average(),
MedianReturn = sortedReturns[sortedReturns.Count / 2],
ReturnLowerBound = sortedReturns[lowerIndex],
ReturnUpperBound = sortedReturns[upperIndex],
ReturnStdDev = CalculateStdDev(returns),
MeanMaxDrawdown = drawdowns.Average(),
MedianMaxDrawdown = sortedDrawdowns[sortedDrawdowns.Count / 2],
DrawdownLowerBound = sortedDrawdowns[lowerIndex],
DrawdownUpperBound = sortedDrawdowns[upperIndex],
🤖 Prompt for AI Agents
In `@src/Builder/Backtest/AdvancedBacktest.cs` around lines 393 - 410, The
confidence index calculations in AdvancedBacktest (lowerIndex/upperIndex) can
produce out-of-range values when NumSimulations is very small; update the index
computation and result selection to clamp to valid bounds: ensure NumSimulations
is treated as at least 1, compute the raw indices for lowerIndex and upperIndex
from (1-ConfidenceLevel)/2 * NumSimulations and (1+ConfidenceLevel)/2 *
NumSimulations then clamp them into [0, sortedReturns.Count-1] and [0,
sortedDrawdowns.Count-1] respectively (use Math.Floor when casting if needed),
and also guard median selection so when lists are empty or have single element
you return that element instead of indexing out of range; update usages in the
MonteCarloResult construction (MeanReturn, MedianReturn, ReturnLowerBound,
ReturnUpperBound, MedianMaxDrawdown, DrawdownLowerBound, DrawdownUpperBound) to
use these safe indices.

Comment on lines +128 to +133
private void CheckEntrySignals(TickerData bar, int barIndex, IndicatorSnapshot snapshot)
{
// For now, use simple price-based entry logic
// In full implementation, this would evaluate configured signals
// This is a placeholder for signal evaluation
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if OpenPosition/CheckEntrySignals are wired elsewhere.
rg -n "OpenPosition\s*\(" --type=cs
rg -n "CheckEntrySignals\s*\(" --type=cs

Repository: Ooples-Finance-LLC/OoplesFinance.StockIndicators

Length of output: 472


🏁 Script executed:

cat -n src/Builder/Backtest/BacktestEngine.cs | sed -n '110,150p'

Repository: Ooples-Finance-LLC/OoplesFinance.StockIndicators

Length of output: 1561


🏁 Script executed:

rg "OpenPosition" --type=cs -B 2 -A 2

Repository: Ooples-Finance-LLC/OoplesFinance.StockIndicators

Length of output: 516


Implement signal evaluation in CheckEntrySignals — backtests currently never trade.

CheckEntrySignals is empty and OpenPosition is never called anywhere in the codebase. With no entry logic implemented, the backtest flow reaches the placeholder method but takes no action, resulting in zero trades for all backtests.

Either implement the signal evaluation logic or restructure the control flow to fail fast and signal incomplete implementation.

🤖 Prompt for AI Agents
In `@src/Builder/Backtest/BacktestEngine.cs` around lines 128 - 133,
CheckEntrySignals currently contains no logic so no trades are opened; implement
basic entry-signal evaluation (or fail fast) by examining the passed TickerData
and IndicatorSnapshot and calling OpenPosition when entry criteria are met.
Specifically, add evaluation inside CheckEntrySignals to detect a simple entry
(e.g., price crossing above a signal line or snapshot.HasBullishSignal) using
the bar and snapshot data, and invoke OpenPosition(bar, barIndex, snapshot) with
appropriate parameters; alternatively, if you don't want placeholder logic,
throw a NotImplementedException from CheckEntrySignals to make incomplete
implementation explicit. Ensure references to CheckEntrySignals, OpenPosition,
TickerData, IndicatorSnapshot and barIndex are used so the control flow actually
opens positions or fails fast.

Comment on lines +414 to +427
private double CalculateRiskBasedSize(double price)
{
var riskPercent = _positionSizing.EquityPercent ?? 2;
var riskAmount = _equity * (riskPercent / 100);
var stopPercent = _riskManagement.StopLossValue ?? 2;
var stopAmount = price * (stopPercent / 100);

if (stopAmount <= 0)
{
return 0;
}

return Math.Floor(riskAmount / stopAmount);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

RiskPercent sizing assumes percent‑based stops.
CalculateRiskBasedSize treats StopLossValue as a percent regardless of StopLossType. This mis-sizes trades for FixedDollar/ATR-based stops. Guard or compute stop distance based on the active stop type.

🐛 Minimal guard (until other stop types are supported)
     private double CalculateRiskBasedSize(double price)
     {
+        if (_riskManagement.StopLossType != StopLossType.FixedPercent)
+            throw new InvalidOperationException("RiskPercent sizing currently requires FixedPercent stop loss.");
         var riskPercent = _positionSizing.EquityPercent ?? 2;
         var riskAmount = _equity * (riskPercent / 100);
         var stopPercent = _riskManagement.StopLossValue ?? 2;
📝 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 double CalculateRiskBasedSize(double price)
{
var riskPercent = _positionSizing.EquityPercent ?? 2;
var riskAmount = _equity * (riskPercent / 100);
var stopPercent = _riskManagement.StopLossValue ?? 2;
var stopAmount = price * (stopPercent / 100);
if (stopAmount <= 0)
{
return 0;
}
return Math.Floor(riskAmount / stopAmount);
}
private double CalculateRiskBasedSize(double price)
{
if (_riskManagement.StopLossType != StopLossType.FixedPercent)
throw new InvalidOperationException("RiskPercent sizing currently requires FixedPercent stop loss.");
var riskPercent = _positionSizing.EquityPercent ?? 2;
var riskAmount = _equity * (riskPercent / 100);
var stopPercent = _riskManagement.StopLossValue ?? 2;
var stopAmount = price * (stopPercent / 100);
if (stopAmount <= 0)
{
return 0;
}
return Math.Floor(riskAmount / stopAmount);
}
🤖 Prompt for AI Agents
In `@src/Builder/Backtest/BacktestEngine.cs` around lines 414 - 427,
CalculateRiskBasedSize currently assumes StopLossValue is a percent and computes
stopAmount as price * (StopLossValue/100); update it to honor the active stop
type by checking _riskManagement.StopLossType (and any enum values like
FixedDollar, ATR, Percent) and computing the stop distance appropriately: for
Percent keep price * (StopLossValue/100), for FixedDollar use stopAmount =
_riskManagement.StopLossValue (absolute dollar distance), for ATR use ATR-based
distance computed from available ATR data (or guard if ATR unavailable). If a
stop type is unsupported or required data is missing, return 0 (or log and
return 0) to avoid mis-sizing; keep references to CalculateRiskBasedSize,
_riskManagement.StopLossType, _riskManagement.StopLossValue, and
_positionSizing.EquityPercent when making the change.

Comment on lines +300 to +304
/// <summary>
/// Gets the net profit/loss (GrossPnL - Fees - Slippage).
/// </summary>
public double NetPnL => GrossPnL - Fees - Slippage;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

NetPnL double‑counts slippage with current execution pricing.
BacktestEngine uses entry/exit prices that already include slippage, so subtracting Slippage again understates PnL and skews metrics. Either compute GrossPnL pre‑slippage or update NetPnL (and its comment) to subtract fees only.

🐛 Suggested alignment (if GrossPnL uses executed prices)
-    /// Gets the net profit/loss (GrossPnL - Fees - Slippage).
+    /// Gets the net profit/loss (GrossPnL - Fees).
@@
-    public double NetPnL => GrossPnL - Fees - Slippage;
+    public double NetPnL => GrossPnL - Fees;
📝 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
/// <summary>
/// Gets the net profit/loss (GrossPnL - Fees - Slippage).
/// </summary>
public double NetPnL => GrossPnL - Fees - Slippage;
/// <summary>
/// Gets the net profit/loss (GrossPnL - Fees).
/// </summary>
public double NetPnL => GrossPnL - Fees;
🤖 Prompt for AI Agents
In `@src/Builder/Backtest/BacktestResults.cs` around lines 300 - 304, NetPnL is
subtracting Slippage even though BacktestEngine's executed entry/exit prices
already include slippage; update the NetPnL property to stop double‑counting by
removing Slippage from the calculation (i.e., NetPnL = GrossPnL - Fees) and
update the XML comment above NetPnL to reflect "Gets the net profit/loss
(GrossPnL - Fees)"; alternatively, if you intend GrossPnL to be pre‑slippage,
compute GrossPnL before applying execution slippage in the BacktestEngine, but
do not subtract Slippage again in the NetPnL getter.

Comment on lines +67 to +85
public static bool IsIndicatorSupported(IndicatorName name)
{
// V2 approach: Try to create the state - if it works, it's supported
try
{
var spec = IndicatorSpecs.Create(name, new GenericIndicatorOptions(Array.Empty<object>()), IndicatorOutput.Primary);
var state = StatefulIndicatorFactory.Create(spec);
// If we got here, it's supported. Dispose if needed.
if (state is IDisposable disposable)
{
disposable.Dispose();
}
return true;
}
catch (NotSupportedException)
{
return false;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Harden IsIndicatorSupported against parameter-validation exceptions.

IndicatorSpecs.Create / StatefulIndicatorFactory.Create can throw for missing/invalid parameters, which currently bubbles despite the method’s bool contract. Treat those as unsupported instead of throwing.

🔧 Proposed fix
-        catch (NotSupportedException)
+        catch (Exception ex) when (ex is NotSupportedException || ex is ArgumentException || ex is InvalidOperationException)
         {
             return false;
         }
🤖 Prompt for AI Agents
In `@src/Builder/Catalogs/IndicatorCatalog.cs` around lines 67 - 85,
IsIndicatorSupported currently only catches NotSupportedException but callers
like IndicatorSpecs.Create and StatefulIndicatorFactory.Create may throw
parameter-validation exceptions (e.g., ArgumentException, ArgumentNullException)
which should be treated as "unsupported"; update IsIndicatorSupported to catch
those validation exceptions in addition to NotSupportedException (or catch
System.ArgumentException and System.ArgumentNullException) around the calls to
IndicatorSpecs.Create and StatefulIndicatorFactory.Create, return false for
those cases, and still ensure the created state is disposed when applicable
(keep the existing disposable check for the state).

Comment on lines +13 to +41
### Current State (Updated 2026-01-24)

| Layer | Implemented | Notes |
|-------|-------------|-------|
| Core Methods | **620** | Span-based implementations (Osc:287 MA:198 Trend:71 Vol:35 Volume:29) |
| ComputeFast Wrappers | **620** | Buffer wrappers in IndicatorCompute.cs |
| SpecOptions Classes | **618** | Typed indicator options |
| TryComputeFast Dispatch | **617** | Routed to fast path methods |
| IndicatorName Total | 773 | Target for 100% coverage |

### Progress Summary

- **Batches 1-24**: Original 409 ComputeFast methods wired
- **Batch 25**: Added 40 new ComputeFast methods for unwired Core methods
- **Batch 26**: Added 36 new ComputeFast methods for additional Core methods
- **Batch 27**: Added 12 new multi-input ComputeFast methods (DeMarker, Vortex, Klinger, etc.)
- **Batch 28**: Added 15 final Core method wrappers (Reverse Engineering RSI, PPO MA, etc.)
- **Batch 29**: Added TripleHullMovingAverage, AdaptiveAutonomousRecursiveMovingAverage Core methods
- **Batch 30**: Added GDEMA, Ehlers FIR/IIR Filter, VolumeAdjustedMovingAverage, AverageDayRange
- **Batches 31-32**: Added Ehlers CenterofGravity, Reflex, Trendflex, StochasticCyberCycle Core methods
- **Batch 33**: Added Pivot Points (Floor, Camarilla, Woodie, Fibonacci, Demark), Channels (Price, Donchian, Linear)
- **Batch 34**: Added ThreeHma, AlphaDecreasingEMA, AdaptiveAutonomousRecursiveTrailingStop, AdaptiveTrailingStop, AtrTrailingStops
- **Batch 35**: Added trailing stop wiring
- **Batch 36**: Added WellesWilderSummation, DampingIndex, DidiIndex Core methods
- **Batch 37**: Added VerticalHorizontalFilter, LinearRegressionSlope, LinearRegressionIntercept wiring
- **Batch 38**: Added MovingAverageRegistry infrastructure for O(1) MA type dispatch
- **Multi-Output Support**: MACD (Line/Signal/Histogram), BollingerBands (Upper/Middle/Lower), Stochastic (K/D)
- **Total**: 618 SpecOptions, 617 dispatch routes

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clarify conflicting progress counts.
The doc shows multiple, conflicting totals (e.g., 620/617 vs ~530/410; remaining 153 vs ~176 vs 263). Consider reconciling or labeling each block as a dated snapshot to avoid confusion.

Also applies to: 54-59, 80-89, 719-737

🤖 Prompt for AI Agents
In `@V2_FAST_PATH_CHECKLIST.md` around lines 13 - 41, The document shows
inconsistent totals across the "Current State" table and "Progress Summary"
(e.g., 620/617 vs earlier 409/~530) and the reviewer flagged other conflicting
ranges (lines 54-59, 80-89, 719-737); reconcile by choosing a single
authoritative set of counts (or compute and update them from the latest source),
update the "Current State" table (Core Methods, ComputeFast Wrappers,
SpecOptions, TryComputeFast Dispatch, IndicatorName Total) to match that
authoritative tally, and add a dated-snapshot annotation to each block in the
Progress Summary (or add a legend explaining date/version for each batch) so
historical snapshots remain clear and non-conflicting.

Comment on lines +63 to +71
```
src/Core/Registry/
├── IMovingAverageCore.cs # Interface definition
├── MovingAverageRegistry.cs # Dictionary registry
└── Implementations/ # Optional subfolder for struct implementations
├── SmaCore.cs
├── EmaCore.cs
└── ...
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add a language tag to the fenced block.

Markdownlint flags the block starting at Line 63 because it lacks a language. Please tag it (e.g., text) to satisfy MD040.

💡 Proposed fix
-```
+```text
 src/Core/Registry/
 ├── IMovingAverageCore.cs      # Interface definition
 ├── MovingAverageRegistry.cs   # Dictionary registry
 └── Implementations/           # Optional subfolder for struct implementations
     ├── SmaCore.cs
     ├── EmaCore.cs
     └── ...
-```
+```
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

63-63: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
In `@V2_MOVINGAVGTYPE_DESIGN.md` around lines 63 - 71, The fenced code block in
V2_MOVINGAVGTYPE_DESIGN.md is missing a language tag (triggering MD040); update
the opening fence for the directory tree block to include a language identifier
like "text" (i.e., change "```" to "```text") so the snippet is properly labeled
and Markdownlint passes; keep the rest of the block (the directory listing and
closing "```") unchanged.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants