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:
Miha Kralj
2025-12-07 16:46:38 -08:00
parent 3734a1c5f6
commit 875998b288
31 changed files with 2445 additions and 1457 deletions
-158
View File
@@ -1,158 +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
# Triangular Moving Average (TRIMA) 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 **Triangular Moving Average (TRIMA)** is a weighted moving average where the weights increase linearly to the middle of the period and then decrease. It is equivalent to a double-smoothed SMA (SMA of an SMA).
**Key characteristics:**
- Triangular weighting (emphasis on middle values)
- Smoother than SMA
- Higher lag than SMA
- O(1) update complexity
#!csharp
// Reference the library
#r "..\..\bin\Debug\net10.0\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:F2}");
}
if (series.Count > count) Console.WriteLine("...");
}
#!markdown
## 1. Manual Data: Batch vs. Streaming
We'll start with a small, manually created dataset.
#!csharp
// Create a small manual dataset
var manualData = new TSeries();
manualData.Add(DateTime.Now, 100.0);
manualData.Add(DateTime.Now.AddMinutes(1), 102.0);
manualData.Add(DateTime.Now.AddMinutes(2), 101.0);
manualData.Add(DateTime.Now.AddMinutes(3), 103.0);
manualData.Add(DateTime.Now.AddMinutes(4), 105.0);
Console.WriteLine("--- Input Data ---");
PrintSeries(manualData, 5);
// Batch Calculation
Console.WriteLine("\n--- Batch TRIMA (Period 3) ---");
var trimaBatch = new Trima(3);
var resultBatch = trimaBatch.Update(manualData);
PrintSeries(resultBatch, 5);
#!markdown
### Streaming Processing
#!csharp
Console.WriteLine("\n--- Streaming TRIMA (Period 3) ---");
var trimaStream = new Trima(3);
foreach (var item in manualData)
{
var result = trimaStream.Update(item);
Console.WriteLine($"Time: {item.Time:HH:mm:ss}, Input: {item.Value:F2}, TRIMA: {result.Value:F2}, IsHot: {trimaStream.IsHot}");
}
// Verify that the last values match
var batchLast = resultBatch.Last().Value;
var streamLast = trimaStream.Value.Value;
Console.WriteLine($"\nMatch: {Math.Abs(batchLast - streamLast) < 1e-10} (Batch: {batchLast:F2}, Stream: {streamLast:F2})");
#!markdown
## 2. 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.");
#!markdown
### Batch vs. Streaming Performance on Large Data
#!csharp
// Batch
var trimaLargeBatch = new Trima(20);
var batchLargeResult = trimaLargeBatch.Update(closeSeries);
Console.WriteLine($"Batch Last Value: {batchLargeResult.Last().Value:F2}");
// Streaming
var trimaLargeStream = new Trima(20);
TValue lastStreamVal = default;
foreach(var item in closeSeries)
{
lastStreamVal = trimaLargeStream.Update(item);
}
Console.WriteLine($"Streaming Last Value: {lastStreamVal.Value:F2}");
// Verify match
Console.WriteLine($"Match: {Math.Abs(batchLargeResult.Last().Value - lastStreamVal.Value) < 1e-10}");
#!markdown
## 3. TRIMA vs SMA Comparison
TRIMA is smoother than SMA but has more lag. Let's compare them on a volatile dataset.
#!csharp
Console.WriteLine("\n--- TRIMA vs SMA 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 trimaCompare = new Trima(10);
var smaCompare = new Sma(10);
Console.WriteLine("Position | Input | TRIMA | SMA | Difference");
Console.WriteLine("---------+--------+---------+---------+-----------");
for (int i = 0; i < compareData.Count; i++)
{
var trimaVal = trimaCompare.Update(compareData[i]);
var smaVal = smaCompare.Update(compareData[i]);
var input = compareData[i].Value;
var diff = trimaVal.Value - smaVal.Value;
Console.WriteLine($" {i,2} | {input,6:F0} | {trimaVal.Value,7:F2} | {smaVal.Value,7:F2} | {diff,+9:F2}");
}
Console.WriteLine("\nNote how TRIMA reacts more gradually to the spike compared to SMA.");
+30
View File
@@ -41,6 +41,36 @@ TRIMA(source, p) = SMA(SMA(source, (p+1)/2), (p+1)/2)
> 🔍 **Technical Note:** The double application of SMA explains why TRIMA provides better smoothing than a single SMA or WMA. This approach effectively applies smoothing twice with optimal period adjustment, creating a -18dB/octave roll-off in the frequency domain compared to -6dB/octave for a simple moving average.
## C# Implementation
### 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 trima = new Trima(source, period: 14);
// 3. Optional: Subscribe to indicator's output
trima.Pub += (item) => Console.WriteLine($"TRIMA Updated: {item.Value}");
// 4. Ingest data into source
// This triggers the chain: source -> trima -> 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.
## Interpretation Details
TRIMA can be used in various trading strategies: