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
+65
View File
@@ -0,0 +1,65 @@
# Averages
| Indicator | Name |
| --------- | ------------------------------ |
| ALMA | Arnaud Legoux MA |
| BESSEL | Bessel Filter |
| BILATERAL | Bilateral Filter |
| BLMA | Blackman Window MA |
| BPF | Ehlers Bandpass Filter |
| BUTTER | Butterworth Filter |
| BWMA | Bessel-Weighted MA |
| CHEBY1 | Chebyshev Type I Filter |
| CHEBY2 | Chebyshev Type II Filter |
| CONV | Convolution MA with any kernel |
| [DEMA](dema/Dema.cs) | Double Exponential MA |
| DSMA | Deviation-Scaled MA |
| DWMA | Double Weighted MA |
| ELLIPTIC | Elliptic (Cauer) Filter |
| [EMA](ema/Ema.cs) | Exponential MA |
| EPMA | Endpoint MA |
| FRAMA | Fractal Adaptive MA |
| GAUSS | Gaussian Filter |
| GWMA | Gaussian-Weighted MA |
| HAMMA | Hamming Window MA |
| HANN | Hann FIR Filter |
| HANMA | Hanning Window MA |
| HEMA | Hull Exponential MA |
| [HMA](hma/Hma.cs) | Hull MA |
| HP | Hodrick-Prescott Filter |
| HPF | Ehlers Highpass Filter |
| HTIT | Hilbert Transform Instantaneous Trend |
| HWMA | Holt Weighted MA |
| JMA | Jurik MA |
| KAMA | Kaufman Adaptive MA |
| KF | Kalman Filter |
| LOESS | LOESS/LOWESS Smoothing |
| LSMA | Least Squares MA |
| LTMA | Linear Trend MA |
| MAMA | MESA Adaptive MA |
| MEDIAN | Median Filter |
| MGDI | McGinley Dynamic Indicator |
| MMA | Modified MA |
| NOTCH | Notch Filter |
| PWMA | Pascal Weighted MA |
| QEMA | Quadruple Exponential MA |
| REMA | Regularized Exponential MA |
| RGMA | Recursive Gaussian MA |
| RMA | wildeR MA (SMMA, MMA) |
| SGF | Savitzky-Golay Filter |
| SGMA | Savitzky-Golay MA |
| SINEMA | Sine-weighted MA |
| [SMA](sma/Sma.cs) | Simple MA |
| SSF | Ehlers Super Smooth Filter |
| T3 | Tillson T3 MA |
| [TEMA](tema/Tema.cs) | Triple Exponential MA |
| [TRIMA](trima/Trima.cs) | Triangular MA |
| USF | Ehlers Ultrasmooth Filter |
| VAMA | Volatility Adjusted MA |
| VIDYA | Variable Index Dynamic Average |
| WIENER | Wiener Filter |
| [WMA](wma/Wma.cs) | Weighted MA |
| YZVAMA | Yang-Zhang Volatility Adjusted MA |
| ZLDEMA | Zero-Lag Double Exponential MA |
| ZLEMA | Zero-Lag Exponential MA |
| ZLTEMA | Zero-Lag Triple Exponential MA |
+87
View File
@@ -0,0 +1,87 @@
using System;
using Xunit;
namespace QuanTAlib.Tests;
public class DemaCoverageTests
{
[Fact]
public void Dema_CompensationLogic_IsTriggered()
{
// Compensation happens when E <= 1e-10
// E starts at 1.0 and decays by (1-alpha) each step.
// We need enough steps to reach 1e-10.
// If period=10, alpha ~ 0.18, decay ~ 0.81
// 0.81^n <= 1e-10 => n * log(0.81) <= -10
// n * -0.09 <= -10 => n >= 111
int count = 200;
int period = 10;
var dema = new Dema(period);
for (int i = 0; i < count; i++)
{
dema.Update(new TValue(DateTime.UtcNow, 100.0));
}
// Just ensuring no exception and value is correct
Assert.Equal(100.0, dema.Last.Value, 1e-9);
}
[Fact]
public void Dema_IsHot_Logic()
{
// IsHot happens when E <= 0.05
// 0.81^n <= 0.05 => n >= 14
int period = 10;
var dema = new Dema(period);
Assert.False(dema.IsHot);
for (int i = 0; i < 50; i++)
{
dema.Update(new TValue(DateTime.UtcNow, 100.0));
if (i > 30) // Should be hot by now
{
Assert.True(dema.IsHot);
}
}
}
[Fact]
public void Dema_StaticCalculate_Alpha_Coverage()
{
double[] source = new double[100];
double[] output = new double[100];
for(int i=0; i<100; i++) source[i] = 100.0;
// Use alpha overload
Dema.Calculate(source.AsSpan(), output.AsSpan(), 0.1);
Assert.Equal(100.0, output[^1], 1e-9);
}
[Fact]
public void Dema_Reset_ClearsState()
{
var dema = new Dema(10);
dema.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.NotEqual(0, dema.Last.Value);
dema.Reset();
Assert.Equal(0, dema.Last.Value);
Assert.False(dema.IsHot);
}
[Fact]
public void Dema_Constructor_Alpha_Validation()
{
Assert.Throws<ArgumentException>(() => new Dema(0.0));
Assert.Throws<ArgumentException>(() => new Dema(1.1));
var dema = new Dema(0.5);
Assert.NotNull(dema);
}
}
-219
View File
@@ -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
# Double Exponential Moving Average (DEMA) 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 DEMA indicator, including mathematical formulas and interpretation, please refer to [Dema.md](Dema.md).
The **Double Exponential Moving Average (DEMA)** is a technical indicator designed to reduce the lag associated with traditional moving averages. It combines a single EMA and a double EMA to achieve higher responsiveness.
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 DEMA 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 DEMA (Period 3) ---");
var demaBatch = new Dema(3);
var resultBatch = demaBatch.Update(manualData);
PrintSeries(resultBatch, 5);
#!markdown
### Streaming Processing
Streaming processing updates the DEMA one data point at a time. This is essential for real-time trading systems where data arrives sequentially.
#!csharp
Console.WriteLine("\n--- Streaming DEMA (Period 3) ---");
var demaStream = new Dema(3);
foreach (var item in manualData)
{
var result = demaStream.Update(item);
Console.WriteLine($"Time: {item.Time:HH:mm:ss}, Input: {item.Value:F2}, DEMA: {result.Value:F2}, IsHot: {demaStream.IsHot}");
}
// Verify that the last values match
var batchLast = resultBatch.Last().Value;
var streamLast = demaStream.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 demaIntra = new Dema(3);
// 1. Process the first 4 bars normally
for (int i = 0; i < 4; i++)
{
demaIntra.Update(manualData[i]);
}
Console.WriteLine($"After 4th bar: {demaIntra.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);
demaIntra.Update(update1, isNew: true); // First update for this bar is "New"
Console.WriteLine($"Update 1 (104.0): {demaIntra.Value.Value:F2}");
// Update 2: Price moves to 106.0 (Same time, same bar)
var update2 = new TValue(manualData[4].Time, 106.0);
demaIntra.Update(update2, isNew: false); // Not new, just an update
Console.WriteLine($"Update 2 (106.0): {demaIntra.Value.Value:F2}");
// Update 3: Final Close at 105.0
var update3 = manualData[4];
demaIntra.Update(update3, isNew: false); // Final update
Console.WriteLine($"Update 3 (105.0): {demaIntra.Value.Value:F2}");
// Verify match with batch result
Console.WriteLine($"Match with Batch: {Math.Abs(demaIntra.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 demaLargeBatch = new Dema(20);
var batchLargeResult = demaLargeBatch.Update(closeSeries);
Console.WriteLine($"Batch Last Value: {batchLargeResult.Last().Value:F2}");
// Streaming
var demaLargeStream = new Dema(20);
TValue lastStreamVal = default;
foreach(var item in closeSeries)
{
lastStreamVal = demaLargeStream.Update(item);
}
Console.WriteLine($"Streaming Last Value: {lastStreamVal.Value:F2}");
#!markdown
## 4. Handling Invalid Values (NaN/Infinity)
`Dema` 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 DEMA
var demaNaN = new Dema(10);
// Feed valid values first
demaNaN.Update(new TValue(DateTime.Now, 100.0));
demaNaN.Update(new TValue(DateTime.Now.AddMinutes(1), 110.0));
Console.WriteLine($"After valid values: {demaNaN.Value.Value:F2}");
// Feed NaN - should use last valid value (110)
var resultAfterNaN = demaNaN.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 = demaNaN.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 = demaNaN.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 demaBatchNaN = new Dema(3);
var resultsWithNaN = demaBatchNaN.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)})");
}
+28
View File
@@ -83,6 +83,34 @@ double[] demaOutput = new double[200000];
Dema.Calculate(source.AsSpan(), demaOutput.AsSpan(), period: 50);
```
### 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 dema = new Dema(source, period: 14);
// 3. Optional: Subscribe to indicator's output
dema.Pub += (item) => Console.WriteLine($"DEMA Updated: {item.Value}");
// 4. Ingest data into source
// This triggers the chain: source -> dema -> 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
`Dema` delegates value handling to the underlying `Ema` instances, which use **last-value substitution** for `NaN` or `Infinity`. This ensures continuity and stability in the output series.
-219
View File
@@ -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)})");
}
+28
View File
@@ -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:
+77
View File
@@ -0,0 +1,77 @@
using System;
using Xunit;
namespace QuanTAlib.Tests;
public class HmaCoverageTests
{
[Fact]
public void Hma_CalculateIntermediate_Simd_Coverage()
{
// CalculateIntermediate uses SIMD if length >= Vector256<double>.Count (4)
int count = 100;
int period = 10;
double[] source = new double[count];
double[] output = new double[count];
for(int i=0; i<count; i++) source[i] = 100.0;
Hma.Calculate(source.AsSpan(), output.AsSpan(), period);
Assert.Equal(100.0, output[^1], 1e-9);
}
[Fact]
public void Hma_CalculateIntermediate_Scalar_Coverage()
{
// Force scalar path by using small length
int count = 3;
int period = 2; // Min period is 2
double[] source = new double[count];
double[] output = new double[count];
for(int i=0; i<count; i++) source[i] = 100.0;
Hma.Calculate(source.AsSpan(), output.AsSpan(), period);
Assert.Equal(100.0, output[^1], 1e-9);
}
[Fact]
public void Hma_Reset_ClearsState()
{
var hma = new Hma(10);
hma.Update(new TValue(DateTime.UtcNow, 100.0));
Assert.NotEqual(0, hma.Last.Value);
hma.Reset();
Assert.Equal(0, hma.Last.Value);
Assert.False(hma.IsHot);
}
[Fact]
public void Hma_UpdateSeries_RestoresState()
{
var hma = new Hma(10);
var series = new TSeries();
for(int i=0; i<20; i++) series.Add(DateTime.UtcNow.AddMinutes(i), 100.0);
hma.Update(series);
// After batch update, the instance state should be consistent with the end of the series
// So next update should continue correctly
var nextVal = hma.Update(new TValue(DateTime.UtcNow.AddMinutes(20), 100.0));
Assert.Equal(100.0, nextVal.Value, 1e-9);
}
[Fact]
public void Hma_Constructor_Validation()
{
Assert.Throws<ArgumentException>(() => new Hma(1));
Assert.Throws<ArgumentException>(() => new Hma(0));
var hma = new Hma(2);
Assert.NotNull(hma);
}
}
+63
View File
@@ -0,0 +1,63 @@
using System.Drawing;
using TradingPlatform.BusinessLayer;
namespace QuanTAlib;
public class HmaIndicator : Indicator, IWatchlistIndicator
{
[InputParameter("Period", sortIndex: 1, 2, 1000, 1, 0)]
public int Period { get; set; } = 14;
[IndicatorExtensions.DataSourceInput]
public SourceType Source { get; set; } = SourceType.Close;
[InputParameter("Show cold values", sortIndex: 21)]
public bool ShowColdValues { get; set; } = true;
private Hma? ma;
private int _warmupBarIndex = -1;
protected LineSeries? Series;
protected string? SourceName;
public int MinHistoryDepths => Period + (int)Math.Sqrt(Period); // Approximate warmup
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
public override string ShortName => $"HMA {Period}:{SourceName}";
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/averages/hma/Hma.cs";
public HmaIndicator()
{
OnBackGround = true;
SeparateWindow = false;
SourceName = Source.ToString();
Name = "HMA - Hull Moving Average";
Description = "Hull Moving Average for reduced lag";
Series = new(name: $"HMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
AddLineSeries(Series);
}
protected override void OnInit()
{
ma = new Hma(Period);
_warmupBarIndex = -1;
SourceName = Source.ToString();
base.OnInit();
}
protected override void OnUpdate(UpdateArgs args)
{
TValue input = this.GetInputValue(args, Source);
bool isNew = args.Reason == UpdateReason.NewBar || args.Reason == UpdateReason.HistoricalBar;
TValue result = ma!.Update(input, isNew);
if (_warmupBarIndex < 0 && ma!.IsHot)
_warmupBarIndex = Count;
Series!.SetValue(result.Value);
Series!.SetMarker(0, Color.Transparent);
}
public override void OnPaintChart(PaintChartEventArgs args)
{
base.OnPaintChart(args);
this.PaintSmoothCurve(args, Series!, _warmupBarIndex, showColdValues: ShowColdValues, tension: 0.2);
}
}
+159
View File
@@ -0,0 +1,159 @@
using System;
using System.Linq;
using Xunit;
namespace QuanTAlib.Tests;
public class HmaTests
{
[Fact]
public void Hma_Constructor_ValidatesInput()
{
Assert.Throws<ArgumentException>(() => new Hma(0));
Assert.Throws<ArgumentException>(() => new Hma(1)); // HMA requires period > 1 for sqrt(period) >= 1
var hma = new Hma(10);
Assert.NotNull(hma);
}
[Fact]
public void Hma_Calc_ReturnsValue()
{
var hma = new Hma(10);
TValue result = hma.Update(new TValue(DateTime.UtcNow, 100));
Assert.True(result.Value > 0);
}
[Fact]
public void Hma_IsHot_BecomesTrue()
{
var hma = new Hma(9); // sqrt(9) = 3
// Full WMA needs 9
// Half WMA needs 4
// Sqrt WMA needs 3
// Pipeline:
// 1. Full/Half produce valid values immediately (but with warmup ramp)
// 2. Sqrt consumes them.
// IsHot is defined as Full.IsHot && Sqrt.IsHot.
// Full becomes hot after 9 updates.
// Sqrt becomes hot after 3 updates.
// So HMA should be hot after 9 updates.
for (int i = 0; i < 8; i++)
{
hma.Update(new TValue(DateTime.UtcNow, 100));
Assert.False(hma.IsHot);
}
hma.Update(new TValue(DateTime.UtcNow, 100));
Assert.True(hma.IsHot);
}
[Fact]
public void Hma_StreamingMatchesBatch()
{
var hmaStreaming = new Hma(14);
var hmaBatch = new Hma(14);
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
var series = new TSeries();
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar.Time, bar.Close);
}
Assert.Equal(100, series.Count);
// Streaming
var streamingResults = new TSeries();
foreach (var item in series)
{
streamingResults.Add(hmaStreaming.Update(item));
}
// Batch
var batchResults = hmaBatch.Update(series);
Assert.Equal(streamingResults.Count, batchResults.Count);
for (int i = 0; i < streamingResults.Count; i++)
{
Assert.Equal(streamingResults[i].Value, batchResults[i].Value, 1e-9);
}
}
[Fact]
public void Hma_StaticCalculate_MatchesInstance()
{
var series = new TSeries();
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar.Time, bar.Close);
}
var instanceResults = new Hma(14).Update(series);
var staticResults = Hma.Calculate(series, 14);
for (int i = 0; i < instanceResults.Count; i++)
{
Assert.Equal(instanceResults[i].Value, staticResults[i].Value, 1e-9);
}
}
[Fact]
public void Hma_SpanCalculate_MatchesSeries()
{
var series = new TSeries();
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
for (int i = 0; i < 100; i++)
{
var bar = gbm.Next(isNew: true);
series.Add(bar.Time, bar.Close);
}
var seriesResults = Hma.Calculate(series, 14);
double[] input = series.Values.ToArray();
double[] output = new double[input.Length];
Hma.Calculate(input.AsSpan(), output.AsSpan(), 14);
for (int i = 0; i < input.Length; i++)
{
Assert.Equal(seriesResults[i].Value, output[i], 1e-9);
}
}
[Fact]
public void Hma_Update_IsNewFalse_CorrectsValue()
{
var hma = new Hma(10);
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
// Feed initial data
for (int i = 0; i < 20; i++)
{
var bar = gbm.Next(isNew: true);
hma.Update(new TValue(bar.Time, bar.Close), isNew: true);
}
// Update with isNew=false (correction)
var newBar = gbm.Next(isNew: true); // Generate a new value
hma.Update(new TValue(newBar.Time, newBar.Close), isNew: true); // Commit it
double valueAfterCommit = hma.Last.Value;
// Now update the SAME bar with a different value
hma.Update(new TValue(newBar.Time, newBar.Close + 10.0), isNew: false);
double valueAfterCorrection = hma.Last.Value;
Assert.NotEqual(valueAfterCommit, valueAfterCorrection);
// Now restore original value
hma.Update(new TValue(newBar.Time, newBar.Close), isNew: false);
Assert.Equal(valueAfterCommit, hma.Last.Value, 1e-9);
}
}
+232
View File
@@ -0,0 +1,232 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Skender.Stock.Indicators;
using Tulip;
using Xunit;
using Xunit.Abstractions;
namespace QuanTAlib.Tests;
public class HmaValidationTests
{
private readonly TBarSeries _bars;
private readonly TSeries _data;
private readonly List<Quote> _skenderQuotes;
private readonly ITestOutputHelper _output;
public HmaValidationTests(ITestOutputHelper output)
{
_output = output;
// 1. Generate 1000 records using GBM feed
var gbm = new GBM(startPrice: 100.0, mu: 0.05, sigma: 0.2, seed: 42);
_bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
// 2. Extract Close TSeries
_data = _bars.Close;
// 3. Prepare data for Skender (List<Quote>)
_skenderQuotes = new List<Quote>();
for (int i = 0; i < _bars.Count; i++)
{
_skenderQuotes.Add(new Quote
{
Date = new DateTime(_bars.Open.Times[i], DateTimeKind.Utc),
Open = (decimal)_bars.Open[i].Value,
High = (decimal)_bars.High[i].Value,
Low = (decimal)_bars.Low[i].Value,
Close = (decimal)_bars.Close[i].Value,
Volume = (decimal)_bars.Volume[i].Value
});
}
}
[Fact]
public void Validate_Skender_Batch()
{
int[] periods = { 9, 14, 20, 50 };
foreach (var period in periods)
{
// Calculate QuanTAlib HMA (batch TSeries)
var hma = new global::QuanTAlib.Hma(period);
var qResult = hma.Update(_data);
// Calculate Skender HMA
var sResult = _skenderQuotes.GetHma(period).ToList();
// Compare last 100 records
VerifyData_Skender(qResult, sResult);
}
_output.WriteLine("HMA Batch(TSeries) validated successfully against Skender");
}
[Fact]
public void Validate_Tulip_Batch()
{
int[] periods = { 9, 14, 20, 50 };
// Prepare data for Tulip (double[])
double[] tData = _data.Select(x => x.Value).ToArray();
foreach (var period in periods)
{
// Calculate QuanTAlib HMA (batch TSeries)
var hma = new global::QuanTAlib.Hma(period);
var qResult = hma.Update(_data);
// Calculate Tulip HMA
var hmaIndicator = Tulip.Indicators.hma;
double[][] inputs = { tData };
double[] options = { period };
// HMA lookback is period + sqrt(period) - 1 roughly
// We'll calculate the output size based on the input size and expected lookback
// Tulip usually returns (input_len - lookback) elements
// But we can just let it fill what it can if we provide a large enough buffer?
// No, Tulip.NET wrapper usually expects exact size or it might crash/misbehave.
// Let's try to be precise.
// WMA(n) lookback = n-1
// HMA = WMA(sqrt(n), 2*WMA(n/2) - WMA(n))
// Path 1: WMA(n) -> valid at n-1
// Path 2: WMA(n/2) -> valid at n/2-1
// Combined: valid at max(n-1, n/2-1) = n-1
// Then WMA(sqrt(n)) on that -> adds sqrt(n)-1 lag
// Total lookback = (n-1) + (sqrt(n)-1) = n + sqrt(n) - 2
int sqrtPeriod = (int)Math.Sqrt(period);
int lookback = period + sqrtPeriod - 2;
double[][] outputs = { new double[tData.Length - lookback] };
hmaIndicator.Run(inputs, options, outputs);
var tResult = outputs[0];
// Compare last 100 records
VerifyData_Tulip(qResult, tResult, lookback);
}
_output.WriteLine("HMA Batch(TSeries) validated successfully against Tulip");
}
[Fact]
public void Validate_Skender_Streaming()
{
int[] periods = { 9, 14, 20, 50 };
foreach (var period in periods)
{
// Calculate QuanTAlib HMA (streaming)
var hma = new global::QuanTAlib.Hma(period);
var qResults = new List<double>();
foreach (var item in _data)
{
qResults.Add(hma.Update(item).Value);
}
// Calculate Skender HMA
var sResult = _skenderQuotes.GetHma(period).ToList();
// Compare last 100 records
VerifyData_Skender_Streaming(qResults, sResult);
}
_output.WriteLine("HMA Streaming validated successfully against Skender");
}
[Fact]
public void Validate_Skender_Span()
{
int[] periods = { 9, 14, 20, 50 };
// Prepare data for Span API
double[] sourceData = _data.Select(x => x.Value).ToArray();
foreach (var period in periods)
{
// Calculate QuanTAlib HMA (Span API)
double[] qOutput = new double[sourceData.Length];
global::QuanTAlib.Hma.Calculate(sourceData.AsSpan(), qOutput.AsSpan(), period);
// Calculate Skender HMA
var sResult = _skenderQuotes.GetHma(period).ToList();
// Compare last 100 records
VerifyData_Skender_Span(qOutput, sResult);
}
_output.WriteLine("HMA Span validated successfully against Skender");
}
private static void VerifyData_Skender(TSeries qSeries, List<HmaResult> sSeries)
{
Assert.Equal(qSeries.Count, sSeries.Count);
int count = qSeries.Count;
int skip = count - 100;
for (int i = skip; i < count; i++)
{
double qValue = qSeries[i].Value;
double? sValue = sSeries[i].Hma;
if (!sValue.HasValue) continue;
Assert.Equal(sValue.Value, qValue, 1e-6);
}
}
private static void VerifyData_Skender_Streaming(List<double> qResults, List<HmaResult> sSeries)
{
Assert.Equal(qResults.Count, sSeries.Count);
int count = qResults.Count;
int skip = count - 100;
for (int i = skip; i < count; i++)
{
double qValue = qResults[i];
double? sValue = sSeries[i].Hma;
if (!sValue.HasValue) continue;
Assert.Equal(sValue.Value, qValue, 1e-6);
}
}
private static void VerifyData_Skender_Span(double[] qOutput, List<HmaResult> sSeries)
{
Assert.Equal(qOutput.Length, sSeries.Count);
int count = qOutput.Length;
int skip = count - 100;
for (int i = skip; i < count; i++)
{
double qValue = qOutput[i];
double? sValue = sSeries[i].Hma;
if (!sValue.HasValue) continue;
Assert.Equal(sValue.Value, qValue, 1e-6);
}
}
private static void VerifyData_Tulip(TSeries qSeries, double[] tOutput, int lookback)
{
int count = qSeries.Count;
int skip = count - 100;
for (int i = skip; i < count; i++)
{
double qValue = qSeries[i].Value;
if (i < lookback) continue;
int tIndex = i - lookback;
if (tIndex >= tOutput.Length) continue;
double tValue = tOutput[tIndex];
Assert.Equal(tValue, qValue, 1e-6);
}
}
}
+193
View File
@@ -0,0 +1,193 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
namespace QuanTAlib;
/// <summary>
/// HMA: Hull Moving Average
/// </summary>
/// <remarks>
/// HMA reduces lag by using a combination of weighted moving averages.
///
/// Calculation:
/// HMA = WMA(sqrt(n), 2 * WMA(n/2, price) - WMA(n, price))
///
/// Sources:
/// https://alan.hull.com.au/hma.html
/// </remarks>
[SkipLocalsInit]
public sealed class Hma : ITValuePublisher
{
private readonly int _period;
private readonly Wma _wmaFull;
private readonly Wma _wmaHalf;
private readonly Wma _wmaSqrt;
public string Name { get; }
public TValue Last { get; private set; }
public bool IsHot => _wmaFull.IsHot && _wmaSqrt.IsHot;
public event Action<TValue>? Pub;
public Hma(int period)
{
if (period <= 1) throw new ArgumentException("Period must be greater than 1", nameof(period));
_period = period;
int halfPeriod = period / 2;
int sqrtPeriod = (int)Math.Sqrt(period);
_wmaFull = new Wma(period);
_wmaHalf = new Wma(halfPeriod);
_wmaSqrt = new Wma(sqrtPeriod);
Name = $"Hma({period})";
}
public Hma(ITValuePublisher source, int period) : this(period)
{
source.Pub += (item) => Update(item);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
// 1. Calculate WMA(n)
TValue full = _wmaFull.Update(input, isNew);
// 2. Calculate WMA(n/2)
TValue half = _wmaHalf.Update(input, isNew);
// 3. Calculate intermediate: 2 * WMA(n/2) - WMA(n)
double intermediate = (2.0 * half.Value) - full.Value;
// 4. Calculate HMA = WMA(sqrt(n), intermediate)
Last = _wmaSqrt.Update(new TValue(input.Time, intermediate), isNew);
Pub?.Invoke(Last);
return Last;
}
public TSeries Update(TSeries source)
{
if (source.Count == 0) return new TSeries(new List<long>(), new List<double>());
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
Calculate(source.Values, vSpan, _period);
source.Times.CopyTo(tSpan);
// Restore state for streaming
_wmaFull.Reset();
_wmaHalf.Reset();
_wmaSqrt.Reset();
int lookback = _period + (int)Math.Sqrt(_period) + 10; // Sufficient lookback
int startIndex = Math.Max(0, len - lookback);
for (int i = startIndex; i < len; i++)
{
Update(source[i]);
}
return new TSeries(t, v);
}
public static TSeries Calculate(TSeries source, int period)
{
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
CollectionsMarshal.SetCount(t, len);
CollectionsMarshal.SetCount(v, len);
Calculate(source.Values, CollectionsMarshal.AsSpan(v), period);
source.Times.CopyTo(CollectionsMarshal.AsSpan(t));
return new TSeries(t, v);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Calculate(ReadOnlySpan<double> source, Span<double> output, int period)
{
if (source.Length != output.Length)
throw new ArgumentException("Source and output must have the same length");
if (period <= 1)
throw new ArgumentException("Period must be greater than 1", nameof(period));
int len = source.Length;
if (len == 0) return;
int halfPeriod = period / 2;
int sqrtPeriod = (int)Math.Sqrt(period);
double[] rentedFull = System.Buffers.ArrayPool<double>.Shared.Rent(len);
Span<double> fullWma = rentedFull.AsSpan(0, len);
double[] rentedHalf = System.Buffers.ArrayPool<double>.Shared.Rent(len);
Span<double> halfWma = rentedHalf.AsSpan(0, len);
// Reuse halfWma buffer for intermediate results
Span<double> intermediate = halfWma;
try
{
Wma.Calculate(source, fullWma, period);
Wma.Calculate(source, halfWma, halfPeriod);
CalculateIntermediate(halfWma, fullWma, intermediate);
Wma.Calculate(intermediate, output, sqrtPeriod);
}
finally
{
System.Buffers.ArrayPool<double>.Shared.Return(rentedFull);
System.Buffers.ArrayPool<double>.Shared.Return(rentedHalf);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void CalculateIntermediate(ReadOnlySpan<double> halfWma, ReadOnlySpan<double> fullWma, Span<double> output)
{
int len = halfWma.Length;
int i = 0;
if (Vector256.IsHardwareAccelerated && len >= Vector256<double>.Count)
{
var vTwo = Vector256.Create(2.0);
ref double halfRef = ref MemoryMarshal.GetReference(halfWma);
ref double fullRef = ref MemoryMarshal.GetReference(fullWma);
ref double outRef = ref MemoryMarshal.GetReference(output);
for (; i <= len - Vector256<double>.Count; i += Vector256<double>.Count)
{
var vHalf = Vector256.LoadUnsafe(ref Unsafe.Add(ref halfRef, i));
var vFull = Vector256.LoadUnsafe(ref Unsafe.Add(ref fullRef, i));
// vResult = 2 * half - full
var vResult = (vHalf * vTwo) - vFull;
Vector256.StoreUnsafe(vResult, ref Unsafe.Add(ref outRef, i));
}
}
for (; i < len; i++)
{
output[i] = (2.0 * halfWma[i]) - fullWma[i];
}
}
public void Reset()
{
_wmaFull.Reset();
_wmaHalf.Reset();
_wmaSqrt.Reset();
Last = default;
}
}
+44
View File
@@ -0,0 +1,44 @@
# HMA: Hull Moving Average
[Pine Script Implementation of HMA](https://github.com/mihakralj/pinescript/blob/main/indicators/trends_FIR/hma.pine)
## Overview and Purpose
The Hull Moving Average (HMA), developed by Alan Hull in 2005, is designed to solve the age-old problem of making a moving average more responsive to current price activity while maintaining curve smoothness. It achieves this by eliminating lag almost entirely and managing to improve smoothing at the same time.
## Core Concepts
* **Lag Reduction:** Uses weighted moving averages (WMA) in a specific combination to offset lag.
* **Smoothness:** The final smoothing step ensures the indicator remains readable and not overly jittery.
* **Formula:** $HMA = WMA(\sqrt{n}, 2 \cdot WMA(n/2, price) - WMA(n, price))$
## Calculation
1. Calculate a WMA with period $n/2$ and multiply by 2.
2. Calculate a WMA with period $n$ and subtract from step 1.
3. Calculate a WMA with period $\sqrt{n}$ using the result of step 2.
## C# Implementation
```csharp
using QuanTAlib;
// Initialize
var hma = new Hma(14);
// Update
var result = hma.Update(new TValue(time, price));
// Batch
var series = Hma.Calculate(sourceSeries, 14);
```
## Performance
* **Streaming:** O(1) complexity per update (uses 3 internal O(1) WMAs).
* **Batch:** Uses SIMD-optimized WMA calculations and vector operations for the intermediate step.
* **Zero Allocation:** Span-based API available for high-performance scenarios.
## References
* [Alan Hull's HMA Description](https://alan.hull.com.au/hma.html)
+229
View File
@@ -0,0 +1,229 @@
using System;
using Xunit;
namespace QuanTAlib.Tests;
public class SmaCoverageTests
{
[Fact]
public void Sma_ResyncLogic_IsTriggeredAndCorrect()
{
// ResyncInterval is 1000.
int count = 2500;
int period = 10;
var sma = new Sma(period);
double constantValue = 100.0;
for (int i = 0; i < count; i++)
{
sma.Update(new TValue(DateTime.UtcNow, constantValue));
if (i >= period)
{
Assert.Equal(constantValue, sma.Last.Value, 1e-9);
}
}
}
[Fact]
public void Sma_SpanCalc_LargeDataset_TriggersResync()
{
int count = 5000;
int period = 10;
double[] source = new double[count];
double[] output = new double[count];
for (int i = 0; i < count; i++)
{
source[i] = 100.0;
}
Sma.Calculate(source.AsSpan(), output.AsSpan(), period);
for (int i = period; i < count; i++)
{
Assert.Equal(100.0, output[i], 1e-9);
}
}
[Fact]
public void Sma_SpanCalc_SimdThreshold_Boundary()
{
// SimdThreshold is 256.
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;
Sma.Calculate(source.AsSpan(), output.AsSpan(), period);
Assert.Equal(100.0, output[^1], 1e-9);
}
}
[Fact]
public void Sma_SpanCalc_Simd_WithResync()
{
int count = 3000;
int period = 5;
double[] source = new double[count];
double[] output = new double[count];
// Linear increase: 0, 1, 2, ...
for (int i = 0; i < count; i++) source[i] = i;
Sma.Calculate(source.AsSpan(), output.AsSpan(), period);
// SMA(5) of x-4, x-3, x-2, x-1, x
// = (5x - 10) / 5 = x - 2
for (int i = period; i < count; i++)
{
double expected = i - 2.0;
Assert.Equal(expected, output[i], 1e-9);
}
}
[Fact]
public void Sma_Constructor_ThrowsOnInvalidPeriod()
{
Assert.Throws<ArgumentException>(() => new Sma(0));
Assert.Throws<ArgumentException>(() => new Sma(-1));
}
[Fact]
public void Sma_StaticCalculate_ThrowsOnInvalidArgs()
{
double[] source = new double[10];
double[] output = new double[5]; // Mismatch
Assert.Throws<ArgumentException>(() => Sma.Calculate(source.AsSpan(), output.AsSpan(), 5));
double[] output2 = new double[10];
Assert.Throws<ArgumentException>(() => Sma.Calculate(source.AsSpan(), output2.AsSpan(), 0));
}
[Fact]
public void Sma_Calculate_EmptyInput_DoesNothing()
{
Sma.Calculate(ReadOnlySpan<double>.Empty, Span<double>.Empty, 5);
// Should not throw
}
[Fact]
public void Sma_Update_WithNaN_UsesLastValid()
{
var sma = new Sma(5);
sma.Update(new TValue(DateTime.UtcNow, 1.0));
sma.Update(new TValue(DateTime.UtcNow, 2.0));
sma.Update(new TValue(DateTime.UtcNow, double.NaN)); // Should use 2.0
// Buffer: 1, 2, 2
// SMA(3) = (1 + 2 + 2) / 3 = 5/3 = 1.666...
Assert.Equal(5.0/3.0, sma.Last.Value, 1e-9);
}
[Fact]
public void Sma_Update_IsNewFalse_UpdatesLastValue()
{
var sma = new Sma(3);
sma.Update(new TValue(DateTime.UtcNow, 1.0));
sma.Update(new TValue(DateTime.UtcNow, 2.0));
// Update existing with 3.0 (replaces 2.0)
sma.Update(new TValue(DateTime.UtcNow, 3.0), isNew: false);
// Buffer should be: 1, 3
// SMA = (1 + 3) / 2 = 2
Assert.Equal(2.0, sma.Last.Value, 1e-9);
}
[Fact]
public void Sma_TSeries_Empty_ReturnsEmpty()
{
var sma = new Sma(5);
var result = sma.Update(new TSeries());
Assert.Empty(result);
}
[Fact]
public void Sma_TSeries_WithNaN_RestoresStateCorrectly()
{
var sma = new Sma(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));
sma.Update(series);
// Buffer: 2.0, 2.0 (from NaN), 4.0
// SMA(3) = (2 + 2 + 4) / 3 = 8/3 = 2.666...
// Let's add one more value to verify state is correct
sma.Update(new TValue(DateTime.UtcNow, 5.0));
// Buffer: 2.0, 4.0, 5.0
// SMA(3) = (2 + 4 + 5) / 3 = 11/3 = 3.666...
Assert.Equal(11.0/3.0, sma.Last.Value, 1e-9);
}
[Fact]
public void Sma_Reset_ClearsState()
{
var sma = new Sma(3);
sma.Update(new TValue(DateTime.UtcNow, 1.0));
sma.Update(new TValue(DateTime.UtcNow, 2.0));
sma.Update(new TValue(DateTime.UtcNow, 3.0));
sma.Reset();
Assert.Equal(0, sma.Last.Value);
// Start fresh
sma.Update(new TValue(DateTime.UtcNow, 10.0));
// Buffer: 10
// SMA = 10
Assert.Equal(10.0, sma.Last.Value);
}
[Fact]
public void Sma_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
Sma.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 Sma_Constructor_WithSource_Subscribes()
{
var source = new Sma(10); // Just using Sma as a publisher
var sma = new Sma(source, 5);
source.Update(new TValue(DateTime.UtcNow, 10.0));
Assert.Equal(10.0, sma.Last.Value);
}
}
-285
View File
@@ -1,285 +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
# Simple Moving Average (SMA) 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 **Simple Moving Average (SMA)** is the most basic form of moving average, calculating the arithmetic mean over a specified period. Unlike the EMA, the SMA assigns equal weight to all data points in the window, making it a good baseline for trend analysis.
**Key characteristics:**
- Equal weighting for all values in the period
- O(1) update complexity using running sum
- O(1) bar correction using scalar state
- Smooth output with good noise reduction
- More lag than EMA due to equal weighting
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. **SMA vs EMA**: Comparing Simple and Exponential 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: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 SMA 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 SMA (Period 3) ---");
var smaBatch = new Sma(3);
var resultBatch = smaBatch.Update(manualData);
PrintSeries(resultBatch, 5);
// Show the calculation for each step
Console.WriteLine("\nCalculation breakdown:");
Console.WriteLine(" SMA[0] = 100 / 1 = 100.00");
Console.WriteLine(" SMA[1] = (100 + 102) / 2 = 101.00");
Console.WriteLine(" SMA[2] = (100 + 102 + 101) / 3 = 101.00");
Console.WriteLine(" SMA[3] = (102 + 101 + 103) / 3 = 102.00");
Console.WriteLine(" SMA[4] = (101 + 103 + 105) / 3 = 103.00");
#!markdown
### Streaming Processing
Streaming processing updates the SMA one data point at a time. This is essential for real-time trading systems where data arrives sequentially.
#!csharp
Console.WriteLine("\n--- Streaming SMA (Period 3) ---");
var smaStream = new Sma(3);
foreach (var item in manualData)
{
var result = smaStream.Update(item);
Console.WriteLine($"Time: {item.Time:HH:mm:ss}, Input: {item.Value:F2}, SMA: {result.Value:F2}, IsHot: {smaStream.IsHot}");
}
// Verify that the last values match
var batchLast = resultBatch.Last().Value;
var streamLast = smaStream.Value.Value;
Console.WriteLine($"\nMatch: {Math.Abs(batchLast - streamLast) < 1e-10} (Batch: {batchLast:F2}, Stream: {streamLast:F2})");
// Show SMA properties
Console.WriteLine($"\nSMA Properties:");
Console.WriteLine($" Name: {smaStream.Name}");
Console.WriteLine($" WarmupPeriod: {smaStream.WarmupPeriod}");
Console.WriteLine($" IsHot: {smaStream.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).
**SMA achieves O(1) bar correction** by saving scalar state after each `isNew=true` update.
#!csharp
Console.WriteLine("\n--- Streaming with Intra-bar Updates ---");
var smaIntra = new Sma(3);
// 1. Process the first 4 bars normally
for (int i = 0; i < 4; i++)
{
smaIntra.Update(manualData[i]);
}
Console.WriteLine($"After 4th bar: {smaIntra.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);
smaIntra.Update(update1, isNew: true); // First update for this bar is "New"
Console.WriteLine($"Update 1 (104.0): {smaIntra.Value.Value:F2}");
// Update 2: Price moves to 106.0 (Same time, same bar)
var update2 = new TValue(manualData[4].Time, 106.0);
smaIntra.Update(update2, isNew: false); // Not new, just an update
Console.WriteLine($"Update 2 (106.0): {smaIntra.Value.Value:F2}");
// Update 3: Final Close at 105.0
var update3 = manualData[4];
smaIntra.Update(update3, isNew: false); // Final update
Console.WriteLine($"Update 3 (105.0): {smaIntra.Value.Value:F2}");
// Verify match with batch result
Console.WriteLine($"Match with Batch: {Math.Abs(smaIntra.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 smaLargeBatch = new Sma(20);
var batchLargeResult = smaLargeBatch.Update(closeSeries);
Console.WriteLine($"Batch Last Value: {batchLargeResult.Last().Value:F2}");
// Streaming
var smaLargeStream = new Sma(20);
TValue lastStreamVal = default;
foreach(var item in closeSeries)
{
lastStreamVal = smaLargeStream.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
## 4. Handling Invalid Values (NaN/Infinity)
`Sma` 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 SMA
var smaNaN = new Sma(10);
// Feed valid values first
smaNaN.Update(new TValue(DateTime.Now, 100.0));
smaNaN.Update(new TValue(DateTime.Now.AddMinutes(1), 110.0));
Console.WriteLine($"After valid values: {smaNaN.Value.Value:F2}");
// Feed NaN - should use last valid value (110)
var resultAfterNaN = smaNaN.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 = smaNaN.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 = smaNaN.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 smaBatchNaN = new Sma(3);
var resultsWithNaN = smaBatchNaN.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)})");
}
#!markdown
## 5. SMA vs EMA Comparison
The SMA and EMA are both trend-following indicators, but they weight data differently:
- **SMA**: Equal weight to all values in the window
- **EMA**: More weight to recent values (exponentially decreasing)
#!csharp
Console.WriteLine("\n--- 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 smaCompare = new Sma(10);
var emaCompare = new Ema(10);
Console.WriteLine("Position | Input | SMA | EMA | Difference");
Console.WriteLine("---------+--------+---------+---------+-----------");
for (int i = 0; i < compareData.Count; i++)
{
var smaVal = smaCompare.Update(compareData[i]);
var emaVal = emaCompare.Update(compareData[i]);
var input = compareData[i].Value;
var diff = smaVal.Value - emaVal.Value;
Console.WriteLine($" {i,2} | {input,6:F0} | {smaVal.Value,7:F2} | {emaVal.Value,7:F2} | {diff,+9:F2}");
}
Console.WriteLine("\nNote: After the spike (position 10), EMA reacts faster due to higher weight on recent values.");
Console.WriteLine("SMA takes longer to reflect changes as all values have equal weight.");
+35 -30
View File
@@ -108,25 +108,13 @@ public sealed class Sma : ITValuePublisher
}
// Removed GetValidValue and UpdateState as they are not used in the new Update logic
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TValue Update(TValue input, bool isNew = true)
{
if (isNew)
{
double val = GetValidValue(input.Value);
double removedValue = _buffer.Count == _buffer.Capacity ? _buffer.Oldest : 0.0;
_sum = _sum - removedValue + val;
_buffer.Add(val);
_tickCount++;
if (_buffer.IsFull && _tickCount >= ResyncInterval)
{
_tickCount = 0;
_sum = _buffer.Sum();
}
UpdateState(val);
_p_sum = _sum;
_p_lastInput = val;
@@ -136,7 +124,7 @@ public sealed class Sma : ITValuePublisher
{
_lastValidValue = _p_lastValidValue;
double val = GetValidValue(input.Value);
_sum = _p_sum - _p_lastInput + val;
_buffer.UpdateNewest(val);
}
@@ -150,7 +138,7 @@ public sealed class Sma : ITValuePublisher
public TSeries Update(TSeries source)
{
if (source.Count == 0) return new TSeries(new List<long>(), new List<double>());
int len = source.Count;
var t = new List<long>(len);
var v = new List<double>(len);
@@ -159,26 +147,43 @@ public sealed class Sma : ITValuePublisher
var tSpan = CollectionsMarshal.AsSpan(t);
var vSpan = CollectionsMarshal.AsSpan(v);
var sourceValues = source.Values;
var sourceTimes = source.Times;
// Reset state for batch calculation
Reset();
// We can optimize this later with specific batch logic, but for now use core loop
for(int i=0; i < len; i++)
Calculate(source.Values, vSpan, _period);
source.Times.CopyTo(tSpan);
// Restore state
int windowSize = Math.Min(len, _period);
int startIndex = len - windowSize;
if (startIndex > 0)
{
double val = GetValidValue(sourceValues[i]);
double removedValue = _buffer.Count == _buffer.Capacity ? _buffer.Oldest : 0.0;
_sum = _sum - removedValue + val;
_buffer.Add(val);
vSpan[i] = _sum / _buffer.Count;
for (int i = startIndex - 1; i >= 0; i--)
{
if (double.IsFinite(source.Values[i]))
{
_lastValidValue = source.Values[i];
break;
}
}
}
else
{
_lastValidValue = 0;
}
_buffer.Clear();
_sum = 0;
_tickCount = 0;
for (int i = startIndex; i < len; i++)
{
double val = GetValidValue(source.Values[i]);
UpdateState(val);
}
sourceTimes.CopyTo(tSpan);
_p_lastValidValue = _lastValidValue;
_p_sum = _sum;
_p_lastInput = sourceValues[len-1];
_p_lastInput = source.Values[len - 1];
_p_lastValidValue = _lastValidValue;
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
return new TSeries(t, v);
+28
View File
@@ -125,6 +125,34 @@ sma.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 sma = new Sma(source, period: 10);
// 3. Optional: Subscribe to indicator's output
sma.Pub += (item) => Console.WriteLine($"SMA Updated: {item.Value}");
// 4. Ingest data into source
// This triggers the chain: source -> sma -> 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)
`Sma` uses **last-value substitution** for handling invalid inputs:
-219
View File
@@ -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
# Triple Exponential Moving Average (TEMA) 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 TEMA indicator, including mathematical formulas and interpretation, please refer to [Tema.md](Tema.md).
The **Triple Exponential Moving Average (TEMA)** is a technical indicator designed to reduce the lag associated with traditional moving averages even further than DEMA. It combines single, double, and triple EMAs to achieve superior responsiveness.
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 TEMA 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 TEMA (Period 3) ---");
var temaBatch = new Tema(3);
var resultBatch = temaBatch.Update(manualData);
PrintSeries(resultBatch, 5);
#!markdown
### Streaming Processing
Streaming processing updates the TEMA one data point at a time. This is essential for real-time trading systems where data arrives sequentially.
#!csharp
Console.WriteLine("\n--- Streaming TEMA (Period 3) ---");
var temaStream = new Tema(3);
foreach (var item in manualData)
{
var result = temaStream.Update(item);
Console.WriteLine($"Time: {item.Time:HH:mm:ss}, Input: {item.Value:F2}, TEMA: {result.Value:F2}, IsHot: {temaStream.IsHot}");
}
// Verify that the last values match
var batchLast = resultBatch.Last().Value;
var streamLast = temaStream.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 temaIntra = new Tema(3);
// 1. Process the first 4 bars normally
for (int i = 0; i < 4; i++)
{
temaIntra.Update(manualData[i]);
}
Console.WriteLine($"After 4th bar: {temaIntra.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);
temaIntra.Update(update1, isNew: true); // First update for this bar is "New"
Console.WriteLine($"Update 1 (104.0): {temaIntra.Value.Value:F2}");
// Update 2: Price moves to 106.0 (Same time, same bar)
var update2 = new TValue(manualData[4].Time, 106.0);
temaIntra.Update(update2, isNew: false); // Not new, just an update
Console.WriteLine($"Update 2 (106.0): {temaIntra.Value.Value:F2}");
// Update 3: Final Close at 105.0
var update3 = manualData[4];
temaIntra.Update(update3, isNew: false); // Final update
Console.WriteLine($"Update 3 (105.0): {temaIntra.Value.Value:F2}");
// Verify match with batch result
Console.WriteLine($"Match with Batch: {Math.Abs(temaIntra.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 temaLargeBatch = new Tema(20);
var batchLargeResult = temaLargeBatch.Update(closeSeries);
Console.WriteLine($"Batch Last Value: {batchLargeResult.Last().Value:F2}");
// Streaming
var temaLargeStream = new Tema(20);
TValue lastStreamVal = default;
foreach(var item in closeSeries)
{
lastStreamVal = temaLargeStream.Update(item);
}
Console.WriteLine($"Streaming Last Value: {lastStreamVal.Value:F2}");
#!markdown
## 4. Handling Invalid Values (NaN/Infinity)
`Tema` 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 TEMA
var temaNaN = new Tema(10);
// Feed valid values first
temaNaN.Update(new TValue(DateTime.Now, 100.0));
temaNaN.Update(new TValue(DateTime.Now.AddMinutes(1), 110.0));
Console.WriteLine($"After valid values: {temaNaN.Value.Value:F2}");
// Feed NaN - should use last valid value (110)
var resultAfterNaN = temaNaN.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 = temaNaN.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 = temaNaN.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 temaBatchNaN = new Tema(3);
var resultsWithNaN = temaBatchNaN.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)})");
}
+28
View File
@@ -82,6 +82,34 @@ double[] temaOutput = new double[200000];
Tema.Calculate(source.AsSpan(), temaOutput.AsSpan(), period: 50);
```
### 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 tema = new Tema(source, period: 14);
// 3. Optional: Subscribe to indicator's output
tema.Pub += (item) => Console.WriteLine($"TEMA Updated: {item.Value}");
// 4. Ingest data into source
// This triggers the chain: source -> tema -> 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
`Tema` delegates value handling to the underlying `Ema` instances, which use **last-value substitution** for `NaN` or `Infinity`. This ensures continuity and stability in the output series.
-4
View File
@@ -1,4 +0,0 @@
# todo
- __KAMA__ (Kaufman Adaptive Moving Average)
- __T3__ (T3)
-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:
+276
View File
@@ -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);
}
}
-323
View File
@@ -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.");
+28
View File
@@ -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:
+142
View File
@@ -0,0 +1,142 @@
| **Indicator Name** | **Libraries** |
| ---------------------------------------------------------------------------- | --------------------------------------------- |
| **Aroon** measures trend strength (Aroon Up/Down) | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Average True Range (ATR)** volatility measure | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Balance of Power (BOP)** momentum indicator | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Bollinger Bands** volatility bands around moving average | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Chaikin Oscillator** (Accumulation/Distribution Osc) volume momentum | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Chande Momentum Oscillator (CMO)** momentum indicator | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Commodity Channel Index (CCI)** deviation from mean price | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Double Exponential Moving Average (DEMA)** a smoother EMA | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Exponential Moving Average (EMA)** weighted moving average | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Linear Regression** (Line of Best Fit) trend line value | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Momentum** (Rate of Change) price change over period | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Money Flow Index (MFI)** volume-weighted RSI | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Moving Average Convergence Divergence (MACD)** trend/momentum oscillator | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Normalized ATR (NATR)** ATR normalized to price | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Parabolic SAR** stop-and-reverse trend indicator | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Relative Strength Index (RSI)** momentum oscillator | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Simple Moving Average (SMA)** arithmetic moving average | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Stochastic Oscillator** (Stoch) %K and %D oscillators | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Stochastic RSI** RSI applied to stochastic formula | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Triple Exponential Moving Average (T3)** Tillsons T3 MA | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **TRIX** (Triple EMA Oscillator) triple EMA rate-of-change | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **True Range (TR)** high/low range measure | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Ultimate Oscillator** multi-period oscillator | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Weighted Moving Average (WMA)** volume/point-weighted MA | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Williams %R** Larry Williams overbought/oversold oscillator | TA-Lib, Tulip, Skender, PineScript, Pandas-TA |
| **Indicator Name** | **Libraries** |
| --------------------------------------------------------------------- | ------------------------------------------------------- |
| **Arnaud Legoux Moving Average (ALMA)** smoothing MA | Skender, PineScript, Pandas-TA (not in TA-Lib or Tulip) |
| **Average Directional Index (ADX)** smoothed DMI oscillator | TA-Lib, Skender, PineScript (not in Tulip or Pandas-TA) |
| **Average Directional Movement Index Rating (ADXR)** lagged ADX | TA-Lib, Tulip, PineScript (not in Skender or Pandas-TA) |
| **Beta Coefficient** Beta (relative volatility vs market) | TA-Lib, Skender, PineScript (not in Tulip or Pandas-TA) |
| **Chaikin Money Flow (CMF)** volume flow oscillator | Skender, PineScript, Pandas-TA (not in TA-Lib or Tulip) |
| **Choppiness Index** market choppiness (trend/no-trend) | Skender, PineScript, Pandas-TA (not in TA-Lib or Tulip) |
| **Ease of Movement (EMV)** price/volume change oscillator | Tulip, PineScript, Pandas-TA (not in TA-Lib or Skender) |
| **Hull Moving Average (HMA)** fast smoothing MA | Tulip, PineScript, Pandas-TA (not in TA-Lib or Skender) |
| **Kaufmans Adaptive Moving Average (KAMA)** volatility-adaptive MA | TA-Lib, PineScript, Pandas-TA (not in Tulip or Skender) |
| **Klinger Volume Oscillator (KVO)** volume force oscillator | Tulip, Skender, PineScript (not in TA-Lib or Pandas-TA) |
| **Mass Index** price range compression index | Tulip, PineScript, Pandas-TA (not in TA-Lib or Skender) |
| **Negative Volume Index (NVI)** volume-based index | Tulip, PineScript, Pandas-TA (not in TA-Lib or Skender) |
| **On-Balance Volume (OBV)** cumulative volume | TA-Lib, Skender, Pandas-TA (not in Tulip or PineScript) |
| **Positive Volume Index (PVI)** volume-based index | Tulip, PineScript, Pandas-TA (not in TA-Lib or Skender) |
| **Qstick** price change oscillator (candlestick average) | Tulip, PineScript, Skender (not in TA-Lib or Pandas-TA) |
| **Schaff Trend Cycle** cycle oscillator by Schaff | Skender, PineScript, Pandas-TA (not in TA-Lib or Tulip) |
| **Smoothed Moving Average (SMMA)** Wilders smoothing (RMA) | Skender, PineScript, Pandas-TA (not in TA-Lib or Tulip) |
| **SuperTrend** ATR-based trend indicator | Skender, PineScript, Pandas-TA (not in TA-Lib or Tulip) |
| **Volatility (Historical Volatility)** statistical volatility | Tulip, PineScript, Pandas-TA (not in TA-Lib or Skender) |
| **Volume Oscillator (PVO/VOSC)** difference in EMAs of volume | Tulip, PineScript, Pandas-TA (not in TA-Lib or Skender) |
| **Williams Alligator** Bill Williams Alligator (3 MAs) | Skender, PineScript, Tulip (not in TA-Lib or Pandas-TA) |
| **Williams Fractal** Bill Williams fractal pattern | Skender, PineScript, Pandas-TA (not in TA-Lib or Tulip) |
| **Indicator Name** | **Libraries** |
| ------------------------------------------------------------------------------------------------------- | --------------------- |
| **ATR Trailing Stop** ATR-based stop indicator | Skender, PineScript |
| **Aberration** trend-following band indicator | PineScript, Pandas-TA |
| **Acceleration Bands** Bollinger-type bands by Price Headley | PineScript, Pandas-TA |
| **Archer Moving Averages Trends (AMAT)** Archers composite trend MA | PineScript, Pandas-TA |
| **Archer On-Balance Volume (AOBV)** Archers OBV variation | PineScript, Pandas-TA |
| **BRAR** Bull Ratio & Bear Ratio indicator | PineScript, Pandas-TA |
| **Bias (BIAS)** price bias from MA (percentage) | PineScript, Pandas-TA |
| **Bull and Bear Power** Elders bull power and bear power | Skender, PineScript |
| **Center of Gravity** Ehlers center-of-gravity oscillator | PineScript, Pandas-TA |
| **Chande Forecast Oscillator (CFO)** deviation from linear regression forecast | PineScript, Pandas-TA |
| **Chande Kroll Stop (CKSP)** volatility stop by Chande & Kroll | Skender, Pandas-TA |
| **Chandelier Exit** ATR-based stop by Chuck LeBeau | Skender, PineScript |
| **ConnorsRSI** Connors 3-component RSI | Skender, PineScript |
| **Dominant Cycle Periods** dominant cycle period (Ehlers) | Skender, Pandas-TA |
| **Elder-ray Index (ERI)** measures bull and bear pressure | Skender, Pandas-TA |
| **Endpoint Moving Average (EPMA)** end-point linear regression MA | Skender, PineScript |
| **Entropy** Shannon entropy of returns | PineScript, Pandas-TA |
| **Even Better Sinewave (EBSW)** improved MESA cycle indicator | PineScript, Pandas-TA |
| **Fractal Chaos Bands** price bands using fractal geometry | Skender, PineScript |
| **Gator Oscillator** Bill Williams Gator (Alligator derivative) | Skender, PineScript |
| **Gann High-Low Activator (HiLo)** trend indicator by Gann | PineScript, Pandas-TA |
| **Heikin-Ashi** (HA candles) averaged candlestick values | Skender, Pandas-TA |
| **Historical Volatility (HV)** statistical volatility (std dev) | Skender, PineScript |
| **Holt-Winter Moving Average (HWMA)** Holt-Winters double EMA | PineScript, Pandas-TA |
| **Inertia** RSI-based trend inertia indicator | PineScript, Pandas-TA |
| **Increasing/Decreasing** price increase/decrease streak | PineScript, Pandas-TA |
| **Ichimoku Cloud** (Ichimoku Kinkō Hyō) five-line system | Skender, Pandas-TA |
| **Least Squares Moving Average (LSMA)** linear regression line as MA | Skender, PineScript |
| **Long Run / Short Run** long-term and short-term trend lines | PineScript, Pandas-TA |
| **Market Facilitation Index (MFI)** volume-price efficiency (Bill Williams) | Tulip, PineScript |
| **McGinley Dynamic** adaptive moving average by McGinley | Skender, Pandas-TA |
| **Median Price** (High+Low)/2 series | TA-Lib, Tulip |
| **Modified Moving Average (MMA)** arithmetic moving average variant | Skender, PineScript |
| **Momentum Oscillator** (alternate term for Momentum) | *See Momentum above* |
| **Pretty Good Oscillator (PGO)** distance from EMA in std dev | PineScript, Pandas-TA |
| **Price Channels** highest high/lowest low channel | Skender, PineScript |
| **Price Distance (PDIST)** distance of price from MA | PineScript, Pandas-TA |
| **Price Momentum Oscillator (PMO)** Tushar Chandes momentum osc | Skender, PineScript |
| **Price Relative Strength (PRS)** ratio of asset to benchmark | Skender, PineScript |
| **Pivot Points** (Floor pivots) support/resistance levels | Skender, PineScript |
| **Rolling Pivot Points** continuously updated pivots | Skender, PineScript |
| **Rescaled Range (R/S) Analysis** Hurst exponent calculation | Skender, PineScript |
| **Relative Vigor Index (RVI)** oscillator of confirmation | PineScript, Pandas-TA |
| **Schaff Trend Cycle** *(see above in 3-library list)* | |
| **Sine Weighted MA (SINWMA)** sine-weighted moving average | PineScript, Pandas-TA |
| **Slope** (Linear Regression Slope) slope of trendline | TA-Lib, Pandas-TA |
| **Standard Error** (of price) std error over period | Tulip, Pandas-TA |
| **Super Smoother Filter (SSF)** Ehlers low-pass filter | PineScript, Pandas-TA |
| **Summation (SUM)** cumulative sum over period | TA-Lib, Tulip |
| **TTM Trend** Trend indicator from TradeTheMarkets | PineScript, Pandas-TA |
| **Typical Price** (H+L+C)/3 series | TA-Lib, Tulip |
| **Ulcer Index (UI)** drawdown volatility measure | Skender, Pandas-TA |
| **Vertical Horizontal Filter (VHF)** trend noise filter | Tulip, PineScript |
| **Volatility Stop** ATR-based stop indicator | Skender, PineScript |
| **Volume Weighted Average Price (VWAP)** price weighted by volume | Skender, Pandas-TA |
| **Volume Profile (VP)** volume distribution by price (histogram) | PineScript, Pandas-TA |
| **Williams %R (percent Range)** *listed above in 5-library list* | |
| **Zig Zag** filtered price swings (visual aid) | Skender, PineScript |
| **Indicator Name** | **Library** |
| ----------------------------------------------------------------------------- | -------------- |
| **Hilbert Transform Dominant Cycle Period** Ehlers cycle period | TA-Lib (only) |
| **Hilbert Transform Dominant Cycle Phase** Ehlers cycle phase | TA-Lib (only) |
| **Hilbert Transform Phasor Components** In-phase/quadrature components | TA-Lib (only) |
| **Hilbert Transform SineWave** Ehlers sine/cosine of cycle | TA-Lib (only) |
| **Hilbert Transform Instantaneous Trendline** Ehlers IMAT | TA-Lib (only) |
| **Hilbert Transform Trend vs Cycle Mode** cycle/trend discrimination | TA-Lib (only) |
| **Highest/Lowest values over period (MIN/MAX)** period extrema | TA-Lib (only) |
| **Index of Highest/Lowest value (MININDEX/MAXINDEX)** extrema index | TA-Lib (only) |
| **Lowest & Highest values (MINMAX)** both extrema in one output | TA-Lib (only) |
| **Pearsons Correlation Coefficient (CORREL)** correlation of two series | TA-Lib (only) |
| **Linear Regression Intercept** intercept of best-fit line | TA-Lib (only) |
| **Linear Regression Angle** angle of best-fit line | TA-Lib (only) |
| **Time Series Forecast (TSF)** forecast of next value via LR | TA-Lib (only) |
| **Vector Trigonometric Functions** (SIN, COS, TAN, etc) elementwise math | Tulip (only) |
| **Vector Arithmetic Ops** (ADD, SUB, MUL, DIV, etc) elementwise math | Tulip (only) |
| **Vector Log, Exp, etc** elementwise transforms (LN, EXP, etc) | Tulip (only) |
| **Crossovers** (Crossany/Crossover) series cross above/below logic | Tulip (only) |
| **Mean Deviation (MD)** mean absolute deviation | Tulip (only) |
| **Standard Error (STDERR)** std error of values | Tulip (only) |
| **Decay** (Linear/Exponential) value decay over time | Tulip (only) |
| **Moving Average Envelopes** percentage envelopes around MA | Skender (only) |
| **Donchian Channels** high/low channel over *n* periods | Skender (only) |
| **Fractal Chaos Bands** *(also listed in 2-library category: Pine)* | |
| **Schaff Trend Cycle** *(also listed in 3-library category)* | |
| **ConnorsRSI** *(if PineScript not included; otherwise 2 libraries)* | |