mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-18 10:38:05 +00:00
Add PWMA implementation and tests; enhance documentation
This commit is contained in:
+28
-23
@@ -4,7 +4,9 @@ Trend indicators help identify the direction and strength of a market trend. Mov
|
||||
|
||||
| Indicator | Full Name | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| [ALMA](trends/alma/Alma.md) | Arnaud Legoux MA | Uses Gaussian distribution weights to balance smoothness and responsiveness. |
|
||||
| ALLIGATOR | Williams Alligator | |
|
||||
| [ALMA](alma/Alma.md) | Arnaud Legoux MA | Uses Gaussian distribution weights to balance smoothness and responsiveness. |
|
||||
| AMAT | Archer Moving Averages Trends | |
|
||||
| BESSEL | Bessel Filter | |
|
||||
| BILATERAL | Bilateral Filter | |
|
||||
| BLMA | Blackman Window MA | |
|
||||
@@ -13,54 +15,57 @@ Trend indicators help identify the direction and strength of a market trend. Mov
|
||||
| BWMA | Bessel-Weighted MA | |
|
||||
| CHEBY1 | Chebyshev Type I Filter | |
|
||||
| CHEBY2 | Chebyshev Type II Filter | |
|
||||
| [CONV](trends/conv/Conv.md) | Convolution Indicator | Applies a custom kernel (weights) to the data window. |
|
||||
| [DEMA](trends/dema/Dema.md) | Double Exponential Moving Average | Reduces lag by placing more weight on recent data than a standard EMA. |
|
||||
| [CONV](conv/Conv.md) | Convolution MA with any kernel | Applies a custom kernel (weights) to the data window. |
|
||||
| [DEMA](dema/Dema.md) | Double Exponential MA | Reduces lag by placing more weight on recent data than a standard EMA. |
|
||||
| DSMA | Deviation-Scaled MA | |
|
||||
| [DWMA](trends/dwma/Dwma.md) | Double Weighted MA | Applies WMA smoothing twice to reduce noise further. |
|
||||
| [DWMA](dwma/Dwma.md) | Double Weighted MA | Applies WMA smoothing twice to reduce noise further. |
|
||||
| ELLIPTIC | Elliptic (Cauer) Filter | |
|
||||
| [EMA](trends/ema/Ema.md) | Exponential Moving Average | Weighted average giving more importance to recent price data. |
|
||||
| [EMA](ema/Ema.md) | Exponential MA | Weighted average giving more importance to recent price data. |
|
||||
| EPMA | Endpoint MA | |
|
||||
| FRAMA | Fractal Adaptive MA | |
|
||||
| FRAMA | Ehlers Fractal Adaptive MA | |
|
||||
| GAUSS | Gaussian Filter | |
|
||||
| GWMA | Gaussian-Weighted MA | |
|
||||
| HAMMA | Hamming Window MA | |
|
||||
| HANN | Hann FIR Filter | |
|
||||
| HANMA | Hanning Window MA | |
|
||||
| HANN | Hann FIR Filter | |
|
||||
| HEMA | Hull Exponential MA | |
|
||||
| [HMA](trends/hma/Hma.md) | Hull Moving Average | Developed by Alan Hull to reduce lag while improving smoothing. |
|
||||
| [HMA](hma/Hma.md) | Hull MA | Developed by Alan Hull to reduce lag while improving smoothing. |
|
||||
| HP | Hodrick-Prescott Filter | |
|
||||
| HPF | Ehlers Highpass Filter | |
|
||||
| HTIT | Hilbert Transform Instantaneous Trend | |
|
||||
| HTIT | Ehlers Hilbert Transform Instantaneous Trend | |
|
||||
| HT_TRENDMODE | Ehlers Hilbert Transform Trend Mode | |
|
||||
| HWMA | Holt Weighted MA | |
|
||||
| [JMA](trends/jma/Jma.md) | Jurik MA | Adaptive moving average that adjusts to market volatility for superior smoothing with minimal lag. |
|
||||
| [KAMA](trends/kama/Kama.md) | Kaufman Adaptive MA | Adapts to market volatility by adjusting its smoothing factor based on an Efficiency Ratio. |
|
||||
| ICHIMOKU | Ichimoku Cloud | |
|
||||
| [JMA](jma/Jma.md) | Jurik MA | Adaptive moving average that adjusts to market volatility for superior smoothing with minimal lag. |
|
||||
| [KAMA](kama/Kama.md) | Kaufman Adaptive MA | Adapts to market volatility by adjusting its smoothing factor based on an Efficiency Ratio. |
|
||||
| KF | Kalman Filter | |
|
||||
| LOESS | LOESS/LOWESS Smoothing | |
|
||||
| [LSMA](trends/lsma/Lsma.md) | Least Squares MA | Calculates the linear regression line for a specified period. |
|
||||
| [LSMA](lsma/Lsma.md) | Least Squares Moving Average | Calculates the linear regression line for a specified period. |
|
||||
| LTMA | Linear Trend MA | |
|
||||
| [MAMA](trends/mama/Mama.md) | MESA Adaptive MA | Adapts to market cycles using Hilbert Transform phase measurement. |
|
||||
| MEDIAN | Median Filter | |
|
||||
| [MAMA](mama/Mama.md) | Ehlers MESA Adaptive MA | Adapts to market cycles using Hilbert Transform phase measurement. |
|
||||
| MGDI | McGinley Dynamic Indicator | |
|
||||
| MMA | Modified MA | |
|
||||
| NOTCH | Notch Filter | |
|
||||
| PWMA | Pascal Weighted MA | |
|
||||
| [PWMA](pwma/Pwma.md) | Parabolic Weighted MA | Uses parabolic weighting ($i^2$) to give more weight to recent data. |
|
||||
| QEMA | Quadruple Exponential MA | |
|
||||
| REMA | Regularized Exponential MA | |
|
||||
| RGMA | Recursive Gaussian MA | |
|
||||
| [RMA](trends/rma/Rma.md) | wildeR MA (SMMA, MMA) | Exponential moving average with alpha = 1/N. |
|
||||
| [RMA](rma/Rma.md) | wildeR MA (SMMA, MMA) | Exponential moving average with alpha = 1/N. |
|
||||
| SGF | Savitzky-Golay Filter | |
|
||||
| SGMA | Savitzky-Golay MA | |
|
||||
| SINEMA | Sine-weighted MA | |
|
||||
| [SMA](trends/sma/Sma.md) | Simple Moving Average | The unweighted mean of the previous n data. |
|
||||
| [SMA](sma/Sma.md) | Simple MA | The unweighted mean of the previous n data. |
|
||||
| SSF | Ehlers Super Smooth Filter | |
|
||||
| [T3](trends/t3/T3.md) | Tillson T3 Moving Average | A smooth moving average that uses a smoothing factor to reduce lag. |
|
||||
| [TEMA](trends/tema/Tema.md) | Triple Exponential Moving Average | Designed to smooth price fluctuations and filter out volatility. |
|
||||
| [TRIMA](trends/trima/Trima.md) | Triangular Moving Average | A double-smoothed SMA that gives more weight to the middle of the data window. |
|
||||
| SUPER | SuperTrend | |
|
||||
| [T3](t3/T3.md) | Tillson T3 MA | A smooth moving average that uses a smoothing factor to reduce lag. |
|
||||
| [TEMA](tema/Tema.md) | Triple Exponential MA | Designed to smooth price fluctuations and filter out volatility. |
|
||||
| [TRIMA](trima/Trima.md) | Triangular MA | A double-smoothed SMA that gives more weight to the middle of the data window. |
|
||||
| TTM | TTM Trend | |
|
||||
| USF | Ehlers Ultrasmooth Filter | |
|
||||
| VAMA | Volatility Adjusted MA | |
|
||||
| [VIDYA](trends/vidya/Vidya.md) | Variable Index Dynamic Average | Adapts smoothing based on volatility using the Chande Momentum Oscillator (CMO). |
|
||||
| VAMA | Volatility Adjusted Moving Average | |
|
||||
| [VIDYA](vidya/Vidya.md) | Variable Index Dynamic Average | Adapts smoothing based on volatility using the Chande Momentum Oscillator (CMO). |
|
||||
| WIENER | Wiener Filter | |
|
||||
| [WMA](trends/wma/Wma.md) | Weighted Moving Average | Assigns a heavier weighting to more current data points since they are more relevant. |
|
||||
| [WMA](wma/Wma.md) | Weighted MA | Assigns a heavier weighting to more current data points since they are more relevant. |
|
||||
| YZVAMA | Yang-Zhang Volatility Adjusted MA | |
|
||||
| ZLDEMA | Zero-Lag Double Exponential MA | |
|
||||
| ZLEMA | Zero-Lag Exponential MA | |
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
using Xunit;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
using QuanTAlib;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class PwmaIndicatorTests
|
||||
{
|
||||
[Fact]
|
||||
public void PwmaIndicator_Constructor_SetsDefaults()
|
||||
{
|
||||
var indicator = new PwmaIndicator();
|
||||
|
||||
Assert.Equal(14, indicator.Period);
|
||||
Assert.Equal(SourceType.Close, indicator.Source);
|
||||
Assert.True(indicator.ShowColdValues);
|
||||
Assert.Equal("PWMA - Parabolic Weighted Moving Average", indicator.Name);
|
||||
Assert.False(indicator.SeparateWindow);
|
||||
Assert.True(indicator.OnBackGround);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PwmaIndicator_MinHistoryDepths_EqualsPeriod()
|
||||
{
|
||||
var indicator = new PwmaIndicator { Period = 20 };
|
||||
|
||||
Assert.Equal(20, indicator.MinHistoryDepths);
|
||||
IWatchlistIndicator watchlistIndicator = indicator;
|
||||
Assert.Equal(20, watchlistIndicator.MinHistoryDepths);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PwmaIndicator_ShortName_IncludesPeriodAndSource()
|
||||
{
|
||||
var indicator = new PwmaIndicator { Period = 15 };
|
||||
|
||||
Assert.Contains("PWMA", indicator.ShortName);
|
||||
Assert.Contains("15", indicator.ShortName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PwmaIndicator_SourceCodeLink_IsValid()
|
||||
{
|
||||
var indicator = new PwmaIndicator();
|
||||
|
||||
Assert.Contains("github.com", indicator.SourceCodeLink);
|
||||
Assert.Contains("Pwma.Quantower.cs", indicator.SourceCodeLink);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PwmaIndicator_Initialize_CreatesInternalPwma()
|
||||
{
|
||||
var indicator = new PwmaIndicator { Period = 10 };
|
||||
|
||||
// Initialize should not throw
|
||||
indicator.Initialize();
|
||||
|
||||
// After init, line series should exist
|
||||
Assert.Single(indicator.LinesSeries);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PwmaIndicator_ProcessUpdate_HistoricalBar_ComputesValue()
|
||||
{
|
||||
var indicator = new PwmaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
// Add historical data
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
// Process update
|
||||
var args = new UpdateArgs(UpdateReason.HistoricalBar);
|
||||
indicator.ProcessUpdate(args);
|
||||
|
||||
// Line series should have a value
|
||||
Assert.Equal(1, indicator.LinesSeries[0].Count);
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PwmaIndicator_ProcessUpdate_NewBar_ComputesValue()
|
||||
{
|
||||
var indicator = new PwmaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
indicator.HistoricalData.AddBar(now.AddMinutes(1), 102, 108, 100, 106);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewBar));
|
||||
|
||||
Assert.Equal(2, indicator.LinesSeries[0].Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PwmaIndicator_ProcessUpdate_NewTick_ProcessesWithoutError()
|
||||
{
|
||||
var indicator = new PwmaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 105, 95, 102);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
double firstValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.NewTick));
|
||||
double secondValue = indicator.LinesSeries[0].GetValue(0);
|
||||
|
||||
Assert.True(double.IsFinite(firstValue));
|
||||
Assert.True(double.IsFinite(secondValue));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PwmaIndicator_OnPaintChart_DoesNotThrow()
|
||||
{
|
||||
var indicator = new PwmaIndicator();
|
||||
indicator.Initialize();
|
||||
|
||||
var method = indicator.GetType().GetMethod("OnPaintChart");
|
||||
Assert.NotNull(method);
|
||||
Assert.Equal(typeof(PwmaIndicator), method.DeclaringType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PwmaIndicator_MultipleUpdates_ProducesCorrectPwmaSequence()
|
||||
{
|
||||
var indicator = new PwmaIndicator { Period = 3 };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
double[] closes = { 100, 102, 104, 103, 105 };
|
||||
|
||||
foreach (var close in closes)
|
||||
{
|
||||
indicator.HistoricalData.AddBar(now, close, close + 2, close - 2, close);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
now = now.AddMinutes(1);
|
||||
}
|
||||
|
||||
// All values should be finite
|
||||
for (int i = 0; i < closes.Length; i++)
|
||||
{
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(closes.Length - 1 - i)));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PwmaIndicator_DifferentSourceTypes_Work()
|
||||
{
|
||||
var sources = new[] { SourceType.Open, SourceType.High, SourceType.Low, SourceType.Close, SourceType.HL2, SourceType.HLC3 };
|
||||
|
||||
foreach (var source in sources)
|
||||
{
|
||||
var indicator = new PwmaIndicator { Period = 3, Source = source };
|
||||
indicator.Initialize();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
indicator.HistoricalData.AddBar(now, 100, 110, 90, 105);
|
||||
indicator.ProcessUpdate(new UpdateArgs(UpdateReason.HistoricalBar));
|
||||
|
||||
Assert.True(double.IsFinite(indicator.LinesSeries[0].GetValue(0)),
|
||||
$"Source {source} should produce finite value");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PwmaIndicator_Period_CanBeChanged()
|
||||
{
|
||||
var indicator = new PwmaIndicator { Period = 5 };
|
||||
Assert.Equal(5, indicator.Period);
|
||||
|
||||
indicator.Period = 20;
|
||||
Assert.Equal(20, indicator.Period);
|
||||
Assert.Equal(20, indicator.MinHistoryDepths);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using System.Drawing;
|
||||
using TradingPlatform.BusinessLayer;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
public class PwmaIndicator : Indicator, IWatchlistIndicator
|
||||
{
|
||||
[InputParameter("Period", sortIndex: 1, 1, 2000, 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 Pwma? _ma;
|
||||
private int _warmupBarIndex = -1;
|
||||
protected LineSeries? Series;
|
||||
protected string? SourceName;
|
||||
|
||||
public int MinHistoryDepths => Period;
|
||||
int IWatchlistIndicator.MinHistoryDepths => MinHistoryDepths;
|
||||
|
||||
public override string ShortName => $"PWMA {Period}:{SourceName}";
|
||||
public override string SourceCodeLink => "https://github.com/mihakralj/QuanTAlib/blob/main/lib/trends/pwma/Pwma.Quantower.cs";
|
||||
|
||||
public PwmaIndicator()
|
||||
{
|
||||
OnBackGround = true;
|
||||
SeparateWindow = false;
|
||||
SourceName = Source.ToString();
|
||||
Name = "PWMA - Parabolic Weighted Moving Average";
|
||||
Description = "Weighted Moving Average with parabolic weighting";
|
||||
Series = new(name: $"PWMA {Period}", color: IndicatorExtensions.Averages, width: 2, style: LineStyle.Solid);
|
||||
AddLineSeries(Series);
|
||||
}
|
||||
|
||||
protected override void OnInit()
|
||||
{
|
||||
_ma = new Pwma(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); //OnPaintChart draws the line, hidden here
|
||||
}
|
||||
|
||||
public override void OnPaintChart(PaintChartEventArgs args)
|
||||
{
|
||||
base.OnPaintChart(args);
|
||||
this.PaintSmoothCurve(args, Series!, _warmupBarIndex, showColdValues: ShowColdValues, tension: 0.2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
#pragma warning disable S2245 // Random is acceptable for simulation/testing purposes
|
||||
public class PwmaTests
|
||||
{
|
||||
[Fact]
|
||||
public void Pwma_Constructor_ValidatesInput()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() => new Pwma(0));
|
||||
Assert.Throws<ArgumentException>(() => new Pwma(-1));
|
||||
|
||||
var pwma = new Pwma(10);
|
||||
Assert.NotNull(pwma);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pwma_Calc_ReturnsValue()
|
||||
{
|
||||
var pwma = new Pwma(10);
|
||||
|
||||
Assert.Equal(0, pwma.Last.Value);
|
||||
|
||||
TValue result = pwma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.True(result.Value > 0);
|
||||
Assert.Equal(result.Value, pwma.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pwma_FirstValue_ReturnsItself()
|
||||
{
|
||||
var pwma = new Pwma(10);
|
||||
|
||||
TValue result = pwma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.Equal(100.0, result.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pwma_Calc_IsNew_AcceptsParameter()
|
||||
{
|
||||
var pwma = new Pwma(10);
|
||||
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 100), isNew: true);
|
||||
double value1 = pwma.Last.Value;
|
||||
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 200), isNew: true);
|
||||
double value2 = pwma.Last.Value;
|
||||
|
||||
// Values should change with new bars
|
||||
Assert.NotEqual(value1, value2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pwma_Calc_IsNew_False_UpdatesValue()
|
||||
{
|
||||
var pwma = new Pwma(10);
|
||||
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 110), isNew: true);
|
||||
double beforeUpdate = pwma.Last.Value;
|
||||
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 120), isNew: false);
|
||||
double afterUpdate = pwma.Last.Value;
|
||||
|
||||
// Update should change the value
|
||||
Assert.NotEqual(beforeUpdate, afterUpdate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pwma_Reset_ClearsState()
|
||||
{
|
||||
var pwma = new Pwma(10);
|
||||
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 105));
|
||||
double valueBefore = pwma.Last.Value;
|
||||
|
||||
pwma.Reset();
|
||||
|
||||
Assert.Equal(0, pwma.Last.Value);
|
||||
|
||||
// After reset, should accept new values
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.NotEqual(0, pwma.Last.Value);
|
||||
Assert.NotEqual(valueBefore, pwma.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pwma_Properties_Accessible()
|
||||
{
|
||||
var pwma = new Pwma(10);
|
||||
|
||||
Assert.Equal(0, pwma.Last.Value);
|
||||
Assert.False(pwma.IsHot);
|
||||
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
Assert.NotEqual(0, pwma.Last.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pwma_IsHot_BecomesTrueWhenBufferFull()
|
||||
{
|
||||
var pwma = new Pwma(5);
|
||||
|
||||
Assert.False(pwma.IsHot);
|
||||
|
||||
for (int i = 1; i <= 4; i++)
|
||||
{
|
||||
pwma.Update(new TValue(DateTime.UtcNow, i * 10));
|
||||
Assert.False(pwma.IsHot);
|
||||
}
|
||||
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 50));
|
||||
Assert.True(pwma.IsHot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pwma_CalculatesCorrectWeightedAverage()
|
||||
{
|
||||
var pwma = new Pwma(3);
|
||||
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 10));
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 20));
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 30));
|
||||
|
||||
// PWMA(3) of 10,20,30 = (1^2*10 + 2^2*20 + 3^2*30) / (1^2 + 2^2 + 3^2)
|
||||
// = (1*10 + 4*20 + 9*30) / (1 + 4 + 9)
|
||||
// = (10 + 80 + 270) / 14
|
||||
// = 360 / 14 = 25.7142857...
|
||||
Assert.Equal(360.0 / 14.0, pwma.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pwma_SlidingWindow_Works()
|
||||
{
|
||||
var pwma = new Pwma(3);
|
||||
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 10));
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 20));
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 30));
|
||||
|
||||
// PWMA(3) of 10,20,30 = 360/14
|
||||
Assert.Equal(360.0 / 14.0, pwma.Last.Value, 1e-10);
|
||||
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 40));
|
||||
|
||||
// PWMA(3) of 20,30,40 = (1^2*20 + 2^2*30 + 3^2*40) / 14
|
||||
// = (20 + 120 + 360) / 14 = 500 / 14 = 35.7142857...
|
||||
Assert.Equal(500.0 / 14.0, pwma.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pwma_IterativeCorrections_RestoreToOriginalState()
|
||||
{
|
||||
var pwma = new Pwma(5);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
|
||||
// Feed 10 new values
|
||||
TValue tenthInput = default;
|
||||
for (int i = 0; i < 10; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
tenthInput = new TValue(bar.Time, bar.Close);
|
||||
pwma.Update(tenthInput, isNew: true);
|
||||
}
|
||||
|
||||
// Remember PWMA state after 10 values
|
||||
double pwmaAfterTen = pwma.Last.Value;
|
||||
|
||||
// Generate 9 corrections with isNew=false (different values)
|
||||
for (int i = 0; i < 9; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: false);
|
||||
pwma.Update(new TValue(bar.Time, bar.Close), isNew: false);
|
||||
}
|
||||
|
||||
// Feed the remembered 10th input again with isNew=false
|
||||
TValue finalPwma = pwma.Update(tenthInput, isNew: false);
|
||||
|
||||
// PWMA should match the original state after 10 values
|
||||
Assert.Equal(pwmaAfterTen, finalPwma.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pwma_BatchCalc_MatchesIterativeCalc()
|
||||
{
|
||||
var pwmaIterative = new Pwma(10);
|
||||
var pwmaBatch = new Pwma(10);
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1);
|
||||
|
||||
// Generate data
|
||||
var series = new TSeries();
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
Assert.True(series.Count > 0);
|
||||
|
||||
// Calculate iteratively
|
||||
var iterativeResults = new TSeries();
|
||||
foreach (var item in series)
|
||||
{
|
||||
iterativeResults.Add(pwmaIterative.Update(item));
|
||||
}
|
||||
|
||||
// Calculate batch
|
||||
var batchResults = pwmaBatch.Update(series);
|
||||
|
||||
// Compare
|
||||
Assert.Equal(iterativeResults.Count, batchResults.Count);
|
||||
for (int i = 0; i < iterativeResults.Count; i++)
|
||||
{
|
||||
Assert.Equal(iterativeResults[i].Value, batchResults[i].Value, 1e-10);
|
||||
Assert.Equal(iterativeResults[i].Time, batchResults[i].Time);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pwma_NaN_Input_UsesLastValidValue()
|
||||
{
|
||||
var pwma = new Pwma(5);
|
||||
|
||||
// Feed some valid values
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
// Feed NaN - should use last valid value (110)
|
||||
var resultAfterNaN = pwma.Update(new TValue(DateTime.UtcNow, double.NaN));
|
||||
|
||||
// Result should be finite (not NaN)
|
||||
Assert.True(double.IsFinite(resultAfterNaN.Value));
|
||||
Assert.NotEqual(0, resultAfterNaN.Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pwma_Infinity_Input_UsesLastValidValue()
|
||||
{
|
||||
var pwma = new Pwma(5);
|
||||
|
||||
// Feed some valid values
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 110));
|
||||
|
||||
// Feed positive infinity - should use last valid value
|
||||
var resultAfterPosInf = pwma.Update(new TValue(DateTime.UtcNow, double.PositiveInfinity));
|
||||
Assert.True(double.IsFinite(resultAfterPosInf.Value));
|
||||
|
||||
// Feed negative infinity - should use last valid value
|
||||
var resultAfterNegInf = pwma.Update(new TValue(DateTime.UtcNow, double.NegativeInfinity));
|
||||
Assert.True(double.IsFinite(resultAfterNegInf.Value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pwma_StaticCalculate_Works()
|
||||
{
|
||||
var series = new TSeries();
|
||||
series.Add(DateTime.UtcNow.Ticks, 10);
|
||||
series.Add(DateTime.UtcNow.Ticks + 1, 20);
|
||||
series.Add(DateTime.UtcNow.Ticks + 2, 30);
|
||||
|
||||
var results = Pwma.Calculate(series, 3);
|
||||
|
||||
Assert.Equal(3, results.Count);
|
||||
// PWMA(3) for last 3 values [10,20,30]: 360/14
|
||||
Assert.Equal(360.0 / 14.0, results.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pwma_MoreWeightOnRecentValues_ThanWma()
|
||||
{
|
||||
var pwma = new Pwma(3);
|
||||
var wma = new Wma(3);
|
||||
|
||||
// Feed same values to both
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 10));
|
||||
wma.Update(new TValue(DateTime.UtcNow, 10));
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 20));
|
||||
wma.Update(new TValue(DateTime.UtcNow, 20));
|
||||
pwma.Update(new TValue(DateTime.UtcNow, 100)); // High recent value
|
||||
wma.Update(new TValue(DateTime.UtcNow, 100));
|
||||
|
||||
// PWMA should be higher than WMA because it weights the high recent value even more (parabolically)
|
||||
// WMA = (1*10 + 2*20 + 3*100) / 6 = 350/6 = 58.333...
|
||||
// PWMA = (1*10 + 4*20 + 9*100) / 14 = 990/14 = 70.714...
|
||||
Assert.True(pwma.Last.Value > wma.Last.Value);
|
||||
Assert.Equal(990.0 / 14.0, pwma.Last.Value, 1e-10);
|
||||
Assert.Equal(350.0 / 6.0, wma.Last.Value, 1e-10);
|
||||
}
|
||||
|
||||
// ============== Span API Tests ==============
|
||||
|
||||
[Fact]
|
||||
public void Pwma_SpanCalc_ValidatesInput()
|
||||
{
|
||||
double[] source = [1, 2, 3, 4, 5];
|
||||
double[] output = new double[5];
|
||||
double[] wrongSizeOutput = new double[3];
|
||||
|
||||
// Period must be > 0
|
||||
Assert.Throws<ArgumentException>(() => Pwma.Calculate(source.AsSpan(), output.AsSpan(), 0));
|
||||
Assert.Throws<ArgumentException>(() => Pwma.Calculate(source.AsSpan(), output.AsSpan(), -1));
|
||||
|
||||
// Output must be same length as source
|
||||
Assert.Throws<ArgumentException>(() => Pwma.Calculate(source.AsSpan(), wrongSizeOutput.AsSpan(), 3));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pwma_SpanCalc_MatchesTSeriesCalc()
|
||||
{
|
||||
var series = new TSeries();
|
||||
double[] source = new double[100];
|
||||
double[] output = new double[100];
|
||||
|
||||
var gbm = new GBM(startPrice: 100.0, mu: 0.02, sigma: 0.1, seed: 42);
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var bar = gbm.Next(isNew: true);
|
||||
source[i] = bar.Close;
|
||||
series.Add(bar.Time, bar.Close);
|
||||
}
|
||||
|
||||
// Calculate with TSeries API
|
||||
var tseriesResult = Pwma.Calculate(series, 10);
|
||||
|
||||
// Calculate with Span API
|
||||
Pwma.Calculate(source.AsSpan(), output.AsSpan(), 10);
|
||||
|
||||
// Compare results
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
Assert.Equal(tseriesResult[i].Value, output[i], 1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pwma_SpanCalc_CalculatesCorrectly()
|
||||
{
|
||||
double[] source = [10, 20, 30];
|
||||
double[] output = new double[3];
|
||||
|
||||
Pwma.Calculate(source.AsSpan(), output.AsSpan(), 3);
|
||||
|
||||
// PWMA(3) warmup:
|
||||
// i=0: 10 (1^2*10 / 1^2) = 10
|
||||
// i=1: (1^2*10 + 2^2*20) / (1^2 + 2^2) = (10 + 80) / 5 = 90/5 = 18
|
||||
// i=2: (1^2*10 + 2^2*20 + 3^2*30) / (1^2 + 2^2 + 3^2) = (10 + 80 + 270) / 14 = 360/14 = 25.714...
|
||||
Assert.Equal(10.0, output[0], 1e-10);
|
||||
Assert.Equal(18.0, output[1], 1e-10);
|
||||
Assert.Equal(360.0 / 14.0, output[2], 1e-10);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Pwma_AllModes_ProduceSameResult()
|
||||
{
|
||||
// Arrange
|
||||
int period = 10;
|
||||
var gbm = new GBM(startPrice: 100, mu: 0.05, sigma: 0.2, seed: 123);
|
||||
var bars = gbm.Fetch(1000, DateTime.UtcNow.Ticks, TimeSpan.FromMinutes(1));
|
||||
var series = bars.Close;
|
||||
|
||||
// 1. Batch Mode
|
||||
var batchSeries = Pwma.Calculate(series, period);
|
||||
double expected = batchSeries.Last.Value;
|
||||
|
||||
// 2. Span Mode
|
||||
var tValues = series.Values.ToArray();
|
||||
var spanInput = new ReadOnlySpan<double>(tValues);
|
||||
var spanOutput = new double[tValues.Length];
|
||||
Pwma.Calculate(spanInput, spanOutput, period);
|
||||
double spanResult = spanOutput[^1];
|
||||
|
||||
// 3. Streaming Mode
|
||||
var streamingInd = new Pwma(period);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
streamingInd.Update(series[i]);
|
||||
}
|
||||
double streamingResult = streamingInd.Last.Value;
|
||||
|
||||
// 4. Eventing Mode
|
||||
var pubSource = new TSeries();
|
||||
var eventingInd = new Pwma(pubSource, period);
|
||||
for (int i = 0; i < series.Count; i++)
|
||||
{
|
||||
pubSource.Add(series[i]);
|
||||
}
|
||||
double eventingResult = eventingInd.Last.Value;
|
||||
|
||||
// Assert
|
||||
Assert.Equal(expected, spanResult, precision: 9);
|
||||
Assert.Equal(expected, streamingResult, precision: 8);
|
||||
Assert.Equal(expected, eventingResult, precision: 8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OoplesFinance.StockIndicators;
|
||||
using OoplesFinance.StockIndicators.Models;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace QuanTAlib.Tests;
|
||||
|
||||
public class PwmaValidationTests
|
||||
{
|
||||
private readonly ValidationTestData _testData;
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public PwmaValidationTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
_testData = new ValidationTestData();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Against_Ooples()
|
||||
{
|
||||
int[] periods = { 5, 10, 20, 50, 100 };
|
||||
|
||||
// Prepare data for Ooples (List<TickerData>)
|
||||
var ooplesData = _testData.SkenderQuotes.Select(q => new TickerData
|
||||
{
|
||||
Date = q.Date,
|
||||
Close = (double)q.Close,
|
||||
High = (double)q.High,
|
||||
Low = (double)q.Low,
|
||||
Open = (double)q.Open,
|
||||
Volume = (double)q.Volume
|
||||
}).ToList();
|
||||
|
||||
foreach (var period in periods)
|
||||
{
|
||||
// Calculate QuanTAlib PWMA
|
||||
var pwma = new global::QuanTAlib.Pwma(period);
|
||||
var qResult = pwma.Update(_testData.Data);
|
||||
|
||||
// Calculate Ooples PWMA
|
||||
var stockData = new StockData(ooplesData);
|
||||
var oResult = stockData.CalculateParabolicWeightedMovingAverage(length: period);
|
||||
var oValues = oResult.OutputValues["Pwma"];
|
||||
|
||||
// Compare
|
||||
ValidationHelper.VerifyData(qResult, oValues, (s) => s, tolerance: 1e-3);
|
||||
}
|
||||
_output.WriteLine("PWMA validated successfully against Ooples");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace QuanTAlib;
|
||||
|
||||
/// <summary>
|
||||
/// PWMA: Parabolic Weighted Moving Average
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// PWMA applies parabolic weighting to data points, giving significantly more weight to recent values.
|
||||
/// Uses triple running sums for O(1) complexity per update.
|
||||
///
|
||||
/// Weights: w(i) = i^2
|
||||
///
|
||||
/// Calculation:
|
||||
/// PWMA = Sum(i^2 * P_i) / Sum(i^2)
|
||||
///
|
||||
/// O(1) update logic:
|
||||
/// S1_new = S1_old - oldest + newest
|
||||
/// S2_new = S2_old - S1_old + n * newest
|
||||
/// S3_new = S3_old - 2*S2_old + S1_old + n^2 * newest
|
||||
///
|
||||
/// Where:
|
||||
/// S1 is simple sum
|
||||
/// S2 is linear weighted sum
|
||||
/// S3 is parabolic weighted sum
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Pwma : ITValuePublisher
|
||||
{
|
||||
private readonly int _period;
|
||||
private readonly double _divisor;
|
||||
private readonly RingBuffer _buffer;
|
||||
|
||||
private record struct State(double Sum, double WSum, double PSum, double LastInput, double LastValidValue, int TickCount);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
private const int ResyncInterval = 1000;
|
||||
|
||||
public string Name { get; }
|
||||
public TValue Last { get; private set; }
|
||||
public bool IsHot => _buffer.IsFull;
|
||||
public event Action<TValue>? Pub;
|
||||
|
||||
public Pwma(int period)
|
||||
{
|
||||
if (period <= 0) throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
_period = period;
|
||||
_divisor = (double)period * (period + 1) * (2 * period + 1) / 6.0;
|
||||
_buffer = new RingBuffer(period);
|
||||
Name = $"Pwma({period})";
|
||||
}
|
||||
|
||||
public Pwma(ITValuePublisher source, int period) : this(period)
|
||||
{
|
||||
source.Pub += (item) => Update(item);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private double GetValidValue(double input)
|
||||
{
|
||||
if (double.IsFinite(input))
|
||||
{
|
||||
_state.LastValidValue = input;
|
||||
return input;
|
||||
}
|
||||
return _state.LastValidValue;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private void UpdateState(double val)
|
||||
{
|
||||
if (_buffer.IsFull)
|
||||
{
|
||||
double oldSum = _state.Sum;
|
||||
double oldWSum = _state.WSum;
|
||||
double oldest = _buffer.Oldest;
|
||||
|
||||
_state.Sum = _state.Sum - oldest + val;
|
||||
_state.WSum = _state.WSum - oldSum + (_period * val);
|
||||
_state.PSum = _state.PSum - 2 * oldWSum + oldSum + ((double)_period * _period * val);
|
||||
}
|
||||
else
|
||||
{
|
||||
int count = _buffer.Count + 1;
|
||||
_state.Sum += val;
|
||||
_state.WSum += count * val;
|
||||
_state.PSum += (double)count * count * val;
|
||||
}
|
||||
|
||||
_buffer.Add(val);
|
||||
|
||||
_state.TickCount++;
|
||||
if (_buffer.IsFull && _state.TickCount >= ResyncInterval)
|
||||
{
|
||||
_state.TickCount = 0;
|
||||
double recalcSum = 0;
|
||||
double recalcWsum = 0;
|
||||
double recalcPsum = 0;
|
||||
int i = 1;
|
||||
foreach (double item in _buffer)
|
||||
{
|
||||
recalcSum += item;
|
||||
recalcWsum += i * item;
|
||||
recalcPsum += (double)i * i * item;
|
||||
i++;
|
||||
}
|
||||
_state.Sum = recalcSum;
|
||||
_state.WSum = recalcWsum;
|
||||
_state.PSum = recalcPsum;
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public TValue Update(TValue input, bool isNew = true)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
double val = GetValidValue(input.Value);
|
||||
UpdateState(val);
|
||||
_state.LastInput = val;
|
||||
_p_state = _state;
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
double val = GetValidValue(input.Value);
|
||||
|
||||
// Recalculate for the updated last value
|
||||
// We can't easily use the O(1) update formula here because we are replacing the newest value,
|
||||
// not shifting the window.
|
||||
// But we can adjust the sums directly.
|
||||
// S1' = S1 - last + new
|
||||
// S2' = S2 - n*last + n*new
|
||||
// S3' = S3 - n^2*last + n^2*new
|
||||
|
||||
int n = _buffer.IsFull ? _period : _buffer.Count;
|
||||
double diff = val - _state.LastInput;
|
||||
|
||||
_state.Sum += diff;
|
||||
_state.WSum += n * diff;
|
||||
_state.PSum += (double)n * n * diff;
|
||||
|
||||
_buffer.UpdateNewest(val);
|
||||
}
|
||||
|
||||
double currentDivisor = _buffer.IsFull ? _divisor : (double)_buffer.Count * (_buffer.Count + 1) * (2 * _buffer.Count + 1) / 6.0;
|
||||
Last = new TValue(input.Time, _state.PSum / currentDivisor);
|
||||
Pub?.Invoke(Last);
|
||||
return Last;
|
||||
}
|
||||
|
||||
public TSeries Update(TSeries source)
|
||||
{
|
||||
if (source.Count == 0) return [];
|
||||
|
||||
int len = source.Count;
|
||||
List<long> t = new(len);
|
||||
List<double> v = new(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
|
||||
int windowSize = Math.Min(len, _period);
|
||||
int startIndex = len - windowSize;
|
||||
|
||||
if (startIndex > 0)
|
||||
{
|
||||
_state.LastValidValue = 0;
|
||||
for (int i = startIndex - 1; i >= 0; i--)
|
||||
{
|
||||
if (double.IsFinite(source.Values[i]))
|
||||
{
|
||||
_state.LastValidValue = source.Values[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_state.LastValidValue = 0;
|
||||
}
|
||||
|
||||
_buffer.Clear();
|
||||
_state.Sum = 0;
|
||||
_state.WSum = 0;
|
||||
_state.PSum = 0;
|
||||
_state.TickCount = 0;
|
||||
|
||||
for (int i = startIndex; i < len; i++)
|
||||
{
|
||||
double val = GetValidValue(source.Values[i]);
|
||||
UpdateState(val);
|
||||
_state.LastInput = val;
|
||||
}
|
||||
|
||||
_p_state = _state;
|
||||
|
||||
Last = new TValue(tSpan[len - 1], vSpan[len - 1]);
|
||||
return new TSeries(t, v);
|
||||
}
|
||||
|
||||
public static TSeries Calculate(TSeries source, int period)
|
||||
{
|
||||
var pwma = new Pwma(period);
|
||||
return pwma.Update(source);
|
||||
}
|
||||
|
||||
[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 <= 0)
|
||||
throw new ArgumentException("Period must be greater than 0", nameof(period));
|
||||
|
||||
int len = source.Length;
|
||||
if (len == 0) return;
|
||||
|
||||
CalculateScalarCore(source, output, period);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void CalculateScalarCore(ReadOnlySpan<double> source, Span<double> output, int period)
|
||||
{
|
||||
int len = source.Length;
|
||||
double divisor = (double)period * (period + 1) * (2 * period + 1) / 6.0;
|
||||
double sum = 0;
|
||||
double wsum = 0;
|
||||
double psum = 0;
|
||||
double lastValid = 0;
|
||||
|
||||
Span<double> buffer = period <= 512 ? stackalloc double[period] : new double[period];
|
||||
int bufferIdx = 0;
|
||||
int i = 0;
|
||||
|
||||
int warmupEnd = Math.Min(period, len);
|
||||
for (; i < warmupEnd; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
lastValid = val;
|
||||
else
|
||||
val = lastValid;
|
||||
|
||||
sum += val;
|
||||
wsum += (i + 1) * val;
|
||||
psum += (double)(i + 1) * (i + 1) * val;
|
||||
buffer[i] = val;
|
||||
|
||||
double currentDivisor = (double)(i + 1) * (i + 2) * (2 * (i + 1) + 1) / 6.0;
|
||||
output[i] = psum / currentDivisor;
|
||||
}
|
||||
|
||||
int tickCount = period;
|
||||
for (; i < len; i++)
|
||||
{
|
||||
double val = source[i];
|
||||
if (double.IsFinite(val))
|
||||
lastValid = val;
|
||||
else
|
||||
val = lastValid;
|
||||
|
||||
double oldSum = sum;
|
||||
double oldWSum = wsum;
|
||||
double oldest = buffer[bufferIdx];
|
||||
|
||||
sum = sum - oldest + val;
|
||||
wsum = wsum - oldSum + (period * val);
|
||||
psum = psum - 2 * oldWSum + oldSum + ((double)period * period * val);
|
||||
|
||||
buffer[bufferIdx] = val;
|
||||
bufferIdx++;
|
||||
if (bufferIdx >= period)
|
||||
bufferIdx = 0;
|
||||
|
||||
tickCount++;
|
||||
if (tickCount >= ResyncInterval)
|
||||
{
|
||||
tickCount = 0;
|
||||
double recalcSum = 0;
|
||||
double recalcWsum = 0;
|
||||
double recalcPsum = 0;
|
||||
|
||||
for (int k = 0; k < period; k++)
|
||||
{
|
||||
int idx = bufferIdx + k;
|
||||
if (idx >= period) idx -= period;
|
||||
|
||||
double v = buffer[idx];
|
||||
recalcSum += v;
|
||||
recalcWsum += (k + 1) * v;
|
||||
recalcPsum += (double)(k + 1) * (k + 1) * v;
|
||||
}
|
||||
sum = recalcSum;
|
||||
wsum = recalcWsum;
|
||||
psum = recalcPsum;
|
||||
}
|
||||
|
||||
output[i] = psum / divisor;
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_buffer.Clear();
|
||||
_state = default;
|
||||
_p_state = default;
|
||||
Last = default;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
# PWMA: Parabolic Weighted Moving Average
|
||||
|
||||
## Overview and Purpose
|
||||
|
||||
The Parabolic Weighted Moving Average (PWMA) is a technical indicator that applies parabolic weighting to price data, assigning significantly higher importance to the most recent observations. While the Weighted Moving Average (WMA) uses linear weighting ($i$), PWMA uses squared weighting ($i^2$), creating an even stronger recency bias. This results in an indicator that tracks price action with exceptional responsiveness, making it ideal for fast-moving markets and as a component in advanced momentum oscillators like Jurik's Velocity (VEL).
|
||||
|
||||
## Core Concepts
|
||||
|
||||
* **Parabolic weighting:** Weights follow a squared progression ($1^2, 2^2, \dots, n^2$), drastically emphasizing recent data over older points.
|
||||
* **Reduced Lag:** The aggressive weighting scheme minimizes lag significantly more than WMA or SMA, allowing for faster trend detection.
|
||||
* **O(1) Complexity:** This implementation uses a triple running sum technique to ensure constant-time updates, regardless of the period length.
|
||||
* **Component Indicator:** PWMA is a critical building block for other indicators, most notably serving as the "fast" component in the Velocity (VEL) indicator calculation ($VEL = PWMA - WMA$).
|
||||
|
||||
The core innovation of PWMA is its use of squared weights, which shifts the center of gravity of the moving average much closer to the current price than linear methods. This makes it highly sensitive to recent price changes while still providing a smooth curve derived from the entire window.
|
||||
|
||||
## Common Settings and Parameters
|
||||
|
||||
| Parameter | Default | Function | When to Adjust |
|
||||
|-----------|---------|----------|---------------|
|
||||
| Period | 14 | Controls the lookback period | Increase for smoother trends, decrease for ultra-fast responsiveness |
|
||||
| Source | Close | Price data used for calculation | Consider using HLC3 for a more balanced price representation |
|
||||
|
||||
**Pro Tip:** Because PWMA is so responsive, it can be prone to overshooting in choppy markets. It is often best used in combination with a slower average (like WMA) to form a MACD-like oscillator or to identify rapid momentum shifts.
|
||||
|
||||
## Calculation and Mathematical Foundation
|
||||
|
||||
**Simplified explanation:**
|
||||
PWMA calculates a weighted average where the weight of each price is the square of its position in the window. For a 5-period PWMA, the weights would be $1, 4, 9, 16, 25$ (for the oldest to newest prices respectively).
|
||||
|
||||
**Technical formula:**
|
||||
$$PWMA = \frac{\sum_{i=1}^{n} i^2 \cdot P_i}{\sum_{i=1}^{n} i^2}$$
|
||||
|
||||
Where:
|
||||
|
||||
* $n$ is the period length
|
||||
* $P_i$ is the price at position $i$ (oldest to newest)
|
||||
* $i^2$ is the parabolic weight
|
||||
* Divisor $= \frac{n(n+1)(2n+1)}{6}$ (sum of squares of first $n$ integers)
|
||||
|
||||
**O(1) Optimization - Triple Running Sums:**
|
||||
|
||||
To achieve constant-time updates, the algorithm maintains three running sums:
|
||||
|
||||
1. **S1 (Simple Sum):** $\sum P_i$
|
||||
2. **S2 (Linear Weighted Sum):** $\sum i \cdot P_i$
|
||||
3. **S3 (Parabolic Weighted Sum):** $\sum i^2 \cdot P_i$
|
||||
|
||||
The recurrence relations for updating these sums when the window slides are:
|
||||
|
||||
$$S_{1,new} = S_{1,old} - P_{oldest} + P_{new}$$
|
||||
$$S_{2,new} = S_{2,old} - S_{1,old} + n \cdot P_{new}$$
|
||||
$$S_{3,new} = S_{3,old} - 2 \cdot S_{2,old} + S_{1,old} + n^2 \cdot P_{new}$$
|
||||
|
||||
$$PWMA = \frac{S_{3,new}}{divisor}$$
|
||||
|
||||
This algebraic expansion allows the indicator to update in constant time (~12 operations) regardless of whether the period is 10 or 10,000.
|
||||
|
||||
## C# Implementation
|
||||
|
||||
The library provides two implementations: a standard scalar version and a high-performance Span-based static version.
|
||||
|
||||
### Single PWMA (`Pwma`)
|
||||
|
||||
The `Pwma` class calculates a single parabolic weighted moving average with O(1) update complexity.
|
||||
|
||||
```csharp
|
||||
using QuanTAlib;
|
||||
|
||||
// Initialize with period 14
|
||||
var pwma = new Pwma(14);
|
||||
|
||||
// Streaming update
|
||||
TValue result = pwma.Update(new TValue(time, price));
|
||||
Console.WriteLine($"Current PWMA: {result.Value}");
|
||||
|
||||
// Access properties
|
||||
Console.WriteLine($"Name: {pwma.Name}"); // "Pwma(14)"
|
||||
Console.WriteLine($"IsHot: {pwma.IsHot}"); // true when buffer is full
|
||||
|
||||
// Batch calculation (TSeries API)
|
||||
TSeries source = ...;
|
||||
TSeries results = Pwma.Calculate(source, 14);
|
||||
|
||||
// High-performance Span API (zero allocation)
|
||||
double[] prices = new double[10000];
|
||||
double[] output = new double[10000];
|
||||
Pwma.Calculate(prices.AsSpan(), output.AsSpan(), period: 14);
|
||||
```
|
||||
|
||||
### Zero-Allocation Span API
|
||||
|
||||
For performance-critical scenarios (backtesting, HFT), use the Span-based overload:
|
||||
|
||||
```csharp
|
||||
// Allocate buffers once, reuse across calculations
|
||||
double[] source = new double[200000];
|
||||
double[] pwmaOutput = new double[200000];
|
||||
|
||||
// Zero heap allocation during calculation
|
||||
Pwma.Calculate(source.AsSpan(), pwmaOutput.AsSpan(), period: 100);
|
||||
|
||||
// Results are written directly to output buffer
|
||||
Console.WriteLine($"Last PWMA: {pwmaOutput[^1]}");
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
|
||||
* **Zero allocation**: No GC pressure during calculation
|
||||
* **Cache-friendly**: Sequential memory access patterns
|
||||
* **O(1) per-bar** via triple running sums
|
||||
* **Compatible** with `ArrayPool<T>` for buffer management
|
||||
|
||||
### Bar Correction (isNew Parameter)
|
||||
|
||||
`Pwma` supports intra-bar updates for real-time trading systems:
|
||||
|
||||
```csharp
|
||||
var pwma = new Pwma(14);
|
||||
|
||||
// Process historical bars
|
||||
for (int i = 0; i < historicalBars.Count; i++)
|
||||
{
|
||||
pwma.Update(historicalBars[i], isNew: true);
|
||||
}
|
||||
|
||||
// Real-time: receive initial tick for new bar
|
||||
pwma.Update(new TValue(time, 100.5), isNew: true);
|
||||
|
||||
// Real-time: price updates within same bar
|
||||
pwma.Update(new TValue(time, 101.0), isNew: false); // O(1) correction
|
||||
pwma.Update(new TValue(time, 100.8), isNew: false); // O(1) correction
|
||||
|
||||
// Bar closes, next bar starts
|
||||
pwma.Update(new TValue(time + 1, 101.2), isNew: true);
|
||||
```
|
||||
|
||||
**Implementation detail:** Bar correction is O(1) using scalar state save/restore.
|
||||
|
||||
### 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
|
||||
var pwma = new Pwma(source, period: 14);
|
||||
|
||||
// 3. Optional: Subscribe to indicator's output
|
||||
pwma.Pub += (item) => Console.WriteLine($"PWMA Updated: {item.Value}");
|
||||
|
||||
// 4. Ingest data into source
|
||||
source.Add(new TValue(DateTime.Now, 100));
|
||||
```
|
||||
|
||||
### Handling Invalid Values (NaN/Infinity)
|
||||
|
||||
`Pwma` uses **last-value substitution** for handling invalid inputs:
|
||||
|
||||
```csharp
|
||||
var pwma = new Pwma(14);
|
||||
|
||||
// Valid values establish baseline
|
||||
pwma.Update(new TValue(time, 100));
|
||||
|
||||
// NaN or Infinity inputs are replaced with last valid value
|
||||
var result = pwma.Update(new TValue(time, double.NaN));
|
||||
Console.WriteLine(double.IsFinite(result.Value)); // true
|
||||
```
|
||||
|
||||
**Behavior:**
|
||||
|
||||
* When `NaN`, `PositiveInfinity`, or `NegativeInfinity` is encountered, the last valid value is substituted
|
||||
* This provides output continuity instead of propagating invalid values
|
||||
* `Reset()` clears the last valid value, so the next valid input establishes a new baseline
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
| Operation | Complexity | Notes |
|
||||
|-----------|------------|-------|
|
||||
| Update (isNew=true) | O(1) | Triple running sums logic |
|
||||
| Update (isNew=false) | O(1) | Scalar state restore + recalculate |
|
||||
| Batch processing | O(n) | Where n is series length |
|
||||
| Memory (single) | O(period) | One RingBuffer for values |
|
||||
| Memory (state) | O(1) | Scalar state struct |
|
||||
|
||||
The implementation uses:
|
||||
|
||||
* **Triple running sums** for O(1) parabolic weighted average calculation
|
||||
* **Scalar state save/restore** for O(1) bar correction
|
||||
* **Pinned memory** in RingBuffer for cache-friendly access
|
||||
* **Periodic Resync** (every 1000 ticks) to prevent floating-point drift
|
||||
|
||||
## Interpretation Details
|
||||
|
||||
PWMA is primarily used for:
|
||||
|
||||
* **High-Speed Trend Detection:** Its low lag makes it excellent for catching trends early.
|
||||
* **Velocity Calculation:** Used in conjunction with WMA to calculate Velocity ($VEL = PWMA - WMA$).
|
||||
* **Dynamic Support/Resistance:** Acts as a tighter support/resistance level than SMA or WMA in strong trends.
|
||||
|
||||
### PWMA vs WMA vs SMA Comparison
|
||||
|
||||
| Aspect | PWMA | WMA | SMA |
|
||||
|--------|------|-----|-----|
|
||||
| Weighting | Parabolic ($i^2$) | Linear ($i$) | Equal ($1$) |
|
||||
| Lag | Lowest | Low | High |
|
||||
| Sensitivity | Highest | High | Low |
|
||||
| Noise filtering | Low | Good | Best |
|
||||
| Best use | Momentum, Velocity | General Trend | Long-term Trend |
|
||||
|
||||
## Limitations and Considerations
|
||||
|
||||
* **Overshoot:** Due to the aggressive weighting, PWMA can overshoot price targets during sudden reversals.
|
||||
* **Noise Sensitivity:** It is more sensitive to market noise than WMA or SMA.
|
||||
* **Drift:** The complex running sum algorithm requires periodic resynchronization (handled internally) to maintain precision over millions of updates.
|
||||
|
||||
## References
|
||||
|
||||
* Jurik Research (concept of parabolic weighting in Velocity)
|
||||
* Colby, Robert W. "The Encyclopedia of Technical Market Indicators." McGraw-Hill, 2002
|
||||
@@ -46,7 +46,7 @@ Where:
|
||||
|
||||
## C# Implementation
|
||||
|
||||
The library provides two implementations: a standard scalar version and a multi-period vector version for calculating multiple SMAs simultaneously.
|
||||
The library provides two implementations: a standard scalar version and a high-performance Span-based static version.
|
||||
|
||||
### Single SMA (`Sma`)
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@ This works because when all weights decrement by 1 (as the window slides), it's
|
||||
|
||||
## C# Implementation
|
||||
|
||||
The library provides two implementations: a standard scalar version and a multi-period vector version for calculating multiple WMAs simultaneously.
|
||||
The library provides two implementations: a standard scalar version and a high-performance Span-based static version.
|
||||
|
||||
### Single WMA (`Wma`)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user