Files
QuanTAlib/lib/averages/wma/Wma.Notebook.dib
T
Miha Kralj 5c1fb18520 Implement Weighted Moving Average (WMA) and Multi-Period WMA (WmaVector) classes with O(1) update complexity
- Added Wma class for calculating the Weighted Moving Average with detailed documentation and optimized performance using dual running sums.
- Introduced WmaVector class to handle multiple WMAs simultaneously, supporting batch calculations and real-time updates.
- Implemented last-value substitution for handling invalid inputs (NaN/Infinity) in both classes.
- Created comprehensive unit tests for Wma and WmaVector to ensure accuracy and reliability of calculations.
- Updated documentation to include usage examples, mathematical foundations, and performance characteristics.
2025-11-29 19:31:50 -08:00

378 lines
13 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!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. **Vectorized Operations**: Calculating multiple WMAs simultaneously.
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. Vectorized WMA (Multiple Periods)
`WmaVector` allows calculating multiple WMAs (e.g., 5, 10, 20) simultaneously. This is useful for comparing different timeframes.
### Vectorized Batch
#!csharp
int[] periods = { 5, 10, 20 };
Console.WriteLine($"\n--- Vectorized Batch WMA (Periods: {string.Join(", ", periods)}) ---");
var wmaVectorBatch = new WmaVector(periods);
var vectorBatchResults = wmaVectorBatch.Calculate(closeSeries);
for (int i = 0; i < periods.Length; i++)
{
Console.WriteLine($"WMA({periods[i]}) Last Value: {vectorBatchResults[i].Last().Value:F4}");
}
#!markdown
### Vectorized Streaming
#!csharp
Console.WriteLine($"\n--- Vectorized Streaming WMA (Periods: {string.Join(", ", periods)}) ---");
var wmaVectorStream = new WmaVector(periods);
TValue[] lastVectorVal = null;
foreach(var item in closeSeries)
{
lastVectorVal = wmaVectorStream.Update(item);
}
for (int i = 0; i < periods.Length; i++)
{
Console.WriteLine($"WMA({periods[i]}) Last Value: {lastVectorVal[i].Value:F4}");
}
// Verification
bool allMatch = true;
for (int i = 0; i < periods.Length; i++)
{
if (Math.Abs(vectorBatchResults[i].Last().Value - lastVectorVal[i].Value) > 1e-10)
{
allMatch = false;
break;
}
}
Console.WriteLine($"\nAll Vectorized Stream/Batch values match: {allMatch}");
#!markdown
## 5. Handling Invalid Values (NaN/Infinity)
Both `Wma` and `WmaVector` use **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
## 6. 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
## 7. 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.");