mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 05:48:06 +00:00
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:
@@ -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/**"
|
||||||
|
# ==============================================
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user