2025-11-29 19:31:50 -08:00
#!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.
2025-12-04 13:49:05 -08:00
4. **Handling Invalid Values**: Last-value substitution for NaN/Infinity.
2025-11-29 19:31:50 -08:00
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
2025-12-04 13:49:05 -08:00
## 4. Handling Invalid Values (NaN/Infinity)
2025-11-29 19:31:50 -08:00
2025-12-04 13:49:05 -08:00
`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.
2025-11-29 19:31:50 -08:00
#!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
2025-12-04 13:49:05 -08:00
## 5. WMA vs SMA vs EMA Comparison
2025-11-29 19:31:50 -08:00
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
2025-12-04 13:49:05 -08:00
## 6. WMA Weights More Recent Values
2025-11-29 19:31:50 -08:00
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.");