Conversation
📝 WalkthroughWalkthroughAdds 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
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)
Estimated code review effort🎯 5 (Critical) | ⏱️ ~150 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
Comment |
7a62673 to
e43b5b8
Compare
There was a problem hiding this comment.
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
csharplanguage.🔎 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
csharplanguage.🔎 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
csharplanguage 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.csprojRun a subset:
-+bash
dotnet run -c Release --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -- --filter SMARun streaming fanout throughput/latency: -``` +```bash dotnet run -c Release --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -- --streaming-perf --ticks 100000Use 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 --extendedCompare 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 ofMath.Logwithout guarding zero/negative input in NET calculationIn
CalculateEhlersNoiseEliminationTechnologyyou 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
CalculateEhlersRocketRelativeStrengthIndexat lines 193–195) and other methods you useMath.Log(...)directly instead of the saferMathHelper.Log, which guards invalid inputs. If any intermediate ratio becomes 0 (or negative due to rounding),Math.Logwill yield-Infinity/NaNand 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: potentialMath.Log(0)/ negative inputIn
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,v2can be zero early on, makingv2 / (range * range)zero and causingMath.Log(0)→-Infinity. This can propagate NaNs/Infinities intoampand then into signals.Consider:
- Guarding the ratio with a small positive floor, and/or
- Using the safer
MathHelper.Logwrapper.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:length3allowed to be zero but used as loop lower bound
CalculateEhlersAutoCorrelationPeriodogramclampslength3as:length3 = Math.Max(length3, 0); ... for (var k = length3; k <= length1; k++) { var prevCorr = i >= k ? corrList[i - k] : 0; ... }
length3is allowed to be 0, which leads to readingcorrList[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:prevFiltduplicatesprevFilter1.Both
prevFilter1(line 705) andprevFilt(line 708) retrieve the last filter value. This is redundant since they hold the same value wheni >= 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: usesinputList.Countinstead ofstockData.Count.For consistency with other methods in this file (and the other loops in this same method), consider using
stockData.Counthere.🔎 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 intermediateSetCustomValuesusage.
SetCustomValues(utmList)is called here to pass data toCalculateRelativeStrengthIndex, which reads fromCustomValuesList. This is a side-effect-based communication pattern. Consider adding a brief comment to clarify this is intentional, as it's not immediately obvious whySetCustomValuesis 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: UnusedtempListvariable.The
tempListis declared on line 72 and populated on line 89, but it's never read. OnlytempSumWindowis 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
tempListdeclaration 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 anArgumentExceptionor 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
catchclause 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 tocurrentValueis 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) andCalculateChandeMomentumOscillatorAverage(line 213) use the output key"Cmoa". While these operate on separateStockDatainstances, 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
$Refdoesn'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
falsebut allows developers to enable it viadotnet build -p:UseBaseline=truewhen 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:macdBlueandmacdYellow.These values are computed using
blueMultandyellowMultparameters 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
macdBlueandmacdYellowinto lists during the loop.Also applies to: 83-84
103-103: Empty list passed toSetCustomValues.Passing an empty list to
SetCustomValuesis 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
macdSignalLineListandmacdHistogramListbut only outputsMacd,I1, andI2. 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:macdMirrorHistogramis computed but not used.The variable
macdMirrorHistogramis 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 usingArray.Copyor 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), andWithInvocationCount(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 theLengthparameter.
CalculateMovingAverageConvergenceDivergence()is called without using theLengthparameter, unlike most other benchmarks. This means it will run with identical inputs across allLengthvariations, wasting benchmark time.Consider either:
- Passing
Lengthto configure MACD's internal periods- Or excluding it from length-parameterized runs
src/Calculations/BollingerBands/BollingerBandsPart2.cs (1)
219-233: MultipleSetCustomValuescalls within one method may be confusing.The pattern of setting
CustomValuesList(lines 219, 232) to pass data intoCalculateStandardDeviationVolatilityis 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
CalculateStandardDeviationVolatilityreads fromCustomValuesList, 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
hLengthtolengthfor each data point to findhh2/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
RollingMinMaxin 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 = 100and 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
kuses 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: unusedprevDecyclervariableIn
CalculateEhlersSimpleDecycleryou compute:var prevDecycler = GetLastOrDefault(decyclerList); var decycler = currentValue - hp; ... var signal = GetCompareSignal(currentValue - decycler, prevValue - prevDecycler);
prevDecycleris 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 onlyIn
CalculateEhlersDetrendedLeadingIndicator, you seed the EMAs with the current price wheni == 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
📒 Files selected for processing (150)
.gitignoreMODERNIZATION_PLAN.mdOPTIMIZATIONS.mdOoplesFinance.StockIndicators.slnREADME.mdbenchmarks/OoplesFinance.StockIndicators.Benchmarks/BenchmarkDataFactory.csbenchmarks/OoplesFinance.StockIndicators.Benchmarks/BenchmarkProfile.csbenchmarks/OoplesFinance.StockIndicators.Benchmarks/IndicatorBenchmarks.csbenchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csprojbenchmarks/OoplesFinance.StockIndicators.Benchmarks/Program.csbenchmarks/OoplesFinance.StockIndicators.Benchmarks/StreamingPerformanceRunner.csbenchmarks/README.mdbenchmarks/setup-baseline.ps1src/Calculations/BollingerBands.cssrc/Calculations/BollingerBands/BollingerBandsPart1.cssrc/Calculations/BollingerBands/BollingerBandsPart2.cssrc/Calculations/Chande.cssrc/Calculations/Chande/ChandeAtoD.cssrc/Calculations/Chande/ChandeEtoK.cssrc/Calculations/Chande/ChandeLtoR.cssrc/Calculations/Chande/ChandeStoZ.cssrc/Calculations/Demark.cssrc/Calculations/Demark/DemarkEtoK.cssrc/Calculations/Demark/DemarkLtoR.cssrc/Calculations/Demark/DemarkStoZ.cssrc/Calculations/Ehlers.cssrc/Calculations/Ehlers/EhlersAtoD.cssrc/Calculations/Ehlers/EhlersEtoK.cssrc/Calculations/Ehlers/EhlersLtoR.cssrc/Calculations/Ehlers/EhlersStoZ.cssrc/Calculations/Inputs.cssrc/Calculations/Inputs/InputsAtoD.cssrc/Calculations/Inputs/InputsEtoK.cssrc/Calculations/Inputs/InputsLtoR.cssrc/Calculations/Inputs/InputsStoZ.cssrc/Calculations/Macd.cssrc/Calculations/Macd/MacdAtoD.cssrc/Calculations/Macd/MacdEtoK.cssrc/Calculations/Macd/MacdLtoR.cssrc/Calculations/Macd/MacdStoZ.cssrc/Calculations/Momentum.cssrc/Calculations/Momentum/MomentumAtoD.cssrc/Calculations/Momentum/MomentumLtoR.cssrc/Calculations/Momentum/MomentumStoZ.cssrc/Calculations/MovingAverages/EhlersMovingAverages.cssrc/Calculations/MovingAverages/MovingAveragesAtoD.cssrc/Calculations/MovingAverages/MovingAveragesEtoL.cssrc/Calculations/MovingAverages/MovingAveragesMtoR.cssrc/Calculations/MovingAverages/MovingAveragesStoZ.cssrc/Calculations/MovingAverages/StandardMovingAverages.cssrc/Calculations/MovingAvg.cssrc/Calculations/Oscillator.cssrc/Calculations/Oscillators/ClassicOscillators.cssrc/Calculations/Oscillators/OscillatorsAtoD.cssrc/Calculations/Oscillators/OscillatorsEtoK.cssrc/Calculations/Oscillators/OscillatorsLtoR.cssrc/Calculations/Oscillators/OscillatorsStoZ.cssrc/Calculations/PivotPoint.cssrc/Calculations/PivotPoint/PivotPointAtoD.cssrc/Calculations/PivotPoint/PivotPointEtoK.cssrc/Calculations/PivotPoint/PivotPointLtoR.cssrc/Calculations/PivotPoint/PivotPointStoZ.cssrc/Calculations/Ppo.cssrc/Calculations/Ppo/PpoAtoD.cssrc/Calculations/Ppo/PpoEtoK.cssrc/Calculations/Ppo/PpoLtoR.cssrc/Calculations/Ppo/PpoStoZ.cssrc/Calculations/PriceChannel.cssrc/Calculations/PriceChannel/PriceChannelAtoD.cssrc/Calculations/PriceChannel/PriceChannelEtoK.cssrc/Calculations/PriceChannel/PriceChannelLtoR.cssrc/Calculations/PriceChannel/PriceChannelStoZ.cssrc/Calculations/Ratio.cssrc/Calculations/Ratio/RatioAtoD.cssrc/Calculations/Ratio/RatioEtoK.cssrc/Calculations/Ratio/RatioLtoR.cssrc/Calculations/Ratio/RatioStoZ.cssrc/Calculations/Rsi.cssrc/Calculations/Rsi/RsiAtoD.cssrc/Calculations/Rsi/RsiEtoK.cssrc/Calculations/Rsi/RsiLtoR.cssrc/Calculations/Rsi/RsiStoZ.cssrc/Calculations/Stochastic.cssrc/Calculations/Stochastic/StochasticAtoD.cssrc/Calculations/Stochastic/StochasticEtoK.cssrc/Calculations/Stochastic/StochasticLtoR.cssrc/Calculations/Stochastic/StochasticStoZ.cssrc/Calculations/TrailingStop.cssrc/Calculations/TrailingStop/TrailingStopAtoD.cssrc/Calculations/TrailingStop/TrailingStopEtoK.cssrc/Calculations/TrailingStop/TrailingStopLtoR.cssrc/Calculations/Trend.cssrc/Calculations/Trend/TrendAtoD.cssrc/Calculations/Trend/TrendEtoK.cssrc/Calculations/Trend/TrendLtoR.cssrc/Calculations/Trend/TrendStoZ.cssrc/Calculations/Volatility.cssrc/Calculations/Volatility/VolatilityAtoD.cssrc/Calculations/Volatility/VolatilityEtoK.cssrc/Calculations/Volatility/VolatilityLtoR.cssrc/Calculations/Volatility/VolatilityStoZ.cssrc/Calculations/Volume.cssrc/Calculations/Volume/VolumeAtoD.cssrc/Calculations/Volume/VolumeEtoK.cssrc/Calculations/Volume/VolumeLtoR.cssrc/Calculations/Volume/VolumeStoZ.cssrc/Calculations/Wilder.cssrc/Calculations/Wilder/WilderAtoD.cssrc/Calculations/Wilder/WilderStoZ.cssrc/Compatibility/SpanCompat.cssrc/Compatibility/VectorMath.cssrc/Core/MovingAverageCore.cssrc/Enums/IndicatorCost.cssrc/Helpers/CalculationsHelper.cssrc/Helpers/PooledRingBuffer.cssrc/Helpers/RollingWindow.cssrc/Models/IndicatorOptions.cssrc/Models/StockData.cssrc/OoplesFinance.StockIndicators.csprojsrc/Streaming/BarAggregator.cssrc/Streaming/BarTimeframe.cssrc/Streaming/IndicatorCostMap.cssrc/Streaming/IndicatorRegistry.cssrc/Streaming/Providers/Alpaca/AlpacaStreamSource.cssrc/Streaming/Providers/Alpaca/AlpacaStreamingSession.cssrc/Streaming/Providers/Alpaca/IAlpacaStreamClient.cssrc/Streaming/StatefulIndicators.cssrc/Streaming/StreamingIndicatorEngine.cssrc/Streaming/StreamingIndicatorState.cssrc/Streaming/StreamingInputSelector.cssrc/Streaming/StreamingInterfaces.cssrc/Streaming/StreamingModels.cssrc/Streaming/StreamingOptions.cssrc/Streaming/StreamingSession.cstests/CalculationsTests/BollingerBandsTests.cstests/CalculationsTests/EhlersTests.cstests/CalculationsTests/MovingAverageTests.cstests/CalculationsTests/OscillatorTests.cstests/CalculationsTests/RsiTests.cstests/CalculationsTests/WilderTests.cstests/ModelsTests/StockDataLazyViewTests.cstests/StreamingTests/BarAggregatorTests.cstests/StreamingTests/IndicatorCostMapTests.cstests/StreamingTests/IndicatorRegistryTests.cstests/StreamingTests/StreamingIndicatorEngineTests.cstests/StreamingTests/StreamingInputSelectorTests.cstests/StreamingTests/StreamingOptionsTests.cstests/StreamingTests/StreamingParityTests.cstests/StreamingTests/StreamingStatefulParityTests.cstests/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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:IndicatorNameis swapped + SimpleWindow outputs the wrong series +length/2int-division.
- Line 1129: TriangleWindow sets
IndicatorName.EhlersSimpleWindowIndicator(should be Triangle).- Line 1181: SimpleWindow sets
IndicatorName.EhlersTriangleWindowIndicator(should be Simple).- Line 1176/1180: SimpleWindow emits
filtListbut signals were computed offfiltMa2List.- Line 1116 and Line 1168:
length / 2is 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
NestedProjectssection correctly nestsOoplesFinance.StockIndicators.BenchmarksunderbenchmarksandOoplesFinance.StockIndicators.DevConsoleunderexamples. However, the core project (OoplesFinance.StockIndicators) is not nested under thesrcfolder 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
testssolution 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
usingwhich will dispose it, butsession.Stop()is never called before disposal. While this may work correctly ifDisposeinternally callsStop, 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:Disposeshould be idempotent and prevent restart after disposal.Currently,
Dispose()only callsStop(), but ifStart()is called again afterDispose(), the subscription could potentially be restarted (since_startedisn'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 variableprevFilt.At Line 709,
prevFiltis assigned viaGetLastOrDefault(filtList)but is never used—the signal comparison on Line 713 usesprevFilter1instead.♻️ 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 ifoptimizedMeanis 0.At Line 86,
$speedup = $baselineMean / $optimizedMeanwill throw if$optimizedMeanis 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 / $optimizedMeansrc/Calculations/Ehlers/EhlersLtoR.cs (2)
76-123: PreferMath.Atan2(imagPart, realPart)overAtan(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 allocatesxArrayper bar.
If this is performance-sensitive, consider reusing a single buffer (or avoid the buffer and read directly frominputListin the nested loops).
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (49)
MODERNIZATION_PLAN.mdOoplesFinance.StockIndicators.slnREADME.mdSTATEFUL_INDICATOR_CHECKLIST.mdbenchmarks/OoplesFinance.StockIndicators.Benchmarks/IndicatorBenchmarks.csbenchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csprojbenchmarks/README.mdbenchmarks/update-performance-chart.ps1examples/OoplesFinance.StockIndicators.DevConsole/OoplesFinance.StockIndicators.DevConsole.csprojexamples/OoplesFinance.StockIndicators.DevConsole/Program.cssrc/Calculations/Ehlers/EhlersAtoD.cssrc/Calculations/Ehlers/EhlersLtoR.cssrc/Calculations/Ehlers/EhlersStoZ.cssrc/Calculations/MovingAverages/EhlersMovingAverages.cssrc/Calculations/MovingAverages/MovingAveragesAtoD.cssrc/Calculations/Volume/VolumeEtoK.cssrc/Helpers/RollingWindow.cssrc/OoplesFinance.StockIndicators.csprojsrc/Streaming/StatefulIndicators.Batch10.cssrc/Streaming/StatefulIndicators.Batch11.cssrc/Streaming/StatefulIndicators.Batch12.cssrc/Streaming/StatefulIndicators.Batch13.cssrc/Streaming/StatefulIndicators.Batch14.cssrc/Streaming/StatefulIndicators.Batch15.cssrc/Streaming/StatefulIndicators.Batch16.cssrc/Streaming/StatefulIndicators.Batch17.cssrc/Streaming/StatefulIndicators.Batch18.cssrc/Streaming/StatefulIndicators.Batch19.cssrc/Streaming/StatefulIndicators.Batch20.cssrc/Streaming/StatefulIndicators.Batch21.cssrc/Streaming/StatefulIndicators.Batch22.cssrc/Streaming/StatefulIndicators.Batch23.cssrc/Streaming/StatefulIndicators.Batch24.cssrc/Streaming/StatefulIndicators.Batch25.cssrc/Streaming/StatefulIndicators.Batch26.cssrc/Streaming/StatefulIndicators.Batch27.cssrc/Streaming/StatefulIndicators.Batch28.cssrc/Streaming/StatefulIndicators.Batch5.cssrc/Streaming/StatefulIndicators.Batch6.cssrc/Streaming/StatefulIndicators.Batch7.cssrc/Streaming/StatefulIndicators.Batch8.cssrc/Streaming/StatefulIndicators.Batch9.cssrc/Streaming/StatefulIndicators.cssrc/Streaming/StreamingIndicatorEngine.cssrc/Streaming/StreamingMultiSeries.cssrc/Streaming/StreamingSession.cstests/OoplesFinance.StockIndicators.Tests.Unit.csprojtests/StreamingTests/StreamingIndicatorEngineTests.cstests/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_COUNTSandOOPLES_BENCHMARK_LENGTHSare a nice touch for flexibility.One minor note: Line 29 references
net10.0which 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.InvariantCultureensures 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
SpreadStateimplementation is a clean example of a multi-series indicator. It correctly usesTryGetLatestto handle missing data and returns appropriate results.
635-664: LGTM!The
ReplayStreamEventstruct 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
ParamsSourceforCountsandLengthsprovides good flexibility. The environment variable fallback chain (OOPLES_BENCHMARK_COUNTS→BENCHMARK_COUNTS→ defaults) is a nice touch for CI/CD integration.
52-85: LGTM!The
LoadIntOverridesimplementation is robust—handles multiple delimiters, validates positive values, deduplicates, and sorts the results.
87-104: LGTM!Proper setup/teardown pattern with
GlobalSetupfor data creation andIterationSetupfor state reset between iterations.
448-461: No changes needed—configuration is correct for benchmarks using IterationSetup.The
WithInvocationCount(1)andWithUnrollFactor(1)settings are not optional here. BenchmarkDotNet forces these values whenIterationSetupis 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
lagIndexcalculation now correctly usesMath.Max(i - halfP, 0)at Line 173, which properly clamps to prevent negative indices while accessing lagged values.
1492-1544:RollingSuminstances created without window size.Similar to the previous comment, Lines 1499-1500 create
RollingSuminstances 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 aboutRollingSumwindow size.
RollingSumuses a cumulative sum pattern where the internal_cumulativelist stores running totals, allowing O(1) calculation of any window sum. The list grows proportionally to the number ofAdd()calls (bounded bystockData.Count), not unboundedly. TheAverage(int length)method correctly respects the length parameter and handles partial windows viaMath.Min(length, _cumulative.Count). The window is controlled by thelength2parameter (50), not by constructor configuration.
206-300: Add unit tests forCalculateEhlersMedianAverageAdaptiveFiltercovering edge cases.The adaptive filter logic modifies the
windowTreein-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 wherelenreduces significantly or whenavailable < 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
SpreadStateprovides 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 * lengththen divides byperiod(avoids/ 0.5 * lengthpitfall).- 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 inEhlersLtoR.cs:13CalculateEhlersRoofingFilterV1: 1 definition inEhlersLtoR.cs:621CalculateEhlersCenterofGravityOscillator: 1 definition inEhlersAtoD.cs:151Likely an incorrect or invalid review comment.
src/Calculations/Ehlers/EhlersAtoD.cs (4)
550-609: Anticipate:startupdate 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.
Using2 * Math.PI / (2 * length1)avoids the left-to-right/ 2 * length1bug.
825-887: Good: SpearmanRank + TruncatedBandPassFilter now size arrays fromlength(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.
There was a problem hiding this comment.
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:IndicatorNameassignments are swapped
CalculateEhlersTriangleWindowIndicatorsetsEhlersSimpleWindowIndicator(Line 1129) andCalculateEhlersSimpleWindowIndicatorsetsEhlersTriangleWindowIndicator(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: preferMath.Atan2overMath.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-barnew double[length+1]allocations
xArrayis 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
lengthis odd,length / 2truncates (Line 1116 / 1168). If you intended half-length as a double, use0.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
📒 Files selected for processing (3)
src/Calculations/Ehlers/EhlersAtoD.cssrc/Calculations/Ehlers/EhlersLtoR.cssrc/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 intermediateCustomValuesListbeing availableSeveral methods immediately consume
.CustomValuesListfrom 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.
There was a problem hiding this comment.
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: clamplength2to prevent negativexxArray[...]indexing.In Line 824–831,
xxArray[upperLength + j - k]can go negative whenlength2 > upperLength + 1, causingIndexOutOfRangeException.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 whenlength == 1(Log(1)=0).Line 1442 can hit
0/0whenlength == 1andratioSum == 1, producingNaNthatMinOrMax(...)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 whenlength2 == 1(NaN risk).Line 341 divides by
Math.Log(length2); withlength2 == 1, this becomes divide-by-zero and can produceNaN(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:IndicatorNameassignments appear swapped.
- Line 1131:
CalculateEhlersTriangleWindowIndicatorsetsEhlersSimpleWindowIndicator.- Line 1183–1184:
CalculateEhlersSimpleWindowIndicatorsetsEhlersTriangleWindowIndicator.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-barnew double[length + 1]in Noise Elimination Technology hot loop.Line 1049 allocates an array for every bar;
lengthis 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 largestockData.Count, this can produce avoidable GC pressure.Also applies to: 909-917
📜 Review details
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
src/Calculations/Ehlers/EhlersAtoD.cssrc/Calculations/Ehlers/EhlersLtoR.cssrc/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.
MinPastValueshas no special assumptions about itsminIndexparameter. Whenlength1 == 1, passingminIndex = 0is valid: the conditioncurrentIndex >= 0always evaluates to true in the loop and the method returns the expected value. The parameter validationMath.Max(length1, 1)ensureslength1 >= 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.
There was a problem hiding this comment.
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 whenlength2 = 1On line 345, the
dbcalculation usesMath.Log(...) / Math.Log(length2). Whenlength2 = 1,Math.Log(1) = 0, which causes division by zero and producesNaN/Infinity.The past review comment suggested changing
Math.Max(length2, 1)toMath.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:CalculateEhlersSimpleWindowIndicatorsets wrongIndicatorNameThe method
CalculateEhlersSimpleWindowIndicatorincorrectly setsIndicatorName.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 whenlength = 1On line 1442, the alpha calculation uses
Math.Log(ratioSum) / Math.Log(length). Whenlength = 1,Math.Log(1) = 0, which causes division by zero and producesNaN/Infinity.The past review comment suggested changing
Math.Max(length, 1)toMath.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
📒 Files selected for processing (3)
src/Calculations/Ehlers/EhlersAtoD.cssrc/Calculations/Ehlers/EhlersLtoR.cssrc/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 implementationThe
CalculateEhlersRoofingFilterV2method 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 issuesThe buffers are now correctly sized based on parameters and allocated once before the main loop, with proper clearing on each iteration. The
upperLength >= 2constraint andlength3 >= length1validation prevent edge-case index issues.
1532-1629: LGTM - Helper methods correctly use try/finally for state restorationBoth
GetCustomValuesListInternalandGetOutputValuesInternalnow 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 calculationThe use of
GetCustomValuesListInternalensures thatCustomValuesListis populated even when the consumer has disabled custom values viaIndicatorOptions. This pattern is consistently applied throughout the file.
513-514: LGTM - Precedence issue fixed with proper parenthesesThe expressions now correctly use
2 * Math.PI / (2 * length1)with parentheses around the denominator, ensuring the angle is inversely proportional tolength1as intended for band-pass filter calculations.
838-839: LGTM - Buffer sizing now based on input parameterThe arrays are now allocated with size
length + 1based on the input parameter rather than a fixed size, eliminating the potentialIndexOutOfRangeExceptionfor larger length values.
1071-1073: LGTM - Math.Log safety check properly implementedThe code now correctly guards against invalid logarithm inputs by checking
temp > 0before callingMath.Log10(temp), preventingNaNor-Infinitypropagation.src/Calculations/Ehlers/EhlersAtoD.cs (3)
13-92: LGTM!The
CalculateEhlersAdaptiveCyberCycleimplementation correctly handles boundary conditions, division by zero checks, and follows the expected pattern for Ehlers indicators.
1-5: Overall structure looks goodThe file implements a comprehensive set of Ehlers indicators following consistent patterns:
- Proper parameter bounds validation with
Math.Max- Safe nested indicator access via
GetOutputValuesInternalandGetCustomValuesListInternal- 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 valuesprevBp1andprevBp2(lines 1213-1214) are fetched once before the candidate-period loop and reused for all periodsj. While period-specific coefficients (alpha, beta, gamma) are recalculated for eachj, the band-pass state should ideally maintain independent history per period to correctly estimate each candidate frequency's energy. Currently, only the finaljiteration'sbpis 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 separatebpstate 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.
There was a problem hiding this comment.
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 returnnullrepeatedly, causing the loop to spin. Consider breaking onnullor 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
StackOverflowExceptionduring 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
0as 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 computeSMA - 0instead of indicating missing data.Consider using
double.NaNas 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
updateslist is mutated directly from the callback (line 485), which is safe forInlineprocessing but would cause race conditions ifBufferedmode were used (callbacks could come from different threads).Since this is demo code and the example explicitly uses
Inlinemode, 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
📒 Files selected for processing (2)
examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketches.csexamples/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 inStart()- 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 structwith 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 ofCultureInfo.InvariantCulturefor 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
TryGetSeriespattern.
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 structfor 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.
There was a problem hiding this comment.
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
📒 Files selected for processing (11)
examples/OoplesFinance.StockIndicators.DevConsole/FacadeSketches.csexamples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV2.csexamples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV3.csexamples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV4.csexamples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV5.csexamples/OoplesFinance.StockIndicators.DevConsole/FacadeSketchesV6.csexamples/OoplesFinance.StockIndicators.DevConsole/Program.cssrc/Streaming/StatefulIndicators.Batch21.cssrc/Streaming/StatefulIndicators.Batch24.cssrc/Streaming/StatefulIndicators.Batch26.cssrc/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.InvariantCulturefor formatting is a good practice.
26-94: LGTM!Both demo methods demonstrate clean builder patterns. The capture pattern for
SeriesHandlevariables (lines 69-71, 113) is safe since theConfigureIndicatorscallback is invoked synchronously within the builder chain.
96-135: LGTM!The streaming demo is well-structured. Using
StreamingProcessingMode.Inlineensures 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
StockIndicatorBuilderimplements a clean fluent builder pattern. Creating defensive copies of_nodesand_keysinBuild()(lines 314-315) prevents external mutation of internal state.
409-424: LGTM!The
Formulamethod 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 returningdouble.NaNis a safe fallback.
554-597: LGTM!The
SignalRuleclass implements correct threshold crossing logic. TheIsCrossTriggeredmethod 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
Resolvemethod 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
SeriesHandleandIndicatorKeystructs correctly implementIEquatable<T>with consistentEqualsandGetHashCodeoverrides, making them suitable for use as dictionary keys. The use ofHashCode.CombineinIndicatorKey.GetHashCode()is the idiomatic approach.
1504-1589: LGTM!The
ReplayStreamSourcedemonstrates proper defensive coding with null checks inSubscribe(lines 1515-1523). TheReplayStreamSubscriptioncorrectly implements the disposal pattern whereDisposecallsStop, 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
ExtractStreamingValuemethod properly handles the case whereupdate.Outputsis null (lines 1629-1631).
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
There was a problem hiding this comment.
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
shellorpowershellas appropriate.Proposed fix
Run optimized benchmarks: -``` +```shell dotnet run -c Release --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csprojRun a subset:
-+shell
dotnet run -c Release --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -- --filter SMARun streaming fanout throughput/latency: -``` +```shell dotnet run -c Release --project benchmarks/OoplesFinance.StockIndicators.Benchmarks/OoplesFinance.StockIndicators.Benchmarks.csproj -- --streaming-perf --ticks 100000Use 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 --extendedCompare 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 buildAlso 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 throwingArgumentOutOfRangeExceptionfor 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.
MinutesandHoursdon'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
Customfactory doesn't validate thatsymbolis meaningful. If a defaultSymbolId(with emptyValue) is passed, it would create an invalid benchmark configuration. Per theSymbolIdconstructor inSymbolId.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 throwInvalidCastExceptionif theOptionstype doesn't match the expected type for the indicator. Consider using pattern matching withisor 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 withSeriesEvaluator.GetOutputKey.This
GetOutputKeyimplementation is identical to the one inSeriesEvaluator.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
GetOutputKeyto a shared location (e.g.,IndicatorOutputMapperclass) that bothStreamingIndicatorFactoryandSeriesEvaluatorcan 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[] namesarray is stored directly asIReadOnlyList<IndicatorName>. WhileIReadOnlyList<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
IndicatorKeyinstances 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 asstatic readonlyfields 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
Executemethod 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.
AutoTradeAdapterBuilderandAutoTradeRuleBuilderare readonly structs. If default-constructed (e.g.,default(AutoTradeAdapterBuilder)), the_catalogand_adapterfields will be null, causingNullReferenceExceptionwhen 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
IndicatorandCreateFormulaacceptspec/formulawithout 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 ofGenericIndicatorOptions.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 thenBuild()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 ofParameterInfo[].Exposing
System.Reflection.ParameterInfo[]directly in the public API couples consumers to reflection types. A custom DTO withName,Type,HasDefaultValue,DefaultValuewould provide a cleaner abstraction and allow future changes without breaking the API.
431-440:AllandCorepresets have identical behavior.Both presets apply the same four indicators. If this is placeholder code, consider adding a TODO; otherwise,
Coreshould likely be a subset ofAllwith fewer indicators, orAllshould include more indicators.
476-549:IndicatorChainexposes fewer indicators thanIndicatorCatalog.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 expandingIndicatorChainfor parity or documenting thatInputshould be used with the catalog for additional indicators.
There was a problem hiding this comment.
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 inFromBatchfactory method.Missing
ArgumentNullExceptionguard for thedataparameter. Passingnullwill create anIndicatorDataSourcewithKind = BatchbutBatchData = null, causing downstream failures.
118-121: Validate null input inFromStreamingfactory method.Missing
ArgumentNullExceptionguard for thesourceparameter, similar to theFromBatchissue.src/Builder/IndicatorRuntime.cs (4)
290-307: Batch mode may emit nothing whenEmitWarmupis false.When
_behavior.EmitWarmupis false, the first (and only) batch snapshot is discarded byPublish(), leavingLatestas null and never invokingUpdated. This effectively makes batch mode non-functional with warmup suppression.
309-355:StreamingSessiondisposed prematurely due tousingstatement.The
using var sessionon line 320 disposes the session whenStartStreaming()returns, butsession.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 inIndicatorRuntime.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 forToListconsistency.
ToArrayefficiently usesAsSpan().CopyTo(), butToListiterates 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
countandarrayprovides 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 inMergeStockData.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.IsHardwareAcceleratedat 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; }
There was a problem hiding this comment.
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.
AddAdapteraccepts null and stores it, which can surface as an NRE later. Consider adding anArgumentNullExceptionguard at the start.
There was a problem hiding this comment.
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 passnode.SeriesKeyintoGetBaseInput(...)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 withStreamingIndicatorFactoryto 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; }
There was a problem hiding this comment.
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: nullmeans 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 withpool: null.src/Builder/IndicatorRuntime.cs (4)
309-355:StreamingSessiondisposed immediately afterStart()returns—session lifetime issue persists.The
using var sessionon line 320 will dispose the session whenStartStreaming()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 newIndicatorSnapshot. 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 whenEmitWarmupis false.When
!_behavior.EmitWarmup && !_hasEmitted,Publishreturns early and sets_hasEmitted = truewithout settingLatest. In batch mode, there's only onePublishcall, soLatestremains null andUpdatedis 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(), callPublish(snapshot, skipWarmupCheck: true).
587-617: Notification dispatch uses fire-and-forget async with Console logging only.The current implementation wraps
NotifyAsyncinTask.Runwith a catch that logs toConsole.WriteLine. This addresses error isolation (notifications won't abort the runtime), but:
- Trade adapter execution (line 615) is still synchronous and can throw, aborting the loop.
Console.WriteLinemay 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:RequiredPercentvalues 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:HttpResponseMessagenot 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:FormUrlEncodedContentnot disposed in SMS channel.While
HttpRequestMessageis disposed (line 288),FormUrlEncodedContent(line 280) andHttpResponseMessage(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:HttpResponseMessagenot 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
barsto a minimum of 1 usingMath.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:
- Document the clamping behavior in the XML doc
- Throw
ArgumentOutOfRangeExceptionfor invalid inputs to fail fastOption 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.
MinutesandHoursaccept negative integers, which would create negativeTimeSpanvalues—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
_countis nearint.MaxValue, expressions like_count + 1(line 269) or_count + items.Length(line 280) could overflow, resulting in a negative value passed toEnsureCapacity. 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: Returnruntime.Latest ?? new object()may hide null results.If
runtime.Latestis null afterStart(), returningnew object()prevents BenchmarkDotNet from optimizing away the call, but it masks potential issues where batch computation didn't produce results. Consider asserting or logging ifLatestis unexpectedly null during benchmark runs.src/Builder/IndicatorRuntime.cs (1)
240-262: Nested lock acquisition inGetOrCreateBuffercould be simplified.The method acquires
_buffersLock, then within that lock acquires_statsLocktwice (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 betweenSignalCondition.IsActiveandSignalRule.IsActive.Both have similar switch expressions for
AboveandBelowtriggers.SignalConditionadditionally handlesBetweenandOutside, whileSignalRuledoes not. This could lead to maintenance issues if logic changes.Consider having
SignalRuledelegate to aSignalConditioninternally 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
MarkdownV2with comprehensive escaping or usingHTMLparse 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>
There was a problem hiding this comment.
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
WrapPooledArrayis used withpool: null,EnsureCapacitycan still return the original (non-pooled) array toSharedPool, corrupting the pool. Guard the return with_ownsArrayand 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
AlpacaTradeAdapternow 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:HttpResponseMessagenot 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:HttpResponseMessageandFormUrlEncodedContentnot disposed in SMS channel.The
contentvariable (line 280) andresponsevariable (line 292) are not disposed, which can leak resources in long-running processes. The past review flagged this, but onlyHttpRequestMessagehasusing.🐛 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
optionswithArgumentNullException, addressing the previous review feedback.src/Builder/StockIndicatorBuilder.cs (1)
74-81: Defaults flag reset is now in place.The
_defaultsApplied = falseassignment on line 77 ensures defaults are properly reapplied whenConfigureIndicatorsis called with new options.src/Builder/Signals/SignalTypes.cs (2)
343-351: Percent threshold clamping is now in place.Line 348 properly clamps
RequiredPercentto [0, 100] range usingMath.Min(100, Math.Max(0, RequiredPercent ?? 0)).
469-475: Range rule evaluation is now wired up.
SignalRangeRuleobjects are now passed toIndicatorRuntimevia_signals.BuildRangeRules()inStockIndicatorBuilder.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.NaNwheny == 0.
🧹 Nitpick comments (5)
src/Builder/IndicatorDataSource.cs (1)
136-157: Consider validating empty/whitespace symbol inFor(string)overload.The
For(string symbol)method passes the string directly toSymbolIdconstructor. Based on the relevant snippet showingSymbolIdconverts 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:
- Exposing an error event/callback
- Using a proper logging abstraction
- 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 inGetParameter<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
TryGetParametervariant that returns a success flag.src/Builder/StockIndicatorBuilder.cs (1)
379-409: Consider iterative approach forAddDependenciesto 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 toIchimokumethod.Unlike
MacdandBollingerBands, theIchimokumethod doesn't acceptIndicatorKeyparameters 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>
There was a problem hiding this comment.
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 StochasticSignaltoSignalFastKand omits any Ichimoku mapping for a Chikou/Histogram output. If the calculation keys differ, extraction will fall back toCustomValuesListand return the wrong series. Please confirm the actualOutputValueskeys 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.
FormUrlEncodedContentandHttpResponseMessagearen’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.
StringContentandHttpResponseMessagearen’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 copyingparameters.
GenericIndicatorOptionsstores 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; + }
- 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>
There was a problem hiding this comment.
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-latestis required fornet461, you could add a matrix to also testnet10.0on 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: trueto work, you need lock files. Alternatively, useactions/cachewith the NuGet packages path.
- 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>
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>
|
There was a problem hiding this comment.
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 validatedAlso applies to: 42-42, 61-61, 71-71, 82-82, 230-230
src/Attributes/ValidationAttributes.cs (2)
44-47: Consider using an enum forPriorityto 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:CanBeNegativecould be inferred fromMinValue.When
MinValue >= 0, havingCanBeNegative = true(the default) is logically inconsistent. Consider computing this fromMinValueor adding documentation noting that users should set it explicitly whenMinValue >= 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 withAddChannel.
AddChannelcorrectly validates for null (line 91), butSms,Webhook, andWebSocket(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 theuriparameter before creatingWebSocketOptions.♻️ 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: ValidatesignalNameOrPatternparameter.If
signalNameOrPatternis null or empty, downstream pattern matching inNotificationRoute.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 byCount - 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 propertiesMethodandObjective.These properties are declared but not used by
GridSearch. Consider either:
- Using them to select behavior within
GridSearch(e.g., forMinDrawdown, negate the objective)- Removing them until
RandomSearch/GeneticAlgorithmare implemented- Documenting they are reserved for future use
544-566: Consider reusing the existingBartype or extracting a shared OHLCV interface.
BarDataduplicates the OHLCV pattern already defined inBar(src/Builder/MarketData/MarketDataTypes.cs). WhileBarincludes additional fields (symbol, vwap), the core OHLCV structure is redundant. To reduce duplication:
- Make symbol optional in
Barand reuse it here, or- Extract a common OHLCV interface that both
BarandBarDataimplement
| public IndicatorBoundsAttribute(double minValue, double maxValue) | ||
| { | ||
| MinValue = minValue; | ||
| MaxValue = maxValue; | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| // 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; |
There was a problem hiding this comment.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| 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], |
There was a problem hiding this comment.
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) = 0upperIndex = (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.
| 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.
| 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 | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if OpenPosition/CheckEntrySignals are wired elsewhere.
rg -n "OpenPosition\s*\(" --type=cs
rg -n "CheckEntrySignals\s*\(" --type=csRepository: 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 2Repository: 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.
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| /// <summary> | ||
| /// Gets the net profit/loss (GrossPnL - Fees - Slippage). | ||
| /// </summary> | ||
| public double NetPnL => GrossPnL - Fees - Slippage; | ||
|
|
There was a problem hiding this comment.
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.
| /// <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.
| 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; | ||
| } | ||
| } |
There was a problem hiding this comment.
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).
| ### 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 | ||
|
|
There was a problem hiding this comment.
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.
| ``` | ||
| src/Core/Registry/ | ||
| ├── IMovingAverageCore.cs # Interface definition | ||
| ├── MovingAverageRegistry.cs # Dictionary registry | ||
| └── Implementations/ # Optional subfolder for struct implementations | ||
| ├── SmaCore.cs | ||
| ├── EmaCore.cs | ||
| └── ... | ||
| ``` |
There was a problem hiding this comment.
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.




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
StockIndicatorBuilderpattern replacesCalculate*extension methodsIndicatorBuffer<T>withArrayPoolfor hot path performanceIAsyncEnumerable<T>for streaming scenariosPerformance Targets
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
All story branches should:
feature/performance-and-refactoringfeature/performance-and-refactoringWhen all stories are complete, this PR merges to master as v2.0.
Breaking Changes
Calculate*methods deprecated (still functional, marked[Obsolete])StockIndicatorBuilderis the primary APIIndicatorBuffer<T>(implementsIReadOnlyList<T>)See #126 for migration guide.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Indicators
Performance & Tooling
Documentation
Project
✏️ Tip: You can customize this high-level summary in your review settings.