mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-25 05:48:06 +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:
@@ -1,219 +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
|
||||
|
||||
# Exponential Moving Average (EMA) 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.
|
||||
|
||||
For detailed documentation on the EMA indicator, including mathematical formulas and interpretation, please refer to [Ema.md](Ema.md).
|
||||
|
||||
The **Exponential Moving Average (EMA)** is a weighted moving average that gives more importance to recent price data. Unlike the Simple Moving Average (SMA), which assigns equal weight to all data points, the EMA reacts more significantly to recent price changes.
|
||||
|
||||
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.
|
||||
|
||||
#!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:F2}");
|
||||
}
|
||||
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 EMA 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, 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 EMA (Period 3) ---");
|
||||
var emaBatch = new Ema(3);
|
||||
var resultBatch = emaBatch.Update(manualData);
|
||||
|
||||
PrintSeries(resultBatch, 5);
|
||||
|
||||
#!markdown
|
||||
|
||||
### Streaming Processing
|
||||
Streaming processing updates the EMA one data point at a time. This is essential for real-time trading systems where data arrives sequentially.
|
||||
|
||||
#!csharp
|
||||
|
||||
Console.WriteLine("\n--- Streaming EMA (Period 3) ---");
|
||||
var emaStream = new Ema(3);
|
||||
|
||||
foreach (var item in manualData)
|
||||
{
|
||||
var result = emaStream.Update(item);
|
||||
Console.WriteLine($"Time: {item.Time:HH:mm:ss}, Input: {item.Value:F2}, EMA: {result.Value:F2}, IsHot: {emaStream.IsHot}");
|
||||
}
|
||||
|
||||
// Verify that the last values match
|
||||
var batchLast = resultBatch.Last().Value;
|
||||
var streamLast = emaStream.Value.Value;
|
||||
Console.WriteLine($"\nMatch: {Math.Abs(batchLast - streamLast) < 1e-10} (Batch: {batchLast:F2}, Stream: {streamLast:F2})");
|
||||
|
||||
#!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).
|
||||
|
||||
#!csharp
|
||||
|
||||
Console.WriteLine("\n--- Streaming with Intra-bar Updates ---");
|
||||
var emaIntra = new Ema(3);
|
||||
|
||||
// 1. Process the first 4 bars normally
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
emaIntra.Update(manualData[i]);
|
||||
}
|
||||
Console.WriteLine($"After 4th bar: {emaIntra.Value.Value:F2}");
|
||||
|
||||
// 2. Simulate intra-bar updates for the 5th bar (Final value is 105.0)
|
||||
// Update 1: Price moves to 104.0
|
||||
var update1 = new TValue(manualData[4].Time, 104.0);
|
||||
emaIntra.Update(update1, isNew: true); // First update for this bar is "New"
|
||||
Console.WriteLine($"Update 1 (104.0): {emaIntra.Value.Value:F2}");
|
||||
|
||||
// Update 2: Price moves to 106.0 (Same time, same bar)
|
||||
var update2 = new TValue(manualData[4].Time, 106.0);
|
||||
emaIntra.Update(update2, isNew: false); // Not new, just an update
|
||||
Console.WriteLine($"Update 2 (106.0): {emaIntra.Value.Value:F2}");
|
||||
|
||||
// Update 3: Final Close at 105.0
|
||||
var update3 = manualData[4];
|
||||
emaIntra.Update(update3, isNew: false); // Final update
|
||||
Console.WriteLine($"Update 3 (105.0): {emaIntra.Value.Value:F2}");
|
||||
|
||||
// Verify match with batch result
|
||||
Console.WriteLine($"Match with Batch: {Math.Abs(emaIntra.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 emaLargeBatch = new Ema(20);
|
||||
var batchLargeResult = emaLargeBatch.Update(closeSeries);
|
||||
Console.WriteLine($"Batch Last Value: {batchLargeResult.Last().Value:F2}");
|
||||
|
||||
// Streaming
|
||||
var emaLargeStream = new Ema(20);
|
||||
TValue lastStreamVal = default;
|
||||
foreach(var item in closeSeries)
|
||||
{
|
||||
lastStreamVal = emaLargeStream.Update(item);
|
||||
}
|
||||
Console.WriteLine($"Streaming Last Value: {lastStreamVal.Value:F2}");
|
||||
|
||||
#!markdown
|
||||
|
||||
## 4. Handling Invalid Values (NaN/Infinity)
|
||||
|
||||
`Ema` 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 EMA
|
||||
var emaNaN = new Ema(10);
|
||||
|
||||
// Feed valid values first
|
||||
emaNaN.Update(new TValue(DateTime.Now, 100.0));
|
||||
emaNaN.Update(new TValue(DateTime.Now.AddMinutes(1), 110.0));
|
||||
Console.WriteLine($"After valid values: {emaNaN.Value.Value:F2}");
|
||||
|
||||
// Feed NaN - should use last valid value (110)
|
||||
var resultAfterNaN = emaNaN.Update(new TValue(DateTime.Now.AddMinutes(2), double.NaN));
|
||||
Console.WriteLine($"After NaN input: {resultAfterNaN.Value:F2} (IsFinite: {double.IsFinite(resultAfterNaN.Value)})");
|
||||
|
||||
// Feed Infinity - should use last valid value (110)
|
||||
var resultAfterInf = emaNaN.Update(new TValue(DateTime.Now.AddMinutes(3), double.PositiveInfinity));
|
||||
Console.WriteLine($"After Infinity input: {resultAfterInf.Value:F2} (IsFinite: {double.IsFinite(resultAfterInf.Value)})");
|
||||
|
||||
// Continue with valid value
|
||||
var resultAfterValid = emaNaN.Update(new TValue(DateTime.Now.AddMinutes(4), 120.0));
|
||||
Console.WriteLine($"After valid value (120): {resultAfterValid.Value:F2}");
|
||||
|
||||
#!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 emaBatchNaN = new Ema(3);
|
||||
var resultsWithNaN = emaBatchNaN.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:F2} (IsFinite: {double.IsFinite(output)})");
|
||||
}
|
||||
@@ -115,6 +115,34 @@ Console.WriteLine($"Last EMA: {emaOutput[^1]}");
|
||||
* **Hunter's bias correction**: Same accuracy as TSeries API
|
||||
* **Compatible** with `ArrayPool<T>` for buffer management
|
||||
|
||||
### 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 ema = new Ema(source, period: 10);
|
||||
|
||||
// 3. Optional: Subscribe to indicator's output
|
||||
ema.Pub += (item) => Console.WriteLine($"EMA Updated: {item.Value}");
|
||||
|
||||
// 4. Ingest data into source
|
||||
// This triggers the chain: source -> ema -> 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)
|
||||
|
||||
`Ema` uses **last-value substitution** for handling invalid inputs:
|
||||
|
||||
Reference in New Issue
Block a user