docs: remove C# Implementation Considerations sections, clean up temp scripts, reorganize test files

- Remove 'C# Implementation Considerations' sections from 34 indicator .md files
- Delete 29 temp PowerShell scripts (_fix_mojibake.ps1, _hex_scan.ps1, etc.)
- Move test files into tests/ subdirectories for consistent project structure
- Add trader-focused bullet points to indicator documentation
This commit is contained in:
Miha Kralj
2026-03-12 12:34:16 -07:00
parent 8937b0c0fa
commit 060649192f
1149 changed files with 1780 additions and 3316 deletions
-116
View File
@@ -172,122 +172,6 @@ QuanTAlib validates against reference implementations that respect the Gaussian
| **TA-Lib** | ❌ | Not included in standard C distribution. |
| **Tulip** | ❌ | Not included. |
## C# Implementation Considerations
### Precomputed Gaussian Weights
Weights are computed once in the constructor and stored in a `double[]` array:
```csharp
_weights = new double[period];
ComputeWeights(_weights, period, offset, sigma, out _invWeightSum);
```
The inverse of the weight sum is precomputed for multiplication instead of division in the hot path.
### State Record Struct with Auto Layout
Minimal state for bar correction:
```csharp
[StructLayout(LayoutKind.Auto)]
private record struct State(double LastValidValue, bool IsInitialized);
```
The `LayoutKind.Auto` lets the JIT optimize field placement for cache efficiency.
### SIMD-Optimized Dot Product
The weighted sum calculation delegates to a SIMD-optimized `DotProduct` extension method:
```csharp
double sum1 = internalBuf.Slice(head, part1Len).DotProduct(_weights.AsSpan(0, part1Len));
double sum2 = internalBuf[..head].DotProduct(_weights.AsSpan(part1Len));
return (sum1 + sum2) * _invWeightSum;
```
The dot product leverages AVX2/AVX-512/NEON intrinsics internally, achieving up to 8× speedup.
### Circular Buffer Handling
The RingBuffer's internal array is accessed directly to split the dot product across the wrap boundary:
```csharp
ReadOnlySpan<double> internalBuf = _buffer.InternalBuffer;
int head = _buffer.StartIndex;
int part1Len = _period - head;
// Part 1: head..end with weights[0..part1Len]
// Part 2: 0..head with weights[part1Len..period]
```
This avoids copying the buffer into a contiguous array.
### Stackalloc/ArrayPool Allocation Strategy
The static `Calculate` method uses stackalloc for small periods and ArrayPool for large:
```csharp
double[]? weightsArray = period > 256 ? ArrayPool<double>.Shared.Rent(period) : null;
Span<double> weights = period <= 256
? stackalloc double[period]
: weightsArray!.AsSpan(0, period);
```
The 256-element threshold balances stack safety with allocation overhead.
### NaN Handling with Initialization Tracking
Non-finite inputs are replaced with the last valid value, with explicit tracking for uninitialized state:
```csharp
private double GetValidValue(double input)
{
if (double.IsFinite(input))
return input;
return _state.IsInitialized ? _state.LastValidValue : double.NaN;
}
```
This prevents NaN propagation while correctly handling series that start with invalid values.
### Incremental Weight Sum for Warmup
During the warmup period, the weight sum is computed incrementally:
```csharp
if (count < period)
{
count++;
currentWeightSum += weights[period - count];
}
```
This avoids recalculating the partial sum on each bar during convergence.
### Separate Internal Update Method
The `Update` method has a private overload with a `publish` parameter:
```csharp
private TValue Update(TValue input, bool isNew, bool publish)
```
This allows state restoration after batch processing without firing events.
### Memory Layout
| Component | Size | Purpose |
| :--- | :--- | :--- |
| `_weights` | 8×period bytes | Precomputed Gaussian weights |
| `_buffer` (RingBuffer) | 32 + 8×period bytes | Sliding window history |
| `_state` | ~16 bytes | LastValidValue, IsInitialized |
| `_p_state` | ~16 bytes | Previous state for rollback |
| Scalars | ~40 bytes | Period, offset, sigma, invWeightSum |
| **Total** | **~104 + 16N bytes** | Per-instance footprint |
For ALMA(50), total memory is approximately 900 bytes per instance.
## Common Pitfalls
1. **Offset Abuse**: Setting offset to `0.99` creates a filter that barely filters. It tracks price so closely you might as well use `Price[0]`. Setting it to `0.5` makes it a centered moving average (great for smoothing, terrible for trading due to repainting if used as such, but ALMA does not repaint). The magic is in the `0.85` region.
-139
View File
@@ -116,145 +116,6 @@ BLMA is validated against a reference implementation using the standard Blackman
| **QuanTAlib** | ✅ | Matches theoretical formula. |
| **PineScript** | ✅ | Matches PineScript reference logic. |
### C# Implementation Considerations
The QuanTAlib BLMA implementation emphasizes precomputation and zero-allocation streaming:
#### Precomputed Weights Array
Blackman window weights are calculated once in the constructor and reused for every update:
```csharp
public Blma(int period)
{
_weights = new double[period];
_weightSum = CalculateWeights(period, _weights);
}
private static double CalculateWeights(int n, Span<double> weights)
{
const double a0 = 0.42;
const double a1 = 0.5;
const double a2 = 0.08;
double invNMinus1 = 1.0 / (n - 1);
for (int i = 0; i < n; i++)
{
double ratio = i * invNMinus1;
double w = a0 - (a1 * Math.Cos(2.0 * Math.PI * ratio))
+ (a2 * Math.Cos(4.0 * Math.PI * ratio));
weights[i] = w;
totalWeight += w;
}
return totalWeight;
}
```
#### RingBuffer with DotProduct Extension
The weighted sum uses an optimized dot product that handles circular buffer wraparound:
```csharp
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double CalculateWeightedSum(RingBuffer buffer, ReadOnlySpan<double> weights)
{
int start = buffer.StartIndex;
int count = buffer.Count;
int capacity = buffer.Capacity;
if (start + count <= capacity)
{
// Contiguous case - single dot product
return buffer.InternalBuffer.Slice(start, count).DotProduct(weights);
}
// Wraparound case - two dot products
int firstPartLength = capacity - start;
int secondPartLength = count - firstPartLength;
double sum1 = buffer.InternalBuffer.Slice(start, firstPartLength).DotProduct(weights[..firstPartLength]);
double sum2 = buffer.InternalBuffer.Slice(0, secondPartLength).DotProduct(weights[firstPartLength..]);
return sum1 + sum2;
}
```
#### Dynamic Warmup Weights
During warmup (fewer than `period` bars), weights are calculated dynamically using stackalloc:
```csharp
if (_buffer.Count < _period)
{
int count = _buffer.Count;
Span<double> currentWeights = stackalloc double[count];
double currentWeightSum = CalculateWeights(count, currentWeights);
result = ComputeWeightedAverage(currentWeightSum, weightedSum, _buffer.Average());
}
```
#### Stackalloc Strategy for Batch Processing
The static `Calculate` method uses stackalloc for small periods (≤256) to avoid heap allocation:
```csharp
Span<double> weights = period <= 256 ? stackalloc double[period] : new double[period];
double weightSum = CalculateWeights(period, weights);
// Buffer for warmup weights to avoid stackalloc in loop
Span<double> warmupWeightsBuffer = period <= 256 ? stackalloc double[period] : new double[period];
```
#### NaN Handling with Last-Valid-Value Substitution
Invalid values are substituted with the last valid value to maintain calculation continuity:
```csharp
double val = input.Value;
if (!double.IsFinite(val))
{
return Last; // Return last result without changing state
}
```
In batch mode:
```csharp
double lastValid = double.NaN;
for (int i = 0; i < source.Length; i++)
{
double val = source[i];
if (!double.IsFinite(val))
val = double.IsNaN(lastValid) ? 0 : lastValid;
else
lastValid = val;
// ...
}
```
#### AggressiveInlining on Hot Paths
Critical methods are marked for inlining:
```csharp
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static double ComputeWeightedAverage(double weightSum, double weightedSum, double fallbackAverage)
{
return Math.Abs(weightSum) < double.Epsilon ? fallbackAverage : weightedSum / weightSum;
}
```
#### Memory Layout
| Field | Type | Size | Purpose |
| :--- | :--- | :---: | :--- |
| `_period` | `int` | 4 | Window size |
| `_buffer` | `RingBuffer` | 8 (ref) | Circular price storage |
| `_weights` | `double[]` | 8 (ref) | Precomputed Blackman weights |
| `_weightSum` | `double` | 8 | Sum of weights (precomputed) |
| **Total** | | **~28 bytes** | Per instance (excluding buffer/array internals) |
**Weight array storage:** `period × 8` bytes (e.g., 160 bytes for period=20)
### Common Pitfalls
* **Lag**: BLMA has more lag than EMA or WMA because it suppresses the most recent data. It is a smoothing filter, not a leading indicator.
-145
View File
@@ -191,151 +191,6 @@ Self-consistency validation ensures:
* NaN handling substitutes last valid value
* Reset produces identical results on replay
### C# Implementation Considerations
The QuanTAlib BWMA implementation optimizes for streaming throughput with precomputed weights and zero-allocation hot paths:
#### Precomputed Weights with Inverse Sum
Weights and the inverse of their sum are calculated once in the constructor, replacing division with multiplication:
```csharp
public Bwma(int period, int order = 0)
{
_weights = new double[period];
ComputeWeights(_weights, period, order, out _invWeightSum);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ComputeWeights(Span<double> weights, int period, int order, out double invWeightSum)
{
double sum = 0;
double scale = period > 1 ? 2.0 / (period - 1) : 0.0;
double power = order * 0.5 + 0.5;
for (int i = 0; i < period; i++)
{
double x = period > 1 ? i * scale - 1.0 : 0.0;
double arg = 1.0 - x * x;
double w = arg > 0.0 ? Math.Pow(arg, power) : 0.0;
weights[i] = w;
sum += w;
}
invWeightSum = sum > 0 ? 1.0 / sum : 0.0; // Precompute inverse
}
```
#### State Record Struct with Auto Layout
State uses `LayoutKind.Auto` for compiler-optimized field arrangement:
```csharp
[StructLayout(LayoutKind.Auto)]
private record struct State
{
public double LastValidValue;
public bool IsInitialized;
}
private State _state;
private State _p_state; // Previous state for bar correction
```
#### FusedMultiplyAdd in Warmup Path
The warmup calculation uses FMA for coordinate mapping and argument computation:
```csharp
for (int i = 0; i < p; i++)
{
double x = Math.FusedMultiplyAdd(i, scale, -1.0); // x = i * scale - 1.0
double arg = Math.FusedMultiplyAdd(-x, x, 1.0); // arg = 1.0 - x * x
// ...
sum = Math.FusedMultiplyAdd(window[i], w, sum); // sum += window[i] * w
}
```
#### Optimized Circular Buffer DotProduct
The hot path handles ring buffer wraparound with two slice dot products:
```csharp
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double CalculateWeightedSum(double fallbackValue)
{
if (_invWeightSum == 0.0) return fallbackValue;
ReadOnlySpan<double> internalBuf = _buffer.InternalBuffer;
int head = _buffer.StartIndex;
int part1Len = _period - head;
double sum1 = internalBuf.Slice(head, part1Len).DotProduct(_weights.AsSpan(0, part1Len));
double sum2 = internalBuf[..head].DotProduct(_weights.AsSpan(part1Len));
return (sum1 + sum2) * _invWeightSum; // Multiply by precomputed inverse
}
```
#### ArrayPool for Large Periods in Batch Mode
The static `Calculate` method uses ArrayPool for periods >256 to avoid large stack allocations:
```csharp
double[]? weightsArray = period > 256 ? ArrayPool<double>.Shared.Rent(period) : null;
Span<double> weights = period <= 256
? stackalloc double[period]
: weightsArray!.AsSpan(0, period);
double[]? ringArray = period > 256 ? ArrayPool<double>.Shared.Rent(period) : null;
Span<double> ring = period <= 256
? stackalloc double[period]
: ringArray!.AsSpan(0, period);
try
{
// Processing loop...
}
finally
{
if (weightsArray != null) ArrayPool<double>.Shared.Return(weightsArray);
if (ringArray != null) ArrayPool<double>.Shared.Return(ringArray);
}
```
#### PineScript-Exact Order Handling
The implementation matches PineScript behavior with special cases for orders 0 and 1:
```csharp
if (order == 0)
{
w = arg; // (1 - x²)^1.0 - parabolic
}
else if (order == 1)
{
w = arg * Math.Sqrt(arg); // (1 - x²)^1.5 - avoids Math.Pow overhead
}
else
{
w = Math.Pow(arg, power); // (1 - x²)^power
}
```
#### Memory Layout
| Field | Type | Size | Purpose |
| :--- | :--- | :---: | :--- |
| `_period` | `int` | 4 | Window length |
| `_order` | `int` | 4 | Bessel order parameter |
| `_power` | `double` | 8 | Precomputed exponent |
| `_weights` | `double[]` | 8 (ref) | Precomputed weights |
| `_invWeightSum` | `double` | 8 | Inverse of weight sum |
| `_buffer` | `RingBuffer` | 8 (ref) | Circular price storage |
| `_state` | `State` | 16 | Current state (LastValidValue, IsInitialized) |
| `_p_state` | `State` | 16 | Previous state for rollback |
| **Total** | | **~72 bytes** | Per instance (excluding buffer/array internals) |
**Weight array storage:** `period × 8` bytes (e.g., 160 bytes for period=20)
## Common Pitfalls
1. **Order Selection Paralysis**: Start with order 0 (parabolic). It's the most balanced choice. Higher orders provide sharper filtering but may over-smooth trend transitions.
-55
View File
@@ -108,61 +108,6 @@ Validation is performed by reproducing standard moving averages (SMA, WMA, TRIMA
| **Tulip** | ✅ | Validated against WMA (using WMA kernel). |
| **Ooples** | ✅ | Validated against WMA (using WMA kernel). |
### C# Implementation Considerations
The QuanTAlib CONV implementation optimizes convolution through pre-allocation and SIMD-accelerated dot products:
**Defensive Kernel Copy**
```csharp
_kernel = new double[_period];
Array.Copy(kernel, _kernel, _period);
```
The kernel is copied to prevent external mutation. This one-time allocation at construction ensures the indicator owns its weight array.
**State Record Struct**
```csharp
[StructLayout(LayoutKind.Auto)]
private record struct State(double LastValidValue);
private State _state;
private State _p_state;
```
Minimal state (just last valid value) enables efficient bar correction via `_p_state` snapshot/restore.
**Circular Buffer Dot Product**
```csharp
int head = _buffer.StartIndex;
int part1Len = _period - head;
result = internalBuf.Slice(head, part1Len).DotProduct(kernelSpan[..part1Len])
+ internalBuf[..head].DotProduct(kernelSpan[part1Len..]);
```
Full buffer requires two `DotProduct` calls to handle the circular wrap. The `DotProduct` extension leverages AVX2/FMA intrinsics when available.
**Stackalloc for Batch Processing**
```csharp
Span<double> window = period <= 256 ? stackalloc double[period] : new double[period];
```
Small kernels (≤256 elements) use stack allocation to avoid heap pressure during batch operations.
**Pre-sized Output Collections**
```csharp
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
```
Batch processing pre-sizes lists to avoid reallocation during population.
**Memory Layout**
| Field | Type | Size | Notes |
|:------|:-----|-----:|:------|
| `_period` | int | 4B | Kernel length |
| `_kernel` | double[] | 8B + N×8B | Weight array reference + data |
| `_buffer` | RingBuffer | ~40B + N×8B | Circular data buffer |
| `_state` | State | 8B | Current last valid value |
| `_p_state` | State | 8B | Previous state for rollback |
| **Total** | | ~68B + 2N×8B | Plus object overhead |
For a typical 14-period kernel: ~68 + 224 ≈ **292 bytes** per instance.
### Common Pitfalls
1. **Kernel Direction**: Our implementation applies the kernel such that the last element of the kernel multiplies the most recent data point. If you import kernels from other DSP libraries, you might need to reverse them.
-128
View File
@@ -94,134 +94,6 @@ Validated against chained WMA implementations in standard libraries.
| **Tulip** | ✅ | Validated against chained `wma`. |
| **Ooples** | ✅ | Validated against chained `CalculateWeightedMovingAverage`. |
### C# Implementation Considerations
The QuanTAlib DWMA implementation leverages composition by chaining two WMA instances, inheriting their O(1) streaming performance:
#### Composition Pattern
DWMA delegates all calculation to two internal WMA instances:
```csharp
[SkipLocalsInit]
public sealed class Dwma : AbstractBase
{
private readonly int _period;
private readonly Wma _wma1;
private readonly Wma _wma2;
public Dwma(int period)
{
_wma1 = new Wma(period);
_wma2 = new Wma(period);
WarmupPeriod = (period * 2) - 1; // Cumulative warmup
}
}
```
#### Minimal Update Logic
The streaming update is extremely simple - just two WMA calls:
```csharp
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override TValue Update(TValue input, bool isNew = true)
{
if (isNew) _sampleCount++;
TValue wma1Result = _wma1.Update(input, isNew);
Last = _wma2.Update(wma1Result, isNew);
PubEvent(Last, isNew);
return Last;
}
```
This design automatically inherits WMA's bar correction capability - when `isNew=false` is passed, both internal WMAs correctly roll back their state.
#### ArrayPool for Batch Intermediate Buffer
The static `Calculate` method uses a temporary buffer for the intermediate WMA result:
```csharp
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
{
int len = source.Length;
double[]? tempArray = len > 1024 ? ArrayPool<double>.Shared.Rent(len) : null;
Span<double> temp = len <= 1024
? stackalloc double[len]
: tempArray!.AsSpan(0, len);
try
{
Wma.Batch(source, temp, period); // First pass
Wma.Batch(temp, output, period); // Second pass
}
finally
{
if (tempArray != null) ArrayPool<double>.Shared.Return(tempArray);
}
}
```
The threshold (1024) is chosen to balance stack safety vs. allocation overhead.
#### State Restoration After Batch
Batch processing restores streaming state by replaying recent bars:
```csharp
public override TSeries Update(TSeries source)
{
// Batch calculate
Calculate(source.Values, vSpan, _period);
// Reset internal state
Reset();
// Replay recent bars to restore streaming state
int lookback = WarmupPeriod + 10;
int startIndex = Math.Max(0, len - lookback);
for (int i = startIndex; i < len; i++)
{
Update(new TValue(source.Times[i], source.Values[i]));
}
_sampleCount = len;
return new TSeries(t, v);
}
```
#### Disposal Pattern
Event subscription is properly cleaned up on disposal:
```csharp
protected override void Dispose(bool disposing)
{
if (disposing && _source != null && _handler != null)
{
_source.Pub -= _handler;
}
base.Dispose(disposing);
}
```
#### Memory Layout
| Field | Type | Size | Purpose |
| :--- | :--- | :---: | :--- |
| `_period` | `int` | 4 | Window size |
| `_wma1` | `Wma` | 8 (ref) | First WMA stage |
| `_wma2` | `Wma` | 8 (ref) | Second WMA stage |
| `_source` | `ITValuePublisher?` | 8 (ref) | Event source |
| `_handler` | `TValuePublishedHandler?` | 8 (ref) | Event handler |
| `_sampleCount` | `int` | 4 | Sample counter |
| **Total** | | **~40 bytes** | Per instance (excludes WMA internals) |
**Total with WMA internals:** Each WMA instance adds ~48 bytes (see WMA docs), so total is ~136 bytes.
### Common Pitfalls
1. **Lag**: This indicator lags. A lot. Do not use it for entry signals on tight timeframes. Use it for trend filtering (e.g., "only buy if price > DWMA").
-165
View File
@@ -199,171 +199,6 @@ QuanTAlib validates GWMA against its mathematical definition and internal consis
| **Tulip** | ❌ | Not included. |
| **Ooples** | ❌ | Not included. |
### C# Implementation Considerations
The QuanTAlib GWMA implementation optimizes for streaming throughput with precomputed weights and careful state management:
#### Precomputed Weights with Inverse Sum
Gaussian weights and the inverse of their sum are calculated once in the constructor:
```csharp
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ComputeWeights(Span<double> weights, int period, double sigma, out double invWeightSum)
{
double center = (period - 1) / 2.0;
double invSigmaP = 1.0 / (sigma * period);
double sum = 0;
for (int i = 0; i < period; i++)
{
double x = (i - center) * invSigmaP;
double w = Math.Exp(-0.5 * x * x);
weights[i] = w;
sum += w;
}
invWeightSum = 1.0 / sum; // Precompute inverse for multiplication
}
```
#### State Record Struct with Auto Layout
State uses `LayoutKind.Auto` for compiler-optimized field arrangement:
```csharp
[StructLayout(LayoutKind.Auto)]
private record struct State
{
public double LastValidValue;
public bool IsInitialized;
}
private State _state;
private State _p_state; // Previous state for bar correction
```
#### FusedMultiplyAdd in Warmup Path
The warmup calculation uses FMA for efficient weighted sum accumulation:
```csharp
private static double CalculateWeightedSumWarmup(ReadOnlySpan<double> window, int p, double sigma, double fallbackValue)
{
double center = (p - 1) * 0.5;
double invSigmaP = 1.0 / (sigma * p);
double sum = 0.0;
double wSum = 0.0;
for (int i = 0; i < p; i++)
{
double x = (i - center) * invSigmaP;
double w = Math.Exp(-0.5 * x * x);
sum = Math.FusedMultiplyAdd(window[i], w, sum); // sum += window[i] * w
wSum += w;
}
return wSum > 0.0 ? sum / wSum : fallbackValue;
}
```
#### Optimized Circular Buffer DotProduct
The hot path handles ring buffer wraparound with two slice dot products:
```csharp
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double CalculateWeightedSum(double fallbackValue)
{
if (_invWeightSum == 0.0) return fallbackValue;
ReadOnlySpan<double> internalBuf = _buffer.InternalBuffer;
int head = _buffer.StartIndex;
int part1Len = _period - head;
double sum1 = internalBuf.Slice(head, part1Len).DotProduct(_weights.AsSpan(0, part1Len));
double sum2 = internalBuf[..head].DotProduct(_weights.AsSpan(part1Len));
return (sum1 + sum2) * _invWeightSum;
}
```
#### ArrayPool for Large Periods in Batch Mode
The static `Calculate` method uses ArrayPool for periods >256:
```csharp
double[]? weightsArray = period > 256 ? ArrayPool<double>.Shared.Rent(period) : null;
Span<double> weights = period <= 256
? stackalloc double[period]
: weightsArray!.AsSpan(0, period);
double[]? ringArray = period > 256 ? ArrayPool<double>.Shared.Rent(period) : null;
Span<double> ring = period <= 256
? stackalloc double[period]
: ringArray!.AsSpan(0, period);
try
{
// Processing loop...
}
finally
{
if (weightsArray != null) ArrayPool<double>.Shared.Return(weightsArray);
if (ringArray != null) ArrayPool<double>.Shared.Return(ringArray);
}
```
#### State Restoration After Batch
Batch processing restores streaming state by seeding last valid value and replaying:
```csharp
public override TSeries Update(TSeries source)
{
Calculate(source.Values, vSpan, _period, _sigma);
// Restore internal state
_buffer.Clear();
int windowSize = Math.Min(len, _period);
int startIndex = len - windowSize;
// Seed last valid value from history before replay window
_state = default;
if (startIndex > 0)
{
for (int i = startIndex - 1; i >= 0; i--)
{
if (double.IsFinite(source.Values[i]))
{
_state.LastValidValue = source.Values[i];
_state.IsInitialized = true;
break;
}
}
}
// Replay to rebuild buffer state
for (int i = startIndex; i < len; i++)
{
Update(source[i], isNew: true, publish: false);
}
return new TSeries(t, v);
}
```
#### Memory Layout
| Field | Type | Size | Purpose |
| :--- | :--- | :---: | :--- |
| `_period` | `int` | 4 | Window length |
| `_sigma` | `double` | 8 | Gaussian width parameter |
| `_weights` | `double[]` | 8 (ref) | Precomputed Gaussian weights |
| `_invWeightSum` | `double` | 8 | Inverse of weight sum |
| `_buffer` | `RingBuffer` | 8 (ref) | Circular price storage |
| `_state` | `State` | 16 | Current state (LastValidValue, IsInitialized) |
| `_p_state` | `State` | 16 | Previous state for rollback |
| **Total** | | **~68 bytes** | Per instance (excluding buffer/array internals) |
**Weight array storage:** `period × 8` bytes (e.g., 160 bytes for period=20)
## Common Pitfalls
1. **Sigma Extremes**:
-70
View File
@@ -184,76 +184,6 @@ QuanTAlib validates HAMMA against its mathematical definition and internal consi
| **Tulip** | ❌ | Not included. |
| **Ooples** | ❌ | Not included. |
### C# Implementation Considerations
The QuanTAlib HAMMA implementation optimizes Hamming window convolution through precomputation and SIMD-accelerated dot products:
**Precomputed Weights with Inverse Sum**
```csharp
ComputeWeights(_weights, period, out _invWeightSum);
// ...
double twoPiOverPm1 = 2.0 * Math.PI / (period - 1);
for (int i = 0; i < period; i++)
{
double w = 0.54 - 0.46 * Math.Cos(twoPiOverPm1 * i);
weights[i] = w;
sum += w;
}
invWeightSum = 1.0 / sum;
```
Trigonometric operations computed once at construction. Normalization uses multiplication by precomputed inverse rather than division per tick.
**State Record Struct**
```csharp
[StructLayout(LayoutKind.Auto)]
private record struct State(double LastValidValue, bool IsInitialized);
private State _state;
private State _p_state;
```
Compiler optimizes field layout. The `IsInitialized` flag tracks whether valid data has been seen for proper NaN handling.
**SIMD-Accelerated Circular Buffer Dot Product**
```csharp
int part1Len = _period - head;
double sum1 = internalBuf.Slice(head, part1Len).DotProduct(_weights.AsSpan(0, part1Len));
double sum2 = internalBuf[..head].DotProduct(_weights.AsSpan(part1Len));
return (sum1 + sum2) * _invWeightSum;
```
Full buffer splits into two `DotProduct` calls to handle circular wrap. The extension leverages AVX2/FMA intrinsics when available.
**Dual Allocation Strategy for Batch**
```csharp
double[]? weightsArray = period > 256 ? ArrayPool<double>.Shared.Rent(period) : null;
Span<double> weights = period <= 256
? stackalloc double[period]
: weightsArray!.AsSpan(0, period);
```
Small periods use stack allocation; large periods use `ArrayPool` to avoid heap pressure while respecting stack limits.
**Incremental Weight Sum During Warmup**
```csharp
if (count < period)
{
count++;
currentWeightSum += weights[period - count];
}
```
Partial buffer normalization accumulates weight sum incrementally rather than recalculating each tick.
**Memory Layout**
| Field | Type | Size | Notes |
|:------|:-----|-----:|:------|
| `_period` | int | 4B | Window length |
| `_weights` | double[] | 8B + L×8B | Hamming coefficients |
| `_invWeightSum` | double | 8B | Precomputed 1/Σw |
| `_buffer` | RingBuffer | ~40B + L×8B | Circular data buffer |
| `_state` | State | 16B | Last valid + initialized flag |
| `_p_state` | State | 16B | Previous state for rollback |
| **Total** | | ~92B + 2L×8B | Plus object overhead |
For a typical 14-period: ~92 + 224 ≈ **316 bytes** per instance.
## Common Pitfalls
1. **Confusing Hamming and Hanning**: Hamming uses 0.54/0.46 coefficients with edge weights of 0.08. Hanning uses 0.5/0.5 with edge weights of 0.0. They're different windows with different properties.
-72
View File
@@ -185,78 +185,6 @@ QuanTAlib validates HANMA against its mathematical definition and internal consi
| **Tulip** | ❌ | Not included. |
| **Ooples** | ❌ | Not included. |
### C# Implementation Considerations
The QuanTAlib HANMA implementation optimizes Hanning window convolution through precomputation and SIMD-accelerated dot products:
**Precomputed Weights with Inverse Sum**
```csharp
ComputeWeights(_weights, period, out _invWeightSum);
// ...
double twoPiOverPm1 = 2.0 * Math.PI / (period - 1);
for (int i = 0; i < period; i++)
{
double w = 0.5 * (1.0 - Math.Cos(twoPiOverPm1 * i));
weights[i] = w;
sum += w;
}
invWeightSum = 1.0 / sum;
```
Trigonometric operations computed once at construction. Normalization uses multiplication by precomputed inverse rather than division per tick.
**State Record Struct**
```csharp
[StructLayout(LayoutKind.Auto)]
private record struct State(double LastValidValue, bool IsInitialized);
private State _state;
private State _p_state;
```
Compiler optimizes field layout. The `IsInitialized` flag tracks whether valid data has been seen for proper NaN handling.
**Zero-Weight Edge Case Handling**
```csharp
if (wSum <= 0)
{
double avg = 0;
for (int i = 0; i < count; i++)
avg += bufferSpan[i];
return avg / count;
}
```
Hanning's zero edge weights can cause zero weight sum during warmup. Falls back to simple average when weight sum is zero.
**SIMD-Accelerated Circular Buffer Dot Product**
```csharp
int part1Len = _period - head;
double sum1 = internalBuf.Slice(head, part1Len).DotProduct(_weights.AsSpan(0, part1Len));
double sum2 = internalBuf[..head].DotProduct(_weights.AsSpan(part1Len));
return (sum1 + sum2) * _invWeightSum;
```
Full buffer splits into two `DotProduct` calls to handle circular wrap. The extension leverages AVX2/FMA intrinsics when available.
**Dual Allocation Strategy for Batch**
```csharp
double[]? weightsArray = period > 256 ? ArrayPool<double>.Shared.Rent(period) : null;
Span<double> weights = period <= 256
? stackalloc double[period]
: weightsArray!.AsSpan(0, period);
```
Small periods use stack allocation; large periods use `ArrayPool` to avoid heap pressure while respecting stack limits.
**Memory Layout**
| Field | Type | Size | Notes |
|:------|:-----|-----:|:------|
| `_period` | int | 4B | Window length |
| `_weights` | double[] | 8B + L×8B | Hanning coefficients |
| `_invWeightSum` | double | 8B | Precomputed 1/Σw |
| `_buffer` | RingBuffer | ~40B + L×8B | Circular data buffer |
| `_state` | State | 16B | Last valid + initialized flag |
| `_p_state` | State | 16B | Previous state for rollback |
| **Total** | | ~92B + 2L×8B | Plus object overhead |
For a typical 14-period: ~92 + 224 ≈ **316 bytes** per instance.
## Common Pitfalls
1. **Confusing Hanning and Hamming**: Hanning uses 0.5 coefficient with edge weights of exactly 0.0. Hamming uses 0.54/0.46 with edge weights of 0.08. They're different windows with different properties.
-130
View File
@@ -108,133 +108,3 @@ Discrepancies exist due to different rounding methods for integer periods.
* **Ooples**: Uses `Math.Round` (nearest integer).
This results in different effective periods for $N=14$ ($\sqrt{14} \approx 3.74 \to 3$ vs $4$) and others where the fractional part $\ge 0.5$. Validation tests match exactly for periods where rounding logic aligns (e.g., $N=9, 20, 50$).
## C# Implementation Considerations
### Compositional Architecture
HMA composes three independent `Wma` instances rather than implementing custom logic:
```csharp
_wmaFull = new Wma(period);
_wmaHalf = new Wma(halfPeriod);
_wmaSqrt = new Wma(_sqrtPeriod);
```
This leverages WMA's optimized O(1) implementation for each component, maintaining the zero-allocation property.
### Streaming Update Pipeline
The hot path chains the three WMA updates with minimal intermediate allocation:
```csharp
TValue full = _wmaFull.Update(input, isNew);
TValue half = _wmaHalf.Update(input, isNew);
double intermediate = (2.0 * half.Value) - full.Value;
Last = _wmaSqrt.Update(new TValue(input.Time, intermediate), isNew);
```
The intermediate value computation uses scalar arithmetic—no buffer required.
### ArrayPool for Batch Processing
The static `Calculate` method rents arrays from `ArrayPool<double>` for temporary storage:
```csharp
double[] rentedFull = System.Buffers.ArrayPool<double>.Shared.Rent(len);
double[] rentedHalf = System.Buffers.ArrayPool<double>.Shared.Rent(len);
try
{
Wma.Batch(source, fullWma, period);
Wma.Batch(source, halfWma, halfPeriod);
CalculateIntermediate(halfWma, fullWma, intermediate);
Wma.Batch(intermediate, output, sqrtPeriod);
}
finally
{
System.Buffers.ArrayPool<double>.Shared.Return(rentedFull);
System.Buffers.ArrayPool<double>.Shared.Return(rentedHalf);
}
```
Buffer reuse: the `halfWma` array doubles as the `intermediate` buffer since values are consumed before being overwritten.
### SIMD-Accelerated Intermediate Calculation
The combiner step $2 \times \text{WMA}_{half} - \text{WMA}_{full}$ is fully vectorized:
```csharp
if (Avx512F.IsSupported && len >= Vector512<double>.Count)
{
var vTwo = Vector512.Create(2.0);
var vResult = Avx512F.Subtract(Avx512F.Multiply(vHalf, vTwo), vFull);
}
else if (Avx2.IsSupported && len >= Vector256<double>.Count)
{
var vTwo = Vector256.Create(2.0);
var vResult = Avx.Subtract(Avx.Multiply(vHalf, vTwo), vFull);
}
else if (AdvSimd.Arm64.IsSupported && len >= Vector128<double>.Count)
{
var vTwo = Vector128.Create(2.0);
var vResult = AdvSimd.Arm64.Subtract(AdvSimd.Arm64.Multiply(vHalf, vTwo), vFull);
}
```
This achieves 8× throughput on AVX-512, 4× on AVX2, and 2× on NEON.
### Unsafe Memory Access
Direct memory references eliminate bounds checking in the SIMD loops:
```csharp
ref double halfRef = ref MemoryMarshal.GetReference(halfWma);
ref double fullRef = ref MemoryMarshal.GetReference(fullWma);
ref double outRef = ref MemoryMarshal.GetReference(output);
var vHalf = Vector512.LoadUnsafe(ref Unsafe.Add(ref halfRef, i));
```
### State Replay for TSeries Update
After batch calculation, streaming state is restored by replaying the trailing window:
```csharp
int lookback = _period + _sqrtPeriod + 10;
int startIndex = Math.Max(0, len - lookback);
for (int i = startIndex; i < len; i++)
{
Update(new TValue(source.Times[i], source.Values[i]));
}
```
This ensures subsequent streaming updates produce correct results after a batch operation.
### Integer Period Truncation
Periods use integer truncation (not rounding) for consistent behavior:
```csharp
int halfPeriod = period / 2;
_sqrtPeriod = (int)Math.Sqrt(period);
```
This differs from some implementations that use `Math.Round`, affecting results for certain periods.
### Memory Layout
| Component | Size | Purpose |
| :--- | :--- | :--- |
| `_wmaFull` | ~152 + 8×period bytes | Full-period WMA |
| `_wmaHalf` | ~152 + 4×period bytes | Half-period WMA |
| `_wmaSqrt` | ~152 + 8×√period bytes | Smoothing WMA |
| Scalars | ~32 bytes | Period values, sample count |
| **Total** | **~488 + 12N + 8√N bytes** | Per-instance footprint |
For HMA(100), total memory is approximately 1.7 KB per instance (three WMA instances combined).
### Common Pitfalls
1. **Overshoot**: Like DEMA, HMA can overshoot price turns because of the lag correction.
2. **Period Sensitivity**: The $\sqrt{N}$ smoothing is hardcoded into the definition. You can't easily tweak the smoothing independently of the lag correction without breaking the "Hull" definition.
3. **Integer Math**: The periods $N/2$ and $\sqrt{N}$ are rounded to integers. This can cause slight discrepancies between implementations depending on rounding rules. Standard integer truncation is used in QuanTAlib.
-131
View File
@@ -109,137 +109,6 @@ Validated against Skender.
| **Tulip** | N/A | Not implemented. |
| **Ooples** | N/A | Not implemented. |
### C# Implementation Considerations
The QuanTAlib LSMA implementation achieves O(1) streaming updates through running sum maintenance with several optimizations:
#### O(1) Running Sum Algorithm
The implementation maintains two running sums (`SumY`, `SumXY`) that enable constant-time updates instead of O(N) recalculation:
```csharp
// O(1) update for sum_xy: sum_xy_new = sum_xy_old + sum_y_prev - n * oldest
_state.SumXY = Math.FusedMultiplyAdd(-_period, oldest, _state.SumXY + prev_sum_y);
// O(1) update for sum_y
_state.SumY = _state.SumY - oldest + val;
```
#### Precomputed Constants
Mathematical constants are computed once in the constructor to avoid redundant calculations:
```csharp
// sum_x = 0 + 1 + ... + (n-1) = n(n-1)/2
_sum_x = 0.5 * period * (period - 1);
// sum_x2 = 0² + ... + (n-1)² = (n-1)n(2n-1)/6
double sum_x2 = (period - 1.0) * period * (2.0 * period - 1.0) / 6.0;
// denominator = n * sum_x2 - sum_x²
_denominator = period * sum_x2 - _sum_x * _sum_x;
```
#### State Record Struct
State uses `LayoutKind.Auto` for compiler-optimized field ordering:
```csharp
[StructLayout(LayoutKind.Auto)]
private record struct State(double SumY, double SumXY, double LastVal, double LastValidValue);
private State _state;
private State _p_state; // Previous state for bar correction
```
#### FusedMultiplyAdd Usage
FMA is used extensively for slope, intercept, and endpoint calculations:
```csharp
double m = Math.FusedMultiplyAdd(n, _state.SumXY, -sx * _state.SumY) / denom;
double b = Math.FusedMultiplyAdd(-m, sx, _state.SumY) / n;
result = Math.FusedMultiplyAdd(-m, _offset, b);
```
#### Periodic Resync
Running sums accumulate floating-point drift; periodic resync every 1000 ticks corrects this:
```csharp
private const int ResyncInterval = 1000;
private void Resync()
{
_state.SumY = _buffer.Sum;
_state.SumXY = 0;
var span = _buffer.GetSpan();
for (int i = 0; i < span.Length; i++)
{
int x = span.Length - 1 - i;
_state.SumXY = Math.FusedMultiplyAdd(x, span[i], _state.SumXY);
}
}
```
#### Stackalloc/ArrayPool Strategy
The static `Calculate` method uses stackalloc for small periods (≤256) to avoid heap allocation:
```csharp
const int StackAllocThreshold = 256;
Span<double> buffer = period <= StackAllocThreshold
? stackalloc double[period]
: new double[period];
```
#### Thread-Safe Disposal
Disposal uses atomic operations for idempotent, thread-safe cleanup:
```csharp
protected override void Dispose(bool disposing)
{
if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0 && _source != null)
{
_source.Pub -= _handler;
_source = null;
}
base.Dispose(disposing);
}
```
#### NaN Handling
Invalid values are replaced with the last valid value to maintain calculation integrity:
```csharp
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private double GetValidValue(double input)
{
if (double.IsFinite(input))
{
_state.LastValidValue = input;
return input;
}
return _state.LastValidValue;
}
```
#### Memory Layout
| Field | Type | Size | Purpose |
| :--- | :--- | :---: | :--- |
| `_period` | `int` | 4 | Lookback window |
| `_offset` | `int` | 4 | Forecast offset |
| `_buffer` | `RingBuffer` | 8 (ref) | Circular storage |
| `_sum_x` | `double` | 8 | Precomputed Σx |
| `_denominator` | `double` | 8 | Precomputed denominator |
| `_state` | `State` | 32 | Current state (SumY, SumXY, LastVal, LastValidValue) |
| `_p_state` | `State` | 32 | Previous state for rollback |
| `_tickCount` | `int` | 4 | Resync counter |
| `_disposed` | `int` | 4 | Atomic disposal flag |
| **Total** | | **~104 bytes** | Per instance (excluding RingBuffer internal storage) |
### Common Pitfalls
1. **Overshoot**: Because it projects a trend, LSMA will overshoot significantly when the trend reverses. It assumes the trend continues.
-92
View File
@@ -97,95 +97,3 @@ Validated against Ooples.
| **TA-Lib** | N/A | Not implemented |
| **Tulip** | N/A | Not implemented. |
## C# Implementation Considerations
QuanTAlib's PWMA uses triple cascading sums to achieve O(1) streaming updates. The implementation demonstrates several high-performance patterns:
### State Management
```csharp
[StructLayout(LayoutKind.Auto)]
private record struct State(
double Sum, // Running sum (S1)
double WSum, // Weighted sum (S2)
double PSum, // Parabolic sum (S3)
double LastInput,
double LastValidValue,
int TickCount)
```
The state captures all three cascading sums needed for the O(1) update formula. `TickCount` tracks iterations for periodic resync.
### Key Optimizations
| Technique | Implementation | Benefit |
| :--- | :--- | :--- |
| **Precomputed divisor** | `_divisor = period * (period + 1.0) * (2.0 * period + 1.0) / 6.0` | Eliminates division in hot path |
| **FMA cascade** | All three sum updates use `FusedMultiplyAdd` | Hardware-accelerated multiply-add |
| **Dual buffer** | `_buffer` + `_p_buffer` for bar correction | O(1) state restoration on `isNew=false` |
| **Periodic resync** | Full recalculation every 1000 ticks | Bounds floating-point drift |
| **stackalloc** | Batch `Calculate` uses stack for period ≤ 512 | Zero heap allocation |
### FMA in Cascade Updates
The cascading sum formulas map directly to FMA operations:
```csharp
// S1 update (simple running sum)
double newSum = _state.Sum - oldest + newest;
// S2 update: S2_new = S2_old - S1_old + N×newest
double newWSum = Math.FusedMultiplyAdd(period, newest, _state.WSum - _state.Sum);
// S3 update: S3_new = S3_old - 2×S2_old + S1_old + N²×newest
double newPSum = Math.FusedMultiplyAdd(period * period, newest,
_state.PSum - 2 * oldWSum + oldSum);
```
### Memory Layout
| Field | Type | Size | Purpose |
| :--- | :--- | :---: | :--- |
| `Sum` | double | 8 bytes | Running sum S1 |
| `WSum` | double | 8 bytes | Weighted sum S2 |
| `PSum` | double | 8 bytes | Parabolic sum S3 |
| `LastInput` | double | 8 bytes | Previous input value |
| `LastValidValue` | double | 8 bytes | NaN substitution |
| `TickCount` | int | 4 bytes | Resync counter |
| **State total** | | **44 bytes** | Compiler-aligned |
| `_buffer` | RingBuffer | 24 + 8N | Sliding window |
| `_p_buffer` | RingBuffer | 24 + 8N | Bar correction backup |
### Bar Correction Pattern
```csharp
if (isNew)
{
_p_state = _state; // Snapshot for rollback
_p_buffer.CopyFrom(_buffer); // Buffer snapshot
}
else
{
_state = _p_state; // Restore previous state
_buffer.CopyFrom(_p_buffer); // Restore buffer
}
```
### Periodic Resync
Triple cascading sums accumulate floating-point errors faster than simple running sums. The implementation resyncs every 1000 ticks:
```csharp
if (_state.TickCount >= 1000)
{
// Full O(N) recalculation to reset drift
RecalculateFromBuffer();
_state = _state with { TickCount = 0 };
}
```
### Common Pitfalls
1. **Resync**: Because triple running sums are used, floating-point errors can accumulate faster than in a simple SMA. The implementation automatically resyncs every 1000 ticks to maintain precision.
2. **Sensitivity**: This indicator is very sensitive to the most recent bar. It can "repaint" visually if used on an open bar (though the math is consistent).
-109
View File
@@ -183,115 +183,6 @@ QuanTAlib validates SGMA against mathematical properties rather than external li
| **Tulip** | ❌ | Not included. |
| **Ooples** | ❌ | Not included. |
## C# Implementation Considerations
QuanTAlib's SGMA uses precomputed weights with SIMD-accelerated dot products for efficient O(N) convolution. The implementation demonstrates several high-performance patterns:
### Weight Precomputation
```csharp
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ComputeWeights(Span<double> weights, int period, int degree, out double invWeightSum)
{
// Hardcoded weights for common periods (degree 2)
if (degree == 2 && period == 9)
{
weights[0] = -0.0281; weights[1] = 0.0337; // ... etc
invWeightSum = 1.0 / sum;
return;
}
// General computation for other cases
}
```
Weights are computed once at construction. The inverse weight sum (`invWeightSum`) replaces division with multiplication in the hot path.
### Key Optimizations
| Technique | Implementation | Benefit |
| :--- | :--- | :--- |
| **Precomputed weights** | `_weights[]` array + `_invWeightSum` | One-time cost at construction |
| **Hardcoded common cases** | Periods 5, 7, 9 with degree 2 | Bypasses general weight loop |
| **SIMD dot product** | `span.DotProduct(weights)` extension | AVX2-accelerated convolution |
| **Inverse multiplication** | `sum * invWeightSum` vs `sum / weightSum` | 15→3 cycles per division |
| **ArrayPool hybrid** | stackalloc ≤256, ArrayPool >256 | Zero allocation for typical periods |
| **FMA chains** | Warmup uses nested `FusedMultiplyAdd` | Hardware-accelerated accumulation |
### SIMD Convolution
The hot path uses split dot products to handle ring buffer wraparound:
```csharp
int part1Len = period - head;
double sum = internalBuf.Slice(head, part1Len).DotProduct(weights.AsSpan(0, part1Len))
+ internalBuf[..head].DotProduct(weights.AsSpan(part1Len));
return sum * invWeightSum;
```
The `DotProduct` extension method uses AVX2/AVX-512 intrinsics when available.
### FMA Chains in Warmup
For warmup periods with known sizes, nested FMA reduces instruction count:
```csharp
// Period 5, degree 2 warmup
double sum = Math.FusedMultiplyAdd(window[0], w0,
Math.FusedMultiplyAdd(window[1], w1,
Math.FusedMultiplyAdd(window[2], w2,
Math.FusedMultiplyAdd(window[3], w3, window[4] * w4))));
```
### Memory Layout
| Field | Type | Size | Purpose |
| :--- | :--- | :---: | :--- |
| `_period` | int | 4 bytes | Adjusted to odd |
| `_degree` | int | 4 bytes | Polynomial degree (0-4) |
| `_weights` | double[] | 8N bytes | Precomputed weight vector |
| `_invWeightSum` | double | 8 bytes | Cached 1/Σw |
| `_buffer` | RingBuffer | 24 + 8N | Sliding window |
| `_lastValidValue` | double | 8 bytes | NaN substitution |
| `_p_lastValidValue` | double | 8 bytes | Bar correction backup |
| **Instance total** | | **~56 + 16N bytes** | N = period |
### Batch Processing Memory Strategy
```csharp
// Threshold: stackalloc for small, ArrayPool for large
double[]? weightsArray = usePeriod > 256 ? ArrayPool<double>.Shared.Rent(usePeriod) : null;
Span<double> weights = usePeriod <= 256
? stackalloc double[usePeriod]
: weightsArray!.AsSpan(0, usePeriod);
try
{
// Process all bars
}
finally
{
if (weightsArray != null) ArrayPool<double>.Shared.Return(weightsArray);
}
```
### Bar Correction Pattern
SGMA uses simple scalar state backup (no buffer duplication needed since RingBuffer handles `UpdateNewest`):
```csharp
if (isNew)
{
_p_lastValidValue = _lastValidValue;
_buffer.Add(val);
}
else
{
_lastValidValue = _p_lastValidValue;
_buffer.UpdateNewest(val);
}
```
## Common Pitfalls
1. **Degree 0 Misuse**: If you want SMA, use SMA. Degree 0 SGMA is mathematically equivalent but wastes cycles on weight calculation.
-109
View File
@@ -113,115 +113,6 @@ Validation tests verify:
- Output bounded by input range
- Warmup weight adaptation
## C# Implementation Considerations
QuanTAlib's SINEMA uses precomputed sine weights with O(N) convolution. The implementation demonstrates several high-performance patterns:
### State Management
```csharp
[StructLayout(LayoutKind.Auto)]
private record struct State(double LastValidValue);
private State _state;
private State _p_state;
```
Minimal state—only the last valid value needs tracking since weights are precomputed and buffer handles windowing.
### Key Optimizations
| Technique | Implementation | Benefit |
| :--- | :--- | :--- |
| **Precomputed weights** | `_weights[]` array at construction | Eliminates `sin()` calls in hot path |
| **Cached weight sum** | `_weightSum` stored at construction | Division uses constant denominator |
| **ArrayPool hybrid** | stackalloc ≤256, ArrayPool >256 | Zero allocation for typical periods |
| **RingBuffer** | `UpdateNewest` for bar correction | O(1) correction without buffer copy |
| **Adaptive warmup** | Recalculates weights for partial buffer | Valid output from first bar |
### Constructor Weight Precomputation
```csharp
public Sinema(int period)
{
_weights = new double[period];
double sum = 0;
for (int i = 0; i < period; i++)
{
_weights[i] = Math.Sin(Math.PI * (i + 1) / period);
sum += _weights[i];
}
_weightSum = sum;
}
```
All `sin()` calls happen once at construction, not per-update.
### Memory Layout
| Field | Type | Size | Purpose |
| :--- | :--- | :---: | :--- |
| `_period` | int | 4 bytes | Window size |
| `_weights` | double[] | 8N bytes | Precomputed sine weights |
| `_weightSum` | double | 8 bytes | Cached Σw for normalization |
| `_buffer` | RingBuffer | 24 + 8N | Sliding window |
| `_state.LastValidValue` | double | 8 bytes | NaN substitution |
| `_p_state.LastValidValue` | double | 8 bytes | Bar correction backup |
| **Instance total** | | **~52 + 16N bytes** | N = period |
### Bar Correction Pattern
```csharp
if (isNew)
{
_p_state = _state;
_buffer.Add(val);
}
else
{
_state = _p_state;
_buffer.UpdateNewest(val);
}
```
Simple state backup; RingBuffer's `UpdateNewest` handles in-place modification.
### Batch Processing Memory Strategy
```csharp
const int StackAllocThreshold = 256;
double[]? rentedBuffer = period > StackAllocThreshold ? ArrayPool<double>.Shared.Rent(period) : null;
double[]? rentedWeights = period > StackAllocThreshold ? ArrayPool<double>.Shared.Rent(period) : null;
Span<double> buffer = rentedBuffer != null
? rentedBuffer.AsSpan(0, period)
: stackalloc double[period];
try { /* process */ }
finally
{
if (rentedBuffer != null) ArrayPool<double>.Shared.Return(rentedBuffer);
if (rentedWeights != null) ArrayPool<double>.Shared.Return(rentedWeights);
}
```
### Warmup Weight Adaptation
During warmup, weights are dynamically recalculated for the partial buffer:
```csharp
if (count < _period)
{
for (int j = 0; j < count; j++)
{
double w = Math.Sin(Math.PI * (j + 1) / count);
sum += buffer[j] * w;
weightSum += w;
}
}
```
This produces valid, smooth output from bar 1 without waiting for a full window.
## Common Pitfalls
1. **O(N) Complexity**: Unlike SMA's O(1) running sum, SINEMA requires O(N) operations per bar. For very long periods (>500), consider whether the smoothness benefits justify the cost.
-88
View File
@@ -116,91 +116,3 @@ For 512 bars:
| **Skender** | ✅ | Matches `GetSma` exactly. |
| **Tulip** | ✅ | Matches `sma` exactly. |
| **Ooples** | ✅ | Matches `CalculateSimpleMovingAverage`. |
## C# Implementation Considerations
### RingBuffer for O(1) Running Sum
The implementation maintains a `RingBuffer` of the most recent $N$ values alongside a running `Sum`. On each update, the oldest value is subtracted and the newest added—eliminating the need to iterate over the entire window:
```csharp
Sum = Math.FusedMultiplyAdd(-_buffer[^1], 1, Sum + p);
_buffer.Add(p, isNew);
```
Using `FusedMultiplyAdd` for the combined subtraction/addition improves numerical stability compared to separate operations.
### State Record Struct
Minimal state is captured in a `record struct` for efficient bar correction:
```csharp
private record struct State(double Sum, double LastValidValue, int TickCount);
```
When `isNew=false`, the implementation restores `_p_state` to revert any partial calculation—enabling accurate bar correction when the same timestamp updates multiple times.
### Periodic Resync for Drift Correction
Floating-point drift accumulates over millions of additions/subtractions. The implementation resyncs every 1000 ticks:
```csharp
if (_state.TickCount >= ResyncPeriod)
{
_state = _state with { Sum = _buffer.Span.Sum(), TickCount = 0 };
}
```
This bounds cumulative error to within `1e-9` of true mean regardless of stream length.
### Multi-Architecture SIMD Implementation
The static `Calculate` method dispatches to architecture-specific implementations:
```csharp
if (Avx512F.IsSupported) CalculateAvx512Core(source, output, period);
else if (Avx2.IsSupported) CalculateAvx2Core(source, output, period);
else if (AdvSimd.Arm64.IsSupported) CalculateNeonCore(source, output, period);
else CalculateScalarCore(source, output, period);
```
- **AVX-512**: Processes 8 doubles simultaneously with 512-bit vectors
- **AVX2**: Processes 4 doubles with 256-bit vectors
- **NEON (ARM64)**: Processes 2 doubles with 128-bit vectors
- **Scalar fallback**: Portable loop for unsupported architectures
### Prefix-Sum Vectorization
For batch processing, the SIMD paths use a prefix-sum technique that enables parallel computation of running sums. The initial window sum is computed with vectorized horizontal addition, then subsequent values use the optimized running-sum pattern.
### ArrayPool for Memory Efficiency
Large period buffers are rented from `ArrayPool<double>` rather than allocated, reducing GC pressure during batch operations. Combined with `stackalloc` for small intermediate buffers, this achieves zero-allocation in hot paths.
### NaN Handling with Last-Valid Substitution
Non-finite inputs are replaced with the last valid value stored in state:
```csharp
p = double.IsFinite(p) ? p : _state.LastValidValue;
```
This prevents NaN propagation through the running sum without requiring expensive validation on every buffer access.
### Memory Layout
| Component | Size | Purpose |
| :--- | :--- | :--- |
| `_buffer` (RingBuffer) | 32 + 8×period bytes | Sliding window history |
| `_state` | ~24 bytes | Sum, LastValidValue, TickCount |
| `_p_state` | ~24 bytes | Previous state for rollback |
| Scalars | ~16 bytes | Period, reciprocal |
| **Total** | **~96 + 8N bytes** | Per-instance footprint |
For SMA(200), total memory is approximately 1.7 KB per instance.
### Common Pitfalls
1. **Lag**: SMA has the most lag of all moving averages (Lag $\approx N/2$).
2. **Drop-off Effect**: An old, large outlier dropping out of the window causes the SMA to jump, even if the current price is flat. This "Barker effect" is why EMAs are often preferred.
3. **NaN Handling**: A single `NaN` in the history window corrupts the entire SMA. QuanTAlib handles this by substituting the last valid value.

Some files were not shown because too many files have changed in this diff Show More