mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 22:08:05 +00:00
Add eventing support to WMA indicator and implement unit tests for various indicators
- Enhanced WMA indicator with event-driven capabilities using ITValuePublisher interface. - Created a new TODO file listing various indicators and their corresponding libraries. - Added unit tests for DEMA, HMA, TEMA, and WMA indicators to ensure proper functionality. - Implemented tests for handling new bars, ticks, and historical data updates across indicators. - Verified that indicators correctly compute values and handle different source types.
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class WmaCoverageTests
|
||||
{
|
||||
[Fact]
|
||||
public void Wma_ResyncLogic_IsTriggeredAndCorrect()
|
||||
{
|
||||
// ResyncInterval is 1000. We need more than that to trigger it.
|
||||
int count = 2500;
|
||||
int period = 10;
|
||||
var wma = new Wma(period);
|
||||
|
||||
// Use a constant value to make verification easy
|
||||
// WMA of constant X is X
|
||||
double constantValue = 100.0;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
wma.Update(new TValue(DateTime.UtcNow, constantValue));
|
||||
|
||||
// After warmup, value should always be constantValue
|
||||
if (i >= period)
|
||||
{
|
||||
Assert.Equal(constantValue, wma.Last.Value, 1e-9);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_SpanCalc_LargeDataset_TriggersResync()
|
||||
{
|
||||
// ResyncInterval is 1000.
|
||||
int count = 5000;
|
||||
int period = 10;
|
||||
double[] source = new double[count];
|
||||
double[] output = new double[count];
|
||||
|
||||
// Fill with constant value
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
source[i] = 100.0;
|
||||
}
|
||||
|
||||
Wma.Calculate(source.AsSpan(), output.AsSpan(), period);
|
||||
|
||||
// Verify all outputs after warmup are correct
|
||||
for (int i = period; i < count; i++)
|
||||
{
|
||||
Assert.Equal(100.0, output[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_SpanCalc_SimdThreshold_Boundary()
|
||||
{
|
||||
// SimdThreshold is 256.
|
||||
// Test just below and just above to ensure both paths work
|
||||
int[] lengths = { 250, 256, 260 };
|
||||
int period = 10;
|
||||
|
||||
foreach (int len in lengths)
|
||||
{
|
||||
double[] source = new double[len];
|
||||
double[] output = new double[len];
|
||||
|
||||
for (int i = 0; i < len; i++) source[i] = 100.0;
|
||||
|
||||
Wma.Calculate(source.AsSpan(), output.AsSpan(), period);
|
||||
|
||||
Assert.Equal(100.0, output[^1], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_SpanCalc_Simd_WithResync()
|
||||
{
|
||||
// This targets the SIMD loop with resync
|
||||
// Need length > SimdThreshold (256) and enough data to hit ResyncInterval (1000)
|
||||
// But wait, the SIMD loop in CalculateSimdCore handles resync internally.
|
||||
// The loop structure is:
|
||||
// while (idx < simdEnd)
|
||||
// nextSync = Math.Min(simdEnd, idx + ResyncInterval)
|
||||
// ... process blocks ...
|
||||
|
||||
int count = 3000;
|
||||
int period = 5;
|
||||
double[] source = new double[count];
|
||||
double[] output = new double[count];
|
||||
|
||||
// Use a pattern that isn't constant to verify calculation accuracy
|
||||
// Linear increase: 0, 1, 2, ...
|
||||
for (int i = 0; i < count; i++) source[i] = i;
|
||||
|
||||
Wma.Calculate(source.AsSpan(), output.AsSpan(), period);
|
||||
|
||||
// Verify a few points
|
||||
// WMA(5) of x-4, x-3, x-2, x-1, x
|
||||
// = (1*(x-4) + 2*(x-3) + 3*(x-2) + 4*(x-1) + 5*x) / 15
|
||||
// = (x-4 + 2x-6 + 3x-6 + 4x-4 + 5x) / 15
|
||||
// = (15x - 20) / 15
|
||||
// = x - 20/15 = x - 1.333...
|
||||
|
||||
for (int i = period; i < count; i++)
|
||||
{
|
||||
double expected = i - (20.0 / 15.0);
|
||||
Assert.Equal(expected, output[i], 1e-9);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_Update_Resync_WithFloatingPointDrift()
|
||||
{
|
||||
// This test tries to accumulate error and see if resync fixes it (or at least doesn't break it)
|
||||
// It's hard to deterministically cause drift, but we can ensure the code path is executed.
|
||||
int period = 10;
|
||||
var wma = new Wma(period);
|
||||
|
||||
// 1200 updates to trigger resync (at 1000)
|
||||
for (int i = 0; i < 1200; i++)
|
||||
{
|
||||
wma.Update(new TValue(DateTime.UtcNow, 1.0));
|
||||
}
|
||||
|
||||
Assert.Equal(1.0, wma.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_Constructor_ThrowsOnInvalidPeriod()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Wma(0));
|
||||
Assert.Throws<ArgumentException>(() => new Wma(-1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_StaticCalculate_ThrowsOnInvalidArgs()
|
||||
{
|
||||
double[] source = new double[10];
|
||||
double[] output = new double[5]; // Mismatch
|
||||
Assert.Throws<ArgumentException>(() => Wma.Calculate(source.AsSpan(), output.AsSpan(), 5));
|
||||
|
||||
double[] output2 = new double[10];
|
||||
Assert.Throws<ArgumentException>(() => Wma.Calculate(source.AsSpan(), output2.AsSpan(), 0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_Calculate_EmptyInput_DoesNothing()
|
||||
{
|
||||
Wma.Calculate(ReadOnlySpan<double>.Empty, Span<double>.Empty, 5);
|
||||
// Should not throw
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_Update_WithNaN_UsesLastValid()
|
||||
{
|
||||
var wma = new Wma(5);
|
||||
wma.Update(new TValue(DateTime.UtcNow, 1.0));
|
||||
wma.Update(new TValue(DateTime.UtcNow, 2.0));
|
||||
wma.Update(new TValue(DateTime.UtcNow, double.NaN)); // Should use 2.0
|
||||
|
||||
// Buffer: 1, 2, 2
|
||||
// WMA(3) = (1*1 + 2*2 + 3*2) / 6 = (1 + 4 + 6) / 6 = 11/6 = 1.8333...
|
||||
// Wait, period is 5.
|
||||
// Buffer: 1, 2, 2
|
||||
// Sum = 5, WSum = 1*1 + 2*2 + 3*2 = 11
|
||||
// Divisor = 3*4/2 = 6
|
||||
// Result = 11/6
|
||||
|
||||
Assert.Equal(11.0/6.0, wma.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_Update_IsNewFalse_UpdatesLastValue()
|
||||
{
|
||||
var wma = new Wma(3);
|
||||
wma.Update(new TValue(DateTime.UtcNow, 1.0));
|
||||
wma.Update(new TValue(DateTime.UtcNow, 2.0));
|
||||
|
||||
// Update existing with 3.0 (replaces 2.0)
|
||||
wma.Update(new TValue(DateTime.UtcNow, 3.0), isNew: false);
|
||||
|
||||
// Buffer should be: 1, 3
|
||||
// Sum = 4, WSum = 1*1 + 2*3 = 7
|
||||
// Divisor = 2*3/2 = 3
|
||||
// Result = 7/3 = 2.333...
|
||||
|
||||
Assert.Equal(7.0/3.0, wma.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_TSeries_Empty_ReturnsEmpty()
|
||||
{
|
||||
var wma = new Wma(5);
|
||||
var result = wma.Update(new TSeries());
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_TSeries_WithNaN_RestoresStateCorrectly()
|
||||
{
|
||||
// This tests the state restoration logic in Update(TSeries)
|
||||
// specifically the loop that looks for _lastValidValue
|
||||
var wma = new Wma(3);
|
||||
var series = new TSeries();
|
||||
series.Add(new TValue(DateTime.UtcNow, 1.0));
|
||||
series.Add(new TValue(DateTime.UtcNow, 2.0));
|
||||
series.Add(new TValue(DateTime.UtcNow, double.NaN));
|
||||
series.Add(new TValue(DateTime.UtcNow, 4.0));
|
||||
|
||||
wma.Update(series);
|
||||
|
||||
// After processing series, internal state should match having processed these sequentially
|
||||
// Last value was 4.0. Previous valid was 2.0 (since NaN used 2.0).
|
||||
// Buffer: 2.0, 2.0 (from NaN), 4.0
|
||||
|
||||
// Let's add one more value to verify state is correct
|
||||
wma.Update(new TValue(DateTime.UtcNow, 5.0));
|
||||
|
||||
// Buffer: 2.0, 4.0, 5.0
|
||||
// WMA(3) = (1*2 + 2*4 + 3*5) / 6 = (2 + 8 + 15) / 6 = 25/6 = 4.1666...
|
||||
|
||||
Assert.Equal(25.0/6.0, wma.Last.Value, 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_Reset_ClearsState()
|
||||
{
|
||||
var wma = new Wma(3);
|
||||
wma.Update(new TValue(DateTime.UtcNow, 1.0));
|
||||
wma.Update(new TValue(DateTime.UtcNow, 2.0));
|
||||
wma.Update(new TValue(DateTime.UtcNow, 3.0));
|
||||
|
||||
wma.Reset();
|
||||
|
||||
Assert.Equal(0, wma.Last.Value);
|
||||
|
||||
// Start fresh
|
||||
wma.Update(new TValue(DateTime.UtcNow, 10.0));
|
||||
// Buffer: 10
|
||||
// WMA = 10
|
||||
Assert.Equal(10.0, wma.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_Calculate_ScalarFallback_WithNaN()
|
||||
{
|
||||
// Force scalar path by including NaN, even with large dataset
|
||||
int count = 1000;
|
||||
double[] source = new double[count];
|
||||
double[] output = new double[count];
|
||||
|
||||
for (int i = 0; i < count; i++) source[i] = 1.0;
|
||||
source[500] = double.NaN; // This should trigger HasNonFiniteValues -> true
|
||||
|
||||
Wma.Calculate(source.AsSpan(), output.AsSpan(), 10);
|
||||
|
||||
// Check around the NaN
|
||||
// Index 500 is NaN, so it uses previous valid (1.0)
|
||||
// So effectively the stream is all 1.0s
|
||||
Assert.Equal(1.0, output[500], 1e-9);
|
||||
Assert.Equal(1.0, output[501], 1e-9);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Wma_Constructor_WithSource_Subscribes()
|
||||
{
|
||||
var source = new Wma(10); // Just using Wma as a publisher
|
||||
var wma = new Wma(source, 5);
|
||||
|
||||
source.Update(new TValue(DateTime.UtcNow, 10.0));
|
||||
|
||||
Assert.Equal(10.0, wma.Last.Value);
|
||||
}
|
||||
}
|
||||
@@ -1,323 +0,0 @@
|
||||
#!meta
|
||||
|
||||
{"kernelInfo":{"defaultKernelName":"csharp","items":[{"name":"csharp"},{"name":"fsharp","languageName":"F#","aliases":["f#","fs"]},{"name":"html","languageName":"HTML"},{"name":"http","languageName":"HTTP"},{"name":"javascript","languageName":"JavaScript","aliases":["js"]},{"name":"mermaid","languageName":"Mermaid"},{"name":"pwsh","languageName":"PowerShell","aliases":["powershell"]},{"name":"value"}]}}
|
||||
|
||||
#!markdown
|
||||
|
||||
# Weighted Moving Average (WMA) Examples
|
||||
|
||||
This is a **.NET Interactive** notebook. To run it, you need the [Polyglot Notebooks](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.dotnet-interactive-vscode) extension installed in VS Code.
|
||||
|
||||
The **Weighted Moving Average (WMA)** applies linear weighting to price data, giving more weight to recent values. Unlike SMA which treats all values equally, WMA assigns weight `n` to the newest value, `n-1` to the second newest, and so on down to weight `1` for the oldest.
|
||||
|
||||
**Key characteristics:**
|
||||
- Linear weighting: newest gets weight n, oldest gets weight 1
|
||||
- O(1) update complexity using dual running sums
|
||||
- O(1) bar correction using scalar state
|
||||
- More responsive than SMA, smoother transitions than EMA
|
||||
- Reduced lag compared to SMA
|
||||
|
||||
This notebook demonstrates:
|
||||
1. **Manual Data Processing**: Understanding Batch vs. Streaming modes.
|
||||
2. **Streaming with `isNew`**: Handling intra-bar updates.
|
||||
3. **Large Dataset Processing**: Using Geometric Brownian Motion (GBM) generated data.
|
||||
4. **Handling Invalid Values**: Last-value substitution for NaN/Infinity.
|
||||
5. **WMA vs SMA vs EMA**: Comparing different moving averages.
|
||||
|
||||
#!csharp
|
||||
|
||||
// Reference the library
|
||||
#r "..\..\bin\QuanTAlib.dll"
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using QuanTAlib;
|
||||
|
||||
// Helper to print TSeries
|
||||
void PrintSeries(TSeries series, int count = 5)
|
||||
{
|
||||
Console.WriteLine($"Series Length: {series.Count}");
|
||||
foreach (var item in series.Take(count))
|
||||
{
|
||||
Console.WriteLine($"Time: {item.Time:HH:mm:ss}, Value: {item.Value:F4}");
|
||||
}
|
||||
if (series.Count > count) Console.WriteLine("...");
|
||||
}
|
||||
|
||||
#!markdown
|
||||
|
||||
## 1. Manual Data: Batch vs. Streaming
|
||||
|
||||
We'll start with a small, manually created dataset to clearly see how Batch and Streaming operations work.
|
||||
|
||||
### Batch Processing
|
||||
Batch processing calculates the WMA for the entire dataset at once. This is efficient for historical analysis.
|
||||
|
||||
#!csharp
|
||||
|
||||
// Create a small manual dataset
|
||||
var manualData = new TSeries();
|
||||
manualData.Add(DateTime.Now, 10.0);
|
||||
manualData.Add(DateTime.Now.AddMinutes(1), 20.0);
|
||||
manualData.Add(DateTime.Now.AddMinutes(2), 30.0);
|
||||
manualData.Add(DateTime.Now.AddMinutes(3), 40.0);
|
||||
manualData.Add(DateTime.Now.AddMinutes(4), 50.0);
|
||||
|
||||
Console.WriteLine("--- Input Data ---");
|
||||
PrintSeries(manualData, 5);
|
||||
|
||||
// Batch Calculation
|
||||
Console.WriteLine("\n--- Batch WMA (Period 3) ---");
|
||||
var wmaBatch = new Wma(3);
|
||||
var resultBatch = wmaBatch.Update(manualData);
|
||||
|
||||
PrintSeries(resultBatch, 5);
|
||||
|
||||
// Show the calculation for each step
|
||||
Console.WriteLine("\nCalculation breakdown (weights = [1, 2, 3], divisor = 6):");
|
||||
Console.WriteLine(" WMA[0] = (1×10) / 1 = 10.0000");
|
||||
Console.WriteLine(" WMA[1] = (1×10 + 2×20) / 3 = 50/3 = 16.6667");
|
||||
Console.WriteLine(" WMA[2] = (1×10 + 2×20 + 3×30) / 6 = 140/6 = 23.3333");
|
||||
Console.WriteLine(" WMA[3] = (1×20 + 2×30 + 3×40) / 6 = 200/6 = 33.3333");
|
||||
Console.WriteLine(" WMA[4] = (1×30 + 2×40 + 3×50) / 6 = 260/6 = 43.3333");
|
||||
|
||||
#!markdown
|
||||
|
||||
### Streaming Processing
|
||||
Streaming processing updates the WMA one data point at a time. This is essential for real-time trading systems where data arrives sequentially.
|
||||
|
||||
#!csharp
|
||||
|
||||
Console.WriteLine("\n--- Streaming WMA (Period 3) ---");
|
||||
var wmaStream = new Wma(3);
|
||||
|
||||
foreach (var item in manualData)
|
||||
{
|
||||
var result = wmaStream.Update(item);
|
||||
Console.WriteLine($"Time: {item.Time:HH:mm:ss}, Input: {item.Value:F2}, WMA: {result.Value:F4}, IsHot: {wmaStream.IsHot}");
|
||||
}
|
||||
|
||||
// Verify that the last values match
|
||||
var batchLast = resultBatch.Last().Value;
|
||||
var streamLast = wmaStream.Value.Value;
|
||||
Console.WriteLine($"\nMatch: {Math.Abs(batchLast - streamLast) < 1e-10} (Batch: {batchLast:F4}, Stream: {streamLast:F4})");
|
||||
|
||||
// Show WMA properties
|
||||
Console.WriteLine($"\nWMA Properties:");
|
||||
Console.WriteLine($" Name: {wmaStream.Name}");
|
||||
Console.WriteLine($" WarmupPeriod: {wmaStream.WarmupPeriod}");
|
||||
Console.WriteLine($" IsHot: {wmaStream.IsHot}");
|
||||
|
||||
#!markdown
|
||||
|
||||
## 2. Streaming with `isNew` (Intra-bar Updates)
|
||||
|
||||
In real-time feeds, you often receive multiple updates for the *same* bar (e.g., price changes within the current minute) before the bar closes.
|
||||
* `isNew = true`: The input is a new bar (advances time).
|
||||
* `isNew = false`: The input is an update to the current bar (recalculates without advancing).
|
||||
|
||||
**WMA achieves O(1) bar correction** by saving scalar state after each `isNew=true` update.
|
||||
|
||||
#!csharp
|
||||
|
||||
Console.WriteLine("\n--- Streaming with Intra-bar Updates ---");
|
||||
var wmaIntra = new Wma(3);
|
||||
|
||||
// 1. Process the first 4 bars normally
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
wmaIntra.Update(manualData[i]);
|
||||
}
|
||||
Console.WriteLine($"After 4th bar (40.0): {wmaIntra.Value.Value:F4}");
|
||||
|
||||
// 2. Simulate intra-bar updates for the 5th bar (Final value is 50.0)
|
||||
// Update 1: Price moves to 45.0
|
||||
var update1 = new TValue(manualData[4].Time, 45.0);
|
||||
wmaIntra.Update(update1, isNew: true); // First update for this bar is "New"
|
||||
Console.WriteLine($"Update 1 (45.0): {wmaIntra.Value.Value:F4}");
|
||||
|
||||
// Update 2: Price moves to 55.0 (Same time, same bar)
|
||||
var update2 = new TValue(manualData[4].Time, 55.0);
|
||||
wmaIntra.Update(update2, isNew: false); // Not new, just an update
|
||||
Console.WriteLine($"Update 2 (55.0): {wmaIntra.Value.Value:F4}");
|
||||
|
||||
// Update 3: Final Close at 50.0
|
||||
var update3 = manualData[4];
|
||||
wmaIntra.Update(update3, isNew: false); // Final update
|
||||
Console.WriteLine($"Update 3 (50.0): {wmaIntra.Value.Value:F4}");
|
||||
|
||||
// Verify match with batch result
|
||||
Console.WriteLine($"Match with Batch: {Math.Abs(wmaIntra.Value.Value - batchLast) < 1e-10}");
|
||||
|
||||
#!markdown
|
||||
|
||||
## 3. Large Dataset: Geometric Brownian Motion (GBM)
|
||||
|
||||
We'll generate a larger dataset (1000 bars) using a Geometric Brownian Motion generator to simulate realistic market data.
|
||||
|
||||
#!csharp
|
||||
|
||||
// Generate 1000 bars of data
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2);
|
||||
var gbmData = gbm.Fetch(1000, DateTime.Now.Ticks, TimeSpan.FromMinutes(1));
|
||||
var closeSeries = gbmData.Close;
|
||||
|
||||
Console.WriteLine($"Generated {closeSeries.Count} bars of GBM data.");
|
||||
Console.WriteLine($"First 5 values: {string.Join(", ", closeSeries.Take(5).Select(x => x.Value.ToString("F2")))}");
|
||||
|
||||
#!markdown
|
||||
|
||||
### Batch vs. Streaming Performance on Large Data
|
||||
|
||||
#!csharp
|
||||
|
||||
// Batch
|
||||
var wmaLargeBatch = new Wma(20);
|
||||
var batchLargeResult = wmaLargeBatch.Update(closeSeries);
|
||||
Console.WriteLine($"Batch Last Value: {batchLargeResult.Last().Value:F4}");
|
||||
|
||||
// Streaming
|
||||
var wmaLargeStream = new Wma(20);
|
||||
TValue lastStreamVal = default;
|
||||
foreach(var item in closeSeries)
|
||||
{
|
||||
lastStreamVal = wmaLargeStream.Update(item);
|
||||
}
|
||||
Console.WriteLine($"Streaming Last Value: {lastStreamVal.Value:F4}");
|
||||
|
||||
// Verify match
|
||||
Console.WriteLine($"Match: {Math.Abs(batchLargeResult.Last().Value - lastStreamVal.Value) < 1e-10}");
|
||||
|
||||
#!markdown
|
||||
|
||||
## 4. Handling Invalid Values (NaN/Infinity)
|
||||
|
||||
`Wma` uses **last-value substitution** for invalid inputs. When a non-finite value (NaN, PositiveInfinity, NegativeInfinity) is encountered, it is replaced with the last valid value. This provides output continuity instead of propagating invalid values through the calculation.
|
||||
|
||||
#!csharp
|
||||
|
||||
Console.WriteLine("\n--- Handling Invalid Values ---");
|
||||
|
||||
// Single WMA
|
||||
var wmaNaN = new Wma(10);
|
||||
|
||||
// Feed valid values first
|
||||
wmaNaN.Update(new TValue(DateTime.Now, 100.0));
|
||||
wmaNaN.Update(new TValue(DateTime.Now.AddMinutes(1), 110.0));
|
||||
Console.WriteLine($"After valid values: {wmaNaN.Value.Value:F4}");
|
||||
|
||||
// Feed NaN - should use last valid value (110)
|
||||
var resultAfterNaN = wmaNaN.Update(new TValue(DateTime.Now.AddMinutes(2), double.NaN));
|
||||
Console.WriteLine($"After NaN input: {resultAfterNaN.Value:F4} (IsFinite: {double.IsFinite(resultAfterNaN.Value)})");
|
||||
|
||||
// Feed Infinity - should use last valid value (110)
|
||||
var resultAfterInf = wmaNaN.Update(new TValue(DateTime.Now.AddMinutes(3), double.PositiveInfinity));
|
||||
Console.WriteLine($"After Infinity input: {resultAfterInf.Value:F4} (IsFinite: {double.IsFinite(resultAfterInf.Value)})");
|
||||
|
||||
// Continue with valid value
|
||||
var resultAfterValid = wmaNaN.Update(new TValue(DateTime.Now.AddMinutes(4), 120.0));
|
||||
Console.WriteLine($"After valid value (120): {resultAfterValid.Value:F4}");
|
||||
|
||||
#!csharp
|
||||
|
||||
Console.WriteLine("\n--- Batch Processing with Invalid Values ---");
|
||||
|
||||
// Create series with NaN values interspersed
|
||||
var seriesWithNaN = new TSeries();
|
||||
seriesWithNaN.Add(DateTime.Now.Ticks, 100.0);
|
||||
seriesWithNaN.Add(DateTime.Now.Ticks + 1, 110.0);
|
||||
seriesWithNaN.Add(DateTime.Now.Ticks + 2, double.NaN);
|
||||
seriesWithNaN.Add(DateTime.Now.Ticks + 3, 120.0);
|
||||
seriesWithNaN.Add(DateTime.Now.Ticks + 4, double.PositiveInfinity);
|
||||
seriesWithNaN.Add(DateTime.Now.Ticks + 5, 130.0);
|
||||
|
||||
var wmaBatchNaN = new Wma(3);
|
||||
var resultsWithNaN = wmaBatchNaN.Update(seriesWithNaN);
|
||||
|
||||
Console.WriteLine("Input → Output:");
|
||||
for (int i = 0; i < seriesWithNaN.Count; i++)
|
||||
{
|
||||
var input = seriesWithNaN[i].Value;
|
||||
var output = resultsWithNaN[i].Value;
|
||||
var inputStr = double.IsFinite(input) ? input.ToString("F2") : input.ToString();
|
||||
Console.WriteLine($" {inputStr,-10} → {output:F4} (IsFinite: {double.IsFinite(output)})");
|
||||
}
|
||||
|
||||
#!markdown
|
||||
|
||||
## 5. WMA vs SMA vs EMA Comparison
|
||||
|
||||
The WMA, SMA, and EMA are all trend-following indicators, but they weight data differently:
|
||||
|
||||
- **SMA**: Equal weight to all values in the window
|
||||
- **WMA**: Linear weights (newest = n, oldest = 1)
|
||||
- **EMA**: Exponential weights (more weight on recent values)
|
||||
|
||||
#!csharp
|
||||
|
||||
Console.WriteLine("\n--- WMA vs SMA vs EMA Comparison (Period 10) ---");
|
||||
|
||||
var compareData = new TSeries();
|
||||
var baseTime = DateTime.Now;
|
||||
for (int i = 0; i < 20; i++)
|
||||
{
|
||||
// Create data with a sudden spike at position 10
|
||||
double value = (i == 10) ? 150.0 : 100.0;
|
||||
compareData.Add(baseTime.AddMinutes(i), value);
|
||||
}
|
||||
|
||||
var wmaCompare = new Wma(10);
|
||||
var smaCompare = new Sma(10);
|
||||
var emaCompare = new Ema(10);
|
||||
|
||||
Console.WriteLine("Position | Input | WMA | SMA | EMA | WMA-SMA");
|
||||
Console.WriteLine("---------+--------+---------+---------+---------+--------");
|
||||
|
||||
for (int i = 0; i < compareData.Count; i++)
|
||||
{
|
||||
var wmaVal = wmaCompare.Update(compareData[i]);
|
||||
var smaVal = smaCompare.Update(compareData[i]);
|
||||
var emaVal = emaCompare.Update(compareData[i]);
|
||||
var input = compareData[i].Value;
|
||||
var diff = wmaVal.Value - smaVal.Value;
|
||||
|
||||
Console.WriteLine($" {i,2} | {input,6:F0} | {wmaVal.Value,7:F2} | {smaVal.Value,7:F2} | {emaVal.Value,7:F2} | {diff,+7:F2}");
|
||||
}
|
||||
|
||||
Console.WriteLine("\nNote: After the spike (position 10):");
|
||||
Console.WriteLine("- EMA reacts fastest due to exponential weighting on recent values");
|
||||
Console.WriteLine("- WMA reacts faster than SMA due to linear weighting");
|
||||
Console.WriteLine("- SMA takes longest as all values have equal weight");
|
||||
Console.WriteLine("- WMA provides a balance between SMA's stability and EMA's responsiveness");
|
||||
|
||||
#!markdown
|
||||
|
||||
## 6. WMA Weights More Recent Values
|
||||
|
||||
This example demonstrates how WMA weights more recent values compared to SMA.
|
||||
|
||||
#!csharp
|
||||
|
||||
Console.WriteLine("\n--- WMA vs SMA: Weight Distribution Effect ---");
|
||||
|
||||
// Create data where the most recent value is significantly different
|
||||
var weightDemo = new TSeries();
|
||||
weightDemo.Add(DateTime.Now, 10.0);
|
||||
weightDemo.Add(DateTime.Now.AddMinutes(1), 20.0);
|
||||
weightDemo.Add(DateTime.Now.AddMinutes(2), 100.0); // High recent value
|
||||
|
||||
var wmaWeight = new Wma(3);
|
||||
var smaWeight = new Sma(3);
|
||||
|
||||
foreach (var item in weightDemo)
|
||||
{
|
||||
wmaWeight.Update(item);
|
||||
smaWeight.Update(item);
|
||||
}
|
||||
|
||||
Console.WriteLine("Data: [10, 20, 100] (oldest to newest)");
|
||||
Console.WriteLine($"\nSMA(3) = (10 + 20 + 100) / 3 = {smaWeight.Value.Value:F4}");
|
||||
Console.WriteLine($"WMA(3) = (1×10 + 2×20 + 3×100) / 6 = {wmaWeight.Value.Value:F4}");
|
||||
Console.WriteLine($"\nWMA is {wmaWeight.Value.Value - smaWeight.Value.Value:F2} higher than SMA");
|
||||
Console.WriteLine("Because WMA gives 3× weight to the recent high value (100)");
|
||||
Console.WriteLine("while SMA treats all values equally.");
|
||||
@@ -140,6 +140,34 @@ wma.Update(new TValue(time + 1, 101.2), isNew: true);
|
||||
|
||||
**Implementation detail:** Bar correction is O(1) using scalar state save/restore, not buffer copying.
|
||||
|
||||
### Eventing and Reactive Support
|
||||
|
||||
This indicator implements the `ITValuePublisher` interface, enabling event-driven and reactive workflows.
|
||||
|
||||
* **Subscription:** Can be constructed with an `ITValuePublisher` (e.g., `TSeries`) to automatically update when the source emits a new value.
|
||||
* **Publication:** Emits a `Pub` event with the new `TValue` whenever it is updated.
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
// 1. Setup a source (publisher)
|
||||
var source = new TSeries();
|
||||
|
||||
// 2. Create indicator subscribed to source
|
||||
// It waits for events from 'source'
|
||||
var wma = new Wma(source, period: 10);
|
||||
|
||||
// 3. Optional: Subscribe to indicator's output
|
||||
wma.Pub += (item) => Console.WriteLine($"WMA Updated: {item.Value}");
|
||||
|
||||
// 4. Ingest data into source
|
||||
// This triggers the chain: source -> wma -> Console.WriteLine
|
||||
source.Add(new TValue(DateTime.Now, 100));
|
||||
source.Add(new TValue(DateTime.Now, 105));
|
||||
```
|
||||
|
||||
This pattern allows building complex, reactive processing pipelines without manual update loops.
|
||||
|
||||
### Handling Invalid Values (NaN/Infinity)
|
||||
|
||||
`Wma` uses **last-value substitution** for handling invalid inputs:
|
||||
|
||||
Reference in New Issue
Block a user