From a9e72dae0d9c0c1c54b950e08a19a3d45d8865f8 Mon Sep 17 00:00:00 2001 From: Miha Kralj Date: Tue, 27 Jan 2026 23:48:33 -0800 Subject: [PATCH] Refactor and enhance various channel indicators for improved performance and stability - Updated Codacy instructions to streamline usage guidelines. - Refactored Bbands class to utilize ArrayPool for memory management, preventing stack overflow on large series. - Changed Fcb class to use long for monotonic deques to avoid truncation issues. - Enhanced Kchannel class to ensure safe defaults for non-finite values. - Improved Maenv class to prevent double-priming during calculations. - Modified Mmchannel class to ensure non-negative buffer indices and removed unnecessary state tracking. - Updated Pchannel class to correctly reference IsHot state. - Refined Regchannel class to avoid double-processing during calculations. - Enhanced Starchannel class to sanitize non-finite values during calculations. - Adjusted Stbands.Quantower.cs to allow finer control over multiplier precision. - Updated Ubands class to only update last valid values on new bars. - Modified Uchannel.Quantower.cs to allow for finer multiplier precision. - Enhanced Vwapbands classes to include standard deviation calculations and ensure consistent array lengths. - Refactored Vwapsd classes to include standard deviation outputs and ensure consistent array lengths. - Updated MonotonicDeque to use long for indices to prevent overflow. - Improved Mdape class to handle zero actual values with a substitute value for error calculation. - Enhanced Rae class to ensure correct state management during updates. - Refined Wmape class to simplify the logic for finding last valid actual and predicted values. - Updated Cmf.Quantower classes to ensure MinHistoryDepths reflects the current period. --- .coderabbit.batch2-tests.yaml | 66 ++++++++++++++ .coderabbit.batch2b-validation.yaml | 45 ++++++++++ .coderabbit.yaml | 88 +++++++++++++++++++ .github/instructions/codacy.instructions.md | 7 -- lib/channels/bbands/Bbands.cs | 82 ++++++++++++----- lib/channels/fcb/Fcb.cs | 27 +++--- lib/channels/kchannel/Kchannel.cs | 21 ++++- lib/channels/maenv/Maenv.cs | 5 +- lib/channels/mmchannel/Mmchannel.cs | 19 ++-- lib/channels/pchannel/Pchannel.cs | 2 +- lib/channels/regchannel/Regchannel.cs | 4 +- lib/channels/starchannel/Starchannel.cs | 33 +++++++ lib/channels/stbands/Stbands.Quantower.cs | 2 +- lib/channels/ubands/Ubands.cs | 10 ++- lib/channels/uchannel/Uchannel.Quantower.cs | 2 +- lib/channels/vwapbands/Vwapbands.Tests.cs | 8 +- .../vwapbands/Vwapbands.Validation.Tests.cs | 3 +- lib/channels/vwapbands/Vwapbands.cs | 13 ++- lib/channels/vwapsd/Vwapsd.Quantower.cs | 24 ++++- lib/channels/vwapsd/Vwapsd.Tests.cs | 10 ++- .../vwapsd/Vwapsd.Validation.Tests.cs | 3 +- lib/channels/vwapsd/Vwapsd.cs | 11 ++- lib/core/collections/MonotonicDeque.cs | 16 ++-- lib/errors/mdape/Mdape.Tests.cs | 17 ++-- lib/errors/mdape/Mdape.cs | 11 ++- lib/errors/rae/Rae.cs | 17 ++-- lib/errors/wmape/Wmape.cs | 24 ++--- lib/volume/cmf/Cmf.Quantower.Tests.cs | 4 +- lib/volume/cmf/Cmf.Quantower.cs | 4 +- 29 files changed, 464 insertions(+), 114 deletions(-) create mode 100644 .coderabbit.batch2-tests.yaml create mode 100644 .coderabbit.batch2b-validation.yaml create mode 100644 .coderabbit.yaml diff --git a/.coderabbit.batch2-tests.yaml b/.coderabbit.batch2-tests.yaml new file mode 100644 index 00000000..7e24f803 --- /dev/null +++ b/.coderabbit.batch2-tests.yaml @@ -0,0 +1,66 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +# CodeRabbit Configuration - Batch 2: Test files only +# To use: rename to .coderabbit.yaml (backup current config first) +# +# Test files breakdown (actual counts): +# - Unit tests (*.Tests.cs excluding Validation): 296 files +# - Validation tests (*.Validation.Tests.cs): 130 files +# +# Both batches fit under 300 limit: +# - Batch 2a: Unit tests only (296 files) - THIS CONFIG +# - Batch 2b: Validation tests (130 files) - see .coderabbit.batch2b-validation.yaml + +language: en-US + +reviews: + request_changes_workflow: false + high_level_summary: true + high_level_summary_placeholder: "@coderabbitai summary" + auto_title_placeholder: "@coderabbitai" + review_status: true + collapse_walkthrough: false + path_instructions: [] + tools: + ast-grep: + essential_rules: true + rule_dirs: + - .coderabbit/ast-grep-rules + + path_filters: + # ============================================ + # BATCH 2: Test files only (426 total) + # Split into 2a (unit) and 2b (validation) if needed + # ============================================ + + # INCLUDE: Unit test files only (Batch 2a - 296 files) + - "lib/**/*.Tests.cs" + - "quantower/**/*.Tests.cs" + + # EXCLUDE: Validation tests (Batch 2b) + - "!**/*.Validation.Tests.cs" + + # EXCLUDE: Build artifacts + - "!**/obj/**" + - "!**/bin/**" + - "!**/Debug/**" + - "!**/Release/**" + + # EXCLUDE: Non-code files + - "!**/*.md" + - "!**/*.pine" + - "!**/*.json" + - "!**/*.yaml" + - "!**/*.yml" + +chat: + auto_reply: true + +# ============================================== +# BATCH 2b: Validation tests +# ============================================== +# If you need to review validation tests separately: +# path_filters: +# - "lib/**/*.Validation.Tests.cs" +# - "!**/obj/**" +# - "!**/bin/**" +# ============================================== \ No newline at end of file diff --git a/.coderabbit.batch2b-validation.yaml b/.coderabbit.batch2b-validation.yaml new file mode 100644 index 00000000..bb0d533e --- /dev/null +++ b/.coderabbit.batch2b-validation.yaml @@ -0,0 +1,45 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +# CodeRabbit Configuration - Batch 2b: Validation test files only +# To use: rename to .coderabbit.yaml (backup current config first) +# +# Validation tests: 130 files (well under 300 limit) + +language: en-US + +reviews: + request_changes_workflow: false + high_level_summary: true + high_level_summary_placeholder: "@coderabbitai summary" + auto_title_placeholder: "@coderabbitai" + review_status: true + collapse_walkthrough: false + path_instructions: [] + tools: + ast-grep: + essential_rules: true + rule_dirs: + - .coderabbit/ast-grep-rules + + path_filters: + # ============================================ + # BATCH 2b: Validation test files only (130 files) + # ============================================ + + # INCLUDE: Validation test files + - "lib/**/*.Validation.Tests.cs" + + # EXCLUDE: Build artifacts + - "!**/obj/**" + - "!**/bin/**" + - "!**/Debug/**" + - "!**/Release/**" + + # EXCLUDE: Non-code files + - "!**/*.md" + - "!**/*.pine" + - "!**/*.json" + - "!**/*.yaml" + - "!**/*.yml" + +chat: + auto_reply: true \ No newline at end of file diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000..d3de5d10 --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,88 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +# CodeRabbit Configuration - Batch 1: Implementation files only +# PR has 492 files, limit is 300. This config focuses on core implementation. + +language: en-US + +reviews: + request_changes_workflow: false + high_level_summary: true + high_level_summary_placeholder: "@coderabbitai summary" + auto_title_placeholder: "@coderabbitai" + review_status: true + collapse_walkthrough: false + path_instructions: [] + tools: + ast-grep: + essential_rules: true + rule_dirs: + - .coderabbit/ast-grep-rules + + path_filters: + # ============================================ + # BATCH 1: Core implementation .cs files only + # Target: ~297 files (under 300 limit) + # ============================================ + + # INCLUDE: Core library implementation files + - "lib/**/*.cs" + + # EXCLUDE: Test files (426 files - Batch 2) + - "!**/*.Tests.cs" + - "!**/*.Validation.Tests.cs" + - "!**/Tests/**" + + # EXCLUDE: Build artifacts + - "!**/obj/**" + - "!**/bin/**" + - "!**/Debug/**" + - "!**/Release/**" + + # EXCLUDE: Documentation (not code review priority) + - "!**/*.md" + - "!**/*.pine" + - "!**/_index.md" + + # EXCLUDE: Quantower adapters (Batch 2) + - "!quantower/**" + + # EXCLUDE: Supporting directories + - "!perf/**" + - "!ndepend/**" + - "!docfx/**" + - "!temp/**" + - "!TestResults/**" + - "!TestResultsCoverage/**" + - "!.sarif/**" + + # EXCLUDE: Config/metadata files + - "!**/*.json" + - "!**/*.yaml" + - "!**/*.yml" + - "!**/*.props" + - "!**/*.targets" + - "!**/*.csproj" + - "!**/*.runsettings" + - "!**/*.editorconfig" + +chat: + auto_reply: true + +# ============================================== +# BATCH 2 INSTRUCTIONS (for future PR): +# ============================================== +# To review test files, create a separate PR or +# modify path_filters to: +# - "lib/**/*.Tests.cs" +# - "lib/**/*.Validation.Tests.cs" +# - "quantower/**/*.cs" +# And exclude implementation files: +# - "!lib/**/[!T]*.cs" (files not starting with T) +# +# Test file breakdown by category: +# - channels: ~40 test files +# - trends_IIR: ~35 test files +# - trends_FIR: ~30 test files +# - filters: ~30 test files +# - Other categories: ~290 test files +# ============================================== \ No newline at end of file diff --git a/.github/instructions/codacy.instructions.md b/.github/instructions/codacy.instructions.md index 7429440c..cb073c46 100644 --- a/.github/instructions/codacy.instructions.md +++ b/.github/instructions/codacy.instructions.md @@ -6,13 +6,6 @@ # Codacy Rules Configuration for AI behavior when interacting with Codacy's MCP Server -## using any tool that accepts the arguments: `provider`, `organization`, or `repository` -- ALWAYS use: - - provider: gh - - organization: mihakralj - - repository: QuanTAlib -- Avoid calling `git remote -v` unless really necessary - ## CRITICAL: After ANY successful `edit_file` or `reapply` operation - YOU MUST IMMEDIATELY run the `codacy_cli_analyze` tool from Codacy's MCP Server for each file that was edited, with: - `rootPath`: set to the workspace path diff --git a/lib/channels/bbands/Bbands.cs b/lib/channels/bbands/Bbands.cs index 4dc9ba5a..235d8eba 100644 --- a/lib/channels/bbands/Bbands.cs +++ b/lib/channels/bbands/Bbands.cs @@ -1,3 +1,4 @@ +using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -168,15 +169,30 @@ public sealed class Bbands : AbstractBase int len = sourceSpan.Length; TSeries middleSeries = new(capacity: len); - Span middleSpan = stackalloc double[len]; - Span upperSpan = stackalloc double[len]; - Span lowerSpan = stackalloc double[len]; - Calculate(sourceSpan, middleSpan, upperSpan, lowerSpan, _period, _multiplier); + // Use ArrayPool to avoid stack overflow for large series + double[] middleRented = ArrayPool.Shared.Rent(len); + double[] upperRented = ArrayPool.Shared.Rent(len); + double[] lowerRented = ArrayPool.Shared.Rent(len); - for (int i = 0; i < len; i++) + try { - middleSeries.Add(timeSpan[i], middleSpan[i], isNew: true); + Span middleSpan = middleRented.AsSpan(0, len); + Span upperSpan = upperRented.AsSpan(0, len); + Span lowerSpan = lowerRented.AsSpan(0, len); + + Calculate(sourceSpan, middleSpan, upperSpan, lowerSpan, _period, _multiplier); + + for (int i = 0; i < len; i++) + { + middleSeries.Add(timeSpan[i], middleSpan[i], isNew: true); + } + } + finally + { + ArrayPool.Shared.Return(middleRented); + ArrayPool.Shared.Return(upperRented); + ArrayPool.Shared.Return(lowerRented); } // Restore state from the last period values @@ -257,8 +273,10 @@ public sealed class Bbands : AbstractBase // Calculate standard deviation and bands using O(n) rolling sums // Instead of O(n²) nested loop, maintain running sum and sumSq + // Track count of finite values to properly compute mean/variance double rollingSum = 0.0; double rollingSumSq = 0.0; + int finiteCount = 0; // Initialize rolling sums for first window for (int i = 0; i < Math.Min(period, len); i++) @@ -268,6 +286,7 @@ public sealed class Bbands : AbstractBase { rollingSum += val; rollingSumSq += val * val; + finiteCount++; } if (i < period - 1) @@ -280,13 +299,22 @@ public sealed class Bbands : AbstractBase // Process first complete window if (len >= period) { - double mean = rollingSum / period; - double variance = (rollingSumSq / period) - (mean * mean); - variance = Math.Max(0.0, variance); // Guard against negative due to floating point - double stdDev = Math.Sqrt(variance); - double offset = multiplier * stdDev; - upper[period - 1] = middle[period - 1] + offset; - lower[period - 1] = middle[period - 1] - offset; + if (finiteCount == period) + { + double mean = rollingSum / finiteCount; + double variance = (rollingSumSq / finiteCount) - (mean * mean); + variance = Math.Max(0.0, variance); // Guard against negative due to floating point + double stdDev = Math.Sqrt(variance); + double offset = multiplier * stdDev; + upper[period - 1] = middle[period - 1] + offset; + lower[period - 1] = middle[period - 1] - offset; + } + else + { + // Not all values in window are finite, emit NaN + upper[period - 1] = double.NaN; + lower[period - 1] = double.NaN; + } } // Process remaining bars with O(1) rolling update @@ -298,6 +326,7 @@ public sealed class Bbands : AbstractBase { rollingSum -= outgoing; rollingSumSq -= outgoing * outgoing; + finiteCount--; } // Add incoming value (current) @@ -306,18 +335,29 @@ public sealed class Bbands : AbstractBase { rollingSum += incoming; rollingSumSq += incoming * incoming; + finiteCount++; } - // Calculate variance from rolling sums: Var = E[X²] - E[X]² - double mean = rollingSum / period; - double variance = (rollingSumSq / period) - (mean * mean); - variance = Math.Max(0.0, variance); // Guard against negative due to floating point + // Only compute bands when all values in window are finite + if (finiteCount == period) + { + // Calculate variance from rolling sums: Var = E[X²] - E[X]² + double mean = rollingSum / finiteCount; + double variance = (rollingSumSq / finiteCount) - (mean * mean); + variance = Math.Max(0.0, variance); // Guard against negative due to floating point - double stdDev = Math.Sqrt(variance); - double offset = multiplier * stdDev; + double stdDev = Math.Sqrt(variance); + double offset = multiplier * stdDev; - upper[i] = middle[i] + offset; - lower[i] = middle[i] - offset; + upper[i] = middle[i] + offset; + lower[i] = middle[i] - offset; + } + else + { + // Window contains non-finite values, emit NaN + upper[i] = double.NaN; + lower[i] = double.NaN; + } } } } diff --git a/lib/channels/fcb/Fcb.cs b/lib/channels/fcb/Fcb.cs index 5d2335fa..0c6f2ca0 100644 --- a/lib/channels/fcb/Fcb.cs +++ b/lib/channels/fcb/Fcb.cs @@ -20,9 +20,9 @@ public sealed class Fcb : ITValuePublisher private readonly double[] _hBuf; private readonly double[] _lBuf; - // Monotonic deques (store indices) - private readonly int[] _hDeque; - private readonly int[] _lDeque; + // Monotonic deques (store indices as long to avoid truncation) + private readonly long[] _hDeque; + private readonly long[] _lDeque; // Deque state private int _hHead; @@ -68,8 +68,8 @@ public sealed class Fcb : ITValuePublisher _hBuf = new double[_period]; _lBuf = new double[_period]; - _hDeque = new int[_period]; - _lDeque = new int[_period]; + _hDeque = new long[_period]; + _lDeque = new long[_period]; Name = $"Fcb({period})"; _barHandler = HandleBar; @@ -128,7 +128,7 @@ public sealed class Fcb : ITValuePublisher while (_hCount > 0) { int backIdx = (_hHead + _hCount - 1) % _period; - int bufIdx = _hDeque[backIdx] % _period; + int bufIdx = (int)(_hDeque[backIdx] % _period); if (_hBuf[bufIdx] <= value) { _hCount--; @@ -140,7 +140,7 @@ public sealed class Fcb : ITValuePublisher } int tail = (_hHead + _hCount) % _period; - _hDeque[tail] = (int)logicalIndex; + _hDeque[tail] = logicalIndex; _hCount++; } @@ -157,7 +157,7 @@ public sealed class Fcb : ITValuePublisher while (_lCount > 0) { int backIdx = (_lHead + _lCount - 1) % _period; - int bufIdx = _lDeque[backIdx] % _period; + int bufIdx = (int)(_lDeque[backIdx] % _period); if (_lBuf[bufIdx] >= value) { _lCount--; @@ -169,7 +169,7 @@ public sealed class Fcb : ITValuePublisher } int tail = (_lHead + _lCount) % _period; - _lDeque[tail] = (int)logicalIndex; + _lDeque[tail] = logicalIndex; _lCount++; } @@ -289,8 +289,8 @@ public sealed class Fcb : ITValuePublisher RebuildDeques(); } - double top = _hBuf[_hDeque[_hHead] % _period]; - double bot = _lBuf[_lDeque[_lHead] % _period]; + double top = _hBuf[(int)(_hDeque[_hHead] % _period)]; + double bot = _lBuf[(int)(_lDeque[_lHead] % _period)]; double mid = (top + bot) * 0.5; if (!_state.IsHot && _index + 1 >= WarmupPeriod) @@ -537,7 +537,10 @@ public sealed class Fcb : ITValuePublisher public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Fcb Indicator) Calculate(TBarSeries source, int period = 20) { - var indicator = new Fcb(source, period); + // Use parameterless constructor to avoid double-priming: + // The Fcb(source, period) constructor already calls Prime(source), + // so calling Update(source) afterwards would Prime again. + var indicator = new Fcb(period); var results = indicator.Update(source); return (results, indicator); } diff --git a/lib/channels/kchannel/Kchannel.cs b/lib/channels/kchannel/Kchannel.cs index bf2cc966..d576a7ac 100644 --- a/lib/channels/kchannel/Kchannel.cs +++ b/lib/channels/kchannel/Kchannel.cs @@ -103,28 +103,43 @@ public sealed class Kchannel : ITValuePublisher { _state = _state with { LastValidClose = close }; } - else + else if (double.IsFinite(_state.LastValidClose)) { close = _state.LastValidClose; } + else + { + // Neither input nor stored value is finite - use safe default + close = 0.0; + } if (double.IsFinite(high)) { _state = _state with { LastValidHigh = high }; } - else + else if (double.IsFinite(_state.LastValidHigh)) { high = _state.LastValidHigh; } + else + { + // Neither input nor stored value is finite - use safe default + high = 0.0; + } if (double.IsFinite(low)) { _state = _state with { LastValidLow = low }; } - else + else if (double.IsFinite(_state.LastValidLow)) { low = _state.LastValidLow; } + else + { + // Neither input nor stored value is finite - use safe default + low = 0.0; + } return (close, high, low); } diff --git a/lib/channels/maenv/Maenv.cs b/lib/channels/maenv/Maenv.cs index ed8baef4..0c01656d 100644 --- a/lib/channels/maenv/Maenv.cs +++ b/lib/channels/maenv/Maenv.cs @@ -603,7 +603,10 @@ public sealed class Maenv : ITValuePublisher public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Maenv Indicator) Calculate(TSeries source, int period = 20, double percentage = 1.0, MaenvType maType = MaenvType.EMA) { - var indicator = new Maenv(source, period, percentage, maType); + // Use parameterless constructor to avoid double-priming: + // The Maenv(source, ...) constructor already calls Prime(source), + // so calling Update(source) afterwards would Prime again. + var indicator = new Maenv(period, percentage, maType); var results = indicator.Update(source); return (results, indicator); } diff --git a/lib/channels/mmchannel/Mmchannel.cs b/lib/channels/mmchannel/Mmchannel.cs index 100a599e..1814422d 100644 --- a/lib/channels/mmchannel/Mmchannel.cs +++ b/lib/channels/mmchannel/Mmchannel.cs @@ -23,7 +23,7 @@ public sealed class Mmchannel : ITValuePublisher private long _index; [StructLayout(LayoutKind.Auto)] - private record struct State(double LastValidHigh, double LastValidLow, bool IsHot); + private record struct State(double LastValidHigh, double LastValidLow); private State _state; private State _p_state; @@ -52,7 +52,7 @@ public sealed class Mmchannel : ITValuePublisher _minDeque = new MonotonicDeque(_period); _count = 0; _index = -1; - _state = new State(double.NaN, double.NaN, false); + _state = new State(double.NaN, double.NaN); _p_state = _state; Name = $"Mmchannel({period})"; @@ -112,7 +112,8 @@ public sealed class Mmchannel : ITValuePublisher _state = _p_state; } - int bufIdx = (int)(_index % _period); + // Defensive guard: ensure non-negative buffer index even if _index is -1 (shouldn't happen but safeguard) + int bufIdx = _index < 0 ? 0 : (int)(_index % _period); var (high, low) = GetValid(input.High, input.Low); // If still no valid data, return NaN placeholders @@ -143,11 +144,6 @@ public sealed class Mmchannel : ITValuePublisher double top = _maxDeque.GetExtremum(_hBuf); double bot = _minDeque.GetExtremum(_lBuf); - if (!IsHot && _count >= _period) - { - _state = _state with { IsHot = true }; - } - // Last returns Upper by default for single-value compatibility Last = new TValue(input.Time, top); Upper = new TValue(input.Time, top); @@ -218,7 +214,7 @@ public sealed class Mmchannel : ITValuePublisher _minDeque.Reset(); _count = 0; _index = -1; - _state = new State(double.NaN, double.NaN, false); + _state = new State(double.NaN, double.NaN); _p_state = _state; Last = default; Upper = default; @@ -262,6 +258,11 @@ public sealed class Mmchannel : ITValuePublisher public static (TSeries Upper, TSeries Lower) Batch(TBarSeries source, int period) { + if (source == null || source.Count == 0) + { + return (new TSeries([], []), new TSeries([], [])); + } + int len = source.Count; var tUpper = new List(len); var vUpper = new List(len); diff --git a/lib/channels/pchannel/Pchannel.cs b/lib/channels/pchannel/Pchannel.cs index deb73926..6145bbfe 100644 --- a/lib/channels/pchannel/Pchannel.cs +++ b/lib/channels/pchannel/Pchannel.cs @@ -237,7 +237,7 @@ public sealed class Pchannel : ITValuePublisher double bot = _lBuf[_lDeque[_lHead] % _period]; double mid = (top + bot) * 0.5; - if (!IsHot && _count >= _period) + if (!_state.IsHot && _count >= _period) { _state = _state with { IsHot = true }; } diff --git a/lib/channels/regchannel/Regchannel.cs b/lib/channels/regchannel/Regchannel.cs index 829c9597..b2d5787b 100644 --- a/lib/channels/regchannel/Regchannel.cs +++ b/lib/channels/regchannel/Regchannel.cs @@ -464,7 +464,9 @@ public sealed class Regchannel : ITValuePublisher public static ((TSeries Middle, TSeries Upper, TSeries Lower) Results, Regchannel Indicator) Calculate(TSeries source, int period = 20, double multiplier = 2.0) { - var indicator = new Regchannel(source, period, multiplier); + // Use parameterless constructor to avoid double-processing: new Regchannel(source, ...) calls Prime(source), + // then Update(source) would call Prime again. + var indicator = new Regchannel(period, multiplier); var results = indicator.Update(source); return (results, indicator); } diff --git a/lib/channels/starchannel/Starchannel.cs b/lib/channels/starchannel/Starchannel.cs index b4f4e099..6e3cb504 100644 --- a/lib/channels/starchannel/Starchannel.cs +++ b/lib/channels/starchannel/Starchannel.cs @@ -322,12 +322,45 @@ public sealed class Starchannel : ITValuePublisher upper[0] = close[0]; lower[0] = close[0]; + // Track last valid values for sanitization + double lastValidClose = close[0]; + double lastValidHigh = high[0]; + double lastValidLow = low[0]; + for (int i = 1; i < len; i++) { double c = close[i]; double h = high[i]; double l = low[i]; + // Sanitize non-finite values (match Update/GetValid behavior) + if (double.IsFinite(c)) + { + lastValidClose = c; + } + else + { + c = lastValidClose; + } + + if (double.IsFinite(h)) + { + lastValidHigh = h; + } + else + { + h = lastValidHigh; + } + + if (double.IsFinite(l)) + { + lastValidLow = l; + } + else + { + l = lastValidLow; + } + // SMA: add current, subtract oldest if beyond window if (i < period) { diff --git a/lib/channels/stbands/Stbands.Quantower.cs b/lib/channels/stbands/Stbands.Quantower.cs index 8cfb43a5..604d74c5 100644 --- a/lib/channels/stbands/Stbands.Quantower.cs +++ b/lib/channels/stbands/Stbands.Quantower.cs @@ -8,7 +8,7 @@ public class StbandsIndicator : Indicator, IWatchlistIndicator [InputParameter("Period", sortIndex: 1, minimum: 1, maximum: 1000, increment: 1, decimalPlaces: 0)] public int Period { get; set; } = 10; - [InputParameter("Multiplier", sortIndex: 2, minimum: 0.1, maximum: 10.0, increment: 0.1, decimalPlaces: 1)] + [InputParameter("Multiplier", sortIndex: 2, minimum: 0.001, maximum: 10.0, increment: 0.1, decimalPlaces: 3)] public double Multiplier { get; set; } = 3.0; [InputParameter("Show cold values", sortIndex: 21)] diff --git a/lib/channels/ubands/Ubands.cs b/lib/channels/ubands/Ubands.cs index 18cdebf4..fcd4026a 100644 --- a/lib/channels/ubands/Ubands.cs +++ b/lib/channels/ubands/Ubands.cs @@ -124,11 +124,15 @@ public sealed class Ubands : AbstractBase } [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] - private double GetFiniteValue(double value) + private double GetFiniteValue(double value, bool isNew) { if (double.IsFinite(value)) { - _state.LastValidValue = value; + // Only update LastValidValue on new bars to avoid corrupting restored state during corrections + if (isNew) + { + _state = _state with { LastValidValue = value }; + } return value; } return double.IsFinite(_state.LastValidValue) ? _state.LastValidValue : 0; @@ -149,7 +153,7 @@ public sealed class Ubands : AbstractBase _state = _p_state; } - double val = GetFiniteValue(input.Value); + double val = GetFiniteValue(input.Value, isNew); // Initialize on first value if (!_state.IsInitialized) diff --git a/lib/channels/uchannel/Uchannel.Quantower.cs b/lib/channels/uchannel/Uchannel.Quantower.cs index 512ec650..7ecc6a98 100644 --- a/lib/channels/uchannel/Uchannel.Quantower.cs +++ b/lib/channels/uchannel/Uchannel.Quantower.cs @@ -11,7 +11,7 @@ public class UchannelIndicator : Indicator, IWatchlistIndicator [InputParameter("Center Period", sortIndex: 2, minimum: 1, maximum: 1000, increment: 1, decimalPlaces: 0)] public int CenterPeriod { get; set; } = 20; - [InputParameter("Multiplier", sortIndex: 3, minimum: 0.1, maximum: 10.0, increment: 0.1, decimalPlaces: 1)] + [InputParameter("Multiplier", sortIndex: 3, minimum: 0.001, maximum: 10.0, increment: 0.1, decimalPlaces: 3)] public double Multiplier { get; set; } = 1.0; [InputParameter("Show cold values", sortIndex: 21)] diff --git a/lib/channels/vwapbands/Vwapbands.Tests.cs b/lib/channels/vwapbands/Vwapbands.Tests.cs index aecbddbc..f71adb1a 100644 --- a/lib/channels/vwapbands/Vwapbands.Tests.cs +++ b/lib/channels/vwapbands/Vwapbands.Tests.cs @@ -441,17 +441,18 @@ public class VwapbandsTests double[] upper2 = new double[5]; double[] lower2 = new double[5]; double[] vwap = new double[5]; + double[] stdDev = new double[5]; double[] wrongSize = new double[3]; // Multiplier must be >= MinMultiplier Assert.Throws(() => Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(), - upper1.AsSpan(), lower1.AsSpan(), upper2.AsSpan(), lower2.AsSpan(), vwap.AsSpan(), 0)); + upper1.AsSpan(), lower1.AsSpan(), upper2.AsSpan(), lower2.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 0)); // All arrays must be same length Assert.Throws(() => Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(), - wrongSize.AsSpan(), lower1.AsSpan(), upper2.AsSpan(), lower2.AsSpan(), vwap.AsSpan(), 1.0)); + wrongSize.AsSpan(), lower1.AsSpan(), upper2.AsSpan(), lower2.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 1.0)); } [Fact] @@ -464,9 +465,10 @@ public class VwapbandsTests double[] upper2 = new double[5]; double[] lower2 = new double[5]; double[] vwap = new double[5]; + double[] stdDev = new double[5]; Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(), - upper1.AsSpan(), lower1.AsSpan(), upper2.AsSpan(), lower2.AsSpan(), vwap.AsSpan(), 1.0); + upper1.AsSpan(), lower1.AsSpan(), upper2.AsSpan(), lower2.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 1.0); foreach (var val in vwap) { diff --git a/lib/channels/vwapbands/Vwapbands.Validation.Tests.cs b/lib/channels/vwapbands/Vwapbands.Validation.Tests.cs index 688dd294..e071ab89 100644 --- a/lib/channels/vwapbands/Vwapbands.Validation.Tests.cs +++ b/lib/channels/vwapbands/Vwapbands.Validation.Tests.cs @@ -124,11 +124,12 @@ public sealed class VwapbandsValidationTests : IDisposable double[] spanLower1 = new double[bars.Count]; double[] spanUpper2 = new double[bars.Count]; double[] spanLower2 = new double[bars.Count]; + double[] spanStdDev = new double[bars.Count]; Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(), spanUpper1.AsSpan(), spanLower1.AsSpan(), spanUpper2.AsSpan(), spanLower2.AsSpan(), - spanVwap.AsSpan(), multiplier); + spanVwap.AsSpan(), spanStdDev.AsSpan(), multiplier); // Compare last 100 values int compareCount = Math.Min(100, bars.Count - 2); diff --git a/lib/channels/vwapbands/Vwapbands.cs b/lib/channels/vwapbands/Vwapbands.cs index e33018b4..e171b896 100644 --- a/lib/channels/vwapbands/Vwapbands.cs +++ b/lib/channels/vwapbands/Vwapbands.cs @@ -351,6 +351,15 @@ public sealed class Vwapbands : AbstractBase /// /// Calculates VWAP Bands using span arrays. /// + /// Source price values (typically HLC3) + /// Volume values + /// Output span for upper band at 1σ + /// Output span for lower band at 1σ + /// Output span for upper band at 2σ + /// Output span for lower band at 2σ + /// Output span for VWAP values + /// Output span for standard deviation values + /// Band multiplier (default 1.0) public static void Calculate( ReadOnlySpan price, ReadOnlySpan volume, @@ -359,11 +368,12 @@ public sealed class Vwapbands : AbstractBase Span upper2, Span lower2, Span vwap, + Span stdDev, double multiplier = DefaultMultiplier) { int len = price.Length; if (len != volume.Length || len != upper1.Length || len != lower1.Length || - len != upper2.Length || len != lower2.Length || len != vwap.Length) + len != upper2.Length || len != lower2.Length || len != vwap.Length || len != stdDev.Length) { throw new ArgumentException("All spans must have the same length.", nameof(price)); } @@ -408,6 +418,7 @@ public sealed class Vwapbands : AbstractBase double stdev = Math.Sqrt(variance); vwap[i] = vwapVal; + stdDev[i] = stdev; upper1[i] = vwapVal + multiplier * stdev; lower1[i] = vwapVal - multiplier * stdev; upper2[i] = vwapVal + 2.0 * multiplier * stdev; diff --git a/lib/channels/vwapsd/Vwapsd.Quantower.cs b/lib/channels/vwapsd/Vwapsd.Quantower.cs index 9594bd9d..fb7355ec 100644 --- a/lib/channels/vwapsd/Vwapsd.Quantower.cs +++ b/lib/channels/vwapsd/Vwapsd.Quantower.cs @@ -31,8 +31,8 @@ public class VwapsdIndicator : Indicator, IWatchlistIndicator Description = "Volume weighted average price with configurable standard deviation bands"; VwapSeries = new("VWAP", Color.Blue, 2, LineStyle.Solid); - UpperSeries = new($"Upper (+{NumDevs}σ)", Color.Red, 1, LineStyle.Solid); - LowerSeries = new($"Lower (-{NumDevs}σ)", Color.Green, 1, LineStyle.Solid); + UpperSeries = new("Upper", Color.Red, 1, LineStyle.Solid); + LowerSeries = new("Lower", Color.Green, 1, LineStyle.Solid); WidthSeries = new("Width", Color.Gray, 1, LineStyle.Dot); AddLineSeries(VwapSeries); @@ -47,9 +47,29 @@ public class VwapsdIndicator : Indicator, IWatchlistIndicator protected override void OnInit() { vwapsd = new(NumDevs); + if (UpperSeries != null) + { + UpperSeries.Name = $"Upper (+{NumDevs:F1}σ)"; + } + if (LowerSeries != null) + { + LowerSeries.Name = $"Lower (-{NumDevs:F1}σ)"; + } base.OnInit(); } + private void UpdateSeriesNames() + { + if (UpperSeries != null) + { + UpperSeries.Name = $"Upper (+{NumDevs:F1}σ)"; + } + if (LowerSeries != null) + { + LowerSeries.Name = $"Lower (-{NumDevs:F1}σ)"; + } + } + protected override void OnUpdate(UpdateArgs args) { var item = HistoricalData[0, SeekOriginHistory.End]; diff --git a/lib/channels/vwapsd/Vwapsd.Tests.cs b/lib/channels/vwapsd/Vwapsd.Tests.cs index 710cf85e..a1d8c10b 100644 --- a/lib/channels/vwapsd/Vwapsd.Tests.cs +++ b/lib/channels/vwapsd/Vwapsd.Tests.cs @@ -467,22 +467,23 @@ public class VwapsdTests double[] upper = new double[5]; double[] lower = new double[5]; double[] vwap = new double[5]; + double[] stdDev = new double[5]; double[] wrongSize = new double[3]; // NumDevs must be >= MinNumDevs Assert.Throws(() => Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(), - upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), 0)); + upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 0)); // NumDevs must be <= MaxNumDevs Assert.Throws(() => Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(), - upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), 6.0)); + upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 6.0)); // All arrays must be same length Assert.Throws(() => Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(), - wrongSize.AsSpan(), lower.AsSpan(), vwap.AsSpan(), 1.0)); + wrongSize.AsSpan(), lower.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 1.0)); } [Fact] @@ -493,9 +494,10 @@ public class VwapsdTests double[] upper = new double[5]; double[] lower = new double[5]; double[] vwap = new double[5]; + double[] stdDev = new double[5]; Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(), - upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), 1.0); + upper.AsSpan(), lower.AsSpan(), vwap.AsSpan(), stdDev.AsSpan(), 1.0); foreach (var val in vwap) { diff --git a/lib/channels/vwapsd/Vwapsd.Validation.Tests.cs b/lib/channels/vwapsd/Vwapsd.Validation.Tests.cs index de88bd26..3eba528e 100644 --- a/lib/channels/vwapsd/Vwapsd.Validation.Tests.cs +++ b/lib/channels/vwapsd/Vwapsd.Validation.Tests.cs @@ -118,10 +118,11 @@ public sealed class VwapsdValidationTests : IDisposable double[] spanVwap = new double[bars.Count]; double[] spanUpper = new double[bars.Count]; double[] spanLower = new double[bars.Count]; + double[] spanStdDev = new double[bars.Count]; Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(), spanUpper.AsSpan(), spanLower.AsSpan(), - spanVwap.AsSpan(), numDevs); + spanVwap.AsSpan(), spanStdDev.AsSpan(), numDevs); // Compare last 100 values int compareCount = Math.Min(100, bars.Count - 2); diff --git a/lib/channels/vwapsd/Vwapsd.cs b/lib/channels/vwapsd/Vwapsd.cs index c8568466..043363e5 100644 --- a/lib/channels/vwapsd/Vwapsd.cs +++ b/lib/channels/vwapsd/Vwapsd.cs @@ -337,16 +337,24 @@ public sealed class Vwapsd : AbstractBase /// /// Calculates VWAP SD Bands using span arrays. /// + /// Source price values (typically HLC3) + /// Volume values + /// Output span for upper band + /// Output span for lower band + /// Output span for VWAP values + /// Output span for standard deviation values + /// Number of standard deviations for bands (default 2.0) public static void Calculate( ReadOnlySpan price, ReadOnlySpan volume, Span upper, Span lower, Span vwap, + Span stdDev, double numDevs = DefaultNumDevs) { int len = price.Length; - if (len != volume.Length || len != upper.Length || len != lower.Length || len != vwap.Length) + if (len != volume.Length || len != upper.Length || len != lower.Length || len != vwap.Length || len != stdDev.Length) { throw new ArgumentException("All spans must have the same length.", nameof(price)); } @@ -396,6 +404,7 @@ public sealed class Vwapsd : AbstractBase double stdev = Math.Sqrt(variance); vwap[i] = vwapVal; + stdDev[i] = stdev; upper[i] = vwapVal + numDevs * stdev; lower[i] = vwapVal - numDevs * stdev; } diff --git a/lib/core/collections/MonotonicDeque.cs b/lib/core/collections/MonotonicDeque.cs index 7eddead7..034e45de 100644 --- a/lib/core/collections/MonotonicDeque.cs +++ b/lib/core/collections/MonotonicDeque.cs @@ -16,7 +16,7 @@ namespace QuanTAlib; [SkipLocalsInit] public sealed class MonotonicDeque { - private readonly int[] _deque; + private readonly long[] _deque; private readonly int _period; private int _head; private int _count; @@ -24,7 +24,7 @@ public sealed class MonotonicDeque /// /// Gets the current front index (the index of the current extremum). /// - public int FrontIndex => _count > 0 ? _deque[_head] : -1; + public long FrontIndex => _count > 0 ? _deque[_head] : -1; /// /// Gets the current element count in the deque. @@ -43,7 +43,7 @@ public sealed class MonotonicDeque } _period = period; - _deque = new int[period]; + _deque = new long[period]; _head = 0; _count = 0; } @@ -70,7 +70,7 @@ public sealed class MonotonicDeque while (_count > 0) { int backIdx = (_head + _count - 1) % _period; - int bufIdx = _deque[backIdx] % _period; + int bufIdx = (int)(_deque[backIdx] % _period); if (buffer[bufIdx] <= value) { _count--; @@ -83,7 +83,7 @@ public sealed class MonotonicDeque // Push new index int tail = (_head + _count) % _period; - _deque[tail] = (int)logicalIndex; + _deque[tail] = logicalIndex; _count++; } @@ -109,7 +109,7 @@ public sealed class MonotonicDeque while (_count > 0) { int backIdx = (_head + _count - 1) % _period; - int bufIdx = _deque[backIdx] % _period; + int bufIdx = (int)(_deque[backIdx] % _period); if (buffer[bufIdx] >= value) { _count--; @@ -122,7 +122,7 @@ public sealed class MonotonicDeque // Push new index int tail = (_head + _count) % _period; - _deque[tail] = (int)logicalIndex; + _deque[tail] = logicalIndex; _count++; } @@ -134,7 +134,7 @@ public sealed class MonotonicDeque [MethodImpl(MethodImplOptions.AggressiveInlining)] public double GetExtremum(double[] buffer) { - return _count > 0 ? buffer[_deque[_head] % _period] : double.NaN; + return _count > 0 ? buffer[(int)(_deque[_head] % _period)] : double.NaN; } /// diff --git a/lib/errors/mdape/Mdape.Tests.cs b/lib/errors/mdape/Mdape.Tests.cs index 7c5b082b..5f44f4d6 100644 --- a/lib/errors/mdape/Mdape.Tests.cs +++ b/lib/errors/mdape/Mdape.Tests.cs @@ -309,17 +309,18 @@ public class MdapeTests } [Fact] - public void Calculate_ZeroActual_ReturnsZeroError() + public void Calculate_ZeroActual_UsesSubstituteValue() { - // When actual is zero or near-zero, should return 0 error (epsilon protection) + // When actual is zero or near-zero, implementation substitutes 1.0 fallback + // to avoid division by zero (epsilon protection means substitute, not return 0) var mdape = new Mdape(3); - mdape.Update(0.0, 10); - mdape.Update(0.0, 20); - mdape.Update(0.0, 30); + mdape.Update(0.0, 10); // actual=1.0 (substituted), pred=10 → |1-10|/1 * 100 = 900% + mdape.Update(0.0, 20); // actual=1.0 (substituted), pred=20 → |1-20|/1 * 100 = 1900% + mdape.Update(0.0, 30); // actual=1.0 (substituted), pred=30 → |1-30|/1 * 100 = 2900% - // With epsilon protection, all errors are 0 - Assert.Equal(0.0, mdape.Last.Value, Precision); + // Median of [900, 1900, 2900] = 1900 + Assert.Equal(1900.0, mdape.Last.Value, Precision); } [Fact] @@ -361,4 +362,4 @@ public class MdapeTests Assert.Equal(mdape1.Last.Value, mdape2.Last.Value, Precision); } -} +} \ No newline at end of file diff --git a/lib/errors/mdape/Mdape.cs b/lib/errors/mdape/Mdape.cs index 5ce2a07b..34836c2b 100644 --- a/lib/errors/mdape/Mdape.cs +++ b/lib/errors/mdape/Mdape.cs @@ -74,9 +74,12 @@ public sealed class Mdape : AbstractBase [MethodImpl(MethodImplOptions.AggressiveInlining)] private TValue UpdateCore(DateTime time, double actualVal, double predictedVal, bool isNew) { - if (!double.IsFinite(actualVal)) + // Validate actual: must be finite AND have sufficient magnitude (matches Batch logic) + if (!double.IsFinite(actualVal) || Math.Abs(actualVal) < 1e-10) { - actualVal = double.IsFinite(_state.LastValidActual) ? _state.LastValidActual : 1.0; + actualVal = double.IsFinite(_state.LastValidActual) && Math.Abs(_state.LastValidActual) >= 1e-10 + ? _state.LastValidActual + : 1.0; } else { @@ -92,10 +95,10 @@ public sealed class Mdape : AbstractBase _state.LastValidPredicted = predictedVal; } - // Calculate absolute percentage error + // Calculate absolute percentage error (absActual guaranteed >= 1e-10 by validation above) double absActual = Math.Abs(actualVal); double absError = Math.Abs(actualVal - predictedVal); - double percentageError = absActual > 1e-10 ? (absError / absActual) * 100.0 : 0.0; + double percentageError = (absError / absActual) * 100.0; if (isNew) { diff --git a/lib/errors/rae/Rae.cs b/lib/errors/rae/Rae.cs index 172380d9..a5ed02ba 100644 --- a/lib/errors/rae/Rae.cs +++ b/lib/errors/rae/Rae.cs @@ -62,6 +62,17 @@ public sealed class Rae : AbstractBase double actualVal = actual.Value; double predictedVal = predicted.Value; + // Snapshot BEFORE any mutations for correct rollback + if (isNew) + { + _p_state = _state; + } + else + { + _state = _p_state; + } + + // Sanitize non-finite values AFTER snapshot/restore if (!double.IsFinite(actualVal)) { actualVal = double.IsFinite(_state.LastValidActual) ? _state.LastValidActual : 0.0; @@ -82,8 +93,6 @@ public sealed class Rae : AbstractBase if (isNew) { - _p_state = _state; - // Update actual buffer for mean calculation double removedActual = _actualBuffer.Count == _actualBuffer.Capacity ? _actualBuffer.Oldest : 0.0; _state.ActualSum = _state.ActualSum - removedActual + actualVal; @@ -115,8 +124,6 @@ public sealed class Rae : AbstractBase } else { - _state = _p_state; - // Update buffers and recalculate sums (buffer state is inconsistent with _p_state) _actualBuffer.UpdateNewest(actualVal); _state.ActualSum = _actualBuffer.RecalculateSum(); @@ -348,4 +355,4 @@ public sealed class Rae : AbstractBase } } } -} +} \ No newline at end of file diff --git a/lib/errors/wmape/Wmape.cs b/lib/errors/wmape/Wmape.cs index 82321eef..9363977d 100644 --- a/lib/errors/wmape/Wmape.cs +++ b/lib/errors/wmape/Wmape.cs @@ -222,22 +222,22 @@ public sealed class Wmape : AbstractBase double lastValidActual = 0; double lastValidPredicted = 0; - for (int k = 0; k < len; k++) - { - if (double.IsFinite(actual[k])) + for (int k = 0; k < len; k++) { - lastValidActual = actual[k]; - break; + if (double.IsFinite(actual[k])) + { + lastValidActual = actual[k]; + break; + } } - } - for (int k = 0; k < len; k++) - { - if (double.IsFinite(predicted[k])) + for (int k = 0; k < len; k++) { - lastValidPredicted = predicted[k]; - break; + if (double.IsFinite(predicted[k])) + { + lastValidPredicted = predicted[k]; + break; + } } - } int bufferIndex = 0; int i = 0; diff --git a/lib/volume/cmf/Cmf.Quantower.Tests.cs b/lib/volume/cmf/Cmf.Quantower.Tests.cs index b5453e76..0d87b1cf 100644 --- a/lib/volume/cmf/Cmf.Quantower.Tests.cs +++ b/lib/volume/cmf/Cmf.Quantower.Tests.cs @@ -13,7 +13,7 @@ public class CmfIndicatorTests Assert.Equal(20, indicator.Period); Assert.True(indicator.SeparateWindow); Assert.True(indicator.OnBackGround); - Assert.Equal(20, CmfIndicator.MinHistoryDepths); + Assert.Equal(20, indicator.MinHistoryDepths); } [Fact] @@ -28,7 +28,7 @@ public class CmfIndicatorTests { var indicator = new CmfIndicator(); - Assert.Equal(20, CmfIndicator.MinHistoryDepths); + Assert.Equal(20, indicator.MinHistoryDepths); Assert.Equal(20, ((IWatchlistIndicator)indicator).MinHistoryDepths); } diff --git a/lib/volume/cmf/Cmf.Quantower.cs b/lib/volume/cmf/Cmf.Quantower.cs index 9394b358..14d43fa2 100644 --- a/lib/volume/cmf/Cmf.Quantower.cs +++ b/lib/volume/cmf/Cmf.Quantower.cs @@ -16,8 +16,8 @@ public sealed class CmfIndicator : Indicator, IWatchlistIndicator private Cmf _cmf = null!; private readonly LineSeries _series; - public static int MinHistoryDepths => 20; - int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; + public int MinHistoryDepths => Period; + int IWatchlistIndicator.MinHistoryDepths => Period; public override string ShortName => $"CMF({Period})"; public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/volume/cmf/Cmf.Quantower.cs";