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.
This commit is contained in:
Miha Kralj
2026-01-27 23:48:33 -08:00
parent 8ac15f1efa
commit a9e72dae0d
29 changed files with 464 additions and 114 deletions
+66
View File
@@ -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/**"
# ==============================================
+45
View File
@@ -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
+88
View File
@@ -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
# ==============================================
@@ -6,13 +6,6 @@
# Codacy Rules # Codacy Rules
Configuration for AI behavior when interacting with Codacy's MCP Server 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 ## 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: - 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 - `rootPath`: set to the workspace path
+61 -21
View File
@@ -1,3 +1,4 @@
using System.Buffers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -168,15 +169,30 @@ public sealed class Bbands : AbstractBase
int len = sourceSpan.Length; int len = sourceSpan.Length;
TSeries middleSeries = new(capacity: len); TSeries middleSeries = new(capacity: len);
Span<double> middleSpan = stackalloc double[len];
Span<double> upperSpan = stackalloc double[len];
Span<double> lowerSpan = stackalloc double[len];
Calculate(sourceSpan, middleSpan, upperSpan, lowerSpan, _period, _multiplier); // Use ArrayPool to avoid stack overflow for large series
double[] middleRented = ArrayPool<double>.Shared.Rent(len);
double[] upperRented = ArrayPool<double>.Shared.Rent(len);
double[] lowerRented = ArrayPool<double>.Shared.Rent(len);
for (int i = 0; i < len; i++) try
{ {
middleSeries.Add(timeSpan[i], middleSpan[i], isNew: true); Span<double> middleSpan = middleRented.AsSpan(0, len);
Span<double> upperSpan = upperRented.AsSpan(0, len);
Span<double> 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<double>.Shared.Return(middleRented);
ArrayPool<double>.Shared.Return(upperRented);
ArrayPool<double>.Shared.Return(lowerRented);
} }
// Restore state from the last period values // 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 // Calculate standard deviation and bands using O(n) rolling sums
// Instead of O(n²) nested loop, maintain running sum and sumSq // 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 rollingSum = 0.0;
double rollingSumSq = 0.0; double rollingSumSq = 0.0;
int finiteCount = 0;
// Initialize rolling sums for first window // Initialize rolling sums for first window
for (int i = 0; i < Math.Min(period, len); i++) for (int i = 0; i < Math.Min(period, len); i++)
@@ -268,6 +286,7 @@ public sealed class Bbands : AbstractBase
{ {
rollingSum += val; rollingSum += val;
rollingSumSq += val * val; rollingSumSq += val * val;
finiteCount++;
} }
if (i < period - 1) if (i < period - 1)
@@ -280,13 +299,22 @@ public sealed class Bbands : AbstractBase
// Process first complete window // Process first complete window
if (len >= period) if (len >= period)
{ {
double mean = rollingSum / period; if (finiteCount == period)
double variance = (rollingSumSq / period) - (mean * mean); {
variance = Math.Max(0.0, variance); // Guard against negative due to floating point double mean = rollingSum / finiteCount;
double stdDev = Math.Sqrt(variance); double variance = (rollingSumSq / finiteCount) - (mean * mean);
double offset = multiplier * stdDev; variance = Math.Max(0.0, variance); // Guard against negative due to floating point
upper[period - 1] = middle[period - 1] + offset; double stdDev = Math.Sqrt(variance);
lower[period - 1] = middle[period - 1] - offset; 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 // Process remaining bars with O(1) rolling update
@@ -298,6 +326,7 @@ public sealed class Bbands : AbstractBase
{ {
rollingSum -= outgoing; rollingSum -= outgoing;
rollingSumSq -= outgoing * outgoing; rollingSumSq -= outgoing * outgoing;
finiteCount--;
} }
// Add incoming value (current) // Add incoming value (current)
@@ -306,18 +335,29 @@ public sealed class Bbands : AbstractBase
{ {
rollingSum += incoming; rollingSum += incoming;
rollingSumSq += incoming * incoming; rollingSumSq += incoming * incoming;
finiteCount++;
} }
// Calculate variance from rolling sums: Var = E[X²] - E[X]² // Only compute bands when all values in window are finite
double mean = rollingSum / period; if (finiteCount == period)
double variance = (rollingSumSq / period) - (mean * mean); {
variance = Math.Max(0.0, variance); // Guard against negative due to floating point // 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 stdDev = Math.Sqrt(variance);
double offset = multiplier * stdDev; double offset = multiplier * stdDev;
upper[i] = middle[i] + offset; upper[i] = middle[i] + offset;
lower[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;
}
} }
} }
} }
+15 -12
View File
@@ -20,9 +20,9 @@ public sealed class Fcb : ITValuePublisher
private readonly double[] _hBuf; private readonly double[] _hBuf;
private readonly double[] _lBuf; private readonly double[] _lBuf;
// Monotonic deques (store indices) // Monotonic deques (store indices as long to avoid truncation)
private readonly int[] _hDeque; private readonly long[] _hDeque;
private readonly int[] _lDeque; private readonly long[] _lDeque;
// Deque state // Deque state
private int _hHead; private int _hHead;
@@ -68,8 +68,8 @@ public sealed class Fcb : ITValuePublisher
_hBuf = new double[_period]; _hBuf = new double[_period];
_lBuf = new double[_period]; _lBuf = new double[_period];
_hDeque = new int[_period]; _hDeque = new long[_period];
_lDeque = new int[_period]; _lDeque = new long[_period];
Name = $"Fcb({period})"; Name = $"Fcb({period})";
_barHandler = HandleBar; _barHandler = HandleBar;
@@ -128,7 +128,7 @@ public sealed class Fcb : ITValuePublisher
while (_hCount > 0) while (_hCount > 0)
{ {
int backIdx = (_hHead + _hCount - 1) % _period; int backIdx = (_hHead + _hCount - 1) % _period;
int bufIdx = _hDeque[backIdx] % _period; int bufIdx = (int)(_hDeque[backIdx] % _period);
if (_hBuf[bufIdx] <= value) if (_hBuf[bufIdx] <= value)
{ {
_hCount--; _hCount--;
@@ -140,7 +140,7 @@ public sealed class Fcb : ITValuePublisher
} }
int tail = (_hHead + _hCount) % _period; int tail = (_hHead + _hCount) % _period;
_hDeque[tail] = (int)logicalIndex; _hDeque[tail] = logicalIndex;
_hCount++; _hCount++;
} }
@@ -157,7 +157,7 @@ public sealed class Fcb : ITValuePublisher
while (_lCount > 0) while (_lCount > 0)
{ {
int backIdx = (_lHead + _lCount - 1) % _period; int backIdx = (_lHead + _lCount - 1) % _period;
int bufIdx = _lDeque[backIdx] % _period; int bufIdx = (int)(_lDeque[backIdx] % _period);
if (_lBuf[bufIdx] >= value) if (_lBuf[bufIdx] >= value)
{ {
_lCount--; _lCount--;
@@ -169,7 +169,7 @@ public sealed class Fcb : ITValuePublisher
} }
int tail = (_lHead + _lCount) % _period; int tail = (_lHead + _lCount) % _period;
_lDeque[tail] = (int)logicalIndex; _lDeque[tail] = logicalIndex;
_lCount++; _lCount++;
} }
@@ -289,8 +289,8 @@ public sealed class Fcb : ITValuePublisher
RebuildDeques(); RebuildDeques();
} }
double top = _hBuf[_hDeque[_hHead] % _period]; double top = _hBuf[(int)(_hDeque[_hHead] % _period)];
double bot = _lBuf[_lDeque[_lHead] % _period]; double bot = _lBuf[(int)(_lDeque[_lHead] % _period)];
double mid = (top + bot) * 0.5; double mid = (top + bot) * 0.5;
if (!_state.IsHot && _index + 1 >= WarmupPeriod) 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) 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); var results = indicator.Update(source);
return (results, indicator); return (results, indicator);
} }
+18 -3
View File
@@ -103,28 +103,43 @@ public sealed class Kchannel : ITValuePublisher
{ {
_state = _state with { LastValidClose = close }; _state = _state with { LastValidClose = close };
} }
else else if (double.IsFinite(_state.LastValidClose))
{ {
close = _state.LastValidClose; close = _state.LastValidClose;
} }
else
{
// Neither input nor stored value is finite - use safe default
close = 0.0;
}
if (double.IsFinite(high)) if (double.IsFinite(high))
{ {
_state = _state with { LastValidHigh = high }; _state = _state with { LastValidHigh = high };
} }
else else if (double.IsFinite(_state.LastValidHigh))
{ {
high = _state.LastValidHigh; high = _state.LastValidHigh;
} }
else
{
// Neither input nor stored value is finite - use safe default
high = 0.0;
}
if (double.IsFinite(low)) if (double.IsFinite(low))
{ {
_state = _state with { LastValidLow = low }; _state = _state with { LastValidLow = low };
} }
else else if (double.IsFinite(_state.LastValidLow))
{ {
low = _state.LastValidLow; low = _state.LastValidLow;
} }
else
{
// Neither input nor stored value is finite - use safe default
low = 0.0;
}
return (close, high, low); return (close, high, low);
} }
+4 -1
View File
@@ -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) 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); var results = indicator.Update(source);
return (results, indicator); return (results, indicator);
} }
+10 -9
View File
@@ -23,7 +23,7 @@ public sealed class Mmchannel : ITValuePublisher
private long _index; private long _index;
[StructLayout(LayoutKind.Auto)] [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 _state;
private State _p_state; private State _p_state;
@@ -52,7 +52,7 @@ public sealed class Mmchannel : ITValuePublisher
_minDeque = new MonotonicDeque(_period); _minDeque = new MonotonicDeque(_period);
_count = 0; _count = 0;
_index = -1; _index = -1;
_state = new State(double.NaN, double.NaN, false); _state = new State(double.NaN, double.NaN);
_p_state = _state; _p_state = _state;
Name = $"Mmchannel({period})"; Name = $"Mmchannel({period})";
@@ -112,7 +112,8 @@ public sealed class Mmchannel : ITValuePublisher
_state = _p_state; _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); var (high, low) = GetValid(input.High, input.Low);
// If still no valid data, return NaN placeholders // If still no valid data, return NaN placeholders
@@ -143,11 +144,6 @@ public sealed class Mmchannel : ITValuePublisher
double top = _maxDeque.GetExtremum(_hBuf); double top = _maxDeque.GetExtremum(_hBuf);
double bot = _minDeque.GetExtremum(_lBuf); double bot = _minDeque.GetExtremum(_lBuf);
if (!IsHot && _count >= _period)
{
_state = _state with { IsHot = true };
}
// Last returns Upper by default for single-value compatibility // Last returns Upper by default for single-value compatibility
Last = new TValue(input.Time, top); Last = new TValue(input.Time, top);
Upper = new TValue(input.Time, top); Upper = new TValue(input.Time, top);
@@ -218,7 +214,7 @@ public sealed class Mmchannel : ITValuePublisher
_minDeque.Reset(); _minDeque.Reset();
_count = 0; _count = 0;
_index = -1; _index = -1;
_state = new State(double.NaN, double.NaN, false); _state = new State(double.NaN, double.NaN);
_p_state = _state; _p_state = _state;
Last = default; Last = default;
Upper = default; Upper = default;
@@ -262,6 +258,11 @@ public sealed class Mmchannel : ITValuePublisher
public static (TSeries Upper, TSeries Lower) Batch(TBarSeries source, int period) 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; int len = source.Count;
var tUpper = new List<long>(len); var tUpper = new List<long>(len);
var vUpper = new List<double>(len); var vUpper = new List<double>(len);
+1 -1
View File
@@ -237,7 +237,7 @@ public sealed class Pchannel : ITValuePublisher
double bot = _lBuf[_lDeque[_lHead] % _period]; double bot = _lBuf[_lDeque[_lHead] % _period];
double mid = (top + bot) * 0.5; double mid = (top + bot) * 0.5;
if (!IsHot && _count >= _period) if (!_state.IsHot && _count >= _period)
{ {
_state = _state with { IsHot = true }; _state = _state with { IsHot = true };
} }
+3 -1
View File
@@ -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) 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); var results = indicator.Update(source);
return (results, indicator); return (results, indicator);
} }
+33
View File
@@ -322,12 +322,45 @@ public sealed class Starchannel : ITValuePublisher
upper[0] = close[0]; upper[0] = close[0];
lower[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++) for (int i = 1; i < len; i++)
{ {
double c = close[i]; double c = close[i];
double h = high[i]; double h = high[i];
double l = low[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 // SMA: add current, subtract oldest if beyond window
if (i < period) if (i < period)
{ {
+1 -1
View File
@@ -8,7 +8,7 @@ public class StbandsIndicator : Indicator, IWatchlistIndicator
[InputParameter("Period", sortIndex: 1, minimum: 1, maximum: 1000, increment: 1, decimalPlaces: 0)] [InputParameter("Period", sortIndex: 1, minimum: 1, maximum: 1000, increment: 1, decimalPlaces: 0)]
public int Period { get; set; } = 10; 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; public double Multiplier { get; set; } = 3.0;
[InputParameter("Show cold values", sortIndex: 21)] [InputParameter("Show cold values", sortIndex: 21)]
+7 -3
View File
@@ -124,11 +124,15 @@ public sealed class Ubands : AbstractBase
} }
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private double GetFiniteValue(double value) private double GetFiniteValue(double value, bool isNew)
{ {
if (double.IsFinite(value)) 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 value;
} }
return double.IsFinite(_state.LastValidValue) ? _state.LastValidValue : 0; return double.IsFinite(_state.LastValidValue) ? _state.LastValidValue : 0;
@@ -149,7 +153,7 @@ public sealed class Ubands : AbstractBase
_state = _p_state; _state = _p_state;
} }
double val = GetFiniteValue(input.Value); double val = GetFiniteValue(input.Value, isNew);
// Initialize on first value // Initialize on first value
if (!_state.IsInitialized) if (!_state.IsInitialized)
+1 -1
View File
@@ -11,7 +11,7 @@ public class UchannelIndicator : Indicator, IWatchlistIndicator
[InputParameter("Center Period", sortIndex: 2, minimum: 1, maximum: 1000, increment: 1, decimalPlaces: 0)] [InputParameter("Center Period", sortIndex: 2, minimum: 1, maximum: 1000, increment: 1, decimalPlaces: 0)]
public int CenterPeriod { get; set; } = 20; 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; public double Multiplier { get; set; } = 1.0;
[InputParameter("Show cold values", sortIndex: 21)] [InputParameter("Show cold values", sortIndex: 21)]
+5 -3
View File
@@ -441,17 +441,18 @@ public class VwapbandsTests
double[] upper2 = new double[5]; double[] upper2 = new double[5];
double[] lower2 = new double[5]; double[] lower2 = new double[5];
double[] vwap = new double[5]; double[] vwap = new double[5];
double[] stdDev = new double[5];
double[] wrongSize = new double[3]; double[] wrongSize = new double[3];
// Multiplier must be >= MinMultiplier // Multiplier must be >= MinMultiplier
Assert.Throws<ArgumentOutOfRangeException>(() => Assert.Throws<ArgumentOutOfRangeException>(() =>
Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(), 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 // All arrays must be same length
Assert.Throws<ArgumentException>(() => Assert.Throws<ArgumentException>(() =>
Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(), 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] [Fact]
@@ -464,9 +465,10 @@ public class VwapbandsTests
double[] upper2 = new double[5]; double[] upper2 = new double[5];
double[] lower2 = new double[5]; double[] lower2 = new double[5];
double[] vwap = new double[5]; double[] vwap = new double[5];
double[] stdDev = new double[5];
Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(), 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) foreach (var val in vwap)
{ {
@@ -124,11 +124,12 @@ public sealed class VwapbandsValidationTests : IDisposable
double[] spanLower1 = new double[bars.Count]; double[] spanLower1 = new double[bars.Count];
double[] spanUpper2 = new double[bars.Count]; double[] spanUpper2 = new double[bars.Count];
double[] spanLower2 = new double[bars.Count]; double[] spanLower2 = new double[bars.Count];
double[] spanStdDev = new double[bars.Count];
Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(), Vwapbands.Calculate(price.AsSpan(), volume.AsSpan(),
spanUpper1.AsSpan(), spanLower1.AsSpan(), spanUpper1.AsSpan(), spanLower1.AsSpan(),
spanUpper2.AsSpan(), spanLower2.AsSpan(), spanUpper2.AsSpan(), spanLower2.AsSpan(),
spanVwap.AsSpan(), multiplier); spanVwap.AsSpan(), spanStdDev.AsSpan(), multiplier);
// Compare last 100 values // Compare last 100 values
int compareCount = Math.Min(100, bars.Count - 2); int compareCount = Math.Min(100, bars.Count - 2);
+12 -1
View File
@@ -351,6 +351,15 @@ public sealed class Vwapbands : AbstractBase
/// <summary> /// <summary>
/// Calculates VWAP Bands using span arrays. /// Calculates VWAP Bands using span arrays.
/// </summary> /// </summary>
/// <param name="price">Source price values (typically HLC3)</param>
/// <param name="volume">Volume values</param>
/// <param name="upper1">Output span for upper band at 1σ</param>
/// <param name="lower1">Output span for lower band at 1σ</param>
/// <param name="upper2">Output span for upper band at 2σ</param>
/// <param name="lower2">Output span for lower band at 2σ</param>
/// <param name="vwap">Output span for VWAP values</param>
/// <param name="stdDev">Output span for standard deviation values</param>
/// <param name="multiplier">Band multiplier (default 1.0)</param>
public static void Calculate( public static void Calculate(
ReadOnlySpan<double> price, ReadOnlySpan<double> price,
ReadOnlySpan<double> volume, ReadOnlySpan<double> volume,
@@ -359,11 +368,12 @@ public sealed class Vwapbands : AbstractBase
Span<double> upper2, Span<double> upper2,
Span<double> lower2, Span<double> lower2,
Span<double> vwap, Span<double> vwap,
Span<double> stdDev,
double multiplier = DefaultMultiplier) double multiplier = DefaultMultiplier)
{ {
int len = price.Length; int len = price.Length;
if (len != volume.Length || len != upper1.Length || len != lower1.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)); 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); double stdev = Math.Sqrt(variance);
vwap[i] = vwapVal; vwap[i] = vwapVal;
stdDev[i] = stdev;
upper1[i] = vwapVal + multiplier * stdev; upper1[i] = vwapVal + multiplier * stdev;
lower1[i] = vwapVal - multiplier * stdev; lower1[i] = vwapVal - multiplier * stdev;
upper2[i] = vwapVal + 2.0 * multiplier * stdev; upper2[i] = vwapVal + 2.0 * multiplier * stdev;
+22 -2
View File
@@ -31,8 +31,8 @@ public class VwapsdIndicator : Indicator, IWatchlistIndicator
Description = "Volume weighted average price with configurable standard deviation bands"; Description = "Volume weighted average price with configurable standard deviation bands";
VwapSeries = new("VWAP", Color.Blue, 2, LineStyle.Solid); VwapSeries = new("VWAP", Color.Blue, 2, LineStyle.Solid);
UpperSeries = new($"Upper (+{NumDevs}σ)", Color.Red, 1, LineStyle.Solid); UpperSeries = new("Upper", Color.Red, 1, LineStyle.Solid);
LowerSeries = new($"Lower (-{NumDevs}σ)", Color.Green, 1, LineStyle.Solid); LowerSeries = new("Lower", Color.Green, 1, LineStyle.Solid);
WidthSeries = new("Width", Color.Gray, 1, LineStyle.Dot); WidthSeries = new("Width", Color.Gray, 1, LineStyle.Dot);
AddLineSeries(VwapSeries); AddLineSeries(VwapSeries);
@@ -47,9 +47,29 @@ public class VwapsdIndicator : Indicator, IWatchlistIndicator
protected override void OnInit() protected override void OnInit()
{ {
vwapsd = new(NumDevs); vwapsd = new(NumDevs);
if (UpperSeries != null)
{
UpperSeries.Name = $"Upper (+{NumDevs:F1}σ)";
}
if (LowerSeries != null)
{
LowerSeries.Name = $"Lower (-{NumDevs:F1}σ)";
}
base.OnInit(); 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) protected override void OnUpdate(UpdateArgs args)
{ {
var item = HistoricalData[0, SeekOriginHistory.End]; var item = HistoricalData[0, SeekOriginHistory.End];
+6 -4
View File
@@ -467,22 +467,23 @@ public class VwapsdTests
double[] upper = new double[5]; double[] upper = new double[5];
double[] lower = new double[5]; double[] lower = new double[5];
double[] vwap = new double[5]; double[] vwap = new double[5];
double[] stdDev = new double[5];
double[] wrongSize = new double[3]; double[] wrongSize = new double[3];
// NumDevs must be >= MinNumDevs // NumDevs must be >= MinNumDevs
Assert.Throws<ArgumentOutOfRangeException>(() => Assert.Throws<ArgumentOutOfRangeException>(() =>
Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(), 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 // NumDevs must be <= MaxNumDevs
Assert.Throws<ArgumentOutOfRangeException>(() => Assert.Throws<ArgumentOutOfRangeException>(() =>
Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(), 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 // All arrays must be same length
Assert.Throws<ArgumentException>(() => Assert.Throws<ArgumentException>(() =>
Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(), 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] [Fact]
@@ -493,9 +494,10 @@ public class VwapsdTests
double[] upper = new double[5]; double[] upper = new double[5];
double[] lower = new double[5]; double[] lower = new double[5];
double[] vwap = new double[5]; double[] vwap = new double[5];
double[] stdDev = new double[5];
Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(), 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) foreach (var val in vwap)
{ {
@@ -118,10 +118,11 @@ public sealed class VwapsdValidationTests : IDisposable
double[] spanVwap = new double[bars.Count]; double[] spanVwap = new double[bars.Count];
double[] spanUpper = new double[bars.Count]; double[] spanUpper = new double[bars.Count];
double[] spanLower = new double[bars.Count]; double[] spanLower = new double[bars.Count];
double[] spanStdDev = new double[bars.Count];
Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(), Vwapsd.Calculate(price.AsSpan(), volume.AsSpan(),
spanUpper.AsSpan(), spanLower.AsSpan(), spanUpper.AsSpan(), spanLower.AsSpan(),
spanVwap.AsSpan(), numDevs); spanVwap.AsSpan(), spanStdDev.AsSpan(), numDevs);
// Compare last 100 values // Compare last 100 values
int compareCount = Math.Min(100, bars.Count - 2); int compareCount = Math.Min(100, bars.Count - 2);
+10 -1
View File
@@ -337,16 +337,24 @@ public sealed class Vwapsd : AbstractBase
/// <summary> /// <summary>
/// Calculates VWAP SD Bands using span arrays. /// Calculates VWAP SD Bands using span arrays.
/// </summary> /// </summary>
/// <param name="price">Source price values (typically HLC3)</param>
/// <param name="volume">Volume values</param>
/// <param name="upper">Output span for upper band</param>
/// <param name="lower">Output span for lower band</param>
/// <param name="vwap">Output span for VWAP values</param>
/// <param name="stdDev">Output span for standard deviation values</param>
/// <param name="numDevs">Number of standard deviations for bands (default 2.0)</param>
public static void Calculate( public static void Calculate(
ReadOnlySpan<double> price, ReadOnlySpan<double> price,
ReadOnlySpan<double> volume, ReadOnlySpan<double> volume,
Span<double> upper, Span<double> upper,
Span<double> lower, Span<double> lower,
Span<double> vwap, Span<double> vwap,
Span<double> stdDev,
double numDevs = DefaultNumDevs) double numDevs = DefaultNumDevs)
{ {
int len = price.Length; 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)); 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); double stdev = Math.Sqrt(variance);
vwap[i] = vwapVal; vwap[i] = vwapVal;
stdDev[i] = stdev;
upper[i] = vwapVal + numDevs * stdev; upper[i] = vwapVal + numDevs * stdev;
lower[i] = vwapVal - numDevs * stdev; lower[i] = vwapVal - numDevs * stdev;
} }
+8 -8
View File
@@ -16,7 +16,7 @@ namespace QuanTAlib;
[SkipLocalsInit] [SkipLocalsInit]
public sealed class MonotonicDeque public sealed class MonotonicDeque
{ {
private readonly int[] _deque; private readonly long[] _deque;
private readonly int _period; private readonly int _period;
private int _head; private int _head;
private int _count; private int _count;
@@ -24,7 +24,7 @@ public sealed class MonotonicDeque
/// <summary> /// <summary>
/// Gets the current front index (the index of the current extremum). /// Gets the current front index (the index of the current extremum).
/// </summary> /// </summary>
public int FrontIndex => _count > 0 ? _deque[_head] : -1; public long FrontIndex => _count > 0 ? _deque[_head] : -1;
/// <summary> /// <summary>
/// Gets the current element count in the deque. /// Gets the current element count in the deque.
@@ -43,7 +43,7 @@ public sealed class MonotonicDeque
} }
_period = period; _period = period;
_deque = new int[period]; _deque = new long[period];
_head = 0; _head = 0;
_count = 0; _count = 0;
} }
@@ -70,7 +70,7 @@ public sealed class MonotonicDeque
while (_count > 0) while (_count > 0)
{ {
int backIdx = (_head + _count - 1) % _period; int backIdx = (_head + _count - 1) % _period;
int bufIdx = _deque[backIdx] % _period; int bufIdx = (int)(_deque[backIdx] % _period);
if (buffer[bufIdx] <= value) if (buffer[bufIdx] <= value)
{ {
_count--; _count--;
@@ -83,7 +83,7 @@ public sealed class MonotonicDeque
// Push new index // Push new index
int tail = (_head + _count) % _period; int tail = (_head + _count) % _period;
_deque[tail] = (int)logicalIndex; _deque[tail] = logicalIndex;
_count++; _count++;
} }
@@ -109,7 +109,7 @@ public sealed class MonotonicDeque
while (_count > 0) while (_count > 0)
{ {
int backIdx = (_head + _count - 1) % _period; int backIdx = (_head + _count - 1) % _period;
int bufIdx = _deque[backIdx] % _period; int bufIdx = (int)(_deque[backIdx] % _period);
if (buffer[bufIdx] >= value) if (buffer[bufIdx] >= value)
{ {
_count--; _count--;
@@ -122,7 +122,7 @@ public sealed class MonotonicDeque
// Push new index // Push new index
int tail = (_head + _count) % _period; int tail = (_head + _count) % _period;
_deque[tail] = (int)logicalIndex; _deque[tail] = logicalIndex;
_count++; _count++;
} }
@@ -134,7 +134,7 @@ public sealed class MonotonicDeque
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public double GetExtremum(double[] buffer) public double GetExtremum(double[] buffer)
{ {
return _count > 0 ? buffer[_deque[_head] % _period] : double.NaN; return _count > 0 ? buffer[(int)(_deque[_head] % _period)] : double.NaN;
} }
/// <summary> /// <summary>
+8 -7
View File
@@ -309,17 +309,18 @@ public class MdapeTests
} }
[Fact] [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); var mdape = new Mdape(3);
mdape.Update(0.0, 10); mdape.Update(0.0, 10); // actual=1.0 (substituted), pred=10 → |1-10|/1 * 100 = 900%
mdape.Update(0.0, 20); mdape.Update(0.0, 20); // actual=1.0 (substituted), pred=20 → |1-20|/1 * 100 = 1900%
mdape.Update(0.0, 30); mdape.Update(0.0, 30); // actual=1.0 (substituted), pred=30 → |1-30|/1 * 100 = 2900%
// With epsilon protection, all errors are 0 // Median of [900, 1900, 2900] = 1900
Assert.Equal(0.0, mdape.Last.Value, Precision); Assert.Equal(1900.0, mdape.Last.Value, Precision);
} }
[Fact] [Fact]
+7 -4
View File
@@ -74,9 +74,12 @@ public sealed class Mdape : AbstractBase
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private TValue UpdateCore(DateTime time, double actualVal, double predictedVal, bool isNew) 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 else
{ {
@@ -92,10 +95,10 @@ public sealed class Mdape : AbstractBase
_state.LastValidPredicted = predictedVal; _state.LastValidPredicted = predictedVal;
} }
// Calculate absolute percentage error // Calculate absolute percentage error (absActual guaranteed >= 1e-10 by validation above)
double absActual = Math.Abs(actualVal); double absActual = Math.Abs(actualVal);
double absError = Math.Abs(actualVal - predictedVal); 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) if (isNew)
{ {
+11 -4
View File
@@ -62,6 +62,17 @@ public sealed class Rae : AbstractBase
double actualVal = actual.Value; double actualVal = actual.Value;
double predictedVal = predicted.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)) if (!double.IsFinite(actualVal))
{ {
actualVal = double.IsFinite(_state.LastValidActual) ? _state.LastValidActual : 0.0; actualVal = double.IsFinite(_state.LastValidActual) ? _state.LastValidActual : 0.0;
@@ -82,8 +93,6 @@ public sealed class Rae : AbstractBase
if (isNew) if (isNew)
{ {
_p_state = _state;
// Update actual buffer for mean calculation // Update actual buffer for mean calculation
double removedActual = _actualBuffer.Count == _actualBuffer.Capacity ? _actualBuffer.Oldest : 0.0; double removedActual = _actualBuffer.Count == _actualBuffer.Capacity ? _actualBuffer.Oldest : 0.0;
_state.ActualSum = _state.ActualSum - removedActual + actualVal; _state.ActualSum = _state.ActualSum - removedActual + actualVal;
@@ -115,8 +124,6 @@ public sealed class Rae : AbstractBase
} }
else else
{ {
_state = _p_state;
// Update buffers and recalculate sums (buffer state is inconsistent with _p_state) // Update buffers and recalculate sums (buffer state is inconsistent with _p_state)
_actualBuffer.UpdateNewest(actualVal); _actualBuffer.UpdateNewest(actualVal);
_state.ActualSum = _actualBuffer.RecalculateSum(); _state.ActualSum = _actualBuffer.RecalculateSum();
+12 -12
View File
@@ -222,22 +222,22 @@ public sealed class Wmape : AbstractBase
double lastValidActual = 0; double lastValidActual = 0;
double lastValidPredicted = 0; double lastValidPredicted = 0;
for (int k = 0; k < len; k++) for (int k = 0; k < len; k++)
{
if (double.IsFinite(actual[k]))
{ {
lastValidActual = actual[k]; if (double.IsFinite(actual[k]))
break; {
lastValidActual = actual[k];
break;
}
} }
} for (int k = 0; k < len; k++)
for (int k = 0; k < len; k++)
{
if (double.IsFinite(predicted[k]))
{ {
lastValidPredicted = predicted[k]; if (double.IsFinite(predicted[k]))
break; {
lastValidPredicted = predicted[k];
break;
}
} }
}
int bufferIndex = 0; int bufferIndex = 0;
int i = 0; int i = 0;
+2 -2
View File
@@ -13,7 +13,7 @@ public class CmfIndicatorTests
Assert.Equal(20, indicator.Period); Assert.Equal(20, indicator.Period);
Assert.True(indicator.SeparateWindow); Assert.True(indicator.SeparateWindow);
Assert.True(indicator.OnBackGround); Assert.True(indicator.OnBackGround);
Assert.Equal(20, CmfIndicator.MinHistoryDepths); Assert.Equal(20, indicator.MinHistoryDepths);
} }
[Fact] [Fact]
@@ -28,7 +28,7 @@ public class CmfIndicatorTests
{ {
var indicator = new CmfIndicator(); var indicator = new CmfIndicator();
Assert.Equal(20, CmfIndicator.MinHistoryDepths); Assert.Equal(20, indicator.MinHistoryDepths);
Assert.Equal(20, ((IWatchlistIndicator)indicator).MinHistoryDepths); Assert.Equal(20, ((IWatchlistIndicator)indicator).MinHistoryDepths);
} }
+2 -2
View File
@@ -16,8 +16,8 @@ public sealed class CmfIndicator : Indicator, IWatchlistIndicator
private Cmf _cmf = null!; private Cmf _cmf = null!;
private readonly LineSeries _series; private readonly LineSeries _series;
public static int MinHistoryDepths => 20; public int MinHistoryDepths => Period;
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths; int IWatchlistIndicator.MinHistoryDepths => Period;
public override string ShortName => $"CMF({Period})"; public override string ShortName => $"CMF({Period})";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/volume/cmf/Cmf.Quantower.cs"; public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/volume/cmf/Cmf.Quantower.cs";