mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-24 13:38:05 +00:00
code reviews
This commit is contained in:
+174
-23
@@ -3,38 +3,164 @@
|
|||||||
# Documentation: https://docs.coderabbit.ai/reference/configuration
|
# Documentation: https://docs.coderabbit.ai/reference/configuration
|
||||||
|
|
||||||
language: en-US
|
language: en-US
|
||||||
tone_instructions: "Focus on performance, memory allocation, SIMD optimization, and numerical accuracy. Flag any heap allocations in hot paths."
|
|
||||||
early_access: true
|
early_access: true
|
||||||
enable_free_tier: true
|
enable_free_tier: true
|
||||||
|
|
||||||
|
tone_instructions: |
|
||||||
|
Voice: Direct, technically precise, skeptical-architect perspective. Persuade through evidence and measurable claims, not marketing speak.
|
||||||
|
|
||||||
|
DO:
|
||||||
|
- Be ruthless with math/correctness, kind to humans
|
||||||
|
- Use precise numbers and quantified impacts (e.g., "3-4% divergence", "~15 cycles per op")
|
||||||
|
- Steel-man alternatives before rebutting with data
|
||||||
|
- Follow evidence chain: Why → How → Proof → So what
|
||||||
|
- Vary sentence length deliberately; use direct cadence
|
||||||
|
- Treat code as primary evidence
|
||||||
|
- Include concrete performance implications (cycles, allocations, memory footprint)
|
||||||
|
|
||||||
|
DON'T:
|
||||||
|
- Use forbidden words: delve, leverage, pivotal, tapestry, landscape, furthermore, "is all about", "unlock the power", transformative, foster, seamless, ecosystem
|
||||||
|
- Use em-dashes or formulaic balanced pro/con structures
|
||||||
|
- Say "we" - be direct
|
||||||
|
- Make humor in performance, security, or math correctness claims
|
||||||
|
- Be vague - quantify everything possible
|
||||||
|
|
||||||
|
PRIORITIES (this library):
|
||||||
|
- Zero heap allocations in hot paths (GC pressure is the enemy)
|
||||||
|
- SIMD/FMA optimization opportunities
|
||||||
|
- O(1) streaming updates
|
||||||
|
- NaN/Infinity robustness
|
||||||
|
- State rollback correctness (isNew=false)
|
||||||
|
- Numerical stability at edge cases
|
||||||
|
|
||||||
reviews:
|
reviews:
|
||||||
profile: assertive # More thorough for a performance-critical library
|
profile: assertive
|
||||||
request_changes_workflow: false
|
request_changes_workflow: false
|
||||||
high_level_summary: true
|
high_level_summary: true
|
||||||
high_level_summary_placeholder: "@coderabbitai summary"
|
high_level_summary_placeholder: "@coderabbitai summary"
|
||||||
|
high_level_summary_instructions: |
|
||||||
|
Generate a concise technical summary optimized for a high-performance C# trading library.
|
||||||
|
|
||||||
|
FORMAT:
|
||||||
|
## Summary
|
||||||
|
[1-2 sentence TL;DR: what changed and why it matters for performance/correctness]
|
||||||
|
|
||||||
|
## Changes by Category
|
||||||
|
|
||||||
|
### 🚀 Performance
|
||||||
|
- [SIMD/FMA optimizations, allocation reductions, O(1) improvements]
|
||||||
|
|
||||||
|
### 🔧 Indicators
|
||||||
|
- [New/modified indicators with parameter signatures]
|
||||||
|
|
||||||
|
### 🛡️ Robustness
|
||||||
|
- [NaN/Infinity handling, state rollback fixes, edge cases]
|
||||||
|
|
||||||
|
### 🧪 Testing
|
||||||
|
- [Validation coverage, new test categories]
|
||||||
|
|
||||||
|
### 📚 Documentation
|
||||||
|
- [Doc updates, validation matrix changes]
|
||||||
|
|
||||||
|
### 🔌 Quantower
|
||||||
|
- [Adapter additions/changes]
|
||||||
|
|
||||||
|
(Omit empty categories)
|
||||||
|
|
||||||
|
## Impact Assessment
|
||||||
|
| Metric | Before | After | Delta |
|
||||||
|
|--------|--------|-------|-------|
|
||||||
|
| Allocations in hot path | ? | ? | ? |
|
||||||
|
| SIMD coverage | ? | ? | ? |
|
||||||
|
| Test coverage | ? | ? | ? |
|
||||||
|
|
||||||
|
(Include table only when quantifiable changes exist)
|
||||||
|
|
||||||
|
## Breaking Changes
|
||||||
|
- [List any API breaks with migration path, or "None"]
|
||||||
|
|
||||||
|
auto_title_instructions: |
|
||||||
|
Generate PR title following conventional commit format: <type>: <imperative verb> <what> [scope]
|
||||||
|
|
||||||
|
TYPES (required prefix):
|
||||||
|
- feat: New indicator, API addition, capability
|
||||||
|
- perf: SIMD optimization, allocation reduction, O(1) improvement
|
||||||
|
- fix: Bug fix, numerical stability, edge case handling
|
||||||
|
- refactor: Code restructure without behavior change
|
||||||
|
- test: Test additions, validation coverage
|
||||||
|
- docs: Documentation, markdown updates
|
||||||
|
|
||||||
|
RULES:
|
||||||
|
- Subject line ≤50 characters
|
||||||
|
- Use imperative mood ("add", "fix", "optimize" not "added", "fixes", "optimizes")
|
||||||
|
- Include [scope] when change is localized (e.g., [Ema], [RingBuffer], [Quantower])
|
||||||
|
- For performance changes, append brief metric hint if space allows
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
- feat: add Jma indicator with adaptive smoothing [trends_IIR]
|
||||||
|
- perf: eliminate allocations in Sma.Update hot path
|
||||||
|
- fix: handle NaN in Rsi state rollback [isNew=false]
|
||||||
|
- refactor: extract SIMD helpers to core/simd
|
||||||
|
- test: add validation tests for Ema vs TA-Lib
|
||||||
|
- docs: update validation matrix for momentum indicators
|
||||||
|
|
||||||
|
AVOID:
|
||||||
|
- Vague titles ("update code", "fix bug", "improvements")
|
||||||
|
- Past tense ("added", "fixed")
|
||||||
|
- Articles ("add the", "fix a")
|
||||||
|
- Exceeding 50 chars (truncate scope if needed)
|
||||||
|
|
||||||
review_status: true
|
review_status: true
|
||||||
commit_status: true
|
commit_status: true
|
||||||
collapse_walkthrough: false
|
collapse_walkthrough: false
|
||||||
changed_files_summary: true
|
changed_files_summary: true
|
||||||
sequence_diagrams: false # Not useful for indicator calculations
|
sequence_diagrams: false
|
||||||
estimate_code_review_effort: true
|
estimate_code_review_effort: true
|
||||||
assess_linked_issues: true
|
assess_linked_issues: true
|
||||||
related_issues: true
|
related_issues: true
|
||||||
related_prs: true
|
related_prs: true
|
||||||
suggested_labels: true
|
suggested_labels: true
|
||||||
suggested_reviewers: true
|
suggested_reviewers: true
|
||||||
poem: false # Keep it professional
|
poem: false
|
||||||
|
|
||||||
path_filters:
|
path_filters:
|
||||||
# Include all source files
|
# --- Core Infrastructure ---
|
||||||
- "**/*.cs"
|
- "lib/core/**/*.cs"
|
||||||
- "**/*.md"
|
- "lib/feeds/**/*.cs"
|
||||||
- "**/*.yaml"
|
- "lib/numerics/**/*.cs"
|
||||||
- "**/*.yml"
|
|
||||||
- "**/*.csproj"
|
|
||||||
- "**/*.props"
|
|
||||||
|
|
||||||
# Exclude build/IDE artifacts
|
# --- Trend Indicators ---
|
||||||
|
- "lib/trends_FIR/**/*.cs"
|
||||||
|
- "lib/trends_IIR/**/*.cs"
|
||||||
|
|
||||||
|
# --- Oscillators & Momentum ---
|
||||||
|
- "lib/oscillators/**/*.cs"
|
||||||
|
- "lib/momentum/**/*.cs"
|
||||||
|
|
||||||
|
# --- Channels & Volatility ---
|
||||||
|
- "lib/channels/**/*.cs"
|
||||||
|
- "lib/volatility/**/*.cs"
|
||||||
|
|
||||||
|
# --- Dynamics & Reversals ---
|
||||||
|
- "lib/dynamics/**/*.cs"
|
||||||
|
- "lib/reversals/**/*.cs"
|
||||||
|
|
||||||
|
# --- Statistics & Errors ---
|
||||||
|
- "lib/statistics/**/*.cs"
|
||||||
|
- "lib/errors/**/*.cs"
|
||||||
|
|
||||||
|
# --- Cycles, Filters, Forecasts ---
|
||||||
|
- "lib/cycles/**/*.cs"
|
||||||
|
- "lib/filters/**/*.cs"
|
||||||
|
- "lib/forecasts/**/*.cs"
|
||||||
|
|
||||||
|
# --- Volume ---
|
||||||
|
- "lib/volume/**/*.cs"
|
||||||
|
|
||||||
|
# --- Quantower Adapters ---
|
||||||
|
- "quantower/**/*.cs"
|
||||||
|
|
||||||
|
# --- Always Excluded ---
|
||||||
- "!**/bin/**"
|
- "!**/bin/**"
|
||||||
- "!**/obj/**"
|
- "!**/obj/**"
|
||||||
- "!**/.vs/**"
|
- "!**/.vs/**"
|
||||||
@@ -45,19 +171,38 @@ reviews:
|
|||||||
- "!**/_site/**"
|
- "!**/_site/**"
|
||||||
- "!**/ndepend/NDependOut/**"
|
- "!**/ndepend/NDependOut/**"
|
||||||
- "!**/perf/publish/**"
|
- "!**/perf/publish/**"
|
||||||
|
- "!**/temp/**"
|
||||||
|
- "!**/*.sarif"
|
||||||
|
- "!**/*.snupkg"
|
||||||
|
- "!**/*.nupkg"
|
||||||
|
|
||||||
path_instructions:
|
path_instructions:
|
||||||
- path: "**/**"
|
- path: "**/**"
|
||||||
instructions: |
|
instructions: |
|
||||||
- ensure thread safety, zero allocations, and proper Span<T>/Memory<T> usage."
|
QuanTAlib code review checklist:
|
||||||
- verify numerical stability, check for SIMD and FMA opportunities
|
|
||||||
- verify RingBuffer usage, ensure O(1) updates validate state rollback for isNew=false.
|
MEMORY & PERFORMANCE:
|
||||||
- check for numerical stability at edge cases, verify NaN/Infinity handling
|
|
||||||
- No heap allocations in Update() methods
|
- No heap allocations in Update() methods
|
||||||
- Use Math.FusedMultiplyAdd for a*b+c patterns
|
- Use Math.FusedMultiplyAdd for a*b+c patterns
|
||||||
- Support isNew=false for bar correction (state rollback)
|
- [MethodImpl(AggressiveInlining)] for hot paths
|
||||||
|
- SIMD opportunities in Calculate(Span) methods
|
||||||
|
- Verify O(1) streaming updates where math allows
|
||||||
|
|
||||||
|
STATE & CORRECTNESS:
|
||||||
|
- Support isNew=false for bar correction (state rollback via _p_state)
|
||||||
- Handle NaN/Infinity with last-valid-value substitution
|
- Handle NaN/Infinity with last-valid-value substitution
|
||||||
- Use [MethodImpl(AggressiveInlining)] for hot paths
|
- Verify RingBuffer usage for sliding windows
|
||||||
|
- Check numerical stability at edge cases (overflow, underflow, div/0)
|
||||||
|
|
||||||
|
API CONSISTENCY:
|
||||||
|
- Dual API: stateful Update + stateless static Calculate
|
||||||
|
- ArgumentException with nameof(param) for validation
|
||||||
|
- ITValuePublisher implementation for reactive chaining
|
||||||
|
|
||||||
|
PATTERNS:
|
||||||
|
- State as private record struct
|
||||||
|
- List<T> for SoA storage (suppress MA0016 locally)
|
||||||
|
- DateTime.UtcNow (never DateTime.Now)
|
||||||
|
|
||||||
auto_review:
|
auto_review:
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -72,12 +217,19 @@ reviews:
|
|||||||
|
|
||||||
finishing_touches:
|
finishing_touches:
|
||||||
docstrings:
|
docstrings:
|
||||||
enabled: false # XML docs handled separately
|
enabled: false
|
||||||
unit_tests:
|
unit_tests:
|
||||||
enabled: false # Tests excluded from review
|
enabled: false
|
||||||
|
|
||||||
tools:
|
tools:
|
||||||
# Disable tools not relevant to C#
|
ast-grep:
|
||||||
|
rule_dirs:
|
||||||
|
- ".coderabbit/ast-grep-rules"
|
||||||
|
util_dirs: []
|
||||||
|
essential_rules: true
|
||||||
|
packages:
|
||||||
|
- "ast-grep-essentials"
|
||||||
|
|
||||||
shellcheck:
|
shellcheck:
|
||||||
enabled: false
|
enabled: false
|
||||||
ruff:
|
ruff:
|
||||||
@@ -99,13 +251,12 @@ reviews:
|
|||||||
detekt:
|
detekt:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
||||||
# Keep enabled for config files
|
|
||||||
yamllint:
|
yamllint:
|
||||||
enabled: true
|
enabled: true
|
||||||
markdownlint:
|
markdownlint:
|
||||||
enabled: true
|
enabled: true
|
||||||
gitleaks:
|
gitleaks:
|
||||||
enabled: true # Security - detect secrets
|
enabled: true
|
||||||
github-checks:
|
github-checks:
|
||||||
enabled: true
|
enabled: true
|
||||||
timeout_ms: 90000
|
timeout_ms: 90000
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# QuanTAlib: Enforce DateTime.UtcNow over DateTime.Now
|
||||||
|
# DCT rule: always use DateTime.UtcNow (never DateTime.Now)
|
||||||
|
|
||||||
|
id: no-datetime-now
|
||||||
|
language: csharp
|
||||||
|
severity: error
|
||||||
|
message: "Use DateTime.UtcNow instead of DateTime.Now - QuanTAlib requires UTC timestamps for consistency"
|
||||||
|
note: "DateTime.Now includes local timezone which causes issues in distributed trading systems"
|
||||||
|
rule:
|
||||||
|
pattern: DateTime.Now
|
||||||
|
fix: DateTime.UtcNow
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# QuanTAlib: Forbid System.Random in tests - use GBM helper instead
|
||||||
|
# DCT rule: Tests MUST use GBM helper, never System.Random
|
||||||
|
|
||||||
|
id: no-random-in-tests
|
||||||
|
language: csharp
|
||||||
|
severity: error
|
||||||
|
message: "Use GBM helper instead of System.Random in tests - ensures reproducible test data"
|
||||||
|
note: "GBM (Geometric Brownian Motion) provides consistent, realistic price data for indicator testing"
|
||||||
|
rule:
|
||||||
|
pattern: new Random($$$)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# QuanTAlib: Require nameof() in ArgumentException
|
||||||
|
# DCT rule: ArgumentException + nameof(param) for proper analyzer support
|
||||||
|
|
||||||
|
id: require-nameof-in-exceptions
|
||||||
|
language: csharp
|
||||||
|
severity: warning
|
||||||
|
message: "Use nameof(parameter) instead of string literal in ArgumentException for refactoring safety"
|
||||||
|
note: "MA0015-friendly: nameof() ensures parameter name stays in sync during refactoring"
|
||||||
|
rule:
|
||||||
|
any:
|
||||||
|
- pattern: throw new ArgumentException($MSG, "$PARAM")
|
||||||
|
- pattern: throw new ArgumentNullException("$PARAM")
|
||||||
|
- pattern: throw new ArgumentOutOfRangeException("$PARAM")
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# QuanTAlib: Suggest FMA for multiply-add patterns
|
||||||
|
# DCT rule: Math.FusedMultiplyAdd(a, b, c) for a*b+c patterns in hot paths
|
||||||
|
|
||||||
|
id: suggest-fma-multiply-add
|
||||||
|
language: csharp
|
||||||
|
severity: hint
|
||||||
|
message: "Consider Math.FusedMultiplyAdd(a, b, c) for better precision and performance in hot paths"
|
||||||
|
note: "FMA provides single-rounding semantics and can be faster on modern CPUs. Use for EMA smoothing, IIR filters, weighted sums."
|
||||||
|
rule:
|
||||||
|
any:
|
||||||
|
- pattern: $A * $B + $C
|
||||||
|
- pattern: $A + $B * $C
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# QuanTAlib: Warn about LINQ in potential hot paths
|
||||||
|
# DCT rule 1: Hot paths allocation-free (no heap alloc); GC pressure enemy
|
||||||
|
|
||||||
|
id: warn-linq-methods
|
||||||
|
language: csharp
|
||||||
|
severity: warning
|
||||||
|
message: "LINQ method detected - verify this is not in a hot path (Update/Calculate). LINQ allocates and causes GC pressure."
|
||||||
|
note: "DCT rule 1: Hot paths must be allocation-free. Replace LINQ with for loops or Span operations in performance-critical code."
|
||||||
|
rule:
|
||||||
|
any:
|
||||||
|
- pattern: $EXPR.Where($$$)
|
||||||
|
- pattern: $EXPR.Select($$$)
|
||||||
|
- pattern: $EXPR.OrderBy($$$)
|
||||||
|
- pattern: $EXPR.OrderByDescending($$$)
|
||||||
|
- pattern: $EXPR.GroupBy($$$)
|
||||||
|
- pattern: $EXPR.ToList()
|
||||||
|
- pattern: $EXPR.ToArray()
|
||||||
|
- pattern: $EXPR.ToDictionary($$$)
|
||||||
|
- pattern: $EXPR.First($$$)
|
||||||
|
- pattern: $EXPR.FirstOrDefault($$$)
|
||||||
|
- pattern: $EXPR.Last($$$)
|
||||||
|
- pattern: $EXPR.LastOrDefault($$$)
|
||||||
|
- pattern: $EXPR.Single($$$)
|
||||||
|
- pattern: $EXPR.SingleOrDefault($$$)
|
||||||
|
- pattern: $EXPR.Any($$$)
|
||||||
|
- pattern: $EXPR.All($$$)
|
||||||
|
- pattern: $EXPR.Count($$$)
|
||||||
|
- pattern: $EXPR.Sum($$$)
|
||||||
|
- pattern: $EXPR.Average($$$)
|
||||||
|
- pattern: $EXPR.Min($$$)
|
||||||
|
- pattern: $EXPR.Max($$$)
|
||||||
|
- pattern: $EXPR.Aggregate($$$)
|
||||||
|
- pattern: $EXPR.Distinct($$$)
|
||||||
|
- pattern: $EXPR.Skip($$$)
|
||||||
|
- pattern: $EXPR.Take($$$)
|
||||||
|
- pattern: $EXPR.Zip($$$)
|
||||||
|
- pattern: $EXPR.Concat($$$)
|
||||||
+133
-39
@@ -39,7 +39,7 @@ public static class ErrorHelpers
|
|||||||
// Try SIMD path - NaN detection is integrated into the SIMD loop
|
// Try SIMD path - NaN detection is integrated into the SIMD loop
|
||||||
if (Avx2.IsSupported && len >= Vector256<double>.Count)
|
if (Avx2.IsSupported && len >= Vector256<double>.Count)
|
||||||
{
|
{
|
||||||
int processedCount = ComputeSignedErrorsSimdWithNaNDetection(actual, predicted, output, lastValidActual, lastValidPredicted);
|
int processedCount = ComputeSignedErrorsSimdWithNaNDetection(actual, predicted, output, ref lastValidActual, ref lastValidPredicted);
|
||||||
if (processedCount == len)
|
if (processedCount == len)
|
||||||
return; // All processed via SIMD
|
return; // All processed via SIMD
|
||||||
// Continue with scalar for remaining elements (NaN was detected)
|
// Continue with scalar for remaining elements (NaN was detected)
|
||||||
@@ -74,7 +74,7 @@ public static class ErrorHelpers
|
|||||||
// Try SIMD path - NaN detection is integrated into the SIMD loop (avoids double-pass)
|
// Try SIMD path - NaN detection is integrated into the SIMD loop (avoids double-pass)
|
||||||
if (Avx2.IsSupported && len >= Vector256<double>.Count)
|
if (Avx2.IsSupported && len >= Vector256<double>.Count)
|
||||||
{
|
{
|
||||||
int processedCount = ComputeAbsoluteErrorsSimdWithNaNDetection(actual, predicted, output, lastValidActual, lastValidPredicted);
|
int processedCount = ComputeAbsoluteErrorsSimdWithNaNDetection(actual, predicted, output, ref lastValidActual, ref lastValidPredicted);
|
||||||
if (processedCount == len)
|
if (processedCount == len)
|
||||||
return; // All processed via SIMD
|
return; // All processed via SIMD
|
||||||
// Continue with scalar for remaining elements (NaN was detected)
|
// Continue with scalar for remaining elements (NaN was detected)
|
||||||
@@ -88,7 +88,7 @@ public static class ErrorHelpers
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Computes squared errors: (actual - predicted)²
|
/// Computes squared errors: (actual - predicted)²
|
||||||
/// Uses AVX2 SIMD when available for clean data, with scalar fallback for NaN handling.
|
/// Uses AVX2 SIMD when available with integrated NaN detection, with scalar fallback for NaN handling.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static void ComputeSquaredErrors(
|
public static void ComputeSquaredErrors(
|
||||||
@@ -106,10 +106,14 @@ public static class ErrorHelpers
|
|||||||
double lastValidActual = FindFirstValidValue(actual);
|
double lastValidActual = FindFirstValidValue(actual);
|
||||||
double lastValidPredicted = FindFirstValidValue(predicted);
|
double lastValidPredicted = FindFirstValidValue(predicted);
|
||||||
|
|
||||||
// Try SIMD path for clean data (no NaN/Inf)
|
// Try SIMD path - NaN detection is integrated into the SIMD loop (avoids double-pass)
|
||||||
if (Avx2.IsSupported && len >= Vector256<double>.Count && IsDataClean(actual, predicted))
|
if (Avx2.IsSupported && len >= Vector256<double>.Count)
|
||||||
{
|
{
|
||||||
ComputeSquaredErrorsSimd(actual, predicted, output);
|
int processedCount = ComputeSquaredErrorsSimdWithNaNDetection(actual, predicted, output, ref lastValidActual, ref lastValidPredicted);
|
||||||
|
if (processedCount == len)
|
||||||
|
return; // All processed via SIMD
|
||||||
|
// Continue with scalar for remaining elements (NaN was detected)
|
||||||
|
ComputeSquaredErrorsScalar(actual.Slice(processedCount), predicted.Slice(processedCount), output.Slice(processedCount), lastValidActual, lastValidPredicted);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,19 +191,15 @@ public static class ErrorHelpers
|
|||||||
double act = actual[i];
|
double act = actual[i];
|
||||||
double pred = predicted[i];
|
double pred = predicted[i];
|
||||||
|
|
||||||
if (double.IsFinite(act)) currentValidActual = act; else act = currentValidActual;
|
#pragma warning disable S1121 // Assignments should not be made from within sub-expressions
|
||||||
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
|
act = double.IsFinite(act) ? (currentValidActual = act) : currentValidActual;
|
||||||
|
pred = double.IsFinite(pred) ? (currentValidPredicted = pred) : currentValidPredicted;
|
||||||
|
#pragma warning restore S1121
|
||||||
|
|
||||||
double absActual = Math.Abs(act);
|
double absActual = Math.Abs(act);
|
||||||
if (absActual < epsilon)
|
output[i] = absActual < epsilon
|
||||||
{
|
? Math.Abs(act - pred)
|
||||||
// Avoid division by zero - use absolute error as fallback
|
: Math.Abs(act - pred) / absActual * 100.0;
|
||||||
output[i] = Math.Abs(act - pred);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
output[i] = Math.Abs(act - pred) / absActual * 100.0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,14 +236,9 @@ public static class ErrorHelpers
|
|||||||
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
|
if (double.IsFinite(pred)) currentValidPredicted = pred; else pred = currentValidPredicted;
|
||||||
|
|
||||||
double denominator = (Math.Abs(act) + Math.Abs(pred)) / 2.0;
|
double denominator = (Math.Abs(act) + Math.Abs(pred)) / 2.0;
|
||||||
if (denominator < epsilon)
|
output[i] = denominator < epsilon
|
||||||
{
|
? 0.0 // Both values near zero
|
||||||
output[i] = 0.0; // Both values near zero
|
: Math.Abs(act - pred) / denominator * 100.0;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
output[i] = Math.Abs(act - pred) / denominator * 100.0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,14 +408,9 @@ public static class ErrorHelpers
|
|||||||
double diff = act - pred;
|
double diff = act - pred;
|
||||||
double absDiff = Math.Abs(diff);
|
double absDiff = Math.Abs(diff);
|
||||||
|
|
||||||
if (absDiff <= delta)
|
output[i] = absDiff <= delta
|
||||||
{
|
? 0.5 * diff * diff
|
||||||
output[i] = 0.5 * diff * diff;
|
: delta * (absDiff - halfDelta);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
output[i] = delta * (absDiff - halfDelta);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -735,14 +725,15 @@ public static class ErrorHelpers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// SIMD path with integrated NaN detection. Returns the number of elements processed.
|
/// SIMD path with integrated NaN detection. Returns the number of elements processed.
|
||||||
/// If NaN is detected, returns the index where NaN was found so caller can continue with scalar.
|
/// If NaN is detected, returns the index where NaN was found so caller can continue with scalar.
|
||||||
|
/// Updates lastValidActual/lastValidPredicted to track last seen finite values for scalar continuation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static int ComputeSignedErrorsSimdWithNaNDetection(
|
private static int ComputeSignedErrorsSimdWithNaNDetection(
|
||||||
ReadOnlySpan<double> actual,
|
ReadOnlySpan<double> actual,
|
||||||
ReadOnlySpan<double> predicted,
|
ReadOnlySpan<double> predicted,
|
||||||
Span<double> output,
|
Span<double> output,
|
||||||
double lastValidActual,
|
ref double lastValidActual,
|
||||||
double lastValidPredicted)
|
ref double lastValidPredicted)
|
||||||
{
|
{
|
||||||
int len = actual.Length;
|
int len = actual.Length;
|
||||||
int vectorSize = Vector256<double>.Count;
|
int vectorSize = Vector256<double>.Count;
|
||||||
@@ -762,7 +753,12 @@ public static class ErrorHelpers
|
|||||||
int mask = Avx.MoveMask(combined);
|
int mask = Avx.MoveMask(combined);
|
||||||
if (mask != 0b1111)
|
if (mask != 0b1111)
|
||||||
{
|
{
|
||||||
// NaN detected - return current position for scalar fallback
|
// NaN detected - update lastValid from previously processed elements before returning
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
lastValidActual = actual[i - 1];
|
||||||
|
lastValidPredicted = predicted[i - 1];
|
||||||
|
}
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -771,6 +767,13 @@ public static class ErrorHelpers
|
|||||||
errorVec.StoreUnsafe(ref MemoryMarshal.GetReference(output.Slice(i)));
|
errorVec.StoreUnsafe(ref MemoryMarshal.GetReference(output.Slice(i)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update lastValid from end of SIMD-processed section
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
lastValidActual = actual[i - 1];
|
||||||
|
lastValidPredicted = predicted[i - 1];
|
||||||
|
}
|
||||||
|
|
||||||
// Handle scalar remainder
|
// Handle scalar remainder
|
||||||
for (; i < len; i++)
|
for (; i < len; i++)
|
||||||
{
|
{
|
||||||
@@ -783,6 +786,8 @@ public static class ErrorHelpers
|
|||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastValidActual = act;
|
||||||
|
lastValidPredicted = pred;
|
||||||
output[i] = act - pred;
|
output[i] = act - pred;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -845,14 +850,15 @@ public static class ErrorHelpers
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// SIMD path with integrated NaN detection for absolute errors. Returns the number of elements processed.
|
/// SIMD path with integrated NaN detection for absolute errors. Returns the number of elements processed.
|
||||||
/// If NaN is detected, returns the index where NaN was found so caller can continue with scalar.
|
/// If NaN is detected, returns the index where NaN was found so caller can continue with scalar.
|
||||||
|
/// Updates lastValidActual/lastValidPredicted to track last seen finite values for scalar continuation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static int ComputeAbsoluteErrorsSimdWithNaNDetection(
|
private static int ComputeAbsoluteErrorsSimdWithNaNDetection(
|
||||||
ReadOnlySpan<double> actual,
|
ReadOnlySpan<double> actual,
|
||||||
ReadOnlySpan<double> predicted,
|
ReadOnlySpan<double> predicted,
|
||||||
Span<double> output,
|
Span<double> output,
|
||||||
double lastValidActual,
|
ref double lastValidActual,
|
||||||
double lastValidPredicted)
|
ref double lastValidPredicted)
|
||||||
{
|
{
|
||||||
int len = actual.Length;
|
int len = actual.Length;
|
||||||
int vectorSize = Vector256<double>.Count;
|
int vectorSize = Vector256<double>.Count;
|
||||||
@@ -875,7 +881,12 @@ public static class ErrorHelpers
|
|||||||
int mask = Avx.MoveMask(combined);
|
int mask = Avx.MoveMask(combined);
|
||||||
if (mask != 0b1111)
|
if (mask != 0b1111)
|
||||||
{
|
{
|
||||||
// NaN detected - return current position for scalar fallback
|
// NaN detected - update lastValid from previously processed elements before returning
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
lastValidActual = actual[i - 1];
|
||||||
|
lastValidPredicted = predicted[i - 1];
|
||||||
|
}
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -885,6 +896,13 @@ public static class ErrorHelpers
|
|||||||
absErrorVec.StoreUnsafe(ref MemoryMarshal.GetReference(output.Slice(i)));
|
absErrorVec.StoreUnsafe(ref MemoryMarshal.GetReference(output.Slice(i)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update lastValid from end of SIMD-processed section
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
lastValidActual = actual[i - 1];
|
||||||
|
lastValidPredicted = predicted[i - 1];
|
||||||
|
}
|
||||||
|
|
||||||
// Handle scalar remainder
|
// Handle scalar remainder
|
||||||
for (; i < len; i++)
|
for (; i < len; i++)
|
||||||
{
|
{
|
||||||
@@ -897,12 +915,88 @@ public static class ErrorHelpers
|
|||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastValidActual = act;
|
||||||
|
lastValidPredicted = pred;
|
||||||
output[i] = Math.Abs(act - pred);
|
output[i] = Math.Abs(act - pred);
|
||||||
}
|
}
|
||||||
|
|
||||||
return len;
|
return len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SIMD path with integrated NaN detection for squared errors. Returns the number of elements processed.
|
||||||
|
/// If NaN is detected, returns the index where NaN was found so caller can continue with scalar.
|
||||||
|
/// Updates lastValidActual/lastValidPredicted to track last seen finite values for scalar continuation.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static int ComputeSquaredErrorsSimdWithNaNDetection(
|
||||||
|
ReadOnlySpan<double> actual,
|
||||||
|
ReadOnlySpan<double> predicted,
|
||||||
|
Span<double> output,
|
||||||
|
ref double lastValidActual,
|
||||||
|
ref double lastValidPredicted)
|
||||||
|
{
|
||||||
|
int len = actual.Length;
|
||||||
|
int vectorSize = Vector256<double>.Count;
|
||||||
|
int vectorEnd = len - (len % vectorSize);
|
||||||
|
|
||||||
|
int i = 0;
|
||||||
|
for (; i < vectorEnd; i += vectorSize)
|
||||||
|
{
|
||||||
|
Vector256<double> actVec = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(actual.Slice(i)));
|
||||||
|
Vector256<double> predVec = Vector256.LoadUnsafe(ref MemoryMarshal.GetReference(predicted.Slice(i)));
|
||||||
|
|
||||||
|
// Check for NaN/Inf: x == x is false for NaN
|
||||||
|
Vector256<double> actCmp = Avx.Compare(actVec, actVec, FloatComparisonMode.OrderedNonSignaling);
|
||||||
|
Vector256<double> predCmp = Avx.Compare(predVec, predVec, FloatComparisonMode.OrderedNonSignaling);
|
||||||
|
Vector256<double> combined = Avx.And(actCmp, predCmp);
|
||||||
|
|
||||||
|
int mask = Avx.MoveMask(combined);
|
||||||
|
if (mask != 0b1111)
|
||||||
|
{
|
||||||
|
// NaN detected - update lastValid from previously processed elements before returning
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
lastValidActual = actual[i - 1];
|
||||||
|
lastValidPredicted = predicted[i - 1];
|
||||||
|
}
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No NaN - compute squared error: (actual - predicted)²
|
||||||
|
Vector256<double> errorVec = Avx.Subtract(actVec, predVec);
|
||||||
|
Vector256<double> sqErrorVec = Avx.Multiply(errorVec, errorVec);
|
||||||
|
sqErrorVec.StoreUnsafe(ref MemoryMarshal.GetReference(output.Slice(i)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update lastValid from end of SIMD-processed section
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
lastValidActual = actual[i - 1];
|
||||||
|
lastValidPredicted = predicted[i - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle scalar remainder
|
||||||
|
for (; i < len; i++)
|
||||||
|
{
|
||||||
|
double act = actual[i];
|
||||||
|
double pred = predicted[i];
|
||||||
|
|
||||||
|
if (!double.IsFinite(act) || !double.IsFinite(pred))
|
||||||
|
{
|
||||||
|
// Return current position - caller will handle with scalar fallback
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastValidActual = act;
|
||||||
|
lastValidPredicted = pred;
|
||||||
|
double diff = act - pred;
|
||||||
|
output[i] = diff * diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static void ComputeAbsoluteErrorsSimd(
|
private static void ComputeAbsoluteErrorsSimd(
|
||||||
ReadOnlySpan<double> actual,
|
ReadOnlySpan<double> actual,
|
||||||
|
|||||||
@@ -289,18 +289,18 @@ public class SimdExtensionsTests
|
|||||||
|
|
||||||
// VarianceSIMD tests
|
// VarianceSIMD tests
|
||||||
[Fact]
|
[Fact]
|
||||||
public void VarianceSIMD_LessThanTwoElements_ReturnsNaN()
|
public void VarianceSIMD_LessThanTwoElements_ReturnsZero()
|
||||||
{
|
{
|
||||||
double[] data = [42.5];
|
double[] data = [42.5];
|
||||||
var span = new ReadOnlySpan<double>(data);
|
var span = new ReadOnlySpan<double>(data);
|
||||||
Assert.True(double.IsNaN(span.VarianceSIMD()));
|
Assert.Equal(0.0, span.VarianceSIMD());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void VarianceSIMD_EmptySpan_ReturnsNaN()
|
public void VarianceSIMD_EmptySpan_ReturnsZero()
|
||||||
{
|
{
|
||||||
var span = ReadOnlySpan<double>.Empty;
|
var span = ReadOnlySpan<double>.Empty;
|
||||||
Assert.True(double.IsNaN(span.VarianceSIMD()));
|
Assert.Equal(0.0, span.VarianceSIMD());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -631,8 +631,8 @@ public class SimdExtensionsTests
|
|||||||
Assert.True(variance > 0);
|
Assert.True(variance > 0);
|
||||||
Assert.True(stdDev > 0);
|
Assert.True(stdDev > 0);
|
||||||
|
|
||||||
Assert.True(sw.ElapsedMilliseconds < 50,
|
Assert.True(sw.ElapsedMilliseconds < 100,
|
||||||
$"SIMD operations took {sw.ElapsedMilliseconds}ms, expected < 50ms");
|
$"SIMD operations took {sw.ElapsedMilliseconds}ms, expected < 100ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -870,26 +870,26 @@ public class SimdScalarFallbackTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void VarianceSIMD_SingleElement_ReturnsNaN()
|
public void VarianceSIMD_SingleElement_ReturnsZero()
|
||||||
{
|
{
|
||||||
double[] data = [42.5];
|
double[] data = [42.5];
|
||||||
var span = new ReadOnlySpan<double>(data);
|
var span = new ReadOnlySpan<double>(data);
|
||||||
Assert.True(double.IsNaN(span.VarianceSIMD()));
|
Assert.Equal(0.0, span.VarianceSIMD());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void StdDevSIMD_SingleElement_ReturnsNaN()
|
public void StdDevSIMD_SingleElement_ReturnsZero()
|
||||||
{
|
{
|
||||||
double[] data = [42.5];
|
double[] data = [42.5];
|
||||||
var span = new ReadOnlySpan<double>(data);
|
var span = new ReadOnlySpan<double>(data);
|
||||||
Assert.True(double.IsNaN(span.StdDevSIMD()));
|
Assert.Equal(0.0, span.StdDevSIMD());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void StdDevSIMD_EmptySpan_ReturnsNaN()
|
public void StdDevSIMD_EmptySpan_ReturnsZero()
|
||||||
{
|
{
|
||||||
var span = ReadOnlySpan<double>.Empty;
|
var span = ReadOnlySpan<double>.Empty;
|
||||||
Assert.True(double.IsNaN(span.StdDevSIMD()));
|
Assert.Equal(0.0, span.StdDevSIMD());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -299,7 +299,8 @@ public static class SimdExtensions
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static double VarianceSIMD(this ReadOnlySpan<double> span, double? mean = null)
|
public static double VarianceSIMD(this ReadOnlySpan<double> span, double? mean = null)
|
||||||
{
|
{
|
||||||
if (span.Length < 2) return double.NaN;
|
// Match VarianceScalar behavior: return 0.0 for length <= 1 to avoid inconsistency
|
||||||
|
if (span.Length <= 1) return 0.0;
|
||||||
|
|
||||||
double m;
|
double m;
|
||||||
if (mean.HasValue)
|
if (mean.HasValue)
|
||||||
@@ -627,32 +628,44 @@ public static class SimdExtensions
|
|||||||
ref double aRef = ref MemoryMarshal.GetReference(a);
|
ref double aRef = ref MemoryMarshal.GetReference(a);
|
||||||
ref double bRef = ref MemoryMarshal.GetReference(b);
|
ref double bRef = ref MemoryMarshal.GetReference(b);
|
||||||
|
|
||||||
|
// Hoist FMA check outside loop for branch prediction optimization
|
||||||
|
bool useFma = Fma.IsSupported;
|
||||||
|
|
||||||
// Unroll loop: Process 16 doubles (4 vectors) at a time
|
// Unroll loop: Process 16 doubles (4 vectors) at a time
|
||||||
if (len >= 16)
|
if (len >= 16)
|
||||||
{
|
{
|
||||||
for (; i <= len - 16; i += 16)
|
if (useFma)
|
||||||
{
|
{
|
||||||
var va1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i));
|
for (; i <= len - 16; i += 16)
|
||||||
var vb1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i));
|
|
||||||
|
|
||||||
var va2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 4));
|
|
||||||
var vb2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 4));
|
|
||||||
|
|
||||||
var va3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 8));
|
|
||||||
var vb3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 8));
|
|
||||||
|
|
||||||
var va4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 12));
|
|
||||||
var vb4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 12));
|
|
||||||
|
|
||||||
if (Fma.IsSupported)
|
|
||||||
{
|
{
|
||||||
|
var va1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i));
|
||||||
|
var vb1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i));
|
||||||
|
var va2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 4));
|
||||||
|
var vb2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 4));
|
||||||
|
var va3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 8));
|
||||||
|
var vb3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 8));
|
||||||
|
var va4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 12));
|
||||||
|
var vb4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 12));
|
||||||
|
|
||||||
vSum = Fma.MultiplyAdd(va1, vb1, vSum);
|
vSum = Fma.MultiplyAdd(va1, vb1, vSum);
|
||||||
vSum2 = Fma.MultiplyAdd(va2, vb2, vSum2);
|
vSum2 = Fma.MultiplyAdd(va2, vb2, vSum2);
|
||||||
vSum3 = Fma.MultiplyAdd(va3, vb3, vSum3);
|
vSum3 = Fma.MultiplyAdd(va3, vb3, vSum3);
|
||||||
vSum4 = Fma.MultiplyAdd(va4, vb4, vSum4);
|
vSum4 = Fma.MultiplyAdd(va4, vb4, vSum4);
|
||||||
}
|
}
|
||||||
else
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (; i <= len - 16; i += 16)
|
||||||
{
|
{
|
||||||
|
var va1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i));
|
||||||
|
var vb1 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i));
|
||||||
|
var va2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 4));
|
||||||
|
var vb2 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 4));
|
||||||
|
var va3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 8));
|
||||||
|
var vb3 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 8));
|
||||||
|
var va4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i + 12));
|
||||||
|
var vb4 = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i + 12));
|
||||||
|
|
||||||
vSum = Avx.Add(vSum, Avx.Multiply(va1, vb1));
|
vSum = Avx.Add(vSum, Avx.Multiply(va1, vb1));
|
||||||
vSum2 = Avx.Add(vSum2, Avx.Multiply(va2, vb2));
|
vSum2 = Avx.Add(vSum2, Avx.Multiply(va2, vb2));
|
||||||
vSum3 = Avx.Add(vSum3, Avx.Multiply(va3, vb3));
|
vSum3 = Avx.Add(vSum3, Avx.Multiply(va3, vb3));
|
||||||
@@ -661,15 +674,24 @@ public static class SimdExtensions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process remaining vectors (4 doubles at a time)
|
// Process remaining vectors (4 doubles at a time) with hoisted branch
|
||||||
for (; i <= len - 4; i += 4)
|
if (useFma)
|
||||||
{
|
{
|
||||||
var va = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i));
|
for (; i <= len - 4; i += 4)
|
||||||
var vb = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i));
|
{
|
||||||
|
var va = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i));
|
||||||
vSum = Fma.IsSupported
|
var vb = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i));
|
||||||
? Fma.MultiplyAdd(va, vb, vSum)
|
vSum = Fma.MultiplyAdd(va, vb, vSum);
|
||||||
: Avx.Add(vSum, Avx.Multiply(va, vb));
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (; i <= len - 4; i += 4)
|
||||||
|
{
|
||||||
|
var va = Vector256.LoadUnsafe(ref Unsafe.Add(ref aRef, i));
|
||||||
|
var vb = Vector256.LoadUnsafe(ref Unsafe.Add(ref bRef, i));
|
||||||
|
vSum = Avx.Add(vSum, Avx.Multiply(va, vb));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine accumulators
|
// Combine accumulators
|
||||||
|
|||||||
@@ -395,7 +395,7 @@ public class TBarTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TBar_WithInfinity_HandlesGracefully()
|
public void TBar_WithPositiveInfinity_HandlesGracefully()
|
||||||
{
|
{
|
||||||
var bar = new TBar(12345, 100, double.PositiveInfinity, 90, 105, 1000);
|
var bar = new TBar(12345, 100, double.PositiveInfinity, 90, 105, 1000);
|
||||||
|
|
||||||
@@ -404,6 +404,17 @@ public class TBarTests
|
|||||||
Assert.True(double.IsPositiveInfinity(bar.HL2)); // Uses High
|
Assert.True(double.IsPositiveInfinity(bar.HL2)); // Uses High
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TBar_WithNegativeInfinity_HandlesGracefully()
|
||||||
|
{
|
||||||
|
var bar = new TBar(12345, 100, 110, double.NegativeInfinity, 105, 1000);
|
||||||
|
|
||||||
|
Assert.True(double.IsNegativeInfinity(bar.Low));
|
||||||
|
Assert.True(double.IsNegativeInfinity(bar.L.Value));
|
||||||
|
Assert.True(double.IsNegativeInfinity(bar.HL2)); // Uses Low
|
||||||
|
Assert.True(double.IsNegativeInfinity(bar.HLC3)); // Uses Low
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void TBar_WithMaxValue_HandlesGracefully()
|
public void TBar_WithMaxValue_HandlesGracefully()
|
||||||
{
|
{
|
||||||
@@ -412,8 +423,8 @@ public class TBarTests
|
|||||||
Assert.Equal(double.MaxValue, bar.Open);
|
Assert.Equal(double.MaxValue, bar.Open);
|
||||||
Assert.Equal(double.MaxValue, bar.High);
|
Assert.Equal(double.MaxValue, bar.High);
|
||||||
Assert.Equal(double.MinValue, bar.Low);
|
Assert.Equal(double.MinValue, bar.Low);
|
||||||
// HL2 calculation with extreme values
|
// HL2 = (MaxValue + MinValue) * 0.5 = 0 (symmetric around zero)
|
||||||
Assert.True(double.IsFinite(bar.HL2) || double.IsInfinity(bar.HL2));
|
Assert.Equal(0.0, bar.HL2);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
+10
-2
@@ -23,11 +23,19 @@ public readonly record struct TBar(long Time, double Open, double High, double L
|
|||||||
// Computed properties (calculated on demand, no storage overhead)
|
// Computed properties (calculated on demand, no storage overhead)
|
||||||
public double HL2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low) * 0.5; }
|
public double HL2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low) * 0.5; }
|
||||||
public double OC2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + Close) * 0.5; }
|
public double OC2 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + Close) * 0.5; }
|
||||||
public double OHL3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low) / 3.0; }
|
public double OHL3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low) * (1.0 / 3.0); }
|
||||||
public double HLC3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close) / 3.0; }
|
public double HLC3 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close) * (1.0 / 3.0); }
|
||||||
public double OHLC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low + Close) * 0.25; }
|
public double OHLC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (Open + High + Low + Close) * 0.25; }
|
||||||
public double HLCC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close + Close) * 0.25; }
|
public double HLCC4 { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => (High + Low + Close + Close) * 0.25; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a TBar from DateTime and OHLCV values.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <b>Performance warning:</b> If <paramref name="time"/>.Kind is not <see cref="DateTimeKind.Utc"/>,
|
||||||
|
/// <see cref="DateTime.ToUniversalTime"/> is called, which allocates. For hot paths, prefer the
|
||||||
|
/// primary constructor with pre-computed UTC ticks.
|
||||||
|
/// </remarks>
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public TBar(DateTime time, double open, double high, double low, double close, double volume)
|
public TBar(DateTime time, double open, double high, double low, double close, double volume)
|
||||||
: this(time.Kind == DateTimeKind.Utc ? time.Ticks : time.ToUniversalTime().Ticks, open, high, low, close, volume)
|
: this(time.Kind == DateTimeKind.Utc ? time.Ticks : time.ToUniversalTime().Ticks, open, high, low, close, volume)
|
||||||
|
|||||||
@@ -281,6 +281,11 @@ public class TBarSeries : IReadOnlyList<TBar>
|
|||||||
public void Add(DateTime time, double open, double high, double low, double close, double volume, bool isNew = true) =>
|
public void Add(DateTime time, double open, double high, double low, double close, double volume, bool isNew = true) =>
|
||||||
Add(new TBar(time.Ticks, open, high, low, close, volume), isNew);
|
Add(new TBar(time.Ticks, open, high, low, close, volume), isNew);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulk add from IEnumerable sources. Fires Pub event for each bar.
|
||||||
|
/// WARNING: This method may allocate if inputs are not already arrays.
|
||||||
|
/// For zero-allocation bulk loading, prefer AddRange with ReadOnlySpan parameters.
|
||||||
|
/// </summary>
|
||||||
public void Add(IEnumerable<long> t, IEnumerable<double> o, IEnumerable<double> h, IEnumerable<double> l, IEnumerable<double> c, IEnumerable<double> v)
|
public void Add(IEnumerable<long> t, IEnumerable<double> o, IEnumerable<double> h, IEnumerable<double> l, IEnumerable<double> c, IEnumerable<double> v)
|
||||||
{
|
{
|
||||||
var tArr = t as long[] ?? t.ToArray();
|
var tArr = t as long[] ?? t.ToArray();
|
||||||
@@ -320,28 +325,37 @@ public class TBarSeries : IReadOnlyList<TBar>
|
|||||||
if (o.Length != len || h.Length != len || l.Length != len || c.Length != len || v.Length != len)
|
if (o.Length != len || h.Length != len || l.Length != len || c.Length != len || v.Length != len)
|
||||||
throw new ArgumentException("All spans must have the same length", nameof(t));
|
throw new ArgumentException("All spans must have the same length", nameof(t));
|
||||||
|
|
||||||
|
if (len == 0) return;
|
||||||
|
|
||||||
|
int oldCount = _c.Count;
|
||||||
|
int newCount = oldCount + len;
|
||||||
|
|
||||||
// Pre-allocate capacity to avoid repeated resizing
|
// Pre-allocate capacity to avoid repeated resizing
|
||||||
int newCapacity = _c.Count + len;
|
if (_t.Capacity < newCount)
|
||||||
if (_t.Capacity < newCapacity)
|
|
||||||
{
|
{
|
||||||
_t.Capacity = newCapacity;
|
_t.Capacity = newCount;
|
||||||
_o.Capacity = newCapacity;
|
_o.Capacity = newCount;
|
||||||
_h.Capacity = newCapacity;
|
_h.Capacity = newCount;
|
||||||
_l.Capacity = newCapacity;
|
_l.Capacity = newCount;
|
||||||
_c.Capacity = newCapacity;
|
_c.Capacity = newCount;
|
||||||
_v.Capacity = newCapacity;
|
_v.Capacity = newCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bulk add without event firing (for initial data loading)
|
// Use SetCount to resize lists without zeroing, then copy via span
|
||||||
for (int i = 0; i < len; i++)
|
CollectionsMarshal.SetCount(_t, newCount);
|
||||||
{
|
CollectionsMarshal.SetCount(_o, newCount);
|
||||||
_t.Add(t[i]);
|
CollectionsMarshal.SetCount(_h, newCount);
|
||||||
_o.Add(o[i]);
|
CollectionsMarshal.SetCount(_l, newCount);
|
||||||
_h.Add(h[i]);
|
CollectionsMarshal.SetCount(_c, newCount);
|
||||||
_l.Add(l[i]);
|
CollectionsMarshal.SetCount(_v, newCount);
|
||||||
_c.Add(c[i]);
|
|
||||||
_v.Add(v[i]);
|
// Direct span copy - zero allocation bulk add
|
||||||
}
|
t.CopyTo(CollectionsMarshal.AsSpan(_t).Slice(oldCount));
|
||||||
|
o.CopyTo(CollectionsMarshal.AsSpan(_o).Slice(oldCount));
|
||||||
|
h.CopyTo(CollectionsMarshal.AsSpan(_h).Slice(oldCount));
|
||||||
|
l.CopyTo(CollectionsMarshal.AsSpan(_l).Slice(oldCount));
|
||||||
|
c.CopyTo(CollectionsMarshal.AsSpan(_c).Slice(oldCount));
|
||||||
|
v.CopyTo(CollectionsMarshal.AsSpan(_v).Slice(oldCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -354,28 +368,46 @@ public class TBarSeries : IReadOnlyList<TBar>
|
|||||||
int len = bars.Length;
|
int len = bars.Length;
|
||||||
if (len == 0) return;
|
if (len == 0) return;
|
||||||
|
|
||||||
|
int oldCount = _c.Count;
|
||||||
|
int newCount = oldCount + len;
|
||||||
|
|
||||||
// Pre-allocate capacity to avoid repeated resizing
|
// Pre-allocate capacity to avoid repeated resizing
|
||||||
int newCapacity = _c.Count + len;
|
if (_t.Capacity < newCount)
|
||||||
if (_t.Capacity < newCapacity)
|
|
||||||
{
|
{
|
||||||
_t.Capacity = newCapacity;
|
_t.Capacity = newCount;
|
||||||
_o.Capacity = newCapacity;
|
_o.Capacity = newCount;
|
||||||
_h.Capacity = newCapacity;
|
_h.Capacity = newCount;
|
||||||
_l.Capacity = newCapacity;
|
_l.Capacity = newCount;
|
||||||
_c.Capacity = newCapacity;
|
_c.Capacity = newCount;
|
||||||
_v.Capacity = newCapacity;
|
_v.Capacity = newCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bulk add without event firing (for initial data loading)
|
// Use SetCount to resize lists without zeroing
|
||||||
|
CollectionsMarshal.SetCount(_t, newCount);
|
||||||
|
CollectionsMarshal.SetCount(_o, newCount);
|
||||||
|
CollectionsMarshal.SetCount(_h, newCount);
|
||||||
|
CollectionsMarshal.SetCount(_l, newCount);
|
||||||
|
CollectionsMarshal.SetCount(_c, newCount);
|
||||||
|
CollectionsMarshal.SetCount(_v, newCount);
|
||||||
|
|
||||||
|
// Get mutable spans for direct write
|
||||||
|
Span<long> tSpan = CollectionsMarshal.AsSpan(_t).Slice(oldCount);
|
||||||
|
Span<double> oSpan = CollectionsMarshal.AsSpan(_o).Slice(oldCount);
|
||||||
|
Span<double> hSpan = CollectionsMarshal.AsSpan(_h).Slice(oldCount);
|
||||||
|
Span<double> lSpan = CollectionsMarshal.AsSpan(_l).Slice(oldCount);
|
||||||
|
Span<double> cSpan = CollectionsMarshal.AsSpan(_c).Slice(oldCount);
|
||||||
|
Span<double> vSpan = CollectionsMarshal.AsSpan(_v).Slice(oldCount);
|
||||||
|
|
||||||
|
// Copy from TBar structs to SoA layout
|
||||||
for (int i = 0; i < len; i++)
|
for (int i = 0; i < len; i++)
|
||||||
{
|
{
|
||||||
ref readonly TBar bar = ref bars[i];
|
ref readonly TBar bar = ref bars[i];
|
||||||
_t.Add(bar.Time);
|
tSpan[i] = bar.Time;
|
||||||
_o.Add(bar.Open);
|
oSpan[i] = bar.Open;
|
||||||
_h.Add(bar.High);
|
hSpan[i] = bar.High;
|
||||||
_l.Add(bar.Low);
|
lSpan[i] = bar.Low;
|
||||||
_c.Add(bar.Close);
|
cSpan[i] = bar.Close;
|
||||||
_v.Add(bar.Volume);
|
vSpan[i] = bar.Volume;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
|
||||||
namespace QuanTAlib.Tests;
|
namespace QuanTAlib.Tests;
|
||||||
|
|
||||||
public class TValueTests
|
public class TValueTests
|
||||||
@@ -42,7 +44,7 @@ public class TValueTests
|
|||||||
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||||
var tValue = new TValue(dt.Ticks, 123.456);
|
var tValue = new TValue(dt.Ticks, 123.456);
|
||||||
|
|
||||||
string result = tValue.ToString();
|
string result = tValue.ToString(null, CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
Assert.Contains("2023-01-01", result, StringComparison.Ordinal);
|
Assert.Contains("2023-01-01", result, StringComparison.Ordinal);
|
||||||
Assert.Contains("12:00:00", result, StringComparison.Ordinal);
|
Assert.Contains("12:00:00", result, StringComparison.Ordinal);
|
||||||
@@ -288,7 +290,7 @@ public class TValueTests
|
|||||||
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||||
var tValue = new TValue(dt.Ticks, double.NaN);
|
var tValue = new TValue(dt.Ticks, double.NaN);
|
||||||
|
|
||||||
string result = tValue.ToString();
|
string result = tValue.ToString(null, CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
Assert.Contains("NaN", result, StringComparison.Ordinal);
|
Assert.Contains("NaN", result, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
@@ -299,7 +301,7 @@ public class TValueTests
|
|||||||
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||||
var tValue = new TValue(dt.Ticks, double.PositiveInfinity);
|
var tValue = new TValue(dt.Ticks, double.PositiveInfinity);
|
||||||
|
|
||||||
string result = tValue.ToString();
|
string result = tValue.ToString(null, CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
Assert.Contains("∞", result, StringComparison.Ordinal);
|
Assert.Contains("∞", result, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
@@ -310,7 +312,7 @@ public class TValueTests
|
|||||||
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
var dt = new DateTime(2023, 1, 1, 12, 0, 0, DateTimeKind.Utc);
|
||||||
var tValue = new TValue(dt.Ticks, -123.456);
|
var tValue = new TValue(dt.Ticks, -123.456);
|
||||||
|
|
||||||
string result = tValue.ToString();
|
string result = tValue.ToString(null, CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
Assert.Contains("-123.46", result, StringComparison.Ordinal);
|
Assert.Contains("-123.46", result, StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
@@ -340,9 +342,11 @@ public class TValueTests
|
|||||||
{
|
{
|
||||||
var tv = new TValue(12345, double.NaN);
|
var tv = new TValue(12345, double.NaN);
|
||||||
|
|
||||||
var hash = tv.GetHashCode();
|
// Should not throw - the record struct implementation handles NaN correctly
|
||||||
|
int hash = tv.GetHashCode();
|
||||||
|
|
||||||
Assert.True(hash != 0 || hash == 0); // Just verify it doesn't throw
|
// Hash should be consistent for same NaN value
|
||||||
|
Assert.Equal(hash, tv.GetHashCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ namespace QuanTAlib;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// A lightweight struct representing a time-value pair.
|
/// A lightweight struct representing a time-value pair.
|
||||||
/// Pure data type: 16 bytes (long + double).
|
/// Pure data type: 16 bytes (long + double).
|
||||||
|
/// Implements ISpanFormattable for allocation-free formatting.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[SkipLocalsInit]
|
[SkipLocalsInit]
|
||||||
[StructLayout(LayoutKind.Auto)]
|
[StructLayout(LayoutKind.Auto)]
|
||||||
public readonly record struct TValue(long Time, double Value)
|
public readonly record struct TValue(long Time, double Value) : ISpanFormattable
|
||||||
{
|
{
|
||||||
public DateTime AsDateTime => new(Time, DateTimeKind.Utc);
|
public DateTime AsDateTime => new(Time, DateTimeKind.Utc);
|
||||||
|
|
||||||
@@ -37,4 +38,90 @@ public readonly record struct TValue(long Time, double Value)
|
|||||||
};
|
};
|
||||||
return $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}, {valueStr}]";
|
return $"[{AsDateTime:yyyy-MM-dd HH:mm:ss}, {valueStr}]";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Formats the TValue using the specified format string.
|
||||||
|
/// Note: Custom format and formatProvider are not supported by TValue.
|
||||||
|
/// If a non-null/non-empty format is provided, a NotSupportedException is thrown.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="format">Must be null or empty; custom formats are not supported.</param>
|
||||||
|
/// <param name="formatProvider">Ignored; TValue uses its own fixed format.</param>
|
||||||
|
/// <returns>The string representation of this TValue.</returns>
|
||||||
|
/// <exception cref="NotSupportedException">Thrown when a non-null/non-empty format is provided.</exception>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public string ToString(string? format, IFormatProvider? formatProvider)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(format))
|
||||||
|
throw new NotSupportedException($"Custom format '{format}' is not supported by TValue. Use ToString() for the default format.");
|
||||||
|
|
||||||
|
return ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Formats the TValue into the provided span without heap allocation.
|
||||||
|
/// Format: "[yyyy-MM-dd HH:mm:ss, value]"
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
|
||||||
|
{
|
||||||
|
charsWritten = 0;
|
||||||
|
|
||||||
|
// Early reject for buffers too small to hold even the timestamp portion.
|
||||||
|
// This is a heuristic check; actual buffer-overflow protection is performed
|
||||||
|
// by the explicit length checks that guard each write operation below.
|
||||||
|
if (destination.Length < 24)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Write opening bracket
|
||||||
|
destination[0] = '[';
|
||||||
|
int pos = 1;
|
||||||
|
|
||||||
|
// Format datetime: yyyy-MM-dd HH:mm:ss (19 chars)
|
||||||
|
if (!AsDateTime.TryFormat(destination.Slice(pos), out int dtChars, "yyyy-MM-dd HH:mm:ss", provider))
|
||||||
|
return false;
|
||||||
|
pos += dtChars;
|
||||||
|
|
||||||
|
// Write separator
|
||||||
|
if (pos + 2 > destination.Length)
|
||||||
|
return false;
|
||||||
|
destination[pos++] = ',';
|
||||||
|
destination[pos++] = ' ';
|
||||||
|
|
||||||
|
// Format value
|
||||||
|
if (double.IsPositiveInfinity(Value))
|
||||||
|
{
|
||||||
|
if (pos + 1 > destination.Length)
|
||||||
|
return false;
|
||||||
|
destination[pos++] = (char)0x221E; //
|
||||||
|
}
|
||||||
|
else if (double.IsNegativeInfinity(Value))
|
||||||
|
{
|
||||||
|
if (pos + 2 > destination.Length)
|
||||||
|
return false;
|
||||||
|
destination[pos++] = '-';
|
||||||
|
destination[pos++] = (char)0x221E; // -
|
||||||
|
}
|
||||||
|
else if (double.IsNaN(Value))
|
||||||
|
{
|
||||||
|
if (pos + 3 > destination.Length)
|
||||||
|
return false;
|
||||||
|
destination[pos++] = 'N';
|
||||||
|
destination[pos++] = 'a';
|
||||||
|
destination[pos++] = 'N';
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!Value.TryFormat(destination.Slice(pos), out int valueChars, "F2", provider))
|
||||||
|
return false;
|
||||||
|
pos += valueChars;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write closing bracket
|
||||||
|
if (pos + 1 > destination.Length)
|
||||||
|
return false;
|
||||||
|
destination[pos++] = ']';
|
||||||
|
|
||||||
|
charsWritten = pos;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -27,7 +27,7 @@ public class HuberTests
|
|||||||
Assert.Contains("Huber", huber.Name, StringComparison.Ordinal);
|
Assert.Contains("Huber", huber.Name, StringComparison.Ordinal);
|
||||||
|
|
||||||
huber.Update(100, 105);
|
huber.Update(100, 105);
|
||||||
Assert.NotEqual(0, huber.Last.Time);
|
Assert.NotEqual(0, huber.Last.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class MaeTests
|
|||||||
Assert.Contains("Mae", mae.Name, StringComparison.Ordinal);
|
Assert.Contains("Mae", mae.Name, StringComparison.Ordinal);
|
||||||
|
|
||||||
mae.Update(100, 105);
|
mae.Update(100, 105);
|
||||||
Assert.NotEqual(0, mae.Last.Time);
|
Assert.NotEqual(0, mae.Last.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class MapdTests
|
|||||||
Assert.Contains("Mapd", mapd.Name, StringComparison.Ordinal);
|
Assert.Contains("Mapd", mapd.Name, StringComparison.Ordinal);
|
||||||
|
|
||||||
mapd.Update(100, 105);
|
mapd.Update(100, 105);
|
||||||
Assert.NotEqual(0, mapd.Last.Time);
|
Assert.NotEqual(0, mapd.Last.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class MapeTests
|
|||||||
Assert.Contains("Mape", mape.Name, StringComparison.Ordinal);
|
Assert.Contains("Mape", mape.Name, StringComparison.Ordinal);
|
||||||
|
|
||||||
mape.Update(100, 105);
|
mape.Update(100, 105);
|
||||||
Assert.NotEqual(0, mape.Last.Time);
|
Assert.NotEqual(0, mape.Last.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class MeTests
|
|||||||
Assert.Contains("Me", me.Name, StringComparison.Ordinal);
|
Assert.Contains("Me", me.Name, StringComparison.Ordinal);
|
||||||
|
|
||||||
me.Update(100, 105);
|
me.Update(100, 105);
|
||||||
Assert.NotEqual(0, me.Last.Time);
|
Assert.NotEqual(0, me.Last.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class MraeTests
|
|||||||
Assert.Contains("Mrae", mrae.Name, StringComparison.Ordinal);
|
Assert.Contains("Mrae", mrae.Name, StringComparison.Ordinal);
|
||||||
|
|
||||||
mrae.Update(100, 105);
|
mrae.Update(100, 105);
|
||||||
Assert.NotEqual(0, mrae.Last.Time);
|
Assert.NotEqual(0, mrae.Last.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class MseTests
|
|||||||
Assert.Contains("Mse", mse.Name, StringComparison.Ordinal);
|
Assert.Contains("Mse", mse.Name, StringComparison.Ordinal);
|
||||||
|
|
||||||
mse.Update(100, 105);
|
mse.Update(100, 105);
|
||||||
Assert.NotEqual(0, mse.Last.Time);
|
Assert.NotEqual(0, mse.Last.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public class RmseTests
|
|||||||
Assert.Contains("Rmse", rmse.Name, StringComparison.Ordinal);
|
Assert.Contains("Rmse", rmse.Name, StringComparison.Ordinal);
|
||||||
|
|
||||||
rmse.Update(100, 105);
|
rmse.Update(100, 105);
|
||||||
Assert.NotEqual(0, rmse.Last.Time);
|
Assert.NotEqual(0, rmse.Last.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ public sealed class CsvFeedTests : IDisposable
|
|||||||
{
|
{
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
|
||||||
foreach (var file in _tempFiles)
|
foreach (var file in _tempFiles)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -666,14 +666,33 @@ public class GBMTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void ImplementsIFeed()
|
public void ImplementsIFeed()
|
||||||
{
|
{
|
||||||
GBM feed = new GBM(startPrice: 100.0, seed: 42);
|
// Verify GBM implements IFeed interface
|
||||||
|
Assert.True(typeof(IFeed).IsAssignableFrom(typeof(GBM)));
|
||||||
|
|
||||||
|
// Use IFeed reference to verify interface contract
|
||||||
|
IFeed feed = new GBM(startPrice: 100.0, seed: 42);
|
||||||
|
|
||||||
|
// Test Next(bool) overload via interface
|
||||||
var bar1 = feed.Next(isNew: true);
|
var bar1 = feed.Next(isNew: true);
|
||||||
Assert.True(bar1.Time > 0);
|
Assert.True(bar1.Time > 0);
|
||||||
|
|
||||||
var bar2 = feed.Next(isNew: true);
|
var bar2 = feed.Next(isNew: true);
|
||||||
Assert.True(bar2.Time > bar1.Time);
|
Assert.True(bar2.Time > bar1.Time);
|
||||||
|
|
||||||
|
// Test Next(ref bool) overload via interface - verify ref parameter behavior
|
||||||
|
bool isNew = true;
|
||||||
|
var bar3 = feed.Next(ref isNew);
|
||||||
|
Assert.True(bar3.Time > bar2.Time);
|
||||||
|
Assert.True(isNew, "GBM should honor isNew=true request and keep it true");
|
||||||
|
|
||||||
|
// Test with isNew=false via interface
|
||||||
|
bool isNewFalse = false;
|
||||||
|
long bar3Time = bar3.Time;
|
||||||
|
var bar3Updated = feed.Next(ref isNewFalse);
|
||||||
|
Assert.Equal(bar3Time, bar3Updated.Time); // Same bar when isNew=false
|
||||||
|
Assert.False(isNewFalse, "GBM should honor isNew=false request and keep it false");
|
||||||
|
|
||||||
|
// Test Fetch via interface
|
||||||
long startTime = DateTime.UtcNow.Ticks;
|
long startTime = DateTime.UtcNow.Ticks;
|
||||||
var series = feed.Fetch(5, startTime, TimeSpan.FromMinutes(1));
|
var series = feed.Fetch(5, startTime, TimeSpan.FromMinutes(1));
|
||||||
Assert.Equal(5, series.Count);
|
Assert.Equal(5, series.Count);
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
namespace QuanTAlib.Tests;
|
namespace QuanTAlib.Tests;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
+76
-15
@@ -1,3 +1,4 @@
|
|||||||
|
using System.Buffers;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
@@ -262,6 +263,7 @@ public sealed class GBM : IFeed
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generates a batch of bars using optimized batch processing with explicit time parameters.
|
/// Generates a batch of bars using optimized batch processing with explicit time parameters.
|
||||||
|
/// Uses stackalloc for small batches to avoid heap allocations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="count">Number of bars to generate (must be positive)</param>
|
/// <param name="count">Number of bars to generate (must be positive)</param>
|
||||||
/// <param name="startTime">Starting timestamp in ticks</param>
|
/// <param name="startTime">Starting timestamp in ticks</param>
|
||||||
@@ -279,14 +281,81 @@ public sealed class GBM : IFeed
|
|||||||
|
|
||||||
var series = new TBarSeries(count);
|
var series = new TBarSeries(count);
|
||||||
|
|
||||||
// Pre-allocate arrays for SoA layout
|
// Threshold for stackalloc: 64 bars * (8 bytes for long + 5*8 bytes for doubles) = 64 * 48 = 3KB
|
||||||
long[] t = new long[count];
|
// Stay well under typical stack limit; use 64 as safe threshold
|
||||||
double[] o = new double[count];
|
const int StackAllocThreshold = 64;
|
||||||
double[] h = new double[count];
|
|
||||||
double[] l = new double[count];
|
|
||||||
double[] c = new double[count];
|
|
||||||
double[] v = new double[count];
|
|
||||||
|
|
||||||
|
// Use stackalloc for small batches to avoid heap allocations
|
||||||
|
if (count <= StackAllocThreshold)
|
||||||
|
{
|
||||||
|
Span<long> t = stackalloc long[count];
|
||||||
|
Span<double> o = stackalloc double[count];
|
||||||
|
Span<double> h = stackalloc double[count];
|
||||||
|
Span<double> l = stackalloc double[count];
|
||||||
|
Span<double> c = stackalloc double[count];
|
||||||
|
Span<double> v = stackalloc double[count];
|
||||||
|
|
||||||
|
FetchCore(count, startTime, interval, t, o, h, l, c, v);
|
||||||
|
|
||||||
|
// Bulk add to series using ReadOnlySpan overload
|
||||||
|
series.AddRange(t, o, h, l, c, v);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Use ArrayPool for larger batches to avoid heap allocations
|
||||||
|
long[]? rentedT = null;
|
||||||
|
double[]? rentedO = null;
|
||||||
|
double[]? rentedH = null;
|
||||||
|
double[]? rentedL = null;
|
||||||
|
double[]? rentedC = null;
|
||||||
|
double[]? rentedV = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
rentedT = ArrayPool<long>.Shared.Rent(count);
|
||||||
|
rentedO = ArrayPool<double>.Shared.Rent(count);
|
||||||
|
rentedH = ArrayPool<double>.Shared.Rent(count);
|
||||||
|
rentedL = ArrayPool<double>.Shared.Rent(count);
|
||||||
|
rentedC = ArrayPool<double>.Shared.Rent(count);
|
||||||
|
rentedV = ArrayPool<double>.Shared.Rent(count);
|
||||||
|
|
||||||
|
// Use only the first 'count' elements (rented arrays may be larger)
|
||||||
|
var t = rentedT.AsSpan(0, count);
|
||||||
|
var o = rentedO.AsSpan(0, count);
|
||||||
|
var h = rentedH.AsSpan(0, count);
|
||||||
|
var l = rentedL.AsSpan(0, count);
|
||||||
|
var c = rentedC.AsSpan(0, count);
|
||||||
|
var v = rentedV.AsSpan(0, count);
|
||||||
|
|
||||||
|
FetchCore(count, startTime, interval, t, o, h, l, c, v);
|
||||||
|
|
||||||
|
// Bulk add to series using ReadOnlySpan overload
|
||||||
|
series.AddRange(t, o, h, l, c, v);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (rentedT != null) ArrayPool<long>.Shared.Return(rentedT);
|
||||||
|
if (rentedO != null) ArrayPool<double>.Shared.Return(rentedO);
|
||||||
|
if (rentedH != null) ArrayPool<double>.Shared.Return(rentedH);
|
||||||
|
if (rentedL != null) ArrayPool<double>.Shared.Return(rentedL);
|
||||||
|
if (rentedC != null) ArrayPool<double>.Shared.Return(rentedC);
|
||||||
|
if (rentedV != null) ArrayPool<double>.Shared.Return(rentedV);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset streaming state after batch
|
||||||
|
_hasCurrentBar = false;
|
||||||
|
|
||||||
|
return series;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Core generation logic shared between stackalloc and heap-allocated paths.
|
||||||
|
/// </summary>
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private void FetchCore(int count, long startTime, TimeSpan interval,
|
||||||
|
Span<long> t, Span<double> o, Span<double> h, Span<double> l, Span<double> c, Span<double> v)
|
||||||
|
{
|
||||||
const double minutesPerYear = 252.0 * 6.5 * 60.0;
|
const double minutesPerYear = 252.0 * 6.5 * 60.0;
|
||||||
double dt = interval.TotalMinutes / minutesPerYear;
|
double dt = interval.TotalMinutes / minutesPerYear;
|
||||||
double drift = (Mu - 0.5 * Sigma * Sigma) * dt;
|
double drift = (Mu - 0.5 * Sigma * Sigma) * dt;
|
||||||
@@ -335,14 +404,6 @@ public sealed class GBM : IFeed
|
|||||||
// Update internal state to continue from end of batch
|
// Update internal state to continue from end of batch
|
||||||
_lastPrice = currentPrice;
|
_lastPrice = currentPrice;
|
||||||
_lastTime = currentTime - timeStep; // Last bar time, not next bar time
|
_lastTime = currentTime - timeStep; // Last bar time, not next bar time
|
||||||
|
|
||||||
// Bulk add to series
|
|
||||||
series.Add(t, o, h, l, c, v);
|
|
||||||
|
|
||||||
// Reset streaming state after batch
|
|
||||||
_hasCurrentBar = false;
|
|
||||||
|
|
||||||
return series;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#pragma warning restore S2245
|
#pragma warning restore S2245
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using TradingPlatform.BusinessLayer;
|
using TradingPlatform.BusinessLayer;
|
||||||
using static QuanTAlib.IndicatorExtensions;
|
using static QuanTAlib.IndicatorExtensions;
|
||||||
|
|
||||||
@@ -19,6 +20,11 @@ public class AccelIndicator : Indicator, IWatchlistIndicator
|
|||||||
private Accel? _accel;
|
private Accel? _accel;
|
||||||
private Func<IHistoryItem, double>? _selector;
|
private Func<IHistoryItem, double>? _selector;
|
||||||
|
|
||||||
|
// Cached markers to avoid per-update allocations
|
||||||
|
private static readonly IndicatorLineMarker GreenMarker = new(Color.Green);
|
||||||
|
private static readonly IndicatorLineMarker RedMarker = new(Color.Red);
|
||||||
|
private static readonly IndicatorLineMarker GrayMarker = new(Color.Gray);
|
||||||
|
|
||||||
public int MinHistoryDepths => 3;
|
public int MinHistoryDepths => 3;
|
||||||
public override string ShortName => "ACCEL";
|
public override string ShortName => "ACCEL";
|
||||||
|
|
||||||
@@ -47,25 +53,42 @@ public class AccelIndicator : Indicator, IWatchlistIndicator
|
|||||||
double value = _selector(item);
|
double value = _selector(item);
|
||||||
bool isNew = args.IsNewBar();
|
bool isNew = args.IsNewBar();
|
||||||
|
|
||||||
TValue input = new(item.TimeLeft, value);
|
ProcessUpdateCore(item.TimeLeft, value, isNew);
|
||||||
_accel.Update(input, isNew);
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private void ProcessUpdateCore(DateTime time, double value, bool isNew)
|
||||||
|
{
|
||||||
|
// Validate non-finite inputs - use last valid if not finite
|
||||||
|
if (!double.IsFinite(value))
|
||||||
|
{
|
||||||
|
value = _accel!.Last.Value;
|
||||||
|
if (!double.IsFinite(value))
|
||||||
|
value = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TValue input = new(time, value);
|
||||||
|
_accel!.Update(input, isNew);
|
||||||
|
|
||||||
bool isHot = _accel.IsHot;
|
bool isHot = _accel.IsHot;
|
||||||
|
double accelValue = _accel.Last.Value; // Cache to avoid repeated property access
|
||||||
|
|
||||||
LinesSeries[0].SetValue(_accel.Last.Value, isHot, ShowColdValues);
|
LinesSeries[0].SetValue(accelValue, isHot, ShowColdValues);
|
||||||
LinesSeries[1].SetValue(0);
|
LinesSeries[1].SetValue(0);
|
||||||
|
|
||||||
if (isHot || ShowColdValues)
|
if (isHot || ShowColdValues)
|
||||||
{
|
{
|
||||||
double accel = _accel.Last.Value;
|
// Use cached markers to avoid per-update allocations
|
||||||
Color color;
|
IndicatorLineMarker marker = GetMarker(accelValue);
|
||||||
if (accel > 0)
|
LinesSeries[0].SetMarker(0, marker);
|
||||||
color = Color.Green;
|
|
||||||
else if (accel < 0)
|
|
||||||
color = Color.Red;
|
|
||||||
else
|
|
||||||
color = Color.Gray;
|
|
||||||
LinesSeries[0].SetMarker(0, new IndicatorLineMarker(color));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static IndicatorLineMarker GetMarker(double value)
|
||||||
|
{
|
||||||
|
if (value > 0) return GreenMarker;
|
||||||
|
if (value < 0) return RedMarker;
|
||||||
|
return GrayMarker;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -171,9 +171,14 @@ public sealed class Accel : AbstractBase
|
|||||||
|
|
||||||
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
public override void Prime(ReadOnlySpan<double> source, TimeSpan? step = null)
|
||||||
{
|
{
|
||||||
foreach (double val in source)
|
// TValue is a readonly record struct - no heap allocation occurs
|
||||||
|
TimeSpan interval = step ?? TimeSpan.FromSeconds(1);
|
||||||
|
DateTime time = DateTime.UtcNow - (interval * source.Length);
|
||||||
|
|
||||||
|
for (int i = 0; i < source.Length; i++)
|
||||||
{
|
{
|
||||||
Update(new TValue(DateTime.MinValue, val));
|
Update(new TValue(time, source[i]), true);
|
||||||
|
time += interval;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,11 @@ public class ChangeIndicator : Indicator, IWatchlistIndicator
|
|||||||
private Change? _change;
|
private Change? _change;
|
||||||
private Func<IHistoryItem, double>? _selector;
|
private Func<IHistoryItem, double>? _selector;
|
||||||
|
|
||||||
|
// Cached markers to avoid per-update allocations
|
||||||
|
private static readonly IndicatorLineMarker GreenMarker = new(Color.Green);
|
||||||
|
private static readonly IndicatorLineMarker RedMarker = new(Color.Red);
|
||||||
|
private static readonly IndicatorLineMarker GrayMarker = new(Color.Gray);
|
||||||
|
|
||||||
public int MinHistoryDepths => Period + 1;
|
public int MinHistoryDepths => Period + 1;
|
||||||
public override string ShortName => $"CHANGE({Period})";
|
public override string ShortName => $"CHANGE({Period})";
|
||||||
|
|
||||||
@@ -55,21 +60,25 @@ public class ChangeIndicator : Indicator, IWatchlistIndicator
|
|||||||
_change.Update(input, isNew);
|
_change.Update(input, isNew);
|
||||||
|
|
||||||
bool isHot = _change.IsHot;
|
bool isHot = _change.IsHot;
|
||||||
|
double changeValue = _change.Last.Value; // Cache to avoid repeated property access
|
||||||
|
|
||||||
LinesSeries[0].SetValue(_change.Last.Value, isHot, ShowColdValues);
|
LinesSeries[0].SetValue(changeValue, isHot, ShowColdValues);
|
||||||
LinesSeries[1].SetValue(0);
|
LinesSeries[1].SetValue(0);
|
||||||
|
|
||||||
if (isHot || ShowColdValues)
|
if (isHot || ShowColdValues)
|
||||||
{
|
{
|
||||||
double change = _change.Last.Value;
|
// Use cached markers to avoid per-update allocations
|
||||||
Color color;
|
IndicatorLineMarker marker = GetMarker(changeValue);
|
||||||
if (change > 0)
|
LinesSeries[0].SetMarker(0, marker);
|
||||||
color = Color.Green;
|
|
||||||
else if (change < 0)
|
|
||||||
color = Color.Red;
|
|
||||||
else
|
|
||||||
color = Color.Gray;
|
|
||||||
LinesSeries[0].SetMarker(0, new IndicatorLineMarker(color));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static IndicatorLineMarker GetMarker(double value)
|
||||||
|
{
|
||||||
|
if (!double.IsFinite(value)) return GrayMarker;
|
||||||
|
if (value > 0) return GreenMarker;
|
||||||
|
if (value < 0) return RedMarker;
|
||||||
|
return GrayMarker;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,10 +162,16 @@ public class ChangeTests
|
|||||||
indicator.Update(_source[i]);
|
indicator.Update(_source[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compare last 10 values
|
// Compare last 10 values between batch and streaming
|
||||||
|
var streamResult = new TSeries();
|
||||||
|
for (int j = 0; j < _source.Count; j++)
|
||||||
|
{
|
||||||
|
streamResult.Add(indicator.Update(_source[j]), true);
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = Math.Max(0, _source.Count - 10); i < _source.Count; i++)
|
for (int i = Math.Max(0, _source.Count - 10); i < _source.Count; i++)
|
||||||
{
|
{
|
||||||
Assert.Equal(batchResult[i].Value, batchResult[i].Value, 1e-10);
|
Assert.Equal(batchResult[i].Value, streamResult[i].Value, 1e-10);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure final values match
|
// Ensure final values match
|
||||||
|
|||||||
@@ -116,4 +116,76 @@ public class ExptransIndicatorTests
|
|||||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExptransIndicator_NaNInput_ProducesFiniteOutput()
|
||||||
|
{
|
||||||
|
var indicator = new ExptransIndicator();
|
||||||
|
indicator.Initialize();
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
// First add a valid bar to establish last valid value
|
||||||
|
indicator.HistoricalData.AddBar(now, 1, 2, 0, 1);
|
||||||
|
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||||
|
|
||||||
|
// Add bar with NaN close - should use last valid value (1), so exp(1) = e
|
||||||
|
indicator.HistoricalData.AddBar(now.AddMinutes(1), double.NaN, double.NaN, double.NaN, double.NaN);
|
||||||
|
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||||
|
|
||||||
|
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||||
|
Assert.Equal(Math.E, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExptransIndicator_InfinityInput_ProducesFiniteOutput()
|
||||||
|
{
|
||||||
|
var indicator = new ExptransIndicator();
|
||||||
|
indicator.Initialize();
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
// First add a valid bar to establish last valid value
|
||||||
|
indicator.HistoricalData.AddBar(now, 1, 2, 0, 1);
|
||||||
|
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||||
|
|
||||||
|
// Add bar with Infinity close - should use last valid value (1), so exp(1) = e
|
||||||
|
indicator.HistoricalData.AddBar(now.AddMinutes(1), double.PositiveInfinity, double.PositiveInfinity, double.NegativeInfinity, double.PositiveInfinity);
|
||||||
|
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||||
|
|
||||||
|
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||||
|
Assert.Equal(Math.E, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExptransIndicator_NewTick_UpdatesSameBar()
|
||||||
|
{
|
||||||
|
var indicator = new ExptransIndicator();
|
||||||
|
indicator.Initialize();
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
indicator.HistoricalData.AddBar(now, 0, 1, -1, 0);
|
||||||
|
|
||||||
|
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||||
|
double firstValue = indicator.LinesSeries[0].GetValue(0);
|
||||||
|
|
||||||
|
// NewTick should recalculate the same bar
|
||||||
|
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||||
|
|
||||||
|
// Value should remain consistent (exp(0) = 1)
|
||||||
|
Assert.Equal(1.0, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||||
|
Assert.Equal(firstValue, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExptransIndicator_KnownValues_ComputesCorrectly()
|
||||||
|
{
|
||||||
|
var indicator = new ExptransIndicator();
|
||||||
|
indicator.Initialize();
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
// exp(2) ≈ 7.389
|
||||||
|
indicator.HistoricalData.AddBar(now, 2, 3, 1, 2);
|
||||||
|
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||||
|
|
||||||
|
Assert.Equal(Math.Exp(2), indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,9 +2,6 @@
|
|||||||
// Transforms values using the exponential function e^x
|
// Transforms values using the exponential function e^x
|
||||||
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Numerics;
|
|
||||||
using System.Runtime.Intrinsics;
|
|
||||||
using System.Runtime.Intrinsics.X86;
|
|
||||||
|
|
||||||
namespace QuanTAlib;
|
namespace QuanTAlib;
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,8 @@ public class HighestTests
|
|||||||
|
|
||||||
indicator.Update(new TValue(time, 15.0));
|
indicator.Update(new TValue(time, 15.0));
|
||||||
indicator.Update(new TValue(time.AddMinutes(1), double.PositiveInfinity));
|
indicator.Update(new TValue(time.AddMinutes(1), double.PositiveInfinity));
|
||||||
Assert.True(double.IsFinite(indicator.Last.Value));
|
// Should use last valid value (15.0) instead of infinity
|
||||||
|
Assert.Equal(15.0, indicator.Last.Value, Tolerance);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -148,33 +148,48 @@ public sealed class Highest : AbstractBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Second pass: compute rolling max using corrected values
|
// Second pass: compute rolling max using corrected values
|
||||||
int dequeStart = 0;
|
// Use circular buffer indexing to avoid compaction overhead
|
||||||
int dequeEnd = 0;
|
// Branch-based wrapping is faster than modulo in hot paths
|
||||||
|
int head = 0; // front of deque (oldest/max)
|
||||||
|
int tail = 0; // back of deque (newest)
|
||||||
|
int count = 0; // number of elements in deque
|
||||||
|
int capacity = deque.Length;
|
||||||
|
|
||||||
for (int i = 0; i < len; i++)
|
for (int i = 0; i < len; i++)
|
||||||
{
|
{
|
||||||
double value = values[i];
|
double value = values[i];
|
||||||
|
|
||||||
// Remove indices outside window
|
// Remove indices outside window from front
|
||||||
while (dequeEnd > dequeStart && deque[dequeStart] <= i - period)
|
while (count > 0 && deque[head] <= i - period)
|
||||||
dequeStart++;
|
|
||||||
|
|
||||||
// Remove smaller values from back
|
|
||||||
while (dequeEnd > dequeStart && values[deque[dequeEnd - 1]] <= value)
|
|
||||||
dequeEnd--;
|
|
||||||
|
|
||||||
// Compact deque if needed
|
|
||||||
if (dequeEnd >= deque.Length)
|
|
||||||
{
|
{
|
||||||
int count = dequeEnd - dequeStart;
|
head++;
|
||||||
for (int j = 0; j < count; j++)
|
if (head >= capacity) head -= capacity;
|
||||||
deque[j] = deque[dequeStart + j];
|
count--;
|
||||||
dequeStart = 0;
|
|
||||||
dequeEnd = count;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deque[dequeEnd++] = i;
|
// Remove smaller values from back
|
||||||
output[i] = values[deque[dequeStart]];
|
while (count > 0)
|
||||||
|
{
|
||||||
|
int backIdx = tail - 1;
|
||||||
|
if (backIdx < 0) backIdx += capacity;
|
||||||
|
if (values[deque[backIdx]] <= value)
|
||||||
|
{
|
||||||
|
tail = backIdx;
|
||||||
|
count--;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add current index at tail
|
||||||
|
deque[tail] = i;
|
||||||
|
tail++;
|
||||||
|
if (tail >= capacity) tail -= capacity;
|
||||||
|
count++;
|
||||||
|
|
||||||
|
output[i] = values[deque[head]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
|
|||||||
@@ -184,7 +184,8 @@ public class JerkIndicatorTests
|
|||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
|
|
||||||
// Cubic trend: changing acceleration = non-zero jerk
|
// Cubic trend: f(x) = x³ has third derivative = 6
|
||||||
|
// Using f(i) = i³, the discrete third differences converge to 6
|
||||||
for (int i = 0; i < 10; i++)
|
for (int i = 0; i < 10; i++)
|
||||||
{
|
{
|
||||||
double price = 100 + i * i * i; // cubic growth
|
double price = 100 + i * i * i; // cubic growth
|
||||||
@@ -193,7 +194,8 @@ public class JerkIndicatorTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
double lastJerk = indicator.LinesSeries[0].GetValue(0);
|
double lastJerk = indicator.LinesSeries[0].GetValue(0);
|
||||||
Assert.True(lastJerk != 0);
|
// For f(x) = x³, discrete third difference = 6
|
||||||
|
Assert.Equal(6.0, lastJerk, 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Xunit;
|
||||||
|
|
||||||
namespace QuanTAlib.Tests;
|
namespace QuanTAlib.Tests;
|
||||||
|
|
||||||
public class JerkTests
|
public class JerkTests
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ public class LineartransIndicatorTests
|
|||||||
[Fact]
|
[Fact]
|
||||||
public void LineartransIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
public void LineartransIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||||
{
|
{
|
||||||
var indicator = new LineartransIndicator();
|
var indicator = new LineartransIndicator { Slope = 2.0, Intercept = 5.0 };
|
||||||
indicator.Initialize();
|
indicator.Initialize();
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
@@ -90,6 +90,8 @@ public class LineartransIndicatorTests
|
|||||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||||
|
|
||||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||||
|
// NewTick recalculates same bar: 2 * 100 + 5 = 205
|
||||||
|
Assert.Equal(205.0, indicator.LinesSeries[0].GetValue(0), 1e-10);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.Intrinsics;
|
using System.Runtime.Intrinsics;
|
||||||
using System.Runtime.Intrinsics.X86;
|
using System.Runtime.Intrinsics.X86;
|
||||||
|
using System.Runtime.Intrinsics.Arm;
|
||||||
|
|
||||||
namespace QuanTAlib;
|
namespace QuanTAlib;
|
||||||
|
|
||||||
@@ -119,6 +120,7 @@ public sealed class Lineartrans : AbstractBase
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates linear transformation over a span of values using SIMD when available.
|
/// Calculates linear transformation over a span of values using SIMD when available.
|
||||||
|
/// Uses FMA intrinsics for y = slope * x + intercept.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output,
|
public static void Calculate(ReadOnlySpan<double> source, Span<double> output,
|
||||||
double slope = 1.0, double intercept = 0.0)
|
double slope = 1.0, double intercept = 0.0)
|
||||||
@@ -132,34 +134,84 @@ public sealed class Lineartrans : AbstractBase
|
|||||||
if (!double.IsFinite(intercept))
|
if (!double.IsFinite(intercept))
|
||||||
throw new ArgumentException("Intercept must be a finite number", nameof(intercept));
|
throw new ArgumentException("Intercept must be a finite number", nameof(intercept));
|
||||||
|
|
||||||
|
// Check for non-finite values - if any exist, use scalar path only
|
||||||
|
// Note: For very large arrays, SIMD-based NaN detection could be faster,
|
||||||
|
// but for typical use cases the scalar pre-scan is sufficient
|
||||||
|
bool hasNonFinite = false;
|
||||||
|
for (int k = 0; k < source.Length && !hasNonFinite; k++)
|
||||||
|
{
|
||||||
|
hasNonFinite = !double.IsFinite(source[k]);
|
||||||
|
}
|
||||||
|
|
||||||
double lastValid = 0.0;
|
double lastValid = 0.0;
|
||||||
int i = 0;
|
int i = 0;
|
||||||
|
|
||||||
// SIMD path for AVX2 (process 4 doubles at a time)
|
// AVX512 FMA path (8 doubles at once)
|
||||||
if (Avx2.IsSupported && source.Length >= Vector256<double>.Count)
|
// Avx512F.FusedMultiplyAdd is independent of Fma.IsSupported
|
||||||
|
if (!hasNonFinite && Avx512F.IsSupported && source.Length >= 8)
|
||||||
{
|
{
|
||||||
int vectorLength = source.Length - (source.Length % Vector256<double>.Count);
|
var slopeVec = Vector512.Create(slope);
|
||||||
|
var interceptVec = Vector512.Create(intercept);
|
||||||
|
int simdEnd = source.Length - (source.Length % 8);
|
||||||
|
|
||||||
for (; i < vectorLength; i += Vector256<double>.Count)
|
for (; i < simdEnd; i += 8)
|
||||||
{
|
{
|
||||||
// Check for finite values and handle last-valid
|
var vals = Vector512.Create(source.Slice(i, 8));
|
||||||
for (int j = 0; j < Vector256<double>.Count; j++)
|
var result = Avx512F.FusedMultiplyAdd(slopeVec, vals, interceptVec);
|
||||||
{
|
result.CopyTo(output.Slice(i, 8));
|
||||||
double val = source[i + j];
|
|
||||||
if (double.IsFinite(val))
|
|
||||||
{
|
|
||||||
lastValid = Math.FusedMultiplyAdd(slope, val, intercept);
|
|
||||||
output[i + j] = lastValid;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
output[i + j] = lastValid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
lastValid = output[simdEnd - 1];
|
||||||
|
}
|
||||||
|
// AVX2 FMA path (4 doubles at once)
|
||||||
|
else if (!hasNonFinite && Fma.IsSupported && source.Length >= 4)
|
||||||
|
{
|
||||||
|
var slopeVec = Vector256.Create(slope);
|
||||||
|
var interceptVec = Vector256.Create(intercept);
|
||||||
|
int simdEnd = source.Length - (source.Length % 4);
|
||||||
|
|
||||||
|
for (; i < simdEnd; i += 4)
|
||||||
|
{
|
||||||
|
var vals = Vector256.Create(source.Slice(i, 4));
|
||||||
|
var result = Fma.MultiplyAdd(slopeVec, vals, interceptVec);
|
||||||
|
result.CopyTo(output.Slice(i, 4));
|
||||||
|
}
|
||||||
|
lastValid = output[simdEnd - 1];
|
||||||
|
}
|
||||||
|
// SSE2 path (2 doubles at once) - fallback for x86/x64 without FMA
|
||||||
|
// Note: This path uses Sse2.Multiply followed by Sse2.Add, which incurs two rounding
|
||||||
|
// steps unlike the FMA paths above. Results may differ by ~1 ULP compared to FMA
|
||||||
|
// on SSE2-only hardware (e.g., older x86/x64 CPUs without AVX2/FMA support).
|
||||||
|
else if (!hasNonFinite && Sse2.IsSupported && source.Length >= 2)
|
||||||
|
{
|
||||||
|
var slopeVec = Vector128.Create(slope);
|
||||||
|
var interceptVec = Vector128.Create(intercept);
|
||||||
|
int simdEnd = source.Length - (source.Length % 2);
|
||||||
|
|
||||||
|
for (; i < simdEnd; i += 2)
|
||||||
|
{
|
||||||
|
var vals = Vector128.Create(source.Slice(i, 2));
|
||||||
|
var result = Sse2.Add(Sse2.Multiply(slopeVec, vals), interceptVec);
|
||||||
|
result.CopyTo(output.Slice(i, 2));
|
||||||
|
}
|
||||||
|
lastValid = output[simdEnd - 1];
|
||||||
|
}
|
||||||
|
// ARM64 NEON FMA path (2 doubles at once)
|
||||||
|
else if (!hasNonFinite && AdvSimd.Arm64.IsSupported && source.Length >= 2)
|
||||||
|
{
|
||||||
|
var slopeVec = Vector128.Create(slope);
|
||||||
|
var interceptVec = Vector128.Create(intercept);
|
||||||
|
int simdEnd = source.Length - (source.Length % 2);
|
||||||
|
|
||||||
|
for (; i < simdEnd; i += 2)
|
||||||
|
{
|
||||||
|
var vals = Vector128.Create(source.Slice(i, 2));
|
||||||
|
var result = AdvSimd.Arm64.FusedMultiplyAdd(interceptVec, vals, slopeVec);
|
||||||
|
result.CopyTo(output.Slice(i, 2));
|
||||||
|
}
|
||||||
|
lastValid = output[simdEnd - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scalar fallback for remaining elements
|
// Scalar fallback for remaining elements or when non-finite values exist
|
||||||
for (; i < source.Length; i++)
|
for (; i < source.Length; i++)
|
||||||
{
|
{
|
||||||
double val = source[i];
|
double val = source[i];
|
||||||
|
|||||||
@@ -87,27 +87,37 @@ public class LogtransValidationTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Logtrans_ProductRule()
|
public void Logtrans_ZeroInput_UsesLastValid()
|
||||||
{
|
{
|
||||||
// ln(a*b) = ln(a) + ln(b)
|
// Zero input uses last valid value (robustness pattern)
|
||||||
double a = 2.5;
|
|
||||||
double b = 3.7;
|
|
||||||
|
|
||||||
var indicator = new Logtrans();
|
var indicator = new Logtrans();
|
||||||
var time = DateTime.UtcNow;
|
var time = DateTime.UtcNow;
|
||||||
|
|
||||||
indicator.Update(new TValue(time, a));
|
// First update with valid value
|
||||||
double lnA = indicator.Last.Value;
|
indicator.Update(new TValue(time, Math.E));
|
||||||
|
double lastValid = indicator.Last.Value; // ln(e) = 1.0
|
||||||
|
|
||||||
indicator.Reset();
|
// Zero input - should use last valid
|
||||||
indicator.Update(new TValue(time, b));
|
indicator.Update(new TValue(time.AddMinutes(1), 0.0));
|
||||||
double lnB = indicator.Last.Value;
|
|
||||||
|
|
||||||
indicator.Reset();
|
Assert.Equal(lastValid, indicator.Last.Value, Tolerance);
|
||||||
indicator.Update(new TValue(time, a * b));
|
}
|
||||||
double lnAB = indicator.Last.Value;
|
|
||||||
|
|
||||||
Assert.Equal(lnA + lnB, lnAB, Tolerance);
|
[Fact]
|
||||||
|
public void Logtrans_NegativeInput_UsesLastValid()
|
||||||
|
{
|
||||||
|
// Negative input uses last valid value (robustness pattern)
|
||||||
|
var indicator = new Logtrans();
|
||||||
|
var time = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// First update with valid value
|
||||||
|
indicator.Update(new TValue(time, 2.0));
|
||||||
|
double lastValid = indicator.Last.Value; // ln(2)
|
||||||
|
|
||||||
|
// Negative input - should use last valid
|
||||||
|
indicator.Update(new TValue(time.AddMinutes(1), -1.0));
|
||||||
|
|
||||||
|
Assert.Equal(lastValid, indicator.Last.Value, Tolerance);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -153,4 +163,111 @@ public class LogtransValidationTests
|
|||||||
|
|
||||||
Assert.Equal(n * lnA, lnAPowN, Tolerance);
|
Assert.Equal(n * lnA, lnAPowN, Tolerance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Logtrans_VerySmallPositive_ApproachesNegativeInfinity()
|
||||||
|
{
|
||||||
|
// ln(ε) → -∞ as ε → 0+
|
||||||
|
var indicator = new Logtrans();
|
||||||
|
var time = DateTime.UtcNow;
|
||||||
|
|
||||||
|
indicator.Update(new TValue(time, double.Epsilon));
|
||||||
|
double result = indicator.Last.Value;
|
||||||
|
|
||||||
|
Assert.True(double.IsFinite(result));
|
||||||
|
Assert.True(result < -700); // ln(double.Epsilon) ≈ -744
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Logtrans_VeryLargeValue_Handles()
|
||||||
|
{
|
||||||
|
// ln(large) should be finite
|
||||||
|
var indicator = new Logtrans();
|
||||||
|
var time = DateTime.UtcNow;
|
||||||
|
|
||||||
|
indicator.Update(new TValue(time, 1e300));
|
||||||
|
double result = indicator.Last.Value;
|
||||||
|
|
||||||
|
Assert.True(double.IsFinite(result));
|
||||||
|
Assert.Equal(Math.Log(1e300), result, Tolerance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Logtrans_Span_ZeroInput_UsesLastValid()
|
||||||
|
{
|
||||||
|
// Span API: zero input uses last valid value (robustness pattern)
|
||||||
|
var values = new double[] { 2.0, 0.0, 3.0 };
|
||||||
|
var output = new double[3];
|
||||||
|
|
||||||
|
Logtrans.Calculate(values, output);
|
||||||
|
|
||||||
|
Assert.Equal(Math.Log(2.0), output[0], Tolerance); // ln(2)
|
||||||
|
Assert.Equal(Math.Log(2.0), output[1], Tolerance); // zero -> uses last valid (ln(2))
|
||||||
|
Assert.Equal(Math.Log(3.0), output[2], Tolerance); // ln(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Logtrans_Span_NegativeInput_UsesLastValid()
|
||||||
|
{
|
||||||
|
// Span API: negative input uses last valid value (robustness pattern)
|
||||||
|
var values = new double[] { 2.0, -5.0, 3.0 };
|
||||||
|
var output = new double[3];
|
||||||
|
|
||||||
|
Logtrans.Calculate(values, output);
|
||||||
|
|
||||||
|
Assert.Equal(Math.Log(2.0), output[0], Tolerance); // ln(2)
|
||||||
|
Assert.Equal(Math.Log(2.0), output[1], Tolerance); // negative -> uses last valid (ln(2))
|
||||||
|
Assert.Equal(Math.Log(3.0), output[2], Tolerance); // ln(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Logtrans_NaNInput_UsesLastValid()
|
||||||
|
{
|
||||||
|
// NaN input uses last valid value (robustness pattern)
|
||||||
|
var indicator = new Logtrans();
|
||||||
|
var time = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// First update with valid value
|
||||||
|
indicator.Update(new TValue(time, Math.E));
|
||||||
|
double lastValid = indicator.Last.Value; // ln(e) = 1.0
|
||||||
|
|
||||||
|
// NaN input - should use last valid
|
||||||
|
indicator.Update(new TValue(time.AddMinutes(1), double.NaN));
|
||||||
|
|
||||||
|
Assert.Equal(lastValid, indicator.Last.Value, Tolerance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Logtrans_PositiveInfinityInput_UsesLastValid()
|
||||||
|
{
|
||||||
|
// Positive infinity input uses last valid value (robustness pattern)
|
||||||
|
var indicator = new Logtrans();
|
||||||
|
var time = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// First update with valid value
|
||||||
|
indicator.Update(new TValue(time, 10.0));
|
||||||
|
double lastValid = indicator.Last.Value; // ln(10)
|
||||||
|
|
||||||
|
// Positive infinity input - should use last valid
|
||||||
|
indicator.Update(new TValue(time.AddMinutes(1), double.PositiveInfinity));
|
||||||
|
|
||||||
|
Assert.Equal(lastValid, indicator.Last.Value, Tolerance);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Logtrans_NegativeInfinityInput_UsesLastValid()
|
||||||
|
{
|
||||||
|
// Negative infinity input uses last valid value (robustness pattern)
|
||||||
|
var indicator = new Logtrans();
|
||||||
|
var time = DateTime.UtcNow;
|
||||||
|
|
||||||
|
// First update with valid value
|
||||||
|
indicator.Update(new TValue(time, 5.0));
|
||||||
|
double lastValid = indicator.Last.Value; // ln(5)
|
||||||
|
|
||||||
|
// Negative infinity input - should use last valid
|
||||||
|
indicator.Update(new TValue(time.AddMinutes(1), double.NegativeInfinity));
|
||||||
|
|
||||||
|
Assert.Equal(lastValid, indicator.Last.Value, Tolerance);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,9 +2,6 @@
|
|||||||
// Transforms values using natural logarithm (base e)
|
// Transforms values using natural logarithm (base e)
|
||||||
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Numerics;
|
|
||||||
using System.Runtime.Intrinsics;
|
|
||||||
using System.Runtime.Intrinsics.X86;
|
|
||||||
|
|
||||||
namespace QuanTAlib;
|
namespace QuanTAlib;
|
||||||
|
|
||||||
@@ -102,7 +99,8 @@ public sealed class Logtrans : AbstractBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Calculates natural logarithm over a span of values using SIMD when available.
|
/// Calculates natural logarithm over a span of values.
|
||||||
|
/// Note: Math.Log has no SIMD intrinsic; uses scalar path with last-valid substitution.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static void Calculate(ReadOnlySpan<double> source, Span<double> output)
|
public static void Calculate(ReadOnlySpan<double> source, Span<double> output)
|
||||||
{
|
{
|
||||||
@@ -112,34 +110,8 @@ public sealed class Logtrans : AbstractBase
|
|||||||
throw new ArgumentException("Output length must be >= source length", nameof(output));
|
throw new ArgumentException("Output length must be >= source length", nameof(output));
|
||||||
|
|
||||||
double lastValid = 0.0;
|
double lastValid = 0.0;
|
||||||
int i = 0;
|
|
||||||
|
|
||||||
// SIMD path for AVX2 (process 4 doubles at a time)
|
for (int i = 0; i < source.Length; i++)
|
||||||
if (Avx2.IsSupported && source.Length >= Vector256<double>.Count)
|
|
||||||
{
|
|
||||||
int vectorLength = source.Length - (source.Length % Vector256<double>.Count);
|
|
||||||
|
|
||||||
for (; i < vectorLength; i += Vector256<double>.Count)
|
|
||||||
{
|
|
||||||
// Process scalar for proper last-valid handling (Logtrans has no SIMD intrinsic)
|
|
||||||
for (int j = 0; j < Vector256<double>.Count; j++)
|
|
||||||
{
|
|
||||||
double val = source[i + j];
|
|
||||||
if (double.IsFinite(val) && val > 0)
|
|
||||||
{
|
|
||||||
lastValid = Math.Log(val);
|
|
||||||
output[i + j] = lastValid;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
output[i + j] = lastValid;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scalar fallback for remaining elements
|
|
||||||
for (; i < source.Length; i++)
|
|
||||||
{
|
{
|
||||||
double val = source[i];
|
double val = source[i];
|
||||||
if (double.IsFinite(val) && val > 0)
|
if (double.IsFinite(val) && val > 0)
|
||||||
|
|||||||
Reference in New Issue
Block a user